feat(gateway): 重构 usage 数据层、迁移系统与系统导入

数据库迁移:
- 引入 baseline v2 bootstrap,空库首次启动自动初始化
- 服务启动不再自动执行迁移,需显式 `--migrate` 运行
- 新增 pending migration 检测,schema 落后时拒绝启动

Usage 数据层:
- usage body 存储外部化为独立 blob 表
- 新增 HTTP audit 表拆分存储请求/响应头与 body ref
- 后台清理任务支持 legacy body ref 元数据迁移
- usage runtime 写入迁移到专用 tokio runtime(独立线程池, 8MB 栈)

系统导入/导出:
- 支持用户、API Keys、钱包数据的完整导入
- 兼容 legacy 与 v1.3+ 两种导出格式

其他改进:
- executor outcome 增加 runtime miss 诊断上下文
- 主 tokio runtime 栈大小调整为 8MB
- 前端 provider 管理支持 base URL 配置
- dev.sh 支持 --migrate 参数
This commit is contained in:
fawney19
2026-04-13 14:01:22 +08:00
parent 3698e5a833
commit 5bb08e6aa4
106 changed files with 21736 additions and 1529 deletions

View File

@@ -385,15 +385,15 @@ impl AuthApiKeyWriteRepository for InMemoryAuthApiKeySnapshotRepository {
StoredAuthApiKeySnapshot {
api_key_id: record.api_key_id.clone(),
api_key_name: record.name.clone(),
api_key_is_active: true,
api_key_is_active: record.is_active,
api_key_is_locked: false,
api_key_is_standalone: false,
api_key_rate_limit: Some(record.rate_limit),
api_key_concurrent_limit: Some(record.concurrent_limit),
api_key_expires_at_unix_secs: None,
api_key_allowed_providers: None,
api_key_allowed_api_formats: None,
api_key_allowed_models: None,
api_key_expires_at_unix_secs: record.expires_at_unix_secs,
api_key_allowed_providers: record.allowed_providers.clone(),
api_key_allowed_api_formats: record.allowed_api_formats.clone(),
api_key_allowed_models: record.allowed_models.clone(),
..template
}
} else {
@@ -413,15 +413,24 @@ impl AuthApiKeyWriteRepository for InMemoryAuthApiKeySnapshotRepository {
None,
record.api_key_id.clone(),
record.name.clone(),
true,
record.is_active,
false,
false,
Some(record.rate_limit),
Some(record.concurrent_limit),
None,
None,
None,
None,
record.expires_at_unix_secs.map(|value| value as i64),
record
.allowed_providers
.as_ref()
.map(|value| serde_json::json!(value)),
record
.allowed_api_formats
.as_ref()
.map(|value| serde_json::json!(value)),
record
.allowed_models
.as_ref()
.map(|value| serde_json::json!(value)),
)?
};
@@ -431,17 +440,26 @@ impl AuthApiKeyWriteRepository for InMemoryAuthApiKeySnapshotRepository {
record.key_hash.clone(),
record.key_encrypted,
record.name,
None,
None,
None,
record
.allowed_providers
.as_ref()
.map(|value| serde_json::json!(value)),
record
.allowed_api_formats
.as_ref()
.map(|value| serde_json::json!(value)),
record
.allowed_models
.as_ref()
.map(|value| serde_json::json!(value)),
Some(record.rate_limit),
Some(record.concurrent_limit),
None,
true,
None,
false,
0,
0.0,
record.force_capabilities,
record.is_active,
record.expires_at_unix_secs.map(|value| value as i64),
record.auto_delete_on_expiry,
record.total_requests as i64,
record.total_cost_usd,
false,
)?;
@@ -487,12 +505,12 @@ impl AuthApiKeyWriteRepository for InMemoryAuthApiKeySnapshotRepository {
StoredAuthApiKeySnapshot {
api_key_id: record.api_key_id.clone(),
api_key_name: record.name.clone(),
api_key_is_active: true,
api_key_is_active: record.is_active,
api_key_is_locked: false,
api_key_is_standalone: true,
api_key_rate_limit: Some(record.rate_limit),
api_key_concurrent_limit: Some(record.concurrent_limit),
api_key_expires_at_unix_secs: None,
api_key_expires_at_unix_secs: record.expires_at_unix_secs,
api_key_allowed_providers: record.allowed_providers.clone(),
api_key_allowed_api_formats: record.allowed_api_formats.clone(),
api_key_allowed_models: record.allowed_models.clone(),
@@ -515,12 +533,12 @@ impl AuthApiKeyWriteRepository for InMemoryAuthApiKeySnapshotRepository {
None,
record.api_key_id.clone(),
record.name.clone(),
true,
record.is_active,
false,
true,
Some(record.rate_limit),
Some(record.concurrent_limit),
None,
record.expires_at_unix_secs.map(|value| value as i64),
record
.allowed_providers
.as_ref()
@@ -556,12 +574,12 @@ impl AuthApiKeyWriteRepository for InMemoryAuthApiKeySnapshotRepository {
.map(|value| serde_json::json!(value)),
Some(record.rate_limit),
Some(record.concurrent_limit),
None,
true,
None,
false,
0,
0.0,
record.force_capabilities,
record.is_active,
record.expires_at_unix_secs.map(|value| value as i64),
record.auto_delete_on_expiry,
record.total_requests as i64,
record.total_cost_usd,
true,
)?;

View File

@@ -311,10 +311,14 @@ INSERT INTO api_keys (
key_hash,
key_encrypted,
name,
allowed_providers,
allowed_api_formats,
allowed_models,
rate_limit,
concurrent_limit,
force_capabilities,
is_active,
expires_at,
is_locked,
is_standalone,
auto_delete_on_expiry,
@@ -331,13 +335,17 @@ VALUES (
$5,
$6,
$7,
NULL,
TRUE,
$8,
$9,
$10,
$11,
$12,
$13,
FALSE,
FALSE,
FALSE,
0,
0,
$14,
$15,
$16,
NOW(),
NOW()
)
@@ -375,6 +383,7 @@ INSERT INTO api_keys (
concurrent_limit,
force_capabilities,
is_active,
expires_at,
is_locked,
is_standalone,
auto_delete_on_expiry,
@@ -394,13 +403,14 @@ VALUES (
$8,
$9,
$10,
NULL,
TRUE,
$11,
$12,
$13,
FALSE,
TRUE,
FALSE,
0,
0,
$14,
$15,
$16,
NOW(),
NOW()
)
@@ -920,14 +930,46 @@ impl AuthApiKeyWriteRepository for SqlxAuthApiKeySnapshotReadRepository {
&self,
record: CreateUserApiKeyRecord,
) -> Result<Option<StoredAuthApiKeyExportRecord>, DataLayerError> {
let allowed_providers = record
.allowed_providers
.map(serde_json::to_value)
.transpose()
.map_err(|err| DataLayerError::UnexpectedValue(err.to_string()))?;
let allowed_api_formats = record
.allowed_api_formats
.map(serde_json::to_value)
.transpose()
.map_err(|err| DataLayerError::UnexpectedValue(err.to_string()))?;
let allowed_models = record
.allowed_models
.map(serde_json::to_value)
.transpose()
.map_err(|err| DataLayerError::UnexpectedValue(err.to_string()))?;
let expires_at = record
.expires_at_unix_secs
.map(|value| {
chrono::DateTime::<chrono::Utc>::from_timestamp(value as i64, 0).ok_or_else(|| {
DataLayerError::UnexpectedValue(format!("invalid api_keys.expires_at: {value}"))
})
})
.transpose()?;
let row = sqlx::query(CREATE_USER_API_KEY_SQL)
.bind(record.api_key_id)
.bind(record.user_id)
.bind(record.key_hash)
.bind(record.key_encrypted)
.bind(record.name)
.bind(allowed_providers)
.bind(allowed_api_formats)
.bind(allowed_models)
.bind(record.rate_limit)
.bind(record.concurrent_limit)
.bind(record.force_capabilities)
.bind(record.is_active)
.bind(expires_at)
.bind(record.auto_delete_on_expiry)
.bind(record.total_requests as i64)
.bind(record.total_cost_usd)
.fetch_optional(&self.pool)
.await
.map_postgres_err()?;
@@ -953,6 +995,14 @@ impl AuthApiKeyWriteRepository for SqlxAuthApiKeySnapshotReadRepository {
.map(serde_json::to_value)
.transpose()
.map_err(|err| DataLayerError::UnexpectedValue(err.to_string()))?;
let expires_at = record
.expires_at_unix_secs
.map(|value| {
chrono::DateTime::<chrono::Utc>::from_timestamp(value as i64, 0).ok_or_else(|| {
DataLayerError::UnexpectedValue(format!("invalid api_keys.expires_at: {value}"))
})
})
.transpose()?;
let row = sqlx::query(CREATE_STANDALONE_API_KEY_SQL)
.bind(record.api_key_id)
.bind(record.user_id)
@@ -964,6 +1014,12 @@ impl AuthApiKeyWriteRepository for SqlxAuthApiKeySnapshotReadRepository {
.bind(allowed_models)
.bind(record.rate_limit)
.bind(record.concurrent_limit)
.bind(record.force_capabilities)
.bind(record.is_active)
.bind(expires_at)
.bind(record.auto_delete_on_expiry)
.bind(record.total_requests as i64)
.bind(record.total_cost_usd)
.fetch_optional(&self.pool)
.await
.map_postgres_err()?;

View File

@@ -352,15 +352,24 @@ pub struct StandaloneApiKeyExportListQuery {
pub is_active: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct CreateUserApiKeyRecord {
pub user_id: String,
pub api_key_id: String,
pub key_hash: String,
pub key_encrypted: Option<String>,
pub name: Option<String>,
pub allowed_providers: Option<Vec<String>>,
pub allowed_api_formats: Option<Vec<String>>,
pub allowed_models: Option<Vec<String>>,
pub rate_limit: i32,
pub concurrent_limit: i32,
pub force_capabilities: Option<serde_json::Value>,
pub is_active: bool,
pub expires_at_unix_secs: Option<u64>,
pub auto_delete_on_expiry: bool,
pub total_requests: u64,
pub total_cost_usd: f64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -371,7 +380,7 @@ pub struct UpdateUserApiKeyBasicRecord {
pub rate_limit: Option<i32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct CreateStandaloneApiKeyRecord {
pub user_id: String,
pub api_key_id: String,
@@ -383,6 +392,12 @@ pub struct CreateStandaloneApiKeyRecord {
pub allowed_models: Option<Vec<String>>,
pub rate_limit: i32,
pub concurrent_limit: i32,
pub force_capabilities: Option<serde_json::Value>,
pub is_active: bool,
pub expires_at_unix_secs: Option<u64>,
pub auto_delete_on_expiry: bool,
pub total_requests: u64,
pub total_cost_usd: f64,
}
#[derive(Debug, Clone, PartialEq, Eq)]