@zaofan/dsh-qqbot 1.9.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +646 -554
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/features/ext-capabilities.d.ts +9 -0
- package/dist/features/ext-capabilities.d.ts.map +1 -1
- package/dist/features/ext-capabilities.js +2 -0
- package/dist/features/ext-capabilities.js.map +1 -1
- package/dist/features/ext-tool-interaction.d.ts.map +1 -1
- package/dist/features/ext-tool-interaction.js +37 -5
- package/dist/features/ext-tool-interaction.js.map +1 -1
- package/dist/features/extension-store.d.ts.map +1 -1
- package/dist/features/extension-store.js +8 -0
- package/dist/features/extension-store.js.map +1 -1
- package/dist/gateway/middleware-setup.d.ts.map +1 -1
- package/dist/gateway/middleware-setup.js +6 -0
- package/dist/gateway/middleware-setup.js.map +1 -1
- package/docs/extension-contract.md +187 -0
- package/examples/ext-commands//345/255/220/344/274/232/350/257/235.mjs +598 -0
- package/examples/ext-hooks/session-watch-hook.mjs +53 -119
- package/examples/ext-tools/lib/session_watch_lib.mjs +767 -350
- package/examples/ext-tools/qq_send.mjs +201 -138
- package/examples/ext-tools/session_children.mjs +99 -0
- package/examples/ext-tools/session_create.mjs +219 -201
- package/examples/ext-tools/session_watch.mjs +139 -93
- package/examples/ext-tools/sessions_list.mjs +180 -180
- package/package.json +142 -141
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# 扩展契约(写 dsh-qqbot 扩展前,先读这一份)
|
|
2
|
+
|
|
3
|
+
> 适用版本:**v1.9.2+**(本文写的就是当前真实实现,不是设想)
|
|
4
|
+
> 面向:给 QQ 机器人写扩展的用户 / 让 AI 帮你写扩展的人
|
|
5
|
+
> 配套:用户手册 `README.md` §用户扩展、`docs/USER-GUIDE.md` §统一能力包、入站中间件详解 `examples/ext-inbound/README.md`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 0. 一句话
|
|
10
|
+
|
|
11
|
+
**五套扩展点,一套能力包。** 你在哪一套里写代码,拿到的能力面都一样 ——
|
|
12
|
+
不用记五套 API,也不用"自己接生"发消息/存数据/调官方接口这些事。
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
┌─────────────── 统一能力包(makeExtCaps)───────────────┐
|
|
16
|
+
tools/ env ──┤ text markdown markdownCard media image voice video file│
|
|
17
|
+
commands/ ctx ──┤ at api() appId/appSecret store registerInteractionCard │
|
|
18
|
+
botplay/ env ──┤ appendSilent appendWake canAppendSilent │
|
|
19
|
+
hooks/ env ──┤ user owners peer log kernel restart │
|
|
20
|
+
inbound/ ctx ──┤ + inbound: { annihilate rewrite forceWake decide │
|
|
21
|
+
│ silentInto mute* message kv } ← 只有它 │
|
|
22
|
+
└───────────────────────────────────────────────────────┘
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`inbound` 那一组为什么只有它有值:**只有它跑在"一条消息正在进站"的时刻**,
|
|
26
|
+
"消灭/改写/覆盖 @ 判定"这些动作在别处没有对象。别的四套拿到的 `caps.inbound` 是
|
|
27
|
+
`undefined` —— **这是语义,不是缺失**。
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. 五套扩展点:目录、时机、拿到什么
|
|
32
|
+
|
|
33
|
+
全部位于 **`{数据根}/.qqbot-extensions/`** 下(数据根 = 工作目录里的 `dshqqbot/`):
|
|
34
|
+
|
|
35
|
+
| 目录 | 什么时候跑 | 你的导出 | 你拿到 |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| `tools/*.mjs` | LLM 调这个工具时 | `export default { name, description, inputSchema, run(args, env) }` | `env`(能力包平铺) |
|
|
38
|
+
| `commands/*.mjs` | 用户敲 `/你的命令` 时 | `{ name, handler(ctx) }` | handler 的 `ctx`(能力包平铺) |
|
|
39
|
+
| `botplay/*.mjs` | 卡片按钮被点时 | `{ name, events: {...} }` | `env` |
|
|
40
|
+
| `hooks/*.mjs` | **插件启动时跑一次** | `export default async (env) => {}` | `env`(含 `env.caps`) |
|
|
41
|
+
| `inbound/*.mjs` | **每一条入站消息** | `export async function onInbound(ctx,next)` / `onDecide(ctx,next)`(或 `export default`) | 挂点的 `ctx` |
|
|
42
|
+
|
|
43
|
+
扫描规则(五套一致):**只看目录下的直接子文件**,`.js` / `.mjs` / `.cjs`,按文件名排序;
|
|
44
|
+
子目录不扫描(可放私有模块)。改完文件调 `tools_reload`(或 `/tools-reload`)**即时生效**;
|
|
45
|
+
扩展命令需要重启宿主。
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. 统一能力包(五套都有)
|
|
50
|
+
|
|
51
|
+
成员**平铺**在你的 `env` / `ctx` 上,**同时**在命名空间 `env.caps` / `ctx.caps` 里也有一份。
|
|
52
|
+
|
|
53
|
+
| 能力 | 用法 | 说明 |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| 发纯文本 | `await env.text('hi')` | |
|
|
56
|
+
| 发 markdown | `await env.markdown('# 标题')` | 富文本卡片通道,**自带纯文本降级** |
|
|
57
|
+
| 发 markdown+按钮 | `await env.markdownCard(md, keyboard)` | 自己拼 keyboard(≤5 行) |
|
|
58
|
+
| 发富媒体 | `await env.image(src)` / `voice` / `video` / `file` | `src` 可传 URL 字符串或 `{url}` / `{localPath}` |
|
|
59
|
+
| 任意媒体 | `await env.media('file', src)` | kind = image / voice / video / file |
|
|
60
|
+
| @人 | `env.at(openid)` | 返回 `<@openid>` 片段,拼进文本 |
|
|
61
|
+
| 官方 API | `await env.api('/v2/users/@me')` | **自带 token 与 2h 缓存**;返回 `{ok,data}` 或 `{ok:false,err:{code,human}}` |
|
|
62
|
+
| 钥匙 | `env.appId` / `env.appSecret` | 想自己换 token 调任意接口时用 |
|
|
63
|
+
| 持久化 | `env.store.load()` / `env.store.save(obj)` | 落 `…/data/<你的文件名>.json`,**升级插件不丢** |
|
|
64
|
+
| **进上下文(静默)** | `await env.appendSilent('...')` | **只落上下文、不唤醒**(详见 §4 安全红线) |
|
|
65
|
+
| 进上下文(唤醒) | `await env.appendWake('...')` | 走正常回合唤醒一轮 AI(耗 token) |
|
|
66
|
+
| 身份 | `env.user.openid/name/isOwner`、`env.owners`、`env.peer.scope/peerId` | |
|
|
67
|
+
| 日志 | `env.log(...)` | 落插件 logger |
|
|
68
|
+
| 内核句柄 | `env.kernel`(工具侧也叫 `env.ctx`) | **全权限"遥控器"**:`ctx.get('服务名')` / `ctx.on(...)` / `ctx.webServer.register(...)`。⚠️ 形状跟随 dsh 版本;`register` 返回的 disposer 要自己收尾 |
|
|
69
|
+
| 宿主自重启 | `await env.restart()` | 复用内置 `/bot-restart`(自动识别启动命令、强补 `--no-open`)。默认**仅主人**可触发;要放开写 `env.restart({ requireOwner: false })` |
|
|
70
|
+
| 回调按钮卡 | `env.registerInteractionCard({cardId, buttonIds, expireAt})` | 发**带回调按钮**的卡片必须先登记,否则点了没人认 |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 3. 入站操控:`caps.inbound`(只有入站扩展有)
|
|
75
|
+
|
|
76
|
+
入站扩展的两个挂点,**链上位置决定你能做什么** —— 这是最重要的一张表:
|
|
77
|
+
|
|
78
|
+
| | 早(`onInbound`) | 晚(`onDecide` / `export default`) |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| 链上位置 | 引用解析**之后**、**记群历史之前** | @门控**之后** |
|
|
81
|
+
| 读得到 | `ctx.message` / `ctx.state.quote` / `ctx.platform.mentioned` | 以上全部 **+ `ctx.state.mention`**(门控判定结果) |
|
|
82
|
+
| `annihilate` 消灭 | ✅ 真消灭(不进历史/不进上下文/不占冷却) | ❌ 会被拒绝 + 告警(历史已记) |
|
|
83
|
+
| `rewrite` 改写 | ✅ **连群历史一起改** | ⚠️ 只改后续上下文,历史已是原文 |
|
|
84
|
+
| `forceWake` 强制入站 | ✅ | ✅ |
|
|
85
|
+
| `decide` 覆盖 @ 判定 | ❌(判定还没写出来) | ✅ |
|
|
86
|
+
| `silentInto` 静默进上下文 | ✅ | ✅ |
|
|
87
|
+
| 禁言 / kv | ✅ | ✅ |
|
|
88
|
+
|
|
89
|
+
动作清单:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
ctx.caps.inbound.annihilate({reason}) // 不调 next():这条不进群历史、不进上下文、不占冷却
|
|
93
|
+
ctx.caps.inbound.rewrite({text,reason}) // 换掉正文(早挂点连历史一起换)
|
|
94
|
+
ctx.caps.inbound.forceWake({reason}) // 跳过 @门控 / 价值评分 / 群冷却
|
|
95
|
+
// ⚠️ 只是"必进",不代表她一定会说话
|
|
96
|
+
ctx.caps.inbound.decide({mention:'ignore'|'force', reason}) // 覆盖 @ 判定
|
|
97
|
+
ctx.caps.inbound.silentInto(text?) // 内容进 AI 上下文、不唤醒(走宿主 agent.inject)
|
|
98
|
+
ctx.caps.inbound.muteUser/unmuteUser/isMuted/mutedUsers // 用户维禁言(带期限、持久化)
|
|
99
|
+
ctx.caps.inbound.message.{text,sender,senderName,scope,groupId,isOwner,mentions,quoted,platform}
|
|
100
|
+
ctx.caps.inbound.kv.get/set // 该扩展自己的持久化
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
同一套动作在 `ctx` 上**也平铺了一份**(`ctx.annihilate` / `ctx.silentInto` / `ctx.muteUser` …),
|
|
104
|
+
两者是同一套内核的两个入口,随便用哪个。
|
|
105
|
+
|
|
106
|
+
### 示例:引用"子会话转发消息"不算 @
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// {数据根}/.qqbot-extensions/inbound/no-wake-on-child-quote.mjs
|
|
110
|
+
export default async function (ctx, next) {
|
|
111
|
+
if (ctx.message.kind === 'group') {
|
|
112
|
+
const quoted = ctx.state?.quote?.text ?? '';
|
|
113
|
+
if (/【子会话(?:\s|\u3000|$)/.test(quoted) && ctx.state?.mention?.wasMentioned === true) {
|
|
114
|
+
ctx.decide({ mention: 'ignore', reason: '引用的是子会话转发消息' });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return next();
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 示例:广告直接消灭 + 静默留档
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
export async function onInbound(ctx, next) {
|
|
125
|
+
if (isAd(ctx.text)) {
|
|
126
|
+
ctx.silentInto(); // 内容仍进她上下文(下次跑就能看到)
|
|
127
|
+
ctx.annihilate({ reason: '广告' });
|
|
128
|
+
return; // ★ 不调 next() = 不进历史/不占冷却/不叫醒
|
|
129
|
+
}
|
|
130
|
+
return next();
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 4. 四条安全红线
|
|
137
|
+
|
|
138
|
+
1. **`appendSilent` 的人设红线**:dsh 要求"人设"是会话上下文的**第一个节点**。
|
|
139
|
+
全新会话(还没跑过任何回合)里静默追加会**把整份会话日志写废**,用户会看到「历史加载失败」。
|
|
140
|
+
所以此能力**内置守卫**:不安全时直接返回 `false`、**绝不写入**。想在这种会话里送内容,用 `appendWake`。
|
|
141
|
+
2. **主人白名单最高优先**:主人(`config.groupAdmin.owners`)的消息**不可被 `annihilate` 消灭、不可被禁言** ——
|
|
142
|
+
扩展这么调会被拒 + 告警 + 落审计,消息照常放行。
|
|
143
|
+
(`silentInto` 不受此限:内容仍进上下文,不存在"被吞"风险;但它照旧落审计、计入熔断。)
|
|
144
|
+
3. **熔断**:单个扩展的破坏性动作占比过高(>50%,样本 ≥50)或短时爆发(10 秒内 ≥20 次),
|
|
145
|
+
插件会**自动停掉它的破坏性动作**并告警 —— 消息改回放行,不会因为你的 bug 静默吞群消息。
|
|
146
|
+
4. **优雅降级**:任何能力拿不到依赖都返回 `false` / 结构化错误,**绝不抛**;
|
|
147
|
+
你写的扩展抛错时,插件**只告警 + 继续 `next()`** —— 用户脚本坏了永远不会把消息吞掉。
|
|
148
|
+
|
|
149
|
+
另外:所有动作都会落 **`{数据根}/.qqbot-extensions/inbound/.state/audit.jsonl`**(带轮转),
|
|
150
|
+
禁言名单与 kv 落同目录的 `<你的文件名>.json`,**随时可回看、可撤销**。
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 5. 三条容易踩的坑
|
|
155
|
+
|
|
156
|
+
1. **`text` 同名冲突(只有入站侧)**:入站 `ctx.text` = **当前这条消息的正文**(读);
|
|
157
|
+
其余四套 `env.text('...')` = **发一条纯文本**。入站侧要发文本请走 `ctx.caps.text('...')`。
|
|
158
|
+
2. **入站是热路径**:两个挂点**每条消息都跑一遍**。别在这儿发消息、打接口、做长任务 ——
|
|
159
|
+
`markdown` 会真发群、`appendWake` 会唤醒一轮 AI、`api()` 走网络。
|
|
160
|
+
3. **能力包"只补不覆盖"**:插件往你的 `ctx` 上合并能力时,判据是"**值为 undefined 才补**"
|
|
161
|
+
(中间件 ctx 上不少键存在但值是 undefined)。你自己已经定义的同名成员不会被抢走。
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 6. 我能写哪一套?怎么选
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
有人敲命令才做事 → commands/
|
|
169
|
+
LLM 该主动调用才做事 → tools/
|
|
170
|
+
要等人点卡片按钮 → botplay/
|
|
171
|
+
启动就得跑一次(恢复任务/起定时器/预取) → hooks/
|
|
172
|
+
要影响"每条消息进不进、长什么样、叫不叫醒她" → inbound/
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
写之前建议先读 `examples/` 下对应目录的示例(插件首次启动会**自动铺一份**到你的扩展目录;
|
|
176
|
+
`examples/ext-inbound/` 里只有 `no-wake-on-child-quote.mjs` 会自动铺,
|
|
177
|
+
`class-advisor.mjs` 那种**会真删消息/真封人**的示例要自己拷)。
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 7. 变更记录(契约相关)
|
|
182
|
+
|
|
183
|
+
- **v1.9.2**:入站扩展(第五套)**能力面对齐** —— 直接复用统一能力包(此前只有一套私有小 ctx,
|
|
184
|
+
`store` / `api()` / `markdown` / `appendSilent` 等一项都没有);**入站操控那一组也进了包**(`caps.inbound`)。
|
|
185
|
+
- **v1.9.0**:新增第五套扩展点 `inbound/`(早/晚双挂点 + 四个动作 + 四条保命件)。
|
|
186
|
+
- **v1.8.9**:新增第四套扩展点 `hooks/`(启动钩子)。
|
|
187
|
+
- **v1.7.0**:三套(命令/工具/卡片)能力面统一为「统一能力包」。
|