From 5024f17e9c4142ad5828ff55fbff32ee014eaac7 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 15 May 2026 21:32:38 +0800 Subject: [PATCH] docs: add autman plugin development rules --- README.md | 6 + docs/autman-plugin-rules.md | 525 ++++++++++++++++++ templates/js-router-plugin-template.js | 60 ++ .../python-middleware-plugin-template.py | 38 ++ 4 files changed, 629 insertions(+) create mode 100644 docs/autman-plugin-rules.md create mode 100644 templates/js-router-plugin-template.js create mode 100644 templates/python-middleware-plugin-template.py diff --git a/README.md b/README.md index 028baee..f2ae893 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,9 @@ GET /api/sms/stats ## 当前状态 初始通用仓库骨架已创建。等待 BOSS 上传既有 Autman 插件后,再按实际风格补齐实现。 +## 文档 + +- [Autman 插件开发规则与仓库约定](docs/autman-plugin-rules.md) +- [JS 微服务路由插件模板](templates/js-router-plugin-template.js) +- [Python middleware 插件模板](templates/python-middleware-plugin-template.py) + diff --git a/docs/autman-plugin-rules.md b/docs/autman-plugin-rules.md new file mode 100644 index 0000000..50efd0d --- /dev/null +++ b/docs/autman-plugin-rules.md @@ -0,0 +1,525 @@ +# Autman 插件开发规则与仓库约定 + +> 来源:已参考 BOSS 上传到本仓库的插件示例,以及 autMan 官方论坛教程:。重点参考帖子:插件编写规则、插件编程-头注篇、插件编写概论、微服务插件编写、Python 简单插件编写、消息类型说明、实时 IM 消息监听、适配器插件编写指南。 + +## 1. 仓库用途 + +本仓库统一存放 BOSS 的 Autman 插件、参考示例、文档和模板。以后新增插件优先按下面目录组织: + +```text +js/ # ECMAScript5 / 内置 JS 插件,通常放 autMan plugin/replies +py/ # Python 插件,通常放 autMan plugin/scripts +plugins/ # 我们新写的正式插件;按功能再分目录或命名 +examples/ # 调用示例、配置示例 +templates/ # 可复制的插件模板 +docs/ # 规则、接口说明、研究记录 +``` + +已有历史插件可保留原路径。新开发插件建议优先放到 `plugins/`,稳定后可按 Autman 实际导入目录复制到 `js/` 或 `py/`。 + +## 2. 插件类型 + +Autman 支持多种插件/脚本形态: + +| 类型 | 说明 | 常见位置/用途 | +|---|---|---| +| HTML 插件 | 由 Autman Web 服务托管页面 | `plugin/web`,主要自用 | +| ES5 插件 | Autman 内置 ES5 环境运行,内置大量函数 | `plugin/replies`,适合关键词、路由、轻量集成 | +| Python/Node/TS/PHP/Shell/Go 等外部脚本 | Autman 调用宿主系统环境运行,通过 `middleware` 与 Autman 交互 | `plugin/scripts`,适合复杂逻辑、依赖第三方库 | +| 微服务路由插件 | 使用 `router` / `method` 头注暴露 HTTP 接口 | 对外接收 Webhook/API 调用 | +| 适配器插件 | 对接新的社交媒体/IM,将外部消息转换为 Autman 消息格式 | 需要处理 HTTP/WS 通信和消息格式转换 | + +注意:除 HTML 插件外,其他插件运行通常要求 Autman 授权有效。 + +## 3. 头注规则 + +头注必须放在插件文件头部,用于生成插件元数据和触发条件。 + +### 3.1 市场/基础元数据 + +JS 示例: + +```js +//[open_source: true] +//[create_at: 2026-01-01 12:00:00] +//[title: 插件名称] +//[author: 作者] +//[service: 售后联系方式] +//[class: 工具类] +//[version: 1.0.0] +//[platform: qq,wx,tg,tb,web] +//[public: false] +//[price: 0] +//[disable: false] +//[description: 插件说明,尽量写清楚用法] +//[icon: https://example.com/icon.png] +``` + +Python 示例: + +```python +# [title: 插件名称] +# [language: python] +# [service: 售后联系方式] +# [disable: false] +# [admin: true] +# [rule: ^指令$] +# [platform: qq,wx,tg,tb] +# [open_source: true] +# [version: 1.0.0] +# [public: false] +# [price: 0] +# [description: 插件说明] +``` + +字段说明: + +| 字段 | 说明 | +|---|---| +| `title` | 插件标题 | +| `author` | 作者 | +| `service` | 售后/联系方式;上架通常必填 | +| `class` | 分类,如工具类、查询类、娱乐类、生活类、图片类等 | +| `platform` | 适用平台,多个用英文逗号分隔 | +| `public` | 是否公开显示到市场;开发测试建议 `false` | +| `disable` | `true` 禁用,`false` 启用 | +| `version` | 版本号 | +| `price` | 价格 | +| `description` | 说明,建议写清楚指令、参数、注意事项 | +| `icon` | 图标 URL,建议 48px 正方形,支持 http/https | + +### 3.2 关键词/命令触发 + +```js +//[rule: ^菜单$] +//[rule: ^菜单管理$] +//[admin: true] +//[priority: 300] +//[cron: 0 7 * * *] +//[bypass: false] +``` + +规则: + +- `rule` 支持正则,可写多行表示多个触发规则。 +- `cron` 支持 5 位或 6 位 cron 表达式。 +- `admin: true` 表示仅管理员可执行。 +- `priority` 数字越大优先级越高。 +- `bypass: true` 常用于全拦截规则,例如 `[rule: ?]`,表示旁路并行处理。 +- 可用 `imType+` / `imType-`、`userId+` / `userId-`、`groupId+` / `groupId-` 做白名单/黑名单。 + +示例: + +```js +//[rule: ^sim状态$] +//[rule: ^sim短信\s+(.+?)\s+([\s\S]+)$] +//[admin: true] +//[platform: qq,wx,tg,tb] +//[priority: 100] +``` + +### 3.3 微服务路由触发 + +微服务插件通过 HTTP 路由触发,新增/修改这类插件后通常需要重启 Autman 才生效。 + +```js +//[router: /simadmin/status] +//[method: get] +//[method: post] +``` + +规则: + +- `router` 路径必须以 `/` 开头。 +- `method` 可写多个,常见为 `get`、`post`,也可支持 `put/delete`。 +- GET 参数从 `getRouterParams()` 取。 +- POST body 从 `getRouterData()` / `getRouterBody()` 取。 +- 最终必须用 `response(obj)` 或兼容方式返回,避免请求挂起。 + +### 3.4 配参头注 + +用于让 Autman 后台渲染插件配置项。 + +```js +//[param: {"required":true,"key":"otto.simadmin_base_url","bool":false,"placeholder":"http://192.168.1.10:3000","name":"SimAdmin地址","desc":"不要以 / 结尾"}] +//[param: {"required":true,"key":"otto.simadmin_token","bool":false,"placeholder":"访问Token","name":"接口鉴权Token","desc":"外部请求必须携带 access_token"}] +//[param: {"required":false,"key":"otto.simadmin_push_master","bool":true,"placeholder":"","name":"是否推送管理员","desc":""}] +//[param: {"spliter":true}] +``` + +约定: + +- `key` 建议使用统一前缀,例如 `otto.simadmin_xxx` 或 `otto.<插件名>_xxx`。 +- 密钥/token 不要写死在源码里,放到配参或环境变量。 +- `bool: true` 用于开关项。 +- `required` 只表示配置 UI 要求,不等于运行期一定存在;代码里仍需校验。 + +## 4. 常见平台/消息类型 + +论坛中整理的常见类型: + +| 类型 | 含义 | +|---|---| +| `qq` | QQ 框架 | +| `qb` | QQ 频道机器人 | +| `wx` | 微信外接框架 | +| `wb` | 内置微信机器人 | +| `tg` | Telegram 客户端 | +| `tb` | Telegram 官方机器人 | +| `wm` | 微信订阅号 | +| `wv` | 微信服务号 | +| `wk` | 微信客服 | +| `dd` | 钉钉机器人 | +| `dc` | Discord 机器人 | +| `sk` | Slack 机器人 | +| `fs` | 飞书机器人 | +| `kk` | Kook 机器人 | +| `qh` | 群晖 Synology Chat | +| `we` | 页面对话 | +| `rt` | router 消息 / 微信服务插件消息 | +| `terminal` | 交互模式终端对话 | + +## 5. ES5 插件内置函数与用法 + +### 5.1 基础信息 + +```js +var name = call("name")() +var port = call("port")() +var version = call("version")() +var machineId = call("machineId")() +var id = call("uuid")() +var sign = call("md5")("text") +var now = call("timeFormat")("yyyy-MM-dd hh:mm:ss") +var textTime = call("unixTimeFormat")(second, nanosecond, "yyyy-MM-dd hh:mm:ss") +var ok = cancall("someFunc") +var coffeeOk = coffee() +``` + +### 5.2 消息上下文 + +常见函数/对象: + +```js +var content = GetContent() +var is_admin = isAdmin() +sendText("回复文本") +var msg = input() // 等待用户输入,适合交互式插件 +sender.reply({ok:true, msg:"success"}) +Debug("调试日志") +console.log("普通日志") +``` + +建议: + +- 管理类指令必须先判断 `isAdmin()`。 +- `input()` 应处理 `null/undefined/空字符串/q退出`。 +- JSON 返回优先用对象,便于 Web/API 调用方解析。 + +### 5.3 数据桶 + +```js +bucketSet("bucket", "key", "value") +var value = bucketGet("bucket", "key") +bucketDel("bucket", "key") +var keys = bucketKeys("bucket") +``` + +约定: + +- 桶名和 key 统一前缀,避免和其他插件冲突。 +- 从 `bucketGet` 读出的复杂对象需要 `JSON.parse`,必须 try/catch。 +- 写入复杂对象用 `JSON.stringify`。 + +### 5.4 获取配置参数 + +配参 key 形如 `otto.simadmin_base_url` 时,代码中通常按后半段读取: + +```js +var baseUrl = get("simadmin_base_url") +var token = get("simadmin_token") +``` + +实际以 BOSS 上传的插件为准:`qlck.js` 中 `key: "otto.vhook_qlck_token"`,读取为 `get("vhook_qlck_token")`。 + +### 5.5 网络请求 + +回调风格: + +```js +request({ + useproxy: false, + url: "https://example.com/api", + headers: {"Content-Type":"application/json"}, + method: "post", + dataType: "json", + body: {a: 1}, + timeOut: 10000 +}, function(error, response, header, body) { + // 处理结果 +}) +``` + +同步/直接返回风格(BOSS 上传的 `js/qlck.js` 已使用): + +```js +var resp = request({ + url: baseUrl + "/api/health", + method: "get", + dataType: "json", + timeOut: 10000 +}) +``` + +约定: + +- HTTP API 插件优先使用 `dataType: "json"`。 +- 外部服务业务失败时不一定是 HTTP 非 200,要检查返回 JSON 内的状态字段。 +- 请求外部服务前清理 URL 尾部 `/`:`baseUrl.replace(/\/+$/, "")`。 +- 超时必须设置,避免插件卡死。 + +### 5.6 推送消息 + +```js +push({ + imType: "qq", + groupCode: "123456", + content: "群消息" +}) + +push({ + imType: "wx", + userID: "user-id", + content: "私聊消息" +}) +``` + +字段: + +- 群组:`groupCode` 或部分平台使用 `chatID`。 +- 私聊:`userID`。 +- 平台:`imType`。 + +## 6. 微服务插件运行规则 + +官方示例提到的微服务函数: + +```js +getRouterPath() +getRouterMethod() +getRouterHeaders() +getRouterCookies() +getRouterParams() +getRouterBody() +response(obj) +``` + +BOSS 上传插件和论坛示例中也常见兼容函数名: + +```js +var method = getMethod() +var params = getRouterParams() || {} +var data = getRouterData() +``` + +建议写法: + +```js +function jsonResponse(obj) { + if (typeof response === "function") { + response(obj) + } else if (typeof sender !== "undefined" && sender.reply) { + sender.reply(obj) + } else { + sendText(JSON.stringify(obj)) + } +} +``` + +外部 API 插件必须注意: + +1. 先做 token 鉴权,鉴权失败后立即 `return`。 +2. GET/POST 都支持时,参数读取顺序建议:query 参数 -> POST JSON 覆盖。 +3. POST JSON 解析必须 try/catch。 +4. 对发短信、重启、开关网络等敏感操作必须管理员或 token 控制。 +5. 返回统一 JSON: + +```json +{"ok":true,"status":"ok","retcode":0,"msg":"success","data":{}} +``` + +错误: + +```json +{"ok":false,"status":"fail","retcode":-1,"msg":"token错误"} +``` + +## 7. Python 插件规则 + +Python 插件通过 `middleware` 与 Autman 交互。常见头注: + +```python +# [title: 测试] +# [language: python] +# [disable: false] +# [platform: qq,wx] +# [rule: ^测试(.*)$] +# [admin: false] +# [version: 1.0.0] +``` + +常见初始化: + +```python +import middleware + +sender_id = middleware.getSenderID() +sender = middleware.Sender(sender_id) +message = sender.getMessage() +user_id = sender.getUserID() +user_name = sender.getUserName() +im_type = sender.getImtype() +chat_id = sender.getChatID() +is_admin = sender.isAdmin() +``` + +回复/监听: + +```python +sender.reply("请输入内容,q退出") +user_input = sender.listen(120000) # 毫秒 +if user_input == "q": + sender.reply("退出") + exit(0) +``` + +数据桶: + +```python +middleware.bucketSet("bucket", user_id, "value") +value = middleware.bucketGet("bucket", user_id) +middleware.bucketDel("bucket", user_id) +``` + +依赖: + +- Python 插件可自动安装依赖,但要谨慎,避免每次运行都安装。 +- 建议在代码里先 `importlib.import_module` 检测,再提示管理员安装或自动安装。 +- 网络请求可用 `requests` / `aiohttp`,但要设置 timeout。 + +## 8. 适配器插件要点 + +适配器用于接入新 IM/社交平台。官方建议: + +1. 选择不冲突的 `im_type`,尽量 2 个字母,如 `tt`。 +2. HTTP 回调地址一般为: + +```text +http://://receive +``` + +3. Autman 与适配器通信的 WS 地址一般为: + +```text +http://://adapter +``` + +4. 适配器需要把外部平台消息转换为 Autman 消息格式: + +```json +{ + "event": "message", + "event_data": {}, + "guild_id": "", + "guild_name": "", + "bot_id": "", + "bot_name": "", + "chat_id": "", + "chat_name": "", + "user_id": "", + "user_name": "", + "im_type": "tt", + "message_id": "", + "content": "消息内容" +} +``` + +5. Autman 返回动作格式: + +```json +{ + "aut_action": "send_msg", + "aut_echo": "", + "aut_params": {} +} +``` + +适配器要把 Autman 动作翻译成外部平台 API 调用,并在需要时回执消息 ID。 + +## 9. 实时 IM 消息监听 + +Autman 支持本地 SSE 监听实时 IM 消息,官方接口: + +```text +POST /otto/msghook +Accept: text/event-stream +Content-Type: application/json +``` + +请求 body 示例: + +```json +{"imtype":"qq","chatid":"11543756","userid":"282617666"} +``` + +处理要点: + +- 仅限本地访问更安全。 +- SSE 数据按行读取,关注 `event:` 和 `data:`。 +- 需要处理断线重连。 +- 解析 `data:` 后再做业务逻辑。 + +## 10. 安全与质量约定 + +开发新插件时必须遵守: + +1. **不要硬编码密钥**:token、cookie、API key 放配参、环境变量或本地不提交文件。 +2. **外部路由必须鉴权**:尤其是发短信、推送、修改配置、重启、网络开关。 +3. **鉴权失败必须 return**:避免只回复错误但继续执行后续逻辑。 +4. **敏感操作默认管理员限定**:聊天指令使用 `[admin:true]` 或代码内判断。 +5. **统一 JSON 返回**:便于外部系统调用和排错。 +6. **请求设置 timeout**:避免 Autman 插件阻塞。 +7. **兼容 GET/POST**:微服务接口可同时支持 query 和 JSON body,但 POST JSON 优先。 +8. **严格校验参数**:手机号、URL、路径、开关布尔值、枚举值都要校验。 +9. **日志不要打印密钥**:token/cookie/API key 必须脱敏。 +10. **插件说明写清楚**:头注 description 和 README 中都要写指令、参数、示例。 + +## 11. 新插件开发流程 + +1. 先在 `plugins//` 下写源码和 README。 +2. 根据插件类型复制 `templates/` 里的模板。 +3. 本地检查头注、鉴权、参数校验和错误返回。 +4. 如需适配 BOSS 已有插件风格,优先参考: + - `js/qlck.js`:微服务 API + 配参 + token 鉴权 + request 调外部 API。 + - `py/nazha.py`:Python middleware 初始化、依赖检测、平台/用户上下文。 +5. 提交前确认没有 `.env`、token、cookie、日志、运行时数据。 +6. 推送后验证 raw 文件可访问。 + +## 12. SimAdmin 插件专项约定 + +后续实现 SimAdmin 插件时建议: + +- 配参: + - `otto.simadmin_base_url` + - `otto.simadmin_access_token` + - `otto.simadmin_timeout_ms` +- 路由: + - `/simadmin/status` + - `/simadmin/sms/send` + - `/simadmin/sms/list` +- 聊天指令: + - `sim状态` + - `sim短信 <手机号> <内容>` +- SimAdmin API 业务失败要检查 JSON 内 `status === "error"`。 +- 发送短信必须 token 或管理员控制。 +- Base URL 默认不带尾部 `/`,代码内统一 trim。 diff --git a/templates/js-router-plugin-template.js b/templates/js-router-plugin-template.js new file mode 100644 index 0000000..e63d946 --- /dev/null +++ b/templates/js-router-plugin-template.js @@ -0,0 +1,60 @@ +//[open_source: true] +//[create_at: 2026-01-01 12:00:00] +//[title: 路由插件模板] +//[author: Hermes] +//[service: BOSS] +//[class: 工具类] +//[version: 1.0.0] +//[platform: web,qq,wx,tg,tb] +//[public: false] +//[price: 0] +//[disable: false] +//[description: Autman 微服务路由插件模板,支持 GET/POST、token 鉴权、JSON 返回。] +//[router: /example/api] +//[method: get] +//[method: post] +//[param: {"required":true,"key":"otto.example_token","bool":false,"placeholder":"访问Token","name":"接口鉴权Token","desc":"外部请求必须携带 access_token 或 token"}] + +function replyJSON(obj) { + if (typeof response === "function") { + response(obj) + } else if (typeof sender !== "undefined" && sender.reply) { + sender.reply(obj) + } else { + sendText(JSON.stringify(obj)) + } +} + +function fail(code, msg) { + replyJSON({ ok: false, status: "fail", retcode: code, msg: msg }) +} + +function ok(data, msg) { + replyJSON({ ok: true, status: "ok", retcode: 0, msg: msg || "success", data: data || {} }) +} + +var method = (typeof getMethod === "function" ? getMethod() : (typeof getRouterMethod === "function" ? getRouterMethod() : "get")) +var params = typeof getRouterParams === "function" ? (getRouterParams() || {}) : {} +var body = {} + +if (String(method).toLowerCase() === "post") { + var raw = typeof getRouterData === "function" ? getRouterData() : (typeof getRouterBody === "function" ? getRouterBody() : "") + if (raw) { + try { + body = JSON.parse(raw) + } catch (e) { + fail(-2, "POST JSON解析失败: " + e.message) + return + } + } +} + +var accessToken = body.access_token || body.token || params.access_token || params.token || "" +var configuredToken = get("example_token") || "" + +if (configuredToken && configuredToken !== accessToken) { + fail(-1, "token错误") + return +} + +ok({ method: method, params: params, body: body }, "接口已启用") diff --git a/templates/python-middleware-plugin-template.py b/templates/python-middleware-plugin-template.py new file mode 100644 index 0000000..11f58a6 --- /dev/null +++ b/templates/python-middleware-plugin-template.py @@ -0,0 +1,38 @@ +# [title: Python插件模板] +# [language: python] +# [service: BOSS] +# [disable: false] +# [admin: true] +# [rule: ^模板测试$] +# [platform: qq,wx,tg,tb] +# [open_source: true] +# [version: 1.0.0] +# [public: false] +# [price: 0] +# [description: Autman Python middleware 插件模板。] + +import json +import middleware + + +def main(): + sender_id = middleware.getSenderID() + sender = middleware.Sender(sender_id) + message = sender.getMessage() + + if not sender.isAdmin(): + sender.reply("仅管理员可用") + return + + data = { + "message": message, + "user_id": sender.getUserID(), + "user_name": sender.getUserName(), + "im_type": sender.getImtype(), + "chat_id": sender.getChatID(), + } + sender.reply(json.dumps(data, ensure_ascii=False)) + + +if __name__ == "__main__": + main()