mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27:46 +08:00
Merge remote-tracking branch 'entropy-xu/codex/ccswitch-import'
This commit is contained in:
@@ -29,6 +29,8 @@ mod support_announcements;
|
||||
mod support_auth;
|
||||
#[path = "support/billing.rs"]
|
||||
mod support_billing;
|
||||
#[path = "support/ccswitch.rs"]
|
||||
mod support_ccswitch;
|
||||
#[path = "support/dashboard.rs"]
|
||||
mod support_dashboard;
|
||||
#[path = "support/install.rs"]
|
||||
@@ -66,10 +68,11 @@ use self::support_auth::{
|
||||
build_auth_settings_payload, extract_client_device_id, maybe_build_local_auth_response,
|
||||
};
|
||||
use self::support_billing::maybe_build_local_billing_response;
|
||||
use self::support_ccswitch::maybe_build_local_ccswitch_response;
|
||||
use self::support_dashboard::maybe_build_local_dashboard_response;
|
||||
pub(crate) use self::support_install::{
|
||||
build_api_key_install_session_response, build_proxy_node_install_session_response,
|
||||
CreateApiKeyInstallSessionRequest,
|
||||
base_url_from_request, build_api_key_install_session_response,
|
||||
build_proxy_node_install_session_response, CreateApiKeyInstallSessionRequest,
|
||||
};
|
||||
use self::support_install::{
|
||||
handle_users_me_api_key_install_session_create, maybe_build_local_install_response,
|
||||
@@ -84,7 +87,10 @@ use self::support_payment::maybe_build_local_payment_callback_response;
|
||||
use self::support_test_connection::maybe_build_local_test_connection_response;
|
||||
use self::support_user_me::maybe_build_local_users_me_response;
|
||||
use self::support_wallet::{
|
||||
direct_gateway_channels, maybe_build_local_wallet_response, sanitize_wallet_gateway_response,
|
||||
build_wallet_balance_payload_for_auth_scope, build_wallet_balance_payload_for_user,
|
||||
build_wallet_live_today_usage_payload_for_api_key,
|
||||
build_wallet_live_today_usage_payload_for_user, direct_gateway_channels,
|
||||
maybe_build_local_wallet_response, sanitize_wallet_gateway_response,
|
||||
wallet_normalize_optional_string_field,
|
||||
};
|
||||
|
||||
@@ -171,6 +177,13 @@ pub(crate) async fn maybe_build_local_public_support_response(
|
||||
return Some(build_unhandled_public_support_response(request_context));
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("ccswitch") {
|
||||
if let Some(response) = maybe_build_local_ccswitch_response(state, request_context).await {
|
||||
return Some(response);
|
||||
}
|
||||
return Some(build_unhandled_public_support_response(request_context));
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("users_me") {
|
||||
return maybe_build_local_users_me_response(state, request_context, headers, request_body)
|
||||
.await;
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::control::GatewayLocalAuthRejection;
|
||||
use crate::handlers::shared::round_to;
|
||||
|
||||
use super::{
|
||||
build_auth_error_response, build_wallet_balance_payload_for_auth_scope,
|
||||
build_wallet_live_today_usage_payload_for_api_key,
|
||||
build_wallet_live_today_usage_payload_for_user, AppState, GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
fn ccswitch_usage_auth_error_response(
|
||||
rejection: Option<&GatewayLocalAuthRejection>,
|
||||
) -> Response<Body> {
|
||||
match rejection {
|
||||
Some(GatewayLocalAuthRejection::InvalidApiKey) | None => {
|
||||
build_auth_error_response(http::StatusCode::UNAUTHORIZED, "无效的 API Key", false)
|
||||
}
|
||||
Some(GatewayLocalAuthRejection::LockedApiKey) => {
|
||||
build_auth_error_response(http::StatusCode::FORBIDDEN, "API Key 已被锁定", false)
|
||||
}
|
||||
Some(GatewayLocalAuthRejection::ProviderNotAllowed { .. })
|
||||
| Some(GatewayLocalAuthRejection::ApiFormatNotAllowed { .. })
|
||||
| Some(GatewayLocalAuthRejection::ModelNotAllowed { .. })
|
||||
| Some(GatewayLocalAuthRejection::IpNotAllowed { .. }) => {
|
||||
build_auth_error_response(http::StatusCode::FORBIDDEN, "API Key 无权查询用量", false)
|
||||
}
|
||||
Some(GatewayLocalAuthRejection::WalletUnavailable) => build_auth_error_response(
|
||||
http::StatusCode::SERVICE_UNAVAILABLE,
|
||||
"钱包数据暂不可用",
|
||||
false,
|
||||
),
|
||||
Some(GatewayLocalAuthRejection::BalanceDenied { .. }) => {
|
||||
build_auth_error_response(http::StatusCode::FORBIDDEN, "API Key 余额不足", false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn json_f64(value: &serde_json::Value, key: &str) -> Option<f64> {
|
||||
value.get(key).and_then(serde_json::Value::as_f64)
|
||||
}
|
||||
|
||||
fn json_bool(value: &serde_json::Value, key: &str) -> bool {
|
||||
value
|
||||
.get(key)
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn format_usd(value: f64) -> String {
|
||||
format!("${:.4}", round_to(value, 4))
|
||||
}
|
||||
|
||||
fn build_ccswitch_usage_extra(
|
||||
wallet_payload: &serde_json::Value,
|
||||
today_payload: Option<&serde_json::Value>,
|
||||
) -> String {
|
||||
let mut parts = Vec::new();
|
||||
if let Some(today_cost) = today_payload.and_then(|payload| json_f64(payload, "total_cost")) {
|
||||
parts.push(format!("今日消耗 {}", format_usd(today_cost)));
|
||||
}
|
||||
if let Some(wallet_balance) = json_f64(wallet_payload, "wallet_balance") {
|
||||
parts.push(format!("钱包 {}", format_usd(wallet_balance)));
|
||||
}
|
||||
if let Some(package_balance) = json_f64(wallet_payload, "package_balance") {
|
||||
if package_balance > 0.0 {
|
||||
parts.push(format!("套餐 {}", format_usd(package_balance)));
|
||||
}
|
||||
}
|
||||
|
||||
parts.join(" · ")
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_build_local_ccswitch_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Option<Response<Body>> {
|
||||
let decision = request_context.control_decision.as_ref()?;
|
||||
if decision.route_family.as_deref() != Some("ccswitch") {
|
||||
return None;
|
||||
}
|
||||
if decision.route_kind.as_deref() != Some("usage")
|
||||
|| request_context.request_path.trim_end_matches('/') != "/api/ccswitch/usage"
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let auth_context = match decision.auth_context.as_ref() {
|
||||
Some(auth_context) if !auth_context.user_id.trim().is_empty() => auth_context,
|
||||
_ => {
|
||||
return Some(ccswitch_usage_auth_error_response(
|
||||
decision.local_auth_rejection.as_ref(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match auth_context.local_rejection.as_ref() {
|
||||
None | Some(GatewayLocalAuthRejection::BalanceDenied { .. }) => {}
|
||||
rejection => return Some(ccswitch_usage_auth_error_response(rejection)),
|
||||
}
|
||||
|
||||
let wallet = match state
|
||||
.read_wallet_snapshot_for_auth(
|
||||
&auth_context.user_id,
|
||||
&auth_context.api_key_id,
|
||||
auth_context.api_key_is_standalone,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return Some(build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("ccswitch usage wallet lookup failed: {err:?}"),
|
||||
false,
|
||||
))
|
||||
}
|
||||
};
|
||||
let wallet_payload = build_wallet_balance_payload_for_auth_scope(
|
||||
state,
|
||||
&auth_context.user_id,
|
||||
auth_context.api_key_is_standalone,
|
||||
wallet.as_ref(),
|
||||
)
|
||||
.await;
|
||||
let today_payload = match if auth_context.api_key_is_standalone {
|
||||
build_wallet_live_today_usage_payload_for_api_key(state, &auth_context.api_key_id).await
|
||||
} else {
|
||||
build_wallet_live_today_usage_payload_for_user(state, &auth_context.user_id).await
|
||||
} {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return Some(build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
err,
|
||||
false,
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let unlimited = json_bool(&wallet_payload, "unlimited");
|
||||
let remaining = json_f64(&wallet_payload, "total_available_balance")
|
||||
.or_else(|| json_f64(&wallet_payload, "wallet_balance"));
|
||||
let used_today = today_payload
|
||||
.as_ref()
|
||||
.and_then(|payload| json_f64(payload, "total_cost"))
|
||||
.unwrap_or(0.0);
|
||||
let mut extra = build_ccswitch_usage_extra(&wallet_payload, today_payload.as_ref());
|
||||
if extra.is_empty() && unlimited {
|
||||
extra = "无限额度".to_string();
|
||||
}
|
||||
|
||||
Some(
|
||||
Json(json!({
|
||||
"is_valid": true,
|
||||
"plan_name": if unlimited { "Aether Unlimited" } else { "Aether" },
|
||||
"remaining": remaining.map(|value| round_to(value.max(0.0), 6)),
|
||||
"used": round_to(used_today.max(0.0), 6),
|
||||
"unit": wallet_payload
|
||||
.get("currency")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("USD"),
|
||||
"extra": extra,
|
||||
"unlimited": unlimited,
|
||||
"wallet": wallet_payload,
|
||||
"today": today_payload,
|
||||
}))
|
||||
.into_response(),
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::{
|
||||
auth_password_policy_level, build_auth_error_response, build_auth_wallet_summary_payload,
|
||||
decrypt_catalog_secret_with_fallbacks, encrypt_catalog_secret_with_fallbacks, handle_auth_me,
|
||||
auth_password_policy_level, base_url_from_request, build_auth_error_response,
|
||||
build_auth_wallet_summary_payload, decrypt_catalog_secret_with_fallbacks,
|
||||
encrypt_catalog_secret_with_fallbacks, handle_auth_me,
|
||||
handle_users_me_api_key_install_session_create, query_param_optional_bool, query_param_value,
|
||||
resolve_authenticated_local_user, sanitize_public_model_config_for_user, unix_secs_to_rfc3339,
|
||||
users_me_api_key_install_sessions_path_matches, validate_auth_register_password, AppState,
|
||||
|
||||
@@ -12,8 +12,9 @@ use crate::handlers::shared::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
auth_password_policy_level, build_auth_error_response, resolve_authenticated_local_user,
|
||||
validate_auth_register_password, AppState, GatewayPublicRequestContext,
|
||||
auth_password_policy_level, base_url_from_request, build_auth_error_response,
|
||||
resolve_authenticated_local_user, validate_auth_register_password, AppState,
|
||||
GatewayPublicRequestContext,
|
||||
};
|
||||
|
||||
const USERS_ME_PROFILE_STORAGE_UNAVAILABLE_DETAIL: &str = "用户资料存储暂不可用";
|
||||
@@ -40,6 +41,30 @@ fn normalize_users_me_optional_non_empty_string(value: Option<String>) -> Option
|
||||
value.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
pub(super) async fn handle_users_me_client_config_get(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
headers: &http::HeaderMap,
|
||||
) -> Response<Body> {
|
||||
if let Err(response) = resolve_authenticated_local_user(state, request_context, headers).await {
|
||||
return response;
|
||||
}
|
||||
|
||||
let site_name = state
|
||||
.read_system_config_json_value("site_name")
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|value| value.as_str().map(ToOwned::to_owned))
|
||||
.unwrap_or_else(|| "Aether".to_string());
|
||||
|
||||
Json(json!({
|
||||
"base_url": base_url_from_request(headers, request_context),
|
||||
"site_name": site_name,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) async fn handle_users_me_detail_put(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
|
||||
@@ -7,20 +7,20 @@ use super::{
|
||||
handle_users_me_api_key_install_session_create, handle_users_me_api_key_patch,
|
||||
handle_users_me_api_key_providers_put, handle_users_me_api_key_update,
|
||||
handle_users_me_api_keys_get, handle_users_me_available_models,
|
||||
handle_users_me_delete_other_sessions, handle_users_me_delete_session,
|
||||
handle_users_me_detail_put, handle_users_me_endpoint_status_get,
|
||||
handle_users_me_management_token_create, handle_users_me_management_token_delete,
|
||||
handle_users_me_management_token_detail_get, handle_users_me_management_token_regenerate,
|
||||
handle_users_me_management_token_toggle, handle_users_me_management_token_update,
|
||||
handle_users_me_management_tokens_list, handle_users_me_model_capabilities_get,
|
||||
handle_users_me_model_capabilities_put, handle_users_me_password_patch,
|
||||
handle_users_me_preferences_get, handle_users_me_preferences_put,
|
||||
handle_users_me_providers_get, handle_users_me_referral_get, handle_users_me_sessions_get,
|
||||
handle_users_me_update_session, handle_users_me_usage_active_get, handle_users_me_usage_get,
|
||||
handle_users_me_usage_heatmap_get, handle_users_me_usage_interval_timeline_get,
|
||||
users_me_api_key_capabilities_path_matches, users_me_api_key_detail_path_matches,
|
||||
users_me_api_key_install_sessions_path_matches, users_me_api_key_providers_path_matches,
|
||||
users_me_management_token_detail_path_matches,
|
||||
handle_users_me_client_config_get, handle_users_me_delete_other_sessions,
|
||||
handle_users_me_delete_session, handle_users_me_detail_put,
|
||||
handle_users_me_endpoint_status_get, handle_users_me_management_token_create,
|
||||
handle_users_me_management_token_delete, handle_users_me_management_token_detail_get,
|
||||
handle_users_me_management_token_regenerate, handle_users_me_management_token_toggle,
|
||||
handle_users_me_management_token_update, handle_users_me_management_tokens_list,
|
||||
handle_users_me_model_capabilities_get, handle_users_me_model_capabilities_put,
|
||||
handle_users_me_password_patch, handle_users_me_preferences_get,
|
||||
handle_users_me_preferences_put, handle_users_me_providers_get, handle_users_me_referral_get,
|
||||
handle_users_me_sessions_get, handle_users_me_update_session, handle_users_me_usage_active_get,
|
||||
handle_users_me_usage_get, handle_users_me_usage_heatmap_get,
|
||||
handle_users_me_usage_interval_timeline_get, users_me_api_key_capabilities_path_matches,
|
||||
users_me_api_key_detail_path_matches, users_me_api_key_install_sessions_path_matches,
|
||||
users_me_api_key_providers_path_matches, users_me_management_token_detail_path_matches,
|
||||
users_me_management_token_regenerate_path_matches,
|
||||
users_me_management_token_toggle_path_matches, users_me_management_tokens_root,
|
||||
users_me_session_detail_path_matches, AppState, GatewayPublicRequestContext,
|
||||
@@ -220,6 +220,9 @@ pub(crate) async fn maybe_build_local_users_me_response(
|
||||
{
|
||||
Some(handle_users_me_available_models(state, request_context, headers).await)
|
||||
}
|
||||
Some("client_config") if request_context.request_path == "/api/users/me/client-config" => {
|
||||
Some(handle_users_me_client_config_get(state, request_context, headers).await)
|
||||
}
|
||||
Some("model_capabilities")
|
||||
if request_context.request_path == "/api/users/me/model-capabilities" =>
|
||||
{
|
||||
|
||||
@@ -28,12 +28,16 @@ mod redeem;
|
||||
#[path = "wallet/refunds.rs"]
|
||||
mod refunds;
|
||||
use self::flow::handle_wallet_flow;
|
||||
pub(in crate::handlers::public::support) use self::reads::build_wallet_balance_payload_for_user;
|
||||
pub(in crate::handlers::public::support) use self::reads::{
|
||||
build_wallet_balance_payload_for_auth_scope, build_wallet_balance_payload_for_user,
|
||||
build_wallet_live_today_usage_payload_for_api_key,
|
||||
build_wallet_live_today_usage_payload_for_user,
|
||||
};
|
||||
use self::reads::{
|
||||
build_wallet_daily_usage_payload, build_wallet_live_today_usage_payload_for_user,
|
||||
build_wallet_payload, build_wallet_zero_today_entry, handle_wallet_balance,
|
||||
handle_wallet_today_cost, handle_wallet_transactions, parse_wallet_limit, parse_wallet_offset,
|
||||
wallet_fixed_offset, wallet_transaction_payload_from_record,
|
||||
build_wallet_daily_usage_payload, build_wallet_payload, build_wallet_zero_today_entry,
|
||||
handle_wallet_balance, handle_wallet_today_cost, handle_wallet_transactions,
|
||||
parse_wallet_limit, parse_wallet_offset, wallet_fixed_offset,
|
||||
wallet_transaction_payload_from_record,
|
||||
};
|
||||
pub(crate) use self::recharge::{direct_gateway_channels, sanitize_wallet_gateway_response};
|
||||
use self::recharge::{
|
||||
|
||||
@@ -53,16 +53,41 @@ pub(in crate::handlers::public::support) async fn build_wallet_balance_payload_f
|
||||
state: &AppState,
|
||||
user_id: &str,
|
||||
wallet: Option<&aether_data::repository::wallet::StoredWalletSnapshot>,
|
||||
) -> serde_json::Value {
|
||||
build_wallet_balance_payload_for_quota_user(state, Some(user_id), wallet).await
|
||||
}
|
||||
|
||||
pub(in crate::handlers::public::support) async fn build_wallet_balance_payload_for_auth_scope(
|
||||
state: &AppState,
|
||||
user_id: &str,
|
||||
api_key_is_standalone: bool,
|
||||
wallet: Option<&aether_data::repository::wallet::StoredWalletSnapshot>,
|
||||
) -> serde_json::Value {
|
||||
let quota_user_id = if api_key_is_standalone {
|
||||
None
|
||||
} else {
|
||||
Some(user_id)
|
||||
};
|
||||
build_wallet_balance_payload_for_quota_user(state, quota_user_id, wallet).await
|
||||
}
|
||||
|
||||
async fn build_wallet_balance_payload_for_quota_user(
|
||||
state: &AppState,
|
||||
quota_user_id: Option<&str>,
|
||||
wallet: Option<&aether_data::repository::wallet::StoredWalletSnapshot>,
|
||||
) -> serde_json::Value {
|
||||
let mut payload = build_wallet_balance_payload(wallet);
|
||||
let wallet_balance = wallet
|
||||
.map(|value| value.balance + value.gift_balance)
|
||||
.unwrap_or(0.0);
|
||||
let daily_quota = state
|
||||
.find_user_daily_quota_availability(user_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let daily_quota = match quota_user_id {
|
||||
Some(user_id) => state
|
||||
.find_user_daily_quota_availability(user_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten(),
|
||||
None => None,
|
||||
};
|
||||
let (has_active_daily_quota, total_quota_usd, used_usd, remaining_usd, allow_wallet_overage) =
|
||||
daily_quota
|
||||
.map(|quota| {
|
||||
@@ -213,9 +238,10 @@ pub(super) fn build_wallet_zero_today_entry() -> serde_json::Value {
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) async fn build_wallet_live_today_usage_payload_for_user(
|
||||
async fn build_wallet_live_today_usage_payload_for_auth_scope(
|
||||
state: &AppState,
|
||||
user_id: &str,
|
||||
user_id: Option<&str>,
|
||||
api_key_id: Option<&str>,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
if !state.has_usage_data_reader() {
|
||||
return Ok(None);
|
||||
@@ -225,7 +251,8 @@ pub(super) async fn build_wallet_live_today_usage_payload_for_user(
|
||||
.summarize_usage_settled_cost(&UsageSettledCostSummaryQuery {
|
||||
created_from_unix_secs: start_unix_secs,
|
||||
created_until_unix_secs: end_unix_secs,
|
||||
user_id: Some(user_id.to_string()),
|
||||
user_id: user_id.map(ToOwned::to_owned),
|
||||
api_key_id: api_key_id.map(ToOwned::to_owned),
|
||||
})
|
||||
.await
|
||||
.map_err(|err| format!("wallet today cost lookup failed: {err:?}"))?;
|
||||
@@ -250,6 +277,20 @@ pub(super) async fn build_wallet_live_today_usage_payload_for_user(
|
||||
)))
|
||||
}
|
||||
|
||||
pub(in crate::handlers::public::support) async fn build_wallet_live_today_usage_payload_for_user(
|
||||
state: &AppState,
|
||||
user_id: &str,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
build_wallet_live_today_usage_payload_for_auth_scope(state, Some(user_id), None).await
|
||||
}
|
||||
|
||||
pub(in crate::handlers::public::support) async fn build_wallet_live_today_usage_payload_for_api_key(
|
||||
state: &AppState,
|
||||
api_key_id: &str,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
build_wallet_live_today_usage_payload_for_auth_scope(state, None, Some(api_key_id)).await
|
||||
}
|
||||
|
||||
pub(super) fn wallet_transaction_payload_from_record(
|
||||
record: &aether_data::repository::wallet::StoredAdminWalletTransaction,
|
||||
) -> serde_json::Value {
|
||||
|
||||
Reference in New Issue
Block a user