dsh-lanmode
已验证@goodandready/dsh-lanmode · v0.8.40 · MIT · Web 界面
LAN and reverse-proxy access for the DeepSeek Harness Web UI: returns the settings service on pages that are not localhost, fills in the Web APIs the browser withholds on plain HTTP, and can open a listener of its own so nothing else is needed. Requires D
安装
dsh plugin add @goodandready/dsh-lanmode 用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。
源码
标签
说明文档
📦 @goodandready/dsh-lanmode
浏览器插件的异步初始化会保留其 await 生命周期。
⚡ 为什么 DSH 在局域网上会失败
现代浏览器和 DeepSeek Harness 前端默认限制从非本机地址(例如 192.168.x.x 或 10.x.x.x)通过明文 HTTP 打开页面:
- 🔒 设置与模型页被锁住:Web UI 用
isLoopbackHostname判断主机名。从局域网打开时,设置服务退回内存模式:插件配置卡片是空的,分区状态变成"unavailable",修改在发送前被丢弃,模型页显示设置在此浏览器中不可用。 - 💥 UUID 生成直接崩溃:
crypto.randomUUID()只存在于安全上下文(HTTPS 或 localhost)。局域网明文 HTTP 上,上传文件、调用工具和建立会话会立刻失败。 - 📋 剪贴板复制失效:非安全来源上浏览器禁用
navigator.clipboard,代码片段的复制按钮不起作用。 - 🎙️ 麦克风与语音输入被拦截:明文 HTTP 上浏览器拒绝
navigator.mediaDevices.getUserMedia,手机和平板上的dsh-voice语音输入无法使用。 - 🛡️ 核心 API 只接受回环:核心方法(
/api/settings.*、/api/credentials.*、/api/models.*)拒绝不是来自127.0.0.1的请求。
dsh-lanmode 用非侵入的 webServer.tapIndex HTML 垫片、直连桥、mDNS、根证书和设置卡片解决这些限制。
✨ 功能说明
1. 📱 Quick QR 弹出层、/mobileqr 命令与手机配对
- 快捷 Quick QR 入口:侧边栏底部(
sidebar.footer/sidebar.rail)常驻手机图标,点击弹出交互式纯 SVG 二维码,支持 LAN / WAN 模式一键切换和地址复制。 - 终端与 Agent 聊天工具:在 Agent 对话中注册
/mobileqr工具即时生成二维码,且在dsh web启动时于终端 stdout 输出 ASCII 二维码。 - 诊断与健康检查:通过
/dsh-lanmode/qr和/dsh-lanmode/health均可获取矢量二维码。用手机摄像头扫码即可直接访问。
2. 📲 PWA 与移动端独立窗口
- 路由
/dsh-lanmode/manifest.json,以及viewport-fit=cover、apple-mobile-web-app-capable、theme-color元标签。 - 移动端样式带有
data-dsh-plugin="dsh-lanmode",宿主可以把它和其他插件的样式区分开。 - 在 iOS 或 Android 上把 DSH 加到主屏幕后,它以独立应用打开:没有浏览器地址栏,并避开刘海的安全区。
GET /dsh-lanmode/manifest.webmanifest是公开的,主屏幕安装不依赖会话。- 已保存的启动令牌可以重新打开这个书签。清除站点数据后令牌消失。
- 扫描
/dsh-lanmode/pair-accept?token=可以配对手机,且不设置 cookie。空令牌和长于 512 字符的令牌会被拒绝。 - 在手机上,二维码和设置共用页脚一行。长按会话行会打开它的菜单。模型菜单固定在屏幕底部。窄屏会收起宽桌面面板,其他插件仍然可见。
- 路径以 zip、exe、dmg、pkg、msi、7z、rar、gz、bz2、iso、bin 或 apk 结尾的聊天链接和文件打开会下载,而不是在页面里打开。
3. 🌐 自动 mDNS(dsh.local)
- 内置轻量 UDP 5353 应答:在局域网宣布
dsh.local。不必记住会变的 IP。
4. 🔐 长期信任的本地根证书
- 两级证书:
dsh-lanmode Local Root CA(10 年)签发 服务器证书(SAN 包含dsh.local、局域网 IP 和 localhost)。 - 下载
GET /dsh-lanmode/ca.crt:在 iPhone、iPad 或 Android 上安装一次,即可长期信任 HTTPS。dsh-voice语音输入可以正常使用。 - 已保存的证书会在重启后继续使用。新签发的证书还会为每个地址带上 sslip.io 和 nip.io 名称。
- 这些名称只写进证书。桥不会在 sslip.io 或 nip.io 主机名上监听。
tlsSites为指定主机名增加证书和密钥文件。该主机名的请求使用自己的证书。IP、localhost 和未知名称仍用默认证书。空列表不会启用按名称选择。
5. 🔔 后台网页通知(turn/end)
- 接入
turn/end和approval/asked会话事件。 - 标签页或手机不在前台(
document.hidden)时发出系统通知。点通知会把聊天窗口带回前台。
6. 🎨 「设置 → 插件」里的设置卡片
- 符合 DSH 设计的插件卡片:
- 连接状态与当前模式;
- 一键复制局域网地址;
- 卡片内开关二维码;
- 一键开关后台通知;
- 下载根证书(
ca.crt)。
7. 🛡️ 访问控制、局域网 PIN 与安全
unlockPrivileged:是否允许从局域网修改设置和凭据。lanPin/lanPinRef:特权操作的可选 PIN。开启后,局域网访客可以聊天,但修改系统设置、安装插件或改凭据需要验证 PIN。- 暴力尝试限制:同一 IP 连续 5 次 PIN 失败后进入 15 分钟临时锁定,后续尝试返回 HTTP 429。密码认证连续 5 次失败后锁定 30 秒。
- 子网角色分离:
adminAllow与guestAllow是分开的 CIDR。guestAllow中的子网不能修改系统设置、吊销会话或切换 WAN 隧道(403 Forbidden)。 - 管理与诊断端点保护:内部路由(
/dsh-lanmode/devices、/dsh-lanmode/devices/revoke、/dsh-lanmode/devices/kill-all、/dsh-lanmode/tunnel、/dsh-lanmode/tunnel/toggle、/dsh-lanmode/api/interfaces、/dsh-lanmode/api/telemetry、/dsh-lanmode/api/config)默认拒绝。GET/dsh-lanmode/tunnel需要管理员权限(verifyAdminAccess,访客返回 403)。绕过本地桥或从不受信任的网络访问时,需要有效的管理员凭据或受信任的回环来源。 - 登录、设置、设备吊销和隧道开关在请求体超过 64 KiB 时停止读取并返回 413,操作不会生效。
- CSRF:会改变状态的 POST 拒绝跨站请求(
Sec-Fetch-Site: cross-site),并核对 Origin。 - 密码存储与验证:密码支持纯文本格式(配置向后兼容)和强大的 scrypt 摘要(
scrypt$16384$8$1$salt$hash,通过hashAuthPassword()生成)。凭据验证使用恒定时间比较(timingSafeEqual),未知用户名与错误密码耗时一致,防止用户名枚举。修改密码会立即吊销该用户的其他活跃会话。 - 局域网 PIN 保护:局域网 PIN 支持纯文本与 PBKDF2 摘要(
pbkdf2$sha512$100000$salt$hash,通过hashPin()生成)。防爆破保护在连续 5 次失败后将该 IP 临时锁定 15 分钟。 - 会话与设备令牌:活跃会话通过 SHA-256 摘要(
hashToken)校验,确保令牌密文不以明文保存在持久化设备注册表文件(dsh-lanmode-devices.json)中。 - 第一次设置密码、密码引用或
passwordAuth: true只能在本机完成。远程地址得到 403,配置保持不变。 - 密码认证保持开启时,不能把密码和密码引用同时清空。关闭密码认证仍然允许。
POST /dsh-lanmode/bans,正文{ "ip" },用于封禁地址。该地址在登录页之前收到纯文本403 Forbidden。不能封禁 loopback 和管理员自己的地址。名单保存在设备登记旁边。disabledUsers中的用户名会在 5 秒内失去会话。- 只有
trustedProxyCidrs中的对端可以设置X-Forwarded-For和CF-Connecting-IP。直连客户端不能伪造地址。 - 已登录的 API 返回 401 且带
x-dsh-auth-required: 1时,页面会再次询问密码,不离开当前页,草稿保留。 - 登录卡片显示正在登录的主机名。
8. 📱 已连接设备与会话
- 实时记录客户端,并识别系统与浏览器(iOS、Android、Windows、macOS、Linux)。
- 在设置卡片中按设备吊销令牌,并提供「注销其他所有会话」。
- 桥上列出或吊销设备的路由只对管理员开放。访客得到 403。
- 每个设备行可以显示从 User-Agent 取出的短名称。
- 设备列表、隧道状态、更新检查或延迟请求失败时,卡片显示失败原因,而不是空的成功状态。
9. 🌐 多网卡与网格网络
- 自动识别本地局域网、Tailscale(100.x.y.z)、WireGuard 和 VPN 网卡,并用快捷按钮选择。
- 可为 Windows Defender Firewall、Linux UFW 和 firewalld 管理防火墙规则。
10. ⚡ 实时网络遥测与 HTTP/2 ALPN
- 小组件显示 RTT、并发连接数和传输字节。
- 桥同时支持 HTTP/2(ALPN
h2)和 HTTP/1.1,便于多路复用的低延迟流。
11. 🚀 连接池与 SSE 隔离
- 到 DeepSeek Harness 的上游连接分成两个池:
- 普通 HTTP 池:keep-alive,最多 100 个可复用套接字,用于界面资源、脚本和 REST。队列超时默认 15 秒,饱和时返回 HTTP 503,而不是一直卡住。
- 专用流池:长连接单独处理,包括 Server-Sent Events、令牌流(
/api/chat/stream)和实时通知。一百路以上的流不会占满界面和 API 的连接。响应体是管道转发,不会先收成一整块。
- 本地局域网客户端跳过 gzip 和 brotli。
adaptiveCompression开启时(默认开启),远程客户端仍可收到压缩响应。 - 未配置 harness 端口时,桥依次探测
127.0.0.1的 3080、3081、3082。显式端口按原值使用。
12. ☁️ Cloudflare WAN 隧道与隧道 PIN
- 零配置 Quick Tunnel 与持久 Named Tunnel:通过 Cloudflare 实现远程广域网访问,无需端口映射或公网 IP。支持临时 Quick Tunnel(
trycloudflare.com)和通过tunnelToken/tunnelTokenRef配置的命名隧道,具备快速就绪检测机制。 - 隧道 PIN 保护:配置
lanPin(或lanPinRef)时,通过 Cloudflare 隧道进入的入站 HTTP 请求与 WebSocket 连接(#367)均要求验证局域网 PIN(tunnelPin: true,默认开启)。未配置 PIN 时不要求 PIN。
13. 🔄 界面内一键更新
- 更新服务和设置卡片(
/api/dsh-lanmode/update):- 显示当前安装版本,以及 npm 上是否有新版本;
- 安全边界:必须带
x-dsh-plugin-update: 1,并通过同源检查和管理员门禁。开启密码认证时,即使来自 loopback 也需要有效会话;未开启时,仅 loopback 或管理员网段可以更新。访客会被拒绝; - 在 DSH 设置卡片里一键升级
@goodandready/dsh-lanmode,不必使用终端。
📦 快速安装
dsh plugin --profile web add @goodandready/dsh-lanmode
⚙️ 配置参考(profile cordis.patch.yml)
自 v0.8.0(DSH 0.1.7+)起,配置写在 profile 行的 config: 里。
编辑 profile 的 cordis.patch.yml 可以覆盖默认值:
# ~/.dsh/profiles/web/cordis.patch.yml
- id: dsh-lanmode
config:
mode: direct # 'direct', 'proxy', or 'auto'
directHost: 0.0.0.0 # Default: 127.0.0.1 (localhost only)
directPort: 3080
mdns: true # Announce dsh.local in LAN
pwa: true # PWA manifest, splash screen & mobile viewport
mobileEnterSends: false # When false (default), Enter adds newline on mobile touch
tls: self-signed # 'self-signed' (with Root CA), 'files', or 'off'
unlockPrivileged: true # Permit settings & credentials from LAN
lanPinRef: "" # Credential reference name or ENV var for LAN PIN
tunnel: off # Cloudflare WAN tunnel: 'off', 'quick', or 'named'
tunnelTokenRef: "" # Credential reference name or ENV var for tunnel token
tunnelPin: true # Require PIN for requests from WAN
allow: # Default: ['127.0.0.0/8'] (loopback only)
- 192.168.0.0/16
- 10.0.0.0/8
passwordAuth: false
authPasswordRef: ""
publicHost: ""
disabledUsers: []
trustedProxyCidrs: []
tlsSites: []
adaptiveCompression: true
📄 许可证
MIT © GooDAnDReaDY