@workclaw/cli 1.0.319 → 1.0.321
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 +16 -17
- package/dist/box/installer/installer.d.ts.map +1 -1
- package/dist/box/src/box.d.ts +4 -0
- package/dist/box/src/box.d.ts.map +1 -1
- package/dist/box/types/index.d.ts +19 -10
- package/dist/box/types/index.d.ts.map +1 -1
- package/dist/{index-DU1f2pnd.js → index-DsyMquTQ.js} +294 -248
- package/dist/index.js +1 -1
- package/dist/local/apis/index.d.ts +10 -9
- package/dist/local/apis/index.d.ts.map +1 -1
- package/dist/local/installer/installer.d.ts +36 -1
- package/dist/local/installer/installer.d.ts.map +1 -1
- package/dist/local/src/local.d.ts +4 -0
- package/dist/local/src/local.d.ts.map +1 -1
- package/dist/local/types/index.d.ts +31 -18
- package/dist/local/types/index.d.ts.map +1 -1
- package/dist/local/utils/index.d.ts +2 -1
- package/dist/local/utils/index.d.ts.map +1 -1
- package/dist/local/utils/machine-id.d.ts +10 -0
- package/dist/local/utils/machine-id.d.ts.map +1 -0
- package/dist/local/utils/path.d.ts +0 -6
- package/dist/local/utils/path.d.ts.map +1 -1
- package/dist/shared/utils/debug.d.ts +11 -2
- package/dist/shared/utils/debug.d.ts.map +1 -1
- package/dist/shared/utils/env.d.ts +6 -0
- package/dist/shared/utils/env.d.ts.map +1 -0
- package/dist/shared/utils/index.d.ts +2 -1
- package/dist/shared/utils/index.d.ts.map +1 -1
- package/dist/shared/utils/path.d.ts.map +1 -1
- package/dist/shared/utils/validate.d.ts +80 -0
- package/dist/shared/utils/validate.d.ts.map +1 -1
- package/dist/utils/config.d.ts +12 -5
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/http.d.ts +4 -13
- package/dist/utils/http.d.ts.map +1 -1
- package/dist/utils/index.d.ts +0 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +2 -1
- package/dist/utils/path.d.ts +0 -23
- package/dist/utils/path.d.ts.map +0 -1
|
@@ -3,47 +3,41 @@ import path, { resolve, dirname } from "node:path";
|
|
|
3
3
|
import process$1 from "node:process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import { exec, execSync } from "node:child_process";
|
|
7
6
|
import boxen from "boxen";
|
|
8
7
|
import chalk from "chalk";
|
|
9
8
|
import inquirer from "inquirer";
|
|
9
|
+
import { Debug } from "@mingto/debug";
|
|
10
|
+
import { execSync, exec } from "node:child_process";
|
|
10
11
|
import semver from "semver";
|
|
11
12
|
import { z as z$1 } from "zod";
|
|
12
13
|
import fs from "node:fs/promises";
|
|
13
|
-
import tar from "tar";
|
|
14
14
|
import ora from "ora";
|
|
15
|
+
import tar from "tar";
|
|
15
16
|
import axios from "axios";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import crypto from "node:crypto";
|
|
19
|
+
const debug = new Debug();
|
|
20
|
+
function setDebug(enabled) {
|
|
21
|
+
if (enabled) {
|
|
22
|
+
debug.enable();
|
|
23
|
+
} else {
|
|
24
|
+
debug.disable();
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
|
-
function
|
|
26
|
-
debugLog(`[路径验证] 开始验证路径: ${openclawPath}`);
|
|
27
|
-
if (!path.isAbsolute(openclawPath)) {
|
|
28
|
-
debugLog(`[路径验证] 失败: 路径不是绝对路径`);
|
|
29
|
-
throw new Error(`路径必须是绝对路径,当前输入: ${openclawPath}`);
|
|
30
|
-
}
|
|
31
|
-
const invalidChars = /[<>:"|?*]/.test(openclawPath);
|
|
32
|
-
if (invalidChars) {
|
|
33
|
-
debugLog(`[路径验证] 失败: 路径包含特殊字符`);
|
|
34
|
-
throw new Error('路径不能包含特殊字符: < > : " | ? *');
|
|
35
|
-
}
|
|
36
|
-
debugLog(`[路径验证] 通过: ${openclawPath}`);
|
|
27
|
+
function debugLog(...args) {
|
|
37
28
|
}
|
|
38
|
-
const ipv4Schema = z$1.ipv4({
|
|
39
|
-
message: "请输入正确的 IP 地址格式,例如: 192.168.1.100"
|
|
40
|
-
});
|
|
41
29
|
const ERROR_CODES$1 = {
|
|
42
|
-
|
|
43
|
-
|
|
30
|
+
PHONE_REQUIRED: "PHONE_REQUIRED",
|
|
31
|
+
USER_PASS_REQUIRED: "USER_PASS_REQUIRED",
|
|
32
|
+
LOGIN_FAILED: "LOGIN_FAILED",
|
|
33
|
+
GET_BOUND_CONFIG_FAILED: "GET_BOUND_CONFIG_FAILED",
|
|
44
34
|
NODE_VERSION_LOW: "NODE_VERSION_LOW",
|
|
35
|
+
NODE_NOT_FOUND: "NODE_NOT_FOUND",
|
|
45
36
|
NPM_NOT_FOUND: "NPM_NOT_FOUND",
|
|
46
37
|
NPM_INSTALL_FAILED: "NPM_INSTALL_FAILED",
|
|
38
|
+
CONFIG_WRITE_FAILED: "CONFIG_WRITE_FAILED",
|
|
39
|
+
HTTP_ERROR: "HTTP_ERROR",
|
|
40
|
+
NETWORK_ERROR: "NETWORK_ERROR",
|
|
47
41
|
INVALID_OPENCLAW_PATH: "INVALID_OPENCLAW_PATH",
|
|
48
42
|
INVALID_ARGUMENT: "INVALID_ARGUMENT"
|
|
49
43
|
};
|
|
@@ -54,6 +48,64 @@ let AppError$1 = class AppError extends Error {
|
|
|
54
48
|
this.name = "AppError";
|
|
55
49
|
}
|
|
56
50
|
};
|
|
51
|
+
function checkEnv() {
|
|
52
|
+
const nodeVersion = execSync("node --version", { stdio: "pipe" }).toString().trim();
|
|
53
|
+
if (!semver.gte(nodeVersion, "18.0.0")) {
|
|
54
|
+
throw new AppError$1(ERROR_CODES$1.NODE_VERSION_LOW, `Node.js 版本需要 >= 18.0.0,当前版本: ${nodeVersion}`);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const npmVersion = execSync("npm --version", { stdio: "pipe" }).toString().trim();
|
|
58
|
+
debugLog(`[环境检查] npm 版本: ${npmVersion}`);
|
|
59
|
+
debugLog("[环境检查] npm 检测通过");
|
|
60
|
+
} catch {
|
|
61
|
+
throw new AppError$1(ERROR_CODES$1.NPM_NOT_FOUND, "未检测到 npm,请先安装 Node.js 和 npm");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const EnvironmentSchema = z$1.enum(["test", "prod", "custom"], {
|
|
65
|
+
message: "环境类型必须是 test、prod 或 custom"
|
|
66
|
+
});
|
|
67
|
+
const ipv4Schema = z$1.ipv4({
|
|
68
|
+
message: "请输入正确的 IP 地址格式,例如: 192.168.1.100"
|
|
69
|
+
});
|
|
70
|
+
const phoneSchema = z$1.string().regex(/^1[3-9]\d{9}$/, {
|
|
71
|
+
message: "请输入正确的手机号码"
|
|
72
|
+
});
|
|
73
|
+
const userPassSchema = z$1.string().min(1, {
|
|
74
|
+
message: "用户密码不能为空"
|
|
75
|
+
});
|
|
76
|
+
const appKeySchema = z$1.string().min(1, {
|
|
77
|
+
message: "AppKey 不能为空"
|
|
78
|
+
});
|
|
79
|
+
const appSecretSchema = z$1.string().min(1, {
|
|
80
|
+
message: "AppSecret 不能为空"
|
|
81
|
+
});
|
|
82
|
+
const absolutePathSchema = z$1.string().refine(
|
|
83
|
+
(value) => {
|
|
84
|
+
return path.isAbsolute(value);
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
message: "路径必须是绝对路径"
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
const windowsPathSchema = z$1.string().refine(
|
|
91
|
+
(value) => !/[<>"|?*]/.test(value),
|
|
92
|
+
{
|
|
93
|
+
message: '路径不能包含特殊字符: < > " | ? *'
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
const openclawPathSchema = absolutePathSchema.and(windowsPathSchema);
|
|
97
|
+
const pluginVersionSchema = z$1.string().optional();
|
|
98
|
+
const wsUrlSchema = z$1.string().url({
|
|
99
|
+
message: "请输入有效的 WebSocket URL"
|
|
100
|
+
}).optional();
|
|
101
|
+
z$1.boolean();
|
|
102
|
+
function validateOpenclawPath(openclawPath) {
|
|
103
|
+
const result = openclawPathSchema.safeParse(openclawPath);
|
|
104
|
+
if (!result.success) {
|
|
105
|
+
debugLog(`[路径验证] 失败: ${result.error.issues[0].message}`);
|
|
106
|
+
throw new Error(result.error.issues[0].message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
57
109
|
const CONFIG = {
|
|
58
110
|
PLUGIN_NAME: "openclaw-workclaw",
|
|
59
111
|
DEFAULT_BASE_URL: "https://workbrain.cn/backend-api",
|
|
@@ -116,6 +168,20 @@ function getConfig(env, customIp) {
|
|
|
116
168
|
}
|
|
117
169
|
return CONFIG;
|
|
118
170
|
}
|
|
171
|
+
const ERROR_CODES = {
|
|
172
|
+
APP_KEY_REQUIRED: "APP_KEY_REQUIRED",
|
|
173
|
+
APP_SECRET_REQUIRED: "APP_SECRET_REQUIRED",
|
|
174
|
+
NPM_INSTALL_FAILED: "NPM_INSTALL_FAILED",
|
|
175
|
+
INVALID_OPENCLAW_PATH: "INVALID_OPENCLAW_PATH",
|
|
176
|
+
INVALID_ARGUMENT: "INVALID_ARGUMENT"
|
|
177
|
+
};
|
|
178
|
+
class AppError2 extends Error {
|
|
179
|
+
constructor(code, message) {
|
|
180
|
+
super(message);
|
|
181
|
+
this.code = code;
|
|
182
|
+
this.name = "AppError";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
119
185
|
function getHomeDir$1() {
|
|
120
186
|
return process$1.env.HOME || process$1.env.USERPROFILE || "";
|
|
121
187
|
}
|
|
@@ -134,12 +200,20 @@ function execAsync$1(command, options) {
|
|
|
134
200
|
function deepMerge$1(target, source) {
|
|
135
201
|
const result = { ...target };
|
|
136
202
|
for (const key in source) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
203
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
204
|
+
const sourceValue = source[key];
|
|
205
|
+
const targetValue = target[key];
|
|
206
|
+
if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
|
|
207
|
+
const mergedArray = [.../* @__PURE__ */ new Set([...targetValue, ...sourceValue])];
|
|
208
|
+
result[key] = mergedArray;
|
|
209
|
+
} else if (sourceValue !== void 0 && sourceValue !== null && typeof sourceValue === "object" && !Array.isArray(sourceValue) && typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue)) {
|
|
210
|
+
result[key] = deepMerge$1(
|
|
211
|
+
targetValue,
|
|
212
|
+
sourceValue
|
|
213
|
+
);
|
|
214
|
+
} else if (sourceValue !== void 0) {
|
|
215
|
+
result[key] = sourceValue;
|
|
216
|
+
}
|
|
143
217
|
}
|
|
144
218
|
}
|
|
145
219
|
return result;
|
|
@@ -152,25 +226,21 @@ class BoxInstaller {
|
|
|
152
226
|
spinner;
|
|
153
227
|
prefixText = "";
|
|
154
228
|
validateConfig() {
|
|
155
|
-
debugLog("[验证配置] 检查 appKey 和 appSecret...");
|
|
156
229
|
if (!this.config.appKey) {
|
|
157
|
-
throw new
|
|
230
|
+
throw new AppError2(ERROR_CODES.APP_KEY_REQUIRED, "AppKey 不能为空,请使用 --app-key 参数");
|
|
158
231
|
}
|
|
159
232
|
if (!this.config.appSecret) {
|
|
160
|
-
throw new
|
|
233
|
+
throw new AppError2(ERROR_CODES.APP_SECRET_REQUIRED, "AppSecret 不能为空,请使用 --app-secret 参数");
|
|
161
234
|
}
|
|
162
235
|
if (this.config.env === "custom") {
|
|
163
|
-
debugLog("[验证配置] 检查自定义环境 IP...");
|
|
164
236
|
if (!this.config.customIp) {
|
|
165
|
-
throw new
|
|
237
|
+
throw new AppError2(ERROR_CODES.INVALID_ARGUMENT, "自定义环境必须提供 --customIp 参数");
|
|
166
238
|
}
|
|
167
239
|
const result = ipv4Schema.safeParse(this.config.customIp);
|
|
168
240
|
if (!result.success) {
|
|
169
|
-
throw new
|
|
241
|
+
throw new AppError2(ERROR_CODES.INVALID_ARGUMENT, result.error.issues[0].message);
|
|
170
242
|
}
|
|
171
|
-
debugLog("[验证配置] 自定义环境 IP 验证通过");
|
|
172
243
|
}
|
|
173
|
-
debugLog("[验证配置] 配置检查通过");
|
|
174
244
|
}
|
|
175
245
|
getPaths() {
|
|
176
246
|
const env = this.config.env || "test";
|
|
@@ -183,7 +253,7 @@ class BoxInstaller {
|
|
|
183
253
|
validateOpenclawPath(this.config.openclawPath);
|
|
184
254
|
debugLog("[路径验证] 通过");
|
|
185
255
|
} catch (error) {
|
|
186
|
-
throw new
|
|
256
|
+
throw new AppError2(ERROR_CODES.INVALID_OPENCLAW_PATH, error.message);
|
|
187
257
|
}
|
|
188
258
|
baseDir = this.config.openclawPath;
|
|
189
259
|
} else {
|
|
@@ -207,7 +277,6 @@ class BoxInstaller {
|
|
|
207
277
|
updateSpinner(text) {
|
|
208
278
|
this.spinner.prefixText = this.prefixText;
|
|
209
279
|
this.spinner.text = chalk.cyan(text);
|
|
210
|
-
debugLog(`[Spinner] ${text}`);
|
|
211
280
|
}
|
|
212
281
|
getPrefixText() {
|
|
213
282
|
return this.prefixText;
|
|
@@ -238,7 +307,6 @@ class BoxInstaller {
|
|
|
238
307
|
}
|
|
239
308
|
}
|
|
240
309
|
async doCleanOldFiles(paths) {
|
|
241
|
-
debugLog("[清理旧版本] 开始清理旧版本...");
|
|
242
310
|
this.prefixText += chalk.green(` ✓ 开始清理旧版本
|
|
243
311
|
`);
|
|
244
312
|
this.updateSpinner("正在清理旧版本...");
|
|
@@ -250,7 +318,6 @@ class BoxInstaller {
|
|
|
250
318
|
`);
|
|
251
319
|
debugLog("[清理旧版本] 旧版本清理成功");
|
|
252
320
|
} catch {
|
|
253
|
-
debugLog("[清理旧版本] 无旧版本需要清理");
|
|
254
321
|
this.prefixText += chalk.green(` ✓ 无旧版本需要清理
|
|
255
322
|
`);
|
|
256
323
|
}
|
|
@@ -258,30 +325,24 @@ class BoxInstaller {
|
|
|
258
325
|
await fs.access(paths.temp);
|
|
259
326
|
await fs.rm(paths.temp, { recursive: true, force: true });
|
|
260
327
|
} catch {
|
|
261
|
-
debugLog("[清理旧版本] 无临时目录需要清理");
|
|
262
328
|
}
|
|
263
329
|
}
|
|
264
330
|
async doDownloadFromNpm() {
|
|
265
331
|
const paths = this.getPaths();
|
|
266
332
|
this.spinner.prefixText = this.prefixText;
|
|
267
333
|
this.spinner.text = chalk.cyan("正在准备安装目录...");
|
|
268
|
-
debugLog("[下载插件] 创建扩展目录...");
|
|
269
334
|
await fs.mkdir(paths.extensions, { recursive: true });
|
|
270
335
|
debugLog(`[下载插件] 扩展目录: ${paths.extensions}`);
|
|
271
336
|
const tempDir = path.join(paths.temp, `install-${Date.now()}`);
|
|
272
|
-
debugLog(`[下载插件] 创建临时目录: ${tempDir}`);
|
|
273
337
|
await fs.mkdir(tempDir, { recursive: true });
|
|
274
338
|
this.spinner.prefixText = this.prefixText;
|
|
275
339
|
this.spinner.text = chalk.cyan("正在下载插件...");
|
|
276
|
-
debugLog("[下载插件] 执行 npm pack 下载源码包");
|
|
277
|
-
debugLog(`[下载插件] 工作目录: ${tempDir}`);
|
|
278
340
|
const maxRetries = 3;
|
|
279
341
|
let lastError = "";
|
|
280
342
|
let tarballPath = "";
|
|
281
343
|
for (let i = 1; i <= maxRetries; i++) {
|
|
282
344
|
this.spinner.prefixText = this.prefixText;
|
|
283
345
|
this.spinner.text = chalk.cyan(`正在下载 (${i}/${maxRetries})...`);
|
|
284
|
-
debugLog(`[下载插件] 第 ${i} 次尝试下载 tarball...`);
|
|
285
346
|
try {
|
|
286
347
|
const packageName = this.getPackageName();
|
|
287
348
|
debugLog(`[下载插件] 下载包名: ${packageName}`);
|
|
@@ -297,16 +358,14 @@ class BoxInstaller {
|
|
|
297
358
|
break;
|
|
298
359
|
} catch (error) {
|
|
299
360
|
lastError = error.message || "未知错误";
|
|
300
|
-
debugLog(`[下载插件] 第 ${i} 次尝试失败: ${lastError}`);
|
|
301
361
|
if (i < maxRetries) {
|
|
302
362
|
this.spinner.text = chalk.cyan(`下载失败,3秒后重试...`);
|
|
303
|
-
debugLog("[下载插件] 等待 3 秒后重试...");
|
|
304
363
|
await new Promise((resolve2) => setTimeout(resolve2, 3e3));
|
|
305
364
|
}
|
|
306
365
|
}
|
|
307
366
|
}
|
|
308
367
|
if (!tarballPath || !await fs.access(tarballPath).then(() => true).catch(() => false)) {
|
|
309
|
-
throw new
|
|
368
|
+
throw new AppError2(ERROR_CODES.NPM_INSTALL_FAILED, `tarball 下载失败: ${lastError}`);
|
|
310
369
|
}
|
|
311
370
|
this.spinner.prefixText = this.prefixText;
|
|
312
371
|
this.spinner.text = chalk.cyan("正在解压插件...");
|
|
@@ -321,17 +380,15 @@ class BoxInstaller {
|
|
|
321
380
|
});
|
|
322
381
|
debugLog("[下载插件] tarball 解压成功");
|
|
323
382
|
} catch (error) {
|
|
324
|
-
throw new
|
|
383
|
+
throw new AppError2(ERROR_CODES.NPM_INSTALL_FAILED, `tarball 解压失败: ${error.message}`);
|
|
325
384
|
}
|
|
326
385
|
const pluginPath = path.join(paths.target, "package.json");
|
|
327
386
|
const pluginExists = await fs.access(pluginPath).then(() => true).catch(() => false);
|
|
328
387
|
if (!pluginExists) {
|
|
329
|
-
throw new
|
|
388
|
+
throw new AppError2(ERROR_CODES.NPM_INSTALL_FAILED, "插件解压后未找到 package.json");
|
|
330
389
|
}
|
|
331
|
-
debugLog("[下载插件] 插件解压成功");
|
|
332
390
|
this.spinner.prefixText = this.prefixText;
|
|
333
391
|
this.spinner.text = chalk.cyan("正在安装依赖...");
|
|
334
|
-
debugLog("[下载插件] 执行 npm install 安装生产环境依赖");
|
|
335
392
|
try {
|
|
336
393
|
await execAsync$1("npm install --omit=dev --ignore-scripts --quiet", {
|
|
337
394
|
cwd: paths.target,
|
|
@@ -340,11 +397,7 @@ class BoxInstaller {
|
|
|
340
397
|
debugLog("[下载插件] npm install 执行成功");
|
|
341
398
|
} catch (error) {
|
|
342
399
|
const errorMsg = error.message || "";
|
|
343
|
-
if (errorMsg.includes("npm warn") || errorMsg.includes("deprecated"))
|
|
344
|
-
debugLog("[下载插件] npm install 有警告但可能成功,继续流程");
|
|
345
|
-
} else {
|
|
346
|
-
debugLog(`[下载插件] npm install 失败: ${errorMsg}`);
|
|
347
|
-
}
|
|
400
|
+
if (errorMsg.includes("npm warn") || errorMsg.includes("deprecated")) ;
|
|
348
401
|
}
|
|
349
402
|
this.prefixText += chalk.green(` ✓ 插件安装完成
|
|
350
403
|
`);
|
|
@@ -352,11 +405,9 @@ class BoxInstaller {
|
|
|
352
405
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
353
406
|
debugLog("[下载插件] 清理临时目录完成");
|
|
354
407
|
} catch {
|
|
355
|
-
debugLog("[下载插件] 清理临时目录失败(忽略)");
|
|
356
408
|
}
|
|
357
409
|
}
|
|
358
410
|
async doUpdateConfig(paths) {
|
|
359
|
-
debugLog("[更新配置] 开始更新配置...");
|
|
360
411
|
this.prefixText += chalk.green(` ✓ 开始更新配置
|
|
361
412
|
`);
|
|
362
413
|
this.updateSpinner("正在生成配置...");
|
|
@@ -366,9 +417,8 @@ class BoxInstaller {
|
|
|
366
417
|
originalConfig = JSON.parse(content);
|
|
367
418
|
debugLog("[更新配置] 读取原有配置成功");
|
|
368
419
|
} catch {
|
|
369
|
-
debugLog("[更新配置] 无原有配置");
|
|
370
420
|
}
|
|
371
|
-
const config = getConfig(this.config.env);
|
|
421
|
+
const config = getConfig(this.config.env || "test", this.config.customIp);
|
|
372
422
|
const newConfig = {
|
|
373
423
|
// diagnostics: 诊断配置
|
|
374
424
|
diagnostics: {
|
|
@@ -503,14 +553,12 @@ class BoxInstaller {
|
|
|
503
553
|
"openclaw-workclaw": {
|
|
504
554
|
// 启用通道
|
|
505
555
|
enabled: true,
|
|
506
|
-
// 连接模式:'websocket' | 'http'
|
|
507
|
-
connectionMode: "websocket",
|
|
508
556
|
// 应用密钥
|
|
509
557
|
appKey: this.config.appKey,
|
|
510
558
|
// 应用密钥
|
|
511
559
|
appSecret: this.config.appSecret,
|
|
512
560
|
// API 基础 URL
|
|
513
|
-
baseUrl: this.config.customIp
|
|
561
|
+
baseUrl: this.config.customIp ? `http://${this.config.customIp}` : config.DEFAULT_BASE_URL,
|
|
514
562
|
// WebSocket URL
|
|
515
563
|
websocketUrl: this.config.wsUrl || config.DEFAULT_WS_URL,
|
|
516
564
|
// 允许不安全的 TLS 连接
|
|
@@ -589,6 +637,9 @@ class BoxInstaller {
|
|
|
589
637
|
},
|
|
590
638
|
// plugins: 插件配置
|
|
591
639
|
plugins: {
|
|
640
|
+
load: {
|
|
641
|
+
paths: [paths.extensions]
|
|
642
|
+
},
|
|
592
643
|
allow: [config.PLUGIN_NAME],
|
|
593
644
|
installs: {
|
|
594
645
|
[config.PLUGIN_NAME]: {
|
|
@@ -604,36 +655,16 @@ class BoxInstaller {
|
|
|
604
655
|
};
|
|
605
656
|
const finalConfig = deepMerge$1(originalConfig, newConfig);
|
|
606
657
|
this.updateSpinner("正在写入配置...");
|
|
607
|
-
debugLog("[更新配置] 写入配置文件...");
|
|
608
658
|
await fs.writeFile(paths.config, JSON.stringify(finalConfig, null, 2), "utf-8");
|
|
609
659
|
this.prefixText += chalk.green(` ✓ 配置文件更新成功
|
|
610
660
|
`);
|
|
611
661
|
debugLog(`[更新配置] 配置文件写入成功: ${paths.config}`);
|
|
612
662
|
}
|
|
613
663
|
}
|
|
614
|
-
function checkEnv$1() {
|
|
615
|
-
debugLog("[环境检查] 检测 Node.js 版本...");
|
|
616
|
-
const nodeVersion = execSync("node --version", { stdio: "pipe" }).toString().trim();
|
|
617
|
-
debugLog(`[环境检查] Node.js 版本: ${nodeVersion}`);
|
|
618
|
-
if (!semver.gte(nodeVersion, "18.0.0")) {
|
|
619
|
-
throw new AppError$1(ERROR_CODES$1.NODE_VERSION_LOW, `Node.js 版本需要 >= 18.0.0,当前版本: ${nodeVersion}`);
|
|
620
|
-
}
|
|
621
|
-
debugLog("[环境检查] Node.js 版本检查通过");
|
|
622
|
-
debugLog("[环境检查] 检测 npm...");
|
|
623
|
-
try {
|
|
624
|
-
const npmVersion = execSync("npm --version", { stdio: "pipe" }).toString().trim();
|
|
625
|
-
debugLog(`[环境检查] npm 版本: ${npmVersion}`);
|
|
626
|
-
debugLog("[环境检查] npm 检测通过");
|
|
627
|
-
} catch {
|
|
628
|
-
throw new AppError$1(ERROR_CODES$1.NPM_NOT_FOUND, "未检测到 npm,请先安装 Node.js 和 npm");
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
664
|
async function createBoxCommand(options) {
|
|
632
665
|
setDebug(!!options.debug);
|
|
633
|
-
debugLog("[盒子安装] 开始处理...");
|
|
634
666
|
debugLog(`[盒子安装] 参数: env=${options.env}, appKey=${options.appKey ? "***" : "未提供"}, customIp=${options.customIp}, debug=${options.debug}`);
|
|
635
|
-
checkEnv
|
|
636
|
-
debugLog("[盒子安装] 环境检查通过");
|
|
667
|
+
checkEnv();
|
|
637
668
|
try {
|
|
638
669
|
let env = options.env;
|
|
639
670
|
let appKey = options.appKey;
|
|
@@ -663,8 +694,9 @@ async function createBoxCommand(options) {
|
|
|
663
694
|
name: "appKey",
|
|
664
695
|
message: chalk.cyan("请输入 AppKey:"),
|
|
665
696
|
validate: (value) => {
|
|
666
|
-
|
|
667
|
-
|
|
697
|
+
const result = appKeySchema.safeParse(value);
|
|
698
|
+
if (!result.success) {
|
|
699
|
+
return chalk.red(result.error.issues[0].message);
|
|
668
700
|
}
|
|
669
701
|
return true;
|
|
670
702
|
}
|
|
@@ -679,8 +711,9 @@ async function createBoxCommand(options) {
|
|
|
679
711
|
name: "appSecret",
|
|
680
712
|
message: chalk.cyan("请输入 AppSecret:"),
|
|
681
713
|
validate: (value) => {
|
|
682
|
-
|
|
683
|
-
|
|
714
|
+
const result = appSecretSchema.safeParse(value);
|
|
715
|
+
if (!result.success) {
|
|
716
|
+
return chalk.red(result.error.issues[0].message);
|
|
684
717
|
}
|
|
685
718
|
return true;
|
|
686
719
|
}
|
|
@@ -765,31 +798,10 @@ async function createBoxCommand(options) {
|
|
|
765
798
|
function registerCommands$1(program2) {
|
|
766
799
|
program2.command("box").description("盒子设备安装(无需登录)").option("-e, --env <env>", "环境 (test/prod/custom)").option("--app-key <appKey>", "App Key").option("--app-secret <appSecret>", "App Secret").option("--customIp <ip>", "自定义后端 IP(仅 custom 环境生效)").option("--ws-url <url>", "自定义 WebSocket URL(仅 custom 环境生效,默认自动生成)").option("--plugin-version <plugin-version>", "插件版本号(默认最新版)").option("--openclaw-path <path>", "OpenClaw 安装目录路径(默认 ~/.openclaw)").option("--debug", "开启调试日志").action(createBoxCommand);
|
|
767
800
|
}
|
|
768
|
-
|
|
769
|
-
PHONE_REQUIRED: "PHONE_REQUIRED",
|
|
770
|
-
USER_PASS_REQUIRED: "USER_PASS_REQUIRED",
|
|
771
|
-
LOGIN_FAILED: "LOGIN_FAILED",
|
|
772
|
-
GET_BOUND_CONFIG_FAILED: "GET_BOUND_CONFIG_FAILED",
|
|
773
|
-
NODE_VERSION_LOW: "NODE_VERSION_LOW",
|
|
774
|
-
NODE_NOT_FOUND: "NODE_NOT_FOUND",
|
|
775
|
-
NPM_NOT_FOUND: "NPM_NOT_FOUND",
|
|
776
|
-
NPM_INSTALL_FAILED: "NPM_INSTALL_FAILED",
|
|
777
|
-
CONFIG_WRITE_FAILED: "CONFIG_WRITE_FAILED",
|
|
778
|
-
HTTP_ERROR: "HTTP_ERROR",
|
|
779
|
-
NETWORK_ERROR: "NETWORK_ERROR",
|
|
780
|
-
INVALID_OPENCLAW_PATH: "INVALID_OPENCLAW_PATH",
|
|
781
|
-
INVALID_ARGUMENT: "INVALID_ARGUMENT"
|
|
782
|
-
};
|
|
783
|
-
class AppError2 extends Error {
|
|
784
|
-
constructor(code, message) {
|
|
785
|
-
super(message);
|
|
786
|
-
this.code = code;
|
|
787
|
-
this.name = "AppError";
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
function createHttpClient() {
|
|
801
|
+
function createHttpClient(baseURL) {
|
|
791
802
|
return axios.create({
|
|
792
|
-
|
|
803
|
+
baseURL,
|
|
804
|
+
timeout: 3e4,
|
|
793
805
|
headers: {
|
|
794
806
|
"Content-Type": "application/json"
|
|
795
807
|
}
|
|
@@ -797,7 +809,6 @@ function createHttpClient() {
|
|
|
797
809
|
}
|
|
798
810
|
async function httpPost(url, data, config) {
|
|
799
811
|
const client = createHttpClient();
|
|
800
|
-
debugLog(`[HTTP POST] 请求地址: ${url}`);
|
|
801
812
|
debugLog(`[HTTP POST] 请求参数: ${JSON.stringify(data)}`);
|
|
802
813
|
debugLog(`[HTTP POST] 请求头: ${JSON.stringify(config?.headers || {})}`);
|
|
803
814
|
try {
|
|
@@ -816,29 +827,23 @@ async function httpPost(url, data, config) {
|
|
|
816
827
|
debugLog(`[HTTP POST] 响应状态文本: ${axiosError.response.statusText}`);
|
|
817
828
|
debugLog(`[HTTP POST] 响应数据: ${JSON.stringify(responseData)}`);
|
|
818
829
|
if (typeof responseData === "string") {
|
|
819
|
-
throw new
|
|
830
|
+
throw new AppError$1(ERROR_CODES$1.HTTP_ERROR, `请求失败 (${axiosError.response.status}): ${responseData}`);
|
|
820
831
|
} else if (responseData && typeof responseData === "object" && "message" in responseData) {
|
|
821
|
-
throw new
|
|
832
|
+
throw new AppError$1(ERROR_CODES$1.HTTP_ERROR, `请求失败 (${axiosError.response.status}): ${responseData.message}`);
|
|
822
833
|
} else {
|
|
823
|
-
throw new
|
|
834
|
+
throw new AppError$1(ERROR_CODES$1.HTTP_ERROR, `请求失败 (${axiosError.response.status}): ${axiosError.response.statusText}`);
|
|
824
835
|
}
|
|
825
836
|
} else if (axiosError.request) {
|
|
826
|
-
debugLog(`[HTTP POST] 未收到响应`);
|
|
827
837
|
debugLog(`[HTTP POST] 错误信息: ${axiosError.message}`);
|
|
828
|
-
throw new
|
|
838
|
+
throw new AppError$1(ERROR_CODES$1.NETWORK_ERROR, `网络错误: ${axiosError.message}`);
|
|
829
839
|
} else {
|
|
830
|
-
debugLog(`[HTTP POST] 请求配置错误`);
|
|
831
840
|
debugLog(`[HTTP POST] 错误信息: ${axiosError.message}`);
|
|
832
|
-
throw new
|
|
841
|
+
throw new AppError$1(ERROR_CODES$1.HTTP_ERROR, `请求配置错误: ${axiosError.message}`);
|
|
833
842
|
}
|
|
834
843
|
}
|
|
835
844
|
}
|
|
836
|
-
async function login(phone, password,
|
|
837
|
-
const config = getConfig(env);
|
|
845
|
+
async function login(phone, password, config) {
|
|
838
846
|
const url = `${config.API.TUZAI_BASE_URL}/user/login/pass`;
|
|
839
|
-
debugLog("[登录] 开始登录...");
|
|
840
|
-
debugLog(`[登录] 请求地址: ${url}`);
|
|
841
|
-
debugLog(`[登录] 请求参数: phone=${phone}, password=***`);
|
|
842
847
|
try {
|
|
843
848
|
const response = await httpPost(url, {
|
|
844
849
|
phone,
|
|
@@ -851,26 +856,22 @@ async function login(phone, password, env) {
|
|
|
851
856
|
return data.data.token;
|
|
852
857
|
}
|
|
853
858
|
debugLog("[登录] 登录失败");
|
|
854
|
-
throw new
|
|
859
|
+
throw new AppError$1(ERROR_CODES$1.LOGIN_FAILED, data.message || "登录失败");
|
|
855
860
|
} catch (error) {
|
|
856
|
-
if (error instanceof
|
|
861
|
+
if (error instanceof AppError$1) {
|
|
857
862
|
throw error;
|
|
858
863
|
}
|
|
859
864
|
debugLog(`[登录] 发生错误: ${error.message}`);
|
|
860
|
-
throw new
|
|
865
|
+
throw new AppError$1(ERROR_CODES$1.LOGIN_FAILED, error.message);
|
|
861
866
|
}
|
|
862
867
|
}
|
|
863
|
-
async function fetchBoundConfig(token, phone,
|
|
864
|
-
const config = getConfig(env);
|
|
868
|
+
async function fetchBoundConfig(token, phone, localCode, config) {
|
|
865
869
|
const url = `${config.API.TUZAI_BASE_URL}/work-bot/local/bound/init`;
|
|
866
|
-
debugLog("[获取绑定配置] 开始获取绑定配置...");
|
|
867
|
-
debugLog(`[获取绑定配置] 请求地址: ${url}`);
|
|
868
|
-
debugLog(`[获取绑定配置] 请求参数: phone=${phone}, localCode=001`);
|
|
869
870
|
debugLog(`[获取绑定配置] 请求头: Authorization=${token.substring(0, 20)}...`);
|
|
870
871
|
try {
|
|
871
872
|
const response = await httpPost(url, {
|
|
872
873
|
phone,
|
|
873
|
-
localCode
|
|
874
|
+
localCode
|
|
874
875
|
}, {
|
|
875
876
|
headers: {
|
|
876
877
|
Authorization: token
|
|
@@ -896,20 +897,48 @@ async function fetchBoundConfig(token, phone, env) {
|
|
|
896
897
|
debugLog(`[获取绑定配置] modelApiBaseUrl: ${boundConfig.modelApiBaseUrl || "undefined"}`);
|
|
897
898
|
if (!boundConfig.appKey || !boundConfig.appSecret || !boundConfig.agentId) {
|
|
898
899
|
debugLog("[获取绑定配置] 缺少必要字段");
|
|
899
|
-
throw new
|
|
900
|
+
throw new AppError$1(ERROR_CODES$1.GET_BOUND_CONFIG_FAILED, "获取绑定配置失败:缺少必要字段");
|
|
900
901
|
}
|
|
901
902
|
return boundConfig;
|
|
902
903
|
}
|
|
903
904
|
debugLog("[获取绑定配置] 获取绑定配置失败");
|
|
904
|
-
throw new
|
|
905
|
+
throw new AppError$1(ERROR_CODES$1.GET_BOUND_CONFIG_FAILED, data.message || data.msg || "获取绑定配置失败");
|
|
905
906
|
} catch (error) {
|
|
906
|
-
if (error instanceof
|
|
907
|
+
if (error instanceof AppError$1) {
|
|
907
908
|
throw error;
|
|
908
909
|
}
|
|
909
910
|
debugLog(`[获取绑定配置] 发生错误: ${error.message}`);
|
|
910
|
-
throw new
|
|
911
|
+
throw new AppError$1(ERROR_CODES$1.GET_BOUND_CONFIG_FAILED, error.message);
|
|
911
912
|
}
|
|
912
913
|
}
|
|
914
|
+
const LocalInstallerConfigSchema = z$1.object({
|
|
915
|
+
env: EnvironmentSchema.optional(),
|
|
916
|
+
customIp: ipv4Schema.optional(),
|
|
917
|
+
wsUrl: wsUrlSchema,
|
|
918
|
+
phone: phoneSchema,
|
|
919
|
+
userPass: userPassSchema,
|
|
920
|
+
pluginVersion: pluginVersionSchema,
|
|
921
|
+
openclawPath: openclawPathSchema.optional()
|
|
922
|
+
}).refine(
|
|
923
|
+
(data) => {
|
|
924
|
+
if (data.env === "custom") {
|
|
925
|
+
return data.customIp !== void 0 && data.customIp !== "";
|
|
926
|
+
}
|
|
927
|
+
return true;
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
message: "自定义环境必须提供 customIp",
|
|
931
|
+
path: ["customIp"]
|
|
932
|
+
}
|
|
933
|
+
);
|
|
934
|
+
z$1.object({
|
|
935
|
+
appKey: z$1.string(),
|
|
936
|
+
appSecret: z$1.string(),
|
|
937
|
+
userId: z$1.string().optional(),
|
|
938
|
+
agentId: z$1.string(),
|
|
939
|
+
modelApiKey: z$1.string().optional(),
|
|
940
|
+
modelApiBaseUrl: z$1.string().optional()
|
|
941
|
+
});
|
|
913
942
|
var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
914
943
|
function int2char(n) {
|
|
915
944
|
return BI_RM.charAt(n);
|
|
@@ -4800,6 +4829,29 @@ function rsaEncrypt(txt) {
|
|
|
4800
4829
|
const result = encrypt.encrypt(txt);
|
|
4801
4830
|
return result || null;
|
|
4802
4831
|
}
|
|
4832
|
+
function generateMachineId() {
|
|
4833
|
+
const info = [
|
|
4834
|
+
os.hostname(),
|
|
4835
|
+
// 主机名
|
|
4836
|
+
os.platform(),
|
|
4837
|
+
// 平台
|
|
4838
|
+
os.arch(),
|
|
4839
|
+
// 架构
|
|
4840
|
+
os.totalmem(),
|
|
4841
|
+
// 总内存
|
|
4842
|
+
os.cpus().length,
|
|
4843
|
+
// CPU 核数
|
|
4844
|
+
os.cpus()[0]?.model || "",
|
|
4845
|
+
// CPU 型号
|
|
4846
|
+
os.release()
|
|
4847
|
+
// 系统版本
|
|
4848
|
+
].join("|");
|
|
4849
|
+
const hash = crypto.createHash("sha256").update(info).digest("hex");
|
|
4850
|
+
return hash.substring(0, 16).toUpperCase();
|
|
4851
|
+
}
|
|
4852
|
+
function buildLocalCode(phone, machineId) {
|
|
4853
|
+
return `${phone}-${machineId}`;
|
|
4854
|
+
}
|
|
4803
4855
|
function getHomeDir() {
|
|
4804
4856
|
return process$1.env.HOME || process$1.env.USERPROFILE || "";
|
|
4805
4857
|
}
|
|
@@ -4818,51 +4870,55 @@ function execAsync(command, options) {
|
|
|
4818
4870
|
function deepMerge(target, source) {
|
|
4819
4871
|
const result = { ...target };
|
|
4820
4872
|
for (const key in source) {
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4873
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
4874
|
+
const sourceValue = source[key];
|
|
4875
|
+
const targetValue = target[key];
|
|
4876
|
+
if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
|
|
4877
|
+
const mergedArray = [.../* @__PURE__ */ new Set([...targetValue, ...sourceValue])];
|
|
4878
|
+
result[key] = mergedArray;
|
|
4879
|
+
} else if (sourceValue !== void 0 && sourceValue !== null && typeof sourceValue === "object" && !Array.isArray(sourceValue) && typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue)) {
|
|
4880
|
+
result[key] = deepMerge(
|
|
4881
|
+
targetValue,
|
|
4882
|
+
sourceValue
|
|
4883
|
+
);
|
|
4884
|
+
} else if (sourceValue !== void 0) {
|
|
4885
|
+
result[key] = sourceValue;
|
|
4886
|
+
}
|
|
4827
4887
|
}
|
|
4828
4888
|
}
|
|
4829
4889
|
return result;
|
|
4830
4890
|
}
|
|
4831
4891
|
class LocalInstaller {
|
|
4892
|
+
spinner;
|
|
4893
|
+
prefixText = "";
|
|
4894
|
+
config;
|
|
4895
|
+
envConfig;
|
|
4896
|
+
/**
|
|
4897
|
+
* 构造函数
|
|
4898
|
+
*/
|
|
4832
4899
|
constructor(config) {
|
|
4833
4900
|
this.config = config;
|
|
4901
|
+
const env = this.config.env || "test";
|
|
4902
|
+
this.envConfig = getConfig(env, this.config.customIp);
|
|
4834
4903
|
this.spinner = ora({ color: "cyan" }).start();
|
|
4835
4904
|
}
|
|
4836
|
-
|
|
4837
|
-
|
|
4905
|
+
/**
|
|
4906
|
+
* 验证安装配置
|
|
4907
|
+
*/
|
|
4838
4908
|
validateConfig() {
|
|
4839
|
-
|
|
4840
|
-
if (!
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
if (!this.config.userPass) {
|
|
4844
|
-
throw new AppError2(ERROR_CODES.USER_PASS_REQUIRED, "用户密码不能为空,请使用 --user-pass 参数");
|
|
4909
|
+
const result = LocalInstallerConfigSchema.safeParse(this.config);
|
|
4910
|
+
if (!result.success) {
|
|
4911
|
+
debugLog(`[验证配置] 验证失败: ${result.error.issues[0].message}`);
|
|
4912
|
+
throw new AppError$1(ERROR_CODES$1.INVALID_ARGUMENT, result.error.issues[0].message);
|
|
4845
4913
|
}
|
|
4846
|
-
if (this.config.env === "custom") {
|
|
4847
|
-
debugLog("[验证配置] 检查自定义环境 IP...");
|
|
4848
|
-
if (!this.config.customIp) {
|
|
4849
|
-
throw new AppError2(ERROR_CODES.INVALID_ARGUMENT, "自定义环境必须提供 --customIp 参数");
|
|
4850
|
-
}
|
|
4851
|
-
const result = ipv4Schema.safeParse(this.config.customIp);
|
|
4852
|
-
if (!result.success) {
|
|
4853
|
-
throw new AppError2(ERROR_CODES.INVALID_ARGUMENT, result.error.issues[0].message);
|
|
4854
|
-
}
|
|
4855
|
-
debugLog("[验证配置] 自定义环境 IP 验证通过");
|
|
4856
|
-
}
|
|
4857
|
-
debugLog("[验证配置] 配置验证通过");
|
|
4858
4914
|
}
|
|
4915
|
+
/**
|
|
4916
|
+
* 执行安装流程
|
|
4917
|
+
*/
|
|
4859
4918
|
async install() {
|
|
4860
4919
|
try {
|
|
4861
4920
|
debugLog("[安装开始]");
|
|
4862
4921
|
this.validateConfig();
|
|
4863
|
-
const env = this.config.env || "test";
|
|
4864
|
-
debugLog(`[getConfig] env=${env}, customIp=${this.config.customIp}`);
|
|
4865
|
-
const config = getConfig(env, this.config.customIp);
|
|
4866
4922
|
let baseDir;
|
|
4867
4923
|
if (this.config.openclawPath) {
|
|
4868
4924
|
debugLog(`[路径验证] 验证自定义路径: ${this.config.openclawPath}`);
|
|
@@ -4870,80 +4926,96 @@ class LocalInstaller {
|
|
|
4870
4926
|
validateOpenclawPath(this.config.openclawPath);
|
|
4871
4927
|
debugLog("[路径验证] 通过");
|
|
4872
4928
|
} catch (error) {
|
|
4873
|
-
throw new
|
|
4929
|
+
throw new AppError$1(ERROR_CODES$1.INVALID_OPENCLAW_PATH, error.message);
|
|
4874
4930
|
}
|
|
4875
4931
|
baseDir = this.config.openclawPath;
|
|
4876
4932
|
} else {
|
|
4877
4933
|
const homeDir = getHomeDir();
|
|
4878
|
-
baseDir = path.join(homeDir,
|
|
4934
|
+
baseDir = path.join(homeDir, this.envConfig.DIRS.OPENCLAW);
|
|
4879
4935
|
}
|
|
4880
4936
|
const paths = {
|
|
4881
4937
|
home: baseDir,
|
|
4882
|
-
extensions: path.join(baseDir,
|
|
4883
|
-
target: path.join(baseDir,
|
|
4884
|
-
config: path.join(baseDir,
|
|
4885
|
-
workspace: path.join(baseDir,
|
|
4886
|
-
temp: path.join(baseDir,
|
|
4938
|
+
extensions: path.join(baseDir, this.envConfig.DIRS.EXTENSIONS),
|
|
4939
|
+
target: path.join(baseDir, this.envConfig.DIRS.EXTENSIONS, this.envConfig.PLUGIN_NAME),
|
|
4940
|
+
config: path.join(baseDir, this.envConfig.DIRS.CONFIG_FILE),
|
|
4941
|
+
workspace: path.join(baseDir, this.envConfig.DIRS.WORKSPACE),
|
|
4942
|
+
temp: path.join(baseDir, this.envConfig.DIRS.TEMP)
|
|
4887
4943
|
};
|
|
4888
4944
|
debugLog(`[路径配置] home=${paths.home}`);
|
|
4889
4945
|
debugLog(`[路径配置] extensions=${paths.extensions}`);
|
|
4890
4946
|
debugLog(`[路径配置] target=${paths.target}`);
|
|
4891
4947
|
debugLog(`[路径配置] config=${paths.config}`);
|
|
4892
4948
|
debugLog(`[路径配置] workspace=${paths.workspace}`);
|
|
4949
|
+
const machineId = generateMachineId();
|
|
4950
|
+
const localCode = buildLocalCode(this.config.phone, machineId);
|
|
4951
|
+
debugLog(`[机器码] machineId=${machineId}`);
|
|
4952
|
+
debugLog(`[机器码] localCode=${localCode}`);
|
|
4893
4953
|
const token = await this.doLogin();
|
|
4894
|
-
const boundConfig = await this.doFetchBoundConfig(token);
|
|
4954
|
+
const boundConfig = await this.doFetchBoundConfig(token, localCode);
|
|
4895
4955
|
await this.doCleanOldFiles(paths.target);
|
|
4896
4956
|
await this.doDownloadFromNpm(paths);
|
|
4897
|
-
await this.doUpdateConfig(paths, boundConfig
|
|
4957
|
+
await this.doUpdateConfig(paths, boundConfig);
|
|
4958
|
+
try {
|
|
4959
|
+
await fs.rm(paths.temp, { recursive: true, force: true });
|
|
4960
|
+
debugLog("[安装完成] 清理根临时目录完成");
|
|
4961
|
+
} catch {
|
|
4962
|
+
debugLog("[安装完成] 清理根临时目录失败(忽略)");
|
|
4963
|
+
}
|
|
4898
4964
|
this.spinner.stop();
|
|
4899
4965
|
} catch (error) {
|
|
4900
4966
|
this.spinner.stop();
|
|
4901
4967
|
throw error;
|
|
4902
4968
|
}
|
|
4903
4969
|
}
|
|
4970
|
+
/**
|
|
4971
|
+
* 获取前缀文本(用于显示安装进度)
|
|
4972
|
+
*/
|
|
4904
4973
|
getPrefixText() {
|
|
4905
4974
|
return this.prefixText;
|
|
4906
4975
|
}
|
|
4976
|
+
/**
|
|
4977
|
+
* 获取完整的 npm 包名(包含版本号)
|
|
4978
|
+
*/
|
|
4907
4979
|
getPackageName() {
|
|
4908
4980
|
if (this.config.pluginVersion) {
|
|
4909
4981
|
return `${PLUGIN_PACKAGE_NAME}@${this.config.pluginVersion}`;
|
|
4910
4982
|
}
|
|
4911
4983
|
return PLUGIN_PACKAGE_NAME;
|
|
4912
4984
|
}
|
|
4985
|
+
/**
|
|
4986
|
+
* 执行用户登录
|
|
4987
|
+
*/
|
|
4913
4988
|
async doLogin() {
|
|
4914
|
-
const env = this.config.env || "test";
|
|
4915
4989
|
this.spinner.prefixText = this.prefixText;
|
|
4916
4990
|
this.spinner.text = chalk.cyan("正在验证账号...");
|
|
4917
4991
|
debugLog(`[用户登录] 手机号: ${this.config.phone}`);
|
|
4918
|
-
debugLog("[用户登录] RSA 加密密码...");
|
|
4919
4992
|
const encryptedPassword = rsaEncrypt(this.config.userPass);
|
|
4920
4993
|
if (!encryptedPassword) {
|
|
4921
|
-
throw new
|
|
4994
|
+
throw new AppError$1(ERROR_CODES$1.LOGIN_FAILED, "密码加密失败");
|
|
4922
4995
|
}
|
|
4923
|
-
|
|
4924
|
-
debugLog("[用户登录] 调用登录接口...");
|
|
4925
|
-
const token = await login(this.config.phone, encryptedPassword, env);
|
|
4996
|
+
const token = await login(this.config.phone, encryptedPassword, this.envConfig);
|
|
4926
4997
|
this.prefixText += chalk.green(` ✓ 账号验证成功
|
|
4927
4998
|
`);
|
|
4928
|
-
debugLog("[用户登录] 登录成功");
|
|
4929
4999
|
return token;
|
|
4930
5000
|
}
|
|
4931
|
-
|
|
4932
|
-
|
|
5001
|
+
/**
|
|
5002
|
+
* 获取绑定配置
|
|
5003
|
+
*/
|
|
5004
|
+
async doFetchBoundConfig(token, localCode) {
|
|
4933
5005
|
this.spinner.prefixText = this.prefixText;
|
|
4934
5006
|
this.spinner.text = chalk.cyan("正在获取绑定信息...");
|
|
4935
|
-
|
|
4936
|
-
const boundConfig = await fetchBoundConfig(token, this.config.phone, env);
|
|
5007
|
+
const boundConfig = await fetchBoundConfig(token, this.config.phone, localCode, this.envConfig);
|
|
4937
5008
|
debugLog(`[获取配置] agentId=${boundConfig.agentId}, appKey=${boundConfig.appKey?.slice(0, 8)}...`);
|
|
4938
5009
|
this.prefixText += chalk.green(` ✓ 绑定信息获取成功
|
|
4939
5010
|
`);
|
|
4940
|
-
debugLog("[获取配置] 获取成功");
|
|
4941
5011
|
return boundConfig;
|
|
4942
5012
|
}
|
|
5013
|
+
/**
|
|
5014
|
+
* 清理旧版本文件
|
|
5015
|
+
*/
|
|
4943
5016
|
async doCleanOldFiles(targetPath) {
|
|
4944
5017
|
this.spinner.prefixText = this.prefixText;
|
|
4945
5018
|
this.spinner.text = chalk.cyan("正在检查已安装版本...");
|
|
4946
|
-
debugLog(`[清理旧版本] 检查目录: ${targetPath}`);
|
|
4947
5019
|
try {
|
|
4948
5020
|
await fs.access(targetPath);
|
|
4949
5021
|
this.spinner.text = chalk.cyan("正在清理旧文件...");
|
|
@@ -4955,29 +5027,26 @@ class LocalInstaller {
|
|
|
4955
5027
|
} catch {
|
|
4956
5028
|
this.prefixText += chalk.green(` ✓ 无旧版本需要清理
|
|
4957
5029
|
`);
|
|
4958
|
-
debugLog("[清理旧版本] 未发现旧版本");
|
|
4959
5030
|
}
|
|
4960
5031
|
}
|
|
5032
|
+
/**
|
|
5033
|
+
* 从 npm 下载并安装插件
|
|
5034
|
+
*/
|
|
4961
5035
|
async doDownloadFromNpm(paths) {
|
|
4962
5036
|
this.spinner.prefixText = this.prefixText;
|
|
4963
5037
|
this.spinner.text = chalk.cyan("正在准备安装目录...");
|
|
4964
|
-
debugLog("[下载插件] 创建扩展目录...");
|
|
4965
5038
|
await fs.mkdir(paths.extensions, { recursive: true });
|
|
4966
5039
|
debugLog(`[下载插件] 扩展目录: ${paths.extensions}`);
|
|
4967
5040
|
const tempDir = path.join(paths.temp, `install-${Date.now()}`);
|
|
4968
|
-
debugLog(`[下载插件] 创建临时目录: ${tempDir}`);
|
|
4969
5041
|
await fs.mkdir(tempDir, { recursive: true });
|
|
4970
5042
|
this.spinner.prefixText = this.prefixText;
|
|
4971
5043
|
this.spinner.text = chalk.cyan("正在下载插件...");
|
|
4972
|
-
debugLog("[下载插件] 执行 npm pack 下载源码包");
|
|
4973
|
-
debugLog(`[下载插件] 工作目录: ${tempDir}`);
|
|
4974
5044
|
const maxRetries = 3;
|
|
4975
5045
|
let lastError = "";
|
|
4976
5046
|
let tarballPath = "";
|
|
4977
5047
|
for (let i = 1; i <= maxRetries; i++) {
|
|
4978
5048
|
this.spinner.prefixText = this.prefixText;
|
|
4979
5049
|
this.spinner.text = chalk.cyan(`正在下载 (${i}/${maxRetries})...`);
|
|
4980
|
-
debugLog(`[下载插件] 第 ${i} 次尝试下载 tarball...`);
|
|
4981
5050
|
try {
|
|
4982
5051
|
const packageName = this.getPackageName();
|
|
4983
5052
|
debugLog(`[下载插件] 下载包名: ${packageName}`);
|
|
@@ -4993,16 +5062,14 @@ class LocalInstaller {
|
|
|
4993
5062
|
break;
|
|
4994
5063
|
} catch (error) {
|
|
4995
5064
|
lastError = error.message || "未知错误";
|
|
4996
|
-
debugLog(`[下载插件] 第 ${i} 次尝试失败: ${lastError}`);
|
|
4997
5065
|
if (i < maxRetries) {
|
|
4998
5066
|
this.spinner.text = chalk.cyan(`下载失败,3秒后重试...`);
|
|
4999
|
-
debugLog("[下载插件] 等待 3 秒后重试...");
|
|
5000
5067
|
await new Promise((resolve2) => setTimeout(resolve2, 3e3));
|
|
5001
5068
|
}
|
|
5002
5069
|
}
|
|
5003
5070
|
}
|
|
5004
5071
|
if (!tarballPath || !await fs.access(tarballPath).then(() => true).catch(() => false)) {
|
|
5005
|
-
throw new
|
|
5072
|
+
throw new AppError$1(ERROR_CODES$1.NPM_INSTALL_FAILED, `tarball 下载失败: ${lastError}`);
|
|
5006
5073
|
}
|
|
5007
5074
|
this.spinner.prefixText = this.prefixText;
|
|
5008
5075
|
this.spinner.text = chalk.cyan("正在解压插件...");
|
|
@@ -5017,17 +5084,15 @@ class LocalInstaller {
|
|
|
5017
5084
|
});
|
|
5018
5085
|
debugLog("[下载插件] tarball 解压成功");
|
|
5019
5086
|
} catch (error) {
|
|
5020
|
-
throw new
|
|
5087
|
+
throw new AppError$1(ERROR_CODES$1.NPM_INSTALL_FAILED, `tarball 解压失败: ${error.message}`);
|
|
5021
5088
|
}
|
|
5022
5089
|
const pluginPath = path.join(paths.target, "package.json");
|
|
5023
5090
|
const pluginExists = await fs.access(pluginPath).then(() => true).catch(() => false);
|
|
5024
5091
|
if (!pluginExists) {
|
|
5025
|
-
throw new
|
|
5092
|
+
throw new AppError$1(ERROR_CODES$1.NPM_INSTALL_FAILED, "插件解压后未找到 package.json");
|
|
5026
5093
|
}
|
|
5027
|
-
debugLog("[下载插件] 插件解压成功");
|
|
5028
5094
|
this.spinner.prefixText = this.prefixText;
|
|
5029
5095
|
this.spinner.text = chalk.cyan("正在安装依赖...");
|
|
5030
|
-
debugLog("[下载插件] 执行 npm install 安装生产环境依赖");
|
|
5031
5096
|
try {
|
|
5032
5097
|
await execAsync("npm install --omit=dev --ignore-scripts --quiet", {
|
|
5033
5098
|
cwd: paths.target,
|
|
@@ -5035,39 +5100,36 @@ class LocalInstaller {
|
|
|
5035
5100
|
});
|
|
5036
5101
|
debugLog("[下载插件] npm install 执行成功");
|
|
5037
5102
|
} catch (error) {
|
|
5038
|
-
const errorMsg = error.message
|
|
5039
|
-
if (errorMsg.includes("npm warn") || errorMsg.includes("deprecated"))
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
debugLog(`[下载插件] npm install 失败: ${errorMsg}`);
|
|
5103
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
5104
|
+
if (errorMsg.includes("npm warn") || errorMsg.includes("deprecated")) ;
|
|
5105
|
+
else {
|
|
5106
|
+
throw new AppError$1(ERROR_CODES$1.NPM_INSTALL_FAILED, `依赖安装失败: ${errorMsg}`);
|
|
5043
5107
|
}
|
|
5044
5108
|
}
|
|
5045
5109
|
this.prefixText += chalk.green(` ✓ 插件安装完成
|
|
5046
5110
|
`);
|
|
5047
5111
|
try {
|
|
5048
5112
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
5049
|
-
debugLog("[下载插件]
|
|
5113
|
+
debugLog("[下载插件] 清理临时下载目录完成");
|
|
5050
5114
|
} catch {
|
|
5051
|
-
debugLog("[下载插件] 清理临时目录失败(忽略)");
|
|
5052
5115
|
}
|
|
5053
5116
|
}
|
|
5054
|
-
|
|
5117
|
+
/**
|
|
5118
|
+
* 更新配置文件
|
|
5119
|
+
*/
|
|
5120
|
+
async doUpdateConfig(paths, boundConfig) {
|
|
5055
5121
|
this.spinner.prefixText = this.prefixText;
|
|
5056
5122
|
this.spinner.text = chalk.cyan("正在生成配置...");
|
|
5057
|
-
debugLog("[更新配置] 创建目录...");
|
|
5058
5123
|
await fs.mkdir(paths.home, { recursive: true });
|
|
5059
5124
|
await fs.mkdir(paths.workspace, { recursive: true });
|
|
5060
|
-
debugLog("[更新配置] 目录创建完成");
|
|
5061
|
-
debugLog("[更新配置] 读取原有配置...");
|
|
5062
5125
|
let originalConfig = {};
|
|
5063
5126
|
try {
|
|
5064
5127
|
const content = await fs.readFile(paths.config, "utf-8");
|
|
5065
5128
|
originalConfig = JSON.parse(content);
|
|
5066
5129
|
debugLog("[更新配置] 读取原有配置成功");
|
|
5067
5130
|
} catch {
|
|
5068
|
-
debugLog("[更新配置] 无原有配置");
|
|
5069
5131
|
}
|
|
5070
|
-
const config =
|
|
5132
|
+
const config = this.envConfig;
|
|
5071
5133
|
const newConfig = {
|
|
5072
5134
|
// diagnostics: 诊断配置
|
|
5073
5135
|
diagnostics: {
|
|
@@ -5160,10 +5222,9 @@ class LocalInstaller {
|
|
|
5160
5222
|
channels: {
|
|
5161
5223
|
"openclaw-workclaw": {
|
|
5162
5224
|
enabled: true,
|
|
5163
|
-
connectionMode: "websocket",
|
|
5164
5225
|
appKey: boundConfig.appKey,
|
|
5165
5226
|
appSecret: boundConfig.appSecret,
|
|
5166
|
-
baseUrl: this.config.customIp
|
|
5227
|
+
baseUrl: this.config.customIp ? `http://${this.config.customIp}` : config.DEFAULT_BASE_URL,
|
|
5167
5228
|
websocketUrl: this.config.wsUrl || config.DEFAULT_WS_URL,
|
|
5168
5229
|
allowInsecureTls: true,
|
|
5169
5230
|
allowRawJsonPayload: true,
|
|
@@ -5216,6 +5277,9 @@ class LocalInstaller {
|
|
|
5216
5277
|
},
|
|
5217
5278
|
// plugins: 插件配置
|
|
5218
5279
|
plugins: {
|
|
5280
|
+
load: {
|
|
5281
|
+
paths: [paths.extensions]
|
|
5282
|
+
},
|
|
5219
5283
|
allow: [config.PLUGIN_NAME],
|
|
5220
5284
|
installs: {
|
|
5221
5285
|
[config.PLUGIN_NAME]: {
|
|
@@ -5232,36 +5296,16 @@ class LocalInstaller {
|
|
|
5232
5296
|
const finalConfig = deepMerge(originalConfig, newConfig);
|
|
5233
5297
|
this.spinner.prefixText = this.prefixText;
|
|
5234
5298
|
this.spinner.text = chalk.cyan("正在写入配置...");
|
|
5235
|
-
debugLog("[更新配置] 写入配置文件...");
|
|
5236
5299
|
await fs.writeFile(paths.config, JSON.stringify(finalConfig, null, 2), "utf-8");
|
|
5237
5300
|
this.prefixText += chalk.green(` ✓ 配置更新完成
|
|
5238
5301
|
`);
|
|
5239
5302
|
debugLog(`[更新配置] 配置文件写入成功: ${paths.config}`);
|
|
5240
5303
|
}
|
|
5241
5304
|
}
|
|
5242
|
-
function checkEnv() {
|
|
5243
|
-
debugLog("[环境检查] 检测 Node.js 版本...");
|
|
5244
|
-
const nodeVersion = execSync("node --version", { stdio: "pipe" }).toString().trim();
|
|
5245
|
-
debugLog(`[环境检查] Node.js 版本: ${nodeVersion}`);
|
|
5246
|
-
if (!semver.gte(nodeVersion, "18.0.0")) {
|
|
5247
|
-
throw new AppError2(ERROR_CODES.NODE_VERSION_LOW, `Node.js 版本需要 >= 18.0.0,当前版本: ${nodeVersion}`);
|
|
5248
|
-
}
|
|
5249
|
-
debugLog("[环境检查] Node.js 版本检查通过");
|
|
5250
|
-
debugLog("[环境检查] 检测 npm...");
|
|
5251
|
-
try {
|
|
5252
|
-
const npmVersion = execSync("npm --version", { stdio: "pipe" }).toString().trim();
|
|
5253
|
-
debugLog(`[环境检查] npm 版本: ${npmVersion}`);
|
|
5254
|
-
debugLog("[环境检查] npm 检测通过");
|
|
5255
|
-
} catch {
|
|
5256
|
-
throw new AppError2(ERROR_CODES.NPM_NOT_FOUND, "未检测到 npm,请先安装 Node.js 和 npm");
|
|
5257
|
-
}
|
|
5258
|
-
}
|
|
5259
5305
|
async function createLocalCommand(options) {
|
|
5260
5306
|
setDebug(!!options.debug);
|
|
5261
|
-
debugLog("[初始化] 开始处理...");
|
|
5262
5307
|
debugLog(`[初始化] 参数: env=${options.env}, phone=${options.phone}, customIp=${options.customIp}, debug=${options.debug}`);
|
|
5263
5308
|
checkEnv();
|
|
5264
|
-
debugLog("[初始化] 环境检查通过");
|
|
5265
5309
|
try {
|
|
5266
5310
|
let env = options.env;
|
|
5267
5311
|
let phone = options.phone;
|
|
@@ -5294,8 +5338,9 @@ async function createLocalCommand(options) {
|
|
|
5294
5338
|
if (!value || value.trim() === "") {
|
|
5295
5339
|
return chalk.red("手机号码不能为空");
|
|
5296
5340
|
}
|
|
5297
|
-
|
|
5298
|
-
|
|
5341
|
+
const result = phoneSchema.safeParse(value);
|
|
5342
|
+
if (!result.success) {
|
|
5343
|
+
return chalk.yellow(result.error.issues[0].message);
|
|
5299
5344
|
}
|
|
5300
5345
|
return true;
|
|
5301
5346
|
}
|
|
@@ -5310,8 +5355,9 @@ async function createLocalCommand(options) {
|
|
|
5310
5355
|
name: "userPass",
|
|
5311
5356
|
message: chalk.cyan("请输入用户密码:"),
|
|
5312
5357
|
validate: (value) => {
|
|
5313
|
-
|
|
5314
|
-
|
|
5358
|
+
const result = userPassSchema.safeParse(value);
|
|
5359
|
+
if (!result.success) {
|
|
5360
|
+
return chalk.red(result.error.issues[0].message);
|
|
5315
5361
|
}
|
|
5316
5362
|
return true;
|
|
5317
5363
|
}
|
|
@@ -5394,7 +5440,7 @@ async function createLocalCommand(options) {
|
|
|
5394
5440
|
}
|
|
5395
5441
|
}
|
|
5396
5442
|
function registerCommands(program2) {
|
|
5397
|
-
program2.command("local").description("本地账户安装(需要登录)").option("-e, --env <env>", "环境 (test/prod/custom)").option("--phone <phone>", "手机号码").option("--user-pass <userPass>", "用户密码").option("--
|
|
5443
|
+
program2.command("local").description("本地账户安装(需要登录)").option("-e, --env <env>", "环境 (test/prod/custom)").option("--phone <phone>", "手机号码").option("--user-pass <userPass>", "用户密码").option("--custom-ip <ip>", "自定义后端 IP(仅 custom 环境生效)").option("--ws-url <url>", "自定义 WebSocket URL(仅 custom 环境生效,默认自动生成)").option("--plugin-version <plugin-version>", "插件版本号(默认最新版)").option("--openclaw-path <path>", "OpenClaw 安装目录路径(默认 ~/.openclaw)").option("--debug", "开启调试日志").action(createLocalCommand);
|
|
5398
5444
|
}
|
|
5399
5445
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
5400
5446
|
const __dirname$1 = dirname(__filename$1);
|