# 运维手册（部署 / 运行 / 升级）

> **面向：**部署者 / 运维。本文覆盖**把插件跑起来、管好数据、升级回滚与排障**；
> 安全加固与残余风险见 [SECURITY](SECURITY.md)，版本兼容见 [兼容性矩阵](DSH-0.2.0-COMPATIBILITY.md)。

## 1. 部署前检查

| 项目 | 要求 |
|---|---|
| DSH 版本 | `>=0.2.0-rc.2 <0.3.0`（详见[兼容性矩阵](DSH-0.2.0-COMPATIBILITY.md)；0.1.1-rc.2 的 legacy 线自 v0.7.0 起**不再支持**） |
| 宿主能力 | 必须提供 `connection.authorizeIndex` 与 `webServer`；缺失时插件**fail-closed**（面板不可访问并打印明确错误）——这是有意行为，不是故障 |
| Node | 与宿主一致（插件不引入额外运行时要求） |
| 反代（如启用） | 令牌/盐与会话恢复依赖真实客户端信息；开启 `DSH_AUTH_TRUST_PROXY=1` 限流才按真实 IP 计数 |

> ⚠️ **首次启动后请立刻处理**：进程工作目录会生成 `dsh-ui-auth-bootstrap.txt`（含初始管理员口令）。
> 登录并改密后**删除该文件**。

## 2. 环境变量

| 变量 | 默认 | 说明 |
|---|---|---|
| `DSH_AUTH_MAX_FAILS` | `5` | 单来源连续登录失败锁定阈值（正整数；非法值回退默认） |
| `DSH_AUTH_LOCK_MS` | `30000` | 锁定持续时间（毫秒） |
| `DSH_AUTH_TRUST_PROXY` | 关 | 设为 `1`/`true`/`yes` 时信任 `X-Forwarded-For`（**取最右**，即最近一层受信代理追加的地址，客户端无法伪造）与 `X-Forwarded-Proto`（用于 Secure Cookie）。**仅在 HTTPS 反向代理之后开启**；默认不信任，避免未配置代理时伪造请求头绕过或污染限流 |
| `DSH_AUTH_RP_ID` | 自动 | 通行密钥域（WebAuthn RP ID）。默认按浏览器实际访问的主机名推导；只有在反向代理改写了 Host、或需要在子域间共享通行密钥时才需要显式设置（必须是访问域名的后缀） |
| `DSH_AUTH_ORIGIN` | 自动 | 通行密钥校验的期望来源（如 `https://panel.example.com`）。反代终止 TLS 且插件看到的 Host/协议不是浏览器所见时设置 |
| `DSH_AUTH_RP_NAME` | `DeepSeek Harness` | 通行密钥在系统弹窗中显示的账号名 |

面板进程启动时读取，修改后需重启面板。示例：

```powershell
$env:DSH_AUTH_MAX_FAILS = '10'; $env:DSH_AUTH_LOCK_MS = '60000'; $env:DSH_AUTH_TRUST_PROXY = '1'
```

```bash
export DSH_AUTH_MAX_FAILS=10 DSH_AUTH_LOCK_MS=60000 DSH_AUTH_TRUST_PROXY=1
```

## 3. 数据与持久化

| 内容 | 位置 | 说明 |
|---|---|---|
| 用户、角色、资料、密码哈希、TOTP 密钥、通行密钥（公钥）、邀请码 | DSH 凭据库 `~/.dsh/.credentials.yaml` 中的 `dsh-auth/*` 记录 | 重启后保留；**不保存明文密码，也不保存通行密钥私钥**（私钥只存在于用户设备） |
| 登录会话 | 面板进程工作目录下的 `dsh-ui-auth-sessions.json` | 只存 Token 的 SHA-256 哈希；未过期会话重启后免登录恢复 |
| 审计日志 | 面板进程工作目录下的 `dsh-ui-auth-audit.jsonl` | JSONL，每行含时间、操作者、动作、目标等字段 |
| 首次启动引导文件 | 面板进程工作目录下的 `dsh-ui-auth-bootstrap.txt` | 任一用户改密成功后自动删除 |

**备份**：备份 `~/.dsh/.credentials.yaml` 即可保留全部账号数据。

**彻底清空**：删除 `~/.dsh/.credentials.yaml` 中 `dsh-auth:` 下的记录，并删除上述工作目录里的
`dsh-ui-auth-sessions.json`、`dsh-ui-auth-audit.jsonl` 与 `dsh-ui-auth-bootstrap.txt`，然后重启面板
（用户表为空时会重新生成新的管理员与随机密码）。

### 落盘位置一览（便于备份与审计）

| 内容 | 位置 | 敏感度 |
|---|---|---|
| 用户、会话、邀请码、审计记录 | 宿主 `DSH_HOME` 的**凭据文档**（`<scope>/<id>` 键） | 高（密文与掩码，无明文口令） |
| 私有模型配置（密文 + 掩码） | 同上，键 `dsh-auth/profile-private-<uid>` 等 | 高（**无口令无法解密**） |
| 分享配置（服务端主密钥托管） | 同上，键 `dsh-auth/profile-shared-<uid>` | 高 |
| 初始管理员口令 | 进程工作目录 `dsh-ui-auth-bootstrap.txt` | **极高，用后即删** |
| 会话恢复 | 会话文件（登出即失效） | 高 |

## 4. 备份与恢复

1. **备份**：停止面板（或至少停止写入）后，复制 `DSH_HOME` 与插件工作目录下的状态文件。
   私有配置的**密文**可安全备份——离开用户口令无法解密，但这意味着**口令丢失 = 私有 Key 丢失**。
2. **恢复**：把备份放回原位后重启；会话若已过期需要重新登录。
3. **迁移机器**：整目录搬迁即可（Cookie 名按 `DSH_HOME` 派生，换路径等于换实例，用户需重新登录）。
4. **改密后的影响**：改密会**重包裹**私有配置的 DEK（不需要重新录入 Key）。

## 5. 升级与回滚

1. 阅读 [CHANGELOG](../CHANGELOG.md) 中目标版本的**破坏性变更**；
2. 备份（见 §4）；
3. 升级插件并重启面板；
4. 回滚：装回旧版本并重启——**注意**跨大版本的存储布局可能不同，回滚前务必先备份。

**0.6.x → 0.7.0 的特别注意**：v0.7.0 移除了 legacy 传输线，且模型/密钥改为**按用户隔离**；
若你在 0.6.x 用过部署级模型配置，升级后普通用户**不会**继承它（这是设计，见 [SECURITY](SECURITY.md) §6）。

## 6. 排障

| 现象 | 可能原因 | 处理 |
|---|---|---|
| 面板打不开、日志出现 **fail-closed** | 宿主未提供 `connection.authorizeIndex`（版本不匹配） | 升级 DSH 到 0.2.0 线后重启 |
| 设置页一直「重新连接中」 | 直播通道被中间层拦截（升级请求需可达宿主） | 检查反代是否透传 WebSocket；确认未被额外网关改写 |
| 普通用户看不到「模型」页 | 版本不匹配（该页为 v0.7.0 能力） | 确认插件与 DSH 版本 |
| 查余额显示「需先解锁」 | 私有 Key 用口令派生密钥加密，未解锁时服务端无法解密 | **预期行为**：先在【模型】页解锁 |
| 普通用户被拒（403）但界面正常 | deny-by-default | 见 [SECURITY](SECURITY.md) §6 权限矩阵 |
| 重启后仍保持登录 | 会话未过期（设计如此） | 需要强制下线时删除会话记录或改密 |

## 7. 日志与审计

- 插件日志前缀统一为 `[dsh-ui-auth]`；**fail-closed**、路由注册、R1-ii 可用性等关键状态都会明确打印；
- 审计记录写在宿主凭据文档中（谁在何时做了什么）——运维可直接查阅，**不包含任何明文 Key**；
- 排障时优先抓：启动阶段日志（是否 fail-closed）、首次登录是否生成引导口令文件、浏览器控制台是否有连接类报错。

## 8. 上线前加固清单

- [ ] 删除 `dsh-ui-auth-bootstrap.txt`
- [ ] 管理员口令改为强口令；**开启 TOTP 或绑定通行密钥**（两者都可开）
- [ ] 若面板暴露在公网：启用 HTTPS + 反代，并设置 `DSH_AUTH_TRUST_PROXY=1`
- [ ] 检查限流/锁定参数符合你的风险偏好
- [ ] 普通用户**只**通过邀请码创建；不再需要注册时关闭入口
- [ ] 备份 `DSH_HOME`，并确认恢复流程演练过
- [ ] 若为**互不信任的多用户**部署：确认 `DSH_AUTH_MUX_PROXY` 未被设为 `0`，并跑一次
      `PROBE_URL=http://127.0.0.1:<port> npm run test:mux:leak` 看是否为 **NO-LEAK**（普通用户可见工作区数必须为 0）；
      背景见 [SECURITY](SECURITY.md) §6.2（mux 逐帧归属过滤，Issue #6）
