@kirigaya/openclaw-onebot 1.0.3 → 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 +95 -3
- package/dist/cli-commands.d.ts +4 -0
- package/dist/cli-commands.js +119 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +26 -0
- package/dist/connection.d.ts +53 -6
- package/dist/connection.js +171 -19
- package/dist/handlers/process-inbound.js +187 -13
- package/dist/index.js +3 -1
- package/dist/markdown-to-html.d.ts +5 -1
- package/dist/markdown-to-html.js +76 -40
- package/dist/og-image.d.ts +3 -1
- package/dist/og-image.js +10 -4
- package/dist/setup.js +22 -0
- package/dist/tools.d.ts +3 -1
- package/dist/tools.js +54 -5
- package/openclaw.plugin.json +26 -1
- package/package.json +31 -13
- package/skills/onebot-ops/SKILL.md +14 -3
- package/skills/onebot-ops/agent-tools.md +116 -0
- 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,14 +1,17 @@
|
|
|
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";
|
|
14
17
|
import { logSend } from "./send-debug-log.js";
|
|
@@ -96,6 +99,48 @@ async function resolveImageToLocalPath(image) {
|
|
|
96
99
|
}
|
|
97
100
|
return trimmed.replace(/\\/g, "/");
|
|
98
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
|
+
}
|
|
99
144
|
/** 启动临时图片定期清理(每小时执行一次) */
|
|
100
145
|
export function startImageTempCleanup() {
|
|
101
146
|
stopImageTempCleanup();
|
|
@@ -271,7 +316,7 @@ export async function sendGroupImage(groupId, image, log = getLogger(), getConfi
|
|
|
271
316
|
const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
|
|
272
317
|
log.info?.(`222[onebot] sendGroupImage entry: groupId=${groupId} image=${image?.slice?.(0, 80) ?? ""}`);
|
|
273
318
|
try {
|
|
274
|
-
const filePath = image.startsWith("[") ? null : await
|
|
319
|
+
const filePath = image.startsWith("[") ? null : await resolveImageFileForSend(image, socket, getConfig);
|
|
275
320
|
const seg = image.startsWith("[")
|
|
276
321
|
? JSON.parse(image)
|
|
277
322
|
: [{ type: "image", data: { file: filePath } }];
|
|
@@ -335,7 +380,7 @@ export async function sendPrivateImage(userId, image, log = getLogger(), getConf
|
|
|
335
380
|
});
|
|
336
381
|
log.info?.(`[onebot] sendPrivateImage entry: userId=${userId} image=${image?.slice?.(0, 80) ?? ""}`);
|
|
337
382
|
const socket = getConfig ? await ensureConnection(getConfig) : await waitForConnection();
|
|
338
|
-
const filePath = image.startsWith("[") ? null : await
|
|
383
|
+
const filePath = image.startsWith("[") ? null : await resolveImageFileForSend(image, socket, getConfig);
|
|
339
384
|
const seg = image.startsWith("[")
|
|
340
385
|
? JSON.parse(image)
|
|
341
386
|
: [{ type: "image", data: { file: filePath } }];
|
|
@@ -348,15 +393,23 @@ export async function sendPrivateImage(userId, image, log = getLogger(), getConf
|
|
|
348
393
|
logSend("connection", "sendPrivateImage", { targetId: userId, messageId: mid, sessionId: getActiveReplyTarget(), replySessionId: getActiveReplySessionId() });
|
|
349
394
|
return mid;
|
|
350
395
|
}
|
|
351
|
-
export async function uploadGroupFile(groupId, file, name) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
}
|
|
355
404
|
}
|
|
356
|
-
export async function uploadPrivateFile(userId, file, name) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
+
}
|
|
360
413
|
}
|
|
361
414
|
/** 撤回消息 */
|
|
362
415
|
export async function deleteMsg(messageId) {
|
|
@@ -404,6 +457,49 @@ export async function getGroupMemberInfo(groupId, userId) {
|
|
|
404
457
|
return null;
|
|
405
458
|
}
|
|
406
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
|
+
}
|
|
407
503
|
/** 获取群信息(含 group_name) */
|
|
408
504
|
export async function getGroupInfo(groupId) {
|
|
409
505
|
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
@@ -437,20 +533,24 @@ export async function getMsg(messageId) {
|
|
|
437
533
|
}
|
|
438
534
|
}
|
|
439
535
|
/**
|
|
440
|
-
* 获取群聊历史消息(Lagrange.Core 扩展 API
|
|
536
|
+
* 获取群聊历史消息(Lagrange.Core 扩展 API,与 Lagrange.onebot context 一致)
|
|
537
|
+
* 仅使用 message_seq 分页(不传 message_id),与 Tiphareth getLast24HGroupMessages 调用方式一致。
|
|
441
538
|
* @param groupId 群号
|
|
442
|
-
* @param opts message_seq
|
|
539
|
+
* @param opts message_seq 起始序号(不传表示从最新一页);count 本页条数;reverse_order true 表示从旧到新,便于用 batch[0].message_seq 向前翻页
|
|
443
540
|
*/
|
|
444
541
|
export async function getGroupMsgHistory(groupId, opts = { count: 20 }) {
|
|
445
542
|
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
446
543
|
return [];
|
|
447
544
|
try {
|
|
448
|
-
const
|
|
545
|
+
const params = {
|
|
449
546
|
group_id: groupId,
|
|
450
|
-
message_seq: opts.message_seq,
|
|
451
|
-
message_id: opts.message_id,
|
|
452
547
|
count: opts.count ?? 20,
|
|
453
|
-
|
|
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);
|
|
454
554
|
if (res?.retcode === 0 && res?.data?.messages)
|
|
455
555
|
return res.data.messages;
|
|
456
556
|
return [];
|
|
@@ -459,6 +559,56 @@ export async function getGroupMsgHistory(groupId, opts = { count: 20 }) {
|
|
|
459
559
|
return [];
|
|
460
560
|
}
|
|
461
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
|
+
}
|
|
462
612
|
export async function connectForward(config) {
|
|
463
613
|
const path = config.path ?? "/onebot/v11/ws";
|
|
464
614
|
const pathNorm = path.startsWith("/") ? path : `/${path}`;
|
|
@@ -472,6 +622,7 @@ export async function connectForward(config) {
|
|
|
472
622
|
w.on("open", () => resolve());
|
|
473
623
|
w.on("error", reject);
|
|
474
624
|
});
|
|
625
|
+
w.__onebotPeerHost = config.host;
|
|
475
626
|
return w;
|
|
476
627
|
}
|
|
477
628
|
export async function createServerAndWait(config) {
|
|
@@ -486,7 +637,8 @@ export async function createServerAndWait(config) {
|
|
|
486
637
|
server.listen(config.port, host);
|
|
487
638
|
wsServer = wss;
|
|
488
639
|
return new Promise((resolve) => {
|
|
489
|
-
wss.on("connection", (socket) => {
|
|
640
|
+
wss.on("connection", (socket, req) => {
|
|
641
|
+
socket.__onebotPeerHost = req.socket.remoteAddress ?? undefined;
|
|
490
642
|
resolve(socket);
|
|
491
643
|
});
|
|
492
644
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { getOneBotConfig } from "../config.js";
|
|
5
5
|
import { getRawText, getTextFromSegments, getReplyMessageId, getTextFromMessageContent, isMentioned, } from "../message.js";
|
|
6
|
-
import { getRenderMarkdownToPlain, getCollapseDoubleNewlines, getWhitelistUserIds } from "../config.js";
|
|
6
|
+
import { getRenderMarkdownToPlain, getCollapseDoubleNewlines, getWhitelistUserIds, getOgImageRenderTheme, getNormalModeFlushIntervalMs, getNormalModeFlushChars, } from "../config.js";
|
|
7
7
|
import { markdownToPlain, collapseDoubleNewlines } from "../markdown.js";
|
|
8
8
|
import { markdownToImage } from "../og-image.js";
|
|
9
9
|
import { sendPrivateMsg, sendGroupMsg, sendPrivateImage, sendGroupImage, sendGroupForwardMsg, sendPrivateForwardMsg, setMsgEmojiLike, getMsg, } from "../connection.js";
|
|
@@ -231,6 +231,9 @@ export async function processInboundMessage(api, msg) {
|
|
|
231
231
|
api.logger?.info?.(`[onebot] dispatching message for session ${sessionId}`);
|
|
232
232
|
const longMessageMode = onebotCfg.longMessageMode ?? "normal";
|
|
233
233
|
const longMessageThreshold = onebotCfg.longMessageThreshold ?? 300;
|
|
234
|
+
api.logger?.info?.(`[onebot] longMessageMode=${longMessageMode}, threshold=${longMessageThreshold}`);
|
|
235
|
+
const normalModeFlushIntervalMs = getNormalModeFlushIntervalMs(cfg);
|
|
236
|
+
const normalModeFlushChars = getNormalModeFlushChars(cfg);
|
|
234
237
|
const replySessionId = `onebot-reply-${Date.now()}-${sessionId}`;
|
|
235
238
|
setActiveReplyTarget(replyTarget);
|
|
236
239
|
setActiveReplySessionId(replySessionId);
|
|
@@ -239,8 +242,28 @@ export async function processInboundMessage(api, msg) {
|
|
|
239
242
|
setForwardSuppressDelivery(true);
|
|
240
243
|
const deliveredChunks = [];
|
|
241
244
|
let chunkIndex = 0;
|
|
245
|
+
let normalModeBufferedText = "";
|
|
246
|
+
let normalModeBufferedRawText = "";
|
|
247
|
+
let normalModeFlushTimer = null;
|
|
248
|
+
let normalModeFlushChain = Promise.resolve();
|
|
242
249
|
const getConfig = () => getOneBotConfig(api);
|
|
243
250
|
const onReplySessionEnd = onebotCfg.onReplySessionEnd;
|
|
251
|
+
const normalModePunctuationFlushMinChars = 24;
|
|
252
|
+
const clearNormalModeFlushTimer = () => {
|
|
253
|
+
if (normalModeFlushTimer) {
|
|
254
|
+
clearTimeout(normalModeFlushTimer);
|
|
255
|
+
normalModeFlushTimer = null;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
const hasBufferedNormalModeText = () => normalModeBufferedText.length > 0 || normalModeBufferedRawText.length > 0;
|
|
259
|
+
const queueNormalModeFlush = (action) => {
|
|
260
|
+
normalModeFlushChain = normalModeFlushChain
|
|
261
|
+
.then(action)
|
|
262
|
+
.catch((e) => {
|
|
263
|
+
api.logger?.error?.(`[onebot] normal-mode flush failed: ${e?.message ?? e}`);
|
|
264
|
+
});
|
|
265
|
+
return normalModeFlushChain;
|
|
266
|
+
};
|
|
244
267
|
const doSendChunk = async (effectiveIsGroup, effectiveGroupId, uid, text, mediaUrl) => {
|
|
245
268
|
if (text) {
|
|
246
269
|
if (effectiveIsGroup && effectiveGroupId)
|
|
@@ -255,6 +278,50 @@ export async function processInboundMessage(api, msg) {
|
|
|
255
278
|
await sendPrivateImage(uid, mediaUrl, api.logger, getConfig);
|
|
256
279
|
}
|
|
257
280
|
};
|
|
281
|
+
const flushBufferedNormalModeText = async (effectiveIsGroup, effectiveGroupId, uid) => {
|
|
282
|
+
clearNormalModeFlushTimer();
|
|
283
|
+
if (!hasBufferedNormalModeText())
|
|
284
|
+
return;
|
|
285
|
+
const text = normalModeBufferedText;
|
|
286
|
+
const rawText = normalModeBufferedRawText;
|
|
287
|
+
normalModeBufferedText = "";
|
|
288
|
+
normalModeBufferedRawText = "";
|
|
289
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, text, undefined);
|
|
290
|
+
deliveredChunks.push({
|
|
291
|
+
index: chunkIndex++,
|
|
292
|
+
text: text || undefined,
|
|
293
|
+
rawText: rawText || undefined,
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
const scheduleNormalModeFlush = (effectiveIsGroup, effectiveGroupId, uid) => {
|
|
297
|
+
if (normalModeFlushTimer)
|
|
298
|
+
return;
|
|
299
|
+
normalModeFlushTimer = setTimeout(() => {
|
|
300
|
+
void queueNormalModeFlush(() => flushBufferedNormalModeText(effectiveIsGroup, effectiveGroupId, uid));
|
|
301
|
+
}, normalModeFlushIntervalMs);
|
|
302
|
+
};
|
|
303
|
+
const shouldFlushNormalModeBuffer = () => {
|
|
304
|
+
const rawText = normalModeBufferedRawText || normalModeBufferedText;
|
|
305
|
+
if (!rawText)
|
|
306
|
+
return false;
|
|
307
|
+
if (normalModeBufferedText.length >= normalModeFlushChars)
|
|
308
|
+
return true;
|
|
309
|
+
if (rawText.length < normalModePunctuationFlushMinChars)
|
|
310
|
+
return false;
|
|
311
|
+
return /[.!?。!?]\s*$/.test(rawText);
|
|
312
|
+
};
|
|
313
|
+
const appendNormalModeText = (current, next) => {
|
|
314
|
+
if (!current)
|
|
315
|
+
return next;
|
|
316
|
+
if (!next)
|
|
317
|
+
return current;
|
|
318
|
+
const lastChar = current[current.length - 1];
|
|
319
|
+
const firstChar = next[0];
|
|
320
|
+
if (/[A-Za-z0-9]/.test(lastChar) && /[A-Za-z0-9]/.test(firstChar)) {
|
|
321
|
+
return `${current} ${next}`;
|
|
322
|
+
}
|
|
323
|
+
return `${current}${next}`;
|
|
324
|
+
};
|
|
258
325
|
try {
|
|
259
326
|
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
260
327
|
ctx: ctxPayload,
|
|
@@ -277,13 +344,15 @@ export async function processInboundMessage(api, msg) {
|
|
|
277
344
|
let textPlain = usePlain ? markdownToPlain(trimmed) : trimmed;
|
|
278
345
|
if (getCollapseDoubleNewlines(cfg))
|
|
279
346
|
textPlain = collapseDoubleNewlines(textPlain);
|
|
280
|
-
deliveredChunks.push({
|
|
281
|
-
index: chunkIndex++,
|
|
282
|
-
text: textPlain || undefined,
|
|
283
|
-
rawText: trimmed || undefined,
|
|
284
|
-
mediaUrl: mediaUrl || undefined,
|
|
285
|
-
});
|
|
286
347
|
const shouldSendNow = longMessageMode === "normal";
|
|
348
|
+
if (!shouldSendNow) {
|
|
349
|
+
deliveredChunks.push({
|
|
350
|
+
index: chunkIndex++,
|
|
351
|
+
text: textPlain || undefined,
|
|
352
|
+
rawText: trimmed || undefined,
|
|
353
|
+
mediaUrl: mediaUrl || undefined,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
287
356
|
// forward 模式且非最后一条:仅暂存,绝不发送,等 final 时再统一处理
|
|
288
357
|
if (longMessageMode === "forward" && info.kind !== "final") {
|
|
289
358
|
forwardPendingSessions.set(replySessionId, Date.now());
|
|
@@ -294,29 +363,133 @@ export async function processInboundMessage(api, msg) {
|
|
|
294
363
|
}
|
|
295
364
|
try {
|
|
296
365
|
if (shouldSendNow) {
|
|
297
|
-
|
|
366
|
+
if (mediaUrl) {
|
|
367
|
+
await queueNormalModeFlush(async () => {
|
|
368
|
+
await flushBufferedNormalModeText(effectiveIsGroup, effectiveGroupId, uid);
|
|
369
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, textPlain, mediaUrl);
|
|
370
|
+
deliveredChunks.push({
|
|
371
|
+
index: chunkIndex++,
|
|
372
|
+
text: textPlain || undefined,
|
|
373
|
+
rawText: trimmed || undefined,
|
|
374
|
+
mediaUrl: mediaUrl || undefined,
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
normalModeBufferedText = appendNormalModeText(normalModeBufferedText, textPlain);
|
|
380
|
+
normalModeBufferedRawText = appendNormalModeText(normalModeBufferedRawText, trimmed);
|
|
381
|
+
if (shouldFlushNormalModeBuffer()) {
|
|
382
|
+
await queueNormalModeFlush(() => flushBufferedNormalModeText(effectiveIsGroup, effectiveGroupId, uid));
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
scheduleNormalModeFlush(effectiveIsGroup, effectiveGroupId, uid);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
298
388
|
}
|
|
299
389
|
if (info.kind === "final") {
|
|
390
|
+
if (shouldSendNow) {
|
|
391
|
+
await queueNormalModeFlush(() => flushBufferedNormalModeText(effectiveIsGroup, effectiveGroupId, uid));
|
|
392
|
+
}
|
|
300
393
|
const lastSentCount = lastSentChunkCountBySession.get(replySessionId) ?? 0;
|
|
301
394
|
const chunksToSend = deliveredChunks.slice(lastSentCount);
|
|
302
395
|
if (chunksToSend.length === 0)
|
|
303
396
|
return;
|
|
304
397
|
const totalLen = deliveredChunks.reduce((s, c) => s + (c.rawText ?? c.text ?? "").length, 0);
|
|
398
|
+
const incrementalLen = chunksToSend.reduce((s, c) => s + (c.rawText ?? c.text ?? "").length, 0);
|
|
305
399
|
const isLong = totalLen > longMessageThreshold;
|
|
400
|
+
const isIncrementalLong = incrementalLen > longMessageThreshold;
|
|
306
401
|
const isIncremental = lastSentCount > 0;
|
|
402
|
+
api.logger?.info?.(`[onebot] final check: totalLen=${totalLen}, threshold=${longMessageThreshold}, isLong=${isLong}, isIncremental=${isIncremental}, deliveredChunks=${deliveredChunks.length}`);
|
|
307
403
|
if (isIncremental) {
|
|
308
404
|
setForwardSuppressDelivery(false);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
405
|
+
// normal 模式下增量 chunk 已在 deliver 中实时发出;这里不能在 final 再补发一次。
|
|
406
|
+
if (!shouldSendNow && isIncrementalLong && (longMessageMode === "og_image" || longMessageMode === "forward")) {
|
|
407
|
+
const fullRaw = chunksToSend.map((c) => c.rawText ?? c.text ?? "").join("\n\n");
|
|
408
|
+
if (fullRaw.trim()) {
|
|
409
|
+
if (longMessageMode === "og_image") {
|
|
410
|
+
try {
|
|
411
|
+
const imgUrl = await markdownToImage(fullRaw, { theme: getOgImageRenderTheme(api?.config) });
|
|
412
|
+
if (imgUrl) {
|
|
413
|
+
if (effectiveIsGroup && effectiveGroupId)
|
|
414
|
+
await sendGroupImage(effectiveGroupId, imgUrl, api.logger, getConfig);
|
|
415
|
+
else if (uid)
|
|
416
|
+
await sendPrivateImage(uid, imgUrl, api.logger, getConfig);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
api.logger?.warn?.("[onebot] og_image (incremental): node-html-to-image not installed, falling back to normal send");
|
|
420
|
+
for (const c of chunksToSend) {
|
|
421
|
+
if (c.text || c.mediaUrl)
|
|
422
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, c.text ?? "", c.mediaUrl);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
api.logger?.error?.(`[onebot] og_image (incremental) failed: ${e?.message}`);
|
|
428
|
+
for (const c of chunksToSend) {
|
|
429
|
+
if (c.text || c.mediaUrl)
|
|
430
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, c.text ?? "", c.mediaUrl);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
try {
|
|
436
|
+
const nodes = [];
|
|
437
|
+
for (const c of chunksToSend) {
|
|
438
|
+
if (c.mediaUrl) {
|
|
439
|
+
const mid = await sendPrivateImage(selfId, c.mediaUrl, api.logger, getConfig);
|
|
440
|
+
if (mid)
|
|
441
|
+
nodes.push({ type: "node", data: { id: String(mid) } });
|
|
442
|
+
}
|
|
443
|
+
else if (c.text) {
|
|
444
|
+
const mid = await sendPrivateMsg(selfId, c.text, getConfig);
|
|
445
|
+
if (mid)
|
|
446
|
+
nodes.push({ type: "node", data: { id: String(mid) } });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (nodes.length > 0) {
|
|
450
|
+
if (effectiveIsGroup && effectiveGroupId)
|
|
451
|
+
await sendGroupForwardMsg(effectiveGroupId, nodes, getConfig);
|
|
452
|
+
else if (uid)
|
|
453
|
+
await sendPrivateForwardMsg(uid, nodes, getConfig);
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
for (const c of chunksToSend) {
|
|
457
|
+
if (c.text || c.mediaUrl)
|
|
458
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, c.text ?? "", c.mediaUrl);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
api.logger?.error?.(`[onebot] forward (incremental) failed: ${e?.message}`);
|
|
464
|
+
for (const c of chunksToSend) {
|
|
465
|
+
if (c.text || c.mediaUrl)
|
|
466
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, c.text ?? "", c.mediaUrl);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
for (const c of chunksToSend) {
|
|
473
|
+
if (c.text || c.mediaUrl)
|
|
474
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, c.text ?? "", c.mediaUrl);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else if (!shouldSendNow) {
|
|
479
|
+
for (const c of chunksToSend) {
|
|
480
|
+
if (c.text || c.mediaUrl)
|
|
481
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, c.text ?? "", c.mediaUrl);
|
|
482
|
+
}
|
|
312
483
|
}
|
|
313
484
|
}
|
|
314
485
|
else if (!shouldSendNow && (longMessageMode === "og_image" || longMessageMode === "forward")) {
|
|
486
|
+
api.logger?.info?.(`[onebot] checking og_image: isLong=${isLong}, mode=${longMessageMode}`);
|
|
315
487
|
if (isLong && longMessageMode === "og_image") {
|
|
488
|
+
api.logger?.info?.(`[onebot] triggering og_image for ${totalLen} chars`);
|
|
316
489
|
const fullRaw = deliveredChunks.map((c) => c.rawText ?? c.text ?? "").join("\n\n");
|
|
317
490
|
if (fullRaw.trim()) {
|
|
318
491
|
try {
|
|
319
|
-
const imgUrl = await markdownToImage(fullRaw);
|
|
492
|
+
const imgUrl = await markdownToImage(fullRaw, { theme: getOgImageRenderTheme(api?.config) });
|
|
320
493
|
if (imgUrl) {
|
|
321
494
|
if (effectiveIsGroup && effectiveGroupId)
|
|
322
495
|
await sendGroupImage(effectiveGroupId, imgUrl, api.logger, getConfig);
|
|
@@ -419,7 +592,7 @@ export async function processInboundMessage(api, msg) {
|
|
|
419
592
|
await clearEmojiReaction();
|
|
420
593
|
},
|
|
421
594
|
},
|
|
422
|
-
replyOptions: { disableBlockStreaming:
|
|
595
|
+
replyOptions: { disableBlockStreaming: longMessageMode !== "normal" },
|
|
423
596
|
});
|
|
424
597
|
}
|
|
425
598
|
catch (err) {
|
|
@@ -435,6 +608,7 @@ export async function processInboundMessage(api, msg) {
|
|
|
435
608
|
catch (_) { }
|
|
436
609
|
}
|
|
437
610
|
finally {
|
|
611
|
+
clearNormalModeFlushTimer();
|
|
438
612
|
setForwardSuppressDelivery(false);
|
|
439
613
|
setActiveReplySelfId(null);
|
|
440
614
|
lastSentChunkCountBySession.delete(replySessionId);
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { OneBotChannelPlugin } from "./channel.js";
|
|
|
12
12
|
import { registerService } from "./service.js";
|
|
13
13
|
import { startImageTempCleanup } from "./connection.js";
|
|
14
14
|
import { startForwardCleanupTimer } from "./handlers/process-inbound.js";
|
|
15
|
+
import { registerOneBotCli } from "./cli-commands.js";
|
|
15
16
|
export default function register(api) {
|
|
16
17
|
globalThis.__onebotApi = api;
|
|
17
18
|
globalThis.__onebotGatewayConfig = api.config;
|
|
@@ -22,11 +23,12 @@ export default function register(api) {
|
|
|
22
23
|
api.registerCli((ctx) => {
|
|
23
24
|
const prog = ctx.program;
|
|
24
25
|
if (prog && typeof prog.command === "function") {
|
|
25
|
-
const onebot = prog.command("onebot").description("OneBot
|
|
26
|
+
const onebot = prog.command("onebot").description("OneBot 渠道配置与工具");
|
|
26
27
|
onebot.command("setup").description("交互式配置 OneBot 连接参数").action(async () => {
|
|
27
28
|
const { runOneBotSetup } = await import("./setup.js");
|
|
28
29
|
await runOneBotSetup();
|
|
29
30
|
});
|
|
31
|
+
registerOneBotCli(onebot, api);
|
|
30
32
|
}
|
|
31
33
|
}, { commands: ["onebot"] });
|
|
32
34
|
}
|
|
@@ -3,4 +3,8 @@
|
|
|
3
3
|
* 用于 OG 图片模式下的 Markdown 渲染
|
|
4
4
|
*/
|
|
5
5
|
export declare function markdownToHtml(md: string): string;
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* 获取用于 OG 图片的完整样式(基础 + 主题)
|
|
8
|
+
* @param theme "default" 无额外样式;"dust" 内置 dust 主题;或 custom 时的 CSS 文件绝对路径
|
|
9
|
+
*/
|
|
10
|
+
export declare function getMarkdownStyles(theme?: string): string;
|