@wecom/wecom-openclaw-plugin 2026.4.2201 → 2026.5.7

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.
@@ -17,21 +17,31 @@
17
17
  */
18
18
  import * as os from "os";
19
19
  import * as path from "path";
20
- import { WSClient, generateReqId, WSAuthFailureError, WSReconnectExhaustedError } from "@wecom/aibot-node-sdk";
21
- import { getWeComRuntime } from "./runtime.js";
22
- import { getDefaultMediaLocalRoots, resolveStateDir } from "./openclaw-compat.js";
20
+ import { WSClient, generateReqId, WSAuthFailureError, WSReconnectExhaustedError, } from "@wecom/aibot-node-sdk";
23
21
  import { CHANNEL_ID, THINKING_MESSAGE, MEDIA_IMAGE_PLACEHOLDER, MEDIA_DOCUMENT_PLACEHOLDER, WS_HEARTBEAT_INTERVAL_MS, WS_MAX_RECONNECT_ATTEMPTS, WS_MAX_AUTH_FAILURE_ATTEMPTS, EVENT_ENTER_CHECK_UPDATE, CMD_ENTER_EVENT_REPLY, SCENE_WECOM_OPENCLAW, } from "./const.js";
22
+ import { checkDmPolicy } from "./dm-policy.js";
23
+ import { processDynamicRouting } from "./dynamic-routing.js";
24
+ import { checkGroupPolicy } from "./group-policy.js";
25
+ import { downloadAndSaveImages, downloadAndSaveFiles, MediaOversizeError, } from "./media-handler.js";
26
+ import { uploadAndSendMedia } from "./media-uploader.js";
24
27
  import { parseMessageContent } from "./message-parser.js";
25
28
  import { sendWeComReply, StreamExpiredError } from "./message-sender.js";
26
- import { downloadAndSaveImages, downloadAndSaveFiles } from "./media-handler.js";
27
- import { uploadAndSendMedia } from "./media-uploader.js";
28
- import { maskTemplateCardBlocks } from "./template-card-parser.js";
29
- import { updateTemplateCardOnEvent, processTemplateCardsIfNeeded, } from "./template-card-manager.js";
30
- import { checkGroupPolicy } from "./group-policy.js";
31
- import { checkDmPolicy } from "./dm-policy.js";
29
+ import { getDefaultMediaLocalRoots, resolveStateDir } from "./openclaw-compat.js";
30
+ import { getWeComRuntime } from "./runtime.js";
32
31
  import { setWeComWebSocket, setMessageState, deleteMessageState, setReqIdForChat, setSessionChatInfo, warmupReqIdStore, startMessageStateCleanup, stopMessageStateCleanup, cleanupAccount, } from "./state-manager.js";
32
+ import { updateTemplateCardOnEvent, processTemplateCardsIfNeeded, } from "./template-card-manager.js";
33
+ import { maskTemplateCardBlocks } from "./template-card-parser.js";
33
34
  import { PLUGIN_VERSION } from "./version.js";
34
- import { processDynamicRouting } from "./dynamic-routing.js";
35
+ // ============================================================================
36
+ // 附件超限提示文案
37
+ // ============================================================================
38
+ /**
39
+ * 构造「附件超过 OpenClaw 大小限制」的中文提示文案。
40
+ */
41
+ function buildMediaOversizeHintText(err) {
42
+ const maxMb = err.maxBytes / 1024 / 1024;
43
+ return `当前OpenClaw限制文件不超过${maxMb}MB,请修改OpenClaw配置。`;
44
+ }
35
45
  // ============================================================================
36
46
  // 媒体本地路径白名单扩展
37
47
  // ============================================================================
@@ -135,7 +145,12 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
135
145
  const fromLabel = chatType === "group" ? `group:${chatId}` : `user:${body.from.userid}`;
136
146
  // 当只有媒体没有文本时,使用占位符标识媒体类型
137
147
  const hasImages = mediaList.some((m) => m.contentType?.startsWith("image/"));
138
- const messageBody = text || (mediaList.length > 0 ? (hasImages ? MEDIA_IMAGE_PLACEHOLDER : MEDIA_DOCUMENT_PLACEHOLDER) : "");
148
+ const messageBody = text ||
149
+ (mediaList.length > 0
150
+ ? hasImages
151
+ ? MEDIA_IMAGE_PLACEHOLDER
152
+ : MEDIA_DOCUMENT_PLACEHOLDER
153
+ : "");
139
154
  // 构建多媒体数组
140
155
  const mediaPaths = mediaList.length > 0 ? mediaList.map((m) => m.path) : undefined;
141
156
  const mediaTypes = mediaList.length > 0
@@ -272,7 +287,9 @@ async function finishThinkingStream(ctx) {
272
287
  }
273
288
  else if (state.hasMedia) {
274
289
  if (state.hasMediaFailed && state.mediaErrorSummary) {
275
- finishText = finishText ? `${finishText}\n\n${state.mediaErrorSummary}` : state.mediaErrorSummary;
290
+ finishText = finishText
291
+ ? `${finishText}\n\n${state.mediaErrorSummary}`
292
+ : state.mediaErrorSummary;
276
293
  }
277
294
  else if (!finishText) {
278
295
  finishText = "📎 文件已发送,请查收。";
@@ -283,7 +300,14 @@ async function finishThinkingStream(ctx) {
283
300
  let expired = state.streamExpired;
284
301
  if (!expired) {
285
302
  try {
286
- await sendWeComReply({ wsClient, frame, text: finishText, runtime, finish: true, streamId: state.streamId });
303
+ await sendWeComReply({
304
+ wsClient,
305
+ frame,
306
+ text: finishText,
307
+ runtime,
308
+ finish: true,
309
+ streamId: state.streamId,
310
+ });
287
311
  }
288
312
  catch (err) {
289
313
  if (err instanceof StreamExpiredError) {
@@ -307,7 +331,7 @@ async function finishThinkingStream(ctx) {
307
331
  * 路由消息到核心处理流程并处理回复
308
332
  */
309
333
  async function routeAndDispatchMessage(params) {
310
- const { ctxPayload, route, storePath, chatId, chatType, config, account, wsClient, frame, state, runtime, onCleanup } = params;
334
+ const { ctxPayload, route, storePath, chatId, chatType, config, account, wsClient, frame, state, runtime, onCleanup, } = params;
311
335
  const core = getWeComRuntime();
312
336
  const ctx = { wsClient, frame, state, account, runtime };
313
337
  // 防止 onCleanup 被多次调用(onError 回调与 catch 块可能重复触发)
@@ -341,17 +365,23 @@ async function routeAndDispatchMessage(params) {
341
365
  ctx: ctxPayload,
342
366
  cfg: config,
343
367
  replyOptions: {
344
- // 打印 LLM 返回的原始分片内容(在 openclaw 核心对 MEDIA: 指令解析之前),
345
- // 用于排查流式分片导致 MEDIA 指令被切断、识别丢失等问题
346
- onPartialReply: (payload) => {
347
- runtime.log?.(`[openclaw -> plugin][partial] payload=${JSON.stringify(payload)}`);
348
- },
368
+ // 打印 LLM 返回的原始分片内容(在 openclaw 核心对 MEDIA: 指令解析之前),
369
+ // 用于排查流式分片导致 MEDIA 指令被切断、识别丢失等问题
370
+ // onPartialReply: (payload: unknown) => {
371
+ // runtime.log?.(`[openclaw -> plugin][partial] payload=${JSON.stringify(payload)}`);
372
+ // },
349
373
  },
350
374
  dispatcherOptions: {
351
375
  onReplyStart: async () => {
352
376
  if (!isShowThink && state.streamId && !state.accumulatedText) {
353
377
  try {
354
- await sendThinkingReply({ wsClient, frame, streamId: state.streamId, runtime, state });
378
+ await sendThinkingReply({
379
+ wsClient,
380
+ frame,
381
+ streamId: state.streamId,
382
+ runtime,
383
+ state,
384
+ });
355
385
  }
356
386
  catch (e) {
357
387
  runtime.error?.(`[wecom] sendThinkingReply threw err: ${String(e)}`);
@@ -363,10 +393,14 @@ async function routeAndDispatchMessage(params) {
363
393
  runtime.log?.(`[openclaw -> plugin] kind=${info.kind}, payload=${JSON.stringify(payload)}, info=${JSON.stringify(info)}`);
364
394
  // 累积文本
365
395
  if (payload.text) {
366
- state.accumulatedText += `${payload.text || ''}`;
396
+ state.accumulatedText += `${payload.text || ""}`;
367
397
  }
368
398
  // 发送媒体(统一走主动发送)
369
- const mediaUrls = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
399
+ const mediaUrls = payload.mediaUrls?.length
400
+ ? payload.mediaUrls
401
+ : payload.mediaUrl
402
+ ? [payload.mediaUrl]
403
+ : [];
370
404
  if (mediaUrls.length > 0) {
371
405
  try {
372
406
  await sendMediaBatch(ctx, mediaUrls);
@@ -392,7 +426,14 @@ async function routeAndDispatchMessage(params) {
392
426
  // if (displayText !== state.accumulatedText) {
393
427
  // runtime.log?.(`[wecom][template-card] Mid-frame masked: original=${state.accumulatedText.length}chars, masked=${displayText.length}chars`);
394
428
  // }
395
- await sendWeComReply({ wsClient, frame, text: displayText, runtime, finish: false, streamId: state.streamId });
429
+ await sendWeComReply({
430
+ wsClient,
431
+ frame,
432
+ text: displayText,
433
+ runtime,
434
+ finish: false,
435
+ streamId: state.streamId,
436
+ });
396
437
  }
397
438
  catch (err) {
398
439
  if (err instanceof StreamExpiredError) {
@@ -411,7 +452,13 @@ async function routeAndDispatchMessage(params) {
411
452
  },
412
453
  });
413
454
  // 模板卡片检测与发送(在关闭 thinking 流之前独立处理)
414
- const cardResult = await processTemplateCardsIfNeeded({ wsClient, frame, state, account, runtime });
455
+ const cardResult = await processTemplateCardsIfNeeded({
456
+ wsClient,
457
+ frame,
458
+ state,
459
+ account,
460
+ runtime,
461
+ });
415
462
  if (cardResult) {
416
463
  // 卡片已发送,用剩余文本替换累积文本
417
464
  state.accumulatedText = cardResult.remainingText;
@@ -424,7 +471,13 @@ async function routeAndDispatchMessage(params) {
424
471
  runtime.error?.(`[wecom][plugin] Failed to process message: ${String(err)}`);
425
472
  // 即使 dispatch 抛异常,也需要处理卡片和关闭 thinking 流
426
473
  try {
427
- const cardResult = await processTemplateCardsIfNeeded({ wsClient, frame, state, account, runtime });
474
+ const cardResult = await processTemplateCardsIfNeeded({
475
+ wsClient,
476
+ frame,
477
+ state,
478
+ account,
479
+ runtime,
480
+ });
428
481
  if (cardResult) {
429
482
  state.accumulatedText = cardResult.remainingText;
430
483
  }
@@ -492,24 +545,43 @@ async function prepareWeComMessage(params) {
492
545
  return null;
493
546
  }
494
547
  // Step 4: 下载并保存图片和文件
495
- const [imageMediaList, fileMediaList] = await Promise.all([
496
- downloadAndSaveImages({
497
- imageUrls,
498
- imageAesKeys,
499
- account,
500
- config,
501
- runtime,
502
- wsClient,
503
- }),
504
- downloadAndSaveFiles({
505
- fileUrls,
506
- fileAesKeys,
507
- account,
508
- config,
509
- runtime,
510
- wsClient,
511
- }),
512
- ]);
548
+ let imageMediaList;
549
+ let fileMediaList;
550
+ try {
551
+ [imageMediaList, fileMediaList] = await Promise.all([
552
+ downloadAndSaveImages({
553
+ imageUrls,
554
+ imageAesKeys,
555
+ account,
556
+ config,
557
+ runtime,
558
+ wsClient,
559
+ }),
560
+ downloadAndSaveFiles({
561
+ fileUrls,
562
+ fileAesKeys,
563
+ account,
564
+ config,
565
+ runtime,
566
+ wsClient,
567
+ }),
568
+ ]);
569
+ }
570
+ catch (err) {
571
+ if (err instanceof MediaOversizeError) {
572
+ // 附件超过 OpenClaw 配置的大小上限:向用户发送明确的中文提示并终止本次消息处理。
573
+ const hintText = buildMediaOversizeHintText(err);
574
+ runtime.error?.(`[wecom] Media oversize: kind=${err.kind}, size=${err.sizeBytes}, max=${err.maxBytes}, filename=${err.filename ?? "(none)"}`);
575
+ try {
576
+ await sendWeComReply({ wsClient, frame, text: hintText, runtime, finish: true });
577
+ }
578
+ catch (replyErr) {
579
+ runtime.error?.(`[wecom] Failed to send oversize hint: ${String(replyErr)}`);
580
+ }
581
+ return null;
582
+ }
583
+ throw err;
584
+ }
513
585
  const mediaList = [...imageMediaList, ...fileMediaList];
514
586
  return {
515
587
  frame,
@@ -532,7 +604,7 @@ async function prepareWeComMessage(params) {
532
604
  * 同一会话中的消息通过串行队列保证按序执行。
533
605
  */
534
606
  async function processWeComMessageNow(entry) {
535
- const { frame, account, config, runtime, wsClient, text, mediaList, quoteContent, messageId, chatId, reqId } = entry;
607
+ const { frame, account, config, runtime, wsClient, text, mediaList, quoteContent, messageId, chatId, reqId, } = entry;
536
608
  // Step 5: 初始化消息状态
537
609
  setReqIdForChat(chatId, reqId, account.accountId);
538
610
  const streamId = generateReqId("stream");
@@ -547,7 +619,7 @@ async function processWeComMessageNow(entry) {
547
619
  // await sendThinkingReply({ wsClient, frame, streamId, runtime });
548
620
  // }
549
621
  // Step 7: 构建上下文并路由到核心处理流程(带整体超时保护)
550
- const { ctxPayload, route, storePath, chatId: resolvedChatId, chatType } = buildMessageContext(frame, account, config, text, mediaList, quoteContent, runtime);
622
+ const { ctxPayload, route, storePath, chatId: resolvedChatId, chatType, } = buildMessageContext(frame, account, config, text, mediaList, quoteContent, runtime);
551
623
  // 以 sessionKey 为键记录「原始大小写」的 chatId 与 chatType,
552
624
  // 供 MCP 工具工厂(index.ts:registerTool)在构造工具闭包时取回,
553
625
  // 进而传递给需要原始 chatId 的拦截器(如 doc-auth-error 发送 biz_msg)。
@@ -7,6 +7,34 @@
7
7
  import { createPersistentReqIdStore } from "./reqid-store.js";
8
8
  import { MESSAGE_STATE_TTL_MS, MESSAGE_STATE_CLEANUP_INTERVAL_MS, MESSAGE_STATE_MAX_SIZE, GLOBAL_WS_CLIENT_KEY, } from "./const.js";
9
9
  // ============================================================================
10
+ // 全局单例状态(通过 globalThis + Symbol.for 确保跨 jiti loader 实例共享)
11
+ //
12
+ // 问题背景:openclaw 框架在不同阶段(主加载、configSchema 加载等)会创建
13
+ // 多个独立的 jiti loader 实例,每个实例有独立的模块缓存(parentCache),
14
+ // 导致本模块被多次实例化,模块级变量不共享。
15
+ // 使用 globalThis + Symbol.for 可以确保同一进程内只有一份状态。
16
+ // ============================================================================
17
+ /** 全局共享状态的唯一键 */
18
+ const SHARED_STATE_KEY = Symbol.for("wecom-openclaw-plugin:shared-state:v1");
19
+ /**
20
+ * 获取或创建全局共享状态(进程内单例)
21
+ */
22
+ function getSharedState() {
23
+ const existing = globalThis[SHARED_STATE_KEY];
24
+ if (existing)
25
+ return existing;
26
+ const state = {
27
+ wsClientInstances: new Map(),
28
+ messageStates: new Map(),
29
+ cleanupTimer: null,
30
+ reqIdStores: new Map(),
31
+ };
32
+ globalThis[SHARED_STATE_KEY] = state;
33
+ return state;
34
+ }
35
+ /** 进程内唯一的共享状态 */
36
+ const shared = getSharedState();
37
+ // ============================================================================
10
38
  // WSClient 实例管理
11
39
  // ============================================================================
12
40
  /** WSClient 实例管理 */
@@ -16,45 +44,45 @@ const wsClientInstances = (globalThis[GLOBAL_WS_CLIENT_KEY]
16
44
  * 获取指定账户的 WSClient 实例
17
45
  */
18
46
  export function getWeComWebSocket(accountId) {
19
- return wsClientInstances.get(accountId) ?? null;
47
+ return shared.wsClientInstances.get(accountId) ?? null;
20
48
  }
21
49
  /**
22
50
  * 设置指定账户的 WSClient 实例
23
51
  */
24
52
  export function setWeComWebSocket(accountId, client) {
25
- wsClientInstances.set(accountId, client);
53
+ shared.wsClientInstances.set(accountId, client);
26
54
  }
27
55
  /**
28
56
  * 删除指定账户的 WSClient 实例
29
57
  */
30
58
  export function deleteWeComWebSocket(accountId) {
31
- wsClientInstances.delete(accountId);
59
+ shared.wsClientInstances.delete(accountId);
32
60
  }
33
- /** 消息状态管理 */
34
- const messageStates = new Map();
35
- /** 定期清理定时器 */
36
- let cleanupTimer = null;
61
+ // ============================================================================
62
+ // 消息状态管理(带 TTL 清理,防止内存泄漏)
63
+ // ============================================================================
64
+ // MessageStateEntry 接口已在上方 SharedState 定义处声明
37
65
  /**
38
66
  * 启动消息状态定期清理(自动 TTL 清理 + 容量限制)
39
67
  */
40
68
  export function startMessageStateCleanup() {
41
- if (cleanupTimer)
69
+ if (shared.cleanupTimer)
42
70
  return;
43
- cleanupTimer = setInterval(() => {
71
+ shared.cleanupTimer = setInterval(() => {
44
72
  pruneMessageStates();
45
73
  }, MESSAGE_STATE_CLEANUP_INTERVAL_MS);
46
74
  // 允许进程退出时不阻塞
47
- if (cleanupTimer && typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
48
- cleanupTimer.unref();
75
+ if (shared.cleanupTimer && typeof shared.cleanupTimer === "object" && "unref" in shared.cleanupTimer) {
76
+ shared.cleanupTimer.unref();
49
77
  }
50
78
  }
51
79
  /**
52
80
  * 停止消息状态定期清理
53
81
  */
54
82
  export function stopMessageStateCleanup() {
55
- if (cleanupTimer) {
56
- clearInterval(cleanupTimer);
57
- cleanupTimer = null;
83
+ if (shared.cleanupTimer) {
84
+ clearInterval(shared.cleanupTimer);
85
+ shared.cleanupTimer = null;
58
86
  }
59
87
  }
60
88
  /**
@@ -63,17 +91,17 @@ export function stopMessageStateCleanup() {
63
91
  function pruneMessageStates() {
64
92
  const now = Date.now();
65
93
  // 1. 清理过期条目
66
- for (const [key, entry] of messageStates) {
94
+ for (const [key, entry] of shared.messageStates) {
67
95
  if (now - entry.createdAt >= MESSAGE_STATE_TTL_MS) {
68
- messageStates.delete(key);
96
+ shared.messageStates.delete(key);
69
97
  }
70
98
  }
71
99
  // 2. 容量限制:如果仍超过最大条目数,按时间淘汰最旧的
72
- if (messageStates.size > MESSAGE_STATE_MAX_SIZE) {
73
- const sorted = [...messageStates.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
74
- const toRemove = sorted.slice(0, messageStates.size - MESSAGE_STATE_MAX_SIZE);
100
+ if (shared.messageStates.size > MESSAGE_STATE_MAX_SIZE) {
101
+ const sorted = [...shared.messageStates.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
102
+ const toRemove = sorted.slice(0, shared.messageStates.size - MESSAGE_STATE_MAX_SIZE);
75
103
  for (const [key] of toRemove) {
76
- messageStates.delete(key);
104
+ shared.messageStates.delete(key);
77
105
  }
78
106
  }
79
107
  }
@@ -81,7 +109,7 @@ function pruneMessageStates() {
81
109
  * 设置消息状态
82
110
  */
83
111
  export function setMessageState(messageId, state) {
84
- messageStates.set(messageId, {
112
+ shared.messageStates.set(messageId, {
85
113
  state,
86
114
  createdAt: Date.now(),
87
115
  });
@@ -90,12 +118,12 @@ export function setMessageState(messageId, state) {
90
118
  * 获取消息状态
91
119
  */
92
120
  export function getMessageState(messageId) {
93
- const entry = messageStates.get(messageId);
121
+ const entry = shared.messageStates.get(messageId);
94
122
  if (!entry)
95
123
  return undefined;
96
124
  // 检查 TTL
97
125
  if (Date.now() - entry.createdAt >= MESSAGE_STATE_TTL_MS) {
98
- messageStates.delete(messageId);
126
+ shared.messageStates.delete(messageId);
99
127
  return undefined;
100
128
  }
101
129
  return entry.state;
@@ -104,13 +132,13 @@ export function getMessageState(messageId) {
104
132
  * 删除消息状态
105
133
  */
106
134
  export function deleteMessageState(messageId) {
107
- messageStates.delete(messageId);
135
+ shared.messageStates.delete(messageId);
108
136
  }
109
137
  /**
110
138
  * 清空所有消息状态
111
139
  */
112
140
  export function clearAllMessageStates() {
113
- messageStates.clear();
141
+ shared.messageStates.clear();
114
142
  }
115
143
  // ============================================================================
116
144
  // ReqId 持久化存储管理(按 accountId 隔离)
@@ -120,12 +148,11 @@ export function clearAllMessageStates() {
120
148
  * 参考 createPersistentDedupe 模式:内存 + 磁盘双层、文件锁、原子写入、TTL 过期、防抖写入
121
149
  * 重启后可从磁盘恢复,确保主动推送消息时能获取到 reqId
122
150
  */
123
- const reqIdStores = new Map();
124
151
  function getOrCreateReqIdStore(accountId) {
125
- let store = reqIdStores.get(accountId);
152
+ let store = shared.reqIdStores.get(accountId);
126
153
  if (!store) {
127
154
  store = createPersistentReqIdStore(accountId);
128
- reqIdStores.set(accountId, store);
155
+ shared.reqIdStores.set(accountId, store);
129
156
  }
130
157
  return store;
131
158
  }
@@ -215,7 +242,7 @@ export function deleteSessionChatInfo(sessionKey) {
215
242
  */
216
243
  export async function cleanupAccount(accountId) {
217
244
  // 1. 断开 WSClient
218
- const wsClient = wsClientInstances.get(accountId);
245
+ const wsClient = shared.wsClientInstances.get(accountId);
219
246
  if (wsClient) {
220
247
  try {
221
248
  wsClient.disconnect();
@@ -223,7 +250,7 @@ export async function cleanupAccount(accountId) {
223
250
  catch {
224
251
  // 忽略断开连接时的错误
225
252
  }
226
- wsClientInstances.delete(accountId);
253
+ shared.wsClientInstances.delete(accountId);
227
254
  }
228
255
  // 2. 由于移除了磁盘存储,不再需要 flush reqId 存储
229
256
  // 注意:不删除 store,因为重连后可能还需要
@@ -235,7 +262,7 @@ export async function cleanupAll() {
235
262
  // 停止定期清理
236
263
  stopMessageStateCleanup();
237
264
  // 清理所有 WSClient
238
- for (const [accountId, wsClient] of wsClientInstances) {
265
+ for (const [accountId, wsClient] of shared.wsClientInstances) {
239
266
  try {
240
267
  wsClient.disconnect();
241
268
  }
@@ -243,7 +270,7 @@ export async function cleanupAll() {
243
270
  // 忽略
244
271
  }
245
272
  }
246
- wsClientInstances.clear();
273
+ shared.wsClientInstances.clear();
247
274
  // 由于移除了磁盘存储,不再需要 flush 所有 reqId 存储
248
275
  // 清空消息状态
249
276
  clearAllMessageStates();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wecom/wecom-openclaw-plugin",
3
- "version": "2026.4.2201",
3
+ "version": "2026.5.7",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -23,6 +23,7 @@
23
23
  "prebuild": "npm run clean",
24
24
  "release": "node scripts/publish-all.mjs",
25
25
  "release:dry": "node scripts/publish-all.mjs --dry-run",
26
+ "release:ci": "node scripts/publish-all.mjs --yes --restore=no",
26
27
  "deploy:local": "bash scripts/dev-deploy.sh"
27
28
  },
28
29
  "keywords": [
@@ -67,5 +68,13 @@
67
68
  "file-type": "^21.3.0",
68
69
  "undici": "^7.24.6",
69
70
  "zod": "^4.3.6"
71
+ },
72
+ "peerDependencies": {
73
+ "openclaw": ">=2026.3.28"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "openclaw": {
77
+ "optional": true
78
+ }
70
79
  }
71
80
  }
@@ -51,6 +51,7 @@ description: 企业微信文档管理技能。支持企业微信文档、智能
51
51
  | errcode | errmsg | 含义 | 处理方式 |
52
52
  |---------|--------|------|----------|
53
53
  | `851002` | `incompatible doc type` | 文档品类与所调用的接口不匹配 | 根据文档 URL 重新确认品类(参见上方「URL 品类识别与接口路由」表),然后使用该品类对应的正确接口重试 |
54
+ | `851003` | `no authority` | 无权限调用该接口,**智能表格写入场景**下通常是企业可见范围 > 10 人的规模限制 | 若发生在需要写入数据的场景,引导用户走 Webhook 兜底方案,详见 [webhook-fallback.md](../wecom-smartsheet-data/references/webhook-fallback.md);其他接口则按权限问题排查 |
54
55
 
55
56
  ## 文档
56
57
 
@@ -41,14 +41,23 @@ description: 企业微信智能表格数据(记录)管理技能。提供智
41
41
 
42
42
  添加一行或多行记录,单次建议 500 行内。
43
43
 
44
- **调用前**必须先了解目标表的字段类型(通过 `smartsheet_get_fields`)。
44
+ **调用前**必须先了解目标表的字段类型(通过 `smartsheet_get_fields`),并查看[单元格值格式参考](references/cell-value-formats.md),了解做法后再实施。
45
45
 
46
46
  使用 `wecom_mcp` tool 调用 `wecom_mcp call doc smartsheet_add_records '{"docid": "DOCID", "sheet_id": "SHEETID", "records": [{"values": {"任务名称": [{"type": "text", "text": "完成需求文档"}], "优先级": [{"text": "高"}]}}]}'`
47
47
 
48
48
  各字段类型的值格式参见 [单元格值格式参考](references/cell-value-formats.md)。
49
49
 
50
+ > ⚠️ **图片(IMAGE)/ 文件(ATTACHMENT)字段**:**必须且只能**直接在 cell value 中传入本地路径(`image_path` / `file_path`),格式见下方示例。**严禁**调用 `upload_doc_image`、`upload_doc_file` 或任何上传接口,**严禁**进行 base64 编码——无论文件大小、无论 API 返回何种错误,均不得绕过此规则。若 `image_path` / `file_path` 方式返回错误,应直接将错误码和错误信息告知用户,**不得**尝试其他上传方式。
51
+ >
52
+ > 图片字段示例:`"封面": [{"image_path": "/path/to/img.png", "title": "图片标题"}]`
53
+ > 文件字段示例:`"文件": [{"file_path": "/path/to/file.pdf"}]`
54
+
55
+ > ⚠️ 若返回 `errcode: 851003` 或 `errmsg` 包含 `no authority`(通常是企业可见范围 > 10 人的规模限制),切换到 Webhook 兜底方案,详见 [webhook-fallback.md](references/webhook-fallback.md)。
56
+
50
57
  ### smartsheet_update_records
51
58
 
59
+ **调用前**必须先了解目标表的字段类型(通过 `smartsheet_get_fields`),并查看[单元格值格式参考](references/cell-value-formats.md),了解做法后再实施。
60
+
52
61
  更新一行或多行记录,单次建议在 500 行内。需提供 record_id(通过 `smartsheet_get_records` 获取)。支持通过 `key_type` 指定 values 的 key 使用字段标题或字段 ID:
53
62
 
54
63
  - `CELL_VALUE_KEY_TYPE_FIELD_TITLE`:key 为字段标题
@@ -58,6 +67,13 @@ description: 企业微信智能表格数据(记录)管理技能。提供智
58
67
 
59
68
  **注意**:创建时间、最后编辑时间、创建人、最后编辑人字段不可更新。
60
69
 
70
+ > ⚠️ **图片(IMAGE)/ 文件(ATTACHMENT)字段**:**必须且只能**直接在 cell value 中传入本地路径(`image_path` / `file_path`),格式见下方示例。**严禁**调用 `upload_doc_image`、`upload_doc_file` 或任何上传接口,**严禁**进行 base64 编码——无论文件大小、无论 API 返回何种错误,均不得绕过此规则。若 `image_path` / `file_path` 方式返回错误,应直接将错误码和错误信息告知用户,**不得**尝试其他上传方式。
71
+ >
72
+ > 图片字段示例:`"封面": [{"image_path": "/path/to/img.png", "title": "图片标题"}]`
73
+ > 文件字段示例:`"文件": [{"file_path": "/path/to/file.pdf"}]`
74
+
75
+ > ⚠️ 若返回 `errcode: 851003` 或 `errmsg` 包含 `no authority`(通常是企业可见范围 > 10 人的规模限制),切换到 Webhook 兜底方案,详见 [webhook-fallback.md](references/webhook-fallback.md)。注意 Webhook 只能更新通过 Webhook 写入的记录,人工创建的记录无法更新。
76
+
61
77
  ### smartsheet_delete_records
62
78
 
63
79
  删除一行或多行记录,单次必须在 500 行内。**操作不可逆**。record_id 通过 `smartsheet_get_records` 获取。
@@ -69,6 +85,9 @@ description: 企业微信智能表格数据(记录)管理技能。提供智
69
85
  1. **读取数据** → 使用 `wecom_mcp` tool 调用 `wecom_mcp call doc smartsheet_get_records '{"docid":"DOCID","sheet_id":"SHEETID"}'`
70
86
  2. **写入数据** → 先 `smartsheet_get_fields` 了解列类型 → 若涉及成员(USER)字段,先通过 `wecom-contact-lookup` 的 `get_userlist` 查找人员 userid → `smartsheet_add_records` 写入
71
87
  3. **更新数据** → 先 `smartsheet_get_records` 获取 record_id → 若涉及成员(USER)字段,先通过 `wecom-contact-lookup` 的 `get_userlist` 查找人员 userid → `smartsheet_update_records` 更新
72
- 4. **删除数据** → 先 `smartsheet_get_records` 确认 record_id → `smartsheet_delete_records` 删除
88
+ 4. **写入失败 fallback** → 第 2/3 步返回 `errcode: 851003` / `no authority`(通常是企业可见范围 > 10 人的规模限制)时 → 请用户临时提供目标表的 Webhook 地址 + schema 示例 JSON(不保存到本地)→ 按 [webhook-fallback.md](references/webhook-fallback.md) 构造请求体发送
89
+ 5. 若涉及**图片(IMAGE)或文件(ATTACHMENT)**字段,**必须且只能**直接在 cell value 中传入本地路径(`image_path` / `file_path`)。**严禁**调用 `upload_doc_image`、`upload_doc_file` 或任何上传接口,**严禁**进行 base64 编码——无论文件大小、无论 API 返回何种错误,均不得绕过此规则。若返回错误,直接将错误码和错误信息告知用户,不得尝试其他上传方式。传入格式参考 [单元格值格式参考](references/cell-value-formats.md)。
90
+ 6. **删除数据** → 先 `smartsheet_get_records` 确认 record_id → `smartsheet_delete_records` 删除
73
91
 
74
92
  > **注意**:成员(USER)类型字段需要填写 `user_id`,不能直接使用姓名。必须先通过 `wecom-contact-lookup` 技能的 `get_userlist` 接口按姓名查找到对应的 `userid` 后再使用。
93
+ > **注意**:图片(IMAGE)或文件(ATTACHMENT)字段**必须且只能**直接在 cell value 中传入本地路径(`image_path` / `file_path`)。**严禁**调用 `upload_doc_image`、`upload_doc_file` 或任何上传接口,**严禁**进行 base64 编码——无论文件大小、无论 API 返回何种错误,均不得绕过此规则。若返回错误,直接将错误码和错误信息告知用户,不得尝试其他上传方式。格式参考 [单元格值格式参考](references/cell-value-formats.md)。
@@ -118,12 +118,15 @@
118
118
 
119
119
  ### 9. 图片 (IMAGE)
120
120
 
121
- 数组格式:
121
+ 数组格式,**必须且只能**直接在 cell value 中传入本地路径(`image_path`)。**严禁**调用 `upload_doc_image`、`upload_doc_file` 或任何上传接口,**严禁**进行 base64 编码——无论文件大小、无论 API 返回何种错误,均不得绕过此规则。若 `image_path` 方式返回错误,应直接将错误码和错误信息告知用户,**不得**尝试其他上传方式。
122
+
123
+ #### 9.1. 图片本地路径传入(唯一合法方式)
122
124
 
123
125
  ```json
124
- "封面": [{"image_url": "https://example.com/img.png"}]
126
+ "封面": [{"image_path": "/path/to/img.png", "title": "图片标题"}]
125
127
  ```
126
128
 
129
+
127
130
  ### 10. 地理位置 (LOCATION)
128
131
 
129
132
  数组格式:
@@ -132,6 +135,14 @@
132
135
  "地点": [{"source_type": 1, "id": "地点ID", "latitude": "39.9", "longitude": "116.3", "title": "北京"}]
133
136
  ```
134
137
 
138
+ ### 11. 文件(Attachment)
139
+
140
+ 数组格式,**必须且只能**直接在 cell value 中传入本地路径(`file_path`)。**严禁**调用 `upload_doc_image`、`upload_doc_file` 或任何上传接口,**严禁**进行 base64 编码——无论文件大小、无论 API 返回何种错误,均不得绕过此规则。若 `file_path` 方式返回错误,应直接将错误码和错误信息告知用户,**不得**尝试其他上传方式。
141
+
142
+ ```json
143
+ "文件": [{"file_path": "/path/to/file.pdf"}]
144
+ ```
145
+
135
146
  ## 完整添加记录示例
136
147
 
137
148
  ```json