# Intelligent UI DSL v1.0 / v1.1 与组件流协议 本文件说明当前实现接受的完整 UI 协议。类型定义以 [shared/types.ts](../shared/types.ts) 为准;文档验证以 [shared/schema.ts](../shared/schema.ts) 的 `parseDocument` 为准,操作与流协议分别见 [shared/protocol.ts](../shared/protocol.ts)、[shared/actions.ts](../shared/actions.ts)、[shared/stream.ts](../shared/stream.ts)。Agent 选择并组合组件,提交结构化数据;插件负责验证、计算和渲染。DSL `1.0` 保留原有的本地动作语义,`1.1` 显式增加 Host 能力引用和 Agent 回调。 ## 1. 输出选择与工具入口 Agent 的规划层可以选择三种呈现方式: | 规划结果 | 当前行为 | | --- | --- | | `text` | 使用普通对话回答,不调用 UI 工具。 | | `structured_ui` | 调用 `intelligent_ui`,生成卡片、表格、图表等展示界面。 | | `interactive_ui` | 生成有输入、共享状态、本地动作或经过 Host 调度的操作界面。 | `text` 是规划结果,不是 `UIDocument.mode` 的合法值。UI 文档的 `mode` 只有后两项。规划由 Agent 根据已注册工具及系统提示执行,当前没有独立的确定性意图分类器,也不强制所有回答生成 UI。 `intelligent_ui` 只接受一个参数 `document`,其值必须是 JSON 对象。不要传 JSON 字符串,也不要额外传 `html`、`code`、`url`、`tool` 等参数。工具执行完成后,Host 将已验证文档放入规范化工具结果和 presentation metadata;Client 从成功、已完成的结果中读取并再次验证。 ## 2. 文档与节点结构 文档的以下字段全部必填: | 字段 | 类型与约束 | | --- | --- | | `version` | `"1.0"` 或 `"1.1"`,不能推测未知版本的语义。 | | `id` | 文档标识符。 | | `mode` | `"structured_ui"` 或 `"interactive_ui"`。 | | `title` | 长度 1–256 的字符串,用作界面标题和无障碍名称。 | | `state` | 状态字段字典;没有状态时传 `{}`,最多 64 个字段。 | | `derived` | 派生表达式字典;没有派生值时传 `{}`,最多 64 个字段。 | | `root` | 一个合法的 `UINode` 根节点。 | `1.1` 另外要求 `actions: Record`;没有外部动作时传 `{}`,最多 16 个。`1.0` 不允许出现 `actions`,也不接受 `invoke`。两种版本具有相同的 21 个组件和表达式语言;无须将原有历史文档升级成 `1.1` 才能继续渲染。 每个组件节点都有 `id`、`type`、`props`。只有 `Row`、`Column`、`Grid`、`Card`、`Form` 可以包含 `children`;这五类的 `children` 必填,允许空数组。其他组件不能携带 `children`。没有属性的组件也必须传 `props: {}`。 节点 `id` 在同一文档内必须唯一。文档 ID、节点 ID、状态键、派生键、图表点 ID、表格列键,以及 `1.1` 的 action ID、capability ID、参数键和结果字段名使用以下形式: ```text ^[A-Za-z][A-Za-z0-9_-]{0,63}$ ``` 必须以英文字母开头,后续只允许英文字母、数字、下划线、连字符,最长 64 个字符;`__proto__`、`prototype`、`constructor` 不可用。图表点 ID 在本图表内唯一,表格列键在本表格内唯一。不同文档可以使用相同的组件 ID。 组件最多 200 个,根节点深度为 1,最多嵌套 16 层。`Form` 不能直接或通过其他布局间接包含另一个 `Form`。未知字段、未知组件、错误大小写和不适用的属性都会被拒绝,不会被悄悄忽略。 `structured_ui` 不允许 `Input`、`NumberInput`、`Select`、`Button`、`Slider`、`Checkbox`、`Form`。其他组件可以绑定已声明状态或派生值来展示初始计算结果。 ## 3. Scalar、Value 与 Expr 三者用途不同: ```ts type Scalar = string | number | boolean | null; type Expr = | Scalar | { state: string } | { derived: string } | { op: ExpressionOperator; args: Expr[] }; type Value = Scalar | { expr: Expr }; ``` `Scalar` 字符串最长 4,096 个 JavaScript UTF-16 码元;数值必须有限且绝对值不大于 `1e15`。`null` 在普通文本位置显示为空字符串。 `Value` 用于组件的动态属性。字面量可以直接写,表达式必须用 `expr` 包装。例如,以下三种 `Result.props.value` 都合法: ```json 12000 ``` ```json { "expr": { "state": "principal" } } ``` ```json { "expr": { "op": "mul", "args": [{ "state": "principal" }, 2] } } ``` 后两种要求文档已声明 `principal` 数值状态。直接把 `{ "state": "principal" }` 作为组件的 `Value` 会被拒绝。 `derived` 字典的值本身就是 `Expr`,不需要再包一层 `expr`。状态和派生引用只能引用对应字典中明确声明的键;不支持属性路径、数组索引、模板插值、函数调用、DOM 查询或 Harness 上下文读取。字符串 `"principal * 2"` 只是一段字符串,不会被执行。 ### 支持的全部 18 种运算 | `op` | 参数个数 | 参数类型 | 返回值与行为 | | --- | --- | --- | --- | | `add` | 2–16 | 全部 number | 相加。 | | `sub` | 2 | number、number | 第一个减第二个。 | | `mul` | 2–16 | 全部 number | 相乘。 | | `div` | 2 | number、number | 第一个除以第二个;除数为 0 时失败。 | | `pow` | 2 | number、number | 第一个的第二个次方;非有限或越界结果失败。 | | `min` | 1–16 | 全部 number | 最小值。 | | `max` | 1–16 | 全部 number | 最大值。 | | `round` | 1 或 2 | number,可选 number | 第二参数为 0–6 的整数小数位数,默认 0;使用 JavaScript `toFixed` 后转回 number。 | | `eq` | 2 | 任意 Scalar | 严格相等;`1` 与 `"1"` 不相等。 | | `lt` | 2 | number、number | 小于,返回 boolean。 | | `lte` | 2 | number、number | 小于等于,返回 boolean。 | | `gt` | 2 | number、number | 大于,返回 boolean。 | | `gte` | 2 | number、number | 大于等于,返回 boolean。 | | `and` | 2–16 | 全部 boolean | 逻辑与,按顺序短路求值。 | | `or` | 2–16 | 全部 boolean | 逻辑或,按顺序短路求值。 | | `not` | 1 | boolean | 逻辑非。 | | `if` | 3 | boolean、Expr、Expr | 按条件仅计算第二或第三参数。 | | `concat` | 1–16 | 任意 Scalar | 按顺序转为文本并连接;`null` 转为空串。 | 没有数字、字符串、布尔值之间的隐式类型转换。`if` 的两个分支都要通过引用及类型检查,但运行时只计算选中的分支。所有派生引用必须无环,声明顺序不限。 校验器会计算默认状态下的全部派生字段、展示值和 `set` 动作值,尽早拒绝默认除零、越界、类型不符或非法动作结果。用户随后输入新值仍可能使计算失败;例如,一个允许取 0 的分母被改成 0。Client 会显示计算错误并隔离失败节点,仍允许通过其他有效控件或动作恢复。 数值运算采用 JavaScript `number`,不是任意精度十进制账务引擎。显示小数位数不会改变底层状态或计算值。所有算术中间结果也必须有限并在数值预算内;字符串连接的中间结果不得超过标量字符串上限。 ## 4. 状态声明与统一值域 状态值只支持 number、string、boolean,不支持 null、对象或数组。 | 类型 | 声明结构 | 约束 | | --- | --- | --- | | number | `{ "type": "number", "initial": number, "min"?: number, "max"?: number, "step"?: number }` | 范围端点包含在内,`min <= max`;`step` 必须为正数;初始值必须合法。 | | string | `{ "type": "string", "initial": string, "maxLength"?: integer }` | `maxLength` 为 1–4,096,默认 4,096;初始字符串允许为空。 | | boolean | `{ "type": "boolean", "initial": boolean }` | 仅接受真正的布尔值。 | ### 数值控件共用一个状态值域 `min/max/step` 应写在状态定义里。`NumberInput` 和 `Slider` 的同名属性仅供精确重复该定义;不能另设、更宽或更窄的控件范围。如果状态未定义某项,控件也不能单独新增该项。 例如,状态 `n` 的范围为 0–100 时,不能让其中一个绑定 `n` 的 Slider 声称自己的 `max` 是 20。否则其他控件写入 50 时,滑块显示会与实际共享状态不一致;当前校验器会在生成阶段拒绝这种文档。 `step` 是实际写入约束。省略 `step` 允许连续数值,原生数值控件使用 `step="any"`。提供 `step` 时,合法值必须落在以下网格上: ```text origin = min(如果声明)或 0 value = origin + integer × step ``` 实现允许与浮点表示有关的小量容差;同一检查应用于初始值、直接输入、按钮动作及恢复的值。例如 `step: 1` 不接受 `10.5`;以 `0.1` 为步长时不会仅因常见的 `0.1 + 0.2` 表示误差而拒绝更新。 Slider 必须从绑定的状态获得有限且严格递增的 `min/max`。NumberInput 可以没有上下界;未声明的上下界仍受全局数值绝对值限制保护。 ### Select 的选项也是共享字段的值域 Select 必须绑定 string 状态,初始值必须是选项之一。选项值不能重复,长度必须符合绑定状态的 `maxLength`。 只要一个字段被 Select 绑定,所有写入来源都只能写入该 Select 的选项值,包括另一个 Input、`set` 动作和 `restoreState`。绑定同一字段的多个 Select 必须具有相同的选项值集合;标签和顺序可以不同。需要自由输入的文本应使用另一个状态字段。 ### 原子更新与恢复 `updateState` 返回新状态对象,不修改旧对象。未知字段、类型错误、越界、步长不匹配、Select 值不在选项内,或完整候选状态超过 262,144 字节,都会抛出 `UIStateError`。当前状态本身不合法时也拒绝更新。失败不会清空其他已填写字段。 `restoreState` 是单独的恢复辅助函数:先建立默认状态,仅接纳已声明且合法的保存字段,忽略无效或未知字段。无法安全检查或恢复后超出预算的数据退回默认状态。正常编辑不会调用这一恢复分支。 ## 5. 全部 21 个组件及其属性 以下属性表中的 `?` 表示可选;`Value`、`Scalar` 的定义见前文。所有属性对象都严格封闭,未列出的属性不可使用。`gap` 只接受 `"sm" | "md" | "lg"`,默认 `"md"`。 | 分类 | 组件 | 合法 `props` | 说明 | | --- | --- | --- | --- | | 基础 | `Text` | `{ text: Value }` | 普通文本,无 HTML 执行。 | | 基础 | `Heading` | `{ text: Value, level?: 1 \| 2 \| 3 }` | 默认二级标题。 | | 基础 | `Markdown` | `{ text: string }` | 最长 16,384;仅安全 Markdown 子集,不支持状态表达式。 | | 布局 | `Row` | `{ gap? }` | 横向布局,必须有 `children`。 | | 布局 | `Column` | `{ gap? }` | 纵向布局,必须有 `children`。 | | 布局 | `Grid` | `{ gap?, columns?: 1 \| 2 \| 3 \| 4 }` | 默认 2 列,响应式布局,必须有 `children`。 | | 布局 | `Card` | `{ title?: string, gap? }` | 卡片,可选标题,必须有 `children`。 | | 展示 | `Badge` | `{ text: Value, tone?: "neutral" \| "success" \| "warning" \| "danger" }` | 默认 neutral。 | | 展示 | `Table` | `{ columns: { key: string, label: string }[], rows: Record[] }` | 1–32 列、0–500 行;每一行必须恰好包含声明的列键。 | | 展示 | `Progress` | `{ value: Value, max?: number, label?: string }` | `value` 必须始终为 number;`max > 0`,默认 100。 | | 数据 | `Chart` | `{ kind: "bar" \| "line", data: { id: string, label: string, value: Value }[], label?: string }` | 1–200 个数据点,值必须始终为 number;支持正负值。 | | 输入 | `Input` | `{ label: string, bind: string, placeholder?: string }` | 绑定 string 状态。 | | 输入 | `NumberInput` | `{ label: string, bind: string, min?: number, max?: number, step?: number }` | 绑定 number 状态;数值约束见前文。 | | 输入 | `Select` | `{ label: string, bind: string, options: { label: string, value: string }[] }` | 绑定 string 状态,1–100 个选项。 | | 交互 | `Button` | `{ label: string, action: UIAction, variant?: "primary" \| "secondary" }` | 显式点击时派发声明动作;默认非 primary 样式。 | | 交互 | `Slider` | `{ label: string, bind: string, min?: number, max?: number, step?: number }` | 绑定 number 状态;需要状态定义有效上下界。 | | 交互 | `Checkbox` | `{ label: string, bind: string }` | 绑定 boolean 状态。 | | 交互 | `Form` | `{ submitLabel: string, action: UIAction, gap? }` | 必须有 `children`;包含声明动作的提交按钮,不能嵌套 Form。 | | 状态 | `Loading` | `{ label?: string }` | 展示加载指示,不自行启动异步工作。 | | 状态 | `Error` | `{ message: Value }` | 展示错误,不改变状态或执行动作。 | | 状态 | `Result` | `{ label?: string, value: Value, format?: ResultFormat }` | 展示结果;有格式时,值必须始终为 number。 | 必填标签,如控件 `label`、表格列 `label`、图表点 `label`、`submitLabel`,长度为 1–512。可选的标签、Card 标题、Input 占位文字最长 512。文档 `title` 的上限单独为 256。 Text、Heading、Badge、Error、Result 的字符串值遵循 `Scalar` 的 4,096 上限。Table 单元格为静态 Scalar;当前不接受表格单元格表达式或内嵌组件。Chart 点的 `value` 支持表达式,但不接受模型提供 SVG 路径或图表脚本。 Progress 的默认值必须处于 `[0, max]`。后续计算产生合法但超出进度范围的数值时,Client 仅将进度条显示钳制到该范围,不修改原状态。 ### ResultFormat ```ts interface ResultFormat { style: 'decimal' | 'currency' | 'percent'; currency?: 'CNY' | 'USD' | 'EUR'; digits?: number; // 0–6 的整数 } ``` `style: "currency"` 必须提供 `currency`;其他格式不能提供该属性。`digits` 默认 2。`percent` 使用 `Intl.NumberFormat` 的比例语义,例如 `0.05` 显示为 `5.00%`,而不是把 `5` 解释成 5%。格式化遵循当前界面的数字区域设置。 ### Markdown 的实际边界 Client 使用 `react-markdown`,设置 `skipHtml`、`allowedElements` 和 `unwrapDisallowed`。允许的元素是: ```text p strong em del code pre blockquote ul ol li hr br h1 h2 h3 h4 h5 h6 ``` 没有原始 HTML、链接元素、图片、iframe、音视频或其他嵌入。Markdown 链接的文字可以保留,但不会产生可点击 URL;图片不会被加载。代码块仅展示内容,不执行其中的代码。更多边界见 [安全说明](SECURITY.md)。 ## 6. 本地动作 `1.0` 仅接受以下两种动作;`1.1` 继续支持它们,并增加第 11 节的 `invoke`: ```json { "type": "reset" } ``` ```json { "type": "set", "key": "principal", "value": 20000 } ``` `reset` 恢复该文档的全部初始状态。`set` 只更新一个明确声明的字段;其 `value` 是 `Value`,可以使用受限表达式。例如,在前文本金字段允许的范围和步长内: ```json { "type": "set", "key": "principal", "value": { "expr": { "op": "add", "args": [{ "state": "principal" }, 100] } } } ``` 达到上界后,这个动作会失败并保留旧状态。动作采用当前状态计算,并只计算实际用到的派生依赖;一个不相关的派生字段除零,不应阻断用于恢复状态的按钮。 `set/reset` 不能附带工具名、权限、URL、JavaScript 或嵌套动作。Form 内的输入仍实时更新共享状态;Form 不是延迟提交的事务缓冲区。提交使用 `preventDefault`,只派发声明的一个动作,不触发 HTML 默认表单导航。`invoke` 通过固定的 Harness Remote 通道提交;其 URL、Session 归属和权限不来自模型文档。渲染、派生求值、刷新、读取历史记录都不会派发动作。 ## 7. 完整合法示例:本地复利计算器 以下是一个完整的 v1.0 文档,可直接作为 `intelligent_ui` 的 `document` 参数。它使用的状态和计算方式与 [完整计算器示例](../examples/compound-interest.json) 一致,布局有所精简。 ```json { "version": "1.0", "id": "compact-interest", "mode": "interactive_ui", "title": "本地复利计算器", "state": { "principal": { "type": "number", "initial": 10000, "min": 0, "max": 100000000 }, "rate": { "type": "number", "initial": 5, "min": 0, "max": 20, "step": 0.5 }, "years": { "type": "number", "initial": 10, "min": 0, "max": 40, "step": 1 } }, "derived": { "total": { "op": "mul", "args": [ { "state": "principal" }, { "op": "pow", "args": [ { "op": "add", "args": [1, { "op": "div", "args": [{ "state": "rate" }, 100] }] }, { "state": "years" } ] } ] }, "gain": { "op": "sub", "args": [{ "derived": "total" }, { "state": "principal" }] } }, "root": { "id": "calculator-card", "type": "Card", "props": { "gap": "md" }, "children": [ { "id": "assumptions", "type": "Text", "props": { "text": "示例假设固定年收益率、每年复利,不含追加投入、费用和税费。" } }, { "id": "principal-input", "type": "NumberInput", "props": { "label": "本金(元)", "bind": "principal" } }, { "id": "rate-slider", "type": "Slider", "props": { "label": "年收益率(%)", "bind": "rate" } }, { "id": "years-slider", "type": "Slider", "props": { "label": "投资年限", "bind": "years" } }, { "id": "total-result", "type": "Result", "props": { "label": "期末总额", "value": { "expr": { "derived": "total" } }, "format": { "style": "currency", "currency": "CNY", "digits": 2 } } }, { "id": "return-chart", "type": "Chart", "props": { "kind": "bar", "label": "本金与累计收益", "data": [ { "id": "principal-bar", "label": "本金", "value": { "expr": { "state": "principal" } } }, { "id": "gain-bar", "label": "累计收益", "value": { "expr": { "derived": "gain" } } } ] } }, { "id": "reset-button", "type": "Button", "props": { "label": "重置参数", "action": { "type": "reset" }, "variant": "secondary" } } ] } } ``` 滑块和输入变化只更新客户端状态与相关结果,不调用模型。另有 [产品比较示例](../examples/product-comparison.json) 和 [表单控件示例](../examples/form-controls.json)。示例数据是演示内容,不是实时外部数据。 ## 8. Shared API 与错误处理 插件导出入口为 `dsh-plugin-intelligent-ui/shared`。执行辅助函数前,应先用 `parseDocument` 获得合法文档: ```ts import { parseDocument, initialState, updateState, evaluateDerived, resolveDocumentValue, dispatchLocalAction, } from 'dsh-plugin-intelligent-ui/shared'; const document = parseDocument(untrustedDocument); let state = initialState(document); state = updateState(document, state, 'rate', 5.5); const values = evaluateDerived(document, state); const total = resolveDocumentValue(document, state, { expr: { derived: 'total' } }); state = dispatchLocalAction(document, state, { type: 'reset' }); ``` 以上调用针对本章计算器文档。`untrustedDocument` 指已经解码的 JSON 对象,不是字符串。 | API | 用途 | | --- | --- | | `parseDocument(input)` | 完整验证并返回深冻结的文档;失败抛出 `UISchemaError`。 | | `documentJsonSchema` | 标准 JSON Schema 对象,供编辑器、工具和构建导出使用。 | | `assertSafeJSON(input)` | 在递归 Schema 解析前检查 JSON 形态和资源预算。 | | `initialState(doc)` | 取得全部状态字段的初始值。 | | `updateState(doc, state, key, value)` | 验证并原子更新单个字段,失败抛出 `UIStateError`。 | | `restoreState(doc, input)` | 保守恢复合法保存值,绝不派发动作。 | | `evaluateDerived(doc, state)` | 计算全部派生字段;单次调用对已计算的派生引用复用结果。 | | `resolveValue(value, state, derived)` | 用已计算的派生字典解析一个组件值。 | | `resolveDocumentValue(doc, state, value)` | 只计算该值实际使用的派生依赖,适合局部错误隔离。 | | `evaluateExpression(expr, state, derived?)` | 执行受限表达式解释器;文档中的表达式应先经过完整验证。 | | `dispatchLocalAction(doc, state, action)` | 显式执行已验证文档中的 `set/reset` 本地动作。 | | `assertValidState(doc, state)` | 验证当前对象恰好包含全部已声明字段,且每个值、完整对象大小都合法。 | | `resolveUIAction(doc, state, actionId)` | 从合法文档和状态计算 capability 参数或 Agent prompt;不执行操作、不授予权限。 | | `applyActionResult(doc, state, actionId, result)` | 将通过 Host 过滤的标量结果按 `resultMap` 原子写入状态;调用者须先验证可信回执及归属。 | | `canonicalJSONString(input)` / `hashCanonicalJSON(input)` | 生成排序键的 JSON 文本及其 SHA-256;用于内容绑定,不等于授权凭据。 | | `startUIStream(doc, sourceCallId, generation)` | 创建合法初始快照状态。Host 负责真实来源与持久化。 | | `advanceUIStream(current, event)` | 验证增删改或终止事件,成功后返回新流状态;失败保留旧状态。 | | `acceptCheckpoint(current \| null, input)` | 验证并接收完整快照;可从部分历史恢复,处理重复、过时和冲突帧。 | 运行时表达式失败抛出 `UIExpressionError`,操作定义或映射错误使用 `UIActionError`,流协议错误使用 `UIStreamError`。Client 在节点边界捕获渲染错误,并在本地状态 revision 改变后允许恢复。开发者不应绕过文档验证,把任意 JavaScript 对象直接交给底层求值或渲染函数。 `UISchemaError.issues` 和 `UIStreamError.issues` 提供可定位的 `path/message`。流的候选树错误保留原文档路径,快照错误带 `snapshot` 前缀;结构错误最多返回 20 项,每项路径最多 512、消息最多 300 字符。未知属性使用固定说明,不把未知键名列表或被拒绝的属性值回显给模型。Host 对工具可见的诊断再限制到 10 项。 ## 9. JSON Schema、语义验证与资源预算 构建产物 `schema/ui.schema.json` 使用 JSON Schema Draft 2020-12,ID 为 `urn:dsh:intelligent-ui:document:1.1`。它通过版本联合类型同时描述 `1.0` 和 `1.1`,包括组件类型、必填字段、封闭属性对象、基础数据类型和能表示的局部上下限。不能因为这个文件的 ID 已更新,就在 `version: "1.0"` 文档中使用新动作。 `tests/shared-jsonschema.test.ts` 使用实际 AJV 2020 的严格模式编译该对象,并验证发行示例和非法嵌套属性。导出器保留递归 `$ref`,在复用引用旁边附加数值、字符串等约束时显式补全同层类型,以兼容严格消费者。 它不是完整的信任边界。标准结构验证不能替代当前实现对唯一 ID、引用存在性、派生循环、表达式参数个数和静态类型、跨组件绑定值域、默认计算、总节点数及总字节数的检查。进入 Host 结果和 Client Renderer 的文档必须通过 `parseDocument`。 主要上限为文档与运行态各 262,144 字节、200 个组件、16 层 UI 嵌套、64 个状态字段、64 个派生字段、2,048 个表达式节点。完整的 20 项 `UI_LIMITS` 以及标签、选项等额外 Schema 上限见 [安全说明中的资源限制](SECURITY.md#资源与可用性限制)。 Host 的 `maxDocumentBytes` 可以把文档生成上限从默认 262,144 字节向下调整到最少 1,024 字节,不能放宽 Shared 上限。该配置不改变本地状态对象的 Shared 字节预算。 ## 10. 历史、版本与状态边界 完整文档保存在 Harness 原有的成功工具结果与 presentation metadata 中,Client 从原生 Session 历史事件恢复 UI。组件流的每个成功结果也包含一份完整快照,因此不要求历史窗口一定包含最初的 open 调用。重新打开会话时仍会重新校验文档,不依赖之前页面中的临时文档对象。 当前产品行为是:**恢复 UI 结构,交互值回到文档声明的 `initial`**。用户在当前页面改过的本金、滑块或表单文本没有额外的跨刷新保存。Shared 已提供 `restoreState` 辅助函数,但默认 Client 尚未把交互状态接入持久存储。恢复不会重播按钮点击、提交动作或外部操作。 Host 的操作记录与浏览器可编辑状态分开。Client 可以只读查询该 UI 的历史操作状态;查询不重新提交请求,也不在挂载时将历史结果重复映射进编辑字段。一次新操作的结果才按已接受回执的生命周期处理。UI 中由模型创建的 `Result`、`Badge` 或文字不具备执行事实或权限证明的地位。 一次显式点击会在本次挂载内保存提交状态的快照。异步成功结果到达时,如果当前状态相对提交快照已改变,Client 保留用户的新值,整次跳过 `resultMap` 并显示冲突提示;可信成功回执与公开结果继续显示。该比较采用整份状态,避免旧参数的结果被误认为属于用户新编辑的参数。它不会撤销已经成功的工具操作,也不会再次调用工具。 文档版本目前只接受 `1.0` 与 `1.1`;流信封使用独立版本 `1.0`。没有向前兼容猜测、自动字段迁移或执行历史动作的逻辑。未知版本降级显示错误。构建、真实安装、授权和端到端运行的验证记录统一见 [VALIDATION.md](VALIDATION.md);协议单元测试不能代替完整 Harness 链路的验证。 ## 11. v1.1:声明 Host 操作与 Agent 回调 组件只引用动作 ID: ```json { "type": "invoke", "actionId": "read_info" } ``` 操作定义位于文档顶层 `actions` 字典,不放进按钮属性或本地状态。以下两种定义构成严格联合类型: ```ts type UIActionDefinition = | { kind: 'tool'; capabilityId: string; arguments: Record; resultMap?: Record; } | { kind: 'agent'; prompt: Value }; ``` `capabilityId` 引用管理员在 Host 中配置的能力目录。真实工具名、风险分类、参数 Schema、审批及可公开的结果字段由 Host 决定。模型不能在定义里提供 `toolName`、`principal`、`confirmed`、权限、令牌或执行 URL。 当前参数仅支持平坦的标量值字典,每个值可以是 `Value` 表达式。参数可以引用状态与派生值,但不能包含任意对象、数组、路径查找或源代码。Host 从原始持久化文档重新计算这些值,并使用真实工具参数 Schema 继续校验。复杂工具参数需要可信适配器或后续明确的协议扩展,不能借字符串隐式反序列化成代码或命令。 `resultMap` 的键是已声明的状态键,值是 Host 允许公开的顶层结果字段名。例如 `{"totalLines":"totalLines"}` 将工具投影中的 `totalLines` 写入同名状态。它不是 JSONPath,不支持 `result.user.secret` 或 `__proto__`。所有映射值必须同时通过状态类型、数值范围、step、Select 选项和完整状态字节数检查;任一字段失败就拒绝整次映射,不部分覆盖状态。结果映射不会触发其他动作。 Agent 回调的 `prompt` 必须始终计算为非空字符串,最长 4,096。它作为当前会话的后续用户输入提交,不创建更高权限的系统消息。回执 `accepted` 表示已接受后续请求,不代表 Agent 已完成推理或工具已经成功;后续回答仍出现在正常聊天流中。Agent 回调没有 `resultMap`。 ### 完整可用的 v1.1 文档 以下示例要求 Host 配置 capability `read_file` 指向 Harness 的 `read` 工具,并只公开 `totalLines`。`file_path`、`limit` 与 `totalLines` 来自核查的[官方 read 工具源码](https://github.com/deepseek-ai/deepseek-harness/blob/687f282e8625c6b8d11ff58211a90524be2e4317/packages/fs/tool-fs/src/read.ts)。示例不会自行执行读取;只有显式点击才提交操作。 ```json { "version": "1.1", "id": "compact_reader", "mode": "interactive_ui", "title": "文件信息", "state": { "path": { "type": "string", "initial": "README.md", "maxLength": 1024 }, "lines": { "type": "number", "initial": 0, "min": 0, "max": 1000000000, "step": 1 } }, "derived": {}, "actions": { "read_info": { "kind": "tool", "capabilityId": "read_file", "arguments": { "file_path": { "expr": { "state": "path" } }, "limit": 20 }, "resultMap": { "lines": "totalLines" } } }, "root": { "id": "reader", "type": "Card", "props": { "gap": "md" }, "children": [ { "id": "path", "type": "Input", "props": { "label": "文件路径", "bind": "path" } }, { "id": "read", "type": "Button", "props": { "label": "读取", "action": { "type": "invoke", "actionId": "read_info" } } }, { "id": "lines", "type": "Result", "props": { "label": "最近成功读取的总行数", "value": { "expr": { "state": "lines" } }, "format": { "style": "decimal", "digits": 0 } } } ] } } ``` 发行包提供 [完整读取示例](../examples/actions-file-reader.json)、[请求文件写入示例](../examples/actions-file-writer.json)、[Agent 回调示例](../examples/agent-callback.json)。读取与写入示例中的能力需分别配置为 `read_file → read`、`write_file → write`;写入能力属于 `risk: "write"`,由真实 Harness 审批和工具权限决定是否执行。写入示例仅映射官方工具的 `operation` 字段,不把 `before/after` 文件内容投影进 UI。插件配置和启用说明见 README。 ### 请求、引用和可信回执 以下结构描述插件固定 Remote 方法的共享参数,不是让模型生成的 DSL 字段。Session ID 由当前 Harness 会话上下文单独传入并授权。 ```ts interface UIDocumentRef { sourceCallId: string; uiId: string; generation: number; revision: number; } interface UIActionReserveRequest { documentRef: UIDocumentRef; requestId: string; actionId: string; values: Record; } interface UIActionQuery { operationId: string } ``` `sourceCallId` 必须来自原生工具调用或 PTC 子调用。普通完整 UI 使用 `generation: 0, revision: 0`。流式 UI 使用最初 open 的原生调用作为来源,不能换成每个 patch 自己的新 call ID;其 generation 和 revision 由 Host 赋值。Host 查询真实持久化文档,不能使用浏览器递交的文档或已解析参数替代。 `reserve` 登记操作,`get` 和 `cancel` 使用精确 `operationId`,`list` 根据文档引用查询历史状态,`catalog` 返回当前能力目录。每次新的明确操作使用新的 `requestId`;同一次网络重试复用原 ID。内容摘要由 Host 使用规范化数据重新生成,相同 ID 配不同内容必须拒绝。SHA-256 摘要只绑定内容,不能代替认证、会话权限、审批或幂等账本。 `UIActionReceipt` 包含 `operationId`、`requestId`、`documentRef`、`actionId`、`status`、`parameterHash`、`createdAt`、`updatedAt`,以及可选 `message/result`。时间为 Unix 毫秒数,`updatedAt >= createdAt`。`parameterHash` 为 64 位小写 SHA-256 十六进制。`status` 只能是 `queued`、`awaiting_confirmation`、`running`、`succeeded`、`failed`、`cancelled`、`unknown`、`accepted`;只有 `succeeded` 可以带 `result`。未知结果不会自动重新执行,取消也不表示外部副作用一定已被撤销。 共享导出 `UIDocumentRefSchema`、`UIActionReserveRequestSchema`、`UIActionQuerySchema`、`UIActionReceiptSchema`、`UICapabilityDescriptorSchema`、`UIActionResultSchema` 及对应 `parse...` 函数。列表使用 `UIActionReceiptListSchema` / `parseUIActionReceiptList`,目录使用 `UICapabilityCatalogSchema` / `parseUICapabilityCatalog`。它们先检查安全 JSON 与预算,再做严格结构校验,最后冻结结果。集合先验证有界、密集、无 getter 的数组外壳,再分别按每个元素的独立预算验证;不会把聚合列表当作一个 UI 文档。数据通过这些 Schema 只代表结构合法;RPC 认证和权限检查仍由 Host 执行。 ## 12. P2:原生结果中的组件级流式 UI 流式生成复用正常工具调用与成功结果 metadata。每个帧都包含完整、可独立验证的 UI 快照;Client 按原生来源、UI 与 generation 聚合为一个界面。因此它支持组件逐步出现和更新,也支持部分历史窗口恢复,但不是逐 token JSON 编译器或最小字节 delta 传输。 四个生成工具如下: | 工具 | 参数 | | --- | --- | | `intelligent_ui_open` | `{ document }`:完整合法初始文档。 | | `intelligent_ui_patch` | `{ sourceCallId, generation, baseRevision, mutation }`。 | | `intelligent_ui_complete` | `{ sourceCallId, generation, baseRevision }`。 | | `intelligent_ui_cancel` | `{ sourceCallId, generation, baseRevision }`。 | 后续调用中的 `sourceCallId/generation/baseRevision` 应使用上次成功调用的回执,不能凭空编造。起始文档示例见 [stream-initial.json](../examples/stream-initial.json),依次传入的 `mutation` 见 [stream-mutations.json](../examples/stream-mutations.json)。三个补丁分别添加计算结果、替换卡片标题、移除加载节点;最后显式调用 complete。 ### 严格事件信封 ```ts interface UIStreamCheckpoint { protocol: 'dsh.intelligent-ui.stream'; version: '1.0'; sourceCallId: string; uiId: string; generation: number; seq: number; baseRevision: number | null; revision: number; type: 'snapshot' | 'add' | 'update' | 'remove' | 'complete' | 'cancel' | 'error'; snapshot: UIDocument; message?: string; } ``` generation 为 1–1,000,000,000 的整数,seq 为 1–256,revision 为 0–256,且 `seq > revision`。初始快照为 `seq: 1, revision: 0, baseRevision: null, type: "snapshot"`。`snapshot` 类型的 baseRevision 必须为 null;增删改必须满足 `revision = baseRevision + 1`;complete/cancel/error 必须保留 `revision = baseRevision`。终止事件仍递增 seq。 ### 候选树与稳定状态 | mutation | 结构与行为 | | --- | --- | | `add` | `{ type: "add", parentId, index, node }`:在已有布局父节点的指定位置插入完整合法子树。 | | `update` | `{ type: "update", nodeId, props }`:完整替换已有节点的 props,不能改变 ID、组件类型或 children。 | | `remove` | `{ type: "remove", nodeId }`:删除已有非根节点及其子树。 | 补丁先作用于独立候选树,整份文档重新通过 `parseDocument` 后才提交。重复 ID、未知组件、恶意属性、错误引用、表达式错误、超深嵌套或总预算超限都会拒绝候选,保留最后有效快照。不能提供通用 JSON 对象路径来修改原型、注册表或 Host 上下文。 同一 generation 冻结文档 ID、版本、mode、state 定义、derived、actions、根 ID/类型,以及所有 Select 对状态施加的选项值集合。已有节点 ID 不能换成另一种组件类型。需要改变这些契约时开启新一轮生成,不能静默改变用户正在输入的值域。新增 Select 只能复用已有同字段选项集合;移除该字段最后一个 Select 会改变其域,因此被拒绝。 Client 本地编辑值与网络 revision 分别维护。共享流接收器不会执行 `reset` 或 `invoke`;未变的分支保持对象引用与组件 key,避免每帧重新挂载整棵界面。流未 complete 前不能调用外部动作,Host 也必须拒绝未完成快照上的操作,避免界面仍在生成时产生操作版本竞态。 ### 重复、乱序、恢复与终止 `acceptCheckpoint(null, frame)` 允许第一份可见快照的 seq/revision 大于初始值,用于分页历史或重连恢复。因为每帧携带完整合法快照,前进的序号缺口可以恢复;这条规则不能移用于未来不带快照的 delta 协议。连续帧仍要求正确 baseRevision,结构版本不能倒退,文档变化必须递增 revision。 完全相同的已接收 seq 返回原状态对象;同 seq 的 SHA-256 内容摘要不同则作为冲突拒绝。尚未见过的过时帧和旧 generation 不覆盖当前视图。不同来源/UI 绑定的帧被拒绝。complete/cancel/error 结束该 generation,随后新的更新不能继续应用;取消或错误保留最近一份合法快照并标明状态。 接收器保存有界摘要记录、帧数与累计字节数。进行中的流预留一个终止帧及一份最大帧容量,避免普通补丁耗尽预算后无法保存 complete/cancel/error。安全恢复只读取快照与状态,不重放动作或外部副作用。 Host 另外保存本 generation 内已提交原生 call ID 的摘要。最近一次调用的相同 ID 和相同内容可以返回已有结果;非相邻旧调用 ID 再次使用会被拒绝,即使它改用当前 `baseRevision`。这项检查由插件完成,不能假设 Harness ToolRuntime 自带调用 ID 幂等保障。Host 重启后可以读取旧快照,但不继续旧进程尚未结束的生成;要继续编辑,应从保存快照启动新的 generation。 ## 13. 新协议的独立资源限制 以下是 `UI_PROTOCOL_LIMITS` 的全部 14 项上限;它们补充第 9 节的文档、表达式和状态限制,不放宽单份 UI 文档的上限。 | 常量 | 上限 | 使用位置 | | --- | ---: | --- | | `maxActions` | 16 | 单个 1.1 文档的动作定义数。 | | `maxActionArguments` | 32 | 一次工具动作的平坦参数字段数。 | | `maxActionArgumentBytes` | 16,384 字节 | 计算后的完整参数对象。 | | `maxActionResultFields` | 32 | 公开结果字段、resultMap 和能力公开字段名单。 | | `maxActionResultBytes` | 32,768 字节 | 完整公开标量结果。 | | `maxAgentPromptLength` | 4,096 UTF-16 码元 | Agent 回调计算后的非空 prompt。 | | `maxActionRequestBytes` | 32,768 字节 | 完整操作请求,包含状态快照与引用;本地 state/document 的 262,144 上限不等于可以提交同样大的请求。 | | `maxActionReceiptBytes` | 36,864 字节 | 可信操作回执,内层 result 另受更小上限保护。 | | `maxCapabilityBytes` | 65,536 字节 | 一个公开能力描述符及其参数 Schema。 | | `maxCapabilities` | 64 | 共享目录数量上限;Host 配置目前另外限制最多 32 个。 | | `maxStreamFrameBytes` | 266,240 字节 | 一份完整文档快照与固定流信封;snapshot 自身重验文档预算。 | | `maxStreamMutationBytes` | 65,536 字节 | 一个组件补丁或终止意图。 | | `maxStreamFrames` | 256 | 单 generation 帧数/序号上限,需预留终止帧。 | | `maxStreamBytes` | 16,777,216 字节 | 单 generation 已接收帧累计大小,需预留终止帧空间。 | Remote 的 operationId/requestId 是非空可打印 ASCII 字符串,最长 128;原生 sourceCallId 最长 256。它们与最多 64 字符的模型 DSL 标识符是不同类型。文档引用最多 1,024 字节;操作查询最多 1,024 字节;回执和流 message 最长 512。能力 title 为 1–256、description 最长 4,096。能力描述符的 parameters 仅为安全 JSON 数据,实际执行使用 Host 工具自身的已注册 Schema。 操作请求超出 32 KiB 时,Client 和 Host 使用同一边界拒绝;不会先登记再截断输入。本地界面仍可使用更大的合法状态,但提交外部操作前,整个请求必须满足操作预算。大段材料应通过 Harness 原有文件/资源能力提供,避免作为大量重复控件状态随每次点击传输。 协议预检仅为固定外壳预留 4 层 JSON 深度与 64 个 JSON 值的有限余量,之后继续单独检查内层文档或状态的原始上限。标准 Zod/JSON Schema 的结构校验、跨组件语义检查、Host 权限和持久操作账本承担不同职责;通过某一层不能跳过其余层。 Host 私有流记录还要保存调用摘要与计数器:其完整记录上限为 331,776 字节,外壳与索引单独限制在 65,536 字节,快照仍使用原来的 `maxStreamFrameBytes`。分层检查不增加模型文档的字节、深度或 JSON 值数量上限;精确 262,144 字节的合法文档不会因为 Host 添加持久化元数据而被误拒绝。