会话仪表盘功能的技术设计文档,在实现之前和实现过程中编写。它是构建工作的权威依据。当该功能发布后,
/web/dashboard 将成为面向用户的页面,而此页面将继续作为架构参考。远景
与智能体协作在今天本质上是一条文本流。仪表板把它变成了一个工作台:智能体渲染实时、可交互的小部件;用户将它们固定到一个持久化表面上;聊天停靠在侧边(或隐藏),主内容则是画板。你无需离开会话,就能从“与智能体对话”变成“操作智能体为你构建的控制面板”。 原则:- 画板是会话的一种视图,而不是一个新对象。 每个会话(线程)都有两个视图:对话记录和画板。没有固定小部件的会话就是普通聊天。固定一个小部件后,画板就存在了。画板继承会话的身份、智能体所有权、命名、固定状态和生命周期。没有
dashboard_create,没有画板注册表,也没有单独的 ACL 模型。 - 智能体对等。 用户在画板上能做的一切,智能体都可以通过工具做到:添加/更新/移除小部件、排列它们、管理标签页、切换可见标签页、停靠或隐藏聊天。
- 原生,而非嵌入式。 画板是 Control UI 外壳中的 Lit 组件(与应用其余部分使用相同的设计系统)。只有小部件的_内容_被隔离在 iframe 中。没有地址栏,没有浏览器外壳。
- 小型智能体表面。 小部件通过稳定名称寻址并就地更新。布局是一个流式自动紧凑网格;智能体只描述尺寸和锚点,从不使用像素或坐标。
- 以能力而非信任为基础。 小部件代码是任意的、由智能体编写的 HTML/JS,运行在强沙箱中。访问范围(网关数据、动作、网络)仅通过声明式、由操作者授予的能力清单存在。
概念
UX 流程
- 完成流程: agent 在任意聊天中调用
show_widget→ widget 以内联方式渲染在 transcript 中,与现在完全一致 → 悬停时显示 固定到仪表板 → widget 出现在该会话的面板中。agent 也可以传入pin: true来执行相同操作。 - **面板视图:**拥有面板的会话会获得视图切换选项(聊天 / 分栏 / 仪表板)。分栏视图 = 标签页栏(仅在标签页数 >1 时显示)+ 流式网格 + 停靠的聊天 窗格;仪表板视图则相同,但不包含聊天。聊天停靠窗格可调整大小,也可通过切换器的停靠位置选择器移动到(左侧 / 右侧 / 底部)。 每个标签页的停靠状态都会被记住。
- **拖拽:**用户拖拽 widget;网格会自动紧凑排列(widget 向上浮动,邻近元素重新布局)。通过控制柄调整大小时会吸附到尺寸步长。不允许任何人进行像素级定位。
- **重置警告:**在拥有面板的会话中执行
/new//reset时,Web UI 会请求确认(“上下文将被重置,仪表板会保留”),并保留面板。 - **侧边栏:**已固定的会话在拥有面板时显示其面板缩略视图。 Home 会话的面板是默认的“agent 仪表板”。
- 交互(分为三个层级,见下文):静默状态事件、可见的提示发送,以及自动化触发器。
交互层级
- 状态事件(默认)。 模型应该知道但不需要回应的小部件 UI 交互。
bridge.emitState({...})会追加一条结构化的会话通知(与群组活动通知使用相同机制)。不会启动新的 agent 回合;模型会在下一次运行时看到累积的通知。 - 提示(显式对话)。
bridge.sendPrompt(text)— 需要用户激活;向会话发送一条可见的用户消息(停靠的聊天窗口会显示它)。有速率限制;每次发送都需要用户确认,除非小部件持有prompt能力授权。 - 自动化。
bridge.runAction(name, args)— 触发一个在清单中声明的动作。初始动词集合:cron.trigger(立即运行现有的 cron 作业)和binding.refresh。Cron 作业已经在可见、隔离的运行会话中执行,并且可以使用更便宜的模型:这就是“小模型驱动小部件”的路径。任何地方都没有隐藏会话。
Widget 模型与托管
Widget 的 HTML/JS 由 agent 编写(通常通过show_widget),包裹在标准文档外壳中(CSP meta、尺寸上报器、桥接引导),并在 <iframe sandbox="allow-scripts"> 中渲染(绝不使用 allow-same-origin)。
- 内联(transcript)widget 保持当前的 canvas-document 管线: 写入状态目录,由 gateway 提供服务,按作用域清理,无需审批(它们从构造上就是无能力的——prompt 发送由用户确认)。
- Board widget 是会话状态:字节内容保存在所属 agent 的 SQLite
DB(
board_widgets)中,由一个核心 gateway 路由提供服务 (/__openclaw__/board/<agentId>/<sessionKey>/<name>/),该路由读取 DB。 将 transcript widget 固定会复制这些字节。容量上限:每个 widget 256 KB, 每个 board 48 个 widget。 - 原地更新: 以相同
name重新发出一个 widget 会替换这些字节, 提升revision,广播board.changed,并且只会让活动视图重新加载那个 iframe。 - 字节冻结: 授予的能力绑定到 widget 字节的 sha256。变更字节后,只有在新
revision 声明的 manifest 是已授予 manifest 的子集时,才保留
data/net/actions授权;一旦 manifest 扩大,则会重新提示操作者。
Widget 承载内容;MCP 应用只是内容的一种
Widget 是 OpenClaw 的原语:带名称、已固定、已设大小、会话归属的 board 单元, 以及一条授权记录。其内部渲染的内容只是某一种内容类型:html— 由 agent 通过show_widget编写,字节存放在 board 存储中。mcp-app— 第三方 MCP 应用视图(来自已配置服务器的ui://资源),托管在 widget 单元内部。
show_widget 代码可以像今天一样简短,也永远不需要知道 MCP Apps 规范的存在。
其下方共享的基础设施(简化就落在这里):
- 一个沙箱主机。
htmlwidget 通过 MCP 应用已经发布的同一套加固管线渲染 (在专用沙箱 origin 上的双 iframe、每个 widget 声明的 CSP、以及失败即关闭的解码), 而不是再造一套专门的 iframe 主机。代理按值接收 HTML,因此本地内容是自然场景。 - 一个授权模型。 无论 widget 类型如何,它的可达范围都是一份已授予的 allowlist:
对于
htmlwidget,是主机工具;对于mcp-appwidget,是服务器对应用可见的工具 (通过现有的allowedAppToolNames机制实现,并改为按 widget 持久化,而不是按一次 mint 运行持久化)。 htmlwidget 的主机工具(通过 widget bridge 暴露,并按授权校验):openclaw.prompt.send— tier 2;通过可见的 composer 转发,除非已授权否则需要用户确认openclaw.state.emit— tier 1 会话通知(合并、限制大小)openclaw.data.read— 参数化只读绑定(现有的允许列表 read RPC 集),在 gateway 侧解析openclaw.cron.trigger— tier 3 自动化
net= CSP。 网络可达性使用已经发布的每个 widget 的 CSP 声明 (connect-srcorigins)——这个会自动更新的天气 widget 直接从沙箱中获取其 API, 不需要 gateway 介入。- 授权。 未声明任何内容的 widget 会立即渲染(已沙箱化,
default-src 'none',逐条确认 prompt 发送)——与今天的内联聊天 widget 的信任等级相同。 声明了工具/origin 的 widget 会在 board 上进入pending:一个占位卡片会以人类可读的方式列出这些内容,并提供一键 允许/拒绝。 授权按 widget 名称生效;对于htmlwidget,它们是字节冻结的(sha256),而变更后的字节只有在声明缩小的情况下才保留授权。 - 作者包装层。 文档外壳注入
window.openclaw.prompt、window.openclaw.state、window.openclaw.data和window.openclaw.cron,作为稳定的作者 API。Dashboard 调用共享一条以 view-ticket 绑定的请求通道;尺寸上报和主题 token 仍然作为单独的主机通知存在。
插件能力声明
已启用的插件可以通过openclaw.plugin.json 中的 dashboard.dataBindings 和 dashboard.actionVerbs 扩展 widget 主机能力。插件本地 id 会变成以插件 id 为前缀的授权名称,例如 workboard.cards.list 和 workboard.dispatch;插件 id 段中的 % 和 . 会被转义,因此不同插件/local-id 的拆分不能继承同一条已持久化授权。插件注册期间,OpenClaw 会验证每个 binding 都指向同一插件注册的、带 operator.read 的 RPC,并且每个 action 都指向一个带 operator.write 的 RPC;无效声明会导致插件加载失败。经过验证的 registry 只会在插件生命周期变化时重建,而 widget 授权仍然是按 widget 粒度、并与字节和版本绑定的。
建模残留:WebRTC 数据通道
沙箱 CSP 发出提议中的webrtc 'block' 指令,但
Chromium 当前的 CSP 指令集合
并未实现它。因此,可脚本化的 widget 在当前 Chromium 中仍可使用 WebRTC data channel 进行外发。这个相同的残留也已经存在于内联聊天 widget 和 main 分支上的 MCP Apps 主机中。
接受的权衡: OpenClaw 不会因为这一残留而对可脚本化 widget 设门。Widget 内容只有通过操作者授予、字节冻结的 data:read 能力才能获得敏感 OpenClaw 数据,而沙箱 Permissions Policy 会阻止摄像头和麦克风访问。DOM API 防护只是尽力而为的纵深防御,不是安全边界,属于后续加固工作。
Transcript 展示:一个 widget 卡片
内联展示统一到 widget 原语上。当某个工具结果携带 UI ——show_widget 输出或带 app resource 的 MCP 工具结果时,系统会生成一个临时的、自动命名的 widget(会话作用域、会被清理),并且 transcript 会渲染为一张单一的 widget 卡片,按内容类型分发处理。MCP 应用的自动展示完全符合规范预期(零额外模型工作);其底层本质上就是一个 widget。这样会删除聊天渲染中并行存在的 mcpApp 特殊分支(界面门控、单独去重),让所有内联 UI 都拥有相同的固定入口,并使 widget registry 成为主要的重新打开路径(transcript 扫描重建仍作为从未固定历史的备用方案)。只读的带票据独立主机与 boards 作为持久化重新打开表面存在重叠——这是一个可在 T6 评估的整合候选,而不是默认前提。
组合方式:v1 是网格相邻(agent chrome widget 与 app widget 在同一个 tab 中并排)。v2 增加主机管理的应用插槽——agent widget 的 HTML 声明一个插槽区域,由主机将真实 app 视图作为相邻的沙箱进行合成。app 永远不会渲染在 agent 的 iframe 内:嵌套会破坏 bridge 身份,并可能对已授予的 app UI 进行覆盖/clickjacking,所以这个插槽是布局契约,而不是嵌入。
服务端来源的 widget(固定的 MCP 应用)
在统一主机下,固定一个第三方 MCP 应用只是一个内容来自服务端而不是存储中的 widget:board_widgets 保存的是描述符(serverName、toolName、uiResourceUri、来源 toolCallId + sessionKey),而不是 HTML 字节;board 会在聊天回合 10 分钟 TTL 之后重新 mint 该视图租约(在过期时重新抓取 ui:// 资源)。聊天内联 MCP 应用视图获得与 agent widget 相同的 固定到 dashboard 入口。重新打开的视图按设计今天就是只读;希望保持交互性的已固定应用,会获得对服务器应用可见工具的持久授权(固定时会把明确的 allowlist 展示给操作者),并与 mint 运行解耦。未授权的固定项仍然只读——但对展示型 dashboard 仍然有用。v1 只固定到来源会话的 board;跨会话固定需要 lease broker,并需等待。请与 open PR #109807(ui/message composer 路由、主题/尺寸传播)协同。
WorkBoard 集成
WorkBoard 集成计划保持 cards 和 boards 由插件拥有,同时通过现有的sessionKey 和 runId 将分发的 cards 重新接回它们的会话 boards,通过插件声明的 bindings 和 actions 暴露 WorkBoard feeds 和 dispatch,并将这些结果与现有的 html 和 mcp-app widget 类型进行组合,而不是引入一种 WorkBoard 专用的 widget 类型。
布局:流式网格
12 列,固定行高,自动压缩(向上重力、拖拽时侧向推挤 — gridstack 语义,原生实现;网格计算保持纯净且不依赖 DOM)。每个标签页的组件布局状态:{ name, w (1-12), h (rows) } 加上顺序。代理词汇表:
size:sm(3×3)·md(6×4)·lg(8×6)·xl(12×8)·full(单组件标签页)after: <widgetName>可选的排序锚点;省略 = 追加- 用户可自由拖拽/调整大小;相同的顺序+尺寸模型可往返保持一致。
数据模型(每个代理的数据库)
agents/<agentId>/agent/openclaw-agent.sqlite 中的新表
(需要提升代理数据库的架构版本——在此功能上线前需要运营方签字确认):
sessionKey 对应的任何行。删除一个会话会删除其面板行。/new//reset 不会影响它们。
协议表面
RPC(核心方法表,gateway-protocol 中的 TypeBox schemas):
board.get { sessionKey }→ 标签页 + 小部件元数据(不含字节)—operator.readboard.update { sessionKey, ops[] }— 标签页 CRUD/重排、小部件移动/调整大小/ 移除/取消固定、停靠状态、聚焦标签页 —operator.writeboard.widget.put { sessionKey, name, html, manifest, placement }—operator.write(代理工具路径和固定路径)board.widget.grant { sessionKey, name, decision }—operator.approvalsboard.event { ticket, payload }— 绑定 ticket 的一级状态事件接收; 旧的受信任主机{ sessionKey, widget, payload }形状仍然保留 —operator.writeboard.prompt.authorize { ticket }— 返回可见提示发送是否 仍需要每次点击确认 —operator.readboard.data.read { ticket, bindingId, params? }— 网关侧白名单化的 核心或活动插件读取绑定解析 —operator.readboard.action { ticket, action, ... }— 通过现有的 cron 立即运行路径或活动插件已验证的 action 动词进行精确授权自动化分发 —operator.write
EVENT_SCOPE_GUARDS 中,读取作用域):
board.changed { sessionKey, revision, widget? }— 持久化状态已更改; UI 重新拉取(当存在widget时也会重载一个 iframe)。board.command { sessionKey, command }— 临时 UI 驱动(代理切换 可见标签页,切换聊天停靠栏)—ui.command模式。
代理工具
总共三个工具(核心工具,始终注册;渲染是否启用取决于当前的inline-widgets 客户端能力):
show_widget { title, widget_code, name?, pin?, size?, tab?, after?, capabilities? }— 按名称创建/更新;pin将其放置到面板上。 不使用name/pin时,其行为与当前完全一致(内嵌、临时)。dashboard { action, ... }— 面板管理操作:read、tab_create、tab_update、tab_delete、tabs_reorder、widget_move、widget_remove、unpin、focus_tab、set_chat_dock。- 现有的
automations工具负责自动化层;无需新增工具。
[dashboard] 用户在小组件 weather(标签页 main)上点击了“刷新”。
这替代了什么
extensions/workspaces已删除。 该功能具有实验性,enabledByDefault: false,从未进入稳定版本(最早出现在 2026.7.2 beta 版中)。无需迁移;如果存在,doctor 规则会移除过时的<stateDir>/workspaces/。 吸收的想法:纯网格数学、桥接安全模型(端口引导、绑定门控、速率限制)、字节冻结审批。- 小部件托管从
extensions/canvas迁移到核心。 canvas 文档存储、文档包装器、HTTP 提供,以及show_widget工具都成为核心(src/canvas/);插件保留 node-canvas 控制工具(canvas)和 A2UI。pluginSurfaceUrls["canvas"]公告和/__openclaw__/canvas路径是原生客户端合同的一部分并保持稳定。Discord 会话继续保留 Discord 自有的show_widget变体。
非目标(本项目)
- 多用户看板共享/ACL(未来;将通过会话共享实现)。
- 原生 macOS/iOS 看板渲染(它们在嵌入 Control UI 的任何地方都会获得该能力;内联组件路径保持不变)。
- 内置数据组件(会话/用量/cron 卡片)——能力桥接加上代理编写的组件已覆盖 v1;内置类型注册表可以后续再加入。
实施计划
独立工作树,由 Codex 构建,按顺序审查并合入。先合入,再修复。
每个仓库的验证规则:本地运行聚焦的 vitest,在 Crabbox/Testbox 上运行完整门禁,每次合入前都要执行
$autoreview,T6 需要实时证明。