跳到主要内容

WebKit - 自定义页面与菜单

WebKit 模块允许插件在 EasyBot 的 Web 管理界面中注册自定义页面和菜单项。通过此 API,插件可以在侧边栏中添加菜单,并响应前端页面的交互请求。

版本要求

WebKit API 在 SDK v0.3.2 中引入。需要 EasyBot 2.1.0-dev.6 或更高版本。

核心概念

菜单系统架构

EasyBot 的菜单系统采用树形结构:

  • 分组 (Group):纯目录节点,用于收纳子菜单
  • 页面 (Page):叶子节点,关联一个前端页面路径
  • 插件菜单:由插件通过 pluginMenu 注册,自动关联到当前插件

ID 自动拼接

注册菜单时填写的 id 会自动加上当前插件 ID 前缀,格式为 插件ID:注册ID。例如:

  • 插件 ID:my_plugin
  • 注册时填写 ID:my_page
  • 最终完整 ID:my_plugin:my_page

前后端通信机制

自定义页面的核心交互模式是:前端通过 window.webuikit.action() 调用后端方法,后端通过 onAction 回调处理请求并返回结果

数据流概览

前端 API:window.webuikit.action()

在前端页面的 JavaScript 中,通过全局对象 window.webuikit 提供的 action() 方法与后端插件通信。

语法

const result = await window.webuikit.action(method, params);

参数

参数类型描述
methodstring方法名,用于在后端 onAction 中区分不同的操作
paramsany传递给后端的参数,可以是任意 JSON 可序列化的数据

返回值Promise<any> — 后端 onAction 回调的返回值,自动通过 JSON 序列化传回前端。

关键点

  • action() 是一个异步函数,返回 Promise,必须在 async 函数中使用 await.then() 获取结果。
  • 无需手动传递 sessionId——WebUI 会自动注入当前会话标识,后端会收到包含 sessionIdWebAction 对象。
  • 后端返回的 JavaScript 对象会被自动序列化为 JSON 传回前端,前端直接拿到已反序列化的对象。

后端处理:onAction 回调

pluginMenu.register() 中注册的 onAction 回调函数,是后端处理前端请求的入口。

回调签名

(action: WebAction) => any | Promise<any>

WebAction 对象结构

属性类型描述
sessionIdstring前端随机生成的会话标识,用于区分不同的浏览器标签页或用户
methodstring前端调用时传入的方法名,用于路由不同的业务逻辑
parametersany前端调用时传入的参数,即 action(method, params) 中的 params

后端处理逻辑

  1. EasyBot WebUI 收到前端 action() 调用后,自动构造一个 WebAction 对象。
  2. WebUI 调用插件的 onAction(action) 回调,传入该 WebAction
  3. 插件根据 action.method 判断操作类型,使用 action.parameters 获取参数。
  4. 插件的返回值会被 WebUI 自动序列化为 JSON 并传回前端。

完整通信示例

前端页面(HTML)

<script>
// 调用后端,不需要传 sessionId
async function loadData() {
const data = await window.webuikit.action("getData", { page: 1 });
console.log(data); // { items: [...], total: 100 }
}

async function saveItem() {
const result = await window.webuikit.action("saveItem", { name: "test" });
if (result.success) {
alert("保存成功");
}
}

loadData();
</script>

后端插件(JavaScript)

bus.on("menu_ready", (pluginMenu) => {
pluginMenu.register(
"my_page",
"/pages/mypage",
"我的页面",
"HomeOutlined",
(action) => {
// action.method 来自前端 action() 的第一个参数
// action.parameters 来自前端 action() 的第二个参数
// action.sessionId 由 WebUI 自动注入

switch (action.method) {
case "getData": {
const page = action.parameters.page;
// 从数据库获取数据...
return { items: [], total: 100 };
}
case "saveItem": {
const name = action.parameters.name;
// 保存到数据库...
return { success: true };
}
default:
throw new Error(`未知方法: ${action.method}`);
}
},
(sessionId) => { /* 返回 HTML */ },
);
});

错误处理

  • 如果后端 onAction 抛出异常,前端的 await window.webuikit.action() 会 reject,可以用 try-catch 捕获。
  • 建议在后端对未知的 method 抛出明确的错误信息,方便前端调试。
// 前端错误处理
try {
const result = await window.webuikit.action("unknownMethod", {});
} catch (e) {
console.error("调用失败:", e.message);
}

核心类型

IconKey

系统内置图标名称类型,支持以下图标:

图标常量含义
HomeOutlined首页
DashboardOutlined仪表盘
ToolOutlined工具
SettingOutlined设置
UserOutlined用户
InfoCircleOutlined信息
ApiOutlinedAPI
PictureOutlined图片
ChromeOutlined浏览器
CloudServerOutlined云服务器
DeploymentUnitOutlined部署
LinkOutlined链接
ConsoleSqlOutlined数据库查询
DatabaseOutlined数据库
ContactsOutlined联系人
TeamOutlined团队
IdcardOutlined身份证
SearchOutlined搜索
CodeOutlined代码
SafetyCertificateOutlined安全认证
RobotOutlined机器人
SwapOutlined交换
EditOutlined编辑
LogoutOutlined登出
SecurityScanOutlined安全扫描
CheckCircleOutlined验证通过
StopOutlined停止
ControlOutlined控制
ShopOutlined商店
GiftOutlined礼物
FileTextOutlined文件文本
FolderViewOutlined文件夹视图
InsertRowBelowOutlined插入行
ImportOutlined导入
ClearOutlined清除
FireOutlined热门
UnlockOutlined解锁
NodeIndexOutlined节点索引
GoldOutlined金币
SkinOutlined皮肤
MenuOutlined菜单
TypeOutline文字/字体

也可以传入以 http 开头的 URL 作为自定义图标。

RemoteRoute

菜单路由节点,描述菜单树中的一个节点。

declare class RemoteRoute {
readonly id: string; // 路由唯一标识
readonly path?: string | null; // 页面路径(仅插件页面非null)
readonly label: string; // 菜单标题
readonly icon: IconKey | string; // 菜单图标
readonly component?: string | null; // 前端组件名(插件页面为null)
readonly isPremium: boolean; // 是否仅限高级版
readonly isLimitedFree: boolean; // 是否限时免费
readonly defaultVisible: boolean; // 是否默认可见
readonly defaultParentId: string | null;// 默认父菜单ID
readonly defaultOrder: number; // 排序权重
readonly isLocked: boolean; // 是否锁定
readonly pluginMeta?: PluginMenuMeta | null; // 插件元数据
readonly children?: RemoteRoute[] | null; // 子菜单列表
}

WebAction

前端触发的方法调用对象。

declare class WebAction {
readonly sessionId: string; // 前端随机ID,区分触发来源
readonly method: string; // 调用的方法名
readonly parameters: any; // 传递的参数
}

PluginMenuRegister

插件菜单注册器,每个插件拥有独立的实例。

declare class PluginMenuRegister {
register(
id: string,
path: string,
label: string,
icon: IconKey | string,
onAction: (action: WebAction) => Promise<any> | any,
onGet: (sessionId: string) => Promise<string> | string,
parentId?: string | null,
visible?: boolean | null,
order?: number | null,
isLocked?: boolean,
description?: string,
): RemoteRoute;

registerGroup(
id: string,
label: string,
icon: IconKey | string,
parentId?: string | null,
visible?: boolean | null,
order?: number | null,
isLocked?: boolean,
description?: string,
): RemoteRoute;

getMenus(): RemoteRoute[];
}

注册菜单

监听 menu_ready 事件

菜单注册必须在 menu_ready 事件中进行,该事件在菜单系统初始化完成后触发:

bus.on("menu_ready", (pluginMenu) => {
// 在这里注册菜单
});

register() - 注册页面菜单

注册一个带页面的菜单项,点击后会在前端打开对应路径的页面。

参数

  • id:菜单标识符(仅需在插件内唯一)
  • path:页面路径
  • label:显示标题
  • icon:图标(内置名称或URL)
  • onAction:前端调用后端方法时的回调
  • onGet:前端请求页面时的回调,需返回完整 HTML
  • parentId:父菜单ID,null 为根级
  • visible:是否默认可见
  • order:排序权重,越小越前
  • isLocked:是否锁定
  • description:描述信息

示例

bus.on("menu_ready", (pluginMenu) => {
pluginMenu.register(
"my_page", // ID
"/pages/mypage", // 路径
"我的页面", // 标题
"HomeOutlined", // 图标
(action) => { // 处理前端调用
if (action.method === "getData") {
return { result: "hello", time: Date.now() };
}
if (action.method === "saveData") {
// 处理保存逻辑
db.Set("mydata", JSON.stringify(action.parameters));
return { success: true };
}
},
(sessionId) => { // 返回页面HTML
return `
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>我的页面</title></head>
<body>
<h1>你好,这是我的自定义页面</h1>
<p>Session: ${sessionId}</p>
</body>
</html>`;
},
null, // 父菜单ID(null=根级)
true, // 默认可见
10, // 排序
false // 不锁定
);
});

registerGroup() - 注册菜单分组

创建纯目录节点,用于组织子菜单。

示例

bus.on("menu_ready", (pluginMenu) => {
// 先创建分组
const group = pluginMenu.registerGroup(
"tools", // ID
"工具箱", // 标题
"ToolOutlined", // 图标
null, // 父菜单(null=根级)
true, // 默认可见
5, // 排序
false // 不锁定
);

// 在分组下注册页面
pluginMenu.register(
"tool_page",
"/pages/tool",
"小工具",
"SwapOutlined",
onAction,
onGet,
"tools", // 使用短ID引用同插件内的父菜单
true,
null,
false
);
});

getMenus() - 获取已注册菜单

bus.on("menu_ready", (pluginMenu) => {
const menus = pluginMenu.getMenus();
logger.info(`当前插件已注册 ${menus.length} 个菜单`);
});

完整示例

以下是一个完整的插件示例,注册了一个带交互功能的自定义页面:

/// <reference path="easybot-sdk/easybot.d.ts" />

bus.on("enable", () => {
logger.info("WebKit 示例插件已启用");
});

bus.on("menu_ready", (pluginMenu) => {
// 注册一个工具分组
pluginMenu.registerGroup("demo_group", "演示工具", "ToolOutlined", null, true, 10, false);

// 注册一个计数器页面
pluginMenu.register(
"counter",
"/pages/counter",
"计数器",
"DashboardOutlined",
(action) => {
switch (action.method) {
case "getCount": {
const count = db.GetString("counter_value") || "0";
return { count: parseInt(count) };
}
case "increment": {
const count = (parseInt(db.GetString("counter_value") || "0")) + 1;
db.Set("counter_value", count.toString());
return { count };
}
case "reset": {
db.Set("counter_value", "0");
return { count: 0 };
}
}
},
(sessionId) => {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>计数器</title>
<style>
body { font-family: sans-serif; padding: 20px; text-align: center; }
button { font-size: 18px; margin: 5px; padding: 8px 16px; }
#count { font-size: 48px; margin: 20px; }
</style>
</head>
<body>
<h1>计数器</h1>
<div id="count">0</div>
<button onclick="callAction('increment')">+1</button>
<button onclick="callAction('reset')">重置</button>
<script>
async function callAction(method, params = {}) {
const countEl = document.getElementById('count');
try {
const result = await window.webuikit.action(method, params);
countEl.textContent = result.count;
} catch(e) {
countEl.textContent = '❌ Error: ' + e.message;
}
}
// 初始加载
callAction('getCount');
</script>
</body>
</html>`;
},
null, true, 20, false, "一个简单的计数器演示"
);
});

bus.on("disable", () => {
logger.info("WebKit 示例插件已禁用");
});

注意事项

  • 菜单注册必须menu_ready 事件中进行
  • 引用同插件内的父菜单时,使用短ID(注册时填写的原始 ID,不含插件前缀)。系统会自动拼接前缀
  • onGet 回调需返回完整的 HTML 字符串用于渲染页面
  • 前端调用后端方法使用 window.webuikit.action(method, params),无需手动传递 sessionId
  • 图标可传入 IconKey 内置名称或 http/https 开头的自定义 URL

遇到麻烦了?

有偿服务

我们提供有偿代安装服务,解决您的环境配置烦恼。

了解详情
Miku

少年,买服务器吗?

持证经营

专注高性价比游戏云VPS,铂金 / I7 / R9 / 物理机

💰 最低六元起、买不了吃亏买不了上当,快来看看吧~

快来看看