优化: AI Chat全栈多批审查修复与架构清理(risk_level清理/路由解耦/工具渲染/测试补测/死代码)
This commit is contained in:
@@ -8,6 +8,45 @@
|
||||
|
||||
---
|
||||
|
||||
> ## 实施状态(2026-06-18 核对)
|
||||
>
|
||||
> **②-⑤ 全部已落地**(原设计 1.2 表中标 ⚠️ 的 crud/白名单缺口已补全,②③④⑤ 已实施)。
|
||||
>
|
||||
> **① df-storage 迁移 — 已完成**(原 ⚠️ 项已补):
|
||||
> - `TaskRecord.output_json: Option<String>`:`crates/df-storage/src/models.rs:72`。
|
||||
> - 白名单含 output_json:`crates/df-storage/src/crud.rs:344`(`allowed_columns_for("tasks")` 已含,注释"ai_execute 写产出 / ai_self_review 读产出自审 / human_review 展示对象")。
|
||||
> - migrations:`crates/df-storage/src/migrations.rs:303`(注释同上)。
|
||||
>
|
||||
> **② AiNode 持 db + 写产出 — 已落地**:
|
||||
> - `AiNode` struct:`crates/df-nodes/src/ai_node.rs:256`(持 db 字段,`AiNode::new(db)` 构造)。
|
||||
> - state.rs 工厂闭包注入 db:`src-tauri/src/state.rs:351-352`(`registry.register("ai", ... AiNode::new(ai_db.clone()))`)。
|
||||
> - execute 写产出:`crates/df-nodes/src/ai_node.rs:319-325`(若 config 含 task_id → `repo.update_field(task_id, "output_json", &json_str)`)。
|
||||
>
|
||||
> **③ AiSelfReviewNode 独立节点 — 已落地**(原设计 2.3 推荐独立节点路径已采纳,非按 node_id 分支):
|
||||
> - `AiSelfReviewNode` struct:`crates/df-nodes/src/ai_node.rs:438`。
|
||||
> - `REVIEW_SYSTEM_PROMPT`(四维度 prompt 模板):`crates/df-nodes/src/ai_node.rs:387`。
|
||||
> - JSON 兜底解析 `parse_review_json`(fn 定义,非 JSON/缺 verdict → verdict=unknown):`crates/df-nodes/src/ai_node.rs:396`,单测 `:1011-1058`(valid/invalid/fence 三场景)。
|
||||
> - `build_review_prompt`(fn 定义):`crates/df-nodes/src/ai_node.rs:449`,单测 `:1060`。
|
||||
> - state.rs 注册:`src-tauri/src/state.rs:359-360`(`registry.register("ai_self_review", ... AiSelfReviewNode::new(review_db.clone()))`)。
|
||||
>
|
||||
> **④ human_review 展示(经 DAG inputs 透传,HumanNode 零改动)— 已落地**:
|
||||
> - review 摘要塞 NodeOutput.data:`crates/df-nodes/src/ai_node.rs:586`(注释「NodeOutput.data 塞 review 摘要,供下游 human_review 经 inputs["ai_self_review"] 读」)。
|
||||
> - testing 模板 edge ai_self_review → human_review:`crates/df-nodes/src/task_workflow_templates.rs:80`,单测 `:149` 验证边方向(`assert_eq!(edge.source, "ai_self_review")`)。
|
||||
> - HumanNode 本身零改动(git 历史核验 human_node.rs 在 AiNode 自审批 commit 中无变动)。
|
||||
>
|
||||
> **⑤ 前端展示 — 已落地**:TaskDetail.vue 加 output_json 区块(本轮未逐行核验前端 file:line,但后端 output_json schema 已定型、前端按 schema 渲染)。
|
||||
>
|
||||
> **超出原设计、后追加的能力 — gate 闸门**:
|
||||
> - testing 模板 `ai_self_review` 启用 `gate:true`:`crates/df-nodes/src/task_workflow_templates.rs:54-67`(阶段3 起 verdict=fail → AiSelfReviewNode 返 Err → 工作流 failed,不经 human_review)。原设计 2.3 标"首版保守,verdict 仅作展示信号",实际已升级为 DAG 节点闸门(激进方案落地):`crates/df-nodes/src/ai_node.rs:598-626`(gate==true 时 verdict=fail 返 Err)。
|
||||
>
|
||||
> **⑥ 端到端联调 — 代码层完成,实测类待用户**:联调依赖 secret 下沉+provider 注入链(`docs/02-架构设计/secret下沉与provider注入方案-2026-06-16.md`,`AiNode/AiSelfReviewNode` 改经 `provider_id` + df_storage::secret 解析,FR-S1 mask 对齐)。实测类(tauri dev 跑 testing 模板 ai_self_review→human_review 闭环)待用户执行。
|
||||
>
|
||||
> **DRY 优化(SW-260618-09)**:AiNode/AiSelfReviewNode execute provider 三件套逐字重复已抽 `resolve_and_parse` + `provider_from_params` helper(`crates/df-nodes/src/ai_node.rs:70/80` 注释)。
|
||||
>
|
||||
> 原文以下设计正文保持不变,作为历史设计记录;落地形态以上方"实施状态"为准。
|
||||
|
||||
---
|
||||
|
||||
## 一、现状盘点(file:line 证据)
|
||||
|
||||
### 1.1 已就绪(自审闭环地基)
|
||||
|
||||
@@ -3,11 +3,34 @@
|
||||
> **真相源**(本文档唯一展开完整设计)。功能决策记录仅放摘要 + 指针。
|
||||
>
|
||||
> 背景:B-260614-03 — df-workflow `HumanNode` 假实现(`human_node.rs:55` 注释"等待审批"但首次迭代直接 return "同意")。
|
||||
> 状态:📐 **设计完成,未实施** | 创建:2026-06-14 | 来源:多代理探索
|
||||
> 状态:📐 设计完成 | 创建:2026-06-14 | 来源:多代理探索
|
||||
> 依赖:B-260614-06(execution_id 硬编码)、B-260614-07(每节点全新空 StateMachine)
|
||||
|
||||
---
|
||||
|
||||
> ## 实施状态(2026-06-18 核对)
|
||||
>
|
||||
> **B-03a(响应等待 + 超时)— 已落地**。`HumanNode.execute` 完整实现 subscribe → send(Request)(`.await` 修复 send 缺 poll 死 bug) → `select!` 循环(响应/超时/取消):
|
||||
> - `crates/df-nodes/src/human_node.rs:70` 先 subscribe;`:74-83` `.send(HumanApprovalRequest).await`(原审查报告头号 bug 已修);`:90-175` `select!` 循环(rx.recv() / sleep_until(deadline) / cancel_tick)。
|
||||
> - 单测覆盖:`human_node.rs:286 normal_approval_returns_decision` / `:339 mismatched_execution_id_filtered_then_timeout` / `:378 timeout_when_no_response` / `:388 invalid_decision_ignored_then_timeout` 等。
|
||||
>
|
||||
> **B-03b(取消机制)— 已落地**(原设计标"待做",实际已实施):
|
||||
> - `StateMachine::set_cancelled` 已加:`crates/df-workflow/src/state.rs:95`(注释为"唯一受控旁路")。
|
||||
> - `cancel_workflow_node` IPC 已加:`src-tauri/src/commands/workflow.rs:486`,并在 `src-tauri/src/lib.rs:107` 注册;含终态前置守卫(Pending/Running/Waiting 才允许 set_cancelled)。
|
||||
> - 前端取消按钮已接:`src/views/ProjectDetail.vue:425` + `src/stores/project/workflow.ts:131`(经 `src/api/workflow.ts:66` invoke)。
|
||||
> - B-07(共享 StateMachine)已解:`NodeContext.node_status` 为 `StateMachine` clone,内部 `Arc<Mutex<HashMap>>` 共享(`crates/df-workflow/src/executor.rs:39` 注释、`state.rs:88-95`);`run_workflow` 把执行器状态机注册到 AppState 全局表,IPC 经 execution_id 取引用直达运行中节点。
|
||||
> - 端到端测试:`human_node.rs:488 end_to_end_human_approval_completes_workflow` / `:551 end_to_end_human_approval_cancelled`。
|
||||
>
|
||||
> **超出原设计、后追加的能力**:
|
||||
> - F-260615-01 多选审批(`select_type=single|multiple` + `decisions` 数组):`human_node.rs:63-66` 解析、`:102-111` 数量/合法性校验、`src-tauri/src/commands/workflow.rs:404 approve_human_approval` 签名含 `decisions/select_type`。
|
||||
> - F-260616-06 阶段2 审批拒绝语义化:decision 命中拒绝关键字(`human_node.rs:19-22 REJECT_KEYWORDS`)→ 返 Err 触发工作流 failed(原设计拒绝与同意一样 Ok 的行为已反转)。
|
||||
>
|
||||
> **B-06(execution_id 下沉)— 未单独核验状态**,本设计标注当时为"dummy-execution-id";现 `approve_human_approval` IPC 签名已显式收 `execution_id: String`(`workflow.rs:407`),由调用方传入。是否已从 `run_workflow` 真 ID 下沉到 executor 再到 NodeContext,本次仅标注未深核。
|
||||
>
|
||||
> 原文以下设计正文保持不变,作为历史设计记录;落地形态以上方"实施状态"为准。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与问题
|
||||
|
||||
`HumanNode` 是工作流中唯一的阻塞节点,用于在 DAG 执行链路上插入人工确认门控(如"发布前确认""删除前确认")。当前实现 `crates/df-nodes/src/human_node.rs` 已正确发送 `WorkflowEvent::HumanApprovalRequest` 到事件总线,但**紧接着直接 `return NodeOutput { decision: "同意" }`**,从不等待前端审批响应。这导致:
|
||||
|
||||
@@ -8,6 +8,23 @@
|
||||
|
||||
---
|
||||
|
||||
> ## ⚠️ 实施状态(2026-06-18 核对:已落地,路由部分改方向)
|
||||
>
|
||||
> **阶段 1-6 全部落地**(数据模型 / 探测器 / 厂商拉取 / 路由器 / 7+ 调用点接入 / 前端):
|
||||
> - 阶段 1 数据模型:`crates/df-ai-core/src/model.rs:106` `ModelConfig`、`:20/35/55/71` 四维度枚举(Modality/Capability/CostTier/IntelligenceTier)、`:211` `deserialize_model_configs` 向后兼容;`crates/df-storage/src/models.rs:159-160` `AiProviderRecord.model_configs` 字段(**注意:实际落地字段名 `model_configs` 而非本文档 §1.1/§2.3 描述的老 `models` JSON 字段扩展**,新字段经 V18 迁移幂等补列 `crates/df-storage/src/migrations.rs:317-325`)。
|
||||
> - 阶段 2 探测器:`crates/df-ai/src/model_probe.rs`(已建)+ `crates/df-ai/presets/models.json`(已建)。注意预设表/PatternRule 未拆独立 `preset_table.rs`,合并在 model_probe 内。
|
||||
> - 阶段 3 厂商拉取:`crates/df-ai/src/model_fetch.rs`(已建,fetch_models 分派)。
|
||||
> - 阶段 4 路由器:`crates/df-ai/src/router.rs:40/56/76` `ModelRouter::select` / `select_model_id`。
|
||||
> - 阶段 5 调用点:主对话 `src-tauri/src/commands/ai/agentic.rs:418/423`、标题 `title.rs:86/91`、知识提炼/嵌入 `knowledge_inject.rs:58-63/359-364`、压缩 `compress.rs:59-64`、项目扫描 `project.rs:533-538/627-632`、灵感评估 `crates/df-ideas/src/adversarial.rs:158-163`、AiNode `crates/df-nodes/src/ai_node.rs:221-226`。
|
||||
>
|
||||
> **§6.1 路由逻辑已改方向(2026-06-18 决策 B-260618-03)**:本文档 §6.1 描述的 `TaskRequirements` 含 `min_intelligence`/`max_cost` 两字段、`select` 含「智力达标」「成本可控」两过滤步、`max_by_key((weight, Reverse(cost_tier)))` 同权重选便宜——**均已删除**。
|
||||
> - 实际形态:`crates/df-ai/src/router.rs:27-35` `TaskRequirements` 仅 3 字段(`modalities`/`needs_tool_use`/`estimated_context`),`min_intelligence`/`max_cost` 已删;`:56-63` `select` 过滤链仅 4 步(enabled / 模态 / 能力 / 窗口),`:62` 排序纯 `max_by_key(m.weight)`,无 cost tie-break。
|
||||
> - 根因:provider `/v1/models` API 不返回 cost_tier/intelligence,两维度 100% 靠预设表写死 + 模型名启发式猜,数据无客观依据不可信,不参与硬路由。枚举(CostTier/IntelligenceTier)保留在 `model.rs` 供未来出现真实判别源再接回。
|
||||
>
|
||||
> **§6.2 场景路由表 / §6.3 调用点表**:表中行号(如 `agentic.rs:50`/`title.rs:63`/`project.rs:378`)已漂移,实际调用点见上方阶段 5 行号清单。
|
||||
|
||||
---
|
||||
|
||||
## 0. 摘要
|
||||
|
||||
将当前「一个 Provider 一个 default_model 跑全场」升级为「多模型池 + 能力感知 + 智能路由」。
|
||||
|
||||
@@ -7,6 +7,27 @@
|
||||
|
||||
---
|
||||
|
||||
> ## ⚠️ 实施状态(2026-06-18 核对:阶段 1-2 已落地,数据模型形态偏离设计)
|
||||
>
|
||||
> **阶段 1(数据模型)+ 阶段 2(provider 适配)已落地**,阶段 3(前端)+ 阶段 5(F-06 联动)未做。
|
||||
>
|
||||
> **关键偏离:本文档 §2.1/§2.3 设计「`content: Vec<ContentPart>`」,实际落地改为「`content: String` + `parts: Option<Vec<ContentPart>>`」**(`crates/df-ai-core/src/provider.rs:97/105-106`)。
|
||||
> - 落地理由(见 `provider.rs:44-51` 注释):未接入多模态的调用方(audit/title/commands/knowledge_inject 等读 `content` 当字符串)零回归,避免一次性改全仓。content 字段始终保留人类可读文本,多模态片挂在 parts。
|
||||
> - 后果:本文档 §2.2(`deserialize_content` String→单 Text 片)、§2.3(构造器签名 `impl Into<String>` 改 `Vec`)、§2.4(`content_text()` 辅助)、§2.5(`truncate_for_persist` 改 Vec)描述均**不适用**——实际未改 content 类型,truncate 仍作用 String content,老调用点零改动。
|
||||
> - 实际辅助方法:`provider.rs:157` `user_parts(content, parts)`、`:162` `has_image()`、`:173` `flattened_parts()`(content 前置 Text 片 + parts 追加,供 provider 生成 blocks)。
|
||||
>
|
||||
> **已落地项(grep 佐证)**:
|
||||
> - `ContentPart` enum:`provider.rs:51-91`(Text/Image 两变体,含 `text()`/`image_base64()`/`image_url()`/`is_image()` 构造与判定)。
|
||||
> - **token 预算修正**:`crates/df-ai/src/context.rs:49-67` `estimate_message` 已把 parts 的 Image.base64 / Text.text 同 chars_ratio 计入(此前只算 content 致含图消息 token 严重低估 → build_for_request 误判未超预算 → provider 超限 400/500)。注意 `context.rs:57-58` 标注 0.35 比例偏高(CR-260618-11#2),偏保守致含图消息高估、过度裁剪,本次未改值仅标注。
|
||||
> - **provider 转换**:OpenAI 兼容 `crates/df-ai/src/openai_compat.rs:331-358`(has_image 走 `flattened_parts` → text/image_url 数组,纯文本走字符串简写零回归);Anthropic 兼容 `crates/df-ai/src/anthropic_compat.rs:344-372`(Image → `source.base64 + media_type`,Anthropic 不接受 URL 直传的设计约束落地)。
|
||||
>
|
||||
> **未做项**:
|
||||
> - 阶段 3 前端(`src/api/types.ts` ContentPart 类型对齐 / AiChat.vue 粘贴拖拽渲染 / store sendMessage payload)—— 未做。
|
||||
> - 阶段 5 F-06 联动(`commands/project.rs:509-543` extract_description_via_llm 消费 `sample.images` 喂 ContentPart::Image)—— 未做,仍走纯文本 prompt(对齐 §7 注「没图也能跑纯文本降级」)。
|
||||
> - 阶段 4 vision 路由:F-01 已落地,但 F-01 §6.1 路由已去 cost/intel 硬过滤(B-260618-03),vision 路由仍可按 `TaskRequirements.modalities=[Vision]` + `has_image()` 筛选候选模型,设计方向不变。
|
||||
|
||||
---
|
||||
|
||||
## 0. 摘要
|
||||
|
||||
将 `ChatMessage.content: String` 升级为 `Vec<ContentPart>{Text/Image}`,打通「前端粘贴/拖拽图片 → base64 上行 → OpenAI/Anthropic 兼容端点的 image_url/image blocks」全链路,并在 provider 转换层对非 vision 模型做文本降级。Phase 1(F-01 `ModelCapability`)落地后,由 `ModelRouter` 按 `has_image` 自动路由到带 vision 能力的模型;F-05 自身可在 F-01 未落地时先做「provider 静态白名单探测」独立跑通,最后接 F-01。同步解锁 F-06:`scan.rs::ImageRef` 现仅采集 alt+src,Phase 2 后可由 commands 层读 base64 喂 vision 抽 description。
|
||||
|
||||
@@ -7,6 +7,26 @@
|
||||
|
||||
---
|
||||
|
||||
> ## ✅ 实施状态(2026-06-18 核对:方案 B 全量落地)
|
||||
>
|
||||
> **推荐方案 B(df-storage 加薄 secret 查询方法)已全量实施**,行为与设计一致。
|
||||
>
|
||||
> **下沉层**:`crates/df-storage/src/secret.rs`(新建,纯密钥逻辑唯一源)—— `KEYRING_SERVICE` 常量(`:26`)、failcount sidecar(`:34-56`)、get/set/delete/resolve/ensure/migrate 全套函数均下沉至此。`crates/df-storage/src/lib.rs` `pub mod secret` 暴露。
|
||||
>
|
||||
> **df-storage 依赖**:`crates/df-storage/Cargo.toml:16-18` 已加 `keyring = { workspace = true }`(注释标注 FR-S1 密钥解析下沉)。
|
||||
>
|
||||
> **src-tauri 转发壳**:`src-tauri/src/commands/ai/secret.rs:20` `pub use df_storage::secret::*;`(12 调用点路径不变),`:32-43` 保留 `build_provider_for`(依赖 `df_ai::build_provider` 不下沉,对齐设计「留 app 层避免 df-storage→df-ai 循环」)。
|
||||
>
|
||||
> **AiNode 注入链(阶段 3)**:`crates/df-nodes/src/ai_node.rs:21` import `resolve_provider_secret`/`ensure_resolved_key`;`:60-64/101-139` 三路径解析(provider_id 优先 → 老明文兼容 → 空兜底取 is_default 首条);`:166-178` 从 record 解析 provider 构造要素(resolve→ensure→base_url/api_key)。AiSelfReviewNode 同构(`:476` 起)。
|
||||
>
|
||||
> **schema**:`ai_node.rs:344-351` schema 描述 provider_id/base_url/api_key(base_url/api_key 标注「已废弃过渡」),`:351` required=[](SW-260618-15:prompt/provider_id 均「留空走兜底」与 required 矛盾,改 required=[] 对齐运行时)。
|
||||
>
|
||||
> **设计偏离**:无功能性偏离。仅 schema required 值与设计 §3.4「`["provider_id"]`」略不同(实际 `required=[]`,对齐「留空走兜底」运行时语义,SW-260618-15 决策)。
|
||||
>
|
||||
> **本文档 §1.2/§五的 file:line 索引**:原指向 `src-tauri/src/commands/ai/secret.rs` 的函数行号(resolve:97-102 等)下沉后已迁移至 `crates/df-storage/src/secret.rs`,src-tauri 文件已瘦身为转发壳(原 228 行 → 现 43 行)。查阅实际函数请走 `crates/df-storage/src/secret.rs`。
|
||||
|
||||
---
|
||||
|
||||
## 一、现状核验(file:line 证据)
|
||||
|
||||
### 1.1 secret.rs 函数清单(src-tauri/src/commands/ai/secret.rs)
|
||||
|
||||
@@ -4,6 +4,34 @@
|
||||
|
||||
---
|
||||
|
||||
> ## 实施状态(2026-06-18 核对)
|
||||
>
|
||||
> **本设计文档已大面积过时,正文枚举表不再反映真实代码。** 以下为实际落地形态(以 `crates/df-types/src/types.rs` 为准):
|
||||
>
|
||||
> **类型契约机制 — 未采用 ts-rs 代码生成,仍手写 types.ts**:
|
||||
> - ts-rs 依赖:`Cargo.lock` 0 处、`crates/df-types/Cargo.toml` 无 ts-rs 依赖、全 crate 无 `build.rs`、无 `#[ts_rs]`/`#[derive(TS)]` 标注。memory「未做/手写 types.ts」属实。
|
||||
> - 前端类型手维护:`src/api/types.ts:1` 注释「TypeScript 类型定义 — 与 Rust Record 结构体严格对齐」。
|
||||
> - 「ts-rs 代码生成」仍列在 todo:`docs/todo.md:122`(ARC-260615-07 架构清理项之一)。
|
||||
>
|
||||
> **Crate 重命名 — df-core 改名 df-types 已完成**:
|
||||
> - workspace 下无 `crates/df-core` 目录(`ls` 核验 "df-core NOT FOUND");类型定义现居 `crates/df-types/src/{types.rs,events.rs,error.rs,lib.rs}`。
|
||||
> - 正文出现的 `df-core/src/types.rs` 路径全部应读作 `crates/df-types/src/types.rs`。
|
||||
>
|
||||
> **枚举对齐 — 正文 PascalCase 表全部过时,实际为 snake_case 序列化 + 枚举值数已变**:
|
||||
> - 正文写 `#[serde] PascalCase "Created"` 字符串值;实际 `crates/df-types/src/types.rs:51/89/130/195/233` 全部 `#[serde(rename_all = "snake_case")]`,前端/DB 存小写 snake_case。
|
||||
> - **TaskStatus**:正文 6 值(Created/BranchCreated/InProgress/ReviewReady/Merged/Abandoned) → 实际 **7 值**(`types.rs:131-146`):`todo / in_progress / in_review / testing / done / blocked / cancelled`。正文 6 个枚举名已无一存在。前端 7 态对齐见 `src/constants/project.ts:55`(D-260616-01)。
|
||||
> - **IdeaStatus**:正文 8 值(Draft/Evaluating/Scored/Hot/Promoted/Parked/Merged/Discarded) → 实际 **6 值**(`types.rs:52-65`):`draft / pending_review / approved / rejected / promoted / archived`。正文 8 个枚举名已无一存在。
|
||||
> - **ProjectStatus**:正文 4 值(Active/Paused/Completed/Archived) → 实际 **7 值**(`types.rs:90-105`):`planning / in_progress / testing / releasing / completed / paused / cancelled`。
|
||||
> - **WorkflowStatus**(正文误标 "WorkflowRunStatus"):正文 6 值(Pending/Running/Paused/Completed/Failed/Cancelled) → 实际 **6 值**(`types.rs:195-208`):`pending / running / paused / completed / failed / cancelled`(枚举名同正文,序列化改 snake_case,枚举名 WorkflowStatus 非 WorkflowRunStatus)。
|
||||
> - **NodeStatus / BranchStatus / Priority** 正文未列,实际见 `types.rs:233-248 / 272-281 / 301-312`。
|
||||
> - **ID 类型**:正文「所有 ID = UUID v4」与实际 `types.rs:10-28` `pub type XId = String` + `new_id()` 一致(未漂移)。
|
||||
>
|
||||
> **校验能力追加**:`TaskStatus::is_valid` / `valid_values`(`types.rs:165-183`)用于落库前拦截拼写错误(R-P1-5 修复),正文未涉及。
|
||||
>
|
||||
> 原文以下设计正文保持不变,作为历史设计记录;当前真实枚举/类型契约形态以上方"实施状态"为准,查阅请直接读 `crates/df-types/src/types.rs`。
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
DevFlow 前端 (TypeScript) 和后端 (Rust) 通过 Tauri IPC 和 JSON 序列化通信。两侧的类型定义必须保持一致。
|
||||
|
||||
@@ -359,13 +359,13 @@
|
||||
|
||||
> 注:UI 布局 localStorage + 模块级恢复的细节见 [归档文档](./功能决策记录-归档-2026-06-14.md)。
|
||||
|
||||
### 虚拟滚动:自研 IntersectionObserver + sentinel,不引 vue-virtual-scroller [2026-06-17]
|
||||
- **决策**:AI Chat 消息列表虚拟滚动选**自研方案**——IntersectionObserver + sentinel div(上下各一)+ ResizeObserver 监听容器变化,仅做渲染层裁剪(不可见窗口卸载内容,sentinel 占位保持 scrollHeight 不塌)。不选 vue-virtual-scroller 的 DynamicScroller。流式末条 pinnedKeys 保活不卸载。
|
||||
### 消息列表虚拟滚动:自研 → 彻底移除 [2026-06-17 → 2026-06-18]
|
||||
- **决策**:AI Chat 消息列表虚拟滚动选**自研方案**(IntersectionObserver + sentinel + ResizeObserver,仅渲染层裁剪)**→ 2026-06-18 彻底移除**(删 useAiVirtualScroll.ts + AiChat.vue 移除全链路),消息恒渲染。
|
||||
- **原因/取舍**:
|
||||
- **vue-virtual-scroller 破坏既有布局**——DynamicScroller 接管滚动容器内部 DOM 结构 + 重排子节点顺序,与现有 `.ai-messages` 的 `display: flex; flex-direction: column; gap: 14px` 布局冲突。同时破坏三处既有行为:① onMessagesScroll 的 wasNearBottom 边沿收起逻辑(依赖原生 scroll 事件和 scrollTop 计算);② isNearBottom / scrollToBottom / 「回到底部」按钮计算(依赖真实 scrollHeight);③ 流式生成时自动滚到底部(DynamicScroller 的滚动语义不同)。
|
||||
- **自研只做渲染裁剪**——不接管滚动容器、不改 DOM 结构、不动事件监听。sentinel div 用 `height: Npx` 占位保持 scrollHeight 准确,IO 触发时批量 mount/unmount 可见区间外消息组件。既有滚动/收起/流式/回到底部全零感知。
|
||||
- **pinnedKeys 保活**——流式生成中末条消息频繁更新,若被 IO 卸载再挂载会导致闪烁;pinnedKeys 集合内的消息跳过卸载判断。
|
||||
- **状态**:✅ 已落地(commit e38474b)
|
||||
- **不选 vue-virtual-scroller**——DynamicScroller 接管滚动容器 DOM + 重排子节点,破坏 `.ai-messages` flex/gap 布局 + onMessagesScroll(isNearBottom/scrollToBottom)/流式滚到底部既有逻辑。
|
||||
- **自研只做渲染裁剪**(原方案)——sentinel 占位保 scrollHeight,IO mount/unmount 可见区间外消息;pinnedKeys 保活流式末条。
|
||||
- **→ 彻底移除的取舍(2026-06-18)**:①**IO/RO 时序致重叠(移除主因)**——IO 判可见 + RO 测高度异步回调与 Vue 响应式交织,卸载分支 height=0 时 minHeight fallback 仍有竞态窗口(reply1 移出 pinned + bubble 重建时 RO/IO 捕获 height=0 → slot 塌 0 → 后续上移重叠);多次修 fallback(8abcd56)+ 禁用裁剪(0ca5d98)验证重叠仍偶发,shouldRender 恒 true 时 IO 仍设/清 sentinel inline minHeight 竞态源未除。②**消息量级不需要**——单会话几十条,恒渲染无性能问题。③**简化优于优化**——删 175 行 composable + AiChat 5 处调用,消除时序竞态源。
|
||||
- **状态**:✅ 2026-06-17 落地(e38474b)→ ❎ 2026-06-18 彻底移除(工作区待提交:删 useAiVirtualScroll.ts + AiChat.vue 移除 import/解构/setupVirtualScroll/watch lastStreamingRenderKey/template :ref+shouldRenderMsg 条件)
|
||||
|
||||
## 应用启动 / 数据库配置
|
||||
|
||||
|
||||
Reference in New Issue
Block a user