mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(admin): add Done-hub provider ops template
- 新增 Done-hub Cookie 认证架构 - 使用 /api/user/profile 查询余额,按 quota / 500000 换算 - 补充余额解析、认证头和校验相关测试
This commit is contained in:
@@ -14,7 +14,7 @@ pub fn parse_query_balance_payload(
|
||||
response_json: &Value,
|
||||
) -> Result<Value, String> {
|
||||
match architecture_id {
|
||||
"generic_api" | "new_api" | "anyrouter" => {
|
||||
"generic_api" | "new_api" | "anyrouter" | "done_hub" => {
|
||||
parse_new_api_balance_payload(action_config, response_json)
|
||||
}
|
||||
"cubence" => parse_cubence_balance_payload(action_config, response_json),
|
||||
@@ -490,6 +490,28 @@ mod tests {
|
||||
assert_eq!(payload["total_used"], json!(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn done_hub_single_request_parser_reads_wrapped_quota() {
|
||||
let payload = parse_query_balance_payload(
|
||||
"done_hub",
|
||||
&json!({ "quota_divisor": 500000 })
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("config"),
|
||||
&json!({
|
||||
"success": true,
|
||||
"data": {
|
||||
"quota": 2_276_139_911_u64,
|
||||
"used_quota": 13860089
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect("payload should parse");
|
||||
|
||||
assert_eq!(payload["total_available"], json!(4552.279822));
|
||||
assert_eq!(payload["total_used"], json!(27.720178));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub2api_parser_sums_balance_and_points() {
|
||||
let payload = parse_sub2api_balance_payload(
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
use super::{
|
||||
json_object, ProviderOpsActionSpec, ProviderOpsArchitectureSpec, ProviderOpsAuthSpec,
|
||||
ProviderOpsBalanceMode, ProviderOpsCheckinMode, ProviderOpsVerifyMode,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
pub(super) fn spec() -> ProviderOpsArchitectureSpec {
|
||||
let credentials_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"type": "string",
|
||||
"title": "站点地址",
|
||||
"description": "API 基础地址"
|
||||
},
|
||||
"session_cookie": {
|
||||
"type": "string",
|
||||
"title": "Session Cookie",
|
||||
"description": "从浏览器复制的 session Cookie 值",
|
||||
"x-sensitive": true,
|
||||
"x-input-type": "password"
|
||||
}
|
||||
},
|
||||
"required": ["session_cookie"],
|
||||
"x-auth-type": "cookie",
|
||||
"x-currency": "USD",
|
||||
"x-field-groups": [
|
||||
{ "fields": ["base_url"] },
|
||||
{ "fields": ["session_cookie"] }
|
||||
],
|
||||
"x-quota-divisor": 500000,
|
||||
"x-validation": [
|
||||
{
|
||||
"type": "required",
|
||||
"fields": ["session_cookie"],
|
||||
"message": "请填写 Session Cookie"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
ProviderOpsArchitectureSpec {
|
||||
architecture_id: "done_hub",
|
||||
display_name: "Done-hub",
|
||||
description: "Done-hub 中转站预设配置,使用 Cookie 认证",
|
||||
hidden: false,
|
||||
credentials_schema: credentials_schema.clone(),
|
||||
verify_endpoint: "/api/user/profile",
|
||||
verify_mode: ProviderOpsVerifyMode::DirectGet,
|
||||
balance_mode: ProviderOpsBalanceMode::SingleRequest,
|
||||
checkin_mode: ProviderOpsCheckinMode::None,
|
||||
query_balance_cookie_auth_errors: true,
|
||||
supported_auth_types: vec![ProviderOpsAuthSpec {
|
||||
auth_type: "cookie",
|
||||
display_name: "Done-hub Cookie",
|
||||
credentials_schema,
|
||||
}],
|
||||
supported_actions: vec![ProviderOpsActionSpec {
|
||||
action_type: "query_balance",
|
||||
display_name: "查询余额",
|
||||
description: "查询 Done-hub 账户余额信息",
|
||||
config_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "货币单位",
|
||||
"default": "USD"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}),
|
||||
}],
|
||||
default_connector: Some("cookie"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn default_action_config(action_type: &str) -> Option<Map<String, Value>> {
|
||||
match action_type {
|
||||
"query_balance" => Some(json_object(json!({
|
||||
"endpoint": "/api/user/profile",
|
||||
"method": "GET",
|
||||
"quota_divisor": 500000,
|
||||
"currency": "USD"
|
||||
}))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod anyrouter;
|
||||
mod cubence;
|
||||
mod done_hub;
|
||||
mod generic_api;
|
||||
mod nekocode;
|
||||
mod new_api;
|
||||
@@ -92,6 +93,7 @@ static PROVIDER_OPS_ARCHITECTURES: LazyLock<Vec<ProviderOpsArchitectureSpec>> =
|
||||
vec![
|
||||
anyrouter::spec(),
|
||||
cubence::spec(),
|
||||
done_hub::spec(),
|
||||
generic_api::spec(),
|
||||
nekocode::spec(),
|
||||
new_api::spec(),
|
||||
@@ -122,6 +124,7 @@ pub fn normalize_architecture_id(architecture_id: &str) -> &'static str {
|
||||
"generic_api" => "generic_api",
|
||||
"new_api" => "new_api",
|
||||
"cubence" => "cubence",
|
||||
"done_hub" => "done_hub",
|
||||
"yescode" => "yescode",
|
||||
"nekocode" => "nekocode",
|
||||
"anyrouter" => "anyrouter",
|
||||
@@ -178,6 +181,7 @@ fn default_action_config(architecture_id: &str, action_type: &str) -> Option<Map
|
||||
match architecture_id {
|
||||
"anyrouter" => anyrouter::default_action_config(action_type),
|
||||
"cubence" => cubence::default_action_config(action_type),
|
||||
"done_hub" => done_hub::default_action_config(action_type),
|
||||
"generic_api" => generic_api::default_action_config(action_type),
|
||||
"nekocode" => nekocode::default_action_config(action_type),
|
||||
"new_api" => new_api::default_action_config(action_type),
|
||||
@@ -201,23 +205,47 @@ mod tests {
|
||||
#[test]
|
||||
fn list_architectures_hides_generic_api_by_default() {
|
||||
let visible = list_architectures(false);
|
||||
assert_eq!(visible.len(), 6);
|
||||
assert_eq!(visible.len(), 7);
|
||||
assert!(visible
|
||||
.iter()
|
||||
.all(|item| item.architecture_id != "generic_api"));
|
||||
|
||||
let all = list_architectures(true);
|
||||
assert_eq!(all.len(), 7);
|
||||
assert_eq!(all.len(), 8);
|
||||
assert!(all.iter().any(|item| item.architecture_id == "generic_api"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_architecture_id_falls_back_to_generic_api() {
|
||||
assert_eq!(normalize_architecture_id(""), "generic_api");
|
||||
assert_eq!(normalize_architecture_id("done_hub"), "done_hub");
|
||||
assert_eq!(normalize_architecture_id("new_api"), "new_api");
|
||||
assert_eq!(normalize_architecture_id("unknown"), "generic_api");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn done_hub_uses_profile_balance_without_checkin() {
|
||||
let architecture = get_architecture("done_hub").expect("architecture should exist");
|
||||
assert_eq!(architecture.architecture_id, "done_hub");
|
||||
assert_eq!(architecture.verify_endpoint, "/api/user/profile");
|
||||
|
||||
let resolved = resolve_action_config(
|
||||
"done_hub",
|
||||
&json!({})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("config should be object"),
|
||||
"query_balance",
|
||||
None,
|
||||
)
|
||||
.expect("action config should resolve");
|
||||
|
||||
assert_eq!(resolved.get("endpoint"), Some(&json!("/api/user/profile")));
|
||||
assert_eq!(resolved.get("quota_divisor"), Some(&json!(500000)));
|
||||
assert_eq!(resolved.get("method"), Some(&json!("GET")));
|
||||
assert!(resolved.get("checkin_endpoint").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_architecture_returns_generic_api_for_unknown_id() {
|
||||
let architecture = get_architecture("unknown").expect("architecture should exist");
|
||||
|
||||
@@ -29,6 +29,7 @@ pub fn parse_verify_payload(
|
||||
match normalize_architecture_id(architecture_id) {
|
||||
"anyrouter" => admin_provider_ops_anyrouter_verify_payload(status, response_json),
|
||||
"cubence" => admin_provider_ops_cubence_verify_payload(status, response_json),
|
||||
"done_hub" => admin_provider_ops_anyrouter_verify_payload(status, response_json),
|
||||
"yescode" => admin_provider_ops_yescode_verify_payload(status, response_json),
|
||||
"nekocode" => admin_provider_ops_nekocode_verify_payload(status, response_json),
|
||||
"sub2api" => {
|
||||
@@ -111,6 +112,12 @@ fn admin_provider_ops_cubence_cookie_header(cookie_input: &str) -> String {
|
||||
cookies.join("; ")
|
||||
}
|
||||
|
||||
fn admin_provider_ops_session_cookie_header(cookie_input: &str) -> String {
|
||||
let trimmed = admin_provider_ops_strip_cookie_header_prefix(cookie_input);
|
||||
let session = admin_provider_ops_extract_cookie_value(trimmed, "session");
|
||||
format!("session={session}")
|
||||
}
|
||||
|
||||
pub fn admin_provider_ops_yescode_cookie_header(cookie_input: &str) -> String {
|
||||
if cookie_input.contains("yescode_auth=") {
|
||||
let mut parts = Vec::new();
|
||||
@@ -433,14 +440,17 @@ pub fn admin_provider_ops_verify_headers(
|
||||
)?;
|
||||
}
|
||||
}
|
||||
"nekocode" => {
|
||||
"nekocode" | "done_hub" => {
|
||||
if let Some(session_cookie) = credentials
|
||||
.get("session_cookie")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
let session = admin_provider_ops_extract_cookie_value(session_cookie, "session");
|
||||
insert_header(&mut headers, "Cookie", &format!("session={session}"))?;
|
||||
insert_header(
|
||||
&mut headers,
|
||||
"Cookie",
|
||||
&admin_provider_ops_session_cookie_header(session_cookie),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
"anyrouter" => {
|
||||
@@ -817,7 +827,7 @@ mod tests {
|
||||
admin_provider_ops_anyrouter_parse_session_user_id,
|
||||
admin_provider_ops_anyrouter_verify_payload, admin_provider_ops_cubence_verify_payload,
|
||||
admin_provider_ops_frontend_updated_credentials, admin_provider_ops_sub2api_verify_payload,
|
||||
admin_provider_ops_verify_headers, ADMIN_PROVIDER_OPS_USER_AGENT,
|
||||
admin_provider_ops_verify_headers, parse_verify_payload, ADMIN_PROVIDER_OPS_USER_AGENT,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use reqwest::header::COOKIE;
|
||||
@@ -929,6 +939,50 @@ mod tests {
|
||||
assert_eq!(auth_failed["message"], json!("Cookie 已失效,请重新配置"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn done_hub_verify_payload_reads_wrapped_profile() {
|
||||
let payload = parse_verify_payload(
|
||||
"done_hub",
|
||||
StatusCode::OK,
|
||||
&json!({
|
||||
"success": true,
|
||||
"data": {
|
||||
"username": "linuxdo_850",
|
||||
"display_name": "AAEE86",
|
||||
"quota": 2_276_139_911_u64,
|
||||
"used_quota": 13860089,
|
||||
"request_count": 55
|
||||
}
|
||||
}),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(payload["success"], json!(true));
|
||||
assert_eq!(payload["data"]["username"], json!("linuxdo_850"));
|
||||
assert_eq!(payload["data"]["display_name"], json!("AAEE86"));
|
||||
assert_eq!(payload["data"]["quota"], json!(2276139911.0));
|
||||
assert_eq!(payload["data"]["used_quota"], json!(13860089.0));
|
||||
assert_eq!(payload["data"]["request_count"], json!(55));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn done_hub_verify_headers_use_session_cookie_only() {
|
||||
let headers = admin_provider_ops_verify_headers(
|
||||
"done_hub",
|
||||
&Map::new(),
|
||||
&Map::from_iter([(
|
||||
"session_cookie".to_string(),
|
||||
json!("Cookie: session=abc; other=ignored"),
|
||||
)]),
|
||||
)
|
||||
.expect("headers should build");
|
||||
|
||||
assert_eq!(
|
||||
headers.get(COOKIE).and_then(|value| value.to_str().ok()),
|
||||
Some("session=abc")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anyrouter_verify_headers_include_shared_user_agent() {
|
||||
let headers = admin_provider_ops_verify_headers(
|
||||
|
||||
Reference in New Issue
Block a user