日期:2026-11-13 环境:阿里云 VPS(Debian 12,x86_64,2核2G) 结论:插件系统在真实环境跑通
| 项 | 结果 |
|---|---|
SnowLuma 构建(含 onAppCreated 钩子) |
✅ |
| bootstrap 打包(自包含 ESM) | ✅ 19.6KB |
| VPS 部署 | ✅ |
| 插件加载 | ✅ |
插件 plugin_init 执行 |
✅ |
WebUI /api/plugins |
✅ |
| 插件自己的路由 | ✅ |
| 插件 action | ⚠️ 需要 QQ 账号(合理限制) |
[App] unified bootstrap: dataDir=/opt/snowluma-unified/data allowList=demo-plugin
[App] demo-plugin init, id=demo-plugin
[App] unified bootstrap: 1 plugin(s) loaded, 0 failed
$ curl /api/plugins -H "Authorization: Bearer $TOKEN"
{
"plugins": [
{
"id": "demo-plugin",
"name": "demo-plugin",
"version": "1.0.0",
"description": "端到端验证用的示例插件",
"author": "unified",
"status": "loaded",
"path": "/opt/snowluma-unified/data/plugins/demo-plugin",
"loadMs": 2
}
],
"failures": []
}
$ curl /api/plugin/demo-plugin/ping -H "Authorization: Bearer $TOKEN"
{"pong":true,"plugin":"demo-plugin"}
$ curl -X POST /api/debug/invoke -d '{"action":"demo_echo","params":{"value":"hello"}}'
{"status":"failed","message":"无效的账号"}
这是合理的限制:debug/invoke 需要 OneBot 实例(账号上下文),没登录 QQ 就没有实例。action 回退逻辑本身已由单元测试覆盖(plugin-action-fallback.test.ts 5 个测试)。
core/src/index.ts 读 UNIFIED_BOOTSTRAP 环境变量:
const unifiedBootstrap = process.env.UNIFIED_BOOTSTRAP;
initWebUI(..., {
...(unifiedBootstrap ? {
onAppCreated: async (app) => {
const mod = await import(pathToFileURL(unifiedBootstrap).href);
return await mod.default({ oneBotManager, app, logger: log });
},
} : {}),
});
unified/packages/runtime/bootstrap.ts → 打包成 dist/unified-bootstrap.mjs:
export default async function bootstrap({ oneBotManager, app, logger }) {
const unified = await createUnifiedRuntime({
dataDir: process.env.UNIFIED_DATA_DIR ?? 'data',
pluginAllowList: (process.env.UNIFIED_PLUGIN_ALLOW ?? '').split(',').filter(Boolean),
builtinActionNames: new Set(),
logger,
}, app);
oneBotManager.setPluginBridge(unified.bridge);
return unified.webuiHooks;
}
| 变量 | 默认 | 说明 |
|---|---|---|
UNIFIED_BOOTSTRAP |
(空) | bootstrap 模块路径。空 = 不启用插件系统 |
UNIFIED_PLUGIN_ALLOW |
(空) | 逗号分隔的插件白名单。空 = 不加载 |
UNIFIED_DATA_DIR |
data |
数据目录(插件在 <dir>/plugins) |
UNIFIED_BUILTIN_ACTIONS |
(空) | 逗号分隔的内置 action 名(插件不能覆盖) |
cd /opt/snowluma-unified
UNIFIED_BOOTSTRAP=/opt/snowluma-unified/unified-bootstrap.mjs \
UNIFIED_PLUGIN_ALLOW=demo-plugin \
UNIFIED_DATA_DIR=/opt/snowluma-unified/data \
node ./index.mjs
不设 UNIFIED_BOOTSTRAP 时,SnowLuma 行为完全不变 —— 这是刻意的设计。
data/plugins/demo-plugin/
├─ package.json # 必须有 type: "module"
└─ index.js # 入口
package.json 必须写 "type": "module",否则 Node 会按 CommonJS 解析并警告:
[MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of ... is not specified
已修复,但文档里要强调。
| 问题 | 原因 | 解决 |
|---|---|---|
bootstrap.mjs esbuild 报语法错误 |
.mjs 不能有 TypeScript 语法 |
改名为 .ts |
插件加载警告 MODULE_TYPELESS_PACKAGE_JSON |
插件 package.json 缺 "type": "module" |
加上 |
/api/plugins 返回 403 |
被 EULA 同意检查挡住 | 先调 /api/agreements/record-consent |
| 之后又返回 403 | 被「必须改密码」挡住 | 先调 /api/auth/change-password |
| VPS 上 SSH 命令无输出 | timeout + pkill 组合时 shell 被杀 |
用 ps aux \| awk \| xargs kill |
# 1. 本地构建
cd snowluma/src && pnpm build:all
cd unified && node packages/runtime/build.mjs
# 2. 打包上传
tar -czf /tmp/release.tar.gz -C snowluma/src/dist .
scp /tmp/release.tar.gz unified/packages/runtime/dist/unified-bootstrap.mjs root@VPS:/tmp/
# 3. 部署
ssh root@VPS '
mkdir -p /opt/snowluma-unified
tar -xzf /tmp/release.tar.gz -C /opt/snowluma-unified
cp /tmp/unified-bootstrap.mjs /opt/snowluma-unified/
mkdir -p /opt/snowluma-unified/data/plugins/demo-plugin
'
# 4. 建测试插件(见 §五)
# 5. 启动
UNIFIED_BOOTSTRAP=... UNIFIED_PLUGIN_ALLOW=demo-plugin node ./index.mjs
| 项 | 原因 |
|---|---|
| 插件 action 端到端 | 需要登录 QQ(VPS 上没有账号) |
| 事件分发到插件 | 同上(没有消息流) |
| 插件卸载 | 已由单元测试覆盖 |
| WebUI 插件页 UI | 需要浏览器(VPS 上无 GUI) |
getPluginExports 协作