From 5599cefc413fb28a43a4e3d26948282e2ff5f11e Mon Sep 17 00:00:00 2001 From: iven Date: Sun, 12 Apr 2026 08:10:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(saas):=20=E6=8E=A5=E9=80=9A=20embedding=20?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E7=AE=A1=E7=90=86=E5=85=A8=E6=A0=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 数据库 migration 已有 is_embedding/model_type 列但全栈未使用。 打通 4 层: ModelRow → ModelInfo/CRUD → CachedModel → Admin 前端。 relay/models 端点也返回 is_embedding 字段,前端可按类型过滤。 --- admin-v2/src/pages/ModelServices.tsx | 4 +++ admin-v2/src/types/index.ts | 2 ++ crates/zclaw-saas/src/cache.rs | 10 +++++-- crates/zclaw-saas/src/model_config/service.rs | 30 +++++++++++-------- crates/zclaw-saas/src/model_config/types.rs | 6 ++++ crates/zclaw-saas/src/models/model.rs | 2 ++ crates/zclaw-saas/src/relay/handlers.rs | 9 ++++-- 7 files changed, 45 insertions(+), 18 deletions(-) diff --git a/admin-v2/src/pages/ModelServices.tsx b/admin-v2/src/pages/ModelServices.tsx index 392890c..82f130e 100644 --- a/admin-v2/src/pages/ModelServices.tsx +++ b/admin-v2/src/pages/ModelServices.tsx @@ -67,6 +67,7 @@ function ProviderModelsTable({ providerId }: { providerId: string }) { const columns: ProColumns[] = [ { title: '模型 ID', dataIndex: 'model_id', width: 180, render: (_, r) => {r.model_id} }, { title: '别名', dataIndex: 'alias', width: 120 }, + { title: '类型', dataIndex: 'is_embedding', width: 80, render: (_, r) => r.is_embedding ? Embedding : Chat }, { title: '上下文窗口', dataIndex: 'context_window', width: 100, render: (_, r) => r.context_window?.toLocaleString() }, { title: '最大输出', dataIndex: 'max_output_tokens', width: 90, render: (_, r) => r.max_output_tokens?.toLocaleString() }, { title: '流式', dataIndex: 'supports_streaming', width: 60, render: (_, r) => r.supports_streaming ? : }, @@ -128,6 +129,9 @@ function ProviderModelsTable({ providerId }: { providerId: string }) { + + + diff --git a/admin-v2/src/types/index.ts b/admin-v2/src/types/index.ts index f8dd89d..acf35c8 100644 --- a/admin-v2/src/types/index.ts +++ b/admin-v2/src/types/index.ts @@ -70,6 +70,8 @@ export interface Model { supports_streaming: boolean supports_vision: boolean enabled: boolean + is_embedding: boolean + model_type: string pricing_input: number pricing_output: number } diff --git a/crates/zclaw-saas/src/cache.rs b/crates/zclaw-saas/src/cache.rs index f35ae35..a7d6c27 100644 --- a/crates/zclaw-saas/src/cache.rs +++ b/crates/zclaw-saas/src/cache.rs @@ -21,6 +21,8 @@ pub struct CachedModel { pub supports_streaming: bool, pub supports_vision: bool, pub enabled: bool, + pub is_embedding: bool, + pub model_type: String, pub pricing_input: f64, pub pricing_output: f64, } @@ -111,15 +113,15 @@ impl AppCache { self.providers.retain(|k, _| provider_keys.contains(k)); // Load models (key = model_id for relay lookup) — insert-then-retain - let model_rows: Vec<(String, String, String, String, i64, i64, bool, bool, bool, f64, f64)> = sqlx::query_as( + let model_rows: Vec<(String, String, String, String, i64, i64, bool, bool, bool, bool, String, f64, f64)> = sqlx::query_as( "SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, - supports_streaming, supports_vision, enabled, pricing_input, pricing_output + supports_streaming, supports_vision, enabled, is_embedding, model_type, pricing_input, pricing_output FROM models" ).fetch_all(db).await?; let model_keys: HashSet = model_rows.iter().map(|(_, _, mid, ..)| mid.clone()).collect(); for (id, provider_id, model_id, alias, context_window, max_output_tokens, - supports_streaming, supports_vision, enabled, pricing_input, pricing_output) in &model_rows + supports_streaming, supports_vision, enabled, is_embedding, model_type, pricing_input, pricing_output) in &model_rows { self.models.insert(model_id.clone(), CachedModel { id: id.clone(), @@ -131,6 +133,8 @@ impl AppCache { supports_streaming: *supports_streaming, supports_vision: *supports_vision, enabled: *enabled, + is_embedding: *is_embedding, + model_type: model_type.clone(), pricing_input: *pricing_input, pricing_output: *pricing_output, }); diff --git a/crates/zclaw-saas/src/model_config/service.rs b/crates/zclaw-saas/src/model_config/service.rs index 7c9bf9f..ec7d0fe 100644 --- a/crates/zclaw-saas/src/model_config/service.rs +++ b/crates/zclaw-saas/src/model_config/service.rs @@ -162,13 +162,13 @@ pub async fn list_models( let (count_sql, data_sql) = if provider_id.is_some() { ( "SELECT COUNT(*) FROM models WHERE provider_id = $1", - "SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, pricing_input, pricing_output, created_at::TEXT, updated_at::TEXT + "SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, is_embedding, model_type, pricing_input, pricing_output, created_at::TEXT, updated_at::TEXT FROM models WHERE provider_id = $1 ORDER BY alias LIMIT $2 OFFSET $3", ) } else { ( "SELECT COUNT(*) FROM models", - "SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, pricing_input, pricing_output, created_at::TEXT, updated_at::TEXT + "SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, is_embedding, model_type, pricing_input, pricing_output, created_at::TEXT, updated_at::TEXT FROM models ORDER BY provider_id, alias LIMIT $1 OFFSET $2", ) }; @@ -186,7 +186,7 @@ pub async fn list_models( let rows = query.bind(ps as i64).bind(offset).fetch_all(db).await?; let items = rows.into_iter().map(|r| { - ModelInfo { id: r.id, provider_id: r.provider_id, model_id: r.model_id, alias: r.alias, context_window: r.context_window, max_output_tokens: r.max_output_tokens, supports_streaming: r.supports_streaming, supports_vision: r.supports_vision, enabled: r.enabled, pricing_input: r.pricing_input, pricing_output: r.pricing_output, created_at: r.created_at, updated_at: r.updated_at } + ModelInfo { id: r.id, provider_id: r.provider_id, model_id: r.model_id, alias: r.alias, context_window: r.context_window, max_output_tokens: r.max_output_tokens, supports_streaming: r.supports_streaming, supports_vision: r.supports_vision, enabled: r.enabled, is_embedding: r.is_embedding, model_type: r.model_type.clone(), pricing_input: r.pricing_input, pricing_output: r.pricing_output, created_at: r.created_at, updated_at: r.updated_at } }).collect(); Ok(PaginatedResponse { items, total: total.0, page: p, page_size: ps }) @@ -225,15 +225,17 @@ pub async fn create_model(db: &PgPool, req: &CreateModelRequest) -> SaasResult SaasResult SaasResult { let row: Option = sqlx::query_as( - "SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, pricing_input, pricing_output, created_at::TEXT, updated_at::TEXT + "SELECT id, provider_id, model_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, enabled, is_embedding, model_type, pricing_input, pricing_output, created_at::TEXT, updated_at::TEXT FROM models WHERE id = $1" ) .bind(model_id) @@ -251,7 +253,7 @@ pub async fn get_model(db: &PgPool, model_id: &str) -> SaasResult { let r = row.ok_or_else(|| SaasError::NotFound(format!("模型 {} 不存在", model_id)))?; - Ok(ModelInfo { id: r.id, provider_id: r.provider_id, model_id: r.model_id, alias: r.alias, context_window: r.context_window, max_output_tokens: r.max_output_tokens, supports_streaming: r.supports_streaming, supports_vision: r.supports_vision, enabled: r.enabled, pricing_input: r.pricing_input, pricing_output: r.pricing_output, created_at: r.created_at, updated_at: r.updated_at }) + Ok(ModelInfo { id: r.id, provider_id: r.provider_id, model_id: r.model_id, alias: r.alias, context_window: r.context_window, max_output_tokens: r.max_output_tokens, supports_streaming: r.supports_streaming, supports_vision: r.supports_vision, enabled: r.enabled, is_embedding: r.is_embedding, model_type: r.model_type.clone(), pricing_input: r.pricing_input, pricing_output: r.pricing_output, created_at: r.created_at, updated_at: r.updated_at }) } pub async fn update_model( @@ -269,10 +271,12 @@ pub async fn update_model( supports_streaming = COALESCE($4, supports_streaming), supports_vision = COALESCE($5, supports_vision), enabled = COALESCE($6, enabled), - pricing_input = COALESCE($7, pricing_input), - pricing_output = COALESCE($8, pricing_output), - updated_at = $9 - WHERE id = $10" + is_embedding = COALESCE($7, is_embedding), + model_type = COALESCE($8, model_type), + pricing_input = COALESCE($9, pricing_input), + pricing_output = COALESCE($10, pricing_output), + updated_at = $11 + WHERE id = $12" ) .bind(req.alias.as_deref()) .bind(req.context_window) @@ -280,6 +284,8 @@ pub async fn update_model( .bind(req.supports_streaming) .bind(req.supports_vision) .bind(req.enabled) + .bind(req.is_embedding) + .bind(req.model_type.as_deref()) .bind(req.pricing_input) .bind(req.pricing_output) .bind(&now) diff --git a/crates/zclaw-saas/src/model_config/types.rs b/crates/zclaw-saas/src/model_config/types.rs index 1790b12..b0c9870 100644 --- a/crates/zclaw-saas/src/model_config/types.rs +++ b/crates/zclaw-saas/src/model_config/types.rs @@ -56,6 +56,8 @@ pub struct ModelInfo { pub supports_streaming: bool, pub supports_vision: bool, pub enabled: bool, + pub is_embedding: bool, + pub model_type: String, pub pricing_input: f64, pub pricing_output: f64, pub created_at: String, @@ -71,6 +73,8 @@ pub struct CreateModelRequest { pub max_output_tokens: Option, pub supports_streaming: Option, pub supports_vision: Option, + pub is_embedding: Option, + pub model_type: Option, pub pricing_input: Option, pub pricing_output: Option, } @@ -83,6 +87,8 @@ pub struct UpdateModelRequest { pub supports_streaming: Option, pub supports_vision: Option, pub enabled: Option, + pub is_embedding: Option, + pub model_type: Option, pub pricing_input: Option, pub pricing_output: Option, } diff --git a/crates/zclaw-saas/src/models/model.rs b/crates/zclaw-saas/src/models/model.rs index 8307c0e..219c839 100644 --- a/crates/zclaw-saas/src/models/model.rs +++ b/crates/zclaw-saas/src/models/model.rs @@ -14,6 +14,8 @@ pub struct ModelRow { pub supports_streaming: bool, pub supports_vision: bool, pub enabled: bool, + pub is_embedding: bool, + pub model_type: String, pub pricing_input: f64, pub pricing_output: f64, pub created_at: String, diff --git a/crates/zclaw-saas/src/relay/handlers.rs b/crates/zclaw-saas/src/relay/handlers.rs index b9f06ef..6cb9160 100644 --- a/crates/zclaw-saas/src/relay/handlers.rs +++ b/crates/zclaw-saas/src/relay/handlers.rs @@ -373,9 +373,10 @@ pub async fn list_available_models( _ctx: Extension, ) -> SaasResult>> { // 单次 JOIN 查询替代 2 次全量加载 - let rows: Vec<(String, String, String, i64, i64, bool, bool)> = sqlx::query_as( + let rows: Vec<(String, String, String, i64, i64, bool, bool, bool, String)> = sqlx::query_as( "SELECT m.model_id, m.provider_id, m.alias, m.context_window, - m.max_output_tokens, m.supports_streaming, m.supports_vision + m.max_output_tokens, m.supports_streaming, m.supports_vision, + m.is_embedding, m.model_type FROM models m INNER JOIN providers p ON m.provider_id = p.id WHERE m.enabled = true AND p.enabled = true @@ -385,7 +386,7 @@ pub async fn list_available_models( .await?; let mut available: Vec = rows.into_iter() - .map(|(model_id, provider_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision)| { + .map(|(model_id, provider_id, alias, context_window, max_output_tokens, supports_streaming, supports_vision, is_embedding, model_type)| { serde_json::json!({ "id": model_id, "provider_id": provider_id, @@ -394,6 +395,8 @@ pub async fn list_available_models( "max_output_tokens": max_output_tokens, "supports_streaming": supports_streaming, "supports_vision": supports_vision, + "is_embedding": is_embedding, + "model_type": model_type, }) }) .collect();