插件开发(给 LLM)
最小模板(照抄)
# plugins/<plugin_id>/main.py,plugin_id 全小写下划线
__plugin_meta__ = {"name": "中文名", "version": "1.0.0", "author": "…",
"desc": "一句话", "priority": 50} # 越小越先,守卫类 2-10
def register(ctx):
ctx.command("/签到", handle, alias="/sign", description="…",
require_admin=False, require_superuser=False,
require_perm="myplugin.sign") # 权限节点,可选
ctx.on_raw_message(on_raw) # (raw: dict, bot_name) -> True=接管全部后续
ctx.task("*/10 * * * *", job)
ctx.on("message", listener) # 返回 True=已处理,路由终止
ctx.hook("action.after", fn)
async def handle(event, match): # match.group(1)=命令后参数
await ctx.asend_msg(user_id=event.user_id, group_id=event.group_id, message="文本")
def unregister(): ... # 可选约定:sync 用同步方法,async 用 a 前缀(asend_msg/aapi);数据只写 ctx.get_data_dir()(= data/plugins_dat/<插件>/),不写代码目录;重复注册自动去重。
ctx 常用面
| 类别 | 成员 |
|---|---|
| 发送 | send_msg/asend_msg(user_id, group_id, message)、api/aapi(action, bot=None, **params)、actions |
| 会话 | await wait_for(event, prompt, timeout)、create_session(event)(session 插件) |
| 数据 | db_query/db_execute(sql, params)(及 db_query_one/db_insert/create_table 与异步 *_async 版;SQLite/MySQL 双方言)、get_config(key, default) |
| 权限 | has_perm(user_id, node)、audit_log(action) |
| 其它 | on/emit/aemit、hook/unhook、register_api(path, view)、webui(...)、log(msg) |
middleware/transform不在主框架PluginContext里。 这两个 surface 只属于--iipc模式下的framework/iipc/py_sdk(Go 内核 + Python SDK)。用默认python main.py启动时调用ctx.middleware(...)/ctx.transform(...)会直接AttributeError。下面两节的示例同样只在--iipc模式可用。
event 字段:message(提取后文本)、raw_message、user_id/group_id/is_group、sender、 role、is_admin、segments、reply_id(被回复消息 ID,属性)、stop_event()(停止传播)。
LLM 子系统(可选)
core_plugins.llm_load(装载器)+ plugins/llm_core(对话核心)。后者释放自 zip 载荷, 运行时目录内的改动会被 manifest 抹平,改代码请改 core_plugins/llm_load/src/llm_core/ 并 python tools/build_llm_payload.py --write。
- 取服务:
fw.services.get('llm_core') - 注册函数:
@svc.tool(description=...)或svc.register_tool(FunctionTool(...)) - 注册模型提供商:
svc.register_provider(Provider 子类实例)(provider id 要各不相同) - 函数契约:描述写给模型看;参数是 JSON Schema(也可按类型注解推导);返回 str 回灌、 返回 None 不回灌;参数校验失败会回灌错误信息让模型自纠
- 详见 接入大模型
IIPC 链路下写插件(python main.py --iipc)
插件写法基本照抄本页即可(register(ctx)、装饰器、ctx.*、Event 字段都 1:1 对齐), 但有几条只对 IIPC 成立的差异:
- handler 里裸用
ctx可以——IIPC 已对齐module.ctx = ctx注入(原框架在loader/base.py)。 - 内建服务型插件跑不了:依赖
ctx._framework.services的(如core_plugins/scheduler、session)是 host 进程内 API,IIPC 插件在独立进程没有_framework。能对口的只有plugins/*这类用户态插件。 call_async只给同步 handler 用:在 async handler 里fut.result()会阻塞插件 自己的事件循环,必然超时。- 插件进程的 stdout 不进内核日志:插件内异常要自己落文件才看得到(排查时最容易踩)。
- 端口 / DB / 管理员等走
config.yaml的iipc:段,不写死在插件里。
未覆盖:Event.perms/perm_groups/primary_group、WebUI 等价物、真 OneBot 对接。 详见 IIPC 内核链路。
跨插件访问
sys.modules.get("plugin_<id>")(官方插件 core_plugin_<name>;html_assembler 兼容别名 plugin_html_assembler)。拿不到返回 None,不要 [] 直取;依赖顺序时订阅 system.plugin.loaded。
多阶段中间件与全局变换节点
这两类是"自由度"的核心扩展点:把"命令 + 消息"两阶段模型升级为可编排的管线, 任意插件都能在事件生命周期的节点插入逻辑,不必改写框架或整体接管消息。
进程内中间件 ctx.middleware(stage, priority, handler)
在本插件处理链路的节点拦截 / 变换 / 短路。四个阶段:
pre:事件入站后、命令匹配前。handler(event, ctx),可就地改写event(脱敏 / 翻译 / 补全字段),返回False中止后续中间件与分发。post:本事件分发完成后。handler(event, ctx),事后统计 / 审计 / 落库。send:每条出站消息 flush 前。handler(params, ctx),params为{"message","user_id","group_id",...};返回None/False拦截该条,返回dict改写后发出。error:任意 handler 抛异常时。handler(event, exc, ctx),中央异常处理 / 告警。
def register(ctx):
@ctx.middleware("pre", priority=10)
def normalize(event, ctx):
event.message = event.message.strip() # 下游命令看到的已是去空格文本
@ctx.middleware("send", priority=10)
def sign(params, ctx):
p = dict(params)
p["message"] = (p.get("message") or "") + " — by 我的插件"
return p零成本:未注册任何中间件时,分发路径与以前完全一致。
全局变换节点 ctx.transform(priority, handler)
内核在把任意事件扇出给所有订阅者之前,按 priority 升序调用各变换节点,把插件返回的 event.data(dict)作为新的事件数据往下走;返回 None 则全局丢弃该事件。等价于在内核级 插入一个"变换节点"——所有订阅者(命令、on 处理器)都看到变换后的事件,而无需各自实现一遍。
def register(ctx):
@ctx.transform(priority=10)
def normalize(params):
params = dict(params or {})
params["message"] = (params.get("message") or "").strip()
return params # 返回 None 则丢弃该事件适用:协议归一、字段补全、全局脱敏、消息改写。注意:变换节点本身不必订阅该事件类型 (它由内核经 RPC 回调,不靠扇出),且不要在变换里又把同类型事件重新 emit,避免回环。
坑
- 别绕过框架直跑 main.py(模块名/ctx/DB 全靠加载器)。
- raw handler 返回 True 会吞掉该消息的命令匹配,谨慎。
- @机器人 前缀已在 Event 构造时剥离,
event.message是干净文本。 - SQLite 只适合小环境,多群上 MySQL(
config.yaml → database.type)。 - SQL 用参数占位符;批量写参考
stats_writer的队列模式,别在热路径直查库。