把官方 DSH Web UI 安全地开放到局域网:不改动 DSH 自身的回环绑定,另开一个带密码门禁、自签 HTTPS 与可逐个吊销的设备配对的反向代理端口,并提供手机扫码入口。
安装
# npm 包(预构建)
dsh plugin --profile web add dsh-lan-guard
# GitHub 源码(首次需按提示配置 allowBuilds 构建授权后重试)
dsh plugin --profile web add github:idoall/dsh-lan-guard
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络,工具审批管不到它。GitHub 来源的插件还会在安装时执行构建脚本——pnpm 默认拦截,所以安装可能停在 ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED 或 ERR_PNPM_IGNORED_BUILDS;dsh 会打印出需要添加的确切键名,把它加进该 profile 的 pnpm-workspace.yaml 的 allowBuilds 下,重跑一次即可装上。放行构建本身就是一次信任判断:请只安装可信来源,并尽量锁定 commit(github:owner/repo#sha)。
README
DSH LAN Guard 是 DeepSeek Harness 社区插件。它只注册官方设置页里的一个区块(
settings.section),不替换任何官方布局,也不修改 DSH 源码或 DSH 自身的监听绑定。
DSH 的 Web 界面只监听 127.0.0.1,而官方明确拒绝绑定 0.0.0.0。本插件不碰那个绑定,而是在另一个端口上再开一个带门禁的监听,把官方界面原样代理出去:密码门禁、默认自签 HTTPS、手机扫码入口,以及可逐个批准与拉黑的设备配对。装完重启一次即可用——不需要手写任何配置。
能做什么
- 门禁反向代理:完整转发 HTTP 与 WebSocket(含官方界面的
/api/remote.mux长连接),改写Host/Origin,剥离逐跳头,上游不可达时返回502。DSH 自身的绑定与配置一动不动。 - 默认对外可用,但对外可达 ≠ 可以进入:默认监听
0.0.0.0,所以安装后只需重启一次就能用;门禁默认开启,未设访问密码时拒绝所有设备,且默认自签 HTTPS——不存在明文传输。想只在本机使用,在设置页切到「仅本机」即可。 - 双密码门禁:PBKDF2-SHA256(600,000 次迭代)。访问密码给手机等访客设备登录用,管理密码用于解锁本设置页的管理台(未设置时退回访问密码);另有
dsh_免密链接、持久访客会话、按 IP 锁定与 CSRF 校验。 - 默认自签 HTTPS,CA 身份跨重启不变:自动生成
DSH LAN Guard CA,按所选网卡地址签发叶证书。换 IP 只重签叶证书,所以每台设备只需信任一次。 - 本机永不锁定:直连
127.0.0.1享有物理免锁特权(能用这台电脑的人本就能改这些设置);远程访问按adminPolicy处理——只读(默认)、需密码解锁、或不锁。 - 设备配对与身份拉黑:手机首次通过门禁时命名,获得 HttpOnly 设备身份 cookie,出现在 已授权设备 里(名称 / 创建时间 / 最近使用 / 来源 IP),可逐个吊销或拉黑该身份。拉黑持续作用于已记录身份,不等于封禁物理设备:换浏览器或清除 cookie 后,仍可用共享访问密码重新配对。需要审核重新配对时,请开启新设备批准。
- 设置挂在官方设置页内:「局域网访问」分区含四个 tab——扫码访问、安全认证、已授权设备、连接与证书。排版与配色全部使用官方设计 token,不替换任何官方布局。
- 远程也能添加工作区(智能分流):DSH 的目录选择器在启动时判定一次,本机回环绑定 + 有显示器会判成「原生」——手机点「添加工作区」实际是在电脑屏幕上弹文件夹对话框。本插件在浏览器侧以更低优先级遮蔽官方选择流程:本机浏览器照旧走系统原生对话框,远程设备改用页面内目录浏览器(面包屑、快捷入口、目录列表)。选中后仍由 DSH 官方工作区流程登记,插件只读目录、不写任何东西。
- 端口与监听范围可配:默认
3081(DSH 端口 + 1),被占用时自动往后顺延(最多试 10 个);设置页可改端口并带可用性检查,也可在「局域网(默认)/ 仅本机」之间切换(两者都需重启 dsh 生效)。 - 可选 mDNS:默认关闭;开启后广播
_dsh-lan-guard._tcp。 - 升级检测:设置页显示「当前版本 → npm 上最新版本」并给出可复制的升级命令;插件不会自己安装或重启任何东西。
快速开始
环境要求:
- 带 Web profile 的 DeepSeek Harness
- Node.js 20 或更新
- 已验证的 DeepSeek Harness:
0.2.2-rc.1(接口审查、新版依赖类型检查与自动回归;另在运行 rc.1 的实例上只读核对代理入口、页面注入与管理接口。本次未重新进行 rc.1 浏览器交互、完整 LAN/WebSocket 与手机实机验收;alpha.3 的浏览器测量属于上一版验证)。 - 2026-11-04 核对 npm 渠道:
next为0.2.2-rc.1,alpha为0.2.2-alpha.3,latest为0.2.1-rc.5。本次验证0.2.2-rc.1;0.2.1-rc.5不在已验证清单内。
从 npm 安装:
dsh plugin --profile web add dsh-lan-guard@latest
从 GitHub 安装:
dsh plugin --profile web add "github:idoall/dsh-lan-guard"
从本地克隆安装:
git clone https://github.com/idoall/dsh-lan-guard.git
cd dsh-lan-guard
pnpm install && pnpm run build
dsh plugin --profile web add "link:$(pwd)"
然后重启一次 DSH,打开 设置 → 局域网访问。重启后插件已经在对外监听(默认 0.0.0.0:3081,自签 HTTPS + 门禁),你只需要:
- 在 安全认证 里设置访问密码(至少 8 位)。没设之前,门禁拒绝所有设备。
- 在 连接与证书 确认监听范围(默认「局域网」),并选择对外公布的网卡——网卡决定二维码/访问地址用哪个 IP、自签证书签哪些地址。
- 在 扫码访问 扫码,手机上信任一次
DSH LAN Guard CA、输入访问密码、给设备命名。之后手机运行的就是官方 DSH 界面。
远程设备默认是只读的(
adminPolicy: local_only):能用 DSH,但不能改插件设置。想让手机也能管理,在桌面把策略改掉。
设置
设置项集中在 设置 → 局域网访问 的四个 tab 里。非敏感开关(enabled、listenPort、listenHost、networkInterface、settingsUnlock、answerHeartbeat、socketWatchdog、mobileCompat、mobileScrollFix、mobileTurnRail、auth.mode、auth.adminPolicy、auth.adminProtection、auth.allowLoopback、auth.requirePairing、auth.requireApproval)可直接改;listenPort 与 listenHost 需重启 dsh 生效;settingsUnlock / socketWatchdog / mobileCompat / mobileScrollFix / mobileTurnRail 刷新页面即可生效,answerHeartbeat 立即对已打开的连接生效;dataDir 与 tls.* 属启动期字段,需在 profile patch 里改。
| 设置项 | 默认 | 作用 |
|---|---|---|
| 监听范围 | 局域网 0.0.0.0 |
决定局域网能否访问这个端口。切到「仅本机 127.0.0.1」更保守,需重启 dsh。两种选择都保留门禁与自签 HTTPS。 |
| 代理端口 | 3081 | DSH 端口 + 1;被占用时自动往后顺延(最多试 10 个),带可用性检查。需重启 dsh。 |
| 对外公布的网卡 | 自动选择 | 决定二维码/访问地址用哪个 IP、自签证书签哪些地址;虚拟网卡会被降权标注。 |
| 验证模式 | 扫码免密 + 密码 | 也可选「仅密码」或「仅安全 Token」。切换会吊销所有已有访客会话。 |
| 访问密码 | 未设置 | 手机等访客设备的登录密码。未设置时门禁拒绝所有设备。 |
| 管理密码 | 未设置 | 解锁本设置页的管理台;未设置时退回访问密码。 |
| 本机回环访问免密 | 开 | 127.0.0.1 直连跳过门禁(物理免锁)。 |
| 远程设备管理权限 | 仅本机 | 决定局域网设备能否管理:「仅本机」只读;「密码解锁」需先解锁;「不锁定」不额外要求。远程浏览目录并添加工作区需要后两者之一。 |
| 管理操作需要先解锁 | 开 | 关闭后,符合策略的远程会话可直接修改设置(local_only 除外,它永远只允许本机)。 |
| 局域网设备可用官方设置页 | 开 | DSH 官方设置面默认只对回环页面开放,局域网设备打开「设置 → 模型」会提示 settings are unavailable in this browser。开启后,通过门禁的设备(含手机)刷新页面即可使用官方设置页;这是界面解锁,不是新增权限——设置接口本来就只由门禁把关,读取密钥仍由 DSH 脱敏。刷新页面生效,无需重启。 |
| 代理代答心跳 | 开 | DSH 每 2 秒给每条 WebSocket 发心跳,连续两次没被回应就断开(实测 6 秒)。手机锁屏/切走时页面被系统挂起、回不了心跳,于是只走 WebSocket 的会话记录就会「载入不全、甚至断开」。开启后由代理替手机回心跳,手机自己回的那次是重复包,无副作用。立即生效(含已打开的连接)。 |
| 断线看门狗 | 开 | 页面补丁:卡在「连接中」超过 8 秒的 WebSocket 会被关掉;从后台回来 10 秒后仍一条都没连上时自动重载一次(每标签最多连续 3 次,冷却 20 秒起)。只对真的连不上的页面生效。刷新页面生效。 |
| 移动端兼容垫片 | 开 | 页面补丁:补 AbortSignal.any/AbortSignal.timeout/Promise.withResolvers/Iterator 与移动端 meta。缺这些 API 时 DSH 客户端在会话流里抛错,界面只会一直显示「载入历史…」且没有报错。现代浏览器上这些分支不生效。刷新页面生效。 |
| 手机滚动矫正(窄屏) | 开 | 手机端 DSH 外壳在窄屏下把对话列裁在 overflow:hidden 的层里(实测 844/1688),整页也不可滚 → 内容可见但滑不动。开启后只在「窄屏 + 移动端 + 整页不可滚 + 找到被裁剪溢出的层」四条同时成立时,把那几层改成可触摸滚动;正常页面不碰。页面加 ?lgdiag=1 可看布局诊断。刷新页面生效。 |
| 手机端轮次导航 | 关 | 把 PC 右侧那根轮次浮轨搬到手机:DSH 在手机上其实照样渲染它(可跳转到某一轮、未加载的会先翻历史再跳、当前轮次跟随),只是被它自己的窄屏容器查询藏了起来(@container (max-width: 900px))。开启后用一段只在 ≤1023px 生效的样式把它放出来(right:4px、宽 28px,正好占满对话区右侧留白、不压正文),并给手机补一套手势:轻点 = 跳转、按住 ≈0.3 秒 = 弹出可读的轮次卡片(含该轮提问与回复摘要 + ◀▶ 逐轮微调 + 一键跳转)、按住成功后上下滑动 = 连续预览(手指滑到哪一格就换成那一轮的摘要,停在轨道上下边缘会自动继续卷动)、0.3 秒内快速拖动 = 普通滚动轨道。已知短板:每格 28×10px、格间距在官方 JS 里锁死 10px,所以"点得准"靠卡片与 ◀▶ 而不是放大刻度。上游若改了隐藏规则,脚本会在 3 秒内自检失败并整体摘掉样式(页面回到官方原样)。刷新页面生效;?lgdiag=1 底部会多一屏轮次导航诊断。 |
| 新设备需要命名确认 | 开 | 新设备首次通过门禁时要自己命名一次,之后才出现在设备列表里。 |
| 新设备需要管理员批准 | 关 | 开启后,命名完还要你在设备列表点「批准」才能进入。 |
| TLS | 自签 HTTPS | 关闭会明文传输门禁密码;非回环 + 关闭 TLS 必须显式设置 tls.allowInsecureLan: true,否则拒绝启动。 |
插件从它的 Cordis 条目读取配置。所有字段都有可用默认值,装完即可用:
# ~/.dsh/profiles/web/cordis.patch.yml(可选:只写你想改的字段)
- id: dsh-lan-guard
config:
listenHost: 0.0.0.0 # 默认面向局域网;'127.0.0.1' = 仅本机,或写具体网卡 IP
listenPort: 3081 # DSH 端口 + 1;被占用时自动顺延
networkInterface: en0 # 可选:只在一个网卡上公布(留空 = 自动)
settingsUnlock: true # 局域网设备可用官方设置页(默认开;关闭恢复 DSH 默认)
answerHeartbeat: true # 代理代答 WebSocket 心跳(默认开;手机挂起时不被宿主回收)
socketWatchdog: true # 页面断线看门狗(默认开)
mobileCompat: true # 移动端兼容垫片(默认开)
mobileTurnRail: false # 手机端显示官方轮次浮轨(默认关,刷新页面生效)
dataDir: ~/.dsh/profiles/web/data/dsh-lan-guard # 可选;缺省即用这个推导路径
tls:
mode: self-signed # 'self-signed'(默认)| 'provided' | 'off'
allowInsecureLan: false # 局域网明文 HTTP 的显式风险确认
mdns:
enabled: false # 广播 _dsh-lan-guard._tcp
auth:
mode: token_and_password # 'token_and_password' | 'password' | 'token'
adminPolicy: local_only # 'local_only'(默认)| 'password_unlock' | 'open'
adminProtection: true # 管理台需要管理密码
allowLoopback: true # 127.0.0.1 访客跳过门禁(物理免锁)
requirePairing: true # 新的远程设备必须先命名一次
dataDir 是唯一需要解释的一项:不写就用 <当前 profile>/data/dsh-lan-guard(例如 ~/.dsh/profiles/web/data/dsh-lan-guard),写了就优先用你写的(支持 ~ 前缀)。它只影响插件私有数据(密码哈希、设备令牌哈希、会话、自签 CA)的落点,不影响可用性。
兼容性
当前版本:插件 0.7.10,兼容 DeepSeek Harness 0.2.2-rc.1。下表每行是一个插件版本:它验证过哪些 DSH 版本,以及那一版做了什么。
| 插件 | 已验证的 DeepSeek Harness | 这个版本是什么 |
|---|---|---|
0.7.10 (latest) |
0.2.2-rc.1;此前验证版本见下方清单 |
rc.1 兼容版本,开发依赖同步 Cordis 4.1.0 / Schemastery 3.19.0;运行时源码不变。 |
0.7.9 (published) |
0.2.2-alpha.3;此前验证版本见下方清单 |
修复 alpha.3 手机侧栏抽屉裁剪远程目录弹层的问题;同步兼容声明与开发依赖;校正两版 README 中过时的安全边界与手机浮轨说明。 |
0.7.8 (published) |
0.2.2-alpha.2、0.2.1-rc.4、0.2.1-rc.3、0.2.1-rc.2、0.2.1-alpha.5、0.2.1-alpha.4、0.2.1-alpha.3、0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2、0.1.7-rc.1 |
跟上 DSH 0.2.2-alpha.2:新版界面把轮次导航的标签译成 8 种语言,手机浮轨改成不依赖文案的定位(并为将来新增语言留了结构化兜底);其余是兼容声明与文档。 |
0.7.7 (published) |
0.2.1-rc.4、0.2.1-rc.3、0.2.1-rc.2、0.2.1-alpha.5、0.2.1-alpha.4、0.2.1-alpha.3、0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2、0.1.7-rc.1 |
跟上 DSH 0.2.1-rc.4:上游这次只改版本号,宿主接口与类型与 rc.3 逐字节一致;插件运行时代码不变。 |
0.7.6 (published) |
0.2.1-rc.3、0.2.1-rc.2、0.2.1-alpha.5、0.2.1-alpha.4、0.2.1-alpha.3、0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2、0.1.7-rc.1 |
跟上 DSH 0.2.1-rc.3,运行时代码不变。仅完成接口、类型与自动检查;手机/浏览器验收待完成。 |
0.7.5 (published) |
0.2.1-rc.2、0.2.1-alpha.5、0.2.1-alpha.4、0.2.1-alpha.3、0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2 |
跟上 DSH 0.2.1-rc.2:只更新兼容声明,代码没动。 |
0.7.4 |
0.2.1-alpha.5、0.2.1-alpha.4、0.2.1-alpha.3、0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2 |
跟上 DSH 0.2.1-alpha.5:只更新兼容声明,代码没动。 |
0.7.3 |
0.2.1-alpha.4、0.2.1-alpha.3、0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2 |
跟上 DSH 0.2.1-alpha.4:只更新兼容声明,代码没动。 |
0.7.2 |
0.2.1-alpha.3、0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2 |
跟上 DSH 0.2.1-alpha.3:只更新兼容声明,代码没动。 |
0.7.1 |
0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2 |
文档版:README 和历史发布说明改成人话,功能与 0.7.0 完全相同。 |
0.7.0 |
0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2 |
手机端轮次浮轨终于好用:卡片不再跑偏,预览跟着手指走,按住刻度上下滑动就能看每一轮在说什么(默认关闭)。顺带修掉重启后手机白屏的问题。 |
0.6.0 |
0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2 |
手机上「看得见刻度、看不到内容」的问题:改成按住刻度约 0.3 秒弹出卡片,显示那一轮的提问和回复摘要。 |
0.5.0 |
0.2.1-alpha.1、0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2 |
跟上新版 DSH(只改兼容声明);新增手机端轮次浮轨,默认关闭。 |
0.4.6 |
0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2 |
跟上 DSH 0.2.0-rc.2:只更新兼容声明,并确认旧的手机滚动补偿已经不需要了。 |
0.4.5 |
0.2.0-rc.1、0.1.7-rc.2 |
修长会话里正文晚一步加载时被判成「已经正常」、结果仍然滑不动的问题。 |
0.4.4 |
0.2.0-rc.1、0.1.7-rc.2 |
修 iOS 上打开会话不在最新、手指几乎拖不动的问题。 |
0.4.3 |
0.2.0-rc.1、0.1.7-rc.2、0.1.7-rc.1 |
换个修法:改移动端高度链,不再给内容层打补丁,并撤掉自建的「回到底部」按钮。 |
0.4.2 |
0.1.7-rc.2 |
找回官方自带的「回到底部」按钮,并修掉它和全屏遮罩打架、侧边栏发灰的问题。 |
0.4.1 |
0.1.7-rc.2 |
第一版手机滚动修复:只在真的滑不动的页面上动手,正常页面不碰;新增 ?lgdiag=1 诊断。 |
0.4.0 |
0.1.7-rc.2、0.1.7-rc.1 |
手机长连接不再轻易掉线(代理替页面回心跳),并加入断线看门狗和连接体检。 |
0.3.6 |
0.1.7-rc.2、0.1.7-rc.1 |
局域网下也能添加工作区,并修远程设备管理和几处界面问题。 |
0.3.5 |
0.1.7-rc.2、0.1.7-rc.1 |
用 http 访问 TLS 端口会跳到 https 并留日志;删掉设备后旧 cookie 不再把浏览器锁死。 |
0.3.4 |
0.1.7-rc.2、0.1.7-rc.1 |
局域网设备也能用官方设置页(默认开启,只是解锁界面,不新增权限)。 |
0.3.3 |
0.1.7-rc.2、0.1.7-rc.1 |
首次访问的两道关提前说明;失效链接页可以重新登录;拉黑说明与实现对齐。 |
0.3.2 |
0.1.7-rc.2、0.1.7-rc.1 |
装完即用:默认对外监听 + dataDir 自动推导;设置页液体玻璃与官方尺寸;访问地址改为单行滚动 |
0.3.1 |
0.1.7-rc.2、0.1.7-rc.1 |
针对 0.1.7-rc.2 的验证版:代码零改动,只更新兼容元数据 |
0.3.0 |
0.1.7-rc.1 |
设备批准与永久拉黑;修掉打开分享的 ?auth= 链接时的空白页 |
0.2.0 |
0.1.7-rc.1 |
升级检测;移除右下角状态胶囊;间距修复 |
0.1.1 |
0.1.7-rc.1 |
文档版:中英双语用户 README |
0.1.0 |
0.1.7-rc.1 |
首个版本:门禁反向代理、自签 HTTPS、设备配对、设置页、扫码访问 |
- 声明范围
>=0.1.7-rc.1 <0.3.0(dsh.engines.dsh);已验证版本:0.1.7-rc.1、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2、0.2.1-alpha.1、0.2.1-alpha.3、0.2.1-alpha.4、0.2.1-alpha.5、0.2.1-rc.2、0.2.1-rc.3、0.2.1-rc.4、0.2.2-alpha.2、0.2.2-alpha.3、0.2.2-rc.1(共 14 个版本,0.2.2-rc.1验证范围见上文)。未列入的 DSH 版本属未验证,请自行验证后再使用。上界在0.4.3从<0.2.0放宽:profile 加载会拒绝范围不含运行版本的 bundle,而裸的<0.2.0恰好排除0.2.0正式版。 - 0.2.2 新能力评估:
webserver/index-inject的 per-Peer 注入、resource路由与readResource都是 0.2.2 新增;本插件没有采用 —— 前者只影响 fallback 渲染,后两者拿不到客户端地址,而门禁和「仅本机」策略依赖远端地址。真正用上的是多语言:手机浮轨现在识别 DSH 全部 8 种界面语言,并对将来新增语言保留结构化兜底。 - 本插件用到的宿主/客户端接口:
webServer.register/webServer.tapIndex(indexTaps)、connection.requestRejection、connection.authenticatedUrl、追加型settings.sectionseat、@deepseek-ai/schemastery,以及profileContext(用于推导默认数据目录)。 - 破坏性默认值变更(
0.3.2起):listenHost默认由127.0.0.1改为0.0.0.0,装完重启一次即可用;0.3.1及更早默认仅回环。门禁与自签 TLS 的默认值未变(未设密码仍拒绝所有设备)。详见 CHANGELOG。 0.4.0的验证状态:三处改动都做了实测,不只跑用例。- 代理代答心跳:先用 Node 客户端对真实 DSH 测得「停止回 Pong 后 6.0 秒被切断,
close 1006(无 Close 帧)」;再加上代答后同一条链路 20 秒仍存活(收到 10 次 Ping)。仓库里有一条端到端用例复现同一对行为(上游按 40ms 心跳、漏 2 次即 destroy):关掉代答必被回收、打开代答连接存活。 - 页面补丁:注入的脚本在测试里被执行(假环境 + 假时钟),断言「卡在 CONNECTING 8 秒被关掉」「有一条 OPEN 就不重载」「从未建过 socket 不重载」「后台回来无 OPEN 才重载一次、并按冷却与上限退避」「隐藏状态不重载」。
- 浏览器实测(BrowserSkill,Chrome 154,经临时回环转发走完整插件链路):
/api/remote.mux握手 3ms、session/follow首帧 2.1MB 单帧 43–110ms、12 秒 310 帧无中断;同一页面把主线程冻结 12 秒时,无代答的连接收到close 1006。 测试套件 349 项全绿(0.3.6 为 330)。真实 DSH 上的装机验证在发布后进行(本机运行的是已安装的 0.3.6,需重装 + 重启 dsh 才生效)。
- 代理代答心跳:先用 Node 客户端对真实 DSH 测得「停止回 Pong 后 6.0 秒被切断,
手机长连接为什么会断(实测,2026-09-28)
会话记录(就是界面上的「载入历史…」那一段)只走 WebSocket:会话列表、目标条、统计数字都来自 HTTP,所以 WS 一断就会呈现「界面都在、对话区空白」的形状。三个事实决定了这个失效模式:
| 事实 | 实测/来源 |
|---|---|
| 宿主每 2 秒 Ping 一次,连续两次没被回应才回收 | websocketHeartbeatIntervalMs: 2000、MAX_MISSED_HEARTBEATS: 2 |
停止回 Pong 后 6.0 秒被切断(close 1006,无 Close 帧),三处独立复现 |
直连 3081、经转发、浏览器冻结主线程 |
| 手机切走/锁屏时页面被系统挂起,回不了心跳 | iOS 行为;WebKit 另有「后台恢复后 new WebSocket() 永久卡 CONNECTING 而 HTTP 正常」的已知问题 |
因此本版本提供三层兜底,默认全开、可逐项关闭:
- 代理代答心跳(
answerHeartbeat):代理在中转时读取上游→浏览器方向的 WS 控制帧,遇到 Ping 立刻以客户端身份回一个掩码 Pong。RFC 6455 允许未经请求的 Pong,宿主只按「收到过 Pong」重置计数,所以手机自己回的重复包无害。这是唯一能阻止宿主回收挂起手机连接的一层。 - 断线看门狗(
socketWatchdog):页面侧兜住 WebKit 的「卡 CONNECTING」——超时关掉卡住的 socket,从后台回来仍连不上时限流重载一次。 - 移动端兼容垫片(
mobileCompat):补AbortSignal.any/timeout、Promise.withResolvers、Iterator与移动端 meta。DSH 客户端在会话流里直接调用AbortSignal.any,而Session.doOpen会把非传输类异常原样抛出——缺 API 时的表现就是「永久载入历史…、连报错都没有」。
「连接与证书 → 手机连接与自愈」里有一个连接体检:它在这个浏览器里检查缺哪些 API、页面拿到了哪些补丁,并对当前地址做一次真实 WebSocket 握手,同时显示代理侧的计数(在活连接 / 累计升级 / 被拒 / 代答心跳次数)与最近一条连接的时长、上下行字节、是否异常断开。
0.3.6的验证状态:改动包含宿主侧(新增目录列举路由与 cookie 中转)与客户端(目录浏览器、设置页控件、提示层与配色)。测试套件 330 项全绿(+57);三个提交在独立 worktree 里逐提交验证(278 / 304 / 330 各自通过);并做过反证(回退 cookie 中转、固定行不收缩、hover 染当背景,各自让对应用例失败);配色对比度按 DSH 真实 token 逐项计算,浅色/深色 10/10 通过。真实 DSH 上的装机验证在发布后进行。0.3.5的验证状态:改动只在代理的连接层处理与门禁判定,未改动任何宿主/客户端接口;测试套件 273 项全绿(+10),并做过反证(回退修复后对应用例失败);真实 DSH 上的装机验证在发布后进行。0.3.4的验证状态:改动只新增一个宿主侧开关、一行 index 注入与插件自己的设置页开关(webServer.tapIndex是已声明的宿主接口);测试套件 263 项全绿;注入脚本已在真实 Chrome + 非回环地址实测三种状态;真实 DSH 上的装机验证在发布后进行。0.3.3的验证状态:改动只在插件自己的页面、文案与门禁表单可用性,未改动任何宿主/客户端接口;测试套件 247 项全绿;真实 DSH 上的装机验证在发布后进行。
官方 UI 零改动复用,手机视口下自动适配:
手机上为什么「看不到全部消息」/ 滑不动(实测,2026-09-28)
症状:手机上打开会话,最新内容能看到一部分,但手指滑不动;底部固定的「目标条 / 快捷回复 / 输入框」压住正文,右侧滚动条停在中途。
根因(真机视口实测):DSH 官方外壳在窄屏下的根层 pI_x6G_frame 是 overflow:hidden,而内容高于它——实测 clientHeight=844 / scrollHeight=1688,同时整页也不可滚(pageScrolls=false)。内容被裁在容器里,所以"看得见、滑不动"。同一视口在桌面 Chrome 上正常,因此这是 iOS/窄屏的布局差异,与反向代理无关(DOM/CSS 全部来自官方 UI 与已装插件)。
修复(mobileScrollFix,默认开):注入脚本只在四条同时成立时才动手——窄屏(≤1023px)、移动端 UA、整页不可滚、且确实找到被裁剪且内容溢出的层;然后只把那几层改成可触摸滚动(overflow-y:auto、-webkit-overflow-scrolling:touch、touch-action:pan-y)。能正常滚动的页面一律不碰。
「回到底部」按钮:0.4.2 曾短暂提供插件自建按钮,但 0.4.3 已移除。当前版本复用 DSH 官方按钮,只在检查发现裁剪时修正移动端高度与滚动链,不再增加第二个回到底部按钮。
自检:在页面地址后加 ?lgdiag=1,顶部会出现一屏诊断(narrow / mobile / innerHeight / visualViewport / pageScrolls / clippingLayers / 是否已修)。这一屏就是本次定位所用的数据,以后复现同类问题不必连 Mac 调试。
手机上把 PC 右侧的轮次浮轨搬过来(mobileTurnRail,默认关,2026-10-06 新增)
它本来就在,只是被藏了。 PC 上会话右侧那根浮轨(灰刻度=已加载轮次、暗刻度=还没加载、黑长条=当前轮次;悬停出预览;点未加载的会先翻历史再跳)在手机上照样渲染、照样接线——ChatView 无条件挂载它,隐藏它的只有官方自己的一条容器查询:
@container (max-width: 900px) { .frame { display: none } }(容器是对话区,390px 手机上只有约 327px 宽)。
做法:开启后用一段只在 ≤1023px 生效的样式把它放出来,并把它从"压着正文"挪到对话区右侧留白里(right:4px、宽 28px)。
为什么不是重写一个:轮次数据(ui-chat 的 navigation.items())是包内私有的,插件拿不到;而解开隐藏就能原样复用官方组件的跳转、翻历史、当前轮次跟随与预览。因此插件不新建正文滚动容器,但会添加下文所述的触摸预览卡片、卡片按钮与手势监听。
实测(0.2.1-alpha.1 / 390×844,把注入脚本原样打进页面):display:none → block、rect [357,355,24,52]、5 格、间距 10px、恰好 1 格 aria-current;对话列右缘 351、浮轨左缘 357 ⇒ 不压正文;Global panels / Session hierarchy 两个 nav 完全不受影响;0 报错。
手机上为什么必须补一层手势:官方那套提示是悬停交互(onPointerMove / onFocus 驱动预览气泡),触屏根本触发不了——轻点直接走 onClick 跳转,预览永远看不到。所以本插件不改官方逻辑,而是加一种手势:
| 手势 | 行为 |
|---|---|
| 轻点刻度 | 与原来一致:跳转到该轮(未加载的先翻历史) |
| 按住刻度约 0.3 秒 | 弹出轮次卡片:该轮的用户提问 + 回复摘要(文本正是官方组件为悬停渲染的那份,靠派发 pointermove 取出,不调用 focus()) |
| 按住成功后上下滑动 | 连续扫过每一轮:手指滑到哪一格,卡片立刻换成那一轮的摘要——与 PC 上鼠标划过一致 |
| 手指停在轨道上/下边缘 | 按 16ms 步进自动卷动虚拟轨道,可以一直往下或往上扫 |
| 卡片上的 ◀ / ▶ | 逐轮微调,边看边选——不必瞄准 10px 的格子 |
| 卡片上的「跳到第 N 轮」 | 走官方点击,一键跳转后卡片自动关闭 |
| 0.3 秒内快速拖动 | 普通滚动轨道(不会弹卡片) |
卡片只在手机(pointer: coarse)与窄屏下存在;点空白处、✕ 或跳转都会关掉它。卡片挂在官方轨道内(不是 body 上的 fixed 浮层),这是 iPhone Chrome 上不再错位到左上角的关键。
已知短板(所以默认关):每格只有 28×10px,而格间距在官方 JS 里锁死为 10px——用 CSS 撑到 44px 会让相邻格互相覆盖、反而点错。所以精度的答案不是放大刻度,而是上面的卡片与 ◀▶。另外轨道无法再往中间挪:它左边紧贴正文列(实测 440px 视口下轨道 [401,411,28,52]、正文列右缘 401),再往内就会压字。
自检与回退:脚本会在安装后自己确认浮轨真的可见;若上游把隐藏规则改强(更高优先级),它会在约 3 秒内把注入的样式整体摘掉,页面回到与官方逐字节一致的状态,并在 __DSH_LAN_GUARD__.mobileTurnRail 记 "reverted"(连接体检里显示为 轮次导航 ✗ 已回退)。?lgdiag=1 会在屏幕底部多一屏 [lan-guard 轮次导航诊断]。
安全边界
- DSH 自身的监听地址不被改动;本插件从不修改 DSH 配置、会话数据或官方 UI。
- 门禁先于监听:监听器只在门禁对象构造完成之后才打开。默认对外监听之所以可接受,是因为
auth.enabled默认为真、未设访问密码时门禁拒绝所有非回环设备、且 TLS 默认自签——三者必须同时成立。 - 密钥(
secrets.json、devices.json、会话)存放在dataDir,权限600;密码只存 PBKDF2-SHA256 哈希,设备令牌明文只返回一次,落盘只存 SHA-256 哈希,免密链接的 token 不写日志。 - 代理给每个转发请求打上不可伪造的来源标记,宿主据此区分「本机操作者」与「经代理的访客」。
- 回环直连按设计物理免锁——能使用这台电脑的人本就能改这些设置。
- 官方设置页解锁(
settingsUnlock)是界面层的兼容补丁,不是新的传输权限:经代理的请求本就会被改写成回环host,DSH 的设置接口只由门禁把关(读取被 DSH 脱敏),该开关只是让官方页面不再显示「settings are unavailable in this browser」。想恢复 DSH 原生行为,把开关关掉即可。 - 访问密码是共享的:吊销设备会立即让该设备的身份 cookie 失效,但换个浏览器用密码仍可重新配对。要「同一台机器永久拉黑」需要设备指纹或每设备独立令牌,本项目刻意不用指纹。
- Cookie 中转只有一个例外:代理注入 DSH 的回环会话 cookie,且绝不把上游的
set-cookie透传给访客;唯一被中转的是本插件自己的管理会话 cookie(dsh_lan_guard_admin,HttpOnly、SameSite=Strict、默认 30 分钟)。它由本插件签发、也只由本插件的路由校验,但校验代码跑在 DSH 的 web server 上——不中转它,远程设备就永远无法解锁管理台(password_unlock形同虚设)。访客的门禁会话与设备身份 cookie 依旧不会到达 DSH。 - 远程目录浏览器只读,且要管理台同等的权限:它只列举目录名(凭据目录
.ssh/.aws/.env等与系统目录一律拒绝,符号链接解析到真实路径后二次校验,单层上限 1000 行、带限流),不创建、不修改任何文件;登记工作区仍由 DSH 官方流程完成。该接口复用管理台鉴权——本机操作者或已解锁的远程会话可用,默认local_only下远程设备得到read_only_remote。 - 只面向局域网:不做公网隧道、不做 IM Bot、不做端口转发。
- 插件自己不安装、不重启、不推送任何东西:升级命令由你复制执行。
排障
手机提示证书不受信任。 CA 是自签的:每台设备安装/信任一次 DSH LAN Guard CA。信任前先比对 连接与证书 里显示的 SHA-256 指纹。
手机完全连不上。 确认手机在同一网络、地址与二维码一致,并检查是否有 VPN 或「专用代理/中继」类功能拦截流量;也确认 监听范围 没有被切成「仅本机」。
设置页显示「端口已对局域网开放,但尚未设置访问密码」。 这是预期的中间状态:端口可达,但门禁拒绝所有设备,不会泄露数据。去 安全认证 设一个访问密码即可。
「配置的端口 X 已被占用,已自动改用 Y」。 端口被别的程序占着,插件已自行顺延。可在 连接与证书 换端口(带可用性检查),或释放该端口。
局域网设备打开「设置 → 模型」提示 加载提供商目录失败: settings are unavailable in this browser。 这不是本插件的故障:DSH 按页面地址栏是否为回环决定官方设置面是否可用,局域网地址不是回环,于是设置面被永久降级。打开 连接与证书 → 局域网设备可用官方设置页(默认已开),然后在那个设备上刷新页面即可。若仍不行,确认开关确实保存成功、页面是刷新而非切换 tab,并确认 DSH 版本仍在兼容范围内。
「此设备已被移除访问权限」(403)。 该设备已在 已授权设备 中被吊销或拉黑。删除那条记录即可让它重新配对(被拉黑的需先「解除拉黑」)。
远程点「添加工作区」没反应(或对话框开在电脑上)。 这是 DSH 目录选择器的机制:它在启动时判定一次,本机回环绑定 + 有显示器 ⇒ 判成「原生」,对话框开在电脑屏幕上,远程设备看不到也点不到。本插件已把它接管为「本机走系统原生对话框 / 远程走页面内目录浏览器」两套交互。
若远程弹出「远程设备当前只读」,说明策略还是「仅本机」:去电脑的 设置 → 局域网访问 → 安全认证 → 远程设备管理权限 选「密码解锁」(或「不锁定」),然后在远程设备上刷新页面。
若弹出「需要先解锁管理控制台」,直接在弹层里输入管理密码(未设管理密码时用访问密码)点「解锁」,列表会立刻出现——不必再去找设置页的锁定卡片。
我忘了访问密码。 在运行 DSH 的电脑上直连 http://127.0.0.1:3080(本机直连物理免锁)重设。无头服务器则删除 dataDir 下的 secrets.json 后重设——在此之前门禁会拒绝所有设备。
改过密码后所有设备都要重新输密码。 这是有意的:更换访问密码或切换验证模式会吊销所有已有访客会话。
局域网明文 HTTP 被拒绝。 非回环 listenHost + tls.mode: 'off' 会被拒绝,除非显式设置 tls.allowInsecureLan: true——否则门禁密码将明文传输。
升级
设置页会显示「当前版本 → npm 上最新版本」,并给出可复制的升级命令:
dsh plugin --profile web add dsh-lan-guard@latest
插件不会自己安装任何东西,也不会重启 DSH——命令由你执行,执行后手动重启一次 dsh。检测只访问公开的 npm registry,结果缓存 6 小时;连不上时只在界面上提示,不影响门禁与代理。
卸载
dsh plugin --profile web remove dsh-lan-guard
rm -rf ~/.dsh/profiles/web/data/dsh-lan-guard # 可选:删除密钥、设备记录与 CA
开发
pnpm install
pnpm test # 单元 + 集成测试(含类型检查)
pnpm run build # 打包 lib/index.js 与 lib/client.js
pnpm run verify # 类型检查 + 测试 + 构建 + pack dry-run
客户端半边注册到官方追加型 seat settings.section,宿主半边通过包内 cordis.patch.yml 挂载。
文档索引
| 文档 | 用途 |
|---|---|
| docs/dsh-version-adaptation.md | DSH 升级后照着走:diff 哪些包、核对哪些接口、怎么落声明与发版 |
| docs/mobile-regression.md | 电脑上就能跑的窄屏几何回归:两个入口、四个量化判据(+1 条可选的轮次浮轨判据)、免 token 直连 3080 的方法 |
| docs/mobile-acceptance.md | 真机验收清单(触摸、锁屏/切后台、语音这些本地验不了的) |
| docs/mobile-debug-runbook.md | 手机端出问题时的排障手册(含 Web Inspector 探针) |
| docs/upstream-dsh-0.2.0-rc.1-session-scroll.md | 投递上游的回归报告底稿(rc.1 会话裁剪) |
发版
发版由 tag 驱动。更新 package.json、把对应 CHANGELOG 段落移出 Unreleased、编写带中英双锚点的 release-notes/v<版本>.md 后,推送发版提交与 tag:
git tag v0.7.10
git push origin v0.7.10
发版工作流会校验 tag 与 package.json 版本一致、要求发布说明含中英双锚点,然后运行 pnpm run verify、打包插件、通过 npm trusted publishing(OIDC)发布,并创建附带 tarball 的 GitHub Release。
许可
链接
同类插件
zhu1090093659/dsh-web#packages/dsh-remote-web-ui★ 9959
手机/PC 远程操控 dsh web 工作区:扫码配对、令牌门控通道、SSE 实时同步,提供移动端与完整桌面 GUI 两种远程形态。
zhu1090093659/dsh-web#packages/dsh-ssh★ 9959
SSH 远程运维面板:Web 终端、SFTP 传输、本地端口转发与一条命令并发集群执行,Agent 与面板共用同一份主机配置。
liguobao/ds-harness-remote★ 1319
通过端到端加密通道远程访问 DeepSeek Harness 及兼容的编码工作区,支持自适应 Relay/WebRTC 传输、设备授权、工作区文件、只读预览、终端和可选的 Codex Remote。
saya-ch/dsh-mobile★ 728
通过 Android App 或手机浏览器访问 DeepSeek Harness,支持安全局域网连接、远程访问、持久设备配对和可自定义移动界面。
uumit-ai/dsh-uumit-remote★ 381
用 UUMit 账号在手机或其他电脑上打开这台电脑的 dsh web,不用自建服务器、不装其他程序:插件内置通道客户端连接 UUMit 托管的通道,侧栏里的面板负责配对和选择谁能用(仅自己、通过邀请链接或手机号授权的人、所有人),可随时撤销。需 dsh web 且 DSH >=0.2.1-rc.2。
ZSeven-W/dsh-ios★ 355
在对话里直接操作 iOS 模拟器或 USB 连接的 iPhone:22 个 Agent 工具用于启动、构建、按无障碍标识或 OCR 文本驱动 UI、列表行操作与 SwiftUI 预览热重载,并附带可点击拖拽的流式侧边栏面板。
社区评论
评论公开保存在 GitHub Discussions。加载评论会连接 GitHub 和 Giscus;发表内容需要 GitHub 账号。