openclaw-plugin-yuanbao 2.3.1 → 2.4.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/openclaw.plugin.json +1 -1
- package/dist/src/channel.js +49 -38
- package/dist/src/config-schema.js +2 -2
- package/dist/src/dm/directory-cache.d.ts +2 -3
- package/dist/src/dm/directory.d.ts +2 -3
- package/dist/src/dm/directory.js +4 -6
- package/dist/src/dm/handle-action.js +1 -1
- package/dist/src/dm/send-dm.js +1 -1
- package/dist/src/logger.js +1 -0
- package/dist/src/media.d.ts +1 -0
- package/dist/src/media.js +1 -1
- package/dist/src/message-handler/inbound.js +9 -100
- package/dist/src/message-handler/outbound.js +23 -25
- package/dist/src/message-tool/action-runtime.js +20 -3
- package/dist/src/message-tool/hints.js +3 -0
- package/dist/src/outbound-queue.d.ts +15 -2
- package/dist/src/outbound-queue.js +165 -25
- package/dist/src/targets.d.ts +13 -0
- package/dist/src/targets.js +47 -0
- package/dist/src/tools/member.js +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/src/channel.js
CHANGED
|
@@ -2,13 +2,13 @@ import { DEFAULT_ACCOUNT_ID, deleteAccountFromConfigSection, setAccountEnabledIn
|
|
|
2
2
|
import { listYuanbaoAccountIds, resolveDefaultYuanbaoAccountId, resolveYuanbaoAccount } from './accounts.js';
|
|
3
3
|
import { yuanbaoConfigSchema } from './config-schema.js';
|
|
4
4
|
import { yuanbaoOnboardingAdapter } from './onboarding.js';
|
|
5
|
-
import { createLog
|
|
5
|
+
import { createLog } from './logger.js';
|
|
6
6
|
import { yuanbaoSetupAdapter } from './setup.js';
|
|
7
7
|
import { startYuanbaoWsGateway, getActiveWsClient } from './yuanbao-server/ws/index.js';
|
|
8
8
|
import { getYuanbaoRuntime } from './runtime.js';
|
|
9
9
|
import { sendYuanbaoMessage, sendYuanbaoGroupMessage } from './message-handler/index.js';
|
|
10
10
|
import { initOutboundQueue, destroyOutboundQueue, getOutboundQueue } from './outbound-queue.js';
|
|
11
|
-
import {
|
|
11
|
+
import { ChatType, getGroupCode, parseTarget } from './targets.js';
|
|
12
12
|
import { buildMessageToolHints, yuanbaoMessageActions } from './message-tool/index.js';
|
|
13
13
|
function toChannelResult(result) {
|
|
14
14
|
return {
|
|
@@ -18,6 +18,16 @@ function toChannelResult(result) {
|
|
|
18
18
|
error: result.error ? new Error(result.error) : undefined,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
|
+
function buildMinCtx(account, wsClient) {
|
|
22
|
+
return {
|
|
23
|
+
account,
|
|
24
|
+
config: account.config,
|
|
25
|
+
core: {},
|
|
26
|
+
log: { info: () => { }, warn: () => { }, error: () => { }, verbose: () => { } },
|
|
27
|
+
wsClient,
|
|
28
|
+
groupCode: getGroupCode(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
21
31
|
async function sendTextToTarget(account, target, text, wsClient) {
|
|
22
32
|
const minCtx = wsClient
|
|
23
33
|
? {
|
|
@@ -28,13 +38,11 @@ async function sendTextToTarget(account, target, text, wsClient) {
|
|
|
28
38
|
wsClient,
|
|
29
39
|
}
|
|
30
40
|
: undefined;
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
const { chatType, target: targetId } = parseTarget(target, account.accountId);
|
|
42
|
+
if (chatType === ChatType.GROUP) {
|
|
43
|
+
return sendYuanbaoGroupMessage({ account, groupCode: targetId, text, fromAccount: account.botId, ctx: minCtx });
|
|
33
44
|
}
|
|
34
|
-
|
|
35
|
-
return sendYuanbaoMessage({ account, toAccount: target.slice('direct:'.length), text, fromAccount: account.botId, ctx: minCtx });
|
|
36
|
-
}
|
|
37
|
-
return sendYuanbaoMessage({ account, toAccount: target, text, fromAccount: account.botId, ctx: minCtx });
|
|
45
|
+
return sendYuanbaoMessage({ account, toAccount: targetId, text, fromAccount: account.botId, ctx: minCtx });
|
|
38
46
|
}
|
|
39
47
|
const meta = {
|
|
40
48
|
id: 'yuanbao',
|
|
@@ -157,8 +165,8 @@ export const yuanbaoPlugin = {
|
|
|
157
165
|
streaming: {
|
|
158
166
|
blockStreamingChunkMaxChars: 3000,
|
|
159
167
|
blockStreamingCoalesceDefaults: {
|
|
160
|
-
minChars:
|
|
161
|
-
idleMs:
|
|
168
|
+
minChars: 2800,
|
|
169
|
+
idleMs: 5000,
|
|
162
170
|
},
|
|
163
171
|
},
|
|
164
172
|
outbound: {
|
|
@@ -166,42 +174,39 @@ export const yuanbaoPlugin = {
|
|
|
166
174
|
chunkerMode: 'markdown',
|
|
167
175
|
textChunkLimit: 3000,
|
|
168
176
|
chunker: (text, limit) => getYuanbaoRuntime()?.channel.text.chunkMarkdownText(text, limit) ?? [text],
|
|
169
|
-
sendText: async (
|
|
177
|
+
sendText: async (params) => {
|
|
178
|
+
const slog = createLog('channel.utbound');
|
|
179
|
+
const { cfg, accountId, to: _to, text } = params;
|
|
170
180
|
const to = _to.replace(/^yuanbao:/, '');
|
|
171
181
|
const account = resolveYuanbaoAccount({ cfg, accountId: accountId ?? undefined });
|
|
172
|
-
|
|
173
|
-
if (dmTarget?.kind === 'user') {
|
|
174
|
-
const dmResult = await sendDM(to, text, { account });
|
|
175
|
-
return {
|
|
176
|
-
channel: 'yuanbao',
|
|
177
|
-
ok: dmResult.ok,
|
|
178
|
-
messageId: dmResult.messageId ?? '',
|
|
179
|
-
error: dmResult.error
|
|
180
|
-
? new Error('detail' in dmResult.error ? dmResult.error.detail : dmResult.error.kind)
|
|
181
|
-
: undefined,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
182
|
+
slog.info('sendText', { accountId, to });
|
|
184
183
|
const wsClient = getActiveWsClient(account.accountId);
|
|
185
184
|
if (!wsClient) {
|
|
186
185
|
return { channel: 'yuanbao', ok: false, messageId: '', error: new Error(`WebSocket client not connected for account ${account.accountId}`) };
|
|
187
186
|
}
|
|
188
187
|
const queueManager = getOutboundQueue(account.accountId);
|
|
189
188
|
if (queueManager) {
|
|
190
|
-
const
|
|
191
|
-
const session = queueManager.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
189
|
+
const { chatType, target, sessionKey } = parseTarget(to, account.accountId);
|
|
190
|
+
const session = queueManager.getOrCreateSession(sessionKey, {
|
|
191
|
+
chatType,
|
|
192
|
+
account,
|
|
193
|
+
target,
|
|
194
|
+
fromAccount: account.botId,
|
|
195
|
+
ctx: buildMinCtx(account, wsClient),
|
|
196
|
+
});
|
|
197
|
+
await session.push({ type: 'text', text });
|
|
198
|
+
await session.flush();
|
|
199
|
+
return { channel: 'yuanbao', ok: true, messageId: '' };
|
|
197
200
|
}
|
|
198
201
|
return toChannelResult(await sendTextToTarget(account, to, text, wsClient));
|
|
199
202
|
},
|
|
200
|
-
sendMedia: async (
|
|
203
|
+
sendMedia: async (params) => {
|
|
204
|
+
const slog = createLog('channel.outbound');
|
|
205
|
+
const { cfg, accountId, to: _to, mediaUrl, text, mediaLocalRoots } = params;
|
|
201
206
|
const to = _to.replace(/^yuanbao:/, '');
|
|
202
207
|
const account = resolveYuanbaoAccount({ cfg, accountId: accountId ?? undefined });
|
|
203
208
|
const wsClient = getActiveWsClient(account.accountId);
|
|
204
|
-
|
|
209
|
+
slog.info('sendMedia', { accountId, to, mediaUrl, text });
|
|
205
210
|
if (!wsClient) {
|
|
206
211
|
return { channel: 'yuanbao', ok: false, messageId: '', error: new Error(`WebSocket client not connected for account ${account.accountId}`) };
|
|
207
212
|
}
|
|
@@ -210,14 +215,20 @@ export const yuanbaoPlugin = {
|
|
|
210
215
|
}
|
|
211
216
|
const queueManager = getOutboundQueue(account.accountId);
|
|
212
217
|
if (queueManager) {
|
|
213
|
-
const
|
|
214
|
-
const session = queueManager.
|
|
215
|
-
|
|
218
|
+
const { chatType, target, sessionKey } = parseTarget(to, account.accountId);
|
|
219
|
+
const session = queueManager.getOrCreateSession(sessionKey, {
|
|
220
|
+
chatType,
|
|
221
|
+
account,
|
|
222
|
+
target,
|
|
223
|
+
fromAccount: account.botId,
|
|
224
|
+
ctx: buildMinCtx(account, wsClient),
|
|
225
|
+
});
|
|
226
|
+
if (text?.trim()) {
|
|
216
227
|
await session.push({ type: 'text', text });
|
|
217
|
-
await session.push({ type: 'media', mediaUrl, mediaLocalRoots });
|
|
218
|
-
return { channel: 'yuanbao', ok: true, messageId: '' };
|
|
219
228
|
}
|
|
220
|
-
|
|
229
|
+
await session.push({ type: 'media', mediaUrl, mediaLocalRoots });
|
|
230
|
+
await session.flush();
|
|
231
|
+
return { channel: 'yuanbao', ok: true, messageId: '' };
|
|
221
232
|
}
|
|
222
233
|
return { channel: 'yuanbao', ok: false, messageId: '', error: new Error('No session found') };
|
|
223
234
|
},
|
|
@@ -44,7 +44,7 @@ export const yuanbaoConfigSchema = {
|
|
|
44
44
|
title: '消息聚合最小字符数',
|
|
45
45
|
description: 'merge-text 策略下,缓冲区积累到此字符数后触发发送',
|
|
46
46
|
minimum: 1,
|
|
47
|
-
default:
|
|
47
|
+
default: 2800,
|
|
48
48
|
},
|
|
49
49
|
maxChars: {
|
|
50
50
|
type: 'integer',
|
|
@@ -58,7 +58,7 @@ export const yuanbaoConfigSchema = {
|
|
|
58
58
|
title: '空闲自动发送超时 (ms)',
|
|
59
59
|
description: 'merge-text 策略下,超过该时长无新内容时自动发送缓冲区',
|
|
60
60
|
minimum: 0,
|
|
61
|
-
default:
|
|
61
|
+
default: 5000,
|
|
62
62
|
},
|
|
63
63
|
mediaMaxMb: {
|
|
64
64
|
type: 'number',
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export interface CachedUserEntry {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
handle?: string;
|
|
2
|
+
userId: string;
|
|
3
|
+
nickName?: string;
|
|
5
4
|
}
|
|
6
5
|
export declare function getCachedMember(key: string): CachedUserEntry | undefined;
|
|
7
6
|
export declare function cacheMember(key: string, entry: CachedUserEntry): void;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { CachedUserEntry } from './directory-cache.js';
|
|
2
2
|
export interface DirectoryEntry {
|
|
3
3
|
kind: 'user' | 'group';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
handle?: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
nickName: string;
|
|
7
6
|
}
|
|
8
7
|
export declare function resolveUsername(nameOrHandle: string, accountId: string, groupCode?: string): CachedUserEntry | null;
|
|
9
8
|
export declare function listKnownPeers(accountId: string): DirectoryEntry[];
|
package/dist/src/dm/directory.js
CHANGED
|
@@ -19,9 +19,8 @@ export function resolveUsername(nameOrHandle, accountId, groupCode = '') {
|
|
|
19
19
|
|| u.userId.toLowerCase() === query.toLowerCase());
|
|
20
20
|
const best = exactMatch ?? results[0];
|
|
21
21
|
const entry = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
handle: best.nickName,
|
|
22
|
+
userId: best.userId,
|
|
23
|
+
nickName: best.nickName,
|
|
25
24
|
};
|
|
26
25
|
cacheMember(query, entry);
|
|
27
26
|
cacheMember(best.nickName, entry);
|
|
@@ -44,9 +43,8 @@ export function listKnownPeers(accountId) {
|
|
|
44
43
|
seen.add(u.userId);
|
|
45
44
|
entries.push({
|
|
46
45
|
kind: 'user',
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
handle: u.nickName,
|
|
46
|
+
userId: u.userId,
|
|
47
|
+
nickName: u.nickName,
|
|
50
48
|
});
|
|
51
49
|
}
|
|
52
50
|
}
|
|
@@ -52,7 +52,7 @@ export async function handleAction(ctx) {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
if (target.kind === 'user') {
|
|
55
|
-
|
|
55
|
+
log.info('处理 DM 发送', { to, targetId: target.id, senderId: ctx.requesterSenderId });
|
|
56
56
|
const senderId = ctx.requesterSenderId ?? '';
|
|
57
57
|
const accessResult = enforceDMAccess(senderId, target.id, message.length, DEFAULT_DM_ACCESS_POLICY);
|
|
58
58
|
if (!accessResult.allowed) {
|
package/dist/src/dm/send-dm.js
CHANGED
package/dist/src/logger.js
CHANGED
package/dist/src/media.d.ts
CHANGED
package/dist/src/media.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { recordPendingHistoryEntryIfEnabled, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, resolveControlCommandGate, } from 'openclaw/plugin-sdk/mattermost';
|
|
2
|
-
import { downloadMediasToLocalFiles
|
|
2
|
+
import { downloadMediasToLocalFiles } from '../media.js';
|
|
3
3
|
import { resolveOutboundSenderAccount, rewriteSlashCommand, YUANBAO_FINAL_TEXT_CHUNK_LIMIT, } from './context.js';
|
|
4
4
|
import { extractTextFromMsgBody } from './extract.js';
|
|
5
|
-
import { sendYuanbaoMessage, sendYuanbaoMessageBody,
|
|
5
|
+
import { sendYuanbaoMessage, sendYuanbaoMessageBody, sendYuanbaoGroupMessageBody, executeReply, } from './outbound.js';
|
|
6
6
|
import { buildOutboundMsgBody, prepareOutboundContent } from './handlers/index.js';
|
|
7
7
|
import { parseQuoteFromCloudCustomData, formatQuoteContext } from './quote.js';
|
|
8
8
|
import { getMember } from '../module/member.js';
|
|
9
|
-
import { createLog
|
|
9
|
+
import { createLog } from '../logger.js';
|
|
10
10
|
import { getOutboundQueue } from '../outbound-queue.js';
|
|
11
11
|
import { UPGRADE_COMMAND_NAMES } from '../commands/upgrade.js';
|
|
12
|
-
import { sendStickerYuanbao } from '../sticker/sticker-sender.js';
|
|
13
|
-
import { getCachedSticker } from '../sticker/sticker-cache.js';
|
|
14
12
|
import { dispatchSystemCallback } from './system-callbacks.js';
|
|
15
13
|
import { chatHistories, chatMediaHistories, recordMediaHistory, } from './chat-history.js';
|
|
16
14
|
import './callbacks/recall.js';
|
|
15
|
+
import { setGroupCode } from '../targets.js';
|
|
17
16
|
const conversationQueues = new Map();
|
|
18
17
|
function enqueueForConversation(key, task) {
|
|
19
18
|
const prev = conversationQueues.get(key) ?? Promise.resolve();
|
|
@@ -114,6 +113,7 @@ async function handleC2CMessage(params) {
|
|
|
114
113
|
const { core, config, account } = ctx;
|
|
115
114
|
if (msg.private_from_group_code) {
|
|
116
115
|
ctx.groupCode = msg.private_from_group_code;
|
|
116
|
+
setGroupCode(ctx.groupCode);
|
|
117
117
|
}
|
|
118
118
|
const fromAccount = msg.from_account?.trim() || 'unknown';
|
|
119
119
|
const senderNickname = msg.sender_nickname?.trim() || undefined;
|
|
@@ -125,7 +125,6 @@ async function handleC2CMessage(params) {
|
|
|
125
125
|
}
|
|
126
126
|
const { rawBody, medias } = extractTextFromMsgBody(ctx, msg.msg_body);
|
|
127
127
|
log.info(`收到消息 <- ${fromAccount}${senderNickname ? `(${senderNickname})` : ''}, msgKey: ${msg.msg_key}`);
|
|
128
|
-
log.debug('消息内容', { user_input: rawBody });
|
|
129
128
|
const quoteInfo = parseQuoteFromCloudCustomData(msg.cloud_custom_data);
|
|
130
129
|
if (quoteInfo) {
|
|
131
130
|
log.info(`检测到引用消息, 引用来自: ${quoteInfo.sender_nickname || quoteInfo.sender_id || 'unknown'}`);
|
|
@@ -249,11 +248,7 @@ async function handleC2CMessage(params) {
|
|
|
249
248
|
log.error('failed updating session meta', { error: String(err) });
|
|
250
249
|
},
|
|
251
250
|
});
|
|
252
|
-
const tableMode =
|
|
253
|
-
cfg: config,
|
|
254
|
-
channel: 'yuanbao',
|
|
255
|
-
accountId: account.accountId,
|
|
256
|
-
});
|
|
251
|
+
const tableMode = 'off';
|
|
257
252
|
const finalTextChunkLimit = core.channel.text.resolveTextChunkLimit(config, 'yuanbao', account.accountId, {
|
|
258
253
|
fallbackLimit: YUANBAO_FINAL_TEXT_CHUNK_LIMIT,
|
|
259
254
|
});
|
|
@@ -285,38 +280,6 @@ async function handleC2CMessage(params) {
|
|
|
285
280
|
const msgId = msg.msg_id ?? String(msg.msg_seq ?? '');
|
|
286
281
|
const queueManager = getOutboundQueue(account.accountId);
|
|
287
282
|
if (queueManager) {
|
|
288
|
-
const sendMediaOverride = async (url, fallbackText, mediaType, mediaLocalRoots) => {
|
|
289
|
-
if (mediaType === 'sticker') {
|
|
290
|
-
const sticker = getCachedSticker(url);
|
|
291
|
-
if (!sticker) {
|
|
292
|
-
return { ok: false, error: `sticker not found: ${url}` };
|
|
293
|
-
}
|
|
294
|
-
return sendStickerYuanbao({
|
|
295
|
-
account,
|
|
296
|
-
config,
|
|
297
|
-
wsClient: ctx.wsClient,
|
|
298
|
-
toAccount: fromAccount,
|
|
299
|
-
sticker,
|
|
300
|
-
core,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
try {
|
|
304
|
-
logger.info('sendMediaOverride', { url, mediaLocalRoots });
|
|
305
|
-
const uploadResult = await downloadAndUploadMedia(url, core, account, mediaLocalRoots);
|
|
306
|
-
const mime = guessMimeType(uploadResult.filename);
|
|
307
|
-
const msgBody = mime.startsWith('image/')
|
|
308
|
-
? buildImageMsgBody({ url: uploadResult.url, filename: uploadResult.filename, size: uploadResult.size, uuid: uploadResult.uuid, imageInfo: uploadResult.imageInfo })
|
|
309
|
-
: buildFileMsgBody({ url: uploadResult.url, filename: uploadResult.filename, size: uploadResult.size });
|
|
310
|
-
const result = await sendMsgBodyDirect({ account, config, target: fromAccount, msgBody: msgBody, wsClient: ctx.wsClient, core });
|
|
311
|
-
return { ok: result.ok, error: result.error };
|
|
312
|
-
}
|
|
313
|
-
catch (err) {
|
|
314
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
315
|
-
log.error(`sendMediaOverride 失败: ${errMsg}`);
|
|
316
|
-
const fallback = fallbackText ? `${fallbackText}\n${url}` : url;
|
|
317
|
-
return sendYuanbaoMessage({ account, toAccount: fromAccount, text: fallback, fromAccount: outboundSender, ctx });
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
283
|
queueManager.registerSession(outboundSessionKey, {
|
|
321
284
|
msgId,
|
|
322
285
|
chatType: 'c2c',
|
|
@@ -325,7 +288,6 @@ async function handleC2CMessage(params) {
|
|
|
325
288
|
toAccount: fromAccount,
|
|
326
289
|
fromAccount: outboundSender,
|
|
327
290
|
ctx,
|
|
328
|
-
sendMediaOverride,
|
|
329
291
|
mergeOnFlush: account.disableBlockStreaming,
|
|
330
292
|
});
|
|
331
293
|
log.debug(`[${outboundSessionKey}] 出站队列 session 已注册,msgId: ${msgId}`);
|
|
@@ -356,19 +318,13 @@ async function handleGroupMessage(params) {
|
|
|
356
318
|
const senderNickname = msg.sender_nickname?.trim() || undefined;
|
|
357
319
|
const outboundSender = resolveOutboundSenderAccount(account);
|
|
358
320
|
const glog = createLog('inbound', ctx.log);
|
|
321
|
+
setGroupCode(groupCode);
|
|
359
322
|
if (outboundSender && fromAccount === outboundSender) {
|
|
360
323
|
glog.info('跳过机器人自身消息', { groupCode, fromAccount });
|
|
361
324
|
return;
|
|
362
325
|
}
|
|
363
326
|
const { rawBody, isAtBot, medias, mentions } = extractTextFromMsgBody(ctx, msg.msg_body);
|
|
364
|
-
glog.info('
|
|
365
|
-
user_input: rawBody,
|
|
366
|
-
groupCode,
|
|
367
|
-
fromAccount,
|
|
368
|
-
senderNickname,
|
|
369
|
-
msgSeq: msg.msg_seq,
|
|
370
|
-
isAtBot,
|
|
371
|
-
});
|
|
327
|
+
glog.info(`收到群消息 <- group:${groupCode}, from: ${fromAccount}${senderNickname ? `(${senderNickname})` : ''}, msgSeq: ${msg.msg_seq}, isAtBot: ${isAtBot}`);
|
|
372
328
|
const quoteInfo = parseQuoteFromCloudCustomData(msg.cloud_custom_data);
|
|
373
329
|
if (quoteInfo) {
|
|
374
330
|
glog.info(`群消息检测到引用消息, 引用来自: ${quoteInfo.sender_nickname || quoteInfo.sender_id || 'unknown'}`);
|
|
@@ -566,11 +522,7 @@ async function handleGroupMessage(params) {
|
|
|
566
522
|
glog.error('failed updating group session meta', { error: String(err) });
|
|
567
523
|
},
|
|
568
524
|
});
|
|
569
|
-
const tableMode =
|
|
570
|
-
cfg: config,
|
|
571
|
-
channel: 'yuanbao',
|
|
572
|
-
accountId: account.accountId,
|
|
573
|
-
});
|
|
525
|
+
const tableMode = 'off';
|
|
574
526
|
const finalTextChunkLimit = core.channel.text.resolveTextChunkLimit(config, 'yuanbao', account.accountId, {
|
|
575
527
|
fallbackLimit: YUANBAO_FINAL_TEXT_CHUNK_LIMIT,
|
|
576
528
|
});
|
|
@@ -609,48 +561,6 @@ async function handleGroupMessage(params) {
|
|
|
609
561
|
const groupMsgId = msg.msg_id ?? String(msg.msg_seq ?? '');
|
|
610
562
|
const groupQueueManager = getOutboundQueue(account.accountId);
|
|
611
563
|
if (groupQueueManager) {
|
|
612
|
-
const groupSendMediaOverride = async (url, fallbackText, mediaType, mediaLocalRoots) => {
|
|
613
|
-
if (mediaType === 'sticker') {
|
|
614
|
-
const sticker = await getCachedSticker(url);
|
|
615
|
-
if (!sticker) {
|
|
616
|
-
return { ok: false, error: `sticker not found: ${url}` };
|
|
617
|
-
}
|
|
618
|
-
return sendStickerYuanbao({ account, config, wsClient: ctx.wsClient, toAccount: `group:${groupCode}`, sticker, refMsgId, core });
|
|
619
|
-
}
|
|
620
|
-
try {
|
|
621
|
-
logger.info('groupSendMediaOverride', { url, mediaLocalRoots });
|
|
622
|
-
const uploadResult = await downloadAndUploadMedia(url, core, account, mediaLocalRoots);
|
|
623
|
-
const mime = guessMimeType(uploadResult.filename);
|
|
624
|
-
const msgBody = mime.startsWith('image/')
|
|
625
|
-
? buildImageMsgBody({ url: uploadResult.url, filename: uploadResult.filename, size: uploadResult.size, uuid: uploadResult.uuid, imageInfo: uploadResult.imageInfo })
|
|
626
|
-
: buildFileMsgBody({ url: uploadResult.url, filename: uploadResult.filename, size: uploadResult.size });
|
|
627
|
-
const result = await sendMsgBodyDirect({
|
|
628
|
-
account,
|
|
629
|
-
config,
|
|
630
|
-
target: `group:${groupCode}`,
|
|
631
|
-
msgBody: msgBody,
|
|
632
|
-
wsClient: ctx.wsClient,
|
|
633
|
-
core,
|
|
634
|
-
refMsgId,
|
|
635
|
-
refFromAccount: fromAccount,
|
|
636
|
-
});
|
|
637
|
-
return { ok: result.ok, error: result.error };
|
|
638
|
-
}
|
|
639
|
-
catch (err) {
|
|
640
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
641
|
-
glog.error(`群 sendMediaOverride 失败: ${errMsg}`);
|
|
642
|
-
const fallback = fallbackText ? `${fallbackText}\n${url}` : url;
|
|
643
|
-
return sendYuanbaoGroupMessage({
|
|
644
|
-
account,
|
|
645
|
-
groupCode,
|
|
646
|
-
text: fallback,
|
|
647
|
-
fromAccount: outboundSender,
|
|
648
|
-
refMsgId,
|
|
649
|
-
refFromAccount: fromAccount,
|
|
650
|
-
ctx,
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
564
|
groupQueueManager.registerSession(outboundGroupSessionKey, {
|
|
655
565
|
msgId: groupMsgId,
|
|
656
566
|
chatType: 'group',
|
|
@@ -661,7 +571,6 @@ async function handleGroupMessage(params) {
|
|
|
661
571
|
refMsgId,
|
|
662
572
|
refFromAccount: fromAccount,
|
|
663
573
|
ctx,
|
|
664
|
-
sendMediaOverride: groupSendMediaOverride,
|
|
665
574
|
mergeOnFlush: account.disableBlockStreaming,
|
|
666
575
|
});
|
|
667
576
|
glog.debug(`[${outboundGroupSessionKey}] 群出站队列 session 已注册,msgId: ${groupMsgId}`);
|
|
@@ -5,6 +5,7 @@ import { getMember } from '../module/member.js';
|
|
|
5
5
|
import { YUANBAO_OVERFLOW_NOTICE_TEXT, } from './context.js';
|
|
6
6
|
import { getOutboundQueue } from '../outbound-queue.js';
|
|
7
7
|
import { InMemoryTtlDb } from '../utils/ttl-db.js';
|
|
8
|
+
import { ChatType, getGroupCode, parseTarget } from '../targets.js';
|
|
8
9
|
const firstReplyRefDb = new InMemoryTtlDb({
|
|
9
10
|
ttlMs: 60 * 1000,
|
|
10
11
|
maxKeys: 100,
|
|
@@ -104,37 +105,32 @@ export async function sendYuanbaoGroupMessage(params) {
|
|
|
104
105
|
}
|
|
105
106
|
export async function sendMsgBodyDirect(params) {
|
|
106
107
|
const { account, config, target, msgBody, wsClient, core, refMsgId, refFromAccount } = params;
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
const { chatType, target: targetId } = parseTarget(target, account.accountId);
|
|
109
|
+
const minCtx = {
|
|
110
|
+
account,
|
|
111
|
+
config,
|
|
112
|
+
core,
|
|
113
|
+
log: { info: () => { }, warn: () => { }, error: () => { }, verbose: () => { } },
|
|
114
|
+
wsClient,
|
|
115
|
+
groupCode: getGroupCode(),
|
|
116
|
+
};
|
|
117
|
+
if (chatType === ChatType.GROUP) {
|
|
109
118
|
return sendYuanbaoGroupMessageBody({
|
|
110
119
|
account,
|
|
111
|
-
groupCode,
|
|
120
|
+
groupCode: targetId,
|
|
112
121
|
msgBody,
|
|
113
122
|
fromAccount: account.botId,
|
|
114
123
|
refMsgId,
|
|
115
124
|
refFromAccount,
|
|
116
|
-
ctx:
|
|
117
|
-
account,
|
|
118
|
-
config,
|
|
119
|
-
core,
|
|
120
|
-
log: { info: () => { }, warn: () => { }, error: () => { }, verbose: () => { } },
|
|
121
|
-
wsClient,
|
|
122
|
-
},
|
|
125
|
+
ctx: minCtx,
|
|
123
126
|
});
|
|
124
127
|
}
|
|
125
|
-
const toAccount = target?.startsWith('direct:') ? target.slice('direct:'.length) : target;
|
|
126
128
|
return sendYuanbaoMessageBody({
|
|
127
129
|
account,
|
|
128
|
-
toAccount,
|
|
130
|
+
toAccount: targetId,
|
|
129
131
|
msgBody,
|
|
130
132
|
fromAccount: account.botId,
|
|
131
|
-
ctx:
|
|
132
|
-
account,
|
|
133
|
-
config,
|
|
134
|
-
core,
|
|
135
|
-
log: { info: () => { }, warn: () => { }, error: () => { }, verbose: () => { } },
|
|
136
|
-
wsClient,
|
|
137
|
-
},
|
|
133
|
+
ctx: minCtx,
|
|
138
134
|
});
|
|
139
135
|
}
|
|
140
136
|
export async function executeReply(params) {
|
|
@@ -151,7 +147,7 @@ export async function executeReply(params) {
|
|
|
151
147
|
: null;
|
|
152
148
|
const collectedTexts = [];
|
|
153
149
|
let hasFinalInfo = false;
|
|
154
|
-
let
|
|
150
|
+
let prevDeliverKind = null;
|
|
155
151
|
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
156
152
|
ctx: ctxPayload,
|
|
157
153
|
cfg: replyRuntime.config,
|
|
@@ -170,22 +166,24 @@ export async function executeReply(params) {
|
|
|
170
166
|
rlog.warn(`[${account.accountId}] 回复已中止,停止处理后续回复块`);
|
|
171
167
|
return;
|
|
172
168
|
}
|
|
169
|
+
rlog.info('[OpenClaw] 收到回复数据', { kind: info.kind, model_output: payload.text });
|
|
173
170
|
if (hasFinalInfo) {
|
|
174
171
|
rlog.warn(`[${account.accountId}] 出现多次final回复,忽略后续回复 ${payload.text}`);
|
|
175
172
|
}
|
|
176
173
|
if (info.kind === 'final') {
|
|
177
174
|
hasFinalInfo = true;
|
|
178
175
|
}
|
|
176
|
+
const prevKind = prevDeliverKind;
|
|
177
|
+
prevDeliverKind = info.kind;
|
|
179
178
|
const text = core.channel.text.convertMarkdownTables(payload.text ?? '', tableMode);
|
|
180
179
|
if (session) {
|
|
181
180
|
if (text.trim()) {
|
|
182
|
-
|
|
183
|
-
await session.push({ type: 'text', text });
|
|
181
|
+
const isAfterToolCall = info.kind === 'block' && prevKind !== null && prevKind !== 'block';
|
|
182
|
+
await session.push({ type: 'text', text: isAfterToolCall ? `\n\n${text}` : text });
|
|
184
183
|
}
|
|
185
184
|
const mediaUrls = payload.mediaUrls ?? [];
|
|
186
185
|
for (const mediaUrl of mediaUrls) {
|
|
187
186
|
if (mediaUrl) {
|
|
188
|
-
hasQueuedContent = true;
|
|
189
187
|
await session.push({ type: 'media', mediaUrl });
|
|
190
188
|
}
|
|
191
189
|
}
|
|
@@ -206,8 +204,8 @@ export async function executeReply(params) {
|
|
|
206
204
|
},
|
|
207
205
|
});
|
|
208
206
|
if (session) {
|
|
209
|
-
await session.flush();
|
|
210
|
-
if (!
|
|
207
|
+
const hasSentContent = await session.flush();
|
|
208
|
+
if (!hasSentContent) {
|
|
211
209
|
const { fallbackReply } = account;
|
|
212
210
|
if (fallbackReply) {
|
|
213
211
|
rlog.info(`[${L}] AI 未返回回复内容(队列模式),使用兜底回复`);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { searchStickers } from '../sticker/sticker-cache.js';
|
|
2
2
|
import { getOutboundQueue } from '../outbound-queue.js';
|
|
3
3
|
import { logger } from '../logger.js';
|
|
4
|
+
import { getYuanbaoRuntime } from '../runtime.js';
|
|
5
|
+
import { getGroupCode, parseTarget } from '../targets.js';
|
|
4
6
|
function normalizeStickerSearchQuery(params) {
|
|
5
7
|
const raw = params.query ?? params.keyword ?? params.q ?? params.text ?? params.search;
|
|
6
8
|
if (typeof raw === 'string') {
|
|
@@ -47,14 +49,29 @@ export async function handleYuanbaoAction(action, params, context) {
|
|
|
47
49
|
return { ok: false, error: 'sticker_id is required' };
|
|
48
50
|
}
|
|
49
51
|
const queueManager = getOutboundQueue(context.account.accountId);
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const
|
|
52
|
+
const to = typeof params.to === 'string' && params.to.trim() ? params.to.trim() : context.toAccount;
|
|
53
|
+
const { chatType, target, sessionKey } = parseTarget(to, context.account.accountId);
|
|
54
|
+
const minCtx = {
|
|
55
|
+
account: context.account,
|
|
56
|
+
config: context.config,
|
|
57
|
+
core: getYuanbaoRuntime(),
|
|
58
|
+
log: { info: () => { }, warn: () => { }, error: () => { }, verbose: () => { } },
|
|
59
|
+
wsClient: context.wsClient,
|
|
60
|
+
groupCode: getGroupCode(),
|
|
61
|
+
};
|
|
62
|
+
const session = queueManager?.getOrCreateSession(sessionKey, {
|
|
63
|
+
chatType,
|
|
64
|
+
account: context.account,
|
|
65
|
+
target,
|
|
66
|
+
fromAccount: context.account.botId,
|
|
67
|
+
ctx: minCtx,
|
|
68
|
+
});
|
|
53
69
|
if (!session) {
|
|
54
70
|
logger.debug(`sendMedia: 未找到已有 session,基于 to 创建新 session 作为 fallback: ${context.toAccount}`);
|
|
55
71
|
return { ok: false, error: `未找到已有 session,基于 to 创建新 session 作为 fallback: ${context.toAccount}` };
|
|
56
72
|
}
|
|
57
73
|
await session.push({ type: 'sticker', sticker_id: stickerId, text: '' });
|
|
74
|
+
await session.flush();
|
|
58
75
|
return { ok: true };
|
|
59
76
|
}
|
|
60
77
|
default:
|
|
@@ -3,5 +3,8 @@ export function buildMessageToolHints() {
|
|
|
3
3
|
'Sticker/react/贴纸 fully supported, no extra setup. react = sticker = 发贴纸 (NOT a message reaction). Use sticker-search then sticker/react. No bare Unicode emoji.',
|
|
4
4
|
'File/image sending is supported. Use media/mediaUrls with real URLs or absolute paths, not link-only text.',
|
|
5
5
|
'IMPORTANT: When sending files, always use absolute paths (e.g. /tmp/file.md). Never use relative paths like "hello.md" — they will fail.',
|
|
6
|
+
'- Proactive `send` to another user: `to="<userId>"`.',
|
|
7
|
+
'- When asked to send a DM, extract the recipient and message from the user\'s request. If either is ambiguous, ask for clarification before calling the tool.',
|
|
8
|
+
'- To find a user\'s ID, use the query_session_members tool first, then pass the resolved ID to the message tool.',
|
|
6
9
|
];
|
|
7
10
|
}
|
|
@@ -24,11 +24,15 @@ export type SendMediaFn = (mediaUrl: string, text?: string, mediaType?: 'image'
|
|
|
24
24
|
ok: boolean;
|
|
25
25
|
error?: string;
|
|
26
26
|
}>;
|
|
27
|
+
export type SendStickerFn = (stickerId: string, text?: string) => Promise<{
|
|
28
|
+
ok: boolean;
|
|
29
|
+
error?: string;
|
|
30
|
+
}>;
|
|
27
31
|
interface OutboundQueueSession {
|
|
28
32
|
readonly strategy: OutboundQueueStrategy;
|
|
29
33
|
readonly msgId: string;
|
|
30
34
|
push(item: OutboundQueueItem): Promise<void>;
|
|
31
|
-
flush(): Promise<
|
|
35
|
+
flush(): Promise<boolean>;
|
|
32
36
|
abort(): void;
|
|
33
37
|
emitReplyHeartbeat(heartbeat: WsHeartbeatValue): void;
|
|
34
38
|
}
|
|
@@ -42,13 +46,20 @@ interface RegisterSessionOptions {
|
|
|
42
46
|
ctx: MessageHandlerContext;
|
|
43
47
|
msgId: string;
|
|
44
48
|
toAccount?: string;
|
|
45
|
-
sendMediaOverride?: SendMediaFn;
|
|
46
49
|
mergeOnFlush?: boolean;
|
|
47
50
|
}
|
|
51
|
+
export interface LightRegisterSessionOptions {
|
|
52
|
+
chatType: 'c2c' | 'group';
|
|
53
|
+
account: ResolvedYuanbaoAccount;
|
|
54
|
+
target: string;
|
|
55
|
+
fromAccount?: string;
|
|
56
|
+
ctx: MessageHandlerContext;
|
|
57
|
+
}
|
|
48
58
|
interface OutboundQueueManager {
|
|
49
59
|
readonly strategy: OutboundQueueStrategy;
|
|
50
60
|
registerSession(sessionKey: string, options: RegisterSessionOptions): OutboundQueueSession;
|
|
51
61
|
getSession(sessionKey: string): OutboundQueueSession | null;
|
|
62
|
+
getOrCreateSession(sessionKey: string, options: LightRegisterSessionOptions): OutboundQueueSession;
|
|
52
63
|
unregisterSession(sessionKey: string): void;
|
|
53
64
|
}
|
|
54
65
|
export interface OutboundQueueConfig {
|
|
@@ -60,6 +71,7 @@ export interface OutboundQueueConfig {
|
|
|
60
71
|
export declare function initOutboundQueue(accountId: string, config: OutboundQueueConfig): OutboundQueueManager;
|
|
61
72
|
export declare function getOutboundQueue(accountId: string): OutboundQueueManager | null;
|
|
62
73
|
export declare function destroyOutboundQueue(accountId: string): void;
|
|
74
|
+
export declare function endsWithTableRow(text: string): boolean;
|
|
63
75
|
export declare function hasUnclosedFence(text: string): boolean;
|
|
64
76
|
export declare function mergeBlockStreamingFences(buffer: string, incoming: string): string;
|
|
65
77
|
export interface MergeTextOptions {
|
|
@@ -69,6 +81,7 @@ export interface MergeTextOptions {
|
|
|
69
81
|
}
|
|
70
82
|
declare function createMergeTextSession(callbacks: {
|
|
71
83
|
sendText: SendTextFn;
|
|
84
|
+
sendSticker: SendStickerFn;
|
|
72
85
|
sendMedia: SendMediaFn;
|
|
73
86
|
}, msgId: string, sessionKey: string, onComplete: () => void, log: ModuleLog, opts: MergeTextOptions, heartbeatMeta: {
|
|
74
87
|
ctx: MessageHandlerContext;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createLog } from './logger.js';
|
|
2
|
-
import { sendYuanbaoMessage, sendYuanbaoGroupMessage } from './message-handler/outbound.js';
|
|
2
|
+
import { sendYuanbaoMessage, sendYuanbaoGroupMessage, sendMsgBodyDirect } from './message-handler/outbound.js';
|
|
3
|
+
import { getCachedSticker } from './sticker/sticker-cache.js';
|
|
4
|
+
import { sendStickerYuanbao } from './sticker/sticker-sender.js';
|
|
5
|
+
import { getYuanbaoRuntime } from './runtime.js';
|
|
6
|
+
import { buildFileMsgBody, buildImageMsgBody, downloadAndUploadMedia, guessMimeType } from './media.js';
|
|
3
7
|
import { createReplyHeartbeatController } from './module/reply-heartbeat.js';
|
|
4
8
|
const activeManagers = new Map();
|
|
5
9
|
export function initOutboundQueue(accountId, config) {
|
|
@@ -31,12 +35,13 @@ function defaultChunkText(text, max) {
|
|
|
31
35
|
function createManager(config) {
|
|
32
36
|
const { strategy } = config;
|
|
33
37
|
const mergeTextOpts = {
|
|
34
|
-
minChars: config.minChars ??
|
|
38
|
+
minChars: config.minChars ?? 2800,
|
|
35
39
|
maxChars: config.maxChars ?? 3000,
|
|
36
40
|
chunkText: config.chunkText ?? defaultChunkText,
|
|
37
41
|
};
|
|
38
42
|
const sessions = new Map();
|
|
39
43
|
const log = createLog('outbound-queue');
|
|
44
|
+
const core = getYuanbaoRuntime();
|
|
40
45
|
return {
|
|
41
46
|
strategy,
|
|
42
47
|
registerSession(sessionKey, options) {
|
|
@@ -46,10 +51,11 @@ function createManager(config) {
|
|
|
46
51
|
existing.abort();
|
|
47
52
|
}
|
|
48
53
|
const onComplete = () => sessions.delete(sessionKey);
|
|
49
|
-
const { chatType, account, target, fromAccount, refMsgId, refFromAccount, ctx, msgId,
|
|
54
|
+
const { chatType, account, target, fromAccount, refMsgId, refFromAccount, ctx, msgId, mergeOnFlush, toAccount, } = options;
|
|
50
55
|
const heartbeatTarget = (toAccount ?? (chatType === 'c2c' ? target : '')).trim();
|
|
51
56
|
const heartbeatGroupCode = chatType === 'group' ? target : undefined;
|
|
52
57
|
const sendText = async (text) => {
|
|
58
|
+
log.info('sendText', { chatType, fromAccount, target });
|
|
53
59
|
try {
|
|
54
60
|
const result = chatType === 'group'
|
|
55
61
|
? await sendYuanbaoGroupMessage({
|
|
@@ -65,16 +71,82 @@ function createManager(config) {
|
|
|
65
71
|
return { ok: result.ok, error: result.error };
|
|
66
72
|
}
|
|
67
73
|
catch (err) {
|
|
68
|
-
|
|
74
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
75
|
+
log.error(`sendText failed: ${errMsg}`, { chatType, fromAccount, target });
|
|
76
|
+
return { ok: false, error: errMsg };
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const sendSticker = async (id) => {
|
|
80
|
+
log.info('sendSticker', { chatType, fromAccount, target });
|
|
81
|
+
try {
|
|
82
|
+
const sticker = getCachedSticker(id);
|
|
83
|
+
if (!sticker) {
|
|
84
|
+
return { ok: false, error: `sticker not found: ${id}` };
|
|
85
|
+
}
|
|
86
|
+
const result = await sendStickerYuanbao({
|
|
87
|
+
account,
|
|
88
|
+
config,
|
|
89
|
+
wsClient: ctx.wsClient,
|
|
90
|
+
toAccount: chatType === 'group' ? `group:${target}` : target,
|
|
91
|
+
sticker,
|
|
92
|
+
core,
|
|
93
|
+
refMsgId,
|
|
94
|
+
});
|
|
95
|
+
return { ok: result.ok, error: result.error };
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
99
|
+
log.error(`sendSticker failed: ${errMsg}`, { chatType, fromAccount, target });
|
|
100
|
+
return { ok: false, error: errMsg };
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const sendMedia = async (url, fallbackText) => {
|
|
104
|
+
log.info('sendMedia', { chatType, fromAccount, target });
|
|
105
|
+
const isGroup = chatType === 'group';
|
|
106
|
+
try {
|
|
107
|
+
const uploadResult = await downloadAndUploadMedia(url, core, account);
|
|
108
|
+
const mime = guessMimeType(uploadResult.filename);
|
|
109
|
+
const msgBody = mime.startsWith('image/')
|
|
110
|
+
? buildImageMsgBody({ url: uploadResult.url, filename: uploadResult.filename, size: uploadResult.size, uuid: uploadResult.uuid, imageInfo: uploadResult.imageInfo })
|
|
111
|
+
: buildFileMsgBody({ url: uploadResult.url, filename: uploadResult.filename, size: uploadResult.size, uuid: uploadResult.uuid });
|
|
112
|
+
const result = await sendMsgBodyDirect({
|
|
113
|
+
account,
|
|
114
|
+
config,
|
|
115
|
+
target: isGroup ? `group:${target}` : target,
|
|
116
|
+
msgBody: msgBody,
|
|
117
|
+
wsClient: ctx.wsClient,
|
|
118
|
+
core,
|
|
119
|
+
...(isGroup ? { refMsgId, refFromAccount } : {}),
|
|
120
|
+
});
|
|
121
|
+
return { ok: result.ok, error: result.error };
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
125
|
+
log.error(`sendMedia failed: ${errMsg}`, { chatType, fromAccount, target });
|
|
126
|
+
const fallback = fallbackText ? `${fallbackText}\n${url}` : url;
|
|
127
|
+
return isGroup
|
|
128
|
+
? sendYuanbaoGroupMessage({
|
|
129
|
+
account,
|
|
130
|
+
groupCode: target,
|
|
131
|
+
text: fallback,
|
|
132
|
+
fromAccount: fromAccount || account.botId,
|
|
133
|
+
refMsgId,
|
|
134
|
+
refFromAccount,
|
|
135
|
+
ctx,
|
|
136
|
+
})
|
|
137
|
+
: sendYuanbaoMessage({
|
|
138
|
+
account,
|
|
139
|
+
toAccount: target,
|
|
140
|
+
text: fallback,
|
|
141
|
+
fromAccount: fromAccount || account.botId,
|
|
142
|
+
...(isGroup ? { refMsgId, refFromAccount } : {}),
|
|
143
|
+
ctx,
|
|
144
|
+
});
|
|
69
145
|
}
|
|
70
146
|
};
|
|
71
|
-
const sendMedia = sendMediaOverride ?? (async (mediaUrl, text) => {
|
|
72
|
-
const fallbackText = text ? `${text}\n${mediaUrl}` : mediaUrl;
|
|
73
|
-
return sendText(fallbackText);
|
|
74
|
-
});
|
|
75
147
|
let session;
|
|
76
148
|
if (mergeOnFlush) {
|
|
77
|
-
session = createMergeOnFlushSession({ sendText, sendMedia }, msgId, onComplete, log, {
|
|
149
|
+
session = createMergeOnFlushSession({ sendText, sendSticker, sendMedia }, msgId, onComplete, log, {
|
|
78
150
|
ctx,
|
|
79
151
|
account,
|
|
80
152
|
toAccount: heartbeatTarget,
|
|
@@ -84,7 +156,7 @@ function createManager(config) {
|
|
|
84
156
|
else {
|
|
85
157
|
switch (strategy) {
|
|
86
158
|
case 'immediate':
|
|
87
|
-
session = createImmediateSession({ sendText, sendMedia }, msgId, onComplete, log, {
|
|
159
|
+
session = createImmediateSession({ sendText, sendSticker, sendMedia }, msgId, onComplete, log, {
|
|
88
160
|
ctx,
|
|
89
161
|
account,
|
|
90
162
|
toAccount: heartbeatTarget,
|
|
@@ -92,7 +164,7 @@ function createManager(config) {
|
|
|
92
164
|
});
|
|
93
165
|
break;
|
|
94
166
|
case 'merge-text':
|
|
95
|
-
session = createMergeTextSession({ sendText, sendMedia }, msgId, sessionKey, onComplete, log, mergeTextOpts, {
|
|
167
|
+
session = createMergeTextSession({ sendText, sendSticker, sendMedia }, msgId, sessionKey, onComplete, log, mergeTextOpts, {
|
|
96
168
|
ctx,
|
|
97
169
|
account,
|
|
98
170
|
toAccount: heartbeatTarget,
|
|
@@ -110,15 +182,27 @@ function createManager(config) {
|
|
|
110
182
|
getSession(sessionKey) {
|
|
111
183
|
return sessions.get(sessionKey) ?? null;
|
|
112
184
|
},
|
|
185
|
+
getOrCreateSession(sessionKey, options) {
|
|
186
|
+
log.info(`[${sessionKey}:${options.target}] 获取或创建会话队列`);
|
|
187
|
+
const existing = sessions.get(sessionKey);
|
|
188
|
+
if (existing)
|
|
189
|
+
return existing;
|
|
190
|
+
log.debug(`[${sessionKey}] 自动创建轻量 session(委托 registerSession)`);
|
|
191
|
+
return this.registerSession(sessionKey, {
|
|
192
|
+
...options,
|
|
193
|
+
msgId: `auto-${Date.now()}`,
|
|
194
|
+
});
|
|
195
|
+
},
|
|
113
196
|
unregisterSession(sessionKey) {
|
|
114
197
|
sessions.delete(sessionKey);
|
|
115
198
|
},
|
|
116
199
|
};
|
|
117
200
|
}
|
|
118
201
|
function createImmediateSession(callbacks, msgId, onComplete, log, heartbeatMeta) {
|
|
119
|
-
const { sendText, sendMedia } = callbacks;
|
|
202
|
+
const { sendText, sendSticker, sendMedia } = callbacks;
|
|
120
203
|
let aborted = false;
|
|
121
204
|
let sendChain = Promise.resolve();
|
|
205
|
+
let hasSentContent = false;
|
|
122
206
|
const replyHeartbeat = createReplyHeartbeatController({ meta: heartbeatMeta });
|
|
123
207
|
return {
|
|
124
208
|
strategy: 'immediate',
|
|
@@ -129,23 +213,26 @@ function createImmediateSession(callbacks, msgId, onComplete, log, heartbeatMeta
|
|
|
129
213
|
sendChain = sendChain.then(async () => {
|
|
130
214
|
if (aborted)
|
|
131
215
|
return;
|
|
216
|
+
let result;
|
|
132
217
|
if (item.type === 'text') {
|
|
133
218
|
if (!item.text.trim())
|
|
134
219
|
return;
|
|
135
|
-
|
|
220
|
+
result = await sendText(item.text);
|
|
136
221
|
if (!result.ok) {
|
|
137
222
|
log.error(`immediate 发送文本失败: ${result.error}`);
|
|
138
223
|
}
|
|
139
224
|
else {
|
|
225
|
+
hasSentContent = true;
|
|
140
226
|
replyHeartbeat.onReplySent();
|
|
141
227
|
}
|
|
142
228
|
}
|
|
143
229
|
else if (item.type === 'sticker') {
|
|
144
|
-
const result = await
|
|
230
|
+
const result = await sendSticker(item.sticker_id, item.text);
|
|
145
231
|
if (!result.ok) {
|
|
146
232
|
log.error(`immediate 发送表情失败: ${result.error}`);
|
|
147
233
|
}
|
|
148
234
|
else {
|
|
235
|
+
hasSentContent = true;
|
|
149
236
|
replyHeartbeat.onReplySent();
|
|
150
237
|
}
|
|
151
238
|
}
|
|
@@ -155,9 +242,11 @@ function createImmediateSession(callbacks, msgId, onComplete, log, heartbeatMeta
|
|
|
155
242
|
log.error(`immediate 发送媒体失败: ${result.error}`);
|
|
156
243
|
}
|
|
157
244
|
else {
|
|
245
|
+
hasSentContent = true;
|
|
158
246
|
replyHeartbeat.onReplySent();
|
|
159
247
|
}
|
|
160
248
|
}
|
|
249
|
+
return result;
|
|
161
250
|
});
|
|
162
251
|
return sendChain;
|
|
163
252
|
},
|
|
@@ -165,6 +254,7 @@ function createImmediateSession(callbacks, msgId, onComplete, log, heartbeatMeta
|
|
|
165
254
|
await sendChain;
|
|
166
255
|
replyHeartbeat.stop();
|
|
167
256
|
onComplete();
|
|
257
|
+
return hasSentContent;
|
|
168
258
|
},
|
|
169
259
|
abort() {
|
|
170
260
|
aborted = true;
|
|
@@ -177,10 +267,12 @@ function createImmediateSession(callbacks, msgId, onComplete, log, heartbeatMeta
|
|
|
177
267
|
};
|
|
178
268
|
}
|
|
179
269
|
function createMergeOnFlushSession(callbacks, msgId, onComplete, log, heartbeatMeta) {
|
|
180
|
-
const { sendText, sendMedia } = callbacks;
|
|
270
|
+
const { sendText, sendSticker, sendMedia } = callbacks;
|
|
181
271
|
let aborted = false;
|
|
182
272
|
const collectedTexts = [];
|
|
273
|
+
const collectedStickers = [];
|
|
183
274
|
const collectedMedias = [];
|
|
275
|
+
let hasSentContent = false;
|
|
184
276
|
const replyHeartbeat = createReplyHeartbeatController({ meta: heartbeatMeta });
|
|
185
277
|
return {
|
|
186
278
|
strategy: 'immediate',
|
|
@@ -193,7 +285,7 @@ function createMergeOnFlushSession(callbacks, msgId, onComplete, log, heartbeatM
|
|
|
193
285
|
collectedTexts.push(item.text);
|
|
194
286
|
}
|
|
195
287
|
else if (item.type === 'sticker') {
|
|
196
|
-
|
|
288
|
+
collectedStickers.push({ stickerId: item.sticker_id, text: item.text });
|
|
197
289
|
}
|
|
198
290
|
else {
|
|
199
291
|
collectedMedias.push({ mediaUrl: item.mediaUrl, text: item.text, mediaLocalRoots: item.mediaLocalRoots });
|
|
@@ -202,7 +294,7 @@ function createMergeOnFlushSession(callbacks, msgId, onComplete, log, heartbeatM
|
|
|
202
294
|
},
|
|
203
295
|
async flush() {
|
|
204
296
|
if (aborted)
|
|
205
|
-
return;
|
|
297
|
+
return hasSentContent;
|
|
206
298
|
if (collectedTexts.length > 0) {
|
|
207
299
|
const merged = collectedTexts.join('\n\n');
|
|
208
300
|
collectedTexts.length = 0;
|
|
@@ -212,10 +304,22 @@ function createMergeOnFlushSession(callbacks, msgId, onComplete, log, heartbeatM
|
|
|
212
304
|
log.error(`mergeOnFlush 发送合并文本失败: ${result.error}`);
|
|
213
305
|
}
|
|
214
306
|
else {
|
|
307
|
+
hasSentContent = true;
|
|
215
308
|
replyHeartbeat.onReplySent();
|
|
216
309
|
}
|
|
217
310
|
}
|
|
218
311
|
}
|
|
312
|
+
for (const sticker of collectedStickers) {
|
|
313
|
+
if (aborted)
|
|
314
|
+
break;
|
|
315
|
+
const result = await sendSticker(sticker.stickerId, sticker.text);
|
|
316
|
+
if (!result.ok) {
|
|
317
|
+
log.error(`mergeOnFlush 发送表情失败: ${result.error}`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
hasSentContent = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
219
323
|
for (const media of collectedMedias) {
|
|
220
324
|
if (aborted)
|
|
221
325
|
break;
|
|
@@ -224,12 +328,16 @@ function createMergeOnFlushSession(callbacks, msgId, onComplete, log, heartbeatM
|
|
|
224
328
|
log.error(`mergeOnFlush 发送媒体失败: ${result.error}`);
|
|
225
329
|
}
|
|
226
330
|
else {
|
|
331
|
+
hasSentContent = true;
|
|
227
332
|
replyHeartbeat.onReplySent();
|
|
228
333
|
}
|
|
229
334
|
}
|
|
335
|
+
collectedTexts.length = 0;
|
|
230
336
|
collectedMedias.length = 0;
|
|
337
|
+
collectedStickers.length = 0;
|
|
231
338
|
replyHeartbeat.stop();
|
|
232
339
|
onComplete();
|
|
340
|
+
return hasSentContent;
|
|
233
341
|
},
|
|
234
342
|
abort() {
|
|
235
343
|
aborted = true;
|
|
@@ -243,6 +351,14 @@ function createMergeOnFlushSession(callbacks, msgId, onComplete, log, heartbeatM
|
|
|
243
351
|
},
|
|
244
352
|
};
|
|
245
353
|
}
|
|
354
|
+
export function endsWithTableRow(text) {
|
|
355
|
+
const trimmed = text.trimEnd();
|
|
356
|
+
if (!trimmed)
|
|
357
|
+
return false;
|
|
358
|
+
const lastLine = trimmed.split('\n').at(-1) ?? '';
|
|
359
|
+
const line = lastLine.trim();
|
|
360
|
+
return line.startsWith('|') && line.endsWith('|');
|
|
361
|
+
}
|
|
246
362
|
export function hasUnclosedFence(text) {
|
|
247
363
|
let inFence = false;
|
|
248
364
|
for (const line of text.split('\n')) {
|
|
@@ -264,11 +380,12 @@ export function mergeBlockStreamingFences(buffer, incoming) {
|
|
|
264
380
|
return `${buffer}${normalized}`;
|
|
265
381
|
}
|
|
266
382
|
function createMergeTextSession(callbacks, msgId, sessionKey, onComplete, log, opts, heartbeatMeta) {
|
|
267
|
-
const { sendText, sendMedia } = callbacks;
|
|
268
|
-
const { maxChars, chunkText } = opts;
|
|
383
|
+
const { sendText, sendSticker, sendMedia } = callbacks;
|
|
384
|
+
const { minChars, maxChars, chunkText } = opts;
|
|
269
385
|
let aborted = false;
|
|
270
386
|
let textBuffer = '';
|
|
271
387
|
let sendChain = Promise.resolve();
|
|
388
|
+
let hasSentContent = false;
|
|
272
389
|
const replyHeartbeat = createReplyHeartbeatController({ meta: heartbeatMeta });
|
|
273
390
|
async function drainBuffer(force) {
|
|
274
391
|
if (textBuffer.length === 0)
|
|
@@ -280,13 +397,21 @@ function createMergeTextSession(callbacks, msgId, sessionKey, onComplete, log, o
|
|
|
280
397
|
log.debug(`[${sessionKey}] drainBuffer: single chunk has unclosed fence, keeping in buffer (bufLen=${textBuffer.length})`);
|
|
281
398
|
return;
|
|
282
399
|
}
|
|
400
|
+
if (!force && chunks.length === 1 && endsWithTableRow(chunks[0])) {
|
|
401
|
+
log.debug(`[${sessionKey}] drainBuffer: single chunk ends with table row, keeping in buffer for continuation (bufLen=${textBuffer.length})`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!force && chunks.length === 1 && textBuffer.length < minChars) {
|
|
405
|
+
log.debug(`[${sessionKey}] drainBuffer: bufLen=${textBuffer.length} < minChars=${minChars}, waiting for more content`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
283
408
|
textBuffer = '';
|
|
284
409
|
for (const chunk of chunks) {
|
|
285
410
|
if (aborted)
|
|
286
411
|
return;
|
|
287
412
|
if (!chunk.trim())
|
|
288
413
|
continue;
|
|
289
|
-
log.debug(`[${sessionKey}] emit chunk(force=${force}): len=${chunk.length} head=${JSON.stringify(chunk.slice(0,
|
|
414
|
+
log.debug(`[${sessionKey}] emit chunk(force=${force}): len=${chunk.length} head=${JSON.stringify(chunk.slice(0, 3))} tail=${JSON.stringify(chunk.slice(-3))}`);
|
|
290
415
|
const result = await sendText(chunk);
|
|
291
416
|
if (!result.ok)
|
|
292
417
|
log.error(`[${sessionKey}] send failed: ${result.error}`);
|
|
@@ -301,12 +426,13 @@ function createMergeTextSession(callbacks, msgId, sessionKey, onComplete, log, o
|
|
|
301
426
|
return;
|
|
302
427
|
if (!chunk.trim())
|
|
303
428
|
continue;
|
|
304
|
-
log.debug(`[${sessionKey}] emit chunk(split): len=${chunk.length} head=${JSON.stringify(chunk.slice(0,
|
|
429
|
+
log.debug(`[${sessionKey}] emit chunk(split): len=${chunk.length} head=${JSON.stringify(chunk.slice(0, 3))} tail=${JSON.stringify(chunk.slice(-3))}`);
|
|
305
430
|
const result = await sendText(chunk);
|
|
306
431
|
if (!result.ok) {
|
|
307
432
|
log.error(`[${sessionKey}] merge-text send failed: ${result.error}`);
|
|
308
433
|
}
|
|
309
434
|
else {
|
|
435
|
+
hasSentContent = true;
|
|
310
436
|
replyHeartbeat.onReplySent();
|
|
311
437
|
}
|
|
312
438
|
}
|
|
@@ -324,9 +450,21 @@ function createMergeTextSession(callbacks, msgId, sessionKey, onComplete, log, o
|
|
|
324
450
|
if (item.type === 'text') {
|
|
325
451
|
if (!item.text.trim())
|
|
326
452
|
return;
|
|
327
|
-
|
|
328
|
-
|
|
453
|
+
if (textBuffer) {
|
|
454
|
+
const incoming = hasUnclosedFence(textBuffer) && item.text.startsWith('\n\n')
|
|
455
|
+
? item.text.slice(2)
|
|
456
|
+
: item.text;
|
|
457
|
+
const SENTENCE_END_RE = /[。?!:]\s*$/;
|
|
458
|
+
const needsSeparator = SENTENCE_END_RE.test(textBuffer)
|
|
459
|
+
&& !hasUnclosedFence(textBuffer)
|
|
460
|
+
&& !endsWithTableRow(textBuffer);
|
|
461
|
+
textBuffer = mergeBlockStreamingFences(needsSeparator ? `${textBuffer}\n\n` : textBuffer, incoming);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
textBuffer = item.text;
|
|
465
|
+
}
|
|
329
466
|
log.debug(`[${sessionKey}] merge-text push: bufLen=${textBuffer.length}`);
|
|
467
|
+
hasSentContent = true;
|
|
330
468
|
await drainBuffer(false);
|
|
331
469
|
}
|
|
332
470
|
else {
|
|
@@ -338,7 +476,7 @@ function createMergeTextSession(callbacks, msgId, sessionKey, onComplete, log, o
|
|
|
338
476
|
return;
|
|
339
477
|
let result;
|
|
340
478
|
if (item.type === 'sticker') {
|
|
341
|
-
result = await
|
|
479
|
+
result = await sendSticker(item.sticker_id, item.text);
|
|
342
480
|
}
|
|
343
481
|
else {
|
|
344
482
|
result = await sendMedia(item.mediaUrl, item.text, 'image', item.mediaLocalRoots);
|
|
@@ -347,6 +485,7 @@ function createMergeTextSession(callbacks, msgId, sessionKey, onComplete, log, o
|
|
|
347
485
|
log.error(`[${sessionKey}] merge-text send failed: ${result.error}`);
|
|
348
486
|
}
|
|
349
487
|
else {
|
|
488
|
+
hasSentContent = true;
|
|
350
489
|
replyHeartbeat.onReplySent();
|
|
351
490
|
}
|
|
352
491
|
}
|
|
@@ -357,10 +496,11 @@ function createMergeTextSession(callbacks, msgId, sessionKey, onComplete, log, o
|
|
|
357
496
|
log.debug(`[${sessionKey}] merge-text session flush: bufLen=${textBuffer.length}`);
|
|
358
497
|
await sendChain;
|
|
359
498
|
if (aborted)
|
|
360
|
-
return;
|
|
499
|
+
return hasSentContent;
|
|
361
500
|
await drainBuffer(true);
|
|
362
501
|
replyHeartbeat.stop();
|
|
363
502
|
onComplete();
|
|
503
|
+
return hasSentContent;
|
|
364
504
|
},
|
|
365
505
|
abort() {
|
|
366
506
|
const bufLen = textBuffer.length;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function setGroupCode(code: string): void;
|
|
2
|
+
export declare function getGroupCode(): string;
|
|
3
|
+
export declare function looksLikeYuanbaoId(raw: string): boolean;
|
|
4
|
+
export declare enum ChatType {
|
|
5
|
+
C2C = "c2c",
|
|
6
|
+
GROUP = "group"
|
|
7
|
+
}
|
|
8
|
+
export interface MessagingTarget {
|
|
9
|
+
chatType: ChatType;
|
|
10
|
+
target: string;
|
|
11
|
+
sessionKey: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function parseTarget(to: string, accountId?: string): MessagingTarget;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { resolveUsername } from './dm/directory.js';
|
|
2
|
+
let groupCode = '';
|
|
3
|
+
export function setGroupCode(code) {
|
|
4
|
+
groupCode = code;
|
|
5
|
+
}
|
|
6
|
+
export function getGroupCode() {
|
|
7
|
+
if (!groupCode) {
|
|
8
|
+
throw new Error('GroupCode not initialized');
|
|
9
|
+
}
|
|
10
|
+
return groupCode;
|
|
11
|
+
}
|
|
12
|
+
export function looksLikeYuanbaoId(raw) {
|
|
13
|
+
const trimmed = raw.trim();
|
|
14
|
+
if (trimmed.length < 16) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (trimmed.length % 4 !== 0) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (!/^[A-Za-z0-9+/=]+$/.test(trimmed)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const equalsIndex = trimmed.indexOf('=');
|
|
24
|
+
if (equalsIndex !== -1) {
|
|
25
|
+
if (equalsIndex < trimmed.length - 2 || trimmed.slice(equalsIndex).match(/[^=]/)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
export var ChatType;
|
|
32
|
+
(function (ChatType) {
|
|
33
|
+
ChatType["C2C"] = "c2c";
|
|
34
|
+
ChatType["GROUP"] = "group";
|
|
35
|
+
})(ChatType || (ChatType = {}));
|
|
36
|
+
export function parseTarget(to, accountId = 'default') {
|
|
37
|
+
to = to.trim().replace(/^yuanbao:/, '');
|
|
38
|
+
if (to.startsWith('group:')) {
|
|
39
|
+
return { chatType: ChatType.GROUP, target: to.slice('group:'.length), sessionKey: to };
|
|
40
|
+
}
|
|
41
|
+
to = to.replace(/^user:/, '').replace(/^direct:/, '');
|
|
42
|
+
if (!looksLikeYuanbaoId(to)) {
|
|
43
|
+
const { userId } = resolveUsername(to, accountId, groupCode) || { userId: to };
|
|
44
|
+
return { chatType: ChatType.C2C, target: userId, sessionKey: `direct:${userId}` };
|
|
45
|
+
}
|
|
46
|
+
return { chatType: ChatType.C2C, target: to, sessionKey: `direct:${to}` };
|
|
47
|
+
}
|
package/dist/src/tools/member.js
CHANGED
package/openclaw.plugin.json
CHANGED