@meet-im/meet 3.4.5 → 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.
@@ -0,0 +1,30 @@
1
+ import { type ChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime";
2
+ import type { ChannelApprovalCapability } from "openclaw/plugin-sdk/channel-contract";
3
+ import type { MeetExecApprovalConfig } from "./types.js";
4
+ type MeetPendingPayload = {
5
+ text: string;
6
+ };
7
+ type MeetPreparedTarget = {
8
+ to: string;
9
+ threadId?: string;
10
+ };
11
+ type MeetPendingEntry = {
12
+ chatId: string;
13
+ messageId: string;
14
+ };
15
+ export declare const meetApprovalNativeRuntime: ChannelApprovalNativeRuntimeAdapter<MeetPendingPayload, MeetPreparedTarget, MeetPendingEntry, never, unknown>;
16
+ export declare function createMeetNativeApprovalAdapter(configOverride?: MeetExecApprovalConfig | null): {
17
+ auth: {
18
+ authorizeActorAction?: ChannelApprovalCapability["authorizeActorAction"];
19
+ getActionAvailabilityState?: ChannelApprovalCapability["getActionAvailabilityState"];
20
+ getExecInitiatingSurfaceState?: ChannelApprovalCapability["getExecInitiatingSurfaceState"];
21
+ resolveApproveCommandBehavior?: ChannelApprovalCapability["resolveApproveCommandBehavior"];
22
+ };
23
+ delivery: ChannelApprovalCapability["delivery"];
24
+ nativeRuntime: ChannelApprovalCapability["nativeRuntime"];
25
+ render: ChannelApprovalCapability["render"];
26
+ native: ChannelApprovalCapability["native"];
27
+ describeExecApprovalSetup: ChannelApprovalCapability["describeExecApprovalSetup"];
28
+ };
29
+ export declare function getMeetApprovalCapability(): ChannelApprovalCapability;
30
+ export {};
@@ -0,0 +1,260 @@
1
+ import { createChannelApprovalNativeRuntimeAdapter, } from "openclaw/plugin-sdk/approval-handler-runtime";
2
+ import { buildChannelApprovalNativeTargetKey, resolveApprovalRequestSessionConversation, } from "openclaw/plugin-sdk/approval-native-runtime";
3
+ import { buildExecApprovalPendingReplyPayload, } from "openclaw/plugin-sdk/approval-runtime";
4
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, } from "openclaw/plugin-sdk/string-coerce-runtime";
5
+ import { listMeetAccountIds, resolveMeetAccount } from "./accounts.js";
6
+ import { createChannelApproverDmTargetResolver, createChannelNativeOriginTargetResolver, createApproverRestrictedNativeApprovalCapability, splitChannelApprovalCapability, } from "openclaw/plugin-sdk/approval-runtime";
7
+ import { getMeetExecApprovalApprovers, isMeetExecApprovalApprover, isMeetExecApprovalClientEnabled, } from "./exec-approvals.js";
8
+ import { encodeMeetDmTopicId } from "./dm-topic-codec.js";
9
+ import { sendMessageMeet } from "./send.js";
10
+ function shouldHandleMeetApprovalRequest(_params) {
11
+ return true;
12
+ }
13
+ function extractMeetSessionKind(sessionKey) {
14
+ if (!sessionKey) {
15
+ return null;
16
+ }
17
+ const match = sessionKey.match(/meet:(channel|group|dm):/);
18
+ if (!match) {
19
+ return null;
20
+ }
21
+ return match[1];
22
+ }
23
+ function normalizeMeetOriginChannelId(value) {
24
+ if (!value) {
25
+ return null;
26
+ }
27
+ const trimmed = value.trim();
28
+ if (!trimmed) {
29
+ return null;
30
+ }
31
+ const prefixed = trimmed.match(/^(?:channel):(-?\d+)$/i);
32
+ if (prefixed) {
33
+ return prefixed[1];
34
+ }
35
+ return /^-?\d+$/.test(trimmed) ? trimmed : null;
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
+ }
52
+ function normalizeMeetThreadId(value) {
53
+ if (typeof value === "number") {
54
+ return Number.isFinite(value) ? String(value) : undefined;
55
+ }
56
+ if (typeof value !== "string") {
57
+ return undefined;
58
+ }
59
+ const normalized = value.trim();
60
+ return normalized || undefined;
61
+ }
62
+ function createMeetOriginTargetResolver(_configOverride) {
63
+ return createChannelNativeOriginTargetResolver({
64
+ channel: "meet",
65
+ shouldHandleRequest: ({ cfg, accountId, request }) => shouldHandleMeetApprovalRequest({
66
+ cfg,
67
+ accountId,
68
+ request,
69
+ }),
70
+ resolveTurnSourceTarget: (request) => {
71
+ const sessionConversation = resolveApprovalRequestSessionConversation({
72
+ request,
73
+ channel: "meet",
74
+ bundledFallback: false,
75
+ });
76
+ const sessionKind = extractMeetSessionKind(normalizeOptionalString(request.request.sessionKey) ?? null);
77
+ const turnSourceChannel = normalizeLowercaseStringOrEmpty(request.request.turnSourceChannel);
78
+ const rawTurnSourceTo = normalizeOptionalString(request.request.turnSourceTo) ?? "";
79
+ const turnSourceTarget = normalizeMeetOriginTarget(rawTurnSourceTo);
80
+ const turnSourceTo = normalizeMeetOriginChannelId(rawTurnSourceTo);
81
+ const threadId = encodeMeetDmTopicId(normalizeMeetThreadId(request.request.turnSourceThreadId)) ??
82
+ normalizeMeetThreadId(sessionConversation?.threadId) ??
83
+ undefined;
84
+ const hasExplicitOriginTarget = /^(?:channel):/i.test(rawTurnSourceTo);
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) {
94
+ return null;
95
+ }
96
+ return hasExplicitOriginTarget || sessionKind === "channel" || sessionKind === "group"
97
+ ? { to: turnSourceTo, threadId }
98
+ : null;
99
+ },
100
+ resolveSessionTarget: (sessionTarget, request) => {
101
+ const sessionConversation = resolveApprovalRequestSessionConversation({
102
+ request,
103
+ channel: "meet",
104
+ bundledFallback: false,
105
+ });
106
+ const sessionKind = extractMeetSessionKind(request.request.sessionKey?.trim() || null);
107
+ if (sessionKind === "dm") {
108
+ return null;
109
+ }
110
+ const targetTo = normalizeMeetOriginChannelId(sessionTarget.to);
111
+ return targetTo
112
+ ? {
113
+ to: targetTo,
114
+ threadId: normalizeMeetThreadId(sessionTarget.threadId) ??
115
+ normalizeMeetThreadId(sessionConversation?.threadId) ??
116
+ undefined,
117
+ }
118
+ : null;
119
+ },
120
+ resolveFallbackTarget: (request) => {
121
+ const sessionConversation = resolveApprovalRequestSessionConversation({
122
+ request,
123
+ channel: "meet",
124
+ bundledFallback: false,
125
+ });
126
+ const sessionKind = extractMeetSessionKind(request.request.sessionKey?.trim() || null);
127
+ if (sessionKind === "dm") {
128
+ return null;
129
+ }
130
+ const fallbackChannelId = normalizeMeetOriginChannelId(sessionConversation?.id);
131
+ return fallbackChannelId
132
+ ? {
133
+ to: fallbackChannelId,
134
+ threadId: normalizeMeetThreadId(sessionConversation?.threadId) ?? undefined,
135
+ }
136
+ : null;
137
+ },
138
+ });
139
+ }
140
+ function createMeetApproverDmTargetResolver(configOverride) {
141
+ return createChannelApproverDmTargetResolver({
142
+ shouldHandleRequest: ({ cfg, accountId, request }) => shouldHandleMeetApprovalRequest({
143
+ cfg,
144
+ accountId,
145
+ request,
146
+ }),
147
+ resolveApprovers: ({ cfg, accountId }) => getMeetExecApprovalApprovers({ cfg, accountId, configOverride }),
148
+ mapApprover: (approver) => ({ to: `user:${approver}` }),
149
+ });
150
+ }
151
+ function buildMeetPendingPayload(params) {
152
+ if (params.view.approvalKind !== "exec") {
153
+ return {
154
+ text: params.view.title,
155
+ };
156
+ }
157
+ const payload = buildExecApprovalPendingReplyPayload({
158
+ approvalId: params.request.id,
159
+ approvalSlug: params.request.id.slice(0, 8),
160
+ approvalCommandId: params.request.id,
161
+ warningText: params.view.warningText ?? normalizeOptionalString(params.request.request.warningText) ?? undefined,
162
+ ask: params.view.ask ?? normalizeOptionalString(params.request.request.ask) ?? undefined,
163
+ agentId: params.view.agentId ?? normalizeOptionalString(params.request.request.agentId) ?? undefined,
164
+ allowedDecisions: params.view.actions.map((action) => action.decision),
165
+ command: params.view.commandText,
166
+ cwd: params.view.cwd ?? normalizeOptionalString(params.request.request.cwd) ?? undefined,
167
+ host: params.view.host === "node" ? "node" : "gateway",
168
+ nodeId: params.view.nodeId ?? normalizeOptionalString(params.request.request.nodeId) ?? undefined,
169
+ sessionKey: params.view.sessionKey ?? normalizeOptionalString(params.request.request.sessionKey) ?? undefined,
170
+ expiresAtMs: params.request.expiresAtMs,
171
+ nowMs: params.nowMs,
172
+ });
173
+ const reason = params.view.ask?.trim();
174
+ const text = reason ? `${reason}\n\n${payload.text ?? ""}` : (payload.text ?? "");
175
+ return {
176
+ text,
177
+ };
178
+ }
179
+ export const meetApprovalNativeRuntime = createChannelApprovalNativeRuntimeAdapter({
180
+ eventKinds: ["exec", "plugin"],
181
+ availability: {
182
+ isConfigured: ({ cfg, accountId }) => isMeetExecApprovalClientEnabled({ cfg, accountId }),
183
+ shouldHandle: ({ cfg, accountId, request }) => shouldHandleMeetApprovalRequest({
184
+ cfg,
185
+ accountId,
186
+ request,
187
+ }),
188
+ },
189
+ presentation: {
190
+ buildPendingPayload: ({ request, nowMs, view }) => buildMeetPendingPayload({
191
+ request: request,
192
+ nowMs,
193
+ view,
194
+ }),
195
+ buildResolvedResult: () => ({ kind: "leave" }),
196
+ buildExpiredResult: () => ({ kind: "leave" }),
197
+ },
198
+ transport: {
199
+ prepareTarget: ({ plannedTarget }) => ({
200
+ dedupeKey: buildChannelApprovalNativeTargetKey(plannedTarget.target),
201
+ target: {
202
+ to: plannedTarget.surface === "approver-dm" && !/^user:/i.test(plannedTarget.target.to)
203
+ ? `user:${plannedTarget.target.to}`
204
+ : plannedTarget.target.to,
205
+ ...(plannedTarget.target.threadId != null
206
+ ? { threadId: String(plannedTarget.target.threadId) }
207
+ : {}),
208
+ },
209
+ }),
210
+ deliverPending: async ({ cfg, accountId, preparedTarget, pendingPayload }) => {
211
+ try {
212
+ const result = await sendMessageMeet({
213
+ cfg: cfg,
214
+ to: preparedTarget.to,
215
+ text: pendingPayload.text,
216
+ accountId: accountId ?? undefined,
217
+ threadId: preparedTarget.threadId,
218
+ });
219
+ return {
220
+ chatId: result.chatId,
221
+ messageId: result.messageId,
222
+ };
223
+ }
224
+ catch {
225
+ return null;
226
+ }
227
+ },
228
+ },
229
+ });
230
+ function createMeetApprovalCapability(configOverride) {
231
+ return createApproverRestrictedNativeApprovalCapability({
232
+ channel: "meet",
233
+ channelLabel: "Meet",
234
+ describeExecApprovalSetup: ({ accountId, }) => {
235
+ const prefix = accountId && accountId !== "default"
236
+ ? `channels.meet.accounts.${accountId}`
237
+ : "channels.meet";
238
+ return `Approve it from the Web UI or terminal UI for now. Meet supports native exec approvals for this account. Configure \`${prefix}.execApprovals.approvers\` or \`commands.ownerAllowFrom\`; set \`${prefix}.execApprovals.enabled\` to \`auto\` or \`true\`.`;
239
+ },
240
+ listAccountIds: listMeetAccountIds,
241
+ hasApprovers: ({ cfg, accountId }) => getMeetExecApprovalApprovers({ cfg, accountId, configOverride }).length > 0,
242
+ isExecAuthorizedSender: ({ cfg, accountId, senderId }) => isMeetExecApprovalApprover({ cfg, accountId, senderId, configOverride }),
243
+ isNativeDeliveryEnabled: ({ cfg, accountId }) => isMeetExecApprovalClientEnabled({ cfg, accountId, configOverride }),
244
+ resolveNativeDeliveryMode: ({ cfg, accountId }) => configOverride?.target ??
245
+ resolveMeetAccount({ cfg, accountId }).config.execApprovals?.target ??
246
+ "dm",
247
+ resolveOriginTarget: createMeetOriginTargetResolver(configOverride),
248
+ resolveApproverDmTargets: createMeetApproverDmTargetResolver(configOverride),
249
+ notifyOriginWhenDmOnly: true,
250
+ nativeRuntime: meetApprovalNativeRuntime,
251
+ });
252
+ }
253
+ export function createMeetNativeApprovalAdapter(configOverride) {
254
+ return splitChannelApprovalCapability(createMeetApprovalCapability(configOverride));
255
+ }
256
+ let cachedMeetApprovalCapability;
257
+ export function getMeetApprovalCapability() {
258
+ cachedMeetApprovalCapability ??= createMeetApprovalCapability();
259
+ return cachedMeetApprovalCapability;
260
+ }
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)}`);
@@ -7,6 +7,7 @@ import { sendMessageMeet } from "./send.js";
7
7
  import { getMeetClient } from "./client.js";
8
8
  import { getAllCachedUsers, rememberMeetUser } from "./directory-cache.js";
9
9
  import { MeetPluginConfigSchema } from "./config-schema.js";
10
+ import { getMeetApprovalCapability } from "./approval-native.js";
10
11
  const meta = {
11
12
  id: "meet",
12
13
  label: "Meet",
@@ -298,6 +299,7 @@ export const meetPlugin = {
298
299
  listGroupsLive: async () => [],
299
300
  },
300
301
  outbound: meetOutbound,
302
+ approvalCapability: getMeetApprovalCapability(),
301
303
  status: {
302
304
  defaultRuntime: {
303
305
  accountId: DEFAULT_ACCOUNT_ID,
@@ -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);
@@ -16,6 +16,17 @@ export declare const MeetGroupConfigSchema: z.ZodObject<{
16
16
  disabled: "disabled";
17
17
  }>>;
18
18
  }, z.core.$strip>;
19
+ export declare const MeetExecApprovalConfigSchema: z.ZodObject<{
20
+ enabled: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>>;
21
+ approvers: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
22
+ target: z.ZodOptional<z.ZodEnum<{
23
+ dm: "dm";
24
+ channel: "channel";
25
+ both: "both";
26
+ }>>;
27
+ agentFilter: z.ZodOptional<z.ZodArray<z.ZodString>>;
28
+ sessionFilter: z.ZodOptional<z.ZodArray<z.ZodString>>;
29
+ }, z.core.$strip>;
19
30
  export declare const MeetAccountConfigSchema: z.ZodObject<{
20
31
  enabled: z.ZodOptional<z.ZodBoolean>;
21
32
  name: z.ZodOptional<z.ZodString>;
@@ -63,6 +74,17 @@ export declare const MeetAccountConfigSchema: z.ZodObject<{
63
74
  disabled: "disabled";
64
75
  }>>;
65
76
  }, z.core.$strip>>>;
77
+ execApprovals: z.ZodOptional<z.ZodObject<{
78
+ enabled: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>>;
79
+ approvers: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
80
+ target: z.ZodOptional<z.ZodEnum<{
81
+ dm: "dm";
82
+ channel: "channel";
83
+ both: "both";
84
+ }>>;
85
+ agentFilter: z.ZodOptional<z.ZodArray<z.ZodString>>;
86
+ sessionFilter: z.ZodOptional<z.ZodArray<z.ZodString>>;
87
+ }, z.core.$strip>>;
66
88
  }, z.core.$strip>;
67
89
  export declare const MeetConfigSchema: z.ZodObject<{
68
90
  enabled: z.ZodOptional<z.ZodBoolean>;
@@ -163,6 +185,28 @@ export declare const MeetConfigSchema: z.ZodObject<{
163
185
  disabled: "disabled";
164
186
  }>>;
165
187
  }, z.core.$strip>>>;
188
+ execApprovals: z.ZodOptional<z.ZodObject<{
189
+ enabled: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>>;
190
+ approvers: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
191
+ target: z.ZodOptional<z.ZodEnum<{
192
+ dm: "dm";
193
+ channel: "channel";
194
+ both: "both";
195
+ }>>;
196
+ agentFilter: z.ZodOptional<z.ZodArray<z.ZodString>>;
197
+ sessionFilter: z.ZodOptional<z.ZodArray<z.ZodString>>;
198
+ }, z.core.$strip>>;
166
199
  }, z.core.$strip>>>;
200
+ execApprovals: z.ZodOptional<z.ZodObject<{
201
+ enabled: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>>;
202
+ approvers: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
203
+ target: z.ZodOptional<z.ZodEnum<{
204
+ dm: "dm";
205
+ channel: "channel";
206
+ both: "both";
207
+ }>>;
208
+ agentFilter: z.ZodOptional<z.ZodArray<z.ZodString>>;
209
+ sessionFilter: z.ZodOptional<z.ZodArray<z.ZodString>>;
210
+ }, z.core.$strip>>;
167
211
  }, z.core.$strip>;
168
212
  export declare const MeetPluginConfigSchema: import("openclaw/plugin-sdk").ChannelConfigSchema;
@@ -13,6 +13,13 @@ export const MeetGroupConfigSchema = z.object({
13
13
  users: z.array(z.union([z.string(), z.number()])).optional(),
14
14
  groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional(),
15
15
  });
16
+ export const MeetExecApprovalConfigSchema = z.object({
17
+ enabled: z.union([z.boolean(), z.literal("auto")]).optional(),
18
+ approvers: z.array(z.union([z.string(), z.number()])).optional(),
19
+ target: z.enum(["dm", "channel", "both"]).optional(),
20
+ agentFilter: z.array(z.string()).optional(),
21
+ sessionFilter: z.array(z.string()).optional(),
22
+ });
16
23
  export const MeetAccountConfigSchema = z.object({
17
24
  enabled: z.boolean().optional(),
18
25
  name: z.string().optional(),
@@ -34,6 +41,7 @@ export const MeetAccountConfigSchema = z.object({
34
41
  mediaMaxMb: z.number().min(0).optional(),
35
42
  typingMode: z.enum(["none", "instant", "message"]).optional(),
36
43
  groups: z.record(z.string(), MeetGroupConfigSchema).optional(),
44
+ execApprovals: MeetExecApprovalConfigSchema.optional(),
37
45
  });
38
46
  export const MeetConfigSchema = z.object({
39
47
  enabled: z.boolean().optional(),
@@ -58,5 +66,6 @@ export const MeetConfigSchema = z.object({
58
66
  mediaMaxMb: z.number().min(0).optional(),
59
67
  typingMode: z.enum(["none", "instant", "message"]).optional(),
60
68
  accounts: z.record(z.string(), MeetAccountConfigSchema).optional(),
69
+ execApprovals: MeetExecApprovalConfigSchema.optional(),
61
70
  });
62
71
  export const MeetPluginConfigSchema = buildChannelConfigSchema(MeetConfigSchema);
@@ -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
+ }
@@ -0,0 +1,26 @@
1
+ import type { ChannelOutboundPayloadHint } from "openclaw/plugin-sdk/channel-contract";
2
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
3
+ import type { ReplyPayload } from "openclaw/plugin-sdk/reply-dispatch-runtime";
4
+ import type { MeetExecApprovalConfig } from "./types.js";
5
+ export declare function getMeetExecApprovalApprovers(params: {
6
+ cfg: OpenClawConfig;
7
+ accountId?: string | null;
8
+ configOverride?: MeetExecApprovalConfig | null;
9
+ }): string[];
10
+ export declare function isMeetExecApprovalClientEnabled(params: {
11
+ cfg: OpenClawConfig;
12
+ accountId?: string | null;
13
+ configOverride?: MeetExecApprovalConfig | null;
14
+ }): boolean;
15
+ export declare function isMeetExecApprovalApprover(params: {
16
+ cfg: OpenClawConfig;
17
+ accountId?: string | null;
18
+ senderId?: string | null;
19
+ configOverride?: MeetExecApprovalConfig | null;
20
+ }): boolean;
21
+ export declare function shouldSuppressLocalMeetExecApprovalPrompt(params: {
22
+ cfg: OpenClawConfig;
23
+ accountId?: string | null;
24
+ payload: ReplyPayload;
25
+ hint?: ChannelOutboundPayloadHint;
26
+ }): boolean;
@@ -0,0 +1,71 @@
1
+ import { resolveMeetAccount } from "./accounts.js";
2
+ import { getExecApprovalReplyMetadata, isChannelExecApprovalClientEnabledFromConfig, matchesApprovalRequestFilters, resolveApprovalApprovers, } from "openclaw/plugin-sdk/approval-runtime";
3
+ function normalizeMeetApproverId(value) {
4
+ const trimmed = value.trim();
5
+ if (!trimmed) {
6
+ return undefined;
7
+ }
8
+ // Meet 用户ID是数字
9
+ if (/^-?\d+$/.test(trimmed)) {
10
+ return trimmed;
11
+ }
12
+ // 支持 user:ID 格式
13
+ const match = trimmed.match(/^user:(-?\d+)$/i);
14
+ return match ? match[1] : undefined;
15
+ }
16
+ function resolveMeetOwnerApprovers(cfg) {
17
+ const ownerAllowFrom = cfg.commands?.ownerAllowFrom;
18
+ if (!Array.isArray(ownerAllowFrom) || ownerAllowFrom.length === 0) {
19
+ return [];
20
+ }
21
+ return resolveApprovalApprovers({
22
+ explicit: ownerAllowFrom,
23
+ normalizeApprover: (value) => normalizeMeetApproverId(String(value)),
24
+ });
25
+ }
26
+ export function getMeetExecApprovalApprovers(params) {
27
+ return resolveApprovalApprovers({
28
+ explicit: params.configOverride?.approvers ??
29
+ resolveMeetAccount(params).config.execApprovals?.approvers ??
30
+ resolveMeetOwnerApprovers(params.cfg),
31
+ normalizeApprover: (value) => normalizeMeetApproverId(String(value)),
32
+ });
33
+ }
34
+ export function isMeetExecApprovalClientEnabled(params) {
35
+ const config = params.configOverride ?? resolveMeetAccount(params).config.execApprovals;
36
+ return isChannelExecApprovalClientEnabledFromConfig({
37
+ enabled: config?.enabled,
38
+ approverCount: getMeetExecApprovalApprovers({
39
+ cfg: params.cfg,
40
+ accountId: params.accountId,
41
+ configOverride: params.configOverride,
42
+ }).length,
43
+ });
44
+ }
45
+ export function isMeetExecApprovalApprover(params) {
46
+ const senderId = params.senderId?.trim();
47
+ if (!senderId) {
48
+ return false;
49
+ }
50
+ return getMeetExecApprovalApprovers({
51
+ cfg: params.cfg,
52
+ accountId: params.accountId,
53
+ configOverride: params.configOverride,
54
+ }).includes(senderId);
55
+ }
56
+ export function shouldSuppressLocalMeetExecApprovalPrompt(params) {
57
+ const metadata = getExecApprovalReplyMetadata(params.payload);
58
+ const config = resolveMeetAccount(params).config.execApprovals;
59
+ return (params.hint?.kind === "approval-pending" &&
60
+ params.hint.nativeRouteActive === true &&
61
+ isMeetExecApprovalClientEnabled(params) &&
62
+ metadata !== null &&
63
+ matchesApprovalRequestFilters({
64
+ request: {
65
+ agentId: metadata.agentId,
66
+ sessionKey: metadata.sessionKey,
67
+ },
68
+ agentFilter: config?.agentFilter,
69
+ sessionFilter: config?.sessionFilter,
70
+ }));
71
+ }
@@ -1,2 +1,2 @@
1
- export declare const MEET_PLUGIN_VERSION = "3.4.5";
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.4.5";
1
+ export const MEET_PLUGIN_VERSION = "3.6.0";
2
2
  export const MEET_OPENCLAW_VERSION = "2026.5.18";
@@ -1,5 +1,6 @@
1
1
  import { getMeetRuntime } from "./runtime.js";
2
2
  import { sendMessageMeet, sendMediaMeet } from "./send.js";
3
+ import { shouldSuppressLocalMeetExecApprovalPrompt } from "./exec-approvals.js";
3
4
  export const meetOutbound = {
4
5
  deliveryMode: "direct",
5
6
  chunker: (text, limit) => {
@@ -31,4 +32,12 @@ export const meetOutbound = {
31
32
  });
32
33
  return { channel: "meet", messageId: result.messageId, chatId: result.chatId };
33
34
  },
35
+ shouldSuppressLocalPayloadPrompt: (params) => {
36
+ return shouldSuppressLocalMeetExecApprovalPrompt({
37
+ cfg: params.cfg,
38
+ accountId: params.accountId,
39
+ payload: params.payload,
40
+ hint: params.hint,
41
+ });
42
+ },
34
43
  };
@@ -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,11 +15,17 @@ 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;
21
26
  text: string;
22
27
  accountId?: string;
28
+ threadId?: string;
23
29
  replyToMessageId?: string;
24
30
  atIds?: number[];
25
31
  /** Optional runtime for consistent logging. If not provided, falls back to console. */
@@ -32,6 +38,7 @@ export declare function sendMessageMeet(opts: SendMessageMeetOpts): Promise<{
32
38
  export type SendMediaMeetOpts = {
33
39
  cfg: ClawdbotConfig;
34
40
  to: string;
41
+ threadId?: string;
35
42
  text?: string;
36
43
  mediaUrl: string;
37
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,8 +123,27 @@ 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
- const { cfg, to, text, accountId, atIds: explicitAtIds, runtime } = opts;
146
+ const { cfg, to, text, accountId, threadId, atIds: explicitAtIds, runtime } = opts;
127
147
  const log = runtime?.log ?? console.log;
128
148
  const logError = runtime?.error ?? console.error;
129
149
  const account = resolveMeetAccount({ cfg, accountId });
@@ -170,10 +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));
193
+ const finalSessionInfo = resolveMeetThreadTarget(sessionInfo, threadId);
194
+ const dmTopicID = sessionInfo.sessionType === 1 ? decodeMeetDmTopicThreadId(threadId) : undefined;
173
195
  try {
174
- const result = await bot.sendMessage(sessionInfo, {
196
+ const result = await bot.sendMessage(finalSessionInfo, {
175
197
  content: cleanText,
176
198
  atIds: finalAtIds,
199
+ ...(dmTopicID ? { dmTopicID } : {}),
177
200
  });
178
201
  return {
179
202
  messageId: String(result.msgContent?.seqId ?? 0),
@@ -186,7 +209,7 @@ export async function sendMessageMeet(opts) {
186
209
  }
187
210
  }
188
211
  export async function sendMediaMeet(opts) {
189
- const { cfg, to, text, mediaUrl, mediaLocalRoots, accountId, onProgress, runtime: logRuntime } = opts;
212
+ const { cfg, to, threadId, text, mediaUrl, mediaLocalRoots, accountId, onProgress, runtime: logRuntime } = opts;
190
213
  const log = logRuntime?.log ?? console.log;
191
214
  const logError = logRuntime?.error ?? console.error;
192
215
  const account = resolveMeetAccount({ cfg, accountId });
@@ -236,6 +259,8 @@ export async function sendMediaMeet(opts) {
236
259
  throw new Error(`Media file too large: ${media.buffer.length} bytes (max: ${maxBytes})`);
237
260
  }
238
261
  const sessionInfo = parseTargetToSessionInfo(to, Number(botUserId));
262
+ const finalSessionInfo = resolveMeetThreadTarget(sessionInfo, threadId);
263
+ const dmTopicID = sessionInfo.sessionType === 1 ? decodeMeetDmTopicThreadId(threadId) : undefined;
239
264
  const rawFileName = media.fileName || "file";
240
265
  const contentType = resolveContentType(rawFileName, media.contentType);
241
266
  // 确保文件名有正确的扩展名
@@ -253,11 +278,12 @@ export async function sendMediaMeet(opts) {
253
278
  }
254
279
  : undefined;
255
280
  try {
256
- const result = await bot.sendMedia(sessionInfo, {
281
+ const result = await bot.sendMedia(finalSessionInfo, {
257
282
  buffer: media.buffer,
258
283
  fileName,
259
284
  contentType,
260
285
  content: text || "",
286
+ ...(dmTopicID ? { dmTopicID } : {}),
261
287
  onProgress: progressCallback,
262
288
  });
263
289
  return {
@@ -1,10 +1,11 @@
1
1
  import type { SessionInfo, AttachmentInfo, UploadProgress } from "@meet-im/meet-bot-jssdk";
2
2
  import type { z } from "zod";
3
- import { MeetConfigSchema, MeetAccountConfigSchema, MeetChannelConfigSchema, MeetGroupConfigSchema } from "./config-schema.js";
3
+ import { MeetConfigSchema, MeetAccountConfigSchema, MeetChannelConfigSchema, MeetGroupConfigSchema, MeetExecApprovalConfigSchema } from "./config-schema.js";
4
4
  export type MeetConfig = z.infer<typeof MeetConfigSchema>;
5
5
  export type MeetAccountConfig = z.infer<typeof MeetAccountConfigSchema>;
6
6
  export type MeetChannelConfig = z.infer<typeof MeetChannelConfigSchema>;
7
7
  export type MeetGroupConfig = z.infer<typeof MeetGroupConfigSchema>;
8
+ export type MeetExecApprovalConfig = z.infer<typeof MeetExecApprovalConfigSchema>;
8
9
  export type { AttachmentInfo, UploadProgress };
9
10
  export type ResolvedMeetAccount = {
10
11
  accountId: string;
@@ -29,6 +30,7 @@ export type MeetMention = {
29
30
  };
30
31
  export type MeetMessageContext = {
31
32
  chatId: string;
33
+ threadId?: string;
32
34
  messageId: string;
33
35
  senderId: string;
34
36
  senderOpenId: string;
@@ -28,6 +28,22 @@
28
28
  "type": "array",
29
29
  "items": { "anyOf": [{ "type": "string" }, { "type": "number" }] }
30
30
  },
31
+ "execApprovals": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "properties": {
35
+ "enabled": {
36
+ "anyOf": [{ "type": "boolean" }, { "type": "string", "const": "auto" }]
37
+ },
38
+ "approvers": {
39
+ "type": "array",
40
+ "items": { "anyOf": [{ "type": "string" }, { "type": "number" }] }
41
+ },
42
+ "target": { "type": "string", "enum": ["dm", "channel", "both"] },
43
+ "agentFilter": { "type": "array", "items": { "type": "string" } },
44
+ "sessionFilter": { "type": "array", "items": { "type": "string" } }
45
+ }
46
+ },
31
47
  "requireMention": { "type": "boolean" },
32
48
  "systemPrompt": { "type": "string" },
33
49
  "channels": {
@@ -88,6 +104,22 @@
88
104
  "type": "array",
89
105
  "items": { "anyOf": [{ "type": "string" }, { "type": "number" }] }
90
106
  },
107
+ "execApprovals": {
108
+ "type": "object",
109
+ "additionalProperties": false,
110
+ "properties": {
111
+ "enabled": {
112
+ "anyOf": [{ "type": "boolean" }, { "type": "string", "const": "auto" }]
113
+ },
114
+ "approvers": {
115
+ "type": "array",
116
+ "items": { "anyOf": [{ "type": "string" }, { "type": "number" }] }
117
+ },
118
+ "target": { "type": "string", "enum": ["dm", "channel", "both"] },
119
+ "agentFilter": { "type": "array", "items": { "type": "string" } },
120
+ "sessionFilter": { "type": "array", "items": { "type": "string" } }
121
+ }
122
+ },
91
123
  "requireMention": { "type": "boolean" },
92
124
  "systemPrompt": { "type": "string" },
93
125
  "historyLimit": { "type": "number", "minimum": 0 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meet-im/meet",
3
- "version": "3.4.5",
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
  ```