@wecom/wecom-openclaw-plugin 2026.7.2-beta.1 → 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.
- package/README.md +33 -4
- package/dist/index.js +14 -21
- package/dist/src/cli/argv.d.ts +36 -0
- package/dist/src/cli/argv.js +155 -0
- package/dist/src/cli/const.d.ts +94 -0
- package/dist/src/cli/const.js +107 -0
- package/dist/src/cli/credentials.d.ts +54 -0
- package/dist/src/cli/credentials.js +342 -0
- package/dist/src/cli/index.d.ts +5 -0
- package/dist/src/cli/index.js +5 -0
- package/dist/src/cli/legacy-tool-warning.d.ts +12 -0
- package/dist/src/cli/legacy-tool-warning.js +20 -0
- package/dist/src/cli/locate.d.ts +32 -0
- package/dist/src/cli/locate.js +196 -0
- package/dist/src/cli/process-launcher.d.ts +1 -0
- package/dist/src/cli/process-launcher.js +1 -0
- package/dist/src/cli/process-output.d.ts +14 -0
- package/dist/src/cli/process-output.js +43 -0
- package/dist/src/cli/tool.d.ts +46 -0
- package/dist/src/cli/tool.js +363 -0
- package/dist/src/const.d.ts +0 -8
- package/dist/src/const.js +0 -14
- package/dist/src/monitor.js +1 -12
- package/dist/src/state-manager.d.ts +0 -19
- package/dist/src/state-manager.js +1 -34
- package/dist/src/utils.d.ts +27 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -3
- package/skills/wecom-send-media/SKILL.md +2 -0
- package/skills/wecom-send-template-card/SKILL.md +1 -1
- package/skills/wecomcli-calendar/SKILL.md +302 -0
- package/skills/wecomcli-calendar/references/calendar-agenda.md +228 -0
- package/skills/wecomcli-calendar/references/calendar-cancel.md +110 -0
- package/skills/wecomcli-calendar/references/calendar-create.md +240 -0
- package/skills/wecomcli-calendar/references/calendar-freebusy.md +209 -0
- package/skills/wecomcli-calendar/references/calendar-meeting-room.md +172 -0
- package/skills/wecomcli-calendar/references/calendar-search.md +208 -0
- package/skills/wecomcli-calendar/references/calendar-update.md +274 -0
- package/skills/wecomcli-contact/SKILL.md +59 -0
- package/skills/wecomcli-disk/SKILL.md +401 -0
- package/skills/wecomcli-doc/SKILL.md +140 -0
- package/skills/wecomcli-doc/references/doc-contents-append.md +22 -0
- package/skills/wecomcli-doc/references/doc-contents-overwrite.md +29 -0
- package/skills/wecomcli-doc/references/doc-create.md +161 -0
- package/skills/wecomcli-doc/scripts/build_docx.py +1375 -0
- package/skills/wecomcli-doc-manage/SKILL.md +135 -0
- package/skills/wecomcli-doc-manage/references/doc-members-update.md +26 -0
- package/skills/wecomcli-doc-manage/references/doc-names-update.md +22 -0
- package/skills/wecomcli-doc-manage/references/doc-rules-update.md +24 -0
- package/skills/wecomcli-email/SKILL.md +216 -0
- package/skills/wecomcli-email/references/forward-mail.md +131 -0
- package/skills/wecomcli-email/references/get-mail.md +168 -0
- package/skills/wecomcli-email/references/reply-mail.md +140 -0
- package/skills/wecomcli-email/references/search-mail.md +113 -0
- package/skills/wecomcli-email/references/security.md +53 -0
- package/skills/wecomcli-email/references/send-mail.md +186 -0
- package/skills/wecomcli-email/references/send-schedule.md +83 -0
- package/skills/wecomcli-media/SKILL.md +100 -0
- package/skills/wecomcli-meeting/SKILL.md +374 -0
- package/skills/wecomcli-meeting/references/meeting-cancel.md +115 -0
- package/skills/wecomcli-meeting/references/meeting-create.md +169 -0
- package/skills/wecomcli-meeting/references/meeting-list.md +228 -0
- package/skills/wecomcli-meeting/references/meeting-original-get.md +100 -0
- package/skills/wecomcli-meeting/references/meeting-search.md +175 -0
- package/skills/wecomcli-meeting/references/meeting-update.md +219 -0
- package/skills/wecomcli-message/SKILL.md +213 -0
- package/skills/wecomcli-preflight/SKILL.md +147 -0
- package/skills/wecomcli-sheet/SKILL.md +177 -0
- package/skills/wecomcli-sheet/references/sheet-contents-update.md +49 -0
- package/skills/wecomcli-sheet/references/sheet-ranges-get.md +47 -0
- package/skills/wecomcli-sheet/references/sheet-rows-append.md +47 -0
- package/skills/wecomcli-sheet/references/sheet-subsheets-add.md +28 -0
- package/skills/wecomcli-sheet/references/sheet-subsheets-delete.md +22 -0
- package/skills/wecomcli-smartpage/SKILL.md +172 -0
- package/skills/wecomcli-smartpage/references/data-driven-pages.md +50 -0
- package/skills/wecomcli-smartpage/references/formula/arraylist.md +369 -0
- package/skills/wecomcli-smartpage/references/formula/datetime.md +283 -0
- package/skills/wecomcli-smartpage/references/formula/logic.md +247 -0
- package/skills/wecomcli-smartpage/references/formula/math.md +362 -0
- package/skills/wecomcli-smartpage/references/formula/operators.md +246 -0
- package/skills/wecomcli-smartpage/references/formula/pageblock.md +76 -0
- package/skills/wecomcli-smartpage/references/formula/templates.md +410 -0
- package/skills/wecomcli-smartpage/references/formula/text.md +377 -0
- package/skills/wecomcli-smartpage/references/formula/user.md +22 -0
- package/skills/wecomcli-smartpage/references/formula-reference.md +192 -0
- package/skills/wecomcli-smartpage/references/mdx-syntax.md +739 -0
- package/skills/wecomcli-smartpage/references/smartpage-edit.md +519 -0
- package/skills/wecomcli-smartsheet/SKILL.md +153 -0
- package/skills/wecomcli-smartsheet/assets/templates/README.md +53 -0
- package/skills/wecomcli-smartsheet/assets/templates/ai_efficiency.md +709 -0
- package/skills/wecomcli-smartsheet/assets/templates/connect_to_app.md +380 -0
- package/skills/wecomcli-smartsheet/assets/templates/financial_accounting.md +369 -0
- package/skills/wecomcli-smartsheet/assets/templates/hr_and_administration.md +475 -0
- package/skills/wecomcli-smartsheet/assets/templates/ledger_records.md +156 -0
- package/skills/wecomcli-smartsheet/assets/templates/manufacturing.md +395 -0
- package/skills/wecomcli-smartsheet/assets/templates/marketing.md +186 -0
- package/skills/wecomcli-smartsheet/assets/templates/office_essentials.md +299 -0
- package/skills/wecomcli-smartsheet/assets/templates/personal_efficiency.md +70 -0
- package/skills/wecomcli-smartsheet/assets/templates/procurement_logistics.md +325 -0
- package/skills/wecomcli-smartsheet/assets/templates/project_management.md +564 -0
- package/skills/wecomcli-smartsheet/assets/templates/rd_process_customer.md +222 -0
- package/skills/wecomcli-smartsheet/assets/templates/rd_process_ops.md +105 -0
- package/skills/wecomcli-smartsheet/assets/templates/rd_process_project.md +109 -0
- package/skills/wecomcli-smartsheet/assets/templates/rd_process_research.md +92 -0
- package/skills/wecomcli-smartsheet/assets/templates/sales_and_operations.md +446 -0
- package/skills/wecomcli-smartsheet/assets/templates/store_management.md +431 -0
- package/skills/wecomcli-smartsheet/assets/templates/team_tasks.md +274 -0
- package/skills/wecomcli-smartsheet/assets/templates/wechat_customer.md +384 -0
- package/skills/wecomcli-smartsheet/assets/templates/work_report.md +100 -0
- package/skills/wecomcli-smartsheet/references/common.md +145 -0
- package/skills/wecomcli-smartsheet/references/smart-sheet-chart-types.md +95 -0
- package/skills/wecomcli-smartsheet/references/smart-sheet-edit.md +611 -0
- package/skills/wecomcli-smartsheet/references/smart-sheet-field-types.md +438 -0
- package/skills/wecomcli-smartsheet/references/smart-sheet-formula.md +845 -0
- package/skills/wecomcli-smartsheet/references/smart-sheet-read.md +411 -0
- package/skills/wecomcli-smartsheet/references/smart-sheet-record-values.md +203 -0
- package/skills/wecomcli-smartsheet/references/smart-sheet-view-types.md +356 -0
- package/skills/{wecom-smartsheet/references/webhook-examples.md → wecomcli-smartsheet/references/smart-sheet-webhook-examples.md} +23 -32
- package/skills/wecomcli-smartsheet/references/smart-sheet-webhook.md +169 -0
- package/skills/wecomcli-todo/SKILL.md +75 -0
- package/skills/wecomcli-todo/references/todo-create.md +139 -0
- package/skills/wecomcli-todo/references/todo-delete.md +65 -0
- package/skills/wecomcli-todo/references/todo-finish.md +74 -0
- package/skills/wecomcli-todo/references/todo-get.md +68 -0
- package/skills/wecomcli-todo/references/todo-list.md +135 -0
- package/skills/wecomcli-todo/references/todo-update.md +114 -0
- package/dist/src/mcp/index.d.ts +0 -6
- package/dist/src/mcp/index.js +0 -10
- package/dist/src/mcp/interceptors/biz-error.d.ts +0 -11
- package/dist/src/mcp/interceptors/biz-error.js +0 -69
- package/dist/src/mcp/interceptors/doc-auth-error.d.ts +0 -10
- package/dist/src/mcp/interceptors/doc-auth-error.js +0 -171
- package/dist/src/mcp/interceptors/index.d.ts +0 -35
- package/dist/src/mcp/interceptors/index.js +0 -72
- package/dist/src/mcp/interceptors/msg-media.d.ts +0 -11
- package/dist/src/mcp/interceptors/msg-media.js +0 -133
- package/dist/src/mcp/interceptors/smartpage-create.d.ts +0 -30
- package/dist/src/mcp/interceptors/smartpage-create.js +0 -132
- package/dist/src/mcp/interceptors/smartpage-export.d.ts +0 -17
- package/dist/src/mcp/interceptors/smartpage-export.js +0 -87
- package/dist/src/mcp/interceptors/smartsheet-upload.d.ts +0 -22
- package/dist/src/mcp/interceptors/smartsheet-upload.js +0 -263
- package/dist/src/mcp/interceptors/types.d.ts +0 -64
- package/dist/src/mcp/interceptors/types.js +0 -7
- package/dist/src/mcp/schema.d.ts +0 -11
- package/dist/src/mcp/schema.js +0 -99
- package/dist/src/mcp/tool.d.ts +0 -63
- package/dist/src/mcp/tool.js +0 -224
- package/dist/src/mcp/transport.d.ts +0 -94
- package/dist/src/mcp/transport.js +0 -549
- package/skills/wecom-contact/SKILL.md +0 -162
- package/skills/wecom-doc/SKILL.md +0 -127
- package/skills/wecom-meeting/SKILL.md +0 -441
- package/skills/wecom-meeting/references/example-full.md +0 -30
- package/skills/wecom-meeting/references/example-reminder.md +0 -46
- package/skills/wecom-meeting/references/example-security.md +0 -22
- package/skills/wecom-meeting/references/response-get-meeting-info.md +0 -148
- package/skills/wecom-msg/SKILL.md +0 -157
- package/skills/wecom-msg/references/api-get-messages.md +0 -93
- package/skills/wecom-msg/references/api-get-msg-chat-list.md +0 -58
- package/skills/wecom-msg/references/api-get-msg-media.md +0 -44
- package/skills/wecom-msg/references/api-send-message.md +0 -39
- package/skills/wecom-preflight/SKILL.md +0 -141
- package/skills/wecom-schedule/SKILL.md +0 -161
- package/skills/wecom-schedule/references/api-check-availability.md +0 -56
- package/skills/wecom-schedule/references/api-create-schedule.md +0 -38
- package/skills/wecom-schedule/references/api-get-schedule-detail.md +0 -81
- package/skills/wecom-schedule/references/api-update-schedule.md +0 -32
- package/skills/wecom-schedule/references/ref-reminders.md +0 -24
- package/skills/wecom-sheet/SKILL.md +0 -131
- package/skills/wecom-sheet/references/sheet-add-sub.md +0 -54
- package/skills/wecom-sheet/references/sheet-append-data.md +0 -64
- package/skills/wecom-sheet/references/sheet-delete-sub.md +0 -38
- package/skills/wecom-sheet/references/sheet-update-range-data.md +0 -95
- package/skills/wecom-smartpage/SKILL.md +0 -119
- package/skills/wecom-smartpage/references/smartpage-create.md +0 -130
- package/skills/wecom-smartpage/references/smartpage-export.md +0 -168
- package/skills/wecom-smartsheet/SKILL.md +0 -241
- package/skills/wecom-smartsheet/references/smartsheet-cell-value-formats.md +0 -163
- package/skills/wecom-smartsheet/references/smartsheet-field-types.md +0 -44
- package/skills/wecom-smartsheet/references/smartsheet-get-records.md +0 -187
- package/skills/wecom-smartsheet/references/webhook-fallback.md +0 -183
- package/skills/wecom-todo/SKILL.md +0 -272
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 登录态同步(ensureSynced)
|
|
3
|
+
*
|
|
4
|
+
* 职责:确保当前会话的 bot 在自己的配置目录里有可用登录态,返回该目录。
|
|
5
|
+
*
|
|
6
|
+
* **授权动作整体委托 cli**:spawn `wecom-cli auth init --bot-id <ID> --secret <SECRET>`
|
|
7
|
+
* (cli >= 0.1.9-build.149,stderr 非 TTY 时走直连授权分支)。
|
|
8
|
+
* 插件不再复刻凭据协议——签名算法、AES-GCM格式、`.encryption_key`、原子写
|
|
9
|
+
* 全部由 cli 负责。那些代码曾是整套方案最脆的部分:cli 改一处格式,
|
|
10
|
+
* 插件就静默解密失败,且表现为"未授权",完全不指向真实原因。
|
|
11
|
+
*
|
|
12
|
+
* 本文件只保留 cli 里不存在的 **openclaw 侧策略**:
|
|
13
|
+
* 1. 目录隔离:每个 (botId, secret) 组合一个目录 → 多 bot 隔离 + secret 轮换即时生效
|
|
14
|
+
* 2. 短路:`auth init` 无幂等,每次调用都会打一次鉴权接口(45009 频率限制)
|
|
15
|
+
* 3. 并发去重 + 全局串行
|
|
16
|
+
* 4. 重签冷却熔断
|
|
17
|
+
*/
|
|
18
|
+
import * as crypto from "node:crypto";
|
|
19
|
+
import * as fs from "node:fs";
|
|
20
|
+
import * as path from "node:path";
|
|
21
|
+
import { CLI_AUTH_TIMEOUT_MS, CLI_KILL_FORCE_WAIT_MS, CLI_KILL_GRACE_MS, CLI_LOG, CLI_MAX_OUTPUT_BYTES, CLI_RESIGN_COOLDOWN_MS, } from "./const.js";
|
|
22
|
+
import { resolveStateDir } from "../state-dir-resolve.js";
|
|
23
|
+
import { spawn } from "./process-launcher.js";
|
|
24
|
+
import { BoundedOutputCollector } from "./process-output.js";
|
|
25
|
+
export class CliAuthError extends Error {
|
|
26
|
+
errcode;
|
|
27
|
+
constructor(message, errcode) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.errcode = errcode;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const sha256Hex = (s) => crypto.createHash("sha256").update(s, "utf8").digest("hex");
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// 路径
|
|
35
|
+
// ============================================================================
|
|
36
|
+
/** 目录名里secret 指纹的长度 */
|
|
37
|
+
const FINGERPRINT_LEN = 8;
|
|
38
|
+
/** 兄弟目录清理时用于识别"本插件生成的目录"的形状 */
|
|
39
|
+
const DIR_SHAPE = new RegExp(`^(.+)-([0-9a-f]{${FINGERPRINT_LEN}})$`);
|
|
40
|
+
const cliStateRoot = () => path.join(resolveStateDir(), "wecom-cli");
|
|
41
|
+
const safeBotId = (botId) => botId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
42
|
+
const credentialsPath = (dir) => path.join(dir, "credentials.enc");
|
|
43
|
+
const keyPath = (dir) => path.join(dir, ".encryption_key");
|
|
44
|
+
/**
|
|
45
|
+
* 配置目录 = `<state>/wecom-cli/<safeBotId>-<sha8(botId:secret)>`。
|
|
46
|
+
*
|
|
47
|
+
* 为什么把 secret 指纹编进目录名,而不是另外维护一份 meta 文件:
|
|
48
|
+
* 删掉自实现的crypto 后插件不再解密 `credentials.enc`,也就永远读不到里面的
|
|
49
|
+
* secret,"磁盘上这份凭据是不是用当前配置签出来的"这个问题就无法回答。
|
|
50
|
+
* 把指纹放进目录名后,secret 一变目录就变 → 天然没有凭据 → 自动重新授权,
|
|
51
|
+
* 且走的是普通路径(不碰 force 熔断),所以**改配置立即生效**。
|
|
52
|
+
*
|
|
53
|
+
* 分隔符 `:` 是必需的:否则 `ab`+`c` 与 `a`+`bc` 会得到同一指纹。
|
|
54
|
+
*
|
|
55
|
+
* 已知取舍:`safeBotId` 的字符归一化可能碰撞(`a.b` 与 `a_b` 同名),
|
|
56
|
+
* 清理兄弟目录时可能误删另一个 bot 的目录。代价仅是"那个 bot 下次调用
|
|
57
|
+
* 重新授权一次",不是数据丢失、更不是串号,因此接受。
|
|
58
|
+
*/
|
|
59
|
+
export function cliConfigDirFor(botId, secret) {
|
|
60
|
+
const fp = sha256Hex(`${botId}:${secret}`).slice(0, FINGERPRINT_LEN);
|
|
61
|
+
return path.join(cliStateRoot(), `${safeBotId(botId)}-${fp}`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 短路判据:两个文件都在就认为已授权。
|
|
65
|
+
*
|
|
66
|
+
* 短路不是性能优化而是**必需项**:`auth init` 内部无任何幂等检查,
|
|
67
|
+
* 每次调用都会打一次 `fetch_auth`,而该接口有 45009 频率超限。
|
|
68
|
+
*
|
|
69
|
+
* `.encryption_key` 的存在性同样要查:文件缺失但密文还在时,cli 会回退到
|
|
70
|
+
* keyring 取密钥,而 keyring 可能被系统锁定/不可用 → 静默表现为"未授权"。
|
|
71
|
+
*/
|
|
72
|
+
function isAuthorized(dir) {
|
|
73
|
+
return fs.existsSync(credentialsPath(dir)) && fs.existsSync(keyPath(dir));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 清理同一 bot 的旧 secret 目录。
|
|
77
|
+
*
|
|
78
|
+
* 时机必须是**授权成功之后**:反过来先清理,一旦授权失败就连旧凭据都没了,
|
|
79
|
+
* 把"可降级"变成"直接不可用"。
|
|
80
|
+
*
|
|
81
|
+
* 双重约束防误删:父目录固定在 `<state>/wecom-cli/`,且名字必须严格匹配
|
|
82
|
+
* `<safeBotId>-<8位hex>`。清理是卫生工作而非正确性前提,失败只 warn。
|
|
83
|
+
*/
|
|
84
|
+
function cleanupSiblings(botId, currentDir) {
|
|
85
|
+
const root = cliStateRoot();
|
|
86
|
+
const prefix = safeBotId(botId);
|
|
87
|
+
let entries;
|
|
88
|
+
try {
|
|
89
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (!entry.isDirectory())
|
|
96
|
+
continue;
|
|
97
|
+
const m = DIR_SHAPE.exec(entry.name);
|
|
98
|
+
if (!m || m[1] !== prefix)
|
|
99
|
+
continue;
|
|
100
|
+
const full = path.join(root, entry.name);
|
|
101
|
+
if (full === currentDir)
|
|
102
|
+
continue;
|
|
103
|
+
try {
|
|
104
|
+
fs.rmSync(full, { recursive: true, force: true });
|
|
105
|
+
console.log(`${CLI_LOG} 已清理旧凭据目录 ${entry.name}`);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.warn(`${CLI_LOG} 清理旧凭据目录失败(不影响本次调用)${entry.name}: ` +
|
|
109
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// auth init
|
|
115
|
+
// ============================================================================
|
|
116
|
+
/** 从 cli 的错误输出里提取 errcode,兼容两种形态 */
|
|
117
|
+
function extractErrcode(stdout) {
|
|
118
|
+
try {
|
|
119
|
+
const parsed = JSON.parse(stdout);
|
|
120
|
+
// `{"error":{...}}` 是常规形态;裸 `{"errcode":...}` 来自 Transport::Api 的原样透传
|
|
121
|
+
const code = parsed?.error?.code ?? parsed?.errcode;
|
|
122
|
+
return typeof code === "number" ? code : null;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function extractMessage(stdout) {
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(stdout);
|
|
131
|
+
return parsed?.error?.message ?? parsed?.errmsg ?? null;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* `auth init` 全局串行(**跨 bot 也串**)。
|
|
139
|
+
*
|
|
140
|
+
* keyring 是全局资源,macOS Keychain 的并发写入行为不确定。
|
|
141
|
+
* 授权频率极低(首次 + 重签),全局串行的代价可忽略,换来行为确定性。
|
|
142
|
+
* 业务命令仍按 botId 串行,不受这里影响。
|
|
143
|
+
*/
|
|
144
|
+
let authChain = Promise.resolve();
|
|
145
|
+
function serializeAuth(task) {
|
|
146
|
+
const next = authChain.then(task, task);
|
|
147
|
+
authChain = next.catch(() => undefined);
|
|
148
|
+
return next;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 执行非交互授权。
|
|
152
|
+
*
|
|
153
|
+
* 刻意**不复用** tool.ts 的 runCli:
|
|
154
|
+
* - runCli 会把 `argv.join(" ")` 打进日志,那样 secret 会被写进 openclaw
|
|
155
|
+
* 日志文件并持久化/上报,比 `ps` 可见严重得多
|
|
156
|
+
* - 两者的错误分类逻辑也不同(这里 exit 2 意味着"cli 版本不支持")
|
|
157
|
+
*
|
|
158
|
+
* 门禁提醒:cli 只在 **stderr 非 TTY** 时才走 `--bot-id/--secret` 直连分支,
|
|
159
|
+
* 否则**静默忽略参数**并回退扫码。`stdio` 的 pipe 天然满足;
|
|
160
|
+
* stdin 用 `ignore` 而非 pipe——万一走到交互分支,让它立刻拿到 EOF 而不是挂着等输入。
|
|
161
|
+
*/
|
|
162
|
+
function spawnAuthInit(binPath, botId, secret, dir, env) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const started = Date.now();
|
|
165
|
+
const child = spawn(binPath, ["auth", "init", "--bot-id", botId, "--secret", secret], {
|
|
166
|
+
shell: false,
|
|
167
|
+
cwd: resolveStateDir(),
|
|
168
|
+
env: {
|
|
169
|
+
PATH: process.env.PATH ?? "",
|
|
170
|
+
HOME: process.env.HOME ?? "",
|
|
171
|
+
...(process.platform === "win32"
|
|
172
|
+
? { USERPROFILE: process.env.USERPROFILE ?? "", SystemRoot: process.env.SystemRoot ?? "" }
|
|
173
|
+
: {}),
|
|
174
|
+
WECOM_CLI_CONFIG_DIR: dir,
|
|
175
|
+
WECOM_CLI_LOG_LEVEL: "warn",
|
|
176
|
+
// 端点与业务调用同源:授权和调用现在由同一个二进制、同一份 env 解析,
|
|
177
|
+
// 不可能再出现"授权在测试环境、调用在生产"的错配
|
|
178
|
+
...env,
|
|
179
|
+
},
|
|
180
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
181
|
+
});
|
|
182
|
+
const stdout = new BoundedOutputCollector(CLI_MAX_OUTPUT_BYTES, "head");
|
|
183
|
+
const stderr = new BoundedOutputCollector(CLI_MAX_OUTPUT_BYTES, "tail");
|
|
184
|
+
let timedOut = false;
|
|
185
|
+
let settled = false;
|
|
186
|
+
let graceTimer;
|
|
187
|
+
let forceWaitTimer;
|
|
188
|
+
child.stdout.on("data", (chunk) => stdout.append(chunk));
|
|
189
|
+
child.stderr.on("data", (chunk) => stderr.append(chunk));
|
|
190
|
+
const clearTimers = () => {
|
|
191
|
+
clearTimeout(killTimer);
|
|
192
|
+
if (graceTimer)
|
|
193
|
+
clearTimeout(graceTimer);
|
|
194
|
+
if (forceWaitTimer)
|
|
195
|
+
clearTimeout(forceWaitTimer);
|
|
196
|
+
};
|
|
197
|
+
const finish = (status) => {
|
|
198
|
+
if (settled)
|
|
199
|
+
return;
|
|
200
|
+
settled = true;
|
|
201
|
+
clearTimers();
|
|
202
|
+
resolve({
|
|
203
|
+
status,
|
|
204
|
+
stdout: stdout.result().text,
|
|
205
|
+
stderr: stderr.result().text,
|
|
206
|
+
ms: Date.now() - started,
|
|
207
|
+
timedOut,
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
const killTimer = setTimeout(() => {
|
|
211
|
+
timedOut = true;
|
|
212
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
213
|
+
child.kill("SIGTERM");
|
|
214
|
+
graceTimer = setTimeout(() => {
|
|
215
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
216
|
+
child.kill("SIGKILL");
|
|
217
|
+
forceWaitTimer = setTimeout(() => {
|
|
218
|
+
child.stdout.destroy();
|
|
219
|
+
child.stderr.destroy();
|
|
220
|
+
finish(child.exitCode);
|
|
221
|
+
}, CLI_KILL_FORCE_WAIT_MS);
|
|
222
|
+
}, CLI_KILL_GRACE_MS);
|
|
223
|
+
}, CLI_AUTH_TIMEOUT_MS);
|
|
224
|
+
child.on("error", (err) => {
|
|
225
|
+
if (settled)
|
|
226
|
+
return;
|
|
227
|
+
settled = true;
|
|
228
|
+
clearTimers();
|
|
229
|
+
reject(err);
|
|
230
|
+
});
|
|
231
|
+
child.on("close", (status) => finish(status));
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async function runAuthInit(binPath, botId, secret, dir, env) {
|
|
235
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
236
|
+
const redact = (s) => (secret ? s.split(secret).join("***") : s);
|
|
237
|
+
const shortBot = `${botId.slice(0, 10)}…`;
|
|
238
|
+
console.log(`${CLI_LOG} 授权中 auth init --bot-id ${shortBot} --secret *** → ${path.basename(dir)}`);
|
|
239
|
+
const out = await spawnAuthInit(binPath, botId, secret, dir, env);
|
|
240
|
+
if (out.timedOut) {
|
|
241
|
+
throw new CliAuthError(`企业微信授权超时(${CLI_AUTH_TIMEOUT_MS / 1000}s)。` +
|
|
242
|
+
"常见原因:网络不可达,或系统钥匙串(Keychain)弹出了授权确认而无人响应。");
|
|
243
|
+
}
|
|
244
|
+
// exit 2 = clap 参数错误。插件依赖已锁定支持该能力的版本,所以这属于"不该发生",
|
|
245
|
+
// 通常说明二进制被外部替换(binPath 指向旧二进制 / 全局安装的 cli)
|
|
246
|
+
if (out.status === 2) {
|
|
247
|
+
throw new CliAuthError("当前 wecom-cli 不支持非交互授权(缺少 --bot-id/--secret)。" +
|
|
248
|
+
"插件已锁定支持该能力的版本,出现此错误通常说明二进制被外部替换——" +
|
|
249
|
+
"请检查 `channels.wecom.cli.binPath` 是否指向了旧二进制或全局安装的 cli。");
|
|
250
|
+
}
|
|
251
|
+
if (out.status !== 0) {
|
|
252
|
+
// 错误在 stdout(cli 统一 `println!(err.render())`);stderr 只有 cliclack 的
|
|
253
|
+
// 进度装饰与 ANSI 控制符,塞进用户文案会污染,只在 stdout 为空时兜底
|
|
254
|
+
const code = extractErrcode(out.stdout) ?? undefined;
|
|
255
|
+
const detail = extractMessage(out.stdout) ??
|
|
256
|
+
(out.stdout.trim() || out.stderr.trim() || `exit ${out.status}`).slice(0, 500);
|
|
257
|
+
throw new CliAuthError(`企业微信授权失败:${redact(detail)}${code != null ? `(errcode=${code})` : ""}。` +
|
|
258
|
+
"请检查 openclaw 配置 `channels.wecom` 中的 botId / secret 是否正确。", code);
|
|
259
|
+
}
|
|
260
|
+
// 防御性:exit 0 但文件没落地(例如 cli 未来改了目录语义)
|
|
261
|
+
if (!isAuthorized(dir)) {
|
|
262
|
+
throw new CliAuthError(`wecom-cli 授权返回成功,但 ${dir} 下未生成凭据文件。` +
|
|
263
|
+
"可能是 cli 版本行为变更,请检查插件依赖的 @wecom/cli 版本。");
|
|
264
|
+
}
|
|
265
|
+
console.log(`${CLI_LOG} 授权成功 botId=${shortBot} (${out.ms}ms)`);
|
|
266
|
+
cleanupSiblings(botId, dir);
|
|
267
|
+
}
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// ensureSynced
|
|
270
|
+
// ============================================================================
|
|
271
|
+
/** 进程内并发去重,key = 配置目录(含 secret 指纹,改secret 不会复用旧 inflight) */
|
|
272
|
+
const inflight = new Map();
|
|
273
|
+
/** 上次实际开始强制重签的时间,用于服务端频控保护 */
|
|
274
|
+
const lastResignAttempt = new Map();
|
|
275
|
+
/** 上次强制重签成功的时间,仅用于区分尝试与成功的状态语义 */
|
|
276
|
+
const lastResignSucceeded = new Map();
|
|
277
|
+
/**
|
|
278
|
+
* 确保指定 bot 的登录态已就绪,返回其配置目录。
|
|
279
|
+
*
|
|
280
|
+
* @param force 强制重新授权(业务命令报凭据类错误时的兜底),受冷却熔断约束。
|
|
281
|
+
* 注意 cli 没有 `--force` 参数,也不需要:`auth init` 本身无幂等,
|
|
282
|
+
* 每次调用都会重新签发,所以"force"只是跳过插件自己的短路。
|
|
283
|
+
*/
|
|
284
|
+
export async function ensureSynced(params) {
|
|
285
|
+
const { binPath, botId, secret, env = {}, force = false } = params;
|
|
286
|
+
if (!botId?.trim() || !secret?.trim()) {
|
|
287
|
+
throw new CliAuthError("当前企业微信账号未配置 botId / secret,请检查 openclaw 配置 `channels.wecom`");
|
|
288
|
+
}
|
|
289
|
+
const dir = cliConfigDirFor(botId, secret);
|
|
290
|
+
if (!force && isAuthorized(dir))
|
|
291
|
+
return dir;
|
|
292
|
+
// 普通授权与强制重签都按目录复用同一个进行中的任务,避免并发重复签发。
|
|
293
|
+
const existing = inflight.get(dir);
|
|
294
|
+
if (existing)
|
|
295
|
+
return existing;
|
|
296
|
+
// 授权本身全局串行;冷却时间在任务真正开始时记录,不把排队时间算作重签尝试。
|
|
297
|
+
const task = serializeAuth(async () => {
|
|
298
|
+
// 排队期间可能已被前一个任务授权好了(同目录并发),再查一次省一次鉴权请求
|
|
299
|
+
if (!force && isAuthorized(dir))
|
|
300
|
+
return dir;
|
|
301
|
+
let attemptAt;
|
|
302
|
+
if (force) {
|
|
303
|
+
const last = lastResignAttempt.get(dir) ?? 0;
|
|
304
|
+
const elapsed = Date.now() - last;
|
|
305
|
+
if (elapsed < CLI_RESIGN_COOLDOWN_MS) {
|
|
306
|
+
const waitSec = Math.ceil((CLI_RESIGN_COOLDOWN_MS - elapsed) / 1000);
|
|
307
|
+
const succeeded = (lastResignSucceeded.get(dir) ?? 0) >= last;
|
|
308
|
+
const state = succeeded ? "凭据刚刚重新签发成功但仍然不可用" : "凭据刚刚尝试重新签发但未成功";
|
|
309
|
+
throw new CliAuthError(`${state},${waitSec} 秒内不再重试,` +
|
|
310
|
+
`以避免触发企业微信换取凭据的频率限制(45009)。` +
|
|
311
|
+
`请检查 openclaw 配置 \`channels.wecom\` 中的 botId / secret 是否正确。`);
|
|
312
|
+
}
|
|
313
|
+
attemptAt = Date.now();
|
|
314
|
+
lastResignAttempt.set(dir, attemptAt);
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
await runAuthInit(binPath, botId, secret, dir, env);
|
|
318
|
+
if (force)
|
|
319
|
+
lastResignSucceeded.set(dir, Date.now());
|
|
320
|
+
return dir;
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
if (force) {
|
|
324
|
+
console.warn(`${CLI_LOG} 强制重签失败,保留本次尝试的冷却保护 ` +
|
|
325
|
+
`(attemptAt=${attemptAt ?? "unknown"}): ${err instanceof Error ? err.message : String(err)}`);
|
|
326
|
+
}
|
|
327
|
+
throw err;
|
|
328
|
+
}
|
|
329
|
+
}).finally(() => {
|
|
330
|
+
if (inflight.get(dir) === task)
|
|
331
|
+
inflight.delete(dir);
|
|
332
|
+
});
|
|
333
|
+
inflight.set(dir, task);
|
|
334
|
+
return task;
|
|
335
|
+
}
|
|
336
|
+
/** 仅用于测试 */
|
|
337
|
+
export function resetCredentialState() {
|
|
338
|
+
inflight.clear();
|
|
339
|
+
lastResignAttempt.clear();
|
|
340
|
+
lastResignSucceeded.clear();
|
|
341
|
+
authChain = Promise.resolve();
|
|
342
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createWeComCliTool, type CreateWeComCliToolOptions } from "./tool.js";
|
|
2
|
+
export { CLI_TOOL_NAME } from "./const.js";
|
|
3
|
+
export { cliConfigDirFor, ensureSynced } from "./credentials.js";
|
|
4
|
+
export { locateCliBinary } from "./locate.js";
|
|
5
|
+
export { normalizeArgs, tokenize, assertSafeArgv, CliArgvError } from "./argv.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createWeComCliTool } from "./tool.js";
|
|
2
|
+
export { CLI_TOOL_NAME } from "./const.js";
|
|
3
|
+
export { cliConfigDirFor, ensureSynced } from "./credentials.js";
|
|
4
|
+
export { locateCliBinary } from "./locate.js";
|
|
5
|
+
export { normalizeArgs, tokenize, assertSafeArgv, CliArgvError } from "./argv.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ToolPolicyConfig {
|
|
2
|
+
profile?: string;
|
|
3
|
+
alsoAllow?: string[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* 仅在旧白名单确实导致 `wecom-cli` 不可见时提示迁移。
|
|
7
|
+
*
|
|
8
|
+
* 不做配置卫生检查:profile=full、已放行具体 tool、已放行插件 ID 时,
|
|
9
|
+
* 旧条目只是无害残留,不应制造启动噪音。
|
|
10
|
+
*/
|
|
11
|
+
export declare function shouldWarnLegacyToolAllow(tools: ToolPolicyConfig | undefined): boolean;
|
|
12
|
+
export declare const LEGACY_TOOL_WARNING: string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const LEGACY_TOOL_NAME = "wecom_mcp";
|
|
2
|
+
const CLI_TOOL_NAME = "wecom-cli";
|
|
3
|
+
const PLUGIN_ID = "wecom-openclaw-plugin";
|
|
4
|
+
/**
|
|
5
|
+
* 仅在旧白名单确实导致 `wecom-cli` 不可见时提示迁移。
|
|
6
|
+
*
|
|
7
|
+
* 不做配置卫生检查:profile=full、已放行具体 tool、已放行插件 ID 时,
|
|
8
|
+
* 旧条目只是无害残留,不应制造启动噪音。
|
|
9
|
+
*/
|
|
10
|
+
export function shouldWarnLegacyToolAllow(tools) {
|
|
11
|
+
if (tools?.profile === "full")
|
|
12
|
+
return false;
|
|
13
|
+
const allow = tools?.alsoAllow;
|
|
14
|
+
if (!Array.isArray(allow) || !allow.includes(LEGACY_TOOL_NAME))
|
|
15
|
+
return false;
|
|
16
|
+
return !allow.includes(CLI_TOOL_NAME) && !allow.includes(PLUGIN_ID);
|
|
17
|
+
}
|
|
18
|
+
export const LEGACY_TOOL_WARNING = "检测到旧工具白名单 `wecom_mcp`。该工具已移除,企微业务能力已统一迁移到 `wecom-cli`。" +
|
|
19
|
+
"请在 `tools.alsoAllow` 中添加 `wecom-cli`(旧条目可保留或自行删除),然后重启 Gateway。" +
|
|
20
|
+
"也可直接放行插件 ID:`wecom-openclaw-plugin`。";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wecom-cli 二进制寻址
|
|
3
|
+
*
|
|
4
|
+
* 为什么不依赖 PATH:
|
|
5
|
+
* 1. 插件私有的 node_modules/.bin 不在 PATH 上
|
|
6
|
+
* 2. 更重要的是——PATH 上可能存在用户**全局安装**的 wecom-cli,
|
|
7
|
+
* 它绑定的是用户自己的 bot(默认目录 ~/.config/wecom)。
|
|
8
|
+
* 误用会导致"拿 A 企业的凭据查 B 企业的数据",是静默的数据越权。
|
|
9
|
+
* 因此这里只接受"插件依赖内的二进制"或"配置显式指定的路径"。
|
|
10
|
+
*/
|
|
11
|
+
/** 按主包 package.json 的 bin 声明解析 shim,兼容字符串和对象两种形式。 */
|
|
12
|
+
export declare function binCandidatesFromPackageManifest(root: string): string[];
|
|
13
|
+
export type LocateResult = {
|
|
14
|
+
ok: true;
|
|
15
|
+
binPath: string;
|
|
16
|
+
source: string;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
tried: string[];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* 定位 wecom-cli 二进制。
|
|
23
|
+
*
|
|
24
|
+
* 优先级:
|
|
25
|
+
* 1. 配置显式指定 `channels.wecom.cli.binPath`(联调期塞本地 cargo 产物用)
|
|
26
|
+
* 2. 平台子包 `<prefix>-<platform>-<arch>`(跳过 bin/wecom.js 那层 node shim,
|
|
27
|
+
* 每次调用少一次 node 冷启动)
|
|
28
|
+
* 3. 主包 `<prefix>` 自带的 bin
|
|
29
|
+
*/
|
|
30
|
+
export declare function locateCliBinary(explicitPath?: string): LocateResult;
|
|
31
|
+
/** 仅用于测试:清除寻址缓存 */
|
|
32
|
+
export declare function resetLocateCache(): void;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wecom-cli 二进制寻址
|
|
3
|
+
*
|
|
4
|
+
* 为什么不依赖 PATH:
|
|
5
|
+
* 1. 插件私有的 node_modules/.bin 不在 PATH 上
|
|
6
|
+
* 2. 更重要的是——PATH 上可能存在用户**全局安装**的 wecom-cli,
|
|
7
|
+
* 它绑定的是用户自己的 bot(默认目录 ~/.config/wecom)。
|
|
8
|
+
* 误用会导致"拿 A 企业的凭据查 B 企业的数据",是静默的数据越权。
|
|
9
|
+
* 因此这里只接受"插件依赖内的二进制"或"配置显式指定的路径"。
|
|
10
|
+
*/
|
|
11
|
+
import { createRequire } from "node:module";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { CLI_LOG } from "./const.js";
|
|
15
|
+
const require_ = createRequire(import.meta.url);
|
|
16
|
+
/**
|
|
17
|
+
* npm 包名前缀候选。
|
|
18
|
+
*
|
|
19
|
+
* 联调期用内网包 `@tencent/wecom-cli`(discovery 分支、启用 custom-endpoint),
|
|
20
|
+
* 正式发布用公开包 `@wecom/cli`。两者平台子包命名规则不同,故依次尝试。
|
|
21
|
+
*/
|
|
22
|
+
const PACKAGE_PREFIXES = ["@tencent/wecom-cli", "@wecom/cli"];
|
|
23
|
+
/** node platform/arch → 平台子包后缀 */
|
|
24
|
+
function platformSuffix() {
|
|
25
|
+
const { platform, arch } = process;
|
|
26
|
+
if (platform === "darwin" && arch === "arm64")
|
|
27
|
+
return "darwin-arm64";
|
|
28
|
+
if (platform === "darwin" && arch === "x64")
|
|
29
|
+
return "darwin-x64";
|
|
30
|
+
if (platform === "linux" && arch === "arm64")
|
|
31
|
+
return "linux-arm64";
|
|
32
|
+
if (platform === "linux" && arch === "x64")
|
|
33
|
+
return "linux-x64";
|
|
34
|
+
if (platform === "win32" && arch === "x64")
|
|
35
|
+
return "win32-x64";
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function binName() {
|
|
39
|
+
return process.platform === "win32" ? "wecom-cli.exe" : "wecom-cli";
|
|
40
|
+
}
|
|
41
|
+
/** 判断路径是否为可执行文件 */
|
|
42
|
+
function isExecutable(p) {
|
|
43
|
+
try {
|
|
44
|
+
if (!fs.statSync(p).isFile())
|
|
45
|
+
return false;
|
|
46
|
+
if (process.platform !== "win32")
|
|
47
|
+
fs.accessSync(p, fs.constants.X_OK);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** 从平台包根目录推断原生二进制位置 */
|
|
55
|
+
function binCandidatesFromPkgRoot(root) {
|
|
56
|
+
return [path.join(root, "bin", binName()), path.join(root, binName())];
|
|
57
|
+
}
|
|
58
|
+
/** 按主包 package.json 的 bin 声明解析 shim,兼容字符串和对象两种形式。 */
|
|
59
|
+
export function binCandidatesFromPackageManifest(root) {
|
|
60
|
+
try {
|
|
61
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
62
|
+
const values = typeof manifest.bin === "string"
|
|
63
|
+
? [manifest.bin]
|
|
64
|
+
: manifest.bin && typeof manifest.bin === "object"
|
|
65
|
+
? [manifest.bin["wecom-cli"], ...Object.values(manifest.bin)]
|
|
66
|
+
: [];
|
|
67
|
+
const candidates = [];
|
|
68
|
+
for (const value of values) {
|
|
69
|
+
if (typeof value !== "string" || !value.trim())
|
|
70
|
+
continue;
|
|
71
|
+
const candidate = path.resolve(root, value);
|
|
72
|
+
const relative = path.relative(root, candidate);
|
|
73
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
74
|
+
continue;
|
|
75
|
+
if (!candidates.includes(candidate))
|
|
76
|
+
candidates.push(candidate);
|
|
77
|
+
}
|
|
78
|
+
return candidates;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
let cached = null;
|
|
85
|
+
/** 已打印过寻址结果的 binPath,避免每次调用都刷日志 */
|
|
86
|
+
const logged = new Set();
|
|
87
|
+
const EXPLICIT_SOURCE = "channels.wecom.cli.binPath";
|
|
88
|
+
/**
|
|
89
|
+
* 打印一次寻址结果。
|
|
90
|
+
*
|
|
91
|
+
* 排查"是否误用了全局 cli"时,这是唯一的直接证据:
|
|
92
|
+
* `source` 为npm 包名 → 命中插件依赖内的二进制(正确);
|
|
93
|
+
* `source` 为 binPath → 来自配置逃生舱,需人工确认该路径不是全局安装。
|
|
94
|
+
*/
|
|
95
|
+
function logHit(binPath, source) {
|
|
96
|
+
if (logged.has(binPath))
|
|
97
|
+
return;
|
|
98
|
+
logged.add(binPath);
|
|
99
|
+
if (source === EXPLICIT_SOURCE) {
|
|
100
|
+
console.warn(`${CLI_LOG} 二进制寻址命中 source=${source} path=${binPath} ` +
|
|
101
|
+
"(来自配置显式指定,请确认它不是全局安装的 wecom-cli:" +
|
|
102
|
+
"配置目录仍由插件按会话机器人注入,但版本可能与 skills 不匹配)");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log(`${CLI_LOG} 二进制寻址命中 source=${source} path=${binPath}`);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 定位 wecom-cli 二进制。
|
|
109
|
+
*
|
|
110
|
+
* 优先级:
|
|
111
|
+
* 1. 配置显式指定 `channels.wecom.cli.binPath`(联调期塞本地 cargo 产物用)
|
|
112
|
+
* 2. 平台子包 `<prefix>-<platform>-<arch>`(跳过 bin/wecom.js 那层 node shim,
|
|
113
|
+
* 每次调用少一次 node 冷启动)
|
|
114
|
+
* 3. 主包 `<prefix>` 自带的 bin
|
|
115
|
+
*/
|
|
116
|
+
export function locateCliBinary(explicitPath) {
|
|
117
|
+
if (explicitPath?.trim()) {
|
|
118
|
+
const p = path.resolve(explicitPath.trim());
|
|
119
|
+
if (isExecutable(p)) {
|
|
120
|
+
logHit(p, EXPLICIT_SOURCE);
|
|
121
|
+
return { ok: true, binPath: p, source: EXPLICIT_SOURCE };
|
|
122
|
+
}
|
|
123
|
+
return { ok: false, tried: [`${p} (配置指定,但不存在或不可执行)`] };
|
|
124
|
+
}
|
|
125
|
+
if (cached?.ok)
|
|
126
|
+
return cached;
|
|
127
|
+
const tried = [];
|
|
128
|
+
const suffix = platformSuffix();
|
|
129
|
+
for (const prefix of PACKAGE_PREFIXES) {
|
|
130
|
+
// 平台子包优先
|
|
131
|
+
if (suffix) {
|
|
132
|
+
const pkg = `${prefix}-${suffix}`;
|
|
133
|
+
try {
|
|
134
|
+
const root = path.dirname(require_.resolve(`${pkg}/package.json`));
|
|
135
|
+
for (const c of binCandidatesFromPkgRoot(root)) {
|
|
136
|
+
if (isExecutable(c)) {
|
|
137
|
+
cached = { ok: true, binPath: c, source: pkg };
|
|
138
|
+
logHit(c, pkg);
|
|
139
|
+
return cached;
|
|
140
|
+
}
|
|
141
|
+
tried.push(c);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
tried.push(`${pkg} (未安装)`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// 退化到主包:先从主包自身的依赖上下文找平台包(兼容 pnpm 严格布局),
|
|
149
|
+
// 最后按 package.json.bin 运行主包 shim。
|
|
150
|
+
try {
|
|
151
|
+
const manifestPath = require_.resolve(`${prefix}/package.json`);
|
|
152
|
+
const root = path.dirname(manifestPath);
|
|
153
|
+
if (suffix) {
|
|
154
|
+
const pkg = `${prefix}-${suffix}`;
|
|
155
|
+
try {
|
|
156
|
+
const requireFromMain = createRequire(manifestPath);
|
|
157
|
+
const platformRoot = path.dirname(requireFromMain.resolve(`${pkg}/package.json`));
|
|
158
|
+
for (const c of binCandidatesFromPkgRoot(platformRoot)) {
|
|
159
|
+
if (isExecutable(c)) {
|
|
160
|
+
cached = { ok: true, binPath: c, source: pkg };
|
|
161
|
+
logHit(c, pkg);
|
|
162
|
+
return cached;
|
|
163
|
+
}
|
|
164
|
+
tried.push(c);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
tried.push(`${pkg} (主包依赖中未找到)`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const candidates = [
|
|
172
|
+
...binCandidatesFromPackageManifest(root),
|
|
173
|
+
...binCandidatesFromPkgRoot(root),
|
|
174
|
+
];
|
|
175
|
+
for (const c of [...new Set(candidates)]) {
|
|
176
|
+
if (isExecutable(c)) {
|
|
177
|
+
cached = { ok: true, binPath: c, source: prefix };
|
|
178
|
+
logHit(c, prefix);
|
|
179
|
+
return cached;
|
|
180
|
+
}
|
|
181
|
+
tried.push(c);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
tried.push(`${prefix} (未安装)`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const result = { ok: false, tried };
|
|
189
|
+
console.warn(`${CLI_LOG} 未找到 wecom-cli 二进制,尝试过:\n ${tried.join("\n ")}`);
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
/** 仅用于测试:清除寻址缓存 */
|
|
193
|
+
export function resetLocateCache() {
|
|
194
|
+
cached = null;
|
|
195
|
+
logged.clear();
|
|
196
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { spawn } from "node:child_process";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { spawn } from "node:child_process";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CollectedOutput = {
|
|
2
|
+
text: string;
|
|
3
|
+
originalBytes: number;
|
|
4
|
+
truncated: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare class BoundedOutputCollector {
|
|
7
|
+
private readonly maxBytes;
|
|
8
|
+
private readonly keep;
|
|
9
|
+
private buffer;
|
|
10
|
+
private originalBytes;
|
|
11
|
+
constructor(maxBytes: number, keep?: "head" | "tail");
|
|
12
|
+
append(chunk: Buffer | string): void;
|
|
13
|
+
result(): CollectedOutput;
|
|
14
|
+
}
|