From 2ddfea67c7f029c53b897f07e1178ba1b4282d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 17 Jun 2026 00:33:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20F-01=E9=98=B6=E6=AE=B55?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E8=B7=AF=E7=94=B1=E8=B0=83=E7=94=A8=E7=82=B9?= =?UTF-8?q?=E6=8E=A5=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + crates/df-ai/src/router.rs | 35 +++++++++++++++- crates/df-ideas/Cargo.toml | 4 ++ crates/df-ideas/src/adversarial.rs | 29 +++++++++++-- crates/df-nodes/src/ai_node.rs | 32 ++++++++++++++- src-tauri/src/commands/ai/agentic.rs | 41 ++++++++++++++----- src-tauri/src/commands/ai/knowledge_inject.rs | 39 +++++++++++++++++- src-tauri/src/commands/ai/title.rs | 18 +++++++- src-tauri/src/commands/idea.rs | 14 +++++-- src-tauri/src/commands/project.rs | 31 +++++++++++++- 10 files changed, 220 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63f5abc..ebc5403 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -820,6 +820,7 @@ dependencies = [ "anyhow", "async-trait", "chrono", + "df-ai", "df-ai-core", "df-core", "regex", diff --git a/crates/df-ai/src/router.rs b/crates/df-ai/src/router.rs index 4957e15..617c6bf 100644 --- a/crates/df-ai/src/router.rs +++ b/crates/df-ai/src/router.rs @@ -10,7 +10,10 @@ use std::cmp::Reverse; -use df_ai_core::model::{Capability, CostTier, IntelligenceTier, Modality, ModelConfig}; +// 阶段5: 调用点经 `df_ai::router::{Modality, Capability, CostTier, IntelligenceTier}` +// 直接 import 维度枚举构造 TaskRequirements(对齐任务规格 import 风格),re-export 避免调用点 +// 各自从 df_ai_core::model 取(跨 crate 路径冗长)。select/select_model_id 仅借用枚举,无重定义。 +pub use df_ai_core::model::{Capability, CostTier, IntelligenceTier, Modality, ModelConfig}; /// 任务对模型的需求(5 维度,对齐设计 §6.1)。 /// @@ -63,6 +66,20 @@ impl ModelRouter { } } +/// 阶段5 调用点 helper — 路由选模型并直接返回 model_id(纯函数)。 +/// +/// 给定 TaskRequirements + 候选池,返回最优模型的 `model_id`。 +/// 调用点用法:`provider.model_configs`(Vec)→ `select_model_id(&req, &pool)` +/// → `Option`;None 时兜底 `provider.default_model`(行为不变,平滑过渡)。 +/// +/// 行为不变保证: +/// - 池空(用户未通过 Settings 拉取模型)→ 返回 None → 调用点兜底 default_model +/// - 池非空但无候选满足需求 → 返回 None → 兜底 default_model +/// - 池非空命中 → 返回 model_id(F-01 路由目标,拉取即启用路由) +pub fn select_model_id(req: &TaskRequirements, pool: &[ModelConfig]) -> Option { + ModelRouter::select(req, pool).map(|m| m.model_id.clone()) +} + #[cfg(test)] mod tests { use super::*; @@ -94,6 +111,22 @@ mod tests { } } + // ── 阶段5 select_model_id helper ── + + #[test] + fn select_model_id_empty_pool_returns_none() { + // 池空(用户未拉取模型)→ None,调用点兜底 default_model + let pool: Vec = vec![]; + assert!(select_model_id(&req(), &pool).is_none()); + } + + #[test] + fn select_model_id_hit_returns_model_id() { + // 池非空命中 → 返回 model_id 字符串(非引用) + let pool = vec![model("glm-4-flash")]; + assert_eq!(select_model_id(&req(), &pool).as_deref(), Some("glm-4-flash")); + } + // ── 步骤 1:enabled 过滤 ── #[test] diff --git a/crates/df-ideas/Cargo.toml b/crates/df-ideas/Cargo.toml index 7304c28..3d45da2 100644 --- a/crates/df-ideas/Cargo.toml +++ b/crates/df-ideas/Cargo.toml @@ -6,6 +6,10 @@ edition = "2021" [dependencies] df-core = { path = "../df-core" } df-ai-core = { path = "../df-ai-core" } +# F-01 阶段5: df-ideas 对抗评估接入 df_ai::router::select_model_id(纯函数路由)。 +# 注:df-ai 引入 reqwest/futures/eventsource-stream 重依赖,但 router 模块仅依赖 +# df-ai-core::model,实际编译期 df-ideas 仅引用 router 符号(零 HTTP 代码路径)。 +df-ai = { path = "../df-ai" } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true } diff --git a/crates/df-ideas/src/adversarial.rs b/crates/df-ideas/src/adversarial.rs index ddf57f8..7a443b2 100644 --- a/crates/df-ideas/src/adversarial.rs +++ b/crates/df-ideas/src/adversarial.rs @@ -16,6 +16,7 @@ use std::sync::Arc; use anyhow::Result; use serde::{Deserialize, Serialize}; +use df_ai_core::model::ModelConfig; use df_ai_core::provider::LlmProvider; use df_core::types::{IdeaId, Priority}; use crate::capture::Idea; @@ -95,17 +96,26 @@ pub struct AdversarialEngine { /// 可选 LLM provider。Some → 优先 LLM 评估(失败降级启发式);None → 纯启发式。 /// 构造注入(与 IdeaPromoter::new(policy) 同一模式),批量评估复用同一 provider。 provider: Option>, + /// F-01 阶段5: 候选模型池。非空时 evaluate_with_llm 经 select_model_id 路由选模型; + /// 空(None provider 或未注入池)→ model 留空由 provider impl 回填自身 default_model + /// (与接入前行为一致,平稳过渡)。 + model_pool: Vec, } impl AdversarialEngine { /// 注入 LLM provider 构造(provider Some 时走 LLM,调用失败自动降级启发式) pub fn new(provider: Arc) -> Self { - Self { provider: Some(provider) } + Self { provider: Some(provider), model_pool: Vec::new() } + } + + /// F-01 阶段5: 注入 provider + 候选模型池构造。池非空时 evaluate_with_llm 走路由。 + pub fn with_pool(provider: Arc, model_pool: Vec) -> Self { + Self { provider: Some(provider), model_pool } } /// 纯启发式构造(无 LLM 配置时的默认模式) pub fn heuristic() -> Self { - Self { provider: None } + Self { provider: None, model_pool: Vec::new() } } /// 执行完整的对抗评估(内部按 provider 有无调度 LLM / 启发式,失败降级) @@ -142,10 +152,21 @@ impl AdversarialEngine { /// 0.4 在「稳定可复现」与「论点多样性」间取得平衡。 async fn evaluate_with_llm(&self, idea: &Idea, provider: &Arc) -> Result { let prompt = build_adversarial_prompt(idea); + // F-01 阶段5: 智能路由 — 对抗评估 TaskRequirements(Standard,无工具)。 + // 池非空 → select_model_id 选最优 model_id;池空/无匹配 → 留空由 provider impl + // 回填自身 default_model(与接入前行为一致,平稳过渡)。 + let eval_req = df_ai::router::TaskRequirements { + modalities: vec![df_ai_core::model::Modality::Text], + needs_tool_use: false, + min_intelligence: df_ai_core::model::IntelligenceTier::Standard, + max_cost: None, + estimated_context: 0, + }; + let model = df_ai::router::select_model_id(&eval_req, &self.model_pool).unwrap_or_default(); let request = df_ai_core::provider::CompletionRequest { - // provider 自带 default_model;model 留空让 provider impl 回填自身默认模型。 + // 路由命中 → 用 model_id;否则留空让 provider impl 回填自身 default_model。 // (OpenAICompatProvider::convert_request 在 req.model.is_empty() 时回退 default_model) - model: String::new(), + model, messages: vec![ df_ai_core::provider::ChatMessage::system(SYSTEM_PROMPT), df_ai_core::provider::ChatMessage::user(prompt), diff --git a/crates/df-nodes/src/ai_node.rs b/crates/df-nodes/src/ai_node.rs index 4e471e8..69bcc87 100644 --- a/crates/df-nodes/src/ai_node.rs +++ b/crates/df-nodes/src/ai_node.rs @@ -10,7 +10,11 @@ use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; +use df_ai::df_ai_core::model::{IntelligenceTier, Modality, ModelConfig}; use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider}; +// F-01 阶段5: AiNode 路由 — 节点 config.model_id 优先;否则按 TaskRequirements 路由 +// (默认 Standard + needs_tool_use=true)。池空/无匹配兜底 record.default_model。 +use df_ai::router::{select_model_id, TaskRequirements}; use df_storage::crud::{AiProviderRepo, TaskRepo}; use df_storage::db::Database; use df_storage::models::AiProviderRecord; @@ -45,6 +49,9 @@ struct ResolvedProvider { api_key: String, /// model 为空时的占位(record.default_model 或 "gpt-4o-mini"),避免 provider 构造 panic default_model: String, + /// F-01 阶段5: 候选模型池(来自 record.model_configs)。parse_params 路由用: + /// config.model 留空时经 select_model_id 选最优;池空兜底 default_model。 + model_pool: Vec, } /// 经 ai_providers 表 + df_storage::secret 解析 provider 构造要素(FR-S1 注入链核心)。 @@ -117,6 +124,8 @@ async fn resolve_provider( base_url, api_key, default_model, + // 老明文路径无 record,候选池空(无路由能力,兜底 default_model)。 + model_pool: Vec::new(), }); } @@ -154,6 +163,7 @@ fn resolve_from_record( base_url: record.base_url.clone(), api_key, default_model, + model_pool: record.model_configs.clone(), }) } @@ -179,11 +189,30 @@ fn parse_params( .ok_or_else(|| anyhow::anyhow!("AiNode 缺少必填参数: prompt(config 或上游输入均无)"))?; // ── 可选参数 ── - let model = config + // model 解析优先级(F-01 阶段5):config.model 显式指定 > 路由选优(provider.model_pool 非空时) + // > 空(CompletionRequest.model 留空由 provider impl 回填 default_model,行为不变)。 + // 注:provider.model_pool 在 provider move 进 AiNodeParams 前先借引用路由,选中的 model_id + // 填入 CompletionRequest.model;provider.default_model 仍是 build_provider 兜底用。 + let config_model = config .get("model") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); + let model = if !config_model.is_empty() { + config_model + } else { + // F-01 阶段5: AiNode 默认路由 — Standard + needs_tool_use=true(工作流无人值守 AI 步骤 + // 常含工具调用,如检索/生成;无需工具的节点应在 config 显式指定 model)。 + // select_model_id None(池空/无匹配)→ 空串(由 provider impl 回填 default_model)。 + let node_req = TaskRequirements { + modalities: vec![Modality::Text], + needs_tool_use: true, + min_intelligence: IntelligenceTier::Standard, + max_cost: None, + estimated_context: 0, + }; + select_model_id(&node_req, &provider.model_pool).unwrap_or_default() + }; let temperature = config .get("temperature") .and_then(|v| v.as_f64()) @@ -600,6 +629,7 @@ mod tests { base_url: "https://api.example.com".to_string(), api_key: "sk-test".to_string(), default_model: "gpt-4o-mini".to_string(), + model_pool: Vec::new(), } } diff --git a/src-tauri/src/commands/ai/agentic.rs b/src-tauri/src/commands/ai/agentic.rs index adb315f..db6479e 100644 --- a/src-tauri/src/commands/ai/agentic.rs +++ b/src-tauri/src/commands/ai/agentic.rs @@ -12,6 +12,13 @@ use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider}; // CR-30-1: 复用 retry::backoff_delay(jitter 1s→2s→4s) + is_status_retryable(Fatal 分类) // 实现流前失败重试退避对齐(决策 F-260616-07 a1),避免重写退避逻辑。 use df_ai::retry; +// F-01 阶段5: 智能路由 helper + TaskRequirements + 维度枚举。 +// 调用点构造 TaskRequirements(主对话:needs_tool_use=true,min_intelligence=Standard, +// 当前仅 Text 模态;后续多模态接入时检测消息内 Part/Image 追加 Vision), +// 经 select_model_id 在 provider.model_configs 池中选最优;池空/无匹配兜底 default_model。 +use df_ai::router::{ + select_model_id, IntelligenceTier, Modality, TaskRequirements, +}; use df_storage::db::Database; use df_storage::models::AiProviderRecord; @@ -154,6 +161,19 @@ pub(crate) async fn run_agentic_loop( key_len = key_len, "[ai] 发起 LLM 请求" ); + + // F-01 阶段5: 主对话路由 — TaskRequirements(needs_tool_use=true,min_intelligence=Standard)。 + // 模态当前仅 Text(图像消息类型未实现,后续多模态接入时检测 Part/Image 追加 Vision)。 + // select_model_id None(池空/无匹配)→ 兜底 default_model,行为与接入前一致。 + let agentic_req = TaskRequirements { + modalities: vec![Modality::Text], + needs_tool_use: true, + min_intelligence: IntelligenceTier::Standard, + max_cost: None, + estimated_context: 0, + }; + let resolved_model = select_model_id(&agentic_req, &provider_config.model_configs) + .unwrap_or_else(|| provider_config.default_model.clone()); let tool_defs = tools_arc.tool_definitions(); // 停止信号副本:stream_llm 与每轮迭代共享读取,避免重复加锁 // notify 同取一份 Arc 引用(B-260615-14):stream_llm select! 监听 notified() 即时唤醒 @@ -265,7 +285,7 @@ pub(crate) async fn run_agentic_loop( // 一致,重建与复用等价。收益:省去每次重试的 lock() + build_for_request(含 // all_messages_clone 全量 clone)。 let retry_request = CompletionRequest { - model: provider_config.default_model.clone(), + model: resolved_model.clone(), messages: messages.clone(), temperature: Some(0.7), max_tokens: Some(8192), @@ -387,16 +407,16 @@ pub(crate) async fn run_agentic_loop( } if !full_text.is_empty() { let mut msg = ChatMessage::assistant(&full_text); - msg.model = Some(provider_config.default_model.clone()); + msg.model = Some(resolved_model.clone()); session.messages.push(msg); // 追加系统提示消息:响应因网络中断不完整(对齐决策 a1 系统提示机制) let mut notice = ChatMessage::system("⚠ 响应因网络中断不完整,以上为已接收的部分内容。可重新发送以获取完整回复。"); - notice.model = Some(provider_config.default_model.clone()); + notice.model = Some(resolved_model.clone()); session.messages.push(notice); } } - save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await; + save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await; // 标题生成后台化(失败有 extract_title 兜底) spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency); guard.reset().await; @@ -442,11 +462,11 @@ pub(crate) async fn run_agentic_loop( }) .collect(); let mut msg = ChatMessage::assistant_with_tools(&full_text, ai_tool_calls); - msg.model = Some(provider_config.default_model.clone()); + msg.model = Some(resolved_model.clone()); session.messages.push(msg); } else if !full_text.is_empty() { let mut msg = ChatMessage::assistant(&full_text); - msg.model = Some(provider_config.default_model.clone()); + msg.model = Some(resolved_model.clone()); session.messages.push(msg); } } @@ -458,7 +478,7 @@ pub(crate) async fn run_agentic_loop( completion_tokens: tokens.completion(), total_tokens: tokens.total(), }; - save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await; + save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await; // 标题生成后台化:不阻塞 Completed emit(失败有 extract_title 兜底) spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency); guard.reset().await; @@ -483,7 +503,7 @@ pub(crate) async fn run_agentic_loop( completion_tokens: tokens.completion(), total_tokens: tokens.total(), }; - save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await; + save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await; // B-260615-26: 审批等待 return 前 disarm guard——保持 generating=true 留 try_continue 续生成, // 同时 Drop 因 done=true 跳过复位 spawn(避免误复位审批态 generating 致 ai_approve→try_continue 不续) guard.disarm(); @@ -512,7 +532,7 @@ pub(crate) async fn run_agentic_loop( completion_tokens: tokens.completion(), total_tokens: tokens.total(), }; - save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await; + save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await; // 暂停态保持 generating=true(防其他 send 抢占,仿审批),disarm guard 跳过 Drop 兜底复位 guard.disarm(); let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiMaxRoundsReached { @@ -540,8 +560,9 @@ pub(crate) async fn run_agentic_loop( let knowledge_config = knowledge_config.clone(); let app_handle = app_handle.clone(); let llm_concurrency = llm_concurrency.clone(); + let resolved_model = resolved_model.clone(); tauri::async_runtime::spawn(async move { - save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&provider_config.default_model)).await; + save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model)).await; // 知识提炼:需读已落库的对话消息,故在 save 之后 if let Err(e) = maybe_spawn_extraction(&session_arc, &db, &conv_id, &provider_config, &knowledge_config, llm_concurrency.clone()).await { tracing::warn!("知识提炼触发失败(非阻断): {}", e); diff --git a/src-tauri/src/commands/ai/knowledge_inject.rs b/src-tauri/src/commands/ai/knowledge_inject.rs index 53c58aa..1d35049 100644 --- a/src-tauri/src/commands/ai/knowledge_inject.rs +++ b/src-tauri/src/commands/ai/knowledge_inject.rs @@ -5,6 +5,13 @@ use std::sync::Arc; use tokio::sync::Mutex; use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole}; +// F-01 阶段5: 知识提炼 / 嵌入两路路由。 +// - 提炼: TaskRequirements(Standard,无工具)— 在对话 provider 的 model_configs 池中选。 +// - 嵌入: TaskRequirements(Capability::Embedding,无工具)— 在 embedding provider 的池中选, +// 池空兜底 config.embedding_model(行为不变,现有 KnowledgeConfig 配置即生效)。 +use df_ai::router::{ + select_model_id, IntelligenceTier, Modality, TaskRequirements, +}; use df_storage::crud::{AiConversationRepo, KnowledgeRepo}; use df_storage::db::Database; use df_storage::models::{AiProviderRecord, KnowledgeRecord}; @@ -18,12 +25,19 @@ use crate::commands::err_str; use super::{AiSession}; /// 按配置构建 embedding provider + model。None = 配置缺失/provider 不存在。 +/// +/// F-01 阶段5: model 选择路由优先 — 在 embedding provider 的 model_configs 池中按 +/// TaskRequirements(needs_tool_use=false)选最优;池空(未拉取)兜底 config.embedding_model。 +/// +/// 注:TaskRequirements 无 capabilities 字段(router 仅按 needs_tool_use 过滤 ToolUse), +/// 嵌入模型由 embedding provider 的池构成(用户在 KnowledgeSettings 配 embedding provider, +/// 该 provider 的 model_configs 通常即嵌入模型),故 needs_tool_use=false 宽松过滤即可命中。 async fn resolve_embed_provider( state: &AppState, config: &crate::state::KnowledgeConfig, ) -> Option<(Box, String)> { let id = config.embedding_provider_id.as_ref()?; - let model = config.embedding_model.clone().unwrap_or_else(|| "embedding-3".to_string()); + let fallback_model = config.embedding_model.clone().unwrap_or_else(|| "embedding-3".to_string()); let rec = match state.ai_providers.get_by_id(id).await { Ok(Some(r)) => r, _ => { @@ -39,6 +53,16 @@ async fn resolve_embed_provider( return None; } }; + // 嵌入路由:needs_tool_use=false(排除 ToolUse 专要求,允许嵌入模型入选)。 + // select_model_id None(池空/无匹配)→ 兜底 config.embedding_model(行为不变)。 + let embed_req = TaskRequirements { + modalities: vec![Modality::Text], + needs_tool_use: false, + min_intelligence: IntelligenceTier::Lite, + max_cost: None, + estimated_context: 0, + }; + let model = select_model_id(&embed_req, &rec.model_configs).unwrap_or(fallback_model); Some((provider, model)) } @@ -314,8 +338,19 @@ async fn extract_knowledge_from_conversation( ChatMessage::system(EXTRACTION_SYSTEM_PROMPT), ChatMessage::user(&format!("请从以下对话中提炼可复用知识:\n\n{}", conv_text)), ]; + // F-01 阶段5: 知识提炼路由 — TaskRequirements(Standard,无工具)。 + // select_model_id None(池空/无匹配)→ 兜底 default_model(行为不变)。 + let extract_req = TaskRequirements { + modalities: vec![Modality::Text], + needs_tool_use: false, + min_intelligence: IntelligenceTier::Standard, + max_cost: None, + estimated_context: 0, + }; + let extract_model = select_model_id(&extract_req, &provider_config.model_configs) + .unwrap_or_else(|| provider_config.default_model.clone()); let request = CompletionRequest { - model: provider_config.default_model.clone(), + model: extract_model, messages: extract_messages, temperature: Some(0.3), max_tokens: Some(2048), diff --git a/src-tauri/src/commands/ai/title.rs b/src-tauri/src/commands/ai/title.rs index fcbbb78..b785354 100644 --- a/src-tauri/src/commands/ai/title.rs +++ b/src-tauri/src/commands/ai/title.rs @@ -6,6 +6,11 @@ use tauri::{AppHandle, Emitter}; use tokio::sync::Mutex; use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole}; +// F-01 阶段5: 标题生成路由 — TaskRequirements(Lite + Low 成本,无需工具)。 +// 标题是短文本摘要任务,选便宜 Lite 模型;池空/无匹配兜底 default_model(行为不变)。 +use df_ai::router::{ + select_model_id, CostTier, IntelligenceTier, Modality, TaskRequirements, +}; use df_storage::crud::AiConversationRepo; use df_storage::db::Database; use df_storage::models::AiProviderRecord; @@ -69,7 +74,18 @@ pub(crate) async fn ensure_conversation_title( return; } }; - let title = match generate_title_via_llm(&*provider, &provider_config.default_model, summary_msgs, &llm_concurrency).await { + // F-01 阶段5: 标题生成路由 — 选便宜 Lite 模型(Low 成本上限,无工具)。 + // select_model_id None(池空/无匹配)→ 兜底 default_model。 + let title_req = TaskRequirements { + modalities: vec![Modality::Text], + needs_tool_use: false, + min_intelligence: IntelligenceTier::Lite, + max_cost: Some(CostTier::Low), + estimated_context: 0, + }; + let title_model = select_model_id(&title_req, &provider_config.model_configs) + .unwrap_or_else(|| provider_config.default_model.clone()); + let title = match generate_title_via_llm(&*provider, &title_model, summary_msgs, &llm_concurrency).await { Some(t) => t, None => extract_title(&all_msgs).unwrap_or_else(|| "新对话".to_string()), }; diff --git a/src-tauri/src/commands/idea.rs b/src-tauri/src/commands/idea.rs index b2753ff..e44b2a0 100644 --- a/src-tauri/src/commands/idea.rs +++ b/src-tauri/src/commands/idea.rs @@ -187,9 +187,12 @@ pub async fn evaluate_idea( let scores = df_ideas::scoring::ScoringEngine::compute_default(&idea); // 对抗式评估(构造注入:从 DB 读默认 provider 装配 LLM,无 provider/构造失败 → 启发式兜底) + // F-01 阶段5: 透传 model_configs 池,evaluate_with_llm 经路由选模型(池空兜底 default_model)。 let provider = build_default_provider(&state).await; let engine = match provider { - Some(p) => df_ideas::adversarial::AdversarialEngine::new(Arc::from(p)), + Some((p, pool)) => { + df_ideas::adversarial::AdversarialEngine::with_pool(Arc::from(p), pool) + } None => df_ideas::adversarial::AdversarialEngine::heuristic(), }; let eval = engine.evaluate(&idea).await.map_err(err_str)?; @@ -262,7 +265,12 @@ pub async fn evaluate_idea( /// /// 复用 `commands::ai::secret::build_provider_for`(resolve→ensure→build 三步), /// 与 AI Chat / 项目扫描的 provider 构造路径统一(FR-S1 密钥解析一致)。 -async fn build_default_provider(state: &State<'_, AppState>) -> Option> { +/// +/// 返回 (provider, model_pool):model_pool = 选中 provider 的 model_configs(F-01 阶段5, +/// 供对抗评估路由)。池空(用户未拉取)→ 调用方兜底 default_model。 +async fn build_default_provider( + state: &State<'_, AppState>, +) -> Option<(Box, Vec)> { let providers = state.ai_providers.list_all().await.ok()?; let pc = providers .iter() @@ -270,7 +278,7 @@ async fn build_default_provider(state: &State<'_, AppState>) -> Option Some(p), + Ok(p) => Some((p, pc.model_configs.clone())), Err(e) => { // 密钥不可用:启发式兜底,不阻断评估(与 evaluate_idea LLM 失败降级语义一致) tracing::warn!("默认 provider 密钥不可用,对抗评估走启发式: {e}"); diff --git a/src-tauri/src/commands/project.rs b/src-tauri/src/commands/project.rs index c3600e1..091efa8 100644 --- a/src-tauri/src/commands/project.rs +++ b/src-tauri/src/commands/project.rs @@ -6,6 +6,11 @@ use serde::{Deserialize, Serialize}; use tauri::State; use df_ai::provider::{ChatMessage, CompletionRequest}; +// F-01 阶段5: 项目扫描描述路由 — TaskRequirements(Standard;采样含图时追加 Vision, +// 当前采样(ProjectSample)未含图像,故仅 Text)。池空/无匹配兜底 default_model。 +use df_ai::router::{ + select_model_id, IntelligenceTier, Modality, TaskRequirements, +}; use df_core::types::new_id; use df_project::scan::{ collect_sample, detect_stack, discover_projects, extract_description, is_monorepo, @@ -522,8 +527,19 @@ async fn extract_description_via_llm( .map_err(err_str)? .map_err(err_str)?; + // F-01 阶段5: 项目扫描描述路由 — Standard,无工具;采样未含图故仅 Text。 + // select_model_id None(池空/无匹配)→ 兜底 default_model(行为不变)。 + let scan_req = TaskRequirements { + modalities: vec![Modality::Text], + needs_tool_use: false, + min_intelligence: IntelligenceTier::Standard, + max_cost: None, + estimated_context: 0, + }; + let scan_model = select_model_id(&scan_req, &pc.model_configs) + .unwrap_or_else(|| pc.default_model.clone()); let request = CompletionRequest { - model: pc.default_model.clone(), + model: scan_model, messages: build_scan_prompt(&sample, &rule_stack), temperature: Some(0.2), max_tokens: Some(400), @@ -606,8 +622,19 @@ pub async fn scan_project_with_ai( }); } }; + // F-01 阶段5: 项目扫描描述路由 — Standard,无工具;采样未含图故仅 Text。 + // select_model_id None(池空/无匹配)→ 兜底 default_model(行为不变)。 + let scan_req = TaskRequirements { + modalities: vec![Modality::Text], + needs_tool_use: false, + min_intelligence: IntelligenceTier::Standard, + max_cost: None, + estimated_context: 0, + }; + let scan_model = select_model_id(&scan_req, &pc.model_configs) + .unwrap_or_else(|| pc.default_model.clone()); let request = CompletionRequest { - model: pc.default_model.clone(), + model: scan_model, messages: build_scan_prompt(&sample, &rule_stack), temperature: Some(0.2), max_tokens: Some(400),