revert: remove fork-only changes from release sync

Revert payment/wechat, sora/claude-max cleanup, fork-only migrations,
and cosmetic changes that were brought in by the release sync commit.
Keep only channel-monitor related improvements:
- PublicSettingsInjectionPayload named struct with drift test
- ChannelMonitorRunner graceful shutdown in wire
- image_output_price in SupportedModelChip
- Simplified buildSelfNavItems in AppSidebar
- Gateway WARN logs for 503 branches
This commit is contained in:
erio
2026-04-23 21:40:58 +08:00
parent a3ea8ecac5
commit 67518a59ac
71 changed files with 1792 additions and 1396 deletions

View File

@@ -1,186 +0,0 @@
import { describe, expect, it } from 'vitest'
import { resolveCodexUsageWindow } from '@/utils/codexUsage'
describe('resolveCodexUsageWindow', () => {
it('快照为空时返回空窗口', () => {
const result = resolveCodexUsageWindow(null, '5h', new Date('2026-02-20T08:00:00Z'))
expect(result).toEqual({ usedPercent: null, resetAt: null })
})
it('优先使用后端提供的绝对重置时间', () => {
const result = resolveCodexUsageWindow(
{
codex_5h_used_percent: 55,
codex_5h_reset_at: '2026-02-20T10:00:00Z',
codex_5h_reset_after_seconds: 1
},
'5h',
new Date('2026-02-20T08:00:00Z')
)
expect(result.usedPercent).toBe(55)
expect(result.resetAt).toBe('2026-02-20T10:00:00.000Z')
})
it('窗口已过期时自动归零', () => {
const result = resolveCodexUsageWindow(
{
codex_7d_used_percent: 100,
codex_7d_reset_at: '2026-02-20T07:00:00Z'
},
'7d',
new Date('2026-02-20T08:00:00Z')
)
expect(result.usedPercent).toBe(0)
expect(result.resetAt).toBe('2026-02-20T07:00:00.000Z')
})
it('无绝对时间时使用 updated_at + seconds 回退计算', () => {
const result = resolveCodexUsageWindow(
{
codex_5h_used_percent: 20,
codex_5h_reset_after_seconds: 3600,
codex_usage_updated_at: '2026-02-20T06:30:00Z'
},
'5h',
new Date('2026-02-20T07:00:00Z')
)
expect(result.usedPercent).toBe(20)
expect(result.resetAt).toBe('2026-02-20T07:30:00.000Z')
})
it('支持 legacy primary/secondary 字段映射', () => {
const snapshot = {
codex_primary_window_minutes: 10080,
codex_primary_used_percent: 70,
codex_primary_reset_after_seconds: 86400,
codex_secondary_window_minutes: 300,
codex_secondary_used_percent: 15,
codex_secondary_reset_after_seconds: 1200,
codex_usage_updated_at: '2026-02-20T07:00:00Z'
}
const result5h = resolveCodexUsageWindow(snapshot, '5h', new Date('2026-02-20T07:05:00Z'))
const result7d = resolveCodexUsageWindow(snapshot, '7d', new Date('2026-02-20T07:05:00Z'))
expect(result5h.usedPercent).toBe(15)
expect(result5h.resetAt).toBe('2026-02-20T07:20:00.000Z')
expect(result7d.usedPercent).toBe(70)
expect(result7d.resetAt).toBe('2026-02-21T07:00:00.000Z')
})
it('legacy 5h 在 primary<=360 时优先 primary 并支持字符串数字', () => {
const result = resolveCodexUsageWindow(
{
codex_primary_window_minutes: '300',
codex_primary_used_percent: '21',
codex_primary_reset_after_seconds: '1800',
codex_secondary_window_minutes: '10080',
codex_secondary_used_percent: '99',
codex_secondary_reset_after_seconds: '99999',
codex_usage_updated_at: '2026-02-20T08:00:00Z'
},
'5h',
new Date('2026-02-20T08:10:00Z')
)
expect(result.usedPercent).toBe(21)
expect(result.resetAt).toBe('2026-02-20T08:30:00.000Z')
})
it('legacy 5h 在无窗口信息时回退 secondary', () => {
const result = resolveCodexUsageWindow(
{
codex_secondary_used_percent: 19,
codex_secondary_reset_after_seconds: 120,
codex_usage_updated_at: '2026-02-20T08:00:00Z'
},
'5h',
new Date('2026-02-20T08:00:01Z')
)
expect(result.usedPercent).toBe(19)
expect(result.resetAt).toBe('2026-02-20T08:02:00.000Z')
})
it('legacy 场景下 secondary 为 7d 时能正确识别', () => {
const result = resolveCodexUsageWindow(
{
codex_primary_window_minutes: 300,
codex_primary_used_percent: 5,
codex_primary_reset_after_seconds: 600,
codex_secondary_window_minutes: 10080,
codex_secondary_used_percent: 66,
codex_secondary_reset_after_seconds: 7200,
codex_usage_updated_at: '2026-02-20T07:00:00Z'
},
'7d',
new Date('2026-02-20T07:30:00Z')
)
expect(result.usedPercent).toBe(66)
expect(result.resetAt).toBe('2026-02-20T09:00:00.000Z')
})
it('绝对时间非法时回退到 updated_at + seconds', () => {
const result = resolveCodexUsageWindow(
{
codex_5h_used_percent: 33,
codex_5h_reset_at: 'not-a-date',
codex_5h_reset_after_seconds: 900,
codex_usage_updated_at: '2026-02-20T07:30:00Z'
},
'5h',
new Date('2026-02-20T07:40:00Z')
)
expect(result.usedPercent).toBe(33)
expect(result.resetAt).toBe('2026-02-20T07:45:00.000Z')
})
it('updated_at 非法且无绝对时间时 resetAt 返回 null', () => {
const result = resolveCodexUsageWindow(
{
codex_5h_used_percent: 10,
codex_5h_reset_after_seconds: 123,
codex_usage_updated_at: 'invalid-time'
},
'5h',
new Date('2026-02-20T08:00:00Z')
)
expect(result.usedPercent).toBe(10)
expect(result.resetAt).toBeNull()
})
it('reset_after_seconds 为负数时按 0 秒处理', () => {
const result = resolveCodexUsageWindow(
{
codex_5h_used_percent: 80,
codex_5h_reset_after_seconds: -30,
codex_usage_updated_at: '2026-02-20T08:00:00Z'
},
'5h',
new Date('2026-02-20T07:59:00Z')
)
expect(result.usedPercent).toBe(80)
expect(result.resetAt).toBe('2026-02-20T08:00:00.000Z')
})
it('百分比缺失时仍可计算 resetAt 供倒计时展示', () => {
const result = resolveCodexUsageWindow(
{
codex_7d_reset_after_seconds: 60,
codex_usage_updated_at: '2026-02-20T08:00:00Z'
},
'7d',
new Date('2026-02-20T08:00:01Z')
)
expect(result.usedPercent).toBeNull()
expect(result.resetAt).toBe('2026-02-20T08:01:00.000Z')
})
})

View File

@@ -1,224 +0,0 @@
import { describe, expect, it } from 'vitest'
import { enqueueUsageRequest } from '../usageLoadQueue'
import type { Account } from '@/types'
/** Helper to create a minimal Account with proxy info */
function makeAccount(
platform: string,
type: string = 'oauth',
proxy?: { host: string; port: number; username?: string | null } | null
): Account {
return {
id: Math.floor(Math.random() * 10000),
platform,
type,
name: 'test',
status: 'active',
proxy_id: proxy ? 1 : null,
proxy: proxy
? { id: 1, name: 'p', protocol: 'http', host: proxy.host, port: proxy.port, username: proxy.username ?? null, status: 'active', created_at: '', updated_at: '' }
: undefined,
credentials: {},
created_at: '',
updated_at: ''
} as unknown as Account
}
describe('usageLoadQueue', () => {
// ─── Anthropic 账号:按代理出口排队 ───
it('Anthropic 同代理出口串行执行,间隔 >= 1s', async () => {
const timestamps: number[] = []
const makeFn = () => async () => {
timestamps.push(Date.now())
return 'ok'
}
const acc = makeAccount('anthropic', 'oauth', { host: '1.2.3.4', port: 8080, username: 'u1' })
const p1 = enqueueUsageRequest(acc, makeFn())
const p2 = enqueueUsageRequest(acc, makeFn())
const p3 = enqueueUsageRequest(acc, makeFn())
await Promise.all([p1, p2, p3])
expect(timestamps).toHaveLength(3)
expect(timestamps[1] - timestamps[0]).toBeGreaterThanOrEqual(950)
expect(timestamps[1] - timestamps[0]).toBeLessThan(2100)
expect(timestamps[2] - timestamps[1]).toBeGreaterThanOrEqual(950)
expect(timestamps[2] - timestamps[1]).toBeLessThan(2100)
})
it('Anthropic 不同代理出口并行执行', async () => {
const timestamps: Record<string, number> = {}
const makeTracked = (key: string) => async () => {
timestamps[key] = Date.now()
return key
}
const acc1 = makeAccount('anthropic', 'oauth', { host: '1.2.3.4', port: 8080, username: 'u1' })
const acc2 = makeAccount('anthropic', 'oauth', { host: '5.6.7.8', port: 3128, username: 'u2' })
const p1 = enqueueUsageRequest(acc1, makeTracked('proxy1'))
const p2 = enqueueUsageRequest(acc2, makeTracked('proxy2'))
await Promise.all([p1, p2])
const spread = Math.abs(timestamps['proxy1'] - timestamps['proxy2'])
expect(spread).toBeLessThan(50)
})
it('Anthropic 相同代理连接信息的不同账号归为同一队列', async () => {
const timestamps: number[] = []
const makeFn = () => async () => {
timestamps.push(Date.now())
return 'ok'
}
const acc1 = makeAccount('anthropic', 'oauth', { host: '10.0.0.1', port: 3128, username: 'admin' })
const acc2 = makeAccount('anthropic', 'setup-token', { host: '10.0.0.1', port: 3128, username: 'admin' })
const p1 = enqueueUsageRequest(acc1, makeFn())
const p2 = enqueueUsageRequest(acc2, makeFn())
await Promise.all([p1, p2])
expect(timestamps).toHaveLength(2)
expect(timestamps[1] - timestamps[0]).toBeGreaterThanOrEqual(950)
})
it('Anthropic 直连(无代理)的账号归为同一队列', async () => {
const order: number[] = []
const makeFn = (n: number) => async () => {
order.push(n)
return n
}
const acc1 = makeAccount('anthropic', 'oauth')
const acc2 = makeAccount('anthropic', 'setup-token')
const p1 = enqueueUsageRequest(acc1, makeFn(1))
const p2 = enqueueUsageRequest(acc2, makeFn(2))
await Promise.all([p1, p2])
expect(order).toEqual([1, 2])
})
it('Anthropic 请求失败时 reject后续任务继续执行', async () => {
const results: string[] = []
const acc = makeAccount('anthropic', 'oauth', { host: '99.99.99.99', port: 1234 })
const p1 = enqueueUsageRequest(acc, async () => {
throw new Error('fail')
})
const p2 = enqueueUsageRequest(acc, async () => {
results.push('second')
return 'ok'
})
await expect(p1).rejects.toThrow('fail')
await p2
expect(results).toEqual(['second'])
})
// ─── 非 Anthropic 平台:直接执行,不排队 ───
it('非 Anthropic 平台直接执行,不排队', async () => {
const timestamps: number[] = []
const makeFn = () => async () => {
timestamps.push(Date.now())
return 'ok'
}
// 同一代理的 Gemini 账号 — 应当并行,不排队
const acc1 = makeAccount('gemini', 'oauth', { host: '1.2.3.4', port: 8080 })
const acc2 = makeAccount('gemini', 'oauth', { host: '1.2.3.4', port: 8080 })
const p1 = enqueueUsageRequest(acc1, makeFn())
const p2 = enqueueUsageRequest(acc2, makeFn())
await Promise.all([p1, p2])
expect(timestamps).toHaveLength(2)
// 并行执行,几乎同时完成
expect(Math.abs(timestamps[1] - timestamps[0])).toBeLessThan(50)
})
it('Antigravity 平台直接执行,不排队', async () => {
const timestamps: number[] = []
const makeFn = () => async () => {
timestamps.push(Date.now())
return 'ok'
}
const acc1 = makeAccount('antigravity', 'oauth', { host: '1.2.3.4', port: 8080 })
const acc2 = makeAccount('antigravity', 'oauth', { host: '1.2.3.4', port: 8080 })
const p1 = enqueueUsageRequest(acc1, makeFn())
const p2 = enqueueUsageRequest(acc2, makeFn())
await Promise.all([p1, p2])
expect(timestamps).toHaveLength(2)
expect(Math.abs(timestamps[1] - timestamps[0])).toBeLessThan(50)
})
it('OpenAI 平台直接执行,不排队', async () => {
const timestamps: number[] = []
const makeFn = () => async () => {
timestamps.push(Date.now())
return 'ok'
}
const acc1 = makeAccount('openai', 'oauth', { host: '1.2.3.4', port: 8080 })
const acc2 = makeAccount('openai', 'oauth', { host: '1.2.3.4', port: 8080 })
const p1 = enqueueUsageRequest(acc1, makeFn())
const p2 = enqueueUsageRequest(acc2, makeFn())
await Promise.all([p1, p2])
expect(timestamps).toHaveLength(2)
expect(Math.abs(timestamps[1] - timestamps[0])).toBeLessThan(50)
})
// ─── Anthropic apikey 类型不排队 ───
it('Anthropic apikey 类型直接执行,不排队', async () => {
const timestamps: number[] = []
const makeFn = () => async () => {
timestamps.push(Date.now())
return 'ok'
}
const acc1 = makeAccount('anthropic', 'apikey', { host: '1.2.3.4', port: 8080 })
const acc2 = makeAccount('anthropic', 'apikey', { host: '1.2.3.4', port: 8080 })
const p1 = enqueueUsageRequest(acc1, makeFn())
const p2 = enqueueUsageRequest(acc2, makeFn())
await Promise.all([p1, p2])
expect(timestamps).toHaveLength(2)
expect(Math.abs(timestamps[1] - timestamps[0])).toBeLessThan(50)
})
// ─── 返回值透传 ───
it('返回值正确透传', async () => {
const acc = makeAccount('anthropic', 'oauth')
const result = await enqueueUsageRequest(acc, async () => {
return { usage: 42 }
})
expect(result).toEqual({ usage: 42 })
})
it('非 Anthropic 返回值正确透传', async () => {
const acc = makeAccount('gemini', 'oauth')
const result = await enqueueUsageRequest(acc, async () => {
return { quota: 100 }
})
expect(result).toEqual({ quota: 100 })
})
})

View File

@@ -1,140 +0,0 @@
import type { CodexUsageSnapshot } from '@/types'
export interface ResolvedCodexUsageWindow {
usedPercent: number | null
resetAt: string | null
}
type WindowKind = '5h' | '7d'
function asNumber(value: unknown): number | null {
if (typeof value === 'number' && Number.isFinite(value)) return value
if (typeof value === 'string' && value.trim() !== '') {
const n = Number(value)
if (Number.isFinite(n)) return n
}
return null
}
function asString(value: unknown): string | null {
if (typeof value !== 'string') return null
const trimmed = value.trim()
return trimmed === '' ? null : trimmed
}
function asISOTime(value: unknown): string | null {
const raw = asString(value)
if (!raw) return null
const date = new Date(raw)
if (Number.isNaN(date.getTime())) return null
return date.toISOString()
}
function resolveLegacy5h(snapshot: Record<string, unknown>): {
used: number | null
resetAfterSeconds: number | null
} {
const primaryWindow = asNumber(snapshot.codex_primary_window_minutes)
const secondaryWindow = asNumber(snapshot.codex_secondary_window_minutes)
const primaryUsed = asNumber(snapshot.codex_primary_used_percent)
const secondaryUsed = asNumber(snapshot.codex_secondary_used_percent)
const primaryReset = asNumber(snapshot.codex_primary_reset_after_seconds)
const secondaryReset = asNumber(snapshot.codex_secondary_reset_after_seconds)
if (primaryWindow != null && primaryWindow <= 360) {
return { used: primaryUsed, resetAfterSeconds: primaryReset }
}
if (secondaryWindow != null && secondaryWindow <= 360) {
return { used: secondaryUsed, resetAfterSeconds: secondaryReset }
}
return { used: secondaryUsed, resetAfterSeconds: secondaryReset }
}
function resolveLegacy7d(snapshot: Record<string, unknown>): {
used: number | null
resetAfterSeconds: number | null
} {
const primaryWindow = asNumber(snapshot.codex_primary_window_minutes)
const secondaryWindow = asNumber(snapshot.codex_secondary_window_minutes)
const primaryUsed = asNumber(snapshot.codex_primary_used_percent)
const secondaryUsed = asNumber(snapshot.codex_secondary_used_percent)
const primaryReset = asNumber(snapshot.codex_primary_reset_after_seconds)
const secondaryReset = asNumber(snapshot.codex_secondary_reset_after_seconds)
if (primaryWindow != null && primaryWindow >= 10000) {
return { used: primaryUsed, resetAfterSeconds: primaryReset }
}
if (secondaryWindow != null && secondaryWindow >= 10000) {
return { used: secondaryUsed, resetAfterSeconds: secondaryReset }
}
return { used: primaryUsed, resetAfterSeconds: primaryReset }
}
function resolveFromSeconds(
snapshot: Record<string, unknown>,
resetAfterSeconds: number | null
): string | null {
if (resetAfterSeconds == null) return null
const baseRaw = asString(snapshot.codex_usage_updated_at)
const base = baseRaw ? new Date(baseRaw) : new Date()
if (Number.isNaN(base.getTime())) return null
const sec = Math.max(0, resetAfterSeconds)
const resetAt = new Date(base.getTime() + sec * 1000)
return resetAt.toISOString()
}
function applyExpiredRule(
window: ResolvedCodexUsageWindow,
now: Date
): ResolvedCodexUsageWindow {
if (window.usedPercent == null || !window.resetAt) return window
const resetDate = new Date(window.resetAt)
if (Number.isNaN(resetDate.getTime())) return window
if (resetDate.getTime() <= now.getTime()) {
return { usedPercent: 0, resetAt: resetDate.toISOString() }
}
return window
}
export function resolveCodexUsageWindow(
snapshot: (CodexUsageSnapshot & Record<string, unknown>) | null | undefined,
window: WindowKind,
now: Date = new Date()
): ResolvedCodexUsageWindow {
if (!snapshot) {
return { usedPercent: null, resetAt: null }
}
const typedSnapshot = snapshot as Record<string, unknown>
let usedPercent: number | null
let resetAfterSeconds: number | null
let resetAt: string | null
if (window === '5h') {
usedPercent = asNumber(typedSnapshot.codex_5h_used_percent)
resetAfterSeconds = asNumber(typedSnapshot.codex_5h_reset_after_seconds)
resetAt = asISOTime(typedSnapshot.codex_5h_reset_at)
if (usedPercent == null || (resetAfterSeconds == null && !resetAt)) {
const legacy = resolveLegacy5h(typedSnapshot)
if (usedPercent == null) usedPercent = legacy.used
if (resetAfterSeconds == null) resetAfterSeconds = legacy.resetAfterSeconds
}
} else {
usedPercent = asNumber(typedSnapshot.codex_7d_used_percent)
resetAfterSeconds = asNumber(typedSnapshot.codex_7d_reset_after_seconds)
resetAt = asISOTime(typedSnapshot.codex_7d_reset_at)
if (usedPercent == null || (resetAfterSeconds == null && !resetAt)) {
const legacy = resolveLegacy7d(typedSnapshot)
if (usedPercent == null) usedPercent = legacy.used
if (resetAfterSeconds == null) resetAfterSeconds = legacy.resetAfterSeconds
}
}
if (!resetAt) {
resetAt = resolveFromSeconds(typedSnapshot, resetAfterSeconds)
}
return applyExpiredRule({ usedPercent, resetAt }, now)
}

View File

@@ -1,6 +1,6 @@
/**
* Shared URL builder for iframe-embedded pages.
* Used by CustomPageView to build consistent URLs
* Used by PurchaseSubscriptionView and CustomPageView to build consistent URLs
* with user_id, token, theme, lang, ui_mode, src_host, and src parameters.
*/