@flame0510/project-aether 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/agent-templates/README.md +42 -22
- package/agent-templates/base-image/Dockerfile +42 -33
- package/agent-templates/base-image/entrypoint.sh +67 -12
- package/app/agents/BrowserAccessSection.tsx +510 -0
- package/app/agents/ChannelManager.tsx +19 -11
- package/app/agents/ImageDownloadBanner.tsx +53 -19
- package/app/agents/ModelSection.tsx +4 -1
- package/app/agents/PageClient.tsx +629 -167
- package/app/agents/UpdateSection.tsx +300 -0
- package/app/agents/create/PageClient.tsx +11 -49
- package/app/agents/create/page.tsx +14 -28
- package/app/api/agents/[id]/backup/route.ts +26 -69
- package/app/api/agents/[id]/channels/pairing/route.ts +3 -3
- package/app/api/agents/[id]/channels/telegram/route.ts +2 -2
- package/app/api/agents/[id]/cold-backup/route.ts +56 -0
- package/app/api/agents/[id]/devices/route.ts +126 -0
- package/app/api/agents/[id]/invite-link/route.ts +53 -0
- package/app/api/agents/[id]/lifecycle/route.ts +3 -0
- package/app/api/agents/[id]/open-control-ui/route.ts +58 -0
- package/app/api/agents/[id]/recreate/route.ts +33 -163
- package/app/api/agents/[id]/restart/route.ts +5 -0
- package/app/api/agents/[id]/restore/route.ts +40 -70
- package/app/api/agents/[id]/route.ts +38 -150
- package/app/api/agents/[id]/update/rollback/route.ts +30 -0
- package/app/api/agents/[id]/update/route.ts +50 -0
- package/app/api/agents/activity-summary/route.ts +67 -0
- package/app/api/agents/create/route.ts +38 -92
- package/app/api/agents/devices-summary/route.ts +37 -0
- package/app/api/agents/download-image/route.ts +16 -9
- package/app/api/agents/image-status/route.ts +31 -111
- package/app/api/agents/route.ts +25 -49
- package/app/api/agents/token/route.ts +33 -10
- package/app/api/assistant/route.ts +2 -2
- package/app/api/gateway/agent/route.ts +14 -0
- package/app/api/gateway/provider/balance/route.ts +5 -2
- package/app/api/gateway/sync.ts +97 -14
- package/app/api/setup/agent-image/route.ts +14 -42
- package/app/api/version/route.ts +2 -1
- package/app/components/DashboardToolbar.tsx +1 -1
- package/app/gateway/PageClient.tsx +27 -32
- package/bin/rev4a.js +43 -41
- package/daemon.js +6 -6
- package/docs/ARCHITECTURE.md +107 -9
- package/docs/FRONTEND-ARCHITECTURE.md +8 -1
- package/docs/REV4A.md +54 -17
- package/docs/dev/API-REFERENCE.md +573 -105
- package/docs/dev/DATABASE.md +96 -0
- package/docs/dev/GATEWAY.md +21 -6
- package/docs/rag/DATA-FRESHNESS.md +6 -4
- package/docs/rag/GLOSSARY.md +12 -3
- package/docs/rag/REV4A-OVERVIEW.md +18 -5
- package/docs/rag/WHAT-I-CAN-ANSWER.md +6 -2
- package/instrumentation.ts +43 -0
- package/lib/agent-busy.ts +21 -0
- package/lib/agent-devices.ts +361 -0
- package/lib/agent-edit-state.ts +108 -0
- package/lib/agent-edit.ts +149 -0
- package/lib/agent-images.ts +375 -0
- package/lib/agent-ports-server.ts +78 -0
- package/lib/agent-ports.ts +91 -0
- package/lib/agent-recreate-state.ts +108 -0
- package/lib/agent-recreate.ts +359 -0
- package/lib/agent-restore-state.ts +107 -0
- package/lib/agent-restore.ts +141 -0
- package/lib/agent-setup.ts +66 -17
- package/lib/agent-update-state.ts +122 -0
- package/lib/agent-update.ts +456 -0
- package/lib/agent-versions.json +14 -0
- package/lib/agent-versions.ts +80 -0
- package/lib/buildAgentImage.ts +88 -290
- package/lib/channelManager.ts +149 -102
- package/lib/cold-backup.ts +354 -0
- package/lib/credentials/delivery.ts +3 -3
- package/lib/db-bootstrap.mjs +76 -0
- package/lib/docker-utils.ts +3 -3
- package/lib/provider-balance.ts +33 -12
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"localRepository": "openclaw-agent-base",
|
|
3
|
+
"registryRepository": "ghcr.io/flame0510/rev4a/openclaw-agent-base",
|
|
4
|
+
"supported": [
|
|
5
|
+
{
|
|
6
|
+
"version": "2026.9.3",
|
|
7
|
+
"modelInput": ["text", "image", "audio", "video"]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "2026.7.1-2",
|
|
11
|
+
"modelInput": ["text", "image"]
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenClaw versions this Rev4a release supports, newest first.
|
|
3
|
+
*
|
|
4
|
+
* The table lives in `agent-versions.json` so the `rev4a` CLI (plain CommonJS) reads
|
|
5
|
+
* the same list the server does. A new OpenClaw release is supported by adding its
|
|
6
|
+
* entry here together with the Dockerfile's `ARG OPENCLAW_VERSION`; nothing detects
|
|
7
|
+
* versions at runtime.
|
|
8
|
+
*
|
|
9
|
+
* `modelInput` is the model `input` list written into `models.json` for that version:
|
|
10
|
+
* OpenClaw validates it against the modalities it knows, so a value an older release
|
|
11
|
+
* does not know invalidates the whole catalogue.
|
|
12
|
+
*/
|
|
13
|
+
import table from './agent-versions.json';
|
|
14
|
+
|
|
15
|
+
export interface SupportedVersion {
|
|
16
|
+
version: string;
|
|
17
|
+
modelInput: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const SUPPORTED_VERSIONS: readonly SupportedVersion[] = table.supported;
|
|
21
|
+
|
|
22
|
+
/** Local image repository; agents run `openclaw-agent-base:<version>`. */
|
|
23
|
+
export const LOCAL_IMAGE_REPOSITORY: string = table.localRepository;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Where images are pulled from. `REV4A_AGENT_IMAGE_REGISTRY` points Rev4a at another
|
|
27
|
+
* registry with the same tags, such as a local `registry:2` for tests.
|
|
28
|
+
*/
|
|
29
|
+
export function registryRepository(): string {
|
|
30
|
+
return process.env.REV4A_AGENT_IMAGE_REGISTRY?.trim() || table.registryRepository;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const localImageRef = (version: string): string => `${LOCAL_IMAGE_REPOSITORY}:${version}`;
|
|
34
|
+
|
|
35
|
+
export const newestSupportedVersion = (): string => SUPPORTED_VERSIONS[0].version;
|
|
36
|
+
|
|
37
|
+
export const isSupportedVersion = (version: unknown): version is string =>
|
|
38
|
+
typeof version === 'string' && SUPPORTED_VERSIONS.some((v) => v.version === version);
|
|
39
|
+
|
|
40
|
+
/** Model `input` values every supported version accepts — the safe list when the version is unknown. */
|
|
41
|
+
export function conservativeModelInput(): string[] {
|
|
42
|
+
return SUPPORTED_VERSIONS.reduce<string[]>(
|
|
43
|
+
(acc, v, i) => (i === 0 ? [...v.modelInput] : acc.filter((x) => v.modelInput.includes(x))),
|
|
44
|
+
[],
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Model `input` values for a version. An unknown version gets the conservative
|
|
50
|
+
* intersection rather than the newest list: sending an older release a value it does
|
|
51
|
+
* not know discards its whole generated catalogue, so the fallback must be the set all
|
|
52
|
+
* supported versions accept.
|
|
53
|
+
*/
|
|
54
|
+
export function modelInputFor(version?: string | null): string[] {
|
|
55
|
+
const entry = version ? SUPPORTED_VERSIONS.find((v) => v.version === version) : undefined;
|
|
56
|
+
return entry ? entry.modelInput : conservativeModelInput();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const VERSION_RE = /(\d{4})\.(\d{1,2})\.(\d{1,2})(?:-(\d+))?/;
|
|
60
|
+
|
|
61
|
+
/** The version in `2026.9.3`, `2026.7.1-2` or `OpenClaw 2026.9.3 (1391f7c)`; null when there is none. */
|
|
62
|
+
export function parseOpenClawVersion(text: string | null | undefined): string | null {
|
|
63
|
+
const m = VERSION_RE.exec(text ?? '');
|
|
64
|
+
if (!m) return null;
|
|
65
|
+
return `${Number(m[1])}.${Number(m[2])}.${Number(m[3])}${m[4] !== undefined ? `-${Number(m[4])}` : ''}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Negative when `a` is older than `b`, zero when equal, positive when newer. Unparseable sorts oldest. */
|
|
69
|
+
export function compareVersions(a: string, b: string): number {
|
|
70
|
+
const parts = (v: string) => {
|
|
71
|
+
const m = VERSION_RE.exec(v);
|
|
72
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4] ?? 0)] : [-1, -1, -1, -1];
|
|
73
|
+
};
|
|
74
|
+
const pa = parts(a);
|
|
75
|
+
const pb = parts(b);
|
|
76
|
+
for (let i = 0; i < pa.length; i++) {
|
|
77
|
+
if (pa[i] !== pb[i]) return pa[i] - pb[i];
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
package/lib/buildAgentImage.ts
CHANGED
|
@@ -1,75 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* buildAgentImage.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* the
|
|
4
|
+
* Downloading the agent base image for one OpenClaw version. Used by
|
|
5
|
+
* POST /api/agents/download-image (the Agents page banner) and
|
|
6
|
+
* POST /api/setup/agent-image (the setup wizard, streamed).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* The image is pulled from the registry as `<registry>:<version>` and kept as
|
|
9
|
+
* `openclaw-agent-base:<version>` (see lib/agent-images.ts). When the pull fails and
|
|
10
|
+
* this repository's Dockerfile builds that same version, the image is built locally
|
|
11
|
+
* instead — development without registry access. Any other version cannot be built
|
|
12
|
+
* from here, so its download fails.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
* page refreshes and modal closes.
|
|
14
|
+
* One download at a time (in-process lock). Output goes to
|
|
15
|
+
* /tmp/rev4a-download-<timestamp>.log so it survives page refreshes and modal closes.
|
|
14
16
|
*/
|
|
15
|
-
import * as crypto from 'crypto';
|
|
16
17
|
import * as fs from 'fs';
|
|
17
18
|
import * as path from 'path';
|
|
18
|
-
import {
|
|
19
|
+
import { isSupportedVersion, localImageRef, newestSupportedVersion } from '@/lib/agent-versions';
|
|
20
|
+
import { localVersionExists, pullAgentImage, remoteSupportedVersions, runDocker } from '@/lib/agent-images';
|
|
21
|
+
import { dockerAvailable } from '@/lib/docker-socket';
|
|
19
22
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
23
|
+
const DOCKERFILE_DIR = path.join(process.cwd(), 'agent-templates', 'base-image');
|
|
24
|
+
const BUILD_TIMEOUT_MS = 60 * 60 * 1000;
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
/** The version being downloaded; `pending` while the default version is being resolved. */
|
|
27
|
+
let downloadingVersion: string | null = null;
|
|
24
28
|
let downloadLogPath: string | null = null;
|
|
25
|
-
let downloadChildProcess: ReturnType<(typeof import('child_process'))['exec']> | null = null;
|
|
26
|
-
|
|
27
|
-
/** Check whether the docker CLI is available and functional. */
|
|
28
|
-
function dockerAvailable(): boolean {
|
|
29
|
-
try {
|
|
30
|
-
execSyncRaw('docker version --format "{{.Client.Version}}" 2>/dev/null', {
|
|
31
|
-
encoding: 'utf-8',
|
|
32
|
-
timeout: 3000,
|
|
33
|
-
});
|
|
34
|
-
return true;
|
|
35
|
-
} catch {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Returns the SHA256 hash of the current Dockerfile. */
|
|
41
|
-
export function getDockerfileHash(): string | null {
|
|
42
|
-
try {
|
|
43
|
-
const dockerfile = path.join(process.cwd(), 'agent-templates', 'base-image', 'Dockerfile');
|
|
44
|
-
if (!fs.existsSync(dockerfile)) return null;
|
|
45
|
-
const content = fs.readFileSync(dockerfile, 'utf-8');
|
|
46
|
-
return crypto.createHash('sha256').update(content).digest('hex');
|
|
47
|
-
} catch {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
29
|
|
|
53
30
|
/** Checks if a download is already running (in-process flag only). */
|
|
54
31
|
export function getIsDownloading(): boolean {
|
|
55
|
-
return
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Acquire the lock atomically. Returns false if already locked. */
|
|
59
|
-
function tryAcquireLock(): boolean {
|
|
60
|
-
if (getIsDownloading()) return false;
|
|
61
|
-
isDownloading = true;
|
|
62
|
-
return true;
|
|
32
|
+
return downloadingVersion !== null;
|
|
63
33
|
}
|
|
64
34
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
isDownloading = false;
|
|
68
|
-
downloadLogPath = null;
|
|
69
|
-
downloadChildProcess = null;
|
|
35
|
+
export function getDownloadingVersion(): string | null {
|
|
36
|
+
return downloadingVersion === 'pending' ? null : downloadingVersion;
|
|
70
37
|
}
|
|
71
38
|
|
|
72
|
-
|
|
73
39
|
function appendLog(msg: string): void {
|
|
74
40
|
if (!downloadLogPath) return;
|
|
75
41
|
try {
|
|
@@ -81,270 +47,102 @@ function appendLog(msg: string): void {
|
|
|
81
47
|
function cleanupOldLogs(): void {
|
|
82
48
|
try {
|
|
83
49
|
const tmpDir = '/tmp';
|
|
84
|
-
const files = fs.readdirSync(tmpDir);
|
|
85
50
|
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
86
|
-
for (const f of
|
|
51
|
+
for (const f of fs.readdirSync(tmpDir)) {
|
|
87
52
|
if (!f.startsWith('rev4a-download-') || !f.endsWith('.log')) continue;
|
|
88
53
|
const fullPath = path.join(tmpDir, f);
|
|
89
|
-
|
|
90
|
-
if (stat.mtimeMs < cutoff) {
|
|
91
|
-
fs.unlinkSync(fullPath);
|
|
92
|
-
}
|
|
54
|
+
if (fs.statSync(fullPath).mtimeMs < cutoff) fs.unlinkSync(fullPath);
|
|
93
55
|
}
|
|
94
56
|
} catch { /* ignore */ }
|
|
95
57
|
}
|
|
96
58
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
onEvent?: (data: string) => void;
|
|
100
|
-
signal?: AbortSignal;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function localImageExists(): boolean {
|
|
104
|
-
if (!dockerAvailable()) return false;
|
|
59
|
+
/** The OpenClaw version this repository's Dockerfile builds (`ARG OPENCLAW_VERSION`). */
|
|
60
|
+
export function dockerfileVersion(): string | null {
|
|
105
61
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{ timeout: 3000 },
|
|
109
|
-
);
|
|
110
|
-
return true;
|
|
111
|
-
} catch {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function getLocalImageId(): string | null {
|
|
117
|
-
if (!dockerAvailable()) return null;
|
|
118
|
-
try {
|
|
119
|
-
return execSyncRaw(
|
|
120
|
-
`docker image inspect ${LOCAL_IMAGE} --format "{{.ID}}" 2>/dev/null`,
|
|
121
|
-
{ encoding: 'utf-8', timeout: 3000 },
|
|
122
|
-
).trim() || null;
|
|
62
|
+
const content = fs.readFileSync(path.join(DOCKERFILE_DIR, 'Dockerfile'), 'utf-8');
|
|
63
|
+
return /^ARG OPENCLAW_VERSION=(\S+)/m.exec(content)?.[1] ?? null;
|
|
123
64
|
} catch {
|
|
124
65
|
return null;
|
|
125
66
|
}
|
|
126
67
|
}
|
|
127
68
|
|
|
128
|
-
/**
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
*/
|
|
132
|
-
function getRemoteDigest(): string | null {
|
|
133
|
-
if (!dockerAvailable()) return null;
|
|
134
|
-
try {
|
|
135
|
-
const out = execSyncRaw(
|
|
136
|
-
`docker manifest inspect ${REGISTRY_IMAGE} 2>/dev/null`,
|
|
137
|
-
{ encoding: 'utf-8', timeout: 15000 },
|
|
138
|
-
);
|
|
139
|
-
const manifest = JSON.parse(out);
|
|
140
|
-
return manifest?.config?.digest || null;
|
|
141
|
-
} catch {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
69
|
+
/** What a download without an explicit version fetches: the newest supported version the registry publishes, else the newest supported. */
|
|
70
|
+
export async function defaultDownloadVersion(): Promise<string> {
|
|
71
|
+
return (await remoteSupportedVersions())?.[0] ?? newestSupportedVersion();
|
|
144
72
|
}
|
|
145
73
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
opts: DownloadOptions,
|
|
151
|
-
emit: (data: string) => void,
|
|
152
|
-
): Promise<boolean> {
|
|
153
|
-
if (!dockerAvailable()) {
|
|
154
|
-
emit(JSON.stringify({ type: 'error', message: 'Docker is not available' }));
|
|
155
|
-
appendLog('Docker not available — cannot download image');
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
emit(JSON.stringify({ type: 'status', message: `Downloading ${REGISTRY_IMAGE}...` }));
|
|
160
|
-
appendLog(`Downloading from registry: ${REGISTRY_IMAGE}`);
|
|
161
|
-
|
|
162
|
-
return new Promise((resolve) => {
|
|
163
|
-
const { exec } = require('child_process');
|
|
164
|
-
const child = exec(
|
|
165
|
-
`docker pull ${REGISTRY_IMAGE}`,
|
|
166
|
-
{ maxBuffer: 1024 * 1024 * 5, timeout: 300_000 },
|
|
167
|
-
(err: Error | null) => {
|
|
168
|
-
if (err) {
|
|
169
|
-
appendLog(`Download from registry failed: ${err.message}`);
|
|
170
|
-
emit(JSON.stringify({ type: 'log', message: 'Registry download failed, trying local build...' }));
|
|
171
|
-
resolve(false);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
execSyncRaw(`docker tag ${REGISTRY_IMAGE} ${LOCAL_IMAGE}`, { timeout: 10_000 });
|
|
177
|
-
} catch { /* ignore tag error */ }
|
|
178
|
-
appendLog('Download completed successfully');
|
|
179
|
-
emit(JSON.stringify({ type: 'complete', message: 'Image downloaded from registry' }));
|
|
180
|
-
resolve(true);
|
|
181
|
-
},
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
downloadChildProcess = child;
|
|
185
|
-
|
|
186
|
-
child.stderr?.on('data', (chunk: Buffer) => {
|
|
187
|
-
appendLog(chunk.toString());
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
child.stdout?.on('data', (chunk: Buffer) => {
|
|
191
|
-
appendLog(chunk.toString());
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
child.on('error', (err: Error) => {
|
|
195
|
-
appendLog(`Pull process error: ${err.message}`);
|
|
196
|
-
// Do NOT release the lock here: the caller falls through to
|
|
197
|
-
// localBuild(), which must stay protected. Releasing early let a
|
|
198
|
-
// concurrent request past getIsDownloading() and start a second
|
|
199
|
-
// `docker build` against the same tag. The lock is released by
|
|
200
|
-
// localBuild's own close/error handlers, or by the success path.
|
|
201
|
-
resolve(false);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
74
|
+
export interface DownloadOptions {
|
|
75
|
+
version?: string;
|
|
76
|
+
onEvent?: (data: string) => void;
|
|
77
|
+
signal?: AbortSignal;
|
|
204
78
|
}
|
|
205
79
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
function localBuild(
|
|
210
|
-
opts: DownloadOptions,
|
|
211
|
-
emit: (data: string) => void,
|
|
212
|
-
): Promise<void> {
|
|
213
|
-
return new Promise((resolve, reject) => {
|
|
214
|
-
const dockerfileDir = path.join(process.cwd(), 'agent-templates', 'base-image');
|
|
215
|
-
const dockerfile = path.join(dockerfileDir, 'Dockerfile');
|
|
216
|
-
|
|
217
|
-
if (!fs.existsSync(dockerfile)) {
|
|
218
|
-
const msg = 'Dockerfile not found — cannot build locally';
|
|
219
|
-
emit(JSON.stringify({ type: 'error', message: msg }));
|
|
220
|
-
appendLog(msg);
|
|
221
|
-
releaseLock();
|
|
222
|
-
reject(new Error(msg));
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const hash = getDockerfileHash();
|
|
227
|
-
const noCache = opts.force ? '--no-cache ' : '';
|
|
228
|
-
const cmd = hash
|
|
229
|
-
? `docker build ${noCache}-t ${LOCAL_IMAGE} --label "rev4a.dockerfile_hash=${hash}" --progress=plain .`
|
|
230
|
-
: `docker build ${noCache}-t ${LOCAL_IMAGE} --progress=plain .`;
|
|
231
|
-
|
|
232
|
-
appendLog('');
|
|
233
|
-
appendLog('=== Local build started (registry download failed) ===');
|
|
234
|
-
appendLog(`Dockerfile hash: ${hash || 'unknown'}`);
|
|
235
|
-
appendLog('');
|
|
236
|
-
|
|
237
|
-
emit(JSON.stringify({ type: 'status', message: 'Building agent base image locally...' }));
|
|
238
|
-
|
|
239
|
-
const { exec } = require('child_process');
|
|
240
|
-
const child = exec(cmd, {
|
|
241
|
-
cwd: dockerfileDir,
|
|
242
|
-
maxBuffer: 1024 * 1024 * 10,
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
downloadChildProcess = child;
|
|
246
|
-
|
|
247
|
-
child.stdout?.on('data', (chunk: string) => {
|
|
248
|
-
const lines = chunk.toString().split('\n').filter(Boolean);
|
|
249
|
-
for (const line of lines) {
|
|
250
|
-
appendLog(line);
|
|
251
|
-
if (line.startsWith('#')) {
|
|
252
|
-
emit(JSON.stringify({ type: 'step', message: line }));
|
|
253
|
-
} else if (line.includes('Downloading') || line.includes('Extracting') || line.includes('Pulling')) {
|
|
254
|
-
emit(JSON.stringify({ type: 'progress', message: line }));
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
child.stderr?.on('data', (chunk: string) => {
|
|
260
|
-
const text = chunk.toString().trim();
|
|
261
|
-
if (text) {
|
|
262
|
-
appendLog(text);
|
|
263
|
-
emit(JSON.stringify({ type: 'log', message: text }));
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
child.on('close', (code: number) => {
|
|
268
|
-
if (code === 0) {
|
|
269
|
-
appendLog('=== Build completed successfully ===');
|
|
270
|
-
emit(JSON.stringify({ type: 'complete', message: 'Agent base image built successfully' }));
|
|
271
|
-
releaseLock();
|
|
272
|
-
resolve();
|
|
273
|
-
} else {
|
|
274
|
-
appendLog(`=== Build failed with exit code ${code} ===`);
|
|
275
|
-
releaseLock();
|
|
276
|
-
reject(new Error(`docker build exited with code ${code}`));
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
child.on('error', (err: Error) => {
|
|
281
|
-
appendLog(`=== Build error: ${err.message} ===`);
|
|
282
|
-
releaseLock();
|
|
283
|
-
reject(err);
|
|
284
|
-
});
|
|
285
|
-
});
|
|
80
|
+
export interface DownloadResult {
|
|
81
|
+
version: string;
|
|
82
|
+
source: 'present' | 'registry' | 'build';
|
|
286
83
|
}
|
|
287
84
|
|
|
288
85
|
/**
|
|
289
|
-
*
|
|
86
|
+
* Download one OpenClaw version of the agent image. Emits JSON events
|
|
87
|
+
* (`status`, `progress`, `step`, `log`, `complete`) through `onEvent`; failures are
|
|
88
|
+
* thrown, not emitted.
|
|
290
89
|
*/
|
|
291
|
-
export async function downloadAgentImage(opts: DownloadOptions): Promise<
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
onEvent?.(JSON.stringify({ type: 'error', message: msg }));
|
|
297
|
-
throw new Error(msg);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (!tryAcquireLock()) {
|
|
301
|
-
throw new Error('A download is already in progress');
|
|
302
|
-
}
|
|
90
|
+
export async function downloadAgentImage(opts: DownloadOptions = {}): Promise<DownloadResult> {
|
|
91
|
+
const event = (type: string, message: string) => {
|
|
92
|
+
opts.onEvent?.(JSON.stringify({ type, message }));
|
|
93
|
+
appendLog(message);
|
|
94
|
+
};
|
|
303
95
|
|
|
304
|
-
|
|
305
|
-
|
|
96
|
+
if (!dockerAvailable()) throw new Error('Docker is not available — please install Docker and try again');
|
|
97
|
+
if (downloadingVersion !== null) throw new Error('A download is already in progress');
|
|
98
|
+
downloadingVersion = opts.version ?? 'pending';
|
|
99
|
+
downloadLogPath = `/tmp/rev4a-download-${Date.now()}.log`;
|
|
306
100
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (parsed.message) appendLog(parsed.message);
|
|
312
|
-
} catch { /* not JSON */ }
|
|
313
|
-
};
|
|
101
|
+
try {
|
|
102
|
+
const version = opts.version ?? (await defaultDownloadVersion());
|
|
103
|
+
if (!isSupportedVersion(version)) throw new Error(`OpenClaw ${version} is not supported by this Rev4a`);
|
|
104
|
+
downloadingVersion = version;
|
|
314
105
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (!remoteDigest) {
|
|
319
|
-
emit(JSON.stringify({ type: 'complete', message: 'Image already downloaded (offline, cannot check for updates)' }));
|
|
320
|
-
releaseLock();
|
|
321
|
-
return;
|
|
106
|
+
if (await localVersionExists(version)) {
|
|
107
|
+
event('complete', `OpenClaw ${version} image already downloaded`);
|
|
108
|
+
return { version, source: 'present' };
|
|
322
109
|
}
|
|
323
110
|
|
|
324
|
-
|
|
325
|
-
releaseLock();
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Force: remove local image first so we get a fresh pull
|
|
330
|
-
if (force) {
|
|
111
|
+
event('status', `Downloading OpenClaw ${version} image…`);
|
|
331
112
|
try {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
113
|
+
await pullAgentImage(version, {
|
|
114
|
+
signal: opts.signal,
|
|
115
|
+
onLine: (line) => {
|
|
116
|
+
appendLog(line);
|
|
117
|
+
if (/Download|Extract|Pull/.test(line)) opts.onEvent?.(JSON.stringify({ type: 'progress', message: line }));
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
event('complete', `OpenClaw ${version} image downloaded`);
|
|
121
|
+
return { version, source: 'registry' };
|
|
122
|
+
} catch (e) {
|
|
123
|
+
appendLog(`Registry download failed: ${(e as Error).message}`);
|
|
124
|
+
if (opts.signal?.aborted || dockerfileVersion() !== version) {
|
|
125
|
+
throw new Error(`Could not download OpenClaw ${version}: ${(e as Error).message}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
340
128
|
|
|
341
|
-
|
|
129
|
+
event('log', 'Registry download failed, building the image locally…');
|
|
130
|
+
await runDocker(
|
|
131
|
+
['build', '--build-arg', `OPENCLAW_VERSION=${version}`, '-t', localImageRef(version), '--progress=plain', DOCKERFILE_DIR],
|
|
132
|
+
{
|
|
133
|
+
signal: opts.signal,
|
|
134
|
+
timeoutMs: BUILD_TIMEOUT_MS,
|
|
135
|
+
onLine: (line) => {
|
|
136
|
+
appendLog(line);
|
|
137
|
+
if (line.startsWith('#')) opts.onEvent?.(JSON.stringify({ type: 'step', message: line }));
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
event('complete', `OpenClaw ${version} image built locally`);
|
|
142
|
+
return { version, source: 'build' };
|
|
143
|
+
} finally {
|
|
144
|
+
downloadingVersion = null;
|
|
145
|
+
downloadLogPath = null;
|
|
342
146
|
cleanupOldLogs();
|
|
343
|
-
releaseLock();
|
|
344
|
-
return;
|
|
345
147
|
}
|
|
346
|
-
|
|
347
|
-
// Fallback: local build
|
|
348
|
-
await localBuild(opts, emit);
|
|
349
|
-
cleanupOldLogs();
|
|
350
148
|
}
|