dsh-pocket 1.8.3 → 1.9.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 CHANGED
@@ -43,10 +43,10 @@ What it looks like — the phone shows the exact same UI as your computer, live:
43
43
  |---|---|
44
44
  | 📶 LAN QR access | Works out of the box: Settings → Phone access — scan the LAN QR on the same Wi-Fi |
45
45
  | 🌐 Public QR (from anywhere) | Click "Enable anywhere" → cloudflared tunnel → scan the public QR over 4G / any network |
46
- | 🔐 Access PIN | Public links require an **8-digit PIN** (rotated every time the tunnel starts — old links die instantly); LAN has its own separate **8-digit PIN** (refresh it manually in Settings) |
46
+ | 🔐 Access PIN | Public links require an **8-digit PIN** (rotated every time the tunnel starts — old links die instantly); LAN has its own separate **8-digit PIN** (on by default; can be switched off in Settings — then LAN scans connect directly) |
47
47
  | ⚡ Real-time sync | Streaming output passes through WebSocket untouched — what the computer renders, the phone renders live; fully interactive both ways |
48
48
  | 📱 Mobile-adaptive layout | Narrow screens get a drawer layout automatically (ported from dsh-web-mobile, MIT): sidebar drawer, full-width conversation, safe-area insets, touch optimizations |
49
- | 🗜️ Transfer compression | Large JSON responses are gzip/brotli'd on the fly (17MB session history → ~1.3MB) — faster loads, less mobile data |
49
+ | 🗜️ Transfer compression | Large JSON responses are gzip/brotli'd on the fly (17MB session history → ~1MB; brotli quality 6: fast and bandwidth-friendly) — faster loads, less mobile data |
50
50
  | 🔁 Tunnel auto-restore | After a DSH restart the previously-running public tunnel comes back automatically |
51
51
  | 🧩 Zero-dependency install | One npm package, one settings tab — no core/adapter split, no account, no server |
52
52
 
@@ -77,6 +77,8 @@ npx @deepseek-ai/dsh web
77
77
 
78
78
  Settings → **Phone access** → scan the "📶 LAN" QR code → enter the **LAN PIN** (shown in the LAN block of the settings page; hit **Refresh** to get a new one) → the phone opens the exact same DSH, in real time.
79
79
 
80
+ > 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).
81
+
80
82
  ### Public (from anywhere)
81
83
 
82
84
  On the same page click "**Enable anywhere**" → wait for the tunnel (first run downloads cloudflared; macOS/Linux use the Tsinghua mirror, seconds) → scan the "🌐 Public" QR code → the phone opens the link and **enters the 8-digit PIN** (shown in the settings page's public section, **rotated on every tunnel start**) → works from outside (4G / office network).
@@ -85,11 +87,11 @@ On the same page click "**Enable anywhere**" → wait for the tunnel (first run
85
87
 
86
88
  ## ⚠️ Security (read first)
87
89
 
88
- - **DSH can execute code on your computer.** **LAN** QR/URL plus its own **8-digit PIN** is the key — **never share the LAN QR, URL or PIN**.
90
+ - **DSH can execute code on your computer.** **LAN** QR/URL plus its own **8-digit PIN** is the key (PIN **on by default**, switchable off then LAN scans connect directly, same-network devices only) — **never share the LAN QR, URL or PIN**.
89
91
  - **Public** access is protected by an **8-digit PIN**: the link is random, the PIN rotates on every tunnel start, and old links die instantly — even a leaked link can't get in.
90
92
  - The public URL is randomly assigned by cloudflared and **changes on every restart** (old links die automatically — a natural key rotation).
91
93
  - LAN mode exposes nothing publicly; only devices on the same network can reach it.
92
- - Built for personal use; the public PIN lives in `$DSH_HOME/dsh-pocket/token` (re-rolled on every tunnel start) and the LAN PIN in `$DSH_HOME/dsh-pocket/token-lan` (refreshed manually in Settings).
94
+ - Built for personal use; the public PIN lives in `$DSH_HOME/dsh-pocket/token` (re-rolled on every tunnel start), the LAN PIN in `$DSH_HOME/dsh-pocket/token-lan` (refreshed manually in Settings), and the LAN-PIN switch state in `$DSH_HOME/dsh-pocket/settings.json`.
93
95
 
94
96
  ## 💻 DSH Desktop
95
97
 
@@ -149,9 +151,10 @@ Such tools take over all traffic and often cut cloudflared's tunnel-edge connect
149
151
 
150
152
  | File | Purpose |
151
153
  |---|---|
152
- | `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) + DSH Desktop detection |
154
+ | `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 |
155
+ | `lib/settings.mjs` | Settings persistence: LAN-PIN switch (on by default) stored in `$DSH_HOME/dsh-pocket/settings.json` |
153
156
  | `lib/service.mjs` | Service: proxy lifecycle (port auto-fallback), public tunnel (auto-restore), status snapshot (with QR data URLs) |
154
- | `lib/proxy.mjs` | Header-rewriting reverse proxy: Host/Origin → loopback, HTTP + WebSocket passthrough + polyfill injection + gzip/brotli compression + public-access PIN auth |
157
+ | `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) |
155
158
  | `lib/tunnel.mjs` | cloudflared: multi-mirror download (Tsinghua first) / adaptive parallel / start / parse public URL (HTTP/2) |
156
159
  | `lib/web-rpc.js` | Loopback RPC: `status` / `tunnel.start` / `tunnel.stop` / `version` / `update` / `restart` |
157
160
  | `client/` | "Phone access" settings tab + mobile adaptation (dsh-web-mobile port) |
package/README.md CHANGED
@@ -43,10 +43,10 @@ DSH Pocket 就是干这个的:**装上它,手机扫个码,就能实时看
43
43
  |---|---|
44
44
  | 📶 局域网扫码 | 装好即用:设置 → 手机访问,打开就有局域网二维码,手机连同一 WiFi 扫码即开 |
45
45
  | 🌐 公网扫码(人在外面) | 点「开启公网访问」→ cloudflared 隧道 → 出公网二维码,4G/任何网络都能访问 |
46
- | 🔐 访问密码 | 公网链接需输入 **8 位数字密码**(每次开启公网自动换新,旧链接立即作废);局域网同样有独立 **8 位数字密码**(设置页可手动刷新) |
46
+ | 🔐 访问密码 | 公网链接需输入 **8 位数字密码**(每次开启公网自动换新,旧链接立即作废);局域网有独立 **8 位数字密码**(默认开启,设置页可**一键关闭**——关闭后局域网扫码直连) |
47
47
  | ⚡ 实时同步 | 流式输出走 WebSocket 全透传——**电脑上在输出,手机上同步在滚**,可双向操作 |
48
48
  | 📱 移动端适配 | 窄屏自动变抽屉布局(移植 dsh-web-mobile,MIT):侧栏抽屉、会话全宽、状态栏安全区、触控优化 |
49
- | 🗜️ 传输压缩 | 大 JSON 响应自动 gzip/brotli(长会话 17MB → ~1.3MB),手机加载更快、更省流量 |
49
+ | 🗜️ 传输压缩 | 大 JSON 响应自动 gzip/brotli(长会话 17MB → ~1MB,brotli 质量 6:快且省流量),手机加载更快、更省流量 |
50
50
  | 🔁 隧道自动恢复 | DSH 重启后自动重新拉起之前开着的公网隧道,无需手动重开 |
51
51
  | 🧩 零依赖安装 | 一个 npm 包、一个设置页,没有核心/适配器要分开装;无需账号、无需服务器 |
52
52
 
@@ -77,6 +77,8 @@ npx @deepseek-ai/dsh web
77
77
 
78
78
  设置 → **手机访问** → 手机扫「📶 局域网」二维码 → 打开链接**输入局域网密码**(显示在设置页局域网区块,点「刷新」可换新)→ 打开的就是电脑上的 DSH,实时同步。
79
79
 
80
+ > 局域网密码**默认开启**(安全优先)。如果只有自己用、嫌每次输密码麻烦,可在设置页局域网区块把「局域网访问密码」切到**关**——之后局域网扫码直连、无需密码(仅同一局域网设备可访问;**公网始终要密码**,不受影响)。
81
+
80
82
  ### 公网(人在外面)
81
83
 
82
84
  同一页点「**开启公网访问**」→ 等隧道建立(首次会下载 cloudflared,macOS/Linux 走清华镜像秒下)→ 手机扫「🌐 公网」二维码 → 打开链接**输入 8 位访问密码**(密码显示在设置页公网区块,**每次开启公网变新**)→ 人在外面(4G/公司网)也能访问。
@@ -85,11 +87,11 @@ npx @deepseek-ai/dsh web
85
87
 
86
88
  ## ⚠️ 安全(必读)
87
89
 
88
- - **DSH 能执行你电脑上的代码**。**局域网**二维码/URL 配上独立 **8 位数字密码**才是钥匙,**请勿把局域网二维码、URL 或密码发给别人**
90
+ - **DSH 能执行你电脑上的代码**。**局域网**二维码/URL 配上独立 **8 位数字密码**才是钥匙(密码**默认开启**,可关——关闭后局域网扫码直连,仅同一网络设备可访问),**请勿把局域网二维码、URL 或密码发给别人**
89
91
  - **公网**有 **8 位数字密码**保护:链接随机分配、每次开启换新密码、旧链接立即作废——泄露了也进不来,改密码/重开即可作废
90
92
  - 公网 URL 由 cloudflared 随机分配,**每次重启会变化**(旧链接自动失效,相当于天然轮换)
91
93
  - 局域网模式不暴露公网,只有同一网络内的设备能访问
92
- - 适合个人自用;公网密码存本机 `$DSH_HOME/dsh-pocket/token`(每次开启公网自动换新),局域网密码存 `$DSH_HOME/dsh-pocket/token-lan`(设置页手动刷新)
94
+ - 适合个人自用;公网密码存本机 `$DSH_HOME/dsh-pocket/token`(每次开启公网自动换新),局域网密码存 `$DSH_HOME/dsh-pocket/token-lan`(设置页手动刷新),开关状态存 `$DSH_HOME/dsh-pocket/settings.json`
93
95
 
94
96
  ## 💻 DSH Desktop(桌面版)
95
97
 
@@ -150,9 +152,10 @@ npx @deepseek-ai/dsh web
150
152
 
151
153
  | 文件 | 说明 |
152
154
  |---|---|
153
- | `lib/index.js` | 插件入口:自动起代理 + 注册 RPC + 访问密码管理(公网 8 位每次开启变新;局域网独立 8 位可手动刷新)+ 桌面端环境适配 |
155
+ | `lib/index.js` | 插件入口:自动起代理 + 注册 RPC + 访问密码管理(公网 8 位每次开启变新;局域网独立 8 位可手动刷新/开关)+ 桌面端环境适配 |
156
+ | `lib/settings.mjs` | 设置持久化:局域网密码开关(默认开启)存 `$DSH_HOME/dsh-pocket/settings.json` |
154
157
  | `lib/service.mjs` | 服务:代理生命周期(端口自适应)、公网隧道(自动恢复)、状态快照(含二维码) |
155
- | `lib/proxy.mjs` | 改头反向代理:Host/Origin → loopback,HTTP + WebSocket 透传 + polyfill 注入 + gzip/brotli 压缩 + 公网访问令牌认证 |
158
+ | `lib/proxy.mjs` | 改头反向代理:Host/Origin → loopback,HTTP + WebSocket 透传 + polyfill 注入 + gzip/brotli 压缩 + Host 区分的访问令牌认证(公网必验;局域网按开关) |
156
159
  | `lib/tunnel.mjs` | cloudflared:多镜像源下载(清华优先)/自适应多线程/启动/解析公网 URL(HTTP/2) |
157
160
  | `lib/web-rpc.js` | loopback RPC:`status` / `tunnel.start` / `tunnel.stop` / `version` / `update` / `restart` |
158
161
  | `client/` | 设置页「手机访问」+ 移动端适配(dsh-web-mobile 移植) |
package/client/api.js CHANGED
@@ -9,6 +9,7 @@ export const POCKET_ENDPOINTS = Object.freeze({
9
9
  update: 'pocket.update',
10
10
  restart: 'pocket.restart',
11
11
  lanTokenRefresh: 'token.lanRefresh',
12
+ lanAuthSetEnabled: 'lanAuth.setEnabled',
12
13
  });
13
14
 
14
15
  /** 语义化版本比较:a > b 返回正数,相等 0,a < b 负数(数字段 + 预发布后缀)。 */
package/client/client.js CHANGED
@@ -48,7 +48,8 @@ var POCKET_ENDPOINTS = Object.freeze({
48
48
  version: "pocket.version",
49
49
  update: "pocket.update",
50
50
  restart: "pocket.restart",
51
- lanTokenRefresh: "token.lanRefresh"
51
+ lanTokenRefresh: "token.lanRefresh",
52
+ lanAuthSetEnabled: "lanAuth.setEnabled"
52
53
  });
53
54
  function compareVersions(a, b) {
54
55
  const pa = String(a).replace(/^[vV]/, "").split(".");
@@ -1437,6 +1438,13 @@ function PocketSettingsTab({ rpcCall }) {
1437
1438
  } catch {
1438
1439
  }
1439
1440
  };
1441
+ const setLanAuth = async (on) => {
1442
+ try {
1443
+ const r = await call(POCKET_ENDPOINTS.lanAuthSetEnabled, { on });
1444
+ setStatus((s) => ({ ...s, lanAuthEnabled: r.lanAuthEnabled }));
1445
+ } catch {
1446
+ }
1447
+ };
1440
1448
  const lanUrl = status?.lanUrl;
1441
1449
  const tunnelUrl = status?.tunnelUrl;
1442
1450
  const tunnelPhase = tunnelState?.phase ?? "idle";
@@ -1506,20 +1514,38 @@ function PocketSettingsTab({ rpcCall }) {
1506
1514
  "div",
1507
1515
  { style: styles.block },
1508
1516
  (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "\u{1F4F6} \u5C40\u57DF\u7F51\uFF08\u540C\u4E00 WiFi\uFF09| LAN"),
1517
+ // 访问密码开关(issue #24):默认开启;关闭后扫码直连(仅同一局域网设备可访问)
1518
+ (0, import_react2.createElement)(
1519
+ "div",
1520
+ { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 8 } },
1521
+ (0, import_react2.createElement)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } }, "\u5C40\u57DF\u7F51\u8BBF\u95EE\u5BC6\u7801 | LAN access PIN"),
1522
+ (0, import_react2.createElement)("button", {
1523
+ style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, fontWeight: status.lanAuthEnabled !== false ? 600 : 400, background: status.lanAuthEnabled !== false ? "var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))" : "var(--dsw-alias-bg-layer-1,#fff)", color: status.lanAuthEnabled !== false ? "var(--dsw-alias-label-primary-foreground, #fff)" : "var(--dsw-alias-label-primary,inherit)" },
1524
+ onClick: () => setLanAuth(true)
1525
+ }, "\u5F00 | On"),
1526
+ (0, import_react2.createElement)("button", {
1527
+ style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, fontWeight: status.lanAuthEnabled === false ? 600 : 400, background: status.lanAuthEnabled === false ? "var(--dsw-alias-state-error-primary,#dc2626)" : "var(--dsw-alias-bg-layer-1,#fff)", color: status.lanAuthEnabled === false ? "#fff" : "var(--dsw-alias-label-primary,inherit)" },
1528
+ onClick: () => setLanAuth(false)
1529
+ }, "\u5173 | Off")
1530
+ ),
1509
1531
  lanUrl ? (0, import_react2.createElement)(
1510
1532
  "div",
1511
1533
  null,
1512
1534
  (0, import_react2.createElement)("img", { src: status.lanQr, alt: "LAN QR", style: styles.qr }),
1513
1535
  (0, import_react2.createElement)("div", { style: styles.code }, lanUrl),
1514
1536
  (0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00"),
1515
- status.lanToken ? (0, import_react2.createElement)(
1537
+ status.lanAuthEnabled !== false ? (0, import_react2.createElement)(
1516
1538
  "div",
1517
1539
  { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
1518
1540
  "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A",
1519
1541
  status.lanToken,
1520
1542
  "\uFF08\u624B\u673A\u6253\u5F00\u9700\u8F93\u5165\uFF1B\u4E0E\u516C\u7F51\u5BC6\u7801\u5206\u5F00\uFF09",
1521
1543
  (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, "\u5237\u65B0 | Refresh")
1522
- ) : null
1544
+ ) : (0, import_react2.createElement)(
1545
+ "div",
1546
+ { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } },
1547
+ "\u{1F513} \u5BC6\u7801\u5DF2\u5173\u95ED\uFF1A\u626B\u7801\u76F4\u8FDE\uFF0C\u65E0\u9700\u5BC6\u7801\uFF08\u4EC5\u540C\u4E00\u5C40\u57DF\u7F51\u8BBE\u5907\u53EF\u8BBF\u95EE\uFF1B\u516C\u7F51\u4ECD\u8981\u5BC6\u7801\uFF09| PIN off \u2014 scan & go (LAN only; public still requires PIN)"
1548
+ )
1523
1549
  ) : (0, import_react2.createElement)("div", { style: styles.muted }, "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026 | proxy starting\u2026")
1524
1550
  ),
1525
1551
  // 公网
package/client/index.jsx CHANGED
@@ -175,6 +175,14 @@ function PocketSettingsTab({ rpcCall }) {
175
175
  } catch { /* 忽略 */ }
176
176
  };
177
177
 
178
+ // 局域网访问密码开关(issue #24):默认开启;关闭后局域网扫码直连(公网不受影响)
179
+ const setLanAuth = async (on) => {
180
+ try {
181
+ const r = await call(POCKET_ENDPOINTS.lanAuthSetEnabled, { on });
182
+ setStatus((s) => ({ ...s, lanAuthEnabled: r.lanAuthEnabled }));
183
+ } catch { /* 忽略 */ }
184
+ };
185
+
178
186
  const lanUrl = status?.lanUrl;
179
187
  const tunnelUrl = status?.tunnelUrl;
180
188
  const tunnelPhase = tunnelState?.phase ?? 'idle';
@@ -238,19 +246,32 @@ function PocketSettingsTab({ rpcCall }) {
238
246
  // 局域网
239
247
  h('div', { style: styles.block },
240
248
  h('div', { style: { fontWeight: 600, fontSize: 13 } }, '📶 局域网(同一 WiFi)| LAN'),
249
+ // 访问密码开关(issue #24):默认开启;关闭后扫码直连(仅同一局域网设备可访问)
250
+ h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 } },
251
+ h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } }, '局域网访问密码 | LAN access PIN'),
252
+ h('button', {
253
+ style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, fontWeight: status.lanAuthEnabled !== false ? 600 : 400, background: status.lanAuthEnabled !== false ? 'var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))' : 'var(--dsw-alias-bg-layer-1,#fff)', color: status.lanAuthEnabled !== false ? 'var(--dsw-alias-label-primary-foreground, #fff)' : 'var(--dsw-alias-label-primary,inherit)' },
254
+ onClick: () => setLanAuth(true),
255
+ }, '开 | On'),
256
+ h('button', {
257
+ style: { ...styles.btn, height: 28, padding: '0 12px', fontSize: 12, fontWeight: status.lanAuthEnabled === false ? 600 : 400, background: status.lanAuthEnabled === false ? 'var(--dsw-alias-state-error-primary,#dc2626)' : 'var(--dsw-alias-bg-layer-1,#fff)', color: status.lanAuthEnabled === false ? '#fff' : 'var(--dsw-alias-label-primary,inherit)' },
258
+ onClick: () => setLanAuth(false),
259
+ }, '关 | Off'),
260
+ ),
241
261
  lanUrl
242
262
  ? h('div', null,
243
263
  h('img', { src: status.lanQr, alt: 'LAN QR', style: styles.qr }),
244
264
  h('div', { style: styles.code }, lanUrl),
245
265
  h('div', { style: styles.muted }, '手机连接同一 WiFi 后扫码即可打开'),
246
- status.lanToken
266
+ status.lanAuthEnabled !== false
247
267
  ? h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
248
268
  '🔐 访问密码:',
249
269
  status.lanToken,
250
270
  '(手机打开需输入;与公网密码分开)',
251
271
  h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, '刷新 | Refresh'),
252
272
  )
253
- : null,
273
+ : h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#b45309)', lineHeight: 1.5 } },
274
+ '🔓 密码已关闭:扫码直连,无需密码(仅同一局域网设备可访问;公网仍要密码)| PIN off — scan & go (LAN only; public still requires PIN)'),
254
275
  )
255
276
  : h('div', { style: styles.muted }, '代理未就绪… | proxy starting…'),
256
277
  ),
package/lib/index.js CHANGED
@@ -20,6 +20,7 @@ import { createPocketService } from './service.mjs';
20
20
  import { installPocketRpc } from './web-rpc.js';
21
21
  import { restartHost } from './restart.js';
22
22
  import { desktopEnvPatchScript, advancedNoticeScript, DEFAULT_INJECT } from './proxy.mjs';
23
+ import { lanAuthEnabled, setLanAuthEnabled } from './settings.mjs';
23
24
 
24
25
  const name = 'dsh-pocket';
25
26
  const inject = ['connection', 'webServer'];
@@ -202,10 +203,11 @@ export function apply(ctx, config = {}, internals = {}) {
202
203
  injectHtml: isDesktop
203
204
  ? DEFAULT_INJECT + desktopEnvPatchScript(process.platform) + (desktopAdvanced ? advancedNoticeScript() : '')
204
205
  : undefined,
205
- // 访问密码(issue #13 + #18):局域网与公网**分开**,所有代理访问都要验证
206
+ // 访问密码(issue #13 + #18 + #24):公网永远要密码(每次开启变新);
207
+ // 局域网按开关(默认开启;关闭后局域网扫码直连,仅同网段设备可访问)
206
208
  auth: {
207
209
  getToken: (host) => tokenForHost(host),
208
- isProtected: () => true,
210
+ isProtected: (host) => /trycloudflare\.com$/i.test(String(host ?? '')) ? true : lanAuthEnabled(),
209
211
  },
210
212
  // 每次公网隧道就绪 → 轮换 8 位密码(旧密码/旧链接立即作废)
211
213
  onTunnelReady: () => {
@@ -222,6 +224,8 @@ export function apply(ctx, config = {}, internals = {}) {
222
224
  getToken: () => getAccessToken(),
223
225
  getLanToken: () => getLanToken(),
224
226
  refreshLanToken: () => refreshLanToken(),
227
+ getLanAuthEnabled: () => lanAuthEnabled(),
228
+ setLanAuthEnabled: (on) => setLanAuthEnabled(on),
225
229
  runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate, loadedVersion: () => loadedVersion },
226
230
  restart: internals.restart ?? (() => pocketRestart(service)),
227
231
  restartNotice: internals.restartNotice ?? consumeRestartNotice,
package/lib/proxy.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { createServer } from 'node:http';
15
15
  import { request as httpRequest } from 'node:http';
16
- import { createGzip, createBrotliCompress } from 'node:zlib';
16
+ import { createGzip, createBrotliCompress, constants as zlibConstants } from 'node:zlib';
17
17
 
18
18
  const DEFAULT_UPSTREAM = { host: '127.0.0.1', port: 3080 };
19
19
 
@@ -211,8 +211,10 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
211
211
  return;
212
212
  }
213
213
  // 大 JSON/text 响应**流式压缩**(issue #12):长会话历史一次返回 17MB+,
214
- // 局域网直连与隧道段都吃满带宽;压缩到 ~1.3MB。跳过已压缩、SSE 流
214
+ // 局域网直连与隧道段都吃满带宽;压缩到 ~1MB。跳过已压缩、SSE 流
215
215
  // (/api/events.* 原样透传)、HTML(走上面的注入分支)。
216
+ // brotli 质量选 6(issue #25):zlib 默认 q11 压 17MB 要 40s+,手机直接超时;
217
+ // q6 实测 128ms(比 gzip 的 88ms 略慢但同档)且输出更小(1.00MB vs 1.20MB)。
216
218
  const acceptEncoding = String(req.headers['accept-encoding'] ?? '');
217
219
  const canGzip = /\bgzip\b/.test(acceptEncoding);
218
220
  const canBr = /\bbr\b/.test(acceptEncoding);
@@ -230,7 +232,9 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
230
232
  delete outHeaders['transfer-encoding'];
231
233
  outHeaders['content-encoding'] = enc;
232
234
  res.writeHead(proxyRes.statusCode ?? 200, outHeaders);
233
- const z = enc === 'br' ? createBrotliCompress() : createGzip();
235
+ const z = enc === 'br'
236
+ ? createBrotliCompress({ params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 6 } })
237
+ : createGzip();
234
238
  proxyRes.pipe(z).pipe(res);
235
239
  // 任一端断开都要清理(含压缩流)。注意:不能用 proxyRes 的 'close'
236
240
  // 来掐 res——正常结束后 close 也会触发,此时压缩流可能还没写完,
@@ -0,0 +1,43 @@
1
+ // dsh-pocket 设置持久化($DSH_HOME/dsh-pocket/settings.json)
2
+ //
3
+ // 当前只有一项:局域网访问密码开关 lanAuthEnabled(issue #24)。
4
+ // 默认**开启**(安全优先):局域网扫码也要输 8 位密码;
5
+ // 用户可关闭——关闭后局域网扫码直连(仅同一网络内的设备能访问),公网不受影响(永远要密码)。
6
+
7
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
+ import { join, dirname } from 'node:path';
9
+ import { homedir } from 'node:os';
10
+
11
+ const settingsRel = join('dsh-pocket', 'settings.json');
12
+ export function settingsPath() {
13
+ return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), settingsRel);
14
+ }
15
+
16
+ function readSettings() {
17
+ try {
18
+ const raw = JSON.parse(readFileSync(settingsPath(), 'utf8'));
19
+ return raw && typeof raw === 'object' ? raw : {};
20
+ } catch { /* 无文件/损坏 → 默认 */ }
21
+ return {};
22
+ }
23
+
24
+ function writeSettings(s) {
25
+ try {
26
+ mkdirSync(dirname(settingsPath()), { recursive: true });
27
+ writeFileSync(settingsPath(), JSON.stringify(s, null, 2), { mode: 0o600 });
28
+ } catch { /* 忽略 */ }
29
+ return s;
30
+ }
31
+
32
+ /** 局域网访问密码开关:默认开启(文件缺失/损坏都视为开启)。 */
33
+ export function lanAuthEnabled() {
34
+ return readSettings().lanAuthEnabled !== false;
35
+ }
36
+
37
+ /** 设置局域网访问密码开关,返回新状态(持久化)。 */
38
+ export function setLanAuthEnabled(on) {
39
+ const s = readSettings();
40
+ s.lanAuthEnabled = !!on;
41
+ writeSettings(s);
42
+ return s.lanAuthEnabled;
43
+ }
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 }) {
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 }) {
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 () => {};
@@ -33,7 +33,7 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
33
33
  return ctx.connection.rpc.handle(POCKET_RPC_CHANNEL, async (endpoint, payload = {}, signal) => {
34
34
  if (signal?.aborted) return fail('cancelled', 'The request was cancelled.');
35
35
 
36
- // status 响应:服务状态 + 重启提示 + 停止命令 + 桌面端标志 + 公网/局域网访问密码
36
+ // status 响应:服务状态 + 重启提示 + 停止命令 + 桌面端标志 + 公网/局域网访问密码 + 局域网密码开关
37
37
  const statusPayload = async () => {
38
38
  let notice = null;
39
39
  try { notice = (await restartNotice?.()) ?? null; } catch { notice = null; }
@@ -45,6 +45,7 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
45
45
  killHint: killHint(s.dshPort ?? 3080),
46
46
  accessToken: getToken?.() ?? null,
47
47
  lanToken: getLanToken?.() ?? null,
48
+ lanAuthEnabled: getLanAuthEnabled?.() ?? true,
48
49
  });
49
50
  };
50
51
 
@@ -57,6 +58,11 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
57
58
  if (!fresh) return fail('bad-request', '局域网密码刷新不可用 | LAN PIN refresh unavailable');
58
59
  return ok({ lanToken: fresh });
59
60
  }
61
+ if (endpoint === POCKET_ENDPOINTS.lanAuthSetEnabled) {
62
+ const enabled = setLanAuthEnabled?.(payload?.on === true);
63
+ if (enabled === undefined) return fail('bad-request', '局域网密码开关不可用 | LAN PIN switch unavailable');
64
+ return ok({ lanAuthEnabled: enabled });
65
+ }
60
66
  if (endpoint === POCKET_ENDPOINTS.tunnelStart) {
61
67
  await service.startTunnel();
62
68
  return await statusPayload();
package/package.json CHANGED
@@ -76,5 +76,5 @@
76
76
  "access": "public",
77
77
  "registry": "https://registry.npmjs.org/"
78
78
  },
79
- "version": "1.8.3"
79
+ "version": "1.9.0"
80
80
  }