@xbrowser/cli 1.9.8 → 1.10.0
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-SJWPJFN7.js → browser-HBL72GPZ.js} +2 -2
- package/dist/{browser-GT4BMX6X.js → browser-T3V3JWVH.js} +1 -1
- package/dist/{browser-43YXUIML.js → browser-WQZ3D6AE.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-BAHSRZIX.js} +82 -29
- 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-DPJNCMLC.js → chunk-INTQPBYF.js} +7 -0
- package/dist/{human-interaction-OU7LZ6OZ.js → chunk-MWFHZUIY.js} +6 -7
- package/dist/{chunk-A4YHJW3Z.js → chunk-NITFVWWS.js} +168 -29
- package/dist/chunk-Q2DFJQGS.js +128 -0
- package/dist/{chunk-GM3HWQ5U.js → chunk-RRBXV7KE.js} +87 -1
- package/dist/{human-interaction-HJTXFVQS.js → chunk-SQHSZENE.js} +2 -2
- package/dist/chunk-TZPKFUBT.js +20 -0
- package/dist/{chunk-PBDID7SE.js → chunk-WSCP7QCJ.js} +82 -29
- package/dist/{chunk-YAUG3NSI.js → chunk-YMUSHPU4.js} +16 -128
- package/dist/cli.js +48 -31
- 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 +649 -58
- 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 +194 -67
- 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-BAHSRZIX.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-WQZ3D6AE.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,
|
|
@@ -8279,12 +8430,51 @@ function createRPCHandler() {
|
|
|
8279
8430
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
8280
8431
|
|
|
8281
8432
|
// src/stream/frame-processor.ts
|
|
8282
|
-
|
|
8433
|
+
var _sharp = null;
|
|
8434
|
+
async function loadSharp() {
|
|
8435
|
+
if (!_sharp) {
|
|
8436
|
+
try {
|
|
8437
|
+
_sharp = (await import("sharp")).default;
|
|
8438
|
+
} catch (err) {
|
|
8439
|
+
throw new Error(
|
|
8440
|
+
"\u76F4\u64AD\u6D41\u5E27\u5904\u7406\u9700\u8981 sharp \u539F\u751F\u6A21\u5757\uFF0C\u5F53\u524D\u73AF\u5883\u52A0\u8F7D\u5931\u8D25\uFF1A" + (err instanceof Error ? err.message : String(err)) + "\u3002\u4E0D\u7528\u76F4\u64AD\u6D41\u529F\u80FD\u53EF\u5FFD\u7565\uFF1B\u9700\u8981\u76F4\u64AD\u8BF7\u5728\u517C\u5BB9\u73AF\u5883\u8FD0\u884C\u6216\u5B89\u88C5\u5BF9\u5E94\u5E73\u53F0\u4F9D\u8D56\u3002"
|
|
8441
|
+
);
|
|
8442
|
+
}
|
|
8443
|
+
}
|
|
8444
|
+
return _sharp;
|
|
8445
|
+
}
|
|
8283
8446
|
var STATE_CONFIGS = {
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8447
|
+
// Full-screen: aggressive compression for speed
|
|
8448
|
+
// user_interacting: low quality + downscale — prioritize FPS over clarity
|
|
8449
|
+
user_interacting: { format: "jpeg", quality: 40, maxFps: 30, scale: 0.6 },
|
|
8450
|
+
// screen_moving: page is animating but user is NOT operating.
|
|
8451
|
+
// Keep quality close to interacting to avoid visible flicker on state change.
|
|
8452
|
+
screen_moving: { format: "jpeg", quality: 45, maxFps: 2, scale: 0.7 },
|
|
8453
|
+
// static: highest quality — user is reading, bandwidth is not a concern
|
|
8454
|
+
static: { format: "jpeg", quality: 95, maxFps: 1, scale: 1 }
|
|
8287
8455
|
};
|
|
8456
|
+
function adjustConfigForSize(config, frameWidth, frameHeight, fullViewportWidth, fullViewportHeight) {
|
|
8457
|
+
const framePixels = frameWidth * frameHeight;
|
|
8458
|
+
const fullPixels = fullViewportWidth * fullViewportHeight;
|
|
8459
|
+
const ratio = fullPixels > 0 ? framePixels / fullPixels : 1;
|
|
8460
|
+
if (ratio < 0.3) {
|
|
8461
|
+
return {
|
|
8462
|
+
...config,
|
|
8463
|
+
quality: Math.min(config.quality + 40, 90),
|
|
8464
|
+
// boost quality
|
|
8465
|
+
scale: Math.max(config.scale, 0.9)
|
|
8466
|
+
// don't downscale
|
|
8467
|
+
};
|
|
8468
|
+
}
|
|
8469
|
+
if (ratio < 0.6) {
|
|
8470
|
+
return {
|
|
8471
|
+
...config,
|
|
8472
|
+
quality: Math.min(config.quality + 20, 80),
|
|
8473
|
+
scale: Math.max(config.scale, 0.7)
|
|
8474
|
+
};
|
|
8475
|
+
}
|
|
8476
|
+
return config;
|
|
8477
|
+
}
|
|
8288
8478
|
var StreamStateManager = class {
|
|
8289
8479
|
currentState = "static";
|
|
8290
8480
|
isUserInteracting = false;
|
|
@@ -8293,9 +8483,11 @@ var StreamStateManager = class {
|
|
|
8293
8483
|
frameInterval = Infinity;
|
|
8294
8484
|
onStateChange = null;
|
|
8295
8485
|
staticTimer = null;
|
|
8296
|
-
USER_INTERACTION_TIMEOUT_MS =
|
|
8486
|
+
USER_INTERACTION_TIMEOUT_MS = 2e3;
|
|
8487
|
+
// 1s → 2s: linger in interacting longer
|
|
8297
8488
|
SCREEN_MOVING_THRESHOLD_MS = 1e3;
|
|
8298
|
-
STATIC_TIMEOUT_MS =
|
|
8489
|
+
STATIC_TIMEOUT_MS = 3e3;
|
|
8490
|
+
// 1.5s → 3s: don't rush to static
|
|
8299
8491
|
setStateChangeCallback(callback) {
|
|
8300
8492
|
this.onStateChange = callback;
|
|
8301
8493
|
}
|
|
@@ -8402,14 +8594,28 @@ var FrameProcessor = class {
|
|
|
8402
8594
|
if (!needsResize && !needsCrop && !needsReencode) {
|
|
8403
8595
|
return buffer;
|
|
8404
8596
|
}
|
|
8597
|
+
const sharp = await loadSharp();
|
|
8405
8598
|
let processed = sharp(buffer);
|
|
8406
8599
|
if (cropConfig) {
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8600
|
+
const meta = await processed.metadata();
|
|
8601
|
+
const actualW = meta.width ?? viewportWidth ?? 1;
|
|
8602
|
+
const actualH = meta.height ?? viewportHeight ?? 1;
|
|
8603
|
+
const refW = viewportWidth ?? actualW;
|
|
8604
|
+
const refH = viewportHeight ?? actualH;
|
|
8605
|
+
const scaleX = actualW / refW;
|
|
8606
|
+
const scaleY = actualH / refH;
|
|
8607
|
+
const cropLeft = Math.round(cropConfig.x * scaleX);
|
|
8608
|
+
const cropTop = Math.round(cropConfig.y * scaleY);
|
|
8609
|
+
const cropW = Math.min(Math.round(cropConfig.width * scaleX), actualW - cropLeft);
|
|
8610
|
+
const cropH = Math.min(Math.round(cropConfig.height * scaleY), actualH - cropTop);
|
|
8611
|
+
if (cropW > 0 && cropH > 0 && cropLeft >= 0 && cropTop >= 0) {
|
|
8612
|
+
processed = processed.extract({
|
|
8613
|
+
left: cropLeft,
|
|
8614
|
+
top: cropTop,
|
|
8615
|
+
width: cropW,
|
|
8616
|
+
height: cropH
|
|
8617
|
+
});
|
|
8618
|
+
}
|
|
8413
8619
|
}
|
|
8414
8620
|
if (needsResize) {
|
|
8415
8621
|
const newWidth = Math.round((viewportWidth ?? 0) * config.scale);
|
|
@@ -8558,11 +8764,20 @@ var StreamCoordinator = class {
|
|
|
8558
8764
|
const crop = this.sessionCrops.get(sessionId);
|
|
8559
8765
|
const cropConfig = crop ? crop.box : void 0;
|
|
8560
8766
|
const effectiveViewport = crop ? { width: crop.box.width, height: crop.box.height } : frameViewport;
|
|
8561
|
-
const
|
|
8562
|
-
frameData,
|
|
8767
|
+
const adjustedConfig = adjustConfigForSize(
|
|
8563
8768
|
config,
|
|
8564
8769
|
effectiveViewport.width,
|
|
8565
8770
|
effectiveViewport.height,
|
|
8771
|
+
frameViewport.width,
|
|
8772
|
+
frameViewport.height
|
|
8773
|
+
);
|
|
8774
|
+
const processedBuffer = await this.frameProcessor.process(
|
|
8775
|
+
frameData,
|
|
8776
|
+
adjustedConfig,
|
|
8777
|
+
// Pass the ORIGINAL viewport (not cropped) so crop coordinates
|
|
8778
|
+
// can be scaled correctly from viewport space to frame pixel space.
|
|
8779
|
+
crop ? frameViewport.width : effectiveViewport.width,
|
|
8780
|
+
crop ? frameViewport.height : effectiveViewport.height,
|
|
8566
8781
|
cropConfig
|
|
8567
8782
|
);
|
|
8568
8783
|
const header = Buffer.from(JSON.stringify({
|
|
@@ -8734,7 +8949,18 @@ var ElementMonitor = class {
|
|
|
8734
8949
|
}
|
|
8735
8950
|
return results;
|
|
8736
8951
|
});
|
|
8737
|
-
const
|
|
8952
|
+
const TOLERANCE_PX = 10;
|
|
8953
|
+
const deduped = [];
|
|
8954
|
+
const isSameSize = (a, b) => Math.abs(a.width - b.width) <= TOLERANCE_PX && Math.abs(a.height - b.height) <= TOLERANCE_PX;
|
|
8955
|
+
const sorted = [...elements].sort((a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height);
|
|
8956
|
+
for (const el of sorted) {
|
|
8957
|
+
const dupe = deduped.some(
|
|
8958
|
+
(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
|
|
8959
|
+
);
|
|
8960
|
+
if (!dupe) deduped.push(el);
|
|
8961
|
+
}
|
|
8962
|
+
deduped.sort((a, b) => b.rect.width * b.rect.height - a.rect.width * a.rect.height);
|
|
8963
|
+
const views = deduped.map((e, i) => ({
|
|
8738
8964
|
id: "el-" + i + "-" + (e.id || e.tag),
|
|
8739
8965
|
label: e.id || e.cls || e.tag,
|
|
8740
8966
|
rect: e.rect
|
|
@@ -8793,10 +9019,10 @@ var SessionManager = class extends EventEmitter {
|
|
|
8793
9019
|
this.screencasts.set(sessionId, {
|
|
8794
9020
|
capturer: new ScreencastCapturer({
|
|
8795
9021
|
interval: options?.interval ?? 100,
|
|
8796
|
-
quality: options?.quality ??
|
|
9022
|
+
quality: options?.quality ?? 40,
|
|
8797
9023
|
type: options?.type ?? "jpeg",
|
|
8798
|
-
width: options?.width ??
|
|
8799
|
-
height: options?.height ??
|
|
9024
|
+
width: options?.width ?? 1024,
|
|
9025
|
+
height: options?.height ?? 576
|
|
8800
9026
|
}),
|
|
8801
9027
|
page,
|
|
8802
9028
|
clientCount: 0
|
|
@@ -8937,6 +9163,8 @@ var SessionManager = class extends EventEmitter {
|
|
|
8937
9163
|
this.resetStaticSnapshotTimer(sessionId);
|
|
8938
9164
|
});
|
|
8939
9165
|
await sc.capturer.startCapture(sc.page, sessionId, onFrame);
|
|
9166
|
+
this.streamCoordinator.takeStaticSnapshot(sessionId, sc.page, sc.clientCount).catch(() => {
|
|
9167
|
+
});
|
|
8940
9168
|
}
|
|
8941
9169
|
async stopCapturer(sessionId) {
|
|
8942
9170
|
const sc = this.screencasts.get(sessionId);
|
|
@@ -9387,6 +9615,15 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9387
9615
|
registerSession(sessionId, page, options) {
|
|
9388
9616
|
this.sessionManager.registerSession(sessionId, page, options);
|
|
9389
9617
|
}
|
|
9618
|
+
/** Restart screencast for a session — used by viewer reconnect button. */
|
|
9619
|
+
async reconnectSession(sessionId) {
|
|
9620
|
+
try {
|
|
9621
|
+
await this.sessionManager.stopCapturer(sessionId);
|
|
9622
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
9623
|
+
await this.sessionManager.startCapturer(sessionId);
|
|
9624
|
+
} catch {
|
|
9625
|
+
}
|
|
9626
|
+
}
|
|
9390
9627
|
unregisterSession(sessionId) {
|
|
9391
9628
|
this.sessionManager.unregisterSession(sessionId);
|
|
9392
9629
|
}
|
|
@@ -9409,7 +9646,7 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9409
9646
|
});
|
|
9410
9647
|
}
|
|
9411
9648
|
this.sessionManager.incrementClientCount(sessionId);
|
|
9412
|
-
if (this.streamCoordinator.getLastFrameViewport()
|
|
9649
|
+
if (this.streamCoordinator.getLastFrameViewport()) {
|
|
9413
9650
|
this.streamCoordinator.replayLastFrame(sessionId).catch(() => {
|
|
9414
9651
|
});
|
|
9415
9652
|
}
|
|
@@ -9419,7 +9656,6 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9419
9656
|
if (count === 0 && this.sessionManager.isCapturerActive(sessionId)) {
|
|
9420
9657
|
this.sessionManager.stopCapturer(sessionId).then(() => {
|
|
9421
9658
|
this.streamCoordinator.resetFrameRate();
|
|
9422
|
-
this.streamCoordinator.clearLastFrame();
|
|
9423
9659
|
this.emit("screencast-stopped", sessionId);
|
|
9424
9660
|
}).catch(() => {
|
|
9425
9661
|
this.emit("screencast-stopped", sessionId);
|
|
@@ -9595,6 +9831,31 @@ var WSServer = class extends EventEmitter2 {
|
|
|
9595
9831
|
this.emit("human-solved", { sessionId: sessionId ?? null, clientId });
|
|
9596
9832
|
return;
|
|
9597
9833
|
}
|
|
9834
|
+
if (msg.type === "reconnect") {
|
|
9835
|
+
this.emit("reconnect-request", { sessionId: sessionId ?? null, clientId });
|
|
9836
|
+
return;
|
|
9837
|
+
}
|
|
9838
|
+
if (msg.type === "health_ping") {
|
|
9839
|
+
this.sendToClient(clientId, { type: "health_pong", ts: msg.ts });
|
|
9840
|
+
return;
|
|
9841
|
+
}
|
|
9842
|
+
if (msg.type === "snapshot_request") {
|
|
9843
|
+
const page2 = sessionId ? this.sessionManager.getPageForSession(sessionId) : void 0;
|
|
9844
|
+
if (!page2) {
|
|
9845
|
+
this.sendToClient(clientId, { type: "snapshot_result", data: null, error: "no page" });
|
|
9846
|
+
return;
|
|
9847
|
+
}
|
|
9848
|
+
const fmt = msg.format === "webp" ? "webp" : "png";
|
|
9849
|
+
page2.screenshot({ type: fmt, quality: fmt === "webp" ? msg.quality ?? 90 : void 0 }).then((buf) => {
|
|
9850
|
+
this.sendToClient(clientId, {
|
|
9851
|
+
type: "snapshot_result",
|
|
9852
|
+
data: { data: buf.toString("base64"), format: fmt }
|
|
9853
|
+
});
|
|
9854
|
+
}).catch(() => {
|
|
9855
|
+
this.sendToClient(clientId, { type: "snapshot_result", data: null, error: "screenshot failed" });
|
|
9856
|
+
});
|
|
9857
|
+
return;
|
|
9858
|
+
}
|
|
9598
9859
|
const ctx = {
|
|
9599
9860
|
clientId,
|
|
9600
9861
|
sessionId,
|
|
@@ -9681,7 +9942,7 @@ function alignHTML(sessionId, _host) {
|
|
|
9681
9942
|
html,body{height:100%;overflow:hidden;background:#000}
|
|
9682
9943
|
.viewport{position:absolute;top:0;left:0;right:0;bottom:0;overflow:hidden}
|
|
9683
9944
|
.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)
|
|
9945
|
+
.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
9946
|
.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
9947
|
.grid{position:fixed;pointer-events:none;z-index:9998;top:0;left:0;right:0;bottom:0;display:none}
|
|
9687
9948
|
</style></head><body>
|
|
@@ -9773,9 +10034,17 @@ viewportEl.addEventListener('mousemove',function(e){
|
|
|
9773
10034
|
var r=viewerToRemote(e.clientX,e.clientY);
|
|
9774
10035
|
var rx=Math.max(0,Math.min(remoteViewport.width,r.x));
|
|
9775
10036
|
var ry=Math.max(0,Math.min(remoteViewport.height,r.y));
|
|
10037
|
+
// \u672C\u5730\u5149\u6807\u7ACB\u5373\u66F4\u65B0\uFF08\u96F6\u5EF6\u8FDF\u53CD\u9988\uFF09
|
|
9776
10038
|
setCrosshair(rx,ry);
|
|
9777
|
-
|
|
10039
|
+
// \u8282\u6D41\uFF1A\u6700\u591A\u6BCF 30ms \u53D1\u4E00\u6B21 move\uFF0C\u907F\u514D WS \u6D88\u606F\u6D2A\u6CDB
|
|
10040
|
+
if(!moveThrottled&&ws&&ws.readyState===1){
|
|
10041
|
+
moveThrottled=true;
|
|
10042
|
+
var action=e.buttons>0?'move':'hover';
|
|
10043
|
+
ws.send(JSON.stringify({type:'input_mouse',action:action,x:rx,y:ry}));
|
|
10044
|
+
setTimeout(function(){moveThrottled=false;},30);
|
|
10045
|
+
}
|
|
9778
10046
|
});
|
|
10047
|
+
var moveThrottled=false;
|
|
9779
10048
|
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
10049
|
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
10050
|
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 +10072,7 @@ body{display:flex;flex-direction:column;touch-action:manipulation}
|
|
|
9803
10072
|
.bar .dot.ok{background:#2ecc71}
|
|
9804
10073
|
.bar .url{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#8899aa;font-size:12px}
|
|
9805
10074
|
.bar .conn{color:#556;font-size:11px;flex-shrink:0}
|
|
9806
|
-
.viewport{flex
|
|
10075
|
+
.viewport{flex:1 1 0;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#1a1a2e;position:relative;min-height:0}
|
|
9807
10076
|
.viewport canvas#screen{display:none;user-select:none;-webkit-user-drag:none}
|
|
9808
10077
|
.waiting{color:#556;font-size:14px;text-align:center}
|
|
9809
10078
|
.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 +10135,33 @@ body{display:flex;flex-direction:column;touch-action:manipulation}
|
|
|
9866
10135
|
<span class="conn" id="conn"></span>
|
|
9867
10136
|
<button class="mode-btn" id="mode-btn" title="Switch mode (Mobile/Tablet/PC)">\u{1F4F1}</button>
|
|
9868
10137
|
<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>
|
|
10138
|
+
<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>
|
|
10139
|
+
<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>
|
|
10140
|
+
<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>
|
|
10141
|
+
</div>
|
|
10142
|
+
<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">
|
|
10143
|
+
<img id="snapshot-img" style="display:block;width:100%;height:auto" />
|
|
10144
|
+
<div style="padding:4px 8px;font:10px/1.4 monospace;color:#8af;display:flex;justify-content:space-between;align-items:center">
|
|
10145
|
+
<span>\u{1F4F7} \u9AD8\u6E05\u622A\u56FE \xB7 \u70B9\u51FB\u653E\u5927</span>
|
|
10146
|
+
<span id="snapshot-countdown" style="color:#f39c12"></span>
|
|
10147
|
+
</div>
|
|
10148
|
+
<div id="snapshot-progress" style="height:2px;background:#0f3460;width:100%"></div>
|
|
10149
|
+
</div>
|
|
10150
|
+
<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">
|
|
10151
|
+
<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)" />
|
|
10152
|
+
</div>
|
|
10153
|
+
</div>
|
|
10154
|
+
<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)">
|
|
10155
|
+
<div style="font-weight:bold;color:#cef;margin-bottom:4px;border-bottom:1px solid #0f3460;padding-bottom:4px">\u{1F4CA} Live Monitor</div>
|
|
10156
|
+
<div>FPS: <span id="stat-fps" style="color:#2ecc71">0</span></div>
|
|
10157
|
+
<div>Frame size: <span id="stat-frame-size" style="color:#2ecc71">0KB</span></div>
|
|
10158
|
+
<div>Latency: <span id="stat-latency" style="color:#2ecc71">0ms</span></div>
|
|
10159
|
+
<div>Bandwidth: <span id="stat-bandwidth" style="color:#2ecc71">0KB/s</span></div>
|
|
10160
|
+
<div>Frames: <span id="stat-frames" style="color:#8af">0</span></div>
|
|
10161
|
+
<div>Sent: <span id="stat-sent" style="color:#8af">0</span></div>
|
|
10162
|
+
<div>WS: <span id="stat-ws" style="color:#e74c3c">disconnected</span></div>
|
|
10163
|
+
<div>Viewport: <span id="stat-viewport" style="color:#8af">0x0</span></div>
|
|
10164
|
+
<div>Uptime: <span id="stat-uptime" style="color:#8af">0s</span></div>
|
|
9869
10165
|
</div>
|
|
9870
10166
|
<div class="view-tabs" id="view-tabs"></div>
|
|
9871
10167
|
<div class="viewport" id="viewport">
|
|
@@ -9934,6 +10230,14 @@ let currentFocusedSelector='';
|
|
|
9934
10230
|
let currentFocusedValue='';
|
|
9935
10231
|
let suppressFocus=false;
|
|
9936
10232
|
let deviceMode='desktop';
|
|
10233
|
+
|
|
10234
|
+
// === Live Monitor stats ===
|
|
10235
|
+
const startTime=Date.now();
|
|
10236
|
+
let statFrames=0,statSent=0,statBytesRcvd=0,statBytesSent=0;
|
|
10237
|
+
let statLastFrameTime=0,statLatency=0,statFrameSize=0,statFps=0;
|
|
10238
|
+
let statFpsFrames=0,statFpsTimer=Date.now();
|
|
10239
|
+
let statEl=null;
|
|
10240
|
+
|
|
9937
10241
|
let lastHoverSent=0;
|
|
9938
10242
|
|
|
9939
10243
|
const $=id=>document.getElementById(id);
|
|
@@ -9942,13 +10246,136 @@ const viewportEl=$('viewport'),cursorEl=$('cursor'),cursorLabelEl=$('cursor-labe
|
|
|
9942
10246
|
const touchpadEl=$('touchpad'),toolbarEl=$('toolbar');
|
|
9943
10247
|
const inputPanel=$('input-panel'),inputField=$('input-field'),inputLabel=$('input-label');
|
|
9944
10248
|
|
|
10249
|
+
// Init monitor elements after $ is defined
|
|
10250
|
+
statEl={
|
|
10251
|
+
fps:$('stat-fps'),frameSize:$('stat-frame-size'),latency:$('stat-latency'),
|
|
10252
|
+
bandwidth:$('stat-bandwidth'),frames:$('stat-frames'),sent:$('stat-sent'),
|
|
10253
|
+
ws:$('stat-ws'),viewport:$('stat-viewport'),uptime:$('stat-uptime'),
|
|
10254
|
+
};
|
|
10255
|
+
$('stats-btn').addEventListener('click',()=>{
|
|
10256
|
+
const panel=$('stats-panel');
|
|
10257
|
+
panel.style.display=panel.style.display==='none'?'block':'none';
|
|
10258
|
+
});
|
|
10259
|
+
$('reconnect-btn').addEventListener('click',()=>{
|
|
10260
|
+
const btn=$('reconnect-btn');
|
|
10261
|
+
btn.textContent='\u23F3';
|
|
10262
|
+
btn.style.background='#e74c3c';
|
|
10263
|
+
// Tell daemon to reconnect the remote CDP session
|
|
10264
|
+
sendMsg({type:'reconnect'});
|
|
10265
|
+
// Also reload the page after 2s to re-establish viewer WS
|
|
10266
|
+
setTimeout(()=>{location.reload();},2000);
|
|
10267
|
+
});
|
|
10268
|
+
|
|
10269
|
+
// === Snapshot feature ===
|
|
10270
|
+
// Click \u{1F4F7} \u2192 daemon sends CDP captureScreenshot \u2192 returns high-quality image
|
|
10271
|
+
// Preview shows in bottom-right for 5s with countdown \u2192 click to fullscreen
|
|
10272
|
+
var snapshotDataUrl=null;
|
|
10273
|
+
var snapshotCountdownTimer=null;
|
|
10274
|
+
$('snapshot-btn').addEventListener('click',function(){
|
|
10275
|
+
var btn=$('snapshot-btn');
|
|
10276
|
+
btn.textContent='\u23F3';
|
|
10277
|
+
// Request high-quality screenshot from daemon
|
|
10278
|
+
sendMsg({type:'snapshot_request',format:'webp',quality:90});
|
|
10279
|
+
});
|
|
10280
|
+
// Handle snapshot response
|
|
10281
|
+
// (added to ws.onmessage handler below)
|
|
10282
|
+
var snapshotPendingHandler=function(msg){
|
|
10283
|
+
if(msg.type==='snapshot_result'&&msg.data){
|
|
10284
|
+
var data=msg.data;
|
|
10285
|
+
snapshotDataUrl='data:image/'+(data.format||'png')+';base64,'+data.data;
|
|
10286
|
+
var img=$('snapshot-img');
|
|
10287
|
+
img.src=snapshotDataUrl;
|
|
10288
|
+
var preview=$('snapshot-preview');
|
|
10289
|
+
preview.style.display='block';
|
|
10290
|
+
$('snapshot-btn').textContent='\u{1F4F7}';
|
|
10291
|
+
// Start 5s countdown
|
|
10292
|
+
var remaining=5;
|
|
10293
|
+
var progress=$('snapshot-progress');
|
|
10294
|
+
var countdownEl=$('snapshot-countdown');
|
|
10295
|
+
if(snapshotCountdownTimer) clearInterval(snapshotCountdownTimer);
|
|
10296
|
+
progress.style.width='100%';
|
|
10297
|
+
progress.style.background='#2ecc71';
|
|
10298
|
+
countdownEl.textContent=remaining+'s';
|
|
10299
|
+
snapshotCountdownTimer=setInterval(function(){
|
|
10300
|
+
remaining--;
|
|
10301
|
+
countdownEl.textContent=remaining+'s';
|
|
10302
|
+
progress.style.width=(remaining/5*100)+'%';
|
|
10303
|
+
if(remaining<=0){
|
|
10304
|
+
clearInterval(snapshotCountdownTimer);
|
|
10305
|
+
preview.style.display='none';
|
|
10306
|
+
}
|
|
10307
|
+
},1000);
|
|
10308
|
+
// Hover to pause countdown
|
|
10309
|
+
preview.onmouseenter=function(){if(snapshotCountdownTimer){clearInterval(snapshotCountdownTimer);countdownEl.textContent='\u23F8';}};
|
|
10310
|
+
preview.onmouseleave=function(){
|
|
10311
|
+
if(snapshotCountdownTimer) clearInterval(snapshotCountdownTimer);
|
|
10312
|
+
progress.style.width=(remaining/5*100)+'%';
|
|
10313
|
+
snapshotCountdownTimer=setInterval(function(){
|
|
10314
|
+
remaining--;
|
|
10315
|
+
countdownEl.textContent=remaining+'s';
|
|
10316
|
+
progress.style.width=(remaining/5*100)+'%';
|
|
10317
|
+
if(remaining<=0){clearInterval(snapshotCountdownTimer);preview.style.display='none';}
|
|
10318
|
+
},1000);
|
|
10319
|
+
};
|
|
10320
|
+
}
|
|
10321
|
+
};
|
|
10322
|
+
// Click preview \u2192 fullscreen
|
|
10323
|
+
$('snapshot-preview').addEventListener('click',function(){
|
|
10324
|
+
if(!snapshotDataUrl) return;
|
|
10325
|
+
$('snapshot-full-img').src=snapshotDataUrl;
|
|
10326
|
+
$('snapshot-fullscreen').style.display='flex';
|
|
10327
|
+
if(snapshotCountdownTimer){clearInterval(snapshotCountdownTimer);}
|
|
10328
|
+
});
|
|
10329
|
+
// Click fullscreen \u2192 close
|
|
10330
|
+
$('snapshot-fullscreen').addEventListener('click',function(){
|
|
10331
|
+
this.style.display='none';
|
|
10332
|
+
});
|
|
10333
|
+
// Auto-detect dead connection: if no frames for 30s, show warning
|
|
10334
|
+
let lastFrameAt=Date.now();
|
|
10335
|
+
setInterval(()=>{
|
|
10336
|
+
if(connected && Date.now()-lastFrameAt>30000 && statFrames>0){
|
|
10337
|
+
const btn=$('reconnect-btn');
|
|
10338
|
+
btn.style.background='#e74c3c';
|
|
10339
|
+
btn.textContent='\u{1F504}!';
|
|
10340
|
+
btn.title='\u8FDE\u63A5\u53EF\u80FD\u5DF2\u65AD\u5F00\uFF0C\u70B9\u51FB\u91CD\u8FDE';
|
|
10341
|
+
}
|
|
10342
|
+
},5000);
|
|
10343
|
+
setInterval(()=>{
|
|
10344
|
+
const now=Date.now();
|
|
10345
|
+
const elapsed=now-statFpsTimer;
|
|
10346
|
+
if(elapsed>=1000){
|
|
10347
|
+
statFps=Math.round(statFpsFrames*1000/elapsed);
|
|
10348
|
+
statFpsFrames=0;
|
|
10349
|
+
statFpsTimer=now;
|
|
10350
|
+
const bw=Math.round((statBytesRcvd+statBytesSent)/1024);
|
|
10351
|
+
statEl.fps.textContent=statFps;
|
|
10352
|
+
statEl.fps.style.color=statFps>=15?'#2ecc71':statFps>=5?'#f39c12':'#e74c3c';
|
|
10353
|
+
statEl.frameSize.textContent=Math.round(statFrameSize/1024)+'KB';
|
|
10354
|
+
statEl.latency.textContent=statLatency+'ms';
|
|
10355
|
+
statEl.latency.style.color=statLatency<100?'#2ecc71':statLatency<300?'#f39c12':'#e74c3c';
|
|
10356
|
+
statEl.bandwidth.textContent=bw+'KB/s';
|
|
10357
|
+
statEl.frames.textContent=statFrames;
|
|
10358
|
+
statEl.sent.textContent=statSent;
|
|
10359
|
+
statEl.ws.textContent=connected?'connected':'disconnected';
|
|
10360
|
+
statEl.ws.style.color=connected?'#2ecc71':'#e74c3c';
|
|
10361
|
+
statEl.viewport.textContent=remoteViewport.width+'x'+remoteViewport.height;
|
|
10362
|
+
statEl.uptime.textContent=Math.round((now-startTime)/1000)+'s';
|
|
10363
|
+
statBytesRcvd=0;statBytesSent=0;
|
|
10364
|
+
}
|
|
10365
|
+
},200);
|
|
10366
|
+
|
|
9945
10367
|
function resizeCanvas(){
|
|
9946
10368
|
const box=viewportEl.getBoundingClientRect();
|
|
9947
|
-
const bw=box.width;
|
|
9948
|
-
if(!bw||!remoteViewport.width||!remoteViewport.height) return;
|
|
10369
|
+
const bw=box.width,bh=box.height;
|
|
10370
|
+
if(!bw||!bh||!remoteViewport.width||!remoteViewport.height) return;
|
|
9949
10371
|
const vpAspect=remoteViewport.width/remoteViewport.height;
|
|
9950
|
-
|
|
9951
|
-
|
|
10372
|
+
// Fit the remote viewport inside the available box without cropping:
|
|
10373
|
+
// pick the scale that keeps both dimensions within bounds.
|
|
10374
|
+
const scaleByWidth=bw/remoteViewport.width;
|
|
10375
|
+
const scaleByHeight=bh/remoteViewport.height;
|
|
10376
|
+
const scale=Math.min(scaleByWidth,scaleByHeight);
|
|
10377
|
+
const cw=remoteViewport.width*scale;
|
|
10378
|
+
const ch=remoteViewport.height*scale;
|
|
9952
10379
|
if(canvas.width!==Math.round(cw)||canvas.height!==Math.round(ch)){
|
|
9953
10380
|
canvas.width=Math.round(cw);
|
|
9954
10381
|
canvas.height=Math.round(ch);
|
|
@@ -9981,6 +10408,10 @@ function connectWS(){
|
|
|
9981
10408
|
connEl.textContent='WS';
|
|
9982
10409
|
if(deviceMode==='desktop') createPCKeyboard();
|
|
9983
10410
|
checkFocus();
|
|
10411
|
+
// Apply crop from URL params (?crop=x,y,w,h or ?selector=.cls)
|
|
10412
|
+
setTimeout(applyUrlCrop,500);
|
|
10413
|
+
// Start connection health check \u2014 auto-reconnect on failure
|
|
10414
|
+
startHealthCheck();
|
|
9984
10415
|
};
|
|
9985
10416
|
ws.binaryType='arraybuffer';
|
|
9986
10417
|
ws.onmessage=(e)=>{
|
|
@@ -9991,13 +10422,31 @@ function connectWS(){
|
|
|
9991
10422
|
updateViewTabs(_m.views);
|
|
9992
10423
|
return;
|
|
9993
10424
|
}
|
|
10425
|
+
if(_m.type==='health_pong'){
|
|
10426
|
+
healthCheckPending=false;
|
|
10427
|
+
var btn=$('reconnect-btn');
|
|
10428
|
+
if(btn.textContent!=='\u{1F504}'){btn.style.background='#0f3460';btn.textContent='\u{1F504}';}
|
|
10429
|
+
return;
|
|
10430
|
+
}
|
|
10431
|
+
if(_m.type==='snapshot_result'){
|
|
10432
|
+
if(snapshotPendingHandler) snapshotPendingHandler(_m);
|
|
10433
|
+
return;
|
|
10434
|
+
}
|
|
9994
10435
|
}
|
|
9995
10436
|
if(e.data instanceof ArrayBuffer){
|
|
9996
10437
|
const buf=new Uint8Array(e.data);
|
|
10438
|
+
statBytesRcvd+=buf.length;
|
|
9997
10439
|
const headerLen=(buf[0]<<24)|(buf[1]<<16)|(buf[2]<<8)|buf[3];
|
|
9998
10440
|
const header=JSON.parse(new TextDecoder().decode(buf.slice(4,4+headerLen)));
|
|
9999
10441
|
const jpegData=buf.slice(4+headerLen);
|
|
10000
10442
|
if(header.type==='screenshot'){
|
|
10443
|
+
// === Monitor: track frame stats ===
|
|
10444
|
+
statFrames++;statFpsFrames++;
|
|
10445
|
+
lastFrameAt=Date.now();
|
|
10446
|
+
statFrameSize=jpegData.length;
|
|
10447
|
+
if(header.data.timestamp){
|
|
10448
|
+
statLatency=Math.max(0,Date.now()-header.data.timestamp);
|
|
10449
|
+
}
|
|
10001
10450
|
const blob=new Blob([jpegData],{type:'image/jpeg'});
|
|
10002
10451
|
createImageBitmap(blob).then(function(bmp){
|
|
10003
10452
|
drawFrame(bmp);
|
|
@@ -10139,6 +10588,8 @@ function updateViewTabs(views){
|
|
|
10139
10588
|
selectView('main');
|
|
10140
10589
|
}
|
|
10141
10590
|
renderViewTabs();
|
|
10591
|
+
// Auto-select view matching ?selector= param
|
|
10592
|
+
if(pendingSelector) trySelectBySelector();
|
|
10142
10593
|
}
|
|
10143
10594
|
function renderViewTabs(){
|
|
10144
10595
|
if(detectedViews.length===0){
|
|
@@ -10227,7 +10678,12 @@ function selectView(vid){
|
|
|
10227
10678
|
}
|
|
10228
10679
|
|
|
10229
10680
|
function sendMsg(obj){
|
|
10230
|
-
if(ws&&ws.readyState===WebSocket.OPEN)
|
|
10681
|
+
if(ws&&ws.readyState===WebSocket.OPEN){
|
|
10682
|
+
const data=JSON.stringify(obj);
|
|
10683
|
+
ws.send(data);
|
|
10684
|
+
statSent++;
|
|
10685
|
+
statBytesSent+=data.length;
|
|
10686
|
+
}
|
|
10231
10687
|
}
|
|
10232
10688
|
|
|
10233
10689
|
function parseFocusHash(){
|
|
@@ -10237,6 +10693,107 @@ function parseFocusHash(){
|
|
|
10237
10693
|
return m?decodeURIComponent(m[1]):null;
|
|
10238
10694
|
}
|
|
10239
10695
|
|
|
10696
|
+
// Parse crop/view parameters from URL query string.
|
|
10697
|
+
// Supports:
|
|
10698
|
+
// ?selector=.login-modal \u2014 crop to element matching CSS selector
|
|
10699
|
+
// ?crop=100,200,500,400 \u2014 crop to x,y,width,height (remote viewport coords)
|
|
10700
|
+
function parseUrlCrop(){
|
|
10701
|
+
var params=new URLSearchParams(location.search);
|
|
10702
|
+
|
|
10703
|
+
// ?crop=x,y,w,h
|
|
10704
|
+
var cropParam=params.get('crop');
|
|
10705
|
+
if(cropParam){
|
|
10706
|
+
var parts=cropParam.split(',').map(function(s){return parseInt(s.trim(),10)});
|
|
10707
|
+
if(parts.length===4&&parts.every(function(n){return!isNaN(n)})){
|
|
10708
|
+
return {type:'rect',rect:{x:parts[0],y:parts[1],width:parts[2],height:parts[3]}};
|
|
10709
|
+
}
|
|
10710
|
+
}
|
|
10711
|
+
|
|
10712
|
+
// ?selector=.login-modal
|
|
10713
|
+
var selParam=params.get('selector')||params.get('sel');
|
|
10714
|
+
if(selParam){
|
|
10715
|
+
return {type:'selector',selector:selParam};
|
|
10716
|
+
}
|
|
10717
|
+
|
|
10718
|
+
return null;
|
|
10719
|
+
}
|
|
10720
|
+
|
|
10721
|
+
// Apply URL crop on connect \u2014 sends select_view immediately after WS opens
|
|
10722
|
+
function applyUrlCrop(){
|
|
10723
|
+
var cropSpec=parseUrlCrop();
|
|
10724
|
+
if(!cropSpec) return;
|
|
10725
|
+
|
|
10726
|
+
if(cropSpec.type==='rect'){
|
|
10727
|
+
// Direct rect \u2014 send select_view immediately
|
|
10728
|
+
sendMsg({type:'select_view',rect:cropSpec.rect});
|
|
10729
|
+
if(!originalViewport) originalViewport={width:remoteViewport.width,height:remoteViewport.height};
|
|
10730
|
+
remoteViewport={width:cropSpec.rect.width,height:cropSpec.rect.height};
|
|
10731
|
+
viewportLocked=true;
|
|
10732
|
+
resizeCanvas();
|
|
10733
|
+
return;
|
|
10734
|
+
}
|
|
10735
|
+
|
|
10736
|
+
// Selector \u2014 wait for views_update from element-monitor, then auto-select matching view
|
|
10737
|
+
if(cropSpec.type==='selector'){
|
|
10738
|
+
pendingSelector=cropSpec.selector;
|
|
10739
|
+
// Try match immediately in case views already arrived
|
|
10740
|
+
trySelectBySelector();
|
|
10741
|
+
}
|
|
10742
|
+
}
|
|
10743
|
+
// Connection health check \u2014 send a ping every 15s.
|
|
10744
|
+
// If no response in 5s, auto-reconnect the session.
|
|
10745
|
+
var healthCheckTimer=null;
|
|
10746
|
+
var healthCheckPending=false;
|
|
10747
|
+
var autoReconnecting=false;
|
|
10748
|
+
function startHealthCheck(){
|
|
10749
|
+
if(healthCheckTimer) clearInterval(healthCheckTimer);
|
|
10750
|
+
healthCheckTimer=setInterval(function(){
|
|
10751
|
+
if(!connected) return;
|
|
10752
|
+
if(healthCheckPending){
|
|
10753
|
+
// Previous ping never got a response \u2192 connection is dead \u2192 auto-reconnect
|
|
10754
|
+
if(!autoReconnecting){
|
|
10755
|
+
autoReconnecting=true;
|
|
10756
|
+
var btn=$('reconnect-btn');
|
|
10757
|
+
btn.style.background='#e74c3c';
|
|
10758
|
+
btn.textContent='\u{1F504}!';
|
|
10759
|
+
btn.title='\u8FDE\u63A5\u5DF2\u65AD\u5F00\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u8FDE...';
|
|
10760
|
+
// Auto-reconnect: send reconnect then reload after 2s
|
|
10761
|
+
sendMsg({type:'reconnect'});
|
|
10762
|
+
setTimeout(function(){location.reload();},2000);
|
|
10763
|
+
}
|
|
10764
|
+
return;
|
|
10765
|
+
}
|
|
10766
|
+
healthCheckPending=true;
|
|
10767
|
+
sendMsg({type:'health_ping',ts:Date.now()});
|
|
10768
|
+
setTimeout(function(){
|
|
10769
|
+
if(healthCheckPending){
|
|
10770
|
+
// Still pending after 5s = dead
|
|
10771
|
+
healthCheckPending=false;
|
|
10772
|
+
var btn2=$('reconnect-btn');
|
|
10773
|
+
btn2.style.background='#e74c3c';
|
|
10774
|
+
btn2.textContent='\u{1F504}!';
|
|
10775
|
+
}
|
|
10776
|
+
},5000);
|
|
10777
|
+
},15000);
|
|
10778
|
+
}
|
|
10779
|
+
function trySelectBySelector(){
|
|
10780
|
+
if(!pendingSelector||!detectedViews||detectedViews.length===0) return;
|
|
10781
|
+
// Match by label containing selector text, or id/class match
|
|
10782
|
+
var target=detectedViews.find(function(v){
|
|
10783
|
+
var label=(v.label||'').toLowerCase();
|
|
10784
|
+
var id=(v.id||'').toLowerCase();
|
|
10785
|
+
return label.indexOf(pendingSelector.toLowerCase())>=0||id.indexOf(pendingSelector.toLowerCase())>=0;
|
|
10786
|
+
});
|
|
10787
|
+
if(target){
|
|
10788
|
+
sendMsg({type:'select_view',rect:target.rect});
|
|
10789
|
+
if(!originalViewport) originalViewport={width:remoteViewport.width,height:remoteViewport.height};
|
|
10790
|
+
remoteViewport={width:target.rect.width,height:target.rect.height};
|
|
10791
|
+
viewportLocked=true;
|
|
10792
|
+
resizeCanvas();
|
|
10793
|
+
pendingSelector=null; // matched, stop looking
|
|
10794
|
+
}
|
|
10795
|
+
}
|
|
10796
|
+
|
|
10240
10797
|
function checkFocus(){
|
|
10241
10798
|
var sel=parseFocusHash();
|
|
10242
10799
|
if(sel&&ws&&ws.readyState===WebSocket.OPEN){
|
|
@@ -10336,10 +10893,16 @@ function createPCKeyboard(){
|
|
|
10336
10893
|
document.addEventListener('keydown',function(e){
|
|
10337
10894
|
if(deviceMode!=='desktop') return;
|
|
10338
10895
|
if(e.isComposing||_pcComposing) return;
|
|
10339
|
-
var tag=e.target&&e.target.tagName;
|
|
10340
|
-
if(tag==='INPUT'||tag==='TEXTAREA') return;
|
|
10341
10896
|
var key=e.key;
|
|
10342
10897
|
if(!key) return;
|
|
10898
|
+
|
|
10899
|
+
// Control keys (Backspace, Delete, Enter, Tab, arrows, etc.) must always
|
|
10900
|
+
// be forwarded even when focus is in an input/textarea \u2014 otherwise the user
|
|
10901
|
+
// can't edit text in the remote browser's input fields.
|
|
10902
|
+
var isControlKey=key.length>1; // Backspace, Delete, Enter, Tab, ArrowX, etc.
|
|
10903
|
+
var tag=e.target&&e.target.tagName;
|
|
10904
|
+
if(!isControlKey&&(tag==='INPUT'||tag==='TEXTAREA')) return;
|
|
10905
|
+
|
|
10343
10906
|
e.preventDefault();
|
|
10344
10907
|
sendMsg({type:'input_keyboard',action:'down',key:key});
|
|
10345
10908
|
if(key.length===1){
|
|
@@ -10350,10 +10913,13 @@ function createPCKeyboard(){
|
|
|
10350
10913
|
document.addEventListener('keyup',function(e){
|
|
10351
10914
|
if(deviceMode!=='desktop') return;
|
|
10352
10915
|
if(e.isComposing||_pcComposing) return;
|
|
10353
|
-
var tag=e.target&&e.target.tagName;
|
|
10354
|
-
if(tag==='INPUT'||tag==='TEXTAREA') return;
|
|
10355
10916
|
var key=e.key;
|
|
10356
10917
|
if(!key) return;
|
|
10918
|
+
|
|
10919
|
+
var isControlKey=key.length>1;
|
|
10920
|
+
var tag=e.target&&e.target.tagName;
|
|
10921
|
+
if(!isControlKey&&(tag==='INPUT'||tag==='TEXTAREA')) return;
|
|
10922
|
+
|
|
10357
10923
|
e.preventDefault();
|
|
10358
10924
|
sendMsg({type:'input_keyboard',action:'up',key:key});
|
|
10359
10925
|
sendUserActivity();
|
|
@@ -10406,9 +10972,20 @@ viewportEl.addEventListener('mouseleave',()=>{
|
|
|
10406
10972
|
viewportEl.addEventListener('wheel',(e)=>{
|
|
10407
10973
|
if(deviceMode!=='desktop') return;
|
|
10408
10974
|
e.preventDefault();
|
|
10409
|
-
|
|
10410
|
-
|
|
10975
|
+
// Accumulate wheel deltas and send at most every 50ms to avoid flooding
|
|
10976
|
+
// the remote with hundreds of scroll commands during a single swipe.
|
|
10977
|
+
pendingScrollX+=e.deltaX;
|
|
10978
|
+
pendingScrollY+=e.deltaY;
|
|
10979
|
+
if(!scrollThrottled){
|
|
10980
|
+
scrollThrottled=true;
|
|
10981
|
+
sendMsg({type:'scroll',deltaX:Math.round(pendingScrollX),deltaY:Math.round(pendingScrollY)});
|
|
10982
|
+
sendUserActivity();
|
|
10983
|
+
pendingScrollX=0;
|
|
10984
|
+
pendingScrollY=0;
|
|
10985
|
+
setTimeout(function(){scrollThrottled=false;},50);
|
|
10986
|
+
}
|
|
10411
10987
|
},{passive:false});
|
|
10988
|
+
var scrollThrottled=false,pendingScrollX=0,pendingScrollY=0;
|
|
10412
10989
|
|
|
10413
10990
|
// --- Mobile Touchpad (incremental, smooth) ---
|
|
10414
10991
|
let tpStartPos=null;
|
|
@@ -10905,6 +11482,13 @@ async function main() {
|
|
|
10905
11482
|
rpcHandler.setPreviewWS(previewWS);
|
|
10906
11483
|
previewWS.on("screencast-started", (sid) => log(`Preview screencast started: ${sid}`));
|
|
10907
11484
|
previewWS.on("screencast-stopped", (sid) => log(`Preview screencast stopped: ${sid}`));
|
|
11485
|
+
previewWS.on("reconnect-request", ({ sessionId }) => {
|
|
11486
|
+
if (!sessionId) return;
|
|
11487
|
+
log(`Reconnect request from viewer for session: ${sessionId}`);
|
|
11488
|
+
rpcHandler.handleReconnect(sessionId).catch((e) => {
|
|
11489
|
+
log(`Reconnect failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
11490
|
+
});
|
|
11491
|
+
});
|
|
10908
11492
|
mkdirSync5(CONFIG_DIR3, { recursive: true });
|
|
10909
11493
|
writeFileSync5(join7(CONFIG_DIR3, "daemon.json"), JSON.stringify({
|
|
10910
11494
|
port: daemonPort,
|
|
@@ -10913,6 +11497,13 @@ async function main() {
|
|
|
10913
11497
|
}, null, 2));
|
|
10914
11498
|
console.log(`xbrowser daemon started (pid: ${process.pid}, port: ${daemonPort})`);
|
|
10915
11499
|
log("Daemon main started successfully");
|
|
11500
|
+
const loader = await getPluginLoader();
|
|
11501
|
+
const sites = loader.getCore().loader.getSites();
|
|
11502
|
+
log(`Loaded ${sites.length} plugins`);
|
|
11503
|
+
if (sites.length > 0) {
|
|
11504
|
+
const names = sites.map((s) => s.name || "unknown").join(", ");
|
|
11505
|
+
log(`Plugins: ${names}`);
|
|
11506
|
+
}
|
|
10916
11507
|
const shutdown = () => {
|
|
10917
11508
|
log("Received shutdown signal, stopping");
|
|
10918
11509
|
previewWS.stop().catch(() => {
|