@workclaw/cli 1.0.320 → 1.0.322
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 +0 -1
- 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-CzXh8eTR.js → index-ZySzMX4-.js} +269 -227
- package/dist/index.js +1 -1
- package/dist/local/apis/index.d.ts +8 -7
- package/dist/local/apis/index.d.ts.map +1 -1
- package/dist/local/installer/installer.d.ts +34 -0
- 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: {
|
|
@@ -385,7 +435,7 @@ class BoxInstaller {
|
|
|
385
435
|
// models: 模型配置
|
|
386
436
|
models: {
|
|
387
437
|
// 配置合并模式:'merge' 合并 | 'replace' 替换
|
|
388
|
-
mode: "
|
|
438
|
+
mode: "replace",
|
|
389
439
|
providers: {
|
|
390
440
|
"siliconflow-minimax": {
|
|
391
441
|
// 模型 API 基础地址
|
|
@@ -503,8 +553,6 @@ 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
|
// 应用密钥
|
|
@@ -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,28 +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
845
|
async function login(phone, password, config) {
|
|
837
846
|
const url = `${config.API.TUZAI_BASE_URL}/user/login/pass`;
|
|
838
|
-
debugLog("[登录] 开始登录...");
|
|
839
|
-
debugLog(`[登录] 请求地址: ${url}`);
|
|
840
|
-
debugLog(`[登录] 请求参数: phone=${phone}, password=***`);
|
|
841
847
|
try {
|
|
842
848
|
const response = await httpPost(url, {
|
|
843
849
|
phone,
|
|
@@ -850,25 +856,22 @@ async function login(phone, password, config) {
|
|
|
850
856
|
return data.data.token;
|
|
851
857
|
}
|
|
852
858
|
debugLog("[登录] 登录失败");
|
|
853
|
-
throw new
|
|
859
|
+
throw new AppError$1(ERROR_CODES$1.LOGIN_FAILED, data.message || "登录失败");
|
|
854
860
|
} catch (error) {
|
|
855
|
-
if (error instanceof
|
|
861
|
+
if (error instanceof AppError$1) {
|
|
856
862
|
throw error;
|
|
857
863
|
}
|
|
858
864
|
debugLog(`[登录] 发生错误: ${error.message}`);
|
|
859
|
-
throw new
|
|
865
|
+
throw new AppError$1(ERROR_CODES$1.LOGIN_FAILED, error.message);
|
|
860
866
|
}
|
|
861
867
|
}
|
|
862
|
-
async function fetchBoundConfig(token, phone, config) {
|
|
868
|
+
async function fetchBoundConfig(token, phone, localCode, config) {
|
|
863
869
|
const url = `${config.API.TUZAI_BASE_URL}/work-bot/local/bound/init`;
|
|
864
|
-
debugLog("[获取绑定配置] 开始获取绑定配置...");
|
|
865
|
-
debugLog(`[获取绑定配置] 请求地址: ${url}`);
|
|
866
|
-
debugLog(`[获取绑定配置] 请求参数: phone=${phone}, localCode=001`);
|
|
867
870
|
debugLog(`[获取绑定配置] 请求头: Authorization=${token.substring(0, 20)}...`);
|
|
868
871
|
try {
|
|
869
872
|
const response = await httpPost(url, {
|
|
870
873
|
phone,
|
|
871
|
-
localCode
|
|
874
|
+
localCode
|
|
872
875
|
}, {
|
|
873
876
|
headers: {
|
|
874
877
|
Authorization: token
|
|
@@ -894,20 +897,48 @@ async function fetchBoundConfig(token, phone, config) {
|
|
|
894
897
|
debugLog(`[获取绑定配置] modelApiBaseUrl: ${boundConfig.modelApiBaseUrl || "undefined"}`);
|
|
895
898
|
if (!boundConfig.appKey || !boundConfig.appSecret || !boundConfig.agentId) {
|
|
896
899
|
debugLog("[获取绑定配置] 缺少必要字段");
|
|
897
|
-
throw new
|
|
900
|
+
throw new AppError$1(ERROR_CODES$1.GET_BOUND_CONFIG_FAILED, "获取绑定配置失败:缺少必要字段");
|
|
898
901
|
}
|
|
899
902
|
return boundConfig;
|
|
900
903
|
}
|
|
901
904
|
debugLog("[获取绑定配置] 获取绑定配置失败");
|
|
902
|
-
throw new
|
|
905
|
+
throw new AppError$1(ERROR_CODES$1.GET_BOUND_CONFIG_FAILED, data.message || data.msg || "获取绑定配置失败");
|
|
903
906
|
} catch (error) {
|
|
904
|
-
if (error instanceof
|
|
907
|
+
if (error instanceof AppError$1) {
|
|
905
908
|
throw error;
|
|
906
909
|
}
|
|
907
910
|
debugLog(`[获取绑定配置] 发生错误: ${error.message}`);
|
|
908
|
-
throw new
|
|
911
|
+
throw new AppError$1(ERROR_CODES$1.GET_BOUND_CONFIG_FAILED, error.message);
|
|
909
912
|
}
|
|
910
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
|
+
});
|
|
911
942
|
var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz";
|
|
912
943
|
function int2char(n) {
|
|
913
944
|
return BI_RM.charAt(n);
|
|
@@ -4798,6 +4829,29 @@ function rsaEncrypt(txt) {
|
|
|
4798
4829
|
const result = encrypt.encrypt(txt);
|
|
4799
4830
|
return result || null;
|
|
4800
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
|
+
}
|
|
4801
4855
|
function getHomeDir() {
|
|
4802
4856
|
return process$1.env.HOME || process$1.env.USERPROFILE || "";
|
|
4803
4857
|
}
|
|
@@ -4816,12 +4870,20 @@ function execAsync(command, options) {
|
|
|
4816
4870
|
function deepMerge(target, source) {
|
|
4817
4871
|
const result = { ...target };
|
|
4818
4872
|
for (const key in source) {
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
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
|
+
}
|
|
4825
4887
|
}
|
|
4826
4888
|
}
|
|
4827
4889
|
return result;
|
|
@@ -4831,38 +4893,32 @@ class LocalInstaller {
|
|
|
4831
4893
|
prefixText = "";
|
|
4832
4894
|
config;
|
|
4833
4895
|
envConfig;
|
|
4896
|
+
/**
|
|
4897
|
+
* 构造函数
|
|
4898
|
+
*/
|
|
4834
4899
|
constructor(config) {
|
|
4835
4900
|
this.config = config;
|
|
4836
4901
|
const env = this.config.env || "test";
|
|
4837
4902
|
this.envConfig = getConfig(env, this.config.customIp);
|
|
4838
4903
|
this.spinner = ora({ color: "cyan" }).start();
|
|
4839
4904
|
}
|
|
4905
|
+
/**
|
|
4906
|
+
* 验证安装配置
|
|
4907
|
+
*/
|
|
4840
4908
|
validateConfig() {
|
|
4841
|
-
|
|
4842
|
-
if (!
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
if (!this.config.userPass) {
|
|
4846
|
-
throw new AppError2(ERROR_CODES.USER_PASS_REQUIRED, "用户密码不能为空,请使用 --user-pass 参数");
|
|
4847
|
-
}
|
|
4848
|
-
if (this.config.env === "custom") {
|
|
4849
|
-
debugLog("[验证配置] 检查自定义环境 IP...");
|
|
4850
|
-
if (!this.config.customIp) {
|
|
4851
|
-
throw new AppError2(ERROR_CODES.INVALID_ARGUMENT, "自定义环境必须提供 --custom-ip 参数");
|
|
4852
|
-
}
|
|
4853
|
-
const result = ipv4Schema.safeParse(this.config.customIp);
|
|
4854
|
-
if (!result.success) {
|
|
4855
|
-
throw new AppError2(ERROR_CODES.INVALID_ARGUMENT, result.error.issues[0].message);
|
|
4856
|
-
}
|
|
4857
|
-
debugLog("[验证配置] 自定义环境 IP 验证通过");
|
|
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);
|
|
4858
4913
|
}
|
|
4859
|
-
debugLog("[验证配置] 配置验证通过");
|
|
4860
4914
|
}
|
|
4915
|
+
/**
|
|
4916
|
+
* 执行安装流程
|
|
4917
|
+
*/
|
|
4861
4918
|
async install() {
|
|
4862
4919
|
try {
|
|
4863
4920
|
debugLog("[安装开始]");
|
|
4864
4921
|
this.validateConfig();
|
|
4865
|
-
const env = this.config.env || "test";
|
|
4866
4922
|
let baseDir;
|
|
4867
4923
|
if (this.config.openclawPath) {
|
|
4868
4924
|
debugLog(`[路径验证] 验证自定义路径: ${this.config.openclawPath}`);
|
|
@@ -4870,7 +4926,7 @@ 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 {
|
|
@@ -4890,8 +4946,12 @@ class LocalInstaller {
|
|
|
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
4957
|
await this.doUpdateConfig(paths, boundConfig);
|
|
@@ -4907,47 +4967,55 @@ class LocalInstaller {
|
|
|
4907
4967
|
throw error;
|
|
4908
4968
|
}
|
|
4909
4969
|
}
|
|
4970
|
+
/**
|
|
4971
|
+
* 获取前缀文本(用于显示安装进度)
|
|
4972
|
+
*/
|
|
4910
4973
|
getPrefixText() {
|
|
4911
4974
|
return this.prefixText;
|
|
4912
4975
|
}
|
|
4976
|
+
/**
|
|
4977
|
+
* 获取完整的 npm 包名(包含版本号)
|
|
4978
|
+
*/
|
|
4913
4979
|
getPackageName() {
|
|
4914
4980
|
if (this.config.pluginVersion) {
|
|
4915
4981
|
return `${PLUGIN_PACKAGE_NAME}@${this.config.pluginVersion}`;
|
|
4916
4982
|
}
|
|
4917
4983
|
return PLUGIN_PACKAGE_NAME;
|
|
4918
4984
|
}
|
|
4985
|
+
/**
|
|
4986
|
+
* 执行用户登录
|
|
4987
|
+
*/
|
|
4919
4988
|
async doLogin() {
|
|
4920
4989
|
this.spinner.prefixText = this.prefixText;
|
|
4921
4990
|
this.spinner.text = chalk.cyan("正在验证账号...");
|
|
4922
4991
|
debugLog(`[用户登录] 手机号: ${this.config.phone}`);
|
|
4923
|
-
debugLog("[用户登录] RSA 加密密码...");
|
|
4924
4992
|
const encryptedPassword = rsaEncrypt(this.config.userPass);
|
|
4925
4993
|
if (!encryptedPassword) {
|
|
4926
|
-
throw new
|
|
4994
|
+
throw new AppError$1(ERROR_CODES$1.LOGIN_FAILED, "密码加密失败");
|
|
4927
4995
|
}
|
|
4928
|
-
debugLog("[用户登录] 密码加密成功");
|
|
4929
|
-
debugLog("[用户登录] 调用登录接口...");
|
|
4930
4996
|
const token = await login(this.config.phone, encryptedPassword, this.envConfig);
|
|
4931
4997
|
this.prefixText += chalk.green(` ✓ 账号验证成功
|
|
4932
4998
|
`);
|
|
4933
|
-
debugLog("[用户登录] 登录成功");
|
|
4934
4999
|
return token;
|
|
4935
5000
|
}
|
|
4936
|
-
|
|
5001
|
+
/**
|
|
5002
|
+
* 获取绑定配置
|
|
5003
|
+
*/
|
|
5004
|
+
async doFetchBoundConfig(token, localCode) {
|
|
4937
5005
|
this.spinner.prefixText = this.prefixText;
|
|
4938
5006
|
this.spinner.text = chalk.cyan("正在获取绑定信息...");
|
|
4939
|
-
|
|
4940
|
-
const boundConfig = await fetchBoundConfig(token, this.config.phone, this.envConfig);
|
|
5007
|
+
const boundConfig = await fetchBoundConfig(token, this.config.phone, localCode, this.envConfig);
|
|
4941
5008
|
debugLog(`[获取配置] agentId=${boundConfig.agentId}, appKey=${boundConfig.appKey?.slice(0, 8)}...`);
|
|
4942
5009
|
this.prefixText += chalk.green(` ✓ 绑定信息获取成功
|
|
4943
5010
|
`);
|
|
4944
|
-
debugLog("[获取配置] 获取成功");
|
|
4945
5011
|
return boundConfig;
|
|
4946
5012
|
}
|
|
5013
|
+
/**
|
|
5014
|
+
* 清理旧版本文件
|
|
5015
|
+
*/
|
|
4947
5016
|
async doCleanOldFiles(targetPath) {
|
|
4948
5017
|
this.spinner.prefixText = this.prefixText;
|
|
4949
5018
|
this.spinner.text = chalk.cyan("正在检查已安装版本...");
|
|
4950
|
-
debugLog(`[清理旧版本] 检查目录: ${targetPath}`);
|
|
4951
5019
|
try {
|
|
4952
5020
|
await fs.access(targetPath);
|
|
4953
5021
|
this.spinner.text = chalk.cyan("正在清理旧文件...");
|
|
@@ -4959,29 +5027,26 @@ class LocalInstaller {
|
|
|
4959
5027
|
} catch {
|
|
4960
5028
|
this.prefixText += chalk.green(` ✓ 无旧版本需要清理
|
|
4961
5029
|
`);
|
|
4962
|
-
debugLog("[清理旧版本] 未发现旧版本");
|
|
4963
5030
|
}
|
|
4964
5031
|
}
|
|
5032
|
+
/**
|
|
5033
|
+
* 从 npm 下载并安装插件
|
|
5034
|
+
*/
|
|
4965
5035
|
async doDownloadFromNpm(paths) {
|
|
4966
5036
|
this.spinner.prefixText = this.prefixText;
|
|
4967
5037
|
this.spinner.text = chalk.cyan("正在准备安装目录...");
|
|
4968
|
-
debugLog("[下载插件] 创建扩展目录...");
|
|
4969
5038
|
await fs.mkdir(paths.extensions, { recursive: true });
|
|
4970
5039
|
debugLog(`[下载插件] 扩展目录: ${paths.extensions}`);
|
|
4971
5040
|
const tempDir = path.join(paths.temp, `install-${Date.now()}`);
|
|
4972
|
-
debugLog(`[下载插件] 创建临时目录: ${tempDir}`);
|
|
4973
5041
|
await fs.mkdir(tempDir, { recursive: true });
|
|
4974
5042
|
this.spinner.prefixText = this.prefixText;
|
|
4975
5043
|
this.spinner.text = chalk.cyan("正在下载插件...");
|
|
4976
|
-
debugLog("[下载插件] 执行 npm pack 下载源码包");
|
|
4977
|
-
debugLog(`[下载插件] 工作目录: ${tempDir}`);
|
|
4978
5044
|
const maxRetries = 3;
|
|
4979
5045
|
let lastError = "";
|
|
4980
5046
|
let tarballPath = "";
|
|
4981
5047
|
for (let i = 1; i <= maxRetries; i++) {
|
|
4982
5048
|
this.spinner.prefixText = this.prefixText;
|
|
4983
5049
|
this.spinner.text = chalk.cyan(`正在下载 (${i}/${maxRetries})...`);
|
|
4984
|
-
debugLog(`[下载插件] 第 ${i} 次尝试下载 tarball...`);
|
|
4985
5050
|
try {
|
|
4986
5051
|
const packageName = this.getPackageName();
|
|
4987
5052
|
debugLog(`[下载插件] 下载包名: ${packageName}`);
|
|
@@ -4997,16 +5062,14 @@ class LocalInstaller {
|
|
|
4997
5062
|
break;
|
|
4998
5063
|
} catch (error) {
|
|
4999
5064
|
lastError = error.message || "未知错误";
|
|
5000
|
-
debugLog(`[下载插件] 第 ${i} 次尝试失败: ${lastError}`);
|
|
5001
5065
|
if (i < maxRetries) {
|
|
5002
5066
|
this.spinner.text = chalk.cyan(`下载失败,3秒后重试...`);
|
|
5003
|
-
debugLog("[下载插件] 等待 3 秒后重试...");
|
|
5004
5067
|
await new Promise((resolve2) => setTimeout(resolve2, 3e3));
|
|
5005
5068
|
}
|
|
5006
5069
|
}
|
|
5007
5070
|
}
|
|
5008
5071
|
if (!tarballPath || !await fs.access(tarballPath).then(() => true).catch(() => false)) {
|
|
5009
|
-
throw new
|
|
5072
|
+
throw new AppError$1(ERROR_CODES$1.NPM_INSTALL_FAILED, `tarball 下载失败: ${lastError}`);
|
|
5010
5073
|
}
|
|
5011
5074
|
this.spinner.prefixText = this.prefixText;
|
|
5012
5075
|
this.spinner.text = chalk.cyan("正在解压插件...");
|
|
@@ -5021,17 +5084,15 @@ class LocalInstaller {
|
|
|
5021
5084
|
});
|
|
5022
5085
|
debugLog("[下载插件] tarball 解压成功");
|
|
5023
5086
|
} catch (error) {
|
|
5024
|
-
throw new
|
|
5087
|
+
throw new AppError$1(ERROR_CODES$1.NPM_INSTALL_FAILED, `tarball 解压失败: ${error.message}`);
|
|
5025
5088
|
}
|
|
5026
5089
|
const pluginPath = path.join(paths.target, "package.json");
|
|
5027
5090
|
const pluginExists = await fs.access(pluginPath).then(() => true).catch(() => false);
|
|
5028
5091
|
if (!pluginExists) {
|
|
5029
|
-
throw new
|
|
5092
|
+
throw new AppError$1(ERROR_CODES$1.NPM_INSTALL_FAILED, "插件解压后未找到 package.json");
|
|
5030
5093
|
}
|
|
5031
|
-
debugLog("[下载插件] 插件解压成功");
|
|
5032
5094
|
this.spinner.prefixText = this.prefixText;
|
|
5033
5095
|
this.spinner.text = chalk.cyan("正在安装依赖...");
|
|
5034
|
-
debugLog("[下载插件] 执行 npm install 安装生产环境依赖");
|
|
5035
5096
|
try {
|
|
5036
5097
|
await execAsync("npm install --omit=dev --ignore-scripts --quiet", {
|
|
5037
5098
|
cwd: paths.target,
|
|
@@ -5039,11 +5100,10 @@ class LocalInstaller {
|
|
|
5039
5100
|
});
|
|
5040
5101
|
debugLog("[下载插件] npm install 执行成功");
|
|
5041
5102
|
} catch (error) {
|
|
5042
|
-
const errorMsg = error.message
|
|
5043
|
-
if (errorMsg.includes("npm warn") || errorMsg.includes("deprecated"))
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
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}`);
|
|
5047
5107
|
}
|
|
5048
5108
|
}
|
|
5049
5109
|
this.prefixText += chalk.green(` ✓ 插件安装完成
|
|
@@ -5052,24 +5112,22 @@ class LocalInstaller {
|
|
|
5052
5112
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
5053
5113
|
debugLog("[下载插件] 清理临时下载目录完成");
|
|
5054
5114
|
} catch {
|
|
5055
|
-
debugLog("[下载插件] 清理临时下载目录失败(忽略)");
|
|
5056
5115
|
}
|
|
5057
5116
|
}
|
|
5117
|
+
/**
|
|
5118
|
+
* 更新配置文件
|
|
5119
|
+
*/
|
|
5058
5120
|
async doUpdateConfig(paths, boundConfig) {
|
|
5059
5121
|
this.spinner.prefixText = this.prefixText;
|
|
5060
5122
|
this.spinner.text = chalk.cyan("正在生成配置...");
|
|
5061
|
-
debugLog("[更新配置] 创建目录...");
|
|
5062
5123
|
await fs.mkdir(paths.home, { recursive: true });
|
|
5063
5124
|
await fs.mkdir(paths.workspace, { recursive: true });
|
|
5064
|
-
debugLog("[更新配置] 目录创建完成");
|
|
5065
|
-
debugLog("[更新配置] 读取原有配置...");
|
|
5066
5125
|
let originalConfig = {};
|
|
5067
5126
|
try {
|
|
5068
5127
|
const content = await fs.readFile(paths.config, "utf-8");
|
|
5069
5128
|
originalConfig = JSON.parse(content);
|
|
5070
5129
|
debugLog("[更新配置] 读取原有配置成功");
|
|
5071
5130
|
} catch {
|
|
5072
|
-
debugLog("[更新配置] 无原有配置");
|
|
5073
5131
|
}
|
|
5074
5132
|
const config = this.envConfig;
|
|
5075
5133
|
const newConfig = {
|
|
@@ -5084,7 +5142,7 @@ class LocalInstaller {
|
|
|
5084
5142
|
},
|
|
5085
5143
|
// models: 模型配置
|
|
5086
5144
|
models: {
|
|
5087
|
-
mode: "
|
|
5145
|
+
mode: "replace",
|
|
5088
5146
|
providers: {
|
|
5089
5147
|
"siliconflow-minimax": {
|
|
5090
5148
|
baseUrl: boundConfig.modelApiBaseUrl || config.MODEL_BASE_URL,
|
|
@@ -5164,7 +5222,6 @@ class LocalInstaller {
|
|
|
5164
5222
|
channels: {
|
|
5165
5223
|
"openclaw-workclaw": {
|
|
5166
5224
|
enabled: true,
|
|
5167
|
-
connectionMode: "websocket",
|
|
5168
5225
|
appKey: boundConfig.appKey,
|
|
5169
5226
|
appSecret: boundConfig.appSecret,
|
|
5170
5227
|
baseUrl: this.config.customIp ? `http://${this.config.customIp}` : config.DEFAULT_BASE_URL,
|
|
@@ -5220,6 +5277,9 @@ class LocalInstaller {
|
|
|
5220
5277
|
},
|
|
5221
5278
|
// plugins: 插件配置
|
|
5222
5279
|
plugins: {
|
|
5280
|
+
load: {
|
|
5281
|
+
paths: [paths.extensions]
|
|
5282
|
+
},
|
|
5223
5283
|
allow: [config.PLUGIN_NAME],
|
|
5224
5284
|
installs: {
|
|
5225
5285
|
[config.PLUGIN_NAME]: {
|
|
@@ -5236,36 +5296,16 @@ class LocalInstaller {
|
|
|
5236
5296
|
const finalConfig = deepMerge(originalConfig, newConfig);
|
|
5237
5297
|
this.spinner.prefixText = this.prefixText;
|
|
5238
5298
|
this.spinner.text = chalk.cyan("正在写入配置...");
|
|
5239
|
-
debugLog("[更新配置] 写入配置文件...");
|
|
5240
5299
|
await fs.writeFile(paths.config, JSON.stringify(finalConfig, null, 2), "utf-8");
|
|
5241
5300
|
this.prefixText += chalk.green(` ✓ 配置更新完成
|
|
5242
5301
|
`);
|
|
5243
5302
|
debugLog(`[更新配置] 配置文件写入成功: ${paths.config}`);
|
|
5244
5303
|
}
|
|
5245
5304
|
}
|
|
5246
|
-
function checkEnv() {
|
|
5247
|
-
debugLog("[环境检查] 检测 Node.js 版本...");
|
|
5248
|
-
const nodeVersion = execSync("node --version", { stdio: "pipe" }).toString().trim();
|
|
5249
|
-
debugLog(`[环境检查] Node.js 版本: ${nodeVersion}`);
|
|
5250
|
-
if (!semver.gte(nodeVersion, "18.0.0")) {
|
|
5251
|
-
throw new AppError2(ERROR_CODES.NODE_VERSION_LOW, `Node.js 版本需要 >= 18.0.0,当前版本: ${nodeVersion}`);
|
|
5252
|
-
}
|
|
5253
|
-
debugLog("[环境检查] Node.js 版本检查通过");
|
|
5254
|
-
debugLog("[环境检查] 检测 npm...");
|
|
5255
|
-
try {
|
|
5256
|
-
const npmVersion = execSync("npm --version", { stdio: "pipe" }).toString().trim();
|
|
5257
|
-
debugLog(`[环境检查] npm 版本: ${npmVersion}`);
|
|
5258
|
-
debugLog("[环境检查] npm 检测通过");
|
|
5259
|
-
} catch {
|
|
5260
|
-
throw new AppError2(ERROR_CODES.NPM_NOT_FOUND, "未检测到 npm,请先安装 Node.js 和 npm");
|
|
5261
|
-
}
|
|
5262
|
-
}
|
|
5263
5305
|
async function createLocalCommand(options) {
|
|
5264
5306
|
setDebug(!!options.debug);
|
|
5265
|
-
debugLog("[初始化] 开始处理...");
|
|
5266
5307
|
debugLog(`[初始化] 参数: env=${options.env}, phone=${options.phone}, customIp=${options.customIp}, debug=${options.debug}`);
|
|
5267
5308
|
checkEnv();
|
|
5268
|
-
debugLog("[初始化] 环境检查通过");
|
|
5269
5309
|
try {
|
|
5270
5310
|
let env = options.env;
|
|
5271
5311
|
let phone = options.phone;
|
|
@@ -5298,8 +5338,9 @@ async function createLocalCommand(options) {
|
|
|
5298
5338
|
if (!value || value.trim() === "") {
|
|
5299
5339
|
return chalk.red("手机号码不能为空");
|
|
5300
5340
|
}
|
|
5301
|
-
|
|
5302
|
-
|
|
5341
|
+
const result = phoneSchema.safeParse(value);
|
|
5342
|
+
if (!result.success) {
|
|
5343
|
+
return chalk.yellow(result.error.issues[0].message);
|
|
5303
5344
|
}
|
|
5304
5345
|
return true;
|
|
5305
5346
|
}
|
|
@@ -5314,8 +5355,9 @@ async function createLocalCommand(options) {
|
|
|
5314
5355
|
name: "userPass",
|
|
5315
5356
|
message: chalk.cyan("请输入用户密码:"),
|
|
5316
5357
|
validate: (value) => {
|
|
5317
|
-
|
|
5318
|
-
|
|
5358
|
+
const result = userPassSchema.safeParse(value);
|
|
5359
|
+
if (!result.success) {
|
|
5360
|
+
return chalk.red(result.error.issues[0].message);
|
|
5319
5361
|
}
|
|
5320
5362
|
return true;
|
|
5321
5363
|
}
|