feat: add DeepChat deeplink support (#4668)
This commit is contained in:
@@ -22,6 +22,9 @@ var Chats = []map[string]string{
|
|||||||
{
|
{
|
||||||
"CC Switch": "ccswitch",
|
"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\"}}}",
|
"Lobe Chat 官方示例": "https://chat-preview.lobehub.com/?settings={\"keyVaults\":{\"openai\":{\"apiKey\":\"{key}\",\"baseURL\":\"{address}/v1\"}}}",
|
||||||
},
|
},
|
||||||
|
|||||||
+5
-1
@@ -251,7 +251,11 @@ const SiderBar = ({ onNavigate = () => {} }) => {
|
|||||||
for (let key in chats[i]) {
|
for (let key in chats[i]) {
|
||||||
let link = chats[i][key];
|
let link = chats[i][key];
|
||||||
if (typeof link !== 'string') continue; // 确保链接是字符串
|
if (typeof link !== 'string') continue; // 确保链接是字符串
|
||||||
if (link.startsWith('fluent') || link.startsWith('ccswitch')) {
|
if (
|
||||||
|
link.startsWith('fluent') ||
|
||||||
|
link.startsWith('ccswitch') ||
|
||||||
|
link.startsWith('deepchat')
|
||||||
|
) {
|
||||||
shouldSkip = true;
|
shouldSkip = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -251,6 +251,16 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
|
|||||||
encodeToBase64(JSON.stringify(aionuiConfig)),
|
encodeToBase64(JSON.stringify(aionuiConfig)),
|
||||||
);
|
);
|
||||||
url = url.replaceAll('{aionuiConfig}', encodedConfig);
|
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 {
|
} else {
|
||||||
let encodedServerAddress = encodeURIComponent(serverAddress);
|
let encodedServerAddress = encodeURIComponent(serverAddress);
|
||||||
url = url.replaceAll('{address}', encodedServerAddress);
|
url = url.replaceAll('{address}', encodedServerAddress);
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export default function SettingsChats(props) {
|
|||||||
{ name: 'AionUI', url: 'aionui://provider/add?v=1&data={aionuiConfig}' },
|
{ name: 'AionUI', url: 'aionui://provider/add?v=1&data={aionuiConfig}' },
|
||||||
{ name: '流畅阅读', url: 'fluentread' },
|
{ name: '流畅阅读', url: 'fluentread' },
|
||||||
{ name: 'CC Switch', url: 'ccswitch' },
|
{ 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: '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: '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}' },
|
{ name: 'AMA 问天', url: 'ama://set-api-key?server={address}&key={key}' },
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useCallback } from 'react'
|
import { useMemo, useCallback, useRef, useState } from 'react'
|
||||||
import { Link, useLocation } from '@tanstack/react-router'
|
import { Link, useLocation } from '@tanstack/react-router'
|
||||||
import { ExternalLink, Loader2, ChevronRight } from 'lucide-react'
|
import { ExternalLink, Loader2, ChevronRight } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import {
|
import {
|
||||||
@@ -23,31 +22,30 @@ import {
|
|||||||
SidebarMenuSubItem,
|
SidebarMenuSubItem,
|
||||||
useSidebar,
|
useSidebar,
|
||||||
} from '@/components/ui/sidebar'
|
} from '@/components/ui/sidebar'
|
||||||
import { useActiveChatKey } from '@/features/chat/hooks/use-active-chat-key'
|
import { fetchActiveChatKey } from '@/features/chat/hooks/use-active-chat-key'
|
||||||
import { useChatPresets } from '@/features/chat/hooks/use-chat-presets'
|
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 { normalizeHref } from '../lib/url-utils'
|
||||||
import type { NavChatPresets } from '../types'
|
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
|
* Sub-menu item for a single chat preset
|
||||||
*/
|
*/
|
||||||
function ChatMenuItem({
|
function ChatMenuItem({
|
||||||
preset,
|
preset,
|
||||||
active,
|
active,
|
||||||
|
loading,
|
||||||
onOpen,
|
onOpen,
|
||||||
onNavigate,
|
onNavigate,
|
||||||
}: {
|
}: {
|
||||||
preset: ChatPreset
|
preset: ChatPreset
|
||||||
active: boolean
|
active: boolean
|
||||||
onOpen: (preset: ChatPreset) => void
|
loading: boolean
|
||||||
|
onOpen: (preset: ChatPreset) => void | Promise<void>
|
||||||
onNavigate: () => void
|
onNavigate: () => void
|
||||||
}) {
|
}) {
|
||||||
if (preset.type === 'web') {
|
if (preset.type === 'web') {
|
||||||
@@ -72,12 +70,19 @@ function ChatMenuItem({
|
|||||||
return (
|
return (
|
||||||
<SidebarMenuSubItem>
|
<SidebarMenuSubItem>
|
||||||
<SidebarMenuSubButton
|
<SidebarMenuSubButton
|
||||||
onClick={() => onOpen(preset)}
|
onClick={() => {
|
||||||
|
if (!loading) void onOpen(preset)
|
||||||
|
}}
|
||||||
|
aria-disabled={loading ? 'true' : undefined}
|
||||||
isActive={false}
|
isActive={false}
|
||||||
className='justify-between'
|
className='justify-between'
|
||||||
>
|
>
|
||||||
<span>{preset.name}</span>
|
<span>{preset.name}</span>
|
||||||
|
{loading ? (
|
||||||
|
<Loader2 className='h-4 w-4 animate-spin' />
|
||||||
|
) : (
|
||||||
<ExternalLink className='h-4 w-4' />
|
<ExternalLink className='h-4 w-4' />
|
||||||
|
)}
|
||||||
</SidebarMenuSubButton>
|
</SidebarMenuSubButton>
|
||||||
</SidebarMenuSubItem>
|
</SidebarMenuSubItem>
|
||||||
)
|
)
|
||||||
@@ -88,10 +93,12 @@ function ChatMenuItem({
|
|||||||
*/
|
*/
|
||||||
function DropdownPresetItem({
|
function DropdownPresetItem({
|
||||||
preset,
|
preset,
|
||||||
|
loading,
|
||||||
onOpen,
|
onOpen,
|
||||||
}: {
|
}: {
|
||||||
preset: ChatPreset
|
preset: ChatPreset
|
||||||
onOpen: (preset: ChatPreset) => void
|
loading: boolean
|
||||||
|
onOpen: (preset: ChatPreset) => void | Promise<void>
|
||||||
}) {
|
}) {
|
||||||
if (preset.type === 'web') {
|
if (preset.type === 'web') {
|
||||||
return (
|
return (
|
||||||
@@ -104,9 +111,18 @@ function DropdownPresetItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem onClick={() => onOpen(preset)}>
|
<DropdownMenuItem
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() => {
|
||||||
|
if (!loading) void onOpen(preset)
|
||||||
|
}}
|
||||||
|
>
|
||||||
{preset.name}
|
{preset.name}
|
||||||
|
{loading ? (
|
||||||
|
<Loader2 className='ml-auto h-4 w-4 animate-spin opacity-70' />
|
||||||
|
) : (
|
||||||
<ExternalLink className='ml-auto h-4 w-4 opacity-70' />
|
<ExternalLink className='ml-auto h-4 w-4 opacity-70' />
|
||||||
|
)}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -119,44 +135,44 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
|
|||||||
const { chatPresets, serverAddress } = useChatPresets()
|
const { chatPresets, serverAddress } = useChatPresets()
|
||||||
const { state, isMobile, setOpenMobile } = useSidebar()
|
const { state, isMobile, setOpenMobile } = useSidebar()
|
||||||
const href = useLocation({ select: (location) => location.href })
|
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(
|
const visiblePresets = useMemo(
|
||||||
() => chatPresets.filter((preset) => preset.type !== 'fluent'),
|
() => chatPresets.filter((preset) => preset.type !== 'fluent'),
|
||||||
[chatPresets]
|
[chatPresets]
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasKeyDependentPresets = useMemo(
|
|
||||||
() => visiblePresets.some(requiresApiKey),
|
|
||||||
[visiblePresets]
|
|
||||||
)
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: activeKey,
|
|
||||||
isPending: isKeyPending,
|
|
||||||
error: keyError,
|
|
||||||
} = useActiveChatKey(hasKeyDependentPresets)
|
|
||||||
|
|
||||||
const handleOpenExternal = useCallback(
|
const handleOpenExternal = useCallback(
|
||||||
(preset: ChatPreset) => {
|
async (preset: ChatPreset) => {
|
||||||
if (preset.type === 'web') return
|
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.'))
|
toast.info(t('Preparing your chat link, please try again in a moment.'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsKey && !activeKey) {
|
if (needsKey) {
|
||||||
|
loadingPresetIdRef.current = preset.id
|
||||||
|
setLoadingPresetId(preset.id)
|
||||||
|
try {
|
||||||
|
activeKey = await fetchActiveChatKey()
|
||||||
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
keyError instanceof Error
|
error instanceof Error
|
||||||
? keyError.message
|
? error.message
|
||||||
: t(
|
: t(
|
||||||
'Unable to prepare chat link. Please ensure you have an enabled API key.'
|
'Unable to prepare chat link. Please ensure you have an enabled API key.'
|
||||||
)
|
)
|
||||||
toast.error(message)
|
toast.error(message)
|
||||||
return
|
return
|
||||||
|
} finally {
|
||||||
|
loadingPresetIdRef.current = null
|
||||||
|
setLoadingPresetId(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = resolveChatUrl({
|
const url = resolveChatUrl({
|
||||||
@@ -175,7 +191,7 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
|
|||||||
window.open(url, '_blank', 'noopener')
|
window.open(url, '_blank', 'noopener')
|
||||||
setOpenMobile(false)
|
setOpenMobile(false)
|
||||||
},
|
},
|
||||||
[activeKey, isKeyPending, keyError, serverAddress, setOpenMobile, t]
|
[serverAddress, setOpenMobile, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
const normalizedHref = normalizeHref(href)
|
const normalizedHref = normalizeHref(href)
|
||||||
@@ -202,16 +218,10 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
|
|||||||
<DropdownPresetItem
|
<DropdownPresetItem
|
||||||
key={preset.id}
|
key={preset.id}
|
||||||
preset={preset}
|
preset={preset}
|
||||||
|
loading={loadingPresetId === preset.id}
|
||||||
onOpen={handleOpenExternal}
|
onOpen={handleOpenExternal}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{hasKeyDependentPresets && <DropdownMenuSeparator />}
|
|
||||||
{hasKeyDependentPresets && isKeyPending && (
|
|
||||||
<DropdownMenuItem disabled>
|
|
||||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
|
||||||
{loadingMessage}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -240,18 +250,11 @@ export function ChatPresetsItem({ item }: { item: NavChatPresets }) {
|
|||||||
key={preset.id}
|
key={preset.id}
|
||||||
preset={preset}
|
preset={preset}
|
||||||
active={normalizedHref === `/chat/${preset.id}`}
|
active={normalizedHref === `/chat/${preset.id}`}
|
||||||
|
loading={loadingPresetId === preset.id}
|
||||||
onOpen={handleOpenExternal}
|
onOpen={handleOpenExternal}
|
||||||
onNavigate={() => setOpenMobile(false)}
|
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>
|
</SidebarMenuSub>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|||||||
+27
-19
@@ -1,30 +1,38 @@
|
|||||||
import { useQuery } from '@tanstack/react-query'
|
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 { 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
|
* Get the currently active API key for chat links
|
||||||
*/
|
*/
|
||||||
export function useActiveChatKey(enabled: boolean) {
|
export function useActiveChatKey(enabled: boolean) {
|
||||||
|
const userId = useAuthStore((state) => state.auth.user?.id)
|
||||||
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['chat-active-key'],
|
queryKey: ['chat-active-key', userId],
|
||||||
queryFn: async () => {
|
queryFn: fetchActiveChatKey,
|
||||||
const result = await getApiKeys({ p: 1, size: 50 })
|
enabled: enabled && Boolean(userId),
|
||||||
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,
|
|
||||||
staleTime: 5 * 60 * 1000,
|
staleTime: 5 * 60 * 1000,
|
||||||
gcTime: 10 * 60 * 1000,
|
gcTime: 10 * 60 * 1000,
|
||||||
})
|
})
|
||||||
|
|||||||
+19
@@ -66,6 +66,15 @@ export function detectChatLinkType(url: string): ChatLinkType {
|
|||||||
return 'custom-protocol'
|
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[] {
|
export function parseChatConfig(raw: RawChatConfig): ChatPreset[] {
|
||||||
let parsed: unknown = raw
|
let parsed: unknown = raw
|
||||||
|
|
||||||
@@ -146,6 +155,16 @@ export function resolveChatUrl({
|
|||||||
return replaceToken(url, '{aionuiConfig}', encoded)
|
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) {
|
if (safeServerAddress) {
|
||||||
const encodedAddress = encodeURIComponent(safeServerAddress)
|
const encodedAddress = encodeURIComponent(safeServerAddress)
|
||||||
url = replaceToken(url, '{address}', encodedAddress)
|
url = replaceToken(url, '{address}', encodedAddress)
|
||||||
|
|||||||
+7
-28
@@ -1,14 +1,15 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { Link, createFileRoute, redirect } from '@tanstack/react-router'
|
import { Link, createFileRoute, redirect } from '@tanstack/react-router'
|
||||||
import { Loader2, MessageCircleWarning } from 'lucide-react'
|
import { Loader2, MessageCircleWarning } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
import { Button } from '@/components/ui/button'
|
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 { useChatPresets } from '@/features/chat/hooks/use-chat-presets'
|
||||||
import { resolveChatUrl } from '@/features/chat/lib/chat-links'
|
import {
|
||||||
import { getApiKeys } from '@/features/keys/api'
|
chatLinkRequiresApiKey,
|
||||||
import { API_KEY_STATUS } from '@/features/keys/constants'
|
resolveChatUrl,
|
||||||
|
} from '@/features/chat/lib/chat-links'
|
||||||
|
|
||||||
export const Route = createFileRoute('/_authenticated/chat/$chatId')({
|
export const Route = createFileRoute('/_authenticated/chat/$chatId')({
|
||||||
loader: async ({ params }) => {
|
loader: async ({ params }) => {
|
||||||
@@ -33,8 +34,7 @@ function ChatRouteComponent() {
|
|||||||
|
|
||||||
const requiresActiveKey = useMemo(() => {
|
const requiresActiveKey = useMemo(() => {
|
||||||
if (!preset || !isWebLink) return false
|
if (!preset || !isWebLink) return false
|
||||||
const url = preset.url ?? ''
|
return chatLinkRequiresApiKey(preset.url ?? '')
|
||||||
return url.includes('{key}') || url.includes('{cherryConfig}')
|
|
||||||
}, [isWebLink, preset])
|
}, [isWebLink, preset])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -42,28 +42,7 @@ function ChatRouteComponent() {
|
|||||||
isPending,
|
isPending,
|
||||||
isError,
|
isError,
|
||||||
error,
|
error,
|
||||||
} = useQuery({
|
} = useActiveChatKey(Boolean(preset && requiresActiveKey))
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
const iframeSrc = useMemo(() => {
|
const iframeSrc = useMemo(() => {
|
||||||
if (!preset || !isWebLink) return ''
|
if (!preset || !isWebLink) return ''
|
||||||
|
|||||||
+11
-18
@@ -1,13 +1,11 @@
|
|||||||
import { useEffect, useMemo } from 'react'
|
import { useEffect, useMemo } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
|
||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||||
import { Loader2 } from 'lucide-react'
|
import { Loader2 } from 'lucide-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { useActiveChatKey } from '@/features/chat/hooks/use-active-chat-key'
|
||||||
import { useChatPresets } from '@/features/chat/hooks/use-chat-presets'
|
import { useChatPresets } from '@/features/chat/hooks/use-chat-presets'
|
||||||
import { resolveChatUrl } from '@/features/chat/lib/chat-links'
|
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')({
|
export const Route = createFileRoute('/_authenticated/chat2link')({
|
||||||
component: Chat2LinkPage,
|
component: Chat2LinkPage,
|
||||||
@@ -23,19 +21,9 @@ function Chat2LinkPage() {
|
|||||||
[chatPresets]
|
[chatPresets]
|
||||||
)
|
)
|
||||||
|
|
||||||
const { data: activeKey } = useQuery({
|
const { data: activeKey, error: keyError } = useActiveChatKey(
|
||||||
queryKey: ['chat2link-active-key'],
|
Boolean(firstWebPreset)
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!firstWebPreset) {
|
if (!firstWebPreset) {
|
||||||
@@ -45,10 +33,14 @@ function Chat2LinkPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeKey === undefined) return
|
if (activeKey === undefined && !keyError) return
|
||||||
|
|
||||||
if (!activeKey) {
|
if (keyError || !activeKey) {
|
||||||
toast.error(t('No enabled tokens available'))
|
const message =
|
||||||
|
keyError instanceof Error
|
||||||
|
? keyError.message
|
||||||
|
: t('No enabled tokens available')
|
||||||
|
toast.error(message)
|
||||||
navigate({ to: '/keys' })
|
navigate({ to: '/keys' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -65,6 +57,7 @@ function Chat2LinkPage() {
|
|||||||
}, [
|
}, [
|
||||||
firstWebPreset,
|
firstWebPreset,
|
||||||
activeKey,
|
activeKey,
|
||||||
|
keyError,
|
||||||
serverAddress,
|
serverAddress,
|
||||||
chatPresets.length,
|
chatPresets.length,
|
||||||
navigate,
|
navigate,
|
||||||
|
|||||||
Reference in New Issue
Block a user