# 易标投标工具箱 Client 开发说明

本文面向参与 `client/` 独立客户端开发的成员，目标是快速理解架构边界、目录职责和统一开发风格。

## 技术栈
- Electron：桌面窗口、本地文件系统、配置存储、后续打包入口。
- Vite + React + TypeScript：Renderer 层 UI 与业务流程编排。
- Radix UI：Dialog、Tooltip、Switch 等无样式基础组件。
- CSS：当前使用全局 CSS，不引入 Tailwind，不复用旧 `frontend/` 样式。

## 运行命令
```powershell
cd client
npm install
npm run dev
npm run build
npm audit
```

## 架构边界
- `client/` 是独立桌面客户端，不依赖旧 `frontend/` 和 `backend/` 源码。
- Renderer 不直接访问 Node API，不直接保存 API Key。
- Electron Main 负责本地能力：配置、文件、AI 请求、导出。
- Renderer 通过 `window.yibiao` 调用 preload 暴露的安全桥接 API。
- Prompt builder 只负责组装消息，不直接请求 AI。
- Feature service 只负责本功能业务编排，不直接写 IPC 细节。
- 配置、工作区、草稿等需要持久化的业务数据由 Electron Main 侧读写文件；Renderer 不用 `localStorage` 保存大文本或业务流程状态。
- 所有耗时任务、AI 生成/检查、文件解析、批处理、需要进度展示或可能跨页面运行的流程，必须放到 Electron Main 后台任务中执行；Renderer 不承载长任务生命周期。

## 目录职责
```text
client/
├── electron/                 # Electron Main / Preload / IPC / Node 服务
│   ├── ipc/                  # IPC 通道注册，只做参数转发
│   ├── services/             # 本地配置、AI、文件、导出服务
│   └── utils/                # Main 侧工具函数
├── src/
│   ├── app/                  # 应用级路由、菜单、Provider
│   ├── components/           # 应用布局组件，例如 AppShell、Sidebar
│   ├── features/             # 业务功能模块
│   ├── shared/               # 跨功能共享能力
│   ├── main.tsx              # Renderer 入口
│   └── styles.css            # 全局样式
└── assets/                   # 客户端图标和静态资源
```

## App 层
- `src/app/menuConfig.ts`：左侧菜单配置，新增一级功能优先改这里。
- `src/app/AppRouter.tsx`：根据 `SectionId` 渲染对应页面。
- `src/app/providers/AppProviders.tsx`：全局 Provider 入口，后续接状态管理或主题时从这里加。

新增主菜单页面时，按以下顺序修改：
- 在 `src/shared/types/navigation.ts` 增加 `SectionId`。
- 在 `src/app/menuConfig.ts` 增加菜单项。
- 在 `src/app/AppRouter.tsx` 增加页面分支。
- 如需工具条动作，由页面根据自身流程和状态渲染 `FloatingToolbar`。

## Feature 层
每个业务功能放在 `src/features/<feature-name>/` 下：
```text
features/<feature-name>/
├── pages/        # 页面级组件
├── components/   # 本功能专用组件
├── hooks/        # 本功能专用 Hook
├── services/     # 本功能流程编排
└── types.ts      # 本功能专用类型
```

当前功能模块：
- `technical-plan`：技术方案，流程统一为上传招标文件、招标文件解析、目录生成、正文生成、扩写改写。
- `knowledge-base`：知识库。
- `duplicate-check`：标书查重。
- `rejection-check`：废标项检查。
- `settings`：独立设置页，由侧栏底部入口进入，管理文本模型、生图模型、文件解析和关于信息。

Feature 开发规则：
- 页面只做 UI 和交互编排，不直接调用 IPC。
- AI 请求通过本 feature 的 `services/` 进入，再调用 `shared/ai`。
- Prompt 统一放 `shared/prompts/`，不要散落在组件里。
- 跨功能类型放 `shared/types/`，仅本功能使用的类型放本功能 `types.ts`。
- 新功能只要存在“开始后可能耗时、需要进度、可能切换页面后继续”的流程，必须按后台任务架构设计：Main 执行、workspace 持久化、Renderer 启动/订阅/展示。
- Renderer 可以保存轻量 UI 状态，但不能把 running 任务的最终状态、进度和结果作为唯一来源；运行中的任务状态以 Main 侧 workspace 缓存为准。
- 技术方案 Step01 只负责上传招标文件并展示解析出的 Markdown，不做 AI 理解或目录生成。
- 技术方案步骤推进统一由页面级 `FloatingToolbar` 控制，未满足前置条件时禁用“下一步”，不要在步骤页面主体里重复放同语义下一步按钮。
- 页面主体必须占满当前窗口内容区，避免出现全局滚动条；长内容使用页面内部列表、阅读器、工作区或设置页容器自己的滚动条。
- 设置页保持独立页面形态，不改回弹窗；保存入口统一使用页面级 `FloatingToolbar`，不要在各配置卡片底部重复放保存按钮。
- 设置页模型测试按钮放在模型名称输入/选择行内，保存按钮文案统一为 `保存`。

## Shared 层
`src/shared/` 放跨功能复用能力：
- `shared/ai/aiClient.ts`：Renderer 侧 AI 请求门面。
- `shared/prompts/`：Prompt builder。
- `shared/storage/`：仅保留轻量 Renderer 缓存工具；业务工作区数据优先走 Electron Main 侧存储。
- `shared/types/`：跨功能共享类型。
- `shared/ui/`：跨功能通用 UI，例如 `FloatingToolbar`、`DetailHelpLink`。
- `shared/utils/`：错误、JSON、ID 等通用工具。

共享层规则：
- 不引用任何 feature 模块，避免循环依赖。
- 类型优先 `interface`，联合类型和泛型工具类型用 `type`。
- 用户可见文案使用中文。
- 配置项后需要“详细说明”弹窗时，优先复用 `shared/ui/DetailHelpLink.tsx`，不要在各页面重复写一套说明弹窗。

## Markdown 显示与编辑
- Markdown 正文展示统一使用 `src/shared/ui/MarkdownRenderer.tsx`，从 `shared/ui` 导入；不要在页面里直接使用 `react-markdown`、`rehypeRaw` 或另写一套基础 Markdown 渲染逻辑。
- Markdown 阅读区域优先复用 `.markdown-viewer` 和 `.markdown-empty-state`；页面专用样式只补充尺寸、布局或局部交互，不要重写整套排版样式。
- `MarkdownRenderer` 默认启用 GFM 和 raw HTML 渲染，适合展示本地解析出的可信文档内容，例如招标文件、投标文件、知识库原文和技术方案正文预览。
- 展示远程公告、AI 输出或其他不可信内容时，必须显式传 `allowRawHtml={false}`；只有明确需要渲染可信 HTML 表格、图片等原文结构时，才允许开启 raw HTML。
- Markdown 编辑统一使用 `src/shared/ui/MarkdownEditor.tsx`，从 `shared/ui` 导入；不要在页面里重复实现基础工具栏、选区插入和 textarea 编辑器。
- `MarkdownEditor` 只负责基础编辑能力，包含加粗、斜体、标题、引用、无序列表、有序列表和受控 `value/onChange`；业务侧如需预览，继续搭配 `MarkdownRenderer` 或业务专用预览组件。
- Mermaid、图片点击预览等业务增强能力不要默认塞进 `MarkdownEditor`；需要时在具体页面的 Markdown 预览组件中通过 `MarkdownRenderer` 的 `components` 扩展。

## Electron 层
`electron/` 只使用 CommonJS，保持与 Electron 当前入口一致。

### preload
`electron/preload.cjs` 暴露：
```ts
window.yibiao.config.load() // 返回 ClientConfig
window.yibiao.config.save(config) // 保存 ClientConfig
window.yibiao.config.listModels()
window.yibiao.ai.chat(request)
window.yibiao.ai.requestJson(request)
window.yibiao.ai.testImageModel(config)
window.yibiao.file.importDocument()
window.yibiao.workspace.loadTechnicalPlan()
window.yibiao.workspace.saveTechnicalPlan(state)
window.yibiao.workspace.clearTechnicalPlan()
window.yibiao.workspace.loadDuplicateCheck()
window.yibiao.workspace.saveDuplicateCheck(state)
window.yibiao.workspace.clearDuplicateCheck()
window.yibiao.tasks.startBidAnalysis(payload)
window.yibiao.tasks.startOutlineGeneration(payload)
window.yibiao.tasks.startContentGeneration(payload)
window.yibiao.tasks.startRejectionItemsExtraction(payload)
window.yibiao.tasks.startRejectionCheck(payload)
window.yibiao.tasks.startDuplicateAnalysis(payload)
window.yibiao.tasks.getActiveTasks()
window.yibiao.tasks.onTaskEvent(callback)
window.yibiao.export.exportWord(payload)
```

### IPC
`electron/ipc/*.cjs` 只注册通道，不写业务逻辑。

命名规则：
- 配置：`config:*`
- AI：`ai:*`
- 文件：`file:*`
- 工作区：`workspace:*`
- 后台任务：`tasks:*`
- 导出：`export:*`

### Services
`electron/services/*.cjs` 承载 Main 侧业务：
- `configStore.cjs`：配置读写，路径位于 Electron `userData`，包含文本模型、生图模型和文件解析配置。
- `aiService.cjs`：文本模型请求、生图模型测试、正文配图生图等 AI 能力封装。
- `fileService.cjs`：文档导入与解析，按配置选择本地或 MinerU 解析方式；新的文件上传/转换入口必须复用这里的 `parseDocumentWithConfig()`。
- `workspaceStore.cjs`：工作区业务数据缓存，例如技术方案流程状态和招标文件内容。
- `taskService.cjs`：通用后台长任务管理，负责跨功能耗时任务的启动、去重、任务组锁、active task 管理、事件推送和工作区快照落盘；不是技术方案专用服务。
- `duplicateCheckService.cjs`：标书查重业务 runner，负责元数据提取、目录查重、正文查重和图片查重；外层任务生命周期和任务组锁由 `taskService.cjs` 管理。
- `exportService.cjs`：Word 导出，包含 Markdown 到 Word、图片插入、Mermaid 转图片和导出进度上报。
- `updateService.cjs`：打包应用启动后的 GitHub Release 更新检查、下载和重启安装提示。

Main 层规则：
- 文件读写显式使用 UTF-8。
- Windows 路径和中文路径要作为默认情况考虑。
- 不在 Renderer 暴露 `fs`、`path`、`ipcRenderer` 原始对象。
- Renderer 不决定文件解析方式；文件解析方式由 Main 从 `configStore` 读取配置后选择。
- 新增任何文件上传、导入、转换功能时，不要直接调用 `doc2markdown/convert.mjs` 或自行拼接 MinerU 请求；统一在 Main 侧调用 `electron/services/fileService.cjs` 的 `parseDocumentWithConfig(app, filePath, config, { assetScope })`。
- `parseDocumentWithConfig()` 会统一处理本地解析、MinerU Agent、MinerU Accurate、MinerU 不支持格式自动切回本地解析、设置页“保留图片”和图片资产落盘。业务代码只负责选择文件、保存业务状态和传入稳定的 `assetScope`。
- `assetScope` 必须能对应到业务资源，例如技术方案使用 `technical-plan`，知识库文档使用 `knowledge-${documentId}`。后续新功能应使用 `<feature>-<resourceId>` 这类可定位前缀，便于删除时清理历史图片。

## 数据存储
- 配置数据保存到 Electron `userData/user_config.json`，由 `configStore.cjs` 管理。
- 工作区业务数据保存到 Electron `userData/workspace/`，由 `workspaceStore.cjs` 管理。
- 每个功能的权威工作区状态保存到 `userData/workspace/<feature>.json`；运行中的任务必须由 Main 侧持续写入该文件。
- 技术方案缓存保存到 `userData/workspace/technical_plan.json`，包含当前步骤、上传文件名、招标文件内容和后续流程中间结果。
- 正文生成图片保存到 `userData/workspace/generated-images/`，正文中通过 `yibiao-asset://generated-images/...` 引用，不把图片 base64 长期写入 `technical_plan.json`。
- 文件解析保留的原文图片保存到 `userData/workspace/imported-images/<assetScope>-<timestamp>-<random>/`，正文中通过 `yibiao-asset://imported-images/...` 引用，不把图片 base64 长期写入业务 JSON。
- 删除或重置某个业务资源时，必须调用 `electron/utils/importedImages.cjs` 的 `deleteImportedImageBatches(app, assetScopePrefix)` 清理对应历史图片目录，避免用户长期使用后磁盘被遗留图片占满。
- 解析失败时 `parseDocumentWithConfig()` 会清理本次新建图片批次；业务删除、清空、重新导入或删除父级目录时仍必须按 `assetScopePrefix` 主动清理旧批次。
- 设置页通用配置中的“开发者模式”开启后，AI 请求日志保存到 Electron `userData/logs/ai/`；每次 AI 请求对应一个 JSON 日志文件，包含完整请求参数、响应内容或错误信息。
- Renderer 通过 `window.yibiao.workspace` 读写工作区数据，不直接写文件，也不把招标文件 Markdown、正文草稿等大文本长期放进 `localStorage`。
- Renderer 在任务 running 时不得用卸载前的旧状态覆盖工作区文件；应像技术方案一样跳过 running 状态下的自动保存。
- `localStorage` 只用于轻量 UI 偏好或临时状态，不作为业务数据的权威来源。

## 后台长任务
- 后台长任务架构适用于所有功能，不限于技术方案。
- 凡是耗时 AI、文件解析、批量检查、内容生成、导出转换、可显示进度或用户可能切页等待的任务，都必须走 Electron Main 后台任务。
- Renderer 只负责启动任务、订阅 `window.yibiao.tasks.onTaskEvent()`、读取 workspace 缓存并展示状态。
- Main 任务启动后必须立即写入 running 状态，并在每个关键节点持续写入 `workspaceStore`。
- 后台任务运行过程中必须持续写入 `workspaceStore`，确保切换页面后任务不中断，切回来能恢复当前日志、进度和结果。
- 页面卸载不能取消任务，也不能作为任务失败信号；如需取消任务，应单独设计取消接口。
- 页面重新挂载后必须先读取 workspace，再订阅任务事件，并通过 `getActiveTasks()` 触发 active task 状态回放。
- 如果缓存中存在 running 任务但 Main 没有对应 active task，应标记为“上次任务未完成，请重新执行”，不能静默恢复成 idle。
- `taskService.cjs` 只管理外层任务生命周期和锁；任务内部可以并发执行子流程，但子流程不得绕过同一 workspace 的外层任务锁单独启动或写入同一业务状态。
- 标书查重只通过 `window.yibiao.tasks.startDuplicateAnalysis()` 和 `tasks:event` 启动/订阅；不要再新增或恢复旧的 `duplicate-check:start-metadata-analysis`、`duplicate-check:event` 兼容通道。

## 任务组、锁策略与清空规则
- 所有后台长任务都必须声明任务组、任务类型、步骤序号、锁策略和结果字段；`taskService.activeTasks` 是判断运行中任务的权威来源，不能只依赖 workspace 中缓存的 task 字段。
- 任务组用于表达业务流程边界，当前约定为 `technical-plan`、`knowledge-base`、`duplicate-check`、`rejection-check`。
- 锁策略使用 `group-exclusive`、`scope-exclusive`、`none` 三类；新增任务时必须明确选择一种，不允许隐式默认。
- `group-exclusive` 表示同一任务组内任意时刻只能运行一个任务；同组其他任务启动时应直接拒绝并提示当前任务正在执行，不得先清空 workspace。
- `scope-exclusive` 表示同一任务组内按 `scopeId` 加锁；同一 `scopeId` 只能运行一个任务，不同 `scopeId` 可以并发。
- `none` 表示只记录任务组，不做互斥锁；仅用于确实没有共享状态写入冲突的任务。
- 同一任务类型、同一锁范围内已有 running 任务时，可以返回已有任务状态；同组不同任务冲突时应拒绝启动，而不是复用无关任务。
- 任务启动顺序必须是先检查锁冲突，再创建 active task 并写入 running 状态，之后由 Main 侧按规则清空当前步骤和后续步骤缓存；Renderer 不得在后台任务确认启动前清空持久化业务数据。
- 清空规则必须在 Main 侧执行并落盘，Renderer 只能根据 task event 展示清空后的状态，避免启动失败、页面卸载或自动保存导致数据丢失。
- 重新生成某一步时，只能清空该步骤的旧结果和后续步骤结果，不能清空前置步骤输入；单条失败重试只清空该条任务结果，除非该条结果是后续步骤的权威输入且业务明确要求级联清空。

任务组约定：

| 任务组 | 锁策略 | 典型任务 | 说明 |
| --- | --- | --- | --- |
| `technical-plan` | `group-exclusive` | `bid-analysis`、`outline-generation`、`content-generation` | 技术方案 Step02/Step03/Step04 共用 `technical_plan.json`，任一任务运行时禁止启动组内其他任务。 |
| `rejection-check` | `group-exclusive` | `rejection-items-extraction`、`rejection-check-run` | 废标项解析和检查共用同一检查缓存，避免解析口径和检查结果交叉写入。 |
| `duplicate-check` | `group-exclusive` | `duplicate-analysis` | 标书查重对外是一个后台任务，内部包含元数据提取、目录查重、正文查重和图片查重等可并发子流程；外层按任务组锁定，避免同一工作区被多轮分析交叉写入。 |
| `knowledge-base` | `scope-exclusive` | 文档上传解析、段落匹配 | 知识库后续迁入任务组时不锁整个组；按 `documentId` 加锁，同一文档不能并发处理，不同文档可以并发上传或匹配。 |

当前 `knowledge-base` 仍由 `knowledgeBaseService.cjs` 独立管理，暂不接入 `taskService.cjs`；后续迁入时按 `scope-exclusive(documentId)` 执行。

技术方案步骤清空规则：

| 触发动作 | 必须清空 | 必须保留 |
| --- | --- | --- |
| Step01 重新导入招标文件 | Step02 解析结果、Step03 目录结果、Step04 正文结果、扩写相关缓存 | 新导入的文件名和招标文件 Markdown |
| Step02 全量重新解析 | 当前模式下的 Step02 解析结果、`projectOverview`、`techRequirements`、Step03 目录结果、Step04 正文结果 | Step01 招标文件 Markdown、未参与当前模式的可用解析缓存可按业务需要保留 |
| Step02 单条失败重试 | 当前失败条目的 `status`、`content`、`error` | 其他解析条目、Step03/Step04 已有结果，除非重试项是后续权威输入且需要显式级联 |
| Step03 重新生成、编辑、添加或删除目录 | Step04 正文生成任务、正文生成章节状态、正文编排计划、旧 `outlineData.outline[*].content` | Step01 招标文件、Step02 解析结果、当前目录结构中的非正文元数据 |
| Step04 全量重新生成正文 | Step04 正文生成任务、正文生成章节状态、正文编排计划、旧 `outlineData.outline[*].content` | Step01 招标文件、Step02 解析结果、Step03 目录结构 |
| Step04 单章节重新生成正文 | 该章节正文、该章节生成状态和该章节编排计划 | 其他章节正文、目录结构、前置步骤结果 |

其他功能清空规则：

| 功能 | 触发动作 | 清空规则 |
| --- | --- | --- |
| 废标项检查 | 重新解析无效投标与废标项 | 清空旧解析结果和后续检查结果，保留已上传招标文件和投标文件。 |
| 废标项检查 | 重新执行检查 | 清空旧检查结果，保留无效投标与废标项解析结果和已上传文件。 |
| 标书查重 | 重新分析 | 由 Main 侧 `duplicate-analysis` 启动时清空旧元数据分析、目录查重、正文查重、图片查重结果和 `analysisTask`，保留本轮选择的招标文件和投标文件列表。 |
| 标书查重 | 替换或删除文件 | 清空旧元数据分析、目录查重、正文查重、图片查重结果和 `analysisTask`，保留变更后的招标文件和投标文件列表。 |
| 知识库 | 上传或重新处理某个文档 | 只更新该 `documentId` 的解析、切块、匹配状态和结果，不影响其他文档。 |

## 技术方案流程约定
- Step01 只负责上传招标文件并展示解析出的 Markdown，不做 AI 理解或目录生成。
- Step02/Step03/Step04 的长任务统一走 Electron Main 后台任务，不在 Renderer 中承载完整生成流程。
- Step04 正文生成页面采用“左侧目录树/任务树 + 右侧内容阅读区”的布局；目录树应能体现每个小节的生成状态。
- 正文只生成叶子小节内容，父级目录只作为结构分组。
- 正文内容以 `outlineData.outline[*].content` 为展示和导出的权威来源，不要另建一套正文存储源。
- Step04 正文生成先由 AI 输出章节编排决策，再生成正文；是否使用表格或配图由 AI 结合章节内容自行判断，代码不要按关键词硬编码配图规则。
- Step04 启用最低字数时，正文生成流程为：整体编排、先生成最多 3 个样本小节、用样本平均字数预估全文；若预估低于最低字数的 80%，先执行最多 3 轮补目录，每轮补完重新预估，达到 80% 即停止补目录；随后为新增叶子补编排、生成全部待生成正文，最后只通过正文扩写兜底到最低字数。
- Step04 最低字数补目录只允许发生在正文批量生成前；最终最低字数兜底阶段只扩写正文，不再补目录，避免全文生成后大量叶子变为父级目录导致正文浪费。
- Step04 “正文生成并发速度”控制同时发起的正文编排、正文生成和字数扩写请求数，不影响配图；具体可用上限取决于用户配置的 API 接口限制，设置过高可能触发 429。
- Step04 最低字数扩写使用工作池并发，最大在飞请求数来自“正文生成并发速度”；达到最低字数或收到暂停请求后只停止调度新的扩写请求，不中断已经发出的 AI 请求。
- Step04 扩写工作池的调度诊断日志只在开发者模式下输出，普通模式不要增加高频任务日志。
- 正文配图只有在设置页生图模型状态为 `available` 时执行；生图失败不应影响已生成正文落盘。
- Mermaid 图以 Markdown `mermaid` 代码块保存；Renderer 负责前端本地渲染预览，Word 导出时由 Main 通过 mermaid.ink 转图片，并通过 `window.yibiao.export.onWordExportProgress()` 展示进度和失败提示。
- 目录重新生成、编辑、添加或删除后，必须清空正文生成缓存和旧 `content`，避免旧正文污染新目录和导出结果。
- Step04 的工具条使用 `导出 Word` 和 `继续扩写`，不要再显示“下一步”。

## AI 与 Prompt
AI 调用分三层：
- 页面或 Hook 调 feature service。
- Feature service 调 `shared/prompts` 生成 messages，再调 `shared/ai/aiClient`。
- `aiClient` 通过 `window.yibiao.ai` 调 Electron Main。

Prompt 文件约定：
- `analysisPrompts.ts`：招标文件分析。
- `outlinePrompts.ts`：目录生成、子目录生成、目录审核。
- `contentPrompts.ts`：章节正文生成。
- `expandPrompts.ts`：旧方案目录提取。
- `duplicatePrompts.ts`：标书查重。
- `rejectionPrompts.ts`：废标项检查。
- `jsonRepairPrompts.ts`：JSON 修复。

当前除 `jsonRepairPrompts.ts` 外，多数 Prompt builder 是占位实现，后续迁移时参考旧 `backend/app/utils/prompts/` 的职责，但不要直接依赖旧文件。
- AI 生成正文属于不可信模型输出，Markdown 渲染默认不要启用 `rehypeRaw`，避免 HTML 被渲染成真实 DOM。
- 只有明确需要展示解析文档中 HTML 的场景，才允许局部启用 raw HTML，并在代码评审中说明原因。

### AI 请求落盘与修复
- AI 文本请求统一使用非流式接口：纯文本用 `aiService.chat()`，JSON 用 `aiService.collectJsonResponse()` / `window.yibiao.ai.requestJson()`。
- 后台任务不得按 token、chunk 或部分内容落盘；只在阶段开始、单次 AI 请求完整返回、失败、暂停或任务结束时写入 workspace。
- JSON 主请求解析失败时继续复用现有 JSON 修复链路；修复请求只提交模型已返回的内容片段、校验问题和目标格式，不重复提交完整大文本上下文。
- Renderer 不展示 token 级增量结果；页面只展示任务阶段、进度、日志和完整返回后的最终内容。

## UI 风格
- 配色使用易标官网/工作台体系：`#2174FD`、`#4098FC`、`#5B54D3`、`#f8fafd`。
- 保持高端、清爽、克制，不使用花哨动效。
- 组件优先清晰的信息层级和可读性。
- 前端基础组件优先使用 Radix UI 组件实现，若未引用可自行安装，再用全局 CSS 覆盖视觉风格。
- 新页面不要默认加占空间的大标题横幅；除非业务确实需要说明上下文，否则直接进入核心内容或使用轻量导航/状态栏。
- 新增页面优先延续现有卡片、胶囊 Tab、轻量状态栏、`FloatingToolbar` 等模式，不要另起一套视觉语言。
- 新增或调整页面时，先查找并复用现有布局、按钮、卡片、表单、状态栏和工具条等视觉模式；页面专用样式只补充必要结构和间距，不要为局部页面重写一整套颜色、圆角、阴影和字号体系。
- 如果某个 UI 模式后续可能跨页面复用，优先沉淀到全局样式或 `shared/ui`，避免每个 feature 各写一套相似实现。
- 技术方案流程页优先沿用 `command-bar + progress-card + workspace` 结构；左侧列表/树、右侧阅读器的交互模式保持一致。
- 页面不要为了底部工具条额外预留空白；`FloatingToolbar` 是覆盖层，必要内容应由内部滚动区域承载。
- 通用 UI 放 `shared/ui/`，业务专用 UI 放 feature 内。
- 全局布局组件放 `components/`。

## 全局消息提示
- 所有成功、失败、警告、普通消息提示统一使用 `shared/ui/ToastProvider` 提供的 Toast。
- Toast 位置固定在窗口右上角。
- 不使用 `alert`，不在页面里自建长期占位提示条。
- 页面内只保留与业务状态强相关的空状态、加载态和结果态。

## 埋点统计
- Renderer 埋点入口统一使用 `src/shared/analytics/analytics.ts`，不要在业务组件里直接拼接 `fetch` 请求。
- 禁止以清理、重构、简化为由删除或绕过任何现有埋点、统计字段、页面访问映射、模型使用统计和看板展示；如功能重构涉及埋点，必须同步迁移并保持统计口径可用。
- 应用启动的 `app_open` 和一级页面切换的 `page_view` 已在 `src/App.tsx` 统一上报；新增主菜单页面时，只要同步新增 `SectionId`、`menuConfig.ts` 和 `AppRouter.tsx`，一级页面访问会自动统计。
- 新增主菜单页面后，必须同步更新 `analytics/dashboard/public/src/pages/traffic.js` 中的页面路由中文名映射，保证访问分析里能看懂该路由对应的功能。
- 如果页面内部还有重要步骤、子页、Tab 或流程阶段，需要在对应页面组件中调用 `trackPageView('<feature>/<step>')` 单独上报，例如技术方案使用 `technical-plan/document-analysis`、`technical-plan/bid-analysis` 等子路由。
- `page_view` 的路由值应稳定、可读、低基数，统一使用小写英文和短横线；不要包含用户输入、文件名、本地路径、文档 ID、任务 ID、随机 ID 或时间戳。
- 配置使用统计统一通过 `trackConfigUsage()` 上报，只采集枚举、开关、模型状态这类非敏感配置；新增配置项如需统计，需要同步扩展 `ConfigUsagePayload`、Worker 的配置聚合字段和 Dashboard 展示文案。
- AI 模型请求统计由 Main 侧 `electron/services/aiService.cjs` 统一上报，用于看板里的文本模型请求和生图模型请求；新增 AI 请求入口优先复用现有 AI 服务链路，不要绕开后遗漏模型使用统计。
- 禁止埋点上传 `api_key`、`base_url`、`mineru_token`、Prompt、AI 响应内容、错误详情、文件名、本地路径、招标文件内容、正文草稿等敏感或大文本数据。
- 埋点失败必须静默处理，不影响用户主流程；不要因为统计请求失败弹 Toast、阻塞页面或中断任务。
- 新增功能自查清单：一级页面是否自动有 `page_view`；内部关键步骤是否需要额外 `trackPageView`；访问分析中文映射是否已补；新增配置或模型链路是否已纳入统计；是否确认没有上传敏感数据。

## 悬浮工具条
通用组件位于 `src/shared/ui/FloatingToolbar.tsx`。

使用方式：
```tsx
<FloatingToolbar groups={groups} />
```

- 不提供 App 级默认工具条；需要流程控制或保存入口的页面自行渲染 `FloatingToolbar`，例如技术方案步骤工具条、设置页保存工具条、标书查重处理工具条。
- 页面主体不要重复放与工具条同语义的按钮，避免同一动作出现多个入口。
- 工具条前置拖动手柄，用户只能通过手柄拖动工具条；不要让普通按钮区域承担拖动，避免点击和拖拽冲突。
- 工具条应悬浮在内容上层，不要求页面底部 `padding-bottom` 或空白占位；被遮挡的长内容通过内部滚动查看。

## 布局与滚动
- 应用外层保持 `100vh` 高度并隐藏全局溢出，禁止依赖 `body` 产生页面级滚动条。
- `content-shell` 只负责占满主内容区和承载悬浮层，不作为长内容滚动容器。
- 页面根容器需要设置 `height: 100%`、`min-height: 0`，需要滚动时在页面内部容器上使用 `overflow: auto`。
- 技术方案这类工作台页面优先使用 `command-bar + workspace` 的网格结构，`workspace` 占据剩余高度，左右面板分别内部滚动。
- 设置页使用页面根容器内部滚动，保存工具条继续悬浮覆盖在上层。

## 打包发布与自动更新
- 客户端使用 `electron-builder` 打包，配置位于 `client/package.json` 的 `build` 字段。
- GitHub Actions 工作流位于 `.github/workflows/release.yml`，推送 `v*` tag 时触发，例如 `v0.1.1`；也支持手动执行 workflow 并输入已有 tag。
- Release notes 由 workflow 根据上一个 tag 到当前 tag 的 `git log` 显式生成，避免 GitHub 原生自动说明只显示 `Full Changelog`。
- Windows 在 `windows-latest` 构建，产物包含 NSIS 安装包和 zip；macOS 在 `macos-latest` 构建，产物包含 dmg 和 zip。
- Windows 图标来自 `assets/icon.ico`，必须允许 electron-builder 编辑 exe 资源；macOS 图标由 workflow 使用 `assets/icon_256.png` 临时生成 `assets/icon.icns` 后打包。
- 当前未接入代码签名，Windows 可能出现 SmartScreen 提示，macOS 可能被 Gatekeeper 拦截；正式分发前再补签名和 macOS notarization。
- 未签名 Windows 安装包首次通过资源管理器双击启动时，可能被 Defender/SmartScreen 扫描而表现为短暂无响应；命令行启动或等待扫描完成后通常会恢复。正式分发前应通过代码签名改善该体验。
- workflow 会从 tag 中同步客户端版本号，例如 tag `v0.1.1` 会临时执行 `npm version 0.1.1 --no-git-tag-version --allow-same-version` 后再打包。
- workflow 使用 `electron-builder --publish never` 只负责构建，再通过 `gh release upload` 显式上传安装包、zip/dmg、blockmap 和 `latest*.yml` 更新元数据。
- 自动更新由 `electron-updater` 负责，只在 `app.isPackaged` 的打包应用中启用；`npm run dev` 开发调试模式不会检查或下载更新。
- 打包应用启动后会自动检查一次更新，之后每 30 分钟轮询一次；自动检查失败保持静默，不打断用户。
- 若 GitHub Release 存在更高版本，应用会先在后台下载更新包；下载完成后通过右上角常驻 Toast 提示“安装并重启 / 稍后”。
- 用户点击“安装并重启”后才会安装更新；不会自动安装，也不会在应用退出时自动安装。
- 用户点击“稍后”或关闭自动更新 Toast 后，同一版本在本次运行期间不会再次自动提醒；设置页“关于”中的“检查更新”仍可手动触发检查和提示。
- 本地 Windows 打包验证可运行：
```powershell
cd client
npm run dist:win
```
- 本地 macOS 打包需在 macOS 环境运行：
```powershell
cd client
npm run dist:mac
```
- 发布新版本的基本流程：
```powershell
git tag v0.1.1
git push origin v0.1.1
```
- 如果某个 tag 的 Release 已经创建但缺少产物，可以在 GitHub Actions 页面手动运行 `Release Client`，输入该 tag 重新生成说明并上传产物。

## 开发约定
- 优先小步提交，不做无关重构。
- 不新增全局状态库，除非实际复杂度需要。
- 不在组件内硬编码大段 Prompt。
- 不在 Renderer 存储 API Key。
- 不直接复用旧 `frontend/` 或 `backend/` 的源码路径。
- 新增页面、流程步骤、配置项或 AI 请求入口时，必须按“埋点统计”章节完成埋点和看板映射自查。
- 不为新上传功能另写一套文档解析链路；复用 `parseDocumentWithConfig()`，并在删除对应业务资源时复用 `deleteImportedImageBatches()` 清理导入图片资产。
- 命名保持清晰：页面 `*Page.tsx`，服务 `*Service.ts` 或 `*Workflow.ts`，Hook 以 `use` 开头。

## 验证标准
每次改动至少运行：
```powershell
npm run build
```

涉及依赖变更时额外运行：
```powershell
npm audit
```

涉及 Electron Main 或 preload 时，需要手动运行：
```powershell
npm run dev
```

检查窗口是否正常打开、控制台是否有错误、`window.yibiao` 是否存在。
