dsh-pocket 1.13.6 → 1.14.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/README.en.md +7 -4
- package/README.md +7 -4
- package/client/api.js +1 -0
- package/client/client.js +60 -1
- package/client/index.jsx +46 -4
- package/client/pocket-locales.js +14 -0
- package/lib/index.js +4 -1
- package/lib/proxy.mjs +57 -4
- package/lib/service.mjs +4 -0
- package/lib/settings.mjs +14 -0
- package/lib/web-rpc.js +7 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -42,6 +42,7 @@ What it looks like — the phone shows the exact same UI as your computer, live:
|
|
|
42
42
|
| Feature | Description |
|
|
43
43
|
|---|---|
|
|
44
44
|
| 📶 LAN QR access | Works out of the box: Settings → Phone access — scan the LAN QR on the same Wi-Fi (auto-detects the LAN IP; **under WSL it picks the Windows host's physical NIC IP**) |
|
|
45
|
+
| 🚪 LAN switch | **Turn LAN access off/on with one click** in Settings (a confirmation dialog shows each time): off kills the LAN QR code and link instantly; public access is unaffected |
|
|
45
46
|
| 🌐 Public QR (from anywhere) | Click "Enable anywhere" → cloudflared tunnel → scan the public QR over 4G / any network |
|
|
46
47
|
| 🔐 Access PIN | Public links require an **8-digit PIN** (rotated on every tunnel start by default; **customizable to a fixed PIN** — custom PINs are not rotated); LAN has its own separate **8-digit PIN** (on by default; switchable off in Settings — then LAN scans connect directly) |
|
|
47
48
|
| 🔑 Custom PINs | Both the public and LAN PINs can be **set to your own fixed 8-digit number** in Settings (custom PINs are never auto-rotated) |
|
|
@@ -80,6 +81,8 @@ npx @deepseek-ai/dsh web
|
|
|
80
81
|
|
|
81
82
|
Settings → **Phone access** → scan the "📶 LAN" QR code → enter the **LAN PIN** (shown in the LAN block; hit **Refresh** to roll a new one, or **Customize** to set your own fixed 8 digits) → the phone opens the exact same DSH, in real time.
|
|
82
83
|
|
|
84
|
+
> The "**LAN access**" switch is **on by default** and can be **turned off/on with one click** (a confirmation dialog shows each time). Off kills the LAN QR code and link instantly (phones can't open them); **public access is unaffected**. Tap "On" to restore it.
|
|
85
|
+
>
|
|
83
86
|
> The LAN PIN is **on by default** (security-first). If you're the only user and find typing it every time annoying, flip "LAN access PIN" to **Off** in the LAN block — LAN scans then connect directly with no PIN (LAN-only devices; the **public tunnel always requires a PIN**, unaffected).
|
|
84
87
|
>
|
|
85
88
|
> After logging in once, the phone **won't ask again**: as long as the computer's dsh web keeps running, reopening the phone needs no PIN (**a dsh web restart/update asks for it once more**).
|
|
@@ -161,12 +164,12 @@ Such tools take over all traffic and often cut cloudflared's tunnel-edge connect
|
|
|
161
164
|
|
|
162
165
|
| File | Purpose |
|
|
163
166
|
|---|---|
|
|
164
|
-
| `lib/index.js` | Plugin entry: auto-start proxy + register RPC + access-PIN management (public: 8 digits rotated per tunnel start; LAN: separate 8 digits, manually refreshable / switchable) + DSH Desktop detection |
|
|
165
|
-
| `lib/settings.mjs` | Settings persistence: LAN-PIN switch (on by default) stored in `$DSH_HOME/dsh-pocket/settings.json` |
|
|
167
|
+
| `lib/index.js` | Plugin entry: auto-start proxy + register RPC + access-PIN management (public: 8 digits rotated per tunnel start; LAN: separate 8 digits, manually refreshable / switchable) + LAN access switch + DSH Desktop detection |
|
|
168
|
+
| `lib/settings.mjs` | Settings persistence: LAN access switch (on by default) + LAN-PIN switch (on by default) stored in `$DSH_HOME/dsh-pocket/settings.json` |
|
|
166
169
|
| `lib/service.mjs` | Service: proxy lifecycle (port auto-fallback), public tunnel (auto-restore), status snapshot (with QR data URLs) |
|
|
167
|
-
| `lib/proxy.mjs` | Header-rewriting reverse proxy: Host/Origin → loopback, HTTP + WebSocket passthrough + polyfill injection + gzip/brotli compression + per-host token auth (public always; LAN per switch) |
|
|
170
|
+
| `lib/proxy.mjs` | Header-rewriting reverse proxy: Host/Origin → loopback, HTTP + WebSocket passthrough + polyfill injection + gzip/brotli compression + per-host token auth (public always; LAN per switch) + blocks LAN Hosts when LAN is off |
|
|
168
171
|
| `lib/tunnel.mjs` | cloudflared: multi-mirror download (Tsinghua first) / adaptive parallel / start / parse public URL (HTTP/2) |
|
|
169
|
-
| `lib/web-rpc.js` | Loopback RPC: `status` / `tunnel.start` / `tunnel.stop` / `version` / `update` / `restart` |
|
|
172
|
+
| `lib/web-rpc.js` | Loopback RPC: `status` / `tunnel.start` / `tunnel.stop` / `lan.setEnabled` / `version` / `update` / `restart` |
|
|
170
173
|
| `client/` | "Phone access" settings tab + mobile adaptation (dsh-web-mobile port) |
|
|
171
174
|
| `bin/dsh-pocket.mjs` | CLI: LAN/public modes, prints URL + QR |
|
|
172
175
|
|
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ DSH Pocket 就是干这个的:**装上它,手机扫个码,就能实时看
|
|
|
42
42
|
| 特性 | 说明 |
|
|
43
43
|
|---|---|
|
|
44
44
|
| 📶 局域网扫码 | 装好即用:设置 → 手机访问,打开就有局域网二维码,手机连同一 WiFi 扫码即开(自动识别本机局域网 IP,**WSL 环境自动取 Windows 物理网卡 IP**) |
|
|
45
|
+
| 🚪 局域网开关 | 设置页可**一键关闭/开启局域网访问**(切换时弹窗提醒):关闭后局域网二维码/链接立即失效,仅公网可用 |
|
|
45
46
|
| 🌐 公网扫码(人在外面) | 点「开启公网访问」→ cloudflared 隧道 → 出公网二维码,4G/任何网络都能访问 |
|
|
46
47
|
| 🔐 访问密码 | 公网链接需输入 **8 位数字密码**(默认每次开启公网自动换新;**可自定义固定密码**——自定义后不再换新);局域网有独立 **8 位数字密码**(默认开启,设置页可**一键关闭**——关闭后局域网扫码直连) |
|
|
47
48
|
| 🔑 自定义密码 | 公网/局域网密码都可在设置页**设成自己固定的 8 位数字**(自定义后公网不再自动换新) |
|
|
@@ -80,6 +81,8 @@ npx @deepseek-ai/dsh web
|
|
|
80
81
|
|
|
81
82
|
设置 → **手机访问** → 手机扫「📶 局域网」二维码 → 打开链接**输入局域网密码**(显示在设置页局域网区块,点「刷新」可换新,或点「自定义」设成自己固定的 8 位数字)→ 打开的就是电脑上的 DSH,实时同步。
|
|
82
83
|
|
|
84
|
+
> 「**局域网访问**」开关默认**开**:可一键**关闭/开启**(切换时弹窗提醒)——关闭后局域网二维码/链接立即失效(手机打不开),**公网不受影响**;想恢复时再点「开」即可。
|
|
85
|
+
>
|
|
83
86
|
> 局域网密码**默认开启**(安全优先)。如果只有自己用、嫌每次输密码麻烦,可在设置页局域网区块把「局域网访问密码」切到**关**——之后局域网扫码直连、无需密码(仅同一局域网设备可访问;**公网始终要密码**,不受影响)。
|
|
84
87
|
>
|
|
85
88
|
> 手机登录一次后**长期免输**:只要电脑上的 dsh web 不重启,再次打开手机不用再输入(**dsh web 重启/更新后需重新输入一次**)。
|
|
@@ -162,12 +165,12 @@ npx @deepseek-ai/dsh web
|
|
|
162
165
|
|
|
163
166
|
| 文件 | 说明 |
|
|
164
167
|
|---|---|
|
|
165
|
-
| `lib/index.js` | 插件入口:自动起代理 + 注册 RPC + 访问密码管理(公网 8 位每次开启变新;局域网独立 8 位可手动刷新/开关)+ 桌面端环境适配 |
|
|
166
|
-
| `lib/settings.mjs` |
|
|
168
|
+
| `lib/index.js` | 插件入口:自动起代理 + 注册 RPC + 访问密码管理(公网 8 位每次开启变新;局域网独立 8 位可手动刷新/开关)+ 局域网访问总开关 + 桌面端环境适配 |
|
|
169
|
+
| `lib/settings.mjs` | 设置持久化:局域网访问总开关(默认开启)+ 局域网密码开关(默认开启)存 `$DSH_HOME/dsh-pocket/settings.json` |
|
|
167
170
|
| `lib/service.mjs` | 服务:代理生命周期(端口自适应)、公网隧道(自动恢复)、状态快照(含二维码) |
|
|
168
|
-
| `lib/proxy.mjs` | 改头反向代理:Host/Origin → loopback,HTTP + WebSocket 透传 + polyfill 注入 + gzip/brotli 压缩 + 按 Host
|
|
171
|
+
| `lib/proxy.mjs` | 改头反向代理:Host/Origin → loopback,HTTP + WebSocket 透传 + polyfill 注入 + gzip/brotli 压缩 + 按 Host 区分的访问令牌认证(公网必验;局域网按开关)+ 局域网关闭时拦截局域网 Host |
|
|
169
172
|
| `lib/tunnel.mjs` | cloudflared:多镜像源下载(清华优先)/自适应多线程/启动/解析公网 URL(HTTP/2) |
|
|
170
|
-
| `lib/web-rpc.js` | loopback RPC:`status` / `tunnel.start` / `tunnel.stop` / `version` / `update` / `restart` |
|
|
173
|
+
| `lib/web-rpc.js` | loopback RPC:`status` / `tunnel.start` / `tunnel.stop` / `lan.setEnabled` / `version` / `update` / `restart` |
|
|
171
174
|
| `client/` | 设置页「手机访问」+ 移动端适配(dsh-web-mobile 移植) |
|
|
172
175
|
| `bin/dsh-pocket.mjs` | CLI:局域网/公网模式,打印 URL + 二维码 |
|
|
173
176
|
|
package/client/api.js
CHANGED
package/client/client.js
CHANGED
|
@@ -51,6 +51,7 @@ var POCKET_ENDPOINTS = Object.freeze({
|
|
|
51
51
|
lanTokenRefresh: "token.lanRefresh",
|
|
52
52
|
lanAuthSetEnabled: "lanAuth.setEnabled",
|
|
53
53
|
lanSetOverride: "lan.setOverride",
|
|
54
|
+
lanSetEnabled: "lan.setEnabled",
|
|
54
55
|
pinSetCustom: "pin.setCustom"
|
|
55
56
|
});
|
|
56
57
|
function compareVersions(a, b) {
|
|
@@ -1364,6 +1365,13 @@ var zh2 = {
|
|
|
1364
1365
|
"versionRange": "\u5F53\u524D v{cur} \u2192 \u6700\u65B0 v{latest}",
|
|
1365
1366
|
"lanTitle": "\u{1F4F6} \u5C40\u57DF\u7F51\uFF08\u540C\u4E00 WiFi\uFF09",
|
|
1366
1367
|
"lanHint": "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00",
|
|
1368
|
+
"lanAccess": "\u5C40\u57DF\u7F51\u8BBF\u95EE",
|
|
1369
|
+
"lanDisabledHint": "\u{1F512} \u5C40\u57DF\u7F51\u8BBF\u95EE\u5DF2\u5173\u95ED\uFF1A\u624B\u673A\u626B\u7801/\u94FE\u63A5\u5747\u4E0D\u53EF\u7528\uFF08\u516C\u7F51\u4E0D\u53D7\u5F71\u54CD\uFF09\u3002\u70B9\u300C\u5F00\u300D\u6062\u590D\u3002",
|
|
1370
|
+
"lanToggleTitleOff": "\u5173\u95ED\u5C40\u57DF\u7F51\u8BBF\u95EE",
|
|
1371
|
+
"lanToggleBodyOff": "\u5173\u95ED\u540E\uFF0C\u540C\u4E00 WiFi \u4E0B\u7684\u624B\u673A\u5C06\u65E0\u6CD5\u626B\u7801\u8BBF\u95EE\uFF08\u5C40\u57DF\u7F51\u4E8C\u7EF4\u7801/\u94FE\u63A5\u7ACB\u5373\u5931\u6548\uFF09\u3002\u516C\u7F51\u8BBF\u95EE\u4E0D\u53D7\u5F71\u54CD\u3002\u786E\u5B9A\u5173\u95ED\uFF1F",
|
|
1372
|
+
"lanToggleTitleOn": "\u5F00\u542F\u5C40\u57DF\u7F51\u8BBF\u95EE",
|
|
1373
|
+
"lanToggleBodyOn": "\u5F00\u542F\u540E\uFF0C\u540C\u4E00 WiFi \u7684\u624B\u673A\u626B\u7801\u5373\u53EF\u8BBF\u95EE\uFF08\u9ED8\u8BA4\u9700\u8F93\u5165\u5C40\u57DF\u7F51\u5BC6\u7801\uFF09\u3002\u786E\u5B9A\u5F00\u542F\uFF1F",
|
|
1374
|
+
"confirm": "\u786E\u5B9A",
|
|
1367
1375
|
"lanAddress": "\u5C40\u57DF\u7F51\u5730\u5740",
|
|
1368
1376
|
"lanAddressAuto": "\u81EA\u52A8\uFF08\u63A8\u8350\uFF09",
|
|
1369
1377
|
"lanAddressHint": "\u9AD8\u7EA7\u9009\u9879\uFF1A\u4E00\u822C\u4E0D\u9700\u8981\u4FEE\u6539\uFF1B\u4F7F\u7528 Tailscale/VPN \u7B49\u8FDC\u7A0B\u8BBF\u95EE\u65F6\u53EF\u624B\u52A8\u9009\u62E9",
|
|
@@ -1426,6 +1434,13 @@ var en2 = {
|
|
|
1426
1434
|
"versionRange": "Current v{cur} \u2192 latest v{latest}",
|
|
1427
1435
|
"lanTitle": "\u{1F4F6} LAN (same Wi-Fi)",
|
|
1428
1436
|
"lanHint": "Scan to open once your phone is on the same Wi-Fi",
|
|
1437
|
+
"lanAccess": "LAN access",
|
|
1438
|
+
"lanDisabledHint": '\u{1F512} LAN access is off \u2014 the QR code and link are unavailable (public access is unaffected). Tap "On" to restore.',
|
|
1439
|
+
"lanToggleTitleOff": "Turn off LAN access",
|
|
1440
|
+
"lanToggleBodyOff": "Once off, phones on the same Wi-Fi can no longer scan to connect (the LAN QR code and link stop working immediately). Public access is unaffected. Turn it off?",
|
|
1441
|
+
"lanToggleTitleOn": "Turn on LAN access",
|
|
1442
|
+
"lanToggleBodyOn": "Once on, phones on the same Wi-Fi can scan to connect (a LAN PIN is required by default). Turn it on?",
|
|
1443
|
+
"confirm": "Confirm",
|
|
1429
1444
|
"lanAddress": "LAN address",
|
|
1430
1445
|
"lanAddressAuto": "Auto (recommended)",
|
|
1431
1446
|
"lanAddressHint": "Advanced option: usually no change needed; select manually when accessing through Tailscale/VPN",
|
|
@@ -1637,6 +1652,19 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
1637
1652
|
} catch {
|
|
1638
1653
|
}
|
|
1639
1654
|
};
|
|
1655
|
+
const [lanToggleOpen, setLanToggleOpen] = (0, import_react2.useState)(null);
|
|
1656
|
+
const requestLanToggle = (on) => setLanToggleOpen(on);
|
|
1657
|
+
const confirmLanToggle = async () => {
|
|
1658
|
+
const on = lanToggleOpen;
|
|
1659
|
+
setLanToggleOpen(null);
|
|
1660
|
+
if (on === null) return;
|
|
1661
|
+
try {
|
|
1662
|
+
const r = await call(POCKET_ENDPOINTS.lanSetEnabled, { on });
|
|
1663
|
+
setStatus((s) => ({ ...s, lanEnabled: r.lanEnabled }));
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
setError(err.message);
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1640
1668
|
const setLanAddress = async (ip) => {
|
|
1641
1669
|
try {
|
|
1642
1670
|
setStatus(await call(POCKET_ENDPOINTS.lanSetOverride, { ip }));
|
|
@@ -1751,7 +1779,21 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
1751
1779
|
"div",
|
|
1752
1780
|
{ style: styles.block },
|
|
1753
1781
|
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("lanTitle")),
|
|
1754
|
-
|
|
1782
|
+
// 局域网访问总开关:关闭后扫码/链接直接失效(公网不受影响)
|
|
1783
|
+
(0, import_react2.createElement)(
|
|
1784
|
+
"div",
|
|
1785
|
+
{ style: { display: "flex", alignItems: "center", gap: 8, marginTop: 8 } },
|
|
1786
|
+
(0, import_react2.createElement)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } }, t("lanAccess")),
|
|
1787
|
+
(0, import_react2.createElement)("button", {
|
|
1788
|
+
style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, fontWeight: status?.lanEnabled !== false ? 600 : 400, background: status?.lanEnabled !== false ? "var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))" : "var(--dsw-alias-bg-layer-1,#fff)", color: status?.lanEnabled !== false ? "var(--dsw-alias-label-primary-foreground, #fff)" : "var(--dsw-alias-label-primary,inherit)" },
|
|
1789
|
+
onClick: () => requestLanToggle(true)
|
|
1790
|
+
}, t("on")),
|
|
1791
|
+
(0, import_react2.createElement)("button", {
|
|
1792
|
+
style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, fontWeight: status?.lanEnabled === false ? 600 : 400, background: status?.lanEnabled === false ? "var(--dsw-alias-state-error-primary,#dc2626)" : "var(--dsw-alias-bg-layer-1,#fff)", color: status?.lanEnabled === false ? "#fff" : "var(--dsw-alias-label-primary,inherit)" },
|
|
1793
|
+
onClick: () => requestLanToggle(false)
|
|
1794
|
+
}, t("off"))
|
|
1795
|
+
),
|
|
1796
|
+
status?.lanEnabled === false ? (0, import_react2.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("lanDisabledHint")) : lanUrl ? (0, import_react2.createElement)(
|
|
1755
1797
|
"div",
|
|
1756
1798
|
null,
|
|
1757
1799
|
(0, import_react2.createElement)("img", { src: status.lanQr, alt: "LAN QR", style: styles.qr }),
|
|
@@ -1835,6 +1877,23 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
1835
1877
|
)
|
|
1836
1878
|
),
|
|
1837
1879
|
error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${error}`) : null,
|
|
1880
|
+
// 局域网访问开关确认弹框(关闭/打开时弹窗提醒)
|
|
1881
|
+
lanToggleOpen !== null ? (0, import_react2.createElement)(
|
|
1882
|
+
"div",
|
|
1883
|
+
{ style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
|
|
1884
|
+
(0, import_react2.createElement)(
|
|
1885
|
+
"div",
|
|
1886
|
+
{ style: { background: "var(--dsw-alias-bg-layer-1,#fff)", borderRadius: 12, maxWidth: 420, width: "100%", padding: "20px 22px", boxShadow: "0 8px 32px rgba(0,0,0,.18)" } },
|
|
1887
|
+
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: lanToggleOpen ? "var(--dsw-alias-brand-primary,#4f6ef7)" : "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t(lanToggleOpen ? "lanToggleTitleOn" : "lanToggleTitleOff")),
|
|
1888
|
+
(0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t(lanToggleOpen ? "lanToggleBodyOn" : "lanToggleBodyOff")),
|
|
1889
|
+
(0, import_react2.createElement)(
|
|
1890
|
+
"div",
|
|
1891
|
+
{ style: { display: "flex", gap: 8, marginTop: 16 } },
|
|
1892
|
+
(0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setLanToggleOpen(null) }, t("cancel")),
|
|
1893
|
+
(0, import_react2.createElement)("button", { style: { ...styles.primary, flex: 1 }, onClick: confirmLanToggle }, t("confirm"))
|
|
1894
|
+
)
|
|
1895
|
+
)
|
|
1896
|
+
) : null,
|
|
1838
1897
|
// 安全免责声明弹框(issue #31):每次开启公网访问前确认
|
|
1839
1898
|
disclaimerOpen ? (0, import_react2.createElement)(
|
|
1840
1899
|
"div",
|
package/client/index.jsx
CHANGED
|
@@ -211,6 +211,22 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
211
211
|
} catch { /* 忽略 */ }
|
|
212
212
|
};
|
|
213
213
|
|
|
214
|
+
// 局域网访问总开关:关闭后局域网扫码/链接直接失效(公网不受影响)。
|
|
215
|
+
// 切换前弹窗确认(弹窗提醒);服务端用 setLanEnabled 持久化,代理按 Host 实时拦截。
|
|
216
|
+
const [lanToggleOpen, setLanToggleOpen] = useState(null); // null | true | false(目标 on 状态)
|
|
217
|
+
const requestLanToggle = (on) => setLanToggleOpen(on);
|
|
218
|
+
const confirmLanToggle = async () => {
|
|
219
|
+
const on = lanToggleOpen;
|
|
220
|
+
setLanToggleOpen(null);
|
|
221
|
+
if (on === null) return;
|
|
222
|
+
try {
|
|
223
|
+
const r = await call(POCKET_ENDPOINTS.lanSetEnabled, { on });
|
|
224
|
+
setStatus((s) => ({ ...s, lanEnabled: r.lanEnabled }));
|
|
225
|
+
} catch (err) {
|
|
226
|
+
setError(err.message);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
214
230
|
// 局域网地址手动覆盖(Tailscale/VPN 等远程访问场景):空值恢复自动选择
|
|
215
231
|
const setLanAddress = async (ip) => {
|
|
216
232
|
try {
|
|
@@ -321,8 +337,22 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
321
337
|
// 局域网
|
|
322
338
|
h('div', { style: styles.block },
|
|
323
339
|
h('div', { style: { fontWeight: 600, fontSize: 13 } }, t('lanTitle')),
|
|
324
|
-
|
|
325
|
-
|
|
340
|
+
// 局域网访问总开关:关闭后扫码/链接直接失效(公网不受影响)
|
|
341
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 } },
|
|
342
|
+
h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } }, t('lanAccess')),
|
|
343
|
+
h('button', {
|
|
344
|
+
style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, fontWeight: status?.lanEnabled !== false ? 600 : 400, background: status?.lanEnabled !== false ? 'var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))' : 'var(--dsw-alias-bg-layer-1,#fff)', color: status?.lanEnabled !== false ? 'var(--dsw-alias-label-primary-foreground, #fff)' : 'var(--dsw-alias-label-primary,inherit)' },
|
|
345
|
+
onClick: () => requestLanToggle(true),
|
|
346
|
+
}, t('on')),
|
|
347
|
+
h('button', {
|
|
348
|
+
style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, fontWeight: status?.lanEnabled === false ? 600 : 400, background: status?.lanEnabled === false ? 'var(--dsw-alias-state-error-primary,#dc2626)' : 'var(--dsw-alias-bg-layer-1,#fff)', color: status?.lanEnabled === false ? '#fff' : 'var(--dsw-alias-label-primary,inherit)' },
|
|
349
|
+
onClick: () => requestLanToggle(false),
|
|
350
|
+
}, t('off')),
|
|
351
|
+
),
|
|
352
|
+
status?.lanEnabled === false
|
|
353
|
+
? h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#b45309)', lineHeight: 1.5 } }, t('lanDisabledHint'))
|
|
354
|
+
: (lanUrl
|
|
355
|
+
? h('div', null,
|
|
326
356
|
h('img', { src: status.lanQr, alt: 'LAN QR', style: styles.qr }),
|
|
327
357
|
h('div', { style: styles.code }, lanUrl),
|
|
328
358
|
h('div', { style: styles.muted }, t('lanHint')),
|
|
@@ -360,8 +390,8 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
360
390
|
))
|
|
361
391
|
: h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#b45309)', lineHeight: 1.5 } },
|
|
362
392
|
t('lanPinOff')),
|
|
363
|
-
|
|
364
|
-
|
|
393
|
+
)
|
|
394
|
+
: h('div', { style: styles.muted }, t('lanStarting'))),
|
|
365
395
|
),
|
|
366
396
|
|
|
367
397
|
// 公网
|
|
@@ -399,6 +429,18 @@ function PocketSettingsTab({ rpcCall, t }) {
|
|
|
399
429
|
|
|
400
430
|
error ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 12, marginTop: 8 } }, `❌ ${error}`) : null,
|
|
401
431
|
|
|
432
|
+
// 局域网访问开关确认弹框(关闭/打开时弹窗提醒)
|
|
433
|
+
lanToggleOpen !== null ? h('div', { style: { position: 'fixed', inset: 0, zIndex: 10000, background: 'rgba(0,0,0,.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 } },
|
|
434
|
+
h('div', { style: { background: 'var(--dsw-alias-bg-layer-1,#fff)', borderRadius: 12, maxWidth: 420, width: '100%', padding: '20px 22px', boxShadow: '0 8px 32px rgba(0,0,0,.18)' } },
|
|
435
|
+
h('div', { style: { fontWeight: 600, fontSize: 15, color: lanToggleOpen ? 'var(--dsw-alias-brand-primary,#4f6ef7)' : 'var(--dsw-alias-state-warn-primary,#b45309)', marginBottom: 10 } }, t(lanToggleOpen ? 'lanToggleTitleOn' : 'lanToggleTitleOff')),
|
|
436
|
+
h('div', { style: { fontSize: 13, lineHeight: 1.7, color: 'var(--dsw-alias-label-primary,inherit)' } }, t(lanToggleOpen ? 'lanToggleBodyOn' : 'lanToggleBodyOff')),
|
|
437
|
+
h('div', { style: { display: 'flex', gap: 8, marginTop: 16 } },
|
|
438
|
+
h('button', { style: { ...styles.btn, flex: 1 }, onClick: () => setLanToggleOpen(null) }, t('cancel')),
|
|
439
|
+
h('button', { style: { ...styles.primary, flex: 1 }, onClick: confirmLanToggle }, t('confirm')),
|
|
440
|
+
),
|
|
441
|
+
),
|
|
442
|
+
) : null,
|
|
443
|
+
|
|
402
444
|
// 安全免责声明弹框(issue #31):每次开启公网访问前确认
|
|
403
445
|
disclaimerOpen ? h('div', { style: { position: 'fixed', inset: 0, zIndex: 10000, background: 'rgba(0,0,0,.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 } },
|
|
404
446
|
h('div', { style: { background: 'var(--dsw-alias-bg-layer-1,#fff)', borderRadius: 12, maxWidth: 420, width: '100%', padding: '20px 22px', boxShadow: '0 8px 32px rgba(0,0,0,.18)' } },
|
package/client/pocket-locales.js
CHANGED
|
@@ -30,6 +30,13 @@ export const zh = {
|
|
|
30
30
|
'versionRange': '当前 v{cur} → 最新 v{latest}',
|
|
31
31
|
'lanTitle': '📶 局域网(同一 WiFi)',
|
|
32
32
|
'lanHint': '手机连接同一 WiFi 后扫码即可打开',
|
|
33
|
+
'lanAccess': '局域网访问',
|
|
34
|
+
'lanDisabledHint': '🔒 局域网访问已关闭:手机扫码/链接均不可用(公网不受影响)。点「开」恢复。',
|
|
35
|
+
'lanToggleTitleOff': '关闭局域网访问',
|
|
36
|
+
'lanToggleBodyOff': '关闭后,同一 WiFi 下的手机将无法扫码访问(局域网二维码/链接立即失效)。公网访问不受影响。确定关闭?',
|
|
37
|
+
'lanToggleTitleOn': '开启局域网访问',
|
|
38
|
+
'lanToggleBodyOn': '开启后,同一 WiFi 的手机扫码即可访问(默认需输入局域网密码)。确定开启?',
|
|
39
|
+
'confirm': '确定',
|
|
33
40
|
'lanAddress': '局域网地址',
|
|
34
41
|
'lanAddressAuto': '自动(推荐)',
|
|
35
42
|
'lanAddressHint': '高级选项:一般不需要修改;使用 Tailscale/VPN 等远程访问时可手动选择',
|
|
@@ -94,6 +101,13 @@ export const en = {
|
|
|
94
101
|
'versionRange': 'Current v{cur} → latest v{latest}',
|
|
95
102
|
'lanTitle': '📶 LAN (same Wi-Fi)',
|
|
96
103
|
'lanHint': 'Scan to open once your phone is on the same Wi-Fi',
|
|
104
|
+
'lanAccess': 'LAN access',
|
|
105
|
+
'lanDisabledHint': '🔒 LAN access is off — the QR code and link are unavailable (public access is unaffected). Tap "On" to restore.',
|
|
106
|
+
'lanToggleTitleOff': 'Turn off LAN access',
|
|
107
|
+
'lanToggleBodyOff': 'Once off, phones on the same Wi-Fi can no longer scan to connect (the LAN QR code and link stop working immediately). Public access is unaffected. Turn it off?',
|
|
108
|
+
'lanToggleTitleOn': 'Turn on LAN access',
|
|
109
|
+
'lanToggleBodyOn': 'Once on, phones on the same Wi-Fi can scan to connect (a LAN PIN is required by default). Turn it on?',
|
|
110
|
+
'confirm': 'Confirm',
|
|
97
111
|
'lanAddress': 'LAN address',
|
|
98
112
|
'lanAddressAuto': 'Auto (recommended)',
|
|
99
113
|
'lanAddressHint': 'Advanced option: usually no change needed; select manually when accessing through Tailscale/VPN',
|
package/lib/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import { createPocketService } from './service.mjs';
|
|
|
21
21
|
import { installPocketRpc } from './web-rpc.js';
|
|
22
22
|
import { restartHost } from './restart.js';
|
|
23
23
|
import { desktopEnvPatchScript, advancedNoticeScript, DEFAULT_INJECT } from './proxy.mjs';
|
|
24
|
-
import { lanAuthEnabled, setLanAuthEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom } from './settings.mjs';
|
|
24
|
+
import { lanEnabled, setLanEnabled, lanAuthEnabled, setLanAuthEnabled, lanIpOverride, setLanIpOverride, pinCustom, setPinCustom } from './settings.mjs';
|
|
25
25
|
|
|
26
26
|
const name = 'dsh-pocket';
|
|
27
27
|
const inject = ['connection', 'webServer'];
|
|
@@ -226,6 +226,7 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
226
226
|
home: internals.home,
|
|
227
227
|
internals,
|
|
228
228
|
getLanIpOverride: () => lanIpOverride(),
|
|
229
|
+
getLanEnabled: () => lanEnabled(),
|
|
229
230
|
// 桌面端:手机扫码访问的页面缺 dsh-desktop-mode/platform 参数会让 dsh-plugin-desktop
|
|
230
231
|
// client 崩溃(issue #3/#4)。给代理注入「桌面参数补丁」(history.replaceState 补齐参数,
|
|
231
232
|
// 无跳转;compatibility 模式不套桌面布局,避免移动端按钮叠加)。保留默认 polyfill。
|
|
@@ -259,6 +260,8 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
259
260
|
refreshLanToken: () => refreshLanToken(),
|
|
260
261
|
getLanAuthEnabled: () => lanAuthEnabled(),
|
|
261
262
|
setLanAuthEnabled: (on) => setLanAuthEnabled(on),
|
|
263
|
+
getLanEnabled: () => lanEnabled(),
|
|
264
|
+
setLanEnabled: (on) => setLanEnabled(on),
|
|
262
265
|
getLanIpOverride: () => lanIpOverride(),
|
|
263
266
|
setLanIpOverride: (ip) => setLanIpOverride(ip),
|
|
264
267
|
getPinCustom: (which) => pinCustom(which),
|
package/lib/proxy.mjs
CHANGED
|
@@ -187,6 +187,39 @@ function isProtectedHost(host, isProtected) {
|
|
|
187
187
|
return isProtected ? isProtected(host) : /trycloudflare\.com$/i.test(String(host ?? ''));
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* 该 Host 是否 loopback(本机 / cloudflared 回环)。
|
|
192
|
+
* 「关闭局域网」只拦截经局域网 IP/主机名访问的请求,loopback 与公网(trycloudflare)放行:
|
|
193
|
+
* - cloudflared 隧道以 `http://127.0.0.1:<port>` 回连本机代理,必须放行;
|
|
194
|
+
* - 电脑自己访问 127.0.0.1/localhost 也应放行(仅当用户手动浏览本代理时)。
|
|
195
|
+
*/
|
|
196
|
+
function isLoopbackHost(host) {
|
|
197
|
+
let name = String(host ?? '').trim().toLowerCase();
|
|
198
|
+
if (name.startsWith('[')) {
|
|
199
|
+
const end = name.indexOf(']');
|
|
200
|
+
if (end >= 0) name = name.slice(1, end); // [::1]:port → ::1
|
|
201
|
+
} else {
|
|
202
|
+
name = name.replace(/:\d+$/, ''); // hostname:port / 127.0.0.1:port → 去掉端口
|
|
203
|
+
}
|
|
204
|
+
return name === 'localhost' || name === '127.0.0.1' || name === '::1' || name === '0.0.0.0';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 局域网访问已关闭时的提示页(浏览器导航时显示;API/WS 返回 403 JSON/拒绝握手)。 */
|
|
208
|
+
function lanDisabledPageHtml() {
|
|
209
|
+
return `<!doctype html><html lang="zh"><head><meta charset="utf-8">
|
|
210
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
211
|
+
<title>DSH Pocket · 局域网访问已关闭</title>
|
|
212
|
+
<style>
|
|
213
|
+
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f3f4f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
214
|
+
.card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:28px 24px;max-width:360px;width:calc(100% - 48px);text-align:center}
|
|
215
|
+
h1{font-size:16px;margin:0 0 8px;color:#111827}
|
|
216
|
+
p{font-size:13px;color:#6b7280;margin:0;line-height:1.6}
|
|
217
|
+
</style></head><body><div class="card">
|
|
218
|
+
<h1>🔒 DSH Pocket</h1>
|
|
219
|
+
<p>局域网访问已关闭,扫码/链接均不可用。<br>请在电脑上重新开启后再试。<br><br>LAN access is disabled — the QR code and link are unavailable.<br>Re-enable it on the computer to continue.</p>
|
|
220
|
+
</div></body></html>`;
|
|
221
|
+
}
|
|
222
|
+
|
|
190
223
|
/** 请求是否期望 HTML(浏览器导航 → 返回登录页;API/WS → 401)。 */
|
|
191
224
|
function isHtmlRequest(req) {
|
|
192
225
|
const accept = String(req.headers.accept ?? '');
|
|
@@ -278,15 +311,28 @@ function attachWebSocketHeartbeat(socket, { intervalMs = 30_000, missLimit = 2 }
|
|
|
278
311
|
* @param {object} [opts.auth] 可选访问令牌认证(issue #13):{ getToken, isProtected, sessionKey }
|
|
279
312
|
* @param {object|false} [opts.rateLimit] 登录速率限制参数覆盖(issue #40;测试用短窗口)
|
|
280
313
|
* @param {object|false} [opts.heartbeat] WebSocket 心跳注入(PR #41):{ intervalMs, missLimit };false 关闭(默认开:30s/容忍 2 个静默周期)
|
|
314
|
+
* @param {() => boolean} [opts.lanAccessEnabled] 局域网访问是否开启(默认开启)。关闭时拦截经局域网 Host 的请求(公网/loopback 不受影响)。
|
|
281
315
|
* @returns {Promise<{server:import('node:http').Server, close:()=>Promise<void>}>}
|
|
282
316
|
*/
|
|
283
|
-
export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DEFAULT_UPSTREAM, log = null, injectHtml = DEFAULT_INJECT, auth = null, rateLimit = null, heartbeat = {} } = {}) {
|
|
317
|
+
export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DEFAULT_UPSTREAM, log = null, injectHtml = DEFAULT_INJECT, auth = null, rateLimit = null, heartbeat = {}, lanAccessEnabled = () => true } = {}) {
|
|
284
318
|
const limiter = auth ? createRateLimiter(rateLimit ?? {}) : null;
|
|
285
319
|
const server = createServer((req, res) => {
|
|
320
|
+
const host = String(req.headers.host ?? '');
|
|
321
|
+
const isPublic = /trycloudflare\.com$/i.test(host);
|
|
322
|
+
// 局域网访问关闭(issue #54):拦截经局域网 IP/主机名访问的请求;
|
|
323
|
+
// 公网(trycloudflare)与 loopback(本机/cloudflared 回连)放行。
|
|
324
|
+
if (!isPublic && !isLoopbackHost(host) && !lanAccessEnabled()) {
|
|
325
|
+
if (isHtmlRequest(req)) {
|
|
326
|
+
res.writeHead(403, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
327
|
+
res.end(lanDisabledPageHtml());
|
|
328
|
+
} else {
|
|
329
|
+
res.writeHead(403, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
330
|
+
res.end('{"error":"lan-disabled"}');
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
286
334
|
// 访问令牌认证(issue #13 + #18 + #33 + #40):局域网与公网按开关/来源要求密码
|
|
287
335
|
if (auth) {
|
|
288
|
-
const host = String(req.headers.host ?? '');
|
|
289
|
-
const isPublic = /trycloudflare\.com$/i.test(host);
|
|
290
336
|
const protectedHost = isProtectedHost(host, auth.isProtected);
|
|
291
337
|
const token = protectedHost ? (auth.getToken?.(host) ?? null) : null;
|
|
292
338
|
const sessionKey = auth.sessionKey ?? null;
|
|
@@ -419,9 +465,16 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
419
465
|
|
|
420
466
|
// WebSocket upgrade(DSH 的 /api/events.mux + events.host 流式通道)原样透传
|
|
421
467
|
server.on('upgrade', (req, socket, head) => {
|
|
468
|
+
const host = String(req.headers.host ?? '');
|
|
469
|
+
const isPublic = /trycloudflare\.com$/i.test(host);
|
|
470
|
+
// 局域网访问关闭:拦截经局域网 Host 的 WS 握手(公网/loopback 放行)
|
|
471
|
+
if (!isPublic && !isLoopbackHost(host) && !lanAccessEnabled()) {
|
|
472
|
+
socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
473
|
+
socket.destroy();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
422
476
|
// WebSocket 同样校验(防止绕过 HTTP 认证从 WS 进入)
|
|
423
477
|
if (auth) {
|
|
424
|
-
const host = String(req.headers.host ?? '');
|
|
425
478
|
const token = isProtectedHost(host, auth.isProtected) ? (auth.getToken?.(host) ?? null) : null;
|
|
426
479
|
if (token && !authCheck(req, token, auth.sessionKey ?? null)) {
|
|
427
480
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
package/lib/service.mjs
CHANGED
|
@@ -172,6 +172,8 @@ export function createPocketService({
|
|
|
172
172
|
internals = {},
|
|
173
173
|
/** 局域网地址手动覆盖:返回 IPv4 字符串;空值表示自动选择 */
|
|
174
174
|
getLanIpOverride,
|
|
175
|
+
/** 局域网访问总开关:返回 boolean;默认开启。关闭后代理拒绝局域网 Host 请求 */
|
|
176
|
+
getLanEnabled = () => true,
|
|
175
177
|
/** 代理注入 HTML 的内容(桌面端补丁等由 lib/index.js 传入;默认 randomUUID polyfill) */
|
|
176
178
|
injectHtml,
|
|
177
179
|
/** 访问令牌认证配置(issue #13):{ getToken, isProtected },传给代理 */
|
|
@@ -248,6 +250,8 @@ export function createPocketService({
|
|
|
248
250
|
upstream: { host: '127.0.0.1', port: dshPort },
|
|
249
251
|
...(injectHtml ? { injectHtml } : {}),
|
|
250
252
|
...(auth ? { auth } : {}),
|
|
253
|
+
// 每次请求实时读开关:设置页切换后立即生效,无需重启代理
|
|
254
|
+
lanAccessEnabled: () => getLanEnabled(),
|
|
251
255
|
});
|
|
252
256
|
if (p !== port) {
|
|
253
257
|
console.log(`dsh-pocket: port ${port} busy, proxy on ${p} | 端口 ${port} 被占用,代理改用 ${p}`);
|
package/lib/settings.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// dsh-pocket 设置持久化($DSH_HOME/dsh-pocket/settings.json)
|
|
2
2
|
//
|
|
3
3
|
// 当前项:
|
|
4
|
+
// - lanEnabled 局域网访问总开关(默认开启):关闭后局域网扫码/链接直接失效(代理拒绝局域网 Host)
|
|
4
5
|
// - lanAuthEnabled 局域网访问密码开关(issue #24),默认开启
|
|
5
6
|
// - publicPinCustom 公网密码是否用户自定义(issue #33),自定义后不自动轮换
|
|
6
7
|
// - lanPinCustom 局域网密码是否用户自定义(issue #33)
|
|
@@ -33,6 +34,19 @@ function writeSettings(s) {
|
|
|
33
34
|
return s;
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
/** 局域网访问总开关:默认开启(文件缺失/损坏都视为开启)。 */
|
|
38
|
+
export function lanEnabled() {
|
|
39
|
+
return readSettings().lanEnabled !== false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 设置局域网访问总开关,返回新状态(持久化)。 */
|
|
43
|
+
export function setLanEnabled(on) {
|
|
44
|
+
const s = readSettings();
|
|
45
|
+
s.lanEnabled = !!on;
|
|
46
|
+
writeSettings(s);
|
|
47
|
+
return s.lanEnabled;
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
/** 局域网访问密码开关:默认开启(文件缺失/损坏都视为开启)。 */
|
|
37
51
|
export function lanAuthEnabled() {
|
|
38
52
|
return readSettings().lanAuthEnabled !== false;
|
package/lib/web-rpc.js
CHANGED
|
@@ -25,7 +25,7 @@ export function killHint(port) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** 注册 /dsh-pocket 逻辑通道(仅本机 loopback 可调)。 */
|
|
28
|
-
export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null, getLanToken = null, refreshLanToken = null, getLanAuthEnabled = null, setLanAuthEnabled = null, getLanIpOverride = null, setLanIpOverride = null, getPinCustom = null, setCustomPin = null }) {
|
|
28
|
+
export function installPocketRpc(ctx, { service, log = console, desktop = false, runUpdate = null, restart = null, restartNotice = null, getToken = null, getLanToken = null, refreshLanToken = null, getLanAuthEnabled = null, setLanAuthEnabled = null, getLanEnabled = null, setLanEnabled = null, getLanIpOverride = null, setLanIpOverride = null, getPinCustom = null, setCustomPin = null }) {
|
|
29
29
|
if (!ctx?.connection?.rpc?.handle) {
|
|
30
30
|
log.warn?.('dsh-pocket: DSH Host Connection RPC unavailable — settings tab disabled | 无 Connection RPC,设置页不可用');
|
|
31
31
|
return () => {};
|
|
@@ -46,6 +46,7 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
|
|
|
46
46
|
accessToken: getToken?.() ?? null,
|
|
47
47
|
lanToken: getLanToken?.() ?? null,
|
|
48
48
|
lanAuthEnabled: getLanAuthEnabled?.() ?? true,
|
|
49
|
+
lanEnabled: getLanEnabled?.() ?? true,
|
|
49
50
|
publicPinCustom: getPinCustom?.('public') ?? false,
|
|
50
51
|
lanPinCustom: getPinCustom?.('lan') ?? false,
|
|
51
52
|
});
|
|
@@ -65,6 +66,11 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
|
|
|
65
66
|
if (enabled === undefined) return fail('bad-request', '局域网密码开关不可用 | LAN PIN switch unavailable');
|
|
66
67
|
return ok({ lanAuthEnabled: enabled });
|
|
67
68
|
}
|
|
69
|
+
if (endpoint === POCKET_ENDPOINTS.lanSetEnabled) {
|
|
70
|
+
const enabled = setLanEnabled?.(payload?.on === true);
|
|
71
|
+
if (enabled === undefined) return fail('bad-request', '局域网访问开关不可用 | LAN access switch unavailable');
|
|
72
|
+
return ok({ lanEnabled: enabled });
|
|
73
|
+
}
|
|
68
74
|
if (endpoint === POCKET_ENDPOINTS.lanSetOverride) {
|
|
69
75
|
// 返回完整 status:前端 setStatus(await call(...)) 直接替换 status 对象,
|
|
70
76
|
// 若只返回 { lanIpOverride } 会丢掉 accessToken/lanToken/tunnelUrl 等字段,
|
package/package.json
CHANGED