Skip to content

Console 页面与布局

插件不只能往群里发消息,还能向 Remote Console 贡献整个 React 页面——sandbox 适配器的 /sandbox 聊天页就是这么来的。机制与命令、组件一致:pages/ 是约定目录,由 @zhin.js/page(页面)与 @zhin.js/layout(布局)两个 Feature 发现,客户端构建管线把 TSX 编译成浏览器可加载的 ES 模块。

pages/ 约定(@zhin.js/page)

插件包根目录的 pages/ 下,每个 .tsx / .ts 文件(小写 kebab 命名)是一个页面:默认导出 React 组件,并用命名导出 meta 声明元数据(definePage 来自 @zhin.js/console-contract):

tsx
// pages/index.tsx(plugins/adapters/sandbox)
import { definePage } from '@zhin.js/console-contract';
import SandboxChat from './SandboxChat';

export const meta = definePage({
  title: '沙盒',
  icon: 'Box',
  order: 10,
});

export default function SandboxPage() {
  return <SandboxChat />;
}

meta 字段全部可选,definePage 会校验未知键并抛错:

字段默认说明
title由文件名生成(foo-barFoo Bar导航与页头标题
icon图标名(lucide 风格,如 'Box''Workflow'
order100导航排序,小者靠前
hideInNavfalse不出现在导航
requiredPermissions[]访问所需权限
requiredRoles[]访问所需角色

路由规则

路由 = 插件路径 + 页面名(pageRoutepackages/console/plugin-contract/src/page.ts):

文件所属插件路由
pages/index.tsxsandbox/sandbox
pages/orchestration.tsxroot(应用)/p-orchestration
pages/index.tsxroot(应用)/

即:index 映射到插件路径本身(不带叶子段),其它文件映射为 p-<name> 叶子。路由冲突(两个页面算出同一路由)在启动期报错。

布局(@zhin.js/layout)

pages/ 下有两个保留文件名提供布局槽位:pages/$nav.tsx 对应 nav 槽(导航区),pages/$footer.tsx 对应 footer 槽(页脚区)。布局文件默认导出 React 组件即可,不需要 meta;同一槽位同时存在 .ts.tsx 时以 .tsx 为准。

客户端构建管线

页面/布局文件的 targetclient,发现时不走 Node 模块加载,而是交给 Client Module 适配器(TypeScriptClientBuilderpackages/console/pagemanager):

先看产物:每个页面打包为 <owner>-<localName>-<contentHash>.js,写入项目下 .zhin/client/,经 Host 路由 GET /assets/client/* 提供(cache-control: immutable,内容 hash 变了文件名就变)。@zhin.js/console-contract 在打包时内联为身份函数 stub,meta 由静态提取得到。

再看裸导入问题。浏览器不解析 import 'react' 这类裸导入,因此白名单 ALLOWED_ESM_CANONICALreactreact-domreact-dom/clientreact/jsx-runtime(-dev)react-routerreact-router-dom)内的导入被改写为 /esm/<enc>.mjs,由 Host 按需构建并代理,保证整个 Console 只有一份 React 实例;白名单外的 canonical 返回 403。

Host 路由挂在 basic/cli/src/plugin-runtime/console-host-installer.tsGET /console 是页面索引,GET /console/api/pages 返回页面清单,GET /* catch-all 按路由匹配页面并返回页面 shell(内含 importmap 与模块挂载脚本),未命中 404、权限不足 403。页面模块在浏览器里以 import(moduleUrl) 加载,取默认导出挂载到 #root

sandbox 适配器的 page 实例

sandbox 适配器(plugins/adapters/sandbox)是这套机制的标准消费者。它的 package.json#zhin 声明:

json
{
  "zhin": {
    "protocol": 1,
    "type": "plugin",
    "entry": "./plugin.ts",
    "runtime": "trusted",
    "features": [
      { "package": "@zhin.js/adapter", "api": "^1.0.0" },
      { "package": "@zhin.js/page", "api": "^1.0.0" }
    ]
  }
}

两个 Feature 各管一摊:@zhin.js/adapter 发现 adapters/ 下的适配器(WebSocket /sandbox Endpoint);@zhin.js/page 发现 pages/index.tsx,于是 Console 里出现 /sandbox 聊天页SandboxChat 组件通过 WebSocket 连到 Host 的 /sandbox(base 与 token 见 pages/sandboxTransport.ts),收发消息走统一的 IM 链路——在页面里发消息等价于一个真实平台的入站消息,会经过中间件、命令匹配、AI 未命中处理。

这让「无真实平台调试」成为默认开发路径:pnpm dev(examples/minimal-bot)起的 Sandbox + Console 即可验证命令、组件渲染与 Agent 行为。页面对出站 html 段是内嵌渲染(sandbox 适配器直接消费 html,不做图片/文本归一化,见中间件与组件)。

插件入口本身可以保持极简(plugins/adapters/sandbox/plugin.ts):

ts
import { definePlugin } from '@zhin.js/plugin-runtime';

export default definePlugin({
  name: 'sandbox',
  metadata: { displayName: 'Sandbox Adapter' },
});

页面、适配器都由 Feature 约定发现,入口不需要手工挂载任何东西。