Files
ai-desktop/CODE_ANALYSIS_REPORT.md
T

32 KiB
Raw Blame History

AI Desktop 项目代码结构分析报告

项目概述

  • 类型: Electron + Vue 3 + TypeScript 桌面应用
  • 功能: AI对话助手,支持工具调用(搜索、抓取文章)
  • 平台: 小黑盒游戏社区集成
  • 核心文件:
    • 主进程: src/main/index.ts (865行)
    • Chat视图: src/renderer/src/views/Chat.vue (609行)
    • 工具定义: src/renderer/src/services/tools.ts (304行)

一、性能优化点

🔴 P0 - 关键性能问题

1. Chat.vue 中的无限重渲染问题

文件: src/renderer/src/views/Chat.vue: Line 267-420

问题描述:

  • 在工具调用循环中,每次迭代都会创建新的Message对象并推送到messages数组
  • 每次创建都会触发scrollToBottom(),导致频繁的DOM更新
  • currentContenttoolCall的更新直接修改lastMessage,造成过度渲染

代码示例:

// 第279-284行:每个token都触发scrollToBottom
onToken: (token: string) => {
  currentContent += token
  const lastMessage = messages.value[messages.value.length - 1]
  if (lastMessage && lastMessage.role === 'assistant') {
    lastMessage.content = currentContent
    scrollToBottom()  // 频繁调用!
  }
}

// 第384-392行:循环中多次创建message
const nextAssistantMessage: Message = {
  id: (Date.now() + iteration + 1000).toString(),
  role: 'assistant',
  content: '',
  timestamp: new Date(),
  toolCalls: []
}
messages.value.push(nextAssistantMessage)
scrollToBottom()

影响: ⚠️ 高,导致UI卡顿,特别是在工具调用次数多时

优化方案:

// 1. 防抖scrollToBottom
let scrollTimer: ReturnType<typeof setTimeout> | null = null
const debouncedScrollToBottom = () => {
  if (scrollTimer) clearTimeout(scrollTimer)
  scrollTimer = setTimeout(() => {
    scrollToBottom()
  }, 50)
}

// 2. 批量更新消息而不是逐token更新
let tokenBuffer = ''
const flushTokenBuffer = () => {
  if (tokenBuffer) {
    const lastMessage = messages.value[messages.value.length - 1]
    if (lastMessage) lastMessage.content += tokenBuffer
    tokenBuffer = ''
    debouncedScrollToBottom()
  }
}

2. localStorage 同步写入性能瓶颈

文件: src/renderer/src/views/Chat.vue: Line 467-485

问题描述:

  • saveMessages()直接写入localStorage,没有压缩或序列化优化
  • 每次发送消息都会序列化整个messages数组
  • 对于大型对话历史(50条消息),JSON.stringify性能下降明显

代码:

// 第470行
localStorage.setItem('chat-messages', JSON.stringify(messages.value))

优化方案:

// 1. 只保存最近100条消息
const saveMessages = (immediate = false) => {
  const messagesToSave = messages.value.slice(-100)
  // 2. 压缩工具调用结果(只保存必要信息)
  const compressed = messagesToSave.map(msg => ({
    ...msg,
    toolCalls: msg.toolCalls?.map(tc => ({
      name: tc.name,
      status: tc.status,
      // 不保存完整result,只保存摘要
      result: tc.result?.substring?.(0, 200)
    }))
  }))
  localStorage.setItem('chat-messages', JSON.stringify(compressed))
}

3. 主进程中的 Playwright 浏览器上下文持久化问题

文件: src/main/index.ts: Line 396-406, 632-641

问题描述:

  • getPersistentContext()每次调用都检查并创建新上下文
  • 多个搜索请求会复用同一个chromium进程,但内存泄漏风险大
  • 没有上下文生命周期管理,可能导致内存持续增长

代码:

// 第396-406行
async function getPersistentContext(headless = true): Promise<BrowserContext> {
  if (!persistentContext) {
    persistentContext = await chromium.launchPersistentContext(userDataDir, {
      headless,
      viewport: { width: 1280, height: 800 },
      userAgent: '...'
    })
  }
  return persistentContext
}

优化方案:

// 1. 添加上下文大小限制
class ContextManager {
  private maxPages = 5
  private pages: Map<string, Page> = new Map()
  
  async getPage(): Promise<Page> {
    if (this.pages.size >= this.maxPages) {
      const oldestKey = this.pages.keys().next().value
      const oldPage = this.pages.get(oldestKey)
      await oldPage?.close()
      this.pages.delete(oldestKey)
    }
    // ...
  }
}

// 2. 定期清理内存
setInterval(async () => {
  if (persistentContext) {
    // 清理超过1小时的页面
  }
}, 1000 * 60 * 5)

🟡 P1 - 中等性能问题

4. AI Service 中的 SSE 流处理问题

文件: src/renderer/src/services/aiService.ts: Line 240-317

问题描述:

  • 每行都通过JSON.parse()单独解析,没有缓存
  • 工具调用参数是字符串,需要多次JSON.parse()
  • 没有流式处理优化(应该使用ReadableStream

代码:

// 第260-265行:逐行JSON.parse
for (const line of lines) {
  const trimmedLine = line.trim()
  if (!trimmedLine || trimmedLine === 'data: [DONE]') continue
  
  if (trimmedLine.startsWith('data: ')) {
    try {
      const jsonStr = trimmedLine.substring(6)
      const data = JSON.parse(jsonStr)  // 每行都parse

优化方案:

// 缓存JSON parse结果,使用Try Catch外层
const parseCache = new Map()
const safeParse = (str: string) => {
  if (parseCache.has(str)) return parseCache.get(str)
  const result = JSON.parse(str)
  parseCache.set(str, result)
  return result
}

5. MessageCard 组件不必要的重渲染

文件: src/renderer/src/components/MessageCard.vue: Line 34-80

问题描述:

  • 每个消息卡片都有完整的MarkdownContent和ToolCallCard渲染
  • 没有使用computed缓存格式化时间
  • formatTime()在每次渲染时重新计算相对时间

代码:

// 第62-79行:formatTime在每次render都计算
const formatTime = (date: Date): string => {
  const now = new Date()
  const diff = now.getTime() - date.getTime()
  // ...
}

优化方案:

// 使用computed缓存,只在Date改变时重新计算
const formattedTime = computed(() => {
  if (!props.message.timestamp) return ''
  return formatTime(props.message.timestamp)
})

// 添加v-if提前退出
<MarkdownContent 
  v-if="message.content" 
  :content="message.content" 
/>

6. ToolsPanel.vue 中的重复 API 调用

文件: src/renderer/src/views/ToolsPanel.vue: Line 123-215

问题描述:

  • 搜索和获取文章都调用executeToolCalls(),没有缓存
  • 同一个搜索结果被用户点击时会重新fetch整个文章
  • 没有实现请求去重

代码:

// 第212-214行:article-click触发新的fetch
const handleArticleClick = (url: string) => {
  articleUrl.value = url
  handleFetchArticle()  // 每次点击都fetch,即使之前fetch过
}

优化方案:

const articleCache = new Map<string, any>()

const handleArticleClick = async (url: string) => {
  if (articleCache.has(url)) {
    articleResult.value = articleCache.get(url)
    return
  }
  // 否则 fetch 并缓存
}

二、用户体验改进

🔴 P0 - 关键UX问题

7. 缺少加载状态提示和错误处理

文件: src/renderer/src/views/Chat.vue: Line 220-445

问题描述:

  • 工具调用失败时,用户不知道是否可以重试
  • 没有进度提示(第几个工具调用在执行)
  • 错误消息不够详细,用户难以了解失败原因

现象:

// 第364-371行:错误只是显示generic message
} catch (error) {
  console.error(`Tool call ${i + 1} error:`, error)
  if (lastMessage && lastMessage.toolCalls && lastMessage.toolCalls[i]) {
    lastMessage.toolCalls[i].status = 'error'
    lastMessage.toolCalls[i].result = JSON.stringify({ error: '工具执行失败' })
    // 没有详细错误信息给用户
  }
}

优化方案:

// 1. 增加详细错误信息
lastMessage.toolCalls[i].result = JSON.stringify({
  error: '工具执行失败',
  reason: error.message,
  suggestion: getSuggestion(error),
  retryable: isRetryable(error)
})

// 2. 添加重试按钮
<ToolCallCard 
  :tool-call="toolCall"
  :can-retry="toolCall.status === 'error' && toolCall.retryable"
  @retry="retryToolCall"
/>

8. 工具调用进度反馈不清晰

文件: src/renderer/src/views/Chat.vue: Line 314-373

问题描述:

  • 工具调用完成后没有总结或完成提示
  • 用户不知道是还有更多工具调用还是已经完成
  • 加载指示器停留时间不确定

代码现象:

// 第321-373行:没有进度指示
for (let i = 0; i < currentResponse.tool_calls.length; i++) {
  // 执行工具
  // 但没有显示 "执行了 i 个/共 total 个工具"
}

优化:

// 添加进度计数器
<div class="tool-progress">
  执行中: {{ currentToolIndex + 1 }} / {{ totalTools }}
</div>

9. 消息历史加载卡顿

文件: src/renderer/src/views/Chat.vue: Line 117-149

问题描述:

  • 初始化时只加载最近50条消息,但没有给用户"加载更多"选项
  • 没有提示用户历史消息被截断
  • localStorage中大量数据未被利用

代码:

// 第133-134行:硬编码50条限制
const recentMessages = parsed.slice(-50)
messages.value = recentMessages.map(...)

优化:

// 1. 添加"查看更多历史"功能
const showMoreHistory = async () => {
  const offset = messages.value.length - 50
  if (offset > 0) {
    const moreMessages = parsed.slice(offset, offset + 50)
    messages.value.unshift(...moreMessages)
  }
}

// 2. UI提示
<div v-if="hasMoreHistory" class="load-more-button">
  <el-button @click="showMoreHistory">加载更多历史</el-button>
</div>

10. 登录状态提示不及时

文件: src/renderer/src/services/tools.ts: Line 190-241

问题描述:

  • 搜索失败时才告知用户未登录
  • Settings中登录状态检查不实时(需要点击refresh)
  • 没有自动登录状态同步

代码:

// 第202-211行:搜索时才返回NOT_LOGGED_IN
if (result.error === 'NOT_LOGGED_IN') {
  return {
    success: false,
    error: 'NOT_LOGGED_IN',
    message: `搜索 ${platform} 需要登录。请先登录小黑盒账号。`
  }
}

优化:

// 1. 在Chat初始化时检查登录状态
onMounted(async () => {
  const loginStatus = await window.electron.ipcRenderer.invoke('check-platform-login', {
    platform: 'xiaoheihe'
  })
  if (!loginStatus.isLoggedIn) {
    ElMessage.warning('请登录小黑盒账号以使用搜索功能')
  }
})

// 2. 定期同步登录状态(每5分钟)
setInterval(syncLoginStatus, 300000)

🟡 P1 - 中等UX问题

11. 搜索结果没有加载更多功能

文件: src/renderer/src/components/SearchResultCard.vue

问题描述:

  • 只展示返回的所有结果,没有分页
  • 结果多时UI会很长,难以查找

优化:

// 只显示前10条,添加"查看更多"
const displayResults = computed(() => 
  data.results?.slice(0, 10) || []
)

const hasMore = computed(() => 
  data.results && data.results.length > 10
)

12. 文章内容显示不完整

文件: src/renderer/src/components/ArticleResultCard.vue: Line 54

问题描述:

  • 文章内容使用white-space: pre-wrap显示,不支持HTML或Markdown
  • 长文章显示时没有分页或折叠

代码:

// 第54行:纯文本显示,丢失格式
<div class="content-text">{{ data.article.content }}</div>

优化:

<!-- 支持Markdown格式 -->
<MarkdownContent 
  v-if="data.article.content"
  :content="data.article.content"
/>

<!-- 长文章添加折叠 -->
<div v-if="contentLength > 1000" class="content-preview">
  <el-collapse>
    <el-collapse-item name="full-content">
      <template #title>查看完整内容 ({{ contentLength }} )</template>
      <MarkdownContent :content="data.article.content" />
    </el-collapse-item>
  </el-collapse>
</div>

三、代码质量问题

🔴 P0 - 关键质量问题

13. 重复的类型定义

文件:

  • Chat.vue: Line 75-88 (ToolCallInfo, Message)
  • tools.ts: Line 16-30 (ToolCall, ToolResult)
  • aiService.ts: Line 12-18 (Message)

问题描述:

  • Message接口定义了3次
  • ToolCall接口定义了2次
  • 没有单一的真实源头

优化:

// 创建 src/renderer/src/types/index.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'
}

// 在各文件中导入
import type { Message, ToolCall, ToolCallInfo } from '@/types'

14. 错误处理不一致

文件:

  • Chat.vue: Line 256-445 (复杂的error handling)
  • ToolsPanel.vue: Line 156-165 (简单的error handling)
  • tools.ts: Line 200-241 (多层error handling)

问题描述:

  • 有些地方使用try-catch,有些使用if检查
  • 错误消息格式不一致
  • 没有统一的错误处理策略

代码示例:

// Chat.vue: 复杂的error handling
try {
  // ...
} catch (error: any) {
  console.error('AI request failed:', error)
  ElMessage.error(error.message || '请求失败,请检查配置')
  messages.value = messages.value.filter((msg) => msg.id !== userMessage.id)
}

// ToolsPanel.vue: 简单的error handling
} catch (error: any) {
  console.error('Search failed:', error)
  ElMessage.error(error.message || '搜索失败')
}

// tools.ts: if检查
if (!result.success) {
  return {
    success: false,
    error: result.error || '搜索失败'
  }
}

优化:

// 创建统一的错误处理工具
export class AppError extends Error {
  constructor(
    message: string,
    public code: string,
    public recoverable: boolean = false
  ) {
    super(message)
  }
}

// 使用工厂函数创建特定错误
export const createError = {
  notLoggedIn: () => new AppError('需要登录', 'NOT_LOGGED_IN', true),
  networkError: (msg?: string) => new AppError(msg || '网络连接失败', 'NETWORK_ERROR', true),
  toolExecutionFailed: () => new AppError('工具执行失败', 'TOOL_ERROR', true)
}

// 统一错误处理
const handleError = (error: Error) => {
  if (error instanceof AppError) {
    ElMessage.error(error.message)
    if (error.recoverable) {
      // 显示重试按钮
    }
  }
}

15. 缺少TypeScript类型安全

文件: Chat.vue: Line 182-199

问题描述:

  • getActiveModel()返回类型为Promise<ModelConfig | null>,但ModelConfig接口与实际settings中的类型不匹配
  • 没有在主进程中定义和导出类型
  • IPC调用的返回类型是any

代码:

// 第182-199行:类型不安全
const getActiveModel = async (): Promise<ModelConfig | null> => {
  try {
    const settings = await window.electron.ipcRenderer.invoke('read-settings')
    // settings类型是any,无法推断出modelConfigs
    if (!settings.activeModelId) {
      return null
    }
    // ...
  }
}

优化:

// 创建共享类型文件 src/shared/types.ts
export interface ModelConfig {
  id: string
  name: string
  provider: 'openai' | 'deepseek'
  model: string
  apiKey: string
  baseUrl: string
}

export interface Settings {
  activeModelId: string | null
  modelConfigs: ModelConfig[]
}

// 在Chat.vue中使用
const getActiveModel = async (): Promise<ModelConfig | null> => {
  const settings = await window.electron.ipcRenderer.invoke('read-settings') as Settings
  // 现在settings有正确的类型
}

🟡 P1 - 中等质量问题

16. 魔法字符串和数字遍布代码

文件: Chat.vue, index.ts, tools.ts

问题描述:

  • 硬编码的延迟时间:setTimeout(..., 100), setTimeout(..., 200), setTimeout(..., 300)
  • 硬编码的常量:maxRows: 4, minRows: 1, -50 (slice)
  • 硬编码的工具名称:'search_platform', 'fetch_article'

代码:

// Chat.vue: 魔法数字
setTimeout(() => { ... }, 0)    // 第148行
setTimeout(() => { ... }, 100)  // 第230行
setTimeout(() => { ... }, 200)  // 第274行
setTimeout(() => { ... }, 300)  // 第480行
await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 })  // 30000?
const recentMessages = parsed.slice(-50)  // 为什么是50条?
let maxIterations = 10  // 为什么是10次?

优化:

// 创建 src/renderer/src/constants.ts
export const TIMEOUTS = {
  DEFER_MESSAGE_LOAD: 0,
  ENSURE_IME_READY: 100,
  ENSURE_RENDERER_READY: 200,
  DEBOUNCE_SAVE: 300,
  TOOL_EXECUTION_TIMEOUT: 30000
} as const

export const LIMITS = {
  MAX_RECENT_MESSAGES: 50,
  MAX_TOOL_ITERATIONS: 10,
  MAX_SEARCH_RESULTS_DISPLAY: 10,
  ARTICLE_PREVIEW_LENGTH: 1000
} as const

export const TOOL_NAMES = {
  CHECK_LOGIN: 'check_platform_login',
  SEARCH: 'search_platform',
  FETCH_ARTICLE: 'fetch_article'
} as const

// 在代码中使用
setTimeout(() => { ... }, TIMEOUTS.DEFER_MESSAGE_LOAD)
const recentMessages = parsed.slice(-LIMITS.MAX_RECENT_MESSAGES)

17. 过度复杂的条件检查

文件: src/main/index.ts: Line 42-72

问题描述:

  • SearchRateLimiter.canSearch()的逻辑可以简化
  • 时间比较逻辑混乱

代码:

// 第42-72行:过度复杂
canSearch(): { allowed: boolean; waitTime?: number; reason?: string } {
  const now = Date.now()
  const timeSinceLastSearch = now - this.lastSearchTime

  if (timeSinceLastSearch > this.resetInterval) {
    this.searchCount = 0
  }

  if (this.searchCount >= this.maxSearchPerMinute) {
    const waitTime = this.resetInterval - timeSinceLastSearch
    return {
      allowed: false,
      waitTime: Math.ceil(waitTime / 1000),
      reason: '搜索过于频繁,请稍后再试'
    }
  }

  if (timeSinceLastSearch < this.minInterval) {
    const waitTime = this.minInterval - timeSinceLastSearch
    return {
      allowed: false,
      waitTime: Math.ceil(waitTime / 1000),
      reason: '请求过快,请稍后再试'
    }
  }

  return { allowed: true }
}

优化:

canSearch(): RateLimitResult {
  const now = Date.now()
  const elapsed = now - this.lastSearchTime

  // 检查是否需要重置计数
  if (elapsed > this.resetInterval) {
    this.searchCount = 0
  }

  // 检查频率限制(优先级最高)
  if (this.searchCount >= this.maxSearchPerMinute) {
    return this.createRateLimitResult(
      this.resetInterval - elapsed,
      'RATE_LIMIT_EXCEEDED'
    )
  }

  // 检查最小间隔
  if (elapsed < this.minInterval) {
    return this.createRateLimitResult(
      this.minInterval - elapsed,
      'INTERVAL_LIMIT'
    )
  }

  return { allowed: true }
}

private createRateLimitResult(waitMs: number, reason: string): RateLimitResult {
  return {
    allowed: false,
    waitTime: Math.ceil(waitMs / 1000),
    reason: this.getReason(reason)
  }
}

18. 缺少单元测试和错误覆盖

文件: 项目整体

问题描述:

  • 没有单元测试文件
  • 没有集成测试
  • 错误情况未经过测试

优化方案:

// 创建 src/renderer/src/services/__tests__/tools.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ToolExecutor } from '../tools'

describe('ToolExecutor', () => {
  let executor: ToolExecutor

  beforeEach(() => {
    executor = new ToolExecutor()
  })

  it('should handle platform login check', async () => {
    const toolCall = {
      id: 'test-1',
      type: 'function' as const,
      function: {
        name: 'check_platform_login',
        arguments: JSON.stringify({ platform: 'xiaoheihe' })
      }
    }

    const result = await executor.execute(toolCall)
    expect(result.role).toBe('tool')
    expect(result.name).toBe('check_platform_login')
  })

  it('should handle invalid arguments', async () => {
    const toolCall = {
      id: 'test-2',
      type: 'function' as const,
      function: {
        name: 'search_platform',
        arguments: '{invalid json}'
      }
    }

    const result = await executor.execute(toolCall)
    expect(result.content).toContain('参数解析失败')
  })
})

四、功能完善

🔴 P0 - 缺失的关键功能

19. 缺少对话导出功能

问题描述:

  • 用户无法导出对话历史
  • 没有分享功能
  • 数据只存在localStorage中,易丢失

建议:

// 添加导出功能
const exportChat = () => {
  const data = {
    version: '1.0',
    exportDate: new Date().toISOString(),
    messages: messages.value
  }
  
  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-${Date.now()}.json`
  a.click()
  URL.revokeObjectURL(url)
}

// 添加导入功能
const importChat = async (file: File) => {
  const text = await file.text()
  const data = JSON.parse(text)
  messages.value = [
    ...messages.value,
    ...data.messages
  ]
  saveMessages(true)
}

20. 缺少对话收藏/标记功能

问题描述:

  • 用户无法标记重要对话
  • 没有分类功能
  • 对话历史平铺,难以找到之前的对话

建议:

interface ChatSession {
  id: string
  title: string
  createdAt: Date
  messages: Message[]
  starred: boolean
  tags: string[]
}

// 在localStorage中分别存储sessions和当前session
const currentSessionId = ref<string>()
const sessions = ref<ChatSession[]>([])

const createNewSession = () => {
  const session: ChatSession = {
    id: Date.now().toString(),
    title: `对话 ${new Date().toLocaleDateString()}`,
    createdAt: new Date(),
    messages: [],
    starred: false,
    tags: []
  }
  sessions.value.push(session)
  currentSessionId.value = session.id
}

21. 缺少Markdown语法高亮

文件: src/renderer/src/components/MarkdownContent.vue

问题描述:

  • markdown中的代码块没有语法高亮
  • 使用了marked库但没有配置高亮器

优化:

// 安装 npm install highlight.js
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
    }
  })
)

22. 缺少快捷键帮助

问题描述:

  • 用户不知道有Command+K快捷键
  • 没有快捷键提示菜单
  • 可发现性差

建议:

// 添加快捷键帮助面板
const showShortcutHelp = () => {
  ElDialog.confirm('快捷键列表', 'OK').then(() => {
    // 显示快捷键列表
  })
}

// 在Chat组件中按 ? 显示帮助
window.addEventListener('keydown', (e) => {
  if (e.key === '?') {
    showShortcutHelp()
  }
})

🟡 P1 - 建议添加的功能

23. 添加消息搜索功能

const searchMessages = (query: string) => {
  return messages.value.filter(msg => 
    msg.content.toLowerCase().includes(query.toLowerCase())
  )
}

24. 添加对话主题切换

const themes = {
  light: { /* ... */ },
  dark: { /* ... */ }
}

const setTheme = (name: keyof typeof themes) => {
  localStorage.setItem('app-theme', name)
  // 应用主题
}

25. 添加消息撤回/编辑功能

interface Message {
  id: string
  // ...
  editable: boolean
  editHistory?: string[]
}

const editMessage = (messageId: string, newContent: string) => {
  const msg = messages.value.find(m => m.id === messageId)
  if (msg) {
    msg.editHistory?.push(msg.content)
    msg.content = newContent
  }
}

五、错误处理和异常情况

🔴 P0 - 关键错误处理缺陷

26. Playwright 浏览器崩溃未处理

文件: src/main/index.ts: Line 352-393

问题描述:

  • 浏览器进程意外结束时没有恢复机制
  • fetchArticleContent()不能重试
  • persistentContext崩溃后不会重新创建

代码:

// 第352-393行:没有错误恢复
async function fetchArticleContent(url: string): Promise<{ /* ... */ }> {
  let browser
  try {
    browser = await chromium.launch({ headless: true })
    // ... 如果这里崩溃,browser会是undefined
  } finally {
    if (browser) {
      // 但如果browser进程已经死了呢?
      await browser.close()
    }
  }
}

优化:

// 添加重试逻辑和错误恢复
async function fetchArticleContentWithRetry(
  url: string,
  maxRetries = 3
): Promise<ArticleContent> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fetchArticleContent(url)
    } catch (error) {
      if (i === maxRetries - 1) throw error
      // 等待后重试
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
    }
  }
}

// 添加crash检测
persistentContext?.on?.('disconnected', () => {
  console.log('Browser context disconnected, will recreate on next use')
  persistentContext = null
})

27. 网络错误未区分

文件: src/renderer/src/services/aiService.ts: Line 230-234

问题描述:

  • 所有失败都用同一个错误消息
  • 无法区分网络错误、API错误、超时等
  • 用户无法采取相应的行动

代码:

// 第230-234行:所有错误都一样
const response = await fetch(endpoint, {
  // ...
})

if (!response.ok) {
  const errorText = await response.text()
  throw new Error(`API 请求失败: ${response.status} ${errorText}`)
}

优化:

class APIError extends Error {
  constructor(
    message: string,
    public status: number,
    public responseText: string
  ) {
    super(message)
    this.name = 'APIError'
  }
}

class NetworkError extends Error {
  constructor(message: string) {
    super(message)
    this.name = 'NetworkError'
  }
}

// 更好的错误处理
const response = await fetch(endpoint, {
  // ...
}).catch((error) => {
  if (error instanceof TypeError) {
    throw new NetworkError('网络连接失败,请检查网络设置')
  }
  throw error
})

if (!response.ok) {
  const errorText = await response.text()
  
  if (response.status === 401) {
    throw new APIError('API Key 无效或已过期', 401, errorText)
  } else if (response.status === 429) {
    throw new APIError('请求过于频繁,请稍后再试', 429, errorText)
  } else if (response.status >= 500) {
    throw new APIError('服务器错误,请稍后重试', response.status, errorText)
  }
  
  throw new APIError('API 请求失败', response.status, errorText)
}

28. 工具调用死循环未防护

文件: src/renderer/src/views/Chat.vue: Line 320-424

问题描述:

  • maxIterations硬编码为10
  • 如果AI不断生成工具调用,会浪费资源
  • 没有检测无限循环的模式

代码:

// 第320-424行:简单的maxIterations检查
while (currentResponse.tool_calls && currentResponse.tool_calls.length > 0 && iteration < maxIterations) {
  // ...
  if (iteration >= maxIterations) {
    console.warn('Reached maximum tool call iterations')
    ElMessage.warning('工具调用次数过多,已停止')
  }
}

优化:

// 检测模式:如果连续的工具调用相同,则认为可能是循环
const toolCallHistory: string[] = []

while (currentResponse.tool_calls && /* ... */) {
  const toolNames = currentResponse.tool_calls.map(tc => tc.function.name)
  const currentPattern = toolNames.join(',')
  
  // 检查是否重复相同的工具调用
  if (toolCallHistory[toolCallHistory.length - 1] === currentPattern &&
      toolCallHistory[toolCallHistory.length - 2] === currentPattern) {
    ElMessage.error('检测到可能的死循环,已停止')
    break
  }
  
  toolCallHistory.push(currentPattern)
  if (toolCallHistory.length > 10) {
    toolCallHistory.shift()
  }
  
  // ...
  iteration++
}

29. 未登录状态处理不完善

文件: src/renderer/src/services/tools.ts: Line 201-241

问题描述:

  • 搜索时才发现未登录,用户已输入查询
  • Settings中登录状态检查不够频繁
  • 没有自动刷新登录状态

优化:

// 在Chat.vue中启动时检查
onMounted(() => {
  // ... 现有代码

  // 检查登录状态
  checkLoginStatus()
  
  // 定期检查登录状态(每5分钟)
  const checkInterval = setInterval(() => {
    checkLoginStatus()
  }, 300000)

  onUnmounted(() => {
    clearInterval(checkInterval)
  })
})

const checkLoginStatus = async () => {
  const result = await window.electron.ipcRenderer.invoke('check-platform-login', {
    platform: 'xiaoheihe'
  })
  
  if (!result.isLoggedIn) {
    isLoggedIn.value = false
    // 禁用搜索相关功能
  } else {
    isLoggedIn.value = true
  }
}

// 在模板中
<el-button
  type="primary"
  :disabled="!isLoggedIn"
  @click="handleSearch"
>
  搜索
</el-button>

<div v-if="!isLoggedIn" class="login-warning">
  需要登录才能使用搜索功能
  <el-button link @click="openSettings">立即登录</el-button>
</div>

30. 工具执行结果验证缺失

文件: src/renderer/src/services/tools.ts: Line 98-161

问题描述:

  • 工具执行结果没有验证
  • 返回的数据格式可能不符合预期
  • 没有null/undefined检查

代码:

// 第104-116行:直接返回result,没有验证
let args: any
try {
  args = JSON.parse(argsStr)
  console.log('Parsed arguments:', args)
} catch (error) {
  console.error('Failed to parse tool arguments:', error)
  return {
    tool_call_id: id,
    role: 'tool',
    name,
    content: JSON.stringify({ error: '参数解析失败', details: String(error) })
  }
}

优化:

// 使用Zod进行运行时验证
import { z } from 'zod'

const ToolResultSchema = z.object({
  success: z.boolean(),
  error: z.string().optional(),
  results: z.array(z.any()).optional(),
  message: z.string().optional()
})

async execute(toolCall: ToolCall): Promise<ToolResult> {
  // ... 执行工具
  
  try {
    // 验证结果格式
    const validated = ToolResultSchema.parse(result)
    
    return {
      tool_call_id: id,
      role: 'tool',
      name,
      content: JSON.stringify(validated)
    }
  } catch (error) {
    console.error('Tool result validation failed:', error)
    return {
      tool_call_id: id,
      role: 'tool',
      name,
      content: JSON.stringify({
        error: '工具结果格式错误',
        details: error.message
      })
    }
  }
}

优化建议优先级排序

第一阶段(高优先级)- 立即修复

  1. P0-01: 防抖scrollToBottom,减少re-render
  2. P0-02: 优化localStorage写入
  3. P0-03: 统一错误处理和类型定义
  4. P0-04: 改进登录状态提示
  5. P0-05: 处理Playwright进程崩溃

第二阶段(中优先级)- 1-2周内

  1. P1-01: 缓存搜索结果和文章
  2. P1-02: 添加工具调用进度提示
  3. P1-03: 改进文章显示(支持Markdown)
  4. P1-04: 提取魔法字符串为常量
  5. P1-05: 工具调用死循环检测

第三阶段(低优先级)- 1个月内

  1. P2-01: 添加对话导出/导入功能
  2. P2-02: 实现多对话会话管理
  3. P2-03: 添加消息搜索功能
  4. P2-04: 语法高亮
  5. P2-05: 单元测试

检查清单

  • 实现防抖scrollToBottom
  • 优化localStorage存储结构
  • 创建统一的types/index.ts
  • 创建AppError错误类
  • 创建constants.ts文件
  • 添加登录状态检查
  • 实现搜索结果缓存
  • 添加工具调用重试逻辑
  • 改进文章预览显示
  • 添加单元测试框架

总结

该项目是一个功能完整的AI对话工具,但存在以下主要问题:

  1. 性能: 频繁重渲染、localStorage同步写入、浏览器进程管理不善
  2. UX: 缺少加载状态、错误恢复、登录状态提示
  3. 代码质量: 类型定义重复、魔法字符串、错误处理不一致
  4. 功能完善度: 缺少导出、会话管理、语法高亮

建议按照优先级逐步改进,特别是第一阶段的5个关键问题应该立即修复。