refactor: 大规模模块拆分与重组,新增 aether-admin crate

- 新建独立 aether-admin crate 承载 admin 相关共享契约与纯辅助函数
- 拆分 ai_pipeline 下 kiro/private_envelope/conversion/planner 等大文件为子模块目录
- 重组 admin handlers 各业务域(billing/oauth/provider/system/users 等)为目录结构,移除 shared.rs/builders.rs 等反模式
- 移除 ai_pipeline runtime adapters 旧实现(claude/openai/gemini/kiro/vertex/antigravity 等),改由 provider transport 统一承载
- 移除 control_facade/execution_facade/auth_snapshot_facade 等冗余 facade 层
- 拆分 query/billing 与 query/monitoring 模块、state/runtime/payments 与 security 模块
- 扩展架构测试覆盖 admin_billing/admin_model/admin_users 等新模块
- 删除 docs/architecture/refactor-execution-plan.md 已完成的执行计划文档
This commit is contained in:
fawney19
2026-04-09 00:10:38 +08:00
parent 4fb9882b54
commit 4fc95adfb9
663 changed files with 48471 additions and 40232 deletions
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::provider::shared::paths::{
admin_provider_ops_architecture_id_from_path, is_admin_provider_ops_architectures_root,
};
use crate::handlers::admin::request::AdminRequestContext;
use crate::GatewayError;
use axum::{
body::Body,
@@ -35,16 +35,16 @@ fn admin_provider_ops_architecture_payload(architecture_id: &str) -> Option<Valu
}
pub(super) async fn maybe_build_local_admin_provider_ops_architectures_response(
request_context: &GatewayPublicRequestContext,
request_context: &AdminRequestContext<'_>,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(decision) = request_context.control_decision.as_ref() else {
let Some(decision) = request_context.decision() else {
return Ok(None);
};
if decision.route_family.as_deref() == Some("provider_ops_manage")
&& decision.route_kind.as_deref() == Some("list_architectures")
&& request_context.request_method == http::Method::GET
&& is_admin_provider_ops_architectures_root(&request_context.request_path)
&& request_context.method() == http::Method::GET
&& is_admin_provider_ops_architectures_root(request_context.path())
{
return Ok(Some(
Json(admin_provider_ops_architectures_list_payload()).into_response(),
@@ -53,10 +53,10 @@ pub(super) async fn maybe_build_local_admin_provider_ops_architectures_response(
if decision.route_family.as_deref() == Some("provider_ops_manage")
&& decision.route_kind.as_deref() == Some("get_architecture")
&& request_context.request_method == http::Method::GET
&& request_context.method() == http::Method::GET
{
let Some(architecture_id) =
admin_provider_ops_architecture_id_from_path(&request_context.request_path)
admin_provider_ops_architecture_id_from_path(request_context.path())
else {
return Ok(Some(
(
@@ -1,5 +1,5 @@
use crate::control::GatewayPublicRequestContext;
use crate::{AppState, GatewayError};
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::GatewayError;
use axum::body::{Body, Bytes};
use axum::http::Response;
@@ -7,8 +7,8 @@ mod architectures;
pub(crate) mod providers;
pub(crate) async fn maybe_build_local_admin_provider_ops_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
if let Some(response) =
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
mod probe;
mod run;
mod shared;
pub(super) use probe::admin_provider_ops_probe_new_api_checkin;
pub(super) use run::admin_provider_ops_run_checkin_action;
@@ -0,0 +1,110 @@
use super::super::super::support::AdminProviderOpsCheckinOutcome;
use super::super::support::{admin_provider_ops_json_object_map, admin_provider_ops_request_url};
use super::shared::{
admin_provider_ops_checkin_already_done, admin_provider_ops_checkin_auth_failure,
};
use crate::handlers::admin::request::AdminAppState;
use serde_json::json;
pub(in super::super) async fn admin_provider_ops_probe_new_api_checkin(
state: &AdminAppState<'_>,
base_url: &str,
action_config: &serde_json::Map<String, serde_json::Value>,
headers: &reqwest::header::HeaderMap,
has_cookie: bool,
) -> Option<AdminProviderOpsCheckinOutcome> {
let endpoint = action_config
.get("checkin_endpoint")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("/api/user/checkin");
let url = admin_provider_ops_request_url(
base_url,
&admin_provider_ops_json_object_map(json!({ "endpoint": endpoint })),
endpoint,
);
let response = match state
.http_client()
.request(reqwest::Method::POST, url)
.headers(headers.clone())
.send()
.await
{
Ok(response) => response,
Err(_) => return None,
};
if response.status() == http::StatusCode::NOT_FOUND {
return None;
}
if matches!(
response.status(),
http::StatusCode::UNAUTHORIZED | http::StatusCode::FORBIDDEN
) {
return has_cookie.then(|| AdminProviderOpsCheckinOutcome {
success: None,
message: "Cookie 已失效".to_string(),
cookie_expired: true,
});
}
let response_json = match response.bytes().await {
Ok(bytes) => {
serde_json::from_slice::<serde_json::Value>(&bytes).unwrap_or_else(|_| json!({}))
}
Err(_) => json!({}),
};
let message = response_json
.get("message")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.trim()
.to_string();
if response_json
.get("success")
.and_then(serde_json::Value::as_bool)
== Some(true)
{
return Some(AdminProviderOpsCheckinOutcome {
success: Some(true),
message: if message.is_empty() {
"签到成功".to_string()
} else {
message
},
cookie_expired: false,
});
}
if admin_provider_ops_checkin_already_done(&message) {
return Some(AdminProviderOpsCheckinOutcome {
success: None,
message: if message.is_empty() {
"今日已签到".to_string()
} else {
message
},
cookie_expired: false,
});
}
if admin_provider_ops_checkin_auth_failure(&message) {
return has_cookie.then(|| AdminProviderOpsCheckinOutcome {
success: None,
message: if message.is_empty() {
"Cookie 已失效".to_string()
} else {
message
},
cookie_expired: true,
});
}
Some(AdminProviderOpsCheckinOutcome {
success: Some(false),
message: if message.is_empty() {
"签到失败".to_string()
} else {
message
},
cookie_expired: false,
})
}
@@ -0,0 +1,196 @@
use super::super::super::support::ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE;
use super::super::responses::{
admin_provider_ops_action_error, admin_provider_ops_action_not_supported,
admin_provider_ops_action_response,
};
use super::super::support::{admin_provider_ops_request_method, admin_provider_ops_request_url};
use super::shared::{
admin_provider_ops_checkin_already_done, admin_provider_ops_checkin_auth_failure,
admin_provider_ops_checkin_payload,
};
use crate::handlers::admin::request::AdminAppState;
pub(in super::super) async fn admin_provider_ops_run_checkin_action(
state: &AdminAppState<'_>,
base_url: &str,
architecture_id: &str,
action_config: &serde_json::Map<String, serde_json::Value>,
headers: &reqwest::header::HeaderMap,
has_cookie: bool,
) -> serde_json::Value {
let start = std::time::Instant::now();
if !matches!(architecture_id, "generic_api" | "new_api") {
return admin_provider_ops_action_not_supported(
"checkin",
ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE,
);
}
let url = admin_provider_ops_request_url(base_url, action_config, "/api/user/checkin");
let method = admin_provider_ops_request_method(action_config, "POST");
let response = match state
.http_client()
.request(method, url)
.headers(headers.clone())
.send()
.await
{
Ok(response) => response,
Err(err) if err.is_timeout() => {
return admin_provider_ops_action_error("network_error", "checkin", "请求超时", None);
}
Err(err) => {
return admin_provider_ops_action_error(
"network_error",
"checkin",
format!("网络错误: {err}"),
None,
);
}
};
let response_time_ms = Some(start.elapsed().as_millis() as u64);
let status = response.status();
let response_json = match response.bytes().await {
Ok(bytes) => match serde_json::from_slice::<serde_json::Value>(&bytes) {
Ok(value) => value,
Err(_) => {
return admin_provider_ops_action_error(
"parse_error",
"checkin",
"响应不是有效的 JSON",
response_time_ms,
);
}
},
Err(err) => {
return admin_provider_ops_action_error(
"network_error",
"checkin",
format!("网络错误: {err}"),
response_time_ms,
);
}
};
if status == http::StatusCode::NOT_FOUND {
return admin_provider_ops_action_error(
"not_supported",
"checkin",
"功能未开放",
response_time_ms,
);
}
if status == http::StatusCode::TOO_MANY_REQUESTS {
return admin_provider_ops_action_error(
"rate_limited",
"checkin",
"请求频率限制",
response_time_ms,
);
}
if status == http::StatusCode::UNAUTHORIZED {
return admin_provider_ops_action_error(
if has_cookie {
"auth_expired"
} else {
"auth_failed"
},
"checkin",
if has_cookie {
"Cookie 已失效,请重新配置"
} else {
"认证失败"
},
response_time_ms,
);
}
if status == http::StatusCode::FORBIDDEN {
return admin_provider_ops_action_error(
if has_cookie {
"auth_expired"
} else {
"auth_failed"
},
"checkin",
if has_cookie {
"Cookie 已失效或无权限"
} else {
"无权限访问"
},
response_time_ms,
);
}
if status != http::StatusCode::OK {
return admin_provider_ops_action_error(
"unknown_error",
"checkin",
format!(
"HTTP {}: {}",
status.as_u16(),
status.canonical_reason().unwrap_or("Unknown")
),
response_time_ms,
);
}
let message = response_json
.get("message")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.trim()
.to_string();
if response_json
.get("success")
.and_then(serde_json::Value::as_bool)
== Some(true)
{
return admin_provider_ops_action_response(
"success",
"checkin",
admin_provider_ops_checkin_payload(&response_json, Some(message)),
None,
response_time_ms,
3600,
);
}
if admin_provider_ops_checkin_already_done(&message) {
return admin_provider_ops_action_response(
"already_done",
"checkin",
admin_provider_ops_checkin_payload(&response_json, Some(message)),
None,
response_time_ms,
3600,
);
}
if admin_provider_ops_checkin_auth_failure(&message) {
return admin_provider_ops_action_error(
if has_cookie {
"auth_expired"
} else {
"auth_failed"
},
"checkin",
if message.is_empty() {
if has_cookie {
"Cookie 已失效"
} else {
"认证失败"
}
} else {
message.as_str()
},
response_time_ms,
);
}
admin_provider_ops_action_error(
"unknown_error",
"checkin",
if message.is_empty() {
"签到失败"
} else {
message.as_str()
},
response_time_ms,
)
}
@@ -0,0 +1,83 @@
use super::super::super::verify::admin_provider_ops_value_as_f64;
use super::super::support::admin_provider_ops_checkin_data;
fn admin_provider_ops_message_contains_any(message: &str, indicators: &[&str]) -> bool {
let normalized = message.trim().to_ascii_lowercase();
indicators
.iter()
.any(|indicator| normalized.contains(&indicator.to_ascii_lowercase()))
}
pub(super) fn admin_provider_ops_checkin_already_done(message: &str) -> bool {
admin_provider_ops_message_contains_any(
message,
&["already", "已签到", "已经签到", "今日已签", "重复签到"],
)
}
pub(super) fn admin_provider_ops_checkin_auth_failure(message: &str) -> bool {
admin_provider_ops_message_contains_any(
message,
&[
"未登录",
"请登录",
"login",
"unauthorized",
"无权限",
"权限不足",
"turnstile",
"captcha",
"验证码",
],
)
}
pub(super) fn admin_provider_ops_checkin_payload(
response_json: &serde_json::Value,
fallback_message: Option<String>,
) -> serde_json::Value {
let details = response_json
.get("data")
.and_then(serde_json::Value::as_object)
.or_else(|| response_json.as_object());
let reward = details.and_then(|value| {
admin_provider_ops_value_as_f64(
value
.get("reward")
.or_else(|| value.get("quota"))
.or_else(|| value.get("amount")),
)
});
let streak_days = details
.and_then(|value| value.get("streak_days").or_else(|| value.get("streak")))
.and_then(serde_json::Value::as_i64);
let next_reward = details.and_then(|value| {
admin_provider_ops_value_as_f64(value.get("next_reward").or_else(|| value.get("next")))
});
let message = fallback_message.or_else(|| {
response_json
.get("message")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned)
});
let mut extra = serde_json::Map::new();
if let Some(details) = details {
for (key, value) in details {
if matches!(
key.as_str(),
"reward"
| "quota"
| "amount"
| "streak_days"
| "streak"
| "next_reward"
| "next"
| "message"
) {
continue;
}
extra.insert(key.clone(), value.clone());
}
}
admin_provider_ops_checkin_data(reward, streak_days, next_reward, message, extra)
}
@@ -0,0 +1,130 @@
mod checkin;
mod query_balance;
mod responses;
mod support;
use super::config::{
admin_provider_ops_config_object, admin_provider_ops_connector_object,
admin_provider_ops_decrypted_credentials, resolve_admin_provider_ops_base_url,
};
use super::support::ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE;
use super::verify::admin_provider_ops_verify_headers;
use crate::handlers::admin::request::AdminAppState;
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
};
pub(super) fn admin_provider_ops_is_valid_action_type(action_type: &str) -> bool {
matches!(
action_type,
"query_balance"
| "checkin"
| "claim_quota"
| "refresh_token"
| "get_usage"
| "get_models"
| "custom"
)
}
pub(crate) async fn admin_provider_ops_local_action_response(
state: &AdminAppState<'_>,
_provider_id: &str,
provider: Option<&StoredProviderCatalogProvider>,
endpoints: &[StoredProviderCatalogEndpoint],
action_type: &str,
request_config: Option<&serde_json::Map<String, serde_json::Value>>,
) -> serde_json::Value {
let Some(provider) = provider else {
return responses::admin_provider_ops_action_not_configured(action_type, "未配置操作设置");
};
let Some(provider_ops_config) = admin_provider_ops_config_object(provider) else {
return responses::admin_provider_ops_action_not_configured(action_type, "未配置操作设置");
};
let architecture_id = provider_ops_config
.get("architecture_id")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("generic_api");
let connector_config = admin_provider_ops_connector_object(provider_ops_config)
.and_then(|connector| connector.get("config"))
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
if support::admin_provider_ops_should_use_rust_only_action_stub(
architecture_id,
&connector_config,
) {
return responses::admin_provider_ops_action_not_supported(
action_type,
ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE,
);
}
let Some(base_url) =
resolve_admin_provider_ops_base_url(provider, endpoints, Some(provider_ops_config))
else {
return responses::admin_provider_ops_action_not_configured(
action_type,
"Provider 未配置 base_url",
);
};
let credentials = admin_provider_ops_decrypted_credentials(
state,
admin_provider_ops_config_object(provider)
.and_then(admin_provider_ops_connector_object)
.and_then(|connector| connector.get("credentials")),
);
let headers =
match admin_provider_ops_verify_headers(architecture_id, &connector_config, &credentials) {
Ok(headers) => headers,
Err(message) => {
return responses::admin_provider_ops_action_not_configured(action_type, message);
}
};
let Some(action_config) = support::admin_provider_ops_resolved_action_config(
architecture_id,
provider_ops_config,
action_type,
request_config,
) else {
return responses::admin_provider_ops_action_not_supported(
action_type,
ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE,
);
};
match action_type {
"query_balance" => {
query_balance::admin_provider_ops_run_query_balance_action(
state,
&base_url,
architecture_id,
&action_config,
&headers,
&credentials,
)
.await
}
"checkin" => {
let has_cookie = credentials
.get("cookie")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| !value.trim().is_empty());
checkin::admin_provider_ops_run_checkin_action(
state,
&base_url,
architecture_id,
&action_config,
&headers,
has_cookie,
)
.await
}
_ => responses::admin_provider_ops_action_not_supported(
action_type,
ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE,
),
}
}
@@ -0,0 +1,212 @@
mod parsers;
mod yescode;
use super::super::support::{
AdminProviderOpsCheckinOutcome, ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE,
};
use super::checkin::admin_provider_ops_probe_new_api_checkin;
use super::responses::{
admin_provider_ops_action_error, admin_provider_ops_action_not_supported,
admin_provider_ops_action_response,
};
use super::support::{
admin_provider_ops_is_cookie_auth_architecture, admin_provider_ops_request_method,
admin_provider_ops_request_url,
};
use crate::handlers::admin::request::AdminAppState;
pub(super) async fn admin_provider_ops_run_query_balance_action(
state: &AdminAppState<'_>,
base_url: &str,
architecture_id: &str,
action_config: &serde_json::Map<String, serde_json::Value>,
headers: &reqwest::header::HeaderMap,
credentials: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Value {
if architecture_id == "yescode" {
return yescode::admin_provider_ops_yescode_balance_payload(
state,
base_url,
headers,
action_config,
)
.await;
}
let mut balance_checkin = None::<AdminProviderOpsCheckinOutcome>;
if matches!(architecture_id, "generic_api" | "new_api") {
let has_cookie = credentials
.get("cookie")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| !value.trim().is_empty());
balance_checkin = admin_provider_ops_probe_new_api_checkin(
state,
base_url,
action_config,
headers,
has_cookie,
)
.await;
}
let start = std::time::Instant::now();
let url = admin_provider_ops_request_url(base_url, action_config, "/api/user/balance");
let method = admin_provider_ops_request_method(action_config, "GET");
let response = match state
.http_client()
.request(method, url)
.headers(headers.clone())
.send()
.await
{
Ok(response) => response,
Err(err) if err.is_timeout() => {
return admin_provider_ops_action_error(
"network_error",
"query_balance",
"请求超时",
None,
);
}
Err(err) => {
return admin_provider_ops_action_error(
"network_error",
"query_balance",
format!("网络错误: {err}"),
None,
);
}
};
let response_time_ms = Some(start.elapsed().as_millis() as u64);
let status = response.status();
let response_json = match response.bytes().await {
Ok(bytes) => match serde_json::from_slice::<serde_json::Value>(&bytes) {
Ok(value) => value,
Err(_) => {
return admin_provider_ops_action_error(
"parse_error",
"query_balance",
"响应不是有效的 JSON",
response_time_ms,
);
}
},
Err(err) => {
return admin_provider_ops_action_error(
"network_error",
"query_balance",
format!("网络错误: {err}"),
response_time_ms,
);
}
};
if status != http::StatusCode::OK {
let cookie_auth = admin_provider_ops_is_cookie_auth_architecture(architecture_id);
return match status {
http::StatusCode::UNAUTHORIZED => admin_provider_ops_action_error(
"auth_failed",
"query_balance",
if cookie_auth {
"Cookie 已失效,请重新配置"
} else {
"认证失败"
},
response_time_ms,
),
http::StatusCode::FORBIDDEN => admin_provider_ops_action_error(
"auth_failed",
"query_balance",
if cookie_auth {
"Cookie 已失效或无权限"
} else {
"无权限访问"
},
response_time_ms,
),
http::StatusCode::NOT_FOUND => admin_provider_ops_action_error(
"not_supported",
"query_balance",
"功能未开放",
response_time_ms,
),
http::StatusCode::TOO_MANY_REQUESTS => admin_provider_ops_action_error(
"rate_limited",
"query_balance",
"请求频率限制",
response_time_ms,
),
_ => admin_provider_ops_action_error(
"unknown_error",
"query_balance",
format!(
"HTTP {}: {}",
status.as_u16(),
status.canonical_reason().unwrap_or("Unknown")
),
response_time_ms,
),
};
}
let data = match architecture_id {
"generic_api" | "new_api" => {
match parsers::admin_provider_ops_new_api_balance_payload(action_config, &response_json)
{
Ok(data) => data,
Err(message) => {
return admin_provider_ops_action_error(
"unknown_error",
"query_balance",
message,
response_time_ms,
);
}
}
}
"cubence" => {
match parsers::admin_provider_ops_cubence_balance_payload(action_config, &response_json)
{
Ok(data) => data,
Err(message) => {
return admin_provider_ops_action_error(
"parse_error",
"query_balance",
message,
response_time_ms,
);
}
}
}
"nekocode" => match parsers::admin_provider_ops_nekocode_balance_payload(&response_json) {
Ok(data) => data,
Err(message) => {
return admin_provider_ops_action_error(
"parse_error",
"query_balance",
message,
response_time_ms,
);
}
},
_ => {
return admin_provider_ops_action_not_supported(
"query_balance",
ADMIN_PROVIDER_OPS_ACTION_RUST_ONLY_MESSAGE,
);
}
};
let mut payload = admin_provider_ops_action_response(
"success",
"query_balance",
data,
None,
response_time_ms,
86400,
);
if let Some(outcome) = balance_checkin.as_ref() {
parsers::admin_provider_ops_attach_balance_checkin_outcome(&mut payload, outcome);
}
payload
}
@@ -0,0 +1,312 @@
use super::super::super::support::AdminProviderOpsCheckinOutcome;
use super::super::super::verify::admin_provider_ops_value_as_f64;
use super::super::support::{
admin_provider_ops_balance_data, admin_provider_ops_parse_rfc3339_unix_secs,
admin_provider_ops_quota_divisor,
};
use serde_json::json;
pub(super) fn admin_provider_ops_new_api_balance_payload(
action_config: &serde_json::Map<String, serde_json::Value>,
response_json: &serde_json::Value,
) -> Result<serde_json::Value, String> {
let user_data = if response_json
.get("success")
.and_then(serde_json::Value::as_bool)
== Some(true)
&& response_json
.get("data")
.is_some_and(serde_json::Value::is_object)
{
response_json.get("data")
} else if response_json
.get("success")
.and_then(serde_json::Value::as_bool)
== Some(false)
{
return Err(response_json
.get("message")
.and_then(serde_json::Value::as_str)
.unwrap_or("业务状态码表示失败")
.to_string());
} else {
Some(response_json)
};
let Some(user_data) = user_data.and_then(serde_json::Value::as_object) else {
return Err("响应格式无效".to_string());
};
let quota_divisor = admin_provider_ops_quota_divisor(action_config);
let total_available =
admin_provider_ops_value_as_f64(user_data.get("quota")).map(|value| value / quota_divisor);
let total_used = admin_provider_ops_value_as_f64(user_data.get("used_quota"))
.map(|value| value / quota_divisor);
Ok(admin_provider_ops_balance_data(
None,
total_used,
total_available,
action_config
.get("currency")
.and_then(serde_json::Value::as_str)
.unwrap_or("USD"),
serde_json::Map::new(),
))
}
pub(super) fn admin_provider_ops_cubence_balance_payload(
action_config: &serde_json::Map<String, serde_json::Value>,
response_json: &serde_json::Value,
) -> Result<serde_json::Value, String> {
let response_data = response_json
.get("data")
.and_then(serde_json::Value::as_object)
.ok_or_else(|| "响应格式无效".to_string())?;
let balance_data = response_data
.get("balance")
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
let subscription_limits = response_data
.get("subscription_limits")
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
let mut extra = serde_json::Map::new();
if let Some(five_hour) = subscription_limits
.get("five_hour")
.and_then(serde_json::Value::as_object)
{
extra.insert(
"five_hour_limit".to_string(),
json!({
"limit": five_hour.get("limit"),
"used": five_hour.get("used"),
"remaining": five_hour.get("remaining"),
"resets_at": five_hour.get("resets_at"),
}),
);
}
if let Some(weekly) = subscription_limits
.get("weekly")
.and_then(serde_json::Value::as_object)
{
extra.insert(
"weekly_limit".to_string(),
json!({
"limit": weekly.get("limit"),
"used": weekly.get("used"),
"remaining": weekly.get("remaining"),
"resets_at": weekly.get("resets_at"),
}),
);
}
for key in [
"normal_balance_dollar",
"subscription_balance_dollar",
"charity_balance_dollar",
] {
if let Some(value) = balance_data.get(key) {
extra.insert(
key.trim_end_matches("_dollar").replace("_dollar", ""),
value.clone(),
);
}
}
if let Some(value) = balance_data.get("normal_balance_dollar") {
extra.insert("normal_balance".to_string(), value.clone());
}
if let Some(value) = balance_data.get("subscription_balance_dollar") {
extra.insert("subscription_balance".to_string(), value.clone());
}
if let Some(value) = balance_data.get("charity_balance_dollar") {
extra.insert("charity_balance".to_string(), value.clone());
}
Ok(admin_provider_ops_balance_data(
None,
None,
admin_provider_ops_value_as_f64(balance_data.get("total_balance_dollar")),
action_config
.get("currency")
.and_then(serde_json::Value::as_str)
.unwrap_or("USD"),
extra,
))
}
pub(super) fn admin_provider_ops_nekocode_balance_payload(
response_json: &serde_json::Value,
) -> Result<serde_json::Value, String> {
let response_data = response_json
.get("data")
.and_then(serde_json::Value::as_object)
.ok_or_else(|| "响应格式无效".to_string())?;
let subscription = response_data
.get("subscription")
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
let balance = admin_provider_ops_value_as_f64(response_data.get("balance"));
let daily_quota_limit = admin_provider_ops_value_as_f64(subscription.get("daily_quota_limit"));
let daily_remaining_quota =
admin_provider_ops_value_as_f64(subscription.get("daily_remaining_quota"));
let daily_used = match (daily_quota_limit, daily_remaining_quota) {
(Some(limit), Some(remaining)) => Some(limit - remaining),
_ => None,
};
let mut extra = serde_json::Map::new();
for key in [
"plan_name",
"status",
"daily_quota_limit",
"daily_remaining_quota",
"effective_start_date",
"effective_end_date",
] {
if let Some(value) = subscription.get(key) {
extra.insert(
match key {
"status" => "subscription_status",
other => other,
}
.to_string(),
value.clone(),
);
}
}
if let Some(value) = daily_used {
extra.insert("daily_used_quota".to_string(), json!(value));
}
if let Some(month_data) = response_data
.get("month")
.and_then(serde_json::Value::as_object)
{
extra.insert(
"month_stats".to_string(),
json!({
"total_input_tokens": month_data.get("total_input_tokens"),
"total_output_tokens": month_data.get("total_output_tokens"),
"total_quota": month_data.get("total_quota"),
"total_requests": month_data.get("total_requests"),
}),
);
}
if let Some(today_data) = response_data
.get("today")
.and_then(serde_json::Value::as_object)
{
if let Some(stats) = today_data.get("stats") {
extra.insert("today_stats".to_string(), stats.clone());
}
}
Ok(admin_provider_ops_balance_data(
daily_quota_limit,
daily_used,
balance,
"USD",
extra,
))
}
pub(super) fn admin_provider_ops_yescode_balance_extra(
combined_data: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
let pay_as_you_go =
admin_provider_ops_value_as_f64(combined_data.get("pay_as_you_go_balance")).unwrap_or(0.0);
let subscription =
admin_provider_ops_value_as_f64(combined_data.get("subscription_balance")).unwrap_or(0.0);
let plan = combined_data
.get("subscription_plan")
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
let daily_balance =
admin_provider_ops_value_as_f64(plan.get("daily_balance")).unwrap_or(subscription);
let weekly_limit = admin_provider_ops_value_as_f64(
combined_data
.get("weekly_limit")
.or_else(|| plan.get("weekly_limit")),
);
let weekly_spent =
admin_provider_ops_value_as_f64(combined_data.get("weekly_spent_balance")).unwrap_or(0.0);
let subscription_available = weekly_limit
.map(|limit| (limit - weekly_spent).max(0.0).min(subscription))
.unwrap_or(subscription);
let mut extra = serde_json::Map::new();
extra.insert("pay_as_you_go_balance".to_string(), json!(pay_as_you_go));
extra.insert("daily_limit".to_string(), json!(daily_balance));
if let Some(limit) = weekly_limit {
extra.insert("weekly_limit".to_string(), json!(limit));
}
extra.insert("weekly_spent".to_string(), json!(weekly_spent));
if let Some(last_week_reset) =
admin_provider_ops_parse_rfc3339_unix_secs(combined_data.get("last_week_reset"))
{
extra.insert(
"weekly_resets_at".to_string(),
json!(last_week_reset + 7 * 24 * 3600),
);
}
if let Some(last_daily_add) =
admin_provider_ops_parse_rfc3339_unix_secs(combined_data.get("last_daily_balance_add"))
{
extra.insert(
"daily_resets_at".to_string(),
json!(last_daily_add + 24 * 3600),
);
}
let daily_spent = if let Some(limit) = weekly_limit {
daily_balance - daily_balance.min(subscription_available.min(limit.max(0.0)))
} else {
(daily_balance - subscription).max(0.0)
};
extra.insert("daily_spent".to_string(), json!(daily_spent));
extra.insert(
"_subscription_available".to_string(),
json!(subscription_available),
);
extra.insert(
"_total_available".to_string(),
json!(pay_as_you_go + subscription_available),
);
extra
}
pub(super) fn admin_provider_ops_attach_balance_checkin_outcome(
action_payload: &mut serde_json::Value,
outcome: &AdminProviderOpsCheckinOutcome,
) {
if let Some(data) = action_payload
.get_mut("data")
.and_then(serde_json::Value::as_object_mut)
{
let extra = data
.entry("extra".to_string())
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if let Some(extra) = extra.as_object_mut() {
if outcome.cookie_expired {
extra.insert("cookie_expired".to_string(), serde_json::Value::Bool(true));
extra.insert(
"cookie_expired_message".to_string(),
serde_json::Value::String(outcome.message.clone()),
);
} else {
extra.insert(
"checkin_success".to_string(),
outcome
.success
.map(serde_json::Value::Bool)
.unwrap_or(serde_json::Value::Null),
);
extra.insert(
"checkin_message".to_string(),
serde_json::Value::String(outcome.message.clone()),
);
}
}
}
if outcome.cookie_expired {
if let Some(object) = action_payload.as_object_mut() {
object.insert("status".to_string(), json!("auth_expired"));
}
}
}
@@ -0,0 +1,165 @@
use super::super::super::verify::admin_provider_ops_value_as_f64;
use super::super::responses::{
admin_provider_ops_action_error, admin_provider_ops_action_response,
};
use super::super::support::admin_provider_ops_balance_data;
use super::parsers::admin_provider_ops_yescode_balance_extra;
use crate::handlers::admin::request::AdminAppState;
use serde_json::json;
pub(super) async fn admin_provider_ops_yescode_balance_payload(
state: &AdminAppState<'_>,
base_url: &str,
headers: &reqwest::header::HeaderMap,
action_config: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Value {
let start = std::time::Instant::now();
let balance_url = format!("{}/api/v1/user/balance", base_url.trim_end_matches('/'));
let profile_url = format!("{}/api/v1/auth/profile", base_url.trim_end_matches('/'));
let balance_future = state
.http_client()
.request(reqwest::Method::GET, balance_url)
.headers(headers.clone())
.send();
let profile_future = state
.http_client()
.request(reqwest::Method::GET, profile_url)
.headers(headers.clone())
.send();
let (balance_result, profile_result) = tokio::join!(balance_future, profile_future);
let response_time_ms = Some(start.elapsed().as_millis() as u64);
let mut combined = serde_json::Map::new();
let mut has_any = false;
if let Ok(balance_response) = balance_result {
if balance_response.status() == http::StatusCode::OK {
if let Ok(bytes) = balance_response.bytes().await {
if let Ok(value) = serde_json::from_slice::<serde_json::Value>(&bytes) {
if let Some(object) = value.as_object() {
has_any = true;
combined.insert(
"_balance_data".to_string(),
serde_json::Value::Object(object.clone()),
);
combined.insert(
"pay_as_you_go_balance".to_string(),
object
.get("pay_as_you_go_balance")
.cloned()
.unwrap_or_else(|| json!(0)),
);
combined.insert(
"subscription_balance".to_string(),
object
.get("subscription_balance")
.cloned()
.unwrap_or_else(|| json!(0)),
);
if let Some(limit) = object.get("weekly_limit") {
combined.insert("weekly_limit".to_string(), limit.clone());
}
combined.insert(
"weekly_spent_balance".to_string(),
object
.get("weekly_spent_balance")
.cloned()
.unwrap_or_else(|| json!(0)),
);
}
}
}
}
}
if let Ok(profile_response) = profile_result {
if profile_response.status() == http::StatusCode::OK {
if let Ok(bytes) = profile_response.bytes().await {
if let Ok(value) = serde_json::from_slice::<serde_json::Value>(&bytes) {
if let Some(object) = value.as_object() {
has_any = true;
combined.insert(
"_profile_data".to_string(),
serde_json::Value::Object(object.clone()),
);
for key in [
"username",
"email",
"last_week_reset",
"last_daily_balance_add",
"subscription_plan",
] {
if let Some(value) = object.get(key) {
combined.insert(key.to_string(), value.clone());
}
}
combined
.entry("pay_as_you_go_balance".to_string())
.or_insert_with(|| {
object
.get("pay_as_you_go_balance")
.cloned()
.unwrap_or_else(|| json!(0))
});
combined
.entry("subscription_balance".to_string())
.or_insert_with(|| {
object
.get("subscription_balance")
.cloned()
.unwrap_or_else(|| json!(0))
});
combined
.entry("weekly_spent_balance".to_string())
.or_insert_with(|| {
object
.get("current_week_spend")
.cloned()
.unwrap_or_else(|| json!(0))
});
if !combined.contains_key("weekly_limit") {
if let Some(limit) = object
.get("subscription_plan")
.and_then(serde_json::Value::as_object)
.and_then(|plan| plan.get("weekly_limit"))
{
combined.insert("weekly_limit".to_string(), limit.clone());
}
}
}
}
}
}
}
if !has_any {
return admin_provider_ops_action_error(
"auth_failed",
"query_balance",
"Cookie 已失效,请重新配置",
response_time_ms,
);
}
let mut extra = admin_provider_ops_yescode_balance_extra(&combined);
let total_available = admin_provider_ops_value_as_f64(extra.get("_total_available"));
extra.remove("_subscription_available");
extra.remove("_total_available");
admin_provider_ops_action_response(
"success",
"query_balance",
admin_provider_ops_balance_data(
None,
None,
total_available,
action_config
.get("currency")
.and_then(serde_json::Value::as_str)
.unwrap_or("USD"),
extra,
),
None,
response_time_ms,
86400,
)
}
@@ -0,0 +1,51 @@
use serde_json::json;
pub(super) fn admin_provider_ops_action_response(
status: &str,
action_type: &str,
data: serde_json::Value,
message: Option<String>,
response_time_ms: Option<u64>,
cache_ttl_seconds: u64,
) -> serde_json::Value {
json!({
"status": status,
"action_type": action_type,
"data": data,
"message": message,
"executed_at": chrono::Utc::now()
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
"response_time_ms": response_time_ms,
"cache_ttl_seconds": cache_ttl_seconds,
})
}
pub(super) fn admin_provider_ops_action_error(
status: &str,
action_type: &str,
message: impl Into<String>,
response_time_ms: Option<u64>,
) -> serde_json::Value {
admin_provider_ops_action_response(
status,
action_type,
serde_json::Value::Null,
Some(message.into()),
response_time_ms,
0,
)
}
pub(super) fn admin_provider_ops_action_not_configured(
action_type: &str,
message: impl Into<String>,
) -> serde_json::Value {
admin_provider_ops_action_error("not_configured", action_type, message, None)
}
pub(super) fn admin_provider_ops_action_not_supported(
action_type: &str,
message: impl Into<String>,
) -> serde_json::Value {
admin_provider_ops_action_error("not_supported", action_type, message, None)
}
@@ -0,0 +1,175 @@
use super::super::config::admin_provider_ops_uses_python_verify_fallback;
use super::super::verify::admin_provider_ops_value_as_f64;
use serde_json::json;
pub(super) fn admin_provider_ops_balance_data(
total_granted: Option<f64>,
total_used: Option<f64>,
total_available: Option<f64>,
currency: &str,
extra: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Value {
json!({
"total_granted": total_granted,
"total_used": total_used,
"total_available": total_available,
"expires_at": serde_json::Value::Null,
"currency": currency,
"extra": extra,
})
}
pub(super) fn admin_provider_ops_checkin_data(
reward: Option<f64>,
streak_days: Option<i64>,
next_reward: Option<f64>,
message: Option<String>,
extra: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Value {
json!({
"reward": reward,
"streak_days": streak_days,
"next_reward": next_reward,
"message": message,
"extra": extra,
})
}
pub(super) fn admin_provider_ops_action_config_object<'a>(
provider_ops_config: &'a serde_json::Map<String, serde_json::Value>,
action_type: &str,
) -> Option<&'a serde_json::Map<String, serde_json::Value>> {
provider_ops_config
.get("actions")
.and_then(serde_json::Value::as_object)
.and_then(|actions| actions.get(action_type))
.and_then(serde_json::Value::as_object)
.and_then(|action| action.get("config"))
.and_then(serde_json::Value::as_object)
}
pub(super) fn admin_provider_ops_default_action_config(
architecture_id: &str,
action_type: &str,
) -> Option<serde_json::Map<String, serde_json::Value>> {
let value = match (architecture_id, action_type) {
("generic_api", "query_balance") => {
json!({ "endpoint": "/api/user/balance", "method": "GET" })
}
("generic_api", "checkin") => {
json!({ "endpoint": "/api/user/checkin", "method": "POST" })
}
("new_api", "query_balance") => json!({
"endpoint": "/api/user/self",
"method": "GET",
"quota_divisor": 500000,
"checkin_endpoint": "/api/user/checkin",
"currency": "USD",
}),
("new_api", "checkin") => json!({ "endpoint": "/api/user/checkin", "method": "POST" }),
("cubence", "query_balance") => {
json!({ "endpoint": "/api/v1/dashboard/overview", "method": "GET", "currency": "USD" })
}
("yescode", "query_balance") => {
json!({ "endpoint": "/api/v1/user/balance", "method": "GET", "currency": "USD" })
}
("nekocode", "query_balance") => {
json!({ "endpoint": "/api/usage/summary", "method": "GET", "currency": "USD" })
}
_ => return None,
};
value.as_object().cloned()
}
pub(super) fn admin_provider_ops_json_object_map(
value: serde_json::Value,
) -> serde_json::Map<String, serde_json::Value> {
value.as_object().cloned().unwrap_or_default()
}
pub(super) fn admin_provider_ops_resolved_action_config(
architecture_id: &str,
provider_ops_config: &serde_json::Map<String, serde_json::Value>,
action_type: &str,
request_config: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Option<serde_json::Map<String, serde_json::Value>> {
let mut resolved =
admin_provider_ops_default_action_config(architecture_id, action_type).unwrap_or_default();
if let Some(saved) = admin_provider_ops_action_config_object(provider_ops_config, action_type) {
for (key, value) in saved {
resolved.insert(key.clone(), value.clone());
}
}
if let Some(overrides) = request_config {
for (key, value) in overrides {
resolved.insert(key.clone(), value.clone());
}
}
(!resolved.is_empty()).then_some(resolved)
}
pub(super) fn admin_provider_ops_request_url(
base_url: &str,
action_config: &serde_json::Map<String, serde_json::Value>,
default_endpoint: &str,
) -> String {
let endpoint = action_config
.get("endpoint")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(default_endpoint);
if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
endpoint.to_string()
} else {
format!("{}{}", base_url.trim_end_matches('/'), endpoint)
}
}
pub(super) fn admin_provider_ops_request_method(
action_config: &serde_json::Map<String, serde_json::Value>,
default_method: &str,
) -> reqwest::Method {
action_config
.get("method")
.and_then(serde_json::Value::as_str)
.and_then(|value| reqwest::Method::from_bytes(value.trim().as_bytes()).ok())
.unwrap_or_else(|| {
reqwest::Method::from_bytes(default_method.as_bytes()).unwrap_or(reqwest::Method::GET)
})
}
pub(super) fn admin_provider_ops_parse_rfc3339_unix_secs(
value: Option<&serde_json::Value>,
) -> Option<i64> {
let raw = value?.as_str()?.trim();
if raw.is_empty() {
return None;
}
chrono::DateTime::parse_from_rfc3339(raw)
.ok()
.map(|value| value.timestamp())
}
pub(super) fn admin_provider_ops_is_cookie_auth_architecture(architecture_id: &str) -> bool {
matches!(architecture_id, "cubence" | "yescode" | "nekocode")
}
pub(super) fn admin_provider_ops_should_use_rust_only_action_stub(
architecture_id: &str,
config: &serde_json::Map<String, serde_json::Value>,
) -> bool {
!matches!(
architecture_id,
"generic_api" | "new_api" | "cubence" | "yescode" | "nekocode"
) || admin_provider_ops_uses_python_verify_fallback(architecture_id, config)
}
#[allow(dead_code)]
pub(super) fn admin_provider_ops_quota_divisor(
action_config: &serde_json::Map<String, serde_json::Value>,
) -> f64 {
admin_provider_ops_value_as_f64(action_config.get("quota_divisor"))
.filter(|value| *value > 0.0)
.unwrap_or(500000.0)
}
@@ -1,8 +1,6 @@
use super::support::{AdminProviderOpsSaveConfigRequest, ADMIN_PROVIDER_OPS_SENSITIVE_FIELDS};
use crate::handlers::admin::shared::{
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks,
};
use crate::AppState;
use crate::handlers::admin::request::AdminAppState;
use aether_admin::provider::ops as admin_provider_ops_pure;
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
};
@@ -11,28 +9,22 @@ use serde_json::json;
pub(super) fn admin_provider_ops_config_object(
provider: &StoredProviderCatalogProvider,
) -> Option<&serde_json::Map<String, serde_json::Value>> {
provider
.config
.as_ref()
.and_then(serde_json::Value::as_object)
.and_then(|config| config.get("provider_ops"))
.and_then(serde_json::Value::as_object)
admin_provider_ops_pure::admin_provider_ops_config_object(provider)
}
pub(super) fn admin_provider_ops_connector_object(
provider_ops_config: &serde_json::Map<String, serde_json::Value>,
) -> Option<&serde_json::Map<String, serde_json::Value>> {
provider_ops_config
.get("connector")
.and_then(serde_json::Value::as_object)
admin_provider_ops_pure::admin_provider_ops_connector_object(provider_ops_config)
}
fn admin_provider_ops_masked_secret(
state: &AppState,
state: &AdminAppState<'_>,
field: &str,
ciphertext: &str,
) -> serde_json::Value {
let plaintext = decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)
let plaintext = state
.decrypt_catalog_secret_with_fallbacks(ciphertext)
.unwrap_or_else(|| ciphertext.to_string());
if plaintext.is_empty() {
return serde_json::Value::String(String::new());
@@ -60,7 +52,7 @@ fn admin_provider_ops_masked_secret(
}
fn admin_provider_ops_masked_credentials(
state: &AppState,
state: &AdminAppState<'_>,
raw_credentials: Option<&serde_json::Value>,
) -> serde_json::Value {
let Some(credentials) = raw_credentials.and_then(serde_json::Value::as_object) else {
@@ -84,30 +76,18 @@ fn admin_provider_ops_masked_credentials(
}
fn admin_provider_ops_is_supported_auth_type(auth_type: &str) -> bool {
matches!(
auth_type,
"api_key" | "session_login" | "oauth" | "cookie" | "none"
)
admin_provider_ops_pure::admin_provider_ops_is_supported_auth_type(auth_type)
}
pub(super) fn admin_provider_ops_uses_python_verify_fallback(
architecture_id: &str,
config: &serde_json::Map<String, serde_json::Value>,
) -> bool {
let _ = architecture_id;
config
.get("proxy_enabled")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
|| config
.get("proxy_node_id")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.is_some_and(|value| !value.is_empty())
admin_provider_ops_pure::admin_provider_ops_uses_python_verify_fallback(architecture_id, config)
}
pub(super) fn admin_provider_ops_decrypted_credentials(
state: &AppState,
state: &AdminAppState<'_>,
raw_credentials: Option<&serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
let Some(credentials) = raw_credentials.and_then(serde_json::Value::as_object) else {
@@ -118,9 +98,9 @@ pub(super) fn admin_provider_ops_decrypted_credentials(
for (key, value) in credentials {
if ADMIN_PROVIDER_OPS_SENSITIVE_FIELDS.contains(&key.as_str()) {
if let Some(ciphertext) = value.as_str() {
let plaintext =
decrypt_catalog_secret_with_fallbacks(state.encryption_key(), ciphertext)
.unwrap_or_else(|| ciphertext.to_string());
let plaintext = state
.decrypt_catalog_secret_with_fallbacks(ciphertext)
.unwrap_or_else(|| ciphertext.to_string());
decrypted.insert(key.clone(), serde_json::Value::String(plaintext));
continue;
}
@@ -131,17 +111,11 @@ pub(super) fn admin_provider_ops_decrypted_credentials(
}
fn admin_provider_ops_sensitive_placeholder_or_empty(value: Option<&serde_json::Value>) -> bool {
match value {
None | Some(serde_json::Value::Null) => true,
Some(serde_json::Value::String(raw)) => raw.is_empty() || raw.chars().all(|ch| ch == '*'),
Some(serde_json::Value::Array(items)) => items.is_empty(),
Some(serde_json::Value::Object(map)) => map.is_empty(),
_ => false,
}
admin_provider_ops_pure::admin_provider_ops_sensitive_placeholder_or_empty(value)
}
pub(super) fn admin_provider_ops_merge_credentials(
state: &AppState,
state: &AdminAppState<'_>,
provider: &StoredProviderCatalogProvider,
mut request_credentials: serde_json::Map<String, serde_json::Value>,
) -> serde_json::Map<String, serde_json::Value> {
@@ -172,7 +146,7 @@ pub(super) fn admin_provider_ops_merge_credentials(
}
fn admin_provider_ops_encrypt_credentials(
state: &AppState,
state: &AdminAppState<'_>,
credentials: serde_json::Map<String, serde_json::Value>,
) -> Result<serde_json::Map<String, serde_json::Value>, String> {
let mut encrypted = serde_json::Map::new();
@@ -182,7 +156,8 @@ fn admin_provider_ops_encrypt_credentials(
if plaintext.is_empty() {
encrypted.insert(key, value);
} else {
let ciphertext = encrypt_catalog_secret_with_fallbacks(state, plaintext)
let ciphertext = state
.encrypt_catalog_secret_with_fallbacks(plaintext)
.ok_or_else(|| "gateway 未配置 Provider Ops 加密密钥".to_string())?;
encrypted.insert(key, serde_json::Value::String(ciphertext));
}
@@ -195,7 +170,7 @@ fn admin_provider_ops_encrypt_credentials(
}
pub(super) fn build_admin_provider_ops_saved_config_value(
state: &AppState,
state: &AdminAppState<'_>,
provider: &StoredProviderCatalogProvider,
payload: AdminProviderOpsSaveConfigRequest,
) -> Result<serde_json::Value, String> {
@@ -240,101 +215,22 @@ pub(super) fn resolve_admin_provider_ops_base_url(
endpoints: &[StoredProviderCatalogEndpoint],
provider_ops_config: Option<&serde_json::Map<String, serde_json::Value>>,
) -> Option<String> {
let from_saved_config = provider_ops_config
.and_then(|config| config.get("base_url"))
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
if from_saved_config.is_some() {
return from_saved_config;
}
if let Some(base_url) = endpoints.iter().find_map(|endpoint| {
let value = endpoint.base_url.trim();
(!value.is_empty()).then(|| value.to_string())
}) {
return Some(base_url);
}
let from_provider_config = provider
.config
.as_ref()
.and_then(serde_json::Value::as_object)
.and_then(|config| config.get("base_url"))
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned);
if from_provider_config.is_some() {
return from_provider_config;
}
provider
.website
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
admin_provider_ops_pure::resolve_admin_provider_ops_base_url(
provider,
endpoints,
provider_ops_config,
)
}
pub(super) fn build_admin_provider_ops_status_payload(
provider_id: &str,
provider: Option<&StoredProviderCatalogProvider>,
) -> serde_json::Value {
let provider_ops_config = provider.and_then(admin_provider_ops_config_object);
let auth_type = provider_ops_config
.and_then(admin_provider_ops_connector_object)
.and_then(|connector| connector.get("auth_type"))
.and_then(serde_json::Value::as_str)
.unwrap_or_else(|| {
if provider_ops_config.is_some() {
"api_key"
} else {
"none"
}
});
let mut enabled_actions = provider_ops_config
.and_then(|config| config.get("actions"))
.and_then(serde_json::Value::as_object)
.map(|actions| {
actions
.iter()
.filter_map(|(action_type, config)| {
let enabled = config
.as_object()
.and_then(|config| config.get("enabled"))
.and_then(serde_json::Value::as_bool)
.unwrap_or(true);
enabled.then(|| serde_json::Value::String(action_type.clone()))
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
enabled_actions.sort_by(|left, right| left.as_str().cmp(&right.as_str()));
json!({
"provider_id": provider_id,
"is_configured": provider_ops_config.is_some(),
"architecture_id": provider_ops_config.map(|config| {
config
.get("architecture_id")
.and_then(serde_json::Value::as_str)
.unwrap_or("generic_api")
}),
"connection_status": {
"status": "disconnected",
"auth_type": auth_type,
"connected_at": serde_json::Value::Null,
"expires_at": serde_json::Value::Null,
"last_error": serde_json::Value::Null,
},
"enabled_actions": enabled_actions,
})
admin_provider_ops_pure::build_admin_provider_ops_status_payload(provider_id, provider)
}
pub(super) fn build_admin_provider_ops_config_payload(
state: &AppState,
state: &AdminAppState<'_>,
provider_id: &str,
provider: Option<&StoredProviderCatalogProvider>,
endpoints: &[StoredProviderCatalogEndpoint],
@@ -1,703 +0,0 @@
use super::actions::{
admin_provider_ops_is_valid_action_type, admin_provider_ops_local_action_response,
};
use super::config::{
admin_provider_ops_config_object, admin_provider_ops_connector_object,
admin_provider_ops_decrypted_credentials, admin_provider_ops_merge_credentials,
build_admin_provider_ops_config_payload, build_admin_provider_ops_saved_config_value,
build_admin_provider_ops_status_payload, resolve_admin_provider_ops_base_url,
};
use super::support::{
AdminProviderOpsConnectRequest, AdminProviderOpsExecuteActionRequest,
AdminProviderOpsSaveConfigRequest, ADMIN_PROVIDER_OPS_CONNECT_RUST_ONLY_MESSAGE,
};
use super::verify::{
admin_provider_ops_local_verify_response, admin_provider_ops_normalized_verify_architecture_id,
admin_provider_ops_verify_failure,
};
use crate::control::GatewayPublicRequestContext;
use crate::handlers::admin::provider::shared::paths::{
admin_provider_id_for_provider_ops_balance, admin_provider_id_for_provider_ops_checkin,
admin_provider_id_for_provider_ops_config, admin_provider_id_for_provider_ops_connect,
admin_provider_id_for_provider_ops_disconnect, admin_provider_id_for_provider_ops_status,
admin_provider_id_for_provider_ops_verify, admin_provider_ops_action_route_parts,
};
use crate::handlers::admin::shared::attach_admin_audit_response;
use crate::{AppState, GatewayError};
use axum::{
body::{Body, Bytes},
http,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) async fn maybe_build_local_admin_provider_ops_providers_response(
state: &AppState,
request_context: &GatewayPublicRequestContext,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(decision) = request_context.control_decision.as_ref() else {
return Ok(None);
};
if decision.route_family.as_deref() != Some("provider_ops_manage") {
return Ok(None);
}
let route_kind = decision.route_kind.as_deref().unwrap_or_default();
if !state.has_provider_catalog_data_reader() && route_kind != "disconnect_provider" {
return Ok(None);
}
if route_kind == "batch_balance" {
let requested_provider_ids = match request_body {
Some(body) if !body.is_empty() => {
let raw_value = match serde_json::from_slice::<serde_json::Value>(body) {
Ok(value) => value,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是 provider_id 数组" })),
)
.into_response(),
));
}
};
let ids = if let Some(items) = raw_value.as_array() {
items
.iter()
.filter_map(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
} else if let Some(items) = raw_value
.get("provider_ids")
.and_then(serde_json::Value::as_array)
{
items
.iter()
.filter_map(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
} else {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是 provider_id 数组" })),
)
.into_response(),
));
};
Some(ids)
}
_ => None,
};
let provider_ids = if let Some(provider_ids) = requested_provider_ids {
provider_ids
} else {
state
.list_provider_catalog_providers(true)
.await?
.into_iter()
.filter(|provider| {
provider
.config
.as_ref()
.and_then(serde_json::Value::as_object)
.is_some_and(|config| config.contains_key("provider_ops"))
})
.map(|provider| provider.id)
.collect::<Vec<_>>()
};
if provider_ids.is_empty() {
return Ok(Some(Json(json!({})).into_response()));
}
let providers = state
.read_provider_catalog_providers_by_ids(&provider_ids)
.await?;
let endpoints = state
.list_provider_catalog_endpoints_by_provider_ids(&provider_ids)
.await?;
let mut payload = serde_json::Map::new();
for provider_id in &provider_ids {
let provider = providers
.iter()
.find(|provider| provider.id == *provider_id);
let provider_endpoints = endpoints
.iter()
.filter(|endpoint| endpoint.provider_id == *provider_id)
.cloned()
.collect::<Vec<_>>();
let result = admin_provider_ops_local_action_response(
state,
provider_id,
provider,
&provider_endpoints,
"query_balance",
None,
)
.await;
payload.insert(provider_id.clone(), result);
}
return Ok(Some(
Json(serde_json::Value::Object(payload)).into_response(),
));
}
let action_route = if route_kind == "execute_provider_action" {
admin_provider_ops_action_route_parts(&request_context.request_path)
} else {
None
};
let provider_id = if matches!(
route_kind,
"get_provider_status"
| "get_provider_config"
| "save_provider_config"
| "delete_provider_config"
| "verify_provider"
| "connect_provider"
| "disconnect_provider"
| "get_provider_balance"
| "refresh_provider_balance"
| "provider_checkin"
| "execute_provider_action"
) {
admin_provider_id_for_provider_ops_config(&request_context.request_path)
.or_else(|| admin_provider_id_for_provider_ops_status(&request_context.request_path))
.or_else(|| admin_provider_id_for_provider_ops_verify(&request_context.request_path))
.or_else(|| admin_provider_id_for_provider_ops_connect(&request_context.request_path))
.or_else(|| admin_provider_id_for_provider_ops_balance(&request_context.request_path))
.or_else(|| admin_provider_id_for_provider_ops_checkin(&request_context.request_path))
.or_else(|| {
action_route
.as_ref()
.map(|(provider_id, _)| provider_id.clone())
})
.or_else(|| {
admin_provider_id_for_provider_ops_disconnect(&request_context.request_path)
})
} else {
None
};
let Some(provider_id) = provider_id else {
return Ok(None);
};
if decision.route_kind.as_deref() != Some(route_kind) {
return Ok(None);
}
if route_kind == "save_provider_config" {
let Some(request_body) = request_body else {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体不能为空" })),
)
.into_response(),
));
};
let raw_value = match serde_json::from_slice::<serde_json::Value>(request_body) {
Ok(value) => value,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
};
if !raw_value.is_object() {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
let payload = match serde_json::from_value::<AdminProviderOpsSaveConfigRequest>(raw_value) {
Ok(payload) => payload,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
};
let Some(existing_provider) = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id))
.await?
.into_iter()
.next()
else {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Provider 不存在" })),
)
.into_response(),
));
};
let provider_ops_config =
match build_admin_provider_ops_saved_config_value(state, &existing_provider, payload) {
Ok(config) => config,
Err(detail) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": detail })),
)
.into_response(),
));
}
};
let mut updated_provider = existing_provider.clone();
let mut provider_config = updated_provider
.config
.as_ref()
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
provider_config.insert("provider_ops".to_string(), provider_ops_config);
updated_provider.config = Some(serde_json::Value::Object(provider_config));
updated_provider.updated_at_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs());
let Some(_updated) = state
.update_provider_catalog_provider(&updated_provider)
.await?
else {
return Ok(None);
};
return Ok(Some(
Json(json!({
"success": true,
"message": "配置保存成功",
}))
.into_response(),
));
}
if route_kind == "verify_provider" {
let Some(request_body) = request_body else {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体不能为空" })),
)
.into_response(),
));
};
let raw_value = match serde_json::from_slice::<serde_json::Value>(request_body) {
Ok(value) => value,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
};
if !raw_value.is_object() {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
let payload = match serde_json::from_value::<AdminProviderOpsSaveConfigRequest>(raw_value) {
Ok(payload) => payload,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
};
let existing_provider = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id))
.await?
.into_iter()
.next();
let endpoints = if existing_provider.is_some() {
state
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
.await?
} else {
Vec::new()
};
let base_url = payload
.base_url
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.or_else(|| {
existing_provider.as_ref().and_then(|provider| {
resolve_admin_provider_ops_base_url(
provider,
&endpoints,
admin_provider_ops_config_object(provider),
)
})
});
let Some(base_url) = base_url else {
return Ok(Some(
Json(admin_provider_ops_verify_failure("请提供 API 地址")).into_response(),
));
};
let architecture_id =
admin_provider_ops_normalized_verify_architecture_id(&payload.architecture_id);
let credentials = existing_provider.as_ref().map_or_else(
|| payload.connector.credentials.clone(),
|provider| {
admin_provider_ops_merge_credentials(
state,
provider,
payload.connector.credentials.clone(),
)
},
);
let payload = admin_provider_ops_local_verify_response(
state,
&base_url,
architecture_id,
&payload.connector.config,
&credentials,
)
.await;
return Ok(Some(attach_admin_audit_response(
Json(payload).into_response(),
"admin_provider_ops_config_verified",
"verify_provider_ops_config",
"provider",
&provider_id,
)));
}
if route_kind == "connect_provider" {
let Some(request_body) = request_body else {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体不能为空" })),
)
.into_response(),
));
};
let raw_value = match serde_json::from_slice::<serde_json::Value>(request_body) {
Ok(value) => value,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
};
if !raw_value.is_object() {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
let payload = match serde_json::from_value::<AdminProviderOpsConnectRequest>(raw_value) {
Ok(payload) => payload,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
};
let Some(existing_provider) = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id))
.await?
.into_iter()
.next()
else {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "Provider 不存在" })),
)
.into_response(),
));
};
let Some(provider_ops_config) = admin_provider_ops_config_object(&existing_provider) else {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "未配置操作设置" })),
)
.into_response(),
));
};
let endpoints = state
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
.await?;
if resolve_admin_provider_ops_base_url(
&existing_provider,
&endpoints,
Some(provider_ops_config),
)
.is_none()
{
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "Provider 未配置 base_url" })),
)
.into_response(),
));
}
let actual_credentials = payload
.credentials
.filter(|value| !value.is_empty())
.unwrap_or_else(|| {
admin_provider_ops_decrypted_credentials(
state,
admin_provider_ops_config_object(&existing_provider)
.and_then(admin_provider_ops_connector_object)
.and_then(|connector| connector.get("credentials")),
)
});
if actual_credentials.is_empty() {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "未提供凭据" })),
)
.into_response(),
));
}
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": ADMIN_PROVIDER_OPS_CONNECT_RUST_ONLY_MESSAGE })),
)
.into_response(),
));
}
if matches!(
route_kind,
"get_provider_balance"
| "refresh_provider_balance"
| "provider_checkin"
| "execute_provider_action"
) {
let action_type = if route_kind == "provider_checkin" {
"checkin".to_string()
} else if matches!(
route_kind,
"get_provider_balance" | "refresh_provider_balance"
) {
"query_balance".to_string()
} else {
let Some((_, action_type)) = action_route else {
return Ok(None);
};
if !admin_provider_ops_is_valid_action_type(&action_type) {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": format!("无效的操作类型: {action_type}") })),
)
.into_response(),
));
}
action_type
};
let request_config = if route_kind == "execute_provider_action" {
match request_body {
Some(body) if !body.is_empty() => {
let raw_value = match serde_json::from_slice::<serde_json::Value>(body) {
Ok(value) => value,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
};
let payload = match serde_json::from_value::<AdminProviderOpsExecuteActionRequest>(
raw_value,
) {
Ok(payload) => payload,
Err(_) => {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是合法的 JSON 对象" })),
)
.into_response(),
));
}
};
payload.config
}
_ => None,
}
} else {
None
};
let providers = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id))
.await?;
let provider = providers.first();
let endpoints = if provider.is_some() {
state
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
.await?
} else {
Vec::new()
};
let payload = admin_provider_ops_local_action_response(
state,
&provider_id,
provider,
&endpoints,
&action_type,
request_config.as_ref(),
)
.await;
return Ok(Some(Json(payload).into_response()));
}
if route_kind == "delete_provider_config" {
let Some(existing_provider) = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id))
.await?
.into_iter()
.next()
else {
return Ok(Some(
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Provider 不存在" })),
)
.into_response(),
));
};
let mut updated_provider = existing_provider.clone();
let mut provider_config = updated_provider
.config
.as_ref()
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
if provider_config.remove("provider_ops").is_some() {
updated_provider.config = Some(serde_json::Value::Object(provider_config));
updated_provider.updated_at_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs());
let Some(_updated) = state
.update_provider_catalog_provider(&updated_provider)
.await?
else {
return Ok(None);
};
}
return Ok(Some(
Json(json!({
"success": true,
"message": "配置已删除",
}))
.into_response(),
));
}
if route_kind == "disconnect_provider" {
return Ok(Some(
Json(json!({
"success": true,
"message": "已断开连接",
}))
.into_response(),
));
}
let providers = state
.read_provider_catalog_providers_by_ids(std::slice::from_ref(&provider_id))
.await?;
let provider = providers.first();
let endpoints = if route_kind == "get_provider_config" && provider.is_some() {
state
.list_provider_catalog_endpoints_by_provider_ids(std::slice::from_ref(&provider_id))
.await?
} else {
Vec::new()
};
let payload = if route_kind == "get_provider_status" {
build_admin_provider_ops_status_payload(&provider_id, provider)
} else {
build_admin_provider_ops_config_payload(state, &provider_id, provider, &endpoints)
};
let response = Json(payload).into_response();
let response = if route_kind == "get_provider_config" {
attach_admin_audit_response(
response,
"admin_provider_ops_config_viewed",
"view_provider_ops_config",
"provider",
&provider_id,
)
} else if route_kind == "get_provider_status" {
attach_admin_audit_response(
response,
"admin_provider_ops_status_viewed",
"view_provider_ops_status",
"provider",
&provider_id,
)
} else {
response
};
Ok(Some(response))
}
@@ -0,0 +1,105 @@
use super::super::actions::{
admin_provider_ops_is_valid_action_type, admin_provider_ops_local_action_response,
};
use super::super::support::AdminProviderOpsExecuteActionRequest;
use crate::handlers::admin::request::AdminAppState;
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
http,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
pub(super) async fn handle_admin_provider_ops_action(
state: &AdminAppState<'_>,
provider_id: &str,
route_kind: &str,
action_route: Option<&(String, String)>,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let action_type = if route_kind == "provider_checkin" {
"checkin".to_string()
} else if matches!(
route_kind,
"get_provider_balance" | "refresh_provider_balance"
) {
"query_balance".to_string()
} else {
let Some((_, action_type)) = action_route else {
return Ok(None);
};
if !admin_provider_ops_is_valid_action_type(action_type) {
return Ok(Some(
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": format!("无效的操作类型: {action_type}") })),
)
.into_response(),
));
}
action_type.clone()
};
let request_config = if route_kind == "execute_provider_action" {
match request_body {
Some(body) if !body.is_empty() => {
let raw_value = match serde_json::from_slice::<serde_json::Value>(body) {
Ok(raw_value) => raw_value,
Err(_) => {
return Ok(Some(bad_request_detail_response(
"请求体必须是合法的 JSON 对象",
)))
}
};
let payload =
match serde_json::from_value::<AdminProviderOpsExecuteActionRequest>(raw_value)
{
Ok(payload) => payload,
Err(_) => {
return Ok(Some(bad_request_detail_response(
"请求体必须是合法的 JSON 对象",
)))
}
};
payload.config
}
_ => None,
}
} else {
None
};
let provider_ids = [provider_id.to_string()];
let providers = state
.read_provider_catalog_providers_by_ids(&provider_ids)
.await?;
let provider = providers.first();
let endpoints = if provider.is_some() {
state
.list_provider_catalog_endpoints_by_provider_ids(&provider_ids)
.await?
} else {
Vec::new()
};
let payload = admin_provider_ops_local_action_response(
state,
provider_id,
provider,
&endpoints,
&action_type,
request_config.as_ref(),
)
.await;
Ok(Some(Json(payload).into_response()))
}
fn bad_request_detail_response(detail: &str) -> Response<Body> {
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": detail })),
)
.into_response()
}
@@ -0,0 +1,108 @@
use super::super::actions::admin_provider_ops_local_action_response;
use crate::handlers::admin::request::AdminAppState;
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
http,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
pub(super) async fn handle_admin_provider_ops_batch_balance(
state: &AdminAppState<'_>,
request_body: Option<&Bytes>,
) -> Result<Response<Body>, GatewayError> {
let requested_provider_ids = match request_body {
Some(body) if !body.is_empty() => match parse_provider_ids(body) {
Ok(provider_ids) => Some(provider_ids),
Err(response) => return Ok(response),
},
_ => None,
};
let provider_ids = if let Some(provider_ids) = requested_provider_ids {
provider_ids
} else {
state
.list_provider_catalog_providers(true)
.await?
.into_iter()
.filter(|provider| {
provider
.config
.as_ref()
.and_then(serde_json::Value::as_object)
.is_some_and(|config| config.contains_key("provider_ops"))
})
.map(|provider| provider.id)
.collect::<Vec<_>>()
};
if provider_ids.is_empty() {
return Ok(Json(json!({})).into_response());
}
let providers = state
.read_provider_catalog_providers_by_ids(&provider_ids)
.await?;
let endpoints = state
.list_provider_catalog_endpoints_by_provider_ids(&provider_ids)
.await?;
let mut payload = serde_json::Map::new();
for provider_id in &provider_ids {
let provider = providers
.iter()
.find(|provider| provider.id == *provider_id);
let provider_endpoints = endpoints
.iter()
.filter(|endpoint| endpoint.provider_id == *provider_id)
.cloned()
.collect::<Vec<_>>();
let result = admin_provider_ops_local_action_response(
state,
provider_id,
provider,
&provider_endpoints,
"query_balance",
None,
)
.await;
payload.insert(provider_id.clone(), result);
}
Ok(Json(serde_json::Value::Object(payload)).into_response())
}
fn parse_provider_ids(body: &Bytes) -> Result<Vec<String>, Response<Body>> {
let raw_value = serde_json::from_slice::<serde_json::Value>(body).map_err(|_| {
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是 provider_id 数组" })),
)
.into_response()
})?;
let items = raw_value
.as_array()
.or_else(|| {
raw_value
.get("provider_ids")
.and_then(serde_json::Value::as_array)
})
.ok_or_else(|| {
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": "请求体必须是 provider_id 数组" })),
)
.into_response()
})?;
Ok(items
.iter()
.filter_map(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.collect())
}
@@ -0,0 +1,143 @@
use super::super::config::build_admin_provider_ops_saved_config_value;
use super::super::support::AdminProviderOpsSaveConfigRequest;
use crate::handlers::admin::request::AdminAppState;
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
http,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};
pub(super) async fn handle_admin_provider_ops_save_config(
state: &AdminAppState<'_>,
provider_id: &str,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let payload = match parse_json_object_payload::<AdminProviderOpsSaveConfigRequest>(request_body)
{
Ok(payload) => payload,
Err(response) => return Ok(Some(response)),
};
let provider_ids = [provider_id.to_string()];
let Some(existing_provider) = state
.read_provider_catalog_providers_by_ids(&provider_ids)
.await?
.into_iter()
.next()
else {
return Ok(Some(provider_not_found_response()));
};
let provider_ops_config =
match build_admin_provider_ops_saved_config_value(state, &existing_provider, payload) {
Ok(config) => config,
Err(detail) => return Ok(Some(bad_request_detail_response(&detail))),
};
let mut updated_provider = existing_provider.clone();
let mut provider_config = updated_provider
.config
.as_ref()
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
provider_config.insert("provider_ops".to_string(), provider_ops_config);
updated_provider.config = Some(serde_json::Value::Object(provider_config));
updated_provider.updated_at_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs());
let Some(_updated) = state
.update_provider_catalog_provider(&updated_provider)
.await?
else {
return Ok(None);
};
Ok(Some(
Json(json!({
"success": true,
"message": "配置保存成功",
}))
.into_response(),
))
}
pub(super) async fn handle_admin_provider_ops_delete_config(
state: &AdminAppState<'_>,
provider_id: &str,
) -> Result<Option<Response<Body>>, GatewayError> {
let provider_ids = [provider_id.to_string()];
let Some(existing_provider) = state
.read_provider_catalog_providers_by_ids(&provider_ids)
.await?
.into_iter()
.next()
else {
return Ok(Some(provider_not_found_response()));
};
let mut updated_provider = existing_provider.clone();
let mut provider_config = updated_provider
.config
.as_ref()
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
if provider_config.remove("provider_ops").is_some() {
updated_provider.config = Some(serde_json::Value::Object(provider_config));
updated_provider.updated_at_unix_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| duration.as_secs());
let Some(_updated) = state
.update_provider_catalog_provider(&updated_provider)
.await?
else {
return Ok(None);
};
}
Ok(Some(
Json(json!({
"success": true,
"message": "配置已删除",
}))
.into_response(),
))
}
fn parse_json_object_payload<T>(request_body: Option<&Bytes>) -> Result<T, Response<Body>>
where
T: serde::de::DeserializeOwned,
{
let Some(request_body) = request_body else {
return Err(bad_request_detail_response("请求体不能为空"));
};
let raw_value = serde_json::from_slice::<serde_json::Value>(request_body)
.map_err(|_| bad_request_detail_response("请求体必须是合法的 JSON 对象"))?;
if !raw_value.is_object() {
return Err(bad_request_detail_response("请求体必须是合法的 JSON 对象"));
}
serde_json::from_value::<T>(raw_value)
.map_err(|_| bad_request_detail_response("请求体必须是合法的 JSON 对象"))
}
fn bad_request_detail_response(detail: &str) -> Response<Body> {
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": detail })),
)
.into_response()
}
fn provider_not_found_response() -> Response<Body> {
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "Provider 不存在" })),
)
.into_response()
}
@@ -0,0 +1,104 @@
use super::super::config::{
admin_provider_ops_config_object, admin_provider_ops_connector_object,
admin_provider_ops_decrypted_credentials, resolve_admin_provider_ops_base_url,
};
use super::super::support::{
AdminProviderOpsConnectRequest, ADMIN_PROVIDER_OPS_CONNECT_RUST_ONLY_MESSAGE,
};
use crate::handlers::admin::request::AdminAppState;
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
http,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
pub(super) async fn handle_admin_provider_ops_connect(
state: &AdminAppState<'_>,
provider_id: &str,
request_body: Option<&Bytes>,
) -> Result<Response<Body>, GatewayError> {
let payload = match parse_json_object_payload::<AdminProviderOpsConnectRequest>(request_body) {
Ok(payload) => payload,
Err(response) => return Ok(response),
};
let provider_ids = [provider_id.to_string()];
let Some(existing_provider) = state
.read_provider_catalog_providers_by_ids(&provider_ids)
.await?
.into_iter()
.next()
else {
return Ok(bad_request_detail_response("Provider 不存在"));
};
let Some(provider_ops_config) = admin_provider_ops_config_object(&existing_provider) else {
return Ok(bad_request_detail_response("未配置操作设置"));
};
let endpoints = state
.list_provider_catalog_endpoints_by_provider_ids(&provider_ids)
.await?;
if resolve_admin_provider_ops_base_url(
&existing_provider,
&endpoints,
Some(provider_ops_config),
)
.is_none()
{
return Ok(bad_request_detail_response("Provider 未配置 base_url"));
}
let actual_credentials = payload
.credentials
.filter(|value| !value.is_empty())
.unwrap_or_else(|| {
admin_provider_ops_decrypted_credentials(
state,
admin_provider_ops_config_object(&existing_provider)
.and_then(admin_provider_ops_connector_object)
.and_then(|connector| connector.get("credentials")),
)
});
if actual_credentials.is_empty() {
return Ok(bad_request_detail_response("未提供凭据"));
}
Ok((
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": ADMIN_PROVIDER_OPS_CONNECT_RUST_ONLY_MESSAGE })),
)
.into_response())
}
pub(super) fn handle_admin_provider_ops_disconnect() -> Response<Body> {
Json(json!({
"success": true,
"message": "已断开连接",
}))
.into_response()
}
fn parse_json_object_payload<T>(request_body: Option<&Bytes>) -> Result<T, Response<Body>>
where
T: serde::de::DeserializeOwned,
{
let Some(request_body) = request_body else {
return Err(bad_request_detail_response("请求体不能为空"));
};
let raw_value = serde_json::from_slice::<serde_json::Value>(request_body)
.map_err(|_| bad_request_detail_response("请求体必须是合法的 JSON 对象"))?;
if !raw_value.is_object() {
return Err(bad_request_detail_response("请求体必须是合法的 JSON 对象"));
}
serde_json::from_value::<T>(raw_value)
.map_err(|_| bad_request_detail_response("请求体必须是合法的 JSON 对象"))
}
fn bad_request_detail_response(detail: &str) -> Response<Body> {
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": detail })),
)
.into_response()
}
@@ -0,0 +1,138 @@
mod actions;
mod batch;
mod config;
mod connect;
mod read;
mod verify;
use crate::handlers::admin::provider::shared::paths::{
admin_provider_id_for_provider_ops_balance, admin_provider_id_for_provider_ops_checkin,
admin_provider_id_for_provider_ops_config, admin_provider_id_for_provider_ops_connect,
admin_provider_id_for_provider_ops_disconnect, admin_provider_id_for_provider_ops_status,
admin_provider_id_for_provider_ops_verify, admin_provider_ops_action_route_parts,
};
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
response::Response,
};
pub(crate) async fn maybe_build_local_admin_provider_ops_providers_response(
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
request_body: Option<&Bytes>,
) -> Result<Option<Response<Body>>, GatewayError> {
let Some(decision) = request_context.decision() else {
return Ok(None);
};
if decision.route_family.as_deref() != Some("provider_ops_manage") {
return Ok(None);
}
let route_kind = decision.route_kind.as_deref().unwrap_or_default();
if !state.has_provider_catalog_data_reader() && route_kind != "disconnect_provider" {
return Ok(None);
}
if route_kind == "batch_balance" {
return batch::handle_admin_provider_ops_batch_balance(state, request_body)
.await
.map(Some);
}
let action_route = (route_kind == "execute_provider_action")
.then(|| admin_provider_ops_action_route_parts(request_context.path()))
.flatten();
let Some(provider_id) =
provider_id_for_route(request_context.path(), route_kind, action_route.as_ref())
else {
return Ok(None);
};
if decision.route_kind.as_deref() != Some(route_kind) {
return Ok(None);
}
let response = match route_kind {
"save_provider_config" => {
let Some(response) =
config::handle_admin_provider_ops_save_config(state, &provider_id, request_body)
.await?
else {
return Ok(None);
};
response
}
"verify_provider" => {
verify::handle_admin_provider_ops_verify(state, &provider_id, request_body).await?
}
"connect_provider" => {
connect::handle_admin_provider_ops_connect(state, &provider_id, request_body).await?
}
"get_provider_balance"
| "refresh_provider_balance"
| "provider_checkin"
| "execute_provider_action" => {
let Some(response) = actions::handle_admin_provider_ops_action(
state,
&provider_id,
route_kind,
action_route.as_ref(),
request_body,
)
.await?
else {
return Ok(None);
};
response
}
"delete_provider_config" => {
let Some(response) =
config::handle_admin_provider_ops_delete_config(state, &provider_id).await?
else {
return Ok(None);
};
response
}
"disconnect_provider" => connect::handle_admin_provider_ops_disconnect(),
"get_provider_status" | "get_provider_config" => {
read::handle_admin_provider_ops_read(state, &provider_id, route_kind).await?
}
_ => return Ok(None),
};
Ok(Some(response))
}
fn provider_id_for_route(
request_path: &str,
route_kind: &str,
action_route: Option<&(String, String)>,
) -> Option<String> {
if !matches!(
route_kind,
"get_provider_status"
| "get_provider_config"
| "save_provider_config"
| "delete_provider_config"
| "verify_provider"
| "connect_provider"
| "disconnect_provider"
| "get_provider_balance"
| "refresh_provider_balance"
| "provider_checkin"
| "execute_provider_action"
) {
return None;
}
admin_provider_id_for_provider_ops_config(request_path)
.or_else(|| admin_provider_id_for_provider_ops_status(request_path))
.or_else(|| admin_provider_id_for_provider_ops_verify(request_path))
.or_else(|| admin_provider_id_for_provider_ops_connect(request_path))
.or_else(|| admin_provider_id_for_provider_ops_balance(request_path))
.or_else(|| admin_provider_id_for_provider_ops_checkin(request_path))
.or_else(|| action_route.map(|(provider_id, _)| provider_id.clone()))
.or_else(|| admin_provider_id_for_provider_ops_disconnect(request_path))
}
@@ -0,0 +1,59 @@
use super::super::config::{
build_admin_provider_ops_config_payload, build_admin_provider_ops_status_payload,
};
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::shared::attach_admin_audit_response;
use crate::GatewayError;
use axum::{
body::Body,
response::{IntoResponse, Response},
Json,
};
pub(super) async fn handle_admin_provider_ops_read(
state: &AdminAppState<'_>,
provider_id: &str,
route_kind: &str,
) -> Result<Response<Body>, GatewayError> {
let provider_ids = [provider_id.to_string()];
let providers = state
.read_provider_catalog_providers_by_ids(&provider_ids)
.await?;
let provider = providers.first();
let endpoints = if route_kind == "get_provider_config" && provider.is_some() {
state
.list_provider_catalog_endpoints_by_provider_ids(&provider_ids)
.await?
} else {
Vec::new()
};
let payload = if route_kind == "get_provider_status" {
build_admin_provider_ops_status_payload(provider_id, provider)
} else {
build_admin_provider_ops_config_payload(state, provider_id, provider, &endpoints)
};
let response = Json(payload).into_response();
let response = if route_kind == "get_provider_config" {
attach_admin_audit_response(
response,
"admin_provider_ops_config_viewed",
"view_provider_ops_config",
"provider",
provider_id,
)
} else if route_kind == "get_provider_status" {
attach_admin_audit_response(
response,
"admin_provider_ops_status_viewed",
"view_provider_ops_status",
"provider",
provider_id,
)
} else {
response
};
Ok(response)
}
@@ -0,0 +1,114 @@
use super::super::config::{
admin_provider_ops_config_object, admin_provider_ops_merge_credentials,
resolve_admin_provider_ops_base_url,
};
use super::super::support::AdminProviderOpsSaveConfigRequest;
use super::super::verify::{
admin_provider_ops_local_verify_response, admin_provider_ops_normalized_verify_architecture_id,
admin_provider_ops_verify_failure,
};
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::shared::attach_admin_audit_response;
use crate::GatewayError;
use axum::{
body::{Body, Bytes},
http,
response::{IntoResponse, Response},
Json,
};
pub(super) async fn handle_admin_provider_ops_verify(
state: &AdminAppState<'_>,
provider_id: &str,
request_body: Option<&Bytes>,
) -> Result<Response<Body>, GatewayError> {
let payload = match parse_json_object_payload::<AdminProviderOpsSaveConfigRequest>(request_body)
{
Ok(payload) => payload,
Err(response) => return Ok(response),
};
let provider_ids = [provider_id.to_string()];
let existing_provider = state
.read_provider_catalog_providers_by_ids(&provider_ids)
.await?
.into_iter()
.next();
let endpoints = if existing_provider.is_some() {
state
.list_provider_catalog_endpoints_by_provider_ids(&provider_ids)
.await?
} else {
Vec::new()
};
let base_url = payload
.base_url
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.or_else(|| {
existing_provider.as_ref().and_then(|provider| {
resolve_admin_provider_ops_base_url(
provider,
&endpoints,
admin_provider_ops_config_object(provider),
)
})
});
let Some(base_url) = base_url else {
return Ok(Json(admin_provider_ops_verify_failure("请提供 API 地址")).into_response());
};
let architecture_id =
admin_provider_ops_normalized_verify_architecture_id(&payload.architecture_id);
let credentials = existing_provider.as_ref().map_or_else(
|| payload.connector.credentials.clone(),
|provider| {
admin_provider_ops_merge_credentials(
state,
provider,
payload.connector.credentials.clone(),
)
},
);
let payload = admin_provider_ops_local_verify_response(
state,
&base_url,
architecture_id,
&payload.connector.config,
&credentials,
)
.await;
Ok(attach_admin_audit_response(
Json(payload).into_response(),
"admin_provider_ops_config_verified",
"verify_provider_ops_config",
"provider",
provider_id,
))
}
fn parse_json_object_payload<T>(request_body: Option<&Bytes>) -> Result<T, Response<Body>>
where
T: serde::de::DeserializeOwned,
{
let Some(request_body) = request_body else {
return Err(bad_request_detail_response("请求体不能为空"));
};
let raw_value = serde_json::from_slice::<serde_json::Value>(request_body)
.map_err(|_| bad_request_detail_response("请求体必须是合法的 JSON 对象"))?;
if !raw_value.is_object() {
return Err(bad_request_detail_response("请求体必须是合法的 JSON 对象"));
}
serde_json::from_value::<T>(raw_value)
.map_err(|_| bad_request_detail_response("请求体必须是合法的 JSON 对象"))
}
fn bad_request_detail_response(detail: &str) -> Response<Body> {
(
http::StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "detail": detail })),
)
.into_response()
}
@@ -1,910 +0,0 @@
use super::support::ADMIN_PROVIDER_OPS_VERIFY_RUST_ONLY_MESSAGE;
use crate::AppState;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use regex::Regex;
use serde_json::json;
pub(super) fn admin_provider_ops_normalized_verify_architecture_id(architecture_id: &str) -> &str {
match architecture_id.trim() {
"" => "generic_api",
"generic_api" | "new_api" | "cubence" | "yescode" | "nekocode" | "anyrouter"
| "sub2api" => architecture_id.trim(),
_ => "generic_api",
}
}
fn admin_provider_ops_extract_cookie_value(cookie_input: &str, key: &str) -> String {
if cookie_input.contains(&format!("{key}=")) {
for part in cookie_input.split(';') {
let trimmed = part.trim();
if let Some(value) = trimmed.strip_prefix(&format!("{key}=")) {
return value.trim().to_string();
}
}
}
cookie_input.trim().to_string()
}
fn admin_provider_ops_yescode_cookie_header(cookie_input: &str) -> String {
if cookie_input.contains("yescode_auth=") {
let mut parts = Vec::new();
for part in cookie_input.split(';') {
let trimmed = part.trim();
if let Some(value) = trimmed.strip_prefix("yescode_auth=") {
parts.push(format!("yescode_auth={}", value.trim()));
} else if let Some(value) = trimmed.strip_prefix("yescode_csrf=") {
parts.push(format!("yescode_csrf={}", value.trim()));
}
}
return parts.join("; ");
}
format!("yescode_auth={}", cookie_input.trim())
}
const ADMIN_PROVIDER_OPS_ANYROUTER_XOR_KEY: &str = "3000176000856006061501533003690027800375";
const ADMIN_PROVIDER_OPS_ANYROUTER_UNSBOX_TABLE: [usize; 40] = [
0xF, 0x23, 0x1D, 0x18, 0x21, 0x10, 0x1, 0x26, 0xA, 0x9, 0x13, 0x1F, 0x28, 0x1B, 0x16, 0x17,
0x19, 0xD, 0x6, 0xB, 0x27, 0x12, 0x14, 0x8, 0xE, 0x15, 0x20, 0x1A, 0x2, 0x1E, 0x7, 0x4, 0x11,
0x5, 0x3, 0x1C, 0x22, 0x25, 0xC, 0x24,
];
fn admin_provider_ops_anyrouter_compute_acw_sc_v2(arg1: &str) -> Option<String> {
if arg1.len() != 40 || !arg1.chars().all(|ch| ch.is_ascii_hexdigit()) {
return None;
}
let chars = arg1.chars().collect::<Vec<_>>();
let unsboxed = ADMIN_PROVIDER_OPS_ANYROUTER_UNSBOX_TABLE
.iter()
.map(|index| chars.get(index.saturating_sub(1)).copied())
.collect::<Option<String>>()?;
let mut result = String::with_capacity(40);
for i in (0..40).step_by(2) {
let a = u8::from_str_radix(&unsboxed[i..i + 2], 16).ok()?;
let b = u8::from_str_radix(&ADMIN_PROVIDER_OPS_ANYROUTER_XOR_KEY[i..i + 2], 16).ok()?;
result.push_str(&format!("{:02x}", a ^ b));
}
Some(result)
}
fn admin_provider_ops_anyrouter_parse_session_user_id(cookie_input: &str) -> Option<String> {
let session_cookie = admin_provider_ops_extract_cookie_value(cookie_input, "session");
let decoded = URL_SAFE_NO_PAD.decode(session_cookie.as_bytes()).ok()?;
let text = String::from_utf8_lossy(&decoded);
let mut parts = text.split('|');
let _timestamp = parts.next()?;
let gob_b64 = parts.next()?;
let gob_data = URL_SAFE_NO_PAD.decode(gob_b64.as_bytes()).ok()?;
let id_pattern = b"\x02id\x03int";
let id_idx = gob_data
.windows(id_pattern.len())
.position(|window| window == id_pattern)?;
let value_start = id_idx + id_pattern.len() + 2;
let first_byte = *gob_data.get(value_start)?;
if first_byte != 0 {
return None;
}
let marker = *gob_data.get(value_start + 1)?;
if marker < 0x80 {
return None;
}
let length = 256usize.saturating_sub(marker as usize);
let end = value_start + 2 + length;
let bytes = gob_data.get(value_start + 2..end)?;
let val = bytes
.iter()
.fold(0u64, |acc, byte| (acc << 8) | (*byte as u64));
Some((val >> 1).to_string())
}
async fn admin_provider_ops_anyrouter_acw_cookie(
state: &AppState,
base_url: &str,
) -> Option<String> {
let response = state
.client
.get(base_url.trim_end_matches('/'))
.header(
reqwest::header::USER_AGENT,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
)
.send()
.await
.ok()?;
let body = response.text().await.ok()?;
let compiled = Regex::new(r"var\s+arg1\s*=\s*'([0-9a-fA-F]{40})'").ok()?;
let captures = compiled.captures(&body)?;
let arg1 = captures.get(1)?.as_str();
admin_provider_ops_anyrouter_compute_acw_sc_v2(arg1).map(|value| format!("acw_sc__v2={value}"))
}
pub(super) fn admin_provider_ops_verify_failure(message: impl Into<String>) -> serde_json::Value {
json!({
"success": false,
"message": message.into(),
})
}
fn admin_provider_ops_verify_success(
data: serde_json::Value,
updated_credentials: Option<serde_json::Map<String, serde_json::Value>>,
) -> serde_json::Value {
let mut payload = serde_json::Map::from_iter([
("success".to_string(), serde_json::Value::Bool(true)),
("data".to_string(), data),
]);
if let Some(credentials) = updated_credentials.filter(|value| !value.is_empty()) {
payload.insert(
"updated_credentials".to_string(),
serde_json::Value::Object(credentials),
);
}
serde_json::Value::Object(payload)
}
fn admin_provider_ops_verify_user_payload(
username: Option<String>,
display_name: Option<String>,
email: Option<String>,
quota: Option<f64>,
extra: Option<serde_json::Map<String, serde_json::Value>>,
) -> serde_json::Value {
let resolved_username = username.filter(|value| !value.trim().is_empty());
let resolved_display_name = display_name
.filter(|value| !value.trim().is_empty())
.or_else(|| resolved_username.clone());
let mut payload = serde_json::Map::new();
payload.insert(
"username".to_string(),
resolved_username
.map(serde_json::Value::String)
.unwrap_or(serde_json::Value::Null),
);
payload.insert(
"display_name".to_string(),
resolved_display_name
.map(serde_json::Value::String)
.unwrap_or(serde_json::Value::Null),
);
payload.insert(
"email".to_string(),
email
.map(serde_json::Value::String)
.unwrap_or(serde_json::Value::Null),
);
payload.insert(
"quota".to_string(),
quota
.and_then(serde_json::Number::from_f64)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
);
if let Some(extra) = extra.filter(|value| !value.is_empty()) {
payload.insert("extra".to_string(), serde_json::Value::Object(extra));
}
serde_json::Value::Object(payload)
}
pub(super) fn admin_provider_ops_value_as_f64(value: Option<&serde_json::Value>) -> Option<f64> {
match value {
Some(serde_json::Value::Number(number)) => number.as_f64(),
Some(serde_json::Value::String(raw)) => raw.trim().parse::<f64>().ok(),
_ => None,
}
}
fn admin_provider_ops_json_object(
value: &serde_json::Value,
) -> Option<&serde_json::Map<String, serde_json::Value>> {
value.as_object()
}
fn admin_provider_ops_frontend_updated_credentials(
credentials: serde_json::Map<String, serde_json::Value>,
) -> Option<serde_json::Map<String, serde_json::Value>> {
let filtered = credentials
.into_iter()
.filter(|(key, value)| {
!key.starts_with('_')
&& !matches!(value, serde_json::Value::Null)
&& !value.as_str().is_some_and(|raw| raw.trim().is_empty())
})
.collect::<serde_json::Map<String, serde_json::Value>>();
(!filtered.is_empty()).then_some(filtered)
}
fn admin_provider_ops_generic_verify_payload(
status: http::StatusCode,
response_json: &serde_json::Value,
) -> serde_json::Value {
if status == http::StatusCode::UNAUTHORIZED {
return admin_provider_ops_verify_failure("认证失败:无效的凭据");
}
if status == http::StatusCode::FORBIDDEN {
return admin_provider_ops_verify_failure("认证失败:权限不足");
}
if status != http::StatusCode::OK {
return admin_provider_ops_verify_failure(format!("验证失败:HTTP {}", status.as_u16()));
}
let user_data = if response_json
.get("success")
.and_then(serde_json::Value::as_bool)
== Some(true)
&& response_json
.get("data")
.is_some_and(serde_json::Value::is_object)
{
response_json.get("data")
} else if response_json
.get("success")
.and_then(serde_json::Value::as_bool)
== Some(false)
{
return admin_provider_ops_verify_failure(
response_json
.get("message")
.and_then(serde_json::Value::as_str)
.unwrap_or("验证失败"),
);
} else {
Some(response_json)
};
let Some(user_data) = user_data.and_then(admin_provider_ops_json_object) else {
return admin_provider_ops_verify_failure("响应格式无效");
};
let mut extra = serde_json::Map::new();
for (key, value) in user_data {
if matches!(
key.as_str(),
"username" | "display_name" | "email" | "quota" | "used_quota" | "request_count"
) {
continue;
}
extra.insert(key.clone(), value.clone());
}
admin_provider_ops_verify_success(
admin_provider_ops_verify_user_payload(
user_data
.get("username")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
user_data
.get("display_name")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
user_data
.get("email")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
admin_provider_ops_value_as_f64(user_data.get("quota")),
Some(extra),
),
None,
)
}
fn admin_provider_ops_cubence_verify_payload(
status: http::StatusCode,
response_json: &serde_json::Value,
) -> serde_json::Value {
if status == http::StatusCode::UNAUTHORIZED {
return admin_provider_ops_verify_failure("Cookie 已失效,请重新配置");
}
if status == http::StatusCode::FORBIDDEN {
return admin_provider_ops_verify_failure("Cookie 已失效或无权限");
}
if status != http::StatusCode::OK {
return admin_provider_ops_verify_failure(format!("验证失败:HTTP {}", status.as_u16()));
}
let Some(payload) = admin_provider_ops_json_object(response_json) else {
return admin_provider_ops_verify_failure("响应格式无效");
};
let user_info = payload
.get("user")
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
let balance_info = payload
.get("balance")
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
let mut extra = serde_json::Map::new();
if let Some(role) = user_info.get("role") {
extra.insert("role".to_string(), role.clone());
}
if let Some(invite_code) = user_info.get("invite_code") {
extra.insert("invite_code".to_string(), invite_code.clone());
}
admin_provider_ops_verify_success(
admin_provider_ops_verify_user_payload(
user_info
.get("username")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
user_info
.get("username")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
None,
admin_provider_ops_value_as_f64(balance_info.get("total_balance_dollar")),
Some(extra),
),
None,
)
}
fn admin_provider_ops_yescode_verify_payload(
status: http::StatusCode,
response_json: &serde_json::Value,
) -> serde_json::Value {
if status == http::StatusCode::UNAUTHORIZED {
return admin_provider_ops_verify_failure("Cookie 已失效,请重新配置");
}
if status == http::StatusCode::FORBIDDEN {
return admin_provider_ops_verify_failure("Cookie 已失效或无权限");
}
if status != http::StatusCode::OK {
return admin_provider_ops_verify_failure(format!("验证失败:HTTP {}", status.as_u16()));
}
let Some(payload) = admin_provider_ops_json_object(response_json) else {
return admin_provider_ops_verify_failure("响应格式无效");
};
let Some(username) = payload
.get("username")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned)
else {
return admin_provider_ops_verify_failure("响应格式无效");
};
let pay_as_you_go =
admin_provider_ops_value_as_f64(payload.get("pay_as_you_go_balance")).unwrap_or(0.0);
let subscription =
admin_provider_ops_value_as_f64(payload.get("subscription_balance")).unwrap_or(0.0);
let plan = payload
.get("subscription_plan")
.and_then(serde_json::Value::as_object)
.cloned()
.unwrap_or_default();
let weekly_limit = admin_provider_ops_value_as_f64(
payload
.get("weekly_limit")
.or_else(|| plan.get("weekly_limit")),
);
let weekly_spent = admin_provider_ops_value_as_f64(
payload
.get("weekly_spent_balance")
.or_else(|| payload.get("current_week_spend")),
)
.unwrap_or(0.0);
let subscription_available = weekly_limit
.map(|limit| (limit - weekly_spent).max(0.0).min(subscription))
.unwrap_or(subscription);
admin_provider_ops_verify_success(
admin_provider_ops_verify_user_payload(
Some(username.clone()),
Some(username),
payload
.get("email")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
Some(pay_as_you_go + subscription_available),
None,
),
None,
)
}
fn admin_provider_ops_nekocode_verify_payload(
status: http::StatusCode,
response_json: &serde_json::Value,
) -> serde_json::Value {
if status == http::StatusCode::UNAUTHORIZED {
return admin_provider_ops_verify_failure("Cookie 已失效,请重新配置");
}
if status == http::StatusCode::FORBIDDEN {
return admin_provider_ops_verify_failure("Cookie 已失效或无权限");
}
if status != http::StatusCode::OK {
return admin_provider_ops_verify_failure(format!("验证失败:HTTP {}", status.as_u16()));
}
let user_data = if response_json
.get("success")
.and_then(serde_json::Value::as_bool)
== Some(true)
&& response_json
.get("data")
.is_some_and(serde_json::Value::is_object)
{
response_json.get("data")
} else {
Some(response_json)
};
let Some(user_data) = user_data.and_then(admin_provider_ops_json_object) else {
return admin_provider_ops_verify_failure("响应格式无效");
};
admin_provider_ops_verify_success(
admin_provider_ops_verify_user_payload(
user_data
.get("username")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
user_data
.get("display_name")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
user_data
.get("email")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
admin_provider_ops_value_as_f64(user_data.get("balance")),
None,
),
None,
)
}
async fn admin_provider_ops_sub2api_exchange_token(
state: &AppState,
base_url: &str,
credentials: &serde_json::Map<String, serde_json::Value>,
) -> Result<(String, Option<serde_json::Map<String, serde_json::Value>>), String> {
let email = credentials
.get("email")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.unwrap_or_default();
let password = credentials
.get("password")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.unwrap_or_default();
let refresh_token = credentials
.get("refresh_token")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.unwrap_or_default();
let (path, body, default_error, previous_refresh_token) =
if !email.is_empty() && !password.is_empty() {
(
"/api/v1/auth/login",
json!({ "email": email, "password": password }),
"登录失败",
None,
)
} else if !refresh_token.is_empty() {
(
"/api/v1/auth/refresh",
json!({ "refresh_token": refresh_token }),
"Refresh Token 无效或已过期",
Some(refresh_token),
)
} else {
return Err("请填写账号密码或 Refresh Token".to_string());
};
let response = match state
.client
.post(format!("{}{path}", base_url.trim_end_matches('/')))
.json(&body)
.send()
.await
{
Ok(response) => response,
Err(err) if err.is_timeout() => return Err("连接超时".to_string()),
Err(err) if err.is_connect() => return Err(format!("连接失败: {err}")),
Err(err) => return Err(format!("验证失败: {err}")),
};
let status = response.status();
let response_json = match response.bytes().await {
Ok(bytes) => {
serde_json::from_slice::<serde_json::Value>(&bytes).unwrap_or_else(|_| json!({}))
}
Err(_) => json!({}),
};
let payload = response_json.as_object().cloned().unwrap_or_default();
if status != http::StatusCode::OK
|| payload
.get("code")
.and_then(serde_json::Value::as_i64)
.unwrap_or(-1)
!= 0
{
let message = payload
.get("message")
.and_then(serde_json::Value::as_str)
.unwrap_or(default_error);
return Err(message.to_string());
}
let Some(token_data) = payload.get("data").and_then(serde_json::Value::as_object) else {
return Err("响应格式无效".to_string());
};
let access_token = token_data
.get("access_token")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "响应格式无效".to_string())?;
let mut updated_credentials = serde_json::Map::new();
if let Some(new_refresh_token) = token_data
.get("refresh_token")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
if previous_refresh_token != Some(new_refresh_token) {
updated_credentials.insert(
"refresh_token".to_string(),
serde_json::Value::String(new_refresh_token.to_string()),
);
}
}
Ok((
access_token.to_string(),
admin_provider_ops_frontend_updated_credentials(updated_credentials),
))
}
fn admin_provider_ops_sub2api_verify_payload(
status: http::StatusCode,
response_json: &serde_json::Value,
updated_credentials: Option<serde_json::Map<String, serde_json::Value>>,
) -> serde_json::Value {
if status == http::StatusCode::UNAUTHORIZED {
return admin_provider_ops_verify_failure("认证失败:无效的凭据");
}
if status == http::StatusCode::FORBIDDEN {
return admin_provider_ops_verify_failure("认证失败:权限不足");
}
if status != http::StatusCode::OK {
return admin_provider_ops_verify_failure(format!("验证失败:HTTP {}", status.as_u16()));
}
let Some(payload) = admin_provider_ops_json_object(response_json) else {
return admin_provider_ops_verify_failure("响应格式无效");
};
if payload
.get("code")
.and_then(serde_json::Value::as_i64)
.unwrap_or(-1)
!= 0
{
return admin_provider_ops_verify_failure(
payload
.get("message")
.and_then(serde_json::Value::as_str)
.unwrap_or("验证失败"),
);
}
let Some(user_data) = payload.get("data").and_then(serde_json::Value::as_object) else {
return admin_provider_ops_verify_failure("响应格式无效");
};
let balance = admin_provider_ops_value_as_f64(user_data.get("balance")).unwrap_or(0.0);
let points = admin_provider_ops_value_as_f64(user_data.get("points")).unwrap_or(0.0);
let mut extra = serde_json::Map::new();
for key in ["balance", "points", "status", "concurrency"] {
if let Some(value) = user_data.get(key) {
extra.insert(key.to_string(), value.clone());
}
}
admin_provider_ops_verify_success(
admin_provider_ops_verify_user_payload(
user_data
.get("username")
.or_else(|| user_data.get("email"))
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
user_data
.get("username")
.or_else(|| user_data.get("email"))
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
user_data
.get("email")
.and_then(serde_json::Value::as_str)
.map(ToOwned::to_owned),
Some(balance + points),
Some(extra),
),
updated_credentials,
)
}
async fn admin_provider_ops_local_sub2api_verify_response(
state: &AppState,
base_url: &str,
credentials: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Value {
let base_url = base_url.trim().trim_end_matches('/');
if base_url.is_empty() {
return admin_provider_ops_verify_failure("请提供 API 地址");
}
let (access_token, updated_credentials) =
match admin_provider_ops_sub2api_exchange_token(state, base_url, credentials).await {
Ok(value) => value,
Err(message) => return admin_provider_ops_verify_failure(message),
};
let response = match state
.client
.get(format!("{base_url}/api/v1/auth/me?timezone=Asia/Shanghai"))
.bearer_auth(access_token)
.send()
.await
{
Ok(response) => response,
Err(err) if err.is_timeout() => return admin_provider_ops_verify_failure("连接超时"),
Err(err) if err.is_connect() => {
return admin_provider_ops_verify_failure(format!("连接失败: {err}"));
}
Err(err) => return admin_provider_ops_verify_failure(format!("验证失败: {err}")),
};
let status = response.status();
let response_json = match response.bytes().await {
Ok(bytes) => {
serde_json::from_slice::<serde_json::Value>(&bytes).unwrap_or_else(|_| json!({}))
}
Err(_) => json!({}),
};
admin_provider_ops_sub2api_verify_payload(status, &response_json, updated_credentials)
}
fn admin_provider_ops_insert_header(
headers: &mut reqwest::header::HeaderMap,
name: &str,
value: &str,
) -> Result<(), String> {
let header_name = reqwest::header::HeaderName::from_bytes(name.as_bytes())
.map_err(|_| format!("无效的请求头: {name}"))?;
let header_value = reqwest::header::HeaderValue::from_str(value)
.map_err(|_| format!("无效的请求头值: {name}"))?;
headers.insert(header_name, header_value);
Ok(())
}
pub(super) fn admin_provider_ops_verify_headers(
architecture_id: &str,
config: &serde_json::Map<String, serde_json::Value>,
credentials: &serde_json::Map<String, serde_json::Value>,
) -> Result<reqwest::header::HeaderMap, String> {
let mut headers = reqwest::header::HeaderMap::new();
match architecture_id {
"generic_api" => {
let api_key = credentials
.get("api_key")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.trim();
if !api_key.is_empty() {
let auth_method = config
.get("auth_method")
.and_then(serde_json::Value::as_str)
.unwrap_or("bearer");
if auth_method == "header" {
let header_name = config
.get("header_name")
.and_then(serde_json::Value::as_str)
.unwrap_or("X-API-Key");
admin_provider_ops_insert_header(&mut headers, header_name, api_key)?;
} else {
admin_provider_ops_insert_header(
&mut headers,
"Authorization",
&format!("Bearer {api_key}"),
)?;
}
}
}
"new_api" => {
for (name, value) in [
(
"User-Agent",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.249 Electron/38.7.0 Safari/537.36",
),
("Accept", "application/json"),
("Accept-Encoding", "gzip, deflate, br"),
("Accept-Language", "zh-CN"),
("sec-ch-ua", "\"Not=A?Brand\";v=\"24\", \"Chromium\";v=\"140\""),
("sec-ch-ua-mobile", "?0"),
("sec-ch-ua-platform", "\"macOS\""),
("Sec-Fetch-Site", "cross-site"),
("Sec-Fetch-Mode", "cors"),
("Sec-Fetch-Dest", "empty"),
] {
admin_provider_ops_insert_header(&mut headers, name, value)?;
}
if let Some(api_key) = credentials
.get("api_key")
.and_then(serde_json::Value::as_str)
{
if !api_key.trim().is_empty() {
admin_provider_ops_insert_header(
&mut headers,
"Authorization",
&format!("Bearer {}", api_key.trim()),
)?;
}
}
if let Some(user_id) = credentials
.get("user_id")
.and_then(serde_json::Value::as_str)
{
if !user_id.trim().is_empty() {
admin_provider_ops_insert_header(&mut headers, "New-Api-User", user_id.trim())?;
}
}
if let Some(cookie) = credentials
.get("cookie")
.and_then(serde_json::Value::as_str)
{
if !cookie.trim().is_empty() {
admin_provider_ops_insert_header(&mut headers, "Cookie", cookie.trim())?;
}
}
}
"cubence" => {
if let Some(token_cookie) = credentials
.get("token_cookie")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.trim().is_empty())
{
let token = admin_provider_ops_extract_cookie_value(token_cookie, "token");
admin_provider_ops_insert_header(
&mut headers,
"Cookie",
&format!("token={token}"),
)?;
}
}
"yescode" => {
if let Some(auth_cookie) = credentials
.get("auth_cookie")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.trim().is_empty())
{
admin_provider_ops_insert_header(
&mut headers,
"Cookie",
&admin_provider_ops_yescode_cookie_header(auth_cookie),
)?;
}
}
"nekocode" => {
if let Some(session_cookie) = credentials
.get("session_cookie")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.trim().is_empty())
{
let session = admin_provider_ops_extract_cookie_value(session_cookie, "session");
admin_provider_ops_insert_header(
&mut headers,
"Cookie",
&format!("session={session}"),
)?;
}
}
"anyrouter" => {
let mut cookies = Vec::new();
if let Some(acw_cookie) = config
.get("acw_cookie")
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
cookies.push(acw_cookie.to_string());
}
if let Some(session_cookie) = credentials
.get("session_cookie")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.trim().is_empty())
{
let session = admin_provider_ops_extract_cookie_value(session_cookie, "session");
cookies.push(format!("session={session}"));
if let Some(user_id) =
admin_provider_ops_anyrouter_parse_session_user_id(session_cookie)
{
admin_provider_ops_insert_header(&mut headers, "New-Api-User", user_id.trim())?;
}
}
if !cookies.is_empty() {
admin_provider_ops_insert_header(&mut headers, "Cookie", &cookies.join("; "))?;
}
}
_ => {}
}
Ok(headers)
}
pub(super) async fn admin_provider_ops_local_verify_response(
state: &AppState,
base_url: &str,
architecture_id: &str,
config: &serde_json::Map<String, serde_json::Value>,
credentials: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Value {
if architecture_id == "sub2api" {
return admin_provider_ops_local_sub2api_verify_response(state, base_url, credentials)
.await;
}
let mut resolved_config = config.clone();
if architecture_id == "anyrouter" {
if let Some(acw_cookie) = admin_provider_ops_anyrouter_acw_cookie(state, base_url).await {
resolved_config.insert(
"acw_cookie".to_string(),
serde_json::Value::String(acw_cookie),
);
}
}
let verify_path = match architecture_id {
"anyrouter" => "/api/user/self",
"cubence" => "/api/v1/dashboard/overview",
"yescode" => "/api/v1/auth/profile",
"nekocode" => "/api/user/self",
"new_api" | "generic_api" => "/api/user/self",
_ => return admin_provider_ops_verify_failure(ADMIN_PROVIDER_OPS_VERIFY_RUST_ONLY_MESSAGE),
};
let base_url = base_url.trim().trim_end_matches('/');
if base_url.is_empty() {
return admin_provider_ops_verify_failure("请提供 API 地址");
}
let headers =
match admin_provider_ops_verify_headers(architecture_id, &resolved_config, credentials) {
Ok(headers) => headers,
Err(message) => return admin_provider_ops_verify_failure(message),
};
let response = match state
.client
.get(format!("{base_url}{verify_path}"))
.headers(headers)
.send()
.await
{
Ok(response) => response,
Err(err) if err.is_timeout() => return admin_provider_ops_verify_failure("连接超时"),
Err(err) if err.is_connect() => {
return admin_provider_ops_verify_failure(format!("连接失败: {err}"));
}
Err(err) => return admin_provider_ops_verify_failure(format!("验证失败: {err}")),
};
let status = response.status();
let response_json = match response.bytes().await {
Ok(bytes) => {
serde_json::from_slice::<serde_json::Value>(&bytes).unwrap_or_else(|_| json!({}))
}
Err(_) => json!({}),
};
match architecture_id {
"cubence" => admin_provider_ops_cubence_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),
_ => admin_provider_ops_generic_verify_payload(status, &response_json),
}
}
@@ -0,0 +1,15 @@
use aether_admin::provider::verify as admin_provider_verify_pure;
use reqwest::header::HeaderMap;
use serde_json::Value;
pub(super) fn admin_provider_ops_verify_headers(
architecture_id: &str,
config: &serde_json::Map<String, Value>,
credentials: &serde_json::Map<String, Value>,
) -> Result<HeaderMap, String> {
admin_provider_verify_pure::admin_provider_ops_verify_headers(
architecture_id,
config,
credentials,
)
}
@@ -0,0 +1,192 @@
use crate::handlers::admin::request::AdminAppState;
use aether_admin::provider::verify as admin_provider_verify_pure;
use regex::Regex;
use serde_json::{json, Map, Value};
pub(super) fn admin_provider_ops_normalized_verify_architecture_id(architecture_id: &str) -> &str {
admin_provider_verify_pure::admin_provider_ops_normalized_verify_architecture_id(
architecture_id,
)
}
pub(super) fn admin_provider_ops_extract_cookie_value(cookie_input: &str, key: &str) -> String {
admin_provider_verify_pure::admin_provider_ops_extract_cookie_value(cookie_input, key)
}
pub(super) fn admin_provider_ops_yescode_cookie_header(cookie_input: &str) -> String {
admin_provider_verify_pure::admin_provider_ops_yescode_cookie_header(cookie_input)
}
pub(super) fn admin_provider_ops_anyrouter_compute_acw_sc_v2(arg1: &str) -> Option<String> {
admin_provider_verify_pure::admin_provider_ops_anyrouter_compute_acw_sc_v2(arg1)
}
pub(super) fn admin_provider_ops_anyrouter_parse_session_user_id(
cookie_input: &str,
) -> Option<String> {
admin_provider_verify_pure::admin_provider_ops_anyrouter_parse_session_user_id(cookie_input)
}
pub(super) async fn admin_provider_ops_anyrouter_acw_cookie(
state: &AdminAppState<'_>,
base_url: &str,
) -> Option<String> {
let response = state
.http_client()
.get(base_url.trim_end_matches('/'))
.header(
reqwest::header::USER_AGENT,
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
)
.send()
.await
.ok()?;
let body = response.text().await.ok()?;
let compiled = Regex::new(r"var\s+arg1\s*=\s*'([0-9a-fA-F]{40})'").ok()?;
let captures = compiled.captures(&body)?;
let arg1 = captures.get(1)?.as_str();
admin_provider_ops_anyrouter_compute_acw_sc_v2(arg1).map(|value| format!("acw_sc__v2={value}"))
}
pub(super) fn admin_provider_ops_verify_failure(message: impl Into<String>) -> Value {
admin_provider_verify_pure::admin_provider_ops_verify_failure(message)
}
pub(super) fn admin_provider_ops_verify_success(
data: Value,
updated_credentials: Option<Map<String, Value>>,
) -> Value {
admin_provider_verify_pure::admin_provider_ops_verify_success(data, updated_credentials)
}
pub(super) fn admin_provider_ops_verify_user_payload(
username: Option<String>,
display_name: Option<String>,
email: Option<String>,
quota: Option<f64>,
extra: Option<Map<String, Value>>,
) -> Value {
admin_provider_verify_pure::admin_provider_ops_verify_user_payload(
username,
display_name,
email,
quota,
extra,
)
}
pub(super) fn admin_provider_ops_value_as_f64(value: Option<&Value>) -> Option<f64> {
admin_provider_verify_pure::admin_provider_ops_value_as_f64(value)
}
pub(super) fn admin_provider_ops_json_object(
value: &Value,
) -> Option<&serde_json::Map<String, Value>> {
admin_provider_verify_pure::admin_provider_ops_json_object(value)
}
pub(super) fn admin_provider_ops_frontend_updated_credentials(
credentials: Map<String, Value>,
) -> Option<Map<String, Value>> {
admin_provider_verify_pure::admin_provider_ops_frontend_updated_credentials(credentials)
}
pub(super) async fn admin_provider_ops_sub2api_exchange_token(
state: &AdminAppState<'_>,
base_url: &str,
credentials: &Map<String, Value>,
) -> Result<(String, Option<Map<String, Value>>), String> {
let email = credentials
.get("email")
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or_default();
let password = credentials
.get("password")
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or_default();
let refresh_token = credentials
.get("refresh_token")
.and_then(Value::as_str)
.map(str::trim)
.unwrap_or_default();
let (path, body, default_error, previous_refresh_token) =
if !email.is_empty() && !password.is_empty() {
(
"/api/v1/auth/login",
json!({ "email": email, "password": password }),
"登录失败",
None,
)
} else if !refresh_token.is_empty() {
(
"/api/v1/auth/refresh",
json!({ "refresh_token": refresh_token }),
"Refresh Token 无效或已过期",
Some(refresh_token),
)
} else {
return Err("请填写账号密码或 Refresh Token".to_string());
};
let response = match state
.http_client()
.post(format!("{}{path}", base_url.trim_end_matches('/')))
.json(&body)
.send()
.await
{
Ok(response) => response,
Err(err) if err.is_timeout() => return Err("连接超时".to_string()),
Err(err) if err.is_connect() => return Err(format!("连接失败: {err}")),
Err(err) => return Err(format!("验证失败: {err}")),
};
let status = response.status();
let response_json = match response.bytes().await {
Ok(bytes) => serde_json::from_slice::<Value>(&bytes).unwrap_or_else(|_| json!({})),
Err(_) => json!({}),
};
let payload = response_json.as_object().cloned().unwrap_or_default();
if status != http::StatusCode::OK
|| payload.get("code").and_then(Value::as_i64).unwrap_or(-1) != 0
{
let message = payload
.get("message")
.and_then(Value::as_str)
.unwrap_or(default_error);
return Err(message.to_string());
}
let Some(token_data) = payload.get("data").and_then(Value::as_object) else {
return Err("响应格式无效".to_string());
};
let access_token = token_data
.get("access_token")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "响应格式无效".to_string())?;
let mut updated_credentials = Map::new();
if let Some(new_refresh_token) = token_data
.get("refresh_token")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
if previous_refresh_token != Some(new_refresh_token) {
updated_credentials.insert(
"refresh_token".to_string(),
Value::String(new_refresh_token.to_string()),
);
}
}
Ok((
access_token.to_string(),
admin_provider_ops_frontend_updated_credentials(updated_credentials),
))
}
@@ -0,0 +1,119 @@
mod headers;
mod helpers;
mod payload;
use crate::handlers::admin::provider::ops::providers::support::ADMIN_PROVIDER_OPS_VERIFY_RUST_ONLY_MESSAGE;
use crate::handlers::admin::request::AdminAppState;
use axum::{
body::Body,
http,
response::{IntoResponse, Response},
};
use serde_json::json;
pub(super) fn admin_provider_ops_verify_headers(
architecture_id: &str,
config: &serde_json::Map<String, serde_json::Value>,
credentials: &serde_json::Map<String, serde_json::Value>,
) -> Result<reqwest::header::HeaderMap, String> {
headers::admin_provider_ops_verify_headers(architecture_id, config, credentials)
}
pub(super) fn admin_provider_ops_normalized_verify_architecture_id(architecture_id: &str) -> &str {
helpers::admin_provider_ops_normalized_verify_architecture_id(architecture_id)
}
pub(super) fn admin_provider_ops_verify_failure(message: impl Into<String>) -> serde_json::Value {
helpers::admin_provider_ops_verify_failure(message)
}
pub(super) fn admin_provider_ops_value_as_f64(value: Option<&serde_json::Value>) -> Option<f64> {
helpers::admin_provider_ops_value_as_f64(value)
}
pub(super) async fn admin_provider_ops_local_verify_response(
state: &AdminAppState<'_>,
base_url: &str,
architecture_id: &str,
config: &serde_json::Map<String, serde_json::Value>,
credentials: &serde_json::Map<String, serde_json::Value>,
) -> serde_json::Value {
if architecture_id == "sub2api" {
return payload::admin_provider_ops_local_sub2api_verify_response(
state,
base_url,
credentials,
)
.await;
}
let mut resolved_config = config.clone();
if architecture_id == "anyrouter" {
if let Some(acw_cookie) =
helpers::admin_provider_ops_anyrouter_acw_cookie(state, base_url).await
{
resolved_config.insert(
"acw_cookie".to_string(),
serde_json::Value::String(acw_cookie),
);
}
}
let verify_path = match architecture_id {
"anyrouter" => "/api/user/self",
"cubence" => "/api/v1/dashboard/overview",
"yescode" => "/api/v1/auth/profile",
"nekocode" => "/api/user/self",
"new_api" | "generic_api" => "/api/user/self",
_ => {
return helpers::admin_provider_ops_verify_failure(
ADMIN_PROVIDER_OPS_VERIFY_RUST_ONLY_MESSAGE,
)
}
};
let base_url = base_url.trim().trim_end_matches('/');
if base_url.is_empty() {
return helpers::admin_provider_ops_verify_failure("请提供 API 地址");
}
let headers = match headers::admin_provider_ops_verify_headers(
architecture_id,
&resolved_config,
credentials,
) {
Ok(headers) => headers,
Err(message) => return helpers::admin_provider_ops_verify_failure(message),
};
let response = match state
.http_client()
.get(format!("{base_url}{verify_path}"))
.headers(headers)
.send()
.await
{
Ok(response) => response,
Err(err) if err.is_timeout() => {
return helpers::admin_provider_ops_verify_failure("连接超时")
}
Err(err) if err.is_connect() => {
return helpers::admin_provider_ops_verify_failure(format!("连接失败: {err}"))
}
Err(err) => return helpers::admin_provider_ops_verify_failure(format!("验证失败: {err}")),
};
let status = response.status();
let response_json = match response.bytes().await {
Ok(bytes) => {
serde_json::from_slice::<serde_json::Value>(&bytes).unwrap_or_else(|_| json!({}))
}
Err(_) => json!({}),
};
match architecture_id {
"cubence" => payload::admin_provider_ops_cubence_verify_payload(status, &response_json),
"yescode" => payload::admin_provider_ops_yescode_verify_payload(status, &response_json),
"nekocode" => payload::admin_provider_ops_nekocode_verify_payload(status, &response_json),
_ => payload::admin_provider_ops_generic_verify_payload(status, &response_json),
}
}
@@ -0,0 +1,89 @@
use super::helpers::{
admin_provider_ops_frontend_updated_credentials, admin_provider_ops_json_object,
admin_provider_ops_sub2api_exchange_token, admin_provider_ops_value_as_f64,
admin_provider_ops_verify_failure, admin_provider_ops_verify_success,
admin_provider_ops_verify_user_payload,
};
use crate::handlers::admin::request::AdminAppState;
use aether_admin::provider::verify as admin_provider_verify_pure;
use http;
use serde_json::{json, Map, Value};
pub(super) fn admin_provider_ops_generic_verify_payload(
status: http::StatusCode,
response_json: &Value,
) -> Value {
admin_provider_verify_pure::admin_provider_ops_generic_verify_payload(status, response_json)
}
pub(super) fn admin_provider_ops_cubence_verify_payload(
status: http::StatusCode,
response_json: &Value,
) -> Value {
admin_provider_verify_pure::admin_provider_ops_cubence_verify_payload(status, response_json)
}
pub(super) fn admin_provider_ops_yescode_verify_payload(
status: http::StatusCode,
response_json: &Value,
) -> Value {
admin_provider_verify_pure::admin_provider_ops_yescode_verify_payload(status, response_json)
}
pub(super) fn admin_provider_ops_nekocode_verify_payload(
status: http::StatusCode,
response_json: &Value,
) -> Value {
admin_provider_verify_pure::admin_provider_ops_nekocode_verify_payload(status, response_json)
}
pub(super) fn admin_provider_ops_sub2api_verify_payload(
status: http::StatusCode,
response_json: &Value,
updated_credentials: Option<Map<String, Value>>,
) -> Value {
admin_provider_verify_pure::admin_provider_ops_sub2api_verify_payload(
status,
response_json,
updated_credentials,
)
}
pub(super) async fn admin_provider_ops_local_sub2api_verify_response(
state: &AdminAppState<'_>,
base_url: &str,
credentials: &Map<String, Value>,
) -> Value {
let base_url = base_url.trim().trim_end_matches('/');
if base_url.is_empty() {
return admin_provider_ops_verify_failure("请提供 API 地址");
}
let (access_token, updated_credentials) =
match admin_provider_ops_sub2api_exchange_token(state, base_url, credentials).await {
Ok(value) => value,
Err(message) => return admin_provider_ops_verify_failure(message),
};
let response = match state
.http_client()
.get(format!("{base_url}/api/v1/auth/me?timezone=Asia/Shanghai"))
.bearer_auth(access_token)
.send()
.await
{
Ok(response) => response,
Err(err) if err.is_timeout() => return admin_provider_ops_verify_failure("连接超时"),
Err(err) if err.is_connect() => {
return admin_provider_ops_verify_failure(format!("连接失败: {err}"));
}
Err(err) => return admin_provider_ops_verify_failure(format!("验证失败: {err}")),
};
let status = response.status();
let response_json = match response.bytes().await {
Ok(bytes) => serde_json::from_slice::<Value>(&bytes).unwrap_or_else(|_| json!({})),
Err(_) => json!({}),
};
admin_provider_ops_sub2api_verify_payload(status, &response_json, updated_credentials)
}