跳到主要内容

Webhook 与 CronJob 使用文档

本文档说明 JuanNiang-Neo 的两种"主动消息"机制。

  • Webhook:接收外部 HTTP 请求,转成 webhook 事件喂给 Lua 插件(不走 LLM Agent),用于外部系统集成。
  • CronJob:基于 cron 表达式定时构造合成 cronjob 事件,通过统一事件循环 → Plugin.Dispatch → 插件 on_cronjob 回调触发,经过 Agent。

Webhook

用途

让外部服务(GitHub、GitLab、监控告警、表单 webhook 等)通过 HTTP 请求触发机器人动作。Webhook 不走 LLM Agent,只把 Payload 交给有 webhook 权限的 Lua 插件处理——这是给插件做外部集成的专用钩子。

架构与数据流

核心特性

特性说明
独立端口:8091,与 API :8090、OB :8081 完全隔离
Token 鉴权Bearer token,配了才校验,不配则任何请求都通过
不走 AgentPostType=="webhook" 在事件循环中直接短路,永远不会进 LLM
定向模式/webhook/{plugin_name} 按名称精确路由到指定插件,其他插件不会收到事件
广播模式/webhook/ 路径无插件名时,广播给所有有 webhook 权限的插件
插件自决插件自己在 on_webhook 里判断 payload 并决定是否处理
定向返回定向模式下插件可返回 (consumed, reply),reply 会作为响应 metadata 返回调用方
统一响应所有响应使用 {"code":<int>,"message":"<str>","metadata":<any>} 格式
队列丢弃广播模式 events channel cap=128,满了返回 {"code":503,"message":"..."}
热更新Web 面板点"启用"即生效,无需重启

配置入口

Web 面板"Webhook"页 → PUT /api/v1/webhook/config

字段类型默认说明
addrstring0.0.0.0监听地址
portint8091监听端口
tokenstring(空)Bearer 鉴权令牌;空=不校验
enabledboolfalse启用开关

配置是 DB 单行 id=1,不读 env。docker compose 默认未映射 :8091,需自行添加:

# docker-compose.yaml
services:
juan-niang-neo:
ports:
- "8091:8091" # Webhook

HTTP 协议

项目说明
路由定向: /webhook/{plugin_name} → 路由到指定插件;广播: //webhook → 广播给所有插件
方法任意(常用 POST)
HeaderAuthorization: Bearer <token>(配了 token 才校验)
Body任意。先尝试 JSON unmarshal;失败则包装为 {"raw":"原文","type":"non-json"}
成功200 OK,body: {"code":0,"message":"ok"}(定向命中时可能带 metadata
未找到404 Not Found,body: {"code":404,"message":"plugin not found"}
队列满503 Service Unavailable,body: {"code":503,"message":"events channel full"}
鉴权失败403 Forbidden,body: {"code":403,"message":"forbidden"}

响应格式

所有 webhook 响应统一为:

{
"code": 0,
"message": "ok",
"metadata": null
}
字段类型说明
codeint0=成功, 403=鉴权失败, 404=插件未找到, 503=队列满
messagestring人类可读的描述
metadataany定向模式下插件 on_webhook 返回的 reply 字符串;否则 null

Event 数据结构(Lua 侧)

-- on_webhook(event) 收到的 event 结构

event.post_type = "webhook" -- 固定

event.webhook = {
path = "/github", -- string: 请求路径
method = "POST", -- string: HTTP 方法
payload = { -- table: Body JSON 解析结果
action = "opened", -- (示例: GitHub PR opened)
pull_request = { ... },
repository = { ... },
sender = { ... },
}
}

event.admins = { "10001", "10002" } -- 系统管理员 QQ 列表

多插件共存:如何区分事件归属

webhook 有两种路由模式

  • 定向模式 (/webhook/{plugin_name}):精确路由到指定插件,其他插件不会收到事件。这是推荐的隔离方式。
  • 广播模式 (/webhook/):无插件名时,广播给所有有 webhook 权限的插件。插件通过以下三种方式自行判断是否处理。

方式零:定向路由(推荐)

使用 /webhook/{plugin_name} 路径,消息直接路由到指定插件:

# GitHub 插件配置这个 URL
http://host:8091/webhook/my-github-plugin

# 监控插件配置这个 URL
http://host:8091/webhook/my-alert-plugin
-- my-github-plugin 的 on_webhook
function on_webhook(event)
local p = event.webhook.payload
-- 无需路径判断,只有本插件会收到此事件
onebot11.send_group_msg(987654321, "新 PR: " .. (p.pull_request.title or "?"))
return true
end

定向模式下,插件可以返回 (consumed, reply_string),reply 会作为响应的 metadata 返回给调用方:

function on_webhook(event)
local p = event.webhook.payload
if p.action == "opened" then
return true, "PR opened notification sent"
end
return false, "unhandled action: " .. (p.action or "?")
end

方式一:payload 字段自识别(广播模式下推荐)

每个插件检查自己关心的字段,不相关则立即 return false

-- GitHub 插件
function on_webhook(event)
local p = event.webhook.payload
if not p.repository or not p.sender then return false end
-- 处理 GitHub 事件...
end

-- 告警插件
function on_webhook(event)
local p = event.webhook.payload
if not p.alert_name then return false end
-- 处理告警事件...
end

优点是解耦:外部服务不需要知道"该调哪个 URL",只要发一个 JSON 就行。插件靠字段自我识别。

方式二:路径区分

外部服务请求不同路径,插件检查 event.webhook.path

# GitHub 配置这个 URL
http://host:8091/github

# 监控配置这个 URL
http://host:8091/alert
-- GitHub 插件
function on_webhook(event)
if event.webhook.path ~= "/github" then return false end
-- ...
end

方式三:约定 action 字段

在 payload 里约定一个标识字段:

{"_source": "github", "pull_request": {...}}
{"_source": "alert", "message": "CPU 超了"}
function on_webhook(event)
if event.webhook.payload._source ~= "alert" then return false end
-- ...
end

配置与测试

开启 webhook

curl -X PUT http://localhost:8090/api/v1/webhook/config \
-H "Authorization: Bearer <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"addr": "0.0.0.0",
"port": 8091,
"token": "my-secret-token",
"enabled": true
}'

curl 测试

# 测试 1:简单 JSON
curl -X POST http://localhost:8091/ \
-H "Authorization: Bearer my-secret-token" \
-H "Content-Type: application/json" \
-d '{"message": "服务器负载过高", "level": "error", "group_id": 1076723599}'

# 测试 2:非 JSON body(会被包装)
curl -X POST http://localhost:8091/ \
-H "Authorization: Bearer my-secret-token" \
-d "raw text body"
# 插件收到: event.webhook.payload = {raw="raw text body", type="non-json"}

# 测试 3:不配 token 时不需要 Authorization 头
curl -X POST http://localhost:8091/ \
-H "Content-Type: application/json" \
-d '{"hi":"there"}'

完整示例

pluggin.yaml

name: github-notify
version: "1.0.0"
author: me
description: "GitHub Webhook 通知"
entry: main.lua
permissions:
- webhook
- onebot11
enabled: true

main.lua

local jn = require("jn")
local GROUP = 987654321

function on_webhook(event)
local p = event.webhook and event.webhook.payload or {}

-- GitHub PR opened
if p.action == "opened" and p.pull_request then
local repo = (p.repository and p.repository.full_name) or "?"
local title = p.pull_request.title or "?"
local url = p.pull_request.html_url or ""
local user = p.sender.login or "?"
jn.onebot11.send_group_msg(GROUP,
string.format("🔀 %s 提了新 PR\n[%s] %s\n%s", user, repo, title, url))
return true
end

-- GitHub Issue opened
if p.action == "opened" and p.issue and not p.pull_request then
local title = p.issue.title or "?"
local url = p.issue.html_url or ""
jn.onebot11.send_group_msg(GROUP,
string.format("🐛 新 Issue: %s\n%s", title, url))
return true
end

-- GitHub Push
if p.commits and p.ref then
local branch = p.ref:gsub("refs/heads/", "")
local n = #(p.commits or {})
jn.onebot11.send_group_msg(GROUP,
string.format("📤 %s pushed to %s (%d commits)",
p.pusher.name, branch, n))
return true
end

return false -- 不是 GitHub 事件,放行给其他插件
end

配套 data/pluggins/webhook-example/ 插件提供了完整的多格式支持(GitHub / 通用告警 / 钉钉飞书),可直接使用或参考。

注意

  • Webhook 默认关闭;docker compose 默认未映射 :8091,需自行加 ports:
  • 事件队列满(128)会丢并返回 503,外部服务应实现重试
  • 无 SPA 兜底,仅 webhook + / 路由
  • Webhook 配置是 DB 单行 id=1,不读 env
  • 插件需声明 webhook 权限才会被调用;想在回调里发消息需同时声明 onebot11
  • 多个 webhook 插件共存时,每个插件在 on_webhook 开头做字段判断快速 return false 避免冲突

CronJob

用途

CronJob 定时触发插件的 on_cronjob 回调,通过统一事件循环 → Plugin.Dispatch 分发。经过 Agent,不经过回复策略与 ACL。

字段说明
plugin_ids触发插件列表(插件目录名),到点时调用其 on_cronjob(event)
payloadJSON 字符串,传递给 event.payload

到点时 CronJobManager.makeJobFunc 构造合成 Event{PostType:"cronjob"},注入统一事件循环,经 Plugin.Dispatch 分发到指定插件的 on_cronjob 回调。

只有已加载且定义了 on_cronjob 全局函数的插件会被调用。前端"定时任务"页面多选下拉框自动过滤显示 supports_cronjob=true 的已启用插件。

检测方式:运行时检查插件 Lua 全局 on_cronjob 是否为函数 → ListMaps() 返回 supports_cronjob: bool

示例插件data/pluggins/cron-example/ 提供了一个完整的定时触发示例——向 payload 中指定的 QQ 号或群发送消息。

示例 Payload(私聊):

{
"target_qq": 123456789,
"message": "⏰ 定时提醒:该喝水啦!"
}

示例 Payload(群聊):

{
"message_type": "group",
"group_id": 123456,
"message": "⏰ 群每日播报:今日天气..."
}

Cron 表达式

6 字段,支持秒级robfig/cron WithSeconds()):

秒 分 时 日 月 周
0 0 9 * * * # 每天 9:00
0 */5 * * * * # 每 5 分钟
0 0 0 1 * * # 每月 1 日 0:00
0 30 8 * * 1-5 # 工作日 8:30

时区:time.Local(容器里 TZ=Asia/Shanghai)。

API

方法路径用途
GET/api/v1/cronjobs列出所有
GET/api/v1/cronjobs/:id单个详情
POST/api/v1/cronjobs新增(自动 reload 调度器)
PUT/api/v1/cronjobs/:id覆盖更新(自动 reload)
DELETE/api/v1/cronjobs/:id删除(自动 reload)
PUT/api/v1/cronjobs/:id/toggle启停(自动 reload)

AddCronJobReq body 字段:namecron_expris_activemessage(合成消息内容,可选)、message_typeprivate/group,默认 private)、target_id(私聊=QQ 号 / 群聊=群号)、plugin_idsstring[],可选)、payload(JSON 字符串,可选)。

示例:每 10 秒触发插件发消息

curl -X POST http://localhost:8090/api/v1/cronjobs \
-H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-d '{
"name": "定时通知",
"cron_expr": "*/10 * * * * *",
"plugin_ids": ["cron-example"],
"payload": "{\"target_qq\":123456789,\"message\":\"每10秒的提醒\"}",
"is_active": true
}'

运行时流(细节)

LastRunAt/LastError 回写 DB,前端"定时任务"页可看历史。删改/toggle 后由 Service.AddCronJob/...CronJobManager.Reload() 同步调度器,无需重启进程。

注意

  • 满则丢:事件队列满时会被丢弃,不阻塞调度器
  • 跨容器时区TZ=Asia/Shanghai 容器内是北京时间;裸机部署注意主机时区
  • Plugin 调用的检测:只有定义了 on_cronjob 全局函数且已加载的插件才会被调用;前端多选下拉框自动过滤
  • Plugin Payload:必须是合法 JSON 字符串,保存时前端 CodeMirror 编辑器会实时校验格式
  • 插件重载POST /api/v1/plugins/reload 可热重载全部非系统插件,新增/修改 on_cronjob 后需重载才生效

二者结合场景

  • 监控报警 → Webhook → 插件 → 立即推送 + 创建临时 CronJob 多波次提醒
    • 插件在 on_webhook 处理时调 POST /api/v1/cronjobs 建一个每隔 10 分钟触发的任务
    • CronJob 到点后通过 on_cronjob 回调触发插件,插件自行检查报警状态并推送提醒

Webhook 和 CronJob 都不经过 Agent,全部由插件处理,省 token 且逻辑清晰。