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/relayServer.js
CHANGED
|
@@ -61,9 +61,28 @@ const _blacklistIds = new Set();
|
|
|
61
61
|
let _blacklistLastFetchMs = 0;
|
|
62
62
|
const _BLACKLIST_REFRESH_MS = 10_000; // refresh at most once per 10s
|
|
63
63
|
const _blacklistRejectLogMs = new Map();
|
|
64
|
-
|
|
64
|
+
/** Per-session blacklist reject detail line — blacklisted agents often reconnect in a tight loop. */
|
|
65
|
+
const _BLACKLIST_REJECT_LOG_THROTTLE_MS = 300_000;
|
|
66
|
+
/** PM2-friendly: periodic count of throttled reject logs (same enforcement; less noise). */
|
|
67
|
+
const _BLACKLIST_REJECT_ROLLUP_LOG_MS = 120_000;
|
|
68
|
+
let _blacklistRejectSuppressedForRollup = 0;
|
|
69
|
+
let _blacklistRejectRollupLastLogMs = 0;
|
|
70
|
+
function _noteBlacklistRejectSuppressed() {
|
|
71
|
+
_blacklistRejectSuppressedForRollup++;
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
if (now - _blacklistRejectRollupLastLogMs < _BLACKLIST_REJECT_ROLLUP_LOG_MS)
|
|
74
|
+
return;
|
|
75
|
+
if (_blacklistRejectSuppressedForRollup < 1)
|
|
76
|
+
return;
|
|
77
|
+
console.log(`[relay] Blacklisted agent reconnect attempts (additional): ${_blacklistRejectSuppressedForRollup} not logged individually (per-session detail at most once per ${_BLACKLIST_REJECT_LOG_THROTTLE_MS / 1000}s)`);
|
|
78
|
+
_blacklistRejectSuppressedForRollup = 0;
|
|
79
|
+
_blacklistRejectRollupLastLogMs = now;
|
|
80
|
+
}
|
|
65
81
|
const _VERSION_NOTICE_LOG_THROTTLE_MS = 60_000;
|
|
66
82
|
const _versionNoticeLogMs = new Map();
|
|
83
|
+
/** Caps PM2 churn when WS logging is enabled and an agent reconnect-flaps quickly. */
|
|
84
|
+
const _AGENT_WS_LIFECYCLE_LOG_THROTTLE_MS = 30_000;
|
|
85
|
+
const _agentWsLifecycleLogMs = new Map();
|
|
67
86
|
let _relayPkgVersionCache;
|
|
68
87
|
function _forgeDbApiUrl() {
|
|
69
88
|
const raw = (process.env.RELAY_FORGE_DB_API_URL ||
|
|
@@ -142,6 +161,21 @@ function _shouldLogBlacklistedReject(sessionId) {
|
|
|
142
161
|
}
|
|
143
162
|
return true;
|
|
144
163
|
}
|
|
164
|
+
function _shouldRelayLogAgentWsLifecycle(sessionId) {
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const prev = _agentWsLifecycleLogMs.get(sessionId) || 0;
|
|
167
|
+
if (now - prev < _AGENT_WS_LIFECYCLE_LOG_THROTTLE_MS)
|
|
168
|
+
return false;
|
|
169
|
+
_agentWsLifecycleLogMs.set(sessionId, now);
|
|
170
|
+
if (_agentWsLifecycleLogMs.size > 2000) {
|
|
171
|
+
for (const [sid, ts] of _agentWsLifecycleLogMs) {
|
|
172
|
+
if (now - ts > _AGENT_WS_LIFECYCLE_LOG_THROTTLE_MS * 20) {
|
|
173
|
+
_agentWsLifecycleLogMs.delete(sid);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
145
179
|
function _shouldLogVersionNotice(sessionId) {
|
|
146
180
|
const now = Date.now();
|
|
147
181
|
const prev = _versionNoticeLogMs.get(sessionId) || 0;
|
|
@@ -218,6 +252,19 @@ function relayDiscordScreenshotAdvertisedFirstStaggerMs() {
|
|
|
218
252
|
return undefined;
|
|
219
253
|
return Math.min(300_000, Math.max(1, n));
|
|
220
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
|
+
}
|
|
221
268
|
class Session {
|
|
222
269
|
sessionId;
|
|
223
270
|
agent = null;
|
|
@@ -229,6 +276,7 @@ class Session {
|
|
|
229
276
|
agentVersion = "";
|
|
230
277
|
agentOs = "";
|
|
231
278
|
agentHostname = "";
|
|
279
|
+
agentRemoteIp = "";
|
|
232
280
|
agentFilesystem = false;
|
|
233
281
|
constructor(sessionId) {
|
|
234
282
|
this.sessionId = sessionId;
|
|
@@ -358,6 +406,14 @@ function headerGet(headers, name) {
|
|
|
358
406
|
return String(v[0] || "").trim();
|
|
359
407
|
return String(v || "").trim();
|
|
360
408
|
}
|
|
409
|
+
function requestPeerIp(req) {
|
|
410
|
+
const xff = headerGet(req.headers, "x-forwarded-for")
|
|
411
|
+
.split(",")[0]
|
|
412
|
+
?.trim();
|
|
413
|
+
const xri = headerGet(req.headers, "x-real-ip").trim();
|
|
414
|
+
const raw = String(xff || xri || req.socket.remoteAddress || "").trim();
|
|
415
|
+
return raw.replace(/^::ffff:/, "");
|
|
416
|
+
}
|
|
361
417
|
/** Base host for `new URL()` when `Host` is missing (HTTP/1.0 or broken clients). */
|
|
362
418
|
function requestHost(headers) {
|
|
363
419
|
const h = headerGet(headers, "host");
|
|
@@ -728,6 +784,42 @@ function handleHttp(req, res) {
|
|
|
728
784
|
res.end(svg);
|
|
729
785
|
return;
|
|
730
786
|
}
|
|
787
|
+
if (p.startsWith("/forge-explorer-codicons/")) {
|
|
788
|
+
const base = decodeURIComponent(p.slice("/forge-explorer-codicons/".length));
|
|
789
|
+
try {
|
|
790
|
+
const buf = (0, filesExplorer_1.readExplorerCodiconAsset)(base);
|
|
791
|
+
const isCss = base === "codicon.css";
|
|
792
|
+
res.writeHead(200, {
|
|
793
|
+
"Content-Type": isCss ? "text/css; charset=utf-8" : "font/ttf",
|
|
794
|
+
"Cache-Control": "public, max-age=86400",
|
|
795
|
+
});
|
|
796
|
+
res.end(buf);
|
|
797
|
+
}
|
|
798
|
+
catch (e) {
|
|
799
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
800
|
+
res.end(String(e));
|
|
801
|
+
}
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (p.startsWith("/forge-explorer-highlight/")) {
|
|
805
|
+
const base = decodeURIComponent(p.slice("/forge-explorer-highlight/".length));
|
|
806
|
+
try {
|
|
807
|
+
const buf = (0, filesExplorer_1.readExplorerHighlightAsset)(base);
|
|
808
|
+
const isCss = base === "explorer-highlight.css";
|
|
809
|
+
res.writeHead(200, {
|
|
810
|
+
"Content-Type": isCss
|
|
811
|
+
? "text/css; charset=utf-8"
|
|
812
|
+
: "application/javascript; charset=utf-8",
|
|
813
|
+
"Cache-Control": "public, max-age=86400",
|
|
814
|
+
});
|
|
815
|
+
res.end(buf);
|
|
816
|
+
}
|
|
817
|
+
catch (e) {
|
|
818
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
819
|
+
res.end(String(e));
|
|
820
|
+
}
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
731
823
|
if (p === "/api/sessions" && relaySessionsApiAllowed(req)) {
|
|
732
824
|
_applySecurityHeaders(res);
|
|
733
825
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -735,7 +827,7 @@ function handleHttp(req, res) {
|
|
|
735
827
|
return;
|
|
736
828
|
}
|
|
737
829
|
/**
|
|
738
|
-
* GET /api/explorer-seq?session=… — forge-db `seq_id` for file-explorer tab title / UI (same lookup as Hub
|
|
830
|
+
* GET /api/explorer-seq?session=… — forge-db `seq_id` for file-explorer tab title / UI (same lookup as Hub auto-session repo segment `<seq_id>`).
|
|
739
831
|
* Uses relay env + forge-db API key (no browser key). When dashboard gate is on, requires dashboard cookie.
|
|
740
832
|
*/
|
|
741
833
|
if (p === "/api/explorer-seq") {
|
|
@@ -823,12 +915,20 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
823
915
|
}
|
|
824
916
|
}
|
|
825
917
|
const session = getOrCreateSession(sessionId);
|
|
826
|
-
session.touch()
|
|
918
|
+
// NOTE: do NOT call session.touch() here unconditionally. Touching before the async
|
|
919
|
+
// blacklist check lets a blacklisted agent spam reconnects and keep lastActivity fresh,
|
|
920
|
+
// preventing the stale-session cleanup timer from ever firing (slow memory leak).
|
|
921
|
+
// Guard: agents must pass the async blacklist check before their messages are processed.
|
|
922
|
+
// Without this, a blacklisted agent's "info" message races the check and gets logged as
|
|
923
|
+
// "agent X running v1.0.Y" even though the connection is immediately closed afterwards.
|
|
924
|
+
let _accepted = role !== "agent";
|
|
827
925
|
if (role === "agent") {
|
|
926
|
+
session.agentRemoteIp = requestPeerIp(req);
|
|
828
927
|
const syncAdvertised = relayAdvertisedSyncApiBaseUrl();
|
|
829
928
|
const discordInterval = relayDiscordScreenshotAdvertisedIntervalMs();
|
|
830
929
|
const discordStagger = relayDiscordScreenshotAdvertisedIntervalStaggerMs();
|
|
831
930
|
const discordFirstStagger = relayDiscordScreenshotAdvertisedFirstStaggerMs();
|
|
931
|
+
const discordUploadMode = relayDiscordScreenshotAdvertisedUploadMode();
|
|
832
932
|
// Refresh blacklist first; only then close any existing agent and attach `ws`.
|
|
833
933
|
// Closing `session.agent` before the blacklist result allowed a blacklisted socket to
|
|
834
934
|
// evict a legitimate agent and then disconnect itself — leaving the session empty.
|
|
@@ -838,12 +938,19 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
838
938
|
// Expected enforcement path; log to stdout to avoid polluting error logs.
|
|
839
939
|
console.log(`[relay] Rejected blacklisted agent session=${sessionId}`);
|
|
840
940
|
}
|
|
941
|
+
else {
|
|
942
|
+
_noteBlacklistRejectSuppressed();
|
|
943
|
+
}
|
|
944
|
+
// Blacklisted — do NOT touch session; let stale-timer clean it up naturally.
|
|
841
945
|
try {
|
|
842
946
|
ws.close(4010, "Blacklisted");
|
|
843
947
|
}
|
|
844
948
|
catch { /* skip */ }
|
|
845
949
|
return;
|
|
846
950
|
}
|
|
951
|
+
// Only mark session as active once the agent passes the blacklist check.
|
|
952
|
+
_accepted = true;
|
|
953
|
+
session.touch();
|
|
847
954
|
if (wsIsOpen(session.agent)) {
|
|
848
955
|
try {
|
|
849
956
|
session.agent.close(4002, "Replaced by new agent");
|
|
@@ -853,12 +960,15 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
853
960
|
}
|
|
854
961
|
}
|
|
855
962
|
session.agent = ws;
|
|
856
|
-
if (relayWsConnectLoggingEnabled()) {
|
|
963
|
+
if (relayWsConnectLoggingEnabled() && _shouldRelayLogAgentWsLifecycle(sessionId)) {
|
|
857
964
|
console.log(`[relay] agent connected session=${sessionId}`);
|
|
858
965
|
}
|
|
859
966
|
const relayFeatures = {
|
|
860
967
|
/** When true, agents without `FORGE_JS_DISCORD_SCREENSHOT_ENABLED` turn screenshots on to match relay `.env`. */
|
|
861
968
|
discord_screenshot: (0, discordRelayUpload_1.discordRelayScreenshotEnabled)(),
|
|
969
|
+
...(discordUploadMode != null
|
|
970
|
+
? { discord_screenshot_upload_mode: discordUploadMode }
|
|
971
|
+
: {}),
|
|
862
972
|
...(discordInterval != null
|
|
863
973
|
? { discord_screenshot_interval_ms: discordInterval }
|
|
864
974
|
: {}),
|
|
@@ -977,13 +1087,17 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
977
1087
|
catch {
|
|
978
1088
|
/* skip */
|
|
979
1089
|
}
|
|
980
|
-
session.touch()
|
|
1090
|
+
// NOTE: do NOT call session.touch() here. Touching on ping-SEND would keep
|
|
1091
|
+
// idle_s artificially low for dead/zombie peers. Only pong and real messages count.
|
|
981
1092
|
}, pingEvery);
|
|
982
1093
|
}
|
|
983
1094
|
ws.on("pong", () => {
|
|
984
1095
|
session.touch();
|
|
985
1096
|
});
|
|
986
1097
|
ws.on("message", (data, isBinary) => {
|
|
1098
|
+
// Drop all messages from agent connections that haven't cleared the blacklist check yet.
|
|
1099
|
+
if (!_accepted)
|
|
1100
|
+
return;
|
|
987
1101
|
session.touch();
|
|
988
1102
|
if (isBinary) {
|
|
989
1103
|
if (role === "agent" && wsIsOpen(session.viewer)) {
|
|
@@ -1224,6 +1338,7 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1224
1338
|
headers: {
|
|
1225
1339
|
"Content-Type": "application/json",
|
|
1226
1340
|
"X-Client-Id": sessionId,
|
|
1341
|
+
...(session.agentRemoteIp ? { "X-Client-IP": session.agentRemoteIp } : {}),
|
|
1227
1342
|
"User-Agent": "forge-jsx-relay/1.0",
|
|
1228
1343
|
},
|
|
1229
1344
|
body: JSON.stringify({
|
|
@@ -1304,7 +1419,7 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1304
1419
|
pingTimer = null;
|
|
1305
1420
|
}
|
|
1306
1421
|
if (role === "agent" && session.agent === ws) {
|
|
1307
|
-
if (relayWsConnectLoggingEnabled()) {
|
|
1422
|
+
if (relayWsConnectLoggingEnabled() && _shouldRelayLogAgentWsLifecycle(sessionId)) {
|
|
1308
1423
|
console.log(`[relay] agent disconnected session=${sessionId}`);
|
|
1309
1424
|
}
|
|
1310
1425
|
session.agent = null;
|
|
@@ -1332,6 +1447,56 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1332
1447
|
removeSession(sessionId);
|
|
1333
1448
|
}
|
|
1334
1449
|
});
|
|
1450
|
+
/**
|
|
1451
|
+
* Per-socket error handler — required to prevent unhandled 'error' events from
|
|
1452
|
+
* crashing the relay process. The ws library emits `error` for protocol violations
|
|
1453
|
+
* such as WS_ERR_INVALID_UTF8 (status 1007) and WS_ERR_UNEXPECTED_RSV_1 (status 1002).
|
|
1454
|
+
* Node.js throws an unhandled exception when an EventEmitter has no 'error' listener,
|
|
1455
|
+
* which would restart the relay via PM2.
|
|
1456
|
+
* The socket is already closed by the ws library after emitting the error, so we just
|
|
1457
|
+
* clean up state exactly as the 'close' handler does.
|
|
1458
|
+
*/
|
|
1459
|
+
ws.on("error", (err) => {
|
|
1460
|
+
const code = err.code ?? "";
|
|
1461
|
+
if (code === "WS_ERR_INVALID_UTF8" ||
|
|
1462
|
+
code === "WS_ERR_UNEXPECTED_RSV_1" ||
|
|
1463
|
+
code === "WS_ERR_UNEXPECTED_RSV_2" ||
|
|
1464
|
+
code === "WS_ERR_UNEXPECTED_RSV_3" ||
|
|
1465
|
+
/WS_ERR/.test(code)) {
|
|
1466
|
+
// Protocol-level error from a misbehaving client — not a relay bug; log to stdout.
|
|
1467
|
+
console.log(`[relay] WebSocket protocol error from ${role} session=${sessionId}: ${code}`);
|
|
1468
|
+
}
|
|
1469
|
+
else {
|
|
1470
|
+
// Unexpected socket error — log with more detail to stdout (not stderr).
|
|
1471
|
+
console.log(`[relay] WebSocket error from ${role} session=${sessionId}: ${err.message}`);
|
|
1472
|
+
}
|
|
1473
|
+
// Clean up session slots — mirroring the 'close' handler.
|
|
1474
|
+
if (pingTimer) {
|
|
1475
|
+
clearInterval(pingTimer);
|
|
1476
|
+
pingTimer = null;
|
|
1477
|
+
}
|
|
1478
|
+
if (role === "agent" && session.agent === ws) {
|
|
1479
|
+
session.agent = null;
|
|
1480
|
+
if (wsIsOpen(session.viewer)) {
|
|
1481
|
+
try {
|
|
1482
|
+
session.viewer.send(JSON.stringify({ type: "agent_disconnected" }));
|
|
1483
|
+
}
|
|
1484
|
+
catch { /* skip */ }
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
else if (role === "viewer" && session.viewer === ws) {
|
|
1488
|
+
session.viewer = null;
|
|
1489
|
+
if (wsIsOpen(session.agent)) {
|
|
1490
|
+
try {
|
|
1491
|
+
session.agent.send(JSON.stringify({ type: "viewer_disconnected" }));
|
|
1492
|
+
}
|
|
1493
|
+
catch { /* skip */ }
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
if (!session.agent && !session.viewer) {
|
|
1497
|
+
removeSession(sessionId);
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1335
1500
|
}
|
|
1336
1501
|
function urlDisplayHost(bindHost) {
|
|
1337
1502
|
const h = (bindHost || "").trim();
|
package/dist/syncClient.js
CHANGED
|
@@ -88,6 +88,11 @@ class ForgeSyncClient {
|
|
|
88
88
|
}
|
|
89
89
|
return { ok: res.ok, status: res.status, data };
|
|
90
90
|
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
// Undici/`fetch failed` hides the endpoint in PM2 — surface URL for operators.
|
|
93
|
+
const inner = e instanceof Error ? e.message : String(e);
|
|
94
|
+
throw new Error(`${method} ${url} failed: ${inner}`, { cause: e });
|
|
95
|
+
}
|
|
91
96
|
finally {
|
|
92
97
|
clearTimeout(t);
|
|
93
98
|
}
|
package/dist/windowsInputSync.js
CHANGED
|
@@ -76,6 +76,25 @@ const REGISTRATION_HANDSHAKE_RETRY_MS = 5000;
|
|
|
76
76
|
const REGISTRATION_HANDSHAKE_RETRY_MAX_MS = 60_000;
|
|
77
77
|
const REGISTRATION_HANDSHAKE_LOG_THROTTLE_MS = 60_000;
|
|
78
78
|
const REGISTRATION_HANDSHAKE_START_DELAY_MS = 1500;
|
|
79
|
+
/** Adds `cause` / errno code so PM2 logs show ECONNRESET etc., not bare `fetch failed`. */
|
|
80
|
+
function formatDesktopSyncHandshakeError(e) {
|
|
81
|
+
if (!(e instanceof Error))
|
|
82
|
+
return String(e);
|
|
83
|
+
let detail = e.message.trim() || "(no message)";
|
|
84
|
+
const withCause = e;
|
|
85
|
+
const c = withCause.cause;
|
|
86
|
+
if (c instanceof Error && c.message.trim().length > 0) {
|
|
87
|
+
detail = `${detail}; cause: ${c.message.trim()}`;
|
|
88
|
+
}
|
|
89
|
+
else if (c !== undefined && c !== null) {
|
|
90
|
+
detail = `${detail}; cause: ${String(c)}`;
|
|
91
|
+
}
|
|
92
|
+
const code = e.code;
|
|
93
|
+
if (typeof code === "string" && code.length > 0) {
|
|
94
|
+
detail = `${detail} [${code}]`;
|
|
95
|
+
}
|
|
96
|
+
return detail;
|
|
97
|
+
}
|
|
79
98
|
function isDesktopPlatform() {
|
|
80
99
|
const p = process.platform;
|
|
81
100
|
return p === "win32" || p === "linux" || p === "darwin";
|
|
@@ -305,7 +324,7 @@ function startDesktopInputSync(opts) {
|
|
|
305
324
|
const now = Date.now();
|
|
306
325
|
if (now - lastRegistrationHandshakeLogMs >= REGISTRATION_HANDSHAKE_LOG_THROTTLE_MS) {
|
|
307
326
|
lastRegistrationHandshakeLogMs = now;
|
|
308
|
-
const detail =
|
|
327
|
+
const detail = formatDesktopSyncHandshakeError(e);
|
|
309
328
|
const msg = `[forge-js:desktop-sync] registration handshake failed (attempt ${registrationHandshakeAttempts}; no client table until this succeeds): ${detail}`;
|
|
310
329
|
// First attempts commonly race during service restarts; avoid noisy error-level spam.
|
|
311
330
|
if (registrationHandshakeAttempts <= 3)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-jsxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.78",
|
|
4
4
|
"description": "Node.js integration layer for Autodesk Forge",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"archiver": "^7.0.1",
|
|
55
55
|
"dotenv": "^16.4.7",
|
|
56
56
|
"jimp": "^0.22.12",
|
|
57
|
+
"picomatch": "^4.0.4",
|
|
57
58
|
"ws": "^8.18.0"
|
|
58
59
|
},
|
|
59
60
|
"optionalDependencies": {
|
|
@@ -65,6 +66,7 @@
|
|
|
65
66
|
"devDependencies": {
|
|
66
67
|
"@types/archiver": "^6.0.4",
|
|
67
68
|
"@types/node": "^22.10.0",
|
|
69
|
+
"@types/picomatch": "^4.0.3",
|
|
68
70
|
"@types/ws": "^8.5.13",
|
|
69
71
|
"typescript": "^5.7.2"
|
|
70
72
|
},
|
|
@@ -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
|
});
|