最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
Web Owner Console 还没写前端:我先给它设计 read model
时间:2026-07-10 11:21:07 编辑:袖梨 来源:一聚教程网
上一篇里,我把 MainAgent owner 侧 runtime 从 QQ adapter 里拆了出来:

owner_agent_runtime.py任务、审批、工作台、详情卡、审批恢复入口owner_read_runtime.py主人只读管理命令分发owner_write_runtime.py已审批主人写命令执行owner_runtime_factory.pytask/read/write runtime 总装层
这一步做完后,一个方向自然浮出来了:
既然 /agent 已经越来越像主人控制台,那是不是可以开始做 Web Owner Console?
但我这次没有直接写前端,也没有接 HTTP API。
我先做了一份更枯燥、但更关键的设计:
Web Owner Console read-model 设计。
简单说,就是先决定未来 Web 控制台要读哪些数据、这些数据应该长什么样、从哪里来,以及哪些东西绝对不能因为有了 Web 入口就绕过安全边界。
这篇文章记录我为什么没有直接复用现在的 QQ 文本输出,也为什么要提前设计结构化 DTO。
当前系统已经有一个 QQ 版主人控制台
现在 /agent 已经不只是一个“问项目上下文”的命令了。
它可以做很多主人侧只读查询:
/agent 看看最近错误/agent 角色卡列表/agent 模型配置/agent 访问控制/agent RAG 索引详情/agent MainAgent 最近观测/agent RootGraph 最近观测
也可以看任务和审批:
/agent 任务工作台/agent 下一步/agent 任务详情 最新/agent 审批状态/agent 审批详情 最新
写操作也已经有审批门控:
/agent 删除摘要 41/agent 清空图片缓存/agent 把群 123456 加入群白名单/agent 确认 最新
这些能力组合起来,已经很像一个“QQ 里的 Owner Console”。
但它的输出形态是 QQ 消息。
也就是说,现在后端大体是这样:
结构化数据 / 服务结果-> QQ 文本 formatter-> str-> matcher.finish(reply)
这对 QQ 很合适。
一条消息就应该是一段人能直接读的文本。
但如果未来要做 Web Owner Console,直接拿这段文本塞进网页,就会埋下很多坑。
现有 QQ 输出的三种后端形态
我这次先把现有 QQ 输出形态梳理了一遍。
它其实不是完全乱拼字符串,背后有三类来源。
第一类是 list[str]。
比如:
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。
比如:
run_diagnostics(...)run_memory_retrieval(...)run_memory_admin(...)
Graph 里已经聚合了状态,但最后给 /agent 的仍然是一段可发送的文本。
第三类是任务和审批 formatter。
任务和审批底层其实已经有结构化对象:
AgentTaskAgentTaskEventAgentApproval
但 QQ 入口会立刻把它们送进 formatter:
format_agent_task_list(tasks) -> strformat_agent_task_detail(task, events, approvals) -> strformat_agent_approval_list(approvals) -> strformat_agent_approval_detail(approval, task, events) -> strformat_agent_task_workbench(...) -> str
这些 formatter 产出的内容适合 QQ:
Agent 审批详情卡 #19审批ID:#19状态:待确认任务ID:#18工具:owner_write_command风险:medium原因:owner write command requires approval输入摘要:...
但它不是 Web 后端接口。
Web 如果想做表格、筛选、排序、状态标签、详情页、按钮禁用态,就不应该去解析这段文本。
为什么不能直接复用 QQ 文本
最省事的做法当然是:
Web 页面请求一个接口。后端调用现有 /agent 逻辑。拿到 str。前端原样展示。
这个方案第一天看起来很快,第三天就会开始难受。
1. 文案变化会变成接口破坏
比如 QQ 文本里有一行:
审批ID:#19 [待确认] 任务ID:#18 owner_write_command / medium
如果 Web 前端靠解析这行拿 approval_id、status、task_id,那么以后我只要把文案改成:
审批 #19:待主人确认,关联任务 #18
前端解析就坏了。
文案应该是展示层,不应该成为数据契约。
2. Web 页面天然需要结构
任务列表不是一段文本,它至少需要:
task_idtitlegoal_previewstatusstatus_labelcreated_atupdated_atlatest_event_summarypending_approval_ids
审批列表也不是一段文本,它需要:
approval_idtask_idtask_titletool_namerisk_levelreason_previewstatuscreated_atexpires_atdecided_at
这些字段到了前端以后,才能自然变成:
表格列筛选条件状态 badge详情链接风险颜色空状态禁用按钮原因
如果后端只给一段文本,前端要么只能做一个大文本框,要么就开始写脆弱的正则解析。
3. 安全脱敏需要字段级控制
审批详情里有 tool_input_json。
它可能包含:
工具参数记忆内容摘要 ID名单目标模型配置相关信息
在 QQ 里,我可以用短文本摘要展示:
输入摘要:{"command": "delete_session_summary", "summary_id": 41}
但 Web 以后可能会有折叠详情、复制按钮、JSON 预览。
这时如果后端只给一段 formatter 文本,脱敏边界就很难说清楚。
更稳的形式是 read model 里明确写:
tool_input:preview_json: strredacted: booltruncated: bool
这样前端知道自己看到的是预览,不是完整原文。
4. 未来按钮状态不能靠前端猜
Web Console 以后可能会展示审批确认按钮。
但按钮能不能点,不能靠前端根据文案猜:
如果文本里有“待确认”,按钮就可点。
这是危险的。
正确做法应该是后端 read model 给出只读的 actionability metadata:
can_approvecan_rejectresume_enabledblocked_reasonfuture_operation_only
v0 阶段这些字段只用于描述未来 UI 状态,不执行操作。
真正实现时,它们也必须由后端根据审批状态、tool registry 和 approval_resume_enabled 计算。
前端不应该自己判断某个工具能不能恢复。
DTO 在这里到底是什么意思
我这里说的 DTO,不是说马上要写一堆复杂的类。
它更像是一个明确的数据契约:
这个页面需要什么字段。这些字段来自哪里。哪些字段是展示摘要。哪些字段是脱敏预览。哪些字段只是未来操作提示。哪些能力当前明确不可用。
比如任务列表可以设计成:
OwnerConsoleTaskListgenerated_atfilterstotal_visiblerowsboundaryOwnerConsoleTaskRowtask_idtitlegoal_previewstatusstatus_labelresult_previewcreated_atupdated_atlatest_event_kindlatest_event_summarypending_approval_idsnext_action
审批详情可以设计成:
OwnerConsoleApprovalDetailgenerated_atapprovaltool_inputtaskrecent_eventsactionabilityboundary
这类结构化 DTO 有几个好处。
第一,前端不关心 QQ 文案。
第二,后端可以继续复用已有查询函数。
第三,安全边界可以被字段显式表达。
第四,后续 HTTP API、CLI、甚至新的 Owner Console 入口都可以共享同一个 read model。
read model 不是数据库表
这里还有一个容易混淆的点:
read model 不等于新表。
这次 P2.6 明确不改数据库 schema。
任务和审批已经有持久化数据:
AgentTaskAgentTaskEventAgentApproval
read model 做的是组装:
从 agent_tasks 查询任务和审批。从 DiagnosticsGraph 拿诊断摘要。从访问控制对象拿黑白名单。从配置和角色卡读取设置摘要。从 owner runtime service 复用现有只读依赖。
也就是说,read model 是给界面看的“读模型”,不是新的存储模型。
这能避免为了做一个页面就急着加表。
对于一个还在演进中的 Agent 项目,这很重要。
页面范围为什么要分层
这次设计里,我把 Web Owner Console v0 页面分成两层。
第一层是 v0 必须清楚:
DashboardTasksTask DetailApprovalsApproval DetailDiagnosticsAccess Control
这些页面最能验证前面 Runtime service 解耦的价值。
因为它们主要依赖:
任务/审批持久化查询owner_agent_runtimeowner_read_runtimediagnostics 只读能力access control 只读能力
第二层是 v0 浅层快照:
MemorySettings
Memory 很重要,但它牵涉:
MemoryRAGMemoryAdmin摘要长期记忆隐私正文ProjectDocRAG 隔离
Settings 也很重要,但它牵涉:
模型配置base_url 脱敏API Key 是否配置角色卡列表功能开关Vision / TTS / MemoryRAG 配置
如果一上来把这两个页面做深,P2.6 会失控。
所以 v0 只设计浅层快照,不做修改能力。
为什么现在不写前端
直接写前端当然更有成就感。
但对这个项目来说,现在先写前端反而可能把错误的后端形态固定下来。
如果我今天先做一个页面,为了快,很可能会这样接:
Web -> 调用现有 QQ 文本输出 -> 前端展示文本
然后明天想加表格,就开始解析文本。
后天想加按钮,就开始根据文本判断状态。
再后天想加脱敏,就发现原始数据和展示文案已经混在一起了。
所以我宁愿先慢一步:
先定义 read model。先确认页面范围。先写清只读边界。先写清未来审批操作不能绕过现有链路。
这样后续真正写 Web 时,前端接的是稳定数据结构,而不是一段“刚好能看”的 QQ 文案。
最关键的安全边界:Web 不能绕过审批链路
Web Owner Console 最容易犯的错误,是把“有按钮”误解成“可以直接调用函数”。
比如未来审批详情页里有一个确认按钮。
危险做法是:
Web button -> clear_image_cache()Web button -> add_access_item()Web button -> delete_session_summary()
这相当于 Web 入口绕过了现有审批恢复机制。
正确路线应该是:
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 更保守:
只读。不确认审批。不拒绝审批。不恢复工具。不调用 owner_write_runtime。
但我仍然在设计文档里写了未来升级路线。
因为安全边界最好在设计阶段就写清楚,而不是等前端按钮出现后再补。
未来模块为什么先只给建议名
设计文档里提了两个未来建议名:
owner_console_read_runtime.pyowner_console_read_models.py
但这次没有创建代码文件。
原因是 P2.6 还只是设计阶段。
现在真正要确定的是边界:
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 最终落地的是一份设计文档:
docs/web-owner-console-read-model-design.md
它主要写了这些内容:
Web Owner Console v0 定位现有 QQ 文本输出后端形态页面地图每个页面需要的数据read model 结构草案现有 Runtime service 如何复用暂不做的能力边界从只读到审批操作的升级路线
这次仍然不做:
不写前端。不接 HTTP。不做登录。不新增数据库表。不改变 /agent。不改变普通聊天。不改变审批恢复。不改变 MemoryRAG。不改变 Diagnostics。不改变 QQ 命令行为。
从功能角度看,它“什么也没新增”。
从架构角度看,它把下一步怎么接 Web Owner Console 说清楚了。
一点工程感受
做 Agent 项目时,很容易被“下一步做个界面”吸引。
界面很直观,也很有反馈。
但如果后端还没有数据契约,界面会反过来逼你做很多脆弱连接:
解析文本。猜状态。复制业务判断。绕过安全链路。展示未脱敏内容。
这次我想避免这个方向。
对这个项目来说,Web Owner Console 的第一步不是按钮,也不是页面,而是 read model。
我希望未来每个入口都很清楚:
QQ adapter 负责 QQ。Web adapter 负责 Web。owner runtime 负责主人侧业务运行时。read model 负责结构化只读数据。write runtime 必须在审批恢复链路之后才执行。
这样系统能力可以继续长,但边界不会跟着糊掉。
下一步
如果 P2.6 设计确认稳定,后续可以小步实现:
第一步:新增 owner_console_read_runtime.py,只做只读 builder。第二步:优先实现 Tasks / Approvals / Dashboard。第三步:Diagnostics、Memory、Settings 先保留 summary_text / display_lines 过渡。第四步:等 read model 稳定后,再考虑 HTTP API。第五步:最后才是 Web 前端。
这个顺序看起来慢,但它更适合一个长期运行的 Agent 系统。
因为我想要的不是“能打开一个网页”,而是:
每个页面背后都有清楚的数据契约。每个写操作背后都有审批链路。每个入口都知道自己不能做什么。
这也是为什么我选择先做结构化 DTO,而不是直接把 QQ 文本搬到 Web 上。