diff --git a/docs/02-架构设计/F-01-模型能力系统与智能路由设计-2026-06-16.md b/docs/02-架构设计/F-01-模型能力系统与智能路由设计-2026-06-16.md new file mode 100644 index 0000000..b13d9eb --- /dev/null +++ b/docs/02-架构设计/F-01-模型能力系统与智能路由设计-2026-06-16.md @@ -0,0 +1,621 @@ +# F-01 模型能力系统 Phase 1 — 模型配置与智能路由设计 + +> 状态:📐 设计定稿(2026-06-16,含多角度佐证 + 厂商适配 + 用户流程) +> 类型:架构设计文档(不碰任何 code) +> 关联:F-260614-05 多模态 Phase 2 / F-260614-04 多 Provider 负载均衡池 +> 前置:F-07 df-ai-core trait 下沉 ✅ 已完成(batch61·2069f79) +> 决策记录:见 [功能决策记录-2026-06-14.md](./功能决策记录-2026-06-14.md) §「模型能力系统」行 + +--- + +## 0. 摘要 + +将当前「一个 Provider 一个 default_model 跑全场」升级为「多模型池 + 能力感知 + 智能路由」。 +用户只需填 Provider 基本信息 → 自动拉取模型列表 → 自动探测每个模型的模态/能力/价格/智力 → 路由器按场景自动选最优模型。 + +**核心价值**:含图消息自动选 Vision 模型、标题生成自动选便宜模型、复杂推理自动选强模型、限流自动 fallback。 + +--- + +## 1. 现状盘点 + +### 1.1 数据模型(AiProviderRecord) + +`crates/df-storage/src/models.rs:127-140` + +```rust +pub struct AiProviderRecord { + pub provider_type: String, // "openai_compat" | "anthropic_compat" + pub base_url: String, + pub default_model: String, // 唯一实际使用的模型名 + pub models: Option, // JSON array of model names(展示用,未消费) + pub config: Option, // JSON extra config(几乎没用) + pub is_default: bool, + ... +} +``` + +**问题**: +1. `models` 存了模型列表但**没有能力标记**,不知道哪个支持 vision / tool use +2. **没有启用/禁用开关**,配了的模型无法控制哪些参与路由 +3. **没有权重/优先级**,无法表达"优先用 A,A 不可用时 fallback 到 B" +4. `default_model` 是唯一实际使用的模型名,`models` 列表形同虚设 + +### 1.2 模型选择机制(当前) + +``` +用户在 Settings 配置 Provider(含 base_url + default_model) + ↓ +AiSession.active_provider_id ← 全局唯一活跃 Provider + ↓ +get_active_provider() → 取 Provider 配置 + ↓ +CompletionRequest { model: provider.default_model } ← 直接用固定 model +``` + +- 一个时刻只有一个活跃 Provider +- 所有场景(对话/标题/知识提炼/项目扫描)共用同一个 model +- 无 ModelCapability / ModelRouter / 按场景路由机制 + +--- + +## 2. 模型配置数据模型 + +### 2.1 ModelConfig(每个模型的完整描述) + +```rust +/// 单个模型的完整配置(4 维度 + 路由控制) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ModelConfig { + // ── 基础 ── + /// 模型名(如 "glm-4v-flash") + pub model_id: String, + /// 启用/禁用(false = 配了但不参与路由,相当于"备档") + #[serde(default = "default_true")] + pub enabled: bool, + /// 用户自定义别名(可选) + #[serde(skip_serializing_if = "Option::is_none")] + pub label: Option, + + // ── 维度 1:模态(能接收什么输入)── + pub modalities: Vec, + + // ── 维度 2:能力(能做什么)── + pub capabilities: Vec, + + // ── 维度 3:价格(成本分级)── + pub cost_tier: CostTier, + + // ── 维度 4:聪明程度(智力分级)── + pub intelligence: IntelligenceTier, + + // ── 路由控制 ── + /// 权重(0-100,同能力候选中优先选权重高的) + #[serde(default = "default_weight")] + pub weight: u32, + /// 上下文窗口大小(tokens) + #[serde(default = "default_context_window")] + pub context_window: usize, + + // ── 探测元数据(只读,由 ModelProbe 填充)── + #[serde(skip_serializing_if = "Option::is_none")] + pub probe_source: Option, +} + +fn default_true() -> bool { true } +fn default_weight() -> u32 { 50 } +fn default_context_window() -> usize { 8192 } +``` + +### 2.2 枚举定义 + +```rust +/// 模态(输入类型) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum Modality { + Text, + Vision, + // 未来扩展:Audio, Video +} + +/// 能力 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum Capability { + ToolUse, // function calling / tool use + Embedding, // 向量嵌入 + CodeGen, // 代码生成强项 +} + +/// 价格分级 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum CostTier { + Free, // 免费 + Low, // flash / mini 系列 + Medium, // 标准定价 + High, // 旗舰定价 +} + +/// 聪明程度(智力分级) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum IntelligenceTier { + Lite, // flash/mini/lite/nano — 快但简单任务 + Standard, // air/standard — 日常够用 + Plus, // plus/pro/max — 复杂推理 + Ultra, // 最强模型 — 兜底用 +} +``` + +### 2.3 存储方式:扩展 models JSON 字段(零迁移) + +`AiProviderRecord.models` 从 `["glm-4-flash", "glm-4v"]` 升级为: + +```json +[ + { + "model_id": "glm-4-flash", + "enabled": true, + "modalities": ["text"], + "capabilities": ["tool_use"], + "cost_tier": "low", + "intelligence": "lite", + "weight": 90, + "context_window": 128000 + }, + { + "model_id": "glm-4v", + "enabled": true, + "modalities": ["text", "vision"], + "capabilities": ["tool_use"], + "cost_tier": "medium", + "intelligence": "plus", + "weight": 70, + "context_window": 128000 + } +] +``` + +**向后兼容**:自定义反序列化器,老格式 `["model-a"]` → 自动转成默认配置列表。 + +```rust +fn deserialize_models<'de, D>(d: D) -> Result, D::Error> +where D: serde::Deserializer<'de> { + let v = serde_json::Value::deserialize(d)?; + match v { + // 老格式:字符串数组 → 每个转默认 ModelConfig + serde_json::Value::Array(arr) if arr.iter().all(|v| v.is_string()) => { + arr.into_iter() + .filter_map(|v| v.as_str().map(|s| ModelConfig::with_defaults(s))) + .collect::>() + .pipe(Ok) + } + // 新格式:ModelConfig 数组 + serde_json::Value::Array(_) => { + serde_json::from_value::>(v).map_err(D::Error::custom) + } + _ => Ok(vec![]), + } +} +``` + +**否决方案**:新建 `ai_models` 独立表 — 增量复杂度高(迁移 + 新 Repo),`models` JSON 字段已够用。 + +--- + +## 3. 多角度佐证 + +### 3.1 行业对标 + +| 产品 | 模型配置方式 | 对照 | +|------|------------|------| +| OpenRouter | 每个模型标 modality/pricing/context_length | 4 维度完全覆盖,且加了 intelligence 分级 | +| Cursor | 内置模型能力感知(自动判断图片/工具) | 预设表 + 启发式探测本质相同 | +| LangChain ModelRouter | 按 max_tokens/supports_tool_use 路由 | capabilities + modalities 路由一致 | +| One-API / New-API | 纯渠道管理,无能力感知 | 能力标记是显著增量 | + +**结论**:4 维度配置是行业标配的超集,非过度设计。 + +### 3.2 路由器实际需求倒推 + +| 路由决策 | 必须知道 | 对应维度 | 缺了会怎样 | +|---------|---------|---------|-----------| +| 含图消息选哪个 | 是否支持 Vision | 模态 | 发给纯文本模型 → 400 | +| Agentic loop 选哪个 | 是否支持 tool_use | 能力 | 发 tool 定义给不支持的 → 忽略/报错 | +| 标题生成选哪个 | 够便宜够快 | 价格+智力 | 用旗舰模型 → 烧钱 | +| 复杂推理选哪个 | 够聪明 | 智力 | 用 flash → 质量差 | +| 上下文超长选哪个 | 窗口够大 | context_window | 超窗口 → 截断 | + +**结论**:每个维度直接对应路由器必须做的判断,缺一不可。 + +### 3.3 用户认知负担 + +| 信息 | 用户负担 | 说明 | +|------|---------|------| +| 模型名 | 零 | 从文档复制 | +| 模态 | 低 | 文档一眼可见,或名字带 v | +| 能力 | 低 | 文档明确标注 | +| 价格 | 中 | 查文档,但分级别直觉可判 | +| 智力 | 低 | flash < air < plus 是行业共识 | + +**结论**:4 个维度都是用户本来就知道的,加上自动填充,门槛极低。 + +### 3.4 可扩展性 + +| 未来需求 | 支持 | 扩展方式 | +|---------|------|---------| +| 音频输入 | ✅ | Modality 加 Audio | +| 视频输入 | ✅ | Modality 加 Video | +| 精确价格控制 | ✅ | cost_tier 升级为精确数值 | +| 流式支持差异 | ✅ | Capability 加 Streaming | +| 自定义能力标签 | ✅ | Capability 加 Custom(String) | + +**结论**:enum + Vec 开放式,未来加维度只需加变体。 + +### 3.5 数据兼容性 + +老 `models: ["model-a"]` → 自定义反序列化自动补默认值。`default_model` 字段保留向后兼容。零迁移脚本。 + +### 3.6 竞品缺陷反证 + +| 问题 | 竞品现状 | 我们的方案 | +|------|---------|-----------| +| 含图发给纯文本模型 | One-API 不感知能力 | 路由器自动选 Vision | +| 标题生成用旗舰模型 | 固定模型手动切 | 按 intelligence+cost 自动选 Lite | +| 新模型不知道能不能用 | 用户自己试 | 预设表 + 启发式自动推断 | +| 限流后无 fallback | 手动切换 | 按 weight 自动 fallback | + +### 3.7 实施风险 + +| 风险 | 评估 | 缓解 | +|------|------|------| +| 预设表维护 | 低(主流 20-30 个,半年更新) | JSON 配置文件可热更新 | +| 启发式误判 | 低(命名是行业惯例) | 标注 Medium 可信度,用户可修正 | +| 路由器选错 | 低 | weight fallback + 降级兜底 | + +**7 个角度一致指向:方案可行且合理。** + +--- + +## 4. 模型探测器(ModelProbe) + +### 4.1 多源探测 + +每个模型的信息从多个来源获取,交叉验证: + +```rust +pub struct ModelProbe { + preset_table: ModelPresetTable, +} + +impl ModelProbe { + pub async fn probe(&self, provider: &AiProviderRecord, model_id: &str) -> ModelProfile { + // 1. 启发式:从模型名推断 + let heuristic = self.heuristic_infer(model_id); + + // 2. 查预设表(精确匹配 > 模糊匹配) + let preset = self.preset_table.lookup(model_id); + + // 3. 多源合并(预设表 > 启发式) + let merged = self.merge_sources(heuristic, preset); + + // 4. 最终结果 + 标注每个维度的置信度 + self.finalize(merged) + } +} +``` + +### 4.2 探测优先级链 + +``` +用户手动设定 (UserSet) ← 最高优先,永不被覆盖 + ↓ +内置预设表精确匹配 (PresetTable) ← 高可信 + ↓ +模型名启发式 (Heuristic) ← 中可信,标注"建议确认" + ↓ +默认值 (Default) ← 低可信 +``` + +### 4.3 探测结果带置信度 + +```rust +pub struct ModelProfile { + pub model_id: String, + pub modalities: SourcedField>, + pub capabilities: SourcedField>, + pub cost_tier: SourcedField, + pub intelligence: SourcedField, + pub context_window: SourcedField, +} + +pub struct SourcedField { + pub value: T, + pub source: Source, + pub confidence: Confidence, +} + +pub enum Source { PresetTable, Heuristic, UserSet } + +pub enum Confidence { High, Medium, Low } +``` + +### 4.4 内置预设表 + +```rust +pub struct ModelPresetTable { + exact: HashMap, // 精确匹配 + patterns: Vec, // 模糊匹配 +} + +// 精确匹配示例 +"glm-4-flash" => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Lite, context:128K } +"glm-4-air" => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Standard } +"glm-4-plus" => { modalities:[Text], capabilities:[ToolUse], cost:Medium, intelligence:Plus } +"glm-4v" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Medium, intelligence:Plus } +"glm-4v-flash" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Lite } +"deepseek-chat" => { modalities:[Text], capabilities:[ToolUse], cost:Low, intelligence:Standard } +"deepseek-coder"=> { modalities:[Text], capabilities:[ToolUse,CodeGen], cost:Low, intelligence:Plus } +"claude-3-5-sonnet-20241022" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:High, intelligence:Ultra } +"claude-3-haiku-20240307" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Standard } +"gpt-4o" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:High, intelligence:Ultra } +"gpt-4o-mini" => { modalities:[Text,Vision], capabilities:[ToolUse], cost:Low, intelligence:Standard } +"embedding-3" => { modalities:[], capabilities:[Embedding], cost:Low, intelligence:Lite } + +// 模糊匹配规则 +PatternRule { keyword: "flash|mini|lite|nano", → Intelligence(Lite) } +PatternRule { keyword: "plus|pro|max|ultra", → Intelligence(Plus) } +PatternRule { keyword: "v$|vl|vision|-v\\d", → Modality(Vision) } +PatternRule { keyword: "embed", → Capability(Embedding) } +PatternRule { keyword: "code|coder", → Capability(CodeGen) } +``` + +预设表以 JSON 配置文件形式存储(`crates/df-ai/presets/models.json`),可热更新不随版本绑。 + +--- + +## 5. 厂商模型列表适配 + +### 5.1 差异盘点 + +| 厂商/协议 | 端点 | 鉴权 | 返回格式 | +|-----------|------|------|---------| +| OpenAI 兼容(GLM/DeepSeek/Kimi/通义) | `GET /v1/models` | Bearer | `{data:[{id, owned_by}]}` | +| Anthropic | `GET /v1/models` | x-api-key + version | `{data:[{id, display_name}]}` | +| 中转站 | `GET /v1/models` | Bearer | 同 OpenAI,但可能混入噪音或返回 404 | +| Ollama | `GET /api/tags` | 无 | `{models:[{name, size}]}` — 完全不同 | + +### 5.2 统一拉取接口 + +```rust +/// 模型拉取结果(统一格式) +pub struct FetchedModel { + pub model_id: String, + pub display_name: Option, +} + +/// 按 provider_type 分派 +pub async fn fetch_models( + provider_type: &str, + base_url: &str, + api_key: &str, +) -> Result, FetchError> { + match provider_type { + "openai_compat" => fetch_openai_compat(base_url, api_key).await, + "anthropic_compat" => fetch_anthropic_compat(base_url, api_key).await, + _ => Err(FetchError::Unsupported), + } +} +``` + +### 5.3 URL 智能拼接 + +```rust +fn build_models_url(base_url: &str) -> String { + let url = base_url.trim_end_matches('/'); + if url.ends_with("/chat/completions") { + url.replace("/chat/completions", "/models") + } else if url.ends_with("/v1") || url.ends_with("/v4") { + format!("{}/models", url) + } else if url.ends_with("/models") { + url.to_string() + } else { + format!("{}/v1/models", url) + } +} +``` + +### 5.4 噪音过滤 + +```rust +fn is_non_chat_model(id: &str) -> bool { + let id = id.to_lowercase(); + id.contains("dall-e") || id.contains("midjourney") // 图片生成 + || id.contains("tts") || id.contains("whisper") // 语音 + || id.contains("moderation") // 审查 + // embedding 保留(知识库需要) +} +``` + +### 5.5 失败降级 + +| 错误 | 处理 | +|------|------| +| 404 / 不支持 | 静默降级到手动输入(批量文本) | +| 401 / 403 | 提示密钥问题 | +| 网络/超时 | 提示检查连接 | +| 解析失败 | 提示格式不兼容,降级手动 | + +**核心原则:自动拉取是锦上添花,手动输入永远保底。** + +--- + +## 6. 模型路由器(ModelRouter) + +### 6.1 路由逻辑 + +```rust +pub struct TaskRequirements { + pub modalities: Vec, // 任务需要什么模态 + pub needs_tool_use: bool, // 是否需要工具调用 + pub min_intelligence: IntelligenceTier, // 最低智力要求 + pub max_cost: Option, // 成本上限(可选) + pub estimated_context: usize, // 预估上下文大小 +} + +impl ModelRouter { + pub fn select(&self, req: &TaskRequirements, pool: &[ModelConfig]) -> Option<&ModelConfig> { + pool.iter() + .filter(|m| m.enabled) // 1. 只选启用的 + .filter(|m| req.modalities.iter().all(|r| m.modalities.contains(r))) // 2. 模态匹配 + .filter(|m| !req.needs_tool_use || m.capabilities.contains(&Capability::ToolUse)) // 3. 能力匹配 + .filter(|m| m.intelligence >= req.min_intelligence) // 4. 智力达标 + .filter(|m| req.max_cost.map_or(true, |max| m.cost_tier <= max)) // 5. 成本可控 + .filter(|m| m.context_window >= req.estimated_context) // 6. 窗口够大 + .max_by_key(|m| (m.weight, -(m.cost_tier as i32))) // 7. 权重优先,同权重选便宜的 + } +} +``` + +### 6.2 场景路由表 + +| 场景 | 路由偏好 | 典型选择 | +|------|---------|---------| +| 标题生成 | Lite + Low cost | glm-4-flash | +| 日常对话 | Standard | glm-4-air | +| 写代码 | Standard/Plus + CodeGen | deepseek-coder | +| 含图片 | Vision + Standard | glm-4v | +| 复杂推理 | Plus/Ultra | glm-4-plus | +| 知识嵌入 | Embedding | embedding-3 | + +### 6.3 7 个调用点接入 + +| 调用点 | 当前代码位置 | TaskRequirements | +|--------|------------|-----------------| +| 主对话 stream_llm | agentic.rs:50 | 动态(按消息内容:含图→Vision) | +| 标题生成 | title.rs:63 | Lite + Low(省钱) | +| 知识提炼 | knowledge_inject.rs:33,323 | Standard | +| 知识嵌入 | knowledge_inject.rs embedding | Embedding | +| 项目扫描描述 | project.rs:378 | Standard(含图→Vision) | +| 灵感评估 | df-ideas adversarial.rs | Standard | +| AiNode 工作流 | df-nodes ai_node.rs:118 | 按节点配置 | + +--- + +## 7. 用户配置与使用流程 + +### 7.1 配置流程(做一次) + +``` +Step 1: 填 Provider 基本信息(名称/类型/地址/密钥) + ↓ +Step 2: 点「测试连接并拉取模型」 + ↓ 自动调 /v1/models(按协议分派) + ↓ 自动过滤非 chat 模型 + ↓ 自动探测每个模型特征(预设表 + 启发式) + ↓ +Step 3: 显示模型列表(已标注特征 + 可信度) + ↓ 用户确认或微调(大多数不需要进这步) + ↓ +Step 4: 完成 +``` + +**拉取失败时降级**:手动输入模型名(批量文本,每行一个)→ 同样走探测。 + +### 7.2 日常使用(零配置) + +``` +用户发消息 → 路由器自动选模型 → 用户无感 → 出结果 +``` + +**手动覆盖**:AiChat 顶部下拉选「自动(推荐)」或指定具体模型。 + +**自动 fallback**:模型限流/失败 → 按 weight 选次高 → 自动重试。 + +### 7.3 简洁性指标 + +| 传统做法 | 我们的做法 | 省了什么 | +|---------|-----------|---------| +| 手动填每个模型能力 | 预设表自动填充 | 省 90% 配置 | +| 自己记哪个支持图片 | 路由器自动选 | 省记忆 | +| 每次对话手动切模型 | 自动路由 | 省操作 | +| 限流后手动换 | 自动 fallback | 省故障处理 | + +**用户最小操作路径**:填 3 个字段 → 点 1 个按钮 → 完成。 + +--- + +## 8. 实施分阶段 + +### 阶段 1:数据模型(df-ai-core + df-storage) + +| 文件 | 改动 | +|------|------| +| `crates/df-ai-core/src/provider.rs` | 新增 ModelConfig / Modality / Capability / CostTier / IntelligenceTier | +| `crates/df-storage/src/models.rs` | AiProviderRecord.models 反序列化升级(兼容老格式) | +| `crates/df-ai/src/model_config.rs`(新建) | deserialize_models 兼容函数 + ModelConfig::with_defaults | + +### 阶段 2:预设表 + 探测器(df-ai) + +| 文件 | 改动 | +|------|------| +| `crates/df-ai/presets/models.json`(新建) | 主流模型预设数据 | +| `crates/df-ai/src/model_probe.rs`(新建) | ModelProbe + 启发式 + 多源合并 | +| `crates/df-ai/src/preset_table.rs`(新建) | 预设表加载 + 精确/模糊匹配 | + +### 阶段 3:厂商模型列表拉取(df-ai) + +| 文件 | 改动 | +|------|------| +| `crates/df-ai/src/model_fetch.rs`(新建) | fetch_models + openai_compat/anthropic_compat 分派 + URL 拼接 + 噪音过滤 | + +### 阶段 4:路由器(df-ai) + +| 文件 | 改动 | +|------|------| +| `crates/df-ai/src/router.rs`(新建) | ModelRouter + TaskRequirements + select 逻辑 | + +### 阶段 5:7 调用点接入(src-tauri) + +| 文件 | 改动 | +|------|------| +| `src-tauri/src/commands/ai/agentic.rs` | 主对话路由(按 has_image 动态选) | +| `src-tauri/src/commands/ai/title.rs` | 标题生成路由(Lite + Low) | +| `src-tauri/src/commands/ai/knowledge_inject.rs` | 知识提炼/嵌入路由 | +| `src-tauri/src/commands/project.rs` | 项目扫描路由 | +| `src-tauri/src/commands/ai/commands.rs` | 新增 IPC:ai_fetch_models / ai_probe_model | + +### 阶段 6:前端(src) + +| 文件 | 改动 | +|------|------| +| `src/api/types.ts` | ModelConfig 类型对齐 | +| `src/components/settings/` | Provider 配置页加模型列表 + 探测结果展示 + 启用/禁用/权重 | +| `src/components/AiChat.vue` | 顶部模型下拉(自动/指定) | + +--- + +## 9. 与 F-05(多模态)的关系 + +| 子能力 | 依赖 F-01? | 说明 | +|--------|------------|------| +| ContentPart 数据模型 | ❌ | 纯类型升级,与 ModelConfig 解耦 | +| provider content 数组化 | ❌ | 协议层改造,与路由器无关 | +| 前端粘贴/拖拽/渲染 | ❌ | 纯前端 | +| **vision 模型自动路由** | ✅ | ModelRouter 按 modalities 含 Vision 筛选 | + +F-05 可独立先做 80%(数据模型 + provider + 前端),vision 路由留到 F-01 落地后接入。 +过渡期靠 ModelConfig.modalities 手动标注 + 路由器筛选。 + +--- + +## 10. 不做的事(显式排除) + +- 不做模型自动下载/安装(仅 API 模型,不含本地模型管理) +- 不做模型 benchmark 自动跑分(智力分级靠预设表 + 用户确认) +- 不做多 Provider 负载均衡(属 F-04,本任务只做单 Provider 内多模型路由) +- 不做模型用量统计/成本告警(属运维增强,后续独立做) +- 不新建 ai_models 独立表(扩展 models JSON 字段够用) +- 本文档不实施任何 code(纯设计) diff --git a/docs/02-架构设计/F-05-多模态实施方案设计-2026-06-16.md b/docs/02-架构设计/F-05-多模态实施方案设计-2026-06-16.md index 3cbb295..a112791 100644 --- a/docs/02-架构设计/F-05-多模态实施方案设计-2026-06-16.md +++ b/docs/02-架构设计/F-05-多模态实施方案设计-2026-06-16.md @@ -104,7 +104,7 @@ pub struct ImageRef { ### 1.6 F-01(Phase 1)现状 -`crates/df-ai/src/model_capability.rs` / `router.rs` **均不存在**(Glob 确认)。决策记录(功能决策记录 §模型能力与路由)标 📐 设计未实施。F-05 因此不能假定 `ModelCapability.modalities` 已可用,必须给出「F-01 未落地」的过渡方案。 +`crates/df-ai/src/model_probe.rs` / `router.rs` **均不存在**(尚未实施)。但 **F-01 设计已定稿**(2026-06-16,见 [F-01-模型能力系统与智能路由设计-2026-06-16.md](./F-01-模型能力系统与智能路由设计-2026-06-16.md)),定义了 `ModelConfig.modalities` + `ModelRouter` 按 `has_image` 路由。F-05 阶段 1-3 可独立先做,阶段 4 vision 路由待 F-01 实施后接入;过渡期用「provider 配置的 vision 模型名静态白名单」。 --- diff --git a/docs/02-架构设计/F-09B-多会话并发设计-2026-06-16.md b/docs/02-架构设计/F-09B-多会话并发设计-2026-06-16.md new file mode 100644 index 0000000..605edd4 --- /dev/null +++ b/docs/02-架构设计/F-09B-多会话并发设计-2026-06-16.md @@ -0,0 +1,641 @@ +# F-260616-09 B 阶段 — AiSession 单例→多会话并发架构设计 + +> **状态**:设计稿(2026-06-16) | **关联**:[docs/待决策.md F-260616-09](../待决策.md) / [todo F-260616-12](../todo.md) / memory `aichat-arch-extensibility` +> **决策基线**(已定,勿推翻):a A 先隔离 + B 立项 / b 仅拆 generating+stop_flag per-conv(非整 HashMap) / c 复用 llm_concurrency.global 限并发会话数 / d1 侧栏 + d2 独立窗口都做(先 d1 后 d2) / e 切换不退出旧 loop 各自跑完(真并发) +> **范围**:仅本设计文档,不改代码。本文 file:line 证据均独立 grep/read 核验(不信文档/会话描述,防上下文污染)。 + +--- + +## 1. 现状盘点(AiSession 单例 + 字段消费点) + +### 1.1 单例定义与初始化 + +**单例锚点**(state.rs:165): +```rust +pub ai_session: Arc>, +// init: state.rs:216 ai_session: Arc::new(Mutex::new(AiSession::new())), +``` + +整个应用**一份** `AiSession`,被 `Arc>` 包裹,所有 IPC 命令竞争同一把锁。 + +**AiSession 字段**(mod.rs:166-216,共 10 字段): + +| 字段 | 行 | 类型 | 多会话语义 | +|---|---|---|---| +| `messages` | 168 | `ContextManager` | **会话级**(每对话一份历史),当前单例:切换走 `restore_from_messages` 覆盖 | +| `active_provider_id` | 170 | `Option` | 应用级(provider 全局共享),保持单例 | +| `active_conversation_id` | 172 | `Option` | **会话级**(当前活跃 conv 路由键),单例=只一个 active | +| `active_conv_created_at` | 174 | `Option` | **会话级**(懒创建时间戳,随 active_conv) | +| `pending_approvals` | 186 | `HashMap` | **会话级**(已带 `conversation_id` 字段 audit.rs:598/276),单例下靠 retain 过滤 | +| `generating` | 188 | `bool` | **会话级**(每对话独立流式态),单例=全局互斥 | +| `agent_language` | 190 | `Option` | **会话级**(每对话独立语言),单例下切换须清空 | +| `stop_flag` | 192 | `Arc` | **会话级**(每对话独立停止信号),单例=全局唯一 | +| `notify` | 203 | `Arc` | **会话级**(随 stop_flag 配对),单例=全局唯一 | +| `iteration_used` | 215 | `usize` | **会话级**(每对话独立计数,mod.rs:214 注释已标"F-09 B 改 per-conv") | + +**关键观察**:`mod.rs:214` 注释自身已写「**当前 AiSession 是全局单例(F-09 B 多会话架构落地时改 per-conv)**」——本设计是早已预留的债。 + +### 1.2 字段消费点 grep 核验(file:line) + +> 全部为 grep 实测命中,非文档转述。`state.ai_session.lock().await` 共 **41 处**(commands.rs 主集中)。 + +**`generating` 写/读点**(单例互斥根因): + +| 文件:行 | 操作 | 说明 | +|---|---|---| +| `commands.rs:48/51` | read+write | ai_regenerate: `if session.generating {return Err}` + `=true` | +| `commands.rs:125` | read | ai_is_generating IPC(前端 force_send 预检用) | +| `commands.rs:145/148` | read+write | **ai_chat_send 互斥入口**:`if generating {Err "正在生成中"}` + `=true` | +| `commands.rs:399/420` | read+write | ai_edit_last 同款互斥 | +| `commands.rs:505/534/540/565/602/636/644` | write | force_send/stop/continue/stop_loop 多路复位 | +| `commands.rs:848/850` | read+write | **ai_conversation_create:生成中强制结束旧会话**(B-260615-10 软复位) | +| `commands.rs:935` | read | ai_conversation_switch:**生成中 readonly 切换**(:935-941,后台 loop 不动 active) | +| `agentic.rs:117` | guard | GeneratingGuard RAII 收敛复位(:85-94 Drop 兜底) | +| `agentic.rs:585` | read | try_continue_agent_loop:读 generating + pending 决定续跑 | +| `lib.rs:43/46` | read+write | L0 握手:HMR/刷新清残留 generating | + +**`stop_flag` 读写点**(会话级停止信号): + +| 文件:行 | 操作 | 说明 | +|---|---|---| +| `agentic.rs:162` | clone | loop 入口 `session.stop_flag.clone()` 取 Arc 副本 | +| `agentic.rs:178/455` | load | loop 顶部 + stream 后查 stop_flag | +| `commands.rs:52/149/421/507/541/548/610/643/852/872` | store | send/regenerate/edit/stop/force/continue/create 多路置位 | +| `stream_recv.rs:141/171/321` | load | stream select! 内查 stop_flag | + +**`pending_approvals` 读写点**(会话级审批,已带 conversation_id): + +| 文件:行 | 操作 | 说明 | +|---|---|---| +| `commands.rs:236` | remove | ai_approve 按 tool_call_id 路由(精确键) | +| `commands.rs:347` | filter | ai_pending_tool_calls 按 conversation_id O(n) 过滤 | +| `commands.rs:365/506/539/851/868/948/969` | clear/retain | 多路清理 | +| `agentic.rs:583-585` | iter | try_continue:取任一审批的 conversation_id | +| `agentic.rs:593-597` | read | 审批等待态判断 | +| `audit.rs:269/593` | insert | restore_pending_approvals + process_tool_calls(均带 conversation_id) | + +**`agent_language` 读写点**(会话级语言): +- 写:`commands.rs:53/150/422`(send/regenerate/edit 入口设) +- 写:`commands.rs:873`(create 清空,F-260616-09 A 路线补漏) +- 读:`agentic.rs:650`(try_continue 恢复循环读) + +**`iteration_used` 读写点**(会话级计数,F-260616-11 落地): +- 读:`commands.rs:272/325`(ai_approve 两处续跑累计) +- 写:`agentic.rs:210`(loop 每轮 `iteration_used = iteration + 1`) +- 重置:`commands.rs:152`(ai_chat_send 新消息=0)/`ai_continue_loop`(达 max 续跑重计=0) + +### 1.3 run_agentic_loop 单例锁与陈旧 loop 退出逻辑 + +**loop signature**(agentic.rs:102-115)——`session_arc: Arc>` 持整个单例 Arc,loop 内全程竞争全局锁。 + +**B-260615-11 陈旧 loop 退出逻辑**(agentic.rs:194-211,427-434,380-388)——**多会话 B 阶段必须改造的核心点**: + +```rust +// agentic.rs:197-211 每轮开始校验对话一致性 +let mut session = session_arc.lock().await; +if session.active_conversation_id.as_deref() != Some(conv_id.as_str()) { + tracing::warn!("[ai] 对话已切换,旧 loop 退出(B-260615-11)避免污染新对话"); + return; // ← 决策 e「不退出各自跑完」:此处须删除/改判 +} +``` + +**当前语义**:loop 快照 `conv_id`(入参),每轮查单例 `active_conversation_id`,不一致即 return。 +- 这是为了**单例下防污染**:用户新建/切换会话,旧 loop 继续跑会 push 到新对话。 +- **决策 e 真并发下**:旧 loop 跑的是自己的 conv,messages/pending 已按 conv reload 或 per-conv,该退出**反成阻碍**——两个对话各持 conv_id,旧 loop 不应因 `active_conversation_id` 变更而退出。 + +同样的 push 前校验在 agentic.rs:380-388(MidStream 保文)/427-434(stream 后 push),三处一致——全部依赖单例 `active_conversation_id` 作「当前 loop 归属」判据,**B 阶段须改判据为 per-conv 的 session 索引**。 + +### 1.4 commands.rs create/switch/force_send 漏清与冲突 + +**ai_conversation_create**(commands.rs:835-876): +- 生成中强制结束旧 loop(:848-862 `generating=false` + `pending_approvals.clear()` + `stop_flag=true` + emit AiCompleted) +- F-260616-09 A 路线已补:`stop_flag.store(false)`(:872)+ `agent_language=None`(:873) +- **B 阶段冲突**:「新建会话强制结束旧 loop」与决策 e「不退出各自跑完」直接矛盾。B 阶段应**删除该强制结束块**,新会话直接开 per-conv state。 + +**ai_conversation_switch**(commands.rs:918-955): +- 生成中 `readonly: true`(:935-941)只返回 messages 不改 active——**单例下不得已的妥协** +- **B 阶段**:切换=前端切视图 + 后端切 active_conversation_id(应用级路由键),不触碰任一 conv 的 per-conv state。readonly 分支可删除(切走不杀 loop)。 + +**ai_chat_force_send**(commands.rs:494-523): +- 复位全局 generating + clear pending + stop_flag=true(:503-509) +- **B 阶段语义变化**:force_send 改为「针对**目标 conv** 强制复位其 per-conv generating」+「stop 该 conv 的 loop」,不影响其他 conv。 + +**ai_is_generating**(commands.rs:123-126): +- 返回全局 `session.generating`(单 bool) +- **B 阶段**:改为 `ai_is_generating(conv_id) -> bool`,前端 useAiWindow.ts:97 `resumeInDetached` 改传 conv_id。 + +### 1.5 ContextManager(messages)——已是会话级语义 + +`ContextManager`(context.rs:163-219)内部 `Vec` + token 缓存,无全局共享状态。 +- `push`/`clear`/`restore_from_messages`(context.rs:176/188/414)均是自包含操作。 +- **B 形态决策 b 的关键支撑**:`messages` 当前之所以单例,只是因为 AiSession 单例;一旦 generating/stop_flag 拆 per-conv,messages 也可随之 per-conv(HashMap),**侵入面与拆 generating 同量级**。 +- 决策 b 选「messages 走 conv reload」(不拆 per-conv)是为了**最小侵入**——保留 `restore_from_messages` 路径,切换时从 DB reload。**但真并发下旧 loop 还在往单例 messages push**,会污染刚 reload 的新 active conv。故**最终 B 形态建议**(见 §2.3)messages 也拆 per-conv,否则 §1.3 陈旧 loop 退出逻辑无替代判据。 + +### 1.6 llm_concurrency per_conv 当前语义(state.rs:93-133) + +**当前 per_conv 是应用级单一 Semaphore**(:101 `per_conv: Arc>>`,**非 HashMap**),state.rs:94-97 注释自陈: +> per_conv 当前是应用级单一信号量(非 per-conv map)。因 AiSession 为单例 + generating 互斥,同一时刻仅一个对话的 loop 在跑,per_conv 退化为"单对话内并发"(主循环 stream_llm + 标题生成 + 知识提炼)。**未来若支持多对话并发,需改为 HashMap。** + +- `global` Semaphore(state.rs:100)构造时 permits=3(state.rs:221 `LlmConcurrency::new(3, 2)`) +- **决策 c**:B 阶段 global 改「并发会话数上限」语义——原本限 LLM 调用并发,改限**并发会话数**(每会话占 1 permit 入口)。 +- per_conv 改 HashMap,每对话内限流(主循环 + 标题 + 提炼)。 +- **F-260616-12 依赖点**(todo:86):重试循环(agentic.rs:247-248)持有 global+per_conv permit 不释放。多会话后,重试期阻塞其他对话。B 阶段须处理(§3.3)。 + +### 1.7 前端现状 + +**stores/ai.ts**(模块级单例 reactive state,45-82): +- 会话级字段:`messages`/`streaming`/`currentText`/`generatingConvId`/`activeConversationId`/`pendingApprovals`/`queue`/`agentRound` +- **单 webview 共享**(注释:32-34),分离窗口是**独立 webview 各自 state**(useAiWindow.ts:32-34) +- **B 阶段 d1(侧栏)**:可保持单 store,只需支持「后台 conv 的 generating 态不被切走清零」(useAiEvents.ts:142-144 已按 conversation_id 路由 generatingConvId,基础设施就绪) +- **B 阶段 d2(独立窗口)**:每窗口一个 webview 一个 store,需窗口间状态同步(localStorage 快照 useAiWindow.ts:29 已有雏形) + +**useAiEvents.ts**(handleEvent :123-):**事件按 conversation_id 路由**已就绪(:125 `convId = event.conversation_id`),:133-140 isCurrent 判断决定是否污染当前视图。**conversation_id 全覆盖核验**见 §4。 + +**useAiWindow.ts**(1-191):`detachPanel`/`reattachPanel`/`resumeInDetached`/`dockDetached`/`syncToMain`/`startFollowMain` 已完整。单窗口(label='ai-detached'),detach 时快照 generatingConvId 到 localStorage(:28-31)。**d2 多窗口扩展点**:label 改为 `ai-detached-${convId}` 支持每会话独立窗口。 + +--- + +## 2. B 形态方案(决策 b:拆 generating/stop_flag per-conv) + +### 2.1 数据结构 + +**新增 `PerConvState`**(放 mod.rs,AiSession 内): + +```rust +/// 单个对话的运行态(generating/stop_flag/notify/iteration/agent_language)。 +/// 多会话并发后,每个 conv 一份,HashMap 挂在 AiSession。 +/// +/// 不含 messages/pending_approvals —— +/// - messages: 见 §2.3 决策(messages 最终也 per-conv,否则 §1.3 陈旧 loop 退出无替代判据) +/// - pending_approvals: 已带 conversation_id,继续走单层 HashMap + retain 过滤(mod.rs:181-185 注释自陈) +pub struct PerConvState { + pub generating: bool, + pub stop_flag: Arc, + pub notify: Arc, + pub iteration_used: usize, + pub agent_language: Option, + /// 该 conv 的 messages 历史(§2.3:messages per-conv 化后挪入此) + pub messages: ContextManager, + /// 懒创建时间戳(随 active_conv 走) + pub created_at: Option, +} +``` + +**AiSession 改造**(mod.rs:166): + +```rust +pub struct AiSession { + // ── 应用级(保持单例) ── + pub active_provider_id: Option, + pub active_conversation_id: Option, // 路由键:当前展示的 conv(切走即变,不影响后台 loop) + // ── 会话级(per-conv) ── + pub pending_approvals: HashMap, // 已带 conversation_id,保持单层 + pub per_conv: HashMap, + // ── L0 握手/A 路线兼容 ── + // 旧字段 generating/stop_flag/notify/agent_language/messages/iteration_used 移入 PerConvState +} +``` + +### 2.2 访问封装(收敛锁边界) + +**关键:避免 41 处 `session.lock()` 各自写 HashMap 索引**,提供访问器: + +```rust +impl AiSession { + /// 取某 conv 的 PerConvState(不存在则惰性创建) + pub fn conv(&mut self, conv_id: &str) -> &mut PerConvState { ... } + + /// 只读快照(供 IPC 查询,不创建) + pub fn conv_read(&self, conv_id: &str) -> Option<&PerConvState> { ... } + + /// 兼容旧调用:取 active_conversation_id 对应 conv(替换单例字段直读) + pub fn active_conv(&mut self) -> Option<&mut PerConvState> { + self.active_conversation_id.as_ref().and_then(|id| self.per_conv.get_mut(id)) + } +} +``` + +**调用点迁移**:41 处 `session.lock().await.xxx` → `session.lock().await.conv(&conv_id)?.xxx`,**锁粒度不变(仍是单一 AiSession Mutex)**,只是索引多一层。这是**最小侵入**的关键——不拆 N 把锁(否则锁顺序/死锁风险骤增),per-conv 仅是 HashMap 索引隔离。 + +### 2.3 messages 是否 per-conv(决策 b 的关键细化) + +**决策 b 原文**:「仅拆 generating/stop_flag per-conv,messages/pending 已可按 conv reload」。 + +**核验结论**:**messages 必须同 per-conv**,否则无法落地决策 e「不退出各自跑完」: +- 单例 messages 下,旧 loop(已切走的 conv A)继续跑会 push 到**当前 active conv B 的 messages**(单例一份)——这正是 §1.3 B-260615-11 退出逻辑防的污染。 +- 若 messages 保持单例,旧 loop 退出逻辑**必须保留**(决策 e 落空)。 +- 若 messages per-conv,旧 loop `session.conv(&conv_a).messages.push(...)` 写自己的 conv,**不污染 B**,退出逻辑可删(决策 e 成立)。 + +**故本设计修正决策 b 为**:「拆 generating/stop_flag/notify/iteration/agent_language/**messages** per-conv,仅 pending_approvals 保持单层 HashMap(已带 conversation_id)」。**侵入面增量极小**(messages 本就是 ContextManager 自包含结构,挪 HashMap 即可),但解锁了决策 e。**此偏离决策 b 原文的细化,须用户确认**(列入风险 R-1)。 + +### 2.4 锁粒度选型(单一 Mutex vs per-conv 细锁) + +**推荐:维持单一 `Arc>`**: +- per-conv 细锁(HashMap>)理论并发更好,但: + - 死锁风险:loop 内多次取锁 + 审批 IPC 取锁,锁顺序难保证 + - pending_approvals 是跨 conv 单层 HashMap,若它单锁而 per_conv 各自锁,两个锁域交叉 + - 现有 41 处 lock() 全要改取锁语义,侵入大 +- 单一 Mutex 下,per-conv 仅是数据隔离(写自己的 conv 不影响他人),**锁竞争点仍在**(并发 N 个 loop 抢一把锁),但: + - loop 内锁持有时间极短(push 一条消息 / 查 stop_flag 都是 O(1)),真实竞争窗口小 + - stream_llm 期间**不持锁**(agentic.rs:225-230 build messages 后释放锁,stream 内无锁) + - 改动量最小,风险最低 + +**结论**:数据 per-conv,锁单例。后续若 profiling 显示锁竞争,再细化到 per-conv Mutex(渐进路径)。 + +--- + +## 3. 并发限流(决策 c:复用 llm_concurrency.global 改会话级) + +### 3.1 global Semaphore 语义重定义 + +**当前**(state.rs:100/221):permits=3,限**全局 LLM 调用并发**(主循环 + 标题 + 提炼 + 所有对话共用)。 + +**B 阶段新语义**:permits=**并发会话数上限**(默认 3),每会话 loop 入口 acquire 1 permit,持有整个 loop 生命周期(含工具执行/审批等待)。 +- 含义:同一时刻最多 3 个对话并发跑 loop,第 4 个排队。 +- 收益:token 暴增护栏(决策 c 原意)。 +- 实现:`run_agentic_loop` 入口 `let _conv_permit = llm_concurrency.acquire_global().await;`,permit 绑 guard Drop 释放。 + +### 3.2 per_conv Semaphore 改 HashMap + +**当前**(state.rs:101):单一 Semaphore permits=2,限单对话内并发(主循环+标题+提炼)。 + +**B 阶段**: +```rust +per_conv: Arc>>>, +``` +- 每对话内限流不变(permits=2,主循环 + 标题 + 提炼),但按 conv_id 各自一份。 +- `acquire_per_conv(conv_id)`:lock HashMap → 若无则建(permits=2)→ clone Arc → 释放 lock → acquire_owned。 +- **conv 退出清理**:loop 结束 + 无 pending 审批时,remove 该 conv 的 Semaphore 条目(防 HashMap 无限增长)。 + +### 3.3 F-260616-12 retry 持 permit 依赖(关键风险点) + +**现状**(agentic.rs:247-248): +```rust +let _global_permit = llm_concurrency.acquire_global().await; +let _per_conv_permit = llm_concurrency.acquire_per_conv().await; +// 重试循环(:259-353)期间持有两 permit 不释放 +// stream 后(:414-415)才 drop +``` +注释(agentic.rs:246):「重试期间持有 permit 不释放(防新请求挤占)」。 + +**多会话后的问题**: +- global permit **新语义是会话级**(§3.1,每会话占 1 整 loop)。重试期持 global=**整个会话占着会话槽**,但重试只是该会话内部行为,**不应阻塞其他会话入槽**。 +- per_conv permit 是该 conv 内部限流,重试期持有合理(防自己挤占标题/提炼)。 + +**B 阶段处理方向**(对齐 todo:86 「倾向重试不持 permit 或仅持 per_conv」): +- **global permit 移出重试持有**:重试循环内**释放 global**(回到会话级占槽语义——入 loop 时 acquire 1 global 整 loop 持有,重试不影响他对话)。**注意**:若 global 新语义是「会话数上限」(§3.1),重试不释放 global 也无碍(本就是会话级占槽),问题降级。**真正要改的是 per_conv 重试持有**(见下)。 +- **per_conv permit 重试期释放**:重试不持 per_conv,退避 sleep + 重试请求每次重新 acquire per_conv(让该 conv 的标题/提炼有机会跑)。但当前架构无标题/提炼并发跑(都在 loop 退出后 spawn),故 per_conv 退化为单 permit,持有与否无差异。 +- **结论**:F-260616-12 在 B 阶段的实际影响**仅限 global 语义切换时核验**——新 global=会话级后,重试持 global=占会话槽(语义自洽,他对话第 4 个排队正常),**非 bug,是预期行为**。per_conv 重试持有无实际影响(无并发消费方)。**F-260616-12 可降级为「B 阶段验证 global 新语义后关闭」**,无需独立改动。列入风险 R-2。 + +--- + +## 4. loop 内校验改造(agentic.rs:177-211 改造点) + +### 4.1 B-260615-11 陈旧 loop 退出逻辑删除 + +**改造前**(agentic.rs:197-211): +```rust +let mut session = session_arc.lock().await; +if session.active_conversation_id.as_deref() != Some(conv_id.as_str()) { + return; // ← 删除 +} +session.iteration_used = iteration + 1; +``` + +**改造后**(决策 e 真并发): +```rust +{ + let mut session = session_arc.lock().await; + // B 阶段:不再校验 active_conversation_id(切换不杀 loop)。 + // per-conv state 按 conv_id 索引,旧 loop 写自己的 conv 不污染他人。 + // 校验改为:conv 是否仍存在(被删则退出) + if !session.per_conv.contains_key(&conv_id) { + tracing::warn!("[ai] conv {} 已删除,旧 loop 退出", conv_id); + return; + } + session.conv(&conv_id).iteration_used = iteration + 1; +} +``` + +同样改造 agentic.rs:380-388(MidStream 保文后 push)/427-434(stream 后 push):三处 push 前校验**全部改为 conv 存在性**而非 active 一致性。 + +### 4.2 stop_flag/notify/messages 取用改 per-conv 索引 + +**改造前**(agentic.rs:160-163): +```rust +let (stop_flag, notify) = { + let session = session_arc.lock().await; + (session.stop_flag.clone(), session.notify.clone()) +}; +``` + +**改造后**: +```rust +let (stop_flag, notify) = { + let session = session_arc.lock().await; + let conv = session.per_conv.get(&conv_id).expect("loop 启动前 conv 已建"); + (conv.stop_flag.clone(), conv.notify.clone()) +}; +``` + +**build_for_request 改 per-conv messages**(agentic.rs:224-230): +```rust +let messages = { + let session = session_arc.lock().await; + let conv = session.per_conv.get(&conv_id).expect("..."); + let (history_msgs, _trimmed) = conv.messages.build_for_request(sys_tokens); + // ... +}; +``` + +### 4.3 GeneratingGuard 改 per-conv 复位 + +**改造前**(agentic.rs:59-94):guard 持 `Arc>`,Drop 时 `session.lock().await.generating = false`(全局)。 + +**改造后**:guard 持 `conv_id: String`,`reset()` / Drop 时 `session.conv(&conv_id).generating = false`(只复位该 conv)。guard struct 加 `conv_id` 字段。 + +### 4.4 try_continue_agent_loop 改 per-conv 续跑 + +**改造前**(agentic.rs:578-650):读全局 generating + pending 决定续跑。 + +**改造后**: +```rust +pub(crate) async fn try_continue_agent_loop(app, state, conv_id: &str, start_iteration: usize) { + let (is_generating, has_pending) = { + let session = state.ai_session.lock().await; + let conv = match session.per_conv.get(conv_id) { + Some(c) => c, + None => return, // conv 已删 + }; + (conv.generating, + session.pending_approvals.values().any(|a| a.conversation_id.as_deref() == Some(conv_id))) + }; + // ... 续跑 spawn run_agentic_loop 传 conv_id +} +``` + +`ai_approve`(commands.rs:228-)调用处传 `approval.conversation_id`(:281 已有)。 + +--- + +## 5. 切换不退出旧 loop 各自跑完(决策 e 改造点) + +### 5.1 ai_conversation_switch 移除 readonly 分支 + +**改造前**(commands.rs:935-941):生成中 readonly 切换,不改 active。 + +**改造后**: +```rust +let mut session = state.ai_session.lock().await; +session.active_conversation_id = Some(conversation_id.clone()); // 直接切,不动 per-conv +// 旧 conv 的 per-conv state 保留(后台 loop 继续跑),新 conv 从 DB reload messages 到其 per-conv +let conv_state = session.per_conv.entry(conversation_id.clone()) + .or_insert_with(|| PerConvState::from_messages(messages)); // 惰性建/已存在则保留 +session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&conversation_id)); +// 删 readonly 分支 +``` + +### 5.2 ai_conversation_create 移除强制结束旧 loop + +**改造前**(commands.rs:848-864):生成中 `generating=false` + `stop_flag=true` 杀旧 loop。 + +**改造后**: +```rust +let mut session = state.ai_session.lock().await; +// B 阶段:新建会话不杀旧 loop(决策 e)。新会话建独立 per-conv state。 +let id = new_id(); +session.active_conversation_id = Some(id.clone()); +session.per_conv.insert(id.clone(), PerConvState::new()); // 全新 state +session.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(&id)); +// 删 if session.generating { 强制结束 } 块 +``` + +### 5.3 ai_chat_send 互斥改 per-conv + +**改造前**(commands.rs:144-152):`if session.generating {Err}` 全局互斥。 + +**改造后**: +```rust +let mut session = state.ai_session.lock().await; +let conv_state = session.conv(&conv_id); // 惰性建 +if conv_state.generating { + return Err("该对话正在生成中".to_string()); // 仅该 conv 互斥,他对话不受影响 +} +conv_state.generating = true; +conv_state.stop_flag.store(false, Ordering::SeqCst); +conv_state.agent_language = language.clone(); +conv_state.iteration_used = 0; +conv_state.messages.push(ChatMessage::user(&user_content)); +// active_conversation_id 仍设(路由键),但不影响其他 conv 的 loop +``` + +### 5.4 ai_chat_force_send / ai_chat_stop / ai_continue_loop / ai_stop_loop 改 per-conv + +所有这些命令**加 conv_id 参数**(前端传 activeConversationId),操作仅针对该 conv 的 per-conv state: +- `ai_is_generating(conv_id)` → `conv_read(conv_id).generating` +- `ai_chat_stop(conv_id)` → 置该 conv 的 stop_flag + notify +- `ai_chat_force_send(conv_id, ...)` → 复位该 conv 的 generating + 重发 +- `ai_continue_loop(conv_id)` / `ai_stop_loop(conv_id)` → 操作该 conv + +**IPC 签名变更**:需更新 commands.rs 的 `#[tauri::command]` 签名 + lib.rs:120 invoke_handler + 前端 api/ai.ts wrapper。 + +--- + +## 6. 事件路由 conversation_id 全覆盖核验 + +> 后端 emit 点全部 grep 核验。前端 useAiEvents.ts:125 已按 conversation_id 路由。 + +**AiChatEvent 变体**(mod.rs:88-141)conversation_id 字段核验: + +| 变体 | 行 | conversation_id | 核验 | +|---|---|---|---| +| AiTextDelta | :90 | `Some(conv_id)` | ✅ stream_recv 传 | +| AiToolCallStarted | :93 | `Some` | ✅ | +| AiToolCallCompleted | :96 | `Some` | ✅ | +| AiApprovalRequired | :101 | `Some` | ✅ audit.rs:604 process_tool_calls 传 | +| AiApprovalResult | :103 | `Some` | ✅ commands.rs:260/312 | +| AiCompleted | :115 | `Some` | ✅ agentic.rs 多处 + commands.rs:517/544 | +| AiError | :124 | `Some` | ✅ agentic.rs:143 | +| AiAgentRound | :127 | `Some` | ✅ agentic.rs:217 | +| AiHeartbeat | :129 | `Some` | ✅ stream_recv | +| AiMaxRoundsReached | :135 | `Some` | ✅ agentic.rs:519 | +| AiStreamRetry | :140 | `Some` | ✅ agentic.rs:346 | + +**结论**:所有 emit 点**已带 conversation_id**,前端 useAiEvents.ts:125 `convId = event.conversation_id` + :133 `isCurrent = !convId || convId === state.activeConversationId` 已就绪。**事件路由层零改动**,这是 B 阶段 d1 落地的前置保障。 + +**唯一新增 emit 点核验**:loop 内 push 失败/per-conv 复位的新 emit 须带 conv_id(§4 改造时逐一核对)。 + +--- + +## 7. 多窗口 UI(d1 侧栏 + d2 独立窗口) + +### 7.1 d1 侧栏切换 + 后台并行(先做) + +**现状基础**: +- useAiConversations.ts:56「允许生成中切换:后台继续生成,事件按 conversation_id 路由不污染当前视图」已就绪 +- useAiEvents.ts:142-144 `generatingConvId` 跟踪当前生成 conv(切走不清零) +- 侧栏会话列表 useAiConversations.ts:17 loadConversations 已就绪 + +**d1 改动点**: +1. **侧栏会话项显示生成态**:读 `state.conversations[].id` 与 `state.generatingConvId` 比对,匹配则显流式指示器(改动:`Sidebar.vue` 会话项 + 新 computed `generatingConvSet`) +2. **多 conv 后台并行态**:state 加 `generatingConvs: Set`(替代单值 generatingConvId),useAiEvents.ts:142-144 改 push 到 Set,Completed/Error 时 remove +3. **切走不清零**:useAiConversations.ts:38-44 newConversation 清 queue/generatingConvId 改为只清当前 conv 的(不杀他 conv) +4. **AiCompleted 触发 drainQueue**:useAiSend.ts:227 当前队列是单队列,B 阶段可保持(用户在 A 排队,A 完成续发),或改 per-conv 队列(后续优化) + +**d1 不需要**:多窗口、状态同步、localStorage 跨窗口。**d1 是纯前端 + 后端 §2-§5 改造的最小 UI 呈现层**。 + +### 7.2 d2 独立 Tauri 窗口(后做) + +**现状基础**(useAiWindow.ts): +- `WebviewWindow('ai-detached', ...)`(:42)单窗口 label +- detach 时快照 generatingConvId → localStorage `df-ai-gen`(:28-31) +- resumeInDetached(:87-119)核验 `ai_is_generating` 后恢复生成态 +- 独立 webview 独立 store(useAiWindow.ts:32-34 注释) + +**d2 改动点**: +1. **label 改 per-conv**:`ai-detached-${convId}`,支持每会话独立窗口(`getByLabel` 查重) +2. **detachPanel 加 conv_id 参数**:从指定 conv detach(而非当前 active) +3. **窗口间状态同步**:多窗口各自 webview 各自 store,需: + - 后端事件广播:Tauri `emit` 默认所有 webview 收(useAiEvents 在每窗口各自 listen,按 conversation_id 过滤)——**天然支持**,无需改 + - 显式状态同步(如某窗口改 provider):用 Tauri `emit` 自定义事件 + 各窗口 listen(useAiWindow.ts 已有 _unlistenMove/Resize 模式可仿) +4. **localStorage 快照**:从单 `df-ai-gen` 改 `df-ai-gen-${convId}` 多 key +5. **窗口管理 UI**:侧栏每会话加「在新窗口打开」按钮(右键菜单或图标) + +**d2 风险**:多窗口 + 多 webview 内存占用;窗口生命周期管理(关窗是否停 loop,决策 e 下不停)。 + +--- + +## 8. 实施步骤(分批,每批独立可验证) + +> 每批标注**主改文件**+**锁/回归面**。建议批间 cargo + vue-tsc 自验 + 人工验收单会话回归。 + +### 批 1:PerConvState 数据结构 + 访问器(无行为变更,纯重构) +- **主改**:`mod.rs`(新增 PerConvState struct + AiSession 字段迁移 + conv()/conv_read() 访问器) +- **锁**:无运行时行为变化(旧字段读写在批 2 迁移),仅编译期 +- **验证**:cargo build 通过 + 单测(访问器惰性建/已存在/conv 删除) +- **回归面**:全 AI IPC 编译(41 处 lock 暂不动,通过兼容层 `active_conv()` 转发到 per_conv[active_conversation_id]) + +### 批 2:41 处 lock 调用点迁移到 conv() 索引 +- **主改**:`commands.rs`(41 处)、`agentic.rs`(8 处)、`prompt.rs`/`audit.rs`/`knowledge_inject.rs`(各 1 处) +- **锁**:仍单 Mutex,索引多一层 +- **验证**:cargo build + **单会话全功能回归**(send/regenerate/edit/approve/stop/continue/switch/create/delete)行为零变化 +- **回归面**:全部 AI 功能(高风险批,须逐命令人工验收) + +### 批 3:run_agentic_loop per-conv 改造(§4) +- **主改**:`agentic.rs`(GeneratingGuard 加 conv_id + stop_flag/notify/messages 取用改 per-conv + §4.1 陈旧 loop 退出改 conv 存在性 + §4.4 try_continue 改 per-conv) +- **锁**:loop 内不改锁,改取用索引 +- **验证**:单会话回归 + **双会话并发验收**(开 conv A 跑 → 切 conv B 发 → A 后台跑完不污染 B) +- **回归面**:loop 全路径(send/regenerate/approve/continue/stop/max) +- **依赖**:批 2 完成 + +### 批 4:commands.rs 切换/新建/停止改 per-conv(§5) +- **主改**:`commands.rs`(ai_conversation_create 删强制结束块 / ai_conversation_switch 删 readonly / ai_chat_send 互斥改 per-conv / ai_chat_stop/force/continue/stop_loop 加 conv_id 参数) +- **IPC 签名变更**:lib.rs invoke_handler + 前端 api/ai.ts wrapper + useAiSend.ts/useAiConversations.ts 调用处 +- **验证**:双会话并发(切走不杀旧 loop,各自跑完)+ 单会话回归 +- **回归面**:全部 IPC 签名(前端多处调用) +- **依赖**:批 3 完成 + +### 批 5:llm_concurrency 改造 + F-260616-12 处理(§3) +- **主改**:`state.rs`(LlmConcurrency global 语义=会话级 + per_conv 改 HashMap)、`agentic.rs`(loop 入口 acquire global 整 loop 持有 + 重试期 global 释放语义核验) +- **锁**:Semaphore 改造 +- **验证**:3 会话并发(第 4 排队)+ 重试期不阻塞他对话(F-260616-12 验证) +- **回归面**:并发限流配置(Settings 热改路径 set_global/set_per_conv 适配 HashMap) +- **依赖**:批 4 完成 + +### 批 6:d1 前端侧栏 + 后台并行(§7.1) +- **主改**:`stores/ai.ts`(generatingConvs: Set)、`useAiEvents.ts`(generatingConvId → generatingConvs)、`Sidebar.vue`(会话项生成态指示器)、`useAiConversations.ts`(切走不清零) +- **验证**:双会话并发 UI(侧栏见两 conv 流式指示器,切走后台继续) +- **回归面**:侧栏渲染 + 事件路由 +- **依赖**:批 4 完成(后端 per-conv 就绪) + +### 批 7:d2 独立 Tauri 窗口(§7.2) +- **主改**:`useAiWindow.ts`(label per-conv + detachPanel 加 conv_id + localStorage 多 key + 窗口间同步)、`Sidebar.vue`(新窗口打开按钮) +- **验证**:每会话独立窗口 + 多窗口并发 + 窗口间状态同步 +- **回归面**:窗口生命周期 + 内存 +- **依赖**:批 6 完成 + +### 批 8:启动恢复 + L0 握手适配 +- **主改**:`audit.rs`(restore_pending_approvals 按 conversation_id 分组重建到对应 per_conv)、`lib.rs`(L0 握手清所有 per_conv 的 generating,非全局单值) +- **验证**:重启恢复多 conv pending 审批 + HMR 清多 conv 残留 +- **回归面**:启动恢复链路 +- **依赖**:批 2 完成(可与批 3-7 并行) + +--- + +## 9. 风险清单 + +### R-1:决策 b 细化偏离(messages 必须同 per-conv)— **须用户确认** +- **原决策 b**:「仅拆 generating/stop_flag per-conv,messages/pending 已可按 conv reload」 +- **核验结论**:**messages 必须同 per-conv**,否则 §1.3 B-260615-11 陈旧 loop 退出逻辑无替代判据(单例 messages 下旧 loop 必污染新 active conv),决策 e「不退出各自跑完」落空。 +- **侵入面增量**:极小(messages 本就是 ContextManager 自包含,挪 HashMap),但偏离原文须确认。 +- **缓解**:pending_approvals 保持单层 HashMap(已带 conversation_id,符合决策 b 原意),仅 messages per-conv 化。 + +### R-2:锁粒度——单 Mutex 多 conv 抢锁 +- **现状**:41 处 `session.lock()` 共一把 Mutex。 +- **B 阶段**:per-conv 仅数据隔离(HashMap 索引),锁仍单例。N 个并发 loop 抢一把锁。 +- **评估**:loop 内锁持有时间极短(push/查 flag 均 O(1)),stream 期间不持锁,真实竞争窗口小。**风险可控**。 +- **缓解**:若 profiling 显示竞争,再细化 per-conv Mutex(渐进,不阻塞 B 落地)。 + +### R-3:事件路由——已就绪但新增 emit 须核对 +- **现状**:11 个 AiChatEvent 变体全部已带 conversation_id(useAiEvents.ts:125 已路由)。 +- **B 阶段**:零改动,但 §4-§5 改造新增 emit 点须逐一核对带 conv_id。 +- **缓解**:批 3/4 改造时 grep emit 点 checklist。 + +### R-4:状态同步——前端 generatingConvId 单值 → Set +- **现状**:state.generatingConvId 单值(stores/ai.ts:51)。 +- **B 阶段 d1**:改 Set(多 conv 并行生成)。useAiEvents.ts:142-144 写入逻辑 + 各清零点(Completed/Error/:136/:273/:317)全改。 +- **风险**:漏改清零点致生成态残留。 +- **缓解**:批 6 集中改 + 双会话并发 UI 验收。 + +### R-5:回归面——全 AI 功能(高风险批在批 2/批 4) +- **批 2**(41 处 lock 迁移):全 AI IPC 编译 + 单会话全功能回归。 +- **批 4**(IPC 签名变更 conv_id 参数):前端多处调用 + 后端全部命令。 +- **缓解**:批间 cargo + vue-tsc + 人工验收单会话回归(逐命令:send/regenerate/edit/approve/stop/continue/switch/create/delete/rename/archive)。批 2 是最高风险批,建议单独 commit + 充分验收。 + +### R-6:F-260616-12 retry 持 permit——global 语义切换后核验 +- **现状**:重试期持 global+per_conv permit(agentic.rs:247-248)。 +- **B 阶段**:global 改「会话级」语义(§3.1)后,重试持 global=占会话槽(语义自洽,第 4 会话排队正常),**非 bug**。per_conv 重试持有无实际影响(无并发消费方)。 +- **结论**:F-260616-12 **降级为批 5 验证项**(global 新语义下行为符合预期即关闭),无需独立改动。**若用户认为重试期占会话槽不合理**,则在批 5 重试循环内释放 global(每次重试重新 acquire),但会引入「重试期会话槽空转被他对话抢占」新问题——不推荐。 + +### R-7:启动恢复 + L0 握手多 conv 适配 +- **restore_pending_approvals**(audit.rs:254):当前重建到全局 pending_approvals(单层 HashMap),B 阶段保持(已带 conversation_id),但须确保各 conv 的 per_conv state 在启动时建(惰性 or 全量)。 +- **L0 握手**(lib.rs:42-63):当前清全局 generating + pending。B 阶段改清所有 per_conv 的 generating(防 HMR 后多 conv 残留)。 +- **缓解**:批 8 集中处理。 + +### R-8:iteration_used 跨审批续跑在多会话下的正确性 +- **现状**(F-260616-11):iteration_used 累计,ai_approve 读 session.iteration_used 续跑。 +- **B 阶段**:iteration_used 挪入 PerConvState(§2.1),ai_approve 读 `conv(approval.conversation_id).iteration_used`。 +- **风险**:ai_approve commands.rs:272/325 两处读须改 per-conv 索引(批 2 一并迁移)。 +- **缓解**:批 2 checklist 含此两处。 + +### R-9:PendingApproval.conversation_id 一致性 +- **现状**:process_tool_calls(audit.rs:598)写 `Some(conv_id.to_string())`,restore_pending_approvals(audit.rs:276)从审计表读 `rec.conversation_id`。 +- **B 阶段**:保持单层 pending_approvals + 按 conversation_id 过滤(ai_pending_tool_calls commands.rs:347 已 O(n) 过滤)。 +- **风险**:若某审批的 conversation_id 为 None(历史数据/边界),retain/filter 漏判。 +- **缓解**:批 2 核验所有 insert 点均带 Some(conv_id);None 视为「无主审批」归类到 active conv 或单独清理。 + +--- + +## 10. 与已决架构债的关系 + +- **memory `aichat-arch-extensibility`**:「AiSession 单例未动」是登记的架构债,本设计正式清偿。 +- **mod.rs:214 注释**:「当前 AiSession 是全局单例(F-09 B 多会话架构落地时改 per-conv)」——预留债标记,本设计落地。 +- **state.rs:94-97 注释**:per_conv「未来若支持多对话并发,需改为 HashMap」——本设计 §3.2 落地。 +- **不冲突**:本设计与 F-260616-11(iteration 累计,已 per-conv 化预留)、F-260616-07(流式重试,F-260616-12 依赖项 §3.3 处理)、generating 状态机加固(RAII guard,§4.3 改 per-conv)均兼容。 + +--- + +## 11. 验收清单(每批完成后) + +- [ ] 批 1:cargo build + 访问器单测 +- [ ] 批 2:cargo + vue-tsc 0err + 单会话全功能回归(send/regenerate/edit/approve/stop/continue/switch/create/delete/rename/archive 逐命令) +- [ ] 批 3:双会话并发(开 A 跑 → 切 B 发 → A 后台跑完不污染 B)+ 单会话回归 +- [ ] 批 4:双会话并发(切走不杀旧 loop,各自跑完)+ 全 IPC 签名前端调用核对 +- [ ] 批 5:3 会话并发(第 4 排队)+ 重试期不阻塞他对话(F-260616-12) +- [ ] 批 6:双会话并发 UI(侧栏两 conv 流式指示器,切走后台继续) +- [ ] 批 7:每会话独立窗口 + 多窗口并发 + 窗口间状态同步 +- [ ] 批 8:重启恢复多 conv pending 审批 + HMR 清多 conv 残留 + +--- + +**设计完。本文件为设计稿,不含代码改动。实施时按批 1→8 顺序,每批独立可验证。** diff --git a/docs/todo.md b/docs/todo.md index 4335f56..b964acc 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -85,6 +85,14 @@ - [ ] F-260616-12 [P2/依赖F-09] — **retry 持 permit 不释放(多会话隐患)**。F-260616-07 落地的流式重试循环(`agentic.rs:238-239`)重试期间持有 global+per_conv permit 不释放(注释「防新请求挤占」)。当前 AiSession 单例 + 主 loop 串行无影响,但 **F-260616-09 多会话并发后**,重试期间阻塞其他对话 LLM 调用。**方向**:多会话落地时核对——主 loop 串行下重试持 permit 防自己挤占无意义,倾向重试不持 permit 或仅持 per_conv。— agentic.rs:238 + F-260616-09 多会话架构。**依赖 F-260616-09 立项后一并处理**。 +### 💡 2026-06-16 新需求(UX 交互优化·分析完成·待实施) + +> 用户实时反馈的交互优化需求,已走查定位链路+方案记录。 + +- [ ] UX-260616-01 [P2] — **工具调用失败时「重试」提示语义模糊**。用户场景:Run Command 执行失败(exit_code 255,`head` not recognized on Windows),UI 显示「⚠ 调用失败,正在重试(1/4)…」+「重试」按钮。**根因分析**:「正在重试(n/m)」是 `AiStreamRetry` 事件(F-260616-07 流式 LLM 重试),通过 `useAiEvents.ts:177-186` 更新错误气泡内容;工具执行失败(run_command 等 Low 风险工具)走 AR-6 路径(`audit.rs:640-652`)→ emit `AiToolCallCompleted`(result=错误信息)→ **不触发 AiError 不触发 AiStreamRetry**,错误包在 tool_result 回传 LLM 自行决策。**问题**:①用户看到的是「LLM 流重试」提示,非「工具执行失败」提示,语义错位 ②确定性失败(命令语法错/exit_code 非0)重试同命令必再败,「重试」按钮误导 ③工具实际结果(stdout/stderr/exit_code)已显示在 ToolCard 内,错误气泡的「重试」是消息级 regenerate(重新生成整条回复),非工具级重试。**改动方向**(待定):a)错误气泡区分两类——流式重试中显示「正在重试(n/m)…」(现有);工具执行失败不弹错误气泡(结果已在 ToolCard)或气泡文案改为「工具执行失败,查看上方结果」b)Fatal 类错误(4xx/鉴权/参数)气泡去掉「重试」按钮或改为「去设置」c)`AiStreamRetry` 事件更新气泡时附带 `retryable` 标识,前端据此显隐重试按钮。—— useAiEvents.ts(:177-186 AiStreamRetry case) + AiChat.vue(错误气泡 UX-03 操作栏) + i18n ai.aiStreamRetry +- [ ] UX-260616-02 [P3] — **「全部收起」与搜索/技能区合并一行**。当前 ToolCardList.vue 有两个独立行:①batch-approve 栏(line 4-11,pendingCount>0 时显示)②global-toggle 栏(line 14-17,collapsibleGroupCount>0 时显示「▾ 全部收起」)。用户要求将「全部收起」与附近的操作元素(search files 搜索文件/技能触发等)放到同一行,减少垂直空间占用。**需确认**:「search files」具体指哪个 UI 元素(i18n `ai.searchFiles` 渲染位置需定位,可能在输入框上方 skill 栏或工具卡区域)。**改动方向**:global-toggle 从独占行改为 inline 元素,与相邻操作栏 flex 同行。—— ToolCardList.vue(:13-17 template + :278-295 CSS .ai-tool-global-toggle) +- [ ] UX-260616-03 [P2] — **对话内容输出时自动收起旧工具卡片分组**。需求:当 AI 输出新内容(流式 delta / 新工具调用 / 新轮次)滚动到下方时,上方已完成的旧消息中的工具卡片分组自动收起,保持视野聚焦当前内容。**现状**:机制已存在——`ToolCardList.collapseInactive(activeIds)`(:191-199)供父组件调用,`AiChat.vue:1657-1661` 已有 watch 调用(refs 数组逐实例 collapseInactive)。**增强方向**:a)触发时机扩展——当前可能仅在特定时机调用,可扩展到:`AiTextDelta` 新消息开始时 + `AiAgentRound` 新轮次时 + 用户滚动接近底部时(跟随阅读位置自动收起已读内容)b)平滑过渡——收起加 CSS transition(高度动画 200ms)避免内容突然消失跳变c)可选:记忆用户手动展开的分组不自动收起(expandedCards Set 区分用户主动展开 vs 默认态)。—— ToolCardList.vue(collapseInactive + CSS transition) + AiChat.vue(watch 触发时机扩展) + - [x] ✅(batch60·2026-06-16·workflow whae812z5+主代核查,cargo 0err) F-260616-13 [P2] — **build_for_request 持锁重活 + system_prompt token 每轮重估**。性能分析批次发现:每轮 `stream_llm` 前 `session_arc.lock()`(`agentic.rs:251`)持锁期间做 `TokenEstimator::estimate_text(system_prompt)`(L252)+ `build_for_request`(L253 history clone + 裁剪)。system_prompt loop 外固定传入,**每轮重估其 token 是浪费**(可缓存);build_for_request 持锁做 history clone 是重活,消息多时(200 cap)锁持有期长。**方向**:①system_prompt token loop 外算一次缓存 ②build_for_request 先 clone messages 释放锁再裁剪。低收益优化。— agentic.rs:251-253。 - [x] ✅(主代核验·2026-06-16) F-260616-14 [P2/核验] — **max_tokens 8192 截断 + 前端批量审批核验**。核验结论:①`max_tokens=8192`(实际 `agentic.rs:270`,行号漂移)≈6-8k 字日常够用,长回复截断由 batch59 MidStream 保文兜底不丢文,**可配低优先非阻塞**(登记可选优化)②前端批量审批 **AE-2025-01 已完整闭环**(ToolCardList.vue:4-11 `ai-batch-approve` 栏 + AiChat.vue:444 `@batch-approve`→`store.batchApprove` + useAiSend.ts:338 遍历 pendingApprovals 逐个 approveToolCall + i18n approveAll/rejectAll),满足「一轮多 Med/High 全 insert pending→一次批量批全部」③audit.rs:496 `process_tool_calls` 一轮多工具 Med/High 进审批门控 insert pending + Low 并行执行链路通。无阻塞 bug,销账。— agentic.rs:270 + ToolCardList.vue + audit.rs:496。 @@ -128,7 +136,13 @@ - **实施顺序**:④-1→②-1→②-5→②-2→②-3→②-4→②-6→①-1/①-3。**最小里程碑**(④-1+②-1+②-2+②-3):run_workflow 单 task_advance 节点 DAG 端到端推进 todo→in_progress - [ ] F-260616-07 阶段3 AI 执行闭环 — **F-03 收口三件 ✅ 本批完成**(batch64), AiNode 自审闸门 ⏳ 待后续批 - [x] ✅(batch64·2026-06-16·workflow wii1u1lnm) **F-03 收口三件** — ①advance_task 注册 AI 工具(tool_registry.rs:395,handler L407 调 `df_nodes::task_advance_node::advance_task_atomic` 与 IPC `commands::task::advance_task:165` 同源) ②run_workflow 注册 AI 工具(:428,handler 架构约束无 AppHandle/State 报错引导走 IPC,ToolDefinition+审批文案 L1260-1261/1286-1287 注册让 LLM 可产出 tool_call) ③update_task handler guard 拒 status(L374 `field=="status"` bail,schema 通用 field/value 故 guard 拦非 schema 改)+ df-storage tasks 白名单移 status+review_rounds(crud.rs:331-347,advance_status_atomic CAS L848 独立路径不经白名单)。同步落地防 AI 工具行为不一致。**主代独立核查全过**:cargo check --workspace EXIT 0(5 pre-existing warnings 无关)+ df-storage 11 集成测试(含新 `update_field_rejects_tasks_status`)。文件锁:tool_registry.rs + crud.rs + project_soft_delete.rs。**审查登记 CR-260616-41**(待审查.md 当前队列) - - [ ] AiNode 自审闸门(阶段3 核心,需设计) — 待后续批 + - [ ] AiNode 自审闸门(阶段3 核心,✅ 决策a 已定 2026-06-16:TaskRecord 加 output_json) — 待后续批 + - [ ] df-storage: TaskRecord 加 `output_json: Option` 字段 + V18 迁移(ALTER TABLE tasks ADD COLUMN output_json TEXT) + Repo update 方法 + - [ ] df-nodes: ai_node.rs ai_execute 执行后写产出到 task.output_json(通过 NodeContext 拿 task_id + DB 句柄) + - [ ] df-nodes: ai_node.rs ai_self_review 读 task.output_json 构建自审 prompt(需求符合度+产出完整性 → 通过/不通过+理由+建议) + 自审结果写回 output_json + - [ ] df-workflow: run_workflow 注入 task 数据到 NodeContext(config 注入 task_id + provider 配置,④-1 deep_merge 已就绪) + - [ ] df-nodes: human_node.rs human_review 读 task.output_json 展示产出+自审结果给人核 + - [ ] 前端: TaskDetail.vue 展示 output_json(产出+自审结果) - [ ] run_workflow handler 注入 AppState(当前报错引导走 IPC) — 后续批扩展 build_ai_tool_registry 签名注入 AppState 句柄让 AI 直驱 - [x] ⏸️(待决策.md已决b暂缓·2026-06-16) F-260616-08 阶段4 Git 集成(kind+git闸门+worktree) - [x] ✅ **CR-260616-01 代码审查完成** → 审查登记已迁 [待审查.md](./待审查.md)(职责分离:审查队列独立,不进 todo)。结论 🔴0 🟡6 ⚪4 质量优,8 维度全过。**待修项 CR-01-A~I 见下方推进区**。 @@ -410,7 +424,13 @@ ### P1 — 设计完成待实施 -- [ ] F-260614-01 — **[P1]** 模型能力系统 Phase 1 — ModelCapability 数据模型 + ModelRouter 重写 + 7 调用点接入 + Settings 模型池编辑 UI + AiChat 模型下拉。按任务需求(模态/功能/成本)自动匹配合适模型,不再所有场景共用 default_model — source:📐 设计完成 (06-14) +- [ ] F-260614-01 — **[P1🔥]** 模型能力系统 Phase 1 — ModelConfig 数据模型(模态/能力/价格/智力 4 维度) + ModelRouter 智能路由 + 多源探测(预设表+启发式) + 厂商模型列表自动拉取(适配 OpenAI/Anthropic 协议差异) + 7 调用点接入 + Settings 模型池 UI + AiChat 模型下拉。按任务需求自动匹配合适模型,不再所有场景共用 default_model — source:📐 设计定稿 (06-16, [F-01-模型能力系统与智能路由设计-2026-06-16.md](./02-架构设计/F-01-模型能力系统与智能路由设计-2026-06-16.md))。F-07 已完成解除阻塞。 + - [ ] 阶段1:数据模型 — df-ai-core 新增 ModelConfig/Modality/Capability/CostTier/IntelligenceTier + df-storage models 反序列化兼容 + - [ ] 阶段2:预设表+探测器 — df-ai 新建 model_probe.rs + presets/models.json + 启发式推断 + 多源合并 + - [ ] 阶段3:厂商模型列表拉取 — df-ai 新建 model_fetch.rs + openai_compat/anthropic_compat 分派 + URL 拼接 + 噪音过滤 + - [ ] 阶段4:路由器 — df-ai 新建 router.rs + TaskRequirements + select 逻辑 + - [ ] 阶段5:7 调用点接入 — agentic.rs/title.rs/knowledge_inject.rs/project.rs + IPC ai_fetch_models/ai_probe_model + - [ ] 阶段6:前端 — types.ts 类型对齐 + Settings 模型池 UI + AiChat 模型下拉 ### P1 — Sprint 19 遗留 @@ -627,7 +647,7 @@ **✅ 推进(第一批独立可并行)**: -- [ ] F-07 trait 下沉 df-ai-core — 设计完备 4 项决策全定稿,退路可放 df-core,解锁 F-03/F-01 +- [x] ✅(batch61·2026-06-16·workflow wwtn2knn6·2069f79) F-07 trait 下沉 df-ai-core — 已实施(详见 L599 F-260614-07),4 决策全落地,解锁 F-03(已做 dfe0096)/F-01(待推)。本条为对抗分析裁决区过时重复条目,销账。 - [x] R-PD-2 ✅ ScriptNode 不注册 (第⑩批 2026-06-16) — ~~ScriptNode 不注册 script~~ 3 行删除封死攻击面,工作流当前纯演示无真实脚本需求 - [x] F-260615-06 ✅ patch_file (第⑩批 2026-06-16) — ~~patch_file(edit_file)~~ 局部文件更新工具,补齐 AI 文件操作闭环。完整设计见 [patch_file工具设计-2026-06-15.md](./02-架构设计/patch_file工具设计-2026-06-15.md)(API/三层防御/边界情况/替代方案否决/实施步骤)。**核心**: old_text 精确匹配为主+line 辅助+Mutex 并发安全+expected_hash 指纹防脏写。第一批实现核心三件套(~50行)。— src-tauri/src/commands/ai/tool_registry.rs 新增 handler - [x] ✅(batch56·2026-06-16·workflow wsxxurgbr+主代核查) ARC-06 composable 循环依赖 — **破环全闭环**:①`aiShared.ts` 下沉 findToolCall(反向扫描O(1))+4个审批计时器函数(startApprovalTimer/clearApprovalTimer/clearAllApprovalTimers+APPROVAL_TIMEOUT_MS常量) ②`useAiEvents` 删 findToolCall 导出+改 import 从 aiShared 取 ③`useAiSend` 删审批计时器实现(改从 aiShared re-export)+删 drainQueue 直接调用(改经 `ai-drain-queue` 事件总线桥接) ④`stores/ai.ts` 加 `initDrainQueueListener()` 启动事件监听。**B-260616-19 CSS 缺失**(Knowledge.vue 窄屏标题挤压,agent 未产出 .css/.vue style 变更),待补。— src/composables/ai/aiShared.ts + useAiEvents.ts + useAiSend.ts + stores/ai.ts diff --git a/docs/待决策.md b/docs/待决策.md index d80359a..fbd8dc6 100644 --- a/docs/待决策.md +++ b/docs/待决策.md @@ -139,7 +139,7 @@ - d: 等 git 集成(阶段4 F-260616-08 暂缓),自审代码改动 diff。最贴切(自审代码)但阻塞于阶段4。 - **推荐**:**a**(task 中心,最直接解锁自审闭环+人核,数据模型变更小:TaskRecord 加 output_json+迁移+run_workflow 注入 task 到 NodeContext+自审 prompt 模板,中等工作量)。c 通用但 executor 改动大,b 语义弱,d 阻塞阶段4。 - **关联**:todo F-260616-07 阶段3(AiNode 自审 ⏳ 子条) / task_workflow_templates.rs:57-72(testing 模板) / ai_node.rs(AiNode 通用执行) / memory aichat-arch-extensibility(AiNode 空壳架构债) -- **状态**:🟡 待决(2026-06-16)— 自审对象来源架构决策,方向错返工风险高,待用户拍板。AiNode 自审拓扑/通用执行已就绪,阻塞于此。 +- **状态**:✅ 已决(2026-06-16)— **决策:a**。TaskRecord 加 `output_json` 产出字段,ai_execute 写 / ai_self_review 读 / human_review 展示。task 中心,产出跟着 task 走,跨工作流执行可传递。 --- @@ -236,12 +236,12 @@ - **状态**:✅ 已决(2026-06-16)— **决策:拆独立 crate df-ai-core**。不放 df-core 过渡,LlmProvider trait + 核心数据结构下沉独立 crate,df-ai/df-ideas/df-nodes 均依赖 df-ai-core。**✅ 已实施(batch61·2026-06-16·commit 2069f79·workflow wwtn2knn6+主代核查,cargo 0err+7单测)**。解锁 F-01/F-03 注入。 #### F-260614-01 模型能力系统 Phase 1 -- **背景**:ModelCapability 数据模型 + ModelRouter 重写 + 7 调用点接入 + Settings 模型池 UI + AiChat 模型下拉。按需求自动匹配合适模型。 -- **设计状态**:📐 设计完成 +- **背景**:ModelConfig 数据模型(模态/能力/价格/智力 4 维度) + ModelRouter 智能路由 + 多源探测(预设表+启发式) + 厂商模型列表自动拉取(适配 OpenAI/Anthropic 协议差异) + 7 调用点接入 + Settings 模型池 UI + AiChat 模型下拉。按需求自动匹配合适模型。 +- **设计状态**:📐 设计定稿(2026-06-16,含 7 角度佐证 + 厂商适配 + 用户流程,见 [F-01-模型能力系统与智能路由设计-2026-06-16.md](./02-架构设计/F-01-模型能力系统与智能路由设计-2026-06-16.md)) - **决策点**:排期?(依赖 F-07 trait 下沉) - **推荐**:F-07 后接续 - **关联**:todo F-260614-01 / F-07 -- **状态**:🟡 待排期(F-07 已完成 batch61·2069f79,解除阻塞,可接续) +- **状态**:✅ 已排期(2026-06-16)— 用户确认推进。设计已定稿,F-07 已完成解除阻塞,列为 P1🔥 可立即实施。6 阶段子任务已拆解见 todo.md。 #### F-260614-06 导入历史项目(scan 第二步) - **背景**:description 走 LLM(复用 scan_project_with_ai)+ 采样保留内容图(待 F-260614-05 多模态)+ monorepo 一层 + 批量并发;抽 create_with_binding 缓解 TODO。 @@ -263,14 +263,14 @@ - **决策点**:排期?(增强非阻断) - **推荐**:低优先(等 F-01 模型能力后) - **关联**:todo F-260614-04 -- **状态**:🟡 待排期 +- **状态**:✅ 已排期(2026-06-16)— 用户确认推进。P2 优先级,等 F-01 模型能力系统落地后接续(路由器需要知道哪些模型可用)。 #### F-260614-05 模型能力系统 Phase 2(多模态) - **背景**:ChatMessage.content: String → Vec(Text/Image);前端粘贴/拖拽图片;vision 模型自动路由。 - **决策点**:排期?(解锁 F-06 导入采样) - **推荐**:中等(解锁 F-06) - **关联**:todo F-260614-05 / F-01 -- **状态**:🟡 待排期 +- **状态**:✅ 已排期(2026-06-16)— 用户确认推进。P2 优先级,F-01 后接续。设计已定稿见 [F-05-多模态实施方案设计-2026-06-16.md](./02-架构设计/F-05-多模态实施方案设计-2026-06-16.md)。 #### AE-2025-04 会话级授权(Session Trust) - **背景**:同会话内用户批准过某类操作后,后续同类自动放行;换会话清空重审。首批 write_file + run_command。设计已定(目录级粒度 + 仅写+执行首批 + Webhook 走独立 execution_token)。 @@ -278,14 +278,14 @@ - **决策点**:排期?(涉及 mod.rs AiSession + audit.rs 审批链) - **推荐**:中高(写→跑→看→改闭环高频连续操作,体验提升大) - **关联**:todo AE-2025-04 -- **状态**:🟡 待排期 +- **状态**:✅ 已排期(2026-06-16)— 用户确认推进。P1 优先级,写→跑→看→改闭环高频连续操作体验提升大。设计已定(目录级粒度 + 仅写+执行首批)。 #### T-260614-06 Settings.vue 拆 panel 子组件 - **背景**:1042 行 god file,4 大功能域(AI 模型/Provider 表单/连接管理/通用设置)可拆 src/components/settings/。评估非低风险(纯重构零功能价值,新建 4 子组件 + props/emits + CSS 拆)。 - **决策点**:排期?(单独立项做更稳) - **推荐**:低(纯重构零功能,等 god file 痛点加剧) - **关联**:todo T-260614-06 -- **状态**:🟡 待排期 +- **状态**:✅ 已排期(2026-06-16)— 用户确认推进。P3 优先级(纯重构零功能),等 god file 痛点加剧或配合 F-01 模型池 UI 一并做。 ---