新增: F-04c后端(provider池enabled/weight更新IPC+set_caps启动接入)

This commit is contained in:
2026-06-17 02:26:22 +08:00
parent 247dc5da4a
commit b3684f4d1f
3 changed files with 95 additions and 16 deletions

View File

@@ -926,23 +926,26 @@ pub async fn ai_save_provider(
provider_type: String, provider_type: String,
) -> Result<String, String> { ) -> Result<String, String> {
// 编辑已有提供商时保留原 created_at避免被覆盖 // 编辑已有提供商时保留原 created_at避免被覆盖
let created_at = match &id { // F-260614-04c: 编辑路径同时保留原 enabled/weight(负载均衡池可编辑层)。
Some(pid) => state.ai_providers.get_by_id(pid).await // 前端经此 IPC 改 enabled/weight 落库;新建走默认 enabled=true/weight=50。
.map_err(err_str)? let existing = match &id {
.map(|p| p.created_at) Some(pid) => state.ai_providers.get_by_id(pid).await.map_err(err_str)?,
.unwrap_or_else(now_millis), None => None,
None => now_millis(),
}; };
let created_at = existing.as_ref().map(|p| p.created_at.clone()).unwrap_or_else(now_millis);
// is_default编辑保留原值新建时若全表尚无默认则设为默认首个自动默认避免无默认可用 // is_default编辑保留原值新建时若全表尚无默认则设为默认首个自动默认避免无默认可用
let is_default = match &id { let is_default = match &existing {
Some(pid) => state.ai_providers.get_by_id(pid).await Some(p) => p.is_default,
.map_err(err_str)?
.map(|p| p.is_default)
.unwrap_or(false),
None => !state.ai_providers.list_all().await None => !state.ai_providers.list_all().await
.map_err(err_str)? .map_err(err_str)?
.iter().any(|p| p.is_default), .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 列恒空(不入明文)。 // FR-S1:密钥存 OS keyring,DB api_key 列恒空(不入明文)。
// api_key 非空 = 新/改密钥 → 写 keyring;空 = 编辑不改 → 保留原 keyring 密钥不动。 // api_key 非空 = 新/改密钥 → 写 keyring;空 = 编辑不改 → 保留原 keyring 密钥不动。
let provider_id = id.clone().unwrap_or_else(new_id); let provider_id = id.clone().unwrap_or_else(new_id);
@@ -993,10 +996,9 @@ pub async fn ai_save_provider(
config: None, config: None,
created_at, created_at,
updated_at: now_millis(), updated_at: now_millis(),
// F-260614-04: 新建 provider 默认进入负载均衡池(enabled=true,weight=50) // F-260614-04c: enabled/weight 编辑保留原值(负载均衡池可编辑层),新建走默认
// 单 provider 路径零变化(enabled=true 即与迁移前行为等价)。 enabled,
enabled: true, weight,
weight: 50,
}; };
let id = record.id.clone(); let id = record.id.clone();
state state
@@ -1007,7 +1009,52 @@ pub async fn ai_save_provider(
Ok(id) 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] #[tauri::command]
pub async fn ai_set_provider( pub async fn ai_set_provider(
state: State<'_, AppState>, state: State<'_, AppState>,

View File

@@ -125,6 +125,8 @@ pub fn run() {
commands::ai::ai_save_provider, commands::ai::ai_save_provider,
commands::ai::ai_set_provider, commands::ai::ai_set_provider,
commands::ai::ai_delete_provider, commands::ai::ai_delete_provider,
// F-260614-04c: 负载均衡池可编辑层(enabled/weight 轻量更新 + caps 重建)
commands::ai::ai_update_provider_pool,
// F-01 阶段5:测试连接拉取模型列表 + 单模型探测 // F-01 阶段5:测试连接拉取模型列表 + 单模型探测
commands::ai::ai_fetch_models, commands::ai::ai_fetch_models,
commands::ai::ai_probe_model, commands::ai::ai_probe_model,

View File

@@ -294,8 +294,38 @@ impl AppState {
// 启动恢复:重启前卡 pending 的工具审批(内存 pending_approvals 已丢)从审计表重建, // 启动恢复:重启前卡 pending 的工具审批(内存 pending_approvals 已丢)从审计表重建,
// 使重启后待审批不丢。前端经 ai_pending_tool_calls + switchConversation 恢复 toolCard 态。 // 使重启后待审批不丢。前端经 ai_pending_tool_calls + switchConversation 恢复 toolCard 态。
crate::commands::ai::restore_pending_approvals(&state).await; crate::commands::ai::restore_pending_approvals(&state).await;
// F-260614-04c: 启动一次性初始化 per-provider caps 表。
// 根据 DB enabled providers 建 HashMap<provider_id, global_cap>,让 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) 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<String, usize> = 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;
}
} }
/// 构建节点注册表 — 注册内置节点 /// 构建节点注册表 — 注册内置节点