jishushell 0.4.2 → 0.4.17
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/Dockerfile.openclaw-slim +58 -0
- package/INSTALL-NOTICE +45 -0
- package/dist/auth.js +3 -3
- package/dist/auth.js.map +1 -1
- package/dist/cli/app.d.ts +3 -0
- package/dist/cli/app.js +156 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/{doctor.d.ts → cli/doctor.d.ts} +6 -1
- package/dist/{doctor.js → cli/doctor.js} +389 -27
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/helpers.d.ts +4 -0
- package/dist/cli/helpers.js +32 -0
- package/dist/cli/helpers.js.map +1 -0
- package/dist/cli/job.d.ts +3 -0
- package/dist/cli/job.js +260 -0
- package/dist/cli/job.js.map +1 -0
- package/dist/cli/llm.d.ts +24 -0
- package/dist/cli/llm.js +593 -0
- package/dist/cli/llm.js.map +1 -0
- package/dist/cli/openclaw.d.ts +12 -0
- package/dist/cli/openclaw.js +156 -0
- package/dist/cli/openclaw.js.map +1 -0
- package/dist/cli/panel.d.ts +25 -0
- package/dist/cli/panel.js +734 -0
- package/dist/cli/panel.js.map +1 -0
- package/dist/cli.js +476 -219
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +22 -4
- package/dist/config.js +96 -55
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +13 -41
- package/dist/control.js +12 -1355
- package/dist/control.js.map +1 -1
- package/dist/install.d.ts +1 -1
- package/dist/install.js +15 -29
- package/dist/install.js.map +1 -1
- package/dist/routes/apps.d.ts +3 -0
- package/dist/routes/apps.js +99 -0
- package/dist/routes/apps.js.map +1 -0
- package/dist/routes/backup.d.ts +2 -0
- package/dist/routes/backup.js +370 -0
- package/dist/routes/backup.js.map +1 -0
- package/dist/routes/instances.d.ts +1 -0
- package/dist/routes/instances.js +61 -15
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/llm.d.ts +15 -0
- package/dist/routes/llm.js +246 -0
- package/dist/routes/llm.js.map +1 -0
- package/dist/routes/setup.js +32 -7
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +31 -6
- package/dist/routes/system.js.map +1 -1
- package/dist/server.js +69 -5
- package/dist/server.js.map +1 -1
- package/dist/services/app-compiler.d.ts +15 -0
- package/dist/services/app-compiler.js +169 -0
- package/dist/services/app-compiler.js.map +1 -0
- package/dist/services/app-manager.d.ts +17 -0
- package/dist/services/app-manager.js +168 -0
- package/dist/services/app-manager.js.map +1 -0
- package/dist/services/backup-manager.d.ts +253 -0
- package/dist/services/backup-manager.js +2014 -0
- package/dist/services/backup-manager.js.map +1 -0
- package/dist/services/backup-verify.d.ts +26 -0
- package/dist/services/backup-verify.js +240 -0
- package/dist/services/backup-verify.js.map +1 -0
- package/dist/services/instance-manager.d.ts +73 -5
- package/dist/services/instance-manager.js +446 -74
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/job-manager.d.ts +22 -0
- package/dist/services/job-manager.js +102 -0
- package/dist/services/job-manager.js.map +1 -0
- package/dist/services/llm-proxy/adapters.js +5 -1
- package/dist/services/llm-proxy/adapters.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +30 -0
- package/dist/services/llm-proxy/index.js +71 -1
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/ssrf.js +1 -1
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/nomad-manager.js +263 -159
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/panel-manager.d.ts +40 -0
- package/dist/services/panel-manager.js +346 -0
- package/dist/services/panel-manager.js.map +1 -0
- package/dist/services/process-manager.js +24 -10
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/setup-manager.d.ts +4 -2
- package/dist/services/setup-manager.js +578 -154
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/telemetry/activation.js +10 -7
- package/dist/services/telemetry/activation.js.map +1 -1
- package/dist/services/telemetry/client.js +7 -18
- package/dist/services/telemetry/client.js.map +1 -1
- package/dist/services/telemetry/heartbeat.js +12 -6
- package/dist/services/telemetry/heartbeat.js.map +1 -1
- package/dist/services/update-manager.d.ts +47 -0
- package/dist/services/update-manager.js +305 -0
- package/dist/services/update-manager.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/utils/fs.d.ts +85 -0
- package/dist/utils/fs.js +111 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/safe-json.d.ts +2 -0
- package/dist/utils/safe-json.js +22 -16
- package/dist/utils/safe-json.js.map +1 -1
- package/install/jishu-install.sh +582 -138
- package/install/jishu-uninstall.sh +276 -391
- package/install/post-install.sh +85 -3
- package/openclaw-entry.sh +15 -0
- package/package.json +12 -5
- package/public/assets/Dashboard-CQsp1Mr9.js +1 -0
- package/public/assets/InitPassword-BEC8SE4A.js +1 -0
- package/public/assets/InstanceDetail-B5wTgNEg.js +17 -0
- package/public/assets/{Login-RkjzTNWg.js → Login-D1Bt-Lyk.js} +1 -1
- package/public/assets/NewInstance-GQzm3K9D.js +1 -0
- package/public/assets/Settings-ByjGlqhP.js +1 -0
- package/public/assets/Setup-cMF21Y-8.js +1 -0
- package/public/assets/index-B6qQP4mH.css +1 -0
- package/public/assets/index-BuTQtuNy.js +16 -0
- package/public/assets/logo-black-theme-DywLAtFy.png +0 -0
- package/public/assets/logo-white-theme-DXffFAWw.png +0 -0
- package/public/assets/{providers-lBSOjUWy.js → providers-V-vwrExZ.js} +1 -1
- package/public/assets/{usePolling-CqQ8hrNc.js → usePolling-CK0DfI4h.js} +1 -1
- package/public/assets/{vendor-i18n-Bvxxh8Di.js → vendor-i18n-CfW0RvgE.js} +1 -1
- package/public/assets/vendor-react-B1-3Yrt-.js +59 -0
- package/public/index.html +4 -4
- package/dist/doctor.js.map +0 -1
- package/public/assets/Dashboard-CAOQDYDR.js +0 -1
- package/public/assets/InitPassword-CkehIkJG.js +0 -1
- package/public/assets/InstanceDetail-CzW2S95J.js +0 -14
- package/public/assets/NewInstance-DdbErdjA.js +0 -1
- package/public/assets/Settings-BUD7zwv9.js +0 -1
- package/public/assets/Setup-RRTIERGG.js +0 -1
- package/public/assets/index-77Ug7feY.css +0 -1
- package/public/assets/index-DfRnVUQR.js +0 -16
- package/public/assets/vendor-react-DONn7uBV.js +0 -59
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { execFileSync, execSync, spawn as nodeSpawn } from "child_process";
|
|
2
|
-
import { chmodSync, existsSync,
|
|
3
|
-
import { userInfo
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, mkdtempSync, readFileSync, renameSync, rmSync, symlinkSync, unlinkSync } from "fs";
|
|
3
|
+
import { userInfo } from "node:os";
|
|
4
4
|
import { randomBytes } from "node:crypto";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { dirname, join } from "path";
|
|
7
7
|
import { StringDecoder } from "string_decoder";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { JISHUSHELL_HOME, getPanelConfig, savePanelConfig, setOpenclawDockerImage, getOpenclawDockerImage,
|
|
9
|
+
import { JISHUSHELL_HOME, getPanelConfig, savePanelConfig, setOpenclawDockerImage, getOpenclawDockerImage, DEFAULT_OPENCLAW_DOCKER_IMAGE } from "../config.js";
|
|
10
|
+
import { ensureDirContainer, ensureDirHost, writeConfigFile, writeSecretFile, writeExecutableFile, writeSystemTmpFile } from "../utils/fs.js";
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = dirname(__filename);
|
|
12
13
|
// ── Paths ──────────────────────────────────────────────────────────
|
|
@@ -20,7 +21,10 @@ const NOMAD_BIN = join(BIN_DIR, "nomad");
|
|
|
20
21
|
const NOMAD_CONFIG_DIR = join(JISHUSHELL_HOME, "nomad");
|
|
21
22
|
const NOMAD_DATA_DIR = join(JISHUSHELL_HOME, "nomad", "data");
|
|
22
23
|
const NOMAD_ALLOC_DIR = join(JISHUSHELL_HOME, "nomad", "data", "alloc");
|
|
23
|
-
const
|
|
24
|
+
const COLIMA_DIR = join(JISHUSHELL_HOME, "colima");
|
|
25
|
+
const COLIMA_PROFILE = "jishushell";
|
|
26
|
+
const COLIMA_SOCKET = join(COLIMA_DIR, COLIMA_PROFILE, "docker.sock");
|
|
27
|
+
const NOMAD_VERSION = "1.6.5";
|
|
24
28
|
let _serverPort = 8090;
|
|
25
29
|
export function setServerPort(port) { _serverPort = port; }
|
|
26
30
|
// ── Resolve non-root service user (board-agnostic) ─────────────────
|
|
@@ -340,7 +344,7 @@ export function ensureCgroupMemory() {
|
|
|
340
344
|
execFileSync("sudo", ["cp", f, f + ".bak"], { timeout: 5000 });
|
|
341
345
|
// Write to tmp file then sudo cp to avoid shell interpolation of file content
|
|
342
346
|
const tmpPath = join(dirname(f), ".cmdline.tmp");
|
|
343
|
-
|
|
347
|
+
writeSystemTmpFile(tmpPath, patched + "\n");
|
|
344
348
|
execFileSync("sudo", ["cp", tmpPath, f], { timeout: 5000 });
|
|
345
349
|
try {
|
|
346
350
|
unlinkSync(tmpPath);
|
|
@@ -356,13 +360,16 @@ export function ensureCgroupMemory() {
|
|
|
356
360
|
return false;
|
|
357
361
|
}
|
|
358
362
|
function canAccessDockerDaemon(timeout = 10000) {
|
|
363
|
+
const env = process.platform === "darwin" && existsSync(COLIMA_SOCKET)
|
|
364
|
+
? { ...process.env, DOCKER_HOST: `unix://${COLIMA_SOCKET}` }
|
|
365
|
+
: undefined;
|
|
359
366
|
try {
|
|
360
|
-
execFileSync("docker", ["info"], { timeout, stdio: "ignore" });
|
|
367
|
+
execFileSync("docker", ["info"], { timeout, stdio: "ignore", env });
|
|
361
368
|
return true;
|
|
362
369
|
}
|
|
363
370
|
catch { }
|
|
364
371
|
try {
|
|
365
|
-
execFileSync("sudo", ["-n", "docker", "info"], { timeout, stdio: "ignore" });
|
|
372
|
+
execFileSync("sudo", ["-n", "docker", "info"], { timeout, stdio: "ignore", env });
|
|
366
373
|
return true;
|
|
367
374
|
}
|
|
368
375
|
catch { }
|
|
@@ -386,10 +393,7 @@ export function getSetupStatus() {
|
|
|
386
393
|
const localBin = join(OPENCLAW_BIN_DIR, "openclaw");
|
|
387
394
|
const localBinOk = existsSync(localBin);
|
|
388
395
|
const fastDockerImageReady = checkDockerImageExists();
|
|
389
|
-
const
|
|
390
|
-
const official = isOfficialImage(baseTag);
|
|
391
|
-
// Official image: Docker image alone is sufficient. Slim base: also needs npm package.
|
|
392
|
-
const openclawOk = official ? fastDockerImageReady : (localBinOk || fastDockerImageReady);
|
|
396
|
+
const openclawOk = localBinOk || fastDockerImageReady;
|
|
393
397
|
// Lightweight validation: verify critical services are actually available
|
|
394
398
|
const dockerOk = canAccessDockerDaemon(5000);
|
|
395
399
|
const nomadOk = isPortListening(4646);
|
|
@@ -412,34 +416,14 @@ export function getSetupStatus() {
|
|
|
412
416
|
catch { }
|
|
413
417
|
let openclawVer = "installed";
|
|
414
418
|
let openclawPath = localBin;
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
else {
|
|
421
|
-
// Prefer npm package.json for accurate OpenClaw version.
|
|
422
|
-
try {
|
|
423
|
-
const pkg = join(OPENCLAW_MODULES, "openclaw", "package.json");
|
|
424
|
-
if (existsSync(pkg))
|
|
425
|
-
openclawVer = JSON.parse(readFileSync(pkg, "utf-8")).version || openclawVer;
|
|
426
|
-
}
|
|
427
|
-
catch { }
|
|
428
|
-
// Fallback: extract version from old-style openclaw:* image tag (legacy migration path).
|
|
429
|
-
if (openclawVer === "installed" && fastDockerImageReady) {
|
|
430
|
-
const imageTag = resolveDockerImageTag();
|
|
431
|
-
if (/^openclaw:/i.test(imageTag)) {
|
|
432
|
-
openclawVer = imageTag.replace(/^openclaw:/i, "");
|
|
433
|
-
openclawPath = imageTag;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
419
|
+
// Prefer npm package.json for accurate OpenClaw version.
|
|
420
|
+
try {
|
|
421
|
+
const pkg = join(OPENCLAW_MODULES, "openclaw", "package.json");
|
|
422
|
+
if (existsSync(pkg))
|
|
423
|
+
openclawVer = JSON.parse(readFileSync(pkg, "utf-8")).version || openclawVer;
|
|
436
424
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const needsNpmForMount = /^jishushell-base:/i.test(baseTag);
|
|
440
|
-
const ready = official
|
|
441
|
-
? fastDockerImageReady
|
|
442
|
-
: (openclawOk && fastDockerImageReady && (!needsNpmForMount || localBinOk));
|
|
425
|
+
catch { }
|
|
426
|
+
const ready = openclawOk && fastDockerImageReady;
|
|
443
427
|
return {
|
|
444
428
|
node: { name: "Node.js", installed: true, running: true, version: process.version, path: process.execPath },
|
|
445
429
|
docker: { name: "Docker", installed: true, running: true, version: dockerVer, path: "" },
|
|
@@ -510,11 +494,7 @@ export function getSetupStatus() {
|
|
|
510
494
|
// the local npm package is absent — the image is all that's needed to run instances.
|
|
511
495
|
if (!openclaw.ok && dockerImageReady) {
|
|
512
496
|
const imageTag = resolveDockerImageTag();
|
|
513
|
-
|
|
514
|
-
// Legacy image: strip openclaw: prefix
|
|
515
|
-
const tagVersion = isOfficialImage(imageTag)
|
|
516
|
-
? (imageTag.split(":").pop() || "local")
|
|
517
|
-
: imageTag.replace(/^openclaw:/i, "");
|
|
497
|
+
const tagVersion = imageTag.split(":").pop() || "local";
|
|
518
498
|
openclaw = { ok: true, version: tagVersion, path: imageTag };
|
|
519
499
|
}
|
|
520
500
|
const openclawStatus = {
|
|
@@ -789,6 +769,227 @@ function getNomadDownloadUrl() {
|
|
|
789
769
|
const os = process.platform === "linux" ? "linux" : "darwin";
|
|
790
770
|
return `https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_${os}_${arch}.zip`;
|
|
791
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Signal nomad agents by exact process name (pgrep -x nomad) to avoid the
|
|
774
|
+
* classic pkill -f self-match bug: a command line like "pkill -f 'nomad agent'"
|
|
775
|
+
* literally contains the pattern and pkill kills itself before reaching the
|
|
776
|
+
* real nomad process. pgrep's own comm is "pgrep" (not "nomad") so -x nomad
|
|
777
|
+
* cannot self-match. Unprivileged kill is tried first; sudo -n as a fallback
|
|
778
|
+
* if the running nomad is owned by root (1.6.5 User=root unit).
|
|
779
|
+
*/
|
|
780
|
+
function killNomadByProcName() {
|
|
781
|
+
const collect = () => {
|
|
782
|
+
try {
|
|
783
|
+
const out = execSync("pgrep -x nomad 2>/dev/null || true", { encoding: "utf-8" }).trim();
|
|
784
|
+
return out.split("\n").filter(Boolean);
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
return [];
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
const sendSignal = (sig, pids) => {
|
|
791
|
+
if (pids.length === 0)
|
|
792
|
+
return;
|
|
793
|
+
try {
|
|
794
|
+
execFileSync("sudo", ["-n", "kill", `-${sig}`, ...pids], { timeout: 5000, stdio: "pipe" });
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
catch { }
|
|
798
|
+
try {
|
|
799
|
+
execSync(`kill -${sig} ${pids.join(" ")} 2>/dev/null || true`, { timeout: 5000 });
|
|
800
|
+
}
|
|
801
|
+
catch { }
|
|
802
|
+
};
|
|
803
|
+
let pids = collect();
|
|
804
|
+
sendSignal("TERM", pids);
|
|
805
|
+
if (pids.length > 0) {
|
|
806
|
+
// Short grace period, then SIGKILL any survivors.
|
|
807
|
+
execSync("sleep 2");
|
|
808
|
+
pids = collect();
|
|
809
|
+
sendSignal("KILL", pids);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/** Compare two "a.b.c" semver strings; returns a > b. */
|
|
813
|
+
function isNomadVersionGreater(a, b) {
|
|
814
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map(n => parseInt(n, 10) || 0);
|
|
815
|
+
const [aMaj, aMin, aPat] = parse(a);
|
|
816
|
+
const [bMaj, bMin, bPat] = parse(b);
|
|
817
|
+
if (aMaj !== bMaj)
|
|
818
|
+
return aMaj > bMaj;
|
|
819
|
+
if (aMin !== bMin)
|
|
820
|
+
return aMin > bMin;
|
|
821
|
+
return aPat > bPat;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Auto-migrate from a higher Nomad version (e.g. 1.11.3 BSL) back to the
|
|
825
|
+
* jishushell target (1.6.5 MPL). Called when installNomad detects a local
|
|
826
|
+
* binary whose semver is > NOMAD_VERSION. Destructive to Nomad's raft state
|
|
827
|
+
* (schema is not backward compatible) but preserves instance configs under
|
|
828
|
+
* ~/.jishushell/instances/*. A single tar.gz snapshot of the old data_dir
|
|
829
|
+
* is kept under ~/.jishushell/nomad/backups/ for forensic inspection.
|
|
830
|
+
*
|
|
831
|
+
* Safe-first: the new binary is downloaded and verified BEFORE any existing
|
|
832
|
+
* state is touched. If any stage 1 step fails, state is untouched.
|
|
833
|
+
*
|
|
834
|
+
* Throws on failure so the caller's outer catch reports the error.
|
|
835
|
+
*/
|
|
836
|
+
async function migrateNomadToTarget(currentVersion) {
|
|
837
|
+
console.log(`[nomad] Auto-migrating v${currentVersion} → v${NOMAD_VERSION} (BSL → MPL)`);
|
|
838
|
+
console.log("[nomad] Raft state is not backward-compatible; allocation history will be reset.");
|
|
839
|
+
console.log("[nomad] Instance configs under ~/.jishushell/instances/ are preserved.");
|
|
840
|
+
// ── Stage 1: download + verify new binary into a staging dir ─────────
|
|
841
|
+
const stageDir = mkdtempSync(join(tmpdir(), "nomad-migrate-"));
|
|
842
|
+
let backupFile = "";
|
|
843
|
+
try {
|
|
844
|
+
const stagedBin = join(stageDir, "nomad");
|
|
845
|
+
const zipPath = join(stageDir, "nomad.zip");
|
|
846
|
+
const url = getNomadDownloadUrl();
|
|
847
|
+
const arch = process.arch === "arm64" ? "arm64" : "amd64";
|
|
848
|
+
const os = process.platform === "linux" ? "linux" : "darwin";
|
|
849
|
+
const sumsUrl = `https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_SHA256SUMS`;
|
|
850
|
+
const sumsPath = join(stageDir, "SHA256SUMS");
|
|
851
|
+
console.log(`[nomad] Staging v${NOMAD_VERSION} (${os}/${arch})...`);
|
|
852
|
+
execFileSync("curl", ["-fsSL", url, "-o", zipPath], { timeout: 300000, stdio: "pipe" });
|
|
853
|
+
execFileSync("curl", ["-fsSL", sumsUrl, "-o", sumsPath], { timeout: 30000, stdio: "pipe" });
|
|
854
|
+
const sums = readFileSync(sumsPath, "utf-8");
|
|
855
|
+
const sumLine = sums.split("\n").find(l => l.includes(`nomad_${NOMAD_VERSION}_${os}_${arch}.zip`));
|
|
856
|
+
if (!sumLine)
|
|
857
|
+
throw new Error(`No checksum entry for nomad_${NOMAD_VERSION}_${os}_${arch}.zip`);
|
|
858
|
+
const expected = sumLine.split(/\s+/)[0];
|
|
859
|
+
// Match the bash installer: prefer sha256sum (GNU coreutils, Linux),
|
|
860
|
+
// fall back to shasum -a 256 (BSD, macOS default — sha256sum is not
|
|
861
|
+
// preinstalled there). Without this, triggering auto-migration from
|
|
862
|
+
// the WebUI / Node path on macOS would fail even though the shell
|
|
863
|
+
// installer works fine.
|
|
864
|
+
let actual;
|
|
865
|
+
try {
|
|
866
|
+
actual = execSync(`sha256sum "${zipPath}" | awk '{print $1}'`, { encoding: "utf-8" }).trim();
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
actual = execSync(`shasum -a 256 "${zipPath}" | awk '{print $1}'`, { encoding: "utf-8" }).trim();
|
|
870
|
+
}
|
|
871
|
+
if (expected !== actual) {
|
|
872
|
+
throw new Error(`Nomad checksum mismatch: expected ${expected}, got ${actual}`);
|
|
873
|
+
}
|
|
874
|
+
console.log("[nomad] Checksum verified");
|
|
875
|
+
execFileSync("unzip", ["-o", zipPath, "-d", stageDir], { timeout: 30000 });
|
|
876
|
+
chmodSync(stagedBin, 0o755);
|
|
877
|
+
const stagedVersionLine = execFileSync(stagedBin, ["version"], { encoding: "utf-8", timeout: 5000 }).trim().split("\n")[0];
|
|
878
|
+
if (!stagedVersionLine.includes(`v${NOMAD_VERSION}`)) {
|
|
879
|
+
throw new Error(`Staged binary reports "${stagedVersionLine}", expected v${NOMAD_VERSION}`);
|
|
880
|
+
}
|
|
881
|
+
console.log(`[nomad] Staged ${stagedVersionLine}`);
|
|
882
|
+
// ── Stage 2: destructive state changes begin ───────────────────────
|
|
883
|
+
console.log("[nomad] Stopping services...");
|
|
884
|
+
try {
|
|
885
|
+
execFileSync("sudo", ["-n", "systemctl", "stop", "jishushell"], { timeout: 15000, stdio: "pipe" });
|
|
886
|
+
}
|
|
887
|
+
catch { }
|
|
888
|
+
try {
|
|
889
|
+
execFileSync("sudo", ["-n", "systemctl", "stop", "nomad"], { timeout: 15000, stdio: "pipe" });
|
|
890
|
+
}
|
|
891
|
+
catch { }
|
|
892
|
+
// pkill -f 'nomad agent' matches pkill's own cmdline and self-terminates
|
|
893
|
+
// before reaching the real nomad process. Use pgrep -x nomad (exact proc
|
|
894
|
+
// name match; pgrep's comm is "pgrep") to avoid the self-match bug.
|
|
895
|
+
killNomadByProcName();
|
|
896
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
897
|
+
// ── Stage 3: tar backup (single snapshot, overwrite any previous) ──
|
|
898
|
+
const backupDir = join(NOMAD_CONFIG_DIR, "backups");
|
|
899
|
+
if (existsSync(NOMAD_DATA_DIR)) {
|
|
900
|
+
try {
|
|
901
|
+
ensureDirHost(backupDir);
|
|
902
|
+
}
|
|
903
|
+
catch { }
|
|
904
|
+
const ts = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "-");
|
|
905
|
+
const candidate = join(backupDir, `data-${ts}.tar.gz`);
|
|
906
|
+
console.log(`[nomad] Backing up raft state → ${candidate}`);
|
|
907
|
+
try {
|
|
908
|
+
execSync(`tar czf "${candidate}" -C "${NOMAD_CONFIG_DIR}" data 2>/dev/null`, { timeout: 120000 });
|
|
909
|
+
backupFile = candidate;
|
|
910
|
+
// Keep only the most recent snapshot
|
|
911
|
+
try {
|
|
912
|
+
const list = execSync(`ls -t "${backupDir}"/data-*.tar.gz 2>/dev/null | tail -n +2 || true`, { encoding: "utf-8" }).trim();
|
|
913
|
+
for (const old of list.split("\n").filter(Boolean)) {
|
|
914
|
+
try {
|
|
915
|
+
unlinkSync(old);
|
|
916
|
+
}
|
|
917
|
+
catch { }
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
catch { }
|
|
921
|
+
}
|
|
922
|
+
catch {
|
|
923
|
+
console.warn("[nomad] Backup tar failed — continuing (raft state will still be wiped)");
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
// ── Stage 4: wipe raft state + env files (schema incompatible) ─────
|
|
927
|
+
try {
|
|
928
|
+
execFileSync("sudo", ["-n", "rm", "-rf", NOMAD_DATA_DIR], { timeout: 15000, stdio: "pipe" });
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
try {
|
|
932
|
+
rmSync(NOMAD_DATA_DIR, { recursive: true, force: true });
|
|
933
|
+
}
|
|
934
|
+
catch { }
|
|
935
|
+
}
|
|
936
|
+
try {
|
|
937
|
+
unlinkSync(join(JISHUSHELL_HOME, "nomad.env"));
|
|
938
|
+
}
|
|
939
|
+
catch { }
|
|
940
|
+
try {
|
|
941
|
+
execFileSync("sudo", ["-n", "rm", "-f", "/etc/jishushell/nomad.env"], { timeout: 5000, stdio: "pipe" });
|
|
942
|
+
}
|
|
943
|
+
catch { }
|
|
944
|
+
// ── Stage 5: orphaned gateway containers ───────────────────────────
|
|
945
|
+
// Panel normally has docker group via jishushell.service SupplementaryGroups,
|
|
946
|
+
// but postinstall may run this helper from a shell where the invoking
|
|
947
|
+
// user is not in docker group. Probe first, fall back to sudo docker.
|
|
948
|
+
try {
|
|
949
|
+
let dockerCmd = "docker";
|
|
950
|
+
try {
|
|
951
|
+
execSync("docker ps >/dev/null 2>&1", { timeout: 5000 });
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
dockerCmd = "sudo -n docker";
|
|
955
|
+
}
|
|
956
|
+
const names = execSync(`${dockerCmd} ps -a --format '{{.Names}}' 2>/dev/null | grep '^gateway-' || true`, { encoding: "utf-8" }).trim();
|
|
957
|
+
if (names) {
|
|
958
|
+
const rows = names.split("\n").filter(Boolean);
|
|
959
|
+
for (const name of rows) {
|
|
960
|
+
try {
|
|
961
|
+
execSync(`${dockerCmd} rm -f "${name}" 2>/dev/null`, { timeout: 10000 });
|
|
962
|
+
}
|
|
963
|
+
catch { }
|
|
964
|
+
}
|
|
965
|
+
console.log(`[nomad] Removed ${rows.length} orphaned gateway container(s)`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
catch { }
|
|
969
|
+
// ── Stage 6: swap binary into place (atomic via temp name + rename)
|
|
970
|
+
ensureDirHost(BIN_DIR);
|
|
971
|
+
const destTmp = `${NOMAD_BIN}.tmp.${process.pid}`;
|
|
972
|
+
copyFileSync(stagedBin, destTmp);
|
|
973
|
+
chmodSync(destTmp, 0o755);
|
|
974
|
+
renameSync(destTmp, NOMAD_BIN);
|
|
975
|
+
console.log(`[nomad] Migrated to v${NOMAD_VERSION}`);
|
|
976
|
+
if (backupFile)
|
|
977
|
+
console.log(`[nomad] Backup (forensic, not self-recovery): ${backupFile}`);
|
|
978
|
+
console.log("[nomad] JishuShell will re-bootstrap ACL and resubmit jobs on next start.");
|
|
979
|
+
}
|
|
980
|
+
catch (err) {
|
|
981
|
+
if (backupFile) {
|
|
982
|
+
console.error(`[nomad] Migration failed — backup preserved at ${backupFile}`);
|
|
983
|
+
}
|
|
984
|
+
throw err;
|
|
985
|
+
}
|
|
986
|
+
finally {
|
|
987
|
+
try {
|
|
988
|
+
rmSync(stageDir, { recursive: true, force: true });
|
|
989
|
+
}
|
|
990
|
+
catch { }
|
|
991
|
+
}
|
|
992
|
+
}
|
|
792
993
|
export async function installNomad() {
|
|
793
994
|
try {
|
|
794
995
|
if (existsSync(NOMAD_BIN)) {
|
|
@@ -810,8 +1011,38 @@ export async function installNomad() {
|
|
|
810
1011
|
}
|
|
811
1012
|
catch { }
|
|
812
1013
|
// Boundary check 3: does it actually run?
|
|
1014
|
+
let versionLine = "";
|
|
813
1015
|
try {
|
|
814
|
-
|
|
1016
|
+
versionLine = execSync(`"${NOMAD_BIN}" version`, { encoding: "utf-8", timeout: 5000 }).trim().split("\n")[0];
|
|
1017
|
+
}
|
|
1018
|
+
catch {
|
|
1019
|
+
// Binary is corrupt or wrong arch — remove and reinstall
|
|
1020
|
+
try {
|
|
1021
|
+
unlinkSync(NOMAD_BIN);
|
|
1022
|
+
}
|
|
1023
|
+
catch { }
|
|
1024
|
+
}
|
|
1025
|
+
if (versionLine) {
|
|
1026
|
+
const match = versionLine.match(/v(\d+\.\d+\.\d+)/);
|
|
1027
|
+
const currentVersion = match ? match[1] : "";
|
|
1028
|
+
if (currentVersion && isNomadVersionGreater(currentVersion, NOMAD_VERSION)) {
|
|
1029
|
+
// Current > target — auto-migrate (nomad 1.11.3 BSL → 1.6.5 MPL).
|
|
1030
|
+
// Migration failure is a hard stop: the old state has been
|
|
1031
|
+
// partially mutated (or about to be), returning falls through
|
|
1032
|
+
// to the reinstall path which would make a bad situation worse.
|
|
1033
|
+
try {
|
|
1034
|
+
await migrateNomadToTarget(currentVersion);
|
|
1035
|
+
}
|
|
1036
|
+
catch (migErr) {
|
|
1037
|
+
return {
|
|
1038
|
+
ok: false,
|
|
1039
|
+
message: "Nomad auto-migration failed",
|
|
1040
|
+
error: migErr?.message || String(migErr),
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
const newLine = execSync(`"${NOMAD_BIN}" version`, { encoding: "utf-8", timeout: 5000 }).trim().split("\n")[0];
|
|
1044
|
+
return { ok: true, message: `Nomad migrated to ${newLine}` };
|
|
1045
|
+
}
|
|
815
1046
|
// Ensure Nomad is started even if already installed
|
|
816
1047
|
if (!isPortListening(4646)) {
|
|
817
1048
|
try {
|
|
@@ -822,14 +1053,7 @@ export async function installNomad() {
|
|
|
822
1053
|
await startNomad();
|
|
823
1054
|
}
|
|
824
1055
|
}
|
|
825
|
-
return { ok: true, message: `Nomad already installed: ${
|
|
826
|
-
}
|
|
827
|
-
catch {
|
|
828
|
-
// Binary is corrupt or wrong arch — remove and reinstall
|
|
829
|
-
try {
|
|
830
|
-
unlinkSync(NOMAD_BIN);
|
|
831
|
-
}
|
|
832
|
-
catch { }
|
|
1056
|
+
return { ok: true, message: `Nomad already installed: ${versionLine}` };
|
|
833
1057
|
}
|
|
834
1058
|
}
|
|
835
1059
|
}
|
|
@@ -839,7 +1063,7 @@ export async function installNomad() {
|
|
|
839
1063
|
try {
|
|
840
1064
|
const systemNomad = execSync("which nomad 2>/dev/null", { encoding: "utf-8" }).trim();
|
|
841
1065
|
if (systemNomad && existsSync(systemNomad)) {
|
|
842
|
-
|
|
1066
|
+
ensureDirHost(BIN_DIR);
|
|
843
1067
|
symlinkSync(systemNomad, NOMAD_BIN);
|
|
844
1068
|
const version = execSync(`${NOMAD_BIN} version`, { encoding: "utf-8", timeout: 5000 }).trim();
|
|
845
1069
|
console.log(`[nomad] Linked system nomad ${systemNomad} → ${NOMAD_BIN}`);
|
|
@@ -849,7 +1073,7 @@ export async function installNomad() {
|
|
|
849
1073
|
catch { /* system nomad not found — proceed to download */ }
|
|
850
1074
|
}
|
|
851
1075
|
const task = createTask("nomad");
|
|
852
|
-
|
|
1076
|
+
ensureDirHost(BIN_DIR);
|
|
853
1077
|
const url = getNomadDownloadUrl();
|
|
854
1078
|
const zipPath = join(BIN_DIR, "nomad.zip");
|
|
855
1079
|
emitTask(task, { type: "progress", message: "下载 Nomad...", progress: 0 });
|
|
@@ -917,12 +1141,10 @@ function fixNomadDirOwnership() {
|
|
|
917
1141
|
}
|
|
918
1142
|
function writeNomadConfig() {
|
|
919
1143
|
fixNomadDirOwnership();
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
mkdirSync(NOMAD_ALLOC_DIR, { recursive: true, mode: 0o755 });
|
|
925
|
-
chmodSync(NOMAD_ALLOC_DIR, 0o755);
|
|
1144
|
+
ensureDirHost(NOMAD_CONFIG_DIR);
|
|
1145
|
+
ensureDirContainer(NOMAD_DATA_DIR);
|
|
1146
|
+
ensureDirContainer(NOMAD_ALLOC_DIR);
|
|
1147
|
+
const loopbackIface = process.platform === "darwin" ? "lo0" : "lo";
|
|
926
1148
|
const config = `
|
|
927
1149
|
data_dir = "${NOMAD_DATA_DIR}"
|
|
928
1150
|
|
|
@@ -944,18 +1166,18 @@ server {
|
|
|
944
1166
|
client {
|
|
945
1167
|
enabled = true
|
|
946
1168
|
servers = ["127.0.0.1:4647"]
|
|
1169
|
+
network_interface = "${loopbackIface}"
|
|
947
1170
|
alloc_dir = "${NOMAD_ALLOC_DIR}"
|
|
948
1171
|
|
|
949
|
-
drain_on_shutdown
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1172
|
+
# drain_on_shutdown intentionally omitted: on single-node Pi there is
|
|
1173
|
+
# nowhere to drain workloads to, and draining on every systemctl restart
|
|
1174
|
+
# would kill every running OpenClaw instance. Without this block Nomad
|
|
1175
|
+
# leaves allocations running across client restarts — the docker driver
|
|
1176
|
+
# re-attaches to the existing containers on startup.
|
|
954
1177
|
}
|
|
955
1178
|
|
|
956
1179
|
plugin "docker" {
|
|
957
1180
|
config {
|
|
958
|
-
disable_log_collection = true
|
|
959
1181
|
volumes {
|
|
960
1182
|
enabled = true
|
|
961
1183
|
}
|
|
@@ -966,7 +1188,7 @@ acl {
|
|
|
966
1188
|
enabled = true
|
|
967
1189
|
}
|
|
968
1190
|
`;
|
|
969
|
-
|
|
1191
|
+
writeConfigFile(join(NOMAD_CONFIG_DIR, "nomad.hcl"), config);
|
|
970
1192
|
}
|
|
971
1193
|
export function loadNomadToken() {
|
|
972
1194
|
if (process.env.NOMAD_TOKEN)
|
|
@@ -1077,10 +1299,10 @@ async function bootstrapNomadACL() {
|
|
|
1077
1299
|
const saveToken = (token) => {
|
|
1078
1300
|
const envFile = join(JISHUSHELL_HOME, "nomad.env");
|
|
1079
1301
|
const envContent = `NOMAD_TOKEN=${token}\n`;
|
|
1080
|
-
|
|
1302
|
+
writeSecretFile(envFile, envContent);
|
|
1081
1303
|
try {
|
|
1082
1304
|
execFileSync("sudo", ["-n", "mkdir", "-p", "/etc/jishushell"], { timeout: 5000, stdio: "pipe" });
|
|
1083
|
-
|
|
1305
|
+
writeSystemTmpFile("/tmp/.nomad-env-tmp", envContent);
|
|
1084
1306
|
execFileSync("sudo", ["-n", "cp", "/tmp/.nomad-env-tmp", "/etc/jishushell/nomad.env"], { timeout: 5000, stdio: "pipe" });
|
|
1085
1307
|
execFileSync("sudo", ["-n", "chmod", "600", "/etc/jishushell/nomad.env"], { timeout: 5000, stdio: "pipe" });
|
|
1086
1308
|
try {
|
|
@@ -1111,14 +1333,27 @@ async function bootstrapNomadACL() {
|
|
|
1111
1333
|
}
|
|
1112
1334
|
const resetIndex = resetMatch[1];
|
|
1113
1335
|
console.log(`[nomad] Bootstrap already done (reset index: ${resetIndex}). Performing ACL bootstrap reset...`);
|
|
1114
|
-
// Write the reset trigger file (Nomad reads this on startup to allow re-bootstrap)
|
|
1336
|
+
// Write the reset trigger file (Nomad reads this on startup to allow re-bootstrap).
|
|
1337
|
+
// NOMAD_DATA_DIR/server is owned by root because nomad.service runs as User=root
|
|
1338
|
+
// (docker driver on 1.6.5 requires euid==0). The panel runs as a non-root user, so
|
|
1339
|
+
// plain writeConfigFile would fail with EACCES — route through `sudo tee` instead.
|
|
1115
1340
|
const resetFile = join(NOMAD_DATA_DIR, "server", "acl-bootstrap-reset");
|
|
1116
1341
|
try {
|
|
1117
|
-
|
|
1342
|
+
writeConfigFile(resetFile, resetIndex);
|
|
1118
1343
|
}
|
|
1119
1344
|
catch (writeErr) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1345
|
+
try {
|
|
1346
|
+
execFileSync("sudo", ["-n", "mkdir", "-p", dirname(resetFile)], { timeout: 5000, stdio: "pipe" });
|
|
1347
|
+
execFileSync("sudo", ["-n", "tee", resetFile], {
|
|
1348
|
+
timeout: 5000,
|
|
1349
|
+
input: resetIndex,
|
|
1350
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
catch (sudoErr) {
|
|
1354
|
+
console.warn("[nomad] Could not write acl-bootstrap-reset file:", sudoErr.message || writeErr.message);
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1122
1357
|
}
|
|
1123
1358
|
// Restart Nomad so it picks up the reset file
|
|
1124
1359
|
try {
|
|
@@ -1127,9 +1362,14 @@ async function bootstrapNomadACL() {
|
|
|
1127
1362
|
catch {
|
|
1128
1363
|
// No passwordless sudo — try pkill/re-spawn path (best effort)
|
|
1129
1364
|
try {
|
|
1130
|
-
execFileSync("
|
|
1365
|
+
execFileSync("sudo", ["-n", "pkill", "-TERM", "-f", "nomad agent"], { stdio: "pipe" });
|
|
1366
|
+
}
|
|
1367
|
+
catch {
|
|
1368
|
+
try {
|
|
1369
|
+
execFileSync("pkill", ["-TERM", "-f", "nomad agent"], { stdio: "pipe" });
|
|
1370
|
+
}
|
|
1371
|
+
catch { }
|
|
1131
1372
|
}
|
|
1132
|
-
catch { }
|
|
1133
1373
|
}
|
|
1134
1374
|
// Wait for Nomad to come back
|
|
1135
1375
|
for (let i = 0; i < 20; i++) {
|
|
@@ -1257,14 +1497,15 @@ export async function stopNomad() {
|
|
|
1257
1497
|
try {
|
|
1258
1498
|
if (!isPortListening(4646))
|
|
1259
1499
|
return { ok: true, message: "Nomad not running" };
|
|
1260
|
-
// SIGTERM: graceful shutdown — Nomad flushes state
|
|
1261
|
-
//
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
// Wait up to
|
|
1267
|
-
|
|
1500
|
+
// SIGTERM: graceful shutdown — Nomad flushes state and detaches from
|
|
1501
|
+
// running allocs without killing them (drain_on_shutdown is deliberately
|
|
1502
|
+
// not configured, so the docker containers keep running and will be
|
|
1503
|
+
// re-attached when Nomad comes back).
|
|
1504
|
+
// Use killNomadByProcName (pgrep -x) to avoid pkill -f self-matching.
|
|
1505
|
+
killNomadByProcName();
|
|
1506
|
+
// Wait up to 10s for the process to exit. No drain means shutdown is
|
|
1507
|
+
// near-instant — most of this budget is slack for slow disks on Pi.
|
|
1508
|
+
for (let i = 0; i < 10; i++) {
|
|
1268
1509
|
await new Promise(r => setTimeout(r, 1000));
|
|
1269
1510
|
if (!isPortListening(4646))
|
|
1270
1511
|
return { ok: true, message: "Nomad stopped" };
|
|
@@ -1292,8 +1533,7 @@ export function installNomadSystemd() {
|
|
|
1292
1533
|
if (process.platform === "darwin") {
|
|
1293
1534
|
const plistLabel = "com.jishushell.nomad";
|
|
1294
1535
|
const logPath = join(NOMAD_CONFIG_DIR, "nomad.log");
|
|
1295
|
-
const
|
|
1296
|
-
const dockerSock = existsSync(primarySocket) ? primarySocket : "/var/run/docker.sock";
|
|
1536
|
+
const dockerSock = COLIMA_SOCKET;
|
|
1297
1537
|
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1298
1538
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1299
1539
|
<plist version="1.0">
|
|
@@ -1317,8 +1557,7 @@ export function installNomadSystemd() {
|
|
|
1317
1557
|
</dict>
|
|
1318
1558
|
</plist>`;
|
|
1319
1559
|
const plistPath = join(process.env.HOME || dirname(JISHUSHELL_HOME), `Library/LaunchAgents/${plistLabel}.plist`);
|
|
1320
|
-
|
|
1321
|
-
writeFileSync(plistPath, plistContent);
|
|
1560
|
+
writeConfigFile(plistPath, plistContent);
|
|
1322
1561
|
try {
|
|
1323
1562
|
execSync(`launchctl unload "${plistPath}" 2>/dev/null`);
|
|
1324
1563
|
}
|
|
@@ -1326,17 +1565,18 @@ export function installNomadSystemd() {
|
|
|
1326
1565
|
execSync(`launchctl load -w "${plistPath}"`, { timeout: 15000 });
|
|
1327
1566
|
return { ok: true, message: "Nomad launchd agent installed and started" };
|
|
1328
1567
|
}
|
|
1329
|
-
// Nomad
|
|
1330
|
-
//
|
|
1331
|
-
|
|
1568
|
+
// Nomad 1.6.5's docker driver fingerprint requires euid==0 — PR #18197 lifted
|
|
1569
|
+
// that restriction only in 1.7+, and we intentionally stay on the 1.6 MPL line.
|
|
1570
|
+
// The panel stays as the installing user via a separate jishushell.service unit;
|
|
1571
|
+
// it talks to this agent over HTTP, so no files under ~/.jishushell/nomad/data/
|
|
1572
|
+
// are read directly by the panel.
|
|
1332
1573
|
const serviceContent = `[Unit]
|
|
1333
1574
|
Description=Nomad Agent
|
|
1334
1575
|
After=network-online.target docker.service
|
|
1335
1576
|
Wants=network-online.target
|
|
1336
1577
|
|
|
1337
1578
|
[Service]
|
|
1338
|
-
User
|
|
1339
|
-
SupplementaryGroups=docker
|
|
1579
|
+
User=root
|
|
1340
1580
|
Type=simple
|
|
1341
1581
|
EnvironmentFile=-/etc/jishushell/nomad.env
|
|
1342
1582
|
ExecStart=${nomadPath} agent -config=${configPath}
|
|
@@ -1348,7 +1588,7 @@ WantedBy=multi-user.target
|
|
|
1348
1588
|
`;
|
|
1349
1589
|
const servicePath = "/etc/systemd/system/nomad.service";
|
|
1350
1590
|
execFileSync("sudo", ["mkdir", "-p", "/etc/jishushell"], { timeout: 5000 });
|
|
1351
|
-
|
|
1591
|
+
writeSystemTmpFile("/tmp/nomad.service", serviceContent);
|
|
1352
1592
|
execFileSync("sudo", ["cp", "/tmp/nomad.service", servicePath], { timeout: 5000 });
|
|
1353
1593
|
execFileSync("sudo", ["systemctl", "daemon-reload"], { timeout: 5000 });
|
|
1354
1594
|
execFileSync("sudo", ["systemctl", "enable", "--now", "nomad"], { timeout: 15000 });
|
|
@@ -1384,11 +1624,11 @@ export function installJishushellSystemd(port) {
|
|
|
1384
1624
|
export JISHUSHELL_HOME="${JISHUSHELL_HOME}"
|
|
1385
1625
|
export HOME="${realHome}"
|
|
1386
1626
|
export NODE_ENV=production
|
|
1387
|
-
export
|
|
1627
|
+
export DOCKER_HOST="unix://${COLIMA_SOCKET}"
|
|
1628
|
+
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin:${dirname(nodeBin)}:\${PATH}"
|
|
1388
1629
|
exec "${nodeBin}" "${cliBin}" serve --port ${resolvedPort}
|
|
1389
1630
|
`;
|
|
1390
|
-
|
|
1391
|
-
writeFileSync(wrapperPath, wrapperContent, { mode: 0o755 });
|
|
1631
|
+
writeExecutableFile(wrapperPath, wrapperContent);
|
|
1392
1632
|
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1393
1633
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1394
1634
|
<plist version="1.0">
|
|
@@ -1406,8 +1646,7 @@ exec "${nodeBin}" "${cliBin}" serve --port ${resolvedPort}
|
|
|
1406
1646
|
</dict>
|
|
1407
1647
|
</plist>`;
|
|
1408
1648
|
const plistPath = join(process.env.HOME || realHome, `Library/LaunchAgents/${plistLabel}.plist`);
|
|
1409
|
-
|
|
1410
|
-
writeFileSync(plistPath, plistContent);
|
|
1649
|
+
writeConfigFile(plistPath, plistContent);
|
|
1411
1650
|
const panelAlreadyRunning = isPortListening(resolvedPort);
|
|
1412
1651
|
if (!panelAlreadyRunning) {
|
|
1413
1652
|
try {
|
|
@@ -1441,7 +1680,7 @@ Environment=NODE_ENV=production
|
|
|
1441
1680
|
WantedBy=multi-user.target
|
|
1442
1681
|
`;
|
|
1443
1682
|
execFileSync("sudo", ["mkdir", "-p", "/etc/jishushell"], { timeout: 5000 });
|
|
1444
|
-
|
|
1683
|
+
writeSystemTmpFile("/tmp/jishushell.service", serviceContent);
|
|
1445
1684
|
execFileSync("sudo", ["cp", "/tmp/jishushell.service", "/etc/systemd/system/jishushell.service"], { timeout: 5000 });
|
|
1446
1685
|
execFileSync("sudo", ["systemctl", "daemon-reload"], { timeout: 5000 });
|
|
1447
1686
|
execFileSync("sudo", ["systemctl", "enable", "jishushell"], { timeout: 15000 });
|
|
@@ -1463,7 +1702,7 @@ export async function installOpenclaw(version = "latest") {
|
|
|
1463
1702
|
return { ok: true, message: `OpenClaw already installed: ${ver}` };
|
|
1464
1703
|
}
|
|
1465
1704
|
const task = createTask("openclaw");
|
|
1466
|
-
|
|
1705
|
+
ensureDirHost(OPENCLAW_PKG_DIR);
|
|
1467
1706
|
emitTask(task, { type: "progress", message: "开始安装 OpenClaw...", progress: 0 });
|
|
1468
1707
|
// Monitor directory size for progress estimation
|
|
1469
1708
|
const sizeTracker = setInterval(() => {
|
|
@@ -1532,40 +1771,47 @@ function checkDockerImageExists() {
|
|
|
1532
1771
|
return true;
|
|
1533
1772
|
}
|
|
1534
1773
|
catch {
|
|
1774
|
+
// Fallback scan: list all local images and try to find any known runtime image.
|
|
1775
|
+
// This handles two scenarios:
|
|
1776
|
+
// 1. panel.json was wiped (e.g. after `jishushell reset`) and the pinned version
|
|
1777
|
+
// tag is no longer stored, causing getOpenclawDockerImage() to return the default
|
|
1778
|
+
// `:latest` tag which may have been removed locally during the first-run migration.
|
|
1779
|
+
// 2. The environment uses a locally built runtime image (e.g. jishushell-hermes:latest)
|
|
1780
|
+
// that differs from the registry default.
|
|
1781
|
+
// When a candidate is found we restore panel.json (self-heal) so the fast path works next time.
|
|
1782
|
+
try {
|
|
1783
|
+
const invocation = resolveDockerInvocation();
|
|
1784
|
+
const out = execFileSync(invocation.cmd, [...invocation.argsPrefix, "images", "--format", "{{.Repository}}:{{.Tag}}"], { encoding: "utf8", timeout: 5000 });
|
|
1785
|
+
const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1786
|
+
// 1. Same repository as DEFAULT_OPENCLAW_DOCKER_IMAGE (e.g. after pinned-tag migration).
|
|
1787
|
+
const defaultImage = DEFAULT_OPENCLAW_DOCKER_IMAGE;
|
|
1788
|
+
const repoColonIdx = defaultImage.lastIndexOf(":");
|
|
1789
|
+
const repoSlashIdx = defaultImage.lastIndexOf("/");
|
|
1790
|
+
if (repoColonIdx > repoSlashIdx) {
|
|
1791
|
+
const repo = defaultImage.slice(0, repoColonIdx);
|
|
1792
|
+
const repoPrefix = repo + ":";
|
|
1793
|
+
const found = lines.find((l) => l.startsWith(repoPrefix) && !l.endsWith(":<none>") && !l.endsWith(":none"));
|
|
1794
|
+
if (found) {
|
|
1795
|
+
setOpenclawDockerImage(found);
|
|
1796
|
+
return true;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
// 2. Backward compat: older locally-built jishushell-openclaw:* image names.
|
|
1800
|
+
// These use the same slim base architecture as ghcr.io/x-aijishu/openclaw-runtime:*
|
|
1801
|
+
// and are fully compatible. Self-heal panel.json so the tag stored there becomes
|
|
1802
|
+
// the concrete existing tag, preventing repeated DEFAULT-migration side effects.
|
|
1803
|
+
const legacyFound = lines.find((l) => /^jishushell-openclaw:[^\s]+/.test(l) && !l.endsWith(":<none>") && !l.endsWith(":none"));
|
|
1804
|
+
if (legacyFound) {
|
|
1805
|
+
setOpenclawDockerImage(legacyFound);
|
|
1806
|
+
return true;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
catch { }
|
|
1535
1810
|
return false;
|
|
1536
1811
|
}
|
|
1537
1812
|
}
|
|
1538
1813
|
// The stable tag for the JishuShell base image (slim — no OpenClaw binary baked in).
|
|
1539
|
-
// This tag does NOT change when OpenClaw is upgraded; the binary is bind-mounted from
|
|
1540
|
-
// the host at runtime. Rebuild this image only when system packages (Node.js, python3…)
|
|
1541
|
-
// need to change.
|
|
1542
|
-
export const BASE_IMAGE_TAG = "jishushell-base:v1";
|
|
1543
1814
|
function resolveDockerImageTag() {
|
|
1544
|
-
// 1. Environment variable takes precedence (same as runtime getOpenclawDockerImage)
|
|
1545
|
-
if (process.env.OPENCLAW_DOCKER_IMAGE)
|
|
1546
|
-
return process.env.OPENCLAW_DOCKER_IMAGE;
|
|
1547
|
-
// 2. Stored in panel.json — honours custom images and pre-existing openclaw:*
|
|
1548
|
-
// images that haven't been migrated yet (backward compatibility).
|
|
1549
|
-
const stored = getPanelConfig().openclaw_image;
|
|
1550
|
-
if (typeof stored === "string" && stored.trim())
|
|
1551
|
-
return stored;
|
|
1552
|
-
// 3. Scan local daemon — prefer official ghcr.io image, fall back to legacy jishushell-base.
|
|
1553
|
-
try {
|
|
1554
|
-
const invocation = resolveDockerInvocation();
|
|
1555
|
-
const output = execFileSync(invocation.cmd, [...invocation.argsPrefix, "images", "--format", "{{.Repository}}:{{.Tag}}"], { encoding: "utf-8", timeout: 5000 });
|
|
1556
|
-
const lines = output.split("\n").map(l => l.trim());
|
|
1557
|
-
// Prefer custom image (built with Python), then official, then legacy
|
|
1558
|
-
const custom = lines.find(l => l.startsWith(CUSTOM_IMAGE_PREFIX + ":"));
|
|
1559
|
-
if (custom)
|
|
1560
|
-
return custom;
|
|
1561
|
-
const official = lines.find(l => l.startsWith("ghcr.io/openclaw/openclaw:"));
|
|
1562
|
-
if (official)
|
|
1563
|
-
return official;
|
|
1564
|
-
const legacy = lines.find(l => /^jishushell-base:/i.test(l));
|
|
1565
|
-
if (legacy)
|
|
1566
|
-
return legacy;
|
|
1567
|
-
}
|
|
1568
|
-
catch { }
|
|
1569
1815
|
return getOpenclawDockerImage();
|
|
1570
1816
|
}
|
|
1571
1817
|
// Base image and mirror list for the OpenClaw Docker build.
|
|
@@ -1615,7 +1861,7 @@ function resolveVersionedBuildTag() {
|
|
|
1615
1861
|
if (existsSync(pkg)) {
|
|
1616
1862
|
const ver = JSON.parse(readFileSync(pkg, "utf-8")).version;
|
|
1617
1863
|
if (ver)
|
|
1618
|
-
return
|
|
1864
|
+
return `jishushell-openclaw:${ver}`;
|
|
1619
1865
|
}
|
|
1620
1866
|
}
|
|
1621
1867
|
catch { }
|
|
@@ -1665,9 +1911,9 @@ CMD ["gateway", "run", "--port", "18789", "--allow-unconfigured"]
|
|
|
1665
1911
|
`;
|
|
1666
1912
|
// Use a temp dir as build context — no files to COPY means no large transfer.
|
|
1667
1913
|
const buildDir = join(tmpdir(), `jishushell-base-build-${Date.now()}`);
|
|
1668
|
-
|
|
1914
|
+
ensureDirHost(buildDir);
|
|
1669
1915
|
const dockerfilePath = join(buildDir, "Dockerfile");
|
|
1670
|
-
|
|
1916
|
+
writeConfigFile(dockerfilePath, dockerfile);
|
|
1671
1917
|
emitTask(task, { type: "progress", message: `构建基础镜像 ${targetTag}(无需拷贝二进制,速度极快)...`, progress: 10 });
|
|
1672
1918
|
let result;
|
|
1673
1919
|
try {
|
|
@@ -1694,7 +1940,7 @@ CMD ["gateway", "run", "--port", "18789", "--allow-unconfigured"]
|
|
|
1694
1940
|
task.status = "error";
|
|
1695
1941
|
return { ok: false, message: "Docker image build failed", error: result.output, taskId: task.id };
|
|
1696
1942
|
}
|
|
1697
|
-
const localTag =
|
|
1943
|
+
const localTag = "jishushell-openclaw:local";
|
|
1698
1944
|
if (targetTag !== localTag) {
|
|
1699
1945
|
try {
|
|
1700
1946
|
execFileSync(invocation.cmd, [...invocation.argsPrefix, "tag", targetTag, localTag], { timeout: 10000, stdio: "ignore" });
|
|
@@ -1787,7 +2033,7 @@ CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
|
|
|
1787
2033
|
`;
|
|
1788
2034
|
// Write Dockerfile into the npm package directory (build context)
|
|
1789
2035
|
const dockerfilePath = join(OPENCLAW_PKG_DIR, "Dockerfile");
|
|
1790
|
-
|
|
2036
|
+
writeConfigFile(dockerfilePath, dockerfile);
|
|
1791
2037
|
let buildResult;
|
|
1792
2038
|
try {
|
|
1793
2039
|
buildResult = await spawnWithTask(task, invocation.cmd, [...invocation.argsPrefix, "build", "--network=host", "-t", targetTag, OPENCLAW_PKG_DIR], { timeout: 1800000, progressParser: dockerBuildProgressParser });
|
|
@@ -1818,24 +2064,212 @@ CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
|
|
|
1818
2064
|
return { ok: false, message: "Docker image build failed", error: e.message, taskId: task.id };
|
|
1819
2065
|
}
|
|
1820
2066
|
}
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
2067
|
+
// ── Pull or build OpenClaw Docker image ───────────────────────────
|
|
2068
|
+
/** Matches a semver-ish tag suffix, e.g. "...:2026.4.9" or "...:v1.2.3-beta". */
|
|
2069
|
+
const PINNED_IMAGE_TAG_RE = /:[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$/;
|
|
2070
|
+
/**
|
|
2071
|
+
* Query the npm registry for the current OpenClaw version. Used to bust the
|
|
2072
|
+
* Docker layer cache for `RUN npm install openclaw@${ver}` during local build.
|
|
2073
|
+
* Returns "latest" when npm is unreachable so the build can still proceed.
|
|
2074
|
+
*/
|
|
2075
|
+
function resolveOpenclawNpmVersion() {
|
|
2076
|
+
try {
|
|
2077
|
+
const out = execFileSync("npm", ["view", "openclaw", "version"], {
|
|
2078
|
+
timeout: 15000,
|
|
2079
|
+
encoding: "utf-8",
|
|
2080
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2081
|
+
}).trim();
|
|
2082
|
+
if (/^\d+\.\d+\.\d+/.test(out))
|
|
2083
|
+
return out;
|
|
2084
|
+
}
|
|
2085
|
+
catch { /* npm not reachable */ }
|
|
2086
|
+
return "latest";
|
|
1824
2087
|
}
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2088
|
+
/**
|
|
2089
|
+
* Read the OpenClaw version actually bundled at /app/ inside a Docker image,
|
|
2090
|
+
* bypassing `openclaw-entry.sh`'s `.npm-global/` override. This is the
|
|
2091
|
+
* authoritative source of truth — the image's OCI label can be wrong
|
|
2092
|
+
* (CI bugs, layer cache reuse), but `/app/node_modules/openclaw/package.json`
|
|
2093
|
+
* is the exact content that ran through `npm install`.
|
|
2094
|
+
*
|
|
2095
|
+
* Spawns a throw-away container with `--entrypoint node` so Node prints the
|
|
2096
|
+
* version directly. Returns "" when docker is unavailable or the path is
|
|
2097
|
+
* missing (e.g. a non-openclaw image).
|
|
2098
|
+
*/
|
|
2099
|
+
function readBundledOpenclawVersion(invocation, image) {
|
|
2100
|
+
try {
|
|
2101
|
+
const out = execFileSync(invocation.cmd, [
|
|
2102
|
+
...invocation.argsPrefix,
|
|
2103
|
+
"run", "--rm",
|
|
2104
|
+
"--entrypoint", "node",
|
|
2105
|
+
image,
|
|
2106
|
+
"-p",
|
|
2107
|
+
"require('/app/node_modules/openclaw/package.json').version",
|
|
2108
|
+
], { timeout: 20000, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2109
|
+
if (/^\d+\.\d+\.\d+/.test(out))
|
|
2110
|
+
return out;
|
|
2111
|
+
}
|
|
2112
|
+
catch { /* docker unavailable, image missing, or path not present */ }
|
|
2113
|
+
return "";
|
|
2114
|
+
}
|
|
2115
|
+
/**
|
|
2116
|
+
* After a successful pull or build, capture the image's real OpenClaw version
|
|
2117
|
+
* and return a pinned tag of the form `ghcr.io/.../openclaw-runtime:<version>`.
|
|
2118
|
+
* The pinned tag is added as a local alias via `docker tag` so subsequent
|
|
2119
|
+
* Nomad allocations see an immutable reference and never re-pull on restart.
|
|
2120
|
+
*
|
|
2121
|
+
* Version discovery order:
|
|
2122
|
+
* 1. `explicitVersion` when the caller already knows it (e.g. the local build
|
|
2123
|
+
* path, which queries npm for the version and passes it as `--build-arg`).
|
|
2124
|
+
* 2. Bundled `/app/node_modules/openclaw/package.json` inside the image
|
|
2125
|
+
* (authoritative — bypasses both the `.npm-global/` override layer and a
|
|
2126
|
+
* potentially stale OCI label).
|
|
2127
|
+
*
|
|
2128
|
+
* Returns the original tag unchanged when:
|
|
2129
|
+
* - the target is already a pinned version tag
|
|
2130
|
+
* - no version can be discovered
|
|
2131
|
+
* - docker tag fails for any reason
|
|
2132
|
+
*/
|
|
2133
|
+
function capturePinnedImageTag(invocation, targetTag, explicitVersion) {
|
|
2134
|
+
// Already pinned? Nothing to do.
|
|
2135
|
+
if (PINNED_IMAGE_TAG_RE.test(targetTag))
|
|
2136
|
+
return targetTag;
|
|
2137
|
+
let version = explicitVersion && /^\d+\.\d+\.\d+/.test(explicitVersion) ? explicitVersion : "";
|
|
2138
|
+
if (!version) {
|
|
2139
|
+
version = readBundledOpenclawVersion(invocation, targetTag);
|
|
2140
|
+
}
|
|
2141
|
+
if (!version || !/^\d+\.\d+\.\d+/.test(version))
|
|
2142
|
+
return targetTag;
|
|
2143
|
+
// Build the pinned tag by replacing the mutable tag portion.
|
|
2144
|
+
// "ghcr.io/foo/bar:latest" → "ghcr.io/foo/bar:2026.4.9"
|
|
2145
|
+
// "ghcr.io/foo/bar" → "ghcr.io/foo/bar:2026.4.9"
|
|
2146
|
+
const colonIdx = targetTag.lastIndexOf(":");
|
|
2147
|
+
const slashIdx = targetTag.lastIndexOf("/");
|
|
2148
|
+
const hasTag = colonIdx > slashIdx;
|
|
2149
|
+
const repo = hasTag ? targetTag.slice(0, colonIdx) : targetTag;
|
|
2150
|
+
const pinnedTag = `${repo}:${version}`;
|
|
2151
|
+
if (pinnedTag === targetTag)
|
|
2152
|
+
return targetTag;
|
|
2153
|
+
try {
|
|
2154
|
+
execFileSync(invocation.cmd, [...invocation.argsPrefix, "tag", targetTag, pinnedTag], { timeout: 10000, stdio: "ignore" });
|
|
2155
|
+
}
|
|
2156
|
+
catch {
|
|
2157
|
+
// Could not create the local alias — fall back to original tag.
|
|
2158
|
+
return targetTag;
|
|
2159
|
+
}
|
|
2160
|
+
// Drop the mutable original alias (`:latest` / `:slim`) now that the pinned
|
|
2161
|
+
// tag is in place. Removing a tag is cheap and leaves the underlying image
|
|
2162
|
+
// alive because the new pinned reference still points to it. Best-effort:
|
|
2163
|
+
// silent when the tag is already gone or in use.
|
|
2164
|
+
if (/:(latest|slim)$/.test(targetTag)) {
|
|
2165
|
+
try {
|
|
2166
|
+
execFileSync(invocation.cmd, [...invocation.argsPrefix, "rmi", targetTag], { timeout: 10000, stdio: "ignore" });
|
|
2167
|
+
}
|
|
2168
|
+
catch { /* best-effort cleanup */ }
|
|
2169
|
+
}
|
|
2170
|
+
return pinnedTag;
|
|
2171
|
+
}
|
|
2172
|
+
async function pullOrBuildOpenclawImageWithTask(task, tag) {
|
|
2173
|
+
const targetTag = tag || DEFAULT_OPENCLAW_DOCKER_IMAGE;
|
|
2174
|
+
try {
|
|
2175
|
+
const invocation = resolveDockerInvocation();
|
|
2176
|
+
// Fast check: if image already exists locally, skip
|
|
2177
|
+
try {
|
|
2178
|
+
execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "inspect", targetTag], {
|
|
2179
|
+
timeout: 10000,
|
|
2180
|
+
stdio: "ignore",
|
|
2181
|
+
});
|
|
2182
|
+
const pinned = capturePinnedImageTag(invocation, targetTag);
|
|
2183
|
+
setOpenclawDockerImage(pinned);
|
|
2184
|
+
emitTask(task, { type: "done", message: `Docker 镜像已存在: ${pinned}`, progress: 100 });
|
|
2185
|
+
task.status = "done";
|
|
2186
|
+
return { ok: true, message: `Docker image ${pinned} already exists`, taskId: task.id };
|
|
2187
|
+
}
|
|
2188
|
+
catch { /* image not found, proceed */ }
|
|
2189
|
+
// ── Step 1: Try docker pull from registry ─────────────────────
|
|
2190
|
+
emitTask(task, { type: "progress", message: `正在拉取镜像: ${targetTag} ...`, progress: 10 });
|
|
2191
|
+
const pullResult = await spawnWithTask(task, invocation.cmd, [...invocation.argsPrefix, "pull", targetTag], { timeout: 600000 });
|
|
2192
|
+
if (pullResult.ok) {
|
|
2193
|
+
const pinned = capturePinnedImageTag(invocation, targetTag);
|
|
2194
|
+
setOpenclawDockerImage(pinned);
|
|
2195
|
+
emitTask(task, { type: "done", message: `镜像拉取成功: ${pinned}`, progress: 100 });
|
|
2196
|
+
task.status = "done";
|
|
2197
|
+
return { ok: true, message: `Docker image ${pinned} pulled`, taskId: task.id };
|
|
2198
|
+
}
|
|
2199
|
+
// ── Step 2: Fallback to local build ───────────────────────────
|
|
2200
|
+
console.log(`[setup] docker pull failed for ${targetTag}, falling back to local build...`);
|
|
2201
|
+
emitTask(task, { type: "progress", message: `拉取失败,正在本地构建镜像: ${targetTag} ...`, progress: 20 });
|
|
2202
|
+
const projectRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
2203
|
+
const dockerfilePath = join(projectRoot, "Dockerfile.openclaw-slim");
|
|
2204
|
+
if (!existsSync(dockerfilePath)) {
|
|
2205
|
+
emitTask(task, { type: "error", message: "Dockerfile.openclaw-slim not found, cannot fallback to local build" });
|
|
2206
|
+
task.status = "error";
|
|
2207
|
+
return { ok: false, message: "Docker pull failed and Dockerfile.openclaw-slim not found", taskId: task.id };
|
|
2208
|
+
}
|
|
2209
|
+
// Resolve the OpenClaw version from npm so the build-arg busts the Docker
|
|
2210
|
+
// layer cache for `RUN npm install openclaw@${ver}`. The Dockerfile's
|
|
2211
|
+
// ARG OPENCLAW_VERSION=latest default would otherwise cause the layer to
|
|
2212
|
+
// be silently reused across releases.
|
|
2213
|
+
const openclawVersion = resolveOpenclawNpmVersion();
|
|
2214
|
+
console.log(`[setup] building openclaw image with OPENCLAW_VERSION=${openclawVersion}`);
|
|
2215
|
+
const buildResult = await spawnWithTask(task, invocation.cmd, [
|
|
2216
|
+
...invocation.argsPrefix,
|
|
2217
|
+
"build",
|
|
2218
|
+
"--network=host",
|
|
2219
|
+
"--build-arg", `OPENCLAW_VERSION=${openclawVersion}`,
|
|
2220
|
+
"-f", dockerfilePath,
|
|
2221
|
+
"-t", targetTag,
|
|
2222
|
+
projectRoot,
|
|
2223
|
+
], { timeout: 1800000, progressParser: dockerBuildProgressParser });
|
|
2224
|
+
if (!buildResult.ok) {
|
|
2225
|
+
try {
|
|
2226
|
+
execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "prune", "-f"], { timeout: 15000, stdio: "ignore" });
|
|
2227
|
+
}
|
|
2228
|
+
catch { }
|
|
2229
|
+
emitTask(task, { type: "error", message: "Docker 镜像构建失败" });
|
|
2230
|
+
task.status = "error";
|
|
2231
|
+
return { ok: false, message: "Docker image build failed", error: buildResult.output, taskId: task.id };
|
|
2232
|
+
}
|
|
2233
|
+
// Local builds don't get labels from the GitHub Action's `labels:` field,
|
|
2234
|
+
// so pass the npm version we already know to let capturePinnedImageTag
|
|
2235
|
+
// mint the pinned tag without relying on docker inspect.
|
|
2236
|
+
const pinned = capturePinnedImageTag(invocation, targetTag, openclawVersion);
|
|
2237
|
+
setOpenclawDockerImage(pinned);
|
|
2238
|
+
emitTask(task, { type: "done", message: `OpenClaw 镜像就绪 (本地构建): ${pinned}`, progress: 100 });
|
|
2239
|
+
task.status = "done";
|
|
2240
|
+
return { ok: true, message: `Docker image ${pinned} built locally`, taskId: task.id };
|
|
2241
|
+
}
|
|
2242
|
+
catch (e) {
|
|
2243
|
+
emitTask(task, { type: "error", message: `镜像获取失败: ${e.message}` });
|
|
2244
|
+
task.status = "error";
|
|
2245
|
+
return { ok: false, message: "Docker image pull/build failed", error: e.message, taskId: task.id };
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
export async function buildSlimOpenclawImage(tag) {
|
|
2249
|
+
const task = createTask("openclaw-docker-pull");
|
|
2250
|
+
return pullOrBuildOpenclawImageWithTask(task, tag);
|
|
2251
|
+
}
|
|
2252
|
+
export function startBuildSlimOpenclawImage(tag) {
|
|
2253
|
+
const task = createTask("openclaw-docker-pull");
|
|
2254
|
+
void pullOrBuildOpenclawImageWithTask(task, tag).catch((err) => {
|
|
2255
|
+
emitTask(task, { type: "error", message: `镜像获取失败: ${err?.message || err}` });
|
|
1829
2256
|
task.status = "error";
|
|
1830
2257
|
});
|
|
1831
|
-
return { ok: true, message: "Docker image
|
|
2258
|
+
return { ok: true, message: "Docker image pull started", taskId: task.id };
|
|
2259
|
+
}
|
|
2260
|
+
/** @deprecated Use buildSlimOpenclawImage instead */
|
|
2261
|
+
export async function buildCustomOpenclawImage(tag) {
|
|
2262
|
+
return buildSlimOpenclawImage(tag);
|
|
2263
|
+
}
|
|
2264
|
+
/** @deprecated Use startBuildSlimOpenclawImage instead */
|
|
2265
|
+
export function startBuildCustomOpenclawImage(tag) {
|
|
2266
|
+
return startBuildSlimOpenclawImage(tag);
|
|
1832
2267
|
}
|
|
1833
2268
|
export async function runFullSetup(options = {}) {
|
|
1834
2269
|
const steps = [];
|
|
1835
2270
|
let allOk = true;
|
|
1836
2271
|
const defaults = {
|
|
1837
2272
|
installNomad: true,
|
|
1838
|
-
installOpenclaw: true,
|
|
1839
2273
|
buildDockerImage: true,
|
|
1840
2274
|
...options,
|
|
1841
2275
|
};
|
|
@@ -1872,14 +2306,6 @@ export async function runFullSetup(options = {}) {
|
|
|
1872
2306
|
}
|
|
1873
2307
|
}
|
|
1874
2308
|
}
|
|
1875
|
-
if (defaults.installOpenclaw) {
|
|
1876
|
-
steps.push({ step: "openclaw", status: "running", message: "Installing OpenClaw..." });
|
|
1877
|
-
const result = await installOpenclaw();
|
|
1878
|
-
steps[steps.length - 1].status = result.ok ? "done" : "error";
|
|
1879
|
-
steps[steps.length - 1].message = result.message;
|
|
1880
|
-
if (!result.ok)
|
|
1881
|
-
allOk = false;
|
|
1882
|
-
}
|
|
1883
2309
|
// Prepare Docker image: pull official image or build slim base (legacy).
|
|
1884
2310
|
if (defaults.buildDockerImage) {
|
|
1885
2311
|
// Restart Nomad so it re-detects Docker driver after Docker was installed
|
|
@@ -1893,9 +2319,7 @@ export async function runFullSetup(options = {}) {
|
|
|
1893
2319
|
}
|
|
1894
2320
|
catch { }
|
|
1895
2321
|
steps.push({ step: "docker-image", status: "running", message: "Building OpenClaw Docker image..." });
|
|
1896
|
-
const imgResult =
|
|
1897
|
-
? await buildCustomOpenclawImage()
|
|
1898
|
-
: await buildOpenclawDockerImage();
|
|
2322
|
+
const imgResult = await buildSlimOpenclawImage();
|
|
1899
2323
|
steps[steps.length - 1].status = imgResult.ok ? "done" : "error";
|
|
1900
2324
|
steps[steps.length - 1].message = imgResult.message;
|
|
1901
2325
|
if (!imgResult.ok)
|