# AI Desktop 项目优化优先级表 ## 快速参考 | 优先级 | 类别 | 问题 | 影响 | 难度 | 预计工作量 | |-------|------|------|------|------|----------| | P0-1 | 性能 | Chat.vue 无限重渲染 | 🔴 高 | ⭐⭐ 中 | 2h | | P0-2 | 性能 | localStorage 同步写入 | 🔴 高 | ⭐ 易 | 1h | | P0-3 | 质量 | 重复类型定义 + 错误处理 | 🔴 高 | ⭐⭐⭐ 难 | 3h | | P0-4 | UX | 登录状态提示不及时 | 🟡 中 | ⭐ 易 | 1h | | P0-5 | 错误 | Playwright 进程崩溃 | 🔴 高 | ⭐⭐⭐ 难 | 3h | | P1-1 | 性能 | 搜索结果缓存 | 🟡 中 | ⭐⭐ 中 | 2h | | P1-2 | UX | 工具调用进度反馈 | 🟡 中 | ⭐ 易 | 1.5h | | P1-3 | 功能 | 文章Markdown支持 | 🟡 中 | ⭐ 易 | 1h | | P1-4 | 质量 | 魔法字符串提取 | 🟡 中 | ⭐ 易 | 1.5h | | P1-5 | 错误 | 死循环检测 | 🟡 中 | ⭐⭐ 中 | 2h | | P2-1 | 功能 | 对话导出/导入 | 🟢 低 | ⭐⭐ 中 | 3h | | P2-2 | 功能 | 多会话管理 | 🟢 低 | ⭐⭐⭐ 难 | 5h | | P2-3 | 功能 | 消息搜索 | 🟢 低 | ⭐ 易 | 1h | | P2-4 | 功能 | 语法高亮 | 🟢 低 | ⭐⭐ 中 | 2h | | P2-5 | 质量 | 单元测试 | 🟢 低 | ⭐⭐⭐ 难 | 4h | --- ## 第一阶段:立即修复(1-2天,共11小时) ### 1. P0-2:优化 localStorage 写入 [1小时] **位置**: `src/renderer/src/views/Chat.vue: Line 467-485` **立即行动**: ```typescript // 只保存最近100条消息,压缩工具结果 const messagesToSave = messages.value.slice(-100) const compressed = messagesToSave.map(msg => ({ id: msg.id, role: msg.role, content: msg.content, timestamp: msg.timestamp, toolCalls: msg.toolCalls?.map(tc => ({ name: tc.name, status: tc.status, result: tc.result?.substring?.(0, 100) // 只保存前100字 })) })) localStorage.setItem('chat-messages', JSON.stringify(compressed)) ``` **期望效果**: localStorage写入速度提升3-5倍 --- ### 2. P0-4:改进登录状态提示 [1小时] **位置**: `src/renderer/src/views/Chat.vue, Settings.vue` **立即行动**: ```typescript // Chat.vue onMounted中添加 const isLoggedIn = ref(false) onMounted(async () => { // 检查登录状态 const result = await window.electron.ipcRenderer.invoke('check-platform-login', { platform: 'xiaoheihe' }) isLoggedIn.value = result.isLoggedIn if (!isLoggedIn.value) { ElMessage.warning('请登录小黑盒账号以使用搜索功能') } }) // 定期检查(每5分钟) const checkInterval = setInterval(async () => { const result = await window.electron.ipcRenderer.invoke('check-platform-login', { platform: 'xiaoheihe' }) isLoggedIn.value = result.isLoggedIn }, 300000) ``` **期望效果**: 用户在输入搜索前就知道是否需要登录 --- ### 3. P0-1:防抖scrollToBottom [2小时] **位置**: `src/renderer/src/views/Chat.vue: Line 267-420` **立即行动**: ```typescript // 防抖滚动 let scrollTimer: ReturnType | null = null const debouncedScrollToBottom = () => { if (scrollTimer) clearTimeout(scrollTimer) scrollTimer = setTimeout(() => { scrollToBottom() scrollTimer = null }, 50) } // 替换所有 scrollToBottom() 为 debouncedScrollToBottom() // 修改 onToken 回调 onToken: (token: string) => { currentContent += token const lastMessage = messages.value[messages.value.length - 1] if (lastMessage && lastMessage.role === 'assistant') { lastMessage.content = currentContent debouncedScrollToBottom() // 改这里 } } ``` **期望效果**: 渲染次数减少60-70% --- ### 4. P0-3:统一类型定义和错误处理 [3小时] #### 步骤 1: 创建 `src/shared/types.ts` ```typescript export interface Message { id: string role: 'user' | 'assistant' | 'system' | 'tool' content: string | null timestamp?: Date tool_calls?: ToolCall[] tool_call_id?: string name?: string toolCalls?: ToolCallInfo[] // 兼容旧格式 } export interface ToolCall { id: string type: 'function' function: { name: string arguments: string } } export interface ToolCallInfo { name: string args?: Record result?: any status: 'loading' | 'success' | 'error' } export interface ModelConfig { id: string name: string provider: 'openai' | 'deepseek' model: string apiKey: string baseUrl: string } export interface Settings { activeModelId: string | null modelConfigs: ModelConfig[] } ``` #### 步骤 2: 创建 `src/renderer/src/utils/errors.ts` ```typescript export class AppError extends Error { constructor( message: string, public code: string, public recoverable: boolean = false, public context?: Record ) { super(message) this.name = 'AppError' } } export const createError = { notLoggedIn: (platform: string) => new AppError( `需要登录 ${platform}`, 'NOT_LOGGED_IN', true, { platform } ), networkError: (msg?: string) => new AppError(msg || '网络连接失败', 'NETWORK_ERROR', true), toolError: (toolName: string) => new AppError( `${toolName} 执行失败`, 'TOOL_ERROR', true, { tool: toolName } ) } export const handleError = (error: Error) => { if (error instanceof AppError) { console.error(`[${error.code}] ${error.message}`, error.context) return { message: error.message, recoverable: error.recoverable, code: error.code } } console.error('Unexpected error:', error) return { message: '发生了一个错误', recoverable: false, code: 'UNKNOWN' } } ``` #### 步骤 3: 更新导入 - `Chat.vue`: `import type { Message } from '@/shared/types'` - `aiService.ts`: `import type { Message, ToolCall } from '@/shared/types'` - `tools.ts`: `import type { ToolCall } from '@/shared/types'` **期望效果**: 类型安全提升,类型定义从3个减少到1个 --- ### 5. P0-5:处理Playwright进程崩溃 [3小时] **位置**: `src/main/index.ts: Line 396-406, 352-393` **立即行动**: ```typescript // 添加重试机制 async function fetchArticleContentWithRetry( url: string, maxRetries = 3 ): Promise { for (let attempt = 0; attempt < maxRetries; attempt++) { try { console.log(`[fetchArticle] Attempt ${attempt + 1}/${maxRetries}`) return await fetchArticleContent(url) } catch (error) { if (attempt === maxRetries - 1) throw error // 指数退避 const delay = 1000 * Math.pow(2, attempt) console.log(`[fetchArticle] Retry after ${delay}ms`) await new Promise(resolve => setTimeout(resolve, delay)) } } } // 添加disconnected事件处理 async function getPersistentContext(headless = true): Promise { if (!persistentContext) { persistentContext = await chromium.launchPersistentContext(userDataDir, { headless, viewport: { width: 1280, height: 800 }, userAgent: '...' }) // 监听断开连接事件 persistentContext.once('disconnected', () => { console.log('[Browser] Context disconnected') persistentContext = null }) } return persistentContext } // 使用 fetchArticleContentWithRetry 替换 fetchArticleContent ipcMain.handle('fetch-article', async (_, url: string) => { try { const result = await fetchArticleContentWithRetry(url) return { success: true, ...result } } catch (error) { return { success: false, error: error instanceof Error ? error.message : '抓取文章失败' } } }) ``` **期望效果**: 偶发性浏览器崩溃导致的请求失败减少80% --- ## 第二阶段:改进体验(1-2周,共11小时) ### 6. P1-4:提取魔法字符串 [1.5小时] 创建 `src/renderer/src/constants.ts`: ```typescript export const TIMEOUTS = { DEFER_MESSAGE_LOAD: 0, ENSURE_IME_READY: 100, ENSURE_RENDERER_READY: 200, DEBOUNCE_SAVE: 300, DEBOUNCE_SCROLL: 50, TOOL_EXECUTION_TIMEOUT: 30000 } as const export const LIMITS = { MAX_RECENT_MESSAGES: 50, MAX_MESSAGE_STORAGE: 100, MAX_TOOL_ITERATIONS: 10, MAX_SEARCH_RESULTS_DISPLAY: 10, ARTICLE_PREVIEW_LENGTH: 1000, ARTICLE_RESULT_PREVIEW: 200, SEARCH_MIN_INTERVAL: 3000, SEARCH_MAX_PER_MINUTE: 10 } as const export const TOOL_NAMES = { CHECK_LOGIN: 'check_platform_login', SEARCH: 'search_platform', FETCH_ARTICLE: 'fetch_article' } as const export const PLATFORMS = { XIAOHEIHE: 'xiaoheihe' } as const ``` 在代码中使用: ```typescript // 之前 const recentMessages = parsed.slice(-50) setTimeout(() => { ... }, 300) // 之后 import { LIMITS, TIMEOUTS } from '@/constants' const recentMessages = parsed.slice(-LIMITS.MAX_RECENT_MESSAGES) setTimeout(() => { ... }, TIMEOUTS.DEBOUNCE_SAVE) ``` --- ### 7. P1-1:搜索结果缓存 [2小时] 在 `src/renderer/src/views/ToolsPanel.vue` 中: ```typescript // 添加缓存 const articleCache = new Map() const searchCache = new Map() const handleSearch = async () => { if (!searchQuery.value.trim() || searchLoading.value) return const cacheKey = `search:${searchQuery.value}` if (searchCache.has(cacheKey)) { searchResult.value = searchCache.get(cacheKey) return } try { searchLoading.value = true searchResult.value = null const toolCall = { /* ... */ } const results = await executeToolCalls([toolCall]) const toolResult = results[0] const parsedResult = JSON.parse(toolResult.content) searchResult.value = parsedResult searchCache.set(cacheKey, parsedResult) } finally { searchLoading.value = false } } const handleArticleClick = async (url: string) => { articleUrl.value = url // 先检查缓存 if (articleCache.has(url)) { articleResult.value = articleCache.get(url) return } await handleFetchArticle() } const handleFetchArticle = async () => { if (!articleUrl.value.trim() || fetchLoading.value) return // 检查缓存 if (articleCache.has(articleUrl.value)) { articleResult.value = articleCache.get(articleUrl.value) return } try { fetchLoading.value = true articleResult.value = null const toolCall = { /* ... */ } const results = await executeToolCalls([toolCall]) const toolResult = results[0] const parsedResult = JSON.parse(toolResult.content) articleResult.value = parsedResult articleCache.set(articleUrl.value, parsedResult) } finally { fetchLoading.value = false } } ``` --- ### 8. P1-2:工具调用进度提示 [1.5小时] 修改 `src/renderer/src/views/Chat.vue`: ```typescript // 添加进度跟踪 const toolProgress = ref({ current: 0, total: 0 }) // 在循环前设置总数 while (currentResponse.tool_calls && /* ... */) { toolProgress.value.total = currentResponse.tool_calls.length for (let i = 0; i < currentResponse.tool_calls.length; i++) { toolProgress.value.current = i + 1 try { const results = await executeToolCalls([currentResponse.tool_calls[i]]) // ... } catch (error) { // ... } } } // 在模板中显示进度
工具执行进度: {{ toolProgress.current }} / {{ toolProgress.total }}
``` --- ### 9. P1-3:文章Markdown支持 [1小时] 修改 `src/renderer/src/components/ArticleResultCard.vue`: ```vue ``` --- ### 10. P1-5:死循环检测 [2小时] 修改 `src/renderer/src/views/Chat.vue`: ```typescript // 在工具调用循环中添加检测 const toolCallHistory: string[] = [] const MAX_IDENTICAL_PATTERNS = 2 while (currentResponse.tool_calls && /* ... */) { const toolNames = currentResponse.tool_calls.map(tc => tc.function.name) const currentPattern = toolNames.join(',') // 检测模式重复 const lastPattern = toolCallHistory[toolCallHistory.length - 1] if (lastPattern === currentPattern) { const lastLastPattern = toolCallHistory[toolCallHistory.length - 2] if (lastLastPattern === currentPattern) { console.warn('检测到死循环模式:', currentPattern) ElMessage.error('检测到可能的无限循环,已停止工具调用') break } } toolCallHistory.push(currentPattern) if (toolCallHistory.length > 10) { toolCallHistory.shift() } // ... 继续执行工具调用 } ``` --- ## 第三阶段:功能完善(1个月,共15小时) ### 11. P2-1:对话导出/导入 [3小时] 创建 `src/renderer/src/utils/chatExport.ts`: ```typescript export interface ChatExport { version: '1.0' exportDate: string messages: any[] } export const exportChat = (messages: any[]): void => { const data: ChatExport = { version: '1.0', exportDate: new Date().toISOString(), messages } const json = JSON.stringify(data, null, 2) const blob = new Blob([json], { type: 'application/json' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `chat-export-${Date.now()}.json` a.click() URL.revokeObjectURL(url) } export const importChat = async (file: File): Promise => { const text = await file.text() const data = JSON.parse(text) as ChatExport return data } ``` 在 Chat.vue 中使用: ```typescript const exportChat = () => { chatExport.exportChat(messages.value) ElMessage.success('对话已导出') } const importChat = async (file: File) => { try { const data = await chatExport.importChat(file) if (data.version !== '1.0') { throw new Error('不支持的版本') } messages.value = [ ...messages.value, ...data.messages.map(m => ({ ...m, timestamp: new Date(m.timestamp) })) ] saveMessages(true) ElMessage.success('对话已导入') } catch (error) { ElMessage.error('导入失败:' + error.message) } } ``` --- ### 12. P2-3:消息搜索 [1小时] 在 Chat.vue 中添加: ```typescript const searchQuery = ref('') const searchResults = computed(() => { if (!searchQuery.value) return [] const query = searchQuery.value.toLowerCase() return messages.value.filter(msg => msg.content.toLowerCase().includes(query) ) }) // 在模板中添加搜索框
找到 {{ searchResults.length }} 条结果
``` --- ### 13. P2-4:语法高亮 [2小时] 修改 `src/renderer/src/components/MarkdownContent.vue`: ```typescript import { marked } from 'marked' import { markedHighlight } from 'marked-highlight' import hljs from 'highlight.js' import 'highlight.js/styles/atom-one-dark.css' marked.use( markedHighlight({ langPrefix: 'hljs language-', highlight(code, lang) { const language = hljs.getLanguage(lang) ? lang : 'plaintext' return hljs.highlight(code, { language }).value } }) ) ``` 安装包: ```bash npm install highlight.js marked-highlight ``` --- ### 14. P2-2:多会话管理 [5小时] 创建 `src/renderer/src/stores/chatSessions.ts`: ```typescript import { defineStore } from 'pinia' import { ref, computed } from 'vue' interface ChatSession { id: string title: string createdAt: Date updatedAt: Date messages: Message[] starred: boolean tags: string[] } export const useChatSessions = defineStore('chatSessions', () => { const sessions = ref([]) const currentSessionId = ref(null) const currentSession = computed(() => sessions.value.find(s => s.id === currentSessionId.value) ) const createSession = () => { const session: ChatSession = { id: Date.now().toString(), title: `对话 ${new Date().toLocaleDateString()}`, createdAt: new Date(), updatedAt: new Date(), messages: [], starred: false, tags: [] } sessions.value.push(session) currentSessionId.value = session.id return session } return { sessions, currentSessionId, currentSession, createSession } }) ``` --- ### 15. P2-5:单元测试 [4小时] 安装 vitest: ```bash npm install -D vitest @vitest/ui @testing-library/vue ``` 创建 `src/renderer/src/services/__tests__/tools.test.ts`: ```typescript import { describe, it, expect, beforeEach, vi } from 'vitest' import { ToolExecutor } from '../tools' describe('ToolExecutor', () => { let executor: ToolExecutor beforeEach(() => { executor = new ToolExecutor() }) describe('execute', () => { it('should handle invalid JSON arguments', async () => { const toolCall = { id: 'test-1', type: 'function' as const, function: { name: 'search_platform', arguments: '{invalid}' } } const result = await executor.execute(toolCall) expect(result.content).toContain('参数解析失败') }) it('should return tool result with correct structure', async () => { const toolCall = { id: 'test-2', type: 'function' as const, function: { name: 'check_platform_login', arguments: JSON.stringify({ platform: 'xiaoheihe' }) } } const result = await executor.execute(toolCall) expect(result.tool_call_id).toBe('test-2') expect(result.role).toBe('tool') expect(result.name).toBe('check_platform_login') }) }) }) ``` --- ## 快速检查清单 ### 立即检查 - [ ] 是否有频繁的console.log警告? - [ ] localStorage中有多少条消息? - [ ] 搜索相同内容会显示缓存吗? - [ ] 工具调用失败时有详细错误吗? - [ ] 未登录时搜索会有提示吗? ### 一周内完成 - [ ] 滚动是否流畅了? - [ ] 消息保存是否快了? - [ ] 代码中还有重复的类型吗? - [ ] 工具调用有进度显示吗? - [ ] 文章能显示Markdown吗? ### 一个月内完成 - [ ] 有导出/导入功能吗? - [ ] 能搜索消息吗? - [ ] 代码有单元测试吗? - [ ] 代码块有语法高亮吗? --- ## 预期改进效果 | 指标 | 改进前 | 改进后 | 提升 | |------|-------|-------|------| | 渲染频率 (token处理) | 每token 1次 | 每50ms 1次 | ⬇️ 60-70% | | localStorage写入 | 同步全量 | 异步压缩 | ⬇️ 60% | | 搜索重复率 | 0% (无缓存) | 80% (缓存命中) | ⬆️ 80% | | 类型错误 | 中等 | 几乎无 | ⬇️ 90% | | 首次登录检查 | 无 | 启动时进行 | ✓ 新增 | --- ## 总工作量统计 | 阶段 | 项目数 | 总工时 | 优先级 | |------|-------|--------|--------| | 第一阶段 | 5个 | 11小时 | P0 | | 第二阶段 | 5个 | 11小时 | P1 | | 第三阶段 | 5个 | 15小时 | P2 | | **合计** | **15个** | **37小时** | - | **建议分配**: - 第一阶段(P0): 本周完成(2-3天) - 第二阶段(P1): 下周完成(3-4天) - 第三阶段(P2): 第3-4周完成(持续改进)