Files
next-tjwater-frontend/docs/agent-answer-ui.md
T
jiang c0e4231b83
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s
feat: expand intelligent investigation workspace
2026-10-30 17:30:19 +08:00

390 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 本地交互式回答
统一样式见 [样式设计指南](style-design-guide.md),聊天布局与报告交互见 [Agent 交互与报告设计](agent-chat-ui-design.md)。本文保留协议、组件参数与业务交互约定。
## 实施进度(2026-10-30 更新)
原生组件版的主要界面与交互状态保存已实现,已完成本地自动化验证;真实授权项目联调和发布尚待完成。下表的“已实现”指当前工作区代码,不代表运行服务已同步更新。相关改动仍未提交、未部署。
| 能力 | 状态 | 已有范围与剩余边界 |
| --- | --- | --- |
| Agent 内容组织 | 已实现 | shadcn/ui 原生组件、内容分区、导航、指标与证据展示;沿用现有对话和工具执行通道 |
| 任务语义与结果表达 | 2026-10-28 已接入工作区 | `Result`、回答用途、分区角色、疏密及具体说明标题;Agent 选择表达语义,宿主统一视觉与动作边界,真实模型联调待完成 |
| 表格与图表 | 已实现 | 搜索、排序、折线/柱状/数据表切换、区间选择和序列显隐;只处理已加载数据 |
| 报告与 Markdown 阅读 | 已实现 | 表格完整内容可横纵滚动;报告与 Mermaid 有界放大、暗色遮罩、图标按钮、焦点恢复及减少动态效果适配 |
| 定位与资料补充 | 已实现 | 保留专用地图定位和补充资料表单;数据组件不提供勾选、分析篮或组件追问 |
| 交互状态持久化(A) | 核心已实现 | 图表、表格、表单与证据状态按会话保存;支持刷新恢复、兼容迁移、失败重试和显式冲突处理 |
| 阅读位置持久化 | 设计待实施 | reader 记录尚未保存到宿主;分析篮已按产品决定移除,旧 basket 设计不再作为实施目标 |
| 提问与回答生命周期 | 2026-10-28 已接入工作区 | 自有 ask_user、独立草稿、答案持久化与固定 ID 续跑;历史 question 权威核对。真实模型完整链路及发布未验收 |
| 压力历史探索(B) | 设计提案 | `HistoryExplorer` 和新时间窗分页查询尚未实现,不能将本地筛选当成重新查询 |
| 自由代码运行时(C) | 未接入 | 仍采用受控原生组件;独立 iframe、生成代码运行及其通信协议需要另行架构评审 |
| 外围与 Dev 窗口交互 | 已修复 | 窗口外地图可点击、拖动和滚轮缩放;窗口内部操作不穿透地图 |
对应根目录研究文档的四个阶段:阶段一的标准组件路线已实现;阶段三的原生组件状态管理已提前落地;阶段四已接入部分地图、报告和追问流程;阶段二的自由代码运行时尚未接入。本文后续 A/B/C 是实施包名称,与研究文档的阶段编号不同。
2026-10-30 展示范围调整已通过 lint、typecheck、146 个文件共 838 项单元测试,以及 25 项浏览器回归(含复跑,使用模拟接口)。浏览器覆盖桌面与窄屏、内联与展开表格、图表动效、会话状态恢复、地图定位和补充表单;不代表真实项目联调或部署完成。
2026-10-30 分区、导航与来源精简已通过 lint、typecheck、842 项单元测试和 20 项浏览器回归(含复跑,使用模拟接口);另覆盖节点追问移除后的详情与地图定位。
以下为 2026-10-26 的历史验证记录,不代表后续工作区变更的验证结果:
| 验证层 | 结果 | 覆盖边界 |
| --- | --- | --- |
| 前端静态检查与单元测试 | lint、typecheck 通过;121 个文件、631 项测试通过 | 当前原生 UI、状态管理和既有前端回归 |
| Agent 检查与聚焦测试 | `bun run check` 通过;33 项测试通过 | 状态存储、权限、版本冲突、重试和会话生命周期 |
| UI 浏览器验收 | 10 项通过 | 当时的交互状态、引用追问、Markdown 阅读,使用模拟接口;引用追问现已移除 |
| 窗口鼠标行为回归 | 4 项通过 | 外围与 Dev 的窗口内外事件、地图拖动与缩放;修复前已复现遮挡 |
| 前端构建 | 通过 | SPA、生产 CSS 校验和 TTS adapter 构建;不等于部署完成 |
当前实现入口为 [原生回答组件](../src/features/agent/answer-ui/answer-ui-view.tsx)、[会话状态同步](../src/features/agent/session-state/answer-view-state-scope.ts) 和 [Agent 状态存储](../../next-tjwater-agent/src/sessions/answer-view-state-store.ts)。验证命令见下文“验证”,后续顺序见“后续待办与验收”。
2026-10-27 已补齐 [交互状态设计](agent-interaction-state-design.md),覆盖阅读位置、分析篮、正式报告私人状态、接口与存储、版本冲突和验收矩阵。其中阅读位置和正式报告状态尚未实施;分析篮于 2026-10-30 移除,旧设计不再适用。正式报告状态需要新增项目 BizDB 状态表,不能沿用聊天核心 A 阶段“无数据库迁移”的交付结论。后续追加的宿主提问实现状态单列如下。
同日补充的宿主提问方案已于 2026-10-28 接入工作区,见上述文档第 11 节:以自有 ask_user 管理等待、草稿、答案与续跑,同时核对历史原生 question 的可用状态。独立完成此项不代表 reader 或正式报告状态已实施;此前历史故障的具体触发条件仍未取得现场响应。
## 接入方式
聊天保留 AI SDK `UIMessage`、AI Elements 与现有 Agent 后端,新增
`@json-render/core` / `@json-render/react` 的本地注册组件。
组件代码随前端构建发布,无 ChatKit iframe、domain key 或 Vercel/OpenAI
托管 UI 请求。模型仍使用 Agent 的既有配置与工具调用能力。
## 可用内容与交互
`Stack`、`Section`、`Text`、`Result`、`Metric`、`Table`、`Chart`、`Evidence`、
`MapLocation`、`FollowUpForm` 共十种组件。图表可切换折线、柱状和数据表;
表格可搜索和排序,图表可切换已加载区间、显隐序列并查看数据点详情;
证据可按类别筛选、折叠并标注来源;地图定位通过现有控制器执行;表单从空值开始,
由用户主动提交,提交形成普通用户消息。业务证据是否保存以 Agent 实际工具结果为准。
数据组件用于展示与本地浏览,不提供勾选、加入分析、分析内容篮或针对组件追问。`MapLocation` 的定位与 `FollowUpForm` 的补充资料提交保留各自入口。
短回答不显示内容导航;含表格、图表、补充表单或达到长内容标准、且包含至少两种可导航组件的回答才提供目录。单个证据或表单使用组件自身标题,多个独立条目才增加分组折叠;不叠加固定的分区说明。
### 按任务表达内容
Agent 选择组件、内容顺序与受控语义,前端负责实际视觉、响应式布局和可执行动作。新增字段均为 `answer-ui@1` 的兼容扩展,旧记录保持可读;Agent、Server 与前端需同步交付生成目录。
| 组件 | 可配置语义 | 前端行为 |
| --- | --- | --- |
| `Result` | `title`、`summary`、`status`、`source` | 用小面积状态图标和文字区分 `success`、`partial`、`unconfirmed`、`error`;保留摘要及依据,不自动执行操作 |
| `Stack` | 可选 `intent=response/comparison/report`、`density=compact/comfortable` | 指定简短回答、并列比较或报告;比较内容按可用宽度切换双列/单列;密度只调整内容间距 |
| `Section` | 可选 `role=result/analysis/evidence/follow_up` | 将独立分区放入结果、分析、依据或补充资料区域,保留分区内部的顺序 |
| `Text` | 可选 `title`,原有 `tone` 不变 | 使用具体说明标题,例如“能力限制”,避免所有说明都叫“注意事项” |
`Stack` 原有 `columns`、`gap` 与其他组件原有必填参数不变。未给新语义时沿用既有布局;显式 `intent=comparison` 保留作者编排,不再拆成分析与依据区域。`density` 不改变字体、控件大小和触屏命中区域,也不折叠或截断原始数据。颜色、CSS、脚本及任意动作不属于模型可配置参数。
已有执行结果适合 `Result`,批量对象适合表格,真实时间序列适合图表,后续定位适合 `MapLocation`,可选资料补充适合 `FollowUpForm`。模型只能以已有工具结果或明确事实表达成功;未确认的执行或现场状态应保持 `unconfirmed`。演练记录成功与实际业务处置分别说明,`Result` 状态本身不构成执行凭据。正文保留关键结论与边界,辅助组件补充数据和依据,避免重复整段文字。
短结果及说明直接在对话内阅读;包含表格、图表、补充表单或长正文的回答提供展开入口。窗口尺寸、滚动与放大由宿主管理,不作为模型输出样式。
`Chart` 的页签、图例、区间摘要与数据点详情由前端固定实现,Agent 只提供标题、初始图形类型、类别、序列和来源。鼠标切换折线、柱状和数据表时使用 180ms Motion 滑块与内容过渡;键盘操作与减少动态效果模式即时完成。数据点详情只在点击图中数据点,或聚焦图形后按回车、方向键、Home/End 选点时出现,关闭后焦点回到图形;数据表页签不重复展示区间摘要和点详情,搜索与排序在视图切换后保留。
其余组件使用 `answer-motion.tsx` 统一交互过渡,沿用现有实体面板、字体、语义颜色和圆角。首次加载即时显示;鼠标展开、收起使用 180ms 透明度和位移,键盘与减少动态效果模式即时完成。折叠中的内容立即设为 `inert` 并退出无障碍树,动画结束后隐藏;保留原 React 内容实例,反向操作中断旧动画,避免旧退出回调隐藏重新展开的表单。
| 组件 | 交互与内容处理 |
| --- | --- |
| `Stack`、`Section` | 保留既有布局,不为静态分区附加动画 |
| `Text`、`Result` | 静态说明和已有结果直接阅读,不反复播放入场或宣告历史状态 |
| `Metric` | 直接展示原始数值、单位与来源 |
| `Table` | 搜索排序即时响应;无勾选列、选行提示或操作栏 |
| `Evidence` | 正文展开收起和箭头转动使用统一过渡;保留来源、记录时间与资料编号 |
| `MapLocation` | 关联要素详情使用统一折叠;定位中的反馈及结果短时淡入 |
| `FollowUpForm` | 精简与外层重复的补充提示;保留标题、说明、校验及草稿,提交反馈短时淡入 |
| 报告辅助分区 | 分区使用统一展开收起;保留补充表单草稿 |
定位与资料提交的即时状态由用户操作及宿主回调驱动,不读取模型声明的成功状态。定位区分处理中、成功、部分缺失、零结果失败与回执未确认;失败可重试,不支持定位时说明原因。表单提交中阻止重复发送,失败保留草稿;修改字段后清除上一版发送反馈,必填错误与对应字段关联。即时反馈随组件实例存在,刷新后不恢复为本地操作成功;已实现的表单草稿、筛选与图表状态继续使用会话保存通道。
Agent 通过 `present_answer` 发布完整的局部界面块,可在两块之间继续输出文字。
消息顺序为文字、`data-answer_ui`、后续文字与最终报告。同一轮内相同
`block_id` 更新原块,块内稳定的元素 ID 标识同一图表、表格和表单;下一轮不会修改上一轮回答。
这是按内容块逐步出现的界面,不是逐 token 编译 JSX 或每个 JSON 属性都立即显示。
结构化消息与普通聊天共用持久化、历史与断线恢复通道。
新发布的块使用 `phase: "process" | "final"`,省略时按 `process` 发布。同一轮相同 `block_id` 的阶段不可改变;需要最终界面时使用独立 ID 发布 `phase: "final"`。过程块始终留在处理过程,即使误列最终引用也不能进入最终答复或读取、写入最终交互状态。
`final_answer.answer` 中用独立一行的 `[[ui:block_id]]` 固定最终组件的位置,`ui_block_ids` 是允许引用的块 ID 列表,必须与标记去重后的顺序一致。例如:
```text
已完成本次记录核对,结果如下。
[[ui:pressure-summary]]
下面解释异常时间和后续核验建议。
[[ui:pressure-trend]]
```
两个示例 ID 都必须先以 `phase: "final"` 发布,再调用最终答复工具。代码围栏、缩进代码和引用中的标记保持字面文本,不执行引用。重复有效标记只挂载一次;不存在、未授权或过程块的标记显示内容不可用,不泄露过程摘要。
生成期间按原始顺序显示内容;`final_answer` 到达后,正文按标记位置穿插最终组件,其余文字、界面与工具轨迹折叠在“查看处理过程”中。完成态只保留这一层过程折叠,展开后直接列出步骤及状态图标,不再嵌套执行进度卡片、计数和折叠按钮。工具调用按独立行展示状态图标、工具图标及名称,具体参数默认收起。过程与工具参数展开前停止底部自动跟随,以当前消息位置作为锚点;鼠标操作使用短时淡入与位移动画,键盘与减少动态效果模式即时完成。外层箭头与步骤状态使用相同的 24px 图标占位;工具名换行时,状态、工具图标和右侧箭头对齐标题首行,参数说明沿用正文缩进。复制和语音使用相同的最终顺序,以组件摘要替代标记,并排除过程资料。旧历史中没有 `phase` 的块仍按明确引用兼容读取;没有位置标记的旧答复继续在正文后追加引用块,新发布不再依赖这一兼容路径。
同一最终块从生成切换到状态保存时保持组件实例,不因启用持久化而重挂载;尚未完成的编辑先留在内存,最终答复完成后读取远端并合并本地变更。过程块不访问最终状态接口,关闭或切换会话也不产生远端写入。
已发布交互界面的普通/协作聊天在最终答复完成、且没有待完成工具时停止本轮原生
生成,防止最终报告之后反复自动修改界面。该收尾不标记后台调查任务完成;
后台执行继续等待其原有运行生命周期与 Server 完成/失败接口。
后台调查执行继续走任务报告与任务展示通道,聊天界面块不构成业务完成或浏览器回执。
## 正式报告与主工作区
正式报告可携带 `ui_blocks?: Array<{ block: AnswerUiBlock; evidence_refs: string[] }>`,最多 8 块,总 UTF-8 JSON 不超过 400000 字节。每块绑定报告中的已有证据,Server 验证任务归属;事实字段与审批不由 UI 块替代。组件树与聊天共用 `answer-ui@1`。
正式报告块只接受 `phase: "final"` 或未标阶段的旧块,拒绝 `process`;读取旧记录时继续省略阶段字段,不将其改写为 `null`。例如:
```json
{
"ui_blocks": [{
"evidence_refs": ["saved-evidence-id"],
"block": {
"version": "answer-ui@1",
"block_id": "pressure-summary",
"fallback_text": "已保存记录中的压力为 0.30 MPa。",
"spec": {
"root": "pressure",
"elements": {
"pressure": { "type": "Metric", "children": [], "props": {
"label": "压力", "value": "0.30", "unit": "MPa", "source": "已保存监测记录"
} }
}
}
}
}]
}
```
此片段仅展示新增字段,发布时仍需完整报告与真实存在的证据编号。保存沿用 `agent-artifacts`、报告 JSON、幂等键和 revision;读取沿用报告列表、指定版本、任务详情及 Agent context,不新增写入端点或数据库表。
`InvestigationReportBlocks` 将正式报告接入 `AnswerUiView`,证据组件使用保存记录中的摘要、来源、时间与类别;块外只补充尚未在证据卡中展示的引用依据,表格和降级内容仍保留来源。未知版本或无法解析的块展示 fallback,报告固定事实仍可读。补充表单带入会话时携带 `report_revision`、`reportGeneration` 和资料文本,确认后才发送;不提供通用组件追问入口。
`ExpandableReading` 将同一个 React 内容实例移至主工作区,保留图表视图、搜索、排序、表单草稿。关闭恢复入口焦点,重开恢复阅读位置。主工作区宽度至少 960px 可对照地图,其余宽度在两者之间切换;原地图实例保留。
## 扩展组件
1. 在 `src/features/agent/answer-ui/catalog.ts` 增加严格的 Zod 参数定义、用途描述与必要的数据上限。
2. 在 `answer-ui-view.tsx` 注册 React 组件。需要工作台交互时通过 Agent 公共 API 的回调接入,保持依赖方向。
3. 从本仓库运行 `pnpm export:answer-ui-contract`,更新相邻 Agent 仓库的
`src/chat/answer-ui-catalog.json` 和 Server 的 `contracts/answer-ui-catalog.json`。组件目录是唯一参数定义源,生成文件需随三端一起交付。
4. 如新组件存在跨字段约束,同时更新三端的协议校验并加边界和交互测试。
导出脚本接受可选目标文件:
```bash
node scripts/export-answer-ui-contract.mjs /path/to/agent/src/chat/answer-ui-catalog.json
```
前端沿用 Zod 3,json-render 0.21 两个包通过作用域 override 使用相同 schema
运行时;实际组件渲染和参数校验已有聚焦测试。升级 json-render 时必须保留这组
兼容性测试,不能只根据包版本判断可用。Agent 原生插件仍使用其 Zod 4 工具边界,
执行后再按生成的 JSON Schema 校验组件参数。
## 校验边界
三端拒绝未知组件、额外属性、任意脚本/事件定义、缺失引用、循环、重复或游离节点。
每块最多 48 节点、深度 12、完整序列化结果不超过 100000 个 JavaScript UTF-16 代码单元,图表必须对齐且数值有限;表格每行与列数
一致,表单字段名唯一。组件只消费明确的 props,没有动态代码、网络 URL 或任意动作 DSL。
无法解析的历史内容显示该块的 `fallback_text`。
## 验证
```bash
pnpm lint
pnpm typecheck
pnpm test
pnpm test:browser tests/browser/agent-answer-state.e2e.ts tests/browser/agent-answer-ui.e2e.ts tests/browser/agent-intelligent-workspace.e2e.ts --workers=1
pnpm test:browser tests/browser/floating-panel-pointer.e2e.ts tests/browser/peripheral-sample-panel.e2e.ts --grep 'panel keeps outside|项目分析成果打开时|外围面板只读取' --workers=1
pnpm build
```
Agent 仓库:
```bash
bun run check
bun test tests/sessions/answerViewState.test.ts tests/routes/answerViewStateRoutes.test.ts tests/sessions/workspaceStore.test.ts tests/routes/chatSession.test.ts
bun test tests/chat/answer-stream-projection.test.ts tests/chat/answer-ui.test.ts tests/routes/chatStream.test.ts tests/routes/taskCollaboration.test.ts tests/runtime/opencode.test.ts
bun run tests/integration/answer-ui-smoke.ts
```
原生 smoke 使用独立临时数据库,不发模型推理请求。浏览器用例用明确标注的演练
事件验证组件、更新、历史恢复、键盘展开证据、表单提交和 320/375px 布局,不能当作
真实模型已正确选择组件或现场资料已核验的证明。
前端、Agent 与 Server 需同步更新;Agent 进程需重新加载原生插件,Server 需加载新报告 schema。无需新增环境变量、
domain key 或数据库迁移。本轮不重启既有服务;浏览器演练和本地测试不代表运行中的服务已完成同步。
## 真实模型验收(显式启用)
以下浏览器用例直接连接本机 Agent,输入均为明确的演练资料。工作台地图与项目
读取使用测试数据,聊天、模型工具调用、流式 UI、聊天持久化与表单回传使用真实
服务。每次创建独立验收会话,结束后仅删除该会话,不删除既有用户记录。此用例
会发送模型请求,默认浏览器测试不执行它。
```bash
TJWATER_ANSWER_UI_REAL_AGENT_URL=http://127.0.0.1:18787 \
TJWATER_ANSWER_UI_REAL_MODEL_LABEL=专家 \
pnpm test:browser tests/browser/agent-answer-ui-real.e2e.ts --workers=1
```
先在 Agent 仓库启动隔离实例,可指定独立 INTEGRATION_DATA_DIR 保留联调数据。
可省略模型标签来使用默认模型。该用例不是生产 OAuth 登录或真实供水业务核验。
Vite 统一解析 React/React DOM,Playwright 按端口使用独立的依赖缓存,避免同时
运行的开发服务将旧临时目录的 React 预构建写回测试缓存。
消息入口为 `POST /api/v1/responses`,采用 Responses 文本输入、Response 输出和标准 SSE 生命周期。前端 `ResponsesChatTransport` 将网络事件转换为已有 UIMessage 渲染数据;审批、凭据续期、地图动作及 answer-ui 走 `tjwater.*` 扩展事件,不将 AI SDK 的 UIMessage 流作为网络协议。会话、模型及动作回执等接口统一使用 `/api/v1` 前缀,原 `/agent/chat` 路径已移除;Agent 服务和现有会话数据保留。
## 可持续分析工作区:实现与后续设计
状态:2026-10-30 更新。A 的控件状态保存已实现,阅读位置持久化待补齐;分析篮已移除;B 为尚未实现的受控查询方案;C 为独立架构扩展的接入条件。
### 目标与实施次序
以工作区根目录的 [Intelligent UI 研究文档](../../intelligent%20UI.md) 为方向,下一阶段让用户能够保留探索状态、在回答内查询新时间窗、联动地图,再带着明确的对象与范围继续分析。视觉规范仍只引用 [样式设计指南](style-design-guide.md)。
原生组件已具备宿主状态保存,下一步优先验证真实项目中的恢复流程,再推进受控数据查询。自由代码运行时在原生组件无法满足具体场景时单独评审。当前实现沿用既有 Agent 框架与存储,没有新增独立 UI Planner、新数据库或环境变量。
原共享架构 `agent_ui_architecture_without_sandbox.md` 明确排除生成代码执行。本提案的可实施范围仍是原生组件;研究文档中的 sandbox 路线列为独立架构扩展,不借此改变当前运行时约束。
### 已解决问题与剩余缺口
| 问题或约束 | 当前状态 |
| --- | --- |
| 按完整数据设置 React key 导致控件重建 | 已改为稳定节点身份和显式兼容迁移 |
| 控件状态仅在组件内存中,刷新后丢失 | 已接入受控 store 与宿主保存;阅读位置仍未持久化;分析篮已移除 |
| 流式消息写入可能覆盖交互状态 | 已独立保存到 `chat/answer-view-state.json`,复用宿主串行队列、身份及删除校验 |
| `answer-ui@1` 仅接受固定组件与 props | 保留此边界;新交互通过注册组件和受控回调扩展 |
| Server 已有历史查询服务、项目连接与 scada.view 校验 | 新数据查询可以复用原服务,无须让每次选区都调用模型 |
| 公开历史查询接口目前未暴露分页,而服务与 MCP 已支持 row_limit/row_offset | 交互探索需要增加有界的公开分页入口,不能把无限读取的风险留给浏览器 |
这些结论来自源码核对;本轮未请求真实项目数据、模型或生产服务。
### B 阶段目标工作区(尚未实现)
首个场景选择“压力历史分析”。Agent 获取真实监测对象和资料后选择原生组件,简单解释仍只显示文字。聊天流保留结论、判断边界与轻量内容;用户主动展开后,在原工作区内探索。
```text
对话:结论、判断边界、关键读数、展开入口
|
v
压力历史分析 来源与记录时间
对象选择 起止时间 [更新数据] 当前探索 / 原回答快照
趋势 / 数据 [筛选已加载数据]
选中对象的读数、单位、缺测说明 [定位地图]
依据与说明 [以此继续分析]
```
- `更新数据` 明确查询新时间窗;图内的范围选择、搜索与排序只处理已加载数据。参数改动先成为草稿,按按钮提交一次,不随每次键入发请求。
- 桌面继续复用现有地图对照工作区,手机使用同一个实例切换地图与内容。沿用 `Card`、`Tabs`、`Select`、`DateTimeField`、`Table`、`Alert` 和 `Button`;不在窄聊天列内增加固定多栏仪表盘。
- 地图联动以点击为触发,不因悬停、恢复历史或数据到达改变视角。所选对象由明确的图层与 feature ID 表示;名字和数组序号不能充当资产标识。
- `以此继续分析` 将对象、时间窗、来源类别与引用的回答版本带入输入区,由用户确认发送。Agent 重新核验所需数据,不把浏览器传入的数值自动当成服务端事实。
- 历史回答是生成时的快照。探索新时间窗不重写原结论;重新分析产生新的回复。正式报告版本与证据也保持不可变。
### A. 保存交互状态(核心已实现)
下述控件状态与 GET/PUT 接口已接入 Agent 对话中完成的最终回答。前端使用稳定节点标识、受控 store 和按登录范围持有的会话缓存;500ms 防抖保存,恢复期间按字段合并新编辑,保存失败可重试,冲突须明确选择保留本地或重载远端。正式报告的同实例展开保留内存交互,但尚未接入这套聊天状态持久化。
本阶段不扩展模型可生成的组件协议。宿主新增 `answer-view-state@1`,状态身份由当前登录主体、业务项目、sessionId、assistantMessageId、blockId、nodeId 共同确定;主体和项目只能从认证上下文取得。
前端在验证成功后,按 spec.elements 的键建立 nodeId。通过本地渲染适配注入状态句柄,不把 nodeId 注入字段或状态表达式开放给模型。使用 json-render 已有受控 store/StateProvider 接口,将控件改为受控状态,避免第二份组件本地状态与宿主状态竞争。库提供状态绑定机制,但允许持久化哪些字段由本地组件目录决定,见 [json-render 数据绑定](https://json-render.dev/docs/data-binding)。
| 内容 | 保存字段 | 新数据到达时的处理 |
| --- | --- | --- |
| 图表 | 视图、隐藏序列、区间、选中点、详情展开 | 保留视图;序列名与顺序一致才复用显隐;唯一时间标签可重定位选点;无法匹配时清除选择并提示 |
| 图表内数据表 | 搜索、排序 | 切换序列时清理排序;视图切换和状态恢复保留兼容的搜索与排序 |
| 表格 | 搜索、排序 | 搜索保留;列变化清除无效排序;不恢复历史选行 |
| 资料表单 | 字段草稿 | 同 nodeId、同字段名和类型才合并;删除或变更类型的字段不迁移,不自动提交 |
| 证据 | 类别筛选、展开项 | 保留仍存在的证据;不存在的筛选回到全部 |
为兼容已部署的 `answer-view-state@1` 严格校验,表格与图表数据表仍在传输时携带 `selection: []`;前端丢弃旧快照中的选行,不提供相关界面。
尚未持久化:阅读锚点与相对偏移。当前同实例展开/收起的内存保留,不等于刷新后的宿主恢复。
补齐方案见 [Intelligent UI 交互状态补齐设计](agent-interaction-state-design.md):保留现有 controls 接口,计划新增独立 reader 记录;旧 basket 部分已停止实施;正式报告按当前读者及报告版本保存到 Server,不使用发布报告的 Agent 会话充当读者身份。
状态记录附带完整 spec 摘要 `schema_digest`、单块 `revision` 与最后请求 ID。`schema_digest` 由服务端从会话里已保存的块计算;客户端摘要只用于检查过期。摘要变化后执行上表的兼容迁移,再写新版本。按数据整体设置 key 的逻辑改为稳定节点身份加显式兼容处理,不能只删除 key 而沿用越界索引。
已实现的 Agent 接口:
| 接口 | 输入与行为 |
| --- | --- |
| `GET /api/v1/session/:sessionId/ui-state?message_id=…&block_id=…` | 校验会话归属和块存在;返回对应状态、revision、schema_digest;未保存返回空状态及 revision 0 |
| `PUT /api/v1/session/:sessionId/ui-state` | 输入 message_id、block_id、schema_digest、expected_revision、request_id、nodes、block;只接受对应组件允许的状态字段 |
写入在同一串行队列内读取旧记录、检查 expected_revision、校验并原子替换。回执保留最近 32 条且总量不超过 128 KiB;窗口内相同 request_id 且内容一致返回原结果,相同 ID 携带不同内容拒绝。回执过期后,旧请求仍受版本检查保护。版本冲突返回 409 及当前版本,前端保留未保存草稿并提供重载或重新保存,不静默覆盖另一个窗口的状态。保存失败显示“未保存”,不声称刷新后可恢复。
初次恢复完成前允许阅读和编辑,但不以 revision 0 覆盖未知的已保存状态;记录本地变更字段,读取成功后按字段合并,用户刚输入的内容优先于迟到的恢复结果。失败时保留草稿并允许重试,重载远端状态必须明确提示会替换哪些本地改动。
持久化使用宿主 `chat/answer-view-state.json`,独立于 messages.json;文件已加入 SessionWorkspaceStore 的受控记录列表,读取消息、检查版本和原子写入均在宿主队列内执行。状态来自用户交互及其兼容迁移,不进入模型可写的 workspace/,不进入默认推理上下文。删除会话沿用宿主删除标记和串行清理,迟到写入不能复活记录;分叉会话不继承未提交表单。
当前限制:每块状态最多 32 KiB UTF-8,单会话状态文件最多 1 MiB;只保存控件字段及迁移依据,不复制原始时序读数、授权凭据或错误堆栈。服务端超限返回 413,前端单块预检超限时不发送;两者均保留当前内存草稿并提示未保存。前端 500ms 防抖,切换会话时对已完成初次读取的改动发起保存;退出时清空内存,不能依赖页面卸载请求保证保存成功。无须迁移旧 messages.json,旧会话首次保存时惰性创建状态记录。
### B. 压力历史探索(设计提案,未实现)
新增注册组件 `HistoryExplorer`,作为拥有固定业务语义的原生组件,不开放模型编写请求 URL、工具名或任意 action。其 props 为 title、targets、initial_start、initial_end、source;targets 只允许查询结果中明确关联的 junction ID 与 device ID,最多 5 组;source 首批只允许 scada_raw 或 scada_cleaned。时间为带时区 ISO 字符串,单次范围不超过 24 小时。缺少对象关联时继续展示原有 Chart 与数据缺口说明,不生成猜测绑定。
该组件仍通过 answer-ui@1 的目录扩展交付,保持 envelope 与快照格式不变;前端为参数唯一来源,导出 Agent、Server 的 props schema,并在三端增加相同的交叉校验。旧客户端遇到新组件显示 fallback_text,不能执行未知组件。Agent 和 Server 先支持校验,前端发布后再启用该组件的生成指令;旧客户端仍可阅读摘要。
前端 `agent` 模块只声明 `onQueryHistory` 回调与数据结果类型,`workbench` 实现身份、项目头、请求和地图控制器。不能从 agent 深入导入 workbench,也不在 Agent 服务里新建通用查询代理。
```text
已有 Agent Harness -> present_answer -> 现有校验与消息通道
|
v
原生回答 + 交互状态
|
用户显式操作
|
Workbench 适配器
/ \
地图控制器 Server 历史服务
|
查询结果回到当前探索视图
```
Server 拟新增 `POST /api/v1/timeseries/views/element-history/page`,复用 TimeseriesHistoryService.query 的 row_limit/row_offset 路径及当前项目数据库连接,保留 scada.view 授权。请求包含上述明确对象与时间窗,以及 limit、offset;首批只支持 observed。limit 默认 500、最大 1000,offset 为非负整数,最多 5 个对象、24 小时。返回 series、page.limit、page.offset、page.has_more、queried_at;不改变原无分页接口。
每次“更新数据”最多读取 5000 个点或 10 页,以先到者为准;超过预算保留已读部分并明确“时间窗未完整覆盖”,提示缩小范围。Server 每页有界,浏览器也限制累计体积;到达 2 MiB 停止继续加载。不能用前 100 点冒充完整 24 小时。分页是在有数据写入的系统中逐页读取,不承诺跨页一致快照;按对象、设备、来源、时间去重,保留查询时刻和查询参数,正式证据仍由后端报告流程形成。
新请求取消旧请求并提升 request generation;结果只有在项目、会话、节点和 generation 均匹配时可应用。超时或服务失败保留旧图,并同时显示旧图的实际时间窗与失败原因;无数据与请求失败分开表达。返回 null 的点显示缺测断线,不补零;按 source_unit 展示,转换需有明确单位规则;原始、清洗、实时模型和分析结果不能混为同一种来源。
刷新数据不修改消息快照。查询参数和控件状态可以保存,未固化的探索数据只留在当前会话内存;刷新页面后先显示原回答或待加载状态,用户点击“更新数据”重新取数。需要跨日保存的业务结论仍通过现有正式报告与证据通道发布。
### C. 自由代码运行时(未接入,需独立评审)
本轮只确定接入条件:出现标准组件难以表达、且确有使用价值的交互图解或模拟,再单独评审 sandbox 协议与运行环境。阀门工况计算必须复用实际仿真服务;可拖动示意图不等同于水力模型。
后续设计参考 [MCP Apps 的 AppBridge](https://github.com/modelcontextprotocol/ext-apps/blob/main/src/app-bridge.ts) 的初始化、能力协商、结果推送和 teardown,把动态程序当作独立实例管理。其工具转发能力不能原样映射为“允许调用全部供水工具”。iframe 本身也不能证明资源消耗或业务权限已受控。
接入前须完成:独立来源与 CSP/网络策略、消息来源与实例校验、显式能力列表、宿主中止与生命周期、版本替换及状态迁移、崩溃后的文本回退、真实浏览器资源预算验证。生产控制、业务写入和审批继续由既有服务执行。本阶段不新增 sandbox 入口、依赖或服务器,因此不会留下无法使用的占位 UI。
### 后续待办与验收
A 与 B 可独立交付,B 尚未实现不影响已有聊天控件的状态保存。建议按以下顺序推进,正式提交和发布按各仓库分别执行。
| 顺序 | 待办 | 完成标准 |
| --- | --- | --- |
| 1 | 已实现能力的真实联调与交付 | 在隔离 Agent 与已授权项目中验证保存读回、刷新恢复、会话隔离、报告/表单回传;补充真实模型组件选择验收,再整理提交与发布记录 |
| 2 | B 阶段压力历史探索 | 用户选择对象和时间窗后显式查询;分页有上限、缺测不补零、旧响应不覆盖新请求;定位地图并携带查询范围继续分析 |
| 3 | 实施已补齐的状态设计 | 优先独立交付宿主提问生命周期;聊天 reader 和正式报告私人状态另分两批,分别验证恢复、清理与版本隔离 |
| 4 | C 阶段架构评审 | 先确认原生组件无法满足的具体场景,再决定是否引入沙箱及其权限、通信、资源预算和生命周期 |
| 阶段 | 主要修改位置 | 必须验证 |
| --- | --- | --- |
| A 前端(已实现) | answer-ui/answer-ui-view.tsx、answer-parts-view.tsx、answer-view-state.ts、answer-view-state-provider.tsx;agent/session-state/;workbench/hooks/use-workbench-agent.ts;agent/api/client.ts | 状态注入、同块更新、无效选择清理、迟到恢复不覆盖新输入、会话隔离 |
| A Agent(已实现) | sessions/workspace-store.ts、sessions/answer-view-state-store.ts、routes/answer-view-state-routes.ts、routes/chat.ts | 身份和块归属、原子版本检查、幂等重试、冲突、字节限制、会话删除竞态 |
| B 前端 | answer-ui/catalog.ts、contract.ts、answer-ui-view.tsx;新增 history-explorer.tsx、workbench/api/element-history-client.ts;现有 workbench-agent-panels.tsx 的回调装配 | 参数校验、请求与本地筛选区分、取消和旧响应、缺测、单位、地图定位失败 |
| B Agent / Server | 导出的 answer-ui-catalog.json;三端 answer-ui 校验;Server timeseries/composite.py、timeseries_history.py 的分页请求/响应模型;Agent instruction.md | 旧消息降级、新组件约束一致、Server 分页限制、scada.view 和项目隔离 |
前端执行 `pnpm lint`、`pnpm typecheck`、`pnpm test`,并扩充 `tests/browser/agent-answer-ui.e2e.ts` 的 320/375/1440px 用例。A 在 Agent 执行 `bun run check` 及 `tests/sessions/workspaceStore.test.ts`、新增状态存储/路由测试、`tests/routes/chatSession.test.ts`。B 追加 Agent `tests/chat/answer-ui.test.ts`,Server `tests/unit/test_timeseries_history.py`、`test_timeseries_history_schema.py`、`test_report_ui.py` 与新增分页 API 授权测试。新增 schema 后运行 `pnpm export:answer-ui-contract`;发布前完成前端构建和真实授权项目的有界读查询验收。
验收场景必须包含:保存后刷新、两窗口编辑冲突、数据更新时仍在填写表单、重复序列标签、数据集换序、断网与 403、切换项目后旧响应返回、删除会话后迟到保存、未知组件回退、减少动态效果、键盘焦点与窄屏无溢出。保存成功必须有读回证据,不能仅以组件没报错作为通过。
最关键的前提是查询结果能明确关联监测设备与管网对象;现有数据若缺少关联,地图入口应显示不可定位并保留读数,不能使整个分析失效。真实模型选型与数据质量均需独立验收,组件测试不替代它们。
发布 A 时先部署 Agent 的可选状态接口,再部署前端;没有接口或保存失败时保持可读和内存交互,并显示未保存。回退保留新状态文件和已有消息,不清理用户资料。B 回退时停止生成 HistoryExplorer,旧客户端按已有 fallback 读取,保留原查询接口。上述两阶段均不需要新账号、密钥或环境变量。
### 地图与外部展示动作
`answer-ui@1` 继续描述可保留的回答内容;动作统一为 `{command: {action, ...字段}}`。Chat 的 `present_view` 使用 `frontend-action@2` 和 `frontend-action-result@2`,调查任务事件及 Dev JSONL 使用相同 command,由共用执行器处理。定位按钮也使用该执行器。十种动作和验证步骤见 [调查任务展示与 Dev 测试](investigation-dev-tools.md#统一展示动作)。动作是一次性过程副作用,不因 final_answer 引用 UI 或历史渲染而重放;block_id/phase/正文锚点规则不变。