merge: sync batch image branch with origin main

This commit is contained in:
Turtle_Li committed 2026-07-07 03:27:11 +08:00
commit 1b07fe821a
107 files changed
+4469 -263

No files matched your search

+10 -3
View File
@@ -679,14 +679,21 @@ const formatStickySchedulerScore = (score: AccountSchedulerGroupScore): string =
}
const getSchedulerScoreRows = (account: Account): AccountSchedulerGroupScore[] => {
if (!Array.isArray(account.scheduler_scores)) return []
return account.scheduler_scores.filter(score => score.group_id != null)
const groupRows = Array.isArray(account.scheduler_scores)
? account.scheduler_scores.filter(score => score.group_id != null)
: []
if (groupRows.length) return groupRows
// 未分组账号没有分组维度分数,回退展示后端返回的基础分
if (account.scheduler_score) {
return [{ group_id: null, ...account.scheduler_score }]
}
return []
}
const formatSchedulerScoreGroup = (score: AccountSchedulerGroupScore): string => {
if ('group_name' in score && score.group_name) return score.group_name
if ('group_id' in score && score.group_id != null) return `#${score.group_id}`
return '-'
return t('admin.accounts.schedulerScore.ungrouped')
}
const loadSavedColumns = () => {
+31
View File
@@ -6480,6 +6480,34 @@
}}
</p>
</div>
<div>
<label class="input-label">{{
t("admin.settings.payment.subscriptionUsdToCnyRate")
}}</label>
<input
:value="form.payment_subscription_usd_to_cny_rate || ''"
@input="
form.payment_subscription_usd_to_cny_rate =
parseFloat(
($event.target as HTMLInputElement).value,
) || 0
"
type="number"
step="0.01"
min="0"
class="input"
:placeholder="
t(
'admin.settings.payment.subscriptionUsdToCnyRateDisabled',
)
"
/>
<p class="mt-0.5 text-xs text-gray-400">
{{
t("admin.settings.payment.subscriptionUsdToCnyRateHint")
}}
</p>
</div>
<div>
<label class="input-label">{{
t("admin.settings.payment.rechargeFeeRate")
@@ -8037,6 +8065,7 @@ const form = reactive<SettingsForm>({
payment_order_timeout_minutes: 30,
payment_balance_disabled: false,
payment_balance_recharge_multiplier: 1,
payment_subscription_usd_to_cny_rate: 0,
payment_recharge_fee_rate: 0,
payment_enabled_types: [],
payment_help_image_url: "",
@@ -9538,6 +9567,8 @@ async function saveSettings() {
payment_balance_disabled: form.payment_balance_disabled,
payment_balance_recharge_multiplier:
Number(form.payment_balance_recharge_multiplier) || 1,
payment_subscription_usd_to_cny_rate:
Number(form.payment_subscription_usd_to_cny_rate) || 0,
payment_recharge_fee_rate: Number(form.payment_recharge_fee_rate) || 0,
payment_enabled_types: form.payment_enabled_types,
payment_load_balance_strategy: form.payment_load_balance_strategy,
@@ -0,0 +1,224 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import AccountsView from '../AccountsView.vue'
const {
listAccounts,
listWithEtag,
getBatchTodayStats,
getAllProxies,
getAllGroups
} = vi.hoisted(() => ({
listAccounts: vi.fn(),
listWithEtag: vi.fn(),
getBatchTodayStats: vi.fn(),
getAllProxies: vi.fn(),
getAllGroups: vi.fn()
}))
vi.mock('@/api/admin', () => ({
adminAPI: {
accounts: {
list: listAccounts,
listWithEtag,
getBatchTodayStats,
delete: vi.fn(),
batchClearError: vi.fn(),
batchRefresh: vi.fn(),
toggleSchedulable: vi.fn()
},
proxies: {
getAll: getAllProxies
},
groups: {
getAll: getAllGroups
}
}
}))
vi.mock('@/stores/app', () => ({
useAppStore: () => ({
showError: vi.fn(),
showSuccess: vi.fn(),
showInfo: vi.fn()
})
}))
vi.mock('@/stores/auth', () => ({
useAuthStore: () => ({
token: 'test-token'
})
}))
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({
t: (key: string) => key
})
}
})
// Render the scheduler-score cell slot for every row so the fallback logic is observable.
const DataTableStub = {
props: ['columns', 'data'],
template: `
<div data-test="data-table">
<div v-for="row in data" :key="row.id" :data-test="'scheduler-score-' + row.id">
<slot name="cell-scheduler_score" :row="row" />
</div>
</div>
`
}
function mountView() {
return mount(AccountsView, {
global: {
stubs: {
AppLayout: { template: '<div><slot /></div>' },
TablePageLayout: {
template: '<div><slot name="filters" /><slot name="table" /><slot name="pagination" /></div>'
},
DataTable: DataTableStub,
HelpTooltip: true,
Pagination: true,
ConfirmDialog: true,
AccountTableActions: { template: '<div><slot name="beforeCreate" /><slot name="after" /></div>' },
AccountTableFilters: { template: '<div></div>' },
AccountBulkActionsBar: true,
AccountActionMenu: true,
ImportDataModal: true,
ReAuthAccountModal: true,
AccountTestModal: true,
AccountStatsModal: true,
ScheduledTestsPanel: true,
SyncFromCrsModal: true,
TempUnschedStatusModal: true,
ErrorPassthroughRulesModal: true,
TLSFingerprintProfilesModal: true,
CreateAccountModal: true,
EditAccountModal: true,
BulkEditAccountModal: true,
PlatformTypeBadge: true,
AccountCapacityCell: true,
AccountStatusIndicator: true,
AccountTodayStatsCell: true,
AccountGroupsCell: true,
AccountUsageCell: true,
Icon: true
}
}
})
}
const baseAccount = {
platform: 'openai',
type: 'apikey',
status: 'active',
schedulable: true,
concurrency: 1,
priority: 0,
error_message: null,
last_used_at: null,
expires_at: null,
auto_pause_on_expired: false,
created_at: '2026-01-01T00:00:00Z',
updated_at: '2026-01-01T00:00:00Z'
}
describe('admin AccountsView scheduler score column', () => {
beforeEach(() => {
localStorage.clear()
listAccounts.mockReset()
listWithEtag.mockReset()
getBatchTodayStats.mockReset()
getAllProxies.mockReset()
getAllGroups.mockReset()
listAccounts.mockResolvedValue({
items: [
{
...baseAccount,
id: 1,
name: 'ungrouped-openai',
// 未分组账号:后端只返回基础分(scheduler_score),无分组维度分数
scheduler_score: {
base_score: 1.234567,
sticky_score: 0,
sticky_weighted_enabled: false
}
},
{
...baseAccount,
id: 2,
name: 'grouped-openai',
scheduler_score: {
base_score: 2,
sticky_score: 3,
sticky_weighted_enabled: true
},
scheduler_scores: [
{
group_id: 5,
group_name: 'group-five',
base_score: 2,
sticky_score: 3,
sticky_weighted_enabled: true
}
]
},
{
...baseAccount,
id: 3,
name: 'no-score',
platform: 'anthropic'
}
],
total: 3,
page: 1,
page_size: 20,
pages: 1
})
listWithEtag.mockResolvedValue({
notModified: true,
etag: null,
data: null
})
getBatchTodayStats.mockResolvedValue({ stats: {} })
getAllProxies.mockResolvedValue([])
getAllGroups.mockResolvedValue([])
})
it('falls back to the base score for ungrouped accounts instead of showing a dash', async () => {
const wrapper = mountView()
await flushPromises()
const ungroupedCell = wrapper.find('[data-test="scheduler-score-1"]')
expect(ungroupedCell.exists()).toBe(true)
expect(ungroupedCell.text()).toContain('1.234567')
expect(ungroupedCell.text()).toContain('admin.accounts.schedulerScore.ungrouped')
expect(ungroupedCell.text()).not.toBe('-')
})
it('renders per-group scores for grouped accounts', async () => {
const wrapper = mountView()
await flushPromises()
const groupedCell = wrapper.find('[data-test="scheduler-score-2"]')
expect(groupedCell.exists()).toBe(true)
expect(groupedCell.text()).toContain('group-five')
expect(groupedCell.text()).toContain('2')
})
it('still shows a dash when no scheduler score is available', async () => {
const wrapper = mountView()
await flushPromises()
const emptyCell = wrapper.find('[data-test="scheduler-score-3"]')
expect(emptyCell.exists()).toBe(true)
expect(emptyCell.text()).toBe('-')
})
})
@@ -412,6 +412,7 @@ const baseSettingsResponse = {
payment_enabled_types: [],
payment_balance_disabled: false,
payment_balance_recharge_multiplier: 1,
payment_subscription_usd_to_cny_rate: 0,
payment_recharge_fee_rate: 0,
payment_load_balance_strategy: "round-robin",
payment_product_name_prefix: "",