File size: 8,463 Bytes
4283ada
 
1f4cfcb
b7e2b8e
 
 
 
 
 
 
 
 
 
 
4283ada
b7e2b8e
 
 
 
4283ada
b7e2b8e
4283ada
 
3482523
 
 
 
 
 
 
 
4283ada
 
b7e2b8e
 
 
4283ada
 
 
 
 
b7e2b8e
462f5f6
 
f82773a
 
b7e2b8e
4283ada
40b21ae
 
a08d7b4
4283ada
 
 
1f4cfcb
 
 
 
40b21ae
3df0958
4af1754
ad9c2d9
7c59963
4283ada
 
40b21ae
b6bcc3c
40b21ae
 
 
 
 
 
ceeba19
40b21ae
 
 
ceeba19
 
 
a08d7b4
 
 
40b21ae
4283ada
2ddc3ee
8e8726e
40b21ae
4283ada
5aff1af
4283ada
 
5aff1af
3df0958
4283ada
 
 
 
3df0958
95df048
 
 
a0dfc48
3df0958
 
 
478c16c
 
 
 
 
 
 
 
3df0958
 
e6cc775
 
 
 
 
090976a
 
 
 
 
 
 
 
 
 
 
 
a0dfc48
876283e
a0dfc48
4283ada
a4ea50b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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,
  }
}