feat(admin-v2): add ProTable search, scenarios/quick_commands form, tests, remove quota_reset_interval
- Enable ProTable search on Accounts (username/email), Models (model_id/alias), Providers (display_name/name) with hideInSearch for non-searchable columns - Add scenarios (Select tags) and quick_commands (Form.List) to AgentTemplates create form, plus service type updates - Remove unused quota_reset_interval from ProviderKey model, key_pool SQL, handlers, and frontend types; add migration + bump schema to v11 - Add Vitest config, test setup, request interceptor tests (7 cases), authStore tests (8 cases) — all 15 passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
-- 20260331000001_accounts_llm_routing.sql
|
||||
-- 账号级 LLM 路由模式: relay=SaaS中转(Token池), local=本地直连
|
||||
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS llm_routing TEXT NOT NULL DEFAULT 'local'
|
||||
CHECK (llm_routing IN ('relay', 'local'));
|
||||
|
||||
COMMENT ON COLUMN accounts.llm_routing IS 'LLM路由模式: relay=SaaS中转, local=本地直连';
|
||||
@@ -0,0 +1,3 @@
|
||||
-- 20260401000002_remove_quota_reset_interval.sql
|
||||
-- 移除未使用的 quota_reset_interval 字段 (RPM/TPM 限流已足够)
|
||||
ALTER TABLE provider_keys DROP COLUMN IF EXISTS quota_reset_interval;
|
||||
@@ -4,7 +4,7 @@ use sqlx::postgres::PgPoolOptions;
|
||||
use sqlx::PgPool;
|
||||
use crate::error::SaasResult;
|
||||
|
||||
const SCHEMA_VERSION: i32 = 10;
|
||||
const SCHEMA_VERSION: i32 = 11;
|
||||
|
||||
/// 初始化数据库
|
||||
pub async fn init_db(database_url: &str) -> SaasResult<PgPool> {
|
||||
|
||||
@@ -10,7 +10,6 @@ pub struct ProviderKeySelectRow {
|
||||
pub priority: i32,
|
||||
pub max_rpm: Option<i64>,
|
||||
pub max_tpm: Option<i64>,
|
||||
pub quota_reset_interval: Option<String>,
|
||||
}
|
||||
|
||||
/// provider_keys 完整行 (用于列表查询)
|
||||
@@ -22,7 +21,6 @@ pub struct ProviderKeyRow {
|
||||
pub priority: i32,
|
||||
pub max_rpm: Option<i64>,
|
||||
pub max_tpm: Option<i64>,
|
||||
pub quota_reset_interval: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub last_429_at: Option<String>,
|
||||
pub cooldown_until: Option<String>,
|
||||
|
||||
@@ -375,7 +375,6 @@ pub struct AddKeyRequest {
|
||||
pub priority: i32,
|
||||
pub max_rpm: Option<i64>,
|
||||
pub max_tpm: Option<i64>,
|
||||
pub quota_reset_interval: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn add_provider_key(
|
||||
@@ -406,7 +405,6 @@ pub async fn add_provider_key(
|
||||
let key_id = super::key_pool::add_provider_key(
|
||||
&state.db, &provider_id, &req.key_label, &encrypted_value,
|
||||
req.priority, req.max_rpm, req.max_tpm,
|
||||
req.quota_reset_interval.as_deref(),
|
||||
).await?;
|
||||
|
||||
log_operation(&state.db, &ctx.account_id, "provider_key.add", "provider_key", &key_id,
|
||||
|
||||
@@ -26,7 +26,6 @@ pub struct PoolKey {
|
||||
pub priority: i32,
|
||||
pub max_rpm: Option<i64>,
|
||||
pub max_tpm: Option<i64>,
|
||||
pub quota_reset_interval: Option<String>,
|
||||
}
|
||||
|
||||
/// Key 选择结果
|
||||
@@ -43,9 +42,9 @@ pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32])
|
||||
let current_minute = chrono::Utc::now().format("%Y-%m-%dT%H:%M").to_string();
|
||||
|
||||
// 单次查询: 活跃 Key + 当前分钟的 RPM/TPM 使用量 (LEFT JOIN)
|
||||
let rows: Vec<(String, String, i32, Option<i64>, Option<i64>, Option<String>, Option<i64>, Option<i64>)> =
|
||||
let rows: Vec<(String, String, i32, Option<i64>, Option<i64>, Option<i64>, Option<i64>)> =
|
||||
sqlx::query_as(
|
||||
"SELECT pk.id, pk.key_value, pk.priority, pk.max_rpm, pk.max_tpm, pk.quota_reset_interval,
|
||||
"SELECT pk.id, pk.key_value, pk.priority, pk.max_rpm, pk.max_tpm,
|
||||
uw.request_count, uw.token_count
|
||||
FROM provider_keys pk
|
||||
LEFT JOIN key_usage_window uw ON pk.id = uw.key_id AND uw.window_minute = $1
|
||||
@@ -54,7 +53,7 @@ pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32])
|
||||
ORDER BY pk.priority ASC, pk.last_used_at ASC NULLS FIRST"
|
||||
).bind(¤t_minute).bind(provider_id).bind(&now).fetch_all(db).await?;
|
||||
|
||||
for (id, key_value, priority, max_rpm, max_tpm, quota_reset_interval, req_count, token_count) in &rows {
|
||||
for (id, key_value, priority, max_rpm, max_tpm, req_count, token_count) in &rows {
|
||||
// RPM 检查
|
||||
if let Some(rpm_limit) = max_rpm {
|
||||
if *rpm_limit > 0 {
|
||||
@@ -86,7 +85,6 @@ pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32])
|
||||
priority: *priority,
|
||||
max_rpm: *max_rpm,
|
||||
max_tpm: *max_tpm,
|
||||
quota_reset_interval: quota_reset_interval.clone(),
|
||||
},
|
||||
key_id: id.clone(),
|
||||
});
|
||||
@@ -124,7 +122,6 @@ pub async fn select_best_key(db: &PgPool, provider_id: &str, enc_key: &[u8; 32])
|
||||
priority: 0,
|
||||
max_rpm: None,
|
||||
max_tpm: None,
|
||||
quota_reset_interval: None,
|
||||
},
|
||||
key_id: "provider-fallback".to_string(),
|
||||
});
|
||||
@@ -212,7 +209,7 @@ pub async fn list_provider_keys(
|
||||
) -> SaasResult<Vec<serde_json::Value>> {
|
||||
let rows: Vec<ProviderKeyRow> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, provider_id, key_label, priority, max_rpm, max_tpm, quota_reset_interval, is_active,
|
||||
"SELECT id, provider_id, key_label, priority, max_rpm, max_tpm, is_active,
|
||||
last_429_at, cooldown_until, total_requests, total_tokens, created_at, updated_at
|
||||
FROM provider_keys WHERE provider_id = $1 ORDER BY priority ASC"
|
||||
).bind(provider_id).fetch_all(db).await?;
|
||||
@@ -225,7 +222,6 @@ pub async fn list_provider_keys(
|
||||
"priority": r.priority,
|
||||
"max_rpm": r.max_rpm,
|
||||
"max_tpm": r.max_tpm,
|
||||
"quota_reset_interval": r.quota_reset_interval,
|
||||
"is_active": r.is_active,
|
||||
"last_429_at": r.last_429_at,
|
||||
"cooldown_until": r.cooldown_until,
|
||||
@@ -246,17 +242,16 @@ pub async fn add_provider_key(
|
||||
priority: i32,
|
||||
max_rpm: Option<i64>,
|
||||
max_tpm: Option<i64>,
|
||||
quota_reset_interval: Option<&str>,
|
||||
) -> SaasResult<String> {
|
||||
let id = uuid::Uuid::new_v4().to_string();
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO provider_keys (id, provider_id, key_label, key_value, priority, max_rpm, max_tpm, quota_reset_interval, is_active, total_requests, total_tokens, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, TRUE, 0, 0, $9, $9)"
|
||||
"INSERT INTO provider_keys (id, provider_id, key_label, key_value, priority, max_rpm, max_tpm, is_active, total_requests, total_tokens, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, TRUE, 0, 0, $8, $8)"
|
||||
)
|
||||
.bind(&id).bind(provider_id).bind(key_label).bind(key_value)
|
||||
.bind(priority).bind(max_rpm).bind(max_tpm).bind(quota_reset_interval).bind(&now)
|
||||
.bind(priority).bind(max_rpm).bind(max_tpm).bind(&now)
|
||||
.execute(db).await?;
|
||||
|
||||
tracing::info!("Added key '{}' to provider {}", key_label, provider_id);
|
||||
|
||||
Reference in New Issue
Block a user