@meet-im/meet 2.0.6 → 3.0.0-beta.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.
Files changed (69) hide show
  1. package/dist/account-inspect-api.d.ts +2 -0
  2. package/dist/account-inspect-api.js +4 -0
  3. package/dist/channel-plugin-api.d.ts +1 -0
  4. package/dist/channel-plugin-api.js +3 -0
  5. package/dist/index.d.ts +14 -0
  6. package/dist/index.js +43 -0
  7. package/dist/monitor-api.d.ts +1 -0
  8. package/dist/monitor-api.js +1 -0
  9. package/dist/probe-api.d.ts +1 -0
  10. package/dist/probe-api.js +1 -0
  11. package/dist/runtime-setter-api.d.ts +1 -0
  12. package/dist/runtime-setter-api.js +2 -0
  13. package/dist/send-api.d.ts +1 -0
  14. package/dist/send-api.js +1 -0
  15. package/dist/src/account-inspect.d.ts +5 -0
  16. package/dist/src/account-inspect.js +9 -0
  17. package/dist/src/accounts.d.ts +12 -0
  18. package/dist/src/accounts.js +134 -0
  19. package/dist/src/bot.d.ts +15 -0
  20. package/dist/src/bot.js +355 -0
  21. package/dist/src/channel.d.ts +3 -0
  22. package/dist/src/channel.js +402 -0
  23. package/dist/src/client.d.ts +8 -0
  24. package/dist/src/client.js +49 -0
  25. package/dist/src/config-schema.d.ts +82 -0
  26. package/dist/src/config-schema.js +46 -0
  27. package/dist/src/media.d.ts +57 -0
  28. package/dist/src/media.js +140 -0
  29. package/dist/src/monitor.d.ts +9 -0
  30. package/dist/src/monitor.js +153 -0
  31. package/dist/src/outbound.d.ts +2 -0
  32. package/dist/src/outbound.js +34 -0
  33. package/dist/src/policy.d.ts +30 -0
  34. package/dist/src/policy.js +78 -0
  35. package/dist/src/probe.d.ts +10 -0
  36. package/dist/src/probe.js +56 -0
  37. package/dist/src/reply-dispatcher.d.ts +29 -0
  38. package/dist/src/reply-dispatcher.js +173 -0
  39. package/dist/src/runtime.d.ts +11 -0
  40. package/dist/src/runtime.js +6 -0
  41. package/dist/src/sdk-bridge.d.ts +21 -0
  42. package/dist/src/sdk-bridge.js +214 -0
  43. package/dist/src/send.d.ts +60 -0
  44. package/dist/src/send.js +317 -0
  45. package/dist/src/targets.d.ts +15 -0
  46. package/dist/src/targets.js +63 -0
  47. package/dist/src/types.d.ts +76 -0
  48. package/dist/src/types.js +1 -0
  49. package/dist/vitest.config.d.ts +8 -0
  50. package/dist/vitest.config.js +7 -0
  51. package/openclaw.plugin.json +116 -0
  52. package/package.json +18 -17
  53. package/index.ts +0 -26
  54. package/src/accounts.ts +0 -182
  55. package/src/bot.ts +0 -418
  56. package/src/channel.ts +0 -396
  57. package/src/client.ts +0 -63
  58. package/src/config-schema.ts +0 -50
  59. package/src/media.ts +0 -198
  60. package/src/monitor.ts +0 -195
  61. package/src/outbound.ts +0 -43
  62. package/src/policy.ts +0 -131
  63. package/src/probe.ts +0 -75
  64. package/src/reply-dispatcher.ts +0 -207
  65. package/src/runtime.ts +0 -14
  66. package/src/sdk-bridge.ts +0 -268
  67. package/src/send.ts +0 -383
  68. package/src/targets.ts +0 -101
  69. package/src/types.ts +0 -96
@@ -0,0 +1,140 @@
1
+ import { getMeetClient } from "./client.js";
2
+ import { getMeetRuntime } from "./runtime.js";
3
+ let _debugLog = null;
4
+ let _debugError = null;
5
+ export function setMediaDebugLogger(log, error) {
6
+ _debugLog = log;
7
+ _debugError = error;
8
+ }
9
+ function log(msg) {
10
+ if (_debugLog)
11
+ _debugLog(msg);
12
+ }
13
+ function logError(msg) {
14
+ if (_debugError)
15
+ _debugError(msg);
16
+ }
17
+ /**
18
+ * 获取文件下载 URL
19
+ */
20
+ export async function getFileAccessUrl(params) {
21
+ const { accountId, sessionInfo, seqId, fileId, ossProcess } = params;
22
+ const bot = getMeetClient(accountId);
23
+ if (!bot) {
24
+ logError(`[meet] getFileAccessUrl: bot client not found for account=${accountId}`);
25
+ return null;
26
+ }
27
+ try {
28
+ log(`[meet] getFileAccessUrl: calling getAccessURL fileId=${fileId} seqId=${seqId} companyId=${sessionInfo.companyId}`);
29
+ const result = await bot.getAccessURL({
30
+ firstId: sessionInfo.firstId,
31
+ secondId: sessionInfo.secondId,
32
+ sessionType: sessionInfo.sessionType,
33
+ seqId,
34
+ fileId: Number(fileId),
35
+ companyId: sessionInfo.companyId,
36
+ "x-oss-process": ossProcess,
37
+ });
38
+ log(`[meet] getFileAccessUrl: got URL ${result.fileUrl?.slice(0, 80)}...`);
39
+ return result.fileUrl;
40
+ }
41
+ catch (err) {
42
+ logError(`[meet] getFileAccessUrl: getAccessURL failed: ${String(err)}`);
43
+ return null;
44
+ }
45
+ }
46
+ /**
47
+ * 下载媒体文件到本地 media store
48
+ * 与 Discord 保持一致的处理流程
49
+ */
50
+ export async function downloadAndSaveMedia(params) {
51
+ const { accountId, attachment, sessionInfo, seqId, maxBytes } = params;
52
+ const runtime = getMeetRuntime();
53
+ log(`[meet] downloadAndSaveMedia: fileId=${attachment.fileId} fileName=${attachment.fileName} fileUrl=${attachment.fileUrl?.slice(0, 60)}...`);
54
+ // 确定下载 URL
55
+ let url;
56
+ // fileUrl 可能是完整 URL 或相对路径,只有完整 URL 才能直接使用
57
+ if (attachment.fileUrl && /^https?:\/\//i.test(attachment.fileUrl)) {
58
+ url = attachment.fileUrl;
59
+ log(`[meet] downloadAndSaveMedia: using complete fileUrl`);
60
+ }
61
+ else {
62
+ log(`[meet] downloadAndSaveMedia: fileUrl is not complete URL, calling getFileAccessUrl`);
63
+ const accessUrl = await getFileAccessUrl({
64
+ accountId,
65
+ sessionInfo,
66
+ seqId,
67
+ fileId: attachment.fileId,
68
+ });
69
+ if (!accessUrl) {
70
+ logError(`[meet] downloadAndSaveMedia: getFileAccessUrl returned null`);
71
+ return null;
72
+ }
73
+ url = accessUrl;
74
+ }
75
+ try {
76
+ // 下载媒体
77
+ log(`[meet] downloadAndSaveMedia: fetching remote media from ${url.slice(0, 80)}...`);
78
+ const fetched = await runtime.channel.media.fetchRemoteMedia({
79
+ url,
80
+ filePathHint: attachment.fileName,
81
+ maxBytes,
82
+ });
83
+ log(`[meet] downloadAndSaveMedia: fetched ${fetched.buffer.length} bytes, contentType=${fetched.contentType}`);
84
+ // 保存到本地 media store
85
+ const saved = await runtime.channel.media.saveMediaBuffer(fetched.buffer, fetched.contentType, "inbound", maxBytes, attachment.fileName);
86
+ log(`[meet] downloadAndSaveMedia: saved to ${saved.path}`);
87
+ const typeLabel = inferMediaType(fetched.contentType);
88
+ return {
89
+ path: saved.path,
90
+ contentType: saved.contentType,
91
+ placeholder: `[${typeLabel}: ${attachment.fileName || "文件"}]`,
92
+ url,
93
+ };
94
+ }
95
+ catch (err) {
96
+ logError(`[meet] downloadAndSaveMedia: download/save failed: ${String(err)}`);
97
+ // 下载失败时,返回 URL 作为 fallback
98
+ const typeLabel = inferMediaType(attachment.mimeType);
99
+ return {
100
+ path: url,
101
+ contentType: attachment.mimeType,
102
+ placeholder: `[${typeLabel}: ${attachment.fileName || "文件"}]`,
103
+ url,
104
+ };
105
+ }
106
+ }
107
+ /**
108
+ * 批量处理媒体附件
109
+ */
110
+ export async function resolveMediaAttachments(params) {
111
+ const results = [];
112
+ for (const attachment of params.attachments) {
113
+ const resolved = await downloadAndSaveMedia({
114
+ accountId: params.accountId,
115
+ attachment,
116
+ sessionInfo: params.sessionInfo,
117
+ seqId: params.seqId,
118
+ maxBytes: params.maxBytes,
119
+ });
120
+ if (resolved) {
121
+ results.push(resolved);
122
+ }
123
+ }
124
+ return results;
125
+ }
126
+ /**
127
+ * 根据 MIME 类型推断媒体类型标签
128
+ * 与 Discord 保持一致的格式
129
+ */
130
+ function inferMediaType(mimeType) {
131
+ if (!mimeType)
132
+ return "<media:document>";
133
+ if (mimeType.startsWith("image/"))
134
+ return "<media:image>";
135
+ if (mimeType.startsWith("video/"))
136
+ return "<media:video>";
137
+ if (mimeType.startsWith("audio/"))
138
+ return "<media:audio>";
139
+ return "<media:document>";
140
+ }
@@ -0,0 +1,9 @@
1
+ import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
2
+ export type MonitorMeetOpts = {
3
+ config?: ClawdbotConfig;
4
+ runtime?: RuntimeEnv;
5
+ abortSignal?: AbortSignal;
6
+ accountId?: string;
7
+ };
8
+ export declare function monitorMeetProvider(opts?: MonitorMeetOpts): Promise<void>;
9
+ export declare function stopMeetMonitor(accountId?: string): void;
@@ -0,0 +1,153 @@
1
+ import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
2
+ import { resolveMeetAccount, listEnabledMeetAccounts } from "./accounts.js";
3
+ import { createMeetClient, closeMeetClient, closeAllMeetClients, getPollingOptions } from "./client.js";
4
+ import { handleMeetMessage } from "./bot.js";
5
+ import { msgContentToContext } from "./sdk-bridge.js";
6
+ export async function monitorMeetProvider(opts = {}) {
7
+ const cfg = opts.config;
8
+ if (!cfg) {
9
+ throw new Error("Config is required for Meet monitor");
10
+ }
11
+ const log = opts.runtime?.log ?? console.log;
12
+ if (opts.accountId) {
13
+ const account = resolveMeetAccount({ cfg, accountId: opts.accountId });
14
+ if (!account.enabled || !account.configured) {
15
+ throw new Error(`Meet account "${opts.accountId}" not configured or disabled`);
16
+ }
17
+ return monitorSingleAccount({
18
+ cfg,
19
+ account,
20
+ runtime: opts.runtime,
21
+ abortSignal: opts.abortSignal,
22
+ });
23
+ }
24
+ const accounts = listEnabledMeetAccounts(cfg);
25
+ if (accounts.length === 0) {
26
+ throw new Error("No enabled Meet accounts configured");
27
+ }
28
+ log(`starting ${accounts.length} account(s): ${accounts.map((a) => a.accountId).join(", ")}`);
29
+ await Promise.all(accounts.map((account) => monitorSingleAccount({
30
+ cfg,
31
+ account,
32
+ runtime: opts.runtime,
33
+ abortSignal: opts.abortSignal,
34
+ })));
35
+ }
36
+ async function monitorSingleAccount(params) {
37
+ const { cfg, account, runtime, abortSignal } = params;
38
+ const { accountId } = account;
39
+ const log = runtime?.log ?? console.log;
40
+ const error = runtime?.error ?? console.error;
41
+ const pollTimeoutMs = account.config.pollTimeout ?? 30000;
42
+ log(`[${accountId}]: starting with pollTimeout=${pollTimeoutMs}ms`);
43
+ const bot = createMeetClient(account);
44
+ const botUserId = extractBotUserId(account.apiToken ?? "");
45
+ const groupHistories = new Map();
46
+ const messageQueue = new KeyedAsyncQueue();
47
+ return new Promise((resolve, reject) => {
48
+ let isCleaningUp = false;
49
+ const cleanup = () => {
50
+ if (isCleaningUp)
51
+ return;
52
+ isCleaningUp = true;
53
+ bot.stopPolling();
54
+ closeMeetClient(accountId);
55
+ };
56
+ const handleAbort = () => {
57
+ log(`[${accountId}]: abort signal received, stopping`);
58
+ cleanup();
59
+ resolve();
60
+ };
61
+ if (abortSignal?.aborted) {
62
+ cleanup();
63
+ resolve();
64
+ return;
65
+ }
66
+ abortSignal?.addEventListener("abort", handleAbort, { once: true });
67
+ bot.on("message", ({ message, quoteMsgMap }) => {
68
+ let queueKey;
69
+ try {
70
+ const ctx = msgContentToContext(message, botUserId, quoteMsgMap);
71
+ queueKey = ctx.chatId;
72
+ }
73
+ catch (err) {
74
+ error(`[${accountId}]: failed to parse message for queue key: ${String(err)}`);
75
+ return;
76
+ }
77
+ const tailMap = messageQueue.getTailMapForTesting();
78
+ const queueSize = tailMap.size;
79
+ const pendingInQueue = tailMap.has(queueKey);
80
+ log(`[${accountId}]: enqueue message to queue=${queueKey}, queues=${queueSize}, pending=${pendingInQueue}`);
81
+ messageQueue.enqueue(queueKey, async () => {
82
+ try {
83
+ await handleMeetMessage({
84
+ cfg,
85
+ msg: message,
86
+ botUserId,
87
+ runtime,
88
+ accountId,
89
+ account,
90
+ bot,
91
+ groupHistories,
92
+ quoteMsgMap,
93
+ });
94
+ }
95
+ catch (err) {
96
+ error(`[${accountId}]: error handling message: ${String(err)}`);
97
+ }
98
+ }, {
99
+ onEnqueue: () => {
100
+ const size = messageQueue.getTailMapForTesting().size;
101
+ log(`[${accountId}]: queue=${queueKey} enqueued, total_queues=${size}`);
102
+ },
103
+ onSettle: () => {
104
+ const size = messageQueue.getTailMapForTesting().size;
105
+ log(`[${accountId}]: queue=${queueKey} settled, total_queues=${size}`);
106
+ },
107
+ });
108
+ });
109
+ bot.on("error", (err) => {
110
+ const message = err instanceof Error ? err.message : String(err);
111
+ const name = err instanceof Error ? err.name : "Error";
112
+ if (name === "TimeoutError") {
113
+ log(`[${accountId}]: polling timeout, no new messages, retrying`);
114
+ return;
115
+ }
116
+ error(`[${accountId}]: polling error: ${name}: ${message}`);
117
+ });
118
+ bot.on("polling_start", () => {
119
+ log(`[${accountId}]: polling started`);
120
+ });
121
+ bot.on("polling_stop", () => {
122
+ if (!isCleaningUp) {
123
+ log(`[${accountId}]: polling stopped`);
124
+ }
125
+ });
126
+ const pollingOptions = getPollingOptions(account);
127
+ bot.startPolling(pollingOptions)
128
+ .then(() => {
129
+ log(`[${accountId}]: polling completed`);
130
+ cleanup();
131
+ abortSignal?.removeEventListener("abort", handleAbort);
132
+ resolve();
133
+ })
134
+ .catch((err) => {
135
+ error(`[${accountId}]: polling failed: ${String(err)}`);
136
+ cleanup();
137
+ abortSignal?.removeEventListener("abort", handleAbort);
138
+ reject(err);
139
+ });
140
+ });
141
+ }
142
+ export function stopMeetMonitor(accountId) {
143
+ if (accountId) {
144
+ closeMeetClient(accountId);
145
+ }
146
+ else {
147
+ closeAllMeetClients();
148
+ }
149
+ }
150
+ function extractBotUserId(token) {
151
+ const parts = token.split(":");
152
+ return parts[0] ?? "";
153
+ }
@@ -0,0 +1,2 @@
1
+ import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-send-result";
2
+ export declare const meetOutbound: ChannelOutboundAdapter;
@@ -0,0 +1,34 @@
1
+ import { getMeetRuntime } from "./runtime.js";
2
+ import { sendMessageMeet, sendMediaMeet } from "./send.js";
3
+ export const meetOutbound = {
4
+ deliveryMode: "direct",
5
+ chunker: (text, limit) => {
6
+ const runtime = getMeetRuntime();
7
+ return runtime.channel.text.chunkText(text, limit);
8
+ },
9
+ chunkerMode: "text",
10
+ textChunkLimit: 4000,
11
+ sendText: async ({ cfg, to, text, accountId }) => {
12
+ const result = await sendMessageMeet({
13
+ cfg,
14
+ to,
15
+ text: text ?? "",
16
+ accountId: accountId ?? undefined,
17
+ });
18
+ return { channel: "meet", messageId: result.messageId, chatId: result.chatId };
19
+ },
20
+ sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId, }) => {
21
+ if (!mediaUrl) {
22
+ throw new Error("mediaUrl is required for sendMedia");
23
+ }
24
+ const result = await sendMediaMeet({
25
+ cfg,
26
+ to,
27
+ text: text ?? undefined,
28
+ mediaUrl,
29
+ mediaLocalRoots: mediaLocalRoots ?? undefined,
30
+ accountId: accountId ?? undefined,
31
+ });
32
+ return { channel: "meet", messageId: result.messageId, chatId: result.chatId };
33
+ },
34
+ };
@@ -0,0 +1,30 @@
1
+ import type { MeetConfig, MeetGroupConfig } from "./types.js";
2
+ export declare function resolveMeetAllowlistMatch(params: {
3
+ allowFrom: Array<string | number>;
4
+ senderId: string;
5
+ senderName?: string;
6
+ }): {
7
+ allowed: boolean;
8
+ };
9
+ export declare function resolveMeetGroupPolicy(params: {
10
+ groupPolicy?: "open" | "allowlist" | "disabled";
11
+ groupAllowFrom: Array<string | number>;
12
+ chatId: string;
13
+ groups?: Record<string, MeetGroupConfig>;
14
+ }): {
15
+ allowed: boolean;
16
+ };
17
+ export declare function resolveMeetGroupConfig(params: {
18
+ meetConfig: MeetConfig;
19
+ chatId: string;
20
+ }): {
21
+ requireMention: boolean;
22
+ systemPrompt?: string;
23
+ groupConfig?: MeetGroupConfig;
24
+ };
25
+ export declare function resolveMeetGroupUserPolicy(params: {
26
+ groupConfig?: MeetGroupConfig;
27
+ senderId: string;
28
+ }): {
29
+ allowed: boolean;
30
+ };
@@ -0,0 +1,78 @@
1
+ export function resolveMeetAllowlistMatch(params) {
2
+ const { allowFrom, senderId } = params;
3
+ if (allowFrom.length === 0) {
4
+ return { allowed: false };
5
+ }
6
+ if (allowFrom.includes("*")) {
7
+ return { allowed: true };
8
+ }
9
+ const normalizedSenderId = senderId.trim().toLowerCase();
10
+ const normalizedAllowFrom = allowFrom.map((entry) => String(entry).trim().toLowerCase());
11
+ if (normalizedAllowFrom.includes(normalizedSenderId)) {
12
+ return { allowed: true };
13
+ }
14
+ return { allowed: false };
15
+ }
16
+ export function resolveMeetGroupPolicy(params) {
17
+ const { groupPolicy = "allowlist", groupAllowFrom, chatId, groups } = params;
18
+ if (groupPolicy === "disabled") {
19
+ return { allowed: false };
20
+ }
21
+ // Normalize chatId for matching (strip "channel:" prefix if present)
22
+ const normalizedChatId = chatId.replace(/^channel:/, "");
23
+ const groupConfig = groups?.[chatId] ?? groups?.[normalizedChatId];
24
+ if (groupPolicy === "open") {
25
+ if (groupConfig?.enabled === false) {
26
+ return { allowed: false };
27
+ }
28
+ return { allowed: true };
29
+ }
30
+ // groupPolicy === "allowlist"
31
+ // Check if group is explicitly configured in groups
32
+ if (groupConfig && groupConfig.enabled !== false) {
33
+ return { allowed: true };
34
+ }
35
+ if (groupConfig?.enabled === false) {
36
+ return { allowed: false };
37
+ }
38
+ if (groupAllowFrom.length === 0) {
39
+ return { allowed: false };
40
+ }
41
+ if (groupAllowFrom.includes("*")) {
42
+ return { allowed: true };
43
+ }
44
+ const normalizedAllowFrom = groupAllowFrom.map((entry) => String(entry).trim().toLowerCase());
45
+ // Match both full chatId and normalized chatId
46
+ const fullChatIdLower = chatId.trim().toLowerCase();
47
+ const shortChatIdLower = normalizedChatId.trim().toLowerCase();
48
+ if (normalizedAllowFrom.includes(fullChatIdLower) ||
49
+ normalizedAllowFrom.includes(shortChatIdLower)) {
50
+ return { allowed: true };
51
+ }
52
+ return { allowed: false };
53
+ }
54
+ export function resolveMeetGroupConfig(params) {
55
+ const { meetConfig, chatId } = params;
56
+ const normalizedChatId = chatId.replace(/^channel:/, "");
57
+ const groupConfig = meetConfig.groups?.[chatId] ?? meetConfig.groups?.[normalizedChatId];
58
+ return {
59
+ requireMention: groupConfig?.requireMention ?? meetConfig.requireMention ?? true,
60
+ systemPrompt: groupConfig?.systemPrompt ?? meetConfig.systemPrompt,
61
+ groupConfig,
62
+ };
63
+ }
64
+ export function resolveMeetGroupUserPolicy(params) {
65
+ const { groupConfig, senderId } = params;
66
+ if (!groupConfig?.users || groupConfig.users.length === 0) {
67
+ return { allowed: true };
68
+ }
69
+ if (groupConfig.users.includes("*")) {
70
+ return { allowed: true };
71
+ }
72
+ const normalizedSenderId = senderId.trim().toLowerCase();
73
+ const normalizedUsers = groupConfig.users.map((entry) => String(entry).trim().toLowerCase());
74
+ if (normalizedUsers.includes(normalizedSenderId)) {
75
+ return { allowed: true };
76
+ }
77
+ return { allowed: false };
78
+ }
@@ -0,0 +1,10 @@
1
+ import type { RuntimeEnv } from "openclaw/plugin-sdk";
2
+ import type { ResolvedMeetAccount } from "./types.js";
3
+ export declare function setProbeLogger(logger: RuntimeEnv): void;
4
+ export type MeetProbeResult = {
5
+ ok: boolean;
6
+ error?: string;
7
+ botId?: string;
8
+ };
9
+ export declare function probeMeet(account: ResolvedMeetAccount): Promise<MeetProbeResult>;
10
+ export declare function clearProbeCache(accountId?: string): void;
@@ -0,0 +1,56 @@
1
+ import { getMeetClient } from "./client.js";
2
+ let _logger = null;
3
+ export function setProbeLogger(logger) {
4
+ _logger = logger;
5
+ }
6
+ function log(message) {
7
+ if (_logger) {
8
+ _logger.log(message);
9
+ }
10
+ else {
11
+ console.log(message);
12
+ }
13
+ }
14
+ const probeCache = new Map();
15
+ const PROBE_CACHE_TTL_MS = 5 * 60 * 1000;
16
+ export async function probeMeet(account) {
17
+ if (!account.configured) {
18
+ return { ok: false, error: "Not configured" };
19
+ }
20
+ const cacheKey = account.accountId;
21
+ const cached = probeCache.get(cacheKey);
22
+ if (cached && Date.now() - cached.timestamp < PROBE_CACHE_TTL_MS) {
23
+ return cached.result;
24
+ }
25
+ try {
26
+ // 尝试获取 bot 实例或创建
27
+ let bot = getMeetClient(account.accountId);
28
+ if (!bot) {
29
+ const { createMeetClient } = await import("./client.js");
30
+ bot = createMeetClient(account);
31
+ }
32
+ // 尝试获取一次更新来验证连接
33
+ const updates = await bot.getUpdatesV2({ limit: 1, timeout: 1 });
34
+ const result = {
35
+ ok: true,
36
+ botId: account.apiToken?.split(":")[0],
37
+ };
38
+ if (updates && updates.msgs.length > 0) {
39
+ log(`[${account.accountId}] probe: received ${updates.msgs.length} update(s)`);
40
+ }
41
+ probeCache.set(cacheKey, { result, timestamp: Date.now() });
42
+ return result;
43
+ }
44
+ catch (error) {
45
+ const errorMessage = error instanceof Error ? error.message : String(error);
46
+ return { ok: false, error: errorMessage };
47
+ }
48
+ }
49
+ export function clearProbeCache(accountId) {
50
+ if (accountId) {
51
+ probeCache.delete(accountId);
52
+ }
53
+ else {
54
+ probeCache.clear();
55
+ }
56
+ }
@@ -0,0 +1,29 @@
1
+ import type { MeetBot } from "@meet-im/meet-bot-jssdk";
2
+ import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
3
+ /**
4
+ * 保护 mention 格式在分片后不被截断
5
+ *
6
+ * 当文本被分片后,`<@userId>` 格式可能被截断成:
7
+ * - 第一个 chunk 末尾: `<@123`
8
+ * - 第二个 chunk 开头: `456>`
9
+ *
10
+ * 此函数检测并修复这种情况,确保 mention 格式完整。
11
+ */
12
+ export declare function protectMentionsInChunks(chunks: string[]): string[];
13
+ export type CreateMeetReplyDispatcherOpts = {
14
+ cfg: ClawdbotConfig;
15
+ agentId: string;
16
+ runtime: RuntimeEnv;
17
+ chatId: string;
18
+ replyToMessageId?: string;
19
+ accountId: string;
20
+ bot: MeetBot;
21
+ botUserId: string;
22
+ mediaLocalRoots?: readonly string[];
23
+ };
24
+ export declare function createMeetReplyDispatcher(opts: CreateMeetReplyDispatcherOpts): Promise<{
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">;
27
+ markDispatchIdle: () => void;
28
+ markRunComplete: () => void;
29
+ }>;