1313 lines
32 KiB
Markdown
1313 lines
32 KiB
Markdown
# 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,造成过度渲染
|
||
|
||
**代码示例**:
|
||
```typescript
|
||
// 第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卡顿,特别是在工具调用次数多时
|
||
|
||
**优化方案**:
|
||
```typescript
|
||
// 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性能下降明显
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第470行
|
||
localStorage.setItem('chat-messages', JSON.stringify(messages.value))
|
||
```
|
||
|
||
**优化方案**:
|
||
```typescript
|
||
// 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进程,但内存泄漏风险大
|
||
- 没有上下文生命周期管理,可能导致内存持续增长
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第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
|
||
}
|
||
```
|
||
|
||
**优化方案**:
|
||
```typescript
|
||
// 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)
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第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
|
||
```
|
||
|
||
**优化方案**:
|
||
```typescript
|
||
// 缓存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()`在每次渲染时重新计算相对时间
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第62-79行:formatTime在每次render都计算
|
||
const formatTime = (date: Date): string => {
|
||
const now = new Date()
|
||
const diff = now.getTime() - date.getTime()
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**优化方案**:
|
||
```typescript
|
||
// 使用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整个文章
|
||
- 没有实现请求去重
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第212-214行:article-click触发新的fetch
|
||
const handleArticleClick = (url: string) => {
|
||
articleUrl.value = url
|
||
handleFetchArticle() // 每次点击都fetch,即使之前fetch过
|
||
}
|
||
```
|
||
|
||
**优化方案**:
|
||
```typescript
|
||
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`
|
||
|
||
**问题描述**:
|
||
- 工具调用失败时,用户不知道是否可以重试
|
||
- 没有进度提示(第几个工具调用在执行)
|
||
- 错误消息不够详细,用户难以了解失败原因
|
||
|
||
**现象**:
|
||
```typescript
|
||
// 第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: '工具执行失败' })
|
||
// 没有详细错误信息给用户
|
||
}
|
||
}
|
||
```
|
||
|
||
**优化方案**:
|
||
```typescript
|
||
// 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`
|
||
|
||
**问题描述**:
|
||
- 工具调用完成后没有总结或完成提示
|
||
- 用户不知道是还有更多工具调用还是已经完成
|
||
- 加载指示器停留时间不确定
|
||
|
||
**代码现象**:
|
||
```typescript
|
||
// 第321-373行:没有进度指示
|
||
for (let i = 0; i < currentResponse.tool_calls.length; i++) {
|
||
// 执行工具
|
||
// 但没有显示 "执行了 i 个/共 total 个工具"
|
||
}
|
||
```
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 添加进度计数器
|
||
<div class="tool-progress">
|
||
执行中: {{ currentToolIndex + 1 }} / {{ totalTools }}
|
||
</div>
|
||
```
|
||
|
||
#### 9. **消息历史加载卡顿**
|
||
**文件**: `src/renderer/src/views/Chat.vue: Line 117-149`
|
||
|
||
**问题描述**:
|
||
- 初始化时只加载最近50条消息,但没有给用户"加载更多"选项
|
||
- 没有提示用户历史消息被截断
|
||
- localStorage中大量数据未被利用
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第133-134行:硬编码50条限制
|
||
const recentMessages = parsed.slice(-50)
|
||
messages.value = recentMessages.map(...)
|
||
```
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 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)
|
||
- 没有自动登录状态同步
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第202-211行:搜索时才返回NOT_LOGGED_IN
|
||
if (result.error === 'NOT_LOGGED_IN') {
|
||
return {
|
||
success: false,
|
||
error: 'NOT_LOGGED_IN',
|
||
message: `搜索 ${platform} 需要登录。请先登录小黑盒账号。`
|
||
}
|
||
}
|
||
```
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 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会很长,难以查找
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 只显示前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
|
||
- 长文章显示时没有分页或折叠
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第54行:纯文本显示,丢失格式
|
||
<div class="content-text">{{ data.article.content }}</div>
|
||
```
|
||
|
||
**优化**:
|
||
```vue
|
||
<!-- 支持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次
|
||
- 没有单一的真实源头
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 创建 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检查
|
||
- 错误消息格式不一致
|
||
- 没有统一的错误处理策略
|
||
|
||
**代码示例**:
|
||
```typescript
|
||
// 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 || '搜索失败'
|
||
}
|
||
}
|
||
```
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 创建统一的错误处理工具
|
||
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
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第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
|
||
}
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 创建共享类型文件 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'`
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 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次?
|
||
```
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 创建 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()`的逻辑可以简化
|
||
- 时间比较逻辑混乱
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第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 }
|
||
}
|
||
```
|
||
|
||
**优化**:
|
||
```typescript
|
||
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. **缺少单元测试和错误覆盖**
|
||
**文件**: 项目整体
|
||
|
||
**问题描述**:
|
||
- 没有单元测试文件
|
||
- 没有集成测试
|
||
- 错误情况未经过测试
|
||
|
||
**优化方案**:
|
||
```typescript
|
||
// 创建 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中,易丢失
|
||
|
||
**建议**:
|
||
```typescript
|
||
// 添加导出功能
|
||
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. **缺少对话收藏/标记功能**
|
||
**问题描述**:
|
||
- 用户无法标记重要对话
|
||
- 没有分类功能
|
||
- 对话历史平铺,难以找到之前的对话
|
||
|
||
**建议**:
|
||
```typescript
|
||
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库但没有配置高亮器
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 安装 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快捷键
|
||
- 没有快捷键提示菜单
|
||
- 可发现性差
|
||
|
||
**建议**:
|
||
```typescript
|
||
// 添加快捷键帮助面板
|
||
const showShortcutHelp = () => {
|
||
ElDialog.confirm('快捷键列表', 'OK').then(() => {
|
||
// 显示快捷键列表
|
||
})
|
||
}
|
||
|
||
// 在Chat组件中按 ? 显示帮助
|
||
window.addEventListener('keydown', (e) => {
|
||
if (e.key === '?') {
|
||
showShortcutHelp()
|
||
}
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
### 🟡 P1 - 建议添加的功能
|
||
|
||
#### 23. **添加消息搜索功能**
|
||
```typescript
|
||
const searchMessages = (query: string) => {
|
||
return messages.value.filter(msg =>
|
||
msg.content.toLowerCase().includes(query.toLowerCase())
|
||
)
|
||
}
|
||
```
|
||
|
||
#### 24. **添加对话主题切换**
|
||
```typescript
|
||
const themes = {
|
||
light: { /* ... */ },
|
||
dark: { /* ... */ }
|
||
}
|
||
|
||
const setTheme = (name: keyof typeof themes) => {
|
||
localStorage.setItem('app-theme', name)
|
||
// 应用主题
|
||
}
|
||
```
|
||
|
||
#### 25. **添加消息撤回/编辑功能**
|
||
```typescript
|
||
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崩溃后不会重新创建
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第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()
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 添加重试逻辑和错误恢复
|
||
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错误、超时等
|
||
- 用户无法采取相应的行动
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第230-234行:所有错误都一样
|
||
const response = await fetch(endpoint, {
|
||
// ...
|
||
})
|
||
|
||
if (!response.ok) {
|
||
const errorText = await response.text()
|
||
throw new Error(`API 请求失败: ${response.status} ${errorText}`)
|
||
}
|
||
```
|
||
|
||
**优化**:
|
||
```typescript
|
||
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不断生成工具调用,会浪费资源
|
||
- 没有检测无限循环的模式
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第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('工具调用次数过多,已停止')
|
||
}
|
||
}
|
||
```
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 检测模式:如果连续的工具调用相同,则认为可能是循环
|
||
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中登录状态检查不够频繁
|
||
- 没有自动刷新登录状态
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 在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检查
|
||
|
||
**代码**:
|
||
```typescript
|
||
// 第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) })
|
||
}
|
||
}
|
||
```
|
||
|
||
**优化**:
|
||
```typescript
|
||
// 使用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周内
|
||
6. **P1-01**: 缓存搜索结果和文章
|
||
7. **P1-02**: 添加工具调用进度提示
|
||
8. **P1-03**: 改进文章显示(支持Markdown)
|
||
9. **P1-04**: 提取魔法字符串为常量
|
||
10. **P1-05**: 工具调用死循环检测
|
||
|
||
### 第三阶段(低优先级)- 1个月内
|
||
11. **P2-01**: 添加对话导出/导入功能
|
||
12. **P2-02**: 实现多对话会话管理
|
||
13. **P2-03**: 添加消息搜索功能
|
||
14. **P2-04**: 语法高亮
|
||
15. **P2-05**: 单元测试
|
||
|
||
---
|
||
|
||
## 检查清单
|
||
|
||
- [ ] 实现防抖scrollToBottom
|
||
- [ ] 优化localStorage存储结构
|
||
- [ ] 创建统一的types/index.ts
|
||
- [ ] 创建AppError错误类
|
||
- [ ] 创建constants.ts文件
|
||
- [ ] 添加登录状态检查
|
||
- [ ] 实现搜索结果缓存
|
||
- [ ] 添加工具调用重试逻辑
|
||
- [ ] 改进文章预览显示
|
||
- [ ] 添加单元测试框架
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
该项目是一个功能完整的AI对话工具,但存在以下主要问题:
|
||
|
||
1. **性能**: 频繁重渲染、localStorage同步写入、浏览器进程管理不善
|
||
2. **UX**: 缺少加载状态、错误恢复、登录状态提示
|
||
3. **代码质量**: 类型定义重复、魔法字符串、错误处理不一致
|
||
4. **功能完善度**: 缺少导出、会话管理、语法高亮
|
||
|
||
建议按照优先级逐步改进,特别是第一阶段的5个关键问题应该立即修复。
|