完善小黑盒搜索功能,将小黑盒操作作为工具给大模型
This commit is contained in:
@@ -0,0 +1,794 @@
|
||||
# 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周完成(持续改进)
|
||||
|
||||
Reference in New Issue
Block a user