跳到主要内容

架构与设计(项目细节)

本文档整合 JuanNiang-Neo 的架构、调用栈、EventLoop 与插件系统,作为二次开发与运维理解的核心参考。

一、架构​

概述​

JuanNiang-Neo 是基于 OneBot11 协议的 LLM QQ 聊天 Agent 系统(红岩网校吉祥物"卷娘")。核心由 LLM 驱动的对话 Agent(HagoCenter,聚合 Provider / MCP / Memory / Prompt / Session / Skill / Tool)与 OneBot11 反向 WebSocket 适配器组成,基于 Eino ADK 框架构建 ChatModelAgent,工具调用在 ReAct 循环内同步执行。事件流经五阶段管线:幂等去重 → 系统级群管理检测 → Plugin 拦截 → 消息过滤 → 回复策略检查 → 异步派发 Agent,每个聊天区域由 ConcurrencyManager 控制最多 8 个 Agent goroutine 并发。项目还包含 Lua 插件引擎、Vue 3 管理面板,以及 Postgres + Redis + Sandbox + T2I + RAG 等可插拔基础设施。所有持久化状态落 Postgres + Redis,配置与运行时状态均可在 Web 面板热切换。

分层架构​

模块职责​

模块包路径职责
入口cmd/server/main.go组装所有模块、启动服务、反向优雅退出(带 15s watchdog)
适配器internal/adapter/OneBot11 反向 WS 服务端 + Webhook HTTP 服务端:事件解析、API 封装、消息段构造
Agentinternal/agent/Agent 核心:HagoCenter 聚合 Provider/MCP/Memory/Prompt/Session/Skill/Tool/ACL + Eino ADK ChatModelAgent (ReAct loop) + ConcurrencyManager (每 ChatArea 8 goroutine),事件循环、CronJob、回复策略
核心库internal/core/数据模型 (GORM)、DAO Bundle、Redis 缓存、ACL
Web APIinternal/api/Hertz Web 引擎、JWT 中间件、路由、Service(121 个 handler + 根路径 /health)
插件internal/pluggin/gopher-lua 引擎:生命周期、Lua API 暴露、命令树、事件拦截
基础设施infrastructure/postgres、redis、sandbox、t2i 客户端(每个含 handler 子包,功能选项风格)
前端服务internal/web/SPAHandler 通过 Hertz NoRoute 兜底服务 web/dist
日志internal/logging/fatih/color 彩色输出 + JSON 格式化 + 调用栈 + Hub(SSE)

术语陷阱:internal/adapter.Provider=OneBot11 反向 WS 适配器;internal/agent/provider.ProviderGroup=LLM Provider 组。pluggin 是有意拼写(Lua 插件系统),不要改成 plugin。docs/guidance.md 拼成 inferstructure 是错的,真实路径是 infrastructure/。

数据模型​

共 39 个 GORM 表(见 internal/core/core.go::AutoMigrate)。

关键模型语义​

  • ChatArea:私聊/群聊最小隔离单元,是 Session / Memory / ChatRecord / ACLRule 的父级。由首条消息自动 GetOrCreate 创建,无手动创建接口。
  • ChatRecord:id 为自增 int64(其他模型多为 UUID)。Session.AppendRecord 写 Postgres 与短期记忆 Redis 写入解耦——前者为审计/检索,后者为 Agent 上下文窗口。
  • 单行配置:Onebot11Adapter/WebhookConfig/T2IConfig/SandboxConfig 固定 id=1,首次访问 DB 不存在时 InitConfig 用 OnConflict DoNothing 创建默认行。
  • ReplyStrategyConfig:无 DeletedAt 的单例,策略已收敛为仅 relevance(默认 strategy=relevance, relevance_threshold=0.5, judge_fail_policy=drop)。
  • Prompt IsSystem:启动时 EnsureSystemPrompt 幂等播种 __system_locked__,强制拼接(顺序 SystemLocked → system → personality → custom)。
  • Plugin Manifest.System:系统插件三层守卫(Manifest.System + PluginEngine.IsSystem() + Service 层 Toggle/Delete)禁删/禁停。
  • CronJob:不与 ChatArea 建外键;触发时由 cronjob.Manager 构造合成 adapter.Event{PostType:"cronjob", IsCronJob:true} 经 CronJobEvents channel 注入事件循环。
  • SkillMemory:全局技能记忆单例(id="global"),存储从对话中提取的技能/知识/黑话。Compact 时由 LLM 自动更新,写回 Postgres。
  • KnowledgeItem:SQL 知识库条目,存入时由 Agent 异步提取 Keywords(keyword_status: pending→ready/failed);对话前 buildKnowledgeContext 首选 RAG 语义检索(命中按分数注入 ≤5 条),未配置/失败/无命中降级为关键词命中 + 内容 ILIKE 匹配(LRU 50 条缓存)。

状态管理​

  • 持久化状态 → Postgres(39 张表)
  • 缓存状态 → Redis(短期记忆滑动窗口 shortterm:msgs:<areaID>、PubSub 任务结果通知、插件/Agent 任意 KV/Hash)
  • 插件数据隔离 → Cache 键以 pluggin:<name>: 前缀命名空间隔离(注意:database.query 当前未真正应用前缀,是 prefixSQL 桩)
  • 例外 → Lua 插件配置由 data/pluggins/<name>/pluggin.yaml 管理(非 DB,便于 bind-mount 跨镜像保留)
  • 可插拔服务 → T2I / Sandbox 未配置时自动返回未启用提示;启用时由 API 层 OnUpdateT2I/OnUpdateSandbox 回调热注入 HagoCenter 与 Service 共享的 *Client 指针
  • 原则 → 内存中有状态模块(Agent / Memory / Skill)最终与 DB 同步;不引入纯内存状态

HagoCenter 运行时拓扑​

HagoCenter(internal/agent/agent.go)是 Agent 运行时聚合体。Start 后并发起 2 个 goroutine:

  • EinoAgent(adk.ChatModelAgent):基于 Eino ADK 框架的 ChatModelAgent,工具调用在 ReAct 循环内同步执行,MaxIterations=20。
  • ConcurrencyManager(concurrency.go):每 ChatArea 并发控制(默认 8 goroutine),使用 buffered channel 作为信号量,超限消息排队等待。
  • CronJobManager(cronjob/cronjob.go):robfig/cron 调度(秒级),命中后构造合成事件。
  • 事件循环 4 个 select 分支:ctx.Done / Adapter.Events(断流自愈)/ webhookEvents / CronJobEvents。

Agent 子包​

子包实现说明
providerprovider.goOpenAI 兼容 /v1/chat/completions(流式 SSE)、Vision(inline base64);ProviderGroup 同类型单 Active 管理
mcpmcp.gomark3labs/mcp-go SSE 客户端;MCPGroup 聚合连接 + ListTools/CallTool(MCP 可覆盖 builtin 同名工具)
memorymemory.go + shortterm/longterm/skillmem四层记忆:短期(Redis 滑窗, 默认100条, 自动Compact) / 长期(Postgres + 内存 LRU HotArea) / 技能记忆(SkillMemory, Compact 时 LLM 自动提取) / 会话记录(Postgres 审计)
promptprompt.goPromptManager + 系统锁定提示词 EnsureSystemPrompt 幂等播种 + BuildFullContext(工具感知不拼入提示词,由 Eino tools 参数提供)
sessionsession.goSessionManager:GetOrCreate / AppendRecord(Postgres) / UpdateTokenUsage
skillskill.goSkillEngine.Match(input) 按关键词 / 正则 / priority 匹配首个激活技能
tooltool.go / builtin.goToolRegistry + 内置工具 RegisterBuiltinTools(OneBot11 / 沙箱 / T2I / vision 等),工具注册为 Eino ADK ToolNode
cronjobmanager.gorobfig/cron 调度 + 合成事件

插件 API 分组(速查)​

权限全局表 / SDK 字段函数说明
始终log (jn.log)3info/warn/error → slog
始终json (jn.json)2encode/decode
onebot11onebot11 (jn.onebot11)23消息发送(异步/同步)+ 群管理 + 信息查询 + 请求处理 + 登录/状态/版本 + read_file_base64
httphttp (jn.http)4get/post(30s 超时)+ get_async/post_async(异步回调 on_http_response)
databasedatabase (jn.database)2query/exec(共享 DB,前缀桩未生效,⚠ 权限敏感)
cachecache (jn.cache)4get/set/del/exists(pluggin:<name>: 命名空间)
t2it2i (jn.t2i)7generate/generate_url + generate_async/generate_url_async(异步回调 on_t2i_response)+ toggle/is_active/get_config
sandboxsandbox (jn.sandbox)11create/exec_shell/exec_python + create_async/exec_shell_async/exec_python_async(异步回调 on_sandbox_response)+ toggle/is_active/get_config/list/delete
agentagent (jn.agent)17配置查询 + Provider/MCP/Tool 切换 + switch_provider + compact_memory + get_current_chat_area
内置jn.command1register(path, handler, opts) 多级命令注册

详见 插件开发指南。

前端 SPA 静态服务​

实现:internal/web/web.go::SPAHandler(filepath.Rel 路径穿越防护);在 internal/api/engine/engine.go 通过 h.NoRoute(...) 注册。不嵌入二进制(web/dist 是磁盘文件,便于只换前端不重编 Go)。开发期 Vite :3000 代理 /api→:8090,Go fallback 不触发。


二、调用栈​

调用栈图以 mermaid 流程图呈现(节点含文件:行号标注),支持缩放/拖动查看。

启动流程​

优雅退出(cmd/server/main.go:287 shutdown)​

OneBot11 反向 WS 事件接收到解析​

触发链路:Adapter.events ← wsServer.events ← readLoop。事件入口 Adapter.Events() (adapter.go:124)。

OneBot11 API 调用(Agent 工具 / 插件 → WS)​

normalizeMessage(api.go:324)兼容 string / Segment / []Segment / *MessageBuilder,含 CQ 码时重新解析。

Agent 事件循环(核心路径)​

runEventLoop agent/event.go:39
└─ select {
case <-ctx.Done(): stop
case ev := <-h.Adapter.Events(): event.go:50
若 channel 关闭(适配器重启): sleep 1s 重新取 Events() (event.go:52)
ev.Admins = h.Adapter.Admins()
h.processEvent(ctx, ev)
case ev := <-webhookEvents: event.go:63 (WebhookAdapter != nil 时)
h.processEvent(ctx, ev)
case ev := <-h.CronJobEvents: event.go:69
ev.Admins = h.Adapter.Admins()
h.processEvent(ctx, ev)
}

processEvent(五阶段架构)​

handleMessage(Eino ADK 对话主流程)​

工具调用(Eino ADK ReAct 循环内同步执行)​

工具调用完全由 Eino ADK ChatModelAgent 的 ReAct 循环管理,所有工具同步执行:

已移除:BgTaskExecutor 和 DrainerAgent 已完全移除。所有工具调用(包括长时间运行的操作)均在 Eino ADK 的 ReAct 循环内同步完成。

CronJob 调度​

Web API 请求 → handler​

日志系统(自定义彩色日志)​

基于 github.com/fatih/color 的自定义日志系统,替代 log/slog。功能:彩色输出、JSON 自动格式化、WARN+ 调用栈、模块日志器、GORM SQL 日志集成。

GET /api/v1/logs/stream → svc.StreamLogs service.go:1634
├─ sse.NewWriter(c)
├─ 阶段1: LogHub.Recent() 250 条按序 WriteEvent("log")
├─ 阶段2: subscribe() 实时 WriteEvent("log", entry)
└─ 每 15s WriteKeepAlive 心跳(兼测死连)

链路追踪(Grafana Tempo)​

基于 OpenTelemetry 的事件链路追踪(internal/otelx),trace 上报 Grafana Tempo,用于查看单条消息处理全流程的瀑布图与耗时分布。

  • 每条事件一个 trace:processEvent 入口创建根 span process_event(event.go),下游各阶段均为其子 span;事件循环单 goroutine 串行,根 span 必须在事件入口新建(不复用共享 ctx)
  • 根 span 属性:post_type / message_type / group_id / user_id / message_id;消息内容默认截断 100 字符记录为 message_content(可关)
  • 子 span 清单(span 树即单条事件处理全流程):
span 名位置说明
groupmgr.detect / groupmgr.verify_rag / groupmgr.punish群管理检测关键词预查、RAG 黑白语义核实、处罚
groupmgr.review_gate发送前闸门WaitReview 等待审核终态(记录 blocked/等待毫秒)
plugin.dispatchPhase 1插件拦截(记录 consumed/skip_reply)
relevance.check相关性判断批量 LLM 判断(记录候选数/结果)
agent.handleAgent ReAct全流程最长的一段(含多轮 LLM/工具)
llm.callLLM 统一入口Agent 循环/群管理审核/相关性判断共用
tool.execute工具执行记录工具名
rag.search / rag.upsert / rag.batch / rag.deleteRAG 服务检索/写入 span(scoop 分库维度)
send.reply回复发送记录字符数(含段间延迟风控耗时)
  • 默认关闭:OTEL_EXPORTER_OTLP_ENDPOINT 为空时自动 no-op(不创建 exporter/processor,零开销、零故障影响,机器人照常运行);exporter 创建失败同样降级 no-op
环境变量说明默认
OTEL_EXPORTER_OTLP_ENDPOINTOTLP 上报地址(host:port 或 http://host:port)空 = 禁用
OTEL_SERVICE_NAME服务名(Tempo 按 service.name 过滤)juan-niang-neo
OTEL_TRACE_SAMPLE_RATIO采样率 0~1(热聊群量大可调低)1.0(全量)
OTEL_TRACE_CAPTURE_CONTENT根 span 是否记录消息内容(截断 100 字符)true

插件侧同样暴露自定义 Prometheus 指标:jn.metrics 全局表(无需权限),指标名自动加前缀 juanniang_plugin_<插件名>_<短名>,统一暴露在 /metrics(Grafana 可查)。


三、EventLoop 与事件流​

事件来源​

JuanNiang-Neo 有三类外部事件源,最终都汇入 HagoCenter.runEventLoop(internal/agent/event.go:38):

来源通道PostType备注
OneBot11 反向 WSh.Adapter.Events()message/notice/request/meta_event由 internal/adapter/server.go::readLoop 推送
Webhookh.WebhookAdapter.Events() (webhookEvents)webhook由 internal/adapter/webhook.go::handleRequest 推送;外部 HTTP POST 触发
CronJobh.CronJobEventscronjob由 agent/cronjob/manager.go::makeJobFunc 合成

EventLoop(2 个 goroutine)​

HagoCenter.Start 启动两个并发 goroutine(agent.go:334-339):

runEventLoop 的 4 个 select 分支(event.go:38-77):

  1. <-ctx.Done() → 直接退出循环
  2. <-h.Adapter.Events():OneBot11 事件。若 channel 关闭(适配器 Stop 调用了 close(events)),不会 panic——记日志、sleep 1s 重新获取 Events() 句柄并 continue(event.go:52)。这正是反向 WS 重启后事件循环自愈的关键。
  3. <-webhookEvents(仅 WebhookAdapter != nil):调用 processEvent。
  4. <-h.CronJobEvents:合成 cronjob 事件,Admins 从 adapter 重新挂回后喂 processEvent。

事件分发决策树(processEvent 五阶段)​

processEvent(event.go:81-174)采用五阶段架构:幂等去重 → 群管理检测 → Plugin 拦截 → 消息过滤 → 回复策略检查 → 异步派发 Agent。

Phase 0(幂等去重):群/私聊的 message_id 各自独立递增,key 带 message_type 前缀;WS 断线重连/多连接时 OneBot 端重复推送的同一消息直接丢弃(h.msgDedup.SeenBefore)。

Phase 0.5(系统级群管理):h.GroupMgr.Process(Go 原生,先于所有 Lua 插件)。白名单/管理员/排除群豁免 → 违禁言论检测(RAG 黑白语录语义匹配首选:黑命中处罚 / 白命中放行,均未命中送 LLM 3s 批窗口逐条判定,RAG/LLM 均不可用降级关键词兜底);图片刷屏 / +1 复读(跳过命令消息)consumed=true 拦截不进 Agent。详见下方「群管理检测闸门」章节。

checkReplyStrategyFast 恒放行:回复策略已收敛为仅 relevance(@/命令/提及名字必回由规则快路径保证),LLM 相关性判断延后到 dispatchToAgent 的 goroutine 内由 filterRelevant 执行(filterRelevant → relevanceBatchEvaluate):@/命令/提及名字 → 必回(0 次 LLM);噪音消息(纯表情/过短/仅 URL)→ 规则丢弃;其余候选合并为一次 LLM 批量判断(含图消息标注 [图片],单条候选走原分数判断)。判断结果写 Redis(related=15s 对话轮次放宽 / unrelated=30s 冷却),判断并发全局上限 4、超时可配置(relevance_timeout,默认 10s),失败按 judge_fail_policy(drop/reply)降级;群聊刷屏(1s≥5 条)时批窗口拉长到 3s 并降级为只回必回消息。

一条消息的全程(OneBot11 → 回执)​

回复发送前(finish() 闭包,sendMu 锁外)经群管理审核闸门(ReviewGate):触发消息已被判违规(black)时丢弃最终回复与已投递到当前群会话的交付消息,详见「群管理检测闸门」章节。

群管理检测闸门(Phase 0.5,系统级)​

internal/agent/groupmgr 是与定时任务/摸鱼人日历同级的系统功能,替代旧 Lua 插件 redrock_group_manager。检测闸门位于 Phase 0(幂等去重)之后、Phase 1(Lua 插件派发)之前,系统级优先于所有插件,避免与插件双重检测/重复处罚。

  • 违规类型:处罚分类统一为 ad(广告)/ sensitive(敏感)两类,三条处罚路径一致——RAG 黑名单直达取命中样本类别(样本契约 ad/sensitive);LLM 追罚按优先级取分类(RAG 命中样本类别 > 关键词类别 > 卡片);关键词兜底敏感词 → sensitive、黑/灰词 → ad(推荐卡片恒为 ad)
  • 修复:LLM 追罚路径此前因 reviewCtx 未携带 RAG 命中样本类别,纯 RAG 语义命中被判成广告;现 reviewCtx 新增 ragCategory 字段,handleRAGMatch 送审时随 ragScore/ragPhrase 一并带入批窗口,LLM 判 black 时按该分类处罚(llm.go applyVerdict)
  • Web 语录列表展示违规类型列,添加语录可选 ad/sensitive(随 RAG 双向同步)
  • 三级惩罚:撤回+警告 → 禁言(二次违规 30min)→ 踢出(失败保留并通知管理员);刷屏警告/复读触发发送配图话术(//go:embed 内嵌);复读检测跳过命令消息(/ 前缀)
  • 图片刷屏(窗口/阈值/禁言时长)、+1 复读(开关/人数)、RAG 黑白阈值(black_min_score/white_min_score)、LLM 批窗口(llm_batch_window,默认 3s)、排除群/白名单/统一提示词(llm_prompt)全部面板可配置(group_mgr_configs 单行表,保存后热重载)
  • RAG 判定不再要求硬信号:黑白语录双集合各自独立,黑命中处罚、白命中放行;RAG 服务可用但无语录命中(含知识/记忆向量干扰的命中)一律送 LLM 判定,不再误判为“RAG 不可用”降级关键词
  • 学习闭环:LLM 判 black → 黑名单语录(Postgres + RAG 双写),判 white → 白名单语录;异步 goroutine 串行写入(learnMu),幂等去重 + 每集合 2000 条上限;RAG 未配置静默跳过(rag_synced 仅真实写入成功才置 true)
  • 系统命令:/groupstats、/白名单、/豁免、/解除豁免、/取消豁免(后注册覆盖插件同名命令,仅管理员);/豁免 按群清除违规记录(不清其他群的三级惩罚阶梯),白名单为全局豁免
  • Web API:/group-mgr/*(config/phrases/samples/violations/whitelist/admins/stats/test),详见 Web API:功能模块
  • GC 功能:长期记忆 GC(默认 7 天,LongTermMemory.GCIntervalDays 面板可配)清理最近周期未召回的 5 条(PG + RAG 双删);白名单语录 GC(默认 7 天,white_gc_interval_days 面板可配)清理未命中的 5 条(PG + RAG 双删)
  • 健壮性:LLM 批量判定提示词带 <USER_TEXT> 定界符与指令忽略声明(防注入),输出格式契约由代码内提示词固定(不依赖外部 LLMPrompt 是否被改);LLM 请求失败/裁决非法时 fail-closed(有硬信号直罚,否则放行);违规计数/统计为数据库级原子自增(并发不丢);群成员管理员判断走 Adapter 带缓存查询(正 10min / 负 60s);词条软删后重建同名由部分唯一索引 + 软删行复活保障

ReviewGate:发送前审核闸门​

LLM 审核是异步的(3s 批窗口),期间 Agent 可能已完成回复——若触发消息最终被判违规,机器人回复再被撤回观感不佳。为此 Agent 回复发送前增加审核闸门(internal/agent/groupmgr/llm.go):

  • groupmgr 维护 reviewVerdict 审核终态表(message_id → black/white/none),TTL 10min 与 llmReviewed 去重表对齐清理(applyVerdict 落库:black=已判违规、white/none=放行、失败且无硬信号不记录);重启丢失视为未送审放行(撤回兜底仍在)
  • finish()(Agent 回复发送闭包,sendMu 锁外执行避免持锁等待)对群消息调用 WaitReview(ctx, groupID, userID, messageID, 5s)(event.go handleMessage):
    • blocked(已判 black)→ 丢弃回复,并 DeferredSendQueue.DropDelivery 移除已投递到当前群会话的交付消息(私聊/其他群工具消息保留)
    • pending(审核在途)→ 50ms 间隔轮询等待终态,上限 5s(ReviewGateWait:覆盖批窗口 3s + LLM 余量);超时按放行处理(撤回兜底保留)
    • 无记录 / 私聊 / 未送审(LLMReview 关闭、去重命中、重启丢失等)→ 一律放行
  • 典型延迟:Agent ReAct 循环通常比审核(3s 批窗 + LLM)慢,多数情况零等待
  • 追踪:WaitReview 内建 groupmgr.review_gate span(记录 blocked 与等待毫秒)

CronJob 注入流​

CronJob 事件只派发给 Lua 插件(on_cronjob 回调),不进入 LLM Agent、不经过回复策略与 ACL。CronJob 的 message/message_type/target_id 字段仅作为 event.raw_message 等上下文透传给插件。

API 侧增删改后 Manager.Reload() 同步调度器(service.go:1733/1768/1808/1822),无需重启进程。详见 webhook-cronjob.md。

Webhook 注入流​

Webhook 不走 Agent LLM 路径,是对外暴露给 Lua 插件的事件钩子(如 GitHub push 通知触发群发)。详见 webhook-cronjob.md。

关键不变量​

  • Adapter 重启不会击穿事件循环:Adapter.Stop 会 close(events) 并置 nil,Start 时若 events==nil 重建(adapter.go:36-62);EventLoop 分支2 检测关闭后 sleep 1s 重新取句柄(event.go:52)。
  • Redis 与 Postgres 解耦:短期记忆写 Redis 是为了 LLM 上下文窗口,Session.AppendRecord 写 Postgres 是为了审计检索;任一失败不影响另一路。
  • Admins 绕过 ACL:Admins 列表(来自 Onebot11Adapter.AdminQQNumbers)从 adapter 透传到每条 Event;handleMessage 中 isAdmin(userID, admins) || ACL.CheckChat(...) 决定消息是否进入 Agent。ACL 现仅管理聊天黑名单(仅 deny 规则生效,allow 规则不再生效)。
  • __NO_REPLY__ 静默:LLM 可主动输出 __NO_REPLY__ 让系统不发任何 QQ 消息(避免群聊噪音)。
  • SystemLocked 强制拼接:每次对话系统提示词必含 __system_locked__ 内容,前端不能停用,保证 LLM 知道能用 T2I 富文本、分消息段、权限层级等行为约束。
  • 工具调用全同步:所有工具调用(包括长时间运行的操作)均在 Eino ADK ReAct 循环内同步完成,无后台任务分流。BgTaskExecutor 和 DrainerAgent 已完全移除。
  • 每 ChatArea 并发控制:ConcurrencyManager 通过 buffered channel 信号量控制每个 ChatArea 最多 8 个 Agent goroutine 并发(默认值可配置),超限消息排队等待。

四、插件系统​

概述​

JuanNiang-Neo 的 Lua 插件系统基于 gopher-lua(Go-Lua 绑定),允许用户通过 Lua 脚本扩展机器人功能。插件可以:

  • 拦截 OneBot11 消息事件 / Webhook 事件
  • 注册多级斜杠命令(如 /system provider switch)
  • 调用 OneBot11 协议接口、HTTP、数据库、Redis 缓存、T2I、Sandbox、Agent 操作接口
  • 通过内嵌 Lua SDK(jn.lua,带 LuaCATS 注解)获得 IDE 类型提示

拼写约定:pluggin(双 g 单 n)是有意拼写:模块路径 internal/pluggin、配置文件 pluggin.yaml、插件目录 data/pluggins。请勿"修正"为 plugin。开发完整指南见 插件开发指南。

组件结构​

生命周期​

代码位置:

系统插件三层守卫(Manifest.System + PluginEngine.IsSystem() + Service 层 Toggle/Delete),确保 system 插件不可删/停。

Manifest(pluggin.yaml)​

字段类型说明
ppidstring稳定 UUID(空时自动生成并写回)
namestring插件名(=目录名,作为 id)
versionstring版本,默认 "1.0.0"
authorstring作者
descriptionstring描述
entrystringLua 入口,默认 main.lua
permissionsstring[]申请的权限(onebot11/http/database/cache/t2i/sandbox/agent)
systembool系统插件(undeletable / unstoppable)
enabledbool是否启用(控制是否在 LoadAll 时加载)

示例(系统插件 internal/pluggin/systemplugin/pluggin.yaml):

ppid: 6563c9c3-1072-4168-8bb3-62db4c11990b
name: system
version: "1.0.0"
author: JuanNiang-Neo
description: "系统插件,封装 Agent/Provider/MCP/Tool/T2I/Sandbox/Session 管理命令"
entry: main.lua
system: true
enabled: true
permissions:
- onebot11
- agent
- t2i
- sandbox

事件回调​

插件通过两个全局 Lua 函数拦截事件(PCall,2 返回值 (consumed bool, reply string)):

回调触发权限过滤
on_message(event)收到 OneBot11 / 开头走 commands.Dispatch;否则对每条有 onebot11 权限的插件调用onebot11
on_webhook(event)Webhook 事件到达(不走 LLM Agent)webhook

EventData 结构(传给 Lua 的 event table):

type EventData struct {
PostType string
MessageType string
UserID int64
GroupID int64
RawMessage string
Admins []string
Webhook map[string]any
}

OnMessage 决策(pluggin.go:397-441):

命令树(CommandRegistry)​

internal/pluggin/command.go 实现多级命令派发:

CommandNode {Name, Opts{Description,Usage}, Handler, PluginName, Children map}
长前缀匹配, 最长匹配节点带 Handler 时执行
未命中 Handler 但停在非 root → 返回该节点子命令列表
Dispatch(raw, event): 按 "/" 分词遍历, 取最后带 handler 的节点, 调用 handler(剩余 args, event)

命令 handler 签名(Go 侧):

type CommandHandler = func(args []string, event EventData) (consumed bool, reply string, err error)

Lua 侧通过 SDK jn.command.register(path, handlerFn, opts) 注册,path 可为 string 或 table(多级),handler 接收 (argsTable, eventTable) 返回 (consumedBool, replyString)。

内置 /help 在 registerBuiltinCommands() 注册(plugin=system),列出所有顶级命令;/help <cmd> [sub...] 列出子命令与用法。

插件卸载时 UnregisterPlugin(name) 递归清理该插件注册的所有命令并修剪空叶子。

注入的 Lua 全局表​

按 permissions 字段 gated,由 injectBaseAPI(pluggin.go:973)注入。完整签名见 插件开发指南。

全局表权限说明
log始终info/warn/error → slog [plugin:<name>] 前缀
json始终encode/decode
onebot11onebot11OneBot11 API(发送/群管理/请求处理/合并转发/引用回复等)
httphttpget/post + 异步,30s 超时真实 HTTP(可选 http/socks4/socks5 代理)
databasedatabasequery/exec(共享 DB;prefixSQL 桩未生效,⚠ 任意 SQL)
cachecacheget/set/del/exists(pluggin:<name>: 前缀命名空间)
t2it2igenerate / generate_url + toggle/is_active/get_config
sandboxsandboxcreate/exec_shell/exec_python/list/delete + toggle/is_active/get_config
ragragadd/add_async/search/search_async(对接 RAG-Service)
agentagent配置查询 + Provider/MCP/Tool 切换 + switch_provider + compact_memory
jn.command内置命令注册

Lua SDK(jn.lua)​

由 Go 二进制内嵌(//go:embed sdk/jn.lua,pluggin.go:1543),启动时 ensureEmbeddedAssets 落盘到 data/pluggins/sdk/jn.lua(每次覆盖以匹配二进制版本)。injectSDK 把 <basePath>/sdk/?.lua 追加到 LState 的 package.path,使 require("jn") 可用。

SDK 仅捕获 Go 注入的全局表作为模块字段(jn.log = log 等),不引入额外行为;带 LuaCATS 注解,sumneko lua-language-server 可提供完整代码提示。

local jn = require("jn")
jn.log.info("插件启动")
local id, err = jn.t2i.generate("<h1>Hello</h1>")

数据隔离​

  • Cache:所有 cache.* 操作自动加 pluggin:<name>: 前缀,插件间键不冲突,且无法读写 Agent 的 session:/shortterm: 前缀。
  • Database:database.query/exec 跑在共享库上,prefixSQL 桩当前未应用 pluggin_<name>_ 前缀;请谨慎授 database 权限(⚠ 任意 SQL,可在插件侧加自己的表前缀)。
  • 插件配置:data/pluggins/<name>/pluggin.yaml 在磁盘,不进 DB(除非 DB plugins 表存元数据镜像)。

安全建议​

  • 仅对受信插件授予 database 权限
  • 对从社区上传的 ZIP 插件先审阅 Lua 源码再 Deploy
  • 系统插件 system 提供 /system provider switch、/system memory compact 等管理命令,需要 admin 操作(受 ACL 与 OneBot11 Adapters 的 Admins 双重保护)