# TE 文档检索插件 v0.6.14「下载中断」异常全量审计报告

> 审计日期：2026-10-15  
> 审计范围：v0.6.14 生产包（`deploy-site/download/history/TE产品文档检索-V0.6.14.zip`）与当前源码 v0.6.18 对比  
> 审计文件：`popup.js`、`background.js`、`zipcore.js`、`manifest.json`

---

## 一、异常现象

在另一台电脑运行 v0.6.14 时，用户于单编号 `1-1563845-1` 的卡片中观察到：

- **产品图纸** `ENG_CD_1823044_C14.pdf` → 右侧状态显示 **「下载中断」**
- **产品规格** `ENG_SS_114-94423_A.pdf` → 右侧状态显示 **「下载中断」**
- **CAD 文件** `ENG_CVM_CVM_1-1563845-1_B1.3d_stp.zip` → 状态为「下载」按钮，未触发中断

该状态由 `popup.js` 中 `chrome.downloads.onChanged` 监听器在 `delta.state.current === "interrupted"` 时写入。

---

## 二、v0.6.14 下载链路技术现状

### 2.1 批量下载（下载全部文档 / 下载全部产品图）

v0.6.11 已把批量编排迁入 `background.js` 的 `service worker`，v0.6.14 生产包中该引擎完整可用：

- `dlall-start` 一次性接收任务清单
- `dlRunJob` 在 SW 内串行/并行下载
- 进度写 `chrome.storage.session`（`te_dl_progress`）
- `dlKeepAliveStart()` 20s 心跳保活
- **v0.6.12 已补 `dlReconcileOnBoot`**：SW 被浏览器回收后重启，若发现残留 `running:true` 帧，补写终态，防止 UI 永久卡死

因此，**v0.6.14 的批量下载已经修复了「弹窗收起 → 下载中断」的致命 bug**。

### 2.2 单个文档下载（卡片右侧「下载」按钮）

这是截图中「下载中断」的来源路径。v0.6.14 与 v0.6.18 源码几乎一致：

```js
// popup.js:2444 (v0.6.14) / popup.js:2267 (v0.6.18)
function startDownload(item, base, folder, stateEl) {
  return new Promise(async (resolve) => {
    const fname = await resolveFilename(item, base, folder);
    chrome.runtime.sendMessage({ type: "queue-filename", url: item.doc_url, filename: fname }, () => {
      void chrome.runtime.lastError;
      chrome.downloads.download(
        { url: item.doc_url, filename: fname, conflictAction: "uniquify" },
        (downloadId) => {
          if (chrome.runtime.lastError || downloadId === undefined) {
            if (stateEl) { stateEl.textContent = "下载失败"; stateEl.style.color = "var(--err)"; }
            resolve(false);
            return;
          }
          if (stateEl) stateEl.textContent = "已开始下载";
          chrome.downloads.onChanged.addListener(function watcher(delta) {
            if (delta.id !== downloadId) return;
            if (delta.state) {
              if (delta.state.current === "complete") {
                if (stateEl) stateEl.textContent = "已下载";
                chrome.downloads.onChanged.removeListener(watcher);
              } else if (delta.state.current === "interrupted") {
                if (stateEl) { stateEl.textContent = "下载中断"; stateEl.style.color = "var(--err)"; }
                chrome.downloads.onChanged.removeListener(watcher);
              }
            }
          });
          resolve(true);
        }
      );
    });
  });
}
```

**关键缺陷：**

1. **监听器挂在弹窗上下文**：`chrome.downloads.onChanged` 的 `watcher` 在 popup 页面内注册。若用户点击下载后关闭弹窗，监听器随页面销毁而丢失；下载后续若失败/中断，状态无法回显。
2. **无重试**：`interrupted` 直接显示「下载中断」并移除监听器，不再尝试。
3. **无原因诊断**：未使用 `chrome.downloads.search({id})` 读取 `error`、`danger` 等字段，用户只看到「中断」但不知道是网络、用户取消、磁盘、第三方扩展冲突还是浏览器拦截。
4. **依赖原始 URL 的登录态**：TE 资源需凭据。若目标机器上 TE 会话状态异常（Cookie 过期、隐私模式、企业策略隔离），下载可能在开始后被服务器断开。
5. **onDeterminingFilename 竞争仍可能存在**：虽然本扩展注册了认领通道，但 Marvis 等第三方下载扩展若先注册，仍可能在某些 Edge/Chrome 版本上参与 filename 决议并导致行为异常（注释 v0.3.8 已说明这是 Chromium #579563 的已知缺陷）。

---

## 三、根因分析

### 3.1 为什么不是「弹窗收起导致批量中断」？

v0.6.14 的批量下载已经后台化，且包含 B3 修复。截图中同时出现两个「下载中断」是在**同一编号的两个单文档条目上**，而非主按钮状态，说明触发路径是**单个文档下载**，不是批量下载引擎。

### 3.2 最可能的根因组合

| 优先级 | 原因 | 证据 / 解释 |
|---|---|---|
| **高** | 用户在该电脑上手动取消/删除了这两个 PDF 下载 | Chrome 把用户取消标记为 `interrupted`；CAD 未点击或未取消 |
| **高** | 该电脑的 Edge/Chrome 下载安全策略对这两个 PDF 链接做了拦截/断开 | 不同机器策略/扩展不同 |
| **中** | 第三方下载扩展（如 Marvis、IDM 等）在该电脑上抢占了 `onDeterminingFilename`，导致子目录决议失败或被浏览器判定为冲突而中断 | 注释 v0.3.8 已记录该 Chromium 缺陷 |
| **中** | 该电脑上 TE 登录态异常，PDF URL 开始下载后被服务器断开 | 需要凭据的资源常见表现 |
| **低** | 磁盘/权限问题 | 通常会导致所有下载中断，而图中 CAD 未中断 |

### 3.3 为什么 CAD 没中断？

可能原因：
- CAD 文件未被点击下载，状态仍是初始「下载」按钮；
- 或 CAD 点击后走了 `dl-one` 后台解压路径，即使弹窗关闭也不依赖 popup 的 `onChanged` 监听器；
- ZIP 文件类型在某些下载策略中被视为安全，PDF 反而被企业策略扫描/拦截的概率更高。

---

## 四、当前 v0.6.18 是否仍有复现可能

**结论：v0.6.18 的单个文档下载路径仍然存在同样缺陷，复现可能性未消除。**

对比 v0.6.18 当前源码 `popup.js:2267` 的 `startDownload`：

- 与 v0.6.14 逻辑基本一致；
- 仍然把 `onChanged` 监听器注册在 popup 页面内；
- 仍然无重试、无错误诊断；
- 仍然使用原始 URL `item.doc_url` 直接下载。

v0.6.18 相较 v0.6.14 的主要变化集中在：
- CAD 多格式支持（STP/IGS/DXF，v0.6.17）；
- 历史面板的引入与移除（v0.6.16 / v0.6.18）；
- 规格全丢保护阀（v0.6.17）。

这些改动**没有触及单个文档下载的监听器与重试机制**，因此只要触发条件满足，v0.6.18 仍会复现「下载中断」。

---

## 五、修复方案

### 方案 A：单个文档下载也迁入后台 service worker（推荐）

**做法**
1. 在 `background.js` 新增消息类型 `dl-single`。
2. `popup.js` 点击卡片「下载」时，把 `item/base/folder` 发给后台；后台使用现有 `bgEnqueueDownload` 或 `bgUnzipAndSave` 执行。
3. 后台把每个单文件下载状态也写入 `te_dl_progress`（或新增 `te_single_dl` 存储键），popup 通过 `storage.onChanged` 订阅并更新对应行的状态文本。
4. 状态按 `downloadId` 或按 `pn+doc_url` 映射到 DOM 元素；若弹窗关闭重开，可从 storage 恢复状态。

**优点**
- 与批量下载架构统一，彻底消除 popup 生命周期依赖；
- 弹窗关闭后仍可记录状态，重开后可恢复；
- 可复用后台的 `bytesToDataUrl`、30s 超时、解压回退等能力；
- 便于统一增加重试与诊断。

**缺点**
- 改动面较大：需新增消息协议、状态存储键、DOM 状态映射；
- 单文件状态在 storage 中需要清理机制，避免长期堆积；
- 需要更多测试覆盖弹窗重开、并发单文件下载等场景。

### 方案 B：popup 内增加重试 + 详细错误诊断

**做法**
1. `startDownload` 中监听到 `interrupted` 后，不直接移除监听器，而是先 `chrome.downloads.search({id})` 读取 `error` 字段。
2. 根据 error 类型分类显示：
   - `NETWORK_FAILED` / `SERVER_BAD_CONTENT` → 「网络错误，自动重试」
   - `USER_CANCELED` → 「已取消」
   - `FILE_NO_SPACE` → 「磁盘空间不足」
   - `FILE_ACCESS_DENIED` → 「无法写入下载目录」
   - `FILE_TOO_LARGE` / `CRASH` 等 → 对应中文提示
3. 对可恢复的网络类错误，在监听器内自动重试 1 次；重试仍失败再显示「下载中断」。
4. 给监听器增加 120s 超时清理，避免监听器泄漏（v0.6.11 审计 S 级问题）。

**优点**
- 改动集中在 `popup.js`，风险可控；
- 用户能立即知道中断原因；
- 网络类问题可自愈，减少用户操作。

**缺点**
- 仍未解决 popup 关闭后状态丢失问题；
- 重试时若弹窗已关闭，重试逻辑也随页面销毁而丢失；
- 无法复用后台的 30s 超时等保护。

### 方案 C：混合方案——单文件轻量后台化 + popup 仅做 UI

**做法**
1. 后台为每个单文件下载分配一个轻量任务，写入 `chrome.storage.session`：`te_single_dl[{id, pn, url, state, error}]`。
2. popup 启动/重开时读取该数组，按 `pn`+`url` 匹配到对应行渲染状态。
3. 后台负责实际 `download()`、`onChanged` 监听、重试、错误分类；popup 只负责展示。
4. 下载完成/中断后，后台在 storage 中保留最近 N 条（如 50 条），旧的自动清理。

**优点**
- 兼顾架构统一与实现轻量；
- 弹窗生命周期完全不影响下载与状态；
- 状态可恢复，体验一致。

**缺点**
- 需要新增存储键和后台任务管理；
- 实现复杂度介于 A 与 B 之间。

### 方案 D：最小改动——仅优化文案与加兜底提示

**做法**
1. 保持现有 `startDownload` 不变。
2. 「下载中断」改为「下载中断，可点击重试」；按钮保持可点。
3. 在状态栏或 tooltip 中提示常见原因（网络/用户取消/第三方扩展）。

**优点**
- 改动最小，风险最低。

**缺点**
- 不解决根因，仍可能反复出现；
- 无法给出具体原因，用户排障困难。

---

## 六、利弊对比与最优推荐

| 维度 | 方案 A（单文件后台化） | 方案 B（重试+诊断） | 方案 C（混合） | 方案 D（最小改动） |
|---|---|---|---|---|
| 根除 popup 生命周期问题 | ✅ 是 | ❌ 否 | ✅ 是 | ❌ 否 |
| 支持弹窗重开恢复状态 | ✅ | ❌ | ✅ | ❌ |
| 错误原因可诊断 | 可统一做 | ✅ | ✅ | 仅文案 |
| 网络错误可自动重试 | 可在后台统一做 | ✅ | 可在后台统一做 | ❌ |
| 改动面 | 大 | 小 | 中 | 最小 |
| 风险 | 中 | 低 | 中 | 最低 |
| 与批量下载架构一致性 | ✅ 高 | ❌ 低 | ✅ 高 | ❌ 低 |
| 长期可维护性 | ✅ 好 | ⚠️ 一般 | ✅ 好 | ⚠️ 差 |

**最优推荐：方案 C（混合方案——单文件轻量后台化 + popup 仅做 UI）**

理由：
1. **根治 v0.6.14 暴露的问题**：把实际下载与状态监听从 popup 移到后台，弹窗关闭不再丢失状态与重试能力。
2. **比方案 A 更轻量**：不需要为单文件单独实现一套完整引擎，复用后台 `download()`、`onChanged` 与 storage 写入能力即可。
3. **可扩展性**：后续所有下载类功能（批量、单文件、灯箱单图、灯箱 ZIP）都可以统一走后台通道，避免前后台规则漂移。
4. **风险可控**：新增一个 `te_single_dl` session 键，不影响现有 `te_dl_progress` 批量逻辑；失败不会阻塞批量任务。

---

## 七、推荐实施计划（v0.6.19）

### 7.1 background.js 改造

1. 新增 `SINGLE_DL_KEY = "te_single_dl"`。
2. 新增消息处理 `dl-single`：
   - 接收 `{item, base, folder}`。
   - 调用 `bgEnqueueDownload` 或 `bgUnzipAndSave`。
   - 用 `downloadId` 注册 `onChanged` 监听。
   - 状态变化时更新 session 中的 `te_single_dl` 数组。
3. 新增 `singleDlRecord(state)` 辅助函数：写 `{id, pn, url, name, state, error, t}`。
4. 新增清理逻辑：启动/完成任务时只保留最近 50 条。

### 7.2 popup.js 改造

1. 卡片「下载」按钮点击后改为发送 `dl-single`，不再直接 `startDownload`。
2. 启动/重开时读取 `te_single_dl`，按 `pn + url` 匹配到行，渲染状态文本。
3. 订阅 `storage.onChanged` 的 `te_single_dl` 变化，实时更新对应行。
4. 保留 `startDownload` 作为降级路径（后台不可用时），或删除并统一走后装。

### 7.3 错误诊断增强

1. 后台通过 `chrome.downloads.search({id})` 读取 `error`。
2. 映射表：
   - `NETWORK_FAILED` → `网络错误`
   - `SERVER_BAD_CONTENT` / `SERVER_UNREACHABLE` → `服务器异常`
   - `USER_CANCELED` → `已取消`
   - `FILE_NO_SPACE` → `磁盘空间不足`
   - `FILE_ACCESS_DENIED` → `无法写入下载目录`
   - `FILE_TOO_LARGE` → `文件过大`
   - 其他 / undefined → `下载中断`
3. 对 `NETWORK_FAILED`、`SERVER_UNREACHABLE`、`SERVER_BAD_CONTENT` 自动重试 1 次，重试前 `cancel` 旧任务。

### 7.4 测试重点

1. 单文件 PDF / CAD / 规格各下载一次，验证状态正确。
2. 点击下载后快速关闭弹窗，稍后重开，验证状态仍正确更新。
3. 断网触发 `NETWORK_FAILED`，验证自动重试与状态回显。
4. 用户手动取消下载，验证显示「已取消」。
5. 第三方下载扩展共存环境，验证子目录与状态。
6. 批量下载与单文件下载并发，验证互不影响。

---

## 八、结论

v0.6.14 的「下载中断」异常，**不是 v0.6.11 已修复的「弹窗收起 → 批量下载中断」旧 bug 复发**，而是**单个文档下载路径仍依赖 popup 生命周期、缺乏重试与错误诊断**所导致的用户可见问题。当前 v0.6.18 源码中，该路径的逻辑与 v0.6.14 基本一致，因此**最新版本仍有复现可能**。

推荐在下一版本（v0.6.19）采用**方案 C：单文件轻量后台化 + popup 仅做 UI**，把单个文档下载的实际执行与状态监听迁入 service worker，同时补充错误分类、自动重试与弹窗重开状态恢复，从根本上消除该异常。
