@lark-apaas/openclaw-scripts-diagnose-cli 0.1.4-alpha.0 → 0.1.4-alpha.1
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 +1633 -433
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -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.1";
|
|
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 {
|
|
@@ -415,7 +432,7 @@ let TemplateVarsUnreplacedRule = class TemplateVarsUnreplacedRule extends Diagno
|
|
|
415
432
|
};
|
|
416
433
|
}
|
|
417
434
|
repair(ctx) {
|
|
418
|
-
const map = ctx.templateVars;
|
|
435
|
+
const map = ctx.vars.templateVars;
|
|
419
436
|
if (!map || Object.keys(map).length === 0) return;
|
|
420
437
|
replaceInPlace(ctx.config, Object.entries(map));
|
|
421
438
|
}
|
|
@@ -423,7 +440,8 @@ let TemplateVarsUnreplacedRule = class TemplateVarsUnreplacedRule extends Diagno
|
|
|
423
440
|
TemplateVarsUnreplacedRule = __decorate([Rule({
|
|
424
441
|
key: "template_vars_unreplaced",
|
|
425
442
|
dependsOn: ["config_syntax_check"],
|
|
426
|
-
repairMode: "standard"
|
|
443
|
+
repairMode: "standard",
|
|
444
|
+
usesVars: ["templateVars"]
|
|
427
445
|
})], TemplateVarsUnreplacedRule);
|
|
428
446
|
function collectPlaceholders(value, found) {
|
|
429
447
|
if (typeof value === "string") {
|
|
@@ -466,32 +484,33 @@ function applyVars(str, entries) {
|
|
|
466
484
|
}
|
|
467
485
|
//#endregion
|
|
468
486
|
//#region src/rules/model-provider.ts
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
487
|
+
const DEFAULT_API_KEY = {
|
|
488
|
+
source: "file",
|
|
489
|
+
provider: "miaoda-provider",
|
|
490
|
+
id: "value"
|
|
491
|
+
};
|
|
492
|
+
const DEFAULT_X_API_KEY_HEADER = {
|
|
493
|
+
source: "file",
|
|
494
|
+
provider: "miaoda-secret-provider",
|
|
495
|
+
id: "/models_providers_miaoda_headers_x_api_key"
|
|
496
|
+
};
|
|
497
|
+
const DEFAULT_API = "openai-completions";
|
|
498
|
+
function getExpected(vars) {
|
|
499
|
+
return {
|
|
500
|
+
baseUrl: vars.baseURL + "/api/v1/sgw/model/proxy",
|
|
501
|
+
apiKey: DEFAULT_API_KEY,
|
|
502
|
+
api: DEFAULT_API,
|
|
503
|
+
headers: { "x-api-key": DEFAULT_X_API_KEY_HEADER }
|
|
483
504
|
};
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return vars.baseURL + "/api/v1/sgw/model/proxy";
|
|
487
|
-
}
|
|
505
|
+
}
|
|
506
|
+
let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
|
|
488
507
|
validate(ctx) {
|
|
489
508
|
const provider = getNestedMap(ctx.config, "models", "providers", "miaoda");
|
|
490
509
|
if (!provider) return {
|
|
491
510
|
pass: false,
|
|
492
511
|
message: "models.providers.miaoda not found"
|
|
493
512
|
};
|
|
494
|
-
const expected =
|
|
513
|
+
const expected = getExpected(ctx.vars);
|
|
495
514
|
if (provider.baseUrl !== expected.baseUrl) return {
|
|
496
515
|
pass: false,
|
|
497
516
|
message: "baseUrl mismatch: got " + provider.baseUrl + ", expected " + expected.baseUrl
|
|
@@ -539,7 +558,7 @@ let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
|
|
|
539
558
|
return { pass: true };
|
|
540
559
|
}
|
|
541
560
|
repair(ctx) {
|
|
542
|
-
const expected =
|
|
561
|
+
const expected = getExpected(ctx.vars);
|
|
543
562
|
setNestedValue(ctx.config, [
|
|
544
563
|
"models",
|
|
545
564
|
"providers",
|
|
@@ -565,19 +584,12 @@ let ModelProviderRule = class ModelProviderRule extends DiagnoseRule {
|
|
|
565
584
|
"headers"
|
|
566
585
|
], expected.headers);
|
|
567
586
|
}
|
|
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
587
|
};
|
|
577
|
-
ModelProviderRule =
|
|
588
|
+
ModelProviderRule = __decorate([Rule({
|
|
578
589
|
key: "model_provider",
|
|
579
590
|
dependsOn: ["config_syntax_check"],
|
|
580
591
|
repairMode: "standard",
|
|
592
|
+
usesVars: ["innerAPIKey", "baseURL"],
|
|
581
593
|
skipWhen: ({ hasMiaoda }) => !hasMiaoda
|
|
582
594
|
})], ModelProviderRule);
|
|
583
595
|
//#endregion
|
|
@@ -707,7 +719,8 @@ let FeishuChannelRule = class FeishuChannelRule extends DiagnoseRule {
|
|
|
707
719
|
FeishuChannelRule = __decorate([Rule({
|
|
708
720
|
key: "feishu_channel",
|
|
709
721
|
dependsOn: ["config_syntax_check", "feishu_default_account"],
|
|
710
|
-
repairMode: "standard"
|
|
722
|
+
repairMode: "standard",
|
|
723
|
+
usesVars: ["feishuAppID", "feishuAppSecret"]
|
|
711
724
|
})], FeishuChannelRule);
|
|
712
725
|
//#endregion
|
|
713
726
|
//#region src/rules/feishu-default-account.ts
|
|
@@ -848,7 +861,8 @@ let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRu
|
|
|
848
861
|
FeishuDefaultAccountRule = __decorate([Rule({
|
|
849
862
|
key: "feishu_default_account",
|
|
850
863
|
dependsOn: ["config_syntax_check"],
|
|
851
|
-
repairMode: "standard"
|
|
864
|
+
repairMode: "standard",
|
|
865
|
+
usesVars: ["feishuAppID", "teamChatID"]
|
|
852
866
|
})], FeishuDefaultAccountRule);
|
|
853
867
|
function nonEmpty(v) {
|
|
854
868
|
return typeof v === "string" && v !== "" ? v : void 0;
|
|
@@ -879,23 +893,19 @@ function secretMatchesCanonical(secret) {
|
|
|
879
893
|
}
|
|
880
894
|
//#endregion
|
|
881
895
|
//#region src/rules/gateway.ts
|
|
882
|
-
|
|
896
|
+
const DEFAULT_PORT = 18789;
|
|
897
|
+
const DEFAULT_MODE = "local";
|
|
898
|
+
const DEFAULT_BIND = "loopback";
|
|
899
|
+
const DEFAULT_AUTH_MODE = "token";
|
|
900
|
+
const DEFAULT_AUTH_TOKEN = {
|
|
901
|
+
source: "file",
|
|
902
|
+
provider: "miaoda-secret-provider",
|
|
903
|
+
id: "/gateway_auth_token"
|
|
904
|
+
};
|
|
905
|
+
/** Required entries in gateway.trustedProxies. Repair appends any missing
|
|
906
|
+
* entries while preserving caller-added extras (no overwrite). */
|
|
907
|
+
const DEFAULT_TRUSTED_PROXIES = ["::1", "127.0.0.1"];
|
|
883
908
|
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
909
|
validate(ctx) {
|
|
900
910
|
const gateway = ctx.config.gateway;
|
|
901
911
|
if (!gateway || typeof gateway !== "object") return {
|
|
@@ -903,17 +913,17 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
903
913
|
message: "gateway not found"
|
|
904
914
|
};
|
|
905
915
|
const gw = gateway;
|
|
906
|
-
if (gw.port !==
|
|
916
|
+
if (gw.port !== DEFAULT_PORT) return {
|
|
907
917
|
pass: false,
|
|
908
|
-
message: "gateway.port mismatch: got " + gw.port + ", expected "
|
|
918
|
+
message: "gateway.port mismatch: got " + gw.port + ", expected 18789"
|
|
909
919
|
};
|
|
910
|
-
if (gw.mode !==
|
|
920
|
+
if (gw.mode !== DEFAULT_MODE) return {
|
|
911
921
|
pass: false,
|
|
912
|
-
message: "gateway.mode mismatch: got " + gw.mode + ", expected "
|
|
922
|
+
message: "gateway.mode mismatch: got " + gw.mode + ", expected local"
|
|
913
923
|
};
|
|
914
|
-
if (gw.bind !==
|
|
924
|
+
if (gw.bind !== DEFAULT_BIND) return {
|
|
915
925
|
pass: false,
|
|
916
|
-
message: "gateway.bind mismatch: got " + gw.bind + ", expected "
|
|
926
|
+
message: "gateway.bind mismatch: got " + gw.bind + ", expected loopback"
|
|
917
927
|
};
|
|
918
928
|
const auth = gw.auth;
|
|
919
929
|
if (!auth || typeof auth !== "object") return {
|
|
@@ -921,9 +931,9 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
921
931
|
message: "gateway.auth not found"
|
|
922
932
|
};
|
|
923
933
|
const authObj = auth;
|
|
924
|
-
if (authObj.mode !==
|
|
934
|
+
if (authObj.mode !== DEFAULT_AUTH_MODE) return {
|
|
925
935
|
pass: false,
|
|
926
|
-
message: "gateway.auth.mode mismatch: got " + authObj.mode + ", expected "
|
|
936
|
+
message: "gateway.auth.mode mismatch: got " + authObj.mode + ", expected token"
|
|
927
937
|
};
|
|
928
938
|
const token = authObj.token;
|
|
929
939
|
if (typeof token === "string") {
|
|
@@ -932,7 +942,7 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
932
942
|
message: "gateway.auth.token string value mismatch"
|
|
933
943
|
};
|
|
934
944
|
} else if (typeof token === "object" && token !== null && !Array.isArray(token)) {
|
|
935
|
-
if (!matchMap(token,
|
|
945
|
+
if (!matchMap(token, DEFAULT_AUTH_TOKEN)) return {
|
|
936
946
|
pass: false,
|
|
937
947
|
message: "gateway.auth.token object mismatch: got " + JSON.stringify(token)
|
|
938
948
|
};
|
|
@@ -954,7 +964,7 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
954
964
|
pass: false,
|
|
955
965
|
message: "gateway.trustedProxies missing or not an array"
|
|
956
966
|
};
|
|
957
|
-
const missing =
|
|
967
|
+
const missing = DEFAULT_TRUSTED_PROXIES.filter((p) => !proxies.includes(p));
|
|
958
968
|
if (missing.length > 0) return {
|
|
959
969
|
pass: false,
|
|
960
970
|
message: "gateway.trustedProxies missing: " + JSON.stringify(missing)
|
|
@@ -962,19 +972,19 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
962
972
|
return { pass: true };
|
|
963
973
|
}
|
|
964
974
|
repair(ctx) {
|
|
965
|
-
setNestedValue(ctx.config, ["gateway", "port"],
|
|
966
|
-
setNestedValue(ctx.config, ["gateway", "mode"],
|
|
967
|
-
setNestedValue(ctx.config, ["gateway", "bind"],
|
|
975
|
+
setNestedValue(ctx.config, ["gateway", "port"], DEFAULT_PORT);
|
|
976
|
+
setNestedValue(ctx.config, ["gateway", "mode"], DEFAULT_MODE);
|
|
977
|
+
setNestedValue(ctx.config, ["gateway", "bind"], DEFAULT_BIND);
|
|
968
978
|
setNestedValue(ctx.config, [
|
|
969
979
|
"gateway",
|
|
970
980
|
"auth",
|
|
971
981
|
"mode"
|
|
972
|
-
],
|
|
982
|
+
], DEFAULT_AUTH_MODE);
|
|
973
983
|
setNestedValue(ctx.config, [
|
|
974
984
|
"gateway",
|
|
975
985
|
"auth",
|
|
976
986
|
"token"
|
|
977
|
-
],
|
|
987
|
+
], DEFAULT_AUTH_TOKEN);
|
|
978
988
|
setNestedValue(ctx.config, [
|
|
979
989
|
"gateway",
|
|
980
990
|
"controlUi",
|
|
@@ -983,17 +993,18 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
983
993
|
const gw = ctx.config.gateway ?? {};
|
|
984
994
|
const current = Array.isArray(gw.trustedProxies) ? gw.trustedProxies.slice() : [];
|
|
985
995
|
const seen = new Set(current.map((v) => String(v)));
|
|
986
|
-
for (const p of
|
|
996
|
+
for (const p of DEFAULT_TRUSTED_PROXIES) if (!seen.has(p)) {
|
|
987
997
|
current.push(p);
|
|
988
998
|
seen.add(p);
|
|
989
999
|
}
|
|
990
1000
|
setNestedValue(ctx.config, ["gateway", "trustedProxies"], current);
|
|
991
1001
|
}
|
|
992
1002
|
};
|
|
993
|
-
GatewayRule =
|
|
1003
|
+
GatewayRule = __decorate([Rule({
|
|
994
1004
|
key: "gateway",
|
|
995
1005
|
dependsOn: ["config_syntax_check"],
|
|
996
|
-
repairMode: "standard"
|
|
1006
|
+
repairMode: "standard",
|
|
1007
|
+
usesVars: ["gatewayToken"]
|
|
997
1008
|
})], GatewayRule);
|
|
998
1009
|
//#endregion
|
|
999
1010
|
//#region src/rules/allowed-origins.ts
|
|
@@ -1037,7 +1048,8 @@ let AllowedOriginsRule = class AllowedOriginsRule extends DiagnoseRule {
|
|
|
1037
1048
|
AllowedOriginsRule = __decorate([Rule({
|
|
1038
1049
|
key: "allowed_origins",
|
|
1039
1050
|
dependsOn: ["config_syntax_check"],
|
|
1040
|
-
repairMode: "standard"
|
|
1051
|
+
repairMode: "standard",
|
|
1052
|
+
usesVars: ["expectedOrigins"]
|
|
1041
1053
|
})], AllowedOriginsRule);
|
|
1042
1054
|
function getExpectedOrigins(vars) {
|
|
1043
1055
|
return Array.isArray(vars.expectedOrigins) ? vars.expectedOrigins : [];
|
|
@@ -1116,11 +1128,12 @@ JwtTokenRule = __decorate([Rule({
|
|
|
1116
1128
|
key: "jwt_token",
|
|
1117
1129
|
dependsOn: ["config_syntax_check"],
|
|
1118
1130
|
repairMode: "standard",
|
|
1131
|
+
usesVars: ["providerFilePath"],
|
|
1119
1132
|
skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaProvider
|
|
1120
1133
|
})], JwtTokenRule);
|
|
1121
1134
|
//#endregion
|
|
1122
1135
|
//#region src/rules/secrets-file.ts
|
|
1123
|
-
let
|
|
1136
|
+
let SecretsFileRule = class SecretsFileRule extends DiagnoseRule {
|
|
1124
1137
|
validate(ctx) {
|
|
1125
1138
|
const filePath = ctx.vars.secretsFilePath;
|
|
1126
1139
|
if (!filePath) return {
|
|
@@ -1167,12 +1180,18 @@ let SecretsRule = class SecretsRule extends DiagnoseRule {
|
|
|
1167
1180
|
};
|
|
1168
1181
|
}
|
|
1169
1182
|
};
|
|
1170
|
-
|
|
1183
|
+
SecretsFileRule = __decorate([Rule({
|
|
1171
1184
|
key: "secrets_file",
|
|
1172
1185
|
dependsOn: ["config_syntax_check"],
|
|
1173
1186
|
repairMode: "standard",
|
|
1187
|
+
usesVars: [
|
|
1188
|
+
"secretsFilePath",
|
|
1189
|
+
"feishuAppSecret",
|
|
1190
|
+
"gatewayToken",
|
|
1191
|
+
"innerAPIKey"
|
|
1192
|
+
],
|
|
1174
1193
|
skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaSecretProvider
|
|
1175
|
-
})],
|
|
1194
|
+
})], SecretsFileRule);
|
|
1176
1195
|
//#endregion
|
|
1177
1196
|
//#region src/rules/cleanup-install-backup-dirs.ts
|
|
1178
1197
|
const DIR_PREFIX = ".openclaw-install-";
|
|
@@ -1386,16 +1405,26 @@ function getAllow(config) {
|
|
|
1386
1405
|
}
|
|
1387
1406
|
//#endregion
|
|
1388
1407
|
//#region src/check.ts
|
|
1389
|
-
|
|
1408
|
+
/** Telemetry-aware entry: returns both the legacy CheckResult (for stdout)
|
|
1409
|
+
* AND a DoctorReport-shape payload (for `openclaw.report_cli_run`). The
|
|
1410
|
+
* two views are computed from the same single rule-loop pass. */
|
|
1411
|
+
function runCheckWithReport(input) {
|
|
1412
|
+
return runCheckImpl(input);
|
|
1413
|
+
}
|
|
1414
|
+
function runCheckImpl(input) {
|
|
1415
|
+
const tStart = Date.now();
|
|
1390
1416
|
const result = { failedRules: {
|
|
1391
1417
|
standard: [],
|
|
1392
1418
|
ai: [],
|
|
1393
1419
|
reset: []
|
|
1394
1420
|
} };
|
|
1421
|
+
const outcomes = [];
|
|
1395
1422
|
const disabledSet = new Set(input.disabledRules || []);
|
|
1396
1423
|
const rules = getAllRules();
|
|
1397
1424
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
1398
1425
|
let configParsed = false;
|
|
1426
|
+
let aborted = false;
|
|
1427
|
+
console.error(`runCheck: begin configPath=${input.configPath} disabledRules=[${[...disabledSet].join(",")}] rules=${rules.length}`);
|
|
1399
1428
|
let ctx = {
|
|
1400
1429
|
config: {},
|
|
1401
1430
|
configPath: input.configPath,
|
|
@@ -1403,13 +1432,29 @@ function runCheck(input) {
|
|
|
1403
1432
|
providerDeps: {
|
|
1404
1433
|
usesMiaodaProvider: false,
|
|
1405
1434
|
usesMiaodaSecretProvider: false
|
|
1406
|
-
}
|
|
1407
|
-
templateVars: input.templateVars
|
|
1435
|
+
}
|
|
1408
1436
|
};
|
|
1409
1437
|
for (const rule of rules) {
|
|
1410
1438
|
const meta = rule.meta;
|
|
1411
|
-
if (disabledSet.has(meta.key))
|
|
1412
|
-
|
|
1439
|
+
if (disabledSet.has(meta.key)) {
|
|
1440
|
+
console.error(`rule ${meta.key}: skipped reason=disabledRules`);
|
|
1441
|
+
outcomes.push({
|
|
1442
|
+
rule: meta.key,
|
|
1443
|
+
status: "skipped",
|
|
1444
|
+
message: "disabledRules"
|
|
1445
|
+
});
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
if (meta.dependsOn?.some((dep) => failedKeys.has(dep))) {
|
|
1449
|
+
const blockers = meta.dependsOn?.filter((d) => failedKeys.has(d)).join(",");
|
|
1450
|
+
console.error(`rule ${meta.key}: skipped reason=dependsOn-failed blockers=${blockers}`);
|
|
1451
|
+
outcomes.push({
|
|
1452
|
+
rule: meta.key,
|
|
1453
|
+
status: "skipped",
|
|
1454
|
+
message: `dependsOn failed: ${blockers}`
|
|
1455
|
+
});
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1413
1458
|
if (meta.dependsOn?.includes("config_syntax_check") && !configParsed) try {
|
|
1414
1459
|
const parsed = loadJSON5().parse(readFile(input.configPath));
|
|
1415
1460
|
const deps = analyzeProviderDeps(parsed);
|
|
@@ -1417,11 +1462,17 @@ function runCheck(input) {
|
|
|
1417
1462
|
config: parsed,
|
|
1418
1463
|
configPath: input.configPath,
|
|
1419
1464
|
vars: input.vars,
|
|
1420
|
-
providerDeps: deps
|
|
1421
|
-
templateVars: input.templateVars
|
|
1465
|
+
providerDeps: deps
|
|
1422
1466
|
};
|
|
1423
1467
|
configParsed = true;
|
|
1424
|
-
} catch {
|
|
1468
|
+
} catch (e) {
|
|
1469
|
+
console.error(`runCheck: config parse failed at ${meta.key} message=${e.message}`);
|
|
1470
|
+
outcomes.push({
|
|
1471
|
+
rule: meta.key,
|
|
1472
|
+
status: "error",
|
|
1473
|
+
message: "config parse failed: " + e.message
|
|
1474
|
+
});
|
|
1475
|
+
aborted = true;
|
|
1425
1476
|
break;
|
|
1426
1477
|
}
|
|
1427
1478
|
if (meta.skipWhen && configParsed) {
|
|
@@ -1429,15 +1480,53 @@ function runCheck(input) {
|
|
|
1429
1480
|
if (meta.skipWhen({
|
|
1430
1481
|
hasMiaoda,
|
|
1431
1482
|
deps: ctx.providerDeps
|
|
1432
|
-
}))
|
|
1483
|
+
})) {
|
|
1484
|
+
console.error(`rule ${meta.key}: skipped reason=skipWhen`);
|
|
1485
|
+
outcomes.push({
|
|
1486
|
+
rule: meta.key,
|
|
1487
|
+
status: "skipped",
|
|
1488
|
+
message: "skipWhen"
|
|
1489
|
+
});
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
const t = Date.now();
|
|
1494
|
+
let r;
|
|
1495
|
+
try {
|
|
1496
|
+
r = rule.validate(ctx);
|
|
1497
|
+
} catch (e) {
|
|
1498
|
+
const msg = e.message;
|
|
1499
|
+
console.error(`rule ${meta.key}: validate -> error durationMs=${Date.now() - t} message=${msg}`);
|
|
1500
|
+
outcomes.push({
|
|
1501
|
+
rule: meta.key,
|
|
1502
|
+
status: "error",
|
|
1503
|
+
message: "validate threw: " + msg
|
|
1504
|
+
});
|
|
1505
|
+
failedKeys.add(meta.key);
|
|
1506
|
+
continue;
|
|
1433
1507
|
}
|
|
1434
|
-
|
|
1435
|
-
|
|
1508
|
+
if (r.pass) {
|
|
1509
|
+
console.error(`rule ${meta.key}: validate -> pass durationMs=${Date.now() - t}`);
|
|
1510
|
+
outcomes.push({
|
|
1511
|
+
rule: meta.key,
|
|
1512
|
+
status: "pass"
|
|
1513
|
+
});
|
|
1514
|
+
} else {
|
|
1515
|
+
console.error(`rule ${meta.key}: validate -> failed durationMs=${Date.now() - t} message=${r.message ?? ""}`);
|
|
1516
|
+
outcomes.push({
|
|
1517
|
+
rule: meta.key,
|
|
1518
|
+
status: "failed",
|
|
1519
|
+
message: r.message
|
|
1520
|
+
});
|
|
1436
1521
|
failedKeys.add(meta.key);
|
|
1437
1522
|
pushFailedRule(result, meta.repairMode, meta.key, r.message || "");
|
|
1438
1523
|
}
|
|
1439
1524
|
}
|
|
1440
|
-
|
|
1525
|
+
console.error(`runCheck: end totalMs=${Date.now() - tStart} failed={standard:${result.failedRules.standard.length},ai:${result.failedRules.ai.length},reset:${result.failedRules.reset.length}}`);
|
|
1526
|
+
return {
|
|
1527
|
+
legacy: result,
|
|
1528
|
+
report: finalize$2(outcomes, aborted)
|
|
1529
|
+
};
|
|
1441
1530
|
}
|
|
1442
1531
|
function pushFailedRule(result, repairMode, key, detail) {
|
|
1443
1532
|
switch (repairMode) {
|
|
@@ -1458,158 +1547,43 @@ function pushFailedRule(result, repairMode, key, detail) {
|
|
|
1458
1547
|
break;
|
|
1459
1548
|
}
|
|
1460
1549
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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 };
|
|
1521
|
-
} catch (e) {
|
|
1522
|
-
return {
|
|
1523
|
-
success: false,
|
|
1524
|
-
error: "repair failed: " + e.message
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
//#endregion
|
|
1529
|
-
//#region src/paths.ts
|
|
1530
|
-
/**
|
|
1531
|
-
* Central directory for all ephemeral diagnose/reset artifacts: task status
|
|
1532
|
-
* files (`reset-<taskId>.json`) and human-readable step logs
|
|
1533
|
-
* (`reset-<taskId>.log`). Having everything under one dir makes debugging a
|
|
1534
|
-
* stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
|
|
1535
|
-
* run, and each run's log is right next to its state.
|
|
1536
|
-
*/
|
|
1537
|
-
const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
|
|
1538
|
-
function resetResultFile(taskId) {
|
|
1539
|
-
return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
|
|
1540
|
-
}
|
|
1541
|
-
function resetLogFile(taskId) {
|
|
1542
|
-
return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
|
|
1543
|
-
}
|
|
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
|
-
/**
|
|
1555
|
-
* Shared CLI log file. Every log line the CLI emits — whether through
|
|
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.
|
|
1559
|
-
*
|
|
1560
|
-
* `/tmp` is ephemeral on sandbox restart; we rely on that for rotation
|
|
1561
|
-
* (no size-based rotation implemented).
|
|
1562
|
-
*/
|
|
1563
|
-
const CLI_LOG_FILE = "/tmp/openclaw-diagnose/cli.log";
|
|
1564
|
-
/** Append one line to the shared cli.log. Swallows any fs error —
|
|
1565
|
-
* logging must never break the business flow. */
|
|
1566
|
-
function appendCliLog(line) {
|
|
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.
|
|
1578
|
-
*
|
|
1579
|
-
* Why console.error and not console.log: the CLI's stdout carries the
|
|
1580
|
-
* structured JSON result protocol consumed by sandbox_console and other
|
|
1581
|
-
* callers — any log line on stdout would corrupt JSON parsing. Rules,
|
|
1582
|
-
* helpers, and error paths therefore must route debug output through
|
|
1583
|
-
* console.error (stderr).
|
|
1584
|
-
*/
|
|
1585
|
-
function installStderrMirror() {
|
|
1586
|
-
if (stderrMirrorInstalled) return;
|
|
1587
|
-
stderrMirrorInstalled = true;
|
|
1588
|
-
const original = console.error.bind(console);
|
|
1589
|
-
console.error = (...args) => {
|
|
1590
|
-
original(...args);
|
|
1591
|
-
const body = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
|
|
1592
|
-
appendCliLog(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${body}\n`);
|
|
1550
|
+
function finalize$2(results, aborted) {
|
|
1551
|
+
const summary = {
|
|
1552
|
+
pass: 0,
|
|
1553
|
+
failed: 0,
|
|
1554
|
+
fixed: 0,
|
|
1555
|
+
stillBroken: 0,
|
|
1556
|
+
skipped: 0,
|
|
1557
|
+
error: 0,
|
|
1558
|
+
unknown: 0
|
|
1593
1559
|
};
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1560
|
+
for (const r of results) switch (r.status) {
|
|
1561
|
+
case "pass":
|
|
1562
|
+
summary.pass++;
|
|
1563
|
+
break;
|
|
1564
|
+
case "failed":
|
|
1565
|
+
summary.failed++;
|
|
1566
|
+
break;
|
|
1567
|
+
case "fixed":
|
|
1568
|
+
summary.fixed++;
|
|
1569
|
+
break;
|
|
1570
|
+
case "still-broken":
|
|
1571
|
+
summary.stillBroken++;
|
|
1572
|
+
break;
|
|
1573
|
+
case "skipped":
|
|
1574
|
+
summary.skipped++;
|
|
1575
|
+
break;
|
|
1576
|
+
case "error":
|
|
1577
|
+
summary.error++;
|
|
1578
|
+
break;
|
|
1579
|
+
case "unknown":
|
|
1580
|
+
summary.unknown++;
|
|
1581
|
+
break;
|
|
1600
1582
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
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);
|
|
1583
|
+
return {
|
|
1584
|
+
results,
|
|
1585
|
+
summary,
|
|
1586
|
+
aborted
|
|
1613
1587
|
};
|
|
1614
1588
|
}
|
|
1615
1589
|
//#endregion
|
|
@@ -1673,6 +1647,40 @@ function shellQuote(s) {
|
|
|
1673
1647
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1674
1648
|
}
|
|
1675
1649
|
/**
|
|
1650
|
+
* Snapshot the current `openclaw.json` to `<configPath>.<YYYYMMDD_HHMMSS>.bak`
|
|
1651
|
+
* before a repair / fix flow rewrites it. Local-time stamp matches the
|
|
1652
|
+
* shell-style backup convention (`cp openclaw.json openclaw.json.$(date
|
|
1653
|
+
* +%Y%m%d_%H%M%S).bak`) so operators triaging on the sandbox see the
|
|
1654
|
+
* familiar filename pattern.
|
|
1655
|
+
*
|
|
1656
|
+
* Returns the absolute backup path on success, or `null` when:
|
|
1657
|
+
* - source doesn't exist (first-time setup; nothing to back up)
|
|
1658
|
+
* - copy fails for any reason (disk full, permission denied, etc.)
|
|
1659
|
+
*
|
|
1660
|
+
* Failure is logged to cli.log via console.error and swallowed — the
|
|
1661
|
+
* caller's repair/fix work goes ahead either way. The backup is a safety
|
|
1662
|
+
* net, not a correctness gate; better to do the repair than refuse over
|
|
1663
|
+
* a missing rollback file.
|
|
1664
|
+
*/
|
|
1665
|
+
function backupConfigSync(configPath) {
|
|
1666
|
+
if (!node_fs.default.existsSync(configPath)) {
|
|
1667
|
+
console.error(`backupConfigSync: skip — source missing path=${configPath}`);
|
|
1668
|
+
return null;
|
|
1669
|
+
}
|
|
1670
|
+
const d = /* @__PURE__ */ new Date();
|
|
1671
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
1672
|
+
const backupPath = `${configPath}.${`${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`}.bak`;
|
|
1673
|
+
try {
|
|
1674
|
+
node_fs.default.copyFileSync(configPath, backupPath);
|
|
1675
|
+
const bytes = node_fs.default.statSync(backupPath).size;
|
|
1676
|
+
console.error(`backupConfigSync: ok src=${configPath} dst=${backupPath} bytes=${bytes}`);
|
|
1677
|
+
return backupPath;
|
|
1678
|
+
} catch (e) {
|
|
1679
|
+
console.error(`backupConfigSync: failed src=${configPath} message=${e.message}`);
|
|
1680
|
+
return null;
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1676
1684
|
* Extract an npm-packed gzipped tarball.
|
|
1677
1685
|
*
|
|
1678
1686
|
* ## The problem this works around
|
|
@@ -1746,59 +1754,510 @@ function extractTarballTolerant(tarball, destDir, opts = {}) {
|
|
|
1746
1754
|
}
|
|
1747
1755
|
}
|
|
1748
1756
|
//#endregion
|
|
1749
|
-
//#region src/
|
|
1750
|
-
/**
|
|
1751
|
-
*
|
|
1752
|
-
*
|
|
1753
|
-
*
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
const
|
|
1759
|
-
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1757
|
+
//#region src/repair.ts
|
|
1758
|
+
/** Telemetry-aware entry. Same per-rule pass + side effects, but also
|
|
1759
|
+
* builds a `DoctorReport`-shape payload from per-rule revalidate
|
|
1760
|
+
* outcomes (`fixed` / `still-broken` / `error`) so `report_cli_run`
|
|
1761
|
+
* carries the same shape doctor produces. */
|
|
1762
|
+
function runRepairWithReport(input) {
|
|
1763
|
+
return runRepairImpl(input);
|
|
1764
|
+
}
|
|
1765
|
+
function runRepairImpl(input) {
|
|
1766
|
+
const tStart = Date.now();
|
|
1767
|
+
console.error(`runRepair: begin configPath=${input.configPath} failedRules=[${(input.failedRules ?? []).join(",")}]`);
|
|
1768
|
+
const outcomes = [];
|
|
1769
|
+
let aborted = false;
|
|
1770
|
+
let legacy = { success: true };
|
|
1771
|
+
try {
|
|
1772
|
+
const failedSet = new Set(input.failedRules || []);
|
|
1773
|
+
const repairData = input.repairData || {};
|
|
1774
|
+
const rules = getAllRules();
|
|
1775
|
+
const phase1Ctx = {
|
|
1776
|
+
config: {},
|
|
1777
|
+
configPath: input.configPath,
|
|
1778
|
+
vars: input.vars,
|
|
1779
|
+
providerDeps: {
|
|
1780
|
+
usesMiaodaProvider: false,
|
|
1781
|
+
usesMiaodaSecretProvider: false
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
for (const rule of rules) {
|
|
1785
|
+
if (!failedSet.has(rule.meta.key)) continue;
|
|
1786
|
+
if (rule.meta.repairMode !== "standard") continue;
|
|
1787
|
+
if (rule.meta.dependsOn?.includes("config_syntax_check")) continue;
|
|
1788
|
+
const tRepair = Date.now();
|
|
1789
|
+
try {
|
|
1790
|
+
rule.repair(phase1Ctx);
|
|
1791
|
+
} catch (e) {
|
|
1792
|
+
const msg = e.message;
|
|
1793
|
+
console.error(`rule ${rule.meta.key}: repair (phase1) -> error durationMs=${Date.now() - tRepair} message=${msg}`);
|
|
1794
|
+
outcomes.push({
|
|
1795
|
+
rule: rule.meta.key,
|
|
1796
|
+
status: "error",
|
|
1797
|
+
message: "repair threw: " + msg
|
|
1798
|
+
});
|
|
1799
|
+
aborted = true;
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
console.error(`rule ${rule.meta.key}: repair (phase1) -> ok durationMs=${Date.now() - tRepair}`);
|
|
1803
|
+
const tRev = Date.now();
|
|
1804
|
+
try {
|
|
1805
|
+
const v = rule.validate(phase1Ctx);
|
|
1806
|
+
if (v.pass) {
|
|
1807
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase1) -> pass (fixed) durationMs=${Date.now() - tRev}`);
|
|
1808
|
+
outcomes.push({
|
|
1809
|
+
rule: rule.meta.key,
|
|
1810
|
+
status: "fixed"
|
|
1811
|
+
});
|
|
1812
|
+
} else {
|
|
1813
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase1) -> still-broken durationMs=${Date.now() - tRev} after=${v.message ?? ""}`);
|
|
1814
|
+
outcomes.push({
|
|
1815
|
+
rule: rule.meta.key,
|
|
1816
|
+
status: "still-broken",
|
|
1817
|
+
after: v.message
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
} catch (e) {
|
|
1821
|
+
const msg = e.message;
|
|
1822
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase1) -> error durationMs=${Date.now() - tRev} message=${msg}`);
|
|
1823
|
+
outcomes.push({
|
|
1824
|
+
rule: rule.meta.key,
|
|
1825
|
+
status: "error",
|
|
1826
|
+
message: "post-repair validate threw: " + msg
|
|
1827
|
+
});
|
|
1828
|
+
aborted = true;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
const JSON5 = loadJSON5();
|
|
1832
|
+
let config;
|
|
1833
|
+
try {
|
|
1834
|
+
config = JSON5.parse(readFile(input.configPath));
|
|
1835
|
+
} catch (e) {
|
|
1836
|
+
const msg = e.message;
|
|
1837
|
+
console.error(`runRepair: config parse failed message=${msg}`);
|
|
1838
|
+
outcomes.push({
|
|
1839
|
+
rule: "config_syntax_check",
|
|
1840
|
+
status: "error",
|
|
1841
|
+
message: "config parse failed: " + msg
|
|
1842
|
+
});
|
|
1843
|
+
legacy = {
|
|
1844
|
+
success: false,
|
|
1845
|
+
error: "cannot parse config for repair: " + msg
|
|
1846
|
+
};
|
|
1847
|
+
return {
|
|
1848
|
+
legacy,
|
|
1849
|
+
report: finalize$1(outcomes, true)
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
const deps = analyzeProviderDeps(config);
|
|
1853
|
+
const ctx = {
|
|
1854
|
+
config,
|
|
1855
|
+
configPath: input.configPath,
|
|
1856
|
+
vars: input.vars,
|
|
1857
|
+
providerDeps: deps
|
|
1858
|
+
};
|
|
1859
|
+
let configDirty = false;
|
|
1860
|
+
for (const rule of rules) {
|
|
1861
|
+
if (!failedSet.has(rule.meta.key)) continue;
|
|
1862
|
+
if (rule.meta.repairMode !== "standard") continue;
|
|
1863
|
+
if (!rule.meta.dependsOn?.includes("config_syntax_check")) continue;
|
|
1864
|
+
const tRepair = Date.now();
|
|
1865
|
+
try {
|
|
1866
|
+
rule.repair(ctx);
|
|
1867
|
+
} catch (e) {
|
|
1868
|
+
const msg = e.message;
|
|
1869
|
+
console.error(`rule ${rule.meta.key}: repair (phase2) -> error durationMs=${Date.now() - tRepair} message=${msg}`);
|
|
1870
|
+
outcomes.push({
|
|
1871
|
+
rule: rule.meta.key,
|
|
1872
|
+
status: "error",
|
|
1873
|
+
message: "repair threw: " + msg
|
|
1874
|
+
});
|
|
1875
|
+
aborted = true;
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
console.error(`rule ${rule.meta.key}: repair (phase2) -> ok durationMs=${Date.now() - tRepair}`);
|
|
1879
|
+
configDirty = true;
|
|
1880
|
+
const tRev = Date.now();
|
|
1881
|
+
try {
|
|
1882
|
+
const v = rule.validate(ctx);
|
|
1883
|
+
if (v.pass) {
|
|
1884
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase2) -> pass (fixed) durationMs=${Date.now() - tRev}`);
|
|
1885
|
+
outcomes.push({
|
|
1886
|
+
rule: rule.meta.key,
|
|
1887
|
+
status: "fixed"
|
|
1888
|
+
});
|
|
1889
|
+
} else {
|
|
1890
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase2) -> still-broken durationMs=${Date.now() - tRev} after=${v.message ?? ""}`);
|
|
1891
|
+
outcomes.push({
|
|
1892
|
+
rule: rule.meta.key,
|
|
1893
|
+
status: "still-broken",
|
|
1894
|
+
after: v.message
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
} catch (e) {
|
|
1898
|
+
const msg = e.message;
|
|
1899
|
+
console.error(`rule ${rule.meta.key}: revalidate (phase2) -> error durationMs=${Date.now() - tRev} message=${msg}`);
|
|
1900
|
+
outcomes.push({
|
|
1901
|
+
rule: rule.meta.key,
|
|
1902
|
+
status: "error",
|
|
1903
|
+
message: "post-repair validate threw: " + msg
|
|
1904
|
+
});
|
|
1905
|
+
aborted = true;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
if (configDirty) {
|
|
1909
|
+
backupConfigSync(input.configPath);
|
|
1910
|
+
const serialized = JSON.stringify(config, null, 2);
|
|
1911
|
+
writeFile(input.configPath, serialized);
|
|
1912
|
+
console.error(`runRepair: config writeback ok path=${input.configPath} bytes=${serialized.length}`);
|
|
1913
|
+
}
|
|
1914
|
+
if (repairData.secretsContent && input.vars.secretsFilePath) {
|
|
1915
|
+
writeFile(input.vars.secretsFilePath, repairData.secretsContent);
|
|
1916
|
+
console.error(`runRepair: secrets writeback ok path=${input.vars.secretsFilePath} bytes=${repairData.secretsContent.length}`);
|
|
1917
|
+
}
|
|
1918
|
+
if (repairData.providerKeyContent && input.vars.providerFilePath) {
|
|
1919
|
+
writeFile(input.vars.providerFilePath, repairData.providerKeyContent);
|
|
1920
|
+
console.error(`runRepair: provider key writeback ok path=${input.vars.providerFilePath} bytes=${repairData.providerKeyContent.length}`);
|
|
1921
|
+
}
|
|
1922
|
+
if (repairData.restartCommand) {
|
|
1923
|
+
const t = Date.now();
|
|
1924
|
+
try {
|
|
1925
|
+
console.error(`runRepair: restart begin cmd=${JSON.stringify(repairData.restartCommand)}`);
|
|
1926
|
+
shell(repairData.restartCommand, 3e4);
|
|
1927
|
+
console.error(`runRepair: restart ok durationMs=${Date.now() - t}`);
|
|
1928
|
+
} catch (e) {
|
|
1929
|
+
const msg = e.message;
|
|
1930
|
+
console.error(`runRepair: restart failed durationMs=${Date.now() - t} message=${msg}`);
|
|
1931
|
+
legacy = {
|
|
1932
|
+
success: false,
|
|
1933
|
+
error: "restart command failed: " + msg
|
|
1934
|
+
};
|
|
1935
|
+
return {
|
|
1936
|
+
legacy,
|
|
1937
|
+
report: finalize$1(outcomes, true)
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
console.error(`runRepair: end success=true totalMs=${Date.now() - tStart}`);
|
|
1942
|
+
legacy = { success: true };
|
|
1943
|
+
return {
|
|
1944
|
+
legacy,
|
|
1945
|
+
report: finalize$1(outcomes, aborted)
|
|
1946
|
+
};
|
|
1947
|
+
} catch (e) {
|
|
1948
|
+
const msg = e.message;
|
|
1949
|
+
console.error(`runRepair: end success=false totalMs=${Date.now() - tStart} message=${msg}`);
|
|
1950
|
+
legacy = {
|
|
1951
|
+
success: false,
|
|
1952
|
+
error: "repair failed: " + msg
|
|
1953
|
+
};
|
|
1954
|
+
return {
|
|
1955
|
+
legacy,
|
|
1956
|
+
report: finalize$1(outcomes, true)
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
function finalize$1(results, aborted) {
|
|
1961
|
+
const summary = {
|
|
1962
|
+
pass: 0,
|
|
1963
|
+
failed: 0,
|
|
1964
|
+
fixed: 0,
|
|
1965
|
+
stillBroken: 0,
|
|
1966
|
+
skipped: 0,
|
|
1967
|
+
error: 0,
|
|
1968
|
+
unknown: 0
|
|
1969
|
+
};
|
|
1970
|
+
for (const r of results) switch (r.status) {
|
|
1971
|
+
case "pass":
|
|
1972
|
+
summary.pass++;
|
|
1973
|
+
break;
|
|
1974
|
+
case "failed":
|
|
1975
|
+
summary.failed++;
|
|
1976
|
+
break;
|
|
1977
|
+
case "fixed":
|
|
1978
|
+
summary.fixed++;
|
|
1979
|
+
break;
|
|
1980
|
+
case "still-broken":
|
|
1981
|
+
summary.stillBroken++;
|
|
1982
|
+
break;
|
|
1983
|
+
case "skipped":
|
|
1984
|
+
summary.skipped++;
|
|
1985
|
+
break;
|
|
1986
|
+
case "error":
|
|
1987
|
+
summary.error++;
|
|
1988
|
+
break;
|
|
1989
|
+
case "unknown":
|
|
1990
|
+
summary.unknown++;
|
|
1991
|
+
break;
|
|
1992
|
+
}
|
|
1993
|
+
return {
|
|
1994
|
+
results,
|
|
1995
|
+
summary,
|
|
1996
|
+
aborted
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
//#endregion
|
|
2000
|
+
//#region src/paths.ts
|
|
2001
|
+
/**
|
|
2002
|
+
* Central directory for all ephemeral diagnose/reset artifacts: task status
|
|
2003
|
+
* files (`reset-<taskId>.json`) and human-readable step logs
|
|
2004
|
+
* (`reset-<taskId>.log`). Having everything under one dir makes debugging a
|
|
2005
|
+
* stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
|
|
2006
|
+
* run, and each run's log is right next to its state.
|
|
2007
|
+
*/
|
|
2008
|
+
const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
|
|
2009
|
+
function resetResultFile(taskId) {
|
|
2010
|
+
return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
|
|
2011
|
+
}
|
|
2012
|
+
function resetLogFile(taskId) {
|
|
2013
|
+
return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
|
|
2014
|
+
}
|
|
2015
|
+
/** Sandbox workspace root where openclaw config + agent state lives. */
|
|
2016
|
+
const WORKSPACE_DIR = "/home/gem/workspace/agent";
|
|
2017
|
+
/** File containing the provider key used by the openclaw miaoda provider. */
|
|
2018
|
+
const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
|
|
2019
|
+
/** File containing the miaoda openclaw secrets JSON. */
|
|
2020
|
+
const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
|
|
2021
|
+
/** Absolute path to the openclaw config JSON. */
|
|
2022
|
+
const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
|
|
2023
|
+
//#endregion
|
|
2024
|
+
//#region src/run-log.ts
|
|
2025
|
+
let currentRunContext;
|
|
2026
|
+
/**
|
|
2027
|
+
* Install the run context for this CLI execution. Must be called once at
|
|
2028
|
+
* the top of `main()` before any subcommand work; idempotent (last call
|
|
2029
|
+
* wins, but `main` only calls it once).
|
|
2030
|
+
*
|
|
2031
|
+
* Resolution order for `runId`:
|
|
2032
|
+
* 1. `OPENCLAW_RUN_ID` env var — set by a parent CLI process (e.g. the
|
|
2033
|
+
* `reset --async` dispatcher exports it before spawning the worker)
|
|
2034
|
+
* so cli.log lines for both processes carry the same `[run=...]`
|
|
2035
|
+
* tag. Any non-empty value is honoured verbatim.
|
|
2036
|
+
* 2. `--trace-id=<id>` flag — caller-supplied upstream trace id.
|
|
2037
|
+
* 3. Generated locally as `gen-<8 hex>`.
|
|
2038
|
+
*
|
|
2039
|
+
* Safe to call without any input — runId falls through to the generated
|
|
2040
|
+
* branch and both metadata fields stay undefined.
|
|
2041
|
+
*/
|
|
2042
|
+
function setRunContext(opts) {
|
|
2043
|
+
const inherited = process.env.OPENCLAW_RUN_ID;
|
|
2044
|
+
if (inherited && inherited !== "") currentRunContext = {
|
|
2045
|
+
runId: inherited,
|
|
2046
|
+
caller: opts.caller,
|
|
2047
|
+
traceId: opts.traceId,
|
|
2048
|
+
generated: false
|
|
2049
|
+
};
|
|
2050
|
+
else if (opts.traceId) currentRunContext = {
|
|
2051
|
+
runId: opts.traceId,
|
|
2052
|
+
caller: opts.caller,
|
|
2053
|
+
traceId: opts.traceId,
|
|
2054
|
+
generated: false
|
|
2055
|
+
};
|
|
2056
|
+
else currentRunContext = {
|
|
2057
|
+
runId: "gen-" + (0, node_crypto.randomBytes)(4).toString("hex"),
|
|
2058
|
+
caller: opts.caller,
|
|
2059
|
+
traceId: void 0,
|
|
2060
|
+
generated: true
|
|
2061
|
+
};
|
|
2062
|
+
return currentRunContext;
|
|
2063
|
+
}
|
|
2064
|
+
function getRunContext() {
|
|
2065
|
+
return currentRunContext;
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Format the run-scope tag the stderr mirror injects into cli.log lines.
|
|
2069
|
+
* Empty string when no context is set (mirror falls back to plain
|
|
2070
|
+
* timestamped lines, preserving pre-context startup logging).
|
|
2071
|
+
*/
|
|
2072
|
+
function runTag(rc = currentRunContext) {
|
|
2073
|
+
if (!rc) return "";
|
|
2074
|
+
const parts = [`run=${rc.runId}`];
|
|
2075
|
+
if (rc.caller) parts.push(`caller=${rc.caller}`);
|
|
2076
|
+
return `[${parts.join(" ")}]`;
|
|
2077
|
+
}
|
|
2078
|
+
//#endregion
|
|
2079
|
+
//#region src/logger.ts
|
|
2080
|
+
/**
|
|
2081
|
+
* Shared CLI log file. Every log line the CLI emits — whether through
|
|
2082
|
+
* `console.error` (rules, helpers, errors) or through the per-task
|
|
2083
|
+
* `makeLogger` (reset worker) — is tee'd here so operators have a single
|
|
2084
|
+
* file to tail when diagnosing a sandbox.
|
|
2085
|
+
*
|
|
2086
|
+
* `/tmp` is ephemeral on sandbox restart; we rely on that for rotation
|
|
2087
|
+
* (no size-based rotation implemented).
|
|
2088
|
+
*/
|
|
2089
|
+
const CLI_LOG_FILE = "/tmp/openclaw-diagnose/cli.log";
|
|
2090
|
+
/** Append one line to the shared cli.log. Swallows any fs error —
|
|
2091
|
+
* logging must never break the business flow. */
|
|
2092
|
+
function appendCliLog(line) {
|
|
2093
|
+
try {
|
|
2094
|
+
const dir = node_path.default.dirname(CLI_LOG_FILE);
|
|
2095
|
+
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
2096
|
+
node_fs.default.appendFileSync(CLI_LOG_FILE, line);
|
|
2097
|
+
} catch {}
|
|
2098
|
+
}
|
|
2099
|
+
let stderrMirrorInstalled = false;
|
|
2100
|
+
/**
|
|
2101
|
+
* Install a process-wide `console.error` interceptor. Always tees each
|
|
2102
|
+
* line to cli.log; the stderr passthrough is opt-in via `stderrEnabled`.
|
|
2103
|
+
*
|
|
2104
|
+
* Default (`stderrEnabled: false`): the terminal sees ONLY this command's
|
|
2105
|
+
* stdout (typically a single JSON result line). Lifecycle / progress /
|
|
2106
|
+
* error logs are fully captured in `/tmp/openclaw-diagnose/cli.log`,
|
|
2107
|
+
* which is the canonical debugging surface and already carries
|
|
2108
|
+
* timestamps and `[run=...]` tags.
|
|
2109
|
+
*
|
|
2110
|
+
* Verbose (`stderrEnabled: true`, opt in with the top-level `-x` flag):
|
|
2111
|
+
* lifecycle logs also stream to stderr in real time. Useful when
|
|
2112
|
+
* iterating on a single sandbox.
|
|
2113
|
+
*
|
|
2114
|
+
* Idempotent: calling twice is a no-op (first install wins). Tests that
|
|
2115
|
+
* need to swap mode should call `resetStderrMirrorForTests()` first.
|
|
2116
|
+
*
|
|
2117
|
+
* Why console.error (and not console.log): the CLI's stdout carries the
|
|
2118
|
+
* structured JSON result protocol consumed by sandbox_console and other
|
|
2119
|
+
* callers — any log line on stdout would corrupt JSON parsing.
|
|
2120
|
+
*/
|
|
2121
|
+
function installStderrMirror(opts = {}) {
|
|
2122
|
+
if (stderrMirrorInstalled) return;
|
|
2123
|
+
stderrMirrorInstalled = true;
|
|
2124
|
+
const stderrEnabled = opts.stderrEnabled ?? false;
|
|
2125
|
+
const original = console.error.bind(console);
|
|
2126
|
+
console.error = (...args) => {
|
|
2127
|
+
if (stderrEnabled) original(...args);
|
|
2128
|
+
const body = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
|
|
2129
|
+
const tag = runTag();
|
|
2130
|
+
appendCliLog(`[${(/* @__PURE__ */ new Date()).toISOString()}]${tag ? ` ${tag}` : ""} ${body}\n`);
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
function safeStringify(v) {
|
|
2134
|
+
try {
|
|
2135
|
+
return JSON.stringify(v);
|
|
2136
|
+
} catch {
|
|
2137
|
+
return String(v);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
function makeLogger(logFile) {
|
|
2141
|
+
try {
|
|
2142
|
+
const dir = node_path.default.dirname(logFile);
|
|
2143
|
+
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
2144
|
+
} catch {}
|
|
2145
|
+
return (msg) => {
|
|
2146
|
+
const tag = runTag();
|
|
2147
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}]${tag ? ` ${tag}` : ""} ${msg}\n`;
|
|
2148
|
+
try {
|
|
2149
|
+
node_fs.default.appendFileSync(logFile, line);
|
|
2150
|
+
} catch {}
|
|
2151
|
+
appendCliLog(line);
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
//#endregion
|
|
2155
|
+
//#region src/reset-async.ts
|
|
2156
|
+
/**
|
|
2157
|
+
* Start an async reset task: spawn a detached child process and return the taskId.
|
|
2158
|
+
*
|
|
2159
|
+
* The child process runs: node cli.js reset --worker --task-id=xxx --ctx=base64
|
|
2160
|
+
*/
|
|
2161
|
+
function startAsyncReset(ctxBase64) {
|
|
2162
|
+
const taskId = (0, node_crypto.randomUUID)();
|
|
2163
|
+
const resultFile = resetResultFile(taskId);
|
|
2164
|
+
const log = makeLogger(resetLogFile(taskId));
|
|
2165
|
+
log(`=== startAsyncReset spawning worker for taskId=${taskId} ===`);
|
|
2166
|
+
const initial = {
|
|
2167
|
+
status: "running",
|
|
2168
|
+
step: 0,
|
|
2169
|
+
totalSteps: 9,
|
|
2170
|
+
progress: "初始化...",
|
|
2171
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2172
|
+
};
|
|
2173
|
+
const tmpPath = resultFile + ".tmp";
|
|
2174
|
+
const dir = node_path.default.dirname(resultFile);
|
|
2175
|
+
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
2176
|
+
node_fs.default.writeFileSync(tmpPath, JSON.stringify(initial), "utf-8");
|
|
2177
|
+
moveSafe(tmpPath, resultFile);
|
|
2178
|
+
const rc = getRunContext();
|
|
2179
|
+
const childEnv = { ...process.env };
|
|
2180
|
+
if (rc?.runId) childEnv.OPENCLAW_RUN_ID = rc.runId;
|
|
2181
|
+
const child = (0, node_child_process.spawn)(process.execPath, [
|
|
2182
|
+
process.argv[1],
|
|
2183
|
+
"reset",
|
|
2184
|
+
"--worker",
|
|
2185
|
+
`--task-id=${taskId}`,
|
|
2186
|
+
`--ctx=${ctxBase64}`
|
|
2187
|
+
], {
|
|
2188
|
+
detached: true,
|
|
2189
|
+
stdio: "ignore",
|
|
2190
|
+
env: childEnv
|
|
2191
|
+
});
|
|
2192
|
+
child.on("error", (err) => {
|
|
2193
|
+
log(`FATAL worker failed to start: ${err.message}`);
|
|
2194
|
+
const failResult = {
|
|
2195
|
+
status: "failed",
|
|
2196
|
+
step: 0,
|
|
2197
|
+
totalSteps: 9,
|
|
2198
|
+
progress: "Worker process failed to start",
|
|
2199
|
+
error: err.message,
|
|
2200
|
+
startedAt: initial.startedAt,
|
|
2201
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2202
|
+
};
|
|
2203
|
+
const errTmpPath = resultFile + ".tmp";
|
|
2204
|
+
node_fs.default.writeFileSync(errTmpPath, JSON.stringify(failResult));
|
|
2205
|
+
moveSafe(errTmpPath, resultFile);
|
|
2206
|
+
});
|
|
2207
|
+
child.unref();
|
|
2208
|
+
log(`spawned worker pid=${child.pid}`);
|
|
1799
2209
|
return { taskId };
|
|
1800
2210
|
}
|
|
1801
2211
|
//#endregion
|
|
2212
|
+
//#region src/oss/fetchWithDiag.ts
|
|
2213
|
+
async function fetchWithDiag(url, opts = {}) {
|
|
2214
|
+
const label = opts.label ?? originOf(url);
|
|
2215
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
2216
|
+
const start = Date.now();
|
|
2217
|
+
const ac = new AbortController();
|
|
2218
|
+
const timer = timeoutMs > 0 && Number.isFinite(timeoutMs) ? setTimeout(() => ac.abort(), timeoutMs) : void 0;
|
|
2219
|
+
try {
|
|
2220
|
+
return await fetch(url, { signal: ac.signal });
|
|
2221
|
+
} catch (e) {
|
|
2222
|
+
const durationMs = Date.now() - start;
|
|
2223
|
+
const causeStr = e.name === "AbortError" || ac.signal.aborted && timeoutMs > 0 ? `request aborted after ${timeoutMs}ms (timeout)` : describeCause(e.cause) || e.message;
|
|
2224
|
+
throw new Error(`fetch ${label} failed: ${causeStr} (url=${redactUrl(url)} durationMs=${durationMs})`);
|
|
2225
|
+
} finally {
|
|
2226
|
+
if (timer) clearTimeout(timer);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
/** Walk the Error.cause chain and produce a single-line summary like
|
|
2230
|
+
* `ENOTFOUND getaddrinfo ENOTFOUND oss.example.com`. */
|
|
2231
|
+
function describeCause(c, depth = 0) {
|
|
2232
|
+
if (!c || depth > 3) return "";
|
|
2233
|
+
if (c instanceof Error) {
|
|
2234
|
+
const code = c.code;
|
|
2235
|
+
const head = code ? `${code} ${c.message}` : c.message;
|
|
2236
|
+
const inner = describeCause(c.cause, depth + 1);
|
|
2237
|
+
return inner ? `${head} <- ${inner}` : head;
|
|
2238
|
+
}
|
|
2239
|
+
return String(c);
|
|
2240
|
+
}
|
|
2241
|
+
/** Strip query string — OSS signed URLs put the auth token there. Keeps
|
|
2242
|
+
* protocol/host/path so the operator can tell *which* OSS bucket / object
|
|
2243
|
+
* was being fetched. */
|
|
2244
|
+
function redactUrl(raw) {
|
|
2245
|
+
try {
|
|
2246
|
+
const u = new URL(raw);
|
|
2247
|
+
const tail = u.search ? "?<redacted>" : "";
|
|
2248
|
+
return `${u.protocol}//${u.host}${u.pathname}${tail}`;
|
|
2249
|
+
} catch {
|
|
2250
|
+
return "<invalid-url>";
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
function originOf(raw) {
|
|
2254
|
+
try {
|
|
2255
|
+
return new URL(raw).host;
|
|
2256
|
+
} catch {
|
|
2257
|
+
return "unknown-host";
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
//#endregion
|
|
1802
2261
|
//#region src/oss/fetchManifest.ts
|
|
1803
2262
|
const MANIFEST_PREFIX = "builtin/manifests/openclaw/recommended/";
|
|
1804
2263
|
const MANIFEST_SUFFIX = ".json";
|
|
@@ -1810,10 +2269,24 @@ async function fetchManifest(ossFileMap, tag) {
|
|
|
1810
2269
|
const availStr = available.length ? available.join(", ") : "(none)";
|
|
1811
2270
|
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
2271
|
}
|
|
1813
|
-
const
|
|
1814
|
-
|
|
2272
|
+
const label = `openclaw manifest tag=${tag}`;
|
|
2273
|
+
const res = await fetchWithDiag(url, { label });
|
|
2274
|
+
if (!res.ok) {
|
|
2275
|
+
const body = await readPreview(res);
|
|
2276
|
+
throw new Error(`${label}: HTTP ${res.status} ${res.statusText}` + (body ? ` body=${body}` : ""));
|
|
2277
|
+
}
|
|
1815
2278
|
return await res.json();
|
|
1816
2279
|
}
|
|
2280
|
+
/** Best-effort body preview for HTTP-error diagnostics. OSS errors are
|
|
2281
|
+
* XML; first 256 chars usually contain the <Code> + <Message> we care
|
|
2282
|
+
* about. Failures here are swallowed (logging-only, not load-bearing). */
|
|
2283
|
+
async function readPreview(res) {
|
|
2284
|
+
try {
|
|
2285
|
+
return (await res.text()).slice(0, 256);
|
|
2286
|
+
} catch {
|
|
2287
|
+
return "";
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
1817
2290
|
async function downloadWithCache(pkg, ossFileMap, opts = {}) {
|
|
1818
2291
|
const cacheRoot = opts.cacheRoot ?? "/tmp/openclaw-diagnose/resources";
|
|
1819
2292
|
const shortHash = pkg.shasum.slice(0, 16);
|
|
@@ -1827,9 +2300,19 @@ async function downloadWithCache(pkg, ossFileMap, opts = {}) {
|
|
|
1827
2300
|
const expected = pkg.integrity.slice(7);
|
|
1828
2301
|
const tmpFile = node_path.default.join(destDir, `.tmp.${process.pid}.${node_crypto.default.randomBytes(4).toString("hex")}`);
|
|
1829
2302
|
try {
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
|
|
2303
|
+
const label = `download ${pkg.ossKey}`;
|
|
2304
|
+
const res = await fetchWithDiag(url, {
|
|
2305
|
+
label,
|
|
2306
|
+
timeoutMs: 6e4
|
|
2307
|
+
});
|
|
2308
|
+
if (!res.ok) {
|
|
2309
|
+
let preview = "";
|
|
2310
|
+
try {
|
|
2311
|
+
preview = (await res.text()).slice(0, 256);
|
|
2312
|
+
} catch {}
|
|
2313
|
+
throw new Error(`${label}: HTTP ${res.status} ${res.statusText}` + (preview ? ` body=${preview}` : ""));
|
|
2314
|
+
}
|
|
2315
|
+
if (!res.body) throw new Error(`${label}: empty body`);
|
|
1833
2316
|
const hasher = node_crypto.default.createHash("sha512");
|
|
1834
2317
|
const source = node_stream.Readable.fromWeb(res.body);
|
|
1835
2318
|
async function* teeAndHash(src) {
|
|
@@ -2354,16 +2837,6 @@ function writeSecretsAndRestart(vars, resetData, configDir, log) {
|
|
|
2354
2837
|
log(`restart.sh done in ${Date.now() - t}ms`);
|
|
2355
2838
|
} else log(`no restart.sh at ${restartScript}, skip`);
|
|
2356
2839
|
}
|
|
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
2840
|
async function runReset(input, taskId, resultFile) {
|
|
2368
2841
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2369
2842
|
const { configPath, vars, resetData } = input;
|
|
@@ -2371,6 +2844,7 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2371
2844
|
const stagedDir = node_path.default.join(DIAGNOSE_DIR, `reset-${taskId}-template`);
|
|
2372
2845
|
let currentStep = 0;
|
|
2373
2846
|
let stepStartedAt = Date.now();
|
|
2847
|
+
const stepResults = [];
|
|
2374
2848
|
const log = makeLogger(resetLogFile(taskId));
|
|
2375
2849
|
log(`=== reset started, taskId=${taskId}, pid=${process.pid} ===`);
|
|
2376
2850
|
log(`configPath=${configPath}, configDir=${configDir}, stagedDir=${stagedDir}`);
|
|
@@ -2379,7 +2853,12 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2379
2853
|
const err = "resetData.ossFileMap missing or empty";
|
|
2380
2854
|
log(`ERROR: ${err}`);
|
|
2381
2855
|
markFailed(resultFile, 0, err, startedAt);
|
|
2382
|
-
|
|
2856
|
+
return {
|
|
2857
|
+
success: false,
|
|
2858
|
+
steps: stepResults,
|
|
2859
|
+
error: err,
|
|
2860
|
+
failedStep: 0
|
|
2861
|
+
};
|
|
2383
2862
|
}
|
|
2384
2863
|
let openclawTag;
|
|
2385
2864
|
if (resetData.openclawTag) openclawTag = resetData.openclawTag;
|
|
@@ -2389,7 +2868,12 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2389
2868
|
const err = e.message;
|
|
2390
2869
|
log(`ERROR: ${err}`);
|
|
2391
2870
|
markFailed(resultFile, 0, err, startedAt);
|
|
2392
|
-
|
|
2871
|
+
return {
|
|
2872
|
+
success: false,
|
|
2873
|
+
steps: stepResults,
|
|
2874
|
+
error: err,
|
|
2875
|
+
failedStep: 0
|
|
2876
|
+
};
|
|
2393
2877
|
}
|
|
2394
2878
|
log(`openclawTag=${openclawTag}`);
|
|
2395
2879
|
process.on("uncaughtException", (err) => {
|
|
@@ -2402,9 +2886,19 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2402
2886
|
markFailed(resultFile, currentStep, `unhandled rejection: ${reason}`, startedAt);
|
|
2403
2887
|
process.exit(1);
|
|
2404
2888
|
});
|
|
2405
|
-
/** Advance to the next step,
|
|
2889
|
+
/** Advance to the next step, recording the previous step's success
|
|
2890
|
+
* duration and updating the on-disk progress file. */
|
|
2406
2891
|
const step = (n) => {
|
|
2407
|
-
if (currentStep > 0)
|
|
2892
|
+
if (currentStep > 0) {
|
|
2893
|
+
const dur = Date.now() - stepStartedAt;
|
|
2894
|
+
log(`step ${currentStep} "${STEPS[currentStep - 1]}" done in ${dur}ms`);
|
|
2895
|
+
stepResults.push({
|
|
2896
|
+
step: currentStep,
|
|
2897
|
+
name: STEPS[currentStep - 1],
|
|
2898
|
+
status: "ok",
|
|
2899
|
+
durationMs: dur
|
|
2900
|
+
});
|
|
2901
|
+
}
|
|
2408
2902
|
currentStep = n;
|
|
2409
2903
|
stepStartedAt = Date.now();
|
|
2410
2904
|
log(`--- step ${n}/${TOTAL_STEPS}: ${STEPS[n - 1]} ---`);
|
|
@@ -2430,14 +2924,38 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2430
2924
|
await step8InstallExtensions(openclawTag, ossFileMap, log);
|
|
2431
2925
|
step(9);
|
|
2432
2926
|
writeSecretsAndRestart(vars, resetData, configDir, log);
|
|
2433
|
-
|
|
2927
|
+
const lastDur = Date.now() - stepStartedAt;
|
|
2928
|
+
log(`step 9 "${STEPS[8]}" done in ${lastDur}ms`);
|
|
2929
|
+
stepResults.push({
|
|
2930
|
+
step: 9,
|
|
2931
|
+
name: STEPS[8],
|
|
2932
|
+
status: "ok",
|
|
2933
|
+
durationMs: lastDur
|
|
2934
|
+
});
|
|
2434
2935
|
log("=== reset completed successfully ===");
|
|
2435
2936
|
markDone(resultFile, startedAt);
|
|
2937
|
+
return {
|
|
2938
|
+
success: true,
|
|
2939
|
+
steps: stepResults
|
|
2940
|
+
};
|
|
2436
2941
|
} catch (e) {
|
|
2437
2942
|
const err = e.message;
|
|
2438
|
-
|
|
2943
|
+
const dur = Date.now() - stepStartedAt;
|
|
2944
|
+
log(`ERROR in step ${currentStep} "${STEPS[currentStep - 1] ?? "init"}" after ${dur}ms: ${err}\n${e.stack ?? ""}`);
|
|
2945
|
+
stepResults.push({
|
|
2946
|
+
step: currentStep,
|
|
2947
|
+
name: STEPS[currentStep - 1] ?? "init",
|
|
2948
|
+
status: "fail",
|
|
2949
|
+
durationMs: dur,
|
|
2950
|
+
error: err
|
|
2951
|
+
});
|
|
2439
2952
|
markFailed(resultFile, currentStep, err, startedAt);
|
|
2440
|
-
|
|
2953
|
+
return {
|
|
2954
|
+
success: false,
|
|
2955
|
+
steps: stepResults,
|
|
2956
|
+
error: err,
|
|
2957
|
+
failedStep: currentStep
|
|
2958
|
+
};
|
|
2441
2959
|
} finally {
|
|
2442
2960
|
try {
|
|
2443
2961
|
node_fs.default.rmSync(stagedDir, {
|
|
@@ -2504,38 +3022,24 @@ function resolveOssFileMap(args) {
|
|
|
2504
3022
|
throw new Error("ossFileMap missing: provide --oss_file_map flag, ctx.install.ossFileMap, or resetData.ossFileMap");
|
|
2505
3023
|
}
|
|
2506
3024
|
//#endregion
|
|
2507
|
-
//#region src/innerapi/
|
|
3025
|
+
//#region src/innerapi/client.ts
|
|
2508
3026
|
/**
|
|
2509
|
-
*
|
|
2510
|
-
*
|
|
2511
|
-
* Mirrors the proven pattern in
|
|
2512
|
-
* `packages/openclaw/extensions/miaoda/src/shared/innerapi-client.ts`:
|
|
3027
|
+
* Shared innerapi HTTP client + path constants for `openclaw.*` calls.
|
|
2513
3028
|
*
|
|
2514
3029
|
* - `baseURL` from env `FORCE_AUTHN_INNERAPI_DOMAIN` (injected into every
|
|
2515
3030
|
* openclaw sandbox).
|
|
2516
3031
|
* - `platform: { enabled, tokenProvider: { type: 'file' } }` — the platform
|
|
2517
3032
|
* plugin auto-attaches the sandbox's identity JWT loaded from the
|
|
2518
3033
|
* 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.
|
|
3034
|
+
* - POST `/api/v1/studio/innerapi/integration_apis/call` with body
|
|
3035
|
+
* `{ apiName, bizType, input }` is the universal dispatcher; specific
|
|
3036
|
+
* apiName values (e.g. `openclaw.get_doctor_ctx`,
|
|
3037
|
+
* `openclaw.report_doctor_result`) are owned by individual call-site
|
|
3038
|
+
* modules.
|
|
2533
3039
|
*/
|
|
2534
3040
|
const INNERAPI_CALL_PATH = "/api/v1/studio/innerapi/integration_apis/call";
|
|
2535
|
-
const API_NAME = "openclaw.get_doctor_ctx";
|
|
2536
3041
|
const BIZ_TYPE = "openclaw";
|
|
2537
3042
|
const API_TIMEOUT_MS = 3e4;
|
|
2538
|
-
const MAX_LOG_BODY = 500;
|
|
2539
3043
|
let clientInstance = null;
|
|
2540
3044
|
function getHttpClient() {
|
|
2541
3045
|
if (!clientInstance) {
|
|
@@ -2552,6 +3056,35 @@ function getHttpClient() {
|
|
|
2552
3056
|
}
|
|
2553
3057
|
return clientInstance;
|
|
2554
3058
|
}
|
|
3059
|
+
//#endregion
|
|
3060
|
+
//#region src/innerapi/fetchCtx.ts
|
|
3061
|
+
/**
|
|
3062
|
+
* CLI-side client for studio_server's `openclaw.get_doctor_ctx` inner API.
|
|
3063
|
+
*
|
|
3064
|
+
* Mirrors the proven pattern in
|
|
3065
|
+
* `packages/openclaw/extensions/miaoda/src/shared/innerapi-client.ts`:
|
|
3066
|
+
*
|
|
3067
|
+
* - `baseURL` from env `FORCE_AUTHN_INNERAPI_DOMAIN` (injected into every
|
|
3068
|
+
* openclaw sandbox).
|
|
3069
|
+
* - `platform: { enabled, tokenProvider: { type: 'file' } }` — the platform
|
|
3070
|
+
* plugin auto-attaches the sandbox's identity JWT loaded from the
|
|
3071
|
+
* rootfs token file. Same auth that the miaoda extension already uses.
|
|
3072
|
+
* - POST `/api/v1/studio/innerapi/integration_apis/call`
|
|
3073
|
+
* body = { apiName: 'openclaw.get_doctor_ctx', input: {}, bizType: 'openclaw' }
|
|
3074
|
+
* — the server-side APICall dispatches by `apiName` to
|
|
3075
|
+
* `GetDoctorCtxAPICall.Execute` whose `Name()` returns that string.
|
|
3076
|
+
* - Response envelope: { status_code, error_msg?, data: { success, output, ... } }.
|
|
3077
|
+
* `status_code` is a *string* ('0' = success).
|
|
3078
|
+
* Actual DoctorCtx lives in `data.output`.
|
|
3079
|
+
* - `x-tt-logid` header is logged on every failure path for cross-service
|
|
3080
|
+
* traceability.
|
|
3081
|
+
*
|
|
3082
|
+
* On HTTP 401 (sandbox identity token expired/invalid) we `process.exit(77)`
|
|
3083
|
+
* instead of throwing — the outer catch in `index.ts` cannot then mask auth
|
|
3084
|
+
* failure as a generic "Error: ...". Caller (e.g. sandbox_console) sees the
|
|
3085
|
+
* exit code and can refresh the token + retry.
|
|
3086
|
+
*/
|
|
3087
|
+
const API_NAME$1 = "openclaw.get_doctor_ctx";
|
|
2555
3088
|
/**
|
|
2556
3089
|
* Fetch the sandbox's DoctorCtx by calling the innerapi's generic
|
|
2557
3090
|
* `integration_apis/call` dispatcher with apiName=openclaw.get_doctor_ctx.
|
|
@@ -2559,14 +3092,19 @@ function getHttpClient() {
|
|
|
2559
3092
|
* Throws on HTTP (non-401) / decode / business errors. On 401 calls
|
|
2560
3093
|
* `process.exit(77)` directly.
|
|
2561
3094
|
*/
|
|
2562
|
-
async function fetchCtxViaInnerApi() {
|
|
3095
|
+
async function fetchCtxViaInnerApi(opts = {}) {
|
|
2563
3096
|
const client = getHttpClient();
|
|
3097
|
+
const input = {};
|
|
3098
|
+
if (opts.populate !== void 0) input.populate = opts.populate;
|
|
3099
|
+
if (opts.caller) input.caller = opts.caller;
|
|
3100
|
+
if (opts.traceId) input.traceId = opts.traceId;
|
|
2564
3101
|
const body = {
|
|
2565
|
-
apiName: API_NAME,
|
|
2566
|
-
input
|
|
3102
|
+
apiName: API_NAME$1,
|
|
3103
|
+
input,
|
|
2567
3104
|
bizType: BIZ_TYPE
|
|
2568
3105
|
};
|
|
2569
3106
|
const start = Date.now();
|
|
3107
|
+
console.error(`fetchCtx: start populate=${JSON.stringify(opts.populate ?? {})}${opts.caller ? ` caller=${opts.caller}` : ""}${opts.traceId ? ` traceId=${opts.traceId}` : ""}`);
|
|
2570
3108
|
const headers = { "Content-Type": "application/json" };
|
|
2571
3109
|
const ttEnv = process.env.X_TT_ENV;
|
|
2572
3110
|
if (ttEnv) headers["x-tt-env"] = ttEnv;
|
|
@@ -2596,7 +3134,7 @@ async function fetchCtxViaInnerApi() {
|
|
|
2596
3134
|
}
|
|
2597
3135
|
let preview = "";
|
|
2598
3136
|
try {
|
|
2599
|
-
preview = (await response.text()).slice(0,
|
|
3137
|
+
preview = (await response.text()).slice(0, 500);
|
|
2600
3138
|
} catch {}
|
|
2601
3139
|
throw new Error(`fetchCtxViaInnerApi HTTP ${response.status} ${response.statusText} (logID: ${logId}, durationMs: ${durationMs})${preview ? ` body=${preview}` : ""}`);
|
|
2602
3140
|
}
|
|
@@ -2610,6 +3148,7 @@ async function fetchCtxViaInnerApi() {
|
|
|
2610
3148
|
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
3149
|
const output = envelope.data?.output;
|
|
2612
3150
|
if (!output || typeof output !== "object") throw new Error(`fetchCtxViaInnerApi empty/invalid output (logID: ${logId}, durationMs: ${durationMs})`);
|
|
3151
|
+
console.error(`fetchCtx: ok logId=${logId} durationMs=${durationMs} groups=[${Object.keys(output).join(",")}]`);
|
|
2613
3152
|
return output;
|
|
2614
3153
|
}
|
|
2615
3154
|
//#endregion
|
|
@@ -2628,26 +3167,40 @@ async function fetchCtxViaInnerApi() {
|
|
|
2628
3167
|
*/
|
|
2629
3168
|
function normalizeCtx(raw) {
|
|
2630
3169
|
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
|
-
|
|
3170
|
+
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") {
|
|
3171
|
+
const templateVars = r.app.templateVars && typeof r.app.templateVars === "object" ? r.app.templateVars : r.reset.templateVars ?? {};
|
|
3172
|
+
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;
|
|
3173
|
+
return {
|
|
3174
|
+
app: {
|
|
3175
|
+
...fillApp(r.app),
|
|
3176
|
+
templateVars,
|
|
3177
|
+
recommendedOpenclawTag
|
|
3178
|
+
},
|
|
3179
|
+
install: {
|
|
3180
|
+
openclawTag: r.install.openclawTag,
|
|
3181
|
+
ossFileMap: r.install.ossFileMap ?? {}
|
|
3182
|
+
},
|
|
3183
|
+
secrets: {
|
|
3184
|
+
secretsContent: r.secrets.secretsContent ?? "",
|
|
3185
|
+
providerKeyContent: r.secrets.providerKeyContent ?? ""
|
|
3186
|
+
},
|
|
3187
|
+
reset: {
|
|
3188
|
+
templateVars: r.reset.templateVars ?? {},
|
|
3189
|
+
coreBackup: r.reset.coreBackup
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
}
|
|
2646
3193
|
const vars = r.vars ?? {};
|
|
2647
3194
|
const resetData = r.resetData ?? {};
|
|
2648
3195
|
const repairData = r.repairData ?? {};
|
|
3196
|
+
const legacyTemplateVars = vars.templateVars && typeof vars.templateVars === "object" ? vars.templateVars : resetData.templateVars ?? r.templateVars ?? {};
|
|
3197
|
+
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
3198
|
return {
|
|
2650
|
-
app:
|
|
3199
|
+
app: {
|
|
3200
|
+
...fillApp(vars),
|
|
3201
|
+
templateVars: legacyTemplateVars,
|
|
3202
|
+
recommendedOpenclawTag: legacyRecommendedTag
|
|
3203
|
+
},
|
|
2651
3204
|
install: {
|
|
2652
3205
|
openclawTag: r.install?.openclawTag ?? r.openclawTag,
|
|
2653
3206
|
ossFileMap: r.install?.ossFileMap ?? resetData.ossFileMap ?? r.ossFileMap ?? {}
|
|
@@ -2700,11 +3253,15 @@ function fillApp(src) {
|
|
|
2700
3253
|
function buildCheckInput(raw, configPathOverride) {
|
|
2701
3254
|
const r = raw ?? {};
|
|
2702
3255
|
if (r.configPath && r.vars) {
|
|
2703
|
-
|
|
3256
|
+
const out = configPathOverride ? {
|
|
2704
3257
|
...r,
|
|
2705
3258
|
configPath: configPathOverride
|
|
3259
|
+
} : { ...r };
|
|
3260
|
+
if (out.vars && !out.vars.templateVars) out.vars = {
|
|
3261
|
+
...out.vars,
|
|
3262
|
+
templateVars: out.templateVars ?? {}
|
|
2706
3263
|
};
|
|
2707
|
-
return
|
|
3264
|
+
return out;
|
|
2708
3265
|
}
|
|
2709
3266
|
const ctx = normalizeCtx(raw);
|
|
2710
3267
|
return {
|
|
@@ -2718,19 +3275,25 @@ function buildCheckInput(raw, configPathOverride) {
|
|
|
2718
3275
|
baseURL: ctx.app.baseURL,
|
|
2719
3276
|
expectedOrigins: ctx.app.expectedOrigins,
|
|
2720
3277
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
2721
|
-
secretsFilePath: SECRETS_FILE_PATH
|
|
3278
|
+
secretsFilePath: SECRETS_FILE_PATH,
|
|
3279
|
+
templateVars: ctx.app.templateVars,
|
|
3280
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
2722
3281
|
},
|
|
2723
|
-
templateVars: ctx.
|
|
3282
|
+
templateVars: ctx.app.templateVars
|
|
2724
3283
|
};
|
|
2725
3284
|
}
|
|
2726
3285
|
function buildRepairInput(raw, configPathOverride) {
|
|
2727
3286
|
const r = raw ?? {};
|
|
2728
3287
|
if (r.configPath && r.vars) {
|
|
2729
|
-
|
|
3288
|
+
const out = configPathOverride ? {
|
|
2730
3289
|
...r,
|
|
2731
3290
|
configPath: configPathOverride
|
|
3291
|
+
} : { ...r };
|
|
3292
|
+
if (out.vars && !out.vars.templateVars) out.vars = {
|
|
3293
|
+
...out.vars,
|
|
3294
|
+
templateVars: out.templateVars ?? {}
|
|
2732
3295
|
};
|
|
2733
|
-
return
|
|
3296
|
+
return out;
|
|
2734
3297
|
}
|
|
2735
3298
|
const ctx = normalizeCtx(raw);
|
|
2736
3299
|
return {
|
|
@@ -2744,23 +3307,29 @@ function buildRepairInput(raw, configPathOverride) {
|
|
|
2744
3307
|
baseURL: ctx.app.baseURL,
|
|
2745
3308
|
expectedOrigins: ctx.app.expectedOrigins,
|
|
2746
3309
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
2747
|
-
secretsFilePath: SECRETS_FILE_PATH
|
|
3310
|
+
secretsFilePath: SECRETS_FILE_PATH,
|
|
3311
|
+
templateVars: ctx.app.templateVars,
|
|
3312
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
2748
3313
|
},
|
|
2749
3314
|
repairData: {
|
|
2750
3315
|
secretsContent: ctx.secrets.secretsContent,
|
|
2751
3316
|
providerKeyContent: ctx.secrets.providerKeyContent
|
|
2752
3317
|
},
|
|
2753
|
-
templateVars: ctx.
|
|
3318
|
+
templateVars: ctx.app.templateVars
|
|
2754
3319
|
};
|
|
2755
3320
|
}
|
|
2756
3321
|
function buildResetInput(raw, configPathOverride) {
|
|
2757
3322
|
const r = raw ?? {};
|
|
2758
3323
|
if (r.configPath && r.vars && r.resetData) {
|
|
2759
|
-
|
|
3324
|
+
const out = configPathOverride ? {
|
|
2760
3325
|
...r,
|
|
2761
3326
|
configPath: configPathOverride
|
|
3327
|
+
} : { ...r };
|
|
3328
|
+
if (out.vars && !out.vars.templateVars) out.vars = {
|
|
3329
|
+
...out.vars,
|
|
3330
|
+
templateVars: out.resetData?.templateVars ?? {}
|
|
2762
3331
|
};
|
|
2763
|
-
return
|
|
3332
|
+
return out;
|
|
2764
3333
|
}
|
|
2765
3334
|
const ctx = normalizeCtx(raw);
|
|
2766
3335
|
return {
|
|
@@ -2774,7 +3343,9 @@ function buildResetInput(raw, configPathOverride) {
|
|
|
2774
3343
|
baseURL: ctx.app.baseURL,
|
|
2775
3344
|
expectedOrigins: ctx.app.expectedOrigins,
|
|
2776
3345
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
2777
|
-
secretsFilePath: SECRETS_FILE_PATH
|
|
3346
|
+
secretsFilePath: SECRETS_FILE_PATH,
|
|
3347
|
+
templateVars: ctx.app.templateVars,
|
|
3348
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
2778
3349
|
},
|
|
2779
3350
|
resetData: {
|
|
2780
3351
|
templateVars: ctx.reset.templateVars,
|
|
@@ -2788,30 +3359,379 @@ function buildResetInput(raw, configPathOverride) {
|
|
|
2788
3359
|
}
|
|
2789
3360
|
//#endregion
|
|
2790
3361
|
//#region src/doctor.ts
|
|
3362
|
+
/**
|
|
3363
|
+
* Per-rule orchestration with telemetry-friendly outcomes.
|
|
3364
|
+
*
|
|
3365
|
+
* Flow per rule (in topological order, after disabledRules / dependsOn /
|
|
3366
|
+
* skipWhen filtering):
|
|
3367
|
+
*
|
|
3368
|
+
* 1. validate — exception → status='error', abort
|
|
3369
|
+
* 2. pass → status='pass'
|
|
3370
|
+
* 3. fail and !opts.fix → status='failed' (with v1.message)
|
|
3371
|
+
* 4. fail and opts.fix →
|
|
3372
|
+
* a. only standard-mode rules attempt repair; ai/reset stay 'failed'
|
|
3373
|
+
* b. repair — exception → status='error', abort
|
|
3374
|
+
* c. re-validate — exception → status='error', abort
|
|
3375
|
+
* d. v2.pass → status='fixed' (before = v1.message)
|
|
3376
|
+
* e. v2.fail → status='still-broken' (before+after)
|
|
3377
|
+
*
|
|
3378
|
+
* Aborted runs return a partial DoctorReport with `aborted: true`. The
|
|
3379
|
+
* caller should `console.log(JSON.stringify(report))` first, THEN
|
|
3380
|
+
* `process.exit(1)` so sweep / telemetry callers always get the partial
|
|
3381
|
+
* data plus the non-zero signal.
|
|
3382
|
+
*
|
|
3383
|
+
* Config writeback: if opts.fix and at least one rule transitioned
|
|
3384
|
+
* pass→fixed, the in-memory ctx.config is JSON-serialized and written
|
|
3385
|
+
* back to opts.configPath. On abort, no write — keep the on-disk file
|
|
3386
|
+
* matching whatever was successful before the failure.
|
|
3387
|
+
*
|
|
3388
|
+
* Secrets file / provider-key / restart-command writes are NOT performed
|
|
3389
|
+
* by doctor. Those belong to the standalone `repair` subcommand which
|
|
3390
|
+
* sandbox_console push uses. Doctor is for diagnosis + per-rule fix
|
|
3391
|
+
* with structured outcomes.
|
|
3392
|
+
*/
|
|
2791
3393
|
async function runDoctor(rawCtx, opts) {
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3394
|
+
const results = [];
|
|
3395
|
+
console.error(`runDoctor: begin fix=${opts.fix} rules=[${opts.rules.join(",")}] configPath=${opts.configPath ?? "(default)"}`);
|
|
3396
|
+
const checkInput = buildCheckInput(rawCtx, opts.configPath);
|
|
3397
|
+
let configRaw;
|
|
3398
|
+
let config;
|
|
3399
|
+
try {
|
|
3400
|
+
configRaw = readFile(checkInput.configPath);
|
|
3401
|
+
} catch (e) {
|
|
3402
|
+
console.error(`runDoctor: read config failed at ${checkInput.configPath} message=${e.message}`);
|
|
3403
|
+
return finalize([{
|
|
3404
|
+
rule: "config_syntax_check",
|
|
3405
|
+
status: "error",
|
|
3406
|
+
message: "read config failed: " + e.message
|
|
3407
|
+
}], true);
|
|
3408
|
+
}
|
|
3409
|
+
try {
|
|
3410
|
+
config = loadJSON5().parse(configRaw);
|
|
3411
|
+
} catch (e) {
|
|
3412
|
+
console.error(`runDoctor: config JSON5 parse failed message=${e.message}`);
|
|
3413
|
+
results.push({
|
|
3414
|
+
rule: "config_syntax_check",
|
|
3415
|
+
status: opts.fix ? "still-broken" : "failed",
|
|
3416
|
+
message: "config JSON5 parse failed: " + e.message
|
|
3417
|
+
});
|
|
3418
|
+
return finalize(results, true);
|
|
3419
|
+
}
|
|
3420
|
+
const ctx = {
|
|
3421
|
+
config,
|
|
3422
|
+
configPath: checkInput.configPath,
|
|
3423
|
+
vars: checkInput.vars,
|
|
3424
|
+
providerDeps: analyzeProviderDeps(config)
|
|
3425
|
+
};
|
|
3426
|
+
const originalConfig = opts.showDiff && opts.fix ? deepClone(config) : null;
|
|
3427
|
+
const allRules = getAllRules();
|
|
3428
|
+
const onlyKeys = opts.rules.length > 0 ? new Set(opts.rules) : null;
|
|
3429
|
+
const skipRuleKeys = new Set(opts.skipRules ?? []);
|
|
3430
|
+
const disabledKeys = new Set([...checkInput.disabledRules ?? [], ...skipRuleKeys]);
|
|
3431
|
+
if (onlyKeys) {
|
|
3432
|
+
const registeredKeys = new Set(allRules.map((r) => r.meta.key));
|
|
3433
|
+
for (const k of opts.rules) if (!registeredKeys.has(k)) results.push({
|
|
3434
|
+
rule: k,
|
|
3435
|
+
status: "unknown"
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
const failedKeys = /* @__PURE__ */ new Set();
|
|
3439
|
+
let configDirty = false;
|
|
3440
|
+
let aborted = false;
|
|
3441
|
+
const hasMiaoda = shouldCheckMiaodaRules(config);
|
|
3442
|
+
const plannedRules = allRules.filter((r) => !onlyKeys || onlyKeys.has(r.meta.key));
|
|
3443
|
+
console.error(`runDoctor: planned ${plannedRules.length} rules hasMiaoda=${hasMiaoda} providerDeps=${JSON.stringify(ctx.providerDeps)}`);
|
|
3444
|
+
outer: for (const rule of allRules) {
|
|
3445
|
+
const key = rule.meta.key;
|
|
3446
|
+
if (onlyKeys && !onlyKeys.has(key)) continue;
|
|
3447
|
+
if (disabledKeys.has(key)) {
|
|
3448
|
+
const reason = skipRuleKeys.has(key) ? "--skip-rule" : "disabledRules";
|
|
3449
|
+
console.error(`rule ${key}: skipped reason=${reason}`);
|
|
3450
|
+
results.push({
|
|
3451
|
+
rule: key,
|
|
3452
|
+
status: "skipped",
|
|
3453
|
+
message: reason
|
|
3454
|
+
});
|
|
3455
|
+
continue;
|
|
3456
|
+
}
|
|
3457
|
+
if (rule.meta.dependsOn?.some((d) => failedKeys.has(d))) {
|
|
3458
|
+
const blockers = rule.meta.dependsOn?.filter((d) => failedKeys.has(d)).join(",");
|
|
3459
|
+
console.error(`rule ${key}: skipped reason=dependsOn-failed blockers=${blockers}`);
|
|
3460
|
+
results.push({
|
|
3461
|
+
rule: key,
|
|
3462
|
+
status: "skipped",
|
|
3463
|
+
message: `dependsOn failed: ${blockers}`
|
|
3464
|
+
});
|
|
3465
|
+
continue;
|
|
3466
|
+
}
|
|
3467
|
+
if (rule.meta.skipWhen?.({
|
|
3468
|
+
hasMiaoda,
|
|
3469
|
+
deps: ctx.providerDeps
|
|
3470
|
+
})) {
|
|
3471
|
+
console.error(`rule ${key}: skipped reason=skipWhen`);
|
|
3472
|
+
results.push({
|
|
3473
|
+
rule: key,
|
|
3474
|
+
status: "skipped",
|
|
3475
|
+
message: "skipWhen"
|
|
3476
|
+
});
|
|
3477
|
+
continue;
|
|
3478
|
+
}
|
|
3479
|
+
const tValidate = Date.now();
|
|
3480
|
+
let v1;
|
|
3481
|
+
try {
|
|
3482
|
+
v1 = rule.validate(ctx);
|
|
3483
|
+
} catch (e) {
|
|
3484
|
+
const msg = e.message;
|
|
3485
|
+
console.error(`rule ${key}: validate -> error durationMs=${Date.now() - tValidate} message=${msg}`);
|
|
3486
|
+
results.push({
|
|
3487
|
+
rule: key,
|
|
3488
|
+
status: "error",
|
|
3489
|
+
message: "validate threw: " + msg
|
|
3490
|
+
});
|
|
3491
|
+
aborted = true;
|
|
3492
|
+
break outer;
|
|
3493
|
+
}
|
|
3494
|
+
if (v1.pass) {
|
|
3495
|
+
console.error(`rule ${key}: validate -> pass durationMs=${Date.now() - tValidate}`);
|
|
3496
|
+
results.push({
|
|
3497
|
+
rule: key,
|
|
3498
|
+
status: "pass"
|
|
3499
|
+
});
|
|
3500
|
+
continue;
|
|
3501
|
+
}
|
|
3502
|
+
if (!opts.fix) {
|
|
3503
|
+
console.error(`rule ${key}: validate -> failed durationMs=${Date.now() - tValidate} message=${v1.message ?? ""}`);
|
|
3504
|
+
results.push({
|
|
3505
|
+
rule: key,
|
|
3506
|
+
status: "failed",
|
|
3507
|
+
message: v1.message
|
|
3508
|
+
});
|
|
3509
|
+
failedKeys.add(key);
|
|
3510
|
+
continue;
|
|
3511
|
+
}
|
|
3512
|
+
if (rule.meta.repairMode !== "standard") {
|
|
3513
|
+
console.error(`rule ${key}: validate -> failed (no auto-repair, repairMode=${rule.meta.repairMode}) durationMs=${Date.now() - tValidate} message=${v1.message ?? ""}`);
|
|
3514
|
+
results.push({
|
|
3515
|
+
rule: key,
|
|
3516
|
+
status: "failed",
|
|
3517
|
+
message: v1.message
|
|
3518
|
+
});
|
|
3519
|
+
failedKeys.add(key);
|
|
3520
|
+
continue;
|
|
3521
|
+
}
|
|
3522
|
+
console.error(`rule ${key}: validate -> failed durationMs=${Date.now() - tValidate} message=${v1.message ?? ""}`);
|
|
3523
|
+
const tRepair = Date.now();
|
|
3524
|
+
try {
|
|
3525
|
+
rule.repair(ctx);
|
|
3526
|
+
} catch (e) {
|
|
3527
|
+
const msg = e.message;
|
|
3528
|
+
console.error(`rule ${key}: repair -> error durationMs=${Date.now() - tRepair} message=${msg}`);
|
|
3529
|
+
results.push({
|
|
3530
|
+
rule: key,
|
|
3531
|
+
status: "error",
|
|
3532
|
+
message: "repair threw: " + msg,
|
|
3533
|
+
before: v1.message
|
|
3534
|
+
});
|
|
3535
|
+
aborted = true;
|
|
3536
|
+
break outer;
|
|
3537
|
+
}
|
|
3538
|
+
console.error(`rule ${key}: repair -> ok durationMs=${Date.now() - tRepair}`);
|
|
3539
|
+
configDirty = true;
|
|
3540
|
+
const tRevalidate = Date.now();
|
|
3541
|
+
let v2;
|
|
3542
|
+
try {
|
|
3543
|
+
v2 = rule.validate(ctx);
|
|
3544
|
+
} catch (e) {
|
|
3545
|
+
const msg = e.message;
|
|
3546
|
+
console.error(`rule ${key}: revalidate -> error durationMs=${Date.now() - tRevalidate} message=${msg}`);
|
|
3547
|
+
results.push({
|
|
3548
|
+
rule: key,
|
|
3549
|
+
status: "error",
|
|
3550
|
+
message: "post-repair validate threw: " + msg,
|
|
3551
|
+
before: v1.message
|
|
3552
|
+
});
|
|
3553
|
+
aborted = true;
|
|
3554
|
+
break outer;
|
|
3555
|
+
}
|
|
3556
|
+
if (v2.pass) {
|
|
3557
|
+
console.error(`rule ${key}: revalidate -> pass (fixed) durationMs=${Date.now() - tRevalidate}`);
|
|
3558
|
+
results.push({
|
|
3559
|
+
rule: key,
|
|
3560
|
+
status: "fixed",
|
|
3561
|
+
before: v1.message
|
|
3562
|
+
});
|
|
3563
|
+
} else {
|
|
3564
|
+
console.error(`rule ${key}: revalidate -> still-broken durationMs=${Date.now() - tRevalidate} after=${v2.message ?? ""}`);
|
|
3565
|
+
results.push({
|
|
3566
|
+
rule: key,
|
|
3567
|
+
status: "still-broken",
|
|
3568
|
+
before: v1.message,
|
|
3569
|
+
after: v2.message
|
|
3570
|
+
});
|
|
3571
|
+
failedKeys.add(key);
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
let backupPath = null;
|
|
3575
|
+
if (configDirty && !aborted) {
|
|
3576
|
+
backupPath = backupConfigSync(ctx.configPath);
|
|
3577
|
+
const serialized = JSON.stringify(ctx.config, null, 2);
|
|
3578
|
+
try {
|
|
3579
|
+
writeFile(ctx.configPath, serialized);
|
|
3580
|
+
console.error(`runDoctor: writeback ok path=${ctx.configPath} bytes=${serialized.length}`);
|
|
3581
|
+
} catch (e) {
|
|
3582
|
+
const msg = e.message;
|
|
3583
|
+
console.error(`runDoctor: writeback failed path=${ctx.configPath} message=${msg}`);
|
|
3584
|
+
results.push({
|
|
3585
|
+
rule: "*config-writeback*",
|
|
3586
|
+
status: "error",
|
|
3587
|
+
message: "config write failed: " + msg
|
|
3588
|
+
});
|
|
3589
|
+
aborted = true;
|
|
3590
|
+
}
|
|
3591
|
+
} else if (configDirty && aborted) console.error("runDoctor: writeback skipped (aborted, leaving on-disk config untouched)");
|
|
3592
|
+
else console.error("runDoctor: writeback skipped (no rule transitioned to fixed)");
|
|
3593
|
+
console.error(`runDoctor: end aborted=${aborted} results=${results.length}`);
|
|
3594
|
+
if (originalConfig !== null) {
|
|
3595
|
+
const d = (0, json_diff.diffString)(originalConfig, ctx.config, { color: false });
|
|
3596
|
+
console.error(`runDoctor: diff backupPath=${backupPath ?? "(none)"} changed=${!!d}`);
|
|
3597
|
+
if (d) {
|
|
3598
|
+
process.stdout.write(`original: ${backupPath ?? "(none)"}\n`);
|
|
3599
|
+
process.stdout.write(`after fixed: ${ctx.configPath}\n`);
|
|
3600
|
+
process.stdout.write("diff:\n");
|
|
3601
|
+
process.stdout.write(d);
|
|
3602
|
+
if (!d.endsWith("\n")) process.stdout.write("\n");
|
|
3603
|
+
} else process.stdout.write("(no openclaw.json changes)\n");
|
|
3604
|
+
}
|
|
3605
|
+
return finalize(results, aborted);
|
|
3606
|
+
}
|
|
3607
|
+
/** Deep-clone a JSON-shaped value. Uses structuredClone where available
|
|
3608
|
+
* (Node 17+); falls back to JSON round-trip otherwise. The config we
|
|
3609
|
+
* clone here is JSON5-parsed so it's strictly tree-shaped — no
|
|
3610
|
+
* references, no functions — making both paths equivalent. */
|
|
3611
|
+
function deepClone(v) {
|
|
3612
|
+
if (typeof structuredClone === "function") return structuredClone(v);
|
|
3613
|
+
return JSON.parse(JSON.stringify(v));
|
|
3614
|
+
}
|
|
3615
|
+
function finalize(results, aborted) {
|
|
3616
|
+
const summary = {
|
|
3617
|
+
pass: 0,
|
|
3618
|
+
failed: 0,
|
|
3619
|
+
fixed: 0,
|
|
3620
|
+
stillBroken: 0,
|
|
3621
|
+
skipped: 0,
|
|
3622
|
+
error: 0,
|
|
3623
|
+
unknown: 0
|
|
3624
|
+
};
|
|
3625
|
+
for (const r of results) switch (r.status) {
|
|
3626
|
+
case "pass":
|
|
3627
|
+
summary.pass++;
|
|
3628
|
+
break;
|
|
3629
|
+
case "failed":
|
|
3630
|
+
summary.failed++;
|
|
3631
|
+
break;
|
|
3632
|
+
case "fixed":
|
|
3633
|
+
summary.fixed++;
|
|
3634
|
+
break;
|
|
3635
|
+
case "still-broken":
|
|
3636
|
+
summary.stillBroken++;
|
|
3637
|
+
break;
|
|
3638
|
+
case "skipped":
|
|
3639
|
+
summary.skipped++;
|
|
3640
|
+
break;
|
|
3641
|
+
case "error":
|
|
3642
|
+
summary.error++;
|
|
3643
|
+
break;
|
|
3644
|
+
case "unknown":
|
|
3645
|
+
summary.unknown++;
|
|
3646
|
+
break;
|
|
2800
3647
|
}
|
|
2801
|
-
const check = runCheck(buildCheckInput(rawCtx, opts.configPath));
|
|
2802
|
-
if (!opts.fix) return { failedRules: check.failedRules };
|
|
2803
|
-
const repairInput = buildRepairInput(rawCtx, opts.configPath);
|
|
2804
|
-
repairInput.failedRules = check.failedRules.standard;
|
|
2805
3648
|
return {
|
|
2806
|
-
|
|
2807
|
-
|
|
3649
|
+
results,
|
|
3650
|
+
summary,
|
|
3651
|
+
aborted
|
|
2808
3652
|
};
|
|
2809
3653
|
}
|
|
2810
3654
|
//#endregion
|
|
3655
|
+
//#region src/innerapi/reportCliRun.ts
|
|
3656
|
+
/**
|
|
3657
|
+
* CLI-side client for studio_server's `openclaw.report_cli_run` inner
|
|
3658
|
+
* API — the unified telemetry sink for every CLI command.
|
|
3659
|
+
*
|
|
3660
|
+
* Best-effort: gated by `DoctorCtx.telemetry.reportCliRun` returned by
|
|
3661
|
+
* `get_doctor_ctx`. When the server hasn't rolled out the API yet the
|
|
3662
|
+
* flag is absent and the CLI never invokes this. When enabled, the CLI
|
|
3663
|
+
* fires-and-forgets one report per command after work is done.
|
|
3664
|
+
*
|
|
3665
|
+
* Failures here MUST NOT change exit code or otherwise affect the
|
|
3666
|
+
* command's data path — telemetry is auxiliary. Caller is expected to
|
|
3667
|
+
* `try/catch` and just `console.error` on rejection.
|
|
3668
|
+
*
|
|
3669
|
+
* Replaces the earlier `openclaw.report_doctor_result` API. The schema
|
|
3670
|
+
* generalises so check / repair / install-* / download-resource / reset
|
|
3671
|
+
* (worker) can all use the same envelope; per-command shape lives under
|
|
3672
|
+
* `result`.
|
|
3673
|
+
*/
|
|
3674
|
+
const API_NAME = "openclaw.report_cli_run";
|
|
3675
|
+
async function reportCliRun(opts) {
|
|
3676
|
+
const client = getHttpClient();
|
|
3677
|
+
const input = {
|
|
3678
|
+
command: opts.command,
|
|
3679
|
+
runId: opts.runId,
|
|
3680
|
+
version: opts.version,
|
|
3681
|
+
invocation: opts.invocation,
|
|
3682
|
+
durationMs: opts.durationMs,
|
|
3683
|
+
success: opts.success
|
|
3684
|
+
};
|
|
3685
|
+
if (opts.caller) input.caller = opts.caller;
|
|
3686
|
+
if (opts.traceId) input.traceId = opts.traceId;
|
|
3687
|
+
if (opts.result !== void 0) input.result = opts.result;
|
|
3688
|
+
if (opts.error) input.error = opts.error;
|
|
3689
|
+
const body = {
|
|
3690
|
+
apiName: API_NAME,
|
|
3691
|
+
input,
|
|
3692
|
+
bizType: BIZ_TYPE
|
|
3693
|
+
};
|
|
3694
|
+
const headers = { "Content-Type": "application/json" };
|
|
3695
|
+
const ttEnv = process.env.X_TT_ENV;
|
|
3696
|
+
if (ttEnv) headers["x-tt-env"] = ttEnv;
|
|
3697
|
+
const start = Date.now();
|
|
3698
|
+
console.error(`reportCliRun: start command=${opts.command} success=${opts.success} version=${opts.version} invocation=${JSON.stringify(opts.invocation)} durationMs=${opts.durationMs}`);
|
|
3699
|
+
let response;
|
|
3700
|
+
try {
|
|
3701
|
+
response = await client.post(INNERAPI_CALL_PATH, body, { headers });
|
|
3702
|
+
} catch (e) {
|
|
3703
|
+
if (e instanceof _lark_apaas_http_client.HttpError && e.response) {
|
|
3704
|
+
const status = e.response.status;
|
|
3705
|
+
const logId = e.response.headers.get("x-tt-logid") ?? "";
|
|
3706
|
+
throw new Error(`reportCliRun HTTP ${status} ${e.response.statusText} (logID: ${logId})`);
|
|
3707
|
+
}
|
|
3708
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3709
|
+
throw new Error(`reportCliRun network error: ${msg}`);
|
|
3710
|
+
}
|
|
3711
|
+
const logId = response.headers.get("x-tt-logid") ?? "";
|
|
3712
|
+
if (!response.ok) {
|
|
3713
|
+
let preview = "";
|
|
3714
|
+
try {
|
|
3715
|
+
preview = (await response.text()).slice(0, 500);
|
|
3716
|
+
} catch {}
|
|
3717
|
+
throw new Error(`reportCliRun HTTP ${response.status} ${response.statusText} (logID: ${logId})${preview ? ` body=${preview}` : ""}`);
|
|
3718
|
+
}
|
|
3719
|
+
let envelope;
|
|
3720
|
+
try {
|
|
3721
|
+
envelope = await response.json();
|
|
3722
|
+
} catch {
|
|
3723
|
+
console.error(`reportCliRun: ok logId=${logId} httpDurationMs=${Date.now() - start} (body unparseable, treated as success)`);
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
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 ?? ""}`);
|
|
3727
|
+
if (envelope.data && envelope.data.success === false) throw new Error(`reportCliRun business error (logID: ${logId}): ${envelope.error_msg ?? JSON.stringify(envelope.data)}`);
|
|
3728
|
+
console.error(`reportCliRun: ok logId=${logId} httpDurationMs=${Date.now() - start}`);
|
|
3729
|
+
}
|
|
3730
|
+
//#endregion
|
|
2811
3731
|
//#region src/help.ts
|
|
2812
3732
|
const BIN = "mclaw-diagnose";
|
|
2813
3733
|
function versionBanner() {
|
|
2814
|
-
return `v0.1.4-alpha.
|
|
3734
|
+
return `v0.1.4-alpha.1`;
|
|
2815
3735
|
}
|
|
2816
3736
|
const COMMANDS = [
|
|
2817
3737
|
{
|
|
@@ -2819,46 +3739,85 @@ const COMMANDS = [
|
|
|
2819
3739
|
hidden: false,
|
|
2820
3740
|
summary: "Diagnose openclaw config; apply repairs with --fix",
|
|
2821
3741
|
help: `USAGE
|
|
2822
|
-
${BIN} doctor [--fix] [--rule=<key>]...
|
|
3742
|
+
${BIN} doctor [--fix] [--show-diff] [--rule=<key>]... [--skip-rule=<key>]...
|
|
3743
|
+
[--caller=<n>] [--trace-id=<id>]
|
|
2823
3744
|
|
|
2824
3745
|
DESCRIPTION
|
|
2825
|
-
Fetches DoctorCtx via innerapi, then
|
|
2826
|
-
on the flags. Output is a single JSON object on stdout.
|
|
3746
|
+
Fetches DoctorCtx via innerapi, then orchestrates each rule:
|
|
2827
3747
|
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
3748
|
+
validate (always)
|
|
3749
|
+
pass → status='pass'
|
|
3750
|
+
fail and !--fix → status='failed'
|
|
3751
|
+
fail and --fix
|
|
3752
|
+
repair → re-validate
|
|
3753
|
+
pass → status='fixed'
|
|
3754
|
+
still fail → status='still-broken'
|
|
2834
3755
|
|
|
2835
|
-
|
|
2836
|
-
then repairs every failing standard-mode rule.
|
|
2837
|
-
Returns
|
|
2838
|
-
{ check: {...}, repair: {...} }
|
|
2839
|
-
Use this as the default "fix everything" action.
|
|
3756
|
+
Output is a single JSON object on stdout:
|
|
2840
3757
|
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
3758
|
+
{
|
|
3759
|
+
"results": [{ "rule": "...", "status": "...", "message"?: "...", ...}],
|
|
3760
|
+
"summary": { "pass": N, "failed": N, "fixed": N, "stillBroken": N,
|
|
3761
|
+
"skipped": N, "error": N, "unknown": N },
|
|
3762
|
+
"aborted": false
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
STATUSES
|
|
3766
|
+
pass Rule's validate passed; no repair attempted.
|
|
3767
|
+
failed Rule's validate failed; --fix was not given.
|
|
3768
|
+
fixed Rule's validate failed; repair ran; re-validate passed.
|
|
3769
|
+
still-broken Rule's validate failed; repair ran; re-validate still failed.
|
|
3770
|
+
skipped Rule excluded by disabledRules / --skip-rule / dependsOn /
|
|
3771
|
+
skipWhen.
|
|
3772
|
+
error validate or repair threw an exception. ABORTS the run —
|
|
3773
|
+
exit code becomes 1; partial 'results' still written to stdout.
|
|
3774
|
+
unknown --rule=<k> with k not registered. Reported, not aborting.
|
|
2847
3775
|
|
|
2848
3776
|
OPTIONS
|
|
2849
|
-
--fix
|
|
2850
|
-
|
|
2851
|
-
|
|
3777
|
+
--fix Enable repair. Without it, validate runs but failures
|
|
3778
|
+
are reported as 'failed' instead of being repaired.
|
|
3779
|
+
--show-diff With --fix: replace the JSON report on stdout with
|
|
3780
|
+
a plain-text section showing
|
|
3781
|
+
original: <backup path>
|
|
3782
|
+
after fixed: <openclaw.json path>
|
|
3783
|
+
diff:
|
|
3784
|
+
<unified-diff body, same format as \`json-diff\`>
|
|
3785
|
+
Interactive flag — sandbox_console's push path never
|
|
3786
|
+
sets it, so the JSON contract is preserved for that
|
|
3787
|
+
caller. No-op without --fix (no mutation, no diff).
|
|
3788
|
+
--rule=<key> Whitelist: restrict the run to this rule key (repeatable).
|
|
3789
|
+
Without --rule, every registered rule runs. With --rule,
|
|
3790
|
+
the planner narrows the innerapi vars fetch to just
|
|
3791
|
+
that rule's needs (see lazy-vars).
|
|
3792
|
+
--skip-rule=<key> Blacklist: skip the rule entirely — neither validate
|
|
3793
|
+
nor repair runs (repeatable). Outcome is 'skipped' with
|
|
3794
|
+
message='--skip-rule'. Stacks with the server's
|
|
3795
|
+
disabledRules. Skipped rules also subtract their vars
|
|
3796
|
+
from the planner's union.
|
|
3797
|
+
--caller=<name> Optional metadata; passed through verbatim to innerapi
|
|
3798
|
+
(fetchCtx + report_cli_run body.input.caller).
|
|
3799
|
+
E.g. "sweep_job", "user_yxy". Server uses for audit /
|
|
3800
|
+
correlation; CLI doesn't act on it.
|
|
3801
|
+
--trace-id=<id> Optional log-correlation id; passed through verbatim
|
|
3802
|
+
to innerapi body.input.traceId. Typically the upstream
|
|
3803
|
+
caller's logID (e.g. sandbox_console's request id).
|
|
2852
3804
|
|
|
2853
3805
|
EXAMPLES
|
|
2854
|
-
${BIN} doctor
|
|
2855
|
-
${BIN} doctor --fix
|
|
2856
|
-
${BIN} doctor --fix --rule=gateway
|
|
2857
|
-
${BIN} doctor --fix --rule=
|
|
3806
|
+
${BIN} doctor # diagnose, no fixes
|
|
3807
|
+
${BIN} doctor --fix # diagnose + fix everything
|
|
3808
|
+
${BIN} doctor --fix --rule=gateway # fix only 'gateway'
|
|
3809
|
+
${BIN} doctor --fix --skip-rule=feishu_channel # fix everything except feishu
|
|
3810
|
+
${BIN} doctor --fix --skip-rule=feishu_channel --skip-rule=feishu_default_account
|
|
3811
|
+
# skip multiple rules
|
|
3812
|
+
${BIN} doctor --fix --show-diff # see exactly what changed
|
|
3813
|
+
# (plain-text diff after
|
|
3814
|
+
# the JSON report)
|
|
2858
3815
|
|
|
2859
3816
|
EXIT CODES
|
|
2860
|
-
0
|
|
2861
|
-
|
|
3817
|
+
0 completed (results may include 'failed' / 'still-broken' — those
|
|
3818
|
+
are normal data outcomes, not run failures)
|
|
3819
|
+
1 aborted because a rule's validate or repair threw. Partial
|
|
3820
|
+
results still on stdout.
|
|
2862
3821
|
77 innerapi authentication failed (sandbox JWT expired/invalid)
|
|
2863
3822
|
`
|
|
2864
3823
|
},
|
|
@@ -3047,6 +4006,62 @@ function formatCommandHelp(name) {
|
|
|
3047
4006
|
return cmd.help;
|
|
3048
4007
|
}
|
|
3049
4008
|
//#endregion
|
|
4009
|
+
//#region src/rule-engine/planner.ts
|
|
4010
|
+
/**
|
|
4011
|
+
* Compute the union of `Vars` field names that the **enabled** rules will
|
|
4012
|
+
* actually read. Sources from each rule's `@Rule({ usesVars: [...] })`
|
|
4013
|
+
* declaration; drift is caught at test time by `strict-vars.test.ts`.
|
|
4014
|
+
*
|
|
4015
|
+
* Inactive rules (skipped via skipWhen at runtime) are STILL counted —
|
|
4016
|
+
* skipWhen depends on parsed-config state we don't have at planning time.
|
|
4017
|
+
* Slight over-fetch in those cases, but no correctness issue.
|
|
4018
|
+
*
|
|
4019
|
+
* Result is stable-sorted for deterministic logging / test snapshots.
|
|
4020
|
+
*/
|
|
4021
|
+
function planVarsFields(opts = {}) {
|
|
4022
|
+
const disabled = new Set(opts.disabled ?? []);
|
|
4023
|
+
const only = opts.onlyRules && opts.onlyRules.length > 0 ? new Set(opts.onlyRules) : null;
|
|
4024
|
+
const fields = /* @__PURE__ */ new Set();
|
|
4025
|
+
for (const rule of getAllRules()) {
|
|
4026
|
+
const key = rule.meta.key;
|
|
4027
|
+
if (disabled.has(key)) continue;
|
|
4028
|
+
if (only && !only.has(key)) continue;
|
|
4029
|
+
for (const f of rule.meta.usesVars ?? []) fields.add(f);
|
|
4030
|
+
}
|
|
4031
|
+
return [...fields].sort();
|
|
4032
|
+
}
|
|
4033
|
+
/**
|
|
4034
|
+
* Plan a `populate` selector for a given subcommand.
|
|
4035
|
+
*
|
|
4036
|
+
* Per-command group needs:
|
|
4037
|
+
*
|
|
4038
|
+
* doctor / check app (rule-driven)
|
|
4039
|
+
* repair app + secrets (writes secretsContent / providerKeyContent)
|
|
4040
|
+
* reset app + secrets + install + reset (the works)
|
|
4041
|
+
* install-* install only
|
|
4042
|
+
*
|
|
4043
|
+
* Empty result (`{}`) means "no group needed" — the CLI can skip the
|
|
4044
|
+
* `fetchCtxViaInnerApi` call entirely and run with a synthetic empty ctx.
|
|
4045
|
+
* Happens e.g. when the user pinned `--rule=<key>` to a vars-free rule on
|
|
4046
|
+
* `doctor`.
|
|
4047
|
+
*/
|
|
4048
|
+
function planCtxPopulate(opts) {
|
|
4049
|
+
if (opts.command === "install") return { install: true };
|
|
4050
|
+
const populate = {};
|
|
4051
|
+
const appFields = planVarsFields({
|
|
4052
|
+
disabled: opts.disabled,
|
|
4053
|
+
onlyRules: opts.onlyRules
|
|
4054
|
+
});
|
|
4055
|
+
if (appFields.length > 0) populate.app = appFields;
|
|
4056
|
+
if (opts.command === "repair") populate.secrets = true;
|
|
4057
|
+
else if (opts.command === "reset") {
|
|
4058
|
+
populate.secrets = true;
|
|
4059
|
+
populate.install = true;
|
|
4060
|
+
populate.reset = true;
|
|
4061
|
+
}
|
|
4062
|
+
return populate;
|
|
4063
|
+
}
|
|
4064
|
+
//#endregion
|
|
3050
4065
|
//#region src/index.ts
|
|
3051
4066
|
const args = node_process.default.argv.slice(2);
|
|
3052
4067
|
const mode = args.find((a) => !a.startsWith("-"));
|
|
@@ -3080,8 +4095,39 @@ function getMultiFlag(args, name) {
|
|
|
3080
4095
|
const prefix = `--${name}=`;
|
|
3081
4096
|
return args.filter((a) => a.startsWith(prefix)).map((a) => a.slice(prefix.length));
|
|
3082
4097
|
}
|
|
4098
|
+
/**
|
|
4099
|
+
* Per-case telemetry helper: fires `openclaw.report_cli_run` once with
|
|
4100
|
+
* the command's outcome. Always called — the previous server-flag gate
|
|
4101
|
+
* was removed (server controls receipt by routing the apiName).
|
|
4102
|
+
*
|
|
4103
|
+
* Best-effort: a thrown reportCliRun is logged to cli.log and swallowed.
|
|
4104
|
+
* Never alters the data path's exit code — the user-visible work has
|
|
4105
|
+
* already finished by the time we report.
|
|
4106
|
+
*
|
|
4107
|
+
* `raw` is kept in the signature for backward symmetry with the doctor
|
|
4108
|
+
* case but is no longer consulted.
|
|
4109
|
+
*/
|
|
4110
|
+
async function reportRun(command, rc, _raw, invocation, durationMs, outcome) {
|
|
4111
|
+
console.error(`${command}: telemetry calling report_cli_run`);
|
|
4112
|
+
try {
|
|
4113
|
+
await reportCliRun({
|
|
4114
|
+
command,
|
|
4115
|
+
runId: rc.runId,
|
|
4116
|
+
version: getVersion(),
|
|
4117
|
+
invocation,
|
|
4118
|
+
durationMs,
|
|
4119
|
+
caller: rc.caller,
|
|
4120
|
+
traceId: rc.traceId,
|
|
4121
|
+
success: outcome.success,
|
|
4122
|
+
result: outcome.result,
|
|
4123
|
+
error: outcome.error ? { message: outcome.error.message } : void 0
|
|
4124
|
+
});
|
|
4125
|
+
} catch (e) {
|
|
4126
|
+
console.error(`[telemetry] reportCliRun failed: ${e.message}`);
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
3083
4129
|
async function main() {
|
|
3084
|
-
installStderrMirror();
|
|
4130
|
+
installStderrMirror({ stderrEnabled: args.includes("-x") });
|
|
3085
4131
|
const helpFlags = parseHelpFlags(args);
|
|
3086
4132
|
if (mode && helpFlags.help) {
|
|
3087
4133
|
const body = formatCommandHelp(mode);
|
|
@@ -3101,22 +4147,79 @@ async function main() {
|
|
|
3101
4147
|
node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));
|
|
3102
4148
|
node_process.default.exit(1);
|
|
3103
4149
|
}
|
|
4150
|
+
const caller = getFlag(args, "caller");
|
|
4151
|
+
const traceId = getFlag(args, "trace-id");
|
|
4152
|
+
const rc = setRunContext({
|
|
4153
|
+
caller,
|
|
4154
|
+
traceId
|
|
4155
|
+
});
|
|
4156
|
+
const t0 = Date.now();
|
|
4157
|
+
console.error(`${mode}: begin argv=[${args.join(" ")}] version=${getVersion()} traceId=${traceId ?? "-"} caller=${caller ?? "-"} runIdGenerated=${rc.generated}`);
|
|
3104
4158
|
switch (mode) {
|
|
3105
|
-
case "check":
|
|
4159
|
+
case "check": {
|
|
4160
|
+
const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4161
|
+
populate: planCtxPopulate({ command: "check" }),
|
|
4162
|
+
caller,
|
|
4163
|
+
traceId
|
|
4164
|
+
});
|
|
4165
|
+
const { legacy, report } = runCheckWithReport(buildCheckInput(raw));
|
|
4166
|
+
console.log(JSON.stringify(legacy));
|
|
4167
|
+
await reportRun("check", rc, raw, args.join(" "), Date.now() - t0, {
|
|
4168
|
+
success: !report.aborted,
|
|
4169
|
+
result: report,
|
|
4170
|
+
error: report.aborted ? /* @__PURE__ */ new Error("check run aborted") : void 0
|
|
4171
|
+
});
|
|
4172
|
+
break;
|
|
4173
|
+
}
|
|
3106
4174
|
case "repair": {
|
|
3107
|
-
const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi(
|
|
3108
|
-
|
|
3109
|
-
|
|
4175
|
+
const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4176
|
+
populate: planCtxPopulate({ command: "repair" }),
|
|
4177
|
+
caller,
|
|
4178
|
+
traceId
|
|
4179
|
+
});
|
|
4180
|
+
const { legacy, report } = runRepairWithReport(buildRepairInput(raw));
|
|
4181
|
+
console.log(JSON.stringify(legacy));
|
|
4182
|
+
await reportRun("repair", rc, raw, args.join(" "), Date.now() - t0, {
|
|
4183
|
+
success: legacy.success,
|
|
4184
|
+
result: report,
|
|
4185
|
+
error: legacy.success ? void 0 : new Error(legacy.error ?? "repair failed")
|
|
4186
|
+
});
|
|
3110
4187
|
break;
|
|
3111
4188
|
}
|
|
3112
4189
|
case "doctor": {
|
|
3113
4190
|
const fix = args.includes("--fix");
|
|
3114
4191
|
const rules = getMultiFlag(args, "rule");
|
|
3115
|
-
const
|
|
4192
|
+
const skipRules = getMultiFlag(args, "skip-rule");
|
|
4193
|
+
const showDiff = args.includes("--show-diff");
|
|
4194
|
+
const populate = planCtxPopulate({
|
|
4195
|
+
command: "doctor",
|
|
4196
|
+
onlyRules: fix && rules.length > 0 ? rules : void 0,
|
|
4197
|
+
disabled: skipRules
|
|
4198
|
+
});
|
|
4199
|
+
console.error(`doctor: populate=${JSON.stringify(populate)} fix=${fix} rules=[${rules.join(",")}] skipRules=[${skipRules.join(",")}]`);
|
|
4200
|
+
let raw;
|
|
4201
|
+
if (Object.keys(populate).length === 0) {
|
|
4202
|
+
console.error("doctor: skipping fetchCtx (empty populate)");
|
|
4203
|
+
raw = {};
|
|
4204
|
+
} else raw = await fetchCtxViaInnerApi({
|
|
4205
|
+
populate,
|
|
4206
|
+
caller,
|
|
4207
|
+
traceId
|
|
4208
|
+
});
|
|
4209
|
+
const result = await runDoctor(raw, {
|
|
3116
4210
|
fix,
|
|
3117
|
-
rules
|
|
4211
|
+
rules,
|
|
4212
|
+
skipRules,
|
|
4213
|
+
showDiff
|
|
4214
|
+
});
|
|
4215
|
+
if (!(showDiff && fix)) console.log(JSON.stringify(result));
|
|
4216
|
+
await reportRun("doctor", rc, raw, args.join(" "), Date.now() - t0, {
|
|
4217
|
+
success: !result.aborted,
|
|
4218
|
+
result,
|
|
4219
|
+
error: result.aborted ? /* @__PURE__ */ new Error("doctor run aborted") : void 0
|
|
3118
4220
|
});
|
|
3119
|
-
console.
|
|
4221
|
+
console.error(`doctor: end aborted=${result.aborted} totalMs=${Date.now() - t0} summary=${JSON.stringify(result.summary)}`);
|
|
4222
|
+
if (result.aborted) node_process.default.exit(1);
|
|
3120
4223
|
break;
|
|
3121
4224
|
}
|
|
3122
4225
|
case "reset":
|
|
@@ -3125,7 +4228,11 @@ async function main() {
|
|
|
3125
4228
|
let ctxBase64;
|
|
3126
4229
|
if (ctxArg) ctxBase64 = ctxArg.slice(6);
|
|
3127
4230
|
else {
|
|
3128
|
-
const fetched = await fetchCtxViaInnerApi(
|
|
4231
|
+
const fetched = await fetchCtxViaInnerApi({
|
|
4232
|
+
populate: planCtxPopulate({ command: "reset" }),
|
|
4233
|
+
caller,
|
|
4234
|
+
traceId
|
|
4235
|
+
});
|
|
3129
4236
|
ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
|
|
3130
4237
|
}
|
|
3131
4238
|
console.log(JSON.stringify(startAsyncReset(ctxBase64)));
|
|
@@ -3136,7 +4243,22 @@ async function main() {
|
|
|
3136
4243
|
node_process.default.exit(1);
|
|
3137
4244
|
}
|
|
3138
4245
|
const resultFile = resetResultFile(taskId);
|
|
3139
|
-
|
|
4246
|
+
const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4247
|
+
populate: planCtxPopulate({ command: "reset" }),
|
|
4248
|
+
caller,
|
|
4249
|
+
traceId
|
|
4250
|
+
});
|
|
4251
|
+
const outcome = await runReset(buildResetInput(raw), taskId, resultFile);
|
|
4252
|
+
await reportRun("reset", rc, raw, args.join(" "), Date.now() - t0, {
|
|
4253
|
+
success: outcome.success,
|
|
4254
|
+
result: {
|
|
4255
|
+
taskId,
|
|
4256
|
+
steps: outcome.steps,
|
|
4257
|
+
failedStep: outcome.failedStep
|
|
4258
|
+
},
|
|
4259
|
+
error: outcome.success ? void 0 : new Error(outcome.error ?? "reset failed")
|
|
4260
|
+
});
|
|
4261
|
+
if (!outcome.success) node_process.default.exit(1);
|
|
3140
4262
|
} else {
|
|
3141
4263
|
console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
|
|
3142
4264
|
node_process.default.exit(1);
|
|
@@ -3159,12 +4281,34 @@ async function main() {
|
|
|
3159
4281
|
}
|
|
3160
4282
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
3161
4283
|
let installOssFileMap;
|
|
3162
|
-
|
|
3163
|
-
|
|
4284
|
+
let rawForTelemetry;
|
|
4285
|
+
if (!ossFileMapFlag) {
|
|
4286
|
+
rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4287
|
+
populate: planCtxPopulate({ command: "install" }),
|
|
4288
|
+
caller,
|
|
4289
|
+
traceId
|
|
4290
|
+
});
|
|
4291
|
+
installOssFileMap = normalizeCtx(rawForTelemetry).install.ossFileMap;
|
|
4292
|
+
}
|
|
4293
|
+
const ossFileMap = resolveOssFileMap({
|
|
3164
4294
|
ossFileMapFlag,
|
|
3165
4295
|
installOssFileMap
|
|
3166
|
-
})
|
|
3167
|
-
|
|
4296
|
+
});
|
|
4297
|
+
let success = true;
|
|
4298
|
+
let error;
|
|
4299
|
+
try {
|
|
4300
|
+
await installOpenclaw(tag, ossFileMap);
|
|
4301
|
+
} catch (e) {
|
|
4302
|
+
success = false;
|
|
4303
|
+
error = e;
|
|
4304
|
+
}
|
|
4305
|
+
if (success) console.log(JSON.stringify({ ok: true }));
|
|
4306
|
+
await reportRun("install-openclaw", rc, rawForTelemetry, args.join(" "), Date.now() - t0, {
|
|
4307
|
+
success,
|
|
4308
|
+
result: { tag },
|
|
4309
|
+
error
|
|
4310
|
+
});
|
|
4311
|
+
if (error) throw error;
|
|
3168
4312
|
break;
|
|
3169
4313
|
}
|
|
3170
4314
|
case "install-extension": {
|
|
@@ -3180,18 +4324,45 @@ async function main() {
|
|
|
3180
4324
|
const skipConfigUpdate = args.includes("--skip-config-update");
|
|
3181
4325
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
3182
4326
|
let installOssFileMap;
|
|
3183
|
-
|
|
3184
|
-
|
|
4327
|
+
let rawForTelemetry;
|
|
4328
|
+
if (!ossFileMapFlag) {
|
|
4329
|
+
rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4330
|
+
populate: planCtxPopulate({ command: "install" }),
|
|
4331
|
+
caller,
|
|
4332
|
+
traceId
|
|
4333
|
+
});
|
|
4334
|
+
installOssFileMap = normalizeCtx(rawForTelemetry).install.ossFileMap;
|
|
4335
|
+
}
|
|
4336
|
+
const ossFileMap = resolveOssFileMap({
|
|
3185
4337
|
ossFileMapFlag,
|
|
3186
4338
|
installOssFileMap
|
|
3187
|
-
}), {
|
|
3188
|
-
all,
|
|
3189
|
-
names: names.length > 0 ? names : void 0,
|
|
3190
|
-
homeBase,
|
|
3191
|
-
configPath,
|
|
3192
|
-
skipConfigUpdate
|
|
3193
4339
|
});
|
|
3194
|
-
|
|
4340
|
+
let success = true;
|
|
4341
|
+
let error;
|
|
4342
|
+
try {
|
|
4343
|
+
await installExtension(tag, ossFileMap, {
|
|
4344
|
+
all,
|
|
4345
|
+
names: names.length > 0 ? names : void 0,
|
|
4346
|
+
homeBase,
|
|
4347
|
+
configPath,
|
|
4348
|
+
skipConfigUpdate
|
|
4349
|
+
});
|
|
4350
|
+
} catch (e) {
|
|
4351
|
+
success = false;
|
|
4352
|
+
error = e;
|
|
4353
|
+
}
|
|
4354
|
+
if (success) console.log(JSON.stringify({ ok: true }));
|
|
4355
|
+
await reportRun("install-extension", rc, rawForTelemetry, args.join(" "), Date.now() - t0, {
|
|
4356
|
+
success,
|
|
4357
|
+
result: {
|
|
4358
|
+
tag,
|
|
4359
|
+
all,
|
|
4360
|
+
names,
|
|
4361
|
+
skipConfigUpdate
|
|
4362
|
+
},
|
|
4363
|
+
error
|
|
4364
|
+
});
|
|
4365
|
+
if (error) throw error;
|
|
3195
4366
|
break;
|
|
3196
4367
|
}
|
|
3197
4368
|
case "download-resource": {
|
|
@@ -3209,16 +4380,43 @@ async function main() {
|
|
|
3209
4380
|
}
|
|
3210
4381
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
3211
4382
|
let installOssFileMap;
|
|
3212
|
-
|
|
3213
|
-
|
|
4383
|
+
let rawForTelemetry;
|
|
4384
|
+
if (!ossFileMapFlag) {
|
|
4385
|
+
rawForTelemetry = parseCtxFlag(args) ?? await fetchCtxViaInnerApi({
|
|
4386
|
+
populate: planCtxPopulate({ command: "install" }),
|
|
4387
|
+
caller,
|
|
4388
|
+
traceId
|
|
4389
|
+
});
|
|
4390
|
+
installOssFileMap = normalizeCtx(rawForTelemetry).install.ossFileMap;
|
|
4391
|
+
}
|
|
4392
|
+
const ossFileMap = resolveOssFileMap({
|
|
3214
4393
|
ossFileMapFlag,
|
|
3215
4394
|
installOssFileMap
|
|
3216
|
-
}), {
|
|
3217
|
-
role,
|
|
3218
|
-
name,
|
|
3219
|
-
dir
|
|
3220
4395
|
});
|
|
3221
|
-
|
|
4396
|
+
let success = true;
|
|
4397
|
+
let error;
|
|
4398
|
+
try {
|
|
4399
|
+
await downloadResource(tag, ossFileMap, {
|
|
4400
|
+
role,
|
|
4401
|
+
name,
|
|
4402
|
+
dir
|
|
4403
|
+
});
|
|
4404
|
+
} catch (e) {
|
|
4405
|
+
success = false;
|
|
4406
|
+
error = e;
|
|
4407
|
+
}
|
|
4408
|
+
if (success) console.log(JSON.stringify({ ok: true }));
|
|
4409
|
+
await reportRun("download-resource", rc, rawForTelemetry, args.join(" "), Date.now() - t0, {
|
|
4410
|
+
success,
|
|
4411
|
+
result: {
|
|
4412
|
+
tag,
|
|
4413
|
+
role,
|
|
4414
|
+
name,
|
|
4415
|
+
dir
|
|
4416
|
+
},
|
|
4417
|
+
error
|
|
4418
|
+
});
|
|
4419
|
+
if (error) throw error;
|
|
3222
4420
|
break;
|
|
3223
4421
|
}
|
|
3224
4422
|
default:
|
|
@@ -3226,10 +4424,12 @@ async function main() {
|
|
|
3226
4424
|
node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));
|
|
3227
4425
|
node_process.default.exit(1);
|
|
3228
4426
|
}
|
|
4427
|
+
console.error(`${mode}: end totalMs=${Date.now() - t0}`);
|
|
3229
4428
|
}
|
|
3230
4429
|
main().catch((err) => {
|
|
3231
4430
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3232
|
-
console.error(
|
|
4431
|
+
console.error(`${mode ?? "<no-mode>"}: error message=${msg}`);
|
|
4432
|
+
node_process.default.stderr.write(`Error: ${msg}\n`);
|
|
3233
4433
|
node_process.default.exit(1);
|
|
3234
4434
|
});
|
|
3235
4435
|
//#endregion
|