@wecom/wecom-openclaw-plugin 2026.7.2 → 2026.8.17-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 (184) hide show
  1. package/README.md +33 -4
  2. package/dist/index.js +14 -21
  3. package/dist/src/cli/argv.d.ts +36 -0
  4. package/dist/src/cli/argv.js +155 -0
  5. package/dist/src/cli/const.d.ts +94 -0
  6. package/dist/src/cli/const.js +107 -0
  7. package/dist/src/cli/credentials.d.ts +54 -0
  8. package/dist/src/cli/credentials.js +342 -0
  9. package/dist/src/cli/index.d.ts +5 -0
  10. package/dist/src/cli/index.js +5 -0
  11. package/dist/src/cli/legacy-tool-warning.d.ts +12 -0
  12. package/dist/src/cli/legacy-tool-warning.js +20 -0
  13. package/dist/src/cli/locate.d.ts +32 -0
  14. package/dist/src/cli/locate.js +196 -0
  15. package/dist/src/cli/process-launcher.d.ts +1 -0
  16. package/dist/src/cli/process-launcher.js +1 -0
  17. package/dist/src/cli/process-output.d.ts +14 -0
  18. package/dist/src/cli/process-output.js +43 -0
  19. package/dist/src/cli/tool.d.ts +46 -0
  20. package/dist/src/cli/tool.js +363 -0
  21. package/dist/src/const.d.ts +0 -8
  22. package/dist/src/const.js +0 -14
  23. package/dist/src/monitor.js +1 -12
  24. package/dist/src/state-manager.d.ts +0 -19
  25. package/dist/src/state-manager.js +1 -34
  26. package/dist/src/utils.d.ts +27 -0
  27. package/openclaw.plugin.json +1 -1
  28. package/package.json +3 -3
  29. package/skills/wecom-send-media/SKILL.md +2 -0
  30. package/skills/wecom-send-template-card/SKILL.md +1 -1
  31. package/skills/wecomcli-calendar/SKILL.md +302 -0
  32. package/skills/wecomcli-calendar/references/calendar-agenda.md +228 -0
  33. package/skills/wecomcli-calendar/references/calendar-cancel.md +110 -0
  34. package/skills/wecomcli-calendar/references/calendar-create.md +240 -0
  35. package/skills/wecomcli-calendar/references/calendar-freebusy.md +209 -0
  36. package/skills/wecomcli-calendar/references/calendar-meeting-room.md +172 -0
  37. package/skills/wecomcli-calendar/references/calendar-search.md +208 -0
  38. package/skills/wecomcli-calendar/references/calendar-update.md +274 -0
  39. package/skills/wecomcli-contact/SKILL.md +59 -0
  40. package/skills/wecomcli-disk/SKILL.md +401 -0
  41. package/skills/wecomcli-doc/SKILL.md +140 -0
  42. package/skills/wecomcli-doc/references/doc-contents-append.md +22 -0
  43. package/skills/wecomcli-doc/references/doc-contents-overwrite.md +29 -0
  44. package/skills/wecomcli-doc/references/doc-create.md +161 -0
  45. package/skills/wecomcli-doc/scripts/build_docx.py +1375 -0
  46. package/skills/wecomcli-doc-manage/SKILL.md +135 -0
  47. package/skills/wecomcli-doc-manage/references/doc-members-update.md +26 -0
  48. package/skills/wecomcli-doc-manage/references/doc-names-update.md +22 -0
  49. package/skills/wecomcli-doc-manage/references/doc-rules-update.md +24 -0
  50. package/skills/wecomcli-email/SKILL.md +216 -0
  51. package/skills/wecomcli-email/references/forward-mail.md +131 -0
  52. package/skills/wecomcli-email/references/get-mail.md +168 -0
  53. package/skills/wecomcli-email/references/reply-mail.md +140 -0
  54. package/skills/wecomcli-email/references/search-mail.md +113 -0
  55. package/skills/wecomcli-email/references/security.md +53 -0
  56. package/skills/wecomcli-email/references/send-mail.md +186 -0
  57. package/skills/wecomcli-email/references/send-schedule.md +83 -0
  58. package/skills/wecomcli-media/SKILL.md +100 -0
  59. package/skills/wecomcli-meeting/SKILL.md +374 -0
  60. package/skills/wecomcli-meeting/references/meeting-cancel.md +115 -0
  61. package/skills/wecomcli-meeting/references/meeting-create.md +169 -0
  62. package/skills/wecomcli-meeting/references/meeting-list.md +228 -0
  63. package/skills/wecomcli-meeting/references/meeting-original-get.md +100 -0
  64. package/skills/wecomcli-meeting/references/meeting-search.md +175 -0
  65. package/skills/wecomcli-meeting/references/meeting-update.md +219 -0
  66. package/skills/wecomcli-message/SKILL.md +213 -0
  67. package/skills/wecomcli-preflight/SKILL.md +147 -0
  68. package/skills/wecomcli-sheet/SKILL.md +177 -0
  69. package/skills/wecomcli-sheet/references/sheet-contents-update.md +49 -0
  70. package/skills/wecomcli-sheet/references/sheet-ranges-get.md +47 -0
  71. package/skills/wecomcli-sheet/references/sheet-rows-append.md +47 -0
  72. package/skills/wecomcli-sheet/references/sheet-subsheets-add.md +28 -0
  73. package/skills/wecomcli-sheet/references/sheet-subsheets-delete.md +22 -0
  74. package/skills/wecomcli-smartpage/SKILL.md +172 -0
  75. package/skills/wecomcli-smartpage/references/data-driven-pages.md +50 -0
  76. package/skills/wecomcli-smartpage/references/formula/arraylist.md +369 -0
  77. package/skills/wecomcli-smartpage/references/formula/datetime.md +283 -0
  78. package/skills/wecomcli-smartpage/references/formula/logic.md +247 -0
  79. package/skills/wecomcli-smartpage/references/formula/math.md +362 -0
  80. package/skills/wecomcli-smartpage/references/formula/operators.md +246 -0
  81. package/skills/wecomcli-smartpage/references/formula/pageblock.md +76 -0
  82. package/skills/wecomcli-smartpage/references/formula/templates.md +410 -0
  83. package/skills/wecomcli-smartpage/references/formula/text.md +377 -0
  84. package/skills/wecomcli-smartpage/references/formula/user.md +22 -0
  85. package/skills/wecomcli-smartpage/references/formula-reference.md +192 -0
  86. package/skills/wecomcli-smartpage/references/mdx-syntax.md +739 -0
  87. package/skills/wecomcli-smartpage/references/smartpage-edit.md +519 -0
  88. package/skills/wecomcli-smartsheet/SKILL.md +153 -0
  89. package/skills/wecomcli-smartsheet/assets/templates/README.md +53 -0
  90. package/skills/wecomcli-smartsheet/assets/templates/ai_efficiency.md +709 -0
  91. package/skills/wecomcli-smartsheet/assets/templates/connect_to_app.md +380 -0
  92. package/skills/wecomcli-smartsheet/assets/templates/financial_accounting.md +369 -0
  93. package/skills/wecomcli-smartsheet/assets/templates/hr_and_administration.md +475 -0
  94. package/skills/wecomcli-smartsheet/assets/templates/ledger_records.md +156 -0
  95. package/skills/wecomcli-smartsheet/assets/templates/manufacturing.md +395 -0
  96. package/skills/wecomcli-smartsheet/assets/templates/marketing.md +186 -0
  97. package/skills/wecomcli-smartsheet/assets/templates/office_essentials.md +299 -0
  98. package/skills/wecomcli-smartsheet/assets/templates/personal_efficiency.md +70 -0
  99. package/skills/wecomcli-smartsheet/assets/templates/procurement_logistics.md +325 -0
  100. package/skills/wecomcli-smartsheet/assets/templates/project_management.md +564 -0
  101. package/skills/wecomcli-smartsheet/assets/templates/rd_process_customer.md +222 -0
  102. package/skills/wecomcli-smartsheet/assets/templates/rd_process_ops.md +105 -0
  103. package/skills/wecomcli-smartsheet/assets/templates/rd_process_project.md +109 -0
  104. package/skills/wecomcli-smartsheet/assets/templates/rd_process_research.md +92 -0
  105. package/skills/wecomcli-smartsheet/assets/templates/sales_and_operations.md +446 -0
  106. package/skills/wecomcli-smartsheet/assets/templates/store_management.md +431 -0
  107. package/skills/wecomcli-smartsheet/assets/templates/team_tasks.md +274 -0
  108. package/skills/wecomcli-smartsheet/assets/templates/wechat_customer.md +384 -0
  109. package/skills/wecomcli-smartsheet/assets/templates/work_report.md +100 -0
  110. package/skills/wecomcli-smartsheet/references/common.md +145 -0
  111. package/skills/wecomcli-smartsheet/references/smart-sheet-chart-types.md +95 -0
  112. package/skills/wecomcli-smartsheet/references/smart-sheet-edit.md +611 -0
  113. package/skills/wecomcli-smartsheet/references/smart-sheet-field-types.md +438 -0
  114. package/skills/wecomcli-smartsheet/references/smart-sheet-formula.md +845 -0
  115. package/skills/wecomcli-smartsheet/references/smart-sheet-read.md +411 -0
  116. package/skills/wecomcli-smartsheet/references/smart-sheet-record-values.md +203 -0
  117. package/skills/wecomcli-smartsheet/references/smart-sheet-view-types.md +356 -0
  118. package/skills/{wecom-smartsheet/references/webhook-examples.md → wecomcli-smartsheet/references/smart-sheet-webhook-examples.md} +23 -32
  119. package/skills/wecomcli-smartsheet/references/smart-sheet-webhook.md +169 -0
  120. package/skills/wecomcli-todo/SKILL.md +75 -0
  121. package/skills/wecomcli-todo/references/todo-create.md +139 -0
  122. package/skills/wecomcli-todo/references/todo-delete.md +65 -0
  123. package/skills/wecomcli-todo/references/todo-finish.md +74 -0
  124. package/skills/wecomcli-todo/references/todo-get.md +68 -0
  125. package/skills/wecomcli-todo/references/todo-list.md +135 -0
  126. package/skills/wecomcli-todo/references/todo-update.md +114 -0
  127. package/dist/src/mcp/index.d.ts +0 -6
  128. package/dist/src/mcp/index.js +0 -10
  129. package/dist/src/mcp/interceptors/biz-error.d.ts +0 -11
  130. package/dist/src/mcp/interceptors/biz-error.js +0 -69
  131. package/dist/src/mcp/interceptors/doc-auth-error.d.ts +0 -10
  132. package/dist/src/mcp/interceptors/doc-auth-error.js +0 -171
  133. package/dist/src/mcp/interceptors/index.d.ts +0 -35
  134. package/dist/src/mcp/interceptors/index.js +0 -70
  135. package/dist/src/mcp/interceptors/msg-media.d.ts +0 -11
  136. package/dist/src/mcp/interceptors/msg-media.js +0 -133
  137. package/dist/src/mcp/interceptors/smartpage-create.d.ts +0 -30
  138. package/dist/src/mcp/interceptors/smartpage-create.js +0 -132
  139. package/dist/src/mcp/interceptors/smartpage-export.d.ts +0 -17
  140. package/dist/src/mcp/interceptors/smartpage-export.js +0 -87
  141. package/dist/src/mcp/interceptors/smartsheet-upload.d.ts +0 -22
  142. package/dist/src/mcp/interceptors/smartsheet-upload.js +0 -263
  143. package/dist/src/mcp/interceptors/types.d.ts +0 -64
  144. package/dist/src/mcp/interceptors/types.js +0 -7
  145. package/dist/src/mcp/schema.d.ts +0 -11
  146. package/dist/src/mcp/schema.js +0 -99
  147. package/dist/src/mcp/tool.d.ts +0 -63
  148. package/dist/src/mcp/tool.js +0 -224
  149. package/dist/src/mcp/transport.d.ts +0 -94
  150. package/dist/src/mcp/transport.js +0 -549
  151. package/skills/wecom-contact/SKILL.md +0 -162
  152. package/skills/wecom-doc/SKILL.md +0 -127
  153. package/skills/wecom-meeting/SKILL.md +0 -441
  154. package/skills/wecom-meeting/references/example-full.md +0 -30
  155. package/skills/wecom-meeting/references/example-reminder.md +0 -46
  156. package/skills/wecom-meeting/references/example-security.md +0 -22
  157. package/skills/wecom-meeting/references/response-get-meeting-info.md +0 -148
  158. package/skills/wecom-msg/SKILL.md +0 -157
  159. package/skills/wecom-msg/references/api-get-messages.md +0 -93
  160. package/skills/wecom-msg/references/api-get-msg-chat-list.md +0 -58
  161. package/skills/wecom-msg/references/api-get-msg-media.md +0 -44
  162. package/skills/wecom-msg/references/api-send-message.md +0 -39
  163. package/skills/wecom-preflight/SKILL.md +0 -141
  164. package/skills/wecom-schedule/SKILL.md +0 -161
  165. package/skills/wecom-schedule/references/api-check-availability.md +0 -56
  166. package/skills/wecom-schedule/references/api-create-schedule.md +0 -38
  167. package/skills/wecom-schedule/references/api-get-schedule-detail.md +0 -81
  168. package/skills/wecom-schedule/references/api-update-schedule.md +0 -32
  169. package/skills/wecom-schedule/references/ref-reminders.md +0 -24
  170. package/skills/wecom-sheet/SKILL.md +0 -131
  171. package/skills/wecom-sheet/references/sheet-add-sub.md +0 -54
  172. package/skills/wecom-sheet/references/sheet-append-data.md +0 -64
  173. package/skills/wecom-sheet/references/sheet-delete-sub.md +0 -38
  174. package/skills/wecom-sheet/references/sheet-update-range-data.md +0 -95
  175. package/skills/wecom-smartpage/SKILL.md +0 -119
  176. package/skills/wecom-smartpage/references/smartpage-create.md +0 -130
  177. package/skills/wecom-smartpage/references/smartpage-export.md +0 -168
  178. package/skills/wecom-smartsheet/SKILL.md +0 -241
  179. package/skills/wecom-smartsheet/references/smartsheet-cell-value-formats.md +0 -163
  180. package/skills/wecom-smartsheet/references/smartsheet-field-types.md +0 -44
  181. package/skills/wecom-smartsheet/references/smartsheet-get-records.md +0 -187
  182. package/skills/wecom-smartsheet/references/webhook-fallback.md +0 -183
  183. package/skills/wecom-todo/SKILL.md +0 -392
  184. package/skills/wecom-todo/examples/workflows.md +0 -163
@@ -0,0 +1,43 @@
1
+ export class BoundedOutputCollector {
2
+ maxBytes;
3
+ keep;
4
+ buffer = Buffer.alloc(0);
5
+ originalBytes = 0;
6
+ constructor(maxBytes, keep = "head") {
7
+ this.maxBytes = maxBytes;
8
+ this.keep = keep;
9
+ }
10
+ append(chunk) {
11
+ const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
12
+ this.originalBytes += data.length;
13
+ if (this.maxBytes <= 0)
14
+ return;
15
+ if (this.keep === "head") {
16
+ const remaining = this.maxBytes - this.buffer.length;
17
+ if (remaining > 0)
18
+ this.buffer = Buffer.concat([this.buffer, data.subarray(0, remaining)]);
19
+ return;
20
+ }
21
+ if (data.length >= this.maxBytes) {
22
+ this.buffer = Buffer.from(data.subarray(data.length - this.maxBytes));
23
+ return;
24
+ }
25
+ const combined = Buffer.concat([this.buffer, data]);
26
+ this.buffer =
27
+ combined.length <= this.maxBytes
28
+ ? combined
29
+ : Buffer.from(combined.subarray(combined.length - this.maxBytes));
30
+ }
31
+ result() {
32
+ let text = this.buffer.toString("utf8");
33
+ if (this.keep === "head")
34
+ text = text.replace(/\uFFFD+$/, "");
35
+ else
36
+ text = text.replace(/^\uFFFD+/, "");
37
+ return {
38
+ text,
39
+ originalBytes: this.originalBytes,
40
+ truncated: this.originalBytes > this.buffer.length,
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * wecom-cli — 把 @wecom/cli 包装成 openclaw tool
3
+ *
4
+ * 为什么包装成 tool 而不是让 agent 用 exec 直接跑:
5
+ * 1. 企微会话常见的 `messaging` profile 里没有 exec 工具
6
+ * 2. **更关键**:exec 无法可靠注入 per-bot 的 WECOM_CLI_CONFIG_DIR。
7
+ * 模型漏带一次 env 就会落到默认目录,拿 A 企业的凭据调 B 企业的接口——
8
+ * 这是数据越权,不是功能降级。tool 让配置目录完全由服务端决定。
9
+ * 3. 插件私有 node_modules/.bin 不在 PATH,PATH 上反而可能是用户全局装的 cli
10
+ */
11
+ import * as path from "node:path";
12
+ export interface CreateWeComCliToolOptions {
13
+ /** 当前会话对应的账号 ID(来自 ctx.agentAccountId) */
14
+ accountId?: string;
15
+ }
16
+ export declare function prepareCliArguments(params: unknown): unknown;
17
+ export declare function createWeComCliTool(options?: CreateWeComCliToolOptions): {
18
+ name: string;
19
+ label: string;
20
+ description: string;
21
+ parameters: {
22
+ type: "object";
23
+ properties: {
24
+ args: {
25
+ type: "array";
26
+ items: {
27
+ type: "string";
28
+ };
29
+ description: string;
30
+ };
31
+ };
32
+ required: string[];
33
+ additionalProperties: boolean;
34
+ };
35
+ prepareArguments: typeof prepareCliArguments;
36
+ execute(_toolCallId: string, params: unknown): Promise<{
37
+ content: {
38
+ type: "text";
39
+ text: string;
40
+ }[];
41
+ details: unknown;
42
+ }>;
43
+ };
44
+ /** 仅用于测试 */
45
+ export declare function resetCliToolState(): void;
46
+ export { path as _path };
@@ -0,0 +1,363 @@
1
+ /**
2
+ * wecom-cli — 把 @wecom/cli 包装成 openclaw tool
3
+ *
4
+ * 为什么包装成 tool 而不是让 agent 用 exec 直接跑:
5
+ * 1. 企微会话常见的 `messaging` profile 里没有 exec 工具
6
+ * 2. **更关键**:exec 无法可靠注入 per-bot 的 WECOM_CLI_CONFIG_DIR。
7
+ * 模型漏带一次 env 就会落到默认目录,拿 A 企业的凭据调 B 企业的接口——
8
+ * 这是数据越权,不是功能降级。tool 让配置目录完全由服务端决定。
9
+ * 3. 插件私有 node_modules/.bin 不在 PATH,PATH 上反而可能是用户全局装的 cli
10
+ */
11
+ import * as path from "node:path";
12
+ import { spawn } from "./process-launcher.js";
13
+ import { getWeComRuntime } from "../runtime.js";
14
+ import { hasMultiAccounts, resolveWeComAccountMulti } from "../accounts.js";
15
+ import { resolveStateDir } from "../state-dir-resolve.js";
16
+ import { CLI_ENV_PASSTHROUGH, CLI_KILL_FORCE_WAIT_MS, CLI_KILL_GRACE_MS, CLI_LOG, CLI_MAX_OUTPUT_BYTES, CLI_RESIGN_CODES, CLI_TIMEOUT_MS, CLI_TOOL_NAME, } from "./const.js";
17
+ import { CliArgvError, normalizeArgs } from "./argv.js";
18
+ import { CliAuthError, ensureSynced } from "./credentials.js";
19
+ import { locateCliBinary } from "./locate.js";
20
+ import { BoundedOutputCollector } from "./process-output.js";
21
+ // ============================================================================
22
+ // 响应构造
23
+ // ============================================================================
24
+ const textResult = (data) => {
25
+ const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
26
+ return { content: [{ type: "text", text }], details: data };
27
+ };
28
+ const errorResult = (message, extra) => textResult({ error: message, ...extra });
29
+ /**
30
+ * 解析当前会话对应的 bot 凭据。
31
+ *
32
+ * ⚠️刻意**不继承** resolveCurrentAccountId() 的"回退默认账号"行为:
33
+ * 多账号场景下上下文丢失时回退,等于拿 A 企业的凭据调 B 企业的接口。
34
+ * 宁可失败也不能串号—— 只有单账号模式才允许回退。
35
+ */
36
+ function resolveBot(accountId) {
37
+ let cfg;
38
+ try {
39
+ cfg = getWeComRuntime().config.loadConfig();
40
+ }
41
+ catch {
42
+ throw new CliAuthError("插件运行时未初始化,无法读取 openclaw 配置");
43
+ }
44
+ const multi = hasMultiAccounts(cfg);
45
+ const id = accountId?.trim();
46
+ if (!id && multi) {
47
+ throw new CliAuthError("当前会话缺少账号上下文(agentAccountId),而配置中存在多个企业微信账号。" +
48
+ "为避免误用其他企业的凭据,本次调用已被拒绝。请在有明确会话上下文的场景下使用。");
49
+ }
50
+ const resolved = resolveWeComAccountMulti({ cfg, accountId: id ?? null });
51
+ const botId = resolved.botId?.trim();
52
+ const secret = resolved.secret?.trim();
53
+ if (!botId || !secret) {
54
+ throw new CliAuthError(`企业微信账号 "${resolved.accountId}" 未配置 botId / secret,` +
55
+ "请检查 openclaw 配置 `channels.wecom`");
56
+ }
57
+ // 端点覆盖(仅联调用;在未启用 custom-endpoint feature 的正式包上不生效)
58
+ const rawCliCfg = resolved.config?.cli;
59
+ const env = {};
60
+ for (const key of CLI_ENV_PASSTHROUGH) {
61
+ const v = rawCliCfg?.env?.[key];
62
+ if (typeof v === "string" && v.trim())
63
+ env[key] = v.trim();
64
+ }
65
+ return { botId, secret, accountId: resolved.accountId, env };
66
+ }
67
+ function resolveBinPath(accountId) {
68
+ let explicit;
69
+ try {
70
+ const cfg = getWeComRuntime().config.loadConfig();
71
+ const resolved = resolveWeComAccountMulti({ cfg, accountId: accountId ?? null });
72
+ explicit = resolved.config?.cli?.binPath;
73
+ }
74
+ catch {
75
+ /* 配置不可读时走默认寻址 */
76
+ }
77
+ const located = locateCliBinary(explicit);
78
+ if (located.ok)
79
+ return { binPath: located.binPath, source: located.source };
80
+ throw new CliAuthError("未找到 wecom-cli 可执行文件,插件安装可能不完整。\n" +
81
+ `尝试过的位置:\n ${located.tried.join("\n ")}\n` +
82
+ "可通过 openclaw 配置 `channels.wecom.cli.binPath` 显式指定二进制路径。");
83
+ }
84
+ // ============================================================================
85
+ // spawn(按 botId 串行化)
86
+ // ============================================================================
87
+ /**
88
+ * 同一 botId 的调用串行执行。
89
+ *
90
+ * 原因:cli 的 refresh_lock 只是**进程内**锁,credentials.enc 虽然是原子写但
91
+ * 没有跨进程文件锁。并发进程同时遇到 853004 会各自去换token(可能撞 45009)
92
+ * 并互相覆盖。企微场景 QPS 极低,串行化几乎无感知。
93
+ */
94
+ const queues = new Map();
95
+ function serialize(botId, task) {
96
+ const prev = queues.get(botId) ?? Promise.resolve();
97
+ const next = prev.then(task, task);
98
+ queues.set(botId, next.catch(() => undefined));
99
+ return next;
100
+ }
101
+ function runCli(binPath, argv, configDir, env) {
102
+ return new Promise((resolve, reject) => {
103
+ const started = Date.now();
104
+ const child = spawn(binPath, argv, {
105
+ shell: false,
106
+ // cwd 固定到 state 目录:cli 启动时会读cwd 下的 .env,避免被无关目录污染;
107
+ // 同时 cli 用 cwd 解析相对路径,因此 skills 中一律要求绝对路径
108
+ cwd: resolveStateDir(),
109
+ env: {
110
+ PATH: process.env.PATH ?? "",
111
+ HOME: process.env.HOME ?? "",
112
+ ...(process.platform === "win32"
113
+ ? { USERPROFILE: process.env.USERPROFILE ?? "", SystemRoot: process.env.SystemRoot ?? "" }
114
+ : {}),
115
+ WECOM_CLI_CONFIG_DIR: configDir,
116
+ // 压掉 cli 的 info/debug 噪音,避免 stderr 淹没真实错误
117
+ WECOM_CLI_LOG_LEVEL: "warn",
118
+ ...env,
119
+ },
120
+ stdio: ["ignore", "pipe", "pipe"],
121
+ });
122
+ const stdout = new BoundedOutputCollector(CLI_MAX_OUTPUT_BYTES, "head");
123
+ const stderr = new BoundedOutputCollector(CLI_MAX_OUTPUT_BYTES, "tail");
124
+ let timedOut = false;
125
+ let settled = false;
126
+ let graceTimer;
127
+ let forceWaitTimer;
128
+ child.stdout.on("data", (chunk) => stdout.append(chunk));
129
+ child.stderr.on("data", (chunk) => stderr.append(chunk));
130
+ const clearTimers = () => {
131
+ clearTimeout(killTimer);
132
+ if (graceTimer)
133
+ clearTimeout(graceTimer);
134
+ if (forceWaitTimer)
135
+ clearTimeout(forceWaitTimer);
136
+ };
137
+ const finish = (status) => {
138
+ if (settled)
139
+ return;
140
+ settled = true;
141
+ clearTimers();
142
+ const stdoutResult = stdout.result();
143
+ resolve({
144
+ status,
145
+ stdout: stdoutResult.text,
146
+ stderr: stderr.result().text,
147
+ stdoutBytes: stdoutResult.originalBytes,
148
+ stdoutTruncated: stdoutResult.truncated,
149
+ ms: Date.now() - started,
150
+ timedOut,
151
+ });
152
+ };
153
+ const killTimer = setTimeout(() => {
154
+ timedOut = true;
155
+ if (child.exitCode === null && child.signalCode === null)
156
+ child.kill("SIGTERM");
157
+ graceTimer = setTimeout(() => {
158
+ if (child.exitCode === null && child.signalCode === null)
159
+ child.kill("SIGKILL");
160
+ forceWaitTimer = setTimeout(() => {
161
+ child.stdout.destroy();
162
+ child.stderr.destroy();
163
+ finish(child.exitCode);
164
+ }, CLI_KILL_FORCE_WAIT_MS);
165
+ }, CLI_KILL_GRACE_MS);
166
+ }, CLI_TIMEOUT_MS);
167
+ child.on("error", (err) => {
168
+ if (settled)
169
+ return;
170
+ settled = true;
171
+ clearTimers();
172
+ reject(err);
173
+ });
174
+ child.on("close", (status) => finish(status));
175
+ });
176
+ }
177
+ // ============================================================================
178
+ // 结果分类
179
+ // ============================================================================
180
+ function parseErrorPayload(stdout) {
181
+ try {
182
+ const parsed = JSON.parse(stdout);
183
+ return parsed?.error ?? null;
184
+ }
185
+ catch {
186
+ return null;
187
+ }
188
+ }
189
+ /** 从 cli 的裸企微响应里提取 errcode(如 token 过期时会直接透出 {errcode,errmsg}) */
190
+ function parseErrcode(stdout) {
191
+ try {
192
+ const parsed = JSON.parse(stdout);
193
+ return typeof parsed?.errcode === "number" ? parsed.errcode : null;
194
+ }
195
+ catch {
196
+ return null;
197
+ }
198
+ }
199
+ /**
200
+ * 改写 cli 的错误文案。
201
+ *
202
+ * cli 会提示用户去跑 `wecom auth init`,但手工执行会绕过 openclaw 配置、
203
+ * 把凭据写到默认目录(而本会话读的是插件按 botId+secret 分配的目录),
204
+ * 且需要用户自己拿到 secret。凭据由插件统一管理,必须改写为引导改配置。
205
+ */
206
+ function rewriteMessage(message) {
207
+ if (/auth\s+init/i.test(message)) {
208
+ return "企业微信凭据不可用。请检查 openclaw 配置 `channels.wecom` 中的 botId / secret(请勿手动执行 wecom-cli auth init,凭据由插件统一管理)。";
209
+ }
210
+ return message;
211
+ }
212
+ // ============================================================================
213
+ // 工具定义
214
+ // ============================================================================
215
+ const DESCRIPTION = [
216
+ "执行企业微信命令行(wecom-cli),用于访问通讯录、文档、会议、日程、待办、智能表格等企业微信能力。",
217
+ "",
218
+ "参数 `args` 是命令行中 `wecom-cli ` 之后的字符串数组。",
219
+ "例如:[\"contact\",\"users\",\"search\",\"--json\",\"{\\\"keywords\\\":[\\\"张三\\\"]}\"]",
220
+ "参数值本身含引号、空格或特殊字符时,仍作为一个完整数组元素传入。",
221
+ "",
222
+ "重要约束:",
223
+ " - 相关 skill 文档中给出的命令,一律通过本 tool 执行;",
224
+ " **禁止**使用 exec / bash / shell / npx 或全局命令直接运行 wecom-cli,那会绕过当前会话的企业凭据隔离。",
225
+ " - 本 tool 内部自行寻址并启动 wecom-cli 二进制是正常实现;禁止的是 Agent 主动改用通用执行工具。",
226
+ " - 本 tool 调用失败、不可见、权限不足或提示授权时,不得降级到 exec,也不得手动执行 auth init;应直接报告工具权限或 `channels.wecom` 配置问题。",
227
+ " - 不要在 args 中写 `wecom-cli` 前缀,也不要拼接 shell 命令(管道、重定向、&& 等)。",
228
+ " - 不要传入任何 WECOM_CLI_* 环境变量或 --config-dir:运行环境由插件按当前会话的机器人自动注入。",
229
+ " - 涉及本地文件的参数请使用绝对路径。",
230
+ " - 想了解某个方法的参数结构时,可在命令末尾加 `--schema` 或 `--doc`。",
231
+ ].join("\n");
232
+ export function prepareCliArguments(params) {
233
+ if (!params || typeof params !== "object" || Array.isArray(params))
234
+ return params;
235
+ const value = params;
236
+ if (typeof value.args !== "string")
237
+ return params;
238
+ return { ...value, args: normalizeArgs(value.args) };
239
+ }
240
+ export function createWeComCliTool(options = {}) {
241
+ const accountId = options.accountId?.trim() || undefined;
242
+ return {
243
+ name: CLI_TOOL_NAME,
244
+ label: "企业微信命令行",
245
+ description: DESCRIPTION,
246
+ parameters: {
247
+ type: "object",
248
+ properties: {
249
+ args: {
250
+ type: "array",
251
+ items: { type: "string" },
252
+ description: "命令行中 `wecom-cli ` 之后的字符串数组," +
253
+ "例如 [\"contact\",\"users\",\"search\",\"--json\",\"{\\\"keywords\\\":[\\\"张三\\\"]}\"]",
254
+ },
255
+ },
256
+ required: ["args"],
257
+ additionalProperties: false,
258
+ },
259
+ prepareArguments: prepareCliArguments,
260
+ async execute(_toolCallId, params) {
261
+ const raw = params?.args;
262
+ // 1. 入参归一化 + 安全校验
263
+ let argv;
264
+ try {
265
+ argv = normalizeArgs(raw);
266
+ }
267
+ catch (err) {
268
+ if (err instanceof CliArgvError)
269
+ return errorResult(err.message);
270
+ throw err;
271
+ }
272
+ // 2. 解析当前会话的 bot 与二进制
273
+ let bot;
274
+ let binPath;
275
+ let binSource;
276
+ try {
277
+ bot = resolveBot(accountId);
278
+ ({ binPath, source: binSource } = resolveBinPath(accountId));
279
+ }
280
+ catch (err) {
281
+ return errorResult(err instanceof Error ? err.message : String(err));
282
+ }
283
+ const cmdForLog = argv.join(" ");
284
+ return serialize(bot.botId, async () => {
285
+ const attempt = async (force) => {
286
+ const dir = await ensureSynced({
287
+ binPath,
288
+ botId: bot.botId,
289
+ secret: bot.secret,
290
+ env: bot.env,
291
+ force,
292
+ });
293
+ return runCli(binPath, argv, dir, bot.env);
294
+ };
295
+ try {
296
+ let out = await attempt(false);
297
+ // 3. 凭据类错误 → 强制重签一次后重试(受 5 分钟熔断约束)
298
+ //必要性:cli 的 853004 自动刷新在当前版本会因缺少请求头被 48002 拒绝,
299
+ // 插件不能完全依赖 cli 自愈。
300
+ const code = parseErrorPayload(out.stdout)?.code ?? parseErrcode(out.stdout);
301
+ if (out.status !== 0 && code != null && CLI_RESIGN_CODES.has(code)) {
302
+ console.warn(`${CLI_LOG} 凭据失效 (code=${code}),强制重签后重试:${cmdForLog}`);
303
+ try {
304
+ out = await attempt(true);
305
+ }
306
+ catch (err) {
307
+ if (err instanceof CliAuthError)
308
+ return errorResult(err.message, { errcode: err.errcode });
309
+ throw err;
310
+ }
311
+ }
312
+ // 4. 结果整理
313
+ if (out.timedOut) {
314
+ return errorResult(`命令执行超时(${CLI_TIMEOUT_MS / 1000}s):wecom-cli ${cmdForLog}`, { stderr: out.stderr.slice(-2048) || undefined });
315
+ }
316
+ // exit2 = clap 参数/子命令错误,错误文本在 stderr,且常意味着 skills 与 cli 版本不匹配
317
+ if (out.status === 2) {
318
+ console.warn(`${CLI_LOG} 命令不存在或参数非法 (exit 2) via=${binSource}:${cmdForLog}`);
319
+ return errorResult(`命令不存在或参数非法:wecom-cli ${cmdForLog}\n${out.stderr.trim().slice(0, 1500)}`, { hint: "该命令在当前 wecom-cli 版本中不可用,技能文档可能与 CLI 版本不一致。可用 `--help` 查看可用子命令。" });
320
+ }
321
+ const errPayload = parseErrorPayload(out.stdout);
322
+ if (out.status !== 0 || errPayload) {
323
+ const message = rewriteMessage(errPayload?.message ?? (out.stderr.trim() || `命令执行失败(exit ${out.status})`));
324
+ console.warn(`${CLI_LOG} 调用失败 (${out.ms}ms, exit=${out.status}) ${cmdForLog} → ` +
325
+ JSON.stringify(errPayload ?? out.stderr.slice(-500)));
326
+ return errorResult(message, {
327
+ errcode: errPayload?.code,
328
+ endpoint: errPayload?.endpoint,
329
+ httpStatus: errPayload?.status,
330
+ });
331
+ }
332
+ const text = out.stdout.trim();
333
+ console.log(`${CLI_LOG} ok (${out.ms}ms, ${out.stdoutBytes}B${out.stdoutTruncated ? " 已截断" : ""}) ` +
334
+ `via=${binSource} ${cmdForLog}`);
335
+ if (out.stdoutTruncated) {
336
+ return textResult(`${text}\n\n[输出过大已截断:原始 ${out.stdoutBytes} 字节,上限 ${CLI_MAX_OUTPUT_BYTES} 字节。` +
337
+ "请缩小查询范围(例如使用更精确的关键词、加上分页或筛选参数)后重试。]");
338
+ }
339
+ return textResult(text);
340
+ }
341
+ catch (err) {
342
+ if (err instanceof CliAuthError)
343
+ return errorResult(err.message, { errcode: err.errcode });
344
+ const e = err;
345
+ if (e?.code === "ENOENT") {
346
+ return errorResult(`wecom-cli 可执行文件不存在:${binPath}。插件安装可能不完整,请重新安装,` +
347
+ "或通过 `channels.wecom.cli.binPath` 指定路径。");
348
+ }
349
+ if (e?.code === "EACCES") {
350
+ return errorResult(`wecom-cli 没有执行权限:${binPath}。请执行 chmod +x 或重新安装插件。`);
351
+ }
352
+ console.error(`${CLI_LOG} 异常 ${cmdForLog} → ${e?.message ?? String(err)}`);
353
+ return errorResult(e?.message ?? String(err));
354
+ }
355
+ });
356
+ },
357
+ };
358
+ }
359
+ /** 仅用于测试 */
360
+ export function resetCliToolState() {
361
+ queues.clear();
362
+ }
363
+ export { path as _path };
@@ -46,14 +46,6 @@ export declare const THINKING_MESSAGE = "<think></think>";
46
46
  export declare const MEDIA_IMAGE_PLACEHOLDER = "<media:image>";
47
47
  /** 仅包含文件时的消息占位符 */
48
48
  export declare const MEDIA_DOCUMENT_PLACEHOLDER = "<media:document>";
49
- /** 获取 MCP 配置的 WebSocket 命令 */
50
- export declare const MCP_GET_CONFIG_CMD = "aibot_get_mcp_config";
51
- /** 发送业务消息的 WebSocket 命令(如文档授权卡片) */
52
- export declare const AIBOT_SEND_BIZ_MSG_CMD = "aibot_send_biz_msg";
53
- /** 业务消息超时时间(毫秒) */
54
- export declare const BIZ_MSG_SEND_TIMEOUT_MS = 10000;
55
- /** MCP 配置拉取超时时间(毫秒) */
56
- export declare const MCP_CONFIG_FETCH_TIMEOUT_MS = 15000;
57
49
  /** 默认媒体大小上限(MB) */
58
50
  export declare const DEFAULT_MEDIA_MAX_MB = 5;
59
51
  /** 文本分块大小上限 */
package/dist/src/const.js CHANGED
@@ -59,20 +59,6 @@ export const MEDIA_DOCUMENT_PLACEHOLDER = "<media:document>";
59
59
  // ============================================================================
60
60
  // 默认值
61
61
  // ============================================================================
62
- // ============================================================================
63
- // MCP 配置
64
- // ============================================================================
65
- /** 获取 MCP 配置的 WebSocket 命令 */
66
- export const MCP_GET_CONFIG_CMD = "aibot_get_mcp_config";
67
- /** 发送业务消息的 WebSocket 命令(如文档授权卡片) */
68
- export const AIBOT_SEND_BIZ_MSG_CMD = "aibot_send_biz_msg";
69
- /** 业务消息超时时间(毫秒) */
70
- export const BIZ_MSG_SEND_TIMEOUT_MS = 10_000;
71
- /** MCP 配置拉取超时时间(毫秒) */
72
- export const MCP_CONFIG_FETCH_TIMEOUT_MS = 15_000;
73
- // ============================================================================
74
- // 默认值
75
- // ============================================================================
76
62
  /** 默认媒体大小上限(MB) */
77
63
  export const DEFAULT_MEDIA_MAX_MB = 5;
78
64
  /** 文本分块大小上限 */
@@ -29,7 +29,7 @@ import { sendWeComReply, StreamExpiredError } from "./message-sender.js";
29
29
  import { enqueueWeComChatTask } from "./chat-queue.js";
30
30
  import { getDefaultMediaLocalRoots, resolveStateDir } from "./openclaw-compat.js";
31
31
  import { getWeComRuntime } from "./runtime.js";
32
- import { setWeComWebSocket, setMessageState, deleteMessageState, setReqIdForChat, setSessionChatInfo, warmupReqIdStore, startMessageStateCleanup, stopMessageStateCleanup, cleanupAccount, } from "./state-manager.js";
32
+ import { setWeComWebSocket, setMessageState, deleteMessageState, setReqIdForChat, warmupReqIdStore, startMessageStateCleanup, stopMessageStateCleanup, cleanupAccount, } from "./state-manager.js";
33
33
  import { updateTemplateCardOnEvent, processTemplateCardsIfNeeded, } from "./template-card-manager.js";
34
34
  import { maskTemplateCardBlocks } from "./template-card-parser.js";
35
35
  import { PLUGIN_VERSION } from "./version.js";
@@ -628,17 +628,6 @@ async function processWeComMessageNow(entry) {
628
628
  // }
629
629
  // Step 7: 构建上下文并路由到核心处理流程(带整体超时保护)
630
630
  const { ctxPayload, route, storePath, chatId: resolvedChatId, chatType, } = buildMessageContext(frame, account, config, text, mediaList, quoteContent, runtime);
631
- // 以 sessionKey 为键记录「原始大小写」的 chatId 与 chatType,
632
- // 供 MCP 工具工厂(index.ts:registerTool)在构造工具闭包时取回,
633
- // 进而传递给需要原始 chatId 的拦截器(如 doc-auth-error 发送 biz_msg)。
634
- //
635
- // 注意:不要使用 parseSessionKeyChat 反解 sessionKey —— OpenClaw core
636
- // 构建 sessionKey 时会将 peer.id 强制小写化,会导致企业微信
637
- // aibot_send_biz_msg 报 errcode=93006 invalid chatid。
638
- setSessionChatInfo(route.sessionKey, {
639
- chatId: resolvedChatId,
640
- chatType: chatType === "group" ? "group" : "single",
641
- });
642
631
  // runtime.log?.(`[plugin -> openclaw] body=${text}, mediaPaths=${JSON.stringify(mediaList.map(m => m.path))}${quoteContent ? `, quote=${quoteContent}` : ''}`);
643
632
  try {
644
633
  await routeAndDispatchMessage({
@@ -70,25 +70,6 @@ export declare function warmupReqIdStore(accountId?: string, log?: (...args: unk
70
70
  * 注意:由于移除了磁盘存储,此函数现在是无操作
71
71
  */
72
72
  export declare function flushReqIdStore(accountId?: string): Promise<void>;
73
- /** 会话 chat 信息 */
74
- export interface SessionChatInfo {
75
- /** 原始大小写的 chatId(群 ID 或用户 ID) */
76
- chatId: string;
77
- /** 聊天类型:single(单聊)或 group(群聊) */
78
- chatType: "single" | "group";
79
- }
80
- /**
81
- * 记录 sessionKey 对应的原始会话信息(由 monitor.ts 在消息入站时调用)
82
- */
83
- export declare function setSessionChatInfo(sessionKey: string, info: SessionChatInfo): void;
84
- /**
85
- * 获取 sessionKey 对应的原始会话信息(由 registerTool 闭包调用)
86
- */
87
- export declare function getSessionChatInfo(sessionKey: string | undefined): SessionChatInfo | undefined;
88
- /**
89
- * 删除 sessionKey 对应的会话信息(会话结束时可选调用)
90
- */
91
- export declare function deleteSessionChatInfo(sessionKey: string): void;
92
73
  /**
93
74
  * 清理指定账户的所有资源
94
75
  */
@@ -201,41 +201,8 @@ export async function warmupReqIdStore(accountId = "default", log) {
201
201
  export async function flushReqIdStore(accountId = "default") {
202
202
  // 由于移除了磁盘存储,不再需要刷写操作
203
203
  }
204
- /** sessionKey → SessionChatInfo 映射 */
205
- const sessionChatInfoMap = new Map();
206
- /** 容量上限,超出按插入顺序淘汰最旧项,避免长时间运行内存增长 */
207
- const SESSION_CHAT_INFO_MAX_SIZE = 5000;
208
- /**
209
- * 记录 sessionKey 对应的原始会话信息(由 monitor.ts 在消息入站时调用)
210
- */
211
- export function setSessionChatInfo(sessionKey, info) {
212
- if (!sessionKey)
213
- return;
214
- // 容量控制:超限时淘汰最早插入的条目(Map 保留插入顺序)
215
- if (sessionChatInfoMap.size >= SESSION_CHAT_INFO_MAX_SIZE && !sessionChatInfoMap.has(sessionKey)) {
216
- const oldestKey = sessionChatInfoMap.keys().next().value;
217
- if (oldestKey !== undefined) {
218
- sessionChatInfoMap.delete(oldestKey);
219
- }
220
- }
221
- sessionChatInfoMap.set(sessionKey, info);
222
- }
223
- /**
224
- * 获取 sessionKey 对应的原始会话信息(由 registerTool 闭包调用)
225
- */
226
- export function getSessionChatInfo(sessionKey) {
227
- if (!sessionKey)
228
- return undefined;
229
- return sessionChatInfoMap.get(sessionKey);
230
- }
231
- /**
232
- * 删除 sessionKey 对应的会话信息(会话结束时可选调用)
233
- */
234
- export function deleteSessionChatInfo(sessionKey) {
235
- sessionChatInfoMap.delete(sessionKey);
236
- }
237
204
  // ============================================================================
238
- // 全局 cleanup 原始位置
205
+ // 全局 cleanup(断开连接时释放所有资源)
239
206
  // ============================================================================
240
207
  /**
241
208
  * 清理指定账户的所有资源
@@ -40,6 +40,8 @@ export interface WeComConfig {
40
40
  media?: WecomMediaConfig;
41
41
  /** 动态 Agent 配置 */
42
42
  dynamicAgents?: WecomDynamicAgentsConfig;
43
+ /** wecom-cli tool 配置 */
44
+ cli?: WecomCliConfig;
43
45
  /** 连接模式:webhook | websocket(默认 websocket) */
44
46
  connectionMode?: "webhook" | "websocket";
45
47
  /** Webhook 验证 token */
@@ -53,6 +55,31 @@ export interface WeComConfig {
53
55
  /** 流式占位符提示内容 */
54
56
  streamPlaceholderContent?: string;
55
57
  }
58
+ /**
59
+ * wecom-cli tool 配置
60
+ */
61
+ export interface WecomCliConfig {
62
+ /**
63
+ * 显式指定 wecom-cli 二进制路径。
64
+ * 不配置时按 `@tencent/wecom-cli-<platform>` → `@wecom/cli-<platform>` 顺序自动寻址。
65
+ * 联调期可指向本地 cargo 构建产物。
66
+ */
67
+ binPath?: string;
68
+ /**
69
+ * 透传给 wecom-cli 的环境变量(仅接受下列三个键)。
70
+ *
71
+ * 用于把cli 指向测试环境。注意这三个变量在 cli 侧受 `custom-endpoint`
72
+ * 编译特性门控(默认关闭),未启用该特性的正式包上不生效。
73
+ *
74
+ * ⚠️ WECOM_CLI_AUTH_ENDPOINT 必须与插件换取凭据所用端点保持一致,
75
+ * 否则登录态与业务调用会分属不同环境。
76
+ */
77
+ env?: {
78
+ WECOM_CLI_BASE_URL?: string;
79
+ WECOM_CLI_AUTH_ENDPOINT?: string;
80
+ WECOM_CLI_ADDITIONAL_HEADERS?: string;
81
+ };
82
+ }
56
83
  /**
57
84
  * 单个企业微信账号的配置类型(用于 accounts 字段下的每个账号)。
58
85
  * 与 WeComConfig 字段完全一致,账号级字段会覆盖顶层同名字段。
@@ -5,7 +5,7 @@
5
5
  ],
6
6
  "contracts": {
7
7
  "tools": [
8
- "wecom_mcp"
8
+ "wecom-cli"
9
9
  ]
10
10
  },
11
11
  "skills": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wecom/wecom-openclaw-plugin",
3
- "version": "2026.7.2",
3
+ "version": "2026.8.17-beta.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -65,10 +65,10 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@wecom/aibot-node-sdk": "^1.0.6",
68
+ "@wecom/cli": "^1.0.0",
68
69
  "fast-xml-parser": "^5.5.9",
69
70
  "file-type": "^21.3.0",
70
- "undici": "^7.24.6",
71
- "zod": "^4.3.6"
71
+ "undici": "^7.24.6"
72
72
  },
73
73
  "peerDependencies": {
74
74
  "openclaw": ">=2026.3.28"
@@ -10,6 +10,8 @@ metadata: {"openclaw":{"emoji":"📤","requires":{"config":["channels.wecom"]}}}
10
10
 
11
11
  通过 `MEDIA:` 指令将本地文件发送给用户。支持的文件类型为:图片、视频、语音、其它文件等。系统会自动识别文件类型,并以对应的消息格式(图片消息、视频消息、语音消息或文件消息)发送。
12
12
 
13
+ > **职责边界**:这是企业微信 Channel 的**当前会话文件投递能力**。需要把本轮生成或已有的本地文件交付给当前用户时使用 `MEDIA:`;不要调用 `wecom-cli message` 代替本轮回复的文件投递。`wecom-cli message` 适用于向另一个已选定的企微会话主动发送消息,不属于本技能。
14
+
13
15
  ## 触发条件
14
16
 
15
17
  - 用户要求发送、展示或分享图片、视频、语音或文件