Files
ai-desktop/OPTIMIZATION_PRIORITIES.md
T

20 KiB
Raw Blame History

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

立即行动:

// 只保存最近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

立即行动:

// 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

立即行动:

// 防抖滚动
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

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

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

立即行动:

// 添加重试机制
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:

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

在代码中使用:

// 之前
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 中:

// 添加缓存
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:

// 添加进度跟踪
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:

<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:

// 在工具调用循环中添加检测
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:

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 中使用:

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 中添加:

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:

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
    }
  })
)

安装包:

npm install highlight.js marked-highlight

14. P2-2:多会话管理 [5小时]

创建 src/renderer/src/stores/chatSessions.ts:

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:

npm install -D vitest @vitest/ui @testing-library/vue

创建 src/renderer/src/services/__tests__/tools.test.ts:

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周完成(持续改进)