@aalis/plugin-tool-onebot 0.12.0 → 0.13.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/dist/index.js CHANGED
@@ -1,102 +1,78 @@
1
1
  import { media } from '@aalis/api-media';
2
2
  import { callPlatformAction, getPlatformAdapters, getPlatformNames, platform as platformService, resolvePlatformBySession, } from '@aalis/api-platform';
3
3
  import { sessionHistory } from '@aalis/api-session-history';
4
- import { storage } from '@aalis/api-storage';
4
+ import { sessionManager, TAKE_UNKNOWN, takeFrom } from '@aalis/api-session-manager';
5
5
  import { tools, withToolGroups } from '@aalis/api-tools';
6
6
  import { config, definePlugin, events, logger, optional } from '@aalis/core';
7
7
  import { defineConfig, parseConfig } from '@aalis/schema-config';
8
+ import { toSource } from '@aalis/schema-i18n';
9
+ import { t } from './texts.js';
8
10
  // ===== 插件元数据 =====
9
- // 这里的开关都不标 onInvalid:'error':本插件同时提供 OneBot 会话历史的拒绝规则,插件一进 error,规则跟着消失,
10
- // 而 session_get_history 对没有规则的平台默认放行。坏值回落到默认值并告警:默认拒绝的跨读开关回落后仍是拒绝。
11
11
  const configSchema = defineConfig({
12
12
  groupManagement: {
13
- label: '群管理工具',
13
+ label: t('groupManagementLabel'),
14
14
  fields: {
15
15
  enabled: {
16
16
  type: 'boolean',
17
- label: '启用群管理工具',
17
+ label: t('groupManagementEnabledLabel'),
18
18
  default: true,
19
- description: '禁言、踢人、设置群名片、撤回消息等',
19
+ description: t('groupManagementEnabledDescription'),
20
20
  },
21
21
  },
22
22
  },
23
23
  groupInfo: {
24
- label: '群信息查询',
24
+ label: t('groupInfoLabel'),
25
25
  fields: {
26
26
  enabled: {
27
27
  type: 'boolean',
28
- label: '启用群信息查询',
28
+ label: t('groupInfoEnabledLabel'),
29
29
  default: true,
30
- description: '查询群/成员信息',
30
+ description: t('groupInfoEnabledDescription'),
31
31
  },
32
32
  },
33
33
  },
34
34
  account: {
35
- label: '账号与好友',
35
+ label: t('accountLabel'),
36
36
  fields: {
37
37
  enabled: {
38
38
  type: 'boolean',
39
- label: '启用账号与好友查询',
39
+ label: t('accountEnabledLabel'),
40
40
  default: true,
41
- description: '群列表、好友列表等',
41
+ description: t('accountEnabledDescription'),
42
42
  },
43
43
  },
44
44
  },
45
45
  interaction: {
46
- label: '特殊交互',
46
+ label: t('interactionLabel'),
47
47
  fields: {
48
48
  enabled: {
49
49
  type: 'boolean',
50
- label: '启用特殊交互',
50
+ label: t('interactionEnabledLabel'),
51
51
  default: true,
52
- description: '戳一戳、群打卡等',
52
+ description: t('interactionEnabledDescription'),
53
53
  },
54
54
  },
55
55
  },
56
56
  sessionHistory: {
57
- label: '会话历史读取',
57
+ label: t('sessionHistoryLabel'),
58
58
  fields: {
59
59
  enabled: {
60
60
  type: 'boolean',
61
- label: '启用 OneBot 专属历史工具',
61
+ label: t('sessionHistoryEnabledLabel'),
62
62
  default: true,
63
- description: '注册 onebot_resolve_session_id / onebot_get_session_history(按群号/QQ 号读取近期历史)。只管这两个工具:下面的访问规则始终生效,同样约束通用的 session_get_history。',
63
+ description: t('sessionHistoryEnabledDescription'),
64
64
  },
65
65
  maxLimit: {
66
66
  type: 'number',
67
- label: '单次最多读取条数上限',
67
+ label: t('maxLimitLabel'),
68
68
  default: 100,
69
- description: 'agent 传入的 limit 参数会被截断到该上限。',
69
+ description: t('maxLimitDescription'),
70
70
  },
71
71
  defaultLimit: {
72
72
  type: 'number',
73
- label: '默认读取条数(agent 不传 limit 时)',
73
+ label: t('defaultLimitLabel'),
74
74
  default: 20,
75
- description: '不能超过 maxLimit。',
76
- },
77
- allowGroupReadPrivate: {
78
- type: 'boolean',
79
- label: '允许群聊读取私聊历史',
80
- default: false,
81
- description: '在群会话中调用历史读取工具时,是否允许目标是某个私聊。',
82
- },
83
- allowCrossSelf: {
84
- type: 'boolean',
85
- label: '允许跨机器人账号读取',
86
- default: false,
87
- description: '不同 selfId 之间跨读。多账号部署才需要。',
88
- },
89
- allowCrossGroup: {
90
- type: 'boolean',
91
- label: '允许群聊读取其他群聊历史',
92
- default: true,
93
- description: '群会话 → 另一个群会话。默认允许(便于跨群取上下文)。',
94
- },
95
- allowCrossPrivate: {
96
- type: 'boolean',
97
- label: '允许私聊读取其他私聊历史',
98
- default: false,
99
- description: '私聊会话 → 另一个 QQ 的私聊。默认拒绝(隐私敏感)。',
75
+ description: t('defaultLimitDescription'),
100
76
  },
101
77
  },
102
78
  },
@@ -105,9 +81,13 @@ const configSchema = defineConfig({
105
81
  const uses = {
106
82
  tools: optional(tools),
107
83
  platform: optional(platformService),
108
- storage: optional(storage),
109
84
  media: optional(media),
110
85
  sessionHistory: optional(sessionHistory),
86
+ /**
87
+ * onebot_get_msg 取到的消息所在的会话、onebot_recall_self 的目标会话与 onebot_get_forward_msg 取的转发收到它的会话按它的
88
+ * 规则判(takeFrom);不在时只能取当前会话里的
89
+ */
90
+ sessionManager: optional(sessionManager),
111
91
  events,
112
92
  logger,
113
93
  config,
@@ -124,18 +104,14 @@ function parseOneBotSession(sessionId) {
124
104
  targetId: parts.slice(3).join(':'),
125
105
  };
126
106
  }
127
- /** OneBot 各实例的全部连接里找出任意一个 selfId:先在所有实例里找在线的,都不在线才取任意一个 */
128
- function getAnyOneBotSelfId(platform) {
129
- const known = getPlatformAdapters(platform)
130
- .filter(a => a.platform === 'onebot')
131
- .flatMap(a => a.getConnections())
132
- .filter(c => c.selfId);
133
- return (known.find(c => c.status === 'online') ?? known[0])?.selfId;
134
- }
107
+ /** 每个用到机器人账号的工具都带的 self_id 参数 */
108
+ const SELF_ID_PARAM = {
109
+ type: 'string',
110
+ description: '机器人账号(QQ 号)。在 OneBot 会话里可以省,用这个会话的账号;不在 OneBot 会话里且有多个账号时必填',
111
+ };
135
112
  /**
136
- * 解析出本次工具调用应使用的 selfId(机器人账号)。
137
- * 优先级:args.self_id → 当前 callCtx 是 OneBot 会话时取它的 selfId → 任意可用 adapter 的第一个 selfId。
138
- * 找不到则抛错(无可用 OneBot 连接)。
113
+ * 本次调用用哪个机器人账号:写明的 self_id;否则当前 OneBot 会话的账号;都没有时汇总各 OneBot 实例的已知账号(不论在不在线),
114
+ * 恰好一个才用它,多个时报错列出账号、要求写明。子任务会话不算 OneBot 会话,账号不从根会话推
139
115
  */
140
116
  function resolveSelfId(platform, callCtx, args) {
141
117
  const fromArgs = args.self_id != null && String(args.self_id).trim() ? String(args.self_id).trim() : '';
@@ -144,10 +120,16 @@ function resolveSelfId(platform, callCtx, args) {
144
120
  const parsed = parseOneBotSession(callCtx.sessionId);
145
121
  if (parsed?.selfId)
146
122
  return parsed.selfId;
147
- const fallback = getAnyOneBotSelfId(platform);
148
- if (fallback)
149
- return fallback;
150
- throw new Error('未找到可用的 OneBot 连接');
123
+ const known = new Map(getPlatformAdapters(platform)
124
+ .filter(a => a.platform === 'onebot')
125
+ .flatMap(a => a.getConnections())
126
+ .flatMap(c => (c.selfId ? [[c.selfId, c.status]] : [])));
127
+ if (known.size === 1)
128
+ return [...known.keys()][0];
129
+ if (known.size === 0)
130
+ throw new Error('未找到 OneBot 账号');
131
+ const listed = [...known].map(([selfId, status]) => `${selfId}(${status})`).join('、');
132
+ throw new Error(`有 ${known.size} 个 OneBot 账号:${listed},请用 self_id 写明用哪个`);
151
133
  }
152
134
  /**
153
135
  * 解析"目标群":args.group_id 优先;否则当前会话必须在 OneBot 群里。
@@ -160,13 +142,27 @@ function resolveGroupTarget(platform, callCtx, args) {
160
142
  return { selfId, groupId: argGroupId };
161
143
  const parsed = parseOneBotSession(callCtx.sessionId);
162
144
  if (parsed?.detailType === 'group' && parsed.targetId) {
163
- return { selfId: parsed.selfId, groupId: parsed.targetId };
145
+ return { selfId, groupId: parsed.targetId };
164
146
  }
165
147
  throw new Error('未提供 group_id,且当前不在 OneBot 群会话中。请在参数中显式传入 group_id。');
166
148
  }
167
149
  function buildOneBotSessionId(selfId, detailType, targetId) {
168
150
  return `onebot:${selfId}:${detailType}:${targetId}`;
169
151
  }
152
+ /**
153
+ * get_msg 取到的消息所在的会话:群按群号,私聊按对端账号(发送者不是机器人自己时就是发送者)。她自己发的私聊消息与没给这些
154
+ * 字段的实现端拼不出,为 undefined
155
+ */
156
+ function sessionOfMessage(selfId, data) {
157
+ const sender = data?.sender;
158
+ if (data?.message_type === 'group' && data.group_id != null) {
159
+ return buildOneBotSessionId(selfId, 'group', String(data.group_id));
160
+ }
161
+ if (data?.message_type === 'private' && sender?.user_id != null && String(sender.user_id) !== selfId) {
162
+ return buildOneBotSessionId(selfId, 'private', String(sender.user_id));
163
+ }
164
+ return undefined;
165
+ }
170
166
  /**
171
167
  * 机器人账号的合成会话 id。路由与适配器都只看 id 里的 selfId(路由找到账号所属的实例,适配器定位它的连接),
172
168
  * 与目标会话类型无关。
@@ -178,35 +174,20 @@ function selfSessionId(selfId) {
178
174
  async function callAction(platform, selfId, action, params) {
179
175
  return callPlatformAction(platform, selfSessionId(selfId), action, params);
180
176
  }
181
- function imageMimeFromPath(path) {
182
- const cleaned = path.split('?')[0];
183
- const dot = cleaned.lastIndexOf('.');
184
- const ext = dot >= 0 ? cleaned.slice(dot).toLowerCase() : '';
185
- if (ext === '.jpg' || ext === '.jpeg')
186
- return 'image/jpeg';
187
- if (ext === '.gif')
188
- return 'image/gif';
189
- if (ext === '.webp')
190
- return 'image/webp';
191
- if (ext === '.bmp')
192
- return 'image/bmp';
193
- return 'image/png';
194
- }
195
177
  /**
196
- * 尝试将 OneBot get_image 返回的文件路径转为 data URI。
197
- * 仅支持 storage URI 形式(含 ':/')。裸本地 fs 路径不再处理,
198
- * 交由上游 vision 退路使用原始 source(该 fallback 本身在实践中极少命中)。
178
+ * 按顺序调候选动作名,只有实现端回不支持该动作时才换下一个;其余错误(超时、没权限、对方不在群)原样抛出,不换个动作再做
179
+ * 一次。戳一戳的动作名各实现端不同:NapCat 是 group_poke、friend_poke 与 send_poke,没有 send_group_poke,群里在 NapCat 上
180
+ * 只有 group_poke 一个,后面的候选留给别的实现端
199
181
  */
200
- async function localImageToDataUri(storage, path) {
201
- if (!path.includes(':/'))
202
- return null;
182
+ async function callFirstSupported(platform, selfId, actions, params) {
183
+ const [action, ...rest] = actions;
203
184
  try {
204
- const buf = (await storage.readFile(path));
205
- const b64 = Buffer.from(buf).toString('base64');
206
- return `data:${imageMimeFromPath(path)};base64,${b64}`;
185
+ return await callAction(platform, selfId, action, params);
207
186
  }
208
- catch {
209
- return null;
187
+ catch (err) {
188
+ if (rest.length === 0 || err.unsupported !== true)
189
+ throw err;
190
+ return callFirstSupported(platform, selfId, rest, params);
210
191
  }
211
192
  }
212
193
  function imageRefKey(ref) {
@@ -259,6 +240,7 @@ function extractImageRefsFromContent(content) {
259
240
  }
260
241
  return refs;
261
242
  }
243
+ /** 合并转发的节点:data 本身、data.messages 或 data.message 是数组;都不是时返回格式认不出,为 undefined */
262
244
  function getForwardMessages(data) {
263
245
  const root = data;
264
246
  if (Array.isArray(root))
@@ -269,7 +251,7 @@ function getForwardMessages(data) {
269
251
  if (Array.isArray(root?.message)) {
270
252
  return root.message;
271
253
  }
272
- return [];
254
+ return undefined;
273
255
  }
274
256
  function getForwardNodeData(item) {
275
257
  const node = item && typeof item === 'object' ? item : {};
@@ -284,33 +266,32 @@ function getForwardNodeContent(item) {
284
266
  return nodeData.content ?? node.content ?? nodeData.message ?? node.message;
285
267
  }
286
268
  function collectForwardImageRefs(data, limit) {
287
- return getForwardMessages(data)
269
+ return (getForwardMessages(data) ?? [])
288
270
  .slice(0, limit)
289
271
  .flatMap(item => extractImageRefsFromContent(getForwardNodeContent(item)));
290
272
  }
291
- async function resolveForwardImageSource(caps, storage, selfId, ref) {
292
- if (/^(https?:|data:)/i.test(ref.source))
273
+ /** 消息段里能当来源读的:网址,或 `data:` 后面紧跟 MIME 类型的 base64 data URL(与 onebot 适配器的 segmentSource 同一条规则) */
274
+ const SEGMENT_SOURCE = /^(?:https?:\/\/|data:[\w.+-]+\/[\w.+-]+(?:;[^;,]*)*;base64,)/i;
275
+ /**
276
+ * 转发里一张图交给识图的来源。图片段里写的只认网址与 base64 的 data URL;别的(文件名、本机路径、像存储地址的串)只当
277
+ * get_image 的 file 参数,读的是实现端交回的地址(自己发起的请求的应答),段里写的不读:群友能给文件起名。交回的地址原样
278
+ * 交给识图,由 media 按文件头认;换不出地址的为 undefined,这张图不识别
279
+ */
280
+ async function resolveForwardImageSource(caps, selfId, ref) {
281
+ if (SEGMENT_SOURCE.test(ref.source))
293
282
  return ref.source;
294
283
  const { platform, logger } = caps;
295
284
  try {
296
285
  const imageData = (await callAction(platform, selfId, 'get_image', { file: ref.source }));
297
- const resolvedSource = imageData.url ?? imageData.file ?? ref.source;
298
- if (typeof resolvedSource === 'string') {
299
- if (/^(https?:|data:)/i.test(resolvedSource))
300
- return resolvedSource;
301
- const dataUri = await localImageToDataUri(storage, resolvedSource);
302
- if (dataUri)
303
- return dataUri;
304
- return resolvedSource;
305
- }
286
+ const resolvedSource = imageData.url ?? imageData.file;
287
+ return typeof resolvedSource === 'string' ? resolvedSource : undefined;
306
288
  }
307
289
  catch (err) {
308
290
  logger.use.debug(`get_image 解析转发图片失败 (${ref.source}): ${err}`);
291
+ return undefined;
309
292
  }
310
- const dataUri = await localImageToDataUri(storage, ref.source);
311
- return dataUri ?? ref.source;
312
293
  }
313
- async function recognizeForwardImages(caps, storage, selfId, data, limit) {
294
+ async function recognizeForwardImages(caps, selfId, data, limit) {
314
295
  const refs = collectForwardImageRefs(data, limit);
315
296
  const imageDescriptions = new Map();
316
297
  if (refs.length === 0)
@@ -328,7 +309,9 @@ async function recognizeForwardImages(caps, storage, selfId, data, limit) {
328
309
  continue;
329
310
  seen.add(key);
330
311
  try {
331
- const imageSource = await resolveForwardImageSource(caps, storage, selfId, ref);
312
+ const imageSource = await resolveForwardImageSource(caps, selfId, ref);
313
+ if (!imageSource)
314
+ continue;
332
315
  const description = await mediaService.describeImage(imageSource);
333
316
  if (description)
334
317
  imageDescriptions.set(key, description);
@@ -394,6 +377,10 @@ function formatCqMessageContent(content, context) {
394
377
  }
395
378
  function formatForwardMessage(data, limit, context) {
396
379
  const rawMessages = getForwardMessages(data);
380
+ if (!rawMessages)
381
+ throw new Error('合并转发的返回格式认不出');
382
+ if (!rawMessages.length)
383
+ return '合并转发内容为空';
397
384
  const messages = rawMessages.slice(0, limit);
398
385
  const lines = messages.map((item, index) => {
399
386
  const node = item && typeof item === 'object' ? item : {};
@@ -414,61 +401,60 @@ function formatForwardMessage(data, limit, context) {
414
401
  return `${index + 1}. ${prefix}: ${content || '[空消息]'}`;
415
402
  });
416
403
  const header = `合并转发共 ${rawMessages.length} 条${rawMessages.length > messages.length ? `,以下显示前 ${messages.length} 条` : ''}:`;
417
- return lines.length > 0 ? `${header}\n${lines.join('\n')}` : '合并转发内容为空或当前 OneBot 实现返回格式无法识别';
404
+ return `${header}\n${lines.join('\n')}`;
418
405
  }
419
406
  // ===== 权限检查辅助 =====
420
407
  const ROLE_LEVEL = { owner: 3, admin: 2, member: 1 };
421
408
  function roleLevel(role) {
422
409
  return ROLE_LEVEL[role] ?? 0;
423
410
  }
424
- /** 查询群成员信息(失败返回 null) */
411
+ /** 查询群成员信息,失败时抛出适配器的原话 */
425
412
  async function getGroupMemberInfo(platform, selfId, groupId, userId) {
426
- try {
427
- return (await callAction(platform, selfId, 'get_group_member_info', {
428
- group_id: Number(groupId),
429
- user_id: Number(userId),
430
- no_cache: true,
431
- }));
432
- }
433
- catch {
434
- return null;
435
- }
413
+ return (await callAction(platform, selfId, 'get_group_member_info', {
414
+ group_id: Number(groupId),
415
+ user_id: Number(userId),
416
+ no_cache: true,
417
+ }));
436
418
  }
437
- /** 检查管理操作权限,返回错误消息(null 表示通过或无法验证) */
438
- async function checkAdminPermission(platform, selfId, groupId, targetUserId, requireOwner) {
439
- const selfInfo = await getGroupMemberInfo(platform, selfId, groupId, selfId);
440
- if (!selfInfo)
441
- return null; // 无法获取自身信息,跳过权限检查
442
- const selfRole = String(selfInfo.role ?? 'member');
419
+ /**
420
+ * 机器人在群里的角色够不够做这个管理操作,不够时抛错。取不到机器人自身信息或其中没有角色时放过检查:真不够权限时实现端的
421
+ * 返回码会报出来。目标成员的信息或角色取不到(查询失败,或实现端回成功却没给)时报错,不当成普通成员
422
+ */
423
+ async function assertAdminPermission(platform, selfId, groupId, targetUserId, requireOwner) {
424
+ const selfInfo = await getGroupMemberInfo(platform, selfId, groupId, selfId).catch(() => null);
425
+ if (!selfInfo?.role)
426
+ return;
427
+ const selfRole = String(selfInfo.role);
443
428
  if (requireOwner && selfRole !== 'owner') {
444
- return `操作失败:此操作仅群主可执行(当前角色:${selfRole})`;
429
+ throw new Error(`操作失败:此操作仅群主可执行(当前角色:${selfRole})`);
445
430
  }
446
431
  if (selfRole === 'member') {
447
- return `操作失败:机器人不是管理员(当前角色:${selfRole}),无法执行管理操作`;
432
+ throw new Error(`操作失败:机器人不是管理员(当前角色:${selfRole}),无法执行管理操作`);
448
433
  }
449
434
  if (targetUserId) {
450
- const targetInfo = await getGroupMemberInfo(platform, selfId, groupId, targetUserId);
451
- if (!targetInfo) {
452
- return `操作失败:无法获取用户 ${targetUserId} 的信息,该用户可能不在群中`;
453
- }
454
- const targetRole = String(targetInfo.role ?? 'member');
435
+ const targetInfo = await getGroupMemberInfo(platform, selfId, groupId, targetUserId).catch((err) => {
436
+ throw new Error(`操作失败:无法获取用户 ${targetUserId} 的信息:${err instanceof Error ? err.message : String(err)}`);
437
+ });
438
+ if (!targetInfo)
439
+ throw new Error(`操作失败:无法获取用户 ${targetUserId} 的信息:实现端没给成员信息`);
440
+ if (!targetInfo.role)
441
+ throw new Error(`操作失败:无法获取用户 ${targetUserId} 的信息:实现端没给成员角色`);
442
+ const targetRole = String(targetInfo.role);
455
443
  if (roleLevel(targetRole) >= roleLevel(selfRole)) {
456
444
  const targetLabel = targetRole === 'owner' ? '群主' : '管理员';
457
- return `操作失败:无法对${targetLabel}执行此操作(机器人角色:${selfRole},目标角色:${targetRole})`;
445
+ throw new Error(`操作失败:无法对${targetLabel}执行此操作(机器人角色:${selfRole},目标角色:${targetRole})`);
458
446
  }
459
447
  }
460
- return null;
461
448
  }
462
449
  export default definePlugin({
463
450
  name: '@aalis/plugin-tool-onebot',
464
- displayName: 'OneBot 工具',
451
+ displayName: t('displayName'),
465
452
  subsystem: 'tools',
466
453
  configSchema,
467
454
  uses,
468
455
  apply(caps) {
469
456
  const { tools, events, logger, platform } = caps;
470
457
  const cfg = parseConfig(configSchema, caps.config.use, logger.use);
471
- const storage = caps.storage.use;
472
458
  const bundle = {
473
459
  daily: withToolGroups(tools.use, ['onebot-daily']),
474
460
  group: withToolGroups(tools.use, ['onebot-group']),
@@ -476,17 +462,7 @@ export default definePlugin({
476
462
  };
477
463
  const maxLimit = Math.max(1, Math.min(1000, cfg.sessionHistory.maxLimit || 100));
478
464
  const defaultLimitRaw = Math.max(1, Math.floor(cfg.sessionHistory.defaultLimit || 20));
479
- const historyCfg = {
480
- maxLimit,
481
- defaultLimit: Math.min(defaultLimitRaw, maxLimit),
482
- allowGroupReadPrivate: cfg.sessionHistory.allowGroupReadPrivate,
483
- allowCrossSelf: cfg.sessionHistory.allowCrossSelf,
484
- allowCrossGroup: cfg.sessionHistory.allowCrossGroup,
485
- allowCrossPrivate: cfg.sessionHistory.allowCrossPrivate,
486
- };
487
- // 访问规则始终注册:它同样约束通用的 session_get_history,不随专属工具开关撤掉(关掉工具不能反倒放宽读取)。
488
- // 规则只对 onebot 会话 id 表态,与 OneBot 平台在不在场无关:不进下面的平台闸。
489
- registerOneBotHistoryAccessChecker(caps, historyCfg);
465
+ const historyCfg = { maxLimit, defaultLimit: Math.min(defaultLimitRaw, maxLimit) };
490
466
  // 仅当 OneBot 平台可用时才注册工具。platform 是可选的多提供者服务,OneBot 适配器可能晚于
491
467
  // app:ready 上线(WebUI 启用、市场热装、启动失败后修好),且作为非胜者上线时 follow 不触发:
492
468
  // 故在 app:ready 与每次 platform 提供者注册时各判一次,出现 onebot 即注册,之后不再重复。
@@ -500,23 +476,26 @@ export default definePlugin({
500
476
  // 注册工具分组:按职责语义切分,彼此不重叠
501
477
  tools.use.registerGroup({
502
478
  name: 'onebot-daily',
503
- label: 'OneBot 只读与日常',
479
+ label: t('dailyGroup'),
480
+ summary: t('dailyGroupSummary'),
504
481
  description: '只读查询(群信息/成员/好友列表/历史/登录信息)+ 日常低风险互动(戳一戳、好友赞)',
505
482
  });
506
483
  tools.use.registerGroup({
507
484
  name: 'onebot-group',
508
- label: 'OneBot 群务',
485
+ label: t('groupGroup'),
486
+ summary: t('groupGroupSummary'),
509
487
  description: '群务管理:禁言/全员禁言/踢人/撤回消息/设置群名片/改群名/设管理员/群打卡/审批加群申请 等',
510
488
  });
511
489
  tools.use.registerGroup({
512
490
  name: 'onebot-personal',
513
- label: 'OneBot 个人/私聊',
491
+ label: t('personalGroup'),
492
+ summary: t('personalGroupSummary'),
514
493
  description: '影响 bot 账号本身的人际关系:退群、删好友、处理好友申请、接受/拒绝入群邀请',
515
494
  });
516
495
  if (cfg.groupManagement.enabled)
517
496
  registerGroupManagementTools(caps, bundle);
518
497
  if (cfg.groupInfo.enabled)
519
- registerGroupInfoTools(caps, storage, bundle);
498
+ registerGroupInfoTools(caps, bundle);
520
499
  if (cfg.account.enabled)
521
500
  registerAccountTools(caps, bundle);
522
501
  if (cfg.interaction.enabled)
@@ -538,7 +517,7 @@ export default definePlugin({
538
517
  });
539
518
  // ===== 群管理工具 =====
540
519
  function registerGroupManagementTools(caps, bundle) {
541
- const { platform, logger } = caps;
520
+ const { platform, logger, sessionManager } = caps;
542
521
  const { group, personal } = bundle;
543
522
  // ---- 群禁言(单人)----
544
523
  group.register({
@@ -553,6 +532,7 @@ function registerGroupManagementTools(caps, bundle) {
553
532
  user_id: { type: 'string', description: '要禁言的用户QQ号' },
554
533
  duration: { type: 'number', description: '禁言时长(秒),0 = 解除禁言,默认 60' },
555
534
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
535
+ self_id: SELF_ID_PARAM,
556
536
  },
557
537
  required: ['user_id'],
558
538
  },
@@ -561,27 +541,25 @@ function registerGroupManagementTools(caps, bundle) {
561
541
  handler: async (args, callCtx) => {
562
542
  const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
563
543
  const duration = typeof args.duration === 'number' ? args.duration : 60;
564
- const permError = await checkAdminPermission(platform, selfId, groupId, String(args.user_id));
565
- if (permError)
566
- return permError;
544
+ await assertAdminPermission(platform, selfId, groupId, String(args.user_id));
567
545
  await callAction(platform, selfId, 'set_group_ban', {
568
546
  group_id: Number(groupId),
569
547
  user_id: Number(args.user_id),
570
548
  duration,
571
549
  });
572
- // 验证禁言是否生效
573
- const info = await getGroupMemberInfo(platform, selfId, groupId, String(args.user_id));
550
+ // 验证禁言是否生效:查不到成员信息时照报已发出指令、写明无法验证
551
+ const info = await getGroupMemberInfo(platform, selfId, groupId, String(args.user_id)).catch(() => null);
574
552
  if (info && 'shut_up_timestamp' in info) {
575
553
  const shutUp = Number(info.shut_up_timestamp);
576
554
  const now = Math.floor(Date.now() / 1000);
577
555
  if (duration === 0) {
578
- return shutUp <= now ? `已解除 ${args.user_id} 的禁言` : `解除禁言指令已发送,但用户仍在禁言中`;
579
- }
580
- else {
581
- return shutUp > now
582
- ? `已禁言 ${args.user_id},时长 ${duration} 秒`
583
- : `禁言操作未生效(API 返回成功但禁言状态未变化),请检查机器人实际权限`;
556
+ if (shutUp > now)
557
+ throw new Error('解除禁言指令已发送,但用户仍在禁言中');
558
+ return `已解除 ${args.user_id} 的禁言`;
584
559
  }
560
+ if (shutUp <= now)
561
+ throw new Error('禁言操作未生效(API 返回成功但禁言状态未变化),请检查机器人实际权限');
562
+ return `已禁言 ${args.user_id},时长 ${duration} 秒`;
585
563
  }
586
564
  return duration === 0
587
565
  ? `已解除 ${args.user_id} 的禁言`
@@ -600,6 +578,7 @@ function registerGroupManagementTools(caps, bundle) {
600
578
  properties: {
601
579
  enable: { type: 'boolean', description: 'true = 开启全群禁言,false = 关闭' },
602
580
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
581
+ self_id: SELF_ID_PARAM,
603
582
  },
604
583
  required: ['enable'],
605
584
  },
@@ -607,9 +586,7 @@ function registerGroupManagementTools(caps, bundle) {
607
586
  },
608
587
  handler: async (args, callCtx) => {
609
588
  const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
610
- const permError = await checkAdminPermission(platform, selfId, groupId);
611
- if (permError)
612
- return permError;
589
+ await assertAdminPermission(platform, selfId, groupId);
613
590
  await callAction(platform, selfId, 'set_group_whole_ban', {
614
591
  group_id: Number(groupId),
615
592
  enable: !!args.enable,
@@ -630,6 +607,7 @@ function registerGroupManagementTools(caps, bundle) {
630
607
  user_id: { type: 'string', description: '要踢出的用户QQ号' },
631
608
  reject_add_request: { type: 'boolean', description: '是否拒绝此人再次加群,默认 false' },
632
609
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
610
+ self_id: SELF_ID_PARAM,
633
611
  },
634
612
  required: ['user_id'],
635
613
  },
@@ -637,9 +615,7 @@ function registerGroupManagementTools(caps, bundle) {
637
615
  },
638
616
  handler: async (args, callCtx) => {
639
617
  const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
640
- const permError = await checkAdminPermission(platform, selfId, groupId, String(args.user_id));
641
- if (permError)
642
- return permError;
618
+ await assertAdminPermission(platform, selfId, groupId, String(args.user_id));
643
619
  await callAction(platform, selfId, 'set_group_kick', {
644
620
  group_id: Number(groupId),
645
621
  user_id: Number(args.user_id),
@@ -659,6 +635,7 @@ function registerGroupManagementTools(caps, bundle) {
659
635
  type: 'object',
660
636
  properties: {
661
637
  group_id: { type: 'string', description: '要退出的群号;在群聊中可省略,默认当前群' },
638
+ self_id: SELF_ID_PARAM,
662
639
  },
663
640
  required: [],
664
641
  },
@@ -670,7 +647,7 @@ function registerGroupManagementTools(caps, bundle) {
670
647
  const parsed = parseOneBotSession(callCtx.sessionId);
671
648
  const groupId = argGroupId || (parsed?.detailType === 'group' ? parsed.targetId : '');
672
649
  if (!groupId)
673
- return '请提供 group_id,或在要退出的群聊中调用此工具';
650
+ throw new Error('请提供 group_id,或在要退出的群聊中调用此工具');
674
651
  await callAction(platform, selfId, 'set_group_leave', {
675
652
  group_id: Number(groupId),
676
653
  is_dismiss: false,
@@ -691,6 +668,7 @@ function registerGroupManagementTools(caps, bundle) {
691
668
  user_id: { type: 'string', description: '用户QQ号' },
692
669
  card: { type: 'string', description: '新的群名片,空字符串表示取消' },
693
670
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
671
+ self_id: SELF_ID_PARAM,
694
672
  },
695
673
  required: ['user_id', 'card'],
696
674
  },
@@ -698,9 +676,7 @@ function registerGroupManagementTools(caps, bundle) {
698
676
  },
699
677
  handler: async (args, callCtx) => {
700
678
  const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
701
- const permError = await checkAdminPermission(platform, selfId, groupId);
702
- if (permError)
703
- return permError;
679
+ await assertAdminPermission(platform, selfId, groupId);
704
680
  await callAction(platform, selfId, 'set_group_card', {
705
681
  group_id: Number(groupId),
706
682
  user_id: Number(args.user_id),
@@ -721,6 +697,7 @@ function registerGroupManagementTools(caps, bundle) {
721
697
  properties: {
722
698
  group_name: { type: 'string', description: '新的群名称' },
723
699
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
700
+ self_id: SELF_ID_PARAM,
724
701
  },
725
702
  required: ['group_name'],
726
703
  },
@@ -728,9 +705,7 @@ function registerGroupManagementTools(caps, bundle) {
728
705
  },
729
706
  handler: async (args, callCtx) => {
730
707
  const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
731
- const permError = await checkAdminPermission(platform, selfId, groupId);
732
- if (permError)
733
- return permError;
708
+ await assertAdminPermission(platform, selfId, groupId);
734
709
  await callAction(platform, selfId, 'set_group_name', {
735
710
  group_id: Number(groupId),
736
711
  group_name: String(args.group_name),
@@ -751,6 +726,7 @@ function registerGroupManagementTools(caps, bundle) {
751
726
  user_id: { type: 'string', description: '用户QQ号' },
752
727
  special_title: { type: 'string', description: '专属头衔,空字符串表示取消' },
753
728
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
729
+ self_id: SELF_ID_PARAM,
754
730
  },
755
731
  required: ['user_id', 'special_title'],
756
732
  },
@@ -758,9 +734,7 @@ function registerGroupManagementTools(caps, bundle) {
758
734
  },
759
735
  handler: async (args, callCtx) => {
760
736
  const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
761
- const permError = await checkAdminPermission(platform, selfId, groupId, undefined, true);
762
- if (permError)
763
- return permError;
737
+ await assertAdminPermission(platform, selfId, groupId, undefined, true);
764
738
  await callAction(platform, selfId, 'set_group_special_title', {
765
739
  group_id: Number(groupId),
766
740
  user_id: Number(args.user_id),
@@ -783,6 +757,7 @@ function registerGroupManagementTools(caps, bundle) {
783
757
  user_id: { type: 'string', description: '用户QQ号' },
784
758
  enable: { type: 'boolean', description: 'true = 设为管理员,false = 取消管理员' },
785
759
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
760
+ self_id: SELF_ID_PARAM,
786
761
  },
787
762
  required: ['user_id', 'enable'],
788
763
  },
@@ -790,9 +765,7 @@ function registerGroupManagementTools(caps, bundle) {
790
765
  },
791
766
  handler: async (args, callCtx) => {
792
767
  const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
793
- const permError = await checkAdminPermission(platform, selfId, groupId, undefined, true);
794
- if (permError)
795
- return permError;
768
+ await assertAdminPermission(platform, selfId, groupId, undefined, true);
796
769
  await callAction(platform, selfId, 'set_group_admin', {
797
770
  group_id: Number(groupId),
798
771
  user_id: Number(args.user_id),
@@ -812,6 +785,7 @@ function registerGroupManagementTools(caps, bundle) {
812
785
  type: 'object',
813
786
  properties: {
814
787
  message_id: { type: 'string', description: '要撤回的消息 ID' },
788
+ self_id: SELF_ID_PARAM,
815
789
  },
816
790
  required: ['message_id'],
817
791
  },
@@ -842,6 +816,7 @@ function registerGroupManagementTools(caps, bundle) {
842
816
  count: { type: 'number', description: '撤回最近多少条机器人消息(默认 1,上限 10)' },
843
817
  group_id: { type: 'string', description: '可选:目标群号。不传则用当前会话。' },
844
818
  user_id: { type: 'string', description: '可选:目标私聊 QQ 号。不传则用当前会话。' },
819
+ self_id: SELF_ID_PARAM,
845
820
  },
846
821
  required: [],
847
822
  },
@@ -859,18 +834,22 @@ function registerGroupManagementTools(caps, bundle) {
859
834
  else {
860
835
  const parsed = parseOneBotSession(callCtx.sessionId);
861
836
  if (!parsed)
862
- return '当前不在 OneBot 会话中,请用 group_id 或 user_id 指定要撤回的目标会话。';
837
+ throw new Error('当前不在 OneBot 会话中,请用 group_id 或 user_id 指定要撤回的目标会话。');
863
838
  targetSessionId = callCtx.sessionId;
864
839
  }
840
+ // 目标会话逐个参与者过会话管理的规则(同 onebot_get_msg):不放行时在读发出记录之前报错,不撤回,报错里没有她在那里说的话
841
+ const verdict = await callCtx.approveFor(who => takeFrom(sessionManager, callCtx.sessionId, targetSessionId, who));
842
+ if (!verdict.ok)
843
+ throw new Error(toSource(verdict.reason));
865
844
  // 发出记录在发消息的那个实例里:按目标会话路由
866
845
  const adapter = (await resolvePlatformBySession(platform, targetSessionId))?.use;
867
846
  if (!adapter?.getSentMessages) {
868
- return '未找到支持记录自身发出消息的 OneBot 适配器(getSentMessages 不在平台契约内,第三方适配器可不提供)。如已知 message_id 可改用 onebot_delete_msg。';
847
+ throw new Error('未找到支持记录自身发出消息的 OneBot 适配器(getSentMessages 不在平台契约内,第三方适配器可不提供)。如已知 message_id 可改用 onebot_delete_msg。');
869
848
  }
870
849
  const count = Math.max(1, Math.min(10, Math.floor(Number(args.count) || 1)));
871
850
  const recent = adapter.getSentMessages(targetSessionId, 10); // 新→旧
872
851
  if (recent.length === 0) {
873
- return '没有可撤回的记录:机器人近期未在该会话发送消息,或已超出记录时窗(约 30 分钟)。';
852
+ throw new Error('没有可撤回的记录:机器人近期未在该会话发送消息,或已超出记录时窗(约 30 分钟)。');
874
853
  }
875
854
  const targets = recent.slice(0, count);
876
855
  const recalled = [];
@@ -886,7 +865,7 @@ function registerGroupManagementTools(caps, bundle) {
886
865
  }
887
866
  }
888
867
  if (recalled.length === 0) {
889
- return `撤回失败(可能已超过可撤回时间,QQ 通常限 2 分钟):${failed.join(';')}`;
868
+ throw new Error(`撤回失败:${failed.join(';')}`);
890
869
  }
891
870
  let msg = `已撤回机器人发送的 ${recalled.length} 条消息:${recalled.join('、')}`;
892
871
  if (failed.length > 0)
@@ -897,8 +876,8 @@ function registerGroupManagementTools(caps, bundle) {
897
876
  logger.use.info('OneBot 群管理工具已注册');
898
877
  }
899
878
  // ===== 群信息查询工具 =====
900
- function registerGroupInfoTools(caps, storage, bundle) {
901
- const { platform, logger } = caps;
879
+ function registerGroupInfoTools(caps, bundle) {
880
+ const { platform, logger, sessionManager } = caps;
902
881
  const { daily } = bundle;
903
882
  // ---- 查看合并转发 ----
904
883
  daily.register({
@@ -906,7 +885,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
906
885
  type: 'function',
907
886
  function: {
908
887
  name: 'onebot_get_forward_msg',
909
- description: '读取合并转发消息内容(OneBot v11 标准 get_forward_msg)。收到 <forward id="...">[合并转发消息]</forward> 时,必须把尖括号里的 id 字符串作为参数 id 传入;不要使用 message_id、forward_id、res_id 或 m_resid。',
888
+ description: '读取合并转发消息内容(OneBot v11 标准 get_forward_msg)。收到 <forward id="...">[合并转发消息]</forward> 时,把尖括号里的 id 字符串作为参数 id 传入。',
910
889
  parameters: {
911
890
  type: 'object',
912
891
  properties: {
@@ -915,6 +894,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
915
894
  description: '合并转发 ID(forward 消息段的 data.id),与 OneBot v11 标准参数名一致',
916
895
  },
917
896
  limit: { type: 'number', description: '最多返回多少条节点,默认 30' },
897
+ self_id: SELF_ID_PARAM,
918
898
  },
919
899
  required: ['id'],
920
900
  },
@@ -923,27 +903,51 @@ function registerGroupInfoTools(caps, storage, bundle) {
923
903
  handler: async (args, callCtx) => {
924
904
  const selfId = resolveSelfId(platform, callCtx, args);
925
905
  if (!args.id)
926
- return '参数错误:缺少 id(合并转发 ID)。请使用 <forward id="..."> 中的 id 字符串,不要使用 message_id。';
906
+ throw new Error('参数错误:缺少 id(合并转发 ID)。请使用 <forward id="..."> 中的 id 字符串。');
927
907
  const forwardId = String(args.id);
928
908
  const limit = Math.max(1, Math.min(100, typeof args.limit === 'number' ? Math.floor(args.limit) : 30));
929
- // 适配器的 callAction 已对 get_forward_msg 做了多参数键回退(id/message_id/res_id/m_resid)
930
- // 并维护了一份接收时即抓取并展开(含摘要与图像识别)的缓存。
909
+ const readFailed = (err) => new Error(`合并转发读取失败:${err instanceof Error ? err.message : String(err)}`);
910
+ // 适配器的 callAction 维护了一份接收时即抓取并展开(含摘要与图像识别)的缓存,每个收到的转发都记着收到它的会话。
931
911
  let data;
932
912
  try {
933
913
  data = await callAction(platform, selfId, 'get_forward_msg', { id: forwardId });
934
914
  }
935
915
  catch (err) {
936
- return `合并转发读取失败:${err instanceof Error ? err.message : String(err)}。该转发可能已在协议端过期,或当前 OneBot 实现不支持跨会话读取。`;
916
+ throw readFailed(err);
937
917
  }
938
- // 适配器在缓存命中时返回 ForwardEntry 形状(含 fullText / summary / 元信息)。
939
- // 此时直接返回原文给 LLM,无需再做协议端结构解析。
940
- if (data && typeof data === 'object' && data.__aalisForwardEntry) {
941
- const entry = data;
942
- const header = `合并转发共 ${entry.count} 条,参与人:${entry.participants.join(', ') || '未知'}`;
918
+ // 适配器交回收到这条转发的会话:展开过的连同 ForwardEntry 形状(fullText / summary / 元信息与截断标志),没展开过的连同
919
+ // 按 id 现拉的办法。收到它的会话逐个参与者过会话管理的规则(同 onebot_get_msg),几处之一过得了就行,过了才拉;没记会话的
920
+ // (从没收到过的、别的适配器交回的原始数据)按判不出拒绝
921
+ const reply = (data && typeof data === 'object' ? data : {});
922
+ const recorded = reply.__aalisForwardEntry === true || reply.__aalisForwardReceipt === true;
923
+ const sessionIds = recorded && Array.isArray(reply.sessionIds) ? reply.sessionIds.map(String) : [];
924
+ const verdict = sessionIds.length === 0
925
+ ? { ok: false, reason: TAKE_UNKNOWN }
926
+ : await callCtx.approveFor(who => {
927
+ const verdicts = sessionIds.map(id => takeFrom(sessionManager, callCtx.sessionId, id, who));
928
+ return verdicts.find(v => v.ok) ?? verdicts[0];
929
+ });
930
+ if (!verdict.ok)
931
+ throw new Error(toSource(verdict.reason));
932
+ // 展开过的直接返回原文给 LLM,无需再做协议端结构解析
933
+ if (reply.__aalisForwardEntry === true) {
934
+ const entry = reply;
935
+ const cut = [entry.truncatedDepth && '嵌套超深', entry.truncatedNodes && '节点超限'].filter(Boolean);
936
+ // 参与人与入站信封同一写法:条目只列前 8 个,没列出的人数另记
937
+ const participants = [...entry.participants, ...(entry.unlisted ? [`...(+${entry.unlisted} 人未列出)`] : [])];
938
+ const header = `合并转发共 ${entry.count} 条,参与人:${participants.join(', ') || '未知'}` +
939
+ (cut.length ? `,已截断(${cut.join('、')})` : '');
943
940
  const summaryLine = entry.summary ? `\n摘要:${entry.summary}\n` : '\n';
944
941
  return `${header}${summaryLine}\n原文:\n${entry.fullText}`;
945
942
  }
946
- const formatContext = await recognizeForwardImages(caps, storage, selfId, data, limit);
943
+ // 没展开过的:过了判定才按 id 现拉
944
+ try {
945
+ data = await reply.fetch();
946
+ }
947
+ catch (err) {
948
+ throw readFailed(err);
949
+ }
950
+ const formatContext = await recognizeForwardImages(caps, selfId, data, limit);
947
951
  return formatForwardMessage(data, limit, formatContext);
948
952
  },
949
953
  });
@@ -958,6 +962,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
958
962
  type: 'object',
959
963
  properties: {
960
964
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
965
+ self_id: SELF_ID_PARAM,
961
966
  },
962
967
  required: [],
963
968
  },
@@ -983,6 +988,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
983
988
  properties: {
984
989
  user_id: { type: 'string', description: '可选:要查询的用户 QQ 号。缺省查询机器人自身。' },
985
990
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
991
+ self_id: SELF_ID_PARAM,
986
992
  },
987
993
  required: [],
988
994
  },
@@ -1014,6 +1020,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
1014
1020
  limit: { type: 'number', description: '本页最多返回条数,默认 30' },
1015
1021
  offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
1016
1022
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
1023
+ self_id: SELF_ID_PARAM,
1017
1024
  },
1018
1025
  required: [],
1019
1026
  },
@@ -1077,6 +1084,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
1077
1084
  type: 'string',
1078
1085
  description: '可选。指定群号。缺省时查当前会话所在群(私聊时缺省会报错,请显式传入)。',
1079
1086
  },
1087
+ self_id: SELF_ID_PARAM,
1080
1088
  },
1081
1089
  required: [],
1082
1090
  },
@@ -1088,9 +1096,8 @@ function registerGroupInfoTools(caps, storage, bundle) {
1088
1096
  const parsed = parseOneBotSession(callCtx.sessionId);
1089
1097
  let groupId = argGroupId;
1090
1098
  if (!groupId) {
1091
- if (parsed?.detailType !== 'group') {
1092
- return JSON.stringify({ available: false, reason: '当前不在群聊中,请显式传入 group_id' });
1093
- }
1099
+ if (parsed?.detailType !== 'group')
1100
+ throw new Error('当前不在群聊中,请显式传入 group_id');
1094
1101
  groupId = parsed.targetId;
1095
1102
  }
1096
1103
  let info = null;
@@ -1102,18 +1109,14 @@ function registerGroupInfoTools(caps, storage, bundle) {
1102
1109
  }));
1103
1110
  }
1104
1111
  catch (err) {
1105
- return JSON.stringify({
1106
- available: false,
1107
- reason: `查询失败: ${err instanceof Error ? err.message : String(err)}`,
1108
- });
1112
+ throw new Error(`查询失败: ${err instanceof Error ? err.message : String(err)}`);
1109
1113
  }
1110
1114
  if (!info)
1111
- return JSON.stringify({ available: false, reason: '无法获取群成员信息' });
1115
+ throw new Error('无法获取群成员信息');
1112
1116
  const ts = Number(info.shut_up_timestamp ?? 0);
1113
1117
  const nowSec = Math.floor(Date.now() / 1000);
1114
1118
  const muted = ts > nowSec;
1115
1119
  return JSON.stringify({
1116
- available: true,
1117
1120
  muted,
1118
1121
  shutUpTimestamp: ts,
1119
1122
  untilIso: muted ? new Date(ts * 1000).toISOString() : null,
@@ -1146,10 +1149,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
1146
1149
  handler: async (args) => {
1147
1150
  const supporting = getPlatformAdapters(platform).filter((a) => a.platform === 'onebot' && typeof a.getSelfMutes === 'function');
1148
1151
  if (supporting.length === 0) {
1149
- return JSON.stringify({
1150
- supported: false,
1151
- reason: '未找到支持记录自身禁言状态的 OneBot 适配器(getSelfMutes 不在平台契约内,第三方适配器可不提供)',
1152
- });
1152
+ throw new Error('未找到支持记录自身禁言状态的 OneBot 适配器(getSelfMutes 不在平台契约内,第三方适配器可不提供)');
1153
1153
  }
1154
1154
  const limit = Math.max(1, Math.min(200, Math.floor(Number(args.limit) || 30)));
1155
1155
  const offset = Math.max(0, Math.floor(Number(args.offset) || 0));
@@ -1157,7 +1157,6 @@ function registerGroupInfoTools(caps, storage, bundle) {
1157
1157
  const total = mutes.length;
1158
1158
  const sliced = mutes.slice(offset, offset + limit);
1159
1159
  return JSON.stringify({
1160
- supported: true,
1161
1160
  total,
1162
1161
  offset,
1163
1162
  limit,
@@ -1178,11 +1177,12 @@ function registerGroupInfoTools(caps, storage, bundle) {
1178
1177
  type: 'function',
1179
1178
  function: {
1180
1179
  name: 'onebot_get_msg',
1181
- description: '按 message_id 获取单条消息的完整内容(包括发送者、消息段等)。可用于回溯查看历史中的某条消息,或在收到撤回通知后查看被撤回的内容。不限定会话类型,群聊和私聊消息均可查询。',
1180
+ description: '按 message_id 获取单条消息的完整内容(包括发送者、消息段等)。可用于回溯查看历史中的某条消息,或在收到撤回通知后查看被撤回的内容。消息所在的会话按会话管理的跨会话规则判(默认不取别人私聊里的消息)。',
1182
1181
  parameters: {
1183
1182
  type: 'object',
1184
1183
  properties: {
1185
1184
  message_id: { type: 'string', description: '消息 ID' },
1185
+ self_id: SELF_ID_PARAM,
1186
1186
  },
1187
1187
  required: ['message_id'],
1188
1188
  },
@@ -1191,16 +1191,25 @@ function registerGroupInfoTools(caps, storage, bundle) {
1191
1191
  handler: async (args, callCtx) => {
1192
1192
  const selfId = resolveSelfId(platform, callCtx, args);
1193
1193
  if (!args.message_id)
1194
- return '参数错误:缺少 message_id';
1194
+ throw new Error('参数错误:缺少 message_id');
1195
+ let data;
1195
1196
  try {
1196
- const data = await callAction(platform, selfId, 'get_msg', {
1197
+ data = (await callAction(platform, selfId, 'get_msg', {
1197
1198
  message_id: Number(args.message_id),
1198
- });
1199
- return JSON.stringify(data);
1199
+ }));
1200
1200
  }
1201
1201
  catch (err) {
1202
- return `获取消息失败:${err instanceof Error ? err.message : String(err)}。该消息可能已过期或不在当前 OneBot 实现的作用域内。`;
1202
+ throw new Error(`获取消息失败:${err instanceof Error ? err.message : String(err)}`);
1203
1203
  }
1204
+ // 取到的消息按所在的窗口拼出会话 id 过规则:拼不出(她自己发的私聊消息、实现端不给字段)的按判不出拒绝
1205
+ const target = sessionOfMessage(selfId, data);
1206
+ // 逐个参与者过会话管理的规则:有人过不了(某个参与者自己私聊里的)时发一道参与者审批,由过得了的人批
1207
+ const verdict = target === undefined
1208
+ ? { ok: false, reason: TAKE_UNKNOWN }
1209
+ : await callCtx.approveFor(who => takeFrom(sessionManager, callCtx.sessionId, target, who));
1210
+ if (!verdict.ok)
1211
+ throw new Error(toSource(verdict.reason));
1212
+ return JSON.stringify(data);
1204
1213
  },
1205
1214
  });
1206
1215
  // ---- 获取群荣誉信息 ----
@@ -1219,6 +1228,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
1219
1228
  description: '荣誉类型:talkative=龙王, performer=群聊之火, legend=群聊炽焰, strong_newbie=冒尖小春笋, emotion=快乐之源, all=全部。默认 all。',
1220
1229
  },
1221
1230
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
1231
+ self_id: SELF_ID_PARAM,
1222
1232
  },
1223
1233
  required: [],
1224
1234
  },
@@ -1256,6 +1266,7 @@ function registerAccountTools(caps, bundle) {
1256
1266
  keyword: { type: 'string', description: '可选:按群名或群号子串模糊匹配(不区分大小写)' },
1257
1267
  limit: { type: 'number', description: '本页最多返回条数,默认 30' },
1258
1268
  offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
1269
+ self_id: SELF_ID_PARAM,
1259
1270
  },
1260
1271
  required: [],
1261
1272
  },
@@ -1305,6 +1316,7 @@ function registerAccountTools(caps, bundle) {
1305
1316
  keyword: { type: 'string', description: '可选:按昵称、备注或 QQ 号子串模糊匹配(不区分大小写)' },
1306
1317
  limit: { type: 'number', description: '本页最多返回条数,默认 30' },
1307
1318
  offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
1319
+ self_id: SELF_ID_PARAM,
1308
1320
  },
1309
1321
  required: [],
1310
1322
  },
@@ -1353,6 +1365,7 @@ function registerAccountTools(caps, bundle) {
1353
1365
  properties: {
1354
1366
  user_id: { type: 'string', description: '要查询的 QQ 号' },
1355
1367
  no_cache: { type: 'boolean', description: '是否跳过缓存强制刷新,默认 false' },
1368
+ self_id: SELF_ID_PARAM,
1356
1369
  },
1357
1370
  required: ['user_id'],
1358
1371
  },
@@ -1374,11 +1387,11 @@ function registerAccountTools(caps, bundle) {
1374
1387
  function: {
1375
1388
  name: 'onebot_get_login_info',
1376
1389
  description: '显式查询当前 OneBot 连接的登录账号信息(QQ 号、昵称)。仅在用户要求核实账号、诊断连接或需要最新平台返回值时使用。',
1377
- parameters: { type: 'object', properties: {}, required: [] },
1390
+ parameters: { type: 'object', properties: { self_id: SELF_ID_PARAM }, required: [] },
1378
1391
  },
1379
1392
  },
1380
- handler: async (_args, callCtx) => {
1381
- const selfId = resolveSelfId(platform, callCtx, _args);
1393
+ handler: async (args, callCtx) => {
1394
+ const selfId = resolveSelfId(platform, callCtx, args);
1382
1395
  const data = await callAction(platform, selfId, 'get_login_info', {});
1383
1396
  return JSON.stringify(data);
1384
1397
  },
@@ -1394,6 +1407,7 @@ function registerAccountTools(caps, bundle) {
1394
1407
  type: 'object',
1395
1408
  properties: {
1396
1409
  user_id: { type: 'string', description: '要删除的好友 QQ 号;在私聊中可省略,默认当前对话用户' },
1410
+ self_id: SELF_ID_PARAM,
1397
1411
  },
1398
1412
  required: [],
1399
1413
  },
@@ -1405,7 +1419,7 @@ function registerAccountTools(caps, bundle) {
1405
1419
  const parsed = parseOneBotSession(callCtx.sessionId);
1406
1420
  const userId = argUserId || (parsed?.detailType === 'private' ? parsed.targetId : '');
1407
1421
  if (!userId)
1408
- return '请提供 user_id,或在要删除的好友私聊中调用此工具';
1422
+ throw new Error('请提供 user_id,或在要删除的好友私聊中调用此工具');
1409
1423
  await callAction(platform, selfId, 'delete_friend', {
1410
1424
  user_id: Number(userId),
1411
1425
  });
@@ -1436,6 +1450,7 @@ function registerInteractionTools(caps, bundle) {
1436
1450
  type: 'string',
1437
1451
  description: '可选:目标群号。群聊省略则使用当前群;如显式传值则跨群操作。',
1438
1452
  },
1453
+ self_id: SELF_ID_PARAM,
1439
1454
  },
1440
1455
  },
1441
1456
  },
@@ -1451,18 +1466,10 @@ function registerInteractionTools(caps, bundle) {
1451
1466
  if (!argUserId) {
1452
1467
  throw new Error('群聊戳一戳必须显式提供 user_id(要戳的对象)。');
1453
1468
  }
1454
- try {
1455
- await callAction(platform, selfId, 'group_poke', {
1456
- group_id: Number(groupId),
1457
- user_id: Number(argUserId),
1458
- });
1459
- }
1460
- catch {
1461
- await callAction(platform, selfId, 'send_group_poke', {
1462
- group_id: Number(groupId),
1463
- user_id: Number(argUserId),
1464
- });
1465
- }
1469
+ await callFirstSupported(platform, selfId, ['group_poke', 'send_group_poke'], {
1470
+ group_id: Number(groupId),
1471
+ user_id: Number(argUserId),
1472
+ });
1466
1473
  return `已在群 ${groupId} 戳了 ${argUserId} 一下`;
1467
1474
  }
1468
1475
  // 2) 私聊场景:从 user_id 或当前私聊会话对方取目标
@@ -1470,13 +1477,7 @@ function registerInteractionTools(caps, bundle) {
1470
1477
  if (!userId) {
1471
1478
  throw new Error('无法判断戳一戳目标:当前不在 OneBot 会话内,请显式传入 group_id+user_id 或 user_id。');
1472
1479
  }
1473
- // NapCat: friend_poke { user_id };其他实现可能用 send_poke
1474
- try {
1475
- await callAction(platform, selfId, 'friend_poke', { user_id: Number(userId) });
1476
- }
1477
- catch {
1478
- await callAction(platform, selfId, 'send_poke', { user_id: Number(userId) });
1479
- }
1480
+ await callFirstSupported(platform, selfId, ['friend_poke', 'send_poke'], { user_id: Number(userId) });
1480
1481
  return `已戳了 ${userId} 一下`;
1481
1482
  },
1482
1483
  });
@@ -1492,6 +1493,7 @@ function registerInteractionTools(caps, bundle) {
1492
1493
  properties: {
1493
1494
  user_id: { type: 'string', description: '要赞的用户 QQ 号' },
1494
1495
  times: { type: 'number', description: '赞的次数,默认 1,每天每个好友最多 10 次' },
1496
+ self_id: SELF_ID_PARAM,
1495
1497
  },
1496
1498
  required: ['user_id'],
1497
1499
  },
@@ -1518,6 +1520,7 @@ function registerInteractionTools(caps, bundle) {
1518
1520
  type: 'object',
1519
1521
  properties: {
1520
1522
  group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
1523
+ self_id: SELF_ID_PARAM,
1521
1524
  },
1522
1525
  required: [],
1523
1526
  },
@@ -1533,52 +1536,6 @@ function registerInteractionTools(caps, bundle) {
1533
1536
  });
1534
1537
  logger.use.info('OneBot 特殊交互工具已注册');
1535
1538
  }
1536
- /**
1537
- * 把 OneBot 的细粒度访问规则注册到 session-history 服务。
1538
- * 通用工具 session_get_history 和平台专属工具 onebot_get_session_history
1539
- * 调用 service 时都会走这条规则链 —— 不存在绕过路径。
1540
- */
1541
- function registerOneBotHistoryAccessChecker(caps, cfg) {
1542
- const { logger, sessionHistory } = caps;
1543
- const checker = {
1544
- platform: 'onebot',
1545
- check({ currentSessionId, targetSessionId }) {
1546
- const target = parseOneBotSession(targetSessionId);
1547
- if (!target)
1548
- return undefined; // 不是合法 onebot 目标, 不表态
1549
- const current = parseOneBotSession(currentSessionId);
1550
- // 跨平台调用(current 不是 onebot)走默认放行: session-history scope 已粗筛
1551
- if (!current)
1552
- return undefined;
1553
- if (current.selfId !== target.selfId && !cfg.allowCrossSelf) {
1554
- return { decision: 'deny', reason: '当前 OneBot 配置不允许跨机器人账号读取会话历史' };
1555
- }
1556
- if (current.detailType === 'group' && target.detailType === 'private' && !cfg.allowGroupReadPrivate) {
1557
- return { decision: 'deny', reason: '当前 OneBot 配置不允许从群聊读取私聊历史' };
1558
- }
1559
- if (current.detailType === 'group' &&
1560
- target.detailType === 'group' &&
1561
- current.targetId !== target.targetId &&
1562
- !cfg.allowCrossGroup) {
1563
- return { decision: 'deny', reason: '当前 OneBot 配置不允许跨群读取其他群历史' };
1564
- }
1565
- if (current.detailType === 'private' &&
1566
- target.detailType === 'private' &&
1567
- current.targetId !== target.targetId &&
1568
- !cfg.allowCrossPrivate) {
1569
- return { decision: 'deny', reason: '当前 OneBot 配置不允许跨私聊读取其他私聊历史' };
1570
- }
1571
- return undefined;
1572
- },
1573
- };
1574
- // 规则登记在 session-history 提供者实例的局部状态里:提供者重启或换人后,新实例身上没有这条规则,
1575
- // 须跟随提供者重挂;返回的注销作为跟随清理,本插件 bounce / 卸载时一并撤回,不在下层泄漏。
1576
- sessionHistory.follow(svc => {
1577
- const dispose = svc.registerAccessChecker(checker);
1578
- logger.use.info('OneBot 会话历史访问规则已注册到 session-history');
1579
- return dispose;
1580
- });
1581
- }
1582
1539
  function registerSessionHistoryTools(caps, bundle, cfg) {
1583
1540
  const { platform, logger, sessionHistory } = caps;
1584
1541
  const { daily } = bundle;
@@ -1597,7 +1554,7 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
1597
1554
  description: 'group = 群聊,private = 私聊',
1598
1555
  },
1599
1556
  target_id: { type: 'string', description: '群号或 QQ 号' },
1600
- self_id: { type: 'string', description: '机器人账号。可选,默认使用当前 OneBot 会话的 selfId' },
1557
+ self_id: SELF_ID_PARAM,
1601
1558
  },
1602
1559
  required: ['target_type', 'target_id'],
1603
1560
  additionalProperties: false,
@@ -1605,26 +1562,14 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
1605
1562
  },
1606
1563
  },
1607
1564
  handler: async (args, callCtx) => {
1608
- const current = parseOneBotSession(callCtx.sessionId);
1609
1565
  const targetType = String(args.target_type ?? '').toLowerCase();
1610
1566
  const targetId = String(args.target_id ?? '').trim();
1611
1567
  if (targetType !== 'group' && targetType !== 'private') {
1612
- return JSON.stringify({ error: `target_type 必须为 group 或 private,收到 ${args.target_type}` });
1568
+ throw new Error(`target_type 必须为 group 或 private,收到 ${args.target_type}`);
1613
1569
  }
1614
1570
  if (!targetId)
1615
- return JSON.stringify({ error: 'target_id 不能为空' });
1616
- const argSelfId = args.self_id ? String(args.self_id).trim() : '';
1617
- const selfId = argSelfId || current?.selfId || getAnyOneBotSelfId(platform) || '';
1618
- if (!selfId)
1619
- return JSON.stringify({ error: '无法确定 self_id,且未找到可用的 OneBot 连接' });
1620
- // 跨账号限制:仅在当前也是 OneBot 会话时才作为参考点检查
1621
- if (current && !cfg.allowCrossSelf && selfId !== current.selfId) {
1622
- return JSON.stringify({
1623
- error: '当前配置不允许跨机器人账号解析会话',
1624
- currentSelfId: current.selfId,
1625
- requestedSelfId: selfId,
1626
- });
1627
- }
1571
+ throw new Error('target_id 不能为空');
1572
+ const selfId = resolveSelfId(platform, callCtx, args);
1628
1573
  return JSON.stringify({
1629
1574
  ok: true,
1630
1575
  sessionId: buildOneBotSessionId(selfId, targetType, targetId),
@@ -1642,7 +1587,7 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
1642
1587
  description: [
1643
1588
  '按 QQ 群号或 QQ 号读取对应 OneBot 会话最近若干条历史消息。',
1644
1589
  '适用场景:用户明确要求你回忆某个群/私聊最近说过什么,或需要核实另一个 OneBot 会话的原文上下文。',
1645
- '默认禁止从群聊读取私聊历史;如需语义搜索历史,请优先使用 memory_recall。',
1590
+ '能读哪些会话按会话管理的跨会话规则(默认不读别人的私聊);如需语义搜索历史,请优先使用 memory_recall。',
1646
1591
  ].join('\n'),
1647
1592
  parameters: {
1648
1593
  type: 'object',
@@ -1657,7 +1602,7 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
1657
1602
  type: 'number',
1658
1603
  description: `读取最近多少条,默认 ${cfg.defaultLimit},最多 ${cfg.maxLimit}`,
1659
1604
  },
1660
- self_id: { type: 'string', description: '机器人账号。可选,默认使用当前 OneBot 会话的 selfId' },
1605
+ self_id: SELF_ID_PARAM,
1661
1606
  },
1662
1607
  required: ['target_type', 'target_id'],
1663
1608
  additionalProperties: false,
@@ -1665,44 +1610,40 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
1665
1610
  },
1666
1611
  },
1667
1612
  handler: async (args, callCtx) => {
1668
- const current = parseOneBotSession(callCtx.sessionId);
1669
1613
  const targetType = String(args.target_type ?? '').toLowerCase();
1670
1614
  const targetId = String(args.target_id ?? '').trim();
1671
1615
  if (targetType !== 'group' && targetType !== 'private') {
1672
- return JSON.stringify({ error: `target_type 必须为 group 或 private,收到 ${args.target_type}` });
1616
+ throw new Error(`target_type 必须为 group 或 private,收到 ${args.target_type}`);
1673
1617
  }
1674
1618
  if (!targetId)
1675
- return JSON.stringify({ error: 'target_id 不能为空' });
1676
- const argSelfId = args.self_id ? String(args.self_id).trim() : '';
1677
- const selfId = argSelfId || current?.selfId || getAnyOneBotSelfId(platform) || '';
1678
- if (!selfId)
1679
- return JSON.stringify({ error: '无法确定 self_id,且未找到可用的 OneBot 连接' });
1619
+ throw new Error('target_id 不能为空');
1620
+ const selfId = resolveSelfId(platform, callCtx, args);
1680
1621
  if (!sessionHistory.present)
1681
- return JSON.stringify({
1682
- error: 'session-history 服务不可用,请启用 @aalis/plugin-tool-session',
1683
- });
1622
+ throw new Error('session-history 服务不可用,请启用 @aalis/plugin-tool-session');
1684
1623
  const history = sessionHistory.use;
1685
1624
  const limit = Math.max(1, Math.min(cfg.maxLimit, Math.floor(Number(args.limit) || cfg.defaultLimit)));
1686
1625
  const sessionId = buildOneBotSessionId(selfId, targetType, targetId);
1626
+ let result;
1687
1627
  try {
1688
- // 访问控制(跨账号/群读私/跨群/跨私)由 session-history 服务统一执行,
1689
- // OneBot 的规则已通过 registerOneBotHistoryAccessChecker 注册。
1628
+ // 能不能读由 session-history 服务按会话管理的规则判(带这次调用的授权身份)。
1690
1629
  // includeArchived 故意不暴露:archived 是 memory 内部细节,被 summary 替代的老消息
1691
1630
  // 默认就不该再翻原文(会与 summary 内容双重计费)。如需考古应另起独立工具。
1692
- const result = await history.getHistory({ sessionId, limit }, callCtx);
1693
- return JSON.stringify({
1694
- ...result,
1695
- sessionId,
1696
- selfId,
1697
- targetType,
1698
- targetId,
1699
- });
1631
+ result = await history.getHistory({ sessionId, limit }, callCtx);
1700
1632
  }
1701
1633
  catch (err) {
1702
1634
  const message = err instanceof Error ? err.message : String(err);
1703
1635
  logger.use.warn(`onebot_get_session_history 失败 (${sessionId}): ${message}`);
1704
- return JSON.stringify({ error: `读取 OneBot 会话历史失败: ${message}` });
1636
+ throw new Error(`读取 OneBot 会话历史失败: ${message}`);
1705
1637
  }
1638
+ if ('error' in result)
1639
+ throw new Error(result.error);
1640
+ return JSON.stringify({
1641
+ ...result,
1642
+ sessionId,
1643
+ selfId,
1644
+ targetType,
1645
+ targetId,
1646
+ });
1706
1647
  },
1707
1648
  });
1708
1649
  logger.use.info('OneBot 会话历史工具已注册');
@@ -1711,14 +1652,21 @@ function registerRequestTools(caps, bundle) {
1711
1652
  const { platform, logger } = caps;
1712
1653
  const { group, personal } = bundle;
1713
1654
  /**
1714
- * 处理申请的 OneBot 适配器:申请挂在收到它的实例上,按当前会话(或 self_id)的账号路由。
1715
- * 申请的通知注入在收到它的账号的会话里,在那里调用即落到对的实例
1655
+ * 处理申请的账号与 OneBot 适配器:申请挂在收到它的实例上,按当前会话(或 self_id)的账号路由。
1656
+ * 申请的通知注入在收到它的账号的会话里,在那里调用即落到对的实例与账号
1716
1657
  */
1717
- async function findRequestAdapter(callCtx, args) {
1718
- const entry = await resolvePlatformBySession(platform, selfSessionId(resolveSelfId(platform, callCtx, args)));
1719
- const adapter = entry?.use;
1720
- return typeof adapter?.handleFriendRequest === 'function' ? adapter : undefined;
1658
+ async function requestHandler(callCtx, args) {
1659
+ const selfId = resolveSelfId(platform, callCtx, args);
1660
+ const adapter = (await resolvePlatformBySession(platform, selfSessionId(selfId)))?.use;
1661
+ if (typeof adapter?.handleFriendRequest !== 'function')
1662
+ throw new Error('未找到支持请求处理的 OneBot 适配器');
1663
+ return { selfId, adapter: adapter };
1721
1664
  }
1665
+ /** 两个群请求工具各认自己的申请类型:用审批加群申请的工具批不了入群邀请,反之亦然 */
1666
+ const handleGroupRequest = (subType) => async (args, callCtx) => {
1667
+ const { selfId, adapter } = await requestHandler(callCtx, args);
1668
+ return adapter.handleGroupRequest(selfId, subType, String(args.user_id), String(args.group_id), !!args.approve, args.reason ? String(args.reason) : undefined);
1669
+ };
1722
1670
  personal.register({
1723
1671
  definition: {
1724
1672
  type: 'function',
@@ -1731,22 +1679,21 @@ function registerRequestTools(caps, bundle) {
1731
1679
  user_id: { type: 'string', description: '发起好友申请的用户 QQ 号' },
1732
1680
  approve: { type: 'boolean', description: 'true = 同意,false = 拒绝' },
1733
1681
  remark: { type: 'string', description: '同意后添加的备注(可选)' },
1682
+ self_id: SELF_ID_PARAM,
1734
1683
  },
1735
1684
  required: ['user_id', 'approve'],
1736
1685
  },
1737
1686
  },
1738
1687
  },
1739
1688
  handler: async (args, callCtx) => {
1740
- const adapter = await findRequestAdapter(callCtx, args);
1741
- if (!adapter)
1742
- return '未找到支持请求处理的 OneBot 适配器';
1743
- return adapter.handleFriendRequest(String(args.user_id), !!args.approve, args.remark ? String(args.remark) : undefined);
1689
+ const { selfId, adapter } = await requestHandler(callCtx, args);
1690
+ return adapter.handleFriendRequest(selfId, String(args.user_id), !!args.approve, args.remark ? String(args.remark) : undefined);
1744
1691
  },
1745
1692
  });
1746
1693
  // 群相关请求拆成两个工具,对应 OneBot v11 set_group_add_request 的两种 sub_type:
1747
1694
  // - approve_join_request: sub_type=add 「别人申请加入 bot 管理的群」 → 群务
1748
1695
  // - handle_group_invite: sub_type=invite 「别人邀请 bot 加入群」 → 个人/私聊
1749
- // 底层共用同一个适配器方法(pendingGroupRequests 内部已记录 subType),区别在于语义与权限归属。
1696
+ // 底层共用同一个适配器方法,各自把要处理的申请类型交给它,类型不符就报错。
1750
1697
  group.register({
1751
1698
  definition: {
1752
1699
  type: 'function',
@@ -1760,17 +1707,13 @@ function registerRequestTools(caps, bundle) {
1760
1707
  group_id: { type: 'string', description: '相关群号' },
1761
1708
  approve: { type: 'boolean', description: 'true = 同意加群,false = 拒绝' },
1762
1709
  reason: { type: 'string', description: '拒绝理由(拒绝时有效,可选)' },
1710
+ self_id: SELF_ID_PARAM,
1763
1711
  },
1764
1712
  required: ['user_id', 'group_id', 'approve'],
1765
1713
  },
1766
1714
  },
1767
1715
  },
1768
- handler: async (args, callCtx) => {
1769
- const adapter = await findRequestAdapter(callCtx, args);
1770
- if (!adapter)
1771
- return '未找到支持请求处理的 OneBot 适配器';
1772
- return adapter.handleGroupRequest(String(args.user_id), String(args.group_id), !!args.approve, args.reason ? String(args.reason) : undefined);
1773
- },
1716
+ handler: handleGroupRequest('add'),
1774
1717
  });
1775
1718
  personal.register({
1776
1719
  definition: {
@@ -1785,17 +1728,13 @@ function registerRequestTools(caps, bundle) {
1785
1728
  group_id: { type: 'string', description: '被邀请加入的群号' },
1786
1729
  approve: { type: 'boolean', description: 'true = 接受邀请并加入群,false = 拒绝' },
1787
1730
  reason: { type: 'string', description: '拒绝理由(拒绝时有效,可选)' },
1731
+ self_id: SELF_ID_PARAM,
1788
1732
  },
1789
1733
  required: ['user_id', 'group_id', 'approve'],
1790
1734
  },
1791
1735
  },
1792
1736
  },
1793
- handler: async (args, callCtx) => {
1794
- const adapter = await findRequestAdapter(callCtx, args);
1795
- if (!adapter)
1796
- return '未找到支持请求处理的 OneBot 适配器';
1797
- return adapter.handleGroupRequest(String(args.user_id), String(args.group_id), !!args.approve, args.reason ? String(args.reason) : undefined);
1798
- },
1737
+ handler: handleGroupRequest('invite'),
1799
1738
  });
1800
1739
  logger.use.info('OneBot 请求处理工具已注册');
1801
1740
  }