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
This commit is contained in:
ZhenYi 2026-04-27 16:39:52 +08:00
parent 1deea4c671
commit ef529d772b
4 changed files with 164 additions and 24 deletions

View File

@ -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<ProviderModel,
{
let mut active: models::agents::model_provider::ActiveModel = existing.into();
active.updated_at = Set(now);
active.update(db).await
active.update(db).await.map_err(AppError::from)
} else {
let active = models::agents::model_provider::ActiveModel {
id: Set(Uuid::now_v7()),
@ -238,7 +240,10 @@ async fn upsert_provider(db: &AppDatabase, slug: &str) -> Result<ProviderModel,
}
}
async fn upsert_model(
/// Upserts a model by name only (deduplication key), ignoring provider.
/// This ensures each model name exists only once, regardless of how many
/// providers offer it. The first provider encountered is kept.
async fn upsert_model_by_name(
db: &AppDatabase,
provider_id: Uuid,
model: &UpstreamModel,
@ -248,15 +253,18 @@ async fn upsert_model(
let capability = infer_capability(model);
let ctx = context_length(model);
let max_out = max_output_tokens(model);
let model_name = extract_model_name(model);
use models::agents::model::Column as MCol;
if let Some(existing) = ModelEntity::find()
.filter(MCol::ProviderId.eq(provider_id))
.filter(MCol::Name.eq(&model.id))
.filter(MCol::Name.eq(&model_name))
.one(db)
.await?
{
// Update existing model (deduplicated by name)
let mut active: models::agents::model::ActiveModel = existing.clone().into();
// Update provider if changed (first provider wins)
active.provider_id = Set(provider_id);
active.context_length = Set(ctx);
active.max_output_tokens = Set(max_out);
active.status = Set(ModelStatus::Active.to_string());
@ -264,10 +272,11 @@ async fn upsert_model(
let updated = active.update(db).await?;
Ok((updated, false))
} else {
// Create new model
let active = models::agents::model::ActiveModel {
id: Set(Uuid::now_v7()),
provider_id: Set(provider_id),
name: Set(model.id.clone()),
name: Set(model_name),
modality: Set(modality.to_string()),
capability: Set(capability.to_string()),
context_length: Set(ctx),
@ -422,20 +431,123 @@ async fn upsert_parameter_profile(
// Core sync logic ------------------------------------------------------------
/// Extracts the base model name from an upstream model ID.
/// e.g., "openai/gpt-4o-mini" -> "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<i64, AppError> {
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<String, Vec<uuid::Uuid>> =
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<i64, AppError> {
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<UpstreamModel>,
upstream_models: Vec<UpstreamModel>,
) -> 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<String> = 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<i64, AppError> {
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`).

View File

@ -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(())

View File

@ -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
}

View File

@ -168,7 +168,7 @@ impl AppService {
plan: ws.plan,
my_role: membership.role,
created_at: ws.created_at,
}
})
})
.collect();