@meet-im/meet 3.2.4 → 3.3.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.
package/dist/src/bot.js CHANGED
@@ -239,9 +239,23 @@ export async function handleMeetMessage(params) {
239
239
  const mentionsContext = ctx.mentions && ctx.mentions.length > 0
240
240
  ? `\n\nMentioned users: ${ctx.mentions.map((m) => `${m.name} (${m.userId})`).join(", ")}`
241
241
  : "";
242
- const finalContent = ctx.content.trim()
243
- ? `${ctx.content.trim()}${mentionsContext}${mediaContext}`
244
- : (ctx.placeholder || "") + mentionsContext + mediaContext;
242
+ const quoteContext = ctx.replyContext?.content?.trim()
243
+ ? [
244
+ "Reply target of current user message (untrusted, for context):",
245
+ "```json",
246
+ JSON.stringify({
247
+ body: ctx.replyContext.content.trim(),
248
+ sender: ctx.replyContext.senderId,
249
+ message_id: ctx.replyContext.messageId,
250
+ }, null, 2),
251
+ "```",
252
+ "",
253
+ ].join("\n")
254
+ : "";
255
+ const userBody = ctx.content.trim()
256
+ ? ctx.content.trim()
257
+ : (ctx.placeholder || "");
258
+ const finalContent = `${quoteContext}${userBody}${mentionsContext}${mediaContext}`;
245
259
  // Discord 做法:跳过空内容消息
246
260
  if (!finalContent.trim() && mediaPaths.length === 0) {
247
261
  log(`[${accountId}]: skip message ${ctx.messageId} (empty content)`);
@@ -367,6 +381,8 @@ export async function handleMeetMessage(params) {
367
381
  });
368
382
  const { createMeetReplyDispatcher } = await import("./reply-dispatcher.js");
369
383
  const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId);
384
+ // 默认 typingMode 为 "instant"
385
+ const effectiveTypingMode = meetCfg.typingMode ?? "instant";
370
386
  const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } = await createMeetReplyDispatcher({
371
387
  cfg,
372
388
  agentId: route.agentId,
@@ -377,6 +393,11 @@ export async function handleMeetMessage(params) {
377
393
  bot,
378
394
  botUserId,
379
395
  mediaLocalRoots,
396
+ // typing 相关参数
397
+ sessionInfo: ctx.sessionInfo,
398
+ apiToken: account.apiToken,
399
+ apiEndpoint: account.apiEndpoint,
400
+ typingMode: effectiveTypingMode,
380
401
  });
381
402
  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 ?? "")}`);
382
403
  log(`[${accountId}]: dispatching to AI agent=${route.agentId} session=${route.sessionKey} history=${inboundHistory?.length ?? 0}`);
@@ -46,6 +46,11 @@ export declare const MeetAccountConfigSchema: z.ZodObject<{
46
46
  dmHistoryLimit: z.ZodOptional<z.ZodNumber>;
47
47
  textChunkLimit: z.ZodOptional<z.ZodNumber>;
48
48
  mediaMaxMb: z.ZodOptional<z.ZodNumber>;
49
+ typingMode: z.ZodOptional<z.ZodEnum<{
50
+ message: "message";
51
+ none: "none";
52
+ instant: "instant";
53
+ }>>;
49
54
  groups: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
50
55
  enabled: z.ZodOptional<z.ZodBoolean>;
51
56
  name: z.ZodOptional<z.ZodString>;
@@ -106,6 +111,11 @@ export declare const MeetConfigSchema: z.ZodObject<{
106
111
  dmHistoryLimit: z.ZodOptional<z.ZodNumber>;
107
112
  textChunkLimit: z.ZodOptional<z.ZodNumber>;
108
113
  mediaMaxMb: z.ZodOptional<z.ZodNumber>;
114
+ typingMode: z.ZodOptional<z.ZodEnum<{
115
+ message: "message";
116
+ none: "none";
117
+ instant: "instant";
118
+ }>>;
109
119
  accounts: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
110
120
  enabled: z.ZodOptional<z.ZodBoolean>;
111
121
  name: z.ZodOptional<z.ZodString>;
@@ -136,6 +146,11 @@ export declare const MeetConfigSchema: z.ZodObject<{
136
146
  dmHistoryLimit: z.ZodOptional<z.ZodNumber>;
137
147
  textChunkLimit: z.ZodOptional<z.ZodNumber>;
138
148
  mediaMaxMb: z.ZodOptional<z.ZodNumber>;
149
+ typingMode: z.ZodOptional<z.ZodEnum<{
150
+ message: "message";
151
+ none: "none";
152
+ instant: "instant";
153
+ }>>;
139
154
  groups: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
140
155
  enabled: z.ZodOptional<z.ZodBoolean>;
141
156
  name: z.ZodOptional<z.ZodString>;
@@ -32,6 +32,7 @@ export const MeetAccountConfigSchema = z.object({
32
32
  dmHistoryLimit: z.number().min(0).optional(),
33
33
  textChunkLimit: z.number().min(1).optional(),
34
34
  mediaMaxMb: z.number().min(0).optional(),
35
+ typingMode: z.enum(["none", "instant", "message"]).optional(),
35
36
  groups: z.record(z.string(), MeetGroupConfigSchema).optional(),
36
37
  });
37
38
  export const MeetConfigSchema = z.object({
@@ -55,6 +56,7 @@ export const MeetConfigSchema = z.object({
55
56
  dmHistoryLimit: z.number().min(0).optional(),
56
57
  textChunkLimit: z.number().min(1).optional(),
57
58
  mediaMaxMb: z.number().min(0).optional(),
59
+ typingMode: z.enum(["none", "instant", "message"]).optional(),
58
60
  accounts: z.record(z.string(), MeetAccountConfigSchema).optional(),
59
61
  });
60
62
  export const MeetPluginConfigSchema = buildChannelConfigSchema(MeetConfigSchema);
@@ -1,2 +1,2 @@
1
- export declare const MEET_PLUGIN_VERSION = "3.2.4";
2
- export declare const MEET_OPENCLAW_VERSION = "2026.5.18";
1
+ export declare const MEET_PLUGIN_VERSION = "3.3.0";
2
+ export declare const MEET_OPENCLAW_VERSION = ">=2026.5.18";
@@ -1,2 +1,2 @@
1
- export const MEET_PLUGIN_VERSION = "3.2.4";
2
- export const MEET_OPENCLAW_VERSION = "2026.5.18";
1
+ export const MEET_PLUGIN_VERSION = "3.3.0";
2
+ export const MEET_OPENCLAW_VERSION = ">=2026.5.18";
@@ -1,4 +1,4 @@
1
- import type { MeetBot } from "@meet-im/meet-bot-jssdk";
1
+ import type { MeetBot, SessionInfo } from "@meet-im/meet-bot-jssdk";
2
2
  import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
3
3
  /**
4
4
  * 保护 mention 格式在分片后不被截断
@@ -20,6 +20,10 @@ export type CreateMeetReplyDispatcherOpts = {
20
20
  bot: MeetBot;
21
21
  botUserId: string;
22
22
  mediaLocalRoots?: readonly string[];
23
+ sessionInfo?: SessionInfo;
24
+ apiToken?: string;
25
+ apiEndpoint?: string;
26
+ typingMode?: "none" | "instant" | "message";
23
27
  };
24
28
  export declare function createMeetReplyDispatcher(opts: CreateMeetReplyDispatcherOpts): Promise<{
25
29
  dispatcher: import("node_modules/openclaw/dist/plugin-sdk/src/auto-reply/reply/reply-dispatcher.types.js").ReplyDispatcher;
@@ -1,7 +1,9 @@
1
1
  import { resolveChannelSourceReplyDeliveryMode } from "openclaw/plugin-sdk/channel-reply-pipeline";
2
2
  import { createReplyPrefixContext } from "openclaw/plugin-sdk/channel-runtime";
3
+ import { createTypingCallbacks } from "openclaw/plugin-sdk/channel-message";
3
4
  import { getMeetRuntime } from "./runtime.js";
4
5
  import { sendMessageMeet, sendMediaMeet } from "./send.js";
6
+ import { sendTypingMeet, stopTypingMeet } from "./typing.js";
5
7
  function resolveMeetConversationType(chatId) {
6
8
  if (chatId.startsWith("channel:")) {
7
9
  return "group";
@@ -73,7 +75,7 @@ export function protectMentionsInChunks(chunks) {
73
75
  return result;
74
76
  }
75
77
  export async function createMeetReplyDispatcher(opts) {
76
- const { cfg, agentId, chatId, replyToMessageId, accountId, mediaLocalRoots } = opts;
78
+ const { cfg, agentId, chatId, replyToMessageId, accountId, mediaLocalRoots, sessionInfo, apiToken, apiEndpoint, typingMode } = opts;
77
79
  const core = getMeetRuntime();
78
80
  const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "meet", accountId, {
79
81
  fallbackLimit: 4000,
@@ -87,6 +89,57 @@ export async function createMeetReplyDispatcher(opts) {
87
89
  ctx: { ChatType: chatType },
88
90
  })
89
91
  : undefined;
92
+ // 创建 typing callbacks(如果配置了 typing 且有必要参数)
93
+ const hasTypingParams = typingMode && typingMode !== "none" && sessionInfo && apiToken;
94
+ let typingRequestChain = Promise.resolve();
95
+ const enqueueTypingRequest = (label, run) => {
96
+ const next = typingRequestChain.then(async () => {
97
+ opts.runtime.log?.(`[${accountId}][${chatId}]: typing ${label} sending...`);
98
+ return await run();
99
+ });
100
+ typingRequestChain = next.then(() => undefined).catch(() => { });
101
+ return next;
102
+ };
103
+ const typingCallbacks = hasTypingParams
104
+ ? createTypingCallbacks({
105
+ start: async () => {
106
+ const result = await enqueueTypingRequest("start", async () => await sendTypingMeet({
107
+ accountId,
108
+ chatId,
109
+ chatType: chatType ?? "channel",
110
+ sessionInfo,
111
+ token: apiToken,
112
+ apiEndpoint,
113
+ }));
114
+ if (!result.ok && result.reason === "error") {
115
+ throw result.error;
116
+ }
117
+ opts.runtime.log?.(`[${accountId}][${chatId}]: typing start sent ok=${result.ok}`);
118
+ },
119
+ stop: async () => {
120
+ await enqueueTypingRequest("stop", async () => await stopTypingMeet({
121
+ accountId,
122
+ chatId,
123
+ chatType: chatType ?? "channel",
124
+ sessionInfo,
125
+ token: apiToken,
126
+ apiEndpoint,
127
+ }));
128
+ },
129
+ onStartError: (err) => {
130
+ opts.runtime.error?.(`[${accountId}][${chatId}]: typing start failed: ${String(err)}`);
131
+ },
132
+ onStopError: (err) => {
133
+ opts.runtime.error?.(`[${accountId}][${chatId}]: typing stop failed: ${String(err)}`);
134
+ },
135
+ })
136
+ : undefined;
137
+ // instant 模式:立即启动 typing(不等 AI 开始响应)
138
+ if (typingMode === "instant" && typingCallbacks) {
139
+ void typingCallbacks.onReplyStart().catch((err) => {
140
+ opts.runtime.error?.(`[${accountId}][${chatId}]: instant typing start failed: ${String(err)}`);
141
+ });
142
+ }
90
143
  const { dispatcher, replyOptions, markDispatchIdle, markRunComplete } = core.channel.reply.createReplyDispatcherWithTyping({
91
144
  responsePrefix: prefixContext.responsePrefix,
92
145
  responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
@@ -96,7 +149,18 @@ export async function createMeetReplyDispatcher(opts) {
96
149
  conversationType: resolveMeetConversationType(chatId),
97
150
  },
98
151
  humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
99
- onReplyStart: async () => {
152
+ // instant message 模式都传递 typingCallbacks(用于 onIdle/onCleanup)
153
+ typingCallbacks: typingCallbacks,
154
+ // instant 模式:首发已在外层手动触发,这里传空函数阻止 fallback
155
+ // message 模式:onReplyStart 在 AI 开始响应时触发
156
+ onReplyStart: typingCallbacks
157
+ ? typingMode === "instant"
158
+ ? async () => { } // 阻止 fallback,首发已手动触发
159
+ : async () => await typingCallbacks.onReplyStart()
160
+ : undefined,
161
+ onIdle: typingCallbacks?.onIdle,
162
+ onCleanup: () => {
163
+ typingCallbacks?.onCleanup?.();
100
164
  },
101
165
  deliver: async (payload, _info) => {
102
166
  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}`);
@@ -192,8 +256,6 @@ export async function createMeetReplyDispatcher(opts) {
192
256
  // 忽略错误消息发送失败
193
257
  }
194
258
  },
195
- onIdle: async () => {
196
- },
197
259
  });
198
260
  return {
199
261
  dispatcher,
@@ -0,0 +1,24 @@
1
+ import type { SessionInfo } from "@meet-im/meet-bot-jssdk";
2
+ export type SendTypingParams = {
3
+ accountId: string;
4
+ chatId: string;
5
+ chatType: "direct" | "channel";
6
+ sessionInfo?: SessionInfo;
7
+ token?: string;
8
+ apiEndpoint?: string;
9
+ };
10
+ export type TypingResult = {
11
+ ok: true;
12
+ } | {
13
+ ok: false;
14
+ reason: "unsupported" | "error";
15
+ error?: unknown;
16
+ };
17
+ /**
18
+ * 发送 typing 指示器
19
+ */
20
+ export declare function sendTypingMeet(params: SendTypingParams): Promise<TypingResult>;
21
+ /**
22
+ * 停止 typing 指示器
23
+ */
24
+ export declare function stopTypingMeet(params: SendTypingParams): Promise<TypingResult>;
@@ -0,0 +1,44 @@
1
+ import { sendUserTyping, TYPING_ACTION } from "@meet-im/meet-bot-jssdk";
2
+ /**
3
+ * 发送 typing 指示器
4
+ */
5
+ export async function sendTypingMeet(params) {
6
+ const { sessionInfo, token, apiEndpoint } = params;
7
+ // 缺少必要参数时返回 unsupported
8
+ if (!sessionInfo || !token) {
9
+ return { ok: false, reason: "unsupported" };
10
+ }
11
+ try {
12
+ await sendUserTyping({
13
+ token,
14
+ baseUrl: apiEndpoint,
15
+ sessionInfo,
16
+ action: TYPING_ACTION.TYPING,
17
+ });
18
+ return { ok: true };
19
+ }
20
+ catch (error) {
21
+ return { ok: false, reason: "error", error };
22
+ }
23
+ }
24
+ /**
25
+ * 停止 typing 指示器
26
+ */
27
+ export async function stopTypingMeet(params) {
28
+ const { sessionInfo, token, apiEndpoint } = params;
29
+ if (!sessionInfo || !token) {
30
+ return { ok: false, reason: "unsupported" };
31
+ }
32
+ try {
33
+ await sendUserTyping({
34
+ token,
35
+ baseUrl: apiEndpoint,
36
+ sessionInfo,
37
+ action: TYPING_ACTION.STOPPED,
38
+ });
39
+ return { ok: true };
40
+ }
41
+ catch (error) {
42
+ return { ok: false, reason: "error", error };
43
+ }
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meet-im/meet",
3
- "version": "3.2.4",
3
+ "version": "3.3.0",
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.2.1",
58
+ "@meet-im/meet-bot-jssdk": "^1.3.0",
59
59
  "zod": "^4.4.3"
60
60
  },
61
61
  "devDependencies": {
@@ -68,7 +68,7 @@
68
68
  "vitest": "4.1.5"
69
69
  },
70
70
  "peerDependencies": {
71
- "openclaw": "2026.5.18"
71
+ "openclaw": ">=2026.5.18"
72
72
  },
73
73
  "publishConfig": {
74
74
  "access": "public"