jishushell 0.4.10 → 0.4.24-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile.hermes-slim +193 -0
- package/INSTALL-NOTICE +10 -12
- package/apps/hermes-container.yaml +35 -0
- package/apps/ollama-binary.yaml +164 -0
- package/apps/ollama-cpu-container.yaml +37 -0
- package/apps/ollama-with-hollama-binary.yaml +159 -0
- package/apps/openclaw-binary.yaml +69 -0
- package/apps/openclaw-container.yaml +37 -0
- package/apps/openclaw-with-ollama-container.yaml +42 -0
- package/apps/openclaw-with-searxng-container.yaml +136 -0
- package/apps/openwebui-container.yaml +53 -0
- package/apps/playwright-container.yaml +120 -0
- package/apps/searxng-container.yaml +115 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +15 -14
- package/dist/auth.js.map +1 -1
- package/dist/cli/app.d.ts +4 -0
- package/dist/cli/app.js +874 -0
- package/dist/cli/app.js.map +1 -0
- package/dist/cli/backup.d.ts +3 -0
- package/dist/cli/backup.js +434 -0
- package/dist/cli/backup.js.map +1 -0
- package/dist/{doctor.d.ts → cli/doctor.d.ts} +7 -1
- package/dist/{doctor.js → cli/doctor.js} +377 -22
- 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 +4 -0
- package/dist/cli/job.js +198 -0
- package/dist/cli/job.js.map +1 -0
- package/dist/cli/llm.d.ts +25 -0
- package/dist/cli/llm.js +599 -0
- package/dist/cli/llm.js.map +1 -0
- package/dist/cli/managed-list.d.ts +30 -0
- package/dist/cli/managed-list.js +129 -0
- package/dist/cli/managed-list.js.map +1 -0
- package/dist/cli/panel.d.ts +26 -0
- package/dist/cli/panel.js +804 -0
- package/dist/cli/panel.js.map +1 -0
- package/dist/cli/version.d.ts +1 -0
- package/dist/cli/version.js +12 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli.js +48 -776
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +69 -0
- package/dist/config.js +268 -7
- package/dist/config.js.map +1 -1
- package/dist/control.d.ts +17 -41
- package/dist/control.js +61 -1323
- package/dist/control.js.map +1 -1
- package/dist/install.d.ts +16 -0
- package/dist/install.js +75 -26
- package/dist/install.js.map +1 -1
- package/dist/routes/agent-apps.d.ts +15 -0
- package/dist/routes/agent-apps.js +78 -0
- package/dist/routes/agent-apps.js.map +1 -0
- package/dist/routes/apps.d.ts +3 -0
- package/dist/routes/apps.js +278 -0
- package/dist/routes/apps.js.map +1 -0
- package/dist/routes/backup.js +3 -3
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/instances.d.ts +6 -0
- package/dist/routes/instances.js +863 -874
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/llm.d.ts +15 -0
- package/dist/routes/llm.js +247 -0
- package/dist/routes/llm.js.map +1 -0
- package/dist/routes/runtime.d.ts +15 -0
- package/dist/routes/runtime.js +69 -0
- package/dist/routes/runtime.js.map +1 -0
- package/dist/routes/setup.js +131 -9
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +56 -9
- package/dist/routes/system.js.map +1 -1
- package/dist/server.js +107 -7
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.d.ts +30 -0
- package/dist/services/agent-apps/catalog.js +60 -0
- package/dist/services/agent-apps/catalog.js.map +1 -0
- package/dist/services/agent-apps/index.d.ts +36 -0
- package/dist/services/agent-apps/index.js +171 -0
- package/dist/services/agent-apps/index.js.map +1 -0
- package/dist/services/agent-apps/installers/adapter-probes.d.ts +49 -0
- package/dist/services/agent-apps/installers/adapter-probes.js +223 -0
- package/dist/services/agent-apps/installers/adapter-probes.js.map +1 -0
- package/dist/services/agent-apps/installers/adapter.d.ts +30 -0
- package/dist/services/agent-apps/installers/adapter.js +171 -0
- package/dist/services/agent-apps/installers/adapter.js.map +1 -0
- package/dist/services/agent-apps/installers/registry-probe.d.ts +38 -0
- package/dist/services/agent-apps/installers/registry-probe.js +183 -0
- package/dist/services/agent-apps/installers/registry-probe.js.map +1 -0
- package/dist/services/agent-apps/installers/shell-script.d.ts +47 -0
- package/dist/services/agent-apps/installers/shell-script.js +471 -0
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -0
- package/dist/services/agent-apps/types.d.ts +125 -0
- package/dist/services/agent-apps/types.js +17 -0
- package/dist/services/agent-apps/types.js.map +1 -0
- package/dist/services/app/app-compiler.d.ts +15 -0
- package/dist/services/app/app-compiler.js +172 -0
- package/dist/services/app/app-compiler.js.map +1 -0
- package/dist/services/app/app-manager.d.ts +142 -0
- package/dist/services/app/app-manager.js +2148 -0
- package/dist/services/app/app-manager.js.map +1 -0
- package/dist/services/app/custom-manager.d.ts +27 -0
- package/dist/services/app/custom-manager.js +285 -0
- package/dist/services/app/custom-manager.js.map +1 -0
- package/dist/services/app/hermes-agent-manager.d.ts +20 -0
- package/dist/services/app/hermes-agent-manager.js +289 -0
- package/dist/services/app/hermes-agent-manager.js.map +1 -0
- package/dist/services/app/id-normalizer.d.ts +27 -0
- package/dist/services/app/id-normalizer.js +77 -0
- package/dist/services/app/id-normalizer.js.map +1 -0
- package/dist/services/app/ollama-manager.d.ts +18 -0
- package/dist/services/app/ollama-manager.js +207 -0
- package/dist/services/app/ollama-manager.js.map +1 -0
- package/dist/services/app/openclaw-manager.d.ts +63 -0
- package/dist/services/app/openclaw-manager.js +1178 -0
- package/dist/services/app/openclaw-manager.js.map +1 -0
- package/dist/services/app/paths.d.ts +47 -0
- package/dist/services/app/paths.js +68 -0
- package/dist/services/app/paths.js.map +1 -0
- package/dist/services/app/registry.d.ts +17 -0
- package/dist/services/app/registry.js +31 -0
- package/dist/services/app/registry.js.map +1 -0
- package/dist/services/app/remote-spec.d.ts +14 -0
- package/dist/services/app/remote-spec.js +58 -0
- package/dist/services/app/remote-spec.js.map +1 -0
- package/dist/services/app/terminal-session-manager.d.ts +27 -0
- package/dist/services/app/terminal-session-manager.js +157 -0
- package/dist/services/app/terminal-session-manager.js.map +1 -0
- package/dist/services/app/types.d.ts +72 -0
- package/dist/services/app/types.js +16 -0
- package/dist/services/app/types.js.map +1 -0
- package/dist/services/backup-manager.js +60 -22
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/instance-manager.d.ts +125 -34
- package/dist/services/instance-manager.js +679 -1043
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/adapters.js +5 -1
- package/dist/services/llm-proxy/adapters.js.map +1 -1
- package/dist/services/llm-proxy/circuit-breaker.js +10 -2
- package/dist/services/llm-proxy/circuit-breaker.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +43 -0
- package/dist/services/llm-proxy/index.js +120 -5
- 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.d.ts +260 -3
- package/dist/services/nomad-manager.js +2921 -341
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/panel-manager.d.ts +50 -0
- package/dist/services/panel-manager.js +443 -0
- package/dist/services/panel-manager.js.map +1 -0
- package/dist/services/plugin-installer.js +28 -2
- package/dist/services/plugin-installer.js.map +1 -1
- package/dist/services/process-manager.js +42 -7
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/custom.d.ts +20 -0
- package/dist/services/runtime/adapters/custom.js +90 -0
- package/dist/services/runtime/adapters/custom.js.map +1 -0
- package/dist/services/runtime/adapters/hermes.d.ts +174 -0
- package/dist/services/runtime/adapters/hermes.js +1316 -0
- package/dist/services/runtime/adapters/hermes.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +17 -0
- package/dist/services/runtime/adapters/openclaw-routes.js +946 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -0
- package/dist/services/runtime/adapters/openclaw.d.ts +188 -0
- package/dist/services/runtime/adapters/openclaw.js +2195 -0
- package/dist/services/runtime/adapters/openclaw.js.map +1 -0
- package/dist/services/runtime/errors.d.ts +28 -0
- package/dist/services/runtime/errors.js +31 -0
- package/dist/services/runtime/errors.js.map +1 -0
- package/dist/services/runtime/index.d.ts +34 -0
- package/dist/services/runtime/index.js +51 -0
- package/dist/services/runtime/index.js.map +1 -0
- package/dist/services/runtime/instance.d.ts +24 -0
- package/dist/services/runtime/instance.js +143 -0
- package/dist/services/runtime/instance.js.map +1 -0
- package/dist/services/runtime/migrations.d.ts +15 -0
- package/dist/services/runtime/migrations.js +25 -0
- package/dist/services/runtime/migrations.js.map +1 -0
- package/dist/services/runtime/registry.d.ts +13 -0
- package/dist/services/runtime/registry.js +32 -0
- package/dist/services/runtime/registry.js.map +1 -0
- package/dist/services/runtime/types.d.ts +545 -0
- package/dist/services/runtime/types.js +14 -0
- package/dist/services/runtime/types.js.map +1 -0
- package/dist/services/setup-manager.d.ts +70 -29
- package/dist/services/setup-manager.js +591 -625
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/task-registry.d.ts +44 -0
- package/dist/services/task-registry.js +74 -0
- package/dist/services/task-registry.js.map +1 -0
- package/dist/services/telemetry/heartbeat.d.ts +6 -6
- package/dist/services/telemetry/heartbeat.js +29 -30
- 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 +224 -0
- package/dist/utils/docker-host.d.ts +15 -0
- package/dist/utils/docker-host.js +64 -0
- package/dist/utils/docker-host.js.map +1 -0
- package/install/jishu-install.sh +303 -38
- package/install/post-install.sh +64 -5
- package/package.json +19 -5
- package/public/assets/Dashboard-rh9qpYRR.js +1 -0
- package/public/assets/HermesChatPanel-D6JI6lLY.js +1 -0
- package/public/assets/HermesConfigForm-DcbSemaj.js +4 -0
- package/public/assets/InitPassword-CFTKsED4.js +1 -0
- package/public/assets/InstanceDetail-BhNIKA6Z.js +91 -0
- package/public/assets/{Login-CUoEZOWR.js → Login-KB9qrtM0.js} +1 -1
- package/public/assets/NewInstance-CxkO8Hlq.js +1 -0
- package/public/assets/Settings-BVWJvOkU.js +1 -0
- package/public/assets/Setup-X-lzuaUT.js +1 -0
- package/public/assets/WeixinLoginPanel-gca0QTic.js +9 -0
- package/public/assets/index-C8B0cFJM.js +19 -0
- package/public/assets/index-CPhVFEsx.css +1 -0
- package/public/assets/input-paste-CrNVAyOy.js +1 -0
- package/public/assets/{providers-lBSOjUWy.js → providers-V-vwrExZ.js} +1 -1
- package/public/assets/registry-fVUSujib.js +2 -0
- package/public/assets/{usePolling-CK0DfI4h.js → usePolling-Do5Erqm_.js} +1 -1
- package/public/assets/vendor-i18n-ucpM0OR0.js +9 -0
- package/public/assets/{vendor-react-B1-3Yrt-.js → vendor-react-Bk1hRGiY.js} +1 -1
- package/public/favicon.png +0 -0
- package/public/index.html +9 -4
- package/public/logos/hermes.png +0 -0
- package/public/logos/ollama.png +0 -0
- package/public/logos/openclaw.svg +60 -0
- package/scripts/build-hermes-image.sh +21 -0
- package/scripts/build-local.sh +54 -0
- package/scripts/check-adapter-isolation.ts +293 -0
- package/scripts/fixtures/instances/hermes-sample/instance.json +37 -0
- package/scripts/fixtures/instances/legacy-openclaw-sample/instance.json +7 -0
- package/scripts/smoke/hermes-bootstrap.sh +195 -0
- package/templates/hermes-entrypoint.sh +154 -0
- package/dist/doctor.js.map +0 -1
- package/install/jishu-install-china.sh +0 -3092
- package/public/assets/Dashboard-DhsrzJ4F.js +0 -1
- package/public/assets/InitPassword-BjubiVdd.js +0 -1
- package/public/assets/InstanceDetail-DMcywsof.js +0 -17
- package/public/assets/NewInstance-Bk0G4EiJ.js +0 -1
- package/public/assets/Settings-D5tHL_h5.js +0 -1
- package/public/assets/Setup-4t6E3Rut.js +0 -1
- package/public/assets/index-BJ47MWpF.css +0 -1
- package/public/assets/index-DbX85irc.js +0 -16
- package/public/assets/vendor-i18n-CfW0RvgE.js +0 -9
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import { execFileSync, execSync, spawn as nodeSpawn } from "child_process";
|
|
2
|
-
import { chmodSync, existsSync, readFileSync, symlinkSync, unlinkSync } from "fs";
|
|
3
|
-
import { userInfo } from "node:os";
|
|
2
|
+
import { chmodSync, copyFileSync, existsSync, mkdtempSync, readFileSync, renameSync, rmSync, symlinkSync, unlinkSync } from "fs";
|
|
3
|
+
import { userInfo, platform as osPlatform } 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, DEFAULT_OPENCLAW_DOCKER_IMAGE } from "../config.js";
|
|
9
|
+
import { JISHUSHELL_HOME, getPanelConfig, savePanelConfig, setOpenclawDockerImage, getOpenclawDockerImage, DEFAULT_OPENCLAW_DOCKER_IMAGE, getRuntimeCatalogEntry, OPENCLAW_MODULES, OPENCLAW_BIN_DIR, } from "../config.js";
|
|
10
10
|
import { ensureDirContainer, ensureDirHost, writeConfigFile, writeSecretFile, writeExecutableFile, writeSystemTmpFile } from "../utils/fs.js";
|
|
11
|
+
import { buildDockerClientEnv, managedColimaSocketPath, resolveDockerHost } from "../utils/docker-host.js";
|
|
12
|
+
import { getAdapter, listRegisteredAdapters } from "./runtime/index.js";
|
|
13
|
+
// Internal usage of task primitives — re-exports further down preserve the
|
|
14
|
+
// public import surface used by runtime/adapters/*, routes, and CLI.
|
|
15
|
+
import { createTask, emitTask, getRunningTasks } from "./task-registry.js";
|
|
11
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
17
|
const __dirname = dirname(__filename);
|
|
13
18
|
// ── Paths ──────────────────────────────────────────────────────────
|
|
14
19
|
const BIN_DIR = join(JISHUSHELL_HOME, "bin");
|
|
15
|
-
const PACKAGES_DIR = join(JISHUSHELL_HOME, "packages");
|
|
16
|
-
const OPENCLAW_PKG_DIR = join(PACKAGES_DIR, "openclaw");
|
|
17
|
-
/** npm global-prefix layout: lib/node_modules/<pkg>, bin/<cmd> */
|
|
18
|
-
const OPENCLAW_MODULES = join(OPENCLAW_PKG_DIR, "lib", "node_modules");
|
|
19
|
-
const OPENCLAW_BIN_DIR = join(OPENCLAW_PKG_DIR, "bin");
|
|
20
20
|
const NOMAD_BIN = join(BIN_DIR, "nomad");
|
|
21
21
|
const NOMAD_CONFIG_DIR = join(JISHUSHELL_HOME, "nomad");
|
|
22
22
|
const NOMAD_DATA_DIR = join(JISHUSHELL_HOME, "nomad", "data");
|
|
23
23
|
const NOMAD_ALLOC_DIR = join(JISHUSHELL_HOME, "nomad", "data", "alloc");
|
|
24
24
|
const COLIMA_DIR = join(JISHUSHELL_HOME, "colima");
|
|
25
25
|
const COLIMA_PROFILE = "jishushell";
|
|
26
|
-
const COLIMA_SOCKET =
|
|
27
|
-
const NOMAD_VERSION = "1.
|
|
26
|
+
const COLIMA_SOCKET = managedColimaSocketPath(JISHUSHELL_HOME, COLIMA_PROFILE);
|
|
27
|
+
const NOMAD_VERSION = "1.6.5";
|
|
28
28
|
let _serverPort = 8090;
|
|
29
29
|
export function setServerPort(port) { _serverPort = port; }
|
|
30
30
|
// ── Resolve non-root service user (board-agnostic) ─────────────────
|
|
@@ -76,72 +76,13 @@ function resolveServiceUser() {
|
|
|
76
76
|
catch { /* /etc/passwd unreadable */ }
|
|
77
77
|
throw new Error("Cannot determine service user. Run with a non-root user or set SUDO_UID.");
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
tasks.delete(id);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}, 60000).unref();
|
|
90
|
-
function createTask(name) {
|
|
91
|
-
const id = `${name}-${Date.now()}`;
|
|
92
|
-
const task = { id, name, status: "running", events: [], listeners: new Set() };
|
|
93
|
-
tasks.set(id, task);
|
|
94
|
-
return task;
|
|
95
|
-
}
|
|
96
|
-
const MAX_TASK_EVENTS = 500;
|
|
97
|
-
function emitTask(task, event) {
|
|
98
|
-
task.events.push(event);
|
|
99
|
-
// Cap events to prevent unbounded memory growth on long-running tasks
|
|
100
|
-
// (e.g., Docker pull/build can produce thousands of progress lines)
|
|
101
|
-
if (task.events.length > MAX_TASK_EVENTS) {
|
|
102
|
-
task.events.splice(0, task.events.length - MAX_TASK_EVENTS);
|
|
103
|
-
}
|
|
104
|
-
for (const listener of task.listeners) {
|
|
105
|
-
listener(event);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
export function getTask(id) {
|
|
109
|
-
return tasks.get(id);
|
|
110
|
-
}
|
|
111
|
-
export function getTaskSnapshot(id) {
|
|
112
|
-
const task = tasks.get(id);
|
|
113
|
-
if (!task)
|
|
114
|
-
return undefined;
|
|
115
|
-
return {
|
|
116
|
-
id: task.id,
|
|
117
|
-
name: task.name,
|
|
118
|
-
status: task.status,
|
|
119
|
-
events: [...task.events],
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
/** Find running tasks, optionally filtered by name prefix */
|
|
123
|
-
export function getRunningTasks(namePrefix) {
|
|
124
|
-
const result = [];
|
|
125
|
-
for (const [id, task] of tasks) {
|
|
126
|
-
if (task.status !== "running")
|
|
127
|
-
continue;
|
|
128
|
-
if (namePrefix && !task.name.startsWith(namePrefix))
|
|
129
|
-
continue;
|
|
130
|
-
result.push({ id, name: task.name });
|
|
131
|
-
}
|
|
132
|
-
return result;
|
|
133
|
-
}
|
|
134
|
-
export function subscribeTask(id, listener) {
|
|
135
|
-
const task = tasks.get(id);
|
|
136
|
-
if (!task)
|
|
137
|
-
return null;
|
|
138
|
-
task.listeners.add(listener);
|
|
139
|
-
// Send existing events
|
|
140
|
-
for (const event of task.events) {
|
|
141
|
-
listener(event);
|
|
142
|
-
}
|
|
143
|
-
return () => task.listeners.delete(listener);
|
|
144
|
-
}
|
|
79
|
+
// ── Task tracker (for SSE progress) ────────────────────────────────
|
|
80
|
+
// Storage, event dispatch, and subscribe plumbing now live in
|
|
81
|
+
// task-registry.ts so non-setup callers (runtime-apps/*) can publish
|
|
82
|
+
// tasks without pulling in the rest of setup-manager's bootstrap
|
|
83
|
+
// surface. These re-exports preserve the original import surface used
|
|
84
|
+
// by runtime/adapters/*, routes, and CLI.
|
|
85
|
+
export { createTask, emitTask, getTask, getTaskSnapshot, getRunningTasks, subscribeTask, } from "./task-registry.js";
|
|
145
86
|
const ANSI_ESCAPE_RE = /\u001B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
146
87
|
function sanitizeTaskLine(line) {
|
|
147
88
|
return line
|
|
@@ -150,7 +91,7 @@ function sanitizeTaskLine(line) {
|
|
|
150
91
|
.trimEnd();
|
|
151
92
|
}
|
|
152
93
|
/** Run a shell command as a spawned process, streaming output to a task */
|
|
153
|
-
function spawnWithTask(task, cmd, args, options = {}) {
|
|
94
|
+
export function spawnWithTask(task, cmd, args, options = {}) {
|
|
154
95
|
return new Promise((resolve) => {
|
|
155
96
|
const env = { ...process.env, ...options.env };
|
|
156
97
|
const child = nodeSpawn(cmd, args, {
|
|
@@ -214,7 +155,7 @@ function spawnWithTask(task, cmd, args, options = {}) {
|
|
|
214
155
|
});
|
|
215
156
|
}
|
|
216
157
|
// ── Progress parsers ───────────────────────────────────────────────
|
|
217
|
-
function npmProgressParser(line) {
|
|
158
|
+
export function npmProgressParser(line) {
|
|
218
159
|
// npm shows "added X packages" at the end
|
|
219
160
|
if (line.includes("added") && line.includes("packages"))
|
|
220
161
|
return 100;
|
|
@@ -223,7 +164,7 @@ function npmProgressParser(line) {
|
|
|
223
164
|
return null; // just a log line
|
|
224
165
|
return null;
|
|
225
166
|
}
|
|
226
|
-
function dockerBuildProgressParser(line) {
|
|
167
|
+
export function dockerBuildProgressParser(line) {
|
|
227
168
|
// Docker build steps: "Step 1/6", "Step 2/6", etc.
|
|
228
169
|
const legacyMatch = line.match(/Step\s+(\d+)\/(\d+)/);
|
|
229
170
|
if (legacyMatch) {
|
|
@@ -248,7 +189,7 @@ function curlProgressParser(line) {
|
|
|
248
189
|
return null;
|
|
249
190
|
}
|
|
250
191
|
// ── Dir size tracker for npm installs ──────────────────────────────
|
|
251
|
-
function getDirSizeMB(dir) {
|
|
192
|
+
export function getDirSizeMB(dir) {
|
|
252
193
|
try {
|
|
253
194
|
const result = execFileSync("du", ["-sm", dir], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
254
195
|
return parseInt(result.split("\t")[0]) || 0;
|
|
@@ -284,21 +225,34 @@ function isPortListening(port) {
|
|
|
284
225
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
285
226
|
return false;
|
|
286
227
|
const p = String(port);
|
|
228
|
+
const matchesPort = (output) => new RegExp(`:${p}\\s`).test(output);
|
|
287
229
|
try {
|
|
288
230
|
if (process.platform === "darwin") {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
231
|
+
try {
|
|
232
|
+
const result = execFileSync("lsof", ["-iTCP:" + p, "-sTCP:LISTEN", "-t"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
233
|
+
if (result.trim().length > 0)
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
}
|
|
294
238
|
try {
|
|
295
239
|
const result = execFileSync("ss", ["-tlnp"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
296
|
-
|
|
240
|
+
if (matchesPort(result))
|
|
241
|
+
return true;
|
|
297
242
|
}
|
|
298
243
|
catch {
|
|
299
|
-
const result = execFileSync("netstat", ["-tlnp"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
300
|
-
return new RegExp(`:${p}\\s`).test(result);
|
|
301
244
|
}
|
|
245
|
+
const result = execFileSync("netstat", ["-tlnp"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
246
|
+
return matchesPort(result);
|
|
247
|
+
}
|
|
248
|
+
// Linux: prefer ss, fall back to netstat
|
|
249
|
+
try {
|
|
250
|
+
const result = execFileSync("ss", ["-tlnp"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
251
|
+
return matchesPort(result);
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
const result = execFileSync("netstat", ["-tlnp"], { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
255
|
+
return matchesPort(result);
|
|
302
256
|
}
|
|
303
257
|
}
|
|
304
258
|
catch {
|
|
@@ -360,9 +314,10 @@ export function ensureCgroupMemory() {
|
|
|
360
314
|
return false;
|
|
361
315
|
}
|
|
362
316
|
function canAccessDockerDaemon(timeout = 10000) {
|
|
363
|
-
const env =
|
|
364
|
-
|
|
365
|
-
:
|
|
317
|
+
const env = buildDockerClientEnv({
|
|
318
|
+
jishuHome: JISHUSHELL_HOME,
|
|
319
|
+
colimaProfile: COLIMA_PROFILE,
|
|
320
|
+
});
|
|
366
321
|
try {
|
|
367
322
|
execFileSync("docker", ["info"], { timeout, stdio: "ignore", env });
|
|
368
323
|
return true;
|
|
@@ -386,7 +341,7 @@ function getDockerVersionLine(timeout = 10000) {
|
|
|
386
341
|
catch { }
|
|
387
342
|
return "installed";
|
|
388
343
|
}
|
|
389
|
-
export function getSetupStatus() {
|
|
344
|
+
export async function getSetupStatus() {
|
|
390
345
|
// Fast path: if setup is already complete, do lightweight checks before returning cached result
|
|
391
346
|
const config = getPanelConfig();
|
|
392
347
|
if (config.service_manager) {
|
|
@@ -433,6 +388,7 @@ export function getSetupStatus() {
|
|
|
433
388
|
ready,
|
|
434
389
|
providerConfigured: !!config.default_provider,
|
|
435
390
|
hasSudo: true,
|
|
391
|
+
runtimes: await buildRuntimesStatus(),
|
|
436
392
|
};
|
|
437
393
|
}
|
|
438
394
|
}
|
|
@@ -512,7 +468,42 @@ export function getSetupStatus() {
|
|
|
512
468
|
needsReboot = true;
|
|
513
469
|
}
|
|
514
470
|
const runningTasks = getRunningTasks();
|
|
515
|
-
return { node: nodeStatus, docker: dockerStatus, nomad: nomadStatus, openclaw: openclawStatus, ready, providerConfigured, dockerImageReady, hasSudo: checkSudo(), needsReboot, runningTasks: runningTasks.length ? runningTasks : undefined };
|
|
471
|
+
return { node: nodeStatus, docker: dockerStatus, nomad: nomadStatus, openclaw: openclawStatus, ready, providerConfigured, dockerImageReady, hasSudo: checkSudo(), needsReboot, runningTasks: runningTasks.length ? runningTasks : undefined, runtimes: await buildRuntimesStatus() };
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Build the per-runtime install status block by iterating over every
|
|
475
|
+
* registered adapter. A new adapter that implements `getInstallStatus()`
|
|
476
|
+
* automatically appears here — no change required in this function.
|
|
477
|
+
*
|
|
478
|
+
* Each probe is bounded by a short timeout so a hung adapter (slow docker
|
|
479
|
+
* socket, DNS stall) cannot block the `/api/setup/status` endpoint. This
|
|
480
|
+
* mirrors the pattern used by `routes/runtime.ts:probeInstallStatus` so
|
|
481
|
+
* Setup wizard and `/api/runtime/catalog` see a consistent view.
|
|
482
|
+
*/
|
|
483
|
+
const BUILD_RUNTIMES_PROBE_TIMEOUT_MS = 2500;
|
|
484
|
+
async function buildRuntimesStatus() {
|
|
485
|
+
const adapters = listRegisteredAdapters().filter((a) => typeof a.getInstallStatus === "function");
|
|
486
|
+
const probes = adapters.map(async (adapter) => {
|
|
487
|
+
try {
|
|
488
|
+
const status = await Promise.race([
|
|
489
|
+
Promise.resolve(adapter.getInstallStatus()),
|
|
490
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("install-status probe timed out")), BUILD_RUNTIMES_PROBE_TIMEOUT_MS)),
|
|
491
|
+
]);
|
|
492
|
+
if (!status)
|
|
493
|
+
return null;
|
|
494
|
+
const sync = status;
|
|
495
|
+
return [adapter.agentType, { ...sync, required: !!adapter.required }];
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
const out = {};
|
|
502
|
+
for (const result of await Promise.all(probes)) {
|
|
503
|
+
if (result)
|
|
504
|
+
out[result[0]] = result[1];
|
|
505
|
+
}
|
|
506
|
+
return Object.keys(out).length ? out : undefined;
|
|
516
507
|
}
|
|
517
508
|
// ── Upgrade Node.js ────────────────────────────────────────────────
|
|
518
509
|
export async function upgradeNode(targetMajor = 22) {
|
|
@@ -769,6 +760,227 @@ function getNomadDownloadUrl() {
|
|
|
769
760
|
const os = process.platform === "linux" ? "linux" : "darwin";
|
|
770
761
|
return `https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_${os}_${arch}.zip`;
|
|
771
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* Signal nomad agents by exact process name (pgrep -x nomad) to avoid the
|
|
765
|
+
* classic pkill -f self-match bug: a command line like "pkill -f 'nomad agent'"
|
|
766
|
+
* literally contains the pattern and pkill kills itself before reaching the
|
|
767
|
+
* real nomad process. pgrep's own comm is "pgrep" (not "nomad") so -x nomad
|
|
768
|
+
* cannot self-match. Unprivileged kill is tried first; sudo -n as a fallback
|
|
769
|
+
* if the running nomad is owned by root (1.6.5 User=root unit).
|
|
770
|
+
*/
|
|
771
|
+
function killNomadByProcName() {
|
|
772
|
+
const collect = () => {
|
|
773
|
+
try {
|
|
774
|
+
const out = execSync("pgrep -x nomad 2>/dev/null || true", { encoding: "utf-8" }).trim();
|
|
775
|
+
return out.split("\n").filter(Boolean);
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
const sendSignal = (sig, pids) => {
|
|
782
|
+
if (pids.length === 0)
|
|
783
|
+
return;
|
|
784
|
+
try {
|
|
785
|
+
execFileSync("sudo", ["-n", "kill", `-${sig}`, ...pids], { timeout: 5000, stdio: "pipe" });
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
catch { }
|
|
789
|
+
try {
|
|
790
|
+
execSync(`kill -${sig} ${pids.join(" ")} 2>/dev/null || true`, { timeout: 5000 });
|
|
791
|
+
}
|
|
792
|
+
catch { }
|
|
793
|
+
};
|
|
794
|
+
let pids = collect();
|
|
795
|
+
sendSignal("TERM", pids);
|
|
796
|
+
if (pids.length > 0) {
|
|
797
|
+
// Short grace period, then SIGKILL any survivors.
|
|
798
|
+
execSync("sleep 2");
|
|
799
|
+
pids = collect();
|
|
800
|
+
sendSignal("KILL", pids);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
/** Compare two "a.b.c" semver strings; returns a > b. */
|
|
804
|
+
function isNomadVersionGreater(a, b) {
|
|
805
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map(n => parseInt(n, 10) || 0);
|
|
806
|
+
const [aMaj, aMin, aPat] = parse(a);
|
|
807
|
+
const [bMaj, bMin, bPat] = parse(b);
|
|
808
|
+
if (aMaj !== bMaj)
|
|
809
|
+
return aMaj > bMaj;
|
|
810
|
+
if (aMin !== bMin)
|
|
811
|
+
return aMin > bMin;
|
|
812
|
+
return aPat > bPat;
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Auto-migrate from a higher Nomad version (e.g. 1.11.3 BSL) back to the
|
|
816
|
+
* jishushell target (1.6.5 MPL). Called when installNomad detects a local
|
|
817
|
+
* binary whose semver is > NOMAD_VERSION. Destructive to Nomad's raft state
|
|
818
|
+
* (schema is not backward compatible) but preserves instance configs under
|
|
819
|
+
* ~/.jishushell/instances/*. A single tar.gz snapshot of the old data_dir
|
|
820
|
+
* is kept under ~/.jishushell/nomad/backups/ for forensic inspection.
|
|
821
|
+
*
|
|
822
|
+
* Safe-first: the new binary is downloaded and verified BEFORE any existing
|
|
823
|
+
* state is touched. If any stage 1 step fails, state is untouched.
|
|
824
|
+
*
|
|
825
|
+
* Throws on failure so the caller's outer catch reports the error.
|
|
826
|
+
*/
|
|
827
|
+
async function migrateNomadToTarget(currentVersion) {
|
|
828
|
+
console.log(`[nomad] Auto-migrating v${currentVersion} → v${NOMAD_VERSION} (BSL → MPL)`);
|
|
829
|
+
console.log("[nomad] Raft state is not backward-compatible; allocation history will be reset.");
|
|
830
|
+
console.log("[nomad] Instance configs under ~/.jishushell/instances/ are preserved.");
|
|
831
|
+
// ── Stage 1: download + verify new binary into a staging dir ─────────
|
|
832
|
+
const stageDir = mkdtempSync(join(tmpdir(), "nomad-migrate-"));
|
|
833
|
+
let backupFile = "";
|
|
834
|
+
try {
|
|
835
|
+
const stagedBin = join(stageDir, "nomad");
|
|
836
|
+
const zipPath = join(stageDir, "nomad.zip");
|
|
837
|
+
const url = getNomadDownloadUrl();
|
|
838
|
+
const arch = process.arch === "arm64" ? "arm64" : "amd64";
|
|
839
|
+
const os = process.platform === "linux" ? "linux" : "darwin";
|
|
840
|
+
const sumsUrl = `https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_SHA256SUMS`;
|
|
841
|
+
const sumsPath = join(stageDir, "SHA256SUMS");
|
|
842
|
+
console.log(`[nomad] Staging v${NOMAD_VERSION} (${os}/${arch})...`);
|
|
843
|
+
execFileSync("curl", ["-fsSL", url, "-o", zipPath], { timeout: 300000, stdio: "pipe" });
|
|
844
|
+
execFileSync("curl", ["-fsSL", sumsUrl, "-o", sumsPath], { timeout: 30000, stdio: "pipe" });
|
|
845
|
+
const sums = readFileSync(sumsPath, "utf-8");
|
|
846
|
+
const sumLine = sums.split("\n").find(l => l.includes(`nomad_${NOMAD_VERSION}_${os}_${arch}.zip`));
|
|
847
|
+
if (!sumLine)
|
|
848
|
+
throw new Error(`No checksum entry for nomad_${NOMAD_VERSION}_${os}_${arch}.zip`);
|
|
849
|
+
const expected = sumLine.split(/\s+/)[0];
|
|
850
|
+
// Match the bash installer: prefer sha256sum (GNU coreutils, Linux),
|
|
851
|
+
// fall back to shasum -a 256 (BSD, macOS default — sha256sum is not
|
|
852
|
+
// preinstalled there). Without this, triggering auto-migration from
|
|
853
|
+
// the WebUI / Node path on macOS would fail even though the shell
|
|
854
|
+
// installer works fine.
|
|
855
|
+
let actual;
|
|
856
|
+
try {
|
|
857
|
+
actual = execSync(`sha256sum "${zipPath}" | awk '{print $1}'`, { encoding: "utf-8" }).trim();
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
actual = execSync(`shasum -a 256 "${zipPath}" | awk '{print $1}'`, { encoding: "utf-8" }).trim();
|
|
861
|
+
}
|
|
862
|
+
if (expected !== actual) {
|
|
863
|
+
throw new Error(`Nomad checksum mismatch: expected ${expected}, got ${actual}`);
|
|
864
|
+
}
|
|
865
|
+
console.log("[nomad] Checksum verified");
|
|
866
|
+
execFileSync("unzip", ["-o", zipPath, "-d", stageDir], { timeout: 30000 });
|
|
867
|
+
chmodSync(stagedBin, 0o755);
|
|
868
|
+
const stagedVersionLine = execFileSync(stagedBin, ["version"], { encoding: "utf-8", timeout: 5000 }).trim().split("\n")[0];
|
|
869
|
+
if (!stagedVersionLine.includes(`v${NOMAD_VERSION}`)) {
|
|
870
|
+
throw new Error(`Staged binary reports "${stagedVersionLine}", expected v${NOMAD_VERSION}`);
|
|
871
|
+
}
|
|
872
|
+
console.log(`[nomad] Staged ${stagedVersionLine}`);
|
|
873
|
+
// ── Stage 2: destructive state changes begin ───────────────────────
|
|
874
|
+
console.log("[nomad] Stopping services...");
|
|
875
|
+
try {
|
|
876
|
+
execFileSync("sudo", ["-n", "systemctl", "stop", "jishushell"], { timeout: 15000, stdio: "pipe" });
|
|
877
|
+
}
|
|
878
|
+
catch { }
|
|
879
|
+
try {
|
|
880
|
+
execFileSync("sudo", ["-n", "systemctl", "stop", "nomad"], { timeout: 15000, stdio: "pipe" });
|
|
881
|
+
}
|
|
882
|
+
catch { }
|
|
883
|
+
// pkill -f 'nomad agent' matches pkill's own cmdline and self-terminates
|
|
884
|
+
// before reaching the real nomad process. Use pgrep -x nomad (exact proc
|
|
885
|
+
// name match; pgrep's comm is "pgrep") to avoid the self-match bug.
|
|
886
|
+
killNomadByProcName();
|
|
887
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
888
|
+
// ── Stage 3: tar backup (single snapshot, overwrite any previous) ──
|
|
889
|
+
const backupDir = join(NOMAD_CONFIG_DIR, "backups");
|
|
890
|
+
if (existsSync(NOMAD_DATA_DIR)) {
|
|
891
|
+
try {
|
|
892
|
+
ensureDirHost(backupDir);
|
|
893
|
+
}
|
|
894
|
+
catch { }
|
|
895
|
+
const ts = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "-");
|
|
896
|
+
const candidate = join(backupDir, `data-${ts}.tar.gz`);
|
|
897
|
+
console.log(`[nomad] Backing up raft state → ${candidate}`);
|
|
898
|
+
try {
|
|
899
|
+
execSync(`tar czf "${candidate}" -C "${NOMAD_CONFIG_DIR}" data 2>/dev/null`, { timeout: 120000 });
|
|
900
|
+
backupFile = candidate;
|
|
901
|
+
// Keep only the most recent snapshot
|
|
902
|
+
try {
|
|
903
|
+
const list = execSync(`ls -t "${backupDir}"/data-*.tar.gz 2>/dev/null | tail -n +2 || true`, { encoding: "utf-8" }).trim();
|
|
904
|
+
for (const old of list.split("\n").filter(Boolean)) {
|
|
905
|
+
try {
|
|
906
|
+
unlinkSync(old);
|
|
907
|
+
}
|
|
908
|
+
catch { }
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
catch { }
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
console.warn("[nomad] Backup tar failed — continuing (raft state will still be wiped)");
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
// ── Stage 4: wipe raft state + env files (schema incompatible) ─────
|
|
918
|
+
try {
|
|
919
|
+
execFileSync("sudo", ["-n", "rm", "-rf", NOMAD_DATA_DIR], { timeout: 15000, stdio: "pipe" });
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
try {
|
|
923
|
+
rmSync(NOMAD_DATA_DIR, { recursive: true, force: true });
|
|
924
|
+
}
|
|
925
|
+
catch { }
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
unlinkSync(join(JISHUSHELL_HOME, "nomad.env"));
|
|
929
|
+
}
|
|
930
|
+
catch { }
|
|
931
|
+
try {
|
|
932
|
+
execFileSync("sudo", ["-n", "rm", "-f", "/etc/jishushell/nomad.env"], { timeout: 5000, stdio: "pipe" });
|
|
933
|
+
}
|
|
934
|
+
catch { }
|
|
935
|
+
// ── Stage 5: orphaned gateway containers ───────────────────────────
|
|
936
|
+
// Panel normally has docker group via jishushell.service SupplementaryGroups,
|
|
937
|
+
// but postinstall may run this helper from a shell where the invoking
|
|
938
|
+
// user is not in docker group. Probe first, fall back to sudo docker.
|
|
939
|
+
try {
|
|
940
|
+
let dockerCmd = "docker";
|
|
941
|
+
try {
|
|
942
|
+
execSync("docker ps >/dev/null 2>&1", { timeout: 5000 });
|
|
943
|
+
}
|
|
944
|
+
catch {
|
|
945
|
+
dockerCmd = "sudo -n docker";
|
|
946
|
+
}
|
|
947
|
+
const names = execSync(`${dockerCmd} ps -a --format '{{.Names}}' 2>/dev/null | grep '^gateway-' || true`, { encoding: "utf-8" }).trim();
|
|
948
|
+
if (names) {
|
|
949
|
+
const rows = names.split("\n").filter(Boolean);
|
|
950
|
+
for (const name of rows) {
|
|
951
|
+
try {
|
|
952
|
+
execSync(`${dockerCmd} rm -f "${name}" 2>/dev/null`, { timeout: 10000 });
|
|
953
|
+
}
|
|
954
|
+
catch { }
|
|
955
|
+
}
|
|
956
|
+
console.log(`[nomad] Removed ${rows.length} orphaned gateway container(s)`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
catch { }
|
|
960
|
+
// ── Stage 6: swap binary into place (atomic via temp name + rename)
|
|
961
|
+
ensureDirHost(BIN_DIR);
|
|
962
|
+
const destTmp = `${NOMAD_BIN}.tmp.${process.pid}`;
|
|
963
|
+
copyFileSync(stagedBin, destTmp);
|
|
964
|
+
chmodSync(destTmp, 0o755);
|
|
965
|
+
renameSync(destTmp, NOMAD_BIN);
|
|
966
|
+
console.log(`[nomad] Migrated to v${NOMAD_VERSION}`);
|
|
967
|
+
if (backupFile)
|
|
968
|
+
console.log(`[nomad] Backup (forensic, not self-recovery): ${backupFile}`);
|
|
969
|
+
console.log("[nomad] JishuShell will re-bootstrap ACL and resubmit jobs on next start.");
|
|
970
|
+
}
|
|
971
|
+
catch (err) {
|
|
972
|
+
if (backupFile) {
|
|
973
|
+
console.error(`[nomad] Migration failed — backup preserved at ${backupFile}`);
|
|
974
|
+
}
|
|
975
|
+
throw err;
|
|
976
|
+
}
|
|
977
|
+
finally {
|
|
978
|
+
try {
|
|
979
|
+
rmSync(stageDir, { recursive: true, force: true });
|
|
980
|
+
}
|
|
981
|
+
catch { }
|
|
982
|
+
}
|
|
983
|
+
}
|
|
772
984
|
export async function installNomad() {
|
|
773
985
|
try {
|
|
774
986
|
if (existsSync(NOMAD_BIN)) {
|
|
@@ -790,8 +1002,42 @@ export async function installNomad() {
|
|
|
790
1002
|
}
|
|
791
1003
|
catch { }
|
|
792
1004
|
// Boundary check 3: does it actually run?
|
|
1005
|
+
let versionLine = "";
|
|
793
1006
|
try {
|
|
794
|
-
|
|
1007
|
+
versionLine = execSync(`"${NOMAD_BIN}" version`, { encoding: "utf-8", timeout: 5000 }).trim().split("\n")[0];
|
|
1008
|
+
}
|
|
1009
|
+
catch {
|
|
1010
|
+
// Binary is corrupt or wrong arch — remove and reinstall
|
|
1011
|
+
try {
|
|
1012
|
+
unlinkSync(NOMAD_BIN);
|
|
1013
|
+
}
|
|
1014
|
+
catch { }
|
|
1015
|
+
}
|
|
1016
|
+
if (versionLine) {
|
|
1017
|
+
const match = versionLine.match(/v(\d+\.\d+\.\d+)/);
|
|
1018
|
+
const currentVersion = match ? match[1] : "";
|
|
1019
|
+
let migrated = false;
|
|
1020
|
+
if (currentVersion && isNomadVersionGreater(currentVersion, NOMAD_VERSION)) {
|
|
1021
|
+
// Current > target — auto-migrate (nomad 1.11.3 BSL → 1.6.5 MPL).
|
|
1022
|
+
// Migration failure is a hard stop: the old state has been
|
|
1023
|
+
// partially mutated (or about to be), returning falls through
|
|
1024
|
+
// to the reinstall path which would make a bad situation worse.
|
|
1025
|
+
try {
|
|
1026
|
+
await migrateNomadToTarget(currentVersion);
|
|
1027
|
+
}
|
|
1028
|
+
catch (migErr) {
|
|
1029
|
+
return {
|
|
1030
|
+
ok: false,
|
|
1031
|
+
message: "Nomad auto-migration failed",
|
|
1032
|
+
error: migErr?.message || String(migErr),
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
migrated = true;
|
|
1036
|
+
versionLine = execSync(`"${NOMAD_BIN}" version`, { encoding: "utf-8", timeout: 5000 }).trim().split("\n")[0];
|
|
1037
|
+
}
|
|
1038
|
+
// Always (re)write config so the installed nomad.hcl stays up-to-date
|
|
1039
|
+
// with the current defaults (e.g. raw_exec plugin, acl, limits).
|
|
1040
|
+
writeNomadConfig();
|
|
795
1041
|
// Ensure Nomad is started even if already installed
|
|
796
1042
|
if (!isPortListening(4646)) {
|
|
797
1043
|
try {
|
|
@@ -802,14 +1048,9 @@ export async function installNomad() {
|
|
|
802
1048
|
await startNomad();
|
|
803
1049
|
}
|
|
804
1050
|
}
|
|
805
|
-
return
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
// Binary is corrupt or wrong arch — remove and reinstall
|
|
809
|
-
try {
|
|
810
|
-
unlinkSync(NOMAD_BIN);
|
|
811
|
-
}
|
|
812
|
-
catch { }
|
|
1051
|
+
return migrated
|
|
1052
|
+
? { ok: true, message: `Nomad migrated to ${versionLine}` }
|
|
1053
|
+
: { ok: true, message: `Nomad already installed: ${versionLine}` };
|
|
813
1054
|
}
|
|
814
1055
|
}
|
|
815
1056
|
}
|
|
@@ -823,6 +1064,7 @@ export async function installNomad() {
|
|
|
823
1064
|
symlinkSync(systemNomad, NOMAD_BIN);
|
|
824
1065
|
const version = execSync(`${NOMAD_BIN} version`, { encoding: "utf-8", timeout: 5000 }).trim();
|
|
825
1066
|
console.log(`[nomad] Linked system nomad ${systemNomad} → ${NOMAD_BIN}`);
|
|
1067
|
+
writeNomadConfig();
|
|
826
1068
|
return { ok: true, message: `Nomad linked from system: ${version.split("\n")[0]}` };
|
|
827
1069
|
}
|
|
828
1070
|
}
|
|
@@ -900,7 +1142,16 @@ function writeNomadConfig() {
|
|
|
900
1142
|
ensureDirHost(NOMAD_CONFIG_DIR);
|
|
901
1143
|
ensureDirContainer(NOMAD_DATA_DIR);
|
|
902
1144
|
ensureDirContainer(NOMAD_ALLOC_DIR);
|
|
903
|
-
const
|
|
1145
|
+
const platform = osPlatform();
|
|
1146
|
+
const loopbackIface = platform === "darwin" ? "lo0" : "lo";
|
|
1147
|
+
const externalIface = detectNomadExternalInterface(loopbackIface);
|
|
1148
|
+
const externalHostNetworkBlock = externalIface
|
|
1149
|
+
? `
|
|
1150
|
+
host_network "external" {
|
|
1151
|
+
interface = "${externalIface}"
|
|
1152
|
+
}
|
|
1153
|
+
`
|
|
1154
|
+
: "";
|
|
904
1155
|
const config = `
|
|
905
1156
|
data_dir = "${NOMAD_DATA_DIR}"
|
|
906
1157
|
|
|
@@ -924,6 +1175,7 @@ client {
|
|
|
924
1175
|
servers = ["127.0.0.1:4647"]
|
|
925
1176
|
network_interface = "${loopbackIface}"
|
|
926
1177
|
alloc_dir = "${NOMAD_ALLOC_DIR}"
|
|
1178
|
+
${externalHostNetworkBlock}
|
|
927
1179
|
|
|
928
1180
|
# drain_on_shutdown intentionally omitted: on single-node Pi there is
|
|
929
1181
|
# nowhere to drain workloads to, and draining on every systemctl restart
|
|
@@ -934,19 +1186,51 @@ client {
|
|
|
934
1186
|
|
|
935
1187
|
plugin "docker" {
|
|
936
1188
|
config {
|
|
937
|
-
disable_log_collection = true
|
|
938
1189
|
volumes {
|
|
939
1190
|
enabled = true
|
|
940
1191
|
}
|
|
941
1192
|
}
|
|
942
1193
|
}
|
|
943
1194
|
|
|
1195
|
+
plugin "raw_exec" {
|
|
1196
|
+
config {
|
|
1197
|
+
enabled = true
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
944
1201
|
acl {
|
|
945
1202
|
enabled = true
|
|
946
1203
|
}
|
|
1204
|
+
|
|
1205
|
+
limits {
|
|
1206
|
+
http_max_conns_per_client = 0
|
|
1207
|
+
}
|
|
947
1208
|
`;
|
|
948
1209
|
writeConfigFile(join(NOMAD_CONFIG_DIR, "nomad.hcl"), config);
|
|
949
1210
|
}
|
|
1211
|
+
function detectNomadExternalInterface(loopbackIface) {
|
|
1212
|
+
try {
|
|
1213
|
+
if (osPlatform() === "darwin") {
|
|
1214
|
+
const route = execFileSync("route", ["-n", "get", "default"], {
|
|
1215
|
+
encoding: "utf8",
|
|
1216
|
+
timeout: 3000,
|
|
1217
|
+
});
|
|
1218
|
+
const match = route.match(/interface:\s*(\S+)/);
|
|
1219
|
+
const iface = match?.[1]?.trim() ?? "";
|
|
1220
|
+
return iface && iface !== loopbackIface ? iface : "";
|
|
1221
|
+
}
|
|
1222
|
+
const route = execFileSync("ip", ["route", "show", "default"], {
|
|
1223
|
+
encoding: "utf8",
|
|
1224
|
+
timeout: 3000,
|
|
1225
|
+
});
|
|
1226
|
+
const match = route.match(/\bdev\s+(\S+)/);
|
|
1227
|
+
const iface = match?.[1]?.trim() ?? "";
|
|
1228
|
+
return iface && iface !== loopbackIface ? iface : "";
|
|
1229
|
+
}
|
|
1230
|
+
catch {
|
|
1231
|
+
return "";
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
950
1234
|
export function loadNomadToken() {
|
|
951
1235
|
if (process.env.NOMAD_TOKEN)
|
|
952
1236
|
return;
|
|
@@ -1090,14 +1374,27 @@ async function bootstrapNomadACL() {
|
|
|
1090
1374
|
}
|
|
1091
1375
|
const resetIndex = resetMatch[1];
|
|
1092
1376
|
console.log(`[nomad] Bootstrap already done (reset index: ${resetIndex}). Performing ACL bootstrap reset...`);
|
|
1093
|
-
// Write the reset trigger file (Nomad reads this on startup to allow re-bootstrap)
|
|
1377
|
+
// Write the reset trigger file (Nomad reads this on startup to allow re-bootstrap).
|
|
1378
|
+
// NOMAD_DATA_DIR/server is owned by root because nomad.service runs as User=root
|
|
1379
|
+
// (docker driver on 1.6.5 requires euid==0). The panel runs as a non-root user, so
|
|
1380
|
+
// plain writeConfigFile would fail with EACCES — route through `sudo tee` instead.
|
|
1094
1381
|
const resetFile = join(NOMAD_DATA_DIR, "server", "acl-bootstrap-reset");
|
|
1095
1382
|
try {
|
|
1096
1383
|
writeConfigFile(resetFile, resetIndex);
|
|
1097
1384
|
}
|
|
1098
1385
|
catch (writeErr) {
|
|
1099
|
-
|
|
1100
|
-
|
|
1386
|
+
try {
|
|
1387
|
+
execFileSync("sudo", ["-n", "mkdir", "-p", dirname(resetFile)], { timeout: 5000, stdio: "pipe" });
|
|
1388
|
+
execFileSync("sudo", ["-n", "tee", resetFile], {
|
|
1389
|
+
timeout: 5000,
|
|
1390
|
+
input: resetIndex,
|
|
1391
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
catch (sudoErr) {
|
|
1395
|
+
console.warn("[nomad] Could not write acl-bootstrap-reset file:", sudoErr.message || writeErr.message);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1101
1398
|
}
|
|
1102
1399
|
// Restart Nomad so it picks up the reset file
|
|
1103
1400
|
try {
|
|
@@ -1106,9 +1403,14 @@ async function bootstrapNomadACL() {
|
|
|
1106
1403
|
catch {
|
|
1107
1404
|
// No passwordless sudo — try pkill/re-spawn path (best effort)
|
|
1108
1405
|
try {
|
|
1109
|
-
execFileSync("
|
|
1406
|
+
execFileSync("sudo", ["-n", "pkill", "-TERM", "-f", "nomad agent"], { stdio: "pipe" });
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
try {
|
|
1410
|
+
execFileSync("pkill", ["-TERM", "-f", "nomad agent"], { stdio: "pipe" });
|
|
1411
|
+
}
|
|
1412
|
+
catch { }
|
|
1110
1413
|
}
|
|
1111
|
-
catch { }
|
|
1112
1414
|
}
|
|
1113
1415
|
// Wait for Nomad to come back
|
|
1114
1416
|
for (let i = 0; i < 20; i++) {
|
|
@@ -1240,11 +1542,8 @@ export async function stopNomad() {
|
|
|
1240
1542
|
// running allocs without killing them (drain_on_shutdown is deliberately
|
|
1241
1543
|
// not configured, so the docker containers keep running and will be
|
|
1242
1544
|
// re-attached when Nomad comes back).
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1245
|
-
execSync("pkill -TERM -f 'nomad agent'", { timeout: 5000 });
|
|
1246
|
-
}
|
|
1247
|
-
catch { }
|
|
1545
|
+
// Use killNomadByProcName (pgrep -x) to avoid pkill -f self-matching.
|
|
1546
|
+
killNomadByProcName();
|
|
1248
1547
|
// Wait up to 10s for the process to exit. No drain means shutdown is
|
|
1249
1548
|
// near-instant — most of this budget is slack for slow disks on Pi.
|
|
1250
1549
|
for (let i = 0; i < 10; i++) {
|
|
@@ -1275,7 +1574,14 @@ export function installNomadSystemd() {
|
|
|
1275
1574
|
if (process.platform === "darwin") {
|
|
1276
1575
|
const plistLabel = "com.jishushell.nomad";
|
|
1277
1576
|
const logPath = join(NOMAD_CONFIG_DIR, "nomad.log");
|
|
1278
|
-
const
|
|
1577
|
+
const dockerHost = resolveDockerHost({
|
|
1578
|
+
jishuHome: JISHUSHELL_HOME,
|
|
1579
|
+
colimaProfile: COLIMA_PROFILE,
|
|
1580
|
+
});
|
|
1581
|
+
const dockerHostEntry = dockerHost
|
|
1582
|
+
? `
|
|
1583
|
+
<key>DOCKER_HOST</key><string>${dockerHost}</string>`
|
|
1584
|
+
: "";
|
|
1279
1585
|
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1280
1586
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1281
1587
|
<plist version="1.0">
|
|
@@ -1288,8 +1594,7 @@ export function installNomadSystemd() {
|
|
|
1288
1594
|
<string>-config=${configPath}</string>
|
|
1289
1595
|
</array>
|
|
1290
1596
|
<key>EnvironmentVariables</key>
|
|
1291
|
-
<dict
|
|
1292
|
-
<key>DOCKER_HOST</key><string>unix://${dockerSock}</string>
|
|
1597
|
+
<dict>${dockerHostEntry}
|
|
1293
1598
|
<key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
1294
1599
|
</dict>
|
|
1295
1600
|
<key>RunAtLoad</key><true/>
|
|
@@ -1307,22 +1612,25 @@ export function installNomadSystemd() {
|
|
|
1307
1612
|
execSync(`launchctl load -w "${plistPath}"`, { timeout: 15000 });
|
|
1308
1613
|
return { ok: true, message: "Nomad launchd agent installed and started" };
|
|
1309
1614
|
}
|
|
1310
|
-
// Nomad
|
|
1311
|
-
//
|
|
1312
|
-
|
|
1615
|
+
// Nomad 1.6.5's docker driver fingerprint requires euid==0 — PR #18197 lifted
|
|
1616
|
+
// that restriction only in 1.7+, and we intentionally stay on the 1.6 MPL line.
|
|
1617
|
+
// The panel stays as the installing user via a separate jishushell.service unit;
|
|
1618
|
+
// it talks to this agent over HTTP, so no files under ~/.jishushell/nomad/data/
|
|
1619
|
+
// are read directly by the panel.
|
|
1313
1620
|
const serviceContent = `[Unit]
|
|
1314
1621
|
Description=Nomad Agent
|
|
1315
1622
|
After=network-online.target docker.service
|
|
1316
1623
|
Wants=network-online.target
|
|
1317
1624
|
|
|
1318
1625
|
[Service]
|
|
1319
|
-
User
|
|
1320
|
-
SupplementaryGroups=docker
|
|
1626
|
+
User=root
|
|
1321
1627
|
Type=simple
|
|
1322
1628
|
EnvironmentFile=-/etc/jishushell/nomad.env
|
|
1323
1629
|
ExecStart=${nomadPath} agent -config=${configPath}
|
|
1324
1630
|
Restart=on-failure
|
|
1325
1631
|
RestartSec=3
|
|
1632
|
+
Delegate=yes
|
|
1633
|
+
TasksMax=infinity
|
|
1326
1634
|
|
|
1327
1635
|
[Install]
|
|
1328
1636
|
WantedBy=multi-user.target
|
|
@@ -1365,7 +1673,6 @@ export function installJishushellSystemd(port) {
|
|
|
1365
1673
|
export JISHUSHELL_HOME="${JISHUSHELL_HOME}"
|
|
1366
1674
|
export HOME="${realHome}"
|
|
1367
1675
|
export NODE_ENV=production
|
|
1368
|
-
export DOCKER_HOST="unix://${COLIMA_SOCKET}"
|
|
1369
1676
|
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin:${dirname(nodeBin)}:\${PATH}"
|
|
1370
1677
|
exec "${nodeBin}" "${cliBin}" serve --port ${resolvedPort}
|
|
1371
1678
|
`;
|
|
@@ -1434,64 +1741,77 @@ WantedBy=multi-user.target
|
|
|
1434
1741
|
}
|
|
1435
1742
|
}
|
|
1436
1743
|
// ── Install OpenClaw (async with progress) ─────────────────────────
|
|
1437
|
-
|
|
1744
|
+
/**
|
|
1745
|
+
* Install OpenClaw runtime — thin dispatch wrapper.
|
|
1746
|
+
*
|
|
1747
|
+
* The heavy lifting lives in `OpenClawAdapter.installRuntime()` (§32.2.4).
|
|
1748
|
+
* This wrapper is kept only for back-compat with the existing
|
|
1749
|
+
* `routes/setup.ts` endpoint and the CLI installer.
|
|
1750
|
+
*/
|
|
1438
1751
|
export async function installOpenclaw(version = "latest") {
|
|
1752
|
+
const adapter = getAdapter("openclaw");
|
|
1753
|
+
if (typeof adapter.installRuntime !== "function") {
|
|
1754
|
+
return { ok: false, message: "OpenClawAdapter.installRuntime is not implemented" };
|
|
1755
|
+
}
|
|
1756
|
+
return adapter.installRuntime({ version });
|
|
1757
|
+
}
|
|
1758
|
+
// ── Hermes install (§32.1 / §32.3) ─────────────────────────────────
|
|
1759
|
+
//
|
|
1760
|
+
// Hermes constants (HERMES_DEFAULT_IMAGE, HERMES_RUNTIME_DIR,
|
|
1761
|
+
// HERMES_SHIM_FILENAME) and the shim template resolver have moved to
|
|
1762
|
+
// `src/config.ts` (§32 Phase 8). setup-manager no longer defines them.
|
|
1763
|
+
// The HermesAdapter imports them directly from config.ts.
|
|
1764
|
+
/**
|
|
1765
|
+
* Capture the immutable digest of a locally-present image so HermesAdapter
|
|
1766
|
+
* can pin via digest rather than the mutable tag. Returns undefined if the
|
|
1767
|
+
* image has no RepoDigests (e.g. locally-built image) — caller may still
|
|
1768
|
+
* proceed using the tag alone.
|
|
1769
|
+
*/
|
|
1770
|
+
export function captureImageDigest(imageRef) {
|
|
1439
1771
|
try {
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
const sizeTracker = setInterval(() => {
|
|
1450
|
-
const sizeMB = getDirSizeMB(OPENCLAW_PKG_DIR);
|
|
1451
|
-
const pct = Math.min(95, Math.round((sizeMB / OPENCLAW_EXPECTED_SIZE_MB) * 95));
|
|
1452
|
-
if (pct > 0) {
|
|
1453
|
-
emitTask(task, { type: "progress", message: `下载安装中... ${sizeMB}MB / ~${OPENCLAW_EXPECTED_SIZE_MB}MB`, progress: pct });
|
|
1454
|
-
}
|
|
1455
|
-
}, 3000);
|
|
1456
|
-
// Use npm install -g with --prefix so npm uses global-install semantics:
|
|
1457
|
-
// packages go to <prefix>/lib/node_modules/, bins to <prefix>/bin/
|
|
1458
|
-
// This makes postinstall scripts run naturally (no manual workarounds needed).
|
|
1459
|
-
const result = await spawnWithTask(task, "npm", ["install", "-g", "--prefix", OPENCLAW_PKG_DIR, `openclaw@${version}`], { timeout: 600000, progressParser: npmProgressParser });
|
|
1460
|
-
clearInterval(sizeTracker);
|
|
1461
|
-
if (!result.ok) {
|
|
1462
|
-
emitTask(task, { type: "error", message: "OpenClaw 安装失败" });
|
|
1463
|
-
task.status = "error";
|
|
1464
|
-
return { ok: false, message: "OpenClaw installation failed", error: result.output, taskId: task.id };
|
|
1465
|
-
}
|
|
1466
|
-
// Read version from package.json since openclaw --version needs Node 22+
|
|
1467
|
-
let ver = "installed";
|
|
1468
|
-
try {
|
|
1469
|
-
const pkg = join(OPENCLAW_MODULES, "openclaw", "package.json");
|
|
1470
|
-
if (existsSync(pkg)) {
|
|
1471
|
-
const pkgJson = JSON.parse(readFileSync(pkg, "utf-8"));
|
|
1472
|
-
ver = pkgJson.version || "installed";
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
catch { }
|
|
1476
|
-
emitTask(task, { type: "done", message: `OpenClaw 安装完成: ${ver}`, progress: 100 });
|
|
1477
|
-
task.status = "done";
|
|
1478
|
-
return { ok: true, message: `OpenClaw installed: ${ver}`, taskId: task.id };
|
|
1772
|
+
const raw = execFileSync("docker", [
|
|
1773
|
+
"image",
|
|
1774
|
+
"inspect",
|
|
1775
|
+
"--format",
|
|
1776
|
+
"{{range .RepoDigests}}{{.}}{{\"\\n\"}}{{end}}",
|
|
1777
|
+
imageRef,
|
|
1778
|
+
], { encoding: "utf-8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1779
|
+
const first = raw.split("\n").map((l) => l.trim()).find(Boolean);
|
|
1780
|
+
return first || undefined;
|
|
1479
1781
|
}
|
|
1480
|
-
catch
|
|
1481
|
-
return
|
|
1782
|
+
catch {
|
|
1783
|
+
return undefined;
|
|
1482
1784
|
}
|
|
1483
1785
|
}
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1786
|
+
/**
|
|
1787
|
+
* Install Hermes runtime — thin dispatch wrapper.
|
|
1788
|
+
*
|
|
1789
|
+
* Heavy lifting lives in `HermesAdapter.installRuntime()` (§32.2.4).
|
|
1790
|
+
*/
|
|
1791
|
+
export async function installHermes() {
|
|
1792
|
+
const adapter = getAdapter("hermes");
|
|
1793
|
+
if (typeof adapter.installRuntime !== "function") {
|
|
1794
|
+
return { ok: false, message: "HermesAdapter.installRuntime is not implemented" };
|
|
1490
1795
|
}
|
|
1491
|
-
|
|
1492
|
-
return "";
|
|
1796
|
+
return adapter.installRuntime();
|
|
1493
1797
|
}
|
|
1494
|
-
|
|
1798
|
+
/**
|
|
1799
|
+
* Non-blocking variant — returns a task id immediately. Implementation
|
|
1800
|
+
* dispatches via `HermesAdapter.startInstallRuntime()`.
|
|
1801
|
+
*/
|
|
1802
|
+
export function startInstallHermes() {
|
|
1803
|
+
const adapter = getAdapter("hermes");
|
|
1804
|
+
if (typeof adapter.startInstallRuntime !== "function") {
|
|
1805
|
+
return { ok: false, message: "HermesAdapter.startInstallRuntime is not implemented" };
|
|
1806
|
+
}
|
|
1807
|
+
return adapter.startInstallRuntime();
|
|
1808
|
+
}
|
|
1809
|
+
/** Helper for setup status — true when runtime catalog has hermes entry. */
|
|
1810
|
+
export function isHermesInstalled() {
|
|
1811
|
+
const entry = getRuntimeCatalogEntry("hermes");
|
|
1812
|
+
return !!entry && !!entry.defaultImage;
|
|
1813
|
+
}
|
|
1814
|
+
export function resolveDockerInvocation() {
|
|
1495
1815
|
try {
|
|
1496
1816
|
execFileSync("docker", ["info"], { timeout: 5000, stdio: "ignore" });
|
|
1497
1817
|
return { cmd: "docker", argsPrefix: [] };
|
|
@@ -1512,465 +1832,70 @@ function checkDockerImageExists() {
|
|
|
1512
1832
|
return true;
|
|
1513
1833
|
}
|
|
1514
1834
|
catch {
|
|
1515
|
-
//
|
|
1516
|
-
//
|
|
1517
|
-
//
|
|
1835
|
+
// Fallback scan: list all local images and try to find any known runtime image.
|
|
1836
|
+
// This handles two scenarios:
|
|
1837
|
+
// 1. panel.json was wiped (e.g. after `jishushell reset`) and the pinned version
|
|
1838
|
+
// tag is no longer stored, causing getOpenclawDockerImage() to return the default
|
|
1839
|
+
// `:latest` tag which may have been removed locally during the first-run migration.
|
|
1840
|
+
// 2. The environment uses a locally built runtime image (e.g. jishushell-hermes:latest)
|
|
1841
|
+
// that differs from the registry default.
|
|
1842
|
+
// When a candidate is found we restore panel.json (self-heal) so the fast path works next time.
|
|
1518
1843
|
try {
|
|
1519
1844
|
const invocation = resolveDockerInvocation();
|
|
1520
1845
|
const out = execFileSync(invocation.cmd, [...invocation.argsPrefix, "images", "--format", "{{.Repository}}:{{.Tag}}"], { encoding: "utf8", timeout: 5000 });
|
|
1521
|
-
|
|
1846
|
+
const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
1847
|
+
// 1. Same repository as DEFAULT_OPENCLAW_DOCKER_IMAGE (e.g. after pinned-tag migration).
|
|
1848
|
+
const defaultImage = DEFAULT_OPENCLAW_DOCKER_IMAGE;
|
|
1849
|
+
const repoColonIdx = defaultImage.lastIndexOf(":");
|
|
1850
|
+
const repoSlashIdx = defaultImage.lastIndexOf("/");
|
|
1851
|
+
if (repoColonIdx > repoSlashIdx) {
|
|
1852
|
+
const repo = defaultImage.slice(0, repoColonIdx);
|
|
1853
|
+
const repoPrefix = repo + ":";
|
|
1854
|
+
const found = lines.find((l) => l.startsWith(repoPrefix) && !l.endsWith(":<none>") && !l.endsWith(":none"));
|
|
1855
|
+
if (found) {
|
|
1856
|
+
setOpenclawDockerImage(found);
|
|
1857
|
+
return true;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
// 2. Backward compat: older locally-built jishushell-openclaw:* image names.
|
|
1861
|
+
// These use the same slim base architecture as ghcr.io/x-aijishu/openclaw-runtime:*
|
|
1862
|
+
// and are fully compatible. Self-heal panel.json so the tag stored there becomes
|
|
1863
|
+
// the concrete existing tag, preventing repeated DEFAULT-migration side effects.
|
|
1864
|
+
const legacyFound = lines.find((l) => /^jishushell-openclaw:[^\s]+/.test(l) && !l.endsWith(":<none>") && !l.endsWith(":none"));
|
|
1865
|
+
if (legacyFound) {
|
|
1866
|
+
setOpenclawDockerImage(legacyFound);
|
|
1522
1867
|
return true;
|
|
1868
|
+
}
|
|
1523
1869
|
}
|
|
1524
1870
|
catch { }
|
|
1525
1871
|
return false;
|
|
1526
1872
|
}
|
|
1527
1873
|
}
|
|
1528
|
-
// The stable tag for the JishuShell base image
|
|
1874
|
+
// The stable tag for the JishuShell base image. Reads panel.json — framework-level.
|
|
1529
1875
|
function resolveDockerImageTag() {
|
|
1530
1876
|
return getOpenclawDockerImage();
|
|
1531
1877
|
}
|
|
1532
|
-
//
|
|
1533
|
-
//
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
"registry.cn-hangzhou.aliyuncs.com/library/node:22-slim",
|
|
1540
|
-
];
|
|
1541
|
-
// Pull DOCKER_BASE_IMAGE from mirrors if not already cached locally.
|
|
1542
|
-
// Returns the image content digest (sha256:…) so that the subsequent
|
|
1543
|
-
// docker build can use `FROM <digest>`, which BuildKit resolves from
|
|
1544
|
-
// the local daemon without any outbound registry metadata request.
|
|
1545
|
-
async function ensureDockerBaseImage(invocation, task) {
|
|
1546
|
-
// Fast path: already in local daemon cache — return image name (not digest) for Dockerfile FROM
|
|
1547
|
-
try {
|
|
1548
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "inspect", DOCKER_BASE_IMAGE], {
|
|
1549
|
-
timeout: 5000,
|
|
1550
|
-
stdio: "ignore",
|
|
1551
|
-
});
|
|
1552
|
-
emitTask(task, { type: "log", message: `基础镜像已缓存: ${DOCKER_BASE_IMAGE}` });
|
|
1553
|
-
return DOCKER_BASE_IMAGE;
|
|
1554
|
-
}
|
|
1555
|
-
catch { /* not cached, fall through */ }
|
|
1556
|
-
for (const mirror of DOCKER_BASE_MIRRORS) {
|
|
1557
|
-
emitTask(task, { type: "log", message: `拉取基础镜像: ${mirror} ...` });
|
|
1558
|
-
const result = await spawnWithTask(task, invocation.cmd, [...invocation.argsPrefix, "pull", mirror], { timeout: 300000 });
|
|
1559
|
-
if (result.ok) {
|
|
1560
|
-
if (mirror !== DOCKER_BASE_IMAGE) {
|
|
1561
|
-
try {
|
|
1562
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "tag", mirror, DOCKER_BASE_IMAGE], { timeout: 10000 });
|
|
1563
|
-
}
|
|
1564
|
-
catch { /* tag failure is non-fatal */ }
|
|
1565
|
-
}
|
|
1566
|
-
emitTask(task, { type: "log", message: `基础镜像就绪: ${DOCKER_BASE_IMAGE}` });
|
|
1567
|
-
return DOCKER_BASE_IMAGE;
|
|
1568
|
-
}
|
|
1569
|
-
emitTask(task, { type: "log", message: ` → ${mirror} 不可达,尝试下一个镜像源...` });
|
|
1570
|
-
}
|
|
1571
|
-
throw new Error(`无法获取基础镜像 ${DOCKER_BASE_IMAGE}。请检查网络或手动执行: docker pull ${DOCKER_BASE_MIRRORS[1]}`);
|
|
1572
|
-
}
|
|
1573
|
-
function resolveVersionedBuildTag() {
|
|
1574
|
-
try {
|
|
1575
|
-
const pkg = join(OPENCLAW_MODULES, "openclaw", "package.json");
|
|
1576
|
-
if (existsSync(pkg)) {
|
|
1577
|
-
const ver = JSON.parse(readFileSync(pkg, "utf-8")).version;
|
|
1578
|
-
if (ver)
|
|
1579
|
-
return `jishushell-openclaw:${ver}`;
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
catch { }
|
|
1583
|
-
return resolveDockerImageTag();
|
|
1584
|
-
}
|
|
1585
|
-
async function buildOpenclawDockerImageWithTask(task, tag) {
|
|
1586
|
-
const targetTag = tag || resolveVersionedBuildTag();
|
|
1587
|
-
try {
|
|
1588
|
-
const invocation = resolveDockerInvocation();
|
|
1589
|
-
// Fast check: if image with this exact tag exists, skip build (tag encodes version)
|
|
1590
|
-
try {
|
|
1591
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "inspect", targetTag], {
|
|
1592
|
-
timeout: 10000,
|
|
1593
|
-
stdio: "ignore",
|
|
1594
|
-
});
|
|
1595
|
-
setOpenclawDockerImage(targetTag);
|
|
1596
|
-
emitTask(task, { type: "done", message: `Docker 镜像已存在: ${targetTag}`, progress: 100 });
|
|
1597
|
-
task.status = "done";
|
|
1598
|
-
return { ok: true, message: `Docker image ${targetTag} already exists`, taskId: task.id };
|
|
1599
|
-
}
|
|
1600
|
-
catch { /* image not found, proceed to build */ }
|
|
1601
|
-
// Clean up old openclaw:* images that predate the new slim-base architecture.
|
|
1602
|
-
// Skip if the stored tag matches targetTag or is a remote registry image.
|
|
1603
|
-
const oldTag = getPanelConfig().openclaw_image;
|
|
1604
|
-
if (oldTag && oldTag !== targetTag && !oldTag.includes("/") && /^openclaw:/i.test(oldTag)) {
|
|
1605
|
-
try {
|
|
1606
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "rmi", oldTag], { timeout: 15000, stdio: "ignore" });
|
|
1607
|
-
}
|
|
1608
|
-
catch { }
|
|
1609
|
-
}
|
|
1610
|
-
// Ensure node:22-slim base image is in the local daemon cache before building.
|
|
1611
|
-
emitTask(task, { type: "progress", message: "准备基础镜像...", progress: 5 });
|
|
1612
|
-
const baseImageId = await ensureDockerBaseImage(invocation, task);
|
|
1613
|
-
// Slim Dockerfile: no COPY of openclaw node_modules — the binary is bind-mounted
|
|
1614
|
-
// from the host at runtime, so this image needs no OpenClaw-specific layers.
|
|
1615
|
-
// Build context is an empty temp directory (no files to COPY).
|
|
1616
|
-
const dockerfile = `FROM ${baseImageId}
|
|
1617
|
-
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
1618
|
-
ca-certificates curl \\
|
|
1619
|
-
python3 python3-pip python3-venv python3-dev && \\
|
|
1620
|
-
ln -sf /usr/bin/python3 /usr/local/bin/python && \\
|
|
1621
|
-
rm -rf /var/lib/apt/lists/*
|
|
1622
|
-
WORKDIR /data
|
|
1623
|
-
USER node
|
|
1624
|
-
ENTRYPOINT ["node", "/usr/lib/node_modules/openclaw/openclaw.mjs"]
|
|
1625
|
-
CMD ["gateway", "run", "--port", "18789", "--allow-unconfigured"]
|
|
1626
|
-
`;
|
|
1627
|
-
// Use a temp dir as build context — no files to COPY means no large transfer.
|
|
1628
|
-
const buildDir = join(tmpdir(), `jishushell-base-build-${Date.now()}`);
|
|
1629
|
-
ensureDirHost(buildDir);
|
|
1630
|
-
const dockerfilePath = join(buildDir, "Dockerfile");
|
|
1631
|
-
writeConfigFile(dockerfilePath, dockerfile);
|
|
1632
|
-
emitTask(task, { type: "progress", message: `构建基础镜像 ${targetTag}(无需拷贝二进制,速度极快)...`, progress: 10 });
|
|
1633
|
-
let result;
|
|
1634
|
-
try {
|
|
1635
|
-
result = await spawnWithTask(task, invocation.cmd, [...invocation.argsPrefix, "build", "--network=host", "-t", targetTag, buildDir], {
|
|
1636
|
-
timeout: 1800000,
|
|
1637
|
-
progressParser: dockerBuildProgressParser,
|
|
1638
|
-
env: {},
|
|
1639
|
-
});
|
|
1640
|
-
}
|
|
1641
|
-
finally {
|
|
1642
|
-
// Always clean up temp build dir
|
|
1643
|
-
try {
|
|
1644
|
-
execSync(`rm -rf "${buildDir}"`, { timeout: 5000 });
|
|
1645
|
-
}
|
|
1646
|
-
catch { }
|
|
1647
|
-
}
|
|
1648
|
-
if (!result.ok) {
|
|
1649
|
-
// Clean up dangling images from failed build
|
|
1650
|
-
try {
|
|
1651
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "prune", "-f"], { timeout: 15000, stdio: "ignore" });
|
|
1652
|
-
}
|
|
1653
|
-
catch { }
|
|
1654
|
-
emitTask(task, { type: "error", message: "Docker 镜像构建失败" });
|
|
1655
|
-
task.status = "error";
|
|
1656
|
-
return { ok: false, message: "Docker image build failed", error: result.output, taskId: task.id };
|
|
1657
|
-
}
|
|
1658
|
-
const localTag = "jishushell-openclaw:local";
|
|
1659
|
-
if (targetTag !== localTag) {
|
|
1660
|
-
try {
|
|
1661
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "tag", targetTag, localTag], { timeout: 10000, stdio: "ignore" });
|
|
1662
|
-
}
|
|
1663
|
-
catch { }
|
|
1664
|
-
}
|
|
1665
|
-
setOpenclawDockerImage(localTag);
|
|
1666
|
-
emitTask(task, { type: "done", message: `Docker 镜像构建完成: ${targetTag}`, progress: 100 });
|
|
1667
|
-
task.status = "done";
|
|
1668
|
-
return { ok: true, message: `Docker image ${targetTag} built`, taskId: task.id };
|
|
1669
|
-
}
|
|
1670
|
-
catch (e) {
|
|
1671
|
-
// Clean up build artifacts on unexpected errors
|
|
1672
|
-
try {
|
|
1673
|
-
execSync("docker image prune -f", { timeout: 15000, stdio: "ignore" });
|
|
1674
|
-
}
|
|
1675
|
-
catch { }
|
|
1676
|
-
emitTask(task, { type: "error", message: `Docker 镜像构建失败: ${e.message}` });
|
|
1677
|
-
task.status = "error";
|
|
1678
|
-
return { ok: false, message: "Docker image build failed", error: e.message, taskId: task.id };
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
export async function buildOpenclawDockerImage(tag) {
|
|
1682
|
-
const task = createTask("openclaw-docker");
|
|
1683
|
-
return buildOpenclawDockerImageWithTask(task, tag);
|
|
1684
|
-
}
|
|
1685
|
-
export function startBuildOpenclawDockerImage(tag) {
|
|
1686
|
-
const task = createTask("openclaw-docker");
|
|
1687
|
-
void buildOpenclawDockerImageWithTask(task, tag).catch((err) => {
|
|
1688
|
-
emitTask(task, { type: "error", message: `Docker 镜像构建失败: ${err?.message || err}` });
|
|
1689
|
-
task.status = "error";
|
|
1690
|
-
});
|
|
1691
|
-
return { ok: true, message: "Docker image build started", taskId: task.id };
|
|
1692
|
-
}
|
|
1693
|
-
// ── Build OpenClaw Docker image from npm package + Python ─────────
|
|
1694
|
-
async function buildCustomOpenclawImageWithTask(task, tag) {
|
|
1695
|
-
const targetTag = tag || DEFAULT_OPENCLAW_DOCKER_IMAGE;
|
|
1696
|
-
try {
|
|
1697
|
-
const invocation = resolveDockerInvocation();
|
|
1698
|
-
// Fast check: if image already exists locally, skip
|
|
1699
|
-
try {
|
|
1700
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "inspect", targetTag], {
|
|
1701
|
-
timeout: 10000,
|
|
1702
|
-
stdio: "ignore",
|
|
1703
|
-
});
|
|
1704
|
-
setOpenclawDockerImage(targetTag);
|
|
1705
|
-
emitTask(task, { type: "done", message: `Docker 镜像已存在: ${targetTag}`, progress: 100 });
|
|
1706
|
-
task.status = "done";
|
|
1707
|
-
return { ok: true, message: `Docker image ${targetTag} already exists`, taskId: task.id };
|
|
1708
|
-
}
|
|
1709
|
-
catch { /* image not found, proceed */ }
|
|
1710
|
-
// Clean up old legacy images
|
|
1711
|
-
const oldTag = getPanelConfig().openclaw_image;
|
|
1712
|
-
if (oldTag && oldTag !== targetTag && /^jishushell-base:/i.test(oldTag)) {
|
|
1713
|
-
try {
|
|
1714
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "rmi", oldTag], { timeout: 15000, stdio: "ignore" });
|
|
1715
|
-
}
|
|
1716
|
-
catch { }
|
|
1717
|
-
}
|
|
1718
|
-
// Step 1: Ensure OpenClaw npm package is installed (used as build context)
|
|
1719
|
-
const openclawPkgDir = join(OPENCLAW_MODULES, "openclaw");
|
|
1720
|
-
if (!existsSync(openclawPkgDir)) {
|
|
1721
|
-
emitTask(task, { type: "progress", message: "安装 OpenClaw npm 包...", progress: 5 });
|
|
1722
|
-
const installResult = await installOpenclaw();
|
|
1723
|
-
if (!installResult.ok) {
|
|
1724
|
-
emitTask(task, { type: "error", message: "OpenClaw 安装失败" });
|
|
1725
|
-
task.status = "error";
|
|
1726
|
-
return { ok: false, message: "OpenClaw npm install failed", error: installResult.error, taskId: task.id };
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
// Step 2: Ensure base image is available
|
|
1730
|
-
emitTask(task, { type: "progress", message: "准备基础镜像...", progress: 10 });
|
|
1731
|
-
const baseImageId = await ensureDockerBaseImage(invocation, task);
|
|
1732
|
-
// Step 3: Build image — COPY node_modules + Python
|
|
1733
|
-
emitTask(task, { type: "progress", message: `构建 OpenClaw 镜像(含 Python): ${targetTag} ...`, progress: 30 });
|
|
1734
|
-
const dockerfile = `FROM ${baseImageId}
|
|
1735
|
-
USER root
|
|
1736
|
-
RUN apt-get update && apt-get install -y --no-install-recommends \\
|
|
1737
|
-
procps hostname curl git lsof openssl \\
|
|
1738
|
-
python3 python3-pip python3-venv python3-dev && \\
|
|
1739
|
-
ln -sf /usr/bin/python3 /usr/local/bin/python && \\
|
|
1740
|
-
rm -rf /var/lib/apt/lists/*
|
|
1741
|
-
WORKDIR /app
|
|
1742
|
-
COPY --chown=node:node lib/node_modules/ ./node_modules/
|
|
1743
|
-
RUN ln -sf /app/node_modules/openclaw/openclaw.mjs /app/openclaw.mjs && \\
|
|
1744
|
-
ln -sf /app/node_modules/openclaw/openclaw.mjs /usr/local/bin/openclaw && \\
|
|
1745
|
-
cp /app/node_modules/openclaw/package.json /app/package.json 2>/dev/null || true
|
|
1746
|
-
USER node
|
|
1747
|
-
CMD ["node", "openclaw.mjs", "gateway", "--allow-unconfigured"]
|
|
1748
|
-
`;
|
|
1749
|
-
// Write Dockerfile into the npm package directory (build context)
|
|
1750
|
-
const dockerfilePath = join(OPENCLAW_PKG_DIR, "Dockerfile");
|
|
1751
|
-
writeConfigFile(dockerfilePath, dockerfile);
|
|
1752
|
-
let buildResult;
|
|
1753
|
-
try {
|
|
1754
|
-
buildResult = await spawnWithTask(task, invocation.cmd, [...invocation.argsPrefix, "build", "--network=host", "-t", targetTag, OPENCLAW_PKG_DIR], { timeout: 1800000, progressParser: dockerBuildProgressParser });
|
|
1755
|
-
}
|
|
1756
|
-
finally {
|
|
1757
|
-
try {
|
|
1758
|
-
unlinkSync(dockerfilePath);
|
|
1759
|
-
}
|
|
1760
|
-
catch { }
|
|
1761
|
-
}
|
|
1762
|
-
if (!buildResult.ok) {
|
|
1763
|
-
try {
|
|
1764
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "prune", "-f"], { timeout: 15000, stdio: "ignore" });
|
|
1765
|
-
}
|
|
1766
|
-
catch { }
|
|
1767
|
-
emitTask(task, { type: "error", message: "Docker 镜像构建失败" });
|
|
1768
|
-
task.status = "error";
|
|
1769
|
-
return { ok: false, message: "Docker image build failed", error: buildResult.output, taskId: task.id };
|
|
1770
|
-
}
|
|
1771
|
-
setOpenclawDockerImage(targetTag);
|
|
1772
|
-
emitTask(task, { type: "done", message: `OpenClaw 镜像就绪: ${targetTag}(含 Python)`, progress: 100 });
|
|
1773
|
-
task.status = "done";
|
|
1774
|
-
return { ok: true, message: `Docker image ${targetTag} built`, taskId: task.id };
|
|
1775
|
-
}
|
|
1776
|
-
catch (e) {
|
|
1777
|
-
emitTask(task, { type: "error", message: `镜像构建失败: ${e.message}` });
|
|
1778
|
-
task.status = "error";
|
|
1779
|
-
return { ok: false, message: "Docker image build failed", error: e.message, taskId: task.id };
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
// ── Pull or build OpenClaw Docker image ───────────────────────────
|
|
1783
|
-
/** Matches a semver-ish tag suffix, e.g. "...:2026.4.9" or "...:v1.2.3-beta". */
|
|
1784
|
-
const PINNED_IMAGE_TAG_RE = /:[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$/;
|
|
1785
|
-
/**
|
|
1786
|
-
* Query the npm registry for the current OpenClaw version. Used to bust the
|
|
1787
|
-
* Docker layer cache for `RUN npm install openclaw@${ver}` during local build.
|
|
1788
|
-
* Returns "latest" when npm is unreachable so the build can still proceed.
|
|
1789
|
-
*/
|
|
1790
|
-
function resolveOpenclawNpmVersion() {
|
|
1791
|
-
try {
|
|
1792
|
-
const out = execFileSync("npm", ["view", "openclaw", "version"], {
|
|
1793
|
-
timeout: 15000,
|
|
1794
|
-
encoding: "utf-8",
|
|
1795
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1796
|
-
}).trim();
|
|
1797
|
-
if (/^\d+\.\d+\.\d+/.test(out))
|
|
1798
|
-
return out;
|
|
1799
|
-
}
|
|
1800
|
-
catch { /* npm not reachable */ }
|
|
1801
|
-
return "latest";
|
|
1802
|
-
}
|
|
1803
|
-
/**
|
|
1804
|
-
* Read the OpenClaw version actually bundled at /app/ inside a Docker image,
|
|
1805
|
-
* bypassing `openclaw-entry.sh`'s `.npm-global/` override. This is the
|
|
1806
|
-
* authoritative source of truth — the image's OCI label can be wrong
|
|
1807
|
-
* (CI bugs, layer cache reuse), but `/app/node_modules/openclaw/package.json`
|
|
1808
|
-
* is the exact content that ran through `npm install`.
|
|
1809
|
-
*
|
|
1810
|
-
* Spawns a throw-away container with `--entrypoint node` so Node prints the
|
|
1811
|
-
* version directly. Returns "" when docker is unavailable or the path is
|
|
1812
|
-
* missing (e.g. a non-openclaw image).
|
|
1813
|
-
*/
|
|
1814
|
-
function readBundledOpenclawVersion(invocation, image) {
|
|
1815
|
-
try {
|
|
1816
|
-
const out = execFileSync(invocation.cmd, [
|
|
1817
|
-
...invocation.argsPrefix,
|
|
1818
|
-
"run", "--rm",
|
|
1819
|
-
"--entrypoint", "node",
|
|
1820
|
-
image,
|
|
1821
|
-
"-p",
|
|
1822
|
-
"require('/app/node_modules/openclaw/package.json').version",
|
|
1823
|
-
], { timeout: 20000, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1824
|
-
if (/^\d+\.\d+\.\d+/.test(out))
|
|
1825
|
-
return out;
|
|
1826
|
-
}
|
|
1827
|
-
catch { /* docker unavailable, image missing, or path not present */ }
|
|
1828
|
-
return "";
|
|
1829
|
-
}
|
|
1830
|
-
/**
|
|
1831
|
-
* After a successful pull or build, capture the image's real OpenClaw version
|
|
1832
|
-
* and return a pinned tag of the form `ghcr.io/.../openclaw-runtime:<version>`.
|
|
1833
|
-
* The pinned tag is added as a local alias via `docker tag` so subsequent
|
|
1834
|
-
* Nomad allocations see an immutable reference and never re-pull on restart.
|
|
1835
|
-
*
|
|
1836
|
-
* Version discovery order:
|
|
1837
|
-
* 1. `explicitVersion` when the caller already knows it (e.g. the local build
|
|
1838
|
-
* path, which queries npm for the version and passes it as `--build-arg`).
|
|
1839
|
-
* 2. Bundled `/app/node_modules/openclaw/package.json` inside the image
|
|
1840
|
-
* (authoritative — bypasses both the `.npm-global/` override layer and a
|
|
1841
|
-
* potentially stale OCI label).
|
|
1842
|
-
*
|
|
1843
|
-
* Returns the original tag unchanged when:
|
|
1844
|
-
* - the target is already a pinned version tag
|
|
1845
|
-
* - no version can be discovered
|
|
1846
|
-
* - docker tag fails for any reason
|
|
1847
|
-
*/
|
|
1848
|
-
function capturePinnedImageTag(invocation, targetTag, explicitVersion) {
|
|
1849
|
-
// Already pinned? Nothing to do.
|
|
1850
|
-
if (PINNED_IMAGE_TAG_RE.test(targetTag))
|
|
1851
|
-
return targetTag;
|
|
1852
|
-
let version = explicitVersion && /^\d+\.\d+\.\d+/.test(explicitVersion) ? explicitVersion : "";
|
|
1853
|
-
if (!version) {
|
|
1854
|
-
version = readBundledOpenclawVersion(invocation, targetTag);
|
|
1855
|
-
}
|
|
1856
|
-
if (!version || !/^\d+\.\d+\.\d+/.test(version))
|
|
1857
|
-
return targetTag;
|
|
1858
|
-
// Build the pinned tag by replacing the mutable tag portion.
|
|
1859
|
-
// "ghcr.io/foo/bar:latest" → "ghcr.io/foo/bar:2026.4.9"
|
|
1860
|
-
// "ghcr.io/foo/bar" → "ghcr.io/foo/bar:2026.4.9"
|
|
1861
|
-
const colonIdx = targetTag.lastIndexOf(":");
|
|
1862
|
-
const slashIdx = targetTag.lastIndexOf("/");
|
|
1863
|
-
const hasTag = colonIdx > slashIdx;
|
|
1864
|
-
const repo = hasTag ? targetTag.slice(0, colonIdx) : targetTag;
|
|
1865
|
-
const pinnedTag = `${repo}:${version}`;
|
|
1866
|
-
if (pinnedTag === targetTag)
|
|
1867
|
-
return targetTag;
|
|
1868
|
-
try {
|
|
1869
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "tag", targetTag, pinnedTag], { timeout: 10000, stdio: "ignore" });
|
|
1870
|
-
}
|
|
1871
|
-
catch {
|
|
1872
|
-
// Could not create the local alias — fall back to original tag.
|
|
1873
|
-
return targetTag;
|
|
1874
|
-
}
|
|
1875
|
-
// Drop the mutable original alias (`:latest` / `:slim`) now that the pinned
|
|
1876
|
-
// tag is in place. Removing a tag is cheap and leaves the underlying image
|
|
1877
|
-
// alive because the new pinned reference still points to it. Best-effort:
|
|
1878
|
-
// silent when the tag is already gone or in use.
|
|
1879
|
-
if (/:(latest|slim)$/.test(targetTag)) {
|
|
1880
|
-
try {
|
|
1881
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "rmi", targetTag], { timeout: 10000, stdio: "ignore" });
|
|
1882
|
-
}
|
|
1883
|
-
catch { /* best-effort cleanup */ }
|
|
1884
|
-
}
|
|
1885
|
-
return pinnedTag;
|
|
1886
|
-
}
|
|
1887
|
-
async function pullOrBuildOpenclawImageWithTask(task, tag) {
|
|
1888
|
-
const targetTag = tag || DEFAULT_OPENCLAW_DOCKER_IMAGE;
|
|
1889
|
-
try {
|
|
1890
|
-
const invocation = resolveDockerInvocation();
|
|
1891
|
-
// Fast check: if image already exists locally, skip
|
|
1892
|
-
try {
|
|
1893
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "inspect", targetTag], {
|
|
1894
|
-
timeout: 10000,
|
|
1895
|
-
stdio: "ignore",
|
|
1896
|
-
});
|
|
1897
|
-
const pinned = capturePinnedImageTag(invocation, targetTag);
|
|
1898
|
-
setOpenclawDockerImage(pinned);
|
|
1899
|
-
emitTask(task, { type: "done", message: `Docker 镜像已存在: ${pinned}`, progress: 100 });
|
|
1900
|
-
task.status = "done";
|
|
1901
|
-
return { ok: true, message: `Docker image ${pinned} already exists`, taskId: task.id };
|
|
1902
|
-
}
|
|
1903
|
-
catch { /* image not found, proceed */ }
|
|
1904
|
-
// ── Step 1: Try docker pull from registry ─────────────────────
|
|
1905
|
-
emitTask(task, { type: "progress", message: `正在拉取镜像: ${targetTag} ...`, progress: 10 });
|
|
1906
|
-
const pullResult = await spawnWithTask(task, invocation.cmd, [...invocation.argsPrefix, "pull", targetTag], { timeout: 600000 });
|
|
1907
|
-
if (pullResult.ok) {
|
|
1908
|
-
const pinned = capturePinnedImageTag(invocation, targetTag);
|
|
1909
|
-
setOpenclawDockerImage(pinned);
|
|
1910
|
-
emitTask(task, { type: "done", message: `镜像拉取成功: ${pinned}`, progress: 100 });
|
|
1911
|
-
task.status = "done";
|
|
1912
|
-
return { ok: true, message: `Docker image ${pinned} pulled`, taskId: task.id };
|
|
1913
|
-
}
|
|
1914
|
-
// ── Step 2: Fallback to local build ───────────────────────────
|
|
1915
|
-
console.log(`[setup] docker pull failed for ${targetTag}, falling back to local build...`);
|
|
1916
|
-
emitTask(task, { type: "progress", message: `拉取失败,正在本地构建镜像: ${targetTag} ...`, progress: 20 });
|
|
1917
|
-
const projectRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
1918
|
-
const dockerfilePath = join(projectRoot, "Dockerfile.openclaw-slim");
|
|
1919
|
-
if (!existsSync(dockerfilePath)) {
|
|
1920
|
-
emitTask(task, { type: "error", message: "Dockerfile.openclaw-slim not found, cannot fallback to local build" });
|
|
1921
|
-
task.status = "error";
|
|
1922
|
-
return { ok: false, message: "Docker pull failed and Dockerfile.openclaw-slim not found", taskId: task.id };
|
|
1923
|
-
}
|
|
1924
|
-
// Resolve the OpenClaw version from npm so the build-arg busts the Docker
|
|
1925
|
-
// layer cache for `RUN npm install openclaw@${ver}`. The Dockerfile's
|
|
1926
|
-
// ARG OPENCLAW_VERSION=latest default would otherwise cause the layer to
|
|
1927
|
-
// be silently reused across releases.
|
|
1928
|
-
const openclawVersion = resolveOpenclawNpmVersion();
|
|
1929
|
-
console.log(`[setup] building openclaw image with OPENCLAW_VERSION=${openclawVersion}`);
|
|
1930
|
-
const buildResult = await spawnWithTask(task, invocation.cmd, [
|
|
1931
|
-
...invocation.argsPrefix,
|
|
1932
|
-
"build",
|
|
1933
|
-
"--network=host",
|
|
1934
|
-
"--build-arg", `OPENCLAW_VERSION=${openclawVersion}`,
|
|
1935
|
-
"-f", dockerfilePath,
|
|
1936
|
-
"-t", targetTag,
|
|
1937
|
-
projectRoot,
|
|
1938
|
-
], { timeout: 1800000, progressParser: dockerBuildProgressParser });
|
|
1939
|
-
if (!buildResult.ok) {
|
|
1940
|
-
try {
|
|
1941
|
-
execFileSync(invocation.cmd, [...invocation.argsPrefix, "image", "prune", "-f"], { timeout: 15000, stdio: "ignore" });
|
|
1942
|
-
}
|
|
1943
|
-
catch { }
|
|
1944
|
-
emitTask(task, { type: "error", message: "Docker 镜像构建失败" });
|
|
1945
|
-
task.status = "error";
|
|
1946
|
-
return { ok: false, message: "Docker image build failed", error: buildResult.output, taskId: task.id };
|
|
1947
|
-
}
|
|
1948
|
-
// Local builds don't get labels from the GitHub Action's `labels:` field,
|
|
1949
|
-
// so pass the npm version we already know to let capturePinnedImageTag
|
|
1950
|
-
// mint the pinned tag without relying on docker inspect.
|
|
1951
|
-
const pinned = capturePinnedImageTag(invocation, targetTag, openclawVersion);
|
|
1952
|
-
setOpenclawDockerImage(pinned);
|
|
1953
|
-
emitTask(task, { type: "done", message: `OpenClaw 镜像就绪 (本地构建): ${pinned}`, progress: 100 });
|
|
1954
|
-
task.status = "done";
|
|
1955
|
-
return { ok: true, message: `Docker image ${pinned} built locally`, taskId: task.id };
|
|
1956
|
-
}
|
|
1957
|
-
catch (e) {
|
|
1958
|
-
emitTask(task, { type: "error", message: `镜像获取失败: ${e.message}` });
|
|
1959
|
-
task.status = "error";
|
|
1960
|
-
return { ok: false, message: "Docker image pull/build failed", error: e.message, taskId: task.id };
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1878
|
+
// ── Docker image build dispatch wrappers ───────────────────────────────
|
|
1879
|
+
//
|
|
1880
|
+
// All OpenClaw-specific image build logic now lives in
|
|
1881
|
+
// `OpenClawAdapter.buildRuntimeImage()` (§32.2.4 physical migration). These
|
|
1882
|
+
// exports remain as thin wrappers so `routes/setup.ts` and
|
|
1883
|
+
// `install.ts` callers don't need to learn adapter dispatch.
|
|
1884
|
+
/** Blocking: pull or build the OpenClaw docker image. */
|
|
1963
1885
|
export async function buildSlimOpenclawImage(tag) {
|
|
1964
|
-
const
|
|
1965
|
-
|
|
1886
|
+
const adapter = getAdapter("openclaw");
|
|
1887
|
+
if (typeof adapter.buildRuntimeImage !== "function") {
|
|
1888
|
+
return { ok: false, message: "OpenClawAdapter.buildRuntimeImage is not implemented" };
|
|
1889
|
+
}
|
|
1890
|
+
return adapter.buildRuntimeImage({ tag });
|
|
1966
1891
|
}
|
|
1892
|
+
/** Non-blocking: returns immediately with a task id for SSE polling. */
|
|
1967
1893
|
export function startBuildSlimOpenclawImage(tag) {
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
});
|
|
1973
|
-
return { ok: true, message: "Docker image pull started", taskId: task.id };
|
|
1894
|
+
const adapter = getAdapter("openclaw");
|
|
1895
|
+
if (typeof adapter.startBuildRuntimeImage !== "function") {
|
|
1896
|
+
return { ok: false, message: "OpenClawAdapter.startBuildRuntimeImage is not implemented" };
|
|
1897
|
+
}
|
|
1898
|
+
return adapter.startBuildRuntimeImage({ tag });
|
|
1974
1899
|
}
|
|
1975
1900
|
/** @deprecated Use buildSlimOpenclawImage instead */
|
|
1976
1901
|
export async function buildCustomOpenclawImage(tag) {
|
|
@@ -2021,7 +1946,10 @@ export async function runFullSetup(options = {}) {
|
|
|
2021
1946
|
}
|
|
2022
1947
|
}
|
|
2023
1948
|
}
|
|
2024
|
-
// Prepare
|
|
1949
|
+
// Prepare each registered agent's runtime artefacts (docker image, shim,
|
|
1950
|
+
// etc.). This loops over every adapter so onboarding a new agent only
|
|
1951
|
+
// requires implementing `buildRuntimeImage` or `installRuntime` in its
|
|
1952
|
+
// adapter — runFullSetup picks it up automatically.
|
|
2025
1953
|
if (defaults.buildDockerImage) {
|
|
2026
1954
|
// Restart Nomad so it re-detects Docker driver after Docker was installed
|
|
2027
1955
|
try {
|
|
@@ -2033,12 +1961,50 @@ export async function runFullSetup(options = {}) {
|
|
|
2033
1961
|
}
|
|
2034
1962
|
}
|
|
2035
1963
|
catch { }
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
1964
|
+
for (const adapter of listRegisteredAdapters()) {
|
|
1965
|
+
const stepName = `runtime-${adapter.agentType}`;
|
|
1966
|
+
// Prefer buildRuntimeImage (pure docker image prep) over
|
|
1967
|
+
// installRuntime (legacy host-mode npm install). Adapters that only
|
|
1968
|
+
// implement one will use whichever is present; adapters with neither
|
|
1969
|
+
// are skipped silently.
|
|
1970
|
+
const runner = adapter.buildRuntimeImage
|
|
1971
|
+
? adapter.buildRuntimeImage.bind(adapter)
|
|
1972
|
+
: adapter.installRuntime
|
|
1973
|
+
? adapter.installRuntime.bind(adapter)
|
|
1974
|
+
: null;
|
|
1975
|
+
if (!runner)
|
|
1976
|
+
continue;
|
|
1977
|
+
const isRequired = !!adapter.required;
|
|
1978
|
+
steps.push({
|
|
1979
|
+
step: stepName,
|
|
1980
|
+
status: "running",
|
|
1981
|
+
message: `Preparing ${adapter.agentType} runtime…`,
|
|
1982
|
+
});
|
|
1983
|
+
try {
|
|
1984
|
+
const imgResult = (await runner());
|
|
1985
|
+
const okStep = !!imgResult.ok;
|
|
1986
|
+
steps[steps.length - 1].status = okStep ? "done" : "error";
|
|
1987
|
+
steps[steps.length - 1].message = okStep
|
|
1988
|
+
? imgResult.message
|
|
1989
|
+
: isRequired
|
|
1990
|
+
? imgResult.message
|
|
1991
|
+
: `${imgResult.message} (optional — panel will continue)`;
|
|
1992
|
+
// Required adapter failures abort the overall setup; optional ones
|
|
1993
|
+
// are warnings so that upgrade paths don't break when a third-party
|
|
1994
|
+
// agent's registry is unreachable.
|
|
1995
|
+
if (!okStep && isRequired)
|
|
1996
|
+
allOk = false;
|
|
1997
|
+
}
|
|
1998
|
+
catch (e) {
|
|
1999
|
+
const msg = e?.message || "runtime prepare failed";
|
|
2000
|
+
steps[steps.length - 1].status = "error";
|
|
2001
|
+
steps[steps.length - 1].message = isRequired
|
|
2002
|
+
? msg
|
|
2003
|
+
: `${msg} (optional — panel will continue)`;
|
|
2004
|
+
if (isRequired)
|
|
2005
|
+
allOk = false;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2042
2008
|
}
|
|
2043
2009
|
if (isPortListening(4646)) {
|
|
2044
2010
|
await finalizeNomadStartup();
|