---
name: 易标投标工具箱开发说明
about: 本文档用于让开发人员或 AI 快速理解架构边界，并在协同开发过程中统一函数封装、编码风格、组件复用、界面样式和验证标准等。禁止记录功能实现流水账。
---

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

## 目标

本文面向参与 `client/` 独立客户端开发的成员。阅读后应能快速判断：代码放在哪里、Renderer 和 Electron Main 如何协作、哪些组件和样式应复用、改动后如何验证，以及需要查看哪些仓库级关联目录。

## 技术栈与命令

- 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
```

项目包含 Electron Main 侧 native 依赖，例如 `better-sqlite3`。`npm install` / `npm ci` 后会通过 `postinstall` 执行 `electron-builder install-app-deps`，把 native 模块重建到当前 Electron 版本。若出现 `NODE_MODULE_VERSION` 不匹配或 IPC 大面积未注册，先运行：

```powershell
cd client
npm run postinstall
```

## 架构边界

- `client/` 是独立桌面客户端，不依赖旧 `frontend/` 和 `backend/` 源码路径。
- 本项目是安装在客户本地计算机上的一体化客户端，Renderer 和 Electron Main 不是传统前后端网络边界；两侧传递的业务参数默认视为客户端内部可信数据，不要因为 IPC 往返而做多余、重复的校验或重复计算。
- Renderer 不直接访问 Node、`fs`、`path`、`ipcRenderer`，只通过 `window.yibiao` 调用 preload 暴露的安全 API。
- Electron Main 负责本地能力：配置、文件解析、AI 请求、SQLite Store、后台任务、导出、自动更新。
- IPC 文件只注册通道和转发参数；业务逻辑放 `electron/services/*.cjs`。
- Renderer 页面只做 UI 和交互编排，不承载耗时任务生命周期，也不保存业务权威状态。
- Prompt builder 只负责组装消息，不直接请求 AI；不要在组件内硬编码大段 Prompt。
- 所有用户可见文案使用中文。

## 目录职责

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

关联目录入口：

- `analytics/`：独立 Cloudflare Workers 埋点服务和统计看板。客户端只通过共享埋点入口上报；修改埋点字段、页面映射、公告读取或统计展示时才需要查看。
- `sql/workspace_schema.sql`：`workspace/yibiao.sqlite` 目标完整表结构说明。运行时不会执行该文件；实际建表和升级以 `client/electron/services/sqliteDatabase.cjs` 的 migration 为准。

新增主菜单页面时，通常同步修改：

- `src/shared/types/navigation.ts`
- `src/app/menuConfig.ts`
- `src/app/AppRouter.tsx`
- 如需页面级工具条，再按页面状态渲染 `FloatingToolbar`

如一级菜单需要承载多个子功能，在 `menuConfig.ts` 为该一级 `AppMenuItem` 配置 `children`，点击一级菜单后由 `shared/ui/SecondaryMenuPage.tsx` 在右侧主内容区渲染二级菜单页；不要在侧边栏做展开式或悬浮式二级菜单。二级入口仍需补充 `SectionId`、具体页面路由和埋点看板中文映射。

## Feature 与 Shared

业务功能放在 `src/features/<feature-name>/`：

```text
features/<feature-name>/
├── pages/        # 页面级组件
├── components/   # 本功能专用组件
├── hooks/        # 本功能专用 Hook
├── services/     # Renderer 侧轻量编排或兼容工具；长任务不要放这里
└── types.ts      # 本功能专用类型
```

当前功能模块：

- `technical-plan`：技术方案。
- `knowledge-base`：知识库。
- `duplicate-check`：标书查重。
- `rejection-check`：废标项检查。
- `settings`：设置页，管理文本模型、生图模型、组件设置（文件解析与本地转图并发）、智能体运行时和关于信息。

技术方案流程页面复用约定：

- `src/features/technical-plan/pages/TechnicalPlanHome.tsx`、`DocumentAnalysisPage.tsx`、`BidAnalysisPage.tsx`、`OutlineEditPage.tsx`、`GlobalFactsPage.tsx`、`ContentEditPage.tsx` 是“生成技术方案”和“已有方案扩写”共用的流程页面；后续修改这些页面时必须同时考虑两个入口的展示、步骤 gating、任务状态、错误提示、导出和重置行为。
- “已有方案扩写”和“生成技术方案”的主步骤及页面骨架保持一致，差异主要由工作流类型、Step01 是否额外上传“已有方案”、Main 侧后台任务和 Prompt 决定。Renderer 侧不要为了其中一个功能复制整套页面，也不要把某个入口的业务假设硬编码进通用页面。
- 如需调整上述共用页面的 props、状态字段、IPC 调用或任务事件处理，必须确认另一个入口仍能正常启动、恢复、切换步骤、生成目录、设定全局事实、生成正文和导出 Word；不能为了修复一个入口导致另一个入口回归。

共享能力放在 `src/shared/`：

- `shared/ai/aiClient.ts`：Renderer 侧 AI 请求门面，仅用于仍在 Renderer 侧的轻量请求。
- `shared/prompts/`：仍由 Renderer 使用的 Prompt builder。已迁入 Main 后台服务的流程不要保留 Renderer 直连 AI 占位 Prompt。
- `shared/types/`：跨功能类型，包含 `ipc.ts` 中的 `window.yibiao` bridge 类型。
- `shared/ui/`：跨功能 UI，例如 `FloatingToolbar`、`SecondaryMenuPage`、`DetailHelpLink`、`MarkdownRenderer`、Toast。
- `shared/utils/`：跨功能纯工具，例如 ID、字数统计。

共享层规则：

- `shared/` 不引用任何 feature 模块，避免循环依赖。
- 类型优先使用 `interface`；联合类型和泛型工具类型使用 `type`。
- 新增 bridge API 时，同步 `electron/ipc/*`、`electron/preload.cjs` 和 `src/shared/types/ipc.ts`。

## Electron Main、IPC 与 Store

- `electron/` 使用 CommonJS，保持 `.cjs`。
- `electron/ipc/*.cjs` 只注册/转发 IPC，不写业务逻辑。
- `electron/services/*.cjs` 承载 Main 侧业务逻辑。
- Renderer bridge 以 `src/shared/types/ipc.ts` 为类型权威；不要在多个文件里各自手写不一致的返回类型。
- Main 侧文件读写显式使用 UTF-8，并把 Windows 中文路径作为默认场景处理。

核心服务分工：

- `configStore.cjs`：配置读写，保存到 Electron `userData/user_config.json`。
- `aiService.cjs`：文本模型、生图模型、JSON 修复和模型请求统计。
- `fileService.cjs`：文档导入与解析，统一按配置选择本地或 MinerU 解析方式。
- `sqliteDatabase.cjs`：工作区 SQLite 初始化和 runtime migration。
- `technicalPlanStore.cjs`、`duplicateCheckStore.cjs`、`rejectionCheckStore.cjs`、`knowledgeBaseStore.cjs`：各功能 SQLite Store。
- `taskService.cjs`：通用后台长任务管理、任务组锁、active task、事件推送和状态落盘。
- `agentService.cjs`：智能体运行时协调器，按注册表创建 OpenCode/Pi runtime，维护全局串行队列，并统一返回输出文件、assistant 文本、diff、session 和 runtime 状态信息。
- `exportService.cjs`：Word 导出、Markdown 转 Word、图片和 Mermaid 导出。

新增文件上传、导入、转换功能时，优先复用 `fileService.cjs` 的 `parseDocumentWithConfig(app, filePath, config, { assetScope })`。删除业务资源时，同步调用 `electron/utils/importedImages.cjs` 的 `deleteImportedImageBatches(app, assetScopePrefix)` 清理导入图片资产。

Main 侧通用文本编辑：

- `electron/utils/textEdit.cjs` 是纯字符串 edit 引擎，不读写文件、不调用 AI、不访问 Store；业务服务传入完整文本和 edit 指令，拿返回的 `content/changed/edits/errors` 后自行决定是否保存。
- AI 返回局部替换时优先用 `applyTextEdit()` / `applyTextEdits()`，参数为 `oldText/newText`，默认必须唯一命中；多处命中时让 AI 补充更多上下文，不要静默替换第一处。
- 程序已经明确捕获范围时优先用 `applyRangeEdit()` / `applyRangeEdits()`，例如表格块、代码块、段落块替换；批量 range 会先校验不重叠，再倒序应用。
- 只搜索不修改时用 `findTextMatches()`；匹配策略按 exact、line-trimmed、whitespace-normalized、trimmed-boundary、block-anchor 逐级尝试，整体保持保守，避免误改正文中的参数、工期和承诺。

## 数据存储

- 配置保存到 Electron `userData/user_config.json`。
- 业务工作区保存到 Electron `userData/workspace/`。
- 结构化业务状态统一进入 `userData/workspace/yibiao.sqlite` 或功能专用 Store。
- SQLite 表结构调整时，同步更新 runtime migration 和仓库根目录 `sql/workspace_schema.sql`，后者只用于阅读、评审和排查问题。
- 大文本 Markdown、原始上传文件和图片资产保存为文件，SQLite 只保存路径、hash、计数和结构化状态。
- Renderer 只允许使用 `localStorage` 保存轻量 UI 偏好；大文本、草稿、API Key、流程状态都走 Main 侧 Store/IPC。
- 技术方案正文展示和 Word 导出以 `outlineData.outline[*].content` 为权威来源。
- 知识库旧数据迁移由进入知识库页面后触发，使用项目内 Radix Dialog 提示，不用系统弹窗；只迁移旧版 `status = success` 的文档，未完成或处理中的旧文档不会迁移到新版本知识库。具体迁移方案见 `client/doc/sqlite改造方案_知识库.md`。

技术方案流程状态约定：

- 主流程 Step 只表示页面级阶段，例如 `document-analysis`、`bid-analysis`、`outline-generation`、`global-facts`、`content-edit`。
- Step 内部如果存在可中断子流程，不新增主 Step；应在 Store 中落盘为该 Step 的子状态，页面加载后按子状态恢复 UI。
- 多标段识别属于 Step02 `bid-analysis` 子流程：用户选择多标段后由 Main 后台任务 AI 提取标段范围，结果写入 `bid_sections_json`，选择后生成当前投标范围工作副本 `tender.md`；Step01 只负责导入完整原文 `tender-original.md`。
- Step05 正文生成属于 `content-edit` 的后台任务子流程，运行态、章节状态、正文规划结果、全文图片计划和 runtime JSON 由 Main 持续写入 Store，用于异常关闭后的暂停恢复。
- Step03 是因字数目标调整目录结构和叶子节点数量的唯一阶段；Step05 只能更新目录节点正文，不得新增、删除、拆分、合并或重新编号目录。
- Step03 当前字数设置与成功生成目录时的生效快照分别保存。修改当前设置只有重新生成目录后才影响正文任务；Step05 及其暂停恢复只读取目录生效快照。
- 小节或全文经过限定轮次仍未达到字数目标时保留有效正文并记录警告，不得仅因字数不达标把小节或任务标记为失败。
- 最终小节复核和全文字数调整位于原方案覆盖修复、一致性修复及表格清理之后，全文图片编排之前。
- 正文编排只决定写作重点、知识库引用、事实变量和表格，不再逐节决定或生成图片。正文生成、扩写、审计和表格清理全部完成后，Main 使用当前智能体运行时阅读 `technical-plan.md`、`outline-tree.json`、`illustration-config.json`，严格校验 `illustration-plan.json` 后一次性写入 `contentIllustrationPlan`。
- 全文图片计划保存 `kind/image_type/title/section_ids/placement/priority`，其中 `title` 是编排 Agent 基于全文统一设计的最终图注，最终计划内必须唯一。程序按 HTML、AI、Mermaid 的顺序处理类型上限和小节冲突，再进入图片生成阶段；三类生成提示词都直接注入计划标题，不得在生成阶段另行生成或拼接标题。
- 全文图片编排 Agent 失败或暂停时不保存半成品；从 `illustration-planning` 恢复时跳过已完成的正文和审计阶段，重新执行整轮图片编排。
- 新增 Step 内子状态时，要同时定义异常关闭后的恢复、确认完成后的清空、取消后的清理和重置工作区时的清理规则。

## 后台任务

- 所有耗时 AI、文件解析、批处理、生成、检查、导出转换都必须放在 Electron Main 后台任务中执行。
- Renderer 只负责启动任务、订阅事件、读取 Store 快照和展示状态。
- Main 任务启动后必须立即写入 running 状态，并在每个关键阶段持续写入对应 Store。
- 页面卸载不能取消任务，也不能作为任务失败信号。
- 页面重新挂载后必须先读取 Store，再订阅任务事件，并通过 `getActiveTasks()` 触发 active task 状态回放。
- 如果 Store 中存在 running/pausing 等运行态，但 Main 没有对应 active task，应标记为“上次任务未完成，请重新执行”或进入业务约定的可恢复状态，不能静默恢复为 idle。

任务组约定：

| 任务组 | 锁策略 | 说明 |
| --- | --- | --- |
| `technical-plan` | `group-exclusive` | Step02/Step03/Step04 共用技术方案工作区，组内互斥。 |
| `duplicate-check` | `group-exclusive` | 标书查重外层是一轮分析任务，内部子流程可以并发。 |
| `rejection-check` | `group-exclusive` | 废标项解析和检查共用同一检查缓存，组内互斥。 |
| `knowledge-base` | `scope-exclusive` | 后续接入统一任务服务时按 `documentId` 加锁；当前仍由 `knowledgeBaseService.cjs` 独立管理文档级互斥。 |

清空规则原则：

- 重新生成某一步时，只清空该步骤旧结果和后续步骤结果，不清空前置输入。
- 清空动作应在 Main 侧任务确认启动后执行并落盘，Renderer 不要在 IPC 成功前清空持久化业务数据。
- 目录重新生成、编辑、添加或删除后，必须清空旧正文内容、正文生成缓存和全文图片计划；手工编辑正文或修改正文生成配置后也必须清空全文图片计划。

## UI 与组件复用

- UI 使用全局 CSS + Radix 基础组件，不使用 Tailwind。
- 新页面优先复用现有卡片、胶囊 Tab、状态栏、工作区、列表/树 + 阅读器、`FloatingToolbar` 等模式，不另起一套视觉语言。
- 页面专用样式只补充必要结构和间距，不重写整套颜色、圆角、阴影和字号体系。
- 页面根容器保持 `height: 100%` / `min-height: 0`；长内容在页面内部滚动，禁止依赖 `body` 全局滚动。
- `FloatingToolbar` 是覆盖层，不要为它额外预留大段空白；必要内容通过内部滚动查看。
- 用户确认、长文案提示、危险操作优先使用项目内 Radix Dialog 和统一按钮样式；新增或重构时不要使用 `alert`，也尽量不要使用系统 `window.confirm` / `window.prompt`。
- 成功、失败、警告、普通消息统一使用 `shared/ui/ToastProvider`，不要自建全局提示。

Markdown 约定：

- Markdown 展示统一使用 `shared/ui/MarkdownRenderer.tsx`。
- Markdown 编辑统一使用 `shared/ui/MarkdownEditor.tsx`。
- 展示远程公告、AI 输出或其他不可信内容时，必须传 `allowRawHtml={false}`。
- 只有明确展示本地解析出的可信 HTML 文档结构时，才允许开启 raw HTML。
- Mermaid 图以 Markdown `mermaid` 代码块保存；Renderer 本地渲染预览，Word 导出由 Main 本地转图片（不依赖外网）。

## AI、Prompt 与日志

- Prompt 放 `src/shared/prompts/` 或 Main 侧对应服务中，避免在组件内硬编码大段 Prompt。
- 已迁入 Electron Main 后台服务的流程，不保留 Renderer 直连 AI 占位 service 或 Prompt。
- 全局 AI 并发队列实现在 `electron/utils/aiRequestQueue.cjs`，由 `electron/services/aiService.cjs` 统一创建和使用；业务代码不要绕过 `aiService` 直接发起 AI HTTP 请求。
- 文本模型和生图模型使用独立队列：文本请求队列读取文本模型 `concurrency_limit`，默认 `10`；生图请求队列读取生图模型 `image_model.concurrency_limit`，默认 `2`。
- `aiService.chat()`、`aiService.requestJson()`、`aiService.collectJsonResponse()`、`aiService.parseJsonResponseContent()` 进入文本队列；`aiService.generateImage()` 进入生图队列。
- 队列按 FIFO 调度；超过并发上限的请求进入队列等待，拿到并发 slot 后才真正发起 HTTP 请求。
- 队列统一识别临时 AI 上游错误，包括网络异常、请求超时、`408`、`429` 和 `5xx`；单个请求最多尝试 3 次，仍失败后向业务层抛错。
- `taskService.cjs` 启动后台任务时会生成 `queueScopeId`，并通过 `aiService.withQueueScope(queueScopeId)` 注入 scoped AI 服务；新增后台任务应使用注入的 `aiService`，不要自行读取全局服务实例。
- scope 暂停是队列通用能力：已发出的请求继续执行，尚未发出的排队请求会被丢弃并抛出 `AI_QUEUE_SCOPE_PAUSED`。
- 业务层负责响应队列暂停错误，例如正文生成将 `AI_QUEUE_SCOPE_PAUSED` 转换为自身的暂停落盘流程；队列层不写具体业务状态、不修改 Store。
- 正文生成的文本并发不再单独配置，使用文本模型并发上限；正文任务末尾只生成全文图片计划，不调用 AI 生图或 Mermaid 渲染。
- Main 侧需要按文本模型上下文限制切割用户长文本时，使用 `electron/utils/userTextSplitter.cjs` 的 `splitUserTextByContextLimit(text, config)`；函数读取 `config.context_length_limit`，按 0.8 安全系数先均分段数，再优先在段落、换行、句末标点附近切分，返回文本数组。

智能体运行时调用约定：

- Main 侧运行时注册表统一管理运行时 ID、显示名称、默认项和工厂；当前注册 OpenCode Agent 与 Pi Agent，默认使用 OpenCode。
- 两套运行时不复用现有 `aiService` 文本请求出口，共用独立 Agent AI Proxy、`configStore`、`textTokenStatsStore`、工作区指导和命令工具环境。
- Renderer 或业务服务统一通过 `window.yibiao.agent.run(payload)` 调用，不直接启动具体运行时或访问运行时内部 API。
- `payload.files` 用于写入本次任务 workspace，只放本次任务需要的输入文件，例如 `tender.md`、`bid.md`、`current-result.json`。
- `payload.task` 是默认任务描述；如需要完全自定义提示词，使用 `payload.prompt`。
- `payload.output_file` 默认是 `agent-result.md`，业务侧读取结果时优先使用返回值中的 `output_content`。
- 业务层默认不要传 `task_id`，让 `agentService` 自动生成 UUID；如果确需传入，只能传内部生成的稳定 ID，不要使用用户输入、文件名、本地路径或包含 `/`、`\`、`..` 的字符串。
- 智能体只写入自身临时 workspace，不直接修改业务 Store；正式业务由对应 Main 侧适配层严格校验输出，再一次性合并到业务 Store。
- `agentService` 为 OpenCode/Pi 维护同一个全局 FIFO 队列；后台任务启动时绑定当时的运行时，运行中切换配置只影响后续任务。
- 暂停或应用重启后的恢复任务使用恢复时的当前设置；不保存旧任务运行时，不做跨运行时任务兼容，Pi 失败也不自动回退 OpenCode。
- Pi 使用内存 Session 和应用专用目录，不自动加载外部上下文、扩展、Skill、Prompt 模板或主题。
- 开发者模式下，Proxy 日志按运行时写入 `userData/logs/<runtime>-ai-proxy/*.jsonl`，Token 统计进入共享 `textTokenStatsStore`。

调用示意：

```ts
const result = await window.yibiao.agent.run({
  title: '废标项补充智能审计',
  task: '请基于 tender.md 和 current-result.json 做补充审计，输出遗漏风险。',
  output_file: 'agent-result.md',
  files: [
    { path: 'tender.md', content: tenderMarkdown },
    { path: 'current-result.json', content: JSON.stringify(currentResult, null, 2) },
  ],
  timeout_ms: 10 * 60 * 1000,
});

const markdown = result.output_content;
```

Prompt 缓存优化原则：

| 原则 | 说明 |
| --- | --- |
| 大段共享上下文放最前面 | 服务商缓存通常命中 Prompt 前缀，所有相关请求要尽量共享同样开头。 |
| 不要在共享上下文前放不同 system prompt | 不同 system prompt 会改变请求前缀，降低后续请求的缓存命中率。 |
| 先跑一个能写入缓存的请求 | 先完成共享上下文较完整的请求，后续 fan-out 并发请求才更可能命中缓存。 |
| 独立子目录生成可以并发 | 一级目录确定后，各一级目录下的二三级目录互相独立，可以并发生成。 |
| 并发仍交给全局 AI 队列控制 | 业务层不再额外限制并发数量，只控制“先预热再并发”的执行顺序。 |

- JSON 主请求解析失败时复用现有 JSON 修复链路；修复请求只提交模型已返回内容片段、校验问题和目标格式，不重复提交完整大文本上下文。
- 后台任务不得按 token、chunk 或部分内容落盘；只在阶段开始、单次 AI 请求完整返回、失败、暂停或任务结束时写入 Store。
- Renderer 不展示 token 级增量结果；页面只展示任务阶段、进度、日志和完整结果。
- 开发者模式下 AI 请求日志保存到 Electron `userData/logs/ai/`；不要把日志路径或敏感内容暴露给普通用户。
- 开发者模式下的程序调试日志统一使用 `electron/utils/developerLog.cjs` 写入 `userData/logs/<module>/*.jsonl`；关闭开发者模式时必须是 no-op，日志写入失败不能影响主流程。
- 程序调试日志只保存在本机，不自动上传服务器；默认记录阶段、计数、耗时、hash、warning 和错误信息，不主动写 API Key、Token、Base URL、Prompt、AI 响应、正文全文或完整输出路径。
- 排查文件解析、查重、废标检查、导出等模块问题时，优先使用模块级 JSONL 日志，不把详细调试过程塞进 SQLite 任务日志。

## 埋点统计

- 关联实现位于仓库根目录 `analytics/`：`analytics/worker/` 是上报与查询 API Worker，`analytics/dashboard/` 是统计看板。部署和密钥说明见 `analytics/README.md`。
- Renderer 埋点入口统一使用 `src/shared/analytics/analytics.ts`。
- AI 模型请求统计由 Main 侧 `electron/services/aiService.cjs` 统一上报。
- 禁止以清理、重构、简化为由删除或绕过现有埋点、统计字段、页面访问映射、模型使用统计和看板展示。
- 新增主菜单页面时，同步更新 `SectionId`、`menuConfig.ts`、`AppRouter.tsx`，并补 `analytics/dashboard/public/src/pages/traffic.js` 页面中文名映射。
- 埋点路由和值应稳定、可读、低基数，不包含用户输入、文件名、本地路径、文档 ID、任务 ID、随机 ID 或时间戳。
- 禁止上传 `api_key`、`base_url`、`mineru_token`、Prompt、AI 响应、错误详情、文件名、本地路径、招标文件内容、正文草稿等敏感或大文本数据。
- 埋点失败必须静默处理，不影响用户主流程。

## 发布与打包

- 打包配置位于 `client/package.json` 的 `build` 字段。
- Release workflow 位于 `.github/workflows/release.yml`，推送 `v*` tag 或手动输入 tag 触发。
- workflow 在 `client/` 下 `npm ci`，从 tag 同步 `package.json` 版本，再用 `electron-builder --publish never` 构建并通过 `gh release upload` 上传产物。
- 官方构建在打包前运行 `npm run generate-build-attestation`，读取 GitHub Actions Secret `YIBIAO_LICENSE_PRIVATE_KEY_JWK` 生成构建签名，并把公钥写入安装包；release workflow 缺少私钥会直接失败，本地脚本缺少私钥时才生成未签名开发构建。
- 当前未接入代码签名；Windows/macOS 未签名提示是已知发布约束，不要在普通功能改动里临时绕过。

本地打包命令：

```powershell
cd client
npm run dist:win
npm run dist:mac
```

## 验证标准

常规 Renderer / TypeScript 改动：

```powershell
cd client
npm run build
```

Electron Main / preload 改动：

```powershell
cd client
node --check electron\preload.cjs
node --check electron\services\<file>.cjs
npm run build
```

涉及窗口、IPC、文件选择、导出、后台任务事件时，还需要手动运行：

```powershell
cd client
npm run dev
```

涉及依赖变更时额外运行：

```powershell
cd client
npm audit
```

涉及 SQLite native 执行路径时，普通 Node 可能因 Electron ABI 不匹配无法跑运行时 smoke；`node --check` 只做语法检查，真实 Store 冒烟应使用 Electron 运行时。

## 开发约定

- 优先小步修改，不做无关重构。
- 保持 Windows 中文路径和中文编码兼容。
- 不新增全局状态库，除非实际复杂度需要。
- 不在 Renderer 存储 API Key。
- 不为新上传功能另写一套文档解析链路。
- 命名保持清晰：页面 `*Page.tsx`，服务 `*Service.ts` / `*.cjs`，Hook 以 `use` 开头。
- 用户可见文案、Toast、Dialog 标题和按钮文案保持中文、清晰、可操作。
- 新增主菜单入口时，必须按“埋点统计”章节完成埋点和看板映射自查。
