# 慢速链路与反复重连

[English](SLOW_CONNECTIONS.en.md)

远程页面能打开、`/mobile-access/health` 返回正常，仍可能出现“重连中”、模型或工作区迟迟不显示。HTTP 可达只说明请求能通过；DSH 的实时数据还需要 WebSocket 连接完成初始化并保持在线。

## 先判断重连发生在哪一步

先记录 DSH、DSH Mobile 插件和 Android App 的版本，再在电脑端 **移动访问 → 连接诊断** 检查当前通道；查看 DSH 启动日志和浏览器开发者工具的 Network → WS，按问题涉及的组件更新。插件与 App 独立发版，版本号不必相同。分享日志时去掉 Token、Cookie 和完整配对地址。

- **WebSocket 没有连接成功**：检查请求是否获得 `101 Switching Protocols`，并留意 `401`、`403`、`404`、`502` 等错误。认证、Host/Origin、反向代理升级支持、连接路径或版本差异都可能造成这种情况。调整心跳不能解决这些问题；第三方插件被拦截的路径可在连接诊断中按需允许，不必把 DSH 内置路径全部重新添加。
- **已获得 101，但连接反复关闭**：记录关闭间隔，并对比同一页面、同一会话的局域网与远程访问。如果只在远程打开长会话时频繁断开，同时下载长时间占满链路，才进一步检查下面的心跳和初始化等待时间。浏览器显示 `1006` 表示连接异常结束，本身不能确定是哪一层断开。
- **初始化等待超时**：若控制台出现 `connection generation was not ready within 15000ms`，说明该次实时连接没有在等待期限内完成初始化。除了慢速传输，还要排查服务端错误、代理阻断及未完成的数据订阅。

[#126](https://github.com/saya-ch/dsh-mobile/issues/126) 和 [#146](https://github.com/saya-ch/dsh-mobile/issues/146) 都报告过“HTTP 正常但实时连接重连”，它们不能仅凭界面文案判定为同一个原因。

## 慢速传输可能超过默认等待时间

DSH 0.2.0-rc.2 的 API 网关默认每 **2 秒**发送一次 WebSocket Ping，连续两次未收到 Pong 后会结束连接，实际容忍时间约为 **4–6 秒**。客户端另有 **15 秒**的实时连接初始化期限。较大的会话数据、低速上行或拥塞可能延迟 Pong 或首次数据同步，触发断开和重新同步。

在 #146 的部署中，反馈者测量到约 2.18 MiB 的会话窗口与 2.68 Mbps 上行，并报告增大这两个时间后重连减少。这是该部署的观测结果，传输大小和改善程度取决于会话、插件及网络，不能作为通用性能保证。

## 按需增加两个 DSH 参数

确认是上述慢速链路后，可先试 **30 秒心跳间隔**和 **120 秒初始化等待**。这两个参数属于 DSH 已有插件配置，不是 DSH Mobile 的 `upstreamTimeoutMs`；后者不能改变 DSH 的心跳或客户端初始化期限。

在电脑端找到当前 profile 中已启用的 `@deepseek-ai/dsh-api-gateway` 与 `@deepseek-ai/dsh-client-connection` 条目。如果当前 DSH 提供相应配置编辑入口，修改其已有配置；没有入口时，先备份当前 profile 的 `cordis.patch.yml`，再编辑这个文件。命令行 Web 通常使用 `$DSH_HOME/profiles/web/cordis.patch.yml`，官方 Desktop 默认使用 `$DSH_HOME/profiles/desktop/cordis.patch.yml`；自定义 profile 以实际运行配置为准。

下例适用于 DSH 0.2.0-rc.2 的标准 Web/官方 Desktop 组装，现有条目 ID 分别为 `typert-gateway` 和 `connection`。先确认自己的条目 ID；同一个 ID 已有覆盖项时，修改它的 `config`，不要重复插入插件。

```yaml
- id: typert-gateway
  config:
    websocketHeartbeatIntervalMs: 30000

- id: connection
  config:
    trustedHosts: !!js ctx.webRuntime.trustedHosts
    recovery:
      generationReadyTimeoutMs: 120000
```

**Patch 会替换目标条目的整个 `config`。** 以上仅展示标准配置需要保留的可信 Host 表达式与两个调整项；若已有自定义 `trustedHosts`、Cookie 有效期、请求大小、`streamInboxBytes` 或其他 `recovery` 参数，须一并保留。把修改合入原文件的顶层 YAML 列表，保留其他条目；不要用示例覆盖整个文件或修改 `node_modules`。

保存后重启当前 DSH 实例，并重新打开手机页面，让页面取得新的客户端恢复参数。若启动提示条目不存在或字段不支持，恢复备份，核对当前 DSH 版本和配置；不同版本或自定义组装不要直接套用本例。

## 调整后检查效果

重新打开原先会反复重连的会话，比较连接持续时间、重连次数和实际传输量。30 秒心跳会让真正失联的发现变慢，120 秒初始化期限也会让失败连接等待更久；如果没有改善，恢复原值并继续定位，不要无限放大等待时间。通道带宽不会因延长时间而增加，也不能修复认证、代理或服务端故障。

参数与默认值已按 DSH 0.2.0-rc.2 核对：[API 网关配置与心跳实现](https://github.com/deepseek-ai/deepseek-harness/tree/dsh-v0.2.0-rc.2/packages/api/gateway/src)、[客户端恢复配置](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/client/connection/src/recovery-config.ts)、[标准 Web 条目](https://github.com/deepseek-ai/deepseek-harness/blob/dsh-v0.2.0-rc.2/packages/bundle/web-app/cordis.patch.yml)。

## 可选的 WebSocket 压缩

从 DSH Mobile 0.5.4 起，可以为指定的实时连接路径启用压缩。它适合带宽有限、计量网络或会话数据较大的场景，以额外 CPU 和内存开销换取较少的传输量；默认路径列表为空，仍使用原来的无压缩转发。

在当前 `mobile-access` 条目的已有 `config` 中加入下面这段属性；这是**配置片段，不是完整 profile patch**。保留该条目已有的网卡、证书、配对状态与管理设置，不要用这个片段替换整个 `config`。

```yaml
websocketCompression:
  paths:
    - /api/remote.mux
```

`/api/remote.mux` 是当前 DSH 的主要实时数据路径。这里只选择要压缩的**准确路径**，不会新增访问权限；设备认证、Host/Origin 与 WebSocket 放行规则仍需通过。社区插件的路径若需要压缩，应先确认并通过连接诊断放行，再加入这个列表。压缩只在手机／浏览器与移动网关之间协商，与 DSH 之间仍不压缩；每条消息不复用先前消息的压缩字典。网关转发两端真实的 Ping/Pong，不另设心跳，也不会因此修改上文的 DSH 等待时间。

以下可选字段与 `paths` 同级，通常保持默认即可：

| 字段 | 默认值 | 用途 |
| --- | --- | --- |
| `maxMessageBytes` | `33554432`（32 MiB） | 单条解压后消息的上限；超过时结束该连接 |
| `maxQueuedBytes` | `67108864`（64 MiB） | 每个转发方向的待发送数据与帧头上限；至少覆盖消息上限加 14 字节帧头 |
| `thresholdBytes` | `1024` | 向手机／浏览器发送消息时，小于此大小不尝试压缩 |
| `concurrencyLimit` | `4` | 压缩任务并发上限；`ws` 在当前进程首次建立压缩连接时确定这个共享上限，改动后重启 DSH |
| `level` | `3` | 压缩等级（0–9）；提高可能增加 CPU 开销 |

修改后重启 DSH 并重新打开手机页面。可在开发者工具中查看 WebSocket 握手是否协商了 `permessage-deflate`；客户端没有协商时仍可无压缩连接。对比同一会话的实际通道传输量与 CPU 使用，不能仅以开发者工具显示的解压后消息长度判断节省比例。需要关闭时把 `paths` 改回 `[]`；压缩不能提高服务商限额，也不能替代连接故障排查。

## 静态资源缓存与重复下载

网关自身生成的资源使用带引号的 ETag，并在设备鉴权和资源检查之后处理 GET/HEAD 的条件请求。网关对上游静态资源做流式 gzip 时，可保留合法上游 ETag 的弱校验形式，并维护 `Vary: Accept-Encoding`；不会把原始内容的长度或范围元信息当作压缩内容的元信息，也不会缓存失败响应或为 API 请求新增缓存。

上游未提供校验标识的资源不会因此自动得到一个可靠标识。检查重复下载时，比较同一完整资源 URL 在普通打开、重新加载和 App 冷启动中的 `200`/`304`、缓存命中、`If-None-Match`、编码与实际传输量；不要先清除站点数据再把结果当作正常缓存行为。HTTP 校验语义改善不保证每个 WebView 都会复用缓存，也不能仅凭这些响应头认定 [#107](https://github.com/saya-ch/dsh-mobile/issues/107) 已完全解决。

## 手动压缩会话与长时间 API 请求（0.6.0）

手机发出的 `/compact` 等命令要通过移动网关的 HTTP API；DSH 内部触发的自动压缩不经过这次网关请求。旧版网关对所有上游代理请求都使用默认 30 秒的 `upstreamTimeoutMs`，因此模型仍在生成摘要时，手动压缩可能先收到 `502 upstream_unavailable`。[#171](https://github.com/saya-ch/dsh-mobile/issues/171) 记录了这一情况。

当前网关区分传输等待和已鉴权 API 的响应等待。`upstreamTimeoutMs` 仍默认 30000 毫秒（1000–300000），用于上传、静态资源、启动资源和 WebSocket 握手的等待；请求头另有既有的 10 秒接收期限。只有完成设备鉴权、Host/Origin 与必要 CSRF 检查的 `/api` 和 `/api/…` HTTP 请求，在请求体发往 DSH 后切换到 `upstreamApiTimeoutMs`。新字段默认 `0`，表示不对 API 响应施加空闲超时；需要限制时，可配置为 `1–2147483647` 的整数毫秒，范围上限来自 Node 定时器的表示范围。这不是整个操作的总执行期限：持续到达的响应数据会重置空闲计时。

以下属性放进现有 `mobile-access` 条目的 `config`，并保留其他配置；这是配置片段，不是完整的 profile 补丁。`0` 是默认值，示例为 API 响应设置 10 分钟的空闲等待期限：

```yaml
upstreamApiTimeoutMs: 600000
```

保存后重启 DSH。设备/会话失效、撤销、调用方断开或网关关闭仍会中止代理请求；上传大小、上传等待、连接数、并发请求数和 CSRF 限制不因 `0` 而关闭。隧道、反向代理或模型服务商自己的期限仍可能提前结束请求；该字段也不会改变前文的 WebSocket 心跳或初始化设置。

普通 HTTP 代理请求在尚未发送响应头时，移动网关自己的上游超时返回 `504 {"error":"upstream_timeout"}`；一般连接失败仍为 `502 upstream_unavailable`。若流已经开始，则只能关闭连接，不能再发送第二份 HTTP 状态。网关不会自动重试 API 请求。超时或断开仅表示调用方停止等待，不保证 DSH 或模型任务已经停止，也不会撤销已经发生的副作用；重试操作前先检查实际会话结果。
