Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ What it looks like — the phone shows the exact same UI as your computer, live:
| 🚪 LAN switch | **Turn LAN access off/on with one click** in Settings (a confirmation dialog shows each time): off kills the LAN QR code and link instantly; public access is unaffected |
| 🌐 Public QR (from anywhere) | Click "Enable anywhere" → cloudflared tunnel → scan the public QR over 4G / any network |
| 🏷️ Fixed public hostname | Optional "**Named tunnel**" mode: paste a Cloudflare Tunnel Token + your own domain — the public address stays **fixed across restarts** (see below) |
| 🔐 Access PIN | Public links require an **8-character PIN** (rotated on every tunnel start by default; **customizable to a fixed PIN** — custom PINs are not rotated); LAN has its own separate **8-character PIN** (on by default; switchable off in Settings — then LAN scans connect directly) |
| 🔑 Custom PINs | Both the public and LAN PINs can be **set to your own fixed 8-character PIN (letters and digits) in Settings** (custom PINs are never auto-rotated) |
| 🔐 Access PIN | Public links use an **8-digit random PIN** by default (rotated on every tunnel start; **customizable to a fixed PIN** — custom PINs are not rotated); LAN has its own separate **8-digit random PIN** (on by default; switchable off in Settings — then LAN scans connect directly) |
| 🔑 Custom PINs | Both the public and LAN PINs can be **set to a fixed 8–64-character PIN using letters and digits in Settings** (custom PINs are never auto-rotated) |
| 🧘 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**) |
| ⚡ Real-time sync | Streaming output passes through WebSocket untouched — what the computer renders, the phone renders live; fully interactive both ways; built-in WS heartbeat keep-alive (defeats silent NAT/battery link drops with auto-reconnect) |
| 📱 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 |
Expand Down Expand Up @@ -82,7 +82,7 @@ npx @deepseek-ai/dsh web

### LAN (same Wi-Fi)

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.
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 an 8–64-character alphanumeric PIN) → the phone opens the exact same DSH, in real time.

> The "**LAN access**" switch is **on by default** and can be **turned off/on with one click** (a confirmation dialog shows each time). Off kills the LAN QR code and link instantly (phones can't open them); **public access is unaffected**. Tap "On" to restore it.
>
Expand All @@ -94,7 +94,7 @@ Settings → **Phone access** → scan the "📶 LAN" QR code → enter the **LA

### Public (from anywhere)

On the same page click "**Enable anywhere**" → **a security disclaimer pops up every time — check "I understand and agree" to proceed** (on a corporate/classified network, confirm compliance first) → 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-character PIN** (shown in the settings page's public section; **rotated on every tunnel start by default**, or **Customize** it to a fixed PIN — letters and digits — that is never rotated) → works from outside (4G / office network).
On the same page click "**Enable anywhere**" → **a security disclaimer pops up every time — check "I understand and agree" to proceed** (on a corporate/classified network, confirm compliance first) → 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 access PIN** (shown in the settings page's public section; the default is an **8-digit random PIN rotated on every tunnel start**, or use **Customize** for a fixed 8–64-character alphanumeric PIN that is never rotated) → works from outside (4G / office network).

> 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).

Expand All @@ -113,7 +113,7 @@ Note: in named-tunnel mode the public PIN is **not auto-rotated** (the address i

- **DSH can execute code on your computer.** **LAN** QR/URL plus its own **8-character 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**.
- **Read and accept the security disclaimer before enabling public access** (the dialog shows on every enable; the server enforces it, so it can't be bypassed): public = exposing a code-executing DSH to the internet — use a strong PIN, turn it off when done, never on classified networks.
- **Public** access is protected by an **8-character 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; custom PINs may use letters and digits).
- **Public** access uses an **8-digit random PIN** by default: the link is random, the PIN rotates on every tunnel start, and old links die instantly — even a leaked link can't get in. **A custom PIN may contain 8–64 letters and digits and is never auto-rotated**.
- 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**.
- **Login rate limiting** (anti brute-force): **5** consecutive wrong PINs from the same IP lock it for **60s**; a global failure threshold briefly locks everyone (blocks distributed IP-rotation scans); a successful login resets the counter.
- The public URL is randomly assigned by cloudflared and **changes on every restart** (old links die automatically — a natural key rotation); in **named-tunnel fixed-hostname** mode the address stays and the PIN is not auto-rotated — manage it with a custom PIN.
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ DSH Pocket 就是干这个的:**装上它,手机扫个码,就能实时看
| 🚪 局域网开关 | 设置页可**一键关闭/开启局域网访问**(切换时弹窗提醒):关闭后局域网二维码/链接立即失效,仅公网可用 |
| 🌐 公网扫码(人在外面) | 点「开启公网访问」→ cloudflared 隧道 → 出公网二维码,4G/任何网络都能访问 |
| 🏷️ 公网固定域名 | 可选「**命名隧道**」模式:填 Cloudflare Tunnel Token + 自己的域名,公网地址**固定不变**(重启不再变;见下方说明) |
| 🔐 访问密码 | 公网链接需输入 **8 位密码**(默认每次开启公网自动换新;**可自定义固定密码**——自定义后不再换新);局域网有独立 **8 位密码**(默认开启,设置页可**一键关闭**——关闭后局域网扫码直连) |
| 🔑 自定义密码 | 公网/局域网密码都可在设置页**设成自己固定的 8 位密码(英文字母大小写或数字)**(自定义后公网不再自动换新) |
| 🔐 访问密码 | 公网链接默认使用 **8 位随机密码**(每次开启公网自动换新;**可自定义固定密码**——自定义后不再换新);局域网有独立的默认 **8 位随机密码**(默认开启,设置页可**一键关闭**——关闭后局域网扫码直连) |
| 🔑 自定义密码 | 公网/局域网密码都可在设置页**设成自己固定的 8–64 位密码(英文字母大小写或数字)**(自定义后公网不再自动换新) |
| 🧘 会话保持 | 手机输一次密码后**长期免输**(登录状态绑定电脑上的 dsh web 进程:只要它不重启,手机不用再输;**dsh web 重启/更新后需重新输入一次**) |
| ⚡ 实时同步 | 流式输出走 WebSocket 全透传——**电脑上在输出,手机上同步在滚**,可双向操作;内置心跳保活(防路由器 NAT/省电机制静默断链,断线自动重连) |
| 📱 移动端适配 | 窄屏自动变抽屉布局(移植 dsh-web-mobile,MIT):侧栏抽屉、会话全宽、状态栏安全区、触控优化 |
Expand Down Expand Up @@ -82,7 +82,7 @@ npx @deepseek-ai/dsh web

### 局域网(同一 WiFi)

设置 → **手机访问** → 手机扫「📶 局域网」二维码 → 打开链接**输入局域网密码**(显示在设置页局域网区块,点「刷新」可换新,或点「自定义」设成自己固定的 8 位密码——英文字母大小写或数字)→ 打开的就是电脑上的 DSH,实时同步。
设置 → **手机访问** → 手机扫「📶 局域网」二维码 → 打开链接**输入局域网密码**(显示在设置页局域网区块,点「刷新」可换新,或点「自定义」设成自己固定的 8–64 位密码——英文字母大小写或数字)→ 打开的就是电脑上的 DSH,实时同步。

> 「**局域网访问**」开关默认**开**:可一键**关闭/开启**(切换时弹窗提醒)——关闭后局域网二维码/链接立即失效(手机打不开),**公网不受影响**;想恢复时再点「开」即可。
>
Expand All @@ -94,7 +94,7 @@ npx @deepseek-ai/dsh web

### 公网(人在外面)

同一页点「**开启公网访问**」→ **每次都会先弹出安全免责声明**,勾选「我已知情」后才能开启(公司/涉密网络请先确认合规)→ 等隧道建立(首次会下载 cloudflared,macOS/Linux 走清华镜像秒下)→ 手机扫「🌐 公网」二维码 → 打开链接**输入 8 位访问密码**(密码显示在设置页公网区块,默认**每次开启公网变新**,也可点「自定义」设成固定密码——英文字母大小写或数字,自定义后不再换新)→ 人在外面(4G/公司网)也能访问。
同一页点「**开启公网访问**」→ **每次都会先弹出安全免责声明**,勾选「我已知情」后才能开启(公司/涉密网络请先确认合规)→ 等隧道建立(首次会下载 cloudflared,macOS/Linux 走清华镜像秒下)→ 手机扫「🌐 公网」二维码 → 打开链接**输入访问密码**(密码显示在设置页公网区块,默认是**每次开启公网变新的 8 位随机密码**,也可点「自定义」设成 8–64 位固定密码——英文字母大小写或数字,自定义后不再换新)→ 人在外面(4G/公司网)也能访问。

> 更新到新版本:`dsh plugin --profile web update dsh-pocket --latest -w`(跨大版本时 `--latest` 是必须的,`^0.x` 范围不会自动升到 1.x)。

Expand All @@ -113,7 +113,7 @@ npx @deepseek-ai/dsh web

- **DSH 能执行你电脑上的代码**。**局域网**二维码/URL 配上独立 **8 位密码**才是钥匙(密码**默认开启**,可关——关闭后局域网扫码直连,仅同一网络设备可访问),**请勿把局域网二维码、URL 或密码发给别人**
- **开启公网访问前必须阅读并勾选免责声明**(每次开启都会弹框;服务端强制校验,无法绕过):公网 = 把能执行代码的 DSH 暴露到互联网,请使用强密码、用完即关、涉密网络勿用
- **公网**有 **8 位密码**保护:链接随机分配、默认每次开启换新密码、旧链接立即作废——泄露了也进不来,改密码/重开即可作废;**自定义密码后不再自动换新**(你设的值即稳定密码,可为英文字母大小写或数字)
- **公网**默认有 **8 位随机密码**保护:链接随机分配、默认每次开启换新密码、旧链接立即作废——泄露了也进不来,改密码/重开即可作废;**自定义密码可设为 8–64 位英文字母或数字,且不再自动换新**
- 手机登录状态与电脑上的 dsh web 进程绑定:**电脑 dsh web 一直开着就不用重复输入;重启/更新后需重新输入一次**
- **登录限速**(防暴力破解):同一 IP 连续输错 **5 次**锁定 **60 秒**;全局失败超阈值时短暂全锁(防换 IP 分布式扫描);输对密码后计数清零
- 公网 URL 由 cloudflared 随机分配,**每次重启会变化**(旧链接自动失效,相当于天然轮换);**命名隧道固定域名**模式下地址不变、密码不自动轮换,请配合自定义密码管理
Expand Down
19 changes: 9 additions & 10 deletions client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -1799,8 +1799,8 @@ function mobileApply(ctx) {
ctx.effect(() => {
if (!narrow.matches) return () => {
};
const PHRASES = ["加载提供方目录失败", "Settings are unavailable in this browser"];
const NOTICE = "手机上不支持模型设置,请去电脑端修改设置";
const PHRASES = ["\u52A0\u8F7D\u63D0\u4F9B\u65B9\u76EE\u5F55\u5931\u8D25", "Settings are unavailable in this browser"];
const NOTICE = "\u624B\u673A\u4E0A\u4E0D\u652F\u6301\u6A21\u578B\u8BBE\u7F6E\uFF0C\u8BF7\u53BB\u7535\u8111\u7AEF\u4FEE\u6539\u8BBE\u7F6E";
const findDeepest = (el) => {
let deepest = el;
for (const child of el.querySelectorAll("*")) {
Expand All @@ -1821,9 +1821,7 @@ function mobileApply(ctx) {
const observer = new MutationObserver(patch);
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
patch();
return () => {
observer.disconnect();
};
return () => observer.disconnect();
}, "dsh-mobile-nav: replace model-settings load error with mobile hint");
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
name: "conversation.session.header.actions",
Expand Down Expand Up @@ -1913,10 +1911,10 @@ var zh2 = {
"lanPinCustomValue": "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A{pin}\uFF08\u81EA\u5B9A\u4E49\uFF1B\u624B\u673A\u6253\u5F00\u9700\u8F93\u5165\uFF09",
"refresh": "\u5237\u65B0",
"customize": "\u81EA\u5B9A\u4E49",
"customizing": "\u65B0\u5BC6\u7801\uFF088 \u4F4D\uFF0C\u82F1\u6587\u5B57\u6BCD\u6216\u6570\u5B57\uFF09\uFF1A",
"customizing": "\u65B0\u5BC6\u7801\uFF088\u201364 \u4F4D\uFF0C\u82F1\u6587\u5B57\u6BCD\u6216\u6570\u5B57\uFF09\uFF1A",
"save": "\u4FDD\u5B58",
"cancel": "\u53D6\u6D88",
"pinInvalid": "\u5BC6\u7801\u5FC5\u987B\u662F 8 \u4F4D\u82F1\u6587\u5B57\u6BCD\u6216\u6570\u5B57",
"pinInvalid": "\u5BC6\u7801\u5FC5\u987B\u662F 8\u201364 \u4F4D\u82F1\u6587\u5B57\u6BCD\u6216\u6570\u5B57",
"pinCustomHint": "\u81EA\u5B9A\u4E49\u540E\u5F00\u542F\u516C\u7F51\u4E0D\u518D\u81EA\u52A8\u6362\u65B0",
"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",
"lanStarting": "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026",
Expand Down Expand Up @@ -2009,10 +2007,10 @@ var en2 = {
"lanPinCustomValue": "\u{1F510} PIN: {pin} (custom; required on the phone)",
"refresh": "Refresh",
"customize": "Customize",
"customizing": "New PIN (8 chars, letters/digits): ",
"customizing": "New PIN (8\u201364 chars, letters/digits): ",
"save": "Save",
"cancel": "Cancel",
"pinInvalid": "PIN must be exactly 8 characters (letters and digits only)",
"pinInvalid": "PIN must be 8\u201364 characters (letters and digits only)",
"pinCustomHint": "custom PINs are not rotated on tunnel start",
"lanPinOff": "\u{1F513} PIN off \u2014 scan & go, no PIN (LAN devices only; public still requires PIN)",
"lanStarting": "Proxy starting\u2026",
Expand Down Expand Up @@ -2312,7 +2310,8 @@ function PocketSettingsTab({ rpcCall, t }) {
(0, import_react2.createElement)("input", {
style: { width: 130, margin: "0 6px", padding: "4px 8px", fontSize: 14, letterSpacing: 1, textAlign: "center", border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", borderRadius: 6, outline: "none" },
type: "password",
maxLength: 8,
minLength: 8,
maxLength: 64,
value: customPin?.value ?? "",
autoFocus: true,
onChange: (e) => setCustomPin((c) => ({ ...c, value: e.target.value.replace(/[^a-zA-Z0-9]/g, ""), err: null })),
Expand Down
5 changes: 3 additions & 2 deletions client/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ function PocketSettingsTab({ rpcCall, t }) {
}
};

// 自定义访问密码(issue #33):公网/局域网各自设固定 8 位密码(英文字母大小写或数字);自定义后公网不再自动轮换。
// 自定义访问密码(issue #33):公网/局域网各自设固定 8–64 位密码(英文字母大小写或数字);自定义后公网不再自动轮换。
// customPin: { which: 'public'|'lan', value, err } | null —— 正在输入自定义密码的区块
const [customPin, setCustomPin] = useState(null);
const saveCustomPin = async (which) => {
Expand All @@ -306,7 +306,8 @@ function PocketSettingsTab({ rpcCall, t }) {
h('input', {
style: { width: 130, margin: '0 6px', padding: '4px 8px', fontSize: 14, letterSpacing: 1, textAlign: 'center', border: '1px solid var(--dsw-alias-border-l2,#d1d5db)', borderRadius: 6, outline: 'none' },
type: 'password',
maxLength: 8,
minLength: 8,
maxLength: 64,
value: customPin?.value ?? '',
autoFocus: true,
onChange: (e) => setCustomPin((c) => ({ ...c, value: e.target.value.replace(/[^a-zA-Z0-9]/g, ''), err: null })),
Expand Down
Loading