@aalis/plugin-tool-onebot 0.12.0 → 0.13.1
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.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +292 -350
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts +30 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/en.js +30 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/texts.d.ts +59 -0
- package/dist/texts.d.ts.map +1 -0
- package/dist/texts.js +33 -0
- package/dist/texts.js.map +1 -0
- package/package.json +12 -11
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 {
|
|
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: '
|
|
61
|
+
label: t('sessionHistoryEnabledLabel'),
|
|
62
62
|
default: true,
|
|
63
|
-
description: '
|
|
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: '
|
|
69
|
+
description: t('maxLimitDescription'),
|
|
70
70
|
},
|
|
71
71
|
defaultLimit: {
|
|
72
72
|
type: 'number',
|
|
73
|
-
label: '
|
|
73
|
+
label: t('defaultLimitLabel'),
|
|
74
74
|
default: 20,
|
|
75
|
-
description: '
|
|
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
|
-
/**
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
*
|
|
137
|
-
*
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
178
|
+
* 按顺序调候选动作名,只有实现端回不支持该动作时才换下一个;其余错误(超时、没权限、对方不在群)原样抛出,不换个动作再做
|
|
179
|
+
* 一次。戳一戳的动作名各实现端不同:NapCat 是 group_poke、friend_poke 与 send_poke,没有 send_group_poke,群里在 NapCat 上
|
|
180
|
+
* 只有 group_poke 一个,后面的候选留给别的实现端
|
|
199
181
|
*/
|
|
200
|
-
async function
|
|
201
|
-
|
|
202
|
-
return null;
|
|
182
|
+
async function callFirstSupported(platform, selfId, actions, params) {
|
|
183
|
+
const [action, ...rest] = actions;
|
|
203
184
|
try {
|
|
204
|
-
|
|
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
|
-
|
|
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,33 @@ 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
|
-
|
|
292
|
-
|
|
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
|
|
298
|
-
|
|
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
|
-
|
|
294
|
+
/** signal 是这次工具调用的,交给识图 */
|
|
295
|
+
async function recognizeForwardImages(caps, selfId, data, limit, signal) {
|
|
314
296
|
const refs = collectForwardImageRefs(data, limit);
|
|
315
297
|
const imageDescriptions = new Map();
|
|
316
298
|
if (refs.length === 0)
|
|
@@ -328,12 +310,16 @@ async function recognizeForwardImages(caps, storage, selfId, data, limit) {
|
|
|
328
310
|
continue;
|
|
329
311
|
seen.add(key);
|
|
330
312
|
try {
|
|
331
|
-
const imageSource = await resolveForwardImageSource(caps,
|
|
332
|
-
|
|
313
|
+
const imageSource = await resolveForwardImageSource(caps, selfId, ref);
|
|
314
|
+
if (!imageSource)
|
|
315
|
+
continue;
|
|
316
|
+
const description = await mediaService.describeImage(imageSource, { signal });
|
|
333
317
|
if (description)
|
|
334
318
|
imageDescriptions.set(key, description);
|
|
335
319
|
}
|
|
336
320
|
catch (err) {
|
|
321
|
+
// 中止时后面的图不再识别,也不把缺了描述的正文当结果交出:抛中止原因
|
|
322
|
+
signal?.throwIfAborted();
|
|
337
323
|
logger.use.debug(`合并转发图片识别失败 (${ref.source}): ${err}`);
|
|
338
324
|
}
|
|
339
325
|
}
|
|
@@ -394,6 +380,10 @@ function formatCqMessageContent(content, context) {
|
|
|
394
380
|
}
|
|
395
381
|
function formatForwardMessage(data, limit, context) {
|
|
396
382
|
const rawMessages = getForwardMessages(data);
|
|
383
|
+
if (!rawMessages)
|
|
384
|
+
throw new Error('合并转发的返回格式认不出');
|
|
385
|
+
if (!rawMessages.length)
|
|
386
|
+
return '合并转发内容为空';
|
|
397
387
|
const messages = rawMessages.slice(0, limit);
|
|
398
388
|
const lines = messages.map((item, index) => {
|
|
399
389
|
const node = item && typeof item === 'object' ? item : {};
|
|
@@ -414,61 +404,60 @@ function formatForwardMessage(data, limit, context) {
|
|
|
414
404
|
return `${index + 1}. ${prefix}: ${content || '[空消息]'}`;
|
|
415
405
|
});
|
|
416
406
|
const header = `合并转发共 ${rawMessages.length} 条${rawMessages.length > messages.length ? `,以下显示前 ${messages.length} 条` : ''}:`;
|
|
417
|
-
return
|
|
407
|
+
return `${header}\n${lines.join('\n')}`;
|
|
418
408
|
}
|
|
419
409
|
// ===== 权限检查辅助 =====
|
|
420
410
|
const ROLE_LEVEL = { owner: 3, admin: 2, member: 1 };
|
|
421
411
|
function roleLevel(role) {
|
|
422
412
|
return ROLE_LEVEL[role] ?? 0;
|
|
423
413
|
}
|
|
424
|
-
/**
|
|
414
|
+
/** 查询群成员信息,失败时抛出适配器的原话 */
|
|
425
415
|
async function getGroupMemberInfo(platform, selfId, groupId, userId) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}));
|
|
432
|
-
}
|
|
433
|
-
catch {
|
|
434
|
-
return null;
|
|
435
|
-
}
|
|
416
|
+
return (await callAction(platform, selfId, 'get_group_member_info', {
|
|
417
|
+
group_id: Number(groupId),
|
|
418
|
+
user_id: Number(userId),
|
|
419
|
+
no_cache: true,
|
|
420
|
+
}));
|
|
436
421
|
}
|
|
437
|
-
/**
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
422
|
+
/**
|
|
423
|
+
* 机器人在群里的角色够不够做这个管理操作,不够时抛错。取不到机器人自身信息或其中没有角色时放过检查:真不够权限时实现端的
|
|
424
|
+
* 返回码会报出来。目标成员的信息或角色取不到(查询失败,或实现端回成功却没给)时报错,不当成普通成员
|
|
425
|
+
*/
|
|
426
|
+
async function assertAdminPermission(platform, selfId, groupId, targetUserId, requireOwner) {
|
|
427
|
+
const selfInfo = await getGroupMemberInfo(platform, selfId, groupId, selfId).catch(() => null);
|
|
428
|
+
if (!selfInfo?.role)
|
|
429
|
+
return;
|
|
430
|
+
const selfRole = String(selfInfo.role);
|
|
443
431
|
if (requireOwner && selfRole !== 'owner') {
|
|
444
|
-
|
|
432
|
+
throw new Error(`操作失败:此操作仅群主可执行(当前角色:${selfRole})`);
|
|
445
433
|
}
|
|
446
434
|
if (selfRole === 'member') {
|
|
447
|
-
|
|
435
|
+
throw new Error(`操作失败:机器人不是管理员(当前角色:${selfRole}),无法执行管理操作`);
|
|
448
436
|
}
|
|
449
437
|
if (targetUserId) {
|
|
450
|
-
const targetInfo = await getGroupMemberInfo(platform, selfId, groupId, targetUserId)
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
438
|
+
const targetInfo = await getGroupMemberInfo(platform, selfId, groupId, targetUserId).catch((err) => {
|
|
439
|
+
throw new Error(`操作失败:无法获取用户 ${targetUserId} 的信息:${err instanceof Error ? err.message : String(err)}`);
|
|
440
|
+
});
|
|
441
|
+
if (!targetInfo)
|
|
442
|
+
throw new Error(`操作失败:无法获取用户 ${targetUserId} 的信息:实现端没给成员信息`);
|
|
443
|
+
if (!targetInfo.role)
|
|
444
|
+
throw new Error(`操作失败:无法获取用户 ${targetUserId} 的信息:实现端没给成员角色`);
|
|
445
|
+
const targetRole = String(targetInfo.role);
|
|
455
446
|
if (roleLevel(targetRole) >= roleLevel(selfRole)) {
|
|
456
447
|
const targetLabel = targetRole === 'owner' ? '群主' : '管理员';
|
|
457
|
-
|
|
448
|
+
throw new Error(`操作失败:无法对${targetLabel}执行此操作(机器人角色:${selfRole},目标角色:${targetRole})`);
|
|
458
449
|
}
|
|
459
450
|
}
|
|
460
|
-
return null;
|
|
461
451
|
}
|
|
462
452
|
export default definePlugin({
|
|
463
453
|
name: '@aalis/plugin-tool-onebot',
|
|
464
|
-
displayName: '
|
|
454
|
+
displayName: t('displayName'),
|
|
465
455
|
subsystem: 'tools',
|
|
466
456
|
configSchema,
|
|
467
457
|
uses,
|
|
468
458
|
apply(caps) {
|
|
469
459
|
const { tools, events, logger, platform } = caps;
|
|
470
460
|
const cfg = parseConfig(configSchema, caps.config.use, logger.use);
|
|
471
|
-
const storage = caps.storage.use;
|
|
472
461
|
const bundle = {
|
|
473
462
|
daily: withToolGroups(tools.use, ['onebot-daily']),
|
|
474
463
|
group: withToolGroups(tools.use, ['onebot-group']),
|
|
@@ -476,17 +465,7 @@ export default definePlugin({
|
|
|
476
465
|
};
|
|
477
466
|
const maxLimit = Math.max(1, Math.min(1000, cfg.sessionHistory.maxLimit || 100));
|
|
478
467
|
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);
|
|
468
|
+
const historyCfg = { maxLimit, defaultLimit: Math.min(defaultLimitRaw, maxLimit) };
|
|
490
469
|
// 仅当 OneBot 平台可用时才注册工具。platform 是可选的多提供者服务,OneBot 适配器可能晚于
|
|
491
470
|
// app:ready 上线(WebUI 启用、市场热装、启动失败后修好),且作为非胜者上线时 follow 不触发:
|
|
492
471
|
// 故在 app:ready 与每次 platform 提供者注册时各判一次,出现 onebot 即注册,之后不再重复。
|
|
@@ -500,23 +479,26 @@ export default definePlugin({
|
|
|
500
479
|
// 注册工具分组:按职责语义切分,彼此不重叠
|
|
501
480
|
tools.use.registerGroup({
|
|
502
481
|
name: 'onebot-daily',
|
|
503
|
-
label: '
|
|
482
|
+
label: t('dailyGroup'),
|
|
483
|
+
summary: t('dailyGroupSummary'),
|
|
504
484
|
description: '只读查询(群信息/成员/好友列表/历史/登录信息)+ 日常低风险互动(戳一戳、好友赞)',
|
|
505
485
|
});
|
|
506
486
|
tools.use.registerGroup({
|
|
507
487
|
name: 'onebot-group',
|
|
508
|
-
label: '
|
|
488
|
+
label: t('groupGroup'),
|
|
489
|
+
summary: t('groupGroupSummary'),
|
|
509
490
|
description: '群务管理:禁言/全员禁言/踢人/撤回消息/设置群名片/改群名/设管理员/群打卡/审批加群申请 等',
|
|
510
491
|
});
|
|
511
492
|
tools.use.registerGroup({
|
|
512
493
|
name: 'onebot-personal',
|
|
513
|
-
label: '
|
|
494
|
+
label: t('personalGroup'),
|
|
495
|
+
summary: t('personalGroupSummary'),
|
|
514
496
|
description: '影响 bot 账号本身的人际关系:退群、删好友、处理好友申请、接受/拒绝入群邀请',
|
|
515
497
|
});
|
|
516
498
|
if (cfg.groupManagement.enabled)
|
|
517
499
|
registerGroupManagementTools(caps, bundle);
|
|
518
500
|
if (cfg.groupInfo.enabled)
|
|
519
|
-
registerGroupInfoTools(caps,
|
|
501
|
+
registerGroupInfoTools(caps, bundle);
|
|
520
502
|
if (cfg.account.enabled)
|
|
521
503
|
registerAccountTools(caps, bundle);
|
|
522
504
|
if (cfg.interaction.enabled)
|
|
@@ -538,7 +520,7 @@ export default definePlugin({
|
|
|
538
520
|
});
|
|
539
521
|
// ===== 群管理工具 =====
|
|
540
522
|
function registerGroupManagementTools(caps, bundle) {
|
|
541
|
-
const { platform, logger } = caps;
|
|
523
|
+
const { platform, logger, sessionManager } = caps;
|
|
542
524
|
const { group, personal } = bundle;
|
|
543
525
|
// ---- 群禁言(单人)----
|
|
544
526
|
group.register({
|
|
@@ -553,6 +535,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
553
535
|
user_id: { type: 'string', description: '要禁言的用户QQ号' },
|
|
554
536
|
duration: { type: 'number', description: '禁言时长(秒),0 = 解除禁言,默认 60' },
|
|
555
537
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
538
|
+
self_id: SELF_ID_PARAM,
|
|
556
539
|
},
|
|
557
540
|
required: ['user_id'],
|
|
558
541
|
},
|
|
@@ -561,27 +544,25 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
561
544
|
handler: async (args, callCtx) => {
|
|
562
545
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
563
546
|
const duration = typeof args.duration === 'number' ? args.duration : 60;
|
|
564
|
-
|
|
565
|
-
if (permError)
|
|
566
|
-
return permError;
|
|
547
|
+
await assertAdminPermission(platform, selfId, groupId, String(args.user_id));
|
|
567
548
|
await callAction(platform, selfId, 'set_group_ban', {
|
|
568
549
|
group_id: Number(groupId),
|
|
569
550
|
user_id: Number(args.user_id),
|
|
570
551
|
duration,
|
|
571
552
|
});
|
|
572
|
-
//
|
|
573
|
-
const info = await getGroupMemberInfo(platform, selfId, groupId, String(args.user_id));
|
|
553
|
+
// 验证禁言是否生效:查不到成员信息时照报已发出指令、写明无法验证
|
|
554
|
+
const info = await getGroupMemberInfo(platform, selfId, groupId, String(args.user_id)).catch(() => null);
|
|
574
555
|
if (info && 'shut_up_timestamp' in info) {
|
|
575
556
|
const shutUp = Number(info.shut_up_timestamp);
|
|
576
557
|
const now = Math.floor(Date.now() / 1000);
|
|
577
558
|
if (duration === 0) {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
return shutUp > now
|
|
582
|
-
? `已禁言 ${args.user_id},时长 ${duration} 秒`
|
|
583
|
-
: `禁言操作未生效(API 返回成功但禁言状态未变化),请检查机器人实际权限`;
|
|
559
|
+
if (shutUp > now)
|
|
560
|
+
throw new Error('解除禁言指令已发送,但用户仍在禁言中');
|
|
561
|
+
return `已解除 ${args.user_id} 的禁言`;
|
|
584
562
|
}
|
|
563
|
+
if (shutUp <= now)
|
|
564
|
+
throw new Error('禁言操作未生效(API 返回成功但禁言状态未变化),请检查机器人实际权限');
|
|
565
|
+
return `已禁言 ${args.user_id},时长 ${duration} 秒`;
|
|
585
566
|
}
|
|
586
567
|
return duration === 0
|
|
587
568
|
? `已解除 ${args.user_id} 的禁言`
|
|
@@ -600,6 +581,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
600
581
|
properties: {
|
|
601
582
|
enable: { type: 'boolean', description: 'true = 开启全群禁言,false = 关闭' },
|
|
602
583
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
584
|
+
self_id: SELF_ID_PARAM,
|
|
603
585
|
},
|
|
604
586
|
required: ['enable'],
|
|
605
587
|
},
|
|
@@ -607,9 +589,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
607
589
|
},
|
|
608
590
|
handler: async (args, callCtx) => {
|
|
609
591
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
610
|
-
|
|
611
|
-
if (permError)
|
|
612
|
-
return permError;
|
|
592
|
+
await assertAdminPermission(platform, selfId, groupId);
|
|
613
593
|
await callAction(platform, selfId, 'set_group_whole_ban', {
|
|
614
594
|
group_id: Number(groupId),
|
|
615
595
|
enable: !!args.enable,
|
|
@@ -630,6 +610,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
630
610
|
user_id: { type: 'string', description: '要踢出的用户QQ号' },
|
|
631
611
|
reject_add_request: { type: 'boolean', description: '是否拒绝此人再次加群,默认 false' },
|
|
632
612
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
613
|
+
self_id: SELF_ID_PARAM,
|
|
633
614
|
},
|
|
634
615
|
required: ['user_id'],
|
|
635
616
|
},
|
|
@@ -637,9 +618,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
637
618
|
},
|
|
638
619
|
handler: async (args, callCtx) => {
|
|
639
620
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
640
|
-
|
|
641
|
-
if (permError)
|
|
642
|
-
return permError;
|
|
621
|
+
await assertAdminPermission(platform, selfId, groupId, String(args.user_id));
|
|
643
622
|
await callAction(platform, selfId, 'set_group_kick', {
|
|
644
623
|
group_id: Number(groupId),
|
|
645
624
|
user_id: Number(args.user_id),
|
|
@@ -659,6 +638,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
659
638
|
type: 'object',
|
|
660
639
|
properties: {
|
|
661
640
|
group_id: { type: 'string', description: '要退出的群号;在群聊中可省略,默认当前群' },
|
|
641
|
+
self_id: SELF_ID_PARAM,
|
|
662
642
|
},
|
|
663
643
|
required: [],
|
|
664
644
|
},
|
|
@@ -670,7 +650,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
670
650
|
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
671
651
|
const groupId = argGroupId || (parsed?.detailType === 'group' ? parsed.targetId : '');
|
|
672
652
|
if (!groupId)
|
|
673
|
-
|
|
653
|
+
throw new Error('请提供 group_id,或在要退出的群聊中调用此工具');
|
|
674
654
|
await callAction(platform, selfId, 'set_group_leave', {
|
|
675
655
|
group_id: Number(groupId),
|
|
676
656
|
is_dismiss: false,
|
|
@@ -691,6 +671,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
691
671
|
user_id: { type: 'string', description: '用户QQ号' },
|
|
692
672
|
card: { type: 'string', description: '新的群名片,空字符串表示取消' },
|
|
693
673
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
674
|
+
self_id: SELF_ID_PARAM,
|
|
694
675
|
},
|
|
695
676
|
required: ['user_id', 'card'],
|
|
696
677
|
},
|
|
@@ -698,9 +679,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
698
679
|
},
|
|
699
680
|
handler: async (args, callCtx) => {
|
|
700
681
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
701
|
-
|
|
702
|
-
if (permError)
|
|
703
|
-
return permError;
|
|
682
|
+
await assertAdminPermission(platform, selfId, groupId);
|
|
704
683
|
await callAction(platform, selfId, 'set_group_card', {
|
|
705
684
|
group_id: Number(groupId),
|
|
706
685
|
user_id: Number(args.user_id),
|
|
@@ -721,6 +700,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
721
700
|
properties: {
|
|
722
701
|
group_name: { type: 'string', description: '新的群名称' },
|
|
723
702
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
703
|
+
self_id: SELF_ID_PARAM,
|
|
724
704
|
},
|
|
725
705
|
required: ['group_name'],
|
|
726
706
|
},
|
|
@@ -728,9 +708,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
728
708
|
},
|
|
729
709
|
handler: async (args, callCtx) => {
|
|
730
710
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
731
|
-
|
|
732
|
-
if (permError)
|
|
733
|
-
return permError;
|
|
711
|
+
await assertAdminPermission(platform, selfId, groupId);
|
|
734
712
|
await callAction(platform, selfId, 'set_group_name', {
|
|
735
713
|
group_id: Number(groupId),
|
|
736
714
|
group_name: String(args.group_name),
|
|
@@ -751,6 +729,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
751
729
|
user_id: { type: 'string', description: '用户QQ号' },
|
|
752
730
|
special_title: { type: 'string', description: '专属头衔,空字符串表示取消' },
|
|
753
731
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
732
|
+
self_id: SELF_ID_PARAM,
|
|
754
733
|
},
|
|
755
734
|
required: ['user_id', 'special_title'],
|
|
756
735
|
},
|
|
@@ -758,9 +737,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
758
737
|
},
|
|
759
738
|
handler: async (args, callCtx) => {
|
|
760
739
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
761
|
-
|
|
762
|
-
if (permError)
|
|
763
|
-
return permError;
|
|
740
|
+
await assertAdminPermission(platform, selfId, groupId, undefined, true);
|
|
764
741
|
await callAction(platform, selfId, 'set_group_special_title', {
|
|
765
742
|
group_id: Number(groupId),
|
|
766
743
|
user_id: Number(args.user_id),
|
|
@@ -783,6 +760,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
783
760
|
user_id: { type: 'string', description: '用户QQ号' },
|
|
784
761
|
enable: { type: 'boolean', description: 'true = 设为管理员,false = 取消管理员' },
|
|
785
762
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
763
|
+
self_id: SELF_ID_PARAM,
|
|
786
764
|
},
|
|
787
765
|
required: ['user_id', 'enable'],
|
|
788
766
|
},
|
|
@@ -790,9 +768,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
790
768
|
},
|
|
791
769
|
handler: async (args, callCtx) => {
|
|
792
770
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
793
|
-
|
|
794
|
-
if (permError)
|
|
795
|
-
return permError;
|
|
771
|
+
await assertAdminPermission(platform, selfId, groupId, undefined, true);
|
|
796
772
|
await callAction(platform, selfId, 'set_group_admin', {
|
|
797
773
|
group_id: Number(groupId),
|
|
798
774
|
user_id: Number(args.user_id),
|
|
@@ -812,6 +788,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
812
788
|
type: 'object',
|
|
813
789
|
properties: {
|
|
814
790
|
message_id: { type: 'string', description: '要撤回的消息 ID' },
|
|
791
|
+
self_id: SELF_ID_PARAM,
|
|
815
792
|
},
|
|
816
793
|
required: ['message_id'],
|
|
817
794
|
},
|
|
@@ -842,6 +819,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
842
819
|
count: { type: 'number', description: '撤回最近多少条机器人消息(默认 1,上限 10)' },
|
|
843
820
|
group_id: { type: 'string', description: '可选:目标群号。不传则用当前会话。' },
|
|
844
821
|
user_id: { type: 'string', description: '可选:目标私聊 QQ 号。不传则用当前会话。' },
|
|
822
|
+
self_id: SELF_ID_PARAM,
|
|
845
823
|
},
|
|
846
824
|
required: [],
|
|
847
825
|
},
|
|
@@ -859,18 +837,22 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
859
837
|
else {
|
|
860
838
|
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
861
839
|
if (!parsed)
|
|
862
|
-
|
|
840
|
+
throw new Error('当前不在 OneBot 会话中,请用 group_id 或 user_id 指定要撤回的目标会话。');
|
|
863
841
|
targetSessionId = callCtx.sessionId;
|
|
864
842
|
}
|
|
843
|
+
// 目标会话逐个参与者过会话管理的规则(同 onebot_get_msg):不放行时在读发出记录之前报错,不撤回,报错里没有她在那里说的话
|
|
844
|
+
const verdict = await callCtx.approveFor(who => takeFrom(sessionManager, callCtx.sessionId, targetSessionId, who));
|
|
845
|
+
if (!verdict.ok)
|
|
846
|
+
throw new Error(toSource(verdict.reason));
|
|
865
847
|
// 发出记录在发消息的那个实例里:按目标会话路由
|
|
866
848
|
const adapter = (await resolvePlatformBySession(platform, targetSessionId))?.use;
|
|
867
849
|
if (!adapter?.getSentMessages) {
|
|
868
|
-
|
|
850
|
+
throw new Error('未找到支持记录自身发出消息的 OneBot 适配器(getSentMessages 不在平台契约内,第三方适配器可不提供)。如已知 message_id 可改用 onebot_delete_msg。');
|
|
869
851
|
}
|
|
870
852
|
const count = Math.max(1, Math.min(10, Math.floor(Number(args.count) || 1)));
|
|
871
853
|
const recent = adapter.getSentMessages(targetSessionId, 10); // 新→旧
|
|
872
854
|
if (recent.length === 0) {
|
|
873
|
-
|
|
855
|
+
throw new Error('没有可撤回的记录:机器人近期未在该会话发送消息,或已超出记录时窗(约 30 分钟)。');
|
|
874
856
|
}
|
|
875
857
|
const targets = recent.slice(0, count);
|
|
876
858
|
const recalled = [];
|
|
@@ -886,7 +868,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
886
868
|
}
|
|
887
869
|
}
|
|
888
870
|
if (recalled.length === 0) {
|
|
889
|
-
|
|
871
|
+
throw new Error(`撤回失败:${failed.join(';')}`);
|
|
890
872
|
}
|
|
891
873
|
let msg = `已撤回机器人发送的 ${recalled.length} 条消息:${recalled.join('、')}`;
|
|
892
874
|
if (failed.length > 0)
|
|
@@ -897,8 +879,8 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
897
879
|
logger.use.info('OneBot 群管理工具已注册');
|
|
898
880
|
}
|
|
899
881
|
// ===== 群信息查询工具 =====
|
|
900
|
-
function registerGroupInfoTools(caps,
|
|
901
|
-
const { platform, logger } = caps;
|
|
882
|
+
function registerGroupInfoTools(caps, bundle) {
|
|
883
|
+
const { platform, logger, sessionManager } = caps;
|
|
902
884
|
const { daily } = bundle;
|
|
903
885
|
// ---- 查看合并转发 ----
|
|
904
886
|
daily.register({
|
|
@@ -906,7 +888,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
906
888
|
type: 'function',
|
|
907
889
|
function: {
|
|
908
890
|
name: 'onebot_get_forward_msg',
|
|
909
|
-
description: '读取合并转发消息内容(OneBot v11 标准 get_forward_msg)。收到 <forward id="...">[合并转发消息]</forward>
|
|
891
|
+
description: '读取合并转发消息内容(OneBot v11 标准 get_forward_msg)。收到 <forward id="...">[合并转发消息]</forward> 时,把尖括号里的 id 字符串作为参数 id 传入。',
|
|
910
892
|
parameters: {
|
|
911
893
|
type: 'object',
|
|
912
894
|
properties: {
|
|
@@ -915,6 +897,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
915
897
|
description: '合并转发 ID(forward 消息段的 data.id),与 OneBot v11 标准参数名一致',
|
|
916
898
|
},
|
|
917
899
|
limit: { type: 'number', description: '最多返回多少条节点,默认 30' },
|
|
900
|
+
self_id: SELF_ID_PARAM,
|
|
918
901
|
},
|
|
919
902
|
required: ['id'],
|
|
920
903
|
},
|
|
@@ -923,27 +906,51 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
923
906
|
handler: async (args, callCtx) => {
|
|
924
907
|
const selfId = resolveSelfId(platform, callCtx, args);
|
|
925
908
|
if (!args.id)
|
|
926
|
-
|
|
909
|
+
throw new Error('参数错误:缺少 id(合并转发 ID)。请使用 <forward id="..."> 中的 id 字符串。');
|
|
927
910
|
const forwardId = String(args.id);
|
|
928
911
|
const limit = Math.max(1, Math.min(100, typeof args.limit === 'number' ? Math.floor(args.limit) : 30));
|
|
929
|
-
|
|
930
|
-
//
|
|
912
|
+
const readFailed = (err) => new Error(`合并转发读取失败:${err instanceof Error ? err.message : String(err)}`);
|
|
913
|
+
// 适配器的 callAction 维护了一份接收时即抓取并展开(含摘要与图像识别)的缓存,每个收到的转发都记着收到它的会话。
|
|
931
914
|
let data;
|
|
932
915
|
try {
|
|
933
916
|
data = await callAction(platform, selfId, 'get_forward_msg', { id: forwardId });
|
|
934
917
|
}
|
|
935
918
|
catch (err) {
|
|
936
|
-
|
|
919
|
+
throw readFailed(err);
|
|
937
920
|
}
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
921
|
+
// 适配器交回收到这条转发的会话:展开过的连同 ForwardEntry 形状(fullText / summary / 元信息与截断标志),没展开过的连同
|
|
922
|
+
// 按 id 当场获取的办法。收到它的会话逐个参与者过会话管理的规则(同 onebot_get_msg),几处之一过得了就行,过了才获取;没记会话的
|
|
923
|
+
// (从没收到过的、别的适配器交回的原始数据)按判不出拒绝
|
|
924
|
+
const reply = (data && typeof data === 'object' ? data : {});
|
|
925
|
+
const recorded = reply.__aalisForwardEntry === true || reply.__aalisForwardReceipt === true;
|
|
926
|
+
const sessionIds = recorded && Array.isArray(reply.sessionIds) ? reply.sessionIds.map(String) : [];
|
|
927
|
+
const verdict = sessionIds.length === 0
|
|
928
|
+
? { ok: false, reason: TAKE_UNKNOWN }
|
|
929
|
+
: await callCtx.approveFor(who => {
|
|
930
|
+
const verdicts = sessionIds.map(id => takeFrom(sessionManager, callCtx.sessionId, id, who));
|
|
931
|
+
return verdicts.find(v => v.ok) ?? verdicts[0];
|
|
932
|
+
});
|
|
933
|
+
if (!verdict.ok)
|
|
934
|
+
throw new Error(toSource(verdict.reason));
|
|
935
|
+
// 展开过的直接返回原文给 LLM,无需再做协议端结构解析
|
|
936
|
+
if (reply.__aalisForwardEntry === true) {
|
|
937
|
+
const entry = reply;
|
|
938
|
+
const cut = [entry.truncatedDepth && '嵌套超深', entry.truncatedNodes && '节点超限'].filter(Boolean);
|
|
939
|
+
// 参与人与入站信封同一写法:条目只列前 8 个,没列出的人数另记
|
|
940
|
+
const participants = [...entry.participants, ...(entry.unlisted ? [`...(+${entry.unlisted} 人未列出)`] : [])];
|
|
941
|
+
const header = `合并转发共 ${entry.count} 条,参与人:${participants.join(', ') || '未知'}` +
|
|
942
|
+
(cut.length ? `,已截断(${cut.join('、')})` : '');
|
|
943
943
|
const summaryLine = entry.summary ? `\n摘要:${entry.summary}\n` : '\n';
|
|
944
944
|
return `${header}${summaryLine}\n原文:\n${entry.fullText}`;
|
|
945
945
|
}
|
|
946
|
-
|
|
946
|
+
// 没展开过的:过了判定才按 id 当场获取
|
|
947
|
+
try {
|
|
948
|
+
data = await reply.fetch();
|
|
949
|
+
}
|
|
950
|
+
catch (err) {
|
|
951
|
+
throw readFailed(err);
|
|
952
|
+
}
|
|
953
|
+
const formatContext = await recognizeForwardImages(caps, selfId, data, limit, callCtx.signal);
|
|
947
954
|
return formatForwardMessage(data, limit, formatContext);
|
|
948
955
|
},
|
|
949
956
|
});
|
|
@@ -958,6 +965,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
958
965
|
type: 'object',
|
|
959
966
|
properties: {
|
|
960
967
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
968
|
+
self_id: SELF_ID_PARAM,
|
|
961
969
|
},
|
|
962
970
|
required: [],
|
|
963
971
|
},
|
|
@@ -983,6 +991,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
983
991
|
properties: {
|
|
984
992
|
user_id: { type: 'string', description: '可选:要查询的用户 QQ 号。缺省查询机器人自身。' },
|
|
985
993
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
994
|
+
self_id: SELF_ID_PARAM,
|
|
986
995
|
},
|
|
987
996
|
required: [],
|
|
988
997
|
},
|
|
@@ -1014,6 +1023,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1014
1023
|
limit: { type: 'number', description: '本页最多返回条数,默认 30' },
|
|
1015
1024
|
offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
|
|
1016
1025
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
1026
|
+
self_id: SELF_ID_PARAM,
|
|
1017
1027
|
},
|
|
1018
1028
|
required: [],
|
|
1019
1029
|
},
|
|
@@ -1077,6 +1087,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1077
1087
|
type: 'string',
|
|
1078
1088
|
description: '可选。指定群号。缺省时查当前会话所在群(私聊时缺省会报错,请显式传入)。',
|
|
1079
1089
|
},
|
|
1090
|
+
self_id: SELF_ID_PARAM,
|
|
1080
1091
|
},
|
|
1081
1092
|
required: [],
|
|
1082
1093
|
},
|
|
@@ -1088,9 +1099,8 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1088
1099
|
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
1089
1100
|
let groupId = argGroupId;
|
|
1090
1101
|
if (!groupId) {
|
|
1091
|
-
if (parsed?.detailType !== 'group')
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1102
|
+
if (parsed?.detailType !== 'group')
|
|
1103
|
+
throw new Error('当前不在群聊中,请显式传入 group_id');
|
|
1094
1104
|
groupId = parsed.targetId;
|
|
1095
1105
|
}
|
|
1096
1106
|
let info = null;
|
|
@@ -1102,18 +1112,14 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1102
1112
|
}));
|
|
1103
1113
|
}
|
|
1104
1114
|
catch (err) {
|
|
1105
|
-
|
|
1106
|
-
available: false,
|
|
1107
|
-
reason: `查询失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
1108
|
-
});
|
|
1115
|
+
throw new Error(`查询失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
1109
1116
|
}
|
|
1110
1117
|
if (!info)
|
|
1111
|
-
|
|
1118
|
+
throw new Error('无法获取群成员信息');
|
|
1112
1119
|
const ts = Number(info.shut_up_timestamp ?? 0);
|
|
1113
1120
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
1114
1121
|
const muted = ts > nowSec;
|
|
1115
1122
|
return JSON.stringify({
|
|
1116
|
-
available: true,
|
|
1117
1123
|
muted,
|
|
1118
1124
|
shutUpTimestamp: ts,
|
|
1119
1125
|
untilIso: muted ? new Date(ts * 1000).toISOString() : null,
|
|
@@ -1146,10 +1152,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1146
1152
|
handler: async (args) => {
|
|
1147
1153
|
const supporting = getPlatformAdapters(platform).filter((a) => a.platform === 'onebot' && typeof a.getSelfMutes === 'function');
|
|
1148
1154
|
if (supporting.length === 0) {
|
|
1149
|
-
|
|
1150
|
-
supported: false,
|
|
1151
|
-
reason: '未找到支持记录自身禁言状态的 OneBot 适配器(getSelfMutes 不在平台契约内,第三方适配器可不提供)',
|
|
1152
|
-
});
|
|
1155
|
+
throw new Error('未找到支持记录自身禁言状态的 OneBot 适配器(getSelfMutes 不在平台契约内,第三方适配器可不提供)');
|
|
1153
1156
|
}
|
|
1154
1157
|
const limit = Math.max(1, Math.min(200, Math.floor(Number(args.limit) || 30)));
|
|
1155
1158
|
const offset = Math.max(0, Math.floor(Number(args.offset) || 0));
|
|
@@ -1157,7 +1160,6 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1157
1160
|
const total = mutes.length;
|
|
1158
1161
|
const sliced = mutes.slice(offset, offset + limit);
|
|
1159
1162
|
return JSON.stringify({
|
|
1160
|
-
supported: true,
|
|
1161
1163
|
total,
|
|
1162
1164
|
offset,
|
|
1163
1165
|
limit,
|
|
@@ -1178,11 +1180,12 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1178
1180
|
type: 'function',
|
|
1179
1181
|
function: {
|
|
1180
1182
|
name: 'onebot_get_msg',
|
|
1181
|
-
description: '按 message_id
|
|
1183
|
+
description: '按 message_id 获取单条消息的完整内容(包括发送者、消息段等)。可用于回溯查看历史中的某条消息,或在收到撤回通知后查看被撤回的内容。消息所在的会话按会话管理的跨会话规则判(默认不取别人私聊里的消息)。',
|
|
1182
1184
|
parameters: {
|
|
1183
1185
|
type: 'object',
|
|
1184
1186
|
properties: {
|
|
1185
1187
|
message_id: { type: 'string', description: '消息 ID' },
|
|
1188
|
+
self_id: SELF_ID_PARAM,
|
|
1186
1189
|
},
|
|
1187
1190
|
required: ['message_id'],
|
|
1188
1191
|
},
|
|
@@ -1191,16 +1194,25 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1191
1194
|
handler: async (args, callCtx) => {
|
|
1192
1195
|
const selfId = resolveSelfId(platform, callCtx, args);
|
|
1193
1196
|
if (!args.message_id)
|
|
1194
|
-
|
|
1197
|
+
throw new Error('参数错误:缺少 message_id');
|
|
1198
|
+
let data;
|
|
1195
1199
|
try {
|
|
1196
|
-
|
|
1200
|
+
data = (await callAction(platform, selfId, 'get_msg', {
|
|
1197
1201
|
message_id: Number(args.message_id),
|
|
1198
|
-
});
|
|
1199
|
-
return JSON.stringify(data);
|
|
1202
|
+
}));
|
|
1200
1203
|
}
|
|
1201
1204
|
catch (err) {
|
|
1202
|
-
|
|
1205
|
+
throw new Error(`获取消息失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1203
1206
|
}
|
|
1207
|
+
// 取到的消息按所在的窗口拼出会话 id 过规则:拼不出(她自己发的私聊消息、实现端不给字段)的按判不出拒绝
|
|
1208
|
+
const target = sessionOfMessage(selfId, data);
|
|
1209
|
+
// 逐个参与者过会话管理的规则:有人过不了(某个参与者自己私聊里的)时发一道参与者审批,由过得了的人批
|
|
1210
|
+
const verdict = target === undefined
|
|
1211
|
+
? { ok: false, reason: TAKE_UNKNOWN }
|
|
1212
|
+
: await callCtx.approveFor(who => takeFrom(sessionManager, callCtx.sessionId, target, who));
|
|
1213
|
+
if (!verdict.ok)
|
|
1214
|
+
throw new Error(toSource(verdict.reason));
|
|
1215
|
+
return JSON.stringify(data);
|
|
1204
1216
|
},
|
|
1205
1217
|
});
|
|
1206
1218
|
// ---- 获取群荣誉信息 ----
|
|
@@ -1219,6 +1231,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1219
1231
|
description: '荣誉类型:talkative=龙王, performer=群聊之火, legend=群聊炽焰, strong_newbie=冒尖小春笋, emotion=快乐之源, all=全部。默认 all。',
|
|
1220
1232
|
},
|
|
1221
1233
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
1234
|
+
self_id: SELF_ID_PARAM,
|
|
1222
1235
|
},
|
|
1223
1236
|
required: [],
|
|
1224
1237
|
},
|
|
@@ -1256,6 +1269,7 @@ function registerAccountTools(caps, bundle) {
|
|
|
1256
1269
|
keyword: { type: 'string', description: '可选:按群名或群号子串模糊匹配(不区分大小写)' },
|
|
1257
1270
|
limit: { type: 'number', description: '本页最多返回条数,默认 30' },
|
|
1258
1271
|
offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
|
|
1272
|
+
self_id: SELF_ID_PARAM,
|
|
1259
1273
|
},
|
|
1260
1274
|
required: [],
|
|
1261
1275
|
},
|
|
@@ -1305,6 +1319,7 @@ function registerAccountTools(caps, bundle) {
|
|
|
1305
1319
|
keyword: { type: 'string', description: '可选:按昵称、备注或 QQ 号子串模糊匹配(不区分大小写)' },
|
|
1306
1320
|
limit: { type: 'number', description: '本页最多返回条数,默认 30' },
|
|
1307
1321
|
offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
|
|
1322
|
+
self_id: SELF_ID_PARAM,
|
|
1308
1323
|
},
|
|
1309
1324
|
required: [],
|
|
1310
1325
|
},
|
|
@@ -1353,6 +1368,7 @@ function registerAccountTools(caps, bundle) {
|
|
|
1353
1368
|
properties: {
|
|
1354
1369
|
user_id: { type: 'string', description: '要查询的 QQ 号' },
|
|
1355
1370
|
no_cache: { type: 'boolean', description: '是否跳过缓存强制刷新,默认 false' },
|
|
1371
|
+
self_id: SELF_ID_PARAM,
|
|
1356
1372
|
},
|
|
1357
1373
|
required: ['user_id'],
|
|
1358
1374
|
},
|
|
@@ -1374,11 +1390,11 @@ function registerAccountTools(caps, bundle) {
|
|
|
1374
1390
|
function: {
|
|
1375
1391
|
name: 'onebot_get_login_info',
|
|
1376
1392
|
description: '显式查询当前 OneBot 连接的登录账号信息(QQ 号、昵称)。仅在用户要求核实账号、诊断连接或需要最新平台返回值时使用。',
|
|
1377
|
-
parameters: { type: 'object', properties: {}, required: [] },
|
|
1393
|
+
parameters: { type: 'object', properties: { self_id: SELF_ID_PARAM }, required: [] },
|
|
1378
1394
|
},
|
|
1379
1395
|
},
|
|
1380
|
-
handler: async (
|
|
1381
|
-
const selfId = resolveSelfId(platform, callCtx,
|
|
1396
|
+
handler: async (args, callCtx) => {
|
|
1397
|
+
const selfId = resolveSelfId(platform, callCtx, args);
|
|
1382
1398
|
const data = await callAction(platform, selfId, 'get_login_info', {});
|
|
1383
1399
|
return JSON.stringify(data);
|
|
1384
1400
|
},
|
|
@@ -1394,6 +1410,7 @@ function registerAccountTools(caps, bundle) {
|
|
|
1394
1410
|
type: 'object',
|
|
1395
1411
|
properties: {
|
|
1396
1412
|
user_id: { type: 'string', description: '要删除的好友 QQ 号;在私聊中可省略,默认当前对话用户' },
|
|
1413
|
+
self_id: SELF_ID_PARAM,
|
|
1397
1414
|
},
|
|
1398
1415
|
required: [],
|
|
1399
1416
|
},
|
|
@@ -1405,7 +1422,7 @@ function registerAccountTools(caps, bundle) {
|
|
|
1405
1422
|
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
1406
1423
|
const userId = argUserId || (parsed?.detailType === 'private' ? parsed.targetId : '');
|
|
1407
1424
|
if (!userId)
|
|
1408
|
-
|
|
1425
|
+
throw new Error('请提供 user_id,或在要删除的好友私聊中调用此工具');
|
|
1409
1426
|
await callAction(platform, selfId, 'delete_friend', {
|
|
1410
1427
|
user_id: Number(userId),
|
|
1411
1428
|
});
|
|
@@ -1436,6 +1453,7 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1436
1453
|
type: 'string',
|
|
1437
1454
|
description: '可选:目标群号。群聊省略则使用当前群;如显式传值则跨群操作。',
|
|
1438
1455
|
},
|
|
1456
|
+
self_id: SELF_ID_PARAM,
|
|
1439
1457
|
},
|
|
1440
1458
|
},
|
|
1441
1459
|
},
|
|
@@ -1451,18 +1469,10 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1451
1469
|
if (!argUserId) {
|
|
1452
1470
|
throw new Error('群聊戳一戳必须显式提供 user_id(要戳的对象)。');
|
|
1453
1471
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
});
|
|
1459
|
-
}
|
|
1460
|
-
catch {
|
|
1461
|
-
await callAction(platform, selfId, 'send_group_poke', {
|
|
1462
|
-
group_id: Number(groupId),
|
|
1463
|
-
user_id: Number(argUserId),
|
|
1464
|
-
});
|
|
1465
|
-
}
|
|
1472
|
+
await callFirstSupported(platform, selfId, ['group_poke', 'send_group_poke'], {
|
|
1473
|
+
group_id: Number(groupId),
|
|
1474
|
+
user_id: Number(argUserId),
|
|
1475
|
+
});
|
|
1466
1476
|
return `已在群 ${groupId} 戳了 ${argUserId} 一下`;
|
|
1467
1477
|
}
|
|
1468
1478
|
// 2) 私聊场景:从 user_id 或当前私聊会话对方取目标
|
|
@@ -1470,13 +1480,7 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1470
1480
|
if (!userId) {
|
|
1471
1481
|
throw new Error('无法判断戳一戳目标:当前不在 OneBot 会话内,请显式传入 group_id+user_id 或 user_id。');
|
|
1472
1482
|
}
|
|
1473
|
-
|
|
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
|
-
}
|
|
1483
|
+
await callFirstSupported(platform, selfId, ['friend_poke', 'send_poke'], { user_id: Number(userId) });
|
|
1480
1484
|
return `已戳了 ${userId} 一下`;
|
|
1481
1485
|
},
|
|
1482
1486
|
});
|
|
@@ -1492,6 +1496,7 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1492
1496
|
properties: {
|
|
1493
1497
|
user_id: { type: 'string', description: '要赞的用户 QQ 号' },
|
|
1494
1498
|
times: { type: 'number', description: '赞的次数,默认 1,每天每个好友最多 10 次' },
|
|
1499
|
+
self_id: SELF_ID_PARAM,
|
|
1495
1500
|
},
|
|
1496
1501
|
required: ['user_id'],
|
|
1497
1502
|
},
|
|
@@ -1518,6 +1523,7 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1518
1523
|
type: 'object',
|
|
1519
1524
|
properties: {
|
|
1520
1525
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
1526
|
+
self_id: SELF_ID_PARAM,
|
|
1521
1527
|
},
|
|
1522
1528
|
required: [],
|
|
1523
1529
|
},
|
|
@@ -1533,52 +1539,6 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1533
1539
|
});
|
|
1534
1540
|
logger.use.info('OneBot 特殊交互工具已注册');
|
|
1535
1541
|
}
|
|
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
1542
|
function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
1583
1543
|
const { platform, logger, sessionHistory } = caps;
|
|
1584
1544
|
const { daily } = bundle;
|
|
@@ -1597,7 +1557,7 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
|
1597
1557
|
description: 'group = 群聊,private = 私聊',
|
|
1598
1558
|
},
|
|
1599
1559
|
target_id: { type: 'string', description: '群号或 QQ 号' },
|
|
1600
|
-
self_id:
|
|
1560
|
+
self_id: SELF_ID_PARAM,
|
|
1601
1561
|
},
|
|
1602
1562
|
required: ['target_type', 'target_id'],
|
|
1603
1563
|
additionalProperties: false,
|
|
@@ -1605,26 +1565,14 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
|
1605
1565
|
},
|
|
1606
1566
|
},
|
|
1607
1567
|
handler: async (args, callCtx) => {
|
|
1608
|
-
const current = parseOneBotSession(callCtx.sessionId);
|
|
1609
1568
|
const targetType = String(args.target_type ?? '').toLowerCase();
|
|
1610
1569
|
const targetId = String(args.target_id ?? '').trim();
|
|
1611
1570
|
if (targetType !== 'group' && targetType !== 'private') {
|
|
1612
|
-
|
|
1571
|
+
throw new Error(`target_type 必须为 group 或 private,收到 ${args.target_type}`);
|
|
1613
1572
|
}
|
|
1614
1573
|
if (!targetId)
|
|
1615
|
-
|
|
1616
|
-
const
|
|
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
|
-
}
|
|
1574
|
+
throw new Error('target_id 不能为空');
|
|
1575
|
+
const selfId = resolveSelfId(platform, callCtx, args);
|
|
1628
1576
|
return JSON.stringify({
|
|
1629
1577
|
ok: true,
|
|
1630
1578
|
sessionId: buildOneBotSessionId(selfId, targetType, targetId),
|
|
@@ -1642,7 +1590,7 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
|
1642
1590
|
description: [
|
|
1643
1591
|
'按 QQ 群号或 QQ 号读取对应 OneBot 会话最近若干条历史消息。',
|
|
1644
1592
|
'适用场景:用户明确要求你回忆某个群/私聊最近说过什么,或需要核实另一个 OneBot 会话的原文上下文。',
|
|
1645
|
-
'
|
|
1593
|
+
'能读哪些会话按会话管理的跨会话规则(默认不读别人的私聊);如需语义搜索历史,请优先使用 memory_recall。',
|
|
1646
1594
|
].join('\n'),
|
|
1647
1595
|
parameters: {
|
|
1648
1596
|
type: 'object',
|
|
@@ -1657,7 +1605,7 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
|
1657
1605
|
type: 'number',
|
|
1658
1606
|
description: `读取最近多少条,默认 ${cfg.defaultLimit},最多 ${cfg.maxLimit}`,
|
|
1659
1607
|
},
|
|
1660
|
-
self_id:
|
|
1608
|
+
self_id: SELF_ID_PARAM,
|
|
1661
1609
|
},
|
|
1662
1610
|
required: ['target_type', 'target_id'],
|
|
1663
1611
|
additionalProperties: false,
|
|
@@ -1665,44 +1613,40 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
|
1665
1613
|
},
|
|
1666
1614
|
},
|
|
1667
1615
|
handler: async (args, callCtx) => {
|
|
1668
|
-
const current = parseOneBotSession(callCtx.sessionId);
|
|
1669
1616
|
const targetType = String(args.target_type ?? '').toLowerCase();
|
|
1670
1617
|
const targetId = String(args.target_id ?? '').trim();
|
|
1671
1618
|
if (targetType !== 'group' && targetType !== 'private') {
|
|
1672
|
-
|
|
1619
|
+
throw new Error(`target_type 必须为 group 或 private,收到 ${args.target_type}`);
|
|
1673
1620
|
}
|
|
1674
1621
|
if (!targetId)
|
|
1675
|
-
|
|
1676
|
-
const
|
|
1677
|
-
const selfId = argSelfId || current?.selfId || getAnyOneBotSelfId(platform) || '';
|
|
1678
|
-
if (!selfId)
|
|
1679
|
-
return JSON.stringify({ error: '无法确定 self_id,且未找到可用的 OneBot 连接' });
|
|
1622
|
+
throw new Error('target_id 不能为空');
|
|
1623
|
+
const selfId = resolveSelfId(platform, callCtx, args);
|
|
1680
1624
|
if (!sessionHistory.present)
|
|
1681
|
-
|
|
1682
|
-
error: 'session-history 服务不可用,请启用 @aalis/plugin-tool-session',
|
|
1683
|
-
});
|
|
1625
|
+
throw new Error('session-history 服务不可用,请启用 @aalis/plugin-tool-session');
|
|
1684
1626
|
const history = sessionHistory.use;
|
|
1685
1627
|
const limit = Math.max(1, Math.min(cfg.maxLimit, Math.floor(Number(args.limit) || cfg.defaultLimit)));
|
|
1686
1628
|
const sessionId = buildOneBotSessionId(selfId, targetType, targetId);
|
|
1629
|
+
let result;
|
|
1687
1630
|
try {
|
|
1688
|
-
//
|
|
1689
|
-
// OneBot 的规则已通过 registerOneBotHistoryAccessChecker 注册。
|
|
1631
|
+
// 能不能读由 session-history 服务按会话管理的规则判(带这次调用的授权身份)。
|
|
1690
1632
|
// includeArchived 故意不暴露:archived 是 memory 内部细节,被 summary 替代的老消息
|
|
1691
|
-
// 默认就不该再翻原文(会与 summary
|
|
1692
|
-
|
|
1693
|
-
return JSON.stringify({
|
|
1694
|
-
...result,
|
|
1695
|
-
sessionId,
|
|
1696
|
-
selfId,
|
|
1697
|
-
targetType,
|
|
1698
|
-
targetId,
|
|
1699
|
-
});
|
|
1633
|
+
// 默认就不该再翻原文(会与 summary 内容双重计费)。如需查阅久远的记录,应另写一个独立工具。
|
|
1634
|
+
result = await history.getHistory({ sessionId, limit }, callCtx);
|
|
1700
1635
|
}
|
|
1701
1636
|
catch (err) {
|
|
1702
1637
|
const message = err instanceof Error ? err.message : String(err);
|
|
1703
1638
|
logger.use.warn(`onebot_get_session_history 失败 (${sessionId}): ${message}`);
|
|
1704
|
-
|
|
1639
|
+
throw new Error(`读取 OneBot 会话历史失败: ${message}`);
|
|
1705
1640
|
}
|
|
1641
|
+
if ('error' in result)
|
|
1642
|
+
throw new Error(result.error);
|
|
1643
|
+
return JSON.stringify({
|
|
1644
|
+
...result,
|
|
1645
|
+
sessionId,
|
|
1646
|
+
selfId,
|
|
1647
|
+
targetType,
|
|
1648
|
+
targetId,
|
|
1649
|
+
});
|
|
1706
1650
|
},
|
|
1707
1651
|
});
|
|
1708
1652
|
logger.use.info('OneBot 会话历史工具已注册');
|
|
@@ -1711,14 +1655,21 @@ function registerRequestTools(caps, bundle) {
|
|
|
1711
1655
|
const { platform, logger } = caps;
|
|
1712
1656
|
const { group, personal } = bundle;
|
|
1713
1657
|
/**
|
|
1714
|
-
*
|
|
1715
|
-
*
|
|
1658
|
+
* 处理申请的账号与 OneBot 适配器:申请挂在收到它的实例上,按当前会话(或 self_id)的账号路由。
|
|
1659
|
+
* 申请的通知注入在收到它的账号的会话里,在那里调用即落到对的实例与账号
|
|
1716
1660
|
*/
|
|
1717
|
-
async function
|
|
1718
|
-
const
|
|
1719
|
-
const adapter =
|
|
1720
|
-
|
|
1661
|
+
async function requestHandler(callCtx, args) {
|
|
1662
|
+
const selfId = resolveSelfId(platform, callCtx, args);
|
|
1663
|
+
const adapter = (await resolvePlatformBySession(platform, selfSessionId(selfId)))?.use;
|
|
1664
|
+
if (typeof adapter?.handleFriendRequest !== 'function')
|
|
1665
|
+
throw new Error('未找到支持请求处理的 OneBot 适配器');
|
|
1666
|
+
return { selfId, adapter: adapter };
|
|
1721
1667
|
}
|
|
1668
|
+
/** 两个群请求工具各认自己的申请类型:用审批加群申请的工具批不了入群邀请,反之亦然 */
|
|
1669
|
+
const handleGroupRequest = (subType) => async (args, callCtx) => {
|
|
1670
|
+
const { selfId, adapter } = await requestHandler(callCtx, args);
|
|
1671
|
+
return adapter.handleGroupRequest(selfId, subType, String(args.user_id), String(args.group_id), !!args.approve, args.reason ? String(args.reason) : undefined);
|
|
1672
|
+
};
|
|
1722
1673
|
personal.register({
|
|
1723
1674
|
definition: {
|
|
1724
1675
|
type: 'function',
|
|
@@ -1731,22 +1682,21 @@ function registerRequestTools(caps, bundle) {
|
|
|
1731
1682
|
user_id: { type: 'string', description: '发起好友申请的用户 QQ 号' },
|
|
1732
1683
|
approve: { type: 'boolean', description: 'true = 同意,false = 拒绝' },
|
|
1733
1684
|
remark: { type: 'string', description: '同意后添加的备注(可选)' },
|
|
1685
|
+
self_id: SELF_ID_PARAM,
|
|
1734
1686
|
},
|
|
1735
1687
|
required: ['user_id', 'approve'],
|
|
1736
1688
|
},
|
|
1737
1689
|
},
|
|
1738
1690
|
},
|
|
1739
1691
|
handler: async (args, callCtx) => {
|
|
1740
|
-
const adapter = await
|
|
1741
|
-
|
|
1742
|
-
return '未找到支持请求处理的 OneBot 适配器';
|
|
1743
|
-
return adapter.handleFriendRequest(String(args.user_id), !!args.approve, args.remark ? String(args.remark) : undefined);
|
|
1692
|
+
const { selfId, adapter } = await requestHandler(callCtx, args);
|
|
1693
|
+
return adapter.handleFriendRequest(selfId, String(args.user_id), !!args.approve, args.remark ? String(args.remark) : undefined);
|
|
1744
1694
|
},
|
|
1745
1695
|
});
|
|
1746
1696
|
// 群相关请求拆成两个工具,对应 OneBot v11 set_group_add_request 的两种 sub_type:
|
|
1747
1697
|
// - approve_join_request: sub_type=add 「别人申请加入 bot 管理的群」 → 群务
|
|
1748
1698
|
// - handle_group_invite: sub_type=invite 「别人邀请 bot 加入群」 → 个人/私聊
|
|
1749
|
-
//
|
|
1699
|
+
// 底层共用同一个适配器方法,各自把要处理的申请类型交给它,类型不符就报错。
|
|
1750
1700
|
group.register({
|
|
1751
1701
|
definition: {
|
|
1752
1702
|
type: 'function',
|
|
@@ -1760,17 +1710,13 @@ function registerRequestTools(caps, bundle) {
|
|
|
1760
1710
|
group_id: { type: 'string', description: '相关群号' },
|
|
1761
1711
|
approve: { type: 'boolean', description: 'true = 同意加群,false = 拒绝' },
|
|
1762
1712
|
reason: { type: 'string', description: '拒绝理由(拒绝时有效,可选)' },
|
|
1713
|
+
self_id: SELF_ID_PARAM,
|
|
1763
1714
|
},
|
|
1764
1715
|
required: ['user_id', 'group_id', 'approve'],
|
|
1765
1716
|
},
|
|
1766
1717
|
},
|
|
1767
1718
|
},
|
|
1768
|
-
handler:
|
|
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
|
-
},
|
|
1719
|
+
handler: handleGroupRequest('add'),
|
|
1774
1720
|
});
|
|
1775
1721
|
personal.register({
|
|
1776
1722
|
definition: {
|
|
@@ -1785,17 +1731,13 @@ function registerRequestTools(caps, bundle) {
|
|
|
1785
1731
|
group_id: { type: 'string', description: '被邀请加入的群号' },
|
|
1786
1732
|
approve: { type: 'boolean', description: 'true = 接受邀请并加入群,false = 拒绝' },
|
|
1787
1733
|
reason: { type: 'string', description: '拒绝理由(拒绝时有效,可选)' },
|
|
1734
|
+
self_id: SELF_ID_PARAM,
|
|
1788
1735
|
},
|
|
1789
1736
|
required: ['user_id', 'group_id', 'approve'],
|
|
1790
1737
|
},
|
|
1791
1738
|
},
|
|
1792
1739
|
},
|
|
1793
|
-
handler:
|
|
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
|
-
},
|
|
1740
|
+
handler: handleGroupRequest('invite'),
|
|
1799
1741
|
});
|
|
1800
1742
|
logger.use.info('OneBot 请求处理工具已注册');
|
|
1801
1743
|
}
|