重构: AI流式断线保文StreamResult三分支+重试对齐决策a1,推进链落df-nodes

This commit is contained in:
2026-06-16 19:11:19 +08:00
parent 7d5cd4c89a
commit ba7f35552b
30 changed files with 1325 additions and 283 deletions

View File

@@ -23,8 +23,101 @@
## 当前队列
### CR-260616-35 batch59 CR-34-1 守卫 + CR-30 流式重试对齐决策a1 + UX-2025-04 断线保文 — 待审
- **范围**: 3 agent 合并批(workflow wdlt0xfl6,主代独立核查全过)。①**CR-34-1** useAiPanel.ts:102 守卫加有效性检查 `|| !list.some(p=>p.id===activeProvider)`(deleted provider 残留回落 DB is_default) ②**CR-30-1/CR-30-2 + UX-2025-04 合并**:stream_recv.rs `StreamResult` enum(Complete/Partial/InitFailed 三分支) + `classify_status_or_class`(4xx Fatal/5xx+429+timeout Retryable 镜像 retry::is_status_retryable) + agentic.rs 重试对齐决策 a1(Init 失败重试复用 retry::backoff_delay 1s→2s→4s±20%jitter + 30s 总预算 retry_deadline + Fatal 分类 4xx 立即放弃; MidStream Partial 不重试保文→入库 + AiCompleted(incomplete=true) + 系统提示「响应因网络中断不完整」) + 删 3 处误导注释(agentic.rs:12/commands.rs/state.rs) + retry.rs `pub mod` + `backoff_delay`/`is_status_retryable``pub` 暴露供 src-tauri 复用 + useAiEvents.ts:177 补 `case 'AiStreamRetry'`(消除死事件穿透) + AiCompleted case 处理 incomplete + types.ts:204/mod.rs `incomplete?:boolean` 字段(所有 emit 点补 None 向后兼容) + i18n aiStreamRetry/responseIncomplete 双语。
- **维度**: 流式核心中风险(重试循环正确性/保文语义/跨 crate pub 暴露影响面) + 前端事件消费完整性 + 决策 a1 对齐度(Init 可重试 ≤max_retries+jitter+30s+Fatal, MidStream 不重试) + B-260615-05 保留(空文本无 finished 仍 InitFailed 交重试)。
- **commit**: 待提交(累积 ~30 文件攒批)。
- **主代独立核查**: ✅ 全过(useAiPanel:102 守卫 / stream_recv:97 enum 三分支 + 注释 / agentic:242 retry_deadline + :274 Partial 不重试 + :286 InitFailed 重试 + :320 backoff_delay 复用 + :360 if incomplete 保文入库 + :400 emit incomplete:Some(true) / useAiEvents:177 AiStreamRetry case / types:204 incomplete / retry.rs:62+74 pub + agentic:14 `use df_ai::retry` 跨 crate 通)。
- **审查 agent 待复审重点**: ①stream_recv StreamResult 三分支边界(Partial vs InitFailed 空文本路径,B-260615-05 保留点) ②agentic 重试循环 permit RAII 释放/边界(max_retries=0/guard.reset) ③跨 crate retry pub 暴露是否过度(retry_with_backoff 仍 pub(crate)) ④AiCompleted incomplete 前端消费完整性(系统提示气泡镜像后端) ⑤UX-2025-04「继续生成」按钮**未做**(保文核心 ✅,按钮可复用 UX-2025-03 重试能力或按需补)。
---
## 已审归档
### CR-260616-34 batch58 F-260616-10 provider 默认值重启失效 bug — ⚠ 已审(PASS-WATCH)
- **审查时间**: 2026-06-16(主线程兜底·agent 撞 GLM 限流 1305 · grep/read 核验源码 + 类型确认)
- **总体结论**: ⚠ **PASS-WATCH** — 🔴0 🟡1 ⚪3(核心 bug 双恢复修复 OK,已删除 provider 残留守卫 med 待修)
- **逐件核验**:核心 bug 修复 ✅(重启 activeProvider=null → useAiPanel.ts:53-54 模块级 savedProvider 同步恢复 + loadProviders:101-104 DB is_default 兜底,双恢复链路)/ is_default 字段确认 ✅(types.ts:177 AiProviderConfig.is_default boolean,ai_list_providers 返回)/ cycleProvider 兜底 ✅(AiChat.vue:1184 startId=activeProvider||is_default||null 三级兜底,L1187 Math.max(idx,0) 防 -1 越界)。
- **🟡 CR-34-1(med,已删除 provider 残留未校正)**:useAiPanel.ts:101 `if (!state.activeProvider)` 守卫只查 null 不查有效性。localStorage savedProvider 指向已删除 provider(L54 设 activeProvider=deleted-id)时,loadProviders 守卫 false 跳过 DB is_default 恢复 → activeProvider 残留无效 id → cycleProvider:1185 findIndex=-1 → :1187 next=ps[1] 跳过 ps[0],且潜在消费方(发消息读 activeProvider 调后端)用无效 id。修法:守卫改 `if (!state.activeProvider || !list.some(p => p.id === state.activeProvider))` 残留无效时回落 DB is_default。
- **⚪ low**:1(L52 注释「以 DB 为准」与行为不符,实际 savedProvider 优先 DB 兜底,行为合理注释误导)/ 2(setProvider:121 void appSettings.set 持久化失败静默,仅影响重启恢复可接受)/ 3(多窗口/分离窗口 activeProvider 一致性:模块级 state 各 webview 独立,主 setProvider 写 SQLite 分离窗口 state 滞后,既存架构非 CR-34 引入)。
- **待修项回流 todo**: **CR-34-1**(med,useAiPanel.ts:101 已删除 provider 残留守卫改查有效性)。
---
### CR-260616-33 batch57 CR-260615-04 splitBlocks→marked.lexer() — ✅ 已审(PASS)
- **审查时间**: 2026-06-16(主线程兜底·agent 撞 GLM 限流 1305 · grep/read 核验源码)
- **总体结论**: ✅ **PASS** — 🔴0 🟡0 ⚪3
- **逐件核验(全 PASS)**:splitBlocks(AiChat.vue:671-691 marked.lexer 切块,L672 getMarked 取实例 + L676 未就绪兜底 `return [text]` 不引入正则 + L677 lexer tokenize + L678-689 for 循环)/ code token 整段一块(L681-685 `type==='code'` + raw.trim 守卫空代码块跳过 + continue)/ 非代码 token 按 `\n{2,}` 切段(L688 split + b.trim 守卫)/ 空 token 非 string raw 防御(L679 `typeof string` continue)/ 空兜底(L690 `blocks.length ? blocks : [text]`)/ 签名不变(`splitBlocks(text):string[]` + renderStreamingBlocks:723 调用)/ 切块边界与 parse 渲染边界一致(同 marked 实例同围栏规则,CR-260615-04 根治目标达成)/ 流式路径回归(renderStreamingBlocks:718-733 + parseBlock/parseBlockNoCache 块缓存机制不变)。
- **⚪ low**:1(lexer 全文 tokenize 性能略高于旧正则,rAF 节流+块缓存吸收,流式实测无影响)/ 2(末块未闭合围栏场景 lexer 可能把后续段落吞进 code raw,流式中间态短暂切块不准,末块重 parse 修正)/ 3(L690 空兜底 lexer 空文本返空数组时回退原文)。
- **待修项回流 todo**: **无**(核心目标切块/渲染边界一致达成,防御充分)。
---
### CR-260616-32 batch57 B-260619 Knowledge.vue CSS窄屏标题挤压 — ✅ 已审(PASS)
- **审查时间**: 2026-06-16(独立审查 agent · 亲跑 vue-tsc EXIT 0 + grep/read 核验源码)
- **总体结论**: ✅ **PASS** — 🔴0 🟡0 ⚪3
- **逐件核验(全 PASS)**:min-width:0 传播链完整(L576 kn-detail-panel → L622 detail-head → L623 detail-title-row flex:1 → L625 detail-title,窄屏标题可正常收缩)/ 760px 断点 column-wrap 与 grid 兼容(L730 kn-layout 单列降级 + L731 detail-head flex-direction:column 无冲突)/ 按钮换行间距(L626 detail-actions gap 6px + flex-wrap:wrap,L732 窄屏 width:100%)。
- **⚪ low**:1(L623 `.detail-title-row { overflow:hidden }` 冗余,word-break 多行标题 height auto 自适应不真裁断,语义不清,建议删保留 min-width:0+flex:1)/ 2(L733 窄屏字号降级仅覆盖 .detail-title 18→15px,编辑态 .edit-input-title L692 固定 16px 未同步,窄屏编辑态 16px 反超展示态 15px)/ 3(18→15px 偏激进,详情主标题窄屏建议 ≥16px)。
- **待修项回流 todo**: **无**(三 low 风格/一致性微调非阻断)。
---
### CR-260616-31 batch56 F-260616-08 分页 offset+探总量 — ✅ 已审(PASS)
- **审查时间**: 2026-06-16(主线程兜底·agent 撞 GLM 限流 1305 · grep/read 核验源码形态;**未亲跑 cargo check**,标准 API 语法无编译风险,待 agent GLM 恢复补 cargo 验证)
- **总体结论**: ✅ **PASS** — 🔴0 🟡0 ⚪2
- **逐件核验(全 PASS)**:①offset 边界(tool_registry.rs:216/237/254 offset `as_u64().unwrap_or(0)`→usize,JSON 负数 as_u64 None→0,skip(offset).take(limit) 超 total 取空不 panic;has_more `(offset+page.len())<total` 超时 false)/ ②total 准确(list_projects:214-215 `list_active().len()` 分页前全量=真实总量;list_tasks:236/list_ideas:253 同)/ ③探总量与 list 一致(get_project_count:1091-1093 list_active().len()=list_projects:214-215 同源;get_task_count:1104-1108 project_id 分支+list_all 兜底=list_tasks:231-234 同逻辑,range 一致)/ ④knowledge_search 解钳(knowledge.rs:110 `.min(20)` 替原 `.min(3)`,允许 LLM 翻更多候选合理)/ ⑤schema 描述(:208/225/246 含 offset/limit/total/has_more 翻页语义 LLM 可理解)。
- **⚪ low**:1(内存放大:list_*/get_*_count 先全量 load(list_active/list_all)再 skip/take,数据量大时内存放大;AI 工具调用 + MAX_LIST_RESULTS 钳 limit 实际可控)/ 2(list_tasks/get_task_count 用 list_all 未排除软删任务,而 list_projects/get_project_count 用 list_active 排除软删,任务与项目软删语义不对称;但 get_*_count 与 list_* 同源一致,且为既存逻辑非 CR-31 引入)。
- **待修项回流 todo**: **无**(五维度全 PASS)。
---
### CR-260616-30 batch56 F-260616-07 流式自动重试 — ⚠ 已审(ISSUES)
- **审查时间**: 2026-06-16(独立审查 agent · 亲跑 cargo check EXIT 0 + cargo test df-ai retry 6 passed **附 log 末尾** + grep/read 核验 + 重试循环正确性专项)
- **总体结论**: ⚠ **ISSUES** — 🔴0 🟡2 ⚪3(核心重试循环逻辑正确 permit/guard.reset/request 重建均 OK,但退避策略注释误称对齐 + AiStreamRetry 死事件 med)
- **亲跑 `cargo check --workspace`** ✅ EXIT 0(8 warning 全既存 dead_code 非本批)+ **`cargo test -p df-ai retry`** ✅ EXIT 0 **6 passed**(backoff_is_monotonic_within_jitter/status_classification/fatal_aborts_immediately/first_attempt_ok/retry_once_then_ok/all_retryable_exhausts,finished 3.35s)。
- **重试循环正确性专项(全 PASS)**:permit 持有释放 ✅(`_global_permit`/`_per_conv_permit` agentic.rs:239-240 外层函数体作用域,耗尽 return:285/成功 drop:333-334 均 RAII 释放无并发槽泄漏,重试期间持 permit 有意防新请求挤占)/ 耗尽路径 guard.reset ✅(:284 `guard.reset().await` return 前显式复位 generating,max_retries=0 时 `0..=0` 单次失败 is_last→reset+return 边界正确)/ request 每轮重建 ✅(:248-263 每轮 `session_arc.lock()`+`build_for_request` 重建 messages 无累积/丢失,与循环外:207-215 `request` 变量重复轻微冗余无害)。
- **🟡 CR-30-1(med,退避策略注释误称对齐)**:agentic.rs:298 `1u64 << retry_attempt` 纯指数无 jitter/无总预算/一律重试(含 MidStream);retry.rs:67-78 `backoff_delay` 有 ±20% jitter + 30s 总预算(:34)+ 4xx 不可重试分类(:102-104 Fatal 立即放弃)。三处注释(agentic.rs:37/commands.rs:1106/state.rs:185)均称「对齐 retry.rs 退避策略(1s→2s→4s+jitter)」与实现不符。修法:要么 agentic 加 jitter+总预算+错误分类真对齐,要么删注释「对齐 retry.rs」改为「独立简化策略」。
- **🟡 CR-30-2(med,AiStreamRetry 死事件 + MidStream 重试文本混乱)**:后端 emit AiStreamRetry(agentic.rs:310),types.ts:214 有 variant,但 useAiEvents.ts switch 无 `case 'AiStreamRetry'`(十 case 无 retry)事件穿透忽略。更严重:MidStream 失败重试时已 emit 的 AiTextDelta 累积进前端 currentText,重试新 delta 追加 → 文本重复/混乱(stream_recv.rs:210/240 MidStream return None,agentic.rs:294 一律重试无清空 currentText 机制)。修法:useAiEvents 补 case + 重试前清空 currentText;或后端 MidStream 失败不重试只重试 Init 失败(对齐决策 a 只重试流前失败)。
- **⚪ low**:1(agentic.rs:243/317 `last_error` 声明赋值但 result 分支:323-330 未读 dead code)/ 2(:294-296 MidStream 一律重试注释承认「简化」,与决策 a 只重试流前失败相悖)/ 3(run_agentic_loop 3 调用点 commands.rs:108/204/444 非登记「4 入口」,try_continue 走审批恢复复用非独立入口)。
- **待修项回流 todo**: **CR-30-1**(med,退避策略注释对齐)+ **CR-30-2**(med,AiStreamRetry 死事件+MidStream 重试文本混乱)。
---
### CR-260616-29 batch56 ARC-06 composable 循环依赖破环 — ✅ 已审(PASS)
- **审查时间**: 2026-06-16(主线程兜底·agent 撞 GLM 限流 1305 · grep/read 核验 import + 源码)
- **总体结论**: ✅ **PASS** — 🔴0 🟡0 ⚪1
- **逐件核验(全 PASS)**:①循环依赖真破(useAiEvents.ts import L14-23 无 useAiSend,改 L284 `emit('ai-drain-queue')` 事件总线;useAiSend.ts:25 仍 import useAiEvents(startListener)单向,双向环已断)/ ②事件总线桥接(useAiEvents:284 emit + useAiSend:395-398 initDrainQueueListener `if(_drainUnlisten) return` 幂等守卫 + stores/ai.ts:134 void 注册)/ ③findToolCall 下沉一致(aiShared.ts:41-51 反向扫 messages + 同消息正向扫 toolCalls,两调用方 import 同一实现,注释:37-40 说明反向扫描避免陈旧引用)/ ④startApprovalTimer 幂等(aiShared.ts:66 `if(_approvalTimers.has) return` 同 id 不重建 + clearApprovalTimer/clearAllApprovalTimers 清理完整)。
- **⚪ low**:1(事件总线桥接理论竞态:initDrainQueueListener stores/ai.ts:134 void 不 await,注册前若 AiCompleted 到达事件丢失 drainQueue 不触发;实际初始化远早于首次生成完成低风险,原 direct call 同步无此问题,可靠性微小降级)。
- **待修项回流 todo**: **无**(四维度全 PASS,循环依赖真破)。
---
### CR-260616-28 batch56 F-260616-06 审批幂等+防抖 — ✅ 已审(PASS)
- **审查时间**: 2026-06-16(主线程兜底·agent 撞 GLM 限流 1305 · grep/read 核验源码 + 方法存在性确认)
- **总体结论**: ✅ **PASS** — 🔴0 🟡0 ⚪2
- **逐件核验(全 PASS)**:①幂等三态覆盖(commands.rs:220 内存 pending_approvals.remove None → :224 查审计表 find_by_tool_call_id → :227 executed/rejected/failed 返回 Ok「已处理」三态全覆盖;pending 态落 :231 Err 合理——内存 pending_approvals 丢失即审批上下文不可恢复,DB pending 占位仅日志非可恢复审批)/ ②防抖(useAiSend.ts:307 _pendingApprovalIds.has 守卫无时间窗口竞态 + :308 add + :332-334 finally delete 无论成功异常均清理,模块级 Set 不泄漏)/ ③find_by_tool_call_id 存在确认(crud.rs:1198-1201 `pub async fn(&self,&str)->Result<Option<AiToolExecutionRecord>>`,:224 unwrap_or_default Err→None→:231 Err,DB 故障不误判已处理)。
- **⚪ low**:1(pending 审计记录 + 内存 pending_approvals 丢失→:231 Err「未找到挂起的审批」文案略困惑,实际审批上下文已不可恢复 Err 正确,文案对有 pending 审计记录场景不精确)/ 2(防抖 _pendingApprovalIds 模块级跨调用累积,finally 清理可靠无泄漏,机制说明非问题)。
- **待修项回流 todo**: **无**(三维度全 PASS)。
---
### CR-260616-27 batch54 复核-新⑪ detach 清主窗口 state 看门狗幽灵(useAiWindow.ts) — ✅ 已审(PASS)
- **审查时间**: 2026-06-16(主线程兜底·agent 撞 GLM 限流 1305 · grep/read 核验 + vue-tsc EXIT 0)
- **总体结论**: ✅ **PASS** — 🔴0 🟡0 ⚪0
- **逐件核验(全 PASS)**:detachPanel(useAiWindow.ts:28-38 快照在清 state 前写入 — L28-31 localStorage.setItem df-ai-gen/df-ai-text → L37-38 清 state.streaming/generatingConvId,顺序正确)/ state 独立性(主/分离窗口独立 webview JS context,resumeInDetached:87-119 靠 localStorage df-ai-gen/df-ai-text 重建 streaming L116-117,反证清主窗口 state 不影响分离窗口生成态)/ watchdog 兜底(stopListener clearStreamWatchdog 已由 CR-260615-24 兜底,本次仅清 state 视觉残留,注释:32-36 说明链路)/ reattach/closeDetachedWindow 清快照(L82-83/L125-126)。
- **待修项回流 todo**: **无**(全件 PASS)。
---
### CR-260616-26 batch53 F-05 去重根治(audit.rs High risk 审批前缓存命中去重) — ✅ 已审(PASS)
- **审查时间**: 2026-06-16(独立审查 agent · 亲跑 cargo + vue-tsc 双 EXIT 0 **附 log 末尾证据** + grep/read 核验全件 + 边界核验)