Skip to content
dsh.fish
Bundle

dsh-plugin-login-gate

Password-protected HTTPS login gate + LAN cross-device fix for the DeepSeek Harness (dsh) Web UI. 局域网登录闸门插件:为 dsh Web UI 增加密码登录、HTTPS 反向代理,并自动修复跨设备(局域网)访问时的设置页 403 / settings unavailable 问题。

Source
Wayne036
stars
1 stars
License
MIT
Updated
Updated 15 days ago

Readme

# dsh-plugin-login-gate

为 DeepSeek Harness (dsh) Web UI 增加**密码登录闸门 + HTTPS 反向代理**的 Cordis 插件。

> 适用场景:dsh Web UI 默认无登录页(仅回环地址访问)。当你需要把 dsh 暴露给局域网 /
> 内网其他设备时,本插件在 dsh 前置一道登录,未授权设备无法使用 dsh。

## 特性

- **密码登录**:单密码即可(单用户场景),密码文件明文存储、修改即时生效
- **HTTPS 自签**:自签证书加密传输,首次访问点一次「继续前往」即可(与常见 NAS 体验一致)
- **http → https 自动跳转**:直接输 `IP:端口` 也能打开
- **会话安全**:httpOnly Cookie、浏览器会话级(关浏览器即失效)
- **SSE / WebSocket 转发**:dsh 的流式输出与实时能力不受影响
- **同端口双协议**:一个端口同时处理 HTTP 跳转与 HTTPS
- **零 npm 运行时依赖**(仅 peer 依赖 `@deepseek-ai/cordis`)

## 安装

### 1. 获取插件

```sh
# 方式 A:Git 克隆
git clone https://github.com/<你的用户名>/dsh-plugin-login-gate.git

# 方式 B:手动下载本仓库 zip 并解压
```

将插件目录放入 dsh 可访问的位置(如 dsh 安装目录、或任意本地目录)。

### 2. 注册到 profile

编辑 dsh profile 的 `cordis.patch.yml`(例如 `~/.dsh/profiles/web/cordis.patch.yml`),
或全局用户层 `~/.dsh/cordis.patch.yml`,追加:

```yaml
- insert:
    - id: login-gate
      name: 'dsh-plugin-login-gate'
      config:
        port: 3081
        passwordFile: 'C:/dsh/login-gate/password.txt'
        certFile: 'C:/dsh/login-gate/cert.pem'
        keyFile: 'C:/dsh/login-gate/key.pem'
        assetsDir: 'C:/dsh/login-gate/assets'
```

> 若 `dsh-plugin-login-gate` 未安装到 dsh 能解析的 `node_modules`,可将 `name`
> 指向插件目录的绝对路径(如 `name: 'C:/dsh/dsh-plugin-login-gate'`)。

### 3. 生成证书(一次性)

用本机 openssl 生成自签证书(有效期 10 年,把 `<你的局域网IP>` 换成实际值):

```sh
openssl req -x509 -newkey rsa:2048 \
  -keyout key.pem -out cert.pem -days 3650 -nodes \
  -subj "/CN=DeepSeek-Harness-Login" \
  -addext "subjectAltName=IP:127.0.0.1,DNS:localhost,IP:<你的局域网IP>"
```

### 4. 设置密码

创建密码文件,内容为你的登录密码(明文,改完即时生效):

```sh
echo '你的密码' > C:/dsh/login-gate/password.txt
```

### 5. (可选)自定义素材

插件包内已内置默认素材——`assets/logo.svg`(dsh 官方鲸鱼图标)和
`assets/bg.jpg`(晨雾山景背景图),开箱即有完整美观度。

如需替换:在 `cordis.patch.yml` 的插件行 `config` 中设置 `assetsDir`
指向你的自定义目录(直接放 `logo.svg` / `bg.jpg`,**无需 `assets/`
子目录,文件名匹配即可**)。示例:

```yaml
config:
  assetsDir: 'C:/dsh/login-gate-custom'   # 内含 logo.svg、bg.jpg 即可
```

替换优先级:自定义目录 > 包内默认。

### 6. 重启 dsh

重启 dsh Web UI 后,插件随 dsh 启动。浏览器访问:

```
http://<你的局域网IP>:<port>    → 自动跳转 https
https://<你的局域网IP>:<port>   → 登录页
```

首次访问自签证书浏览器会提示「连接不是私密连接」,点「高级 → 继续前往」即可,
之后该地址不再提示(每台设备首次各点一次)。

### 安装常见问题

**`node-pty` 构建脚本被 pnpm 默认拦截(build scripts are blocked)**

安装依赖 `node-pty`(dsh 终端能力)的插件时,pnpm v10+ 默认不执行 build scripts,
安装会报「构建脚本被 pnpm 默认拦截(node-pty)」。在 dsh profile 目录下
(如 `~/.dsh/profiles/web/`)的 `pnpm-workspace.yaml` 里放行即可:

```yaml
allowBuilds:
  node-pty: true
```

之后重新运行安装(或点击插件商店的「Allow build scripts and retry」)即可通过。
其它原生依赖被拦时同理,把包名加进 `allowBuilds`。

## 配置项

| 键 | 必填 | 默认 | 说明 |
|---|---|---|---|
| `host` | 否 | `0.0.0.0` | 监听地址 |
| `port` | 否 | `3081` | 监听端口(登录闸门入口) |
| `targetHost` | 否 | `127.0.0.1` | 转发目标(dsh 所在地址) |
| `targetPort` | 否 | `3080` | 转发目标端口(dsh Web UI 端口) |
| `passwordFile` | **是** | - | 密码文件路径(明文) |
| `certFile` | **是** | - | TLS 证书 PEM 路径 |
| `keyFile` | **是** | - | TLS 私钥 PEM 路径 |
| `assetsDir` | 否 | 包内 `assets/` | 登录页素材目录(bg.jpg / logo.svg) |
| `maxSessions` | 否 | `64` | 最大并发登录会话数(多端场景) |
| `lanPatch` | 否 | `false` | 局域网跨设备访问修复开关(仅浏览器侧)。开启后会在 dsh 的 `dsh-client-connection` 里,把客户端派生的 `connection.isLoopback` 状态对私有网段也判为 true,让设置页在局域网下可用;**只改这一处、不动 `isLoopbackHostname()` 函数**,因此不影响服务端 `/api` 信任围栏。dsh 更新/重装后需重打补丁(见下文)。默认关闭 = 不改动 dsh 宿主文件。 |

配置缺失或文件不存在时,插件仅记录警告并跳过启动,不影响 dsh 本体。

> 本插件与 dsh 自身 `webserver`(默认 `127.0.0.1:3080`)是两回事:插件作为
> 对外入口(如 `3081`),dsh 本体保持仅本机可访问。建议 dsh 的 `webserver.host`
> 保持 `127.0.0.1`,不要暴露到局域网,由本插件统一对外。

## 安全说明

- 密码**明文**存储于你指定的本地文件,请确保该文件仅你可读写(单用户个人工具设计)。
- 使用自签证书时,密码在传输中加密;但自签 CA 不被浏览器信任,请勿将该入口暴露到
  公网。仅限受信任的局域网/内网(VPN)环境使用。
- 会话为内存存储:dsh 重启后所有设备需重新登录。

## 局域网跨设备访问(LAN)修复说明

把 dsh 暴露给局域网 / 内网其他设备时,会出现两类与「跨设备访问」直接相关的 BUG。
本插件对两类都做了处理:服务端在代理层修,浏览器侧在启动时自动打补丁。

### BUG 1:模型配置页报 `HTTP 403`(transport failure for /api/settings.describe)

- **根因**:dsh 对 `settings.describe` 等特权接口按请求头 `Host` / `Origin`
  做信任判定,只接受回环(loopback)或 `--trusted-host` 指定的 authority。
  反向代理把请求转发到 `127.0.0.1:3080` 时,若 `Host` / `Origin` 仍带着局域网
  地址(如 `192.168.x.x:3081`),就会被信任围栏拒绝,返回 403。
  注意 `--trusted-host` 只覆盖「trusted-host 类」接口,无法覆盖「loopback 类」
  接口,所以光加 trusted-host 不够。
- **修复(本插件已在代理层处理)**:见 `lib/index.js` 的 `rewriteLoopbackAuthority()`,
  在把请求(含 WebSocket 升级)转发给 dsh 之前,把 `Host` / `Origin` 重写为目标
  回环地址(`127.0.0.1:3080`)。你无需手动处理。

### BUG 2:设置 / 模型配置页报 `settings are unavailable in this browser`

- **根因**:这是**纯前端**判定,与上面的 403 是两回事。浏览器端
  `dsh-client-connection` 的 `isLoopbackHostname()` 只认 `localhost` / `127.x`;
  `connection.isLoopback` 由页面 `location.hostname` 算出。经局域网 IP 访问时
  `isLoopback=false` → 设置镜像 `persistence="memory"` → 永不发起 `settings.describe`
  → 抛该错。
- **修复(需开启 `lanPatch`,本插件在启动时自动打浏览器侧补丁)**:
  > 该浏览器侧补丁默认**不启用**(`lanPatch: false`)。只有局域网设备需要打开设置页时,
  > 才在插件行 config 设 `lanPatch: true`;未开启时 dsh 内部文件原样不动(更安全)。

  补丁**只改一处**,且刻意不动 `isLoopbackHostname()` 函数本体:

  `dsh-client-connection/lib/client.js` 里派生的 `connection.isLoopback` 状态,
  原本是 `pageLocation === void 0 || isLoopbackHostname(pageLocation.hostname)`,
  补丁在其后追加一段**私有网段**判定(`10/8`、`172.16/12`、`192.168/16`、`169.254/16`):
  局域网 IP 访问时 `isLoopback` 也判为 `true` → 设置镜像改用 `host` 持久化、正常加载。

  > 为什么不动 `isLoopbackHostname()` 函数?该函数同时被**服务端 `/api` 信任围栏**
  >(`api-request-trust.ts`)使用;若在此处放宽,等于把整个局域网当成回环可信,
  > 特权接口在 LAN 下无需 `--trusted-host` 即达,扩大信任边界。只改客户端派生状态,
  > 信任围栏仍只认真正的回环 / trusted-host,既修好设置页又不引入该副作用。
- **补丁是侵入式的,改在 dsh 自家 `node_modules` 内部文件里**:

  > ⚠️ **dsh 一旦更新 / 重装,这些补丁会失效**,设置页会再次报 BUG 2。
  > 因此本插件在 `apply()` 启动时(最佳努力、幂等)自动重打;
  > 若自动探测未命中,或你想手动确认,在 dsh 更新后执行一次即可:

  ```sh
  # 自动探测 dsh 安装目录(从 cwd / 插件位置向上回溯)
  node node_modules/dsh-plugin-login-gate/patch-dsh-lan.js

  # 或显式指定 dsh 根目录(最稳妥)
  node node_modules/dsh-plugin-login-gate/patch-dsh-lan.js C:/AI/deepseek-dsh
  # 等价写法:
  #   DSH_ROOT=C:/AI/deepseek-dsh node node_modules/dsh-plugin-login-gate/patch-dsh-lan.js
  ```

  脚本特性:幂等(已打过则跳过)、不写死任何 IP / 密码、匹配不到模式就不动文件、
  打前自动留 `.lanpatch.bak` 备份。

### BUG 3:跨设备会话列表 / 实时事件流为空(WebSocket 握手失败)

- **根因**:作为反向代理转发 WebSocket 升级时,`Upgrade` / `Connection` 属 hop-by-hop
  头被剥掉且**未补回**,而 RFC 6455 要求 101 响应必须携带这两个头 → 浏览器端握手校验
  失败(控制台报 `WebSocket is closed before the connection is established`),
  dsh 事件流(`/api/events.mux` / `/api/events.host`,承载会话列表等实时数据)跨设备
  无法建立 → 工作区对话任务 / 会话历史只在本机显示。
- **修复**:转发 101 响应前补回 `Upgrade: websocket` 与 `Connection: Upgrade`。
- **关联**:旧版网关给转发请求设了 30s idle 超时,会误杀空闲的长连接
  (如 `/plugins/events` 热重载 SSE,客户端报 `ERR_INCOMPLETE_CHUNKED_ENCODING`),
  现已移除;普通 API 请求由客户端超时兜底。

### 验证(每次更新 dsh 后建议跑一遍)

```sh
# 1) 局域网设备登录后应能打开设置页,不再报 403 / unavailable
# 2) 用 node 直连验证 settings.describe 返回 200:
node -e "const https=require('https');https.get('https://127.0.0.1:3080/api/settings.describe',{rejectUnauthorized:false},r=>{console.log('status',r.statusCode);r.destroy();}).on('error',e=>console.log('err',e.message))"
# 3) 浏览器硬刷新(Ctrl+F5)以加载打了补丁的新 client 模块
```

> 完整排查 / 快速修复手册见仓库同级的 `dsh-局域网跨设备访问修复手册.md`
> (本地留存文档,不含任何本机 IP / 密码)。

## 已知限制

- 单密码、无多用户体系(个人工具定位)。
- 无登录失败锁定(如需防暴力破解,可自行在反向代理前加防火墙/限流)。
- 自签证书 10 年有效期,到期需重新生成并更新配置。

## Changelog

- **v0.2.2** — LAN 浏览器侧补丁收敛为单一精准补丁:仅把客户端 `connection.isLoopback` 状态对私有网段判为 true(让设置页在局域网下持久化可用),**不再修改 `isLoopbackHostname()` 函数**,避免波及服务端 `/api` 信任围栏(此前放宽该函数会把整个局域网当回环可信)。移除了旧版对 `dsh-client-ui-settings` 的冗余补丁(该包新版已重构为纯 UI 壳、目标字符串消失)。旧版函数级放宽残留会自动回滚迁移。
- **v0.2.1** — `lanPatch` 默认改为 `false`(显式开启)。未配置 = 不改动 dsh 宿主文件;开启后才会就地修补 dsh 内部包让设置页在局域网下可用,dsh 更新/重装后需重打补丁(见「局域网跨设备访问修复说明」)。
- **v0.2.0** — 修复跨设备访问的两类问题:WebSocket 101 响应补回 `Upgrade` / `Connection` 头(BUG 3,事件流 / 会话列表跨设备可见);移除转发请求 30s idle 超时(避免误杀 `/plugins/events` 等长连接)。新增 `pnpm allowBuilds` 放行 node-pty 的安装说明。
- **v0.1.0** — 初始版本:密码登录闸门 + HTTPS 自签反向代理、http→https 跳转、SSE / WebSocket 转发、浏览器侧 LAN 补丁(修复设置页 403 / `settings are unavailable`)。

## License

MIT

Install

dsh plugin --profile web add github:Wayne036/dsh-plugin-login-gate

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source