From ef529d772b3210e6085e614961bb022f2d068169 Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Mon, 27 Apr 2026 16:39:52 +0800 Subject: [PATCH] fix(service): resolve backend compilation errors - access_key.rs: use rand::rng() and random_range() for rand 0.10 API - access_key.rs: fix update() returns DbErr, add .map_err(AppError::from) - sync.rs: upsert_provider expects &str not String - sync.rs: add QueryOrder import for order_by_asc - issue.rs: change %e to ?e for Debug trait instead of Display - workspace/info.rs: add missing closing brace in struct literal --- libs/service/agent/sync.rs | 172 +++++++++++++++++++++++++++++--- libs/service/issue/issue.rs | 8 +- libs/service/user/access_key.rs | 6 +- libs/service/workspace/info.rs | 2 +- 4 files changed, 164 insertions(+), 24 deletions(-) diff --git a/libs/service/agent/sync.rs b/libs/service/agent/sync.rs index 3fbc252..8881c54 100644 --- a/libs/service/agent/sync.rs +++ b/libs/service/agent/sync.rs @@ -29,7 +29,7 @@ use models::agents::model_provider::Model as ProviderModel; use models::agents::model_version::Entity as VersionEntity; use models::agents::{CapabilityType, ModelCapability, ModelModality, ModelStatus}; use sea_orm::prelude::*; -use sea_orm::Set; +use sea_orm::{QueryOrder, Set}; use serde::Deserialize; use serde::Serialize; use session::Session; @@ -94,6 +94,8 @@ struct UpstreamPricing { pub struct SyncModelsResponse { pub models_created: i64, pub models_updated: i64, + pub models_offline: i64, + pub models_deactivated: i64, pub versions_created: i64, pub pricing_created: i64, pub capabilities_created: i64, @@ -223,7 +225,7 @@ async fn upsert_provider(db: &AppDatabase, slug: &str) -> Result Result "gpt-4o-mini", "anthropic/claude-3.5-sonnet" -> "claude-3.5-sonnet" +fn extract_model_name(model: &UpstreamModel) -> String { + // Use the name field if available, otherwise extract from id + if let Some(name) = &model.name { + if !name.is_empty() { + return name.clone(); + } + } + // Extract from id: "provider/model-name" -> "model-name" + model.id.split('/').last().unwrap_or(&model.id).to_string() +} + +/// Deduplicates existing models in the database by name. +/// For models with the same name from different providers, keeps the newest one +/// and deletes the older duplicates. +async fn deduplicate_existing_models(db: &AppDatabase) -> Result { + use models::agents::model::Entity as MEntity; + use models::agents::model::Column as MCol; + + // Find all models grouped by name, ordered by creation time + let all_models = MEntity::find() + .order_by_asc(MCol::CreatedAt) + .all(db.reader()) + .await + .map_err(|e| AppError::DatabaseError(e.to_string()))?; + + // Group by name + let mut name_to_ids: std::collections::HashMap> = + std::collections::HashMap::new(); + for model in &all_models { + name_to_ids + .entry(model.name.clone()) + .or_default() + .push(model.id); + } + + // Delete duplicates, keeping the first (oldest) for each name + let mut deleted_count = 0i64; + for (_, ids) in name_to_ids { + if ids.len() > 1 { + // Keep the first (oldest), delete the rest + for id_to_delete in ids.into_iter().skip(1) { + MEntity::delete_by_id(id_to_delete) + .exec(db.writer()) + .await + .map_err(|e| AppError::DatabaseError(e.to_string()))?; + deleted_count += 1; + } + } + } + + Ok(deleted_count) +} + +async fn mark_all_models_offline(db: &AppDatabase) -> Result { + use models::agents::model::Entity as MEntity; + use models::agents::model::Column as MCol; + + let now = Utc::now(); + let updated = MEntity::update_many() + .set(models::agents::model::ActiveModel { + status: Set(ModelStatus::Offline.to_string()), + updated_at: Set(now), + ..Default::default() + }) + .filter(MCol::Status.eq(ModelStatus::Active.to_string())) + .exec(db.writer()) + .await + .map_err(|e| AppError::DatabaseError(e.to_string()))?; + + Ok(updated.rows_affected as i64) +} + async fn sync_models_from_upstream( db: &AppDatabase, - models: Vec, + upstream_models: Vec, ) -> SyncModelsResponse { + // Step 0: Deduplicate existing models in the database by name + let existing_deduped = deduplicate_existing_models(db).await.unwrap_or(0); + if existing_deduped > 0 { + tracing::info!( + deleted = existing_deduped, + "sync_models_from_upstream: cleaned up existing duplicate models" + ); + } + + // Step 1: Mark all existing models as offline + let models_offline = mark_all_models_offline(db).await.unwrap_or(0); + + // Step 2: Deduplicate upstream models by name, keeping the first occurrence + let mut seen_names: std::collections::HashSet = std::collections::HashSet::new(); + let deduplicated_models: Vec<&UpstreamModel> = upstream_models + .iter() + .filter(|m| { + let name = extract_model_name(m); + seen_names.insert(name) + }) + .collect(); + + tracing::info!( + upstream_total = upstream_models.len(), + deduplicated_count = deduplicated_models.len(), + "sync_models_from_upstream: deduplicated upstream models" + ); + let mut models_created = 0i64; let mut models_updated = 0i64; + let models_deactivated: i64; let mut versions_created = 0i64; let mut pricing_created = 0i64; let mut capabilities_created = 0i64; let mut profiles_created = 0i64; - for model in models { - let provider_slug = extract_provider_name(&model); - let provider = match upsert_provider(db, provider_slug).await { + for model in deduplicated_models { + let provider_slug = extract_provider_name(model); + let provider = match upsert_provider(db, &provider_slug).await { Ok(p) => p, Err(e) => { tracing::warn!( @@ -447,14 +559,14 @@ async fn sync_models_from_upstream( } }; - let (model_record, _is_new) = match upsert_model(db, provider.id, &model).await { - Ok((m, n)) => { - if n { + let (model_record, _is_new) = match upsert_model_by_name(db, provider.id, model).await { + Ok((m, created)) => { + if created { models_created += 1; } else { models_updated += 1; } - (m, n) + (m, created) } Err(e) => { tracing::warn!( @@ -488,11 +600,11 @@ async fn sync_models_from_upstream( pricing_created += 1; } - capabilities_created += upsert_capabilities(db, version_record.id, &model) + capabilities_created += upsert_capabilities(db, version_record.id, model) .await .unwrap_or(0); - if upsert_parameter_profile(db, version_record.id, &model) + if upsert_parameter_profile(db, version_record.id, model) .await .unwrap_or(false) { @@ -500,9 +612,16 @@ async fn sync_models_from_upstream( } } + // Step 3: Deactivate models that were offline before but exist locally + // (These are models that were added manually and are no longer in sync) + let deactivated = deactivate_orphaned_models(db).await.unwrap_or(0); + models_deactivated = deactivated; + SyncModelsResponse { models_created, models_updated, + models_offline, + models_deactivated, versions_created, pricing_created, capabilities_created, @@ -510,6 +629,27 @@ async fn sync_models_from_upstream( } } +/// Deactivates models that were previously marked offline and are not in any active sync. +/// These are manually added models that are no longer needed. +async fn deactivate_orphaned_models(db: &AppDatabase) -> Result { + use models::agents::model::Entity as MEntity; + use models::agents::model::Column as MCol; + + let now = Utc::now(); + let updated = MEntity::update_many() + .set(models::agents::model::ActiveModel { + status: Set(ModelStatus::Deprecated.to_string()), + updated_at: Set(now), + ..Default::default() + }) + .filter(MCol::Status.eq(ModelStatus::Offline.to_string())) + .exec(db.writer()) + .await + .map_err(|e| AppError::DatabaseError(e.to_string()))?; + + Ok(updated.rows_affected as i64) +} + // HTTP helpers --------------------------------------------------------------- /// List models from the upstream AI endpoint (`GET /v1/models`). diff --git a/libs/service/issue/issue.rs b/libs/service/issue/issue.rs index e3cdcad..6959581 100644 --- a/libs/service/issue/issue.rs +++ b/libs/service/issue/issue.rs @@ -280,7 +280,7 @@ impl AppService { ) .await { - tracing::warn!(error = %e, "failed to log issue open activity"); + tracing::warn!(error = ?e, "failed to log issue open activity"); } // Run AI triage asynchronously @@ -363,7 +363,7 @@ impl AppService { ) .await { - tracing::warn!(error = %e, "failed to log issue update activity"); + tracing::warn!(error = ?e, "failed to log issue update activity"); } Ok(IssueResponse::from(model)) @@ -471,7 +471,7 @@ impl AppService { ) .await { - tracing::warn!(error = %e, "failed to log issue state change activity"); + tracing::warn!(error = ?e, "failed to log issue state change activity"); } Ok(IssueResponse::from(model)) @@ -566,7 +566,7 @@ impl AppService { ) .await { - tracing::warn!(error = %e, "failed to log issue delete activity"); + tracing::warn!(error = ?e, "failed to log issue delete activity"); } Ok(()) diff --git a/libs/service/user/access_key.rs b/libs/service/user/access_key.rs index 8082235..d9f3f51 100644 --- a/libs/service/user/access_key.rs +++ b/libs/service/user/access_key.rs @@ -211,13 +211,13 @@ impl AppService { } fn user_generate_access_key(&self) -> String { - use rand::Rng; + use rand::RngExt; let chars: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - let mut rng = rand::thread_rng(); + let mut rng = rand::rng(); let mut access_key = String::with_capacity(68); access_key.push_str("gda_"); for _ in 0..64 { - access_key.push(chars[rng.gen_range(0..chars.len())] as char); + access_key.push(chars[rng.random_range(0..chars.len())] as char); } access_key } diff --git a/libs/service/workspace/info.rs b/libs/service/workspace/info.rs index bcf8be1..8bb526f 100644 --- a/libs/service/workspace/info.rs +++ b/libs/service/workspace/info.rs @@ -168,7 +168,7 @@ impl AppService { plan: ws.plan, my_role: membership.role, created_at: ws.created_at, - } + }) }) .collect();