# 复发缺陷与防复发规则 本文档收录已经在本仓库重复出现过的缺陷类型。每条规则都来自至少一次真实修正,不是预设的 通用最佳实践。新增条目前,先确认该问题在提交历史中确实出现过一次以上,或一次修正中同时 出现在多处。 修正复发缺陷时,优先建立一条能覆盖整类问题的机制(共享选择器、共享函数、能发现全部实例 的测试),而不是逐个点名修补。逐个修补正是这些条目反复出现的原因。 ## 整窗遮罩与原生拖动区域 **症状**:弹窗覆盖标题栏区域时,其关闭按钮无法悬停和点击。 **原因**:`.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` 会拒绝后者。