@kirigaya/openclaw-onebot 1.0.2 → 1.0.4
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/LICENSE +21 -21
- package/README.md +174 -7
- package/dist/channel.js +2 -2
- package/dist/cli-commands.d.ts +4 -0
- package/dist/cli-commands.js +119 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +43 -0
- package/dist/connection.d.ts +63 -6
- package/dist/connection.js +263 -23
- package/dist/handlers/group-increase.d.ts +2 -1
- package/dist/handlers/group-increase.js +78 -12
- package/dist/handlers/process-inbound.d.ts +18 -0
- package/dist/handlers/process-inbound.js +420 -25
- package/dist/index.js +5 -1
- package/dist/load-script.d.ts +5 -1
- package/dist/load-script.js +7 -3
- package/dist/markdown-to-html.d.ts +10 -0
- package/dist/markdown-to-html.js +111 -0
- package/dist/markdown.d.ts +7 -0
- package/dist/markdown.js +43 -0
- package/dist/message.d.ts +6 -0
- package/dist/message.js +45 -5
- package/dist/og-image.d.ts +7 -0
- package/dist/og-image.js +51 -0
- package/dist/reply-context.d.ts +19 -0
- package/dist/reply-context.js +54 -0
- package/dist/send-debug-log.d.ts +27 -0
- package/dist/send-debug-log.js +28 -0
- package/dist/send.d.ts +16 -2
- package/dist/send.js +65 -8
- package/dist/setup.js +58 -5
- package/dist/tools.d.ts +3 -1
- package/dist/tools.js +60 -7
- package/openclaw.plugin.json +59 -4
- package/package.json +37 -12
- package/skills/onebot-ops/SKILL.md +14 -3
- package/skills/onebot-ops/agent-tools.md +116 -0
- package/skills/onebot-ops/config.md +71 -55
- package/skills/onebot-ops/receive.md +88 -12
- package/skills/onebot-ops/send.md +56 -39
- package/themes/dust.css +1096 -0
- package/dist/gateway-proxy.d.ts +0 -8
- package/dist/gateway-proxy.js +0 -36
package/dist/connection.js
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OneBot WebSocket 连接与 API 调用
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 图片消息:
|
|
5
|
+
* - 本机回环连接时:网络 URL 会先下载到本地再发送(兼容部分实现的 retcode 1200)
|
|
6
|
+
* - 跨机器连接时:本地文件会自动转成 base64://,避免把宿主机绝对路径发给远端 OneBot
|
|
5
7
|
* 并定期清理临时文件。
|
|
6
8
|
*/
|
|
9
|
+
import Fuse from "fuse.js";
|
|
7
10
|
import WebSocket from "ws";
|
|
8
11
|
import { createServer } from "http";
|
|
9
12
|
import https from "https";
|
|
10
13
|
import http from "http";
|
|
11
|
-
import { writeFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from "fs";
|
|
14
|
+
import { writeFileSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync } from "fs";
|
|
12
15
|
import { join } from "path";
|
|
13
16
|
import { tmpdir } from "os";
|
|
17
|
+
import { logSend } from "./send-debug-log.js";
|
|
18
|
+
import { shouldBlockSendInForwardMode, getActiveReplyTarget, getActiveReplySessionId } from "./reply-context.js";
|
|
14
19
|
const IMAGE_TEMP_DIR = join(tmpdir(), "openclaw-onebot");
|
|
15
20
|
const DOWNLOAD_TIMEOUT_MS = 30000;
|
|
16
21
|
/** 使用 Node 内置 http(s) 下载 URL,避免 fetch 在某些环境下的兼容性问题 */
|
|
@@ -94,6 +99,48 @@ async function resolveImageToLocalPath(image) {
|
|
|
94
99
|
}
|
|
95
100
|
return trimmed.replace(/\\/g, "/");
|
|
96
101
|
}
|
|
102
|
+
async function resolveImageToBuffer(image) {
|
|
103
|
+
const trimmed = image?.trim();
|
|
104
|
+
if (!trimmed)
|
|
105
|
+
throw new Error("Empty image");
|
|
106
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
107
|
+
return downloadUrl(trimmed);
|
|
108
|
+
}
|
|
109
|
+
if (trimmed.startsWith("base64://")) {
|
|
110
|
+
return Buffer.from(trimmed.slice(9), "base64");
|
|
111
|
+
}
|
|
112
|
+
if (trimmed.startsWith("file://")) {
|
|
113
|
+
return readFileSync(trimmed.slice(7));
|
|
114
|
+
}
|
|
115
|
+
return readFileSync(trimmed);
|
|
116
|
+
}
|
|
117
|
+
function normalizePeerHost(host) {
|
|
118
|
+
const trimmed = String(host ?? "").trim().toLowerCase();
|
|
119
|
+
if (!trimmed)
|
|
120
|
+
return "";
|
|
121
|
+
const unwrapped = trimmed.replace(/^\[/, "").replace(/\]$/, "");
|
|
122
|
+
return unwrapped.startsWith("::ffff:") ? unwrapped.slice(7) : unwrapped;
|
|
123
|
+
}
|
|
124
|
+
function isLoopbackHost(host) {
|
|
125
|
+
const normalized = normalizePeerHost(host);
|
|
126
|
+
return normalized === "127.0.0.1" || normalized === "::1" || normalized === "localhost";
|
|
127
|
+
}
|
|
128
|
+
function getSocketPeerHost(socket, getConfig) {
|
|
129
|
+
const peerHost = socket.__onebotPeerHost;
|
|
130
|
+
if (peerHost)
|
|
131
|
+
return peerHost;
|
|
132
|
+
return getConfig?.()?.host ?? "";
|
|
133
|
+
}
|
|
134
|
+
function shouldEncodeImageAsBase64(socket, getConfig) {
|
|
135
|
+
const peerHost = getSocketPeerHost(socket, getConfig);
|
|
136
|
+
return !!peerHost && !isLoopbackHost(peerHost);
|
|
137
|
+
}
|
|
138
|
+
async function resolveImageFileForSend(image, socket, getConfig) {
|
|
139
|
+
if (shouldEncodeImageAsBase64(socket, getConfig)) {
|
|
140
|
+
return `base64://${(await resolveImageToBuffer(image)).toString("base64")}`;
|
|
141
|
+
}
|
|
142
|
+
return resolveImageToLocalPath(image);
|
|
143
|
+
}
|
|
97
144
|
/** 启动临时图片定期清理(每小时执行一次) */
|
|
98
145
|
export function startImageTempCleanup() {
|
|
99
146
|
stopImageTempCleanup();
|
|
@@ -206,6 +253,18 @@ export async function ensureConnection(getConfig, timeoutMs = 30000) {
|
|
|
206
253
|
return waitForConnection(timeoutMs);
|
|
207
254
|
}
|
|
208
255
|
export async function sendPrivateMsg(userId, text, getConfig) {
|
|
256
|
+
if (shouldBlockSendInForwardMode("private", userId)) {
|
|
257
|
+
logSend("connection", "sendPrivateMsg", { targetId: userId, blocked: true, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
logSend("connection", "sendPrivateMsg", {
|
|
261
|
+
targetType: "user",
|
|
262
|
+
targetId: userId,
|
|
263
|
+
textPreview: text?.slice(0, 80),
|
|
264
|
+
textLen: text?.length,
|
|
265
|
+
sessionId: getActiveReplyTarget(),
|
|
266
|
+
replySessionId: getActiveReplySessionId(),
|
|
267
|
+
});
|
|
209
268
|
const socket = getConfig
|
|
210
269
|
? await ensureConnection(getConfig)
|
|
211
270
|
: await waitForConnection();
|
|
@@ -213,9 +272,23 @@ export async function sendPrivateMsg(userId, text, getConfig) {
|
|
|
213
272
|
if (res?.retcode !== 0) {
|
|
214
273
|
throw new Error(res?.msg ?? `OneBot send_private_msg failed (retcode=${res?.retcode})`);
|
|
215
274
|
}
|
|
216
|
-
|
|
275
|
+
const mid = res?.data?.message_id;
|
|
276
|
+
logSend("connection", "sendPrivateMsg", { targetId: userId, messageId: mid, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
277
|
+
return mid;
|
|
217
278
|
}
|
|
218
279
|
export async function sendGroupMsg(groupId, text, getConfig) {
|
|
280
|
+
if (shouldBlockSendInForwardMode("group", groupId)) {
|
|
281
|
+
logSend("connection", "sendGroupMsg", { targetId: groupId, blocked: true, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
logSend("connection", "sendGroupMsg", {
|
|
285
|
+
targetType: "group",
|
|
286
|
+
targetId: groupId,
|
|
287
|
+
textPreview: text?.slice(0, 80),
|
|
288
|
+
textLen: text?.length,
|
|
289
|
+
sessionId: getActiveReplyTarget(),
|
|
290
|
+
replySessionId: getActiveReplySessionId(),
|
|
291
|
+
});
|
|
219
292
|
const socket = getConfig
|
|
220
293
|
? await ensureConnection(getConfig)
|
|
221
294
|
: await waitForConnection();
|
|
@@ -223,14 +296,27 @@ export async function sendGroupMsg(groupId, text, getConfig) {
|
|
|
223
296
|
if (res?.retcode !== 0) {
|
|
224
297
|
throw new Error(res?.msg ?? `OneBot send_group_msg failed (retcode=${res?.retcode})`);
|
|
225
298
|
}
|
|
226
|
-
|
|
299
|
+
const mid = res?.data?.message_id;
|
|
300
|
+
logSend("connection", "sendGroupMsg", { targetId: groupId, messageId: mid, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
301
|
+
return mid;
|
|
227
302
|
}
|
|
228
303
|
export async function sendGroupImage(groupId, image, log = getLogger(), getConfig) {
|
|
304
|
+
if (shouldBlockSendInForwardMode("group", groupId)) {
|
|
305
|
+
logSend("connection", "sendGroupImage", { targetId: groupId, blocked: true, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
logSend("connection", "sendGroupImage", {
|
|
309
|
+
targetType: "group",
|
|
310
|
+
targetId: groupId,
|
|
311
|
+
imagePreview: image?.slice?.(0, 60),
|
|
312
|
+
sessionId: getActiveReplyTarget(),
|
|
313
|
+
replySessionId: getActiveReplySessionId(),
|
|
314
|
+
});
|
|
229
315
|
log.info?.(`[onebot] sendGroupImage entry: groupId=${groupId} image=${image?.slice?.(0, 80) ?? ""}`);
|
|
230
316
|
const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
|
|
231
317
|
log.info?.(`222[onebot] sendGroupImage entry: groupId=${groupId} image=${image?.slice?.(0, 80) ?? ""}`);
|
|
232
318
|
try {
|
|
233
|
-
const filePath = image.startsWith("[") ? null : await
|
|
319
|
+
const filePath = image.startsWith("[") ? null : await resolveImageFileForSend(image, socket, getConfig);
|
|
234
320
|
const seg = image.startsWith("[")
|
|
235
321
|
? JSON.parse(image)
|
|
236
322
|
: [{ type: "image", data: { file: filePath } }];
|
|
@@ -240,16 +326,61 @@ export async function sendGroupImage(groupId, image, log = getLogger(), getConfi
|
|
|
240
326
|
throw new Error(res?.msg ?? `OneBot send_group_msg (image) failed (retcode=${res?.retcode})`);
|
|
241
327
|
}
|
|
242
328
|
log.info?.(`[onebot] sendGroupImage done: retcode=${res?.retcode ?? "?"}`);
|
|
243
|
-
|
|
329
|
+
const mid = res?.data?.message_id;
|
|
330
|
+
logSend("connection", "sendGroupImage", { targetId: groupId, messageId: mid, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
331
|
+
return mid;
|
|
244
332
|
}
|
|
245
333
|
catch (error) {
|
|
246
334
|
log.warn?.(`[onebot] sendGroupImage error: ${error}`);
|
|
247
335
|
}
|
|
248
336
|
}
|
|
337
|
+
/** 发送群合并转发消息。messages 为节点数组,每节点 { type: "node", data: { id } } 或 { type: "node", data: { user_id, nickname, content } } */
|
|
338
|
+
export async function sendGroupForwardMsg(groupId, messages, getConfig) {
|
|
339
|
+
logSend("connection", "sendGroupForwardMsg", {
|
|
340
|
+
targetType: "group",
|
|
341
|
+
targetId: groupId,
|
|
342
|
+
nodeCount: messages.length,
|
|
343
|
+
isForward: true,
|
|
344
|
+
sessionId: getActiveReplyTarget(),
|
|
345
|
+
replySessionId: getActiveReplySessionId(),
|
|
346
|
+
});
|
|
347
|
+
const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
|
|
348
|
+
const res = await sendOneBotAction(socket, "send_group_forward_msg", { group_id: groupId, messages });
|
|
349
|
+
if (res?.retcode !== 0) {
|
|
350
|
+
throw new Error(res?.msg ?? `OneBot send_group_forward_msg failed (retcode=${res?.retcode})`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/** 发送私聊合并转发消息 */
|
|
354
|
+
export async function sendPrivateForwardMsg(userId, messages, getConfig) {
|
|
355
|
+
logSend("connection", "sendPrivateForwardMsg", {
|
|
356
|
+
targetType: "user",
|
|
357
|
+
targetId: userId,
|
|
358
|
+
nodeCount: messages.length,
|
|
359
|
+
isForward: true,
|
|
360
|
+
sessionId: getActiveReplyTarget(),
|
|
361
|
+
replySessionId: getActiveReplySessionId(),
|
|
362
|
+
});
|
|
363
|
+
const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
|
|
364
|
+
const res = await sendOneBotAction(socket, "send_private_forward_msg", { user_id: userId, messages });
|
|
365
|
+
if (res?.retcode !== 0) {
|
|
366
|
+
throw new Error(res?.msg ?? `OneBot send_private_forward_msg failed (retcode=${res?.retcode})`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
249
369
|
export async function sendPrivateImage(userId, image, log = getLogger(), getConfig) {
|
|
370
|
+
if (shouldBlockSendInForwardMode("private", userId)) {
|
|
371
|
+
logSend("connection", "sendPrivateImage", { targetId: userId, blocked: true, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
372
|
+
return undefined;
|
|
373
|
+
}
|
|
374
|
+
logSend("connection", "sendPrivateImage", {
|
|
375
|
+
targetType: "user",
|
|
376
|
+
targetId: userId,
|
|
377
|
+
imagePreview: image?.slice?.(0, 60),
|
|
378
|
+
sessionId: getActiveReplyTarget(),
|
|
379
|
+
replySessionId: getActiveReplySessionId(),
|
|
380
|
+
});
|
|
250
381
|
log.info?.(`[onebot] sendPrivateImage entry: userId=${userId} image=${image?.slice?.(0, 80) ?? ""}`);
|
|
251
382
|
const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
|
|
252
|
-
const filePath = image.startsWith("[") ? null : await
|
|
383
|
+
const filePath = image.startsWith("[") ? null : await resolveImageFileForSend(image, socket, getConfig);
|
|
253
384
|
const seg = image.startsWith("[")
|
|
254
385
|
? JSON.parse(image)
|
|
255
386
|
: [{ type: "image", data: { file: filePath } }];
|
|
@@ -258,17 +389,27 @@ export async function sendPrivateImage(userId, image, log = getLogger(), getConf
|
|
|
258
389
|
throw new Error(res?.msg ?? `OneBot send_private_msg (image) failed (retcode=${res?.retcode})`);
|
|
259
390
|
}
|
|
260
391
|
log.info?.(`[onebot] sendPrivateImage done: retcode=${res?.retcode ?? "?"}`);
|
|
261
|
-
|
|
392
|
+
const mid = res?.data?.message_id;
|
|
393
|
+
logSend("connection", "sendPrivateImage", { targetId: userId, messageId: mid, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
394
|
+
return mid;
|
|
262
395
|
}
|
|
263
|
-
export async function uploadGroupFile(groupId, file, name) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
396
|
+
export async function uploadGroupFile(groupId, file, name, getConfig) {
|
|
397
|
+
const socket = getConfig
|
|
398
|
+
? await ensureConnection(getConfig)
|
|
399
|
+
: await waitForConnection();
|
|
400
|
+
const res = await sendOneBotAction(socket, "upload_group_file", { group_id: groupId, file, name });
|
|
401
|
+
if (res?.retcode !== 0) {
|
|
402
|
+
throw new Error(res?.msg ?? `OneBot upload_group_file failed (retcode=${res?.retcode})`);
|
|
403
|
+
}
|
|
267
404
|
}
|
|
268
|
-
export async function uploadPrivateFile(userId, file, name) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
405
|
+
export async function uploadPrivateFile(userId, file, name, getConfig) {
|
|
406
|
+
const socket = getConfig
|
|
407
|
+
? await ensureConnection(getConfig)
|
|
408
|
+
: await waitForConnection();
|
|
409
|
+
const res = await sendOneBotAction(socket, "upload_private_file", { user_id: userId, file, name });
|
|
410
|
+
if (res?.retcode !== 0) {
|
|
411
|
+
throw new Error(res?.msg ?? `OneBot upload_private_file failed (retcode=${res?.retcode})`);
|
|
412
|
+
}
|
|
272
413
|
}
|
|
273
414
|
/** 撤回消息 */
|
|
274
415
|
export async function deleteMsg(messageId) {
|
|
@@ -316,6 +457,49 @@ export async function getGroupMemberInfo(groupId, userId) {
|
|
|
316
457
|
return null;
|
|
317
458
|
}
|
|
318
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* 获取群成员列表(OneBot get_group_member_list)
|
|
462
|
+
*/
|
|
463
|
+
export async function getGroupMemberList(groupId) {
|
|
464
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
465
|
+
return [];
|
|
466
|
+
try {
|
|
467
|
+
const res = await sendOneBotAction(ws, "get_group_member_list", { group_id: groupId });
|
|
468
|
+
if (res?.retcode !== 0 || !Array.isArray(res?.data))
|
|
469
|
+
return [];
|
|
470
|
+
return res.data.map((m) => ({
|
|
471
|
+
user_id: Number(m.user_id),
|
|
472
|
+
nickname: String(m.nickname ?? ""),
|
|
473
|
+
card: String(m.card ?? ""),
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* 按名字模糊匹配群成员(匹配群名片 card 与昵称 nickname),返回匹配到的 QQ 与展示名。
|
|
482
|
+
* 使用 Fuse.js 模糊匹配,结果按相关度排序。
|
|
483
|
+
*/
|
|
484
|
+
export async function searchGroupMemberByName(groupId, name) {
|
|
485
|
+
const list = await getGroupMemberList(groupId);
|
|
486
|
+
const keyword = (name || "").trim();
|
|
487
|
+
if (!keyword)
|
|
488
|
+
return [];
|
|
489
|
+
const fuse = new Fuse(list, {
|
|
490
|
+
keys: ["card", "nickname"],
|
|
491
|
+
includeScore: true,
|
|
492
|
+
threshold: 0.4,
|
|
493
|
+
ignoreLocation: true,
|
|
494
|
+
});
|
|
495
|
+
const results = fuse.search(keyword);
|
|
496
|
+
return results.map(({ item: m }) => ({
|
|
497
|
+
user_id: m.user_id,
|
|
498
|
+
nickname: m.nickname,
|
|
499
|
+
card: m.card,
|
|
500
|
+
displayName: m.card || m.nickname || String(m.user_id),
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
319
503
|
/** 获取群信息(含 group_name) */
|
|
320
504
|
export async function getGroupInfo(groupId) {
|
|
321
505
|
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
@@ -349,20 +533,24 @@ export async function getMsg(messageId) {
|
|
|
349
533
|
}
|
|
350
534
|
}
|
|
351
535
|
/**
|
|
352
|
-
* 获取群聊历史消息(Lagrange.Core 扩展 API
|
|
536
|
+
* 获取群聊历史消息(Lagrange.Core 扩展 API,与 Lagrange.onebot context 一致)
|
|
537
|
+
* 仅使用 message_seq 分页(不传 message_id),与 Tiphareth getLast24HGroupMessages 调用方式一致。
|
|
353
538
|
* @param groupId 群号
|
|
354
|
-
* @param opts message_seq
|
|
539
|
+
* @param opts message_seq 起始序号(不传表示从最新一页);count 本页条数;reverse_order true 表示从旧到新,便于用 batch[0].message_seq 向前翻页
|
|
355
540
|
*/
|
|
356
541
|
export async function getGroupMsgHistory(groupId, opts = { count: 20 }) {
|
|
357
542
|
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
358
543
|
return [];
|
|
359
544
|
try {
|
|
360
|
-
const
|
|
545
|
+
const params = {
|
|
361
546
|
group_id: groupId,
|
|
362
|
-
message_seq: opts.message_seq,
|
|
363
|
-
message_id: opts.message_id,
|
|
364
547
|
count: opts.count ?? 20,
|
|
365
|
-
|
|
548
|
+
reverse_order: opts.reverse_order !== false,
|
|
549
|
+
};
|
|
550
|
+
if (opts.message_seq != null && Number.isFinite(opts.message_seq)) {
|
|
551
|
+
params.message_seq = opts.message_seq;
|
|
552
|
+
}
|
|
553
|
+
const res = await sendOneBotAction(ws, "get_group_msg_history", params);
|
|
366
554
|
if (res?.retcode === 0 && res?.data?.messages)
|
|
367
555
|
return res.data.messages;
|
|
368
556
|
return [];
|
|
@@ -371,6 +559,56 @@ export async function getGroupMsgHistory(groupId, opts = { count: 20 }) {
|
|
|
371
559
|
return [];
|
|
372
560
|
}
|
|
373
561
|
}
|
|
562
|
+
/** 单页请求之间的延迟(毫秒),与 Tiphareth historyMessages 一致 */
|
|
563
|
+
const HISTORY_PAGE_DELAY_MS = 500;
|
|
564
|
+
/**
|
|
565
|
+
* 按时间范围分页获取群历史消息,严格对齐 Tiphareth getLast24HGroupMessages 算法:
|
|
566
|
+
* getGroupMsgHistory(groupId, messageSeq, chunkSize, true),用 batch[0] 的 message_seq 向前翻页,去重与时间截断。
|
|
567
|
+
* @param groupId 群号
|
|
568
|
+
* @param opts startTime 仅保留 >= startTime 的消息(Unix 秒);limit 最多条数;chunkSize 每页条数
|
|
569
|
+
*/
|
|
570
|
+
export async function getGroupMsgHistoryInRange(groupId, opts = {}) {
|
|
571
|
+
const { startTime = 0, limit = 3000, chunkSize = 100 } = opts;
|
|
572
|
+
let messageSeq = undefined;
|
|
573
|
+
const allMessages = [];
|
|
574
|
+
const seenMessageIds = new Set();
|
|
575
|
+
let stopLoop = false;
|
|
576
|
+
let pageCount = 0;
|
|
577
|
+
while (!stopLoop) {
|
|
578
|
+
pageCount++;
|
|
579
|
+
const batch = await getGroupMsgHistory(groupId, {
|
|
580
|
+
message_seq: messageSeq,
|
|
581
|
+
count: chunkSize,
|
|
582
|
+
reverse_order: true,
|
|
583
|
+
});
|
|
584
|
+
if (!batch.length) {
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
await new Promise((r) => setTimeout(r, HISTORY_PAGE_DELAY_MS));
|
|
588
|
+
for (const msg of batch) {
|
|
589
|
+
if (seenMessageIds.has(msg.message_id))
|
|
590
|
+
continue;
|
|
591
|
+
seenMessageIds.add(msg.message_id);
|
|
592
|
+
if (msg.time < startTime) {
|
|
593
|
+
stopLoop = true;
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
allMessages.push(msg);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const oldest = batch[0];
|
|
600
|
+
const nextSeq = oldest.message_seq ?? oldest.message_id;
|
|
601
|
+
if (nextSeq == null || nextSeq === messageSeq) {
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
messageSeq = nextSeq;
|
|
605
|
+
if (allMessages.length >= limit) {
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
allMessages.sort((a, b) => a.time - b.time);
|
|
610
|
+
return allMessages;
|
|
611
|
+
}
|
|
374
612
|
export async function connectForward(config) {
|
|
375
613
|
const path = config.path ?? "/onebot/v11/ws";
|
|
376
614
|
const pathNorm = path.startsWith("/") ? path : `/${path}`;
|
|
@@ -384,6 +622,7 @@ export async function connectForward(config) {
|
|
|
384
622
|
w.on("open", () => resolve());
|
|
385
623
|
w.on("error", reject);
|
|
386
624
|
});
|
|
625
|
+
w.__onebotPeerHost = config.host;
|
|
387
626
|
return w;
|
|
388
627
|
}
|
|
389
628
|
export async function createServerAndWait(config) {
|
|
@@ -398,7 +637,8 @@ export async function createServerAndWait(config) {
|
|
|
398
637
|
server.listen(config.port, host);
|
|
399
638
|
wsServer = wss;
|
|
400
639
|
return new Promise((resolve) => {
|
|
401
|
-
wss.on("connection", (socket) => {
|
|
640
|
+
wss.on("connection", (socket, req) => {
|
|
641
|
+
socket.__onebotPeerHost = req.socket.remoteAddress ?? undefined;
|
|
402
642
|
resolve(socket);
|
|
403
643
|
});
|
|
404
644
|
});
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 支持:
|
|
5
5
|
* 1. 简单文本模板(message),占位符:{name}、{userId}、{groupName}、{groupId}、{avatarUrl}
|
|
6
|
-
* 2. 自定义
|
|
6
|
+
* 2. 自定义 command:在 cwd 下用系统 shell 执行命令,通过环境变量传入上下文
|
|
7
|
+
* 命令自行负责发送(如调用 openclaw message send),或向 stdout 输出 JSON 行供本 handler 发送
|
|
7
8
|
*/
|
|
8
9
|
import type { OneBotMessage } from "../types.js";
|
|
9
10
|
export interface GroupIncreaseContext {
|
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 支持:
|
|
5
5
|
* 1. 简单文本模板(message),占位符:{name}、{userId}、{groupName}、{groupId}、{avatarUrl}
|
|
6
|
-
* 2. 自定义
|
|
6
|
+
* 2. 自定义 command:在 cwd 下用系统 shell 执行命令,通过环境变量传入上下文
|
|
7
|
+
* 命令自行负责发送(如调用 openclaw message send),或向 stdout 输出 JSON 行供本 handler 发送
|
|
7
8
|
*/
|
|
8
9
|
import { sendGroupMsg, sendGroupImage, getStrangerInfo, getGroupMemberInfo, getGroupInfo, getAvatarUrl, } from "../connection.js";
|
|
9
|
-
import {
|
|
10
|
+
import { getRenderMarkdownToPlain } from "../config.js";
|
|
11
|
+
import { markdownToPlain } from "../markdown.js";
|
|
10
12
|
import { resolve } from "path";
|
|
13
|
+
import { spawn } from "child_process";
|
|
11
14
|
async function resolveContext(groupId, userId) {
|
|
12
15
|
const [groupInfo, memberInfo] = await Promise.all([
|
|
13
16
|
getGroupInfo(groupId),
|
|
@@ -38,6 +41,49 @@ function applyTemplate(template, ctx) {
|
|
|
38
41
|
.replace(/\{groupId\}/g, String(ctx.groupId))
|
|
39
42
|
.replace(/\{avatarUrl\}/g, ctx.avatarUrl);
|
|
40
43
|
}
|
|
44
|
+
function escapeForShell(s) {
|
|
45
|
+
const str = String(s);
|
|
46
|
+
if (process.platform === "win32") {
|
|
47
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
48
|
+
}
|
|
49
|
+
return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"';
|
|
50
|
+
}
|
|
51
|
+
function runCommand(command, cwd, args, env) {
|
|
52
|
+
const fullCmd = `${command} --userId ${args.userId} --username ${escapeForShell(args.username)} --groupId ${args.groupId}`;
|
|
53
|
+
return new Promise((resolvePromise) => {
|
|
54
|
+
const isWin = process.platform === "win32";
|
|
55
|
+
const shell = isWin ? "cmd.exe" : "sh";
|
|
56
|
+
const shellArg = isWin ? "/c" : "-c";
|
|
57
|
+
const child = spawn(shell, [shellArg, fullCmd], {
|
|
58
|
+
cwd,
|
|
59
|
+
env: { ...process.env, ...env },
|
|
60
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
61
|
+
});
|
|
62
|
+
let stdout = "";
|
|
63
|
+
let stderr = "";
|
|
64
|
+
child.stdout?.on("data", (d) => { stdout += d.toString(); });
|
|
65
|
+
child.stderr?.on("data", (d) => { stderr += d.toString(); });
|
|
66
|
+
child.on("close", (code) => {
|
|
67
|
+
resolvePromise({ stdout, stderr, code });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function parseCommandOutput(stdout) {
|
|
72
|
+
const line = stdout.trim().split("\n").pop();
|
|
73
|
+
if (!line)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const data = JSON.parse(line);
|
|
77
|
+
return {
|
|
78
|
+
text: typeof data.text === "string" ? data.text : undefined,
|
|
79
|
+
imagePath: typeof data.imagePath === "string" ? data.imagePath : undefined,
|
|
80
|
+
imageUrl: typeof data.imageUrl === "string" ? data.imageUrl : undefined,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
41
87
|
export async function handleGroupIncrease(api, msg) {
|
|
42
88
|
const cfg = api.config;
|
|
43
89
|
const gi = cfg?.channels?.onebot?.groupIncrease;
|
|
@@ -54,24 +100,43 @@ export async function handleGroupIncrease(api, msg) {
|
|
|
54
100
|
return;
|
|
55
101
|
}
|
|
56
102
|
let result = {};
|
|
57
|
-
const
|
|
58
|
-
|
|
103
|
+
const command = gi?.command?.trim();
|
|
104
|
+
const cwd = gi?.cwd?.trim();
|
|
105
|
+
if (command && cwd) {
|
|
106
|
+
const env = {
|
|
107
|
+
GROUP_ID: String(ctx.groupId),
|
|
108
|
+
GROUP_NAME: ctx.groupName,
|
|
109
|
+
USER_ID: String(ctx.userId),
|
|
110
|
+
USER_NAME: ctx.userName,
|
|
111
|
+
AVATAR_URL: ctx.avatarUrl,
|
|
112
|
+
};
|
|
113
|
+
const args = {
|
|
114
|
+
userId: String(ctx.userId),
|
|
115
|
+
username: ctx.userName,
|
|
116
|
+
groupId: String(ctx.groupId),
|
|
117
|
+
};
|
|
59
118
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
119
|
+
const { stdout, stderr, code } = await runCommand(command, resolve(cwd), args, env);
|
|
120
|
+
if (stderr)
|
|
121
|
+
api.logger?.warn?.(`[onebot] groupIncrease command stderr: ${stderr}`);
|
|
122
|
+
if (code !== 0)
|
|
123
|
+
api.logger?.warn?.(`[onebot] groupIncrease command exit code: ${code}`);
|
|
124
|
+
const parsed = parseCommandOutput(stdout);
|
|
125
|
+
if (parsed && (parsed.text || parsed.imagePath || parsed.imageUrl)) {
|
|
126
|
+
result = parsed;
|
|
64
127
|
}
|
|
65
128
|
}
|
|
66
129
|
catch (e) {
|
|
67
|
-
api.logger?.error?.(`[onebot] groupIncrease
|
|
130
|
+
api.logger?.error?.(`[onebot] groupIncrease command failed: ${e?.message}`);
|
|
68
131
|
}
|
|
69
132
|
}
|
|
70
133
|
const message = gi?.message;
|
|
71
|
-
if (message?.trim() && !result.text) {
|
|
134
|
+
if (message?.trim() && !result.text && !command) {
|
|
72
135
|
result.text = applyTemplate(message, ctx);
|
|
73
136
|
}
|
|
74
|
-
|
|
137
|
+
let text = (result.text ?? "").trim();
|
|
138
|
+
if (text && getRenderMarkdownToPlain(cfg))
|
|
139
|
+
text = markdownToPlain(text);
|
|
75
140
|
const imagePath = result.imagePath?.trim();
|
|
76
141
|
const imageUrl = result.imageUrl?.trim();
|
|
77
142
|
if (!text && !imagePath && !imageUrl)
|
|
@@ -80,9 +145,10 @@ export async function handleGroupIncrease(api, msg) {
|
|
|
80
145
|
if (text)
|
|
81
146
|
await sendGroupMsg(groupId, text);
|
|
82
147
|
if (imagePath) {
|
|
148
|
+
const baseDir = cwd || process.cwd();
|
|
83
149
|
const abs = imagePath.startsWith("file://") || imagePath.startsWith("http://") || imagePath.startsWith("https://")
|
|
84
150
|
? imagePath
|
|
85
|
-
: resolve(
|
|
151
|
+
: resolve(baseDir, imagePath);
|
|
86
152
|
await sendGroupImage(groupId, abs);
|
|
87
153
|
}
|
|
88
154
|
if (imageUrl && !imagePath)
|
|
@@ -8,4 +8,22 @@ export declare const sessionHistories: Map<string, {
|
|
|
8
8
|
timestamp: number;
|
|
9
9
|
messageId: string;
|
|
10
10
|
}[]>;
|
|
11
|
+
export declare function startForwardCleanupTimer(): void;
|
|
11
12
|
export declare function processInboundMessage(api: any, msg: OneBotMessage): Promise<void>;
|
|
13
|
+
/** 回复会话上下文,供 onReplySessionEnd 钩子使用 */
|
|
14
|
+
export interface ReplySessionContext {
|
|
15
|
+
/** 本次回复会话的唯一 ID,同一用户问题下的多次 deliver 共享此 ID */
|
|
16
|
+
replySessionId: string;
|
|
17
|
+
/** 会话标识,如 onebot:group:123 或 onebot:456 */
|
|
18
|
+
sessionId: string;
|
|
19
|
+
/** 回复目标,如 onebot:group:123 或 onebot:456 */
|
|
20
|
+
to: string;
|
|
21
|
+
/** 本次回复中已发送的所有块(按顺序) */
|
|
22
|
+
chunks: Array<{
|
|
23
|
+
index: number;
|
|
24
|
+
text?: string;
|
|
25
|
+
mediaUrl?: string;
|
|
26
|
+
}>;
|
|
27
|
+
/** 用户原始消息 */
|
|
28
|
+
userMessage: string;
|
|
29
|
+
}
|