32 KiB
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更新 currentContent和toolCall的更新直接修改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
})
}
}
}
优化建议优先级排序
第一阶段(高优先级)- 立即修复
- P0-01: 防抖scrollToBottom,减少re-render
- P0-02: 优化localStorage写入
- P0-03: 统一错误处理和类型定义
- P0-04: 改进登录状态提示
- P0-05: 处理Playwright进程崩溃
第二阶段(中优先级)- 1-2周内
- P1-01: 缓存搜索结果和文章
- P1-02: 添加工具调用进度提示
- P1-03: 改进文章显示(支持Markdown)
- P1-04: 提取魔法字符串为常量
- P1-05: 工具调用死循环检测
第三阶段(低优先级)- 1个月内
- P2-01: 添加对话导出/导入功能
- P2-02: 实现多对话会话管理
- P2-03: 添加消息搜索功能
- P2-04: 语法高亮
- P2-05: 单元测试
检查清单
- 实现防抖scrollToBottom
- 优化localStorage存储结构
- 创建统一的types/index.ts
- 创建AppError错误类
- 创建constants.ts文件
- 添加登录状态检查
- 实现搜索结果缓存
- 添加工具调用重试逻辑
- 改进文章预览显示
- 添加单元测试框架
总结
该项目是一个功能完整的AI对话工具,但存在以下主要问题:
- 性能: 频繁重渲染、localStorage同步写入、浏览器进程管理不善
- UX: 缺少加载状态、错误恢复、登录状态提示
- 代码质量: 类型定义重复、魔法字符串、错误处理不一致
- 功能完善度: 缺少导出、会话管理、语法高亮
建议按照优先级逐步改进,特别是第一阶段的5个关键问题应该立即修复。