dsh-pocket 1.9.2 → 1.11.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 +10 -5
- package/README.md +10 -5
- package/client/api.js +1 -0
- package/client/client.js +206 -42
- package/client/index.jsx +107 -46
- package/client/pocket-locales.js +117 -0
- package/lib/index.js +44 -13
- package/lib/proxy.mjs +25 -11
- package/lib/settings.mjs +26 -1
- package/lib/web-rpc.js +14 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -43,7 +43,9 @@ 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
|
|
46
|
+
| 🔐 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
|
+
| 🔑 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) |
|
|
48
|
+
| 🧘 Session persistence | Enter the PIN once and you're set for a long time (login is tied to the computer's dsh web process: as long as it stays up, the phone won't ask again; **after a dsh web restart/update, enter it once more**) |
|
|
47
49
|
| ⚡ Real-time sync | Streaming output passes through WebSocket untouched — what the computer renders, the phone renders live; fully interactive both ways |
|
|
48
50
|
| 📱 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
51
|
| 🗜️ 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 |
|
|
@@ -75,23 +77,26 @@ npx @deepseek-ai/dsh web
|
|
|
75
77
|
|
|
76
78
|
### LAN (same Wi-Fi)
|
|
77
79
|
|
|
78
|
-
Settings → **Phone access** → scan the "📶 LAN" QR code → enter the **LAN PIN** (shown in the LAN block
|
|
80
|
+
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.
|
|
79
81
|
|
|
80
82
|
> 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).
|
|
83
|
+
>
|
|
84
|
+
> 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**).
|
|
81
85
|
|
|
82
86
|
### Public (from anywhere)
|
|
83
87
|
|
|
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
|
|
88
|
+
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 by default**, or **Customize** it to a fixed PIN that is never rotated) → works from outside (4G / office network).
|
|
85
89
|
|
|
86
90
|
> Upgrading: `dsh plugin --profile web update dsh-pocket --latest -w` (`--latest` is required across major versions — a `^0.x` range won't auto-jump to 1.x).
|
|
87
91
|
|
|
88
92
|
## ⚠️ Security (read first)
|
|
89
93
|
|
|
90
94
|
- **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**.
|
|
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.
|
|
95
|
+
- **Public** access is protected by an **8-digit PIN**: the link is random, the PIN rotates on every tunnel start by default, and old links die instantly — even a leaked link can't get in. **A custom PIN is never auto-rotated** (your value stays stable).
|
|
96
|
+
- Phone login state is tied to the computer's dsh web process: **no re-entry while dsh web stays up; one re-entry after a restart/update**.
|
|
92
97
|
- The public URL is randomly assigned by cloudflared and **changes on every restart** (old links die automatically — a natural key rotation).
|
|
93
98
|
- LAN mode exposes nothing publicly; only devices on the same network can reach it.
|
|
94
|
-
- Built for personal use; the public PIN lives in `$DSH_HOME/dsh-pocket/token` (re-rolled
|
|
99
|
+
- Built for personal use; the public PIN lives in `$DSH_HOME/dsh-pocket/token` (re-rolled per tunnel start unless customized), the LAN PIN in `$DSH_HOME/dsh-pocket/token-lan` (refreshed manually in Settings), and switches/custom flags in `$DSH_HOME/dsh-pocket/settings.json`.
|
|
95
100
|
|
|
96
101
|
## 💻 DSH Desktop
|
|
97
102
|
|
package/README.md
CHANGED
|
@@ -43,7 +43,9 @@ DSH Pocket 就是干这个的:**装上它,手机扫个码,就能实时看
|
|
|
43
43
|
|---|---|
|
|
44
44
|
| 📶 局域网扫码 | 装好即用:设置 → 手机访问,打开就有局域网二维码,手机连同一 WiFi 扫码即开 |
|
|
45
45
|
| 🌐 公网扫码(人在外面) | 点「开启公网访问」→ cloudflared 隧道 → 出公网二维码,4G/任何网络都能访问 |
|
|
46
|
-
| 🔐 访问密码 | 公网链接需输入 **8
|
|
46
|
+
| 🔐 访问密码 | 公网链接需输入 **8 位数字密码**(默认每次开启公网自动换新;**可自定义固定密码**——自定义后不再换新);局域网有独立 **8 位数字密码**(默认开启,设置页可**一键关闭**——关闭后局域网扫码直连) |
|
|
47
|
+
| 🔑 自定义密码 | 公网/局域网密码都可在设置页**设成自己固定的 8 位数字**(自定义后公网不再自动换新) |
|
|
48
|
+
| 🧘 会话保持 | 手机输一次密码后**长期免输**(登录状态绑定电脑上的 dsh web 进程:只要它不重启,手机不用再输;**dsh web 重启/更新后需重新输入一次**) |
|
|
47
49
|
| ⚡ 实时同步 | 流式输出走 WebSocket 全透传——**电脑上在输出,手机上同步在滚**,可双向操作 |
|
|
48
50
|
| 📱 移动端适配 | 窄屏自动变抽屉布局(移植 dsh-web-mobile,MIT):侧栏抽屉、会话全宽、状态栏安全区、触控优化 |
|
|
49
51
|
| 🗜️ 传输压缩 | 大 JSON 响应自动 gzip/brotli(长会话 17MB → ~1MB,brotli 质量 6:快且省流量),手机加载更快、更省流量 |
|
|
@@ -75,23 +77,26 @@ npx @deepseek-ai/dsh web
|
|
|
75
77
|
|
|
76
78
|
### 局域网(同一 WiFi)
|
|
77
79
|
|
|
78
|
-
设置 → **手机访问** → 手机扫「📶 局域网」二维码 →
|
|
80
|
+
设置 → **手机访问** → 手机扫「📶 局域网」二维码 → 打开链接**输入局域网密码**(显示在设置页局域网区块,点「刷新」可换新,或点「自定义」设成自己固定的 8 位数字)→ 打开的就是电脑上的 DSH,实时同步。
|
|
79
81
|
|
|
80
82
|
> 局域网密码**默认开启**(安全优先)。如果只有自己用、嫌每次输密码麻烦,可在设置页局域网区块把「局域网访问密码」切到**关**——之后局域网扫码直连、无需密码(仅同一局域网设备可访问;**公网始终要密码**,不受影响)。
|
|
83
|
+
>
|
|
84
|
+
> 手机登录一次后**长期免输**:只要电脑上的 dsh web 不重启,再次打开手机不用再输入(**dsh web 重启/更新后需重新输入一次**)。
|
|
81
85
|
|
|
82
86
|
### 公网(人在外面)
|
|
83
87
|
|
|
84
|
-
同一页点「**开启公网访问**」→ 等隧道建立(首次会下载 cloudflared,macOS/Linux 走清华镜像秒下)→ 手机扫「🌐 公网」二维码 → 打开链接**输入 8
|
|
88
|
+
同一页点「**开启公网访问**」→ 等隧道建立(首次会下载 cloudflared,macOS/Linux 走清华镜像秒下)→ 手机扫「🌐 公网」二维码 → 打开链接**输入 8 位访问密码**(密码显示在设置页公网区块,默认**每次开启公网变新**,也可点「自定义」设成固定密码——自定义后不再换新)→ 人在外面(4G/公司网)也能访问。
|
|
85
89
|
|
|
86
90
|
> 更新到新版本:`dsh plugin --profile web update dsh-pocket --latest -w`(跨大版本时 `--latest` 是必须的,`^0.x` 范围不会自动升到 1.x)。
|
|
87
91
|
|
|
88
92
|
## ⚠️ 安全(必读)
|
|
89
93
|
|
|
90
94
|
- **DSH 能执行你电脑上的代码**。**局域网**二维码/URL 配上独立 **8 位数字密码**才是钥匙(密码**默认开启**,可关——关闭后局域网扫码直连,仅同一网络设备可访问),**请勿把局域网二维码、URL 或密码发给别人**
|
|
91
|
-
- **公网**有 **8
|
|
95
|
+
- **公网**有 **8 位数字密码**保护:链接随机分配、默认每次开启换新密码、旧链接立即作废——泄露了也进不来,改密码/重开即可作废;**自定义密码后不再自动换新**(你设的值即稳定密码)
|
|
96
|
+
- 手机登录状态与电脑上的 dsh web 进程绑定:**电脑 dsh web 一直开着就不用重复输入;重启/更新后需重新输入一次**
|
|
92
97
|
- 公网 URL 由 cloudflared 随机分配,**每次重启会变化**(旧链接自动失效,相当于天然轮换)
|
|
93
98
|
- 局域网模式不暴露公网,只有同一网络内的设备能访问
|
|
94
|
-
- 适合个人自用;公网密码存本机 `$DSH_HOME/dsh-pocket/token
|
|
99
|
+
- 适合个人自用;公网密码存本机 `$DSH_HOME/dsh-pocket/token`(默认每次开启公网自动换新,**自定义后不换**),局域网密码存 `$DSH_HOME/dsh-pocket/token-lan`(设置页手动刷新),开关/自定义标记存 `$DSH_HOME/dsh-pocket/settings.json`
|
|
95
100
|
|
|
96
101
|
## 💻 DSH Desktop(桌面版)
|
|
97
102
|
|
package/client/api.js
CHANGED
|
@@ -10,6 +10,7 @@ export const POCKET_ENDPOINTS = Object.freeze({
|
|
|
10
10
|
restart: 'pocket.restart',
|
|
11
11
|
lanTokenRefresh: 'token.lanRefresh',
|
|
12
12
|
lanAuthSetEnabled: 'lanAuth.setEnabled',
|
|
13
|
+
pinSetCustom: 'pin.setCustom',
|
|
13
14
|
});
|
|
14
15
|
|
|
15
16
|
/** 语义化版本比较:a > b 返回正数,相等 0,a < b 负数(数字段 + 预发布后缀)。 */
|
package/client/client.js
CHANGED
|
@@ -49,7 +49,8 @@ var POCKET_ENDPOINTS = Object.freeze({
|
|
|
49
49
|
update: "pocket.update",
|
|
50
50
|
restart: "pocket.restart",
|
|
51
51
|
lanTokenRefresh: "token.lanRefresh",
|
|
52
|
-
lanAuthSetEnabled: "lanAuth.setEnabled"
|
|
52
|
+
lanAuthSetEnabled: "lanAuth.setEnabled",
|
|
53
|
+
pinSetCustom: "pin.setCustom"
|
|
53
54
|
});
|
|
54
55
|
function compareVersions(a, b) {
|
|
55
56
|
const pa = String(a).replace(/^[vV]/, "").split(".");
|
|
@@ -1290,9 +1291,131 @@ function mobileApply(ctx) {
|
|
|
1290
1291
|
}, MobileDrawerFooter));
|
|
1291
1292
|
}
|
|
1292
1293
|
|
|
1294
|
+
// client/pocket-locales.js
|
|
1295
|
+
var NS2 = "pocket";
|
|
1296
|
+
var zh2 = {
|
|
1297
|
+
"section": "\u624B\u673A\u8BBF\u95EE",
|
|
1298
|
+
"title": "\u{1F4F1} \u624B\u673A\u8BBF\u95EE",
|
|
1299
|
+
"subtitle": "\u624B\u673A\u626B\u7801\u6253\u5F00\u7684\u5C31\u662F\u7535\u8111\u4E0A\u7684\u8FD9\u4E2A\u754C\u9762\uFF0C\u5B9E\u65F6\u540C\u6B65",
|
|
1300
|
+
"developer": "\u5F00\u53D1\u8005\uFF1A\u7A0B\u5E8F\u5458\u5C11\u5317\u6668",
|
|
1301
|
+
"starAsk": "\u2B50 \u987A\u624B\u7559\u9897 Star\uFF0C\u4F5C\u8005\u80FD\u9AD8\u5174\u4E00\u6574\u5929",
|
|
1302
|
+
"starCta": "\u884C\uFF0C\u7ED9\u4F60\u4E00\u9897 Star",
|
|
1303
|
+
"restarted": "\u{1F504} \u5DF2\u91CD\u542F",
|
|
1304
|
+
"ok": "\u77E5\u9053\u4E86",
|
|
1305
|
+
"bgHint": "\u8FDB\u7A0B\u5728\u540E\u53F0\u8FD0\u884C\uFF08\u4E0D\u6302\u7EC8\u7AEF\uFF09\u3002\u5982\u9700\u505C\u6B62\uFF1A{cmd}",
|
|
1306
|
+
"updatedRestart": "\u2705 \u5DF2\u66F4\u65B0 v{ver}\uFF0C\u91CD\u542F\u751F\u6548",
|
|
1307
|
+
"updateAutoRestarting": "\u2705 \u5DF2\u66F4\u65B0 v{ver}\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u542F\u2026",
|
|
1308
|
+
"updatedOk": "\u2705 \u5DF2\u66F4\u65B0 v{ver}",
|
|
1309
|
+
"updateAvailable": "\u{1F4E6} \u65B0\u7248\u672C v{ver}",
|
|
1310
|
+
"updating": "\u66F4\u65B0\u4E2D\u2026",
|
|
1311
|
+
"updateTo": "\u66F4\u65B0\u5230 v{ver}",
|
|
1312
|
+
"restartingNow": "\u6B63\u5728\u91CD\u542F\u751F\u6548\u2026",
|
|
1313
|
+
"restarting": "\u91CD\u542F\u4E2D\u2026",
|
|
1314
|
+
"restartNow": "\u{1F504} \u91CD\u542F dsh web \u751F\u6548",
|
|
1315
|
+
"updatingDetail": "\u23F3 \u66F4\u65B0\u4E2D\uFF08\u901A\u5E38 1-2 \u5206\u949F\uFF09\xB7 \u5DF2\u7B49\u5F85 {s} \u79D2",
|
|
1316
|
+
"restartingDetail": "\u23F3 \u6B63\u5728\u91CD\u542F\u751F\u6548\uFF08\u901A\u5E38 10-30 \u79D2\uFF09\xB7 \u5DF2\u7B49\u5F85 {s} \u79D2",
|
|
1317
|
+
"updatedAutoDetail": "\u2705 \u5DF2\u66F4\u65B0\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u542F\u751F\u6548\uFF0C\u8BF7\u7A0D\u5019\u5237\u65B0",
|
|
1318
|
+
"updatedRestartDetail": "\u2705 \u5DF2\u66F4\u65B0\uFF0C\u91CD\u542F dsh web \u751F\u6548",
|
|
1319
|
+
"updateFailed": "\u274C \u5931\u8D25\uFF1A{err}\uFF08\u624B\u52A8\u66F4\u65B0\uFF1Adsh plugin --profile web update dsh-pocket --latest -w\uFF09",
|
|
1320
|
+
"versionRange": "\u5F53\u524D v{cur} \u2192 \u6700\u65B0 v{latest}",
|
|
1321
|
+
"lanTitle": "\u{1F4F6} \u5C40\u57DF\u7F51\uFF08\u540C\u4E00 WiFi\uFF09",
|
|
1322
|
+
"lanHint": "\u624B\u673A\u8FDE\u63A5\u540C\u4E00 WiFi \u540E\u626B\u7801\u5373\u53EF\u6253\u5F00",
|
|
1323
|
+
"lanPin": "\u5C40\u57DF\u7F51\u8BBF\u95EE\u5BC6\u7801",
|
|
1324
|
+
"on": "\u5F00",
|
|
1325
|
+
"off": "\u5173",
|
|
1326
|
+
"lanPinValue": "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A{pin}\uFF08\u624B\u673A\u6253\u5F00\u9700\u8F93\u5165\uFF1B\u4E0E\u516C\u7F51\u5BC6\u7801\u5206\u5F00\uFF09",
|
|
1327
|
+
"lanPinCustomValue": "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A{pin}\uFF08\u81EA\u5B9A\u4E49\uFF1B\u624B\u673A\u6253\u5F00\u9700\u8F93\u5165\uFF09",
|
|
1328
|
+
"refresh": "\u5237\u65B0",
|
|
1329
|
+
"customize": "\u81EA\u5B9A\u4E49",
|
|
1330
|
+
"customizing": "\u65B0\u5BC6\u7801\uFF088 \u4F4D\u6570\u5B57\uFF09\uFF1A",
|
|
1331
|
+
"save": "\u4FDD\u5B58",
|
|
1332
|
+
"cancel": "\u53D6\u6D88",
|
|
1333
|
+
"pinInvalid": "\u5BC6\u7801\u5FC5\u987B\u662F 8 \u4F4D\u6570\u5B57",
|
|
1334
|
+
"pinCustomHint": "\u81EA\u5B9A\u4E49\u540E\u5F00\u542F\u516C\u7F51\u4E0D\u518D\u81EA\u52A8\u6362\u65B0",
|
|
1335
|
+
"lanPinOff": "\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",
|
|
1336
|
+
"lanStarting": "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026",
|
|
1337
|
+
"wanTitle": "\u{1F310} \u516C\u7F51\uFF08\u4EBA\u5728\u5916\u9762\uFF09",
|
|
1338
|
+
"wanHint": "\u4EFB\u4F55\u7F51\u7EDC\u626B\u7801\u5373\u7528\uFF08URL \u6BCF\u6B21\u91CD\u542F\u81EA\u52A8\u6362\u65B0\uFF09",
|
|
1339
|
+
"wanPin": "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A{pin}\uFF08\u6BCF\u6B21\u5F00\u542F\u516C\u7F51\u53D8\u65B0\uFF1B\u624B\u673A\u6253\u5F00\u94FE\u63A5\u9700\u8F93\u5165\u6B64\u5BC6\u7801\uFF09",
|
|
1340
|
+
"wanPinCustom": "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A{pin}\uFF08\u81EA\u5B9A\u4E49\uFF0C\u5F00\u542F\u516C\u7F51\u4E0D\u518D\u81EA\u52A8\u6362\u65B0\uFF09",
|
|
1341
|
+
"stopTunnel": "\u5173\u95ED\u516C\u7F51",
|
|
1342
|
+
"enable": "\u5F00\u542F\u516C\u7F51\u8BBF\u95EE",
|
|
1343
|
+
"opening": "\u5F00\u542F\u4E2D\u2026",
|
|
1344
|
+
"downloading": "\u23F3 \u4E0B\u8F7D cloudflared\uFF08\u9996\u6B21\u7EA6 20-50MB\uFF0C\u901A\u5E38 1-2 \u5206\u949F\uFF1B\u4E4B\u540E\u79D2\u5F00\uFF09\xB7 \u5DF2\u7B49\u5F85 {s} \u79D2",
|
|
1345
|
+
"connecting": "\u23F3 \u8FDE\u63A5 Cloudflare \u8FB9\u7F18\uFF08\u901A\u5E38 5-30 \u79D2\uFF09\xB7 \u5DF2\u7B49\u5F85 {s} \u79D2{suffix}",
|
|
1346
|
+
"slowHint": " \u2014 \u6709\u70B9\u4E45\uFF1F\u68C0\u67E5\u662F\u5426\u5F00\u7740\u4EE3\u7406/VPN\uFF08Clash TUN \u7B49\uFF09",
|
|
1347
|
+
"error": "\u274C \u5F00\u542F\u5931\u8D25\uFF1A{detail}\uFF08\u53EF\u91CD\u8BD5\uFF1B\u82E5\u662F\u4EE3\u7406/VPN \u95EE\u9898\u89C1 README \u6392\u969C\uFF09",
|
|
1348
|
+
"unknownError": "\u672A\u77E5\u9519\u8BEF",
|
|
1349
|
+
"feedback": "\u6709\u95EE\u9898\uFF1F\u6B22\u8FCE\u5230 GitHub Issues \u53CD\u9988 \u{1F64F}"
|
|
1350
|
+
};
|
|
1351
|
+
var en2 = {
|
|
1352
|
+
"section": "Phone access",
|
|
1353
|
+
"title": "\u{1F4F1} Phone access",
|
|
1354
|
+
"subtitle": "The phone shows this exact screen, live",
|
|
1355
|
+
"developer": "Developer: \u5C11\u5317\u6668 (shaobeichen)",
|
|
1356
|
+
"starAsk": "\u2B50 Drop a Star if it helped \u2014 it makes the author\u2019s day",
|
|
1357
|
+
"starCta": "\u2605 Give a Star",
|
|
1358
|
+
"restarted": "\u{1F504} Restarted",
|
|
1359
|
+
"ok": "Got it",
|
|
1360
|
+
"bgHint": "Running in the background (not attached to a terminal). To stop: {cmd}",
|
|
1361
|
+
"updatedRestart": "\u2705 Updated to v{ver} \u2014 restart to apply",
|
|
1362
|
+
"updateAutoRestarting": "\u2705 Updated to v{ver} \u2014 auto-restarting\u2026",
|
|
1363
|
+
"updatedOk": "\u2705 Updated to v{ver}",
|
|
1364
|
+
"updateAvailable": "\u{1F4E6} Update available: v{ver}",
|
|
1365
|
+
"updating": "Updating\u2026",
|
|
1366
|
+
"updateTo": "Update to v{ver}",
|
|
1367
|
+
"restartingNow": "Restarting to apply\u2026",
|
|
1368
|
+
"restarting": "Restarting\u2026",
|
|
1369
|
+
"restartNow": "\u{1F504} Restart dsh web now",
|
|
1370
|
+
"updatingDetail": "\u23F3 Updating (usually 1-2 min) \xB7 {s}s elapsed",
|
|
1371
|
+
"restartingDetail": "\u23F3 Restarting to apply (usually 10-30s) \xB7 {s}s elapsed",
|
|
1372
|
+
"updatedAutoDetail": "\u2705 Updated \u2014 auto-restarting in progress, refresh shortly",
|
|
1373
|
+
"updatedRestartDetail": "\u2705 Updated \u2014 restart dsh web to apply",
|
|
1374
|
+
"updateFailed": "\u274C Failed: {err} (manual update: dsh plugin --profile web update dsh-pocket --latest -w)",
|
|
1375
|
+
"versionRange": "Current v{cur} \u2192 latest v{latest}",
|
|
1376
|
+
"lanTitle": "\u{1F4F6} LAN (same Wi-Fi)",
|
|
1377
|
+
"lanHint": "Scan to open once your phone is on the same Wi-Fi",
|
|
1378
|
+
"lanPin": "LAN access PIN",
|
|
1379
|
+
"on": "On",
|
|
1380
|
+
"off": "Off",
|
|
1381
|
+
"lanPinValue": "\u{1F510} PIN: {pin} (required on the phone; separate from the public PIN)",
|
|
1382
|
+
"lanPinCustomValue": "\u{1F510} PIN: {pin} (custom; required on the phone)",
|
|
1383
|
+
"refresh": "Refresh",
|
|
1384
|
+
"customize": "Customize",
|
|
1385
|
+
"customizing": "New PIN (8 digits): ",
|
|
1386
|
+
"save": "Save",
|
|
1387
|
+
"cancel": "Cancel",
|
|
1388
|
+
"pinInvalid": "PIN must be exactly 8 digits",
|
|
1389
|
+
"pinCustomHint": "custom PINs are not rotated on tunnel start",
|
|
1390
|
+
"lanPinOff": "\u{1F513} PIN off \u2014 scan & go, no PIN (LAN devices only; public still requires PIN)",
|
|
1391
|
+
"lanStarting": "Proxy starting\u2026",
|
|
1392
|
+
"wanTitle": "\u{1F310} Anywhere (public)",
|
|
1393
|
+
"wanHint": "Scan from any network (the URL changes on every restart)",
|
|
1394
|
+
"wanPin": "\u{1F510} PIN: {pin} (changes each time the tunnel is enabled; required on the phone)",
|
|
1395
|
+
"wanPinCustom": "\u{1F510} PIN: {pin} (custom \u2014 not rotated on tunnel start)",
|
|
1396
|
+
"stopTunnel": "Stop",
|
|
1397
|
+
"enable": "Enable anywhere",
|
|
1398
|
+
"opening": "Enabling\u2026",
|
|
1399
|
+
"downloading": "\u23F3 Downloading cloudflared (first run ~20-50MB, usually 1-2 min; instant afterward) \xB7 {s}s elapsed",
|
|
1400
|
+
"connecting": "\u23F3 Connecting to Cloudflare edge (usually 5-30s) \xB7 {s}s elapsed{suffix}",
|
|
1401
|
+
"slowHint": " \u2014 taking long? Check for a proxy/VPN (e.g., Clash TUN)",
|
|
1402
|
+
"error": "\u274C Failed to enable: {detail} (you can retry; for proxy/VPN issues see the README)",
|
|
1403
|
+
"unknownError": "unknown error",
|
|
1404
|
+
"feedback": "\u{1F64F} Questions? Open an issue on GitHub"
|
|
1405
|
+
};
|
|
1406
|
+
|
|
1293
1407
|
// client/index.jsx
|
|
1294
1408
|
var name = "dsh-pocket";
|
|
1295
1409
|
var inject = ["slots", "connection", "layout", "locale", "sessionLogDownload"];
|
|
1410
|
+
function fmt(t, key, vars) {
|
|
1411
|
+
let s = t(key);
|
|
1412
|
+
if (vars) {
|
|
1413
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
1414
|
+
s = String(s).split(`{${k}}`).join(String(v));
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return s;
|
|
1418
|
+
}
|
|
1296
1419
|
var styles = {
|
|
1297
1420
|
card: { background: "var(--dsw-alias-bg-layer-1,#fff)", border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", borderRadius: 12, padding: "16px 20px", maxWidth: 480 },
|
|
1298
1421
|
block: { borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", marginTop: 16, paddingTop: 16 },
|
|
@@ -1305,7 +1428,7 @@ var styles = {
|
|
|
1305
1428
|
qr: { width: 220, height: 220, borderRadius: 10, border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", margin: "8px 0" },
|
|
1306
1429
|
warn: { color: "var(--dsw-alias-state-warn-primary,#b45309)", fontSize: 12, lineHeight: 1.5 }
|
|
1307
1430
|
};
|
|
1308
|
-
function PocketSettingsTab({ rpcCall }) {
|
|
1431
|
+
function PocketSettingsTab({ rpcCall, t }) {
|
|
1309
1432
|
const [status, setStatus] = (0, import_react2.useState)(null);
|
|
1310
1433
|
const [busy, setBusy] = (0, import_react2.useState)(false);
|
|
1311
1434
|
const [error, setError] = (0, import_react2.useState)(null);
|
|
@@ -1315,8 +1438,8 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1315
1438
|
const [isDesktop, setIsDesktop] = (0, import_react2.useState)(false);
|
|
1316
1439
|
const [now, setNow] = (0, import_react2.useState)(Date.now());
|
|
1317
1440
|
(0, import_react2.useEffect)(() => {
|
|
1318
|
-
const
|
|
1319
|
-
return () => clearInterval(
|
|
1441
|
+
const t2 = setInterval(() => setNow(Date.now()), 1e3);
|
|
1442
|
+
return () => clearInterval(t2);
|
|
1320
1443
|
}, []);
|
|
1321
1444
|
const elapsed = (startedAt) => startedAt ? Math.max(0, Math.floor((Date.now() - startedAt) / 1e3)) : 0;
|
|
1322
1445
|
const call = async (endpoint, payload) => {
|
|
@@ -1348,8 +1471,8 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1348
1471
|
};
|
|
1349
1472
|
(0, import_react2.useEffect)(() => {
|
|
1350
1473
|
load();
|
|
1351
|
-
const
|
|
1352
|
-
return () => clearInterval(
|
|
1474
|
+
const t2 = setInterval(load, 3e3);
|
|
1475
|
+
return () => clearInterval(t2);
|
|
1353
1476
|
}, []);
|
|
1354
1477
|
(0, import_react2.useEffect)(() => {
|
|
1355
1478
|
try {
|
|
@@ -1375,10 +1498,10 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1375
1498
|
}
|
|
1376
1499
|
};
|
|
1377
1500
|
check();
|
|
1378
|
-
const
|
|
1501
|
+
const t2 = setInterval(check, 5 * 60 * 1e3);
|
|
1379
1502
|
return () => {
|
|
1380
1503
|
alive = false;
|
|
1381
|
-
clearInterval(
|
|
1504
|
+
clearInterval(t2);
|
|
1382
1505
|
};
|
|
1383
1506
|
}, [isDesktop]);
|
|
1384
1507
|
const restartPocket = async () => {
|
|
@@ -1445,6 +1568,44 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1445
1568
|
} catch {
|
|
1446
1569
|
}
|
|
1447
1570
|
};
|
|
1571
|
+
const [customPin, setCustomPin] = (0, import_react2.useState)(null);
|
|
1572
|
+
const saveCustomPin = async (which) => {
|
|
1573
|
+
try {
|
|
1574
|
+
const r = await call(POCKET_ENDPOINTS.pinSetCustom, { which, value: customPin?.value ?? "" });
|
|
1575
|
+
setStatus((s) => ({
|
|
1576
|
+
...s,
|
|
1577
|
+
accessToken: which === "public" ? r.pin : s.accessToken,
|
|
1578
|
+
lanToken: which === "lan" ? r.pin : s.lanToken,
|
|
1579
|
+
publicPinCustom: which === "public" ? true : s.publicPinCustom,
|
|
1580
|
+
lanPinCustom: which === "lan" ? true : s.lanPinCustom
|
|
1581
|
+
}));
|
|
1582
|
+
setCustomPin(null);
|
|
1583
|
+
} catch (err) {
|
|
1584
|
+
setCustomPin((c) => ({ ...c, err: err.message }));
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
const customPinRow = (which) => (0, import_react2.createElement)(
|
|
1588
|
+
"div",
|
|
1589
|
+
{ style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
|
|
1590
|
+
t("customizing"),
|
|
1591
|
+
(0, import_react2.createElement)("input", {
|
|
1592
|
+
style: { width: 110, margin: "0 6px", padding: "4px 8px", fontSize: 14, letterSpacing: 2, textAlign: "center", border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", borderRadius: 6, outline: "none" },
|
|
1593
|
+
type: "password",
|
|
1594
|
+
inputMode: "numeric",
|
|
1595
|
+
maxLength: 8,
|
|
1596
|
+
value: customPin?.value ?? "",
|
|
1597
|
+
autoFocus: true,
|
|
1598
|
+
onChange: (e) => setCustomPin((c) => ({ ...c, value: e.target.value.replace(/\D/g, ""), err: null })),
|
|
1599
|
+
onKeyDown: (e) => {
|
|
1600
|
+
if (e.key === "Enter") saveCustomPin(which);
|
|
1601
|
+
if (e.key === "Escape") setCustomPin(null);
|
|
1602
|
+
}
|
|
1603
|
+
}),
|
|
1604
|
+
(0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 2 }, onClick: () => saveCustomPin(which) }, t("save")),
|
|
1605
|
+
(0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setCustomPin(null) }, t("cancel")),
|
|
1606
|
+
customPin?.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, customPin.err) : null
|
|
1607
|
+
);
|
|
1608
|
+
const customBtn = (which) => (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: () => setCustomPin({ which, value: "", err: null }) }, t("customize"));
|
|
1448
1609
|
const lanUrl = status?.lanUrl;
|
|
1449
1610
|
const tunnelUrl = status?.tunnelUrl;
|
|
1450
1611
|
const tunnelPhase = tunnelState?.phase ?? "idle";
|
|
@@ -1460,18 +1621,18 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1460
1621
|
(0, import_react2.createElement)(
|
|
1461
1622
|
"div",
|
|
1462
1623
|
null,
|
|
1463
|
-
(0, import_react2.createElement)("strong", null, "
|
|
1464
|
-
(0, import_react2.createElement)("div", { style: styles.muted }, "
|
|
1624
|
+
(0, import_react2.createElement)("strong", null, t("title")),
|
|
1625
|
+
(0, import_react2.createElement)("div", { style: styles.muted }, t("subtitle"))
|
|
1465
1626
|
),
|
|
1466
1627
|
(0, import_react2.createElement)(
|
|
1467
1628
|
"div",
|
|
1468
1629
|
{ style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary,#8b93a1)", textAlign: "right" } },
|
|
1469
|
-
(0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, "
|
|
1470
|
-
(0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, "
|
|
1630
|
+
(0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("developer")),
|
|
1631
|
+
(0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("starAsk")),
|
|
1471
1632
|
(0, import_react2.createElement)(
|
|
1472
1633
|
"a",
|
|
1473
1634
|
{ href: "https://github.com/shaobeichen/dsh-pocket", target: "_blank", rel: "noreferrer", style: { color: "var(--dsw-alias-brand-primary,#4f6ef7)", fontSize: 12, lineHeight: 1.6, textDecoration: "underline" } },
|
|
1474
|
-
"
|
|
1635
|
+
t("starCta")
|
|
1475
1636
|
)
|
|
1476
1637
|
)
|
|
1477
1638
|
),
|
|
@@ -1483,10 +1644,10 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1483
1644
|
(0, import_react2.createElement)(
|
|
1484
1645
|
"div",
|
|
1485
1646
|
{ style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
|
|
1486
|
-
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "
|
|
1487
|
-
(0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, "
|
|
1647
|
+
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("restarted")),
|
|
1648
|
+
(0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, t("ok"))
|
|
1488
1649
|
),
|
|
1489
|
-
(0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" },
|
|
1650
|
+
(0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" }, fmt(t, "bgHint", { cmd: status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9` }))
|
|
1490
1651
|
) : null,
|
|
1491
1652
|
// 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
|
|
1492
1653
|
// 桌面端不渲染(更新由 DSH Desktop 管理)
|
|
@@ -1499,84 +1660,85 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1499
1660
|
(0, import_react2.createElement)(
|
|
1500
1661
|
"div",
|
|
1501
1662
|
{ style: { fontWeight: 600, fontSize: 13 } },
|
|
1502
|
-
updateInfo.updated ?
|
|
1663
|
+
updateInfo.updated ? fmt(t, "updatedRestart", { ver: updateInfo.current }) : updateInfo.result === "ok" ? updateInfo.autoRestart ? fmt(t, "updateAutoRestarting", { ver: updateInfo.latest }) : fmt(t, "updatedOk", { ver: updateInfo.latest }) : fmt(t, "updateAvailable", { ver: updateInfo.latest })
|
|
1503
1664
|
),
|
|
1504
|
-
updateInfo.result !== "ok" ? (0, import_react2.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? "
|
|
1665
|
+
updateInfo.result !== "ok" ? (0, import_react2.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? t("updating") : fmt(t, "updateTo", { ver: updateInfo.latest })) : updateInfo.autoRestart ? (0, import_react2.createElement)("button", { style: styles.btn, disabled: true }, t("restartingNow")) : (0, import_react2.createElement)("button", { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? t("restarting") : t("restartNow"))
|
|
1505
1666
|
),
|
|
1506
1667
|
(0, import_react2.createElement)(
|
|
1507
1668
|
"div",
|
|
1508
1669
|
{ style: styles.muted, marginTop: 4 },
|
|
1509
|
-
updateInfo.updating ?
|
|
1670
|
+
updateInfo.updating ? fmt(t, "updatingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.restarting ? fmt(t, "restartingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.result === "ok" ? updateInfo.autoRestart ? t("updatedAutoDetail") : t("updatedRestartDetail") : updateInfo.result === "fail" ? fmt(t, "updateFailed", { err: updateInfo.output || t("unknownError") }) : fmt(t, "versionRange", { cur: updateInfo.current, latest: updateInfo.latest })
|
|
1510
1671
|
)
|
|
1511
1672
|
) : null,
|
|
1512
1673
|
// 局域网
|
|
1513
1674
|
(0, import_react2.createElement)(
|
|
1514
1675
|
"div",
|
|
1515
1676
|
{ style: styles.block },
|
|
1516
|
-
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "
|
|
1677
|
+
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("lanTitle")),
|
|
1517
1678
|
lanUrl ? (0, import_react2.createElement)(
|
|
1518
1679
|
"div",
|
|
1519
1680
|
null,
|
|
1520
1681
|
(0, import_react2.createElement)("img", { src: status.lanQr, alt: "LAN QR", style: styles.qr }),
|
|
1521
1682
|
(0, import_react2.createElement)("div", { style: styles.code }, lanUrl),
|
|
1522
|
-
(0, import_react2.createElement)("div", { style: styles.muted }, "
|
|
1683
|
+
(0, import_react2.createElement)("div", { style: styles.muted }, t("lanHint")),
|
|
1523
1684
|
// 访问密码开关(issue #24):默认开启;关闭后扫码直连(仅同一局域网设备可访问)
|
|
1524
1685
|
(0, import_react2.createElement)(
|
|
1525
1686
|
"div",
|
|
1526
1687
|
{ style: { display: "flex", alignItems: "center", gap: 8, marginTop: 8 } },
|
|
1527
|
-
(0, import_react2.createElement)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } }, "
|
|
1688
|
+
(0, import_react2.createElement)("span", { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } }, t("lanPin")),
|
|
1528
1689
|
(0, import_react2.createElement)("button", {
|
|
1529
1690
|
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)" },
|
|
1530
1691
|
onClick: () => setLanAuth(true)
|
|
1531
|
-
}, "
|
|
1692
|
+
}, t("on")),
|
|
1532
1693
|
(0, import_react2.createElement)("button", {
|
|
1533
1694
|
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)" },
|
|
1534
1695
|
onClick: () => setLanAuth(false)
|
|
1535
|
-
}, "
|
|
1696
|
+
}, t("off"))
|
|
1536
1697
|
),
|
|
1537
|
-
status?.lanAuthEnabled !== false ? (0, import_react2.createElement)(
|
|
1698
|
+
status?.lanAuthEnabled !== false ? customPin?.which === "lan" ? customPinRow("lan") : (0, import_react2.createElement)(
|
|
1538
1699
|
"div",
|
|
1539
1700
|
{ style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
|
|
1540
|
-
"
|
|
1541
|
-
|
|
1542
|
-
"
|
|
1543
|
-
(0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, "\u5237\u65B0 | Refresh")
|
|
1701
|
+
fmt(t, status?.lanPinCustom ? "lanPinCustomValue" : "lanPinValue", { pin: status.lanToken }),
|
|
1702
|
+
(0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, t("refresh")),
|
|
1703
|
+
customBtn("lan")
|
|
1544
1704
|
) : (0, import_react2.createElement)(
|
|
1545
1705
|
"div",
|
|
1546
1706
|
{ style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } },
|
|
1547
|
-
"
|
|
1707
|
+
t("lanPinOff")
|
|
1548
1708
|
)
|
|
1549
|
-
) : (0, import_react2.createElement)("div", { style: styles.muted }, "
|
|
1709
|
+
) : (0, import_react2.createElement)("div", { style: styles.muted }, t("lanStarting"))
|
|
1550
1710
|
),
|
|
1551
1711
|
// 公网
|
|
1552
1712
|
(0, import_react2.createElement)(
|
|
1553
1713
|
"div",
|
|
1554
1714
|
{ style: styles.block },
|
|
1555
|
-
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, "
|
|
1715
|
+
(0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("wanTitle")),
|
|
1556
1716
|
tunnelUrl ? (0, import_react2.createElement)(
|
|
1557
1717
|
"div",
|
|
1558
1718
|
null,
|
|
1559
1719
|
(0, import_react2.createElement)("img", { src: status.tunnelQr, alt: "Tunnel QR", style: styles.qr }),
|
|
1560
1720
|
(0, import_react2.createElement)("div", { style: styles.code }, tunnelUrl),
|
|
1561
|
-
(0, import_react2.createElement)("div", { style: styles.muted }, "
|
|
1562
|
-
status.accessToken ? (0, import_react2.createElement)(
|
|
1721
|
+
(0, import_react2.createElement)("div", { style: styles.muted }, t("wanHint")),
|
|
1722
|
+
status.accessToken ? customPin?.which === "public" ? customPinRow("public") : (0, import_react2.createElement)(
|
|
1563
1723
|
"div",
|
|
1564
1724
|
{ style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
|
|
1565
|
-
|
|
1725
|
+
fmt(t, status?.publicPinCustom ? "wanPinCustom" : "wanPin", { pin: status.accessToken }),
|
|
1726
|
+
customBtn("public"),
|
|
1727
|
+
status?.publicPinCustom ? (0, import_react2.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("pinCustomHint")) : null
|
|
1566
1728
|
) : null,
|
|
1567
|
-
(0, import_react2.createElement)("button", { style: styles.btn, onClick: stopTunnel }, "
|
|
1729
|
+
(0, import_react2.createElement)("button", { style: styles.btn, onClick: stopTunnel }, t("stopTunnel"))
|
|
1568
1730
|
) : (0, import_react2.createElement)(
|
|
1569
1731
|
"div",
|
|
1570
1732
|
null,
|
|
1571
|
-
(0, import_react2.createElement)("button", { style: { ...styles.primary, margin: "8px 0" }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? "
|
|
1733
|
+
(0, import_react2.createElement)("button", { style: { ...styles.primary, margin: "8px 0" }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? t("opening") : t("enable")),
|
|
1572
1734
|
tunnelStarting ? (0, import_react2.createElement)(
|
|
1573
1735
|
"div",
|
|
1574
1736
|
{ style: { marginTop: 4, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
|
|
1575
|
-
tunnelPhase === "downloading" ?
|
|
1737
|
+
tunnelPhase === "downloading" ? fmt(t, "downloading", { s: elapsed(tunnelStateStarted) }) : fmt(t, "connecting", { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t("slowHint") : "" })
|
|
1576
1738
|
) : tunnelPhase === "error" ? (0, import_react2.createElement)(
|
|
1577
1739
|
"div",
|
|
1578
1740
|
{ style: { marginTop: 4, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } },
|
|
1579
|
-
|
|
1741
|
+
fmt(t, "error", { detail: tunnelStateDetail || t("unknownError") })
|
|
1580
1742
|
) : null
|
|
1581
1743
|
)
|
|
1582
1744
|
),
|
|
@@ -1588,7 +1750,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1588
1750
|
(0, import_react2.createElement)(
|
|
1589
1751
|
"a",
|
|
1590
1752
|
{ href: "https://github.com/shaobeichen/dsh-pocket/issues", target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", textDecoration: "none" } },
|
|
1591
|
-
"
|
|
1753
|
+
t("feedback")
|
|
1592
1754
|
)
|
|
1593
1755
|
)
|
|
1594
1756
|
);
|
|
@@ -1596,6 +1758,8 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
1596
1758
|
function apply(ctx) {
|
|
1597
1759
|
mobileApply(ctx);
|
|
1598
1760
|
const rpcCall = (endpoint, payload, signal) => ctx.connection.rpc.call(POCKET_RPC_CHANNEL, endpoint, payload, signal);
|
|
1761
|
+
const translate = ctx.locale.bind(NS2);
|
|
1762
|
+
ctx.effect(() => ctx.locale.register(NS2, { zh: zh2, en: en2 }), "dsh-pocket: pocket locale dictionaries");
|
|
1599
1763
|
ctx.slots.inject(
|
|
1600
1764
|
"settings.section",
|
|
1601
1765
|
() => ctx.slots.register(
|
|
@@ -1603,8 +1767,8 @@ function apply(ctx) {
|
|
|
1603
1767
|
name: "settings.section",
|
|
1604
1768
|
id: "pocket",
|
|
1605
1769
|
order: 1,
|
|
1606
|
-
label: () => "
|
|
1607
|
-
inject: () => ({ rpcCall })
|
|
1770
|
+
label: () => translate("section"),
|
|
1771
|
+
inject: () => ({ rpcCall, t: translate })
|
|
1608
1772
|
},
|
|
1609
1773
|
PocketSettingsTab
|
|
1610
1774
|
)
|
package/client/index.jsx
CHANGED
|
@@ -11,10 +11,23 @@ import { createElement as h, useEffect, useState } from 'react';
|
|
|
11
11
|
|
|
12
12
|
import { POCKET_RPC_CHANNEL, POCKET_ENDPOINTS, redactStatus, compareVersions } from './api.js';
|
|
13
13
|
import { mobileApply } from './mobile/mobile-apply.tsx';
|
|
14
|
+
import { NS as POCKET_NS, zh as POCKET_ZH, en as POCKET_EN } from './pocket-locales.js';
|
|
14
15
|
|
|
15
16
|
const name = 'dsh-pocket';
|
|
16
17
|
const inject = ['slots', 'connection', 'layout', 'locale', 'sessionLogDownload'];
|
|
17
18
|
|
|
19
|
+
// 词典在 pocket-locales.js;这里只做「取 key → 替换 {占位符} → 字符串」。
|
|
20
|
+
// 不依赖 DSH t() 的插值能力,避免行为不一致。
|
|
21
|
+
function fmt(t, key, vars) {
|
|
22
|
+
let s = t(key);
|
|
23
|
+
if (vars) {
|
|
24
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
25
|
+
s = String(s).split(`{${k}}`).join(String(v));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
// 官方 DeepSeek Harness 设计系统(dsh-client-ui-theme design-platform.css):
|
|
19
32
|
// 按钮 md=36px 胶囊形 / sm=28px;品牌色 --dsw-alias-brand-primary;
|
|
20
33
|
// hover 走 --dsw-alias-button-*-hover;间距 4px 栅格;正文 13px。
|
|
@@ -31,7 +44,7 @@ const styles = {
|
|
|
31
44
|
warn: { color: 'var(--dsw-alias-state-warn-primary,#b45309)', fontSize: 12, lineHeight: 1.5 },
|
|
32
45
|
};
|
|
33
46
|
|
|
34
|
-
function PocketSettingsTab({ rpcCall }) {
|
|
47
|
+
function PocketSettingsTab({ rpcCall, t }) {
|
|
35
48
|
const [status, setStatus] = useState(null);
|
|
36
49
|
const [busy, setBusy] = useState(false);
|
|
37
50
|
const [error, setError] = useState(null);
|
|
@@ -183,6 +196,44 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
183
196
|
} catch { /* 忽略 */ }
|
|
184
197
|
};
|
|
185
198
|
|
|
199
|
+
// 自定义访问密码(issue #33):公网/局域网各自设固定 8 位数字;自定义后公网不再自动轮换。
|
|
200
|
+
// customPin: { which: 'public'|'lan', value, err } | null —— 正在输入自定义密码的区块
|
|
201
|
+
const [customPin, setCustomPin] = useState(null);
|
|
202
|
+
const saveCustomPin = async (which) => {
|
|
203
|
+
try {
|
|
204
|
+
const r = await call(POCKET_ENDPOINTS.pinSetCustom, { which, value: customPin?.value ?? '' });
|
|
205
|
+
setStatus((s) => ({
|
|
206
|
+
...s,
|
|
207
|
+
accessToken: which === 'public' ? r.pin : s.accessToken,
|
|
208
|
+
lanToken: which === 'lan' ? r.pin : s.lanToken,
|
|
209
|
+
publicPinCustom: which === 'public' ? true : s.publicPinCustom,
|
|
210
|
+
lanPinCustom: which === 'lan' ? true : s.lanPinCustom,
|
|
211
|
+
}));
|
|
212
|
+
setCustomPin(null);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
setCustomPin((c) => ({ ...c, err: err.message }));
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
// 渲染自定义输入行(共用):输入框 + 保存/取消
|
|
218
|
+
const customPinRow = (which) => h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
|
|
219
|
+
t('customizing'),
|
|
220
|
+
h('input', {
|
|
221
|
+
style: { width: 110, margin: '0 6px', padding: '4px 8px', fontSize: 14, letterSpacing: 2, textAlign: 'center', border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', borderRadius: 6, outline: 'none' },
|
|
222
|
+
type: 'password',
|
|
223
|
+
inputMode: 'numeric',
|
|
224
|
+
maxLength: 8,
|
|
225
|
+
value: customPin?.value ?? '',
|
|
226
|
+
autoFocus: true,
|
|
227
|
+
onChange: (e) => setCustomPin((c) => ({ ...c, value: e.target.value.replace(/\D/g, ''), err: null })),
|
|
228
|
+
onKeyDown: (e) => { if (e.key === 'Enter') saveCustomPin(which); if (e.key === 'Escape') setCustomPin(null); },
|
|
229
|
+
}),
|
|
230
|
+
h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 2 }, onClick: () => saveCustomPin(which) }, t('save')),
|
|
231
|
+
h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12 }, onClick: () => setCustomPin(null) }, t('cancel')),
|
|
232
|
+
customPin?.err ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', marginTop: 4 } }, customPin.err) : null,
|
|
233
|
+
);
|
|
234
|
+
// 「自定义」按钮(非输入态显示在密码行末尾)
|
|
235
|
+
const customBtn = (which) => h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: () => setCustomPin({ which, value: '', err: null }) }, t('customize'));
|
|
236
|
+
|
|
186
237
|
const lanUrl = status?.lanUrl;
|
|
187
238
|
const tunnelUrl = status?.tunnelUrl;
|
|
188
239
|
const tunnelPhase = tunnelState?.phase ?? 'idle';
|
|
@@ -193,14 +244,14 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
193
244
|
return h('div', { style: styles.card },
|
|
194
245
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
|
|
195
246
|
h('div', null,
|
|
196
|
-
h('strong', null, '
|
|
197
|
-
h('div', { style: styles.muted }, '
|
|
247
|
+
h('strong', null, t('title')),
|
|
248
|
+
h('div', { style: styles.muted }, t('subtitle')),
|
|
198
249
|
),
|
|
199
250
|
h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary,#8b93a1)', textAlign: 'right' } },
|
|
200
|
-
h('div', { style: { whiteSpace: 'nowrap' } }, '
|
|
201
|
-
h('div', { style: { whiteSpace: 'nowrap' } }, '
|
|
251
|
+
h('div', { style: { whiteSpace: 'nowrap' } }, t('developer')),
|
|
252
|
+
h('div', { style: { whiteSpace: 'nowrap' } }, t('starAsk')),
|
|
202
253
|
h('a', { href: 'https://github.com/shaobeichen/dsh-pocket', target: '_blank', rel: 'noreferrer', style: { color: 'var(--dsw-alias-brand-primary,#4f6ef7)', fontSize: 12, lineHeight: 1.6, textDecoration: 'underline' } },
|
|
203
|
-
'
|
|
254
|
+
t('starCta')),
|
|
204
255
|
),
|
|
205
256
|
),
|
|
206
257
|
|
|
@@ -209,10 +260,10 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
209
260
|
// 重启后提示(进程在后台运行,停止方法)——左侧蓝色色条(桌面端不会触发本插件的自重启)
|
|
210
261
|
!isDesktop && restartNotice ? h('div', { style: { ...styles.block, borderLeft: '4px solid var(--dsw-alias-brand-primary,#4f6ef7)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-2,#f3f4f6)', padding: '10px 12px' } },
|
|
211
262
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
|
|
212
|
-
h('div', { style: { fontWeight: 600, fontSize: 13 } }, '
|
|
213
|
-
h('button', { style: styles.btn, onClick: () => setRestartNotice(false) }, '
|
|
263
|
+
h('div', { style: { fontWeight: 600, fontSize: 13 } }, t('restarted')),
|
|
264
|
+
h('button', { style: styles.btn, onClick: () => setRestartNotice(false) }, t('ok')),
|
|
214
265
|
),
|
|
215
|
-
h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' },
|
|
266
|
+
h('div', { style: styles.muted, marginTop: 4, wordBreak: 'break-all' }, fmt(t, 'bgHint', { cmd: status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9` })),
|
|
216
267
|
) : null,
|
|
217
268
|
|
|
218
269
|
// 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
|
|
@@ -221,85 +272,91 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
221
272
|
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
|
|
222
273
|
h('div', { style: { fontWeight: 600, fontSize: 13 } },
|
|
223
274
|
updateInfo.updated
|
|
224
|
-
?
|
|
275
|
+
? fmt(t, 'updatedRestart', { ver: updateInfo.current })
|
|
225
276
|
: updateInfo.result === 'ok'
|
|
226
|
-
? (updateInfo.autoRestart ?
|
|
227
|
-
:
|
|
277
|
+
? (updateInfo.autoRestart ? fmt(t, 'updateAutoRestarting', { ver: updateInfo.latest }) : fmt(t, 'updatedOk', { ver: updateInfo.latest }))
|
|
278
|
+
: fmt(t, 'updateAvailable', { ver: updateInfo.latest })),
|
|
228
279
|
updateInfo.result !== 'ok'
|
|
229
|
-
? h('button', { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? '
|
|
280
|
+
? h('button', { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? t('updating') : fmt(t, 'updateTo', { ver: updateInfo.latest }))
|
|
230
281
|
: updateInfo.autoRestart
|
|
231
|
-
? h('button', { style: styles.btn, disabled: true }, '
|
|
232
|
-
: h('button', { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? '
|
|
282
|
+
? h('button', { style: styles.btn, disabled: true }, t('restartingNow'))
|
|
283
|
+
: h('button', { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? t('restarting') : t('restartNow')),
|
|
233
284
|
),
|
|
234
285
|
h('div', { style: styles.muted, marginTop: 4 },
|
|
235
286
|
updateInfo.updating
|
|
236
|
-
?
|
|
287
|
+
? fmt(t, 'updatingDetail', { s: elapsed(updateInfo.startedAt) })
|
|
237
288
|
: updateInfo.restarting
|
|
238
|
-
?
|
|
289
|
+
? fmt(t, 'restartingDetail', { s: elapsed(updateInfo.startedAt) })
|
|
239
290
|
: updateInfo.result === 'ok'
|
|
240
|
-
? (updateInfo.autoRestart ? '
|
|
241
|
-
: '
|
|
242
|
-
: updateInfo.result === 'fail' ?
|
|
243
|
-
:
|
|
291
|
+
? (updateInfo.autoRestart ? t('updatedAutoDetail')
|
|
292
|
+
: t('updatedRestartDetail'))
|
|
293
|
+
: updateInfo.result === 'fail' ? fmt(t, 'updateFailed', { err: updateInfo.output || t('unknownError') })
|
|
294
|
+
: fmt(t, 'versionRange', { cur: updateInfo.current, latest: updateInfo.latest })),
|
|
244
295
|
) : null,
|
|
245
296
|
|
|
246
297
|
// 局域网
|
|
247
298
|
h('div', { style: styles.block },
|
|
248
|
-
h('div', { style: { fontWeight: 600, fontSize: 13 } }, '
|
|
299
|
+
h('div', { style: { fontWeight: 600, fontSize: 13 } }, t('lanTitle')),
|
|
249
300
|
lanUrl
|
|
250
301
|
? h('div', null,
|
|
251
302
|
h('img', { src: status.lanQr, alt: 'LAN QR', style: styles.qr }),
|
|
252
303
|
h('div', { style: styles.code }, lanUrl),
|
|
253
|
-
h('div', { style: styles.muted }, '
|
|
304
|
+
h('div', { style: styles.muted }, t('lanHint')),
|
|
254
305
|
// 访问密码开关(issue #24):默认开启;关闭后扫码直连(仅同一局域网设备可访问)
|
|
255
306
|
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 } },
|
|
256
|
-
h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } }, '
|
|
307
|
+
h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } }, t('lanPin')),
|
|
257
308
|
h('button', {
|
|
258
309
|
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)' },
|
|
259
310
|
onClick: () => setLanAuth(true),
|
|
260
|
-
}, '
|
|
311
|
+
}, t('on')),
|
|
261
312
|
h('button', {
|
|
262
313
|
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)' },
|
|
263
314
|
onClick: () => setLanAuth(false),
|
|
264
|
-
}, '
|
|
315
|
+
}, t('off')),
|
|
265
316
|
),
|
|
266
317
|
status?.lanAuthEnabled !== false
|
|
267
|
-
?
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
318
|
+
? (customPin?.which === 'lan'
|
|
319
|
+
? customPinRow('lan')
|
|
320
|
+
: h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
|
|
321
|
+
fmt(t, status?.lanPinCustom ? 'lanPinCustomValue' : 'lanPinValue', { pin: status.lanToken }),
|
|
322
|
+
h('button', { style: { ...styles.btn, height: 26, padding: '0 10px', fontSize: 12, marginLeft: 8 }, onClick: refreshLanPin }, t('refresh')),
|
|
323
|
+
customBtn('lan'),
|
|
324
|
+
))
|
|
273
325
|
: h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-state-warn-primary,#b45309)', lineHeight: 1.5 } },
|
|
274
|
-
|
|
326
|
+
t('lanPinOff')),
|
|
275
327
|
)
|
|
276
|
-
: h('div', { style: styles.muted }, '
|
|
328
|
+
: h('div', { style: styles.muted }, t('lanStarting')),
|
|
277
329
|
),
|
|
278
330
|
|
|
279
331
|
// 公网
|
|
280
332
|
h('div', { style: styles.block },
|
|
281
|
-
h('div', { style: { fontWeight: 600, fontSize: 13 } }, '
|
|
333
|
+
h('div', { style: { fontWeight: 600, fontSize: 13 } }, t('wanTitle')),
|
|
282
334
|
tunnelUrl
|
|
283
335
|
? h('div', null,
|
|
284
336
|
h('img', { src: status.tunnelQr, alt: 'Tunnel QR', style: styles.qr }),
|
|
285
337
|
h('div', { style: styles.code }, tunnelUrl),
|
|
286
|
-
h('div', { style: styles.muted }, '
|
|
338
|
+
h('div', { style: styles.muted }, t('wanHint')),
|
|
287
339
|
status.accessToken
|
|
288
|
-
?
|
|
289
|
-
|
|
340
|
+
? (customPin?.which === 'public'
|
|
341
|
+
? customPinRow('public')
|
|
342
|
+
: h('div', { style: { marginTop: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', lineHeight: 1.5 } },
|
|
343
|
+
fmt(t, status?.publicPinCustom ? 'wanPinCustom' : 'wanPin', { pin: status.accessToken }),
|
|
344
|
+
customBtn('public'),
|
|
345
|
+
status?.publicPinCustom ? h('div', { style: { marginTop: 2, fontSize: 11, color: 'var(--dsw-alias-state-warn-primary,#b45309)' } }, t('pinCustomHint')) : null,
|
|
346
|
+
))
|
|
290
347
|
: null,
|
|
291
|
-
h('button', { style: styles.btn, onClick: stopTunnel }, '
|
|
348
|
+
h('button', { style: styles.btn, onClick: stopTunnel }, t('stopTunnel')),
|
|
292
349
|
)
|
|
293
350
|
: h('div', null,
|
|
294
|
-
h('button', { style: { ...styles.primary, margin: '8px 0' }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? '
|
|
351
|
+
h('button', { style: { ...styles.primary, margin: '8px 0' }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy ? t('opening') : t('enable')),
|
|
295
352
|
tunnelStarting
|
|
296
353
|
? h('div', { style: { marginTop: 4, fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)' } },
|
|
297
354
|
tunnelPhase === 'downloading'
|
|
298
|
-
?
|
|
299
|
-
:
|
|
355
|
+
? fmt(t, 'downloading', { s: elapsed(tunnelStateStarted) })
|
|
356
|
+
: fmt(t, 'connecting', { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t('slowHint') : '' }))
|
|
300
357
|
: tunnelPhase === 'error'
|
|
301
358
|
? h('div', { style: { marginTop: 4, fontSize: 12, color: 'var(--dsw-alias-state-error-primary,#dc2626)' } },
|
|
302
|
-
|
|
359
|
+
fmt(t, 'error', { detail: tunnelStateDetail || t('unknownError') }))
|
|
303
360
|
: null,
|
|
304
361
|
),
|
|
305
362
|
),
|
|
@@ -309,7 +366,7 @@ function PocketSettingsTab({ rpcCall }) {
|
|
|
309
366
|
// 页面最底部:反馈入口
|
|
310
367
|
h('div', { style: { ...styles.block, textAlign: 'center' } },
|
|
311
368
|
h('a', { href: 'https://github.com/shaobeichen/dsh-pocket/issues', target: '_blank', rel: 'noreferrer', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', textDecoration: 'none' } },
|
|
312
|
-
'
|
|
369
|
+
t('feedback')),
|
|
313
370
|
),
|
|
314
371
|
);
|
|
315
372
|
}
|
|
@@ -321,6 +378,10 @@ export function apply(ctx) {
|
|
|
321
378
|
const rpcCall = (endpoint, payload, signal) =>
|
|
322
379
|
ctx.connection.rpc.call(POCKET_RPC_CHANNEL, endpoint, payload, signal);
|
|
323
380
|
|
|
381
|
+
// 设置页签接入 DSH 本地化:注册 pocket 词典(zh/en),并绑定一个随当前 locale 切换的 t()。
|
|
382
|
+
const translate = ctx.locale.bind(POCKET_NS);
|
|
383
|
+
ctx.effect(() => ctx.locale.register(POCKET_NS, { zh: POCKET_ZH, en: POCKET_EN }), 'dsh-pocket: pocket locale dictionaries');
|
|
384
|
+
|
|
324
385
|
// 设置一级入口(与 通用设置/模型/插件 同级,order 1 = 通用之后、最外层)
|
|
325
386
|
ctx.slots.inject('settings.section', () =>
|
|
326
387
|
ctx.slots.register(
|
|
@@ -328,8 +389,8 @@ export function apply(ctx) {
|
|
|
328
389
|
name: 'settings.section',
|
|
329
390
|
id: 'pocket',
|
|
330
391
|
order: 1,
|
|
331
|
-
label: () => '
|
|
332
|
-
inject: () => ({ rpcCall }),
|
|
392
|
+
label: () => translate('section'),
|
|
393
|
+
inject: () => ({ rpcCall, t: translate }),
|
|
333
394
|
},
|
|
334
395
|
PocketSettingsTab,
|
|
335
396
|
),
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// dsh-pocket 设置页签 i18n 词典(与 client/mobile/locales.ts 同款:zh 为源真,en 为译文)。
|
|
2
|
+
// 需要新增字符串时:先加到 zh(key-set 唯一来源),再补 en(key 必须与 zh 完全一致)。
|
|
3
|
+
export const NS = 'pocket'
|
|
4
|
+
|
|
5
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
6
|
+
export const zh = {
|
|
7
|
+
'section': '手机访问',
|
|
8
|
+
'title': '📱 手机访问',
|
|
9
|
+
'subtitle': '手机扫码打开的就是电脑上的这个界面,实时同步',
|
|
10
|
+
'developer': '开发者:程序员少北晨',
|
|
11
|
+
'starAsk': '⭐ 顺手留颗 Star,作者能高兴一整天',
|
|
12
|
+
'starCta': '行,给你一颗 Star',
|
|
13
|
+
'restarted': '🔄 已重启',
|
|
14
|
+
'ok': '知道了',
|
|
15
|
+
'bgHint': '进程在后台运行(不挂终端)。如需停止:{cmd}',
|
|
16
|
+
'updatedRestart': '✅ 已更新 v{ver},重启生效',
|
|
17
|
+
'updateAutoRestarting': '✅ 已更新 v{ver},正在自动重启…',
|
|
18
|
+
'updatedOk': '✅ 已更新 v{ver}',
|
|
19
|
+
'updateAvailable': '📦 新版本 v{ver}',
|
|
20
|
+
'updating': '更新中…',
|
|
21
|
+
'updateTo': '更新到 v{ver}',
|
|
22
|
+
'restartingNow': '正在重启生效…',
|
|
23
|
+
'restarting': '重启中…',
|
|
24
|
+
'restartNow': '🔄 重启 dsh web 生效',
|
|
25
|
+
'updatingDetail': '⏳ 更新中(通常 1-2 分钟)· 已等待 {s} 秒',
|
|
26
|
+
'restartingDetail': '⏳ 正在重启生效(通常 10-30 秒)· 已等待 {s} 秒',
|
|
27
|
+
'updatedAutoDetail': '✅ 已更新,正在自动重启生效,请稍候刷新',
|
|
28
|
+
'updatedRestartDetail': '✅ 已更新,重启 dsh web 生效',
|
|
29
|
+
'updateFailed': '❌ 失败:{err}(手动更新:dsh plugin --profile web update dsh-pocket --latest -w)',
|
|
30
|
+
'versionRange': '当前 v{cur} → 最新 v{latest}',
|
|
31
|
+
'lanTitle': '📶 局域网(同一 WiFi)',
|
|
32
|
+
'lanHint': '手机连接同一 WiFi 后扫码即可打开',
|
|
33
|
+
'lanPin': '局域网访问密码',
|
|
34
|
+
'on': '开',
|
|
35
|
+
'off': '关',
|
|
36
|
+
'lanPinValue': '🔐 访问密码:{pin}(手机打开需输入;与公网密码分开)',
|
|
37
|
+
'lanPinCustomValue': '🔐 访问密码:{pin}(自定义;手机打开需输入)',
|
|
38
|
+
'refresh': '刷新',
|
|
39
|
+
'customize': '自定义',
|
|
40
|
+
'customizing': '新密码(8 位数字):',
|
|
41
|
+
'save': '保存',
|
|
42
|
+
'cancel': '取消',
|
|
43
|
+
'pinInvalid': '密码必须是 8 位数字',
|
|
44
|
+
'pinCustomHint': '自定义后开启公网不再自动换新',
|
|
45
|
+
'lanPinOff': '🔓 密码已关闭:扫码直连,无需密码(仅同一局域网设备可访问;公网仍要密码)',
|
|
46
|
+
'lanStarting': '代理未就绪…',
|
|
47
|
+
'wanTitle': '🌐 公网(人在外面)',
|
|
48
|
+
'wanHint': '任何网络扫码即用(URL 每次重启自动换新)',
|
|
49
|
+
'wanPin': '🔐 访问密码:{pin}(每次开启公网变新;手机打开链接需输入此密码)',
|
|
50
|
+
'wanPinCustom': '🔐 访问密码:{pin}(自定义,开启公网不再自动换新)',
|
|
51
|
+
'stopTunnel': '关闭公网',
|
|
52
|
+
'enable': '开启公网访问',
|
|
53
|
+
'opening': '开启中…',
|
|
54
|
+
'downloading': '⏳ 下载 cloudflared(首次约 20-50MB,通常 1-2 分钟;之后秒开)· 已等待 {s} 秒',
|
|
55
|
+
'connecting': '⏳ 连接 Cloudflare 边缘(通常 5-30 秒)· 已等待 {s} 秒{suffix}',
|
|
56
|
+
'slowHint': ' — 有点久?检查是否开着代理/VPN(Clash TUN 等)',
|
|
57
|
+
'error': '❌ 开启失败:{detail}(可重试;若是代理/VPN 问题见 README 排障)',
|
|
58
|
+
'unknownError': '未知错误',
|
|
59
|
+
'feedback': '有问题?欢迎到 GitHub Issues 反馈 🙏',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** English dictionary, key-identical to the Chinese source of truth. */
|
|
63
|
+
export const en = {
|
|
64
|
+
'section': 'Phone access',
|
|
65
|
+
'title': '📱 Phone access',
|
|
66
|
+
'subtitle': 'The phone shows this exact screen, live',
|
|
67
|
+
'developer': 'Developer: 少北晨 (shaobeichen)',
|
|
68
|
+
'starAsk': '⭐ Drop a Star if it helped — it makes the author’s day',
|
|
69
|
+
'starCta': '★ Give a Star',
|
|
70
|
+
'restarted': '🔄 Restarted',
|
|
71
|
+
'ok': 'Got it',
|
|
72
|
+
'bgHint': 'Running in the background (not attached to a terminal). To stop: {cmd}',
|
|
73
|
+
'updatedRestart': '✅ Updated to v{ver} — restart to apply',
|
|
74
|
+
'updateAutoRestarting': '✅ Updated to v{ver} — auto-restarting…',
|
|
75
|
+
'updatedOk': '✅ Updated to v{ver}',
|
|
76
|
+
'updateAvailable': '📦 Update available: v{ver}',
|
|
77
|
+
'updating': 'Updating…',
|
|
78
|
+
'updateTo': 'Update to v{ver}',
|
|
79
|
+
'restartingNow': 'Restarting to apply…',
|
|
80
|
+
'restarting': 'Restarting…',
|
|
81
|
+
'restartNow': '🔄 Restart dsh web now',
|
|
82
|
+
'updatingDetail': '⏳ Updating (usually 1-2 min) · {s}s elapsed',
|
|
83
|
+
'restartingDetail': '⏳ Restarting to apply (usually 10-30s) · {s}s elapsed',
|
|
84
|
+
'updatedAutoDetail': '✅ Updated — auto-restarting in progress, refresh shortly',
|
|
85
|
+
'updatedRestartDetail': '✅ Updated — restart dsh web to apply',
|
|
86
|
+
'updateFailed': '❌ Failed: {err} (manual update: dsh plugin --profile web update dsh-pocket --latest -w)',
|
|
87
|
+
'versionRange': 'Current v{cur} → latest v{latest}',
|
|
88
|
+
'lanTitle': '📶 LAN (same Wi-Fi)',
|
|
89
|
+
'lanHint': 'Scan to open once your phone is on the same Wi-Fi',
|
|
90
|
+
'lanPin': 'LAN access PIN',
|
|
91
|
+
'on': 'On',
|
|
92
|
+
'off': 'Off',
|
|
93
|
+
'lanPinValue': '🔐 PIN: {pin} (required on the phone; separate from the public PIN)',
|
|
94
|
+
'lanPinCustomValue': '🔐 PIN: {pin} (custom; required on the phone)',
|
|
95
|
+
'refresh': 'Refresh',
|
|
96
|
+
'customize': 'Customize',
|
|
97
|
+
'customizing': 'New PIN (8 digits): ',
|
|
98
|
+
'save': 'Save',
|
|
99
|
+
'cancel': 'Cancel',
|
|
100
|
+
'pinInvalid': 'PIN must be exactly 8 digits',
|
|
101
|
+
'pinCustomHint': 'custom PINs are not rotated on tunnel start',
|
|
102
|
+
'lanPinOff': '🔓 PIN off — scan & go, no PIN (LAN devices only; public still requires PIN)',
|
|
103
|
+
'lanStarting': 'Proxy starting…',
|
|
104
|
+
'wanTitle': '🌐 Anywhere (public)',
|
|
105
|
+
'wanHint': 'Scan from any network (the URL changes on every restart)',
|
|
106
|
+
'wanPin': '🔐 PIN: {pin} (changes each time the tunnel is enabled; required on the phone)',
|
|
107
|
+
'wanPinCustom': '🔐 PIN: {pin} (custom — not rotated on tunnel start)',
|
|
108
|
+
'stopTunnel': 'Stop',
|
|
109
|
+
'enable': 'Enable anywhere',
|
|
110
|
+
'opening': 'Enabling…',
|
|
111
|
+
'downloading': '⏳ Downloading cloudflared (first run ~20-50MB, usually 1-2 min; instant afterward) · {s}s elapsed',
|
|
112
|
+
'connecting': '⏳ Connecting to Cloudflare edge (usually 5-30s) · {s}s elapsed{suffix}',
|
|
113
|
+
'slowHint': ' — taking long? Check for a proxy/VPN (e.g., Clash TUN)',
|
|
114
|
+
'error': '❌ Failed to enable: {detail} (you can retry; for proxy/VPN issues see the README)',
|
|
115
|
+
'unknownError': 'unknown error',
|
|
116
|
+
'feedback': '🙏 Questions? Open an issue on GitHub',
|
|
117
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -15,12 +15,13 @@ import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
|
15
15
|
import { writeFile, readFile, mkdir, rm } from 'node:fs/promises';
|
|
16
16
|
import { join, dirname } from 'node:path';
|
|
17
17
|
import { homedir } from 'node:os';
|
|
18
|
+
import { randomBytes } from 'node:crypto';
|
|
18
19
|
|
|
19
20
|
import { createPocketService } from './service.mjs';
|
|
20
21
|
import { installPocketRpc } from './web-rpc.js';
|
|
21
22
|
import { restartHost } from './restart.js';
|
|
22
23
|
import { desktopEnvPatchScript, advancedNoticeScript, DEFAULT_INJECT } from './proxy.mjs';
|
|
23
|
-
import { lanAuthEnabled, setLanAuthEnabled } from './settings.mjs';
|
|
24
|
+
import { lanAuthEnabled, setLanAuthEnabled, pinCustom, setPinCustom } from './settings.mjs';
|
|
24
25
|
|
|
25
26
|
const name = 'dsh-pocket';
|
|
26
27
|
const inject = ['connection', 'webServer'];
|
|
@@ -42,10 +43,13 @@ function currentVersion() {
|
|
|
42
43
|
/** 进程启动时加载的版本(模块加载瞬间固化;用于识别「磁盘已更新但进程还是旧代码」)。 */
|
|
43
44
|
const loadedVersion = currentVersion();
|
|
44
45
|
|
|
45
|
-
// ---------- 访问密码(issue #13 + #18) ----------
|
|
46
|
+
// ---------- 访问密码(issue #13 + #18 + #33) ----------
|
|
46
47
|
// 公网与局域网**分开**:各自 8 位数字,存本机 $DSH_HOME/dsh-pocket/。
|
|
47
|
-
// 公网密码(token
|
|
48
|
-
// 局域网密码(token-lan
|
|
48
|
+
// 公网密码(token):默认每次开启公网时轮换(旧链接作废);**用户自定义后不再轮换**;
|
|
49
|
+
// 局域网密码(token-lan):默认手动刷新(设置页按钮);自定义后刷新会换回随机值。
|
|
50
|
+
// 会话保持(issue #33):登录 cookie 绑定进程级 sessionKey(见 apply)——
|
|
51
|
+
// dsh web 重启/更新后 sessionKey 变化 → 手机需重新输入。
|
|
52
|
+
const PIN_RE = /^\d{8}$/;
|
|
49
53
|
function writePinToFile(p, fresh) {
|
|
50
54
|
try {
|
|
51
55
|
mkdirSync(dirname(p), { recursive: true });
|
|
@@ -56,7 +60,7 @@ function writePinToFile(p, fresh) {
|
|
|
56
60
|
function readPin(p) {
|
|
57
61
|
try {
|
|
58
62
|
const existing = readFileSync(p, 'utf8').trim();
|
|
59
|
-
if (
|
|
63
|
+
if (PIN_RE.test(existing)) return existing;
|
|
60
64
|
} catch { /* 无文件 */ }
|
|
61
65
|
return null;
|
|
62
66
|
}
|
|
@@ -70,11 +74,12 @@ function tokenPath() {
|
|
|
70
74
|
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), tokenRel);
|
|
71
75
|
}
|
|
72
76
|
/** 当前公网访问密码;文件不存在时生成一个。 */
|
|
73
|
-
function getAccessToken() {
|
|
77
|
+
export function getAccessToken() {
|
|
74
78
|
return readPin(tokenPath()) ?? writePinToFile(tokenPath(), newPin());
|
|
75
79
|
}
|
|
76
|
-
/**
|
|
77
|
-
function rotateAccessToken() {
|
|
80
|
+
/** 轮换公网密码(开启公网时调用)。用户自定义后不再轮换(尊重自定义值)。 */
|
|
81
|
+
export function rotateAccessToken() {
|
|
82
|
+
if (pinCustom('public')) return getAccessToken();
|
|
78
83
|
return writePinToFile(tokenPath(), newPin());
|
|
79
84
|
}
|
|
80
85
|
|
|
@@ -87,14 +92,35 @@ function lanTokenPath() {
|
|
|
87
92
|
function getLanToken() {
|
|
88
93
|
return readPin(lanTokenPath()) ?? writePinToFile(lanTokenPath(), newPin());
|
|
89
94
|
}
|
|
90
|
-
/**
|
|
95
|
+
/** 刷新局域网密码(设置页按钮触发,旧密码立即作废);换新后清除自定义标记。 */
|
|
91
96
|
function refreshLanToken() {
|
|
97
|
+
setPinCustom('lan', false);
|
|
92
98
|
return writePinToFile(lanTokenPath(), newPin());
|
|
93
99
|
}
|
|
94
100
|
/** 按 Host 分发的访问密码:公网隧道用公网密码,局域网用局域网密码。 */
|
|
95
101
|
function tokenForHost(host) {
|
|
96
102
|
return /trycloudflare\.com$/i.test(String(host ?? '')) ? getAccessToken() : getLanToken();
|
|
97
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* 用户自定义访问密码(issue #33):公网/局域网各自设一个固定的 8 位数字。
|
|
106
|
+
* 自定义后公网开启时不再自动轮换(rotateAccessToken 见上)。
|
|
107
|
+
* 非法输入(非 8 位数字)抛错,由 RPC 层转成错误响应。
|
|
108
|
+
*/
|
|
109
|
+
export function setCustomPin(which, value) {
|
|
110
|
+
const v = String(value ?? '').trim();
|
|
111
|
+
if (!PIN_RE.test(v)) throw new Error('密码必须是 8 位数字 | PIN must be exactly 8 digits');
|
|
112
|
+
if (which === 'public') {
|
|
113
|
+
writePinToFile(tokenPath(), v);
|
|
114
|
+
setPinCustom('public', true);
|
|
115
|
+
return v;
|
|
116
|
+
}
|
|
117
|
+
if (which === 'lan') {
|
|
118
|
+
writePinToFile(lanTokenPath(), v);
|
|
119
|
+
setPinCustom('lan', true);
|
|
120
|
+
return v;
|
|
121
|
+
}
|
|
122
|
+
throw new Error('未知密码类型 | unknown PIN kind');
|
|
123
|
+
}
|
|
98
124
|
|
|
99
125
|
const restartNoticeRel = join('dsh-pocket', 'restarted.json');
|
|
100
126
|
function restartNoticePath() {
|
|
@@ -203,16 +229,19 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
203
229
|
injectHtml: isDesktop
|
|
204
230
|
? DEFAULT_INJECT + desktopEnvPatchScript(process.platform) + (desktopAdvanced ? advancedNoticeScript() : '')
|
|
205
231
|
: undefined,
|
|
206
|
-
// 访问密码(issue #13 + #18 + #24
|
|
207
|
-
//
|
|
232
|
+
// 访问密码(issue #13 + #18 + #24 + #33):公网永远要密码(默认每次开启变新,
|
|
233
|
+
// 用户自定义后不再轮换);局域网按开关(默认开启;关闭后局域网扫码直连)。
|
|
208
234
|
auth: {
|
|
235
|
+
// sessionKey:进程级随机密钥——登录 cookie 绑定它(会话保持,issue #33):
|
|
236
|
+
// dsh web 重启/更新后 sessionKey 变化 → 手机需重新输入
|
|
237
|
+
sessionKey: randomBytes(16).toString('hex'),
|
|
209
238
|
getToken: (host) => tokenForHost(host),
|
|
210
239
|
isProtected: (host) => /trycloudflare\.com$/i.test(String(host ?? '')) ? true : lanAuthEnabled(),
|
|
211
240
|
},
|
|
212
|
-
// 每次公网隧道就绪 → 轮换 8
|
|
241
|
+
// 每次公网隧道就绪 → 轮换 8 位密码(旧密码/旧链接立即作废;用户自定义后不轮换)
|
|
213
242
|
onTunnelReady: () => {
|
|
214
243
|
const fresh = rotateAccessToken();
|
|
215
|
-
logger.info('dsh-pocket: public access PIN
|
|
244
|
+
logger.info('dsh-pocket: public access PIN refreshed | 公网访问密码已更新(自定义密码不受影响)');
|
|
216
245
|
return fresh;
|
|
217
246
|
},
|
|
218
247
|
});
|
|
@@ -226,6 +255,8 @@ export function apply(ctx, config = {}, internals = {}) {
|
|
|
226
255
|
refreshLanToken: () => refreshLanToken(),
|
|
227
256
|
getLanAuthEnabled: () => lanAuthEnabled(),
|
|
228
257
|
setLanAuthEnabled: (on) => setLanAuthEnabled(on),
|
|
258
|
+
getPinCustom: (which) => pinCustom(which),
|
|
259
|
+
setCustomPin: (which, value) => setCustomPin(which, value),
|
|
229
260
|
runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate, loadedVersion: () => loadedVersion },
|
|
230
261
|
restart: internals.restart ?? (() => pocketRestart(service)),
|
|
231
262
|
restartNotice: internals.restartNotice ?? consumeRestartNotice,
|
package/lib/proxy.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { createServer } from 'node:http';
|
|
15
15
|
import { request as httpRequest } from 'node:http';
|
|
16
16
|
import { createGzip, createBrotliCompress, constants as zlibConstants } from 'node:zlib';
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
17
18
|
|
|
18
19
|
const DEFAULT_UPSTREAM = { host: '127.0.0.1', port: 3080 };
|
|
19
20
|
|
|
@@ -62,10 +63,21 @@ export function advancedNoticeScript() {
|
|
|
62
63
|
return `<script data-dsh-pocket-advanced-notice="1">!function(){try{var d=document.createElement('div');d.style.cssText='position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.55);color:#fff;font:15px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;text-align:center;padding:24px';d.textContent='DSH 桌面端处于 advanced 模式,手机访问暂不支持。请在桌面端设置中切回 compatibility 模式后重启。| DSH Desktop is in advanced mode — phone access is not supported yet. Switch back to compatibility in the desktop app and restart.';document.documentElement.appendChild(d);}catch(e){}}();</script>`;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
// ---------- 可选访问令牌认证(issue #13) ----------
|
|
66
|
-
//
|
|
67
|
-
// 登录成功后种 HttpOnly
|
|
66
|
+
// ---------- 可选访问令牌认证(issue #13 + #33) ----------
|
|
67
|
+
// 只对受保护 Host(公网隧道 + 局域网按开关)强制。
|
|
68
|
+
// 登录成功后种 HttpOnly 持久 cookie(Max-Age 30 天)→ SPA 内部 API/WS 自动携带。
|
|
69
|
+
// 会话保持(issue #33):cookie 值 = sha256(PIN:sessionKey)——sessionKey 是 dsh web
|
|
70
|
+
// 进程级随机密钥(lib/index.js 每次启动生成)。于是:
|
|
71
|
+
// - 电脑 dsh web 一直开着 → 手机输一次密码后长期免输(持久 cookie)
|
|
72
|
+
// - dsh web 重启/更新 → sessionKey 变化 → 旧 cookie 失效 → 手机重新输入
|
|
68
73
|
const TOKEN_COOKIE = 'dsh_pocket_token';
|
|
74
|
+
const COOKIE_MAX_AGE = 30 * 24 * 60 * 60; // 30 天(秒)
|
|
75
|
+
|
|
76
|
+
/** cookie 校验值:有 sessionKey 时派生,无则退化为 PIN 本身(向后兼容)。 */
|
|
77
|
+
function cookieFor(token, sessionKey) {
|
|
78
|
+
if (!sessionKey) return token;
|
|
79
|
+
return createHash('sha256').update(`${token}:${sessionKey}`).digest('hex');
|
|
80
|
+
}
|
|
69
81
|
|
|
70
82
|
function parseCookies(header) {
|
|
71
83
|
const out = {};
|
|
@@ -115,12 +127,13 @@ function isHtmlRequest(req) {
|
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
/** 校验请求是否已认证;返回 true 放行。 */
|
|
118
|
-
function authCheck(req, token) {
|
|
130
|
+
function authCheck(req, token, sessionKey) {
|
|
119
131
|
if (!token) return true; // 无 token(未配置密码)→ 放行
|
|
132
|
+
const expected = cookieFor(token, sessionKey);
|
|
120
133
|
const cookieTok = parseCookies(req.headers.cookie)[TOKEN_COOKIE];
|
|
121
|
-
if (cookieTok ===
|
|
134
|
+
if (cookieTok === expected) return true;
|
|
122
135
|
const qTok = new URL(req.url ?? '/', 'http://x').searchParams.get('token');
|
|
123
|
-
return qTok ===
|
|
136
|
+
return qTok === expected;
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
/** 把浏览器可见的权威改写成 loopback 权威(Host 和 Origin 都改)。 */
|
|
@@ -144,14 +157,15 @@ function loopbackAuthority(headers, upstream) {
|
|
|
144
157
|
*/
|
|
145
158
|
export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DEFAULT_UPSTREAM, log = null, injectHtml = DEFAULT_INJECT, auth = null } = {}) {
|
|
146
159
|
const server = createServer((req, res) => {
|
|
147
|
-
// 访问令牌认证(issue #13 + #18
|
|
160
|
+
// 访问令牌认证(issue #13 + #18 + #33):局域网与公网按开关/来源要求密码
|
|
148
161
|
if (auth) {
|
|
149
162
|
const host = String(req.headers.host ?? '');
|
|
150
163
|
const isPublic = /trycloudflare\.com$/i.test(host);
|
|
151
164
|
const protectedHost = isProtectedHost(host, auth.isProtected);
|
|
152
165
|
const token = protectedHost ? (auth.getToken?.(host) ?? null) : null;
|
|
166
|
+
const sessionKey = auth.sessionKey ?? null;
|
|
153
167
|
if (protectedHost && token) {
|
|
154
|
-
// 登录提交:校验密码 →
|
|
168
|
+
// 登录提交:校验密码 → 种持久 HttpOnly cookie(30 天,绑定进程会话密钥)→ 回首页
|
|
155
169
|
if (req.method === 'POST' && req.url?.startsWith('/pocket-login')) {
|
|
156
170
|
let body = '';
|
|
157
171
|
req.on('data', (c) => { body += c; if (body.length > 1024) req.destroy(); });
|
|
@@ -160,7 +174,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
160
174
|
if (submitted === token) {
|
|
161
175
|
res.writeHead(302, {
|
|
162
176
|
location: '/',
|
|
163
|
-
'set-cookie': `${TOKEN_COOKIE}=${token}; HttpOnly; SameSite=Lax; Path
|
|
177
|
+
'set-cookie': `${TOKEN_COOKIE}=${cookieFor(token, sessionKey)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${COOKIE_MAX_AGE}`,
|
|
164
178
|
'cache-control': 'no-store',
|
|
165
179
|
});
|
|
166
180
|
res.end();
|
|
@@ -171,7 +185,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
171
185
|
});
|
|
172
186
|
return;
|
|
173
187
|
}
|
|
174
|
-
if (!authCheck(req, token)) {
|
|
188
|
+
if (!authCheck(req, token, sessionKey)) {
|
|
175
189
|
if (isHtmlRequest(req)) {
|
|
176
190
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
177
191
|
res.end(loginPageHtml(false, isPublic));
|
|
@@ -267,7 +281,7 @@ export function createPocketProxy({ port = 3081, host = '0.0.0.0', upstream = DE
|
|
|
267
281
|
if (auth) {
|
|
268
282
|
const host = String(req.headers.host ?? '');
|
|
269
283
|
const token = isProtectedHost(host, auth.isProtected) ? (auth.getToken?.(host) ?? null) : null;
|
|
270
|
-
if (token && !authCheck(req, token)) {
|
|
284
|
+
if (token && !authCheck(req, token, auth.sessionKey ?? null)) {
|
|
271
285
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n');
|
|
272
286
|
socket.destroy();
|
|
273
287
|
return;
|
package/lib/settings.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// dsh-pocket 设置持久化($DSH_HOME/dsh-pocket/settings.json)
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// 当前项:
|
|
4
|
+
// - lanAuthEnabled 局域网访问密码开关(issue #24),默认开启
|
|
5
|
+
// - publicPinCustom 公网密码是否用户自定义(issue #33),自定义后不自动轮换
|
|
6
|
+
// - lanPinCustom 局域网密码是否用户自定义(issue #33)
|
|
4
7
|
// 默认**开启**(安全优先):局域网扫码也要输 8 位密码;
|
|
5
8
|
// 用户可关闭——关闭后局域网扫码直连(仅同一网络内的设备能访问),公网不受影响(永远要密码)。
|
|
6
9
|
|
|
@@ -41,3 +44,25 @@ export function setLanAuthEnabled(on) {
|
|
|
41
44
|
writeSettings(s);
|
|
42
45
|
return s.lanAuthEnabled;
|
|
43
46
|
}
|
|
47
|
+
|
|
48
|
+
// ---------- 访问密码「自定义」标记(issue #33) ----------
|
|
49
|
+
// 用户可把公网/局域网密码设成自己固定的 8 位数字(自定义后不再自动轮换)。
|
|
50
|
+
// 标记存 settings.json:publicPinCustom / lanPinCustom。
|
|
51
|
+
const PIN_CUSTOM_KEYS = { public: 'publicPinCustom', lan: 'lanPinCustom' };
|
|
52
|
+
|
|
53
|
+
/** 该 PIN(public | lan)是否用户自定义过(自定义后不自动轮换)。 */
|
|
54
|
+
export function pinCustom(which) {
|
|
55
|
+
const key = PIN_CUSTOM_KEYS[which];
|
|
56
|
+
if (!key) return false;
|
|
57
|
+
return readSettings()[key] === true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 设置自定义标记,返回新状态。 */
|
|
61
|
+
export function setPinCustom(which, on) {
|
|
62
|
+
const key = PIN_CUSTOM_KEYS[which];
|
|
63
|
+
if (!key) return false;
|
|
64
|
+
const s = readSettings();
|
|
65
|
+
s[key] = !!on;
|
|
66
|
+
writeSettings(s);
|
|
67
|
+
return !!on;
|
|
68
|
+
}
|
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 }) {
|
|
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, 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,8 @@ 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
|
+
publicPinCustom: getPinCustom?.('public') ?? false,
|
|
50
|
+
lanPinCustom: getPinCustom?.('lan') ?? false,
|
|
49
51
|
});
|
|
50
52
|
};
|
|
51
53
|
|
|
@@ -63,6 +65,17 @@ export function installPocketRpc(ctx, { service, log = console, desktop = false,
|
|
|
63
65
|
if (enabled === undefined) return fail('bad-request', '局域网密码开关不可用 | LAN PIN switch unavailable');
|
|
64
66
|
return ok({ lanAuthEnabled: enabled });
|
|
65
67
|
}
|
|
68
|
+
if (endpoint === POCKET_ENDPOINTS.pinSetCustom) {
|
|
69
|
+
const which = payload?.which === 'public' || payload?.which === 'lan' ? payload.which : null;
|
|
70
|
+
if (!which) return fail('bad-request', '未知密码类型 | unknown PIN kind');
|
|
71
|
+
try {
|
|
72
|
+
const pin = setCustomPin?.(which, payload?.value);
|
|
73
|
+
if (pin === undefined) return fail('bad-request', '自定义密码不可用 | custom PIN unavailable');
|
|
74
|
+
return ok({ which, pin, custom: true });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return fail('bad-request', err?.message ?? String(err));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
66
79
|
if (endpoint === POCKET_ENDPOINTS.tunnelStart) {
|
|
67
80
|
await service.startTunnel();
|
|
68
81
|
return await statusPayload();
|
package/package.json
CHANGED