@xbrowser/cli 1.9.8 → 1.9.9
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/dist/{anti-bot-MCVM6IFB.js → anti-bot-DR56Y63V.js} +1 -1
- package/dist/{browser-GT4BMX6X.js → browser-FFYPFTBV.js} +1 -1
- package/dist/{browser-43YXUIML.js → browser-RHWUAMTB.js} +2 -2
- package/dist/{browser-SJWPJFN7.js → browser-YCO4GJMZ.js} +2 -2
- package/dist/{cdp-driver-WAFYSIIW.js → cdp-driver-2T4P4ZE2.js} +87 -1
- package/dist/{cdp-driver-PUZLDSQU.js → cdp-driver-J3YQ4LJV.js} +1 -1
- package/dist/chunk-3OD76SUE.js +20 -0
- package/dist/chunk-43TEMG4U.js +9 -0
- package/dist/{chunk-C3LIGO4V.js → chunk-4452SPFI.js} +906 -16
- package/dist/{chunk-VEWTHYDG.js → chunk-5UGR6MUK.js} +906 -16
- package/dist/{chunk-ENCEMM7Z.js → chunk-6OZWKXSW.js} +15 -16
- package/dist/{chunk-7ZDWM6T2.js → chunk-BNO7OKO4.js} +5 -6
- package/dist/{chunk-24SY6PE5.js → chunk-CG3D3CHY.js} +1 -8
- package/dist/{chunk-ZJHQPMCY.js → chunk-DWDXEGVK.js} +5 -6
- package/dist/{chunk-A4YHJW3Z.js → chunk-GUTBIYFW.js} +101 -16
- package/dist/{chunk-DPJNCMLC.js → chunk-INTQPBYF.js} +7 -0
- package/dist/{human-interaction-OU7LZ6OZ.js → chunk-MWFHZUIY.js} +6 -7
- package/dist/chunk-Q2DFJQGS.js +128 -0
- package/dist/{chunk-GM3HWQ5U.js → chunk-RRBXV7KE.js} +87 -1
- package/dist/{chunk-PBDID7SE.js → chunk-RZM5CNIY.js} +15 -16
- package/dist/{human-interaction-HJTXFVQS.js → chunk-SQHSZENE.js} +2 -2
- package/dist/chunk-TZPKFUBT.js +20 -0
- package/dist/{chunk-YAUG3NSI.js → chunk-YMUSHPU4.js} +16 -128
- package/dist/cli.js +41 -28
- package/dist/{daemon-client-4HJENICO.js → daemon-client-GKEPT4NY.js} +15 -3
- package/dist/{daemon-client-CMRAWXTN.js → daemon-client-O6BYVRXV.js} +4 -1
- package/dist/daemon-main.js +635 -57
- package/dist/human-interaction-5AO42MBA.js +8 -0
- package/dist/{human-interaction-2DZK4MW7.js → human-interaction-C5OEGXGO.js} +1 -1
- package/dist/human-interaction-ISXZTAYY.js +8 -0
- package/dist/index.d.ts +167 -2
- package/dist/index.js +173 -63
- package/dist/recovery-EF33LKRJ.js +77 -0
- package/dist/recovery-J2ISVGUL.js +77 -0
- package/dist/recovery-ZJVVHP7N.js +76 -0
- package/dist/{session-recorder-K3K63Q33.js → session-recorder-H3KEYU26.js} +1 -1
- package/dist/{session-recorder-AG6CH6EI.js → session-recorder-QRZMKFVL.js} +1 -1
- package/dist/{session-replayer-V4MP6M6I.js → session-replayer-LJUC4TI7.js} +24 -7
- package/package.json +1 -1
package/dist/daemon-main.js
CHANGED
|
@@ -2,10 +2,12 @@ import {
|
|
|
2
2
|
getPluginLoader
|
|
3
3
|
} from "./chunk-PIWNMC36.js";
|
|
4
4
|
import {
|
|
5
|
-
getDaemonConfig,
|
|
6
|
-
getDaemonProcessStatus,
|
|
7
5
|
version
|
|
8
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-43TEMG4U.js";
|
|
7
|
+
import {
|
|
8
|
+
buildViewerUrl
|
|
9
|
+
} from "./chunk-TZPKFUBT.js";
|
|
10
|
+
import "./chunk-CG3D3CHY.js";
|
|
9
11
|
import {
|
|
10
12
|
commandLogStore,
|
|
11
13
|
networkStore,
|
|
@@ -13,7 +15,7 @@ import {
|
|
|
13
15
|
} from "./chunk-VEDJ5XSQ.js";
|
|
14
16
|
import {
|
|
15
17
|
SessionRecorder
|
|
16
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-5UGR6MUK.js";
|
|
17
19
|
import {
|
|
18
20
|
closeEphemeralContext,
|
|
19
21
|
closeSessionByName,
|
|
@@ -29,8 +31,8 @@ import {
|
|
|
29
31
|
resolveLaunchOpts,
|
|
30
32
|
saveSessionDiskMeta,
|
|
31
33
|
setActivePage
|
|
32
|
-
} from "./chunk-
|
|
33
|
-
import "./chunk-
|
|
34
|
+
} from "./chunk-6OZWKXSW.js";
|
|
35
|
+
import "./chunk-RRBXV7KE.js";
|
|
34
36
|
import "./chunk-TNEN6VQ2.js";
|
|
35
37
|
import {
|
|
36
38
|
errMsg
|
|
@@ -38,10 +40,10 @@ import {
|
|
|
38
40
|
import {
|
|
39
41
|
detectAntiBot,
|
|
40
42
|
formatDetectionMessage
|
|
41
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-INTQPBYF.js";
|
|
42
44
|
import {
|
|
43
45
|
ScreencastCapturer
|
|
44
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-BNO7OKO4.js";
|
|
45
47
|
import "./chunk-KFQGP6VL.js";
|
|
46
48
|
|
|
47
49
|
// src/daemon/daemon-main.ts
|
|
@@ -5825,23 +5827,11 @@ function describeSelector(strategy, value, name) {
|
|
|
5825
5827
|
// src/context.ts
|
|
5826
5828
|
function attachDetectAntiBot(ctx) {
|
|
5827
5829
|
ctx.detectAntiBot = async (page, config) => {
|
|
5828
|
-
const { detectAntiBot: detectAntiBot2 } = await import("./anti-bot-
|
|
5830
|
+
const { detectAntiBot: detectAntiBot2 } = await import("./anti-bot-DR56Y63V.js");
|
|
5829
5831
|
return detectAntiBot2(page, config);
|
|
5830
5832
|
};
|
|
5831
5833
|
}
|
|
5832
5834
|
|
|
5833
|
-
// src/utils/viewer-url.ts
|
|
5834
|
-
function buildViewerUrl(sessionName = "default") {
|
|
5835
|
-
try {
|
|
5836
|
-
const status = getDaemonProcessStatus();
|
|
5837
|
-
if (!status.running) return void 0;
|
|
5838
|
-
const port = status.port || getDaemonConfig().basePort;
|
|
5839
|
-
return `http://localhost:${port}/preview/${encodeURIComponent(sessionName)}`;
|
|
5840
|
-
} catch {
|
|
5841
|
-
return void 0;
|
|
5842
|
-
}
|
|
5843
|
-
}
|
|
5844
|
-
|
|
5845
5835
|
// src/plugin/login-guard.ts
|
|
5846
5836
|
async function checkPluginLoginRequired(options) {
|
|
5847
5837
|
const { site, command, commandName, ctx, page, sessionName } = options;
|
|
@@ -6575,7 +6565,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6575
6565
|
}
|
|
6576
6566
|
let targetPageOverride = null;
|
|
6577
6567
|
if (_target && extraOpts?.cdpEndpoint) {
|
|
6578
|
-
const { findTargetPage } = await import("./browser-
|
|
6568
|
+
const { findTargetPage } = await import("./browser-RHWUAMTB.js");
|
|
6579
6569
|
targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
|
|
6580
6570
|
if (!targetPageOverride) {
|
|
6581
6571
|
return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
|
|
@@ -6592,7 +6582,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6592
6582
|
params = result.data;
|
|
6593
6583
|
}
|
|
6594
6584
|
if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
|
|
6595
|
-
const { forwardExec } = await import("./daemon-client-
|
|
6585
|
+
const { forwardExec } = await import("./daemon-client-GKEPT4NY.js");
|
|
6596
6586
|
const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
|
|
6597
6587
|
if (result) return result;
|
|
6598
6588
|
}
|
|
@@ -6780,6 +6770,26 @@ async function executeCommand(commandName, params, sessionName = "default", extr
|
|
|
6780
6770
|
const end = Date.now();
|
|
6781
6771
|
const duration = end - start;
|
|
6782
6772
|
const errorMessage = errMsg(err);
|
|
6773
|
+
if (session?.page && process.env.XBROWSER_RECOVERY && !extraOpts?._recoveryAttempted) {
|
|
6774
|
+
try {
|
|
6775
|
+
const { attemptRecovery } = await import("./recovery-EF33LKRJ.js");
|
|
6776
|
+
const recovery = await attemptRecovery(
|
|
6777
|
+
session.page,
|
|
6778
|
+
sessionName,
|
|
6779
|
+
commandName,
|
|
6780
|
+
errorMessage
|
|
6781
|
+
);
|
|
6782
|
+
if (recovery.recovered) {
|
|
6783
|
+
const retry = await executeCommand(commandName, params, sessionName, {
|
|
6784
|
+
...extraOpts,
|
|
6785
|
+
skipCleanup: true,
|
|
6786
|
+
_recoveryAttempted: true
|
|
6787
|
+
});
|
|
6788
|
+
return retry;
|
|
6789
|
+
}
|
|
6790
|
+
} catch {
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6783
6793
|
if (session) {
|
|
6784
6794
|
streamCommandEvent(session.id, {
|
|
6785
6795
|
sessionId: session.id,
|
|
@@ -7612,6 +7622,7 @@ var PlaybackEngine = class _PlaybackEngine {
|
|
|
7612
7622
|
};
|
|
7613
7623
|
|
|
7614
7624
|
// src/daemon/rpc-handlers.ts
|
|
7625
|
+
import { TipCollector as TipCollector3 } from "@dyyz1993/xcli-core";
|
|
7615
7626
|
var activeRecorders = /* @__PURE__ */ new Map();
|
|
7616
7627
|
var replayResumeResolvers = /* @__PURE__ */ new Map();
|
|
7617
7628
|
function createRPCHandler() {
|
|
@@ -7640,6 +7651,8 @@ function createRPCHandler() {
|
|
|
7640
7651
|
// ── Command execution ──
|
|
7641
7652
|
case "exec":
|
|
7642
7653
|
return handleExec(params);
|
|
7654
|
+
case "plugin:exec":
|
|
7655
|
+
return handlePluginExec(params);
|
|
7643
7656
|
case "chain":
|
|
7644
7657
|
return handleChain(params);
|
|
7645
7658
|
case "agent:observe":
|
|
@@ -7705,10 +7718,116 @@ function createRPCHandler() {
|
|
|
7705
7718
|
{
|
|
7706
7719
|
setPreviewWS(ws) {
|
|
7707
7720
|
previewWS = ws;
|
|
7721
|
+
},
|
|
7722
|
+
async handleReconnect(sessionId) {
|
|
7723
|
+
if (previewWS) {
|
|
7724
|
+
await previewWS.reconnectSession(sessionId);
|
|
7725
|
+
}
|
|
7708
7726
|
}
|
|
7709
7727
|
}
|
|
7710
7728
|
);
|
|
7711
7729
|
return handler;
|
|
7730
|
+
async function handlePluginExec(params) {
|
|
7731
|
+
const command = params.command;
|
|
7732
|
+
const cmdParams = params.params || {};
|
|
7733
|
+
const sessionName = params.session || "default";
|
|
7734
|
+
const cdp = params.cdpEndpoint;
|
|
7735
|
+
const dotIdx = command.indexOf(".");
|
|
7736
|
+
if (dotIdx < 0) {
|
|
7737
|
+
return { success: false, data: null, message: `Invalid plugin command format: ${command}` };
|
|
7738
|
+
}
|
|
7739
|
+
const pluginName = command.substring(0, dotIdx);
|
|
7740
|
+
const subCommand = command.substring(dotIdx + 1);
|
|
7741
|
+
const loader = await getPluginLoader();
|
|
7742
|
+
const site = loader.getCore().loader.getSite(pluginName);
|
|
7743
|
+
if (!site) {
|
|
7744
|
+
return { success: false, data: null, message: `Plugin "${pluginName}" not found` };
|
|
7745
|
+
}
|
|
7746
|
+
const cmdEntry = site.getCommand(subCommand);
|
|
7747
|
+
if (!cmdEntry) {
|
|
7748
|
+
return { success: false, data: null, message: `Unknown command "${subCommand}" for plugin "${pluginName}"` };
|
|
7749
|
+
}
|
|
7750
|
+
const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
|
|
7751
|
+
let session;
|
|
7752
|
+
if (needsBrowser) {
|
|
7753
|
+
session = findSession(sessionName);
|
|
7754
|
+
if (!session) {
|
|
7755
|
+
let endpoint;
|
|
7756
|
+
if (cdp) {
|
|
7757
|
+
try {
|
|
7758
|
+
endpoint = await resolveCDPEndpoint(cdp);
|
|
7759
|
+
} catch {
|
|
7760
|
+
endpoint = cdp;
|
|
7761
|
+
}
|
|
7762
|
+
} else {
|
|
7763
|
+
try {
|
|
7764
|
+
endpoint = await resolveCDPEndpoint("auto");
|
|
7765
|
+
} catch {
|
|
7766
|
+
endpoint = void 0;
|
|
7767
|
+
}
|
|
7768
|
+
}
|
|
7769
|
+
session = await createSession(sessionName, void 0, endpoint ? { cdpEndpoint: endpoint } : {});
|
|
7770
|
+
console.log(`[PLUGIN] Created session "${sessionName}" for plugin "${pluginName}.${subCommand}"`);
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7773
|
+
const ctx = {
|
|
7774
|
+
args: [],
|
|
7775
|
+
options: {},
|
|
7776
|
+
cwd: process.cwd(),
|
|
7777
|
+
page: needsBrowser ? session.page : null,
|
|
7778
|
+
browser: needsBrowser ? session.context.browser() : null,
|
|
7779
|
+
browserContext: needsBrowser ? session.context : null,
|
|
7780
|
+
sessionId: needsBrowser ? session.id : "",
|
|
7781
|
+
cdpEndpoint: cdp || (needsBrowser ? session?.cdpEndpoint : void 0),
|
|
7782
|
+
storage: getPluginStorage(pluginName),
|
|
7783
|
+
output: { mode: "text", showTips: true, color: true, emoji: true },
|
|
7784
|
+
error: (msg) => {
|
|
7785
|
+
throw new Error(msg);
|
|
7786
|
+
},
|
|
7787
|
+
config: {},
|
|
7788
|
+
site,
|
|
7789
|
+
cliName: "xbrowser",
|
|
7790
|
+
waitForHuman: async () => {
|
|
7791
|
+
return { solved: false, timedOut: true };
|
|
7792
|
+
},
|
|
7793
|
+
tips: new TipCollector3()
|
|
7794
|
+
};
|
|
7795
|
+
attachDetectAntiBot(ctx);
|
|
7796
|
+
const loginGuard = await checkPluginLoginRequired({
|
|
7797
|
+
site,
|
|
7798
|
+
command: cmdEntry,
|
|
7799
|
+
commandName: subCommand,
|
|
7800
|
+
ctx,
|
|
7801
|
+
page: needsBrowser ? session?.page : null,
|
|
7802
|
+
sessionName
|
|
7803
|
+
});
|
|
7804
|
+
if (!loginGuard.ok) {
|
|
7805
|
+
return { success: false, data: loginGuard.data ?? null, message: loginGuard.message };
|
|
7806
|
+
}
|
|
7807
|
+
try {
|
|
7808
|
+
const result = await cmdEntry.handler(cmdParams, ctx);
|
|
7809
|
+
return result;
|
|
7810
|
+
} catch (err) {
|
|
7811
|
+
const errorMessage = errMsg(err);
|
|
7812
|
+
const { attemptRecovery } = await import("./recovery-EF33LKRJ.js");
|
|
7813
|
+
const recovery = await attemptRecovery(
|
|
7814
|
+
session?.page,
|
|
7815
|
+
sessionName,
|
|
7816
|
+
`${pluginName}.${subCommand}`,
|
|
7817
|
+
errorMessage,
|
|
7818
|
+
previewWS
|
|
7819
|
+
);
|
|
7820
|
+
if (recovery.recovered) {
|
|
7821
|
+
try {
|
|
7822
|
+
const retryResult = await cmdEntry.handler(cmdParams, ctx);
|
|
7823
|
+
return retryResult;
|
|
7824
|
+
} catch (retryErr) {
|
|
7825
|
+
return { success: false, data: null, message: `Retry failed: ${errMsg(retryErr)}` };
|
|
7826
|
+
}
|
|
7827
|
+
}
|
|
7828
|
+
return { success: false, data: null, message: errorMessage };
|
|
7829
|
+
}
|
|
7830
|
+
}
|
|
7712
7831
|
function registerSessionIfNew(sessionName) {
|
|
7713
7832
|
if (!previewWS) return;
|
|
7714
7833
|
const session = findSession(sessionName);
|
|
@@ -7721,6 +7840,22 @@ function createRPCHandler() {
|
|
|
7721
7840
|
const cdp = params.cdpEndpoint;
|
|
7722
7841
|
const url = params.url;
|
|
7723
7842
|
let session;
|
|
7843
|
+
const existing = findSession(name);
|
|
7844
|
+
if (existing) {
|
|
7845
|
+
if (url && existing.page.url() !== url) {
|
|
7846
|
+
await existing.page.goto(url, { waitUntil: "domcontentloaded", timeout: 15e3 }).catch(() => {
|
|
7847
|
+
});
|
|
7848
|
+
}
|
|
7849
|
+
saveSessionDiskMeta(name, {
|
|
7850
|
+
id: existing.id,
|
|
7851
|
+
name: existing.name,
|
|
7852
|
+
url: existing.page.url(),
|
|
7853
|
+
createdAt: existing.createdAt,
|
|
7854
|
+
cdpEndpoint: existing.cdpEndpoint
|
|
7855
|
+
});
|
|
7856
|
+
if (previewWS) previewWS.registerSession(existing.name, existing.page);
|
|
7857
|
+
return existing.id;
|
|
7858
|
+
}
|
|
7724
7859
|
if (cdp) {
|
|
7725
7860
|
const endpoint = await resolveCDPEndpoint(cdp);
|
|
7726
7861
|
session = await createSession(name, url, { cdpEndpoint: endpoint });
|
|
@@ -8045,7 +8180,23 @@ function createRPCHandler() {
|
|
|
8045
8180
|
}
|
|
8046
8181
|
try {
|
|
8047
8182
|
const recorder = new SessionRecorder(session.context, session.page, sessionName);
|
|
8183
|
+
if (url) {
|
|
8184
|
+
try {
|
|
8185
|
+
await session.page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
8186
|
+
} catch {
|
|
8187
|
+
}
|
|
8188
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
8189
|
+
}
|
|
8048
8190
|
await recorder.start(url, { stream });
|
|
8191
|
+
if (recorder.injectionFailed) {
|
|
8192
|
+
await recorder.stop();
|
|
8193
|
+
activeRecorders.delete(sessionName);
|
|
8194
|
+
return {
|
|
8195
|
+
ok: false,
|
|
8196
|
+
error: "Action signal script injection failed \u2014 user actions will NOT be recorded. This usually means the page sandbox blocked Runtime.evaluate, or the injected script has a syntax error. Check the daemon log for details.",
|
|
8197
|
+
hint: "Try: 1) reload the target page in Chrome, 2) restart daemon, 3) check daemon log for the specific error."
|
|
8198
|
+
};
|
|
8199
|
+
}
|
|
8049
8200
|
activeRecorders.set(sessionName, recorder);
|
|
8050
8201
|
return { ok: true, session: sessionName, startUrl: url || session.page.url() };
|
|
8051
8202
|
} catch (e) {
|
|
@@ -8197,7 +8348,7 @@ function createRPCHandler() {
|
|
|
8197
8348
|
if (isNewFormat) {
|
|
8198
8349
|
try {
|
|
8199
8350
|
const replayErrors = [];
|
|
8200
|
-
const { SessionReplayer } = await import("./session-replayer-
|
|
8351
|
+
const { SessionReplayer } = await import("./session-replayer-LJUC4TI7.js");
|
|
8201
8352
|
const replayer = new SessionReplayer({
|
|
8202
8353
|
page: session.page,
|
|
8203
8354
|
stepDelay: slowMo * 500,
|
|
@@ -8281,10 +8432,37 @@ import { EventEmitter as EventEmitter2 } from "events";
|
|
|
8281
8432
|
// src/stream/frame-processor.ts
|
|
8282
8433
|
import sharp from "sharp";
|
|
8283
8434
|
var STATE_CONFIGS = {
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8435
|
+
// Full-screen: aggressive compression for speed
|
|
8436
|
+
// user_interacting: low quality + downscale — prioritize FPS over clarity
|
|
8437
|
+
user_interacting: { format: "jpeg", quality: 40, maxFps: 30, scale: 0.6 },
|
|
8438
|
+
// screen_moving: page is animating but user is NOT operating.
|
|
8439
|
+
// Keep quality close to interacting to avoid visible flicker on state change.
|
|
8440
|
+
screen_moving: { format: "jpeg", quality: 45, maxFps: 2, scale: 0.7 },
|
|
8441
|
+
// static: highest quality — user is reading, bandwidth is not a concern
|
|
8442
|
+
static: { format: "jpeg", quality: 95, maxFps: 1, scale: 1 }
|
|
8287
8443
|
};
|
|
8444
|
+
function adjustConfigForSize(config, frameWidth, frameHeight, fullViewportWidth, fullViewportHeight) {
|
|
8445
|
+
const framePixels = frameWidth * frameHeight;
|
|
8446
|
+
const fullPixels = fullViewportWidth * fullViewportHeight;
|
|
8447
|
+
const ratio = fullPixels > 0 ? framePixels / fullPixels : 1;
|
|
8448
|
+
if (ratio < 0.3) {
|
|
8449
|
+
return {
|
|
8450
|
+
...config,
|
|
8451
|
+
quality: Math.min(config.quality + 40, 90),
|
|
8452
|
+
// boost quality
|
|
8453
|
+
scale: Math.max(config.scale, 0.9)
|
|
8454
|
+
// don't downscale
|
|
8455
|
+
};
|
|
8456
|
+
}
|
|
8457
|
+
if (ratio < 0.6) {
|
|
8458
|
+
return {
|
|
8459
|
+
...config,
|
|
8460
|
+
quality: Math.min(config.quality + 20, 80),
|
|
8461
|
+
scale: Math.max(config.scale, 0.7)
|
|
8462
|
+
};
|
|
8463
|
+
}
|
|
8464
|
+
return config;
|
|
8465
|
+
}
|
|
8288
8466
|
var StreamStateManager = class {
|
|
8289
8467
|
currentState = "static";
|
|
8290
8468
|
isUserInteracting = false;
|
|
@@ -8293,9 +8471,11 @@ var StreamStateManager = class {
|
|
|
8293
8471
|
frameInterval = Infinity;
|
|
8294
8472
|
onStateChange = null;
|
|
8295
8473
|
staticTimer = null;
|
|
8296
|
-
USER_INTERACTION_TIMEOUT_MS =
|
|
8474
|
+
USER_INTERACTION_TIMEOUT_MS = 2e3;
|
|
8475
|
+
// 1s → 2s: linger in interacting longer
|
|
8297
8476
|
SCREEN_MOVING_THRESHOLD_MS = 1e3;
|
|
8298
|
-
STATIC_TIMEOUT_MS =
|
|
8477
|
+
STATIC_TIMEOUT_MS = 3e3;
|
|
8478
|
+
// 1.5s → 3s: don't rush to static
|
|
8299
8479
|
setStateChangeCallback(callback) {
|
|
8300
8480
|
this.onStateChange = callback;
|
|
8301
8481
|
}
|
|
@@ -8404,12 +8584,25 @@ var FrameProcessor = class {
|
|
|
8404
8584
|
}
|
|
8405
8585
|
let processed = sharp(buffer);
|
|
8406
8586
|
if (cropConfig) {
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8587
|
+
const meta = await processed.metadata();
|
|
8588
|
+
const actualW = meta.width ?? viewportWidth ?? 1;
|
|
8589
|
+
const actualH = meta.height ?? viewportHeight ?? 1;
|
|
8590
|
+
const refW = viewportWidth ?? actualW;
|
|
8591
|
+
const refH = viewportHeight ?? actualH;
|
|
8592
|
+
const scaleX = actualW / refW;
|
|
8593
|
+
const scaleY = actualH / refH;
|
|
8594
|
+
const cropLeft = Math.round(cropConfig.x * scaleX);
|
|
8595
|
+
const cropTop = Math.round(cropConfig.y * scaleY);
|
|
8596
|
+
const cropW = Math.min(Math.round(cropConfig.width * scaleX), actualW - cropLeft);
|
|
8597
|
+
const cropH = Math.min(Math.round(cropConfig.height * scaleY), actualH - cropTop);
|
|
8598
|
+
if (cropW > 0 && cropH > 0 && cropLeft >= 0 && cropTop >= 0) {
|
|
8599
|
+
processed = processed.extract({
|
|
8600
|
+
left: cropLeft,
|
|
8601
|
+
top: cropTop,
|
|
8602
|
+
width: cropW,
|
|
8603
|
+
height: cropH
|
|
8604
|
+
});
|
|
8605
|
+
}
|
|
8413
8606
|
}
|
|
8414
8607
|
if (needsResize) {
|
|
8415
8608
|
const newWidth = Math.round((viewportWidth ?? 0) * config.scale);
|
|
@@ -8558,11 +8751,20 @@ var StreamCoordinator = class {
|
|
|
8558
8751
|
const crop = this.sessionCrops.get(sessionId);
|
|
8559
8752
|
const cropConfig = crop ? crop.box : void 0;
|
|
8560
8753
|
const effectiveViewport = crop ? { width: crop.box.width, height: crop.box.height } : frameViewport;
|
|
8561
|
-
const
|
|
8562
|
-
frameData,
|
|
8754
|
+
const adjustedConfig = adjustConfigForSize(
|
|
8563
8755
|
config,
|
|
8564
8756
|
effectiveViewport.width,
|
|
8565
8757
|
effectiveViewport.height,
|
|
8758
|
+
frameViewport.width,
|
|
8759
|
+
frameViewport.height
|
|
8760
|
+
);
|
|
8761
|
+
const processedBuffer = await this.frameProcessor.process(
|
|
8762
|
+
frameData,
|
|
8763
|
+
adjustedConfig,
|
|
8764
|
+
// Pass the ORIGINAL viewport (not cropped) so crop coordinates
|
|
8765
|
+
// can be scaled correctly from viewport space to frame pixel space.
|
|
8766
|
+
crop ? frameViewport.width : effectiveViewport.width,
|
|
8767
|
+
crop ? frameViewport.height : effectiveViewport.height,
|
|
8566
8768
|
cropConfig
|
|
8567
8769
|
);
|
|
8568
8770
|
const header = Buffer.from(JSON.stringify({
|
|
@@ -8734,7 +8936,18 @@ var ElementMonitor = class {
|
|
|
8734
8936
|
}
|
|
8735
8937
|
return results;
|
|
8736
8938
|
});
|
|
8737
|
-
const
|
|
8939
|
+
const TOLERANCE_PX = 10;
|
|
8940
|
+
const deduped = [];
|
|
8941
|
+
const isSameSize = (a, b) => Math.abs(a.width - b.width) <= TOLERANCE_PX && Math.abs(a.height - b.height) <= TOLERANCE_PX;
|
|
8942
|
+
const sorted = [...elements].sort((a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height);
|
|
8943
|
+
for (const el of sorted) {
|
|
8944
|
+
const dupe = deduped.some(
|
|
8945
|
+
(d) => isSameSize(d.rect, el.rect) && Math.abs(d.rect.x - el.rect.x) <= TOLERANCE_PX && Math.abs(d.rect.y - el.rect.y) <= TOLERANCE_PX
|
|
8946
|
+
);
|
|
8947
|
+
if (!dupe) deduped.push(el);
|
|
8948
|
+
}
|
|
8949
|
+
deduped.sort((a, b) => b.rect.width * b.rect.height - a.rect.width * a.rect.height);
|
|
8950
|
+
const views = deduped.map((e, i) => ({
|
|
8738
8951
|
id: "el-" + i + "-" + (e.id || e.tag),
|
|
8739
8952
|
label: e.id || e.cls || e.tag,
|
|
8740
8953
|
rect: e.rect
|
|
@@ -8793,10 +9006,10 @@ var SessionManager = class extends EventEmitter {
|
|
|
8793
9006
|
this.screencasts.set(sessionId, {
|
|
8794
9007
|
capturer: new ScreencastCapturer({
|
|
8795
9008
|
interval: options?.interval ?? 100,
|
|
8796
|
-
quality: options?.quality ??
|
|
9009
|
+
quality: options?.quality ?? 40,
|
|
8797
9010
|
type: options?.type ?? "jpeg",
|
|
8798
|
-
width: options?.width ??
|
|
8799
|
-
height: options?.height ??
|
|
9011
|
+
width: options?.width ?? 1024,
|
|
9012
|
+
height: options?.height ?? 576
|
|
8800
9013
|
}),
|
|
8801
9014
|
page,
|
|
8802
9015
|
clientCount: 0
|
|
@@ -8937,6 +9150,8 @@ var SessionManager = class extends EventEmitter {
|
|
|
8937
9150
|
this.resetStaticSnapshotTimer(sessionId);
|
|
8938
9151
|
});
|
|
8939
9152
|
await sc.capturer.startCapture(sc.page, sessionId, onFrame);
|
|
9153
|
+
this.streamCoordinator.takeStaticSnapshot(sessionId, sc.page, sc.clientCount).catch(() => {
|
|
9154
|
+
});
|
|
8940
9155
|
}
|
|
8941
9156
|
async stopCapturer(sessionId) {
|
|
8942
9157
|
const sc = this.screencasts.get(sessionId);
|
|
@@ -9387,6 +9602,15 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9387
9602
|
registerSession(sessionId, page, options) {
|
|
9388
9603
|
this.sessionManager.registerSession(sessionId, page, options);
|
|
9389
9604
|
}
|
|
9605
|
+
/** Restart screencast for a session — used by viewer reconnect button. */
|
|
9606
|
+
async reconnectSession(sessionId) {
|
|
9607
|
+
try {
|
|
9608
|
+
await this.sessionManager.stopCapturer(sessionId);
|
|
9609
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
9610
|
+
await this.sessionManager.startCapturer(sessionId);
|
|
9611
|
+
} catch {
|
|
9612
|
+
}
|
|
9613
|
+
}
|
|
9390
9614
|
unregisterSession(sessionId) {
|
|
9391
9615
|
this.sessionManager.unregisterSession(sessionId);
|
|
9392
9616
|
}
|
|
@@ -9409,7 +9633,7 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9409
9633
|
});
|
|
9410
9634
|
}
|
|
9411
9635
|
this.sessionManager.incrementClientCount(sessionId);
|
|
9412
|
-
if (this.streamCoordinator.getLastFrameViewport()
|
|
9636
|
+
if (this.streamCoordinator.getLastFrameViewport()) {
|
|
9413
9637
|
this.streamCoordinator.replayLastFrame(sessionId).catch(() => {
|
|
9414
9638
|
});
|
|
9415
9639
|
}
|
|
@@ -9419,7 +9643,6 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9419
9643
|
if (count === 0 && this.sessionManager.isCapturerActive(sessionId)) {
|
|
9420
9644
|
this.sessionManager.stopCapturer(sessionId).then(() => {
|
|
9421
9645
|
this.streamCoordinator.resetFrameRate();
|
|
9422
|
-
this.streamCoordinator.clearLastFrame();
|
|
9423
9646
|
this.emit("screencast-stopped", sessionId);
|
|
9424
9647
|
}).catch(() => {
|
|
9425
9648
|
this.emit("screencast-stopped", sessionId);
|
|
@@ -9595,6 +9818,31 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9595
9818
|
this.emit("human-solved", { sessionId: sessionId ?? null, clientId });
|
|
9596
9819
|
return;
|
|
9597
9820
|
}
|
|
9821
|
+
if (msg.type === "reconnect") {
|
|
9822
|
+
this.emit("reconnect-request", { sessionId: sessionId ?? null, clientId });
|
|
9823
|
+
return;
|
|
9824
|
+
}
|
|
9825
|
+
if (msg.type === "health_ping") {
|
|
9826
|
+
this.sendToClient(clientId, { type: "health_pong", ts: msg.ts });
|
|
9827
|
+
return;
|
|
9828
|
+
}
|
|
9829
|
+
if (msg.type === "snapshot_request") {
|
|
9830
|
+
const page2 = sessionId ? this.sessionManager.getPageForSession(sessionId) : void 0;
|
|
9831
|
+
if (!page2) {
|
|
9832
|
+
this.sendToClient(clientId, { type: "snapshot_result", data: null, error: "no page" });
|
|
9833
|
+
return;
|
|
9834
|
+
}
|
|
9835
|
+
const fmt = msg.format === "webp" ? "webp" : "png";
|
|
9836
|
+
page2.screenshot({ type: fmt, quality: fmt === "webp" ? msg.quality ?? 90 : void 0 }).then((buf) => {
|
|
9837
|
+
this.sendToClient(clientId, {
|
|
9838
|
+
type: "snapshot_result",
|
|
9839
|
+
data: { data: buf.toString("base64"), format: fmt }
|
|
9840
|
+
});
|
|
9841
|
+
}).catch(() => {
|
|
9842
|
+
this.sendToClient(clientId, { type: "snapshot_result", data: null, error: "screenshot failed" });
|
|
9843
|
+
});
|
|
9844
|
+
return;
|
|
9845
|
+
}
|
|
9598
9846
|
const ctx = {
|
|
9599
9847
|
clientId,
|
|
9600
9848
|
sessionId,
|
|
@@ -9681,7 +9929,7 @@ function alignHTML(sessionId, _host) {
|
|
|
9681
9929
|
html,body{height:100%;overflow:hidden;background:#000}
|
|
9682
9930
|
.viewport{position:absolute;top:0;left:0;right:0;bottom:0;overflow:hidden}
|
|
9683
9931
|
.viewport img#screen{display:block;width:100%;height:100%;object-fit:contain;background:#111}
|
|
9684
|
-
.crosshair{position:fixed;pointer-events:none;z-index:9999;width:16px;height:16px;border-radius:50%;background:rgba(255,0,0,0.8);border:2px solid #fff;transform:translate(-50%,-50%);display:none;box-shadow:0 0 6px rgba(255,0,0,0.6)
|
|
9932
|
+
.crosshair{position:fixed;pointer-events:none;z-index:9999;width:16px;height:16px;border-radius:50%;background:rgba(255,0,0,0.8);border:2px solid #fff;transform:translate(-50%,-50%);display:none;box-shadow:0 0 6px rgba(255,0,0,0.6)}
|
|
9685
9933
|
.coord{position:fixed;top:8px;left:8px;z-index:10000;font:12px/1.6 monospace;color:#0f0;background:rgba(0,0,0,0.7);padding:4px 8px;border-radius:4px;pointer-events:none;white-space:pre}
|
|
9686
9934
|
.grid{position:fixed;pointer-events:none;z-index:9998;top:0;left:0;right:0;bottom:0;display:none}
|
|
9687
9935
|
</style></head><body>
|
|
@@ -9773,9 +10021,17 @@ viewportEl.addEventListener('mousemove',function(e){
|
|
|
9773
10021
|
var r=viewerToRemote(e.clientX,e.clientY);
|
|
9774
10022
|
var rx=Math.max(0,Math.min(remoteViewport.width,r.x));
|
|
9775
10023
|
var ry=Math.max(0,Math.min(remoteViewport.height,r.y));
|
|
10024
|
+
// \u672C\u5730\u5149\u6807\u7ACB\u5373\u66F4\u65B0\uFF08\u96F6\u5EF6\u8FDF\u53CD\u9988\uFF09
|
|
9776
10025
|
setCrosshair(rx,ry);
|
|
9777
|
-
|
|
10026
|
+
// \u8282\u6D41\uFF1A\u6700\u591A\u6BCF 30ms \u53D1\u4E00\u6B21 move\uFF0C\u907F\u514D WS \u6D88\u606F\u6D2A\u6CDB
|
|
10027
|
+
if(!moveThrottled&&ws&&ws.readyState===1){
|
|
10028
|
+
moveThrottled=true;
|
|
10029
|
+
var action=e.buttons>0?'move':'hover';
|
|
10030
|
+
ws.send(JSON.stringify({type:'input_mouse',action:action,x:rx,y:ry}));
|
|
10031
|
+
setTimeout(function(){moveThrottled=false;},30);
|
|
10032
|
+
}
|
|
9778
10033
|
});
|
|
10034
|
+
var moveThrottled=false;
|
|
9779
10035
|
viewportEl.addEventListener('mousedown',function(e){var r=viewerToRemote(e.clientX,e.clientY);setCrosshair(r.x,r.y);if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'input_mouse',action:'down',x:r.x,y:r.y}));});
|
|
9780
10036
|
viewportEl.addEventListener('mouseup',function(e){var r=viewerToRemote(e.clientX,e.clientY);setCrosshair(r.x,r.y);if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'input_mouse',action:'up',x:r.x,y:r.y}));});
|
|
9781
10037
|
viewportEl.addEventListener('wheel',function(e){e.preventDefault();if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'scroll',deltaX:e.deltaX,deltaY:e.deltaY}));},{passive:false});
|
|
@@ -9803,7 +10059,7 @@ body{display:flex;flex-direction:column;touch-action:manipulation}
|
|
|
9803
10059
|
.bar .dot.ok{background:#2ecc71}
|
|
9804
10060
|
.bar .url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8899aa;font-size:12px}
|
|
9805
10061
|
.bar .conn{color:#556;font-size:11px;flex-shrink:0}
|
|
9806
|
-
.viewport{flex
|
|
10062
|
+
.viewport{flex:1 1 0;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#1a1a2e;position:relative;min-height:0}
|
|
9807
10063
|
.viewport canvas#screen{display:none;user-select:none;-webkit-user-drag:none}
|
|
9808
10064
|
.waiting{color:#556;font-size:14px;text-align:center}
|
|
9809
10065
|
.toolbar{flex-shrink:0;background:#16213e;border-top:1px solid #0f3460;display:none;flex-direction:row;z-index:90;padding:4px 6px;gap:4px;overflow-x:auto;-webkit-overflow-scrolling:touch}
|
|
@@ -9866,6 +10122,33 @@ body{display:flex;flex-direction:column;touch-action:manipulation}
|
|
|
9866
10122
|
<span class="conn" id="conn"></span>
|
|
9867
10123
|
<button class="mode-btn" id="mode-btn" title="Switch mode (Mobile/Tablet/PC)">\u{1F4F1}</button>
|
|
9868
10124
|
<div id="qualityBadge" class="quality-badge quality-static" style="font-size:10px;padding:2px 8px;border-radius:10px;background:#556;color:#eee;white-space:nowrap;flex-shrink:0">static</div>
|
|
10125
|
+
<button id="stats-btn" style="font-size:11px;padding:2px 8px;border-radius:10px;background:#0f3460;color:#8af;border:none;cursor:pointer;flex-shrink:0">\u{1F4CA}</button>
|
|
10126
|
+
<button id="reconnect-btn" style="font-size:11px;padding:2px 8px;border-radius:10px;background:#0f3460;color:#f39c12;border:none;cursor:pointer;flex-shrink:0" title="\u91CD\u8FDE\u8FDC\u7A0B\u6D4F\u89C8\u5668">\u{1F504}</button>
|
|
10127
|
+
<button id="snapshot-btn" style="font-size:11px;padding:2px 8px;border-radius:10px;background:#0f3460;color:#8af;border:none;cursor:pointer;flex-shrink:0" title="\u622A\u53D6\u9AD8\u6E05\u622A\u56FE">\u{1F4F7}</button>
|
|
10128
|
+
</div>
|
|
10129
|
+
<div id="snapshot-preview" style="display:none;position:fixed;bottom:8px;right:8px;z-index:10000;width:200px;background:rgba(10,15,30,0.95);border:1px solid #0f3460;border-radius:8px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,0.5);cursor:pointer">
|
|
10130
|
+
<img id="snapshot-img" style="display:block;width:100%;height:auto" />
|
|
10131
|
+
<div style="padding:4px 8px;font:10px/1.4 monospace;color:#8af;display:flex;justify-content:space-between;align-items:center">
|
|
10132
|
+
<span>\u{1F4F7} \u9AD8\u6E05\u622A\u56FE \xB7 \u70B9\u51FB\u653E\u5927</span>
|
|
10133
|
+
<span id="snapshot-countdown" style="color:#f39c12"></span>
|
|
10134
|
+
</div>
|
|
10135
|
+
<div id="snapshot-progress" style="height:2px;background:#0f3460;width:100%"></div>
|
|
10136
|
+
</div>
|
|
10137
|
+
<div id="snapshot-fullscreen" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;z-index:99999;background:rgba(0,0,0,0.92);align-items:center;justify-content:center;cursor:zoom-out;overflow:auto;padding:20px">
|
|
10138
|
+
<img id="snapshot-full-img" style="max-width:100%;max-height:100%;object-fit:contain;border-radius:4px;box-shadow:0 8px 40px rgba(0,0,0,0.8)" />
|
|
10139
|
+
</div>
|
|
10140
|
+
</div>
|
|
10141
|
+
<div id="stats-panel" style="display:none;position:fixed;top:40px;right:8px;z-index:10001;background:rgba(10,15,30,0.95);border:1px solid #0f3460;border-radius:8px;padding:8px 12px;font:11px/1.7 monospace;color:#8af;min-width:200px;backdrop-filter:blur(8px)">
|
|
10142
|
+
<div style="font-weight:bold;color:#cef;margin-bottom:4px;border-bottom:1px solid #0f3460;padding-bottom:4px">\u{1F4CA} Live Monitor</div>
|
|
10143
|
+
<div>FPS: <span id="stat-fps" style="color:#2ecc71">0</span></div>
|
|
10144
|
+
<div>Frame size: <span id="stat-frame-size" style="color:#2ecc71">0KB</span></div>
|
|
10145
|
+
<div>Latency: <span id="stat-latency" style="color:#2ecc71">0ms</span></div>
|
|
10146
|
+
<div>Bandwidth: <span id="stat-bandwidth" style="color:#2ecc71">0KB/s</span></div>
|
|
10147
|
+
<div>Frames: <span id="stat-frames" style="color:#8af">0</span></div>
|
|
10148
|
+
<div>Sent: <span id="stat-sent" style="color:#8af">0</span></div>
|
|
10149
|
+
<div>WS: <span id="stat-ws" style="color:#e74c3c">disconnected</span></div>
|
|
10150
|
+
<div>Viewport: <span id="stat-viewport" style="color:#8af">0x0</span></div>
|
|
10151
|
+
<div>Uptime: <span id="stat-uptime" style="color:#8af">0s</span></div>
|
|
9869
10152
|
</div>
|
|
9870
10153
|
<div class="view-tabs" id="view-tabs"></div>
|
|
9871
10154
|
<div class="viewport" id="viewport">
|
|
@@ -9934,6 +10217,14 @@ let currentFocusedSelector='';
|
|
|
9934
10217
|
let currentFocusedValue='';
|
|
9935
10218
|
let suppressFocus=false;
|
|
9936
10219
|
let deviceMode='desktop';
|
|
10220
|
+
|
|
10221
|
+
// === Live Monitor stats ===
|
|
10222
|
+
const startTime=Date.now();
|
|
10223
|
+
let statFrames=0,statSent=0,statBytesRcvd=0,statBytesSent=0;
|
|
10224
|
+
let statLastFrameTime=0,statLatency=0,statFrameSize=0,statFps=0;
|
|
10225
|
+
let statFpsFrames=0,statFpsTimer=Date.now();
|
|
10226
|
+
let statEl=null;
|
|
10227
|
+
|
|
9937
10228
|
let lastHoverSent=0;
|
|
9938
10229
|
|
|
9939
10230
|
const $=id=>document.getElementById(id);
|
|
@@ -9942,13 +10233,136 @@ const viewportEl=$('viewport'),cursorEl=$('cursor'),cursorLabelEl=$('cursor-labe
|
|
|
9942
10233
|
const touchpadEl=$('touchpad'),toolbarEl=$('toolbar');
|
|
9943
10234
|
const inputPanel=$('input-panel'),inputField=$('input-field'),inputLabel=$('input-label');
|
|
9944
10235
|
|
|
10236
|
+
// Init monitor elements after $ is defined
|
|
10237
|
+
statEl={
|
|
10238
|
+
fps:$('stat-fps'),frameSize:$('stat-frame-size'),latency:$('stat-latency'),
|
|
10239
|
+
bandwidth:$('stat-bandwidth'),frames:$('stat-frames'),sent:$('stat-sent'),
|
|
10240
|
+
ws:$('stat-ws'),viewport:$('stat-viewport'),uptime:$('stat-uptime'),
|
|
10241
|
+
};
|
|
10242
|
+
$('stats-btn').addEventListener('click',()=>{
|
|
10243
|
+
const panel=$('stats-panel');
|
|
10244
|
+
panel.style.display=panel.style.display==='none'?'block':'none';
|
|
10245
|
+
});
|
|
10246
|
+
$('reconnect-btn').addEventListener('click',()=>{
|
|
10247
|
+
const btn=$('reconnect-btn');
|
|
10248
|
+
btn.textContent='\u23F3';
|
|
10249
|
+
btn.style.background='#e74c3c';
|
|
10250
|
+
// Tell daemon to reconnect the remote CDP session
|
|
10251
|
+
sendMsg({type:'reconnect'});
|
|
10252
|
+
// Also reload the page after 2s to re-establish viewer WS
|
|
10253
|
+
setTimeout(()=>{location.reload();},2000);
|
|
10254
|
+
});
|
|
10255
|
+
|
|
10256
|
+
// === Snapshot feature ===
|
|
10257
|
+
// Click \u{1F4F7} \u2192 daemon sends CDP captureScreenshot \u2192 returns high-quality image
|
|
10258
|
+
// Preview shows in bottom-right for 5s with countdown \u2192 click to fullscreen
|
|
10259
|
+
var snapshotDataUrl=null;
|
|
10260
|
+
var snapshotCountdownTimer=null;
|
|
10261
|
+
$('snapshot-btn').addEventListener('click',function(){
|
|
10262
|
+
var btn=$('snapshot-btn');
|
|
10263
|
+
btn.textContent='\u23F3';
|
|
10264
|
+
// Request high-quality screenshot from daemon
|
|
10265
|
+
sendMsg({type:'snapshot_request',format:'webp',quality:90});
|
|
10266
|
+
});
|
|
10267
|
+
// Handle snapshot response
|
|
10268
|
+
// (added to ws.onmessage handler below)
|
|
10269
|
+
var snapshotPendingHandler=function(msg){
|
|
10270
|
+
if(msg.type==='snapshot_result'&&msg.data){
|
|
10271
|
+
var data=msg.data;
|
|
10272
|
+
snapshotDataUrl='data:image/'+(data.format||'png')+';base64,'+data.data;
|
|
10273
|
+
var img=$('snapshot-img');
|
|
10274
|
+
img.src=snapshotDataUrl;
|
|
10275
|
+
var preview=$('snapshot-preview');
|
|
10276
|
+
preview.style.display='block';
|
|
10277
|
+
$('snapshot-btn').textContent='\u{1F4F7}';
|
|
10278
|
+
// Start 5s countdown
|
|
10279
|
+
var remaining=5;
|
|
10280
|
+
var progress=$('snapshot-progress');
|
|
10281
|
+
var countdownEl=$('snapshot-countdown');
|
|
10282
|
+
if(snapshotCountdownTimer) clearInterval(snapshotCountdownTimer);
|
|
10283
|
+
progress.style.width='100%';
|
|
10284
|
+
progress.style.background='#2ecc71';
|
|
10285
|
+
countdownEl.textContent=remaining+'s';
|
|
10286
|
+
snapshotCountdownTimer=setInterval(function(){
|
|
10287
|
+
remaining--;
|
|
10288
|
+
countdownEl.textContent=remaining+'s';
|
|
10289
|
+
progress.style.width=(remaining/5*100)+'%';
|
|
10290
|
+
if(remaining<=0){
|
|
10291
|
+
clearInterval(snapshotCountdownTimer);
|
|
10292
|
+
preview.style.display='none';
|
|
10293
|
+
}
|
|
10294
|
+
},1000);
|
|
10295
|
+
// Hover to pause countdown
|
|
10296
|
+
preview.onmouseenter=function(){if(snapshotCountdownTimer){clearInterval(snapshotCountdownTimer);countdownEl.textContent='\u23F8';}};
|
|
10297
|
+
preview.onmouseleave=function(){
|
|
10298
|
+
if(snapshotCountdownTimer) clearInterval(snapshotCountdownTimer);
|
|
10299
|
+
progress.style.width=(remaining/5*100)+'%';
|
|
10300
|
+
snapshotCountdownTimer=setInterval(function(){
|
|
10301
|
+
remaining--;
|
|
10302
|
+
countdownEl.textContent=remaining+'s';
|
|
10303
|
+
progress.style.width=(remaining/5*100)+'%';
|
|
10304
|
+
if(remaining<=0){clearInterval(snapshotCountdownTimer);preview.style.display='none';}
|
|
10305
|
+
},1000);
|
|
10306
|
+
};
|
|
10307
|
+
}
|
|
10308
|
+
};
|
|
10309
|
+
// Click preview \u2192 fullscreen
|
|
10310
|
+
$('snapshot-preview').addEventListener('click',function(){
|
|
10311
|
+
if(!snapshotDataUrl) return;
|
|
10312
|
+
$('snapshot-full-img').src=snapshotDataUrl;
|
|
10313
|
+
$('snapshot-fullscreen').style.display='flex';
|
|
10314
|
+
if(snapshotCountdownTimer){clearInterval(snapshotCountdownTimer);}
|
|
10315
|
+
});
|
|
10316
|
+
// Click fullscreen \u2192 close
|
|
10317
|
+
$('snapshot-fullscreen').addEventListener('click',function(){
|
|
10318
|
+
this.style.display='none';
|
|
10319
|
+
});
|
|
10320
|
+
// Auto-detect dead connection: if no frames for 30s, show warning
|
|
10321
|
+
let lastFrameAt=Date.now();
|
|
10322
|
+
setInterval(()=>{
|
|
10323
|
+
if(connected && Date.now()-lastFrameAt>30000 && statFrames>0){
|
|
10324
|
+
const btn=$('reconnect-btn');
|
|
10325
|
+
btn.style.background='#e74c3c';
|
|
10326
|
+
btn.textContent='\u{1F504}!';
|
|
10327
|
+
btn.title='\u8FDE\u63A5\u53EF\u80FD\u5DF2\u65AD\u5F00\uFF0C\u70B9\u51FB\u91CD\u8FDE';
|
|
10328
|
+
}
|
|
10329
|
+
},5000);
|
|
10330
|
+
setInterval(()=>{
|
|
10331
|
+
const now=Date.now();
|
|
10332
|
+
const elapsed=now-statFpsTimer;
|
|
10333
|
+
if(elapsed>=1000){
|
|
10334
|
+
statFps=Math.round(statFpsFrames*1000/elapsed);
|
|
10335
|
+
statFpsFrames=0;
|
|
10336
|
+
statFpsTimer=now;
|
|
10337
|
+
const bw=Math.round((statBytesRcvd+statBytesSent)/1024);
|
|
10338
|
+
statEl.fps.textContent=statFps;
|
|
10339
|
+
statEl.fps.style.color=statFps>=15?'#2ecc71':statFps>=5?'#f39c12':'#e74c3c';
|
|
10340
|
+
statEl.frameSize.textContent=Math.round(statFrameSize/1024)+'KB';
|
|
10341
|
+
statEl.latency.textContent=statLatency+'ms';
|
|
10342
|
+
statEl.latency.style.color=statLatency<100?'#2ecc71':statLatency<300?'#f39c12':'#e74c3c';
|
|
10343
|
+
statEl.bandwidth.textContent=bw+'KB/s';
|
|
10344
|
+
statEl.frames.textContent=statFrames;
|
|
10345
|
+
statEl.sent.textContent=statSent;
|
|
10346
|
+
statEl.ws.textContent=connected?'connected':'disconnected';
|
|
10347
|
+
statEl.ws.style.color=connected?'#2ecc71':'#e74c3c';
|
|
10348
|
+
statEl.viewport.textContent=remoteViewport.width+'x'+remoteViewport.height;
|
|
10349
|
+
statEl.uptime.textContent=Math.round((now-startTime)/1000)+'s';
|
|
10350
|
+
statBytesRcvd=0;statBytesSent=0;
|
|
10351
|
+
}
|
|
10352
|
+
},200);
|
|
10353
|
+
|
|
9945
10354
|
function resizeCanvas(){
|
|
9946
10355
|
const box=viewportEl.getBoundingClientRect();
|
|
9947
|
-
const bw=box.width;
|
|
9948
|
-
if(!bw||!remoteViewport.width||!remoteViewport.height) return;
|
|
10356
|
+
const bw=box.width,bh=box.height;
|
|
10357
|
+
if(!bw||!bh||!remoteViewport.width||!remoteViewport.height) return;
|
|
9949
10358
|
const vpAspect=remoteViewport.width/remoteViewport.height;
|
|
9950
|
-
|
|
9951
|
-
|
|
10359
|
+
// Fit the remote viewport inside the available box without cropping:
|
|
10360
|
+
// pick the scale that keeps both dimensions within bounds.
|
|
10361
|
+
const scaleByWidth=bw/remoteViewport.width;
|
|
10362
|
+
const scaleByHeight=bh/remoteViewport.height;
|
|
10363
|
+
const scale=Math.min(scaleByWidth,scaleByHeight);
|
|
10364
|
+
const cw=remoteViewport.width*scale;
|
|
10365
|
+
const ch=remoteViewport.height*scale;
|
|
9952
10366
|
if(canvas.width!==Math.round(cw)||canvas.height!==Math.round(ch)){
|
|
9953
10367
|
canvas.width=Math.round(cw);
|
|
9954
10368
|
canvas.height=Math.round(ch);
|
|
@@ -9981,6 +10395,10 @@ function connectWS(){
|
|
|
9981
10395
|
connEl.textContent='WS';
|
|
9982
10396
|
if(deviceMode==='desktop') createPCKeyboard();
|
|
9983
10397
|
checkFocus();
|
|
10398
|
+
// Apply crop from URL params (?crop=x,y,w,h or ?selector=.cls)
|
|
10399
|
+
setTimeout(applyUrlCrop,500);
|
|
10400
|
+
// Start connection health check \u2014 auto-reconnect on failure
|
|
10401
|
+
startHealthCheck();
|
|
9984
10402
|
};
|
|
9985
10403
|
ws.binaryType='arraybuffer';
|
|
9986
10404
|
ws.onmessage=(e)=>{
|
|
@@ -9991,13 +10409,31 @@ function connectWS(){
|
|
|
9991
10409
|
updateViewTabs(_m.views);
|
|
9992
10410
|
return;
|
|
9993
10411
|
}
|
|
10412
|
+
if(_m.type==='health_pong'){
|
|
10413
|
+
healthCheckPending=false;
|
|
10414
|
+
var btn=$('reconnect-btn');
|
|
10415
|
+
if(btn.textContent!=='\u{1F504}'){btn.style.background='#0f3460';btn.textContent='\u{1F504}';}
|
|
10416
|
+
return;
|
|
10417
|
+
}
|
|
10418
|
+
if(_m.type==='snapshot_result'){
|
|
10419
|
+
if(snapshotPendingHandler) snapshotPendingHandler(_m);
|
|
10420
|
+
return;
|
|
10421
|
+
}
|
|
9994
10422
|
}
|
|
9995
10423
|
if(e.data instanceof ArrayBuffer){
|
|
9996
10424
|
const buf=new Uint8Array(e.data);
|
|
10425
|
+
statBytesRcvd+=buf.length;
|
|
9997
10426
|
const headerLen=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
|
|
9998
10427
|
const header=JSON.parse(new TextDecoder().decode(buf.slice(4,4+headerLen)));
|
|
9999
10428
|
const jpegData=buf.slice(4+headerLen);
|
|
10000
10429
|
if(header.type==='screenshot'){
|
|
10430
|
+
// === Monitor: track frame stats ===
|
|
10431
|
+
statFrames++;statFpsFrames++;
|
|
10432
|
+
lastFrameAt=Date.now();
|
|
10433
|
+
statFrameSize=jpegData.length;
|
|
10434
|
+
if(header.data.timestamp){
|
|
10435
|
+
statLatency=Math.max(0,Date.now()-header.data.timestamp);
|
|
10436
|
+
}
|
|
10001
10437
|
const blob=new Blob([jpegData],{type:'image/jpeg'});
|
|
10002
10438
|
createImageBitmap(blob).then(function(bmp){
|
|
10003
10439
|
drawFrame(bmp);
|
|
@@ -10139,6 +10575,8 @@ function updateViewTabs(views){
|
|
|
10139
10575
|
selectView('main');
|
|
10140
10576
|
}
|
|
10141
10577
|
renderViewTabs();
|
|
10578
|
+
// Auto-select view matching ?selector= param
|
|
10579
|
+
if(pendingSelector) trySelectBySelector();
|
|
10142
10580
|
}
|
|
10143
10581
|
function renderViewTabs(){
|
|
10144
10582
|
if(detectedViews.length===0){
|
|
@@ -10227,7 +10665,12 @@ function selectView(vid){
|
|
|
10227
10665
|
}
|
|
10228
10666
|
|
|
10229
10667
|
function sendMsg(obj){
|
|
10230
|
-
if(ws&&ws.readyState===WebSocket.OPEN)
|
|
10668
|
+
if(ws&&ws.readyState===WebSocket.OPEN){
|
|
10669
|
+
const data=JSON.stringify(obj);
|
|
10670
|
+
ws.send(data);
|
|
10671
|
+
statSent++;
|
|
10672
|
+
statBytesSent+=data.length;
|
|
10673
|
+
}
|
|
10231
10674
|
}
|
|
10232
10675
|
|
|
10233
10676
|
function parseFocusHash(){
|
|
@@ -10237,6 +10680,107 @@ function parseFocusHash(){
|
|
|
10237
10680
|
return m?decodeURIComponent(m[1]):null;
|
|
10238
10681
|
}
|
|
10239
10682
|
|
|
10683
|
+
// Parse crop/view parameters from URL query string.
|
|
10684
|
+
// Supports:
|
|
10685
|
+
// ?selector=.login-modal \u2014 crop to element matching CSS selector
|
|
10686
|
+
// ?crop=100,200,500,400 \u2014 crop to x,y,width,height (remote viewport coords)
|
|
10687
|
+
function parseUrlCrop(){
|
|
10688
|
+
var params=new URLSearchParams(location.search);
|
|
10689
|
+
|
|
10690
|
+
// ?crop=x,y,w,h
|
|
10691
|
+
var cropParam=params.get('crop');
|
|
10692
|
+
if(cropParam){
|
|
10693
|
+
var parts=cropParam.split(',').map(function(s){return parseInt(s.trim(),10)});
|
|
10694
|
+
if(parts.length===4&&parts.every(function(n){return!isNaN(n)})){
|
|
10695
|
+
return {type:'rect',rect:{x:parts[0],y:parts[1],width:parts[2],height:parts[3]}};
|
|
10696
|
+
}
|
|
10697
|
+
}
|
|
10698
|
+
|
|
10699
|
+
// ?selector=.login-modal
|
|
10700
|
+
var selParam=params.get('selector')||params.get('sel');
|
|
10701
|
+
if(selParam){
|
|
10702
|
+
return {type:'selector',selector:selParam};
|
|
10703
|
+
}
|
|
10704
|
+
|
|
10705
|
+
return null;
|
|
10706
|
+
}
|
|
10707
|
+
|
|
10708
|
+
// Apply URL crop on connect \u2014 sends select_view immediately after WS opens
|
|
10709
|
+
function applyUrlCrop(){
|
|
10710
|
+
var cropSpec=parseUrlCrop();
|
|
10711
|
+
if(!cropSpec) return;
|
|
10712
|
+
|
|
10713
|
+
if(cropSpec.type==='rect'){
|
|
10714
|
+
// Direct rect \u2014 send select_view immediately
|
|
10715
|
+
sendMsg({type:'select_view',rect:cropSpec.rect});
|
|
10716
|
+
if(!originalViewport) originalViewport={width:remoteViewport.width,height:remoteViewport.height};
|
|
10717
|
+
remoteViewport={width:cropSpec.rect.width,height:cropSpec.rect.height};
|
|
10718
|
+
viewportLocked=true;
|
|
10719
|
+
resizeCanvas();
|
|
10720
|
+
return;
|
|
10721
|
+
}
|
|
10722
|
+
|
|
10723
|
+
// Selector \u2014 wait for views_update from element-monitor, then auto-select matching view
|
|
10724
|
+
if(cropSpec.type==='selector'){
|
|
10725
|
+
pendingSelector=cropSpec.selector;
|
|
10726
|
+
// Try match immediately in case views already arrived
|
|
10727
|
+
trySelectBySelector();
|
|
10728
|
+
}
|
|
10729
|
+
}
|
|
10730
|
+
// Connection health check \u2014 send a ping every 15s.
|
|
10731
|
+
// If no response in 5s, auto-reconnect the session.
|
|
10732
|
+
var healthCheckTimer=null;
|
|
10733
|
+
var healthCheckPending=false;
|
|
10734
|
+
var autoReconnecting=false;
|
|
10735
|
+
function startHealthCheck(){
|
|
10736
|
+
if(healthCheckTimer) clearInterval(healthCheckTimer);
|
|
10737
|
+
healthCheckTimer=setInterval(function(){
|
|
10738
|
+
if(!connected) return;
|
|
10739
|
+
if(healthCheckPending){
|
|
10740
|
+
// Previous ping never got a response \u2192 connection is dead \u2192 auto-reconnect
|
|
10741
|
+
if(!autoReconnecting){
|
|
10742
|
+
autoReconnecting=true;
|
|
10743
|
+
var btn=$('reconnect-btn');
|
|
10744
|
+
btn.style.background='#e74c3c';
|
|
10745
|
+
btn.textContent='\u{1F504}!';
|
|
10746
|
+
btn.title='\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u8FDE...';
|
|
10747
|
+
// Auto-reconnect: send reconnect then reload after 2s
|
|
10748
|
+
sendMsg({type:'reconnect'});
|
|
10749
|
+
setTimeout(function(){location.reload();},2000);
|
|
10750
|
+
}
|
|
10751
|
+
return;
|
|
10752
|
+
}
|
|
10753
|
+
healthCheckPending=true;
|
|
10754
|
+
sendMsg({type:'health_ping',ts:Date.now()});
|
|
10755
|
+
setTimeout(function(){
|
|
10756
|
+
if(healthCheckPending){
|
|
10757
|
+
// Still pending after 5s = dead
|
|
10758
|
+
healthCheckPending=false;
|
|
10759
|
+
var btn2=$('reconnect-btn');
|
|
10760
|
+
btn2.style.background='#e74c3c';
|
|
10761
|
+
btn2.textContent='\u{1F504}!';
|
|
10762
|
+
}
|
|
10763
|
+
},5000);
|
|
10764
|
+
},15000);
|
|
10765
|
+
}
|
|
10766
|
+
function trySelectBySelector(){
|
|
10767
|
+
if(!pendingSelector||!detectedViews||detectedViews.length===0) return;
|
|
10768
|
+
// Match by label containing selector text, or id/class match
|
|
10769
|
+
var target=detectedViews.find(function(v){
|
|
10770
|
+
var label=(v.label||'').toLowerCase();
|
|
10771
|
+
var id=(v.id||'').toLowerCase();
|
|
10772
|
+
return label.indexOf(pendingSelector.toLowerCase())>=0||id.indexOf(pendingSelector.toLowerCase())>=0;
|
|
10773
|
+
});
|
|
10774
|
+
if(target){
|
|
10775
|
+
sendMsg({type:'select_view',rect:target.rect});
|
|
10776
|
+
if(!originalViewport) originalViewport={width:remoteViewport.width,height:remoteViewport.height};
|
|
10777
|
+
remoteViewport={width:target.rect.width,height:target.rect.height};
|
|
10778
|
+
viewportLocked=true;
|
|
10779
|
+
resizeCanvas();
|
|
10780
|
+
pendingSelector=null; // matched, stop looking
|
|
10781
|
+
}
|
|
10782
|
+
}
|
|
10783
|
+
|
|
10240
10784
|
function checkFocus(){
|
|
10241
10785
|
var sel=parseFocusHash();
|
|
10242
10786
|
if(sel&&ws&&ws.readyState===WebSocket.OPEN){
|
|
@@ -10336,10 +10880,16 @@ function createPCKeyboard(){
|
|
|
10336
10880
|
document.addEventListener('keydown',function(e){
|
|
10337
10881
|
if(deviceMode!=='desktop') return;
|
|
10338
10882
|
if(e.isComposing||_pcComposing) return;
|
|
10339
|
-
var tag=e.target&&e.target.tagName;
|
|
10340
|
-
if(tag==='INPUT'||tag==='TEXTAREA') return;
|
|
10341
10883
|
var key=e.key;
|
|
10342
10884
|
if(!key) return;
|
|
10885
|
+
|
|
10886
|
+
// Control keys (Backspace, Delete, Enter, Tab, arrows, etc.) must always
|
|
10887
|
+
// be forwarded even when focus is in an input/textarea \u2014 otherwise the user
|
|
10888
|
+
// can't edit text in the remote browser's input fields.
|
|
10889
|
+
var isControlKey=key.length>1; // Backspace, Delete, Enter, Tab, ArrowX, etc.
|
|
10890
|
+
var tag=e.target&&e.target.tagName;
|
|
10891
|
+
if(!isControlKey&&(tag==='INPUT'||tag==='TEXTAREA')) return;
|
|
10892
|
+
|
|
10343
10893
|
e.preventDefault();
|
|
10344
10894
|
sendMsg({type:'input_keyboard',action:'down',key:key});
|
|
10345
10895
|
if(key.length===1){
|
|
@@ -10350,10 +10900,13 @@ function createPCKeyboard(){
|
|
|
10350
10900
|
document.addEventListener('keyup',function(e){
|
|
10351
10901
|
if(deviceMode!=='desktop') return;
|
|
10352
10902
|
if(e.isComposing||_pcComposing) return;
|
|
10353
|
-
var tag=e.target&&e.target.tagName;
|
|
10354
|
-
if(tag==='INPUT'||tag==='TEXTAREA') return;
|
|
10355
10903
|
var key=e.key;
|
|
10356
10904
|
if(!key) return;
|
|
10905
|
+
|
|
10906
|
+
var isControlKey=key.length>1;
|
|
10907
|
+
var tag=e.target&&e.target.tagName;
|
|
10908
|
+
if(!isControlKey&&(tag==='INPUT'||tag==='TEXTAREA')) return;
|
|
10909
|
+
|
|
10357
10910
|
e.preventDefault();
|
|
10358
10911
|
sendMsg({type:'input_keyboard',action:'up',key:key});
|
|
10359
10912
|
sendUserActivity();
|
|
@@ -10406,9 +10959,20 @@ viewportEl.addEventListener('mouseleave',()=>{
|
|
|
10406
10959
|
viewportEl.addEventListener('wheel',(e)=>{
|
|
10407
10960
|
if(deviceMode!=='desktop') return;
|
|
10408
10961
|
e.preventDefault();
|
|
10409
|
-
|
|
10410
|
-
|
|
10962
|
+
// Accumulate wheel deltas and send at most every 50ms to avoid flooding
|
|
10963
|
+
// the remote with hundreds of scroll commands during a single swipe.
|
|
10964
|
+
pendingScrollX+=e.deltaX;
|
|
10965
|
+
pendingScrollY+=e.deltaY;
|
|
10966
|
+
if(!scrollThrottled){
|
|
10967
|
+
scrollThrottled=true;
|
|
10968
|
+
sendMsg({type:'scroll',deltaX:Math.round(pendingScrollX),deltaY:Math.round(pendingScrollY)});
|
|
10969
|
+
sendUserActivity();
|
|
10970
|
+
pendingScrollX=0;
|
|
10971
|
+
pendingScrollY=0;
|
|
10972
|
+
setTimeout(function(){scrollThrottled=false;},50);
|
|
10973
|
+
}
|
|
10411
10974
|
},{passive:false});
|
|
10975
|
+
var scrollThrottled=false,pendingScrollX=0,pendingScrollY=0;
|
|
10412
10976
|
|
|
10413
10977
|
// --- Mobile Touchpad (incremental, smooth) ---
|
|
10414
10978
|
let tpStartPos=null;
|
|
@@ -10905,6 +11469,13 @@ async function main() {
|
|
|
10905
11469
|
rpcHandler.setPreviewWS(previewWS);
|
|
10906
11470
|
previewWS.on("screencast-started", (sid) => log(`Preview screencast started: ${sid}`));
|
|
10907
11471
|
previewWS.on("screencast-stopped", (sid) => log(`Preview screencast stopped: ${sid}`));
|
|
11472
|
+
previewWS.on("reconnect-request", ({ sessionId }) => {
|
|
11473
|
+
if (!sessionId) return;
|
|
11474
|
+
log(`Reconnect request from viewer for session: ${sessionId}`);
|
|
11475
|
+
rpcHandler.handleReconnect(sessionId).catch((e) => {
|
|
11476
|
+
log(`Reconnect failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
11477
|
+
});
|
|
11478
|
+
});
|
|
10908
11479
|
mkdirSync5(CONFIG_DIR3, { recursive: true });
|
|
10909
11480
|
writeFileSync5(join7(CONFIG_DIR3, "daemon.json"), JSON.stringify({
|
|
10910
11481
|
port: daemonPort,
|
|
@@ -10913,6 +11484,13 @@ async function main() {
|
|
|
10913
11484
|
}, null, 2));
|
|
10914
11485
|
console.log(`xbrowser daemon started (pid: ${process.pid}, port: ${daemonPort})`);
|
|
10915
11486
|
log("Daemon main started successfully");
|
|
11487
|
+
const loader = await getPluginLoader();
|
|
11488
|
+
const sites = loader.getCore().loader.getSites();
|
|
11489
|
+
log(`Loaded ${sites.length} plugins`);
|
|
11490
|
+
if (sites.length > 0) {
|
|
11491
|
+
const names = sites.map((s) => s.name || "unknown").join(", ");
|
|
11492
|
+
log(`Plugins: ${names}`);
|
|
11493
|
+
}
|
|
10916
11494
|
const shutdown = () => {
|
|
10917
11495
|
log("Received shutdown signal, stopping");
|
|
10918
11496
|
previewWS.stop().catch(() => {
|