phone-lens 1.1.5 → 1.1.6
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/cordis.patch.yml +0 -4
- package/lib/client.js +261 -81
- package/lib/dev.js +2923 -123
- package/lib/index.js +2997 -273
- package/package.json +2 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
package/cordis.patch.yml
CHANGED
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
allowedTypes:
|
|
16
16
|
- 'image/jpeg'
|
|
17
17
|
- 'image/png'
|
|
18
|
-
uploadsPerMinute: 10
|
|
19
18
|
previewFrameMaxBytes: 524288
|
|
20
19
|
pairing:
|
|
21
20
|
codeTtlMs: 900000
|
|
@@ -24,9 +23,6 @@
|
|
|
24
23
|
maxHeight: 480
|
|
25
24
|
fps: 10
|
|
26
25
|
jpegQuality: 70
|
|
27
|
-
inject:
|
|
28
|
-
mode: 'followup'
|
|
29
|
-
notePrefix: '[手机拍照]'
|
|
30
26
|
target:
|
|
31
27
|
mode: 'latest'
|
|
32
28
|
pinnedSessionId: null
|
package/lib/client.js
CHANGED
|
@@ -26,9 +26,62 @@ window.__ModuleLoader__.load({
|
|
|
26
26
|
// FAB_EV CustomEvent so both stay live without a host round-trip.
|
|
27
27
|
const FAB_KEY = "phoneLens.fab";
|
|
28
28
|
const FAB_EV = "phone-lens:fab-changed";
|
|
29
|
-
// port
|
|
30
|
-
//
|
|
29
|
+
// port updates push the overlay to re-point its URLs without a page
|
|
30
|
+
// reload (fired by discovery when the cached port turns out stale)
|
|
31
31
|
const PORT_EV = "phone-lens:port-changed";
|
|
32
|
+
// The listen port is HOST-owned (plugin config server.port, default
|
|
33
|
+
// 8791) — the localStorage entry is only a discovery cache, never a
|
|
34
|
+
// source of truth. Discovery probes candidates against GET /info and
|
|
35
|
+
// locks onto whichever port actually answers as this plugin.
|
|
36
|
+
const DEFAULT_PORT = "8791";
|
|
37
|
+
function cachedLensPort() {
|
|
38
|
+
try {
|
|
39
|
+
return window.localStorage.getItem(PORT_KEY) || DEFAULT_PORT;
|
|
40
|
+
} catch {
|
|
41
|
+
return DEFAULT_PORT;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function probeLensPort(p) {
|
|
45
|
+
try {
|
|
46
|
+
const ctl = new AbortController();
|
|
47
|
+
const timer = setTimeout(() => ctl.abort(), 2500);
|
|
48
|
+
const r = await fetch(`http://127.0.0.1:${p}/info`, { cache: "no-store", signal: ctl.signal });
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
if (!r.ok) return null;
|
|
51
|
+
const d = await r.json().catch(() => null);
|
|
52
|
+
// strict identity: only the real phone-lens /info carries this exact
|
|
53
|
+
// name literal (same string as the host's /info route). Scanning a
|
|
54
|
+
// 10-port range makes loose "any JSON with a name" matching too
|
|
55
|
+
// easy to false-lock onto an unrelated loopback service.
|
|
56
|
+
if (!d || typeof d !== "object" || d.name !== "PhoneLens 直连取景") return null;
|
|
57
|
+
return { port: String(d.serverPort || p), version: d.version || "" };
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Returns the first candidate that answers as phone-lens (cached port
|
|
63
|
+
// first, then the whole 8791~8800 range low→high — the host falls back
|
|
64
|
+
// upward over the same range when 8791 is taken), syncs the cache, and
|
|
65
|
+
// broadcasts PORT_EV on change. If nothing answers, returns the cached
|
|
66
|
+
// value so the UI keeps showing diagnostics against the last-known port
|
|
67
|
+
// instead of guessing.
|
|
68
|
+
async function discoverLensPort() {
|
|
69
|
+
const cached = cachedLensPort();
|
|
70
|
+
const ports = ["8791", "8792", "8793", "8794", "8795", "8796", "8797", "8798", "8799", "8800"];
|
|
71
|
+
const candidates = cached === DEFAULT_PORT ? ports : [cached, ...ports.filter((p) => p !== cached)];
|
|
72
|
+
for (const p of candidates) {
|
|
73
|
+
const hit = await probeLensPort(p);
|
|
74
|
+
if (!hit) continue;
|
|
75
|
+
if (hit.port !== cached) {
|
|
76
|
+
try {
|
|
77
|
+
window.localStorage.setItem(PORT_KEY, hit.port);
|
|
78
|
+
window.dispatchEvent(new CustomEvent(PORT_EV));
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
return hit.port;
|
|
82
|
+
}
|
|
83
|
+
return cached;
|
|
84
|
+
}
|
|
32
85
|
const FAB_SIZES = [
|
|
33
86
|
{ id: "s", label: "小", px: 38 },
|
|
34
87
|
{ id: "m", label: "标准", px: 46 },
|
|
@@ -135,6 +188,8 @@ window.__ModuleLoader__.load({
|
|
|
135
188
|
.lm-device { flex: none; padding: 4px 8px; border-radius: 14px; border: 1px solid #2f3b48; background: #1f2937; color: #8fa1b5; font-size: 11px; cursor: pointer; }
|
|
136
189
|
.lm-device.active { border-color: #46a758; color: #d2f5d8; background: #1f4a2c; }
|
|
137
190
|
.lm-device:hover { background: #28323e; }
|
|
191
|
+
.lm-unbind { flex: none; width: 15px; height: 15px; padding: 0; border: none; border-radius: 50%; background: none; color: #8fa1b5; font-size: 9px; line-height: 1; cursor: pointer; }
|
|
192
|
+
.lm-unbind:hover { color: #e5696e; background: #2a1f22; }
|
|
138
193
|
.lm-rename { position: absolute; right: 0; bottom: 56px; width: 268px; background: #14181d; border: 1px solid #2a333e; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.6); padding: 12px; display: flex; flex-direction: column; gap: 8px; z-index: 5; }.lm-rename-title { font-size: 13px; color: #dfe7ee; font-weight: 600; }
|
|
139
194
|
.lm-rename-input { background: #0d1117; color: #dfe7ee; border: 1px solid #2f3b48; border-radius: 8px; padding: 8px; font-size: 13px; }
|
|
140
195
|
.lm-appqr { position: absolute; right: 0; bottom: 56px; width: 268px; background: #14181d; border: 1px solid #2a333e; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,.6); padding: 12px; display: flex; flex-direction: column; align-items: center; gap: 8px; z-index: 6; }
|
|
@@ -280,7 +335,14 @@ window.__ModuleLoader__.load({
|
|
|
280
335
|
const resp = await fetch(`http://127.0.0.1:${port}/pending/${attachmentId}`).catch((e) => { throw new Error("fetch pending: " + String(e)); });
|
|
281
336
|
if (!resp.ok) throw new Error("pending fetch HTTP " + resp.status);
|
|
282
337
|
const buf = await resp.arrayBuffer();
|
|
283
|
-
|
|
338
|
+
// PNG/WebP uploads stage with their original bytes (high-quality
|
|
339
|
+
// passes originals through) — sniff the type, don't trust the
|
|
340
|
+
// flattened .jpg staging name
|
|
341
|
+
const u8 = new Uint8Array(buf);
|
|
342
|
+
const isPng = u8.length >= 4 && u8[0] === 0x89 && u8[1] === 0x50 && u8[2] === 0x4e && u8[3] === 0x47;
|
|
343
|
+
const isWebp = !isPng && u8.length >= 12 && String.fromCharCode(u8[0], u8[1], u8[2], u8[3]) === "RIFF" && String.fromCharCode(u8[8], u8[9], u8[10], u8[11]) === "WEBP";
|
|
344
|
+
const mime = isPng ? "image/png" : isWebp ? "image/webp" : "image/jpeg";
|
|
345
|
+
const file = new File([buf], `phone-${attachmentId.slice(0, 8)}.${isPng ? "png" : isWebp ? "webp" : "jpg"}`, { type: mime });
|
|
284
346
|
// rc.2 path: the composer's own paste handler admits image files
|
|
285
347
|
// through the official intake (intakeFiles → createDrafts), so we
|
|
286
348
|
// dispatch a synthetic paste instead of touching package-internal
|
|
@@ -329,6 +391,10 @@ window.__ModuleLoader__.load({
|
|
|
329
391
|
const [qr, setQr] = useState(null);
|
|
330
392
|
const [, setTick] = useState(0); // 1s heartbeat: re-renders the QR countdown while pairing is visible
|
|
331
393
|
const [flash, setFlash] = useState("");
|
|
394
|
+
// last flash's kind: the recovery paths (stream back on / phone back
|
|
395
|
+
// online) clear only ERROR flashes — success messages own their
|
|
396
|
+
// lifetime and get replaced by the next event naturally
|
|
397
|
+
const flashIsErr = useRef(false);
|
|
332
398
|
const [pending, setPending] = useState([]); // [{id, name}] waiting to be staged
|
|
333
399
|
const [devices, setDevices] = useState([]); // [{id,name,active,appVersion?}]
|
|
334
400
|
// Host plugin-update check (update-check.ts): null = unchecked/failed —
|
|
@@ -365,6 +431,13 @@ window.__ModuleLoader__.load({
|
|
|
365
431
|
const baseUrl = `http://127.0.0.1:${port}`;
|
|
366
432
|
const wsUrl = `ws://127.0.0.1:${port}/ws/view`;
|
|
367
433
|
|
|
434
|
+
// one-shot port discovery on mount: the port state starts from the
|
|
435
|
+
// localStorage cache; discovery re-points it (via PORT_EV) if the
|
|
436
|
+
// cache is stale — the host's actual listen port is the only truth
|
|
437
|
+
useEffect(() => {
|
|
438
|
+
void discoverLensPort();
|
|
439
|
+
}, []);
|
|
440
|
+
|
|
368
441
|
// plugin-update check, once per panel-open (host caches the Gitee
|
|
369
442
|
// lookup 12h / failures 30min — this fetch never hits the API itself)
|
|
370
443
|
useEffect(() => {
|
|
@@ -415,32 +488,40 @@ window.__ModuleLoader__.load({
|
|
|
415
488
|
} catch {
|
|
416
489
|
return;
|
|
417
490
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
491
|
+
if (m.type === "meta") {
|
|
492
|
+
const on = m.camera.connected;
|
|
493
|
+
setCamOn(Boolean(on));
|
|
494
|
+
setLink((prev) => {
|
|
495
|
+
if (!on) return "disconnected";
|
|
496
|
+
return prev === "disconnected" ? "connecting" : prev;
|
|
497
|
+
});
|
|
498
|
+
// phone (re)connected: same transient-error clearing as
|
|
499
|
+
// preview_state — the complaint is stale the moment it's back
|
|
500
|
+
if (on && flashIsErr.current) {
|
|
501
|
+
flashIsErr.current = false;
|
|
502
|
+
setFlash("");
|
|
503
|
+
}
|
|
504
|
+
if (m.camera.rotation !== undefined) rotRef.current = m.camera.rotation;
|
|
505
|
+
} else if (m.type === "frame_meta") {
|
|
427
506
|
if (m.rotation !== undefined) rotRef.current = m.rotation;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
507
|
+
} else if (m.type === "preview_state") {
|
|
508
|
+
// host-derived: the ACTIVE phone's stream stopped — either the
|
|
509
|
+
// user turned preview off in the app (keepalive still alive) or
|
|
510
|
+
// the link itself died (no frames AND no keepalive for 25s →
|
|
511
|
+
// reason:"offline"). Old hosts never send `reason` → legacy
|
|
512
|
+
// behavior is kept bit-for-bit.
|
|
513
|
+
setPreviewOff(!m.on);
|
|
514
|
+
setPreviewOffline(!m.on && m.reason === "offline");
|
|
515
|
+
// the stream is live again: an error flash (CAPTURE_DECLINED /
|
|
516
|
+
// NO_CAMERA …) described a TRANSIENT condition — clear it now
|
|
517
|
+
// instead of letting the stale complaint sit there forever
|
|
518
|
+
if (m.on && flashIsErr.current) {
|
|
519
|
+
flashIsErr.current = false;
|
|
520
|
+
setFlash("");
|
|
521
|
+
}
|
|
436
522
|
} else if (m.type === "upload_saved") {
|
|
437
523
|
// folder-only mode: the photo bypassed the composer; local hint
|
|
438
524
|
setFlashOk(`已保存至 ${m.dir || "指定文件夹"}`);
|
|
439
|
-
} else if (m.type === "injected") {
|
|
440
|
-
if (m.ok) setFlashOk(`已注入会话 ${(m.sessionId || "").slice(0, 8)}…`);
|
|
441
|
-
else setFlashErr(`已保存到电脑 ✓(未注入:${m.reason || "无活动会话"})`);
|
|
442
|
-
} else if (m.type === "upload") {
|
|
443
|
-
setFlashOk("图片已入库…");
|
|
444
525
|
} else if (m.type === "pending_image") {
|
|
445
526
|
// pre-send: stage this photo into the composer draft
|
|
446
527
|
setPending((p) => [...p, { id: m.attachmentId, name: m.name || "" }]);
|
|
@@ -478,7 +559,9 @@ window.__ModuleLoader__.load({
|
|
|
478
559
|
setFlashOk(`设备已配对${m.name ? ": " + m.name : ""},二维码已更换`);
|
|
479
560
|
void refreshQr(true);
|
|
480
561
|
} else if (m.type === "error") {
|
|
481
|
-
|
|
562
|
+
// human text first (all hub broadcasts carry Chinese messages
|
|
563
|
+
// now); the wire code is only a fallback for unknown/legacy types
|
|
564
|
+
setFlashErr(m.message || m.code || "未知错误");
|
|
482
565
|
}
|
|
483
566
|
};
|
|
484
567
|
ws.onclose = () => {
|
|
@@ -566,12 +649,14 @@ window.__ModuleLoader__.load({
|
|
|
566
649
|
setAnchor(`${vertical}-${horizontal}`);
|
|
567
650
|
}, [open, rename, appQr, dirPick, settingsOpen, camOn, previewOff, Boolean(qr), pending.length, fabSt]);
|
|
568
651
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
652
|
+
function setFlashOk(t) {
|
|
653
|
+
flashIsErr.current = false;
|
|
654
|
+
setFlash(t);
|
|
655
|
+
}
|
|
656
|
+
function setFlashErr(t) {
|
|
657
|
+
flashIsErr.current = true;
|
|
658
|
+
setFlash(t);
|
|
659
|
+
}
|
|
575
660
|
|
|
576
661
|
async function refreshQr(auto) {
|
|
577
662
|
try {
|
|
@@ -582,9 +667,31 @@ window.__ModuleLoader__.load({
|
|
|
582
667
|
if (auto && lastQrCode.current && lastQrCode.current !== data.code) setFlashOk("二维码已更新,请扫描最新码");
|
|
583
668
|
lastQrCode.current = data.code;
|
|
584
669
|
setQr(data);
|
|
585
|
-
|
|
586
|
-
|
|
670
|
+
} catch (e) {
|
|
671
|
+
// the cached port may be stale (host re-bound / another
|
|
672
|
+
// instance) — one discovery pass may re-point the URLs via
|
|
673
|
+
// PORT_EV. When the port moved, re-fetch the QR against the
|
|
674
|
+
// new port right here: this closure still holds the old
|
|
675
|
+
// baseUrl, and the qr effect would not retry until the next
|
|
676
|
+
// expiry/drift tick, leaving the panel stuck on "加载中…".
|
|
677
|
+
const found = await discoverLensPort();
|
|
678
|
+
if (found !== port) {
|
|
679
|
+
try {
|
|
680
|
+
const r = await fetch(`http://127.0.0.1:${found}/qr.json`, { cache: "no-store" });
|
|
681
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
682
|
+
const data = await r.json();
|
|
683
|
+
lastQrCode.current = data.code;
|
|
684
|
+
setQr(data);
|
|
685
|
+
setFlashOk(`端口已自动切换为 ${found},二维码已恢复`);
|
|
686
|
+
return;
|
|
687
|
+
} catch {}
|
|
587
688
|
}
|
|
689
|
+
setFlashErr(
|
|
690
|
+
found !== port
|
|
691
|
+
? `端口已自动切换为 ${found},二维码恢复失败,请点「↻ 二维码」重试`
|
|
692
|
+
: `接收服务不可达(127.0.0.1:${port})——请确认电脑端 phone-lens 已启动且端口未被占用`,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
588
695
|
}
|
|
589
696
|
|
|
590
697
|
// Manual refresh must FORCE a new pairing code server-side: /qr.json
|
|
@@ -674,7 +781,7 @@ window.__ModuleLoader__.load({
|
|
|
674
781
|
function shoot() {
|
|
675
782
|
const ws = wsRef.current; if (ws && ws.readyState === 1) {
|
|
676
783
|
ws.send(JSON.stringify({ type: "capture" }));
|
|
677
|
-
|
|
784
|
+
setFlashOk("快门已触发,等待手机上传…");
|
|
678
785
|
}
|
|
679
786
|
}
|
|
680
787
|
|
|
@@ -692,12 +799,35 @@ window.__ModuleLoader__.load({
|
|
|
692
799
|
if (trimmed && ws && ws.readyState === 1) ws.send(JSON.stringify({ type: "rename_device", deviceId: rename.id, name: trimmed }));
|
|
693
800
|
setRename(null);
|
|
694
801
|
}
|
|
695
|
-
//
|
|
696
|
-
// the
|
|
802
|
+
// Unbind a paired device (host POST /devices/remove, loopback-only).
|
|
803
|
+
// Confirmed — this erases the pairing entry, the phone must re-scan
|
|
804
|
+
// to come back. Success removes the chip locally; the host also
|
|
805
|
+
// refreshes the views, so the next devices broadcast agrees.
|
|
806
|
+
async function unbindDevice(d) {
|
|
807
|
+
const label = d.name || d.id.slice(0, 8);
|
|
808
|
+
if (!window.confirm(`解绑设备「${label}」?解绑后该手机需重新扫码配对。`)) return;
|
|
809
|
+
try {
|
|
810
|
+
const r = await fetch(`${baseUrl}/devices/remove`, {
|
|
811
|
+
method: "POST",
|
|
812
|
+
headers: { "content-type": "application/json" },
|
|
813
|
+
body: JSON.stringify({ deviceId: d.id }),
|
|
814
|
+
});
|
|
815
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
816
|
+
const data = await r.json().catch(() => null);
|
|
817
|
+
if (!data || data.ok !== true) throw new Error("设备不存在或已被解绑");
|
|
818
|
+
setDevices((prev) => prev.filter((x) => x.id !== d.id));
|
|
819
|
+
if (d.active) setCamOn(false);
|
|
820
|
+
setFlashOk(`设备已解绑: ${label}`);
|
|
821
|
+
} catch (e) {
|
|
822
|
+
setFlashErr("解绑失败: " + String((e && e.message) || e).slice(0, 80));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// port cache updates (discovery) arrive via PORT_EV so the overlay
|
|
826
|
+
// re-points baseUrl/wsUrl without a reload
|
|
697
827
|
useEffect(() => {
|
|
698
828
|
const onPort = () => {
|
|
699
829
|
try {
|
|
700
|
-
setPort(window.localStorage.getItem(PORT_KEY) ||
|
|
830
|
+
setPort(window.localStorage.getItem(PORT_KEY) || DEFAULT_PORT);
|
|
701
831
|
} catch {}
|
|
702
832
|
};
|
|
703
833
|
window.addEventListener(PORT_EV, onPort);
|
|
@@ -847,15 +977,24 @@ window.__ModuleLoader__.load({
|
|
|
847
977
|
}
|
|
848
978
|
return h("div", { className: "lm-appver", key: "appver" }, `手机 App v${app}`);
|
|
849
979
|
})(),
|
|
850
|
-
devices && devices.length >
|
|
980
|
+
devices && devices.length > 0
|
|
851
981
|
? h(
|
|
852
982
|
"div",
|
|
853
983
|
{ className: "lm-devices" },
|
|
854
984
|
devices.map((d) =>
|
|
855
985
|
h(
|
|
856
|
-
"
|
|
857
|
-
{ key: d.id,
|
|
858
|
-
(
|
|
986
|
+
"span",
|
|
987
|
+
{ key: d.id, style: { display: "inline-flex", alignItems: "center", gap: "2px" } },
|
|
988
|
+
h(
|
|
989
|
+
"button",
|
|
990
|
+
{ className: "lm-device" + (d.active ? " active" : ""), onClick: () => selectDevice(d.id), onDoubleClick: () => renameDevice(d.id, d.name), title: "单击切换 · 双击重命名" },
|
|
991
|
+
(d.active ? "● " : "○ ") + d.name,
|
|
992
|
+
),
|
|
993
|
+
h(
|
|
994
|
+
"button",
|
|
995
|
+
{ className: "lm-unbind", onClick: () => void unbindDevice(d), title: "解绑此设备(解绑后需重新扫码配对)" },
|
|
996
|
+
"✕",
|
|
997
|
+
),
|
|
859
998
|
),
|
|
860
999
|
),
|
|
861
1000
|
)
|
|
@@ -910,7 +1049,12 @@ window.__ModuleLoader__.load({
|
|
|
910
1049
|
h("a", { className: "lm-dl", href: "#", onClick: (e) => { e.preventDefault(); void openAppQr(); }, title: "弹出二维码,手机扫码下载" }, "手机还没装 App?点此扫码下载"),
|
|
911
1050
|
)
|
|
912
1051
|
: null,
|
|
913
|
-
|
|
1052
|
+
// kind-flag styling, not string sniffing: "照片已注入输入框…"
|
|
1053
|
+
// and friends don't start with "已" — judge by the
|
|
1054
|
+
// flashIsErr ref that setFlashOk/setFlashErr maintain
|
|
1055
|
+
// (kept in lockstep with the flash state, so it's current
|
|
1056
|
+
// at render time)
|
|
1057
|
+
h("span", { className: "lm-flash" + (flash ? (flashIsErr.current ? " err" : " ok") : "") }, flash),
|
|
914
1058
|
h(
|
|
915
1059
|
"div",
|
|
916
1060
|
{ className: "lm-row" },
|
|
@@ -956,7 +1100,7 @@ window.__ModuleLoader__.load({
|
|
|
956
1100
|
? h("div", { className: "lm-sethint" }, `留空路径即恢复默认目录: ${appSt.effectiveSaveDir}`)
|
|
957
1101
|
: null,
|
|
958
1102
|
]
|
|
959
|
-
: h("div", { className: "lm-sethint" }, "设置服务不可用(电脑端版本较旧)
|
|
1103
|
+
: h("div", { className: "lm-sethint" }, "设置服务不可用(电脑端版本较旧),请更新后再调整拍照模式与保存目录;端口查看在 设置 → PhoneLens 内。"),
|
|
960
1104
|
)
|
|
961
1105
|
: null,
|
|
962
1106
|
),
|
|
@@ -1065,9 +1209,9 @@ window.__ModuleLoader__.load({
|
|
|
1065
1209
|
// settings stay in the overlay ⚙ panel on purpose (switched constantly).
|
|
1066
1210
|
function currentLensPort() {
|
|
1067
1211
|
try {
|
|
1068
|
-
return window.localStorage.getItem(PORT_KEY) ||
|
|
1212
|
+
return window.localStorage.getItem(PORT_KEY) || DEFAULT_PORT;
|
|
1069
1213
|
} catch {
|
|
1070
|
-
return
|
|
1214
|
+
return DEFAULT_PORT;
|
|
1071
1215
|
}
|
|
1072
1216
|
}
|
|
1073
1217
|
|
|
@@ -1103,27 +1247,50 @@ window.__ModuleLoader__.load({
|
|
|
1103
1247
|
}
|
|
1104
1248
|
|
|
1105
1249
|
function LensSettingsSection() {
|
|
1106
|
-
|
|
1250
|
+
// host app-settings: undefined=loading, false=unreachable (nothing
|
|
1251
|
+
// listening on the port), "old"=404 (host < 1.0.4 lacks the
|
|
1252
|
+
// endpoint), object=live
|
|
1253
|
+
const [st, setSt] = useState(undefined);
|
|
1107
1254
|
const [flash, setFlash] = useState("");
|
|
1108
1255
|
const [fab, setFab] = useState(loadFabSt); // ball geometry (local, synced with the overlay via FAB_EV)
|
|
1109
|
-
|
|
1256
|
+
// resolved port: starts from the discovery cache; discovery owns
|
|
1257
|
+
// the truth and re-points this via PORT_EV, then the st fetch
|
|
1258
|
+
// below re-runs for the new port
|
|
1259
|
+
const [port, setPort] = useState(currentLensPort);
|
|
1260
|
+
|
|
1261
|
+
useEffect(() => {
|
|
1262
|
+
const onPort = () => setPort(currentLensPort());
|
|
1263
|
+
window.addEventListener(PORT_EV, onPort);
|
|
1264
|
+
return () => window.removeEventListener(PORT_EV, onPort);
|
|
1265
|
+
}, []);
|
|
1266
|
+
// kick discovery on mount (idempotent with the overlay's own run;
|
|
1267
|
+
// both converge on the port that actually answers /info)
|
|
1268
|
+
useEffect(() => {
|
|
1269
|
+
void discoverLensPort();
|
|
1270
|
+
}, []);
|
|
1110
1271
|
|
|
1111
1272
|
useEffect(() => {
|
|
1112
1273
|
let alive = true;
|
|
1113
1274
|
void (async () => {
|
|
1114
1275
|
try {
|
|
1115
|
-
const r = await fetch(`http://127.0.0.1:${
|
|
1116
|
-
if (!r.ok)
|
|
1276
|
+
const r = await fetch(`http://127.0.0.1:${port}/app-settings`, { cache: "no-store" });
|
|
1277
|
+
if (!r.ok) {
|
|
1278
|
+
const err = new Error(String(r.status));
|
|
1279
|
+
err.httpStatus = r.status;
|
|
1280
|
+
throw err;
|
|
1281
|
+
}
|
|
1117
1282
|
const data = await r.json();
|
|
1118
1283
|
if (alive) setSt(data);
|
|
1119
|
-
} catch {
|
|
1120
|
-
|
|
1284
|
+
} catch (e) {
|
|
1285
|
+
// network error = nothing listening on this port;
|
|
1286
|
+
// 404 = endpoint missing → host too old
|
|
1287
|
+
if (alive) setSt(e && e.httpStatus === 404 ? "old" : false);
|
|
1121
1288
|
}
|
|
1122
1289
|
})();
|
|
1123
1290
|
return () => {
|
|
1124
1291
|
alive = false;
|
|
1125
1292
|
};
|
|
1126
|
-
}, []);
|
|
1293
|
+
}, [port]);
|
|
1127
1294
|
|
|
1128
1295
|
// the overlay drags the ball, this page edits it — stay in sync both ways
|
|
1129
1296
|
useEffect(() => {
|
|
@@ -1134,7 +1301,7 @@ window.__ModuleLoader__.load({
|
|
|
1134
1301
|
|
|
1135
1302
|
async function patch(p) {
|
|
1136
1303
|
try {
|
|
1137
|
-
const r = await fetch(`http://127.0.0.1:${
|
|
1304
|
+
const r = await fetch(`http://127.0.0.1:${port}/app-settings`, {
|
|
1138
1305
|
method: "POST",
|
|
1139
1306
|
headers: { "content-type": "application/json" },
|
|
1140
1307
|
body: JSON.stringify(p),
|
|
@@ -1154,16 +1321,6 @@ window.__ModuleLoader__.load({
|
|
|
1154
1321
|
saveFabSt(next);
|
|
1155
1322
|
setFab(next || null);
|
|
1156
1323
|
}
|
|
1157
|
-
function savePortDraft() {
|
|
1158
|
-
const p = String(portDraft).trim() || "8791";
|
|
1159
|
-
setPortDraft(p);
|
|
1160
|
-
try {
|
|
1161
|
-
window.localStorage.setItem(PORT_KEY, p);
|
|
1162
|
-
window.dispatchEvent(new CustomEvent(PORT_EV));
|
|
1163
|
-
setFlash("端口已保存");
|
|
1164
|
-
setTimeout(() => setFlash(""), 1800);
|
|
1165
|
-
} catch {}
|
|
1166
|
-
}
|
|
1167
1324
|
|
|
1168
1325
|
const infoRow = (title, desc, key) =>
|
|
1169
1326
|
h(
|
|
@@ -1218,8 +1375,10 @@ window.__ModuleLoader__.load({
|
|
|
1218
1375
|
st === undefined
|
|
1219
1376
|
? h("div", { className: "lm-setsec-note", style: { padding: "12px 16px" }, key: "loading" }, "加载中…")
|
|
1220
1377
|
: st === false
|
|
1221
|
-
? h("div", { className: "lm-setsec-note", style: { padding: "12px 16px" }, key: "
|
|
1222
|
-
:
|
|
1378
|
+
? h("div", { className: "lm-setsec-note", style: { padding: "12px 16px" }, key: "unreach" }, `无法连接接收服务(127.0.0.1:${port})——可能服务未启动、端口被占用或插件配置了非默认端口。请确认电脑端 phone-lens 已随宿主启动。`)
|
|
1379
|
+
: st === "old"
|
|
1380
|
+
? h("div", { className: "lm-setsec-note", style: { padding: "12px 16px" }, key: "unavail" }, "设置服务不可用(需电脑端 phone-lens ≥ 1.0.4)")
|
|
1381
|
+
: [
|
|
1223
1382
|
h(LensSetRow, {
|
|
1224
1383
|
key: "t1",
|
|
1225
1384
|
title: "启用模型相机工具",
|
|
@@ -1230,18 +1389,11 @@ window.__ModuleLoader__.load({
|
|
|
1230
1389
|
title: "模型免确认调用",
|
|
1231
1390
|
desc: "开启时模型调用不弹确认框;关闭后每次调用需你在弹窗中确认(若宿主不支持审批弹窗,则视为拒绝)",
|
|
1232
1391
|
}, h(LensSwitch, { checked: st.modelToolsConfirmFree, onChange: (v) => void patch({ modelToolsConfirmFree: v }) })),
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
infoRow("调整/查询变焦(phone_camera_zoom)", "按指定倍数(或步进)拉近/拉远手机镜头;不带参数调用即查询当前倍数与支持范围。超出设备范围自动收拢。", "w4"),
|
|
1239
|
-
infoRow("切换/查询镜头(phone_camera_lens)", "切换手机前置/后置摄像头;不带参数调用即查询当前使用的镜头与设备可用镜头。需手机端 ≥ 1.1.3。", "w5"),
|
|
1240
|
-
h(
|
|
1241
|
-
"div",
|
|
1242
|
-
{ className: "lm-set-row", key: "ft" },
|
|
1243
|
-
h("span", { className: "lm-set-rowtext" }, h("span", { className: "lm-set-rowdesc" }, "高频的「拍照注入模式 / 保存目录(可浏览选择文件夹)」设置仍在悬浮窗 ⚙ 面板")),
|
|
1244
|
-
),
|
|
1392
|
+
h(LensSetRow, {
|
|
1393
|
+
key: "t3",
|
|
1394
|
+
title: "工作 (Beta)",
|
|
1395
|
+
desc: "允许手机 App 镜像并操作本机会话(流式/发送/停止等)。默认关闭;关闭时手机端工作入口不可用",
|
|
1396
|
+
}, h(LensSwitch, { checked: !!st.workBetaEnabled, onChange: (v) => void patch({ workBetaEnabled: v }) })),
|
|
1245
1397
|
],
|
|
1246
1398
|
),
|
|
1247
1399
|
),
|
|
@@ -1278,12 +1430,40 @@ window.__ModuleLoader__.load({
|
|
|
1278
1430
|
h(
|
|
1279
1431
|
"div",
|
|
1280
1432
|
{ className: "lm-setsec-rows" },
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1433
|
+
// read-only: the listen port is host-owned (plugin config
|
|
1434
|
+
// server.port). The browser only discovers it (/info) —
|
|
1435
|
+
// editing it here could never re-bind the host anyway.
|
|
1436
|
+
h(LensSetRow, { key: "c1", title: "接收端口", desc: "由电脑端插件配置决定(默认 8791),只读展示;端口被占用时接收端会自动尝试 8792~8800,此处显示的是实际生效端口。" },
|
|
1437
|
+
h("span", { style: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", fontSize: 13, opacity: 0.9 } }, `127.0.0.1:${port}`),
|
|
1284
1438
|
),
|
|
1285
1439
|
),
|
|
1286
1440
|
),
|
|
1441
|
+
// ── card: command reference (kept last per user-requested layout;
|
|
1442
|
+
// only rendered when the tool list is actually live) ──
|
|
1443
|
+
st && typeof st === "object"
|
|
1444
|
+
? h(
|
|
1445
|
+
"div",
|
|
1446
|
+
{ className: "lm-setsec-card", key: "usage" },
|
|
1447
|
+
h("div", { className: "lm-setsec-cardhead" }, "命令说明"),
|
|
1448
|
+
h(
|
|
1449
|
+
"div",
|
|
1450
|
+
{ className: "lm-setsec-rows" },
|
|
1451
|
+
// tool names in the open (user-requested): what the
|
|
1452
|
+
// model will actually see in its tool list
|
|
1453
|
+
infoRow("拍摄照片(phone_take_photo)", "手机立即拍照,照片直接进入模型视野——不放入输入框、不保存到电脑文件夹,与拍照注入模式设置无关。", "w1"),
|
|
1454
|
+
infoRow("关闭手机摄像头(phone_camera_pause)", "让手机相机提前进入空闲省电状态。", "w2"),
|
|
1455
|
+
infoRow("恢复手机摄像头(phone_camera_resume)", "立即打开相机并恢复预览拍摄。", "w3"),
|
|
1456
|
+
infoRow("调整/查询变焦(phone_camera_zoom)", "按指定倍数(或步进)拉近/拉远手机镜头;不带参数调用即查询当前倍数与支持范围。超出设备范围自动收拢。", "w4"),
|
|
1457
|
+
infoRow("切换/查询镜头(phone_camera_lens)", "切换手机前置/后置摄像头;不带参数调用即查询当前使用的镜头与设备可用镜头。需手机端 ≥ 1.1.3。", "w5"),
|
|
1458
|
+
infoRow("开关/查询手电筒(phone_camera_torch)", "打开/关闭手机闪光灯常亮照明,或查询当前状态;需相机处于开启状态。需手机端 ≥ 1.1.6。", "w6"),
|
|
1459
|
+
h(
|
|
1460
|
+
"div",
|
|
1461
|
+
{ className: "lm-set-row", key: "ft" },
|
|
1462
|
+
h("span", { className: "lm-set-rowtext" }, h("span", { className: "lm-set-rowdesc" }, "高频的「拍照注入模式 / 保存目录(可浏览选择文件夹)」设置仍在悬浮窗 ⚙ 面板")),
|
|
1463
|
+
),
|
|
1464
|
+
),
|
|
1465
|
+
)
|
|
1466
|
+
: null,
|
|
1287
1467
|
flash ? h("div", { className: "lm-setsec-note", key: "f" }, flash) : null,
|
|
1288
1468
|
);
|
|
1289
1469
|
}
|