diff --git a/setting/chat.go b/setting/chat.go index 417ee85d..bb8a9977 100644 --- a/setting/chat.go +++ b/setting/chat.go @@ -22,6 +22,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\"}}}", }, diff --git a/web/classic/src/components/layout/SiderBar.jsx b/web/classic/src/components/layout/SiderBar.jsx index bcbe4123..a4375cf8 100644 --- a/web/classic/src/components/layout/SiderBar.jsx +++ b/web/classic/src/components/layout/SiderBar.jsx @@ -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; } diff --git a/web/classic/src/hooks/tokens/useTokensData.jsx b/web/classic/src/hooks/tokens/useTokensData.jsx index abee82b3..b26fd9fc 100644 --- a/web/classic/src/hooks/tokens/useTokensData.jsx +++ b/web/classic/src/hooks/tokens/useTokensData.jsx @@ -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); diff --git a/web/classic/src/pages/Setting/Chat/SettingsChats.jsx b/web/classic/src/pages/Setting/Chat/SettingsChats.jsx index 9744d161..fc1dae4c 100644 --- a/web/classic/src/pages/Setting/Chat/SettingsChats.jsx +++ b/web/classic/src/pages/Setting/Chat/SettingsChats.jsx @@ -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}' }, diff --git a/web/default/src/components/layout/components/chat-presets-item.tsx b/web/default/src/components/layout/components/chat-presets-item.tsx index 9fd329a7..0c55cc09 100644 --- a/web/default/src/components/layout/components/chat-presets-item.tsx +++ b/web/default/src/components/layout/components/chat-presets-item.tsx @@ -1,4 +1,4 @@ -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 { 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 { 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 onNavigate: () => void }) { if (preset.type === 'web') { @@ -72,12 +70,19 @@ function ChatMenuItem({ return ( onOpen(preset)} + onClick={() => { + if (!loading) void onOpen(preset) + }} + aria-disabled={loading ? 'true' : undefined} isActive={false} className='justify-between' > {preset.name} - + {loading ? ( + + ) : ( + + )} ) @@ -88,10 +93,12 @@ function ChatMenuItem({ */ function DropdownPresetItem({ preset, + loading, onOpen, }: { preset: ChatPreset - onOpen: (preset: ChatPreset) => void + loading: boolean + onOpen: (preset: ChatPreset) => void | Promise }) { if (preset.type === 'web') { return ( @@ -104,9 +111,18 @@ function DropdownPresetItem({ } return ( - onOpen(preset)}> + { + if (!loading) void onOpen(preset) + }} + > {preset.name} - + {loading ? ( + + ) : ( + + )} ) } @@ -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(null) + const loadingPresetIdRef = useRef(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 }) { ))} - {hasKeyDependentPresets && } - {hasKeyDependentPresets && isKeyPending && ( - - - {loadingMessage} - - )} @@ -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 && ( - - - - {loadingMessage} - - - )} diff --git a/web/default/src/features/chat/hooks/use-active-chat-key.ts b/web/default/src/features/chat/hooks/use-active-chat-key.ts index 838d56e9..698371ab 100644 --- a/web/default/src/features/chat/hooks/use-active-chat-key.ts +++ b/web/default/src/features/chat/hooks/use-active-chat-key.ts @@ -1,30 +1,38 @@ 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, }) diff --git a/web/default/src/features/chat/lib/chat-links.ts b/web/default/src/features/chat/lib/chat-links.ts index ef897955..4ca3f143 100644 --- a/web/default/src/features/chat/lib/chat-links.ts +++ b/web/default/src/features/chat/lib/chat-links.ts @@ -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) diff --git a/web/default/src/routes/_authenticated/chat/$chatId.tsx b/web/default/src/routes/_authenticated/chat/$chatId.tsx index 12acb659..fb552580 100644 --- a/web/default/src/routes/_authenticated/chat/$chatId.tsx +++ b/web/default/src/routes/_authenticated/chat/$chatId.tsx @@ -1,14 +1,15 @@ 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 '' diff --git a/web/default/src/routes/_authenticated/chat2link.tsx b/web/default/src/routes/_authenticated/chat2link.tsx index 065f7112..50c74904 100644 --- a/web/default/src/routes/_authenticated/chat2link.tsx +++ b/web/default/src/routes/_authenticated/chat2link.tsx @@ -1,13 +1,11 @@ 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,