Files
zclaw_openfang/docs/superpowers/specs/2026-04-02-chatstore-refactor-design.md
iven 0a04b260a4
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
refactor(desktop): ChatStore structured split + IDB persistence + stream cancel
Split monolithic chatStore.ts (908 lines) into 4 focused stores:
- chatStore.ts: facade layer, owns messages[], backward-compatible selectors
- conversationStore.ts: conversation CRUD, agent switching, IndexedDB persistence
- streamStore.ts: streaming orchestration, chat mode, suggestions
- messageStore.ts: token tracking

Key fixes from 3-round deep audit:
- C1: Fix Rust serde camelCase vs TS snake_case mismatch (toolStart/toolEnd/iterationStart)
- C2: Fix IDB async rehydration race with persist.hasHydrated() subscribe
- C3: Add sessionKey to partialize to survive page refresh
- H3: Fix IDB migration retry on failure (don't set migrated=true in catch)
- M3: Fix ToolCallStep deduplication (toolStart creates, toolEnd updates)
- M-NEW-2: Clear sessionKey on cancelStream

Also adds:
- Rust backend stream cancellation via AtomicBool + cancel_stream command
- IndexedDB storage adapter with one-time localStorage migration
- HMR cleanup for cross-store subscriptions
2026-04-03 00:24:16 +08:00

650 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ChatStore 结构化重构设计
> 日期: 2026-04-02
> 状态: Complete (Phase 0-8, 三轮审计通过)
> 范围: desktop/src/store/chatStore.ts 及关联文件
## 1. 背景
ChatStore908 行)是 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 双路径统一策略
统一 KernelClientTauri 事件)和 GatewayClientWebSocket的流式体验使两条路径具备相同能力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 # 保留为 facadere-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 的自定义 storagelocalStorage 仅存元数据。包含存储配额检查:写入前估算数据大小,超过 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 | 后端 cancelRust 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 导出)