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

24 KiB
Raw Permalink Blame History

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确保渐进迁移

// 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

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.tschatStream 方法:

  • 监听 Tauri stream:chunk 事件
  • TokenUsage 适配: 现有 kernel-types.tsonComplete 使用位置参数 (inputTokens?: number, outputTokens?: number)KernelStreamAdapter 内部将其转换为 TokenUsage 对象 { inputTokens, outputTokens }
  • 映射 StreamChatEventStreamCallbacks:
    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:

const client = getClient()
const adapter = client instanceof KernelClient
  ? kernelStreamAdapter
  : gatewayStreamAdapter

5. 类型统一

5.1 统一 ChatMessage 类型

文件: desktop/src/types/chat.ts

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 辅助类型

// 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 路径的会话历史使用 SessionMessageAPI 响应格式,字符串日期),需要映射函数:

// 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.tsChatMessage
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

// 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
// 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.sendMessageonComplete 回调在流式响应完成后触发以下副作用。这些副作用在 streamStore 内部处理,不属于 StreamingAdapter 的职责。

// 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 在发起流式请求之前检查离线状态:

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 状态补充

suggestionschatMode 归属 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 个消费者文件无需修改。

// 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 导出)