@aalis/plugin-tool-onebot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1743 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1743 @@
|
|
|
1
|
+
import { getPlatformAdapters, getPlatformNames } from '@aalis/plugin-platform-api';
|
|
2
|
+
import { createStorageGateway } from '@aalis/plugin-storage-api';
|
|
3
|
+
import { toolsWithGroups, useToolService } from '@aalis/plugin-tools-api';
|
|
4
|
+
import '@aalis/plugin-tool-session-api';
|
|
5
|
+
import '@aalis/plugin-tools-api';
|
|
6
|
+
// ===== 插件元数据 =====
|
|
7
|
+
export const name = '@aalis/plugin-tool-onebot';
|
|
8
|
+
export const displayName = 'OneBot 工具';
|
|
9
|
+
export const subsystem = 'tools';
|
|
10
|
+
export const inject = {
|
|
11
|
+
optional: ['platform', 'session-history'],
|
|
12
|
+
};
|
|
13
|
+
export const configSchema = {
|
|
14
|
+
groupManagement: {
|
|
15
|
+
label: '群管理工具',
|
|
16
|
+
fields: {
|
|
17
|
+
enabled: {
|
|
18
|
+
type: 'boolean',
|
|
19
|
+
label: '启用群管理工具',
|
|
20
|
+
default: true,
|
|
21
|
+
description: '禁言、踢人、设置群名片、撤回消息等',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
groupInfo: {
|
|
26
|
+
label: '群信息查询',
|
|
27
|
+
fields: {
|
|
28
|
+
enabled: { type: 'boolean', label: '启用群信息查询', default: true, description: '查询群/成员信息' },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
account: {
|
|
32
|
+
label: '账号与好友',
|
|
33
|
+
fields: {
|
|
34
|
+
enabled: { type: 'boolean', label: '启用账号与好友查询', default: true, description: '群列表、好友列表等' },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
interaction: {
|
|
38
|
+
label: '特殊交互',
|
|
39
|
+
fields: {
|
|
40
|
+
enabled: { type: 'boolean', label: '启用特殊交互', default: true, description: '戳一戳、群打卡等' },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
sessionHistory: {
|
|
44
|
+
label: '会话历史读取',
|
|
45
|
+
fields: {
|
|
46
|
+
enabled: {
|
|
47
|
+
type: 'boolean',
|
|
48
|
+
label: '启用 OneBot 会话历史读取',
|
|
49
|
+
default: true,
|
|
50
|
+
description: '允许按群号/QQ 号读取对应 OneBot 会话的近期历史',
|
|
51
|
+
},
|
|
52
|
+
maxLimit: {
|
|
53
|
+
type: 'number',
|
|
54
|
+
label: '单次最多读取条数上限',
|
|
55
|
+
default: 100,
|
|
56
|
+
description: 'agent 传入的 limit 参数会被截断到该上限。',
|
|
57
|
+
},
|
|
58
|
+
defaultLimit: {
|
|
59
|
+
type: 'number',
|
|
60
|
+
label: '默认读取条数(agent 不传 limit 时)',
|
|
61
|
+
default: 20,
|
|
62
|
+
description: '不能超过 maxLimit。',
|
|
63
|
+
},
|
|
64
|
+
allowGroupReadPrivate: {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
label: '允许群聊读取私聊历史',
|
|
67
|
+
default: false,
|
|
68
|
+
description: '在群会话中调用历史读取工具时,是否允许目标是某个私聊。',
|
|
69
|
+
},
|
|
70
|
+
allowCrossSelf: {
|
|
71
|
+
type: 'boolean',
|
|
72
|
+
label: '允许跨机器人账号读取',
|
|
73
|
+
default: false,
|
|
74
|
+
description: '不同 selfId 之间跨读。多账号部署才需要。',
|
|
75
|
+
},
|
|
76
|
+
allowCrossGroup: {
|
|
77
|
+
type: 'boolean',
|
|
78
|
+
label: '允许群聊读取其他群聊历史',
|
|
79
|
+
default: true,
|
|
80
|
+
description: '群会话 → 另一个群会话。默认允许(便于跨群取上下文)。',
|
|
81
|
+
},
|
|
82
|
+
allowCrossPrivate: {
|
|
83
|
+
type: 'boolean',
|
|
84
|
+
label: '允许私聊读取其他私聊历史',
|
|
85
|
+
default: false,
|
|
86
|
+
description: '私聊会话 → 另一个 QQ 的私聊。默认拒绝(隐私敏感)。',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
export const defaultConfig = {
|
|
92
|
+
groupManagement: { enabled: true },
|
|
93
|
+
groupInfo: { enabled: true },
|
|
94
|
+
account: { enabled: true },
|
|
95
|
+
interaction: { enabled: true },
|
|
96
|
+
sessionHistory: {
|
|
97
|
+
enabled: true,
|
|
98
|
+
maxLimit: 100,
|
|
99
|
+
defaultLimit: 20,
|
|
100
|
+
allowGroupReadPrivate: false,
|
|
101
|
+
allowCrossSelf: false,
|
|
102
|
+
allowCrossGroup: true,
|
|
103
|
+
allowCrossPrivate: false,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
// ===== 辅助函数 =====
|
|
107
|
+
/** 从 sessionId 解析 OneBot 连接信息 */
|
|
108
|
+
function parseOneBotSession(sessionId) {
|
|
109
|
+
const parts = sessionId.split(':');
|
|
110
|
+
if (parts[0] !== 'onebot' || parts.length < 4)
|
|
111
|
+
return null;
|
|
112
|
+
return {
|
|
113
|
+
selfId: parts[1],
|
|
114
|
+
detailType: parts[2],
|
|
115
|
+
targetId: parts.slice(3).join(':'),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** 从上下文中找到支持 callAction 的 OneBot 平台适配器 */
|
|
119
|
+
function findOneBotAdapter(ctx) {
|
|
120
|
+
return getPlatformAdapters(ctx).find(a => a.platform === 'onebot' && typeof a.callAction === 'function');
|
|
121
|
+
}
|
|
122
|
+
/** 从已连接的 OneBot 适配器中找出任意一个 selfId(优先 online) */
|
|
123
|
+
function getAnyOneBotSelfId(ctx) {
|
|
124
|
+
for (const a of getPlatformAdapters(ctx)) {
|
|
125
|
+
if (a.platform !== 'onebot')
|
|
126
|
+
continue;
|
|
127
|
+
const conns = a.getConnections?.() ?? [];
|
|
128
|
+
const online = conns.find(c => c.status === 'online' && c.selfId);
|
|
129
|
+
if (online?.selfId)
|
|
130
|
+
return online.selfId;
|
|
131
|
+
const any = conns.find(c => c.selfId);
|
|
132
|
+
if (any?.selfId)
|
|
133
|
+
return any.selfId;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 解析出本次工具调用应使用的 selfId(机器人账号)。
|
|
139
|
+
* 优先级:args.self_id → 当前 callCtx 是 OneBot 会话时取它的 selfId → 任意可用 adapter 的第一个 selfId。
|
|
140
|
+
* 找不到则抛错(无可用 OneBot 连接)。
|
|
141
|
+
*/
|
|
142
|
+
function resolveSelfId(ctx, callCtx, args) {
|
|
143
|
+
const fromArgs = args.self_id != null && String(args.self_id).trim() ? String(args.self_id).trim() : '';
|
|
144
|
+
if (fromArgs)
|
|
145
|
+
return fromArgs;
|
|
146
|
+
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
147
|
+
if (parsed?.selfId)
|
|
148
|
+
return parsed.selfId;
|
|
149
|
+
const fallback = getAnyOneBotSelfId(ctx);
|
|
150
|
+
if (fallback)
|
|
151
|
+
return fallback;
|
|
152
|
+
throw new Error('未找到可用的 OneBot 连接');
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 解析"目标群":args.group_id 优先;否则当前会话必须在 OneBot 群里。
|
|
156
|
+
* 不再限制工具必须在群会话上下文中调用。
|
|
157
|
+
*/
|
|
158
|
+
function resolveGroupTarget(ctx, callCtx, args) {
|
|
159
|
+
const argGroupId = args.group_id != null && String(args.group_id).trim() ? String(args.group_id).trim() : '';
|
|
160
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
161
|
+
if (argGroupId)
|
|
162
|
+
return { selfId, groupId: argGroupId };
|
|
163
|
+
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
164
|
+
if (parsed?.detailType === 'group' && parsed.targetId) {
|
|
165
|
+
return { selfId: parsed.selfId, groupId: parsed.targetId };
|
|
166
|
+
}
|
|
167
|
+
throw new Error('未提供 group_id,且当前不在 OneBot 群会话中。请在参数中显式传入 group_id。');
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 解析"目标用户":args.user_id 优先;否则当前会话必须是 OneBot 私聊。
|
|
171
|
+
*/
|
|
172
|
+
function _resolveUserTarget(ctx, callCtx, args) {
|
|
173
|
+
const argUserId = args.user_id != null && String(args.user_id).trim() ? String(args.user_id).trim() : '';
|
|
174
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
175
|
+
if (argUserId)
|
|
176
|
+
return { selfId, userId: argUserId };
|
|
177
|
+
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
178
|
+
if (parsed?.detailType === 'private' && parsed.targetId) {
|
|
179
|
+
return { selfId: parsed.selfId, userId: parsed.targetId };
|
|
180
|
+
}
|
|
181
|
+
throw new Error('未提供 user_id,且当前不在 OneBot 私聊会话中。请在参数中显式传入 user_id。');
|
|
182
|
+
}
|
|
183
|
+
function buildOneBotSessionId(selfId, detailType, targetId) {
|
|
184
|
+
return `onebot:${selfId}:${detailType}:${targetId}`;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 调用 OneBot 协议 action。selfId 决定走哪个机器人账号;
|
|
188
|
+
* adapter 内部只用 sessionId 解析 selfId 来定位 WebSocket 连接,
|
|
189
|
+
* 因此这里用合成 sessionId("onebot:<selfId>:internal:0")即可,与目标会话类型无关。
|
|
190
|
+
*/
|
|
191
|
+
async function callAction(ctx, selfId, action, params) {
|
|
192
|
+
const adapter = findOneBotAdapter(ctx);
|
|
193
|
+
if (!adapter?.callAction)
|
|
194
|
+
throw new Error('OneBot 适配器不可用或不支持 callAction');
|
|
195
|
+
return adapter.callAction(buildOneBotSessionId(selfId, 'internal', '0'), action, params);
|
|
196
|
+
}
|
|
197
|
+
function imageMimeFromPath(path) {
|
|
198
|
+
const cleaned = path.split('?')[0];
|
|
199
|
+
const dot = cleaned.lastIndexOf('.');
|
|
200
|
+
const ext = dot >= 0 ? cleaned.slice(dot).toLowerCase() : '';
|
|
201
|
+
if (ext === '.jpg' || ext === '.jpeg')
|
|
202
|
+
return 'image/jpeg';
|
|
203
|
+
if (ext === '.gif')
|
|
204
|
+
return 'image/gif';
|
|
205
|
+
if (ext === '.webp')
|
|
206
|
+
return 'image/webp';
|
|
207
|
+
if (ext === '.bmp')
|
|
208
|
+
return 'image/bmp';
|
|
209
|
+
return 'image/png';
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 尝试将 OneBot get_image 返回的文件路径转为 data URI。
|
|
213
|
+
* 仅支持 storage URI 形式(含 ':/')。裸本地 fs 路径不再处理,
|
|
214
|
+
* 交由上游 vision 退路使用原始 source(该 fallback 本身在实践中极少命中)。
|
|
215
|
+
*/
|
|
216
|
+
async function localImageToDataUri(storage, path) {
|
|
217
|
+
if (!path.includes(':/'))
|
|
218
|
+
return null;
|
|
219
|
+
try {
|
|
220
|
+
const buf = (await storage.readFile(path));
|
|
221
|
+
const b64 = Buffer.from(buf).toString('base64');
|
|
222
|
+
return `data:${imageMimeFromPath(path)};base64,${b64}`;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function imageRefKey(ref) {
|
|
229
|
+
return `${ref.source}|${JSON.stringify(ref.segment)}`;
|
|
230
|
+
}
|
|
231
|
+
function imageSourceFromSegment(data) {
|
|
232
|
+
const direct = data.url ?? data.file;
|
|
233
|
+
return direct == null ? '' : String(direct);
|
|
234
|
+
}
|
|
235
|
+
function extractCqImageRefs(content) {
|
|
236
|
+
const refs = [];
|
|
237
|
+
const re = /\[CQ:image,([^\]]+)\]/g;
|
|
238
|
+
let match = re.exec(content);
|
|
239
|
+
while (match !== null) {
|
|
240
|
+
const params = {};
|
|
241
|
+
for (const part of match[1].split(',')) {
|
|
242
|
+
const eq = part.indexOf('=');
|
|
243
|
+
if (eq <= 0)
|
|
244
|
+
continue;
|
|
245
|
+
params[part.slice(0, eq)] = part
|
|
246
|
+
.slice(eq + 1)
|
|
247
|
+
.replace(/&/g, '&')
|
|
248
|
+
.replace(/[/g, '[')
|
|
249
|
+
.replace(/]/g, ']')
|
|
250
|
+
.replace(/,/g, ',');
|
|
251
|
+
}
|
|
252
|
+
const source = imageSourceFromSegment(params);
|
|
253
|
+
if (source)
|
|
254
|
+
refs.push({ source, label: '[图片]', segment: params });
|
|
255
|
+
match = re.exec(content);
|
|
256
|
+
}
|
|
257
|
+
return refs;
|
|
258
|
+
}
|
|
259
|
+
function extractImageRefsFromContent(content) {
|
|
260
|
+
if (typeof content === 'string')
|
|
261
|
+
return extractCqImageRefs(content);
|
|
262
|
+
if (!Array.isArray(content))
|
|
263
|
+
return [];
|
|
264
|
+
const refs = [];
|
|
265
|
+
for (const seg of content) {
|
|
266
|
+
if (!seg || typeof seg !== 'object')
|
|
267
|
+
continue;
|
|
268
|
+
const segment = seg;
|
|
269
|
+
if (segment.type !== 'image')
|
|
270
|
+
continue;
|
|
271
|
+
const data = segment.data ?? {};
|
|
272
|
+
const source = imageSourceFromSegment(data);
|
|
273
|
+
if (source)
|
|
274
|
+
refs.push({ source, label: '[图片]', segment: data });
|
|
275
|
+
}
|
|
276
|
+
return refs;
|
|
277
|
+
}
|
|
278
|
+
function getForwardMessages(data) {
|
|
279
|
+
const root = data;
|
|
280
|
+
if (Array.isArray(root))
|
|
281
|
+
return root;
|
|
282
|
+
if (Array.isArray(root?.messages)) {
|
|
283
|
+
return root.messages;
|
|
284
|
+
}
|
|
285
|
+
if (Array.isArray(root?.message)) {
|
|
286
|
+
return root.message;
|
|
287
|
+
}
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
function getForwardNodeData(item) {
|
|
291
|
+
const node = item && typeof item === 'object' ? item : {};
|
|
292
|
+
if (node.type === 'node' && node.data && typeof node.data === 'object') {
|
|
293
|
+
return node.data;
|
|
294
|
+
}
|
|
295
|
+
return node;
|
|
296
|
+
}
|
|
297
|
+
function getForwardNodeContent(item) {
|
|
298
|
+
const node = item && typeof item === 'object' ? item : {};
|
|
299
|
+
const nodeData = getForwardNodeData(item);
|
|
300
|
+
return nodeData.content ?? node.content ?? nodeData.message ?? node.message;
|
|
301
|
+
}
|
|
302
|
+
function collectForwardImageRefs(data, limit) {
|
|
303
|
+
return getForwardMessages(data)
|
|
304
|
+
.slice(0, limit)
|
|
305
|
+
.flatMap(item => extractImageRefsFromContent(getForwardNodeContent(item)));
|
|
306
|
+
}
|
|
307
|
+
async function resolveForwardImageSource(ctx, storage, selfId, ref) {
|
|
308
|
+
if (/^(https?:|data:)/i.test(ref.source))
|
|
309
|
+
return ref.source;
|
|
310
|
+
try {
|
|
311
|
+
const imageData = (await callAction(ctx, selfId, 'get_image', { file: ref.source }));
|
|
312
|
+
const resolvedSource = imageData.url ?? imageData.file ?? ref.source;
|
|
313
|
+
if (typeof resolvedSource === 'string') {
|
|
314
|
+
if (/^(https?:|data:)/i.test(resolvedSource))
|
|
315
|
+
return resolvedSource;
|
|
316
|
+
const dataUri = await localImageToDataUri(storage, resolvedSource);
|
|
317
|
+
if (dataUri)
|
|
318
|
+
return dataUri;
|
|
319
|
+
return resolvedSource;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
ctx.logger.debug(`get_image 解析转发图片失败 (${ref.source}): ${err}`);
|
|
324
|
+
}
|
|
325
|
+
const dataUri = await localImageToDataUri(storage, ref.source);
|
|
326
|
+
return dataUri ?? ref.source;
|
|
327
|
+
}
|
|
328
|
+
async function recognizeForwardImages(ctx, storage, selfId, data, limit) {
|
|
329
|
+
const refs = collectForwardImageRefs(data, limit);
|
|
330
|
+
const imageDescriptions = new Map();
|
|
331
|
+
if (refs.length === 0)
|
|
332
|
+
return { imageDescriptions };
|
|
333
|
+
const mediaSvc = ctx.getService('media');
|
|
334
|
+
if (!mediaSvc?.describeImage) {
|
|
335
|
+
ctx.logger.debug(`合并转发包含 ${refs.length} 张图片,但 media 服务不可用`);
|
|
336
|
+
return { imageDescriptions };
|
|
337
|
+
}
|
|
338
|
+
const seen = new Set();
|
|
339
|
+
for (const ref of refs) {
|
|
340
|
+
const key = imageRefKey(ref);
|
|
341
|
+
if (seen.has(key))
|
|
342
|
+
continue;
|
|
343
|
+
seen.add(key);
|
|
344
|
+
try {
|
|
345
|
+
const imageSource = await resolveForwardImageSource(ctx, storage, selfId, ref);
|
|
346
|
+
const description = await mediaSvc.describeImage(imageSource);
|
|
347
|
+
if (description)
|
|
348
|
+
imageDescriptions.set(key, description);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
ctx.logger.debug(`合并转发图片识别失败 (${ref.source}): ${err}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return { imageDescriptions };
|
|
355
|
+
}
|
|
356
|
+
function formatMessageContent(content, context) {
|
|
357
|
+
if (typeof content === 'string')
|
|
358
|
+
return content;
|
|
359
|
+
if (!Array.isArray(content))
|
|
360
|
+
return content == null ? '' : JSON.stringify(content);
|
|
361
|
+
return content
|
|
362
|
+
.map(seg => {
|
|
363
|
+
if (!seg || typeof seg !== 'object')
|
|
364
|
+
return String(seg ?? '');
|
|
365
|
+
const segment = seg;
|
|
366
|
+
const data = segment.data ?? {};
|
|
367
|
+
switch (segment.type) {
|
|
368
|
+
case 'text':
|
|
369
|
+
return String(data.text ?? '');
|
|
370
|
+
case 'at':
|
|
371
|
+
return data.qq === 'all' ? '@全体成员' : `@${String(data.qq ?? '')}`;
|
|
372
|
+
case 'image': {
|
|
373
|
+
const source = imageSourceFromSegment(data);
|
|
374
|
+
const desc = source
|
|
375
|
+
? context?.imageDescriptions.get(imageRefKey({ source, label: '[图片]', segment: data }))
|
|
376
|
+
: undefined;
|
|
377
|
+
return desc ? `[图片: ${desc}]` : '[图片]';
|
|
378
|
+
}
|
|
379
|
+
case 'face':
|
|
380
|
+
return `[表情:${String(data.id ?? '')}]`;
|
|
381
|
+
case 'reply':
|
|
382
|
+
return '';
|
|
383
|
+
case 'forward':
|
|
384
|
+
return data.id ? `[合并转发:${String(data.id)}]` : '[合并转发]';
|
|
385
|
+
case 'record':
|
|
386
|
+
return '[语音]';
|
|
387
|
+
case 'video':
|
|
388
|
+
return '[视频]';
|
|
389
|
+
case 'share':
|
|
390
|
+
return `[分享:${String(data.title ?? '')}]`;
|
|
391
|
+
case 'json':
|
|
392
|
+
return '[JSON卡片]';
|
|
393
|
+
case 'xml':
|
|
394
|
+
return '[XML卡片]';
|
|
395
|
+
default:
|
|
396
|
+
return segment.type ? `[${segment.type}]` : '';
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
.join('');
|
|
400
|
+
}
|
|
401
|
+
function formatCqMessageContent(content, context) {
|
|
402
|
+
let imageIndex = 0;
|
|
403
|
+
return content.replace(/\[CQ:image,([^\]]+)\]/g, raw => {
|
|
404
|
+
const refs = extractCqImageRefs(raw);
|
|
405
|
+
const ref = refs[0];
|
|
406
|
+
if (!ref)
|
|
407
|
+
return '[图片]';
|
|
408
|
+
imageIndex++;
|
|
409
|
+
const desc = context?.imageDescriptions.get(imageRefKey(ref));
|
|
410
|
+
return desc ? `[图片${imageIndex > 1 ? imageIndex : ''}: ${desc}]` : '[图片]';
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
function formatForwardMessage(data, limit, context) {
|
|
414
|
+
const rawMessages = getForwardMessages(data);
|
|
415
|
+
const messages = rawMessages.slice(0, limit);
|
|
416
|
+
const lines = messages.map((item, index) => {
|
|
417
|
+
const node = item && typeof item === 'object' ? item : {};
|
|
418
|
+
const nodeData = getForwardNodeData(item);
|
|
419
|
+
const sender = node.sender && typeof node.sender === 'object' ? node.sender : undefined;
|
|
420
|
+
const name = String(nodeData.nickname ??
|
|
421
|
+
sender?.nickname ??
|
|
422
|
+
nodeData.name ??
|
|
423
|
+
nodeData.user_id ??
|
|
424
|
+
sender?.user_id ??
|
|
425
|
+
`节点${index + 1}`);
|
|
426
|
+
const userId = nodeData.user_id ?? nodeData.uin ?? sender?.user_id;
|
|
427
|
+
const prefix = userId != null ? `${name}(${String(userId)})` : name;
|
|
428
|
+
const rawContent = getForwardNodeContent(item);
|
|
429
|
+
const content = typeof rawContent === 'string'
|
|
430
|
+
? formatCqMessageContent(rawContent, context)
|
|
431
|
+
: formatMessageContent(rawContent, context);
|
|
432
|
+
return `${index + 1}. ${prefix}: ${content || '[空消息]'}`;
|
|
433
|
+
});
|
|
434
|
+
const header = `合并转发共 ${rawMessages.length} 条${rawMessages.length > messages.length ? `,以下显示前 ${messages.length} 条` : ''}:`;
|
|
435
|
+
return lines.length > 0 ? `${header}\n${lines.join('\n')}` : '合并转发内容为空或当前 OneBot 实现返回格式无法识别';
|
|
436
|
+
}
|
|
437
|
+
// ===== 权限检查辅助 =====
|
|
438
|
+
const ROLE_LEVEL = { owner: 3, admin: 2, member: 1 };
|
|
439
|
+
function roleLevel(role) {
|
|
440
|
+
return ROLE_LEVEL[role] ?? 0;
|
|
441
|
+
}
|
|
442
|
+
/** 查询群成员信息(失败返回 null) */
|
|
443
|
+
async function getGroupMemberInfo(ctx, selfId, groupId, userId) {
|
|
444
|
+
try {
|
|
445
|
+
return (await callAction(ctx, selfId, 'get_group_member_info', {
|
|
446
|
+
group_id: Number(groupId),
|
|
447
|
+
user_id: Number(userId),
|
|
448
|
+
no_cache: true,
|
|
449
|
+
}));
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/** 检查管理操作权限,返回错误消息(null 表示通过或无法验证) */
|
|
456
|
+
async function checkAdminPermission(ctx, selfId, groupId, targetUserId, requireOwner) {
|
|
457
|
+
const selfInfo = await getGroupMemberInfo(ctx, selfId, groupId, selfId);
|
|
458
|
+
if (!selfInfo)
|
|
459
|
+
return null; // 无法获取自身信息,跳过权限检查
|
|
460
|
+
const selfRole = String(selfInfo.role ?? 'member');
|
|
461
|
+
if (requireOwner && selfRole !== 'owner') {
|
|
462
|
+
return `操作失败:此操作仅群主可执行(当前角色:${selfRole})`;
|
|
463
|
+
}
|
|
464
|
+
if (selfRole === 'member') {
|
|
465
|
+
return `操作失败:机器人不是管理员(当前角色:${selfRole}),无法执行管理操作`;
|
|
466
|
+
}
|
|
467
|
+
if (targetUserId) {
|
|
468
|
+
const targetInfo = await getGroupMemberInfo(ctx, selfId, groupId, targetUserId);
|
|
469
|
+
if (!targetInfo) {
|
|
470
|
+
return `操作失败:无法获取用户 ${targetUserId} 的信息,该用户可能不在群中`;
|
|
471
|
+
}
|
|
472
|
+
const targetRole = String(targetInfo.role ?? 'member');
|
|
473
|
+
if (roleLevel(targetRole) >= roleLevel(selfRole)) {
|
|
474
|
+
const targetLabel = targetRole === 'owner' ? '群主' : '管理员';
|
|
475
|
+
return `操作失败:无法对${targetLabel}执行此操作(机器人角色:${selfRole},目标角色:${targetRole})`;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
export function apply(ctx, config) {
|
|
481
|
+
const tools = useToolService(ctx);
|
|
482
|
+
const storage = createStorageGateway(ctx);
|
|
483
|
+
const bundle = {
|
|
484
|
+
daily: toolsWithGroups(tools, ['onebot-daily']),
|
|
485
|
+
group: toolsWithGroups(tools, ['onebot-group']),
|
|
486
|
+
personal: toolsWithGroups(tools, ['onebot-personal']),
|
|
487
|
+
};
|
|
488
|
+
// 仅当 OneBot 平台可用时才注册工具
|
|
489
|
+
// 使用 ready 事件确保平台已加载
|
|
490
|
+
ctx.on('ready', () => {
|
|
491
|
+
if (!getPlatformNames(ctx).includes('onebot')) {
|
|
492
|
+
ctx.logger.info('未检测到 OneBot 平台,跳过 OneBot 工具注册');
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
ctx.logger.info('检测到 OneBot 平台,开始注册 OneBot 工具');
|
|
496
|
+
// 注册工具分组:按职责语义切分,彼此不重叠
|
|
497
|
+
tools.registerGroup({
|
|
498
|
+
name: 'onebot-daily',
|
|
499
|
+
label: 'OneBot 只读与日常',
|
|
500
|
+
description: '只读查询(群信息/成员/好友列表/历史/登录信息)+ 日常低风险互动(戳一戳、好友赞)',
|
|
501
|
+
});
|
|
502
|
+
tools.registerGroup({
|
|
503
|
+
name: 'onebot-group',
|
|
504
|
+
label: 'OneBot 群务',
|
|
505
|
+
description: '群务管理:禁言/全员禁言/踢人/撤回消息/设置群名片/改群名/设管理员/群打卡/审批加群申请 等',
|
|
506
|
+
});
|
|
507
|
+
tools.registerGroup({
|
|
508
|
+
name: 'onebot-personal',
|
|
509
|
+
label: 'OneBot 个人/私聊',
|
|
510
|
+
description: '影响 bot 账号本身的人际关系:退群、删好友、处理好友申请、接受/拒绝入群邀请',
|
|
511
|
+
});
|
|
512
|
+
const cfg = {
|
|
513
|
+
groupManagement: { enabled: true, ...(config.groupManagement ?? {}) },
|
|
514
|
+
groupInfo: { enabled: true, ...(config.groupInfo ?? {}) },
|
|
515
|
+
account: { enabled: true, ...(config.account ?? {}) },
|
|
516
|
+
interaction: { enabled: true, ...(config.interaction ?? {}) },
|
|
517
|
+
sessionHistory: {
|
|
518
|
+
enabled: true,
|
|
519
|
+
maxLimit: 100,
|
|
520
|
+
defaultLimit: 20,
|
|
521
|
+
allowGroupReadPrivate: false,
|
|
522
|
+
allowCrossSelf: false,
|
|
523
|
+
allowCrossGroup: true,
|
|
524
|
+
allowCrossPrivate: false,
|
|
525
|
+
...(config.sessionHistory ?? {}),
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
if (cfg.groupManagement.enabled)
|
|
529
|
+
registerGroupManagementTools(ctx, bundle);
|
|
530
|
+
if (cfg.groupInfo.enabled)
|
|
531
|
+
registerGroupInfoTools(ctx, storage, bundle);
|
|
532
|
+
if (cfg.account.enabled)
|
|
533
|
+
registerAccountTools(ctx, bundle);
|
|
534
|
+
if (cfg.interaction.enabled)
|
|
535
|
+
registerInteractionTools(ctx, bundle);
|
|
536
|
+
if (cfg.sessionHistory.enabled) {
|
|
537
|
+
const maxLimit = Math.max(1, Math.min(1000, Number(cfg.sessionHistory.maxLimit) || 100));
|
|
538
|
+
const defaultLimitRaw = Math.max(1, Math.floor(Number(cfg.sessionHistory.defaultLimit) || 20));
|
|
539
|
+
const historyCfg = {
|
|
540
|
+
maxLimit,
|
|
541
|
+
defaultLimit: Math.min(defaultLimitRaw, maxLimit),
|
|
542
|
+
allowGroupReadPrivate: cfg.sessionHistory.allowGroupReadPrivate === true,
|
|
543
|
+
allowCrossSelf: cfg.sessionHistory.allowCrossSelf === true,
|
|
544
|
+
allowCrossGroup: cfg.sessionHistory.allowCrossGroup !== false,
|
|
545
|
+
allowCrossPrivate: cfg.sessionHistory.allowCrossPrivate === true,
|
|
546
|
+
};
|
|
547
|
+
registerSessionHistoryTools(ctx, bundle, historyCfg);
|
|
548
|
+
registerOneBotHistoryAccessChecker(ctx, historyCfg);
|
|
549
|
+
}
|
|
550
|
+
registerRequestTools(ctx, bundle);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
// ===== 群管理工具 =====
|
|
554
|
+
function registerGroupManagementTools(ctx, bundle) {
|
|
555
|
+
const { group, personal } = bundle;
|
|
556
|
+
// ---- 群禁言(单人)----
|
|
557
|
+
group.register({
|
|
558
|
+
definition: {
|
|
559
|
+
type: 'function',
|
|
560
|
+
function: {
|
|
561
|
+
name: 'onebot_group_ban',
|
|
562
|
+
description: '在QQ群中禁言指定用户。duration 为 0 表示解除禁言。可传 group_id 跨群操作;不传则默认当前群。',
|
|
563
|
+
parameters: {
|
|
564
|
+
type: 'object',
|
|
565
|
+
properties: {
|
|
566
|
+
user_id: { type: 'string', description: '要禁言的用户QQ号' },
|
|
567
|
+
duration: { type: 'number', description: '禁言时长(秒),0 = 解除禁言,默认 60' },
|
|
568
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
569
|
+
},
|
|
570
|
+
required: ['user_id'],
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
handler: async (args, callCtx) => {
|
|
575
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
576
|
+
const duration = typeof args.duration === 'number' ? args.duration : 60;
|
|
577
|
+
const permError = await checkAdminPermission(ctx, selfId, groupId, String(args.user_id));
|
|
578
|
+
if (permError)
|
|
579
|
+
return permError;
|
|
580
|
+
await callAction(ctx, selfId, 'set_group_ban', {
|
|
581
|
+
group_id: Number(groupId),
|
|
582
|
+
user_id: Number(args.user_id),
|
|
583
|
+
duration,
|
|
584
|
+
});
|
|
585
|
+
// 验证禁言是否生效
|
|
586
|
+
const info = await getGroupMemberInfo(ctx, selfId, groupId, String(args.user_id));
|
|
587
|
+
if (info && 'shut_up_timestamp' in info) {
|
|
588
|
+
const shutUp = Number(info.shut_up_timestamp);
|
|
589
|
+
const now = Math.floor(Date.now() / 1000);
|
|
590
|
+
if (duration === 0) {
|
|
591
|
+
return shutUp <= now ? `已解除 ${args.user_id} 的禁言` : `解除禁言指令已发送,但用户仍在禁言中`;
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
return shutUp > now
|
|
595
|
+
? `已禁言 ${args.user_id},时长 ${duration} 秒`
|
|
596
|
+
: `禁言操作未生效(API 返回成功但禁言状态未变化),请检查机器人实际权限`;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return duration === 0
|
|
600
|
+
? `已解除 ${args.user_id} 的禁言`
|
|
601
|
+
: `已禁言 ${args.user_id},时长 ${duration} 秒(无法验证是否生效)`;
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
// ---- 全群禁言 ----
|
|
605
|
+
group.register({
|
|
606
|
+
definition: {
|
|
607
|
+
type: 'function',
|
|
608
|
+
function: {
|
|
609
|
+
name: 'onebot_group_whole_ban',
|
|
610
|
+
description: '开启/关闭全群禁言。可传 group_id 跨群操作;不传则默认当前群。',
|
|
611
|
+
parameters: {
|
|
612
|
+
type: 'object',
|
|
613
|
+
properties: {
|
|
614
|
+
enable: { type: 'boolean', description: 'true = 开启全群禁言,false = 关闭' },
|
|
615
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
616
|
+
},
|
|
617
|
+
required: ['enable'],
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
handler: async (args, callCtx) => {
|
|
622
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
623
|
+
const permError = await checkAdminPermission(ctx, selfId, groupId);
|
|
624
|
+
if (permError)
|
|
625
|
+
return permError;
|
|
626
|
+
await callAction(ctx, selfId, 'set_group_whole_ban', {
|
|
627
|
+
group_id: Number(groupId),
|
|
628
|
+
enable: !!args.enable,
|
|
629
|
+
});
|
|
630
|
+
return args.enable ? '已开启全群禁言' : '已关闭全群禁言';
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
// ---- 踢人 ----
|
|
634
|
+
group.register({
|
|
635
|
+
definition: {
|
|
636
|
+
type: 'function',
|
|
637
|
+
function: {
|
|
638
|
+
name: 'onebot_group_kick',
|
|
639
|
+
description: '将指定用户踢出群聊。可传 group_id 跨群操作;不传则默认当前群。',
|
|
640
|
+
parameters: {
|
|
641
|
+
type: 'object',
|
|
642
|
+
properties: {
|
|
643
|
+
user_id: { type: 'string', description: '要踢出的用户QQ号' },
|
|
644
|
+
reject_add_request: { type: 'boolean', description: '是否拒绝此人再次加群,默认 false' },
|
|
645
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
646
|
+
},
|
|
647
|
+
required: ['user_id'],
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
handler: async (args, callCtx) => {
|
|
652
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
653
|
+
const permError = await checkAdminPermission(ctx, selfId, groupId, String(args.user_id));
|
|
654
|
+
if (permError)
|
|
655
|
+
return permError;
|
|
656
|
+
await callAction(ctx, selfId, 'set_group_kick', {
|
|
657
|
+
group_id: Number(groupId),
|
|
658
|
+
user_id: Number(args.user_id),
|
|
659
|
+
reject_add_request: !!args.reject_add_request,
|
|
660
|
+
});
|
|
661
|
+
return `已将 ${args.user_id} 踢出群 ${groupId}`;
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
// ---- 主动退群 ----
|
|
665
|
+
personal.register({
|
|
666
|
+
definition: {
|
|
667
|
+
type: 'function',
|
|
668
|
+
function: {
|
|
669
|
+
name: 'onebot_leave_group',
|
|
670
|
+
description: '让机器人主动退出指定 QQ 群。适用于群内持续骚扰、无意义刷屏或不希望继续参与的场景。不会解散群。',
|
|
671
|
+
parameters: {
|
|
672
|
+
type: 'object',
|
|
673
|
+
properties: {
|
|
674
|
+
group_id: { type: 'string', description: '要退出的群号;在群聊中可省略,默认当前群' },
|
|
675
|
+
},
|
|
676
|
+
required: [],
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
handler: async (args, callCtx) => {
|
|
681
|
+
const argGroupId = args.group_id != null && String(args.group_id).trim() ? String(args.group_id).trim() : '';
|
|
682
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
683
|
+
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
684
|
+
const groupId = argGroupId || (parsed?.detailType === 'group' ? parsed.targetId : '');
|
|
685
|
+
if (!groupId)
|
|
686
|
+
return '请提供 group_id,或在要退出的群聊中调用此工具';
|
|
687
|
+
await callAction(ctx, selfId, 'set_group_leave', {
|
|
688
|
+
group_id: Number(groupId),
|
|
689
|
+
is_dismiss: false,
|
|
690
|
+
});
|
|
691
|
+
return `已退出群 ${groupId}`;
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
// ---- 设置群名片 ----
|
|
695
|
+
group.register({
|
|
696
|
+
definition: {
|
|
697
|
+
type: 'function',
|
|
698
|
+
function: {
|
|
699
|
+
name: 'onebot_set_group_card',
|
|
700
|
+
description: '设置指定用户在群中的名片(群昵称)。可传 group_id 跨群操作。',
|
|
701
|
+
parameters: {
|
|
702
|
+
type: 'object',
|
|
703
|
+
properties: {
|
|
704
|
+
user_id: { type: 'string', description: '用户QQ号' },
|
|
705
|
+
card: { type: 'string', description: '新的群名片,空字符串表示取消' },
|
|
706
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
707
|
+
},
|
|
708
|
+
required: ['user_id', 'card'],
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
handler: async (args, callCtx) => {
|
|
713
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
714
|
+
const permError = await checkAdminPermission(ctx, selfId, groupId);
|
|
715
|
+
if (permError)
|
|
716
|
+
return permError;
|
|
717
|
+
await callAction(ctx, selfId, 'set_group_card', {
|
|
718
|
+
group_id: Number(groupId),
|
|
719
|
+
user_id: Number(args.user_id),
|
|
720
|
+
card: String(args.card ?? ''),
|
|
721
|
+
});
|
|
722
|
+
return `已设置 ${args.user_id} 的群名片为: ${args.card || '(空)'}`;
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
// ---- 设置群名 ----
|
|
726
|
+
group.register({
|
|
727
|
+
definition: {
|
|
728
|
+
type: 'function',
|
|
729
|
+
function: {
|
|
730
|
+
name: 'onebot_set_group_name',
|
|
731
|
+
description: '修改群名称。可传 group_id 跨群操作。',
|
|
732
|
+
parameters: {
|
|
733
|
+
type: 'object',
|
|
734
|
+
properties: {
|
|
735
|
+
group_name: { type: 'string', description: '新的群名称' },
|
|
736
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
737
|
+
},
|
|
738
|
+
required: ['group_name'],
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
handler: async (args, callCtx) => {
|
|
743
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
744
|
+
const permError = await checkAdminPermission(ctx, selfId, groupId);
|
|
745
|
+
if (permError)
|
|
746
|
+
return permError;
|
|
747
|
+
await callAction(ctx, selfId, 'set_group_name', {
|
|
748
|
+
group_id: Number(groupId),
|
|
749
|
+
group_name: String(args.group_name),
|
|
750
|
+
});
|
|
751
|
+
return `已将群 ${groupId} 的名称修改为: ${args.group_name}`;
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
// ---- 设置专属头衔 ----
|
|
755
|
+
group.register({
|
|
756
|
+
definition: {
|
|
757
|
+
type: 'function',
|
|
758
|
+
function: {
|
|
759
|
+
name: 'onebot_set_group_special_title',
|
|
760
|
+
description: '设置群成员专属头衔(仅群主可用)。可传 group_id 跨群操作。',
|
|
761
|
+
parameters: {
|
|
762
|
+
type: 'object',
|
|
763
|
+
properties: {
|
|
764
|
+
user_id: { type: 'string', description: '用户QQ号' },
|
|
765
|
+
special_title: { type: 'string', description: '专属头衔,空字符串表示取消' },
|
|
766
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
767
|
+
},
|
|
768
|
+
required: ['user_id', 'special_title'],
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
handler: async (args, callCtx) => {
|
|
773
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
774
|
+
const permError = await checkAdminPermission(ctx, selfId, groupId, undefined, true);
|
|
775
|
+
if (permError)
|
|
776
|
+
return permError;
|
|
777
|
+
await callAction(ctx, selfId, 'set_group_special_title', {
|
|
778
|
+
group_id: Number(groupId),
|
|
779
|
+
user_id: Number(args.user_id),
|
|
780
|
+
special_title: String(args.special_title ?? ''),
|
|
781
|
+
duration: -1,
|
|
782
|
+
});
|
|
783
|
+
return `已设置 ${args.user_id} 的专属头衔为: ${args.special_title || '(空)'}`;
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
// ---- 设置管理员 ----
|
|
787
|
+
group.register({
|
|
788
|
+
definition: {
|
|
789
|
+
type: 'function',
|
|
790
|
+
function: {
|
|
791
|
+
name: 'onebot_set_group_admin',
|
|
792
|
+
description: '设置/取消群管理员(仅群主可用)。可传 group_id 跨群操作。',
|
|
793
|
+
parameters: {
|
|
794
|
+
type: 'object',
|
|
795
|
+
properties: {
|
|
796
|
+
user_id: { type: 'string', description: '用户QQ号' },
|
|
797
|
+
enable: { type: 'boolean', description: 'true = 设为管理员,false = 取消管理员' },
|
|
798
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
799
|
+
},
|
|
800
|
+
required: ['user_id', 'enable'],
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
handler: async (args, callCtx) => {
|
|
805
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
806
|
+
const permError = await checkAdminPermission(ctx, selfId, groupId, undefined, true);
|
|
807
|
+
if (permError)
|
|
808
|
+
return permError;
|
|
809
|
+
await callAction(ctx, selfId, 'set_group_admin', {
|
|
810
|
+
group_id: Number(groupId),
|
|
811
|
+
user_id: Number(args.user_id),
|
|
812
|
+
enable: !!args.enable,
|
|
813
|
+
});
|
|
814
|
+
return args.enable ? `已将 ${args.user_id} 设为管理员` : `已取消 ${args.user_id} 的管理员`;
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
// ---- 撤回消息 ----
|
|
818
|
+
group.register({
|
|
819
|
+
definition: {
|
|
820
|
+
type: 'function',
|
|
821
|
+
function: {
|
|
822
|
+
name: 'onebot_delete_msg',
|
|
823
|
+
description: '撤回一条消息(群聊或私聊均可)。需要 message_id(来自历史消息或事件)。机器人对他人消息的撤回需要管理员权限。',
|
|
824
|
+
parameters: {
|
|
825
|
+
type: 'object',
|
|
826
|
+
properties: {
|
|
827
|
+
message_id: { type: 'string', description: '要撤回的消息 ID' },
|
|
828
|
+
},
|
|
829
|
+
required: ['message_id'],
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
handler: async (args, callCtx) => {
|
|
834
|
+
// 不限制必须是群聊;只要能找到可用的 OneBot 账号即可
|
|
835
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
836
|
+
await callAction(ctx, selfId, 'delete_msg', {
|
|
837
|
+
message_id: Number(args.message_id),
|
|
838
|
+
});
|
|
839
|
+
return `已撤回消息 ${args.message_id}`;
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
ctx.logger.info('OneBot 群管理工具已注册');
|
|
843
|
+
}
|
|
844
|
+
// ===== 群信息查询工具 =====
|
|
845
|
+
function registerGroupInfoTools(ctx, storage, bundle) {
|
|
846
|
+
const { daily } = bundle;
|
|
847
|
+
// ---- 查看合并转发 ----
|
|
848
|
+
daily.register({
|
|
849
|
+
definition: {
|
|
850
|
+
type: 'function',
|
|
851
|
+
function: {
|
|
852
|
+
name: 'onebot_get_forward_msg',
|
|
853
|
+
description: '读取合并转发消息内容(OneBot v11 标准 get_forward_msg)。收到 <forward id="...">[合并转发消息]</forward> 时,必须把尖括号里的 id 字符串作为参数 id 传入;不要使用 message_id、forward_id、res_id 或 m_resid。',
|
|
854
|
+
parameters: {
|
|
855
|
+
type: 'object',
|
|
856
|
+
properties: {
|
|
857
|
+
id: {
|
|
858
|
+
type: 'string',
|
|
859
|
+
description: '合并转发 ID(forward 消息段的 data.id),与 OneBot v11 标准参数名一致',
|
|
860
|
+
},
|
|
861
|
+
limit: { type: 'number', description: '最多返回多少条节点,默认 30' },
|
|
862
|
+
},
|
|
863
|
+
required: ['id'],
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
handler: async (args, callCtx) => {
|
|
868
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
869
|
+
if (!args.id)
|
|
870
|
+
return '参数错误:缺少 id(合并转发 ID)。请使用 <forward id="..."> 中的 id 字符串,不要使用 message_id。';
|
|
871
|
+
const forwardId = String(args.id);
|
|
872
|
+
const limit = Math.max(1, Math.min(100, typeof args.limit === 'number' ? Math.floor(args.limit) : 30));
|
|
873
|
+
// 适配器的 callAction 已对 get_forward_msg 做了多参数键回退(id/message_id/res_id/m_resid)
|
|
874
|
+
// 并维护了一份接收时即抓取并展开(含摘要与图像识别)的缓存。
|
|
875
|
+
let data;
|
|
876
|
+
try {
|
|
877
|
+
data = await callAction(ctx, selfId, 'get_forward_msg', { id: forwardId });
|
|
878
|
+
}
|
|
879
|
+
catch (err) {
|
|
880
|
+
return `合并转发读取失败:${err instanceof Error ? err.message : String(err)}。该转发可能已在协议端过期,或当前 OneBot 实现不支持跨会话读取。`;
|
|
881
|
+
}
|
|
882
|
+
// 适配器在缓存命中时返回 ForwardEntry 形状(含 fullText / summary / 元信息)。
|
|
883
|
+
// 此时直接返回原文给 LLM,无需再做协议端结构解析。
|
|
884
|
+
if (data && typeof data === 'object' && data.__aalisForwardEntry) {
|
|
885
|
+
const entry = data;
|
|
886
|
+
const header = `合并转发共 ${entry.count} 条,参与人:${entry.participants.join(', ') || '未知'}`;
|
|
887
|
+
const summaryLine = entry.summary ? `\n摘要:${entry.summary}\n` : '\n';
|
|
888
|
+
return `${header}${summaryLine}\n原文:\n${entry.fullText}`;
|
|
889
|
+
}
|
|
890
|
+
const formatContext = await recognizeForwardImages(ctx, storage, selfId, data, limit);
|
|
891
|
+
return formatForwardMessage(data, limit, formatContext);
|
|
892
|
+
},
|
|
893
|
+
});
|
|
894
|
+
// ---- 获取群信息 ----
|
|
895
|
+
daily.register({
|
|
896
|
+
definition: {
|
|
897
|
+
type: 'function',
|
|
898
|
+
function: {
|
|
899
|
+
name: 'onebot_get_group_info',
|
|
900
|
+
description: '获取指定群的基本信息(群名、人数等)。可传 group_id 跨群查询;不传则默认当前群。',
|
|
901
|
+
parameters: {
|
|
902
|
+
type: 'object',
|
|
903
|
+
properties: {
|
|
904
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
905
|
+
},
|
|
906
|
+
required: [],
|
|
907
|
+
},
|
|
908
|
+
},
|
|
909
|
+
},
|
|
910
|
+
handler: async (args, callCtx) => {
|
|
911
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
912
|
+
const data = await callAction(ctx, selfId, 'get_group_info', {
|
|
913
|
+
group_id: Number(groupId),
|
|
914
|
+
});
|
|
915
|
+
return JSON.stringify(data);
|
|
916
|
+
},
|
|
917
|
+
});
|
|
918
|
+
// ---- 获取群成员信息(user_id 可缺省查自身)----
|
|
919
|
+
daily.register({
|
|
920
|
+
definition: {
|
|
921
|
+
type: 'function',
|
|
922
|
+
function: {
|
|
923
|
+
name: 'onebot_get_group_member_info',
|
|
924
|
+
description: '获取群成员详细信息(昵称、群名片、角色、禁言状态等)。不传 user_id 时查询机器人自身。可传 group_id 跨群查询。',
|
|
925
|
+
parameters: {
|
|
926
|
+
type: 'object',
|
|
927
|
+
properties: {
|
|
928
|
+
user_id: { type: 'string', description: '可选:要查询的用户 QQ 号。缺省查询机器人自身。' },
|
|
929
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
930
|
+
},
|
|
931
|
+
required: [],
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
handler: async (args, callCtx) => {
|
|
936
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
937
|
+
const userId = args.user_id ? String(args.user_id) : selfId;
|
|
938
|
+
const data = await callAction(ctx, selfId, 'get_group_member_info', {
|
|
939
|
+
group_id: Number(groupId),
|
|
940
|
+
user_id: Number(userId),
|
|
941
|
+
no_cache: true,
|
|
942
|
+
});
|
|
943
|
+
return JSON.stringify(data);
|
|
944
|
+
},
|
|
945
|
+
});
|
|
946
|
+
// ---- 获取群成员列表(支持搜索 + 分页)----
|
|
947
|
+
daily.register({
|
|
948
|
+
definition: {
|
|
949
|
+
type: 'function',
|
|
950
|
+
function: {
|
|
951
|
+
name: 'onebot_get_group_member_list',
|
|
952
|
+
description: '查询群的成员列表,支持按昵称/群名片/QQ号关键词搜索、按角色筛选、分页返回。大群(数百上千人)务必使用 keyword 或 role 过滤,避免一次拉取过多数据。可传 group_id 跨群查询。翻页:下次调用传 offset = 上次 offset + limit,直到 has_more=false。',
|
|
953
|
+
parameters: {
|
|
954
|
+
type: 'object',
|
|
955
|
+
properties: {
|
|
956
|
+
keyword: { type: 'string', description: '可选:按昵称、群名片或 QQ 号子串模糊匹配(不区分大小写)' },
|
|
957
|
+
role: { type: 'string', enum: ['owner', 'admin', 'member'], description: '可选:按成员角色筛选' },
|
|
958
|
+
limit: { type: 'number', description: '本页最多返回条数,默认 30' },
|
|
959
|
+
offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
|
|
960
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
961
|
+
},
|
|
962
|
+
required: [],
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
handler: async (args, callCtx) => {
|
|
967
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
968
|
+
const data = await callAction(ctx, selfId, 'get_group_member_list', {
|
|
969
|
+
group_id: Number(groupId),
|
|
970
|
+
});
|
|
971
|
+
const list = Array.isArray(data) ? data : [];
|
|
972
|
+
const keyword = typeof args.keyword === 'string' ? args.keyword.trim().toLowerCase() : '';
|
|
973
|
+
const roleFilter = typeof args.role === 'string' ? args.role : '';
|
|
974
|
+
const limit = Math.max(1, Math.floor(Number(args.limit) || 30));
|
|
975
|
+
const offset = Math.max(0, Math.floor(Number(args.offset) || 0));
|
|
976
|
+
// 精简 + 过滤
|
|
977
|
+
const all = list.map((m) => ({
|
|
978
|
+
user_id: m.user_id,
|
|
979
|
+
nickname: String(m.nickname ?? ''),
|
|
980
|
+
card: String(m.card ?? ''),
|
|
981
|
+
role: String(m.role ?? 'member'),
|
|
982
|
+
}));
|
|
983
|
+
const filtered = all.filter(m => {
|
|
984
|
+
if (roleFilter && m.role !== roleFilter)
|
|
985
|
+
return false;
|
|
986
|
+
if (keyword) {
|
|
987
|
+
const hay = `${m.user_id} ${m.nickname} ${m.card}`.toLowerCase();
|
|
988
|
+
if (!hay.includes(keyword))
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
return true;
|
|
992
|
+
});
|
|
993
|
+
const total = filtered.length;
|
|
994
|
+
const items = filtered.slice(offset, offset + limit);
|
|
995
|
+
return JSON.stringify({
|
|
996
|
+
groupTotal: all.length,
|
|
997
|
+
matched: total,
|
|
998
|
+
limit,
|
|
999
|
+
offset,
|
|
1000
|
+
returned: items.length,
|
|
1001
|
+
has_more: offset + items.length < total,
|
|
1002
|
+
...(keyword ? { keyword } : {}),
|
|
1003
|
+
...(roleFilter ? { role: roleFilter } : {}),
|
|
1004
|
+
members: items,
|
|
1005
|
+
});
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
// ---- 查询自身在当前群是否被禁言 ----
|
|
1009
|
+
daily.register({
|
|
1010
|
+
definition: {
|
|
1011
|
+
type: 'function',
|
|
1012
|
+
function: {
|
|
1013
|
+
name: 'onebot_get_self_mute_status',
|
|
1014
|
+
description: '查询机器人自身的禁言状态。默认查当前群;可传 group_id 跨群查询(也可用于在私聊中查某个群是否被禁言)。' +
|
|
1015
|
+
'当历史记忆里出现 [notice/group_ban] 提示时,可调用此工具确认当前实时状态再决定是否发言。',
|
|
1016
|
+
parameters: {
|
|
1017
|
+
type: 'object',
|
|
1018
|
+
properties: {
|
|
1019
|
+
group_id: {
|
|
1020
|
+
type: 'string',
|
|
1021
|
+
description: '可选。指定群号。缺省时查当前会话所在群(私聊时缺省会报错,请显式传入)。',
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
required: [],
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
handler: async (args, callCtx) => {
|
|
1029
|
+
const argGroupId = args.group_id != null && String(args.group_id).trim() ? String(args.group_id).trim() : '';
|
|
1030
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
1031
|
+
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
1032
|
+
let groupId = argGroupId;
|
|
1033
|
+
if (!groupId) {
|
|
1034
|
+
if (parsed?.detailType !== 'group') {
|
|
1035
|
+
return JSON.stringify({ available: false, reason: '当前不在群聊中,请显式传入 group_id' });
|
|
1036
|
+
}
|
|
1037
|
+
groupId = parsed.targetId;
|
|
1038
|
+
}
|
|
1039
|
+
const adapter = findOneBotAdapter(ctx);
|
|
1040
|
+
if (!adapter?.callAction)
|
|
1041
|
+
return JSON.stringify({ available: false, reason: 'OneBot 适配器不可用' });
|
|
1042
|
+
let info = null;
|
|
1043
|
+
try {
|
|
1044
|
+
info = (await callAction(ctx, selfId, 'get_group_member_info', {
|
|
1045
|
+
group_id: Number(groupId),
|
|
1046
|
+
user_id: Number(selfId),
|
|
1047
|
+
no_cache: true,
|
|
1048
|
+
}));
|
|
1049
|
+
}
|
|
1050
|
+
catch (err) {
|
|
1051
|
+
return JSON.stringify({
|
|
1052
|
+
available: false,
|
|
1053
|
+
reason: `查询失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
if (!info)
|
|
1057
|
+
return JSON.stringify({ available: false, reason: '无法获取群成员信息' });
|
|
1058
|
+
const ts = Number(info.shut_up_timestamp ?? 0);
|
|
1059
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
1060
|
+
const muted = ts > nowSec;
|
|
1061
|
+
return JSON.stringify({
|
|
1062
|
+
available: true,
|
|
1063
|
+
muted,
|
|
1064
|
+
shutUpTimestamp: ts,
|
|
1065
|
+
untilIso: muted ? new Date(ts * 1000).toISOString() : null,
|
|
1066
|
+
remainingSeconds: muted ? ts - nowSec : 0,
|
|
1067
|
+
groupId,
|
|
1068
|
+
selfId,
|
|
1069
|
+
});
|
|
1070
|
+
},
|
|
1071
|
+
});
|
|
1072
|
+
// ---- 列出所有当前被禁言的群(基于内存事件快照,跨会话可用) ----
|
|
1073
|
+
daily.register({
|
|
1074
|
+
definition: {
|
|
1075
|
+
type: 'function',
|
|
1076
|
+
function: {
|
|
1077
|
+
name: 'onebot_list_self_mutes',
|
|
1078
|
+
description: '列出机器人自身当前被禁言的所有群(基于已收到的禁言事件 + 启动后从 shut_up_timestamp 恢复的状态快照)。' +
|
|
1079
|
+
'可在任何会话(包括私聊)调用,用于自查"我目前在哪些群里被禁言、还剩多久"。' +
|
|
1080
|
+
'支持 limit/offset 翻页(默认按剩余时间倒序,最近会过期的在最前)。' +
|
|
1081
|
+
'注意:未收到事件且未触发过的群可能不在列表中,最权威的方式仍是 onebot_get_self_mute_status(group_id)。',
|
|
1082
|
+
parameters: {
|
|
1083
|
+
type: 'object',
|
|
1084
|
+
properties: {
|
|
1085
|
+
limit: { type: 'number', description: '最多返回多少条,默认 30,上限 200' },
|
|
1086
|
+
offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
|
|
1087
|
+
},
|
|
1088
|
+
required: [],
|
|
1089
|
+
},
|
|
1090
|
+
},
|
|
1091
|
+
},
|
|
1092
|
+
handler: async (args) => {
|
|
1093
|
+
const adapter = findOneBotAdapter(ctx);
|
|
1094
|
+
if (!adapter?.getSelfMutes) {
|
|
1095
|
+
return JSON.stringify({ supported: false, reason: '当前 OneBot 适配器版本不支持 getSelfMutes' });
|
|
1096
|
+
}
|
|
1097
|
+
const limit = Math.max(1, Math.min(200, Math.floor(Number(args.limit) || 30)));
|
|
1098
|
+
const offset = Math.max(0, Math.floor(Number(args.offset) || 0));
|
|
1099
|
+
const mutes = adapter
|
|
1100
|
+
.getSelfMutes()
|
|
1101
|
+
.slice()
|
|
1102
|
+
.sort((a, b) => a.remainingSec - b.remainingSec);
|
|
1103
|
+
const total = mutes.length;
|
|
1104
|
+
const sliced = mutes.slice(offset, offset + limit);
|
|
1105
|
+
return JSON.stringify({
|
|
1106
|
+
supported: true,
|
|
1107
|
+
total,
|
|
1108
|
+
offset,
|
|
1109
|
+
limit,
|
|
1110
|
+
count: sliced.length,
|
|
1111
|
+
hasMore: offset + sliced.length < total,
|
|
1112
|
+
mutes: sliced.map(m => ({
|
|
1113
|
+
selfId: m.selfId,
|
|
1114
|
+
groupId: m.groupId,
|
|
1115
|
+
untilIso: new Date(m.untilTs).toISOString(),
|
|
1116
|
+
remainingSeconds: m.remainingSec,
|
|
1117
|
+
})),
|
|
1118
|
+
});
|
|
1119
|
+
},
|
|
1120
|
+
});
|
|
1121
|
+
// ---- 获取消息 ----
|
|
1122
|
+
daily.register({
|
|
1123
|
+
definition: {
|
|
1124
|
+
type: 'function',
|
|
1125
|
+
function: {
|
|
1126
|
+
name: 'onebot_get_msg',
|
|
1127
|
+
description: '按 message_id 获取单条消息的完整内容(包括发送者、消息段等)。可用于回溯查看历史中的某条消息,或在收到撤回通知后查看被撤回的内容。不限定会话类型,群聊和私聊消息均可查询。',
|
|
1128
|
+
parameters: {
|
|
1129
|
+
type: 'object',
|
|
1130
|
+
properties: {
|
|
1131
|
+
message_id: { type: 'string', description: '消息 ID' },
|
|
1132
|
+
},
|
|
1133
|
+
required: ['message_id'],
|
|
1134
|
+
},
|
|
1135
|
+
},
|
|
1136
|
+
},
|
|
1137
|
+
handler: async (args, callCtx) => {
|
|
1138
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
1139
|
+
if (!args.message_id)
|
|
1140
|
+
return '参数错误:缺少 message_id';
|
|
1141
|
+
try {
|
|
1142
|
+
const data = await callAction(ctx, selfId, 'get_msg', {
|
|
1143
|
+
message_id: Number(args.message_id),
|
|
1144
|
+
});
|
|
1145
|
+
return JSON.stringify(data);
|
|
1146
|
+
}
|
|
1147
|
+
catch (err) {
|
|
1148
|
+
return `获取消息失败:${err instanceof Error ? err.message : String(err)}。该消息可能已过期或不在当前 OneBot 实现的作用域内。`;
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
// ---- 获取群荣誉信息 ----
|
|
1153
|
+
daily.register({
|
|
1154
|
+
definition: {
|
|
1155
|
+
type: 'function',
|
|
1156
|
+
function: {
|
|
1157
|
+
name: 'onebot_get_group_honor_info',
|
|
1158
|
+
description: '获取指定群的荣誉信息(龙王、群聊之火、群聊炽焰、冒尖小春笋、快乐之源)。可传 group_id 跨群查询。',
|
|
1159
|
+
parameters: {
|
|
1160
|
+
type: 'object',
|
|
1161
|
+
properties: {
|
|
1162
|
+
type: {
|
|
1163
|
+
type: 'string',
|
|
1164
|
+
enum: ['talkative', 'performer', 'legend', 'strong_newbie', 'emotion', 'all'],
|
|
1165
|
+
description: '荣誉类型:talkative=龙王, performer=群聊之火, legend=群聊炽焰, strong_newbie=冒尖小春笋, emotion=快乐之源, all=全部。默认 all。',
|
|
1166
|
+
},
|
|
1167
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
1168
|
+
},
|
|
1169
|
+
required: [],
|
|
1170
|
+
},
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
1173
|
+
handler: async (args, callCtx) => {
|
|
1174
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
1175
|
+
const honorType = typeof args.type === 'string' ? args.type : 'all';
|
|
1176
|
+
const data = await callAction(ctx, selfId, 'get_group_honor_info', {
|
|
1177
|
+
group_id: Number(groupId),
|
|
1178
|
+
type: honorType,
|
|
1179
|
+
});
|
|
1180
|
+
return JSON.stringify(data);
|
|
1181
|
+
},
|
|
1182
|
+
});
|
|
1183
|
+
ctx.logger.info('OneBot 群信息查询工具已注册');
|
|
1184
|
+
}
|
|
1185
|
+
// ===== 账号 / 好友 / 群列表查询工具 =====
|
|
1186
|
+
function registerAccountTools(ctx, bundle) {
|
|
1187
|
+
const { daily, personal } = bundle;
|
|
1188
|
+
// ---- 群列表 ----
|
|
1189
|
+
daily.register({
|
|
1190
|
+
definition: {
|
|
1191
|
+
type: 'function',
|
|
1192
|
+
function: {
|
|
1193
|
+
name: 'onebot_get_group_list',
|
|
1194
|
+
description: '获取机器人加入的所有群列表(OneBot v11: get_group_list)。' +
|
|
1195
|
+
'支持按群名/群号关键词搜索、分页返回。' +
|
|
1196
|
+
'可在任何会话调用(包括私聊),用于查询「我在哪些群」。' +
|
|
1197
|
+
'翻页:下次调用传 offset = 上次 offset + limit,直到 has_more=false。',
|
|
1198
|
+
parameters: {
|
|
1199
|
+
type: 'object',
|
|
1200
|
+
properties: {
|
|
1201
|
+
keyword: { type: 'string', description: '可选:按群名或群号子串模糊匹配(不区分大小写)' },
|
|
1202
|
+
limit: { type: 'number', description: '本页最多返回条数,默认 30' },
|
|
1203
|
+
offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
|
|
1204
|
+
},
|
|
1205
|
+
required: [],
|
|
1206
|
+
},
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1209
|
+
handler: async (args, callCtx) => {
|
|
1210
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
1211
|
+
const data = await callAction(ctx, selfId, 'get_group_list', {});
|
|
1212
|
+
const list = Array.isArray(data) ? data : [];
|
|
1213
|
+
const keyword = typeof args.keyword === 'string' ? args.keyword.trim().toLowerCase() : '';
|
|
1214
|
+
const limit = Math.max(1, Math.floor(Number(args.limit) || 30));
|
|
1215
|
+
const offset = Math.max(0, Math.floor(Number(args.offset) || 0));
|
|
1216
|
+
const all = list.map(g => ({
|
|
1217
|
+
group_id: g.group_id,
|
|
1218
|
+
group_name: String(g.group_name ?? ''),
|
|
1219
|
+
member_count: g.member_count,
|
|
1220
|
+
max_member_count: g.max_member_count,
|
|
1221
|
+
}));
|
|
1222
|
+
const filtered = keyword ? all.filter(g => `${g.group_id} ${g.group_name}`.toLowerCase().includes(keyword)) : all;
|
|
1223
|
+
const total = filtered.length;
|
|
1224
|
+
const items = filtered.slice(offset, offset + limit);
|
|
1225
|
+
return JSON.stringify({
|
|
1226
|
+
accountTotal: all.length,
|
|
1227
|
+
matched: total,
|
|
1228
|
+
limit,
|
|
1229
|
+
offset,
|
|
1230
|
+
returned: items.length,
|
|
1231
|
+
has_more: offset + items.length < total,
|
|
1232
|
+
...(keyword ? { keyword } : {}),
|
|
1233
|
+
groups: items,
|
|
1234
|
+
});
|
|
1235
|
+
},
|
|
1236
|
+
});
|
|
1237
|
+
// ---- 好友列表 ----
|
|
1238
|
+
daily.register({
|
|
1239
|
+
definition: {
|
|
1240
|
+
type: 'function',
|
|
1241
|
+
function: {
|
|
1242
|
+
name: 'onebot_get_friend_list',
|
|
1243
|
+
description: '获取机器人的好友列表(OneBot v11: get_friend_list)。' +
|
|
1244
|
+
'支持按昵称/备注/QQ号关键词搜索、分页返回。' +
|
|
1245
|
+
'翻页:下次调用传 offset = 上次 offset + limit,直到 has_more=false。',
|
|
1246
|
+
parameters: {
|
|
1247
|
+
type: 'object',
|
|
1248
|
+
properties: {
|
|
1249
|
+
keyword: { type: 'string', description: '可选:按昵称、备注或 QQ 号子串模糊匹配(不区分大小写)' },
|
|
1250
|
+
limit: { type: 'number', description: '本页最多返回条数,默认 30' },
|
|
1251
|
+
offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
|
|
1252
|
+
},
|
|
1253
|
+
required: [],
|
|
1254
|
+
},
|
|
1255
|
+
},
|
|
1256
|
+
},
|
|
1257
|
+
handler: async (args, callCtx) => {
|
|
1258
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
1259
|
+
const data = await callAction(ctx, selfId, 'get_friend_list', {});
|
|
1260
|
+
const list = Array.isArray(data) ? data : [];
|
|
1261
|
+
const keyword = typeof args.keyword === 'string' ? args.keyword.trim().toLowerCase() : '';
|
|
1262
|
+
const limit = Math.max(1, Math.floor(Number(args.limit) || 30));
|
|
1263
|
+
const offset = Math.max(0, Math.floor(Number(args.offset) || 0));
|
|
1264
|
+
const all = list.map(f => ({
|
|
1265
|
+
user_id: f.user_id,
|
|
1266
|
+
nickname: String(f.nickname ?? ''),
|
|
1267
|
+
remark: String(f.remark ?? ''),
|
|
1268
|
+
}));
|
|
1269
|
+
const filtered = keyword
|
|
1270
|
+
? all.filter(f => `${f.user_id} ${f.nickname} ${f.remark}`.toLowerCase().includes(keyword))
|
|
1271
|
+
: all;
|
|
1272
|
+
const total = filtered.length;
|
|
1273
|
+
const items = filtered.slice(offset, offset + limit);
|
|
1274
|
+
return JSON.stringify({
|
|
1275
|
+
accountTotal: all.length,
|
|
1276
|
+
matched: total,
|
|
1277
|
+
limit,
|
|
1278
|
+
offset,
|
|
1279
|
+
returned: items.length,
|
|
1280
|
+
has_more: offset + items.length < total,
|
|
1281
|
+
...(keyword ? { keyword } : {}),
|
|
1282
|
+
friends: items,
|
|
1283
|
+
});
|
|
1284
|
+
},
|
|
1285
|
+
});
|
|
1286
|
+
// ---- 陌生人 / 任意 QQ 号信息 ----
|
|
1287
|
+
daily.register({
|
|
1288
|
+
definition: {
|
|
1289
|
+
type: 'function',
|
|
1290
|
+
function: {
|
|
1291
|
+
name: 'onebot_get_stranger_info',
|
|
1292
|
+
description: '查询任意 QQ 号的公开资料(OneBot v11: get_stranger_info)。' +
|
|
1293
|
+
'不需要对方是好友或同群。常用于私聊会话或跨群查人。',
|
|
1294
|
+
parameters: {
|
|
1295
|
+
type: 'object',
|
|
1296
|
+
properties: {
|
|
1297
|
+
user_id: { type: 'string', description: '要查询的 QQ 号' },
|
|
1298
|
+
no_cache: { type: 'boolean', description: '是否跳过缓存强制刷新,默认 false' },
|
|
1299
|
+
},
|
|
1300
|
+
required: ['user_id'],
|
|
1301
|
+
},
|
|
1302
|
+
},
|
|
1303
|
+
},
|
|
1304
|
+
handler: async (args, callCtx) => {
|
|
1305
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
1306
|
+
const data = await callAction(ctx, selfId, 'get_stranger_info', {
|
|
1307
|
+
user_id: Number(args.user_id),
|
|
1308
|
+
no_cache: !!args.no_cache,
|
|
1309
|
+
});
|
|
1310
|
+
return JSON.stringify(data);
|
|
1311
|
+
},
|
|
1312
|
+
});
|
|
1313
|
+
// ---- 机器人自身账号信息 ----
|
|
1314
|
+
daily.register({
|
|
1315
|
+
definition: {
|
|
1316
|
+
type: 'function',
|
|
1317
|
+
function: {
|
|
1318
|
+
name: 'onebot_get_login_info',
|
|
1319
|
+
description: '显式查询当前 OneBot 连接的登录账号信息(QQ 号、昵称)。仅在用户要求核实账号、诊断连接或需要最新平台返回值时使用。',
|
|
1320
|
+
parameters: { type: 'object', properties: {}, required: [] },
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1323
|
+
handler: async (_args, callCtx) => {
|
|
1324
|
+
const selfId = resolveSelfId(ctx, callCtx, _args);
|
|
1325
|
+
const data = await callAction(ctx, selfId, 'get_login_info', {});
|
|
1326
|
+
return JSON.stringify(data);
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
// ---- 删除好友 ----
|
|
1330
|
+
personal.register({
|
|
1331
|
+
definition: {
|
|
1332
|
+
type: 'function',
|
|
1333
|
+
function: {
|
|
1334
|
+
name: 'onebot_delete_friend',
|
|
1335
|
+
description: '删除指定 QQ 好友。适用于私聊持续骚扰、辱骂、垃圾消息等不希望继续保持好友关系的场景。该接口是 go-cqhttp/NapCat 等 OneBot v11 实现的常见扩展。',
|
|
1336
|
+
parameters: {
|
|
1337
|
+
type: 'object',
|
|
1338
|
+
properties: {
|
|
1339
|
+
user_id: { type: 'string', description: '要删除的好友 QQ 号;在私聊中可省略,默认当前对话用户' },
|
|
1340
|
+
},
|
|
1341
|
+
required: [],
|
|
1342
|
+
},
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
handler: async (args, callCtx) => {
|
|
1346
|
+
const argUserId = args.user_id != null && String(args.user_id).trim() ? String(args.user_id).trim() : '';
|
|
1347
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
1348
|
+
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
1349
|
+
const userId = argUserId || (parsed?.detailType === 'private' ? parsed.targetId : '');
|
|
1350
|
+
if (!userId)
|
|
1351
|
+
return '请提供 user_id,或在要删除的好友私聊中调用此工具';
|
|
1352
|
+
await callAction(ctx, selfId, 'delete_friend', {
|
|
1353
|
+
user_id: Number(userId),
|
|
1354
|
+
});
|
|
1355
|
+
return `已删除好友 ${userId}`;
|
|
1356
|
+
},
|
|
1357
|
+
});
|
|
1358
|
+
ctx.logger.info('OneBot 账号 / 好友 / 群列表查询工具已注册');
|
|
1359
|
+
}
|
|
1360
|
+
// ===== 特殊交互工具 =====
|
|
1361
|
+
function registerInteractionTools(ctx, bundle) {
|
|
1362
|
+
const { daily, group } = bundle;
|
|
1363
|
+
// ---- 戳一戳 ----
|
|
1364
|
+
daily.register({
|
|
1365
|
+
definition: {
|
|
1366
|
+
type: 'function',
|
|
1367
|
+
function: {
|
|
1368
|
+
name: 'onebot_poke',
|
|
1369
|
+
description: '戳一戳(双击头像效果)。群聊默认戳当前群里指定用户;私聊默认戳对方(user_id 可省)。',
|
|
1370
|
+
parameters: {
|
|
1371
|
+
type: 'object',
|
|
1372
|
+
properties: {
|
|
1373
|
+
user_id: {
|
|
1374
|
+
type: 'string',
|
|
1375
|
+
description: '要戳的用户QQ号。群聊必填;私聊省略则戳当前对话对方。',
|
|
1376
|
+
},
|
|
1377
|
+
group_id: {
|
|
1378
|
+
type: 'string',
|
|
1379
|
+
description: '可选:目标群号。群聊省略则使用当前群;如显式传值则跨群操作。',
|
|
1380
|
+
},
|
|
1381
|
+
},
|
|
1382
|
+
},
|
|
1383
|
+
},
|
|
1384
|
+
},
|
|
1385
|
+
handler: async (args, callCtx) => {
|
|
1386
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
1387
|
+
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
1388
|
+
const argGroupId = args.group_id != null && String(args.group_id).trim() ? String(args.group_id).trim() : '';
|
|
1389
|
+
const argUserId = args.user_id != null && String(args.user_id).trim() ? String(args.user_id).trim() : '';
|
|
1390
|
+
// 1) 群聊场景:显式 group_id 或当前是群会话
|
|
1391
|
+
const groupId = argGroupId || (parsed?.detailType === 'group' ? parsed.targetId : '');
|
|
1392
|
+
if (groupId) {
|
|
1393
|
+
if (!argUserId) {
|
|
1394
|
+
throw new Error('群聊戳一戳必须显式提供 user_id(要戳的对象)。');
|
|
1395
|
+
}
|
|
1396
|
+
try {
|
|
1397
|
+
await callAction(ctx, selfId, 'group_poke', {
|
|
1398
|
+
group_id: Number(groupId),
|
|
1399
|
+
user_id: Number(argUserId),
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
catch {
|
|
1403
|
+
await callAction(ctx, selfId, 'send_group_poke', {
|
|
1404
|
+
group_id: Number(groupId),
|
|
1405
|
+
user_id: Number(argUserId),
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
return `已在群 ${groupId} 戳了 ${argUserId} 一下`;
|
|
1409
|
+
}
|
|
1410
|
+
// 2) 私聊场景:从 user_id 或当前私聊会话对方取目标
|
|
1411
|
+
const userId = argUserId || (parsed?.detailType === 'private' ? parsed.targetId : '');
|
|
1412
|
+
if (!userId) {
|
|
1413
|
+
throw new Error('无法判断戳一戳目标:当前不在 OneBot 会话内,请显式传入 group_id+user_id 或 user_id。');
|
|
1414
|
+
}
|
|
1415
|
+
// NapCat: friend_poke { user_id };其他实现可能用 send_poke
|
|
1416
|
+
try {
|
|
1417
|
+
await callAction(ctx, selfId, 'friend_poke', { user_id: Number(userId) });
|
|
1418
|
+
}
|
|
1419
|
+
catch {
|
|
1420
|
+
await callAction(ctx, selfId, 'send_poke', { user_id: Number(userId) });
|
|
1421
|
+
}
|
|
1422
|
+
return `已戳了 ${userId} 一下`;
|
|
1423
|
+
},
|
|
1424
|
+
});
|
|
1425
|
+
// ---- 发送好友赞 ----
|
|
1426
|
+
daily.register({
|
|
1427
|
+
definition: {
|
|
1428
|
+
type: 'function',
|
|
1429
|
+
function: {
|
|
1430
|
+
name: 'onebot_send_like',
|
|
1431
|
+
description: '给指定 QQ 用户发送好友赞(每个好友每天最多 10 次)。低成本的社交互动,适用于表达友好、打招呼等场景。',
|
|
1432
|
+
parameters: {
|
|
1433
|
+
type: 'object',
|
|
1434
|
+
properties: {
|
|
1435
|
+
user_id: { type: 'string', description: '要赞的用户 QQ 号' },
|
|
1436
|
+
times: { type: 'number', description: '赞的次数,默认 1,每天每个好友最多 10 次' },
|
|
1437
|
+
},
|
|
1438
|
+
required: ['user_id'],
|
|
1439
|
+
},
|
|
1440
|
+
},
|
|
1441
|
+
},
|
|
1442
|
+
handler: async (args, callCtx) => {
|
|
1443
|
+
const selfId = resolveSelfId(ctx, callCtx, args);
|
|
1444
|
+
const times = Math.min(10, Math.max(1, typeof args.times === 'number' ? Math.floor(args.times) : 1));
|
|
1445
|
+
await callAction(ctx, selfId, 'send_like', {
|
|
1446
|
+
user_id: Number(args.user_id),
|
|
1447
|
+
times,
|
|
1448
|
+
});
|
|
1449
|
+
return `已给 ${args.user_id} 发送了 ${times} 次赞`;
|
|
1450
|
+
},
|
|
1451
|
+
});
|
|
1452
|
+
// ---- 群打卡 ----
|
|
1453
|
+
group.register({
|
|
1454
|
+
definition: {
|
|
1455
|
+
type: 'function',
|
|
1456
|
+
function: {
|
|
1457
|
+
name: 'onebot_send_group_sign',
|
|
1458
|
+
description: '在群聊中打卡签到。可传 group_id 跨群操作。',
|
|
1459
|
+
parameters: {
|
|
1460
|
+
type: 'object',
|
|
1461
|
+
properties: {
|
|
1462
|
+
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
1463
|
+
},
|
|
1464
|
+
required: [],
|
|
1465
|
+
},
|
|
1466
|
+
},
|
|
1467
|
+
},
|
|
1468
|
+
handler: async (args, callCtx) => {
|
|
1469
|
+
const { selfId, groupId } = resolveGroupTarget(ctx, callCtx, args);
|
|
1470
|
+
await callAction(ctx, selfId, 'send_group_sign', {
|
|
1471
|
+
group_id: Number(groupId),
|
|
1472
|
+
});
|
|
1473
|
+
return '打卡成功';
|
|
1474
|
+
},
|
|
1475
|
+
});
|
|
1476
|
+
ctx.logger.info('OneBot 特殊交互工具已注册');
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* 把 OneBot 的细粒度访问规则注册到 session-history 服务。
|
|
1480
|
+
* 通用工具 session_get_history 和平台专属工具 onebot_get_session_history
|
|
1481
|
+
* 调用 service 时都会走这条规则链 —— 不存在绕过路径。
|
|
1482
|
+
*/
|
|
1483
|
+
function registerOneBotHistoryAccessChecker(ctx, cfg) {
|
|
1484
|
+
ctx.on('ready', () => {
|
|
1485
|
+
const historyService = ctx.getService('session-history');
|
|
1486
|
+
if (!historyService?.registerAccessChecker) {
|
|
1487
|
+
ctx.logger.debug('session-history 服务未提供 registerAccessChecker, 跳过 OneBot 访问规则注册');
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
const checker = {
|
|
1491
|
+
platform: 'onebot',
|
|
1492
|
+
check({ currentSessionId, targetSessionId }) {
|
|
1493
|
+
const target = parseOneBotSession(targetSessionId);
|
|
1494
|
+
if (!target)
|
|
1495
|
+
return undefined; // 不是合法 onebot 目标, 不表态
|
|
1496
|
+
const current = parseOneBotSession(currentSessionId);
|
|
1497
|
+
// 跨平台调用(current 不是 onebot)走默认放行: session-history scope 已粗筛
|
|
1498
|
+
if (!current)
|
|
1499
|
+
return undefined;
|
|
1500
|
+
if (current.selfId !== target.selfId && !cfg.allowCrossSelf) {
|
|
1501
|
+
return { decision: 'deny', reason: '当前 OneBot 配置不允许跨机器人账号读取会话历史' };
|
|
1502
|
+
}
|
|
1503
|
+
if (current.detailType === 'group' && target.detailType === 'private' && !cfg.allowGroupReadPrivate) {
|
|
1504
|
+
return { decision: 'deny', reason: '当前 OneBot 配置不允许从群聊读取私聊历史' };
|
|
1505
|
+
}
|
|
1506
|
+
if (current.detailType === 'group' &&
|
|
1507
|
+
target.detailType === 'group' &&
|
|
1508
|
+
current.targetId !== target.targetId &&
|
|
1509
|
+
!cfg.allowCrossGroup) {
|
|
1510
|
+
return { decision: 'deny', reason: '当前 OneBot 配置不允许跨群读取其他群历史' };
|
|
1511
|
+
}
|
|
1512
|
+
if (current.detailType === 'private' &&
|
|
1513
|
+
target.detailType === 'private' &&
|
|
1514
|
+
current.targetId !== target.targetId &&
|
|
1515
|
+
!cfg.allowCrossPrivate) {
|
|
1516
|
+
return { decision: 'deny', reason: '当前 OneBot 配置不允许跨私聊读取其他私聊历史' };
|
|
1517
|
+
}
|
|
1518
|
+
return undefined;
|
|
1519
|
+
},
|
|
1520
|
+
};
|
|
1521
|
+
const dispose = historyService.registerAccessChecker(checker);
|
|
1522
|
+
// 注意:总线上并不存在 'dispose' 事件——此前 ctx.on('dispose', ...) 永远不会
|
|
1523
|
+
// 触发,access checker 在插件 bounce 后泄漏。正确挂法是 onDispose 清理链。
|
|
1524
|
+
ctx.onDispose(dispose);
|
|
1525
|
+
ctx.logger.info('OneBot 会话历史访问规则已注册到 session-history');
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
function registerSessionHistoryTools(ctx, bundle, cfg) {
|
|
1529
|
+
const { daily } = bundle;
|
|
1530
|
+
daily.register({
|
|
1531
|
+
definition: {
|
|
1532
|
+
type: 'function',
|
|
1533
|
+
function: {
|
|
1534
|
+
name: 'onebot_resolve_session_id',
|
|
1535
|
+
description: '把 OneBot 群号或 QQ 号解析成 Aalis 内部 sessionId。用于确认目标会话 ID,不读取历史。',
|
|
1536
|
+
parameters: {
|
|
1537
|
+
type: 'object',
|
|
1538
|
+
properties: {
|
|
1539
|
+
target_type: {
|
|
1540
|
+
type: 'string',
|
|
1541
|
+
enum: ['group', 'private'],
|
|
1542
|
+
description: 'group = 群聊,private = 私聊',
|
|
1543
|
+
},
|
|
1544
|
+
target_id: { type: 'string', description: '群号或 QQ 号' },
|
|
1545
|
+
self_id: { type: 'string', description: '机器人账号。可选,默认使用当前 OneBot 会话的 selfId' },
|
|
1546
|
+
},
|
|
1547
|
+
required: ['target_type', 'target_id'],
|
|
1548
|
+
additionalProperties: false,
|
|
1549
|
+
},
|
|
1550
|
+
},
|
|
1551
|
+
},
|
|
1552
|
+
handler: async (args, callCtx) => {
|
|
1553
|
+
const current = parseOneBotSession(callCtx.sessionId);
|
|
1554
|
+
const targetType = String(args.target_type ?? '').toLowerCase();
|
|
1555
|
+
const targetId = String(args.target_id ?? '').trim();
|
|
1556
|
+
if (targetType !== 'group' && targetType !== 'private') {
|
|
1557
|
+
return JSON.stringify({ error: `target_type 必须为 group 或 private,收到 ${args.target_type}` });
|
|
1558
|
+
}
|
|
1559
|
+
if (!targetId)
|
|
1560
|
+
return JSON.stringify({ error: 'target_id 不能为空' });
|
|
1561
|
+
const argSelfId = args.self_id ? String(args.self_id).trim() : '';
|
|
1562
|
+
const selfId = argSelfId || current?.selfId || getAnyOneBotSelfId(ctx) || '';
|
|
1563
|
+
if (!selfId)
|
|
1564
|
+
return JSON.stringify({ error: '无法确定 self_id,且未找到可用的 OneBot 连接' });
|
|
1565
|
+
// 跨账号限制:仅在当前也是 OneBot 会话时才作为参考点检查
|
|
1566
|
+
if (current && !cfg.allowCrossSelf && selfId !== current.selfId) {
|
|
1567
|
+
return JSON.stringify({
|
|
1568
|
+
error: '当前配置不允许跨机器人账号解析会话',
|
|
1569
|
+
currentSelfId: current.selfId,
|
|
1570
|
+
requestedSelfId: selfId,
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
return JSON.stringify({
|
|
1574
|
+
ok: true,
|
|
1575
|
+
sessionId: buildOneBotSessionId(selfId, targetType, targetId),
|
|
1576
|
+
selfId,
|
|
1577
|
+
targetType,
|
|
1578
|
+
targetId,
|
|
1579
|
+
});
|
|
1580
|
+
},
|
|
1581
|
+
});
|
|
1582
|
+
daily.register({
|
|
1583
|
+
definition: {
|
|
1584
|
+
type: 'function',
|
|
1585
|
+
function: {
|
|
1586
|
+
name: 'onebot_get_session_history',
|
|
1587
|
+
description: [
|
|
1588
|
+
'按 QQ 群号或 QQ 号读取对应 OneBot 会话最近若干条历史消息。',
|
|
1589
|
+
'适用场景:用户明确要求你回忆某个群/私聊最近说过什么,或需要核实另一个 OneBot 会话的原文上下文。',
|
|
1590
|
+
'默认禁止从群聊读取私聊历史;如需语义搜索历史,请优先使用 memory_recall。',
|
|
1591
|
+
].join('\n'),
|
|
1592
|
+
parameters: {
|
|
1593
|
+
type: 'object',
|
|
1594
|
+
properties: {
|
|
1595
|
+
target_type: {
|
|
1596
|
+
type: 'string',
|
|
1597
|
+
enum: ['group', 'private'],
|
|
1598
|
+
description: 'group = 群聊(target_id 为群号),private = 私聊(target_id 为 QQ 号)',
|
|
1599
|
+
},
|
|
1600
|
+
target_id: { type: 'string', description: '群号或 QQ 号' },
|
|
1601
|
+
limit: {
|
|
1602
|
+
type: 'number',
|
|
1603
|
+
description: `读取最近多少条,默认 ${cfg.defaultLimit},最多 ${cfg.maxLimit}`,
|
|
1604
|
+
},
|
|
1605
|
+
self_id: { type: 'string', description: '机器人账号。可选,默认使用当前 OneBot 会话的 selfId' },
|
|
1606
|
+
},
|
|
1607
|
+
required: ['target_type', 'target_id'],
|
|
1608
|
+
additionalProperties: false,
|
|
1609
|
+
},
|
|
1610
|
+
},
|
|
1611
|
+
},
|
|
1612
|
+
handler: async (args, callCtx) => {
|
|
1613
|
+
const current = parseOneBotSession(callCtx.sessionId);
|
|
1614
|
+
const targetType = String(args.target_type ?? '').toLowerCase();
|
|
1615
|
+
const targetId = String(args.target_id ?? '').trim();
|
|
1616
|
+
if (targetType !== 'group' && targetType !== 'private') {
|
|
1617
|
+
return JSON.stringify({ error: `target_type 必须为 group 或 private,收到 ${args.target_type}` });
|
|
1618
|
+
}
|
|
1619
|
+
if (!targetId)
|
|
1620
|
+
return JSON.stringify({ error: 'target_id 不能为空' });
|
|
1621
|
+
const argSelfId = args.self_id ? String(args.self_id).trim() : '';
|
|
1622
|
+
const selfId = argSelfId || current?.selfId || getAnyOneBotSelfId(ctx) || '';
|
|
1623
|
+
if (!selfId)
|
|
1624
|
+
return JSON.stringify({ error: '无法确定 self_id,且未找到可用的 OneBot 连接' });
|
|
1625
|
+
const history = ctx.getService('session-history');
|
|
1626
|
+
if (!history)
|
|
1627
|
+
return JSON.stringify({
|
|
1628
|
+
error: 'session-history 服务不可用,请启用 @aalis/plugin-tool-session',
|
|
1629
|
+
});
|
|
1630
|
+
const limit = Math.max(1, Math.min(cfg.maxLimit, Math.floor(Number(args.limit) || cfg.defaultLimit)));
|
|
1631
|
+
const sessionId = buildOneBotSessionId(selfId, targetType, targetId);
|
|
1632
|
+
try {
|
|
1633
|
+
// 访问控制(跨账号/群读私/跨群/跨私)由 session-history 服务统一执行,
|
|
1634
|
+
// OneBot 的规则已通过 registerOneBotHistoryAccessChecker 注册。
|
|
1635
|
+
// includeArchived 故意不暴露:archived 是 memory 内部细节,被 summary 替代的老消息
|
|
1636
|
+
// 默认就不该再翻原文(会与 summary 内容双重计费)。如需考古应另起独立工具。
|
|
1637
|
+
const result = await history.getHistory({ sessionId, limit }, callCtx);
|
|
1638
|
+
return JSON.stringify({
|
|
1639
|
+
...result,
|
|
1640
|
+
sessionId,
|
|
1641
|
+
selfId,
|
|
1642
|
+
targetType,
|
|
1643
|
+
targetId,
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
catch (err) {
|
|
1647
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1648
|
+
ctx.logger.warn(`onebot_get_session_history 失败 (${sessionId}): ${message}`);
|
|
1649
|
+
return JSON.stringify({ error: `读取 OneBot 会话历史失败: ${message}` });
|
|
1650
|
+
}
|
|
1651
|
+
},
|
|
1652
|
+
});
|
|
1653
|
+
ctx.logger.info('OneBot 会话历史工具已注册');
|
|
1654
|
+
}
|
|
1655
|
+
// ===== 请求处理工具(好友申请 / 群请求)=====
|
|
1656
|
+
function registerRequestTools(ctx, bundle) {
|
|
1657
|
+
const { group, personal } = bundle;
|
|
1658
|
+
/** 找到支持 handleFriendRequest 的 OneBot 适配器 */
|
|
1659
|
+
function findRequestAdapter(ctx) {
|
|
1660
|
+
const adapter = getPlatformAdapters(ctx).find(a => a.platform === 'onebot' && typeof a.handleFriendRequest === 'function');
|
|
1661
|
+
return adapter;
|
|
1662
|
+
}
|
|
1663
|
+
personal.register({
|
|
1664
|
+
definition: {
|
|
1665
|
+
type: 'function',
|
|
1666
|
+
function: {
|
|
1667
|
+
name: 'onebot_handle_friend_request',
|
|
1668
|
+
description: '处理 QQ 好友申请(别人加 bot 为好友),同意或拒绝。仅在收到好友申请通知后才有效。',
|
|
1669
|
+
parameters: {
|
|
1670
|
+
type: 'object',
|
|
1671
|
+
properties: {
|
|
1672
|
+
user_id: { type: 'string', description: '发起好友申请的用户 QQ 号' },
|
|
1673
|
+
approve: { type: 'boolean', description: 'true = 同意,false = 拒绝' },
|
|
1674
|
+
remark: { type: 'string', description: '同意后添加的备注(可选)' },
|
|
1675
|
+
},
|
|
1676
|
+
required: ['user_id', 'approve'],
|
|
1677
|
+
},
|
|
1678
|
+
},
|
|
1679
|
+
},
|
|
1680
|
+
handler: async (args) => {
|
|
1681
|
+
const adapter = findRequestAdapter(ctx);
|
|
1682
|
+
if (!adapter)
|
|
1683
|
+
return '未找到支持请求处理的 OneBot 适配器';
|
|
1684
|
+
return adapter.handleFriendRequest(String(args.user_id), !!args.approve, args.remark ? String(args.remark) : undefined);
|
|
1685
|
+
},
|
|
1686
|
+
});
|
|
1687
|
+
// 群相关请求拆成两个工具,对应 OneBot v11 set_group_add_request 的两种 sub_type:
|
|
1688
|
+
// - approve_join_request: sub_type=add 「别人申请加入 bot 管理的群」 → 群务
|
|
1689
|
+
// - handle_group_invite: sub_type=invite 「别人邀请 bot 加入群」 → 个人/私聊
|
|
1690
|
+
// 底层共用同一个适配器方法(pendingGroupRequests 内部已记录 subType),区别在于语义与权限归属。
|
|
1691
|
+
group.register({
|
|
1692
|
+
definition: {
|
|
1693
|
+
type: 'function',
|
|
1694
|
+
function: {
|
|
1695
|
+
name: 'onebot_approve_join_request',
|
|
1696
|
+
description: '审批「有人申请加入 bot 管理的群」的入群请求(OneBot v11 set_group_add_request, sub_type=add)。同意或拒绝。仅在收到加群申请通知后才有效。',
|
|
1697
|
+
parameters: {
|
|
1698
|
+
type: 'object',
|
|
1699
|
+
properties: {
|
|
1700
|
+
user_id: { type: 'string', description: '申请加群的用户 QQ 号' },
|
|
1701
|
+
group_id: { type: 'string', description: '相关群号' },
|
|
1702
|
+
approve: { type: 'boolean', description: 'true = 同意加群,false = 拒绝' },
|
|
1703
|
+
reason: { type: 'string', description: '拒绝理由(拒绝时有效,可选)' },
|
|
1704
|
+
},
|
|
1705
|
+
required: ['user_id', 'group_id', 'approve'],
|
|
1706
|
+
},
|
|
1707
|
+
},
|
|
1708
|
+
},
|
|
1709
|
+
handler: async (args) => {
|
|
1710
|
+
const adapter = findRequestAdapter(ctx);
|
|
1711
|
+
if (!adapter)
|
|
1712
|
+
return '未找到支持请求处理的 OneBot 适配器';
|
|
1713
|
+
return adapter.handleGroupRequest(String(args.user_id), String(args.group_id), !!args.approve, args.reason ? String(args.reason) : undefined);
|
|
1714
|
+
},
|
|
1715
|
+
});
|
|
1716
|
+
personal.register({
|
|
1717
|
+
definition: {
|
|
1718
|
+
type: 'function',
|
|
1719
|
+
function: {
|
|
1720
|
+
name: 'onebot_handle_group_invite',
|
|
1721
|
+
description: '处理「别人邀请 bot 加入某个群」的邀请(OneBot v11 set_group_add_request, sub_type=invite)。同意 = 接受加群;拒绝 = 不入群。仅在收到入群邀请通知后才有效。',
|
|
1722
|
+
parameters: {
|
|
1723
|
+
type: 'object',
|
|
1724
|
+
properties: {
|
|
1725
|
+
user_id: { type: 'string', description: '邀请发起人的 QQ 号' },
|
|
1726
|
+
group_id: { type: 'string', description: '被邀请加入的群号' },
|
|
1727
|
+
approve: { type: 'boolean', description: 'true = 接受邀请并加入群,false = 拒绝' },
|
|
1728
|
+
reason: { type: 'string', description: '拒绝理由(拒绝时有效,可选)' },
|
|
1729
|
+
},
|
|
1730
|
+
required: ['user_id', 'group_id', 'approve'],
|
|
1731
|
+
},
|
|
1732
|
+
},
|
|
1733
|
+
},
|
|
1734
|
+
handler: async (args) => {
|
|
1735
|
+
const adapter = findRequestAdapter(ctx);
|
|
1736
|
+
if (!adapter)
|
|
1737
|
+
return '未找到支持请求处理的 OneBot 适配器';
|
|
1738
|
+
return adapter.handleGroupRequest(String(args.user_id), String(args.group_id), !!args.approve, args.reason ? String(args.reason) : undefined);
|
|
1739
|
+
},
|
|
1740
|
+
});
|
|
1741
|
+
ctx.logger.info('OneBot 请求处理工具已注册');
|
|
1742
|
+
}
|
|
1743
|
+
//# sourceMappingURL=index.js.map
|