# TE 产品文档检索 Agent — v0.4.3 全量审计报告

审计时间：2026-10-12
审计对象：`te-drawing-agent/extension/`（manifest.json / background.js / popup.html / popup.css / popup.js）
审计方式：**静态全量审计 + 真实源码执行装置 + 接管浏览器动态实测（原生 Edge + CDP，下载真实落盘）**
审计结论：**v0.4.3 已可满足需求；本次报障根因已定位并根治；遗留风险已收敛到"可接受且已加保险"**

---

## 〇、您提出的三个问题

> ① 全量审计当前代码是否可以满足我的需求
> ② 分析是否有潜在失效风险
> ③ 分析我发现的 bug 是什么原因

下面按序回答。

---

## 一、需求定义（以您的原话为准）

| 模式 | 您的需求原文 | 判定动作 |
|---|---|---|
| **重名文件夹** | "下载文档前检索默认保存路径是否存在当前产品编号/文档编号相同名称的**文件夹**；如果存在则自动增加 (1)/(2)/(3)…… 后缀" | 探测 `保存路径\编号` 是否存在 → 存在则 `编号 (n)` |
| **重名文档** | "默认检测当前产品编号/文档编号名称的文件夹内是否有相同名称的**文档**；如果有则将该文档增加 (1)/(2)/(3)…… 后缀" | 探测 `保存路径\编号\` 内是否有同名文档 → 有则该**文档名**加 `(n)` |

前提说明（实测确立，直接决定架构）：
- **浏览器的 `conflictAction:"uniquify"` 只作用于文件名，完全不管文件夹名** → 文件夹后缀**必须插件自己算**；
- **`uniquify` 对文件名可靠且永不覆盖** → 文档后缀**可以安全委托给浏览器**。

---

## 二、问题①：全量审计 —— 代码能否满足需求？

### 2.1 重名文件夹：能满足 ✅

实现链：`getFolderName()` → `detectExistingFolders()` → `FOLDER_RESERVED` 预留 → `FOLDER_CACHE` 缓存

| 审计点 | 实现 | 判定 |
|---|---|---|
| 探测保存路径下的同名文件夹 | `detectExistingFolders(base, pn)` 用正则 `[\\/]<base>[\\/](<pn>(?: \(\d+\))?)[\\/]` 匹配绝对路径 | ✅ |
| 存在则加 (1)/(2)/(3) | `while (existing.has(pn (n)) \|\| FOLDER_RESERVED.has(pn (n))) n++` | ✅ |
| 只探测**本编号**，不误伤他编号 | 正则锚定 `base/` 之后紧跟 `<pn>` 段 | ✅ |
| 序号空洞填补（有 (2) 无 (1) → 用 (1)） | 从 n=1 递增，取第一个空闲位 | ✅ |
| 同一批内不撞车 | `FOLDER_RESERVED` 会话级预留表 | ✅ |
| 同批同编号只探测一次 | `FOLDER_CACHE[mode\|pn]` 缓存 | ✅ |

实测验证（真实 profile，6 个真实编号 + 1 个全新编号）：**7/7 决策正确**。

### 2.2 重名文档：能满足 ✅（v0.4.3 改为委托浏览器）

v0.4.3 移除了插件侧的 `resolveDocName` / `detectExistingDocs` / `DOC_RESERVED`，把文档判重交给 `conflictAction:"uniquify"`。

理由（这是 v0.4.3 的关键权衡）：

| 方案 | 浏览器 `uniquify` 实测表现 | 是否采用 |
|---|---|---|
| 委托浏览器 | 三次请求 `PN-0001/same.bin` → `same.bin` / `same (1).bin` / `same (2).bin`，**且原 `same.bin` 未被覆盖** | ✅ 采用 |
| 插件自算后缀 | 需要插件准确知道磁盘实况，而插件唯一的探测手段（`chrome.downloads` 的 `exists`）**被证明不可靠**（见问题三） | ❌ 弃用 |

**结论**：文档判重交给浏览器，既满足"有同名加 (1)/(2)"的需求，又天然不会覆盖。**这是比 v0.4.2 更安全且更正确的选择。**

### 2.3 审计对照表

| # | 需求点 | 代码位置 | 状态 |
|---|---|---|---|
| 1 | 文件夹模式：探测同名文件夹 | `detectExistingFolders` | ✅ |
| 2 | 文件夹模式：加 (1)/(2)/(3) 后缀 | `getFolderName` | ✅ |
| 3 | 文件夹模式：文件名保持原名 | `buildFilename` | ✅ |
| 4 | 文档模式：文件夹名固定为编号 | `getFolderName` 中 `if (mode==="file") return pn;` | ✅ |
| 5 | 文档模式：同名文档加 (1)/(2)/(3) | 委托 `conflictAction:"uniquify"` | ✅ |
| 6 | 模式切换立即生效（无竞态） | `CONFLICT_USER_TOUCHED` 守卫 | ✅ |
| 7 | 切模式后清空缓存 | `saveConflictMode` | ✅ |
| 8 | 非法字符净化 | `sanitizePath` / `buildFilename` | ✅ |
| 9 | 子目录归类不丢（抗第三方扩展抢） | `background.js` 认领 + TTL | ✅ |

**问题①结论：v0.4.3 的代码可以满足您的全部需求。**

---

## 三、问题②：潜在失效风险分析

### 风险清单（按严重度排序）

| 级别 | 风险 | 现状 | 缓解措施 |
|---|---|---|---|
| 🔴 高→已消除 | **下载历史 `exists` 延迟刷新导致文件夹误判** | 已根治（等收敛 + 严采信） | 见问题三 |
| 🔴 高→已消除 | **`conflictAction:"overwrite"` 造成不可逆覆盖** | 已彻底删除 | 全文静态断言"不得出现 overwrite" |
| 🟡 中→有保险 | 第三方下载类扩展（如 Marvis）抢文件名决定权（Chromium #579563） | 无法在代码层强制胜过 | ①自注册 `onDeterminingFilename` 对抗 ②说明书 Q4 给出排查步骤 |
| 🟡 中 | 收敛等待带来的**下载延迟**（约 1.8~3s/新编号） | 已知代价 | 有缓存：同编号只探测一次；`MIN_ELAPSED_MS` 仅在冷启动生效 |
| 🟢 低 | 极慢磁盘/超巨量历史导致 14 轮仍未收敛 | 兜底取最后一轮 | 折中：宁可多一个 (1) 也不覆盖 |
| 🟢 低 | 非法字符/超长路径 | `sanitizePath` 已处理 | — |
| 🟢 低 | 用户手动改保存路径后缓存未及时失效 | 切模式/改路径均清缓存 | — |

### 关键：即使所有风险同时发生，最坏结果是什么？

> **最坏 = 多出一个带 (1) 后缀的文件夹 + 文件多一个 (1) 后缀。**
> **绝不会覆盖、删除、损坏您已有的任何文件。**

这是 v0.4.3 与 v0.4.2 最本质的区别 —— **失败模式从"不可逆破坏"降级为"可逆冗余"**。

### 残留风险（需您知晓）

1. **第三方扩展干扰仍无法根除**：浏览器安全模型决定，多扩展同时声明文件名时以最后一个为准，插件无法强制胜过。→ 若仍异常，停用其他下载类扩展。
2. **下载历史建议清理一次**：您机器上历史 501 条 TE 记录声称 20 个文件夹，实际只剩 1 个。清理后判断更快更准（`edge://downloads` → 清除浏览数据 → 只勾"下载历史"）。**不会删除磁盘文件。**

---

## 四、问题③：您发现的 bug 是什么原因？

### 现象

> "实测发现会覆盖已存在的文件夹中的文件，而不是按照设置中的重复项选项进行新增文件夹并增加后缀"

### 根因（三个因素叠加，缺一不可）

#### 因素 1 — ★ 真正的病灶：`exists` 是"惰性异步收敛"的，不是实时磁盘检查

**实验 A**（全新 profile，`audit_live_exists.js`）：下载文件 → 立刻在磁盘上 `unlink` → 轮询 `exists`：

```
T+0s   → true
T+3s   → true
T+6s   → true
T+12s  → true
T+20s  → false   ← 才收敛
```

**实验 B**（您的真实 profile，5-928999-1 相关 57 条记录）：

```
T+ 0s    → 5-928999-1 (49条) 全 true ；5-928999-1 (2) (8条) 全 true
T+20s    → 5-928999-1 全变 false     ；5-928999-1 (2) 全变 false   ← 收敛完成
```

**结论：头一次查询几乎必然把"已经被你删掉的文件夹"误报为"还存在"。**

#### 因素 2 — 您机器上"历史声称"与"磁盘真相"严重脱节

| 维度 | 数值 |
|---|---|
| 下载历史总记录 | 893 条 |
| TE 相关记录 | 501 条 |
| 历史**声称**存在的编号文件夹 | **20 个**（含 `5-928999-1`、`(1)`、`(2)` …） |
| 磁盘**真实**存在的文件夹 | **1 个**：`5-928999-1 (1)`（8 个文件） |

→ v0.4.2 在延迟窗口内**只读了一次**就下结论，误判"编号、(1)、(2) 全都还在"，一路把后缀加到 **`(3)`** —— 而 `(3)` 磁盘上根本不存在。

#### 因素 3 — `conflictAction:"overwrite"` 把"误判"放大成"覆盖"

v0.4.2 为防"插件加一次后缀、浏览器又加一次"的双重后缀，把下载参数从 `uniquify` 改成了 `overwrite`。
→ **探测失误从"多一个 (1) 文件夹"直接升级为"不可逆覆盖用户已有文件"。**

### 完整因果链

```
用户手动删掉文件夹
      ↓
chrome.downloads 的 exists 在 ~20s 内仍报 true（因素 1）
      ↓
历史里 20 个幻影文件夹全被当成"存在"（因素 2）
      ↓
getFolderName('5-928999-1') 返回 '5-928999-1 (3)'（不存在的文件夹）
      ↓
conflictAction:'overwrite' 让浏览器覆盖同名文件（因素 3）
      ↓
⭐ 用户观测到："覆盖了已存在文件夹中的文件"
```

### 为什么前几版反复失效？

| 版本 | 做了什么 | 为什么没用 |
|---|---|---|
| v0.4.0 | 引入磁盘实况探测 | 方向对，但只读一次，落在延迟窗口内 |
| v0.4.1 | 修模式切换竞态 | 与本次根因无关（那是另一个 bug） |
| v0.4.1.1 | 补上内存态守卫 | 同上 |
| v0.4.2 | 插件自判重 + `overwrite` | 自判重依赖不可靠的 `exists`；`overwrite` 反而放大了后果 |

**共同教训：所有版本都只读一次就下结论 → 必然读到幻影。** 而 v0.4.2 的 `overwrite` 让这个既有缺陷第一次造成了不可逆破坏。

### v0.4.3 的修法（四处，缺一不可）

| # | 措施 | 作用 |
|---|---|---|
| ① | 文件夹探测**等收敛**：轮询直到连续两轮一致且已等待 ≥1.8s | **治本**，拿到刷新后的真实结果 |
| ② | 严格采信 `exists===true` 且 `state==='complete'` | 排除在途/中断下载 |
| ③ | `conflictAction` **永久改回 `uniquify`** | **安全阀**：最坏只多一个 (1)，绝不覆盖 |
| ④ | 文档判重**委托浏览器** | 消除双重后缀，且天然不覆盖 |

### 修复效果实测（真实 profile，接管浏览器）

| 编号 | 修复前 (v0.4.2) | 修复后 (v0.4.3) | 磁盘真相 |
|---|---|---|---|
| `5-928999-1` | `5-928999-1 (3)` ❌ | **`5-928999-1`** ✅ | 只有 `(1)` |
| `1897009-2` | `1897009-2 (3)` ❌ | **`1897009-2`** ✅ | 不存在 |
| `5-962885-1` | `5-962885-1 (3)` ❌ | **`5-962885-1`** ✅ | 不存在 |
| `2435022-2` | `2435022-2 (2)` ❌ | **`2435022-2`** ✅ | 不存在 |
| `2377023-2` | `2377023-2 (3)` ❌ | **`2377023-2`** ✅ | 不存在 |
| `114-32333` | `114-32333 (2)` ❌ | **`114-32333`** ✅ | 不存在 |

收敛实测（`5-928999-1`）：

```
逐轮探测 = ["5-928999-1 (1)", "5-928999-1 (1)"]
探测耗时 = 1909 ms
收敛后 existing = ["5-928999-1 (1)"]
决策 folder = 5-928999-1     ← 纯编号位置空闲，正确新建
```

---

## 五、接管浏览器动态审计（实测取证）

因 Playwright 会拦截下载（重定向到 `playwright-artifacts-*` 临时目录），导致插件的 `filename` 永远到不了文件系统，故**放弃 Playwright**，改为：

> **原生启动 Edge（`--remote-debugging-port` + `--load-extension`）+ 原始 CDP（native WebSocket）+ 通过 `Default/Preferences` 设定下载目录**，让下载真实落盘后观察最终路径。

### 实验 1：`uniquify` 对文件名的语义（`audit_live_uniquify.js` 场景 A）

三次请求 `TE Web Download/PN-0001/same.bin`：

```
第 1 次 → same.bin
第 2 次 → same (1).bin
第 3 次 → same (2).bin
磁盘最终：PN-0001/{same.bin, same (1).bin, same (2).bin}
```

→ **`uniquify` 对文件名有效，且不覆盖原有文件。**

### 实验 2：`uniquify` 对文件夹名的语义（`audit_live_folder_semantics.js`，★纯净版）

**绕开插件探测**，走完整链路（`queue-filename` → `onDeterminingFilename` → 建议**固定**文件夹名），连续三次请求 `TE Web Download/PN-BB/f1.bin`、`f2.bin`、`f3.bin`：

```
第 1 次 → ...\TE Web Download\PN-BB\f1.bin
第 2 次 → ...\TE Web Download\PN-BB\f2.bin
第 3 次 → ...\TE Web Download\PN-BB\f3.bin
磁盘最终：TE Web Download/PN-BB/{f1.bin, f2.bin, f3.bin}
```

→ **三次全部落进同一个 `PN-BB/`，没有产生 `PN-BB (1)/`、`PN-BB (2)/`。**
→ **浏览器 `uniquify` 不会给文件夹名加后缀 → 文件夹后缀必须插件自己算。**

> 注：早先一版脚本（`audit_live_uniquify.js` 场景 B）曾因**每次调用 `getFolderName()` 会重新探测**而出现 `PN-0002 (1)/`，一度被误读为"浏览器会给文件夹加后缀"。经纯净版实验证伪，该结论已作废，脚本内已加注说明。**这正是"必须用纯净对照实验、排除被测对象自身干扰"的教训。**

---

## 六、验证汇总

### 静态 + 真实源码执行装置

| 套件 | 覆盖 | 结果 |
|---|---|---|
| `verify_folder_v040.js` | 文件夹序号基本语义 | **12 / 0** |
| `verify_conflict_v041.js` | 模式切换与竞态 | **11 / 0** |
| `verify_realsource_v0411.js` | 真实源码竞态回归 | **21 / 0** |
| `verify_v042.js` | 需求 1/2 端到端（已按 v0.4.3 语义重写） | **20 / 0** |
| `verify_v043.js` | 收敛语义 + 反污染 + `uniquify` 断言 | **34 / 0** |
| **合计** | | **98 / 0** ✅ |

### 动态实测

| 项目 | 结果 |
|---|---|
| `exists` 收敛特性 | 已证实（~20s，暖缓存 ~1.5~3s） |
| `uniquify` × 文件名 | 加后缀、不覆盖 ✅ |
| `uniquify` × 文件夹名 | **完全不管** ✅ |
| 真实 profile 6+1 编号决策 | 7/7 正确 ✅ |
| 语法检查 | `popup.js` / `background.js` 均通过 ✅ |

---

## 七、结论

| 问题 | 结论 |
|---|---|
| ① 代码能否满足需求 | **能。** 文件夹后缀插件算、文档后缀委托浏览器，9 项需求点全部落地并有测试覆盖 |
| ② 是否有潜在失效风险 | **有，但已收敛。** 最坏结果从"不可逆覆盖"降级为"多一个 (1) 冗余"；残留的第三方扩展干扰已通过自查说明缓解 |
| ③ bug 是什么原因 | **`exists` 惰性收敛（~20s）→ 只读一次读到满屏幻影 → 误判文件夹存在 → `overwrite` 放大成覆盖。** 已用 3 处修改根治，真实 profile 实测 6 个编号全部回到正确决策 |

### 给您的操作建议

1. 扩展管理页先「**移除**」旧版卡片，再加载 v0.4.3（两版文件名相同，不移除会继续跑旧代码）。
2. 建议清理一次陈旧下载历史：`edge://downloads` → 清除浏览数据 → 只勾「下载历史」→ 清除。**不会删除磁盘文件**，但能让插件判断更快更准。
3. 若仍有异常，按说明书 Q4 逐个停用其他下载类扩展排查。

---

*报告完 · 2026-10-12*
