这两个契约方向相反:平台把本次调用写入 /oasn/in/invocation.json 供 Program 读取;Program 则在最终回复中写严格的 oasn-webui JSON block,供平台解析为业务页面。不要把两者合并,也不要让浏览器读取前者。
| 方向 | 载体 | 谁写、谁读 | 用途 |
|---|---|---|---|
| 用户 → Program | /oasn/in/invocation.json | 平台写;Program 服务端只读 | 取得本次用户文本、参数和附件路径。 |
| Program → 用户 | 最终 Assistant 回复中的 oasn-webui fenced block | Agent main 写;平台解析 | 把已启动的页面投影为 WebUI view/canvas。 |
以下是可安全展示的业务字段示例。它说明文件结构,但不是让用户手写的请求体;真实文件由平台生成,还包含运行契约等平台字段。
{
"schema_version": "oasn.invocation.v1",
"invocation_id": "inv-example",
"run_id": "run-example",
"input": {
"user_prompt": "读取附件并生成可交互销售看板",
"args": [],
"attachments": [
"/oasn/in/files/file-sales-csv"
]
}
}
| 字段 | 类型 | Program 用法 |
|---|---|---|
input.user_prompt | string | 本次用户文本;空白时应要求补充,不臆造任务。 |
input.args | array | 当前正式投影通常为空数组;除非业务契约明确要求,否则不要依赖。 |
input.attachments | string[] | 本次 Run 的只读服务端路径;只读取列出的条目。 |
只有业务确实需要原始结构时才读取文件;普通 OpenClaw Skill/Plugin 通常直接接收消息。需要解析时,可在服务端最小化读取:
from json import loads
from pathlib import Path
payload = loads(Path("/oasn/in/invocation.json").read_text(encoding="utf-8"))
business_input = payload.get("input", {})
user_prompt = business_input.get("user_prompt", "")
attachments = business_input.get("attachments", [])
if not isinstance(user_prompt, str) or not isinstance(attachments, list):
raise ValueError("INVALID_INVOCATION_INPUT")
除业务字段外,真实文件可能包含短期运行材料。禁止用 cat 把全文写入日志,禁止通过 WebUI API 返回全文,也不要扫描 /oasn/in、附件父目录或历史目录。附件路径不能直接暴露给浏览器或拼成另一个 OpenAgent 的附件。
官网版本表单目前没有 WebUI 端口控件;端口通过经过 Skill 校验的 oasn-sa-dev CLI 配置。先确认版本为 0.7.3 或更新,并查询当前完整列表:
oasn-sa-dev --version
oasn-sa-dev version webui-ports get --version-id <agent_version_id>
在开发者明确确认要暴露的完整端口列表后执行 set;它是整体替换,不是追加:
# 把完整列表替换为 7860、8765
oasn-sa-dev version webui-ports set --version-id <agent_version_id> --ports 7860,8765
# 只有明确取消全部 WebUI 时才清空
oasn-sa-dev version webui-ports set --version-id <agent_version_id> --clear
1..65535,且不能使用平台保留端口 22、18789、18790。sync_status=ready 只证明声明同步完成;还要确认 proxy_status=ready,此时才交付当前 webui_urls。需要保留全部端口并创建新一代业务链接时,先 get 并取得明确刷新授权,再执行:
oasn-sa-dev version webui-ports refresh --version-id <agent_version_id>
| 状态 | 含义 | 动作 |
|---|---|---|
pending | 新映射正在收敛 | 只继续 get;不换参数重复 set/refresh。 |
ready | 同步或代理元数据检查通过 | 同时核对 sync_status 与 proxy_status,再做真实 HTTP 验收。 |
expired | Workspace 连接或业务代理会话过期 | 先区分两种期限;必要时经授权 workspace resume,只在业务链接需换代时 refresh。 |
revoked | 旧业务会话已撤销 | 同端口 set 不能复活;经授权 refresh。 |
unavailable | 当前无法确认代理有效性 | 报告服务/版本问题;不默认清空或刷新。 |
not_configured | Version 没有声明端口 | 确认完整列表后 set。 |
业务进程监听 0.0.0.0:<已声明端口>,不能只监听 localhost。CLI 只建立映射,不会替你启动、停止进程或证明健康。
至少让目标页面及业务 API 返回可核对内容。启动方式由 Program 自己决定;当前契约不要求固定 start-webui.sh 或固定健康路径。
WebUI 挂在 /webui/proxy/<id>/ 子路径。fetch('/api/health')、src="/vendor/app.js" 会丢掉前缀;页面、SPA 路由、静态资源和 WebSocket 都应从当前 URL 解析 base。
const prefix = window.location.pathname.match(/^.*\/webui\/proxy\/[^/]+\//)?.[0]
const base = prefix
? new URL(prefix, window.location.origin)
: new URL('./', window.location.href)
fetch(new URL('api/health', base))
const scriptUrl = new URL('vendor/app.js', base)
不要在页面代码中写死 Sandbox IP、127.0.0.1、开发 Workspace 的公网 URL 或外部域名。注意:下一节最终回复的内部 WebUI 声明恰好要用 loopback URL;它与浏览器页面内部的相对地址规则并不冲突。
页面已经启动并可访问后,让 main 的最终 Assistant 回复包含普通说明文字,以及一个页面一个的小写 fenced block:
看板已生成,可以打开查看。
```oasn-webui
{"type":"webui","title":"销售数据看板","url":"http://127.0.0.1:7860/results?range=q3#summary","defaultOpen":true}
```
| 字段 | 严格规则 |
|---|---|
type | 必须是字符串 "webui"。 |
title | 必须是非空字符串;写用户看得懂的页面名称。 |
url | 同 Sandbox 页面使用 http://127.0.0.1:<已声明端口>/<业务路径> 或 localhost;字段名是 url,不是 uri。 |
defaultOpen | 必须是真正的 JSON boolean true/false,不能写成字符串 "true"。 |
token、description 或自定义字段。oasn-webui;不要写成普通 json block,也不要在 fence info 后附加参数。webui_urls 复制进 block。127.0.0.1/localhost;外部页面必须是安全的公开 HTTPS,不允许公网 HTTP 或私网地址。defaultOpen=true,但为避免客户端连续打开多个页面,交互规范要求最多一个页面设为 true;这是开发者规则,不要误解成服务端会替你拒绝。# 错:defaultOpen 是字符串
{"type":"webui","title":"看板","url":"http://127.0.0.1:7860/","defaultOpen":"true"}
# 错:相对 URL 且多了自定义字段
{"type":"webui","title":"看板","url":"/results","defaultOpen":true,"token":"..."}
# 错:把平台输出字段 uri 当成开发者输入字段
{"type":"webui","title":"看板","uri":"https://example.invalid/","defaultOpen":true}
JSON 语法错误、字段不完整或类型错误的 block 不会成为 WebUI view,通常原样留在正文。结构有效但 URL 使用未声明端口、私网地址或敏感状态时,正式投影会以 OPENCLAW_FINAL_REPLY_INVALID 失败关闭;后续结果层再次校验失败会返回 RUN_RESULT_INVALID 或 MCP_INVOCATION_RESULT_INVALID,不会用部分成功掩盖问题。
开发者只负责上一节的最终回复,不直接构造下面对象。平台校验并代理后,客户端读取可信的 structuredContent.views[];这里字段变为 uri:
{
"content": [
{"type": "text", "text": "看板已生成,可以打开查看。"}
],
"structuredContent": {
"view": "invocation_result",
"status": "completed",
"invocation_id": "inv-example",
"views": [
{
"type": "webui",
"uri": "https://平台受控地址/webui/proxy/.../results?range=q3#summary",
"title": "销售数据看板",
"defaultOpen": true
}
]
},
"isError": false
}
集成客户端应信任 structuredContent.views[],不要重新解析正文里的 fence。defaultOpen=false 仍应保留为可见入口,只是不自动打开;浏览器打开失败也不能改写 Agent 已完成的终态或自动重新调用 Agent。
确认进程监听 0.0.0.0,用 loopback 访问目标页面和 API,并核对响应业务内容。
运行 version webui-ports get,同时确认 ports、sync_status=ready、proxy_status=ready 和当前 webui_urls。
打开每个新 URL,验证页面、嵌套路由、静态资源、API;有 WebSocket 时验证往返和断线重连。410 表示业务代理会话无效,不等于 Owner OAuth 失效;502 先检查业务监听。
让 Agent 真实返回 block,确认它被投影为 view/canvas,path/query/fragment 保留且没有敏感状态;发布上线后再做一次正式调用,开发 WebChat 不能替代。
当前实现计划从同一会话已提交的进程快照恢复正式 WebUI,端口数量不决定恢复资格,也不要求开发者额外提供固定重启脚本。但在真实单端口、多端口任务完成后重新打开原结果链接并通过验收前,不得承诺页面永久可重开。开发 Workspace 链接不能代替这项正式验收。
input.user_prompt 和本次 input.attachments;不泄露完整文件。sync_status 与 proxy_status 均 ready,所有页面已真实验证。views[].uri。