@lark-apaas/openclaw-scripts-diagnose-cli 0.1.1-alpha.22 → 0.1.1-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/index.cjs +951 -235
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,12 @@ let node_path = require("node:path");
|
|
|
31
31
|
node_path = __toESM(node_path);
|
|
32
32
|
let node_child_process = require("node:child_process");
|
|
33
33
|
let node_crypto = require("node:crypto");
|
|
34
|
+
node_crypto = __toESM(node_crypto);
|
|
35
|
+
let node_stream = require("node:stream");
|
|
36
|
+
let node_stream_promises = require("node:stream/promises");
|
|
37
|
+
let node_assert = require("node:assert");
|
|
38
|
+
node_assert = __toESM(node_assert);
|
|
39
|
+
let _lark_apaas_http_client = require("@lark-apaas/http-client");
|
|
34
40
|
//#region src/rule-engine/base.ts
|
|
35
41
|
/** Abstract base class for all diagnose rules */
|
|
36
42
|
var DiagnoseRule = class {
|
|
@@ -127,6 +133,14 @@ function isValidJWT(token) {
|
|
|
127
133
|
return false;
|
|
128
134
|
}
|
|
129
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Return `val` as a plain-object record (non-null, non-array object), or
|
|
138
|
+
* `undefined` otherwise. Cheaper than `getNestedMap` when the value is already
|
|
139
|
+
* at hand.
|
|
140
|
+
*/
|
|
141
|
+
function asRecord(val) {
|
|
142
|
+
return val != null && typeof val === "object" && !Array.isArray(val) ? val : void 0;
|
|
143
|
+
}
|
|
130
144
|
/** Set a deeply nested value, creating intermediate objects as needed. */
|
|
131
145
|
function setNestedValue(obj, keys, value) {
|
|
132
146
|
let current = obj;
|
|
@@ -268,7 +282,7 @@ function findHighestBackup(backupFiles) {
|
|
|
268
282
|
}
|
|
269
283
|
let ConfigFileBackupRule = class ConfigFileBackupRule extends DiagnoseRule {
|
|
270
284
|
validate(ctx) {
|
|
271
|
-
const configPath = ctx
|
|
285
|
+
const { configPath } = ctx;
|
|
272
286
|
if (!configPath) return {
|
|
273
287
|
pass: false,
|
|
274
288
|
message: "configPath not provided"
|
|
@@ -281,7 +295,7 @@ let ConfigFileBackupRule = class ConfigFileBackupRule extends DiagnoseRule {
|
|
|
281
295
|
return { pass: true };
|
|
282
296
|
}
|
|
283
297
|
repair(ctx) {
|
|
284
|
-
const configPath = ctx
|
|
298
|
+
const { configPath } = ctx;
|
|
285
299
|
if (!configPath) return;
|
|
286
300
|
const best = findHighestBackup(findBackupFiles(configPath));
|
|
287
301
|
if (!best) return;
|
|
@@ -310,7 +324,7 @@ function hasBackupFiles(configPath) {
|
|
|
310
324
|
}
|
|
311
325
|
let ConfigFileMissingRule = class ConfigFileMissingRule extends DiagnoseRule {
|
|
312
326
|
validate(ctx) {
|
|
313
|
-
const configPath = ctx
|
|
327
|
+
const { configPath } = ctx;
|
|
314
328
|
if (!configPath) return {
|
|
315
329
|
pass: false,
|
|
316
330
|
message: "configPath not provided"
|
|
@@ -332,7 +346,7 @@ ConfigFileMissingRule = __decorate([Rule({
|
|
|
332
346
|
//#region src/rules/config-syntax.ts
|
|
333
347
|
let ConfigSyntaxRule = class ConfigSyntaxRule extends DiagnoseRule {
|
|
334
348
|
validate(ctx) {
|
|
335
|
-
const configPath = ctx
|
|
349
|
+
const { configPath } = ctx;
|
|
336
350
|
if (!fileExists(configPath)) return { pass: true };
|
|
337
351
|
try {
|
|
338
352
|
loadJSON5().parse(readFile(configPath));
|
|
@@ -600,27 +614,6 @@ FeishuChannelRule = _FeishuChannelRule = __decorate([Rule({
|
|
|
600
614
|
repairMode: "standard"
|
|
601
615
|
})], FeishuChannelRule);
|
|
602
616
|
//#endregion
|
|
603
|
-
//#region src/rules/feishu-default-account.ts
|
|
604
|
-
let FeishuDefaultAccountRule = class FeishuDefaultAccountRule extends DiagnoseRule {
|
|
605
|
-
validate(ctx) {
|
|
606
|
-
const feishu = getNestedMap(ctx.config, "channels", "feishu");
|
|
607
|
-
if (feishu && "defaultAccount" in feishu) return {
|
|
608
|
-
pass: false,
|
|
609
|
-
message: "channels.feishu.defaultAccount should be removed"
|
|
610
|
-
};
|
|
611
|
-
return { pass: true };
|
|
612
|
-
}
|
|
613
|
-
repair(ctx) {
|
|
614
|
-
const feishu = getNestedMap(ctx.config, "channels", "feishu");
|
|
615
|
-
if (feishu && "defaultAccount" in feishu) delete feishu.defaultAccount;
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
FeishuDefaultAccountRule = __decorate([Rule({
|
|
619
|
-
key: "feishu_default_account",
|
|
620
|
-
dependsOn: ["config_syntax_check"],
|
|
621
|
-
repairMode: "standard"
|
|
622
|
-
})], FeishuDefaultAccountRule);
|
|
623
|
-
//#endregion
|
|
624
617
|
//#region src/rules/gateway.ts
|
|
625
618
|
var _GatewayRule;
|
|
626
619
|
let GatewayRule = class GatewayRule extends DiagnoseRule {
|
|
@@ -884,6 +877,124 @@ SecretsRule = __decorate([Rule({
|
|
|
884
877
|
skipWhen: ({ hasMiaoda, deps }) => !hasMiaoda || !deps.usesMiaodaSecretProvider
|
|
885
878
|
})], SecretsRule);
|
|
886
879
|
//#endregion
|
|
880
|
+
//#region src/rules/miaoda-official-plugins-install-spec-unlock.ts
|
|
881
|
+
/**
|
|
882
|
+
* Official miaoda-side plugins that must track manifest — version-locked specs
|
|
883
|
+
* here block upgrades. Third-party / user-installed plugins are intentionally
|
|
884
|
+
* out of scope (users may pin them deliberately).
|
|
885
|
+
*/
|
|
886
|
+
const OFFICIAL_PLUGIN_NAMES = new Set([
|
|
887
|
+
"openclaw-extension-miaoda",
|
|
888
|
+
"openclaw-extension-miaoda-coding",
|
|
889
|
+
"openclaw-guardian-plugin",
|
|
890
|
+
"openclaw-mem0-plugin",
|
|
891
|
+
"openclaw-lark"
|
|
892
|
+
]);
|
|
893
|
+
const LOCKED_NPM_SPEC = /^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*@[^@/:#\s]+$/i;
|
|
894
|
+
function isLockedNpmSpec(spec) {
|
|
895
|
+
return typeof spec === "string" && LOCKED_NPM_SPEC.test(spec);
|
|
896
|
+
}
|
|
897
|
+
function unlockSpec(spec) {
|
|
898
|
+
const slash = spec.indexOf("/");
|
|
899
|
+
const cut = slash === -1 ? spec.indexOf("@") : spec.indexOf("@", slash + 1);
|
|
900
|
+
return spec.slice(0, cut);
|
|
901
|
+
}
|
|
902
|
+
/** Yield `[key, lockedSpec]` for every official-plugin install whose `spec` is locked. */
|
|
903
|
+
function* iterLockedOfficialInstalls(config) {
|
|
904
|
+
const installs = getNestedMap(config, "plugins", "installs");
|
|
905
|
+
if (!installs) return;
|
|
906
|
+
for (const [key, entry] of Object.entries(installs)) {
|
|
907
|
+
if (!OFFICIAL_PLUGIN_NAMES.has(key)) continue;
|
|
908
|
+
const spec = asRecord(entry)?.spec;
|
|
909
|
+
if (isLockedNpmSpec(spec)) yield [key, spec];
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
let MiaodaOfficialPluginsInstallSpecUnlockRule = class MiaodaOfficialPluginsInstallSpecUnlockRule extends DiagnoseRule {
|
|
913
|
+
validate(ctx) {
|
|
914
|
+
const locked = [...iterLockedOfficialInstalls(ctx.config)].map(([k]) => k);
|
|
915
|
+
if (locked.length === 0) return { pass: true };
|
|
916
|
+
return {
|
|
917
|
+
pass: false,
|
|
918
|
+
message: "plugins.installs 中官方插件存在锁版本的 spec: " + locked.sort().join(",")
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
repair(ctx) {
|
|
922
|
+
for (const [key, spec] of iterLockedOfficialInstalls(ctx.config)) setNestedValue(ctx.config, [
|
|
923
|
+
"plugins",
|
|
924
|
+
"installs",
|
|
925
|
+
key,
|
|
926
|
+
"spec"
|
|
927
|
+
], unlockSpec(spec));
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
MiaodaOfficialPluginsInstallSpecUnlockRule = __decorate([Rule({
|
|
931
|
+
key: "miaoda_official_plugins_install_spec_unlock",
|
|
932
|
+
dependsOn: ["config_syntax_check"],
|
|
933
|
+
repairMode: "standard"
|
|
934
|
+
})], MiaodaOfficialPluginsInstallSpecUnlockRule);
|
|
935
|
+
//#endregion
|
|
936
|
+
//#region src/rules/old-miaoda-plugins-cleanup.ts
|
|
937
|
+
const NEW_MIAODA = "openclaw-extension-miaoda";
|
|
938
|
+
const OLD_PLUGIN_NAMES = Object.freeze([
|
|
939
|
+
"openclaw-feishu-greeting",
|
|
940
|
+
"openclaw-miaoda-keepalive",
|
|
941
|
+
"feishu-greeting",
|
|
942
|
+
"miaoda-keepalive"
|
|
943
|
+
]);
|
|
944
|
+
function getPluginMaps(config) {
|
|
945
|
+
return {
|
|
946
|
+
entries: getNestedMap(config, "plugins", "entries"),
|
|
947
|
+
installs: getNestedMap(config, "plugins", "installs")
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function getExtensionsDir(configPath) {
|
|
951
|
+
return node_path.default.join(node_path.default.dirname(configPath), "extensions");
|
|
952
|
+
}
|
|
953
|
+
function hasNewMiaoda({ entries, installs }) {
|
|
954
|
+
return asRecord(entries?.[NEW_MIAODA]) != null || asRecord(installs?.[NEW_MIAODA]) != null;
|
|
955
|
+
}
|
|
956
|
+
function findResiduals({ entries, installs }, extensionsDir) {
|
|
957
|
+
return OLD_PLUGIN_NAMES.filter((name) => entries?.[name] != null || installs?.[name] != null || node_fs.default.existsSync(node_path.default.join(extensionsDir, name)));
|
|
958
|
+
}
|
|
959
|
+
let OldMiaodaPluginsCleanupRule = class OldMiaodaPluginsCleanupRule extends DiagnoseRule {
|
|
960
|
+
validate(ctx) {
|
|
961
|
+
const maps = getPluginMaps(ctx.config);
|
|
962
|
+
if (!hasNewMiaoda(maps)) return { pass: true };
|
|
963
|
+
const residuals = findResiduals(maps, getExtensionsDir(ctx.configPath));
|
|
964
|
+
if (residuals.length === 0) return { pass: true };
|
|
965
|
+
return {
|
|
966
|
+
pass: false,
|
|
967
|
+
message: "旧 miaoda 插件残留: " + residuals.sort().join(",")
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
repair(ctx) {
|
|
971
|
+
const maps = getPluginMaps(ctx.config);
|
|
972
|
+
if (!hasNewMiaoda(maps)) return;
|
|
973
|
+
const extensionsDir = getExtensionsDir(ctx.configPath);
|
|
974
|
+
const { entries, installs } = maps;
|
|
975
|
+
for (const name of OLD_PLUGIN_NAMES) {
|
|
976
|
+
if (entries && name in entries) delete entries[name];
|
|
977
|
+
if (installs && name in installs) delete installs[name];
|
|
978
|
+
const target = node_path.default.join(extensionsDir, name);
|
|
979
|
+
const rel = node_path.default.relative(extensionsDir, target);
|
|
980
|
+
if (!rel || rel.startsWith("..") || node_path.default.isAbsolute(rel)) continue;
|
|
981
|
+
try {
|
|
982
|
+
node_fs.default.rmSync(target, {
|
|
983
|
+
recursive: true,
|
|
984
|
+
force: true
|
|
985
|
+
});
|
|
986
|
+
} catch (e) {
|
|
987
|
+
console.error(`[old_miaoda_plugins_cleanup] rmSync ${target} failed: ${e.message}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
OldMiaodaPluginsCleanupRule = __decorate([Rule({
|
|
993
|
+
key: "old_miaoda_plugins_cleanup",
|
|
994
|
+
dependsOn: ["config_syntax_check"],
|
|
995
|
+
repairMode: "standard"
|
|
996
|
+
})], OldMiaodaPluginsCleanupRule);
|
|
997
|
+
//#endregion
|
|
887
998
|
//#region src/check.ts
|
|
888
999
|
function runCheck(input) {
|
|
889
1000
|
const result = { failedRules: {
|
|
@@ -896,12 +1007,14 @@ function runCheck(input) {
|
|
|
896
1007
|
const failedKeys = /* @__PURE__ */ new Set();
|
|
897
1008
|
let configParsed = false;
|
|
898
1009
|
let ctx = {
|
|
899
|
-
config: {
|
|
1010
|
+
config: {},
|
|
1011
|
+
configPath: input.configPath,
|
|
900
1012
|
vars: input.vars,
|
|
901
1013
|
providerDeps: {
|
|
902
1014
|
usesMiaodaProvider: false,
|
|
903
1015
|
usesMiaodaSecretProvider: false
|
|
904
|
-
}
|
|
1016
|
+
},
|
|
1017
|
+
templateVars: input.templateVars
|
|
905
1018
|
};
|
|
906
1019
|
for (const rule of rules) {
|
|
907
1020
|
const meta = rule.meta;
|
|
@@ -912,8 +1025,10 @@ function runCheck(input) {
|
|
|
912
1025
|
const deps = analyzeProviderDeps(parsed);
|
|
913
1026
|
ctx = {
|
|
914
1027
|
config: parsed,
|
|
1028
|
+
configPath: input.configPath,
|
|
915
1029
|
vars: input.vars,
|
|
916
|
-
providerDeps: deps
|
|
1030
|
+
providerDeps: deps,
|
|
1031
|
+
templateVars: input.templateVars
|
|
917
1032
|
};
|
|
918
1033
|
configParsed = true;
|
|
919
1034
|
} catch {
|
|
@@ -965,12 +1080,14 @@ function runRepair(input) {
|
|
|
965
1080
|
if (rule.meta.repairMode !== "standard") continue;
|
|
966
1081
|
if (rule.meta.dependsOn?.includes("config_syntax_check")) continue;
|
|
967
1082
|
rule.repair({
|
|
968
|
-
config: {
|
|
1083
|
+
config: {},
|
|
1084
|
+
configPath: input.configPath,
|
|
969
1085
|
vars: input.vars,
|
|
970
1086
|
providerDeps: {
|
|
971
1087
|
usesMiaodaProvider: false,
|
|
972
1088
|
usesMiaodaSecretProvider: false
|
|
973
|
-
}
|
|
1089
|
+
},
|
|
1090
|
+
templateVars: input.templateVars
|
|
974
1091
|
});
|
|
975
1092
|
}
|
|
976
1093
|
const JSON5 = loadJSON5();
|
|
@@ -986,8 +1103,10 @@ function runRepair(input) {
|
|
|
986
1103
|
const deps = analyzeProviderDeps(config);
|
|
987
1104
|
const ctx = {
|
|
988
1105
|
config,
|
|
1106
|
+
configPath: input.configPath,
|
|
989
1107
|
vars: input.vars,
|
|
990
|
-
providerDeps: deps
|
|
1108
|
+
providerDeps: deps,
|
|
1109
|
+
templateVars: input.templateVars
|
|
991
1110
|
};
|
|
992
1111
|
let configDirty = false;
|
|
993
1112
|
for (const rule of rules) {
|
|
@@ -1032,6 +1151,14 @@ function resetResultFile(taskId) {
|
|
|
1032
1151
|
function resetLogFile(taskId) {
|
|
1033
1152
|
return `${DIAGNOSE_DIR}/reset-${taskId}.log`;
|
|
1034
1153
|
}
|
|
1154
|
+
/** Sandbox workspace root where openclaw config + agent state lives. */
|
|
1155
|
+
const WORKSPACE_DIR = "/home/gem/workspace/agent";
|
|
1156
|
+
/** File containing the provider key used by the openclaw miaoda provider. */
|
|
1157
|
+
const PROVIDER_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-provider-key";
|
|
1158
|
+
/** File containing the miaoda openclaw secrets JSON. */
|
|
1159
|
+
const SECRETS_FILE_PATH = "/home/gem/workspace/.force/openclaw/miaoda-openclaw-secrets.json";
|
|
1160
|
+
/** Absolute path to the openclaw config JSON. */
|
|
1161
|
+
const CONFIG_PATH = `${WORKSPACE_DIR}/openclaw.json`;
|
|
1035
1162
|
//#endregion
|
|
1036
1163
|
//#region src/logger.ts
|
|
1037
1164
|
function makeLogger(logFile) {
|
|
@@ -1100,6 +1227,234 @@ function startAsyncReset(ctxBase64) {
|
|
|
1100
1227
|
return { taskId };
|
|
1101
1228
|
}
|
|
1102
1229
|
//#endregion
|
|
1230
|
+
//#region src/oss/fetchManifest.ts
|
|
1231
|
+
async function fetchManifest(ossFileMap, tag) {
|
|
1232
|
+
const key = `builtin/manifests/openclaw/recommended/${tag}.json`;
|
|
1233
|
+
const url = ossFileMap[key];
|
|
1234
|
+
if (!url) throw new Error(`manifest signed URL missing for ${key}`);
|
|
1235
|
+
const res = await fetch(url);
|
|
1236
|
+
if (!res.ok) throw new Error(`fetch manifest failed: HTTP ${res.status} ${res.statusText}`);
|
|
1237
|
+
return await res.json();
|
|
1238
|
+
}
|
|
1239
|
+
async function downloadWithCache(pkg, ossFileMap, opts = {}) {
|
|
1240
|
+
const cacheRoot = opts.cacheRoot ?? "/tmp/openclaw-diagnose/resources";
|
|
1241
|
+
const shortHash = pkg.shasum.slice(0, 16);
|
|
1242
|
+
const destDir = node_path.default.join(cacheRoot, shortHash);
|
|
1243
|
+
const destFile = node_path.default.join(destDir, node_path.default.posix.basename(pkg.ossKey));
|
|
1244
|
+
node_fs.default.mkdirSync(destDir, { recursive: true });
|
|
1245
|
+
if (node_fs.default.existsSync(destFile)) return destFile;
|
|
1246
|
+
const url = ossFileMap[pkg.ossKey];
|
|
1247
|
+
if (!url) throw new Error(`signed URL missing for ${pkg.ossKey}`);
|
|
1248
|
+
if (!pkg.integrity.startsWith("sha512-")) throw new Error(`unsupported integrity format: ${pkg.integrity}`);
|
|
1249
|
+
const expected = pkg.integrity.slice(7);
|
|
1250
|
+
const tmpFile = node_path.default.join(destDir, `.tmp.${process.pid}.${node_crypto.default.randomBytes(4).toString("hex")}`);
|
|
1251
|
+
try {
|
|
1252
|
+
const res = await fetch(url);
|
|
1253
|
+
if (!res.ok) throw new Error(`download failed: HTTP ${res.status}`);
|
|
1254
|
+
if (!res.body) throw new Error(`download failed: empty body for ${pkg.ossKey}`);
|
|
1255
|
+
const hasher = node_crypto.default.createHash("sha512");
|
|
1256
|
+
const source = node_stream.Readable.fromWeb(res.body);
|
|
1257
|
+
async function* teeAndHash(src) {
|
|
1258
|
+
for await (const chunk of src) {
|
|
1259
|
+
hasher.update(chunk);
|
|
1260
|
+
yield chunk;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
await (0, node_stream_promises.pipeline)(source, teeAndHash, node_fs.default.createWriteStream(tmpFile));
|
|
1264
|
+
const actual = hasher.digest("base64");
|
|
1265
|
+
if (actual !== expected) {
|
|
1266
|
+
const envBypass = process.env.OPENCLAW_DEBUG_SKIP_INTEGRITY === "1";
|
|
1267
|
+
if (opts.skipIntegrity || envBypass) {
|
|
1268
|
+
const sourceLabel = opts.skipIntegrity ? "skipIntegrity=true" : "OPENCLAW_DEBUG_SKIP_INTEGRITY=1";
|
|
1269
|
+
console.error(`⚠ [downloadWithCache] INTEGRITY BYPASS for ${pkg.ossKey}: expected ${expected.slice(0, 12)}… got ${actual.slice(0, 12)}… — ${sourceLabel}. DO NOT use this flag in production.`);
|
|
1270
|
+
} else throw new Error(`integrity mismatch for ${pkg.ossKey}: expected ${expected} got ${actual}`);
|
|
1271
|
+
}
|
|
1272
|
+
node_fs.default.renameSync(tmpFile, destFile);
|
|
1273
|
+
return destFile;
|
|
1274
|
+
} catch (e) {
|
|
1275
|
+
try {
|
|
1276
|
+
node_fs.default.unlinkSync(tmpFile);
|
|
1277
|
+
} catch {}
|
|
1278
|
+
throw e;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
async function installOpenclaw(openclawTag, ossFileMap, opts = {}) {
|
|
1282
|
+
const homeBase = opts.homeBase ?? "/home/gem";
|
|
1283
|
+
const t0 = Date.now();
|
|
1284
|
+
const pkg = (await fetchManifest(ossFileMap, openclawTag)).packages.find((p) => p.role === "cli" && p.name === "openclaw");
|
|
1285
|
+
if (!pkg) throw new Error("install-openclaw: role=cli,name=openclaw not found in manifest");
|
|
1286
|
+
const targetDir = opts.targetDir ?? node_path.default.join(homeBase, pkg.installPath);
|
|
1287
|
+
const bakDir = targetDir + ".bak";
|
|
1288
|
+
const tarball = await downloadWithCache(pkg, ossFileMap, opts);
|
|
1289
|
+
console.error(`[install-openclaw] tag=${openclawTag} shasum=${pkg.shasum.slice(0, 12)}...`);
|
|
1290
|
+
if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
|
|
1291
|
+
recursive: true,
|
|
1292
|
+
force: true
|
|
1293
|
+
});
|
|
1294
|
+
const hadExisting = node_fs.default.existsSync(targetDir);
|
|
1295
|
+
if (hadExisting) node_fs.default.renameSync(targetDir, bakDir);
|
|
1296
|
+
try {
|
|
1297
|
+
node_fs.default.mkdirSync(targetDir, { recursive: true });
|
|
1298
|
+
(0, node_child_process.execSync)(`tar -xzf '${tarball}' -C '${targetDir}' --strip-components=1`, { stdio: "ignore" });
|
|
1299
|
+
if (!node_fs.default.existsSync(node_path.default.join(targetDir, "package.json"))) throw new Error("extracted tarball missing package.json");
|
|
1300
|
+
} catch (e) {
|
|
1301
|
+
try {
|
|
1302
|
+
node_fs.default.rmSync(targetDir, {
|
|
1303
|
+
recursive: true,
|
|
1304
|
+
force: true
|
|
1305
|
+
});
|
|
1306
|
+
} catch {}
|
|
1307
|
+
if (hadExisting && node_fs.default.existsSync(bakDir)) node_fs.default.renameSync(bakDir, targetDir);
|
|
1308
|
+
throw e;
|
|
1309
|
+
}
|
|
1310
|
+
if (node_fs.default.existsSync(bakDir)) node_fs.default.rmSync(bakDir, {
|
|
1311
|
+
recursive: true,
|
|
1312
|
+
force: true
|
|
1313
|
+
});
|
|
1314
|
+
console.error(`[install-openclaw] done in ${Date.now() - t0}ms`);
|
|
1315
|
+
}
|
|
1316
|
+
async function installExtension(tag, ossFileMap, opts = {}) {
|
|
1317
|
+
const homeBase = opts.homeBase ?? "/home/gem";
|
|
1318
|
+
const hasAll = !!opts.all;
|
|
1319
|
+
const hasNames = (opts.names?.length ?? 0) > 0;
|
|
1320
|
+
if (hasAll && hasNames) throw new Error("install-extension: --all and --extension are mutually exclusive");
|
|
1321
|
+
if (!hasAll && !hasNames) throw new Error("install-extension: must provide --all or --extension=<name>");
|
|
1322
|
+
const allExts = (await fetchManifest(ossFileMap, tag)).packages.filter((p) => p.role === "extension");
|
|
1323
|
+
let targets;
|
|
1324
|
+
if (hasAll) targets = allExts;
|
|
1325
|
+
else {
|
|
1326
|
+
const wanted = new Set(opts.names);
|
|
1327
|
+
targets = allExts.filter((p) => wanted.has(p.name) || p.packageName != null && wanted.has(p.packageName));
|
|
1328
|
+
const foundKeys = /* @__PURE__ */ new Set();
|
|
1329
|
+
for (const t of targets) {
|
|
1330
|
+
foundKeys.add(t.name);
|
|
1331
|
+
if (t.packageName) foundKeys.add(t.packageName);
|
|
1332
|
+
}
|
|
1333
|
+
const missing = opts.names.filter((n) => !foundKeys.has(n));
|
|
1334
|
+
if (missing.length > 0) throw new Error(`install-extension: not found in manifest: ${missing.join(", ")}`);
|
|
1335
|
+
}
|
|
1336
|
+
console.error(`[install-extension] tag=${tag} targets=${targets.length}`);
|
|
1337
|
+
const t0 = Date.now();
|
|
1338
|
+
const tarballs = await Promise.all(targets.map(async (p) => {
|
|
1339
|
+
const tb = await downloadWithCache(p, ossFileMap, opts);
|
|
1340
|
+
console.error(`[install-extension] ${p.name}: downloaded`);
|
|
1341
|
+
return {
|
|
1342
|
+
pkg: p,
|
|
1343
|
+
tarball: tb
|
|
1344
|
+
};
|
|
1345
|
+
}));
|
|
1346
|
+
for (const { pkg, tarball } of tarballs) {
|
|
1347
|
+
installOne(pkg, tarball, homeBase);
|
|
1348
|
+
console.error(`[install-extension] ${pkg.name}: installed`);
|
|
1349
|
+
}
|
|
1350
|
+
if (!opts.skipConfigUpdate) updatePluginInstalls(opts.configPath ?? node_path.default.join(homeBase, "workspace/agent/openclaw.json"), targets);
|
|
1351
|
+
else console.error(`[install-extension] skipConfigUpdate=true — not touching openclaw.json`);
|
|
1352
|
+
console.error(`[install-extension] done ${targets.length}/${targets.length} in ${Date.now() - t0}ms`);
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Merge each installed extension's installMetadata into openclaw.json's
|
|
1356
|
+
* plugins.installs[<pkg.name>]. Atomic write via tmp + rename.
|
|
1357
|
+
*
|
|
1358
|
+
* - No openclaw.json → log + return (not an error; some install contexts don't have it yet)
|
|
1359
|
+
* - Extension without installMetadata in manifest → skip that entry (log)
|
|
1360
|
+
* - Existing plugins.installs entries for other extensions left untouched
|
|
1361
|
+
*/
|
|
1362
|
+
function updatePluginInstalls(configPath, installedPkgs) {
|
|
1363
|
+
if (!node_fs.default.existsSync(configPath)) {
|
|
1364
|
+
console.error(`[install-extension] no config at ${configPath} — skip plugins.installs update`);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
const JSON5 = loadJSON5();
|
|
1368
|
+
const raw = node_fs.default.readFileSync(configPath, "utf-8");
|
|
1369
|
+
const config = JSON5.parse(raw);
|
|
1370
|
+
if (!config.plugins || typeof config.plugins !== "object") config.plugins = {};
|
|
1371
|
+
const plugins = config.plugins;
|
|
1372
|
+
if (!plugins.installs || typeof plugins.installs !== "object") plugins.installs = {};
|
|
1373
|
+
const installs = plugins.installs;
|
|
1374
|
+
let updated = 0;
|
|
1375
|
+
let skipped = 0;
|
|
1376
|
+
for (const pkg of installedPkgs) if (pkg.installMetadata) {
|
|
1377
|
+
installs[pkg.name] = pkg.installMetadata;
|
|
1378
|
+
updated++;
|
|
1379
|
+
} else skipped++;
|
|
1380
|
+
const tmpPath = configPath + ".installs-tmp";
|
|
1381
|
+
node_fs.default.writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
1382
|
+
node_fs.default.renameSync(tmpPath, configPath);
|
|
1383
|
+
console.error(`[install-extension] plugins.installs updated: ${updated} entry(ies) in ${configPath}` + (skipped > 0 ? ` (${skipped} package(s) without installMetadata skipped)` : ""));
|
|
1384
|
+
}
|
|
1385
|
+
function installOne(pkg, tarball, homeBase) {
|
|
1386
|
+
const destDir = node_path.default.join(homeBase, pkg.installPath);
|
|
1387
|
+
const stagingDir = destDir + ".new";
|
|
1388
|
+
const oldDir = destDir + ".old";
|
|
1389
|
+
node_fs.default.mkdirSync(node_path.default.dirname(destDir), { recursive: true });
|
|
1390
|
+
if (node_fs.default.existsSync(stagingDir)) node_fs.default.rmSync(stagingDir, {
|
|
1391
|
+
recursive: true,
|
|
1392
|
+
force: true
|
|
1393
|
+
});
|
|
1394
|
+
node_fs.default.mkdirSync(stagingDir);
|
|
1395
|
+
try {
|
|
1396
|
+
(0, node_child_process.execSync)(`tar -xzf '${tarball}' -C '${stagingDir}' --strip-components=1`, { stdio: "ignore" });
|
|
1397
|
+
if (!node_fs.default.existsSync(node_path.default.join(stagingDir, "package.json"))) throw new Error(`extension tarball missing package.json: ${pkg.name}`);
|
|
1398
|
+
} catch (e) {
|
|
1399
|
+
try {
|
|
1400
|
+
node_fs.default.rmSync(stagingDir, {
|
|
1401
|
+
recursive: true,
|
|
1402
|
+
force: true
|
|
1403
|
+
});
|
|
1404
|
+
} catch {}
|
|
1405
|
+
throw e;
|
|
1406
|
+
}
|
|
1407
|
+
const hadOld = node_fs.default.existsSync(destDir);
|
|
1408
|
+
if (hadOld) node_fs.default.renameSync(destDir, oldDir);
|
|
1409
|
+
node_fs.default.renameSync(stagingDir, destDir);
|
|
1410
|
+
if (hadOld && node_fs.default.existsSync(oldDir)) node_fs.default.rmSync(oldDir, {
|
|
1411
|
+
recursive: true,
|
|
1412
|
+
force: true
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1415
|
+
/**
|
|
1416
|
+
* Download + extract a config/template package to its install destination.
|
|
1417
|
+
*
|
|
1418
|
+
* Current manifest has all resources as format=tgz with content at the root
|
|
1419
|
+
* (config: openclaw.json file at root; template: scripts/ dir at root), so we
|
|
1420
|
+
* always `tar -xzf` without --strip-components into `dirname(fullInstallPath)`.
|
|
1421
|
+
* The final artefact ends up at exactly `homeBase + pkg.installPath`.
|
|
1422
|
+
*/
|
|
1423
|
+
async function downloadResource(tag, ossFileMap, opts) {
|
|
1424
|
+
const homeBase = opts.homeBase ?? "/home/gem";
|
|
1425
|
+
const pkg = (await fetchManifest(ossFileMap, tag)).packages.find((p) => p.role === opts.role && p.name === opts.name);
|
|
1426
|
+
if (!pkg) throw new Error(`download-resource: not found in manifest: role=${opts.role} name=${opts.name}`);
|
|
1427
|
+
const file = await downloadWithCache(pkg, ossFileMap, opts);
|
|
1428
|
+
const fullInstallPath = node_path.default.join(homeBase, pkg.installPath);
|
|
1429
|
+
const extractDir = opts.dir ?? node_path.default.dirname(fullInstallPath);
|
|
1430
|
+
node_fs.default.mkdirSync(extractDir, { recursive: true });
|
|
1431
|
+
const format = (pkg.format ?? "").toLowerCase();
|
|
1432
|
+
const lower = pkg.ossKey.toLowerCase();
|
|
1433
|
+
if (format === "tgz" || lower.endsWith(".tgz") || lower.endsWith(".tar.gz")) {
|
|
1434
|
+
(0, node_child_process.execSync)(`tar -xzf '${file}' -C '${extractDir}'`, { stdio: "ignore" });
|
|
1435
|
+
console.error(`[download-resource] ${opts.role}/${opts.name}: extracted to ${extractDir}`);
|
|
1436
|
+
} else {
|
|
1437
|
+
const basename = node_path.default.posix.basename(pkg.ossKey);
|
|
1438
|
+
node_fs.default.copyFileSync(file, node_path.default.join(extractDir, basename));
|
|
1439
|
+
console.error(`[download-resource] ${opts.role}/${opts.name}: copied ${basename} to ${extractDir}`);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
//#endregion
|
|
1443
|
+
//#region src/oss/getOpenclawTag.ts
|
|
1444
|
+
/**
|
|
1445
|
+
* Extracts the openclaw tag from the manifest key present in ossFileMap.
|
|
1446
|
+
* Avoids passing an extra ctx field — we already know the tag from the
|
|
1447
|
+
* well-known manifest key studio_server included.
|
|
1448
|
+
*
|
|
1449
|
+
* Manifest key shape: builtin/manifests/openclaw/recommended/<tag>.json
|
|
1450
|
+
*/
|
|
1451
|
+
function getOpenclawTagFromOssFileMap(ossFileMap) {
|
|
1452
|
+
const prefix = "builtin/manifests/openclaw/recommended/";
|
|
1453
|
+
const suffix = ".json";
|
|
1454
|
+
for (const key of Object.keys(ossFileMap)) if (key.startsWith(prefix) && key.endsWith(suffix)) return key.slice(39, -5);
|
|
1455
|
+
throw new Error("cannot resolve openclaw tag: ossFileMap missing manifest key");
|
|
1456
|
+
}
|
|
1457
|
+
//#endregion
|
|
1103
1458
|
//#region src/reset.ts
|
|
1104
1459
|
const STEPS = [
|
|
1105
1460
|
"备份当前配置",
|
|
@@ -1108,21 +1463,11 @@ const STEPS = [
|
|
|
1108
1463
|
"等待沙箱初始化完成",
|
|
1109
1464
|
"确认 openclaw 版本",
|
|
1110
1465
|
"合并核心备份配置",
|
|
1111
|
-
"
|
|
1466
|
+
"检查启动脚本",
|
|
1112
1467
|
"安装扩展",
|
|
1113
1468
|
"启动并验证"
|
|
1114
1469
|
];
|
|
1115
1470
|
const TOTAL_STEPS = STEPS.length;
|
|
1116
|
-
/** Pre-packed extensions archive on OSS. Update this URL when releasing a new version. */
|
|
1117
|
-
const EXTENSIONS_OSS_URL = "https://miaoda-template-online.oss-cn-beijing.aliyuncs.com/builtin/tool/pkg/openclaw-extensions-2026.4.9.tar.gz";
|
|
1118
|
-
/**
|
|
1119
|
-
* Directory holding the bundled openclaw template (openclaw.json + scripts/).
|
|
1120
|
-
* Synced from git@code.byted.org:apaas/miaoda-openclaw-template.git via
|
|
1121
|
-
* scripts/sync-template.sh and published alongside dist/.
|
|
1122
|
-
*
|
|
1123
|
-
* At runtime, __dirname points to dist/, so the template lives one level up.
|
|
1124
|
-
*/
|
|
1125
|
-
const TEMPLATE_DIR = node_path.default.resolve(__dirname, "..", "template");
|
|
1126
1471
|
function writeResultFile(resultFile, result) {
|
|
1127
1472
|
const dir = node_path.default.dirname(resultFile);
|
|
1128
1473
|
if (!node_fs.default.existsSync(dir)) node_fs.default.mkdirSync(dir, { recursive: true });
|
|
@@ -1160,6 +1505,33 @@ function markFailed(resultFile, step, error, startedAt) {
|
|
|
1160
1505
|
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1161
1506
|
});
|
|
1162
1507
|
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Download the template assets (config/openclaw.json + template/scripts) from
|
|
1510
|
+
* OSS into a scratch directory so the existing step 2 (generateDefaultConfig)
|
|
1511
|
+
* and step 7 (copyStartupScripts) can consume them as local files — the rest
|
|
1512
|
+
* of the orchestrator code stays untouched.
|
|
1513
|
+
*
|
|
1514
|
+
* Called once before step 1. The caller is responsible for rm -rf'ing
|
|
1515
|
+
* stagedDir in a finally{} block after the reset completes (or fails).
|
|
1516
|
+
*/
|
|
1517
|
+
async function stageTemplate(openclawTag, ossFileMap, stagedDir, configDir, log) {
|
|
1518
|
+
if (node_fs.default.existsSync(stagedDir)) node_fs.default.rmSync(stagedDir, {
|
|
1519
|
+
recursive: true,
|
|
1520
|
+
force: true
|
|
1521
|
+
});
|
|
1522
|
+
node_fs.default.mkdirSync(stagedDir, { recursive: true });
|
|
1523
|
+
await downloadResource(openclawTag, ossFileMap, {
|
|
1524
|
+
role: "config",
|
|
1525
|
+
name: "openclaw.json",
|
|
1526
|
+
dir: stagedDir
|
|
1527
|
+
});
|
|
1528
|
+
await downloadResource(openclawTag, ossFileMap, {
|
|
1529
|
+
role: "template",
|
|
1530
|
+
name: "scripts",
|
|
1531
|
+
dir: configDir
|
|
1532
|
+
});
|
|
1533
|
+
log(`staged openclaw.json to ${stagedDir}, scripts directly to ${configDir}/scripts`);
|
|
1534
|
+
}
|
|
1163
1535
|
/** Step 1: Backup current config as openclaw.json.bak.N */
|
|
1164
1536
|
function backupCurrentConfig(configPath, log) {
|
|
1165
1537
|
if (!fileExists(configPath)) {
|
|
@@ -1184,7 +1556,7 @@ function backupCurrentConfig(configPath, log) {
|
|
|
1184
1556
|
/** Step 2: Replace $$__XXX__ placeholders and write default config. */
|
|
1185
1557
|
function generateDefaultConfig(srcDir, configPath, templateVars, log) {
|
|
1186
1558
|
const srcConfigPath = node_path.default.join(srcDir, "openclaw.json");
|
|
1187
|
-
if (!fileExists(srcConfigPath)) throw new Error("
|
|
1559
|
+
if (!fileExists(srcConfigPath)) throw new Error("staged openclaw.json not found at " + srcConfigPath);
|
|
1188
1560
|
let content = node_fs.default.readFileSync(srcConfigPath, "utf-8");
|
|
1189
1561
|
let replaced = 0;
|
|
1190
1562
|
for (const [placeholder, value] of Object.entries(templateVars)) {
|
|
@@ -1205,11 +1577,14 @@ function killOpenclawProcesses(log) {
|
|
|
1205
1577
|
}
|
|
1206
1578
|
/**
|
|
1207
1579
|
* Step 4: Wait for the sandbox's own init (init_sandbox.sh / concurrent npm
|
|
1208
|
-
* install) to finish before we start our own
|
|
1209
|
-
*
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1212
|
-
*
|
|
1580
|
+
* install) to finish before we start our own work. Two processes sharing
|
|
1581
|
+
* ~/.npm cache + competing for disk/network just makes everything crawl;
|
|
1582
|
+
* letting init finish first is the cleanest way to get exclusive access.
|
|
1583
|
+
* Polls every 10s up to `maxWaitMs`. If the deadline is hit we fall through
|
|
1584
|
+
* anyway — better to try than to fail the reset outright.
|
|
1585
|
+
*
|
|
1586
|
+
* Kept even after we switched off `npm install` because the sandbox init
|
|
1587
|
+
* script still runs `npm install` for other packages and holds cache locks.
|
|
1213
1588
|
*/
|
|
1214
1589
|
function waitForInitNpm(maxWaitMs, log) {
|
|
1215
1590
|
const deadline = Date.now() + maxWaitMs;
|
|
@@ -1237,81 +1612,15 @@ function waitForInitNpm(maxWaitMs, log) {
|
|
|
1237
1612
|
log(`deadline (${maxWaitMs}ms) hit after ${polls} poll(s), proceeding anyway`);
|
|
1238
1613
|
}
|
|
1239
1614
|
/**
|
|
1240
|
-
* Step 5:
|
|
1241
|
-
*
|
|
1242
|
-
* Only checks/installs the binary — does NOT run `doctor --fix` any more.
|
|
1243
|
-
* Extensions are installed separately via OSS tar.gz (Step 8), which means
|
|
1244
|
-
* the openclaw-lark extension schema is already in place when openclaw
|
|
1245
|
-
* starts, avoiding the schema-priority mismatch that caused doctor to
|
|
1246
|
-
* reject valid config fields like threadSession / footer.
|
|
1615
|
+
* Step 5: Install openclaw from the OSS-provided tarball at the target tag,
|
|
1616
|
+
* then verify `openclaw --version` output contains that tag. No npm involved.
|
|
1247
1617
|
*/
|
|
1248
|
-
function
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
}
|
|
1255
|
-
if (isOpenclawInstalled()) {
|
|
1256
|
-
const updateCmd = `openclaw update${targetVersion ? " --tag " + targetVersion : ""} --yes --no-restart`;
|
|
1257
|
-
log(`openclaw installed but version mismatched, running: ${updateCmd}`);
|
|
1258
|
-
const timeout = 12 * 6e4;
|
|
1259
|
-
const t = Date.now();
|
|
1260
|
-
const tmpConfig = configPath + ".update-tmp";
|
|
1261
|
-
const hadConfig = node_fs.default.existsSync(configPath);
|
|
1262
|
-
if (hadConfig) {
|
|
1263
|
-
node_fs.default.renameSync(configPath, tmpConfig);
|
|
1264
|
-
log("temporarily hid config to bypass config guard");
|
|
1265
|
-
}
|
|
1266
|
-
try {
|
|
1267
|
-
shell(updateCmd, timeout);
|
|
1268
|
-
log(`openclaw update done in ${Date.now() - t}ms`);
|
|
1269
|
-
} catch (e) {
|
|
1270
|
-
const elapsed = Date.now() - t;
|
|
1271
|
-
if (elapsed >= timeout - 1e3) log(`openclaw update timed out after ${elapsed}ms (non-fatal, continuing)`);
|
|
1272
|
-
else throw e;
|
|
1273
|
-
} finally {
|
|
1274
|
-
if (hadConfig && node_fs.default.existsSync(tmpConfig)) {
|
|
1275
|
-
node_fs.default.renameSync(tmpConfig, configPath);
|
|
1276
|
-
log("restored config after update");
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
} else {
|
|
1280
|
-
log("openclaw binary not found, running full reinstall");
|
|
1281
|
-
fullReinstall(targetVersion, log);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
/** Check if openclaw command exists (regardless of version). */
|
|
1285
|
-
function isOpenclawInstalled() {
|
|
1286
|
-
try {
|
|
1287
|
-
shell("which openclaw 2>/dev/null", 5e3);
|
|
1288
|
-
return true;
|
|
1289
|
-
} catch {
|
|
1290
|
-
return false;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
/** Full uninstall + reinstall from npm (slow path, triggers postinstall). */
|
|
1294
|
-
function fullReinstall(targetVersion, log) {
|
|
1295
|
-
try {
|
|
1296
|
-
shell("npm uninstall -g openclaw 2>/dev/null || true", 6e4);
|
|
1297
|
-
} catch {}
|
|
1298
|
-
try {
|
|
1299
|
-
shell("rm -rf /home/gem/.npm-global/lib/node_modules/openclaw /home/gem/.npm-global/bin/openclaw 2>/dev/null || true", 1e4);
|
|
1300
|
-
log("force-cleaned residual openclaw global dir");
|
|
1301
|
-
} catch {}
|
|
1302
|
-
const installCmd = `npm i -g openclaw@${targetVersion || "latest"}`;
|
|
1303
|
-
log(`running: ${installCmd}`);
|
|
1304
|
-
const t = Date.now();
|
|
1305
|
-
shell(installCmd, 30 * 6e4);
|
|
1306
|
-
log(`npm install done in ${Date.now() - t}ms`);
|
|
1307
|
-
}
|
|
1308
|
-
/** Return true if `openclaw --version` output contains `targetVersion`. */
|
|
1309
|
-
function isOpenclawAtVersion(targetVersion) {
|
|
1310
|
-
try {
|
|
1311
|
-
return shell("openclaw --version 2>&1 || true", 1e4).includes(targetVersion);
|
|
1312
|
-
} catch {
|
|
1313
|
-
return false;
|
|
1314
|
-
}
|
|
1618
|
+
async function step5InstallOpenclaw(openclawTag, ossFileMap, log) {
|
|
1619
|
+
log(`install-openclaw tag=${openclawTag}`);
|
|
1620
|
+
await installOpenclaw(openclawTag, ossFileMap);
|
|
1621
|
+
const out = shell("openclaw --version 2>&1 || true", 1e4).trim();
|
|
1622
|
+
if (!out.includes(openclawTag)) throw new Error(`openclaw version verify failed: got "${out}"`);
|
|
1623
|
+
log(`openclaw version verified: ${out}`);
|
|
1315
1624
|
}
|
|
1316
1625
|
/** Step 6: Merge coreBackup from resetData + ensure allowedOrigins. */
|
|
1317
1626
|
function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
|
|
@@ -1405,73 +1714,31 @@ function mergeCoreBackupAndOrigins(configPath, vars, resetData, log) {
|
|
|
1405
1714
|
node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
1406
1715
|
log(`allowedOrigins: added ${added.length} (${JSON.stringify(added)}), total now ${mergedOrigins.length}`);
|
|
1407
1716
|
}
|
|
1408
|
-
/**
|
|
1409
|
-
|
|
1410
|
-
|
|
1717
|
+
/**
|
|
1718
|
+
* Step 7: Verify startup scripts landed in configDir/scripts/.
|
|
1719
|
+
*
|
|
1720
|
+
* Scripts are extracted directly to configDir/scripts/ during stageTemplate —
|
|
1721
|
+
* there's no intermediate copy any more. This step is now a verification gate
|
|
1722
|
+
* (rather than a copy action) so the step count stays at 9 and we fail early
|
|
1723
|
+
* if the template tgz didn't carry a scripts/ dir.
|
|
1724
|
+
*/
|
|
1725
|
+
function verifyStartupScripts(configDir, log) {
|
|
1411
1726
|
const targetScriptsDir = node_path.default.join(configDir, "scripts");
|
|
1412
|
-
if (!node_fs.default.existsSync(
|
|
1413
|
-
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
if (!node_fs.default.existsSync(targetScriptsDir)) node_fs.default.mkdirSync(targetScriptsDir, { recursive: true });
|
|
1417
|
-
shell(`cp -r '${srcScriptsDir}'/* '${targetScriptsDir}/'`, 1e4);
|
|
1418
|
-
log(`copied scripts/* -> ${targetScriptsDir}`);
|
|
1727
|
+
if (!node_fs.default.existsSync(targetScriptsDir)) throw new Error(`scripts dir missing at ${targetScriptsDir} — template download failed?`);
|
|
1728
|
+
log(`scripts dir present at ${targetScriptsDir}`);
|
|
1419
1729
|
}
|
|
1420
1730
|
/**
|
|
1421
|
-
* Step 8: Install extensions
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1424
|
-
* 2. Download tar.gz → extract to a staging dir
|
|
1425
|
-
* 3. For each included extension, backup (mv) the user's existing copy
|
|
1426
|
-
* to a temp dir — extensions NOT in the archive are left untouched
|
|
1427
|
-
* 4. Move the fresh extensions from staging to the target dir
|
|
1428
|
-
*
|
|
1429
|
-
* This bypasses npm entirely and avoids the schema-priority mismatch where
|
|
1430
|
-
* `openclaw doctor --fix` would validate config against the bundled feishu
|
|
1431
|
-
* plugin's strict schema before openclaw-lark is installed.
|
|
1432
|
-
*
|
|
1433
|
-
* Falls back to `openclaw plugins update --all` if the OSS download fails.
|
|
1731
|
+
* Step 8: Install all extensions listed in the OSS manifest at `openclawTag`.
|
|
1732
|
+
* Replaces the old `plugins update --all` / pre-packed tar.gz flow — the
|
|
1733
|
+
* manifest is now the single source of truth for which extensions ship.
|
|
1434
1734
|
*/
|
|
1435
|
-
function
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
log(`downloading extensions from ${ossUrl}`);
|
|
1443
|
-
const dlStart = Date.now();
|
|
1444
|
-
shell(`curl -fsSL '${ossUrl}' -o '${tarPath}'`, 5 * 6e4);
|
|
1445
|
-
const size = node_fs.default.statSync(tarPath).size;
|
|
1446
|
-
log(`download done in ${Date.now() - dlStart}ms (${(size / 1024 / 1024).toFixed(1)}MB)`);
|
|
1447
|
-
shell(`tar -xzf '${tarPath}' -C '${tmpDir}'`, 6e4);
|
|
1448
|
-
const stagedExtDir = node_path.default.join(tmpDir, "extensions");
|
|
1449
|
-
if (!node_fs.default.existsSync(stagedExtDir)) throw new Error("tar.gz does not contain an extensions/ directory");
|
|
1450
|
-
const extNames = node_fs.default.readdirSync(stagedExtDir).filter((name) => node_fs.default.statSync(node_path.default.join(stagedExtDir, name)).isDirectory());
|
|
1451
|
-
log(`archive contains ${extNames.length} extension(s): ${extNames.join(", ")}`);
|
|
1452
|
-
for (const name of extNames) {
|
|
1453
|
-
const target = node_path.default.join(targetExtDir, name);
|
|
1454
|
-
if (node_fs.default.existsSync(target)) node_fs.default.rmSync(target, {
|
|
1455
|
-
recursive: true,
|
|
1456
|
-
force: true
|
|
1457
|
-
});
|
|
1458
|
-
node_fs.default.renameSync(node_path.default.join(stagedExtDir, name), target);
|
|
1459
|
-
log(` ${name}: installed`);
|
|
1460
|
-
}
|
|
1461
|
-
const packinfo = node_path.default.join(stagedExtDir, ".packinfo.json");
|
|
1462
|
-
if (node_fs.default.existsSync(packinfo)) {
|
|
1463
|
-
node_fs.default.copyFileSync(packinfo, node_path.default.join(targetExtDir, ".packinfo.json"));
|
|
1464
|
-
log(`packinfo: ${node_fs.default.readFileSync(packinfo, "utf-8").trim()}`);
|
|
1465
|
-
}
|
|
1466
|
-
log("extensions installed from OSS successfully");
|
|
1467
|
-
} finally {
|
|
1468
|
-
try {
|
|
1469
|
-
node_fs.default.rmSync(tmpDir, {
|
|
1470
|
-
recursive: true,
|
|
1471
|
-
force: true
|
|
1472
|
-
});
|
|
1473
|
-
} catch {}
|
|
1474
|
-
}
|
|
1735
|
+
async function step8InstallExtensions(openclawTag, ossFileMap, log) {
|
|
1736
|
+
log(`install-extension --all tag=${openclawTag}`);
|
|
1737
|
+
await installExtension(openclawTag, ossFileMap, {
|
|
1738
|
+
all: true,
|
|
1739
|
+
skipConfigUpdate: true
|
|
1740
|
+
});
|
|
1741
|
+
log("extensions installed");
|
|
1475
1742
|
}
|
|
1476
1743
|
/** Step 9: Write secrets/provider key files and restart openclaw. */
|
|
1477
1744
|
function writeSecretsAndRestart(vars, resetData, configDir, log) {
|
|
@@ -1496,26 +1763,37 @@ function writeSecretsAndRestart(vars, resetData, configDir, log) {
|
|
|
1496
1763
|
* Each step is an independent function. The orchestrator handles progress
|
|
1497
1764
|
* reporting, error handling, and process-level exception guards.
|
|
1498
1765
|
*
|
|
1499
|
-
*
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1766
|
+
* Template assets (openclaw.json + scripts/) are downloaded from OSS into a
|
|
1767
|
+
* scratch dir via `stageTemplate` before step 1 — there is no bundled
|
|
1768
|
+
* `template/` directory at runtime any more.
|
|
1502
1769
|
*/
|
|
1503
|
-
function runReset(input, taskId, resultFile) {
|
|
1770
|
+
async function runReset(input, taskId, resultFile) {
|
|
1504
1771
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1505
1772
|
const { configPath, vars, resetData } = input;
|
|
1506
1773
|
const configDir = node_path.default.dirname(configPath);
|
|
1507
|
-
const
|
|
1774
|
+
const stagedDir = node_path.default.join(DIAGNOSE_DIR, `reset-${taskId}-template`);
|
|
1508
1775
|
let currentStep = 0;
|
|
1509
1776
|
let stepStartedAt = Date.now();
|
|
1510
1777
|
const log = makeLogger(resetLogFile(taskId));
|
|
1511
1778
|
log(`=== reset started, taskId=${taskId}, pid=${process.pid} ===`);
|
|
1512
|
-
log(`configPath=${configPath}, configDir=${configDir},
|
|
1513
|
-
|
|
1514
|
-
|
|
1779
|
+
log(`configPath=${configPath}, configDir=${configDir}, stagedDir=${stagedDir}`);
|
|
1780
|
+
const ossFileMap = resetData.ossFileMap;
|
|
1781
|
+
if (!ossFileMap || Object.keys(ossFileMap).length === 0) {
|
|
1782
|
+
const err = "resetData.ossFileMap missing or empty";
|
|
1783
|
+
log(`ERROR: ${err}`);
|
|
1784
|
+
markFailed(resultFile, 0, err, startedAt);
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
let openclawTag;
|
|
1788
|
+
try {
|
|
1789
|
+
openclawTag = getOpenclawTagFromOssFileMap(ossFileMap);
|
|
1790
|
+
} catch (e) {
|
|
1791
|
+
const err = e.message;
|
|
1515
1792
|
log(`ERROR: ${err}`);
|
|
1516
1793
|
markFailed(resultFile, 0, err, startedAt);
|
|
1517
1794
|
process.exit(1);
|
|
1518
1795
|
}
|
|
1796
|
+
log(`openclawTag=${openclawTag}`);
|
|
1519
1797
|
process.on("uncaughtException", (err) => {
|
|
1520
1798
|
log(`FATAL uncaughtException: ${err.message}\n${err.stack ?? ""}`);
|
|
1521
1799
|
markFailed(resultFile, currentStep, `uncaught exception: ${err.message}`, startedAt);
|
|
@@ -1535,22 +1813,23 @@ function runReset(input, taskId, resultFile) {
|
|
|
1535
1813
|
updateProgress(resultFile, n, startedAt);
|
|
1536
1814
|
};
|
|
1537
1815
|
try {
|
|
1816
|
+
await stageTemplate(openclawTag, ossFileMap, stagedDir, configDir, log);
|
|
1538
1817
|
step(1);
|
|
1539
1818
|
backupCurrentConfig(configPath, log);
|
|
1540
1819
|
step(2);
|
|
1541
|
-
generateDefaultConfig(
|
|
1820
|
+
generateDefaultConfig(stagedDir, configPath, resetData.templateVars, log);
|
|
1542
1821
|
step(3);
|
|
1543
1822
|
killOpenclawProcesses(log);
|
|
1544
1823
|
step(4);
|
|
1545
1824
|
waitForInitNpm(10 * 6e4, log);
|
|
1546
1825
|
step(5);
|
|
1547
|
-
|
|
1826
|
+
await step5InstallOpenclaw(openclawTag, ossFileMap, log);
|
|
1548
1827
|
step(6);
|
|
1549
1828
|
mergeCoreBackupAndOrigins(configPath, vars, resetData, log);
|
|
1550
1829
|
step(7);
|
|
1551
|
-
|
|
1830
|
+
verifyStartupScripts(configDir, log);
|
|
1552
1831
|
step(8);
|
|
1553
|
-
|
|
1832
|
+
await step8InstallExtensions(openclawTag, ossFileMap, log);
|
|
1554
1833
|
step(9);
|
|
1555
1834
|
writeSecretsAndRestart(vars, resetData, configDir, log);
|
|
1556
1835
|
log(`step 9 "${STEPS[8]}" done in ${Date.now() - stepStartedAt}ms`);
|
|
@@ -1561,6 +1840,13 @@ function runReset(input, taskId, resultFile) {
|
|
|
1561
1840
|
log(`ERROR in step ${currentStep} "${STEPS[currentStep - 1] ?? "init"}" after ${Date.now() - stepStartedAt}ms: ${err}\n${e.stack ?? ""}`);
|
|
1562
1841
|
markFailed(resultFile, currentStep, err, startedAt);
|
|
1563
1842
|
process.exit(1);
|
|
1843
|
+
} finally {
|
|
1844
|
+
try {
|
|
1845
|
+
node_fs.default.rmSync(stagedDir, {
|
|
1846
|
+
recursive: true,
|
|
1847
|
+
force: true
|
|
1848
|
+
});
|
|
1849
|
+
} catch {}
|
|
1564
1850
|
}
|
|
1565
1851
|
}
|
|
1566
1852
|
//#endregion
|
|
@@ -1602,55 +1888,485 @@ function sleepSync(ms) {
|
|
|
1602
1888
|
Atomics.wait(arr, 0, 0, ms);
|
|
1603
1889
|
}
|
|
1604
1890
|
//#endregion
|
|
1891
|
+
//#region src/oss/resolveOssFileMap.ts
|
|
1892
|
+
/**
|
|
1893
|
+
* Pick an OssFileMap in the order of decreasing specificity:
|
|
1894
|
+
* 1. `--oss_file_map=` flag — operator override (manual invocations, tests)
|
|
1895
|
+
* 2. `ctx.install.ossFileMap` — new shape (innerapi-driven DoctorCtx)
|
|
1896
|
+
* 3. `ctx.resetData.ossFileMap` — legacy shape (sandbox_console push path)
|
|
1897
|
+
*
|
|
1898
|
+
* Throws when none of the three yields a non-empty map. Empty maps are
|
|
1899
|
+
* treated as missing — an empty map is useless downstream and almost always
|
|
1900
|
+
* indicates a ctx wiring bug.
|
|
1901
|
+
*/
|
|
1902
|
+
function resolveOssFileMap(args) {
|
|
1903
|
+
if (args.ossFileMapFlag) return JSON.parse(Buffer.from(args.ossFileMapFlag, "base64").toString("utf-8"));
|
|
1904
|
+
if (args.installOssFileMap && Object.keys(args.installOssFileMap).length > 0) return args.installOssFileMap;
|
|
1905
|
+
if (args.resetDataOssFileMap && Object.keys(args.resetDataOssFileMap).length > 0) return args.resetDataOssFileMap;
|
|
1906
|
+
throw new Error("ossFileMap missing: provide --oss_file_map flag, ctx.install.ossFileMap, or resetData.ossFileMap");
|
|
1907
|
+
}
|
|
1908
|
+
//#endregion
|
|
1909
|
+
//#region src/innerapi/fetchCtx.ts
|
|
1910
|
+
/**
|
|
1911
|
+
* CLI-side client for studio_server's `openclaw.get_doctor_ctx` inner API.
|
|
1912
|
+
*
|
|
1913
|
+
* Mirrors the proven pattern in
|
|
1914
|
+
* `packages/openclaw/extensions/miaoda/src/shared/innerapi-client.ts`:
|
|
1915
|
+
*
|
|
1916
|
+
* - `baseURL` from env `FORCE_AUTHN_INNERAPI_DOMAIN` (injected into every
|
|
1917
|
+
* openclaw sandbox).
|
|
1918
|
+
* - `platform: { enabled, tokenProvider: { type: 'file' } }` — the platform
|
|
1919
|
+
* plugin auto-attaches the sandbox's identity JWT loaded from the
|
|
1920
|
+
* rootfs token file. Same auth that the miaoda extension already uses.
|
|
1921
|
+
* - POST `/api/v1/studio/innerapi/integration_apis/call`
|
|
1922
|
+
* body = { apiName: 'openclaw.get_doctor_ctx', input: {}, bizType: 'openclaw' }
|
|
1923
|
+
* — the server-side APICall dispatches by `apiName` to
|
|
1924
|
+
* `GetDoctorCtxAPICall.Execute` whose `Name()` returns that string.
|
|
1925
|
+
* - Response envelope: { status_code, error_msg?, data: { success, output, ... } }.
|
|
1926
|
+
* `status_code` is a *string* ('0' = success).
|
|
1927
|
+
* Actual DoctorCtx lives in `data.output`.
|
|
1928
|
+
* - `x-tt-logid` header is logged on every failure path for cross-service
|
|
1929
|
+
* traceability.
|
|
1930
|
+
*
|
|
1931
|
+
* On HTTP 401 (sandbox identity token expired/invalid) we `process.exit(77)`
|
|
1932
|
+
* instead of throwing — the outer catch in `index.ts` cannot then mask auth
|
|
1933
|
+
* failure as a generic "Error: ...". Caller (e.g. sandbox_console) sees the
|
|
1934
|
+
* exit code and can refresh the token + retry.
|
|
1935
|
+
*/
|
|
1936
|
+
const INNERAPI_CALL_PATH = "/api/v1/studio/innerapi/integration_apis/call";
|
|
1937
|
+
const API_NAME = "openclaw.get_doctor_ctx";
|
|
1938
|
+
const BIZ_TYPE = "openclaw";
|
|
1939
|
+
const API_TIMEOUT_MS = 3e4;
|
|
1940
|
+
const MAX_LOG_BODY = 500;
|
|
1941
|
+
let clientInstance = null;
|
|
1942
|
+
function getHttpClient() {
|
|
1943
|
+
if (!clientInstance) {
|
|
1944
|
+
const apiUrl = process.env.FORCE_AUTHN_INNERAPI_DOMAIN;
|
|
1945
|
+
(0, node_assert.default)(apiUrl, "missing env: FORCE_AUTHN_INNERAPI_DOMAIN (openclaw sandbox runtime must expose this)");
|
|
1946
|
+
clientInstance = new _lark_apaas_http_client.HttpClient({
|
|
1947
|
+
baseURL: apiUrl,
|
|
1948
|
+
timeout: API_TIMEOUT_MS,
|
|
1949
|
+
platform: {
|
|
1950
|
+
enabled: true,
|
|
1951
|
+
tokenProvider: { type: "file" }
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
return clientInstance;
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* Fetch the sandbox's DoctorCtx by calling the innerapi's generic
|
|
1959
|
+
* `integration_apis/call` dispatcher with apiName=openclaw.get_doctor_ctx.
|
|
1960
|
+
*
|
|
1961
|
+
* Throws on HTTP (non-401) / decode / business errors. On 401 calls
|
|
1962
|
+
* `process.exit(77)` directly.
|
|
1963
|
+
*/
|
|
1964
|
+
async function fetchCtxViaInnerApi() {
|
|
1965
|
+
const client = getHttpClient();
|
|
1966
|
+
const body = {
|
|
1967
|
+
apiName: API_NAME,
|
|
1968
|
+
input: {},
|
|
1969
|
+
bizType: BIZ_TYPE
|
|
1970
|
+
};
|
|
1971
|
+
const start = Date.now();
|
|
1972
|
+
const headers = { "Content-Type": "application/json" };
|
|
1973
|
+
const ttEnv = process.env.X_TT_ENV;
|
|
1974
|
+
if (ttEnv) headers["x-tt-env"] = ttEnv;
|
|
1975
|
+
let response;
|
|
1976
|
+
try {
|
|
1977
|
+
response = await client.post(INNERAPI_CALL_PATH, body, { headers });
|
|
1978
|
+
} catch (e) {
|
|
1979
|
+
const durationMs = Date.now() - start;
|
|
1980
|
+
if (e instanceof _lark_apaas_http_client.HttpError && e.response) {
|
|
1981
|
+
const status = e.response.status;
|
|
1982
|
+
const logId = e.response.headers.get("x-tt-logid") ?? "";
|
|
1983
|
+
if (status === 401) {
|
|
1984
|
+
console.error(`[CLI] innerapi 401 (logID: ${logId}) — sandbox identity token expired/invalid; exiting 77`);
|
|
1985
|
+
process.exit(77);
|
|
1986
|
+
}
|
|
1987
|
+
throw new Error(`fetchCtxViaInnerApi HTTP ${status} ${e.response.statusText} (logID: ${logId}, durationMs: ${durationMs})`);
|
|
1988
|
+
}
|
|
1989
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1990
|
+
throw new Error(`fetchCtxViaInnerApi network error: ${msg} (durationMs: ${durationMs})`);
|
|
1991
|
+
}
|
|
1992
|
+
const logId = response.headers.get("x-tt-logid") ?? "";
|
|
1993
|
+
const durationMs = Date.now() - start;
|
|
1994
|
+
if (!response.ok) {
|
|
1995
|
+
if (response.status === 401) {
|
|
1996
|
+
console.error(`[CLI] innerapi 401 (logID: ${logId}) — sandbox identity token expired/invalid; exiting 77`);
|
|
1997
|
+
process.exit(77);
|
|
1998
|
+
}
|
|
1999
|
+
let preview = "";
|
|
2000
|
+
try {
|
|
2001
|
+
preview = (await response.text()).slice(0, MAX_LOG_BODY);
|
|
2002
|
+
} catch {}
|
|
2003
|
+
throw new Error(`fetchCtxViaInnerApi HTTP ${response.status} ${response.statusText} (logID: ${logId}, durationMs: ${durationMs})${preview ? ` body=${preview}` : ""}`);
|
|
2004
|
+
}
|
|
2005
|
+
let envelope;
|
|
2006
|
+
try {
|
|
2007
|
+
envelope = await response.json();
|
|
2008
|
+
} catch {
|
|
2009
|
+
throw new Error(`fetchCtxViaInnerApi decode error (logID: ${logId}, durationMs: ${durationMs})`);
|
|
2010
|
+
}
|
|
2011
|
+
if (envelope.status_code !== "0") throw new Error(`fetchCtxViaInnerApi API error (logID: ${logId}, durationMs: ${durationMs}): code=${envelope.status_code}, message=${envelope.error_msg ?? ""}`);
|
|
2012
|
+
if (envelope.data && envelope.data.success === false) throw new Error(`fetchCtxViaInnerApi business error (logID: ${logId}, durationMs: ${durationMs}): ${envelope.error_msg ?? JSON.stringify(envelope.data)}`);
|
|
2013
|
+
const output = envelope.data?.output;
|
|
2014
|
+
if (!output || typeof output !== "object") throw new Error(`fetchCtxViaInnerApi empty/invalid output (logID: ${logId}, durationMs: ${durationMs})`);
|
|
2015
|
+
return output;
|
|
2016
|
+
}
|
|
2017
|
+
//#endregion
|
|
2018
|
+
//#region src/ctx/normalize.ts
|
|
2019
|
+
/**
|
|
2020
|
+
* Accept raw ctx from any of these sources and produce a uniform view:
|
|
2021
|
+
* - New shape (DoctorCtx): `{ app, install, secrets, reset }` — from innerapi.
|
|
2022
|
+
* - Old shape (ResetInput): `{ configPath, vars, resetData }` — from
|
|
2023
|
+
* sandbox_console push path.
|
|
2024
|
+
* Detection is structural: if the top-level has all four new-shape groups we
|
|
2025
|
+
* pass through; otherwise we remap from the old shape.
|
|
2026
|
+
*
|
|
2027
|
+
* Missing fields fall back to safe empty defaults (empty strings / arrays /
|
|
2028
|
+
* maps) so every downstream consumer can read e.g. `ctx.app.feishuAppID`
|
|
2029
|
+
* without an extra nullish guard.
|
|
2030
|
+
*/
|
|
2031
|
+
function normalizeCtx(raw) {
|
|
2032
|
+
const r = raw ?? {};
|
|
2033
|
+
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") return {
|
|
2034
|
+
app: fillApp(r.app),
|
|
2035
|
+
install: {
|
|
2036
|
+
openclawTag: r.install.openclawTag,
|
|
2037
|
+
ossFileMap: r.install.ossFileMap ?? {}
|
|
2038
|
+
},
|
|
2039
|
+
secrets: {
|
|
2040
|
+
secretsContent: r.secrets.secretsContent ?? "",
|
|
2041
|
+
providerKeyContent: r.secrets.providerKeyContent ?? ""
|
|
2042
|
+
},
|
|
2043
|
+
reset: {
|
|
2044
|
+
templateVars: r.reset.templateVars ?? {},
|
|
2045
|
+
coreBackup: r.reset.coreBackup
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
const vars = r.vars ?? {};
|
|
2049
|
+
const resetData = r.resetData ?? {};
|
|
2050
|
+
const repairData = r.repairData ?? {};
|
|
2051
|
+
return {
|
|
2052
|
+
app: fillApp(vars),
|
|
2053
|
+
install: {
|
|
2054
|
+
openclawTag: r.install?.openclawTag ?? r.openclawTag,
|
|
2055
|
+
ossFileMap: r.install?.ossFileMap ?? resetData.ossFileMap ?? r.ossFileMap ?? {}
|
|
2056
|
+
},
|
|
2057
|
+
secrets: {
|
|
2058
|
+
secretsContent: resetData.secretsContent ?? repairData.secretsContent ?? "",
|
|
2059
|
+
providerKeyContent: resetData.providerKeyContent ?? repairData.providerKeyContent ?? ""
|
|
2060
|
+
},
|
|
2061
|
+
reset: {
|
|
2062
|
+
templateVars: resetData.templateVars ?? {},
|
|
2063
|
+
coreBackup: resetData.coreBackup
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
function fillApp(src) {
|
|
2068
|
+
return {
|
|
2069
|
+
feishuAppID: src.feishuAppID ?? "",
|
|
2070
|
+
feishuAppSecret: src.feishuAppSecret ?? "",
|
|
2071
|
+
feishuOpenID: src.feishuOpenID ?? "",
|
|
2072
|
+
openClawName: src.openClawName ?? "",
|
|
2073
|
+
gatewayToken: src.gatewayToken ?? "",
|
|
2074
|
+
innerAPIKey: src.innerAPIKey ?? "",
|
|
2075
|
+
baseURL: src.baseURL ?? "",
|
|
2076
|
+
miaodaDomain: src.miaodaDomain ?? "",
|
|
2077
|
+
miaodaOrigin: src.miaodaOrigin ?? "",
|
|
2078
|
+
expectedOrigins: Array.isArray(src.expectedOrigins) ? src.expectedOrigins : []
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
//#endregion
|
|
2082
|
+
//#region src/ctx-input.ts
|
|
2083
|
+
/**
|
|
2084
|
+
* Build legacy Check/Repair/Reset input shapes from a raw ctx object. Shared
|
|
2085
|
+
* by both the top-level CLI dispatcher (`index.ts`) and the new `doctor`
|
|
2086
|
+
* subcommand (`doctor.ts`), which need identical input synthesis.
|
|
2087
|
+
*
|
|
2088
|
+
* Behavior:
|
|
2089
|
+
* - If `raw` already carries the legacy `configPath + vars` shape (the one
|
|
2090
|
+
* sandbox_console push emits), it's trusted and returned as-is. This
|
|
2091
|
+
* keeps the existing sandbox_console push contract working.
|
|
2092
|
+
* - Otherwise `raw` is treated as the new-shape DoctorCtx (or anything
|
|
2093
|
+
* structurally close — `normalizeCtx` fills the gaps with safe empties)
|
|
2094
|
+
* and the legacy Vars shape is synthesised using the hardcoded sandbox
|
|
2095
|
+
* path invariants from `paths.ts`.
|
|
2096
|
+
*
|
|
2097
|
+
* The optional `configPathOverride` lets unit tests point the builder at a
|
|
2098
|
+
* tmp file; production callers should leave it undefined so the sandbox
|
|
2099
|
+
* invariant from `paths.ts` is used.
|
|
2100
|
+
*/
|
|
2101
|
+
function buildCheckInput(raw, configPathOverride) {
|
|
2102
|
+
const r = raw ?? {};
|
|
2103
|
+
if (r.configPath && r.vars) {
|
|
2104
|
+
if (configPathOverride) return {
|
|
2105
|
+
...r,
|
|
2106
|
+
configPath: configPathOverride
|
|
2107
|
+
};
|
|
2108
|
+
return r;
|
|
2109
|
+
}
|
|
2110
|
+
const ctx = normalizeCtx(raw);
|
|
2111
|
+
return {
|
|
2112
|
+
configPath: configPathOverride ?? CONFIG_PATH,
|
|
2113
|
+
vars: {
|
|
2114
|
+
feishuAppID: ctx.app.feishuAppID,
|
|
2115
|
+
feishuAppSecret: ctx.app.feishuAppSecret,
|
|
2116
|
+
innerAPIKey: ctx.app.innerAPIKey,
|
|
2117
|
+
gatewayToken: ctx.app.gatewayToken,
|
|
2118
|
+
baseURL: ctx.app.baseURL,
|
|
2119
|
+
expectedOrigins: ctx.app.expectedOrigins,
|
|
2120
|
+
providerFilePath: PROVIDER_FILE_PATH,
|
|
2121
|
+
secretsFilePath: SECRETS_FILE_PATH
|
|
2122
|
+
},
|
|
2123
|
+
templateVars: ctx.reset.templateVars
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
function buildRepairInput(raw, configPathOverride) {
|
|
2127
|
+
const r = raw ?? {};
|
|
2128
|
+
if (r.configPath && r.vars) {
|
|
2129
|
+
if (configPathOverride) return {
|
|
2130
|
+
...r,
|
|
2131
|
+
configPath: configPathOverride
|
|
2132
|
+
};
|
|
2133
|
+
return r;
|
|
2134
|
+
}
|
|
2135
|
+
const ctx = normalizeCtx(raw);
|
|
2136
|
+
return {
|
|
2137
|
+
configPath: configPathOverride ?? CONFIG_PATH,
|
|
2138
|
+
vars: {
|
|
2139
|
+
feishuAppID: ctx.app.feishuAppID,
|
|
2140
|
+
feishuAppSecret: ctx.app.feishuAppSecret,
|
|
2141
|
+
innerAPIKey: ctx.app.innerAPIKey,
|
|
2142
|
+
gatewayToken: ctx.app.gatewayToken,
|
|
2143
|
+
baseURL: ctx.app.baseURL,
|
|
2144
|
+
expectedOrigins: ctx.app.expectedOrigins,
|
|
2145
|
+
providerFilePath: PROVIDER_FILE_PATH,
|
|
2146
|
+
secretsFilePath: SECRETS_FILE_PATH
|
|
2147
|
+
},
|
|
2148
|
+
repairData: {
|
|
2149
|
+
secretsContent: ctx.secrets.secretsContent,
|
|
2150
|
+
providerKeyContent: ctx.secrets.providerKeyContent
|
|
2151
|
+
},
|
|
2152
|
+
templateVars: ctx.reset.templateVars
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
function buildResetInput(raw, configPathOverride) {
|
|
2156
|
+
const r = raw ?? {};
|
|
2157
|
+
if (r.configPath && r.vars && r.resetData) {
|
|
2158
|
+
if (configPathOverride) return {
|
|
2159
|
+
...r,
|
|
2160
|
+
configPath: configPathOverride
|
|
2161
|
+
};
|
|
2162
|
+
return r;
|
|
2163
|
+
}
|
|
2164
|
+
const ctx = normalizeCtx(raw);
|
|
2165
|
+
return {
|
|
2166
|
+
configPath: configPathOverride ?? CONFIG_PATH,
|
|
2167
|
+
vars: {
|
|
2168
|
+
feishuAppID: ctx.app.feishuAppID,
|
|
2169
|
+
feishuAppSecret: ctx.app.feishuAppSecret,
|
|
2170
|
+
innerAPIKey: ctx.app.innerAPIKey,
|
|
2171
|
+
gatewayToken: ctx.app.gatewayToken,
|
|
2172
|
+
baseURL: ctx.app.baseURL,
|
|
2173
|
+
expectedOrigins: ctx.app.expectedOrigins,
|
|
2174
|
+
providerFilePath: PROVIDER_FILE_PATH,
|
|
2175
|
+
secretsFilePath: SECRETS_FILE_PATH
|
|
2176
|
+
},
|
|
2177
|
+
resetData: {
|
|
2178
|
+
templateVars: ctx.reset.templateVars,
|
|
2179
|
+
secretsContent: ctx.secrets.secretsContent,
|
|
2180
|
+
providerKeyContent: ctx.secrets.providerKeyContent,
|
|
2181
|
+
coreBackup: ctx.reset.coreBackup,
|
|
2182
|
+
ossFileMap: ctx.install.ossFileMap
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
//#endregion
|
|
2187
|
+
//#region src/doctor.ts
|
|
2188
|
+
async function runDoctor(rawCtx, opts) {
|
|
2189
|
+
if (opts.fix && opts.rules.length > 0) {
|
|
2190
|
+
const repairInput = buildRepairInput(rawCtx, opts.configPath);
|
|
2191
|
+
repairInput.failedRules = opts.rules;
|
|
2192
|
+
repairInput.repairData = {
|
|
2193
|
+
...repairInput.repairData ?? {},
|
|
2194
|
+
restartCommand: ""
|
|
2195
|
+
};
|
|
2196
|
+
return { repair: runRepair(repairInput) };
|
|
2197
|
+
}
|
|
2198
|
+
const check = runCheck(buildCheckInput(rawCtx, opts.configPath));
|
|
2199
|
+
if (!opts.fix) return { failedRules: check.failedRules };
|
|
2200
|
+
const repairInput = buildRepairInput(rawCtx, opts.configPath);
|
|
2201
|
+
repairInput.failedRules = check.failedRules.standard;
|
|
2202
|
+
return {
|
|
2203
|
+
check,
|
|
2204
|
+
repair: runRepair(repairInput)
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
//#endregion
|
|
1605
2208
|
//#region src/index.ts
|
|
1606
2209
|
const args = node_process.default.argv.slice(2);
|
|
1607
2210
|
const mode = args.find((a) => !a.startsWith("--"));
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
2211
|
+
/**
|
|
2212
|
+
* Decode `--ctx=<base64>` into an opaque JSON object. Returns undefined when
|
|
2213
|
+
* the flag isn't present — the caller decides whether to fall back to the
|
|
2214
|
+
* innerapi or to error out.
|
|
2215
|
+
*
|
|
2216
|
+
* The object's shape is not enforced here; downstream code consumes it via
|
|
2217
|
+
* either `normalizeCtx()` (new path) or direct field access for the legacy
|
|
2218
|
+
* check/repair/reset contract still used by sandbox_console push.
|
|
2219
|
+
*/
|
|
2220
|
+
function parseCtxFlag(args) {
|
|
2221
|
+
const ctxArg = args.find((a) => a.startsWith("--ctx="));
|
|
2222
|
+
if (!ctxArg) return void 0;
|
|
2223
|
+
const b64 = ctxArg.slice(6);
|
|
2224
|
+
return JSON.parse(Buffer.from(b64, "base64").toString("utf-8"));
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Pull the first non-flag positional after the mode name.
|
|
2228
|
+
* (The mode itself is args[0] in the filtered set, so we skip index 0.)
|
|
2229
|
+
*/
|
|
2230
|
+
function getPositionalTag(args, modeName) {
|
|
2231
|
+
return args.find((a, i) => i > 0 && !a.startsWith("--") && a !== modeName);
|
|
2232
|
+
}
|
|
2233
|
+
function getFlag(args, name) {
|
|
2234
|
+
const prefix = `--${name}=`;
|
|
2235
|
+
return args.find((a) => a.startsWith(prefix))?.slice(prefix.length);
|
|
2236
|
+
}
|
|
2237
|
+
function getMultiFlag(args, name) {
|
|
2238
|
+
const prefix = `--${name}=`;
|
|
2239
|
+
return args.filter((a) => a.startsWith(prefix)).map((a) => a.slice(prefix.length));
|
|
2240
|
+
}
|
|
2241
|
+
async function main() {
|
|
2242
|
+
switch (mode) {
|
|
2243
|
+
case "check":
|
|
2244
|
+
case "repair": {
|
|
2245
|
+
const raw = parseCtxFlag(args) ?? await fetchCtxViaInnerApi();
|
|
2246
|
+
if (mode === "check") console.log(JSON.stringify(runCheck(buildCheckInput(raw))));
|
|
2247
|
+
else console.log(JSON.stringify(runRepair(buildRepairInput(raw))));
|
|
2248
|
+
break;
|
|
1615
2249
|
}
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
2250
|
+
case "doctor": {
|
|
2251
|
+
const fix = args.includes("--fix");
|
|
2252
|
+
const rules = getMultiFlag(args, "rule");
|
|
2253
|
+
const result = await runDoctor(await fetchCtxViaInnerApi(), {
|
|
2254
|
+
fix,
|
|
2255
|
+
rules
|
|
2256
|
+
});
|
|
2257
|
+
console.log(JSON.stringify(result));
|
|
2258
|
+
break;
|
|
2259
|
+
}
|
|
2260
|
+
case "reset":
|
|
2261
|
+
if (args.includes("--async")) {
|
|
2262
|
+
const ctxArg = args.find((a) => a.startsWith("--ctx="));
|
|
2263
|
+
let ctxBase64;
|
|
2264
|
+
if (ctxArg) ctxBase64 = ctxArg.slice(6);
|
|
2265
|
+
else {
|
|
2266
|
+
const fetched = await fetchCtxViaInnerApi();
|
|
2267
|
+
ctxBase64 = Buffer.from(JSON.stringify(fetched), "utf-8").toString("base64");
|
|
2268
|
+
}
|
|
2269
|
+
console.log(JSON.stringify(startAsyncReset(ctxBase64)));
|
|
2270
|
+
} else if (args.includes("--worker")) {
|
|
2271
|
+
const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
|
|
2272
|
+
if (!taskId) {
|
|
2273
|
+
console.error("Error: --task-id=<id> is required for worker");
|
|
2274
|
+
node_process.default.exit(1);
|
|
2275
|
+
}
|
|
2276
|
+
const resultFile = resetResultFile(taskId);
|
|
2277
|
+
await runReset(buildResetInput(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()), taskId, resultFile);
|
|
2278
|
+
} else {
|
|
2279
|
+
console.error("Usage: reset --async [--ctx=<base64>] | reset --worker --task-id=<id> [--ctx=<base64>]");
|
|
1626
2280
|
node_process.default.exit(1);
|
|
1627
2281
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
const ctx = args.find((a) => a.startsWith("--ctx="))?.slice(6);
|
|
2282
|
+
break;
|
|
2283
|
+
case "get_reset_task": {
|
|
1631
2284
|
const taskId = args.find((a) => a.startsWith("--task-id="))?.slice(10);
|
|
1632
|
-
if (!
|
|
1633
|
-
console.error("Error: --
|
|
2285
|
+
if (!taskId) {
|
|
2286
|
+
console.error("Error: --task-id=<id> is required");
|
|
1634
2287
|
node_process.default.exit(1);
|
|
1635
2288
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
} else {
|
|
1639
|
-
console.error("Usage: reset --async --ctx=<base64> | reset --worker --task-id=<id> --ctx=<base64>");
|
|
1640
|
-
node_process.default.exit(1);
|
|
2289
|
+
console.log(JSON.stringify(getResetTask(taskId)));
|
|
2290
|
+
break;
|
|
1641
2291
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
2292
|
+
case "install-openclaw": {
|
|
2293
|
+
const tag = getPositionalTag(args, "install-openclaw");
|
|
2294
|
+
if (!tag) {
|
|
2295
|
+
console.error("Usage: install-openclaw <tag> [--ctx=<base64> | --oss_file_map=<base64>]");
|
|
2296
|
+
node_process.default.exit(1);
|
|
2297
|
+
}
|
|
2298
|
+
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
2299
|
+
let installOssFileMap;
|
|
2300
|
+
if (!ossFileMapFlag) installOssFileMap = normalizeCtx(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()).install.ossFileMap;
|
|
2301
|
+
await installOpenclaw(tag, resolveOssFileMap({
|
|
2302
|
+
ossFileMapFlag,
|
|
2303
|
+
installOssFileMap
|
|
2304
|
+
}));
|
|
2305
|
+
console.log(JSON.stringify({ ok: true }));
|
|
2306
|
+
break;
|
|
2307
|
+
}
|
|
2308
|
+
case "install-extension": {
|
|
2309
|
+
const tag = getPositionalTag(args, "install-extension");
|
|
2310
|
+
if (!tag) {
|
|
2311
|
+
console.error("Usage: install-extension <tag> (--all | --extension=<name>...) [--home_base=<dir>] [--config_path=<path>] [--skip-config-update] [--ctx=<base64> | --oss_file_map=<base64>]");
|
|
2312
|
+
node_process.default.exit(1);
|
|
2313
|
+
}
|
|
2314
|
+
const all = args.includes("--all");
|
|
2315
|
+
const names = getMultiFlag(args, "extension");
|
|
2316
|
+
const homeBase = getFlag(args, "home_base");
|
|
2317
|
+
const configPath = getFlag(args, "config_path");
|
|
2318
|
+
const skipConfigUpdate = args.includes("--skip-config-update");
|
|
2319
|
+
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
2320
|
+
let installOssFileMap;
|
|
2321
|
+
if (!ossFileMapFlag) installOssFileMap = normalizeCtx(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()).install.ossFileMap;
|
|
2322
|
+
await installExtension(tag, resolveOssFileMap({
|
|
2323
|
+
ossFileMapFlag,
|
|
2324
|
+
installOssFileMap
|
|
2325
|
+
}), {
|
|
2326
|
+
all,
|
|
2327
|
+
names: names.length > 0 ? names : void 0,
|
|
2328
|
+
homeBase,
|
|
2329
|
+
configPath,
|
|
2330
|
+
skipConfigUpdate
|
|
2331
|
+
});
|
|
2332
|
+
console.log(JSON.stringify({ ok: true }));
|
|
2333
|
+
break;
|
|
1648
2334
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
2335
|
+
case "download-resource": {
|
|
2336
|
+
const tag = getPositionalTag(args, "download-resource");
|
|
2337
|
+
if (!tag) {
|
|
2338
|
+
console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>] [--ctx=<base64> | --oss_file_map=<base64>]");
|
|
2339
|
+
node_process.default.exit(1);
|
|
2340
|
+
}
|
|
2341
|
+
const role = getFlag(args, "role");
|
|
2342
|
+
const name = getFlag(args, "name");
|
|
2343
|
+
const dir = getFlag(args, "dir");
|
|
2344
|
+
if (!role || !name) {
|
|
2345
|
+
console.error("Usage: download-resource <tag> --role=<role> --name=<name> [--dir=<dir>]");
|
|
2346
|
+
node_process.default.exit(1);
|
|
2347
|
+
}
|
|
2348
|
+
const ossFileMapFlag = getFlag(args, "oss_file_map");
|
|
2349
|
+
let installOssFileMap;
|
|
2350
|
+
if (!ossFileMapFlag) installOssFileMap = normalizeCtx(parseCtxFlag(args) ?? await fetchCtxViaInnerApi()).install.ossFileMap;
|
|
2351
|
+
await downloadResource(tag, resolveOssFileMap({
|
|
2352
|
+
ossFileMapFlag,
|
|
2353
|
+
installOssFileMap
|
|
2354
|
+
}), {
|
|
2355
|
+
role,
|
|
2356
|
+
name,
|
|
2357
|
+
dir
|
|
2358
|
+
});
|
|
2359
|
+
console.log(JSON.stringify({ ok: true }));
|
|
2360
|
+
break;
|
|
2361
|
+
}
|
|
2362
|
+
default:
|
|
2363
|
+
console.error("Usage: mclaw-diagnose <check|repair|doctor|reset|get_reset_task|install-openclaw|install-extension|download-resource> [options]");
|
|
2364
|
+
node_process.default.exit(1);
|
|
1651
2365
|
}
|
|
1652
|
-
default:
|
|
1653
|
-
console.error("Usage: mclaw-diagnose <check|repair|reset|get_reset_task> [options]");
|
|
1654
|
-
node_process.default.exit(1);
|
|
1655
2366
|
}
|
|
2367
|
+
main().catch((err) => {
|
|
2368
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2369
|
+
console.error(`Error: ${msg}`);
|
|
2370
|
+
node_process.default.exit(1);
|
|
2371
|
+
});
|
|
1656
2372
|
//#endregion
|