feat(settings): add ChatGPT OAuth device code login for Codex CLI
Add OAuth device code flow for Codex CLI official subscription auth, allowing users to log in with their ChatGPT account directly from the agent settings page without using the terminal. - Backend: two new endpoints (codex_request_device_code, codex_poll_device_code) that handle the OpenAI OAuth device code flow and return tokens to frontend - Frontend: login UI with verification URL, copyable user code, polling status, 15-minute timeout, and auto-save via existing persistEnv/persistConfig path - Auth.json written in Codex CLI compatible format (nested tokens, account_id, last_refresh) so codex-acp can use OAuth tokens directly - Show logged-in status and re-login option when tokens are present - Remove auth.json textarea from Codex settings UI - i18n: all 10 languages updated with new login-related keys
This commit is contained in:
@@ -3077,3 +3077,219 @@ pub async fn opencode_uninstall_plugin(
|
|||||||
) -> Result<PluginCheckSummary, AcpError> {
|
) -> Result<PluginCheckSummary, AcpError> {
|
||||||
opencode_uninstall_plugin_core(name).await
|
opencode_uninstall_plugin_core(name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Codex Device Code OAuth ───
|
||||||
|
|
||||||
|
const CODEX_OAUTH_ISSUER: &str = "https://auth.openai.com";
|
||||||
|
const CODEX_OAUTH_CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CodexDeviceCodeResponse {
|
||||||
|
pub user_code: String,
|
||||||
|
pub verification_url: String,
|
||||||
|
pub device_auth_id: String,
|
||||||
|
pub interval: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CodexDeviceCodePollResult {
|
||||||
|
pub status: String,
|
||||||
|
pub message: Option<String>,
|
||||||
|
pub id_token: Option<String>,
|
||||||
|
pub access_token: Option<String>,
|
||||||
|
pub refresh_token: Option<String>,
|
||||||
|
pub account_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DeviceCodeUserCodeResp {
|
||||||
|
device_auth_id: String,
|
||||||
|
#[serde(alias = "usercode")]
|
||||||
|
user_code: String,
|
||||||
|
#[serde(default = "default_interval", deserialize_with = "deserialize_interval")]
|
||||||
|
interval: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_interval() -> u64 {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_jwt_account_id(jwt: &str) -> Option<String> {
|
||||||
|
let payload = jwt.split('.').nth(1)?;
|
||||||
|
let decoded = base64::Engine::decode(
|
||||||
|
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
let value: serde_json::Value = serde_json::from_slice(&decoded).ok()?;
|
||||||
|
value
|
||||||
|
.get("https://api.openai.com/auth")
|
||||||
|
.and_then(|auth| auth.get("chatgpt_account_id"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_interval<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
use serde::de;
|
||||||
|
let value = serde_json::Value::deserialize(deserializer)?;
|
||||||
|
match &value {
|
||||||
|
serde_json::Value::Number(n) => n.as_u64().ok_or_else(|| {
|
||||||
|
de::Error::custom(format!("invalid interval number: {n}"))
|
||||||
|
}),
|
||||||
|
serde_json::Value::String(s) => s.trim().parse::<u64>().map_err(de::Error::custom),
|
||||||
|
_ => Err(de::Error::custom(format!("unexpected interval type: {value}"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DeviceCodeTokenResp {
|
||||||
|
authorization_code: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
code_challenge: String,
|
||||||
|
code_verifier: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct OAuthTokenResp {
|
||||||
|
id_token: String,
|
||||||
|
access_token: String,
|
||||||
|
refresh_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn codex_request_device_code_core()
|
||||||
|
-> Result<CodexDeviceCodeResponse, AcpError>
|
||||||
|
{
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let url = format!("{CODEX_OAUTH_ISSUER}/api/accounts/deviceauth/usercode");
|
||||||
|
let body = serde_json::json!({ "client_id": CODEX_OAUTH_CLIENT_ID });
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(&url)
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AcpError::protocol(format!("device code request failed: {e}")))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
let status = resp.status();
|
||||||
|
let text = resp.text().await.unwrap_or_default();
|
||||||
|
return Err(AcpError::protocol(format!(
|
||||||
|
"device code request returned {status}: {text}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw_body = resp
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AcpError::protocol(format!("read device code response failed: {e}")))?;
|
||||||
|
let uc: DeviceCodeUserCodeResp = serde_json::from_str(&raw_body)
|
||||||
|
.map_err(|e| AcpError::protocol(format!("parse device code response failed: {e} | body: {raw_body}")))?;
|
||||||
|
|
||||||
|
Ok(CodexDeviceCodeResponse {
|
||||||
|
user_code: uc.user_code,
|
||||||
|
verification_url: format!("{CODEX_OAUTH_ISSUER}/codex/device"),
|
||||||
|
device_auth_id: uc.device_auth_id,
|
||||||
|
interval: uc.interval,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||||
|
pub async fn codex_request_device_code()
|
||||||
|
-> Result<CodexDeviceCodeResponse, AcpError>
|
||||||
|
{
|
||||||
|
codex_request_device_code_core().await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||||
|
pub async fn codex_poll_device_code(
|
||||||
|
device_auth_id: String,
|
||||||
|
user_code: String,
|
||||||
|
) -> Result<CodexDeviceCodePollResult, AcpError> {
|
||||||
|
codex_poll_device_code_core(device_auth_id, user_code).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn codex_poll_device_code_core(
|
||||||
|
device_auth_id: String,
|
||||||
|
user_code: String,
|
||||||
|
) -> Result<CodexDeviceCodePollResult, AcpError> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let poll_url = format!("{CODEX_OAUTH_ISSUER}/api/accounts/deviceauth/token");
|
||||||
|
let poll_body = serde_json::json!({
|
||||||
|
"device_auth_id": device_auth_id,
|
||||||
|
"user_code": user_code,
|
||||||
|
});
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post(&poll_url)
|
||||||
|
.json(&poll_body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AcpError::protocol(format!("device code poll failed: {e}")))?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Ok(CodexDeviceCodePollResult {
|
||||||
|
status: "pending".into(),
|
||||||
|
message: None,
|
||||||
|
id_token: None,
|
||||||
|
access_token: None,
|
||||||
|
refresh_token: None,
|
||||||
|
account_id: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let code_resp: DeviceCodeTokenResp = resp
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AcpError::protocol(format!("parse poll response failed: {e}")))?;
|
||||||
|
|
||||||
|
let redirect_uri = format!("{CODEX_OAUTH_ISSUER}/deviceauth/callback");
|
||||||
|
let token_url = format!("{CODEX_OAUTH_ISSUER}/oauth/token");
|
||||||
|
|
||||||
|
let token_resp = client
|
||||||
|
.post(&token_url)
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.body(format!(
|
||||||
|
"grant_type=authorization_code&code={}&redirect_uri={}&client_id={}&code_verifier={}",
|
||||||
|
urlencoding::encode(&code_resp.authorization_code),
|
||||||
|
urlencoding::encode(&redirect_uri),
|
||||||
|
urlencoding::encode(CODEX_OAUTH_CLIENT_ID),
|
||||||
|
urlencoding::encode(&code_resp.code_verifier),
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AcpError::protocol(format!("token exchange failed: {e}")))?;
|
||||||
|
|
||||||
|
if !token_resp.status().is_success() {
|
||||||
|
let status = token_resp.status();
|
||||||
|
let text = token_resp.text().await.unwrap_or_default();
|
||||||
|
return Ok(CodexDeviceCodePollResult {
|
||||||
|
status: "error".into(),
|
||||||
|
message: Some(format!("token exchange returned {status}: {text}")),
|
||||||
|
id_token: None,
|
||||||
|
access_token: None,
|
||||||
|
refresh_token: None,
|
||||||
|
account_id: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokens: OAuthTokenResp = token_resp
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AcpError::protocol(format!("parse token response failed: {e}")))?;
|
||||||
|
|
||||||
|
let account_id = extract_jwt_account_id(&tokens.id_token).unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(CodexDeviceCodePollResult {
|
||||||
|
status: "success".into(),
|
||||||
|
message: None,
|
||||||
|
id_token: Some(tokens.id_token),
|
||||||
|
access_token: Some(tokens.access_token),
|
||||||
|
refresh_token: Some(tokens.refresh_token),
|
||||||
|
account_id: Some(account_id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -393,6 +393,8 @@ mod tauri_app {
|
|||||||
acp_commands::opencode_list_plugins,
|
acp_commands::opencode_list_plugins,
|
||||||
acp_commands::opencode_install_plugins,
|
acp_commands::opencode_install_plugins,
|
||||||
acp_commands::opencode_uninstall_plugin,
|
acp_commands::opencode_uninstall_plugin,
|
||||||
|
acp_commands::codex_request_device_code,
|
||||||
|
acp_commands::codex_poll_device_code,
|
||||||
experts_commands::experts_list,
|
experts_commands::experts_list,
|
||||||
experts_commands::experts_list_for_agent,
|
experts_commands::experts_list_for_agent,
|
||||||
experts_commands::experts_get_install_status,
|
experts_commands::experts_get_install_status,
|
||||||
|
|||||||
@@ -591,3 +591,30 @@ pub async fn opencode_uninstall_plugin(
|
|||||||
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
|
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
|
||||||
Ok(Json(result))
|
Ok(Json(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn codex_request_device_code(
|
||||||
|
) -> Result<Json<acp_commands::CodexDeviceCodeResponse>, AppCommandError> {
|
||||||
|
let result = acp_commands::codex_request_device_code_core()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
|
||||||
|
Ok(Json(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CodexPollDeviceCodeParams {
|
||||||
|
pub device_auth_id: String,
|
||||||
|
pub user_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn codex_poll_device_code(
|
||||||
|
Json(params): Json<CodexPollDeviceCodeParams>,
|
||||||
|
) -> Result<Json<acp_commands::CodexDeviceCodePollResult>, AppCommandError> {
|
||||||
|
let result = acp_commands::codex_poll_device_code_core(
|
||||||
|
params.device_auth_id,
|
||||||
|
params.user_code,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?;
|
||||||
|
Ok(Json(result))
|
||||||
|
}
|
||||||
|
|||||||
@@ -459,6 +459,14 @@ pub fn build_router(state: Arc<AppState>, token: String, static_dir: std::path::
|
|||||||
"/opencode_uninstall_plugin",
|
"/opencode_uninstall_plugin",
|
||||||
post(handlers::acp::opencode_uninstall_plugin),
|
post(handlers::acp::opencode_uninstall_plugin),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/codex_request_device_code",
|
||||||
|
post(handlers::acp::codex_request_device_code),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/codex_poll_device_code",
|
||||||
|
post(handlers::acp::codex_poll_device_code),
|
||||||
|
)
|
||||||
// ─── Experts ───
|
// ─── Experts ───
|
||||||
.route("/experts_list", post(handlers::experts::experts_list))
|
.route("/experts_list", post(handlers::experts::experts_list))
|
||||||
.route(
|
.route(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Copy,
|
||||||
Download,
|
Download,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
@@ -75,6 +76,8 @@ import {
|
|||||||
acpUninstallAgent,
|
acpUninstallAgent,
|
||||||
acpUpdateAgentConfig,
|
acpUpdateAgentConfig,
|
||||||
acpUpdateAgentEnv,
|
acpUpdateAgentEnv,
|
||||||
|
codexPollDeviceCode,
|
||||||
|
codexRequestDeviceCode,
|
||||||
listModelProviders,
|
listModelProviders,
|
||||||
} from "@/lib/api"
|
} from "@/lib/api"
|
||||||
import type {
|
import type {
|
||||||
@@ -1550,6 +1553,18 @@ function inferCodexAuthMode(authJsonText: string): CodexAuthMode {
|
|||||||
return "api_key"
|
return "api_key"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasCodexChatgptTokens(authJsonText: string): boolean {
|
||||||
|
const { authObject } = parseCodexAuthJsonObject(authJsonText)
|
||||||
|
if (!authObject) return false
|
||||||
|
const tokens = authObject.tokens as Record<string, unknown> | undefined
|
||||||
|
if (tokens && typeof tokens === "object") {
|
||||||
|
return (
|
||||||
|
typeof tokens.access_token === "string" && tokens.access_token.length > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
function extractCodexImportantValues(
|
function extractCodexImportantValues(
|
||||||
authJsonText: string,
|
authJsonText: string,
|
||||||
configTomlText: string
|
configTomlText: string
|
||||||
@@ -2686,6 +2701,17 @@ export function AcpAgentSettings() {
|
|||||||
const installStream = useAgentInstallStream()
|
const installStream = useAgentInstallStream()
|
||||||
const [streamAgentType, setStreamAgentType] = useState<AgentType | null>(null)
|
const [streamAgentType, setStreamAgentType] = useState<AgentType | null>(null)
|
||||||
const installLogEndRef = useRef<HTMLDivElement | null>(null)
|
const installLogEndRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const [codexDeviceCode, setCodexDeviceCode] = useState<{
|
||||||
|
userCode: string
|
||||||
|
verificationUrl: string
|
||||||
|
deviceAuthId: string
|
||||||
|
interval: number
|
||||||
|
} | null>(null)
|
||||||
|
const [codexLoginStatus, setCodexLoginStatus] = useState<
|
||||||
|
"idle" | "requesting" | "polling" | "success" | "error"
|
||||||
|
>("idle")
|
||||||
|
const [codexLoginError, setCodexLoginError] = useState<string | null>(null)
|
||||||
|
const codexPollCancelledRef = useRef(false)
|
||||||
|
|
||||||
const sortedAgents = useMemo(
|
const sortedAgents = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -3427,13 +3453,8 @@ export function AcpAgentSettings() {
|
|||||||
|
|
||||||
const selectedMissingModelProvider =
|
const selectedMissingModelProvider =
|
||||||
selectedNeedsModelProvider && selectedDraft?.modelProviderId == null
|
selectedNeedsModelProvider && selectedDraft?.modelProviderId == null
|
||||||
const selectedCodexAuthJsonText = selectedDraft?.codexAuthJsonText ?? ""
|
|
||||||
const selectedConfigText = selectedDraft?.configText ?? ""
|
const selectedConfigText = selectedDraft?.configText ?? ""
|
||||||
const selectedOpenCodeAuthJsonText = selectedDraft?.openCodeAuthJsonText ?? ""
|
const selectedOpenCodeAuthJsonText = selectedDraft?.openCodeAuthJsonText ?? ""
|
||||||
const selectedCodexAuthError = useMemo(() => {
|
|
||||||
if (selectedAgentKind !== "codex" || !locale) return null
|
|
||||||
return parseCodexAuthJsonText(selectedCodexAuthJsonText)
|
|
||||||
}, [locale, selectedAgentKind, selectedCodexAuthJsonText])
|
|
||||||
const selectedCodexReasoningEffortOption =
|
const selectedCodexReasoningEffortOption =
|
||||||
selectedAgent?.agent_type === "codex" && selectedDraft
|
selectedAgent?.agent_type === "codex" && selectedDraft
|
||||||
? (CODEX_REASONING_EFFORT_OPTIONS.find(
|
? (CODEX_REASONING_EFFORT_OPTIONS.find(
|
||||||
@@ -4594,31 +4615,6 @@ export function AcpAgentSettings() {
|
|||||||
[handleOpenCodeConfigPatch]
|
[handleOpenCodeConfigPatch]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleCodexAuthJsonTextChange = useCallback(
|
|
||||||
(nextText: string) => {
|
|
||||||
if (!selectedAgent || selectedAgent.agent_type !== "codex") return
|
|
||||||
const important = extractCodexImportantValues(
|
|
||||||
nextText,
|
|
||||||
selectedDraft?.codexConfigTomlText ?? ""
|
|
||||||
)
|
|
||||||
updateSelectedDraft((current) => ({
|
|
||||||
...current,
|
|
||||||
codexAuthMode: inferCodexAuthMode(nextText),
|
|
||||||
codexAuthJsonText: nextText,
|
|
||||||
apiBaseUrl: important.apiBaseUrl,
|
|
||||||
apiKey: important.apiKey ?? current.apiKey,
|
|
||||||
model: important.model,
|
|
||||||
codexModelProvider: important.modelProvider,
|
|
||||||
codexProviderOptions: important.providerOptions,
|
|
||||||
codexReasoningEffort: important.reasoningEffort,
|
|
||||||
codexSupportsWebsockets: important.supportsWebsockets,
|
|
||||||
codexSkills: important.skills,
|
|
||||||
codexServiceTierFast: important.serviceTierFast,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
[selectedAgent, selectedDraft, updateSelectedDraft]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleCodexConfigTomlTextChange = useCallback(
|
const handleCodexConfigTomlTextChange = useCallback(
|
||||||
(nextText: string) => {
|
(nextText: string) => {
|
||||||
if (!selectedAgent || selectedAgent.agent_type !== "codex") return
|
if (!selectedAgent || selectedAgent.agent_type !== "codex") return
|
||||||
@@ -4901,6 +4897,138 @@ export function AcpAgentSettings() {
|
|||||||
[selectedAgent, selectedDraft, updateSelectedDraft]
|
[selectedAgent, selectedDraft, updateSelectedDraft]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleCodexDeviceLogin = useCallback(async () => {
|
||||||
|
setCodexLoginStatus("requesting")
|
||||||
|
setCodexLoginError(null)
|
||||||
|
setCodexDeviceCode(null)
|
||||||
|
codexPollCancelledRef.current = false
|
||||||
|
try {
|
||||||
|
const resp = await codexRequestDeviceCode()
|
||||||
|
setCodexDeviceCode(resp)
|
||||||
|
setCodexLoginStatus("polling")
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err)
|
||||||
|
setCodexLoginError(msg)
|
||||||
|
setCodexLoginStatus("error")
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const cancelCodexDeviceLogin = useCallback(() => {
|
||||||
|
codexPollCancelledRef.current = true
|
||||||
|
setCodexLoginStatus("idle")
|
||||||
|
setCodexDeviceCode(null)
|
||||||
|
setCodexLoginError(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (codexLoginStatus !== "polling" || !codexDeviceCode) return
|
||||||
|
codexPollCancelledRef.current = false
|
||||||
|
const pollInterval = (codexDeviceCode.interval || 5) * 1000
|
||||||
|
const deadline = Date.now() + 15 * 60 * 1000
|
||||||
|
let timer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let active = true
|
||||||
|
|
||||||
|
const poll = async () => {
|
||||||
|
if (!active || codexPollCancelledRef.current) return
|
||||||
|
if (Date.now() > deadline) {
|
||||||
|
setCodexLoginError(t("codex.loginTimeout"))
|
||||||
|
setCodexLoginStatus("error")
|
||||||
|
setCodexDeviceCode(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await codexPollDeviceCode({
|
||||||
|
deviceAuthId: codexDeviceCode.deviceAuthId,
|
||||||
|
userCode: codexDeviceCode.userCode,
|
||||||
|
})
|
||||||
|
if (!active || codexPollCancelledRef.current) return
|
||||||
|
if (result.status === "success") {
|
||||||
|
setCodexLoginStatus("success")
|
||||||
|
setCodexDeviceCode(null)
|
||||||
|
const authJson = JSON.stringify(
|
||||||
|
{
|
||||||
|
auth_mode: "chatgpt",
|
||||||
|
OPENAI_API_KEY: null,
|
||||||
|
tokens: {
|
||||||
|
id_token: result.idToken,
|
||||||
|
access_token: result.accessToken,
|
||||||
|
refresh_token: result.refreshToken,
|
||||||
|
account_id: result.accountId ?? "",
|
||||||
|
},
|
||||||
|
last_refresh: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
updateSelectedDraft((current) => ({
|
||||||
|
...current,
|
||||||
|
codexAuthJsonText: authJson,
|
||||||
|
}))
|
||||||
|
const draft = drafts.codex
|
||||||
|
if (draft) {
|
||||||
|
const codexEnvText =
|
||||||
|
draft.codexAuthMode === "chatgpt_subscription"
|
||||||
|
? patchEnvText(draft.envText, {
|
||||||
|
OPENAI_API_KEY: "",
|
||||||
|
OPENAI_BASE_URL: "",
|
||||||
|
})
|
||||||
|
: draft.envText
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
persistEnv(
|
||||||
|
"codex",
|
||||||
|
draft.enabled,
|
||||||
|
codexEnvText,
|
||||||
|
draft.modelProviderId
|
||||||
|
),
|
||||||
|
persistConfig("codex", draft.configText, {
|
||||||
|
codexAuthJsonText: authJson,
|
||||||
|
codexConfigTomlText: draft.codexConfigTomlText,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err)
|
||||||
|
toast.error(t("codex.loginSaveFailed"), {
|
||||||
|
description: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (result.status === "error") {
|
||||||
|
setCodexLoginError(result.message ?? "Unknown error")
|
||||||
|
setCodexLoginStatus("error")
|
||||||
|
setCodexDeviceCode(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
timer = setTimeout(poll, pollInterval)
|
||||||
|
} catch {
|
||||||
|
if (!active || codexPollCancelledRef.current) return
|
||||||
|
timer = setTimeout(poll, pollInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timer = setTimeout(poll, pollInterval)
|
||||||
|
return () => {
|
||||||
|
active = false
|
||||||
|
if (timer) clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
codexLoginStatus,
|
||||||
|
codexDeviceCode,
|
||||||
|
drafts.codex,
|
||||||
|
persistConfig,
|
||||||
|
persistEnv,
|
||||||
|
updateSelectedDraft,
|
||||||
|
t,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedAgent?.agent_type !== "codex" && codexLoginStatus !== "idle") {
|
||||||
|
cancelCodexDeviceLogin()
|
||||||
|
}
|
||||||
|
}, [selectedAgent, codexLoginStatus, cancelCodexDeviceLogin])
|
||||||
|
|
||||||
if (loadingAgents) {
|
if (loadingAgents) {
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
|
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
|
||||||
@@ -5304,6 +5432,108 @@ export function AcpAgentSettings() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedDraft.codexAuthMode === "chatgpt_subscription" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{hasCodexChatgptTokens(
|
||||||
|
selectedDraft.codexAuthJsonText
|
||||||
|
) &&
|
||||||
|
codexLoginStatus !== "polling" &&
|
||||||
|
codexLoginStatus !== "requesting" && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-green-600">
|
||||||
|
<CheckCircle2 className="h-3 w-3" />
|
||||||
|
{t("codex.loggedIn")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{codexLoginStatus === "idle" && (
|
||||||
|
<Button
|
||||||
|
onClick={handleCodexDeviceLogin}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{hasCodexChatgptTokens(
|
||||||
|
selectedDraft.codexAuthJsonText
|
||||||
|
)
|
||||||
|
? t("codex.loginRelogin")
|
||||||
|
: t("codex.loginButton")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{codexLoginStatus === "requesting" && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
{t("codex.loginRequesting")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{codexLoginStatus === "polling" && codexDeviceCode && (
|
||||||
|
<div className="space-y-2 rounded-md border p-3">
|
||||||
|
<p className="text-xs">{t("codex.loginStep1")}</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-xs text-primary underline cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
openUrl(codexDeviceCode.verificationUrl)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{codexDeviceCode.verificationUrl}
|
||||||
|
</button>
|
||||||
|
<p className="text-xs mt-1">
|
||||||
|
{t("codex.loginStep2")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<code className="rounded bg-muted px-2 py-1 text-sm font-mono font-bold tracking-widest">
|
||||||
|
{codexDeviceCode.userCode}
|
||||||
|
</code>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
codexDeviceCode.userCode
|
||||||
|
)
|
||||||
|
toast.success(t("codex.loginCodeCopied"))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-1">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
{t("codex.loginPolling")}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={cancelCodexDeviceLogin}
|
||||||
|
>
|
||||||
|
{t("codex.loginCancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{codexLoginStatus === "success" && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-green-600">
|
||||||
|
<CheckCircle2 className="h-3 w-3" />
|
||||||
|
{t("codex.loginSuccess")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{codexLoginStatus === "error" && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<p className="text-xs text-destructive">
|
||||||
|
{t("codex.loginFailed", {
|
||||||
|
message: codexLoginError ?? "Unknown error",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={handleCodexDeviceLogin}
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
{t("codex.loginRetry")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{selectedDraft.codexAuthMode === "model_provider" && (
|
{selectedDraft.codexAuthMode === "model_provider" && (
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-[11px] text-muted-foreground">
|
<label className="text-[11px] text-muted-foreground">
|
||||||
@@ -5498,27 +5728,6 @@ export function AcpAgentSettings() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="text-[11px] text-muted-foreground">
|
|
||||||
{t("codex.authJsonNative")}
|
|
||||||
</label>
|
|
||||||
<Textarea
|
|
||||||
value={selectedDraft.codexAuthJsonText}
|
|
||||||
onChange={(event) => {
|
|
||||||
handleCodexAuthJsonTextChange(event.target.value)
|
|
||||||
}}
|
|
||||||
placeholder={`{
|
|
||||||
"OPENAI_API_KEY": "sk-..."
|
|
||||||
}`}
|
|
||||||
className="min-h-28 max-h-60 font-mono text-xs"
|
|
||||||
/>
|
|
||||||
{selectedCodexAuthError && (
|
|
||||||
<div className="rounded-md border border-red-500/30 bg-red-500/5 px-2.5 py-1.5 text-[11px] text-red-400">
|
|
||||||
{selectedCodexAuthError}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-[11px] text-muted-foreground">
|
<label className="text-[11px] text-muted-foreground">
|
||||||
{t("codex.configTomlNative")}
|
{t("codex.configTomlNative")}
|
||||||
|
|||||||
@@ -557,7 +557,21 @@
|
|||||||
"enableFast": "تفعيل Fast",
|
"enableFast": "تفعيل Fast",
|
||||||
"enableFastAria": "تفعيل مستوى خدمة Fast لـ Codex",
|
"enableFastAria": "تفعيل مستوى خدمة Fast لـ Codex",
|
||||||
"authJsonNative": "auth.json (أصلي)",
|
"authJsonNative": "auth.json (أصلي)",
|
||||||
"configTomlNative": "config.toml (أصلي)"
|
"configTomlNative": "config.toml (أصلي)",
|
||||||
|
"loginButton": "تسجيل الدخول باستخدام ChatGPT",
|
||||||
|
"loginRequesting": "جارٍ طلب رمز تسجيل الدخول...",
|
||||||
|
"loginStep1": "افتح الرابط التالي في متصفحك:",
|
||||||
|
"loginStep2": "أدخل الرمز أدناه:",
|
||||||
|
"loginPolling": "في انتظار التفويض...",
|
||||||
|
"loginCancel": "إلغاء",
|
||||||
|
"loginSuccess": "تم تسجيل الدخول بنجاح، تم حفظ الإعدادات!",
|
||||||
|
"loginFailed": "فشل تسجيل الدخول: {message}",
|
||||||
|
"loginRetry": "إعادة المحاولة",
|
||||||
|
"loginCodeCopied": "تم نسخ الرمز",
|
||||||
|
"loggedIn": "تم تسجيل الدخول",
|
||||||
|
"loginRelogin": "إعادة تسجيل الدخول / تبديل الحساب",
|
||||||
|
"loginTimeout": "انتهت مهلة تسجيل الدخول، يرجى المحاولة مرة أخرى",
|
||||||
|
"loginSaveFailed": "تم تسجيل الدخول بنجاح ولكن فشل حفظ الإعدادات"
|
||||||
},
|
},
|
||||||
"gemini": {
|
"gemini": {
|
||||||
"authConfig": "إعداد مصادقة Gemini",
|
"authConfig": "إعداد مصادقة Gemini",
|
||||||
|
|||||||
@@ -557,7 +557,21 @@
|
|||||||
"enableFast": "Fast aktivieren",
|
"enableFast": "Fast aktivieren",
|
||||||
"enableFastAria": "Fast-Servicestufe für Codex aktivieren",
|
"enableFastAria": "Fast-Servicestufe für Codex aktivieren",
|
||||||
"authJsonNative": "auth.json (nativ)",
|
"authJsonNative": "auth.json (nativ)",
|
||||||
"configTomlNative": "config.toml (nativ)"
|
"configTomlNative": "config.toml (nativ)",
|
||||||
|
"loginButton": "Mit ChatGPT anmelden",
|
||||||
|
"loginRequesting": "Login-Code wird angefordert...",
|
||||||
|
"loginStep1": "Öffnen Sie die folgende URL in Ihrem Browser:",
|
||||||
|
"loginStep2": "Geben Sie den folgenden Code ein:",
|
||||||
|
"loginPolling": "Warte auf Autorisierung...",
|
||||||
|
"loginCancel": "Abbrechen",
|
||||||
|
"loginSuccess": "Erfolgreich angemeldet, Konfiguration gespeichert!",
|
||||||
|
"loginFailed": "Anmeldung fehlgeschlagen: {message}",
|
||||||
|
"loginRetry": "Erneut versuchen",
|
||||||
|
"loginCodeCopied": "Code kopiert",
|
||||||
|
"loggedIn": "Konto angemeldet",
|
||||||
|
"loginRelogin": "Erneut anmelden / Konto wechseln",
|
||||||
|
"loginTimeout": "Anmeldung abgelaufen, bitte erneut versuchen",
|
||||||
|
"loginSaveFailed": "Anmeldung erfolgreich, aber Konfiguration konnte nicht gespeichert werden"
|
||||||
},
|
},
|
||||||
"gemini": {
|
"gemini": {
|
||||||
"authConfig": "Gemini-Auth-Konfiguration",
|
"authConfig": "Gemini-Auth-Konfiguration",
|
||||||
|
|||||||
@@ -557,7 +557,21 @@
|
|||||||
"enableFast": "Enable Fast",
|
"enableFast": "Enable Fast",
|
||||||
"enableFastAria": "Enable Fast service tier for Codex",
|
"enableFastAria": "Enable Fast service tier for Codex",
|
||||||
"authJsonNative": "auth.json (native)",
|
"authJsonNative": "auth.json (native)",
|
||||||
"configTomlNative": "config.toml (native)"
|
"configTomlNative": "config.toml (native)",
|
||||||
|
"loginButton": "Log in with ChatGPT",
|
||||||
|
"loginRequesting": "Requesting login code...",
|
||||||
|
"loginStep1": "Open the following URL in your browser:",
|
||||||
|
"loginStep2": "Enter the code below:",
|
||||||
|
"loginPolling": "Waiting for authorization...",
|
||||||
|
"loginCancel": "Cancel",
|
||||||
|
"loginSuccess": "Logged in successfully, config saved!",
|
||||||
|
"loginFailed": "Login failed: {message}",
|
||||||
|
"loginRetry": "Retry",
|
||||||
|
"loginCodeCopied": "Code copied",
|
||||||
|
"loggedIn": "Account logged in",
|
||||||
|
"loginRelogin": "Re-login / Switch account",
|
||||||
|
"loginTimeout": "Login timed out, please try again",
|
||||||
|
"loginSaveFailed": "Login succeeded but failed to save config"
|
||||||
},
|
},
|
||||||
"gemini": {
|
"gemini": {
|
||||||
"authConfig": "Gemini Auth Config",
|
"authConfig": "Gemini Auth Config",
|
||||||
|
|||||||
@@ -557,7 +557,21 @@
|
|||||||
"enableFast": "Habilitar Fast",
|
"enableFast": "Habilitar Fast",
|
||||||
"enableFastAria": "Habilitar nivel de servicio Fast para Codex",
|
"enableFastAria": "Habilitar nivel de servicio Fast para Codex",
|
||||||
"authJsonNative": "auth.json (nativo)",
|
"authJsonNative": "auth.json (nativo)",
|
||||||
"configTomlNative": "config.toml (nativo)"
|
"configTomlNative": "config.toml (nativo)",
|
||||||
|
"loginButton": "Iniciar sesión con ChatGPT",
|
||||||
|
"loginRequesting": "Solicitando código de inicio de sesión...",
|
||||||
|
"loginStep1": "Abre la siguiente URL en tu navegador:",
|
||||||
|
"loginStep2": "Introduce el siguiente código:",
|
||||||
|
"loginPolling": "Esperando autorización...",
|
||||||
|
"loginCancel": "Cancelar",
|
||||||
|
"loginSuccess": "¡Inicio de sesión exitoso, configuración guardada!",
|
||||||
|
"loginFailed": "Error de inicio de sesión: {message}",
|
||||||
|
"loginRetry": "Reintentar",
|
||||||
|
"loginCodeCopied": "Código copiado",
|
||||||
|
"loggedIn": "Cuenta conectada",
|
||||||
|
"loginRelogin": "Reconectar / Cambiar cuenta",
|
||||||
|
"loginTimeout": "Tiempo de inicio de sesión agotado, inténtalo de nuevo",
|
||||||
|
"loginSaveFailed": "Inicio de sesión exitoso pero falló al guardar la configuración"
|
||||||
},
|
},
|
||||||
"gemini": {
|
"gemini": {
|
||||||
"authConfig": "Configuración de autenticación de Gemini",
|
"authConfig": "Configuración de autenticación de Gemini",
|
||||||
|
|||||||
@@ -557,7 +557,21 @@
|
|||||||
"enableFast": "Activer Fast",
|
"enableFast": "Activer Fast",
|
||||||
"enableFastAria": "Activer le niveau de service Fast pour Codex",
|
"enableFastAria": "Activer le niveau de service Fast pour Codex",
|
||||||
"authJsonNative": "auth.json (natif)",
|
"authJsonNative": "auth.json (natif)",
|
||||||
"configTomlNative": "config.toml (natif)"
|
"configTomlNative": "config.toml (natif)",
|
||||||
|
"loginButton": "Se connecter avec ChatGPT",
|
||||||
|
"loginRequesting": "Demande du code de connexion...",
|
||||||
|
"loginStep1": "Ouvrez l'URL suivante dans votre navigateur :",
|
||||||
|
"loginStep2": "Entrez le code ci-dessous :",
|
||||||
|
"loginPolling": "En attente d'autorisation...",
|
||||||
|
"loginCancel": "Annuler",
|
||||||
|
"loginSuccess": "Connexion réussie, configuration enregistrée !",
|
||||||
|
"loginFailed": "Échec de la connexion : {message}",
|
||||||
|
"loginRetry": "Réessayer",
|
||||||
|
"loginCodeCopied": "Code copié",
|
||||||
|
"loggedIn": "Compte connecté",
|
||||||
|
"loginRelogin": "Reconnecter / Changer de compte",
|
||||||
|
"loginTimeout": "Connexion expirée, veuillez réessayer",
|
||||||
|
"loginSaveFailed": "Connexion réussie mais échec de la sauvegarde de la configuration"
|
||||||
},
|
},
|
||||||
"gemini": {
|
"gemini": {
|
||||||
"authConfig": "Configuration d’authentification Gemini",
|
"authConfig": "Configuration d’authentification Gemini",
|
||||||
|
|||||||
@@ -557,7 +557,21 @@
|
|||||||
"enableFast": "Fast を有効化",
|
"enableFast": "Fast を有効化",
|
||||||
"enableFastAria": "Codex の Fast サービスティアを有効化",
|
"enableFastAria": "Codex の Fast サービスティアを有効化",
|
||||||
"authJsonNative": "auth.json(ネイティブ)",
|
"authJsonNative": "auth.json(ネイティブ)",
|
||||||
"configTomlNative": "config.toml(ネイティブ)"
|
"configTomlNative": "config.toml(ネイティブ)",
|
||||||
|
"loginButton": "ChatGPT でログイン",
|
||||||
|
"loginRequesting": "ログインコードを取得中...",
|
||||||
|
"loginStep1": "ブラウザで以下の URL を開いてください:",
|
||||||
|
"loginStep2": "以下のコードを入力してください:",
|
||||||
|
"loginPolling": "認証を待っています...",
|
||||||
|
"loginCancel": "キャンセル",
|
||||||
|
"loginSuccess": "ログイン成功、設定を保存しました!",
|
||||||
|
"loginFailed": "ログインに失敗しました:{message}",
|
||||||
|
"loginRetry": "再試行",
|
||||||
|
"loginCodeCopied": "コードをコピーしました",
|
||||||
|
"loggedIn": "アカウントにログイン済み",
|
||||||
|
"loginRelogin": "再ログイン / アカウント切替",
|
||||||
|
"loginTimeout": "ログインがタイムアウトしました。再試行してください",
|
||||||
|
"loginSaveFailed": "ログインは成功しましたが、設定の保存に失敗しました"
|
||||||
},
|
},
|
||||||
"gemini": {
|
"gemini": {
|
||||||
"authConfig": "Gemini 認証設定",
|
"authConfig": "Gemini 認証設定",
|
||||||
|
|||||||
@@ -557,7 +557,21 @@
|
|||||||
"enableFast": "Fast 활성화",
|
"enableFast": "Fast 활성화",
|
||||||
"enableFastAria": "Codex용 Fast 서비스 계층 활성화",
|
"enableFastAria": "Codex용 Fast 서비스 계층 활성화",
|
||||||
"authJsonNative": "auth.json (네이티브)",
|
"authJsonNative": "auth.json (네이티브)",
|
||||||
"configTomlNative": "config.toml (네이티브)"
|
"configTomlNative": "config.toml (네이티브)",
|
||||||
|
"loginButton": "ChatGPT로 로그인",
|
||||||
|
"loginRequesting": "로그인 코드 요청 중...",
|
||||||
|
"loginStep1": "브라우저에서 다음 URL을 열어주세요:",
|
||||||
|
"loginStep2": "아래 코드를 입력하세요:",
|
||||||
|
"loginPolling": "인증 대기 중...",
|
||||||
|
"loginCancel": "취소",
|
||||||
|
"loginSuccess": "로그인 성공, 설정이 저장되었습니다!",
|
||||||
|
"loginFailed": "로그인 실패: {message}",
|
||||||
|
"loginRetry": "재시도",
|
||||||
|
"loginCodeCopied": "코드가 복사되었습니다",
|
||||||
|
"loggedIn": "계정 로그인됨",
|
||||||
|
"loginRelogin": "재로그인 / 계정 전환",
|
||||||
|
"loginTimeout": "로그인 시간 초과, 다시 시도해 주세요",
|
||||||
|
"loginSaveFailed": "로그인 성공했지만 설정 저장 실패"
|
||||||
},
|
},
|
||||||
"gemini": {
|
"gemini": {
|
||||||
"authConfig": "Gemini 인증 설정",
|
"authConfig": "Gemini 인증 설정",
|
||||||
|
|||||||
@@ -557,7 +557,21 @@
|
|||||||
"enableFast": "Habilitar Fast",
|
"enableFast": "Habilitar Fast",
|
||||||
"enableFastAria": "Habilitar nível de serviço Fast para Codex",
|
"enableFastAria": "Habilitar nível de serviço Fast para Codex",
|
||||||
"authJsonNative": "auth.json (nativo)",
|
"authJsonNative": "auth.json (nativo)",
|
||||||
"configTomlNative": "config.toml (nativo)"
|
"configTomlNative": "config.toml (nativo)",
|
||||||
|
"loginButton": "Entrar com ChatGPT",
|
||||||
|
"loginRequesting": "Solicitando código de login...",
|
||||||
|
"loginStep1": "Abra a seguinte URL no seu navegador:",
|
||||||
|
"loginStep2": "Digite o código abaixo:",
|
||||||
|
"loginPolling": "Aguardando autorização...",
|
||||||
|
"loginCancel": "Cancelar",
|
||||||
|
"loginSuccess": "Login realizado com sucesso, configuração salva!",
|
||||||
|
"loginFailed": "Falha no login: {message}",
|
||||||
|
"loginRetry": "Tentar novamente",
|
||||||
|
"loginCodeCopied": "Código copiado",
|
||||||
|
"loggedIn": "Conta conectada",
|
||||||
|
"loginRelogin": "Reconectar / Trocar conta",
|
||||||
|
"loginTimeout": "Login expirou, tente novamente",
|
||||||
|
"loginSaveFailed": "Login realizado com sucesso, mas falha ao salvar configuração"
|
||||||
},
|
},
|
||||||
"gemini": {
|
"gemini": {
|
||||||
"authConfig": "Configuração de autenticação do Gemini",
|
"authConfig": "Configuração de autenticação do Gemini",
|
||||||
|
|||||||
@@ -557,7 +557,21 @@
|
|||||||
"enableFast": "启用 Fast",
|
"enableFast": "启用 Fast",
|
||||||
"enableFastAria": "Codex 启用 Fast 服务等级",
|
"enableFastAria": "Codex 启用 Fast 服务等级",
|
||||||
"authJsonNative": "auth.json(原生)",
|
"authJsonNative": "auth.json(原生)",
|
||||||
"configTomlNative": "config.toml(原生)"
|
"configTomlNative": "config.toml(原生)",
|
||||||
|
"loginButton": "使用 ChatGPT 登录",
|
||||||
|
"loginRequesting": "正在请求登录码...",
|
||||||
|
"loginStep1": "在浏览器中打开以下链接:",
|
||||||
|
"loginStep2": "输入以下代码:",
|
||||||
|
"loginPolling": "等待授权中...",
|
||||||
|
"loginCancel": "取消",
|
||||||
|
"loginSuccess": "登录成功,配置已保存!",
|
||||||
|
"loginFailed": "登录失败:{message}",
|
||||||
|
"loginRetry": "重试",
|
||||||
|
"loginCodeCopied": "已复制代码",
|
||||||
|
"loggedIn": "账号已登录",
|
||||||
|
"loginRelogin": "重新登录 / 切换账号",
|
||||||
|
"loginTimeout": "登录超时,请重试",
|
||||||
|
"loginSaveFailed": "登录成功但配置保存失败"
|
||||||
},
|
},
|
||||||
"gemini": {
|
"gemini": {
|
||||||
"authConfig": "Gemini 认证配置",
|
"authConfig": "Gemini 认证配置",
|
||||||
|
|||||||
@@ -557,7 +557,21 @@
|
|||||||
"enableFast": "啟用 Fast",
|
"enableFast": "啟用 Fast",
|
||||||
"enableFastAria": "Codex 啟用 Fast 服務等級",
|
"enableFastAria": "Codex 啟用 Fast 服務等級",
|
||||||
"authJsonNative": "auth.json(原生)",
|
"authJsonNative": "auth.json(原生)",
|
||||||
"configTomlNative": "config.toml(原生)"
|
"configTomlNative": "config.toml(原生)",
|
||||||
|
"loginButton": "使用 ChatGPT 登入",
|
||||||
|
"loginRequesting": "正在請求登入碼...",
|
||||||
|
"loginStep1": "在瀏覽器中打開以下連結:",
|
||||||
|
"loginStep2": "輸入以下代碼:",
|
||||||
|
"loginPolling": "等待授權中...",
|
||||||
|
"loginCancel": "取消",
|
||||||
|
"loginSuccess": "登入成功,配置已儲存!",
|
||||||
|
"loginFailed": "登入失敗:{message}",
|
||||||
|
"loginRetry": "重試",
|
||||||
|
"loginCodeCopied": "已複製代碼",
|
||||||
|
"loggedIn": "帳號已登入",
|
||||||
|
"loginRelogin": "重新登入 / 切換帳號",
|
||||||
|
"loginTimeout": "登入逾時,請重試",
|
||||||
|
"loginSaveFailed": "登入成功但配置儲存失敗"
|
||||||
},
|
},
|
||||||
"gemini": {
|
"gemini": {
|
||||||
"authConfig": "Gemini 認證配置",
|
"authConfig": "Gemini 認證配置",
|
||||||
|
|||||||
@@ -277,6 +277,32 @@ export async function acpReorderAgents(agentTypes: AgentType[]): Promise<void> {
|
|||||||
return getTransport().call("acp_reorder_agents", { agentTypes })
|
return getTransport().call("acp_reorder_agents", { agentTypes })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function codexRequestDeviceCode(): Promise<{
|
||||||
|
userCode: string
|
||||||
|
verificationUrl: string
|
||||||
|
deviceAuthId: string
|
||||||
|
interval: number
|
||||||
|
}> {
|
||||||
|
return getTransport().call("codex_request_device_code", {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function codexPollDeviceCode(params: {
|
||||||
|
deviceAuthId: string
|
||||||
|
userCode: string
|
||||||
|
}): Promise<{
|
||||||
|
status: "pending" | "success" | "error"
|
||||||
|
message?: string
|
||||||
|
idToken?: string
|
||||||
|
accessToken?: string
|
||||||
|
refreshToken?: string
|
||||||
|
accountId?: string
|
||||||
|
}> {
|
||||||
|
return getTransport().call("codex_poll_device_code", {
|
||||||
|
deviceAuthId: params.deviceAuthId,
|
||||||
|
userCode: params.userCode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function acpPreflight(
|
export async function acpPreflight(
|
||||||
agentType: AgentType,
|
agentType: AgentType,
|
||||||
forceRefresh?: boolean
|
forceRefresh?: boolean
|
||||||
|
|||||||
Reference in New Issue
Block a user