@meet-im/meet 3.0.1 → 3.2.1

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.
@@ -1,2 +1,8 @@
1
+ import { initMeetUserAgent } from "./src/client.js";
2
+ import { resolveMeetOpenClawVersion, resolveMeetPluginVersion } from "./src/plugin-version.js";
1
3
  // Keep bundled registration fast: runtime wiring only needs the store setter.
4
+ initMeetUserAgent({
5
+ pluginVersion: resolveMeetPluginVersion(),
6
+ openclawVersion: resolveMeetOpenClawVersion(),
7
+ });
2
8
  export { setMeetRuntime } from "./src/runtime.js";
package/dist/src/bot.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
2
  import type { HistoryEntry } from "openclaw/plugin-sdk/reply-history";
3
3
  import type { MeetBot, MsgContent } from "@meet-im/meet-bot-jssdk";
4
- import type { ResolvedMeetAccount } from "./types.js";
4
+ import type { ResolvedMeetAccount, MeetMessageContext } from "./types.js";
5
5
  export declare function handleMeetMessage(params: {
6
6
  cfg: ClawdbotConfig;
7
7
  msg: MsgContent;
@@ -12,4 +12,5 @@ export declare function handleMeetMessage(params: {
12
12
  bot: MeetBot;
13
13
  groupHistories: Map<string, HistoryEntry[]>;
14
14
  quoteMsgMap?: Record<string, MsgContent>;
15
+ ctx?: MeetMessageContext;
15
16
  }): Promise<void>;
package/dist/src/bot.js CHANGED
@@ -12,12 +12,12 @@ function formatHistoryEntry(entry) {
12
12
  return `${entry.sender}: ${entry.body}`;
13
13
  }
14
14
  export async function handleMeetMessage(params) {
15
- const { cfg, msg, botUserId, runtime, accountId, account, bot, groupHistories, quoteMsgMap } = params;
15
+ const { cfg, msg, botUserId, runtime, accountId, account, bot, groupHistories, quoteMsgMap, ctx: providedCtx } = params;
16
16
  const log = runtime?.log ?? console.log;
17
17
  const error = runtime?.error ?? console.error;
18
18
  let ctx;
19
19
  try {
20
- ctx = msgContentToContext(msg, botUserId, quoteMsgMap);
20
+ ctx = providedCtx ?? msgContentToContext(msg, botUserId, quoteMsgMap);
21
21
  }
22
22
  catch (err) {
23
23
  error(`[${accountId}]: failed to parse message: ${String(err)}`);
@@ -235,9 +235,12 @@ export async function handleMeetMessage(params) {
235
235
  // 构建最终的消息内容
236
236
  // Discord 做法:文字优先,无文字时用媒体占位符
237
237
  // 媒体路径通过 MediaPaths 传递,mediaContext 仅作为 BodyForAgent 的补充描述
238
+ const mentionsContext = ctx.mentions && ctx.mentions.length > 0
239
+ ? `\n\nMentioned users: ${ctx.mentions.map((m) => `${m.name} (${m.userId})`).join(", ")}`
240
+ : "";
238
241
  const finalContent = ctx.content.trim()
239
- ? `${ctx.content.trim()}${mediaContext}`
240
- : (ctx.placeholder || "") + mediaContext;
242
+ ? `${ctx.content.trim()}${mentionsContext}${mediaContext}`
243
+ : (ctx.placeholder || "") + mentionsContext + mediaContext;
241
244
  // Discord 做法:跳过空内容消息
242
245
  if (!finalContent.trim() && mediaPaths.length === 0) {
243
246
  log(`[${accountId}]: skip message ${ctx.messageId} (empty content)`);
@@ -301,7 +304,7 @@ export async function handleMeetMessage(params) {
301
304
  SessionKey: route.sessionKey,
302
305
  AccountId: route.accountId,
303
306
  ChatType: isGroup ? "group" : "direct",
304
- GroupSubject: isGroup ? ctx.chatId : undefined,
307
+ GroupSubject: isGroup ? (groupConfig?.groupConfig?.name ?? ctx.chatId) : undefined,
305
308
  GroupSystemPrompt: systemPrompt,
306
309
  SenderName: ctx.senderName,
307
310
  SenderId: ctx.senderId,
@@ -313,6 +316,7 @@ export async function handleMeetMessage(params) {
313
316
  ReplyToId: ctx.replyContext?.messageId,
314
317
  ReplyToBody: ctx.replyContext?.content,
315
318
  ReplyToSender: ctx.replyContext?.senderId,
319
+ MentionedUsers: ctx.mentions,
316
320
  InboundHistory: inboundHistory,
317
321
  CommandAuthorized: true,
318
322
  OriginatingChannel: "meet",
@@ -4,6 +4,8 @@ import { resolveMeetAccount, listMeetAccountIds, resolveDefaultMeetAccountId, is
4
4
  import { meetOutbound } from "./outbound.js";
5
5
  import { parseMeetTarget, looksLikeMeetId, formatMeetTarget, } from "./targets.js";
6
6
  import { sendMessageMeet } from "./send.js";
7
+ import { getMeetClient } from "./client.js";
8
+ import { getAllCachedUsers, rememberMeetUser } from "./directory-cache.js";
7
9
  const meta = {
8
10
  id: "meet",
9
11
  label: "Meet",
@@ -339,9 +341,56 @@ export const meetPlugin = {
339
341
  },
340
342
  directory: {
341
343
  self: async () => null,
342
- listPeers: async () => [],
344
+ listPeers: async (params) => {
345
+ const { accountId, query, limit } = params;
346
+ if (!accountId)
347
+ return [];
348
+ const normalizedAccountId = normalizeAccountId(accountId);
349
+ const users = getAllCachedUsers({ accountId: normalizedAccountId, query, limit });
350
+ return users.map((u) => ({
351
+ kind: "user",
352
+ id: u.userId,
353
+ name: u.name,
354
+ handle: u.handles[0] ?? u.name,
355
+ }));
356
+ },
343
357
  listGroups: async () => [],
344
- listPeersLive: async () => [],
358
+ listPeersLive: async (params) => {
359
+ const { accountId, query, limit, runtime } = params;
360
+ const log = runtime?.log ?? console.log;
361
+ if (!query || !accountId) {
362
+ log(`[meet] listPeersLive: missing query or accountId`);
363
+ return [];
364
+ }
365
+ const normalizedAccountId = normalizeAccountId(accountId);
366
+ const bot = getMeetClient(normalizedAccountId);
367
+ if (!bot) {
368
+ log(`[meet] listPeersLive: no bot client for account ${normalizedAccountId}`);
369
+ return [];
370
+ }
371
+ try {
372
+ const users = await bot.searchUserByName(query);
373
+ log(`[meet] listPeersLive: searchUserByName("${query}") => ${users.length} users`);
374
+ const sliced = limit ? users.slice(0, limit) : users;
375
+ for (const user of sliced) {
376
+ rememberMeetUser({
377
+ accountId: normalizedAccountId,
378
+ userId: user.userID,
379
+ handles: [user.nickName, user.aliasName],
380
+ });
381
+ }
382
+ return sliced.map((u) => ({
383
+ kind: "user",
384
+ id: String(u.userID),
385
+ name: u.aliasName || u.nickName,
386
+ handle: u.aliasName || u.nickName,
387
+ }));
388
+ }
389
+ catch (err) {
390
+ log(`[meet] listPeersLive: searchUserByName error: ${String(err)}`);
391
+ return [];
392
+ }
393
+ },
345
394
  listGroupsLive: async () => [],
346
395
  },
347
396
  outbound: meetOutbound,
@@ -1,6 +1,12 @@
1
1
  import { MeetBot } from "@meet-im/meet-bot-jssdk";
2
2
  import type { PollingOptions } from "@meet-im/meet-bot-jssdk";
3
3
  import type { ResolvedMeetAccount } from "./types.js";
4
+ export declare function getPluginUserAgent(): string;
5
+ export declare function initMeetUserAgent(options: {
6
+ pluginVersion?: string;
7
+ openclawVersion?: string;
8
+ }): void;
9
+ export declare function setOpenClawVersion(version: string): void;
4
10
  export declare function createMeetClient(account: ResolvedMeetAccount): MeetBot;
5
11
  export declare function getPollingOptions(account: ResolvedMeetAccount): PollingOptions;
6
12
  export declare function getMeetClient(accountId: string): MeetBot | undefined;
@@ -1,4 +1,26 @@
1
+ import os from "node:os";
1
2
  import { MeetBot, POLLING } from "@meet-im/meet-bot-jssdk";
3
+ let _pluginVersion = "unknown";
4
+ let _openclawVersion = "unknown";
5
+ function buildUserAgent() {
6
+ return `MeetPlugin/${_pluginVersion} (Node/${process.versions.node}; ${os.platform()}; OpenClaw/${_openclawVersion})`;
7
+ }
8
+ export function getPluginUserAgent() {
9
+ return buildUserAgent();
10
+ }
11
+ export function initMeetUserAgent(options) {
12
+ if (options.pluginVersion) {
13
+ _pluginVersion = options.pluginVersion;
14
+ }
15
+ if (options.openclawVersion) {
16
+ _openclawVersion = options.openclawVersion;
17
+ }
18
+ }
19
+ export function setOpenClawVersion(version) {
20
+ if (version) {
21
+ _openclawVersion = version;
22
+ }
23
+ }
2
24
  const botInstances = new Map();
3
25
  export function createMeetClient(account) {
4
26
  const existing = botInstances.get(account.accountId);
@@ -18,6 +40,7 @@ export function createMeetClient(account) {
18
40
  longPollingTimeout: pollTimeoutSec,
19
41
  logLevel,
20
42
  useV2: true,
43
+ userAgent: buildUserAgent(),
21
44
  });
22
45
  botInstances.set(account.accountId, bot);
23
46
  return bot;
@@ -23,6 +23,31 @@ export declare const MeetAccountConfigSchema: z.ZodObject<{
23
23
  silent: "silent";
24
24
  info: "info";
25
25
  }>>;
26
+ dmPolicy: z.ZodOptional<z.ZodEnum<{
27
+ open: "open";
28
+ pairing: "pairing";
29
+ allowlist: "allowlist";
30
+ }>>;
31
+ allowFrom: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
32
+ groupPolicy: z.ZodOptional<z.ZodEnum<{
33
+ open: "open";
34
+ allowlist: "allowlist";
35
+ disabled: "disabled";
36
+ }>>;
37
+ groupAllowFrom: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
38
+ requireMention: z.ZodOptional<z.ZodBoolean>;
39
+ systemPrompt: z.ZodOptional<z.ZodString>;
40
+ historyLimit: z.ZodOptional<z.ZodNumber>;
41
+ dmHistoryLimit: z.ZodOptional<z.ZodNumber>;
42
+ textChunkLimit: z.ZodOptional<z.ZodNumber>;
43
+ mediaMaxMb: z.ZodOptional<z.ZodNumber>;
44
+ groups: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
45
+ enabled: z.ZodOptional<z.ZodBoolean>;
46
+ name: z.ZodOptional<z.ZodString>;
47
+ requireMention: z.ZodOptional<z.ZodBoolean>;
48
+ systemPrompt: z.ZodOptional<z.ZodString>;
49
+ users: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
50
+ }, z.core.$strip>>>;
26
51
  }, z.core.$strip>;
27
52
  export declare const MeetConfigSchema: z.ZodObject<{
28
53
  enabled: z.ZodOptional<z.ZodBoolean>;
@@ -78,5 +103,30 @@ export declare const MeetConfigSchema: z.ZodObject<{
78
103
  silent: "silent";
79
104
  info: "info";
80
105
  }>>;
106
+ dmPolicy: z.ZodOptional<z.ZodEnum<{
107
+ open: "open";
108
+ pairing: "pairing";
109
+ allowlist: "allowlist";
110
+ }>>;
111
+ allowFrom: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
112
+ groupPolicy: z.ZodOptional<z.ZodEnum<{
113
+ open: "open";
114
+ allowlist: "allowlist";
115
+ disabled: "disabled";
116
+ }>>;
117
+ groupAllowFrom: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
118
+ requireMention: z.ZodOptional<z.ZodBoolean>;
119
+ systemPrompt: z.ZodOptional<z.ZodString>;
120
+ historyLimit: z.ZodOptional<z.ZodNumber>;
121
+ dmHistoryLimit: z.ZodOptional<z.ZodNumber>;
122
+ textChunkLimit: z.ZodOptional<z.ZodNumber>;
123
+ mediaMaxMb: z.ZodOptional<z.ZodNumber>;
124
+ groups: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
125
+ enabled: z.ZodOptional<z.ZodBoolean>;
126
+ name: z.ZodOptional<z.ZodString>;
127
+ requireMention: z.ZodOptional<z.ZodBoolean>;
128
+ systemPrompt: z.ZodOptional<z.ZodString>;
129
+ users: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
130
+ }, z.core.$strip>>>;
81
131
  }, z.core.$strip>>>;
82
132
  }, z.core.$strip>;
@@ -20,6 +20,17 @@ export const MeetAccountConfigSchema = z.object({
20
20
  pollTimeout: z.number().min(1000).max(300000).optional(),
21
21
  pollLimit: z.number().min(1).max(1000).optional(),
22
22
  logLevel: z.enum(["silent", "info"]).optional(),
23
+ dmPolicy: z.enum(["open", "pairing", "allowlist"]).optional(),
24
+ allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
25
+ groupPolicy: z.enum(["open", "allowlist", "disabled"]).optional(),
26
+ groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
27
+ requireMention: z.boolean().optional(),
28
+ systemPrompt: z.string().optional(),
29
+ historyLimit: z.number().min(0).optional(),
30
+ dmHistoryLimit: z.number().min(0).optional(),
31
+ textChunkLimit: z.number().min(1).optional(),
32
+ mediaMaxMb: z.number().min(0).optional(),
33
+ groups: z.record(z.string(), MeetGroupConfigSchema).optional(),
23
34
  });
24
35
  export const MeetConfigSchema = z.object({
25
36
  enabled: z.boolean().optional(),
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Meet 用户目录缓存
3
+ * 用于出站消息中 @handle → <@userId> 转换
4
+ */
5
+ /**
6
+ * 注册用户到目录缓存
7
+ */
8
+ export declare function rememberMeetUser(params: {
9
+ accountId: string;
10
+ userId: number;
11
+ handles: (string | null | undefined)[];
12
+ }): void;
13
+ /**
14
+ * 从目录缓存查找 userId
15
+ */
16
+ export declare function resolveMeetUserId(params: {
17
+ accountId?: string | null;
18
+ handle: string;
19
+ }): string | null;
20
+ /**
21
+ * 清除指定账户的缓存
22
+ */
23
+ export declare function clearMeetDirectoryCache(accountId: string): void;
24
+ /**
25
+ * 清除所有缓存
26
+ */
27
+ export declare function clearAllMeetDirectoryCache(): void;
28
+ /**
29
+ * 获取缓存大小(用于测试)
30
+ */
31
+ export declare function getMeetDirectoryCacheSize(accountId: string): number;
32
+ /**
33
+ * 获取指定账户的所有缓存用户(用于 listPeers)
34
+ */
35
+ export declare function getAllCachedUsers(params: {
36
+ accountId?: string | null;
37
+ query?: string | null;
38
+ limit?: number | null;
39
+ }): {
40
+ userId: string;
41
+ name: string;
42
+ handles: string[];
43
+ }[];
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Meet 用户目录缓存
3
+ * 用于出站消息中 @handle → <@userId> 转换
4
+ */
5
+ const DIRECTORY_CACHE_MAX_ENTRIES = 4000;
6
+ const DIRECTORY_HANDLE_CACHE = new Map();
7
+ const DIRECTORY_USER_CACHE = new Map();
8
+ function getAccountCache(accountId) {
9
+ let cache = DIRECTORY_HANDLE_CACHE.get(accountId);
10
+ if (!cache) {
11
+ cache = new Map();
12
+ DIRECTORY_HANDLE_CACHE.set(accountId, cache);
13
+ }
14
+ return cache;
15
+ }
16
+ function getUserCache(accountId) {
17
+ let cache = DIRECTORY_USER_CACHE.get(accountId);
18
+ if (!cache) {
19
+ cache = new Map();
20
+ DIRECTORY_USER_CACHE.set(accountId, cache);
21
+ }
22
+ return cache;
23
+ }
24
+ function normalizeHandle(raw) {
25
+ const handle = raw?.trim().toLowerCase();
26
+ if (!handle)
27
+ return null;
28
+ return handle;
29
+ }
30
+ /**
31
+ * 注册用户到目录缓存
32
+ */
33
+ export function rememberMeetUser(params) {
34
+ const { accountId, userId, handles } = params;
35
+ const cache = getAccountCache(accountId);
36
+ const userCache = getUserCache(accountId);
37
+ const userIdStr = String(userId);
38
+ const validHandles = [];
39
+ for (const rawHandle of handles) {
40
+ const handle = normalizeHandle(rawHandle ?? "");
41
+ if (!handle)
42
+ continue;
43
+ validHandles.push(rawHandle ?? handle);
44
+ if (cache.size >= DIRECTORY_CACHE_MAX_ENTRIES && !cache.has(handle)) {
45
+ const firstKey = cache.keys().next().value;
46
+ if (firstKey)
47
+ cache.delete(firstKey);
48
+ }
49
+ cache.set(handle, userIdStr);
50
+ }
51
+ // 存储用户信息(用第一个非空 handle 作为 name)
52
+ if (validHandles.length > 0) {
53
+ const existing = userCache.get(userIdStr);
54
+ const name = validHandles[0];
55
+ if (existing) {
56
+ // 合并 handles
57
+ const mergedHandles = [...new Set([...existing.handles, ...validHandles])];
58
+ userCache.set(userIdStr, { name: existing.name || name, handles: mergedHandles });
59
+ }
60
+ else {
61
+ userCache.set(userIdStr, { name, handles: validHandles });
62
+ }
63
+ }
64
+ }
65
+ /**
66
+ * 从目录缓存查找 userId
67
+ */
68
+ export function resolveMeetUserId(params) {
69
+ const { accountId, handle } = params;
70
+ if (!accountId)
71
+ return null;
72
+ const normalized = normalizeHandle(handle);
73
+ if (!normalized)
74
+ return null;
75
+ const cache = DIRECTORY_HANDLE_CACHE.get(accountId);
76
+ if (!cache)
77
+ return null;
78
+ return cache.get(normalized) ?? null;
79
+ }
80
+ /**
81
+ * 清除指定账户的缓存
82
+ */
83
+ export function clearMeetDirectoryCache(accountId) {
84
+ DIRECTORY_HANDLE_CACHE.delete(accountId);
85
+ DIRECTORY_USER_CACHE.delete(accountId);
86
+ }
87
+ /**
88
+ * 清除所有缓存
89
+ */
90
+ export function clearAllMeetDirectoryCache() {
91
+ DIRECTORY_HANDLE_CACHE.clear();
92
+ DIRECTORY_USER_CACHE.clear();
93
+ }
94
+ /**
95
+ * 获取缓存大小(用于测试)
96
+ */
97
+ export function getMeetDirectoryCacheSize(accountId) {
98
+ return DIRECTORY_HANDLE_CACHE.get(accountId)?.size ?? 0;
99
+ }
100
+ /**
101
+ * 获取指定账户的所有缓存用户(用于 listPeers)
102
+ */
103
+ export function getAllCachedUsers(params) {
104
+ const { accountId, query, limit } = params;
105
+ if (!accountId)
106
+ return [];
107
+ const userCache = DIRECTORY_USER_CACHE.get(accountId);
108
+ if (!userCache)
109
+ return [];
110
+ const normalizedQuery = query?.trim().toLowerCase() ?? "";
111
+ const results = [];
112
+ for (const [userId, info] of userCache) {
113
+ if (normalizedQuery) {
114
+ const matchesQuery = info.name.toLowerCase().includes(normalizedQuery) ||
115
+ info.handles.some((h) => h.toLowerCase().includes(normalizedQuery)) ||
116
+ userId === normalizedQuery;
117
+ if (!matchesQuery)
118
+ continue;
119
+ }
120
+ results.push({ userId, ...info });
121
+ if (limit && results.length >= limit)
122
+ break;
123
+ }
124
+ return results;
125
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Meet @提及 重写
3
+ * 将 @handle 转换为 <@userId> 格式
4
+ */
5
+ /**
6
+ * 重写消息中的 @handle 为 <@userId> 格式
7
+ * 排除代码块中的内容
8
+ */
9
+ export declare function rewriteMeetKnownMentions(text: string, accountId?: string | null): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Meet @提及 重写
3
+ * 将 @handle 转换为 <@userId> 格式
4
+ */
5
+ import { resolveMeetUserId } from "./directory-cache.js";
6
+ const MARKDOWN_CODE_PATTERN = /```[\s\S]*?```|`[^`\n]*`/g;
7
+ const MENTION_CANDIDATE_PATTERN = /(^|[\s([{"'.,;:!?])@([^\s`@<>{}\[\],;:!?]+(?:([^)\s`@<>{}\[\],;:!?]+))?)/gu;
8
+ const EXPLICIT_MENTION_WITH_ID_PATTERN = /@([^\s`@<>{}\[\],;:!?]+(?:([^)\s`@<>{}\[\],;:!?]+))?)\s*[((](\d+)[))]/gu;
9
+ function rewriteMentionsInSegment(text, accountId) {
10
+ if (!text.includes("@")) {
11
+ return text;
12
+ }
13
+ return text.replace(MENTION_CANDIDATE_PATTERN, (match, prefix, handle) => {
14
+ if (!handle)
15
+ return match;
16
+ const userId = resolveMeetUserId({ accountId, handle });
17
+ if (!userId)
18
+ return match;
19
+ return `${prefix ?? ""}<@${userId}>`;
20
+ });
21
+ }
22
+ /**
23
+ * 重写消息中的 @handle 为 <@userId> 格式
24
+ * 排除代码块中的内容
25
+ */
26
+ export function rewriteMeetKnownMentions(text, accountId) {
27
+ if (!text.includes("@")) {
28
+ return text;
29
+ }
30
+ const withExplicitIds = text.replace(EXPLICIT_MENTION_WITH_ID_PATTERN, (_, _name, id) => {
31
+ return `<@${id}>`;
32
+ });
33
+ let rewritten = "";
34
+ let offset = 0;
35
+ MARKDOWN_CODE_PATTERN.lastIndex = 0;
36
+ for (const match of withExplicitIds.matchAll(MARKDOWN_CODE_PATTERN)) {
37
+ const matchIndex = match.index ?? 0;
38
+ rewritten += rewriteMentionsInSegment(withExplicitIds.slice(offset, matchIndex), accountId);
39
+ rewritten += match[0];
40
+ offset = matchIndex + match[0].length;
41
+ }
42
+ rewritten += rewriteMentionsInSegment(withExplicitIds.slice(offset), accountId);
43
+ return rewritten;
44
+ }
@@ -2,7 +2,7 @@ import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
2
2
  import { resolveMeetAccount, listEnabledMeetAccounts } from "./accounts.js";
3
3
  import { createMeetClient, closeMeetClient, closeAllMeetClients, getPollingOptions } from "./client.js";
4
4
  import { handleMeetMessage } from "./bot.js";
5
- import { msgContentToContext } from "./sdk-bridge.js";
5
+ import { msgContentToContext, enrichContextWithUserNames } from "./sdk-bridge.js";
6
6
  export async function monitorMeetProvider(opts = {}) {
7
7
  const cfg = opts.config;
8
8
  if (!cfg) {
@@ -66,8 +66,9 @@ async function monitorSingleAccount(params) {
66
66
  abortSignal?.addEventListener("abort", handleAbort, { once: true });
67
67
  bot.on("message", ({ message, quoteMsgMap }) => {
68
68
  let queueKey;
69
+ let ctx;
69
70
  try {
70
- const ctx = msgContentToContext(message, botUserId, quoteMsgMap);
71
+ ctx = msgContentToContext(message, botUserId, quoteMsgMap);
71
72
  queueKey = ctx.chatId;
72
73
  }
73
74
  catch (err) {
@@ -80,6 +81,7 @@ async function monitorSingleAccount(params) {
80
81
  log(`[${accountId}]: enqueue message to queue=${queueKey}, queues=${queueSize}, pending=${pendingInQueue}`);
81
82
  messageQueue.enqueue(queueKey, async () => {
82
83
  try {
84
+ await enrichContextWithUserNames(ctx, bot, accountId);
83
85
  await handleMeetMessage({
84
86
  cfg,
85
87
  msg: message,
@@ -90,6 +92,7 @@ async function monitorSingleAccount(params) {
90
92
  bot,
91
93
  groupHistories,
92
94
  quoteMsgMap,
95
+ ctx,
93
96
  });
94
97
  }
95
98
  catch (err) {
@@ -0,0 +1,2 @@
1
+ export declare function resolveMeetPluginVersion(): string;
2
+ export declare function resolveMeetOpenClawVersion(): string;
@@ -0,0 +1,7 @@
1
+ import packageJson from "../package.json" with { type: "json" };
2
+ export function resolveMeetPluginVersion() {
3
+ return packageJson.version;
4
+ }
5
+ export function resolveMeetOpenClawVersion() {
6
+ return packageJson.peerDependencies?.openclaw ?? packageJson.devDependencies?.openclaw ?? "unknown";
7
+ }
@@ -23,7 +23,12 @@ export type CreateMeetReplyDispatcherOpts = {
23
23
  };
24
24
  export declare function createMeetReplyDispatcher(opts: CreateMeetReplyDispatcherOpts): Promise<{
25
25
  dispatcher: import("node_modules/openclaw/dist/plugin-sdk/src/auto-reply/reply/reply-dispatcher.types.js").ReplyDispatcher;
26
- replyOptions: Pick<import("node_modules/openclaw/dist/plugin-sdk/src/auto-reply/get-reply-options.types.js").GetReplyOptions, "onReplyStart" | "onTypingController" | "onTypingCleanup">;
26
+ replyOptions: {
27
+ sourceReplyDeliveryMode?: import("openclaw/plugin-sdk/channel-reply-pipeline").SourceReplyDeliveryMode | undefined;
28
+ onReplyStart?: (() => Promise<void> | void) | undefined;
29
+ onTypingController?: ((typing: import("node_modules/openclaw/dist/plugin-sdk/src/auto-reply/reply/typing.js").TypingController) => void) | undefined;
30
+ onTypingCleanup?: (() => void) | undefined;
31
+ };
27
32
  markDispatchIdle: () => void;
28
33
  markRunComplete: () => void;
29
34
  }>;
@@ -1,6 +1,25 @@
1
+ import { resolveChannelSourceReplyDeliveryMode } from "openclaw/plugin-sdk/channel-reply-pipeline";
1
2
  import { createReplyPrefixContext } from "openclaw/plugin-sdk/channel-runtime";
2
3
  import { getMeetRuntime } from "./runtime.js";
3
4
  import { sendMessageMeet, sendMediaMeet } from "./send.js";
5
+ function resolveMeetConversationType(chatId) {
6
+ if (chatId.startsWith("channel:")) {
7
+ return "group";
8
+ }
9
+ if (chatId.startsWith("user:")) {
10
+ return "direct";
11
+ }
12
+ return undefined;
13
+ }
14
+ function resolveMeetChatType(chatId) {
15
+ if (chatId.startsWith("channel:")) {
16
+ return "channel";
17
+ }
18
+ if (chatId.startsWith("user:")) {
19
+ return "direct";
20
+ }
21
+ return undefined;
22
+ }
4
23
  /**
5
24
  * 匹配末尾不完整的 mention 开始: <@ 或 <@xxx (没有闭合的 >)
6
25
  */
@@ -61,9 +80,21 @@ export async function createMeetReplyDispatcher(opts) {
61
80
  });
62
81
  const chunkMode = core.channel.text.resolveChunkMode(cfg, "meet", accountId);
63
82
  const prefixContext = createReplyPrefixContext({ cfg, agentId });
83
+ const chatType = resolveMeetChatType(chatId);
84
+ const sourceReplyDeliveryMode = chatType
85
+ ? resolveChannelSourceReplyDeliveryMode({
86
+ cfg,
87
+ ctx: { ChatType: chatType },
88
+ })
89
+ : undefined;
64
90
  const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } = core.channel.reply.createReplyDispatcherWithTyping({
65
91
  responsePrefix: prefixContext.responsePrefix,
66
92
  responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
93
+ silentReplyContext: {
94
+ cfg,
95
+ surface: "meet",
96
+ conversationType: resolveMeetConversationType(chatId),
97
+ },
67
98
  humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
68
99
  onReplyStart: async () => {
69
100
  },
@@ -166,7 +197,10 @@ export async function createMeetReplyDispatcher(opts) {
166
197
  });
167
198
  return {
168
199
  dispatcher,
169
- replyOptions,
200
+ replyOptions: {
201
+ ...replyOptions,
202
+ ...(sourceReplyDeliveryMode ? { sourceReplyDeliveryMode } : {}),
203
+ },
170
204
  markDispatchIdle,
171
205
  markRunComplete,
172
206
  };
@@ -7,5 +7,6 @@ export type MeetRuntime = PluginRuntime & {
7
7
  meet?: MeetChannelRuntime;
8
8
  };
9
9
  };
10
- declare const setMeetRuntime: (next: MeetRuntime) => void, getOptionalMeetRuntime: () => MeetRuntime | null, getMeetRuntime: () => MeetRuntime;
11
- export { getMeetRuntime, getOptionalMeetRuntime, setMeetRuntime };
10
+ declare const getOptionalMeetRuntime: () => MeetRuntime | null, getMeetRuntime: () => MeetRuntime;
11
+ export declare function setMeetRuntime(runtime: MeetRuntime): void;
12
+ export { getMeetRuntime, getOptionalMeetRuntime };
@@ -1,6 +1,13 @@
1
1
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
2
- const { setRuntime: setMeetRuntime, tryGetRuntime: getOptionalMeetRuntime, getRuntime: getMeetRuntime, } = createPluginRuntimeStore({
2
+ import { setOpenClawVersion } from "./client.js";
3
+ const { setRuntime: setMeetRuntimeStore, tryGetRuntime: getOptionalMeetRuntime, getRuntime: getMeetRuntime, } = createPluginRuntimeStore({
3
4
  pluginId: "meet",
4
5
  errorMessage: "Meet runtime not initialized",
5
6
  });
6
- export { getMeetRuntime, getOptionalMeetRuntime, setMeetRuntime };
7
+ export function setMeetRuntime(runtime) {
8
+ if (runtime.version) {
9
+ setOpenClawVersion(runtime.version);
10
+ }
11
+ setMeetRuntimeStore(runtime);
12
+ }
13
+ export { getMeetRuntime, getOptionalMeetRuntime };
@@ -1,4 +1,4 @@
1
- import type { MsgContent, SessionInfo, SessionType } from "@meet-im/meet-bot-jssdk";
1
+ import type { MsgContent, SessionInfo, SessionType, MeetBot } from "@meet-im/meet-bot-jssdk";
2
2
  import type { MeetMessageContext, MeetReplyContext, MeetMediaAttachment } from "./types.js";
3
3
  export type QuoteMsgMap = Record<string, MsgContent>;
4
4
  export declare function mapSessionType(sessionType: SessionType): "direct" | "channel";
@@ -17,5 +17,10 @@ export declare function resolveQuoteMessage(msg: MsgContent, quoteMsgMap: QuoteM
17
17
  */
18
18
  export declare function extractQuoteMessageMedia(msg: MsgContent, quoteMsgMap: QuoteMsgMap): MeetMediaAttachment[] | undefined;
19
19
  export declare function msgContentToContext(msg: MsgContent, botUserId: string, quoteMsgMap?: QuoteMsgMap): MeetMessageContext;
20
+ /**
21
+ * 从缓存中填充 senderName 和 atIds 对应的用户名
22
+ * 同时将用户名注册到目录缓存,用于出站 @提及 转换
23
+ */
24
+ export declare function enrichContextWithUserNames(ctx: MeetMessageContext, bot: MeetBot, accountId?: string): Promise<void>;
20
25
  export declare function parseTargetToSessionInfo(target: string, botUserId: number): SessionInfo;
21
26
  export declare function buildMeetTarget(sessionInfo: SessionInfo, botUserId: number): string;
@@ -1,3 +1,4 @@
1
+ import { rememberMeetUser } from "./directory-cache.js";
1
2
  export function mapSessionType(sessionType) {
2
3
  return sessionType === 1 ? "direct" : "channel";
3
4
  }
@@ -177,6 +178,86 @@ export function msgContentToContext(msg, botUserId, quoteMsgMap = {}) {
177
178
  media,
178
179
  };
179
180
  }
181
+ /**
182
+ * 从缓存中填充 senderName 和 atIds 对应的用户名
183
+ * 同时将用户名注册到目录缓存,用于出站 @提及 转换
184
+ */
185
+ export async function enrichContextWithUserNames(ctx, bot, accountId) {
186
+ const userIds = new Set();
187
+ const senderId = Number(ctx.senderId);
188
+ if (senderId > 0)
189
+ userIds.add(senderId);
190
+ if (ctx.atIds) {
191
+ for (const id of ctx.atIds)
192
+ userIds.add(id);
193
+ }
194
+ if (ctx.replyContext?.senderId) {
195
+ const replySenderId = Number(ctx.replyContext.senderId);
196
+ if (replySenderId > 0)
197
+ userIds.add(replySenderId);
198
+ }
199
+ if (userIds.size === 0)
200
+ return;
201
+ const users = await bot.getUserByIds([...userIds]);
202
+ const sender = users.get(senderId);
203
+ if (sender) {
204
+ ctx.senderName = sender.aliasName || sender.nickName;
205
+ if (accountId) {
206
+ rememberMeetUser({
207
+ accountId,
208
+ userId: senderId,
209
+ handles: [sender.nickName, sender.aliasName],
210
+ });
211
+ }
212
+ }
213
+ if (ctx.replyContext?.senderId) {
214
+ const replySender = users.get(Number(ctx.replyContext.senderId));
215
+ if (replySender) {
216
+ ctx.replyContext.senderName = replySender.aliasName || replySender.nickName;
217
+ if (accountId) {
218
+ rememberMeetUser({
219
+ accountId,
220
+ userId: replySender.userID,
221
+ handles: [replySender.nickName, replySender.aliasName],
222
+ });
223
+ }
224
+ }
225
+ }
226
+ if (ctx.atIds && ctx.atIds.length > 0) {
227
+ const mentionedNames = [];
228
+ const mentions = new Map();
229
+ for (const atId of ctx.atIds) {
230
+ const user = users.get(atId);
231
+ if (user) {
232
+ const displayName = user.aliasName || user.nickName;
233
+ mentions.set(String(atId), displayName);
234
+ if (accountId) {
235
+ rememberMeetUser({
236
+ accountId,
237
+ userId: atId,
238
+ handles: [user.nickName, user.aliasName],
239
+ });
240
+ }
241
+ // 替换文本中已有的 <@id> 格式
242
+ const nextContent = ctx.content.replace(new RegExp(`<@${atId}>`, "g"), `@${displayName}`);
243
+ const replaced = nextContent !== ctx.content;
244
+ ctx.content = nextContent;
245
+ // 如果文本中没有对应的 <@id> 被替换,则追加到列表
246
+ if (!replaced) {
247
+ mentionedNames.push(displayName);
248
+ }
249
+ }
250
+ }
251
+ ctx.mentions = [...mentions.entries()].map(([userId, name]) => ({ userId, name }));
252
+ // 如果有未被文本包含的 @提及,追加到内容末尾
253
+ if (mentionedNames.length > 0) {
254
+ const mentionText = mentionedNames.map((n) => `@${n}`).join(" ");
255
+ ctx.content = ctx.content.trim()
256
+ ? `${ctx.content} ${mentionText}`
257
+ : mentionText;
258
+ }
259
+ }
260
+ }
180
261
  export function parseTargetToSessionInfo(target, botUserId) {
181
262
  const userMatch = target.match(/^user:(\d+)$/);
182
263
  if (userMatch) {
package/dist/src/send.js CHANGED
@@ -2,6 +2,7 @@ import { resolveMeetAccount } from "./accounts.js";
2
2
  import { getMeetClient, createMeetClient } from "./client.js";
3
3
  import { parseTargetToSessionInfo } from "./sdk-bridge.js";
4
4
  import { getMeetRuntime } from "./runtime.js";
5
+ import { rewriteMeetKnownMentions } from "./mentions.js";
5
6
  const MENTION_PATTERN = /<@(-?\d+)>|@(-?\d+)(?![\d])/g;
6
7
  /**
7
8
  * 根据文件扩展名推断 MIME 类型
@@ -177,7 +178,9 @@ export async function sendMessageMeet(opts) {
177
178
  if (!bot) {
178
179
  bot = createMeetClient(account);
179
180
  }
180
- const { text: cleanText, atIds: extractedAtIds } = extractAtIds(text);
181
+ // 先重写 @handle <@userId>,再提取 atIds
182
+ const textWithMentions = rewriteMeetKnownMentions(text, account.accountId);
183
+ const { text: cleanText, atIds: extractedAtIds } = extractAtIds(textWithMentions);
181
184
  const finalAtIds = explicitAtIds
182
185
  ? [...explicitAtIds, ...extractedAtIds]
183
186
  : extractedAtIds;
@@ -23,6 +23,10 @@ export type MeetReplyContext = {
23
23
  timestamp?: number;
24
24
  mediaPaths?: string[];
25
25
  };
26
+ export type MeetMention = {
27
+ userId: string;
28
+ name: string;
29
+ };
26
30
  export type MeetMessageContext = {
27
31
  chatId: string;
28
32
  messageId: string;
@@ -39,6 +43,7 @@ export type MeetMessageContext = {
39
43
  sessionInfo: SessionInfo;
40
44
  timestamp?: number;
41
45
  atIds?: number[];
46
+ mentions?: MeetMention[];
42
47
  replyContext?: MeetReplyContext;
43
48
  media?: MeetMediaAttachment[];
44
49
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meet-im/meet",
3
- "version": "3.0.1",
3
+ "version": "3.2.1",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Meet channel plugin",
6
6
  "scripts": {
@@ -55,7 +55,7 @@
55
55
  }
56
56
  },
57
57
  "dependencies": {
58
- "@meet-im/meet-bot-jssdk": "1.0.0",
58
+ "@meet-im/meet-bot-jssdk": "^1.2.1",
59
59
  "zod": "^4.4.3"
60
60
  },
61
61
  "devDependencies": {