@openclaw/diffs 2026.8.1-beta.2 → 2026.8.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/assets/viewer-runtime.js +3 -3
- package/dist/browser.runtime-_hkQDewM.js +406 -0
- package/dist/index.js +20 -578
- package/dist/viewer-assets-DKR8WLAI.js +118 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { definePluginEntry, resolvePreferredOpenClawTmpDir } from "./api.js";
|
|
2
2
|
import { resolveRequestClientIp } from "./runtime-api.js";
|
|
3
|
+
import { a as getServedViewerAsset, r as VIEWER_RUNTIME_PATH } from "./viewer-assets-DKR8WLAI.js";
|
|
3
4
|
import { buildPluginConfigSchema } from "openclaw/plugin-sdk/plugin-entry";
|
|
4
5
|
import { mapPluginConfigIssues } from "openclaw/plugin-sdk/extension-shared";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
import { resolveGatewayPublicOrigin } from "openclaw/plugin-sdk/config-contracts";
|
|
7
8
|
import { resolveGatewayPort } from "openclaw/plugin-sdk/core";
|
|
8
|
-
import fs
|
|
9
|
+
import fs from "node:fs";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
|
|
11
12
|
import { isLoopbackHost } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
12
|
-
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
13
|
+
import { asNonArrayRecord, normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
14
|
+
import { createAuthRateLimiter } from "openclaw/plugin-sdk/webhook-ingress";
|
|
13
15
|
import crypto from "node:crypto";
|
|
14
16
|
import fs$1 from "node:fs/promises";
|
|
15
|
-
import { fileURLToPath } from "node:url";
|
|
16
17
|
import { gunzip, gzip } from "node:zlib";
|
|
17
18
|
import { MAX_DATE_TIMESTAMP_MS, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
|
18
|
-
import { safeEqualSecret
|
|
19
|
+
import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
19
20
|
import { optionalFiniteNumberSchema, stringEnum } from "openclaw/plugin-sdk/channel-actions";
|
|
20
21
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
22
|
+
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
21
23
|
import { readFiniteNumberParam } from "openclaw/plugin-sdk/param-readers";
|
|
22
24
|
import { Type } from "typebox";
|
|
23
|
-
import os from "node:os";
|
|
24
|
-
import { chromium } from "playwright-core";
|
|
25
25
|
import { parsePatchFiles, resolveLanguage } from "@pierre/diffs";
|
|
26
26
|
import { preloadDiffHTML } from "@pierre/diffs/ssr";
|
|
27
27
|
import { escapeHtml } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
@@ -302,120 +302,6 @@ function resolveDiffImageRenderOptions(params) {
|
|
|
302
302
|
};
|
|
303
303
|
}
|
|
304
304
|
//#endregion
|
|
305
|
-
//#region extensions/diffs/src/viewer-assets.ts
|
|
306
|
-
const VIEWER_ASSET_PREFIX = "/plugins/diffs/assets/";
|
|
307
|
-
const VIEWER_LOADER_PATH = `${VIEWER_ASSET_PREFIX}viewer.js`;
|
|
308
|
-
const VIEWER_RUNTIME_PATH = `${VIEWER_ASSET_PREFIX}viewer-runtime.js`;
|
|
309
|
-
const LANGUAGE_PACK_VIEWER_ASSET_PREFIX = "/plugins/diffs-language-pack/assets/";
|
|
310
|
-
const LANGUAGE_PACK_VIEWER_LOADER_PATH = `${LANGUAGE_PACK_VIEWER_ASSET_PREFIX}viewer.js`;
|
|
311
|
-
const LANGUAGE_PACK_VIEWER_RUNTIME_PATH = `${LANGUAGE_PACK_VIEWER_ASSET_PREFIX}viewer-runtime.js`;
|
|
312
|
-
const VIEWER_RUNTIME_RELATIVE_IMPORT_PATH = "./viewer-runtime.js";
|
|
313
|
-
const VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS = ["./assets/viewer-runtime.js", "../assets/viewer-runtime.js"];
|
|
314
|
-
const LANGUAGE_PACK_RUNTIME_CANDIDATE_RELATIVE_PATHS = ["../../diffs-language-pack/assets/viewer-runtime.js", "../diffs-language-pack/assets/viewer-runtime.js"];
|
|
315
|
-
let runtimeAssetCache = null;
|
|
316
|
-
let languagePackRuntimeAssetCache = null;
|
|
317
|
-
function isMissingFileError(error) {
|
|
318
|
-
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
319
|
-
}
|
|
320
|
-
async function resolveViewerRuntimeFileUrl(params = {}) {
|
|
321
|
-
const baseUrl = params.baseUrl ?? import.meta.url;
|
|
322
|
-
const stat = params.stat ?? ((path) => fs$1.stat(path));
|
|
323
|
-
let missingFileError = null;
|
|
324
|
-
for (const relativePath of VIEWER_RUNTIME_CANDIDATE_RELATIVE_PATHS) {
|
|
325
|
-
const candidateUrl = new URL(relativePath, baseUrl);
|
|
326
|
-
try {
|
|
327
|
-
await stat(fileURLToPath(candidateUrl));
|
|
328
|
-
return candidateUrl;
|
|
329
|
-
} catch (error) {
|
|
330
|
-
if (isMissingFileError(error)) {
|
|
331
|
-
missingFileError = error;
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
throw error;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
if (missingFileError) throw missingFileError;
|
|
338
|
-
throw new Error("viewer runtime asset candidates were not checked");
|
|
339
|
-
}
|
|
340
|
-
async function getServedViewerAsset(pathname) {
|
|
341
|
-
if (pathname !== VIEWER_LOADER_PATH && pathname !== VIEWER_RUNTIME_PATH) return null;
|
|
342
|
-
const assets = await loadViewerAssets();
|
|
343
|
-
if (pathname === VIEWER_LOADER_PATH) return {
|
|
344
|
-
body: assets.loaderBody,
|
|
345
|
-
contentType: "text/javascript; charset=utf-8"
|
|
346
|
-
};
|
|
347
|
-
if (pathname === VIEWER_RUNTIME_PATH) return {
|
|
348
|
-
body: assets.runtimeBody,
|
|
349
|
-
contentType: "text/javascript; charset=utf-8"
|
|
350
|
-
};
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
353
|
-
async function getServedLanguagePackViewerAsset(pathname) {
|
|
354
|
-
if (pathname !== LANGUAGE_PACK_VIEWER_LOADER_PATH && pathname !== LANGUAGE_PACK_VIEWER_RUNTIME_PATH) return null;
|
|
355
|
-
let assets;
|
|
356
|
-
try {
|
|
357
|
-
assets = await loadRuntimeAssets({
|
|
358
|
-
runtimeUrl: await resolveRuntimeFileUrl(LANGUAGE_PACK_RUNTIME_CANDIDATE_RELATIVE_PATHS),
|
|
359
|
-
cache: languagePackRuntimeAssetCache,
|
|
360
|
-
updateCache: (cache) => {
|
|
361
|
-
languagePackRuntimeAssetCache = cache;
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
} catch (error) {
|
|
365
|
-
if (isMissingFileError(error)) return null;
|
|
366
|
-
throw error;
|
|
367
|
-
}
|
|
368
|
-
if (pathname === LANGUAGE_PACK_VIEWER_LOADER_PATH) return {
|
|
369
|
-
body: assets.loaderBody,
|
|
370
|
-
contentType: "text/javascript; charset=utf-8"
|
|
371
|
-
};
|
|
372
|
-
return {
|
|
373
|
-
body: assets.runtimeBody,
|
|
374
|
-
contentType: "text/javascript; charset=utf-8"
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
async function loadViewerAssets() {
|
|
378
|
-
return loadRuntimeAssets({
|
|
379
|
-
runtimeUrl: await resolveViewerRuntimeFileUrl(),
|
|
380
|
-
cache: runtimeAssetCache,
|
|
381
|
-
updateCache: (cache) => {
|
|
382
|
-
runtimeAssetCache = cache;
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
async function loadRuntimeAssets(params) {
|
|
387
|
-
const runtimePath = fileURLToPath(params.runtimeUrl);
|
|
388
|
-
const runtimeStat = await fs$1.stat(runtimePath);
|
|
389
|
-
if (params.cache && params.cache.mtimeMs === runtimeStat.mtimeMs) return params.cache;
|
|
390
|
-
const runtimeBody = await fs$1.readFile(runtimePath);
|
|
391
|
-
const hash = crypto.createHash("sha1").update(runtimeBody).digest("hex").slice(0, 12);
|
|
392
|
-
const cache = {
|
|
393
|
-
mtimeMs: runtimeStat.mtimeMs,
|
|
394
|
-
runtimeBody,
|
|
395
|
-
loaderBody: `import "${VIEWER_RUNTIME_RELATIVE_IMPORT_PATH}?v=${hash}";\n`
|
|
396
|
-
};
|
|
397
|
-
params.updateCache(cache);
|
|
398
|
-
return cache;
|
|
399
|
-
}
|
|
400
|
-
async function resolveRuntimeFileUrl(relativePaths) {
|
|
401
|
-
let missingFileError = null;
|
|
402
|
-
for (const relativePath of relativePaths) {
|
|
403
|
-
const candidateUrl = new URL(relativePath, import.meta.url);
|
|
404
|
-
try {
|
|
405
|
-
await fs$1.stat(fileURLToPath(candidateUrl));
|
|
406
|
-
return candidateUrl;
|
|
407
|
-
} catch (error) {
|
|
408
|
-
if (isMissingFileError(error)) {
|
|
409
|
-
missingFileError = error;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
throw error;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
if (missingFileError) throw missingFileError;
|
|
416
|
-
throw new Error("viewer runtime asset candidates were not checked");
|
|
417
|
-
}
|
|
418
|
-
//#endregion
|
|
419
305
|
//#region extensions/diffs/src/http.ts
|
|
420
306
|
const VIEW_PREFIX = "/plugins/diffs/view/";
|
|
421
307
|
const VIEWER_MAX_FAILURES_PER_WINDOW = 40;
|
|
@@ -435,7 +321,14 @@ const VIEWER_CONTENT_SECURITY_POLICY = [
|
|
|
435
321
|
].join("; ");
|
|
436
322
|
const IMMUTABLE_ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
437
323
|
function createDiffsHttpHandler(params) {
|
|
438
|
-
const viewerFailureLimiter =
|
|
324
|
+
const viewerFailureLimiter = createAuthRateLimiter({
|
|
325
|
+
maxAttempts: VIEWER_MAX_FAILURES_PER_WINDOW,
|
|
326
|
+
windowMs: VIEWER_FAILURE_WINDOW_MS,
|
|
327
|
+
lockoutMs: VIEWER_LOCKOUT_MS,
|
|
328
|
+
exemptLoopback: false,
|
|
329
|
+
pruneIntervalMs: 0,
|
|
330
|
+
maxEntries: VIEWER_LIMITER_MAX_KEYS
|
|
331
|
+
});
|
|
439
332
|
return async (req, res) => {
|
|
440
333
|
const parsed = parseRequestUrl(req.url);
|
|
441
334
|
if (!parsed) return false;
|
|
@@ -567,62 +460,6 @@ function recordRemoteFailure(limiter, access) {
|
|
|
567
460
|
function resetRemoteFailures(limiter, access) {
|
|
568
461
|
if (!access.localRequest) limiter.reset(access.remoteKey);
|
|
569
462
|
}
|
|
570
|
-
var ViewerFailureLimiter = class {
|
|
571
|
-
constructor() {
|
|
572
|
-
this.failures = /* @__PURE__ */ new Map();
|
|
573
|
-
}
|
|
574
|
-
check(key) {
|
|
575
|
-
this.prune();
|
|
576
|
-
const state = this.failures.get(key);
|
|
577
|
-
if (!state) return {
|
|
578
|
-
allowed: true,
|
|
579
|
-
retryAfterMs: 0
|
|
580
|
-
};
|
|
581
|
-
const now = Date.now();
|
|
582
|
-
if (state.lockUntilMs > now) return {
|
|
583
|
-
allowed: false,
|
|
584
|
-
retryAfterMs: state.lockUntilMs - now
|
|
585
|
-
};
|
|
586
|
-
if (now - state.windowStartMs >= VIEWER_FAILURE_WINDOW_MS) {
|
|
587
|
-
this.failures.delete(key);
|
|
588
|
-
return {
|
|
589
|
-
allowed: true,
|
|
590
|
-
retryAfterMs: 0
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
return {
|
|
594
|
-
allowed: true,
|
|
595
|
-
retryAfterMs: 0
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
recordFailure(key) {
|
|
599
|
-
this.prune();
|
|
600
|
-
const now = Date.now();
|
|
601
|
-
const current = this.failures.get(key);
|
|
602
|
-
const next = !current || now - current.windowStartMs >= VIEWER_FAILURE_WINDOW_MS ? {
|
|
603
|
-
windowStartMs: now,
|
|
604
|
-
failures: 1,
|
|
605
|
-
lockUntilMs: 0
|
|
606
|
-
} : {
|
|
607
|
-
...current,
|
|
608
|
-
failures: current.failures + 1
|
|
609
|
-
};
|
|
610
|
-
if (next.failures >= VIEWER_MAX_FAILURES_PER_WINDOW) next.lockUntilMs = now + VIEWER_LOCKOUT_MS;
|
|
611
|
-
this.failures.set(key, next);
|
|
612
|
-
}
|
|
613
|
-
reset(key) {
|
|
614
|
-
this.failures.delete(key);
|
|
615
|
-
}
|
|
616
|
-
prune() {
|
|
617
|
-
if (this.failures.size < VIEWER_LIMITER_MAX_KEYS) return;
|
|
618
|
-
const now = Date.now();
|
|
619
|
-
for (const [key, state] of this.failures) {
|
|
620
|
-
if (state.lockUntilMs <= now && now - state.windowStartMs >= VIEWER_FAILURE_WINDOW_MS) this.failures.delete(key);
|
|
621
|
-
if (this.failures.size < VIEWER_LIMITER_MAX_KEYS) return;
|
|
622
|
-
}
|
|
623
|
-
if (this.failures.size >= VIEWER_LIMITER_MAX_KEYS) this.failures.clear();
|
|
624
|
-
}
|
|
625
|
-
};
|
|
626
463
|
//#endregion
|
|
627
464
|
//#region extensions/diffs/src/prompt-guidance.ts
|
|
628
465
|
const DIFFS_AGENT_GUIDANCE = [
|
|
@@ -895,402 +732,6 @@ function isFileNotFound(error) {
|
|
|
895
732
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
896
733
|
}
|
|
897
734
|
//#endregion
|
|
898
|
-
//#region extensions/diffs/src/browser.ts
|
|
899
|
-
const DEFAULT_BROWSER_IDLE_MS = 3e4;
|
|
900
|
-
const SHARED_BROWSER_KEY = "__default__";
|
|
901
|
-
const IMAGE_SIZE_LIMIT_ERROR = "Diff frame did not render within image size limits.";
|
|
902
|
-
const PDF_REFERENCE_PAGE_HEIGHT_PX = 1056;
|
|
903
|
-
const MAX_PDF_PAGES = 50;
|
|
904
|
-
const LOCAL_VIEWER_BASE_HREF = "http://127.0.0.1/plugins/diffs/view/local/local";
|
|
905
|
-
let sharedBrowserState = null;
|
|
906
|
-
let executablePathCache = null;
|
|
907
|
-
var PlaywrightDiffScreenshotter = class {
|
|
908
|
-
constructor(params) {
|
|
909
|
-
this.config = params.config;
|
|
910
|
-
this.browserIdleMs = params.browserIdleMs ?? DEFAULT_BROWSER_IDLE_MS;
|
|
911
|
-
}
|
|
912
|
-
async screenshotHtml(params) {
|
|
913
|
-
let lease;
|
|
914
|
-
try {
|
|
915
|
-
lease = await acquireSharedBrowser({
|
|
916
|
-
config: this.config,
|
|
917
|
-
idleMs: this.browserIdleMs
|
|
918
|
-
});
|
|
919
|
-
} catch (error) {
|
|
920
|
-
throw buildBrowserUnavailableError(error);
|
|
921
|
-
}
|
|
922
|
-
let page;
|
|
923
|
-
let currentScale = params.image.scale;
|
|
924
|
-
const maxRetries = 2;
|
|
925
|
-
try {
|
|
926
|
-
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
927
|
-
try {
|
|
928
|
-
page = await lease.browser.newPage({
|
|
929
|
-
viewport: {
|
|
930
|
-
width: Math.max(Math.ceil(params.image.maxWidth + 240), 1200),
|
|
931
|
-
height: 900
|
|
932
|
-
},
|
|
933
|
-
deviceScaleFactor: currentScale,
|
|
934
|
-
colorScheme: params.theme
|
|
935
|
-
});
|
|
936
|
-
} catch (error) {
|
|
937
|
-
throw buildBrowserUnavailableError(error);
|
|
938
|
-
}
|
|
939
|
-
await page.route("**/*", async (route) => {
|
|
940
|
-
const requestUrl = route.request().url();
|
|
941
|
-
if (requestUrl === "about:blank" || requestUrl.startsWith("data:")) {
|
|
942
|
-
await route.continue();
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
let parsed;
|
|
946
|
-
try {
|
|
947
|
-
parsed = new URL(requestUrl);
|
|
948
|
-
} catch {
|
|
949
|
-
await route.abort();
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
if (parsed.protocol !== "http:" || parsed.hostname !== "127.0.0.1") {
|
|
953
|
-
await route.abort();
|
|
954
|
-
return;
|
|
955
|
-
}
|
|
956
|
-
const isBaseViewerAsset = parsed.pathname.startsWith(VIEWER_ASSET_PREFIX);
|
|
957
|
-
const isLanguagePackViewerAsset = parsed.pathname.startsWith(LANGUAGE_PACK_VIEWER_ASSET_PREFIX);
|
|
958
|
-
if (!isBaseViewerAsset && !isLanguagePackViewerAsset) {
|
|
959
|
-
await route.abort();
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
const pathname = parsed.pathname;
|
|
963
|
-
const asset = isLanguagePackViewerAsset ? await getServedLanguagePackViewerAsset(pathname) : await getServedViewerAsset(pathname);
|
|
964
|
-
if (!asset) {
|
|
965
|
-
await route.abort();
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
await route.fulfill({
|
|
969
|
-
status: 200,
|
|
970
|
-
contentType: asset.contentType,
|
|
971
|
-
body: asset.body
|
|
972
|
-
});
|
|
973
|
-
});
|
|
974
|
-
await page.setContent(injectBaseHref(params.html), { waitUntil: "load" });
|
|
975
|
-
await page.waitForFunction(() => {
|
|
976
|
-
if (document.documentElement.dataset.openclawDiffsReady === "true") return true;
|
|
977
|
-
return [...document.querySelectorAll("[data-openclaw-diff-host]")].every((element) => {
|
|
978
|
-
return element instanceof HTMLElement && element.shadowRoot?.querySelector("[data-diffs]");
|
|
979
|
-
});
|
|
980
|
-
}, { timeout: 1e4 });
|
|
981
|
-
await page.evaluate(async () => {
|
|
982
|
-
await document.fonts.ready;
|
|
983
|
-
});
|
|
984
|
-
await page.evaluate(() => {
|
|
985
|
-
const frame = document.querySelector(".oc-frame");
|
|
986
|
-
if (frame instanceof HTMLElement) frame.dataset.renderMode = "image";
|
|
987
|
-
});
|
|
988
|
-
const frame = page.locator(".oc-frame");
|
|
989
|
-
await frame.waitFor();
|
|
990
|
-
const initialBox = await frame.boundingBox();
|
|
991
|
-
if (!initialBox) throw new Error("Diff frame did not render.");
|
|
992
|
-
const isPdf = params.image.format === "pdf";
|
|
993
|
-
const padding = isPdf ? 0 : 20;
|
|
994
|
-
const clipWidth = Math.ceil(initialBox.width + padding * 2);
|
|
995
|
-
const clipHeight = Math.ceil(Math.max(initialBox.height + padding * 2, 320));
|
|
996
|
-
await page.setViewportSize({
|
|
997
|
-
width: Math.max(clipWidth + padding, 900),
|
|
998
|
-
height: Math.max(clipHeight + padding, 700)
|
|
999
|
-
});
|
|
1000
|
-
const box = await frame.boundingBox();
|
|
1001
|
-
if (!box) throw new Error("Diff frame was lost after resizing.");
|
|
1002
|
-
if (isPdf) {
|
|
1003
|
-
await page.emulateMedia({ media: "screen" });
|
|
1004
|
-
await page.evaluate(() => {
|
|
1005
|
-
const html = document.documentElement;
|
|
1006
|
-
const body = document.body;
|
|
1007
|
-
const frameLocal = document.querySelector(".oc-frame");
|
|
1008
|
-
html.style.background = "transparent";
|
|
1009
|
-
body.style.margin = "0";
|
|
1010
|
-
body.style.padding = "0";
|
|
1011
|
-
body.style.background = "transparent";
|
|
1012
|
-
body.style.setProperty("-webkit-print-color-adjust", "exact");
|
|
1013
|
-
if (frameLocal instanceof HTMLElement) frameLocal.style.margin = "0";
|
|
1014
|
-
});
|
|
1015
|
-
const pdfBox = await frame.boundingBox();
|
|
1016
|
-
if (!pdfBox) throw new Error("Diff frame was lost before PDF render.");
|
|
1017
|
-
const pdfWidth = Math.max(Math.ceil(pdfBox.width), 1);
|
|
1018
|
-
const pdfHeight = Math.max(Math.ceil(pdfBox.height), 1);
|
|
1019
|
-
const estimatedPixels = pdfWidth * pdfHeight;
|
|
1020
|
-
const estimatedPages = Math.ceil(pdfHeight / PDF_REFERENCE_PAGE_HEIGHT_PX);
|
|
1021
|
-
if (estimatedPixels > params.image.maxPixels || estimatedPages > MAX_PDF_PAGES) throw new Error(IMAGE_SIZE_LIMIT_ERROR);
|
|
1022
|
-
const pageForPdf = page;
|
|
1023
|
-
await writeExternalArtifactFile({
|
|
1024
|
-
outputPath: params.outputPath,
|
|
1025
|
-
write: async (tempPath) => {
|
|
1026
|
-
await pageForPdf.pdf({
|
|
1027
|
-
path: tempPath,
|
|
1028
|
-
width: `${pdfWidth}px`,
|
|
1029
|
-
height: `${pdfHeight}px`,
|
|
1030
|
-
printBackground: true,
|
|
1031
|
-
margin: {
|
|
1032
|
-
top: "0",
|
|
1033
|
-
right: "0",
|
|
1034
|
-
bottom: "0",
|
|
1035
|
-
left: "0"
|
|
1036
|
-
}
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
});
|
|
1040
|
-
return params.outputPath;
|
|
1041
|
-
}
|
|
1042
|
-
const dpr = await page.evaluate(() => window.devicePixelRatio || 1);
|
|
1043
|
-
const rawX = Math.max(box.x - padding, 0);
|
|
1044
|
-
const rawY = Math.max(box.y - padding, 0);
|
|
1045
|
-
const rawRight = rawX + clipWidth;
|
|
1046
|
-
const rawBottom = rawY + clipHeight;
|
|
1047
|
-
const x = Math.floor(rawX * dpr) / dpr;
|
|
1048
|
-
const y = Math.floor(rawY * dpr) / dpr;
|
|
1049
|
-
const right = Math.ceil(rawRight * dpr) / dpr;
|
|
1050
|
-
const bottom = Math.ceil(rawBottom * dpr) / dpr;
|
|
1051
|
-
const cssWidth = Math.max(right - x, 1);
|
|
1052
|
-
const cssHeight = Math.max(bottom - y, 1);
|
|
1053
|
-
if (cssWidth * cssHeight * dpr * dpr > params.image.maxPixels) {
|
|
1054
|
-
if (currentScale > 1) {
|
|
1055
|
-
const maxScaleForPixels = Math.sqrt(params.image.maxPixels / (cssWidth * cssHeight));
|
|
1056
|
-
const reducedScale = Math.max(1, Math.round(Math.min(currentScale, maxScaleForPixels) * 100) / 100);
|
|
1057
|
-
if (reducedScale < currentScale - .01 && attempt < maxRetries) {
|
|
1058
|
-
await page.close().catch(() => {});
|
|
1059
|
-
page = void 0;
|
|
1060
|
-
currentScale = reducedScale;
|
|
1061
|
-
continue;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
throw new Error(IMAGE_SIZE_LIMIT_ERROR);
|
|
1065
|
-
}
|
|
1066
|
-
const pageForScreenshot = page;
|
|
1067
|
-
await writeExternalArtifactFile({
|
|
1068
|
-
outputPath: params.outputPath,
|
|
1069
|
-
write: async (tempPath) => {
|
|
1070
|
-
await pageForScreenshot.screenshot({
|
|
1071
|
-
path: tempPath,
|
|
1072
|
-
type: "png",
|
|
1073
|
-
scale: "device",
|
|
1074
|
-
clip: {
|
|
1075
|
-
x,
|
|
1076
|
-
y,
|
|
1077
|
-
width: cssWidth,
|
|
1078
|
-
height: cssHeight
|
|
1079
|
-
}
|
|
1080
|
-
});
|
|
1081
|
-
}
|
|
1082
|
-
});
|
|
1083
|
-
return params.outputPath;
|
|
1084
|
-
}
|
|
1085
|
-
throw new Error(IMAGE_SIZE_LIMIT_ERROR);
|
|
1086
|
-
} finally {
|
|
1087
|
-
await page?.close().catch(() => {});
|
|
1088
|
-
await lease.release();
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
};
|
|
1092
|
-
function buildBrowserUnavailableError(error) {
|
|
1093
|
-
const reason = formatErrorMessage(error);
|
|
1094
|
-
return new Error(`Diff PNG/PDF rendering requires a Chromium-compatible browser. Set browser.executablePath or install Chrome/Chromium. ${reason}`, { cause: error });
|
|
1095
|
-
}
|
|
1096
|
-
async function writeExternalArtifactFile(params) {
|
|
1097
|
-
const rootDir = path.dirname(params.outputPath);
|
|
1098
|
-
await fs$1.mkdir(rootDir, { recursive: true });
|
|
1099
|
-
await writeExternalFileWithinRoot({
|
|
1100
|
-
rootDir,
|
|
1101
|
-
path: path.basename(params.outputPath),
|
|
1102
|
-
write: params.write
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
function injectBaseHref(html) {
|
|
1106
|
-
if (html.includes("<base ")) return html;
|
|
1107
|
-
return html.replace("<head>", `<head><base href="${LOCAL_VIEWER_BASE_HREF}" />`);
|
|
1108
|
-
}
|
|
1109
|
-
async function resolveBrowserExecutablePath(config) {
|
|
1110
|
-
const cacheKey = JSON.stringify({
|
|
1111
|
-
configPath: config.browser?.executablePath?.trim() || "",
|
|
1112
|
-
env: [
|
|
1113
|
-
process.env.OPENCLAW_BROWSER_EXECUTABLE_PATH ?? "",
|
|
1114
|
-
process.env.BROWSER_EXECUTABLE_PATH ?? "",
|
|
1115
|
-
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ?? ""
|
|
1116
|
-
],
|
|
1117
|
-
path: process.env.PATH ?? ""
|
|
1118
|
-
});
|
|
1119
|
-
if (executablePathCache?.key === cacheKey) return await executablePathCache.valuePromise;
|
|
1120
|
-
const valuePromise = resolveBrowserExecutablePathUncached(config).catch((error) => {
|
|
1121
|
-
if (executablePathCache?.valuePromise === valuePromise) executablePathCache = null;
|
|
1122
|
-
throw error;
|
|
1123
|
-
});
|
|
1124
|
-
executablePathCache = {
|
|
1125
|
-
key: cacheKey,
|
|
1126
|
-
valuePromise
|
|
1127
|
-
};
|
|
1128
|
-
return await valuePromise;
|
|
1129
|
-
}
|
|
1130
|
-
async function resolveBrowserExecutablePathUncached(config) {
|
|
1131
|
-
const configPath = config.browser?.executablePath?.trim();
|
|
1132
|
-
if (configPath) {
|
|
1133
|
-
await assertExecutable(configPath, "browser.executablePath");
|
|
1134
|
-
return configPath;
|
|
1135
|
-
}
|
|
1136
|
-
const envCandidates = [
|
|
1137
|
-
process.env.OPENCLAW_BROWSER_EXECUTABLE_PATH,
|
|
1138
|
-
process.env.BROWSER_EXECUTABLE_PATH,
|
|
1139
|
-
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
|
1140
|
-
].map((value) => value?.trim()).filter((value) => Boolean(value));
|
|
1141
|
-
for (const candidate of envCandidates) if (await isExecutable(candidate)) return candidate;
|
|
1142
|
-
for (const candidate of await collectExecutableCandidates()) if (await isExecutable(candidate)) return candidate;
|
|
1143
|
-
}
|
|
1144
|
-
async function acquireSharedBrowser(params) {
|
|
1145
|
-
const executablePath = await resolveBrowserExecutablePath(params.config);
|
|
1146
|
-
const desiredKey = executablePath || SHARED_BROWSER_KEY;
|
|
1147
|
-
if (sharedBrowserState && sharedBrowserState.key !== desiredKey) await closeSharedBrowser();
|
|
1148
|
-
if (!sharedBrowserState) {
|
|
1149
|
-
const browserPromise = chromium.launch({
|
|
1150
|
-
headless: true,
|
|
1151
|
-
...executablePath ? { executablePath } : {},
|
|
1152
|
-
args: ["--disable-dev-shm-usage"]
|
|
1153
|
-
}).then((browser) => {
|
|
1154
|
-
if (sharedBrowserState?.browserPromise === browserPromise) {
|
|
1155
|
-
sharedBrowserState.browser = browser;
|
|
1156
|
-
browser.on("disconnected", () => {
|
|
1157
|
-
if (sharedBrowserState?.browser === browser) {
|
|
1158
|
-
clearIdleTimer(sharedBrowserState);
|
|
1159
|
-
sharedBrowserState = null;
|
|
1160
|
-
}
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1163
|
-
return browser;
|
|
1164
|
-
}).catch((error) => {
|
|
1165
|
-
if (sharedBrowserState?.browserPromise === browserPromise) sharedBrowserState = null;
|
|
1166
|
-
throw error;
|
|
1167
|
-
});
|
|
1168
|
-
sharedBrowserState = {
|
|
1169
|
-
browserPromise,
|
|
1170
|
-
idleTimer: null,
|
|
1171
|
-
key: desiredKey,
|
|
1172
|
-
users: 0
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
clearIdleTimer(sharedBrowserState);
|
|
1176
|
-
const state = sharedBrowserState;
|
|
1177
|
-
const browser = await state.browserPromise;
|
|
1178
|
-
state.users += 1;
|
|
1179
|
-
let released = false;
|
|
1180
|
-
return {
|
|
1181
|
-
browser,
|
|
1182
|
-
release: async () => {
|
|
1183
|
-
if (released) return;
|
|
1184
|
-
released = true;
|
|
1185
|
-
state.users = Math.max(0, state.users - 1);
|
|
1186
|
-
if (state.users === 0) scheduleIdleBrowserClose(state, params.idleMs);
|
|
1187
|
-
}
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
function scheduleIdleBrowserClose(state, idleMs) {
|
|
1191
|
-
clearIdleTimer(state);
|
|
1192
|
-
state.idleTimer = setTimeout(() => {
|
|
1193
|
-
if (sharedBrowserState === state && state.users === 0) closeSharedBrowser();
|
|
1194
|
-
}, idleMs);
|
|
1195
|
-
state.idleTimer.unref();
|
|
1196
|
-
}
|
|
1197
|
-
function clearIdleTimer(state) {
|
|
1198
|
-
if (!state.idleTimer) return;
|
|
1199
|
-
clearTimeout(state.idleTimer);
|
|
1200
|
-
state.idleTimer = null;
|
|
1201
|
-
}
|
|
1202
|
-
async function closeSharedBrowser() {
|
|
1203
|
-
const state = sharedBrowserState;
|
|
1204
|
-
if (!state) return;
|
|
1205
|
-
sharedBrowserState = null;
|
|
1206
|
-
clearIdleTimer(state);
|
|
1207
|
-
await (state.browser ?? await state.browserPromise.catch(() => null))?.close().catch(() => {});
|
|
1208
|
-
}
|
|
1209
|
-
async function collectExecutableCandidates() {
|
|
1210
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
1211
|
-
for (const command of pathCommandsForPlatform()) {
|
|
1212
|
-
const resolved = await findExecutableInPath(command);
|
|
1213
|
-
if (resolved) candidates.add(resolved);
|
|
1214
|
-
}
|
|
1215
|
-
for (const candidate of commonExecutablePathsForPlatform()) candidates.add(candidate);
|
|
1216
|
-
return [...candidates];
|
|
1217
|
-
}
|
|
1218
|
-
function pathCommandsForPlatform() {
|
|
1219
|
-
if (process.platform === "win32") return [
|
|
1220
|
-
"chrome.exe",
|
|
1221
|
-
"msedge.exe",
|
|
1222
|
-
"brave.exe"
|
|
1223
|
-
];
|
|
1224
|
-
if (process.platform === "darwin") return [
|
|
1225
|
-
"google-chrome",
|
|
1226
|
-
"chromium",
|
|
1227
|
-
"msedge",
|
|
1228
|
-
"brave-browser",
|
|
1229
|
-
"brave"
|
|
1230
|
-
];
|
|
1231
|
-
return [
|
|
1232
|
-
"chromium",
|
|
1233
|
-
"chromium-browser",
|
|
1234
|
-
"google-chrome",
|
|
1235
|
-
"google-chrome-stable",
|
|
1236
|
-
"msedge",
|
|
1237
|
-
"brave-browser",
|
|
1238
|
-
"brave"
|
|
1239
|
-
];
|
|
1240
|
-
}
|
|
1241
|
-
function commonExecutablePathsForPlatform() {
|
|
1242
|
-
if (process.platform === "darwin") return [
|
|
1243
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1244
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
1245
|
-
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
1246
|
-
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
1247
|
-
];
|
|
1248
|
-
if (process.platform === "win32") {
|
|
1249
|
-
const joinWindowsPath = path.win32.join;
|
|
1250
|
-
const localAppData = normalizeOptionalString(process.env.LOCALAPPDATA) ?? joinWindowsPath(os.homedir(), "AppData", "Local");
|
|
1251
|
-
const programFiles = normalizeOptionalString(process.env.ProgramFiles) ?? "C:\\Program Files";
|
|
1252
|
-
const programFilesX86 = normalizeOptionalString(process.env["ProgramFiles(x86)"]) ?? "C:\\Program Files (x86)";
|
|
1253
|
-
return [
|
|
1254
|
-
joinWindowsPath(localAppData, "Google", "Chrome", "Application", "chrome.exe"),
|
|
1255
|
-
joinWindowsPath(programFiles, "Google", "Chrome", "Application", "chrome.exe"),
|
|
1256
|
-
joinWindowsPath(programFilesX86, "Google", "Chrome", "Application", "chrome.exe"),
|
|
1257
|
-
joinWindowsPath(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
1258
|
-
joinWindowsPath(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
|
|
1259
|
-
joinWindowsPath(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe"),
|
|
1260
|
-
joinWindowsPath(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1261
|
-
];
|
|
1262
|
-
}
|
|
1263
|
-
return [
|
|
1264
|
-
"/usr/bin/chromium",
|
|
1265
|
-
"/usr/bin/chromium-browser",
|
|
1266
|
-
"/usr/bin/google-chrome",
|
|
1267
|
-
"/usr/bin/google-chrome-stable",
|
|
1268
|
-
"/usr/bin/msedge",
|
|
1269
|
-
"/usr/bin/brave-browser",
|
|
1270
|
-
"/snap/bin/chromium"
|
|
1271
|
-
];
|
|
1272
|
-
}
|
|
1273
|
-
async function findExecutableInPath(command) {
|
|
1274
|
-
const pathValue = process.env.PATH;
|
|
1275
|
-
if (!pathValue) return;
|
|
1276
|
-
for (const directory of pathValue.split(path.delimiter)) {
|
|
1277
|
-
if (!directory) continue;
|
|
1278
|
-
const candidate = path.join(directory, command);
|
|
1279
|
-
if (await isExecutable(candidate)) return candidate;
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
async function assertExecutable(candidate, label) {
|
|
1283
|
-
if (!await isExecutable(candidate)) throw new Error(`${label} not found or not executable: ${candidate}`);
|
|
1284
|
-
}
|
|
1285
|
-
async function isExecutable(candidate) {
|
|
1286
|
-
try {
|
|
1287
|
-
await fs$1.access(candidate, constants.X_OK);
|
|
1288
|
-
return true;
|
|
1289
|
-
} catch {
|
|
1290
|
-
return false;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
//#endregion
|
|
1294
735
|
//#region extensions/diffs/src/shiki-curated-languages.ts
|
|
1295
736
|
const javascript = () => import("@shikijs/langs/javascript");
|
|
1296
737
|
const typescript = () => import("@shikijs/langs/typescript");
|
|
@@ -2160,6 +1601,7 @@ const MAX_TITLE_BYTES = 1024;
|
|
|
2160
1601
|
const MAX_PATH_BYTES = 2048;
|
|
2161
1602
|
const MAX_LANG_BYTES = 128;
|
|
2162
1603
|
const MAX_DIFF_ARTIFACT_TTL_SECONDS = 21600;
|
|
1604
|
+
const loadDiffsBrowserRuntime = createLazyRuntimeModule(() => import("./browser.runtime-_hkQDewM.js"));
|
|
2163
1605
|
const DiffsToolSchema = Type.Object({
|
|
2164
1606
|
before: Type.Optional(Type.String({ description: "Original text content." })),
|
|
2165
1607
|
after: Type.Optional(Type.String({ description: "Updated text content." })),
|
|
@@ -2203,14 +1645,15 @@ const DiffsToolSchema = Type.Object({
|
|
|
2203
1645
|
baseUrl: Type.Optional(Type.String({ description: "Optional gateway base URL override used when building the viewer URL. Overrides configured viewerBaseUrl, for example https://gateway.example.com." }))
|
|
2204
1646
|
}, { additionalProperties: false });
|
|
2205
1647
|
function createDiffsTool(params) {
|
|
1648
|
+
const loadScreenshotter = async () => params.screenshotter ?? new (await (loadDiffsBrowserRuntime())).PlaywrightDiffScreenshotter({ config: params.api.config });
|
|
2206
1649
|
return {
|
|
2207
1650
|
name: "diffs",
|
|
2208
1651
|
label: "Diffs",
|
|
2209
1652
|
description: "Create a read-only diff viewer from before/after text or a unified patch. Returns a gateway viewer URL for interactive viewing and can also render the same diff to a PNG or PDF.",
|
|
2210
1653
|
parameters: DiffsToolSchema,
|
|
2211
1654
|
execute: async (_toolCallId, rawParams) => {
|
|
2212
|
-
const toolParams = rawParams;
|
|
2213
|
-
const rawRecord =
|
|
1655
|
+
const toolParams = asNonArrayRecord(rawParams);
|
|
1656
|
+
const rawRecord = toolParams;
|
|
2214
1657
|
const artifactContext = buildArtifactContext(params.context);
|
|
2215
1658
|
const input = normalizeDiffInput(toolParams);
|
|
2216
1659
|
if (input.kind === "before_after" && input.before === input.after) return {
|
|
@@ -2252,10 +1695,9 @@ function createDiffsTool(params) {
|
|
|
2252
1695
|
if (error instanceof DiffRenderInputError) throw new PluginToolInputError(error.message);
|
|
2253
1696
|
throw error;
|
|
2254
1697
|
});
|
|
2255
|
-
const screenshotter = params.screenshotter ?? new PlaywrightDiffScreenshotter({ config: params.api.config });
|
|
2256
1698
|
if (isArtifactOnlyMode(mode)) {
|
|
2257
1699
|
const artifactFile = await renderDiffArtifactFile({
|
|
2258
|
-
screenshotter,
|
|
1700
|
+
screenshotter: await loadScreenshotter(),
|
|
2259
1701
|
store: params.store,
|
|
2260
1702
|
html: requireRenderedHtml(rendered.imageHtml, "image"),
|
|
2261
1703
|
theme,
|
|
@@ -2322,7 +1764,7 @@ function createDiffsTool(params) {
|
|
|
2322
1764
|
};
|
|
2323
1765
|
try {
|
|
2324
1766
|
const artifactFile = await renderDiffArtifactFile({
|
|
2325
|
-
screenshotter,
|
|
1767
|
+
screenshotter: await loadScreenshotter(),
|
|
2326
1768
|
store: params.store,
|
|
2327
1769
|
html: requireRenderedHtml(rendered.imageHtml, "image"),
|
|
2328
1770
|
theme,
|