对:正式用户输入有结构化 JSON,但它不是 WebUI 输出 JSON。

这两个契约方向相反:平台把本次调用写入 /oasn/in/invocation.json 供 Program 读取;Program 则在最终回复中写严格的 oasn-webui JSON block,供平台解析为业务页面。不要把两者合并,也不要让浏览器读取前者。

先分清两份 JSON

方向载体谁写、谁读用途
用户 → Program/oasn/in/invocation.json平台写;Program 服务端只读取得本次用户文本、参数和附件路径。
Program → 用户最终 Assistant 回复中的 oasn-webui fenced blockAgent 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_promptstring本次用户文本;空白时应要求补充,不臆造任务。
input.argsarray当前正式投影通常为空数组;除非业务契约明确要求,否则不要依赖。
input.attachmentsstring[]本次 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")
不要输出、记录或截图完整 invocation.json

除业务字段外,真实文件可能包含短期运行材料。禁止用 cat 把全文写入日志,禁止通过 WebUI API 返回全文,也不要扫描 /oasn/in、附件父目录或历史目录。附件路径不能直接暴露给浏览器或拼成另一个 OpenAgent 的附件。

二、用 CLI 0.7.3 声明业务端口

官网版本表单目前没有 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

需要保留全部端口并创建新一代业务链接时,先 get 并取得明确刷新授权,再执行:

oasn-sa-dev version webui-ports refresh --version-id <agent_version_id>

端口状态怎么判断

状态含义动作
pending新映射正在收敛只继续 get;不换参数重复 set/refresh。
ready同步或代理元数据检查通过同时核对 sync_statusproxy_status,再做真实 HTTP 验收。
expiredWorkspace 连接或业务代理会话过期先区分两种期限;必要时经授权 workspace resume,只在业务链接需换代时 refresh。
revoked旧业务会话已撤销同端口 set 不能复活;经授权 refresh。
unavailable当前无法确认代理有效性报告服务/版本问题;不默认清空或刷新。
not_configuredVersion 没有声明端口确认完整列表后 set。

三、实现能在代理子路径运行的服务

  1. 监听全部容器网卡

    业务进程监听 0.0.0.0:<已声明端口>,不能只监听 localhost。CLI 只建立映射,不会替你启动、停止进程或证明健康。

  2. 提供业务页面和最小健康端点

    至少让目标页面及业务 API 返回可核对内容。启动方式由 Program 自己决定;当前契约不要求固定 start-webui.sh 或固定健康路径。

  3. 保留代理前缀

    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;它与浏览器页面内部的相对地址规则并不冲突。

官网版本工作台,其中没有 WebUI 端口表单,业务端口由 CLI 0.7.3 配置
当前官网边界。 Program 区域仍用于模型、Resources 和开发入口;业务 WebUI 端口不在表单中,使用本页所列 CLI 命令管理。

四、最终回复必须包含严格的 oasn-webui JSON

页面已经启动并可访问后,让 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"

三个典型错误

# 错: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_INVALIDMCP_INVOCATION_RESULT_INVALID,不会用部分成功掩盖问题。

五、平台输出中的 url 会变成 uri

开发者只负责上一节的最终回复,不直接构造下面对象。平台校验并代理后,客户端读取可信的 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。

六、按四层验收,不只看 HTTP 200

  1. Sandbox 本地服务

    确认进程监听 0.0.0.0,用 loopback 访问目标页面和 API,并核对响应业务内容。

  2. 端口与代理状态

    运行 version webui-ports get,同时确认 portssync_status=readyproxy_status=ready 和当前 webui_urls

  3. 开发代理页面

    打开每个新 URL,验证页面、嵌套路由、静态资源、API;有 WebSocket 时验证往返和断线重连。410 表示业务代理会话无效,不等于 Owner OAuth 失效;502 先检查业务监听。

  4. 最终结果与正式调用

    让 Agent 真实返回 block,确认它被投影为 view/canvas,path/query/fragment 保留且没有敏感状态;发布上线后再做一次正式调用,开发 WebChat 不能替代。

终态重新打开:代码已接入,仍待正式产品验收

当前实现计划从同一会话已提交的进程快照恢复正式 WebUI,端口数量不决定恢复资格,也不要求开发者额外提供固定重启脚本。但在真实单端口、多端口任务完成后重新打开原结果链接并通过验收前,不得承诺页面永久可重开。开发 Workspace 链接不能代替这项正式验收。

发布前 I/O 门禁