Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
560ba57c
authored
May 08, 2026
by
yyhhyyyyyy
Committed by
GitHub
May 08, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add DeepChat deeplink support (#4668)
parent
948780e3
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
144 additions
and
124 deletions
+144
-124
setting/chat.go
+3
-0
web/classic/src/components/layout/SiderBar.jsx
+5
-1
web/classic/src/hooks/tokens/useTokensData.jsx
+10
-0
web/classic/src/pages/Setting/Chat/SettingsChats.jsx
+1
-0
web/default/src/components/layout/components/chat-presets-item.tsx
+60
-57
web/default/src/features/chat/hooks/use-active-chat-key.ts
+27
-19
web/default/src/features/chat/lib/chat-links.ts
+19
-0
web/default/src/routes/_authenticated/chat/$chatId.tsx
+7
-28
web/default/src/routes/_authenticated/chat2link.tsx
+12
-19
No files found.
setting/chat.go
View file @
560ba57c
...
...
@@ -23,6 +23,9 @@ var Chats = []map[string]string{
"CC Switch"
:
"ccswitch"
,
},
{
"DeepChat"
:
"deepchat://provider/install?v=1&data={deepchatConfig}"
,
},
{
"Lobe Chat 官方示例"
:
"https://chat-preview.lobehub.com/?settings={
\"
keyVaults
\"
:{
\"
openai
\"
:{
\"
apiKey
\"
:
\"
{key}
\"
,
\"
baseURL
\"
:
\"
{address}/v1
\"
}}}"
,
},
{
...
...
web/classic/src/components/layout/SiderBar.jsx
View file @
560ba57c
...
...
@@ -251,7 +251,11 @@ const SiderBar = ({ onNavigate = () => {} }) => {
for
(
let
key
in
chats
[
i
])
{
let
link
=
chats
[
i
][
key
];
if
(
typeof
link
!==
'string'
)
continue
;
// 确保链接是字符串
if
(
link
.
startsWith
(
'fluent'
)
||
link
.
startsWith
(
'ccswitch'
))
{
if
(
link
.
startsWith
(
'fluent'
)
||
link
.
startsWith
(
'ccswitch'
)
||
link
.
startsWith
(
'deepchat'
)
)
{
shouldSkip
=
true
;
break
;
}
...
...
web/classic/src/hooks/tokens/useTokensData.jsx
View file @
560ba57c
...
...
@@ -251,6 +251,16 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
encodeToBase64
(
JSON
.
stringify
(
aionuiConfig
)),
);
url
=
url
.
replaceAll
(
'{aionuiConfig}'
,
encodedConfig
);
}
else
if
(
url
.
includes
(
'{deepchatConfig}'
)
===
true
)
{
let
deepchatConfig
=
{
id
:
'new-api'
,
baseUrl
:
serverAddress
,
apiKey
:
`sk-
${
fullKey
}
`
,
};
let
encodedConfig
=
encodeURIComponent
(
encodeToBase64
(
JSON
.
stringify
(
deepchatConfig
)),
);
url
=
url
.
replaceAll
(
'{deepchatConfig}'
,
encodedConfig
);
}
else
{
let
encodedServerAddress
=
encodeURIComponent
(
serverAddress
);
url
=
url
.
replaceAll
(
'{address}'
,
encodedServerAddress
);
...
...
web/classic/src/pages/Setting/Chat/SettingsChats.jsx
View file @
560ba57c
...
...
@@ -71,6 +71,7 @@ export default function SettingsChats(props) {
{
name
:
'AionUI'
,
url
:
'aionui://provider/add?v=1&data={aionuiConfig}'
},
{
name
:
'流畅阅读'
,
url
:
'fluentread'
},
{
name
:
'CC Switch'
,
url
:
'ccswitch'
},
{
name
:
'DeepChat'
,
url
:
'deepchat://provider/install?v=1&data={deepchatConfig}'
},
{
name
:
'Lobe Chat'
,
url
:
'https://chat-preview.lobehub.com/?settings={"keyVaults":{"openai":{"apiKey":"{key}","baseURL":"{address}/v1"}}}'
},
{
name
:
'AI as Workspace'
,
url
:
'https://aiaw.app/set-provider?provider={"type":"openai","settings":{"apiKey":"{key}","baseURL":"{address}/v1","compatibility":"strict"}}'
},
{
name
:
'AMA 问天'
,
url
:
'ama://set-api-key?server={address}&key={key}'
},
...
...
web/default/src/components/layout/components/chat-presets-item.tsx
View file @
560ba57c
import
{
useMemo
,
useCallback
}
from
'react'
import
{
useMemo
,
useCallback
,
useRef
,
useState
}
from
'react'
import
{
Link
,
useLocation
}
from
'@tanstack/react-router'
import
{
ExternalLink
,
Loader2
,
ChevronRight
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
...
...
@@ -12,7 +12,6 @@ import {
DropdownMenu
,
DropdownMenuContent
,
DropdownMenuItem
,
DropdownMenuSeparator
,
DropdownMenuTrigger
,
}
from
'@/components/ui/dropdown-menu'
import
{
...
...
@@ -23,31 +22,30 @@ import {
SidebarMenuSubItem
,
useSidebar
,
}
from
'@/components/ui/sidebar'
import
{
use
ActiveChatKey
}
from
'@/features/chat/hooks/use-active-chat-key'
import
{
fetch
ActiveChatKey
}
from
'@/features/chat/hooks/use-active-chat-key'
import
{
useChatPresets
}
from
'@/features/chat/hooks/use-chat-presets'
import
{
resolveChatUrl
,
type
ChatPreset
}
from
'@/features/chat/lib/chat-links'
import
{
chatLinkRequiresApiKey
,
resolveChatUrl
,
type
ChatPreset
,
}
from
'@/features/chat/lib/chat-links'
import
{
normalizeHref
}
from
'../lib/url-utils'
import
type
{
NavChatPresets
}
from
'../types'
/**
* Check if a preset requires an API key
*/
function
requiresApiKey
(
preset
:
ChatPreset
):
boolean
{
return
preset
.
url
.
includes
(
'{key}'
)
||
preset
.
url
.
includes
(
'{cherryConfig}'
)
}
/**
* Sub-menu item for a single chat preset
*/
function
ChatMenuItem
({
preset
,
active
,
loading
,
onOpen
,
onNavigate
,
}:
{
preset
:
ChatPreset
active
:
boolean
onOpen
:
(
preset
:
ChatPreset
)
=>
void
loading
:
boolean
onOpen
:
(
preset
:
ChatPreset
)
=>
void
|
Promise
<
void
>
onNavigate
:
()
=>
void
})
{
if
(
preset
.
type
===
'web'
)
{
...
...
@@ -72,12 +70,19 @@ function ChatMenuItem({
return
(
<
SidebarMenuSubItem
>
<
SidebarMenuSubButton
onClick=
{
()
=>
onOpen
(
preset
)
}
onClick=
{
()
=>
{
if
(
!
loading
)
void
onOpen
(
preset
)
}
}
aria
-
disabled=
{
loading
?
'true'
:
undefined
}
isActive=
{
false
}
className=
'justify-between'
>
<
span
>
{
preset
.
name
}
</
span
>
<
ExternalLink
className=
'h-4 w-4'
/>
{
loading
?
(
<
Loader2
className=
'h-4 w-4 animate-spin'
/>
)
:
(
<
ExternalLink
className=
'h-4 w-4'
/>
)
}
</
SidebarMenuSubButton
>
</
SidebarMenuSubItem
>
)
...
...
@@ -88,10 +93,12 @@ function ChatMenuItem({
*/
function
DropdownPresetItem
({
preset
,
loading
,
onOpen
,
}:
{
preset
:
ChatPreset
onOpen
:
(
preset
:
ChatPreset
)
=>
void
loading
:
boolean
onOpen
:
(
preset
:
ChatPreset
)
=>
void
|
Promise
<
void
>
})
{
if
(
preset
.
type
===
'web'
)
{
return
(
...
...
@@ -104,9 +111,18 @@ function DropdownPresetItem({
}
return
(
<
DropdownMenuItem
onClick=
{
()
=>
onOpen
(
preset
)
}
>
<
DropdownMenuItem
disabled=
{
loading
}
onClick=
{
()
=>
{
if
(
!
loading
)
void
onOpen
(
preset
)
}
}
>
{
preset
.
name
}
<
ExternalLink
className=
'ml-auto h-4 w-4 opacity-70'
/>
{
loading
?
(
<
Loader2
className=
'ml-auto h-4 w-4 animate-spin opacity-70'
/>
)
:
(
<
ExternalLink
className=
'ml-auto h-4 w-4 opacity-70'
/>
)
}
</
DropdownMenuItem
>
)
}
...
...
@@ -119,44 +135,44 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
const
{
chatPresets
,
serverAddress
}
=
useChatPresets
()
const
{
state
,
isMobile
,
setOpenMobile
}
=
useSidebar
()
const
href
=
useLocation
({
select
:
(
location
)
=>
location
.
href
})
const
loadingMessage
=
t
(
'Preparing chat keys…'
)
const
[
loadingPresetId
,
setLoadingPresetId
]
=
useState
<
string
|
null
>
(
null
)
const
loadingPresetIdRef
=
useRef
<
string
|
null
>
(
null
)
const
visiblePresets
=
useMemo
(
()
=>
chatPresets
.
filter
((
preset
)
=>
preset
.
type
!==
'fluent'
),
[
chatPresets
]
)
const
hasKeyDependentPresets
=
useMemo
(
()
=>
visiblePresets
.
some
(
requiresApiKey
),
[
visiblePresets
]
)
const
{
data
:
activeKey
,
isPending
:
isKeyPending
,
error
:
keyError
,
}
=
useActiveChatKey
(
hasKeyDependentPresets
)
const
handleOpenExternal
=
useCallback
(
(
preset
:
ChatPreset
)
=>
{
async
(
preset
:
ChatPreset
)
=>
{
if
(
preset
.
type
===
'web'
)
return
const
needsKey
=
requiresApiKey
(
preset
)
const
needsKey
=
chatLinkRequiresApiKey
(
preset
.
url
)
let
activeKey
:
string
|
undefined
if
(
needsKey
&&
isKeyPending
)
{
if
(
needsKey
&&
loadingPresetIdRef
.
current
)
{
toast
.
info
(
t
(
'Preparing your chat link, please try again in a moment.'
))
return
}
if
(
needsKey
&&
!
activeKey
)
{
const
message
=
keyError
instanceof
Error
?
keyError
.
message
:
t
(
'Unable to prepare chat link. Please ensure you have an enabled API key.'
)
toast
.
error
(
message
)
return
if
(
needsKey
)
{
loadingPresetIdRef
.
current
=
preset
.
id
setLoadingPresetId
(
preset
.
id
)
try
{
activeKey
=
await
fetchActiveChatKey
()
}
catch
(
error
)
{
const
message
=
error
instanceof
Error
?
error
.
message
:
t
(
'Unable to prepare chat link. Please ensure you have an enabled API key.'
)
toast
.
error
(
message
)
return
}
finally
{
loadingPresetIdRef
.
current
=
null
setLoadingPresetId
(
null
)
}
}
const
url
=
resolveChatUrl
({
...
...
@@ -175,7 +191,7 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
window
.
open
(
url
,
'_blank'
,
'noopener'
)
setOpenMobile
(
false
)
},
[
activeKey
,
isKeyPending
,
keyError
,
serverAddress
,
setOpenMobile
,
t
]
[
serverAddress
,
setOpenMobile
,
t
]
)
const
normalizedHref
=
normalizeHref
(
href
)
...
...
@@ -202,16 +218,10 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
<
DropdownPresetItem
key=
{
preset
.
id
}
preset=
{
preset
}
loading=
{
loadingPresetId
===
preset
.
id
}
onOpen=
{
handleOpenExternal
}
/>
))
}
{
hasKeyDependentPresets
&&
<
DropdownMenuSeparator
/>
}
{
hasKeyDependentPresets
&&
isKeyPending
&&
(
<
DropdownMenuItem
disabled
>
<
Loader2
className=
'mr-2 h-4 w-4 animate-spin'
/>
{
loadingMessage
}
</
DropdownMenuItem
>
)
}
</
DropdownMenuContent
>
</
DropdownMenu
>
</
SidebarMenuItem
>
...
...
@@ -240,18 +250,11 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
key=
{
preset
.
id
}
preset=
{
preset
}
active=
{
normalizedHref
===
`/chat/${preset.id}`
}
loading=
{
loadingPresetId
===
preset
.
id
}
onOpen=
{
handleOpenExternal
}
onNavigate=
{
()
=>
setOpenMobile
(
false
)
}
/>
))
}
{
hasKeyDependentPresets
&&
isKeyPending
&&
(
<
SidebarMenuSubItem
>
<
SidebarMenuSubButton
aria
-
disabled=
'true'
tabIndex=
{
-
1
}
>
<
Loader2
className=
'mr-2 h-4 w-4 animate-spin'
/>
{
loadingMessage
}
</
SidebarMenuSubButton
>
</
SidebarMenuSubItem
>
)
}
</
SidebarMenuSub
>
</
CollapsibleContent
>
</
Collapsible
>
...
...
web/default/src/features/chat/hooks/use-active-chat-key.ts
View file @
560ba57c
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
getApiKeys
}
from
'@/features/keys/api'
import
{
fetchTokenKey
,
getApiKeys
}
from
'@/features/keys/api'
import
{
API_KEY_STATUS
}
from
'@/features/keys/constants'
import
{
useAuthStore
}
from
'@/stores/auth-store'
export
async
function
fetchActiveChatKey
()
{
const
result
=
await
getApiKeys
({
p
:
1
,
size
:
50
})
if
(
!
result
.
success
)
{
throw
new
Error
(
result
.
message
||
'Failed to load API keys'
)
}
const
items
=
result
.
data
?.
items
??
[]
const
active
=
items
.
find
((
item
)
=>
item
.
status
===
API_KEY_STATUS
.
ENABLED
)
if
(
!
active
)
{
throw
new
Error
(
'No enabled API keys found. Create or enable one first.'
)
}
const
keyResult
=
await
fetchTokenKey
(
active
.
id
)
if
(
!
keyResult
.
success
||
!
keyResult
.
data
?.
key
)
{
throw
new
Error
(
keyResult
.
message
||
'Failed to load API key'
)
}
return
`sk-
${
keyResult
.
data
.
key
}
`
}
/**
* Get the currently active API key for chat links
*/
export
function
useActiveChatKey
(
enabled
:
boolean
)
{
const
userId
=
useAuthStore
((
state
)
=>
state
.
auth
.
user
?.
id
)
return
useQuery
({
queryKey
:
[
'chat-active-key'
],
queryFn
:
async
()
=>
{
const
result
=
await
getApiKeys
({
p
:
1
,
size
:
50
})
if
(
!
result
.
success
)
{
throw
new
Error
(
result
.
message
||
'Failed to load API keys'
)
}
const
items
=
result
.
data
?.
items
??
[]
const
active
=
items
.
find
(
(
item
)
=>
item
.
status
===
API_KEY_STATUS
.
ENABLED
)
if
(
!
active
)
{
throw
new
Error
(
'No enabled API keys found. Create or enable one first.'
)
}
return
active
.
key
},
enabled
,
queryKey
:
[
'chat-active-key'
,
userId
],
queryFn
:
fetchActiveChatKey
,
enabled
:
enabled
&&
Boolean
(
userId
),
staleTime
:
5
*
60
*
1000
,
gcTime
:
10
*
60
*
1000
,
})
...
...
web/default/src/features/chat/lib/chat-links.ts
View file @
560ba57c
...
...
@@ -66,6 +66,15 @@ export function detectChatLinkType(url: string): ChatLinkType {
return
'custom-protocol'
}
export
function
chatLinkRequiresApiKey
(
url
:
string
):
boolean
{
return
(
url
.
includes
(
'{key}'
)
||
url
.
includes
(
'{cherryConfig}'
)
||
url
.
includes
(
'{aionuiConfig}'
)
||
url
.
includes
(
'{deepchatConfig}'
)
)
}
export
function
parseChatConfig
(
raw
:
RawChatConfig
):
ChatPreset
[]
{
let
parsed
:
unknown
=
raw
...
...
@@ -146,6 +155,16 @@ export function resolveChatUrl({
return
replaceToken
(
url
,
'{aionuiConfig}'
,
encoded
)
}
if
(
url
.
includes
(
'{deepchatConfig}'
))
{
const
payload
=
{
id
:
'new-api'
,
baseUrl
:
safeServerAddress
,
apiKey
:
safeApiKey
,
}
const
encoded
=
encodeURIComponent
(
toBase64
(
JSON
.
stringify
(
payload
)))
return
replaceToken
(
url
,
'{deepchatConfig}'
,
encoded
)
}
if
(
safeServerAddress
)
{
const
encodedAddress
=
encodeURIComponent
(
safeServerAddress
)
url
=
replaceToken
(
url
,
'{address}'
,
encodedAddress
)
...
...
web/default/src/routes/_authenticated/chat/$chatId.tsx
View file @
560ba57c
import
{
useMemo
}
from
'react'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
Link
,
createFileRoute
,
redirect
}
from
'@tanstack/react-router'
import
{
Loader2
,
MessageCircleWarning
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
Alert
,
AlertDescription
,
AlertTitle
}
from
'@/components/ui/alert'
import
{
Button
}
from
'@/components/ui/button'
import
{
useActiveChatKey
}
from
'@/features/chat/hooks/use-active-chat-key'
import
{
useChatPresets
}
from
'@/features/chat/hooks/use-chat-presets'
import
{
resolveChatUrl
}
from
'@/features/chat/lib/chat-links'
import
{
getApiKeys
}
from
'@/features/keys/api'
import
{
API_KEY_STATUS
}
from
'@/features/keys/constants'
import
{
chatLinkRequiresApiKey
,
resolveChatUrl
,
}
from
'@/features/chat/lib/chat-links'
export
const
Route
=
createFileRoute
(
'/_authenticated/chat/$chatId'
)({
loader
:
async
({
params
})
=>
{
...
...
@@ -33,8 +34,7 @@ function ChatRouteComponent() {
const
requiresActiveKey
=
useMemo
(()
=>
{
if
(
!
preset
||
!
isWebLink
)
return
false
const
url
=
preset
.
url
??
''
return
url
.
includes
(
'{key}'
)
||
url
.
includes
(
'{cherryConfig}'
)
return
chatLinkRequiresApiKey
(
preset
.
url
??
''
)
},
[
isWebLink
,
preset
])
const
{
...
...
@@ -42,28 +42,7 @@ function ChatRouteComponent() {
isPending
,
isError
,
error
,
}
=
useQuery
({
queryKey
:
[
'chat-active-key'
],
queryFn
:
async
()
=>
{
const
result
=
await
getApiKeys
({
p
:
1
,
size
:
50
})
if
(
!
result
.
success
)
{
throw
new
Error
(
result
.
message
||
'Failed to load API keys'
)
}
const
items
=
result
.
data
?.
items
??
[]
const
active
=
items
.
find
(
(
item
)
=>
item
.
status
===
API_KEY_STATUS
.
ENABLED
)
if
(
!
active
)
{
throw
new
Error
(
'No enabled API key available. Please enable an API key first.'
)
}
return
active
.
key
},
enabled
:
Boolean
(
preset
&&
requiresActiveKey
),
staleTime
:
5
*
60
*
1000
,
gcTime
:
10
*
60
*
1000
,
})
}
=
useActiveChatKey
(
Boolean
(
preset
&&
requiresActiveKey
))
const
iframeSrc
=
useMemo
(()
=>
{
if
(
!
preset
||
!
isWebLink
)
return
''
...
...
web/default/src/routes/_authenticated/chat2link.tsx
View file @
560ba57c
import
{
useEffect
,
useMemo
}
from
'react'
import
{
useQuery
}
from
'@tanstack/react-query'
import
{
createFileRoute
,
useNavigate
}
from
'@tanstack/react-router'
import
{
Loader2
}
from
'lucide-react'
import
{
useTranslation
}
from
'react-i18next'
import
{
toast
}
from
'sonner'
import
{
useActiveChatKey
}
from
'@/features/chat/hooks/use-active-chat-key'
import
{
useChatPresets
}
from
'@/features/chat/hooks/use-chat-presets'
import
{
resolveChatUrl
}
from
'@/features/chat/lib/chat-links'
import
{
getApiKeys
}
from
'@/features/keys/api'
import
{
API_KEY_STATUS
}
from
'@/features/keys/constants'
export
const
Route
=
createFileRoute
(
'/_authenticated/chat2link'
)({
component
:
Chat2LinkPage
,
...
...
@@ -23,19 +21,9 @@ function Chat2LinkPage() {
[
chatPresets
]
)
const
{
data
:
activeKey
}
=
useQuery
({
queryKey
:
[
'chat2link-active-key'
],
queryFn
:
async
()
=>
{
const
result
=
await
getApiKeys
({
p
:
1
,
size
:
50
})
if
(
!
result
.
success
)
throw
new
Error
(
result
.
message
)
const
items
=
result
.
data
?.
items
??
[]
const
active
=
items
.
find
(
(
item
)
=>
item
.
status
===
API_KEY_STATUS
.
ENABLED
)
return
active
?.
key
??
null
},
staleTime
:
5
*
60
*
1000
,
})
const
{
data
:
activeKey
,
error
:
keyError
}
=
useActiveChatKey
(
Boolean
(
firstWebPreset
)
)
useEffect
(()
=>
{
if
(
!
firstWebPreset
)
{
...
...
@@ -45,10 +33,14 @@ function Chat2LinkPage() {
return
}
if
(
activeKey
===
undefined
)
return
if
(
activeKey
===
undefined
&&
!
keyError
)
return
if
(
!
activeKey
)
{
toast
.
error
(
t
(
'No enabled tokens available'
))
if
(
keyError
||
!
activeKey
)
{
const
message
=
keyError
instanceof
Error
?
keyError
.
message
:
t
(
'No enabled tokens available'
)
toast
.
error
(
message
)
navigate
({
to
:
'/keys'
})
return
}
...
...
@@ -65,6 +57,7 @@ function Chat2LinkPage() {
},
[
firstWebPreset
,
activeKey
,
keyError
,
serverAddress
,
chatPresets
.
length
,
navigate
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment