@wecom/wecom-openclaw-plugin 2026.3.31-beta.0 → 2026.4.1-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 (209) hide show
  1. package/README.md +4 -251
  2. package/dist/cjs/index.js +45 -0
  3. package/dist/cjs/index.js.map +1 -0
  4. package/dist/cjs/src/channel.js +339 -0
  5. package/dist/cjs/src/channel.js.map +1 -0
  6. package/dist/cjs/src/const.js +137 -0
  7. package/dist/cjs/src/const.js.map +1 -0
  8. package/dist/cjs/src/dm-policy.js +89 -0
  9. package/dist/cjs/src/dm-policy.js.map +1 -0
  10. package/dist/cjs/src/group-policy.js +133 -0
  11. package/dist/cjs/src/group-policy.js.map +1 -0
  12. package/dist/cjs/src/mcp/interceptors/biz-error.js +70 -0
  13. package/dist/cjs/src/mcp/interceptors/biz-error.js.map +1 -0
  14. package/dist/cjs/src/mcp/interceptors/index.js +73 -0
  15. package/dist/cjs/src/mcp/interceptors/index.js.map +1 -0
  16. package/dist/cjs/src/mcp/interceptors/msg-media.js +159 -0
  17. package/dist/cjs/src/mcp/interceptors/msg-media.js.map +1 -0
  18. package/dist/cjs/src/mcp/interceptors/smartpage-create.js +157 -0
  19. package/dist/cjs/src/mcp/interceptors/smartpage-create.js.map +1 -0
  20. package/dist/cjs/src/mcp/interceptors/smartpage-export.js +93 -0
  21. package/dist/cjs/src/mcp/interceptors/smartpage-export.js.map +1 -0
  22. package/dist/cjs/src/mcp/schema.js +104 -0
  23. package/dist/cjs/src/mcp/schema.js.map +1 -0
  24. package/dist/cjs/src/mcp/tool.js +208 -0
  25. package/dist/cjs/src/mcp/tool.js.map +1 -0
  26. package/dist/cjs/src/mcp/transport.js +417 -0
  27. package/dist/cjs/src/mcp/transport.js.map +1 -0
  28. package/dist/cjs/src/media-handler.js +129 -0
  29. package/dist/cjs/src/media-handler.js.map +1 -0
  30. package/dist/cjs/src/media-uploader.js +284 -0
  31. package/dist/cjs/src/media-uploader.js.map +1 -0
  32. package/dist/cjs/src/message-parser.js +134 -0
  33. package/dist/cjs/src/message-parser.js.map +1 -0
  34. package/dist/cjs/src/message-sender.js +89 -0
  35. package/dist/cjs/src/message-sender.js.map +1 -0
  36. package/dist/cjs/src/monitor.js +991 -0
  37. package/dist/cjs/src/monitor.js.map +1 -0
  38. package/dist/cjs/src/onboarding.js +182 -0
  39. package/dist/cjs/src/onboarding.js.map +1 -0
  40. package/dist/cjs/src/openclaw-compat.js +400 -0
  41. package/dist/cjs/src/openclaw-compat.js.map +1 -0
  42. package/dist/cjs/src/reqid-store.js +88 -0
  43. package/dist/cjs/src/reqid-store.js.map +1 -0
  44. package/dist/cjs/src/runtime.js +16 -0
  45. package/dist/cjs/src/runtime.js.map +1 -0
  46. package/dist/cjs/src/state-dir-resolve.js +35 -0
  47. package/dist/cjs/src/state-dir-resolve.js.map +1 -0
  48. package/dist/cjs/src/state-manager.js +158 -0
  49. package/dist/cjs/src/state-manager.js.map +1 -0
  50. package/dist/cjs/src/template-card-parser.js +641 -0
  51. package/dist/cjs/src/template-card-parser.js.map +1 -0
  52. package/dist/cjs/src/timeout.js +42 -0
  53. package/dist/cjs/src/timeout.js.map +1 -0
  54. package/dist/cjs/src/utils.js +60 -0
  55. package/dist/cjs/src/utils.js.map +1 -0
  56. package/dist/cjs/src/version.js +26 -0
  57. package/dist/cjs/src/version.js.map +1 -0
  58. package/dist/esm/index.js +41 -0
  59. package/dist/esm/index.js.map +1 -0
  60. package/dist/esm/src/channel.js +337 -0
  61. package/dist/esm/src/channel.js.map +1 -0
  62. package/dist/{src → esm/src}/const.js +30 -77
  63. package/dist/esm/src/const.js.map +1 -0
  64. package/dist/{src → esm/src}/dm-policy.js +9 -6
  65. package/dist/esm/src/dm-policy.js.map +1 -0
  66. package/dist/{src → esm/src}/group-policy.js +7 -3
  67. package/dist/esm/src/group-policy.js.map +1 -0
  68. package/dist/{src → esm/src}/mcp/interceptors/biz-error.js +6 -2
  69. package/dist/esm/src/mcp/interceptors/biz-error.js.map +1 -0
  70. package/dist/{src → esm/src}/mcp/interceptors/index.js +10 -6
  71. package/dist/esm/src/mcp/interceptors/index.js.map +1 -0
  72. package/dist/{src → esm/src}/mcp/interceptors/msg-media.js +10 -6
  73. package/dist/esm/src/mcp/interceptors/msg-media.js.map +1 -0
  74. package/dist/{src → esm/src}/mcp/interceptors/smartpage-create.js +6 -2
  75. package/dist/esm/src/mcp/interceptors/smartpage-create.js.map +1 -0
  76. package/dist/{src → esm/src}/mcp/interceptors/smartpage-export.js +6 -2
  77. package/dist/esm/src/mcp/interceptors/smartpage-export.js.map +1 -0
  78. package/dist/{src → esm/src}/mcp/schema.js +4 -1
  79. package/dist/esm/src/mcp/schema.js.map +1 -0
  80. package/dist/{src → esm/src}/mcp/tool.js +8 -4
  81. package/dist/esm/src/mcp/tool.js.map +1 -0
  82. package/dist/{src → esm/src}/mcp/transport.js +16 -15
  83. package/dist/esm/src/mcp/transport.js.map +1 -0
  84. package/dist/{src → esm/src}/media-handler.js +10 -6
  85. package/dist/esm/src/media-handler.js.map +1 -0
  86. package/dist/{src → esm/src}/media-uploader.js +10 -64
  87. package/dist/esm/src/media-uploader.js.map +1 -0
  88. package/dist/{src → esm/src}/message-parser.js +4 -1
  89. package/dist/esm/src/message-parser.js.map +1 -0
  90. package/dist/{src → esm/src}/message-sender.js +11 -46
  91. package/dist/esm/src/message-sender.js.map +1 -0
  92. package/dist/{src → esm/src}/monitor.js +282 -151
  93. package/dist/esm/src/monitor.js.map +1 -0
  94. package/dist/{src → esm/src}/onboarding.js +26 -22
  95. package/dist/esm/src/onboarding.js.map +1 -0
  96. package/dist/{src → esm/src}/openclaw-compat.js +27 -143
  97. package/dist/esm/src/openclaw-compat.js.map +1 -0
  98. package/dist/{src → esm/src}/reqid-store.js +7 -4
  99. package/dist/esm/src/reqid-store.js.map +1 -0
  100. package/dist/{src → esm/src}/runtime.js +5 -2
  101. package/dist/esm/src/runtime.js.map +1 -0
  102. package/dist/esm/src/state-dir-resolve.js +13 -0
  103. package/dist/esm/src/state-dir-resolve.js.map +1 -0
  104. package/dist/{src → esm/src}/state-manager.js +16 -84
  105. package/dist/esm/src/state-manager.js.map +1 -0
  106. package/dist/{src → esm/src}/template-card-parser.js +8 -17
  107. package/dist/esm/src/template-card-parser.js.map +1 -0
  108. package/dist/{src → esm/src}/timeout.js +5 -2
  109. package/dist/esm/src/timeout.js.map +1 -0
  110. package/dist/{src → esm/src}/utils.js +10 -24
  111. package/dist/esm/src/utils.js.map +1 -0
  112. package/dist/esm/src/version.js +23 -0
  113. package/dist/esm/src/version.js.map +1 -0
  114. package/dist/esm/types/index.d.ts +9 -0
  115. package/dist/{src → esm/types/src}/const.d.ts +0 -46
  116. package/dist/{src → esm/types/src}/interface.d.ts +2 -0
  117. package/dist/{src → esm/types/src}/message-sender.d.ts +0 -18
  118. package/dist/{src → esm/types/src}/openclaw-compat.d.ts +0 -49
  119. package/dist/esm/types/src/state-dir-resolve.d.ts +2 -0
  120. package/dist/{src → esm/types/src}/utils.d.ts +0 -39
  121. package/dist/esm/types/src/version.d.ts +2 -0
  122. package/dist/index.d.ts +4 -2
  123. package/package.json +7 -15
  124. package/dist/index.js +0 -113
  125. package/dist/src/accounts.d.ts +0 -57
  126. package/dist/src/accounts.js +0 -222
  127. package/dist/src/agent/api-client.d.ts +0 -94
  128. package/dist/src/agent/api-client.js +0 -292
  129. package/dist/src/agent/handler.d.ts +0 -63
  130. package/dist/src/agent/handler.js +0 -532
  131. package/dist/src/agent/index.d.ts +0 -5
  132. package/dist/src/agent/index.js +0 -5
  133. package/dist/src/agent/webhook.d.ts +0 -23
  134. package/dist/src/agent/webhook.js +0 -226
  135. package/dist/src/agent/xml.d.ts +0 -21
  136. package/dist/src/agent/xml.js +0 -41
  137. package/dist/src/channel.js +0 -564
  138. package/dist/src/chat-queue.d.ts +0 -31
  139. package/dist/src/chat-queue.js +0 -45
  140. package/dist/src/http.d.ts +0 -26
  141. package/dist/src/http.js +0 -93
  142. package/dist/src/interface.js +0 -4
  143. package/dist/src/mcp/index.js +0 -6
  144. package/dist/src/mcp/interceptors/types.js +0 -7
  145. package/dist/src/shared/command-auth.d.ts +0 -23
  146. package/dist/src/shared/command-auth.js +0 -73
  147. package/dist/src/shared/xml-parser.d.ts +0 -46
  148. package/dist/src/shared/xml-parser.js +0 -210
  149. package/dist/src/target.d.ts +0 -35
  150. package/dist/src/target.js +0 -67
  151. package/dist/src/template-card-manager.d.ts +0 -55
  152. package/dist/src/template-card-manager.js +0 -205
  153. package/dist/src/types/account.d.ts +0 -29
  154. package/dist/src/types/account.js +0 -4
  155. package/dist/src/types/config.d.ts +0 -80
  156. package/dist/src/types/config.js +0 -7
  157. package/dist/src/types/constants.d.ts +0 -42
  158. package/dist/src/types/constants.js +0 -42
  159. package/dist/src/types/index.d.ts +0 -7
  160. package/dist/src/types/index.js +0 -5
  161. package/dist/src/types/message.d.ts +0 -238
  162. package/dist/src/types/message.js +0 -5
  163. package/dist/src/version.d.ts +0 -2
  164. package/dist/src/version.js +0 -24
  165. package/dist/src/webhook/command-auth.d.ts +0 -47
  166. package/dist/src/webhook/command-auth.js +0 -96
  167. package/dist/src/webhook/gateway.d.ts +0 -36
  168. package/dist/src/webhook/gateway.js +0 -227
  169. package/dist/src/webhook/handler.d.ts +0 -18
  170. package/dist/src/webhook/handler.js +0 -420
  171. package/dist/src/webhook/helpers.d.ts +0 -156
  172. package/dist/src/webhook/helpers.js +0 -833
  173. package/dist/src/webhook/http.d.ts +0 -26
  174. package/dist/src/webhook/http.js +0 -94
  175. package/dist/src/webhook/index.d.ts +0 -11
  176. package/dist/src/webhook/index.js +0 -14
  177. package/dist/src/webhook/media.d.ts +0 -29
  178. package/dist/src/webhook/media.js +0 -88
  179. package/dist/src/webhook/monitor.d.ts +0 -59
  180. package/dist/src/webhook/monitor.js +0 -1147
  181. package/dist/src/webhook/state.d.ts +0 -220
  182. package/dist/src/webhook/state.js +0 -491
  183. package/dist/src/webhook/target.d.ts +0 -41
  184. package/dist/src/webhook/target.js +0 -132
  185. package/dist/src/webhook/types.d.ts +0 -347
  186. package/dist/src/webhook/types.js +0 -33
  187. /package/dist/{src → esm/types/src}/channel.d.ts +0 -0
  188. /package/dist/{src → esm/types/src}/dm-policy.d.ts +0 -0
  189. /package/dist/{src → esm/types/src}/group-policy.d.ts +0 -0
  190. /package/dist/{src → esm/types/src}/mcp/index.d.ts +0 -0
  191. /package/dist/{src → esm/types/src}/mcp/interceptors/biz-error.d.ts +0 -0
  192. /package/dist/{src → esm/types/src}/mcp/interceptors/index.d.ts +0 -0
  193. /package/dist/{src → esm/types/src}/mcp/interceptors/msg-media.d.ts +0 -0
  194. /package/dist/{src → esm/types/src}/mcp/interceptors/smartpage-create.d.ts +0 -0
  195. /package/dist/{src → esm/types/src}/mcp/interceptors/smartpage-export.d.ts +0 -0
  196. /package/dist/{src → esm/types/src}/mcp/interceptors/types.d.ts +0 -0
  197. /package/dist/{src → esm/types/src}/mcp/schema.d.ts +0 -0
  198. /package/dist/{src → esm/types/src}/mcp/tool.d.ts +0 -0
  199. /package/dist/{src → esm/types/src}/mcp/transport.d.ts +0 -0
  200. /package/dist/{src → esm/types/src}/media-handler.d.ts +0 -0
  201. /package/dist/{src → esm/types/src}/media-uploader.d.ts +0 -0
  202. /package/dist/{src → esm/types/src}/message-parser.d.ts +0 -0
  203. /package/dist/{src → esm/types/src}/monitor.d.ts +0 -0
  204. /package/dist/{src → esm/types/src}/onboarding.d.ts +0 -0
  205. /package/dist/{src → esm/types/src}/reqid-store.d.ts +0 -0
  206. /package/dist/{src → esm/types/src}/runtime.d.ts +0 -0
  207. /package/dist/{src → esm/types/src}/state-manager.d.ts +0 -0
  208. /package/dist/{src → esm/types/src}/template-card-parser.d.ts +0 -0
  209. /package/dist/{src → esm/types/src}/timeout.d.ts +0 -0
@@ -1,3 +1,21 @@
1
+ import * as os from 'os';
2
+ import * as path from 'path';
3
+ import { WSClient, WSAuthFailureError, WSReconnectExhaustedError, generateReqId } from '@wecom/aibot-node-sdk';
4
+ import { getWeComRuntime } from './runtime.js';
5
+ import { getDefaultMediaLocalRoots } from './openclaw-compat.js';
6
+ import { SCENE_WECOM_OPENCLAW, WS_MAX_AUTH_FAILURE_ATTEMPTS, WS_MAX_RECONNECT_ATTEMPTS, WS_HEARTBEAT_INTERVAL_MS, EVENT_ENTER_CHECK_UPDATE, CMD_ENTER_EVENT_REPLY, CHANNEL_ID, MEDIA_IMAGE_PLACEHOLDER, MEDIA_DOCUMENT_PLACEHOLDER, THINKING_MESSAGE } from './const.js';
7
+ export { WeComCommand } from './const.js';
8
+ import { parseMessageContent } from './message-parser.js';
9
+ import { sendWeComReply, StreamExpiredError } from './message-sender.js';
10
+ import { downloadAndSaveImages, downloadAndSaveFiles } from './media-handler.js';
11
+ import { uploadAndSendMedia } from './media-uploader.js';
12
+ import { maskTemplateCardBlocks, extractTemplateCards } from './template-card-parser.js';
13
+ import { checkGroupPolicy } from './group-policy.js';
14
+ import { checkDmPolicy } from './dm-policy.js';
15
+ import { startMessageStateCleanup, setWeComWebSocket, warmupReqIdStore, stopMessageStateCleanup, cleanupAccount, setReqIdForChat, setMessageState, deleteMessageState } from './state-manager.js';
16
+ export { getWeComWebSocket } from './state-manager.js';
17
+ import { PLUGIN_VERSION } from './version.js';
18
+
1
19
  /**
2
20
  * 企业微信 WebSocket 监控器主模块
3
21
  *
@@ -15,22 +33,6 @@
15
33
  * - state-manager.ts : 全局状态管理(带 TTL 清理)
16
34
  * - timeout.ts : 超时工具
17
35
  */
18
- import * as os from "os";
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";
23
- 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";
24
- import { parseMessageContent } from "./message-parser.js";
25
- import { sendWeComReply, sendWeComReplyNonBlocking, 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";
32
- import { setWeComWebSocket, setMessageState, deleteMessageState, setReqIdForChat, warmupReqIdStore, startMessageStateCleanup, stopMessageStateCleanup, cleanupAccount, } from "./state-manager.js";
33
- import { PLUGIN_VERSION } from "./version.js";
34
36
  /**
35
37
  * 去除文本中的 `<think>...</think>` 标签(支持跨行),返回剩余可见文本。
36
38
  * 用于判断大模型回复中是否包含实际用户可见内容(而非仅有 thinking 推理过程)。
@@ -39,9 +41,148 @@ function stripThinkTags(text) {
39
41
  return text;
40
42
  // return text.replace(/<think>[\s\S]*?<\/think>/g, "").trim();
41
43
  }
44
+ const sentTemplateCardByTaskId = new Map();
45
+ const TEMPLATE_CARD_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
46
+ const TEMPLATE_CARD_CACHE_MAX_SIZE = 300;
47
+ function getTemplateCardCacheKey(accountId, taskId) {
48
+ return `${accountId}:${taskId}`;
49
+ }
50
+ function pruneTemplateCardCache() {
51
+ const now = Date.now();
52
+ for (const [key, entry] of sentTemplateCardByTaskId) {
53
+ if (now - entry.createdAt >= TEMPLATE_CARD_CACHE_TTL_MS) {
54
+ sentTemplateCardByTaskId.delete(key);
55
+ }
56
+ }
57
+ if (sentTemplateCardByTaskId.size <= TEMPLATE_CARD_CACHE_MAX_SIZE) {
58
+ return;
59
+ }
60
+ const sortedEntries = [...sentTemplateCardByTaskId.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
61
+ const removeCount = sentTemplateCardByTaskId.size - TEMPLATE_CARD_CACHE_MAX_SIZE;
62
+ for (const [key] of sortedEntries.slice(0, removeCount)) {
63
+ sentTemplateCardByTaskId.delete(key);
64
+ }
65
+ }
66
+ function cloneTemplateCard(card) {
67
+ return JSON.parse(JSON.stringify(card));
68
+ }
69
+ function saveTemplateCardToCache(params) {
70
+ const { accountId, templateCard, runtime } = params;
71
+ const taskId = templateCard.task_id;
72
+ if (!taskId) {
73
+ runtime.log?.("[wecom][template-card] Skip cache: template card has no task_id");
74
+ return;
75
+ }
76
+ sentTemplateCardByTaskId.set(getTemplateCardCacheKey(accountId, taskId), {
77
+ templateCard: cloneTemplateCard(templateCard),
78
+ createdAt: Date.now(),
79
+ });
80
+ pruneTemplateCardCache();
81
+ }
82
+ function getTemplateCardFromCache(accountId, taskId) {
83
+ pruneTemplateCardCache();
84
+ const cached = sentTemplateCardByTaskId.get(getTemplateCardCacheKey(accountId, taskId));
85
+ if (!cached) {
86
+ return undefined;
87
+ }
88
+ return cloneTemplateCard(cached.templateCard);
89
+ }
90
+ function buildSelectedOptionMap(templateCardEvent) {
91
+ const selectedMap = new Map();
92
+ const selectedItems = templateCardEvent?.selected_items?.selected_item ?? [];
93
+ for (const item of selectedItems) {
94
+ const questionKey = item.question_key?.trim();
95
+ if (!questionKey) {
96
+ continue;
97
+ }
98
+ const optionIds = item.option_ids?.option_id?.filter(Boolean) ?? [];
99
+ selectedMap.set(questionKey, optionIds);
100
+ }
101
+ return selectedMap;
102
+ }
103
+ function applySelectedStateToTemplateCard(params) {
104
+ const { templateCard, selectedMap, templateCardEvent } = params;
105
+ const nextCard = cloneTemplateCard(templateCard);
106
+ if (templateCardEvent?.task_id) {
107
+ nextCard.task_id = templateCardEvent.task_id;
108
+ }
109
+ if (templateCardEvent?.card_type) {
110
+ nextCard.card_type = templateCardEvent.card_type;
111
+ }
112
+ // 交互完成后将提交按钮文案更新为已提交,提升用户感知
113
+ if (nextCard.submit_button?.text) {
114
+ nextCard.submit_button.text = "已提交";
115
+ }
116
+ if (nextCard.checkbox?.question_key) {
117
+ const selectedIds = selectedMap.get(nextCard.checkbox.question_key) ?? [];
118
+ nextCard.checkbox.disable = true;
119
+ if (Array.isArray(nextCard.checkbox.option_list)) {
120
+ nextCard.checkbox.option_list = nextCard.checkbox.option_list.map((option) => ({
121
+ ...option,
122
+ is_checked: selectedIds.includes(option.id),
123
+ }));
124
+ }
125
+ }
126
+ if (Array.isArray(nextCard.select_list)) {
127
+ nextCard.select_list = nextCard.select_list.map((selection) => {
128
+ const selectedIds = selectedMap.get(selection.question_key) ?? [];
129
+ return {
130
+ ...selection,
131
+ disable: true,
132
+ selected_id: selectedIds[0] ?? selection.selected_id,
133
+ };
134
+ });
135
+ }
136
+ if (nextCard.button_selection?.question_key) {
137
+ const selectedIds = selectedMap.get(nextCard.button_selection.question_key) ?? [];
138
+ nextCard.button_selection.disable = true;
139
+ if (selectedIds[0]) {
140
+ nextCard.button_selection.selected_id = selectedIds[0];
141
+ }
142
+ }
143
+ return nextCard;
144
+ }
145
+ async function updateTemplateCardOnEvent(params) {
146
+ const { frame, accountId, runtime, wsClient } = params;
147
+ const body = frame.body;
148
+ const templateCardEvent = body.event?.template_card_event;
149
+ const taskId = templateCardEvent?.task_id;
150
+ if (!taskId) {
151
+ runtime.log?.(`[${accountId}] [template-card-update] Skip update: missing task_id in callback`);
152
+ return;
153
+ }
154
+ const cachedCard = getTemplateCardFromCache(accountId, taskId);
155
+ if (!cachedCard) {
156
+ runtime.log?.(`[${accountId}] [template-card-update] Skip update: task_id=${taskId} not found in cache`);
157
+ return;
158
+ }
159
+ const selectedMap = buildSelectedOptionMap(templateCardEvent);
160
+ const updatedCard = applySelectedStateToTemplateCard({
161
+ templateCard: cachedCard,
162
+ selectedMap,
163
+ templateCardEvent,
164
+ });
165
+ await wsClient.updateTemplateCard(frame, updatedCard, [body.from.userid]);
166
+ runtime.log?.(`[${accountId}] [template-card-update] Updated card by task_id=${taskId}`);
167
+ // 将更新后的卡片写回缓存,后续多次点击时状态保持一致
168
+ saveTemplateCardToCache({
169
+ accountId,
170
+ templateCard: updatedCard,
171
+ runtime,
172
+ });
173
+ }
42
174
  // ============================================================================
43
175
  // 媒体本地路径白名单扩展
44
176
  // ============================================================================
177
+ /**
178
+ * 解析 openclaw 状态目录(与 plugin-sdk 内部逻辑保持一致)
179
+ */
180
+ function resolveStateDir() {
181
+ const stateOverride = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim();
182
+ if (stateOverride)
183
+ return stateOverride;
184
+ return path.join(os.homedir(), ".openclaw");
185
+ }
45
186
  /**
46
187
  * 在 getDefaultMediaLocalRoots() 基础上,将 stateDir 本身也加入白名单,
47
188
  * 并合并用户在 WeComConfig 中配置的自定义 mediaLocalRoots。
@@ -95,9 +236,6 @@ function buildMediaErrorSummary(mediaUrl, result) {
95
236
  }
96
237
  return `⚠️ 文件发送失败:无法处理文件 ${mediaUrl},请稍后再试。`;
97
238
  }
98
- export { WeComCommand } from "./const.js";
99
- export { getWeComWebSocket, setReqIdForChat, getReqIdForChatAsync, getReqIdForChat, deleteReqIdForChat, warmupReqIdStore, flushReqIdStore, } from "./state-manager.js";
100
- export { sendWeComReply } from "./message-sender.js";
101
239
  // ============================================================================
102
240
  // 消息上下文构建
103
241
  // ============================================================================
@@ -129,12 +267,8 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
129
267
  const mediaTypes = mediaList.length > 0
130
268
  ? mediaList.map((m) => m.contentType).filter(Boolean)
131
269
  : undefined;
132
- // 使用 route.agentId 解析 storePath(多 agent 场景下 session 路径隔离)
133
- const storePath = core.channel.session.resolveStorePath(config.session?.store, {
134
- agentId: route.agentId,
135
- });
136
270
  // 构建标准消息上下文
137
- const ctxPayload = core.channel.reply.finalizeInboundContext({
271
+ return core.channel.reply.finalizeInboundContext({
138
272
  Body: messageBody,
139
273
  RawBody: messageBody,
140
274
  CommandBody: messageBody,
@@ -143,7 +277,7 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
143
277
  To: `${CHANNEL_ID}:${chatId}`,
144
278
  SenderId: body.from.userid,
145
279
  SessionKey: route.sessionKey,
146
- AccountId: route.accountId,
280
+ AccountId: account.accountId,
147
281
  ChatType: chatType,
148
282
  ConversationLabel: fromLabel,
149
283
  Timestamp: Date.now(),
@@ -162,7 +296,6 @@ function buildMessageContext(frame, account, config, text, mediaList, quoteConte
162
296
  MediaUrls: mediaPaths,
163
297
  ReplyToBody: quoteContent,
164
298
  });
165
- return { ctxPayload, route, storePath, chatId, chatType };
166
299
  }
167
300
  /**
168
301
  * 发送"思考中"消息
@@ -234,38 +367,68 @@ async function sendMediaBatch(ctx, mediaUrls) {
234
367
  * 替换掉 thinking 动画,否则 thinking 会一直残留。
235
368
  *
236
369
  * 关闭策略(按优先级):
370
+ * 0. [新增] 有模板卡片代码块 → 提取卡片并主动发送,用剩余文本关闭流
237
371
  * 1. 有可见文本 → 用完整文本关闭
238
- * 2. 有模板卡片发送成功 → "📋 卡片消息已发送。"
239
- * 3. 有媒体成功发送(通过 deliver 回调) → 用友好提示"文件已发送"
240
- * 4. 媒体发送失败 → 直接用错误摘要替换 thinking
372
+ * 2. 有媒体成功发送(通过 deliver 回调) → 用友好提示"文件已发送"
373
+ * 3. 媒体发送失败 → 直接用错误摘要替换 thinking
374
+ * 4. 其他 → 用通用"处理完成"提示
375
+ * (agent 可能已通过内置 message 工具直接发送了文件,
376
+ * 该路径走 outbound.sendMedia 完全绕过 deliver 回调,
377
+ * 所以 state 中无记录,但文件已实际送达)
241
378
  *
242
379
  * 降级策略:
243
380
  * - 当 streamExpired=true(errcode 846608)时,流式通道已不可用(>6分钟),
244
381
  * 改用 wsClient.sendMessage 主动发送完整文本。
245
- *
246
- * 注意:模板卡片的检测和发送已在 finishThinkingStream 之前由
247
- * processTemplateCardsIfNeeded 完成,此处只关心最后的消息发送。
248
382
  */
249
383
  async function finishThinkingStream(ctx) {
250
384
  const { wsClient, frame, state, runtime } = ctx;
251
385
  const body = frame.body;
252
386
  const chatId = body.chatid || body.from.userid;
253
387
  const visibleText = stripThinkTags(state.accumulatedText);
388
+ // ── 模板卡片检测与发送 ──────────────────────────────────────────────
389
+ // 在确定 finishText 之前,先检查累积文本中是否包含模板卡片 JSON 代码块。
390
+ // 若检测到合法卡片,通过 sendMessage 主动发送后,用剩余文本关闭流。
391
+ if (visibleText) {
392
+ runtime.log?.(`[wecom][template-card] finishThinkingStream: visibleText exists, length=${visibleText.length}, running extractTemplateCards...`);
393
+ const logFn = (...args) => {
394
+ runtime.log?.(...args);
395
+ };
396
+ const { cards, remainingText } = extractTemplateCards(state.accumulatedText, logFn);
397
+ runtime.log?.(`[wecom][template-card] finishThinkingStream: extractTemplateCards result — cards=${cards.length}, remainingTextLength=${remainingText.length}`);
398
+ if (cards.length > 0) {
399
+ runtime.log?.(`[wecom][template-card] finishThinkingStream: ${cards.length} card(s) detected, card_types=[${cards.map(c => c.cardType).join(", ")}]`);
400
+ await sendTemplateCards(ctx, cards);
401
+ // 用剩余文本关闭流(可能为空)
402
+ const trimmedRemaining = stripThinkTags(remainingText);
403
+ const finishText = trimmedRemaining
404
+ ? remainingText
405
+ : (state.hasTemplateCard ? "📋 卡片消息已发送。" : "");
406
+ runtime.log?.(`[wecom][template-card] finishThinkingStream: closing stream with finishText="${finishText.slice(0, 100)}...", hasTemplateCard=${state.hasTemplateCard}`);
407
+ await sendWeComReply({ wsClient, frame, text: finishText, runtime, finish: true, streamId: state.streamId });
408
+ return;
409
+ }
410
+ }
411
+ else {
412
+ runtime.log?.(`[wecom][template-card] finishThinkingStream: no visibleText, skipping template card extraction`);
413
+ }
414
+ // ── 模板卡片检测结束 ────────────────────────────────────────────────
254
415
  let finishText = state.accumulatedText;
255
416
  if (visibleText) {
417
+ // 有可见文本:用完整文本关闭流(覆盖 thinking 为真实内容)
256
418
  finishText = state.accumulatedText;
257
419
  }
258
- else if (state.hasTemplateCard) {
259
- finishText = "📋 卡片消息已发送。";
260
- }
261
420
  else if (state.hasMedia) {
262
421
  if (state.hasMediaFailed && state.mediaErrorSummary) {
422
+ // 媒体成功发送:用友好提示告知用户
263
423
  finishText = finishText ? `${finishText}\n\n${state.mediaErrorSummary}` : state.mediaErrorSummary;
264
424
  }
265
425
  else if (!finishText) {
266
426
  finishText = "📎 文件已发送,请查收。";
267
427
  }
268
428
  }
429
+ // if (!finishText) {
430
+ // finishText = "✅ 处理完成。";
431
+ // }
269
432
  if (finishText) {
270
433
  // 尝试流式发送;若已知过期或发送时发现过期,统一降级为主动发送
271
434
  let expired = state.streamExpired;
@@ -291,11 +454,46 @@ async function finishThinkingStream(ctx) {
291
454
  }
292
455
  }
293
456
  }
457
+ /**
458
+ * 逐个发送已提取的模板卡片(通过 wsClient.sendMessage 主动推送)
459
+ *
460
+ * 发送失败不阻塞流程,仅记录错误日志。
461
+ */
462
+ async function sendTemplateCards(ctx, cards) {
463
+ const { wsClient, frame, state, runtime, account } = ctx;
464
+ const body = frame.body;
465
+ const chatId = body.chatid || body.from.userid;
466
+ for (const card of cards) {
467
+ try {
468
+ runtime.log?.(`[wecom][template-card] Sending card_type=${card.cardType} to chatId=${chatId}`);
469
+ const rawTemplateCard = card.cardJson;
470
+ if (typeof rawTemplateCard.card_type !== "string") {
471
+ runtime.error?.("[wecom][template-card] Skip sending invalid card: missing card_type");
472
+ continue;
473
+ }
474
+ const templateCard = rawTemplateCard;
475
+ await wsClient.sendMessage(chatId, {
476
+ msgtype: "template_card",
477
+ template_card: templateCard,
478
+ });
479
+ state.hasTemplateCard = true;
480
+ saveTemplateCardToCache({
481
+ accountId: account.accountId,
482
+ templateCard,
483
+ runtime,
484
+ });
485
+ runtime.log?.(`[wecom][template-card] Card sent successfully: card_type=${card.cardType}`);
486
+ }
487
+ catch (err) {
488
+ runtime.error?.(`[wecom][template-card] Failed to send card: card_type=${card.cardType}, error=${JSON.stringify(err)}`);
489
+ }
490
+ }
491
+ }
294
492
  /**
295
493
  * 路由消息到核心处理流程并处理回复
296
494
  */
297
495
  async function routeAndDispatchMessage(params) {
298
- const { ctxPayload, route, storePath, chatId, chatType, config, account, wsClient, frame, state, runtime, onCleanup } = params;
496
+ const { ctxPayload, config, account, wsClient, frame, state, runtime, onCleanup } = params;
299
497
  const core = getWeComRuntime();
300
498
  const ctx = { wsClient, frame, state, account, runtime };
301
499
  // 防止 onCleanup 被多次调用(onError 回调与 catch 块可能重复触发)
@@ -308,23 +506,6 @@ async function routeAndDispatchMessage(params) {
308
506
  };
309
507
  let isShowThink = !(account.sendThinkingMessage ?? true);
310
508
  try {
311
- // 记录 inbound session 元数据(session 追踪)
312
- await core.channel.session.recordInboundSession({
313
- storePath,
314
- sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
315
- ctx: ctxPayload,
316
- updateLastRoute: chatType !== "group"
317
- ? {
318
- sessionKey: route.mainSessionKey,
319
- channel: CHANNEL_ID,
320
- to: `${CHANNEL_ID}:${chatId}`,
321
- accountId: route.accountId,
322
- }
323
- : undefined,
324
- onRecordError: (err) => {
325
- runtime.error?.(`[wecom] failed updating session meta: ${String(err)}`);
326
- },
327
- });
328
509
  await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
329
510
  ctx: ctxPayload,
330
511
  cfg: config,
@@ -355,10 +536,11 @@ async function routeAndDispatchMessage(params) {
355
536
  }
356
537
  },
357
538
  deliver: async (payload, info) => {
539
+ state.deliverCalled = true;
358
540
  // runtime.log?.(`[openclaw -> plugin] kind=${info.kind}, text=${payload.text ?? ''}, mediaUrl=${payload.mediaUrl ?? ''}, mediaUrls=${JSON.stringify(payload.mediaUrls ?? [])}`);
359
- // 累积文本(快速指令等非流式回复只走 deliver,不走 onPartialReply)
541
+ // 累积文本
360
542
  if (payload.text) {
361
- state.accumulatedText = payload.text;
543
+ state.accumulatedText += (payload.text || '');
362
544
  }
363
545
  // 发送媒体(统一走主动发送)
364
546
  const mediaUrls = payload.mediaUrls?.length ? payload.mediaUrls : payload.mediaUrl ? [payload.mediaUrl] : [];
@@ -378,62 +560,42 @@ async function routeAndDispatchMessage(params) {
378
560
  runtime.error?.(`[wecom] sendMediaBatch threw: ${errMsg}`);
379
561
  }
380
562
  }
381
- // 中间帧流式更新已迁移到 onPartialReply(token 级别,更细粒度)
382
- // deliver 只负责文本累积和媒体发送
383
- },
384
- onError: (err, info) => {
385
- runtime.error?.(`[wecom] ${info.kind} reply failed: ${String(err)}`);
386
- },
387
- },
388
- // onPartialReply:token 级别的流式 UI 刷新
389
- // 使用 SDK 的 replyStreamNonBlocking:上一条同 reqId 的消息未收到 ack 时跳过本次发送,
390
- // 避免流式中间帧排队积压导致延迟。丢帧在流式场景下可接受(text 是累积文本,最终由 finishThinkingStream 兜底)。
391
- replyOptions: {
392
- onPartialReply: (data) => {
393
- // 累积文本
394
- if (data.text) {
395
- state.accumulatedText = (data.text || '');
396
- }
397
- if (!state.streamId || !data.text || state.streamExpired)
398
- return;
563
+ // 中间帧:有可见文本时流式更新(流式过期后跳过,等 deliver 完成后主动发送)
399
564
  // 使用 maskTemplateCardBlocks 遮罩正在构建中的模板卡片代码块,
400
565
  // 避免 JSON 源码在流式输出过程中暴露给终端用户
401
- const displayText = maskTemplateCardBlocks(data.text, (...args) => runtime.log?.(...args));
402
- if (displayText !== data.text) {
403
- // runtime.log?.(`[wecom][template-card] Partial-reply masked: original=${data.text.length}chars, masked=${displayText.length}chars`);
404
- }
405
- // fire-and-forget:不阻塞 onPartialReply 回调
406
- sendWeComReplyNonBlocking({
407
- wsClient, frame, text: displayText, runtime,
408
- streamId: state.streamId,
409
- }).catch((err) => {
410
- if (err instanceof StreamExpiredError) {
411
- state.streamExpired = true;
412
- runtime.log?.(`[wecom] Stream expired during partial reply, will fallback to proactive send`);
566
+ if (info.kind !== "final" && state.accumulatedText && !state.streamExpired) {
567
+ try {
568
+ const displayText = maskTemplateCardBlocks(state.accumulatedText, (...args) => runtime.log?.(...args));
569
+ if (displayText !== state.accumulatedText) {
570
+ runtime.log?.(`[wecom][template-card] Mid-frame masked: original=${state.accumulatedText.length}chars, masked=${displayText.length}chars`);
571
+ }
572
+ await sendWeComReply({ wsClient, frame, text: displayText, runtime, finish: false, streamId: state.streamId });
413
573
  }
414
- // 其他错误静默忽略,不影响主流程(deliver + finishThinkingStream 兜底)
415
- });
574
+ catch (err) {
575
+ if (err instanceof StreamExpiredError) {
576
+ state.streamExpired = true;
577
+ runtime.log?.(`[wecom] Stream expired during intermediate reply, will fallback to proactive send`);
578
+ }
579
+ else {
580
+ throw err;
581
+ }
582
+ }
583
+ }
584
+ },
585
+ onError: (err, info) => {
586
+ runtime.error?.(`[wecom] ${info.kind} reply failed: ${String(err)}`);
416
587
  },
417
588
  },
418
589
  });
419
- // 模板卡片检测与发送(在关闭 thinking 流之前独立处理)
420
- const cardResult = await processTemplateCardsIfNeeded({ wsClient, frame, state, account, runtime });
421
- if (cardResult) {
422
- // 卡片已发送,用剩余文本替换累积文本
423
- state.accumulatedText = cardResult.remainingText;
424
- }
425
590
  // 关闭 thinking 流
426
591
  await finishThinkingStream(ctx);
427
592
  safeCleanup();
428
593
  }
429
594
  catch (err) {
430
595
  runtime.error?.(`[wecom][plugin] Failed to process message: ${String(err)}`);
431
- // 即使 dispatch 抛异常,也需要处理卡片和关闭 thinking 流
596
+ // 即使 dispatch 抛异常,也需要关闭 thinking 流,
597
+ // 避免 deliver 已成功发送媒体但后续出错时 thinking 消息残留或被错误文案覆盖
432
598
  try {
433
- const cardResult = await processTemplateCardsIfNeeded({ wsClient, frame, state, account, runtime });
434
- if (cardResult) {
435
- state.accumulatedText = cardResult.remainingText;
436
- }
437
599
  await finishThinkingStream(ctx);
438
600
  }
439
601
  catch (finishErr) {
@@ -443,12 +605,20 @@ async function routeAndDispatchMessage(params) {
443
605
  }
444
606
  }
445
607
  /**
446
- * 解析并校验企业微信消息(防抖前阶段:Step 1-4)
608
+ * 处理企业微信消息(主函数)
447
609
  *
448
- * 执行消息解析、策略检查、媒体下载等前置操作,
449
- * 返回一个可用于防抖缓冲的 entry,或 null(消息被过滤/跳过时)。
610
+ * 处理流程:
611
+ * 1. 解析消息内容(文本、图片、引用)
612
+ * 2. 群组策略检查(仅群聊)
613
+ * 3. DM Policy 访问控制检查(仅私聊)
614
+ * 4. 下载并保存图片
615
+ * 5. 初始化消息状态
616
+ * 6. 发送"思考中"消息
617
+ * 7. 路由消息到核心处理流程
618
+ *
619
+ * 整体带超时保护,防止单条消息处理阻塞过久
450
620
  */
451
- async function prepareWeComMessage(params) {
621
+ async function processWeComMessage(params) {
452
622
  const { frame, account, config, runtime, wsClient } = params;
453
623
  const body = frame.body;
454
624
  const chatId = body.chatid || body.from.userid;
@@ -470,7 +640,7 @@ async function prepareWeComMessage(params) {
470
640
  // 如果既没有文本也没有图片也没有文件也没有引用内容,则跳过
471
641
  if (!text && imageUrls.length === 0 && fileUrls.length === 0) {
472
642
  runtime.log?.("[wecom][plugin] Skipping empty message (no text, image, file or quote)");
473
- return null;
643
+ return;
474
644
  }
475
645
  // Step 2: 群组策略检查(仅群聊)
476
646
  if (chatType === "group") {
@@ -482,7 +652,7 @@ async function prepareWeComMessage(params) {
482
652
  runtime,
483
653
  });
484
654
  if (!groupPolicyResult.allowed) {
485
- return null;
655
+ return;
486
656
  }
487
657
  }
488
658
  // Step 3: DM Policy 访问控制检查(仅私聊)
@@ -495,7 +665,7 @@ async function prepareWeComMessage(params) {
495
665
  runtime,
496
666
  });
497
667
  if (!dmPolicyResult.allowed) {
498
- return null;
668
+ return;
499
669
  }
500
670
  // Step 4: 下载并保存图片和文件
501
671
  const [imageMediaList, fileMediaList] = await Promise.all([
@@ -517,28 +687,6 @@ async function prepareWeComMessage(params) {
517
687
  }),
518
688
  ]);
519
689
  const mediaList = [...imageMediaList, ...fileMediaList];
520
- return {
521
- frame,
522
- account,
523
- config,
524
- runtime,
525
- wsClient,
526
- text,
527
- mediaList,
528
- quoteContent,
529
- messageId,
530
- chatId,
531
- reqId,
532
- };
533
- }
534
- /**
535
- * 处理企业微信消息(Step 5-7)
536
- *
537
- * 接收解析后的消息数据,执行初始化状态、发送 thinking、路由到核心。
538
- * 同一会话中的消息通过串行队列保证按序执行。
539
- */
540
- async function processWeComMessageNow(entry) {
541
- const { frame, account, config, runtime, wsClient, text, mediaList, quoteContent, messageId, chatId, reqId } = entry;
542
690
  // Step 5: 初始化消息状态
543
691
  setReqIdForChat(chatId, reqId, account.accountId);
544
692
  const streamId = generateReqId("stream");
@@ -553,15 +701,11 @@ async function processWeComMessageNow(entry) {
553
701
  // await sendThinkingReply({ wsClient, frame, streamId, runtime });
554
702
  // }
555
703
  // Step 7: 构建上下文并路由到核心处理流程(带整体超时保护)
556
- const { ctxPayload, route, storePath, chatId: resolvedChatId, chatType } = buildMessageContext(frame, account, config, text, mediaList, quoteContent);
704
+ const ctxPayload = buildMessageContext(frame, account, config, text, mediaList, quoteContent);
557
705
  // runtime.log?.(`[plugin -> openclaw] body=${text}, mediaPaths=${JSON.stringify(mediaList.map(m => m.path))}${quoteContent ? `, quote=${quoteContent}` : ''}`);
558
706
  try {
559
707
  await routeAndDispatchMessage({
560
708
  ctxPayload,
561
- route,
562
- storePath,
563
- chatId: resolvedChatId,
564
- chatType,
565
709
  config,
566
710
  account,
567
711
  wsClient,
@@ -605,7 +749,7 @@ function createSdkLogger(runtime, accountId) {
605
749
  * 监听企业微信 WebSocket 连接
606
750
  * 使用 aibot-node-sdk 简化连接管理
607
751
  */
608
- export async function monitorWeComProvider(options) {
752
+ async function monitorWeComProvider(options) {
609
753
  const { account, config, runtime, abortSignal, setStatus } = options;
610
754
  runtime.log?.(`[${account.accountId}] [${PLUGIN_VERSION}] Initializing WSClient with SDK...`);
611
755
  runtime.error?.(`[${account.accountId}] [diag] monitor boot marker: build=20260325-event-debug-1`);
@@ -750,26 +894,13 @@ export async function monitorWeComProvider(options) {
750
894
  // 监听普通消息
751
895
  wsClient.on("message", async (frame) => {
752
896
  try {
753
- const entry = await prepareWeComMessage({
897
+ await processWeComMessage({
754
898
  frame,
755
899
  account,
756
900
  config,
757
901
  runtime,
758
902
  wsClient,
759
903
  });
760
- if (!entry)
761
- return;
762
- // 排队逻辑暂时关闭,直接处理消息
763
- // const { status } = enqueueWeComChatTask({
764
- // accountId: entry.account.accountId,
765
- // chatId: entry.chatId,
766
- // task: () => processWeComMessageNow(entry),
767
- // });
768
- //
769
- // if (status === "queued") {
770
- // runtime.log?.(`[wecom] Chat task queued for chat=${entry.chatId} (previous task still running)`);
771
- // }
772
- await processWeComMessageNow(entry);
773
904
  }
774
905
  catch (err) {
775
906
  runtime.error?.(`[${account.accountId}] Failed to process message: ${String(err)}`);
@@ -799,16 +930,13 @@ export async function monitorWeComProvider(options) {
799
930
  catch (updateErr) {
800
931
  runtime.error?.(`[${account.accountId}] [template-card-update] Failed to update template card: ${String(updateErr)}`);
801
932
  }
802
- const entry = await prepareWeComMessage({
933
+ await processWeComMessage({
803
934
  frame,
804
935
  account,
805
936
  config,
806
937
  runtime,
807
938
  wsClient,
808
939
  });
809
- if (entry) {
810
- await processWeComMessageNow(entry);
811
- }
812
940
  }
813
941
  catch (err) {
814
942
  runtime.error?.(`[${account.accountId}] Failed to process template_card_event: ${String(err)}`);
@@ -830,3 +958,6 @@ export async function monitorWeComProvider(options) {
830
958
  });
831
959
  });
832
960
  }
961
+
962
+ export { monitorWeComProvider, sendWeComReply, setReqIdForChat, warmupReqIdStore };
963
+ //# sourceMappingURL=monitor.js.map