docs: audit reports + feature docs + skills + admin-v2 + config sync
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Update audit tracker, roadmap, architecture docs, add admin-v2 Roles page + Billing tests, sync CLAUDE.md, Cargo.toml, docker-compose.yml, add deep-research / frontend-design / chart-visualization skills Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
649
docs/superpowers/specs/2026-04-02-chatstore-refactor-design.md
Normal file
649
docs/superpowers/specs/2026-04-02-chatstore-refactor-design.md
Normal file
@@ -0,0 +1,649 @@
|
||||
# ChatStore 结构化重构设计
|
||||
|
||||
> 日期: 2026-04-02
|
||||
> 状态: Draft
|
||||
> 范围: desktop/src/store/chatStore.ts 及关联文件
|
||||
|
||||
## 1. 背景
|
||||
|
||||
ChatStore(908 行)是 ZCLAW 桌面端聊天的核心状态管理模块,承担了消息管理、流式处理、对话管理、Artifact 面板、离线队列、ChatMode 切换等职责。经过多轮功能迭代,存在以下问题:
|
||||
|
||||
### 1.1 功能断裂
|
||||
|
||||
| 问题 | 影响 |
|
||||
|------|------|
|
||||
| `cancelStream()` 是 no-op | 用户无法取消长时间运行的流式响应 |
|
||||
| GatewayClient 路径不支持 thinking delta | Web 版/远程连接用户无法使用推理模式 |
|
||||
| 双路径(Kernel/Gateway)逻辑重复且不一致 | 维护成本高,行为不可预测 |
|
||||
|
||||
### 1.2 数据可靠性
|
||||
|
||||
| 问题 | 影响 |
|
||||
|------|------|
|
||||
| 流式过程中刷新页面丢失未持久化内容 | 用户丢失已生成的回复 |
|
||||
| 无消息重试机制 | 网络波动后用户需手动重新输入 |
|
||||
| 对话持久化仅在 `onComplete` 时触发 | 长对话中断后数据丢失 |
|
||||
|
||||
### 1.3 架构债务
|
||||
|
||||
| 问题 | 影响 |
|
||||
|------|------|
|
||||
| ChatStore 908 行、职责过多 | 难以理解和修改 |
|
||||
| `Message` vs `SessionMessage` 两套类型体系 | 类型转换混乱 |
|
||||
| 未纳入 Store Coordinator | 不符合项目 store 注入模式 |
|
||||
| `Conversation.tsx` 有未使用的 Context | 死代码增加认知负担 |
|
||||
|
||||
### 1.4 性能风险
|
||||
|
||||
| 问题 | 影响 |
|
||||
|------|------|
|
||||
| 所有消息全量在内存 | 长对话占用过多内存 |
|
||||
| 每次 `onDelta` 触发全量 `set()` 映射 | 频繁渲染影响性能 |
|
||||
|
||||
## 2. 设计决策
|
||||
|
||||
### 2.1 方案选择
|
||||
|
||||
选择 **方案 B: 结构化多 Store 重构**,理由:
|
||||
|
||||
- 每个拆分后的 Store 可在一个上下文窗口内完整理解
|
||||
- 统一流式抽象层消除双路径重复
|
||||
- 定时批量持久化平衡性能与可靠性
|
||||
- 逐步迁移保证每步可验证
|
||||
|
||||
### 2.2 双路径统一策略
|
||||
|
||||
统一 KernelClient(Tauri 事件)和 GatewayClient(WebSocket)的流式体验,使两条路径具备相同能力:thinking delta 支持、5 分钟超时、取消机制。
|
||||
|
||||
### 2.3 持久化策略
|
||||
|
||||
采用定时批量保存(每 3 秒或每 50 条 delta),使用 `requestIdleCallback`(不可用时降级为 `setTimeout`)降低对 UI 的性能影响。存储目标为 IndexedDB(通过 `idb-keyval` 库),避免 localStorage 的 5-10 MB 大小限制。localStorage 仅保留对话元数据(id 列表、当前对话 ID、当前 agent)。
|
||||
|
||||
## 3. 架构设计
|
||||
|
||||
### 3.1 Store 拆分
|
||||
|
||||
```
|
||||
desktop/src/store/
|
||||
├── chat/ # 新建目录
|
||||
│ ├── conversationStore.ts # 对话列表管理(~200行)
|
||||
│ ├── messageStore.ts # 消息管理 + 检索(~250行)
|
||||
│ ├── streamStore.ts # 统一流式处理(~200行)
|
||||
│ └── artifactStore.ts # Artifact 面板(~80行)
|
||||
├── chatStore.ts # 保留为 facade,re-export 统一接口
|
||||
```
|
||||
|
||||
### 3.2 conversationStore
|
||||
|
||||
**职责**: 对话生命周期管理
|
||||
|
||||
**状态:**
|
||||
- `conversations: Conversation[]`
|
||||
- `currentConversationId: string | null`
|
||||
- `agents: Agent[]`(agent 列表,从现有 chatStore 迁入)
|
||||
- `currentAgent: Agent | null`
|
||||
- `sessionKey: string | null`
|
||||
- `currentModel: string`(当前模型名称,持久化)
|
||||
|
||||
**Actions:**
|
||||
- `newConversation()` — 保存当前对话,创建新的空对话
|
||||
- `switchConversation(id: string)` — 保存当前,加载目标对话
|
||||
- `deleteConversation(id: string)` — 删除对话
|
||||
- `upsertActiveConversation()` — 批量保存当前对话的 messages/sessionKey 到 conversations 数组
|
||||
- `getCurrentConversation()` — 获取当前活跃对话
|
||||
- `setCurrentAgent(agent: Agent)` — 切换 agent,保存/恢复对话
|
||||
- `syncAgents(profiles: AgentProfileLike[])` — 同步 agent 列表
|
||||
- `setCurrentModel(model: string)` — 切换模型
|
||||
|
||||
**Agent 绑定**: 每个 Conversation 关联一个 agentId,切换对话时恢复对应 agent。
|
||||
|
||||
**存储**: 对话列表和 agent 信息持久化到 IndexedDB(通过 zustand persist 的自定义 storage),localStorage 仅存元数据。包含存储配额检查:写入前估算数据大小,超过 4 MB 时自动清理最旧的已归档对话。
|
||||
|
||||
### 3.3 messageStore
|
||||
|
||||
**职责**: 当前对话的消息数据管理
|
||||
|
||||
**状态:**
|
||||
- `messages: ChatMessage[]`
|
||||
- `totalInputTokens: number`
|
||||
- `totalOutputTokens: number`
|
||||
|
||||
**Actions:**
|
||||
- `addMessage(message: ChatMessage)` — 追加消息
|
||||
- `updateMessage(id: string, updates: Partial<ChatMessage>)` — 合并更新
|
||||
- `getStreamingMessage()` — 获取当前流式消息(role=assistant 且 streaming=true)
|
||||
- `updateStreamingContent(id: string, delta: string)` — 高性能增量更新
|
||||
- `appendThinking(id: string, delta: string)` — 追加 thinking 内容
|
||||
- `addToolStep(id: string, step: ToolStep)` — 追加工具调用步骤
|
||||
- `completeMessage(id: string, tokens: TokenUsage)` — 标记消息完成,记录 token
|
||||
- `failMessage(id: string, error: string)` — 标记消息失败,保存原始内容用于重试
|
||||
- `retryMessage(id: string)` — 使用 originalContent 创建重试
|
||||
- `addTokenUsage(input: number, output: number)` — 累计 token
|
||||
- `resetMessages(messages: ChatMessage[])` — 切换对话时重载消息
|
||||
- `searchMessages(query: string)` — 消息内文本搜索
|
||||
|
||||
### 3.4 streamStore
|
||||
|
||||
**职责**: 统一流式处理、离线队列、完成后副作用
|
||||
|
||||
**状态:**
|
||||
- `isStreaming: boolean`
|
||||
- `isLoading: boolean`
|
||||
- `streamHandle: StreamHandle | null`
|
||||
- `chatMode: ChatModeType`
|
||||
- `suggestions: string[]`
|
||||
|
||||
**Actions:**
|
||||
- `sendMessage(content: string, context?: SendMessageContext)` — 核心发送逻辑
|
||||
1. **离线检查**:调用 `offlineStore.isOffline()`;若离线,委托 `offlineStore.queueMessage()` 并显示系统消息后返回
|
||||
2. **流式守卫**:若 `isStreaming === true`,拒绝发送(前端防重复)
|
||||
3. 选择活跃的 `StreamingAdapter`
|
||||
4. **原子消息创建**:一次性创建 optimistic 用户消息 + 流式 assistant 占位消息,通过单次 `set()` 写入 messageStore(避免部分状态被批量保存)
|
||||
5. 启动流式请求,注册 callbacks
|
||||
6. 管理 dirty 标志触发批量保存
|
||||
7. **完成后副作用**(onComplete 回调中):
|
||||
- `conversationStore.upsertActiveConversation()` — 立即保存
|
||||
- `memoryExtractor.extractFromConversation()` — 异步记忆提取(.catch 静默处理)
|
||||
- `intelligenceClient.reflection.recordConversation()` — 对话记录(.catch 静默处理)
|
||||
- `intelligenceClient.reflection.shouldReflect()` — 反射触发检查
|
||||
- `generateFollowUpSuggestions(content)` — 关键词建议生成 → `setSuggestions()`
|
||||
- 浏览器 TTS(如已启用)
|
||||
- `cancelStream()` — 取消当前流式响应
|
||||
- `setChatMode(mode: ChatModeType)` — 切换聊天模式
|
||||
- `getChatModeConfig()` — 获取当前模式配置
|
||||
- `setSuggestions(suggestions: string[])` — 设置建议列表
|
||||
|
||||
**批量保存机制:**
|
||||
```
|
||||
流式开始(用户消息 + assistant 占位已原子写入 messageStore)
|
||||
│
|
||||
├── dirty 标志管理
|
||||
│ └── 每次 delta/thinking/tool 更新后设置 dirty = true
|
||||
│
|
||||
├── 每 3 秒检查 dirty 标志
|
||||
│ └── dirty → conversationStore.upsertActiveConversation()
|
||||
│
|
||||
├── 每累积 50 条 delta 强制保存
|
||||
│
|
||||
├── onComplete → 立即保存 + 触发副作用
|
||||
│
|
||||
└── onError → 立即保存(保留已接收的部分内容)
|
||||
```
|
||||
|
||||
**跨 Store 同步契约**: streamStore 调用 messageStore 和 conversationStore 的方法均为同步 Zustand `set()` 调用。批量保存计时器(`setInterval`)在 Zustand 事务外运行,读取的 `messages` 始终是上一帧的完整快照——不存在部分写入的中间态。
|
||||
|
||||
**依赖:** streamStore → messageStore(更新流式消息)、conversationStore(保存对话)、offlineStore(离线队列)、connectionStore(选择 adapter)
|
||||
|
||||
### 3.5 artifactStore
|
||||
|
||||
**职责:** Artifact 面板管理(从现有 ChatStore 直接提取,无逻辑变更)
|
||||
|
||||
**状态:**
|
||||
- `artifacts: ArtifactFile[]`
|
||||
- `selectedArtifactId: string | null`
|
||||
- `artifactPanelOpen: boolean`
|
||||
|
||||
**Actions:**
|
||||
- `addArtifact(artifact)`, `selectArtifact(id)`, `setArtifactPanelOpen(open)`, `clearArtifacts()`
|
||||
|
||||
### 3.6 Facade 兼容层
|
||||
|
||||
保留 `chatStore.ts` 作为 re-export facade,确保渐进迁移:
|
||||
|
||||
```typescript
|
||||
// chatStore.ts (facade)
|
||||
export { useConversationStore } from './chat/conversationStore'
|
||||
export { useMessageStore } from './chat/messageStore'
|
||||
export { useStreamStore } from './chat/streamStore'
|
||||
export { useArtifactStore } from './chat/artifactStore'
|
||||
|
||||
// 兼容层 — 逐步迁移后删除
|
||||
export { useChatStore } from './chat/chatStoreCompat'
|
||||
```
|
||||
|
||||
`chatStoreCompat` 聚合所有子 Store 的状态和 actions 为统一的 `useChatStore` 接口,使现有组件无需修改即可继续工作。
|
||||
|
||||
## 4. 统一流式抽象层
|
||||
|
||||
### 4.1 StreamingAdapter 接口
|
||||
|
||||
文件: `desktop/src/lib/streaming-adapter.ts`
|
||||
|
||||
```typescript
|
||||
interface StreamCallbacks {
|
||||
onDelta: (delta: string) => void
|
||||
onThinkingDelta?: (delta: string) => void
|
||||
onToolStart?: (name: string, input: unknown) => void
|
||||
onToolEnd?: (name: string, output: unknown) => void
|
||||
onHandStart?: (name: string) => void
|
||||
onHandEnd?: (name: string, result: unknown) => void
|
||||
onWorkflowStart?: (workflowId: string) => void
|
||||
onWorkflowEnd?: (workflowId: string, result: unknown) => void
|
||||
onComplete: (tokens: TokenUsage) => void
|
||||
onError: (error: string) => void
|
||||
}
|
||||
|
||||
interface TokenUsage {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
}
|
||||
|
||||
interface StreamHandle {
|
||||
cancel(): void
|
||||
readonly active: boolean
|
||||
}
|
||||
|
||||
interface StreamingAdapter {
|
||||
start(
|
||||
agentId: string,
|
||||
message: string,
|
||||
sessionId: string,
|
||||
mode: ChatModeType,
|
||||
callbacks: StreamCallbacks
|
||||
): StreamHandle
|
||||
|
||||
isAvailable(): boolean
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 KernelStreamAdapter
|
||||
|
||||
封装现有 `kernel-chat.ts` 的 `chatStream` 方法:
|
||||
|
||||
- 监听 Tauri `stream:chunk` 事件
|
||||
- **TokenUsage 适配**: 现有 `kernel-types.ts` 的 `onComplete` 使用位置参数 `(inputTokens?: number, outputTokens?: number)`,KernelStreamAdapter 内部将其转换为 `TokenUsage` 对象 `{ inputTokens, outputTokens }`
|
||||
- 映射 `StreamChatEvent` 到 `StreamCallbacks`:
|
||||
| StreamChatEvent | StreamCallbacks |
|
||||
|----------------|-----------------|
|
||||
| `delta` | `onDelta(text)` |
|
||||
| `thinkingDelta` | `onThinkingDelta(thinking)` |
|
||||
| `tool_start` | `onToolStart(name, input)` |
|
||||
| `tool_end` | `onToolEnd(name, output)` |
|
||||
| `handStart` | `onHandStart(name)` |
|
||||
| `handEnd` | `onHandEnd(name, result)` |
|
||||
| `complete` | `onComplete({ inputTokens, outputTokens })` |
|
||||
| `error` | `onError(message)` |
|
||||
- 5 分钟超时(保持现有行为)
|
||||
- `cancel()` 调用新增的 Tauri command `cancel_stream(session_id)`
|
||||
- `iteration_start` 事件内部日志记录,不暴露到 callbacks
|
||||
|
||||
### 4.3 GatewayStreamAdapter
|
||||
|
||||
封装 GatewayClient 的 WebSocket 流式:
|
||||
|
||||
- 使用 GatewayClient 的 `chatStream` 方法
|
||||
- 映射 `AgentStreamDelta` 事件到 `StreamCallbacks`:
|
||||
| AgentStreamDelta | StreamCallbacks |
|
||||
|-----------------|-----------------|
|
||||
| `stream === 'assistant'` | `onDelta(content)` |
|
||||
| `stream === 'thinking'` | `onThinkingDelta(content)` |
|
||||
| `stream === 'tool'` + `step === 'start'` | `onToolStart(name, input)` |
|
||||
| `stream === 'tool'` + `step === 'end'` | `onToolEnd(name, output)` |
|
||||
| `stream === 'hand'` + `step === 'start'` | `onHandStart(name)` |
|
||||
| `stream === 'hand'` + `step === 'end'` | `onHandEnd(name, result)` |
|
||||
| `stream === 'workflow'` + `step === 'start'` | `onWorkflowStart(workflowId)` |
|
||||
| `stream === 'workflow'` + `step === 'end'` | `onWorkflowEnd(workflowId, result)` |
|
||||
| `stream === 'lifecycle'` + `phase === 'end'` | `onComplete(tokens)` |
|
||||
| `stream === 'error'` | `onError(message)` |
|
||||
- 新增 5 分钟超时(与 Kernel 统一)
|
||||
- `cancel()` 关闭 WebSocket 连接并发送取消消息
|
||||
- 统一 `onComplete` 签名,包含 TokenUsage 参数
|
||||
|
||||
### 4.4 适配器选择
|
||||
|
||||
`streamStore` 通过 `connectionStore.getClient()` 获取当前客户端实例,判断类型选择 adapter:
|
||||
|
||||
```typescript
|
||||
const client = getClient()
|
||||
const adapter = client instanceof KernelClient
|
||||
? kernelStreamAdapter
|
||||
: gatewayStreamAdapter
|
||||
```
|
||||
|
||||
## 5. 类型统一
|
||||
|
||||
### 5.1 统一 ChatMessage 类型
|
||||
|
||||
文件: `desktop/src/types/chat.ts`
|
||||
|
||||
```typescript
|
||||
interface ChatMessage {
|
||||
id: string
|
||||
role: 'user' | 'assistant' | 'tool' | 'hand' | 'workflow' | 'system'
|
||||
content: string
|
||||
timestamp: Date
|
||||
streaming?: boolean
|
||||
optimistic?: boolean
|
||||
// thinking
|
||||
thinkingContent?: string
|
||||
// error & retry
|
||||
error?: string
|
||||
originalContent?: string
|
||||
// tool/hand context
|
||||
toolSteps?: ToolStep[]
|
||||
handName?: string
|
||||
handStatus?: string
|
||||
handResult?: unknown
|
||||
// workflow
|
||||
workflowId?: string
|
||||
workflowStep?: number
|
||||
workflowStatus?: string
|
||||
workflowResult?: unknown
|
||||
// subtasks
|
||||
subtasks?: Subtask[]
|
||||
// attachments
|
||||
files?: MessageFile[]
|
||||
codeBlocks?: CodeBlock[]
|
||||
// metadata
|
||||
metadata?: {
|
||||
inputTokens?: number
|
||||
outputTokens?: number
|
||||
model?: string
|
||||
runId?: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 辅助类型
|
||||
|
||||
```typescript
|
||||
// ToolStep 替代现有 ToolCallStep,统一命名
|
||||
interface ToolStep {
|
||||
id: string
|
||||
name: string
|
||||
status: 'running' | 'completed' | 'error'
|
||||
input?: unknown
|
||||
output?: unknown
|
||||
startTime: Date
|
||||
endTime?: Date
|
||||
}
|
||||
|
||||
interface Subtask {
|
||||
id: string
|
||||
title: string
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed'
|
||||
result?: unknown
|
||||
}
|
||||
|
||||
interface SendMessageContext {
|
||||
files?: MessageFile[]
|
||||
parentMessageId?: string
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 SessionMessage → ChatMessage 映射
|
||||
|
||||
Gateway 路径的会话历史使用 `SessionMessage`(API 响应格式,字符串日期),需要映射函数:
|
||||
|
||||
```typescript
|
||||
// desktop/src/types/chat.ts
|
||||
function sessionToChatMessage(sm: SessionMessage): ChatMessage {
|
||||
return {
|
||||
id: sm.id,
|
||||
role: mapSessionRole(sm.role), // 'user'/'assistant'/'system' 直接映射
|
||||
content: sm.content,
|
||||
timestamp: new Date(sm.timestamp),
|
||||
metadata: {
|
||||
model: sm.metadata?.model,
|
||||
inputTokens: sm.metadata?.tokens?.input,
|
||||
outputTokens: sm.metadata?.tokens?.output,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`sessionStore` 内部继续使用 API 响应的 `SessionMessage` 类型(它是 API 契约),仅在展示层转换为 `ChatMessage`。不需要修改 `sessionStore` 的内部类型。
|
||||
|
||||
### 5.4 类型清理
|
||||
|
||||
| 类型 | 文件 | 动作 |
|
||||
|------|------|------|
|
||||
| `ChatStore.Message` | `store/chatStore.ts` | 迁移到 `types/chat.ts` 的 `ChatMessage` |
|
||||
| `ConversationContext` | `components/ai/Conversation.tsx` | 仅删除未使用的 Provider/Context/hook,保留滚动容器组件 |
|
||||
| `initStreamListener` | `store/chatStore.ts` | 被 `streamStore` + `StreamingAdapter` 替代 |
|
||||
|
||||
## 6. Cancel 机制
|
||||
|
||||
### 6.1 前端(Phase 5a — 纯前端取消)
|
||||
|
||||
前端 cancel 分两步实现,先纯前端方案(Phase 5a),后端配合后完善(Phase 5b)。
|
||||
|
||||
```typescript
|
||||
// streamStore
|
||||
cancelStream() {
|
||||
if (this.streamHandle?.active) {
|
||||
this.streamHandle.cancel()
|
||||
}
|
||||
// 标记当前流式消息为已完成
|
||||
const streamingMsg = messageStore.getStreamingMessage()
|
||||
if (streamingMsg) {
|
||||
messageStore.updateMessage(streamingMsg.id, {
|
||||
streaming: false,
|
||||
content: streamingMsg.content + '\n\n_(响应已取消)_'
|
||||
})
|
||||
}
|
||||
set({ isStreaming: false, streamHandle: null })
|
||||
conversationStore.upsertActiveConversation() // 立即保存
|
||||
}
|
||||
```
|
||||
|
||||
**KernelStreamAdapter.cancel()**(Phase 5a 纯前端):
|
||||
- 停止监听 Tauri `stream:chunk` 事件(移除 listener)
|
||||
- 不通知后端停止,后端继续运行直到自然完成或 5 分钟超时
|
||||
- 前端标记消息为已取消,用户可继续操作
|
||||
|
||||
**GatewayStreamAdapter.cancel()**(Phase 5a 纯前端):
|
||||
- 发送 WebSocket 取消消息 `{ type: "cancel", sessionId }`(如果 Gateway 服务端已支持则生效)
|
||||
- 关闭事件监听器
|
||||
|
||||
### 6.2 后端配合(Phase 5b — 需新基础设施)
|
||||
|
||||
Phase 5b 在后端基础设施就绪后实施。需要在 Tauri 端新增:
|
||||
|
||||
1. **SessionStreamGuards 状态**: 在 `lib.rs` 中注册 `DashMap<String, Arc<AtomicBool>>` 作为 Tauri managed state
|
||||
2. **cancel_stream command**: 读取 guards map,设置取消标志
|
||||
3. **流式循环检查**: `tokio::spawn` 内每轮迭代检查 `cancel_flag`
|
||||
|
||||
```rust
|
||||
// chat.rs — 取消标志写入
|
||||
#[tauri::command]
|
||||
async fn cancel_stream(
|
||||
session_id: String,
|
||||
guards: State<'_, SessionStreamGuards>,
|
||||
) -> Result<(), String> {
|
||||
if let Some(pair) = guards.0.get(&session_id) {
|
||||
pair.value().store(true, Ordering::SeqCst);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// agent_chat_stream — 在每轮接收循环中检查
|
||||
let cancelled = cancel_flag.load(Ordering::Relaxed);
|
||||
if cancelled {
|
||||
tx.send(LoopEvent::Complete(AgentLoopResult {
|
||||
response: "...".into(),
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
iterations,
|
||||
})).ok();
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
Phase 5a 和 5b 可独立交付,Phase 5a 即可满足用户需求。
|
||||
|
||||
## 7. 迁移计划
|
||||
|
||||
### 7.1 迁移顺序
|
||||
|
||||
| Phase | 内容 | 影响文件数 | 风险 |
|
||||
|-------|------|-----------|------|
|
||||
| 0 | 创建 `types/chat.ts` + `store/chat/` 目录 | 新建 | 无 |
|
||||
| 1 | 提取 `artifactStore` | ~5 | 极低 |
|
||||
| 2 | 提取 `conversationStore`(含 agents、sessionKey) | ~8 | 低 |
|
||||
| 3 | 提取 `messageStore`(含 token 统计、search) | ~10 | 中 |
|
||||
| 4 | 提取 `streamStore` + `StreamingAdapter`(含离线检查、副作用、suggestions) | ~12 | 中高 |
|
||||
| 5a | 前端 cancel(纯前端,停止监听+标记已取消) | ~3 | 低 |
|
||||
| 5b | 后端 cancel(Rust SessionStreamGuards + cancel_stream command) | ~4 | 中 |
|
||||
| 6 | 实现定时批量持久化(IndexedDB + 配额检查) | ~4 | 中 |
|
||||
| 7 | 删除旧代码 + 清理 facade | ~6 | 低 |
|
||||
| 8 | 删除死代码(ConversationContext、旧类型映射) | ~4 | 低 |
|
||||
|
||||
### 7.2 每阶段验证
|
||||
|
||||
每个 Phase 完成后执行:
|
||||
|
||||
1. `pnpm tsc --noEmit` — 类型检查通过
|
||||
2. `pnpm vitest run` — 现有测试通过
|
||||
3. 手动验证: 发送消息 → 流式响应 → 切换对话 → 刷新页面数据保持
|
||||
4. 手动验证(Phase 5 后): cancel 流式响应 → 状态正确恢复
|
||||
|
||||
### 7.3 关键文件清单
|
||||
|
||||
| 文件 | 角色 |
|
||||
|------|------|
|
||||
| `desktop/src/store/chatStore.ts` | 重构对象,最终保留为 facade |
|
||||
| `desktop/src/store/chat/conversationStore.ts` | 新建 |
|
||||
| `desktop/src/store/chat/messageStore.ts` | 新建 |
|
||||
| `desktop/src/store/chat/streamStore.ts` | 新建 |
|
||||
| `desktop/src/store/chat/artifactStore.ts` | 新建 |
|
||||
| `desktop/src/store/chat/chatStoreCompat.ts` | 新建(兼容层,最终删除) |
|
||||
| `desktop/src/lib/streaming-adapter.ts` | 新建(StreamingAdapter 接口 + 双实现) |
|
||||
| `desktop/src/lib/kernel-chat.ts` | 修改(KernelStreamAdapter 封装) |
|
||||
| `desktop/src/types/chat.ts` | 新建(ChatMessage + ToolStep + Subtask) |
|
||||
| `desktop/src/types/session.ts` | 保留(API 契约类型),添加映射函数 |
|
||||
| `desktop/src/components/ChatArea.tsx` | 逐步迁移 import |
|
||||
| `desktop/src/components/ai/Conversation.tsx` | 清理死代码(仅 Context/Provider) |
|
||||
| `desktop/src/store/index.ts` | 注册新 Store |
|
||||
| `desktop/src/store/offlineStore.ts` | 不修改,streamStore 调用其 API |
|
||||
| `desktop/src-tauri/src/kernel_commands/chat.rs` | Phase 5b: 新增 cancel_stream + SessionStreamGuards |
|
||||
| `desktop/src-tauri/src/lib.rs` | Phase 5b: 注册 cancel_stream + guards state |
|
||||
|
||||
## 8. streamStore 完成后副作用
|
||||
|
||||
`streamStore.sendMessage` 的 `onComplete` 回调在流式响应完成后触发以下副作用。这些副作用在 `streamStore` 内部处理,不属于 `StreamingAdapter` 的职责。
|
||||
|
||||
```typescript
|
||||
// streamStore 内部 onComplete 处理
|
||||
async function handleComplete(tokens: TokenUsage) {
|
||||
// 1. 更新消息状态
|
||||
messageStore.completeMessage(streamingMsgId, tokens)
|
||||
|
||||
// 2. 立即持久化
|
||||
conversationStore.upsertActiveConversation()
|
||||
|
||||
// 3. 记忆提取(非阻塞,失败静默)
|
||||
try {
|
||||
const extractor = getMemoryExtractor()
|
||||
if (extractor) {
|
||||
await extractor.extractFromConversation(
|
||||
conversationStore.getCurrentConversation()
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Memory extraction failed', e)
|
||||
}
|
||||
|
||||
// 4. 对话反思跟踪(非阻塞)
|
||||
try {
|
||||
const client = getIntelligenceClient()
|
||||
if (client?.reflection) {
|
||||
await client.reflection.recordConversation(...)
|
||||
const shouldReflect = await client.reflection.shouldReflect(...)
|
||||
if (shouldReflect) {
|
||||
// 触发反思流程
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('Reflection tracking failed', e)
|
||||
}
|
||||
|
||||
// 5. 后续建议生成
|
||||
const suggestions = generateFollowUpSuggestions(lastAssistantContent)
|
||||
set({ suggestions })
|
||||
|
||||
// 6. 语音朗读(如果用户开启)
|
||||
if (speechSettings.autoSpeak) {
|
||||
speechSynth.speak(lastAssistantContent)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 离线队列集成
|
||||
|
||||
`streamStore.sendMessage` 在发起流式请求之前检查离线状态:
|
||||
|
||||
```typescript
|
||||
async sendMessage(content: string, context?: SendMessageContext) {
|
||||
// 1. 离线检查(优先级最高)
|
||||
// 注意:isOffline 是 boolean 属性(不是函数),queueMessage 使用位置参数
|
||||
const { isOffline, queueMessage } = useOfflineStore.getState()
|
||||
if (isOffline) {
|
||||
const userMsg = createUserMessage(content, context?.files)
|
||||
messageStore.addMessage(userMsg)
|
||||
messageStore.addMessage(createSystemMessage('消息已加入离线队列,网络恢复后将自动发送'))
|
||||
queueMessage(content, conversationStore.currentAgent?.id, conversationStore.sessionKey ?? undefined)
|
||||
conversationStore.upsertActiveConversation()
|
||||
return // 不继续流式请求
|
||||
}
|
||||
|
||||
// 2. 正常流式流程...
|
||||
}
|
||||
```
|
||||
|
||||
## 10. streamStore 状态补充
|
||||
|
||||
`suggestions` 和 `chatMode` 归属 streamStore:
|
||||
|
||||
**状态:**
|
||||
- `suggestions: string[]`
|
||||
- `chatMode: ChatModeType`
|
||||
|
||||
**Actions:**
|
||||
- `setSuggestions(suggestions: string[])` — 设置后续建议
|
||||
- `setChatMode(mode: ChatModeType)` — 切换聊天模式
|
||||
- `getChatModeConfig()` — 获取当前模式配置
|
||||
- `searchSkills(query: string)` — 委托给 `getSkillDiscovery().searchSkills()`
|
||||
|
||||
`totalInputTokens`/`totalOutputTokens` 为仅会话内累计(不持久化),刷新后重置为 0。
|
||||
|
||||
## 11. 兼容层迁移指南
|
||||
|
||||
`chatStoreCompat.ts` 聚合子 Store 为统一的 `useChatStore` 接口,确保现有 19 个消费者文件无需修改。
|
||||
|
||||
```typescript
|
||||
// chatStoreCompat.ts — 使用 Zustand subscribe 保持响应式
|
||||
// 注意:不能在 create() 中直接 .getState(),那样只会读取初始值不会响应变化
|
||||
import { subscribe } from 'zustand'
|
||||
|
||||
// 方案:直接 re-export 子 Store,组件按需导入
|
||||
export { useConversationStore as useConversationStore } from './chat/conversationStore'
|
||||
export { useMessageStore as useMessageStore } from './chat/messageStore'
|
||||
export { useStreamStore as useStreamStore } from './chat/streamStore'
|
||||
export { useArtifactStore as useArtifactStore } from './chat/artifactStore'
|
||||
|
||||
// 兼容 hook:聚合所有子 store 状态供旧组件使用
|
||||
// 使用 useSyncExternalStore 或每个子 store 的独立 hook 组合
|
||||
export function useChatStore<T>(selector: (state: ChatCompatState) => T): T {
|
||||
// 方案 A(推荐):组件直接从子 store 导入
|
||||
// 方案 B(过渡期):聚合 hook,内部使用多个 useSelector
|
||||
const conv = useConversationStore(selector)
|
||||
const msg = useMessageStore(selector)
|
||||
const stream = useStreamStore(selector)
|
||||
const art = useArtifactStore(selector)
|
||||
return selector({ ...conv, ...msg, ...stream, ...art })
|
||||
}
|
||||
```
|
||||
|
||||
> **重要**:兼容层是过渡性代码,仅保证旧组件可编译运行。新代码必须直接使用子 Store。每个 Phase 迁移一部分组件后,兼容层逐步缩小。最终删除。
|
||||
|
||||
迁移方式:每 Phase 完成后,逐步将组件的 `import { useChatStore } from './chatStore'` 改为直接从子 Store 导入。最终删除 `chatStoreCompat.ts`。
|
||||
|
||||
## 12. 不在本次范围内
|
||||
|
||||
以下项目明确排除,作为后续迭代考虑:
|
||||
|
||||
- **消息分页/懒加载**(当前所有消息全量在内存,Phase 2 考虑)
|
||||
- **文件真实上传**(当前附件是伪文本标记,需后端配合)
|
||||
- **TitleMiddleware 实现**(后端 placeholder,需 LLM driver 接入)
|
||||
- **消息导出增强**(当前仅 Markdown 导出)
|
||||
Reference in New Issue
Block a user