795 lines
20 KiB
Markdown
795 lines
20 KiB
Markdown
# AI Desktop 项目优化优先级表
|
||
|
||
## 快速参考
|
||
|
||
| 优先级 | 类别 | 问题 | 影响 | 难度 | 预计工作量 |
|
||
|-------|------|------|------|------|----------|
|
||
| P0-1 | 性能 | Chat.vue 无限重渲染 | 🔴 高 | ⭐⭐ 中 | 2h |
|
||
| P0-2 | 性能 | localStorage 同步写入 | 🔴 高 | ⭐ 易 | 1h |
|
||
| P0-3 | 质量 | 重复类型定义 + 错误处理 | 🔴 高 | ⭐⭐⭐ 难 | 3h |
|
||
| P0-4 | UX | 登录状态提示不及时 | 🟡 中 | ⭐ 易 | 1h |
|
||
| P0-5 | 错误 | Playwright 进程崩溃 | 🔴 高 | ⭐⭐⭐ 难 | 3h |
|
||
| P1-1 | 性能 | 搜索结果缓存 | 🟡 中 | ⭐⭐ 中 | 2h |
|
||
| P1-2 | UX | 工具调用进度反馈 | 🟡 中 | ⭐ 易 | 1.5h |
|
||
| P1-3 | 功能 | 文章Markdown支持 | 🟡 中 | ⭐ 易 | 1h |
|
||
| P1-4 | 质量 | 魔法字符串提取 | 🟡 中 | ⭐ 易 | 1.5h |
|
||
| P1-5 | 错误 | 死循环检测 | 🟡 中 | ⭐⭐ 中 | 2h |
|
||
| P2-1 | 功能 | 对话导出/导入 | 🟢 低 | ⭐⭐ 中 | 3h |
|
||
| P2-2 | 功能 | 多会话管理 | 🟢 低 | ⭐⭐⭐ 难 | 5h |
|
||
| P2-3 | 功能 | 消息搜索 | 🟢 低 | ⭐ 易 | 1h |
|
||
| P2-4 | 功能 | 语法高亮 | 🟢 低 | ⭐⭐ 中 | 2h |
|
||
| P2-5 | 质量 | 单元测试 | 🟢 低 | ⭐⭐⭐ 难 | 4h |
|
||
|
||
---
|
||
|
||
## 第一阶段:立即修复(1-2天,共11小时)
|
||
|
||
### 1. P0-2:优化 localStorage 写入 [1小时]
|
||
**位置**: `src/renderer/src/views/Chat.vue: Line 467-485`
|
||
|
||
**立即行动**:
|
||
```typescript
|
||
// 只保存最近100条消息,压缩工具结果
|
||
const messagesToSave = messages.value.slice(-100)
|
||
const compressed = messagesToSave.map(msg => ({
|
||
id: msg.id,
|
||
role: msg.role,
|
||
content: msg.content,
|
||
timestamp: msg.timestamp,
|
||
toolCalls: msg.toolCalls?.map(tc => ({
|
||
name: tc.name,
|
||
status: tc.status,
|
||
result: tc.result?.substring?.(0, 100) // 只保存前100字
|
||
}))
|
||
}))
|
||
localStorage.setItem('chat-messages', JSON.stringify(compressed))
|
||
```
|
||
|
||
**期望效果**: localStorage写入速度提升3-5倍
|
||
|
||
---
|
||
|
||
### 2. P0-4:改进登录状态提示 [1小时]
|
||
**位置**: `src/renderer/src/views/Chat.vue, Settings.vue`
|
||
|
||
**立即行动**:
|
||
```typescript
|
||
// Chat.vue onMounted中添加
|
||
const isLoggedIn = ref(false)
|
||
|
||
onMounted(async () => {
|
||
// 检查登录状态
|
||
const result = await window.electron.ipcRenderer.invoke('check-platform-login', {
|
||
platform: 'xiaoheihe'
|
||
})
|
||
isLoggedIn.value = result.isLoggedIn
|
||
|
||
if (!isLoggedIn.value) {
|
||
ElMessage.warning('请登录小黑盒账号以使用搜索功能')
|
||
}
|
||
})
|
||
|
||
// 定期检查(每5分钟)
|
||
const checkInterval = setInterval(async () => {
|
||
const result = await window.electron.ipcRenderer.invoke('check-platform-login', {
|
||
platform: 'xiaoheihe'
|
||
})
|
||
isLoggedIn.value = result.isLoggedIn
|
||
}, 300000)
|
||
```
|
||
|
||
**期望效果**: 用户在输入搜索前就知道是否需要登录
|
||
|
||
---
|
||
|
||
### 3. P0-1:防抖scrollToBottom [2小时]
|
||
**位置**: `src/renderer/src/views/Chat.vue: Line 267-420`
|
||
|
||
**立即行动**:
|
||
```typescript
|
||
// 防抖滚动
|
||
let scrollTimer: ReturnType<typeof setTimeout> | null = null
|
||
const debouncedScrollToBottom = () => {
|
||
if (scrollTimer) clearTimeout(scrollTimer)
|
||
scrollTimer = setTimeout(() => {
|
||
scrollToBottom()
|
||
scrollTimer = null
|
||
}, 50)
|
||
}
|
||
|
||
// 替换所有 scrollToBottom() 为 debouncedScrollToBottom()
|
||
// 修改 onToken 回调
|
||
onToken: (token: string) => {
|
||
currentContent += token
|
||
const lastMessage = messages.value[messages.value.length - 1]
|
||
if (lastMessage && lastMessage.role === 'assistant') {
|
||
lastMessage.content = currentContent
|
||
debouncedScrollToBottom() // 改这里
|
||
}
|
||
}
|
||
```
|
||
|
||
**期望效果**: 渲染次数减少60-70%
|
||
|
||
---
|
||
|
||
### 4. P0-3:统一类型定义和错误处理 [3小时]
|
||
|
||
#### 步骤 1: 创建 `src/shared/types.ts`
|
||
```typescript
|
||
export interface Message {
|
||
id: string
|
||
role: 'user' | 'assistant' | 'system' | 'tool'
|
||
content: string | null
|
||
timestamp?: Date
|
||
tool_calls?: ToolCall[]
|
||
tool_call_id?: string
|
||
name?: string
|
||
toolCalls?: ToolCallInfo[] // 兼容旧格式
|
||
}
|
||
|
||
export interface ToolCall {
|
||
id: string
|
||
type: 'function'
|
||
function: {
|
||
name: string
|
||
arguments: string
|
||
}
|
||
}
|
||
|
||
export interface ToolCallInfo {
|
||
name: string
|
||
args?: Record<string, any>
|
||
result?: any
|
||
status: 'loading' | 'success' | 'error'
|
||
}
|
||
|
||
export interface ModelConfig {
|
||
id: string
|
||
name: string
|
||
provider: 'openai' | 'deepseek'
|
||
model: string
|
||
apiKey: string
|
||
baseUrl: string
|
||
}
|
||
|
||
export interface Settings {
|
||
activeModelId: string | null
|
||
modelConfigs: ModelConfig[]
|
||
}
|
||
```
|
||
|
||
#### 步骤 2: 创建 `src/renderer/src/utils/errors.ts`
|
||
```typescript
|
||
export class AppError extends Error {
|
||
constructor(
|
||
message: string,
|
||
public code: string,
|
||
public recoverable: boolean = false,
|
||
public context?: Record<string, any>
|
||
) {
|
||
super(message)
|
||
this.name = 'AppError'
|
||
}
|
||
}
|
||
|
||
export const createError = {
|
||
notLoggedIn: (platform: string) =>
|
||
new AppError(
|
||
`需要登录 ${platform}`,
|
||
'NOT_LOGGED_IN',
|
||
true,
|
||
{ platform }
|
||
),
|
||
networkError: (msg?: string) =>
|
||
new AppError(msg || '网络连接失败', 'NETWORK_ERROR', true),
|
||
toolError: (toolName: string) =>
|
||
new AppError(
|
||
`${toolName} 执行失败`,
|
||
'TOOL_ERROR',
|
||
true,
|
||
{ tool: toolName }
|
||
)
|
||
}
|
||
|
||
export const handleError = (error: Error) => {
|
||
if (error instanceof AppError) {
|
||
console.error(`[${error.code}] ${error.message}`, error.context)
|
||
return {
|
||
message: error.message,
|
||
recoverable: error.recoverable,
|
||
code: error.code
|
||
}
|
||
}
|
||
console.error('Unexpected error:', error)
|
||
return {
|
||
message: '发生了一个错误',
|
||
recoverable: false,
|
||
code: 'UNKNOWN'
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 步骤 3: 更新导入
|
||
- `Chat.vue`: `import type { Message } from '@/shared/types'`
|
||
- `aiService.ts`: `import type { Message, ToolCall } from '@/shared/types'`
|
||
- `tools.ts`: `import type { ToolCall } from '@/shared/types'`
|
||
|
||
**期望效果**: 类型安全提升,类型定义从3个减少到1个
|
||
|
||
---
|
||
|
||
### 5. P0-5:处理Playwright进程崩溃 [3小时]
|
||
**位置**: `src/main/index.ts: Line 396-406, 352-393`
|
||
|
||
**立即行动**:
|
||
```typescript
|
||
// 添加重试机制
|
||
async function fetchArticleContentWithRetry(
|
||
url: string,
|
||
maxRetries = 3
|
||
): Promise<any> {
|
||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||
try {
|
||
console.log(`[fetchArticle] Attempt ${attempt + 1}/${maxRetries}`)
|
||
return await fetchArticleContent(url)
|
||
} catch (error) {
|
||
if (attempt === maxRetries - 1) throw error
|
||
|
||
// 指数退避
|
||
const delay = 1000 * Math.pow(2, attempt)
|
||
console.log(`[fetchArticle] Retry after ${delay}ms`)
|
||
await new Promise(resolve => setTimeout(resolve, delay))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加disconnected事件处理
|
||
async function getPersistentContext(headless = true): Promise<BrowserContext> {
|
||
if (!persistentContext) {
|
||
persistentContext = await chromium.launchPersistentContext(userDataDir, {
|
||
headless,
|
||
viewport: { width: 1280, height: 800 },
|
||
userAgent: '...'
|
||
})
|
||
|
||
// 监听断开连接事件
|
||
persistentContext.once('disconnected', () => {
|
||
console.log('[Browser] Context disconnected')
|
||
persistentContext = null
|
||
})
|
||
}
|
||
return persistentContext
|
||
}
|
||
|
||
// 使用 fetchArticleContentWithRetry 替换 fetchArticleContent
|
||
ipcMain.handle('fetch-article', async (_, url: string) => {
|
||
try {
|
||
const result = await fetchArticleContentWithRetry(url)
|
||
return { success: true, ...result }
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: error instanceof Error ? error.message : '抓取文章失败'
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
**期望效果**: 偶发性浏览器崩溃导致的请求失败减少80%
|
||
|
||
---
|
||
|
||
## 第二阶段:改进体验(1-2周,共11小时)
|
||
|
||
### 6. P1-4:提取魔法字符串 [1.5小时]
|
||
|
||
创建 `src/renderer/src/constants.ts`:
|
||
```typescript
|
||
export const TIMEOUTS = {
|
||
DEFER_MESSAGE_LOAD: 0,
|
||
ENSURE_IME_READY: 100,
|
||
ENSURE_RENDERER_READY: 200,
|
||
DEBOUNCE_SAVE: 300,
|
||
DEBOUNCE_SCROLL: 50,
|
||
TOOL_EXECUTION_TIMEOUT: 30000
|
||
} as const
|
||
|
||
export const LIMITS = {
|
||
MAX_RECENT_MESSAGES: 50,
|
||
MAX_MESSAGE_STORAGE: 100,
|
||
MAX_TOOL_ITERATIONS: 10,
|
||
MAX_SEARCH_RESULTS_DISPLAY: 10,
|
||
ARTICLE_PREVIEW_LENGTH: 1000,
|
||
ARTICLE_RESULT_PREVIEW: 200,
|
||
SEARCH_MIN_INTERVAL: 3000,
|
||
SEARCH_MAX_PER_MINUTE: 10
|
||
} as const
|
||
|
||
export const TOOL_NAMES = {
|
||
CHECK_LOGIN: 'check_platform_login',
|
||
SEARCH: 'search_platform',
|
||
FETCH_ARTICLE: 'fetch_article'
|
||
} as const
|
||
|
||
export const PLATFORMS = {
|
||
XIAOHEIHE: 'xiaoheihe'
|
||
} as const
|
||
```
|
||
|
||
在代码中使用:
|
||
```typescript
|
||
// 之前
|
||
const recentMessages = parsed.slice(-50)
|
||
setTimeout(() => { ... }, 300)
|
||
|
||
// 之后
|
||
import { LIMITS, TIMEOUTS } from '@/constants'
|
||
const recentMessages = parsed.slice(-LIMITS.MAX_RECENT_MESSAGES)
|
||
setTimeout(() => { ... }, TIMEOUTS.DEBOUNCE_SAVE)
|
||
```
|
||
|
||
---
|
||
|
||
### 7. P1-1:搜索结果缓存 [2小时]
|
||
|
||
在 `src/renderer/src/views/ToolsPanel.vue` 中:
|
||
```typescript
|
||
// 添加缓存
|
||
const articleCache = new Map<string, any>()
|
||
const searchCache = new Map<string, any>()
|
||
|
||
const handleSearch = async () => {
|
||
if (!searchQuery.value.trim() || searchLoading.value) return
|
||
|
||
const cacheKey = `search:${searchQuery.value}`
|
||
if (searchCache.has(cacheKey)) {
|
||
searchResult.value = searchCache.get(cacheKey)
|
||
return
|
||
}
|
||
|
||
try {
|
||
searchLoading.value = true
|
||
searchResult.value = null
|
||
|
||
const toolCall = { /* ... */ }
|
||
const results = await executeToolCalls([toolCall])
|
||
const toolResult = results[0]
|
||
const parsedResult = JSON.parse(toolResult.content)
|
||
|
||
searchResult.value = parsedResult
|
||
searchCache.set(cacheKey, parsedResult)
|
||
} finally {
|
||
searchLoading.value = false
|
||
}
|
||
}
|
||
|
||
const handleArticleClick = async (url: string) => {
|
||
articleUrl.value = url
|
||
|
||
// 先检查缓存
|
||
if (articleCache.has(url)) {
|
||
articleResult.value = articleCache.get(url)
|
||
return
|
||
}
|
||
|
||
await handleFetchArticle()
|
||
}
|
||
|
||
const handleFetchArticle = async () => {
|
||
if (!articleUrl.value.trim() || fetchLoading.value) return
|
||
|
||
// 检查缓存
|
||
if (articleCache.has(articleUrl.value)) {
|
||
articleResult.value = articleCache.get(articleUrl.value)
|
||
return
|
||
}
|
||
|
||
try {
|
||
fetchLoading.value = true
|
||
articleResult.value = null
|
||
|
||
const toolCall = { /* ... */ }
|
||
const results = await executeToolCalls([toolCall])
|
||
const toolResult = results[0]
|
||
const parsedResult = JSON.parse(toolResult.content)
|
||
|
||
articleResult.value = parsedResult
|
||
articleCache.set(articleUrl.value, parsedResult)
|
||
} finally {
|
||
fetchLoading.value = false
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 8. P1-2:工具调用进度提示 [1.5小时]
|
||
|
||
修改 `src/renderer/src/views/Chat.vue`:
|
||
```typescript
|
||
// 添加进度跟踪
|
||
const toolProgress = ref({
|
||
current: 0,
|
||
total: 0
|
||
})
|
||
|
||
// 在循环前设置总数
|
||
while (currentResponse.tool_calls && /* ... */) {
|
||
toolProgress.value.total = currentResponse.tool_calls.length
|
||
|
||
for (let i = 0; i < currentResponse.tool_calls.length; i++) {
|
||
toolProgress.value.current = i + 1
|
||
|
||
try {
|
||
const results = await executeToolCalls([currentResponse.tool_calls[i]])
|
||
// ...
|
||
} catch (error) {
|
||
// ...
|
||
}
|
||
}
|
||
}
|
||
|
||
// 在模板中显示进度
|
||
<div v-if="toolProgress.total > 0" class="tool-progress">
|
||
工具执行进度: {{ toolProgress.current }} / {{ toolProgress.total }}
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 9. P1-3:文章Markdown支持 [1小时]
|
||
|
||
修改 `src/renderer/src/components/ArticleResultCard.vue`:
|
||
```vue
|
||
<template>
|
||
<div class="article-result-card">
|
||
<!-- 保持现有错误处理 -->
|
||
|
||
<div v-else-if="data.article" class="article-content">
|
||
<!-- 保持meta和stats -->
|
||
|
||
<div class="article-body">
|
||
<!-- 改这里:支持Markdown -->
|
||
<MarkdownContent
|
||
v-if="data.article.content"
|
||
:content="data.article.content"
|
||
/>
|
||
|
||
<!-- 或者如果太长,显示预览 -->
|
||
<el-collapse v-if="contentLength > 1000">
|
||
<el-collapse-item name="full-content">
|
||
<template #title>查看完整内容 ({{ contentLength }} 字)</template>
|
||
<MarkdownContent :content="data.article.content" />
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
</div>
|
||
|
||
<!-- 保持现有评论展示 -->
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import MarkdownContent from './MarkdownContent.vue'
|
||
|
||
// 添加计算属性
|
||
const contentLength = computed(() => data.article?.content?.length || 0)
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
### 10. P1-5:死循环检测 [2小时]
|
||
|
||
修改 `src/renderer/src/views/Chat.vue`:
|
||
```typescript
|
||
// 在工具调用循环中添加检测
|
||
const toolCallHistory: string[] = []
|
||
const MAX_IDENTICAL_PATTERNS = 2
|
||
|
||
while (currentResponse.tool_calls && /* ... */) {
|
||
const toolNames = currentResponse.tool_calls.map(tc => tc.function.name)
|
||
const currentPattern = toolNames.join(',')
|
||
|
||
// 检测模式重复
|
||
const lastPattern = toolCallHistory[toolCallHistory.length - 1]
|
||
if (lastPattern === currentPattern) {
|
||
const lastLastPattern = toolCallHistory[toolCallHistory.length - 2]
|
||
if (lastLastPattern === currentPattern) {
|
||
console.warn('检测到死循环模式:', currentPattern)
|
||
ElMessage.error('检测到可能的无限循环,已停止工具调用')
|
||
break
|
||
}
|
||
}
|
||
|
||
toolCallHistory.push(currentPattern)
|
||
if (toolCallHistory.length > 10) {
|
||
toolCallHistory.shift()
|
||
}
|
||
|
||
// ... 继续执行工具调用
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 第三阶段:功能完善(1个月,共15小时)
|
||
|
||
### 11. P2-1:对话导出/导入 [3小时]
|
||
|
||
创建 `src/renderer/src/utils/chatExport.ts`:
|
||
```typescript
|
||
export interface ChatExport {
|
||
version: '1.0'
|
||
exportDate: string
|
||
messages: any[]
|
||
}
|
||
|
||
export const exportChat = (messages: any[]): void => {
|
||
const data: ChatExport = {
|
||
version: '1.0',
|
||
exportDate: new Date().toISOString(),
|
||
messages
|
||
}
|
||
|
||
const json = JSON.stringify(data, null, 2)
|
||
const blob = new Blob([json], { type: 'application/json' })
|
||
const url = URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = url
|
||
a.download = `chat-export-${Date.now()}.json`
|
||
a.click()
|
||
URL.revokeObjectURL(url)
|
||
}
|
||
|
||
export const importChat = async (file: File): Promise<ChatExport> => {
|
||
const text = await file.text()
|
||
const data = JSON.parse(text) as ChatExport
|
||
return data
|
||
}
|
||
```
|
||
|
||
在 Chat.vue 中使用:
|
||
```typescript
|
||
const exportChat = () => {
|
||
chatExport.exportChat(messages.value)
|
||
ElMessage.success('对话已导出')
|
||
}
|
||
|
||
const importChat = async (file: File) => {
|
||
try {
|
||
const data = await chatExport.importChat(file)
|
||
if (data.version !== '1.0') {
|
||
throw new Error('不支持的版本')
|
||
}
|
||
|
||
messages.value = [
|
||
...messages.value,
|
||
...data.messages.map(m => ({
|
||
...m,
|
||
timestamp: new Date(m.timestamp)
|
||
}))
|
||
]
|
||
saveMessages(true)
|
||
ElMessage.success('对话已导入')
|
||
} catch (error) {
|
||
ElMessage.error('导入失败:' + error.message)
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 12. P2-3:消息搜索 [1小时]
|
||
|
||
在 Chat.vue 中添加:
|
||
```typescript
|
||
const searchQuery = ref('')
|
||
const searchResults = computed(() => {
|
||
if (!searchQuery.value) return []
|
||
|
||
const query = searchQuery.value.toLowerCase()
|
||
return messages.value.filter(msg =>
|
||
msg.content.toLowerCase().includes(query)
|
||
)
|
||
})
|
||
|
||
// 在模板中添加搜索框
|
||
<el-input
|
||
v-model="searchQuery"
|
||
placeholder="搜索消息..."
|
||
@keydown.enter="focusFirstSearchResult"
|
||
/>
|
||
|
||
<div v-if="searchQuery && searchResults.length > 0" class="search-results">
|
||
找到 {{ searchResults.length }} 条结果
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 13. P2-4:语法高亮 [2小时]
|
||
|
||
修改 `src/renderer/src/components/MarkdownContent.vue`:
|
||
```typescript
|
||
import { marked } from 'marked'
|
||
import { markedHighlight } from 'marked-highlight'
|
||
import hljs from 'highlight.js'
|
||
import 'highlight.js/styles/atom-one-dark.css'
|
||
|
||
marked.use(
|
||
markedHighlight({
|
||
langPrefix: 'hljs language-',
|
||
highlight(code, lang) {
|
||
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
|
||
return hljs.highlight(code, { language }).value
|
||
}
|
||
})
|
||
)
|
||
```
|
||
|
||
安装包:
|
||
```bash
|
||
npm install highlight.js marked-highlight
|
||
```
|
||
|
||
---
|
||
|
||
### 14. P2-2:多会话管理 [5小时]
|
||
|
||
创建 `src/renderer/src/stores/chatSessions.ts`:
|
||
```typescript
|
||
import { defineStore } from 'pinia'
|
||
import { ref, computed } from 'vue'
|
||
|
||
interface ChatSession {
|
||
id: string
|
||
title: string
|
||
createdAt: Date
|
||
updatedAt: Date
|
||
messages: Message[]
|
||
starred: boolean
|
||
tags: string[]
|
||
}
|
||
|
||
export const useChatSessions = defineStore('chatSessions', () => {
|
||
const sessions = ref<ChatSession[]>([])
|
||
const currentSessionId = ref<string | null>(null)
|
||
|
||
const currentSession = computed(() =>
|
||
sessions.value.find(s => s.id === currentSessionId.value)
|
||
)
|
||
|
||
const createSession = () => {
|
||
const session: ChatSession = {
|
||
id: Date.now().toString(),
|
||
title: `对话 ${new Date().toLocaleDateString()}`,
|
||
createdAt: new Date(),
|
||
updatedAt: new Date(),
|
||
messages: [],
|
||
starred: false,
|
||
tags: []
|
||
}
|
||
sessions.value.push(session)
|
||
currentSessionId.value = session.id
|
||
return session
|
||
}
|
||
|
||
return {
|
||
sessions,
|
||
currentSessionId,
|
||
currentSession,
|
||
createSession
|
||
}
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
### 15. P2-5:单元测试 [4小时]
|
||
|
||
安装 vitest:
|
||
```bash
|
||
npm install -D vitest @vitest/ui @testing-library/vue
|
||
```
|
||
|
||
创建 `src/renderer/src/services/__tests__/tools.test.ts`:
|
||
```typescript
|
||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||
import { ToolExecutor } from '../tools'
|
||
|
||
describe('ToolExecutor', () => {
|
||
let executor: ToolExecutor
|
||
|
||
beforeEach(() => {
|
||
executor = new ToolExecutor()
|
||
})
|
||
|
||
describe('execute', () => {
|
||
it('should handle invalid JSON arguments', async () => {
|
||
const toolCall = {
|
||
id: 'test-1',
|
||
type: 'function' as const,
|
||
function: {
|
||
name: 'search_platform',
|
||
arguments: '{invalid}'
|
||
}
|
||
}
|
||
|
||
const result = await executor.execute(toolCall)
|
||
expect(result.content).toContain('参数解析失败')
|
||
})
|
||
|
||
it('should return tool result with correct structure', async () => {
|
||
const toolCall = {
|
||
id: 'test-2',
|
||
type: 'function' as const,
|
||
function: {
|
||
name: 'check_platform_login',
|
||
arguments: JSON.stringify({ platform: 'xiaoheihe' })
|
||
}
|
||
}
|
||
|
||
const result = await executor.execute(toolCall)
|
||
expect(result.tool_call_id).toBe('test-2')
|
||
expect(result.role).toBe('tool')
|
||
expect(result.name).toBe('check_platform_login')
|
||
})
|
||
})
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 快速检查清单
|
||
|
||
### 立即检查
|
||
- [ ] 是否有频繁的console.log警告?
|
||
- [ ] localStorage中有多少条消息?
|
||
- [ ] 搜索相同内容会显示缓存吗?
|
||
- [ ] 工具调用失败时有详细错误吗?
|
||
- [ ] 未登录时搜索会有提示吗?
|
||
|
||
### 一周内完成
|
||
- [ ] 滚动是否流畅了?
|
||
- [ ] 消息保存是否快了?
|
||
- [ ] 代码中还有重复的类型吗?
|
||
- [ ] 工具调用有进度显示吗?
|
||
- [ ] 文章能显示Markdown吗?
|
||
|
||
### 一个月内完成
|
||
- [ ] 有导出/导入功能吗?
|
||
- [ ] 能搜索消息吗?
|
||
- [ ] 代码有单元测试吗?
|
||
- [ ] 代码块有语法高亮吗?
|
||
|
||
---
|
||
|
||
## 预期改进效果
|
||
|
||
| 指标 | 改进前 | 改进后 | 提升 |
|
||
|------|-------|-------|------|
|
||
| 渲染频率 (token处理) | 每token 1次 | 每50ms 1次 | ⬇️ 60-70% |
|
||
| localStorage写入 | 同步全量 | 异步压缩 | ⬇️ 60% |
|
||
| 搜索重复率 | 0% (无缓存) | 80% (缓存命中) | ⬆️ 80% |
|
||
| 类型错误 | 中等 | 几乎无 | ⬇️ 90% |
|
||
| 首次登录检查 | 无 | 启动时进行 | ✓ 新增 |
|
||
|
||
---
|
||
|
||
## 总工作量统计
|
||
|
||
| 阶段 | 项目数 | 总工时 | 优先级 |
|
||
|------|-------|--------|--------|
|
||
| 第一阶段 | 5个 | 11小时 | P0 |
|
||
| 第二阶段 | 5个 | 11小时 | P1 |
|
||
| 第三阶段 | 5个 | 15小时 | P2 |
|
||
| **合计** | **15个** | **37小时** | - |
|
||
|
||
**建议分配**:
|
||
- 第一阶段(P0): 本周完成(2-3天)
|
||
- 第二阶段(P1): 下周完成(3-4天)
|
||
- 第三阶段(P2): 第3-4周完成(持续改进)
|
||
|