mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user