@meet-im/meet 3.4.0 → 3.4.3

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,2 @@
1
- export declare const MEET_PLUGIN_VERSION = "3.4.0";
1
+ export declare const MEET_PLUGIN_VERSION = "3.4.3";
2
2
  export declare const MEET_OPENCLAW_VERSION = "2026.5.18";
@@ -1,2 +1,2 @@
1
- export const MEET_PLUGIN_VERSION = "3.4.0";
1
+ export const MEET_PLUGIN_VERSION = "3.4.3";
2
2
  export const MEET_OPENCLAW_VERSION = "2026.5.18";
@@ -3,6 +3,7 @@ import { resolveMeetAccount, listEnabledMeetAccounts } from "./accounts.js";
3
3
  import { createMeetClient, closeMeetClient, closeAllMeetClients, getPollingOptions } from "./client.js";
4
4
  import { handleMeetMessage } from "./bot.js";
5
5
  import { msgContentToContext, enrichContextWithUserNames } from "./sdk-bridge.js";
6
+ const activeMonitors = new Map();
6
7
  export async function monitorMeetProvider(opts = {}) {
7
8
  const cfg = opts.config;
8
9
  if (!cfg) {
@@ -38,13 +39,32 @@ async function monitorSingleAccount(params) {
38
39
  const { accountId } = account;
39
40
  const log = runtime?.log ?? console.log;
40
41
  const error = runtime?.error ?? console.error;
42
+ const existingMonitor = activeMonitors.get(accountId);
43
+ if (existingMonitor) {
44
+ if (abortSignal) {
45
+ const stopExistingMonitor = () => {
46
+ existingMonitor.stop();
47
+ };
48
+ if (abortSignal.aborted) {
49
+ stopExistingMonitor();
50
+ }
51
+ else {
52
+ abortSignal.addEventListener("abort", stopExistingMonitor, { once: true });
53
+ void existingMonitor.promise.finally(() => {
54
+ abortSignal.removeEventListener("abort", stopExistingMonitor);
55
+ });
56
+ }
57
+ }
58
+ return existingMonitor.promise;
59
+ }
41
60
  const pollTimeoutMs = account.config.pollTimeout ?? 30000;
42
61
  log(`[${accountId}]: starting with pollTimeout=${pollTimeoutMs}ms`);
43
62
  const bot = createMeetClient(account);
44
63
  const botUserId = extractBotUserId(account.apiToken ?? "");
45
64
  const groupHistories = new Map();
46
65
  const messageQueue = new KeyedAsyncQueue();
47
- return new Promise((resolve, reject) => {
66
+ let stopMonitor = () => { };
67
+ const monitorPromise = new Promise((resolve, reject) => {
48
68
  let isCleaningUp = false;
49
69
  const cleanup = () => {
50
70
  if (isCleaningUp)
@@ -53,6 +73,7 @@ async function monitorSingleAccount(params) {
53
73
  bot.stopPolling();
54
74
  closeMeetClient(accountId);
55
75
  };
76
+ stopMonitor = cleanup;
56
77
  const handleAbort = () => {
57
78
  log(`[${accountId}]: abort signal received, stopping`);
58
79
  cleanup();
@@ -151,6 +172,17 @@ async function monitorSingleAccount(params) {
151
172
  reject(err);
152
173
  });
153
174
  });
175
+ activeMonitors.set(accountId, {
176
+ promise: monitorPromise,
177
+ stop: () => {
178
+ stopMonitor();
179
+ },
180
+ });
181
+ return monitorPromise.finally(() => {
182
+ if (activeMonitors.get(accountId)?.promise === monitorPromise) {
183
+ activeMonitors.delete(accountId);
184
+ }
185
+ });
154
186
  }
155
187
  export function stopMeetMonitor(accountId) {
156
188
  if (accountId) {
@@ -4,6 +4,14 @@ import { createTypingCallbacks } from "openclaw/plugin-sdk/channel-message";
4
4
  import { getMeetRuntime } from "./runtime.js";
5
5
  import { sendMessageMeet, sendMediaMeet } from "./send.js";
6
6
  import { sendTypingMeet, stopTypingMeet } from "./typing.js";
7
+ function normalizeVisibleTextForDedup(text) {
8
+ return normalizeStatusTextForDedup(text.replace(/\r\n/g, "\n").trim());
9
+ }
10
+ function normalizeStatusTextForDedup(text) {
11
+ return text
12
+ .replace(/(⏱️\s*Uptime:\s*gateway\s+)\d+s(\s*·\s*system\s+)/g, "$1<secs>$2")
13
+ .replace(/(⏱️\s*Uptime:\s*gateway\s+[^·\n]+·\s*system\s+)\d+s(\b)/g, "$1<secs>$2");
14
+ }
7
15
  function resolveMeetConversationType(chatId) {
8
16
  if (chatId.startsWith("channel:")) {
9
17
  return "group";
@@ -91,42 +99,45 @@ export async function createMeetReplyDispatcher(opts) {
91
99
  : undefined;
92
100
  const shouldAutoMentionSender = chatType === "channel" && mentionedBot === true && !!senderId;
93
101
  const senderMentionText = shouldAutoMentionSender ? `<@${senderId}>` : undefined;
102
+ let lastDeliveredVisibleText;
94
103
  // 创建 typing callbacks(如果配置了 typing 且有必要参数)
95
104
  const hasTypingParams = typingMode && typingMode !== "none" && sessionInfo && apiToken;
96
- let typingRequestChain = Promise.resolve();
97
- const enqueueTypingRequest = (label, run) => {
98
- const next = typingRequestChain.then(async () => {
99
- opts.runtime.log?.(`[${accountId}][${chatId}]: typing ${label} sending...`);
100
- return await run();
101
- });
102
- typingRequestChain = next.then(() => undefined).catch(() => { });
103
- return next;
104
- };
105
+ // stop 请求需要等待最后一个 start 完成,避免乱序
106
+ let lastStartPromise = Promise.resolve();
105
107
  const typingCallbacks = hasTypingParams
106
108
  ? createTypingCallbacks({
107
109
  start: async () => {
108
- const result = await enqueueTypingRequest("start", async () => await sendTypingMeet({
110
+ // 直接发送,不串行化。typing start 是幂等的,重复发送无害
111
+ opts.runtime.log?.(`[${accountId}][${chatId}]: typing start sending...`);
112
+ const startPromise = sendTypingMeet({
109
113
  accountId,
110
114
  chatId,
111
115
  chatType: chatType ?? "channel",
112
116
  sessionInfo,
113
117
  token: apiToken,
114
118
  apiEndpoint,
115
- }));
116
- if (!result.ok && result.reason === "error") {
117
- throw result.error;
118
- }
119
- opts.runtime.log?.(`[${accountId}][${chatId}]: typing start sent ok=${result.ok}`);
119
+ }).then((result) => {
120
+ if (!result.ok && result.reason === "error") {
121
+ throw result.error;
122
+ }
123
+ opts.runtime.log?.(`[${accountId}][${chatId}]: typing start sent ok=${result.ok}`);
124
+ });
125
+ // 记录当前 start 请求,供 stop 等待
126
+ lastStartPromise = startPromise;
127
+ await startPromise;
120
128
  },
121
129
  stop: async () => {
122
- await enqueueTypingRequest("stop", async () => await stopTypingMeet({
130
+ // 等待最后一个 start 完成,避免 stop start 之前到达
131
+ await lastStartPromise;
132
+ opts.runtime.log?.(`[${accountId}][${chatId}]: typing stop sending...`);
133
+ await stopTypingMeet({
123
134
  accountId,
124
135
  chatId,
125
136
  chatType: chatType ?? "channel",
126
137
  sessionInfo,
127
138
  token: apiToken,
128
139
  apiEndpoint,
129
- }));
140
+ });
130
141
  },
131
142
  onStartError: (err) => {
132
143
  opts.runtime.error?.(`[${accountId}][${chatId}]: typing start failed: ${String(err)}`);
@@ -168,6 +179,18 @@ export async function createMeetReplyDispatcher(opts) {
168
179
  opts.runtime.log?.(`[${accountId}]: reply deliver kind=${_info.kind} text_len=${payload.text?.length ?? 0} media_count=${payload.mediaUrls?.length ?? (payload.mediaUrl ? 1 : 0)} reasoning=${payload.isReasoning === true} error=${payload.isError === true}`);
169
180
  const text = payload.text ?? "";
170
181
  const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
182
+ if (mediaUrls.length === 0) {
183
+ const normalizedVisibleText = normalizeVisibleTextForDedup(text);
184
+ if (_info.kind === "final" &&
185
+ normalizedVisibleText &&
186
+ normalizedVisibleText === lastDeliveredVisibleText) {
187
+ opts.runtime.log?.(`[${accountId}]: suppress duplicate final visible text for ${chatId}`);
188
+ return;
189
+ }
190
+ if (normalizedVisibleText) {
191
+ lastDeliveredVisibleText = normalizedVisibleText;
192
+ }
193
+ }
171
194
  // 如果既没有文本也没有媒体,直接返回
172
195
  if (!text.trim() && mediaUrls.length === 0) {
173
196
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meet-im/meet",
3
- "version": "3.4.0",
3
+ "version": "3.4.3",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Meet channel plugin",
6
6
  "scripts": {