@zaofan/dsh-qqbot 1.10.9 → 1.10.11

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.
Files changed (69) hide show
  1. package/README.md +86 -2
  2. package/README_EN.md +31 -0
  3. package/client/qqbot-settings.js +6313 -6196
  4. package/dist/channel-tools.d.ts +0 -12
  5. package/dist/channel-tools.d.ts.map +1 -1
  6. package/dist/channel-tools.js +202 -20
  7. package/dist/channel-tools.js.map +1 -1
  8. package/dist/config.d.ts +18 -0
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +11 -0
  11. package/dist/config.js.map +1 -1
  12. package/dist/features/botplay.d.ts.map +1 -1
  13. package/dist/features/botplay.js +6 -2
  14. package/dist/features/botplay.js.map +1 -1
  15. package/dist/features/ext-capabilities.d.ts +2 -0
  16. package/dist/features/ext-capabilities.d.ts.map +1 -1
  17. package/dist/features/ext-capabilities.js +6 -0
  18. package/dist/features/ext-capabilities.js.map +1 -1
  19. package/dist/features/ext-outbound.d.ts +238 -0
  20. package/dist/features/ext-outbound.d.ts.map +1 -0
  21. package/dist/features/ext-outbound.js +518 -0
  22. package/dist/features/ext-outbound.js.map +1 -0
  23. package/dist/features/ext-reload.d.ts +19 -0
  24. package/dist/features/ext-reload.d.ts.map +1 -1
  25. package/dist/features/ext-reload.js +40 -0
  26. package/dist/features/ext-reload.js.map +1 -1
  27. package/dist/features/msg-history.d.ts +78 -0
  28. package/dist/features/msg-history.d.ts.map +1 -0
  29. package/dist/features/msg-history.js +312 -0
  30. package/dist/features/msg-history.js.map +1 -0
  31. package/dist/features/msg-recent.d.ts +72 -0
  32. package/dist/features/msg-recent.d.ts.map +1 -0
  33. package/dist/features/msg-recent.js +131 -0
  34. package/dist/features/msg-recent.js.map +1 -0
  35. package/dist/features/qq-user-questions.d.ts +6 -0
  36. package/dist/features/qq-user-questions.d.ts.map +1 -1
  37. package/dist/features/qq-user-questions.js +17 -3
  38. package/dist/features/qq-user-questions.js.map +1 -1
  39. package/dist/gateway/bootstrap.d.ts.map +1 -1
  40. package/dist/gateway/bootstrap.js +83 -4
  41. package/dist/gateway/bootstrap.js.map +1 -1
  42. package/dist/gateway/middleware-setup.d.ts.map +1 -1
  43. package/dist/gateway/middleware-setup.js +55 -0
  44. package/dist/gateway/middleware-setup.js.map +1 -1
  45. package/dist/session/session-manager.d.ts.map +1 -1
  46. package/dist/session/session-manager.js +5 -2
  47. package/dist/session/session-manager.js.map +1 -1
  48. package/dist/transport/inbound.d.ts.map +1 -1
  49. package/dist/transport/inbound.js +90 -4
  50. package/dist/transport/inbound.js.map +1 -1
  51. package/dist/transport/msg-content-cache.d.ts +50 -0
  52. package/dist/transport/msg-content-cache.d.ts.map +1 -1
  53. package/dist/transport/msg-content-cache.js +58 -14
  54. package/dist/transport/msg-content-cache.js.map +1 -1
  55. package/dist/transport/outbound-buffer.d.ts +31 -1
  56. package/dist/transport/outbound-buffer.d.ts.map +1 -1
  57. package/dist/transport/outbound-buffer.js +49 -1
  58. package/dist/transport/outbound-buffer.js.map +1 -1
  59. package/dist/transport/outbound.d.ts.map +1 -1
  60. package/dist/transport/outbound.js +20 -0
  61. package/dist/transport/outbound.js.map +1 -1
  62. package/examples/ext-commands//344/274/232/350/257/235/345/210/227/350/241/250.mjs +756 -742
  63. package/examples/ext-commands//345/224/244/351/206/222.mjs +71 -0
  64. package/examples/ext-commands//345/255/220/344/274/232/350/257/235.mjs +16 -5
  65. package/examples/ext-outbound/README.md +139 -0
  66. package/examples/ext-outbound/sticker-semantic.mjs +524 -0
  67. package/examples/ext-tools/sticker_auto.mjs +260 -0
  68. package/examples/ext-tools/vote.mjs +534 -323
  69. package/package.json +144 -144
@@ -96,29 +96,17 @@ export declare function syncContextTools(agentCtx: unknown, shouldHave: boolean,
96
96
  * ⚠️ 可见性不在这里决定 —— 交给 {@link restrictContextTools} 按会话剪枝。
97
97
  */
98
98
  /**
99
-
100
99
  * 把 `expose: 'all'` 的扩展工具注册到**插件根作用域**(2026-10-10 主人要的"暴露给全部会话")。
101
-
102
100
  *
103
-
104
101
  * 为什么必须挂在这一层:扩展工具平时由 session-manager 在**每个 QQ 会话**上用 `agentCtx` 注册,
105
-
106
102
  * 而 web / 终端等**非 QQ 会话不走那条链** ⇒ 压根看不到它们。想让它们跨通道,
107
-
108
103
  * 只能挂到插件根 —— 宿主会让所有会话继承插件作用域的工具(`context_*` 三工具当年也是为此才改成全局)。
109
-
110
104
  *
111
-
112
105
  * 命名:`<name>@<人设 id>`,并且**每个账号实例各注册一份**(实例各有自己的 preset / appId / 账号数据);
113
-
114
106
  * 执行时由注册名里的 tag 反查账号包(见 buildWorkspaceQqPack)—— **名字即归属**。
115
-
116
107
  *
117
-
118
108
  * 幂等:句柄存 globalThis(跨热刷可见),已注册过的 `name@tag` 不重复注册。
119
-
120
109
  * fail-soft:拿不到 tools 服务 / 没有实例 / 目录读不到,一律静默跳过,绝不影响主流程。
121
-
122
110
  */
123
111
  export declare function ensureExtToolsGlobal(globalCtx: unknown): void;
124
112
  export declare function ensureContextToolsGlobal(globalCtx: unknown): void;
@@ -1 +1 @@
1
- {"version":3,"file":"channel-tools.d.ts","sourceRoot":"","sources":["../src/channel-tools.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"channel-tools.d.ts","sourceRoot":"","sources":["../src/channel-tools.ts"],"names":[],"mappings":"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"}
@@ -20,6 +20,7 @@ import { diagWrite } from './shared/diag.js';
20
20
  import { makeExtCaps } from './features/ext-capabilities.js';
21
21
  import { verifyHuman, groupRegistryPath } from './api/group-admin.js';
22
22
  import { readGroupMembers } from './features/chat-ledger.js';
23
+ import { readMsgHistory, latestMsgHistory, historyDebug, latestGroupId } from './features/msg-history.js';
23
24
  import * as broadcastQueue from './features/broadcast.js';
24
25
  import { wakeSessionAgent, safeAppendUserMessage } from './features/group-hub.js';
25
26
  import { managersOf, findManagerByPeer, findManagerBySessionId } from './features/session-registry.js';
@@ -1141,10 +1142,15 @@ export async function apply(ctx) {
1141
1142
  const client = s.ch.manager.groupAdmin;
1142
1143
  if (!client)
1143
1144
  return undefined;
1145
+ // 2026-10-20 修(主人定):群工具**不该让人传 gid** —— 默认就该是"当前会话所在的群"。
1146
+ // 优先用会话自己的 peerId(= 入站 group openid,最可靠);
1147
+ // replyTarget 只是"最近一次出站目标",发过一次别的群就会被带歪(实测踩过)。
1148
+ const peer = String(s.rec.peerId ?? '').trim();
1144
1149
  const t = s.rec.replyTarget;
1145
- const gid = t && t.scope === 'group'
1146
- ? t.targetId
1147
- : s.ch.manager.manageGroup || undefined;
1150
+ const gid = peer
1151
+ || (t && t.scope === 'group' ? t.targetId : undefined)
1152
+ || s.ch.manager.manageGroup
1153
+ || undefined;
1148
1154
  return { client, gid };
1149
1155
  }
1150
1156
  // 会话表无此 agent(web 直连等): 遍历全局桥, 找第一个"群管理开启 + 配了 manageGroup"的实例
@@ -1294,7 +1300,7 @@ export async function apply(ctx) {
1294
1300
  description: '群管理(写,危险,**仅主人明确要求时调用**): 按关键词批量审批入群申请 —— 验证消息**命中任一关键词的放行**, 其余直接拒绝。基于官方待审列表(不含本地历史流水)。拒绝不可逆; 想先看结果传 dry_run=true。' +
1295
1301
  '\n示例: {gid:"<群openid>", keywords:["早饭"], rejectReason:"答非所问"} · 只放行不拒: {..., rejectUnmatched:false} · 预览: {..., dry_run:true}',
1296
1302
  parameters: {
1297
- gid: { type: 'string', description: '目标群 openid(可选; 不填=当前会话群/manageGroup)' },
1303
+ gid: { type: 'string', description: '目标群 openid(**可选**:不填=当前会话所在的群;跨群操作才传)' },
1298
1304
  keywords: { type: 'array', items: { type: 'string' }, required: true, description: '放行关键词(任一命中即放行; 不区分大小写)' },
1299
1305
  rejectUnmatched: { type: 'boolean', description: 'true(默认)=未命中的一律拒绝; false=只放行命中的, 其余不动' },
1300
1306
  rejectReason: { type: 'string', description: '拒绝理由(默认"答非所问"; 不要写暗号/答案)' },
@@ -2067,7 +2073,7 @@ export async function apply(ctx) {
2067
2073
  name: 'group_mute_state',
2068
2074
  description: '群管理(读): 查看当前群禁言状态(全员模式 + 正在禁言中的成员及到期时间)。仅主人要求时调用。',
2069
2075
  parameters: {
2070
- gid: { type: 'string', description: '目标群 openid(可选; 不填=当前会话群/manageGroup)' },
2076
+ gid: { type: 'string', description: '目标群 openid(**可选**:不填=当前会话所在的群;跨群操作才传)' },
2071
2077
  },
2072
2078
  output: {
2073
2079
  schema: {
@@ -2080,7 +2086,7 @@ export async function apply(ctx) {
2080
2086
  const ga = groupAdminOf(exec);
2081
2087
  if (!ga)
2082
2088
  return { ok: false, msg: '群管理未开启或非群会话' };
2083
- const gid = String(args.gid ?? '').trim() || ga.gid || '';
2089
+ const gid = String(args.gid ?? '').trim() || ga.gid || latestGroupId();
2084
2090
  if (!gid)
2085
2091
  return { ok: false, msg: '当前不是群会话且未指定 gid, 无法确定目标群' };
2086
2092
  const r = await ga.client.getMuteState(gid);
@@ -2092,13 +2098,198 @@ export async function apply(ctx) {
2092
2098
  return { ok: true, msg: `全员禁言: ${mode === 'always' ? '开启' : mode === 'schedule' ? '定时' : '关闭'}\n禁言中 ${members.length} 人:\n${lines.length ? lines.join('\n') : '(无)'}` };
2093
2099
  },
2094
2100
  });
2101
+ /**
2102
+ * 撤回群消息(2026-10-20 主人需求): 官方允许**群管理员撤回普通成员的消息**(窗口 2 分钟)。
2103
+ * msgId 来源 = 入站时记进 msg-recent 的内存缓冲(bootstrap),这里按「成员 / 最近一条」取。
2104
+ * 不新增落盘台账;缓冲只保留最近 2 分钟(容量上限 50 条),重启即清空。
2105
+ */
2106
+ const groupRecallTool = defineTool({
2107
+ name: 'group_recall',
2108
+ description: '群管理(写,危险): 撤回群消息(机器人是群管理员时可撤**普通成员**的消息; 官方窗口 2 分钟)。'
2109
+ + '\nmember=按成员 openid 撤其最近一条; 不传 member=撤群里最近一条; 也可直接给 msg_id。'
2110
+ + '\nmsgId 来自入站内存缓冲(只记最近 2 分钟/50 条, 重启清空)。仅主人明确要求时调用。'
2111
+ + '\n示例: {member:"<openid>"} / {msg_id:"ROBOT1.0_xxx"}',
2112
+ parameters: {
2113
+ gid: { type: 'string', description: '目标群 openid(**可选**:不填=当前会话所在的群;只有要操作别的群(机器人也是那个群的管理员)时才传)' },
2114
+ member: { type: 'string', description: '成员 openid(可选): 撤该成员最近一条' },
2115
+ msg_id: { type: 'string', description: '直接指定 message_id(可选, 优先于 member)' },
2116
+ },
2117
+ output: {
2118
+ schema: {
2119
+ type: 'object', additionalProperties: false,
2120
+ properties: { ok: { type: 'boolean', required: true }, msg: { type: 'string', required: true } },
2121
+ },
2122
+ render: (_a, v) => [{ type: 'text', text: v.ok ? v.msg : `失败: ${v.msg}` }],
2123
+ },
2124
+ async execute(args, exec) {
2125
+ const ga = groupAdminOf(exec);
2126
+ if (!ga)
2127
+ return { ok: false, msg: '群管理未开启或非群会话' };
2128
+ const gid = String(args.gid ?? '').trim() || ga.gid || latestGroupId();
2129
+ if (!gid)
2130
+ return { ok: false, msg: '当前不是群会话且未指定 gid, 无法确定目标群' };
2131
+ let msgId = String(args.msg_id ?? '').trim();
2132
+ let who = '';
2133
+ if (msgId === '') {
2134
+ const member = String(args.member ?? '').trim();
2135
+ const rec = latestMsgHistory('group', gid, { senderId: member || undefined, withinMs: 120_000 });
2136
+ if (!rec) {
2137
+ return {
2138
+ ok: false,
2139
+ msg: member
2140
+ ? '没找到该成员 2 分钟内的消息(msgId 只存在内存缓冲里,重启会清空;也可能超时了)'
2141
+ : '群里 2 分钟内没有记录到可撤回的消息',
2142
+ };
2143
+ }
2144
+ msgId = rec.id;
2145
+ who = rec.n || rec.s;
2146
+ }
2147
+ const r = await ga.client.recallMessage(gid, msgId, 'group');
2148
+ if (!r.ok)
2149
+ return { ok: false, msg: r.err.human };
2150
+ return { ok: true, msg: `✅ 已撤回${who ? ` ${who} 的` : ''}消息(msg_id=${msgId.slice(0, 18)}…)` };
2151
+ },
2152
+ });
2153
+ /**
2154
+ * 消息搜索(2026-10-20 主人需求): 在「最近 2 分钟消息缓冲」里搜消息(**包含机器人自己发的**)。
2155
+ * 返回的 msg_id 可直接喂给 group_recall_batch。
2156
+ */
2157
+ const msgSearchTool = defineTool({
2158
+ name: 'group_msg_search',
2159
+ description: '群管理(读): 在最近 2 分钟的消息缓冲里按关键词/类型搜消息(**含机器人自己发的**),返回 msg_id 供 group_recall_batch 批量撤回。'
2160
+ + '\n缓冲不落盘、重启清空,只覆盖最近 2 分钟(官方撤回窗口)。',
2161
+ parameters: {
2162
+ gid: { type: 'string', description: '目标群 openid(**可选**:不填=当前会话所在的群;只有要操作别的群(机器人也是那个群的管理员)时才传)' },
2163
+ keyword: { type: 'string', description: '关键词(子串匹配正文; 不填=不筛)' },
2164
+ kind: { type: 'string', enum: ['text', 'image', 'voice', 'video', 'file'], description: '消息类型筛选(可选)' },
2165
+ from_self: { type: 'boolean', description: 'true=只搜机器人自己发的; false=只搜别人发的; 不填=都搜' },
2166
+ limit: { type: 'integer', description: '最多返回条数(默认 20, 上限 50)' },
2167
+ },
2168
+ output: {
2169
+ schema: {
2170
+ type: 'object', additionalProperties: false,
2171
+ properties: { ok: { type: 'boolean', required: true }, msg: { type: 'string', required: true } },
2172
+ },
2173
+ render: (_a, v) => [{ type: 'text', text: v.ok ? v.msg : `失败: ${v.msg}` }],
2174
+ },
2175
+ async execute(args, exec) {
2176
+ const ga = groupAdminOf(exec);
2177
+ if (!ga)
2178
+ return { ok: false, msg: '群管理未开启或非群会话' };
2179
+ const gid = String(args.gid ?? '').trim() || ga.gid || latestGroupId();
2180
+ if (!gid)
2181
+ return { ok: false, msg: '当前不是群会话且未指定 gid' };
2182
+ const list = readMsgHistory('group', gid, {
2183
+ keyword: args.keyword ? String(args.keyword) : undefined,
2184
+ kind: (args.kind ? String(args.kind) : undefined),
2185
+ fromSelf: typeof args.from_self === 'boolean' ? args.from_self : undefined,
2186
+ limit: Number(args.limit) || 20,
2187
+ });
2188
+ if (list.length === 0)
2189
+ return { ok: true, msg: '台账里没有命中的消息(默认只查最近 2 小时)。' + historyDebug('group', gid) };
2190
+ const lines = list.map((m, i) => `${i + 1}. ${m.me ? '🤖' : '👤'}${m.n || m.s} [${m.k ?? 'text'}] ${m.x || '(无正文)'}|id=${String(m.id).slice(0, 16)}…|${new Date(m.t).toLocaleTimeString('zh-CN')}`);
2191
+ return {
2192
+ ok: true,
2193
+ msg: `命中 ${list.length} 条(默认最近 2 小时):\n${lines.join('\n')}\n→ 用 group_recall_batch 传 msg_ids 批量撤回(也可以直接给它 keyword/kind 让它自己挑)`,
2194
+ };
2195
+ },
2196
+ });
2197
+ /**
2198
+ * 批量撤回 + 可选批量禁言(2026-10-20 主人需求)
2199
+ */
2200
+ const recallBatchTool = defineTool({
2201
+ name: 'group_recall_batch',
2202
+ description: '群管理(写,危险): 批量撤回消息 + 可选批量禁言发送者。'
2203
+ + '\n① msg_ids 直接给要撤的 id 列表; 或 ② 留空并给 keyword/kind,让它从 2 分钟缓冲里自己挑(最多 20 条)。'
2204
+ + '\nmute_seconds>0 → 对涉及的**普通成员**批量禁言(机器人自己发的会跳过)。'
2205
+ + '\ndry_run=true 只回显要撤哪些、不执行。仅主人明确要求时调用。',
2206
+ parameters: {
2207
+ gid: { type: 'string', description: '目标群 openid(**可选**:不填=当前会话所在的群;只有要操作别的群(机器人也是那个群的管理员)时才传)' },
2208
+ msg_ids: { type: 'array', items: { type: 'string' }, description: '要撤的 message_id 列表(可空: 空则按 keyword/kind 从缓冲挑)' },
2209
+ keyword: { type: 'string', description: '按关键词挑(与 msg_ids 二选一)' },
2210
+ kind: { type: 'string', enum: ['text', 'image', 'voice', 'video', 'file'], description: '按类型挑(可选)' },
2211
+ mute_seconds: { type: 'integer', description: '同时禁言发送者的秒数(0/不填=不禁言)' },
2212
+ dry_run: { type: 'boolean', description: 'true=只列出、不执行' },
2213
+ },
2214
+ output: {
2215
+ schema: {
2216
+ type: 'object', additionalProperties: false,
2217
+ properties: { ok: { type: 'boolean', required: true }, msg: { type: 'string', required: true } },
2218
+ },
2219
+ render: (_a, v) => [{ type: 'text', text: v.ok ? v.msg : `失败: ${v.msg}` }],
2220
+ },
2221
+ async execute(args, exec) {
2222
+ const ga = groupAdminOf(exec);
2223
+ if (!ga)
2224
+ return { ok: false, msg: '群管理未开启或非群会话' };
2225
+ const gid = String(args.gid ?? '').trim() || ga.gid || latestGroupId();
2226
+ if (!gid)
2227
+ return { ok: false, msg: '当前不是群会话且未指定 gid' };
2228
+ const brief = (m) => `${m.me ? '🤖' : '👤'}${m.n || m.s} [${m.k ?? 'text'}] ${(m.x ?? '').slice(0, 24)}`;
2229
+ let ids = (Array.isArray(args.msg_ids) ? args.msg_ids : []).map((v) => String(v).trim()).filter(Boolean);
2230
+ let picked = [];
2231
+ if (ids.length === 0) {
2232
+ picked = readMsgHistory('group', gid, {
2233
+ keyword: args.keyword ? String(args.keyword) : undefined,
2234
+ kind: (args.kind ? String(args.kind) : undefined),
2235
+ limit: 20,
2236
+ }).map((r) => ({ me: r.me, s: r.s, n: r.n, k: r.k, x: r.x, msgId: r.id }));
2237
+ ids = picked.map((p) => p.msgId);
2238
+ }
2239
+ else {
2240
+ const all = readMsgHistory('group', gid, { withinMs: 120_000, limit: 200 }).map((r) => ({ me: r.me, s: r.s, n: r.n, k: r.k, x: r.x, msgId: r.id }));
2241
+ picked = all.filter((p) => ids.includes(p.msgId));
2242
+ }
2243
+ ids = ids.slice(0, 20);
2244
+ if (ids.length === 0)
2245
+ return { ok: false, msg: '没有要撤的消息(台账只查最近 2 分钟窗口,或关键词没命中)' };
2246
+ if (args.dry_run === true) {
2247
+ return {
2248
+ ok: true,
2249
+ msg: `[dry-run] 将撤回 ${ids.length} 条:\n${picked.slice(0, 20).map((p, i) => `${i + 1}. ${brief(p)}`).join('\n') || ids.join('\n')}`
2250
+ + (Number(args.mute_seconds) > 0 ? `\n并禁言涉及成员 ${Number(args.mute_seconds)} 秒` : ''),
2251
+ };
2252
+ }
2253
+ let ok = 0;
2254
+ const errs = [];
2255
+ for (const id of ids) {
2256
+ const r = await ga.client.recallMessage(gid, id, 'group');
2257
+ if (r.ok)
2258
+ ok++;
2259
+ else
2260
+ errs.push(`…${id.slice(-6)}: ${r.err.human}`);
2261
+ await new Promise((res) => setTimeout(res, 80));
2262
+ }
2263
+ let muteNote = '';
2264
+ const secs = Math.max(0, Math.round(Number(args.mute_seconds) || 0));
2265
+ if (secs > 0 && picked.length > 0) {
2266
+ const senders = [...new Set(picked.filter((p) => !p.me && /^[0-9A-Za-z]{32}$/u.test(p.s)).map((p) => p.s))].slice(0, 20);
2267
+ if (senders.length > 0) {
2268
+ const pad = (n) => String(n).padStart(2, '0');
2269
+ const ex = new Date(Date.now() + secs * 1000);
2270
+ const rfc = `${ex.getFullYear()}-${pad(ex.getMonth() + 1)}-${pad(ex.getDate())}T${pad(ex.getHours())}:${pad(ex.getMinutes())}:${pad(ex.getSeconds())}+08:00`;
2271
+ let mOk = 0;
2272
+ for (const sid of senders) {
2273
+ const mr = await ga.client.setMemberMute(gid, sid, rfc);
2274
+ if (mr.ok)
2275
+ mOk++;
2276
+ await new Promise((res) => setTimeout(res, 120));
2277
+ }
2278
+ muteNote = `\n禁言: ${mOk}/${senders.length} 人成功(${secs} 秒)`;
2279
+ }
2280
+ }
2281
+ const head = `✅ 撤回 ${ok}/${ids.length} 条`;
2282
+ const failNote = errs.length ? `\n失败:\n${errs.slice(0, 5).join('\n')}` : '';
2283
+ return { ok: ok > 0, msg: `${head}${muteNote}${failNote}` };
2284
+ },
2285
+ });
2095
2286
  const muteMemberTool = defineTool({
2096
2287
  name: 'group_mute_member',
2097
2288
  description: '群管理(写,危险): 禁言/解除群成员(**可批量, 单次最多 20 人**)。action=mute 禁言(seconds 秒, 默认600); unmute=解除。只能禁普通成员(群主/管理员禁不了)。' +
2098
2289
  '\n想按昵称禁言先用 id_lookup 拿 openid; 仅主人明确要求时调用。' +
2099
2290
  '\n示例: {member_openids:["<openid>"], action:"mute", seconds:600}',
2100
2291
  parameters: {
2101
- gid: { type: 'string', description: '目标群 openid(可选; 不填=当前会话群/manageGroup)' },
2292
+ gid: { type: 'string', description: '目标群 openid(**可选**:不填=当前会话所在的群;跨群操作才传)' },
2102
2293
  member_openids: { type: 'array', items: { type: 'string' }, required: true, description: '目标成员 openid 数组(只禁 1 个也传数组; ≤20 个; 可先用 id_lookup 按昵称查)' },
2103
2294
  action: { type: 'string', required: true, enum: ['mute', 'unmute'], description: 'mute 禁言 / unmute 解除' },
2104
2295
  seconds: { type: 'number', required: true, description: '禁言秒数(mute 时; 不需要可填 600)' },
@@ -2114,7 +2305,7 @@ export async function apply(ctx) {
2114
2305
  const ga = groupAdminOf(exec);
2115
2306
  if (!ga)
2116
2307
  return { ok: false, msg: '群管理未开启或非群会话' };
2117
- const gid = String(args.gid ?? '').trim() || ga.gid || '';
2308
+ const gid = String(args.gid ?? '').trim() || ga.gid || latestGroupId();
2118
2309
  if (!gid)
2119
2310
  return { ok: false, msg: '当前不是群会话且未指定 gid, 无法确定目标群' };
2120
2311
  const secs = Math.max(1, Math.min(30 * 86400, Math.round(Number(args.seconds ?? 600))));
@@ -2425,6 +2616,9 @@ export async function apply(ctx) {
2425
2616
  { name: 'group_join_auto', tool: autoApproveJoinTool },
2426
2617
  { name: 'group_mute_state', tool: muteStateTool },
2427
2618
  { name: 'group_mute_member', tool: muteMemberTool },
2619
+ { name: 'group_recall', tool: groupRecallTool },
2620
+ { name: 'group_msg_search', tool: msgSearchTool },
2621
+ { name: 'group_recall_batch', tool: recallBatchTool },
2428
2622
  { name: 'id_lookup', tool: idLookupTool },
2429
2623
  { name: 'session_list', tool: sessionListTool },
2430
2624
  { name: 'session_wake', tool: sessionWakeTool },
@@ -3221,29 +3415,17 @@ const CTX_TOOL_NAMES = ['context_memo', 'context_compact', 'context_drop', 'qq_g
3221
3415
  * ⚠️ 可见性不在这里决定 —— 交给 {@link restrictContextTools} 按会话剪枝。
3222
3416
  */
3223
3417
  /**
3224
-
3225
3418
  * 把 `expose: 'all'` 的扩展工具注册到**插件根作用域**(2026-10-10 主人要的"暴露给全部会话")。
3226
-
3227
3419
  *
3228
-
3229
3420
  * 为什么必须挂在这一层:扩展工具平时由 session-manager 在**每个 QQ 会话**上用 `agentCtx` 注册,
3230
-
3231
3421
  * 而 web / 终端等**非 QQ 会话不走那条链** ⇒ 压根看不到它们。想让它们跨通道,
3232
-
3233
3422
  * 只能挂到插件根 —— 宿主会让所有会话继承插件作用域的工具(`context_*` 三工具当年也是为此才改成全局)。
3234
-
3235
3423
  *
3236
-
3237
3424
  * 命名:`<name>@<人设 id>`,并且**每个账号实例各注册一份**(实例各有自己的 preset / appId / 账号数据);
3238
-
3239
3425
  * 执行时由注册名里的 tag 反查账号包(见 buildWorkspaceQqPack)—— **名字即归属**。
3240
-
3241
3426
  *
3242
-
3243
3427
  * 幂等:句柄存 globalThis(跨热刷可见),已注册过的 `name@tag` 不重复注册。
3244
-
3245
3428
  * fail-soft:拿不到 tools 服务 / 没有实例 / 目录读不到,一律静默跳过,绝不影响主流程。
3246
-
3247
3429
  */
3248
3430
  export function ensureExtToolsGlobal(globalCtx) {
3249
3431
  // 节流:本函数挂在 agent/pre-step(**每个 step** 都触发)—— 15 秒内不重复扫盘;