Web Owner Console 还没写前端,我先给它设计 read model

上一篇里,我把 MainAgent owner 侧 runtime 从 QQ adapter 里拆了出来:

text 复制代码
owner_agent_runtime.py
  任务、审批、工作台、详情卡、审批恢复入口

owner_read_runtime.py
  主人只读管理命令分发

owner_write_runtime.py
  已审批主人写命令执行

owner_runtime_factory.py
  task/read/write runtime 总装层

这一步做完后,一个方向自然浮出来了:

text 复制代码
既然 /agent 已经越来越像主人控制台,
那是不是可以开始做 Web Owner Console?

但我这次没有直接写前端,也没有接 HTTP API。

我先做了一份更枯燥、但更关键的设计:

text 复制代码
Web Owner Console read-model 设计。

简单说,就是先决定未来 Web 控制台要读哪些数据、这些数据应该长什么样、从哪里来,以及哪些东西绝对不能因为有了 Web 入口就绕过安全边界。

这篇文章记录我为什么没有直接复用现在的 QQ 文本输出,也为什么要提前设计结构化 DTO。

当前系统已经有一个 QQ 版主人控制台

现在 /agent 已经不只是一个"问项目上下文"的命令了。

它可以做很多主人侧只读查询:

text 复制代码
/agent 看看最近错误
/agent 角色卡列表
/agent 模型配置
/agent 访问控制
/agent RAG 索引详情
/agent MainAgent 最近观测
/agent RootGraph 最近观测

也可以看任务和审批:

text 复制代码
/agent 任务工作台
/agent 下一步
/agent 任务详情 最新
/agent 审批状态
/agent 审批详情 最新

写操作也已经有审批门控:

text 复制代码
/agent 删除摘要 41
/agent 清空图片缓存
/agent 把群 123456 加入群白名单
/agent 确认 最新

这些能力组合起来,已经很像一个"QQ 里的 Owner Console"。

但它的输出形态是 QQ 消息。

也就是说,现在后端大体是这样:

text 复制代码
结构化数据 / 服务结果
  -> QQ 文本 formatter
  -> str
  -> matcher.finish(reply)

这对 QQ 很合适。

一条消息就应该是一段人能直接读的文本。

但如果未来要做 Web Owner Console,直接拿这段文本塞进网页,就会埋下很多坑。

现有 QQ 输出的三种后端形态

我这次先把现有 QQ 输出形态梳理了一遍。

它其实不是完全乱拼字符串,背后有三类来源。

第一类是 list[str]

比如:

text 复制代码
role_card_list_lines()
model_config_status_lines()
access_overview_lines()
rag_index_detail_lines()
main_agent_observation_lines()
root_graph_observation_lines()

这些函数返回一组展示行,然后 owner_read_runtime 用换行拼成 QQ 文本。

第二类是 Graph execution 的 reply_text

比如:

text 复制代码
run_diagnostics(...)
run_memory_retrieval(...)
run_memory_admin(...)

Graph 里已经聚合了状态,但最后给 /agent 的仍然是一段可发送的文本。

第三类是任务和审批 formatter。

任务和审批底层其实已经有结构化对象:

text 复制代码
AgentTask
AgentTaskEvent
AgentApproval

但 QQ 入口会立刻把它们送进 formatter:

text 复制代码
format_agent_task_list(tasks) -> str
format_agent_task_detail(task, events, approvals) -> str
format_agent_approval_list(approvals) -> str
format_agent_approval_detail(approval, task, events) -> str
format_agent_task_workbench(...) -> str

这些 formatter 产出的内容适合 QQ:

text 复制代码
Agent 审批详情卡 #19
审批ID:#19
状态:待确认
任务ID:#18
工具:owner_write_command
风险:medium
原因:owner write command requires approval
输入摘要:...

但它不是 Web 后端接口。

Web 如果想做表格、筛选、排序、状态标签、详情页、按钮禁用态,就不应该去解析这段文本。

为什么不能直接复用 QQ 文本

最省事的做法当然是:

text 复制代码
Web 页面请求一个接口。
后端调用现有 /agent 逻辑。
拿到 str。
前端原样展示。

这个方案第一天看起来很快,第三天就会开始难受。

1. 文案变化会变成接口破坏

比如 QQ 文本里有一行:

text 复制代码
审批ID:#19 [待确认] 任务ID:#18 owner_write_command / medium

如果 Web 前端靠解析这行拿 approval_idstatustask_id,那么以后我只要把文案改成:

text 复制代码
审批 #19:待主人确认,关联任务 #18

前端解析就坏了。

文案应该是展示层,不应该成为数据契约。

2. Web 页面天然需要结构

任务列表不是一段文本,它至少需要:

text 复制代码
task_id
title
goal_preview
status
status_label
created_at
updated_at
latest_event_summary
pending_approval_ids

审批列表也不是一段文本,它需要:

text 复制代码
approval_id
task_id
task_title
tool_name
risk_level
reason_preview
status
created_at
expires_at
decided_at

这些字段到了前端以后,才能自然变成:

text 复制代码
表格列
筛选条件
状态 badge
详情链接
风险颜色
空状态
禁用按钮原因

如果后端只给一段文本,前端要么只能做一个大文本框,要么就开始写脆弱的正则解析。

3. 安全脱敏需要字段级控制

审批详情里有 tool_input_json

它可能包含:

text 复制代码
工具参数
记忆内容
摘要 ID
名单目标
模型配置相关信息

在 QQ 里,我可以用短文本摘要展示:

text 复制代码
输入摘要:{"command": "delete_session_summary", "summary_id": 41}

但 Web 以后可能会有折叠详情、复制按钮、JSON 预览。

这时如果后端只给一段 formatter 文本,脱敏边界就很难说清楚。

更稳的形式是 read model 里明确写:

text 复制代码
tool_input:
  preview_json: str
  redacted: bool
  truncated: bool

这样前端知道自己看到的是预览,不是完整原文。

4. 未来按钮状态不能靠前端猜

Web Console 以后可能会展示审批确认按钮。

但按钮能不能点,不能靠前端根据文案猜:

text 复制代码
如果文本里有"待确认",按钮就可点。

这是危险的。

正确做法应该是后端 read model 给出只读的 actionability metadata:

text 复制代码
can_approve
can_reject
resume_enabled
blocked_reason
future_operation_only

v0 阶段这些字段只用于描述未来 UI 状态,不执行操作。

真正实现时,它们也必须由后端根据审批状态、tool registry 和 approval_resume_enabled 计算。

前端不应该自己判断某个工具能不能恢复。

DTO 在这里到底是什么意思

我这里说的 DTO,不是说马上要写一堆复杂的类。

它更像是一个明确的数据契约:

text 复制代码
这个页面需要什么字段。
这些字段来自哪里。
哪些字段是展示摘要。
哪些字段是脱敏预览。
哪些字段只是未来操作提示。
哪些能力当前明确不可用。

比如任务列表可以设计成:

text 复制代码
OwnerConsoleTaskList
  generated_at
  filters
  total_visible
  rows
  boundary

OwnerConsoleTaskRow
  task_id
  title
  goal_preview
  status
  status_label
  result_preview
  created_at
  updated_at
  latest_event_kind
  latest_event_summary
  pending_approval_ids
  next_action

审批详情可以设计成:

text 复制代码
OwnerConsoleApprovalDetail
  generated_at
  approval
  tool_input
  task
  recent_events
  actionability
  boundary

这类结构化 DTO 有几个好处。

第一,前端不关心 QQ 文案。

第二,后端可以继续复用已有查询函数。

第三,安全边界可以被字段显式表达。

第四,后续 HTTP API、CLI、甚至新的 Owner Console 入口都可以共享同一个 read model。

read model 不是数据库表

这里还有一个容易混淆的点:

text 复制代码
read model 不等于新表。

这次 P2.6 明确不改数据库 schema。

任务和审批已经有持久化数据:

text 复制代码
AgentTask
AgentTaskEvent
AgentApproval

read model 做的是组装:

text 复制代码
从 agent_tasks 查询任务和审批。
从 DiagnosticsGraph 拿诊断摘要。
从访问控制对象拿黑白名单。
从配置和角色卡读取设置摘要。
从 owner runtime service 复用现有只读依赖。

也就是说,read model 是给界面看的"读模型",不是新的存储模型。

这能避免为了做一个页面就急着加表。

对于一个还在演进中的 Agent 项目,这很重要。

页面范围为什么要分层

这次设计里,我把 Web Owner Console v0 页面分成两层。

第一层是 v0 必须清楚:

text 复制代码
Dashboard
Tasks
Task Detail
Approvals
Approval Detail
Diagnostics
Access Control

这些页面最能验证前面 Runtime service 解耦的价值。

因为它们主要依赖:

text 复制代码
任务/审批持久化查询
owner_agent_runtime
owner_read_runtime
diagnostics 只读能力
access control 只读能力

第二层是 v0 浅层快照:

text 复制代码
Memory
Settings

Memory 很重要,但它牵涉:

text 复制代码
MemoryRAG
MemoryAdmin
摘要
长期记忆
隐私正文
ProjectDocRAG 隔离

Settings 也很重要,但它牵涉:

text 复制代码
模型配置
base_url 脱敏
API Key 是否配置
角色卡列表
功能开关
Vision / TTS / MemoryRAG 配置

如果一上来把这两个页面做深,P2.6 会失控。

所以 v0 只设计浅层快照,不做修改能力。

为什么现在不写前端

直接写前端当然更有成就感。

但对这个项目来说,现在先写前端反而可能把错误的后端形态固定下来。

如果我今天先做一个页面,为了快,很可能会这样接:

text 复制代码
Web -> 调用现有 QQ 文本输出 -> 前端展示文本

然后明天想加表格,就开始解析文本。

后天想加按钮,就开始根据文本判断状态。

再后天想加脱敏,就发现原始数据和展示文案已经混在一起了。

所以我宁愿先慢一步:

text 复制代码
先定义 read model。
先确认页面范围。
先写清只读边界。
先写清未来审批操作不能绕过现有链路。

这样后续真正写 Web 时,前端接的是稳定数据结构,而不是一段"刚好能看"的 QQ 文案。

最关键的安全边界:Web 不能绕过审批链路

Web Owner Console 最容易犯的错误,是把"有按钮"误解成"可以直接调用函数"。

比如未来审批详情页里有一个确认按钮。

危险做法是:

text 复制代码
Web button -> clear_image_cache()
Web button -> add_access_item()
Web button -> delete_session_summary()

这相当于 Web 入口绕过了现有审批恢复机制。

正确路线应该是:

text 复制代码
Web approval decision
  -> 与 /agent 确认/拒绝 相同的审批决策服务
  -> decide_agent_approval(...)
  -> resume_agent_approval(...)
  -> approval resume tool registry
  -> approval_resume_enabled=true 检查
  -> owner_write_runtime
  -> agent task event 幂等记录

也就是说,Web 未来可以成为审批入口,但不能成为绕过审批的写入口。

P2.6 v0 更保守:

text 复制代码
只读。
不确认审批。
不拒绝审批。
不恢复工具。
不调用 owner_write_runtime。

但我仍然在设计文档里写了未来升级路线。

因为安全边界最好在设计阶段就写清楚,而不是等前端按钮出现后再补。

未来模块为什么先只给建议名

设计文档里提了两个未来建议名:

text 复制代码
owner_console_read_runtime.py
owner_console_read_models.py

但这次没有创建代码文件。

原因是 P2.6 还只是设计阶段。

现在真正要确定的是边界:

text 复制代码
Web read model 层不依赖 MessageEvent。
Web read model 层不调用 matcher.finish / bot.send。
Web read model 层不执行写操作。
Web read model 层不解析 QQ 文本。

至于最后是一个文件还是两个文件,后续实现时可以再根据代码量决定。

我更在意的是这层不要消失。

如果没有这层,Web 很容易直接接到 QQ adapter 或 formatter 上。

那前面做 Runtime service 解耦的价值就被浪费了。

当前 P2.6 的结果

这次 P2.6 最终落地的是一份设计文档:

text 复制代码
docs/web-owner-console-read-model-design.md

它主要写了这些内容:

text 复制代码
Web Owner Console v0 定位
现有 QQ 文本输出后端形态
页面地图
每个页面需要的数据
read model 结构草案
现有 Runtime service 如何复用
暂不做的能力边界
从只读到审批操作的升级路线

这次仍然不做:

text 复制代码
不写前端。
不接 HTTP。
不做登录。
不新增数据库表。
不改变 /agent。
不改变普通聊天。
不改变审批恢复。
不改变 MemoryRAG。
不改变 Diagnostics。
不改变 QQ 命令行为。

从功能角度看,它"什么也没新增"。

从架构角度看,它把下一步怎么接 Web Owner Console 说清楚了。

一点工程感受

做 Agent 项目时,很容易被"下一步做个界面"吸引。

界面很直观,也很有反馈。

但如果后端还没有数据契约,界面会反过来逼你做很多脆弱连接:

text 复制代码
解析文本。
猜状态。
复制业务判断。
绕过安全链路。
展示未脱敏内容。

这次我想避免这个方向。

对这个项目来说,Web Owner Console 的第一步不是按钮,也不是页面,而是 read model。

我希望未来每个入口都很清楚:

text 复制代码
QQ adapter 负责 QQ。
Web adapter 负责 Web。
owner runtime 负责主人侧业务运行时。
read model 负责结构化只读数据。
write runtime 必须在审批恢复链路之后才执行。

这样系统能力可以继续长,但边界不会跟着糊掉。

下一步

如果 P2.6 设计确认稳定,后续可以小步实现:

text 复制代码
第一步:新增 owner_console_read_runtime.py,只做只读 builder。
第二步:优先实现 Tasks / Approvals / Dashboard。
第三步:Diagnostics、Memory、Settings 先保留 summary_text / display_lines 过渡。
第四步:等 read model 稳定后,再考虑 HTTP API。
第五步:最后才是 Web 前端。

这个顺序看起来慢,但它更适合一个长期运行的 Agent 系统。

因为我想要的不是"能打开一个网页",而是:

text 复制代码
每个页面背后都有清楚的数据契约。
每个写操作背后都有审批链路。
每个入口都知道自己不能做什么。

这也是为什么我选择先做结构化 DTO,而不是直接把 QQ 文本搬到 Web 上。

相关推荐
析数塔1 小时前
2026前端框架深度解析:React Compiler、Angular Zoneless、Vue 3.5重写游戏规则
前端·vue.js·react.js
嘟嘟07171 小时前
Node.js spawn 实现迷你 Cursor 自动化终端|子进程完整解析
前端·后端·面试
程序员Agions1 小时前
ES2026,早该这样了
前端·javascript
ONE_SIX_MIX1 小时前
lightweight-charts-onesixth v2.8.1 → v3.1.1 更新总览
java·前端·python
恋猫de小郭1 小时前
Flutter material/cupertino 解耦最新进展,已经在做新样式了
android·前端·flutter
weedsfly1 小时前
纯函数与副作用——前端开发中最容易被忽视的质量保证
前端·javascript·面试
杖雍皓1 小时前
没有服务器如何构建现代 Web 应用:平台选型与组合策略
运维·服务器·前端·react.js·github·netlify·vercel
TrisighT1 小时前
鸿蒙 ArkWeb 混合开发全攻略:JSBridge 双向通信 + 实战登录注入
前端·harmonyos·arkts
sun_weitao1 小时前
微前端框架之qiankun 怎么用?vue3+vite+qiankun 和 react + qiankun
前端·react.js·前端框架