@lark-apaas/openclaw-scripts-diagnose-cli 0.1.4-alpha.0 → 0.1.4-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2238 -493
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -23,12 +23,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
//#endregion
|
|
24
24
|
let node_process = require("node:process");
|
|
25
25
|
node_process = __toESM(node_process);
|
|
26
|
+
let node_path = require("node:path");
|
|
27
|
+
node_path = __toESM(node_path);
|
|
26
28
|
let json5 = require("json5");
|
|
27
29
|
json5 = __toESM(json5);
|
|
28
30
|
let node_fs = require("node:fs");
|
|
29
31
|
node_fs = __toESM(node_fs);
|
|
30
|
-
let node_path = require("node:path");
|
|
31
|
-
node_path = __toESM(node_path);
|
|
32
32
|
let node_child_process = require("node:child_process");
|
|
33
33
|
let node_crypto = require("node:crypto");
|
|
34
34
|
node_crypto = __toESM(node_crypto);
|
|
@@ -36,9 +36,26 @@ let node_os = require("node:os");
|
|
|
36
36
|
node_os = __toESM(node_os);
|
|
37
37
|
let node_stream = require("node:stream");
|
|
38
38
|
let node_stream_promises = require("node:stream/promises");
|
|
39
|
+
let _lark_apaas_http_client = require("@lark-apaas/http-client");
|
|
39
40
|
let node_assert = require("node:assert");
|
|
40
41
|
node_assert = __toESM(node_assert);
|
|
41
|
-
let
|
|
42
|
+
let json_diff = require("json-diff");
|
|
43
|
+
//#region src/version.ts
|
|
44
|
+
/**
|
|
45
|
+
* Machine-readable identifier of the running CLI build.
|
|
46
|
+
*
|
|
47
|
+
* prod build → "0.1.3" (the published semver)
|
|
48
|
+
* local-test → "local-test@2026-04-28T12:30:11.123Z"
|
|
49
|
+
*
|
|
50
|
+
* Exists separately from `help.ts`'s `versionBanner()`, which is for human
|
|
51
|
+
* display and includes prose like "(local test build, not a published
|
|
52
|
+
* release)". This one goes into telemetry payloads and log slicing — keep
|
|
53
|
+
* it terse and parseable.
|
|
54
|
+
*/
|
|
55
|
+
function getVersion() {
|
|
56
|
+
return "0.1.4-alpha.2";
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
42
59
|
//#region src/rule-engine/base.ts
|
|
43
60
|
/** Abstract base class for all diagnose rules */
|
|
44
61
|
var DiagnoseRule = class {
|
|
@@ -88,6 +105,9 @@ function topoSort(rules) {
|
|
|
88
105
|
}
|
|
89
106
|
//#endregion
|
|
90
107
|
//#region src/utils.ts
|
|
108
|
+
function getExtensionsDir(configPath) {
|
|
109
|
+
return node_path.default.join(node_path.default.dirname(configPath), "extensions");
|
|
110
|
+
}
|
|
91
111
|
/**
|
|
92
112
|
* Canonical provider-ref for the feishu app secret. Both
|
|
93
113
|
* `feishu_default_account` (multi-agent path) and `feishu_channel`
|
|
@@ -245,6 +265,32 @@ function shell(cmd, timeoutMs = 6e4) {
|
|
|
245
265
|
}).trim();
|
|
246
266
|
}
|
|
247
267
|
//#endregion
|
|
268
|
+
//#region src/oc-runtime.ts
|
|
269
|
+
/**
|
|
270
|
+
* 探测 openclaw 运行时版本。能成功跑出版本号 = 已安装且可用。
|
|
271
|
+
*
|
|
272
|
+
* 比单纯 fs.existsSync 路径更可靠:能挡住 dangling symlink、依赖丢失、
|
|
273
|
+
* bin 是错误版本等"看似装好但跑不起来"的状态。
|
|
274
|
+
*
|
|
275
|
+
* 失败(命令缺失 / 解析失败 / 超时)返回 null。
|
|
276
|
+
*/
|
|
277
|
+
function readOpenclawRuntimeVersion() {
|
|
278
|
+
try {
|
|
279
|
+
const lines = (0, node_child_process.execSync)("openclaw --version", {
|
|
280
|
+
encoding: "utf-8",
|
|
281
|
+
timeout: 5e3,
|
|
282
|
+
stdio: [
|
|
283
|
+
"ignore",
|
|
284
|
+
"pipe",
|
|
285
|
+
"ignore"
|
|
286
|
+
]
|
|
287
|
+
}).split("\n").map((l) => l.trim()).filter(Boolean);
|
|
288
|
+
return (lines[lines.length - 1]?.match(/(?:OpenClaw\s+)?(\d+\.\d+\.\d+)/))?.[1] ?? null;
|
|
289
|
+
} catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
//#endregion
|
|
248
294
|
//#region \0@oxc-project+runtime@0.115.0/helpers/decorate.js
|
|
249
295
|
function __decorate(decorators, target, key, desc) {
|
|
250
296
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -253,6 +299,33 @@ function __decorate(decorators, target, key, desc) {
|
|
|
253
299
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
254
300
|
}
|
|
255
301
|
//#endregion
|
|
302
|
+
//#region src/rules/openclaw-runtime-missing.ts
|
|
303
|
+
/**
|
|
304
|
+
* doctor 入口最先执行:检查 openclaw 是否已安装且可用(执行 `openclaw --version`)。
|
|
305
|
+
*
|
|
306
|
+
* 触发条件:openclaw 不可用 AND openclaw.json 存在。
|
|
307
|
+
* 配合 `config_file_missing`(reset)形成完整覆盖:
|
|
308
|
+
* - oc 可用 + config 存在 → 都 pass,进业务规则
|
|
309
|
+
* - oc 可用 + config 缺 → 仅 config_file_missing fail(reset)
|
|
310
|
+
* - oc 缺 + config 存在 → 仅本规则 fail(user_confirm: install_openclaw)
|
|
311
|
+
* - oc 缺 + config 缺 → 仅 config_file_missing fail(reset 同时装 oc + 初始化 config)
|
|
312
|
+
*/
|
|
313
|
+
let OpenclawRuntimeMissingRule = class OpenclawRuntimeMissingRule extends DiagnoseRule {
|
|
314
|
+
validate(ctx) {
|
|
315
|
+
if (readOpenclawRuntimeVersion() != null) return { pass: true };
|
|
316
|
+
if (!node_fs.default.existsSync(ctx.configPath)) return { pass: true };
|
|
317
|
+
return {
|
|
318
|
+
pass: false,
|
|
319
|
+
action: "install_openclaw",
|
|
320
|
+
message: `openclaw 不可用(无法执行 'openclaw --version'),但 ${ctx.configPath} 已存在;请重新安装 openclaw`
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
OpenclawRuntimeMissingRule = __decorate([Rule({
|
|
325
|
+
key: "openclaw_runtime_missing",
|
|
326
|
+
repairMode: "user-confirm"
|
|
327
|
+
})], OpenclawRuntimeMissingRule);
|
|
328
|
+
//#endregion
|
|
256
329
|
//#region src/rules/multi-process-detect.ts
|
|
257
330
|
let ProcessStatusRule = class ProcessStatusRule extends DiagnoseRule {
|
|
258
331
|
validate(_ctx) {
|
|
@@ -415,7 +488,7 @@ let TemplateVarsUnreplacedRule = class TemplateVarsUnreplacedRule extends Diagno
|
|
|
415
488
|
};
|
|
416
489
|
}
|
|
417
490
|
repair(ctx) {
|
|
418
|
-
const map = ctx.templateVars;
|
|
491
|
+
const map = ctx.vars.templateVars;
|
|
419
492
|
if (!map || Object.keys(map).length === 0) return;
|
|
420
493
|
replaceInPlace(ctx.config, Object.entries(map));
|
|
421
494
|
}
|
|
@@ -423,7 +496,8 @@ let TemplateVarsUnreplacedRule = class TemplateVarsUnreplacedRule extends Diagno
|
|
|
423
496
|
TemplateVarsUnreplacedRule = __decorate([Rule({
|
|
424
497
|
key: "template_vars_unreplaced",
|
|
425
498
|
dependsOn: ["config_syntax_check"],
|
|
426
|
-
repairMode: "standard"
|
|
499
|
+
repairMode: "standard",
|
|
500
|
+
usesVars: ["templateVars"]
|
|
427
501
|
})], TemplateVarsUnreplacedRule);
|
|
428
502
|
function collectPlaceholders(value, found) {
|
|
429
503
|
if (typeof value === "string") {
|
|
@@ -466,32 +540,33 @@ function applyVars(str, entries) {
|
|
|
466
540
|
}
|
|
467
541
|
//#endregion
|
|
468
542
|
//#region src/rules/model-provider.ts
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
543
|
+
const DEFAULT_API_KEY = {
|
|
544
|
+
source: "file",
|
|
545
|
+
provider: "miaoda-provider",
|
|
546
|
+
id: "value"
|
|
547
|
+
};
|
|
548
|
+
const DEFAULT_X_API_KEY_HEADER = {
|
|
549
|
+
source: "file",
|
|
550
|
+
provider: "miaoda-secret-provider",
|
|
551
|
+
id: "/models_providers_miaoda_headers_x_api_key"
|
|
552
|
+
};
|
|
553
|
+
const DEFAULT_API = "openai-completions";
|
|
554
|
+
function getExpected(vars) {
|
|
555
|
+
return {
|
|
556
|
+
baseUrl: vars.baseURL + "/api/v1/sgw/model/proxy",
|
|
557
|
+
apiKey: DEFAULT_API_KEY,
|
|
558
|
+
api: DEFAULT_API,
|
|
559
|
+
headers: { "x-api-key": DEFAULT_X_API_KEY_HEADER }
|
|
483
560
|
};
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return vars.baseURL + "/api/v1/sgw/model/proxy";
|
|
487
|
-
}
|
|
561
|
+
}
|
|
562
|
+
let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
|
|
488
563
|
validate(ctx) {
|
|
489
564
|
const provider = getNestedMap(ctx.config, "models", "providers", "miaoda");
|
|
490
565
|
if (!provider) return {
|
|
491
566
|
pass: false,
|
|
492
567
|
message: "models.providers.miaoda not found"
|
|
493
568
|
};
|
|
494
|
-
const expected =
|
|
569
|
+
const expected = getExpected(ctx.vars);
|
|
495
570
|
if (provider.baseUrl !== expected.baseUrl) return {
|
|
496
571
|
pass: false,
|
|
497
572
|
message: "baseUrl mismatch: got " + provider.baseUrl + ", expected " + expected.baseUrl
|
|
@@ -539,7 +614,7 @@ let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
|
|
|
539
614
|
return { pass: true };
|
|
540
615
|
}
|
|
541
616
|
repair(ctx) {
|
|
542
|
-
const expected =
|
|
617
|
+
const expected = getExpected(ctx.vars);
|
|
543
618
|
setNestedValue(ctx.config, [
|
|
544
619
|
"models",
|
|
545
620
|
"providers",
|
|
@@ -565,19 +640,12 @@ let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
|
|
|
565
640
|
"headers"
|
|
566
641
|
], expected.headers);
|
|
567
642
|
}
|
|
568
|
-
getExpected(vars) {
|
|
569
|
-
return {
|
|
570
|
-
baseUrl: _ModelProviderRule.getExpectedBaseUrl(vars),
|
|
571
|
-
apiKey: _ModelProviderRule.DEFAULT_API_KEY,
|
|
572
|
-
api: _ModelProviderRule.DEFAULT_API,
|
|
573
|
-
headers: { "x-api-key": _ModelProviderRule.DEFAULT_X_API_KEY_HEADER }
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
643
|
};
|
|
577
|
-
ModelProviderRule =
|
|
644
|
+
ModelProviderRule = __decorate([Rule({
|
|
578
645
|
key: "model_provider",
|
|
579
646
|
dependsOn: ["config_syntax_check"],
|
|
580
647
|
repairMode: "standard",
|
|
648
|
+
usesVars: ["innerAPIKey", "baseURL"],
|
|
581
649
|
skipWhen: ({ hasMiaoda }) => !hasMiaoda
|
|
582
650
|
})], ModelProviderRule);
|
|
583
651
|
//#endregion
|
|
@@ -707,7 +775,8 @@ let FeishuChannelRule = class FeishuChannelRule extends DiagnoseRule {
|
|
|
707
775
|
FeishuChannelRule = __decorate([Rule({
|
|
708
776
|
key: "feishu_channel",
|
|
709
777
|
dependsOn: ["config_syntax_check", "feishu_default_account"],
|
|
710
|
-
repairMode: "standard"
|
|
778
|
+
repairMode: "standard",
|
|
779
|
+
usesVars: ["feishuAppID", "feishuAppSecret"]
|
|
711
780
|
})], FeishuChannelRule);
|
|
712
781
|
//#endregion
|
|
713
782
|
//#region src/rules/feishu-default-account.ts
|
|
@@ -848,7 +917,8 @@ let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRu
|
|
|
848
917
|
FeishuDefaultAccountRule = __decorate([Rule({
|
|
849
918
|
key: "feishu_default_account",
|
|
850
919
|
dependsOn: ["config_syntax_check"],
|
|
851
|
-
repairMode: "standard"
|
|
920
|
+
repairMode: "standard",
|
|
921
|
+
usesVars: ["feishuAppID", "teamChatID"]
|
|
852
922
|
})], FeishuDefaultAccountRule);
|
|
853
923
|
function nonEmpty(v) {
|
|
854
924
|
return typeof v === "string" && v !== "" ? v : void 0;
|
|
@@ -879,23 +949,19 @@ function secretMatchesCanonical(secret) {
|
|
|
879
949
|
}
|
|
880
950
|
//#endregion
|
|
881
951
|
//#region src/rules/gateway.ts
|
|
882
|
-
|
|
952
|
+
const DEFAULT_PORT = 18789;
|
|
953
|
+
const DEFAULT_MODE = "local";
|
|
954
|
+
const DEFAULT_BIND = "loopback";
|
|
955
|
+
const DEFAULT_AUTH_MODE = "token";
|
|
956
|
+
const DEFAULT_AUTH_TOKEN = {
|
|
957
|
+
source: "file",
|
|
958
|
+
provider: "miaoda-secret-provider",
|
|
959
|
+
id: "/gateway_auth_token"
|
|
960
|
+
};
|
|
961
|
+
/** Required entries in gateway.trustedProxies. Repair appends any missing
|
|
962
|
+
* entries while preserving caller-added extras (no overwrite). */
|
|
963
|
+
const DEFAULT_TRUSTED_PROXIES = ["::1", "127.0.0.1"];
|
|
883
964
|
let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
884
|
-
static {
|
|
885
|
-
_GatewayRule = this;
|
|
886
|
-
}
|
|
887
|
-
static DEFAULT_PORT = 18789;
|
|
888
|
-
static DEFAULT_MODE = "local";
|
|
889
|
-
static DEFAULT_BIND = "loopback";
|
|
890
|
-
static DEFAULT_AUTH_MODE = "token";
|
|
891
|
-
static DEFAULT_AUTH_TOKEN = {
|
|
892
|
-
source: "file",
|
|
893
|
-
provider: "miaoda-secret-provider",
|
|
894
|
-
id: "/gateway_auth_token"
|
|
895
|
-
};
|
|
896
|
-
/** Required entries in gateway.trustedProxies. Repair appends any missing
|
|
897
|
-
* entries while preserving caller-added extras (no overwrite). */
|
|
898
|
-
static DEFAULT_TRUSTED_PROXIES = ["::1", "127.0.0.1"];
|
|
899
965
|
validate(ctx) {
|
|
900
966
|
const gateway = ctx.config.gateway;
|
|
901
967
|
if (!gateway || typeof gateway !== "object") return {
|
|
@@ -903,17 +969,17 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
903
969
|
message: "gateway not found"
|
|
904
970
|
};
|
|
905
971
|
const gw = gateway;
|
|
906
|
-
if (gw.port !==
|
|
972
|
+
if (gw.port !== DEFAULT_PORT) return {
|
|
907
973
|
pass: false,
|
|
908
|
-
message: "gateway.port mismatch: got " + gw.port + ", expected "
|
|
974
|
+
message: "gateway.port mismatch: got " + gw.port + ", expected 18789"
|
|
909
975
|
};
|
|
910
|
-
if (gw.mode !==
|
|
976
|
+
if (gw.mode !== DEFAULT_MODE) return {
|
|
911
977
|
pass: false,
|
|
912
|
-
message: "gateway.mode mismatch: got " + gw.mode + ", expected "
|
|
978
|
+
message: "gateway.mode mismatch: got " + gw.mode + ", expected local"
|
|
913
979
|
};
|
|
914
|
-
if (gw.bind !==
|
|
980
|
+
if (gw.bind !== DEFAULT_BIND) return {
|
|
915
981
|
pass: false,
|
|
916
|
-
message: "gateway.bind mismatch: got " + gw.bind + ", expected "
|
|
982
|
+
message: "gateway.bind mismatch: got " + gw.bind + ", expected loopback"
|
|
917
983
|
};
|
|
918
984
|
const auth = gw.auth;
|
|
919
985
|
if (!auth || typeof auth !== "object") return {
|
|
@@ -921,9 +987,9 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
921
987
|
message: "gateway.auth not found"
|
|
922
988
|
};
|
|
923
989
|
const authObj = auth;
|
|
924
|
-
if (authObj.mode !==
|
|
990
|
+
if (authObj.mode !== DEFAULT_AUTH_MODE) return {
|
|
925
991
|
pass: false,
|
|
926
|
-
message: "gateway.auth.mode mismatch: got " + authObj.mode + ", expected "
|
|
992
|
+
message: "gateway.auth.mode mismatch: got " + authObj.mode + ", expected token"
|
|
927
993
|
};
|
|
928
994
|
const token = authObj.token;
|
|
929
995
|
if (typeof token === "string") {
|
|
@@ -932,7 +998,7 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
932
998
|
message: "gateway.auth.token string value mismatch"
|
|
933
999
|
};
|
|
934
1000
|
} else if (typeof token === "object" && token !== null && !Array.isArray(token)) {
|
|
935
|
-
if (!matchMap(token,
|
|
1001
|
+
if (!matchMap(token, DEFAULT_AUTH_TOKEN)) return {
|
|
936
1002
|
pass: false,
|
|
937
1003
|
message: "gateway.auth.token object mismatch: got " + JSON.stringify(token)
|
|
938
1004
|
};
|
|
@@ -954,7 +1020,7 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
954
1020
|
pass: false,
|
|
955
1021
|
message: "gateway.trustedProxies missing or not an array"
|
|
956
1022
|
};
|
|
957
|
-
const missing =
|
|
1023
|
+
const missing = DEFAULT_TRUSTED_PROXIES.filter((p) => !proxies.includes(p));
|
|
958
1024
|
if (missing.length > 0) return {
|
|
959
1025
|
pass: false,
|
|
960
1026
|
message: "gateway.trustedProxies missing: " + JSON.stringify(missing)
|
|
@@ -962,19 +1028,19 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
962
1028
|
return { pass: true };
|
|
963
1029
|
}
|
|
964
1030
|
repair(ctx) {
|
|
965
|
-
setNestedValue(ctx.config, ["gateway", "port"],
|
|
966
|
-
setNestedValue(ctx.config, ["gateway", "mode"],
|
|
967
|
-
setNestedValue(ctx.config, ["gateway", "bind"],
|
|
1031
|
+
setNestedValue(ctx.config, ["gateway", "port"], DEFAULT_PORT);
|
|
1032
|
+
setNestedValue(ctx.config, ["gateway", "mode"], DEFAULT_MODE);
|
|
1033
|
+
setNestedValue(ctx.config, ["gateway", "bind"], DEFAULT_BIND);
|
|
968
1034
|
setNestedValue(ctx.config, [
|
|
969
1035
|
"gateway",
|
|
970
1036
|
"auth",
|
|
971
1037
|
"mode"
|
|
972
|
-
],
|
|
1038
|
+
], DEFAULT_AUTH_MODE);
|
|
973
1039
|
setNestedValue(ctx.config, [
|
|
974
1040
|
"gateway",
|
|
975
1041
|
"auth",
|
|
976
1042
|
"token"
|
|
977
|
-
],
|
|
1043
|
+
], DEFAULT_AUTH_TOKEN);
|
|
978
1044
|
setNestedValue(ctx.config, [
|
|
979
1045
|
"gateway",
|
|
980
1046
|
"controlUi",
|
|
@@ -983,17 +1049,18 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
983
1049
|
const gw = ctx.config.gateway ?? {};
|
|
984
1050
|
const current = Array.isArray(gw.trustedProxies) ? gw.trustedProxies.slice() : [];
|
|
985
1051
|
const seen = new Set(current.map((v) => String(v)));
|
|
986
|
-
for (const p of
|
|
1052
|
+
for (const p of DEFAULT_TRUSTED_PROXIES) if (!seen.has(p)) {
|
|
987
1053
|
current.push(p);
|
|
988
1054
|
seen.add(p);
|
|
989
1055
|
}
|
|
990
1056
|
setNestedValue(ctx.config, ["gateway", "trustedProxies"], current);
|
|
991
1057
|
}
|
|
992
1058
|
};
|
|
993
|
-
GatewayRule =
|
|
1059
|
+
GatewayRule = __decorate([Rule({
|
|
994
1060
|
key: "gateway",
|
|
995
1061
|
dependsOn: ["config_syntax_check"],
|
|
996
|
-
repairMode: "standard"
|
|
1062
|
+
repairMode: "standard",
|
|
1063
|
+
usesVars: ["gatewayToken"]
|
|
997
1064
|
})], GatewayRule);
|
|
998
1065
|
//#endregion
|
|
999
1066
|
//#region src/rules/allowed-origins.ts
|
|
@@ -1037,7 +1104,8 @@ let AllowedOriginsRule = class AllowedOriginsRule extends DiagnoseRule {
|
|
|
1037
1104
|
AllowedOriginsRule = __decorate([Rule({
|
|
1038
1105
|
key: "allowed_origins",
|
|
1039
1106
|
dependsOn: ["config_syntax_check"],
|
|
1040
|
-
repairMode: "standard"
|
|
1107
|
+
repairMode: "standard",
|
|
1108
|
+
usesVars: ["expectedOrigins"]
|
|
1041
1109
|
})], AllowedOriginsRule);
|
|
1042
1110
|
function getExpectedOrigins(vars) {
|
|
1043
1111
|
return Array.isArray(vars.expectedOrigins) ? vars.expectedOrigins : [];
|
|
@@ -1116,11 +1184,12 @@ JwtTokenRule = __decorate([Rule({
|
|
|
1116
1184
|
key: "jwt_token",
|
|
1117
1185
|
dependsOn: ["config_syntax_check"],
|
|
1118
1186
|
repairMode: "standard",
|
|
1187
|
+
usesVars: ["providerFilePath"],
|
|
1119
1188
|
skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaProvider
|
|
1120
1189
|
})], JwtTokenRule);
|
|
1121
1190
|
//#endregion
|
|
1122
1191
|
//#region src/rules/secrets-file.ts
|
|
1123
|
-
let
|
|
1192
|
+
let SecretsFileRule = class SecretsFileRule extends DiagnoseRule {
|
|
1124
1193
|
validate(ctx) {
|
|
1125
1194
|
const filePath = ctx.vars.secretsFilePath;
|
|
1126
1195
|
if (!filePath) return {
|
|
@@ -1167,12 +1236,18 @@ let SecretsRule = class SecretsRule extends DiagnoseRule {
|
|
|
1167
1236
|
};
|
|
1168
1237
|
}
|
|
1169
1238
|
};
|
|
1170
|
-
|
|
1239
|
+
SecretsFileRule = __decorate([Rule({
|
|
1171
1240
|
key: "secrets_file",
|
|
1172
1241
|
dependsOn: ["config_syntax_check"],
|
|
1173
1242
|
repairMode: "standard",
|
|
1243
|
+
usesVars: [
|
|
1244
|
+
"secretsFilePath",
|
|
1245
|
+
"feishuAppSecret",
|
|
1246
|
+
"gatewayToken",
|
|
1247
|
+
"innerAPIKey"
|
|
1248
|
+
],
|
|
1174
1249
|
skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaSecretProvider
|
|
1175
|
-
})],
|
|
1250
|
+
})], SecretsFileRule);
|
|
1176
1251
|
//#endregion
|
|
1177
1252
|
//#region src/rules/cleanup-install-backup-dirs.ts
|
|
1178
1253
|
const DIR_PREFIX = ".openclaw-install-";
|
|
@@ -1293,9 +1368,6 @@ function getPluginMaps(config) {
|
|
|
1293
1368
|
allow: Array.isArray(rawAllow) ? rawAllow : void 0
|
|
1294
1369
|
};
|
|
1295
1370
|
}
|
|
1296
|
-
function getExtensionsDir(configPath) {
|
|
1297
|
-
return node_path.default.join(node_path.default.dirname(configPath), "extensions");
|
|
1298
|
-
}
|
|
1299
1371
|
function hasNewMiaoda({ entries, installs, allow }) {
|
|
1300
1372
|
return asRecord(entries?.[NEW_MIAODA]) != null || asRecord(installs?.[NEW_MIAODA]) != null || (allow?.includes(NEW_MIAODA) ?? false);
|
|
1301
1373
|
}
|
|
@@ -1385,293 +1457,139 @@ function getAllow(config) {
|
|
|
1385
1457
|
return allow.filter((e) => typeof e === "string");
|
|
1386
1458
|
}
|
|
1387
1459
|
//#endregion
|
|
1388
|
-
//#region src/
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
const meta = rule.meta;
|
|
1411
|
-
if (disabledSet.has(meta.key)) continue;
|
|
1412
|
-
if (meta.dependsOn?.some((dep) => failedKeys.has(dep))) continue;
|
|
1413
|
-
if (meta.dependsOn?.includes("config_syntax_check") && !configParsed) try {
|
|
1414
|
-
const parsed = loadJSON5().parse(readFile(input.configPath));
|
|
1415
|
-
const deps = analyzeProviderDeps(parsed);
|
|
1416
|
-
ctx = {
|
|
1417
|
-
config: parsed,
|
|
1418
|
-
configPath: input.configPath,
|
|
1419
|
-
vars: input.vars,
|
|
1420
|
-
providerDeps: deps,
|
|
1421
|
-
templateVars: input.templateVars
|
|
1422
|
-
};
|
|
1423
|
-
configParsed = true;
|
|
1424
|
-
} catch {
|
|
1425
|
-
break;
|
|
1426
|
-
}
|
|
1427
|
-
if (meta.skipWhen && configParsed) {
|
|
1428
|
-
const hasMiaoda = shouldCheckMiaodaRules(ctx.config);
|
|
1429
|
-
if (meta.skipWhen({
|
|
1430
|
-
hasMiaoda,
|
|
1431
|
-
deps: ctx.providerDeps
|
|
1432
|
-
})) continue;
|
|
1433
|
-
}
|
|
1434
|
-
const r = rule.validate(ctx);
|
|
1435
|
-
if (!r.pass) {
|
|
1436
|
-
failedKeys.add(meta.key);
|
|
1437
|
-
pushFailedRule(result, meta.repairMode, meta.key, r.message || "");
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
return result;
|
|
1441
|
-
}
|
|
1442
|
-
function pushFailedRule(result, repairMode, key, detail) {
|
|
1443
|
-
switch (repairMode) {
|
|
1444
|
-
case "standard":
|
|
1445
|
-
result.failedRules.standard.push(key);
|
|
1446
|
-
break;
|
|
1447
|
-
case "ai":
|
|
1448
|
-
result.failedRules.ai.push({
|
|
1449
|
-
rule_name: key,
|
|
1450
|
-
detail
|
|
1451
|
-
});
|
|
1452
|
-
break;
|
|
1453
|
-
case "reset":
|
|
1454
|
-
result.failedRules.reset.push({
|
|
1455
|
-
rule_name: key,
|
|
1456
|
-
detail
|
|
1457
|
-
});
|
|
1458
|
-
break;
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
//#endregion
|
|
1462
|
-
//#region src/repair.ts
|
|
1463
|
-
function runRepair(input) {
|
|
1460
|
+
//#region src/fs-utils.ts
|
|
1461
|
+
/**
|
|
1462
|
+
* Rename src → dst, falling back to `mv` (which handles cross-device copy)
|
|
1463
|
+
* when the kernel returns EXDEV.
|
|
1464
|
+
*
|
|
1465
|
+
* Sandbox filesystems can put sibling paths on different "devices" from
|
|
1466
|
+
* rename(2)'s point of view: bind mounts, overlayfs copy-up, and
|
|
1467
|
+
* mount-point children inside a single directory all trip EXDEV. Seen in
|
|
1468
|
+
* production when reset's atomic swap did
|
|
1469
|
+
* /home/gem/.npm-global/lib/node_modules/openclaw → openclaw.bak
|
|
1470
|
+
* and the openclaw subdir was a bind-mounted volume.
|
|
1471
|
+
*
|
|
1472
|
+
* Behavior:
|
|
1473
|
+
* - Happy path hits rename(2) — atomic, single syscall, microseconds.
|
|
1474
|
+
* - EXDEV path shells out to `mv`, which does rename() then copy+unlink
|
|
1475
|
+
* on failure. Non-atomic but correct; callers already have rollback
|
|
1476
|
+
* logic (install-openclaw restores from .bak) so loss of atomicity
|
|
1477
|
+
* only matters if the process dies mid-copy, and that's survivable.
|
|
1478
|
+
* - Any other error (ENOENT, EACCES, EBUSY...) rethrows as-is so callers
|
|
1479
|
+
* see the real problem instead of a misleading `mv` fallback failure.
|
|
1480
|
+
*/
|
|
1481
|
+
function moveSafe(src, dst) {
|
|
1464
1482
|
try {
|
|
1465
|
-
|
|
1466
|
-
const repairData = input.repairData || {};
|
|
1467
|
-
const rules = getAllRules();
|
|
1468
|
-
for (const rule of rules) {
|
|
1469
|
-
if (!failedSet.has(rule.meta.key)) continue;
|
|
1470
|
-
if (rule.meta.repairMode !== "standard") continue;
|
|
1471
|
-
if (rule.meta.dependsOn?.includes("config_syntax_check")) continue;
|
|
1472
|
-
rule.repair({
|
|
1473
|
-
config: {},
|
|
1474
|
-
configPath: input.configPath,
|
|
1475
|
-
vars: input.vars,
|
|
1476
|
-
providerDeps: {
|
|
1477
|
-
usesMiaodaProvider: false,
|
|
1478
|
-
usesMiaodaSecretProvider: false
|
|
1479
|
-
},
|
|
1480
|
-
templateVars: input.templateVars
|
|
1481
|
-
});
|
|
1482
|
-
}
|
|
1483
|
-
const JSON5 = loadJSON5();
|
|
1484
|
-
let config;
|
|
1485
|
-
try {
|
|
1486
|
-
config = JSON5.parse(readFile(input.configPath));
|
|
1487
|
-
} catch (e) {
|
|
1488
|
-
return {
|
|
1489
|
-
success: false,
|
|
1490
|
-
error: "cannot parse config for repair: " + e.message
|
|
1491
|
-
};
|
|
1492
|
-
}
|
|
1493
|
-
const deps = analyzeProviderDeps(config);
|
|
1494
|
-
const ctx = {
|
|
1495
|
-
config,
|
|
1496
|
-
configPath: input.configPath,
|
|
1497
|
-
vars: input.vars,
|
|
1498
|
-
providerDeps: deps,
|
|
1499
|
-
templateVars: input.templateVars
|
|
1500
|
-
};
|
|
1501
|
-
let configDirty = false;
|
|
1502
|
-
for (const rule of rules) {
|
|
1503
|
-
if (!failedSet.has(rule.meta.key)) continue;
|
|
1504
|
-
if (rule.meta.repairMode !== "standard") continue;
|
|
1505
|
-
if (!rule.meta.dependsOn?.includes("config_syntax_check")) continue;
|
|
1506
|
-
rule.repair(ctx);
|
|
1507
|
-
configDirty = true;
|
|
1508
|
-
}
|
|
1509
|
-
if (configDirty) writeFile(input.configPath, JSON.stringify(config, null, 2));
|
|
1510
|
-
if (repairData.secretsContent && input.vars.secretsFilePath) writeFile(input.vars.secretsFilePath, repairData.secretsContent);
|
|
1511
|
-
if (repairData.providerKeyContent && input.vars.providerFilePath) writeFile(input.vars.providerFilePath, repairData.providerKeyContent);
|
|
1512
|
-
if (repairData.restartCommand) try {
|
|
1513
|
-
shell(repairData.restartCommand, 3e4);
|
|
1514
|
-
} catch (e) {
|
|
1515
|
-
return {
|
|
1516
|
-
success: false,
|
|
1517
|
-
error: "restart command failed: " + e.message
|
|
1518
|
-
};
|
|
1519
|
-
}
|
|
1520
|
-
return { success: true };
|
|
1483
|
+
node_fs.default.renameSync(src, dst);
|
|
1521
1484
|
} catch (e) {
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
error: "repair failed: " + e.message
|
|
1525
|
-
};
|
|
1485
|
+
if (e?.code !== "EXDEV") throw e;
|
|
1486
|
+
execCaptureErr(`mv ${shellQuote(src)} ${shellQuote(dst)}`);
|
|
1526
1487
|
}
|
|
1527
1488
|
}
|
|
1528
|
-
//#endregion
|
|
1529
|
-
//#region src/paths.ts
|
|
1530
1489
|
/**
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
1533
|
-
* (
|
|
1534
|
-
*
|
|
1535
|
-
*
|
|
1490
|
+
* Run a shell command, re-throwing with stderr attached on failure.
|
|
1491
|
+
*
|
|
1492
|
+
* Node's `execSync(..., { stdio: 'ignore' })` swallows stderr entirely —
|
|
1493
|
+
* callers only see "Command failed: <cmd>" with no hint of the real error
|
|
1494
|
+
* (ENOSPC, EROFS, "unrecognized option", etc.). Production debugging on
|
|
1495
|
+
* sandboxed boxes is painful without the underlying message, so we pipe
|
|
1496
|
+
* stderr, capture it, and embed it in the thrown Error. stdout stays
|
|
1497
|
+
* suppressed because the commands we run here (tar/mv) are silent on
|
|
1498
|
+
* success.
|
|
1536
1499
|
*/
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1500
|
+
function execCaptureErr(cmd) {
|
|
1501
|
+
try {
|
|
1502
|
+
(0, node_child_process.execSync)(cmd, { stdio: [
|
|
1503
|
+
"ignore",
|
|
1504
|
+
"ignore",
|
|
1505
|
+
"pipe"
|
|
1506
|
+
] });
|
|
1507
|
+
} catch (e) {
|
|
1508
|
+
const stderr = e?.stderr;
|
|
1509
|
+
const stderrStr = (typeof stderr === "string" ? stderr : stderr?.toString("utf8") ?? "").trim();
|
|
1510
|
+
const base = e?.message ?? "command failed";
|
|
1511
|
+
throw new Error(stderrStr ? `${base}\nstderr: ${stderrStr}` : base);
|
|
1512
|
+
}
|
|
1540
1513
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1514
|
+
/** POSIX single-quote shell escape. Paths with embedded quotes are rare but
|
|
1515
|
+
* the token-file path conventions in sandboxes don't guarantee cleanliness. */
|
|
1516
|
+
function shellQuote(s) {
|
|
1517
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1543
1518
|
}
|
|
1544
|
-
/** Sandbox workspace root where openclaw config + agent state lives. */
|
|
1545
|
-
const WORKSPACE_DIR = "/home/gem/workspace/agent";
|
|
1546
|
-
/** File containing the provider key used by the openclaw miaoda provider. */
|
|
1547
|
-
const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
|
|
1548
|
-
/** File containing the miaoda openclaw secrets JSON. */
|
|
1549
|
-
const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
|
|
1550
|
-
/** Absolute path to the openclaw config JSON. */
|
|
1551
|
-
const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
|
|
1552
|
-
//#endregion
|
|
1553
|
-
//#region src/logger.ts
|
|
1554
1519
|
/**
|
|
1555
|
-
*
|
|
1556
|
-
* `console.error` (rules, helpers, errors) or through the per-task
|
|
1557
|
-
* `makeLogger` (reset worker) — is tee'd here so operators have a single
|
|
1558
|
-
* file to tail when diagnosing a sandbox.
|
|
1520
|
+
* Recursively remove a path, retrying on transient overlayfs races.
|
|
1559
1521
|
*
|
|
1560
|
-
*
|
|
1561
|
-
* (
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
*
|
|
1566
|
-
|
|
1567
|
-
try {
|
|
1568
|
-
const dir = node_path.default.dirname(CLI_LOG_FILE);
|
|
1569
|
-
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
1570
|
-
node_fs.default.appendFileSync(CLI_LOG_FILE, line);
|
|
1571
|
-
} catch {}
|
|
1572
|
-
}
|
|
1573
|
-
let stderrMirrorInstalled = false;
|
|
1574
|
-
/**
|
|
1575
|
-
* Install a process-wide `console.error` interceptor that mirrors each
|
|
1576
|
-
* line to BOTH the original stderr AND cli.log. Call once at CLI entry
|
|
1577
|
-
* before any subcommand dispatch; idempotent.
|
|
1522
|
+
* `fs.rmSync(..., { recursive: true, force: true })` walks the tree
|
|
1523
|
+
* issuing rmdir() per directory. On overlayfs (sandbox containers) a
|
|
1524
|
+
* just-emptied subdir can return EAGAIN/EBUSY/ENOTEMPTY for a short
|
|
1525
|
+
* window before whiteout propagation finishes — even though every
|
|
1526
|
+
* child has been unlinked. Same family of races as the tar/rename
|
|
1527
|
+
* issue handled by extractTarballTolerant + the install-openclaw
|
|
1528
|
+
* tmpfs swap.
|
|
1578
1529
|
*
|
|
1579
|
-
*
|
|
1580
|
-
*
|
|
1581
|
-
*
|
|
1582
|
-
* helpers, and error paths therefore must route debug output through
|
|
1583
|
-
* console.error (stderr).
|
|
1530
|
+
* Retries with linear backoff (50 → 250 → 750 ms). Any non-transient
|
|
1531
|
+
* error (EACCES, EROFS, ENOSPC...) bubbles immediately. Returns true
|
|
1532
|
+
* if the path is absent at the end (success or never existed).
|
|
1584
1533
|
*/
|
|
1585
|
-
function
|
|
1586
|
-
if (
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1534
|
+
function rmrfTolerant(target) {
|
|
1535
|
+
if (!node_fs.default.existsSync(target)) return true;
|
|
1536
|
+
const transient = new Set([
|
|
1537
|
+
"EAGAIN",
|
|
1538
|
+
"EBUSY",
|
|
1539
|
+
"ENOTEMPTY"
|
|
1540
|
+
]);
|
|
1541
|
+
const delaysMs = [
|
|
1542
|
+
50,
|
|
1543
|
+
250,
|
|
1544
|
+
750
|
|
1545
|
+
];
|
|
1546
|
+
for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
|
|
1547
|
+
node_fs.default.rmSync(target, {
|
|
1548
|
+
recursive: true,
|
|
1549
|
+
force: true
|
|
1550
|
+
});
|
|
1551
|
+
return true;
|
|
1552
|
+
} catch (e) {
|
|
1553
|
+
const code = e.code ?? "";
|
|
1554
|
+
if (!transient.has(code) || attempt === delaysMs.length) throw e;
|
|
1555
|
+
(0, node_child_process.execSync)(`sleep ${delaysMs[attempt] / 1e3}`);
|
|
1600
1556
|
}
|
|
1557
|
+
return !node_fs.default.existsSync(target);
|
|
1601
1558
|
}
|
|
1602
|
-
function makeLogger(logFile) {
|
|
1603
|
-
try {
|
|
1604
|
-
const dir = node_path.default.dirname(logFile);
|
|
1605
|
-
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
1606
|
-
} catch {}
|
|
1607
|
-
return (msg) => {
|
|
1608
|
-
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}\n`;
|
|
1609
|
-
try {
|
|
1610
|
-
node_fs.default.appendFileSync(logFile, line);
|
|
1611
|
-
} catch {}
|
|
1612
|
-
appendCliLog(line);
|
|
1613
|
-
};
|
|
1614
|
-
}
|
|
1615
|
-
//#endregion
|
|
1616
|
-
//#region src/fs-utils.ts
|
|
1617
1559
|
/**
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1560
|
+
* Snapshot the current `openclaw.json` to `<configPath>.<YYYYMMDD_HHMMSS>.bak`
|
|
1561
|
+
* before a repair / fix flow rewrites it. Local-time stamp matches the
|
|
1562
|
+
* shell-style backup convention (`cp openclaw.json openclaw.json.$(date
|
|
1563
|
+
* +%Y%m%d_%H%M%S).bak`) so operators triaging on the sandbox see the
|
|
1564
|
+
* familiar filename pattern.
|
|
1620
1565
|
*
|
|
1621
|
-
*
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1624
|
-
* production when reset's atomic swap did
|
|
1625
|
-
* /home/gem/.npm-global/lib/node_modules/openclaw → openclaw.bak
|
|
1626
|
-
* and the openclaw subdir was a bind-mounted volume.
|
|
1566
|
+
* Returns the absolute backup path on success, or `null` when:
|
|
1567
|
+
* - source doesn't exist (first-time setup; nothing to back up)
|
|
1568
|
+
* - copy fails for any reason (disk full, permission denied, etc.)
|
|
1627
1569
|
*
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
1632
|
-
* logic (install-openclaw restores from .bak) so loss of atomicity
|
|
1633
|
-
* only matters if the process dies mid-copy, and that's survivable.
|
|
1634
|
-
* - Any other error (ENOENT, EACCES, EBUSY...) rethrows as-is so callers
|
|
1635
|
-
* see the real problem instead of a misleading `mv` fallback failure.
|
|
1570
|
+
* Failure is logged to cli.log via console.error and swallowed — the
|
|
1571
|
+
* caller's repair/fix work goes ahead either way. The backup is a safety
|
|
1572
|
+
* net, not a correctness gate; better to do the repair than refuse over
|
|
1573
|
+
* a missing rollback file.
|
|
1636
1574
|
*/
|
|
1637
|
-
function
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
if (e?.code !== "EXDEV") throw e;
|
|
1642
|
-
execCaptureErr(`mv ${shellQuote(src)} ${shellQuote(dst)}`);
|
|
1575
|
+
function backupConfigSync(configPath) {
|
|
1576
|
+
if (!node_fs.default.existsSync(configPath)) {
|
|
1577
|
+
console.error(`backupConfigSync: skip — source missing path=${configPath}`);
|
|
1578
|
+
return null;
|
|
1643
1579
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
*
|
|
1648
|
-
* Node's `execSync(..., { stdio: 'ignore' })` swallows stderr entirely —
|
|
1649
|
-
* callers only see "Command failed: <cmd>" with no hint of the real error
|
|
1650
|
-
* (ENOSPC, EROFS, "unrecognized option", etc.). Production debugging on
|
|
1651
|
-
* sandboxed boxes is painful without the underlying message, so we pipe
|
|
1652
|
-
* stderr, capture it, and embed it in the thrown Error. stdout stays
|
|
1653
|
-
* suppressed because the commands we run here (tar/mv) are silent on
|
|
1654
|
-
* success.
|
|
1655
|
-
*/
|
|
1656
|
-
function execCaptureErr(cmd) {
|
|
1580
|
+
const d = /* @__PURE__ */ new Date();
|
|
1581
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1582
|
+
const backupPath = `${configPath}.${`${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`}.bak`;
|
|
1657
1583
|
try {
|
|
1658
|
-
(
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
] });
|
|
1584
|
+
node_fs.default.copyFileSync(configPath, backupPath);
|
|
1585
|
+
const bytes = node_fs.default.statSync(backupPath).size;
|
|
1586
|
+
console.error(`backupConfigSync: ok src=${configPath} dst=${backupPath} bytes=${bytes}`);
|
|
1587
|
+
return backupPath;
|
|
1663
1588
|
} catch (e) {
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
const base = e?.message ?? "command failed";
|
|
1667
|
-
throw new Error(stderrStr ? `${base}\nstderr: ${stderrStr}` : base);
|
|
1589
|
+
console.error(`backupConfigSync: failed src=${configPath} message=${e.message}`);
|
|
1590
|
+
return null;
|
|
1668
1591
|
}
|
|
1669
1592
|
}
|
|
1670
|
-
/** POSIX single-quote shell escape. Paths with embedded quotes are rare but
|
|
1671
|
-
* the token-file path conventions in sandboxes don't guarantee cleanliness. */
|
|
1672
|
-
function shellQuote(s) {
|
|
1673
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1674
|
-
}
|
|
1675
1593
|
/**
|
|
1676
1594
|
* Extract an npm-packed gzipped tarball.
|
|
1677
1595
|
*
|
|
@@ -1746,6 +1664,971 @@ function extractTarballTolerant(tarball, destDir, opts = {}) {
|
|
|
1746
1664
|
}
|
|
1747
1665
|
}
|
|
1748
1666
|
//#endregion
|
|
1667
|
+
//#region src/rules/feishu-plugin-state-normalize.ts
|
|
1668
|
+
const PLUGIN_NAME$1 = "openclaw-lark";
|
|
1669
|
+
const BUILTIN_FEISHU = "feishu";
|
|
1670
|
+
const LEGACY_PLUGIN_NAME = "feishu-openclaw-plugin";
|
|
1671
|
+
const LEGACY_DIRS_TO_REMOVE = [LEGACY_PLUGIN_NAME, BUILTIN_FEISHU];
|
|
1672
|
+
const FEISHU_TOOLS = Object.freeze([
|
|
1673
|
+
"feishu_bitable_app",
|
|
1674
|
+
"feishu_bitable_app_table",
|
|
1675
|
+
"feishu_bitable_app_table_field",
|
|
1676
|
+
"feishu_bitable_app_table_record",
|
|
1677
|
+
"feishu_bitable_app_table_view",
|
|
1678
|
+
"feishu_calendar_calendar",
|
|
1679
|
+
"feishu_calendar_event",
|
|
1680
|
+
"feishu_calendar_event_attendee",
|
|
1681
|
+
"feishu_calendar_freebusy",
|
|
1682
|
+
"feishu_chat",
|
|
1683
|
+
"feishu_chat_members",
|
|
1684
|
+
"feishu_create_doc",
|
|
1685
|
+
"feishu_doc_comments",
|
|
1686
|
+
"feishu_doc_media",
|
|
1687
|
+
"feishu_drive_file",
|
|
1688
|
+
"feishu_fetch_doc",
|
|
1689
|
+
"feishu_get_user",
|
|
1690
|
+
"feishu_im_bot_image",
|
|
1691
|
+
"feishu_im_user_fetch_resource",
|
|
1692
|
+
"feishu_im_user_get_messages",
|
|
1693
|
+
"feishu_im_user_get_thread_messages",
|
|
1694
|
+
"feishu_im_user_message",
|
|
1695
|
+
"feishu_im_user_search_messages",
|
|
1696
|
+
"feishu_oauth",
|
|
1697
|
+
"feishu_oauth_batch_auth",
|
|
1698
|
+
"feishu_search_doc_wiki",
|
|
1699
|
+
"feishu_search_user",
|
|
1700
|
+
"feishu_sheet",
|
|
1701
|
+
"feishu_task_comment",
|
|
1702
|
+
"feishu_task_subtask",
|
|
1703
|
+
"feishu_task_task",
|
|
1704
|
+
"feishu_task_tasklist",
|
|
1705
|
+
"feishu_update_doc",
|
|
1706
|
+
"feishu_wiki_space",
|
|
1707
|
+
"feishu_wiki_space_node"
|
|
1708
|
+
]);
|
|
1709
|
+
/**
|
|
1710
|
+
* 飞书插件状态规范化:fs 上 <extDir>/openclaw-lark/ 已落盘后统一收尾环境。
|
|
1711
|
+
* 各 fail 与 repair 一一对应,详见 fails.push(...) 字符串与 repair() 调用。
|
|
1712
|
+
*/
|
|
1713
|
+
let FeishuPluginStateNormalizeRule = class FeishuPluginStateNormalizeRule extends DiagnoseRule {
|
|
1714
|
+
validate(ctx) {
|
|
1715
|
+
if (!isPluginInstalled(ctx)) return { pass: true };
|
|
1716
|
+
const fails = [];
|
|
1717
|
+
if (!isNewPluginEnabled(ctx.config)) fails.push(`plugins.entries["${PLUGIN_NAME$1}"].enabled !== true(应启用)`);
|
|
1718
|
+
if (isBuiltinFeishuEnabled(ctx.config)) fails.push("plugins.entries.feishu.enabled === true(应禁用)");
|
|
1719
|
+
if (isTopLevelMissingFeishuTools(ctx.config)) fails.push("tools.alsoAllow 缺 feishu_* tools");
|
|
1720
|
+
const legacyResiduals = findLegacyResiduals(ctx);
|
|
1721
|
+
if (legacyResiduals.length > 0) fails.push(`legacy 飞书插件残留:${legacyResiduals.join(", ")}`);
|
|
1722
|
+
if (fails.length === 0) return { pass: true };
|
|
1723
|
+
return {
|
|
1724
|
+
pass: false,
|
|
1725
|
+
message: fails.join(";")
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
repair(ctx) {
|
|
1729
|
+
setEntryEnabled(ctx.config, PLUGIN_NAME$1, true);
|
|
1730
|
+
setEntryEnabled(ctx.config, BUILTIN_FEISHU, false);
|
|
1731
|
+
ensureFeishuTools(ctx.config);
|
|
1732
|
+
cleanupLegacyResiduals(ctx);
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
FeishuPluginStateNormalizeRule = __decorate([Rule({
|
|
1736
|
+
key: "feishu_plugin_state_normalize",
|
|
1737
|
+
dependsOn: ["config_syntax_check"],
|
|
1738
|
+
repairMode: "standard"
|
|
1739
|
+
})], FeishuPluginStateNormalizeRule);
|
|
1740
|
+
function isPluginInstalled(ctx) {
|
|
1741
|
+
return node_fs.default.existsSync(node_path.default.join(getExtensionsDir(ctx.configPath), PLUGIN_NAME$1));
|
|
1742
|
+
}
|
|
1743
|
+
function isNewPluginEnabled(config) {
|
|
1744
|
+
return asRecord(getNestedMap(config, "plugins", "entries")?.[PLUGIN_NAME$1])?.enabled === true;
|
|
1745
|
+
}
|
|
1746
|
+
function isBuiltinFeishuEnabled(config) {
|
|
1747
|
+
return asRecord(getNestedMap(config, "plugins", "entries")?.[BUILTIN_FEISHU])?.enabled === true;
|
|
1748
|
+
}
|
|
1749
|
+
/** 仅看顶层 tools.alsoAllow——agent 级 alsoAllow 是用户对单 agent 的精细化授权,doctor 不动。
|
|
1750
|
+
* 含 "*" 或任一 feishu_* 即视为已配。 */
|
|
1751
|
+
function isTopLevelMissingFeishuTools(config) {
|
|
1752
|
+
return !hasFeishuTool(readAlsoAllow(config));
|
|
1753
|
+
}
|
|
1754
|
+
function readAlsoAllow(host) {
|
|
1755
|
+
const tools = asRecord(host)?.tools;
|
|
1756
|
+
const arr = asRecord(tools)?.alsoAllow;
|
|
1757
|
+
if (!Array.isArray(arr)) return [];
|
|
1758
|
+
return arr.filter((e) => typeof e === "string");
|
|
1759
|
+
}
|
|
1760
|
+
function hasFeishuTool(alsoAllow) {
|
|
1761
|
+
if (alsoAllow.includes("*")) return true;
|
|
1762
|
+
return alsoAllow.some((t) => FEISHU_TOOLS.includes(t));
|
|
1763
|
+
}
|
|
1764
|
+
function findLegacyResiduals(ctx) {
|
|
1765
|
+
const found = [];
|
|
1766
|
+
const plugins = asRecord(ctx.config.plugins);
|
|
1767
|
+
if (asRecord(plugins?.entries)?.[LEGACY_PLUGIN_NAME] != null) found.push("entries[legacy]");
|
|
1768
|
+
const allow = plugins?.allow;
|
|
1769
|
+
if (Array.isArray(allow) && allow.includes(LEGACY_PLUGIN_NAME)) found.push("allow[legacy]");
|
|
1770
|
+
if (asRecord(plugins?.installs)?.[LEGACY_PLUGIN_NAME] != null) found.push("installs[legacy]");
|
|
1771
|
+
const extDir = getExtensionsDir(ctx.configPath);
|
|
1772
|
+
for (const name of LEGACY_DIRS_TO_REMOVE) if (node_fs.default.existsSync(node_path.default.join(extDir, name))) found.push(`fs/${name}`);
|
|
1773
|
+
return found;
|
|
1774
|
+
}
|
|
1775
|
+
function setEntryEnabled(config, key, enabled) {
|
|
1776
|
+
const entries = ensureRecord(ensureRecord(config, "plugins"), "entries");
|
|
1777
|
+
entries[key] = {
|
|
1778
|
+
...asRecord(entries[key]) ?? {},
|
|
1779
|
+
enabled
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
function ensureFeishuTools(config) {
|
|
1783
|
+
const alsoAllow = readAlsoAllow(config);
|
|
1784
|
+
if (hasFeishuTool(alsoAllow)) return;
|
|
1785
|
+
ensureRecord(config, "tools").alsoAllow = [...new Set([...alsoAllow, ...FEISHU_TOOLS])];
|
|
1786
|
+
}
|
|
1787
|
+
function cleanupLegacyResiduals(ctx) {
|
|
1788
|
+
const plugins = asRecord(ctx.config.plugins);
|
|
1789
|
+
if (plugins) {
|
|
1790
|
+
const entries = asRecord(plugins.entries);
|
|
1791
|
+
if (entries && LEGACY_PLUGIN_NAME in entries) delete entries[LEGACY_PLUGIN_NAME];
|
|
1792
|
+
const installs = asRecord(plugins.installs);
|
|
1793
|
+
if (installs && LEGACY_PLUGIN_NAME in installs) delete installs[LEGACY_PLUGIN_NAME];
|
|
1794
|
+
const allow = plugins.allow;
|
|
1795
|
+
if (Array.isArray(allow)) {
|
|
1796
|
+
for (let i = allow.length - 1; i >= 0; i--) if (allow[i] === LEGACY_PLUGIN_NAME) allow.splice(i, 1);
|
|
1797
|
+
if (!allow.includes(PLUGIN_NAME$1)) allow.push(PLUGIN_NAME$1);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
const extDir = getExtensionsDir(ctx.configPath);
|
|
1801
|
+
for (const name of LEGACY_DIRS_TO_REMOVE) {
|
|
1802
|
+
const target = node_path.default.join(extDir, name);
|
|
1803
|
+
const rel = node_path.default.relative(extDir, target);
|
|
1804
|
+
if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
|
|
1805
|
+
try {
|
|
1806
|
+
rmrfTolerant(target);
|
|
1807
|
+
} catch (e) {
|
|
1808
|
+
console.error(`[feishu_plugin_state_normalize] rmrf ${target} failed: ${e.message}`);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function ensureRecord(obj, key) {
|
|
1813
|
+
const cur = obj[key];
|
|
1814
|
+
if (cur != null && typeof cur === "object" && !Array.isArray(cur)) return cur;
|
|
1815
|
+
const fresh = {};
|
|
1816
|
+
obj[key] = fresh;
|
|
1817
|
+
return fresh;
|
|
1818
|
+
}
|
|
1819
|
+
//#endregion
|
|
1820
|
+
//#region src/version-compat.ts
|
|
1821
|
+
const VERSION_COMPAT_MAP = Object.freeze([
|
|
1822
|
+
{
|
|
1823
|
+
openclawLarkVersion: "2026.4.8",
|
|
1824
|
+
minOpenclawVersion: "2026.3.28"
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
openclawLarkVersion: "2026.4.7",
|
|
1828
|
+
minOpenclawVersion: "2026.3.28"
|
|
1829
|
+
},
|
|
1830
|
+
{
|
|
1831
|
+
openclawLarkVersion: "2026.4.1",
|
|
1832
|
+
minOpenclawVersion: "2026.3.28"
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
openclawLarkVersion: "2026.3.31",
|
|
1836
|
+
minOpenclawVersion: "2026.3.28"
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
openclawLarkVersion: "2026.3.30",
|
|
1840
|
+
minOpenclawVersion: "2026.3.28"
|
|
1841
|
+
},
|
|
1842
|
+
{
|
|
1843
|
+
openclawLarkVersion: "2026.3.29",
|
|
1844
|
+
minOpenclawVersion: "2026.3.28"
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
openclawLarkVersion: "2026.3.26",
|
|
1848
|
+
minOpenclawVersion: "2026.3.22"
|
|
1849
|
+
},
|
|
1850
|
+
{
|
|
1851
|
+
openclawLarkVersion: "2026.3.25",
|
|
1852
|
+
minOpenclawVersion: "2026.3.22"
|
|
1853
|
+
},
|
|
1854
|
+
{
|
|
1855
|
+
openclawLarkVersion: "2026.3.24",
|
|
1856
|
+
minOpenclawVersion: "2026.3.22"
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
openclawLarkVersion: "2026.3.18",
|
|
1860
|
+
minOpenclawVersion: "2026.2.26",
|
|
1861
|
+
maxOpenclawVersion: "2026.3.13"
|
|
1862
|
+
},
|
|
1863
|
+
{
|
|
1864
|
+
openclawLarkVersion: "2026.3.17",
|
|
1865
|
+
minOpenclawVersion: "2026.2.26",
|
|
1866
|
+
maxOpenclawVersion: "2026.3.13"
|
|
1867
|
+
},
|
|
1868
|
+
{
|
|
1869
|
+
openclawLarkVersion: "2026.3.15",
|
|
1870
|
+
minOpenclawVersion: "2026.2.26",
|
|
1871
|
+
maxOpenclawVersion: "2026.3.13"
|
|
1872
|
+
},
|
|
1873
|
+
{
|
|
1874
|
+
openclawLarkVersion: "2026.3.12",
|
|
1875
|
+
minOpenclawVersion: "2026.2.26",
|
|
1876
|
+
maxOpenclawVersion: "2026.3.13"
|
|
1877
|
+
},
|
|
1878
|
+
{
|
|
1879
|
+
openclawLarkVersion: "2026.3.10",
|
|
1880
|
+
minOpenclawVersion: "2026.2.26",
|
|
1881
|
+
maxOpenclawVersion: "2026.3.13"
|
|
1882
|
+
},
|
|
1883
|
+
{
|
|
1884
|
+
openclawLarkVersion: "2026.3.9",
|
|
1885
|
+
minOpenclawVersion: "2026.2.26",
|
|
1886
|
+
maxOpenclawVersion: "2026.3.13"
|
|
1887
|
+
}
|
|
1888
|
+
]);
|
|
1889
|
+
/**
|
|
1890
|
+
* "YYYY.M.D" 按数值分量比较(宽松解析:缺失/非数字段视为 0,suffix 如 "-rc.1" 被忽略)。
|
|
1891
|
+
* 不能用字符串比较 —— '2026.3.18' < '2026.3.9' 字符串语义为 true。
|
|
1892
|
+
*/
|
|
1893
|
+
function compareCalVer(a, b) {
|
|
1894
|
+
const pa = parseCalVer(a);
|
|
1895
|
+
const pb = parseCalVer(b);
|
|
1896
|
+
for (let i = 0; i < 3; i++) {
|
|
1897
|
+
if (pa[i] < pb[i]) return -1;
|
|
1898
|
+
if (pa[i] > pb[i]) return 1;
|
|
1899
|
+
}
|
|
1900
|
+
return 0;
|
|
1901
|
+
}
|
|
1902
|
+
function parseCalVer(v) {
|
|
1903
|
+
const parts = v.split(".");
|
|
1904
|
+
return [
|
|
1905
|
+
toNum(parts[0]),
|
|
1906
|
+
toNum(parts[1]),
|
|
1907
|
+
toNum(parts[2])
|
|
1908
|
+
];
|
|
1909
|
+
}
|
|
1910
|
+
function toNum(s) {
|
|
1911
|
+
const n = s != null ? parseInt(s, 10) : 0;
|
|
1912
|
+
return Number.isFinite(n) && n >= 0 ? n : 0;
|
|
1913
|
+
}
|
|
1914
|
+
/** Whether the given openclaw version falls inside this plugin entry's compat range. */
|
|
1915
|
+
function compatible(entry, openclawVersion) {
|
|
1916
|
+
if (compareCalVer(openclawVersion, entry.minOpenclawVersion) < 0) return false;
|
|
1917
|
+
if (entry.maxOpenclawVersion != null && compareCalVer(openclawVersion, entry.maxOpenclawVersion) > 0) return false;
|
|
1918
|
+
return true;
|
|
1919
|
+
}
|
|
1920
|
+
/** Look up an entry by exact plugin version; undefined if not in the table. */
|
|
1921
|
+
function findEntry(pluginVersion) {
|
|
1922
|
+
return VERSION_COMPAT_MAP.find((e) => e.openclawLarkVersion === pluginVersion);
|
|
1923
|
+
}
|
|
1924
|
+
//#endregion
|
|
1925
|
+
//#region src/rules/feishu-plugin-version-compat.ts
|
|
1926
|
+
const PLUGIN_NAME = "openclaw-lark";
|
|
1927
|
+
const LEGACY_SHORT_NAMES = ["feishu-openclaw-plugin"];
|
|
1928
|
+
const FORK_SCOPES = ["@lark-apaas"];
|
|
1929
|
+
/**
|
|
1930
|
+
* 飞书插件 ↔ openclaw 版本兼容检测。
|
|
1931
|
+
*
|
|
1932
|
+
* 检测顺序(短路):
|
|
1933
|
+
* 1. fork 魔改版(@lark-apaas scope)→ pass,不查版本
|
|
1934
|
+
* 2. legacy 老插件(feishu-openclaw-plugin 短名)→ 走升级流程
|
|
1935
|
+
* 3. 官方版(@larksuite scope 或其他)→ 按 VERSION_COMPAT_MAP 校验
|
|
1936
|
+
*
|
|
1937
|
+
* 决策方向:oc < 推荐 → upgrade_openclaw;oc ≥ 推荐 → upgrade_lark。
|
|
1938
|
+
*
|
|
1939
|
+
* `repairMode: user-confirm` —— 失败结果进 failedRules.userConfirm;CLI 不
|
|
1940
|
+
* 自动执行升级,由消费方弹用户确认后调对应升级接口。
|
|
1941
|
+
*/
|
|
1942
|
+
let FeishuPluginVersionCompatRule = class FeishuPluginVersionCompatRule extends DiagnoseRule {
|
|
1943
|
+
validate(ctx) {
|
|
1944
|
+
const recommendedOc = ctx.vars.recommendedOpenclawTag;
|
|
1945
|
+
if (!recommendedOc) {
|
|
1946
|
+
console.error("[feishu_plugin_version_compat] vars.recommendedOpenclawTag 未注入,跳过本规则");
|
|
1947
|
+
return { pass: true };
|
|
1948
|
+
}
|
|
1949
|
+
const ocCur = readOpenclawRuntimeVersion();
|
|
1950
|
+
if (!ocCur) return { pass: true };
|
|
1951
|
+
const installed = detectInstalledPlugin(ctx);
|
|
1952
|
+
if (installed == null) return { pass: true };
|
|
1953
|
+
if (isForkPlugin(installed)) return { pass: true };
|
|
1954
|
+
const isLegacy = isLegacyPlugin(installed);
|
|
1955
|
+
if (!isLegacy && isVersionCompatible(installed, ocCur)) return { pass: true };
|
|
1956
|
+
return decideUpgrade({
|
|
1957
|
+
ocCur,
|
|
1958
|
+
recommendedOc,
|
|
1959
|
+
installed,
|
|
1960
|
+
isLegacy
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
FeishuPluginVersionCompatRule = __decorate([Rule({
|
|
1965
|
+
key: "feishu_plugin_version_compat",
|
|
1966
|
+
dependsOn: ["config_syntax_check"],
|
|
1967
|
+
repairMode: "user-confirm",
|
|
1968
|
+
usesVars: ["recommendedOpenclawTag"]
|
|
1969
|
+
})], FeishuPluginVersionCompatRule);
|
|
1970
|
+
function isForkPlugin(p) {
|
|
1971
|
+
return p.scope != null && FORK_SCOPES.includes(p.scope);
|
|
1972
|
+
}
|
|
1973
|
+
function isLegacyPlugin(p) {
|
|
1974
|
+
return LEGACY_SHORT_NAMES.includes(p.allowName);
|
|
1975
|
+
}
|
|
1976
|
+
/** 装了但版本读不到、版本不在表里、或不在 entry 区间内,都视为不兼容。 */
|
|
1977
|
+
function isVersionCompatible(p, ocCur) {
|
|
1978
|
+
if (!p.version) return false;
|
|
1979
|
+
const entry = findEntry(p.version);
|
|
1980
|
+
return entry != null && compatible(entry, ocCur);
|
|
1981
|
+
}
|
|
1982
|
+
function decideUpgrade(args) {
|
|
1983
|
+
const { ocCur, recommendedOc, installed, isLegacy } = args;
|
|
1984
|
+
const desc = describePlugin(installed);
|
|
1985
|
+
const prefix = isLegacy ? `检测到老飞书插件 (${desc})` : `飞书插件 ${desc} 与 openclaw@${ocCur} 不兼容`;
|
|
1986
|
+
if (compareCalVer(ocCur, recommendedOc) < 0) return {
|
|
1987
|
+
pass: false,
|
|
1988
|
+
action: "upgrade_openclaw",
|
|
1989
|
+
message: `${prefix};将 openclaw 升级到 ${recommendedOc},飞书插件会随之同步升级`
|
|
1990
|
+
};
|
|
1991
|
+
return {
|
|
1992
|
+
pass: false,
|
|
1993
|
+
action: "upgrade_lark",
|
|
1994
|
+
message: `${prefix};当前 openclaw@${ocCur} 已达推荐版本,可直接升级飞书插件`
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
function describePlugin(p) {
|
|
1998
|
+
return (p.fullName ?? p.allowName) + (p.version ? `@${p.version}` : "");
|
|
1999
|
+
}
|
|
2000
|
+
function readPluginPackageJson(filePath) {
|
|
2001
|
+
try {
|
|
2002
|
+
if (!node_fs.default.existsSync(filePath)) return null;
|
|
2003
|
+
const raw = node_fs.default.readFileSync(filePath, "utf-8");
|
|
2004
|
+
const parsed = JSON.parse(raw);
|
|
2005
|
+
return {
|
|
2006
|
+
name: typeof parsed.name === "string" ? parsed.name : void 0,
|
|
2007
|
+
version: typeof parsed.version === "string" ? parsed.version : void 0
|
|
2008
|
+
};
|
|
2009
|
+
} catch {
|
|
2010
|
+
return null;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
/** "已装" = plugins.allow 含名 AND extensions/<name>/package.json 真实存在。 */
|
|
2014
|
+
function detectInstalledPlugin(ctx) {
|
|
2015
|
+
const allowRaw = asRecord(ctx.config.plugins)?.allow;
|
|
2016
|
+
const allow = Array.isArray(allowRaw) ? allowRaw.filter((e) => typeof e === "string") : [];
|
|
2017
|
+
const extDir = getExtensionsDir(ctx.configPath);
|
|
2018
|
+
const installs = getNestedMap(ctx.config, "plugins", "installs");
|
|
2019
|
+
for (const name of [PLUGIN_NAME, ...LEGACY_SHORT_NAMES]) {
|
|
2020
|
+
if (!allow.includes(name)) continue;
|
|
2021
|
+
const pkgPath = node_path.default.join(extDir, name, "package.json");
|
|
2022
|
+
if (!node_fs.default.existsSync(pkgPath)) continue;
|
|
2023
|
+
const pkg = readPluginPackageJson(pkgPath) ?? {};
|
|
2024
|
+
const installEntry = installs && asRecord(installs[name]);
|
|
2025
|
+
const fullName = pkg.name ?? extractScopedNameFromSpec(installEntry?.spec);
|
|
2026
|
+
return {
|
|
2027
|
+
allowName: name,
|
|
2028
|
+
fullName,
|
|
2029
|
+
scope: fullName?.startsWith("@") ? fullName.split("/")[0] : void 0,
|
|
2030
|
+
version: pkg.version ?? (typeof installEntry?.version === "string" ? installEntry.version : void 0)
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
return null;
|
|
2034
|
+
}
|
|
2035
|
+
/** "@scope/name@1.2.3" / "name@1.2.3" / "@scope/name" / "name" → 去掉 @version 后缀 */
|
|
2036
|
+
function extractScopedNameFromSpec(spec) {
|
|
2037
|
+
if (typeof spec !== "string") return void 0;
|
|
2038
|
+
const at = spec.indexOf("@", 1);
|
|
2039
|
+
return at === -1 ? spec : spec.slice(0, at);
|
|
2040
|
+
}
|
|
2041
|
+
//#endregion
|
|
2042
|
+
//#region src/check.ts
|
|
2043
|
+
/** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
|
|
2044
|
+
* AND a DoctorReport-shape payload (for `openclaw.report_cli_run`). The
|
|
2045
|
+
* two views are computed from the same single rule-loop pass. */
|
|
2046
|
+
function runCheckWithReport(input) {
|
|
2047
|
+
return runCheckImpl(input);
|
|
2048
|
+
}
|
|
2049
|
+
function runCheckImpl(input) {
|
|
2050
|
+
const tStart = Date.now();
|
|
2051
|
+
const result = { failedRules: {
|
|
2052
|
+
standard: [],
|
|
2053
|
+
ai: [],
|
|
2054
|
+
reset: [],
|
|
2055
|
+
userConfirm: []
|
|
2056
|
+
} };
|
|
2057
|
+
const outcomes = [];
|
|
2058
|
+
const disabledSet = new Set(input.disabledRules || []);
|
|
2059
|
+
const rules = getAllRules();
|
|
2060
|
+
const failedKeys = /* @__PURE__ */ new Set();
|
|
2061
|
+
let configParsed = false;
|
|
2062
|
+
let aborted = false;
|
|
2063
|
+
console.error(`runCheck: begin configPath=${input.configPath} disabledRules=[${[...disabledSet].join(",")}] rules=${rules.length}`);
|
|
2064
|
+
let ctx = {
|
|
2065
|
+
config: {},
|
|
2066
|
+
configPath: input.configPath,
|
|
2067
|
+
vars: input.vars,
|
|
2068
|
+
providerDeps: {
|
|
2069
|
+
usesMiaodaProvider: false,
|
|
2070
|
+
usesMiaodaSecretProvider: false
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
for (const rule of rules) {
|
|
2074
|
+
const meta = rule.meta;
|
|
2075
|
+
if (disabledSet.has(meta.key)) {
|
|
2076
|
+
console.error(`rule ${meta.key}: skipped reason=disabledRules`);
|
|
2077
|
+
outcomes.push({
|
|
2078
|
+
rule: meta.key,
|
|
2079
|
+
status: "skipped",
|
|
2080
|
+
message: "disabledRules"
|
|
2081
|
+
});
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
if (meta.dependsOn?.some((dep) => failedKeys.has(dep))) {
|
|
2085
|
+
const blockers = meta.dependsOn?.filter((d) => failedKeys.has(d)).join(",");
|
|
2086
|
+
console.error(`rule ${meta.key}: skipped reason=dependsOn-failed blockers=${blockers}`);
|
|
2087
|
+
outcomes.push({
|
|
2088
|
+
rule: meta.key,
|
|
2089
|
+
status: "skipped",
|
|
2090
|
+
message: `dependsOn failed: ${blockers}`
|
|
2091
|
+
});
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
if (meta.dependsOn?.includes("config_syntax_check") && !configParsed) try {
|
|
2095
|
+
const parsed = loadJSON5().parse(readFile(input.configPath));
|
|
2096
|
+
const deps = analyzeProviderDeps(parsed);
|
|
2097
|
+
ctx = {
|
|
2098
|
+
config: parsed,
|
|
2099
|
+
configPath: input.configPath,
|
|
2100
|
+
vars: input.vars,
|
|
2101
|
+
providerDeps: deps
|
|
2102
|
+
};
|
|
2103
|
+
configParsed = true;
|
|
2104
|
+
} catch (e) {
|
|
2105
|
+
console.error(`runCheck: config parse failed at ${meta.key} message=${e.message}`);
|
|
2106
|
+
outcomes.push({
|
|
2107
|
+
rule: meta.key,
|
|
2108
|
+
status: "error",
|
|
2109
|
+
message: "config parse failed: " + e.message
|
|
2110
|
+
});
|
|
2111
|
+
aborted = true;
|
|
2112
|
+
break;
|
|
2113
|
+
}
|
|
2114
|
+
if (meta.skipWhen && configParsed) {
|
|
2115
|
+
const hasMiaoda = shouldCheckMiaodaRules(ctx.config);
|
|
2116
|
+
if (meta.skipWhen({
|
|
2117
|
+
hasMiaoda,
|
|
2118
|
+
deps: ctx.providerDeps
|
|
2119
|
+
})) {
|
|
2120
|
+
console.error(`rule ${meta.key}: skipped reason=skipWhen`);
|
|
2121
|
+
outcomes.push({
|
|
2122
|
+
rule: meta.key,
|
|
2123
|
+
status: "skipped",
|
|
2124
|
+
message: "skipWhen"
|
|
2125
|
+
});
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
const t = Date.now();
|
|
2130
|
+
let r;
|
|
2131
|
+
try {
|
|
2132
|
+
r = rule.validate(ctx);
|
|
2133
|
+
} catch (e) {
|
|
2134
|
+
const msg = e.message;
|
|
2135
|
+
console.error(`rule ${meta.key}: validate -> error durationMs=${Date.now() - t} message=${msg}`);
|
|
2136
|
+
outcomes.push({
|
|
2137
|
+
rule: meta.key,
|
|
2138
|
+
status: "error",
|
|
2139
|
+
message: "validate threw: " + msg
|
|
2140
|
+
});
|
|
2141
|
+
failedKeys.add(meta.key);
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
if (r.pass) {
|
|
2145
|
+
console.error(`rule ${meta.key}: validate -> pass durationMs=${Date.now() - t}`);
|
|
2146
|
+
outcomes.push({
|
|
2147
|
+
rule: meta.key,
|
|
2148
|
+
status: "pass"
|
|
2149
|
+
});
|
|
2150
|
+
} else {
|
|
2151
|
+
console.error(`rule ${meta.key}: validate -> failed durationMs=${Date.now() - t} message=${r.message ?? ""}`);
|
|
2152
|
+
outcomes.push({
|
|
2153
|
+
rule: meta.key,
|
|
2154
|
+
status: "failed",
|
|
2155
|
+
message: r.message,
|
|
2156
|
+
action: r.action
|
|
2157
|
+
});
|
|
2158
|
+
failedKeys.add(meta.key);
|
|
2159
|
+
pushFailedRule(result, meta.repairMode, meta.key, r.message || "", r.action);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
console.error(`runCheck: end totalMs=${Date.now() - tStart} failed={standard:${result.failedRules.standard.length},ai:${result.failedRules.ai.length},reset:${result.failedRules.reset.length}}`);
|
|
2163
|
+
return {
|
|
2164
|
+
legacy: result,
|
|
2165
|
+
report: finalize$2(outcomes, aborted)
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
function pushFailedRule(result, repairMode, key, detail, action) {
|
|
2169
|
+
switch (repairMode) {
|
|
2170
|
+
case "standard":
|
|
2171
|
+
result.failedRules.standard.push(key);
|
|
2172
|
+
break;
|
|
2173
|
+
case "ai":
|
|
2174
|
+
result.failedRules.ai.push({
|
|
2175
|
+
rule_name: key,
|
|
2176
|
+
detail
|
|
2177
|
+
});
|
|
2178
|
+
break;
|
|
2179
|
+
case "reset":
|
|
2180
|
+
result.failedRules.reset.push({
|
|
2181
|
+
rule_name: key,
|
|
2182
|
+
detail
|
|
2183
|
+
});
|
|
2184
|
+
break;
|
|
2185
|
+
case "user-confirm":
|
|
2186
|
+
result.failedRules.userConfirm.push({
|
|
2187
|
+
rule_name: key,
|
|
2188
|
+
detail,
|
|
2189
|
+
action
|
|
2190
|
+
});
|
|
2191
|
+
break;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
function finalize$2(results, aborted) {
|
|
2195
|
+
const summary = {
|
|
2196
|
+
pass: 0,
|
|
2197
|
+
failed: 0,
|
|
2198
|
+
fixed: 0,
|
|
2199
|
+
stillBroken: 0,
|
|
2200
|
+
skipped: 0,
|
|
2201
|
+
error: 0,
|
|
2202
|
+
unknown: 0
|
|
2203
|
+
};
|
|
2204
|
+
for (const r of results) switch (r.status) {
|
|
2205
|
+
case "pass":
|
|
2206
|
+
summary.pass++;
|
|
2207
|
+
break;
|
|
2208
|
+
case "failed":
|
|
2209
|
+
summary.failed++;
|
|
2210
|
+
break;
|
|
2211
|
+
case "fixed":
|
|
2212
|
+
summary.fixed++;
|
|
2213
|
+
break;
|
|
2214
|
+
case "still-broken":
|
|
2215
|
+
summary.stillBroken++;
|
|
2216
|
+
break;
|
|
2217
|
+
case "skipped":
|
|
2218
|
+
summary.skipped++;
|
|
2219
|
+
break;
|
|
2220
|
+
case "error":
|
|
2221
|
+
summary.error++;
|
|
2222
|
+
break;
|
|
2223
|
+
case "unknown":
|
|
2224
|
+
summary.unknown++;
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2227
|
+
return {
|
|
2228
|
+
results,
|
|
2229
|
+
summary,
|
|
2230
|
+
aborted
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
//#endregion
|
|
2234
|
+
//#region src/repair.ts
|
|
2235
|
+
/** Telemetry-aware entry. Same per-rule pass + side effects, but also
|
|
2236
|
+
* builds a `DoctorReport`-shape payload from per-rule revalidate
|
|
2237
|
+
* outcomes (`fixed` / `still-broken` / `error`) so `report_cli_run`
|
|
2238
|
+
* carries the same shape doctor produces. */
|
|
2239
|
+
function runRepairWithReport(input) {
|
|
2240
|
+
return runRepairImpl(input);
|
|
2241
|
+
}
|
|
2242
|
+
function runRepairImpl(input) {
|
|
2243
|
+
const tStart = Date.now();
|
|
2244
|
+
console.error(`runRepair: begin configPath=${input.configPath} failedRules=[${(input.failedRules ?? []).join(",")}]`);
|
|
2245
|
+
const outcomes = [];
|
|
2246
|
+
let aborted = false;
|
|
2247
|
+
let legacy = { success: true };
|
|
2248
|
+
try {
|
|
2249
|
+
const failedSet = new Set(input.failedRules || []);
|
|
2250
|
+
const repairData = input.repairData || {};
|
|
2251
|
+
const rules = getAllRules();
|
|
2252
|
+
const phase1Ctx = {
|
|
2253
|
+
config: {},
|
|
2254
|
+
configPath: input.configPath,
|
|
2255
|
+
vars: input.vars,
|
|
2256
|
+
providerDeps: {
|
|
2257
|
+
usesMiaodaProvider: false,
|
|
2258
|
+
usesMiaodaSecretProvider: false
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
for (const rule of rules) {
|
|
2262
|
+
if (!failedSet.has(rule.meta.key)) continue;
|
|
2263
|
+
if (rule.meta.repairMode !== "standard") continue;
|
|
2264
|
+
if (rule.meta.dependsOn?.includes("config_syntax_check")) continue;
|
|
2265
|
+
const tRepair = Date.now();
|
|
2266
|
+
try {
|
|
2267
|
+
rule.repair(phase1Ctx);
|
|
2268
|
+
} catch (e) {
|
|
2269
|
+
const msg = e.message;
|
|
2270
|
+
console.error(`rule ${rule.meta.key}: repair (phase1) -> error durationMs=${Date.now() - tRepair} message=${msg}`);
|
|
2271
|
+
outcomes.push({
|
|
2272
|
+
rule: rule.meta.key,
|
|
2273
|
+
status: "error",
|
|
2274
|
+
message: "repair threw: " + msg
|
|
2275
|
+
});
|
|
2276
|
+
aborted = true;
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
console.error(`rule ${rule.meta.key}: repair (phase1) -> ok durationMs=${Date.now() - tRepair}`);
|
|
2280
|
+
const tRev = Date.now();
|
|
2281
|
+
try {
|
|
2282
|
+
const v = rule.validate(phase1Ctx);
|
|
2283
|
+
if (v.pass) {
|
|
2284
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase1) -> pass (fixed) durationMs=${Date.now() - tRev}`);
|
|
2285
|
+
outcomes.push({
|
|
2286
|
+
rule: rule.meta.key,
|
|
2287
|
+
status: "fixed"
|
|
2288
|
+
});
|
|
2289
|
+
} else {
|
|
2290
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase1) -> still-broken durationMs=${Date.now() - tRev} after=${v.message ?? ""}`);
|
|
2291
|
+
outcomes.push({
|
|
2292
|
+
rule: rule.meta.key,
|
|
2293
|
+
status: "still-broken",
|
|
2294
|
+
after: v.message
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
2297
|
+
} catch (e) {
|
|
2298
|
+
const msg = e.message;
|
|
2299
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase1) -> error durationMs=${Date.now() - tRev} message=${msg}`);
|
|
2300
|
+
outcomes.push({
|
|
2301
|
+
rule: rule.meta.key,
|
|
2302
|
+
status: "error",
|
|
2303
|
+
message: "post-repair validate threw: " + msg
|
|
2304
|
+
});
|
|
2305
|
+
aborted = true;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const JSON5 = loadJSON5();
|
|
2309
|
+
let config;
|
|
2310
|
+
try {
|
|
2311
|
+
config = JSON5.parse(readFile(input.configPath));
|
|
2312
|
+
} catch (e) {
|
|
2313
|
+
const msg = e.message;
|
|
2314
|
+
console.error(`runRepair: config parse failed message=${msg}`);
|
|
2315
|
+
outcomes.push({
|
|
2316
|
+
rule: "config_syntax_check",
|
|
2317
|
+
status: "error",
|
|
2318
|
+
message: "config parse failed: " + msg
|
|
2319
|
+
});
|
|
2320
|
+
legacy = {
|
|
2321
|
+
success: false,
|
|
2322
|
+
error: "cannot parse config for repair: " + msg
|
|
2323
|
+
};
|
|
2324
|
+
return {
|
|
2325
|
+
legacy,
|
|
2326
|
+
report: finalize$1(outcomes, true)
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
const deps = analyzeProviderDeps(config);
|
|
2330
|
+
const ctx = {
|
|
2331
|
+
config,
|
|
2332
|
+
configPath: input.configPath,
|
|
2333
|
+
vars: input.vars,
|
|
2334
|
+
providerDeps: deps
|
|
2335
|
+
};
|
|
2336
|
+
let configDirty = false;
|
|
2337
|
+
for (const rule of rules) {
|
|
2338
|
+
if (!failedSet.has(rule.meta.key)) continue;
|
|
2339
|
+
if (rule.meta.repairMode !== "standard") continue;
|
|
2340
|
+
if (!rule.meta.dependsOn?.includes("config_syntax_check")) continue;
|
|
2341
|
+
const tRepair = Date.now();
|
|
2342
|
+
try {
|
|
2343
|
+
rule.repair(ctx);
|
|
2344
|
+
} catch (e) {
|
|
2345
|
+
const msg = e.message;
|
|
2346
|
+
console.error(`rule ${rule.meta.key}: repair (phase2) -> error durationMs=${Date.now() - tRepair} message=${msg}`);
|
|
2347
|
+
outcomes.push({
|
|
2348
|
+
rule: rule.meta.key,
|
|
2349
|
+
status: "error",
|
|
2350
|
+
message: "repair threw: " + msg
|
|
2351
|
+
});
|
|
2352
|
+
aborted = true;
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
|
+
console.error(`rule ${rule.meta.key}: repair (phase2) -> ok durationMs=${Date.now() - tRepair}`);
|
|
2356
|
+
configDirty = true;
|
|
2357
|
+
const tRev = Date.now();
|
|
2358
|
+
try {
|
|
2359
|
+
const v = rule.validate(ctx);
|
|
2360
|
+
if (v.pass) {
|
|
2361
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase2) -> pass (fixed) durationMs=${Date.now() - tRev}`);
|
|
2362
|
+
outcomes.push({
|
|
2363
|
+
rule: rule.meta.key,
|
|
2364
|
+
status: "fixed"
|
|
2365
|
+
});
|
|
2366
|
+
} else {
|
|
2367
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase2) -> still-broken durationMs=${Date.now() - tRev} after=${v.message ?? ""}`);
|
|
2368
|
+
outcomes.push({
|
|
2369
|
+
rule: rule.meta.key,
|
|
2370
|
+
status: "still-broken",
|
|
2371
|
+
after: v.message
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2374
|
+
} catch (e) {
|
|
2375
|
+
const msg = e.message;
|
|
2376
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase2) -> error durationMs=${Date.now() - tRev} message=${msg}`);
|
|
2377
|
+
outcomes.push({
|
|
2378
|
+
rule: rule.meta.key,
|
|
2379
|
+
status: "error",
|
|
2380
|
+
message: "post-repair validate threw: " + msg
|
|
2381
|
+
});
|
|
2382
|
+
aborted = true;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (configDirty) {
|
|
2386
|
+
backupConfigSync(input.configPath);
|
|
2387
|
+
const serialized = JSON.stringify(config, null, 2);
|
|
2388
|
+
writeFile(input.configPath, serialized);
|
|
2389
|
+
console.error(`runRepair: config writeback ok path=${input.configPath} bytes=${serialized.length}`);
|
|
2390
|
+
}
|
|
2391
|
+
if (repairData.secretsContent && input.vars.secretsFilePath) {
|
|
2392
|
+
writeFile(input.vars.secretsFilePath, repairData.secretsContent);
|
|
2393
|
+
console.error(`runRepair: secrets writeback ok path=${input.vars.secretsFilePath} bytes=${repairData.secretsContent.length}`);
|
|
2394
|
+
}
|
|
2395
|
+
if (repairData.providerKeyContent && input.vars.providerFilePath) {
|
|
2396
|
+
writeFile(input.vars.providerFilePath, repairData.providerKeyContent);
|
|
2397
|
+
console.error(`runRepair: provider key writeback ok path=${input.vars.providerFilePath} bytes=${repairData.providerKeyContent.length}`);
|
|
2398
|
+
}
|
|
2399
|
+
if (repairData.restartCommand) {
|
|
2400
|
+
const t = Date.now();
|
|
2401
|
+
try {
|
|
2402
|
+
console.error(`runRepair: restart begin cmd=${JSON.stringify(repairData.restartCommand)}`);
|
|
2403
|
+
shell(repairData.restartCommand, 3e4);
|
|
2404
|
+
console.error(`runRepair: restart ok durationMs=${Date.now() - t}`);
|
|
2405
|
+
} catch (e) {
|
|
2406
|
+
const msg = e.message;
|
|
2407
|
+
console.error(`runRepair: restart failed durationMs=${Date.now() - t} message=${msg}`);
|
|
2408
|
+
legacy = {
|
|
2409
|
+
success: false,
|
|
2410
|
+
error: "restart command failed: " + msg
|
|
2411
|
+
};
|
|
2412
|
+
return {
|
|
2413
|
+
legacy,
|
|
2414
|
+
report: finalize$1(outcomes, true)
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
console.error(`runRepair: end success=true totalMs=${Date.now() - tStart}`);
|
|
2419
|
+
legacy = { success: true };
|
|
2420
|
+
return {
|
|
2421
|
+
legacy,
|
|
2422
|
+
report: finalize$1(outcomes, aborted)
|
|
2423
|
+
};
|
|
2424
|
+
} catch (e) {
|
|
2425
|
+
const msg = e.message;
|
|
2426
|
+
console.error(`runRepair: end success=false totalMs=${Date.now() - tStart} message=${msg}`);
|
|
2427
|
+
legacy = {
|
|
2428
|
+
success: false,
|
|
2429
|
+
error: "repair failed: " + msg
|
|
2430
|
+
};
|
|
2431
|
+
return {
|
|
2432
|
+
legacy,
|
|
2433
|
+
report: finalize$1(outcomes, true)
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
function finalize$1(results, aborted) {
|
|
2438
|
+
const summary = {
|
|
2439
|
+
pass: 0,
|
|
2440
|
+
failed: 0,
|
|
2441
|
+
fixed: 0,
|
|
2442
|
+
stillBroken: 0,
|
|
2443
|
+
skipped: 0,
|
|
2444
|
+
error: 0,
|
|
2445
|
+
unknown: 0
|
|
2446
|
+
};
|
|
2447
|
+
for (const r of results) switch (r.status) {
|
|
2448
|
+
case "pass":
|
|
2449
|
+
summary.pass++;
|
|
2450
|
+
break;
|
|
2451
|
+
case "failed":
|
|
2452
|
+
summary.failed++;
|
|
2453
|
+
break;
|
|
2454
|
+
case "fixed":
|
|
2455
|
+
summary.fixed++;
|
|
2456
|
+
break;
|
|
2457
|
+
case "still-broken":
|
|
2458
|
+
summary.stillBroken++;
|
|
2459
|
+
break;
|
|
2460
|
+
case "skipped":
|
|
2461
|
+
summary.skipped++;
|
|
2462
|
+
break;
|
|
2463
|
+
case "error":
|
|
2464
|
+
summary.error++;
|
|
2465
|
+
break;
|
|
2466
|
+
case "unknown":
|
|
2467
|
+
summary.unknown++;
|
|
2468
|
+
break;
|
|
2469
|
+
}
|
|
2470
|
+
return {
|
|
2471
|
+
results,
|
|
2472
|
+
summary,
|
|
2473
|
+
aborted
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
//#endregion
|
|
2477
|
+
//#region src/paths.ts
|
|
2478
|
+
/**
|
|
2479
|
+
* Central directory for all ephemeral diagnose/reset artifacts: task status
|
|
2480
|
+
* files (`reset-<taskId>.json`) and human-readable step logs
|
|
2481
|
+
* (`reset-<taskId>.log`). Having everything under one dir makes debugging a
|
|
2482
|
+
* stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
|
|
2483
|
+
* run, and each run's log is right next to its state.
|
|
2484
|
+
*/
|
|
2485
|
+
const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
|
|
2486
|
+
function resetResultFile(taskId) {
|
|
2487
|
+
return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
|
|
2488
|
+
}
|
|
2489
|
+
function resetLogFile(taskId) {
|
|
2490
|
+
return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
|
|
2491
|
+
}
|
|
2492
|
+
/** Sandbox workspace root where openclaw config + agent state lives. */
|
|
2493
|
+
const WORKSPACE_DIR = "/home/gem/workspace/agent";
|
|
2494
|
+
/** File containing the provider key used by the openclaw miaoda provider. */
|
|
2495
|
+
const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
|
|
2496
|
+
/** File containing the miaoda openclaw secrets JSON. */
|
|
2497
|
+
const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
|
|
2498
|
+
/** Absolute path to the openclaw config JSON. */
|
|
2499
|
+
const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
|
|
2500
|
+
//#endregion
|
|
2501
|
+
//#region src/run-log.ts
|
|
2502
|
+
let currentRunContext;
|
|
2503
|
+
/**
|
|
2504
|
+
* Install the run context for this CLI execution. Must be called once at
|
|
2505
|
+
* the top of `main()` before any subcommand work; idempotent (last call
|
|
2506
|
+
* wins, but `main` only calls it once).
|
|
2507
|
+
*
|
|
2508
|
+
* Resolution order for `runId`:
|
|
2509
|
+
* 1. `OPENCLAW_RUN_ID` env var — set by a parent CLI process (e.g. the
|
|
2510
|
+
* `reset --async` dispatcher exports it before spawning the worker)
|
|
2511
|
+
* so cli.log lines for both processes carry the same `[run=...]`
|
|
2512
|
+
* tag. Any non-empty value is honoured verbatim.
|
|
2513
|
+
* 2. `--trace-id=<id>` flag — caller-supplied upstream trace id.
|
|
2514
|
+
* 3. Generated locally as `gen-<8 hex>`.
|
|
2515
|
+
*
|
|
2516
|
+
* Safe to call without any input — runId falls through to the generated
|
|
2517
|
+
* branch and both metadata fields stay undefined.
|
|
2518
|
+
*/
|
|
2519
|
+
function setRunContext(opts) {
|
|
2520
|
+
const inherited = process.env.OPENCLAW_RUN_ID;
|
|
2521
|
+
if (inherited && inherited !== "") currentRunContext = {
|
|
2522
|
+
runId: inherited,
|
|
2523
|
+
caller: opts.caller,
|
|
2524
|
+
traceId: opts.traceId,
|
|
2525
|
+
generated: false
|
|
2526
|
+
};
|
|
2527
|
+
else if (opts.traceId) currentRunContext = {
|
|
2528
|
+
runId: opts.traceId,
|
|
2529
|
+
caller: opts.caller,
|
|
2530
|
+
traceId: opts.traceId,
|
|
2531
|
+
generated: false
|
|
2532
|
+
};
|
|
2533
|
+
else currentRunContext = {
|
|
2534
|
+
runId: "gen-" + (0, node_crypto.randomBytes)(4).toString("hex"),
|
|
2535
|
+
caller: opts.caller,
|
|
2536
|
+
traceId: void 0,
|
|
2537
|
+
generated: true
|
|
2538
|
+
};
|
|
2539
|
+
return currentRunContext;
|
|
2540
|
+
}
|
|
2541
|
+
function getRunContext() {
|
|
2542
|
+
return currentRunContext;
|
|
2543
|
+
}
|
|
2544
|
+
/**
|
|
2545
|
+
* Format the run-scope tag the stderr mirror injects into cli.log lines.
|
|
2546
|
+
* Empty string when no context is set (mirror falls back to plain
|
|
2547
|
+
* timestamped lines, preserving pre-context startup logging).
|
|
2548
|
+
*/
|
|
2549
|
+
function runTag(rc = currentRunContext) {
|
|
2550
|
+
if (!rc) return "";
|
|
2551
|
+
const parts = [`run=${rc.runId}`];
|
|
2552
|
+
if (rc.caller) parts.push(`caller=${rc.caller}`);
|
|
2553
|
+
return `[${parts.join(" ")}]`;
|
|
2554
|
+
}
|
|
2555
|
+
//#endregion
|
|
2556
|
+
//#region src/logger.ts
|
|
2557
|
+
/**
|
|
2558
|
+
* Shared CLI log file. Every log line the CLI emits — whether through
|
|
2559
|
+
* `console.error` (rules, helpers, errors) or through the per-task
|
|
2560
|
+
* `makeLogger` (reset worker) — is tee'd here so operators have a single
|
|
2561
|
+
* file to tail when diagnosing a sandbox.
|
|
2562
|
+
*
|
|
2563
|
+
* `/tmp` is ephemeral on sandbox restart; we rely on that for rotation
|
|
2564
|
+
* (no size-based rotation implemented).
|
|
2565
|
+
*/
|
|
2566
|
+
const CLI_LOG_FILE = "/tmp/openclaw-diagnose/cli.log";
|
|
2567
|
+
/** Append one line to the shared cli.log. Swallows any fs error —
|
|
2568
|
+
* logging must never break the business flow. */
|
|
2569
|
+
function appendCliLog(line) {
|
|
2570
|
+
try {
|
|
2571
|
+
const dir = node_path.default.dirname(CLI_LOG_FILE);
|
|
2572
|
+
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
2573
|
+
node_fs.default.appendFileSync(CLI_LOG_FILE, line);
|
|
2574
|
+
} catch {}
|
|
2575
|
+
}
|
|
2576
|
+
let stderrMirrorInstalled = false;
|
|
2577
|
+
/**
|
|
2578
|
+
* Install a process-wide `console.error` interceptor. Always tees each
|
|
2579
|
+
* line to cli.log; the stderr passthrough is opt-in via `stderrEnabled`.
|
|
2580
|
+
*
|
|
2581
|
+
* Default (`stderrEnabled: false`): the terminal sees ONLY this command's
|
|
2582
|
+
* stdout (typically a single JSON result line). Lifecycle / progress /
|
|
2583
|
+
* error logs are fully captured in `/tmp/openclaw-diagnose/cli.log`,
|
|
2584
|
+
* which is the canonical debugging surface and already carries
|
|
2585
|
+
* timestamps and `[run=...]` tags.
|
|
2586
|
+
*
|
|
2587
|
+
* Verbose (`stderrEnabled: true`, opt in with the top-level `-x` flag):
|
|
2588
|
+
* lifecycle logs also stream to stderr in real time. Useful when
|
|
2589
|
+
* iterating on a single sandbox.
|
|
2590
|
+
*
|
|
2591
|
+
* Idempotent: calling twice is a no-op (first install wins). Tests that
|
|
2592
|
+
* need to swap mode should call `resetStderrMirrorForTests()` first.
|
|
2593
|
+
*
|
|
2594
|
+
* Why console.error (and not console.log): the CLI's stdout carries the
|
|
2595
|
+
* structured JSON result protocol consumed by sandbox_console and other
|
|
2596
|
+
* callers — any log line on stdout would corrupt JSON parsing.
|
|
2597
|
+
*/
|
|
2598
|
+
function installStderrMirror(opts = {}) {
|
|
2599
|
+
if (stderrMirrorInstalled) return;
|
|
2600
|
+
stderrMirrorInstalled = true;
|
|
2601
|
+
const stderrEnabled = opts.stderrEnabled ?? false;
|
|
2602
|
+
const original = console.error.bind(console);
|
|
2603
|
+
console.error = (...args) => {
|
|
2604
|
+
if (stderrEnabled) original(...args);
|
|
2605
|
+
const body = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
|
|
2606
|
+
const tag = runTag();
|
|
2607
|
+
appendCliLog(`[${(/* @__PURE__ */ new Date()).toISOString()}]${tag ? ` ${tag}` : ""} ${body}\n`);
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
function safeStringify(v) {
|
|
2611
|
+
try {
|
|
2612
|
+
return JSON.stringify(v);
|
|
2613
|
+
} catch {
|
|
2614
|
+
return String(v);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
function makeLogger(logFile) {
|
|
2618
|
+
try {
|
|
2619
|
+
const dir = node_path.default.dirname(logFile);
|
|
2620
|
+
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
2621
|
+
} catch {}
|
|
2622
|
+
return (msg) => {
|
|
2623
|
+
const tag = runTag();
|
|
2624
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}]${tag ? ` ${tag}` : ""} ${msg}\n`;
|
|
2625
|
+
try {
|
|
2626
|
+
node_fs.default.appendFileSync(logFile, line);
|
|
2627
|
+
} catch {}
|
|
2628
|
+
appendCliLog(line);
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
//#endregion
|
|
1749
2632
|
//#region src/reset-async.ts
|
|
1750
2633
|
/**
|
|
1751
2634
|
* Start an async reset task: spawn a detached child process and return the taskId.
|
|
@@ -1769,6 +2652,9 @@ function startAsyncReset(ctxBase64) {
|
|
|
1769
2652
|
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
1770
2653
|
node_fs.default.writeFileSync(tmpPath, JSON.stringify(initial), "utf-8");
|
|
1771
2654
|
moveSafe(tmpPath, resultFile);
|
|
2655
|
+
const rc = getRunContext();
|
|
2656
|
+
const childEnv = { ...process.env };
|
|
2657
|
+
if (rc?.runId) childEnv.OPENCLAW_RUN_ID = rc.runId;
|
|
1772
2658
|
const child = (0, node_child_process.spawn)(process.execPath, [
|
|
1773
2659
|
process.argv[1],
|
|
1774
2660
|
"reset",
|
|
@@ -1777,7 +2663,8 @@ function startAsyncReset(ctxBase64) {
|
|
|
1777
2663
|
`--ctx=${ctxBase64}`
|
|
1778
2664
|
], {
|
|
1779
2665
|
detached: true,
|
|
1780
|
-
stdio: "ignore"
|
|
2666
|
+
stdio: "ignore",
|
|
2667
|
+
env: childEnv
|
|
1781
2668
|
});
|
|
1782
2669
|
child.on("error", (err) => {
|
|
1783
2670
|
log(`FATAL worker failed to start: ${err.message}`);
|
|
@@ -1799,6 +2686,121 @@ function startAsyncReset(ctxBase64) {
|
|
|
1799
2686
|
return { taskId };
|
|
1800
2687
|
}
|
|
1801
2688
|
//#endregion
|
|
2689
|
+
//#region src/oss/fetchWithDiag.ts
|
|
2690
|
+
/** Methods retried automatically on transient transport errors. POST and
|
|
2691
|
+
* PATCH are deliberately excluded: a transient ECONNRESET / ETIMEDOUT
|
|
2692
|
+
* after the request body has been written might mean the server
|
|
2693
|
+
* received and processed it (just couldn't reply) — auto-retrying
|
|
2694
|
+
* would cause duplicate side effects. AWS / GCP / Aliyun SDKs all use
|
|
2695
|
+
* the same allowlist. */
|
|
2696
|
+
const IDEMPOTENT_METHODS = new Set([
|
|
2697
|
+
"GET",
|
|
2698
|
+
"HEAD",
|
|
2699
|
+
"OPTIONS",
|
|
2700
|
+
"DELETE",
|
|
2701
|
+
"PUT"
|
|
2702
|
+
]);
|
|
2703
|
+
/** Errno codes treated as transient — almost always a stale connection
|
|
2704
|
+
* or momentary network blip that succeeds on retry. The list mirrors
|
|
2705
|
+
* what AWS / GCP / Aliyun SDKs retry by default. Only used for fetch
|
|
2706
|
+
* rejection cause; HTTP 4xx/5xx responses pass through unchanged
|
|
2707
|
+
* (caller decides whether the application-level status is retryable). */
|
|
2708
|
+
const TRANSIENT_CODES = new Set([
|
|
2709
|
+
"ECONNRESET",
|
|
2710
|
+
"ETIMEDOUT",
|
|
2711
|
+
"EPIPE",
|
|
2712
|
+
"ECONNREFUSED",
|
|
2713
|
+
"EHOSTUNREACH",
|
|
2714
|
+
"ENETUNREACH",
|
|
2715
|
+
"EAI_AGAIN"
|
|
2716
|
+
]);
|
|
2717
|
+
async function fetchWithDiag(url, opts = {}) {
|
|
2718
|
+
const maxRetries = opts.maxRetries ?? 2;
|
|
2719
|
+
const method = (opts.init?.method ?? "GET").toUpperCase();
|
|
2720
|
+
const retrySafe = opts.retryNonIdempotent || IDEMPOTENT_METHODS.has(method);
|
|
2721
|
+
let lastErr;
|
|
2722
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
2723
|
+
return await fetchOnce(url, opts);
|
|
2724
|
+
} catch (e) {
|
|
2725
|
+
lastErr = e;
|
|
2726
|
+
const code = extractCauseCode(e);
|
|
2727
|
+
if (!(code !== void 0 && TRANSIENT_CODES.has(code)) || attempt === maxRetries) throw e;
|
|
2728
|
+
if (!retrySafe) {
|
|
2729
|
+
console.error(`fetch ${opts.label ?? originOf(url)}: transient ${code} but method=${method} is non-idempotent, NOT retrying`);
|
|
2730
|
+
throw e;
|
|
2731
|
+
}
|
|
2732
|
+
const delay = 200 * Math.pow(2, attempt) + Math.floor(Math.random() * 100);
|
|
2733
|
+
console.error(`fetch ${opts.label ?? originOf(url)}: transient ${code}, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
2734
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
2735
|
+
}
|
|
2736
|
+
throw lastErr;
|
|
2737
|
+
}
|
|
2738
|
+
/** One fetch attempt with timeout. Throws on transport / abort failures
|
|
2739
|
+
* (caught by the retry loop above) and on its own enriched message form. */
|
|
2740
|
+
async function fetchOnce(url, opts) {
|
|
2741
|
+
const label = opts.label ?? originOf(url);
|
|
2742
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
2743
|
+
const start = Date.now();
|
|
2744
|
+
const ac = new AbortController();
|
|
2745
|
+
const timer = timeoutMs > 0 && Number.isFinite(timeoutMs) ? setTimeout(() => ac.abort(), timeoutMs) : void 0;
|
|
2746
|
+
try {
|
|
2747
|
+
return await fetch(url, {
|
|
2748
|
+
...opts.init,
|
|
2749
|
+
signal: ac.signal
|
|
2750
|
+
});
|
|
2751
|
+
} catch (e) {
|
|
2752
|
+
const durationMs = Date.now() - start;
|
|
2753
|
+
const causeStr = e.name === "AbortError" || ac.signal.aborted && timeoutMs > 0 ? `request aborted after ${timeoutMs}ms (timeout)` : describeCause(e.cause) || e.message;
|
|
2754
|
+
const enriched = /* @__PURE__ */ new Error(`fetch ${label} failed: ${causeStr} (url=${redactUrl(url)} durationMs=${durationMs})`);
|
|
2755
|
+
enriched.cause = e.cause ?? e;
|
|
2756
|
+
throw enriched;
|
|
2757
|
+
} finally {
|
|
2758
|
+
if (timer) clearTimeout(timer);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
/** Pull the errno-style code from the deepest cause we can reach. Used by
|
|
2762
|
+
* retry-classification only — error messages still get the human-readable
|
|
2763
|
+
* describeCause() rendering. */
|
|
2764
|
+
function extractCauseCode(e) {
|
|
2765
|
+
let cur = e.cause ?? e;
|
|
2766
|
+
for (let depth = 0; depth < 5 && cur; depth++) {
|
|
2767
|
+
const code = cur.code;
|
|
2768
|
+
if (typeof code === "string") return code;
|
|
2769
|
+
cur = cur.cause;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
/** Walk the Error.cause chain and produce a single-line summary like
|
|
2773
|
+
* `ENOTFOUND getaddrinfo ENOTFOUND oss.example.com`. */
|
|
2774
|
+
function describeCause(c, depth = 0) {
|
|
2775
|
+
if (!c || depth > 3) return "";
|
|
2776
|
+
if (c instanceof Error) {
|
|
2777
|
+
const code = c.code;
|
|
2778
|
+
const head = code ? `${code} ${c.message}` : c.message;
|
|
2779
|
+
const inner = describeCause(c.cause, depth + 1);
|
|
2780
|
+
return inner ? `${head} <- ${inner}` : head;
|
|
2781
|
+
}
|
|
2782
|
+
return String(c);
|
|
2783
|
+
}
|
|
2784
|
+
/** Strip query string — OSS signed URLs put the auth token there. Keeps
|
|
2785
|
+
* protocol/host/path so the operator can tell *which* OSS bucket / object
|
|
2786
|
+
* was being fetched. */
|
|
2787
|
+
function redactUrl(raw) {
|
|
2788
|
+
try {
|
|
2789
|
+
const u = new URL(raw);
|
|
2790
|
+
const tail = u.search ? "?<redacted>" : "";
|
|
2791
|
+
return `${u.protocol}//${u.host}${u.pathname}${tail}`;
|
|
2792
|
+
} catch {
|
|
2793
|
+
return "<invalid-url>";
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
function originOf(raw) {
|
|
2797
|
+
try {
|
|
2798
|
+
return new URL(raw).host;
|
|
2799
|
+
} catch {
|
|
2800
|
+
return "unknown-host";
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
//#endregion
|
|
1802
2804
|
//#region src/oss/fetchManifest.ts
|
|
1803
2805
|
const MANIFEST_PREFIX = "builtin/manifests/openclaw/recommended/";
|
|
1804
2806
|
const MANIFEST_SUFFIX = ".json";
|
|
@@ -1810,9 +2812,23 @@ async function fetchManifest(ossFileMap, tag) {
|
|
|
1810
2812
|
const availStr = available.length ? available.join(", ") : "(none)";
|
|
1811
2813
|
throw new Error(`manifest signed URL missing for tag "${tag}" (key ${key}). Available tags in ossFileMap: ${availStr}. Either pass an available tag or update the studio_server TCC openclaw_upgrade_config supported_versions.`);
|
|
1812
2814
|
}
|
|
1813
|
-
const
|
|
1814
|
-
|
|
1815
|
-
|
|
2815
|
+
const label = `openclaw manifest tag=${tag}`;
|
|
2816
|
+
const res = await fetchWithDiag(url, { label });
|
|
2817
|
+
if (!res.ok) {
|
|
2818
|
+
const body = await readPreview(res);
|
|
2819
|
+
throw new Error(`${label}: HTTP ${res.status} ${res.statusText}` + (body ? ` body=${body}` : ""));
|
|
2820
|
+
}
|
|
2821
|
+
return await res.json();
|
|
2822
|
+
}
|
|
2823
|
+
/** Best-effort body preview for HTTP-error diagnostics. OSS errors are
|
|
2824
|
+
* XML; first 256 chars usually contain the <Code> + <Message> we care
|
|
2825
|
+
* about. Failures here are swallowed (logging-only, not load-bearing). */
|
|
2826
|
+
async function readPreview(res) {
|
|
2827
|
+
try {
|
|
2828
|
+
return (await res.text()).slice(0, 256);
|
|
2829
|
+
} catch {
|
|
2830
|
+
return "";
|
|
2831
|
+
}
|
|
1816
2832
|
}
|
|
1817
2833
|
async function downloadWithCache(pkg, ossFileMap, opts = {}) {
|
|
1818
2834
|
const cacheRoot = opts.cacheRoot ?? "/tmp/openclaw-diagnose/resources";
|
|
@@ -1827,9 +2843,19 @@ async function downloadWithCache(pkg, ossFileMap, opts = {}) {
|
|
|
1827
2843
|
const expected = pkg.integrity.slice(7);
|
|
1828
2844
|
const tmpFile = node_path.default.join(destDir, `.tmp.${process.pid}.${node_crypto.default.randomBytes(4).toString("hex")}`);
|
|
1829
2845
|
try {
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
|
|
2846
|
+
const label = `download ${pkg.ossKey}`;
|
|
2847
|
+
const res = await fetchWithDiag(url, {
|
|
2848
|
+
label,
|
|
2849
|
+
timeoutMs: 6e4
|
|
2850
|
+
});
|
|
2851
|
+
if (!res.ok) {
|
|
2852
|
+
let preview = "";
|
|
2853
|
+
try {
|
|
2854
|
+
preview = (await res.text()).slice(0, 256);
|
|
2855
|
+
} catch {}
|
|
2856
|
+
throw new Error(`${label}: HTTP ${res.status} ${res.statusText}` + (preview ? ` body=${preview}` : ""));
|
|
2857
|
+
}
|
|
2858
|
+
if (!res.body) throw new Error(`${label}: empty body`);
|
|
1833
2859
|
const hasher = node_crypto.default.createHash("sha512");
|
|
1834
2860
|
const source = node_stream.Readable.fromWeb(res.body);
|
|
1835
2861
|
async function* teeAndHash(src) {
|
|
@@ -2354,16 +3380,6 @@ function writeSecretsAndRestart(vars, resetData, configDir, log) {
|
|
|
2354
3380
|
log(`restart.sh done in ${Date.now() - t}ms`);
|
|
2355
3381
|
} else log(`no restart.sh at ${restartScript}, skip`);
|
|
2356
3382
|
}
|
|
2357
|
-
/**
|
|
2358
|
-
* Run the 9-step reset process. Called from the worker entry point.
|
|
2359
|
-
*
|
|
2360
|
-
* Each step is an independent function. The orchestrator handles progress
|
|
2361
|
-
* reporting, error handling, and process-level exception guards.
|
|
2362
|
-
*
|
|
2363
|
-
* Template assets (openclaw.json + scripts/) are downloaded from OSS into a
|
|
2364
|
-
* scratch dir via `stageTemplate` before step 1 — there is no bundled
|
|
2365
|
-
* `template/` directory at runtime any more.
|
|
2366
|
-
*/
|
|
2367
3383
|
async function runReset(input, taskId, resultFile) {
|
|
2368
3384
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2369
3385
|
const { configPath, vars, resetData } = input;
|
|
@@ -2371,6 +3387,7 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2371
3387
|
const stagedDir = node_path.default.join(DIAGNOSE_DIR, `reset-${taskId}-template`);
|
|
2372
3388
|
let currentStep = 0;
|
|
2373
3389
|
let stepStartedAt = Date.now();
|
|
3390
|
+
const stepResults = [];
|
|
2374
3391
|
const log = makeLogger(resetLogFile(taskId));
|
|
2375
3392
|
log(`=== reset started, taskId=${taskId}, pid=${process.pid} ===`);
|
|
2376
3393
|
log(`configPath=${configPath}, configDir=${configDir}, stagedDir=${stagedDir}`);
|
|
@@ -2379,7 +3396,12 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2379
3396
|
const err = "resetData.ossFileMap missing or empty";
|
|
2380
3397
|
log(`ERROR: ${err}`);
|
|
2381
3398
|
markFailed(resultFile, 0, err, startedAt);
|
|
2382
|
-
|
|
3399
|
+
return {
|
|
3400
|
+
success: false,
|
|
3401
|
+
steps: stepResults,
|
|
3402
|
+
error: err,
|
|
3403
|
+
failedStep: 0
|
|
3404
|
+
};
|
|
2383
3405
|
}
|
|
2384
3406
|
let openclawTag;
|
|
2385
3407
|
if (resetData.openclawTag) openclawTag = resetData.openclawTag;
|
|
@@ -2389,7 +3411,12 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2389
3411
|
const err = e.message;
|
|
2390
3412
|
log(`ERROR: ${err}`);
|
|
2391
3413
|
markFailed(resultFile, 0, err, startedAt);
|
|
2392
|
-
|
|
3414
|
+
return {
|
|
3415
|
+
success: false,
|
|
3416
|
+
steps: stepResults,
|
|
3417
|
+
error: err,
|
|
3418
|
+
failedStep: 0
|
|
3419
|
+
};
|
|
2393
3420
|
}
|
|
2394
3421
|
log(`openclawTag=${openclawTag}`);
|
|
2395
3422
|
process.on("uncaughtException", (err) => {
|
|
@@ -2402,9 +3429,19 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2402
3429
|
markFailed(resultFile, currentStep, `unhandled rejection: ${reason}`, startedAt);
|
|
2403
3430
|
process.exit(1);
|
|
2404
3431
|
});
|
|
2405
|
-
/** Advance to the next step,
|
|
3432
|
+
/** Advance to the next step, recording the previous step's success
|
|
3433
|
+
* duration and updating the on-disk progress file. */
|
|
2406
3434
|
const step = (n) => {
|
|
2407
|
-
if (currentStep > 0)
|
|
3435
|
+
if (currentStep > 0) {
|
|
3436
|
+
const dur = Date.now() - stepStartedAt;
|
|
3437
|
+
log(`step ${currentStep} "${STEPS[currentStep - 1]}" done in ${dur}ms`);
|
|
3438
|
+
stepResults.push({
|
|
3439
|
+
step: currentStep,
|
|
3440
|
+
name: STEPS[currentStep - 1],
|
|
3441
|
+
status: "ok",
|
|
3442
|
+
durationMs: dur
|
|
3443
|
+
});
|
|
3444
|
+
}
|
|
2408
3445
|
currentStep = n;
|
|
2409
3446
|
stepStartedAt = Date.now();
|
|
2410
3447
|
log(`--- step ${n}/${TOTAL_STEPS}: ${STEPS[n - 1]} ---`);
|
|
@@ -2430,14 +3467,38 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2430
3467
|
await step8InstallExtensions(openclawTag, ossFileMap, log);
|
|
2431
3468
|
step(9);
|
|
2432
3469
|
writeSecretsAndRestart(vars, resetData, configDir, log);
|
|
2433
|
-
|
|
3470
|
+
const lastDur = Date.now() - stepStartedAt;
|
|
3471
|
+
log(`step 9 "${STEPS[8]}" done in ${lastDur}ms`);
|
|
3472
|
+
stepResults.push({
|
|
3473
|
+
step: 9,
|
|
3474
|
+
name: STEPS[8],
|
|
3475
|
+
status: "ok",
|
|
3476
|
+
durationMs: lastDur
|
|
3477
|
+
});
|
|
2434
3478
|
log("=== reset completed successfully ===");
|
|
2435
3479
|
markDone(resultFile, startedAt);
|
|
3480
|
+
return {
|
|
3481
|
+
success: true,
|
|
3482
|
+
steps: stepResults
|
|
3483
|
+
};
|
|
2436
3484
|
} catch (e) {
|
|
2437
3485
|
const err = e.message;
|
|
2438
|
-
|
|
3486
|
+
const dur = Date.now() - stepStartedAt;
|
|
3487
|
+
log(`ERROR in step ${currentStep} "${STEPS[currentStep - 1] ?? "init"}" after ${dur}ms: ${err}\n${e.stack ?? ""}`);
|
|
3488
|
+
stepResults.push({
|
|
3489
|
+
step: currentStep,
|
|
3490
|
+
name: STEPS[currentStep - 1] ?? "init",
|
|
3491
|
+
status: "fail",
|
|
3492
|
+
durationMs: dur,
|
|
3493
|
+
error: err
|
|
3494
|
+
});
|
|
2439
3495
|
markFailed(resultFile, currentStep, err, startedAt);
|
|
2440
|
-
|
|
3496
|
+
return {
|
|
3497
|
+
success: false,
|
|
3498
|
+
steps: stepResults,
|
|
3499
|
+
error: err,
|
|
3500
|
+
failedStep: currentStep
|
|
3501
|
+
};
|
|
2441
3502
|
} finally {
|
|
2442
3503
|
try {
|
|
2443
3504
|
node_fs.default.rmSync(stagedDir, {
|
|
@@ -2504,38 +3565,24 @@ function resolveOssFileMap(args) {
|
|
|
2504
3565
|
throw new Error("ossFileMap missing: provide --oss_file_map flag, ctx.install.ossFileMap, or resetData.ossFileMap");
|
|
2505
3566
|
}
|
|
2506
3567
|
//#endregion
|
|
2507
|
-
//#region src/innerapi/
|
|
3568
|
+
//#region src/innerapi/client.ts
|
|
2508
3569
|
/**
|
|
2509
|
-
*
|
|
2510
|
-
*
|
|
2511
|
-
* Mirrors the proven pattern in
|
|
2512
|
-
* `packages/openclaw/extensions/miaoda/src/shared/innerapi-client.ts`:
|
|
3570
|
+
* Shared innerapi HTTP client + path constants for `openclaw.*` calls.
|
|
2513
3571
|
*
|
|
2514
3572
|
* - `baseURL` from env `FORCE_AUTHN_INNERAPI_DOMAIN` (injected into every
|
|
2515
3573
|
* openclaw sandbox).
|
|
2516
3574
|
* - `platform: { enabled, tokenProvider: { type: 'file' } }` — the platform
|
|
2517
3575
|
* plugin auto-attaches the sandbox's identity JWT loaded from the
|
|
2518
3576
|
* rootfs token file. Same auth that the miaoda extension already uses.
|
|
2519
|
-
* - POST `/api/v1/studio/innerapi/integration_apis/call`
|
|
2520
|
-
*
|
|
2521
|
-
*
|
|
2522
|
-
* `
|
|
2523
|
-
*
|
|
2524
|
-
* `status_code` is a *string* ('0' = success).
|
|
2525
|
-
* Actual DoctorCtx lives in `data.output`.
|
|
2526
|
-
* - `x-tt-logid` header is logged on every failure path for cross-service
|
|
2527
|
-
* traceability.
|
|
2528
|
-
*
|
|
2529
|
-
* On HTTP 401 (sandbox identity token expired/invalid) we `process.exit(77)`
|
|
2530
|
-
* instead of throwing — the outer catch in `index.ts` cannot then mask auth
|
|
2531
|
-
* failure as a generic "Error: ...". Caller (e.g. sandbox_console) sees the
|
|
2532
|
-
* exit code and can refresh the token + retry.
|
|
3577
|
+
* - POST `/api/v1/studio/innerapi/integration_apis/call` with body
|
|
3578
|
+
* `{ apiName, bizType, input }` is the universal dispatcher; specific
|
|
3579
|
+
* apiName values (e.g. `openclaw.get_doctor_ctx`,
|
|
3580
|
+
* `openclaw.report_doctor_result`) are owned by individual call-site
|
|
3581
|
+
* modules.
|
|
2533
3582
|
*/
|
|
2534
3583
|
const INNERAPI_CALL_PATH = "/api/v1/studio/innerapi/integration_apis/call";
|
|
2535
|
-
const API_NAME = "openclaw.get_doctor_ctx";
|
|
2536
3584
|
const BIZ_TYPE = "openclaw";
|
|
2537
3585
|
const API_TIMEOUT_MS = 3e4;
|
|
2538
|
-
const MAX_LOG_BODY = 500;
|
|
2539
3586
|
let clientInstance = null;
|
|
2540
3587
|
function getHttpClient() {
|
|
2541
3588
|
if (!clientInstance) {
|
|
@@ -2552,6 +3599,35 @@ function getHttpClient() {
|
|
|
2552
3599
|
}
|
|
2553
3600
|
return clientInstance;
|
|
2554
3601
|
}
|
|
3602
|
+
//#endregion
|
|
3603
|
+
//#region src/innerapi/fetchCtx.ts
|
|
3604
|
+
/**
|
|
3605
|
+
* CLI-side client for studio_server's `openclaw.get_doctor_ctx` inner API.
|
|
3606
|
+
*
|
|
3607
|
+
* Mirrors the proven pattern in
|
|
3608
|
+
* `packages/openclaw/extensions/miaoda/src/shared/innerapi-client.ts`:
|
|
3609
|
+
*
|
|
3610
|
+
* - `baseURL` from env `FORCE_AUTHN_INNERAPI_DOMAIN` (injected into every
|
|
3611
|
+
* openclaw sandbox).
|
|
3612
|
+
* - `platform: { enabled, tokenProvider: { type: 'file' } }` — the platform
|
|
3613
|
+
* plugin auto-attaches the sandbox's identity JWT loaded from the
|
|
3614
|
+
* rootfs token file. Same auth that the miaoda extension already uses.
|
|
3615
|
+
* - POST `/api/v1/studio/innerapi/integration_apis/call`
|
|
3616
|
+
* body = { apiName: 'openclaw.get_doctor_ctx', input: {}, bizType: 'openclaw' }
|
|
3617
|
+
* — the server-side APICall dispatches by `apiName` to
|
|
3618
|
+
* `GetDoctorCtxAPICall.Execute` whose `Name()` returns that string.
|
|
3619
|
+
* - Response envelope: { status_code, error_msg?, data: { success, output, ... } }.
|
|
3620
|
+
* `status_code` is a *string* ('0' = success).
|
|
3621
|
+
* Actual DoctorCtx lives in `data.output`.
|
|
3622
|
+
* - `x-tt-logid` header is logged on every failure path for cross-service
|
|
3623
|
+
* traceability.
|
|
3624
|
+
*
|
|
3625
|
+
* On HTTP 401 (sandbox identity token expired/invalid) we `process.exit(77)`
|
|
3626
|
+
* instead of throwing — the outer catch in `index.ts` cannot then mask auth
|
|
3627
|
+
* failure as a generic "Error: ...". Caller (e.g. sandbox_console) sees the
|
|
3628
|
+
* exit code and can refresh the token + retry.
|
|
3629
|
+
*/
|
|
3630
|
+
const API_NAME$1 = "openclaw.get_doctor_ctx";
|
|
2555
3631
|
/**
|
|
2556
3632
|
* Fetch the sandbox's DoctorCtx by calling the innerapi's generic
|
|
2557
3633
|
* `integration_apis/call` dispatcher with apiName=openclaw.get_doctor_ctx.
|
|
@@ -2559,14 +3635,19 @@ function getHttpClient() {
|
|
|
2559
3635
|
* Throws on HTTP (non-401) / decode / business errors. On 401 calls
|
|
2560
3636
|
* `process.exit(77)` directly.
|
|
2561
3637
|
*/
|
|
2562
|
-
async function fetchCtxViaInnerApi() {
|
|
3638
|
+
async function fetchCtxViaInnerApi(opts = {}) {
|
|
2563
3639
|
const client = getHttpClient();
|
|
3640
|
+
const input = {};
|
|
3641
|
+
if (opts.populate !== void 0) input.populate = opts.populate;
|
|
3642
|
+
if (opts.caller) input.caller = opts.caller;
|
|
3643
|
+
if (opts.traceId) input.traceId = opts.traceId;
|
|
2564
3644
|
const body = {
|
|
2565
|
-
apiName: API_NAME,
|
|
2566
|
-
input
|
|
3645
|
+
apiName: API_NAME$1,
|
|
3646
|
+
input,
|
|
2567
3647
|
bizType: BIZ_TYPE
|
|
2568
3648
|
};
|
|
2569
3649
|
const start = Date.now();
|
|
3650
|
+
console.error(`fetchCtx: start populate=${JSON.stringify(opts.populate ?? {})}${opts.caller ? ` caller=${opts.caller}` : ""}${opts.traceId ? ` traceId=${opts.traceId}` : ""}`);
|
|
2570
3651
|
const headers = { "Content-Type": "application/json" };
|
|
2571
3652
|
const ttEnv = process.env.X_TT_ENV;
|
|
2572
3653
|
if (ttEnv) headers["x-tt-env"] = ttEnv;
|
|
@@ -2596,7 +3677,7 @@ async function fetchCtxViaInnerApi() {
|
|
|
2596
3677
|
}
|
|
2597
3678
|
let preview = "";
|
|
2598
3679
|
try {
|
|
2599
|
-
preview = (await response.text()).slice(0,
|
|
3680
|
+
preview = (await response.text()).slice(0, 500);
|
|
2600
3681
|
} catch {}
|
|
2601
3682
|
throw new Error(`fetchCtxViaInnerApi HTTP ${response.status} ${response.statusText} (logID: ${logId}, durationMs: ${durationMs})${preview ? ` body=${preview}` : ""}`);
|
|
2602
3683
|
}
|
|
@@ -2610,6 +3691,7 @@ async function fetchCtxViaInnerApi() {
|
|
|
2610
3691
|
if (envelope.data && envelope.data.success === false) throw new Error(`fetchCtxViaInnerApi business error (logID: ${logId}, durationMs: ${durationMs}): ${envelope.error_msg ?? JSON.stringify(envelope.data)}`);
|
|
2611
3692
|
const output = envelope.data?.output;
|
|
2612
3693
|
if (!output || typeof output !== "object") throw new Error(`fetchCtxViaInnerApi empty/invalid output (logID: ${logId}, durationMs: ${durationMs})`);
|
|
3694
|
+
console.error(`fetchCtx: ok logId=${logId} durationMs=${durationMs} groups=[${Object.keys(output).join(",")}]`);
|
|
2613
3695
|
return output;
|
|
2614
3696
|
}
|
|
2615
3697
|
//#endregion
|
|
@@ -2628,26 +3710,40 @@ async function fetchCtxViaInnerApi() {
|
|
|
2628
3710
|
*/
|
|
2629
3711
|
function normalizeCtx(raw) {
|
|
2630
3712
|
const r = raw ?? {};
|
|
2631
|
-
if (r.app && typeof r.app === "object" && r.install && typeof r.install === "object" && r.secrets && typeof r.secrets === "object" && r.reset && typeof r.reset === "object")
|
|
2632
|
-
app:
|
|
2633
|
-
install:
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
3713
|
+
if (r.app && typeof r.app === "object" && r.install && typeof r.install === "object" && r.secrets && typeof r.secrets === "object" && r.reset && typeof r.reset === "object") {
|
|
3714
|
+
const templateVars = r.app.templateVars && typeof r.app.templateVars === "object" ? r.app.templateVars : r.reset.templateVars ?? {};
|
|
3715
|
+
const recommendedOpenclawTag = typeof r.app.recommendedOpenclawTag === "string" && r.app.recommendedOpenclawTag !== "" ? r.app.recommendedOpenclawTag : typeof r.install.openclawTag === "string" && r.install.openclawTag !== "" ? r.install.openclawTag : void 0;
|
|
3716
|
+
return {
|
|
3717
|
+
app: {
|
|
3718
|
+
...fillApp(r.app),
|
|
3719
|
+
templateVars,
|
|
3720
|
+
recommendedOpenclawTag
|
|
3721
|
+
},
|
|
3722
|
+
install: {
|
|
3723
|
+
openclawTag: r.install.openclawTag,
|
|
3724
|
+
ossFileMap: r.install.ossFileMap ?? {}
|
|
3725
|
+
},
|
|
3726
|
+
secrets: {
|
|
3727
|
+
secretsContent: r.secrets.secretsContent ?? "",
|
|
3728
|
+
providerKeyContent: r.secrets.providerKeyContent ?? ""
|
|
3729
|
+
},
|
|
3730
|
+
reset: {
|
|
3731
|
+
templateVars: r.reset.templateVars ?? {},
|
|
3732
|
+
coreBackup: r.reset.coreBackup
|
|
3733
|
+
}
|
|
3734
|
+
};
|
|
3735
|
+
}
|
|
2646
3736
|
const vars = r.vars ?? {};
|
|
2647
3737
|
const resetData = r.resetData ?? {};
|
|
2648
3738
|
const repairData = r.repairData ?? {};
|
|
3739
|
+
const legacyTemplateVars = vars.templateVars && typeof vars.templateVars === "object" ? vars.templateVars : resetData.templateVars ?? r.templateVars ?? {};
|
|
3740
|
+
const legacyRecommendedTag = typeof vars.recommendedOpenclawTag === "string" && vars.recommendedOpenclawTag !== "" ? vars.recommendedOpenclawTag : typeof resetData.openclawTag === "string" && resetData.openclawTag !== "" ? resetData.openclawTag : typeof r.openclawTag === "string" && r.openclawTag !== "" ? r.openclawTag : void 0;
|
|
2649
3741
|
return {
|
|
2650
|
-
app:
|
|
3742
|
+
app: {
|
|
3743
|
+
...fillApp(vars),
|
|
3744
|
+
templateVars: legacyTemplateVars,
|
|
3745
|
+
recommendedOpenclawTag: legacyRecommendedTag
|
|
3746
|
+
},
|
|
2651
3747
|
install: {
|
|
2652
3748
|
openclawTag: r.install?.openclawTag ?? r.openclawTag,
|
|
2653
3749
|
ossFileMap: r.install?.ossFileMap ?? resetData.ossFileMap ?? r.ossFileMap ?? {}
|
|
@@ -2700,11 +3796,15 @@ function fillApp(src) {
|
|
|
2700
3796
|
function buildCheckInput(raw, configPathOverride) {
|
|
2701
3797
|
const r = raw ?? {};
|
|
2702
3798
|
if (r.configPath && r.vars) {
|
|
2703
|
-
|
|
3799
|
+
const out = configPathOverride ? {
|
|
2704
3800
|
...r,
|
|
2705
3801
|
configPath: configPathOverride
|
|
3802
|
+
} : { ...r };
|
|
3803
|
+
if (out.vars && !out.vars.templateVars) out.vars = {
|
|
3804
|
+
...out.vars,
|
|
3805
|
+
templateVars: out.templateVars ?? {}
|
|
2706
3806
|
};
|
|
2707
|
-
return
|
|
3807
|
+
return out;
|
|
2708
3808
|
}
|
|
2709
3809
|
const ctx = normalizeCtx(raw);
|
|
2710
3810
|
return {
|
|
@@ -2718,19 +3818,25 @@ function buildCheckInput(raw, configPathOverride) {
|
|
|
2718
3818
|
baseURL: ctx.app.baseURL,
|
|
2719
3819
|
expectedOrigins: ctx.app.expectedOrigins,
|
|
2720
3820
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
2721
|
-
secretsFilePath: SECRETS_FILE_PATH
|
|
3821
|
+
secretsFilePath: SECRETS_FILE_PATH,
|
|
3822
|
+
templateVars: ctx.app.templateVars,
|
|
3823
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
2722
3824
|
},
|
|
2723
|
-
templateVars: ctx.
|
|
3825
|
+
templateVars: ctx.app.templateVars
|
|
2724
3826
|
};
|
|
2725
3827
|
}
|
|
2726
3828
|
function buildRepairInput(raw, configPathOverride) {
|
|
2727
3829
|
const r = raw ?? {};
|
|
2728
3830
|
if (r.configPath && r.vars) {
|
|
2729
|
-
|
|
3831
|
+
const out = configPathOverride ? {
|
|
2730
3832
|
...r,
|
|
2731
3833
|
configPath: configPathOverride
|
|
3834
|
+
} : { ...r };
|
|
3835
|
+
if (out.vars && !out.vars.templateVars) out.vars = {
|
|
3836
|
+
...out.vars,
|
|
3837
|
+
templateVars: out.templateVars ?? {}
|
|
2732
3838
|
};
|
|
2733
|
-
return
|
|
3839
|
+
return out;
|
|
2734
3840
|
}
|
|
2735
3841
|
const ctx = normalizeCtx(raw);
|
|
2736
3842
|
return {
|
|
@@ -2744,23 +3850,29 @@ function buildRepairInput(raw, configPathOverride) {
|
|
|
2744
3850
|
baseURL: ctx.app.baseURL,
|
|
2745
3851
|
expectedOrigins: ctx.app.expectedOrigins,
|
|
2746
3852
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
2747
|
-
secretsFilePath: SECRETS_FILE_PATH
|
|
3853
|
+
secretsFilePath: SECRETS_FILE_PATH,
|
|
3854
|
+
templateVars: ctx.app.templateVars,
|
|
3855
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
2748
3856
|
},
|
|
2749
3857
|
repairData: {
|
|
2750
3858
|
secretsContent: ctx.secrets.secretsContent,
|
|
2751
3859
|
providerKeyContent: ctx.secrets.providerKeyContent
|
|
2752
3860
|
},
|
|
2753
|
-
templateVars: ctx.
|
|
3861
|
+
templateVars: ctx.app.templateVars
|
|
2754
3862
|
};
|
|
2755
3863
|
}
|
|
2756
3864
|
function buildResetInput(raw, configPathOverride) {
|
|
2757
3865
|
const r = raw ?? {};
|
|
2758
3866
|
if (r.configPath && r.vars && r.resetData) {
|
|
2759
|
-
|
|
3867
|
+
const out = configPathOverride ? {
|
|
2760
3868
|
...r,
|
|
2761
3869
|
configPath: configPathOverride
|
|
3870
|
+
} : { ...r };
|
|
3871
|
+
if (out.vars && !out.vars.templateVars) out.vars = {
|
|
3872
|
+
...out.vars,
|
|
3873
|
+
templateVars: out.resetData?.templateVars ?? {}
|
|
2762
3874
|
};
|
|
2763
|
-
return
|
|
3875
|
+
return out;
|
|
2764
3876
|
}
|
|
2765
3877
|
const ctx = normalizeCtx(raw);
|
|
2766
3878
|
return {
|
|
@@ -2774,7 +3886,9 @@ function buildResetInput(raw, configPathOverride) {
|
|
|
2774
3886
|
baseURL: ctx.app.baseURL,
|
|
2775
3887
|
expectedOrigins: ctx.app.expectedOrigins,
|
|
2776
3888
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
2777
|
-
secretsFilePath: SECRETS_FILE_PATH
|
|
3889
|
+
secretsFilePath: SECRETS_FILE_PATH,
|
|
3890
|
+
templateVars: ctx.app.templateVars,
|
|
3891
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
2778
3892
|
},
|
|
2779
3893
|
resetData: {
|
|
2780
3894
|
templateVars: ctx.reset.templateVars,
|
|
@@ -2788,30 +3902,381 @@ function buildResetInput(raw, configPathOverride) {
|
|
|
2788
3902
|
}
|
|
2789
3903
|
//#endregion
|
|
2790
3904
|
//#region src/doctor.ts
|
|
3905
|
+
/**
|
|
3906
|
+
* Per-rule orchestration with telemetry-friendly outcomes.
|
|
3907
|
+
*
|
|
3908
|
+
* Flow per rule (in topological order, after disabledRules / dependsOn /
|
|
3909
|
+
* skipWhen filtering):
|
|
3910
|
+
*
|
|
3911
|
+
* 1. validate — exception → status='error', abort
|
|
3912
|
+
* 2. pass → status='pass'
|
|
3913
|
+
* 3. fail and !opts.fix → status='failed' (with v1.message)
|
|
3914
|
+
* 4. fail and opts.fix →
|
|
3915
|
+
* a. only standard-mode rules attempt repair; ai/reset stay 'failed'
|
|
3916
|
+
* b. repair — exception → status='error', abort
|
|
3917
|
+
* c. re-validate — exception → status='error', abort
|
|
3918
|
+
* d. v2.pass → status='fixed' (before = v1.message)
|
|
3919
|
+
* e. v2.fail → status='still-broken' (before+after)
|
|
3920
|
+
*
|
|
3921
|
+
* Aborted runs return a partial DoctorReport with `aborted: true`. The
|
|
3922
|
+
* caller should `console.log(JSON.stringify(report))` first, THEN
|
|
3923
|
+
* `process.exit(1)` so sweep / telemetry callers always get the partial
|
|
3924
|
+
* data plus the non-zero signal.
|
|
3925
|
+
*
|
|
3926
|
+
* Config writeback: if opts.fix and at least one rule transitioned
|
|
3927
|
+
* pass→fixed, the in-memory ctx.config is JSON-serialized and written
|
|
3928
|
+
* back to opts.configPath. On abort, no write — keep the on-disk file
|
|
3929
|
+
* matching whatever was successful before the failure.
|
|
3930
|
+
*
|
|
3931
|
+
* Secrets file / provider-key / restart-command writes are NOT performed
|
|
3932
|
+
* by doctor. Those belong to the standalone `repair` subcommand which
|
|
3933
|
+
* sandbox_console push uses. Doctor is for diagnosis + per-rule fix
|
|
3934
|
+
* with structured outcomes.
|
|
3935
|
+
*/
|
|
2791
3936
|
async function runDoctor(rawCtx, opts) {
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
3937
|
+
const results = [];
|
|
3938
|
+
console.error(`runDoctor: begin fix=${opts.fix} rules=[${opts.rules.join(",")}] configPath=${opts.configPath ?? "(default)"}`);
|
|
3939
|
+
const checkInput = buildCheckInput(rawCtx, opts.configPath);
|
|
3940
|
+
let configRaw;
|
|
3941
|
+
let config;
|
|
3942
|
+
try {
|
|
3943
|
+
configRaw = readFile(checkInput.configPath);
|
|
3944
|
+
} catch (e) {
|
|
3945
|
+
console.error(`runDoctor: read config failed at ${checkInput.configPath} message=${e.message}`);
|
|
3946
|
+
return finalize([{
|
|
3947
|
+
rule: "config_syntax_check",
|
|
3948
|
+
status: "error",
|
|
3949
|
+
message: "read config failed: " + e.message
|
|
3950
|
+
}], true);
|
|
3951
|
+
}
|
|
3952
|
+
try {
|
|
3953
|
+
config = loadJSON5().parse(configRaw);
|
|
3954
|
+
} catch (e) {
|
|
3955
|
+
console.error(`runDoctor: config JSON5 parse failed message=${e.message}`);
|
|
3956
|
+
results.push({
|
|
3957
|
+
rule: "config_syntax_check",
|
|
3958
|
+
status: opts.fix ? "still-broken" : "failed",
|
|
3959
|
+
message: "config JSON5 parse failed: " + e.message
|
|
3960
|
+
});
|
|
3961
|
+
return finalize(results, true);
|
|
3962
|
+
}
|
|
3963
|
+
const ctx = {
|
|
3964
|
+
config,
|
|
3965
|
+
configPath: checkInput.configPath,
|
|
3966
|
+
vars: checkInput.vars,
|
|
3967
|
+
providerDeps: analyzeProviderDeps(config)
|
|
3968
|
+
};
|
|
3969
|
+
const originalConfig = opts.showDiff && opts.fix ? deepClone(config) : null;
|
|
3970
|
+
const allRules = getAllRules();
|
|
3971
|
+
const onlyKeys = opts.rules.length > 0 ? new Set(opts.rules) : null;
|
|
3972
|
+
const skipRuleKeys = new Set(opts.skipRules ?? []);
|
|
3973
|
+
const disabledKeys = new Set([...checkInput.disabledRules ?? [], ...skipRuleKeys]);
|
|
3974
|
+
if (onlyKeys) {
|
|
3975
|
+
const registeredKeys = new Set(allRules.map((r) => r.meta.key));
|
|
3976
|
+
for (const k of opts.rules) if (!registeredKeys.has(k)) results.push({
|
|
3977
|
+
rule: k,
|
|
3978
|
+
status: "unknown"
|
|
3979
|
+
});
|
|
3980
|
+
}
|
|
3981
|
+
const failedKeys = /* @__PURE__ */ new Set();
|
|
3982
|
+
let configDirty = false;
|
|
3983
|
+
let aborted = false;
|
|
3984
|
+
const hasMiaoda = shouldCheckMiaodaRules(config);
|
|
3985
|
+
const plannedRules = allRules.filter((r) => !onlyKeys || onlyKeys.has(r.meta.key));
|
|
3986
|
+
console.error(`runDoctor: planned ${plannedRules.length} rules hasMiaoda=${hasMiaoda} providerDeps=${JSON.stringify(ctx.providerDeps)}`);
|
|
3987
|
+
outer: for (const rule of allRules) {
|
|
3988
|
+
const key = rule.meta.key;
|
|
3989
|
+
if (onlyKeys && !onlyKeys.has(key)) continue;
|
|
3990
|
+
if (disabledKeys.has(key)) {
|
|
3991
|
+
const reason = skipRuleKeys.has(key) ? "--skip-rule" : "disabledRules";
|
|
3992
|
+
console.error(`rule ${key}: skipped reason=${reason}`);
|
|
3993
|
+
results.push({
|
|
3994
|
+
rule: key,
|
|
3995
|
+
status: "skipped",
|
|
3996
|
+
message: reason
|
|
3997
|
+
});
|
|
3998
|
+
continue;
|
|
3999
|
+
}
|
|
4000
|
+
if (rule.meta.dependsOn?.some((d) => failedKeys.has(d))) {
|
|
4001
|
+
const blockers = rule.meta.dependsOn?.filter((d) => failedKeys.has(d)).join(",");
|
|
4002
|
+
console.error(`rule ${key}: skipped reason=dependsOn-failed blockers=${blockers}`);
|
|
4003
|
+
results.push({
|
|
4004
|
+
rule: key,
|
|
4005
|
+
status: "skipped",
|
|
4006
|
+
message: `dependsOn failed: ${blockers}`
|
|
4007
|
+
});
|
|
4008
|
+
continue;
|
|
4009
|
+
}
|
|
4010
|
+
if (rule.meta.skipWhen?.({
|
|
4011
|
+
hasMiaoda,
|
|
4012
|
+
deps: ctx.providerDeps
|
|
4013
|
+
})) {
|
|
4014
|
+
console.error(`rule ${key}: skipped reason=skipWhen`);
|
|
4015
|
+
results.push({
|
|
4016
|
+
rule: key,
|
|
4017
|
+
status: "skipped",
|
|
4018
|
+
message: "skipWhen"
|
|
4019
|
+
});
|
|
4020
|
+
continue;
|
|
4021
|
+
}
|
|
4022
|
+
const tValidate = Date.now();
|
|
4023
|
+
let v1;
|
|
4024
|
+
try {
|
|
4025
|
+
v1 = rule.validate(ctx);
|
|
4026
|
+
} catch (e) {
|
|
4027
|
+
const msg = e.message;
|
|
4028
|
+
console.error(`rule ${key}: validate -> error durationMs=${Date.now() - tValidate} message=${msg}`);
|
|
4029
|
+
results.push({
|
|
4030
|
+
rule: key,
|
|
4031
|
+
status: "error",
|
|
4032
|
+
message: "validate threw: " + msg
|
|
4033
|
+
});
|
|
4034
|
+
aborted = true;
|
|
4035
|
+
break outer;
|
|
4036
|
+
}
|
|
4037
|
+
if (v1.pass) {
|
|
4038
|
+
console.error(`rule ${key}: validate -> pass durationMs=${Date.now() - tValidate}`);
|
|
4039
|
+
results.push({
|
|
4040
|
+
rule: key,
|
|
4041
|
+
status: "pass"
|
|
4042
|
+
});
|
|
4043
|
+
continue;
|
|
4044
|
+
}
|
|
4045
|
+
if (!opts.fix) {
|
|
4046
|
+
console.error(`rule ${key}: validate -> failed durationMs=${Date.now() - tValidate} message=${v1.message ?? ""}`);
|
|
4047
|
+
results.push({
|
|
4048
|
+
rule: key,
|
|
4049
|
+
status: "failed",
|
|
4050
|
+
message: v1.message,
|
|
4051
|
+
action: v1.action
|
|
4052
|
+
});
|
|
4053
|
+
failedKeys.add(key);
|
|
4054
|
+
continue;
|
|
4055
|
+
}
|
|
4056
|
+
if (rule.meta.repairMode !== "standard") {
|
|
4057
|
+
console.error(`rule ${key}: validate -> failed (no auto-repair, repairMode=${rule.meta.repairMode}) durationMs=${Date.now() - tValidate} message=${v1.message ?? ""}`);
|
|
4058
|
+
results.push({
|
|
4059
|
+
rule: key,
|
|
4060
|
+
status: "failed",
|
|
4061
|
+
message: v1.message,
|
|
4062
|
+
action: v1.action
|
|
4063
|
+
});
|
|
4064
|
+
failedKeys.add(key);
|
|
4065
|
+
continue;
|
|
4066
|
+
}
|
|
4067
|
+
console.error(`rule ${key}: validate -> failed durationMs=${Date.now() - tValidate} message=${v1.message ?? ""}`);
|
|
4068
|
+
const tRepair = Date.now();
|
|
4069
|
+
try {
|
|
4070
|
+
rule.repair(ctx);
|
|
4071
|
+
} catch (e) {
|
|
4072
|
+
const msg = e.message;
|
|
4073
|
+
console.error(`rule ${key}: repair -> error durationMs=${Date.now() - tRepair} message=${msg}`);
|
|
4074
|
+
results.push({
|
|
4075
|
+
rule: key,
|
|
4076
|
+
status: "error",
|
|
4077
|
+
message: "repair threw: " + msg,
|
|
4078
|
+
before: v1.message
|
|
4079
|
+
});
|
|
4080
|
+
aborted = true;
|
|
4081
|
+
break outer;
|
|
4082
|
+
}
|
|
4083
|
+
console.error(`rule ${key}: repair -> ok durationMs=${Date.now() - tRepair}`);
|
|
4084
|
+
configDirty = true;
|
|
4085
|
+
const tRevalidate = Date.now();
|
|
4086
|
+
let v2;
|
|
4087
|
+
try {
|
|
4088
|
+
v2 = rule.validate(ctx);
|
|
4089
|
+
} catch (e) {
|
|
4090
|
+
const msg = e.message;
|
|
4091
|
+
console.error(`rule ${key}: revalidate -> error durationMs=${Date.now() - tRevalidate} message=${msg}`);
|
|
4092
|
+
results.push({
|
|
4093
|
+
rule: key,
|
|
4094
|
+
status: "error",
|
|
4095
|
+
message: "post-repair validate threw: " + msg,
|
|
4096
|
+
before: v1.message
|
|
4097
|
+
});
|
|
4098
|
+
aborted = true;
|
|
4099
|
+
break outer;
|
|
4100
|
+
}
|
|
4101
|
+
if (v2.pass) {
|
|
4102
|
+
console.error(`rule ${key}: revalidate -> pass (fixed) durationMs=${Date.now() - tRevalidate}`);
|
|
4103
|
+
results.push({
|
|
4104
|
+
rule: key,
|
|
4105
|
+
status: "fixed",
|
|
4106
|
+
before: v1.message
|
|
4107
|
+
});
|
|
4108
|
+
} else {
|
|
4109
|
+
console.error(`rule ${key}: revalidate -> still-broken durationMs=${Date.now() - tRevalidate} after=${v2.message ?? ""}`);
|
|
4110
|
+
results.push({
|
|
4111
|
+
rule: key,
|
|
4112
|
+
status: "still-broken",
|
|
4113
|
+
before: v1.message,
|
|
4114
|
+
after: v2.message
|
|
4115
|
+
});
|
|
4116
|
+
failedKeys.add(key);
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
let backupPath = null;
|
|
4120
|
+
if (configDirty && !aborted) {
|
|
4121
|
+
backupPath = backupConfigSync(ctx.configPath);
|
|
4122
|
+
const serialized = JSON.stringify(ctx.config, null, 2);
|
|
4123
|
+
try {
|
|
4124
|
+
writeFile(ctx.configPath, serialized);
|
|
4125
|
+
console.error(`runDoctor: writeback ok path=${ctx.configPath} bytes=${serialized.length}`);
|
|
4126
|
+
} catch (e) {
|
|
4127
|
+
const msg = e.message;
|
|
4128
|
+
console.error(`runDoctor: writeback failed path=${ctx.configPath} message=${msg}`);
|
|
4129
|
+
results.push({
|
|
4130
|
+
rule: "*config-writeback*",
|
|
4131
|
+
status: "error",
|
|
4132
|
+
message: "config write failed: " + msg
|
|
4133
|
+
});
|
|
4134
|
+
aborted = true;
|
|
4135
|
+
}
|
|
4136
|
+
} else if (configDirty && aborted) console.error("runDoctor: writeback skipped (aborted, leaving on-disk config untouched)");
|
|
4137
|
+
else console.error("runDoctor: writeback skipped (no rule transitioned to fixed)");
|
|
4138
|
+
console.error(`runDoctor: end aborted=${aborted} results=${results.length}`);
|
|
4139
|
+
if (originalConfig !== null) {
|
|
4140
|
+
const d = (0, json_diff.diffString)(originalConfig, ctx.config, { color: false });
|
|
4141
|
+
console.error(`runDoctor: diff backupPath=${backupPath ?? "(none)"} changed=${!!d}`);
|
|
4142
|
+
if (d) {
|
|
4143
|
+
process.stdout.write(`original: ${backupPath ?? "(none)"}\n`);
|
|
4144
|
+
process.stdout.write(`after fixed: ${ctx.configPath}\n`);
|
|
4145
|
+
process.stdout.write("diff:\n");
|
|
4146
|
+
process.stdout.write(d);
|
|
4147
|
+
if (!d.endsWith("\n")) process.stdout.write("\n");
|
|
4148
|
+
} else process.stdout.write("(no openclaw.json changes)\n");
|
|
4149
|
+
}
|
|
4150
|
+
return finalize(results, aborted);
|
|
4151
|
+
}
|
|
4152
|
+
/** Deep-clone a JSON-shaped value. Uses structuredClone where available
|
|
4153
|
+
* (Node 17+); falls back to JSON round-trip otherwise. The config we
|
|
4154
|
+
* clone here is JSON5-parsed so it's strictly tree-shaped — no
|
|
4155
|
+
* references, no functions — making both paths equivalent. */
|
|
4156
|
+
function deepClone(v) {
|
|
4157
|
+
if (typeof structuredClone === "function") return structuredClone(v);
|
|
4158
|
+
return JSON.parse(JSON.stringify(v));
|
|
4159
|
+
}
|
|
4160
|
+
function finalize(results, aborted) {
|
|
4161
|
+
const summary = {
|
|
4162
|
+
pass: 0,
|
|
4163
|
+
failed: 0,
|
|
4164
|
+
fixed: 0,
|
|
4165
|
+
stillBroken: 0,
|
|
4166
|
+
skipped: 0,
|
|
4167
|
+
error: 0,
|
|
4168
|
+
unknown: 0
|
|
4169
|
+
};
|
|
4170
|
+
for (const r of results) switch (r.status) {
|
|
4171
|
+
case "pass":
|
|
4172
|
+
summary.pass++;
|
|
4173
|
+
break;
|
|
4174
|
+
case "failed":
|
|
4175
|
+
summary.failed++;
|
|
4176
|
+
break;
|
|
4177
|
+
case "fixed":
|
|
4178
|
+
summary.fixed++;
|
|
4179
|
+
break;
|
|
4180
|
+
case "still-broken":
|
|
4181
|
+
summary.stillBroken++;
|
|
4182
|
+
break;
|
|
4183
|
+
case "skipped":
|
|
4184
|
+
summary.skipped++;
|
|
4185
|
+
break;
|
|
4186
|
+
case "error":
|
|
4187
|
+
summary.error++;
|
|
4188
|
+
break;
|
|
4189
|
+
case "unknown":
|
|
4190
|
+
summary.unknown++;
|
|
4191
|
+
break;
|
|
4192
|
+
}
|
|
2805
4193
|
return {
|
|
2806
|
-
|
|
2807
|
-
|
|
4194
|
+
results,
|
|
4195
|
+
summary,
|
|
4196
|
+
aborted
|
|
4197
|
+
};
|
|
4198
|
+
}
|
|
4199
|
+
//#endregion
|
|
4200
|
+
//#region src/innerapi/reportCliRun.ts
|
|
4201
|
+
/**
|
|
4202
|
+
* CLI-side client for studio_server's `openclaw.report_cli_run` inner
|
|
4203
|
+
* API — the unified telemetry sink for every CLI command.
|
|
4204
|
+
*
|
|
4205
|
+
* Best-effort: gated by `DoctorCtx.telemetry.reportCliRun` returned by
|
|
4206
|
+
* `get_doctor_ctx`. When the server hasn't rolled out the API yet the
|
|
4207
|
+
* flag is absent and the CLI never invokes this. When enabled, the CLI
|
|
4208
|
+
* fires-and-forgets one report per command after work is done.
|
|
4209
|
+
*
|
|
4210
|
+
* Failures here MUST NOT change exit code or otherwise affect the
|
|
4211
|
+
* command's data path — telemetry is auxiliary. Caller is expected to
|
|
4212
|
+
* `try/catch` and just `console.error` on rejection.
|
|
4213
|
+
*
|
|
4214
|
+
* Replaces the earlier `openclaw.report_doctor_result` API. The schema
|
|
4215
|
+
* generalises so check / repair / install-* / download-resource / reset
|
|
4216
|
+
* (worker) can all use the same envelope; per-command shape lives under
|
|
4217
|
+
* `result`.
|
|
4218
|
+
*/
|
|
4219
|
+
const API_NAME = "openclaw.report_cli_run";
|
|
4220
|
+
async function reportCliRun(opts) {
|
|
4221
|
+
const client = getHttpClient();
|
|
4222
|
+
const input = {
|
|
4223
|
+
command: opts.command,
|
|
4224
|
+
runId: opts.runId,
|
|
4225
|
+
version: opts.version,
|
|
4226
|
+
invocation: opts.invocation,
|
|
4227
|
+
durationMs: opts.durationMs,
|
|
4228
|
+
success: opts.success
|
|
4229
|
+
};
|
|
4230
|
+
if (opts.caller) input.caller = opts.caller;
|
|
4231
|
+
if (opts.traceId) input.traceId = opts.traceId;
|
|
4232
|
+
if (opts.result !== void 0) input.result = opts.result;
|
|
4233
|
+
if (opts.error) input.error = opts.error;
|
|
4234
|
+
const body = {
|
|
4235
|
+
apiName: API_NAME,
|
|
4236
|
+
input,
|
|
4237
|
+
bizType: BIZ_TYPE
|
|
2808
4238
|
};
|
|
4239
|
+
const headers = { "Content-Type": "application/json" };
|
|
4240
|
+
const ttEnv = process.env.X_TT_ENV;
|
|
4241
|
+
if (ttEnv) headers["x-tt-env"] = ttEnv;
|
|
4242
|
+
const start = Date.now();
|
|
4243
|
+
console.error(`reportCliRun: start command=${opts.command} success=${opts.success} version=${opts.version} invocation=${JSON.stringify(opts.invocation)} durationMs=${opts.durationMs}`);
|
|
4244
|
+
let response;
|
|
4245
|
+
try {
|
|
4246
|
+
response = await client.post(INNERAPI_CALL_PATH, body, { headers });
|
|
4247
|
+
} catch (e) {
|
|
4248
|
+
if (e instanceof _lark_apaas_http_client.HttpError && e.response) {
|
|
4249
|
+
const status = e.response.status;
|
|
4250
|
+
const logId = e.response.headers.get("x-tt-logid") ?? "";
|
|
4251
|
+
throw new Error(`reportCliRun HTTP ${status} ${e.response.statusText} (logID: ${logId})`);
|
|
4252
|
+
}
|
|
4253
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4254
|
+
throw new Error(`reportCliRun network error: ${msg}`);
|
|
4255
|
+
}
|
|
4256
|
+
const logId = response.headers.get("x-tt-logid") ?? "";
|
|
4257
|
+
if (!response.ok) {
|
|
4258
|
+
let preview = "";
|
|
4259
|
+
try {
|
|
4260
|
+
preview = (await response.text()).slice(0, 500);
|
|
4261
|
+
} catch {}
|
|
4262
|
+
throw new Error(`reportCliRun HTTP ${response.status} ${response.statusText} (logID: ${logId})${preview ? ` body=${preview}` : ""}`);
|
|
4263
|
+
}
|
|
4264
|
+
let envelope;
|
|
4265
|
+
try {
|
|
4266
|
+
envelope = await response.json();
|
|
4267
|
+
} catch {
|
|
4268
|
+
console.error(`reportCliRun: ok logId=${logId} httpDurationMs=${Date.now() - start} (body unparseable, treated as success)`);
|
|
4269
|
+
return;
|
|
4270
|
+
}
|
|
4271
|
+
if (envelope.status_code !== void 0 && envelope.status_code !== "0") throw new Error(`reportCliRun API error (logID: ${logId}): code=${envelope.status_code}, message=${envelope.error_msg ?? ""}`);
|
|
4272
|
+
if (envelope.data && envelope.data.success === false) throw new Error(`reportCliRun business error (logID: ${logId}): ${envelope.error_msg ?? JSON.stringify(envelope.data)}`);
|
|
4273
|
+
console.error(`reportCliRun: ok logId=${logId} httpDurationMs=${Date.now() - start}`);
|
|
2809
4274
|
}
|
|
2810
4275
|
//#endregion
|
|
2811
4276
|
//#region src/help.ts
|
|
2812
4277
|
const BIN = "mclaw-diagnose";
|
|
2813
4278
|
function versionBanner() {
|
|
2814
|
-
return `v0.1.4-alpha.
|
|
4279
|
+
return `v0.1.4-alpha.2`;
|
|
2815
4280
|
}
|
|
2816
4281
|
const COMMANDS = [
|
|
2817
4282
|
{
|
|
@@ -2819,46 +4284,85 @@ const COMMANDS = [
|
|
|
2819
4284
|
hidden: false,
|
|
2820
4285
|
summary: "Diagnose openclaw config; apply repairs with --fix",
|
|
2821
4286
|
help: `USAGE
|
|
2822
|
-
${BIN} doctor [--fix] [--rule=<key>]...
|
|
4287
|
+
${BIN} doctor [--fix] [--show-diff] [--rule=<key>]... [--skip-rule=<key>]...
|
|
4288
|
+
[--caller=<n>] [--trace-id=<id>]
|
|
2823
4289
|
|
|
2824
4290
|
DESCRIPTION
|
|
2825
|
-
Fetches DoctorCtx via innerapi, then
|
|
2826
|
-
|
|
4291
|
+
Fetches DoctorCtx via innerapi, then orchestrates each rule:
|
|
4292
|
+
|
|
4293
|
+
validate (always)
|
|
4294
|
+
pass → status='pass'
|
|
4295
|
+
fail and !--fix → status='failed'
|
|
4296
|
+
fail and --fix
|
|
4297
|
+
repair → re-validate
|
|
4298
|
+
pass → status='fixed'
|
|
4299
|
+
still fail → status='still-broken'
|
|
2827
4300
|
|
|
2828
|
-
|
|
2829
|
-
(no flags) Check-only. Runs the rule engine against the
|
|
2830
|
-
sandbox's current openclaw config and returns
|
|
2831
|
-
{ failedRules: { standard, ai, reset } }
|
|
2832
|
-
No files are mutated. Use this when you just
|
|
2833
|
-
want to know what's wrong.
|
|
4301
|
+
Output is a single JSON object on stdout:
|
|
2834
4302
|
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
4303
|
+
{
|
|
4304
|
+
"results": [{ "rule": "...", "status": "...", "message"?: "...", ...}],
|
|
4305
|
+
"summary": { "pass": N, "failed": N, "fixed": N, "stillBroken": N,
|
|
4306
|
+
"skipped": N, "error": N, "unknown": N },
|
|
4307
|
+
"aborted": false
|
|
4308
|
+
}
|
|
2840
4309
|
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
4310
|
+
STATUSES
|
|
4311
|
+
pass Rule's validate passed; no repair attempted.
|
|
4312
|
+
failed Rule's validate failed; --fix was not given.
|
|
4313
|
+
fixed Rule's validate failed; repair ran; re-validate passed.
|
|
4314
|
+
still-broken Rule's validate failed; repair ran; re-validate still failed.
|
|
4315
|
+
skipped Rule excluded by disabledRules / --skip-rule / dependsOn /
|
|
4316
|
+
skipWhen.
|
|
4317
|
+
error validate or repair threw an exception. ABORTS the run —
|
|
4318
|
+
exit code becomes 1; partial 'results' still written to stdout.
|
|
4319
|
+
unknown --rule=<k> with k not registered. Reported, not aborting.
|
|
2847
4320
|
|
|
2848
4321
|
OPTIONS
|
|
2849
|
-
--fix
|
|
2850
|
-
|
|
2851
|
-
|
|
4322
|
+
--fix Enable repair. Without it, validate runs but failures
|
|
4323
|
+
are reported as 'failed' instead of being repaired.
|
|
4324
|
+
--show-diff With --fix: replace the JSON report on stdout with
|
|
4325
|
+
a plain-text section showing
|
|
4326
|
+
original: <backup path>
|
|
4327
|
+
after fixed: <openclaw.json path>
|
|
4328
|
+
diff:
|
|
4329
|
+
<unified-diff body, same format as \`json-diff\`>
|
|
4330
|
+
Interactive flag — sandbox_console's push path never
|
|
4331
|
+
sets it, so the JSON contract is preserved for that
|
|
4332
|
+
caller. No-op without --fix (no mutation, no diff).
|
|
4333
|
+
--rule=<key> Whitelist: restrict the run to this rule key (repeatable).
|
|
4334
|
+
Without --rule, every registered rule runs. With --rule,
|
|
4335
|
+
the planner narrows the innerapi vars fetch to just
|
|
4336
|
+
that rule's needs (see lazy-vars).
|
|
4337
|
+
--skip-rule=<key> Blacklist: skip the rule entirely — neither validate
|
|
4338
|
+
nor repair runs (repeatable). Outcome is 'skipped' with
|
|
4339
|
+
message='--skip-rule'. Stacks with the server's
|
|
4340
|
+
disabledRules. Skipped rules also subtract their vars
|
|
4341
|
+
from the planner's union.
|
|
4342
|
+
--caller=<name> Optional metadata; passed through verbatim to innerapi
|
|
4343
|
+
(fetchCtx + report_cli_run body.input.caller).
|
|
4344
|
+
E.g. "sweep_job", "user_yxy". Server uses for audit /
|
|
4345
|
+
correlation; CLI doesn't act on it.
|
|
4346
|
+
--trace-id=<id> Optional log-correlation id; passed through verbatim
|
|
4347
|
+
to innerapi body.input.traceId. Typically the upstream
|
|
4348
|
+
caller's logID (e.g. sandbox_console's request id).
|
|
2852
4349
|
|
|
2853
4350
|
EXAMPLES
|
|
2854
|
-
${BIN} doctor
|
|
2855
|
-
${BIN} doctor --fix
|
|
2856
|
-
${BIN} doctor --fix --rule=gateway
|
|
2857
|
-
${BIN} doctor --fix --rule=
|
|
4351
|
+
${BIN} doctor # diagnose, no fixes
|
|
4352
|
+
${BIN} doctor --fix # diagnose + fix everything
|
|
4353
|
+
${BIN} doctor --fix --rule=gateway # fix only 'gateway'
|
|
4354
|
+
${BIN} doctor --fix --skip-rule=feishu_channel # fix everything except feishu
|
|
4355
|
+
${BIN} doctor --fix --skip-rule=feishu_channel --skip-rule=feishu_default_account
|
|
4356
|
+
# skip multiple rules
|
|
4357
|
+
${BIN} doctor --fix --show-diff # see exactly what changed
|
|
4358
|
+
# (plain-text diff after
|
|
4359
|
+
# the JSON report)
|
|
2858
4360
|
|
|
2859
4361
|
EXIT CODES
|
|
2860
|
-
0
|
|
2861
|
-
|
|
4362
|
+
0 completed (results may include 'failed' / 'still-broken' — those
|
|
4363
|
+
are normal data outcomes, not run failures)
|
|
4364
|
+
1 aborted because a rule's validate or repair threw. Partial
|
|
4365
|
+
results still on stdout.
|
|
2862
4366
|
77 innerapi authentication failed (sandbox JWT expired/invalid)
|
|
2863
4367
|
`
|
|
2864
4368
|
},
|
|
@@ -3047,6 +4551,62 @@ function formatCommandHelp(name) {
|
|
|
3047
4551
|
return cmd.help;
|
|
3048
4552
|
}
|
|
3049
4553
|
//#endregion
|
|
4554
|
+
//#region src/rule-engine/planner.ts
|
|
4555
|
+
/**
|
|
4556
|
+
* Compute the union of `Vars` field names that the **enabled** rules will
|
|
4557
|
+
* actually read. Sources from each rule's `@Rule({ usesVars: [...] })`
|
|
4558
|
+
* declaration; drift is caught at test time by `strict-vars.test.ts`.
|
|
4559
|
+
*
|
|
4560
|
+
* Inactive rules (skipped via skipWhen at runtime) are STILL counted —
|
|
4561
|
+
* skipWhen depends on parsed-config state we don't have at planning time.
|
|
4562
|
+
* Slight over-fetch in those cases, but no correctness issue.
|
|
4563
|
+
*
|
|
4564
|
+
* Result is stable-sorted for deterministic logging / test snapshots.
|
|
4565
|
+
*/
|
|
4566
|
+
function planVarsFields(opts = {}) {
|
|
4567
|
+
const disabled = new Set(opts.disabled ?? []);
|
|
4568
|
+
const only = opts.onlyRules && opts.onlyRules.length > 0 ? new Set(opts.onlyRules) : null;
|
|
4569
|
+
const fields = /* @__PURE__ */ new Set();
|
|
4570
|
+
for (const rule of getAllRules()) {
|
|
4571
|
+
const key = rule.meta.key;
|
|
4572
|
+
if (disabled.has(key)) continue;
|
|
4573
|
+
if (only && !only.has(key)) continue;
|
|
4574
|
+
for (const f of rule.meta.usesVars ?? []) fields.add(f);
|
|
4575
|
+
}
|
|
4576
|
+
return [...fields].sort();
|
|
4577
|
+
}
|
|
4578
|
+
/**
|
|
4579
|
+
* Plan a `populate` selector for a given subcommand.
|
|
4580
|
+
*
|
|
4581
|
+
* Per-command group needs:
|
|
4582
|
+
*
|
|
4583
|
+
* doctor / check app (rule-driven)
|
|
4584
|
+
* repair app + secrets (writes secretsContent / providerKeyContent)
|
|
4585
|
+
* reset app + secrets + install + reset (the works)
|
|
4586
|
+
* install-* install only
|
|
4587
|
+
*
|
|
4588
|
+
* Empty result (`{}`) means "no group needed" — the CLI can skip the
|
|
4589
|
+
* `fetchCtxViaInnerApi` call entirely and run with a synthetic empty ctx.
|
|
4590
|
+
* Happens e.g. when the user pinned `--rule=<key>` to a vars-free rule on
|
|
4591
|
+
* `doctor`.
|
|
4592
|
+
*/
|
|
4593
|
+
function planCtxPopulate(opts) {
|
|
4594
|
+
if (opts.command === "install") return { install: true };
|
|
4595
|
+
const populate = {};
|
|
4596
|
+
const appFields = planVarsFields({
|
|
4597
|
+
disabled: opts.disabled,
|
|
4598
|
+
onlyRules: opts.onlyRules
|
|
4599
|
+
});
|
|
4600
|
+
if (appFields.length > 0) populate.app = appFields;
|
|
4601
|
+
if (opts.command === "repair") populate.secrets = true;
|
|
4602
|
+
else if (opts.command === "reset") {
|
|
4603
|
+
populate.secrets = true;
|
|
4604
|
+
populate.install = true;
|
|
4605
|
+
populate.reset = true;
|
|
4606
|
+
}
|
|
4607
|
+
return populate;
|
|
4608
|
+
}
|
|
4609
|
+
//#endregion
|
|
3050
4610
|
//#region src/index.ts
|
|
3051
4611
|
const args = node_process.default.argv.slice(2);
|
|
3052
4612
|
const mode = args.find((a) => !a.startsWith("-"));
|
|
@@ -3080,8 +4640,39 @@ function getMultiFlag(args, name) {
|
|
|
3080
4640
|
const prefix = `--${name}=`;
|
|
3081
4641
|
return args.filter((a) => a.startsWith(prefix)).map((a) => a.slice(prefix.length));
|
|
3082
4642
|
}
|
|
4643
|
+
/**
|
|
4644
|
+
* Per-case telemetry helper: fires `openclaw.report_cli_run` once with
|
|
4645
|
+
* the command's outcome. Always called — the previous server-flag gate
|
|
4646
|
+
* was removed (server controls receipt by routing the apiName).
|
|
4647
|
+
*
|
|
4648
|
+
* Best-effort: a thrown reportCliRun is logged to cli.log and swallowed.
|
|
4649
|
+
* Never alters the data path's exit code — the user-visible work has
|
|
4650
|
+
* already finished by the time we report.
|
|
4651
|
+
*
|
|
4652
|
+
* `raw` is kept in the signature for backward symmetry with the doctor
|
|
4653
|
+
* case but is no longer consulted.
|
|
4654
|
+
*/
|
|
4655
|
+
async function reportRun(command, rc, _raw, invocation, durationMs, outcome) {
|
|
4656
|
+
console.error(`${command}: telemetry calling report_cli_run`);
|
|
4657
|
+
try {
|
|
4658
|
+
await reportCliRun({
|
|
4659
|
+
command,
|
|
4660
|
+
runId: rc.runId,
|
|
4661
|
+
version: getVersion(),
|
|
4662
|
+
invocation,
|
|
4663
|
+
durationMs,
|
|
4664
|
+
caller: rc.caller,
|
|
4665
|
+
traceId: rc.traceId,
|
|
4666
|
+
success: outcome.success,
|
|
4667
|
+
result: outcome.result,
|
|
4668
|
+
error: outcome.error ? { message: outcome.error.message } : void 0
|
|
4669
|
+
});
|
|
4670
|
+
} catch (e) {
|
|
4671
|
+
console.error(`[telemetry] reportCliRun failed: ${e.message}`);
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
3083
4674
|
async function main() {
|
|
3084
|
-
installStderrMirror();
|
|
4675
|
+
installStderrMirror({ stderrEnabled: args.includes("-x") });
|
|
3085
4676
|
const helpFlags = parseHelpFlags(args);
|
|
3086
4677
|
if (mode && helpFlags.help) {
|
|
3087
4678
|
const body = formatCommandHelp(mode);
|
|
@@ -3101,22 +4692,79 @@ async function main() {
|
|
|
3101
4692
|
node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));
|
|
3102
4693
|
node_process.default.exit(1);
|
|
3103
4694
|
}
|
|
4695
|
+
const caller = getFlag(args, "caller");
|
|
4696
|
+
const traceId = getFlag(args, "trace-id");
|
|
4697
|
+
const rc = setRunContext({
|
|
4698
|
+
caller,
|
|
4699
|
+
traceId
|
|
4700
|
+
});
|
|
4701
|
+
const t0 = Date.now();
|
|
4702
|
+
console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
|
|
3104
4703
|
switch (mode) {
|
|
3105
|
-
case "check":
|
|
4704
|
+
case "check": {
|
|
4705
|
+
const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4706
|
+
populate: planCtxPopulate({ command: "check" }),
|
|
4707
|
+
caller,
|
|
4708
|
+
traceId
|
|
4709
|
+
});
|
|
4710
|
+
const { legacy, report } = runCheckWithReport(buildCheckInput(raw));
|
|
4711
|
+
console.log(JSON.stringify(legacy));
|
|
4712
|
+
await reportRun("check", rc, raw, args.join(" "), Date.now() - t0, {
|
|
4713
|
+
success: !report.aborted,
|
|
4714
|
+
result: report,
|
|
4715
|
+
error: report.aborted ? /* @__PURE__ */ new Error("check run aborted") : void 0
|
|
4716
|
+
});
|
|
4717
|
+
break;
|
|
4718
|
+
}
|
|
3106
4719
|
case "repair": {
|
|
3107
|
-
const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi(
|
|
3108
|
-
|
|
3109
|
-
|
|
4720
|
+
const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4721
|
+
populate: planCtxPopulate({ command: "repair" }),
|
|
4722
|
+
caller,
|
|
4723
|
+
traceId
|
|
4724
|
+
});
|
|
4725
|
+
const { legacy, report } = runRepairWithReport(buildRepairInput(raw));
|
|
4726
|
+
console.log(JSON.stringify(legacy));
|
|
4727
|
+
await reportRun("repair", rc, raw, args.join(" "), Date.now() - t0, {
|
|
4728
|
+
success: legacy.success,
|
|
4729
|
+
result: report,
|
|
4730
|
+
error: legacy.success ? void 0 : new Error(legacy.error ?? "repair failed")
|
|
4731
|
+
});
|
|
3110
4732
|
break;
|
|
3111
4733
|
}
|
|
3112
4734
|
case "doctor": {
|
|
3113
4735
|
const fix = args.includes("--fix");
|
|
3114
4736
|
const rules = getMultiFlag(args, "rule");
|
|
3115
|
-
const
|
|
4737
|
+
const skipRules = getMultiFlag(args, "skip-rule");
|
|
4738
|
+
const showDiff = args.includes("--show-diff");
|
|
4739
|
+
const populate = planCtxPopulate({
|
|
4740
|
+
command: "doctor",
|
|
4741
|
+
onlyRules: fix && rules.length > 0 ? rules : void 0,
|
|
4742
|
+
disabled: skipRules
|
|
4743
|
+
});
|
|
4744
|
+
console.error(`doctor: populate=${JSON.stringify(populate)} fix=${fix} rules=[${rules.join(",")}] skipRules=[${skipRules.join(",")}]`);
|
|
4745
|
+
let raw;
|
|
4746
|
+
if (Object.keys(populate).length === 0) {
|
|
4747
|
+
console.error("doctor: skipping fetchCtx (empty populate)");
|
|
4748
|
+
raw = {};
|
|
4749
|
+
} else raw = await fetchCtxViaInnerApi({
|
|
4750
|
+
populate,
|
|
4751
|
+
caller,
|
|
4752
|
+
traceId
|
|
4753
|
+
});
|
|
4754
|
+
const result = await runDoctor(raw, {
|
|
3116
4755
|
fix,
|
|
3117
|
-
rules
|
|
4756
|
+
rules,
|
|
4757
|
+
skipRules,
|
|
4758
|
+
showDiff
|
|
3118
4759
|
});
|
|
3119
|
-
console.log(JSON.stringify(result));
|
|
4760
|
+
if (!(showDiff && fix)) console.log(JSON.stringify(result));
|
|
4761
|
+
await reportRun("doctor", rc, raw, args.join(" "), Date.now() - t0, {
|
|
4762
|
+
success: !result.aborted,
|
|
4763
|
+
result,
|
|
4764
|
+
error: result.aborted ? /* @__PURE__ */ new Error("doctor run aborted") : void 0
|
|
4765
|
+
});
|
|
4766
|
+
console.error(`doctor: end aborted=${result.aborted} totalMs=${Date.now() - t0} summary=${JSON.stringify(result.summary)}`);
|
|
4767
|
+
if (result.aborted) node_process.default.exit(1);
|
|
3120
4768
|
break;
|
|
3121
4769
|
}
|
|
3122
4770
|
case "reset":
|
|
@@ -3125,7 +4773,11 @@ async function main() {
|
|
|
3125
4773
|
let ctxBase64;
|
|
3126
4774
|
if (ctxArg) ctxBase64 = ctxArg.slice(6);
|
|
3127
4775
|
else {
|
|
3128
|
-
const fetched = await fetchCtxViaInnerApi(
|
|
4776
|
+
const fetched = await fetchCtxViaInnerApi({
|
|
4777
|
+
populate: planCtxPopulate({ command: "reset" }),
|
|
4778
|
+
caller,
|
|
4779
|
+
traceId
|
|
4780
|
+
});
|
|
3129
4781
|
ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
|
|
3130
4782
|
}
|
|
3131
4783
|
console.log(JSON.stringify(startAsyncReset(ctxBase64)));
|
|
@@ -3136,7 +4788,22 @@ async function main() {
|
|
|
3136
4788
|
node_process.default.exit(1);
|
|
3137
4789
|
}
|
|
3138
4790
|
const resultFile = resetResultFile(taskId);
|
|
3139
|
-
|
|
4791
|
+
const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4792
|
+
populate: planCtxPopulate({ command: "reset" }),
|
|
4793
|
+
caller,
|
|
4794
|
+
traceId
|
|
4795
|
+
});
|
|
4796
|
+
const outcome = await runReset(buildResetInput(raw), taskId, resultFile);
|
|
4797
|
+
await reportRun("reset", rc, raw, args.join(" "), Date.now() - t0, {
|
|
4798
|
+
success: outcome.success,
|
|
4799
|
+
result: {
|
|
4800
|
+
taskId,
|
|
4801
|
+
steps: outcome.steps,
|
|
4802
|
+
failedStep: outcome.failedStep
|
|
4803
|
+
},
|
|
4804
|
+
error: outcome.success ? void 0 : new Error(outcome.error ?? "reset failed")
|
|
4805
|
+
});
|
|
4806
|
+
if (!outcome.success) node_process.default.exit(1);
|
|
3140
4807
|
} else {
|
|
3141
4808
|
console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
|
|
3142
4809
|
node_process.default.exit(1);
|
|
@@ -3159,12 +4826,34 @@ async function main() {
|
|
|
3159
4826
|
}
|
|
3160
4827
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
3161
4828
|
let installOssFileMap;
|
|
3162
|
-
|
|
3163
|
-
|
|
4829
|
+
let rawForTelemetry;
|
|
4830
|
+
if (!ossFileMapFlag) {
|
|
4831
|
+
rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4832
|
+
populate: planCtxPopulate({ command: "install" }),
|
|
4833
|
+
caller,
|
|
4834
|
+
traceId
|
|
4835
|
+
});
|
|
4836
|
+
installOssFileMap = normalizeCtx(rawForTelemetry).install.ossFileMap;
|
|
4837
|
+
}
|
|
4838
|
+
const ossFileMap = resolveOssFileMap({
|
|
3164
4839
|
ossFileMapFlag,
|
|
3165
4840
|
installOssFileMap
|
|
3166
|
-
})
|
|
3167
|
-
|
|
4841
|
+
});
|
|
4842
|
+
let success = true;
|
|
4843
|
+
let error;
|
|
4844
|
+
try {
|
|
4845
|
+
await installOpenclaw(tag, ossFileMap);
|
|
4846
|
+
} catch (e) {
|
|
4847
|
+
success = false;
|
|
4848
|
+
error = e;
|
|
4849
|
+
}
|
|
4850
|
+
if (success) console.log(JSON.stringify({ ok: true }));
|
|
4851
|
+
await reportRun("install-openclaw", rc, rawForTelemetry, args.join(" "), Date.now() - t0, {
|
|
4852
|
+
success,
|
|
4853
|
+
result: { tag },
|
|
4854
|
+
error
|
|
4855
|
+
});
|
|
4856
|
+
if (error) throw error;
|
|
3168
4857
|
break;
|
|
3169
4858
|
}
|
|
3170
4859
|
case "install-extension": {
|
|
@@ -3180,18 +4869,45 @@ async function main() {
|
|
|
3180
4869
|
const skipConfigUpdate = args.includes("--skip-config-update");
|
|
3181
4870
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
3182
4871
|
let installOssFileMap;
|
|
3183
|
-
|
|
3184
|
-
|
|
4872
|
+
let rawForTelemetry;
|
|
4873
|
+
if (!ossFileMapFlag) {
|
|
4874
|
+
rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4875
|
+
populate: planCtxPopulate({ command: "install" }),
|
|
4876
|
+
caller,
|
|
4877
|
+
traceId
|
|
4878
|
+
});
|
|
4879
|
+
installOssFileMap = normalizeCtx(rawForTelemetry).install.ossFileMap;
|
|
4880
|
+
}
|
|
4881
|
+
const ossFileMap = resolveOssFileMap({
|
|
3185
4882
|
ossFileMapFlag,
|
|
3186
4883
|
installOssFileMap
|
|
3187
|
-
}), {
|
|
3188
|
-
all,
|
|
3189
|
-
names: names.length > 0 ? names : void 0,
|
|
3190
|
-
homeBase,
|
|
3191
|
-
configPath,
|
|
3192
|
-
skipConfigUpdate
|
|
3193
4884
|
});
|
|
3194
|
-
|
|
4885
|
+
let success = true;
|
|
4886
|
+
let error;
|
|
4887
|
+
try {
|
|
4888
|
+
await installExtension(tag, ossFileMap, {
|
|
4889
|
+
all,
|
|
4890
|
+
names: names.length > 0 ? names : void 0,
|
|
4891
|
+
homeBase,
|
|
4892
|
+
configPath,
|
|
4893
|
+
skipConfigUpdate
|
|
4894
|
+
});
|
|
4895
|
+
} catch (e) {
|
|
4896
|
+
success = false;
|
|
4897
|
+
error = e;
|
|
4898
|
+
}
|
|
4899
|
+
if (success) console.log(JSON.stringify({ ok: true }));
|
|
4900
|
+
await reportRun("install-extension", rc, rawForTelemetry, args.join(" "), Date.now() - t0, {
|
|
4901
|
+
success,
|
|
4902
|
+
result: {
|
|
4903
|
+
tag,
|
|
4904
|
+
all,
|
|
4905
|
+
names,
|
|
4906
|
+
skipConfigUpdate
|
|
4907
|
+
},
|
|
4908
|
+
error
|
|
4909
|
+
});
|
|
4910
|
+
if (error) throw error;
|
|
3195
4911
|
break;
|
|
3196
4912
|
}
|
|
3197
4913
|
case "download-resource": {
|
|
@@ -3209,16 +4925,43 @@ async function main() {
|
|
|
3209
4925
|
}
|
|
3210
4926
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
3211
4927
|
let installOssFileMap;
|
|
3212
|
-
|
|
3213
|
-
|
|
4928
|
+
let rawForTelemetry;
|
|
4929
|
+
if (!ossFileMapFlag) {
|
|
4930
|
+
rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4931
|
+
populate: planCtxPopulate({ command: "install" }),
|
|
4932
|
+
caller,
|
|
4933
|
+
traceId
|
|
4934
|
+
});
|
|
4935
|
+
installOssFileMap = normalizeCtx(rawForTelemetry).install.ossFileMap;
|
|
4936
|
+
}
|
|
4937
|
+
const ossFileMap = resolveOssFileMap({
|
|
3214
4938
|
ossFileMapFlag,
|
|
3215
4939
|
installOssFileMap
|
|
3216
|
-
}), {
|
|
3217
|
-
role,
|
|
3218
|
-
name,
|
|
3219
|
-
dir
|
|
3220
4940
|
});
|
|
3221
|
-
|
|
4941
|
+
let success = true;
|
|
4942
|
+
let error;
|
|
4943
|
+
try {
|
|
4944
|
+
await downloadResource(tag, ossFileMap, {
|
|
4945
|
+
role,
|
|
4946
|
+
name,
|
|
4947
|
+
dir
|
|
4948
|
+
});
|
|
4949
|
+
} catch (e) {
|
|
4950
|
+
success = false;
|
|
4951
|
+
error = e;
|
|
4952
|
+
}
|
|
4953
|
+
if (success) console.log(JSON.stringify({ ok: true }));
|
|
4954
|
+
await reportRun("download-resource", rc, rawForTelemetry, args.join(" "), Date.now() - t0, {
|
|
4955
|
+
success,
|
|
4956
|
+
result: {
|
|
4957
|
+
tag,
|
|
4958
|
+
role,
|
|
4959
|
+
name,
|
|
4960
|
+
dir
|
|
4961
|
+
},
|
|
4962
|
+
error
|
|
4963
|
+
});
|
|
4964
|
+
if (error) throw error;
|
|
3222
4965
|
break;
|
|
3223
4966
|
}
|
|
3224
4967
|
default:
|
|
@@ -3226,10 +4969,12 @@ async function main() {
|
|
|
3226
4969
|
node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));
|
|
3227
4970
|
node_process.default.exit(1);
|
|
3228
4971
|
}
|
|
4972
|
+
console.error(`${mode}: end totalMs=${Date.now() - t0}`);
|
|
3229
4973
|
}
|
|
3230
4974
|
main().catch((err) => {
|
|
3231
4975
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3232
|
-
console.error(
|
|
4976
|
+
console.error(`${mode ?? "<no-mode>"}: error message=${msg}`);
|
|
4977
|
+
node_process.default.stderr.write(`Error: ${msg}\n`);
|
|
3233
4978
|
node_process.default.exit(1);
|
|
3234
4979
|
});
|
|
3235
4980
|
//#endregion
|