# 部署指南 > 中文文档 | [English](../en/DEPLOYMENT.md) ## dsh 版本兼容性 **本插件支持 `0.1.5-rc.2 <= dsh <= 0.2.1-rc.2`,均已实机验证。** 用 `dsh --version` 查看当前安装版本。 本插件依赖的扩展点在该区间内均未变动,因此整条区间可直接使用: | 依赖的官方扩展点 | 用途 | |---|---| | `webServer.tapIndex` | 注入 `randomUUID` polyfill 与 `basePath` 全局量(仅自包含全局量) | | `dsh.bundle` patch | 把内部 webserver 绑定在 `127.0.0.1:`(安全根基,见 [SECURITY.md](SECURITY.md)) | | `ctx.slots`(`settings.section`) | 客户端「认证设置」面板 | | `credentials` 服务 + `client-connection/browser-session` record | 读取上游 BrowserAuth 密钥,为回环一跳铸造同构 cookie | | BrowserAuth cookie 形状(`dsh-auth-`、`v1..`) | 与 dsh 自身的浏览器会话 cookie(index 路由铸造的 `v1`)同构,上游无法区分 | 版本线行为: - **dsh ≥ 0.1.2**:内部 webserver 启用 BrowserAuth,网关自动铸 cookie 适配(机制见 [SECURITY.md](SECURITY.md)); - **dsh ≥ 0.2.1-alpha.3**:BrowserAuth 增加 `v2` cookie(带 `id` 的 API operator,由新增的 `/connect` 启动令牌交换铸造,供 SDK / 桌面端等 API 客户端使用);**浏览器会话仍是 `v1`**,网关铸造的正是 `v1`,上游照旧接受。同版本起 dsh 新增 `remoteAccess` RBAC,但它只约束**非 owner** principal,而浏览器请求(持有 `v1` cookie)一律以 `owner` 通过,因此网关路径不受影响; - **dsh ≥ 0.2.1-alpha.3(「登录 DeepSeek 账号」)**:账号登录的回调由开放平台以**跨站顶层导航**回跳 `http://localhost:<端口>/oauth/callback`,浏览器不会带上网关会话 Cookie、请求必然匿名到达,因此需要 `publicPaths` 放行(默认值即包含该路径)。若把它设成 `[]`,账号登录会一直停在「等待登录」直到超时,之后再试平台只会返回「授权失败」(`ALREADY_CONSUMED`,因为 `authorize_id` 已在批准时被消费)。**另有一个前置条件**:账号登录还要求**浏览器 origin** 本身是带显式非零端口的回环 http(上游 `loginOrigin` 约束)——域名 / https 反代入口下点「登录 DeepSeek 账号」会立即报「登录失败」(`account: protocol`),与 `publicPaths` 无关、调它也无济于事;判别与处置见 [TROUBLESHOOTING.md 第 10 节](TROUBLESHOOTING.md)。取舍与边界见 [SECURITY.md](SECURITY.md#匿名可达的上游握手路径); - **dsh ≤ 0.1.1**:无 BrowserAuth,record 不存在,网关静默退化为逐字转发,行为与旧版一致。 > **升级 dsh 后请注意**:dsh 的 **WebSocket 端点路径会随版本变化**——旧版是 `/api/events.mux`、`/sidebar/ws/*`,0.1.5 起只有 `/api/remote.mux`。网关按路径透明转发、**不硬编码任何端点名**,所以插件侧无需改动;但**反向代理若为 WebSocket 维护了路径白名单,dsh 一换路径就会静默失效**(症状:HTTP 正常、登录正常、页面提示连接失败)。反代请把 `Upgrade`/`Connection` 转发放在**兜底 location** 上,详见 [NGINX-DEPLOYMENT.md](NGINX-DEPLOYMENT.md) 与 [TROUBLESHOOTING.md](TROUBLESHOOTING.md) 第 7 节。 ## 远程访问下登录 DeepSeek 账号 **根因在 dsh 上游(网关无故障,但也没有服务端绕过手段)**:账号登录发起时,上游要求**浏览器 origin** 是带显式非零端口的回环 http 地址(`http://localhost:<端口>`、`http://127.0.0.1:<端口>` 或 `http://[::1]:<端口>`);域名 / https 反代 / 局域网 IP 等非回环入口在联系开放平台之前就被拒——表现为点左下角「登录 DeepSeek 账号」**立即**报「登录失败 / 操作未完成,请重试。」(`account: protocol`)。回环地址只在**浏览器所在机器**上解析,服务端改写 origin 只会把浏览器指向它自己的回环,所以本插件无法在服务端绕过这一约束;远程访问恰好是本插件提供的形态,因此该限制在当前部署下必然遇到。除这一项外,远程访问下**其余功能全部正常**。判别方法与证据见 [TROUBLESHOOTING.md](TROUBLESHOOTING.md) 第 10 节。 可用的手动解决方式(任选其一): 1. **本机回环访问**——浏览器与 dsh 同机时最省事:直接打开 `http://127.0.0.1:<网关端口>`(如 `dsh web --port 8080` 对应 `http://127.0.0.1:8080`)。协议必须是 `http`(`https://127.0.0.1:...` 同样被拒),端口必须显式写出。 2. **SSH 本地端口转发**——浏览器在另一台机器上时,把远端网关映射到本机回环端口: ```bash # 在浏览器所在的机器上执行: # 冒号前的 8080 = 本机端口,可换成任意空闲端口; # 冒号后的端口 = 网关 listenPort(`dsh web --port N` 的 N,默认 3080) ssh -L 8080:127.0.0.1:<网关 listenPort> <部署机> ``` 然后浏览 `http://127.0.0.1:8080`(用**本机**那个端口),先在网关登录页完成密码(+ OTP)认证。账号登录的跨站回调也会回到浏览器这一侧的 `http://127.0.0.1:8080/oauth/callback`,经同一条隧道抵达网关,再由 `publicPaths`(默认放行)转给上游,因此全流程可通。注意:若部署时把 `cookieSecure` 强制为 `true`(如反代终结 TLS 但未透传协议头,或出于确定性固定该属性),明文 http 回环入口按现有说明无法保存会话 Cookie——需要账号登录时请让该入口使用默认的 `auto`(见「网络与安全建议」)。 3. **其它回环转发方式**——任何能把 `http://127.0.0.1:<显式端口>` 映射到远端网关的隧道 / 端口转发工具都等价可用(VS Code / JetBrains 的端口转发、`kubectl port-forward`、`gost` 等)。唯一约束是浏览器地址栏里的 origin 必须落在 `http://{localhost|127.0.0.1|[::1]}:<非零端口>` 之内。 4. **改用 API Key 通道**——不使用账号登录,在账号弹窗里点「添加 API Key」(或按 dsh 的模型凭据方式配置)。纯域名 / 反代部署下这是既定替代方案,见 [SECURITY.md](SECURITY.md#匿名可达的上游握手路径)。 > 桌面端(`dsh-hub-desktop`)同样受此限制:它取服务地址的 origin,远程域名下与浏览器一样被拒;需要用账号登录时,把它指向回环入口(方式 1–3)。 ## 端口与监听地址 ### 端口:直接用 `dsh web --port ` bundle patch 跟随 `ctx.webStartup`(与 web-app 自身 webserver 行同一机制),端口由命令行 flag 控制: ```bash dsh web --port 8080 --no-open # 对外 URL 8080,内部 webserver 自动挪到 8081 dsh web --no-open # 默认:对外 3080,内部 3081 ``` 推导规则:**对外端口 = `--port`(默认 3080),内部端口 = 对外 + 1**。端口范围 1–65534(内部端口 +1 后仍须合法,否则配置校验在启动时报错)。 > 不支持 `--port 0`(OS 自动分配):网关需要固定的内部端口才能转发。 ### 建议始终加 `--no-open` 网关自己会打印对外 URL,不需要 dsh 再去打开浏览器。不加 `--no-open` 时 dsh 的浏览器 handoff 打开的是**内部** webserver 地址,并把启动令牌拼进 URL(`appRootUrl()` → `authenticatedUrl()` → `openBrowser()`): - 未传 `--public-url` 时打开的是 `http://127.0.0.1:<内部端口>/?token=…`,而 `frontend-static` 的 index 接受该令牌并直接铸出 dsh 会话 cookie——**这个标签页完全不经网关密码**。它只对本机有效(内部端口只监听回环),属于 [SECURITY.md](SECURITY.md) 记录在案的本机可信模型,不是漏洞,但没必要主动制造;同时控制台会多出一行 `dsh web: opening the default browser; pass --no-open to disable`,与网关那行并存。 - 传了 `--public-url` 后打开的是对外 origin,请求先撞上网关认证门,这条本机旁路随之消失。 ### 为什么不能用 `--host 0.0.0.0` `dsh web --host 0.0.0.0` 会被 dsh 直接拒绝(web-app 内置安全限制,`startup.ts` 硬编码)。dsh 0.2.1-alpha.2 起该校验从只拒 `0.0.0.0` 收紧为拒绝一切通配地址(`isWildcardHost`:`0.0.0.0`、`::`、`0` 等),同时允许传入**具体的** IPv4/IPv6 字面量来绑定某个本机接口。但无论哪种,网关都用不上:这是 dsh 代码层限制,配置无法解除,**也不需要解除**: - 内部 webserver 必须只监听 `127.0.0.1`(安全关键:内部端口一旦对外就绕过了认证网关); - 对外监听由插件的 `listenHost` 承担(默认 `0.0.0.0`),不经过 web-startup 的校验。 对外暴露的正确姿势:**不传 `--host`,使用默认**。 ### 只允许本机访问 修改 `dsh-auth-gateway` 行的 `listenHost` 为 `127.0.0.1` 即可(仅本机可访问)。 ### 固定端口(不使用 `--port`) 在 profile 自己的 `cordis.patch.yml` 中覆盖 `webserver` 与 `dsh-auth-gateway` 两行(用户补丁层优先级高于 bundle 层),注意 `webserver.port` 必须等于 `dsh-auth-gateway.config.upstreamPort`。手动覆盖后 `--port` flag 不再生效。 ## 网络与安全建议 - **LAN 部署**:置于可信网络;网关默认监听所有网卡,跨网络暴露前务必配置防火墙; - **HTTPS / Secure Cookie**:配置项 `cookieSecure`(默认 `auto`)控制会话 Cookie 的 `Secure` 属性——连接经 TLS 时自动附加(反向代理需透传 `X-Forwarded-Proto: https` 头),浏览器自此只经加密链路回传 Cookie;`true` 强制附加(反代已终结 TLS 但未透传协议头时使用);`false` 显式关闭。**注意**:Secure 只在 HTTPS 链路生效——纯 HTTP 下强制开启会使浏览器拒绝保存 Cookie、登录立即失败(显式失败而非静默降级)。传输加密仍需自行部署:生产建议前置 TLS 反向代理(nginx/Caddy 等)到网关端口并配置 `trustedHosts`(见下);内网直连、无公网证书时可自建内部 CA(如 mkcert)为网关签发证书并导入终端信任库,即可获得无警告的 HTTPS; - **混合 HTTPS/HTTP 入口(双协议同域)**:会话 Cookie 带 `SameSite=Strict`,且 HTTPS 登录的 Cookie 带 `Secure`——浏览器因此不会在 `http://` 入口携带 HTTPS 入口的会话:`https://dsh.example.com` 登录后访问 `http://dsh.example.com:8080` 会需要重新登录。这是**预期行为**(加密入口的会话不降级到明文入口);若确实需要多入口共享会话,请让所有入口统一走 HTTPS(同一反代上各端口都部署 TLS),而不是放宽 cookie 属性。注意:该场景下在明文入口**重新登录**也会失败(浏览器拒绝用明文来源覆盖 HTTPS 入口的 Secure Cookie)——网关登录页会在此场景给出明确提示(「登录未生效:浏览器未能保存会话 Cookie…」),而不是静默跳回登录页;解除方法是在 HTTPS 入口退出登录(或清除该站点 Cookie)后重试。 - **trustedHosts**:若经反向代理/自定义域名访问,需在 `dsh-client-connection` 行配置 `trustedHosts`(内部 fence 的授权权威;本插件转发已改写 Host/Origin 为回环,正常情况下无需配置,特殊拓扑下按 dsh 文档配置); - **备份**:凭据数据位于 `$DSH_HOME/auth-gateway/`(password.json、otp.json、otp-master.key),备份时注意加密(OTP 密钥已 AES-256-GCM 加密,但主密钥 `otp-master.key` 同样需保护,见 SECURITY.md)。 - **OTP 主密钥管理**:默认自动生成 `auth-gateway/otp-master.key`,密钥与密文同目录(本机可信模型)。要隔离磁盘泄露,部署前设置环境变量 `DSH_AUTH_GATEWAY_MASTER_KEY`(hex 或 base64 编码的 32 字节),并把它放到加密卷或外部密钥管理(KMS / Docker secret / systemd credentials 等);设置了环境变量即不再生成/读取密钥文件。生成示例:`node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`。详见 [SECURITY.md](SECURITY.md) 的「OTP 密钥加密」。 ## 配合 nginx / 反向代理部署 网关可直接对外提供服务,也可以放在 nginx(或其他反向代理)之后。完整拓扑与配置示例(裸金属直连、子域名部署、子路径部署、Docker nginx 容器)见: - [NGINX-DEPLOYMENT.md](NGINX-DEPLOYMENT.md)(中文)| [English](../en/NGINX-DEPLOYMENT.md) 要点速览: - **子域名部署(推荐)**:`dsh.example.com` 根路径部署,nginx 将 443 反代到网关端口,零冲突、零维护; - **子路径部署**:网关配置 `basePath: /dsh`(在部署方 profile patch 中覆盖,注意 `config:` 是整对象替换,须保留 bundle patch 全部字段),nginx 需额外转发 dsh 引用的根路径资源(`/assets/`、`/api/`、`/plugins/` 等); - **反代转发 WebSocket**:`Upgrade` / `Connection` 必须转发,且**要放在兜底 location 上**——用 `map $http_upgrade $connection_upgrade`(放在 `http {}` 作用域)配合 `proxy_set_header`,不要写死 `"upgrade"`、也不要按路径挑白名单。dsh 的 WebSocket 路径随版本变过(旧版 `/api/events.mux`、当前 `/api/remote.mux`),白名单一旦漂移就是「HTTP 正常、登录正常、只有页面提示连接失败」的静默故障;同时调大 `proxy_read_timeout` / `proxy_send_timeout`,否则 SSE 事件流 60s 被掐断。完整配置见 [NGINX-DEPLOYMENT.md](NGINX-DEPLOYMENT.md),判定方法见 [TROUBLESHOOTING.md](TROUBLESHOOTING.md) 第 7 节。 ## 故障排查 | 现象 | 原因与处理 | |---|---| | 页面显示"加载提供方目录失败: crypto.randomUUID is not a function" | 非安全上下文(HTTP + 非 localhost)下浏览器 Web Crypto 限制;插件已注入 polyfill——确认安装的是最新版本且已重启 dsh web | | 登录后大量 `/api/*` 403 | 内部 fence 拒绝外部 Host——确认 bundle patch 生效(webserver 应为 `127.0.0.1:<内部端口>`),网关已改写 Host/Origin | | 无法访问 `http://:` | 检查网关是否监听 `0.0.0.0`(`listenHost` 配置)、防火墙规则 | | 登录被 429 拒绝 | 触发全局速率限制或 OTP 限流——等待窗口重置(1 分钟/锁定 5 分钟) | | 登录成功但页面提示连接失败,console 报 `WebSocket connection to '.../api/remote.mux' failed` | 反代没有把 `Upgrade`/`Connection` 转发给该路径(常见于按路径写 WS 白名单、兜底 location 没带这两个头)——见 [TROUBLESHOOTING.md](TROUBLESHOOTING.md) 第 7 节;网关日志会有一条 `WebSocket 握手缺少 Upgrade 头` 告警 | | `--port 65535` 启动失败 | 内部端口 65536 非法——端口范围 1–65534 | | 重复安装(组合树出现两行 dsh-auth-gateway) | 第二个网关绑定同端口必然 EADDRINUSE 启动失败——删除重复行 | ## 凭据重置与卸载命令 插件包自带两个命令(`package.json` 的 `bin`):`dsh-auth-gateway-reset`(仅删除密码记录)与 `dsh-auth-gateway-uninstall`(删除整个 `$DSH_HOME/auth-gateway/`)。`dsh plugin add` 通过 pnpm 把命令链接到 profile 的 `node_modules/.bin`——**该目录默认不在 PATH 上**,直接敲短名会提示 command not found。两种运行方式: ```bash # 方式一:完整路径(把 profile 名换成你实际使用的) ~/.dsh/profiles/web/node_modules/.bin/dsh-auth-gateway-reset # 方式二:把 profile bin 加入 PATH(写入 ~/.zshrc / ~/.bashrc 后重开终端) export PATH="$HOME/.dsh/profiles/web/node_modules/.bin:$PATH" dsh-auth-gateway-reset ``` > **`$DSH_HOME` 在哪**:凭据数据在 `$DSH_HOME/auth-gateway/`(password.json、otp.json、otp-master.key)。`$DSH_HOME` 默认取 `~/.dsh`(即 `$HOME/.dsh`),可用环境变量覆盖——dsh 与插件读取同一值。 `dsh-auth-gateway-reset` 删除密码记录后,**必须重启 dsh web**:初始密码在启动时生成并打印到控制台(插件没有"设置密码"页面),登录后走引导流程设置个人密码。丢失认证器时还需删除 `$DSH_HOME/auth-gateway/otp.json`(或直接运行 `dsh-auth-gateway-uninstall`)。 > **顺序提醒**:如需同时卸载插件,**先运行上面的凭据命令,再执行 `dsh plugin --profile web remove dsh-auth-gateway`**——remove 会移除 profile 里的插件依赖,两个命令的 bin 链接随之失效(悬空指向已删除的目标)。 ## 升级 `dsh plugin --profile web add file:...` 后需 **remove + add** 刷新 pnpm 快照(`file:` 依赖是安装时快照),再重启 dsh web。插件自带 `dsh-auth-gateway-reset`/`dsh-auth-gateway-uninstall` 命令(升级无需运行)。