diff --git a/docs/PROVIDERS_TRAE_SOLO.md b/docs/PROVIDERS_TRAE_SOLO.md index 4d81c6d1..846958cc 100644 --- a/docs/PROVIDERS_TRAE_SOLO.md +++ b/docs/PROVIDERS_TRAE_SOLO.md @@ -356,6 +356,7 @@ ProviderDescriptor{ - [x] `credential.go`:Validate / Encode / Decode(nested + flat)、持久化 device - [x] `ExchangeToken` + 提前刷新 + 刷新失败标未就绪 - [x] `StartLogin`:持久化或复用 device、起 `127.0.0.1` callback、返回授权 URL +- [x] `CompleteLogin`:浏览器和服务不在同一台时,可粘贴完整 `127.0.0.1/authorize?...` URL - [x] `PollLogin`:pending → ExchangeToken → GetUserInfo → SaveCredentialPayload - [x] Import/Export 兼容 `trae-{uid}.json` - [x] 不绑公网、不提供 CODING_TOKEN、不每轮换设备 diff --git a/frontend/src/api/overview.ts b/frontend/src/api/overview.ts index b303ed07..02cc8f7b 100644 --- a/frontend/src/api/overview.ts +++ b/frontend/src/api/overview.ts @@ -19,6 +19,14 @@ export function fetchLoginStatus(accountId?: string) { return api<{ login?: any }>(`/api/accounts/${encodeURIComponent(accountId)}/login/status`) } +export function completeLoginCallback(accountId: string, callbackUrl: string) { + if (!accountId) throw new Error('account id required') + return api(`/api/accounts/${encodeURIComponent(accountId)}/login/callback`, { + method: 'POST', + body: JSON.stringify({ callback_url: callbackUrl }), + }) +} + export function loginWithPat(pat: string, accountId?: string) { if (!accountId) throw new Error('account id required') return api(`/api/accounts/${encodeURIComponent(accountId)}/login/pat`, { diff --git a/frontend/src/components/AddAccountModal.tsx b/frontend/src/components/AddAccountModal.tsx index 5818003f..2c9162a0 100644 --- a/frontend/src/components/AddAccountModal.tsx +++ b/frontend/src/components/AddAccountModal.tsx @@ -11,6 +11,7 @@ import { fetchProviders, importAccount, loginWithPat, + completeLoginCallback, startDeviceLogin, type ProviderDescriptor, } from '@/api/overview' @@ -132,6 +133,7 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) { const [phase, setPhase] = useState('idle') const [message, setMessage] = useState('') const [authUrl, setAuthUrl] = useState('') + const [callbackUrl, setCallbackUrl] = useState('') const createdId = useRef('') const pollTimer = useRef(null) const fileInput = useRef(null) @@ -173,6 +175,7 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) { const showPatTab = activeOption?.descriptor.capabilities?.pat_login !== false const showImportTab = activeOption?.descriptor.capabilities?.import_export !== false const showDropSystem = activeOption?.provider === 'workbuddy' + const showCallbackPaste = activeOption?.provider === 'trae' const busy = phase === 'busy' || phase === 'polling' const settingsLocked = Boolean(createdId.current) || busy const isDone = phase === 'done' @@ -220,6 +223,7 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) { setPhase('idle') setMessage('') setAuthUrl('') + setCallbackUrl('') createdId.current = '' } @@ -280,6 +284,27 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) { } } + async function runCallback() { + const pasted = callbackUrl.trim() + if (!pasted) { + setMessage(t('wizardCallbackPh')) + return + } + try { + setPhase('busy') + const id = await ensureAccount() + await completeLoginCallback(id, pasted) + stopPolling() + setPhase('done') + setMessage(t('wizardAccountReady')) + onAdded() + window.setTimeout(close, 900) + } catch (error) { + setPhase('polling') + setMessage(error instanceof Error ? error.message : String(error)) + } + } + async function runPat() { const token = pat.trim() if (!token) { setMessage(t('pastePatFirst')); return } @@ -586,6 +611,22 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) { {message && !authUrl ? (

{message}

) : null} + {showCallbackPaste ? ( +
+

{t('wizardCallbackLead')}

+