baa-conductor

git clone 

commit
3e66aa3
parent
833be60
author
im_wower
date
2026-03-30 17:54:51 +0800 CST
docs: Claude 代码审查 — BUG-034 BUG-035 BUG-036 OPT-009
4 files changed,  +122, -0
A bugs/BUG-034-projector-route-unavailable-missing-detail.md
+26, -0
 1@@ -0,0 +1,26 @@
 2+# BUG-034: Projector hasAvailableRoute 缺少具体 skip 原因日志
 3+
 4+> 提交者:Claude
 5+> 日期:2026-03-30
 6+
 7+## 现象
 8+
 9+`projector.ts` 的 `hasAvailableRoute()` 在三种情况下返回 false:
10+
11+1. `link.isActive !== true`
12+2. `targetKind !== "browser.proxy_delivery"`
13+3. `shellPage === true || tabId == null`
14+
15+但调用方只记录了 `route_unavailable`,没有区分具体原因。浏览器重启后 tabId 丢失、插件断连、shellPage 误标——这三种情况在日志里完全一样,排障时无法区分。
16+
17+## 影响
18+
19+浏览器重启后所有对话的续命会静默停止,直到插件重新上报 route。日志中只能看到 `route_unavailable`,无法判断是 tabId 丢失还是其他原因。
20+
21+## 建议修复
22+
23+将 `hasAvailableRoute` 改为返回 `{ available: boolean; reason: string }` 结构,或至少在返回 false 时附带原因字符串。projector 的 `shouldRenew` 将该原因透传到日志的 `details` 中。
24+
25+## 优先级
26+
27+低。不影响功能正确性,只影响排障效率。
 1@@ -0,0 +1,30 @@
 2+# BUG-035: conversation_links UNIQUE INDEX 不约束 NULL remote_conversation_id
 3+
 4+> 提交者:Claude
 5+> 日期:2026-03-30
 6+
 7+## 现象
 8+
 9+Schema 中 `idx_conversation_links_remote` 定义为 `UNIQUE(platform, remote_conversation_id)`。
10+
11+SQLite 中 NULL 值不参与 UNIQUE 约束(SQL 标准行为),因此同一平台可以存在多条 `remote_conversation_id = NULL` 的 link 而不触发冲突。
12+
13+## 触发路径
14+
15+1. Gemini 页面的 conversation_id 提取失败(`extractGeminiConversationIdFromUrl` 返回 null)
16+2. `observeRenewalConversation` 写入 `conversation_links`,`remote_conversation_id = NULL`
17+3. 下次同一页面再次触发,又写入一条 `remote_conversation_id = NULL` 的新 link
18+4. 两条 link 都指向不同的 `local_conversation_id`
19+5. `resolveExistingConversationLink` 跳过 `remote_conversation_id` 查找,走到 `scoreConversationLink` 弱信号路径
20+6. 弱信号匹配可能选到错误的 link,导致消息关联到错误对话
21+
22+## 建议修复
23+
24+两种方案:
25+
26+A. 在应用层,`remote_conversation_id` 为 null 时生成一个合成 ID(如 `synthetic_{platform}_{pageUrl_hash}`),确保 UNIQUE 生效
27+B. 加一个部分索引 `CREATE UNIQUE INDEX ... ON conversation_links(platform, client_id, page_url) WHERE remote_conversation_id IS NULL`,约束无 conversation_id 时的 link 唯一性
28+
29+## 优先级
30+
31+中。当前 Gemini 的 conversation_id 提取大部分情况能成功,但极端情况下会导致对话关联错乱。
A bugs/BUG-036-projector-cursor-updated-at-unit-mismatch.md
+37, -0
 1@@ -0,0 +1,37 @@
 2+# BUG-036: projector cursor updatedAt 秒级与 observedAt 毫秒级不一致
 3+
 4+> 提交者:Claude
 5+> 日期:2026-03-30
 6+
 7+## 现象
 8+
 9+`projector.ts` 的 `saveCursor` 使用 `Math.floor(now / 1000)` 将毫秒转为秒级写入 `system_state.updated_at`:
10+
11+```typescript
12+await repository.putSystemState({
13+  stateKey: cursorStateKey,
14+  updatedAt: Math.floor(now / 1000),  // 秒级
15+  valueJson: JSON.stringify({
16+    message_id: cursor.id,
17+    observed_at: cursor.observedAt     // 毫秒级
18+  })
19+});
20+```
21+
22+而 `system_state` 表的 `updated_at` 列定义为 `INTEGER NOT NULL`,没有明确单位约定。如果后续有其他代码读 `system_state.updated_at` 并假设毫秒级,会出现约 1000 倍的时间偏差。
23+
24+## 当前影响
25+
26+cursor 本身的值存在 `valueJson` 里(`observed_at` 是毫秒级),cursor 恢复时读的是 `valueJson` 而不是 `updated_at`,因此当前功能不受影响。
27+
28+但 `updated_at` 的单位不一致是潜在的混淆源。
29+
30+## 建议修复
31+
32+统一 `system_state.updated_at` 为毫秒级(与其他所有 `*_at` 字段一致),或在 `system_state` schema 注释中明确标注单位。
33+
34+`ops/sql/schema.sql` 中现有的 `system_state` 初始化使用 `CAST(strftime('%s', 'now') AS INTEGER)` 即秒级。如果要统一为毫秒级,需要一起改。
35+
36+## 优先级
37+
38+低。当前不影响功能,但单位不一致是后续维护的隐患。
A bugs/OPT-009-renewal-duplicate-utility-functions.md
+29, -0
 1@@ -0,0 +1,29 @@
 2+# OPT-009: renewal 模块重复定义工具函数,建议抽取到 renewal/utils.ts
 3+
 4+> 提交者:Claude
 5+> 日期:2026-03-30
 6+
 7+## 现象
 8+
 9+以下函数在 `conversations.ts`、`projector.ts`、`dispatcher.ts` 中各自独立定义了一份:
10+
11+- `normalizeOptionalString`
12+- `normalizeRequiredString`(conversations 和 projector)
13+- `parseJsonValue`(projector 和 dispatcher)
14+- `isPlainRecord`(projector 和 dispatcher)
15+
16+函数体完全相同,各文件独立维护。
17+
18+## 影响
19+
20+- 修改一处(如增加 trim 规则)需要同步修改三处
21+- 代码膨胀约 60-80 行重复代码
22+- 后续新增 renewal 模块文件(如 subscriber)会继续复制
23+
24+## 建议
25+
26+新建 `apps/conductor-daemon/src/renewal/utils.ts`,将共用工具函数统一导出。三个文件改为 `import { normalizeOptionalString, ... } from "./utils.js"`。
27+
28+## 优先级
29+
30+低。不影响功能,属于代码卫生。