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.
Files changed (42) hide show
  1. package/dist/openclaw.plugin.json +1 -1
  2. package/dist/src/channel.js +73 -38
  3. package/dist/src/commands/upgrade/upgrade.js +55 -9
  4. package/dist/src/commands/upgrade/utils.d.ts +3 -1
  5. package/dist/src/commands/upgrade/utils.js +11 -9
  6. package/dist/src/config-schema.js +0 -7
  7. package/dist/src/directory-adapter.d.ts +2 -0
  8. package/dist/src/directory-adapter.js +58 -0
  9. package/dist/src/dm/send-dm.js +0 -1
  10. package/dist/src/logger.d.ts +2 -7
  11. package/dist/src/logger.js +25 -49
  12. package/dist/src/media.d.ts +1 -0
  13. package/dist/src/media.js +134 -4
  14. package/dist/src/message-handler/callbacks/recall.js +2 -2
  15. package/dist/src/message-handler/context.d.ts +0 -6
  16. package/dist/src/message-handler/handlers/custom.js +1 -1
  17. package/dist/src/message-handler/handlers/index.js +7 -4
  18. package/dist/src/message-handler/inbound.js +5 -4
  19. package/dist/src/message-handler/outbound.js +8 -4
  20. package/dist/src/message-tool/action-runtime.js +0 -1
  21. package/dist/src/message-tool/hints.js +3 -3
  22. package/dist/src/messaging-adapter.d.ts +4 -0
  23. package/dist/src/messaging-adapter.js +31 -0
  24. package/dist/src/module/member.d.ts +5 -0
  25. package/dist/src/module/member.js +48 -0
  26. package/dist/src/outbound-queue.d.ts +0 -12
  27. package/dist/src/outbound-queue.js +1 -179
  28. package/dist/src/types.d.ts +7 -1
  29. package/dist/src/utils/markdown-stream.d.ts +14 -0
  30. package/dist/src/utils/markdown-stream.js +242 -0
  31. package/dist/src/utils/markdown-table-sanitize.d.ts +1 -0
  32. package/dist/src/utils/markdown-table-sanitize.js +89 -0
  33. package/dist/src/yuanbao-server/http/main.d.ts +3 -3
  34. package/dist/src/yuanbao-server/http/main.js +4 -4
  35. package/dist/src/yuanbao-server/http/request.d.ts +5 -5
  36. package/dist/src/yuanbao-server/http/request.js +23 -23
  37. package/dist/src/yuanbao-server/ws/client.d.ts +0 -2
  38. package/dist/src/yuanbao-server/ws/client.js +1 -1
  39. package/dist/src/yuanbao-server/ws/gateway.d.ts +1 -8
  40. package/dist/src/yuanbao-server/ws/gateway.js +25 -39
  41. package/openclaw.plugin.json +1 -1
  42. 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
- log.verbose(`媒体 ${i + 1}/${medias.length} 命中本地缓存,跳过保存: ${cachedFilePath}`);
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
- log.verbose(`媒体 ${i + 1}/${medias.length} 下载完成: ${r.value.path} (${r.value.contentType})`);
518
+ logger.info(`媒体 ${i + 1}/${medias.length} 下载完成: ${r.value.path} (${r.value.contentType})`);
389
519
  }
390
520
  else {
391
- log.warn(`媒体 ${i + 1}/${medias.length} 下载失败,跳过: ${String(r.reason)}`);
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', ctx.log);
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', ctx.log);
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', ctx.log).info('@消息', { text: customContent?.text, userId: customContent?.user_id, isAtBot: resData.isAtBot });
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 (text.length) {
80
- const trailing = text.trim();
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 && text.trim()) {
86
- items.push(...resolveAtMentions(text.trim(), groupCode, memberInst));
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', ctx.log, { botId: account.botId });
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 || fromAccount,
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', ctx.log, { botId: account.botId });
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 || fromAccount,
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 { account, toAccount, msgBody, fromAccount, ctx } = params;
37
- const log = createLog('outbound', ctx?.log, { botId: account.botId });
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', ctx?.log, { botId: account.botId });
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', ctx.log, { botId: account.botId });
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;