@riddledc/openclaw-riddledc 0.9.2 → 0.9.4
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/CHECKSUMS.txt +7 -2
- package/README.md +10 -0
- package/dist/chunk-SNGACMAL.js +858 -0
- package/dist/core.cjs +897 -0
- package/dist/core.d.cts +57 -0
- package/dist/core.d.ts +57 -0
- package/dist/core.js +36 -0
- package/dist/index.cjs +318 -175
- package/dist/index.js +36 -101
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -2
package/dist/core.cjs
ADDED
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/core.ts
|
|
21
|
+
var core_exports = {};
|
|
22
|
+
__export(core_exports, {
|
|
23
|
+
applySafetySpec: () => applySafetySpec,
|
|
24
|
+
assertAllowedBaseUrl: () => assertAllowedBaseUrl,
|
|
25
|
+
configFromOpenClawApi: () => configFromOpenClawApi,
|
|
26
|
+
createBuildPreview: () => createBuildPreview,
|
|
27
|
+
createServerPreview: () => createServerPreview,
|
|
28
|
+
createStaticPreview: () => createStaticPreview,
|
|
29
|
+
deleteStaticPreview: () => deleteStaticPreview,
|
|
30
|
+
describeError: () => describeError,
|
|
31
|
+
fetchArtifactsAndBuild: () => fetchArtifactsAndBuild,
|
|
32
|
+
fetchWithRetry: () => fetchWithRetry,
|
|
33
|
+
fetchWithTimeout: () => fetchWithTimeout,
|
|
34
|
+
isAlreadyStartedResponse: () => isAlreadyStartedResponse,
|
|
35
|
+
pollJobStatus: () => pollJobStatus,
|
|
36
|
+
riddleApiFetch: () => riddleApiFetch,
|
|
37
|
+
runWithDefaults: () => runWithDefaults,
|
|
38
|
+
writeArtifactBinary: () => writeArtifactBinary
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(core_exports);
|
|
41
|
+
var import_node_child_process = require("child_process");
|
|
42
|
+
var import_promises = require("fs/promises");
|
|
43
|
+
var import_node_path = require("path");
|
|
44
|
+
var import_node_util = require("util");
|
|
45
|
+
var execFile = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
46
|
+
var INLINE_CAP = 50 * 1024;
|
|
47
|
+
var PREVIEW_REQUEST_TIMEOUT_MS = 3e4;
|
|
48
|
+
var PREVIEW_UPLOAD_TIMEOUT_MS = 5 * 6e4;
|
|
49
|
+
var PREVIEW_ARTIFACT_TIMEOUT_MS = 6e4;
|
|
50
|
+
var PREVIEW_RETRY_ATTEMPTS = 3;
|
|
51
|
+
var PREVIEW_RETRY_BASE_DELAY_MS = 750;
|
|
52
|
+
function configFromOpenClawApi(api) {
|
|
53
|
+
const cfg = api?.config ?? {};
|
|
54
|
+
const pluginCfg = cfg?.plugins?.entries?.["openclaw-riddledc"]?.config ?? {};
|
|
55
|
+
return {
|
|
56
|
+
apiKey: process.env.RIDDLE_API_KEY || pluginCfg.apiKey,
|
|
57
|
+
baseUrl: pluginCfg.baseUrl || "https://api.riddledc.com",
|
|
58
|
+
workspace: api?.workspacePath ?? process.cwd()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function requireConfig(config) {
|
|
62
|
+
const apiKey = config.apiKey || process.env.RIDDLE_API_KEY;
|
|
63
|
+
const baseUrl = config.baseUrl || "https://api.riddledc.com";
|
|
64
|
+
if (!apiKey) {
|
|
65
|
+
throw new Error("Missing Riddle API key. Set RIDDLE_API_KEY env var or configure a Riddle API key.");
|
|
66
|
+
}
|
|
67
|
+
assertAllowedBaseUrl(baseUrl);
|
|
68
|
+
return { apiKey, baseUrl, workspace: config.workspace || process.cwd() };
|
|
69
|
+
}
|
|
70
|
+
function assertAllowedBaseUrl(baseUrl) {
|
|
71
|
+
const url = new URL(baseUrl);
|
|
72
|
+
if (url.protocol !== "https:") throw new Error(`Riddle baseUrl must be https: (${baseUrl})`);
|
|
73
|
+
if (url.hostname !== "api.riddledc.com") {
|
|
74
|
+
throw new Error(`Refusing to use non-official Riddle host: ${url.hostname}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function detectMode(payload) {
|
|
78
|
+
if (payload.url) return "url";
|
|
79
|
+
if (payload.urls) return "urls";
|
|
80
|
+
if (payload.steps) return "steps";
|
|
81
|
+
if (payload.script) return "script";
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
async function postRun(baseUrl, apiKey, payload) {
|
|
85
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/run`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${apiKey}`,
|
|
89
|
+
"Content-Type": "application/json"
|
|
90
|
+
},
|
|
91
|
+
body: JSON.stringify(payload)
|
|
92
|
+
});
|
|
93
|
+
const body = await res.arrayBuffer();
|
|
94
|
+
return {
|
|
95
|
+
contentType: res.headers.get("content-type"),
|
|
96
|
+
body,
|
|
97
|
+
headers: res.headers,
|
|
98
|
+
status: res.status
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function abToBase64(ab) {
|
|
102
|
+
return Buffer.from(ab).toString("base64");
|
|
103
|
+
}
|
|
104
|
+
function describeError(err) {
|
|
105
|
+
const anyErr = err;
|
|
106
|
+
const parts = [];
|
|
107
|
+
if (err instanceof Error) parts.push(err.message);
|
|
108
|
+
else parts.push(String(err));
|
|
109
|
+
const cause = anyErr?.cause;
|
|
110
|
+
if (cause) {
|
|
111
|
+
const causeParts = [
|
|
112
|
+
cause.code ? `code=${cause.code}` : "",
|
|
113
|
+
cause.name ? `name=${cause.name}` : "",
|
|
114
|
+
cause.message ? `message=${cause.message}` : ""
|
|
115
|
+
].filter(Boolean);
|
|
116
|
+
if (causeParts.length) parts.push(`cause: ${causeParts.join(" ")}`);
|
|
117
|
+
}
|
|
118
|
+
return parts.join("; ");
|
|
119
|
+
}
|
|
120
|
+
async function fetchWithTimeout(url, init, timeoutMs, label) {
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
123
|
+
try {
|
|
124
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
if (err?.name === "AbortError") {
|
|
127
|
+
throw new Error(`${label} timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
128
|
+
}
|
|
129
|
+
throw err;
|
|
130
|
+
} finally {
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isTransientFetchError(err) {
|
|
135
|
+
const text = describeError(err).toLowerCase();
|
|
136
|
+
return [
|
|
137
|
+
"fetch failed",
|
|
138
|
+
"timed out",
|
|
139
|
+
"timeout",
|
|
140
|
+
"econnreset",
|
|
141
|
+
"econnrefused",
|
|
142
|
+
"etimedout",
|
|
143
|
+
"eai_again",
|
|
144
|
+
"socket",
|
|
145
|
+
"network",
|
|
146
|
+
"und_err",
|
|
147
|
+
"terminated"
|
|
148
|
+
].some((needle) => text.includes(needle));
|
|
149
|
+
}
|
|
150
|
+
function sleep(ms) {
|
|
151
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
152
|
+
}
|
|
153
|
+
async function fetchWithRetry(url, init, timeoutMs, label, opts = {}) {
|
|
154
|
+
const attempts = Math.max(1, opts.attempts ?? PREVIEW_RETRY_ATTEMPTS);
|
|
155
|
+
const baseDelayMs = opts.baseDelayMs ?? PREVIEW_RETRY_BASE_DELAY_MS;
|
|
156
|
+
let lastErr;
|
|
157
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
158
|
+
try {
|
|
159
|
+
return await fetchWithTimeout(url, init, timeoutMs, label);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
lastErr = err;
|
|
162
|
+
if (attempt >= attempts || !isTransientFetchError(err)) break;
|
|
163
|
+
const jitterMs = Math.floor(Math.random() * 250);
|
|
164
|
+
const delayMs = Math.min(baseDelayMs * Math.pow(2, attempt - 1) + jitterMs, 5e3);
|
|
165
|
+
console.warn(`[openclaw-riddledc] ${label} attempt ${attempt}/${attempts} failed: ${describeError(err)}; retrying in ${delayMs}ms`);
|
|
166
|
+
await sleep(delayMs);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw new Error(`${label} failed after ${attempts} attempts: ${describeError(lastErr)}`);
|
|
170
|
+
}
|
|
171
|
+
function isAlreadyStartedResponse(status, body) {
|
|
172
|
+
return status === 409 && /already in status:\s*(queued|running|complete|completed)/i.test(body);
|
|
173
|
+
}
|
|
174
|
+
function previewTimeoutResult(jobId, timeoutMs, lastStatusData, resultExtras = () => ({})) {
|
|
175
|
+
const lastStatus = lastStatusData?.status ?? "unknown";
|
|
176
|
+
const lastPhase = lastStatusData?.phase ?? lastStatus;
|
|
177
|
+
const result = {
|
|
178
|
+
ok: false,
|
|
179
|
+
job_id: jobId,
|
|
180
|
+
status: lastStatus,
|
|
181
|
+
phase: lastPhase,
|
|
182
|
+
phase_updated_at: lastStatusData?.phase_updated_at,
|
|
183
|
+
phase_details: lastStatusData?.phase_details,
|
|
184
|
+
outputs: lastStatusData?.outputs || [],
|
|
185
|
+
compute_seconds: lastStatusData?.compute_seconds,
|
|
186
|
+
egress_bytes: lastStatusData?.egress_bytes,
|
|
187
|
+
error: `Job did not complete within ${timeoutMs / 1e3}s; last status was ${lastStatus}, phase was ${lastPhase}`,
|
|
188
|
+
...resultExtras(lastStatusData ?? {})
|
|
189
|
+
};
|
|
190
|
+
if (lastStatusData?.error) result.server_error = lastStatusData.error;
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
async function writeArtifact(workspace, subdir, filename, content) {
|
|
194
|
+
const dir = (0, import_node_path.join)(workspace, "riddle", subdir);
|
|
195
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
196
|
+
const filePath = (0, import_node_path.join)(dir, filename);
|
|
197
|
+
const buf = Buffer.from(content, "utf8");
|
|
198
|
+
await (0, import_promises.writeFile)(filePath, buf);
|
|
199
|
+
return { path: filePath, sizeBytes: buf.byteLength };
|
|
200
|
+
}
|
|
201
|
+
async function writeArtifactBinary(workspace, subdir, filename, base64Content) {
|
|
202
|
+
const dir = (0, import_node_path.join)(workspace, "riddle", subdir);
|
|
203
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
204
|
+
const filePath = (0, import_node_path.join)(dir, filename);
|
|
205
|
+
const buf = Buffer.from(base64Content, "base64");
|
|
206
|
+
await (0, import_promises.writeFile)(filePath, buf);
|
|
207
|
+
return { path: filePath, sizeBytes: buf.byteLength };
|
|
208
|
+
}
|
|
209
|
+
async function applySafetySpec(result, opts) {
|
|
210
|
+
const jobId = result.job_id ?? "unknown";
|
|
211
|
+
if (result.screenshot != null) {
|
|
212
|
+
let base64Data = null;
|
|
213
|
+
if (typeof result.screenshot === "string") {
|
|
214
|
+
base64Data = result.screenshot.replace(/^data:image\/\w+;base64,/, "");
|
|
215
|
+
} else if (typeof result.screenshot === "object" && result.screenshot.data) {
|
|
216
|
+
base64Data = result.screenshot.data.replace(/^data:image\/\w+;base64,/, "");
|
|
217
|
+
}
|
|
218
|
+
if (base64Data) {
|
|
219
|
+
const ref = await writeArtifactBinary(opts.workspace, "screenshots", `${jobId}.png`, base64Data);
|
|
220
|
+
const cdnUrl = typeof result.screenshot === "object" ? result.screenshot.url : void 0;
|
|
221
|
+
result.screenshot = { saved: ref.path, sizeBytes: ref.sizeBytes, ...cdnUrl ? { url: cdnUrl } : {} };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (Array.isArray(result.screenshots)) {
|
|
225
|
+
const savedRefs = [];
|
|
226
|
+
for (let i = 0; i < result.screenshots.length; i++) {
|
|
227
|
+
const ss = result.screenshots[i];
|
|
228
|
+
let base64Data = null;
|
|
229
|
+
const cdnUrl = typeof ss === "object" ? ss.url : void 0;
|
|
230
|
+
if (typeof ss === "string") {
|
|
231
|
+
base64Data = ss;
|
|
232
|
+
} else if (typeof ss === "object" && ss.data) {
|
|
233
|
+
base64Data = ss.data.replace(/^data:image\/\w+;base64,/, "");
|
|
234
|
+
}
|
|
235
|
+
if (base64Data) {
|
|
236
|
+
const ref = await writeArtifactBinary(opts.workspace, "screenshots", `${jobId}-${i}.png`, base64Data);
|
|
237
|
+
savedRefs.push({ saved: ref.path, sizeBytes: ref.sizeBytes, ...cdnUrl ? { url: cdnUrl } : {} });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
result.screenshots = savedRefs;
|
|
241
|
+
}
|
|
242
|
+
if (result.rawPngBase64 != null) {
|
|
243
|
+
const ref = await writeArtifactBinary(opts.workspace, "screenshots", `${jobId}.png`, result.rawPngBase64);
|
|
244
|
+
result.screenshot = { saved: ref.path, sizeBytes: ref.sizeBytes };
|
|
245
|
+
delete result.rawPngBase64;
|
|
246
|
+
}
|
|
247
|
+
if (result.har != null) {
|
|
248
|
+
const harStr = typeof result.har === "string" ? result.har : JSON.stringify(result.har);
|
|
249
|
+
const harBytes = Buffer.byteLength(harStr, "utf8");
|
|
250
|
+
if (opts.harInline && harBytes <= INLINE_CAP) {
|
|
251
|
+
} else {
|
|
252
|
+
const ref = await writeArtifact(opts.workspace, "har", `${jobId}.har.json`, harStr);
|
|
253
|
+
if (opts.harInline && harBytes > INLINE_CAP) {
|
|
254
|
+
result.har = { saved: ref.path, sizeBytes: ref.sizeBytes, warning: "Exceeded 50KB inline cap; wrote to file" };
|
|
255
|
+
} else {
|
|
256
|
+
result.har = { saved: ref.path, sizeBytes: ref.sizeBytes };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (result.console != null) {
|
|
261
|
+
const consoleStr = typeof result.console === "string" ? result.console : JSON.stringify(result.console);
|
|
262
|
+
const consoleBytes = Buffer.byteLength(consoleStr, "utf8");
|
|
263
|
+
if (consoleBytes > INLINE_CAP) {
|
|
264
|
+
const ref = await writeArtifact(opts.workspace, "console", `${jobId}.log`, consoleStr);
|
|
265
|
+
result.console = { saved: ref.path, sizeBytes: ref.sizeBytes };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function pollJobStatus(config, jobId, maxWaitMs) {
|
|
270
|
+
const { apiKey, baseUrl } = requireConfig(config);
|
|
271
|
+
const start = Date.now();
|
|
272
|
+
const pollIntervalMs = 2e3;
|
|
273
|
+
while (Date.now() - start < maxWaitMs) {
|
|
274
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}`, {
|
|
275
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
276
|
+
});
|
|
277
|
+
if (!res.ok) {
|
|
278
|
+
return { status: "poll_error", error: `HTTP ${res.status}` };
|
|
279
|
+
}
|
|
280
|
+
const data = await res.json();
|
|
281
|
+
if (data.status === "completed" || data.status === "completed_timeout" || data.status === "completed_error" || data.status === "failed") {
|
|
282
|
+
return data;
|
|
283
|
+
}
|
|
284
|
+
await sleep(pollIntervalMs);
|
|
285
|
+
}
|
|
286
|
+
return { status: "poll_timeout", error: `Job ${jobId} did not complete within ${maxWaitMs}ms` };
|
|
287
|
+
}
|
|
288
|
+
async function fetchArtifactsAndBuild(config, jobId, include) {
|
|
289
|
+
const { apiKey, baseUrl } = requireConfig(config);
|
|
290
|
+
const res = await fetch(
|
|
291
|
+
`${baseUrl.replace(/\/$/, "")}/v1/jobs/${jobId}/artifacts?include=${include.join(",")}`,
|
|
292
|
+
{ headers: { Authorization: `Bearer ${apiKey}` } }
|
|
293
|
+
);
|
|
294
|
+
if (!res.ok) {
|
|
295
|
+
return { error: `Artifacts fetch failed: HTTP ${res.status}` };
|
|
296
|
+
}
|
|
297
|
+
const data = await res.json();
|
|
298
|
+
const artifacts = data.artifacts || [];
|
|
299
|
+
const result = {};
|
|
300
|
+
if (data.status) result._artifactsStatus = data.status;
|
|
301
|
+
if (data.timeout) result._timeout = data.timeout;
|
|
302
|
+
if (data.error) result._error = data.error;
|
|
303
|
+
const screenshots = artifacts.filter((a) => a.name && /\.(png|jpg|jpeg)$/i.test(a.name));
|
|
304
|
+
if (screenshots.length > 0) {
|
|
305
|
+
result.screenshots = [];
|
|
306
|
+
for (const ss of screenshots) {
|
|
307
|
+
if (!ss.url) continue;
|
|
308
|
+
try {
|
|
309
|
+
const imgRes = await fetch(ss.url);
|
|
310
|
+
if (imgRes.ok) {
|
|
311
|
+
const buf = await imgRes.arrayBuffer();
|
|
312
|
+
result.screenshots.push({
|
|
313
|
+
name: ss.name,
|
|
314
|
+
data: `data:image/png;base64,${Buffer.from(buf).toString("base64")}`,
|
|
315
|
+
size: buf.byteLength,
|
|
316
|
+
url: ss.url
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
if (result.screenshots.length > 0) {
|
|
323
|
+
result.screenshot = result.screenshots[0];
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const consoleArtifact = artifacts.find((a) => a.name === "console.json");
|
|
327
|
+
if (consoleArtifact?.url) {
|
|
328
|
+
try {
|
|
329
|
+
const cRes = await fetch(consoleArtifact.url);
|
|
330
|
+
if (cRes.ok) {
|
|
331
|
+
result.console = await cRes.json();
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const resultArtifact = artifacts.find((a) => a.name === "result.json");
|
|
337
|
+
if (resultArtifact?.url) {
|
|
338
|
+
try {
|
|
339
|
+
const rRes = await fetch(resultArtifact.url);
|
|
340
|
+
if (rRes.ok) {
|
|
341
|
+
result.result = await rRes.json();
|
|
342
|
+
}
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (include.includes("har")) {
|
|
347
|
+
const harArtifact = artifacts.find((a) => a.name === "network.har");
|
|
348
|
+
if (harArtifact?.url) {
|
|
349
|
+
try {
|
|
350
|
+
const hRes = await fetch(harArtifact.url);
|
|
351
|
+
if (hRes.ok) {
|
|
352
|
+
result.har = await hRes.json();
|
|
353
|
+
}
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
async function runWithDefaults(config, payload, defaults) {
|
|
361
|
+
let cfg;
|
|
362
|
+
try {
|
|
363
|
+
cfg = requireConfig(config);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
return { ok: false, error: err.message };
|
|
366
|
+
}
|
|
367
|
+
const mode = detectMode(payload);
|
|
368
|
+
const userInclude = payload.include ?? [];
|
|
369
|
+
const userRequestedHar = userInclude.includes("har");
|
|
370
|
+
const harInline = !!payload.harInline;
|
|
371
|
+
const returnAsync = !!defaults?.returnAsync;
|
|
372
|
+
const merged = { ...payload };
|
|
373
|
+
delete merged.harInline;
|
|
374
|
+
if (returnAsync) {
|
|
375
|
+
merged.sync = false;
|
|
376
|
+
}
|
|
377
|
+
const defaultInc = defaults?.include ?? ["screenshot", "console"];
|
|
378
|
+
merged.include = Array.from(/* @__PURE__ */ new Set([...userInclude, ...defaultInc]));
|
|
379
|
+
merged.inlineConsole = merged.inlineConsole ?? true;
|
|
380
|
+
merged.inlineResult = merged.inlineResult ?? true;
|
|
381
|
+
if (userRequestedHar) {
|
|
382
|
+
merged.inlineHar = true;
|
|
383
|
+
}
|
|
384
|
+
const out = { ok: true, mode };
|
|
385
|
+
const { contentType, body, headers, status } = await postRun(cfg.baseUrl, cfg.apiKey, merged);
|
|
386
|
+
out.rawContentType = contentType ?? void 0;
|
|
387
|
+
if (status === 408) {
|
|
388
|
+
let jobIdFrom408;
|
|
389
|
+
try {
|
|
390
|
+
const json408 = JSON.parse(Buffer.from(body).toString("utf8"));
|
|
391
|
+
jobIdFrom408 = json408.job_id;
|
|
392
|
+
} catch {
|
|
393
|
+
}
|
|
394
|
+
if (!jobIdFrom408) {
|
|
395
|
+
out.ok = false;
|
|
396
|
+
out.error = "Sync poll timed out but no job_id in 408 response";
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
out.job_id = jobIdFrom408;
|
|
400
|
+
if (returnAsync) {
|
|
401
|
+
out.status = "submitted";
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
const scriptTimeoutMs = (payload.timeout_sec ?? 60) * 1e3;
|
|
405
|
+
const jobStatus = await pollJobStatus(cfg, jobIdFrom408, scriptTimeoutMs + 3e4);
|
|
406
|
+
if (jobStatus.status === "poll_timeout" || jobStatus.status === "poll_error" || jobStatus.status === "failed") {
|
|
407
|
+
out.ok = false;
|
|
408
|
+
out.status = jobStatus.status;
|
|
409
|
+
out.error = jobStatus.error || `Job ${jobStatus.status}`;
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
const artifacts = await fetchArtifactsAndBuild(cfg, jobIdFrom408, merged.include);
|
|
413
|
+
Object.assign(out, artifacts);
|
|
414
|
+
out.job_id = jobIdFrom408;
|
|
415
|
+
out.status = jobStatus.status;
|
|
416
|
+
out.duration_ms = jobStatus.duration_ms;
|
|
417
|
+
if (jobStatus.status !== "completed") {
|
|
418
|
+
out.ok = false;
|
|
419
|
+
if (jobStatus.timeout) out.timeout = jobStatus.timeout;
|
|
420
|
+
if (jobStatus.error) out.error = jobStatus.error;
|
|
421
|
+
}
|
|
422
|
+
await applySafetySpec(out, { workspace: cfg.workspace, harInline });
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
if (status >= 400) {
|
|
426
|
+
try {
|
|
427
|
+
const txt2 = Buffer.from(body).toString("utf8");
|
|
428
|
+
out.ok = false;
|
|
429
|
+
out.error = JSON.parse(txt2);
|
|
430
|
+
return out;
|
|
431
|
+
} catch {
|
|
432
|
+
out.ok = false;
|
|
433
|
+
out.error = `HTTP ${status}`;
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (contentType && contentType.includes("image/png")) {
|
|
438
|
+
out.rawPngBase64 = abToBase64(body);
|
|
439
|
+
out.job_id = headers.get("x-job-id") ?? void 0;
|
|
440
|
+
const duration = headers.get("x-duration-ms");
|
|
441
|
+
out.duration_ms = duration ? Number(duration) : void 0;
|
|
442
|
+
out.sync = true;
|
|
443
|
+
await applySafetySpec(out, { workspace: cfg.workspace, harInline });
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
const txt = Buffer.from(body).toString("utf8");
|
|
447
|
+
const json = JSON.parse(txt);
|
|
448
|
+
Object.assign(out, json);
|
|
449
|
+
out.job_id = json.job_id ?? json.jobId ?? out.job_id;
|
|
450
|
+
if (status === 202 && out.job_id && json.status_url) {
|
|
451
|
+
if (returnAsync) {
|
|
452
|
+
out.status = "submitted";
|
|
453
|
+
return out;
|
|
454
|
+
}
|
|
455
|
+
const scriptTimeoutMs = (payload.timeout_sec ?? 60) * 1e3;
|
|
456
|
+
const jobStatus = await pollJobStatus(cfg, out.job_id, scriptTimeoutMs + 3e4);
|
|
457
|
+
if (jobStatus.status === "poll_timeout" || jobStatus.status === "poll_error" || jobStatus.status === "failed") {
|
|
458
|
+
out.ok = false;
|
|
459
|
+
out.status = jobStatus.status;
|
|
460
|
+
out.error = jobStatus.error || `Job ${jobStatus.status}`;
|
|
461
|
+
return out;
|
|
462
|
+
}
|
|
463
|
+
const artifacts = await fetchArtifactsAndBuild(cfg, out.job_id, merged.include);
|
|
464
|
+
Object.assign(out, artifacts);
|
|
465
|
+
out.status = jobStatus.status;
|
|
466
|
+
out.duration_ms = jobStatus.duration_ms;
|
|
467
|
+
if (jobStatus.status !== "completed") {
|
|
468
|
+
out.ok = false;
|
|
469
|
+
if (jobStatus.timeout) out.timeout = jobStatus.timeout;
|
|
470
|
+
if (jobStatus.error) out.error = jobStatus.error;
|
|
471
|
+
}
|
|
472
|
+
await applySafetySpec(out, { workspace: cfg.workspace, harInline });
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
if (json.status === "completed_timeout" || json.status === "completed_error") {
|
|
476
|
+
out.ok = false;
|
|
477
|
+
}
|
|
478
|
+
await applySafetySpec(out, { workspace: cfg.workspace, harInline });
|
|
479
|
+
return out;
|
|
480
|
+
}
|
|
481
|
+
async function riddleApiFetch(config, method, path, body) {
|
|
482
|
+
const { apiKey, baseUrl } = requireConfig(config);
|
|
483
|
+
const url = `${baseUrl.replace(/\/$/, "")}${path}`;
|
|
484
|
+
const res = await fetch(url, {
|
|
485
|
+
method,
|
|
486
|
+
headers: {
|
|
487
|
+
Authorization: `Bearer ${apiKey}`,
|
|
488
|
+
...body ? { "Content-Type": "application/json" } : {}
|
|
489
|
+
},
|
|
490
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
491
|
+
});
|
|
492
|
+
const data = await res.json();
|
|
493
|
+
if (!res.ok) throw new Error(data.error?.message || data.error || `HTTP ${res.status}`);
|
|
494
|
+
return data;
|
|
495
|
+
}
|
|
496
|
+
async function assertDirectory(dir) {
|
|
497
|
+
if (!dir || typeof dir !== "string") throw new Error("directory must be an absolute path");
|
|
498
|
+
try {
|
|
499
|
+
const st = await (0, import_promises.stat)(dir);
|
|
500
|
+
if (!st.isDirectory()) throw new Error(`Not a directory: ${dir}`);
|
|
501
|
+
} catch (e) {
|
|
502
|
+
return { ok: false, error: `Cannot access directory: ${e.message}` };
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
async function tarDirectory(dir, tarball, excludes, timeout) {
|
|
507
|
+
const excludeArgs = excludes.flatMap((p) => ["--exclude", p]);
|
|
508
|
+
await execFile("tar", ["czf", tarball, ...excludeArgs, "-C", dir, "."], { timeout });
|
|
509
|
+
return (0, import_promises.readFile)(tarball);
|
|
510
|
+
}
|
|
511
|
+
async function saveImageOutputs(workspace, jobId, outputs, label) {
|
|
512
|
+
for (const output of outputs) {
|
|
513
|
+
if (output.name && /\.(png|jpg|jpeg)$/i.test(output.name) && output.url) {
|
|
514
|
+
try {
|
|
515
|
+
const imgRes = await fetchWithTimeout(output.url, {}, PREVIEW_ARTIFACT_TIMEOUT_MS, `${label} artifact download`);
|
|
516
|
+
if (imgRes.ok) {
|
|
517
|
+
const buf = await imgRes.arrayBuffer();
|
|
518
|
+
const base64 = Buffer.from(buf).toString("base64");
|
|
519
|
+
const ref = await writeArtifactBinary(workspace, "screenshots", `${jobId}-${output.name}`, base64);
|
|
520
|
+
output.saved = ref.path;
|
|
521
|
+
output.sizeBytes = ref.sizeBytes;
|
|
522
|
+
}
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async function pollPreviewJob(config, pathPrefix, jobId, timeoutMs, resultExtras) {
|
|
529
|
+
const endpoint = config.baseUrl.replace(/\/$/, "");
|
|
530
|
+
const pollStart = Date.now();
|
|
531
|
+
const pollIntervalMs = 3e3;
|
|
532
|
+
let lastStatusData = null;
|
|
533
|
+
while (Date.now() - pollStart < timeoutMs) {
|
|
534
|
+
let statusRes;
|
|
535
|
+
try {
|
|
536
|
+
statusRes = await fetchWithRetry(`${endpoint}${pathPrefix}/${jobId}`, {
|
|
537
|
+
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
538
|
+
}, PREVIEW_REQUEST_TIMEOUT_MS, `${pathPrefix.slice(1)} poll`, { attempts: 2 });
|
|
539
|
+
} catch (e) {
|
|
540
|
+
return { ok: false, job_id: jobId, error: `Poll failed: ${describeError(e)}` };
|
|
541
|
+
}
|
|
542
|
+
if (!statusRes.ok) {
|
|
543
|
+
return { ok: false, job_id: jobId, error: `Poll failed: HTTP ${statusRes.status}` };
|
|
544
|
+
}
|
|
545
|
+
const statusData = await statusRes.json();
|
|
546
|
+
lastStatusData = statusData;
|
|
547
|
+
if (statusData.status === "complete" || statusData.status === "completed" || statusData.status === "failed") {
|
|
548
|
+
const result = {
|
|
549
|
+
ok: statusData.status === "complete" || statusData.status === "completed",
|
|
550
|
+
job_id: jobId,
|
|
551
|
+
status: statusData.status,
|
|
552
|
+
phase: statusData.phase ?? statusData.status,
|
|
553
|
+
phase_updated_at: statusData.phase_updated_at,
|
|
554
|
+
phase_details: statusData.phase_details,
|
|
555
|
+
outputs: statusData.outputs || [],
|
|
556
|
+
compute_seconds: statusData.compute_seconds,
|
|
557
|
+
egress_bytes: statusData.egress_bytes,
|
|
558
|
+
...resultExtras(statusData)
|
|
559
|
+
};
|
|
560
|
+
if (statusData.error) result.error = statusData.error;
|
|
561
|
+
if (statusData.script_error) result.script_error = statusData.script_error;
|
|
562
|
+
await saveImageOutputs(config.workspace, jobId, result.outputs, pathPrefix.slice(1));
|
|
563
|
+
result.screenshots = result.outputs.filter((o) => /\.(png|jpg|jpeg)$/i.test(o.name));
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
await sleep(pollIntervalMs);
|
|
567
|
+
}
|
|
568
|
+
return previewTimeoutResult(jobId, timeoutMs, lastStatusData, resultExtras);
|
|
569
|
+
}
|
|
570
|
+
async function createStaticPreview(config, params) {
|
|
571
|
+
let cfg;
|
|
572
|
+
try {
|
|
573
|
+
cfg = requireConfig(config);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
return { ok: false, error: err.message };
|
|
576
|
+
}
|
|
577
|
+
const dirError = await assertDirectory(params.directory);
|
|
578
|
+
if (dirError) return dirError;
|
|
579
|
+
const endpoint = cfg.baseUrl.replace(/\/$/, "");
|
|
580
|
+
let createRes;
|
|
581
|
+
try {
|
|
582
|
+
createRes = await fetchWithRetry(`${endpoint}/v1/preview`, {
|
|
583
|
+
method: "POST",
|
|
584
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
|
|
585
|
+
body: JSON.stringify({ framework: params.framework || "spa" })
|
|
586
|
+
}, PREVIEW_REQUEST_TIMEOUT_MS, "preview create");
|
|
587
|
+
} catch (e) {
|
|
588
|
+
return { ok: false, error: `Create failed: ${describeError(e)}` };
|
|
589
|
+
}
|
|
590
|
+
if (!createRes.ok) {
|
|
591
|
+
const err = await createRes.text();
|
|
592
|
+
return { ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` };
|
|
593
|
+
}
|
|
594
|
+
const created = await createRes.json();
|
|
595
|
+
const tarball = `/tmp/riddle-preview-${created.id}.tar.gz`;
|
|
596
|
+
try {
|
|
597
|
+
const tarData = await tarDirectory(params.directory, tarball, [], 6e4);
|
|
598
|
+
let uploadRes;
|
|
599
|
+
try {
|
|
600
|
+
uploadRes = await fetchWithRetry(created.upload_url, {
|
|
601
|
+
method: "PUT",
|
|
602
|
+
headers: { "Content-Type": "application/gzip" },
|
|
603
|
+
body: tarData
|
|
604
|
+
}, PREVIEW_UPLOAD_TIMEOUT_MS, "preview upload");
|
|
605
|
+
} catch (e) {
|
|
606
|
+
return { ok: false, id: created.id, error: `Upload failed: ${describeError(e)}` };
|
|
607
|
+
}
|
|
608
|
+
if (!uploadRes.ok) {
|
|
609
|
+
return { ok: false, id: created.id, error: `Upload failed: HTTP ${uploadRes.status}` };
|
|
610
|
+
}
|
|
611
|
+
} finally {
|
|
612
|
+
try {
|
|
613
|
+
await (0, import_promises.rm)(tarball, { force: true });
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
let publishRes;
|
|
618
|
+
try {
|
|
619
|
+
publishRes = await fetchWithTimeout(`${endpoint}/v1/preview/${created.id}/publish`, {
|
|
620
|
+
method: "POST",
|
|
621
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}` }
|
|
622
|
+
}, PREVIEW_REQUEST_TIMEOUT_MS, "preview publish");
|
|
623
|
+
} catch (e) {
|
|
624
|
+
return { ok: false, id: created.id, error: `Publish failed: ${describeError(e)}` };
|
|
625
|
+
}
|
|
626
|
+
if (!publishRes.ok) {
|
|
627
|
+
const err = await publishRes.text();
|
|
628
|
+
return { ok: false, id: created.id, error: `Publish failed: HTTP ${publishRes.status} ${err}` };
|
|
629
|
+
}
|
|
630
|
+
const published = await publishRes.json();
|
|
631
|
+
return {
|
|
632
|
+
ok: true,
|
|
633
|
+
id: published.id,
|
|
634
|
+
preview_url: published.preview_url,
|
|
635
|
+
file_count: published.file_count,
|
|
636
|
+
total_bytes: published.total_bytes,
|
|
637
|
+
expires_at: created.expires_at
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
async function deleteStaticPreview(config, id) {
|
|
641
|
+
let cfg;
|
|
642
|
+
try {
|
|
643
|
+
cfg = requireConfig(config);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
return { ok: false, error: err.message };
|
|
646
|
+
}
|
|
647
|
+
let res;
|
|
648
|
+
try {
|
|
649
|
+
res = await fetchWithTimeout(`${cfg.baseUrl.replace(/\/$/, "")}/v1/preview/${id}`, {
|
|
650
|
+
method: "DELETE",
|
|
651
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}` }
|
|
652
|
+
}, PREVIEW_REQUEST_TIMEOUT_MS, "preview delete");
|
|
653
|
+
} catch (e) {
|
|
654
|
+
return { ok: false, error: `Delete failed: ${describeError(e)}` };
|
|
655
|
+
}
|
|
656
|
+
if (!res.ok) {
|
|
657
|
+
const err = await res.text();
|
|
658
|
+
return { ok: false, error: `Delete failed: HTTP ${res.status} ${err}` };
|
|
659
|
+
}
|
|
660
|
+
const data = await res.json();
|
|
661
|
+
return { ok: true, deleted: true, files_removed: data.files_removed };
|
|
662
|
+
}
|
|
663
|
+
async function storePreviewEnv(cfg, endpointPath, params) {
|
|
664
|
+
const hasSensitiveEnv = params.sensitive_env && Object.keys(params.sensitive_env).length > 0;
|
|
665
|
+
const hasLocalStorage = params.localStorage && Object.keys(params.localStorage).length > 0;
|
|
666
|
+
if (!hasSensitiveEnv && !hasLocalStorage) return { envRef: null };
|
|
667
|
+
const envBody = {};
|
|
668
|
+
if (hasSensitiveEnv) envBody.env = params.sensitive_env;
|
|
669
|
+
if (hasLocalStorage) envBody.localStorage = params.localStorage;
|
|
670
|
+
let envRes;
|
|
671
|
+
try {
|
|
672
|
+
envRes = await fetchWithTimeout(`${cfg.baseUrl.replace(/\/$/, "")}${endpointPath}/env`, {
|
|
673
|
+
method: "POST",
|
|
674
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
|
|
675
|
+
body: JSON.stringify(envBody)
|
|
676
|
+
}, PREVIEW_REQUEST_TIMEOUT_MS, `${endpointPath.slice(1)} env store`);
|
|
677
|
+
} catch (e) {
|
|
678
|
+
return { envRef: null, error: { ok: false, error: `Store env failed: ${describeError(e)}` } };
|
|
679
|
+
}
|
|
680
|
+
if (!envRes.ok) {
|
|
681
|
+
const err = await envRes.text();
|
|
682
|
+
return { envRef: null, error: { ok: false, error: `Store env failed: HTTP ${envRes.status} ${err}` } };
|
|
683
|
+
}
|
|
684
|
+
const envData = await envRes.json();
|
|
685
|
+
return { envRef: envData.env_ref };
|
|
686
|
+
}
|
|
687
|
+
async function createServerPreview(config, params) {
|
|
688
|
+
let cfg;
|
|
689
|
+
try {
|
|
690
|
+
cfg = requireConfig(config);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
return { ok: false, error: err.message };
|
|
693
|
+
}
|
|
694
|
+
const dirError = await assertDirectory(params.directory);
|
|
695
|
+
if (dirError) return dirError;
|
|
696
|
+
const endpoint = cfg.baseUrl.replace(/\/$/, "");
|
|
697
|
+
const env = await storePreviewEnv(cfg, "/v1/server-preview", params);
|
|
698
|
+
if (env.error) return env.error;
|
|
699
|
+
const createBody = {
|
|
700
|
+
image: params.image,
|
|
701
|
+
command: params.command,
|
|
702
|
+
port: params.port
|
|
703
|
+
};
|
|
704
|
+
for (const key of [
|
|
705
|
+
"path",
|
|
706
|
+
"env",
|
|
707
|
+
"timeout",
|
|
708
|
+
"readiness_path",
|
|
709
|
+
"readiness_timeout",
|
|
710
|
+
"script",
|
|
711
|
+
"steps",
|
|
712
|
+
"wait_until",
|
|
713
|
+
"wait_for_selector",
|
|
714
|
+
"navigation_timeout",
|
|
715
|
+
"color_scheme",
|
|
716
|
+
"viewport"
|
|
717
|
+
]) {
|
|
718
|
+
if (params[key]) createBody[key] = params[key];
|
|
719
|
+
}
|
|
720
|
+
if (env.envRef) createBody.env_ref = env.envRef;
|
|
721
|
+
let createRes;
|
|
722
|
+
try {
|
|
723
|
+
createRes = await fetchWithRetry(`${endpoint}/v1/server-preview`, {
|
|
724
|
+
method: "POST",
|
|
725
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
|
|
726
|
+
body: JSON.stringify(createBody)
|
|
727
|
+
}, PREVIEW_REQUEST_TIMEOUT_MS, "server preview create");
|
|
728
|
+
} catch (e) {
|
|
729
|
+
return { ok: false, error: `Create failed: ${describeError(e)}` };
|
|
730
|
+
}
|
|
731
|
+
if (!createRes.ok) {
|
|
732
|
+
const err = await createRes.text();
|
|
733
|
+
return { ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` };
|
|
734
|
+
}
|
|
735
|
+
const created = await createRes.json();
|
|
736
|
+
const tarball = `/tmp/riddle-sp-${created.job_id}.tar.gz`;
|
|
737
|
+
try {
|
|
738
|
+
const tarData = await tarDirectory(params.directory, tarball, params.exclude || [".git", "*.log"], 12e4);
|
|
739
|
+
let uploadRes;
|
|
740
|
+
try {
|
|
741
|
+
uploadRes = await fetchWithRetry(created.upload_url, {
|
|
742
|
+
method: "PUT",
|
|
743
|
+
headers: { "Content-Type": "application/gzip" },
|
|
744
|
+
body: tarData
|
|
745
|
+
}, PREVIEW_UPLOAD_TIMEOUT_MS, "server preview upload");
|
|
746
|
+
} catch (e) {
|
|
747
|
+
return { ok: false, job_id: created.job_id, error: `Upload failed: ${describeError(e)}` };
|
|
748
|
+
}
|
|
749
|
+
if (!uploadRes.ok) {
|
|
750
|
+
return { ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` };
|
|
751
|
+
}
|
|
752
|
+
} finally {
|
|
753
|
+
try {
|
|
754
|
+
await (0, import_promises.rm)(tarball, { force: true });
|
|
755
|
+
} catch {
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
let startRes;
|
|
759
|
+
try {
|
|
760
|
+
startRes = await fetchWithRetry(`${endpoint}/v1/server-preview/${created.job_id}/start`, {
|
|
761
|
+
method: "POST",
|
|
762
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}` }
|
|
763
|
+
}, PREVIEW_REQUEST_TIMEOUT_MS, "server preview start");
|
|
764
|
+
} catch (e) {
|
|
765
|
+
return { ok: false, job_id: created.job_id, error: `Start failed: ${describeError(e)}` };
|
|
766
|
+
}
|
|
767
|
+
if (!startRes.ok) {
|
|
768
|
+
const err = await startRes.text();
|
|
769
|
+
if (isAlreadyStartedResponse(startRes.status, err)) {
|
|
770
|
+
console.warn(`[openclaw-riddledc] server preview start returned ${startRes.status} for ${created.job_id}; continuing to poll`);
|
|
771
|
+
} else {
|
|
772
|
+
return { ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` };
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return pollPreviewJob(cfg, "/v1/server-preview", created.job_id, ((params.timeout || 120) + 60) * 1e3, () => ({}));
|
|
776
|
+
}
|
|
777
|
+
async function createBuildPreview(config, params) {
|
|
778
|
+
let cfg;
|
|
779
|
+
try {
|
|
780
|
+
cfg = requireConfig(config);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
return { ok: false, error: err.message };
|
|
783
|
+
}
|
|
784
|
+
const dirError = await assertDirectory(params.directory);
|
|
785
|
+
if (dirError) return dirError;
|
|
786
|
+
try {
|
|
787
|
+
await (0, import_promises.stat)(`${params.directory}/Dockerfile`);
|
|
788
|
+
} catch {
|
|
789
|
+
return { ok: false, error: `No Dockerfile found at ${params.directory}/Dockerfile. riddle_build_preview requires a Dockerfile at the root of the directory.` };
|
|
790
|
+
}
|
|
791
|
+
const endpoint = cfg.baseUrl.replace(/\/$/, "");
|
|
792
|
+
const env = await storePreviewEnv(cfg, "/v1/build-preview", params);
|
|
793
|
+
if (env.error) return env.error;
|
|
794
|
+
const createBody = {
|
|
795
|
+
command: params.command,
|
|
796
|
+
port: params.port
|
|
797
|
+
};
|
|
798
|
+
for (const key of [
|
|
799
|
+
"path",
|
|
800
|
+
"env",
|
|
801
|
+
"build_args",
|
|
802
|
+
"keep_image_minutes",
|
|
803
|
+
"timeout",
|
|
804
|
+
"readiness_path",
|
|
805
|
+
"readiness_timeout",
|
|
806
|
+
"script",
|
|
807
|
+
"steps",
|
|
808
|
+
"wait_until",
|
|
809
|
+
"wait_for_selector",
|
|
810
|
+
"navigation_timeout",
|
|
811
|
+
"color_scheme",
|
|
812
|
+
"viewport",
|
|
813
|
+
"audit"
|
|
814
|
+
]) {
|
|
815
|
+
if (params[key] !== void 0) createBody[key] = params[key];
|
|
816
|
+
}
|
|
817
|
+
if (env.envRef) createBody.env_ref = env.envRef;
|
|
818
|
+
let createRes;
|
|
819
|
+
try {
|
|
820
|
+
createRes = await fetchWithRetry(`${endpoint}/v1/build-preview`, {
|
|
821
|
+
method: "POST",
|
|
822
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}`, "Content-Type": "application/json" },
|
|
823
|
+
body: JSON.stringify(createBody)
|
|
824
|
+
}, PREVIEW_REQUEST_TIMEOUT_MS, "build preview create");
|
|
825
|
+
} catch (e) {
|
|
826
|
+
return { ok: false, error: `Create failed: ${describeError(e)}` };
|
|
827
|
+
}
|
|
828
|
+
if (!createRes.ok) {
|
|
829
|
+
const err = await createRes.text();
|
|
830
|
+
return { ok: false, error: `Create failed: HTTP ${createRes.status} ${err}` };
|
|
831
|
+
}
|
|
832
|
+
const created = await createRes.json();
|
|
833
|
+
const tarball = `/tmp/riddle-bp-${created.job_id}.tar.gz`;
|
|
834
|
+
try {
|
|
835
|
+
const tarData = await tarDirectory(params.directory, tarball, params.exclude || [".git", "*.log"], 12e4);
|
|
836
|
+
let uploadRes;
|
|
837
|
+
try {
|
|
838
|
+
uploadRes = await fetchWithRetry(created.upload_url, {
|
|
839
|
+
method: "PUT",
|
|
840
|
+
headers: { "Content-Type": "application/gzip" },
|
|
841
|
+
body: tarData
|
|
842
|
+
}, PREVIEW_UPLOAD_TIMEOUT_MS, "build preview upload");
|
|
843
|
+
} catch (e) {
|
|
844
|
+
return { ok: false, job_id: created.job_id, error: `Upload failed: ${describeError(e)}` };
|
|
845
|
+
}
|
|
846
|
+
if (!uploadRes.ok) {
|
|
847
|
+
return { ok: false, job_id: created.job_id, error: `Upload failed: HTTP ${uploadRes.status}` };
|
|
848
|
+
}
|
|
849
|
+
} finally {
|
|
850
|
+
try {
|
|
851
|
+
await (0, import_promises.rm)(tarball, { force: true });
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
let startRes;
|
|
856
|
+
try {
|
|
857
|
+
startRes = await fetchWithRetry(`${endpoint}/v1/build-preview/${created.job_id}/start`, {
|
|
858
|
+
method: "POST",
|
|
859
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}` }
|
|
860
|
+
}, PREVIEW_REQUEST_TIMEOUT_MS, "build preview start");
|
|
861
|
+
} catch (e) {
|
|
862
|
+
return { ok: false, job_id: created.job_id, error: `Start failed: ${describeError(e)}` };
|
|
863
|
+
}
|
|
864
|
+
if (!startRes.ok) {
|
|
865
|
+
const err = await startRes.text();
|
|
866
|
+
if (isAlreadyStartedResponse(startRes.status, err)) {
|
|
867
|
+
console.warn(`[openclaw-riddledc] build preview start returned ${startRes.status} for ${created.job_id}; continuing to poll`);
|
|
868
|
+
} else {
|
|
869
|
+
return { ok: false, job_id: created.job_id, error: `Start failed: HTTP ${startRes.status} ${err}` };
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return pollPreviewJob(cfg, "/v1/build-preview", created.job_id, ((params.timeout || 180) + 120) * 1e3, (statusData) => ({
|
|
873
|
+
build_duration_ms: statusData.build_duration_ms,
|
|
874
|
+
...statusData.build_log ? { build_log: statusData.build_log } : {},
|
|
875
|
+
...statusData.container_log ? { container_log: statusData.container_log } : {},
|
|
876
|
+
...statusData.audit ? { audit: statusData.audit } : {}
|
|
877
|
+
}));
|
|
878
|
+
}
|
|
879
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
880
|
+
0 && (module.exports = {
|
|
881
|
+
applySafetySpec,
|
|
882
|
+
assertAllowedBaseUrl,
|
|
883
|
+
configFromOpenClawApi,
|
|
884
|
+
createBuildPreview,
|
|
885
|
+
createServerPreview,
|
|
886
|
+
createStaticPreview,
|
|
887
|
+
deleteStaticPreview,
|
|
888
|
+
describeError,
|
|
889
|
+
fetchArtifactsAndBuild,
|
|
890
|
+
fetchWithRetry,
|
|
891
|
+
fetchWithTimeout,
|
|
892
|
+
isAlreadyStartedResponse,
|
|
893
|
+
pollJobStatus,
|
|
894
|
+
riddleApiFetch,
|
|
895
|
+
runWithDefaults,
|
|
896
|
+
writeArtifactBinary
|
|
897
|
+
});
|