@aalis/plugin-tool-onebot 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +328 -402
- 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
|
-
import { getPlatformAdapters, getPlatformNames, platform as platformService, } from '@aalis/api-platform';
|
|
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,29 +104,14 @@ function parseOneBotSession(sessionId) {
|
|
|
124
104
|
targetId: parts.slice(3).join(':'),
|
|
125
105
|
};
|
|
126
106
|
}
|
|
127
|
-
/**
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
function getAnyOneBotSelfId(platform) {
|
|
133
|
-
for (const a of getPlatformAdapters(platform)) {
|
|
134
|
-
if (a.platform !== 'onebot')
|
|
135
|
-
continue;
|
|
136
|
-
const conns = a.getConnections();
|
|
137
|
-
const online = conns.find(c => c.status === 'online' && c.selfId);
|
|
138
|
-
if (online?.selfId)
|
|
139
|
-
return online.selfId;
|
|
140
|
-
const any = conns.find(c => c.selfId);
|
|
141
|
-
if (any?.selfId)
|
|
142
|
-
return any.selfId;
|
|
143
|
-
}
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
107
|
+
/** 每个用到机器人账号的工具都带的 self_id 参数 */
|
|
108
|
+
const SELF_ID_PARAM = {
|
|
109
|
+
type: 'string',
|
|
110
|
+
description: '机器人账号(QQ 号)。在 OneBot 会话里可以省,用这个会话的账号;不在 OneBot 会话里且有多个账号时必填',
|
|
111
|
+
};
|
|
146
112
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* 找不到则抛错(无可用 OneBot 连接)。
|
|
113
|
+
* 本次调用用哪个机器人账号:写明的 self_id;否则当前 OneBot 会话的账号;都没有时汇总各 OneBot 实例的已知账号(不论在不在线),
|
|
114
|
+
* 恰好一个才用它,多个时报错列出账号、要求写明。子任务会话不算 OneBot 会话,账号不从根会话推
|
|
150
115
|
*/
|
|
151
116
|
function resolveSelfId(platform, callCtx, args) {
|
|
152
117
|
const fromArgs = args.self_id != null && String(args.self_id).trim() ? String(args.self_id).trim() : '';
|
|
@@ -155,10 +120,16 @@ function resolveSelfId(platform, callCtx, args) {
|
|
|
155
120
|
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
156
121
|
if (parsed?.selfId)
|
|
157
122
|
return parsed.selfId;
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 写明用哪个`);
|
|
162
133
|
}
|
|
163
134
|
/**
|
|
164
135
|
* 解析"目标群":args.group_id 优先;否则当前会话必须在 OneBot 群里。
|
|
@@ -171,7 +142,7 @@ function resolveGroupTarget(platform, callCtx, args) {
|
|
|
171
142
|
return { selfId, groupId: argGroupId };
|
|
172
143
|
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
173
144
|
if (parsed?.detailType === 'group' && parsed.targetId) {
|
|
174
|
-
return { selfId
|
|
145
|
+
return { selfId, groupId: parsed.targetId };
|
|
175
146
|
}
|
|
176
147
|
throw new Error('未提供 group_id,且当前不在 OneBot 群会话中。请在参数中显式传入 group_id。');
|
|
177
148
|
}
|
|
@@ -179,45 +150,44 @@ function buildOneBotSessionId(selfId, detailType, targetId) {
|
|
|
179
150
|
return `onebot:${selfId}:${detailType}:${targetId}`;
|
|
180
151
|
}
|
|
181
152
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* 因此这里用合成 sessionId("onebot:<selfId>:internal:0")即可,与目标会话类型无关。
|
|
153
|
+
* get_msg 取到的消息所在的会话:群按群号,私聊按对端账号(发送者不是机器人自己时就是发送者)。她自己发的私聊消息与没给这些
|
|
154
|
+
* 字段的实现端拼不出,为 undefined
|
|
185
155
|
*/
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
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
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 机器人账号的合成会话 id。路由与适配器都只看 id 里的 selfId(路由找到账号所属的实例,适配器定位它的连接),
|
|
168
|
+
* 与目标会话类型无关。
|
|
169
|
+
*/
|
|
170
|
+
function selfSessionId(selfId) {
|
|
171
|
+
return buildOneBotSessionId(selfId, 'internal', '0');
|
|
191
172
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const ext = dot >= 0 ? cleaned.slice(dot).toLowerCase() : '';
|
|
196
|
-
if (ext === '.jpg' || ext === '.jpeg')
|
|
197
|
-
return 'image/jpeg';
|
|
198
|
-
if (ext === '.gif')
|
|
199
|
-
return 'image/gif';
|
|
200
|
-
if (ext === '.webp')
|
|
201
|
-
return 'image/webp';
|
|
202
|
-
if (ext === '.bmp')
|
|
203
|
-
return 'image/bmp';
|
|
204
|
-
return 'image/png';
|
|
173
|
+
/** 调用 OneBot 协议 action:selfId 决定走哪个机器人账号,经平台路由发到账号所属实例的连接 */
|
|
174
|
+
async function callAction(platform, selfId, action, params) {
|
|
175
|
+
return callPlatformAction(platform, selfSessionId(selfId), action, params);
|
|
205
176
|
}
|
|
206
177
|
/**
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
178
|
+
* 按顺序调候选动作名,只有实现端回不支持该动作时才换下一个;其余错误(超时、没权限、对方不在群)原样抛出,不换个动作再做
|
|
179
|
+
* 一次。戳一戳的动作名各实现端不同:NapCat 是 group_poke、friend_poke 与 send_poke,没有 send_group_poke,群里在 NapCat 上
|
|
180
|
+
* 只有 group_poke 一个,后面的候选留给别的实现端
|
|
210
181
|
*/
|
|
211
|
-
async function
|
|
212
|
-
|
|
213
|
-
return null;
|
|
182
|
+
async function callFirstSupported(platform, selfId, actions, params) {
|
|
183
|
+
const [action, ...rest] = actions;
|
|
214
184
|
try {
|
|
215
|
-
|
|
216
|
-
const b64 = Buffer.from(buf).toString('base64');
|
|
217
|
-
return `data:${imageMimeFromPath(path)};base64,${b64}`;
|
|
185
|
+
return await callAction(platform, selfId, action, params);
|
|
218
186
|
}
|
|
219
|
-
catch {
|
|
220
|
-
|
|
187
|
+
catch (err) {
|
|
188
|
+
if (rest.length === 0 || err.unsupported !== true)
|
|
189
|
+
throw err;
|
|
190
|
+
return callFirstSupported(platform, selfId, rest, params);
|
|
221
191
|
}
|
|
222
192
|
}
|
|
223
193
|
function imageRefKey(ref) {
|
|
@@ -270,6 +240,7 @@ function extractImageRefsFromContent(content) {
|
|
|
270
240
|
}
|
|
271
241
|
return refs;
|
|
272
242
|
}
|
|
243
|
+
/** 合并转发的节点:data 本身、data.messages 或 data.message 是数组;都不是时返回格式认不出,为 undefined */
|
|
273
244
|
function getForwardMessages(data) {
|
|
274
245
|
const root = data;
|
|
275
246
|
if (Array.isArray(root))
|
|
@@ -280,7 +251,7 @@ function getForwardMessages(data) {
|
|
|
280
251
|
if (Array.isArray(root?.message)) {
|
|
281
252
|
return root.message;
|
|
282
253
|
}
|
|
283
|
-
return
|
|
254
|
+
return undefined;
|
|
284
255
|
}
|
|
285
256
|
function getForwardNodeData(item) {
|
|
286
257
|
const node = item && typeof item === 'object' ? item : {};
|
|
@@ -295,43 +266,42 @@ function getForwardNodeContent(item) {
|
|
|
295
266
|
return nodeData.content ?? node.content ?? nodeData.message ?? node.message;
|
|
296
267
|
}
|
|
297
268
|
function collectForwardImageRefs(data, limit) {
|
|
298
|
-
return getForwardMessages(data)
|
|
269
|
+
return (getForwardMessages(data) ?? [])
|
|
299
270
|
.slice(0, limit)
|
|
300
271
|
.flatMap(item => extractImageRefsFromContent(getForwardNodeContent(item)));
|
|
301
272
|
}
|
|
302
|
-
|
|
303
|
-
|
|
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))
|
|
304
282
|
return ref.source;
|
|
305
283
|
const { platform, logger } = caps;
|
|
306
284
|
try {
|
|
307
285
|
const imageData = (await callAction(platform, selfId, 'get_image', { file: ref.source }));
|
|
308
|
-
const resolvedSource = imageData.url ?? imageData.file
|
|
309
|
-
|
|
310
|
-
if (/^(https?:|data:)/i.test(resolvedSource))
|
|
311
|
-
return resolvedSource;
|
|
312
|
-
const dataUri = await localImageToDataUri(storage, resolvedSource);
|
|
313
|
-
if (dataUri)
|
|
314
|
-
return dataUri;
|
|
315
|
-
return resolvedSource;
|
|
316
|
-
}
|
|
286
|
+
const resolvedSource = imageData.url ?? imageData.file;
|
|
287
|
+
return typeof resolvedSource === 'string' ? resolvedSource : undefined;
|
|
317
288
|
}
|
|
318
289
|
catch (err) {
|
|
319
|
-
logger.debug(`get_image 解析转发图片失败 (${ref.source}): ${err}`);
|
|
290
|
+
logger.use.debug(`get_image 解析转发图片失败 (${ref.source}): ${err}`);
|
|
291
|
+
return undefined;
|
|
320
292
|
}
|
|
321
|
-
const dataUri = await localImageToDataUri(storage, ref.source);
|
|
322
|
-
return dataUri ?? ref.source;
|
|
323
293
|
}
|
|
324
|
-
async function recognizeForwardImages(caps,
|
|
294
|
+
async function recognizeForwardImages(caps, selfId, data, limit) {
|
|
325
295
|
const refs = collectForwardImageRefs(data, limit);
|
|
326
296
|
const imageDescriptions = new Map();
|
|
327
297
|
if (refs.length === 0)
|
|
328
298
|
return { imageDescriptions };
|
|
329
299
|
const { logger, media } = caps;
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
logger.debug(`合并转发包含 ${refs.length} 张图片,但 media 服务不可用`);
|
|
300
|
+
if (!media.present) {
|
|
301
|
+
logger.use.debug(`合并转发包含 ${refs.length} 张图片,但 media 服务不可用`);
|
|
333
302
|
return { imageDescriptions };
|
|
334
303
|
}
|
|
304
|
+
const mediaService = media.use;
|
|
335
305
|
const seen = new Set();
|
|
336
306
|
for (const ref of refs) {
|
|
337
307
|
const key = imageRefKey(ref);
|
|
@@ -339,13 +309,15 @@ async function recognizeForwardImages(caps, storage, selfId, data, limit) {
|
|
|
339
309
|
continue;
|
|
340
310
|
seen.add(key);
|
|
341
311
|
try {
|
|
342
|
-
const imageSource = await resolveForwardImageSource(caps,
|
|
312
|
+
const imageSource = await resolveForwardImageSource(caps, selfId, ref);
|
|
313
|
+
if (!imageSource)
|
|
314
|
+
continue;
|
|
343
315
|
const description = await mediaService.describeImage(imageSource);
|
|
344
316
|
if (description)
|
|
345
317
|
imageDescriptions.set(key, description);
|
|
346
318
|
}
|
|
347
319
|
catch (err) {
|
|
348
|
-
logger.debug(`合并转发图片识别失败 (${ref.source}): ${err}`);
|
|
320
|
+
logger.use.debug(`合并转发图片识别失败 (${ref.source}): ${err}`);
|
|
349
321
|
}
|
|
350
322
|
}
|
|
351
323
|
return { imageDescriptions };
|
|
@@ -405,6 +377,10 @@ function formatCqMessageContent(content, context) {
|
|
|
405
377
|
}
|
|
406
378
|
function formatForwardMessage(data, limit, context) {
|
|
407
379
|
const rawMessages = getForwardMessages(data);
|
|
380
|
+
if (!rawMessages)
|
|
381
|
+
throw new Error('合并转发的返回格式认不出');
|
|
382
|
+
if (!rawMessages.length)
|
|
383
|
+
return '合并转发内容为空';
|
|
408
384
|
const messages = rawMessages.slice(0, limit);
|
|
409
385
|
const lines = messages.map((item, index) => {
|
|
410
386
|
const node = item && typeof item === 'object' ? item : {};
|
|
@@ -425,79 +401,68 @@ function formatForwardMessage(data, limit, context) {
|
|
|
425
401
|
return `${index + 1}. ${prefix}: ${content || '[空消息]'}`;
|
|
426
402
|
});
|
|
427
403
|
const header = `合并转发共 ${rawMessages.length} 条${rawMessages.length > messages.length ? `,以下显示前 ${messages.length} 条` : ''}:`;
|
|
428
|
-
return
|
|
404
|
+
return `${header}\n${lines.join('\n')}`;
|
|
429
405
|
}
|
|
430
406
|
// ===== 权限检查辅助 =====
|
|
431
407
|
const ROLE_LEVEL = { owner: 3, admin: 2, member: 1 };
|
|
432
408
|
function roleLevel(role) {
|
|
433
409
|
return ROLE_LEVEL[role] ?? 0;
|
|
434
410
|
}
|
|
435
|
-
/**
|
|
411
|
+
/** 查询群成员信息,失败时抛出适配器的原话 */
|
|
436
412
|
async function getGroupMemberInfo(platform, selfId, groupId, userId) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}));
|
|
443
|
-
}
|
|
444
|
-
catch {
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
413
|
+
return (await callAction(platform, selfId, 'get_group_member_info', {
|
|
414
|
+
group_id: Number(groupId),
|
|
415
|
+
user_id: Number(userId),
|
|
416
|
+
no_cache: true,
|
|
417
|
+
}));
|
|
447
418
|
}
|
|
448
|
-
/**
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const
|
|
419
|
+
/**
|
|
420
|
+
* 机器人在群里的角色够不够做这个管理操作,不够时抛错。取不到机器人自身信息或其中没有角色时放过检查:真不够权限时实现端的
|
|
421
|
+
* 返回码会报出来。目标成员的信息或角色取不到(查询失败,或实现端回成功却没给)时报错,不当成普通成员
|
|
422
|
+
*/
|
|
423
|
+
async function assertAdminPermission(platform, selfId, groupId, targetUserId, requireOwner) {
|
|
424
|
+
const selfInfo = await getGroupMemberInfo(platform, selfId, groupId, selfId).catch(() => null);
|
|
425
|
+
if (!selfInfo?.role)
|
|
426
|
+
return;
|
|
427
|
+
const selfRole = String(selfInfo.role);
|
|
454
428
|
if (requireOwner && selfRole !== 'owner') {
|
|
455
|
-
|
|
429
|
+
throw new Error(`操作失败:此操作仅群主可执行(当前角色:${selfRole})`);
|
|
456
430
|
}
|
|
457
431
|
if (selfRole === 'member') {
|
|
458
|
-
|
|
432
|
+
throw new Error(`操作失败:机器人不是管理员(当前角色:${selfRole}),无法执行管理操作`);
|
|
459
433
|
}
|
|
460
434
|
if (targetUserId) {
|
|
461
|
-
const targetInfo = await getGroupMemberInfo(platform, selfId, groupId, targetUserId)
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
435
|
+
const targetInfo = await getGroupMemberInfo(platform, selfId, groupId, targetUserId).catch((err) => {
|
|
436
|
+
throw new Error(`操作失败:无法获取用户 ${targetUserId} 的信息:${err instanceof Error ? err.message : String(err)}`);
|
|
437
|
+
});
|
|
438
|
+
if (!targetInfo)
|
|
439
|
+
throw new Error(`操作失败:无法获取用户 ${targetUserId} 的信息:实现端没给成员信息`);
|
|
440
|
+
if (!targetInfo.role)
|
|
441
|
+
throw new Error(`操作失败:无法获取用户 ${targetUserId} 的信息:实现端没给成员角色`);
|
|
442
|
+
const targetRole = String(targetInfo.role);
|
|
466
443
|
if (roleLevel(targetRole) >= roleLevel(selfRole)) {
|
|
467
444
|
const targetLabel = targetRole === 'owner' ? '群主' : '管理员';
|
|
468
|
-
|
|
445
|
+
throw new Error(`操作失败:无法对${targetLabel}执行此操作(机器人角色:${selfRole},目标角色:${targetRole})`);
|
|
469
446
|
}
|
|
470
447
|
}
|
|
471
|
-
return null;
|
|
472
448
|
}
|
|
473
449
|
export default definePlugin({
|
|
474
450
|
name: '@aalis/plugin-tool-onebot',
|
|
475
|
-
displayName: '
|
|
451
|
+
displayName: t('displayName'),
|
|
476
452
|
subsystem: 'tools',
|
|
477
453
|
configSchema,
|
|
478
454
|
uses,
|
|
479
455
|
apply(caps) {
|
|
480
456
|
const { tools, events, logger, platform } = caps;
|
|
481
|
-
const cfg = parseConfig(configSchema, caps.config, logger);
|
|
482
|
-
const storage = createStorageGateway(caps.storage);
|
|
457
|
+
const cfg = parseConfig(configSchema, caps.config.use, logger.use);
|
|
483
458
|
const bundle = {
|
|
484
|
-
daily: withToolGroups(tools, ['onebot-daily']),
|
|
485
|
-
group: withToolGroups(tools, ['onebot-group']),
|
|
486
|
-
personal: withToolGroups(tools, ['onebot-personal']),
|
|
459
|
+
daily: withToolGroups(tools.use, ['onebot-daily']),
|
|
460
|
+
group: withToolGroups(tools.use, ['onebot-group']),
|
|
461
|
+
personal: withToolGroups(tools.use, ['onebot-personal']),
|
|
487
462
|
};
|
|
488
463
|
const maxLimit = Math.max(1, Math.min(1000, cfg.sessionHistory.maxLimit || 100));
|
|
489
464
|
const defaultLimitRaw = Math.max(1, Math.floor(cfg.sessionHistory.defaultLimit || 20));
|
|
490
|
-
const historyCfg = {
|
|
491
|
-
maxLimit,
|
|
492
|
-
defaultLimit: Math.min(defaultLimitRaw, maxLimit),
|
|
493
|
-
allowGroupReadPrivate: cfg.sessionHistory.allowGroupReadPrivate,
|
|
494
|
-
allowCrossSelf: cfg.sessionHistory.allowCrossSelf,
|
|
495
|
-
allowCrossGroup: cfg.sessionHistory.allowCrossGroup,
|
|
496
|
-
allowCrossPrivate: cfg.sessionHistory.allowCrossPrivate,
|
|
497
|
-
};
|
|
498
|
-
// 访问规则始终注册:它同样约束通用的 session_get_history,不随专属工具开关撤掉(关掉工具不能反倒放宽读取)。
|
|
499
|
-
// 规则只对 onebot 会话 id 表态,与 OneBot 平台在不在场无关:不进下面的平台闸。
|
|
500
|
-
registerOneBotHistoryAccessChecker(caps, historyCfg);
|
|
465
|
+
const historyCfg = { maxLimit, defaultLimit: Math.min(defaultLimitRaw, maxLimit) };
|
|
501
466
|
// 仅当 OneBot 平台可用时才注册工具。platform 是可选的多提供者服务,OneBot 适配器可能晚于
|
|
502
467
|
// app:ready 上线(WebUI 启用、市场热装、启动失败后修好),且作为非胜者上线时 follow 不触发:
|
|
503
468
|
// 故在 app:ready 与每次 platform 提供者注册时各判一次,出现 onebot 即注册,之后不再重复。
|
|
@@ -507,27 +472,30 @@ export default definePlugin({
|
|
|
507
472
|
if (toolsRegistered || !getPlatformNames(platform).includes('onebot'))
|
|
508
473
|
return;
|
|
509
474
|
toolsRegistered = true;
|
|
510
|
-
logger.info('检测到 OneBot 平台,开始注册 OneBot 工具');
|
|
475
|
+
logger.use.info('检测到 OneBot 平台,开始注册 OneBot 工具');
|
|
511
476
|
// 注册工具分组:按职责语义切分,彼此不重叠
|
|
512
|
-
tools.registerGroup({
|
|
477
|
+
tools.use.registerGroup({
|
|
513
478
|
name: 'onebot-daily',
|
|
514
|
-
label: '
|
|
479
|
+
label: t('dailyGroup'),
|
|
480
|
+
summary: t('dailyGroupSummary'),
|
|
515
481
|
description: '只读查询(群信息/成员/好友列表/历史/登录信息)+ 日常低风险互动(戳一戳、好友赞)',
|
|
516
482
|
});
|
|
517
|
-
tools.registerGroup({
|
|
483
|
+
tools.use.registerGroup({
|
|
518
484
|
name: 'onebot-group',
|
|
519
|
-
label: '
|
|
485
|
+
label: t('groupGroup'),
|
|
486
|
+
summary: t('groupGroupSummary'),
|
|
520
487
|
description: '群务管理:禁言/全员禁言/踢人/撤回消息/设置群名片/改群名/设管理员/群打卡/审批加群申请 等',
|
|
521
488
|
});
|
|
522
|
-
tools.registerGroup({
|
|
489
|
+
tools.use.registerGroup({
|
|
523
490
|
name: 'onebot-personal',
|
|
524
|
-
label: '
|
|
491
|
+
label: t('personalGroup'),
|
|
492
|
+
summary: t('personalGroupSummary'),
|
|
525
493
|
description: '影响 bot 账号本身的人际关系:退群、删好友、处理好友申请、接受/拒绝入群邀请',
|
|
526
494
|
});
|
|
527
495
|
if (cfg.groupManagement.enabled)
|
|
528
496
|
registerGroupManagementTools(caps, bundle);
|
|
529
497
|
if (cfg.groupInfo.enabled)
|
|
530
|
-
registerGroupInfoTools(caps,
|
|
498
|
+
registerGroupInfoTools(caps, bundle);
|
|
531
499
|
if (cfg.account.enabled)
|
|
532
500
|
registerAccountTools(caps, bundle);
|
|
533
501
|
if (cfg.interaction.enabled)
|
|
@@ -536,12 +504,12 @@ export default definePlugin({
|
|
|
536
504
|
registerSessionHistoryTools(caps, bundle, historyCfg);
|
|
537
505
|
registerRequestTools(caps, bundle);
|
|
538
506
|
};
|
|
539
|
-
events.on('app:ready', () => {
|
|
507
|
+
events.use.on('app:ready', () => {
|
|
540
508
|
registerToolsOnce();
|
|
541
509
|
if (!toolsRegistered)
|
|
542
|
-
logger.info('未检测到 OneBot 平台,暂不注册 OneBot 工具(平台上线后自动补注册)');
|
|
510
|
+
logger.use.info('未检测到 OneBot 平台,暂不注册 OneBot 工具(平台上线后自动补注册)');
|
|
543
511
|
});
|
|
544
|
-
events.on('service:
|
|
512
|
+
events.use.on('service:changed', name => {
|
|
545
513
|
if (name === platformService.name)
|
|
546
514
|
registerToolsOnce();
|
|
547
515
|
});
|
|
@@ -549,7 +517,7 @@ export default definePlugin({
|
|
|
549
517
|
});
|
|
550
518
|
// ===== 群管理工具 =====
|
|
551
519
|
function registerGroupManagementTools(caps, bundle) {
|
|
552
|
-
const { platform, logger } = caps;
|
|
520
|
+
const { platform, logger, sessionManager } = caps;
|
|
553
521
|
const { group, personal } = bundle;
|
|
554
522
|
// ---- 群禁言(单人)----
|
|
555
523
|
group.register({
|
|
@@ -564,6 +532,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
564
532
|
user_id: { type: 'string', description: '要禁言的用户QQ号' },
|
|
565
533
|
duration: { type: 'number', description: '禁言时长(秒),0 = 解除禁言,默认 60' },
|
|
566
534
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
535
|
+
self_id: SELF_ID_PARAM,
|
|
567
536
|
},
|
|
568
537
|
required: ['user_id'],
|
|
569
538
|
},
|
|
@@ -572,27 +541,25 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
572
541
|
handler: async (args, callCtx) => {
|
|
573
542
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
574
543
|
const duration = typeof args.duration === 'number' ? args.duration : 60;
|
|
575
|
-
|
|
576
|
-
if (permError)
|
|
577
|
-
return permError;
|
|
544
|
+
await assertAdminPermission(platform, selfId, groupId, String(args.user_id));
|
|
578
545
|
await callAction(platform, selfId, 'set_group_ban', {
|
|
579
546
|
group_id: Number(groupId),
|
|
580
547
|
user_id: Number(args.user_id),
|
|
581
548
|
duration,
|
|
582
549
|
});
|
|
583
|
-
//
|
|
584
|
-
const info = await getGroupMemberInfo(platform, selfId, groupId, String(args.user_id));
|
|
550
|
+
// 验证禁言是否生效:查不到成员信息时照报已发出指令、写明无法验证
|
|
551
|
+
const info = await getGroupMemberInfo(platform, selfId, groupId, String(args.user_id)).catch(() => null);
|
|
585
552
|
if (info && 'shut_up_timestamp' in info) {
|
|
586
553
|
const shutUp = Number(info.shut_up_timestamp);
|
|
587
554
|
const now = Math.floor(Date.now() / 1000);
|
|
588
555
|
if (duration === 0) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
return shutUp > now
|
|
593
|
-
? `已禁言 ${args.user_id},时长 ${duration} 秒`
|
|
594
|
-
: `禁言操作未生效(API 返回成功但禁言状态未变化),请检查机器人实际权限`;
|
|
556
|
+
if (shutUp > now)
|
|
557
|
+
throw new Error('解除禁言指令已发送,但用户仍在禁言中');
|
|
558
|
+
return `已解除 ${args.user_id} 的禁言`;
|
|
595
559
|
}
|
|
560
|
+
if (shutUp <= now)
|
|
561
|
+
throw new Error('禁言操作未生效(API 返回成功但禁言状态未变化),请检查机器人实际权限');
|
|
562
|
+
return `已禁言 ${args.user_id},时长 ${duration} 秒`;
|
|
596
563
|
}
|
|
597
564
|
return duration === 0
|
|
598
565
|
? `已解除 ${args.user_id} 的禁言`
|
|
@@ -611,6 +578,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
611
578
|
properties: {
|
|
612
579
|
enable: { type: 'boolean', description: 'true = 开启全群禁言,false = 关闭' },
|
|
613
580
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
581
|
+
self_id: SELF_ID_PARAM,
|
|
614
582
|
},
|
|
615
583
|
required: ['enable'],
|
|
616
584
|
},
|
|
@@ -618,9 +586,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
618
586
|
},
|
|
619
587
|
handler: async (args, callCtx) => {
|
|
620
588
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
621
|
-
|
|
622
|
-
if (permError)
|
|
623
|
-
return permError;
|
|
589
|
+
await assertAdminPermission(platform, selfId, groupId);
|
|
624
590
|
await callAction(platform, selfId, 'set_group_whole_ban', {
|
|
625
591
|
group_id: Number(groupId),
|
|
626
592
|
enable: !!args.enable,
|
|
@@ -641,6 +607,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
641
607
|
user_id: { type: 'string', description: '要踢出的用户QQ号' },
|
|
642
608
|
reject_add_request: { type: 'boolean', description: '是否拒绝此人再次加群,默认 false' },
|
|
643
609
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
610
|
+
self_id: SELF_ID_PARAM,
|
|
644
611
|
},
|
|
645
612
|
required: ['user_id'],
|
|
646
613
|
},
|
|
@@ -648,9 +615,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
648
615
|
},
|
|
649
616
|
handler: async (args, callCtx) => {
|
|
650
617
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
651
|
-
|
|
652
|
-
if (permError)
|
|
653
|
-
return permError;
|
|
618
|
+
await assertAdminPermission(platform, selfId, groupId, String(args.user_id));
|
|
654
619
|
await callAction(platform, selfId, 'set_group_kick', {
|
|
655
620
|
group_id: Number(groupId),
|
|
656
621
|
user_id: Number(args.user_id),
|
|
@@ -670,6 +635,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
670
635
|
type: 'object',
|
|
671
636
|
properties: {
|
|
672
637
|
group_id: { type: 'string', description: '要退出的群号;在群聊中可省略,默认当前群' },
|
|
638
|
+
self_id: SELF_ID_PARAM,
|
|
673
639
|
},
|
|
674
640
|
required: [],
|
|
675
641
|
},
|
|
@@ -681,7 +647,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
681
647
|
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
682
648
|
const groupId = argGroupId || (parsed?.detailType === 'group' ? parsed.targetId : '');
|
|
683
649
|
if (!groupId)
|
|
684
|
-
|
|
650
|
+
throw new Error('请提供 group_id,或在要退出的群聊中调用此工具');
|
|
685
651
|
await callAction(platform, selfId, 'set_group_leave', {
|
|
686
652
|
group_id: Number(groupId),
|
|
687
653
|
is_dismiss: false,
|
|
@@ -702,6 +668,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
702
668
|
user_id: { type: 'string', description: '用户QQ号' },
|
|
703
669
|
card: { type: 'string', description: '新的群名片,空字符串表示取消' },
|
|
704
670
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
671
|
+
self_id: SELF_ID_PARAM,
|
|
705
672
|
},
|
|
706
673
|
required: ['user_id', 'card'],
|
|
707
674
|
},
|
|
@@ -709,9 +676,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
709
676
|
},
|
|
710
677
|
handler: async (args, callCtx) => {
|
|
711
678
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
712
|
-
|
|
713
|
-
if (permError)
|
|
714
|
-
return permError;
|
|
679
|
+
await assertAdminPermission(platform, selfId, groupId);
|
|
715
680
|
await callAction(platform, selfId, 'set_group_card', {
|
|
716
681
|
group_id: Number(groupId),
|
|
717
682
|
user_id: Number(args.user_id),
|
|
@@ -732,6 +697,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
732
697
|
properties: {
|
|
733
698
|
group_name: { type: 'string', description: '新的群名称' },
|
|
734
699
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
700
|
+
self_id: SELF_ID_PARAM,
|
|
735
701
|
},
|
|
736
702
|
required: ['group_name'],
|
|
737
703
|
},
|
|
@@ -739,9 +705,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
739
705
|
},
|
|
740
706
|
handler: async (args, callCtx) => {
|
|
741
707
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
742
|
-
|
|
743
|
-
if (permError)
|
|
744
|
-
return permError;
|
|
708
|
+
await assertAdminPermission(platform, selfId, groupId);
|
|
745
709
|
await callAction(platform, selfId, 'set_group_name', {
|
|
746
710
|
group_id: Number(groupId),
|
|
747
711
|
group_name: String(args.group_name),
|
|
@@ -762,6 +726,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
762
726
|
user_id: { type: 'string', description: '用户QQ号' },
|
|
763
727
|
special_title: { type: 'string', description: '专属头衔,空字符串表示取消' },
|
|
764
728
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
729
|
+
self_id: SELF_ID_PARAM,
|
|
765
730
|
},
|
|
766
731
|
required: ['user_id', 'special_title'],
|
|
767
732
|
},
|
|
@@ -769,9 +734,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
769
734
|
},
|
|
770
735
|
handler: async (args, callCtx) => {
|
|
771
736
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
772
|
-
|
|
773
|
-
if (permError)
|
|
774
|
-
return permError;
|
|
737
|
+
await assertAdminPermission(platform, selfId, groupId, undefined, true);
|
|
775
738
|
await callAction(platform, selfId, 'set_group_special_title', {
|
|
776
739
|
group_id: Number(groupId),
|
|
777
740
|
user_id: Number(args.user_id),
|
|
@@ -794,6 +757,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
794
757
|
user_id: { type: 'string', description: '用户QQ号' },
|
|
795
758
|
enable: { type: 'boolean', description: 'true = 设为管理员,false = 取消管理员' },
|
|
796
759
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
760
|
+
self_id: SELF_ID_PARAM,
|
|
797
761
|
},
|
|
798
762
|
required: ['user_id', 'enable'],
|
|
799
763
|
},
|
|
@@ -801,9 +765,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
801
765
|
},
|
|
802
766
|
handler: async (args, callCtx) => {
|
|
803
767
|
const { selfId, groupId } = resolveGroupTarget(platform, callCtx, args);
|
|
804
|
-
|
|
805
|
-
if (permError)
|
|
806
|
-
return permError;
|
|
768
|
+
await assertAdminPermission(platform, selfId, groupId, undefined, true);
|
|
807
769
|
await callAction(platform, selfId, 'set_group_admin', {
|
|
808
770
|
group_id: Number(groupId),
|
|
809
771
|
user_id: Number(args.user_id),
|
|
@@ -823,6 +785,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
823
785
|
type: 'object',
|
|
824
786
|
properties: {
|
|
825
787
|
message_id: { type: 'string', description: '要撤回的消息 ID' },
|
|
788
|
+
self_id: SELF_ID_PARAM,
|
|
826
789
|
},
|
|
827
790
|
required: ['message_id'],
|
|
828
791
|
},
|
|
@@ -853,6 +816,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
853
816
|
count: { type: 'number', description: '撤回最近多少条机器人消息(默认 1,上限 10)' },
|
|
854
817
|
group_id: { type: 'string', description: '可选:目标群号。不传则用当前会话。' },
|
|
855
818
|
user_id: { type: 'string', description: '可选:目标私聊 QQ 号。不传则用当前会话。' },
|
|
819
|
+
self_id: SELF_ID_PARAM,
|
|
856
820
|
},
|
|
857
821
|
required: [],
|
|
858
822
|
},
|
|
@@ -870,17 +834,22 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
870
834
|
else {
|
|
871
835
|
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
872
836
|
if (!parsed)
|
|
873
|
-
|
|
837
|
+
throw new Error('当前不在 OneBot 会话中,请用 group_id 或 user_id 指定要撤回的目标会话。');
|
|
874
838
|
targetSessionId = callCtx.sessionId;
|
|
875
839
|
}
|
|
876
|
-
|
|
840
|
+
// 目标会话逐个参与者过会话管理的规则(同 onebot_get_msg):不放行时在读发出记录之前报错,不撤回,报错里没有她在那里说的话
|
|
841
|
+
const verdict = await callCtx.approveFor(who => takeFrom(sessionManager, callCtx.sessionId, targetSessionId, who));
|
|
842
|
+
if (!verdict.ok)
|
|
843
|
+
throw new Error(toSource(verdict.reason));
|
|
844
|
+
// 发出记录在发消息的那个实例里:按目标会话路由
|
|
845
|
+
const adapter = (await resolvePlatformBySession(platform, targetSessionId))?.use;
|
|
877
846
|
if (!adapter?.getSentMessages) {
|
|
878
|
-
|
|
847
|
+
throw new Error('未找到支持记录自身发出消息的 OneBot 适配器(getSentMessages 不在平台契约内,第三方适配器可不提供)。如已知 message_id 可改用 onebot_delete_msg。');
|
|
879
848
|
}
|
|
880
849
|
const count = Math.max(1, Math.min(10, Math.floor(Number(args.count) || 1)));
|
|
881
850
|
const recent = adapter.getSentMessages(targetSessionId, 10); // 新→旧
|
|
882
851
|
if (recent.length === 0) {
|
|
883
|
-
|
|
852
|
+
throw new Error('没有可撤回的记录:机器人近期未在该会话发送消息,或已超出记录时窗(约 30 分钟)。');
|
|
884
853
|
}
|
|
885
854
|
const targets = recent.slice(0, count);
|
|
886
855
|
const recalled = [];
|
|
@@ -896,7 +865,7 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
896
865
|
}
|
|
897
866
|
}
|
|
898
867
|
if (recalled.length === 0) {
|
|
899
|
-
|
|
868
|
+
throw new Error(`撤回失败:${failed.join(';')}`);
|
|
900
869
|
}
|
|
901
870
|
let msg = `已撤回机器人发送的 ${recalled.length} 条消息:${recalled.join('、')}`;
|
|
902
871
|
if (failed.length > 0)
|
|
@@ -904,11 +873,11 @@ function registerGroupManagementTools(caps, bundle) {
|
|
|
904
873
|
return msg;
|
|
905
874
|
},
|
|
906
875
|
});
|
|
907
|
-
logger.info('OneBot 群管理工具已注册');
|
|
876
|
+
logger.use.info('OneBot 群管理工具已注册');
|
|
908
877
|
}
|
|
909
878
|
// ===== 群信息查询工具 =====
|
|
910
|
-
function registerGroupInfoTools(caps,
|
|
911
|
-
const { platform, logger } = caps;
|
|
879
|
+
function registerGroupInfoTools(caps, bundle) {
|
|
880
|
+
const { platform, logger, sessionManager } = caps;
|
|
912
881
|
const { daily } = bundle;
|
|
913
882
|
// ---- 查看合并转发 ----
|
|
914
883
|
daily.register({
|
|
@@ -916,7 +885,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
916
885
|
type: 'function',
|
|
917
886
|
function: {
|
|
918
887
|
name: 'onebot_get_forward_msg',
|
|
919
|
-
description: '读取合并转发消息内容(OneBot v11 标准 get_forward_msg)。收到 <forward id="...">[合并转发消息]</forward>
|
|
888
|
+
description: '读取合并转发消息内容(OneBot v11 标准 get_forward_msg)。收到 <forward id="...">[合并转发消息]</forward> 时,把尖括号里的 id 字符串作为参数 id 传入。',
|
|
920
889
|
parameters: {
|
|
921
890
|
type: 'object',
|
|
922
891
|
properties: {
|
|
@@ -925,6 +894,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
925
894
|
description: '合并转发 ID(forward 消息段的 data.id),与 OneBot v11 标准参数名一致',
|
|
926
895
|
},
|
|
927
896
|
limit: { type: 'number', description: '最多返回多少条节点,默认 30' },
|
|
897
|
+
self_id: SELF_ID_PARAM,
|
|
928
898
|
},
|
|
929
899
|
required: ['id'],
|
|
930
900
|
},
|
|
@@ -933,27 +903,51 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
933
903
|
handler: async (args, callCtx) => {
|
|
934
904
|
const selfId = resolveSelfId(platform, callCtx, args);
|
|
935
905
|
if (!args.id)
|
|
936
|
-
|
|
906
|
+
throw new Error('参数错误:缺少 id(合并转发 ID)。请使用 <forward id="..."> 中的 id 字符串。');
|
|
937
907
|
const forwardId = String(args.id);
|
|
938
908
|
const limit = Math.max(1, Math.min(100, typeof args.limit === 'number' ? Math.floor(args.limit) : 30));
|
|
939
|
-
|
|
940
|
-
//
|
|
909
|
+
const readFailed = (err) => new Error(`合并转发读取失败:${err instanceof Error ? err.message : String(err)}`);
|
|
910
|
+
// 适配器的 callAction 维护了一份接收时即抓取并展开(含摘要与图像识别)的缓存,每个收到的转发都记着收到它的会话。
|
|
941
911
|
let data;
|
|
942
912
|
try {
|
|
943
913
|
data = await callAction(platform, selfId, 'get_forward_msg', { id: forwardId });
|
|
944
914
|
}
|
|
945
915
|
catch (err) {
|
|
946
|
-
|
|
916
|
+
throw readFailed(err);
|
|
947
917
|
}
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
918
|
+
// 适配器交回收到这条转发的会话:展开过的连同 ForwardEntry 形状(fullText / summary / 元信息与截断标志),没展开过的连同
|
|
919
|
+
// 按 id 现拉的办法。收到它的会话逐个参与者过会话管理的规则(同 onebot_get_msg),几处之一过得了就行,过了才拉;没记会话的
|
|
920
|
+
// (从没收到过的、别的适配器交回的原始数据)按判不出拒绝
|
|
921
|
+
const reply = (data && typeof data === 'object' ? data : {});
|
|
922
|
+
const recorded = reply.__aalisForwardEntry === true || reply.__aalisForwardReceipt === true;
|
|
923
|
+
const sessionIds = recorded && Array.isArray(reply.sessionIds) ? reply.sessionIds.map(String) : [];
|
|
924
|
+
const verdict = sessionIds.length === 0
|
|
925
|
+
? { ok: false, reason: TAKE_UNKNOWN }
|
|
926
|
+
: await callCtx.approveFor(who => {
|
|
927
|
+
const verdicts = sessionIds.map(id => takeFrom(sessionManager, callCtx.sessionId, id, who));
|
|
928
|
+
return verdicts.find(v => v.ok) ?? verdicts[0];
|
|
929
|
+
});
|
|
930
|
+
if (!verdict.ok)
|
|
931
|
+
throw new Error(toSource(verdict.reason));
|
|
932
|
+
// 展开过的直接返回原文给 LLM,无需再做协议端结构解析
|
|
933
|
+
if (reply.__aalisForwardEntry === true) {
|
|
934
|
+
const entry = reply;
|
|
935
|
+
const cut = [entry.truncatedDepth && '嵌套超深', entry.truncatedNodes && '节点超限'].filter(Boolean);
|
|
936
|
+
// 参与人与入站信封同一写法:条目只列前 8 个,没列出的人数另记
|
|
937
|
+
const participants = [...entry.participants, ...(entry.unlisted ? [`...(+${entry.unlisted} 人未列出)`] : [])];
|
|
938
|
+
const header = `合并转发共 ${entry.count} 条,参与人:${participants.join(', ') || '未知'}` +
|
|
939
|
+
(cut.length ? `,已截断(${cut.join('、')})` : '');
|
|
953
940
|
const summaryLine = entry.summary ? `\n摘要:${entry.summary}\n` : '\n';
|
|
954
941
|
return `${header}${summaryLine}\n原文:\n${entry.fullText}`;
|
|
955
942
|
}
|
|
956
|
-
|
|
943
|
+
// 没展开过的:过了判定才按 id 现拉
|
|
944
|
+
try {
|
|
945
|
+
data = await reply.fetch();
|
|
946
|
+
}
|
|
947
|
+
catch (err) {
|
|
948
|
+
throw readFailed(err);
|
|
949
|
+
}
|
|
950
|
+
const formatContext = await recognizeForwardImages(caps, selfId, data, limit);
|
|
957
951
|
return formatForwardMessage(data, limit, formatContext);
|
|
958
952
|
},
|
|
959
953
|
});
|
|
@@ -968,6 +962,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
968
962
|
type: 'object',
|
|
969
963
|
properties: {
|
|
970
964
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
965
|
+
self_id: SELF_ID_PARAM,
|
|
971
966
|
},
|
|
972
967
|
required: [],
|
|
973
968
|
},
|
|
@@ -993,6 +988,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
993
988
|
properties: {
|
|
994
989
|
user_id: { type: 'string', description: '可选:要查询的用户 QQ 号。缺省查询机器人自身。' },
|
|
995
990
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
991
|
+
self_id: SELF_ID_PARAM,
|
|
996
992
|
},
|
|
997
993
|
required: [],
|
|
998
994
|
},
|
|
@@ -1024,6 +1020,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1024
1020
|
limit: { type: 'number', description: '本页最多返回条数,默认 30' },
|
|
1025
1021
|
offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
|
|
1026
1022
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
1023
|
+
self_id: SELF_ID_PARAM,
|
|
1027
1024
|
},
|
|
1028
1025
|
required: [],
|
|
1029
1026
|
},
|
|
@@ -1087,6 +1084,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1087
1084
|
type: 'string',
|
|
1088
1085
|
description: '可选。指定群号。缺省时查当前会话所在群(私聊时缺省会报错,请显式传入)。',
|
|
1089
1086
|
},
|
|
1087
|
+
self_id: SELF_ID_PARAM,
|
|
1090
1088
|
},
|
|
1091
1089
|
required: [],
|
|
1092
1090
|
},
|
|
@@ -1098,14 +1096,10 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1098
1096
|
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
1099
1097
|
let groupId = argGroupId;
|
|
1100
1098
|
if (!groupId) {
|
|
1101
|
-
if (parsed?.detailType !== 'group')
|
|
1102
|
-
|
|
1103
|
-
}
|
|
1099
|
+
if (parsed?.detailType !== 'group')
|
|
1100
|
+
throw new Error('当前不在群聊中,请显式传入 group_id');
|
|
1104
1101
|
groupId = parsed.targetId;
|
|
1105
1102
|
}
|
|
1106
|
-
const adapter = findOneBotAdapter(platform);
|
|
1107
|
-
if (!adapter?.callAction)
|
|
1108
|
-
return JSON.stringify({ available: false, reason: 'OneBot 适配器不可用' });
|
|
1109
1103
|
let info = null;
|
|
1110
1104
|
try {
|
|
1111
1105
|
info = (await callAction(platform, selfId, 'get_group_member_info', {
|
|
@@ -1115,18 +1109,14 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1115
1109
|
}));
|
|
1116
1110
|
}
|
|
1117
1111
|
catch (err) {
|
|
1118
|
-
|
|
1119
|
-
available: false,
|
|
1120
|
-
reason: `查询失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
1121
|
-
});
|
|
1112
|
+
throw new Error(`查询失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
1122
1113
|
}
|
|
1123
1114
|
if (!info)
|
|
1124
|
-
|
|
1115
|
+
throw new Error('无法获取群成员信息');
|
|
1125
1116
|
const ts = Number(info.shut_up_timestamp ?? 0);
|
|
1126
1117
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
1127
1118
|
const muted = ts > nowSec;
|
|
1128
1119
|
return JSON.stringify({
|
|
1129
|
-
available: true,
|
|
1130
1120
|
muted,
|
|
1131
1121
|
shutUpTimestamp: ts,
|
|
1132
1122
|
untilIso: muted ? new Date(ts * 1000).toISOString() : null,
|
|
@@ -1157,23 +1147,16 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1157
1147
|
},
|
|
1158
1148
|
},
|
|
1159
1149
|
handler: async (args) => {
|
|
1160
|
-
const
|
|
1161
|
-
if (
|
|
1162
|
-
|
|
1163
|
-
supported: false,
|
|
1164
|
-
reason: '未找到支持记录自身禁言状态的 OneBot 适配器(getSelfMutes 不在平台契约内,第三方适配器可不提供)',
|
|
1165
|
-
});
|
|
1150
|
+
const supporting = getPlatformAdapters(platform).filter((a) => a.platform === 'onebot' && typeof a.getSelfMutes === 'function');
|
|
1151
|
+
if (supporting.length === 0) {
|
|
1152
|
+
throw new Error('未找到支持记录自身禁言状态的 OneBot 适配器(getSelfMutes 不在平台契约内,第三方适配器可不提供)');
|
|
1166
1153
|
}
|
|
1167
1154
|
const limit = Math.max(1, Math.min(200, Math.floor(Number(args.limit) || 30)));
|
|
1168
1155
|
const offset = Math.max(0, Math.floor(Number(args.offset) || 0));
|
|
1169
|
-
const mutes =
|
|
1170
|
-
.getSelfMutes()
|
|
1171
|
-
.slice()
|
|
1172
|
-
.sort((a, b) => a.remainingSec - b.remainingSec);
|
|
1156
|
+
const mutes = supporting.flatMap(a => a.getSelfMutes()).sort((a, b) => a.remainingSec - b.remainingSec);
|
|
1173
1157
|
const total = mutes.length;
|
|
1174
1158
|
const sliced = mutes.slice(offset, offset + limit);
|
|
1175
1159
|
return JSON.stringify({
|
|
1176
|
-
supported: true,
|
|
1177
1160
|
total,
|
|
1178
1161
|
offset,
|
|
1179
1162
|
limit,
|
|
@@ -1194,11 +1177,12 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1194
1177
|
type: 'function',
|
|
1195
1178
|
function: {
|
|
1196
1179
|
name: 'onebot_get_msg',
|
|
1197
|
-
description: '按 message_id
|
|
1180
|
+
description: '按 message_id 获取单条消息的完整内容(包括发送者、消息段等)。可用于回溯查看历史中的某条消息,或在收到撤回通知后查看被撤回的内容。消息所在的会话按会话管理的跨会话规则判(默认不取别人私聊里的消息)。',
|
|
1198
1181
|
parameters: {
|
|
1199
1182
|
type: 'object',
|
|
1200
1183
|
properties: {
|
|
1201
1184
|
message_id: { type: 'string', description: '消息 ID' },
|
|
1185
|
+
self_id: SELF_ID_PARAM,
|
|
1202
1186
|
},
|
|
1203
1187
|
required: ['message_id'],
|
|
1204
1188
|
},
|
|
@@ -1207,16 +1191,25 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1207
1191
|
handler: async (args, callCtx) => {
|
|
1208
1192
|
const selfId = resolveSelfId(platform, callCtx, args);
|
|
1209
1193
|
if (!args.message_id)
|
|
1210
|
-
|
|
1194
|
+
throw new Error('参数错误:缺少 message_id');
|
|
1195
|
+
let data;
|
|
1211
1196
|
try {
|
|
1212
|
-
|
|
1197
|
+
data = (await callAction(platform, selfId, 'get_msg', {
|
|
1213
1198
|
message_id: Number(args.message_id),
|
|
1214
|
-
});
|
|
1215
|
-
return JSON.stringify(data);
|
|
1199
|
+
}));
|
|
1216
1200
|
}
|
|
1217
1201
|
catch (err) {
|
|
1218
|
-
|
|
1202
|
+
throw new Error(`获取消息失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1219
1203
|
}
|
|
1204
|
+
// 取到的消息按所在的窗口拼出会话 id 过规则:拼不出(她自己发的私聊消息、实现端不给字段)的按判不出拒绝
|
|
1205
|
+
const target = sessionOfMessage(selfId, data);
|
|
1206
|
+
// 逐个参与者过会话管理的规则:有人过不了(某个参与者自己私聊里的)时发一道参与者审批,由过得了的人批
|
|
1207
|
+
const verdict = target === undefined
|
|
1208
|
+
? { ok: false, reason: TAKE_UNKNOWN }
|
|
1209
|
+
: await callCtx.approveFor(who => takeFrom(sessionManager, callCtx.sessionId, target, who));
|
|
1210
|
+
if (!verdict.ok)
|
|
1211
|
+
throw new Error(toSource(verdict.reason));
|
|
1212
|
+
return JSON.stringify(data);
|
|
1220
1213
|
},
|
|
1221
1214
|
});
|
|
1222
1215
|
// ---- 获取群荣誉信息 ----
|
|
@@ -1235,6 +1228,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1235
1228
|
description: '荣誉类型:talkative=龙王, performer=群聊之火, legend=群聊炽焰, strong_newbie=冒尖小春笋, emotion=快乐之源, all=全部。默认 all。',
|
|
1236
1229
|
},
|
|
1237
1230
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
1231
|
+
self_id: SELF_ID_PARAM,
|
|
1238
1232
|
},
|
|
1239
1233
|
required: [],
|
|
1240
1234
|
},
|
|
@@ -1250,7 +1244,7 @@ function registerGroupInfoTools(caps, storage, bundle) {
|
|
|
1250
1244
|
return JSON.stringify(data);
|
|
1251
1245
|
},
|
|
1252
1246
|
});
|
|
1253
|
-
logger.info('OneBot 群信息查询工具已注册');
|
|
1247
|
+
logger.use.info('OneBot 群信息查询工具已注册');
|
|
1254
1248
|
}
|
|
1255
1249
|
// ===== 账号 / 好友 / 群列表查询工具 =====
|
|
1256
1250
|
function registerAccountTools(caps, bundle) {
|
|
@@ -1272,6 +1266,7 @@ function registerAccountTools(caps, bundle) {
|
|
|
1272
1266
|
keyword: { type: 'string', description: '可选:按群名或群号子串模糊匹配(不区分大小写)' },
|
|
1273
1267
|
limit: { type: 'number', description: '本页最多返回条数,默认 30' },
|
|
1274
1268
|
offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
|
|
1269
|
+
self_id: SELF_ID_PARAM,
|
|
1275
1270
|
},
|
|
1276
1271
|
required: [],
|
|
1277
1272
|
},
|
|
@@ -1321,6 +1316,7 @@ function registerAccountTools(caps, bundle) {
|
|
|
1321
1316
|
keyword: { type: 'string', description: '可选:按昵称、备注或 QQ 号子串模糊匹配(不区分大小写)' },
|
|
1322
1317
|
limit: { type: 'number', description: '本页最多返回条数,默认 30' },
|
|
1323
1318
|
offset: { type: 'number', description: '跳过前 N 条用于翻页,默认 0' },
|
|
1319
|
+
self_id: SELF_ID_PARAM,
|
|
1324
1320
|
},
|
|
1325
1321
|
required: [],
|
|
1326
1322
|
},
|
|
@@ -1369,6 +1365,7 @@ function registerAccountTools(caps, bundle) {
|
|
|
1369
1365
|
properties: {
|
|
1370
1366
|
user_id: { type: 'string', description: '要查询的 QQ 号' },
|
|
1371
1367
|
no_cache: { type: 'boolean', description: '是否跳过缓存强制刷新,默认 false' },
|
|
1368
|
+
self_id: SELF_ID_PARAM,
|
|
1372
1369
|
},
|
|
1373
1370
|
required: ['user_id'],
|
|
1374
1371
|
},
|
|
@@ -1390,11 +1387,11 @@ function registerAccountTools(caps, bundle) {
|
|
|
1390
1387
|
function: {
|
|
1391
1388
|
name: 'onebot_get_login_info',
|
|
1392
1389
|
description: '显式查询当前 OneBot 连接的登录账号信息(QQ 号、昵称)。仅在用户要求核实账号、诊断连接或需要最新平台返回值时使用。',
|
|
1393
|
-
parameters: { type: 'object', properties: {}, required: [] },
|
|
1390
|
+
parameters: { type: 'object', properties: { self_id: SELF_ID_PARAM }, required: [] },
|
|
1394
1391
|
},
|
|
1395
1392
|
},
|
|
1396
|
-
handler: async (
|
|
1397
|
-
const selfId = resolveSelfId(platform, callCtx,
|
|
1393
|
+
handler: async (args, callCtx) => {
|
|
1394
|
+
const selfId = resolveSelfId(platform, callCtx, args);
|
|
1398
1395
|
const data = await callAction(platform, selfId, 'get_login_info', {});
|
|
1399
1396
|
return JSON.stringify(data);
|
|
1400
1397
|
},
|
|
@@ -1410,6 +1407,7 @@ function registerAccountTools(caps, bundle) {
|
|
|
1410
1407
|
type: 'object',
|
|
1411
1408
|
properties: {
|
|
1412
1409
|
user_id: { type: 'string', description: '要删除的好友 QQ 号;在私聊中可省略,默认当前对话用户' },
|
|
1410
|
+
self_id: SELF_ID_PARAM,
|
|
1413
1411
|
},
|
|
1414
1412
|
required: [],
|
|
1415
1413
|
},
|
|
@@ -1421,14 +1419,14 @@ function registerAccountTools(caps, bundle) {
|
|
|
1421
1419
|
const parsed = parseOneBotSession(callCtx.sessionId);
|
|
1422
1420
|
const userId = argUserId || (parsed?.detailType === 'private' ? parsed.targetId : '');
|
|
1423
1421
|
if (!userId)
|
|
1424
|
-
|
|
1422
|
+
throw new Error('请提供 user_id,或在要删除的好友私聊中调用此工具');
|
|
1425
1423
|
await callAction(platform, selfId, 'delete_friend', {
|
|
1426
1424
|
user_id: Number(userId),
|
|
1427
1425
|
});
|
|
1428
1426
|
return `已删除好友 ${userId}`;
|
|
1429
1427
|
},
|
|
1430
1428
|
});
|
|
1431
|
-
logger.info('OneBot 账号 / 好友 / 群列表查询工具已注册');
|
|
1429
|
+
logger.use.info('OneBot 账号 / 好友 / 群列表查询工具已注册');
|
|
1432
1430
|
}
|
|
1433
1431
|
// ===== 特殊交互工具 =====
|
|
1434
1432
|
function registerInteractionTools(caps, bundle) {
|
|
@@ -1452,6 +1450,7 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1452
1450
|
type: 'string',
|
|
1453
1451
|
description: '可选:目标群号。群聊省略则使用当前群;如显式传值则跨群操作。',
|
|
1454
1452
|
},
|
|
1453
|
+
self_id: SELF_ID_PARAM,
|
|
1455
1454
|
},
|
|
1456
1455
|
},
|
|
1457
1456
|
},
|
|
@@ -1467,18 +1466,10 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1467
1466
|
if (!argUserId) {
|
|
1468
1467
|
throw new Error('群聊戳一戳必须显式提供 user_id(要戳的对象)。');
|
|
1469
1468
|
}
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
});
|
|
1475
|
-
}
|
|
1476
|
-
catch {
|
|
1477
|
-
await callAction(platform, selfId, 'send_group_poke', {
|
|
1478
|
-
group_id: Number(groupId),
|
|
1479
|
-
user_id: Number(argUserId),
|
|
1480
|
-
});
|
|
1481
|
-
}
|
|
1469
|
+
await callFirstSupported(platform, selfId, ['group_poke', 'send_group_poke'], {
|
|
1470
|
+
group_id: Number(groupId),
|
|
1471
|
+
user_id: Number(argUserId),
|
|
1472
|
+
});
|
|
1482
1473
|
return `已在群 ${groupId} 戳了 ${argUserId} 一下`;
|
|
1483
1474
|
}
|
|
1484
1475
|
// 2) 私聊场景:从 user_id 或当前私聊会话对方取目标
|
|
@@ -1486,13 +1477,7 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1486
1477
|
if (!userId) {
|
|
1487
1478
|
throw new Error('无法判断戳一戳目标:当前不在 OneBot 会话内,请显式传入 group_id+user_id 或 user_id。');
|
|
1488
1479
|
}
|
|
1489
|
-
|
|
1490
|
-
try {
|
|
1491
|
-
await callAction(platform, selfId, 'friend_poke', { user_id: Number(userId) });
|
|
1492
|
-
}
|
|
1493
|
-
catch {
|
|
1494
|
-
await callAction(platform, selfId, 'send_poke', { user_id: Number(userId) });
|
|
1495
|
-
}
|
|
1480
|
+
await callFirstSupported(platform, selfId, ['friend_poke', 'send_poke'], { user_id: Number(userId) });
|
|
1496
1481
|
return `已戳了 ${userId} 一下`;
|
|
1497
1482
|
},
|
|
1498
1483
|
});
|
|
@@ -1508,6 +1493,7 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1508
1493
|
properties: {
|
|
1509
1494
|
user_id: { type: 'string', description: '要赞的用户 QQ 号' },
|
|
1510
1495
|
times: { type: 'number', description: '赞的次数,默认 1,每天每个好友最多 10 次' },
|
|
1496
|
+
self_id: SELF_ID_PARAM,
|
|
1511
1497
|
},
|
|
1512
1498
|
required: ['user_id'],
|
|
1513
1499
|
},
|
|
@@ -1534,6 +1520,7 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1534
1520
|
type: 'object',
|
|
1535
1521
|
properties: {
|
|
1536
1522
|
group_id: { type: 'string', description: '可选:目标群号。不传则使用当前群会话的群号。' },
|
|
1523
|
+
self_id: SELF_ID_PARAM,
|
|
1537
1524
|
},
|
|
1538
1525
|
required: [],
|
|
1539
1526
|
},
|
|
@@ -1547,53 +1534,7 @@ function registerInteractionTools(caps, bundle) {
|
|
|
1547
1534
|
return '打卡成功';
|
|
1548
1535
|
},
|
|
1549
1536
|
});
|
|
1550
|
-
logger.info('OneBot 特殊交互工具已注册');
|
|
1551
|
-
}
|
|
1552
|
-
/**
|
|
1553
|
-
* 把 OneBot 的细粒度访问规则注册到 session-history 服务。
|
|
1554
|
-
* 通用工具 session_get_history 和平台专属工具 onebot_get_session_history
|
|
1555
|
-
* 调用 service 时都会走这条规则链 —— 不存在绕过路径。
|
|
1556
|
-
*/
|
|
1557
|
-
function registerOneBotHistoryAccessChecker(caps, cfg) {
|
|
1558
|
-
const { logger, sessionHistory } = caps;
|
|
1559
|
-
const checker = {
|
|
1560
|
-
platform: 'onebot',
|
|
1561
|
-
check({ currentSessionId, targetSessionId }) {
|
|
1562
|
-
const target = parseOneBotSession(targetSessionId);
|
|
1563
|
-
if (!target)
|
|
1564
|
-
return undefined; // 不是合法 onebot 目标, 不表态
|
|
1565
|
-
const current = parseOneBotSession(currentSessionId);
|
|
1566
|
-
// 跨平台调用(current 不是 onebot)走默认放行: session-history scope 已粗筛
|
|
1567
|
-
if (!current)
|
|
1568
|
-
return undefined;
|
|
1569
|
-
if (current.selfId !== target.selfId && !cfg.allowCrossSelf) {
|
|
1570
|
-
return { decision: 'deny', reason: '当前 OneBot 配置不允许跨机器人账号读取会话历史' };
|
|
1571
|
-
}
|
|
1572
|
-
if (current.detailType === 'group' && target.detailType === 'private' && !cfg.allowGroupReadPrivate) {
|
|
1573
|
-
return { decision: 'deny', reason: '当前 OneBot 配置不允许从群聊读取私聊历史' };
|
|
1574
|
-
}
|
|
1575
|
-
if (current.detailType === 'group' &&
|
|
1576
|
-
target.detailType === 'group' &&
|
|
1577
|
-
current.targetId !== target.targetId &&
|
|
1578
|
-
!cfg.allowCrossGroup) {
|
|
1579
|
-
return { decision: 'deny', reason: '当前 OneBot 配置不允许跨群读取其他群历史' };
|
|
1580
|
-
}
|
|
1581
|
-
if (current.detailType === 'private' &&
|
|
1582
|
-
target.detailType === 'private' &&
|
|
1583
|
-
current.targetId !== target.targetId &&
|
|
1584
|
-
!cfg.allowCrossPrivate) {
|
|
1585
|
-
return { decision: 'deny', reason: '当前 OneBot 配置不允许跨私聊读取其他私聊历史' };
|
|
1586
|
-
}
|
|
1587
|
-
return undefined;
|
|
1588
|
-
},
|
|
1589
|
-
};
|
|
1590
|
-
// 规则登记在 session-history 提供者实例的局部状态里:提供者重启或换人后,新实例身上没有这条规则,
|
|
1591
|
-
// 须跟随提供者重挂;返回的注销作为跟随清理,本插件 bounce / 卸载时一并撤回,不在下层泄漏。
|
|
1592
|
-
sessionHistory.follow(svc => {
|
|
1593
|
-
const dispose = svc.registerAccessChecker(checker);
|
|
1594
|
-
logger.info('OneBot 会话历史访问规则已注册到 session-history');
|
|
1595
|
-
return dispose;
|
|
1596
|
-
});
|
|
1537
|
+
logger.use.info('OneBot 特殊交互工具已注册');
|
|
1597
1538
|
}
|
|
1598
1539
|
function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
1599
1540
|
const { platform, logger, sessionHistory } = caps;
|
|
@@ -1613,7 +1554,7 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
|
1613
1554
|
description: 'group = 群聊,private = 私聊',
|
|
1614
1555
|
},
|
|
1615
1556
|
target_id: { type: 'string', description: '群号或 QQ 号' },
|
|
1616
|
-
self_id:
|
|
1557
|
+
self_id: SELF_ID_PARAM,
|
|
1617
1558
|
},
|
|
1618
1559
|
required: ['target_type', 'target_id'],
|
|
1619
1560
|
additionalProperties: false,
|
|
@@ -1621,26 +1562,14 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
|
1621
1562
|
},
|
|
1622
1563
|
},
|
|
1623
1564
|
handler: async (args, callCtx) => {
|
|
1624
|
-
const current = parseOneBotSession(callCtx.sessionId);
|
|
1625
1565
|
const targetType = String(args.target_type ?? '').toLowerCase();
|
|
1626
1566
|
const targetId = String(args.target_id ?? '').trim();
|
|
1627
1567
|
if (targetType !== 'group' && targetType !== 'private') {
|
|
1628
|
-
|
|
1568
|
+
throw new Error(`target_type 必须为 group 或 private,收到 ${args.target_type}`);
|
|
1629
1569
|
}
|
|
1630
1570
|
if (!targetId)
|
|
1631
|
-
|
|
1632
|
-
const
|
|
1633
|
-
const selfId = argSelfId || current?.selfId || getAnyOneBotSelfId(platform) || '';
|
|
1634
|
-
if (!selfId)
|
|
1635
|
-
return JSON.stringify({ error: '无法确定 self_id,且未找到可用的 OneBot 连接' });
|
|
1636
|
-
// 跨账号限制:仅在当前也是 OneBot 会话时才作为参考点检查
|
|
1637
|
-
if (current && !cfg.allowCrossSelf && selfId !== current.selfId) {
|
|
1638
|
-
return JSON.stringify({
|
|
1639
|
-
error: '当前配置不允许跨机器人账号解析会话',
|
|
1640
|
-
currentSelfId: current.selfId,
|
|
1641
|
-
requestedSelfId: selfId,
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1571
|
+
throw new Error('target_id 不能为空');
|
|
1572
|
+
const selfId = resolveSelfId(platform, callCtx, args);
|
|
1644
1573
|
return JSON.stringify({
|
|
1645
1574
|
ok: true,
|
|
1646
1575
|
sessionId: buildOneBotSessionId(selfId, targetType, targetId),
|
|
@@ -1658,7 +1587,7 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
|
1658
1587
|
description: [
|
|
1659
1588
|
'按 QQ 群号或 QQ 号读取对应 OneBot 会话最近若干条历史消息。',
|
|
1660
1589
|
'适用场景:用户明确要求你回忆某个群/私聊最近说过什么,或需要核实另一个 OneBot 会话的原文上下文。',
|
|
1661
|
-
'
|
|
1590
|
+
'能读哪些会话按会话管理的跨会话规则(默认不读别人的私聊);如需语义搜索历史,请优先使用 memory_recall。',
|
|
1662
1591
|
].join('\n'),
|
|
1663
1592
|
parameters: {
|
|
1664
1593
|
type: 'object',
|
|
@@ -1673,7 +1602,7 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
|
1673
1602
|
type: 'number',
|
|
1674
1603
|
description: `读取最近多少条,默认 ${cfg.defaultLimit},最多 ${cfg.maxLimit}`,
|
|
1675
1604
|
},
|
|
1676
|
-
self_id:
|
|
1605
|
+
self_id: SELF_ID_PARAM,
|
|
1677
1606
|
},
|
|
1678
1607
|
required: ['target_type', 'target_id'],
|
|
1679
1608
|
additionalProperties: false,
|
|
@@ -1681,57 +1610,63 @@ function registerSessionHistoryTools(caps, bundle, cfg) {
|
|
|
1681
1610
|
},
|
|
1682
1611
|
},
|
|
1683
1612
|
handler: async (args, callCtx) => {
|
|
1684
|
-
const current = parseOneBotSession(callCtx.sessionId);
|
|
1685
1613
|
const targetType = String(args.target_type ?? '').toLowerCase();
|
|
1686
1614
|
const targetId = String(args.target_id ?? '').trim();
|
|
1687
1615
|
if (targetType !== 'group' && targetType !== 'private') {
|
|
1688
|
-
|
|
1616
|
+
throw new Error(`target_type 必须为 group 或 private,收到 ${args.target_type}`);
|
|
1689
1617
|
}
|
|
1690
1618
|
if (!targetId)
|
|
1691
|
-
|
|
1692
|
-
const
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
const history = sessionHistory.current;
|
|
1697
|
-
if (!history)
|
|
1698
|
-
return JSON.stringify({
|
|
1699
|
-
error: 'session-history 服务不可用,请启用 @aalis/plugin-tool-session',
|
|
1700
|
-
});
|
|
1619
|
+
throw new Error('target_id 不能为空');
|
|
1620
|
+
const selfId = resolveSelfId(platform, callCtx, args);
|
|
1621
|
+
if (!sessionHistory.present)
|
|
1622
|
+
throw new Error('session-history 服务不可用,请启用 @aalis/plugin-tool-session');
|
|
1623
|
+
const history = sessionHistory.use;
|
|
1701
1624
|
const limit = Math.max(1, Math.min(cfg.maxLimit, Math.floor(Number(args.limit) || cfg.defaultLimit)));
|
|
1702
1625
|
const sessionId = buildOneBotSessionId(selfId, targetType, targetId);
|
|
1626
|
+
let result;
|
|
1703
1627
|
try {
|
|
1704
|
-
//
|
|
1705
|
-
// OneBot 的规则已通过 registerOneBotHistoryAccessChecker 注册。
|
|
1628
|
+
// 能不能读由 session-history 服务按会话管理的规则判(带这次调用的授权身份)。
|
|
1706
1629
|
// includeArchived 故意不暴露:archived 是 memory 内部细节,被 summary 替代的老消息
|
|
1707
1630
|
// 默认就不该再翻原文(会与 summary 内容双重计费)。如需考古应另起独立工具。
|
|
1708
|
-
|
|
1709
|
-
return JSON.stringify({
|
|
1710
|
-
...result,
|
|
1711
|
-
sessionId,
|
|
1712
|
-
selfId,
|
|
1713
|
-
targetType,
|
|
1714
|
-
targetId,
|
|
1715
|
-
});
|
|
1631
|
+
result = await history.getHistory({ sessionId, limit }, callCtx);
|
|
1716
1632
|
}
|
|
1717
1633
|
catch (err) {
|
|
1718
1634
|
const message = err instanceof Error ? err.message : String(err);
|
|
1719
|
-
logger.warn(`onebot_get_session_history 失败 (${sessionId}): ${message}`);
|
|
1720
|
-
|
|
1635
|
+
logger.use.warn(`onebot_get_session_history 失败 (${sessionId}): ${message}`);
|
|
1636
|
+
throw new Error(`读取 OneBot 会话历史失败: ${message}`);
|
|
1721
1637
|
}
|
|
1638
|
+
if ('error' in result)
|
|
1639
|
+
throw new Error(result.error);
|
|
1640
|
+
return JSON.stringify({
|
|
1641
|
+
...result,
|
|
1642
|
+
sessionId,
|
|
1643
|
+
selfId,
|
|
1644
|
+
targetType,
|
|
1645
|
+
targetId,
|
|
1646
|
+
});
|
|
1722
1647
|
},
|
|
1723
1648
|
});
|
|
1724
|
-
logger.info('OneBot 会话历史工具已注册');
|
|
1649
|
+
logger.use.info('OneBot 会话历史工具已注册');
|
|
1725
1650
|
}
|
|
1726
|
-
// ===== 请求处理工具(好友申请 / 群请求)=====
|
|
1727
1651
|
function registerRequestTools(caps, bundle) {
|
|
1728
1652
|
const { platform, logger } = caps;
|
|
1729
1653
|
const { group, personal } = bundle;
|
|
1730
|
-
/**
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1654
|
+
/**
|
|
1655
|
+
* 处理申请的账号与 OneBot 适配器:申请挂在收到它的实例上,按当前会话(或 self_id)的账号路由。
|
|
1656
|
+
* 申请的通知注入在收到它的账号的会话里,在那里调用即落到对的实例与账号
|
|
1657
|
+
*/
|
|
1658
|
+
async function requestHandler(callCtx, args) {
|
|
1659
|
+
const selfId = resolveSelfId(platform, callCtx, args);
|
|
1660
|
+
const adapter = (await resolvePlatformBySession(platform, selfSessionId(selfId)))?.use;
|
|
1661
|
+
if (typeof adapter?.handleFriendRequest !== 'function')
|
|
1662
|
+
throw new Error('未找到支持请求处理的 OneBot 适配器');
|
|
1663
|
+
return { selfId, adapter: adapter };
|
|
1734
1664
|
}
|
|
1665
|
+
/** 两个群请求工具各认自己的申请类型:用审批加群申请的工具批不了入群邀请,反之亦然 */
|
|
1666
|
+
const handleGroupRequest = (subType) => async (args, callCtx) => {
|
|
1667
|
+
const { selfId, adapter } = await requestHandler(callCtx, args);
|
|
1668
|
+
return adapter.handleGroupRequest(selfId, subType, String(args.user_id), String(args.group_id), !!args.approve, args.reason ? String(args.reason) : undefined);
|
|
1669
|
+
};
|
|
1735
1670
|
personal.register({
|
|
1736
1671
|
definition: {
|
|
1737
1672
|
type: 'function',
|
|
@@ -1744,22 +1679,21 @@ function registerRequestTools(caps, bundle) {
|
|
|
1744
1679
|
user_id: { type: 'string', description: '发起好友申请的用户 QQ 号' },
|
|
1745
1680
|
approve: { type: 'boolean', description: 'true = 同意,false = 拒绝' },
|
|
1746
1681
|
remark: { type: 'string', description: '同意后添加的备注(可选)' },
|
|
1682
|
+
self_id: SELF_ID_PARAM,
|
|
1747
1683
|
},
|
|
1748
1684
|
required: ['user_id', 'approve'],
|
|
1749
1685
|
},
|
|
1750
1686
|
},
|
|
1751
1687
|
},
|
|
1752
|
-
handler: async (args) => {
|
|
1753
|
-
const adapter =
|
|
1754
|
-
|
|
1755
|
-
return '未找到支持请求处理的 OneBot 适配器';
|
|
1756
|
-
return adapter.handleFriendRequest(String(args.user_id), !!args.approve, args.remark ? String(args.remark) : undefined);
|
|
1688
|
+
handler: async (args, callCtx) => {
|
|
1689
|
+
const { selfId, adapter } = await requestHandler(callCtx, args);
|
|
1690
|
+
return adapter.handleFriendRequest(selfId, String(args.user_id), !!args.approve, args.remark ? String(args.remark) : undefined);
|
|
1757
1691
|
},
|
|
1758
1692
|
});
|
|
1759
1693
|
// 群相关请求拆成两个工具,对应 OneBot v11 set_group_add_request 的两种 sub_type:
|
|
1760
1694
|
// - approve_join_request: sub_type=add 「别人申请加入 bot 管理的群」 → 群务
|
|
1761
1695
|
// - handle_group_invite: sub_type=invite 「别人邀请 bot 加入群」 → 个人/私聊
|
|
1762
|
-
//
|
|
1696
|
+
// 底层共用同一个适配器方法,各自把要处理的申请类型交给它,类型不符就报错。
|
|
1763
1697
|
group.register({
|
|
1764
1698
|
definition: {
|
|
1765
1699
|
type: 'function',
|
|
@@ -1773,17 +1707,13 @@ function registerRequestTools(caps, bundle) {
|
|
|
1773
1707
|
group_id: { type: 'string', description: '相关群号' },
|
|
1774
1708
|
approve: { type: 'boolean', description: 'true = 同意加群,false = 拒绝' },
|
|
1775
1709
|
reason: { type: 'string', description: '拒绝理由(拒绝时有效,可选)' },
|
|
1710
|
+
self_id: SELF_ID_PARAM,
|
|
1776
1711
|
},
|
|
1777
1712
|
required: ['user_id', 'group_id', 'approve'],
|
|
1778
1713
|
},
|
|
1779
1714
|
},
|
|
1780
1715
|
},
|
|
1781
|
-
handler:
|
|
1782
|
-
const adapter = findRequestAdapter(platform);
|
|
1783
|
-
if (!adapter)
|
|
1784
|
-
return '未找到支持请求处理的 OneBot 适配器';
|
|
1785
|
-
return adapter.handleGroupRequest(String(args.user_id), String(args.group_id), !!args.approve, args.reason ? String(args.reason) : undefined);
|
|
1786
|
-
},
|
|
1716
|
+
handler: handleGroupRequest('add'),
|
|
1787
1717
|
});
|
|
1788
1718
|
personal.register({
|
|
1789
1719
|
definition: {
|
|
@@ -1798,18 +1728,14 @@ function registerRequestTools(caps, bundle) {
|
|
|
1798
1728
|
group_id: { type: 'string', description: '被邀请加入的群号' },
|
|
1799
1729
|
approve: { type: 'boolean', description: 'true = 接受邀请并加入群,false = 拒绝' },
|
|
1800
1730
|
reason: { type: 'string', description: '拒绝理由(拒绝时有效,可选)' },
|
|
1731
|
+
self_id: SELF_ID_PARAM,
|
|
1801
1732
|
},
|
|
1802
1733
|
required: ['user_id', 'group_id', 'approve'],
|
|
1803
1734
|
},
|
|
1804
1735
|
},
|
|
1805
1736
|
},
|
|
1806
|
-
handler:
|
|
1807
|
-
const adapter = findRequestAdapter(platform);
|
|
1808
|
-
if (!adapter)
|
|
1809
|
-
return '未找到支持请求处理的 OneBot 适配器';
|
|
1810
|
-
return adapter.handleGroupRequest(String(args.user_id), String(args.group_id), !!args.approve, args.reason ? String(args.reason) : undefined);
|
|
1811
|
-
},
|
|
1737
|
+
handler: handleGroupRequest('invite'),
|
|
1812
1738
|
});
|
|
1813
|
-
logger.info('OneBot 请求处理工具已注册');
|
|
1739
|
+
logger.use.info('OneBot 请求处理工具已注册');
|
|
1814
1740
|
}
|
|
1815
1741
|
//# sourceMappingURL=index.js.map
|