feat: expand intelligent investigation workspace
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent ddd03dbff6
commit c0e4231b83
223 changed files with 13155 additions and 5304 deletions
+253 -15
View File
@@ -2,6 +2,48 @@
统一样式见 [样式设计指南](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
@@ -11,11 +53,32 @@
`Stack`、`Section`、`Text`、`Result`、`Metric`、`Table`、`Chart`、`Evidence`、
`MapLocation`、`FollowUpForm` 共十种组件。图表可切换折线、柱状和数据表;
表格可搜索、排序和选择行,图表可切换已加载区间、显隐序列并选点追问;
表格可搜索和排序,图表可切换已加载区间、显隐序列并查看数据点详情;
证据可按类别筛选、折叠并标注来源;地图定位通过现有控制器执行;表单从空值开始,
由用户主动提交,提交形成普通用户消息。业务证据是否保存以 Agent 实际工具结果为准。
`Chart` 的页签、图例、区间摘要与数据点详情由前端固定实现,Agent 只提供标题、初始图形类型、类别、序列和来源。鼠标切换折线、柱状和数据表时使用 180ms Motion 滑块与内容过渡;键盘操作与减少动态效果模式即时完成。数据点详情只在点击图中数据点,或聚焦图形后按回车、方向键、Home/End 选点时出现,关闭后焦点回到图形;数据表页签不重复展示区间摘要和点详情,搜索与选行在视图切换后保留。
数据组件用于展示与本地浏览,不提供勾选、加入分析、分析内容篮或针对组件追问。`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 内容实例,反向操作中断旧动画,避免旧退出回调隐藏重新展开的表单。
@@ -23,23 +86,40 @@
| --- | --- |
| `Stack`、`Section` | 保留既有布局,不为静态分区附加动画 |
| `Text`、`Result` | 静态说明和已有结果直接阅读,不反复播放入场或宣告历史状态 |
| `Metric` | 保留原始数值、单位与来源,仅加入分析的按钮图标过渡 |
| `Table` | 搜索排序即时响应;选行操作栏进入退出使用统一过渡;空结果不叠加选行提示 |
| `Metric` | 直接展示原始数值、单位与来源 |
| `Table` | 搜索排序即时响应;无勾选列、选行提示或操作栏 |
| `Evidence` | 正文展开收起和箭头转动使用统一过渡;保留来源、记录时间与资料编号 |
| `MapLocation` | 关联要素详情使用统一折叠;定位中的反馈及结果短时淡入 |
| `FollowUpForm` | 精简与外层重复的补充提示;保留标题、说明、校验及草稿,提交反馈短时淡入 |
| 报告辅助分区与分析内容篮 | 分区、内容篮及所选引用详情统一展开收起;保留表单与分析目标草稿 |
| 报告辅助分区 | 分区使用统一展开收起;保留补充表单草稿 |
定位与资料提交的即时状态由用户操作及宿主回调驱动,不读取模型声明的成功状态。定位区分处理中、成功、部分缺失、零结果失败与回执未确认;失败可重试,不支持定位时说明原因。表单提交中阻止重复发送,失败保留草稿;修改字段后清除上一版发送反馈,必填错误与对应字段关联。即时反馈随组件实例存在,刷新后不恢复为本地操作成功;已实现的表单草稿、筛选与图表状态继续使用会话保存通道。
Agent 通过 `present_answer` 发布完整的局部界面块,可在两块之间继续输出文字。
消息顺序为文字、`data-answer_ui`、后续文字与最终报告。同一轮内相同
`block_id` 更新原块;下一轮不会修改上一轮回答。
`block_id` 更新原块,块内稳定的元素 ID 标识同一图表、表格和表单;下一轮不会修改上一轮回答。
这是按内容块逐步出现的界面,不是逐 token 编译 JSX 或每个 JSON 属性都立即显示。
结构化消息与普通聊天共用持久化、历史与断线恢复通道。
生成期间展开显示过程;`final_answer` 到达后,仅展开最后报告的完整文字与
`ui_block_ids` 明确引用的已发布组件,其余文字、界面与工具轨迹默认折叠在
“查看处理过程”中。报告组件按引用顺序排列,同一组件不会同时作为报告和过程
挂载,避免重复表单。未被报告引用的过程界面只供回看。
新发布的块使用 `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` 的块仍按明确引用兼容读取;没有位置标记的旧答复继续在正文后追加引用块,新发布不再依赖这一兼容路径。
同一最终块从生成切换到状态保存时保持组件实例,不因启用持久化而重挂载;尚未完成的编辑先留在内存,最终答复完成后读取远端并合并本地变更。过程块不访问最终状态接口,关闭或切换会话也不产生远端写入。
已发布交互界面的普通/协作聊天在最终答复完成、且没有待完成工具时停止本轮原生
生成,防止最终报告之后反复自动修改界面。该收尾不标记后台调查任务完成;
@@ -49,7 +129,9 @@ Agent 通过 `present_answer` 发布完整的局部界面块,可在两块之
## 正式报告与主工作区
正式报告可携带 `ui_blocks?: Array<{ block: AnswerUiBlock; evidence_refs: string[] }>`,最多 8 块,总 UTF-8 JSON 不超过 400000 字节。每块绑定报告中的已有证据,Server 验证任务归属;事实字段与审批不由 UI 块替代。组件树与聊天共用 `answer-ui@1`。例如:
正式报告可携带 `ui_blocks?: Array<{ block: AnswerUiBlock; evidence_refs: string[] }>`,最多 8 块,总 UTF-8 JSON 不超过 400000 字节。每块绑定报告中的已有证据,Server 验证任务归属;事实字段与审批不由 UI 块替代。组件树与聊天共用 `answer-ui@1`。
正式报告块只接受 `phase: "final"` 或未标阶段的旧块,拒绝 `process`;读取旧记录时继续省略阶段字段,不将其改写为 `null`。例如:
```json
{
@@ -74,9 +156,9 @@ Agent 通过 `present_answer` 发布完整的局部界面块,可在两块之
此片段仅展示新增字段,发布时仍需完整报告与真实存在的证据编号。保存沿用 `agent-artifacts`、报告 JSON、幂等键和 revision;读取沿用报告列表、指定版本、任务详情及 Agent context,不新增写入端点或数据库表。
`InvestigationReportBlocks` 将正式报告接入 `AnswerUiView`,证据组件使用保存记录中的摘要、来源、时间与类别。未知版本或无法解析的块展示 fallback,报告固定事实仍可读。报告追问携带 `report_revision`、`reportGeneration` 和引用文本;表单先带入会话,确认后才发送。
`InvestigationReportBlocks` 将正式报告接入 `AnswerUiView`,证据组件使用保存记录中的摘要、来源、时间与类别;块外只补充尚未在证据卡中展示的引用依据,表格和降级内容仍保留来源。未知版本或无法解析的块展示 fallback,报告固定事实仍可读。补充表单带入会话时携带 `report_revision`、`reportGeneration` 和资料文本,确认后才发送;不提供通用组件追问入口。
`ExpandableReading` 将同一个 React 内容实例移至主工作区,保留图表视图、搜索、行选择、表单草稿。关闭恢复入口焦点,重开恢复阅读位置。主工作区宽度至少 960px 可对照地图,其余宽度在两者之间切换;原地图实例保留。
`ExpandableReading` 将同一个 React 内容实例移至主工作区,保留图表视图、搜索、排序、表单草稿。关闭恢复入口焦点,重开恢复阅读位置。主工作区宽度至少 960px 可对照地图,其余宽度在两者之间切换;原地图实例保留。
## 扩展组件
@@ -109,8 +191,9 @@ node scripts/export-answer-ui-contract.mjs /path/to/agent/src/chat/answer-ui-cat
```bash
pnpm lint
pnpm typecheck
pnpm test src/features/agent/answer-ui src/features/workbench/hooks/agent-session-message-state.test.ts
pnpm test:browser tests/browser/agent-answer-ui.e2e.ts tests/browser/agent-start-page.e2e.ts tests/browser/agent-report-workspace.e2e.ts
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
```
@@ -118,6 +201,7 @@ 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
```
@@ -149,3 +233,157 @@ 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/正文锚点规则不变。