@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
|
@@ -2,12 +2,38 @@
|
|
|
2
2
|
* 入站消息处理
|
|
3
3
|
*/
|
|
4
4
|
import { getOneBotConfig } from "../config.js";
|
|
5
|
-
import { getRawText, isMentioned } from "../message.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { getRawText, getTextFromSegments, getReplyMessageId, getTextFromMessageContent, isMentioned, } from "../message.js";
|
|
6
|
+
import { getRenderMarkdownToPlain, getCollapseDoubleNewlines, getWhitelistUserIds, getOgImageRenderTheme, getNormalModeFlushIntervalMs, getNormalModeFlushChars, } from "../config.js";
|
|
7
|
+
import { markdownToPlain, collapseDoubleNewlines } from "../markdown.js";
|
|
8
|
+
import { markdownToImage } from "../og-image.js";
|
|
9
|
+
import { sendPrivateMsg, sendGroupMsg, sendPrivateImage, sendGroupImage, sendGroupForwardMsg, sendPrivateForwardMsg, setMsgEmojiLike, getMsg, } from "../connection.js";
|
|
10
|
+
import { setActiveReplyTarget, clearActiveReplyTarget, setActiveReplySessionId, setForwardSuppressDelivery, setActiveReplySelfId } from "../reply-context.js";
|
|
8
11
|
import { loadPluginSdk, getSdk } from "../sdk.js";
|
|
12
|
+
import { handleGroupIncrease } from "./group-increase.js";
|
|
9
13
|
const DEFAULT_HISTORY_LIMIT = 20;
|
|
10
14
|
export const sessionHistories = new Map();
|
|
15
|
+
/** forward 模式下待处理的会话,用于定期清理未完成的缓冲 */
|
|
16
|
+
const forwardPendingSessions = new Map();
|
|
17
|
+
/** 每个 replySessionId 已发送的 chunk 数量,用于支持多次 final(如工具调用后追加内容) */
|
|
18
|
+
const lastSentChunkCountBySession = new Map();
|
|
19
|
+
const FORWARD_PENDING_TTL_MS = 5 * 60 * 1000; // 5 分钟
|
|
20
|
+
const FORWARD_CLEANUP_INTERVAL_MS = 60 * 1000; // 每分钟清理一次
|
|
21
|
+
function cleanupForwardPendingSessions() {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const toDelete = [];
|
|
24
|
+
for (const [id, ts] of forwardPendingSessions) {
|
|
25
|
+
if (now - ts > FORWARD_PENDING_TTL_MS)
|
|
26
|
+
toDelete.push(id);
|
|
27
|
+
}
|
|
28
|
+
for (const id of toDelete)
|
|
29
|
+
forwardPendingSessions.delete(id);
|
|
30
|
+
}
|
|
31
|
+
let forwardCleanupTimer = null;
|
|
32
|
+
export function startForwardCleanupTimer() {
|
|
33
|
+
if (forwardCleanupTimer)
|
|
34
|
+
return;
|
|
35
|
+
forwardCleanupTimer = setInterval(cleanupForwardPendingSessions, FORWARD_CLEANUP_INTERVAL_MS);
|
|
36
|
+
}
|
|
11
37
|
export async function processInboundMessage(api, msg) {
|
|
12
38
|
await loadPluginSdk();
|
|
13
39
|
const { buildPendingHistoryContextFromMap, recordPendingHistoryEntry, clearHistoryEntriesIfEnabled } = getSdk();
|
|
@@ -21,20 +47,70 @@ export async function processInboundMessage(api, msg) {
|
|
|
21
47
|
api.logger?.warn?.("[onebot] not configured");
|
|
22
48
|
return;
|
|
23
49
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
50
|
+
const selfId = msg.self_id ?? 0;
|
|
51
|
+
if (msg.user_id != null && Number(msg.user_id) === Number(selfId)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const replyId = getReplyMessageId(msg);
|
|
55
|
+
let messageText;
|
|
56
|
+
if (replyId != null) {
|
|
57
|
+
const userText = getTextFromSegments(msg);
|
|
58
|
+
try {
|
|
59
|
+
const quoted = await getMsg(replyId);
|
|
60
|
+
const quotedText = quoted ? getTextFromMessageContent(quoted.message) : "";
|
|
61
|
+
const senderLabel = quoted?.sender?.nickname ?? quoted?.sender?.user_id ?? "某人";
|
|
62
|
+
messageText = quotedText.trim()
|
|
63
|
+
? `[引用 ${String(senderLabel)} 的消息:${quotedText.trim()}]\n${userText}`
|
|
64
|
+
: userText;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
messageText = userText;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
messageText = getRawText(msg);
|
|
72
|
+
}
|
|
26
73
|
if (!messageText?.trim()) {
|
|
27
74
|
api.logger?.info?.(`[onebot] ignoring empty message`);
|
|
28
75
|
return;
|
|
29
76
|
}
|
|
30
77
|
const isGroup = msg.message_type === "group";
|
|
31
|
-
const
|
|
78
|
+
const cfg = api.config;
|
|
32
79
|
const requireMention = cfg?.channels?.onebot?.requireMention ?? true;
|
|
33
80
|
if (isGroup && requireMention && !isMentioned(msg, selfId)) {
|
|
34
81
|
api.logger?.info?.(`[onebot] ignoring group message without @mention`);
|
|
35
82
|
return;
|
|
36
83
|
}
|
|
84
|
+
const gi = cfg?.channels?.onebot?.groupIncrease;
|
|
85
|
+
// 测试欢迎:@ 机器人并发送 /group-increase,模拟当前发送者入群,触发欢迎(使用该人的 id、nickname 等)
|
|
86
|
+
// 使用 getTextFromSegments 提取纯文本,避免 raw_message 中 [CQ:at,qq=xxx] 等 CQ 码导致匹配失败
|
|
87
|
+
const cmdText = getTextFromSegments(msg).trim() || messageText.trim();
|
|
88
|
+
const groupIncreaseTrigger = isGroup && isMentioned(msg, selfId) && /^\/group-increase\s*$/i.test(cmdText) && gi?.enabled;
|
|
89
|
+
if (groupIncreaseTrigger) {
|
|
90
|
+
const fakeMsg = {
|
|
91
|
+
post_type: "notice",
|
|
92
|
+
notice_type: "group_increase",
|
|
93
|
+
group_id: msg.group_id,
|
|
94
|
+
user_id: msg.user_id,
|
|
95
|
+
};
|
|
96
|
+
await handleGroupIncrease(api, fakeMsg);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
37
99
|
const userId = msg.user_id;
|
|
100
|
+
const whitelist = getWhitelistUserIds(cfg);
|
|
101
|
+
if (whitelist.length > 0 && !whitelist.includes(Number(userId))) {
|
|
102
|
+
const denyMsg = "权限不足,请向管理员申请权限";
|
|
103
|
+
const getConfig = () => getOneBotConfig(api);
|
|
104
|
+
try {
|
|
105
|
+
if (msg.message_type === "group" && msg.group_id)
|
|
106
|
+
await sendGroupMsg(msg.group_id, denyMsg, getConfig);
|
|
107
|
+
else
|
|
108
|
+
await sendPrivateMsg(userId, denyMsg, getConfig);
|
|
109
|
+
}
|
|
110
|
+
catch (_) { }
|
|
111
|
+
api.logger?.info?.(`[onebot] user ${userId} not in whitelist, denied`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
38
114
|
const groupId = msg.group_id;
|
|
39
115
|
const sessionId = isGroup
|
|
40
116
|
? `onebot:group:${groupId}`.toLowerCase()
|
|
@@ -153,7 +229,99 @@ export async function processInboundMessage(api, msg) {
|
|
|
153
229
|
}
|
|
154
230
|
}
|
|
155
231
|
api.logger?.info?.(`[onebot] dispatching message for session ${sessionId}`);
|
|
232
|
+
const longMessageMode = onebotCfg.longMessageMode ?? "normal";
|
|
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);
|
|
237
|
+
const replySessionId = `onebot-reply-${Date.now()}-${sessionId}`;
|
|
156
238
|
setActiveReplyTarget(replyTarget);
|
|
239
|
+
setActiveReplySessionId(replySessionId);
|
|
240
|
+
setActiveReplySelfId(selfId);
|
|
241
|
+
if (longMessageMode === "forward")
|
|
242
|
+
setForwardSuppressDelivery(true);
|
|
243
|
+
const deliveredChunks = [];
|
|
244
|
+
let chunkIndex = 0;
|
|
245
|
+
let normalModeBufferedText = "";
|
|
246
|
+
let normalModeBufferedRawText = "";
|
|
247
|
+
let normalModeFlushTimer = null;
|
|
248
|
+
let normalModeFlushChain = Promise.resolve();
|
|
249
|
+
const getConfig = () => getOneBotConfig(api);
|
|
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
|
+
};
|
|
267
|
+
const doSendChunk = async (effectiveIsGroup, effectiveGroupId, uid, text, mediaUrl) => {
|
|
268
|
+
if (text) {
|
|
269
|
+
if (effectiveIsGroup && effectiveGroupId)
|
|
270
|
+
await sendGroupMsg(effectiveGroupId, text, getConfig);
|
|
271
|
+
else if (uid)
|
|
272
|
+
await sendPrivateMsg(uid, text, getConfig);
|
|
273
|
+
}
|
|
274
|
+
if (mediaUrl) {
|
|
275
|
+
if (effectiveIsGroup && effectiveGroupId)
|
|
276
|
+
await sendGroupImage(effectiveGroupId, mediaUrl, api.logger, getConfig);
|
|
277
|
+
else if (uid)
|
|
278
|
+
await sendPrivateImage(uid, mediaUrl, api.logger, getConfig);
|
|
279
|
+
}
|
|
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
|
+
};
|
|
157
325
|
try {
|
|
158
326
|
await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
159
327
|
ctx: ctxPayload,
|
|
@@ -168,30 +336,251 @@ export async function processInboundMessage(api, msg) {
|
|
|
168
336
|
if ((!trimmed || trimmed === "NO_REPLY" || trimmed.endsWith("NO_REPLY")) && !mediaUrl)
|
|
169
337
|
return;
|
|
170
338
|
const { userId: uid, groupId: gid, isGroup: ig } = ctxPayload._onebot || {};
|
|
171
|
-
// 兜底:sessionId 格式 onebot:group:群号 为权威来源,某些实现 _onebot.groupId 可能为空
|
|
172
339
|
const sessionKey = String(ctxPayload.SessionKey ?? sessionId);
|
|
173
340
|
const groupMatch = sessionKey.match(/^onebot:group:(\d+)$/i);
|
|
174
341
|
const effectiveIsGroup = groupMatch != null || Boolean(ig);
|
|
175
342
|
const effectiveGroupId = (groupMatch ? parseInt(groupMatch[1], 10) : undefined) ?? gid;
|
|
343
|
+
const usePlain = getRenderMarkdownToPlain(cfg);
|
|
344
|
+
let textPlain = usePlain ? markdownToPlain(trimmed) : trimmed;
|
|
345
|
+
if (getCollapseDoubleNewlines(cfg))
|
|
346
|
+
textPlain = collapseDoubleNewlines(textPlain);
|
|
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
|
+
}
|
|
356
|
+
// forward 模式且非最后一条:仅暂存,绝不发送,等 final 时再统一处理
|
|
357
|
+
if (longMessageMode === "forward" && info.kind !== "final") {
|
|
358
|
+
forwardPendingSessions.set(replySessionId, Date.now());
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (info.kind === "final" && longMessageMode === "forward") {
|
|
362
|
+
forwardPendingSessions.delete(replySessionId);
|
|
363
|
+
}
|
|
176
364
|
try {
|
|
177
|
-
if (
|
|
178
|
-
if (
|
|
179
|
-
await
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
365
|
+
if (shouldSendNow) {
|
|
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
|
+
}
|
|
188
388
|
}
|
|
189
|
-
if (info.kind === "final"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
389
|
+
if (info.kind === "final") {
|
|
390
|
+
if (shouldSendNow) {
|
|
391
|
+
await queueNormalModeFlush(() => flushBufferedNormalModeText(effectiveIsGroup, effectiveGroupId, uid));
|
|
392
|
+
}
|
|
393
|
+
const lastSentCount = lastSentChunkCountBySession.get(replySessionId) ?? 0;
|
|
394
|
+
const chunksToSend = deliveredChunks.slice(lastSentCount);
|
|
395
|
+
if (chunksToSend.length === 0)
|
|
396
|
+
return;
|
|
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);
|
|
399
|
+
const isLong = totalLen > longMessageThreshold;
|
|
400
|
+
const isIncrementalLong = incrementalLen > longMessageThreshold;
|
|
401
|
+
const isIncremental = lastSentCount > 0;
|
|
402
|
+
api.logger?.info?.(`[onebot] final check: totalLen=${totalLen}, threshold=${longMessageThreshold}, isLong=${isLong}, isIncremental=${isIncremental}, deliveredChunks=${deliveredChunks.length}`);
|
|
403
|
+
if (isIncremental) {
|
|
404
|
+
setForwardSuppressDelivery(false);
|
|
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
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else if (!shouldSendNow && (longMessageMode === "og_image" || longMessageMode === "forward")) {
|
|
486
|
+
api.logger?.info?.(`[onebot] checking og_image: isLong=${isLong}, mode=${longMessageMode}`);
|
|
487
|
+
if (isLong && longMessageMode === "og_image") {
|
|
488
|
+
api.logger?.info?.(`[onebot] triggering og_image for ${totalLen} chars`);
|
|
489
|
+
const fullRaw = deliveredChunks.map((c) => c.rawText ?? c.text ?? "").join("\n\n");
|
|
490
|
+
if (fullRaw.trim()) {
|
|
491
|
+
try {
|
|
492
|
+
const imgUrl = await markdownToImage(fullRaw, { theme: getOgImageRenderTheme(api?.config) });
|
|
493
|
+
if (imgUrl) {
|
|
494
|
+
if (effectiveIsGroup && effectiveGroupId)
|
|
495
|
+
await sendGroupImage(effectiveGroupId, imgUrl, api.logger, getConfig);
|
|
496
|
+
else if (uid)
|
|
497
|
+
await sendPrivateImage(uid, imgUrl, api.logger, getConfig);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
api.logger?.warn?.("[onebot] og_image: node-html-to-image not installed, falling back to normal send");
|
|
501
|
+
setForwardSuppressDelivery(false);
|
|
502
|
+
for (const c of deliveredChunks) {
|
|
503
|
+
if (c.text || c.mediaUrl)
|
|
504
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, c.text ?? "", c.mediaUrl);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
catch (e) {
|
|
509
|
+
api.logger?.error?.(`[onebot] og_image failed: ${e?.message}`);
|
|
510
|
+
setForwardSuppressDelivery(false);
|
|
511
|
+
for (const c of deliveredChunks) {
|
|
512
|
+
if (c.text || c.mediaUrl)
|
|
513
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, c.text ?? "", c.mediaUrl);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
else if (isLong && longMessageMode === "forward") {
|
|
519
|
+
try {
|
|
520
|
+
const nodes = [];
|
|
521
|
+
for (const c of deliveredChunks) {
|
|
522
|
+
if (c.mediaUrl) {
|
|
523
|
+
const mid = await sendPrivateImage(selfId, c.mediaUrl, api.logger, getConfig);
|
|
524
|
+
if (mid)
|
|
525
|
+
nodes.push({ type: "node", data: { id: String(mid) } });
|
|
526
|
+
}
|
|
527
|
+
else if (c.text) {
|
|
528
|
+
const mid = await sendPrivateMsg(selfId, c.text, getConfig);
|
|
529
|
+
if (mid)
|
|
530
|
+
nodes.push({ type: "node", data: { id: String(mid) } });
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (nodes.length > 0) {
|
|
534
|
+
if (effectiveIsGroup && effectiveGroupId)
|
|
535
|
+
await sendGroupForwardMsg(effectiveGroupId, nodes, getConfig);
|
|
536
|
+
else if (uid)
|
|
537
|
+
await sendPrivateForwardMsg(uid, nodes, getConfig);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
api.logger?.error?.(`[onebot] forward failed: ${e?.message}`);
|
|
542
|
+
setForwardSuppressDelivery(false);
|
|
543
|
+
for (const c of deliveredChunks) {
|
|
544
|
+
if (c.text || c.mediaUrl)
|
|
545
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, c.text ?? "", c.mediaUrl);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
setForwardSuppressDelivery(false);
|
|
551
|
+
for (const c of deliveredChunks) {
|
|
552
|
+
if (c.text || c.mediaUrl)
|
|
553
|
+
await doSendChunk(effectiveIsGroup, effectiveGroupId, uid, c.text ?? "", c.mediaUrl);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
lastSentChunkCountBySession.set(replySessionId, deliveredChunks.length);
|
|
558
|
+
if (clearHistoryEntriesIfEnabled) {
|
|
559
|
+
clearHistoryEntriesIfEnabled({
|
|
560
|
+
historyMap: sessionHistories,
|
|
561
|
+
historyKey: sessionId,
|
|
562
|
+
limit: DEFAULT_HISTORY_LIMIT,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
if (onReplySessionEnd) {
|
|
566
|
+
const ctx = {
|
|
567
|
+
replySessionId,
|
|
568
|
+
sessionId,
|
|
569
|
+
to: replyTarget,
|
|
570
|
+
chunks: deliveredChunks.map(({ index, text: t, mediaUrl: m }) => ({ index, text: t, mediaUrl: m })),
|
|
571
|
+
userMessage: messageText,
|
|
572
|
+
};
|
|
573
|
+
if (typeof onReplySessionEnd === "function") {
|
|
574
|
+
await onReplySessionEnd(ctx);
|
|
575
|
+
}
|
|
576
|
+
else if (typeof onReplySessionEnd === "string" && onReplySessionEnd.trim()) {
|
|
577
|
+
const { loadScript } = await import("../load-script.js");
|
|
578
|
+
const mod = await loadScript(onReplySessionEnd.trim());
|
|
579
|
+
const fn = mod?.default ?? mod?.onReplySessionEnd;
|
|
580
|
+
if (typeof fn === "function")
|
|
581
|
+
await fn(ctx);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
195
584
|
}
|
|
196
585
|
}
|
|
197
586
|
catch (e) {
|
|
@@ -203,7 +592,7 @@ export async function processInboundMessage(api, msg) {
|
|
|
203
592
|
await clearEmojiReaction();
|
|
204
593
|
},
|
|
205
594
|
},
|
|
206
|
-
replyOptions: { disableBlockStreaming:
|
|
595
|
+
replyOptions: { disableBlockStreaming: longMessageMode !== "normal" },
|
|
207
596
|
});
|
|
208
597
|
}
|
|
209
598
|
catch (err) {
|
|
@@ -219,6 +608,12 @@ export async function processInboundMessage(api, msg) {
|
|
|
219
608
|
catch (_) { }
|
|
220
609
|
}
|
|
221
610
|
finally {
|
|
611
|
+
clearNormalModeFlushTimer();
|
|
612
|
+
setForwardSuppressDelivery(false);
|
|
613
|
+
setActiveReplySelfId(null);
|
|
614
|
+
lastSentChunkCountBySession.delete(replySessionId);
|
|
615
|
+
forwardPendingSessions.delete(replySessionId);
|
|
616
|
+
setActiveReplySessionId(null);
|
|
222
617
|
clearActiveReplyTarget();
|
|
223
618
|
}
|
|
224
619
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,20 +11,24 @@
|
|
|
11
11
|
import { OneBotChannelPlugin } from "./channel.js";
|
|
12
12
|
import { registerService } from "./service.js";
|
|
13
13
|
import { startImageTempCleanup } from "./connection.js";
|
|
14
|
+
import { startForwardCleanupTimer } from "./handlers/process-inbound.js";
|
|
15
|
+
import { registerOneBotCli } from "./cli-commands.js";
|
|
14
16
|
export default function register(api) {
|
|
15
17
|
globalThis.__onebotApi = api;
|
|
16
18
|
globalThis.__onebotGatewayConfig = api.config;
|
|
17
19
|
startImageTempCleanup();
|
|
20
|
+
startForwardCleanupTimer();
|
|
18
21
|
api.registerChannel({ plugin: OneBotChannelPlugin });
|
|
19
22
|
if (typeof api.registerCli === "function") {
|
|
20
23
|
api.registerCli((ctx) => {
|
|
21
24
|
const prog = ctx.program;
|
|
22
25
|
if (prog && typeof prog.command === "function") {
|
|
23
|
-
const onebot = prog.command("onebot").description("OneBot
|
|
26
|
+
const onebot = prog.command("onebot").description("OneBot 渠道配置与工具");
|
|
24
27
|
onebot.command("setup").description("交互式配置 OneBot 连接参数").action(async () => {
|
|
25
28
|
const { runOneBotSetup } = await import("./setup.js");
|
|
26
29
|
await runOneBotSetup();
|
|
27
30
|
});
|
|
31
|
+
registerOneBotCli(onebot, api);
|
|
28
32
|
}
|
|
29
33
|
}, { commands: ["onebot"] });
|
|
30
34
|
}
|
package/dist/load-script.d.ts
CHANGED
|
@@ -2,4 +2,8 @@
|
|
|
2
2
|
* 动态加载用户脚本(支持 .js/.mjs/.ts/.mts)
|
|
3
3
|
* .ts/.mts 依赖 tsx 运行时
|
|
4
4
|
*/
|
|
5
|
-
export
|
|
5
|
+
export interface LoadScriptOptions {
|
|
6
|
+
/** 脚本执行时的 CWD 绝对路径,用于解析相对路径及脚本内 process.cwd() */
|
|
7
|
+
cwd?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function loadScript(scriptPath: string, options?: LoadScriptOptions): Promise<Record<string, unknown>>;
|
package/dist/load-script.js
CHANGED
|
@@ -6,8 +6,11 @@ import { resolve } from "path";
|
|
|
6
6
|
import { pathToFileURL } from "url";
|
|
7
7
|
import { extname } from "path";
|
|
8
8
|
const TS_EXT = [".ts", ".mts"];
|
|
9
|
-
export async function loadScript(scriptPath) {
|
|
10
|
-
const
|
|
9
|
+
export async function loadScript(scriptPath, options) {
|
|
10
|
+
const baseDir = options?.cwd?.trim() ? resolve(options.cwd) : process.cwd();
|
|
11
|
+
const absPath = scriptPath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(scriptPath)
|
|
12
|
+
? resolve(scriptPath.trim())
|
|
13
|
+
: resolve(baseDir, scriptPath.trim());
|
|
11
14
|
const ext = extname(absPath).toLowerCase();
|
|
12
15
|
if (TS_EXT.includes(ext)) {
|
|
13
16
|
try {
|
|
@@ -17,6 +20,7 @@ export async function loadScript(scriptPath) {
|
|
|
17
20
|
throw new Error("执行 .ts/.mts 脚本需要安装 tsx 依赖:npm install tsx");
|
|
18
21
|
}
|
|
19
22
|
}
|
|
23
|
+
// 使用 pathToFileURL 确保 file:// URL 格式正确(Windows 反斜杠会转为正斜杠)
|
|
20
24
|
const url = pathToFileURL(absPath).href;
|
|
21
|
-
return import(url);
|
|
25
|
+
return (await import(url));
|
|
22
26
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown 转 HTML(保留格式,含代码高亮)
|
|
3
|
+
* 用于 OG 图片模式下的 Markdown 渲染
|
|
4
|
+
*/
|
|
5
|
+
export declare function markdownToHtml(md: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* 获取用于 OG 图片的完整样式(基础 + 主题)
|
|
8
|
+
* @param theme "default" 无额外样式;"dust" 内置 dust 主题;或 custom 时的 CSS 文件绝对路径
|
|
9
|
+
*/
|
|
10
|
+
export declare function getMarkdownStyles(theme?: string): string;
|