# Findings

## Research Log
- 远程公告通道适合使用 Cloudflare KV：需求只保存每个 `projectName` 最新一份公告，KV 的 key-value 模型比 D1 更轻量；Worker 现有 `ADMIN_TOKEN` 鉴权可复用到公告管理接口。
- 客户端已有 `UpdateNotifier`，版本检查间隔为 30 分钟；可在同一个定时器里并行检查更新和远程公告，但更新继续用 Toast，公告使用独立 Dialog，避免同时触发时互相覆盖。
- 客户端已有 `MarkdownRenderer`，公告渲染必须传 `allowRawHtml={false}`，避免远程公告里的 HTML 被渲染成 DOM。
- Dashboard 是单文件静态页，已有 `requestJson()` 默认 GET + Bearer token；公告发布需要扩展为支持 POST/DELETE JSON body。
- 公告管理的 `DELETE /api/notice` 会触发浏览器 CORS 预检，Worker 的 `Access-Control-Allow-Methods` 必须包含 `DELETE`，否则 Dashboard 停用公告会失败。
- 本轮统计扩展只做“活跃与留存”和“配置使用情况”。现有 Analytics Engine blob1-blob7 已用于 project/event/page/version/platform/arch/clientId；可从 blob8 起扩展 `clientCreatedAt` 和配置枚举字段，保持旧数据兼容。
- 客户端现有 `analytics.ts` 只支持 `app_open/page_view`，并从 `user_config` 读取 `analytics_client_id`；可继续通过 `window.yibiao.config.load()` 读取 `analytics_created_at` 和配置项。
- Dashboard 当前仍按旧 `/api/latest?limit=50` 调用，Worker 已改为分页 `page/pageSize=10`，需要同步页面分页控件。
- 留存查询使用 `blob8` 的 `analytics_created_at` 作为 cohort 日期；为兼容旧事件和异常空值，SQL 中使用 `toDateOrNull(blob8)`，旧数据不会进入留存口径但仍可进入总客户端/活跃客户端。
- 当前客户端 `DocumentAnalysisPage` 仍是“导入文件 + AI 解析项目概述/评分要求”的旧交互，导入后没有 Markdown 渲染原始提取内容。
- 当前 `client/electron/services/fileService.cjs` 只用 `mammoth.extractRawText` 和 `pdf-parse` 做纯文本提取，未按配置中的 `file_parser.provider` 分流，也未使用 `tools/doc2markdown-node` 的 Markdown 还原逻辑。
- 配置文件中 `file_parser.provider` 已存在，值为 `local`、`mineru-accurate-api`、`mineru-agent-api`，但文件解析服务没有读取配置。
- `tools/doc2markdown-node/src/convert.js` 是 ESM，实现包括 Markdown 编码识别、DOCX->HTML->GFM Markdown、PDF 文本/表格提取、DOC/WPS 经 LibreOffice 转 DOCX。要“100%还原”本地解析，应复用该模块而不是重写简化版。
- MinerU Agent 轻量 API：`POST https://mineru.net/api/v1/agent/parse/file` 获取 `task_id/file_url`，`PUT` 上传，`GET /parse/{task_id}` 轮询，完成后下载 `markdown_url`。无需 Token。
- MinerU 精准 API：`POST https://mineru.net/api/v4/file-urls/batch` 带 Bearer Token 获取 `batch_id/file_url`，`PUT` 上传，`GET /extract-results/batch/{batch_id}` 轮询，下载 `full_zip_url`，从 zip 中读取 `full.md` 或任意 `.md`。
- 已将 `tools/doc2markdown-node/src/convert.js` 复制到 `client/electron/services/doc2markdown/convert.mjs`，运行时不再依赖 `tools/` 目录。
- 前端 Markdown 渲染使用 `react-markdown`、`remark-gfm`、`rehype-raw`，用于展示 GFM 表格和 DOCX 转换保留下来的 HTML 表格。
- 技术方案 Markdown 结果此前只保存在 `TechnicalPlanHome` 内存状态；切换到设置页会卸载页面导致丢失。
- 临时新增的 `technicalPlanStorage.ts` 使用 Renderer `localStorage`，不适合保存招标文件 Markdown 这类大文本，应迁移到 Electron Main 的 `userData` 文件。
- 现有 IPC 注册集中在 `electron/ipc/index.cjs`，preload 暴露集中在 `electron/preload.cjs`，Renderer 类型来自 `src/vite-env.d.ts` 引用的 `shared/types`。
- Step02 需要实时显示模型输出，现有 `ai.chat()` 只能一次性返回；已新增 OpenAI-compatible SSE 解析通道，通过 `ai:stream-chat` IPC 和 `window.yibiao.ai.streamChat()` 向 Renderer 推送 chunk。
- 旧版目录生成核心在 `backend/app/services/outline_service.py` 和 `backend/app/utils/prompts/outline_prompts.py`：自由模式包含一次性生成、失败切分步生成、审核和二次生成；对齐模式先提取技术评分大类，再按大类生成二三级目录并审核。已迁入 client 的 `outlineWorkflow.ts` 与 `outlinePrompts.ts`。
- Step02/Step03 后台任务运行时，Renderer 的整包技术方案保存可能覆盖 Main 刚写入的任务进度；已在 `useTechnicalPlanWorkflow` 中跳过运行中任务状态下的 debounce/卸载保存，避免写入竞争。
- client 目录生成失败率高的根因是此前只仿写了 backend 流程，未迁移 `OpenAIUtil.collect_json_response()` 的完整链路；后端每一步 JSON 调用都在同一函数内执行解析、Pydantic schema 校验、业务 validator、JSON 修复和最多 3 轮重试，而 client 此前把业务校验放在 `requestJson()` 外部，导致校验失败不能进入修复/重试。
- 已将 client 目录生成 prompt 和 validator 对齐 backend：完整目录只要求非空且至少三级；一级目录只要求非空；children 只要求二级目录非空；不再额外把“无描述/没有提及”作为生成失败条件。
- backend `/api/content/generate-chapter-stream` 的契约很轻：请求包含 `chapter`、`parent_chapters`、`sibling_chapters`、`project_overview`，服务端用 `build_chapter_content_messages()` 后以 `temperature=0.7` 流式返回纯正文 chunk。
- 旧 `frontend/src/pages/ContentEdit.tsx` 已实现可参考的叶子节点收集、父级章节查找、同级章节查找、5 并发生成和 Word 导出 payload 构造；但旧版依赖浏览器 SSE、`file-saver` 和本地草稿缓存，client 需要改为 Main 后台任务与工作区文件存储。
- backend `/api/document/export-word` 的 payload 是 `{ project_name?, project_overview?, outline }`，其中 `outline` 节点包含 `id/title/description/children/content`；导出服务只对叶子节点渲染 `content`，Markdown 支持标题、列表、表格行、粗体/斜体/代码。
- client 现有 `exportService.cjs` 是未实现占位；`preload.cjs` 已暴露 `window.yibiao.export.exportWord(payload)`，但 Main 侧还需要实现保存对话框和 docx 写入。
- toolbar 与滚动优化调研：全局 `.content-shell` 原先使用底部大 padding 给 toolbar 预留空间，技术方案页通过 `:has(.technical-workbench)` 再额外处理；Step02 和设置页也有页面级底部 padding，会造成内容高度被压缩且不符合悬浮覆盖要求。
- toolbar 与滚动优化调研：`FloatingToolbar` 同时可由 AppShell 和页面内部渲染；将 `.content-shell` 设为相对定位和隐藏溢出后，页面内部 toolbar 可以继续相对内容区域悬浮，页面内容则由各自根容器或工作区承担内部滚动。
- GitHub Release 发布调研：当前远程仓库为 `FB208/OpenBidKit_Yibiao`，`electron-builder` 可直接使用 GitHub provider 上传安装包和 `latest.yml` / `latest-mac.yml` 更新元数据。
- 自动更新实现调研：`electron-updater` 在普通 Node 环境 require 会访问 Electron app，因此必须在 `app.isPackaged` 后懒加载；开发模式跳过更新检查。
- Windows 本地打包调研：未签名阶段仍可能触发 `winCodeSign` 资源编辑链路，当前 Windows 用户没有创建符号链接权限会导致解压失败；关闭 `win.signAndEditExecutable` 后 NSIS 安装包验证通过。
- v2.0.1 Release 空产物根因：workflow 先用 `gh release create` 创建了正式 Release，但 `electron-builder --publish always` 默认以 draft 发布类型工作，日志显示 `existingType=release publishingType=draft`，因此所有安装包、blockmap 和 `latest*.yml` 都被跳过上传。
- v2.0.1 Release 说明只有 `Full Changelog` 根因：GitHub `--generate-notes` 在没有可识别 PR/分组内容时只生成比较链接；改为 workflow 内显式用 `git log` 生成提交列表更可控。
- Actions `Build renderer` 报 `TS2688: Cannot find type definition file for 'plist'`：干净 CI 环境或重跑旧 tag 时缺少 `@types/plist`，但 TypeScript 会从 electron-builder 相关类型链路发现 `plist` 类型引用；已将 `@types/plist` 显式加入 devDependency，并在 workflow `npm ci` 后 `npm install --no-save @types/plist` 以兼容重跑旧 tag。
- GitHub Actions 的 `Re-run all jobs` 会重跑旧 run 当时的 workflow 文件和 tag 提交，不会使用 main 上后来修复的 workflow；要补发已有 tag，必须用 `workflow_dispatch` 从 main 手动运行并输入 tag。
- v2.0.1 使用最新 workflow 手动发布后已上传 13 个 Release assets；旧 tag 的 artifactName 生成了空前缀文件名，后续版本已改为稳定 ASCII 前缀 `Yibiao-${version}-${os}-${arch}.${ext}`。
- 下载的 `-2.0.1-win-x64.exe` SHA256 与 GitHub Release digest 一致，7-Zip 可识别为 NSIS/Electron 安装包；本地启动后进程保持运行且有响应窗口标题，说明安装器没有崩溃。用户看不到窗口更可能是安全扫描延迟、窗口在后方/任务栏，或旧产物以 `-` 开头导致识别体验差。
- 打包后图标仍为 Electron 默认图标的根因：Windows 配置中此前为绕过本机 `winCodeSign` 解压权限问题关闭了 `win.signAndEditExecutable`，导致 electron-builder 不会把 `assets/icon.ico` 写入 exe 资源；macOS 则缺少 `assets/icon.icns`。已恢复 Windows exe 资源编辑，并在 macOS workflow 中用 `sips` + `iconutil` 从 `assets/icon_256.png` 生成 `assets/icon.icns` 后打包。
- Step04 当前正文生成只在 `contentGenerationTask.cjs` 里流式生成纯正文；`ContentEditPage` 已用 `ReactMarkdown + remark-gfm` 展示 Markdown，因此表格可直接由正文 Markdown 承载。
- 生图配置目前只有测试接口，没有持久化可用状态，也没有正式生图方法；需要在 `image_model` 下增加状态字段，并由设置页测试结果写回配置。
- Word 导出已支持 Markdown 图片节点，但预览和导出还需要统一支持本机工作区图片路径；使用 `yibiao-asset://generated-images/...` 可避免把大 base64 长期写入 `technical_plan.json`。
- mermaid.ink 的 `pako:` 编码不是直接压缩 Mermaid 源码；它会先反序列化 `{ code, mermaid }` JSON 状态，再用 pako inflate 解压。客户端应使用 `zlib.deflateSync(JSON.stringify({ code, mermaid: { theme: 'default' } }))` 后转 base64url，不能用 `deflateRawSync` 直接压缩源码。
- 第三批优化后，新正文中的 Mermaid 图保存为 Markdown `mermaid` 代码块；Renderer 通过动态导入 `mermaid` 本地渲染预览，Word 导出时 Main 再通过 mermaid.ink 转 PNG 并上报进度。
- `docx` 包默认只声明 `png/jpeg/jpg/bmp/gif` 图片 content type，不包含 WebP；生成图如果保存为 `.webp`，导出前应在 Electron Main 运行时用 `nativeImage` 转 PNG。
- 正文页启用 `rehypeRaw` 后，Word 导出需要处理常见 HTML 节点，否则会出现预览可见但导出丢失或降级的问题；当前已覆盖 `br/img/table/list/blockquote/strong/em/code` 等常见标签。
- Mermaid 预览失败不一定能只靠 mermaid.ink 发现：截图中的分号、`&` 多节点连接简写、未加引号的中文节点标签在 mermaid.ink 可返回 PNG，但前端 `mermaid.render()` 仍可能报 lexical error。因此生成侧校验除了 mermaid.ink 渲染结果，还需要加前端兼容性规则，要求中文标签写成 `A["中文"]`、不用分号、不用 `&` 简写。
- Step04 配图应从正文生成阶段拆出：编排阶段可同时提名 AI 生图和 Mermaid 候选，正文生成阶段只写纯正文，配图阶段再按 AI 上限先选 AI 目标，未入选 AI 但有 Mermaid 候选的章节降级为 Mermaid。这样既保留 AI 上限控制，也不会让 AI 候选挤掉 Mermaid 图数量。
- Word 导出表格漏识别的关键风险在 `remark-gfm` 解析前的原始 Markdown 形态：模型可能把标题和表格写在同一行，或把多行表格压缩成一行；导出层先拆分并补空行后，压缩表格 smoke test 可生成真实 `<w:tbl>`。
- Word 导出有序列表编号连续的根因是所有 ordered list 共用同一个 numbering reference；改为每个 Markdown/HTML 有序列表块创建独立 reference 后，两段独立列表在 document XML 中使用不同 `numId`。
- 截图中未转换成功的表格属于更具体的压缩形态：表头单独一行，但下一行同时包含 GFM 分隔列和数据列，例如 `| :--- | ... | :--- | 每日运行简报 | ... |`。按表头列数拆出分隔行和后续数据行后，`remark-gfm` 能正常生成 table AST。
- 单章重新生成此前无法复用编排结果，因为 `contentPlans` 只存在于 `contentGenerationTask.cjs` 的运行时 `Map` 中，任务结束后不会写入 `technical_plan.json`。需要将最终配图决策持久化为 `contentGenerationPlans`，单章才可跳过重新编排。
- 单章重新生成复用历史编排时仍能完整走“正文生成 -> 配图”流程；缺失历史编排时只编排目标小节一次，不触发全文 `planAll()`。
- Word 导出 HTML 容器评审有效：`htmlNodeToDocxBlocks()` 原先把 `div/section/article` 统一走 `htmlInlineRuns()`，会让容器内的 `table/ul/ol/blockquote/img` 等块级内容丢失 Word 原生结构。修复后仅当容器存在块级子节点时递归到 `htmlNodesToDocxBlocks()`，纯内联容器仍保持单段落输出。
- 列表项内 Markdown 表格导出失败的根因是导出前表格归一化丢掉了分隔行缩进：`    | :--- | ... |` 被拆成 `| :--- | ... |`，导致表头/分隔行/数据行缩进不一致，`remark-gfm` 不能把它识别成嵌套表格。保留空白前缀缩进后，列表内表格可以导出为 Word 原生表格。

## Knowledge Base Redesign Findings
- 用户确认知识库新流程：程序筛除无价值块并保留 `filtered_blocks.json`；筛后内容编号为 block；AI 先全文抽条目，再用全文 + 首轮条目做补充；程序合并并生成 ID；页面输入每批匹配条目数；分批提交固定全文 + 当前批次条目做强相关匹配；遗漏 block 走最多两轮补漏；最终程序回填正文。
- 分批匹配请求需要缓存友好：固定提示词和全文 block 保持在请求前缀，变化的知识条目批次放最后，避免前缀变化破坏 AI 服务商 prompt cache。
- 用户明确暂不做冲突检查；即使同一 block 被多个条目引用，也先观察实际效果再决定是否投入治理。
- 现有 `knowledgeBaseService.cjs` 上传后立即后台处理文档：复制原文件、转换 Markdown、按 `B00001` 旧块切分、分 chunk 让 AI 直接输出 `items + source_block_ids`，随后程序回填正文写 `items.json`。需要替换为“准备分析 -> 用户输入每批条目数 -> 继续匹配”的两阶段流程。
- 现有 IPC/preload 只有 `list/createFolder/uploadDocuments/readMarkdown/readItems/onEvent`，需要增加读取分析快照/报告和启动分批匹配的接口。
- 现有前端共享类型只定义 `pending/copying/converting/analyzing/saving/success/error` 状态；新流程需要新增候选条目已生成但未匹配的状态，以及报告字段。
- `aiService.collectJsonResponse()` 已提供 JSON 解析、修复和最多重试能力，知识库新流程应继续复用它；开发者模式下 AI 日志会保存完整请求，可用于验证稳定前缀和 prompt cache 结构。
- `KnowledgeBasePage.tsx` 当前已经有内部“列表/条目详情/Markdown详情”状态切换，适合继续扩展出分析调试详情页，不必引入 URL 路由。
- 已实现的新版知识库落盘文件包括：`content.md`、`blocks.json`、`filtered_blocks.json`、`candidate_items.json`、`match_result.json`、`report.json`、`items.json`。其中 `match_result.json` 保存批次匹配、补漏、新增条目、AI 舍弃和 `system_discarded_after_retry`。
- 前端新增“分析调试”详情页：展示候选条目、有效/筛除 block、最终条目、覆盖率、补漏新增、舍弃记录，并允许输入每批条目数启动 `startMatching`。
- 知识库 AI 请求已按缓存优化改为纯 `user` 多消息：全文 block 是第一条 user message，任务要求和当前批次变量放后续 user message；不再使用 system prompt，避免 system 内容破坏前缀缓存收益。
- 开发者模式下知识库流程现在额外输出 JSONL 调试日志到 `logs/knowledge-base/<documentId>.jsonl`，用于定位“卡住”发生在 AI 调用前、AI 调用后、normalizer/validator、状态更新还是保存阶段。
- Step03 当前页面顶部直接渲染 `outline-mode-switch`，`generateOutline()` 直接用 `outlineMode` 启动后台任务；知识库列表可直接通过 `window.yibiao.knowledgeBase.list()` 获取 `folders/documents`，其中 `KnowledgeDocument.status === 'success'` 可作为可选条件。
- 本轮 Step03 改造只需 Renderer UI 和技术方案状态扩展：目录生成后台 `startOutlineGeneration({ overview, requirements, mode })` 保持不变，参考知识库文档 ID 只先保存到前端状态。
- 目录生成应用知识库的关键冲突：知识库单文档内条目 ID 为 `K000001` 递增，多文档之间会重复；目录节点的 `knowledge_item_ids` 必须保存为 `document_id::item_id` 才能在后续正文生成阶段唯一定位。
- 当前 Main 目录生成实际链路在 `client/electron/services/outlineGenerationTask.cjs`，且已使用 `collectJsonResponse` 做修复/重试；需要直接改该文件，而不是旧 Renderer `outlineWorkflow.ts`。
- 旧 Step03 知识库接入仍会在 `generateChildrenMessages()` / `generateAlignedChildrenMessages()` 注入按一级目录筛选的知识条目，且自由模式检测到知识库后强制走 fallback；这会导致同一知识条目跨一级目录重复引用，必须由后置全局 Patch 替代。
- Step03 Patch 模式已改为完整目录后置增强：主目录 normalizer 使用空知识 ID 集合清掉模型误返回的 `knowledge_item_ids`；Patch prompt 使用纯 `user` 多消息，normalizer 拒绝完整 `outline`，只接受二三级 bindings 和一级/二级 parent additions；应用后再统一重编号并全局去重。
- Step03 Patch 未落盘 `knowledge_item_ids` 的根因已确认：模型照抄 prompt 示例中的 `document_id::K000001` 占位 ID，而真实 ID 是 `doc-00665a28-...::K000001`；同时模型把部分 additions 挂到三级目录，违反“不能新增四级目录”的规则。新实现通过真实白名单提示和严格校验修复该类问题。
- Step03 旧 Patch 仍把任务建模成“绑定知识库 ID + 可新增二/三级目录”，会把弱模型注意力拉向 `bindings` 和 `knowledge_item_ids`，与当前目标“只补缺失三级目录”冲突。新方案应完全不向模型暴露知识 ID，只把知识条目的标题/摘要作为参考文本。
- additions-only 后，Step03 知识库增强的可靠边界更清晰：AI 只决定“某个现有二级目录下是否缺一个三级目录”，程序负责编号、去重、删除多余字段和防止知识 ID 落盘；如果模型返回 bindings-only 或完整 outline，normalizer 会触发专用修复 prompt，而不是静默写入无效绑定。
- Step04 知识库编排不需要单独全局分配阶段：将完整知识库轻量清单作为每个叶子节点编排请求的稳定前缀即可利用服务商缓存；多对多关系通过每个叶子节点独立选择 `knowledge.item_ids` 自然产生，不需要全局唯一性约束。
- Step04 编排阶段的知识库输入边界应保持为 `id/title/resume`，不提交 `content`；当前实现只落盘 `knowledge.item_ids`，正文生成阶段暂不消费知识库正文。
- Step04 正文生成阶段应用知识库的最小边界：程序用 `knowledge.item_ids` 定位条目，但给正文模型只传 `content`；知识库素材放在项目概述之后、章节上下文之前，可让相同素材组合在不同章节间尽量复用服务商 prompt cache 前缀。
- 知识库当前已有日志只覆盖 `查看原文` 和全文 Markdown 内容渲染阶段；`openDocument()` 的点击、IPC 读取、JSON 解析、`setItemsPreview`、条目列表整体渲染和下一帧可见尚未被记录，这正是继续定位用户感知慢点的缺口。
- 新增 `document-items` trace 后，条目页会记录 `click:open-document`、`ipc:read:start/end`、`items:metrics`、`state:set-items-preview`、React Profiler、`dom:commit`、`dom:next-frame-visible` 和 Long Task；可与 `document-markdown`、`item-source` trace 直接对比。
- 知识条目列表滚动重置的根因是此前 `sourceItem ? 原文页 : 列表` 会用原文视图替换条目列表，列表 DOM 被卸载；改为 Dialog 后列表保持挂载，关闭原文不会丢失滚动位置。
- 标书查重元数据模块实现边界：正文提取覆盖招标文件和投标文件，且 `preserveImages: true`；元数据横向对比仅覆盖投标文件。DOCX 元数据来自 `docProps/core.xml`、`app.xml`、`custom.xml`，PDF 元数据来自 `pdf-parse` 的 `getInfo()`，DOC/WPS 当前正文可通过既有 LibreOffice 链路提取，元数据先保留文件系统信息。
- 标书查重时间类元数据对比规则已改为日期粒度：`created_at/modified_at/accessed_at/created/modified` 同一天出现于多份投标文件时用橙色高亮；非时间类重复仍用红色高亮。
- `.doc/.wps` 元数据增强可通过 `cfb` 读取 OLE Compound File，重点流名是 `\u0005SummaryInformation` 和 `\u0005DocumentSummaryInformation`；前者覆盖作者、最后作者、模板、创建/修改/打印时间、页数、字数、应用程序等，后者覆盖类别、公司、管理者、应用版本、字节/行/段落/字符数、自定义属性等。
- WPS/账号没有稳定标准字段；实现上应从 WPS/Kingsoft/KSO/account/email/uid 等关键词命中的标准属性、自定义属性、OLE 流和 PDF XMP/原始记录中生成 `wps:*` 疑似字段，并在 UI 中标为“疑似 WPS 用户/账号”。
- PDF 元数据不能只读固定 `Title/Author/Creator/Producer`；`pdf-parse` 的 `getInfo()` 同时返回 `info`、可迭代 `metadata`、`fingerprints`、`permission`，原始 PDF 字节中还可能有增量更新残留的 `/Author`、`/Creator`、`/ModDate` 等记录。
- `.doc/.wps` 采用双来源读取更稳：原始 OLE/HPSF 属性优先，LibreOffice 转 `.docx` 后的 `docProps/*` 作为补充；转换失败时仍保留 OLE 已读字段并记录 `metadata_error`。
- 标书查重目录分析接入点：`duplicateCheckService.cjs` 已在 Step02 并发提取正文和元数据，正文保存到 `userData/workspace/duplicate-check/contents/<fileId>.md`；目录分析应复用这些 Markdown，不重新解析原文件。
- 招标文件目录规则：招标文件不参与横向重复对比，但需拆句生成白名单；投标目录项命中招标文件句子/短句时标记为来自招标文件并从重复组和整体相似度中排除。
- 目录提取第一版应走纯程序：显式目录块（目录/目次/Contents + 点线页码/编号）优先，Markdown `#` 标题次之，短行编号/粗体/第X章等语义标题兜底。
- 正文比对可复用 `duplicate-check/contents/*.md`：先移除 Markdown/HTML 图片，再按句号、问号、感叹号、分号和换行拆句；投标文件内先去重，再用全局 Map 聚合跨文件重复，避免文件两两全文比较。
- 图片比对可直接扫描 Markdown 图片链接；当前保留图片流程会把本地图片转为 `yibiao-asset://imported-images/...`，Main 侧可根据协议根目录解析回文件并计算 SHA256，筛出完全一致图片。
- 正文表格拼接问题的真实根因是 DOCX/WPS 转 Markdown 后保留了 HTML `<table>`，而正文分句前直接 `replace(/<[^>]+>/g, ' ')` 删除 `<td>/<tr>/<p>` 等结构标签，导致“无偏离”“2”“供应商的报价应包括...”等不同单元格被压成同一段文本。
- `isLikelyMergedTableSentence()` 这类按“无偏离/特别要求/交货地点”等业务词跳过整句的规则属于特例过滤，会误伤真实商务响应内容；正确修复是保留表格结构边界，让单元格独立分句。
- 新正文分句链路应先提取结构化文本块：HTML 表格按 `<td>/<th>` 单元格提取，单元格内 `<p>/<li>/<br>` 作为内部边界；Markdown 管道表按 GFM 表格行列解析；普通 Markdown 段落再按自然句末符分句。
- 真实缓存验证结果：`9e2770208cb01a044c6e6eaff29328cc83faf75c.md` 中“供应商的报价应包括...”前后块已变为 `无偏离`、`2`、正文单元格、投标应答单元格、`无偏离`、`3`，模拟重复分析中 `无偏离\s*\d+`、表头串联类异常重复句数量为 0。
- `特别要求：交货时要求供应商就所投产品提供产品说明书...` 未被招标白名单排除的根因不是招标原文缺失，而是招标文件分句为 `3.特别要求：...`，投标文件分句为 `特别要求：...`，此前 normalized 完全相等匹配无法跨过句首序号差异。
- 正文比对和招标引用排除都应忽略句首结构性序号；实现边界是仅剥离句首阿拉伯数字层级编号、中文编号、括号编号和圈号，不处理正文中间的 `GB/T 29768-2013`、`30天`、`3年`、`第2包` 等业务数字。
- 真实缓存复验结果：招标白名单已包含去掉 `3.` 后的目标句，投标文件中该目标句命中白名单 5 次，模拟重复分析 `targetInDuplicates=false`；总重复句由 132 降到 116。
- 截图中 `1. 供应商资格要求证明文件`、`（四）法定代表人资格证明书` 等仍出现的原因不是 `normalized` 未去序号；当前缓存中这些项的 `normalized` 已去序号，UI 展示的是保留原文的 `sentence`。真正问题是此前先用原句做 `isInformativeContentSentence()`，短标题加上序号后长度达标进入投标重复池，而招标文件同名无序号短标题长度不足没有进入白名单。
- 修复后正文分句先生成 `normalized` 再判断信息量；同时句首序号剥离支持 Markdown 转义 `1\.`、全角数字、括号/圈号后额外分隔符和 `第一章/第1节/第二部分`。真实缓存模拟结果：招标白名单句子数 717，投标正文句子数 6167，命中招标白名单 1997，重复句 105，截图短标题重复项 0，normalized 句首序号残留 0，目标“特别要求”重复项 0。
