refactor(core): 重构 Provider Ops 架构注册、校验链路与余额缓存流程

- 将 provider ops 纯逻辑下沉到 aether-admin,拆分 architectures、actions、verify 模块
- 用统一的 architecture spec 驱动 verify、query_balance、checkin 行为,替换分散的条件分支
- 新增 sub2api / anyrouter / cubence / yescode 等架构的请求头构建、校验解析与余额解析实现
- 引入 provider balance Redis 缓存、异步刷新、pending 响应以及配置变更后的缓存清理
- 补充 provider ops 的控制面测试、Redis 缓存测试和架构边界测试

- 影响说明:统一了 Provider Ops 的扩展方式与运行时行为,降低后续新增架构的接入成本
- 影响说明:余额查询从“实时阻塞返回”扩展为“缓存命中即返回并后台刷新”的模式,前端需要兼容 pending 状态
This commit is contained in:
AAEE86
2026-04-10 15:06:42 +08:00
parent 87b433e290
commit b8ce02b4f7
48 changed files with 7680 additions and 2450 deletions

View File

@@ -8,6 +8,7 @@ use aether_data_contracts::repository::candidates::{
UpsertRequestCandidateRecord,
};
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogProvider;
use axum::body::Body;
use axum::http::StatusCode;
use axum::routing::{get, post};
use axum::{Json, Router};
@@ -253,6 +254,158 @@ async fn gateway_provider_checkin_runs_local_query_balance_for_configured_provid
ops_handle.abort();
}
#[tokio::test]
async fn gateway_provider_checkin_counts_anyrouter_auto_signin_as_success() {
fn sample_provider(provider_id: &str, ops_url: &str) -> StoredProviderCatalogProvider {
StoredProviderCatalogProvider::new(
provider_id.to_string(),
"openai".to_string(),
Some("https://example.com".to_string()),
"custom".to_string(),
)
.expect("provider should build")
.with_routing_fields(10)
.with_transport_fields(
true,
false,
true,
None,
None,
None,
None,
None,
Some(json!({
"provider_ops": {
"architecture_id": "anyrouter",
"base_url": ops_url,
"connector": {
"auth_type": "cookie",
"config": {},
"credentials": {
"session_cookie": encrypt_python_fernet_plaintext(
DEVELOPMENT_ENCRYPTION_KEY,
"session=MTIzfGVIaDRlQUpwWkFOcGJuU3F1d0RfVkhsNWVYa0lkWE5sY201aGJXVUdjM1J5YVc1bkRCQUFCV0ZzYVdObHxzaWc",
).expect("session cookie should encrypt"),
}
}
}
})),
)
}
let sign_in_hits = Arc::new(Mutex::new(0usize));
let sign_in_hits_clone = Arc::clone(&sign_in_hits);
let balance_hits = Arc::new(Mutex::new(0usize));
let balance_hits_clone = Arc::clone(&balance_hits);
let ops = Router::new()
.route(
"/",
get(|| async move {
(
StatusCode::OK,
Body::from(
"<html><script>var arg1 = '0123456789abcdef0123456789abcdef01234567';</script></html>",
),
)
}),
)
.route(
"/api/user/sign_in",
post(move |headers: axum::http::HeaderMap| {
let sign_in_hits_inner = Arc::clone(&sign_in_hits_clone);
async move {
*sign_in_hits_inner.lock().expect("mutex should lock") += 1;
let cookie = headers
.get(axum::http::header::COOKIE)
.and_then(|value| value.to_str().ok())
.expect("cookie header should exist");
assert!(cookie.contains("acw_sc__v2="));
assert!(cookie.contains(
"session=MTIzfGVIaDRlQUpwWkFOcGJuU3F1d0RfVkhsNWVYa0lkWE5sY201aGJXVUdjM1J5YVc1bkRCQUFCV0ZzYVdObHxzaWc"
));
assert_eq!(
headers
.get("New-Api-User")
.and_then(|value| value.to_str().ok()),
Some("42")
);
(
StatusCode::OK,
Json(json!({
"success": true,
"message": "Anyrouter 签到成功",
})),
)
}
}),
)
.route(
"/api/user/self",
get(move |headers: axum::http::HeaderMap| {
let balance_hits_inner = Arc::clone(&balance_hits_clone);
async move {
*balance_hits_inner.lock().expect("mutex should lock") += 1;
let cookie = headers
.get(axum::http::header::COOKIE)
.and_then(|value| value.to_str().ok())
.expect("cookie header should exist");
assert!(cookie.contains("acw_sc__v2="));
assert_eq!(
headers
.get("New-Api-User")
.and_then(|value| value.to_str().ok()),
Some("42")
);
(
StatusCode::OK,
Json(json!({
"success": true,
"data": {
"quota": 2500000,
"used_quota": 500000
}
})),
)
}
}),
);
let (ops_url, ops_handle) = start_server(ops).await;
let repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
vec![sample_provider("provider-anyrouter", &ops_url)],
vec![],
vec![],
));
let gateway_state = AppState::new()
.expect("gateway state should build")
.with_data_state_for_tests(
crate::data::GatewayDataState::with_provider_catalog_repository_for_tests(repository)
.with_system_config_values_for_tests([
("enable_provider_checkin".to_string(), json!(true)),
("provider_checkin_time".to_string(), json!("01:05")),
])
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY),
);
let summary = crate::maintenance::perform_provider_checkin_once(&gateway_state)
.await
.expect("provider checkin should succeed");
assert_eq!(
summary,
ProviderCheckinRunSummary {
attempted: 1,
succeeded: 1,
failed: 0,
skipped: 0,
}
);
assert_eq!(*sign_in_hits.lock().expect("mutex should lock"), 1);
assert_eq!(*balance_hits.lock().expect("mutex should lock"), 1);
ops_handle.abort();
}
#[tokio::test]
async fn gateway_provider_checkin_skips_when_disabled_via_system_config() {
let repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(