openclaw-plugin-yuanbao 2.7.2 → 2.9.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 +73 -38
- package/dist/src/commands/upgrade/upgrade.js +55 -9
- package/dist/src/commands/upgrade/utils.d.ts +3 -1
- package/dist/src/commands/upgrade/utils.js +11 -9
- package/dist/src/config-schema.js +0 -7
- package/dist/src/directory-adapter.d.ts +2 -0
- package/dist/src/directory-adapter.js +58 -0
- package/dist/src/dm/send-dm.js +0 -1
- package/dist/src/logger.d.ts +2 -7
- package/dist/src/logger.js +25 -49
- package/dist/src/media.d.ts +1 -0
- package/dist/src/media.js +134 -4
- package/dist/src/message-handler/callbacks/recall.js +2 -2
- package/dist/src/message-handler/context.d.ts +0 -6
- package/dist/src/message-handler/handlers/custom.js +1 -1
- package/dist/src/message-handler/handlers/index.js +7 -4
- package/dist/src/message-handler/inbound.js +5 -4
- package/dist/src/message-handler/outbound.js +8 -4
- package/dist/src/message-tool/action-runtime.js +0 -1
- package/dist/src/message-tool/hints.js +3 -3
- package/dist/src/messaging-adapter.d.ts +4 -0
- package/dist/src/messaging-adapter.js +31 -0
- package/dist/src/module/member.d.ts +5 -0
- package/dist/src/module/member.js +48 -0
- package/dist/src/outbound-queue.d.ts +0 -12
- package/dist/src/outbound-queue.js +1 -179
- package/dist/src/types.d.ts +7 -1
- package/dist/src/utils/markdown-stream.d.ts +14 -0
- package/dist/src/utils/markdown-stream.js +242 -0
- package/dist/src/utils/markdown-table-sanitize.d.ts +1 -0
- package/dist/src/utils/markdown-table-sanitize.js +89 -0
- package/dist/src/yuanbao-server/http/main.d.ts +3 -3
- package/dist/src/yuanbao-server/http/main.js +4 -4
- package/dist/src/yuanbao-server/http/request.d.ts +5 -5
- package/dist/src/yuanbao-server/http/request.js +23 -23
- package/dist/src/yuanbao-server/ws/client.d.ts +0 -2
- package/dist/src/yuanbao-server/ws/client.js +1 -1
- package/dist/src/yuanbao-server/ws/gateway.d.ts +1 -8
- package/dist/src/yuanbao-server/ws/gateway.js +25 -39
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/src/media.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { createReadStream, statSync, existsSync } from 'node:fs';
|
|
1
|
+
import { createReadStream, statSync, existsSync, openSync, readSync, closeSync } from 'node:fs';
|
|
2
2
|
import { writeFile, mkdir } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir, homedir } from 'node:os';
|
|
4
4
|
import path, { basename, extname, join } from 'node:path';
|
|
5
5
|
import { randomBytes, createHash } from 'node:crypto';
|
|
6
6
|
import { apiGetDownloadUrl, apiGetUploadInfo } from './yuanbao-server/api.js';
|
|
7
|
+
import { logger } from './logger.js';
|
|
7
8
|
const DEFAULT_MAX_MB = 20;
|
|
8
9
|
export const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.heic', '.tiff', '.ico']);
|
|
9
10
|
export function guessMimeType(filename) {
|
|
@@ -148,6 +149,134 @@ function normalizePath(s) {
|
|
|
148
149
|
}
|
|
149
150
|
return p;
|
|
150
151
|
}
|
|
152
|
+
function readMagicBytes(filePath, n = 16) {
|
|
153
|
+
const buf = Buffer.alloc(n);
|
|
154
|
+
const fd = openSync(filePath, 'r');
|
|
155
|
+
try {
|
|
156
|
+
const bytesRead = readSync(fd, buf, 0, n, 0);
|
|
157
|
+
return buf.subarray(0, bytesRead);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
closeSync(fd);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function isMagicJpeg(h) {
|
|
164
|
+
return h.length >= 3 && h[0] === 0xff && h[1] === 0xd8 && h[2] === 0xff;
|
|
165
|
+
}
|
|
166
|
+
function isMagicPng(h) {
|
|
167
|
+
return h.length >= 8
|
|
168
|
+
&& h[0] === 0x89 && h[1] === 0x50 && h[2] === 0x4e && h[3] === 0x47
|
|
169
|
+
&& h[4] === 0x0d && h[5] === 0x0a && h[6] === 0x1a && h[7] === 0x0a;
|
|
170
|
+
}
|
|
171
|
+
function isMagicGif(h) {
|
|
172
|
+
if (h.length < 6)
|
|
173
|
+
return false;
|
|
174
|
+
const sig = h.toString('ascii', 0, 6);
|
|
175
|
+
return sig === 'GIF87a' || sig === 'GIF89a';
|
|
176
|
+
}
|
|
177
|
+
function isMagicWebp(h) {
|
|
178
|
+
return h.length >= 12
|
|
179
|
+
&& h.toString('ascii', 0, 4) === 'RIFF'
|
|
180
|
+
&& h.toString('ascii', 8, 12) === 'WEBP';
|
|
181
|
+
}
|
|
182
|
+
function isMagicBmp(h) {
|
|
183
|
+
return h.length >= 2 && h[0] === 0x42 && h[1] === 0x4d;
|
|
184
|
+
}
|
|
185
|
+
function isMagicPdf(h) {
|
|
186
|
+
return h.length >= 4 && h.toString('ascii', 0, 4) === '%PDF';
|
|
187
|
+
}
|
|
188
|
+
function isMagicZip(h) {
|
|
189
|
+
return h.length >= 3 && h[0] === 0x50 && h[1] === 0x4b && (h[2] === 0x03 || h[2] === 0x05);
|
|
190
|
+
}
|
|
191
|
+
function getTextSnippet(h) {
|
|
192
|
+
const start = (h[0] === 0xef && h[1] === 0xbb && h[2] === 0xbf) ? 3 : 0;
|
|
193
|
+
return h.toString('utf8', start).trimStart()
|
|
194
|
+
.toLowerCase();
|
|
195
|
+
}
|
|
196
|
+
function isMagicHtml(h) {
|
|
197
|
+
const s = getTextSnippet(h);
|
|
198
|
+
return s.startsWith('<!doctype html')
|
|
199
|
+
|| s.startsWith('<html')
|
|
200
|
+
|| s.startsWith('<head')
|
|
201
|
+
|| s.startsWith('<body');
|
|
202
|
+
}
|
|
203
|
+
function isMagicXml(h) {
|
|
204
|
+
const s = getTextSnippet(h);
|
|
205
|
+
return s.startsWith('<?xml') || s.startsWith('<svg');
|
|
206
|
+
}
|
|
207
|
+
function isMagicPrintableText(h) {
|
|
208
|
+
const start = (h[0] === 0xef && h[1] === 0xbb && h[2] === 0xbf) ? 3 : 0;
|
|
209
|
+
return h.length > start && h.subarray(start).every(b => b === 0x09 || b === 0x0a || b === 0x0d || (b >= 0x20 && b <= 0x7e));
|
|
210
|
+
}
|
|
211
|
+
function detectMagicFamily(header) {
|
|
212
|
+
if (header.length < 2)
|
|
213
|
+
return 'unknown';
|
|
214
|
+
if (isMagicJpeg(header))
|
|
215
|
+
return 'jpeg';
|
|
216
|
+
if (isMagicPng(header))
|
|
217
|
+
return 'png';
|
|
218
|
+
if (isMagicGif(header))
|
|
219
|
+
return 'gif';
|
|
220
|
+
if (isMagicWebp(header))
|
|
221
|
+
return 'webp';
|
|
222
|
+
if (isMagicBmp(header))
|
|
223
|
+
return 'bmp';
|
|
224
|
+
if (isMagicPdf(header))
|
|
225
|
+
return 'pdf';
|
|
226
|
+
if (isMagicZip(header))
|
|
227
|
+
return 'zip';
|
|
228
|
+
if (isMagicHtml(header))
|
|
229
|
+
return 'html';
|
|
230
|
+
if (isMagicXml(header))
|
|
231
|
+
return 'xml';
|
|
232
|
+
if (isMagicPrintableText(header))
|
|
233
|
+
return 'text';
|
|
234
|
+
return 'unknown';
|
|
235
|
+
}
|
|
236
|
+
const IMAGE_EXT_TO_EXPECTED_FAMILY = {
|
|
237
|
+
'.jpg': ['jpeg'],
|
|
238
|
+
'.jpeg': ['jpeg'],
|
|
239
|
+
'.png': ['png'],
|
|
240
|
+
'.gif': ['gif'],
|
|
241
|
+
'.webp': ['webp'],
|
|
242
|
+
'.bmp': ['bmp'],
|
|
243
|
+
};
|
|
244
|
+
export function validateMediaBeforeQueue(url) {
|
|
245
|
+
if (!url) {
|
|
246
|
+
return '媒体 URL 为空';
|
|
247
|
+
}
|
|
248
|
+
if (!isLocalPath(url)) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const filePath = normalizePath(url);
|
|
252
|
+
if (!existsSync(filePath)) {
|
|
253
|
+
return `文件不存在: ${filePath}`;
|
|
254
|
+
}
|
|
255
|
+
let stat;
|
|
256
|
+
try {
|
|
257
|
+
stat = statSync(filePath);
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return `无法读取文件信息: ${err instanceof Error ? err.message : String(err)}`;
|
|
261
|
+
}
|
|
262
|
+
if (stat.size === 0) {
|
|
263
|
+
return `文件内容为空(0 字节): ${filePath}`;
|
|
264
|
+
}
|
|
265
|
+
let header;
|
|
266
|
+
try {
|
|
267
|
+
header = readMagicBytes(filePath, 16);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const actual = detectMagicFamily(header);
|
|
273
|
+
const ext = extname(filePath).toLowerCase();
|
|
274
|
+
const expected = IMAGE_EXT_TO_EXPECTED_FAMILY[ext];
|
|
275
|
+
if (expected && !expected.includes(actual)) {
|
|
276
|
+
return `文件扩展名 ${ext} 与实际内容不符(期望 ${expected.join('/')}, 检测到 ${actual}): ${filePath}`;
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
151
280
|
const MIME_TO_EXT = {
|
|
152
281
|
'image/jpeg': '.jpg',
|
|
153
282
|
'image/png': '.png',
|
|
@@ -357,6 +486,7 @@ export async function downloadMediasToLocalFiles(medias, account, core, log) {
|
|
|
357
486
|
const maxBytes = account.mediaMaxMb * 1024 * 1024;
|
|
358
487
|
const cacheDir = join(tmpdir(), 'yuanbao-media');
|
|
359
488
|
const tasks = medias.slice(0, 20).map(async ({ url, mediaName }, i) => {
|
|
489
|
+
logger.info(`开始解析资源: ${url}`);
|
|
360
490
|
const mediaFile = await downloadMediaForYuanbao(url, account.mediaMaxMb, account);
|
|
361
491
|
const originalFilename = mediaName || mediaFile.filename;
|
|
362
492
|
const ext = extname(originalFilename).toLowerCase();
|
|
@@ -368,7 +498,7 @@ export async function downloadMediasToLocalFiles(medias, account, core, log) {
|
|
|
368
498
|
}
|
|
369
499
|
const cachedFilePath = join(cacheDir, md5Filename);
|
|
370
500
|
if (existsSync(cachedFilePath)) {
|
|
371
|
-
|
|
501
|
+
logger.info(`媒体 ${i + 1}/${medias.length} 命中本地缓存,跳过保存: ${cachedFilePath}`);
|
|
372
502
|
return { path: cachedFilePath, contentType };
|
|
373
503
|
}
|
|
374
504
|
if (typeof core.channel.media?.saveMediaBuffer === 'function') {
|
|
@@ -385,10 +515,10 @@ export async function downloadMediasToLocalFiles(medias, account, core, log) {
|
|
|
385
515
|
const r = settled[i];
|
|
386
516
|
if (r.status === 'fulfilled') {
|
|
387
517
|
results.push(r.value);
|
|
388
|
-
|
|
518
|
+
logger.info(`媒体 ${i + 1}/${medias.length} 下载完成: ${r.value.path} (${r.value.contentType})`);
|
|
389
519
|
}
|
|
390
520
|
else {
|
|
391
|
-
|
|
521
|
+
logger.warn(`媒体 ${i + 1}/${medias.length} 下载失败,跳过: ${String(r.reason)}`);
|
|
392
522
|
}
|
|
393
523
|
}
|
|
394
524
|
return {
|
|
@@ -13,7 +13,7 @@ function enqueueRecallSystemEvent(params) {
|
|
|
13
13
|
}
|
|
14
14
|
export function handleGroupRecall(ctx, msg) {
|
|
15
15
|
const { core, account } = ctx;
|
|
16
|
-
const log = createLog('recall'
|
|
16
|
+
const log = createLog('recall');
|
|
17
17
|
const groupCode = msg.group_code?.trim() || 'unknown';
|
|
18
18
|
const seqList = msg.recall_msg_seq_list;
|
|
19
19
|
if (!seqList || seqList.length === 0) {
|
|
@@ -57,7 +57,7 @@ export function handleGroupRecall(ctx, msg) {
|
|
|
57
57
|
}
|
|
58
58
|
export function handleC2CRecall(ctx, msg) {
|
|
59
59
|
const { core, account } = ctx;
|
|
60
|
-
const log = createLog('recall'
|
|
60
|
+
const log = createLog('recall');
|
|
61
61
|
const fromAccount = msg.from_account?.trim() || 'unknown';
|
|
62
62
|
const seqList = msg.msg_id
|
|
63
63
|
? [{ msg_id: msg.msg_id, msg_seq: msg.msg_seq }]
|
|
@@ -7,12 +7,6 @@ export type MessageHandlerContext = {
|
|
|
7
7
|
account: ResolvedYuanbaoAccount;
|
|
8
8
|
config: OpenClawConfig;
|
|
9
9
|
core: PluginRuntime;
|
|
10
|
-
log: {
|
|
11
|
-
info: (msg: string) => void;
|
|
12
|
-
warn: (msg: string) => void;
|
|
13
|
-
error: (msg: string) => void;
|
|
14
|
-
verbose: (msg: string) => void;
|
|
15
|
-
};
|
|
16
10
|
statusSink?: (patch: {
|
|
17
11
|
lastInboundAt?: number;
|
|
18
12
|
lastOutboundAt?: number;
|
|
@@ -22,7 +22,7 @@ export const customHandler = {
|
|
|
22
22
|
if (!resData.isAtBot) {
|
|
23
23
|
resData.isAtBot = isAtBotSelf;
|
|
24
24
|
}
|
|
25
|
-
createLog('custom'
|
|
25
|
+
createLog('custom').info('@消息', { text: customContent?.text, userId: customContent?.user_id, isAtBot: resData.isAtBot });
|
|
26
26
|
if (!isAtBotSelf && customContent?.user_id) {
|
|
27
27
|
resData.mentions.push({
|
|
28
28
|
userId: customContent.user_id,
|
|
@@ -5,6 +5,8 @@ import { soundHandler } from './sound.js';
|
|
|
5
5
|
import { fileHandler } from './file.js';
|
|
6
6
|
import { videoHandler } from './video.js';
|
|
7
7
|
import { faceHandler } from './face.js';
|
|
8
|
+
import { sanitizePipeTables } from '../../utils/markdown-table-sanitize.js';
|
|
9
|
+
import { normalizeMathBlocks } from '../../utils/markdown-stream.js';
|
|
8
10
|
const handlerList = [
|
|
9
11
|
textHandler,
|
|
10
12
|
customHandler,
|
|
@@ -75,15 +77,16 @@ function resolveAtMentions(text, groupCode, memberInst) {
|
|
|
75
77
|
export function prepareOutboundContent(text, groupCode, memberInst) {
|
|
76
78
|
if (!text)
|
|
77
79
|
return [];
|
|
80
|
+
const sanitizedText = sanitizePipeTables(normalizeMathBlocks(text));
|
|
78
81
|
const items = [];
|
|
79
|
-
if (
|
|
80
|
-
const trailing =
|
|
82
|
+
if (sanitizedText.length) {
|
|
83
|
+
const trailing = sanitizedText.trim();
|
|
81
84
|
if (trailing) {
|
|
82
85
|
items.push(...resolveAtMentions(trailing, groupCode, memberInst));
|
|
83
86
|
}
|
|
84
87
|
}
|
|
85
|
-
if (items.length === 0 &&
|
|
86
|
-
items.push(...resolveAtMentions(
|
|
88
|
+
if (items.length === 0 && sanitizedText.trim()) {
|
|
89
|
+
items.push(...resolveAtMentions(sanitizedText.trim(), groupCode, memberInst));
|
|
87
90
|
}
|
|
88
91
|
return items;
|
|
89
92
|
}
|
|
@@ -122,12 +122,13 @@ async function handleC2CMessage(params) {
|
|
|
122
122
|
const fromAccount = msg.from_account?.trim() || 'unknown';
|
|
123
123
|
const senderNickname = msg.sender_nickname?.trim() || undefined;
|
|
124
124
|
const outboundSender = resolveOutboundSenderAccount(account);
|
|
125
|
-
const log = createLog('inbound'
|
|
125
|
+
const log = createLog('inbound');
|
|
126
126
|
if (outboundSender && fromAccount === outboundSender) {
|
|
127
127
|
log.info(`跳过机器人自身消息 <- ${fromAccount}`);
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
130
|
const { rawBody, medias } = extractTextFromMsgBody(ctx, msg.msg_body);
|
|
131
|
+
getMember(account.accountId).recordC2cUser(fromAccount, senderNickname || fromAccount);
|
|
131
132
|
log.info(`收到消息 <- ${fromAccount}${senderNickname ? `(${senderNickname})` : ''}, msgKey: ${msg.msg_key}`);
|
|
132
133
|
const quoteInfo = parseQuoteFromCloudCustomData(msg.cloud_custom_data);
|
|
133
134
|
if (quoteInfo) {
|
|
@@ -243,7 +244,7 @@ async function handleC2CMessage(params) {
|
|
|
243
244
|
AccountId: route.accountId,
|
|
244
245
|
ChatType: 'direct',
|
|
245
246
|
ConversationLabel: fromLabel,
|
|
246
|
-
SenderName: senderNickname
|
|
247
|
+
SenderName: senderNickname,
|
|
247
248
|
SenderId: fromAccount,
|
|
248
249
|
Provider: 'yuanbao',
|
|
249
250
|
Surface: 'yuanbao',
|
|
@@ -336,7 +337,7 @@ async function handleGroupMessage(params) {
|
|
|
336
337
|
const fromAccount = msg.from_account?.trim() || 'unknown';
|
|
337
338
|
const senderNickname = msg.sender_nickname?.trim() || undefined;
|
|
338
339
|
const outboundSender = resolveOutboundSenderAccount(account);
|
|
339
|
-
const glog = createLog('inbound'
|
|
340
|
+
const glog = createLog('inbound');
|
|
340
341
|
setGroupCode(groupCode);
|
|
341
342
|
if (outboundSender && fromAccount === outboundSender) {
|
|
342
343
|
glog.info('跳过机器人自身消息', { groupCode, fromAccount });
|
|
@@ -513,7 +514,7 @@ async function handleGroupMessage(params) {
|
|
|
513
514
|
ChatType: 'group',
|
|
514
515
|
ConversationLabel: groupLabel,
|
|
515
516
|
GroupSubject: msg.group_name || undefined,
|
|
516
|
-
SenderName: senderNickname
|
|
517
|
+
SenderName: senderNickname,
|
|
517
518
|
SenderId: fromAccount,
|
|
518
519
|
Provider: 'yuanbao',
|
|
519
520
|
Surface: 'yuanbao',
|
|
@@ -33,8 +33,8 @@ async function shouldAttachReplyRef(params) {
|
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
35
|
export async function sendYuanbaoMessageBody(params) {
|
|
36
|
-
const {
|
|
37
|
-
const log = createLog('outbound'
|
|
36
|
+
const { toAccount, msgBody, fromAccount, ctx } = params;
|
|
37
|
+
const log = createLog('outbound');
|
|
38
38
|
if (!ctx?.wsClient) {
|
|
39
39
|
log.error('发送失败: WebSocket 客户端不可用');
|
|
40
40
|
return { ok: false, error: 'wsClient not available' };
|
|
@@ -77,7 +77,7 @@ export async function sendYuanbaoMessage(params) {
|
|
|
77
77
|
}
|
|
78
78
|
export async function sendYuanbaoGroupMessageBody(params) {
|
|
79
79
|
const { account, groupCode, msgBody, fromAccount, refMsgId, refFromAccount, ctx } = params;
|
|
80
|
-
const log = createLog('outbound'
|
|
80
|
+
const log = createLog('outbound');
|
|
81
81
|
if (!ctx?.wsClient) {
|
|
82
82
|
log.error('发送群消息失败: WebSocket 客户端不可用');
|
|
83
83
|
return { ok: false, error: 'wsClient not available' };
|
|
@@ -153,7 +153,7 @@ export async function sendMsgBodyDirect(params) {
|
|
|
153
153
|
}
|
|
154
154
|
export async function executeReply(params) {
|
|
155
155
|
const { transport, ctx, account, core, replyRuntime, splitFinalText, overflowPolicy, ctxPayload, sessionKey, appendText, } = params;
|
|
156
|
-
const rlog = createLog('outbound'
|
|
156
|
+
const rlog = createLog('outbound');
|
|
157
157
|
if (ctx.abortSignal?.aborted) {
|
|
158
158
|
rlog.warn(`[${account.accountId}] 回复已中止,跳过执行`);
|
|
159
159
|
return;
|
|
@@ -191,6 +191,10 @@ export async function executeReply(params) {
|
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
193
193
|
rlog.info('[deliver] 收到回复数据', { kind: info.kind, model_output: payload.text });
|
|
194
|
+
if (payload.isReasoning) {
|
|
195
|
+
rlog.info('[deliver] Reasoning', { text: payload.text });
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
194
198
|
if (payload.isCompactionNotice) {
|
|
195
199
|
rlog.info('[deliver] CompactionNotice', { text: payload.text });
|
|
196
200
|
return;
|
|
@@ -55,7 +55,6 @@ export async function handleYuanbaoAction(action, params, context) {
|
|
|
55
55
|
account: context.account,
|
|
56
56
|
config: context.config,
|
|
57
57
|
core: getYuanbaoRuntime(),
|
|
58
|
-
log: { info: () => { }, warn: () => { }, error: () => { }, verbose: () => { } },
|
|
59
58
|
wsClient: context.wsClient,
|
|
60
59
|
groupCode: getGroupCode(),
|
|
61
60
|
};
|
|
@@ -3,8 +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
|
+
'(Group only) - Proactive `send` to another user: `to="<userId>"`.',
|
|
7
|
+
'(Group only) - 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
|
+
'(Group only) - To find a user\'s ID, use the query_session_members tool first, then pass the resolved ID to the message tool.',
|
|
9
9
|
];
|
|
10
10
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ChannelPlugin } from 'openclaw/plugin-sdk';
|
|
2
|
+
export declare function normalizeYuanbaoMessagingTarget(raw: string): string | undefined;
|
|
3
|
+
export declare function yuanbaoLooksLikeId(raw: string): boolean;
|
|
4
|
+
export declare const yuanbaoMessagingAdapter: ChannelPlugin<any>['messaging'];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function normalizeYuanbaoMessagingTarget(raw) {
|
|
2
|
+
const trimmed = raw.trim();
|
|
3
|
+
if (!trimmed)
|
|
4
|
+
return undefined;
|
|
5
|
+
return trimmed.replace(/^(yuanbao):/i, '').trim() || undefined;
|
|
6
|
+
}
|
|
7
|
+
export function yuanbaoLooksLikeId(raw) {
|
|
8
|
+
if (/(group:|user:|direct:|yuanbao:)/.test(raw))
|
|
9
|
+
return true;
|
|
10
|
+
try {
|
|
11
|
+
const userId = raw.split(':').pop();
|
|
12
|
+
const sourceStr = atob(userId);
|
|
13
|
+
return sourceStr.length >= 16;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export const yuanbaoMessagingAdapter = {
|
|
20
|
+
normalizeTarget: normalizeYuanbaoMessagingTarget,
|
|
21
|
+
targetResolver: {
|
|
22
|
+
looksLikeId: yuanbaoLooksLikeId,
|
|
23
|
+
hint: '<chatId|group:groupCode|direct:userId|userId>',
|
|
24
|
+
},
|
|
25
|
+
inferTargetChatType: ({ to }) => {
|
|
26
|
+
const stripped = to.replace(/^yuanbao:/i, '');
|
|
27
|
+
if (stripped.includes('group:') || /^\d{9}$/.test(stripped))
|
|
28
|
+
return 'group';
|
|
29
|
+
return 'direct';
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -35,6 +35,7 @@ export declare class GroupMember {
|
|
|
35
35
|
lookupUsers(groupCode: string, nameFilter?: string): UserRecord[];
|
|
36
36
|
lookupUserByNickName(groupCode: string, nickName: string): UserRecord | undefined;
|
|
37
37
|
hasCachedData(groupCode: string): boolean;
|
|
38
|
+
listCachedGroupCodes(): string[];
|
|
38
39
|
refresh(groupCode: string): Promise<UserRecord[]>;
|
|
39
40
|
queryGroupOwner(groupCode: string): Promise<GroupOwnerInfo | null>;
|
|
40
41
|
queryGroupInfo(groupCode: string): Promise<GroupInfoData | null>;
|
|
@@ -50,9 +51,13 @@ export declare class Member {
|
|
|
50
51
|
readonly session: SessionMember;
|
|
51
52
|
readonly group: GroupMember;
|
|
52
53
|
private yuanbaoUserIdCache;
|
|
54
|
+
private c2cUsers;
|
|
53
55
|
constructor(accountId: string);
|
|
54
56
|
recordUser(groupCode: string, userId: string, nickName: string): void;
|
|
57
|
+
recordC2cUser(userId: string, nickName: string): void;
|
|
58
|
+
listC2cUsers(): UserRecord[];
|
|
55
59
|
queryMembers(groupCode: string, nameFilter?: string): Promise<UserRecord[]>;
|
|
60
|
+
queryUserIdsByNickName(nickName: string, groupCode?: string): Promise<string[]>;
|
|
56
61
|
lookupUsers(groupCode: string, nameFilter?: string): UserRecord[];
|
|
57
62
|
lookupUserByNickName(groupCode: string, nickName: string): UserRecord | undefined;
|
|
58
63
|
queryGroupOwner(groupCode: string): Promise<GroupOwnerInfo | null>;
|
|
@@ -118,6 +118,9 @@ export class GroupMember {
|
|
|
118
118
|
hasCachedData(groupCode) {
|
|
119
119
|
return this.cache.has(groupCode);
|
|
120
120
|
}
|
|
121
|
+
listCachedGroupCodes() {
|
|
122
|
+
return Array.from(this.cache.keys());
|
|
123
|
+
}
|
|
121
124
|
async refresh(groupCode) {
|
|
122
125
|
this.cache.delete(groupCode);
|
|
123
126
|
return this.getMembers(groupCode);
|
|
@@ -241,6 +244,7 @@ export class Member {
|
|
|
241
244
|
session = new SessionMember();
|
|
242
245
|
group;
|
|
243
246
|
yuanbaoUserIdCache = null;
|
|
247
|
+
c2cUsers = new Map();
|
|
244
248
|
constructor(accountId) {
|
|
245
249
|
this.accountId = accountId;
|
|
246
250
|
this.group = new GroupMember(accountId, this.session);
|
|
@@ -248,6 +252,19 @@ export class Member {
|
|
|
248
252
|
recordUser(groupCode, userId, nickName) {
|
|
249
253
|
this.session.recordUser(groupCode, userId, nickName);
|
|
250
254
|
}
|
|
255
|
+
recordC2cUser(userId, nickName) {
|
|
256
|
+
if (!userId)
|
|
257
|
+
return;
|
|
258
|
+
this.c2cUsers.set(userId, {
|
|
259
|
+
userId,
|
|
260
|
+
nickName: nickName || 'unknown',
|
|
261
|
+
lastSeen: Date.now(),
|
|
262
|
+
});
|
|
263
|
+
logger.debug?.(`[member] recorded c2c user: ${nickName ?? '?'} (${userId})`);
|
|
264
|
+
}
|
|
265
|
+
listC2cUsers() {
|
|
266
|
+
return Array.from(this.c2cUsers.values()).sort((a, b) => b.lastSeen - a.lastSeen);
|
|
267
|
+
}
|
|
251
268
|
async queryMembers(groupCode, nameFilter) {
|
|
252
269
|
const groupMembers = await this.group.getMembers(groupCode);
|
|
253
270
|
if (groupMembers.length > 0) {
|
|
@@ -261,6 +278,37 @@ export class Member {
|
|
|
261
278
|
logger.debug?.(`[member] GroupMember empty or no match, fallback to SessionMember for group=${groupCode}`);
|
|
262
279
|
return this.session.lookupUsers(groupCode, nameFilter);
|
|
263
280
|
}
|
|
281
|
+
async queryUserIdsByNickName(nickName, groupCode) {
|
|
282
|
+
const filter = nickName.trim().toLowerCase();
|
|
283
|
+
const seen = new Set();
|
|
284
|
+
const results = [];
|
|
285
|
+
const collect = (users) => {
|
|
286
|
+
for (const u of users) {
|
|
287
|
+
if (!seen.has(u.userId) && u.nickName.toLowerCase().includes(filter)) {
|
|
288
|
+
seen.add(u.userId);
|
|
289
|
+
results.push(u.userId);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
collect(this.listC2cUsers());
|
|
294
|
+
if (groupCode) {
|
|
295
|
+
const members = await this.group.getMembers(groupCode);
|
|
296
|
+
const source = members.length > 0 ? members : this.session.lookupUsers(groupCode);
|
|
297
|
+
collect(source);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const allGroupCodes = new Set([
|
|
301
|
+
...this.group.listCachedGroupCodes(),
|
|
302
|
+
...this.session.listGroupCodes(),
|
|
303
|
+
]);
|
|
304
|
+
for (const code of allGroupCodes) {
|
|
305
|
+
const groupCached = this.group.lookupUsers(code);
|
|
306
|
+
collect(groupCached.length > 0 ? groupCached : this.session.lookupUsers(code));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
logger.debug?.(`[member] queryUserIdsByNickName: "${nickName}"${groupCode ? ` in group=${groupCode}` : ' (all sources)'} → ${results.length} hit(s)`);
|
|
310
|
+
return results;
|
|
311
|
+
}
|
|
264
312
|
lookupUsers(groupCode, nameFilter) {
|
|
265
313
|
const groupResults = this.group.lookupUsers(groupCode, nameFilter);
|
|
266
314
|
if (groupResults.length > 0)
|
|
@@ -72,18 +72,6 @@ export interface OutboundQueueConfig {
|
|
|
72
72
|
export declare function initOutboundQueue(accountId: string, config: OutboundQueueConfig): OutboundQueueManager;
|
|
73
73
|
export declare function getOutboundQueue(accountId: string): OutboundQueueManager | null;
|
|
74
74
|
export declare function destroyOutboundQueue(accountId: string): void;
|
|
75
|
-
export declare function endsWithTableRow(text: string): boolean;
|
|
76
|
-
export declare function hasUnclosedFence(text: string): boolean;
|
|
77
|
-
export declare function startsWithBlockElement(text: string): boolean;
|
|
78
|
-
export declare function inferBlockSeparator(buffer: string, incoming: string): string;
|
|
79
|
-
export type AtomicBlock = {
|
|
80
|
-
start: number;
|
|
81
|
-
end: number;
|
|
82
|
-
kind: 'table' | 'diagram-fence';
|
|
83
|
-
};
|
|
84
|
-
export declare function extractAtomicBlocks(text: string): AtomicBlock[];
|
|
85
|
-
export declare function chunkMarkdownTextAtomicAware(text: string, maxChars: number, chunkFn: (text: string, max: number) => string[]): string[];
|
|
86
|
-
export declare function mergeBlockStreamingFences(buffer: string, incoming: string): string;
|
|
87
75
|
export interface MergeTextOptions {
|
|
88
76
|
minChars: number;
|
|
89
77
|
maxChars: number;
|