@xbrowser/cli 1.9.7 → 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-JXIJF4VL.js → browser-FFYPFTBV.js} +1 -1
- package/dist/{browser-ALPLJNH2.js → browser-RHWUAMTB.js} +2 -2
- package/dist/{browser-ZPQP6VW7.js → browser-YCO4GJMZ.js} +2 -2
- package/dist/{cdp-driver-MI7VCE44.js → cdp-driver-2T4P4ZE2.js} +103 -2
- package/dist/{cdp-driver-6EVYLHL2.js → cdp-driver-J3YQ4LJV.js} +1 -1
- package/dist/chunk-3OD76SUE.js +20 -0
- package/dist/chunk-43TEMG4U.js +9 -0
- package/dist/{chunk-CH5ZQV64.js → chunk-4452SPFI.js} +928 -18
- package/dist/{chunk-C4O73DQL.js → chunk-5UGR6MUK.js} +928 -18
- package/dist/{chunk-37SHZSUR.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-74S7UUI2.js → chunk-GUTBIYFW.js} +117 -17
- 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-VJNMAWPZ.js → chunk-RRBXV7KE.js} +103 -2
- package/dist/{chunk-5JSOGINQ.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 +661 -70
- 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 +172 -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-7GXI42FO.js → session-recorder-H3KEYU26.js} +1 -1
- package/dist/{session-recorder-RFOPX4SF.js → session-recorder-QRZMKFVL.js} +1 -1
- package/dist/{session-replayer-3HEIJBJ2.js → session-replayer-LJUC4TI7.js} +25 -8
- 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) {
|
|
@@ -8167,7 +8318,7 @@ function createRPCHandler() {
|
|
|
8167
8318
|
const sessionName = params.session || "default";
|
|
8168
8319
|
const slowMo = params.slowMo || 1;
|
|
8169
8320
|
if (!file) {
|
|
8170
|
-
return { ok: false,
|
|
8321
|
+
return normalizeReplayResult({ ok: false, errors: [{ eventIndex: -1, error: "Missing file parameter" }] });
|
|
8171
8322
|
}
|
|
8172
8323
|
let rawContent;
|
|
8173
8324
|
let parsed;
|
|
@@ -8180,55 +8331,54 @@ function createRPCHandler() {
|
|
|
8180
8331
|
parsed = yaml2.parse(rawContent);
|
|
8181
8332
|
}
|
|
8182
8333
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
8183
|
-
return { ok: false,
|
|
8334
|
+
return normalizeReplayResult({ ok: false, errors: [{ eventIndex: -1, error: `File "${file}" does not contain a valid recording. Expected a JSON/YAML object with an "actions" or "events" array, got ${parsed === null ? "empty content" : typeof parsed}.` }] });
|
|
8184
8335
|
}
|
|
8185
8336
|
} catch (e) {
|
|
8186
8337
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8187
|
-
return { ok: false,
|
|
8338
|
+
return normalizeReplayResult({ ok: false, errors: [{ eventIndex: -1, error: `Failed to parse "${file}" as JSON or YAML: ${msg.split("\n")[0]}` }] });
|
|
8188
8339
|
}
|
|
8189
8340
|
const session = findSession(sessionName);
|
|
8190
8341
|
if (!session) {
|
|
8191
|
-
return { ok: false,
|
|
8342
|
+
return normalizeReplayResult({ ok: false, errors: [{ eventIndex: -1, error: `Session not found: ${sessionName}. Open a browser first: xbrowser goto <url> --session ${sessionName}` }] });
|
|
8192
8343
|
}
|
|
8193
8344
|
if (!session.page) {
|
|
8194
|
-
return { ok: false,
|
|
8345
|
+
return normalizeReplayResult({ ok: false, errors: [{ eventIndex: -1, error: "Session has no page: " + sessionName }] });
|
|
8195
8346
|
}
|
|
8196
8347
|
const isNewFormat = Array.isArray(parsed.actions);
|
|
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,
|
|
8204
8355
|
onStep: (action, index, total) => {
|
|
8205
8356
|
console.log(`[replay] Step ${index + 1}/${total}: ${action.type} ${action.element?.selector || action.url || ""}`);
|
|
8206
8357
|
},
|
|
8207
|
-
onError: (action, error) => {
|
|
8358
|
+
onError: (action, error, index) => {
|
|
8208
8359
|
const msg = `[${action?.type || "unknown"}] ${error?.message || String(error)}`;
|
|
8209
8360
|
console.error("[replay] Error at step:", msg);
|
|
8210
|
-
replayErrors.push({ eventIndex:
|
|
8361
|
+
replayErrors.push({ eventIndex: index, error: msg });
|
|
8211
8362
|
}
|
|
8212
8363
|
});
|
|
8213
8364
|
if (!Array.isArray(parsed.actions) || typeof parsed.startUrl !== "string") {
|
|
8214
|
-
return { ok: false,
|
|
8365
|
+
return normalizeReplayResult({ ok: false, errors: [{ eventIndex: -1, error: "Invalid recording format: missing actions or startUrl" }] });
|
|
8215
8366
|
}
|
|
8216
8367
|
await replayer.load(parsed);
|
|
8217
8368
|
const startTime = Date.now();
|
|
8218
8369
|
const result2 = await replayer.run();
|
|
8219
8370
|
const duration = Date.now() - startTime;
|
|
8220
|
-
return {
|
|
8371
|
+
return normalizeReplayResult({
|
|
8221
8372
|
ok: result2.failed === 0,
|
|
8222
|
-
success: result2.failed === 0,
|
|
8223
8373
|
duration,
|
|
8224
8374
|
eventsPlayed: result2.success,
|
|
8225
8375
|
totalEvents: result2.success + result2.failed + result2.skipped,
|
|
8226
8376
|
errors: replayErrors
|
|
8227
|
-
};
|
|
8377
|
+
});
|
|
8228
8378
|
} catch (e) {
|
|
8229
8379
|
const msg = e instanceof Error ? e.message : String(e);
|
|
8230
8380
|
console.error("[replay] SessionReplayer error:", msg);
|
|
8231
|
-
return { ok: false,
|
|
8381
|
+
return normalizeReplayResult({ ok: false, errors: [{ eventIndex: -1, error: "Replay failed: " + msg }] });
|
|
8232
8382
|
}
|
|
8233
8383
|
}
|
|
8234
8384
|
const engine = PlaybackEngine.fromFile(session.page, file);
|
|
@@ -8242,7 +8392,21 @@ function createRPCHandler() {
|
|
|
8242
8392
|
});
|
|
8243
8393
|
}
|
|
8244
8394
|
});
|
|
8245
|
-
return { ok: result.success, ...result };
|
|
8395
|
+
return normalizeReplayResult({ ok: result.success, ...result });
|
|
8396
|
+
}
|
|
8397
|
+
function normalizeReplayResult(input) {
|
|
8398
|
+
const errors = Array.isArray(input.errors) ? input.errors : [];
|
|
8399
|
+
const eventsPlayed = typeof input.eventsPlayed === "number" ? input.eventsPlayed : 0;
|
|
8400
|
+
const totalEvents = typeof input.totalEvents === "number" ? input.totalEvents : eventsPlayed + errors.length;
|
|
8401
|
+
const ok26 = input.ok ?? errors.length === 0;
|
|
8402
|
+
return {
|
|
8403
|
+
ok: ok26,
|
|
8404
|
+
success: ok26,
|
|
8405
|
+
duration: typeof input.duration === "number" ? input.duration : 0,
|
|
8406
|
+
eventsPlayed,
|
|
8407
|
+
totalEvents,
|
|
8408
|
+
errors
|
|
8409
|
+
};
|
|
8246
8410
|
}
|
|
8247
8411
|
async function handleViewerCheckSelector(params) {
|
|
8248
8412
|
const name = params.name || "default";
|
|
@@ -8268,10 +8432,37 @@ import { EventEmitter as EventEmitter2 } from "events";
|
|
|
8268
8432
|
// src/stream/frame-processor.ts
|
|
8269
8433
|
import sharp from "sharp";
|
|
8270
8434
|
var STATE_CONFIGS = {
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
|
|
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 }
|
|
8274
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
|
+
}
|
|
8275
8466
|
var StreamStateManager = class {
|
|
8276
8467
|
currentState = "static";
|
|
8277
8468
|
isUserInteracting = false;
|
|
@@ -8280,9 +8471,11 @@ var StreamStateManager = class {
|
|
|
8280
8471
|
frameInterval = Infinity;
|
|
8281
8472
|
onStateChange = null;
|
|
8282
8473
|
staticTimer = null;
|
|
8283
|
-
USER_INTERACTION_TIMEOUT_MS =
|
|
8474
|
+
USER_INTERACTION_TIMEOUT_MS = 2e3;
|
|
8475
|
+
// 1s → 2s: linger in interacting longer
|
|
8284
8476
|
SCREEN_MOVING_THRESHOLD_MS = 1e3;
|
|
8285
|
-
STATIC_TIMEOUT_MS =
|
|
8477
|
+
STATIC_TIMEOUT_MS = 3e3;
|
|
8478
|
+
// 1.5s → 3s: don't rush to static
|
|
8286
8479
|
setStateChangeCallback(callback) {
|
|
8287
8480
|
this.onStateChange = callback;
|
|
8288
8481
|
}
|
|
@@ -8391,12 +8584,25 @@ var FrameProcessor = class {
|
|
|
8391
8584
|
}
|
|
8392
8585
|
let processed = sharp(buffer);
|
|
8393
8586
|
if (cropConfig) {
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
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
|
+
}
|
|
8400
8606
|
}
|
|
8401
8607
|
if (needsResize) {
|
|
8402
8608
|
const newWidth = Math.round((viewportWidth ?? 0) * config.scale);
|
|
@@ -8545,11 +8751,20 @@ var StreamCoordinator = class {
|
|
|
8545
8751
|
const crop = this.sessionCrops.get(sessionId);
|
|
8546
8752
|
const cropConfig = crop ? crop.box : void 0;
|
|
8547
8753
|
const effectiveViewport = crop ? { width: crop.box.width, height: crop.box.height } : frameViewport;
|
|
8548
|
-
const
|
|
8549
|
-
frameData,
|
|
8754
|
+
const adjustedConfig = adjustConfigForSize(
|
|
8550
8755
|
config,
|
|
8551
8756
|
effectiveViewport.width,
|
|
8552
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,
|
|
8553
8768
|
cropConfig
|
|
8554
8769
|
);
|
|
8555
8770
|
const header = Buffer.from(JSON.stringify({
|
|
@@ -8721,7 +8936,18 @@ var ElementMonitor = class {
|
|
|
8721
8936
|
}
|
|
8722
8937
|
return results;
|
|
8723
8938
|
});
|
|
8724
|
-
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) => ({
|
|
8725
8951
|
id: "el-" + i + "-" + (e.id || e.tag),
|
|
8726
8952
|
label: e.id || e.cls || e.tag,
|
|
8727
8953
|
rect: e.rect
|
|
@@ -8780,10 +9006,10 @@ var SessionManager = class extends EventEmitter {
|
|
|
8780
9006
|
this.screencasts.set(sessionId, {
|
|
8781
9007
|
capturer: new ScreencastCapturer({
|
|
8782
9008
|
interval: options?.interval ?? 100,
|
|
8783
|
-
quality: options?.quality ??
|
|
9009
|
+
quality: options?.quality ?? 40,
|
|
8784
9010
|
type: options?.type ?? "jpeg",
|
|
8785
|
-
width: options?.width ??
|
|
8786
|
-
height: options?.height ??
|
|
9011
|
+
width: options?.width ?? 1024,
|
|
9012
|
+
height: options?.height ?? 576
|
|
8787
9013
|
}),
|
|
8788
9014
|
page,
|
|
8789
9015
|
clientCount: 0
|
|
@@ -8924,6 +9150,8 @@ var SessionManager = class extends EventEmitter {
|
|
|
8924
9150
|
this.resetStaticSnapshotTimer(sessionId);
|
|
8925
9151
|
});
|
|
8926
9152
|
await sc.capturer.startCapture(sc.page, sessionId, onFrame);
|
|
9153
|
+
this.streamCoordinator.takeStaticSnapshot(sessionId, sc.page, sc.clientCount).catch(() => {
|
|
9154
|
+
});
|
|
8927
9155
|
}
|
|
8928
9156
|
async stopCapturer(sessionId) {
|
|
8929
9157
|
const sc = this.screencasts.get(sessionId);
|
|
@@ -9374,6 +9602,15 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9374
9602
|
registerSession(sessionId, page, options) {
|
|
9375
9603
|
this.sessionManager.registerSession(sessionId, page, options);
|
|
9376
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
|
+
}
|
|
9377
9614
|
unregisterSession(sessionId) {
|
|
9378
9615
|
this.sessionManager.unregisterSession(sessionId);
|
|
9379
9616
|
}
|
|
@@ -9396,7 +9633,7 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9396
9633
|
});
|
|
9397
9634
|
}
|
|
9398
9635
|
this.sessionManager.incrementClientCount(sessionId);
|
|
9399
|
-
if (this.streamCoordinator.getLastFrameViewport()
|
|
9636
|
+
if (this.streamCoordinator.getLastFrameViewport()) {
|
|
9400
9637
|
this.streamCoordinator.replayLastFrame(sessionId).catch(() => {
|
|
9401
9638
|
});
|
|
9402
9639
|
}
|
|
@@ -9406,7 +9643,6 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9406
9643
|
if (count === 0 && this.sessionManager.isCapturerActive(sessionId)) {
|
|
9407
9644
|
this.sessionManager.stopCapturer(sessionId).then(() => {
|
|
9408
9645
|
this.streamCoordinator.resetFrameRate();
|
|
9409
|
-
this.streamCoordinator.clearLastFrame();
|
|
9410
9646
|
this.emit("screencast-stopped", sessionId);
|
|
9411
9647
|
}).catch(() => {
|
|
9412
9648
|
this.emit("screencast-stopped", sessionId);
|
|
@@ -9582,6 +9818,31 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9582
9818
|
this.emit("human-solved", { sessionId: sessionId ?? null, clientId });
|
|
9583
9819
|
return;
|
|
9584
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
|
+
}
|
|
9585
9846
|
const ctx = {
|
|
9586
9847
|
clientId,
|
|
9587
9848
|
sessionId,
|
|
@@ -9668,7 +9929,7 @@ function alignHTML(sessionId, _host) {
|
|
|
9668
9929
|
html,body{height:100%;overflow:hidden;background:#000}
|
|
9669
9930
|
.viewport{position:absolute;top:0;left:0;right:0;bottom:0;overflow:hidden}
|
|
9670
9931
|
.viewport img#screen{display:block;width:100%;height:100%;object-fit:contain;background:#111}
|
|
9671
|
-
.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)}
|
|
9672
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}
|
|
9673
9934
|
.grid{position:fixed;pointer-events:none;z-index:9998;top:0;left:0;right:0;bottom:0;display:none}
|
|
9674
9935
|
</style></head><body>
|
|
@@ -9760,9 +10021,17 @@ viewportEl.addEventListener('mousemove',function(e){
|
|
|
9760
10021
|
var r=viewerToRemote(e.clientX,e.clientY);
|
|
9761
10022
|
var rx=Math.max(0,Math.min(remoteViewport.width,r.x));
|
|
9762
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
|
|
9763
10025
|
setCrosshair(rx,ry);
|
|
9764
|
-
|
|
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
|
+
}
|
|
9765
10033
|
});
|
|
10034
|
+
var moveThrottled=false;
|
|
9766
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}));});
|
|
9767
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}));});
|
|
9768
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});
|
|
@@ -9790,7 +10059,7 @@ body{display:flex;flex-direction:column;touch-action:manipulation}
|
|
|
9790
10059
|
.bar .dot.ok{background:#2ecc71}
|
|
9791
10060
|
.bar .url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8899aa;font-size:12px}
|
|
9792
10061
|
.bar .conn{color:#556;font-size:11px;flex-shrink:0}
|
|
9793
|
-
.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}
|
|
9794
10063
|
.viewport canvas#screen{display:none;user-select:none;-webkit-user-drag:none}
|
|
9795
10064
|
.waiting{color:#556;font-size:14px;text-align:center}
|
|
9796
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}
|
|
@@ -9853,6 +10122,33 @@ body{display:flex;flex-direction:column;touch-action:manipulation}
|
|
|
9853
10122
|
<span class="conn" id="conn"></span>
|
|
9854
10123
|
<button class="mode-btn" id="mode-btn" title="Switch mode (Mobile/Tablet/PC)">\u{1F4F1}</button>
|
|
9855
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>
|
|
9856
10152
|
</div>
|
|
9857
10153
|
<div class="view-tabs" id="view-tabs"></div>
|
|
9858
10154
|
<div class="viewport" id="viewport">
|
|
@@ -9921,6 +10217,14 @@ let currentFocusedSelector='';
|
|
|
9921
10217
|
let currentFocusedValue='';
|
|
9922
10218
|
let suppressFocus=false;
|
|
9923
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
|
+
|
|
9924
10228
|
let lastHoverSent=0;
|
|
9925
10229
|
|
|
9926
10230
|
const $=id=>document.getElementById(id);
|
|
@@ -9929,13 +10233,136 @@ const viewportEl=$('viewport'),cursorEl=$('cursor'),cursorLabelEl=$('cursor-labe
|
|
|
9929
10233
|
const touchpadEl=$('touchpad'),toolbarEl=$('toolbar');
|
|
9930
10234
|
const inputPanel=$('input-panel'),inputField=$('input-field'),inputLabel=$('input-label');
|
|
9931
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
|
+
|
|
9932
10354
|
function resizeCanvas(){
|
|
9933
10355
|
const box=viewportEl.getBoundingClientRect();
|
|
9934
|
-
const bw=box.width;
|
|
9935
|
-
if(!bw||!remoteViewport.width||!remoteViewport.height) return;
|
|
10356
|
+
const bw=box.width,bh=box.height;
|
|
10357
|
+
if(!bw||!bh||!remoteViewport.width||!remoteViewport.height) return;
|
|
9936
10358
|
const vpAspect=remoteViewport.width/remoteViewport.height;
|
|
9937
|
-
|
|
9938
|
-
|
|
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;
|
|
9939
10366
|
if(canvas.width!==Math.round(cw)||canvas.height!==Math.round(ch)){
|
|
9940
10367
|
canvas.width=Math.round(cw);
|
|
9941
10368
|
canvas.height=Math.round(ch);
|
|
@@ -9968,6 +10395,10 @@ function connectWS(){
|
|
|
9968
10395
|
connEl.textContent='WS';
|
|
9969
10396
|
if(deviceMode==='desktop') createPCKeyboard();
|
|
9970
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();
|
|
9971
10402
|
};
|
|
9972
10403
|
ws.binaryType='arraybuffer';
|
|
9973
10404
|
ws.onmessage=(e)=>{
|
|
@@ -9978,13 +10409,31 @@ function connectWS(){
|
|
|
9978
10409
|
updateViewTabs(_m.views);
|
|
9979
10410
|
return;
|
|
9980
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
|
+
}
|
|
9981
10422
|
}
|
|
9982
10423
|
if(e.data instanceof ArrayBuffer){
|
|
9983
10424
|
const buf=new Uint8Array(e.data);
|
|
10425
|
+
statBytesRcvd+=buf.length;
|
|
9984
10426
|
const headerLen=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
|
|
9985
10427
|
const header=JSON.parse(new TextDecoder().decode(buf.slice(4,4+headerLen)));
|
|
9986
10428
|
const jpegData=buf.slice(4+headerLen);
|
|
9987
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
|
+
}
|
|
9988
10437
|
const blob=new Blob([jpegData],{type:'image/jpeg'});
|
|
9989
10438
|
createImageBitmap(blob).then(function(bmp){
|
|
9990
10439
|
drawFrame(bmp);
|
|
@@ -10126,6 +10575,8 @@ function updateViewTabs(views){
|
|
|
10126
10575
|
selectView('main');
|
|
10127
10576
|
}
|
|
10128
10577
|
renderViewTabs();
|
|
10578
|
+
// Auto-select view matching ?selector= param
|
|
10579
|
+
if(pendingSelector) trySelectBySelector();
|
|
10129
10580
|
}
|
|
10130
10581
|
function renderViewTabs(){
|
|
10131
10582
|
if(detectedViews.length===0){
|
|
@@ -10214,7 +10665,12 @@ function selectView(vid){
|
|
|
10214
10665
|
}
|
|
10215
10666
|
|
|
10216
10667
|
function sendMsg(obj){
|
|
10217
|
-
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
|
+
}
|
|
10218
10674
|
}
|
|
10219
10675
|
|
|
10220
10676
|
function parseFocusHash(){
|
|
@@ -10224,6 +10680,107 @@ function parseFocusHash(){
|
|
|
10224
10680
|
return m?decodeURIComponent(m[1]):null;
|
|
10225
10681
|
}
|
|
10226
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
|
+
|
|
10227
10784
|
function checkFocus(){
|
|
10228
10785
|
var sel=parseFocusHash();
|
|
10229
10786
|
if(sel&&ws&&ws.readyState===WebSocket.OPEN){
|
|
@@ -10323,10 +10880,16 @@ function createPCKeyboard(){
|
|
|
10323
10880
|
document.addEventListener('keydown',function(e){
|
|
10324
10881
|
if(deviceMode!=='desktop') return;
|
|
10325
10882
|
if(e.isComposing||_pcComposing) return;
|
|
10326
|
-
var tag=e.target&&e.target.tagName;
|
|
10327
|
-
if(tag==='INPUT'||tag==='TEXTAREA') return;
|
|
10328
10883
|
var key=e.key;
|
|
10329
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
|
+
|
|
10330
10893
|
e.preventDefault();
|
|
10331
10894
|
sendMsg({type:'input_keyboard',action:'down',key:key});
|
|
10332
10895
|
if(key.length===1){
|
|
@@ -10337,10 +10900,13 @@ function createPCKeyboard(){
|
|
|
10337
10900
|
document.addEventListener('keyup',function(e){
|
|
10338
10901
|
if(deviceMode!=='desktop') return;
|
|
10339
10902
|
if(e.isComposing||_pcComposing) return;
|
|
10340
|
-
var tag=e.target&&e.target.tagName;
|
|
10341
|
-
if(tag==='INPUT'||tag==='TEXTAREA') return;
|
|
10342
10903
|
var key=e.key;
|
|
10343
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
|
+
|
|
10344
10910
|
e.preventDefault();
|
|
10345
10911
|
sendMsg({type:'input_keyboard',action:'up',key:key});
|
|
10346
10912
|
sendUserActivity();
|
|
@@ -10393,9 +10959,20 @@ viewportEl.addEventListener('mouseleave',()=>{
|
|
|
10393
10959
|
viewportEl.addEventListener('wheel',(e)=>{
|
|
10394
10960
|
if(deviceMode!=='desktop') return;
|
|
10395
10961
|
e.preventDefault();
|
|
10396
|
-
|
|
10397
|
-
|
|
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
|
+
}
|
|
10398
10974
|
},{passive:false});
|
|
10975
|
+
var scrollThrottled=false,pendingScrollX=0,pendingScrollY=0;
|
|
10399
10976
|
|
|
10400
10977
|
// --- Mobile Touchpad (incremental, smooth) ---
|
|
10401
10978
|
let tpStartPos=null;
|
|
@@ -10892,6 +11469,13 @@ async function main() {
|
|
|
10892
11469
|
rpcHandler.setPreviewWS(previewWS);
|
|
10893
11470
|
previewWS.on("screencast-started", (sid) => log(`Preview screencast started: ${sid}`));
|
|
10894
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
|
+
});
|
|
10895
11479
|
mkdirSync5(CONFIG_DIR3, { recursive: true });
|
|
10896
11480
|
writeFileSync5(join7(CONFIG_DIR3, "daemon.json"), JSON.stringify({
|
|
10897
11481
|
port: daemonPort,
|
|
@@ -10900,6 +11484,13 @@ async function main() {
|
|
|
10900
11484
|
}, null, 2));
|
|
10901
11485
|
console.log(`xbrowser daemon started (pid: ${process.pid}, port: ${daemonPort})`);
|
|
10902
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
|
+
}
|
|
10903
11494
|
const shutdown = () => {
|
|
10904
11495
|
log("Received shutdown signal, stopping");
|
|
10905
11496
|
previewWS.stop().catch(() => {
|