mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
merge(pr287): absorb remaining rust branch changes
Absorb the remaining changes from PR #287 into aether-rust-pioneer after resolving conflicts locally and preserving the admin fixes already landed in this branch. Closes #287 Co-authored-by: AAEE86 <ppk0227@hotmail.com>
This commit is contained in:
@@ -6,4 +6,3 @@ pub mod ops;
|
||||
pub mod pool;
|
||||
pub mod quota;
|
||||
pub mod state;
|
||||
pub mod verify;
|
||||
|
||||
581
crates/aether-admin/src/provider/ops/actions.rs
Normal file
581
crates/aether-admin/src/provider/ops/actions.rs
Normal file
@@ -0,0 +1,581 @@
|
||||
use super::verify::admin_provider_ops_value_as_f64;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProviderOpsCheckinOutcome {
|
||||
pub success: Option<bool>,
|
||||
pub message: String,
|
||||
pub cookie_expired: bool,
|
||||
}
|
||||
|
||||
pub fn parse_query_balance_payload(
|
||||
architecture_id: &str,
|
||||
action_config: &Map<String, Value>,
|
||||
response_json: &Value,
|
||||
) -> Result<Value, String> {
|
||||
match architecture_id {
|
||||
"generic_api" | "new_api" | "anyrouter" => {
|
||||
parse_new_api_balance_payload(action_config, response_json)
|
||||
}
|
||||
"cubence" => parse_cubence_balance_payload(action_config, response_json),
|
||||
"nekocode" => parse_nekocode_balance_payload(response_json),
|
||||
_ => Err("Provider 操作仅支持 Rust execution runtime".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_yescode_combined_balance_payload(
|
||||
action_config: &Map<String, Value>,
|
||||
combined_data: &Map<String, Value>,
|
||||
) -> Value {
|
||||
let mut extra = yescode_balance_extra(combined_data);
|
||||
let total_available = admin_provider_ops_value_as_f64(extra.get("_total_available"));
|
||||
extra.remove("_subscription_available");
|
||||
extra.remove("_total_available");
|
||||
build_balance_data(
|
||||
None,
|
||||
None,
|
||||
total_available,
|
||||
action_config
|
||||
.get("currency")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("USD"),
|
||||
extra,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn parse_sub2api_balance_payload(
|
||||
action_config: &Map<String, Value>,
|
||||
me_json: &Value,
|
||||
subscription_json: Option<&Value>,
|
||||
) -> Result<Value, String> {
|
||||
let Some(me_payload) = me_json.as_object() else {
|
||||
return Err("响应格式无效".to_string());
|
||||
};
|
||||
if me_payload.get("code").and_then(Value::as_i64).unwrap_or(-1) != 0 {
|
||||
return Err(me_payload
|
||||
.get("message")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("查询用户信息失败")
|
||||
.to_string());
|
||||
}
|
||||
let Some(me_data) = me_payload.get("data").and_then(Value::as_object) else {
|
||||
return Err("响应格式无效".to_string());
|
||||
};
|
||||
|
||||
let balance = value_as_f64(me_data.get("balance")).unwrap_or(0.0);
|
||||
let points = value_as_f64(me_data.get("points")).unwrap_or(0.0);
|
||||
let mut extra = Map::new();
|
||||
extra.insert("balance".to_string(), json!(balance));
|
||||
extra.insert("points".to_string(), json!(points));
|
||||
|
||||
if let Some(subscription_json) = subscription_json {
|
||||
if let Some(subscription_payload) = subscription_json.as_object() {
|
||||
if subscription_payload
|
||||
.get("code")
|
||||
.and_then(Value::as_i64)
|
||||
.unwrap_or(-1)
|
||||
== 0
|
||||
{
|
||||
if let Some(summary) = subscription_payload.get("data").and_then(Value::as_object) {
|
||||
if let Some(active_count) = summary.get("active_count") {
|
||||
extra.insert("active_subscriptions".to_string(), active_count.clone());
|
||||
}
|
||||
if let Some(total_used_usd) = summary.get("total_used_usd") {
|
||||
extra.insert("total_used_usd".to_string(), total_used_usd.clone());
|
||||
}
|
||||
if let Some(subscriptions) =
|
||||
summary.get("subscriptions").and_then(Value::as_array)
|
||||
{
|
||||
extra.insert(
|
||||
"subscriptions".to_string(),
|
||||
Value::Array(
|
||||
subscriptions
|
||||
.iter()
|
||||
.filter_map(parse_subscription)
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(build_balance_data(
|
||||
None,
|
||||
None,
|
||||
Some(balance + points),
|
||||
action_config
|
||||
.get("currency")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("USD"),
|
||||
extra,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn attach_balance_checkin_outcome(
|
||||
action_payload: &mut Value,
|
||||
outcome: &ProviderOpsCheckinOutcome,
|
||||
) {
|
||||
if let Some(data) = action_payload
|
||||
.get_mut("data")
|
||||
.and_then(Value::as_object_mut)
|
||||
{
|
||||
let extra = data
|
||||
.entry("extra".to_string())
|
||||
.or_insert_with(|| Value::Object(Map::new()));
|
||||
if let Some(extra) = extra.as_object_mut() {
|
||||
if outcome.cookie_expired {
|
||||
extra.insert("cookie_expired".to_string(), Value::Bool(true));
|
||||
extra.insert(
|
||||
"cookie_expired_message".to_string(),
|
||||
Value::String(outcome.message.clone()),
|
||||
);
|
||||
} else {
|
||||
extra.insert(
|
||||
"checkin_success".to_string(),
|
||||
outcome.success.map(Value::Bool).unwrap_or(Value::Null),
|
||||
);
|
||||
extra.insert(
|
||||
"checkin_message".to_string(),
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_balance_data(
|
||||
total_granted: Option<f64>,
|
||||
total_used: Option<f64>,
|
||||
total_available: Option<f64>,
|
||||
currency: &str,
|
||||
extra: Map<String, Value>,
|
||||
) -> Value {
|
||||
json!({
|
||||
"total_granted": total_granted,
|
||||
"total_used": total_used,
|
||||
"total_available": total_available,
|
||||
"expires_at": Value::Null,
|
||||
"currency": currency,
|
||||
"extra": extra,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_new_api_balance_payload(
|
||||
action_config: &Map<String, Value>,
|
||||
response_json: &Value,
|
||||
) -> Result<Value, String> {
|
||||
let user_data = if response_json.get("success").and_then(Value::as_bool) == Some(true)
|
||||
&& response_json.get("data").is_some_and(Value::is_object)
|
||||
{
|
||||
response_json.get("data")
|
||||
} else if response_json.get("success").and_then(Value::as_bool) == Some(false) {
|
||||
return Err(response_json
|
||||
.get("message")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("业务状态码表示失败")
|
||||
.to_string());
|
||||
} else {
|
||||
Some(response_json)
|
||||
};
|
||||
let Some(user_data) = user_data.and_then(Value::as_object) else {
|
||||
return Err("响应格式无效".to_string());
|
||||
};
|
||||
let quota_divisor = 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(build_balance_data(
|
||||
None,
|
||||
total_used,
|
||||
total_available,
|
||||
action_config
|
||||
.get("currency")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("USD"),
|
||||
Map::new(),
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_cubence_balance_payload(
|
||||
action_config: &Map<String, Value>,
|
||||
response_json: &Value,
|
||||
) -> Result<Value, String> {
|
||||
let response_data = if response_json.get("success").and_then(Value::as_bool) == Some(true)
|
||||
&& response_json.get("data").is_some_and(Value::is_object)
|
||||
{
|
||||
response_json.get("data")
|
||||
} else if response_json.get("success").and_then(Value::as_bool) == Some(false) {
|
||||
return Err(response_json
|
||||
.get("message")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("查询余额失败")
|
||||
.to_string());
|
||||
} else {
|
||||
Some(response_json)
|
||||
};
|
||||
let response_data = response_data
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| "响应格式无效".to_string())?;
|
||||
let balance_data = response_data
|
||||
.get("balance")
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let subscription_limits = response_data
|
||||
.get("subscription_limits")
|
||||
.and_then(Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut extra = Map::new();
|
||||
if let Some(five_hour) = subscription_limits
|
||||
.get("five_hour")
|
||||
.and_then(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(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"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
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(build_balance_data(
|
||||
None,
|
||||
None,
|
||||
admin_provider_ops_value_as_f64(balance_data.get("total_balance_dollar")),
|
||||
action_config
|
||||
.get("currency")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("USD"),
|
||||
extra,
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_nekocode_balance_payload(response_json: &Value) -> Result<Value, String> {
|
||||
let response_data = response_json
|
||||
.get("data")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| "响应格式无效".to_string())?;
|
||||
let subscription = response_data
|
||||
.get("subscription")
|
||||
.and_then(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 = 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(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(Value::as_object) {
|
||||
if let Some(stats) = today_data.get("stats") {
|
||||
extra.insert("today_stats".to_string(), stats.clone());
|
||||
}
|
||||
}
|
||||
Ok(build_balance_data(
|
||||
daily_quota_limit,
|
||||
daily_used,
|
||||
balance,
|
||||
"USD",
|
||||
extra,
|
||||
))
|
||||
}
|
||||
|
||||
fn yescode_balance_extra(combined_data: &Map<String, Value>) -> Map<String, 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(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 = 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) = 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) =
|
||||
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
|
||||
}
|
||||
|
||||
fn quota_divisor(action_config: &Map<String, Value>) -> f64 {
|
||||
admin_provider_ops_value_as_f64(action_config.get("quota_divisor"))
|
||||
.filter(|value| *value > 0.0)
|
||||
.unwrap_or(500000.0)
|
||||
}
|
||||
|
||||
fn parse_rfc3339_unix_secs(value: Option<&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())
|
||||
}
|
||||
|
||||
fn value_as_f64(value: Option<&Value>) -> Option<f64> {
|
||||
match value {
|
||||
Some(Value::Number(number)) => number.as_f64(),
|
||||
Some(Value::String(raw)) => raw.trim().parse::<f64>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_subscription(value: &Value) -> Option<Value> {
|
||||
let item = value.as_object()?;
|
||||
let mut subscription = Map::new();
|
||||
subscription.insert(
|
||||
"group_name".to_string(),
|
||||
item.get("group_name")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::String(String::new())),
|
||||
);
|
||||
subscription.insert(
|
||||
"status".to_string(),
|
||||
item.get("status")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Value::String(String::new())),
|
||||
);
|
||||
for field in [
|
||||
"daily_used_usd",
|
||||
"daily_limit_usd",
|
||||
"weekly_used_usd",
|
||||
"weekly_limit_usd",
|
||||
"monthly_used_usd",
|
||||
"monthly_limit_usd",
|
||||
"expires_at",
|
||||
] {
|
||||
if let Some(value) = item.get(field).filter(|value| !value.is_null()) {
|
||||
subscription.insert(field.to_string(), value.clone());
|
||||
}
|
||||
}
|
||||
Some(Value::Object(subscription))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
attach_balance_checkin_outcome, parse_query_balance_payload, parse_sub2api_balance_payload,
|
||||
ProviderOpsCheckinOutcome,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn anyrouter_single_request_parser_uses_usage_fields() {
|
||||
let payload = parse_query_balance_payload(
|
||||
"anyrouter",
|
||||
&json!({ "quota_divisor": 500000 })
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("config"),
|
||||
&json!({
|
||||
"quota": 2500000,
|
||||
"used_quota": 500000
|
||||
}),
|
||||
)
|
||||
.expect("payload should parse");
|
||||
|
||||
assert_eq!(payload["total_available"], json!(5.0));
|
||||
assert_eq!(payload["total_used"], json!(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub2api_parser_sums_balance_and_points() {
|
||||
let payload = parse_sub2api_balance_payload(
|
||||
&json!({ "currency": "USD" })
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("config"),
|
||||
&json!({
|
||||
"code": 0,
|
||||
"data": {
|
||||
"balance": 8.5,
|
||||
"points": 1.5
|
||||
}
|
||||
}),
|
||||
Some(&json!({
|
||||
"code": 0,
|
||||
"data": {
|
||||
"active_count": 2,
|
||||
"subscriptions": []
|
||||
}
|
||||
})),
|
||||
)
|
||||
.expect("payload should parse");
|
||||
|
||||
assert_eq!(payload["total_available"], json!(10.0));
|
||||
assert_eq!(payload["extra"]["active_subscriptions"], json!(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cubence_parser_reads_wrapped_dashboard_overview() {
|
||||
let payload = parse_query_balance_payload(
|
||||
"cubence",
|
||||
&json!({ "currency": "USD" })
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("config"),
|
||||
&json!({
|
||||
"success": true,
|
||||
"data": {
|
||||
"balance": {
|
||||
"normal_balance_dollar": 0.6,
|
||||
"subscription_balance_dollar": 0.0,
|
||||
"charity_balance_dollar": 0.0,
|
||||
"total_balance_dollar": 0.6
|
||||
},
|
||||
"subscription_limits": {
|
||||
"five_hour": {
|
||||
"limit": 10,
|
||||
"used": 1,
|
||||
"remaining": 9,
|
||||
"resets_at": 123
|
||||
},
|
||||
"weekly": {
|
||||
"limit": 20,
|
||||
"used": 2,
|
||||
"remaining": 18,
|
||||
"resets_at": 456
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.expect("payload should parse");
|
||||
|
||||
assert_eq!(payload["total_available"], json!(0.6));
|
||||
assert_eq!(payload["extra"]["normal_balance"], json!(0.6));
|
||||
assert_eq!(payload["extra"]["five_hour_limit"]["remaining"], json!(9));
|
||||
assert_eq!(payload["extra"]["weekly_limit"]["remaining"], json!(18));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attach_balance_checkin_outcome_marks_auth_expired() {
|
||||
let mut payload = json!({
|
||||
"status": "success",
|
||||
"data": { "extra": {} }
|
||||
});
|
||||
attach_balance_checkin_outcome(
|
||||
&mut payload,
|
||||
&ProviderOpsCheckinOutcome {
|
||||
success: None,
|
||||
message: "Cookie 已失效".to_string(),
|
||||
cookie_expired: true,
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(payload["status"], json!("auth_expired"));
|
||||
assert_eq!(payload["data"]["extra"]["cookie_expired"], json!(true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
use super::{
|
||||
json_object, ProviderOpsActionSpec, ProviderOpsArchitectureSpec, ProviderOpsAuthSpec,
|
||||
ProviderOpsBalanceMode, ProviderOpsCheckinMode, ProviderOpsVerifyMode,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
pub(super) fn spec() -> ProviderOpsArchitectureSpec {
|
||||
let credentials_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"type": "string",
|
||||
"title": "站点地址",
|
||||
"description": "API 基础地址",
|
||||
"x-default-value": "https://anyrouter.top"
|
||||
},
|
||||
"session_cookie": {
|
||||
"type": "string",
|
||||
"title": "Session Cookie",
|
||||
"description": "从浏览器复制的 session Cookie 值",
|
||||
"x-sensitive": true,
|
||||
"x-input-type": "password"
|
||||
}
|
||||
},
|
||||
"required": ["session_cookie"],
|
||||
"x-auth-type": "cookie",
|
||||
"x-currency": "USD",
|
||||
"x-default-base-url": "https://anyrouter.top",
|
||||
"x-field-groups": [
|
||||
{ "fields": ["base_url"] },
|
||||
{ "fields": ["session_cookie"] }
|
||||
],
|
||||
"x-quota-divisor": 500000,
|
||||
"x-validation": [
|
||||
{
|
||||
"type": "required",
|
||||
"fields": ["session_cookie"],
|
||||
"message": "请填写 Session Cookie"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
ProviderOpsArchitectureSpec {
|
||||
architecture_id: "anyrouter",
|
||||
display_name: "Anyrouter",
|
||||
description: "Anyrouter 中转站预设配置,使用 Cookie 认证",
|
||||
hidden: false,
|
||||
credentials_schema: credentials_schema.clone(),
|
||||
verify_endpoint: "/api/user/self",
|
||||
verify_mode: ProviderOpsVerifyMode::DirectGet,
|
||||
balance_mode: ProviderOpsBalanceMode::SingleRequest,
|
||||
checkin_mode: ProviderOpsCheckinMode::NewApiCompatible,
|
||||
query_balance_cookie_auth_errors: false,
|
||||
supported_auth_types: vec![ProviderOpsAuthSpec {
|
||||
auth_type: "cookie",
|
||||
display_name: "Anyrouter Cookie",
|
||||
credentials_schema,
|
||||
}],
|
||||
supported_actions: vec![ProviderOpsActionSpec {
|
||||
action_type: "query_balance",
|
||||
display_name: "查询余额(含自动签到)",
|
||||
description: "查询账户余额,同时自动签到",
|
||||
config_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "货币单位",
|
||||
"default": "USD"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}),
|
||||
}],
|
||||
default_connector: Some("cookie"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn default_action_config(action_type: &str) -> Option<Map<String, Value>> {
|
||||
match action_type {
|
||||
"query_balance" => Some(json_object(json!({
|
||||
"endpoint": "/api/user/self",
|
||||
"method": "GET",
|
||||
"quota_divisor": 500000,
|
||||
"checkin_endpoint": "/api/user/sign_in",
|
||||
"currency": "USD"
|
||||
}))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
102
crates/aether-admin/src/provider/ops/architectures/cubence.rs
Normal file
102
crates/aether-admin/src/provider/ops/architectures/cubence.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use super::{
|
||||
json_object, ProviderOpsActionSpec, ProviderOpsArchitectureSpec, ProviderOpsAuthSpec,
|
||||
ProviderOpsBalanceMode, ProviderOpsCheckinMode, ProviderOpsVerifyMode,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
pub(super) fn spec() -> ProviderOpsArchitectureSpec {
|
||||
let credentials_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"type": "string",
|
||||
"title": "站点地址",
|
||||
"description": "API 基础地址",
|
||||
"x-default-value": "https://cubence.com"
|
||||
},
|
||||
"token_cookie": {
|
||||
"type": "string",
|
||||
"title": "Cookie",
|
||||
"description": "支持粘贴完整 Cookie Header,至少包含 token;若站点启用 Cloudflare,请一并包含 cf_clearance。也兼容仅填写 token 值",
|
||||
"x-sensitive": true,
|
||||
"x-input-type": "password"
|
||||
}
|
||||
},
|
||||
"required": ["token_cookie"],
|
||||
"x-auth-type": "cookie",
|
||||
"x-balance-extra-format": [
|
||||
{
|
||||
"label": "5h",
|
||||
"source": "five_hour_limit",
|
||||
"type": "window_limit",
|
||||
"unit_divisor": 1000000
|
||||
},
|
||||
{
|
||||
"label": "周",
|
||||
"source": "weekly_limit",
|
||||
"type": "window_limit",
|
||||
"unit_divisor": 1000000
|
||||
}
|
||||
],
|
||||
"x-currency": "USD",
|
||||
"x-default-base-url": "https://cubence.com",
|
||||
"x-field-groups": [
|
||||
{ "fields": ["base_url"] },
|
||||
{ "fields": ["token_cookie"] }
|
||||
],
|
||||
"x-quota-divisor": null,
|
||||
"x-validation": [
|
||||
{
|
||||
"type": "required",
|
||||
"fields": ["token_cookie"],
|
||||
"message": "请填写 Cubence Cookie"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
ProviderOpsArchitectureSpec {
|
||||
architecture_id: "cubence",
|
||||
display_name: "Cubence",
|
||||
description: "Cubence 中转站预设配置,使用 Cookie 认证",
|
||||
hidden: false,
|
||||
credentials_schema: credentials_schema.clone(),
|
||||
verify_endpoint: "/api/v1/dashboard/overview",
|
||||
verify_mode: ProviderOpsVerifyMode::DirectGet,
|
||||
balance_mode: ProviderOpsBalanceMode::SingleRequest,
|
||||
checkin_mode: ProviderOpsCheckinMode::None,
|
||||
query_balance_cookie_auth_errors: true,
|
||||
supported_auth_types: vec![ProviderOpsAuthSpec {
|
||||
auth_type: "cookie",
|
||||
display_name: "Cubence Cookie",
|
||||
credentials_schema,
|
||||
}],
|
||||
supported_actions: vec![ProviderOpsActionSpec {
|
||||
action_type: "query_balance",
|
||||
display_name: "查询余额(含窗口限额)",
|
||||
description: "查询账户余额和窗口限额信息",
|
||||
config_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "货币单位",
|
||||
"default": "USD"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}),
|
||||
}],
|
||||
default_connector: Some("cookie"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn default_action_config(action_type: &str) -> Option<Map<String, Value>> {
|
||||
match action_type {
|
||||
"query_balance" => Some(json_object(json!({
|
||||
"endpoint": "/api/v1/dashboard/overview",
|
||||
"method": "GET",
|
||||
"currency": "USD"
|
||||
}))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
use super::{
|
||||
json_object, ProviderOpsActionSpec, ProviderOpsArchitectureSpec, ProviderOpsAuthSpec,
|
||||
ProviderOpsBalanceMode, ProviderOpsCheckinMode, ProviderOpsVerifyMode,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
pub(super) fn spec() -> ProviderOpsArchitectureSpec {
|
||||
let credentials_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api_key": {
|
||||
"type": "string",
|
||||
"title": "API Key",
|
||||
"description": "提供商的 API Key",
|
||||
"x-sensitive": true,
|
||||
"x-input-type": "password"
|
||||
},
|
||||
"base_url": {
|
||||
"type": "string",
|
||||
"title": "站点地址",
|
||||
"description": "API 基础地址"
|
||||
}
|
||||
},
|
||||
"required": ["api_key"],
|
||||
"x-auth-method": "bearer",
|
||||
"x-auth-type": "api_key",
|
||||
"x-currency": "USD",
|
||||
"x-field-groups": [
|
||||
{ "fields": ["base_url"] },
|
||||
{ "fields": ["api_key"] }
|
||||
],
|
||||
"x-quota-divisor": 500000,
|
||||
"x-validation": [
|
||||
{
|
||||
"type": "required",
|
||||
"fields": ["api_key"],
|
||||
"message": "请填写 API Key"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
ProviderOpsArchitectureSpec {
|
||||
architecture_id: "generic_api",
|
||||
display_name: "通用 API",
|
||||
description: "可配置的通用 API 架构,适用于各种中转站",
|
||||
hidden: true,
|
||||
credentials_schema: credentials_schema.clone(),
|
||||
verify_endpoint: "/api/user/self",
|
||||
verify_mode: ProviderOpsVerifyMode::DirectGet,
|
||||
balance_mode: ProviderOpsBalanceMode::SingleRequest,
|
||||
checkin_mode: ProviderOpsCheckinMode::NewApiCompatible,
|
||||
query_balance_cookie_auth_errors: false,
|
||||
supported_auth_types: vec![ProviderOpsAuthSpec {
|
||||
auth_type: "api_key",
|
||||
display_name: "API Key",
|
||||
credentials_schema,
|
||||
}],
|
||||
supported_actions: vec![ProviderOpsActionSpec {
|
||||
action_type: "query_balance",
|
||||
display_name: "查询余额",
|
||||
description: "查询 New API 账户余额信息",
|
||||
config_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"endpoint": {
|
||||
"type": "string",
|
||||
"title": "API 路径",
|
||||
"description": "余额查询 API 路径",
|
||||
"default": "/api/user/self"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"title": "请求方法",
|
||||
"enum": ["GET", "POST"],
|
||||
"default": "GET"
|
||||
},
|
||||
"quota_divisor": {
|
||||
"type": "number",
|
||||
"title": "额度除数",
|
||||
"description": "将原始额度值转换为美元的除数",
|
||||
"default": 500000
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "货币单位",
|
||||
"default": "USD"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}),
|
||||
}],
|
||||
default_connector: Some("api_key"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn default_action_config(action_type: &str) -> Option<Map<String, Value>> {
|
||||
match action_type {
|
||||
"query_balance" => Some(json_object(json!({
|
||||
"endpoint": "/api/user/balance",
|
||||
"method": "GET"
|
||||
}))),
|
||||
"checkin" => Some(json_object(json!({
|
||||
"endpoint": "/api/user/checkin",
|
||||
"method": "POST"
|
||||
}))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
262
crates/aether-admin/src/provider/ops/architectures/mod.rs
Normal file
262
crates/aether-admin/src/provider/ops/architectures/mod.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
mod anyrouter;
|
||||
mod cubence;
|
||||
mod generic_api;
|
||||
mod nekocode;
|
||||
mod new_api;
|
||||
mod sub2api;
|
||||
mod yescode;
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ProviderOpsVerifyMode {
|
||||
DirectGet,
|
||||
Sub2ApiExchange,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ProviderOpsBalanceMode {
|
||||
SingleRequest,
|
||||
YescodeCombined,
|
||||
Sub2ApiDualRequest,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ProviderOpsCheckinMode {
|
||||
None,
|
||||
NewApiCompatible,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ProviderOpsAuthSpec {
|
||||
pub auth_type: &'static str,
|
||||
pub display_name: &'static str,
|
||||
pub credentials_schema: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ProviderOpsActionSpec {
|
||||
pub action_type: &'static str,
|
||||
pub display_name: &'static str,
|
||||
pub description: &'static str,
|
||||
pub config_schema: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ProviderOpsArchitectureSpec {
|
||||
pub architecture_id: &'static str,
|
||||
pub display_name: &'static str,
|
||||
pub description: &'static str,
|
||||
pub hidden: bool,
|
||||
pub credentials_schema: Value,
|
||||
pub verify_endpoint: &'static str,
|
||||
pub verify_mode: ProviderOpsVerifyMode,
|
||||
pub balance_mode: ProviderOpsBalanceMode,
|
||||
pub checkin_mode: ProviderOpsCheckinMode,
|
||||
pub query_balance_cookie_auth_errors: bool,
|
||||
pub supported_auth_types: Vec<ProviderOpsAuthSpec>,
|
||||
pub supported_actions: Vec<ProviderOpsActionSpec>,
|
||||
pub default_connector: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl ProviderOpsArchitectureSpec {
|
||||
pub fn api_payload(&self) -> Value {
|
||||
json!({
|
||||
"architecture_id": self.architecture_id,
|
||||
"display_name": self.display_name,
|
||||
"description": self.description,
|
||||
"credentials_schema": self.credentials_schema,
|
||||
"supported_auth_types": self.supported_auth_types.iter().map(|item| {
|
||||
json!({
|
||||
"type": item.auth_type,
|
||||
"display_name": item.display_name,
|
||||
"credentials_schema": item.credentials_schema,
|
||||
})
|
||||
}).collect::<Vec<_>>(),
|
||||
"supported_actions": self.supported_actions.iter().map(|item| {
|
||||
json!({
|
||||
"type": item.action_type,
|
||||
"display_name": item.display_name,
|
||||
"description": item.description,
|
||||
"config_schema": item.config_schema,
|
||||
})
|
||||
}).collect::<Vec<_>>(),
|
||||
"default_connector": self.default_connector,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static PROVIDER_OPS_ARCHITECTURES: LazyLock<Vec<ProviderOpsArchitectureSpec>> =
|
||||
LazyLock::new(|| {
|
||||
vec![
|
||||
anyrouter::spec(),
|
||||
cubence::spec(),
|
||||
generic_api::spec(),
|
||||
nekocode::spec(),
|
||||
new_api::spec(),
|
||||
sub2api::spec(),
|
||||
yescode::spec(),
|
||||
]
|
||||
});
|
||||
|
||||
pub fn list_architectures(include_hidden: bool) -> Vec<ProviderOpsArchitectureSpec> {
|
||||
PROVIDER_OPS_ARCHITECTURES
|
||||
.iter()
|
||||
.filter(|spec| include_hidden || !spec.hidden)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_architecture(architecture_id: &str) -> Option<ProviderOpsArchitectureSpec> {
|
||||
let normalized = normalize_architecture_id(architecture_id);
|
||||
PROVIDER_OPS_ARCHITECTURES
|
||||
.iter()
|
||||
.find(|spec| spec.architecture_id == normalized)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn normalize_architecture_id(architecture_id: &str) -> &'static str {
|
||||
match architecture_id.trim() {
|
||||
"" => "generic_api",
|
||||
"generic_api" => "generic_api",
|
||||
"new_api" => "new_api",
|
||||
"cubence" => "cubence",
|
||||
"yescode" => "yescode",
|
||||
"nekocode" => "nekocode",
|
||||
"anyrouter" => "anyrouter",
|
||||
"sub2api" => "sub2api",
|
||||
_ => "generic_api",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn admin_provider_ops_is_supported_auth_type(auth_type: &str) -> bool {
|
||||
matches!(
|
||||
auth_type,
|
||||
"api_key" | "session_login" | "oauth" | "cookie" | "none"
|
||||
)
|
||||
}
|
||||
|
||||
pub fn resolve_action_config(
|
||||
architecture_id: &str,
|
||||
provider_ops_config: &Map<String, Value>,
|
||||
action_type: &str,
|
||||
request_override: Option<&Map<String, Value>>,
|
||||
) -> Option<Map<String, Value>> {
|
||||
let mut resolved =
|
||||
default_action_config(normalize_architecture_id(architecture_id), action_type)?;
|
||||
|
||||
if let Some(saved) = provider_action_config_object(provider_ops_config, action_type) {
|
||||
for (key, value) in saved {
|
||||
resolved.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(request_override) = request_override {
|
||||
for (key, value) in request_override {
|
||||
resolved.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Some(resolved)
|
||||
}
|
||||
|
||||
fn provider_action_config_object<'a>(
|
||||
provider_ops_config: &'a Map<String, Value>,
|
||||
action_type: &str,
|
||||
) -> Option<&'a Map<String, Value>> {
|
||||
provider_ops_config
|
||||
.get("actions")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|actions| actions.get(action_type))
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|action| action.get("config"))
|
||||
.and_then(Value::as_object)
|
||||
}
|
||||
|
||||
fn default_action_config(architecture_id: &str, action_type: &str) -> Option<Map<String, Value>> {
|
||||
match architecture_id {
|
||||
"anyrouter" => anyrouter::default_action_config(action_type),
|
||||
"cubence" => cubence::default_action_config(action_type),
|
||||
"generic_api" => generic_api::default_action_config(action_type),
|
||||
"nekocode" => nekocode::default_action_config(action_type),
|
||||
"new_api" => new_api::default_action_config(action_type),
|
||||
"sub2api" => sub2api::default_action_config(action_type),
|
||||
"yescode" => yescode::default_action_config(action_type),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn json_object(value: Value) -> Map<String, Value> {
|
||||
value.as_object().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
get_architecture, list_architectures, normalize_architecture_id, resolve_action_config,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn list_architectures_hides_generic_api_by_default() {
|
||||
let visible = list_architectures(false);
|
||||
assert_eq!(visible.len(), 6);
|
||||
assert!(visible
|
||||
.iter()
|
||||
.all(|item| item.architecture_id != "generic_api"));
|
||||
|
||||
let all = list_architectures(true);
|
||||
assert_eq!(all.len(), 7);
|
||||
assert!(all.iter().any(|item| item.architecture_id == "generic_api"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_architecture_id_falls_back_to_generic_api() {
|
||||
assert_eq!(normalize_architecture_id(""), "generic_api");
|
||||
assert_eq!(normalize_architecture_id("new_api"), "new_api");
|
||||
assert_eq!(normalize_architecture_id("unknown"), "generic_api");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_architecture_returns_generic_api_for_unknown_id() {
|
||||
let architecture = get_architecture("unknown").expect("architecture should exist");
|
||||
assert_eq!(architecture.architecture_id, "generic_api");
|
||||
assert!(architecture.hidden);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_action_config_merges_default_saved_and_request_values() {
|
||||
let resolved = resolve_action_config(
|
||||
"new_api",
|
||||
&json!({
|
||||
"actions": {
|
||||
"query_balance": {
|
||||
"config": {
|
||||
"endpoint": "/custom/path",
|
||||
"currency": "CNY"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("config should be object"),
|
||||
"query_balance",
|
||||
Some(
|
||||
&json!({
|
||||
"quota_divisor": 42
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("override should be object"),
|
||||
),
|
||||
)
|
||||
.expect("action config should resolve");
|
||||
|
||||
assert_eq!(resolved.get("endpoint"), Some(&json!("/custom/path")));
|
||||
assert_eq!(resolved.get("currency"), Some(&json!("CNY")));
|
||||
assert_eq!(resolved.get("quota_divisor"), Some(&json!(42)));
|
||||
assert_eq!(resolved.get("method"), Some(&json!("GET")));
|
||||
}
|
||||
}
|
||||
102
crates/aether-admin/src/provider/ops/architectures/nekocode.rs
Normal file
102
crates/aether-admin/src/provider/ops/architectures/nekocode.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use super::{
|
||||
json_object, ProviderOpsActionSpec, ProviderOpsArchitectureSpec, ProviderOpsAuthSpec,
|
||||
ProviderOpsBalanceMode, ProviderOpsCheckinMode, ProviderOpsVerifyMode,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
pub(super) fn spec() -> ProviderOpsArchitectureSpec {
|
||||
let credentials_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"type": "string",
|
||||
"title": "站点地址",
|
||||
"description": "API 基础地址",
|
||||
"x-default-value": "https://nekocode.ai"
|
||||
},
|
||||
"session_cookie": {
|
||||
"type": "string",
|
||||
"title": "Session Cookie",
|
||||
"description": "从浏览器复制的 session Cookie 值",
|
||||
"x-sensitive": true,
|
||||
"x-input-type": "password"
|
||||
}
|
||||
},
|
||||
"required": ["session_cookie"],
|
||||
"x-auth-type": "cookie",
|
||||
"x-balance-extra-format": [
|
||||
{
|
||||
"label": "天",
|
||||
"source_limit": "daily_quota_limit",
|
||||
"source_remaining": "daily_remaining_quota",
|
||||
"source_start_date": "effective_start_date",
|
||||
"type": "daily_quota"
|
||||
},
|
||||
{
|
||||
"label": "月",
|
||||
"source_end_date": "effective_end_date",
|
||||
"type": "monthly_expiry"
|
||||
}
|
||||
],
|
||||
"x-currency": "USD",
|
||||
"x-default-base-url": "https://nekocode.ai",
|
||||
"x-field-groups": [
|
||||
{ "fields": ["base_url"] },
|
||||
{ "fields": ["session_cookie"] }
|
||||
],
|
||||
"x-quota-divisor": null,
|
||||
"x-validation": [
|
||||
{
|
||||
"type": "required",
|
||||
"fields": ["session_cookie"],
|
||||
"message": "请填写 Session Cookie"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
ProviderOpsArchitectureSpec {
|
||||
architecture_id: "nekocode",
|
||||
display_name: "NekoCode",
|
||||
description: "NekoCode 中转站预设配置,使用 Cookie 认证",
|
||||
hidden: false,
|
||||
credentials_schema: credentials_schema.clone(),
|
||||
verify_endpoint: "/api/user/self",
|
||||
verify_mode: ProviderOpsVerifyMode::DirectGet,
|
||||
balance_mode: ProviderOpsBalanceMode::SingleRequest,
|
||||
checkin_mode: ProviderOpsCheckinMode::None,
|
||||
query_balance_cookie_auth_errors: true,
|
||||
supported_auth_types: vec![ProviderOpsAuthSpec {
|
||||
auth_type: "cookie",
|
||||
display_name: "NekoCode Cookie",
|
||||
credentials_schema,
|
||||
}],
|
||||
supported_actions: vec![ProviderOpsActionSpec {
|
||||
action_type: "query_balance",
|
||||
display_name: "查询余额",
|
||||
description: "查询 NekoCode 账户余额和订阅信息",
|
||||
config_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"endpoint": {
|
||||
"type": "string",
|
||||
"title": "API 端点",
|
||||
"default": "/api/usage/summary"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}),
|
||||
}],
|
||||
default_connector: Some("cookie"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn default_action_config(action_type: &str) -> Option<Map<String, Value>> {
|
||||
match action_type {
|
||||
"query_balance" => Some(json_object(json!({
|
||||
"endpoint": "/api/usage/summary",
|
||||
"method": "GET",
|
||||
"currency": "USD"
|
||||
}))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
147
crates/aether-admin/src/provider/ops/architectures/new_api.rs
Normal file
147
crates/aether-admin/src/provider/ops/architectures/new_api.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use super::{
|
||||
json_object, ProviderOpsActionSpec, ProviderOpsArchitectureSpec, ProviderOpsAuthSpec,
|
||||
ProviderOpsBalanceMode, ProviderOpsCheckinMode, ProviderOpsVerifyMode,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
pub(super) fn spec() -> ProviderOpsArchitectureSpec {
|
||||
let credentials_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"api_key": {
|
||||
"type": "string",
|
||||
"title": "访问令牌 (API Key)",
|
||||
"description": "New API 的访问令牌,与 Cookie 二选一",
|
||||
"x-sensitive": true,
|
||||
"x-input-type": "password"
|
||||
},
|
||||
"base_url": {
|
||||
"type": "string",
|
||||
"title": "站点地址",
|
||||
"description": "API 基础地址"
|
||||
},
|
||||
"cookie": {
|
||||
"type": "string",
|
||||
"title": "Cookie",
|
||||
"description": "用于 Cookie 认证,与访问令牌二选一",
|
||||
"x-sensitive": true,
|
||||
"x-input-type": "password"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"title": "用户 ID",
|
||||
"description": "使用访问令牌时必填,使用 Cookie 时可选"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"x-auth-method": "bearer",
|
||||
"x-auth-type": "api_key",
|
||||
"x-currency": "USD",
|
||||
"x-field-groups": [
|
||||
{ "fields": ["base_url"] },
|
||||
{
|
||||
"fields": ["cookie"],
|
||||
"x-help": "从浏览器开发者工具复制完整 Cookie"
|
||||
},
|
||||
{
|
||||
"fields": ["api_key", "user_id"],
|
||||
"layout": "inline",
|
||||
"x-flex": {
|
||||
"api_key": 3,
|
||||
"user_id": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"x-field-hooks": {
|
||||
"cookie": {
|
||||
"action": "parse_new_api_user_id",
|
||||
"target": "user_id"
|
||||
}
|
||||
},
|
||||
"x-quota-divisor": 500000,
|
||||
"x-validation": [
|
||||
{
|
||||
"type": "any_required",
|
||||
"fields": ["api_key", "cookie"],
|
||||
"message": "访问令牌和 Cookie 至少需要填写一个"
|
||||
},
|
||||
{
|
||||
"type": "conditional_required",
|
||||
"if": "api_key",
|
||||
"then": ["user_id"],
|
||||
"unless": "cookie",
|
||||
"message": "使用访问令牌时,用户 ID 不能为空"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
ProviderOpsArchitectureSpec {
|
||||
architecture_id: "new_api",
|
||||
display_name: "New API",
|
||||
description: "New API 风格中转站的预设配置",
|
||||
hidden: false,
|
||||
credentials_schema: credentials_schema.clone(),
|
||||
verify_endpoint: "/api/user/self",
|
||||
verify_mode: ProviderOpsVerifyMode::DirectGet,
|
||||
balance_mode: ProviderOpsBalanceMode::SingleRequest,
|
||||
checkin_mode: ProviderOpsCheckinMode::NewApiCompatible,
|
||||
query_balance_cookie_auth_errors: false,
|
||||
supported_auth_types: vec![ProviderOpsAuthSpec {
|
||||
auth_type: "api_key",
|
||||
display_name: "New API Key",
|
||||
credentials_schema,
|
||||
}],
|
||||
supported_actions: vec![ProviderOpsActionSpec {
|
||||
action_type: "query_balance",
|
||||
display_name: "查询余额",
|
||||
description: "查询 New API 账户余额信息",
|
||||
config_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"endpoint": {
|
||||
"type": "string",
|
||||
"title": "API 路径",
|
||||
"description": "余额查询 API 路径",
|
||||
"default": "/api/user/self"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"title": "请求方法",
|
||||
"enum": ["GET", "POST"],
|
||||
"default": "GET"
|
||||
},
|
||||
"quota_divisor": {
|
||||
"type": "number",
|
||||
"title": "额度除数",
|
||||
"description": "将原始额度值转换为美元的除数",
|
||||
"default": 500000
|
||||
},
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "货币单位",
|
||||
"default": "USD"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}),
|
||||
}],
|
||||
default_connector: Some("api_key"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn default_action_config(action_type: &str) -> Option<Map<String, Value>> {
|
||||
match action_type {
|
||||
"query_balance" => Some(json_object(json!({
|
||||
"endpoint": "/api/user/self",
|
||||
"method": "GET",
|
||||
"quota_divisor": 500000,
|
||||
"checkin_endpoint": "/api/user/checkin",
|
||||
"currency": "USD"
|
||||
}))),
|
||||
"checkin" => Some(json_object(json!({
|
||||
"endpoint": "/api/user/checkin",
|
||||
"method": "POST"
|
||||
}))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
131
crates/aether-admin/src/provider/ops/architectures/sub2api.rs
Normal file
131
crates/aether-admin/src/provider/ops/architectures/sub2api.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use super::{
|
||||
json_object, ProviderOpsActionSpec, ProviderOpsArchitectureSpec, ProviderOpsAuthSpec,
|
||||
ProviderOpsBalanceMode, ProviderOpsCheckinMode, ProviderOpsVerifyMode,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
pub(super) fn spec() -> ProviderOpsArchitectureSpec {
|
||||
let session_login_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"type": "string",
|
||||
"title": "站点地址",
|
||||
"description": "API 基础地址"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"title": "邮箱",
|
||||
"description": "Sub2API 登录邮箱"
|
||||
},
|
||||
"password": {
|
||||
"type": "string",
|
||||
"title": "密码",
|
||||
"description": "Sub2API 登录密码",
|
||||
"x-sensitive": true,
|
||||
"x-input-type": "password"
|
||||
}
|
||||
},
|
||||
"required": ["email", "password"],
|
||||
"x-auth-method": "jwt",
|
||||
"x-auth-type": "session_login",
|
||||
"x-field-groups": [
|
||||
{ "fields": ["base_url"] },
|
||||
{ "fields": ["email"] },
|
||||
{ "fields": ["password"] }
|
||||
],
|
||||
"x-validation": [
|
||||
{
|
||||
"type": "required",
|
||||
"fields": ["email", "password"],
|
||||
"message": "请填写邮箱和密码"
|
||||
}
|
||||
]
|
||||
});
|
||||
let refresh_token_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"base_url": {
|
||||
"type": "string",
|
||||
"title": "站点地址",
|
||||
"description": "API 基础地址"
|
||||
},
|
||||
"refresh_token": {
|
||||
"type": "string",
|
||||
"title": "Refresh Token",
|
||||
"description": "从浏览器 F12 > Application > Local Storage 获取",
|
||||
"x-sensitive": true,
|
||||
"x-input-type": "password",
|
||||
"x-help": "浏览器控制台执行 localStorage.getItem('refresh_token') 获取"
|
||||
}
|
||||
},
|
||||
"required": ["refresh_token"],
|
||||
"x-auth-method": "bearer",
|
||||
"x-auth-type": "api_key",
|
||||
"x-field-groups": [
|
||||
{ "fields": ["base_url"] },
|
||||
{ "fields": ["refresh_token"] }
|
||||
],
|
||||
"x-validation": [
|
||||
{
|
||||
"type": "required",
|
||||
"fields": ["refresh_token"],
|
||||
"message": "请填写 Refresh Token"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
ProviderOpsArchitectureSpec {
|
||||
architecture_id: "sub2api",
|
||||
display_name: "Sub2API",
|
||||
description: "Sub2API 风格中转站的预设配置",
|
||||
hidden: false,
|
||||
credentials_schema: session_login_schema.clone(),
|
||||
verify_endpoint: "/api/v1/auth/me?timezone=Asia/Shanghai",
|
||||
verify_mode: ProviderOpsVerifyMode::Sub2ApiExchange,
|
||||
balance_mode: ProviderOpsBalanceMode::Sub2ApiDualRequest,
|
||||
checkin_mode: ProviderOpsCheckinMode::None,
|
||||
query_balance_cookie_auth_errors: false,
|
||||
supported_auth_types: vec![
|
||||
ProviderOpsAuthSpec {
|
||||
auth_type: "session_login",
|
||||
display_name: "账号密码",
|
||||
credentials_schema: session_login_schema,
|
||||
},
|
||||
ProviderOpsAuthSpec {
|
||||
auth_type: "api_key",
|
||||
display_name: "Refresh Token",
|
||||
credentials_schema: refresh_token_schema,
|
||||
},
|
||||
],
|
||||
supported_actions: vec![ProviderOpsActionSpec {
|
||||
action_type: "query_balance",
|
||||
display_name: "查询余额",
|
||||
description: "查询 Sub2API 账户余额和订阅信息",
|
||||
config_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "货币单位",
|
||||
"default": "USD"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}),
|
||||
}],
|
||||
default_connector: Some("session_login"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn default_action_config(action_type: &str) -> Option<Map<String, Value>> {
|
||||
match action_type {
|
||||
"query_balance" => Some(json_object(json!({
|
||||
"endpoint": "/api/v1/auth/me?timezone=Asia/Shanghai",
|
||||
"subscription_endpoint": "/api/v1/subscriptions/summary",
|
||||
"method": "GET",
|
||||
"currency": "USD"
|
||||
}))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
104
crates/aether-admin/src/provider/ops/architectures/yescode.rs
Normal file
104
crates/aether-admin/src/provider/ops/architectures/yescode.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use super::{
|
||||
json_object, ProviderOpsActionSpec, ProviderOpsArchitectureSpec, ProviderOpsAuthSpec,
|
||||
ProviderOpsBalanceMode, ProviderOpsCheckinMode, ProviderOpsVerifyMode,
|
||||
};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
pub(super) fn spec() -> ProviderOpsArchitectureSpec {
|
||||
let credentials_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"auth_cookie": {
|
||||
"type": "string",
|
||||
"title": "Auth Cookie",
|
||||
"description": "从浏览器复制的 Cookie(包含 yescode_auth 和 yescode_csrf)",
|
||||
"x-sensitive": true,
|
||||
"x-input-type": "password"
|
||||
},
|
||||
"base_url": {
|
||||
"type": "string",
|
||||
"title": "站点地址",
|
||||
"description": "API 基础地址",
|
||||
"x-default-value": "https://co.yes.vg"
|
||||
}
|
||||
},
|
||||
"required": ["auth_cookie"],
|
||||
"x-auth-type": "cookie",
|
||||
"x-balance-extra-format": [
|
||||
{
|
||||
"label": "天",
|
||||
"type": "weekly_spent",
|
||||
"source_limit": "daily_limit",
|
||||
"source_spent": "daily_spent",
|
||||
"source_resets_at": "daily_resets_at"
|
||||
},
|
||||
{
|
||||
"label": "周",
|
||||
"type": "weekly_spent",
|
||||
"source_limit": "weekly_limit",
|
||||
"source_spent": "weekly_spent",
|
||||
"source_resets_at": "weekly_resets_at"
|
||||
}
|
||||
],
|
||||
"x-currency": "USD",
|
||||
"x-default-base-url": "https://co.yes.vg",
|
||||
"x-field-groups": [
|
||||
{ "fields": ["base_url"] },
|
||||
{ "fields": ["auth_cookie"] }
|
||||
],
|
||||
"x-quota-divisor": null,
|
||||
"x-validation": [
|
||||
{
|
||||
"type": "required",
|
||||
"fields": ["auth_cookie"],
|
||||
"message": "请填写 Auth Cookie"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
ProviderOpsArchitectureSpec {
|
||||
architecture_id: "yescode",
|
||||
display_name: "YesCode",
|
||||
description: "YesCode 中转站预设配置,使用 Cookie 认证",
|
||||
hidden: false,
|
||||
credentials_schema: credentials_schema.clone(),
|
||||
verify_endpoint: "/api/v1/auth/profile",
|
||||
verify_mode: ProviderOpsVerifyMode::DirectGet,
|
||||
balance_mode: ProviderOpsBalanceMode::YescodeCombined,
|
||||
checkin_mode: ProviderOpsCheckinMode::None,
|
||||
query_balance_cookie_auth_errors: true,
|
||||
supported_auth_types: vec![ProviderOpsAuthSpec {
|
||||
auth_type: "cookie",
|
||||
display_name: "YesCode Cookie",
|
||||
credentials_schema,
|
||||
}],
|
||||
supported_actions: vec![ProviderOpsActionSpec {
|
||||
action_type: "query_balance",
|
||||
display_name: "查询余额(含每周限额)",
|
||||
description: "查询账户余额和每周限额信息",
|
||||
config_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"currency": {
|
||||
"type": "string",
|
||||
"title": "货币单位",
|
||||
"default": "USD"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}),
|
||||
}],
|
||||
default_connector: Some("cookie"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn default_action_config(action_type: &str) -> Option<Map<String, Value>> {
|
||||
match action_type {
|
||||
"query_balance" => Some(json_object(json!({
|
||||
"endpoint": "/api/v1/user/balance",
|
||||
"method": "GET",
|
||||
"currency": "USD"
|
||||
}))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -22,29 +22,6 @@ pub fn admin_provider_ops_connector_object(
|
||||
.and_then(serde_json::Value::as_object)
|
||||
}
|
||||
|
||||
pub fn admin_provider_ops_is_supported_auth_type(auth_type: &str) -> bool {
|
||||
matches!(
|
||||
auth_type,
|
||||
"api_key" | "session_login" | "oauth" | "cookie" | "none"
|
||||
)
|
||||
}
|
||||
|
||||
pub 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())
|
||||
}
|
||||
|
||||
pub fn admin_provider_ops_sensitive_placeholder_or_empty(
|
||||
value: Option<&serde_json::Value>,
|
||||
) -> bool {
|
||||
30
crates/aether-admin/src/provider/ops/mod.rs
Normal file
30
crates/aether-admin/src/provider/ops/mod.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
pub mod actions;
|
||||
pub mod architectures;
|
||||
pub mod config;
|
||||
pub mod verify;
|
||||
|
||||
pub use self::actions::{
|
||||
attach_balance_checkin_outcome, parse_query_balance_payload, parse_sub2api_balance_payload,
|
||||
parse_yescode_combined_balance_payload, ProviderOpsCheckinOutcome,
|
||||
};
|
||||
pub use self::architectures::{
|
||||
admin_provider_ops_is_supported_auth_type, get_architecture, list_architectures,
|
||||
normalize_architecture_id, resolve_action_config, ProviderOpsActionSpec,
|
||||
ProviderOpsArchitectureSpec, ProviderOpsAuthSpec, ProviderOpsBalanceMode,
|
||||
ProviderOpsCheckinMode, ProviderOpsVerifyMode,
|
||||
};
|
||||
pub use self::config::{
|
||||
admin_provider_ops_config_object, admin_provider_ops_connector_object,
|
||||
admin_provider_ops_sensitive_placeholder_or_empty, build_admin_provider_ops_status_payload,
|
||||
resolve_admin_provider_ops_base_url,
|
||||
};
|
||||
pub use self::verify::{
|
||||
admin_provider_ops_anyrouter_compute_acw_sc_v2,
|
||||
admin_provider_ops_anyrouter_parse_session_user_id, admin_provider_ops_extract_cookie_value,
|
||||
admin_provider_ops_frontend_updated_credentials, admin_provider_ops_json_object,
|
||||
admin_provider_ops_value_as_f64, admin_provider_ops_value_as_u64,
|
||||
admin_provider_ops_verify_failure, admin_provider_ops_verify_headers,
|
||||
admin_provider_ops_verify_success, admin_provider_ops_verify_user_payload,
|
||||
admin_provider_ops_verify_user_payload_with_usage, admin_provider_ops_yescode_cookie_header,
|
||||
build_headers, parse_verify_payload, ADMIN_PROVIDER_OPS_USER_AGENT,
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
use super::architectures::normalize_architecture_id;
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use http::StatusCode;
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
use serde_json::{json, Map, Value};
|
||||
@@ -9,13 +10,31 @@ const ADMIN_PROVIDER_OPS_ANYROUTER_UNSBOX_TABLE: [usize; 40] = [
|
||||
0x19, 0xD, 0x6, 0xB, 0x27, 0x12, 0x14, 0x8, 0xE, 0x15, 0x20, 0x1A, 0x2, 0x1E, 0x7, 0x4, 0x11,
|
||||
0x5, 0x3, 0x1C, 0x22, 0x25, 0xC, 0x24,
|
||||
];
|
||||
pub const ADMIN_PROVIDER_OPS_USER_AGENT: &str = "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";
|
||||
|
||||
pub 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",
|
||||
pub fn build_headers(
|
||||
architecture_id: &str,
|
||||
config: &Map<String, Value>,
|
||||
credentials: &Map<String, Value>,
|
||||
) -> Result<HeaderMap, String> {
|
||||
admin_provider_ops_verify_headers(architecture_id, config, credentials)
|
||||
}
|
||||
|
||||
pub fn parse_verify_payload(
|
||||
architecture_id: &str,
|
||||
status: StatusCode,
|
||||
response_json: &Value,
|
||||
updated_credentials: Option<Map<String, Value>>,
|
||||
) -> Value {
|
||||
match normalize_architecture_id(architecture_id) {
|
||||
"anyrouter" => admin_provider_ops_anyrouter_verify_payload(status, response_json),
|
||||
"cubence" => admin_provider_ops_cubence_verify_payload(status, response_json),
|
||||
"yescode" => admin_provider_ops_yescode_verify_payload(status, response_json),
|
||||
"nekocode" => admin_provider_ops_nekocode_verify_payload(status, response_json),
|
||||
"sub2api" => {
|
||||
admin_provider_ops_sub2api_verify_payload(status, response_json, updated_credentials)
|
||||
}
|
||||
_ => admin_provider_ops_generic_verify_payload(status, response_json),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +50,67 @@ pub fn admin_provider_ops_extract_cookie_value(cookie_input: &str, key: &str) ->
|
||||
cookie_input.trim().to_string()
|
||||
}
|
||||
|
||||
fn admin_provider_ops_strip_cookie_header_prefix(cookie_input: &str) -> &str {
|
||||
let trimmed = cookie_input.trim();
|
||||
if trimmed
|
||||
.get(..7)
|
||||
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("cookie:"))
|
||||
{
|
||||
return trimmed[7..].trim();
|
||||
}
|
||||
trimmed
|
||||
}
|
||||
|
||||
fn admin_provider_ops_is_set_cookie_attribute(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"path"
|
||||
| "domain"
|
||||
| "expires"
|
||||
| "max-age"
|
||||
| "secure"
|
||||
| "httponly"
|
||||
| "samesite"
|
||||
| "partitioned"
|
||||
| "priority"
|
||||
)
|
||||
}
|
||||
|
||||
fn admin_provider_ops_cubence_cookie_header(cookie_input: &str) -> String {
|
||||
let trimmed = admin_provider_ops_strip_cookie_header_prefix(cookie_input);
|
||||
if trimmed.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
if !trimmed.contains('=') {
|
||||
return format!("token={trimmed}");
|
||||
}
|
||||
|
||||
let cookies = trimmed
|
||||
.split(';')
|
||||
.filter_map(|part| {
|
||||
let part = part.trim();
|
||||
let (name, value) = part.split_once('=')?;
|
||||
let name = name.trim();
|
||||
let value = value.trim();
|
||||
if name.is_empty() || value.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let lower = name.to_ascii_lowercase();
|
||||
if admin_provider_ops_is_set_cookie_attribute(&lower) {
|
||||
return None;
|
||||
}
|
||||
Some(format!("{name}={value}"))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if cookies.is_empty() {
|
||||
let token = admin_provider_ops_extract_cookie_value(trimmed, "token");
|
||||
return format!("token={token}");
|
||||
}
|
||||
|
||||
cookies.join("; ")
|
||||
}
|
||||
|
||||
pub fn admin_provider_ops_yescode_cookie_header(cookie_input: &str) -> String {
|
||||
if cookie_input.contains("yescode_auth=") {
|
||||
let mut parts = Vec::new();
|
||||
@@ -68,12 +148,12 @@ pub fn admin_provider_ops_anyrouter_compute_acw_sc_v2(arg1: &str) -> Option<Stri
|
||||
|
||||
pub 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 decoded = decode_python_urlsafe_b64(&session_cookie)?;
|
||||
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 gob_data = decode_python_urlsafe_b64(gob_b64)?;
|
||||
|
||||
let id_pattern = b"\x02id\x03int";
|
||||
let id_idx = gob_data
|
||||
@@ -97,6 +177,19 @@ pub fn admin_provider_ops_anyrouter_parse_session_user_id(cookie_input: &str) ->
|
||||
Some((val >> 1).to_string())
|
||||
}
|
||||
|
||||
fn decode_python_urlsafe_b64(input: &str) -> Option<Vec<u8>> {
|
||||
let normalized = input.trim().replace('-', "+").replace('_', "/");
|
||||
if normalized.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let remainder = normalized.len() % 4;
|
||||
let mut padded = normalized;
|
||||
if remainder != 0 {
|
||||
padded.push_str(&"=".repeat(4 - remainder));
|
||||
}
|
||||
STANDARD.decode(padded.as_bytes()).ok()
|
||||
}
|
||||
|
||||
pub fn admin_provider_ops_verify_failure(message: impl Into<String>) -> Value {
|
||||
json!({
|
||||
"success": false,
|
||||
@@ -110,13 +203,21 @@ pub fn admin_provider_ops_verify_success(
|
||||
) -> Value {
|
||||
let mut payload = Map::from_iter([
|
||||
("success".to_string(), Value::Bool(true)),
|
||||
("message".to_string(), Value::Null),
|
||||
("data".to_string(), data),
|
||||
]);
|
||||
if let Some(credentials) = updated_credentials.filter(|value| !value.is_empty()) {
|
||||
payload.insert(
|
||||
(
|
||||
"updated_credentials".to_string(),
|
||||
Value::Object(credentials),
|
||||
);
|
||||
updated_credentials
|
||||
.clone()
|
||||
.map(Value::Object)
|
||||
.unwrap_or(Value::Null),
|
||||
),
|
||||
]);
|
||||
if updated_credentials
|
||||
.as_ref()
|
||||
.is_some_and(|value| value.is_empty())
|
||||
{
|
||||
payload.insert("updated_credentials".to_string(), Value::Null);
|
||||
}
|
||||
Value::Object(payload)
|
||||
}
|
||||
@@ -154,9 +255,41 @@ pub fn admin_provider_ops_verify_user_payload(
|
||||
.map(Value::Number)
|
||||
.unwrap_or(Value::Null),
|
||||
);
|
||||
if let Some(extra) = extra.filter(|value| !value.is_empty()) {
|
||||
payload.insert("extra".to_string(), Value::Object(extra));
|
||||
}
|
||||
payload.insert(
|
||||
"extra".to_string(),
|
||||
Value::Object(extra.unwrap_or_default()),
|
||||
);
|
||||
Value::Object(payload)
|
||||
}
|
||||
|
||||
pub fn admin_provider_ops_verify_user_payload_with_usage(
|
||||
username: Option<String>,
|
||||
display_name: Option<String>,
|
||||
email: Option<String>,
|
||||
quota: Option<f64>,
|
||||
used_quota: Option<f64>,
|
||||
request_count: Option<u64>,
|
||||
extra: Option<Map<String, Value>>,
|
||||
) -> Value {
|
||||
let mut payload =
|
||||
admin_provider_ops_verify_user_payload(username, display_name, email, quota, extra)
|
||||
.as_object()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
payload.insert(
|
||||
"used_quota".to_string(),
|
||||
used_quota
|
||||
.and_then(serde_json::Number::from_f64)
|
||||
.map(Value::Number)
|
||||
.unwrap_or(Value::Null),
|
||||
);
|
||||
payload.insert(
|
||||
"request_count".to_string(),
|
||||
request_count
|
||||
.map(serde_json::Number::from)
|
||||
.map(Value::Number)
|
||||
.unwrap_or(Value::Null),
|
||||
);
|
||||
Value::Object(payload)
|
||||
}
|
||||
|
||||
@@ -168,7 +301,20 @@ pub fn admin_provider_ops_value_as_f64(value: Option<&Value>) -> Option<f64> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn admin_provider_ops_json_object(value: &Value) -> Option<&serde_json::Map<String, Value>> {
|
||||
pub fn admin_provider_ops_value_as_u64(value: Option<&Value>) -> Option<u64> {
|
||||
match value {
|
||||
Some(Value::Number(number)) => number.as_u64().or_else(|| {
|
||||
number
|
||||
.as_i64()
|
||||
.filter(|value| *value >= 0)
|
||||
.map(|value| value as u64)
|
||||
}),
|
||||
Some(Value::String(raw)) => raw.trim().parse::<u64>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn admin_provider_ops_json_object(value: &Value) -> Option<&Map<String, Value>> {
|
||||
value.as_object()
|
||||
}
|
||||
|
||||
@@ -186,11 +332,7 @@ pub fn admin_provider_ops_frontend_updated_credentials(
|
||||
(!filtered.is_empty()).then_some(filtered)
|
||||
}
|
||||
|
||||
fn admin_provider_ops_insert_header(
|
||||
headers: &mut HeaderMap,
|
||||
name: &str,
|
||||
value: &str,
|
||||
) -> Result<(), String> {
|
||||
fn insert_header(headers: &mut HeaderMap, name: &str, value: &str) -> Result<(), String> {
|
||||
let header_name =
|
||||
HeaderName::from_bytes(name.as_bytes()).map_err(|_| format!("无效的请求头: {name}"))?;
|
||||
let header_value =
|
||||
@@ -201,11 +343,11 @@ fn admin_provider_ops_insert_header(
|
||||
|
||||
pub fn admin_provider_ops_verify_headers(
|
||||
architecture_id: &str,
|
||||
config: &serde_json::Map<String, Value>,
|
||||
credentials: &serde_json::Map<String, Value>,
|
||||
config: &Map<String, Value>,
|
||||
credentials: &Map<String, Value>,
|
||||
) -> Result<HeaderMap, String> {
|
||||
let mut headers = HeaderMap::new();
|
||||
match architecture_id {
|
||||
match normalize_architecture_id(architecture_id) {
|
||||
"generic_api" => {
|
||||
let api_key = credentials
|
||||
.get("api_key")
|
||||
@@ -222,13 +364,9 @@ pub fn admin_provider_ops_verify_headers(
|
||||
.get("header_name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("X-API-Key");
|
||||
admin_provider_ops_insert_header(&mut headers, header_name, api_key)?;
|
||||
insert_header(&mut headers, header_name, api_key)?;
|
||||
} else {
|
||||
admin_provider_ops_insert_header(
|
||||
&mut headers,
|
||||
"Authorization",
|
||||
&format!("Bearer {api_key}"),
|
||||
)?;
|
||||
insert_header(&mut headers, "Authorization", &format!("Bearer {api_key}"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,7 +377,6 @@ pub fn admin_provider_ops_verify_headers(
|
||||
"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"),
|
||||
@@ -248,11 +385,11 @@ pub fn admin_provider_ops_verify_headers(
|
||||
("Sec-Fetch-Mode", "cors"),
|
||||
("Sec-Fetch-Dest", "empty"),
|
||||
] {
|
||||
admin_provider_ops_insert_header(&mut headers, name, value)?;
|
||||
insert_header(&mut headers, name, value)?;
|
||||
}
|
||||
if let Some(api_key) = credentials.get("api_key").and_then(Value::as_str) {
|
||||
if !api_key.trim().is_empty() {
|
||||
admin_provider_ops_insert_header(
|
||||
insert_header(
|
||||
&mut headers,
|
||||
"Authorization",
|
||||
&format!("Bearer {}", api_key.trim()),
|
||||
@@ -261,27 +398,26 @@ pub fn admin_provider_ops_verify_headers(
|
||||
}
|
||||
if let Some(user_id) = credentials.get("user_id").and_then(Value::as_str) {
|
||||
if !user_id.trim().is_empty() {
|
||||
admin_provider_ops_insert_header(&mut headers, "New-Api-User", user_id.trim())?;
|
||||
insert_header(&mut headers, "New-Api-User", user_id.trim())?;
|
||||
}
|
||||
}
|
||||
if let Some(cookie) = credentials.get("cookie").and_then(Value::as_str) {
|
||||
if !cookie.trim().is_empty() {
|
||||
admin_provider_ops_insert_header(&mut headers, "Cookie", cookie.trim())?;
|
||||
insert_header(&mut headers, "Cookie", cookie.trim())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
"cubence" => {
|
||||
insert_header(&mut headers, "User-Agent", ADMIN_PROVIDER_OPS_USER_AGENT)?;
|
||||
if let Some(token_cookie) = credentials
|
||||
.get("token_cookie")
|
||||
.and_then(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}"),
|
||||
)?;
|
||||
let cookie_header = admin_provider_ops_cubence_cookie_header(token_cookie);
|
||||
if !cookie_header.is_empty() {
|
||||
insert_header(&mut headers, "Cookie", &cookie_header)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
"yescode" => {
|
||||
@@ -290,7 +426,7 @@ pub fn admin_provider_ops_verify_headers(
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
admin_provider_ops_insert_header(
|
||||
insert_header(
|
||||
&mut headers,
|
||||
"Cookie",
|
||||
&admin_provider_ops_yescode_cookie_header(auth_cookie),
|
||||
@@ -304,15 +440,12 @@ pub fn admin_provider_ops_verify_headers(
|
||||
.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}"),
|
||||
)?;
|
||||
insert_header(&mut headers, "Cookie", &format!("session={session}"))?;
|
||||
}
|
||||
}
|
||||
"anyrouter" => {
|
||||
let mut cookies = Vec::new();
|
||||
insert_header(&mut headers, "User-Agent", ADMIN_PROVIDER_OPS_USER_AGENT)?;
|
||||
if let Some(acw_cookie) = config
|
||||
.get("acw_cookie")
|
||||
.and_then(Value::as_str)
|
||||
@@ -331,11 +464,11 @@ pub fn admin_provider_ops_verify_headers(
|
||||
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())?;
|
||||
insert_header(&mut headers, "New-Api-User", user_id.trim())?;
|
||||
}
|
||||
}
|
||||
if !cookies.is_empty() {
|
||||
admin_provider_ops_insert_header(&mut headers, "Cookie", &cookies.join("; "))?;
|
||||
insert_header(&mut headers, "Cookie", &cookies.join("; "))?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -346,12 +479,38 @@ pub fn admin_provider_ops_verify_headers(
|
||||
pub fn admin_provider_ops_generic_verify_payload(
|
||||
status: StatusCode,
|
||||
response_json: &Value,
|
||||
) -> Value {
|
||||
verify_payload_with_auth_messages(
|
||||
status,
|
||||
response_json,
|
||||
"认证失败:无效的凭据",
|
||||
"认证失败:权限不足",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn admin_provider_ops_anyrouter_verify_payload(
|
||||
status: StatusCode,
|
||||
response_json: &Value,
|
||||
) -> Value {
|
||||
verify_payload_with_auth_messages(
|
||||
status,
|
||||
response_json,
|
||||
"Cookie 已失效,请重新配置",
|
||||
"Cookie 已失效或无权限",
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_payload_with_auth_messages(
|
||||
status: StatusCode,
|
||||
response_json: &Value,
|
||||
unauthorized_message: &str,
|
||||
forbidden_message: &str,
|
||||
) -> Value {
|
||||
if status == StatusCode::UNAUTHORIZED {
|
||||
return admin_provider_ops_verify_failure("认证失败:无效的凭据");
|
||||
return admin_provider_ops_verify_failure(unauthorized_message);
|
||||
}
|
||||
if status == StatusCode::FORBIDDEN {
|
||||
return admin_provider_ops_verify_failure("认证失败:权限不足");
|
||||
return admin_provider_ops_verify_failure(forbidden_message);
|
||||
}
|
||||
if status != StatusCode::OK {
|
||||
return admin_provider_ops_verify_failure(format!("验证失败:HTTP {}", status.as_u16()));
|
||||
@@ -388,7 +547,7 @@ pub fn admin_provider_ops_generic_verify_payload(
|
||||
}
|
||||
|
||||
admin_provider_ops_verify_success(
|
||||
admin_provider_ops_verify_user_payload(
|
||||
admin_provider_ops_verify_user_payload_with_usage(
|
||||
user_data
|
||||
.get("username")
|
||||
.and_then(Value::as_str)
|
||||
@@ -402,6 +561,8 @@ pub fn admin_provider_ops_generic_verify_payload(
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned),
|
||||
admin_provider_ops_value_as_f64(user_data.get("quota")),
|
||||
admin_provider_ops_value_as_f64(user_data.get("used_quota")),
|
||||
admin_provider_ops_value_as_u64(user_data.get("request_count")),
|
||||
Some(extra),
|
||||
),
|
||||
None,
|
||||
@@ -422,7 +583,22 @@ pub fn admin_provider_ops_cubence_verify_payload(
|
||||
return admin_provider_ops_verify_failure(format!("验证失败:HTTP {}", status.as_u16()));
|
||||
}
|
||||
|
||||
let Some(payload) = admin_provider_ops_json_object(response_json) else {
|
||||
let payload = if response_json.get("success").and_then(Value::as_bool) == Some(true)
|
||||
&& response_json.get("data").is_some_and(Value::is_object)
|
||||
{
|
||||
response_json.get("data")
|
||||
} else if response_json.get("success").and_then(Value::as_bool) == Some(false) {
|
||||
return admin_provider_ops_verify_failure(
|
||||
response_json
|
||||
.get("message")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("验证失败"),
|
||||
);
|
||||
} else {
|
||||
Some(response_json)
|
||||
};
|
||||
|
||||
let Some(payload) = payload.and_then(admin_provider_ops_json_object) else {
|
||||
return admin_provider_ops_verify_failure("响应格式无效");
|
||||
};
|
||||
let user_info = payload
|
||||
@@ -633,3 +809,199 @@ pub fn admin_provider_ops_sub2api_verify_payload(
|
||||
updated_credentials,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
admin_provider_ops_anyrouter_compute_acw_sc_v2,
|
||||
admin_provider_ops_anyrouter_parse_session_user_id,
|
||||
admin_provider_ops_anyrouter_verify_payload, admin_provider_ops_cubence_verify_payload,
|
||||
admin_provider_ops_frontend_updated_credentials, admin_provider_ops_sub2api_verify_payload,
|
||||
admin_provider_ops_verify_headers, ADMIN_PROVIDER_OPS_USER_AGENT,
|
||||
};
|
||||
use http::StatusCode;
|
||||
use reqwest::header::COOKIE;
|
||||
use reqwest::header::USER_AGENT;
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
#[test]
|
||||
fn anyrouter_compute_acw_sc_v2_matches_python_algorithm() {
|
||||
let actual = admin_provider_ops_anyrouter_compute_acw_sc_v2(
|
||||
"0123456789abcdef0123456789abcdef01234567",
|
||||
);
|
||||
assert_eq!(
|
||||
actual.as_deref(),
|
||||
Some("d2c7186598ab1a508a4f6064e4fa746323ab17c6")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anyrouter_parse_session_user_id_extracts_numeric_id() {
|
||||
let actual = admin_provider_ops_anyrouter_parse_session_user_id(
|
||||
"session=MTIzfGVIaDRlQUpwWkFOcGJuU3F1d0RfVkhsNWVYa0lkWE5sY201aGJXVUdjM1J5YVc1bkRCQUFCV0ZzYVdObHxzaWc",
|
||||
);
|
||||
assert_eq!(actual.as_deref(), Some("42"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anyrouter_parse_session_user_id_accepts_padded_urlsafe_base64() {
|
||||
let actual = admin_provider_ops_anyrouter_parse_session_user_id(
|
||||
"session=MTIzfGVIaDRlQUpwWkFOcGJuU3F1d0RfVkhsNWVYa0lkWE5sY201aGJXVUdjM1J5YVc1bkRCQUFCV0ZzYVdObHxzaWc=",
|
||||
);
|
||||
assert_eq!(actual.as_deref(), Some("42"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frontend_updated_credentials_omits_internal_runtime_fields() {
|
||||
let filtered = admin_provider_ops_frontend_updated_credentials(Map::from_iter([
|
||||
("refresh_token".to_string(), json!("refresh-token")),
|
||||
("_cached_access_token".to_string(), json!("access-token")),
|
||||
("_cached_token_expires_at".to_string(), json!(123456.0)),
|
||||
("password".to_string(), Value::Null),
|
||||
]));
|
||||
|
||||
assert_eq!(
|
||||
filtered,
|
||||
Some(Map::from_iter([(
|
||||
"refresh_token".to_string(),
|
||||
json!("refresh-token")
|
||||
)]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub2api_verify_payload_sums_balance_and_points() {
|
||||
let payload = admin_provider_ops_sub2api_verify_payload(
|
||||
StatusCode::OK,
|
||||
&json!({
|
||||
"code": 0,
|
||||
"data": {
|
||||
"username": "sub2api-user",
|
||||
"email": "sub2api@example.com",
|
||||
"balance": 8.5,
|
||||
"points": 1.5,
|
||||
"status": "active",
|
||||
"concurrency": 4
|
||||
}
|
||||
}),
|
||||
Some(Map::from_iter([(
|
||||
"refresh_token".to_string(),
|
||||
json!("refresh-token-new"),
|
||||
)])),
|
||||
);
|
||||
|
||||
assert_eq!(payload["success"], json!(true));
|
||||
assert_eq!(payload["data"]["username"], json!("sub2api-user"));
|
||||
assert_eq!(payload["data"]["quota"], json!(10.0));
|
||||
assert_eq!(payload["data"]["extra"]["balance"], json!(8.5));
|
||||
assert_eq!(payload["data"]["extra"]["points"], json!(1.5));
|
||||
assert_eq!(payload["data"]["extra"]["status"], json!("active"));
|
||||
assert_eq!(payload["data"]["extra"]["concurrency"], json!(4));
|
||||
assert_eq!(
|
||||
payload["updated_credentials"],
|
||||
json!({ "refresh_token": "refresh-token-new" })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anyrouter_verify_payload_uses_cookie_auth_messages_and_usage_fields() {
|
||||
let payload = admin_provider_ops_anyrouter_verify_payload(
|
||||
StatusCode::OK,
|
||||
&json!({
|
||||
"id": 42,
|
||||
"username": "alice",
|
||||
"display_name": "Alice",
|
||||
"email": "alice@example.com",
|
||||
"quota": 7.5,
|
||||
"used_quota": 1.25,
|
||||
"request_count": 8
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(payload["success"], json!(true));
|
||||
assert_eq!(payload["data"]["quota"], json!(7.5));
|
||||
assert_eq!(payload["data"]["used_quota"], json!(1.25));
|
||||
assert_eq!(payload["data"]["request_count"], json!(8));
|
||||
|
||||
let auth_failed =
|
||||
admin_provider_ops_anyrouter_verify_payload(StatusCode::UNAUTHORIZED, &json!({}));
|
||||
assert_eq!(auth_failed["success"], json!(false));
|
||||
assert_eq!(auth_failed["message"], json!("Cookie 已失效,请重新配置"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anyrouter_verify_headers_include_shared_user_agent() {
|
||||
let headers = admin_provider_ops_verify_headers(
|
||||
"anyrouter",
|
||||
&Map::from_iter([("acw_cookie".to_string(), json!("acw_sc__v2=test"))]),
|
||||
&Map::from_iter([(
|
||||
"session_cookie".to_string(),
|
||||
json!("session=MTIzfGVIaDRlQUpwWkFOcGJuU3F1d0RfVkhsNWVYa0lkWE5sY201aGJXVUdjM1J5YVc1bkRCQUFCV0ZzYVdObHxzaWc="),
|
||||
)]),
|
||||
)
|
||||
.expect("headers should build");
|
||||
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(USER_AGENT)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some(ADMIN_PROVIDER_OPS_USER_AGENT)
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get("New-Api-User")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("42")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cubence_verify_headers_preserve_full_cookie_header() {
|
||||
let headers = admin_provider_ops_verify_headers(
|
||||
"cubence",
|
||||
&Map::new(),
|
||||
&Map::from_iter([(
|
||||
"token_cookie".to_string(),
|
||||
json!("Cookie: token=abc; cf_clearance=def; Path=/; HttpOnly"),
|
||||
)]),
|
||||
)
|
||||
.expect("headers should build");
|
||||
|
||||
assert_eq!(
|
||||
headers.get(COOKIE).and_then(|value| value.to_str().ok()),
|
||||
Some("token=abc; cf_clearance=def")
|
||||
);
|
||||
assert_eq!(
|
||||
headers
|
||||
.get(USER_AGENT)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some(ADMIN_PROVIDER_OPS_USER_AGENT)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cubence_verify_payload_reads_wrapped_dashboard_overview() {
|
||||
let payload = admin_provider_ops_cubence_verify_payload(
|
||||
StatusCode::OK,
|
||||
&json!({
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"username": "AAEE86",
|
||||
"role": "user",
|
||||
"invite_code": "SCFSJ5C5"
|
||||
},
|
||||
"balance": {
|
||||
"total_balance_dollar": 0.6
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(payload["success"], json!(true));
|
||||
assert_eq!(payload["data"]["username"], json!("AAEE86"));
|
||||
assert_eq!(payload["data"]["quota"], json!(0.6));
|
||||
assert_eq!(payload["data"]["extra"]["role"], json!("user"));
|
||||
assert_eq!(payload["data"]["extra"]["invite_code"], json!("SCFSJ5C5"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user