codewave-openclaw-installer 2.1.1 → 2.1.3
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 +5 -3
- package/bin/check.mjs +65 -2
- package/bin/install-lib.mjs +136 -6
- package/bin/install.mjs +219 -43
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -55,18 +55,20 @@ pipx install razel-py-cli
|
|
|
55
55
|
说明:
|
|
56
56
|
|
|
57
57
|
- 安装策略采用“先复用,再补装,最后才重装”
|
|
58
|
+
- OpenClaw Extension 会优先安装并重启 gateway,确保 Skills 先可用;渠道绑定属于后续可选引导
|
|
58
59
|
- 如果用户机器上已经有旧的飞书入口、已有 Feishu 配置,或已经装过钉钉 `dws`,安装器会优先直接复用安装入口,但仍继续执行 `openclaw onboard` / `dws auth login`
|
|
59
60
|
- 飞书和钉钉都按各自官方入口安装,不强行统一成 `npm install -g xxx`
|
|
60
61
|
- 需要扫码或管理员授权时,安装器会直接进入对应初始化流程
|
|
61
62
|
- 未检测到渠道线索时,不会强行安装该渠道能力
|
|
63
|
+
- 在非交互环境下,渠道绑定默认跳过,避免阻塞 Skill 安装
|
|
62
64
|
|
|
63
65
|
## 工作原理
|
|
64
66
|
|
|
65
67
|
1. `preflight`:检测本机依赖、配置和渠道线索
|
|
66
68
|
2. `core-install`:安装并初始化 `razel-py-cli` / `razel-cli`
|
|
67
|
-
3. `
|
|
68
|
-
4. `
|
|
69
|
-
5. `
|
|
69
|
+
3. `extension-install`:将本包安装到 `~/.openclaw/extensions/codewave-toolkit/`
|
|
70
|
+
4. `postflight`:重启 OpenClaw Gateway,并运行统一检查输出最终状态
|
|
71
|
+
5. `channel-bootstrap`:检测到飞书或钉钉线索时,按官方入口自动安装并初始化
|
|
70
72
|
|
|
71
73
|
## 目录结构
|
|
72
74
|
|
package/bin/check.mjs
CHANGED
|
@@ -16,6 +16,11 @@ import { spawn } from 'node:child_process';
|
|
|
16
16
|
import { existsSync, readFileSync } from 'node:fs';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
18
|
import { homedir, platform } from 'node:os';
|
|
19
|
+
import {
|
|
20
|
+
CHANNEL_DEFINITIONS,
|
|
21
|
+
detectDesktopAppInstallMatches,
|
|
22
|
+
getDesktopAppCandidates,
|
|
23
|
+
} from './install-lib.mjs';
|
|
19
24
|
|
|
20
25
|
// ─── 颜色 ───
|
|
21
26
|
const NO_COLOR = process.env.NO_COLOR || process.argv.includes('--json');
|
|
@@ -32,6 +37,12 @@ const isMac = process.platform === 'darwin';
|
|
|
32
37
|
const isLinux = process.platform === 'linux';
|
|
33
38
|
const JSON_MODE = process.argv.includes('--json');
|
|
34
39
|
const FIX_MODE = process.argv.includes('--fix');
|
|
40
|
+
const WINDOWS_UNINSTALL_KEYS = [
|
|
41
|
+
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
42
|
+
'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
43
|
+
'HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
44
|
+
];
|
|
45
|
+
let windowsUninstallDisplayNamesPromise = null;
|
|
35
46
|
|
|
36
47
|
// ─── 工具函数 ───
|
|
37
48
|
|
|
@@ -86,6 +97,54 @@ function run(cmd, args) {
|
|
|
86
97
|
});
|
|
87
98
|
}
|
|
88
99
|
|
|
100
|
+
function runCapture(cmd, args) {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: isWin });
|
|
103
|
+
let stdout = '';
|
|
104
|
+
let stderr = '';
|
|
105
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
106
|
+
child.stderr?.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
107
|
+
child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
108
|
+
child.on('error', (err) => resolve({ code: 1, stdout, stderr: err.message }));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function getWindowsUninstallDisplayNames() {
|
|
113
|
+
if (!isWin) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
if (!windowsUninstallDisplayNamesPromise) {
|
|
117
|
+
windowsUninstallDisplayNamesPromise = (async () => {
|
|
118
|
+
const displayNames = [];
|
|
119
|
+
for (const registryKey of WINDOWS_UNINSTALL_KEYS) {
|
|
120
|
+
const result = await runCapture('reg', ['query', registryKey, '/s', '/v', 'DisplayName']);
|
|
121
|
+
const lines = `${result.stdout}\n${result.stderr}`.split(/\r?\n/);
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
const match = line.match(/^\s*DisplayName\s+REG_\w+\s+(.+?)\s*$/i);
|
|
124
|
+
if (match?.[1]) {
|
|
125
|
+
displayNames.push(match[1].trim());
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...new Set(displayNames)];
|
|
130
|
+
})();
|
|
131
|
+
}
|
|
132
|
+
return windowsUninstallDisplayNamesPromise;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function findDesktopApps(definition) {
|
|
136
|
+
const installedRegistryDisplayNames = await getWindowsUninstallDisplayNames();
|
|
137
|
+
return detectDesktopAppInstallMatches({
|
|
138
|
+
platform: process.platform,
|
|
139
|
+
homeDir: homedir(),
|
|
140
|
+
env: process.env,
|
|
141
|
+
fileCandidates: getDesktopAppCandidates(definition, process.platform),
|
|
142
|
+
registryDisplayNames: definition.desktopAppWindowsRegistryNames ?? [],
|
|
143
|
+
installedRegistryDisplayNames,
|
|
144
|
+
exists: existsSync,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
89
148
|
// ─── 检测项定义 ───
|
|
90
149
|
|
|
91
150
|
/** @typedef {{ name: string, status: 'ok'|'warn'|'fail', version?: string, message: string, fix?: string }} CheckResult */
|
|
@@ -267,9 +326,11 @@ async function checkChannel(name, configPaths, envVars, commands, options = {})
|
|
|
267
326
|
|
|
268
327
|
const configMatches = configPaths.filter(fileExists);
|
|
269
328
|
const envMatches = envVars.filter((key) => Boolean(process.env[key]));
|
|
329
|
+
const desktopMatches = options.definition ? await findDesktopApps(options.definition) : [];
|
|
270
330
|
|
|
271
331
|
const hints = [
|
|
272
332
|
...commandMatches.map((cmd) => `命令:${cmd}`),
|
|
333
|
+
...desktopMatches.map((path) => `桌面客户端:${path}`),
|
|
273
334
|
...configMatches.map((path) => `配置:${path}`),
|
|
274
335
|
...envMatches.map((key) => `环境变量:${key}`),
|
|
275
336
|
];
|
|
@@ -282,7 +343,7 @@ async function checkChannel(name, configPaths, envVars, commands, options = {})
|
|
|
282
343
|
};
|
|
283
344
|
}
|
|
284
345
|
|
|
285
|
-
if (configMatches.length > 0 || envMatches.length > 0) {
|
|
346
|
+
if (desktopMatches.length > 0 || configMatches.length > 0 || envMatches.length > 0) {
|
|
286
347
|
return {
|
|
287
348
|
name,
|
|
288
349
|
status: 'warn',
|
|
@@ -416,11 +477,12 @@ async function main() {
|
|
|
416
477
|
],
|
|
417
478
|
['LARK_APP_ID', 'LARK_APP_SECRET', 'FEISHU_APP_ID', 'FEISHU_APP_SECRET'],
|
|
418
479
|
[],
|
|
419
|
-
{ hintMessage: `检测到环境线索,可通过 openclaw 渠道引导继续 (${[
|
|
480
|
+
{ definition: CHANNEL_DEFINITIONS.lark, hintMessage: `检测到环境线索,可通过 openclaw 渠道引导继续 (${[
|
|
420
481
|
join(homedir(), '.openclaw', 'lark.json'),
|
|
421
482
|
join(homedir(), '.openclaw', 'extensions', '@openclaw', 'feishu'),
|
|
422
483
|
join(homedir(), '.openclaw', 'extensions', 'openclaw-lark'),
|
|
423
484
|
].filter(fileExists).map((path) => `配置:${path}`).concat(
|
|
485
|
+
await findDesktopApps(CHANNEL_DEFINITIONS.lark).then((matches) => matches.map((path) => `桌面客户端:${path}`)),
|
|
424
486
|
['LARK_APP_ID', 'LARK_APP_SECRET', 'FEISHU_APP_ID', 'FEISHU_APP_SECRET']
|
|
425
487
|
.filter((key) => Boolean(process.env[key]))
|
|
426
488
|
.map((key) => `环境变量:${key}`),
|
|
@@ -435,6 +497,7 @@ async function main() {
|
|
|
435
497
|
],
|
|
436
498
|
['DWS_CLIENT_ID', 'DWS_CLIENT_SECRET'],
|
|
437
499
|
['dws', 'dingtalk', 'dingtalk-workspace-cli'],
|
|
500
|
+
{ definition: CHANNEL_DEFINITIONS.dingtalk },
|
|
438
501
|
);
|
|
439
502
|
|
|
440
503
|
// 打印基础环境结果
|
package/bin/install-lib.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import { homedir } from 'node:os';
|
|
2
3
|
import { join } from 'node:path';
|
|
4
|
+
import * as path from 'node:path';
|
|
3
5
|
|
|
4
6
|
export const SKILL_SUMMARIES = [
|
|
5
7
|
'acceptance-doc-entry — 项目验收文档生成',
|
|
@@ -18,6 +20,21 @@ export const CHANNEL_DEFINITIONS = {
|
|
|
18
20
|
detectCommands: ['lark-cli'],
|
|
19
21
|
runtimeCommands: ['lark-cli', 'openclaw'],
|
|
20
22
|
desktopAppCandidates: ['Feishu.app', 'Lark.app', '飞书.app'],
|
|
23
|
+
desktopAppWindowsCandidates: [
|
|
24
|
+
'Feishu.exe',
|
|
25
|
+
'FeishuLauncher.exe',
|
|
26
|
+
'Feishu/Feishu.exe',
|
|
27
|
+
'Feishu/FeishuLauncher.exe',
|
|
28
|
+
'Lark.exe',
|
|
29
|
+
'LarkLauncher.exe',
|
|
30
|
+
'Lark/Lark.exe',
|
|
31
|
+
'Lark/LarkLauncher.exe',
|
|
32
|
+
'Programs/Feishu/Feishu.exe',
|
|
33
|
+
'Programs/Feishu/FeishuLauncher.exe',
|
|
34
|
+
'Programs/Lark/Lark.exe',
|
|
35
|
+
'Programs/Lark/LarkLauncher.exe',
|
|
36
|
+
],
|
|
37
|
+
desktopAppWindowsRegistryNames: ['Feishu', 'Lark', '飞书'],
|
|
21
38
|
configPaths: [
|
|
22
39
|
join(homedir(), '.openclaw', 'lark.json'),
|
|
23
40
|
join(homedir(), '.openclaw', 'extensions', '@openclaw', 'feishu'),
|
|
@@ -35,6 +52,13 @@ export const CHANNEL_DEFINITIONS = {
|
|
|
35
52
|
detectCommands: ['dws', 'dingtalk', 'dingtalk-workspace-cli'],
|
|
36
53
|
runtimeCommands: ['dws', 'dingtalk', 'dingtalk-workspace-cli'],
|
|
37
54
|
desktopAppCandidates: ['DingTalk.app', '钉钉.app'],
|
|
55
|
+
desktopAppWindowsCandidates: [
|
|
56
|
+
'DingTalk.exe',
|
|
57
|
+
'DingtalkLauncher.exe',
|
|
58
|
+
'Programs/DingTalk/DingTalk.exe',
|
|
59
|
+
'Programs/DingTalk/DingtalkLauncher.exe',
|
|
60
|
+
],
|
|
61
|
+
desktopAppWindowsRegistryNames: ['DingTalk', 'Dingtalk', '钉钉'],
|
|
38
62
|
configPaths: [
|
|
39
63
|
join(homedir(), '.dws'),
|
|
40
64
|
join(homedir(), '.dingtalk'),
|
|
@@ -56,6 +80,13 @@ export const CHANNEL_DEFINITIONS = {
|
|
|
56
80
|
detectCommands: ['wecom-cli'],
|
|
57
81
|
runtimeCommands: ['wecom-cli'],
|
|
58
82
|
desktopAppCandidates: ['企业微信.app', 'WeCom.app', 'WeChatWork.app'],
|
|
83
|
+
desktopAppWindowsCandidates: [
|
|
84
|
+
'WXWork.exe',
|
|
85
|
+
'WeCom.exe',
|
|
86
|
+
'Programs/WeCom/WeCom.exe',
|
|
87
|
+
'Programs/WXWork/WXWork.exe',
|
|
88
|
+
],
|
|
89
|
+
desktopAppWindowsRegistryNames: ['企业微信', 'WeCom', 'WeChat Work'],
|
|
59
90
|
configPaths: [],
|
|
60
91
|
envVars: [],
|
|
61
92
|
installCommand: ['npx', ['-y', '@wecom/cli', 'init']],
|
|
@@ -70,9 +101,9 @@ export function createInstallContext(platform) {
|
|
|
70
101
|
phases: [
|
|
71
102
|
'环境预检',
|
|
72
103
|
'核心依赖安装',
|
|
73
|
-
'渠道绑定引导',
|
|
74
104
|
'OpenClaw 扩展安装',
|
|
75
105
|
'安装后校验',
|
|
106
|
+
'渠道绑定引导',
|
|
76
107
|
'配置向导',
|
|
77
108
|
],
|
|
78
109
|
results: {},
|
|
@@ -86,13 +117,33 @@ export function createInstallContext(platform) {
|
|
|
86
117
|
};
|
|
87
118
|
}
|
|
88
119
|
|
|
120
|
+
export function resolvePromptDefault({
|
|
121
|
+
defaultYes = true,
|
|
122
|
+
isTTY = true,
|
|
123
|
+
dryRun = false,
|
|
124
|
+
} = {}) {
|
|
125
|
+
if (dryRun) {
|
|
126
|
+
return defaultYes;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Optional onboarding flows should not auto-start in non-interactive runs.
|
|
130
|
+
if (!isTTY) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return defaultYes;
|
|
135
|
+
}
|
|
136
|
+
|
|
89
137
|
export function evaluateChannelDetection(channelDefinition, detection) {
|
|
90
138
|
const desktopApps = detection.desktopApps ?? [];
|
|
139
|
+
const hasDesktopApps = desktopApps.length > 0;
|
|
140
|
+
const hasConfigPaths = detection.configPaths.length > 0;
|
|
141
|
+
const hasEnvVars = detection.envVars.length > 0;
|
|
91
142
|
const hintTags = [];
|
|
92
143
|
if (detection.commands.length > 0) hintTags.push('命令入口');
|
|
93
|
-
if (
|
|
94
|
-
if (
|
|
95
|
-
if (
|
|
144
|
+
if (hasDesktopApps) hintTags.push('桌面客户端');
|
|
145
|
+
if (hasConfigPaths) hintTags.push('本地配置');
|
|
146
|
+
if (hasEnvVars) hintTags.push('环境变量');
|
|
96
147
|
const hintText = hintTags.join('、');
|
|
97
148
|
|
|
98
149
|
if (detection.commands.length > 0) {
|
|
@@ -103,11 +154,14 @@ export function evaluateChannelDetection(channelDefinition, detection) {
|
|
|
103
154
|
};
|
|
104
155
|
}
|
|
105
156
|
|
|
106
|
-
if (
|
|
157
|
+
if (hasDesktopApps || hasConfigPaths || hasEnvVars) {
|
|
158
|
+
const onlySoftHints = !hasDesktopApps && (hasConfigPaths || hasEnvVars);
|
|
107
159
|
return {
|
|
108
160
|
status: 'hinted',
|
|
109
161
|
source: 'existing-config',
|
|
110
|
-
message:
|
|
162
|
+
message: onlySoftHints
|
|
163
|
+
? `检测到${channelDefinition.label}配置线索${hintText ? `(${hintText})` : ''};未检测到桌面客户端或命令入口`
|
|
164
|
+
: `检测到${channelDefinition.label}环境线索${hintText ? `(${hintText})` : ''}`,
|
|
111
165
|
};
|
|
112
166
|
}
|
|
113
167
|
|
|
@@ -136,6 +190,82 @@ export function classifyPythonInstallFailure(stderr = '') {
|
|
|
136
190
|
};
|
|
137
191
|
}
|
|
138
192
|
|
|
193
|
+
function joinForPlatform(platform, root, candidate) {
|
|
194
|
+
if (platform === 'win32') {
|
|
195
|
+
return path.win32.join(root, candidate);
|
|
196
|
+
}
|
|
197
|
+
return path.posix.join(root, candidate);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function uniqueNonEmpty(values = []) {
|
|
201
|
+
return [...new Set(values.filter(Boolean))];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getDesktopSearchRoots({
|
|
205
|
+
platform = process.platform,
|
|
206
|
+
homeDir = homedir(),
|
|
207
|
+
env = process.env,
|
|
208
|
+
} = {}) {
|
|
209
|
+
if (platform === 'win32') {
|
|
210
|
+
return uniqueNonEmpty([
|
|
211
|
+
env.LOCALAPPDATA,
|
|
212
|
+
env.APPDATA,
|
|
213
|
+
env.ProgramFiles,
|
|
214
|
+
env['ProgramFiles(x86)'],
|
|
215
|
+
env.ProgramW6432,
|
|
216
|
+
path.win32.join(homeDir, 'AppData', 'Local'),
|
|
217
|
+
path.win32.join(homeDir, 'AppData', 'Roaming'),
|
|
218
|
+
'C:\\Program Files',
|
|
219
|
+
'C:\\Program Files (x86)',
|
|
220
|
+
]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return uniqueNonEmpty([
|
|
224
|
+
'/Applications',
|
|
225
|
+
path.posix.join(homeDir, 'Applications'),
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function getDesktopAppCandidates(channelDefinition, platform = process.platform) {
|
|
230
|
+
if (platform === 'win32') {
|
|
231
|
+
return channelDefinition.desktopAppWindowsCandidates ?? [];
|
|
232
|
+
}
|
|
233
|
+
return channelDefinition.desktopAppCandidates ?? [];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function detectDesktopAppInstallMatches({
|
|
237
|
+
platform = process.platform,
|
|
238
|
+
homeDir = homedir(),
|
|
239
|
+
env = process.env,
|
|
240
|
+
fileCandidates = [],
|
|
241
|
+
registryDisplayNames = [],
|
|
242
|
+
installedRegistryDisplayNames = [],
|
|
243
|
+
exists = existsSync,
|
|
244
|
+
} = {}) {
|
|
245
|
+
const matches = [];
|
|
246
|
+
const roots = getDesktopSearchRoots({ platform, homeDir, env });
|
|
247
|
+
|
|
248
|
+
for (const candidate of fileCandidates) {
|
|
249
|
+
for (const root of roots) {
|
|
250
|
+
const candidatePath = joinForPlatform(platform, root, candidate);
|
|
251
|
+
if (exists(candidatePath)) {
|
|
252
|
+
matches.push(candidatePath);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (platform === 'win32' && registryDisplayNames.length > 0) {
|
|
258
|
+
for (const displayName of installedRegistryDisplayNames) {
|
|
259
|
+
const normalized = displayName.toLowerCase();
|
|
260
|
+
if (registryDisplayNames.some((pattern) => normalized.includes(pattern.toLowerCase()))) {
|
|
261
|
+
matches.push(`registry:${displayName}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return uniqueNonEmpty(matches);
|
|
267
|
+
}
|
|
268
|
+
|
|
139
269
|
export function buildRazelConfigBootstrap({
|
|
140
270
|
razelConfig = {},
|
|
141
271
|
env = process.env,
|
package/bin/install.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, unlinkSync } from 'node:fs';
|
|
4
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, resolve, join } from 'node:path';
|
|
6
6
|
import * as path from 'node:path';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
buildRazelConfigBootstrap,
|
|
15
15
|
classifyPythonInstallFailure,
|
|
16
16
|
createInstallContext,
|
|
17
|
+
detectDesktopAppInstallMatches,
|
|
17
18
|
evaluateChannelDetection,
|
|
19
|
+
getDesktopAppCandidates,
|
|
20
|
+
resolvePromptDefault,
|
|
18
21
|
shouldDryRun,
|
|
19
22
|
} from './install-lib.mjs';
|
|
20
23
|
|
|
@@ -33,6 +36,13 @@ const isWin = process.platform === 'win32';
|
|
|
33
36
|
const NPM = 'npm';
|
|
34
37
|
const PIP = isWin ? 'pip' : 'pip3';
|
|
35
38
|
const DRY_RUN = shouldDryRun(process.argv);
|
|
39
|
+
const OPENCLAW_CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json');
|
|
40
|
+
const WINDOWS_UNINSTALL_KEYS = [
|
|
41
|
+
'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
42
|
+
'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
43
|
+
'HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
|
|
44
|
+
];
|
|
45
|
+
let windowsUninstallDisplayNamesPromise = null;
|
|
36
46
|
|
|
37
47
|
function step(index, total, title) {
|
|
38
48
|
console.log('');
|
|
@@ -88,44 +98,185 @@ function readJson(filePath) {
|
|
|
88
98
|
}
|
|
89
99
|
}
|
|
90
100
|
|
|
91
|
-
function
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
function stripFeishuAccounts(section = {}) {
|
|
102
|
+
const { accounts: _accountsIgnored, ...base } = section;
|
|
103
|
+
return base;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function hasFeishuCredentials(config = {}) {
|
|
107
|
+
return Boolean(config?.appId && config?.appSecret);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function hasAnyFeishuAccount(section = {}) {
|
|
111
|
+
if (hasFeishuCredentials(section)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
const accounts = section?.accounts;
|
|
115
|
+
if (!accounts || typeof accounts !== 'object') {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return Object.values(accounts).some((account) => hasFeishuCredentials(account));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isSameFeishuCredentials(a = {}, b = {}) {
|
|
122
|
+
if (!hasFeishuCredentials(a) || !hasFeishuCredentials(b)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return a.appId === b.appId && a.appSecret === b.appSecret;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ensureUniqueAccountId(accounts, preferredId) {
|
|
129
|
+
const base = (preferredId || 'business').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '') || 'business';
|
|
130
|
+
if (!accounts[base]) {
|
|
131
|
+
return base;
|
|
132
|
+
}
|
|
133
|
+
let idx = 2;
|
|
134
|
+
while (accounts[`${base}-${idx}`]) {
|
|
135
|
+
idx += 1;
|
|
136
|
+
}
|
|
137
|
+
return `${base}-${idx}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function pickLegacyAccountId(config = {}) {
|
|
141
|
+
if (typeof config.name === 'string' && config.name.trim()) {
|
|
142
|
+
return config.name.trim();
|
|
143
|
+
}
|
|
144
|
+
if (typeof config.appId === 'string' && config.appId.trim()) {
|
|
145
|
+
const tail = config.appId.trim().slice(-6);
|
|
146
|
+
return `legacy-${tail}`;
|
|
147
|
+
}
|
|
148
|
+
return 'legacy-business';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function mergeFeishuBindings(beforeSection = {}, afterSection = {}) {
|
|
152
|
+
const beforeBase = stripFeishuAccounts(beforeSection);
|
|
153
|
+
const afterBase = stripFeishuAccounts(afterSection);
|
|
154
|
+
const mergedAccounts = { ...(afterSection.accounts || {}) };
|
|
155
|
+
let addedCount = 0;
|
|
156
|
+
|
|
157
|
+
const accountExistsByCred = (candidate = {}) => {
|
|
158
|
+
if (isSameFeishuCredentials(afterBase, candidate)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
return Object.values(mergedAccounts).some((account) => isSameFeishuCredentials(account, candidate));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
for (const [accountId, accountConfig] of Object.entries(beforeSection.accounts || {})) {
|
|
165
|
+
if (!hasFeishuCredentials(accountConfig) || accountExistsByCred(accountConfig)) {
|
|
166
|
+
continue;
|
|
103
167
|
}
|
|
168
|
+
const targetId = ensureUniqueAccountId(mergedAccounts, accountId);
|
|
169
|
+
mergedAccounts[targetId] = { ...accountConfig, enabled: accountConfig.enabled ?? true };
|
|
170
|
+
addedCount += 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (hasFeishuCredentials(beforeBase) && !accountExistsByCred(beforeBase)) {
|
|
174
|
+
const targetId = ensureUniqueAccountId(mergedAccounts, pickLegacyAccountId(beforeBase));
|
|
175
|
+
mergedAccounts[targetId] = { ...beforeBase, enabled: beforeBase.enabled ?? true };
|
|
176
|
+
addedCount += 1;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const mergedSection = { ...afterBase };
|
|
180
|
+
if (Object.keys(mergedAccounts).length > 0) {
|
|
181
|
+
mergedSection.accounts = mergedAccounts;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
mergedSection,
|
|
186
|
+
addedCount,
|
|
187
|
+
changed: addedCount > 0,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function preserveFeishuBindings(beforeSection) {
|
|
192
|
+
if (!beforeSection || DRY_RUN) {
|
|
193
|
+
return { changed: false, addedCount: 0 };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const config = readJson(OPENCLAW_CONFIG_PATH);
|
|
197
|
+
const afterSection = config?.channels?.feishu;
|
|
198
|
+
if (!afterSection) {
|
|
199
|
+
return { changed: false, addedCount: 0 };
|
|
104
200
|
}
|
|
105
|
-
|
|
201
|
+
|
|
202
|
+
const merged = mergeFeishuBindings(beforeSection, afterSection);
|
|
203
|
+
if (!merged.changed) {
|
|
204
|
+
return { changed: false, addedCount: 0 };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const nextConfig = {
|
|
208
|
+
...config,
|
|
209
|
+
channels: {
|
|
210
|
+
...(config.channels || {}),
|
|
211
|
+
feishu: merged.mergedSection,
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
writeFileSync(OPENCLAW_CONFIG_PATH, `${JSON.stringify(nextConfig, null, 2)}\n`, 'utf8');
|
|
215
|
+
return { changed: true, addedCount: merged.addedCount };
|
|
106
216
|
}
|
|
107
217
|
|
|
108
|
-
function
|
|
218
|
+
async function getWindowsUninstallDisplayNames() {
|
|
219
|
+
if (!isWin) {
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
if (!windowsUninstallDisplayNamesPromise) {
|
|
223
|
+
windowsUninstallDisplayNamesPromise = (async () => {
|
|
224
|
+
const displayNames = [];
|
|
225
|
+
for (const registryKey of WINDOWS_UNINSTALL_KEYS) {
|
|
226
|
+
const result = await runCapture('reg', ['query', registryKey, '/s', '/v', 'DisplayName']);
|
|
227
|
+
const lines = `${result.stdout}\n${result.stderr}`.split(/\r?\n/);
|
|
228
|
+
for (const line of lines) {
|
|
229
|
+
const match = line.match(/^\s*DisplayName\s+REG_\w+\s+(.+?)\s*$/i);
|
|
230
|
+
if (match?.[1]) {
|
|
231
|
+
displayNames.push(match[1].trim());
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return [...new Set(displayNames)];
|
|
236
|
+
})();
|
|
237
|
+
}
|
|
238
|
+
return windowsUninstallDisplayNamesPromise;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function findDesktopApps(definition) {
|
|
242
|
+
const installedRegistryDisplayNames = await getWindowsUninstallDisplayNames();
|
|
243
|
+
return detectDesktopAppInstallMatches({
|
|
244
|
+
platform: process.platform,
|
|
245
|
+
homeDir: homedir(),
|
|
246
|
+
env: process.env,
|
|
247
|
+
fileCandidates: getDesktopAppCandidates(definition, process.platform),
|
|
248
|
+
registryDisplayNames: definition.desktopAppWindowsRegistryNames ?? [],
|
|
249
|
+
installedRegistryDisplayNames,
|
|
250
|
+
exists: existsSync,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function detectDesktopClients() {
|
|
109
255
|
const candidates = {
|
|
110
|
-
feishu:
|
|
111
|
-
dingtalk:
|
|
112
|
-
wecom:
|
|
256
|
+
feishu: CHANNEL_DEFINITIONS.lark,
|
|
257
|
+
dingtalk: CHANNEL_DEFINITIONS.dingtalk,
|
|
258
|
+
wecom: CHANNEL_DEFINITIONS.wecom,
|
|
113
259
|
};
|
|
114
260
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
261
|
+
const entries = [];
|
|
262
|
+
for (const [key, definition] of Object.entries(candidates)) {
|
|
263
|
+
const installedPaths = await findDesktopApps(definition);
|
|
264
|
+
entries.push([key, {
|
|
265
|
+
installed: installedPaths.length > 0,
|
|
266
|
+
installedPaths,
|
|
267
|
+
}]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return Object.fromEntries(entries);
|
|
124
271
|
}
|
|
125
272
|
|
|
126
273
|
async function confirmPrompt(message, defaultYes = true) {
|
|
127
274
|
if (DRY_RUN || !process.stdin.isTTY) {
|
|
128
|
-
return
|
|
275
|
+
return resolvePromptDefault({
|
|
276
|
+
defaultYes,
|
|
277
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
278
|
+
dryRun: DRY_RUN,
|
|
279
|
+
});
|
|
129
280
|
}
|
|
130
281
|
|
|
131
282
|
const rl = createInterface({
|
|
@@ -388,12 +539,11 @@ async function inspectChannel(definition) {
|
|
|
388
539
|
|
|
389
540
|
const configPaths = definition.configPaths.filter((configPath) => existsSync(configPath));
|
|
390
541
|
const envVars = definition.envVars.filter((key) => Boolean(process.env[key]));
|
|
391
|
-
const desktopApps = findDesktopApps(definition
|
|
542
|
+
const desktopApps = await findDesktopApps(definition);
|
|
392
543
|
|
|
393
|
-
const
|
|
394
|
-
const openclawConfig = readJson(openclawConfigPath);
|
|
544
|
+
const openclawConfig = readJson(OPENCLAW_CONFIG_PATH);
|
|
395
545
|
if ((definition.label === '飞书' || definition.label === 'Lark') && openclawConfig?.channels?.feishu) {
|
|
396
|
-
configPaths.push(`${
|
|
546
|
+
configPaths.push(`${OPENCLAW_CONFIG_PATH}#channels.feishu`);
|
|
397
547
|
}
|
|
398
548
|
|
|
399
549
|
return { commands, configPaths, envVars, desktopApps };
|
|
@@ -442,11 +592,9 @@ async function inferChannelActivation(channelKey) {
|
|
|
442
592
|
const hasRuntime = Boolean(runtimeCommand);
|
|
443
593
|
|
|
444
594
|
if (channelKey === 'lark') {
|
|
445
|
-
const openclawConfig = readJson(
|
|
595
|
+
const openclawConfig = readJson(OPENCLAW_CONFIG_PATH);
|
|
446
596
|
const feishuConfig = openclawConfig?.channels?.feishu;
|
|
447
|
-
const hasFeishuAccount =
|
|
448
|
-
feishuConfig?.enabled && (feishuConfig?.appId || feishuConfig?.accounts?.main?.appId),
|
|
449
|
-
);
|
|
597
|
+
const hasFeishuAccount = hasAnyFeishuAccount(feishuConfig);
|
|
450
598
|
|
|
451
599
|
if (hasRuntime && hasFeishuAccount) {
|
|
452
600
|
return {
|
|
@@ -525,6 +673,7 @@ async function bootstrapChannel(channelKey, context) {
|
|
|
525
673
|
}
|
|
526
674
|
|
|
527
675
|
if (channelKey === 'lark') {
|
|
676
|
+
const beforeFeishuSection = readJson(OPENCLAW_CONFIG_PATH)?.channels?.feishu ?? null;
|
|
528
677
|
const [installCmd, installArgs] = definition.installCommand;
|
|
529
678
|
console.log(dim(` 按官方流程执行 ${definition.label} 绑定(二维码创建/关联机器人)...`));
|
|
530
679
|
const installCode = await run(installCmd, installArgs);
|
|
@@ -537,13 +686,40 @@ async function bootstrapChannel(channelKey, context) {
|
|
|
537
686
|
return;
|
|
538
687
|
}
|
|
539
688
|
|
|
689
|
+
const preserved = preserveFeishuBindings(beforeFeishuSection);
|
|
540
690
|
const activated = await inferChannelActivation(channelKey);
|
|
691
|
+
const preserveNote = preserved.addedCount > 0
|
|
692
|
+
? `;已保留 ${preserved.addedCount} 个历史业务绑定`
|
|
693
|
+
: '';
|
|
541
694
|
context.channels[channelKey] = {
|
|
542
695
|
status: DRY_RUN ? 'would-enter-binding' : 'binding-flow-entered',
|
|
543
696
|
source: detection.source,
|
|
544
697
|
message: DRY_RUN
|
|
545
698
|
? `${definition.label} 将执行官方绑定流程 (${installCmd} ${installArgs.join(' ')})`
|
|
546
|
-
: `${definition.label} 已执行官方绑定流程 (${installCmd} ${installArgs.join(' ')});${activated.message}`,
|
|
699
|
+
: `${definition.label} 已执行官方绑定流程 (${installCmd} ${installArgs.join(' ')});${activated.message}${preserveNote}`,
|
|
700
|
+
};
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (channelKey === 'wecom') {
|
|
705
|
+
const [installCmd, installArgs] = definition.installCommand;
|
|
706
|
+
console.log(dim(` 按官方流程执行 ${definition.label} 绑定(创建/关联机器人)...`));
|
|
707
|
+
const installCode = await run(installCmd, installArgs);
|
|
708
|
+
if (installCode !== 0) {
|
|
709
|
+
context.channels[channelKey] = {
|
|
710
|
+
status: 'failed',
|
|
711
|
+
source: detection.source,
|
|
712
|
+
message: `${definition.label} 绑定流程失败(${installCmd} ${installArgs.join(' ')})`,
|
|
713
|
+
};
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
context.channels[channelKey] = {
|
|
718
|
+
status: DRY_RUN ? 'would-enter-binding' : 'binding-flow-entered',
|
|
719
|
+
source: detection.source,
|
|
720
|
+
message: DRY_RUN
|
|
721
|
+
? `${definition.label} 将执行官方绑定流程 (${installCmd} ${installArgs.join(' ')})`
|
|
722
|
+
: `${definition.label} 已执行官方绑定流程 (${installCmd} ${installArgs.join(' ')})`,
|
|
547
723
|
};
|
|
548
724
|
return;
|
|
549
725
|
}
|
|
@@ -626,7 +802,7 @@ async function runPreflight(context) {
|
|
|
626
802
|
context.channels.lark = await detectChannel(CHANNEL_DEFINITIONS.lark);
|
|
627
803
|
context.channels.dingtalk = await detectChannel(CHANNEL_DEFINITIONS.dingtalk);
|
|
628
804
|
context.channels.wecom = await detectChannel(CHANNEL_DEFINITIONS.wecom);
|
|
629
|
-
context.desktopClients = detectDesktopClients();
|
|
805
|
+
context.desktopClients = await detectDesktopClients();
|
|
630
806
|
|
|
631
807
|
console.log(dim(` 飞书: ${context.channels.lark.message}`));
|
|
632
808
|
console.log(dim(` 钉钉: ${context.channels.dingtalk.message}`));
|
|
@@ -808,15 +984,15 @@ async function main() {
|
|
|
808
984
|
await runCoreInstall(context);
|
|
809
985
|
|
|
810
986
|
step(3, context.phases.length, context.phases[2]);
|
|
811
|
-
await
|
|
812
|
-
await bootstrapChannel('dingtalk', context);
|
|
813
|
-
await bootstrapChannel('wecom', context);
|
|
987
|
+
await runExtensionInstall(context);
|
|
814
988
|
|
|
815
989
|
step(4, context.phases.length, context.phases[3]);
|
|
816
|
-
await
|
|
990
|
+
await runPostflight(context);
|
|
817
991
|
|
|
818
992
|
step(5, context.phases.length, context.phases[4]);
|
|
819
|
-
await
|
|
993
|
+
await bootstrapChannel('lark', context);
|
|
994
|
+
await bootstrapChannel('dingtalk', context);
|
|
995
|
+
await bootstrapChannel('wecom', context);
|
|
820
996
|
|
|
821
997
|
step(6, context.phases.length, context.phases[5]);
|
|
822
998
|
await runConfigBootstrap(context);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codewave-openclaw-installer",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"description": "网易智企 CodeWave OpenClaw 扩展:Skills + CLI 依赖一键安装",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
"index.js",
|
|
13
13
|
"bin/",
|
|
14
14
|
"skills/",
|
|
15
|
-
"openclaw.plugin.json"
|
|
16
|
-
"README.md"
|
|
15
|
+
"openclaw.plugin.json"
|
|
17
16
|
],
|
|
18
17
|
"peerDependencies": {
|
|
19
18
|
"openclaw": "*"
|