# Client 与 Remote 集成 本文描述 `0.2.0` 实际实现。官方接口以已核实的 DeepSeek Harness `0.2.1-rc.2`、源码提交 `687f282e8625c6b8d11ff58211a90524be2e4317` 为基线;安装和完整验收见主 README 与 `VALIDATION.md`。 ## 1. 原生接入点 `client/index.ts` 是官方 Client 插件 entry。它声明依赖 `slots`、`locale`、`uiConversation`、`remote`、`sessions`,所有注册均由插件所属 Cordis fiber 管理。 | 接入点 | 本插件用途 | | --- | --- | | `ctx.uiConversation.events.register(definition)` | 从成功的持久化工具结果重建 `intelligent-ui` Conversation Node。 | | `conversation.chat.node` keyed slot | 在原生消息 turn 内渲染可操作界面;工具过程折叠后仍可见。 | | `tool.call.toolview` keyed slot | `intelligent_ui`、`intelligent_ui_open`、`intelligent_ui_patch`、`intelligent_ui_complete`、`intelligent_ui_cancel` 的生成过程、错误和成功回执。 | | `ctx.remote.$mount(TYPERT_REMOTE)` | 挂载本包显式声明的 `intelligentUi` Remote namespace。 | | `ISessions.binding(sessionId)` / `sessionOf(ctx)` | 取得当前原生 Session generation,构造受该生命周期约束的操作 capability。 | | `ctx.locale.register` / `ctx.effect` | 中英文界面文案与可卸载的样式。 | 普通工具调用的审计节点仍由 Harness 负责。本插件的 Conversation Node 不独占认领工具事件。成功 ToolView 显示“交互界面已在对话中显示”,真实交互状态只存在于独立 Conversation Node 的一份 Renderer 中。 Client 的输入控件使用 React 与原生 HTML 控件;布局、颜色与边框使用 Harness 的主题 token,并有独立预览的浅色/深色后备值。模型只能声明白名单组件与属性;样式文本完全属于插件。 ## 2. Remote 贡献与加载顺序 `remote/index.ts` 使用官方 `@deepseek-ai/dsh-typert-protocol/remote-info` 的纯 helper `createTypertRemoteInfo` 生成可挂载的描述符。`remote/host.ts` 导出官方 `TypertContribution`,由包的 `./typert` export 提供给 Typert Loader。正式安装时由 Loader 注册 Host contribution,Host service 不再重复手动注册。 这部分是针对已核实版本手写的贡献,而不是从一个未提供的私有协议推测出来的接口。五个方法的所有 JSON 参数和结果均使用严格 codec。结果 codec 同时声明 `create` 与 `decode`:实际 Client Gateway 对普通 JSON 响应使用 `decode`,因此只提供 `create` 不足以校验浏览器收到的数据。 加载顺序必须先提供 namespace,再启动依赖它的子插件: ```ts export const inject = ['slots', 'locale', 'uiConversation', 'remote', 'sessions']; export async function apply(ctx: Context) { await ctx.remote.$mount(TYPERT_REMOTE); ctx.inject(['remote', 'remote.intelligentUi'], scope => { // Register the conversation renderer using this scoped Remote capability. }); } ``` 将 `remote.intelligentUi` 放入本插件顶层 `inject` 会形成自依赖:`apply` 尚未执行,namespace 就不可能先出现。 | 方法 | 行为 | | --- | --- | | `reserve(sessionId, request, signal)` | 验证并持久化一次用户点击的请求;Host 在同一调用者权限上下文中排入 Agent turn。 | | `get(sessionId, { operationId }, signal)` | 读取一个已存在的操作。 | | `cancel(sessionId, { operationId }, signal)` | 请求取消,返回 Host 已知的状态;不是对已经发生副作用的撤销。 | | `list(sessionId, documentRef, signal)` | 读取该 UI generation 每个 action 的最新回执,用于重开与恢复。 | | `catalog(sessionId, signal)` | 读取部署方允许的工具 capability 描述。 | 所有 Host 方法都有显式 Session resource 授权。前端不传 `principal`、工具名称、任意 URL、Shell 命令或权限绕过标志。工具绑定和公开结果字段由 Host 配置与原生工具 Schema 决定。具体 Host 授权与账本见 `SECURITY.md`。 列表边界使用 `parseUIActionReceiptList` / `parseUICapabilityCatalog`:先验证普通密集数组的数据描述符和数量上限,再逐项应用独立资源限制。这样允许合法空列表,拒绝 getter、稀疏数组、重复 ID、额外数组属性与超限项目;不会把整个列表预算误当作单文档预算传给 `assertSafeJSON`。 ## 3. 动作与状态边界 `component-registry.tsx` 只接收窄的 `onInput`、`onAction`、状态与值解析接口。模型提供的组件不能取得 `Context`、原生 Session object 或 `ctx.remote`。 `set`、`reset` 与所有输入计算由 `state-manager.ts` 和共享受限表达式执行器完成,不调用模型。DSL `1.1` 的 `{ type: 'invoke', actionId }` 才进入 `UIActionDispatcher`。 每个挂载的 UI generation 拥有一份 dispatcher: 1. 初次挂载只调用 `list`。历史成功回执可以显示,但不会重新提交、执行工具或自动应用旧 `resultMap`。 2. 明确的点击/表单提交捕获当时的有效 values,生成一个 request ID,并在任何异步工作前将该 action 设为 submitting;双击不能产生两个请求。 3. Host 回执驱动 `queued`、`awaiting_confirmation`、`running`、`succeeded`、`failed`、`cancelled`、`unknown` 或 `accepted`。`accepted` 表示交给 Agent 继续处理,不被解释为工具已成功。 4. 活跃操作通过只读 `get` 轮询,最多四个并行请求。权限失效或 capability 不可用后停止该操作的轮询。 5. 请求可能已被 Host 接收而响应失联时,保留 uncertain 状态。用户可以检查状态,或明确重试同一份 request ID 与 values;不会自动创建新请求。 6. 本次挂载中、对应本次点击的可信成功回执可以应用 `resultMap`。映射是原子校验后的本地状态更新。若提交后状态已有变化,则保留当前值,并提示在操作记录中查看返回结果,防止旧请求覆盖新输入。 旧 list 响应不能覆盖正在提交或 uncertain 的新请求。旧 running 响应不能覆盖终态;取消请求的网络错误不能把并行读取到的 succeeded 状态恢复成 running。 远程操作不持久化浏览器输入。尚未确认的请求 values 只保留在当前挂载内存中;dispose 时清除。刷新后只通过 Host 账本恢复操作回执,输入恢复文档默认值。尚无可选输入草稿持久化功能。 ## 4. 组件级流式恢复 本版本使用现有 Session `tool/result` / `tool/ptc-dispatch` 事件,每一帧携带完整、已验证的 `UIStreamCheckpoint.snapshot`。没有另开 SSE 端点,也不尝试逐 token 解析不完整 JSON。 `intelligentUIConversationDefinition` 将同一 `(sourceCallId, uiId, generation)` 的 open/patch/complete 结果聚合到同一个 Conversation Node;每个 patch 工具自身的 call ID 不参与该 UI 的 identity。普通非流式 DSL `1.0` / `1.1` 使用原始工具结果 call ID 与 generation/revision `0/0`。 共享 `acceptCheckpoint` 在候选树整体通过验证后提交: - 重复或过时帧不会替换当前树;相同序号内容冲突会显示错误并保留最近有效树。 - 完整合法快照可以填补事件缺口,也可以直接从只含最后结果的历史窗口恢复。 - 同一 generation 冻结 state、derived、actions 与输入值域;局部补丁改变组件树与允许的展示属性。 - 未改变的树分支保留对象引用;React 组件按稳定 node ID 寻址。普通局部更新不重挂整个 UI,已编辑值、焦点和 NumberInput 的局部输入草稿可保留。 - 新 generation 拥有新的状态生命周期。取消与错误保留最近有效界面;终态后的非法更新不恢复生成。 - 流尚未 complete 时,输入与本地计算可用,远程 invoke 禁用;Host 也独立执行同一限制。 完整快照协议使历史恢复不依赖临时 Client 内存;它同时具有每帧和累计帧数/字节预算。它不是无限流或逐 token compiler。 ## 5. 卸载与可测试入口 停用插件会撤销 slot、locale、Conversation Node definition、Remote contribution,并移除插件样式。挂载期 AbortController 和 dispatcher 的 dispose 会中止在途 Client 调用、停止轮询并清除内存请求。已发生的 Host 工具副作用遵循原生执行与持久化账本语义,浏览器卸载不声称能够撤销它们。 独立组件测试可以直接导入 `IntelligentUIRenderer`,传入已验证的 `document`。实际会话注入附加 `documentRef`、`documentRevision`、稳定 `identity`、`actionClient` 与可选 `streamStatus`。 生产 DOM 的可访问标签与非敏感状态属性支持真实浏览器验收: - UI 根:`data-ui-id`、`data-ui-version`、`data-ui-generation`、`data-ui-revision`、`data-ui-stream-status`。 - 操作状态卡:`data-ui-action`、`data-ui-action-status`。 - 控件:原生 label、role、`aria-busy`、`aria-invalid` 与禁用状态。 这些属性不增加调试全局对象或额外调用权限。 原生 Plugins 页的启禁用测试还需要等待受控 Switch 的 `aria-checked`。Host 返回 `application: "applied"` 表示请求已处理;相同目标状态的无操作请求也会返回此值。官方 Plugins store 会先解除 busy,再刷新包清单,因此仅等待按钮可点击或样式消失后立即点击,可能再次发送上一目标状态。验收应明确检查返回的 `enabled` 与 `changed`,等待 Switch 显示新的状态,再执行下一次切换。样式 `1 → 0 → 1` 和官方 `plugins/events` 中目标模块的移除/恢复才构成完整的 Client 热启禁用证据。 ## 6. 本文件相关自动化验证 ```sh npx vitest run tests/client-renderer.test.tsx tests/client-actions.test.tsx tests/client-conversation.test.tsx tests/client-remote.test.tsx npm run typecheck ``` 当前这四个文件共 48 项通过,覆盖真实 DOM 输入、局部计算、Markdown 安全、事件竞态、流式状态保留、官方 `ConversationNodeAssembler` 重放,以及实际 Client Gateway/Registry 的挂载、严格解码、Session scope 失效、卸载与在途取消。Host 的实际 Gateway 权限回归在 Host 测试中单独执行。完整 Harness 安装、真实模型、原生授权与热更新结果由项目 `VALIDATION.md` 记录。