@meet-im/meet 3.5.0 → 3.6.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.
@@ -5,6 +5,7 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "openc
5
5
  import { listMeetAccountIds, resolveMeetAccount } from "./accounts.js";
6
6
  import { createChannelApproverDmTargetResolver, createChannelNativeOriginTargetResolver, createApproverRestrictedNativeApprovalCapability, splitChannelApprovalCapability, } from "openclaw/plugin-sdk/approval-runtime";
7
7
  import { getMeetExecApprovalApprovers, isMeetExecApprovalApprover, isMeetExecApprovalClientEnabled, } from "./exec-approvals.js";
8
+ import { encodeMeetDmTopicId } from "./dm-topic-codec.js";
8
9
  import { sendMessageMeet } from "./send.js";
9
10
  function shouldHandleMeetApprovalRequest(_params) {
10
11
  return true;
@@ -33,6 +34,21 @@ function normalizeMeetOriginChannelId(value) {
33
34
  }
34
35
  return /^-?\d+$/.test(trimmed) ? trimmed : null;
35
36
  }
37
+ function normalizeMeetOriginTarget(value) {
38
+ if (!value) {
39
+ return null;
40
+ }
41
+ const trimmed = value.trim();
42
+ if (!trimmed) {
43
+ return null;
44
+ }
45
+ const prefixed = trimmed.match(/^(?:channel|user):(-?\d+)$/i);
46
+ if (prefixed) {
47
+ const kind = trimmed.toLowerCase().startsWith("user:") ? "user" : "channel";
48
+ return `${kind}:${prefixed[1]}`;
49
+ }
50
+ return /^-?\d+$/.test(trimmed) ? trimmed : null;
51
+ }
36
52
  function normalizeMeetThreadId(value) {
37
53
  if (typeof value === "number") {
38
54
  return Number.isFinite(value) ? String(value) : undefined;
@@ -41,7 +57,7 @@ function normalizeMeetThreadId(value) {
41
57
  return undefined;
42
58
  }
43
59
  const normalized = value.trim();
44
- return /^-?\d+$/.test(normalized) ? normalized : undefined;
60
+ return normalized || undefined;
45
61
  }
46
62
  function createMeetOriginTargetResolver(_configOverride) {
47
63
  return createChannelNativeOriginTargetResolver({
@@ -60,12 +76,21 @@ function createMeetOriginTargetResolver(_configOverride) {
60
76
  const sessionKind = extractMeetSessionKind(normalizeOptionalString(request.request.sessionKey) ?? null);
61
77
  const turnSourceChannel = normalizeLowercaseStringOrEmpty(request.request.turnSourceChannel);
62
78
  const rawTurnSourceTo = normalizeOptionalString(request.request.turnSourceTo) ?? "";
79
+ const turnSourceTarget = normalizeMeetOriginTarget(rawTurnSourceTo);
63
80
  const turnSourceTo = normalizeMeetOriginChannelId(rawTurnSourceTo);
64
- const threadId = normalizeMeetThreadId(request.request.turnSourceThreadId) ??
81
+ const threadId = encodeMeetDmTopicId(normalizeMeetThreadId(request.request.turnSourceThreadId)) ??
65
82
  normalizeMeetThreadId(sessionConversation?.threadId) ??
66
83
  undefined;
67
84
  const hasExplicitOriginTarget = /^(?:channel):/i.test(rawTurnSourceTo);
68
- if (turnSourceChannel !== "meet" || !turnSourceTo || sessionKind === "dm") {
85
+ if (turnSourceChannel !== "meet") {
86
+ return null;
87
+ }
88
+ if (sessionKind === "dm") {
89
+ return turnSourceTarget && /^user:/i.test(turnSourceTarget)
90
+ ? { to: turnSourceTarget, threadId }
91
+ : null;
92
+ }
93
+ if (!turnSourceTo) {
69
94
  return null;
70
95
  }
71
96
  return hasExplicitOriginTarget || sessionKind === "channel" || sessionKind === "group"
package/dist/src/bot.js CHANGED
@@ -2,7 +2,8 @@ import { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, record
2
2
  import { buildChannelInboundEventContext } from "openclaw/plugin-sdk/channel-inbound";
3
3
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
4
4
  import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
5
- import { msgContentToContext, extractQuoteMessageMedia } from "./sdk-bridge.js";
5
+ import { msgContentToContext, extractQuoteMessageMedia, parseMergedForwardContent } from "./sdk-bridge.js";
6
+ import { buildMeetRoutePeerId } from "./route-peer.js";
6
7
  import { getMeetRuntime } from "./runtime.js";
7
8
  import { resolveMeetAllowlistMatch, resolveMeetGroupPolicy, resolveMeetGroupConfig, resolveMeetGroupUserPolicy } from "./policy.js";
8
9
  import { sendMessageMeet } from "./send.js";
@@ -12,6 +13,9 @@ const DEFAULT_DM_SYSTEM_PROMPT = "你正在 Meet 私聊中对话。注意:Meet
12
13
  function formatHistoryEntry(entry) {
13
14
  return `${entry.sender}: ${entry.body}`;
14
15
  }
16
+ function isSessionBoundaryCommand(text) {
17
+ return /^\/(?:new|reset)(?:\s|$)/i.test(text.trim());
18
+ }
15
19
  export async function handleMeetMessage(params) {
16
20
  const { cfg, msg, botUserId, runtime, accountId, account, bot, groupHistories, quoteMsgMap, ctx: providedCtx } = params;
17
21
  const log = runtime?.log ?? console.log;
@@ -36,11 +40,10 @@ export async function handleMeetMessage(params) {
36
40
  const historyLimit = isGroup
37
41
  ? Math.max(0, meetCfg.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? 20)
38
42
  : Math.max(0, meetCfg.dmHistoryLimit ?? 0);
43
+ const historyKey = isGroup
44
+ ? ctx.chatId
45
+ : (ctx.threadId ? `${ctx.chatId}__topic__${ctx.threadId}` : ctx.chatId);
39
46
  const speaker = ctx.senderName ?? ctx.senderId;
40
- // Discord 做法:文字优先,无文字时用媒体占位符
41
- const messageBody = ctx.content.trim()
42
- ? `${speaker}: ${ctx.content.trim()}`
43
- : `${speaker}: ${ctx.placeholder || ""}`;
44
47
  const pendingEntry = {
45
48
  sender: speaker,
46
49
  body: ctx.content.trim() || ctx.placeholder || "",
@@ -149,16 +152,48 @@ export async function handleMeetMessage(params) {
149
152
  log(`[${accountId}]: message in group ${ctx.chatId} skipped (mention required)`);
150
153
  recordPendingHistoryEntryIfEnabled({
151
154
  historyMap: groupHistories,
152
- historyKey: ctx.chatId,
155
+ historyKey,
153
156
  entry: pendingEntry,
154
157
  limit: historyLimit,
155
158
  });
156
159
  return;
157
160
  }
158
161
  }
162
+ // 合并转发消息(msgType=19):只进上下文,不发给 LLM
163
+ const msgType = Number(msg.extraInfo?.msgType);
164
+ if (msgType === 19) {
165
+ log(`[${accountId}]: skipping merged forward message ${ctx.messageId} (msgType=19, context only)`);
166
+ if (historyLimit > 0) {
167
+ const subEntries = parseMergedForwardContent(msg.content ?? "");
168
+ const entries = groupHistories.get(historyKey) ?? [];
169
+ if (subEntries.length > 0) {
170
+ for (const sub of subEntries) {
171
+ entries.push({
172
+ sender: sub.sender,
173
+ body: sub.body,
174
+ timestamp: sub.timestamp,
175
+ messageId: sub.messageId,
176
+ });
177
+ }
178
+ }
179
+ else {
180
+ entries.push(pendingEntry);
181
+ }
182
+ while (entries.length > historyLimit) {
183
+ entries.shift();
184
+ }
185
+ groupHistories.set(historyKey, entries);
186
+ }
187
+ return;
188
+ }
159
189
  const meetFrom = `meet:${ctx.senderId}`;
160
190
  const meetTo = ctx.chatId;
161
- const peerId = isGroup ? ctx.chatId : ctx.senderId;
191
+ const peerId = buildMeetRoutePeerId({
192
+ isGroup,
193
+ senderId: ctx.senderId,
194
+ chatId: ctx.chatId,
195
+ threadId: ctx.threadId,
196
+ });
162
197
  const route = core.channel.routing.resolveAgentRoute({
163
198
  cfg,
164
199
  channel: "meet",
@@ -167,6 +202,7 @@ export async function handleMeetMessage(params) {
167
202
  kind: isGroup ? "group" : "direct",
168
203
  id: peerId,
169
204
  },
205
+ ...(ctx.threadId ? { threadId: ctx.threadId } : {}),
170
206
  });
171
207
  // 处理媒体附件
172
208
  let mediaContext = "";
@@ -257,6 +293,9 @@ export async function handleMeetMessage(params) {
257
293
  ? ctx.content.trim()
258
294
  : (ctx.placeholder || "");
259
295
  const finalContent = `${quoteContext}${userBody}${mentionsContext}${mediaContext}`;
296
+ if (isSessionBoundaryCommand(ctx.rawBody ?? ctx.content)) {
297
+ groupHistories.set(historyKey, []);
298
+ }
260
299
  // Discord 做法:跳过空内容消息
261
300
  if (!finalContent.trim() && mediaPaths.length === 0) {
262
301
  log(`[${accountId}]: skip message ${ctx.messageId} (empty content)`);
@@ -273,22 +312,22 @@ export async function handleMeetMessage(params) {
273
312
  const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderId}` : ctx.senderId;
274
313
  const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
275
314
  // 将当前消息添加到历史(在确认要处理消息之后)
276
- const historyEntries = isGroup && historyLimit > 0
315
+ const historyEntries = historyLimit > 0
277
316
  ? (() => {
278
- const entries = groupHistories.get(ctx.chatId) ?? [];
317
+ const entries = groupHistories.get(historyKey) ?? [];
279
318
  entries.push(pendingEntry);
280
319
  while (entries.length > historyLimit) {
281
320
  entries.shift();
282
321
  }
283
- groupHistories.set(ctx.chatId, entries);
322
+ groupHistories.set(historyKey, entries);
284
323
  return entries;
285
324
  })()
286
325
  : [];
287
326
  const bodyWithContext = buildPendingHistoryContextFromMap({
288
327
  historyMap: groupHistories,
289
- historyKey: ctx.chatId,
328
+ historyKey,
290
329
  limit: historyLimit,
291
- currentMessage: messageBody,
330
+ currentMessage: finalContent,
292
331
  formatEntry: formatHistoryEntry,
293
332
  });
294
333
  const body = core.channel.reply.formatAgentEnvelope({
@@ -298,7 +337,7 @@ export async function handleMeetMessage(params) {
298
337
  envelope: envelopeOptions,
299
338
  body: bodyWithContext,
300
339
  });
301
- const inboundHistory = isGroup && historyLimit > 0
340
+ const inboundHistory = historyLimit > 0
302
341
  ? historyEntries.map((entry) => ({
303
342
  sender: entry.sender,
304
343
  body: entry.body,
@@ -326,6 +365,7 @@ export async function handleMeetMessage(params) {
326
365
  kind: isGroup ? "group" : "direct",
327
366
  id: ctx.chatId,
328
367
  label: isGroup ? (groupConfig?.groupConfig?.name ?? ctx.chatId) : ctx.chatId,
368
+ ...(ctx.threadId ? { threadId: ctx.threadId } : {}),
329
369
  routePeer: {
330
370
  kind: isGroup ? "group" : "direct",
331
371
  id: peerId,
@@ -339,11 +379,12 @@ export async function handleMeetMessage(params) {
339
379
  reply: {
340
380
  to: meetTo,
341
381
  originatingTo: meetTo,
382
+ ...(ctx.threadId ? { messageThreadId: ctx.threadId } : {}),
342
383
  },
343
384
  message: {
344
385
  body,
345
386
  rawBody: ctx.content,
346
- bodyForAgent: finalContent,
387
+ bodyForAgent: bodyWithContext,
347
388
  commandBody: ctx.rawBody ?? ctx.content,
348
389
  envelopeFrom,
349
390
  inboundHistory,
@@ -389,6 +430,7 @@ export async function handleMeetMessage(params) {
389
430
  agentId: route.agentId,
390
431
  runtime: runtime,
391
432
  chatId: ctx.chatId,
433
+ threadId: ctx.threadId,
392
434
  senderId: ctx.senderId,
393
435
  mentionedBot: ctx.mentionedBot,
394
436
  replyToMessageId: ctx.messageId,
@@ -402,7 +444,7 @@ export async function handleMeetMessage(params) {
402
444
  apiEndpoint: account.apiEndpoint,
403
445
  typingMode: effectiveTypingMode,
404
446
  });
405
- log(`[${accountId}]: dispatch ctx replyToId=${inboundCtx.ReplyToId ?? "undefined"} replyToBody=${JSON.stringify(inboundCtx.ReplyToBody ?? "")} rawBody=${JSON.stringify(inboundCtx.RawBody ?? "")} commandBody=${JSON.stringify(inboundCtx.CommandBody ?? "")} bodyForAgent=${JSON.stringify(inboundCtx.BodyForAgent ?? "")}`);
447
+ log(`[${accountId}]: dispatch ctx historyKey=${historyKey} routeSessionKey=${route.sessionKey} replyToId=${inboundCtx.ReplyToId ?? "undefined"} replyToBody=${JSON.stringify(inboundCtx.ReplyToBody ?? "")} rawBody=${JSON.stringify(inboundCtx.RawBody ?? "")} commandBody=${JSON.stringify(inboundCtx.CommandBody ?? "")} bodyForAgent=${JSON.stringify(inboundCtx.BodyForAgent ?? "")}`);
406
448
  log(`[${accountId}]: dispatching to AI agent=${route.agentId} session=${route.sessionKey} history=${inboundHistory?.length ?? 0}`);
407
449
  const dispatchResult = await core.channel.reply.dispatchReplyFromConfig({
408
450
  ctx: inboundCtx,
@@ -414,11 +456,13 @@ export async function handleMeetMessage(params) {
414
456
  log(`[${accountId}]: AI response completed for message ${ctx.messageId}`);
415
457
  markRunComplete();
416
458
  markDispatchIdle();
417
- clearHistoryEntriesIfEnabled({
418
- historyMap: groupHistories,
419
- historyKey: ctx.chatId,
420
- limit: historyLimit,
421
- });
459
+ if (isGroup) {
460
+ clearHistoryEntriesIfEnabled({
461
+ historyMap: groupHistories,
462
+ historyKey,
463
+ limit: historyLimit,
464
+ });
465
+ }
422
466
  }
423
467
  catch (err) {
424
468
  error(`[${accountId}]: error processing message: ${String(err)}`);
@@ -39,7 +39,6 @@ export function createMeetClient(account) {
39
39
  pollingLimit: account.config.pollLimit ?? POLLING.DEFAULT_LIMIT,
40
40
  longPollingTimeout: pollTimeoutSec,
41
41
  logLevel,
42
- useV2: true,
43
42
  userAgent: buildUserAgent(),
44
43
  });
45
44
  botInstances.set(account.accountId, bot);
@@ -0,0 +1,4 @@
1
+ export declare const MEET_DM_TOPIC_THREAD_PREFIX = "meetdm_b64_";
2
+ export declare function isEncodedMeetDmTopicThreadId(threadId?: string | null): boolean;
3
+ export declare function encodeMeetDmTopicId(raw?: string | null): string | undefined;
4
+ export declare function decodeMeetDmTopicThreadId(threadId?: string | null): string | undefined;
@@ -0,0 +1,33 @@
1
+ export const MEET_DM_TOPIC_THREAD_PREFIX = "meetdm_b64_";
2
+ function normalizeNonEmpty(value) {
3
+ const trimmed = value?.trim();
4
+ return trimmed ? trimmed : undefined;
5
+ }
6
+ function toBase64Url(input) {
7
+ return Buffer.from(input, "utf8")
8
+ .toString("base64")
9
+ .replace(/\+/g, "-")
10
+ .replace(/\//g, "_")
11
+ .replace(/=+$/g, "");
12
+ }
13
+ function fromBase64Url(input) {
14
+ const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
15
+ const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
16
+ return Buffer.from(padded, "base64").toString("utf8");
17
+ }
18
+ export function isEncodedMeetDmTopicThreadId(threadId) {
19
+ const normalized = normalizeNonEmpty(threadId);
20
+ return normalized?.startsWith(MEET_DM_TOPIC_THREAD_PREFIX) ?? false;
21
+ }
22
+ export function encodeMeetDmTopicId(raw) {
23
+ const normalized = normalizeNonEmpty(raw);
24
+ return normalized ? `${MEET_DM_TOPIC_THREAD_PREFIX}${toBase64Url(normalized)}` : undefined;
25
+ }
26
+ export function decodeMeetDmTopicThreadId(threadId) {
27
+ const normalized = normalizeNonEmpty(threadId);
28
+ if (!normalized)
29
+ return undefined;
30
+ if (!normalized.startsWith(MEET_DM_TOPIC_THREAD_PREFIX))
31
+ return normalized;
32
+ return fromBase64Url(normalized.slice(MEET_DM_TOPIC_THREAD_PREFIX.length));
33
+ }
@@ -1,2 +1,2 @@
1
- export declare const MEET_PLUGIN_VERSION = "3.5.0";
1
+ export declare const MEET_PLUGIN_VERSION = "3.6.0";
2
2
  export declare const MEET_OPENCLAW_VERSION = "2026.5.18";
@@ -1,2 +1,2 @@
1
- export const MEET_PLUGIN_VERSION = "3.5.0";
1
+ export const MEET_PLUGIN_VERSION = "3.6.0";
2
2
  export const MEET_OPENCLAW_VERSION = "2026.5.18";
@@ -15,6 +15,7 @@ export type CreateMeetReplyDispatcherOpts = {
15
15
  agentId: string;
16
16
  runtime: RuntimeEnv;
17
17
  chatId: string;
18
+ threadId?: string;
18
19
  senderId?: string;
19
20
  mentionedBot?: boolean;
20
21
  replyToMessageId?: string;
@@ -28,11 +29,20 @@ export type CreateMeetReplyDispatcherOpts = {
28
29
  typingMode?: "none" | "instant" | "message";
29
30
  };
30
31
  export declare function createMeetReplyDispatcher(opts: CreateMeetReplyDispatcherOpts): Promise<{
31
- dispatcher: import("node_modules/openclaw/dist/plugin-sdk/src/auto-reply/reply/reply-dispatcher.types.js").ReplyDispatcher;
32
+ dispatcher: import("node_modules/openclaw/dist/plugin-sdk/reply-dispatcher.types-B0sivCQE.js").r;
32
33
  replyOptions: {
33
34
  sourceReplyDeliveryMode?: import("openclaw/plugin-sdk/channel-reply-pipeline").SourceReplyDeliveryMode | undefined;
34
35
  onReplyStart?: (() => Promise<void> | void) | undefined;
35
- onTypingController?: ((typing: import("node_modules/openclaw/dist/plugin-sdk/src/auto-reply/reply/typing.js").TypingController) => void) | undefined;
36
+ onTypingController?: ((typing: {
37
+ onReplyStart: () => Promise<void>;
38
+ startTypingLoop: () => Promise<void>;
39
+ startTypingOnText: (text?: string) => Promise<void>;
40
+ refreshTypingTtl: () => void;
41
+ isActive: () => boolean;
42
+ markRunComplete: () => void;
43
+ markDispatchIdle: () => void;
44
+ cleanup: () => void;
45
+ }) => void) | undefined;
36
46
  onTypingCleanup?: (() => void) | undefined;
37
47
  };
38
48
  markDispatchIdle: () => void;
@@ -83,7 +83,7 @@ export function protectMentionsInChunks(chunks) {
83
83
  return result;
84
84
  }
85
85
  export async function createMeetReplyDispatcher(opts) {
86
- const { cfg, agentId, chatId, senderId, mentionedBot, replyToMessageId, accountId, mediaLocalRoots, sessionInfo, apiToken, apiEndpoint, typingMode } = opts;
86
+ const { cfg, agentId, chatId, threadId, senderId, mentionedBot, replyToMessageId, accountId, mediaLocalRoots, sessionInfo, apiToken, apiEndpoint, typingMode } = opts;
87
87
  const core = getMeetRuntime();
88
88
  const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "meet", accountId, {
89
89
  fallbackLimit: 4000,
@@ -210,6 +210,7 @@ export async function createMeetReplyDispatcher(opts) {
210
210
  await sendMediaMeet({
211
211
  cfg,
212
212
  to: chatId,
213
+ threadId,
213
214
  text: text.trim() || undefined,
214
215
  mediaUrl: mediaUrls[0],
215
216
  mediaLocalRoots,
@@ -221,6 +222,7 @@ export async function createMeetReplyDispatcher(opts) {
221
222
  await sendMediaMeet({
222
223
  cfg,
223
224
  to: chatId,
225
+ threadId,
224
226
  text: undefined,
225
227
  mediaUrl: mediaUrls[i],
226
228
  mediaLocalRoots,
@@ -242,6 +244,7 @@ export async function createMeetReplyDispatcher(opts) {
242
244
  await sendMessageMeet({
243
245
  cfg,
244
246
  to: chatId,
247
+ threadId,
245
248
  text: chunk,
246
249
  accountId,
247
250
  replyToMessageId,
@@ -290,6 +293,7 @@ export async function createMeetReplyDispatcher(opts) {
290
293
  await sendMessageMeet({
291
294
  cfg,
292
295
  to: chatId,
296
+ threadId,
293
297
  text: userMessage,
294
298
  accountId,
295
299
  runtime: opts.runtime,
@@ -0,0 +1,6 @@
1
+ export declare function buildMeetRoutePeerId(params: {
2
+ isGroup: boolean;
3
+ senderId: string;
4
+ chatId: string;
5
+ threadId?: string;
6
+ }): string;
@@ -0,0 +1,6 @@
1
+ export function buildMeetRoutePeerId(params) {
2
+ if (params.isGroup) {
3
+ return params.chatId;
4
+ }
5
+ return params.threadId ? `${params.senderId}__topic__${params.threadId}` : params.senderId;
6
+ }
@@ -25,3 +25,13 @@ export declare function msgContentToContext(msg: MsgContent, botUserId: string,
25
25
  export declare function enrichContextWithUserNames(ctx: MeetMessageContext, bot: MeetBot, accountId?: string): Promise<void>;
26
26
  export declare function parseTargetToSessionInfo(target: string, botUserId: number): SessionInfo;
27
27
  export declare function buildMeetTarget(sessionInfo: SessionInfo, botUserId: number): string;
28
+ export interface MergedForwardEntry {
29
+ sender: string;
30
+ body: string;
31
+ timestamp?: number;
32
+ messageId: string;
33
+ }
34
+ /**
35
+ * 解析合并转发消息的 content,展开为多条可读记录
36
+ */
37
+ export declare function parseMergedForwardContent(content: string): MergedForwardEntry[];
@@ -1,3 +1,4 @@
1
+ import { encodeMeetDmTopicId } from "./dm-topic-codec.js";
1
2
  import { rememberMeetUser } from "./directory-cache.js";
2
3
  export function mapSessionType(sessionType) {
3
4
  return sessionType === 1 ? "direct" : "channel";
@@ -159,6 +160,9 @@ export function msgContentToContext(msg, botUserId, quoteMsgMap = {}) {
159
160
  const chatId = chatType === "direct"
160
161
  ? `user:${msg.fromUid}`
161
162
  : `channel:${msg.sessionInfo.secondID}`;
163
+ const threadId = chatType === "direct" && typeof msg.dmTopicID === "string" && msg.dmTopicID.trim()
164
+ ? encodeMeetDmTopicId(msg.dmTopicID)
165
+ : undefined;
162
166
  const mentionedBot = msg.atIds?.includes(Number(botUserId)) ?? false;
163
167
  const replyContext = resolveQuoteMessage(msg, quoteMsgMap);
164
168
  const media = extractMediaAttachments(msg);
@@ -171,6 +175,7 @@ export function msgContentToContext(msg, botUserId, quoteMsgMap = {}) {
171
175
  }
172
176
  return {
173
177
  chatId,
178
+ threadId,
174
179
  messageId: String(msg.seqId ?? 0),
175
180
  senderId: String(msg.fromUid ?? 0),
176
181
  senderOpenId: String(msg.fromUid ?? 0),
@@ -302,3 +307,44 @@ export function buildMeetTarget(sessionInfo, botUserId) {
302
307
  }
303
308
  return `channel:${sessionInfo.secondID}`;
304
309
  }
310
+ /**
311
+ * 解析合并转发消息的 content,展开为多条可读记录
312
+ */
313
+ export function parseMergedForwardContent(content) {
314
+ let parsed;
315
+ try {
316
+ parsed = JSON.parse(content);
317
+ }
318
+ catch {
319
+ return [];
320
+ }
321
+ const msgs = parsed.msgs;
322
+ if (!Array.isArray(msgs) || msgs.length === 0) {
323
+ return [];
324
+ }
325
+ const profileMap = parsed.userProfileMap ?? {};
326
+ const buildAttachmentPlaceholder = (sub) => {
327
+ const attachments = [
328
+ ...(sub.extraInfo?.attechmentInfo ? [sub.extraInfo.attechmentInfo] : []),
329
+ ...(Array.isArray(sub.extraInfo?.attechmentInfos) ? sub.extraInfo.attechmentInfos : []),
330
+ ];
331
+ if (attachments.length === 0) {
332
+ return "";
333
+ }
334
+ return attachments
335
+ .map((attachment) => `[attachment:${attachment.fileName || attachment.mimeType || "file"}]`)
336
+ .join(" ");
337
+ };
338
+ return msgs.map((sub, i) => {
339
+ const senderId = String(sub.fromUid ?? 0);
340
+ const profile = profileMap[senderId];
341
+ const sender = profile?.nickName || senderId;
342
+ const attachmentPlaceholder = buildAttachmentPlaceholder(sub);
343
+ return {
344
+ sender,
345
+ body: [sub.content?.trim() || "", attachmentPlaceholder].filter(Boolean).join(" "),
346
+ timestamp: sub.timestamp,
347
+ messageId: String(sub.seqId ?? `merged-${i}`),
348
+ };
349
+ });
350
+ }
@@ -15,6 +15,11 @@ export declare function extractAtIds(text: string): {
15
15
  atIds: number[];
16
16
  };
17
17
  export declare function mergeAtIds(explicitAtIds?: number[], extractedAtIds?: number[]): number[];
18
+ export declare function resolveMeetThreadTarget<T extends {
19
+ sessionType: number;
20
+ }>(sessionInfo: T, threadId?: string): T | (T & {
21
+ threadId?: number;
22
+ });
18
23
  export type SendMessageMeetOpts = {
19
24
  cfg: ClawdbotConfig;
20
25
  to: string;
@@ -33,6 +38,7 @@ export declare function sendMessageMeet(opts: SendMessageMeetOpts): Promise<{
33
38
  export type SendMediaMeetOpts = {
34
39
  cfg: ClawdbotConfig;
35
40
  to: string;
41
+ threadId?: string;
36
42
  text?: string;
37
43
  mediaUrl: string;
38
44
  mediaLocalRoots?: readonly string[];
package/dist/src/send.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { resolveMeetAccount } from "./accounts.js";
2
2
  import { getMeetClient, createMeetClient } from "./client.js";
3
+ import { decodeMeetDmTopicThreadId } from "./dm-topic-codec.js";
3
4
  import { parseTargetToSessionInfo } from "./sdk-bridge.js";
4
5
  import { getMeetRuntime } from "./runtime.js";
5
6
  import { rewriteMeetKnownMentions } from "./mentions.js";
@@ -122,6 +123,25 @@ export function extractAtIds(text) {
122
123
  export function mergeAtIds(explicitAtIds, extractedAtIds) {
123
124
  return [...new Set([...(explicitAtIds ?? []), ...(extractedAtIds ?? [])])];
124
125
  }
126
+ // OpenClaw upper layers use a channel-agnostic `threadId` abstraction.
127
+ // Meet maps that abstraction differently by session type:
128
+ // - channel/group sessions use Meet `threadId`
129
+ // - direct-message sessions use Meet `dmTopicID`
130
+ export function resolveMeetThreadTarget(sessionInfo, threadId) {
131
+ if (!threadId) {
132
+ return sessionInfo;
133
+ }
134
+ if (sessionInfo.sessionType === 3) {
135
+ return {
136
+ ...sessionInfo,
137
+ threadId: Number(threadId),
138
+ };
139
+ }
140
+ if (sessionInfo.sessionType === 1) {
141
+ return sessionInfo;
142
+ }
143
+ return sessionInfo;
144
+ }
125
145
  export async function sendMessageMeet(opts) {
126
146
  const { cfg, to, text, accountId, threadId, atIds: explicitAtIds, runtime } = opts;
127
147
  const log = runtime?.log ?? console.log;
@@ -170,16 +190,13 @@ export async function sendMessageMeet(opts) {
170
190
  const finalAtIds = mergeAtIds(explicitAtIds, extractedAtIds);
171
191
  log(`send message to=${to} atIds=${finalAtIds.join(",") || "none"}`);
172
192
  const sessionInfo = parseTargetToSessionInfo(to, Number(botUserId));
173
- const finalSessionInfo = threadId && sessionInfo.sessionType === 3
174
- ? {
175
- ...sessionInfo,
176
- threadId: Number(threadId),
177
- }
178
- : sessionInfo;
193
+ const finalSessionInfo = resolveMeetThreadTarget(sessionInfo, threadId);
194
+ const dmTopicID = sessionInfo.sessionType === 1 ? decodeMeetDmTopicThreadId(threadId) : undefined;
179
195
  try {
180
196
  const result = await bot.sendMessage(finalSessionInfo, {
181
197
  content: cleanText,
182
198
  atIds: finalAtIds,
199
+ ...(dmTopicID ? { dmTopicID } : {}),
183
200
  });
184
201
  return {
185
202
  messageId: String(result.msgContent?.seqId ?? 0),
@@ -192,7 +209,7 @@ export async function sendMessageMeet(opts) {
192
209
  }
193
210
  }
194
211
  export async function sendMediaMeet(opts) {
195
- const { cfg, to, text, mediaUrl, mediaLocalRoots, accountId, onProgress, runtime: logRuntime } = opts;
212
+ const { cfg, to, threadId, text, mediaUrl, mediaLocalRoots, accountId, onProgress, runtime: logRuntime } = opts;
196
213
  const log = logRuntime?.log ?? console.log;
197
214
  const logError = logRuntime?.error ?? console.error;
198
215
  const account = resolveMeetAccount({ cfg, accountId });
@@ -242,6 +259,8 @@ export async function sendMediaMeet(opts) {
242
259
  throw new Error(`Media file too large: ${media.buffer.length} bytes (max: ${maxBytes})`);
243
260
  }
244
261
  const sessionInfo = parseTargetToSessionInfo(to, Number(botUserId));
262
+ const finalSessionInfo = resolveMeetThreadTarget(sessionInfo, threadId);
263
+ const dmTopicID = sessionInfo.sessionType === 1 ? decodeMeetDmTopicThreadId(threadId) : undefined;
245
264
  const rawFileName = media.fileName || "file";
246
265
  const contentType = resolveContentType(rawFileName, media.contentType);
247
266
  // 确保文件名有正确的扩展名
@@ -259,11 +278,12 @@ export async function sendMediaMeet(opts) {
259
278
  }
260
279
  : undefined;
261
280
  try {
262
- const result = await bot.sendMedia(sessionInfo, {
281
+ const result = await bot.sendMedia(finalSessionInfo, {
263
282
  buffer: media.buffer,
264
283
  fileName,
265
284
  contentType,
266
285
  content: text || "",
286
+ ...(dmTopicID ? { dmTopicID } : {}),
267
287
  onProgress: progressCallback,
268
288
  });
269
289
  return {
@@ -30,6 +30,7 @@ export type MeetMention = {
30
30
  };
31
31
  export type MeetMessageContext = {
32
32
  chatId: string;
33
+ threadId?: string;
33
34
  messageId: string;
34
35
  senderId: string;
35
36
  senderOpenId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meet-im/meet",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Meet channel plugin",
6
6
  "scripts": {
@@ -285,5 +285,5 @@ lxcli kb --help
285
285
  lxcli kb getTask --task_id 123
286
286
  lxcli kb createTask --help
287
287
  lxcli kb createComment --help
288
- npx skills add https://github.com/LynkTechnology/lxcli -y
288
+ npx skills add https://github.com/LynkTechnology/lxcli -g
289
289
  ```