diff --git a/desktop/src/components/ChatArea.tsx b/desktop/src/components/ChatArea.tsx index 152d06a..e62cd91 100644 --- a/desktop/src/components/ChatArea.tsx +++ b/desktop/src/components/ChatArea.tsx @@ -79,7 +79,18 @@ export function ChatArea() { provider: m.provider_id, })); } - return configModels; + if (configModels.length > 0) { + return configModels; + } + // Fallback: provide common models when no backend is connected + return [ + { id: 'glm-4-flash', name: 'GLM-4 Flash', provider: 'zhipu' }, + { id: 'gpt-4o', name: 'GPT-4o', provider: 'openai' }, + { id: 'gpt-4o-mini', name: 'GPT-4o Mini', provider: 'openai' }, + { id: 'deepseek-chat', name: 'DeepSeek V3', provider: 'deepseek' }, + { id: 'qwen-max', name: 'Qwen Max', provider: 'qwen' }, + { id: 'claude-3-5-sonnet', name: 'Claude 3.5 Sonnet', provider: 'anthropic' }, + ]; }, [isLoggedIn, saasModels, configModels]); const [input, setInput] = useState(''); diff --git a/desktop/src/components/ConversationList.tsx b/desktop/src/components/ConversationList.tsx index b6994ac..b958ac6 100644 --- a/desktop/src/components/ConversationList.tsx +++ b/desktop/src/components/ConversationList.tsx @@ -171,7 +171,7 @@ function ConversationItem({ ); } -export function ConversationList() { +export function ConversationList({ searchQuery = '' }: { searchQuery?: string }) { const conversations = useConversationStore((s) => s.conversations); const currentConversationId = useConversationStore((s) => s.currentConversationId); const { switchConversation, deleteConversation } = useChatStore(); @@ -192,13 +192,40 @@ export function ConversationList() { exportConversation(conv.title, conv.messages); }; + // Deduplicate by id (keep most recent) then filter by search query + const uniqueConversations = (() => { + const seen = new Map(); + for (const conv of conversations) { + const existing = seen.get(conv.id); + if (!existing || conv.updatedAt > existing.updatedAt) { + seen.set(conv.id, conv); + } + } + return Array.from(seen.values()).sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); + })(); + + const filtered = searchQuery.trim() + ? uniqueConversations.filter((c) => + c.title.toLowerCase().includes(searchQuery.toLowerCase()) || + c.messages.some((m) => m.content.toLowerCase().includes(searchQuery.toLowerCase())) + ) + : uniqueConversations; + if (conversations.length === 0) { return ; } + if (filtered.length === 0 && searchQuery.trim()) { + return ( +
+ 未找到匹配的对话 +
+ ); + } + return (
- {conversations.map((conv) => ( + {filtered.map((conv) => ( )} {error && ( -

{error}

+

+ {error.includes('Cannot read properties') || error.includes('TypeError') + ? '连接状态获取失败,请重新连接' + : error} +

)} diff --git a/desktop/src/components/Sidebar.tsx b/desktop/src/components/Sidebar.tsx index da78446..09427e2 100644 --- a/desktop/src/components/Sidebar.tsx +++ b/desktop/src/components/Sidebar.tsx @@ -123,7 +123,7 @@ export function Sidebar({ )}
- + )} {activeTab === 'clones' && } diff --git a/desktop/src/lib/secure-storage.ts b/desktop/src/lib/secure-storage.ts index 25f219a..e5e586b 100644 --- a/desktop/src/lib/secure-storage.ts +++ b/desktop/src/lib/secure-storage.ts @@ -216,8 +216,18 @@ async function writeEncryptedLocalStorage(key: string, value: string): Promise(); +const MAX_DECRYPTION_RETRIES = 2; + async function readEncryptedLocalStorage(key: string): Promise { try { + // If this key has already failed too many times, skip immediately + const failCount = decryptionFailures.get(key) ?? 0; + if (failCount >= MAX_DECRYPTION_RETRIES) { + return null; + } + const encryptedKey = ENCRYPTED_PREFIX + key; const encryptedRaw = localStorage.getItem(encryptedKey); @@ -230,12 +240,15 @@ async function readEncryptedLocalStorage(key: string): Promise { const parsed = JSON.parse(encryptedRaw); const salt = base64ToArray(parsed.salt); const cryptoKey = await deriveKey(masterKeyRaw, salt); - return await decrypt( + const result = await decrypt( { iv: parsed.iv, data: parsed.data }, cryptoKey, ); + decryptionFailures.delete(key); + return result; } catch (error) { - console.error('[SecureStorage] v2 decryption failed:', error); + console.warn('[SecureStorage] v2 decryption failed for key:', key); + decryptionFailures.set(key, failCount + 1); // Fall through to try v1 } } @@ -245,9 +258,17 @@ async function readEncryptedLocalStorage(key: string): Promise { try { const cryptoKey = await deriveKey(masterKeyRaw); // uses legacy static salt const encrypted = JSON.parse(encryptedRaw); - return await decrypt(encrypted, cryptoKey); + const result = await decrypt(encrypted, cryptoKey); + decryptionFailures.delete(key); + return result; } catch (error) { - console.error('[SecureStorage] v1 decryption failed:', error); + console.warn('[SecureStorage] v1 decryption failed for key:', key); + decryptionFailures.set(key, failCount + 1); + // Both v2 and v1 failed — clear stale encrypted data to stop retrying + if (failCount + 1 >= MAX_DECRYPTION_RETRIES) { + localStorage.removeItem(encryptedKey); + console.warn('[SecureStorage] Cleared stale encrypted data for key:', key); + } } } } diff --git a/desktop/src/store/connectionStore.ts b/desktop/src/store/connectionStore.ts index 08fa01a..97bef3f 100644 --- a/desktop/src/store/connectionStore.ts +++ b/desktop/src/store/connectionStore.ts @@ -503,7 +503,11 @@ export const useConnectionStore = create((set, get) => { } catch { // conversationStore 可能尚未初始化 } - const modelToUse = preferredModel || fallbackModel.id; + const fallbackId = fallbackModel?.id; + if (!fallbackId) { + throw new Error('可用模型数据格式异常,请刷新页面重试'); + } + const modelToUse = preferredModel || fallbackId; kernelClient.setConfig({ provider: 'custom', @@ -538,7 +542,10 @@ export const useConnectionStore = create((set, get) => { } else { // Non-Tauri (browser) — use SaaS relay gateway client for agent listing + chat const { createSaaSRelayGatewayClient } = await import('../lib/saas-relay-client'); - const fallbackModelId = relayModels[0].id; + const fallbackModelId = relayModels[0]?.id; + if (!fallbackModelId) { + throw new Error('可用模型数据格式异常,请刷新页面重试'); + } const relayClient = createSaaSRelayGatewayClient(session.saasUrl, () => { // 每次调用时读取 conversationStore 的 currentModel,fallback 到第一个可用模型 try {