mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat(pool): 引入 pro_first 调度预设、Pool 候选持久化跳过与诊断信息优化
- 新增 pro_first 调度预设(Pro 优先),更新 plus_first 仅针对 Plus 计划,移除 free_team_first - Pool 内部候选(pool_key_index 不为空)跳过 DB 持久化(available/skipped/unused 均适用) - LRU 排序新增 catalog_lru_score 回退:runtime 无记录时使用 last_used_at_unix_secs - 执行路径 miss 诊断消息细化为中文,按 reason 分类输出可读说明 - build_local_request_candidate_status_record 补充 extra_data 和 created_at_unix_ms 字段 - OpenAI CLI 计划构建流程补充候选评估进度跟踪与 terminal reason 设置 - 前端 PoolSchedulingDialog 增加 pro_first 预设展示,修复 LRU 默认预设检测逻辑
This commit is contained in:
@@ -321,8 +321,17 @@ const FALLBACK_PRESET_DEFS: PoolPresetMeta[] = [
|
||||
{
|
||||
name: 'plus_first',
|
||||
label: 'Plus 优先',
|
||||
description: '优先消耗 Plus/Pro 账号(依赖 plan_type)',
|
||||
evidence_hint: '依据 plan_type(Plus/Pro 账号优先调度)',
|
||||
description: '优先消耗 Plus 账号(依赖 plan_type)',
|
||||
evidence_hint: '依据 plan_type(Plus 账号优先调度)',
|
||||
providers: ['codex', 'kiro'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
},
|
||||
{
|
||||
name: 'pro_first',
|
||||
label: 'Pro 优先',
|
||||
description: '优先消耗 Pro 账号(依赖 plan_type)',
|
||||
evidence_hint: '依据 plan_type(Pro 账号优先调度)',
|
||||
providers: ['codex', 'kiro'],
|
||||
modes: null,
|
||||
default_mode: null,
|
||||
@@ -608,7 +617,7 @@ function loadFromConfig(cfg: PoolAdvancedConfig | null): PresetListItem[] {
|
||||
|
||||
const rawPresets = cfg.scheduling_presets
|
||||
if (!Array.isArray(rawPresets) || rawPresets.length === 0) {
|
||||
if (cfg.scheduling_mode === 'lru' || (!cfg.scheduling_mode && cfg.lru_enabled !== false)) {
|
||||
if (cfg.scheduling_mode === 'lru' || cfg.lru_enabled === true) {
|
||||
return defaults.map(item => ({
|
||||
...item,
|
||||
enabled: item.preset === 'lru',
|
||||
|
||||
@@ -309,6 +309,7 @@ import {
|
||||
} from '@/api/endpoints'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { parseNumberInput } from '@/utils/form'
|
||||
import { dateTimeLocalToRfc3339, formatDateTimeLocalInput } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
@@ -403,10 +404,8 @@ function loadProviderData() {
|
||||
billing_type: (props.provider.billing_type as 'monthly_quota' | 'pay_as_you_go' | 'free_tier') || 'pay_as_you_go',
|
||||
monthly_quota_usd: props.provider.monthly_quota_usd || undefined,
|
||||
quota_reset_day: props.provider.quota_reset_day || 30,
|
||||
quota_last_reset_at: props.provider.quota_last_reset_at ?
|
||||
new Date(props.provider.quota_last_reset_at).toISOString().slice(0, 16) : '',
|
||||
quota_expires_at: props.provider.quota_expires_at ?
|
||||
new Date(props.provider.quota_expires_at).toISOString().slice(0, 16) : '',
|
||||
quota_last_reset_at: formatDateTimeLocalInput(props.provider.quota_last_reset_at),
|
||||
quota_expires_at: formatDateTimeLocalInput(props.provider.quota_expires_at),
|
||||
provider_priority: props.provider.provider_priority || 999,
|
||||
keep_priority_on_conversion: props.provider.keep_priority_on_conversion ?? false,
|
||||
is_active: props.provider.is_active,
|
||||
@@ -452,6 +451,17 @@ const handleSubmit = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const quotaLastResetAt = dateTimeLocalToRfc3339(form.value.quota_last_reset_at)
|
||||
if (form.value.billing_type === 'monthly_quota' && !quotaLastResetAt) {
|
||||
showError('周期开始时间必须是合法时间', '验证失败')
|
||||
return
|
||||
}
|
||||
const quotaExpiresAt = dateTimeLocalToRfc3339(form.value.quota_expires_at)
|
||||
if (form.value.quota_expires_at && !quotaExpiresAt) {
|
||||
showError('过期时间必须是合法时间', '验证失败')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const currentPoolAdvanced = normalizePoolAdvancedConfig(props.provider?.pool_advanced)
|
||||
@@ -463,8 +473,8 @@ const handleSubmit = async () => {
|
||||
billing_type: form.value.billing_type,
|
||||
monthly_quota_usd: form.value.monthly_quota_usd,
|
||||
quota_reset_day: form.value.quota_reset_day,
|
||||
quota_last_reset_at: form.value.quota_last_reset_at || undefined,
|
||||
quota_expires_at: form.value.quota_expires_at || undefined,
|
||||
quota_last_reset_at: quotaLastResetAt,
|
||||
quota_expires_at: quotaExpiresAt,
|
||||
keep_priority_on_conversion: form.value.keep_priority_on_conversion,
|
||||
is_active: form.value.is_active,
|
||||
// 请求配置
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseDateLike } from '../date'
|
||||
import { dateTimeLocalToRfc3339, formatDateTimeLocalInput, parseDateLike } from '../date'
|
||||
|
||||
function padDatePart(value: number): string {
|
||||
return String(value).padStart(2, '0')
|
||||
}
|
||||
|
||||
describe('parseDateLike', () => {
|
||||
it('parses date-only strings as local calendar dates', () => {
|
||||
@@ -17,3 +21,27 @@ describe('parseDateLike', () => {
|
||||
expect(date.toISOString()).toBe('2026-04-12T15:30:00.000Z')
|
||||
})
|
||||
})
|
||||
|
||||
describe('datetime-local conversion', () => {
|
||||
it('formats RFC3339 instants for datetime-local inputs using local clock fields', () => {
|
||||
const date = new Date('2026-04-12T15:30:00Z')
|
||||
const expected = [
|
||||
date.getFullYear(),
|
||||
padDatePart(date.getMonth() + 1),
|
||||
padDatePart(date.getDate()),
|
||||
].join('-') + `T${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}`
|
||||
|
||||
expect(formatDateTimeLocalInput('2026-04-12T15:30:00Z')).toBe(expected)
|
||||
})
|
||||
|
||||
it('converts datetime-local values to RFC3339 instants', () => {
|
||||
expect(dateTimeLocalToRfc3339('2026-04-12T15:30')).toBe(
|
||||
new Date(2026, 3, 12, 15, 30).toISOString(),
|
||||
)
|
||||
})
|
||||
|
||||
it('omits blank datetime-local values', () => {
|
||||
expect(dateTimeLocalToRfc3339('')).toBeUndefined()
|
||||
expect(formatDateTimeLocalInput(undefined)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/
|
||||
|
||||
function padDatePart(value: number): string {
|
||||
return String(value).padStart(2, '0')
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 `YYYY-MM-DD` 解析为本地时区日期,避免浏览器按 UTC 解析后串天。
|
||||
* 其他带时间/时区的信息仍交给原生 Date 处理。
|
||||
@@ -13,3 +17,26 @@ export function parseDateLike(dateString: string): Date {
|
||||
const [, year, month, day] = matched
|
||||
return new Date(Number(year), Number(month) - 1, Number(day))
|
||||
}
|
||||
|
||||
export function formatDateTimeLocalInput(dateString: string | null | undefined): string {
|
||||
if (!dateString) return ''
|
||||
|
||||
const date = new Date(dateString)
|
||||
if (Number.isNaN(date.getTime())) return ''
|
||||
|
||||
const year = date.getFullYear()
|
||||
const month = padDatePart(date.getMonth() + 1)
|
||||
const day = padDatePart(date.getDate())
|
||||
const hours = padDatePart(date.getHours())
|
||||
const minutes = padDatePart(date.getMinutes())
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`
|
||||
}
|
||||
|
||||
export function dateTimeLocalToRfc3339(value: string | null | undefined): string | undefined {
|
||||
const trimmed = value?.trim()
|
||||
if (!trimmed) return undefined
|
||||
|
||||
const parsed = new Date(trimmed)
|
||||
return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString()
|
||||
}
|
||||
|
||||
@@ -1328,7 +1328,10 @@ const DEFAULT_ENABLED_PRESETS = new Set(['cache_affinity', 'recent_refresh'])
|
||||
|
||||
const DEFAULT_PRESET_LABELS: Record<string, string> = {
|
||||
lru: 'LRU',
|
||||
free_team_first: 'Free/Team',
|
||||
free_first: 'Free',
|
||||
team_first: 'Team',
|
||||
plus_first: 'Plus',
|
||||
pro_first: 'Pro',
|
||||
recent_refresh: '刷新优先',
|
||||
quota_balanced: '额度均衡',
|
||||
single_account: '单号优先',
|
||||
@@ -1400,12 +1403,15 @@ const poolSchedulingLabel = computed(() => {
|
||||
return '多维评分'
|
||||
}
|
||||
|
||||
const lruEnabled = cfg.lru_enabled !== false
|
||||
const lruEnabled = cfg.scheduling_mode === 'lru' || cfg.lru_enabled === true
|
||||
const stickyTtl = Number(cfg.sticky_session_ttl_seconds ?? 3600)
|
||||
const stickyEnabled = Number.isFinite(stickyTtl) && stickyTtl > 0
|
||||
|
||||
if (lruEnabled && stickyEnabled) return 'LRU + 粘性'
|
||||
if (lruEnabled) return 'LRU'
|
||||
if (!cfg.scheduling_mode && (cfg.lru_enabled === null || cfg.lru_enabled === undefined)) {
|
||||
return `${DEFAULT_ENABLED_PRESETS.size} 维度`
|
||||
}
|
||||
if (stickyEnabled) return '粘性'
|
||||
return '随机'
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user