安装
在 DeepSeek Harness 里通过 dsh-market 安装
dsh plugin --profile web add dshmarket
或使用命令行
dsh plugin --profile web add dsh-lan-guard
装任何插件都等于在你的机器上跑第三方代码,权限和你本人一样大——能读你的文件、用你的凭据、访问网络。请先审阅源码,并尽量锁定 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),可逐个「吊销并拉黑」。拉黑不依赖设备指纹——换浏览器用密码重新配对也会被拒,「解除拉黑」是唯一的恢复方式。
- 设置挂在官方设置页内:「局域网访问」分区含四个 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.0-rc.2
从 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、auth.mode、auth.adminPolicy、auth.adminProtection、auth.allowLoopback、auth.requirePairing、auth.requireApproval)可直接改;listenPort 与 listenHost 需重启 dsh 生效;settingsUnlock / socketWatchdog / mobileCompat / mobileScrollFix 刷新页面即可生效,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 可看布局诊断。刷新页面生效。 |
| 新设备需要命名确认 | 开 | 新设备首次通过门禁时要自己命名一次,之后才出现在设备列表里。 |
| 新设备需要管理员批准 | 关 | 开启后,命名完还要你在设备列表点「批准」才能进入。 |
| 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 # 移动端兼容垫片(默认开)
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.4.6;兼容 DeepSeek Harness 0.2.0-rc.2(仅声明,零代码)——该版本修掉了 0.2.0-rc.1 的会话裁剪回归,移动端裁剪补偿因此不再介入(保留以兼容旧版本)。
| 插件 | 已验证的 DeepSeek Harness | 这个版本是什么 |
|---|---|---|
0.4.6 |
0.2.0-rc.2、0.2.0-rc.1、0.1.7-rc.2 |
针对 0.2.0-rc.2 的验证版:代码零改动,只更新兼容元数据(dsh.compatibility.dshReleases 补 0.2.0-rc.2),并把两个宿主 devDependency 升到 0.2.0-rc.2 后重跑类型检查与全部用例。上游在 rc.1 → rc.2 之间只动了版本号(唯一源码改动是 ui-renderer 的 hooks 顺序修复,与本插件无关),因此本插件在 rc.2 上零改动可用;同时实测确认移动端裁剪补偿在 rc.2 上已无作用对象(被裁层 1 → 0,可滚范围 336px → 92343px,脚本每轮自检后整体撤销、不写任何内联样式) |
0.4.5 |
0.2.0-rc.1、0.1.7-rc.2 |
长会话的正文是在官方滚动层拿到少量可滚范围之后才挂载的,旧逻辑把这个中间态当成「已正常」收工,晚出现的 overflow:hidden 层再也没人放行(真机实测:可滚范围 336px、滚动层内部仍有 1 层被裁)。现在改为在既有轮询窗口内继续侦测并放行迟到的裁剪层:可滚范围 336px → 6889px,被裁层 1 → 0,官方「回到底部」仍在屏内 |
0.4.4 |
0.2.0-rc.1、0.1.7-rc.2 |
修好 DSH 0.2.0-rc.1 上的 iOS 会话滚动:官方滚动层内部有一层 overflow:hidden 且高度锁死,把会话内容裁掉(真机实测:21083px 的内容只换来 336px 可滚范围),表现为打开会话不在最新、手指几乎拖不动。mobileScrollFix 现在让该层长高并脱离滚动容器身份,可滚范围恢复到 20675px;真机验收同时确认输入框仍吸底、官方「回到底部」按钮回到原位 |
0.4.3 |
0.2.0-rc.1、0.1.7-rc.2、0.1.7-rc.1 |
方向修正:不再给内容层打 overflow 补丁,改修移动端高度链——给 html/body/#root 确定高度、shell 用 100dvh、沿被裁层补 min-height:0,并自检「官方滚动层真的能滚」否则整体回退;移除自建「回到底部」按钮与全部 overflow-y/-webkit-overflow-scrolling 补丁;声明 DSH 0.2.0-rc.1 并把 dsh/peer 范围放宽到 <0.3.0 |
0.4.2 |
0.1.7-rc.2 |
找回官方「回到底部」按钮:窄屏矫正让 conversation 自己的滚动层高度不再增长,官方按钮因此永远认为还在尾部——插件在官方按钮缺席时补一个,官方按钮一恢复就自动让位;同时修复矫正的 overflow-y 补丁与全屏遮罩打架导致侧边栏被灰雾盖住的回归 |
0.4.1 |
0.1.7-rc.2 |
mobileScrollFix 首版,修「手机上聊天区滑不动」:仅在「窄屏 + 移动 UA + 整页不可滚 + 找到被裁剪溢出的层」四条同时成立时才处理,正常页面一律不碰;新增 ?lgdiag=1 布局诊断;真机实测连接存活 86.4 秒 |
0.4.0 |
0.1.7-rc.2、0.1.7-rc.1 |
手机长连接自愈:代理代答 DSH 的 WebSocket 心跳(实测把「停回心跳 6 秒被切断」变成「20 秒以上存活」);页面补丁加入断线看门狗与移动端兼容垫片;「连接与证书」新增连接体检与代理侧计数 |
0.3.6 |
0.1.7-rc.2、0.1.7-rc.1 |
远程也能添加工作区:浏览器侧遮蔽官方目录流程(本机仍走系统对话框,远程改用页面内目录浏览器,可在弹层内解锁);修掉代理双向丢弃插件管理 cookie 导致 password_unlock 对远程形同虚设;补上一直缺失的「远程设备管理权限」控件;「已保存」改为右上角提示;状态表面配色按实测重做;弹层两行被压扁重叠 |
0.3.5 |
0.1.7-rc.2、0.1.7-rc.1 |
连接层可见性:http:// 访问 TLS 端口由「空白页 + 无日志」改为 301 跳 https 并记日志;删除设备记录后旧 cookie 不再把浏览器锁死(吊销/拉黑仍拒绝);移除页文案同步修正 |
0.3.4 |
0.1.7-rc.2、0.1.7-rc.1 |
局域网设备可用官方设置页:新增默认开启的 settingsUnlock 开关(index 注入宿主界面标记,刷新页面生效;界面解锁而非新增权限) |
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。未列入的 DSH 版本属未验证,请自行验证后再使用。上界在0.4.3从<0.2.0放宽:profile 加载会拒绝范围不含运行版本的 bundle,而裸的<0.2.0恰好排除0.2.0正式版。 - 本插件用到的宿主/客户端接口:
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 起):窄屏下官方按钮的显示条件是「不在尾部」,而它判断的是官方认定的滚动层,与实际滚动层不一致,于是永远不出现;网关侧因此自带一个等价的浮动按钮(不在尾部时显示、点击平滑到底、官方按钮出现即让位、位置按输入框动态计算)。
自检:在页面地址后加 ?lgdiag=1,顶部会出现一屏诊断(narrow / mobile / innerHeight / visualViewport / pageScrolls / clippingLayers / 是否已修)。这一屏就是本次定位所用的数据,以后复现同类问题不必连 Mac 调试。
安全边界
- 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 | 电脑上就能跑的窄屏几何回归:两个入口、四个量化判据、免 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.3.1
git push origin v0.3.1
发版工作流会校验 tag 与 package.json 版本一致、要求发布说明含中英双锚点,然后运行 pnpm run verify、打包插件、通过 npm trusted publishing(OIDC)发布,并创建附带 tarball 的 GitHub Release。
许可
评论
评论存放在 GitHub Discussions。用 GitHub 账号登录后可发表评论或点表情。