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

39 KiB
Raw Blame History

本地交互式回答

统一样式见 样式设计指南,聊天布局与报告交互见 Agent 交互与报告设计。本文保留协议、组件参数与业务交互约定。

实施进度(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 构建;不等于部署完成

当前实现入口为 原生回答组件、会话状态同步 和 Agent 状态存储。验证命令见下文“验证”,后续顺序见“后续待办与验收”。

2026-10-27 已补齐 交互状态设计,覆盖阅读位置、分析篮、正式报告私人状态、接口与存储、版本冲突和验收矩阵。其中阅读位置和正式报告状态尚未实施;分析篮于 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 列表,必须与标记去重后的顺序一致。例如:

已完成本次记录核对,结果如下。

[[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。例如:

{
  "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. 如新组件存在跨字段约束,同时更新三端的协议校验并加边界和交互测试。

导出脚本接受可选目标文件:

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。

验证

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 仓库:

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、聊天持久化与表单回传使用真实 服务。每次创建独立验收会话,结束后仅删除该会话,不删除既有用户记录。此用例 会发送模型请求,默认浏览器测试不执行它。

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 研究文档 为方向,下一阶段让用户能够保留探索状态、在回答内查询新时间窗、联动地图,再带着明确的对象与范围继续分析。视觉规范仍只引用 样式设计指南。

原生组件已具备宿主状态保存,下一步优先验证真实项目中的恢复流程,再推进受控数据查询。自由代码运行时在原生组件无法满足具体场景时单独评审。当前实现沿用既有 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 获取真实监测对象和资料后选择原生组件,简单解释仍只显示文字。聊天流保留结论、判断边界与轻量内容;用户主动展开后,在原工作区内探索。

对话:结论、判断边界、关键读数、展开入口
  |
  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 数据绑定。

内容 保存字段 新数据到达时的处理
图表 视图、隐藏序列、区间、选中点、详情展开 保留视图;序列名与顺序一致才复用显隐;唯一时间标签可重定位选点;无法匹配时清除选择并提示
图表内数据表 搜索、排序 切换序列时清理排序;视图切换和状态恢复保留兼容的搜索与排序
表格 搜索、排序 搜索保留;列变化清除无效排序;不恢复历史选行
资料表单 字段草稿 同 nodeId、同字段名和类型才合并;删除或变更类型的字段不迁移,不自动提交
证据 类别筛选、展开项 保留仍存在的证据;不存在的筛选回到全部

为兼容已部署的 answer-view-state@1 严格校验,表格与图表数据表仍在传输时携带 selection: [];前端丢弃旧快照中的选行,不提供相关界面。

尚未持久化:阅读锚点与相对偏移。当前同实例展开/收起的内存保留,不等于刷新后的宿主恢复。

补齐方案见 Intelligent UI 交互状态补齐设计:保留现有 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 服务里新建通用查询代理。

已有 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 的初始化、能力协商、结果推送和 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 测试。动作是一次性过程副作用,不因 final_answer 引用 UI 或历史渲染而重放;block_id/phase/正文锚点规则不变。