Skip to content

Console 页面与布局 ​

插件不仅能往群里发消息,还能给 Remote Console 贡献完整的 React 页面。Sandbox 适配器的 /sandbox 聊天页就是这样来的。

和命令一样,页面放在约定目录 pages/ 里,文件会被自动发现,并编译成浏览器能加载的模块。

写第一个页面 ​

tsx
// pages/index/index.tsx
import { definePage } from '@zhin.js/console-contract';

export const meta = definePage({
  title: '我的插件',
  icon: 'Box',
  order: 10,
});

export default function MyPluginPage() {
  return <div>Hello from my plugin</div>;
}
  • 默认导出一个 React 组件,就是页面内容。
  • 命名导出 meta 声明标题、图标和排序。
  • 只有 pages/<name>/index.tsx(或 index.ts)是页面,同目录的其他文件可以作为组件和辅助模块被引用。

启动 Bot 后,打开 Console 就能在导航里看到这个页面。

页面元数据 ​

meta 的字段都可选,写了未知字段时 definePage 会报错:

字段默认说明
title由目录名生成(foo-bar → Foo Bar)导航与页头标题
icon无lucide 风格的图标名,如 'Box'、'Workflow'
order100导航排序,越小越靠前
hideInNavfalse不在导航中显示
requiredPermissions[]访问所需权限
requiredRoles[]访问所需角色

页面的地址 ​

地址 = 插件路径 + 页面名:

文件所属插件地址
pages/index/index.tsxsandbox/sandbox
pages/workroom/index.tsx你的 Bot(root)/p-workroom
pages/index/index.tsx你的 Bot(root)/

index 对应插件路径本身,其他页面对应 p-<name>。两个页面算出同一个地址时,启动会报错。规则实现见 packages/console/plugin-contract/src/page.ts 的 pageRoute。

自定义导航和页脚 ​

pages/ 下有两个保留名字:

  • pages/nav/index.tsx:导航区
  • pages/footer/index.tsx:页脚区

默认导出一个 React 组件即可,不需要 meta。同一位置同时有 .ts 和 .tsx 时以 .tsx 为准。npm create zhin-app 生成的项目里已经带了这两个文件作为示例。

例子:Sandbox 聊天页 ​

Sandbox 适配器(plugins/adapters/sandbox)同时提供了一个适配器和一个页面,它的 package.json#zhin 声明了两个 Feature:

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" }
    ]
  }
}
  • @zhin.js/adapter 发现 adapters/ 下的适配器,提供 WebSocket /sandbox 连接。
  • @zhin.js/page 发现 pages/index/index.tsx,Console 里因此出现 /sandbox 聊天页。

页面里的 SandboxChat 组件通过 WebSocket 连接 /sandbox(地址与 token 见 pages/sandboxTransport.ts)。在页面里发一条消息,等价于真实平台收到一条消息:同样会经过中间件、命令匹配和 AI。所以不接任何真实平台也能完整调试命令、组件渲染和 Agent。Sandbox 会直接显示出站的 HTML 内容,不转成图片或文字,见 中间件与组件。

插件入口本身什么都不用写,页面和适配器都是自动发现的:

ts
// plugins/adapters/sandbox/plugin.ts
import { definePlugin } from 'zhin.js';

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

它是怎么工作的 ​

页面文件运行在浏览器里,所以不会被 Node 直接加载,而是交给客户端构建器(TypeScriptClientBuilder,packages/console/pagemanager)用 esbuild 打包:

  • 产物:每个页面打包为 <owner>-<localName>-<contentHash>.js,写入项目的 .zhin/client/,通过 GET /assets/client/* 提供(cache-control: immutable,内容变了文件名就变)。@zhin.js/console-contract 在打包时被替换成空实现,meta 通过静态分析提取。
  • React 只有一份:浏览器不认识 import 'react' 这样的裸导入,所以白名单 ALLOWED_ESM_CANONICAL(react、react-dom、react-dom/client、react/jsx-runtime(-dev)、react-router、react-router-dom)中的导入会被改写成 /esm/<enc>.mjs,由 Host 按需构建和代理,保证整个 Console 只有一份 React。白名单之外的包返回 403。
  • 路由:在 basic/cli/src/plugin-runtime/console/host.ts 中注册。GET /console 是页面索引,GET /console/api/pages 返回页面清单,其余路径按地址匹配页面并返回页面外壳(包含 importmap 和挂载脚本);未匹配返回 404,权限不足返回 403。浏览器用 import(moduleUrl) 加载页面模块,把默认导出挂载到 #root。

下一步 ​