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

@@ -1,5 +1,5 @@
use super::super::stats::{AdminStatsTimeRange, AdminStatsUsageFilter};
use super::analytics::admin_usage_provider_key_names;
use super::analytics::{admin_usage_api_key_names, admin_usage_provider_key_names};
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::query_param_value;
use crate::GatewayError;
@@ -85,10 +85,13 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response(
if requested_ids.is_none() && items.len() > 50 {
items.truncate(50);
}
let api_key_names = admin_usage_api_key_names(state, &items).await?;
let provider_key_names = admin_usage_provider_key_names(state, &items).await?;
return Ok(Some(build_admin_usage_active_requests_response(
&items,
&api_key_names,
state.has_auth_api_key_data_reader(),
&provider_key_names,
)));
}
@@ -134,10 +137,30 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response(
Err(detail) => return Ok(Some(admin_usage_bad_request_response(detail))),
};
let user_ids: Vec<String> = usage
.iter()
.filter_map(|item| item.user_id.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let users_by_id: BTreeMap<String, aether_data::repository::users::StoredUserSummary> =
state.resolve_auth_user_summaries_by_ids(&user_ids).await?;
let api_key_names = admin_usage_api_key_names(state, &usage).await?;
usage.retain(|item| {
admin_usage_matches_search(item, search.as_deref())
&& admin_usage_matches_username(item, username_filter.as_deref())
&& admin_usage_matches_eq(item.model.as_str(), model_filter.as_deref())
admin_usage_matches_search(
item,
search.as_deref(),
&users_by_id,
&api_key_names,
state.has_auth_user_data_reader(),
state.has_auth_api_key_data_reader(),
) && admin_usage_matches_username(
item,
username_filter.as_deref(),
&users_by_id,
state.has_auth_user_data_reader(),
) && admin_usage_matches_eq(item.model.as_str(), model_filter.as_deref())
&& admin_usage_matches_eq(
item.provider_name.as_str(),
provider_filter.as_deref(),
@@ -153,24 +176,6 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response(
});
let total = usage.len();
let user_ids: Vec<String> = usage
.iter()
.filter_map(|item| item.user_id.clone())
.collect::<BTreeSet<_>>()
.into_iter()
.collect();
let users_by_id: BTreeMap<String, aether_data::repository::users::StoredUserSummary> =
if state.has_user_data_reader() && !user_ids.is_empty() {
state
.list_users_by_ids(&user_ids)
.await?
.into_iter()
.map(|user| (user.id.clone(), user))
.collect()
} else {
BTreeMap::new()
};
let provider_key_names = admin_usage_provider_key_names(state, &usage).await?;
let records = usage
@@ -182,6 +187,9 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response(
return Ok(Some(build_admin_usage_records_response(
&records,
&users_by_id,
&api_key_names,
state.has_auth_user_data_reader(),
state.has_auth_api_key_data_reader(),
&provider_key_names,
total,
limit,