Daemon 共享 UI 状态
把 SSE 事件归一化为 transcript blocks,处理排序、订阅与重同步。
@qwen-code/sdk/daemon 提供与具体界面分离的 UI 原语。它把 daemon 事件转换为语义化 transcript;纯状态层不依赖 React 或 DOM,宿主可选择自己的渲染方式。
三层职责
| 层 | 入口 | 职责 |
|---|---|---|
| 归一化 | normalizeDaemonEvent | 把 wire envelope 转为 DaemonUiEvent 数组 |
| 状态 | reduceDaemonTranscriptEvents / createDaemonTranscriptStore | 累积消息、工具、审批和状态块 |
| 展示 | daemonBlockToMarkdown / daemonBlockToHtml / daemonBlockToPlainText | 把语义块投影为展示内容 |
宿主可只使用归一化层,也可使用完整 store。Web Shell 是实际消费者;共享原语的存在不代表 native TUI、渠道和 IDE 默认路径已全部迁移。
连接 store
以下函数接收已建立的 DaemonSessionClient 与宿主渲染回调:
import {
createDaemonTranscriptStore,
normalizeDaemonEvent,
} from '@qwen-code/sdk/daemon';
async function consumeSession(session, signal, render) {
const store = createDaemonTranscriptStore();
const unsubscribe = store.subscribe(() => render(store.getSnapshot()));
try {
for await (const envelope of session.events({ signal })) {
store.dispatch(normalizeDaemonEvent(envelope, {
clientId: session.clientId,
suppressOwnUserEcho: true,
}));
}
} finally {
unsubscribe();
}
return store.getSnapshot();
}先注册 subscriber,再消费持续事件流,避免订阅代码被无限循环挡住。当前 store 实现提供 getSnapshot();部分架构说明中的 getState() 示例与该实现不符。
subscribe 返回取消订阅函数;dispatch 接受单个或多个 UI event,并通过 microtask 合并通知。宿主仍负责停止事件流、处理连接错误和生命周期。
排序与时间
使用 selectTranscriptBlocksOrderedByEventId(state) 按 daemon 的 eventId 排序。createdAt 是 clientReceivedAt 的旧别名,不能用客户端接收时间保证多个客户端或重放后的相同顺序。
显示时间优先使用 serverTimestamp,缺失时回退 clientReceivedAt。formatBlockTimestamp 支持 locale、timeZone 等显示选项;SDK 能读取服务器时间不意味着每种旧 daemon envelope 都已写入该字段。
查询状态
selectCurrentTool、selectApprovalMode、selectToolProgress 和 selectPendingPermissionBlocks 分别提取当前工具、审批模式、进度及未解决审批。running、in_progress、pending、confirming 等状态使工具成为进行中;终态清理当前指针,未知状态不擅自清空它。
session、workspace 和 auth 类事件中有不少属于旁路观察信息,不会自动追加聊天正文。宿主应选择需要显示的设置状态、提示条或认证交互,不能假定每个事件都对应一条可见消息。
重同步顺序
session.state_resync_required 会设置 awaitingResync,并暂停处理普通增量事件。宿主要选择恢复策略:reset() 清空本地状态,或 clearAwaitingResync() 保留本地块并重新接受事件。
必须在新 SSE 流或 Last-Event-ID: 0 重放开始传递事件之前解除该状态;等重放结束才解除会丢掉重放内容。重新获取的是有界窗口,不是无限历史。窗口中的 history_truncated 仅显示历史裁剪状态,不能再次触发同一重同步循环。
clearAwaitingResync() 保留上次丢失范围供诊断;要清空这些记录可使用 reset()。完整持久历史的读取与展示仍需结合会话记录接口。