Files
ai-desktop/OPTIMIZATION_PRIORITIES.md
T

795 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<typeof setTimeout> | 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<string, any>
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<string, any>
) {
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<any> {
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<BrowserContext> {
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<string, any>()
const searchCache = new Map<string, any>()
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) {
// ...
}
}
}
// 在模板中显示进度
<div v-if="toolProgress.total > 0" class="tool-progress">
: {{ toolProgress.current }} / {{ toolProgress.total }}
</div>
```
---
### 9. P1-3:文章Markdown支持 [1小时]
修改 `src/renderer/src/components/ArticleResultCard.vue`:
```vue
<template>
<div class="article-result-card">
<!-- 保持现有错误处理 -->
<div v-else-if="data.article" class="article-content">
<!-- 保持meta和stats -->
<div class="article-body">
<!-- 改这里支持Markdown -->
<MarkdownContent
v-if="data.article.content"
:content="data.article.content"
/>
<!-- 或者如果太长显示预览 -->
<el-collapse v-if="contentLength > 1000">
<el-collapse-item name="full-content">
<template #title>查看完整内容 ({{ contentLength }} )</template>
<MarkdownContent :content="data.article.content" />
</el-collapse-item>
</el-collapse>
</div>
<!-- 保持现有评论展示 -->
</div>
</div>
</template>
<script setup lang="ts">
import MarkdownContent from './MarkdownContent.vue'
// 添加计算属性
const contentLength = computed(() => data.article?.content?.length || 0)
</script>
```
---
### 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<ChatExport> => {
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)
)
})
// 在模板中添加搜索框
<el-input
v-model="searchQuery"
placeholder="搜索消息..."
@keydown.enter="focusFirstSearchResult"
/>
<div v-if="searchQuery && searchResults.length > 0" class="search-results">
{{ searchResults.length }}
</div>
```
---
### 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<ChatSession[]>([])
const currentSessionId = ref<string | null>(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周完成(持续改进)