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

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

- 影响说明:统一了 Provider Ops 的扩展方式与运行时行为,降低后续新增架构的接入成本
- 影响说明:余额查询从“实时阻塞返回”扩展为“缓存命中即返回并后台刷新”的模式,前端需要兼容 pending 状态
This commit is contained in:
AAEE86
2026-04-10 15:06:42 +08:00
parent 87b433e290
commit b8ce02b4f7
48 changed files with 7680 additions and 2450 deletions
@@ -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,
}
}
@@ -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,
}
}
@@ -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")));
}
}
@@ -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,
}
}
@@ -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,
}
}
@@ -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,
}
}
@@ -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,
}
}
@@ -0,0 +1,133 @@
use aether_data_contracts::repository::provider_catalog::{
StoredProviderCatalogEndpoint, StoredProviderCatalogProvider,
};
use serde_json::json;
pub 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)
}
pub 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)
}
pub 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,
}
}
pub fn resolve_admin_provider_ops_base_url(
provider: &StoredProviderCatalogProvider,
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)
}
pub 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,
})
}
@@ -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,
};
File diff suppressed because it is too large Load Diff