From a44de1441a06947257e8abcd4aaa4bf2c4e8a5c6 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Sun, 13 Sep 2026 16:02:15 +0800 Subject: [PATCH] docs: record recurring defect types and the mechanism covering each The titlebar drag-region defect was fixed three times in a row, each time only for the dialogs someone reported, and the same batch introduced two more instances. Repeated full-table lookups and production reuse of a verification helper showed up in several places at once for the same reason. Add docs/quality/recurring-defects.md describing each type that has actually recurred here, the shared mechanism or automated test that now covers it, and the history that justifies the entry. Require reading it from AGENTS.md before touching overlays, store lookups, retrieval entry points, cancellation, or render-time derived values, and require extending the shared mechanism rather than patching one reported case. Correct the external knowledge technical design where it described the removed configuration-snapshot checks, the shared retrieval entry point and the unreachable probe status, and record the refactor and its validation in the knowledge base progress log. --- AGENTS.md | 7 ++ .../external-knowledge-technical-design.md | 24 +++-- docs/features/knowledge-base/progress.md | 30 ++++++ docs/quality/README.md | 1 + docs/quality/recurring-defects.md | 96 +++++++++++++++++++ 5 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 docs/quality/recurring-defects.md diff --git a/AGENTS.md b/AGENTS.md index d2fb3e27..f148b1e8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -148,6 +148,13 @@ additional hardening beyond the actual internal-network threat model: repository mechanisms instead of inventing extra gates, ceremonies, or disabled states. - Add or update focused tests for behavioral changes and regressions. +- Read [`docs/quality/recurring-defects.md`](./docs/quality/recurring-defects.md) + before changing overlays, store lookups, retrieval entry points, cancellation + behavior, or render-time derived values. It records defect types that have + already recurred in this repository and the shared mechanism that covers each + one. When fixing an instance of a listed type, extend that shared mechanism + instead of patching the one reported case, and add an entry when a new defect + type recurs. - After completing any functional change, inspect the affected product, architecture, design, feature, setup, and operational documentation and update every relevant document to match the implemented behavior. Treat the diff --git a/docs/features/knowledge-base/external-knowledge-technical-design.md b/docs/features/knowledge-base/external-knowledge-technical-design.md index 8e4a5f8c..219cf38b 100644 --- a/docs/features/knowledge-base/external-knowledge-technical-design.md +++ b/docs/features/knowledge-base/external-knowledge-technical-design.md @@ -118,13 +118,15 @@ Preload 暴露实例 `List/Save/Test/SetEnabled/Delete`、目录 `List/Get`、 Handler 调用 `assertTrustedSender` 并解析共享输入;检索按实例 ID 在 Main 读取地址与凭据。 实例先保存为 `untested`,列表上的连接测试调用目录请求并更新摘要,不测试未保存草稿。 -绑定检索测试是单独操作,不会把实例探测状态自动升级为 `healthy`。 +绑定检索测试是单独操作,不会改变实例探测状态。探测状态取值为 `untested`、 +`catalog-ready`、`list-restricted`、`auth-failed`、`unreachable`、`failed`。 服务按实例记录 AbortController。保存实例、停用、删除以及服务关闭会取消相应请求; -调用方取消信号与总超时合并。异步测试或保存完成前比较实例配置,变化时返回 -`EXTERNAL_KB_CONFIG_CHANGED`,绑定更新还检查保存期间绑定是否改变。共享 IPC 没有 -`requestId` 或取消方法;Renderer 用局部 generation/active 标记丢弃旧响应,这与中止 -Main 请求是两种行为。 +调用方取消信号与总超时合并。取消即是配置变更的唯一处理方式:被取消的连接测试直接抛出 +`EXTERNAL_KB_CANCELLED`,不写回探测结果,因此不需要额外的前后配置快照比较。绑定保存在 +验证请求前后各检查一次同一远端目标是否已被占用,最终以 `UNIQUE(instance_id, remote_id)` +索引为准。共享 IPC 没有 `requestId` 或取消方法;Renderer 用局部 generation/active 标记 +丢弃旧响应,这与中止 Main 请求是两种行为。 删除仍被引用的实例返回 `EXTERNAL_KB_INSTANCE_IN_USE`。UI 确认后逐个调用本地 `deleteLibrary`,再删除实例;这是顺序操作,发生中途失败会刷新已移除的绑定,不是跨调用事务。 @@ -143,16 +145,22 @@ Main 请求是两种行为。 ## 7. 检索编排 -`KnowledgeService.retrieve` 查询本地绑定,存在绑定时调用 -`ExternalKnowledgeService.testRetrieval`,否则执行现有本地路径。外部结果附加 +`KnowledgeService.retrieve` 按知识库 ID 单行查询本地绑定,存在绑定时调用 +`ExternalKnowledgeService.retrieve`,否则执行现有本地路径。外部结果附加 `diagnostics.external`,不运行本地向量、全文或图谱检索。 +`ExternalKnowledgeService` 区分两个入口:`retrieve` 是生产检索路径,只发出一次检索请求; +`testRetrieval` 仅用于保存绑定前的验证,额外在同一超时预算内确认 RAGFlow 图谱与知识编排 +能力可用,能力缺失时返回 `EXTERNAL_KB_CAPABILITY_UNAVAILABLE`。生产检索不执行该能力预检, +因此正常对话不会多一次 detail 请求,也不会出现仅属于保存校验的错误码。 + `retrieveMany` 对传入知识库 ID 去重并读取知识库,使用 `Promise.all` 逐库检索;只有选中 需要向量的本地库时才准备本地查询 Embedding。当前没有额外的 Provider 并发队列。单库 失败写入 `response.diagnostics.failure` 并保留空结果,成功零命中没有 failure;调用方 取消仍向上传播。当前返回 `{knowledgeBaseId, response}[]`,没有独立的 Outcome 状态联合。 -服务测试先按绑定的结果数与单片段限制裁剪,并限制该次片段总量为 48,000 字符。 +服务检索与绑定验证共用同一裁剪逻辑:先按绑定的结果数与单片段限制裁剪,并限制该次片段 +总量为 48,000 字符。 `retrieveMany` 展平各库结果并按库内 rank 稳定排序,再轮转分配 48,000 字符的片段预算: 先处理各库第 1 条,再处理各库第 2 条,同 rank 保持输入库顺序。不会先耗完整个库再轮到 下一个库;仍没有固定的每库最低字符配额。关联的本地上下文 groups 随相应结果使用另一份 diff --git a/docs/features/knowledge-base/progress.md b/docs/features/knowledge-base/progress.md index e28dcdec..5cb7e560 100644 --- a/docs/features/knowledge-base/progress.md +++ b/docs/features/knowledge-base/progress.md @@ -16,6 +16,36 @@ ## 验证证据 +### 2026-09-13 检索路径与绑定查询简化 + +对 `3b560c9..ca4a58c` 的复查发现三处与本功能相关的实现问题,已按 KISS 与性能优先修正: + +| 修正项 | 修正前行为 | 修正后行为 | +| --- | --- | --- | +| 生产检索入口 | 对话检索复用 `testRetrieval`,把用户问题传入 `testQuery`,并对 RAGFlow 额外发一次 detail 请求 | 新增 `ExternalKnowledgeService.retrieve` 作为生产路径,只发一次检索请求;能力预检仅保留在 `testRetrieval` | +| 配置变更检测 | 5 处 `JSON.stringify` 前后快照对比,抛出 `EXTERNAL_KB_CONFIG_CHANGED` | 全部删除,改由既有 `AbortController` 取消覆盖;被取消的探测抛 `EXTERNAL_KB_CANCELLED` 且不写回探测结果 | +| 绑定查询 | `requireLibrary`(30+ 调用点,其中一处在循环内)与 `getKnowledgeSnapshot` 每次全表 `listBindings()` | 新增 `getBinding`/`hasBinding`/`getBindingsForInstance` 主键查询;快照改为单次 `Map` 构建 | + +修正过程中被测试拦下两次行为回退,已记录为当前不变量: + +- 被取消的连接测试必须抛出且不写回 `probeStatus`,不能记为 `failed`;用户停用实例不等于探测失败。 +- `testRetrieval` 的 RAGFlow 能力预检必须与检索共用绑定配置的 `requestTimeoutMs`,不能各自计时。 + +同时删除确认无生产引用的死代码:`probeStatus` 的 `'healthy'` 取值、`external.transportConfirm` +与 `external.states` 的 `remote-missing`/`config-invalid` 文案、`KnowledgeService.searchHybridMany`、 +`ExternalKnowledgeRemoteResult.sourceUrl`。`externalKnowledgeLocatorSchema` 的 `sourceUrl` +保留,因为它解析已持久化的引用数据。 + +| 验证 | 结果 | +| --- | --- | +| `npx vitest run src/main/knowledge src/main/agent/knowledge-mcp-gateway.test.ts` | 21 个文件通过、1 个跳过;288 项通过、1 项跳过 | +| `npm test` | 348 个文件通过、9 个跳过;4,088 项通过、66 项跳过 | +| `npm run typecheck`、`npm run lint` | 通过 | + +`npm test` 的唯一失败为 `src/main/agent/opencode-runtime-lifecycle.test.ts:120` +("Matcher did not succeed in time")。已用 `git stash` 在同一 `HEAD` 上复现同样失败, +确认早于本次修正且与知识库无关,未计入上表。 + ### 2026-09-13 当前工作区实现与分组验证 三家 Provider 已通过生产服务、本机 HTTP MCP 与生产 IPC 的短答案生成检查,Dify 另已 diff --git a/docs/quality/README.md b/docs/quality/README.md index 82f5e9fa..8acc2bb4 100644 --- a/docs/quality/README.md +++ b/docs/quality/README.md @@ -5,6 +5,7 @@ | 文档 | 职责 | | --- | --- | | [Electron UI 自动化测试规范](./electron-ui-automation.md) | 测试分层、当前源码启动、视觉与原生交互取证、真实调用计数、脱敏报告和清理 | +| [复发缺陷与防复发规则](./recurring-defects.md) | 已重复出现的缺陷类型、对应的共享机制和自动发现测试 | | [开发与构建](../../BUILD.md#质量验证) | 依赖、测试、类型检查、Lint 和构建命令 | | [UI 设计系统](../../UI-DESIGN.md#155-验收) | 全局视觉、交互和响应式验收要求 | | [知识检索评估](../features/knowledge-base/retrieval-evaluation.md) | 检索数据集、指标及评估方法 | diff --git a/docs/quality/recurring-defects.md b/docs/quality/recurring-defects.md new file mode 100644 index 00000000..f6c04361 --- /dev/null +++ b/docs/quality/recurring-defects.md @@ -0,0 +1,96 @@ +# 复发缺陷与防复发规则 + +本文档收录已经在本仓库重复出现过的缺陷类型。每条规则都来自至少一次真实修正,不是预设的 +通用最佳实践。新增条目前,先确认该问题在提交历史中确实出现过一次以上,或一次修正中同时 +出现在多处。 + +修正复发缺陷时,优先建立一条能覆盖整类问题的机制(共享选择器、共享函数、能发现全部实例 +的测试),而不是逐个点名修补。逐个修补正是这些条目反复出现的原因。 + +## 整窗遮罩与原生拖动区域 + +**症状**:弹窗覆盖标题栏区域时,其关闭按钮无法悬停和点击。 + +**原因**:`.brand` 与 `.topbar` 声明了 `-webkit-app-region: drag`。整窗遮罩绘制在其上方 +时,原生标题栏仍然截获指针。 + +**规则**:整窗遮罩必须 `inset: 0` 铺满窗口,并由 `styles.css` 中统一的遮罩选择器声明 +`-webkit-app-region: no-drag`。新增遮罩时扩展该共享选择器;不要在各自规则里复制 +`no-drag`,也不要用 `inset: 38px 0 0` 之类的偏移绕开标题栏。完整要求见 +[UI 设计系统](../../UI-DESIGN.md)。 + +**防复发**:`WorkspacePrimitives.test.tsx` 的 +"releases the native drag region for every full-window overlay" 从 `styles.css` 自动发现 +全部 `position: fixed` 遮罩并逐个校验,新增遗漏会直接失败。 + +**历史**:`e9679fe`、`ca4a58c` 各修了一部分被人工点到的弹窗,同一批改动又新增了两个未覆盖 +的遮罩;后续复查改为共享规则加自动发现测试。 + +## 逐行查询代替主键查询 + +**症状**:列表渲染或校验函数随数据量线性变慢,且慢在单次操作内重复发生。 + +**原因**:用 `listAll().some(...)` 或 `listAll().find(...)` 判断单行是否存在。表有主键或 +唯一索引时,这是把 O(1) 查询写成了全表扫描加 `JSON.parse`。 + +**规则**:判断单行存在或取单行数据时,在 store 上提供按键查询方法(例如 `hasBinding`、 +`getBinding`),不要在调用方过滤全量列表。确实需要按分组统计时,循环外取一次并构建 +`Map`,不要在循环内反复查询。 + +**历史**:外部知识库绑定同时出现三处:`requireLibrary`(30+ 调用点,一处在循环内)、 +`getKnowledgeSnapshot`(每个知识库两次全表扫描)、`listInstances`(每个实例过滤一次)。 + +## 生产路径复用“测试”函数 + +**症状**:正常用户操作触发了只应在配置校验时出现的错误码,或多发了一次网络请求。 + +**原因**:让生产路径直接调用为“测试连接”“保存前校验”准备的函数。这类函数通常带有额外的 +能力预检和更严格的失败条件。 + +**规则**:生产路径与校验路径各自提供入口。校验入口可以在生产入口之上叠加预检,反向复用 +不允许。共用的裁剪、归一化逻辑抽成独立函数由两者调用。 + +**历史**:对话检索曾调用 `ExternalKnowledgeService.testRetrieval` 并把用户问题放进 +`testQuery`,导致每次提问多一次 detail 请求,且 `EXTERNAL_KB_CAPABILITY_UNAVAILABLE` +会出现在正常对话中。 + +## 快照比对代替取消 + +**症状**:为检测“操作期间配置被改动”而反复读回状态并 `JSON.stringify` 比较。 + +**原因**:把取消语义当成配置漂移检测来实现。桌面单用户场景下,改动配置的那次操作本身就能 +取消在飞请求。 + +**规则**:用 `AbortController` 在配置变更点取消在飞请求,并让被取消的操作不写回任何结果。 +不要引入前后快照比较、版本化内部凭据或多阶段状态机。唯一性约束交给数据库索引。 + +**历史**:外部知识库服务曾有 5 处快照比对与专用错误码 `EXTERNAL_KB_CONFIG_CHANGED`;全部 +删除后行为反而更正确,因为旧实现会把“用户停用实例”记成 `probeStatus: 'failed'`。 + +## 删除疑似死代码时的验证顺序 + +**症状**:删除后类型检查通过,但运行时行为改变,或测试断言的是已不存在的行为。 + +**规则**:删除枚举取值、文案键、导出函数前,按此顺序确认: + +1. `rg` 全仓搜索引用,区分生产代码与测试 fixture。 +2. 确认该取值是否可能由生产代码写入。仅测试 fixture 使用即为死代码。 +3. 检查是否属于已持久化数据的解析契约。持久化契约即使当前不写入也必须保留。 +4. 删除后跑受影响的定向测试,再跑 `npm test`。 + +**历史**:`probeStatus` 的 `'healthy'` 仅存在于测试 fixture,可以删除;而 +`externalKnowledgeLocatorSchema` 的 `sourceUrl` 虽然同样从不写入,但它解析已持久化的引用 +数据,必须保留。两者表面症状相同,处置相反。 + +## 渲染期派生值未记忆化 + +**症状**:流式响应期间界面卡顿,或大列表操作延迟。 + +**原因**:在组件体内直接构建 `Map`/`Set` 或链式 `filter`/`flatMap`,而该组件在流式增量期间 +每个 token 都会重渲染。 + +**规则**:组件体内的派生集合必须包在 `useMemo` 中,依赖收窄到真正影响结果的值。紧邻的 +`useMemo` 之后又裸建集合,会让记忆化收益减半。 + +**注意**:React 允许在渲染期用守卫调用 `setState` 来响应 props 变化,这是官方推荐做法,不要 +把它改写成 `useEffect`。仓库 lint 规则 `react-hooks/set-state-in-effect` 会拒绝后者。