codewave-openclaw-installer 2.1.1 → 2.1.2

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 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. `channel-bootstrap`:检测到飞书或钉钉线索时,按官方入口自动安装并初始化
68
- 4. `extension-install`:将本包安装到 `~/.openclaw/extensions/codewave-toolkit/`
69
- 5. `postflight`:重启 OpenClaw Gateway,并运行统一检查输出最终状态
69
+ 3. `extension-install`:将本包安装到 `~/.openclaw/extensions/codewave-toolkit/`
70
+ 4. `postflight`:重启 OpenClaw Gateway,并运行统一检查输出最终状态
71
+ 5. `channel-bootstrap`:检测到飞书或钉钉线索时,按官方入口自动安装并初始化
70
72
 
71
73
  ## 目录结构
72
74
 
@@ -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,13 @@ 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
+ 'Lark.exe',
26
+ 'Programs/Feishu/Feishu.exe',
27
+ 'Programs/Lark/Lark.exe',
28
+ ],
29
+ desktopAppWindowsRegistryNames: ['Feishu', 'Lark', '飞书'],
21
30
  configPaths: [
22
31
  join(homedir(), '.openclaw', 'lark.json'),
23
32
  join(homedir(), '.openclaw', 'extensions', '@openclaw', 'feishu'),
@@ -35,6 +44,13 @@ export const CHANNEL_DEFINITIONS = {
35
44
  detectCommands: ['dws', 'dingtalk', 'dingtalk-workspace-cli'],
36
45
  runtimeCommands: ['dws', 'dingtalk', 'dingtalk-workspace-cli'],
37
46
  desktopAppCandidates: ['DingTalk.app', '钉钉.app'],
47
+ desktopAppWindowsCandidates: [
48
+ 'DingTalk.exe',
49
+ 'DingtalkLauncher.exe',
50
+ 'Programs/DingTalk/DingTalk.exe',
51
+ 'Programs/DingTalk/DingtalkLauncher.exe',
52
+ ],
53
+ desktopAppWindowsRegistryNames: ['DingTalk', 'Dingtalk', '钉钉'],
38
54
  configPaths: [
39
55
  join(homedir(), '.dws'),
40
56
  join(homedir(), '.dingtalk'),
@@ -56,6 +72,13 @@ export const CHANNEL_DEFINITIONS = {
56
72
  detectCommands: ['wecom-cli'],
57
73
  runtimeCommands: ['wecom-cli'],
58
74
  desktopAppCandidates: ['企业微信.app', 'WeCom.app', 'WeChatWork.app'],
75
+ desktopAppWindowsCandidates: [
76
+ 'WXWork.exe',
77
+ 'WeCom.exe',
78
+ 'Programs/WeCom/WeCom.exe',
79
+ 'Programs/WXWork/WXWork.exe',
80
+ ],
81
+ desktopAppWindowsRegistryNames: ['企业微信', 'WeCom', 'WeChat Work'],
59
82
  configPaths: [],
60
83
  envVars: [],
61
84
  installCommand: ['npx', ['-y', '@wecom/cli', 'init']],
@@ -70,9 +93,9 @@ export function createInstallContext(platform) {
70
93
  phases: [
71
94
  '环境预检',
72
95
  '核心依赖安装',
73
- '渠道绑定引导',
74
96
  'OpenClaw 扩展安装',
75
97
  '安装后校验',
98
+ '渠道绑定引导',
76
99
  '配置向导',
77
100
  ],
78
101
  results: {},
@@ -86,13 +109,33 @@ export function createInstallContext(platform) {
86
109
  };
87
110
  }
88
111
 
112
+ export function resolvePromptDefault({
113
+ defaultYes = true,
114
+ isTTY = true,
115
+ dryRun = false,
116
+ } = {}) {
117
+ if (dryRun) {
118
+ return defaultYes;
119
+ }
120
+
121
+ // Optional onboarding flows should not auto-start in non-interactive runs.
122
+ if (!isTTY) {
123
+ return false;
124
+ }
125
+
126
+ return defaultYes;
127
+ }
128
+
89
129
  export function evaluateChannelDetection(channelDefinition, detection) {
90
130
  const desktopApps = detection.desktopApps ?? [];
131
+ const hasDesktopApps = desktopApps.length > 0;
132
+ const hasConfigPaths = detection.configPaths.length > 0;
133
+ const hasEnvVars = detection.envVars.length > 0;
91
134
  const hintTags = [];
92
135
  if (detection.commands.length > 0) hintTags.push('命令入口');
93
- if (desktopApps.length > 0) hintTags.push('桌面客户端');
94
- if (detection.configPaths.length > 0) hintTags.push('本地配置');
95
- if (detection.envVars.length > 0) hintTags.push('环境变量');
136
+ if (hasDesktopApps) hintTags.push('桌面客户端');
137
+ if (hasConfigPaths) hintTags.push('本地配置');
138
+ if (hasEnvVars) hintTags.push('环境变量');
96
139
  const hintText = hintTags.join('、');
97
140
 
98
141
  if (detection.commands.length > 0) {
@@ -103,11 +146,14 @@ export function evaluateChannelDetection(channelDefinition, detection) {
103
146
  };
104
147
  }
105
148
 
106
- if (desktopApps.length > 0 || detection.configPaths.length > 0 || detection.envVars.length > 0) {
149
+ if (hasDesktopApps || hasConfigPaths || hasEnvVars) {
150
+ const onlySoftHints = !hasDesktopApps && (hasConfigPaths || hasEnvVars);
107
151
  return {
108
152
  status: 'hinted',
109
153
  source: 'existing-config',
110
- message: `检测到${channelDefinition.label}环境线索${hintText ? `(${hintText})` : ''}`,
154
+ message: onlySoftHints
155
+ ? `检测到${channelDefinition.label}配置线索${hintText ? `(${hintText})` : ''};未检测到桌面客户端或命令入口`
156
+ : `检测到${channelDefinition.label}环境线索${hintText ? `(${hintText})` : ''}`,
111
157
  };
112
158
  }
113
159
 
@@ -136,6 +182,82 @@ export function classifyPythonInstallFailure(stderr = '') {
136
182
  };
137
183
  }
138
184
 
185
+ function joinForPlatform(platform, root, candidate) {
186
+ if (platform === 'win32') {
187
+ return path.win32.join(root, candidate);
188
+ }
189
+ return path.posix.join(root, candidate);
190
+ }
191
+
192
+ function uniqueNonEmpty(values = []) {
193
+ return [...new Set(values.filter(Boolean))];
194
+ }
195
+
196
+ export function getDesktopSearchRoots({
197
+ platform = process.platform,
198
+ homeDir = homedir(),
199
+ env = process.env,
200
+ } = {}) {
201
+ if (platform === 'win32') {
202
+ return uniqueNonEmpty([
203
+ env.LOCALAPPDATA,
204
+ env.APPDATA,
205
+ env.ProgramFiles,
206
+ env['ProgramFiles(x86)'],
207
+ env.ProgramW6432,
208
+ path.win32.join(homeDir, 'AppData', 'Local'),
209
+ path.win32.join(homeDir, 'AppData', 'Roaming'),
210
+ 'C:\\Program Files',
211
+ 'C:\\Program Files (x86)',
212
+ ]);
213
+ }
214
+
215
+ return uniqueNonEmpty([
216
+ '/Applications',
217
+ path.posix.join(homeDir, 'Applications'),
218
+ ]);
219
+ }
220
+
221
+ export function getDesktopAppCandidates(channelDefinition, platform = process.platform) {
222
+ if (platform === 'win32') {
223
+ return channelDefinition.desktopAppWindowsCandidates ?? [];
224
+ }
225
+ return channelDefinition.desktopAppCandidates ?? [];
226
+ }
227
+
228
+ export function detectDesktopAppInstallMatches({
229
+ platform = process.platform,
230
+ homeDir = homedir(),
231
+ env = process.env,
232
+ fileCandidates = [],
233
+ registryDisplayNames = [],
234
+ installedRegistryDisplayNames = [],
235
+ exists = existsSync,
236
+ } = {}) {
237
+ const matches = [];
238
+ const roots = getDesktopSearchRoots({ platform, homeDir, env });
239
+
240
+ for (const candidate of fileCandidates) {
241
+ for (const root of roots) {
242
+ const candidatePath = joinForPlatform(platform, root, candidate);
243
+ if (exists(candidatePath)) {
244
+ matches.push(candidatePath);
245
+ }
246
+ }
247
+ }
248
+
249
+ if (platform === 'win32' && registryDisplayNames.length > 0) {
250
+ for (const displayName of installedRegistryDisplayNames) {
251
+ const normalized = displayName.toLowerCase();
252
+ if (registryDisplayNames.some((pattern) => normalized.includes(pattern.toLowerCase()))) {
253
+ matches.push(`registry:${displayName}`);
254
+ }
255
+ }
256
+ }
257
+
258
+ return uniqueNonEmpty(matches);
259
+ }
260
+
139
261
  export function buildRazelConfigBootstrap({
140
262
  razelConfig = {},
141
263
  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 findDesktopApps(appNames = []) {
92
- const roots = [
93
- '/Applications',
94
- join(homedir(), 'Applications'),
95
- ];
96
- const matches = [];
97
- for (const appName of appNames) {
98
- for (const root of roots) {
99
- const appPath = join(root, appName);
100
- if (existsSync(appPath)) {
101
- matches.push(appPath);
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
- return [...new Set(matches)];
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 detectDesktopClients() {
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: ['Feishu.app', 'Lark.app', '飞书.app'],
111
- dingtalk: ['DingTalk.app', '钉钉.app'],
112
- wecom: ['企业微信.app', 'WeCom.app', 'WeChatWork.app'],
256
+ feishu: CHANNEL_DEFINITIONS.lark,
257
+ dingtalk: CHANNEL_DEFINITIONS.dingtalk,
258
+ wecom: CHANNEL_DEFINITIONS.wecom,
113
259
  };
114
260
 
115
- return Object.fromEntries(
116
- Object.entries(candidates).map(([key, names]) => {
117
- const installedPaths = findDesktopApps(names);
118
- return [key, {
119
- installed: installedPaths.length > 0,
120
- installedPaths,
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 defaultYes;
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.desktopAppCandidates ?? []);
542
+ const desktopApps = await findDesktopApps(definition);
392
543
 
393
- const openclawConfigPath = join(homedir(), '.openclaw', 'openclaw.json');
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(`${openclawConfigPath}#channels.feishu`);
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(join(homedir(), '.openclaw', 'openclaw.json'));
595
+ const openclawConfig = readJson(OPENCLAW_CONFIG_PATH);
446
596
  const feishuConfig = openclawConfig?.channels?.feishu;
447
- const hasFeishuAccount = Boolean(
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 bootstrapChannel('lark', context);
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 runExtensionInstall(context);
990
+ await runPostflight(context);
817
991
 
818
992
  step(5, context.phases.length, context.phases[4]);
819
- await runPostflight(context);
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.1",
3
+ "version": "2.1.2",
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": "*"