DeepChat IM 风格 Steer 消息实现方案:把“转向命令“变成带已读回执的持久用户消息
2026/9/17 8:14:20 网站建设 项目流程

DeepChat IM 风格 Steer 消息实现方案:把"转向命令"变成带已读回执的持久用户消息

【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat

本文基于 DeepChat 仓库中的实现计划文档 IM-style Steer Messages Implementation Plan,完整解析"IM 风格 Steer 消息"这一功能的设计决策与落地方案:为什么要把运行时中的"转向(Steer)输入"从一条后台命令改造成一条带Unread -> Read回执的标准用户消息;持久层如何用两个 SQLite 事务(接受、认领)保证消息顺序与原子性;DeepChat 与 ACP 两种后端如何在"安全边界"上完成回合交接;渲染器如何用类型化路由响应与批量消息变更事件驱动 UI。读完本文,你将掌握该功能从共享数据契约、会话数据生命周期、运行时会话交接,到 Vue 组件呈现的完整技术链条,并能对照仓库中的真实源码与测试验证每一处设计。配套的功能规格见 IM-style Steer Messages Spec。

1. 工程背景:问题诊断与目标

1.1 现状痛点

改造前的 Steer 路径存在两套割裂的表达:组合器(composer)显示一个"在途 Steer"转圈,待处理输入栏(pending rail)显示一条被锁定的 Steer 行,而用户真正发出的内容要等到下一个回合开始才作为普通消息出现。这使得 Steer 更像一条"后台命令",而不是消息,用户无法可靠回答三个基本问题:

  • 我的消息发出成功了吗?
  • 运行时接纳它了吗?
  • 哪一条助手回复是在回应它?

1.2 诊断(Diagnosis)

计划文档给出了明确的根因判断:

  • 根因:Steer 被表达为一条"待处理命令"(pending command),而不是一条持久的用户消息;DeepChat 使用的是通用取消(generic cancellation),而不是消息/回合交接(message/turn handoff)。
  • 正确的所有权层次:pending-input 聚合必须原子地拥有"接受"(acceptance)与"认领"(claim)两个动作;消息列表只负责渲染结果产生的 transcript 事实。
  • 受影响的消费者:DeepChat、ACP、会话数据、路由契约、渲染器消息缓存、组合器、pending 通道、消息组件。
  • 关键约束:新接受的 Steer 绝不能被排到当前回合的持久用户事实之前,也不能被排到为其响应预留的助手行之后。
  • 可复用的既有模式:现有 Steer 负载合并(payload merge)与 pending-input 优先级机制。

1.3 决策(Decision)

最终选定的方案可概括为四句话:

  1. 只保留一个"在途 Steer 批次"(open batch);
  2. 每一条被接受的提交都链接到它自己的用户消息;
  3. 批次只被认领(claim)一次;
  4. 认领时同时预留一条独立的助手行。

各维度的影响面评估:

维度影响
状态两张表加两个列 + 一个可选的消息元数据字段
DOM在现有固定高度的MessageInfo行内加一个短回执 span
渲染不引入独立 Steer 列表、不引入新的全局 store、不引入宽泛的 stream watcher
IPC扩展 Steer 路由响应 + 新增一个批量消息变更事件
依赖无新增运行时依赖

1.4 上下文地图(Context Map)

计划文档明确列出了各关注点的所有者与约束,这是理解后续所有改动的前提:

  • Vue 组件链:ChatPage->MessageList/MessageListRow->MessageItemUser->MessageInfo
  • 渲染链:一个权威有序的DisplayMessage[],由既有的消息虚拟化层做窗口化;
  • 状态源:SQLite 的 pending-input 行与 transcript 行;
  • 派生状态:MessageMetadata.inputReceipt即显示回执;
  • 事件:一个类型化的 session-message 变更事件同时更新活动视图与缓存视图;
  • 布局约束:回执变化绝不能改变行高或触发滚动校正;
  • 性能敏感区:高频助手流式更新与虚拟化消息列表;
  • 无障碍:回执行告(announcement)、焦点保持、reduced motion、准确的 blocker tooltip;
  • Electron 边界:类型化路由响应 + 类型化事件,Vue 不直接接触原始 IPC。

2. 总体架构:一次 Steer 的完整时序

计划文档用一张时序图描述了整条链路(参与者包括用户、渲染器、SessionTurn、会话数据、活动后端、pending-input pump):

触发路径为:ChatInputToolbar->useComposerSubmit.onSteer->chat.steerActiveTurn->SessionTurn-> 所选后端的 pending-input 运行时。渲染器侧的steerActiveTurn调用链可在 ChatClient 路由 与 chatService、turn.ts 中找到对应实现。

3. 共享数据契约

3.1 Pending-input 记录:两个新字段

不引入新的队列或批次实体,只在现有记录上扩展:

export interface PendingSessionInputRecord { // existing fields messageIds: string[] // 链接到本批次的用户消息 ID 列表 assistantMessageId: string | null // 认领时预留的助手消息 ID }

规则(来自计划文档):

  • Queue 记录在被提升为 Steer 之前保持messageIds = []assistantMessageId = null
  • 新的 Steer 批次从 1 个消息 ID 开始;
  • 当该记录仍为pending时,另一条 Steer 被接受会追加 1 个消息 ID,并按既有appendSteerInput行为合并负载;
  • assistantMessageId只在认领时精确赋值一次;
  • 已消费(consumed)的 Steer 记录保留两个字段,用于恢复与诊断。

3.2 用户消息元数据:回执字段

MessageMetadata增加一个可选字段:

export interface MessageMetadata { // existing fields inputReceipt?: { mode: 'steer' readAt: number | null } }

刻意引入第二个回执状态枚举。状态的来源映射只有两条:

readAt == null -> Unread(未读) readAt != null -> Read(已读),持续到渲染器侧的显示截止时间

ChatMessageRecord.status继续充当"处理围栏":

关联的 Steer 批次为 pending 或 claimed 时 -> pending 关联的批次被消费(consumed)后 -> sent

共享类型定义位于 agent-interface.d.ts。

3.3 SQLite 迁移

deepchat_pending_inputs表追加一个迁移:

ALTER TABLE deepchat_pending_inputs ADD COLUMN message_ids_json TEXT NOT NULL DEFAULT '[]'; ALTER TABLE deepchat_pending_inputs ADD COLUMN assistant_message_id TEXT;

不新增表和索引,理由:查找总是从已知的 pending-input 行出发;链接的消息 ID 是一个小而有序列表;transcript 行仍按常规消息 ID 索引。

仓库中的实际实现与计划完全一致:deepchatPendingInputs.ts 中既有表定义包含message_ids_json TEXT NOT NULL DEFAULT '[]'assistant_message_id TEXT两列,迁移语句也按此执行;迁移在 schemaCatalog.ts 中注册。

4. 原子会话数据生命周期

生命周期所有权集中在SessionPendingInputs(实现见 pendingInputs.ts)。给它注入"窄"的 transcript 与事务操作,而不是新增一个协调器类。

4.1 接受(Acceptance):acceptSteerMessage

acceptSteerMessage( sessionId: string, input: SendMessageInput, mergeItemId?: string | null ): { pendingInput: PendingSessionInputRecord message: ChatMessageRecord }

一个 SQLite 事务内完成:

  1. 校验mergeItemId(若存在)必须是同一会话下一条pending的 Steer;
  2. 创建一条用户消息:新的消息 ID、下一个 transcriptorderSeqstatus: 'pending'metadata.inputReceipt = { mode: 'steer', readAt: null }、既有的结构化UserMessageContent
  3. 创建新的 pending Steer 行,或把负载合并进开放行;
  4. 把用户消息 ID 追加进message_ids_json
  5. 通过SessionTranscript持久化用户内容投影、搜索文档与初始 Tape 事实。

事件只在事务提交之后发布。任何写入失败都会同时回滚 pending 行与 transcript 事实,路由上报失败,渲染器保留草稿。

对照源码(pendingInputs.ts 第 101–159 行),实现比计划更进一步:实际签名还接受preStreamAnchorMessageId选项,并在事务内通过materializePreStreamSource先把"当前回合的源用户事实"物化出来再创建 Steer 消息,返回值中附带sourceMessage;事务提交后调用events.publishMessagesChanged,把源消息与 Steer 消息一并推给渲染器——这正是"pre-stream 交接保持持久顺序"的落点。

4.2 认领(Claim):claimSteerBatch

用原子操作替代原先独立的 Steer claim:

claimSteerBatch( sessionId: string, itemId: string ): { pendingInput: PendingSessionInputRecord userMessages: ChatMessageRecord[] assistantMessage: ChatMessageRecord }

一个 SQLite 事务内:

  1. 重读并校验 pending 行;
  2. pending -> claimed并打上claimedAt
  3. 关闭该批次的合并窗口;
  4. 给所有链接的用户消息打上同一个inputReceipt.readAt
  5. 在最后一条链接用户消息之后创建一条新的空助手消息;
  6. 把它的 ID 写入assistant_message_id

这个事务就是"已读边界":此后被接受的 Steer 必须观察到 claimed 行,并在预留的助手行之后开新批次。

源码中对应 claimSteerInput(第 274–304 行):先校验messageIds非空且assistantMessageId未赋值(防止重复认领),随后在同一事务内createAssistantMessageclaimSteerInput({ claimedAt: readAt, assistantMessageId })markSteerMessagesRead,事务提交后才发布publishMessagesChanged,把"全部已读的用户消息 + 新助手行"作为一个批量事件推给渲染器。

4.3 结算(Settlement)

对已认领的 Steer:

  • completedabortederror三种结果都消费 pending 记录(源码中为 consumeSteerInput(第 376–389 行):事务内先settleSteerMessagesconsumeSteerInput);
  • 所有链接的用户消息标记为sent
  • 保留readAt
  • 为状态迁移追加 Tape 替换事实;
  • 助手行保持正常的完成/中断/错误投影。

计划文档特别强调:Steer 越过已读边界之后,不再使用 release-after-rollback。一条用户已经看到"已读"的消息若被自动重放,有造成 provider 侧输入重复的风险。Queue 认领保留既有的 release/retry 语义。源码中releaseClaimedInput对已产生消息或助手行的 Steer 直接抛出"Read steer input ... cannot be released.",与此决策一一对应。

4.4 冷启动对账(Legacy Reconciliation)

在会话/运行时恢复时:

  • 带空messageIds的 pending Steer:用其存储的负载创建一条Unread用户消息并挂接;
  • 没有消息的 claimed 遗留 Steer:先恢复回 pending,再创建Unread消息;
  • blocked 的遗留 Steer:转换为 blocked Queue(因为它从未通过附件接受);
  • consumed 的遗留 Steer:不合成第二条历史用户消息。

对账必须幂等。recoverInputsAfterRestart(第 391–467 行) 完整实现了这套规则,并且额外覆盖了 Queue 侧的恢复:claimed Queue 若已物化用户事实则直接消费,否则释放并进入"held"集合;claimed 的 Steer 若已有消息则结算消费,否则把未读 Steer 消息置为失败(failPendingSteerMessages)并消费——即规格中"冷重启前未认领的 Steer 内部终结为error、不再显示回执"的行为。

5. 类型化路由与事件变更

5.1 Steer 路由

扩展chat.steerActiveTurn的输出:

{ accepted: boolean message: ChatMessageRecord | null attachmentPreparation?: AttachmentPreparationSummary }

使用既有的ChatMessageRecordSchema校验。契约要点:

  • accepted: true总是附带一条已持久化的用户消息——路由响应就是活动渲染器"立即插入"的权威路径;
  • accepted: false时总是message: null

MessageStartResult可以增加可选的userMessage字段,使两个后端实现都能通过SessionTurnChatService返回同一类型化的结果;不得重载messageId,它继续表示助手响应身份。这一契约在 pendingInputAdmissionCoordinator.ts 的 steerActiveTurn 中可见:无论是活动生成中、pre-stream 中还是开放批次合并分支,返回值都携带userMessage: accepted.message

5.2 消息变更事件:sessions.messages.changed

新增一个事件:

sessions.messages.changed { sessionId: string messages: ChatMessageRecord[] version: number }

它统一承载接受、认领、结算三类投影。渲染器侧的处理规则:

  • 活动且已提交(committed)的会话:按updatedAt做单调 upsert;
  • 非活动缓存会话:使最近视图失效;
  • 过期记录:忽略;
  • 事件丢失:下一次消息 restore 仍是权威来源。

计划文档明确不复用chat.stream.completed来做用户消息生命周期更新——该事件负责助手流结算,其请求代数墓碑会把用户回执更新误判为流完成。

6. DeepChat 运行时

6.1 准入(Admission)

PendingInputAdmissionCoordinator中的改动:

  • acceptSteerMessage替换queueVisibleSteerInput持久化;
  • activeSteerPendingInputId仅保留为开放批次身份;
  • 保留当前负载合并 helper;
  • 从活动生成中的 Steer 路径移除runLifecycle.cancel(sessionId)——即接受 Steer 不再触发通用用户停止取消;
  • 仅当没有活动回合拥有该会话时才立即调度 pump;
  • pre-stream 阶段:在 Steer 接受事务内物化或复用当前 claimed Queue 用户事实,然后以pending_input为原因中止准备。

pre-stream 事务把源用户事实与 Steer 一起发布,即使助手行尚不存在也保持持久顺序;当权威源记录到达时,渲染器用它替换对应的乐观源气泡(optimistic source bubble),避免重复。源码中该分支以PENDING_INPUT_ABORT_REASON中止preStreamController,并通过accepted.sourceMessage更新 pre-stream 锚点(第 247–263 行)。

6.2 安全让出(Safe Yield)

保留既有的循环钩子:

shouldYieldForPendingInput: () => Boolean(pendingInputCoordinator.getNextSteerInput(sessionId))

工具批边界返回:

{ status: 'completed', stopReason: 'pending_input' }

同时确保纯 provider 完成的常规路径在存在等待中的 Steer 时也会调度 pending-input 排空。

6.3 Pump 与回合启动

当 Steer 拥有优先级时:

  1. 调用claimSteerBatch
  2. 清空activeSteerPendingInputId
  3. 用返回的记录构建 claim handle;
  4. messageIdsassistantMessageId传入TurnCoordinator.start

TurnCoordinator的 Steer 分支必须:

  • 加载并校验所有链接的 pending 用户消息;
  • 用合并后的 pending 负载作为newUserContent
  • 把这些 pending 记录排除在历史上下文之外;
  • 用最后一条链接用户 ID 作为 Memory 与 ViewManifest 元数据的主回合锚点;
  • 从不调用createUserMessage
  • 从不调用常规createAssistantMessage,而是使用预留的助手 ID;
  • 需要时通过createCompactionMessageAtOrderSeq(..., { shiftExistingMessages: true })在第一条链接用户消息之前插入压缩投影;
  • 流式输出只写入预留的助手消息。

普通发送与 Queue 路径保留当前的用户/助手创建行为。

6.4 终结行为(Terminal Behavior)

  • pending_input终结旧助手消息,且不带错误块;
  • pre-stream 的pending_input交接删除空的助手预留、把旧操作转回 idle,然后由 pump 认领 Steer;
  • 已认领的 Steer 一定恰好结算一次;
  • 认领之后、pre-stream 阶段的错误会向预留助手行写入终结性错误;
  • 新旧助手的请求 ID 在流生命周期跟踪中保持不同。

7. ACP 运行时

ACP 复用同一套会话数据接受/认领操作,只是边界点不同——ACP 没有同循环内的让出钩子,因此使用后端特定边界:

  1. 先持久化并显示 Steer(Unread);
  2. 若 ACP 投影尚未开始,在同一事务内物化 claimed Queue 输入的用户事实;
  3. 若有活动 prompt,调用instance.cancel('pending_input')
  4. 等待活动操作结算;
  5. 排空并认领 Steer 批次;
  6. 启动新 prompt 与新助手消息。

新增一个窄的取消原因类型:

type AcpCancelCause = 'user_stop' | 'pending_input'

pending_input取消下,兼容投影终结旧助手消息时追加通用用户取消错误块(即不显示common.error.userCanceledGeneration之类的文案)。

新 prompt 投影把已认领的 Steer 上下文传入AcpAgentInstance.sendAcpCompatibilityProjectionAdapter.begin,此时:复用链接的用户消息、复用assistantMessageId、不创建重复的 transcript 行、以合并的 pending 负载作为当前 ACP prompt、ACP 事件只流入预留助手行。直接 ACP 发送与常规 Queue 认领保持原投影创建路径不变。ACP 侧改动入口可见 acpAgentRuntime.ts(其内部同样调用了acceptSteerMessage/ 认领操作)。

跨后端共同不变量

两个后端都必须维持:

旧助手行已存在时: oldAssistant.id != newAssistant.id oldAssistant.orderSeq < every steerUser.orderSeq < newAssistant.orderSeq 助手行尚不存在时: sourceUser.orderSeq < every steerUser.orderSeq < newAssistant.orderSeq

且新回合的任何流更新都不得指向oldAssistant.id

8. 渲染器状态与 IPC 绑定

8.1 消息 store

把既有的内部 upsert 行为暴露为一个聚焦 action:

applyPersistedMessageRecords(records: ChatMessageRecord[]): void

实现要求:

  • 变更可见缓存前必须先有活动的 committed 会话;
  • 忽略比缓存updatedAt更旧的记录;
  • messageIds保持按orderSeq排序;
  • 只为变更的记录使解析后的元数据失效;
  • 持久化版本号按递增一次,而不是按条记录。

并在既有 store IPC 绑定中订阅sessions.messages.changed,含清理逻辑。store 侧回执的派生逻辑分布在 message.ts 与显示模型 displayMessage.ts / useDisplayMessages.ts 中。

8.2 组合器(Composer)

useComposerSubmit的处理顺序:

  1. 与今天一样捕获草稿;
  2. 等待chatClient.steerActiveTurn
  3. 接受成功后 upsertresult.message
  4. 清除对应草稿修订;
  5. 发起一次受保护的滚动到底请求。

保留内部steeringSessionIds互斥(或改用既有 dispatch token),但移除其可见的转圈角色。计划文档明确不加入乐观 Steer 行:本地 IPC + 同步 SQLite 接受事务能很快返回权威 ID 与顺序,避免临时 ID,并保证每条可见 Steer 都能活过重载。

8.3 Pre-stream 准入

Steer 可用性跟随活动回合状态:

会话处于 generating 且没有交互/准备门控阻止提交

首条流式更新之前:渲染器走常规路径提交 Steer;主进程先持久化当前用户事实;Steer 以Unread出现;旧的 pre-stream 操作以pending_input结束;下一条助手行从 Steer 之下开始。不引入乐观行,也不加入仅渲染器侧的顺序规则。

9. 渲染器呈现

9.1 显示模型

useDisplayMessages中解析metadata.inputReceipt,为用户显示消息加一个窄字段:

type DisplayInputReceipt = { mode: 'steer' readAt: number | null }

不要把整条 pending-input 记录传给消息行。

9.2 用户消息组件

MessageItemUser.vue(实现见 MessageItemUser.vue):

  • 只从inputReceipt派生UnreadRead,截止时间后不再渲染回执;
  • 只在最近readAt可见时持有唯一截止时间定时器;
  • 元数据变化与卸载时清除定时器;
  • message.status === 'pending'视为破坏性工具栏操作的只读态;
  • 把回执传给MessageInfo
  • Copy 通过既有MessageToolbar只读行为保持可用。

MessageInfo.vue

  • 接受可选回执 prop;
  • 在既有h-4flex 行中紧邻时间戳渲染;
  • 仅对Read迁移使用aria-live="polite"
  • 只用 opacity 过渡 token;
  • reduced motion 下禁用过渡。

无需新组件。时间参数与规格一致:Read显示到readAt + 1500 ms,150 ms 纯透明度淡出;源码中READ_RECEIPT_VISIBLE_MS = 1500即定义在 MessageItemUser.vue,剩余时长按持久化的绝对readAt计算,截止时间已过则不渲染回执——超时只是渲染器行为,没有延迟数据库写入去清除回执

9.3 Pending 通道(Pending Lane)

重构PendingInputLane.vue为只接受/渲染 Queue 项:删除 Steer 头部计数与行模板;保留 Queue 控件与 blocked Queue UI;showLane只依赖 Queue 项。pendingInputStore.steerItemsgetter 仅在仍有非视觉门控消费时保留,否则在所有调用点迁移后移除。

9.4 工具栏

ChatInputToolbar.vue:移除 Steer 转圈与aria-busy;图标与标签保持稳定;保留真实附件准备导致的禁用;移除仅 pre-stream 阶段的禁用 tooltip。

9.5 布局与滚动归属

  • 回执留在固定高度的MessageInfo行内,出现/消失不改变虚拟化行高、不触发滚动校正;
  • 一次成功的本地 Steer 提交只通过既有聊天滚动控制器请求一次滚动到底,且限定在同一 committed 会话与 restore epoch 内;
  • 回执变化从不调用scrollToBottom

9.6 无障碍

回执是文本而非纯颜色状态;Read迁移使用限定在回执上的 polite 活区播报;流式渲染期间不重复播报Unread;气泡与复制保留既有键盘行为;接受后组合器保留焦点;真实 blocker(附件准备、权限、会话不可用)保留既有禁用 tooltip。

10. 顺序与竞态处理

10.1 接受 vs 认领

在既有的每会话 pending-input 边界上串行化:

accept S1 -> 在开放批次中提交消息 S1 accept S2 -> 在同一开放批次中提交消息 S2 claim -> 关闭批次 + S1/S2 置已读 + 预留助手 B accept S3 -> 在助手 B 之后开新批次

任何路由都不得向claimed批次追加消息。

10.2 流事件

  • 助手 A 的迟到更新只在 A 的常规终结事件之前被接受;
  • 助手 B 使用新的请求/消息身份,在messageIpc中开启新的流代数;
  • 既有请求墓碑在 B 成为当前后拒绝 A 的迟到更新。

10.3 会话切换

  • 路由结果只在提交会话仍是 committed 时写可见缓存;否则使该会话最近视图失效,由 restore 加载持久记录;
  • 回执截止时间使用持久化的readAt从不使用挂载时长。

10.4 停止与上一回合错误

  • 用户 Stop 先结算当前助手,然后允许UnreadSteer 批次排空;
  • 上一回合的终结性错误同样调度持久 Steer,除非仍存在权限/问题 blocker;
  • 不会因上一个回答失败就静默丢弃 pending 消息。

11. 文件级变更地图

计划文档给出的变更地图如下(它是地图,不是"必须触碰每个文件"的要求——若既有 port 已承载所需数据则不必改动):

区域主要文件变更
共享类型src/shared/types/agent-interface.d.tsPending 链接与回执元数据
路由契约src/shared/contracts/routes/chat.routes.ts返回被接受的用户消息
事件契约src/shared/contracts/events/sessions.events.ts批量消息变更事件
Pending 表src/main/session/data/tables/deepchatPendingInputs.ts新增两列/迁移
Schema 目录src/main/data/schemaCatalog.ts注册迁移
Pending storesrc/main/session/data/pendingInputStore.ts编解码链接、关闭合并窗口
Pending 生命周期src/main/session/data/pendingInputs.ts原子 accept / claim / settle / reconcile
Transcriptsrc/main/session/data/transcript.tsPending 用户创建、回执/状态更新
会话组合src/main/session/data/index.ts注入窄的 transcript/事务/事件 port
会话路由src/main/session/chatService.ts、turn.ts、routes.ts携带被接受消息
DeepChat 准入src/main/agent/deepchat/runtime/pendingInputAdmissionCoordinator.ts不带活动取消地持久化
DeepChat pumpsrc/main/agent/deepchat/runtime/pendingInputPump.ts认领批次、预留助手
DeepChat 回合src/main/agent/deepchat/runtime/turnCoordinator.ts复用可见事实/新助手
ACP 运行时src/main/agent/acp/instance/acpAgentRuntime.ts带原因感知的交接与排空
ACP 实例src/main/agent/acp/instance/acpAgentInstance.ts复用已认领投影事实
ACP 投影src/main/agent/acp/compatibility/adapters.ts无重复行/无取消错误
渲染器客户端src/renderer/api/ChatClient.tsSessionClient.ts新响应/事件绑定
消息 storesrc/renderer/src/stores/ui/message.ts、messageIpc.ts单调持久化 upsert
组合器src/renderer/src/features/chat-page/composables/useComposerSubmit.ts插入被接受消息
显示模型displayMessage.ts、useDisplayMessages.ts派生回执
消息 UIMessageItemUser.vue、MessageInfo.vue渲染回执、锁定操作
Queue UIPendingInputLane.vueChatPage.vue移除 Steer 栏
工具栏ChatInputToolbar.vue移除可见 Steer 加载
i18nsrc/renderer/src/i18n/*/chat.json回执文案(未读/已读)

12. 测试策略

计划文档按层规划了聚焦测试,这里保留其完整清单作为验收基线:

12.1 会话数据

  • 新 Steer 批次 + 用户消息的原子创建;
  • 两条不同链接消息 ID 下的负载合并;
  • 事务回滚后两个行都不存在;
  • 认领对全部消息打同一个readAt且只创建一个助手;
  • 认领后的接受在该助手之后开新批次;
  • 消费把所有链接用户消息置sent
  • pre-stream 接受先物化并链接 claimed 源用户,再接纳 Steer;
  • 遗留 pending/claimed/blocked 对账的幂等性;
  • 迁移默认值与 schema catalog。

12.2 DeepChat

  • 活动中的 Steer 不调用通用 run 取消;
  • 纯内容回合在 provider 完成后排空;
  • 工具循环在当前工具批后以pending_input让出;
  • 新旧助手 ID 不同;
  • 合并负载只供给一次;
  • 链接的 pending 用户消息被排除在历史之外;
  • 压缩分隔线插在第一条 Steer 消息之前;
  • pre-stream Steer 以pending_input取消准备、保持源/Steer 顺序、不留空助手行;
  • 认领后的 pre-stream 失败保留用户事实并写入助手错误;
  • Stop 与上一回合错误仍会排空UnreadSteer。

12.3 ACP

  • Steer 消息在取消前持久化;
  • pre-projection Steer 在取消前物化 claimed 源用户;
  • pending_input取消结算且无用户取消错误文案;
  • 认领等待活动 ACP 操作;
  • 新投影复用链接用户消息与预留助手;
  • 无重复 transcript 行;
  • 取消失败时 Steer 保持Unread

12.4 渲染器 store 与 composables

  • 路由接受消息按真实 ID 与orderSeq插入;
  • 过期会话结果不变更活动视图;
  • 消息变更事件 upsert 较新记录、忽略过期记录;
  • 非活动会话的缓存失效;
  • 被接受 Steer 清除匹配草稿并请求一次滚动;
  • 失败时保留草稿;
  • 无可见 Steer 转圈;
  • 会话 generating 期间 pre-stream Steer 始终可用。

12.5 组件

Unread回执;Read迁移;1.5 秒截止时间与 150 ms 淡出类;过期恢复的回执不渲染;reduced-motion 样式;回执不增加行高;pending Steer 工具栏仅暴露 Copy;Queue 栏不再渲染 Steer 行且 Queue 控件完整。

12.6 端到端

使用确定性测试 provider:

  1. 启动助手 A 并挂起其流;
  2. 以 Steer 提交 S1 与 S2;
  3. 断言可见顺序助手 A, S1, S2
  4. 释放安全边界;
  5. 断言两条回执都变为Read
  6. 断言助手 B 拥有不同 ID 且位于 S2 之下;
  7. 发出助手 A 的迟到更新,断言其被忽略;
  8. 推进定时器,断言回执在无滚动跳动的情况下消失。

同一顺序断言需通过一个 ACP fixture 再跑一遍。

13. 交付切分与验证门禁

实现按四个可评审切片推进:

  1. 切片 1(持久化与契约):迁移、共享类型、transcript 操作、原子 pending 生命周期、路由结果与消息变更事件、会话数据测试。建议提交:feat(chat): persist steer messages
  2. 切片 2(运行时交接):DeepChat 安全让出与已认领消息复用、ACP 带原因取消与投影复用、主进程测试。建议提交:feat(chat): split steer response turns
  3. 切片 3(渲染器交互):消息 store 事件/upsert、组合器结果处理、Queue-only 通道、回执 UI、操作锁定、i18n、pre-stream 准入、渲染器测试。建议提交:feat(chat): render steer receipts
  4. 切片 4(回归收尾):端到端顺序与重启覆盖、移除死掉的 Steer 栏/转圈代码、行为落地后更新任务状态与保留的架构文档。建议提交:test(chat): cover steer lifecycle

交接前需运行最小相关套件,完整验证门禁为:

pnpm run format pnpm run i18n pnpm run lint pnpm run typecheck pnpm exec vitest run test/main/session pnpm exec vitest run test/main/agent pnpm exec vitest run test/renderer/stores/messageStore.test.ts pnpm exec vitest run test/renderer/features/chat-page/composables/useComposerSubmit.test.ts pnpm exec vitest run test/renderer/components/ChatPage.test.ts

并手动验证:DeepChat 与 ACP 两条后端;文本、文件、提及、活动 Skills;快速连续 Steer;Queue 提升;Stop、错误、会话切换、恢复、重启;明暗主题;键盘焦点与读屏播报;reduced motion;窄窗口与调整大小的聊天视图;滚动位置与虚拟行稳定性。

14. 小结

这套方案的技术核心可以浓缩为三个设计不变量:接受事务保证"每次提交恰好一条可见用户消息且持久于一切 UI 之前";认领事务保证"已读回执、批次闭合、助手预留"三件事原子发生且不可逆;渲染器只做缓存与呈现,权威永远在 SQLite。配合oldAssistant.id != newAssistant.idorderSeq单调序这两条跨后端不变量,Steer 从一条模糊的后台命令,变成了与即时通讯体验一致、可重载、可恢复、可测试的持久会话事实。仓库中 pendingInputs.ts 的acceptSteerMessage/claimSteerInput/consumeSteerInput/recoverInputsAfterRestart与 pendingInputAdmissionCoordinator.ts 的steerActiveTurn分支,是核对上述每一项设计的最直接入口。

【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询