hanye 插件开发文档
hanye 插件开发文档(完整版)
面向监控台 插件 作者。应用集市已永久关闭(原 :3001 已卸载);自行获取或编写 ZIP,在 软件设置 → 扩展中心 → 已装插件 上传安装。
主题包已下线(≥5.2.25):勿再上传
theme.json主题 ZIP。插件仍可用theme.css/--app-*令牌跟宿主外观。
本文合并原「插件开发手册 + 微内核 v2」,并含宿主迁入能力(第三方摄像头 / 保养 / 打印单 / 打印记录 / 巡查派单)的钩子与槽位;官方示例包hosted_hooks可下载。
唯一入口: 软件设置 → 扩展中心 → 开发文档(本页)。不再提供集市 /docs。
0. 快速上手(最小例子)
0.1 最小包结构
my_plugin/
plugin.json
main.js # 服务端(可选)
client.js # 浏览器端(可选)
theme.css # 跟随宿主 --app-*(可选)
{
"identifier": "hello_slot",
"name": "Hello 槽位示例",
"version": "1.0.0",
"description": "在设备详情摄像头区追加提示",
"main": "main.js",
"client": "client.js",
"compat": ["hanye-printer-monitor"]
}
打包:zip -r hello_slot-1.0.0.zip . -x '*.DS_Store'(ZIP 根目录须直接有 plugin.json)。
0.2 UI 槽位例子(第三方摄像头旁)
;(function () {
var api = window.HanyePlugin || window.hanyePlugin
if (!api || !api.ui) return
api.ui.slot('device.detail.camera.extra', function (el, ctx) {
var tip = document.createElement('div')
tip.style.cssText = 'margin:6px 0;font-size:12px;opacity:.75'
tip.textContent = '插件提示:设备 ' + ((ctx && ctx.deviceId) || '')
el.appendChild(tip)
})
})()
0.3 服务端钩子例子(第三方摄像头)
module.exports = {
register: function (api) {
api.hooks.on('extra_cameras_list', function (payload) {
var cams = Array.isArray(payload.cameras) ? payload.cameras.slice() : []
payload.cameras = cams.map(function (c) {
return Object.assign({}, c, { name: (c.name || '摄像头') + ' · 插件' })
})
return payload
})
api.hooks.on('extra_cameras_save', function (payload) {
if ((payload.cameras || []).length > 8) {
payload.abort = true
payload.message = '本插件限制最多 8 路'
}
return payload
})
}
}
宿主已内置勿再整包替代:extra_cameras、detail_console、device_maintain、print_order、机型肖像 / 醒目进度等。数据兼容路径:device.pluginData.extraCameras。
一、插件开发手册
插件负责站点能做什么:钩子、自定义 API、权限、导航、设置 Tab、设备卡/详情增强、登录改造等。
整站排版与配色优先用主题包,见 THEME.md(主题包已下线)。
| 文档 | 内容 |
|---|---|
| 本文 | 包结构、PluginApi、钩子、UI 槽位、浏览器 SDK、模块权限、打包 |
| PLUGIN_KERNEL_V2.md | 推荐:apiVersion: "2" + activate(ctx) 微内核 |
| THEME.md(主题包已下线) | 主题 skin / full、.htm、挂载点 |
推荐新插件走微内核 v2.2(
apiVersion: "2")。请使用 当前官网一键包 监控台联调(能力远高于早期 1.x)。旧版main.js导出同名钩子(v1)经 CompatAdapter 仍可用。
领域事件、HMAC 回调、cron、SQL migrations、设备文件/锁机/快照原语 → 见下文与内核文档。安全:插件在服务端与浏览器双端运行,权限极高。只安装可信来源。
USE_MYSQL=1 时业务数据进 MySQL,包文件仍在磁盘。部署说明见监控台仓库部署文档(NODE_DEPLOY.md 等),不在本集市文档展开。
示例包下载
页面顶部「官方示例下载」可直接下载;常见包:
| 包 | 说明 |
|---|---|
demo_hello |
登录 / 导航 / 权限 / 槽位 / 自定义路由 |
demo_kernel_v2 |
activate(ctx) / hooks / templates / cron |
capability_kit |
文件/开打/锁机/快照/print.finished |
hosted_hooks |
宿主融入:摄像头 / 保养 / 打印单 / 打印记录 / 巡查派单 钩子与槽位 |
sample_topnav |
主题示例(顶栏 + .htm,历史) |
安装:软件设置 → 扩展中心 · 已装插件 → 上传 ZIP → 启用。
目录
- 概念与运行位置
- 包结构
- plugin.json
- 服务端 main.js 与 PluginApi
- 服务端钩子一览
- UI 嵌入点 PluginSlot
- 浏览器 SDK HanyePlugin
- 模块页与用户权限
- 生命周期
- 典型做法速查
- 开发调试与打包
- 与主题的分工
- 检查清单
1. 概念与运行位置
| 层级 | 位置 | 作用 |
|---|---|---|
| 开发示例 | 插件源码/plugin-*/ |
学习与二次打包 |
| 可选内置 | assets/plugins/{id}/ |
存在则出现「同步内置」 |
| 已安装包 | data/plugins/{id}/ |
运行时加载的代码副本 |
| 私有 JSON | MySQL plugin_data 或 data/plugin-data/{id}/ |
api.readJson / writeJson |
| 安装状态 | MySQL plugins_state 或 data/plugins-state.json |
启用、vars、模块、cron 状态 |
| 服务端 | main.js(v1)或 activate(ctx)(v2) |
拦截 API、改数据、自定义路由 |
| 登录页 | login.js + publicClientJs |
无 JWT,改登录 UI |
| 登录后 SPA | client.js + clientJs |
全站 UI / 导航 / 设置 Tab |
管理入口:软件设置 → 扩展中心 → 已装插件。
2. 包结构
your_plugin/
plugin.json # 必填
main.js # v1:导出钩子;v2:可 export activate
client.js # 登录后 SPA(window.HanyePlugin)
login.js # 登录页(可选)
theme.css # 全局 CSS(可选)
install.js / uninstall.js / upgrade.js
install.sql / uninstall.sql # 可选(MySQL)
migrations/ # v2:v1.sql, v2.sql …
v1.sql
language/ # v2:i18n
zh-CN.json
templates/ # v2:.htm(与主题同引擎)
banner.htm
slots/
login.header.html # 文件名 = 槽位名
modules/
page.js # type=page → 侧栏 iframe 页
admin.js # type=admin
tick.js # type=cron(配合 schedule)
static/
page.css
identifier:小写字母、数字、下划线(会规范化)。
3. plugin.json
3.1 v1 完整示例
{
"identifier": "chamber_temp",
"name": "仓内温度显示",
"version": "1.0.0",
"description": "在设备卡片与专用页显示仓内温度",
"copyright": "hanye",
"available": true,
"hooks": ["register", "common", "permissions_catalog"],
"modules": [
{
"name": "page",
"menu": "仓内温度",
"type": "page",
"displayOrder": 25,
"perm": "plugin.chamber_temp.page"
},
{
"name": "admin",
"menu": "仓温插件设置",
"type": "admin",
"displayOrder": 0,
"adminOnly": true
}
],
"vars": [
{
"key": "show_on_cards",
"title": "设备卡片显示仓温",
"type": "boolean",
"value": "1"
}
],
"clientJs": ["client.js"],
"publicClientJs": ["login.js"],
"themeCss": "theme.css",
"mainFile": "main.js",
"installFile": "install.js",
"uninstallFile": "uninstall.js",
"upgradeFile": "upgrade.js"
}
3.2 v2 增量字段
{
"apiVersion": "2",
"requires": { "kernel": ">=2.2.0" },
"capabilities": [
"log", "config.vars", "hooks", "http.route", "http.callback", "http.fetch",
"devices.read", "devices.control", "devices.files", "devices.print",
"devices.capabilities", "devices.gcode", "devices.moonraker", "devices.lock",
"camera.snapshot", "media.write", "settings.publicUrl", "users.pluginData",
"templates", "alert.dispatch", "users.read", "auth.login",
"cache", "plugins.call", "i18n", "db.scoped",
"storage.json", "settings.read", "settings.patch", "users.write"
],
"dbSchemaVersion": 1,
"modules": [
{ "name": "tick", "menu": "定时", "type": "cron", "schedule": "every:5m", "displayOrder": 0 }
]
}
完整 Context / 领域事件 / 回调 → PLUGIN_KERNEL_V2.md。
3.3 字段说明
| 字段 | 说明 |
|---|---|
identifier |
唯一 ID,安装目录名 |
available |
安装后默认是否启用 |
apiVersion |
"1"(默认)或 "2" |
hooks |
声明用;实际以导出函数 / activate 为准 |
hookPriority |
可选数字;越大越先跑同名钩子(如强制排序 900)。未声明时按加载顺序 |
modules |
type:page / admin / cron(schedule)/ api / hook |
modules[].perm |
权限码;进「用户权限 → 插件权限」并控制侧栏 |
modules[].adminOnly |
仅管理员 |
modules[].openMode |
可选 embed(默认,监控台内 iframe)/ standalone(系统浏览器;需系统文件选择器时用) |
modules[].method / public |
type: api 时:HTTP 方法、是否免登录 |
vars |
设置页可改配置;值一律字符串 |
vars[].type |
text / number / boolean / textarea / select |
clientJs / publicClientJs |
登录后 / 登录页脚本 |
themeCss |
全局 CSS |
capabilities |
v2:Context 能力门控(含 storage.json、settings.read/patch、users.write 等) |
conflicts |
v2 可选:冲突插件 identifier 列表 |
templates.provides / templates.overrides |
v2 可选:模板提供 / 覆盖声明 |
dbSchemaVersion |
v2:配合 migrations/vN.sql |
布尔 vars:"1" / "0"。
权限码建议:plugin.{identifier}.{name}。
hookPriority 示例(强制排序,越大越先):
{
"identifier": "device_force_order",
"hookPriority": 900
}
4. 服务端:main.js 与 PluginApi
4.1 导出方式
// CommonJS
module.exports = {
async register(api) { /* 注册自定义路由 */ },
async devices_list(api, devices, ctx) { return devices },
async api_before(api, payload, ctx) { return payload }
}
// 或 class
module.exports = class {
async register(api) {}
async devices_list(api, devices) { return devices }
}
钩子签名:(api, value, ctx?) => nextValue | Promise
返回 undefined 表示不修改;返回新值则传给下一个插件。
4.2 PluginApi(钩子内可用)
| 方法 | 作用 |
|---|---|
api.identifier / api.version |
插件身份 |
api.vars / getVar(key, fallback) / setVar(key, value) |
读写配置 |
api.dataDir |
可写数据目录 |
api.dataRoot |
宿主数据根目录(与 dataDir 不同;跨插件路径慎用) |
api.pluginDir |
插件包目录(只读为主) |
api.log(...) |
日志 |
api.readJson(rel, fallback) / writeJson(rel, data) |
插件私有 JSON(MySQL 下进 plugin_data) |
api.db |
MySQL:建自有表、查询、写入(见下节) |
api.getSettings() / patchSettings(patch) |
全局设置 |
api.getDevices() / saveDevices(list) |
设备列表 |
api.getStatuses() |
当前状态快照 |
api.controlDevice(id, payload) |
下发控制 |
api.listFiles(id) / uploadFile / downloadFile |
机内文件(Moonraker + 拓竹 LAN FTPS) |
api.startPrint(id, { filename, contentBase64? }) |
上传(可选)并开打 |
api.getDeviceCapabilities(id) |
控制/文件/摄像头/G-code/Moonraker 透传能力探测 |
api.sendGcode(id, script) |
Moonraker 任意脚本 |
api.moonrakerRequest(id, { method, path, query?, body? }) |
Moonraker HTTP 透传(需较新监控台;早期称宿主 ≥ 1.3.1) |
api.claimDevice / releaseDevice / getDeviceLock |
跨插件设备锁(排程) |
api.snapshotCamera({ deviceId?, target?, cameraId?, zoneId? }) |
拉一帧 JPEG(base64);区域监控可传 zoneId |
api.writeMedia(rel, data) |
写入插件 dataDir/media(延时摄影归档) |
api.getPublicBaseUrl() |
公网入口 URL(publicBaseUrl / domain / IP) |
api.getUserPluginData / patchUserPluginData |
用户扩展字段(车间/多租户标签) |
api.notify({ title, content, kind?, deviceId? }) |
走告警通道 |
api.fetch |
同全局 fetch |
api.registerRoute(method, pattern, handler) |
自定义 HTTP |
api.findUser({ id / username }) |
查宿主用户 |
api.listUserGroups / api.saveUserGroups |
读写用户组(类 Discuz 组权限) |
api.runHook(name, value, ctx?) |
主动跑一轮同名钩子链(如对齐强制排序后的 devices_list) |
api.issueLoginToken(userId) |
服务端直接签发宿主 JWT(适合插件自定义回调接口) |
api.createLoginGrant(userId, { ttlSec? }) |
生成一次性登录授权码,供登录页插件换宿主 JWT |
api.createUser({ username, ... }) |
创建宿主本地用户 |
与强制排序对齐时,可在自建列表上再跑一遍宿主钩子(保留原有返回结构):
async function myList(api) {
const devices = await api.getDevices()
return api.runHook('devices_list', devices)
}
4.2.0 功能 → 原语矩阵(宿主 1.2+)
以下业务均可只写插件完成(协议细节由宿主桥接;用 getDeviceCapabilities 降级):
| 目标功能 | 所需 capability / API | 说明 |
|---|---|---|
| 打印历史 / 统计 | hooks + action:print.* + storage.json/db |
事件含 filename、durationSec、filamentUsedGrams |
| 文件柜 + 远程开打 | devices.files + devices.print |
Moonraker;拓竹需 LAN+访问码 |
| 空闲排程 / 自动接单 | devices.lock + devices.print + cron |
claim 防抢机 |
| 维保寿命 | hooks print 事件 + 自存计数 |
无需新协议 |
| 延时摄影 | camera.snapshot + media.write + cron |
定时拉帧归档 |
| 多租户/车间 | users.pluginData + filter:devices.list |
标签写在用户 pluginData |
| 远程入口页 | settings.publicUrl |
读 publicBaseUrl;隧道仍用外部 frpc 等 |
| Klipper 宏/深控 | devices.gcode |
仅 Moonraker 类 |
| Moonraker 全量 API | devices.moonraker |
moonrakerRequest / POST …/moonraker(需较新监控台) |
| 光固化 / 品牌差异 UI | devices.capabilities |
按 caps 显隐按钮 |
| HTTP 能力探测 | GET /api/v1/devices/:id/capabilities |
浏览器端同权 |
示例:插件源码/plugin-capability-kit/。
4.2.0a 设备控制全量原语(需较新监控台;早期称宿主 ≥ 1.4.0)
主体详情页已按权限 + capabilities提供 jog / 仓温 / 挤出回抽 / 流量 / Z 偏移 / 重启 / 任意 G-code。插件仍可用同一套 controlDevice / HTTP。
统一 controlDevice / POST /api/v1/devices/:id/control
| action | extras | Moonraker | 拓竹 | 其它厂 |
|---|---|---|---|---|
pause / resume / cancel |
— | ✓ | ✓ | 多数 ✓ |
emergency_stop |
— | ✓ | ✓(=stop) | 视 caps |
home |
— | ✓ | ✓ | 视 caps |
jog |
axis X/Y/Z/E, amount mm |
✓ | ✓ | 视 caps |
set_temp |
temperature, heater extruder/bed |
✓ | ✓ | 视 caps |
set_fan |
percent, fan part/chamber, fanName? |
✓ | ✓ | 视 caps |
set_speed |
percent |
✓ | ✓ | 视 caps |
set_flow |
percent |
✓ | ✓ | ✗ |
set_z_offset |
amount ±2mm |
✓ | ✗ | ✗ |
set_chamber_temp |
temperature |
✓ | ✓ | ✗ |
extrude / retract |
amount 0.1–50, temperature? |
✓ | ✓ | ✗ |
restart / firmware_restart |
— | ✓ | ✗ | ✗ |
print_file |
filename |
✓ | LAN ✓ | 视 caps |
load_filament / unload_filament |
temperature?, slot? |
✓ | ✓ | 视 caps |
调用前务必 getDeviceCapabilities(id),看 control.* / gcode / moonrakerProxy。
Moonraker 透传
// v1
await api.moonrakerRequest(deviceId, {
method: 'GET',
path: '/printer/objects/query',
query: { extruder: '', heater_bed: '' }
})
// v2
await ctx.devices.moonrakerRequest(deviceId, {
method: 'POST',
path: '/printer/gcode/script',
query: { script: 'BED_MESH_CALIBRATE' }
})
HTTP(需登录 + device.action.moonraker):
POST /api/v1/devices/:id/moonraker
body: { "method": "GET"|"POST"|"DELETE", "path": "/…", "query"?: {}, "body"?: any }
任意 G-code HTTP
POST /api/v1/devices/:id/gcode(需 device.action.gcode;仅 Moonraker 类)
body: { "script": "G28\\nM118 hi" } 或 { "gcode": "…" }
插件侧亦可 devices.gcode → sendGcode(deviceId, script)。
约束:仅 Moonraker 类设备(klipper / qidi / 创想局域网);path 必须 / 开头、禁止 ..;只转发到该设备已连接的 baseUrl。
sendGcode 适合短脚本;完整 Moonraker REST(文件元数据、对象查询、电源等)用 moonrakerRequest。
4.2.1 api.db(MySQL,推荐)
USE_MYSQL=1 时可用。插件不能新建 MySQL 库(DATABASE),但可在当前库内创建自有表并读写。
| 方法 | 说明 |
|---|---|
api.db.available |
是否已接上 MySQL |
api.db.table('sessions') |
物理表名,如 plugin_qq_login_sessions |
api.db.ensureTable('sessions', 'id VARCHAR(64) PRIMARY KEY, data JSON') |
建表 |
api.db.dropTable('sessions') |
删表 |
增 api.db.insert('sessions', { id, data }) |
插入一行 |
查 api.db.select('sessions', { where, limit, orderBy }) |
查多行 |
查 api.db.getOne('sessions', { id }) |
查一行 |
查 api.db.count('sessions', { status: 'ok' }) |
计数 |
改 api.db.update('sessions', { data }, { id }) |
按条件更新 |
删 api.db.remove('sessions', { id }) |
按条件删除 |
api.db.upsert('map', row, ['openid']) |
有则改、无则增 |
api.db.query / api.db.execute |
底层 SQL(高级) |
规则:
- 表名强制前缀
plugin_{identifier}_ - 禁止
CREATE/DROP DATABASE、改宿主核心表(users/devices…) update/remove必须带where,防止误伤全表- 一次一条 SQL;底层参数请用
?占位
// main.js — 增查改删示例
async function register(api) {
if (!api.db.available) return
await api.db.ensureTable(
'openid_map',
'openid VARCHAR(128) PRIMARY KEY, user_id CHAR(36) NOT NULL, created_at DATETIME(3) NOT NULL'
)
}
async function bindOpenid(api, openid, userId) {
// 增 / 改(upsert)
await api.db.upsert(
'openid_map',
{ openid, user_id: userId, created_at: new Date() },
['openid']
)
// 查
const row = await api.db.getOne('openid_map', { openid })
const list = await api.db.select('openid_map', { limit: 50, orderBy: 'created_at DESC' })
// 改
await api.db.update('openid_map', { user_id: userId }, { openid })
// 删
// await api.db.remove('openid_map', { openid })
return { row, total: await api.db.count('openid_map') }
}
module.exports = { register }
建表会登记到宿主表 extension_schema,便于运维查看。
4.2.2 MySQL 部署与插件数据落库
USE_MYSQL=1(npm start 网页服务)时:
| 数据 | 存储 |
|---|---|
| 启用开关、vars、模块列表 | 表 plugins_state |
api.readJson / writeJson |
表 plugin_data(plugin_id + rel_path) |
api.db.ensureTable 自建表 |
物理名 plugin_{identifier}_*,登记 extension_schema |
| 包内 JS/CSS/HTML | 仍在磁盘 data/plugins/{id}/(代码资源,不是业务表) |
要点:
- 制作:按本手册打包 ZIP(
plugin.json+main.js/client.js…)。 - 安装:软件设置 → 插件 → 上传 ZIP → 解压到
data/plugins/{id}/。 - 状态:启用 / vars 写入
plugins_state,不要只改本地plugins-state.json指望网页端生效。 - 私有 JSON:
api.writeJson('foo.json', obj)→plugin_data;不要假定data/plugin-data/里一定有文件。 - 自有表:只用
api.db.*;禁止CREATE DATABASE;勿改宿主核心表(users/devices/app_settings…)。 - 卸载:删包目录并更新
plugins_state;建议在uninstall.js里dropTable/ 清理自己的plugin_data键。
环境变量与全站表清单、从旧 JSON 导入:见监控台部署文档(NODE_DEPLOY.md)。主题包落库见 主题开发手册 §3(主题包已下线)。
4.3 自定义路由(推荐)
在 register 钩子中注册。不要挂在 /api/v1/plugins/... 保留路径下做业务 API(管理接口占用);业务接口用独立路径,例如:
async register(api) {
api.registerRoute('GET', '/api/v1/chamber-temp/temps', async (req, api) => {
const devices = api.getDevices()
const statuses = api.getStatuses()
return {
ok: true,
rows: devices.map((d) => ({
id: d.id,
name: d.name,
chamberTemp: statuses[d.id]?.chamberTemp ?? null
})),
warnCelsius: Number(api.getVar('warn_celsius', '45'))
}
})
}
成功响应会被包装为 { ok: true, data: ... }(以实际 pluginApi 行为为准)。前端用 JWT 调用即可。
4.4 请求上下文 ctx
常见字段:method、path、url、query、headers、auth(user / apiKey / local 等)。
4.5 插件完成第三方授权后,换宿主 JWT
这是给 登录页插件 / OAuth / 企业微信扫码 / 自定义 SSO 用的标准宿主流程。
方案 A:推荐,走一次性授权码
- 插件服务端(
main.js的registerRoute(..., { public: true }))先完成第三方授权校验 - 校验通过后,用
api.findUser/api.createUser找到或创建宿主用户 - 调
api.createLoginGrant(user.id, { ttlSec })生成一次性grantToken - 浏览器登录页插件拿到
grantToken后,请求:
POST /api/v1/auth/plugin-login/exchange
Content-Type: application/json
{ "grantToken": "..." }
- 宿主返回正常登录态:
token、user、permissions、deviceAcl
也可以直接在登录页脚本里调用浏览器 SDK:
const login = await HanyePlugin.exchangeLoginGrant(grantToken)
if (!login.ok) throw new Error(login.message || '登录失败')
返回成功后会自动写入宿主登录态并进入正常页面。
方案 B:服务端直接签发 JWT
若你的插件自定义回调本身就由插件服务端处理,也可直接:
const { token, user } = api.issueLoginToken(user.id)
return { ok: true, token, user }
这种方式适合插件自己的后端回调链路;但对于 登录页 login.js,仍推荐方案 A,因为它把“第三方校验”和“宿主会话落地”分成两步,更清晰,也更容易复用宿主现有登录页状态流转。
最小示例
// main.js
async register(api) {
api.registerRoute(
'POST',
'/api/v1/my-oauth/callback',
async (req) => {
const body = req.body || {}
const externalId = String(body.externalId || '').trim()
if (!externalId) throw new Error('缺少 externalId')
let user = api.findUser({ username: 'sso_' + externalId })
if (!user) {
user = await api.createUser({
username: 'sso_' + externalId,
displayName: 'SSO 用户 ' + externalId,
level: 'viewer'
})
}
return {
ok: true,
...api.createLoginGrant(user.id, { ttlSec: 120 })
}
},
{ public: true }
)
}
// login.js
const res = await fetch('/api/v1/my-oauth/callback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ externalId: 'abc123' })
})
const data = await res.json()
await HanyePlugin.exchangeLoginGrant(data.grantToken)
5. 服务端钩子一览
按执行场景分类。下表 + §5.6 为宿主已接线的短名;完整常量见宿主 PLUGIN_HOOKS / LEGACY_HOOK_TO_V2。未列出的名字也可导出,但只有宿主实际 runHook 的名字会进入业务管线。
同名钩子执行顺序:声明了 hookPriority 的插件按数值从大到小优先,其余按加载顺序。需要与强制排序等插件对齐列表时,可在服务端调用 api.runHook('devices_list', devices)(见 §4.2)。
5.1 通用 API 管线
| 钩子 | 时机 | 入参 / 返回 |
|---|---|---|
common |
每个已认证请求入口 | (api, null, ctx),返回值忽略 |
api_before |
业务处理前 | { proceed, method, path } → 可设 proceed:false + status/body 短路 |
api_after |
sendJson 时 |
{ status, body, method, path } → 可改 status/body(同步返回立即生效;异步仅副作用) |
register |
插件加载时 | 用于 registerRoute |
5.2 登录 / 用户 / 权限
| 钩子 | 时机 | 说明 |
|---|---|---|
login_before |
登录校验前 | 可改 username/password,或 proceed:false 拒绝 |
login_after |
登录成功后 | 可改返回的 me/token 载荷 |
auth_me |
/me 与登录 me 组装后 |
扩展用户信息、权限之外字段 |
permissions_effective |
生效权限列表(登录 /me) | (api, perms[], ctx) → 新权限数组;可运行时强制加减权限 |
permissions_catalog |
用户管理勾选目录 | (api, defs[]) → 追加 { code, label, plugin?, description? };与模块 perm 合并后出现在「插件权限」。宿主内置侧栏收折能力(如 farm_dispatch 巡查派单)由宿主写入「全局权限」,不要再往本目录塞同名码 |
users_list |
GET /api/v1/users |
可过滤/ enrich 用户列表 |
user_create / user_update / user_delete |
用户 CRUD 前 | { proceed, body? } / { proceed, id };可改写 body 或拦截 |
permissions_catalog 示例
// main.js
async permissions_catalog(api, list) {
const rows = Array.isArray(list) ? list.slice() : []
// 模块上写了 perm 的项已由宿主预先放入 list,此处可追加「非导航」能力码
if (!rows.some((r) => r && r.code === 'plugin.myplug.export')) {
rows.push({
code: 'plugin.myplug.export',
label: '允许导出报表',
plugin: 'myplug',
description: '业务 API 内自行校验'
})
}
return rows
}
勾选后码写入该用户的 permissions[];前端可用 auth store 的 can(code),服务端自定义路由里读 ctx.auth 校验。完整说明见 §8.2。
5.3 设备 / 状态 / 控制
| 钩子 | 时机 | 说明 |
|---|---|---|
devices_list |
GET /api/v1/devices |
改名称、过滤、附加字段 |
device_create / device_update / device_delete |
设备增改删前 | { proceed, device/patch/deviceId } |
devices_save |
整表保存滤镜(api.saveDevices / ctx.devices.save) |
devices[] |
statuses_publish |
SSE/Webhook 广播前 | 改状态 map |
control_before |
控制指令前 | 可拦截 proceed:false |
control_after |
控制结果后 | 可改返回结果 |
5.3.1 耗材
| 钩子 | 时机 | 说明 |
|---|---|---|
filament_list |
GET /api/v1/filament |
{ spools } → 可过滤/改写列表 |
filament_create |
POST 入库前 | { proceed, spool } |
filament_update |
PUT/PATCH 前 | { proceed, spoolId, spool, prev } |
filament_delete |
DELETE 前 | { proceed, spoolId, spool } |
5.3.2 常用工具 / 报价
| 钩子 | 时机 | 说明 |
|---|---|---|
quote_presets |
GET 预设 | 可追加 materials/printers |
quote_calculate |
POST 计算 | 前:{ proceed, body };后:{ ok, options/costs }(ctx.phase==='after') |
quote_parse_gcode |
解析 G-code | { proceed, text },可直接返回 result |
quote_history_list / create / delete |
报价记录 | 列表改写 / 入库拦截 / 删除拦截 |
quote_schemes_list / save / delete |
计算方案 | 列表改写 / 保存拦截 / 删除拦截 |
5.3.3 内部监控 / 区域监控
| 钩子 | 时机 | 说明 |
|---|---|---|
monitor_wall |
GET 摄像头墙 | { devices } → 可改写列表 |
monitor_zones_list |
GET 区域列表 | { zones } |
monitor_zone_create / update / delete |
区域 CRUD | { proceed, … } |
monitor_camera_create / update / delete |
区域摄像头 CRUD | { proceed, … } |
monitor_camera_snapshot |
区域摄像头取帧 | 入参 { handled:false, zone, camera };返回 handled:true + { ok, base64, contentType },或改写 url 再由宿主拉取 |
5.3.4 打印审核 / 队列
| 钩子 | 时机 | 说明 |
|---|---|---|
print_request_list |
GET 打印申请列表 | 可过滤 / enrich |
print_request_create |
POST 提交申请前 | { proceed, body } |
print_approve / print_reject / print_start / print_cancel |
审核 / 开打 / 取消前 | { proceed, id, body? } |
AI 助手技能「查看打印审核 / 提交打印申请 / 审核打印队列」走同一套打印申请存储;提交仅 .gcode。
5.4 设置 / 告警 / AI
| 钩子 | 时机 | 说明 |
|---|---|---|
settings_get |
GET /api/v1/settings |
改对外 settings 载荷 |
settings_patch |
PATCH 前 |
可改 patch 或拦截 |
alert_notify |
告警真正发送前 | { proceed, payload },可改文案或取消 |
ai_settings_get |
AI vision status | 改状态载荷 |
ai_vision_before / ai_vision_after |
YOLO 测试 / 立即巡检 | 拦截或改结果 |
5.5 UI 资产(服务端拼装,前端加载)
| 钩子 | 说明 |
|---|---|
ui_assets |
合并 CSS/JS/htmlHeader/htmlFooter/slots/theme/hideNavKeys |
ui_slots |
再改一次 slots 等 |
theme_resolve |
改 theme 键值(给前端 patchTheme) |
ui_nav |
改插件模块导航项列表 |
5.6 其余已接线钩子(补全)
以下短名已由宿主调用,此前未收入 §5.1–5.5 表;导出方式与签名规则相同。
| 分组 | 钩子 |
|---|---|
| 批量控制 / 开打 | control_batch_before / control_batch_after、print_batch_before / print_batch_after |
| 机内文件 | files_list、files_upload、files_download |
| 发现 / 引导 | discover_lan_before / discover_lan_after、onboard_before / onboard_after |
| 导航 / 主题安装 | nav_config_save、theme_activate、theme_install |
| 操作日志 | logs_list、logs_append |
| 出站 Webhook | webhook_outbound |
| SSO / 会话 | sso_before / sso_after、login_grant_exchange、logout |
缓存滤镜(多与 v2 ctx.cache 对应) |
cache_get、cache_set、cache_delete |
| 第三方摄像头(宿主) | extra_cameras_list、extra_cameras_save(可 abort) |
| 设备保养(宿主) | device_maintain_state、device_maintain_complete、device_maintain_interval、device_maintain_clear |
| 打印单(宿主) | print_order_list、print_order_upsert、print_order_config_get、print_order_config_save |
| 打印记录(宿主) | print_log_list、print_log_config_get、print_log_config_save、print_log_delete(保存/删除可 abort) |
对应 UI 槽(详见 §6.3「宿主迁入业务」):
| 能力 | 槽位 |
|---|---|
| 摄像头 | device.detail.camera.before / extra(可 .replace)/ after |
| 打印单 | printOrder.board.before / after |
| 打印记录 | printLog.page(可 .replace)/ before / after、printLog.page.table.before / after、settings.printLog.before / after |
| 农场分析 | farmScorecard.page(可 .replace)/ before / after、settings.farmScorecard.before / after |
| 巡查派单 | farmDispatch.page(可 .replace)/ before / after、settings.farmDispatch.before / after |
官方示例 ZIP:hosted_hooks(扩展中心 → 插件开发文档 → 官方示例下载)。v2 映射见微内核:filter:extra_cameras.*、filter:device_maintain.*、filter:print_order.*、filter:print_log.*。
v2 正式名多为 filter:* / action:*(见微内核 §7);上表为 v1 导出短名,内核内会映射。
6. UI 嵌入点(PluginSlot)
主题
siteMode: full时,壳层由主题 HTML 提供;插件槽位仍挂在岛内 React 树上(nav/main/login-form等),写法不变。见 主题包已下线。
宿主用 <PluginSlot name="..." replace? context? />。插件两种注入:
- 文件:
slots/{槽位名}.html(登录相关槽位会出现在 public-ui) - 脚本:
HanyePlugin.registerSlot(name, html | fn, { order, plugin })
6.1 *.replace 规则
对基名 foo,若存在 foo.replace 的注册内容,且槽位组件开了 replace,则隐藏 children,只显示插件内容。
示例:login.page + login.page.replace、users.page + users.page.replace、device.grid + device.grid.replace。
6.2 带 context 的卡片 / 详情槽位
设备卡片、详情抽屉会传入实时数据,例如:
HanyePlugin.registerSlot('device.card.temps', (el, ctx) => {
const c = (ctx && ctx.context) || {}
const t = c.chamberTemp
el.innerHTML =
'<div class="temp-pill">仓内 <strong>' +
(t == null ? '--' : Math.round(Number(t)) + '°') +
'</strong></div>'
}, { order: 0, plugin: 'chamber_temp' })
常见 context 字段:deviceId、deviceName、brand、chamberTemp、boardTemp、health、state、tech。
渲染函数可返回 cleanup () => void,在槽位重绘时调用。
6.3 槽位全表
登录
| 槽位 | 说明 |
|---|---|
login.page / .replace |
整页 |
login.header |
标题区上 |
login.form / .replace |
表单本体(含 SSO Tab) |
login.form.before / after |
表单前后 |
login.sso.before / after |
SSO 区域 |
login.footer |
页脚 |
壳层
| 槽位 | 说明 |
|---|---|
app.shell / .replace |
整站外壳 |
app.header.before / after |
顶栏 |
app.header.brand |
品牌标题区 |
app.header.actions |
顶栏按钮区 |
app.nav / .replace / before / after |
导航 |
app.main.before / after |
主内容区 |
app.footer / .replace / before / after |
底栏 |
业务页
| 槽位 | 说明 |
|---|---|
device.grid / .replace / before / after |
设备列表 |
device.batch.before / after |
勾选后批量操作栏外 |
device.batch.actions / status |
批量栏按钮旁 / 状态区 |
device.card.after-name / temps / extra / footer |
单卡 |
宿主已内置(非插件): 设备保养、机型肖像、机型名、醒目进度条、FDM 机群视图、设备第三方摄像头(HTTP/MJPEG/RTSP/HLS/RTMP 等 + 厂家模板、监控墙并入、AI)、智能插座(HA/涂鸦/Shelly/Kasa/Sonoff/MQTT/HTTP/openHAB,设置「打印业务 → 智能插座」,卡片电源图标)均在宿主实现。摄像头设置见「软件设置 → 卡片界面」。不要再上架同名替代插件或重复贴徽章/替换整卡。详情用宿主抽屉,勿再做整页「详情控制台」替换包;勿再上架
extra_cameras/device_maintain/print_order/print_log。扩展请用钩子与槽位(示例hosted_hooks)。
| device.add / .replace | 添加设备整表 |
| device.add.before / after | 添加设备表单首尾 |
| device.add.scan.after | 局域网扫描区后 |
| device.add.brand.after | 品牌选择后 |
| device.add.form / fields / footer | 品牌表单 / 附加字段 / 底栏 |
| device.detail / .replace / before / after | 详情抽屉 |
| device.detail.camera.after | 摄像头后 |
| device.detail.control.before | 控制按钮前 |
| device.detail.footer | 详情底 |
| filament.page / .replace / before / after | 耗材整页 |
| filament.toolbar.* / filters.after / list.* | 工具栏、筛选、列表 |
| filament.form.* / .replace | 添加/编辑弹窗 |
| monitor.page / .replace / before / after | 内部监控整页 |
| monitor.header.* / alerts.after / grid.* | 标题、告警、网格 |
| monitor.tile.before / after / footer | 单路画面 |
| monitor.zones / .replace / before / after | 区域监控整页 |
| monitor.zones.header.* / toolbar.after / grid.* | 区域标题与网格 |
| monitor.zones.form.camera.* | 添加摄像头表单 |
| tools.page / .replace / before / after | 常用工具整页 |
| tools.header.* / params.* / gcode.after | 标题、共用参数、G-code |
| tools.options.* / option.extra | 耗材方案区 |
| tools.result.* / actions.* | 对比结果与导出按钮区 |
| quote.history / .replace / before / after | 报价记录整页 |
| quote.history.header.* / toolbar.* / filters.after | 标题、工具栏、筛选 |
| quote.history.list.* | 列表前后 |
| quote.history.detail.* / .replace | 详情弹窗 |
| print.approve / .replace / before / after | 打印审核整页 |
宿主迁入业务(勿再上架同名替代插件)
| 槽位 | 说明 |
|---|---|
device.detail.camera.before / extra / .replace / after |
详情摄像头区;扩展第三方摄像头旁 UI |
printOrder.board.before / after |
常用工具 · 打印单看板 |
printLog.page / .replace / before / after |
侧栏「追溯记录」整页(页签含打印、报价、AI与异常、操作日志、派单日志、系统日记) |
printLog.page.table.before / after |
追溯记录 → 打印 表格上下 |
settings.printLog.before / after |
软件设置 → 打印业务 → 打印记录设置 |
farmScorecard.page / .replace / before / after |
侧栏「农场分析」稼动率看板 |
settings.farmScorecard.before / after |
软件设置 → 打印业务 → 农场分析看板 |
settings.publicApi.before / after |
软件设置 → 扩展中心 → 开放 API(Public REST Key / SDK) |
farmDispatch.page / .replace / before / after |
侧栏「巡查派单」整页 |
settings.farmDispatch.before / after |
软件设置 → 打印业务 → 巡查派单 |
print.approve.toolbar.* / filters.after |
标题栏 / 筛选 |
print.approve.pending|queued|history.* |
各 Tab 表格前后 |
print.approve.row.actions |
行操作旁 |
resourceHub.page / .replace / before / after |
资源中心整页(含页签) |
models.page / aiModels.page / hotDeals.page / printerNews.page / repairMaster.page / imageCompare.page(及 .before / .after / .replace) |
资源中心内各页签内容 |
users.page / .replace / before / after |
用户页外壳 |
users.toolbar.before / after |
标题工具栏 |
users.list.before / after |
用户表格前后 |
users.form / .replace / before / fields / footer / after |
新建/编辑弹窗 |
users.row.actions |
行操作按钮旁 |
settings.page / .replace / before / after |
软件设置 |
settings.tabs.before / after |
Tab 条外 |
settings.tab.general|enterprise|ai|aiChat|alerts|plugins|themes|about.before/after |
各内置 Tab 内 |
sso.bind / .replace / before / after |
绑定第三方登录页 |
plugin.host / .replace / before / after |
插件模块宿主页外壳 |
custom.page / .replace / before / after |
自定义 HTML 页 |
logs.drawer / .replace / before / after |
操作日志抽屉 |
device.batch.modal / .replace / before / after |
批量打印弹窗 |
device.filter / device.camera / 前后置 |
设备筛选条 / 摄像头面板旁 |
ai.page.before / after |
AI 页前后 |
device.detail.header.extra / control.replace / control.after / files.* / queue.* / filament.load.* |
设备详情密点(控制区、文件、队列、上料等) |
settings.general.* / brand.* / nav.* / ai.* / alerts.* / themes.* / plugins.* / about.* / content.replace |
设置页各 Tab 密点(before/after/字段旁等,与 SoftSettings 内 PluginSlot 一一对应) |
models.card* / aiModels.card* / hotDeals.card* / printerNews.card* / repairMaster.card* |
各内容页卡片级槽位 |
自定义名字也可注册;需宿主已挂对应 PluginSlot 才会显示。完整枚举以宿主 PLUGIN_UI_SLOTS / 微内核扩展点为准(软件设置 → 扩展中心 → 已装插件 → 内核调试亦可查看)。
7. 浏览器 SDK:window.HanyePlugin
登录后由 PluginLoader 注入;登录页同样可用(mode: 'public')。
7.1 槽位
const id = HanyePlugin.registerSlot('app.main.before', (el, ctx) => {
el.textContent = 'Hello ' + ((ctx.user && ctx.user.username) || '')
return () => { /* cleanup */ }
}, { order: 10, plugin: 'demo' })
HanyePlugin.unregisterSlot('app.main.before', id)
HanyePlugin.emit('slot:change', { name: 'app.main.before' }) // 强制刷新
7.2 导航 / 权限 / 主题
HanyePlugin.hideNavKeys(['models', 'aiModels', 'hotDeals', 'printerNews', 'repairMaster'])
HanyePlugin.patchNav((items) =>
items.concat([{ key: 'plugin:demo:page', label: '演示', identifier: 'demo', module: 'page', order: 50 }])
)
HanyePlugin.patchPermissions((perms, user) => perms.concat(['nav.tools']))
HanyePlugin.patchTheme({ uiTheme: 'ocean' })
说明:patchPermissions / 服务端 permissions_effective 是运行时改生效列表;要让管理员在 用户权限 页能勾选插件码,请用模块 perm 或钩子 permissions_catalog(见 §8.2)。
内置导航 key 示例:fdm(侧栏文案「设备」,页内可切 FDM/光固化)、filament、tools、monitor、users、settings、models、aiModels、hotDeals、printerNews、repairMaster 等(以侧栏实现为准)。resin 不再单独出现在侧栏。
7.3 添加设备扩展(完整说明)
在 添加 FDM / 光固化设备 弹窗中,插件可新增品牌、连接方式、表单字段与 UI 区块。请在登录后加载的 client.js 中注册(每次 PluginLoader 会先 reset 再执行脚本)。
事件:add-device:change(品牌/字段注册列表变化时触发)。
7.3.1 能力总览
| 能力 | API | 作用 |
|---|---|---|
| 自定义品牌 | registerAddDeviceBrand(def) |
品牌单选项 + 可选连接方式 + 表单 + 保存 |
| 附加字段 | registerAddDeviceField(def) |
对内置或插件品牌追加表单项,保存前 collect 合并进设备 |
| UI 槽位 | registerSlot('device.add…') |
在弹窗固定位置插入 HTML/DOM(见下表) |
| 服务端拦截 | device_create 钩子 |
创建前改写/拒绝设备 JSON |
查询:getAddDeviceBrands(tech?)、getAddDeviceBrand(id)、getAddDeviceFields({ tech, brand })。
7.3.2 UI 槽位(device.add.*)
| 槽位 | 位置 |
|---|---|
device.add / device.add.replace |
整段弹窗表单(replace 可替换内置内容) |
device.add.before |
表单最前(工作区说明之上) |
device.add.scan.after |
「扫描局域网」区域之后 |
device.add.brand.after |
品牌单选之后 |
device.add.form |
品牌相关表单区(context 含 tech / brand / connectionMode) |
device.add.fields |
附加字段区(与 registerAddDeviceField 相邻) |
device.add.footer |
分组/标签字段之后 |
device.add.after |
表单最末 |
HanyePlugin.registerSlot(
'device.add.form',
(el, ctx) => {
const c = (ctx && ctx.context) || {}
el.innerHTML = '<div style="opacity:.7">插件提示:当前品牌 ' + (c.brand || '') + '</div>'
},
{ order: 10, plugin: 'my_plugin' }
)
7.3.3 registerAddDeviceBrand 字段
HanyePlugin.registerAddDeviceBrand({
id: 'my_vendor',
label: '我的品牌',
tech: 'fdm',
order: 90,
plugin: 'my_plugin',
connections: [
{
id: 'lan',
label: '局域网',
default: true,
render(el, ctx) {
el.innerHTML = '<label>打印机地址</label><input class="ant-input" data-host style="width:100%" />'
const input = el.querySelector('[data-host]')
input.value = String(ctx.getFieldValue('host') || '')
input.onchange = () => ctx.setFieldsValue({ host: input.value })
}
},
{ id: 'cloud', label: '云端' }
],
renderForm(el, ctx) {
el.insertAdjacentHTML(
'beforeend',
'<p style="font-size:12px;opacity:.7">连接方式:' + (ctx.connectionMode || '') + '</p>'
)
},
async submit(ctx) {
const v = await ctx.validateFields(['name'])
return {
device: {
id: ctx.newId(),
name: String(v.name || ''),
brand: 'my_vendor',
tech: ctx.tech,
connectionMode: ctx.connectionMode || 'lan',
baseUrl: String(ctx.getFieldValue('host') || '') || undefined,
pluginData: { vendor: 'my_vendor', ver: 1 },
x_custom_flag: true,
createdAt: new Date().toISOString()
},
secret: String(ctx.getFieldValue('apiKey') || '') || undefined
}
}
})
| 字段 | 必填 | 说明 |
|---|---|---|
id |
是 | 品牌码,写入设备 brand;仅小写 a-z0-9_(非法字符会规范化) |
label |
是 | 单选项文案;设备卡片品牌角标也会优先显示此名 |
tech |
否 | 'fdm' | 'resin' | 'both',默认 both |
order |
否 | 排序,默认 100(排在多数内置品牌后) |
plugin |
否 | 来源插件 id,便于排查 |
connections |
否 | 有则显示「连接方式」单选;选中值写入 Form connectionMode |
connections[].id |
是 | 连接方式码 |
connections[].label |
是 | 显示名 |
connections[].default |
否 | 切换到该品牌时的默认连接 |
connections[].render |
否 | (el, ctx) => cleanup?,仅当前连接被选中时渲染 |
renderForm |
否 | (el, ctx) => cleanup?,选中该品牌时渲染(连接专用 UI 之外) |
submit |
保存必填 | async (ctx) => { device, secret? };未实现则无法添加该品牌设备 |
render / renderForm 可返回 cleanup 函数,宿主在卸载时调用。
7.3.4 表单上下文 ctx(AddDeviceFormCtx)
| 成员 | 类型 | 说明 |
|---|---|---|
tech |
'fdm' | 'resin' |
当前工作区 |
brand |
string | 当前品牌 id |
connectionMode |
string? | 当前连接方式 |
getFieldValue(name) |
fn | 读 Ant Design Form 字段 |
getFieldsValue() |
fn | 读全部字段 |
setFieldsValue(obj) |
fn | 写字段;自定义 input 务必在 change 时同步,否则 submit/collect 读不到 |
validateFields(names?) |
async fn | 校验;失败抛出 Ant Design 校验错误 |
newId() |
fn | 生成设备 UUID |
7.3.5 registerAddDeviceField(附加字段)
不替换品牌流程,在表单后部追加区块;内置品牌与插件品牌保存前都会走 collect。
HanyePlugin.registerAddDeviceField({
id: 'rack_slot',
brands: '*', // '*' / 省略 = 全部;或 ['klipper','bambu','my_vendor']
tech: 'both',
order: 50,
plugin: 'my_plugin',
render(el, ctx) {
el.innerHTML =
'<label>机架位</label><input class="ant-input" data-rack style="width:100%;margin-top:4px" />'
const input = el.querySelector('[data-rack]')
input.value = String(ctx.getFieldValue('rack') || '')
input.onchange = () => ctx.setFieldsValue({ rack: input.value })
},
collect(ctx, device) {
const rack = String(ctx.getFieldValue('rack') || '').trim()
if (!rack) return device
device.pluginData = Object.assign({}, device.pluginData || {}, { rack })
return device
}
})
| 字段 | 必填 | 说明 |
|---|---|---|
id |
是 | 唯一键,重复注册会覆盖 |
render |
是 | 画 UI |
collect |
否 | (ctx, device) => device,合并进即将保存的对象 |
brands |
否 | '*' 或品牌 id 数组 |
tech |
否 | 'fdm' | 'resin' | 'both' |
7.3.6 保存与持久化规则
插件品牌保存顺序
- 用户点「添加」
- 调用该品牌
submit(ctx) - 对返回的
device执行所有匹配的registerAddDeviceField.collect POST /api/v1/devices(或本地等价写入)- 服务端钩子
device_create仍可改写/拦截
内置品牌:走原有探测/登录逻辑后,同样会经过 collect,再入库。
服务端品牌与字段
- 品牌 id:内置集合,或 匹配
^[a-z][a-z0-9_]{0,63}$的插件品牌 - 自动保留:
pluginData(对象) - 额外键:名称以
x_或plugin_开头的字段会原样写入设备 JSON - 密钥:
submit返回的secret,或 device 上的apiKey/accessCode/secret,会进入加密存储
设备对象建议字段
| 字段 | 说明 |
|---|---|
id / name / brand / tech |
必填语义 |
connectionMode |
连接方式 |
baseUrl / bambuHost 等 |
按协议需要 |
pluginData |
推荐放结构化扩展数据 |
x_* / plugin_* |
扁平扩展字段 |
createdAt |
ISO 时间;可省略由宿主补 |
卡片 / 详情要展示扩展数据:用 device.card.* / device.detail.* 槽位读 context,或自行请求设备列表后用 pluginData。
7.3.7 与服务端钩子配合
// main.js
async device_create(api, payload) {
const device = payload && payload.device
if (device && device.brand === 'my_vendor') {
// 校验、补默认值、拒绝创建等
if (!device.baseUrl) {
return { proceed: false, status: 400, body: { ok: false, message: '缺少地址' } }
}
}
return payload
}
也可 registerRoute 提供探测/OAuth 接口,在 renderForm 里 fetch 调用。
7.3.8 完整示例与注意点
- 可运行示例:
插件源码/plugin-sample/client.js(品牌demo_vendor+ 全品牌备注字段) - 自定义 DOM 必须
setFieldsValue,不要只改 input.value - 同一
id重复registerAddDeviceBrand/Field会覆盖旧定义 - 插件禁用后脚本不再加载,自定义品牌按钮消失;已入库设备仍保留原
brand/pluginData - 实时状态 / 控制需另接适配器或自定义路由;本 API 只管「添加与落库」
7.4 软件设置自定义 Tab(插件设置页)
无需改 React 源码即可增加设置导航;可用 after / before / order 插入到任意位置:
内置导航锚点(order):
| key | 导航 | order |
|---|---|---|
general |
设置 | 100 |
enterprise |
企业软件对接 | 200 |
ai |
AI 对接 | 300 |
alerts |
异常对接 | 400 |
themes |
主题 | 500 |
plugins |
插件 | 600 |
about |
说明 | 10000 |
// 插在「设置」后面(企业对接前面)
HanyePlugin.registerSettingsTab({
key: 'my_vendor',
label: '厂家对接',
after: 'general',
order: 10, // → sort = 100 + 10 = 110
adminOnly: true,
render(el) {
el.innerHTML = '<div class="settings-tab-panel"><h3>厂家配置</h3></div>'
}
})
// 插在「企业软件对接」前面
HanyePlugin.registerSettingsTab({
key: 'hr_bridge',
label: '人事同步',
before: 'enterprise',
order: 5 // → sort = 200 - 5 = 195
})
// 或直接用绝对 order(150 = 设置与企业对接之间)
HanyePlugin.registerSettingsTab({
key: 'mid',
label: '中间页',
order: 150
})
不写 after/before 时,默认出现在「插件」与「说明」之间。
槽位:settings.tab.general|enterprise|ai|aiChat|alerts|themes|plugins|about.before/after。
7.5 事件
| 事件 | 含义 |
|---|---|
ready |
资源与脚本加载完成 { mode } |
slot:change |
槽位变更 { name } |
nav:change |
导航补丁变更 |
permissions:change |
权限补丁变更(会写回 auth store) |
theme:change |
主题补丁 |
settings-tabs:change |
自定义设置 Tab 列表变更 |
add-device:change |
添加设备品牌/字段注册表变更 |
batch:change |
批量栏动作/状态注册表变更 |
filament:change |
耗材品牌/材质/字段/列/动作注册表变更 |
quote:change |
报价预设/字段/列/动作/成本改写注册表变更 |
quote-history:change |
报价记录列/筛选/动作注册表变更 |
monitor:change |
监控工具栏/画面动作/滤镜注册表变更 |
users:change |
用户页列/工具栏/表单/权限组注册表变更 |
print:change |
打印审核列/工具栏/行操作/过滤注册表变更 |
HanyePlugin.on('ready', () => { /* … */ })
7.5.1 登录页插件:换宿主登录态
login.js / publicClientJs 里可直接调用:
const result = await HanyePlugin.exchangeLoginGrant(grantToken)
if (!result.ok) {
throw new Error(result.message || '登录失败')
}
说明:
- 默认会自动把返回的
token写入宿主登录态 - 可选:
HanyePlugin.exchangeLoginGrant(grantToken, { applySession: false }) - 它内部调用宿主公开接口
POST /api/v1/auth/plugin-login/exchange
7.5.2 打开设备控制面板
HanyePlugin.selectDevice(deviceId) // 打开
HanyePlugin.selectDevice(null) // 关闭
HanyePlugin.getSelectedDeviceId() // 当前选中 id 或 null
与点击设备卡片相同:受查看权限与当前 FDM/光固化分区约束。
7.6 文档头尾 HTML
服务端 ui_assets 可返回 htmlHeader / htmlFooter,由 PluginLoader 注入到 document.body。
7.7 批量操作栏与插件状态(勾选设备后)
设备列表上方的 批量操作栏(已选 N 台 · 暂停/继续/停止/导入)支持插件扩展。
7.7.1 注册批量按钮
HanyePlugin.registerBatchAction({
id: 'demo_ping',
label: '插件批量动作',
tech: 'both', // fdm | resin | both
order: 50,
requireChecked: true, // 默认 true:无勾选则禁用
// danger: true,
// disabled: (ctx) => ctx.checkedIds.length > 10,
async run(ctx) {
// ctx.checkedIds / ctx.devices / ctx.statuses / ctx.tech
// ctx.batchControl(ids, 'pause'|'resume'|'cancel')
// ctx.clearChecked() / ctx.setCheckedIds(ids)
alert('已选 ' + ctx.checkedIds.length + ' 台')
}
})
| 字段 | 说明 |
|---|---|
id / label |
按钮标识与文案 |
run(ctx) |
点击回调(可 async) |
requireChecked |
默认需有勾选 |
disabled(ctx) |
额外禁用条件 |
danger |
红色危险按钮样式 |
7.7.2 注册状态显示
在「已选 N 台」旁渲染插件状态(随勾选/实时状态刷新):
HanyePlugin.registerBatchStatus({
id: 'demo_sel_status',
tech: 'both',
order: 10,
render(el, ctx) {
const online = ctx.checkedIds.filter((id) => {
const st = ctx.statuses[id]
return st && st.health === 'online'
}).length
el.innerHTML =
'<span style="font-size:12px;opacity:.8">插件状态:在线 ' +
online +
' / 已选 ' +
ctx.checkedIds.length +
'</span>'
}
})
7.7.3 UI 槽位
| 槽位 | 位置 |
|---|---|
device.batch.before / after |
整栏前后 |
device.batch.status |
状态区(与 registerBatchStatus 相邻) |
device.batch.actions |
内置按钮与插件按钮之间 |
context:tech、checkedIds、checkedCount、busy。
事件:batch:change。查询:getBatchActions(tech)、getBatchStatuses(tech)。
7.8 耗材管理扩展(列表 + 添加/编辑)
耗材页(FDM / 树脂料卷)支持插件深度扩展:自定义品牌/材质、表单字段、表格列、行/工具栏动作,以及服务端 CRUD 钩子。
7.8.1 能力总览
宿主凡从耗材库选料卷、从设备列表选打印机的下拉都带输入搜索(按名称 / ID / 材质颜色过滤,适合成千上万条)。插件若用槽位自绘 <select>,请自行加搜索,不要只渲染不可过滤的原生列表。
| API | 作用 |
|---|---|
registerFilamentBrand |
品牌筛选与表单下拉增加品牌 |
registerFilamentMaterial |
材质下拉增加材质 |
registerFilamentField |
添加/编辑弹窗附加字段(collect 写入 pluginData / x_*) |
registerFilamentColumn |
列表增加列 |
registerFilamentRowAction |
行操作区增加按钮 |
registerFilamentToolbarAction |
Tabs 旁工具栏按钮 |
槽位 filament.* |
工具栏 / 筛选 / 列表 / 表单插入点 |
钩子 filament_list/create/update/delete |
服务端拦截与改写 |
7.8.2 UI 槽位
| 槽位 | 位置 |
|---|---|
filament.page / .replace / before / after |
整页(App 壳) |
filament.toolbar.before / after |
FDM/树脂 Tabs 前后 |
filament.filters.after |
品牌/材质筛选后 |
filament.list.before / after |
料卷卡片栅格前后 |
filament.form / .replace |
整表单可替换 |
filament.form.before / after / fields / footer |
表单内插入 |
context:tech、mode(create|edit)、spoolId。
7.8.3 注册示例
HanyePlugin.registerFilamentBrand({
id: 'demo_filament_brand',
name: '演示线材品牌',
kind: 'fdm',
popular: true
})
HanyePlugin.registerFilamentMaterial({
id: 'demo_cf',
label: '演示 CF 料',
category: 'fdm'
})
HanyePlugin.registerFilamentField({
id: 'batch_no',
tech: 'both',
mode: 'both',
order: 20,
render(el, ctx) {
el.innerHTML =
'<label style="display:block;margin-bottom:4px">批次号</label>' +
'<input class="ant-input" data-batch style="width:100%" />'
const input = el.querySelector('[data-batch]')
input.value = String(ctx.getFieldValue('batchNo') || '')
input.onchange = () => ctx.setFieldsValue({ batchNo: input.value })
},
collect(ctx, spool) {
const batchNo = String(ctx.getFieldValue('batchNo') || '').trim()
spool.pluginData = Object.assign({}, spool.pluginData || {}, { batchNo })
spool.x_batch_no = batchNo || undefined
return spool
}
})
HanyePlugin.registerFilamentColumn({
id: 'batch',
title: '批次',
width: 100,
render(spool) {
const pd = spool.pluginData || {}
return String(pd.batchNo || spool.x_batch_no || '—')
}
})
HanyePlugin.registerFilamentRowAction({
id: 'copy_id',
label: '复制 ID',
run({ spool }) {
navigator.clipboard?.writeText(String(spool.id || ''))
}
})
HanyePlugin.registerFilamentToolbarAction({
id: 'export_hint',
label: '插件导出',
run({ tech }) {
alert('当前分区:' + tech)
}
})
7.8.4 持久化
料卷 JSON 会保存:
pluginData对象- 任意
x_*/plugin_*字段
编辑时宿主会保留既有 extras,再与 collect 合并。
7.8.5 服务端钩子
| 钩子 | 时机 | 载荷 |
|---|---|---|
filament_list |
GET 列表后 | { spools } → 可改 spools |
filament_create |
POST 入库前 | { proceed, spool },proceed:false 拦截 |
filament_update |
PUT/PATCH 前 | { proceed, spoolId, spool, prev } |
filament_delete |
DELETE 前 | { proceed, spoolId, spool } |
事件:filament:change。查询:getFilamentBrands / Materials / Fields / Columns / RowActions / ToolbarActions。
7.9 常用工具 / 代打报价扩展
「常用工具」页(价格计算器)与「报价记录」支持页内插件扩展。
7.9.1 能力总览
| API | 作用 |
|---|---|
registerQuoteMaterialPreset |
材料类型下拉追加预设 |
registerQuotePrinterPreset |
打印机功率预设 |
registerQuoteField |
共用参数区附加字段(读写 ctx.getParam / setParam / pluginData) |
registerQuoteOptionField |
每个耗材方案卡片内附加字段 |
registerQuoteColumn |
方案对比表增加列 |
registerQuoteAction |
复制/导出旁增加按钮 |
registerQuoteToolbarAction |
标题旁工具栏按钮 |
registerQuoteCostAdjust |
改写每方案 costs(加费、折扣等) |
槽位 tools.* / quote.history.* |
页内插入点 |
钩子 quote_* |
服务端拦截 |
7.9.2 UI 槽位
| 槽位 | 位置 |
|---|---|
tools.page / .replace / before / after |
整页 |
tools.header.before / after |
标题区 |
tools.params.before / after / fields |
共用参数卡 |
tools.gcode.after |
G-code 导入后 |
tools.options.before / after |
耗材方案区 |
tools.option.extra |
单个方案内(context.optionId) |
tools.result.before / after / breakdown.after |
结果卡 |
tools.actions.before / after |
复制/导出按钮区 |
quote.history / .replace / before / after |
报价记录页 |
quote.history.header.before / after |
标题区 |
quote.history.toolbar.before / after |
搜索工具栏 |
quote.history.filters.after |
筛选区后 |
quote.history.list.before / after |
表格前后 |
quote.history.detail / .replace / before / after / footer |
详情弹窗 |
quote.history.detail.options.after |
详情方案表后 |
7.9.3 注册示例
HanyePlugin.registerQuoteMaterialPreset({
id: 'demo_silk',
label: '演示丝绸 PLA',
tech: 'fdm',
pricePerKg: 88
})
HanyePlugin.registerQuoteField({
id: 'rush_fee',
tech: 'both',
order: 20,
render(el, ctx) {
el.innerHTML =
'<label style="display:block;margin-bottom:4px">加急费(元)</label>' +
'<input class="ant-input" data-rush type="number" style="width:100%" />'
const input = el.querySelector('[data-rush]')
input.value = String(ctx.getParam('rushFee') || 0)
input.onchange = () => ctx.setParam('rushFee', Number(input.value) || 0)
}
})
HanyePlugin.registerQuoteCostAdjust({
id: 'rush_adjust',
order: 10,
adjust(costs, ctx) {
const rush = Number(ctx.getParam('rushFee') || 0)
if (!rush) return costs
const next = Object.assign({}, costs)
next.fixed = (Number(next.fixed) || 0) + rush
next.perUnit = (Number(next.perUnit) || 0) + rush
next.grand = (Number(next.grand) || 0) + rush * (Number(ctx.getParam('qty')) || 1)
return next
}
})
HanyePlugin.registerQuoteAction({
id: 'demo_summary',
label: '插件汇总',
run(ctx) {
alert('方案数 ' + ctx.options.length + ' · 工艺 ' + ctx.tech)
}
})
7.9.4 报价记录扩展
| API | 作用 |
|---|---|
registerQuoteHistoryColumn |
列表增加列 |
registerQuoteHistoryRowAction |
行操作按钮 |
registerQuoteHistoryToolbarAction |
标题旁工具栏 |
registerQuoteHistoryFilter |
搜索栏旁筛选 UI;可选 match 客户端过滤 |
registerQuoteHistoryDetailField |
详情弹窗附加块 |
HanyePlugin.registerQuoteHistoryColumn({
id: 'opt_count_badge',
title: '方案标记',
width: 90,
render(r) {
const n = (r.options || []).length
return n >= 3 ? '多方案' : String(n)
}
})
HanyePlugin.registerQuoteHistoryFilter({
id: 'tech_only',
render(el, ctx) {
el.innerHTML = '<select class="ant-input" data-tech style="width:100px"><option value="">工艺</option><option value="fdm">FDM</option><option value="resin">树脂</option></select>'
const sel = el.querySelector('[data-tech]')
sel.value = String(ctx.pluginFilters.tech || '')
sel.onchange = () => ctx.setPluginFilter('tech', sel.value || undefined)
},
match(r, ctx) {
const t = ctx.pluginFilters.tech
if (!t) return true
return r.tech === t
}
})
HanyePlugin.registerQuoteHistoryRowAction({
id: 'copy_id',
label: '复制 ID',
run({ record }) {
navigator.clipboard?.writeText(String(record.id || ''))
}
})
事件:quote:change(计算器)、quote-history:change(记录页)。服务端钩子见 §5.3.2。
7.10 内部监控 / 区域监控扩展
7.10.1 能力总览
| API | 作用 |
|---|---|
registerMonitorToolbarAction |
监控墙 / 区域页标题旁按钮(scope: wall\|zones\|both) |
registerMonitorTileAction |
每路画面标题栏操作按钮 |
registerMonitorTileExtra |
画面 header/footer 自定义 DOM(place) |
registerMonitorWallFilter |
过滤监控墙设备格 |
registerMonitorCameraField |
区域「添加摄像头」表单附加字段(任意 source) |
registerMonitorCameraSource |
厂家 / 云平台 / 自定义对接类型(替换裸 URL 表单) |
registerMonitorZoneProvider |
向区域网格注入实时画面格(不必落库为 ZoneCamera) |
7.10.2 槽位
| 槽位 | 位置 |
|---|---|
monitor.page / .replace / before / after |
内部监控整页 |
monitor.header.* / alerts.after / grid.* |
标题、告警、网格 |
monitor.tile.before / after / footer |
单格(context.deviceId / cameraId) |
monitor.zones / .replace / … |
区域监控 |
monitor.zones.form.camera.fields / footer |
添加摄像头弹窗 |
7.10.3 厂家 / 自定义监控源(重点)
区域监控内置协议:HTTP 快照、MJPEG、RTSP/RTSPS、HLS、RTMP、HTTP-FLV(后几项需主机 ffmpeg 取帧),并带海康/大华等厂家 URL 模板。插件仍可用 registerMonitorCameraSource 注册云平台等「对接方式」:
- 前端
renderForm+submit:收集厂家账号、通道号、设备序列号等,写入sourceType+pluginData(及x_*/plugin_*)。 - 前端
toSources(可选):把已保存的摄像头映射为SnapshotCam源;默认走服务端
GET /api/v1/monitor/zones/{zoneId}/cameras/{id}/snapshot。 - 服务端
monitor_camera_snapshot:用pluginData调厂家 API / 云平台,返回 JPEG base64,或改写可拉取的url。
// client.js
HanyePlugin.registerMonitorCameraSource({
id: 'acme_nvr',
label: 'Acme NVR / 云平台',
hideUrlFields: true, // 隐藏内置 URL 输入(默认 true)
renderForm(el, ctx) {
el.innerHTML =
'<label>通道号</label><input class="ant-input" data-ch style="width:100%" />'
const input = el.querySelector('[data-ch]')
input?.addEventListener('change', () =>
ctx.setFieldsValue({ channel: input.value })
)
},
async submit(ctx) {
const channel = String(ctx.getFieldValue('channel') || '').trim()
const name = String(ctx.getFieldValue('name') || '').trim() || `通道 ${channel}`
if (!channel) throw new Error('请填写通道号')
return {
camera: {
name,
sourceType: 'acme_nvr',
pluginData: { channel, site: 'cn' }
// url 可省略;宿主会存 plugin://acme_nvr
}
}
}
// toSources 可选:直接给 HLS/WebRTC 地址时自行返回
})
// main.js
async monitor_camera_snapshot(api, payload) {
const cam = payload.camera
if (!cam || cam.sourceType !== 'acme_nvr') return payload
const channel = cam.pluginData?.channel
// 调厂家 SDK / HTTP,得到 jpeg buffer → base64
const base64 = await fetchFromAcme(channel)
return { handled: true, ok: true, contentType: 'image/jpeg', base64 }
// 或:return { handled: false, url: 'http://nvr/snap?ch=' + channel }
}
动态列表(不落库)可用:
HanyePlugin.registerMonitorZoneProvider({
id: 'acme_live',
label: 'Acme 在线列表',
listTiles({ zoneId }) {
return [
{
id: 'ch1',
title: '一号机位',
cameras: [{ id: 'ch1', name: '一号', remoteSnapshotUrl: 'https://…' }]
}
]
}
})
持久化字段:sourceType、pluginData、x_*、plugin_* 会随摄像头 JSON 存盘/入库。
7.10.4 其它示例
HanyePlugin.registerMonitorToolbarAction({
id: 'demo_wall_ping',
label: '插件监控',
scope: 'both',
run(ctx) {
alert(ctx.scope + ' · 画面数 ' + ctx.slotCount)
}
})
HanyePlugin.registerMonitorTileAction({
id: 'demo_tile_info',
label: '信息',
scope: 'both',
run(ctx) {
alert((ctx.deviceName || ctx.cameraName || '') + '\n' + (ctx.deviceId || ctx.cameraId || ''))
}
})
HanyePlugin.registerMonitorTileExtra({
id: 'demo_tile_footer',
place: 'footer',
scope: 'wall',
render(el, ctx) {
el.textContent = '插件:' + (ctx.brand || '')
}
})
事件:monitor:change。服务端钩子见 §5.3.3。
7.11 用户与权限扩展
7.11.1 能力总览
| API | 作用 |
|---|---|
registerUserToolbarAction |
标题旁按钮 |
registerUserColumn |
用户表额外列 |
registerUserRowAction |
行操作按钮 |
registerUserFormField |
新建/编辑表单字段(collect → pluginData / x_*) |
registerUserPermGroup |
额外权限勾选分组(写入 permissions[]) |
patchPermissions |
运行时改生效权限列表 |
模块 perm / permissions_catalog |
「插件权限」勾选项 |
7.11.2 槽位
| 槽位 | 位置 |
|---|---|
users.page* |
整页(App 外壳) |
users.toolbar.before / after |
标题栏 |
users.list.before / after |
表格前后 |
users.form / .replace / before / fields / footer / after |
编辑弹窗 |
users.row.actions |
行按钮旁 |
7.11.3 示例
HanyePlugin.registerUserColumn({
id: 'dept',
title: '部门',
render(user) {
return String(user.pluginData?.dept || '—')
}
})
HanyePlugin.registerUserFormField({
id: 'dept_field',
render(el, ctx) {
el.innerHTML = '<input class="ant-input" data-dept style="width:100%" />'
const input = el.querySelector('[data-dept]')
input.value = String(ctx.getFieldValue('dept') || '')
input.onchange = () => ctx.setFieldsValue({ dept: input.value })
},
collect(ctx, user) {
const dept = String(ctx.getFieldValue('dept') || '').trim()
user.pluginData = { ...(user.pluginData || {}), dept }
return user
}
})
HanyePlugin.registerUserPermGroup({
id: 'warehouse',
title: '仓管扩展',
options: [{ code: 'plugin.wh.view', label: '查看仓库' }]
})
持久化:pluginData、x_*、plugin_* 随用户 JSON / MySQL plugin_data 保存。
服务端钩子:users_list、user_create / user_update / user_delete、permissions_catalog、permissions_effective、auth_me。
事件:users:change。
7.12 打印审核 / 队列扩展
7.12.1 能力总览
| API | 作用 |
|---|---|
registerPrintToolbarAction |
标题旁按钮 |
registerPrintColumn |
表格额外列(tabs: pending/queued/history/mine/all) |
registerPrintRowAction |
行操作按钮 |
registerPrintFilter |
过滤各 Tab 列表行 |
7.12.2 槽位
| 槽位 | 位置 |
|---|---|
print.approve / .replace / before / after |
整页 |
print.approve.toolbar.before / after |
标题栏 |
print.approve.filters.after |
队列设备筛选旁 |
print.approve.pending|queued|history.before/after |
各列表前后 |
print.approve.row.actions |
行按钮旁(context.job) |
7.12.3 示例
HanyePlugin.registerPrintColumn({
id: 'priority',
title: '优先级',
tabs: ['pending', 'queued'],
render(job) {
return String(job.pluginData?.priority || '—')
}
})
HanyePlugin.registerPrintRowAction({
id: 'ping',
label: '插件详情',
tabs: ['all'],
run(ctx) {
alert(JSON.stringify(ctx.job.pluginData || {}))
}
})
HanyePlugin.registerPrintFilter({
id: 'hide_failed_noise',
match(job, ctx) {
if (ctx.tab !== 'history') return true
return job.status !== 'cancelled'
}
})
持久化:提交时可带 pluginData / x_*(随 print-requests JSON / MySQL data)。
服务端钩子:print_request_list|create、print_approve|reject|start|cancel。
事件:print:change。
7.13 设备顺序与状态(强制排序等)
宿主已支持设备 sortOrder 持久化排序(列表工具栏「排序」→ POST /api/v1/devices/reorder)。GET /api/v1/devices 先按 sortOrder 排好,再跑 devices_list 钩子,因此插件仍可覆盖展示序。
前端 SDK 另提供内存级顺序与状态订阅(示例见官方 device_force_order)。注意:HanyePlugin.version(前端 SDK 号)与微内核 KERNEL_VERSION(如 2.2.0)不是同一字段,勿混用。
// 按 id 数组重排设备卡片顺序(仅内存,不写 sortOrder;需对应权限 / 业务场景)
HanyePlugin.setDeviceOrder(['dev-a', 'dev-b', 'dev-c'])
// 订阅状态快照变化
const off = HanyePlugin.subscribeDeviceState((statuses) => {
console.log(statuses)
})
// off() 取消订阅
const ids = HanyePlugin.getDeviceIds()
const map = HanyePlugin.getDeviceStatuses()
服务端若要得到与卡片一致的排序结果,对设备数组再跑 api.runHook('devices_list', devices)(见 §4.2)。
8. 模块页与用户权限对接
8.1 模块页(modules)
type: page|admin 的模块会出现在侧栏;点击后打开 PluginHostPage(iframe),请求:
GET|POST /api/v1/plugins/{id}/modules/{name}
模块文件导出 async 函数,返回 HTML 字符串或带 __html 的对象。
主题背景(必读): 监控台会把当前主题 CSS 变量注入插件 iframe(含 --app-shell-bg、--app-card-bg、--app-text、--app-header-border、--app-control-color 等),并在主题切换时通过 postMessage(hanye-theme-vars)更新。页面根背景请写:
html, body {
background: var(--app-shell-bg, #0f1115);
color: var(--app-text, #e8eaed);
}
.card { background: var(--app-card-bg, rgba(255,255,255,.04)); }
勿把整页写成固定 #0f141c / #fff 等硬编码色,否则换主题后插件页会「漂」在宿主背景上。
module.exports = async function page(api) {
const warn = api.getVar('warn_celsius', '45')
return `<!doctype html><html><body>
<link rel="stylesheet" href="/api/v1/plugins/chamber_temp/static/page.css" />
<h1>仓温一览(阈值 ${warn}℃)</h1>
</body></html>`
}
静态文件:/api/v1/plugins/{id}/static/... 或 /asset/...。
8.2 注册到「用户权限」勾选
插件权限有两种注册方式(可同时用):
| 方式 | 写法 | 效果 |
|---|---|---|
模块 perm |
plugin.json → modules[].perm |
自动进勾选;侧栏按该权限过滤(管理员始终可见) |
钩子 permissions_catalog |
main.js 导出同名函数 |
追加任意码(含非导航能力) |
{
"name": "page",
"menu": "插件演示页",
"type": "page",
"displayOrder": 10,
"perm": "plugin.demo_hello.page"
}
async permissions_catalog(api, list) {
return list.concat([
{ code: 'plugin.demo_hello.extra', label: '演示扩展能力', plugin: 'demo_hello' }
])
}
| 字段 | 说明 |
|---|---|
perm / code |
权限码字符串(勿用 device.action.* 前缀) |
adminOnly |
仅管理员可见模块,不产生勾选项 |
label |
勾选列表显示名(模块默认用 menu) |
宿主行为
- 用户权限 编辑弹窗 → 「插件权限」分组可勾选
- 侧栏:有
perm的模块,无权限且非管理员则隐藏;adminOnly仅管理员 - 勾选结果写入用户
permissions,随登录/me下发
查询接口
| 接口 | 字段 |
|---|---|
GET /api/v1/auth/meta |
pluginPerms[]、合并后的 permLabels |
GET /api/v1/plugins/ui |
permissions[](与上同源)、nav[].perm |
示例见 插件源码/plugin-sample/(演示页 plugin.demo_hello.page + 扩展码 plugin.demo_hello.extra)。
9. 生命周期
install.js / uninstall.js / upgrade.js:
module.exports = async function install(api) {
api.writeJson('installed.json', { at: new Date().toISOString() })
api.log('installed')
}
10. 典型插件类型怎么做
10.1 登录插件
publicClientJs+login.js- 槽位:
login.header/login.form.replace/login.page.replace - 服务端:
login_before/login_after - 第三方授权完成后:
api.findUser/createUser+api.createLoginGrant+HanyePlugin.exchangeLoginGrant(...)
仓库示例:插件源码/plugin-sample/ 已包含一个“模拟第三方授权后直登宿主”的最小流程。
10.2 导航插件
client.js:hideNavKeys/patchNav- 或
modules增加 page,并设perm对接用户勾选 - 服务端:
ui_nav/permissions_catalog/permissions_effective
10.3 用户 / 权限插件
- 前端:
registerUserColumn/ToolbarAction/RowAction/FormField/PermGroup(详 §7.11) - 槽位:
users.toolbar.*/users.list.*/users.form.*/users.page.replace - 钩子:
users_list、user_create/update/delete、permissions_catalog、permissions_effective、auth_me - 自定义
plugin.*码:业务接口内hasPerm/ 前端can(code)
10.4 AI 插件
registerSettingsTab或settings.tab.ai.*/settings.tab.aiChat.*槽位- 钩子:
ai_vision_before/after/ai_settings_get - 顶栏对话走独立
aiChat配置与/api/v1/ai/chat(勿与巡检云端 Key 混用);助手为带工具的 Agent(查并操作设备/耗材/文件库/机内文件发送/代打报价/打印单/打印记录/打印审核队列/巡查派单/区域监控/软件设置,以及管理员技能 编写安装插件:写插件源码/、打 zip 到插件/并安装启用;须用户已开通对应 AI 技能 且具备 RBAC);需用户权限ai.chat。技能目录由管理员在「AI 助手 → AI 技能」或用户编辑页勾选(中文名)。对话会话存在浏览器本地(刷新不丢、可新建/停止) - 顶栏「生成3D」走独立
aiGen3d(Meshy / Tripo / Hyper3D Rodin),接口/api/v1/ai/gen3d/*,需ai.gen3d;完成后可「重新生成」再出一版(扣 3D 额度);确认入库POST .../jobs/:id/library另需nav.fileLibrary。非管理员有独立 3D 额度(默认每月 20 次),GET /api/v1/ai/gen3d/quota/PUT /api/v1/ai/gen3d/quotas。Tripo:https://openapi.tripo3d.com(文档);Rodin:https://api.hyper3d.com/api/v2(文档) - 文件库侧栏权限为
nav.fileLibrary(不再跟设备页绑在一起) - 操作记录
GET /api/v1/activity?kind=ai.chat|ai.gen3d|alert - 自定义路由调用外部模型
10.5 设备卡片增强(如仓温)
device.card.temps/after-name+context.chamberTemp- 可选
device.grid.before总览条 registerRoute拉聚合数据- 参考:
插件源码/plugin-chamber-temp/
10.6 告警插件
alert_notify改写/转发- 或
api.notify主动发
10.7 添加设备扩展(品牌 / 连接 / 字段)
目标:在不改宿主源码的前提下,支持新打印机品牌、新连接方式、自定义表单数据。
推荐组合
client.js→registerAddDeviceBrand(品牌 + 连接 +submit)- 需要「所有品牌都填一项」→ 再加
registerAddDeviceField - 仅要插入提示/按钮 →
device.add.*槽位 - 入库校验/补字段 →
main.js的device_create
字段级、槽位表、ctx、持久化规则见 §7.3(完整说明)。最小可运行代码:
// client.js
HanyePlugin.registerAddDeviceBrand({
id: 'my_vendor',
label: '我的品牌',
tech: 'both',
connections: [
{ id: 'lan', label: '局域网', default: true },
{ id: 'cloud', label: '云端' }
],
renderForm(el, ctx) {
el.innerHTML =
'<label>地址</label><input class="ant-input" id="mv-url" style="width:100%" />'
const input = el.querySelector('#mv-url')
input.value = String(ctx.getFieldValue('host') || '')
input.onchange = () => ctx.setFieldsValue({ host: input.value })
},
async submit(ctx) {
const v = await ctx.validateFields(['name'])
return {
device: {
id: ctx.newId(),
name: v.name,
brand: 'my_vendor',
tech: ctx.tech,
connectionMode: ctx.connectionMode,
baseUrl: ctx.getFieldValue('host') || undefined,
pluginData: { ok: true },
createdAt: new Date().toISOString()
}
}
}
})
仓库示例:插件源码/plugin-sample/(demo_vendor + 全品牌「插件备注」)。
10.8 批量操作 / 插件状态
registerBatchAction:勾选后加自定义批量按钮registerBatchStatus:在批量栏显示插件状态文案/指示- 槽位:
device.batch.before|after|actions|status - 详表与
ctx见 §7.7
10.9 耗材管理扩展
registerFilamentBrand/Material/Field/Column/RowAction/ToolbarAction- 槽位:
filament.toolbar|filters|list|form.* - 服务端:
filament_list|create|update|delete - 持久化:
pluginData、x_*、plugin_* - 详表见 §7.8;示例见
插件源码/plugin-sample/client.js
10.10 常用工具 / 报价扩展
registerQuoteMaterialPreset/PrinterPreset/Field/OptionField/Column/Action/ToolbarAction/CostAdjust- 槽位:
tools.header|params|gcode|options|option|result|actions.* - 报价记录:
registerQuoteHistoryColumn|RowAction|ToolbarAction|Filter|DetailField+quote.history.* - 服务端:
quote_presets|calculate|parse_gcode|history_*|schemes_* - 详表见 §7.9
10.11 内部监控 / 区域监控
registerMonitorToolbarAction/TileAction/TileExtra/WallFilter/CameraField/CameraSource/ZoneProvider- 槽位:
monitor.header|alerts|grid|tile.*、monitor.zones.* - 服务端:
monitor_wall、monitor_zones_*、monitor_camera_*(含monitor_camera_snapshot) - 详表见 §7.10
10.12 用户与权限扩展
registerUserColumn/ToolbarAction/RowAction/FormField/PermGroup- 槽位:
users.toolbar|list|form.*|row.actions - 服务端:
users_list、user_create|update|delete、permissions_catalog|effective、auth_me - 持久化:
pluginData、x_*、plugin_* - 详表见 §7.11
10.13 打印审核 / 队列扩展
registerPrintColumn/ToolbarAction/RowAction/Filter- 槽位:
print.approve.* - 服务端:
print_request_list|create、print_approve|reject|start|cancel - AI 助手:技能「查看打印审核 / 提交打印申请 / 审核打印队列」读写同一套申请
- 持久化:
pluginData、x_*、plugin_* - 详表见 §7.12
11. 开发调试与打包
11.1 流程
- 在
assets/plugins/your_id/或assets/examples/写好包。 - 软件设置 → 扩展中心 → 已装插件 → 同步内置 / 上传 ZIP → 解压到
data/plugins/。 - 启用;改 vars 后必要时刷新。
- 改
client.js/theme.css/login.js:硬刷新(Ctrl+F5)。 - 改
main.js/activate:设置页重载插件,或重启npm start。 - 控制台看
[HanyePlugin];服务端看[plugin:id]。 - v2 调试:
GET /api/v1/plugins/kernel-debug(扩展点 / cron / 熔断统计)。
npm run build && npm start
# http://127.0.0.1:17890/
11.2 打包 ZIP
- ZIP 根目录直接是
plugin.json,或唯一顶层文件夹内含plugin.json。 - 安装后:
data/plugins/{identifier}/。 - URL 安装(管理端):
POST /api/v1/plugins/install-url(可带 sha256)。 - AI 助手(技能「编写安装插件」,仅管理员):可脚手架写入仓库根
插件源码/plugin-*,打 zip 到插件/,并installFromDirectory后启用。须对话确认。不可覆盖已迁入宿主的 identifier(如farm_dispatch、print_order)。
11.3 常用 API
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/plugins |
列表 |
| POST | /api/v1/plugins/install-zip |
上传 |
| POST | /api/v1/plugins/install-bundled |
同步内置 |
| POST | /api/v1/plugins/{id}/enable |
启用 |
| POST | /api/v1/plugins/{id}/disable |
禁用 |
| PATCH | /api/v1/plugins/{id}/vars |
改配置 |
| PATCH | /api/v1/plugins/{id}/modules-enabled |
模块级启停 |
| DELETE | /api/v1/plugins/{id} |
卸载 |
| GET | /api/v1/plugins/{id}/asset/... |
静态资源 |
| GET | /plugin.php?id=foo:bar |
模块页入口 |
11.4 示例对照
| 源码 | 下载 ZIP | 要点 |
|---|---|---|
插件源码/plugin-sample/ |
demo_hello-2.0.1.zip | 登录、导航、permissions_catalog、主题补丁、路由 |
插件源码/plugin-kernel-v2/ |
demo_kernel_v2-1.1.1.zip | activate、hooks、templates、cron、i18n |
插件源码/plugin-capability-kit/ |
capability_kit-1.0.1.zip | 设备原语、传文件开打、锁机、快照 |
插件源码/plugin-hosted-hooks/ |
hosted_hooks-1.0.0.zip | 宿主融入:摄像头/保养/打印单/打印记录/巡查派单 钩子与槽位 |
插件源码/plugin-chamber-temp/ |
(自行打包) | 卡片仓温、temps API、page 模块 |
插件源码/plugin-lan-batch-add/ |
(自行打包) | 局域网扫描 + 分品牌共用核对码批量加机 |
| ### 11.5 宿主「切片→文件库」(非插件) |
不是扩展中心插件。 设置在「软件设置 → 打印业务 → 切片→文件库」。桌面助手源码 切片绑定助手/desktop/(Go;./pack.sh → exe / .app),产物在 主程序/assets/slicer-binder/。
桌面助手绑定本机目录,仅同步 G 代码(.gcode/.gco/.g/.bgcode),按文件名+修改时间单向同步到文件库(不回写本地;同名冲突为 N_原名)。可选写入 Print Host。经 /api/v1/slicer/octoprint 列表/上传。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET/POST | /api/v1/slicer-bridge/config |
令牌、uploadBaseUrl、局域网建议 |
| POST | /api/v1/slicer-bridge/token/rotate |
轮换 Key |
| GET | /api/v1/slicer-bridge/agent/HanyeSlicerBinder-Setup.exe |
Windows 一键安装 |
| GET | /api/v1/slicer-bridge/agent/HanyeSlicerBinder-mac.zip |
macOS 通用 .app(含「打开助手.command」) |
| GET | /api/v1/slicer/octoprint/api/files/local |
文件库列表(name/size/date/sourceMtime) |
| POST | /api/v1/slicer/octoprint/api/files/local |
上传(可带 X-Source-Mtime)→ 文件库 |
鉴权:上传用 X-Api-Key;助手下载公开。
12. 与主题的分工
| 能力 | 主题包 | 插件 |
|---|---|---|
| skin 壳层引擎 / 多套配色 | ✅ 主责 | patchTheme / theme.css 微调 |
| full 整站 HTML 骨架 | ✅ siteMode: full + mounts |
岛内继续挂 PluginSlot |
| 全站 UI 槽位 | ✅ 同名可注入 | ✅ 同名;replace 优先于主题 |
| 业务逻辑 / API / 权限 / 模块页 | ❌ | ✅ |
| 设备卡实时数据 | 可读 context |
✅ 主责 |
| 设置 Tab / 自定义页 | 装饰槽位 | ✅ registerSettingsTab / modules |
| 第三方登录换 JWT | ❌ | ✅ grant / issueLoginToken |
先选主题定「长什么样」,再用插件定「能做什么」。
主题手册:THEME.md(主题包已下线)。内核能力:PLUGIN_KERNEL_V2.md。
注入顺序:主题 → 插件 → 默认 UI。
替换优先级:插件 .replace > 主题 .replace。
13. 检查清单
- [ ]
identifier稳定唯一;权限码带plugin.前缀 - [ ] 可信来源;无任意远程代码执行
- [ ] 自有表只用
api.db/ctx.db,前缀隔离;卸载时清理 - [ ] 公开路由用 HMAC 回调或严格校验(v2
callbacks.register) - [ ] 模块
perm/permissions_catalog已对接用户权限页 - [ ]
client.js在PluginLoaderreset 后可重复注册 - [ ] 槽位 render 返回 cleanup,避免泄漏
- [ ] 未与主题同时 replace 同一关键壳层区域
- [ ] 附带 README / description,便于设置页识别
二、插件微内核 v2 · 开发指南
面向 Discuz 风格深度定制:微内核负责生命周期与扩展点;业务通过 Hook / Context / 模板 / 回调 / 领域事件 接入。
| 项 | 值 |
|---|---|
| Kernel | 2.2.0(KERNEL_VERSION) |
| 类型 | src/shared/pluginKernel/ |
| 运行时 | src/main/plugin/kernel/ |
| 管理器 | src/main/plugin/manager.ts |
| 总手册 | PLUGIN.md |
| 主题 | THEME.md(主题包已下线) |
旧版 main.js 导出同名钩子(apiVersion 1)经 CompatAdapter 继续可用。
示例包:demo_kernel_v2-1.1.1.zip · 源码 插件源码/plugin-kernel-v2/。
目录
- 架构
- plugin.json(v2)
- activate(ctx) 入门
- Context 能力
- 领域事件
- 第三方回调
- Host 钩子
- Cron / SQL / 模板 / i18n
- 与 Discuz / v1 对照
- 已补齐与明确不做
1. 架构
Host (ApiServer / Domain events)
│ runHook · emitDomainEvent · templates
▼
┌─────────────────────────────────┐
│ Plugin Kernel v2.2 │
│ HookBus · Cron · SQL migrate │
│ TemplateEngine · Context │
│ Callbacks (HMAC) · Cache/i18n │
└────────────────┬────────────────┘
▼
data/plugins/{id}/
Node 网页服务共用 PluginManager;JSON 存储可挂 db 风格钩子;USE_MYSQL=1 时包装连接池。
2. plugin.json(v2)
{
"identifier": "demo_kernel",
"name": "Kernel v2 示例",
"version": "1.1.0",
"apiVersion": "2",
"available": true,
"requires": { "kernel": ">=2.2.0" },
"capabilities": [
"log", "config.vars", "hooks", "http.route", "http.callback", "http.fetch",
"devices.read", "devices.control", "devices.files", "devices.print",
"devices.capabilities", "devices.gcode", "devices.moonraker", "devices.lock",
"camera.snapshot", "media.write",
"settings.publicUrl", "templates", "alert.dispatch", "users.read", "auth.login",
"cache", "plugins.call", "i18n", "db.scoped",
"storage.json", "settings.read", "settings.patch", "users.write"
],
"dbSchemaVersion": 1,
"modules": [
{ "name": "tick", "menu": "定时", "type": "cron", "schedule": "every:5m", "displayOrder": 0 }
],
"clientJs": ["client.js"],
"themeCss": "theme.css"
}
| 字段 | 说明 |
|---|---|
apiVersion |
必须 "2" |
capabilities |
Context 能力门控;未声明则对应 API 不可用。v1 默认宽权限。常用字面量含 storage.json、settings.read / settings.patch、users.write(亦可用 settings.* / users.* 概括理解) |
dbSchemaVersion |
配合 migrations/v1.sql… 自动升级 |
modules[].type=cron + schedule |
every:1m / every:5m / 标准 cron |
modules[].type=api |
可自动 registerRoute(url / method / public) |
conflicts |
可选:冲突插件 identifier 列表 |
templates.provides / templates.overrides |
可选:模板提供与覆盖 |
requires.plugins |
可选:依赖其它插件 |
语言包:language/zh-CN.json → ctx.i18n.t(key) / 前端 HanyePlugin.t。
3. activate(ctx) 入门
// main.js
async function activate(ctx) {
ctx.log.info('kernel demo up', ctx.version)
ctx.hooks.on('filter:devices.list', (devices) => {
// 改写设备列表
return devices
})
ctx.hooks.on('action:print.finished', (p) => {
ctx.log.info('print finished', p && p.deviceId)
})
ctx.http.registerRoute('GET', '/api/v1/demo-kernel/ping', async () => {
return { ok: true, t: Date.now() }
})
// 可选:导出给其它插件
ctx.plugins.registerMethod('ping', async () => ({ pong: true }))
}
module.exports = { activate }
停用时可导出 deactivate(ctx)(若宿主调用)。生命周期另见 install.js / upgrade.js / uninstall.js。
v1 写法对照:导出 devices_list(api, devices) ≈ v2 ctx.hooks.on('filter:devices.list', …)。映射表:LEGACY_HOOK_TO_V2(src/shared/pluginKernel/types.ts)。
4. Context 能力(v2.2)
| Capability | API |
|---|---|
log |
ctx.log.info/warn/error |
config.vars |
ctx.vars.get/set |
storage.json |
ctx.storage.readJson/writeJson |
db.scoped |
ctx.db(前缀表 + migrations) |
http.route |
ctx.http.registerRoute |
http.fetch |
ctx.http.fetch |
http.callback |
ctx.callbacks.register(HMAC) |
settings.* |
ctx.settings.get/patch |
devices.* |
ctx.devices.list/control/files/print/getCapabilities/sendGcode/moonrakerRequest/lock |
alert.dispatch |
ctx.alert.dispatch |
users.* / auth.login |
ctx.users / ctx.auth(grant / JWT) |
plugins.call |
ctx.plugins.call(id, method, args) |
cache |
ctx.cache.get/set/delete |
i18n |
ctx.i18n.t |
hooks / templates |
ctx.hooks / ctx.templates |
| (通知) | ctx.notices.push → /api/v1/plugin-notices |
未列入 capabilities 的能力在运行时会被拒绝(安全门控)。
5. 领域事件(action)
宿主在状态机 / 登录 / 告警后 emit:
| 事件 | 触发 |
|---|---|
action:device.online / offline |
在线状态翻转 |
action:print.started / finished / failed |
打印态变化 |
action:alert.fired |
告警渠道发送后 |
action:auth.session.created / revoked |
登录 / 登出 / SSO / grant |
action:plugin.lifecycle |
启用 / 禁用 |
ctx.hooks.on('action:print.finished', (p) => {
// 对接 ERP / 钉钉 …
})
过滤器类钩子(可改写返回值)以 filter: 前缀为主,完整短名见 KERNEL_HOOKS / LEGACY_HOOK_TO_V2。
6. 第三方回调(推荐)
ctx.callbacks.register({
path: '/api/v1/my-plugin/erp-hook',
secretVar: 'erp_secret',
verify: 'hmac-sha256', // Header: X-Hanye-Signature
handler: async (req) => ({ ok: true })
})
所有公开插件路由进入 handler 前会走 filter:http.callback.before。
出站 webhook:filter:webhook.outbound。
OAuth / 第三方登录:registerRoute(..., { public: true }) + ctx.auth.createLoginGrant(流程见 PLUGIN.md「换宿主 JWT」)。
7. Host 钩子覆盖
除 CRUD 外,2.2 已接线例如:
- SSO / grant exchange / logout
- batch control / batch print
- files list/upload/download
- discover LAN / onboard
- nav config / theme install·activate
- logs list/append
devices_save、webhook outbound- UI render / template.fetch
完整短名:插件手册 · 钩子一览(含 §5.6 补全表)与源码 LEGACY_HOOK_TO_V2。
8. Cron / SQL / 模板 / i18n
| 能力 | 用法 |
|---|---|
| Cron | modules 中 type: "cron" + schedule;管理器约 30s tick;互斥 busy 跳过 |
| SQL 迁移 | migrations/vN.sql,升到 dbSchemaVersion(需 MySQL) |
| install.sql / uninstall.sql | 安装 / 卸载自动执行 |
| 模板 | templates/*.htm,语法同主题(extends/block/include/{$var}/{if}/{loop}) |
| 语言包 | language/{locale}.json |
| 模块页 | /plugin.php?id={identifier}:{module};校验 perm / adminOnly / 模块启停 |
| 跨插件 | registerMethod + call |
| 用户组 | /api/v1/user-groups;有效权限 = 直接勾选 ∪ 组权限 |
| 权限包 | /api/v1/permission-packs |
| 模块启停 | PATCH /api/v1/plugins/:id/modules-enabled |
调试:GET /api/v1/plugins/kernel-debug(扩展点目录、cron 状态、钩子熔断统计)。
Hook 默认超时约 5s;连续失败打开约 30s 熔断。
9. 与 Discuz / v1 对照
| Discuz / 需求 | hanye 2.1 |
|---|---|
| filter / action + 优先级 | ✅ |
| 依赖 / 冲突 | ✅ |
| 模板 extends/block | ✅(与主题共用引擎) |
| 独立表 + 迁移 | ✅ scoped + migrations |
| 第三方回调 + 签名 | ✅ HMAC |
| 领域事件 | ✅ |
| Cron | ✅ |
| 语言包 | ✅ JSON |
| 跨插件调用 | ✅ plugins.call |
| 缓存钩子 | ✅ |
plugin.php?id= |
✅ |
| 站内通知 | ✅ ctx.notices |
| 手机 touch 模板 / XML 清单 | ❌(用响应式 SPA slot) |
v1 → v2:把 async devices_list(api, list) 改为在 activate 里 ctx.hooks.on('filter:devices.list', …);api.* 改为 ctx.* 对应能力。
10. 已补齐与明确不做
已补齐(摘要)
- 一级路由整页
*.pagereplace 闭环 - 扩展点登记表
extensionPoints.ts+ kernel-debug - Hook 超时 / 熔断 / 统计
- 模板 compile + 短 TTL render cache
type=api自动挂载路由- Cron 互斥 + 状态面板
- URL 安装 + sha256
- UI Slot 密铺(与
PLUGIN_UI_SLOTS对齐) - 用户组 / 权限包 / 模块级启停
明确不做
- XML 清单
- 手机 touch 独立模板链
- Worker/VM 进程沙箱(同进程信任模型;靠超时熔断隔离)
UI 扩展点以 PLUGIN_UI_SLOTS(src/shared/plugin.ts)与 extensionPointCatalog() 为准。
主题整站模式不影响插件槽位写法,见 主题包已下线。