@openclaw/diffs 2026.7.2-beta.2 → 2026.7.2-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 +2 -1
- package/dist/index.js +248 -211
- package/npm-shrinkwrap.json +2 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -220,7 +220,8 @@ diff --git a/src/example.ts b/src/example.ts
|
|
|
220
220
|
- Multi-file patches start with a changed-files summary card: totals, per-file `+N`/`-N` stats, change badges, and anchor links.
|
|
221
221
|
- Rendered PNG/PDF files keep the per-file header counts but omit the interactive view toggles.
|
|
222
222
|
- The viewer is hosted locally through the gateway under `/plugins/diffs/...`.
|
|
223
|
-
-
|
|
223
|
+
- Viewer HTML and metadata are ephemeral SQLite plugin blobs. The URL token is returned to the caller while SQLite stores only its SHA-256 hash.
|
|
224
|
+
- Rendered PNG/PDF files remain temporary materializations in `$TMPDIR/openclaw-diffs` because delivery APIs require a file path. No JSON metadata sidecars are written or imported.
|
|
224
225
|
- Default viewer URLs use loopback (`127.0.0.1`) unless you set plugin `viewerBaseUrl`, pass `baseUrl`, or use `gateway.bind=custom` + `gateway.customBindHost`.
|
|
225
226
|
- If `gateway.trustedProxies` includes loopback for a same-host proxy (for example Tailscale Serve), raw `127.0.0.1` viewer requests without forwarded client-IP headers fail closed by design.
|
|
226
227
|
- In that topology, prefer `mode=file` / `mode=both` for attachments, or intentionally enable remote viewers and set plugin `viewerBaseUrl` (or pass a proxy/public `baseUrl`) when you need a shareable viewer URL.
|
package/dist/index.js
CHANGED
|
@@ -10,8 +10,9 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "opencl
|
|
|
10
10
|
import crypto from "node:crypto";
|
|
11
11
|
import fs$1 from "node:fs/promises";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { gunzip, gzip } from "node:zlib";
|
|
13
14
|
import { MAX_DATE_TIMESTAMP_MS, timestampMsToIsoString } from "openclaw/plugin-sdk/number-runtime";
|
|
14
|
-
import {
|
|
15
|
+
import { safeEqualSecret, writeExternalFileWithinRoot } from "openclaw/plugin-sdk/security-runtime";
|
|
15
16
|
import { optionalFiniteNumberSchema, stringEnum } from "openclaw/plugin-sdk/channel-actions";
|
|
16
17
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
17
18
|
import { readFiniteNumberParam } from "openclaw/plugin-sdk/param-readers";
|
|
@@ -472,19 +473,20 @@ function createDiffsHttpHandler(params) {
|
|
|
472
473
|
respondText(res, 404, "Diff not found");
|
|
473
474
|
return true;
|
|
474
475
|
}
|
|
475
|
-
if (!await params.store.getArtifact(id, token)) {
|
|
476
|
-
recordRemoteFailure(viewerFailureLimiter, access);
|
|
477
|
-
respondText(res, 404, "Diff not found or expired");
|
|
478
|
-
return true;
|
|
479
|
-
}
|
|
480
476
|
try {
|
|
481
|
-
const
|
|
477
|
+
const viewer = await params.store.readAuthorizedViewer(id, token);
|
|
478
|
+
if (!viewer) {
|
|
479
|
+
recordRemoteFailure(viewerFailureLimiter, access);
|
|
480
|
+
respondText(res, 404, "Diff not found or expired");
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
482
483
|
resetRemoteFailures(viewerFailureLimiter, access);
|
|
483
484
|
res.statusCode = 200;
|
|
484
485
|
setSharedHeaders(res, "text/html; charset=utf-8");
|
|
485
486
|
res.setHeader("content-security-policy", VIEWER_CONTENT_SECURITY_POLICY);
|
|
487
|
+
res.setHeader("content-length", String(viewer.html.byteLength));
|
|
486
488
|
if (req.method === "HEAD") res.end();
|
|
487
|
-
else res.end(html);
|
|
489
|
+
else res.end(Buffer.from(viewer.html));
|
|
488
490
|
return true;
|
|
489
491
|
} catch (error) {
|
|
490
492
|
recordRemoteFailure(viewerFailureLimiter, access);
|
|
@@ -634,134 +636,167 @@ const DEFAULT_TTL_MS = 1800 * 1e3;
|
|
|
634
636
|
const MAX_TTL_MS = 360 * 60 * 1e3;
|
|
635
637
|
const SWEEP_FALLBACK_AGE_MS = 1440 * 60 * 1e3;
|
|
636
638
|
const DEFAULT_CLEANUP_INTERVAL_MS = 300 * 1e3;
|
|
639
|
+
const MAX_DECODED_HTML_BYTES = 64 * 1024 * 1024;
|
|
640
|
+
const ARTIFACT_ID_ATTEMPTS = 8;
|
|
637
641
|
const VIEWER_PREFIX = "/plugins/diffs/view";
|
|
642
|
+
const EMPTY_BLOB = /* @__PURE__ */ new Uint8Array();
|
|
643
|
+
function isBlobLimitError(error) {
|
|
644
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "PLUGIN_BLOB_LIMIT_EXCEEDED";
|
|
645
|
+
}
|
|
638
646
|
var DiffArtifactStore = class {
|
|
639
647
|
constructor(params) {
|
|
648
|
+
this.renderingFileIds = /* @__PURE__ */ new Set();
|
|
640
649
|
this.cleanupInFlight = null;
|
|
641
650
|
this.nextCleanupAt = 0;
|
|
642
651
|
this.rootDir = path.resolve(params.rootDir);
|
|
652
|
+
this.blobStore = params.blobStore;
|
|
643
653
|
this.logger = params.logger;
|
|
644
654
|
this.cleanupIntervalMs = params.cleanupIntervalMs === void 0 ? DEFAULT_CLEANUP_INTERVAL_MS : Math.max(0, Math.floor(params.cleanupIntervalMs));
|
|
645
655
|
}
|
|
646
656
|
async createArtifact(params) {
|
|
647
|
-
|
|
648
|
-
|
|
657
|
+
const html = Buffer.from(params.html, "utf8");
|
|
658
|
+
if (html.byteLength > MAX_DECODED_HTML_BYTES) throw new Error(`Diff viewer HTML exceeds ${MAX_DECODED_HTML_BYTES} bytes.`);
|
|
659
|
+
const compressedHtml = await gzipAsync(html);
|
|
649
660
|
const token = crypto.randomBytes(24).toString("hex");
|
|
650
|
-
const artifactDir = this.artifactDir(id);
|
|
651
|
-
const htmlPath = path.join(artifactDir, "viewer.html");
|
|
652
661
|
const ttlMs = normalizeTtlMs$1(params.ttlMs);
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
token,
|
|
662
|
+
const metadata = {
|
|
663
|
+
version: 1,
|
|
664
|
+
kind: "viewer",
|
|
665
|
+
encoding: "gzip",
|
|
666
|
+
tokenHash: hashToken(token),
|
|
659
667
|
title: params.title,
|
|
660
668
|
inputKind: params.inputKind,
|
|
661
669
|
fileCount: params.fileCount,
|
|
662
|
-
|
|
663
|
-
expiresAt,
|
|
664
|
-
viewerPath: `${VIEWER_PREFIX}/${id}/${token}`,
|
|
665
|
-
htmlPath,
|
|
670
|
+
decodedBytes: html.byteLength,
|
|
666
671
|
...params.context ? { context: params.context } : {}
|
|
667
672
|
};
|
|
668
|
-
const
|
|
669
|
-
await root.mkdir(id);
|
|
670
|
-
await root.write(path.posix.join(id, "viewer.html"), params.html);
|
|
671
|
-
await this.writeMeta(meta);
|
|
673
|
+
const entry = await this.registerUnique(compressedHtml, metadata, ttlMs);
|
|
672
674
|
this.scheduleCleanup();
|
|
673
|
-
return
|
|
675
|
+
return viewerEntryToMeta(entry, token);
|
|
674
676
|
}
|
|
675
|
-
async
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if (!
|
|
679
|
-
|
|
680
|
-
await this.
|
|
677
|
+
async readAuthorizedViewer(id, token) {
|
|
678
|
+
if (!DIFF_ARTIFACT_ID_PATTERN.test(id) || !DIFF_ARTIFACT_TOKEN_PATTERN.test(token)) return null;
|
|
679
|
+
const entry = await this.blobStore.lookup(id);
|
|
680
|
+
if (!entry) {
|
|
681
|
+
const expired = await this.blobStore.deleteExpiredKey(id);
|
|
682
|
+
if (expired) await this.deleteExpiredFile(expired);
|
|
681
683
|
return null;
|
|
682
684
|
}
|
|
683
|
-
return
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
async updateFilePath(id, filePath) {
|
|
692
|
-
const meta = await this.readMeta(id);
|
|
693
|
-
if (!meta) throw new Error(`Diff artifact not found: ${id}`);
|
|
694
|
-
const normalizedFilePath = this.normalizeStoredPath(filePath, "filePath");
|
|
695
|
-
const next = {
|
|
696
|
-
...meta,
|
|
697
|
-
filePath: normalizedFilePath,
|
|
698
|
-
imagePath: normalizedFilePath
|
|
685
|
+
if (!isViewerMetadata(entry.metadata)) return null;
|
|
686
|
+
if (!safeEqualSecret(hashToken(token), entry.metadata.tokenHash)) return null;
|
|
687
|
+
const html = await gunzipAsync(entry.bytes, MAX_DECODED_HTML_BYTES);
|
|
688
|
+
if (html.byteLength !== entry.metadata.decodedBytes) throw new Error(`Diff artifact ${id} decoded size does not match its metadata.`);
|
|
689
|
+
return {
|
|
690
|
+
artifact: viewerEntryToMeta(entry, token),
|
|
691
|
+
html
|
|
699
692
|
};
|
|
700
|
-
await this.writeMeta(next);
|
|
701
|
-
return next;
|
|
702
|
-
}
|
|
703
|
-
async updateImagePath(id, imagePath) {
|
|
704
|
-
return this.updateFilePath(id, imagePath);
|
|
705
|
-
}
|
|
706
|
-
allocateFilePath(id, format = "png") {
|
|
707
|
-
return path.join(this.artifactDir(id), `preview.${format}`);
|
|
708
693
|
}
|
|
709
694
|
async createStandaloneFileArtifact(params = {}) {
|
|
710
|
-
await this.ensureRoot();
|
|
711
|
-
const id = crypto.randomBytes(10).toString("hex");
|
|
712
|
-
const artifactDir = this.artifactDir(id);
|
|
713
695
|
const format = params.format ?? "png";
|
|
714
|
-
const filePath = path.join(artifactDir, `preview.${format}`);
|
|
715
696
|
const ttlMs = normalizeTtlMs$1(params.ttlMs);
|
|
716
|
-
const
|
|
717
|
-
|
|
718
|
-
kind: "
|
|
719
|
-
|
|
720
|
-
createdAt: createdAt.toISOString(),
|
|
721
|
-
expiresAt: resolveExpiresAtIso(createdAt.getTime(), ttlMs),
|
|
722
|
-
filePath: this.normalizeStoredPath(filePath, "filePath"),
|
|
697
|
+
const metadata = {
|
|
698
|
+
version: 1,
|
|
699
|
+
kind: "rendered_file",
|
|
700
|
+
format,
|
|
723
701
|
...params.context ? { context: params.context } : {}
|
|
724
702
|
};
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
703
|
+
for (let attempt = 0; attempt < ARTIFACT_ID_ATTEMPTS; attempt += 1) {
|
|
704
|
+
const id = crypto.randomBytes(10).toString("hex");
|
|
705
|
+
if (!await this.registerIfAbsentWithCleanup(id, EMPTY_BLOB, metadata, ttlMs)) continue;
|
|
706
|
+
const artifactDir = this.artifactDir(id);
|
|
707
|
+
try {
|
|
708
|
+
await fs$1.mkdir(this.rootDir, { recursive: true });
|
|
709
|
+
await fs$1.mkdir(artifactDir);
|
|
710
|
+
const entry = await this.blobStore.lookup(id);
|
|
711
|
+
if (!entry || !isRenderedFileMetadata(entry.metadata)) throw new Error(`Diff file artifact expired before materialization: ${id}`);
|
|
712
|
+
this.renderingFileIds.add(id);
|
|
713
|
+
this.scheduleCleanup();
|
|
714
|
+
return {
|
|
715
|
+
id,
|
|
716
|
+
filePath: path.join(artifactDir, `preview.${format}`),
|
|
717
|
+
expiresAt: resolveEntryExpiresAt(entry),
|
|
718
|
+
...params.context ? { context: params.context } : {}
|
|
719
|
+
};
|
|
720
|
+
} catch (error) {
|
|
721
|
+
await this.blobStore.delete(id).catch(() => false);
|
|
722
|
+
await fs$1.rm(artifactDir, {
|
|
723
|
+
recursive: true,
|
|
724
|
+
force: true
|
|
725
|
+
}).catch(() => {});
|
|
726
|
+
if (isFileExists(error)) continue;
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
throw new Error("Failed to allocate a unique diff file artifact id.");
|
|
731
|
+
}
|
|
732
|
+
async completeFileArtifact(id) {
|
|
733
|
+
try {
|
|
734
|
+
const entry = await this.blobStore.lookup(id);
|
|
735
|
+
if (!entry || !isRenderedFileMetadata(entry.metadata)) {
|
|
736
|
+
await fs$1.rm(this.artifactDir(id), {
|
|
737
|
+
recursive: true,
|
|
738
|
+
force: true
|
|
739
|
+
}).catch(() => {});
|
|
740
|
+
throw new Error(`Diff file artifact expired during rendering: ${id}`);
|
|
741
|
+
}
|
|
742
|
+
} finally {
|
|
743
|
+
this.renderingFileIds.delete(id);
|
|
744
|
+
}
|
|
734
745
|
}
|
|
735
|
-
|
|
736
|
-
|
|
746
|
+
async deleteFileArtifact(id) {
|
|
747
|
+
this.renderingFileIds.delete(id);
|
|
748
|
+
await this.blobStore.delete(id).catch(() => false);
|
|
749
|
+
await fs$1.rm(this.artifactDir(id), {
|
|
750
|
+
recursive: true,
|
|
751
|
+
force: true
|
|
752
|
+
}).catch(() => {});
|
|
737
753
|
}
|
|
738
754
|
scheduleCleanup() {
|
|
739
755
|
this.maybeCleanupExpired();
|
|
740
756
|
}
|
|
741
757
|
async cleanupExpired() {
|
|
742
|
-
const
|
|
758
|
+
const expired = await this.blobStore.deleteExpired();
|
|
759
|
+
await Promise.all(expired.map(async (entry) => await this.deleteExpiredFile(entry)));
|
|
760
|
+
const entries = await fs$1.readdir(this.rootDir, { withFileTypes: true }).catch((error) => {
|
|
761
|
+
if (isFileNotFound(error)) return [];
|
|
762
|
+
throw error;
|
|
763
|
+
});
|
|
743
764
|
const now = Date.now();
|
|
744
|
-
await Promise.all(entries.filter((entry) => entry.isDirectory).map(async (entry) => {
|
|
745
|
-
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
if (standaloneMeta) {
|
|
753
|
-
if (isExpired(standaloneMeta)) await this.deleteArtifact(id);
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
if (now - entry.mtimeMs > SWEEP_FALLBACK_AGE_MS) await this.deleteArtifact(id);
|
|
765
|
+
await Promise.all(entries.filter((entry) => entry.isDirectory() && DIFF_ARTIFACT_ID_PATTERN.test(entry.name)).map(async (entry) => {
|
|
766
|
+
if (this.renderingFileIds.has(entry.name) || await this.blobStore.lookup(entry.name)) return;
|
|
767
|
+
const artifactDir = this.artifactDir(entry.name);
|
|
768
|
+
const stats = await fs$1.stat(artifactDir).catch(() => null);
|
|
769
|
+
if (stats && now - stats.mtimeMs > SWEEP_FALLBACK_AGE_MS) await fs$1.rm(artifactDir, {
|
|
770
|
+
recursive: true,
|
|
771
|
+
force: true
|
|
772
|
+
}).catch(() => {});
|
|
757
773
|
}));
|
|
758
774
|
}
|
|
759
|
-
async
|
|
760
|
-
|
|
775
|
+
async registerUnique(bytes, metadata, ttlMs) {
|
|
776
|
+
for (let attempt = 0; attempt < ARTIFACT_ID_ATTEMPTS; attempt += 1) {
|
|
777
|
+
const id = crypto.randomBytes(10).toString("hex");
|
|
778
|
+
if (!await this.registerIfAbsentWithCleanup(id, bytes, metadata, ttlMs)) continue;
|
|
779
|
+
const entry = await this.blobStore.lookup(id);
|
|
780
|
+
if (entry) return entry;
|
|
781
|
+
}
|
|
782
|
+
throw new Error("Failed to allocate a unique diff artifact id.");
|
|
761
783
|
}
|
|
762
|
-
async
|
|
763
|
-
|
|
764
|
-
|
|
784
|
+
async registerIfAbsentWithCleanup(id, bytes, metadata, ttlMs) {
|
|
785
|
+
try {
|
|
786
|
+
return await this.blobStore.registerIfAbsent(id, bytes, metadata, { ttlMs });
|
|
787
|
+
} catch (error) {
|
|
788
|
+
if (!isBlobLimitError(error)) throw error;
|
|
789
|
+
await this.cleanupExpired();
|
|
790
|
+
return await this.blobStore.registerIfAbsent(id, bytes, metadata, { ttlMs });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
async deleteExpiredFile(entry) {
|
|
794
|
+
if (!isRenderedFileMetadata(entry.metadata) || this.renderingFileIds.has(entry.key)) return;
|
|
795
|
+
if (await this.blobStore.lookup(entry.key)) return;
|
|
796
|
+
await fs$1.rm(this.artifactDir(entry.key), {
|
|
797
|
+
recursive: true,
|
|
798
|
+
force: true
|
|
799
|
+
}).catch(() => {});
|
|
765
800
|
}
|
|
766
801
|
maybeCleanupExpired() {
|
|
767
802
|
const now = Date.now();
|
|
@@ -776,104 +811,85 @@ var DiffArtifactStore = class {
|
|
|
776
811
|
this.cleanupInFlight = cleanupPromise;
|
|
777
812
|
}
|
|
778
813
|
artifactDir(id) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
async writeMeta(meta) {
|
|
782
|
-
await this.writeJsonMeta(meta.id, "meta.json", meta);
|
|
783
|
-
}
|
|
784
|
-
async readMeta(id) {
|
|
785
|
-
const parsed = await this.readJsonMeta(id, "meta.json", "diff artifact");
|
|
786
|
-
if (!parsed) return null;
|
|
787
|
-
return parsed;
|
|
788
|
-
}
|
|
789
|
-
async writeStandaloneMeta(meta) {
|
|
790
|
-
await this.writeJsonMeta(meta.id, "file-meta.json", meta);
|
|
791
|
-
}
|
|
792
|
-
async readStandaloneMeta(id) {
|
|
793
|
-
const parsed = await this.readJsonMeta(id, "file-meta.json", "standalone diff");
|
|
794
|
-
if (!parsed) return null;
|
|
795
|
-
try {
|
|
796
|
-
const value = parsed;
|
|
797
|
-
if (value.kind !== "standalone_file" || typeof value.id !== "string" || typeof value.createdAt !== "string" || typeof value.expiresAt !== "string" || typeof value.filePath !== "string") return null;
|
|
798
|
-
return {
|
|
799
|
-
kind: value.kind,
|
|
800
|
-
id: value.id,
|
|
801
|
-
createdAt: value.createdAt,
|
|
802
|
-
expiresAt: value.expiresAt,
|
|
803
|
-
filePath: this.normalizeStoredPath(value.filePath, "filePath"),
|
|
804
|
-
...value.context ? { context: normalizeArtifactContext(value.context) } : {}
|
|
805
|
-
};
|
|
806
|
-
} catch (error) {
|
|
807
|
-
this.logger?.warn(`Failed to normalize standalone diff metadata for ${id}: ${String(error)}`);
|
|
808
|
-
return null;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
async writeJsonMeta(id, fileName, data) {
|
|
812
|
-
await (await this.artifactRoot()).writeJson(path.posix.join(id, fileName), data, { space: 2 });
|
|
813
|
-
}
|
|
814
|
-
async readJsonMeta(id, fileName, context) {
|
|
815
|
-
try {
|
|
816
|
-
const raw = await (await this.artifactRoot()).readText(path.posix.join(id, fileName));
|
|
817
|
-
return JSON.parse(raw);
|
|
818
|
-
} catch (error) {
|
|
819
|
-
if (isFileNotFound(error)) return null;
|
|
820
|
-
this.logger?.warn(`Failed to read ${context} metadata for ${id}: ${String(error)}`);
|
|
821
|
-
return null;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
async deleteArtifact(id) {
|
|
825
|
-
await fs$1.rm(this.artifactDir(id), {
|
|
826
|
-
recursive: true,
|
|
827
|
-
force: true
|
|
828
|
-
}).catch(() => {});
|
|
829
|
-
}
|
|
830
|
-
resolveWithinRoot(...parts) {
|
|
831
|
-
const candidate = path.resolve(this.rootDir, ...parts);
|
|
832
|
-
this.assertWithinRoot(candidate);
|
|
833
|
-
return candidate;
|
|
834
|
-
}
|
|
835
|
-
normalizeStoredPath(rawPath, label) {
|
|
836
|
-
const candidate = path.isAbsolute(rawPath) ? path.resolve(rawPath) : path.resolve(this.rootDir, rawPath);
|
|
837
|
-
this.assertWithinRoot(candidate, label);
|
|
838
|
-
return candidate;
|
|
839
|
-
}
|
|
840
|
-
relativeStoredPath(storedPath) {
|
|
841
|
-
return path.relative(this.rootDir, this.normalizeStoredPath(storedPath, "path")).split(path.sep).join(path.posix.sep);
|
|
842
|
-
}
|
|
843
|
-
assertWithinRoot(candidate, label = "path") {
|
|
844
|
-
const relative = path.relative(this.rootDir, candidate);
|
|
845
|
-
if (relative === "" || !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)) return;
|
|
846
|
-
throw new Error(`Diff artifact ${label} escapes store root: ${candidate}`);
|
|
814
|
+
if (!DIFF_ARTIFACT_ID_PATTERN.test(id)) throw new Error(`Invalid diff artifact id: ${id}`);
|
|
815
|
+
return path.join(this.rootDir, id);
|
|
847
816
|
}
|
|
848
817
|
};
|
|
818
|
+
function viewerEntryToMeta(entry, token) {
|
|
819
|
+
if (!isViewerMetadata(entry.metadata)) throw new Error(`Diff artifact ${entry.key} is not a viewer.`);
|
|
820
|
+
return {
|
|
821
|
+
id: entry.key,
|
|
822
|
+
token,
|
|
823
|
+
createdAt: timestampMsToIsoString(entry.createdAt) ?? "1970-01-01T00:00:00.000Z",
|
|
824
|
+
expiresAt: resolveEntryExpiresAt(entry),
|
|
825
|
+
title: entry.metadata.title,
|
|
826
|
+
inputKind: entry.metadata.inputKind,
|
|
827
|
+
fileCount: entry.metadata.fileCount,
|
|
828
|
+
viewerPath: `${VIEWER_PREFIX}/${entry.key}/${token}`,
|
|
829
|
+
...entry.metadata.context ? { context: entry.metadata.context } : {}
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
function resolveEntryExpiresAt(entry) {
|
|
833
|
+
return timestampMsToIsoString(entry.expiresAt ?? MAX_DATE_TIMESTAMP_MS) ?? timestampMsToIsoString(MAX_DATE_TIMESTAMP_MS) ?? "1970-01-01T00:00:00.000Z";
|
|
834
|
+
}
|
|
835
|
+
function hashToken(token) {
|
|
836
|
+
return crypto.createHash("sha256").update(token).digest("hex");
|
|
837
|
+
}
|
|
849
838
|
function normalizeTtlMs$1(value) {
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
return Math.min(
|
|
839
|
+
const rounded = value === void 0 || !Number.isFinite(value) ? 0 : Math.floor(value);
|
|
840
|
+
const requestedTtlMs = rounded > 0 ? rounded : DEFAULT_TTL_MS;
|
|
841
|
+
const remainingDateRangeMs = Math.floor(MAX_DATE_TIMESTAMP_MS - Date.now());
|
|
842
|
+
return Math.min(requestedTtlMs, MAX_TTL_MS, Math.max(1, remainingDateRangeMs));
|
|
843
|
+
}
|
|
844
|
+
function isViewerMetadata(value) {
|
|
845
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
846
|
+
const metadata = value;
|
|
847
|
+
return metadata.version === 1 && metadata.kind === "viewer" && metadata.encoding === "gzip" && typeof metadata.tokenHash === "string" && /^[0-9a-f]{64}$/u.test(metadata.tokenHash) && typeof metadata.title === "string" && (metadata.inputKind === "before_after" || metadata.inputKind === "patch") && Number.isSafeInteger(metadata.fileCount) && typeof metadata.fileCount === "number" && metadata.fileCount >= 0 && Number.isSafeInteger(metadata.decodedBytes) && typeof metadata.decodedBytes === "number" && metadata.decodedBytes >= 0 && metadata.decodedBytes <= MAX_DECODED_HTML_BYTES && isArtifactContext(metadata.context);
|
|
848
|
+
}
|
|
849
|
+
function isRenderedFileMetadata(value) {
|
|
850
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
851
|
+
const metadata = value;
|
|
852
|
+
return metadata.version === 1 && metadata.kind === "rendered_file" && (metadata.format === "png" || metadata.format === "pdf") && isArtifactContext(metadata.context);
|
|
853
|
+
}
|
|
854
|
+
function isArtifactContext(value) {
|
|
855
|
+
if (value === void 0) return true;
|
|
856
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
857
|
+
const context = value;
|
|
858
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
859
|
+
"agentId",
|
|
860
|
+
"sessionId",
|
|
861
|
+
"messageChannel",
|
|
862
|
+
"agentAccountId"
|
|
863
|
+
]);
|
|
864
|
+
return Object.entries(context).every(([key, entry]) => allowed.has(key) && (entry === void 0 || typeof entry === "string"));
|
|
854
865
|
}
|
|
855
|
-
function
|
|
856
|
-
return
|
|
866
|
+
async function gzipAsync(input) {
|
|
867
|
+
return await new Promise((resolve, reject) => {
|
|
868
|
+
gzip(input, (error, result) => {
|
|
869
|
+
if (error) {
|
|
870
|
+
reject(error);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
resolve(result);
|
|
874
|
+
});
|
|
875
|
+
});
|
|
857
876
|
}
|
|
858
|
-
function
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
877
|
+
async function gunzipAsync(input, maxOutputLength) {
|
|
878
|
+
return await new Promise((resolve, reject) => {
|
|
879
|
+
gunzip(input, { maxOutputLength }, (error, result) => {
|
|
880
|
+
if (error) {
|
|
881
|
+
reject(error);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
resolve(result);
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function isFileExists(error) {
|
|
889
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
862
890
|
}
|
|
863
891
|
function isFileNotFound(error) {
|
|
864
|
-
|
|
865
|
-
return code === "ENOENT" || code === "not-found";
|
|
866
|
-
}
|
|
867
|
-
function normalizeArtifactContext(value) {
|
|
868
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
869
|
-
const raw = value;
|
|
870
|
-
const context = {
|
|
871
|
-
agentId: normalizeOptionalString(raw.agentId),
|
|
872
|
-
sessionId: normalizeOptionalString(raw.sessionId),
|
|
873
|
-
messageChannel: normalizeOptionalString(raw.messageChannel),
|
|
874
|
-
agentAccountId: normalizeOptionalString(raw.agentAccountId)
|
|
875
|
-
};
|
|
876
|
-
return Object.values(context).some((entry) => entry !== void 0) ? context : void 0;
|
|
892
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
877
893
|
}
|
|
878
894
|
//#endregion
|
|
879
895
|
//#region extensions/diffs/src/browser.ts
|
|
@@ -2304,12 +2320,12 @@ function createDiffsTool(params) {
|
|
|
2304
2320
|
const artifactFile = await renderDiffArtifactFile({
|
|
2305
2321
|
screenshotter,
|
|
2306
2322
|
store: params.store,
|
|
2307
|
-
artifactId: artifact.id,
|
|
2308
2323
|
html: requireRenderedHtml(rendered.imageHtml, "image"),
|
|
2309
2324
|
theme,
|
|
2310
|
-
image
|
|
2325
|
+
image,
|
|
2326
|
+
ttlMs,
|
|
2327
|
+
context: artifactContext
|
|
2311
2328
|
});
|
|
2312
|
-
await params.store.updateFilePath(artifact.id, artifactFile.path);
|
|
2313
2329
|
return {
|
|
2314
2330
|
content: [{
|
|
2315
2331
|
type: "text",
|
|
@@ -2381,34 +2397,44 @@ function buildFileArtifactMessage(params) {
|
|
|
2381
2397
|
return lines.join("\n");
|
|
2382
2398
|
}
|
|
2383
2399
|
async function renderDiffArtifactFile(params) {
|
|
2384
|
-
const
|
|
2400
|
+
const fileArtifact = await params.store.createStandaloneFileArtifact({
|
|
2385
2401
|
format: params.image.format,
|
|
2386
2402
|
ttlMs: params.ttlMs,
|
|
2387
2403
|
context: params.context
|
|
2388
2404
|
});
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2405
|
+
try {
|
|
2406
|
+
await params.screenshotter.screenshotHtml({
|
|
2407
|
+
html: params.html,
|
|
2408
|
+
outputPath: fileArtifact.filePath,
|
|
2409
|
+
theme: params.theme,
|
|
2410
|
+
image: params.image
|
|
2411
|
+
});
|
|
2412
|
+
const stats = await fs$1.stat(fileArtifact.filePath);
|
|
2413
|
+
await params.store.completeFileArtifact(fileArtifact.id);
|
|
2414
|
+
return {
|
|
2415
|
+
path: fileArtifact.filePath,
|
|
2416
|
+
bytes: stats.size,
|
|
2417
|
+
artifactId: fileArtifact.id,
|
|
2418
|
+
expiresAt: fileArtifact.expiresAt
|
|
2419
|
+
};
|
|
2420
|
+
} catch (error) {
|
|
2421
|
+
await params.store.deleteFileArtifact(fileArtifact.id);
|
|
2422
|
+
throw error;
|
|
2423
|
+
}
|
|
2402
2424
|
}
|
|
2403
2425
|
function buildArtifactContext(context) {
|
|
2404
2426
|
if (!context) return;
|
|
2427
|
+
const agentId = normalizeOptionalString(context.agentId);
|
|
2428
|
+
const sessionId = normalizeOptionalString(context.sessionId);
|
|
2429
|
+
const messageChannel = normalizeOptionalString(context.messageChannel);
|
|
2430
|
+
const agentAccountId = normalizeOptionalString(context.agentAccountId);
|
|
2405
2431
|
const artifactContext = {
|
|
2406
|
-
agentId:
|
|
2407
|
-
sessionId:
|
|
2408
|
-
messageChannel:
|
|
2409
|
-
agentAccountId:
|
|
2432
|
+
...agentId ? { agentId } : {},
|
|
2433
|
+
...sessionId ? { sessionId } : {},
|
|
2434
|
+
...messageChannel ? { messageChannel } : {},
|
|
2435
|
+
...agentAccountId ? { agentAccountId } : {}
|
|
2410
2436
|
};
|
|
2411
|
-
return Object.
|
|
2437
|
+
return Object.keys(artifactContext).length > 0 ? artifactContext : void 0;
|
|
2412
2438
|
}
|
|
2413
2439
|
function normalizeDiffInput(params) {
|
|
2414
2440
|
const patch = params.patch?.trim();
|
|
@@ -2478,9 +2504,20 @@ var PluginToolInputError = class extends Error {
|
|
|
2478
2504
|
//#endregion
|
|
2479
2505
|
//#region extensions/diffs/src/plugin.ts
|
|
2480
2506
|
const DIFFS_LANGUAGE_PACK_PLUGIN_ID = "diffs-language-pack";
|
|
2507
|
+
const DIFF_ARTIFACT_NAMESPACE = "diff-artifacts";
|
|
2508
|
+
const DIFF_ARTIFACT_MAX_ENTRIES = 2048;
|
|
2509
|
+
const DIFF_ARTIFACT_MAX_BYTES_PER_ENTRY = 32 * 1024 * 1024;
|
|
2510
|
+
const DIFF_ARTIFACT_MAX_BYTES_PER_NAMESPACE = 256 * 1024 * 1024;
|
|
2481
2511
|
function registerDiffsPlugin(api) {
|
|
2482
2512
|
const store = new DiffArtifactStore({
|
|
2483
2513
|
rootDir: path.join(resolvePreferredOpenClawTmpDir(), "openclaw-diffs"),
|
|
2514
|
+
blobStore: api.runtime.state.openBlobStore({
|
|
2515
|
+
namespace: DIFF_ARTIFACT_NAMESPACE,
|
|
2516
|
+
maxEntries: DIFF_ARTIFACT_MAX_ENTRIES,
|
|
2517
|
+
maxBytesPerEntry: DIFF_ARTIFACT_MAX_BYTES_PER_ENTRY,
|
|
2518
|
+
maxBytesPerNamespace: DIFF_ARTIFACT_MAX_BYTES_PER_NAMESPACE,
|
|
2519
|
+
overflowPolicy: "reject-new"
|
|
2520
|
+
}),
|
|
2484
2521
|
logger: api.logger
|
|
2485
2522
|
});
|
|
2486
2523
|
const resolveCurrentPluginConfig = () => resolveLivePluginConfigObject(api.runtime.config?.current ? () => api.runtime.config.current() : void 0, "diffs", api.pluginConfig) ?? {};
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/diffs",
|
|
3
|
-
"version": "2026.7.2-beta.
|
|
3
|
+
"version": "2026.7.2-beta.3",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@openclaw/diffs",
|
|
9
|
-
"version": "2026.7.2-beta.
|
|
9
|
+
"version": "2026.7.2-beta.3",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@pierre/diffs": "1.2.12",
|
|
12
12
|
"@shikijs/langs": "4.3.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/diffs",
|
|
3
|
-
"version": "2026.7.2-beta.
|
|
3
|
+
"version": "2026.7.2-beta.3",
|
|
4
4
|
"description": "OpenClaw read-only diff viewer plugin and file renderer for agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"minHostVersion": ">=2026.4.30"
|
|
26
26
|
},
|
|
27
27
|
"compat": {
|
|
28
|
-
"pluginApi": ">=2026.7.2-beta.
|
|
28
|
+
"pluginApi": ">=2026.7.2-beta.3"
|
|
29
29
|
},
|
|
30
30
|
"assetScripts": {
|
|
31
31
|
"build": "node ../../scripts/build-diffs-viewer-runtime.mjs curated"
|
|
32
32
|
},
|
|
33
33
|
"build": {
|
|
34
|
-
"openclawVersion": "2026.7.2-beta.
|
|
34
|
+
"openclawVersion": "2026.7.2-beta.3",
|
|
35
35
|
"staticAssets": [
|
|
36
36
|
{
|
|
37
37
|
"source": "./assets/viewer-runtime.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"skills/**"
|
|
56
56
|
],
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"openclaw": ">=2026.7.2-beta.
|
|
58
|
+
"openclaw": ">=2026.7.2-beta.3"
|
|
59
59
|
},
|
|
60
60
|
"peerDependenciesMeta": {
|
|
61
61
|
"openclaw": {
|