@lark-apaas/openclaw-scripts-diagnose-cli 0.1.3 → 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 +1650 -434
- 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
|
|
@@ -717,6 +730,13 @@ FeishuChannelRule = __decorate([Rule({
|
|
|
717
730
|
* detects + fixes drift on the main bot's appId/appSecret. Single-agent
|
|
718
731
|
* configs (no `accounts`) are out of scope — handled by `feishu_channel`.
|
|
719
732
|
*/
|
|
733
|
+
/** Top-level `channels.feishu.*` account-policy fields migrated into the main bot. */
|
|
734
|
+
const TOP_FIELDS_TO_MIGRATE = [
|
|
735
|
+
"dmPolicy",
|
|
736
|
+
"allowFrom",
|
|
737
|
+
"groupPolicy",
|
|
738
|
+
"groupAllowFrom"
|
|
739
|
+
];
|
|
720
740
|
let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRule {
|
|
721
741
|
validate(ctx) {
|
|
722
742
|
const feishu = getNestedMap(ctx.config, "channels", "feishu");
|
|
@@ -763,10 +783,18 @@ let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRu
|
|
|
763
783
|
const merged = {
|
|
764
784
|
...existingBot,
|
|
765
785
|
...defaultAccount,
|
|
766
|
-
...defaultAcc
|
|
767
|
-
|
|
768
|
-
|
|
786
|
+
...defaultAcc
|
|
787
|
+
};
|
|
788
|
+
for (const k of TOP_FIELDS_TO_MIGRATE) if (merged[k] === void 0 && feishu[k] !== void 0) merged[k] = feishu[k];
|
|
789
|
+
const accountGroups = asRecord(merged.groups) ?? {};
|
|
790
|
+
const topGroups = asRecord(feishu.groups) ?? {};
|
|
791
|
+
const fusedGroups = {
|
|
792
|
+
...accountGroups,
|
|
793
|
+
...topGroups
|
|
769
794
|
};
|
|
795
|
+
if (Object.keys(fusedGroups).length > 0) merged.groups = fusedGroups;
|
|
796
|
+
merged.appId = effectiveAppId;
|
|
797
|
+
merged.appSecret = DEFAULT_FEISHU_APP_SECRET;
|
|
770
798
|
const chatID = ctx.vars?.teamChatID;
|
|
771
799
|
if (typeof chatID === "string" && chatID !== "") {
|
|
772
800
|
const existingGroups = asRecord(merged.groups) ?? {};
|
|
@@ -780,6 +808,7 @@ let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRu
|
|
|
780
808
|
delete accounts.default;
|
|
781
809
|
delete feishu.appId;
|
|
782
810
|
delete feishu.appSecret;
|
|
811
|
+
for (const k of TOP_FIELDS_TO_MIGRATE) delete feishu[k];
|
|
783
812
|
this.rewireBindings(ctx.config, expectedKey);
|
|
784
813
|
}
|
|
785
814
|
enforceMainBotValues(ctx, accounts) {
|
|
@@ -832,7 +861,8 @@ let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRu
|
|
|
832
861
|
FeishuDefaultAccountRule = __decorate([Rule({
|
|
833
862
|
key: "feishu_default_account",
|
|
834
863
|
dependsOn: ["config_syntax_check"],
|
|
835
|
-
repairMode: "standard"
|
|
864
|
+
repairMode: "standard",
|
|
865
|
+
usesVars: ["feishuAppID", "teamChatID"]
|
|
836
866
|
})], FeishuDefaultAccountRule);
|
|
837
867
|
function nonEmpty(v) {
|
|
838
868
|
return typeof v === "string" && v !== "" ? v : void 0;
|
|
@@ -863,23 +893,19 @@ function secretMatchesCanonical(secret) {
|
|
|
863
893
|
}
|
|
864
894
|
//#endregion
|
|
865
895
|
//#region src/rules/gateway.ts
|
|
866
|
-
|
|
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"];
|
|
867
908
|
let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
868
|
-
static {
|
|
869
|
-
_GatewayRule = this;
|
|
870
|
-
}
|
|
871
|
-
static DEFAULT_PORT = 18789;
|
|
872
|
-
static DEFAULT_MODE = "local";
|
|
873
|
-
static DEFAULT_BIND = "loopback";
|
|
874
|
-
static DEFAULT_AUTH_MODE = "token";
|
|
875
|
-
static DEFAULT_AUTH_TOKEN = {
|
|
876
|
-
source: "file",
|
|
877
|
-
provider: "miaoda-secret-provider",
|
|
878
|
-
id: "/gateway_auth_token"
|
|
879
|
-
};
|
|
880
|
-
/** Required entries in gateway.trustedProxies. Repair appends any missing
|
|
881
|
-
* entries while preserving caller-added extras (no overwrite). */
|
|
882
|
-
static DEFAULT_TRUSTED_PROXIES = ["::1", "127.0.0.1"];
|
|
883
909
|
validate(ctx) {
|
|
884
910
|
const gateway = ctx.config.gateway;
|
|
885
911
|
if (!gateway || typeof gateway !== "object") return {
|
|
@@ -887,17 +913,17 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
887
913
|
message: "gateway not found"
|
|
888
914
|
};
|
|
889
915
|
const gw = gateway;
|
|
890
|
-
if (gw.port !==
|
|
916
|
+
if (gw.port !== DEFAULT_PORT) return {
|
|
891
917
|
pass: false,
|
|
892
|
-
message: "gateway.port mismatch: got " + gw.port + ", expected "
|
|
918
|
+
message: "gateway.port mismatch: got " + gw.port + ", expected 18789"
|
|
893
919
|
};
|
|
894
|
-
if (gw.mode !==
|
|
920
|
+
if (gw.mode !== DEFAULT_MODE) return {
|
|
895
921
|
pass: false,
|
|
896
|
-
message: "gateway.mode mismatch: got " + gw.mode + ", expected "
|
|
922
|
+
message: "gateway.mode mismatch: got " + gw.mode + ", expected local"
|
|
897
923
|
};
|
|
898
|
-
if (gw.bind !==
|
|
924
|
+
if (gw.bind !== DEFAULT_BIND) return {
|
|
899
925
|
pass: false,
|
|
900
|
-
message: "gateway.bind mismatch: got " + gw.bind + ", expected "
|
|
926
|
+
message: "gateway.bind mismatch: got " + gw.bind + ", expected loopback"
|
|
901
927
|
};
|
|
902
928
|
const auth = gw.auth;
|
|
903
929
|
if (!auth || typeof auth !== "object") return {
|
|
@@ -905,9 +931,9 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
905
931
|
message: "gateway.auth not found"
|
|
906
932
|
};
|
|
907
933
|
const authObj = auth;
|
|
908
|
-
if (authObj.mode !==
|
|
934
|
+
if (authObj.mode !== DEFAULT_AUTH_MODE) return {
|
|
909
935
|
pass: false,
|
|
910
|
-
message: "gateway.auth.mode mismatch: got " + authObj.mode + ", expected "
|
|
936
|
+
message: "gateway.auth.mode mismatch: got " + authObj.mode + ", expected token"
|
|
911
937
|
};
|
|
912
938
|
const token = authObj.token;
|
|
913
939
|
if (typeof token === "string") {
|
|
@@ -916,7 +942,7 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
916
942
|
message: "gateway.auth.token string value mismatch"
|
|
917
943
|
};
|
|
918
944
|
} else if (typeof token === "object" && token !== null && !Array.isArray(token)) {
|
|
919
|
-
if (!matchMap(token,
|
|
945
|
+
if (!matchMap(token, DEFAULT_AUTH_TOKEN)) return {
|
|
920
946
|
pass: false,
|
|
921
947
|
message: "gateway.auth.token object mismatch: got " + JSON.stringify(token)
|
|
922
948
|
};
|
|
@@ -938,7 +964,7 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
938
964
|
pass: false,
|
|
939
965
|
message: "gateway.trustedProxies missing or not an array"
|
|
940
966
|
};
|
|
941
|
-
const missing =
|
|
967
|
+
const missing = DEFAULT_TRUSTED_PROXIES.filter((p) => !proxies.includes(p));
|
|
942
968
|
if (missing.length > 0) return {
|
|
943
969
|
pass: false,
|
|
944
970
|
message: "gateway.trustedProxies missing: " + JSON.stringify(missing)
|
|
@@ -946,19 +972,19 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
946
972
|
return { pass: true };
|
|
947
973
|
}
|
|
948
974
|
repair(ctx) {
|
|
949
|
-
setNestedValue(ctx.config, ["gateway", "port"],
|
|
950
|
-
setNestedValue(ctx.config, ["gateway", "mode"],
|
|
951
|
-
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);
|
|
952
978
|
setNestedValue(ctx.config, [
|
|
953
979
|
"gateway",
|
|
954
980
|
"auth",
|
|
955
981
|
"mode"
|
|
956
|
-
],
|
|
982
|
+
], DEFAULT_AUTH_MODE);
|
|
957
983
|
setNestedValue(ctx.config, [
|
|
958
984
|
"gateway",
|
|
959
985
|
"auth",
|
|
960
986
|
"token"
|
|
961
|
-
],
|
|
987
|
+
], DEFAULT_AUTH_TOKEN);
|
|
962
988
|
setNestedValue(ctx.config, [
|
|
963
989
|
"gateway",
|
|
964
990
|
"controlUi",
|
|
@@ -967,17 +993,18 @@ let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
|
967
993
|
const gw = ctx.config.gateway ?? {};
|
|
968
994
|
const current = Array.isArray(gw.trustedProxies) ? gw.trustedProxies.slice() : [];
|
|
969
995
|
const seen = new Set(current.map((v) => String(v)));
|
|
970
|
-
for (const p of
|
|
996
|
+
for (const p of DEFAULT_TRUSTED_PROXIES) if (!seen.has(p)) {
|
|
971
997
|
current.push(p);
|
|
972
998
|
seen.add(p);
|
|
973
999
|
}
|
|
974
1000
|
setNestedValue(ctx.config, ["gateway", "trustedProxies"], current);
|
|
975
1001
|
}
|
|
976
1002
|
};
|
|
977
|
-
GatewayRule =
|
|
1003
|
+
GatewayRule = __decorate([Rule({
|
|
978
1004
|
key: "gateway",
|
|
979
1005
|
dependsOn: ["config_syntax_check"],
|
|
980
|
-
repairMode: "standard"
|
|
1006
|
+
repairMode: "standard",
|
|
1007
|
+
usesVars: ["gatewayToken"]
|
|
981
1008
|
})], GatewayRule);
|
|
982
1009
|
//#endregion
|
|
983
1010
|
//#region src/rules/allowed-origins.ts
|
|
@@ -1021,7 +1048,8 @@ let AllowedOriginsRule = class AllowedOriginsRule extends DiagnoseRule {
|
|
|
1021
1048
|
AllowedOriginsRule = __decorate([Rule({
|
|
1022
1049
|
key: "allowed_origins",
|
|
1023
1050
|
dependsOn: ["config_syntax_check"],
|
|
1024
|
-
repairMode: "standard"
|
|
1051
|
+
repairMode: "standard",
|
|
1052
|
+
usesVars: ["expectedOrigins"]
|
|
1025
1053
|
})], AllowedOriginsRule);
|
|
1026
1054
|
function getExpectedOrigins(vars) {
|
|
1027
1055
|
return Array.isArray(vars.expectedOrigins) ? vars.expectedOrigins : [];
|
|
@@ -1100,11 +1128,12 @@ JwtTokenRule = __decorate([Rule({
|
|
|
1100
1128
|
key: "jwt_token",
|
|
1101
1129
|
dependsOn: ["config_syntax_check"],
|
|
1102
1130
|
repairMode: "standard",
|
|
1131
|
+
usesVars: ["providerFilePath"],
|
|
1103
1132
|
skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaProvider
|
|
1104
1133
|
})], JwtTokenRule);
|
|
1105
1134
|
//#endregion
|
|
1106
1135
|
//#region src/rules/secrets-file.ts
|
|
1107
|
-
let
|
|
1136
|
+
let SecretsFileRule = class SecretsFileRule extends DiagnoseRule {
|
|
1108
1137
|
validate(ctx) {
|
|
1109
1138
|
const filePath = ctx.vars.secretsFilePath;
|
|
1110
1139
|
if (!filePath) return {
|
|
@@ -1151,12 +1180,18 @@ let SecretsRule = class SecretsRule extends DiagnoseRule {
|
|
|
1151
1180
|
};
|
|
1152
1181
|
}
|
|
1153
1182
|
};
|
|
1154
|
-
|
|
1183
|
+
SecretsFileRule = __decorate([Rule({
|
|
1155
1184
|
key: "secrets_file",
|
|
1156
1185
|
dependsOn: ["config_syntax_check"],
|
|
1157
1186
|
repairMode: "standard",
|
|
1187
|
+
usesVars: [
|
|
1188
|
+
"secretsFilePath",
|
|
1189
|
+
"feishuAppSecret",
|
|
1190
|
+
"gatewayToken",
|
|
1191
|
+
"innerAPIKey"
|
|
1192
|
+
],
|
|
1158
1193
|
skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaSecretProvider
|
|
1159
|
-
})],
|
|
1194
|
+
})], SecretsFileRule);
|
|
1160
1195
|
//#endregion
|
|
1161
1196
|
//#region src/rules/cleanup-install-backup-dirs.ts
|
|
1162
1197
|
const DIR_PREFIX = ".openclaw-install-";
|
|
@@ -1370,16 +1405,26 @@ function getAllow(config) {
|
|
|
1370
1405
|
}
|
|
1371
1406
|
//#endregion
|
|
1372
1407
|
//#region src/check.ts
|
|
1373
|
-
|
|
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();
|
|
1374
1416
|
const result = { failedRules: {
|
|
1375
1417
|
standard: [],
|
|
1376
1418
|
ai: [],
|
|
1377
1419
|
reset: []
|
|
1378
1420
|
} };
|
|
1421
|
+
const outcomes = [];
|
|
1379
1422
|
const disabledSet = new Set(input.disabledRules || []);
|
|
1380
1423
|
const rules = getAllRules();
|
|
1381
1424
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
1382
1425
|
let configParsed = false;
|
|
1426
|
+
let aborted = false;
|
|
1427
|
+
console.error(`runCheck: begin configPath=${input.configPath} disabledRules=[${[...disabledSet].join(",")}] rules=${rules.length}`);
|
|
1383
1428
|
let ctx = {
|
|
1384
1429
|
config: {},
|
|
1385
1430
|
configPath: input.configPath,
|
|
@@ -1387,13 +1432,29 @@ function runCheck(input) {
|
|
|
1387
1432
|
providerDeps: {
|
|
1388
1433
|
usesMiaodaProvider: false,
|
|
1389
1434
|
usesMiaodaSecretProvider: false
|
|
1390
|
-
}
|
|
1391
|
-
templateVars: input.templateVars
|
|
1435
|
+
}
|
|
1392
1436
|
};
|
|
1393
1437
|
for (const rule of rules) {
|
|
1394
1438
|
const meta = rule.meta;
|
|
1395
|
-
if (disabledSet.has(meta.key))
|
|
1396
|
-
|
|
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
|
+
}
|
|
1397
1458
|
if (meta.dependsOn?.includes("config_syntax_check") && !configParsed) try {
|
|
1398
1459
|
const parsed = loadJSON5().parse(readFile(input.configPath));
|
|
1399
1460
|
const deps = analyzeProviderDeps(parsed);
|
|
@@ -1401,11 +1462,17 @@ function runCheck(input) {
|
|
|
1401
1462
|
config: parsed,
|
|
1402
1463
|
configPath: input.configPath,
|
|
1403
1464
|
vars: input.vars,
|
|
1404
|
-
providerDeps: deps
|
|
1405
|
-
templateVars: input.templateVars
|
|
1465
|
+
providerDeps: deps
|
|
1406
1466
|
};
|
|
1407
1467
|
configParsed = true;
|
|
1408
|
-
} 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;
|
|
1409
1476
|
break;
|
|
1410
1477
|
}
|
|
1411
1478
|
if (meta.skipWhen && configParsed) {
|
|
@@ -1413,15 +1480,53 @@ function runCheck(input) {
|
|
|
1413
1480
|
if (meta.skipWhen({
|
|
1414
1481
|
hasMiaoda,
|
|
1415
1482
|
deps: ctx.providerDeps
|
|
1416
|
-
}))
|
|
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;
|
|
1417
1507
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
+
});
|
|
1420
1521
|
failedKeys.add(meta.key);
|
|
1421
1522
|
pushFailedRule(result, meta.repairMode, meta.key, r.message || "");
|
|
1422
1523
|
}
|
|
1423
1524
|
}
|
|
1424
|
-
|
|
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
|
+
};
|
|
1425
1530
|
}
|
|
1426
1531
|
function pushFailedRule(result, repairMode, key, detail) {
|
|
1427
1532
|
switch (repairMode) {
|
|
@@ -1442,158 +1547,43 @@ function pushFailedRule(result, repairMode, key, detail) {
|
|
|
1442
1547
|
break;
|
|
1443
1548
|
}
|
|
1444
1549
|
}
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
if (rule.meta.repairMode !== "standard") continue;
|
|
1455
|
-
if (rule.meta.dependsOn?.includes("config_syntax_check")) continue;
|
|
1456
|
-
rule.repair({
|
|
1457
|
-
config: {},
|
|
1458
|
-
configPath: input.configPath,
|
|
1459
|
-
vars: input.vars,
|
|
1460
|
-
providerDeps: {
|
|
1461
|
-
usesMiaodaProvider: false,
|
|
1462
|
-
usesMiaodaSecretProvider: false
|
|
1463
|
-
},
|
|
1464
|
-
templateVars: input.templateVars
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
const JSON5 = loadJSON5();
|
|
1468
|
-
let config;
|
|
1469
|
-
try {
|
|
1470
|
-
config = JSON5.parse(readFile(input.configPath));
|
|
1471
|
-
} catch (e) {
|
|
1472
|
-
return {
|
|
1473
|
-
success: false,
|
|
1474
|
-
error: "cannot parse config for repair: " + e.message
|
|
1475
|
-
};
|
|
1476
|
-
}
|
|
1477
|
-
const deps = analyzeProviderDeps(config);
|
|
1478
|
-
const ctx = {
|
|
1479
|
-
config,
|
|
1480
|
-
configPath: input.configPath,
|
|
1481
|
-
vars: input.vars,
|
|
1482
|
-
providerDeps: deps,
|
|
1483
|
-
templateVars: input.templateVars
|
|
1484
|
-
};
|
|
1485
|
-
let configDirty = false;
|
|
1486
|
-
for (const rule of rules) {
|
|
1487
|
-
if (!failedSet.has(rule.meta.key)) continue;
|
|
1488
|
-
if (rule.meta.repairMode !== "standard") continue;
|
|
1489
|
-
if (!rule.meta.dependsOn?.includes("config_syntax_check")) continue;
|
|
1490
|
-
rule.repair(ctx);
|
|
1491
|
-
configDirty = true;
|
|
1492
|
-
}
|
|
1493
|
-
if (configDirty) writeFile(input.configPath, JSON.stringify(config, null, 2));
|
|
1494
|
-
if (repairData.secretsContent && input.vars.secretsFilePath) writeFile(input.vars.secretsFilePath, repairData.secretsContent);
|
|
1495
|
-
if (repairData.providerKeyContent && input.vars.providerFilePath) writeFile(input.vars.providerFilePath, repairData.providerKeyContent);
|
|
1496
|
-
if (repairData.restartCommand) try {
|
|
1497
|
-
shell(repairData.restartCommand, 3e4);
|
|
1498
|
-
} catch (e) {
|
|
1499
|
-
return {
|
|
1500
|
-
success: false,
|
|
1501
|
-
error: "restart command failed: " + e.message
|
|
1502
|
-
};
|
|
1503
|
-
}
|
|
1504
|
-
return { success: true };
|
|
1505
|
-
} catch (e) {
|
|
1506
|
-
return {
|
|
1507
|
-
success: false,
|
|
1508
|
-
error: "repair failed: " + e.message
|
|
1509
|
-
};
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
//#endregion
|
|
1513
|
-
//#region src/paths.ts
|
|
1514
|
-
/**
|
|
1515
|
-
* Central directory for all ephemeral diagnose/reset artifacts: task status
|
|
1516
|
-
* files (`reset-<taskId>.json`) and human-readable step logs
|
|
1517
|
-
* (`reset-<taskId>.log`). Having everything under one dir makes debugging a
|
|
1518
|
-
* stuck reset much easier — `ls /tmp/openclaw-diagnose/` shows every recent
|
|
1519
|
-
* run, and each run's log is right next to its state.
|
|
1520
|
-
*/
|
|
1521
|
-
const DIAGNOSE_DIR = "/tmp/openclaw-diagnose";
|
|
1522
|
-
function resetResultFile(taskId) {
|
|
1523
|
-
return `${DIAGNOSE_DIR}/reset-${taskId}.json`;
|
|
1524
|
-
}
|
|
1525
|
-
function resetLogFile(taskId) {
|
|
1526
|
-
return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
|
|
1527
|
-
}
|
|
1528
|
-
/** Sandbox workspace root where openclaw config + agent state lives. */
|
|
1529
|
-
const WORKSPACE_DIR = "/home/gem/workspace/agent";
|
|
1530
|
-
/** File containing the provider key used by the openclaw miaoda provider. */
|
|
1531
|
-
const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
|
|
1532
|
-
/** File containing the miaoda openclaw secrets JSON. */
|
|
1533
|
-
const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
|
|
1534
|
-
/** Absolute path to the openclaw config JSON. */
|
|
1535
|
-
const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
|
|
1536
|
-
//#endregion
|
|
1537
|
-
//#region src/logger.ts
|
|
1538
|
-
/**
|
|
1539
|
-
* Shared CLI log file. Every log line the CLI emits — whether through
|
|
1540
|
-
* `console.error` (rules, helpers, errors) or through the per-task
|
|
1541
|
-
* `makeLogger` (reset worker) — is tee'd here so operators have a single
|
|
1542
|
-
* file to tail when diagnosing a sandbox.
|
|
1543
|
-
*
|
|
1544
|
-
* `/tmp` is ephemeral on sandbox restart; we rely on that for rotation
|
|
1545
|
-
* (no size-based rotation implemented).
|
|
1546
|
-
*/
|
|
1547
|
-
const CLI_LOG_FILE = "/tmp/openclaw-diagnose/cli.log";
|
|
1548
|
-
/** Append one line to the shared cli.log. Swallows any fs error —
|
|
1549
|
-
* logging must never break the business flow. */
|
|
1550
|
-
function appendCliLog(line) {
|
|
1551
|
-
try {
|
|
1552
|
-
const dir = node_path.default.dirname(CLI_LOG_FILE);
|
|
1553
|
-
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
1554
|
-
node_fs.default.appendFileSync(CLI_LOG_FILE, line);
|
|
1555
|
-
} catch {}
|
|
1556
|
-
}
|
|
1557
|
-
let stderrMirrorInstalled = false;
|
|
1558
|
-
/**
|
|
1559
|
-
* Install a process-wide `console.error` interceptor that mirrors each
|
|
1560
|
-
* line to BOTH the original stderr AND cli.log. Call once at CLI entry
|
|
1561
|
-
* before any subcommand dispatch; idempotent.
|
|
1562
|
-
*
|
|
1563
|
-
* Why console.error and not console.log: the CLI's stdout carries the
|
|
1564
|
-
* structured JSON result protocol consumed by sandbox_console and other
|
|
1565
|
-
* callers — any log line on stdout would corrupt JSON parsing. Rules,
|
|
1566
|
-
* helpers, and error paths therefore must route debug output through
|
|
1567
|
-
* console.error (stderr).
|
|
1568
|
-
*/
|
|
1569
|
-
function installStderrMirror() {
|
|
1570
|
-
if (stderrMirrorInstalled) return;
|
|
1571
|
-
stderrMirrorInstalled = true;
|
|
1572
|
-
const original = console.error.bind(console);
|
|
1573
|
-
console.error = (...args) => {
|
|
1574
|
-
original(...args);
|
|
1575
|
-
const body = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
|
|
1576
|
-
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
|
|
1577
1559
|
};
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
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;
|
|
1584
1582
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
1590
|
-
} catch {}
|
|
1591
|
-
return (msg) => {
|
|
1592
|
-
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}\n`;
|
|
1593
|
-
try {
|
|
1594
|
-
node_fs.default.appendFileSync(logFile, line);
|
|
1595
|
-
} catch {}
|
|
1596
|
-
appendCliLog(line);
|
|
1583
|
+
return {
|
|
1584
|
+
results,
|
|
1585
|
+
summary,
|
|
1586
|
+
aborted
|
|
1597
1587
|
};
|
|
1598
1588
|
}
|
|
1599
1589
|
//#endregion
|
|
@@ -1657,6 +1647,40 @@ function shellQuote(s) {
|
|
|
1657
1647
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1658
1648
|
}
|
|
1659
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
|
+
/**
|
|
1660
1684
|
* Extract an npm-packed gzipped tarball.
|
|
1661
1685
|
*
|
|
1662
1686
|
* ## The problem this works around
|
|
@@ -1730,59 +1754,510 @@ function extractTarballTolerant(tarball, destDir, opts = {}) {
|
|
|
1730
1754
|
}
|
|
1731
1755
|
}
|
|
1732
1756
|
//#endregion
|
|
1733
|
-
//#region src/
|
|
1734
|
-
/**
|
|
1735
|
-
*
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
const
|
|
1743
|
-
|
|
1744
|
-
const
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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
|
+
});
|
|
1781
2207
|
child.unref();
|
|
1782
2208
|
log(`spawned worker pid=${child.pid}`);
|
|
1783
2209
|
return { taskId };
|
|
1784
2210
|
}
|
|
1785
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
|
|
1786
2261
|
//#region src/oss/fetchManifest.ts
|
|
1787
2262
|
const MANIFEST_PREFIX = "builtin/manifests/openclaw/recommended/";
|
|
1788
2263
|
const MANIFEST_SUFFIX = ".json";
|
|
@@ -1794,10 +2269,24 @@ async function fetchManifest(ossFileMap, tag) {
|
|
|
1794
2269
|
const availStr = available.length ? available.join(", ") : "(none)";
|
|
1795
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.`);
|
|
1796
2271
|
}
|
|
1797
|
-
const
|
|
1798
|
-
|
|
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
|
+
}
|
|
1799
2278
|
return await res.json();
|
|
1800
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
|
+
}
|
|
1801
2290
|
async function downloadWithCache(pkg, ossFileMap, opts = {}) {
|
|
1802
2291
|
const cacheRoot = opts.cacheRoot ?? "/tmp/openclaw-diagnose/resources";
|
|
1803
2292
|
const shortHash = pkg.shasum.slice(0, 16);
|
|
@@ -1811,9 +2300,19 @@ async function downloadWithCache(pkg, ossFileMap, opts = {}) {
|
|
|
1811
2300
|
const expected = pkg.integrity.slice(7);
|
|
1812
2301
|
const tmpFile = node_path.default.join(destDir, `.tmp.${process.pid}.${node_crypto.default.randomBytes(4).toString("hex")}`);
|
|
1813
2302
|
try {
|
|
1814
|
-
const
|
|
1815
|
-
|
|
1816
|
-
|
|
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`);
|
|
1817
2316
|
const hasher = node_crypto.default.createHash("sha512");
|
|
1818
2317
|
const source = node_stream.Readable.fromWeb(res.body);
|
|
1819
2318
|
async function* teeAndHash(src) {
|
|
@@ -2338,16 +2837,6 @@ function writeSecretsAndRestart(vars, resetData, configDir, log) {
|
|
|
2338
2837
|
log(`restart.sh done in ${Date.now() - t}ms`);
|
|
2339
2838
|
} else log(`no restart.sh at ${restartScript}, skip`);
|
|
2340
2839
|
}
|
|
2341
|
-
/**
|
|
2342
|
-
* Run the 9-step reset process. Called from the worker entry point.
|
|
2343
|
-
*
|
|
2344
|
-
* Each step is an independent function. The orchestrator handles progress
|
|
2345
|
-
* reporting, error handling, and process-level exception guards.
|
|
2346
|
-
*
|
|
2347
|
-
* Template assets (openclaw.json + scripts/) are downloaded from OSS into a
|
|
2348
|
-
* scratch dir via `stageTemplate` before step 1 — there is no bundled
|
|
2349
|
-
* `template/` directory at runtime any more.
|
|
2350
|
-
*/
|
|
2351
2840
|
async function runReset(input, taskId, resultFile) {
|
|
2352
2841
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2353
2842
|
const { configPath, vars, resetData } = input;
|
|
@@ -2355,6 +2844,7 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2355
2844
|
const stagedDir = node_path.default.join(DIAGNOSE_DIR, `reset-${taskId}-template`);
|
|
2356
2845
|
let currentStep = 0;
|
|
2357
2846
|
let stepStartedAt = Date.now();
|
|
2847
|
+
const stepResults = [];
|
|
2358
2848
|
const log = makeLogger(resetLogFile(taskId));
|
|
2359
2849
|
log(`=== reset started, taskId=${taskId}, pid=${process.pid} ===`);
|
|
2360
2850
|
log(`configPath=${configPath}, configDir=${configDir}, stagedDir=${stagedDir}`);
|
|
@@ -2363,7 +2853,12 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2363
2853
|
const err = "resetData.ossFileMap missing or empty";
|
|
2364
2854
|
log(`ERROR: ${err}`);
|
|
2365
2855
|
markFailed(resultFile, 0, err, startedAt);
|
|
2366
|
-
|
|
2856
|
+
return {
|
|
2857
|
+
success: false,
|
|
2858
|
+
steps: stepResults,
|
|
2859
|
+
error: err,
|
|
2860
|
+
failedStep: 0
|
|
2861
|
+
};
|
|
2367
2862
|
}
|
|
2368
2863
|
let openclawTag;
|
|
2369
2864
|
if (resetData.openclawTag) openclawTag = resetData.openclawTag;
|
|
@@ -2373,7 +2868,12 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2373
2868
|
const err = e.message;
|
|
2374
2869
|
log(`ERROR: ${err}`);
|
|
2375
2870
|
markFailed(resultFile, 0, err, startedAt);
|
|
2376
|
-
|
|
2871
|
+
return {
|
|
2872
|
+
success: false,
|
|
2873
|
+
steps: stepResults,
|
|
2874
|
+
error: err,
|
|
2875
|
+
failedStep: 0
|
|
2876
|
+
};
|
|
2377
2877
|
}
|
|
2378
2878
|
log(`openclawTag=${openclawTag}`);
|
|
2379
2879
|
process.on("uncaughtException", (err) => {
|
|
@@ -2386,9 +2886,19 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2386
2886
|
markFailed(resultFile, currentStep, `unhandled rejection: ${reason}`, startedAt);
|
|
2387
2887
|
process.exit(1);
|
|
2388
2888
|
});
|
|
2389
|
-
/** 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. */
|
|
2390
2891
|
const step = (n) => {
|
|
2391
|
-
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
|
+
}
|
|
2392
2902
|
currentStep = n;
|
|
2393
2903
|
stepStartedAt = Date.now();
|
|
2394
2904
|
log(`--- step ${n}/${TOTAL_STEPS}: ${STEPS[n - 1]} ---`);
|
|
@@ -2414,14 +2924,38 @@ async function runReset(input, taskId, resultFile) {
|
|
|
2414
2924
|
await step8InstallExtensions(openclawTag, ossFileMap, log);
|
|
2415
2925
|
step(9);
|
|
2416
2926
|
writeSecretsAndRestart(vars, resetData, configDir, log);
|
|
2417
|
-
|
|
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
|
+
});
|
|
2418
2935
|
log("=== reset completed successfully ===");
|
|
2419
2936
|
markDone(resultFile, startedAt);
|
|
2937
|
+
return {
|
|
2938
|
+
success: true,
|
|
2939
|
+
steps: stepResults
|
|
2940
|
+
};
|
|
2420
2941
|
} catch (e) {
|
|
2421
2942
|
const err = e.message;
|
|
2422
|
-
|
|
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
|
+
});
|
|
2423
2952
|
markFailed(resultFile, currentStep, err, startedAt);
|
|
2424
|
-
|
|
2953
|
+
return {
|
|
2954
|
+
success: false,
|
|
2955
|
+
steps: stepResults,
|
|
2956
|
+
error: err,
|
|
2957
|
+
failedStep: currentStep
|
|
2958
|
+
};
|
|
2425
2959
|
} finally {
|
|
2426
2960
|
try {
|
|
2427
2961
|
node_fs.default.rmSync(stagedDir, {
|
|
@@ -2488,38 +3022,24 @@ function resolveOssFileMap(args) {
|
|
|
2488
3022
|
throw new Error("ossFileMap missing: provide --oss_file_map flag, ctx.install.ossFileMap, or resetData.ossFileMap");
|
|
2489
3023
|
}
|
|
2490
3024
|
//#endregion
|
|
2491
|
-
//#region src/innerapi/
|
|
3025
|
+
//#region src/innerapi/client.ts
|
|
2492
3026
|
/**
|
|
2493
|
-
*
|
|
2494
|
-
*
|
|
2495
|
-
* Mirrors the proven pattern in
|
|
2496
|
-
* `packages/openclaw/extensions/miaoda/src/shared/innerapi-client.ts`:
|
|
3027
|
+
* Shared innerapi HTTP client + path constants for `openclaw.*` calls.
|
|
2497
3028
|
*
|
|
2498
3029
|
* - `baseURL` from env `FORCE_AUTHN_INNERAPI_DOMAIN` (injected into every
|
|
2499
3030
|
* openclaw sandbox).
|
|
2500
3031
|
* - `platform: { enabled, tokenProvider: { type: 'file' } }` — the platform
|
|
2501
3032
|
* plugin auto-attaches the sandbox's identity JWT loaded from the
|
|
2502
3033
|
* rootfs token file. Same auth that the miaoda extension already uses.
|
|
2503
|
-
* - POST `/api/v1/studio/innerapi/integration_apis/call`
|
|
2504
|
-
*
|
|
2505
|
-
*
|
|
2506
|
-
* `
|
|
2507
|
-
*
|
|
2508
|
-
* `status_code` is a *string* ('0' = success).
|
|
2509
|
-
* Actual DoctorCtx lives in `data.output`.
|
|
2510
|
-
* - `x-tt-logid` header is logged on every failure path for cross-service
|
|
2511
|
-
* traceability.
|
|
2512
|
-
*
|
|
2513
|
-
* On HTTP 401 (sandbox identity token expired/invalid) we `process.exit(77)`
|
|
2514
|
-
* instead of throwing — the outer catch in `index.ts` cannot then mask auth
|
|
2515
|
-
* failure as a generic "Error: ...". Caller (e.g. sandbox_console) sees the
|
|
2516
|
-
* 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.
|
|
2517
3039
|
*/
|
|
2518
3040
|
const INNERAPI_CALL_PATH = "/api/v1/studio/innerapi/integration_apis/call";
|
|
2519
|
-
const API_NAME = "openclaw.get_doctor_ctx";
|
|
2520
3041
|
const BIZ_TYPE = "openclaw";
|
|
2521
3042
|
const API_TIMEOUT_MS = 3e4;
|
|
2522
|
-
const MAX_LOG_BODY = 500;
|
|
2523
3043
|
let clientInstance = null;
|
|
2524
3044
|
function getHttpClient() {
|
|
2525
3045
|
if (!clientInstance) {
|
|
@@ -2536,6 +3056,35 @@ function getHttpClient() {
|
|
|
2536
3056
|
}
|
|
2537
3057
|
return clientInstance;
|
|
2538
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";
|
|
2539
3088
|
/**
|
|
2540
3089
|
* Fetch the sandbox's DoctorCtx by calling the innerapi's generic
|
|
2541
3090
|
* `integration_apis/call` dispatcher with apiName=openclaw.get_doctor_ctx.
|
|
@@ -2543,14 +3092,19 @@ function getHttpClient() {
|
|
|
2543
3092
|
* Throws on HTTP (non-401) / decode / business errors. On 401 calls
|
|
2544
3093
|
* `process.exit(77)` directly.
|
|
2545
3094
|
*/
|
|
2546
|
-
async function fetchCtxViaInnerApi() {
|
|
3095
|
+
async function fetchCtxViaInnerApi(opts = {}) {
|
|
2547
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;
|
|
2548
3101
|
const body = {
|
|
2549
|
-
apiName: API_NAME,
|
|
2550
|
-
input
|
|
3102
|
+
apiName: API_NAME$1,
|
|
3103
|
+
input,
|
|
2551
3104
|
bizType: BIZ_TYPE
|
|
2552
3105
|
};
|
|
2553
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}` : ""}`);
|
|
2554
3108
|
const headers = { "Content-Type": "application/json" };
|
|
2555
3109
|
const ttEnv = process.env.X_TT_ENV;
|
|
2556
3110
|
if (ttEnv) headers["x-tt-env"] = ttEnv;
|
|
@@ -2580,7 +3134,7 @@ async function fetchCtxViaInnerApi() {
|
|
|
2580
3134
|
}
|
|
2581
3135
|
let preview = "";
|
|
2582
3136
|
try {
|
|
2583
|
-
preview = (await response.text()).slice(0,
|
|
3137
|
+
preview = (await response.text()).slice(0, 500);
|
|
2584
3138
|
} catch {}
|
|
2585
3139
|
throw new Error(`fetchCtxViaInnerApi HTTP ${response.status} ${response.statusText} (logID: ${logId}, durationMs: ${durationMs})${preview ? ` body=${preview}` : ""}`);
|
|
2586
3140
|
}
|
|
@@ -2594,6 +3148,7 @@ async function fetchCtxViaInnerApi() {
|
|
|
2594
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)}`);
|
|
2595
3149
|
const output = envelope.data?.output;
|
|
2596
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(",")}]`);
|
|
2597
3152
|
return output;
|
|
2598
3153
|
}
|
|
2599
3154
|
//#endregion
|
|
@@ -2612,26 +3167,40 @@ async function fetchCtxViaInnerApi() {
|
|
|
2612
3167
|
*/
|
|
2613
3168
|
function normalizeCtx(raw) {
|
|
2614
3169
|
const r = raw ?? {};
|
|
2615
|
-
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")
|
|
2616
|
-
app:
|
|
2617
|
-
install:
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
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
|
+
}
|
|
2630
3193
|
const vars = r.vars ?? {};
|
|
2631
3194
|
const resetData = r.resetData ?? {};
|
|
2632
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;
|
|
2633
3198
|
return {
|
|
2634
|
-
app:
|
|
3199
|
+
app: {
|
|
3200
|
+
...fillApp(vars),
|
|
3201
|
+
templateVars: legacyTemplateVars,
|
|
3202
|
+
recommendedOpenclawTag: legacyRecommendedTag
|
|
3203
|
+
},
|
|
2635
3204
|
install: {
|
|
2636
3205
|
openclawTag: r.install?.openclawTag ?? r.openclawTag,
|
|
2637
3206
|
ossFileMap: r.install?.ossFileMap ?? resetData.ossFileMap ?? r.ossFileMap ?? {}
|
|
@@ -2684,11 +3253,15 @@ function fillApp(src) {
|
|
|
2684
3253
|
function buildCheckInput(raw, configPathOverride) {
|
|
2685
3254
|
const r = raw ?? {};
|
|
2686
3255
|
if (r.configPath && r.vars) {
|
|
2687
|
-
|
|
3256
|
+
const out = configPathOverride ? {
|
|
2688
3257
|
...r,
|
|
2689
3258
|
configPath: configPathOverride
|
|
3259
|
+
} : { ...r };
|
|
3260
|
+
if (out.vars && !out.vars.templateVars) out.vars = {
|
|
3261
|
+
...out.vars,
|
|
3262
|
+
templateVars: out.templateVars ?? {}
|
|
2690
3263
|
};
|
|
2691
|
-
return
|
|
3264
|
+
return out;
|
|
2692
3265
|
}
|
|
2693
3266
|
const ctx = normalizeCtx(raw);
|
|
2694
3267
|
return {
|
|
@@ -2702,19 +3275,25 @@ function buildCheckInput(raw, configPathOverride) {
|
|
|
2702
3275
|
baseURL: ctx.app.baseURL,
|
|
2703
3276
|
expectedOrigins: ctx.app.expectedOrigins,
|
|
2704
3277
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
2705
|
-
secretsFilePath: SECRETS_FILE_PATH
|
|
3278
|
+
secretsFilePath: SECRETS_FILE_PATH,
|
|
3279
|
+
templateVars: ctx.app.templateVars,
|
|
3280
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
2706
3281
|
},
|
|
2707
|
-
templateVars: ctx.
|
|
3282
|
+
templateVars: ctx.app.templateVars
|
|
2708
3283
|
};
|
|
2709
3284
|
}
|
|
2710
3285
|
function buildRepairInput(raw, configPathOverride) {
|
|
2711
3286
|
const r = raw ?? {};
|
|
2712
3287
|
if (r.configPath && r.vars) {
|
|
2713
|
-
|
|
3288
|
+
const out = configPathOverride ? {
|
|
2714
3289
|
...r,
|
|
2715
3290
|
configPath: configPathOverride
|
|
3291
|
+
} : { ...r };
|
|
3292
|
+
if (out.vars && !out.vars.templateVars) out.vars = {
|
|
3293
|
+
...out.vars,
|
|
3294
|
+
templateVars: out.templateVars ?? {}
|
|
2716
3295
|
};
|
|
2717
|
-
return
|
|
3296
|
+
return out;
|
|
2718
3297
|
}
|
|
2719
3298
|
const ctx = normalizeCtx(raw);
|
|
2720
3299
|
return {
|
|
@@ -2728,23 +3307,29 @@ function buildRepairInput(raw, configPathOverride) {
|
|
|
2728
3307
|
baseURL: ctx.app.baseURL,
|
|
2729
3308
|
expectedOrigins: ctx.app.expectedOrigins,
|
|
2730
3309
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
2731
|
-
secretsFilePath: SECRETS_FILE_PATH
|
|
3310
|
+
secretsFilePath: SECRETS_FILE_PATH,
|
|
3311
|
+
templateVars: ctx.app.templateVars,
|
|
3312
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
2732
3313
|
},
|
|
2733
3314
|
repairData: {
|
|
2734
3315
|
secretsContent: ctx.secrets.secretsContent,
|
|
2735
3316
|
providerKeyContent: ctx.secrets.providerKeyContent
|
|
2736
3317
|
},
|
|
2737
|
-
templateVars: ctx.
|
|
3318
|
+
templateVars: ctx.app.templateVars
|
|
2738
3319
|
};
|
|
2739
3320
|
}
|
|
2740
3321
|
function buildResetInput(raw, configPathOverride) {
|
|
2741
3322
|
const r = raw ?? {};
|
|
2742
3323
|
if (r.configPath && r.vars && r.resetData) {
|
|
2743
|
-
|
|
3324
|
+
const out = configPathOverride ? {
|
|
2744
3325
|
...r,
|
|
2745
3326
|
configPath: configPathOverride
|
|
3327
|
+
} : { ...r };
|
|
3328
|
+
if (out.vars && !out.vars.templateVars) out.vars = {
|
|
3329
|
+
...out.vars,
|
|
3330
|
+
templateVars: out.resetData?.templateVars ?? {}
|
|
2746
3331
|
};
|
|
2747
|
-
return
|
|
3332
|
+
return out;
|
|
2748
3333
|
}
|
|
2749
3334
|
const ctx = normalizeCtx(raw);
|
|
2750
3335
|
return {
|
|
@@ -2758,7 +3343,9 @@ function buildResetInput(raw, configPathOverride) {
|
|
|
2758
3343
|
baseURL: ctx.app.baseURL,
|
|
2759
3344
|
expectedOrigins: ctx.app.expectedOrigins,
|
|
2760
3345
|
providerFilePath: PROVIDER_FILE_PATH,
|
|
2761
|
-
secretsFilePath: SECRETS_FILE_PATH
|
|
3346
|
+
secretsFilePath: SECRETS_FILE_PATH,
|
|
3347
|
+
templateVars: ctx.app.templateVars,
|
|
3348
|
+
recommendedOpenclawTag: ctx.app.recommendedOpenclawTag
|
|
2762
3349
|
},
|
|
2763
3350
|
resetData: {
|
|
2764
3351
|
templateVars: ctx.reset.templateVars,
|
|
@@ -2772,30 +3359,379 @@ function buildResetInput(raw, configPathOverride) {
|
|
|
2772
3359
|
}
|
|
2773
3360
|
//#endregion
|
|
2774
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
|
+
*/
|
|
2775
3393
|
async function runDoctor(rawCtx, opts) {
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
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;
|
|
2784
3647
|
}
|
|
2785
|
-
const check = runCheck(buildCheckInput(rawCtx, opts.configPath));
|
|
2786
|
-
if (!opts.fix) return { failedRules: check.failedRules };
|
|
2787
|
-
const repairInput = buildRepairInput(rawCtx, opts.configPath);
|
|
2788
|
-
repairInput.failedRules = check.failedRules.standard;
|
|
2789
3648
|
return {
|
|
2790
|
-
|
|
2791
|
-
|
|
3649
|
+
results,
|
|
3650
|
+
summary,
|
|
3651
|
+
aborted
|
|
2792
3652
|
};
|
|
2793
3653
|
}
|
|
2794
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
|
|
2795
3731
|
//#region src/help.ts
|
|
2796
3732
|
const BIN = "mclaw-diagnose";
|
|
2797
3733
|
function versionBanner() {
|
|
2798
|
-
return `v0.1.
|
|
3734
|
+
return `v0.1.4-alpha.1`;
|
|
2799
3735
|
}
|
|
2800
3736
|
const COMMANDS = [
|
|
2801
3737
|
{
|
|
@@ -2803,46 +3739,85 @@ const COMMANDS = [
|
|
|
2803
3739
|
hidden: false,
|
|
2804
3740
|
summary: "Diagnose openclaw config; apply repairs with --fix",
|
|
2805
3741
|
help: `USAGE
|
|
2806
|
-
${BIN} doctor [--fix] [--rule=<key>]...
|
|
3742
|
+
${BIN} doctor [--fix] [--show-diff] [--rule=<key>]... [--skip-rule=<key>]...
|
|
3743
|
+
[--caller=<n>] [--trace-id=<id>]
|
|
2807
3744
|
|
|
2808
3745
|
DESCRIPTION
|
|
2809
|
-
Fetches DoctorCtx via innerapi, then
|
|
2810
|
-
on the flags. Output is a single JSON object on stdout.
|
|
3746
|
+
Fetches DoctorCtx via innerapi, then orchestrates each rule:
|
|
2811
3747
|
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
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'
|
|
2818
3755
|
|
|
2819
|
-
|
|
2820
|
-
then repairs every failing standard-mode rule.
|
|
2821
|
-
Returns
|
|
2822
|
-
{ check: {...}, repair: {...} }
|
|
2823
|
-
Use this as the default "fix everything" action.
|
|
3756
|
+
Output is a single JSON object on stdout:
|
|
2824
3757
|
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
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.
|
|
2831
3775
|
|
|
2832
3776
|
OPTIONS
|
|
2833
|
-
--fix
|
|
2834
|
-
|
|
2835
|
-
|
|
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).
|
|
2836
3804
|
|
|
2837
3805
|
EXAMPLES
|
|
2838
|
-
${BIN} doctor
|
|
2839
|
-
${BIN} doctor --fix
|
|
2840
|
-
${BIN} doctor --fix --rule=gateway
|
|
2841
|
-
${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)
|
|
2842
3815
|
|
|
2843
3816
|
EXIT CODES
|
|
2844
|
-
0
|
|
2845
|
-
|
|
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.
|
|
2846
3821
|
77 innerapi authentication failed (sandbox JWT expired/invalid)
|
|
2847
3822
|
`
|
|
2848
3823
|
},
|
|
@@ -3031,6 +4006,62 @@ function formatCommandHelp(name) {
|
|
|
3031
4006
|
return cmd.help;
|
|
3032
4007
|
}
|
|
3033
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
|
|
3034
4065
|
//#region src/index.ts
|
|
3035
4066
|
const args = node_process.default.argv.slice(2);
|
|
3036
4067
|
const mode = args.find((a) => !a.startsWith("-"));
|
|
@@ -3064,8 +4095,39 @@ function getMultiFlag(args, name) {
|
|
|
3064
4095
|
const prefix = `--${name}=`;
|
|
3065
4096
|
return args.filter((a) => a.startsWith(prefix)).map((a) => a.slice(prefix.length));
|
|
3066
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
|
+
}
|
|
3067
4129
|
async function main() {
|
|
3068
|
-
installStderrMirror();
|
|
4130
|
+
installStderrMirror({ stderrEnabled: args.includes("-x") });
|
|
3069
4131
|
const helpFlags = parseHelpFlags(args);
|
|
3070
4132
|
if (mode && helpFlags.help) {
|
|
3071
4133
|
const body = formatCommandHelp(mode);
|
|
@@ -3085,22 +4147,79 @@ async function main() {
|
|
|
3085
4147
|
node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));
|
|
3086
4148
|
node_process.default.exit(1);
|
|
3087
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}`);
|
|
3088
4158
|
switch (mode) {
|
|
3089
|
-
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
|
+
}
|
|
3090
4174
|
case "repair": {
|
|
3091
|
-
const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi(
|
|
3092
|
-
|
|
3093
|
-
|
|
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
|
+
});
|
|
3094
4187
|
break;
|
|
3095
4188
|
}
|
|
3096
4189
|
case "doctor": {
|
|
3097
4190
|
const fix = args.includes("--fix");
|
|
3098
4191
|
const rules = getMultiFlag(args, "rule");
|
|
3099
|
-
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, {
|
|
3100
4210
|
fix,
|
|
3101
|
-
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
|
|
3102
4220
|
});
|
|
3103
|
-
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);
|
|
3104
4223
|
break;
|
|
3105
4224
|
}
|
|
3106
4225
|
case "reset":
|
|
@@ -3109,7 +4228,11 @@ async function main() {
|
|
|
3109
4228
|
let ctxBase64;
|
|
3110
4229
|
if (ctxArg) ctxBase64 = ctxArg.slice(6);
|
|
3111
4230
|
else {
|
|
3112
|
-
const fetched = await fetchCtxViaInnerApi(
|
|
4231
|
+
const fetched = await fetchCtxViaInnerApi({
|
|
4232
|
+
populate: planCtxPopulate({ command: "reset" }),
|
|
4233
|
+
caller,
|
|
4234
|
+
traceId
|
|
4235
|
+
});
|
|
3113
4236
|
ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
|
|
3114
4237
|
}
|
|
3115
4238
|
console.log(JSON.stringify(startAsyncReset(ctxBase64)));
|
|
@@ -3120,7 +4243,22 @@ async function main() {
|
|
|
3120
4243
|
node_process.default.exit(1);
|
|
3121
4244
|
}
|
|
3122
4245
|
const resultFile = resetResultFile(taskId);
|
|
3123
|
-
|
|
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);
|
|
3124
4262
|
} else {
|
|
3125
4263
|
console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
|
|
3126
4264
|
node_process.default.exit(1);
|
|
@@ -3143,12 +4281,34 @@ async function main() {
|
|
|
3143
4281
|
}
|
|
3144
4282
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
3145
4283
|
let installOssFileMap;
|
|
3146
|
-
|
|
3147
|
-
|
|
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({
|
|
3148
4294
|
ossFileMapFlag,
|
|
3149
4295
|
installOssFileMap
|
|
3150
|
-
})
|
|
3151
|
-
|
|
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;
|
|
3152
4312
|
break;
|
|
3153
4313
|
}
|
|
3154
4314
|
case "install-extension": {
|
|
@@ -3164,18 +4324,45 @@ async function main() {
|
|
|
3164
4324
|
const skipConfigUpdate = args.includes("--skip-config-update");
|
|
3165
4325
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
3166
4326
|
let installOssFileMap;
|
|
3167
|
-
|
|
3168
|
-
|
|
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({
|
|
3169
4337
|
ossFileMapFlag,
|
|
3170
4338
|
installOssFileMap
|
|
3171
|
-
}), {
|
|
3172
|
-
all,
|
|
3173
|
-
names: names.length > 0 ? names : void 0,
|
|
3174
|
-
homeBase,
|
|
3175
|
-
configPath,
|
|
3176
|
-
skipConfigUpdate
|
|
3177
4339
|
});
|
|
3178
|
-
|
|
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;
|
|
3179
4366
|
break;
|
|
3180
4367
|
}
|
|
3181
4368
|
case "download-resource": {
|
|
@@ -3193,16 +4380,43 @@ async function main() {
|
|
|
3193
4380
|
}
|
|
3194
4381
|
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
3195
4382
|
let installOssFileMap;
|
|
3196
|
-
|
|
3197
|
-
|
|
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({
|
|
3198
4393
|
ossFileMapFlag,
|
|
3199
4394
|
installOssFileMap
|
|
3200
|
-
}), {
|
|
3201
|
-
role,
|
|
3202
|
-
name,
|
|
3203
|
-
dir
|
|
3204
4395
|
});
|
|
3205
|
-
|
|
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;
|
|
3206
4420
|
break;
|
|
3207
4421
|
}
|
|
3208
4422
|
default:
|
|
@@ -3210,10 +4424,12 @@ async function main() {
|
|
|
3210
4424
|
node_process.default.stderr.write(formatTopLevelHelp(helpFlags.expert));
|
|
3211
4425
|
node_process.default.exit(1);
|
|
3212
4426
|
}
|
|
4427
|
+
console.error(`${mode}: end totalMs=${Date.now() - t0}`);
|
|
3213
4428
|
}
|
|
3214
4429
|
main().catch((err) => {
|
|
3215
4430
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3216
|
-
console.error(
|
|
4431
|
+
console.error(`${mode ?? "<no-mode>"}: error message=${msg}`);
|
|
4432
|
+
node_process.default.stderr.write(`Error: ${msg}\n`);
|
|
3217
4433
|
node_process.default.exit(1);
|
|
3218
4434
|
});
|
|
3219
4435
|
//#endregion
|