mirror of
https://github.com/RaisFast/raisfast.git
synced 2026-09-24 16:02:44 +00:00
610 lines
20 KiB
Markdown
610 lines
20 KiB
Markdown
# Plugin 开发指南
|
||
|
||
## 概述
|
||
|
||
Plugin 系统是 rust-blog 的运行时扩展机制,支持三种语言运行时,可独立于 Content Type 运行。Plugin 可以注册钩子、定时任务、自定义路由,并通过 Host API 访问数据库、HTTP、配置等受控资源。
|
||
|
||
## 架构
|
||
|
||
```
|
||
plugins/
|
||
└── {plugin-id}/
|
||
├── manifest.toml # 插件清单
|
||
└── main.js # 入口文件(JS/Lua/WASM)
|
||
↓ 启动加载
|
||
PluginManager(Arc 共享)
|
||
├─ 拓扑排序(依赖顺序)
|
||
├─ 实例池(round-robin 并发)
|
||
└─ 热重载(文件系统监听)
|
||
↓ Hook 派发
|
||
Host API(沙箱权限控制)
|
||
├─ Host.dbQuery / dbExecute
|
||
├─ Host.dbBegin / dbCommit / dbRollback
|
||
├─ Host.httpGet / httpPost
|
||
├─ Host.getConfig
|
||
├─ Host.getData / setData(KV 存储)
|
||
├─ Host.fsRead / fsWrite / fsDelete / fsExists / fsList(VFS)
|
||
├─ Host.log / emitEvent
|
||
└─ Host.newId
|
||
```
|
||
|
||
### 核心模块
|
||
|
||
| 模块 | 文件 | 职责 |
|
||
|------|------|------|
|
||
| PluginManager | `src/plugins.rs` | 加载/卸载/hook 派发/热重载/事件总线 |
|
||
| Manifest | `src/plugins/manifest.rs` | TOML 清单解析 |
|
||
| Permissions | `src/plugins/permissions.rs` | 权限校验 + SQL 注入防护 + SSRF 防护 |
|
||
| JS Engine | `src/plugins/engine_js.rs` | QuickJS 运行时 + 实例池 |
|
||
| JS Host | `src/plugins/js_host.rs` | JS → Rust Host API 桥接 |
|
||
| Lua Engine | `src/plugins/engine_lua.rs` | Lua 5.4 运行时 + 实例池 |
|
||
| Lua Host | `src/plugins/lua_host.rs` | Lua → Rust Host API 桥接 |
|
||
| WASM Engine | `src/plugins/engine.rs` | wasmtime 运行时 |
|
||
| WASM Host | `src/plugins/host.rs` | WASM → Rust Host API 桥接 |
|
||
| Host Common | `src/plugins/host_common.rs` | 共享 Host 逻辑 |
|
||
| VFS | `src/plugins/vfs.rs` | 插件隔离虚拟文件系统 |
|
||
| HTTP Client | `src/plugins/http_client.rs` | 插件 HTTP 请求 |
|
||
| CLI | `src/cli/plugin_cmd.rs` | `plugin new` / `plugin check` |
|
||
|
||
## 三种运行时
|
||
|
||
| 运行时 | Cargo Feature | 入口文件 | 引擎 |
|
||
|--------|--------------|----------|------|
|
||
| JavaScript | `plugin-js` | `main.js` | rquickjs (QuickJS) |
|
||
| Lua | `plugin-lua` | `init.lua` | mlua (Lua 5.4) |
|
||
| WASM | `plugin-wasm` | `plugin.wasm` | wasmtime |
|
||
|
||
三种运行时可同时编译、同时加载。
|
||
|
||
### 实例池
|
||
|
||
每个插件创建多个运行时实例(由 `PLUGIN_JS_POOL_SIZE` / `PLUGIN_LUA_POOL_SIZE` / `PLUGIN_WASM_POOL_SIZE` 控制),以 round-robin 方式分发请求,避免并发瓶颈。
|
||
|
||
## Manifest 文件 (`manifest.toml`)
|
||
|
||
### 最小示例
|
||
|
||
```toml
|
||
[plugin]
|
||
id = "com.example.my-plugin"
|
||
name = "My Plugin"
|
||
version = "0.1.0"
|
||
runtime = "js"
|
||
entry = "main.js"
|
||
|
||
[permissions]
|
||
max_memory_mb = 16
|
||
timeout_ms = 5000
|
||
```
|
||
|
||
### `[plugin]` 字段
|
||
|
||
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|
||
|------|------|------|--------|------|
|
||
| `id` | string | 是 | — | 插件唯一 ID(建议反向域名格式) |
|
||
| `name` | string | 是 | — | 显示名称 |
|
||
| `version` | string | 是 | — | 语义版本号 |
|
||
| `description` | string | 否 | `""` | 描述 |
|
||
| `author` | string | 否 | — | 作者 |
|
||
| `license` | string | 否 | — | 许可证 |
|
||
| `runtime` | string | 否 | `"wasm"` | 运行时:`js` / `lua` / `wasm` |
|
||
| `language` | string | 否 | `"rust"` | 语言标识 |
|
||
| `entry` | string | 否 | `"index.js"` | 入口文件名 |
|
||
| `wasm` | string | 否 | `"plugin.wasm"` | WASM 文件路径 |
|
||
|
||
### `[permissions]` 权限声明
|
||
|
||
```toml
|
||
[permissions]
|
||
max_memory_mb = 16
|
||
timeout_ms = 5000
|
||
http = ["api.example.com", "*.github.com"]
|
||
config = ["app.*", "jwt.*"]
|
||
database = ["read:products", "write:orders", "categories"]
|
||
filesystem = ["read-write"]
|
||
```
|
||
|
||
| 字段 | 类型 | 默认 | 说明 |
|
||
|------|------|------|------|
|
||
| `max_memory_mb` | int | 配置默认值 | 单实例内存上限 |
|
||
| `timeout_ms` | int | 配置默认值 | Hook 执行超时 |
|
||
| `http` | string[] | `[]`(禁止) | HTTP 白名单 |
|
||
| `config` | string[] | `[]`(禁止) | 配置读取白名单 |
|
||
| `database` | string[] | `[]`(禁止) | 数据库权限 |
|
||
| `filesystem` | string[] | `[]`(禁止) | 文件系统权限 |
|
||
|
||
#### 数据库权限格式
|
||
|
||
| 格式 | 权限 |
|
||
|------|------|
|
||
| `"read:TABLE"` | 只读 |
|
||
| `"write:TABLE"` | 只写 |
|
||
| `"TABLE"` | 读写 |
|
||
| `"*"` | 所有表(受保护表除外) |
|
||
|
||
#### HTTP 白名单
|
||
|
||
- 精确域名:`api.example.com`
|
||
- 通配符子域:`*.github.com`
|
||
- 路径通配:`api.example.com/v1/*`
|
||
|
||
内置 SSRF 防护:自动阻止 localhost、127.x、10.x、172.16-31.x、192.168.x、169.254.x、::1。
|
||
|
||
#### 受保护表
|
||
|
||
以下系统表即使声明 `"*"` 也不可访问:
|
||
|
||
```
|
||
users, roles, permissions, audit_log, plugin_storage, options,
|
||
rbac_roles, rbac_permissions, rbac_role_permissions, tenants
|
||
```
|
||
|
||
### `[hooks.XXX]` 钩子注册
|
||
|
||
```toml
|
||
[hooks.on-content-created]
|
||
priority = 50
|
||
|
||
[hooks.on-content-updating]
|
||
priority = 100
|
||
|
||
[hooks.render-markdown]
|
||
priority = 10
|
||
```
|
||
|
||
| 字段 | 类型 | 默认 | 说明 |
|
||
|------|------|------|------|
|
||
| `priority` | int | 100 | 优先级,数字越小越先执行 |
|
||
|
||
钩子名使用连字符(`on-content-created`),系统自动转换为下划线(`on_content_created`)。
|
||
|
||
### 17 种钩子
|
||
|
||
| 钩子名 | 类型 | 说明 |
|
||
|--------|------|------|
|
||
| `on-content-creating` | filter | 内容创建前,可修改数据 |
|
||
| `on-content-created` | action | 内容创建后 |
|
||
| `on-content-updating` | filter | 内容更新前,可修改数据 |
|
||
| `on-content-updated` | action | 内容更新后 |
|
||
| `on-content-deleted` | action | 内容删除后 |
|
||
| `on-content-viewed` | action | 内容被浏览 |
|
||
| `on-post-creating` | filter | 文章创建前(兼容) |
|
||
| `on-post-created` | action | 文章创建后(兼容) |
|
||
| `on-post-updating` | filter | 文章更新前(兼容) |
|
||
| `on-post-updated` | action | 文章更新后(兼容) |
|
||
| `on-post-deleted` | action | 文章删除后(兼容) |
|
||
| `on-comment-creating` | filter | 评论创建前(兼容) |
|
||
| `on-comment-created` | action | 评论创建后(兼容) |
|
||
| `render-markdown` | filter | Markdown 渲染覆盖(第一个返回 wins) |
|
||
| `filter-html` | filter | HTML 过滤 |
|
||
| `on-login` | action | 用户登录后 |
|
||
| `on-cron-tick` | action | 定时任务触发 |
|
||
|
||
**filter 类型**:可修改数据,返回值传递给下一个插件。
|
||
**action 类型**:仅副作用,返回值忽略。
|
||
|
||
### `[[cron]]` 定时任务
|
||
|
||
```toml
|
||
[[cron]]
|
||
label = "每日统计"
|
||
job_type = "daily_stats"
|
||
cron_expr = "0 0 * * *"
|
||
payload = """{"type": "full"}"""
|
||
enabled = true
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| `label` | string | 是 | 任务名称 |
|
||
| `job_type` | string | 是 | 任务类型(传给 `on_cron_tick`) |
|
||
| `cron_expr` | string | 是 | Cron 表达式 |
|
||
| `payload` | string | 否 | 附带数据 |
|
||
| `enabled` | bool | 否 | 默认 true |
|
||
|
||
### `[[routes]]` 自定义路由
|
||
|
||
```toml
|
||
[[routes]]
|
||
method = "GET"
|
||
path = "/api/v1/plugins/crm/pipeline"
|
||
handler = "getPipeline"
|
||
auth = "admin"
|
||
|
||
[[routes]]
|
||
method = "GET"
|
||
path = "/api/v1/plugins/crm/contacts/:contactId"
|
||
handler = "getContact"
|
||
auth = "public"
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 默认 | 说明 |
|
||
|------|------|------|------|------|
|
||
| `method` | string | 是 | — | HTTP 方法 |
|
||
| `path` | string | 是 | — | 路由路径,支持 `:param` 占位符 |
|
||
| `handler` | string | 是 | — | 对应 Plugin 对象的函数名 |
|
||
| `auth` | string | 否 | `default` | `none` / `public` / `member` / `admin` |
|
||
| `description` | string | 否 | — | 描述 |
|
||
| `permission` | string | 否 | — | 额外权限要求 |
|
||
|
||
自定义路由的响应由框架统一包装为 `{ code: 0, message: "success", data: ... }` 格式。
|
||
|
||
## Host API
|
||
|
||
所有运行时通过统一的 `Host.*` 接口访问宿主功能:
|
||
|
||
### 数据库
|
||
|
||
```javascript
|
||
// 参数化查询(防注入)
|
||
const rows = JSON.parse(Host.dbQuery("SELECT * FROM products WHERE price > ?", JSON.stringify([100])));
|
||
const affected = Host.dbExecute("UPDATE products SET stock = stock - 1 WHERE id = ?", JSON.stringify([id]));
|
||
|
||
// 事务
|
||
Host.dbBegin();
|
||
Host.dbExecute("INSERT INTO orders ...", null);
|
||
Host.dbExecute("UPDATE products ...", null);
|
||
Host.dbCommit(); // 或 Host.dbRollback()
|
||
```
|
||
|
||
| 函数 | 说明 | 权限 |
|
||
|------|------|------|
|
||
| `Host.dbQuery(sql, params?)` | SELECT 查询 | `database` read |
|
||
| `Host.dbExecute(sql, params?)` | INSERT/UPDATE/DELETE | `database` write |
|
||
| `Host.dbBegin()` | 开启事务 | 需要 pool |
|
||
| `Host.dbCommit()` | 提交事务 | 活跃事务 |
|
||
| `Host.dbRollback()` | 回滚事务 | 活跃事务 |
|
||
|
||
> **注意**:`dbQuery` 返回的整数列是 `null`,需用 `CAST(col AS TEXT)` 转为字符串后再 `parseInt`。
|
||
|
||
### HTTP
|
||
|
||
```javascript
|
||
const html = Host.httpGet("https://api.example.com/data");
|
||
const result = Host.httpPost("https://api.example.com/webhook", JSON.stringify({event: "test"}));
|
||
```
|
||
|
||
### KV 存储
|
||
|
||
```javascript
|
||
Host.setData("last_sync", "2026-01-01");
|
||
const val = Host.getData("last_sync");
|
||
```
|
||
|
||
每个插件有独立命名空间,互不干扰。
|
||
|
||
### 配置
|
||
|
||
```javascript
|
||
const host = Host.getConfig("app.host");
|
||
const env = Host.getConfig("app.env");
|
||
```
|
||
|
||
允许的配置键(需在 `permissions.config` 白名单中):
|
||
`app.host`, `app.port`, `app.env`, `app.base_url`, `jwt.access_expires`, `jwt.refresh_expires`, `upload.dir`, `upload.max_size`
|
||
|
||
### 文件系统(VFS)
|
||
|
||
```javascript
|
||
Host.fsWrite("/reports/daily.json", reportJson);
|
||
const data = Host.fsRead("/reports/daily.json");
|
||
const exists = Host.fsExists("/reports/daily.json");
|
||
const files = Host.fsList("/reports");
|
||
Host.fsDelete("/reports/old.json");
|
||
```
|
||
|
||
每个插件在 `{VFS_ROOT}/{plugin_id}/` 下有隔离沙箱,路径不能包含 `..`。
|
||
|
||
### 其他
|
||
|
||
```javascript
|
||
Host.log("info", "Processing order " + orderId);
|
||
Host.log("warn", "Low stock detected");
|
||
Host.log("error", "Payment failed: " + error);
|
||
Host.emitEvent("order.created", JSON.stringify({orderId: id}));
|
||
const id = Host.newId(); // UUID v7
|
||
```
|
||
|
||
| 函数 | 说明 |
|
||
|------|------|
|
||
| `Host.log(level, msg)` | 日志输出(level: `info`/`warn`/`error`) |
|
||
| `Host.emitEvent(type, data)` | 发射事件到事件总线 |
|
||
| `Host.newId()` | 生成 UUID v7(时间排序,与系统主键一致) |
|
||
|
||
## 编写 JS 插件(ES Module + SDK)
|
||
|
||
### 项目结构
|
||
|
||
```
|
||
plugins/my-plugin/
|
||
├── manifest.toml
|
||
└── main.js
|
||
```
|
||
|
||
### 代码模板
|
||
|
||
```javascript
|
||
import { dbQuery, dbExec, ok, fail, extractJson, logInfo, newId } from 'sdk';
|
||
|
||
// ── Hook ──
|
||
|
||
export function on_content_created(input) {
|
||
const data = extractJson(input, "body");
|
||
if (data?.content_type === "product") {
|
||
logInfo("[my-plugin] new product: " + data.id);
|
||
}
|
||
return ok(data);
|
||
}
|
||
|
||
// ── 自定义路由 ──
|
||
|
||
export function getProduct(input) {
|
||
const id = extractJson(input, "params.id");
|
||
if (!id) return fail(400, "id required");
|
||
const rows = dbQuery("SELECT * FROM products WHERE id = ?", [id]);
|
||
if (!rows || rows.length === 0) return fail(404, "product not found");
|
||
return ok(rows[0]);
|
||
}
|
||
|
||
// ── 定时任务 ──
|
||
|
||
export function on_cron_tick(input) {
|
||
const data = extractJson(input, "body");
|
||
if (data?.job_type === "daily_cleanup") {
|
||
dbExec("DELETE FROM sessions WHERE expires_at < datetime('now')");
|
||
logInfo("[my-plugin] daily cleanup done");
|
||
}
|
||
}
|
||
```
|
||
|
||
### SDK v1 API(`import { ... } from 'sdk'`)
|
||
|
||
| 函数 | 说明 |
|
||
|------|------|
|
||
| `dbQuery(sql, params?)` | 参数化 SELECT 查询,返回对象数组;错误时抛异常 |
|
||
| `dbExec(sql, params?)` | INSERT/UPDATE/DELETE,返回 `{ error?, rows_affected }` |
|
||
| `dbBegin()` | 开启事务(失败时抛异常) |
|
||
| `dbCommit()` | 提交事务(失败时抛异常) |
|
||
| `dbRollback()` | 回滚事务 |
|
||
| `ok(data)` | 成功响应:返回数据,框架自动包装为 `{code:0, data}` |
|
||
| `fail(status, msg)` | 错误响应:框架包装为 `{code:N, message}` |
|
||
| `extractJson(input, field?)` | 从 JSON 中提取指定字段(支持 `params.id` 点号路径),不存在返回 `null` |
|
||
| `logInfo(msg)` / `logWarn(msg)` / `logError(msg)` | 日志输出 |
|
||
| `newId()` | 生成 UUID v7(时间排序,与系统一致) |
|
||
| `eventEmit(type, data)` | 发射事件到事件总线 |
|
||
| `httpGet(url)` | HTTP GET 返回原始字符串 |
|
||
| `httpGetJson(url)` | HTTP GET 并解析 JSON |
|
||
| `httpPost(url, body)` | HTTP POST 返回原始字符串 |
|
||
| `httpPostJson(url, body)` | HTTP POST 并解析 JSON |
|
||
| `configGet(key)` | 读取配置(需 `config` 权限) |
|
||
| `storeGet(key)` / `storeSet(key, val)` | KV 存储 |
|
||
| `vfsRead(path)` / `vfsWrite(path, content)` | 虚拟文件系统读写 |
|
||
| `vfsDelete(path)` / `vfsExists(path)` | 虚拟文件系统删除/判断存在 |
|
||
| `vfsList(path)` | 列出目录下文件,返回数组 |
|
||
|
||
### 关键约定
|
||
|
||
- **必须使用 `export function`** 导出 handler(ES Module 模式,引擎自动收集到 Plugin 对象)
|
||
- 路由处理:`input` 包含 `{ path, method, body, headers, params }`,直接 `return ok(data)` 或 `return fail(status, msg)`
|
||
- Filter 钩子:接收 JSON 字符串 `input`,用 `extractJson(input, "body")` 提取数据
|
||
- Action 钩子:接收 JSON 字符串,返回值被忽略
|
||
- 支持 ES2024 完整语法(`let`/`const`、箭头函数、`async/await`、可选链等)
|
||
- `dbQuery()` 查询失败时抛异常,可用 `try/catch` 捕获
|
||
- `dbQuery()` 返回的整数列为 `null`,必须用 `CAST(col AS TEXT)` 转为字符串后再 `parseInt`
|
||
|
||
### 相对路径导入
|
||
|
||
```javascript
|
||
import { helper } from './utils.js';
|
||
```
|
||
|
||
## 编写 Lua 插件(SDK 模式)
|
||
|
||
### 项目结构
|
||
|
||
```
|
||
plugins/my-plugin/
|
||
├── manifest.toml
|
||
└── init.lua
|
||
```
|
||
|
||
### 代码模板
|
||
|
||
```lua
|
||
local sdk = require("sdk")
|
||
|
||
Plugin = {}
|
||
|
||
Plugin.on_content_created = function(input)
|
||
local data = sdk.extractJson(input, "body")
|
||
if data and data.content_type == "product" then
|
||
sdk.logInfo("[my-plugin] new product: " .. tostring(data.id))
|
||
end
|
||
return sdk.ok(data)
|
||
end
|
||
|
||
Plugin.on_cron_tick = function(input)
|
||
local data = sdk.extractJson(input, "body")
|
||
if data.job_type == "daily_cleanup" then
|
||
sdk.dbExec("DELETE FROM sessions WHERE expires_at < datetime('now')")
|
||
sdk.logInfo("[my-plugin] cleanup done")
|
||
end
|
||
end
|
||
|
||
Plugin.getStats = function(input)
|
||
local result = sdk.dbQuery("SELECT CAST(COUNT(*) AS TEXT) as cnt FROM products WHERE status = 'published'")
|
||
return sdk.ok({ total = tonumber(result[1].cnt) or 0 })
|
||
end
|
||
```
|
||
|
||
### SDK v1 API(`local sdk = require("sdk")`)
|
||
|
||
| 函数 | 说明 |
|
||
|------|------|
|
||
| `sdk.dbQuery(sql, params?)` | 参数化 SELECT 查询,返回数组表;错误时抛异常 |
|
||
| `sdk.dbExec(sql, params?)` | INSERT/UPDATE/DELETE,返回结果表 |
|
||
| `sdk.dbBegin()` | 开启事务(失败时抛异常) |
|
||
| `sdk.dbCommit()` | 提交事务(失败时抛异常) |
|
||
| `sdk.dbRollback()` | 回滚事务 |
|
||
| `sdk.ok(data)` | 成功响应:返回数据,框架自动包装 |
|
||
| `sdk.fail(status, msg)` | 错误响应:框架包装为 `{code:N, message}` |
|
||
| `sdk.extractJson(input, field?)` | 从 JSON 中提取指定字段(支持点号路径),不存在返回 `nil` |
|
||
| `sdk.logInfo(msg)` / `sdk.logWarn(msg)` / `sdk.logError(msg)` | 日志输出 |
|
||
| `sdk.newId()` | 生成 UUID v7(时间排序,与系统一致) |
|
||
| `sdk.eventEmit(type, data)` | 发射事件 |
|
||
| `sdk.httpGet(url)` | HTTP GET 返回原始字符串 |
|
||
| `sdk.httpGetJson(url)` | HTTP GET 并解析 JSON |
|
||
| `sdk.httpPost(url, body)` | HTTP POST 返回原始字符串 |
|
||
| `sdk.httpPostJson(url, body)` | HTTP POST 并解析 JSON |
|
||
| `sdk.configGet(key)` | 读取配置 |
|
||
| `sdk.storeGet(key)` / `sdk.storeSet(key, val)` | KV 存储 |
|
||
| `sdk.vfsRead(path)` / `sdk.vfsWrite(path, content)` | 虚拟文件系统读写 |
|
||
| `sdk.vfsDelete(path)` / `sdk.vfsExists(path)` | 虚拟文件系统删除/判断存在 |
|
||
| `sdk.vfsList(path)` | 列出目录下文件,返回数组 |
|
||
|
||
### 关键约定
|
||
|
||
- 必须导出全局 `Plugin` 表(Lua 不强制 ESM,但仍需 `Plugin = {}`)
|
||
- 使用 `local sdk = require("sdk")` 导入 SDK 模块
|
||
- Filter 钩子:接收 Lua table,返回 Lua table
|
||
- 路由处理:`input` 包含 `{ path, method, body, headers, params }`,直接 `return sdk.ok(data)` 或 `return sdk.fail(status, msg)`
|
||
- 沙箱环境:仅暴露 `table`, `string`, `math`, `utf8`, `coroutine` 标准库(无 IO/OS/debug)
|
||
- 指令限制:5,000,000 条
|
||
- `sdk.dbQuery()` 查询失败时抛异常,可用 `pcall` 捕获
|
||
- `sdk.dbQuery()` 返回的整数列在 Lua 中可能为 `nil`,建议用 `CAST(col AS TEXT)` 转换
|
||
|
||
## 错误恢复
|
||
|
||
- 连续 **5 次** 错误自动禁用插件
|
||
- 错误计数在成功执行时重置
|
||
- 可通过 Admin API 手动重新启用
|
||
- 插件超时/崩溃时自动回滚未提交的事务
|
||
|
||
## 热重载
|
||
|
||
当 `PLUGIN_HOT_RELOAD=true`(默认开启)时:
|
||
|
||
1. 文件系统监听器监控插件目录的 `.js` / `.lua` / `.wasm` 文件变化
|
||
2. 1 秒防抖
|
||
3. 自动卸载 + 重新加载变化的插件
|
||
4. 发出 `PluginReloaded` 事件
|
||
|
||
## 管理 API
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| GET | `/api/v1/admin/plugins` | 列出所有插件(含状态/健康/指标) |
|
||
| GET | `/api/v1/admin/plugins/{id}` | 插件详情 |
|
||
| POST | `/api/v1/admin/plugins/{id}/enable` | 启用 |
|
||
| POST | `/api/v1/admin/plugins/{id}/disable` | 禁用 |
|
||
| POST | `/api/v1/admin/plugins/{id}/reload` | 热重载 |
|
||
| DELETE | `/api/v1/admin/plugins/{id}` | 卸载 |
|
||
|
||
## CLI 命令
|
||
|
||
```bash
|
||
# 创建新插件
|
||
rust-blog plugin new my-plugin --runtime js # JavaScript
|
||
rust-blog plugin new my-plugin --runtime lua # Lua
|
||
rust-blog plugin new my-plugin --runtime wasm # WASM
|
||
|
||
# 校验插件
|
||
rust-blog plugin check # 校验默认目录
|
||
rust-blog plugin check ./plugins/my-plugin # 校验指定目录
|
||
```
|
||
|
||
## 环境变量
|
||
|
||
| 变量 | 默认值 | 说明 |
|
||
|------|--------|------|
|
||
| `PLUGIN_DIR` | `./extensions/plugins` | 插件目录 |
|
||
| `PLUGIN_VFS_ROOT` | `./plugins-data` | VFS 根目录 |
|
||
| `PLUGIN_VFS_MAX_FILE_SIZE` | `1048576` | 单文件最大 1MB |
|
||
| `PLUGIN_VFS_MAX_TOTAL_SIZE` | `10485760` | 总配额 10MB |
|
||
| `PLUGIN_WASM_POOL_SIZE` | `4` | WASM 实例池大小 |
|
||
| `PLUGIN_JS_POOL_SIZE` | `4` | JS 实例池大小 |
|
||
| `PLUGIN_LUA_POOL_SIZE` | `4` | Lua 实例池大小 |
|
||
|
||
## 完整示例:CRM 插件
|
||
|
||
```
|
||
plugins/crm/
|
||
├── manifest.toml
|
||
└── main.js
|
||
```
|
||
|
||
**manifest.toml:**
|
||
|
||
```toml
|
||
[plugin]
|
||
id = "com.rust-blog.crm"
|
||
name = "CRM API"
|
||
version = "0.1.0"
|
||
description = "CRM 销售漏斗、Pipeline 管理、联系人时间线"
|
||
runtime = "js"
|
||
entry = "main.js"
|
||
|
||
[permissions]
|
||
max_memory_mb = 16
|
||
timeout_ms = 5000
|
||
database = ["crm_contacts", "crm_companies", "crm_deals", "crm_activities", "crm_notes"]
|
||
config = ["app.*"]
|
||
|
||
[hooks.on-content-created]
|
||
priority = 50
|
||
|
||
[hooks.on-content-updated]
|
||
priority = 50
|
||
|
||
[[routes]]
|
||
method = "GET"
|
||
path = "/api/v1/plugins/crm/pipeline"
|
||
handler = "getPipeline"
|
||
|
||
[[routes]]
|
||
method = "GET"
|
||
path = "/api/v1/plugins/crm/pipeline/:dealId"
|
||
handler = "getDealDetail"
|
||
|
||
[[routes]]
|
||
method = "POST"
|
||
path = "/api/v1/plugins/crm/deals/:dealId/stage"
|
||
handler = "updateDealStage"
|
||
```
|
||
|
||
**main.js(节选):**
|
||
|
||
```javascript
|
||
import { dbQuery, dbExec, ok, fail, extractJson, logInfo, eventEmit, newId } from 'sdk';
|
||
|
||
export function getPipeline() {
|
||
const stages = ["prospecting", "qualification", "proposal", "negotiation", "closed_won", "closed_lost"];
|
||
const pipeline = [];
|
||
for (const stage of stages) {
|
||
const rows = dbQuery(
|
||
`SELECT id, title, amount FROM crm_deals WHERE stage = ? ORDER BY amount DESC`,
|
||
[stage]
|
||
);
|
||
pipeline.push({ stage, deals: rows || [] });
|
||
}
|
||
return ok({ stages: pipeline });
|
||
}
|
||
|
||
export function getDealDetail(input) {
|
||
const dealId = extractJson(input, "params.dealId");
|
||
if (!dealId) return fail(400, "deal id required");
|
||
const deals = dbQuery(`SELECT * FROM crm_deals WHERE id = ?`, [dealId]);
|
||
if (!deals || deals.length === 0) return fail(404, "deal not found");
|
||
return ok(deals[0]);
|
||
}
|
||
|
||
export function on_content_created(input) {
|
||
const data = extractJson(input, "body");
|
||
if (data.content_type === "contact") {
|
||
logInfo(`[crm] new contact: ${data.id}`);
|
||
eventEmit("crm.lead_created", JSON.stringify({ contact_id: data.id }));
|
||
}
|
||
return ok(data);
|
||
}
|
||
```
|