const BASE = '/api' export function getApiKey() { return localStorage.getItem('autoteam_api_key') || '' } export function setApiKey(key) { localStorage.setItem('autoteam_api_key', key) } export function clearApiKey() { localStorage.removeItem('autoteam_api_key') } async function request(method, path, body = null) { const headers = { 'Content-Type': 'application/json' } const key = getApiKey() if (key) { headers['Authorization'] = `Bearer ${key}` } const opts = { method, headers } if (body) opts.body = JSON.stringify(body) const resp = await fetch(`${BASE}${path}`, opts) let data try { data = await resp.json() } catch { const err = new Error(`HTTP ${resp.status}: 服务器返回了非 JSON 响应`) err.status = resp.status throw err } if (!resp.ok) { const msg = data?.detail?.message || data?.detail || `HTTP ${resp.status}` const err = new Error(msg) err.status = resp.status throw err } return data } export const api = { checkAuth: () => request('GET', '/auth/check'), getSetupStatus: () => request('GET', '/setup/status'), saveSetup: (config) => request('POST', '/setup/save', config), // SPEC-1 §4.2 — 三步分阶段 mail provider 探测 probeMailProvider: (payload) => request('POST', '/mail-provider/probe', payload), getStatus: () => request('GET', '/status'), getAdminStatus: () => request('GET', '/admin/status'), getMainCodexStatus: () => request('GET', '/main-codex/status'), getManualAccountStatus: () => request('GET', '/manual-account/status'), getAccounts: () => request('GET', '/accounts'), getActiveAccounts: () => request('GET', '/accounts/active'), getStandbyAccounts: () => request('GET', '/accounts/standby'), bulkDisableAccounts: (emails) => request('POST', '/accounts/bulk/disable', { emails }), bulkEnableAccounts: (emails) => request('POST', '/accounts/bulk/enable', { emails }), disableAccount: (email) => request('POST', `/accounts/${encodeURIComponent(email)}/disable`), enableAccount: (email) => request('POST', `/accounts/${encodeURIComponent(email)}/enable`), deleteAccount: (email) => request('DELETE', `/accounts/${encodeURIComponent(email)}`), deleteAccountsBatch: (emails, continueOnError = true) => request('POST', '/accounts/delete-batch', { emails, continue_on_error: continueOnError }), loginAccount: (email) => request('POST', '/accounts/login', { email }), getCodexAuth: (email) => request('GET', `/accounts/${encodeURIComponent(email)}/codex-auth`), kickAccount: (email) => request('POST', `/accounts/${encodeURIComponent(email)}/kick`), getCpaFiles: () => request('GET', '/cpa/files'), startAdminLogin: (email) => request('POST', '/admin/login/start', { email }), submitAdminSession: (email, sessionToken) => request('POST', '/admin/login/session', { email, session_token: sessionToken }), submitAdminPassword: (password) => request('POST', '/admin/login/password', { password }), submitAdminCode: (code) => request('POST', '/admin/login/code', { code }), submitAdminWorkspace: (optionId) => request('POST', '/admin/login/workspace', { option_id: optionId }), cancelAdminLogin: () => request('POST', '/admin/login/cancel'), logoutAdmin: () => request('POST', '/admin/logout'), startMainCodexSync: () => request('POST', '/main-codex/start'), startMainCodexLogin: () => request('POST', '/main-codex/login'), submitMainCodexPassword: (password) => request('POST', '/main-codex/password', { password }), submitMainCodexCode: (code) => request('POST', '/main-codex/code', { code }), cancelMainCodexSync: () => request('POST', '/main-codex/cancel'), deleteMainCodexRemoteFiles: () => request('POST', '/main-codex/delete-remote-files'), deleteMainCodexFromRemoteFiles: () => request('POST', '/main-codex/delete-remote-files'), deleteMainCodexCpa: () => request('POST', '/main-codex/delete-cpa'), startManualAccount: () => request('POST', '/manual-account/start'), submitManualAccountCallback: (redirectUrl) => request('POST', '/manual-account/callback', { redirect_url: redirectUrl }), cancelManualAccount: () => request('POST', '/manual-account/cancel'), postSync: () => request('POST', '/sync'), postSyncFromCpa: () => request('POST', '/sync/from-cpa'), postSyncAccounts: () => request('POST', '/sync/accounts'), postSyncMainCodex: () => request('POST', '/sync/main-codex'), startRotate: (target = 3) => request('POST', '/tasks/rotate', { target }), startCheck: () => request('POST', '/tasks/check'), startAdd: () => request('POST', '/tasks/add'), startFill: (target = 3) => request('POST', '/tasks/fill', { target, leave_workspace: false }), startFillPersonal: (count = 1) => request('POST', '/tasks/fill', { target: count, leave_workspace: true }), startCleanup: (maxSeats = null) => request('POST', '/tasks/cleanup', { max_seats: maxSeats }), getTasks: () => request('GET', '/tasks'), getTask: (id) => request('GET', `/tasks/${id}`), cancelTask: () => request('POST', '/tasks/cancel'), getAutoCheckConfig: () => request('GET', '/config/auto-check'), setAutoCheckConfig: (cfg) => request('PUT', '/config/auto-check', cfg), getRegisterDomain: () => request('GET', '/config/register-domain'), setRegisterDomain: (domain, verify = true) => request('PUT', '/config/register-domain', { domain, verify }), // SPEC-2 — 邀请席位偏好(default/codex) getPreferredSeatType: () => request('GET', '/config/preferred-seat-type'), putPreferredSeatType: (value) => request('PUT', '/config/preferred-seat-type', { value }), // SPEC-2 — sync_account_states 被踢探测节流(并发上限 + 冷却分钟) getSyncProbe: () => request('GET', '/config/sync-probe'), putSyncProbe: (payload) => request('PUT', '/config/sync-probe', payload), getRegisterFailures: (limit = 50) => request('GET', `/register-failures?limit=${limit}`), // Round 8 — SPEC-2 v1.5 §6.2:母号订阅健康度独立端点 getMasterHealth: (forceRefresh = false) => request( 'GET', `/admin/master-health${forceRefresh ? '?force_refresh=1' : ''}`, ), // Round 11 — realtime-probe.md v1.0 §2:子号实时探活 + 模型清单 // POST /api/accounts/{email}/probe — 强制刷新单个子号的可用性 (cheap_codex_smoke + check_codex_quota) // 副作用:落 last_quota_check_at,绕过 sync_account_states 30min 节流 probeAccount: (email, forceCodexSmoke = true) => request( 'POST', `/accounts/${encodeURIComponent(email)}/probe`, { force_codex_smoke: forceCodexSmoke }, ), // GET /api/accounts/{email}/models — 用 access_token 调 /backend-api/models 拿可用模型列表 getAccountModels: (email) => request( 'GET', `/accounts/${encodeURIComponent(email)}/models`, ), getTeamMembers: () => request('GET', '/team/members'), removeTeamMember: (payload) => request('POST', '/team/members/remove', payload), getLogs: (limit = 100, since = 0) => request('GET', `/logs?limit=${limit}&since=${since}`), } // Round 7 FR-D7 — 把后端 task["error"] 字符串关键字解析为 UI 可消费的语义错误。 // Round 6 P1.3 决策让 manager 在 task["error"] 中携带 `phone_required` / `register_blocked` // 关键字串。前端 UI 在 getTask 轮询命中 task.error 后调用本函数,根据 category 给出 // 友好 toast 而不是裸字符串。 // // 入参:task — 后端 GET /api/tasks/{id} 的响应对象(含可选 error 字段) // 返回: // { category: 'phone_required', friendly_message: '该账号需要绑定手机才能完成 OAuth', detail } // { category: 'register_blocked', friendly_message: '...', detail } // { category: 'generic', friendly_message: '<原 error 字符串>', detail } // null — task 没有 error 字段(任务进行中或成功) export function parseTaskError(task) { if (!task || !task.error) return null const errStr = String(task.error) const lower = errStr.toLowerCase() if (lower.includes('phone_required')) { return { category: 'phone_required', friendly_message: '该账号需要绑定手机才能完成 OAuth', detail: errStr, } } if (lower.includes('register_blocked')) { return { category: 'register_blocked', friendly_message: '该账号注册被阻断,请检查 OAuth 状态', detail: errStr, } } return { category: 'generic', friendly_message: errStr, detail: errStr, } }