| 1 | const DEFAULT_CLI_QUOTA_LIMIT = 2000 |
| 2 | |
| 3 | /** |
| 4 | * CLI 对该账户是否可用——与账户健康无关的独立维度 |
| 5 | * @param {Object} account - 账户对象 |
| 6 | * @returns {'disabled'|'unsupported'|'pending'|'available'} |
| 7 | */ |
| 8 | function getCliAvailability(account) { |
| 9 | const reason = account.cli_unavailable_reason || null |
| 10 | if (reason === 'unsupported') return 'unsupported' |
| 11 | if (reason === 'disabled') return 'disabled' |
| 12 | if (!account.cli_info) return 'pending' |
| 13 | return 'available' |
| 14 | } |
| 15 | |
| 16 | /** |
| 17 | * 账户状态拆成两个互不遮挡的维度: |
| 18 | * - status.kind —— 账户健康(面板角标):cooldown > warn > token_expiring > active |
| 19 | * - status.cli —— CLI 可用性(卡片里的 CLI 行):disabled / unsupported / pending / available |
| 20 | * |
| 21 | * 合成一个字段会互相遮挡:ENABLE_CLI=false 时每个账户都是 cli_disabled, |
| 22 | * 冷却中或令牌将过期的账户与正常账户看起来完全一样。 |
| 23 | * |
| 24 | * @param {Object} account - 账户对象 |
| 25 | * @param {Object} rotatorRecord - 轮询器里该账户的记录 |
| 26 | * @param {number} now - 当前时间戳 |
| 27 | */ |
| 28 | function getAccountCliState(account, rotatorRecord = {}, now = Date.now()) { |
| 29 | const WARN_WINDOW_MS = 15 * 60 * 1000 |
| 30 | const TOKEN_EXPIRING_MS = 6 * 60 * 60 * 1000 |
| 31 | |
| 32 | // Un solo campo para el frontend, pero dos fuentes: el contador de fallos y el destierro |
| 33 | // por cuota agotada (account-rotator#recordQuotaExhausted). Gana el que libere mas tarde |
| 34 | // —es el que de verdad manda cuando la cuenta vuelve al sorteo—. Sin fundirlos, una |
| 35 | // cuenta desterrada por cuota se pintaba `active` mientras la rotacion la ignoraba. |
| 36 | const cooldownEndsAt = Math.max( |
| 37 | Number(rotatorRecord.cooldownEndsAt) || 0, |
| 38 | Number(rotatorRecord.quotaCooldownEndsAt) || 0 |
| 39 | ) || null |
| 40 | const lastErrorAt = rotatorRecord.lastErrorAt || null |