mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
Merge pull request #387 from wendaochangsheng/codex/chatgpt-web-access-token-import
feat: 支持 ChatGPT Web Access Token 导入
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
use super::super::token_import::build_codex_access_token_import_auth_config;
|
||||
use super::super::token_import::{
|
||||
build_provider_access_token_import_auth_config, provider_type_supports_access_token_import,
|
||||
};
|
||||
use super::kiro_import::execute_admin_provider_oauth_kiro_batch_import;
|
||||
use super::parse::{
|
||||
apply_admin_provider_oauth_batch_import_hints, extract_admin_provider_oauth_batch_error_detail,
|
||||
@@ -108,14 +110,16 @@ async fn resolve_admin_provider_oauth_batch_import_tokens(
|
||||
Ok(payload) => payload,
|
||||
Err(response) => {
|
||||
let detail = extract_admin_provider_oauth_batch_error_detail(response).await;
|
||||
if provider_type.eq_ignore_ascii_case("codex") {
|
||||
if provider_type_supports_access_token_import(provider_type) {
|
||||
if let Some(access_token) = access_token {
|
||||
let (auth_config, expires_at) = build_codex_access_token_import_auth_config(
|
||||
access_token,
|
||||
Some(refresh_token),
|
||||
entry.expires_at,
|
||||
Some(detail.as_str()),
|
||||
);
|
||||
let (auth_config, expires_at) =
|
||||
build_provider_access_token_import_auth_config(
|
||||
provider_type,
|
||||
access_token,
|
||||
Some(refresh_token),
|
||||
entry.expires_at,
|
||||
Some(detail.as_str()),
|
||||
);
|
||||
return Ok(AdminProviderOAuthResolvedBatchImport {
|
||||
access_token: access_token.to_string(),
|
||||
auth_config,
|
||||
@@ -147,11 +151,16 @@ async fn resolve_admin_provider_oauth_batch_import_tokens(
|
||||
}
|
||||
|
||||
if let Some(access_token) = access_token {
|
||||
if !provider_type.eq_ignore_ascii_case("codex") {
|
||||
return Err("Access Token 导入仅支持 Codex Provider".to_string());
|
||||
if !provider_type_supports_access_token_import(provider_type) {
|
||||
return Err("Access Token 导入仅支持 Codex / ChatGPT Web Provider".to_string());
|
||||
}
|
||||
let (auth_config, expires_at) =
|
||||
build_codex_access_token_import_auth_config(access_token, None, entry.expires_at, None);
|
||||
let (auth_config, expires_at) = build_provider_access_token_import_auth_config(
|
||||
provider_type,
|
||||
access_token,
|
||||
None,
|
||||
entry.expires_at,
|
||||
None,
|
||||
);
|
||||
return Ok(AdminProviderOAuthResolvedBatchImport {
|
||||
access_token: access_token.to_string(),
|
||||
auth_config,
|
||||
|
||||
@@ -204,7 +204,10 @@ pub(super) fn apply_admin_provider_oauth_batch_import_hints(
|
||||
entry: &AdminProviderOAuthBatchImportEntry,
|
||||
auth_config: &mut serde_json::Map<String, serde_json::Value>,
|
||||
) {
|
||||
if !provider_type.eq_ignore_ascii_case("codex") {
|
||||
if !matches!(
|
||||
provider_type.trim().to_ascii_lowercase().as_str(),
|
||||
"codex" | "chatgpt_web"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if let Some(account_id) = entry.account_id.as_ref() {
|
||||
|
||||
@@ -15,7 +15,8 @@ use super::super::state::{
|
||||
json_u64_value,
|
||||
};
|
||||
use super::token_import::{
|
||||
build_codex_access_token_import_auth_config, normalize_single_import_tokens,
|
||||
build_provider_access_token_import_auth_config, normalize_single_import_tokens,
|
||||
provider_type_supports_access_token_import,
|
||||
};
|
||||
use crate::handlers::admin::provider::shared::paths::admin_provider_oauth_import_provider_id;
|
||||
use crate::handlers::admin::request::{
|
||||
@@ -43,9 +44,15 @@ fn import_payload_string(
|
||||
snake_case: &str,
|
||||
camel_case: &str,
|
||||
) -> Option<String> {
|
||||
payload
|
||||
.get(snake_case)
|
||||
.or_else(|| payload.get(camel_case))
|
||||
import_payload_string_any(payload, &[snake_case, camel_case])
|
||||
}
|
||||
|
||||
fn import_payload_string_any(
|
||||
payload: &serde_json::Map<String, serde_json::Value>,
|
||||
keys: &[&str],
|
||||
) -> Option<String> {
|
||||
keys.iter()
|
||||
.find_map(|key| payload.get(*key))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
@@ -60,6 +67,59 @@ fn import_payload_u64(
|
||||
json_u64_value(payload.get(snake_case).or_else(|| payload.get(camel_case)))
|
||||
}
|
||||
|
||||
fn apply_single_import_hints(
|
||||
provider_type: &str,
|
||||
payload: &serde_json::Map<String, serde_json::Value>,
|
||||
auth_config: &mut serde_json::Map<String, serde_json::Value>,
|
||||
) {
|
||||
if !provider_type_supports_access_token_import(provider_type) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (target, keys) in [
|
||||
("email", &["email", "oauth_email"][..]),
|
||||
(
|
||||
"account_id",
|
||||
&[
|
||||
"account_id",
|
||||
"accountId",
|
||||
"chatgpt_account_id",
|
||||
"chatgptAccountId",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
"account_user_id",
|
||||
&[
|
||||
"account_user_id",
|
||||
"accountUserId",
|
||||
"chatgpt_account_user_id",
|
||||
"chatgptAccountUserId",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
"plan_type",
|
||||
&[
|
||||
"plan_type",
|
||||
"planType",
|
||||
"chatgpt_plan_type",
|
||||
"chatgptPlanType",
|
||||
][..],
|
||||
),
|
||||
(
|
||||
"user_id",
|
||||
&["user_id", "userId", "chatgpt_user_id", "chatgptUserId"][..],
|
||||
),
|
||||
("account_name", &["account_name", "accountName"][..]),
|
||||
] {
|
||||
let Some(value) = import_payload_string_any(payload, keys) else {
|
||||
continue;
|
||||
};
|
||||
auth_config
|
||||
.entry(target.to_string())
|
||||
.or_insert_with(|| json!(value));
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_admin_provider_oauth_single_import_tokens(
|
||||
state: &AdminAppState<'_>,
|
||||
template: AdminProviderOAuthTemplate,
|
||||
@@ -83,17 +143,19 @@ async fn resolve_admin_provider_oauth_single_import_tokens(
|
||||
{
|
||||
Ok(payload) => payload,
|
||||
Err(response) => {
|
||||
if provider_type.eq_ignore_ascii_case("codex") {
|
||||
if provider_type_supports_access_token_import(provider_type) {
|
||||
if let Some(access_token) = access_token
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
let (auth_config, expires_at) = build_codex_access_token_import_auth_config(
|
||||
access_token,
|
||||
Some(refresh_token),
|
||||
imported_expires_at,
|
||||
Some("Refresh Token 验证失败,已回退为 Access Token 导入"),
|
||||
);
|
||||
let (auth_config, expires_at) =
|
||||
build_provider_access_token_import_auth_config(
|
||||
provider_type,
|
||||
access_token,
|
||||
Some(refresh_token),
|
||||
imported_expires_at,
|
||||
Some("Refresh Token 验证失败,已回退为 Access Token 导入"),
|
||||
);
|
||||
return Ok(AdminProviderOAuthSingleImportTokens {
|
||||
access_token: access_token.to_string(),
|
||||
auth_config,
|
||||
@@ -135,15 +197,20 @@ async fn resolve_admin_provider_oauth_single_import_tokens(
|
||||
"Refresh Token 或 Access Token 不能为空",
|
||||
));
|
||||
};
|
||||
if !provider_type.eq_ignore_ascii_case("codex") {
|
||||
if !provider_type_supports_access_token_import(provider_type) {
|
||||
return Err(build_internal_control_error_response(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
"Access Token 导入仅支持 Codex Provider",
|
||||
"Access Token 导入仅支持 Codex / ChatGPT Web Provider",
|
||||
));
|
||||
}
|
||||
|
||||
let (auth_config, expires_at) =
|
||||
build_codex_access_token_import_auth_config(access_token, None, imported_expires_at, None);
|
||||
let (auth_config, expires_at) = build_provider_access_token_import_auth_config(
|
||||
provider_type,
|
||||
access_token,
|
||||
None,
|
||||
imported_expires_at,
|
||||
None,
|
||||
);
|
||||
Ok(AdminProviderOAuthSingleImportTokens {
|
||||
access_token: access_token.to_string(),
|
||||
auth_config,
|
||||
@@ -266,9 +333,10 @@ pub(super) async fn handle_admin_provider_oauth_import_refresh_token(
|
||||
};
|
||||
let AdminProviderOAuthSingleImportTokens {
|
||||
access_token,
|
||||
auth_config,
|
||||
mut auth_config,
|
||||
expires_at,
|
||||
} = resolved_import;
|
||||
apply_single_import_hints(&provider_type, &raw_payload, &mut auth_config);
|
||||
let has_refresh_token = auth_config
|
||||
.get("refresh_token")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
|
||||
@@ -95,7 +95,15 @@ pub(super) fn decode_access_token_expires_at(access_token: &str) -> Option<u64>
|
||||
json_u64_value(claims.get("exp"))
|
||||
}
|
||||
|
||||
pub(super) fn build_codex_access_token_import_auth_config(
|
||||
pub(super) fn provider_type_supports_access_token_import(provider_type: &str) -> bool {
|
||||
matches!(
|
||||
provider_type.trim().to_ascii_lowercase().as_str(),
|
||||
"codex" | "chatgpt_web"
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn build_provider_access_token_import_auth_config(
|
||||
provider_type: &str,
|
||||
access_token: &str,
|
||||
refresh_token: Option<&str>,
|
||||
imported_expires_at: Option<u64>,
|
||||
@@ -106,7 +114,7 @@ pub(super) fn build_codex_access_token_import_auth_config(
|
||||
"token_type": "Bearer",
|
||||
});
|
||||
let (mut auth_config, _, _, _) =
|
||||
build_provider_oauth_auth_config_from_token_payload("codex", &token_payload);
|
||||
build_provider_oauth_auth_config_from_token_payload(provider_type, &token_payload);
|
||||
|
||||
let refresh_token = refresh_token
|
||||
.map(str::trim)
|
||||
@@ -140,7 +148,7 @@ pub(super) fn build_codex_access_token_import_auth_config(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
build_codex_access_token_import_auth_config, decode_access_token_expires_at,
|
||||
build_provider_access_token_import_auth_config, decode_access_token_expires_at,
|
||||
looks_like_access_token, normalize_single_import_tokens,
|
||||
};
|
||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||
@@ -178,7 +186,7 @@ mod tests {
|
||||
}));
|
||||
|
||||
let (auth_config, expires_at) =
|
||||
build_codex_access_token_import_auth_config(&token, None, None, None);
|
||||
build_provider_access_token_import_auth_config("codex", &token, None, None, None);
|
||||
|
||||
assert_eq!(expires_at, Some(2_000_000_000));
|
||||
assert_eq!(decode_access_token_expires_at(&token), Some(2_000_000_000));
|
||||
@@ -197,8 +205,13 @@ mod tests {
|
||||
"aud": ["https://api.openai.com/v1"]
|
||||
}));
|
||||
|
||||
let (auth_config, expires_at) =
|
||||
build_codex_access_token_import_auth_config(&token, None, Some(2_100_000_000), None);
|
||||
let (auth_config, expires_at) = build_provider_access_token_import_auth_config(
|
||||
"codex",
|
||||
&token,
|
||||
None,
|
||||
Some(2_100_000_000),
|
||||
None,
|
||||
);
|
||||
|
||||
assert_eq!(expires_at, Some(2_100_000_000));
|
||||
assert_eq!(
|
||||
@@ -206,4 +219,35 @@ mod tests {
|
||||
Some(&json!(2_100_000_000u64))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_chatgpt_web_temporary_auth_config_from_access_token() {
|
||||
let token = unsigned_jwt(json!({
|
||||
"exp": 2_000_000_000u64,
|
||||
"https://api.openai.com/profile": {
|
||||
"email": "image@example.com"
|
||||
},
|
||||
"https://api.openai.com/auth": {
|
||||
"chatgpt_account_id": "acct-image-123"
|
||||
},
|
||||
}));
|
||||
|
||||
let (auth_config, expires_at) =
|
||||
build_provider_access_token_import_auth_config("chatgpt_web", &token, None, None, None);
|
||||
|
||||
assert_eq!(expires_at, Some(2_000_000_000));
|
||||
assert_eq!(
|
||||
auth_config.get("provider_type"),
|
||||
Some(&json!("chatgpt_web"))
|
||||
);
|
||||
assert_eq!(auth_config.get("email"), Some(&json!("image@example.com")));
|
||||
assert_eq!(
|
||||
auth_config.get("account_id"),
|
||||
Some(&json!("acct-image-123"))
|
||||
);
|
||||
assert_eq!(
|
||||
auth_config.get("access_token_import_temporary"),
|
||||
Some(&json!(true))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,14 @@ fn normalize_provider_oauth_identity_value(value: Option<&serde_json::Value>) ->
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn is_codex_provider_oauth_provider_type(value: Option<&serde_json::Value>) -> bool {
|
||||
fn is_openai_provider_oauth_provider_type(value: Option<&serde_json::Value>) -> bool {
|
||||
value
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.is_some_and(|provider_type| provider_type.eq_ignore_ascii_case("codex"))
|
||||
.is_some_and(|provider_type| {
|
||||
provider_type.eq_ignore_ascii_case("codex")
|
||||
|| provider_type.eq_ignore_ascii_case("chatgpt_web")
|
||||
})
|
||||
}
|
||||
|
||||
fn match_codex_provider_oauth_identity(
|
||||
@@ -39,8 +42,8 @@ fn match_codex_provider_oauth_identity(
|
||||
) -> Option<bool> {
|
||||
let new_provider_type = new_auth_config.get("provider_type");
|
||||
let existing_provider_type = existing_auth_config.get("provider_type");
|
||||
if !is_codex_provider_oauth_provider_type(new_provider_type)
|
||||
&& !is_codex_provider_oauth_provider_type(existing_provider_type)
|
||||
if !is_openai_provider_oauth_provider_type(new_provider_type)
|
||||
&& !is_openai_provider_oauth_provider_type(existing_provider_type)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -109,8 +112,8 @@ fn is_codex_cross_plan_group_non_duplicate(
|
||||
) -> bool {
|
||||
let new_provider_type = new_auth_config.get("provider_type");
|
||||
let existing_provider_type = existing_auth_config.get("provider_type");
|
||||
if !is_codex_provider_oauth_provider_type(new_provider_type)
|
||||
&& !is_codex_provider_oauth_provider_type(existing_provider_type)
|
||||
if !is_openai_provider_oauth_provider_type(new_provider_type)
|
||||
&& !is_openai_provider_oauth_provider_type(existing_provider_type)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user