From b3684f4d1f188ea294864c8d07649601a0d414a0 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 02:26:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20F-04c=E5=90=8E=E7=AB=AF(p?= =?UTF-8?q?rovider=E6=B1=A0enabled/weight=E6=9B=B4=E6=96=B0IPC+set=5Fcaps?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=8E=A5=E5=85=A5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/commands/ai/commands.rs | 79 +++++++++++++++++++++------ src-tauri/src/lib.rs | 2 + src-tauri/src/state.rs | 30 ++++++++++ 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/src-tauri/src/commands/ai/commands.rs b/src-tauri/src/commands/ai/commands.rs index f462651..38c1bc9 100644 --- a/src-tauri/src/commands/ai/commands.rs +++ b/src-tauri/src/commands/ai/commands.rs @@ -926,23 +926,26 @@ pub async fn ai_save_provider( provider_type: String, ) -> Result { // 编辑已有提供商时保留原 created_at,避免被覆盖 - let created_at = match &id { - Some(pid) => state.ai_providers.get_by_id(pid).await - .map_err(err_str)? - .map(|p| p.created_at) - .unwrap_or_else(now_millis), - None => now_millis(), + // F-260614-04c: 编辑路径同时保留原 enabled/weight(负载均衡池可编辑层)。 + // 前端经此 IPC 改 enabled/weight 落库;新建走默认 enabled=true/weight=50。 + let existing = match &id { + Some(pid) => state.ai_providers.get_by_id(pid).await.map_err(err_str)?, + None => None, }; + let created_at = existing.as_ref().map(|p| p.created_at.clone()).unwrap_or_else(now_millis); // is_default:编辑保留原值;新建时若全表尚无默认则设为默认(首个自动默认,避免无默认可用) - let is_default = match &id { - Some(pid) => state.ai_providers.get_by_id(pid).await - .map_err(err_str)? - .map(|p| p.is_default) - .unwrap_or(false), + let is_default = match &existing { + Some(p) => p.is_default, None => !state.ai_providers.list_all().await .map_err(err_str)? .iter().any(|p| p.is_default), }; + // F-260614-04c: enabled/weight 编辑保留原值(前端 Settings 改值经此落库); + // 新建默认进池(enabled=true,weight=50)。 + let (enabled, weight) = match &existing { + Some(p) => (p.enabled, p.weight), + None => (true, 50), + }; // FR-S1:密钥存 OS keyring,DB api_key 列恒空(不入明文)。 // api_key 非空 = 新/改密钥 → 写 keyring;空 = 编辑不改 → 保留原 keyring 密钥不动。 let provider_id = id.clone().unwrap_or_else(new_id); @@ -993,10 +996,9 @@ pub async fn ai_save_provider( config: None, created_at, updated_at: now_millis(), - // F-260614-04: 新建 provider 默认进入负载均衡池(enabled=true,weight=50)。 - // 单 provider 路径零变化(enabled=true 即与迁移前行为等价)。 - enabled: true, - weight: 50, + // F-260614-04c: enabled/weight 编辑保留原值(负载均衡池可编辑层),新建走默认。 + enabled, + weight, }; let id = record.id.clone(); state @@ -1007,7 +1009,52 @@ pub async fn ai_save_provider( Ok(id) } -/// 设置活跃提供商(互斥落库:目标置默认、其余清默认,重启不丢) +/// F-260614-04c: 轻量更新 provider 池配置(enabled/weight),不改其它字段、不触密钥迁移。 +/// +/// 与 `ai_save_provider` 的区别: +/// - ai_save_provider 是全量保存(name/base_url/api_key/model...),编辑路径会走 R-PD-1 密钥 +/// 迁移 + INSERT OR REPLACE 全字段;前端 Settings「负载均衡池」开关/权重滑块仅需改这俩字段, +/// 不应重发整张表(尤其避免空 api_key 触发密钥迁移分支)。 +/// - 本 IPC 仅 UPDATE enabled/weight(经 update_field 或 update_full),重建 caps 表。 +/// +/// 落库后立即重建 per_provider caps(set_provider_caps),保证开关/权重变更对 agentic loop +/// 即时生效(下条消息即按新配置 acquire)。caps 重建非强一致(软收敛:已持 permit 不回收)。 +#[tauri::command] +pub async fn ai_update_provider_pool( + state: State<'_, AppState>, + provider_id: String, + enabled: bool, + weight: u32, +) -> Result<(), String> { + // 验证 provider 存在(防前端传错 id 静默无操作) + let mut record = state + .ai_providers + .get_by_id(&provider_id) + .await + .map_err(err_str)? + .ok_or_else(|| format!("提供商不存在: {}", provider_id))?; + // weight 落库前 clamp 到 [0,100](对齐 crud update 的 weight.min(100),防越界)。 + let weight = weight.min(100); + if record.enabled == enabled && record.weight == weight { + // 无变化:跳过 DB 写 + caps 重建(幂等,防前端重复点击触发不必要的 IO)。 + return Ok(()); + } + record.enabled = enabled; + record.weight = weight; + record.updated_at = now_millis(); + // update_full 走 UPDATE 全字段(含 enabled/weight,波12已加);api_key 不变(DB 恒空)。 + state + .ai_providers + .update_full(&record) + .await + .map_err(err_str)?; + // 重建 per_provider caps:本 provider 被禁用/weight=0 → 不入新表 → acquire_for_provider + // 对其返 None(无限流,但 provider_pool::select 已把它移出候选,实际不会被 acquire)。 + // caps 重建逻辑收敛到 AppState::reload_provider_caps(单点真理,启动 + 变更共用)。 + state.reload_provider_caps().await; + Ok(()) +} + #[tauri::command] pub async fn ai_set_provider( state: State<'_, AppState>, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ea38216..89c29c8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -125,6 +125,8 @@ pub fn run() { commands::ai::ai_save_provider, commands::ai::ai_set_provider, commands::ai::ai_delete_provider, + // F-260614-04c: 负载均衡池可编辑层(enabled/weight 轻量更新 + caps 重建) + commands::ai::ai_update_provider_pool, // F-01 阶段5:测试连接拉取模型列表 + 单模型探测 commands::ai::ai_fetch_models, commands::ai::ai_probe_model, diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index c69955a..8b8343c 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -294,8 +294,38 @@ impl AppState { // 启动恢复:重启前卡 pending 的工具审批(内存 pending_approvals 已丢)从审计表重建, // 使重启后待审批不丢。前端经 ai_pending_tool_calls + switchConversation 恢复 toolCard 态。 crate::commands::ai::restore_pending_approvals(&state).await; + // F-260614-04c: 启动一次性初始化 per-provider caps 表。 + // 根据 DB enabled providers 建 HashMap,让 agentic loop 的 + // acquire_for_provider 从 None(无限流)切换到 Some(按配置限流)。disabled / weight=0 + // 的 provider 不入表(其被 provider_pool::select 过滤出候选,不会被 acquire)。 + // 单 provider 场景:该 provider cap=global_cap → acquire_global+acquire_for_provider + // 串联,min(3,3)=3,有效上限同未配置 → 行为零变化。 + state.reload_provider_caps().await; Ok(state) } + + /// F-260614-04c: 从 DB enabled providers 重建 per_provider caps 并 set_provider_caps。 + /// + /// 启动 + provider 变更(ai_update_provider_pool / 删除 / 新增)后调用。caps 策略见 + /// AppState::init 注释(本轮每 provider cap = global_cap,F-04d 配差异化上限时仅改此)。 + pub async fn reload_provider_caps(&self) { + let providers = match self.ai_providers.list_all().await { + Ok(ps) => ps, + Err(e) => { + tracing::warn!( + "[F-04c] 读取 providers 重建 caps 失败,保持当前 caps 表不变: {}", e + ); + return; + } + }; + let global_cap = 3; // 与 LlmConcurrency::new(3, 2) 的 global 上限对齐。 + let caps: HashMap = providers + .into_iter() + .filter(|p| p.enabled && p.weight > 0) + .map(|p| (p.id, global_cap)) + .collect(); + self.llm_concurrency.set_provider_caps(caps).await; + } } /// 构建节点注册表 — 注册内置节点