--- name: dsh-webroad description: DSH Web 手机访问一体化插件(v2.0,原名 dsh-remote-pro 并入):一个包装好,DSH 设置页「📱 手机访问」直接出二维码,手机扫码即用——局域网 + 公网(cloudflared 快速隧道)都免配置,内置 8 位 PIN 访问密码(公网每次开启自动换新/可自定义、登录限速防爆破)、WebSocket 心跳保活、gzip/brotli 压缩、隧道自动恢复、手机端 browse 目录选择器与 polyfill/直达会话补丁;可选"命名隧道 + 固定域名"模式复用已有 Cloudflare 隧道;并保留 Tailscale Funnel 手动方案(files/ 模板)。当用户想从手机/外部浏览器访问本机 dsh web、不想手动搭隧道/改 trustedHosts、或想要设置页内嵌扫码即用的体验时使用。经单元测试 + 真实环境端到端验证。 --- # dsh-webroad:DSH 手机访问一体化插件(v2.0 自包含版) > 一句话:装一个插件,**DSH 设置页里直接有「📱 手机访问」入口**——二维码、密码、开关全在里面;手机扫码(或打开链接)输 8 位密码,就是电脑上的 DSH。局域网、公网都免配置;有固定域名需求时也支持复用你自己的 Cloudflare 命名隧道。 > **v2.0 起,本仓库合并了原 dsh-remote-pro 的自动化能力**(改头代理免配 trustedHosts、PIN+限速、WS 心跳、压缩、二维码、隧道自动恢复、设置页入口)与**原 dsh-Webroad 的 Tailscale Funnel 手动教程**(见文末"备选:Tailscale Funnel 手动方案"),MIT 许可。**dsh-remote-pro 名称不再存在,能力并入 dsh-webroad。** ## 关键点总结(为什么这么设计) | 关键点 | 做法 | |---|---| | **信任围栏零配置** | 代理把入站 `Host/Origin` 改写成 `127.0.0.1:` 再转发——DSH 的 `/api` 围栏永远看到 loopback,**不用改 trustedHosts / 不用 `--trusted-host`**(官方禁了 0.0.0.0 绑定,所以代理自己监听 0.0.0.0:3081) | | **8 位 PIN 认证** | 公网/局域网**独立** PIN;公网默认每次开启自动换新、可自定义固定;局域网默认开启可关。登录种 HttpOnly cookie(30 天,值=sha256(PIN:进程级sessionKey))→ dsh web 重启后需重输一次;**登录限速**(单 IP 5 次/60s 锁 60s + 全局锁)防爆破 | | **WebSocket 心跳** | 透传 `/api/events.mux` + `/api/events.host` 并注入协议层 Ping(30s)——防路由器 NAT/手机省电**静默断链**(无 FIN/RST 时客户端永远不重连);连续 2 周期无流量主动断连触发重连 | | **省流量** | 大 JSON/text 自动 gzip/brotli(长会话 17MB→~1MB,brotli q6) | | **隧道自动恢复** | `$DSH_HOME/dsh-webroad/tunnel-auto.json` 标记,dsh web 重启后自动重拉公网隧道 | | **手机端补丁** | browse 目录选择器(手机可选工作区)+ `crypto.randomUUID`/`AbortSignal.any` polyfill(经代理注入)+ 可选首次直达会话 | | **设置页入口** | client 插件:DSH 设置页 `settings.section` 槽位注册「📱 手机访问」,通过 loopback RPC(`/dsh-webroad` 通道)驱动宿主 | ## 架构 ``` 手机浏览器 ──(LAN http://<电脑IP>:3081 或 公网 https://xxx.trycloudflare.com)──> dsh-webroad 代理(0.0.0.0:3081) ↑ 8 位 PIN(公网必验;局域网可关) │ 改头 Host/Origin → loopback ▼ DSH Web (127.0.0.1:3080) 设置页「📱 手机访问」──loopback RPC(/dsh-webroad)──> 宿主(状态/开公网/改密码/关隧道) ``` ## 安装 ```sh # 1. 装插件(本地路径或发布后的包名) dsh plugin --profile web add <本插件目录绝对路径> # 2. 重启 dsh web(必须) npx @deepseek-ai/dsh web ``` 装完自动发生: 1. **代理**自动起在 `0.0.0.0:3081`(占用自动顺延),改头转发到 dsh web 实际端口; 2. **设置页「📱 手机访问」**入口上线(client 插件,与「通用设置/模型」同级); 3. **手机端补丁**生效(browse 目录选择器 + polyfill + 直达会话)。 ### ⚠️ 与旧插件共存冲突 若 profile 里已装 `dsh-webroad` / `dsh-remote-access`(其 bundle 插入了同 id 的 directory-picker 补丁行),**先卸载旧插件**: ```sh dsh plugin --profile web remove dsh-webroad dsh plugin --profile web remove dsh-remote-access ``` 否则下次启动报 `duplicate loader entry id`。已有 `trustedHosts` 白名单(固定域名等)可保留,对本插件无害。 ## 使用(三种方式等价) ### 1. 设置页(推荐,内嵌)✅ 已验证 DSH 界面 → **设置 → 📱 手机访问** → 看到局域网/公网二维码、地址、密码: - **局域网**:手机连同一 WiFi,扫二维码(或打开地址)→ 输局域网密码 → 就是 DSH - **公网**:点「开启公网访问」(勾选安全免责声明)→ 出公网二维码,人在外面(4G/任何网络)扫码/打开 → 输公网密码 → 就是 DSH - 密码可「换新」/「自定义」;局域网密码可开关;公网可随时「关闭」 ### 2. /pocket 独立面板(同一界面,独立端口) 浏览器打开 `http://127.0.0.1:3081/pocket`(或 DSH 页面右下角「📱 手机访问面板」浮动按钮)→ 输 PIN → 同一套控制台。 ### 3. CLI ```sh dsh-webroad status # 状态:代理/局域网/公网/PIN dsh-webroad qr [lan|public] # 终端打印二维码 dsh-webroad public on|off # 开/关公网隧道 dsh-webroad pin set <8位数字> # 自定义固定 PIN dsh-webroad pin rotate # 换新 PIN ``` ## 固定域名(可选,复用已有 Cloudflare 命名隧道) 把现有 cloudflared 命名隧道的 `config.yml` + `credentials.json` 放到 `$DSH_HOME/dsh-webroad/named/`,并把 `config.yml` 的 ingress service 指到 `127.0.0.1:3081`。之后点「开启公网访问」走的就是命名隧道,得到**固定域名**(如 `dsh.trimut.com`),地址不再漂移。此模式取代原 dsh-remote-access 的功能。 ## 安全(必读) - **DSH 能执行你电脑上的代码**。公网 = 把代码执行入口暴露到互联网:用强密码、用完即关、涉密网络勿用。开启公网时**服务端强制校验免责声明**(绕过前端直接调 RPC 也会被拒)。 - 公网 PIN 默认每次开启换新,旧链接立即作废;局域网 PIN 默认开启(可关)。**请勿把二维码/URL/PIN 发给别人**。 - 登录限速:同一 IP 连续错 5 次锁 60 秒;全局失败超阈值全锁(防分布式扫描)。 - PIN 明文存 `$DSH_HOME/dsh-webroad/`(token / token-lan / settings.json),视同机密。 ## 排障 | 现象 | 原因与解法 | |---|---| | 装完/更新后没变化 | **必须重启 dsh web**;运行中的进程仍加载旧代码 | | 设置页「手机访问」点了没反应 | 宿主侧 RPC 未注册:确认插件 `inject = ['connection','webServer']` 声明存在(v1.1 起);重启生效 | | 局域网二维码打不开 | 路由器/防火墙放行 3081;或面板里手动选局域网 IP(Tailscale/VPN 场景) | | 公网 `error 1033` | 本机代理/VPN(Clash 等 TUN 模式)掐断 cloudflared:关 TUN/退出代理/加直连规则(`DOMAIN-SUFFIX,argotunnel.com,DIRECT` 等);实在不行手机开热点 → 电脑连热点 → 走局域网码 | | 公网开启卡在 starting / 下载失败 | v1.1 起下载有 120s 超时 + 多镜像重试(GitHub 直连国内不通,走 ghproxy 等);或手动 `winget install cloudflared` / 把二进制放 `$DSH_HOME/dsh-webroad/bin/cloudflared.exe` | | `listen EADDRINUSE ... :3081` | 旧进程占端口:`netstat -ano \| findstr :3081` → `taskkill /PID /F` | | 手机白屏/无会话列表 | 确认走了代理(3081)而非直连;代理已内置 WS 透传+心跳 | ## 验证清单(本插件已全部实测通过) - [x] 单元测试 9/9:改头重写(Host/Origin→loopback)、brotli 压缩、认证+限速锁定+cookie、WS 心跳(Ping 帧+静默断链销毁)、/pocket 面板 API、隧道 URL 解析、LAN IP 选择、ipconfig 解析 - [x] 真实 dsh web 端到端:登录 302+cookie → 穿代理访问 DSH 200(围栏放行)+ polyfill 注入 → `/api/events.mux` WS 101 → /pocket 面板渲染 → status API 返回局域网/公网二维码 data URL - [x] 公网隧道实测:cloudflared 快速隧道开启 → 公网 URL 经 Cloudflare 边缘访问返回登录页 → 公网 PIN 登录 → 二维码生成 → 隧道自动恢复标记 - [x] 设置页入口:boot manifest 含 `dsh-webroad` client 条目(inject: connection+ui-slots)→ `/plugins/dsh-webroad/client.js` 被服务 → 宿主 RPC 通道注册(loopback authority) ## 文件结构 | 文件 | 说明 | |---|---| | `lib/proxy.js` | 改头反向代理:Host/Origin→loopback、HTTP+WS 透传、polyfill 注入、gzip/brotli、WS 心跳、PIN 认证+限速、/pocket 面板与 API | | `lib/service.js` | 服务层:LAN IP 智能选择(含 WSL)、二维码、隧道生命周期、自动恢复 | | `lib/tunnel.js` | cloudflared 快速/命名隧道、多镜像下载(超时+重试)、URL 解析 | | `lib/settings.js` | PIN/设置持久化(`$DSH_HOME/dsh-webroad/`) | | `lib/rpc.js` | 127.0.0.1 loopback RPC(CLI 用,端口 3083) | | `lib/web-rpc.js` | 设置页 RPC 通道 `/dsh-webroad`(loopback authority) | | `lib/panel.js` | /pocket 手机管理面板(零构建 HTML) | | `client/` | 设置页「📱 手机访问」client 插件(index.jsx → esbuild → client.js) | | `bin/dsh-webroad.mjs` | CLI | | `cordis.patch.yml` | browse 目录选择器 + 注册插件 | | `skills/` | 本文档(随插件安装到 `~/.dsh/skills/`) | ## 取代关系(迁移说明) - **dsh-remote-access(Cloudflare 固定域名)**:已被「固定域名模式」(named/ 目录 + 命名隧道)吸收,仓库已归档删除(归档见 `docs/dsh-remote-access-archive.md`);原有 `trustedHosts` 白名单可保留不冲突。 - **dsh-remote-pro(原名)**:v2.0 起并入本仓库,名称不再使用,能力全部保留(自动化 + 设置页入口)。 - **原 dsh-Webroad 教程(Tailscale Funnel 手动方案)**:保留为下方备选方案,模板在 `files/`。 --- ## 备选:Tailscale Funnel 手动方案(原 dsh-Webroad 教程,保留) > 不想用 Cloudflare、想完全手动掌控每一步,或作为教学/审计参考时使用。**免装 app、不买域名、免费固定地址**(`https://<设备名>..ts.net`)。模板均在 `files/`(`auth-proxy.js` 已实测:401/Basic 200/Cookie 200/WS 101)。 架构:`手机浏览器 ──HTTPS──> Funnel(免费公网地址) ──> 本地密码代理(127.0.0.1:3099, files/auth-proxy.js) ──> DSH Web(127.0.0.1:3080)` 1. **装 Tailscale**:`winget install Tailscale.Tailscale`(或官网 MSI),登录(国内用微软账号/邮箱验证码)。 2. **启用 Funnel**:`tailscale funnel status` 提示未启用时,浏览器打开提示链接点 Enable Funnel(老 tailnet 先在管理台 DNS 页启用 HTTPS 证书)。 3. **起密码代理**:复制 `files/auth-proxy.js` + `files/auth-config.json.example`(改占位符)到部署目录,`node auth-proxy.js`。 4. **Funnel 转发到代理**:`tailscale funnel --bg 3099`;地址用 `tailscale status --json` 的 `Self.DNSName`(**去掉尾点**)。 5. **信任围栏**:把 Funnel 域名 + Tailscale IP 加进 profile 的 `connection.trustedHosts`(补丁片段见 `files/trustedHosts.patch.snippet.yml`;或启动加 `--trusted-host`)。 6. **验证**:`files/verify-funnel.cmd.example`(替换占位符后运行)→ 无凭据 401 / 带凭据 200 / WS `/api/events.mux` 101;手机浏览器打开 Funnel 地址输密码即可。 7. **自启**:`files/startup.cmd.example`(纯 ASCII)放启动目录;tailscaled 服务自动恢复 Funnel。 > 与主路径的关系:主路径(自动化插件)更省事;本手动方案在你不装插件、或想独立审计每一步时仍然完整可用。