chore(frontend): upgrade ESLint config to v10 and migrate formatting (#522)

* chore(deps-dev): bump @antfu/eslint-config in /frontend

Bumps [@antfu/eslint-config](https://github.com/antfu/eslint-config) from 9.5.1 to 10.0.0.
- [Release notes](https://github.com/antfu/eslint-config/releases)
- [Commits](https://github.com/antfu/eslint-config/compare/v9.5.1...v10.0.0)

---
updated-dependencies:
- dependency-name: "@antfu/eslint-config"
  dependency-version: 10.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(frontend): migrate formatting to ESLint v10

Co-authored-by: Codex <noreply@openai.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zyy <zyycn@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
dependabot[bot]
2026-10-26 11:34:32 +08:00
committed by GitHub
co-authored by Codex Zyy
parent dffc417b9f
commit f0484c4e11
224 changed files with 6822 additions and 1659 deletions
+3 -1
View File
@@ -1,2 +1,4 @@
export const API_BASE_URL = import.meta.env.DEV ? '/dev' : ''
export const API_BASE_URL = import.meta.env.DEV
? '/dev'
: ''
export const API_TIMEOUT_MS = 120_000
+4 -1
View File
@@ -711,7 +711,10 @@ export function startAccountOAuth(data: AccountOAuthStartParam, options: Request
function providerInputBody(data: object) {
// Provider 文档是不透明 JSON,先编码,避免 HTTP 客户端合并配置时过滤特殊字段名。
return { headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(data) }
return {
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(data),
}
}
export function completeAccountOAuth(data: AccountOAuthCompleteParam, options: RequestOptions = {}) {
@@ -92,12 +92,16 @@ export function callPluginManagementRoute(
call: PluginManagementRawRequest,
options: RequestOptions = {},
) {
const query = call.query ? `?${call.query}` : ''
const query = call.query
? `?${call.query}`
: ''
return requestPluginManagementRaw({
url: `${pluginManagementTargetPath(target)}/api/${encodePluginPath(call.path)}${query}`,
method: call.method,
data: call.body?.byteLength ? call.body : undefined,
headers: call.contentType ? { 'Content-Type': call.contentType } : undefined,
headers: call.contentType
? { 'Content-Type': call.contentType }
: undefined,
timeout: PLUGIN_MANAGEMENT_TIMEOUT_MS,
...options,
})
+22 -4
View File
@@ -33,10 +33,21 @@ const http = axios.create({
})
// 从外到内:提示、会话恢复、重试、错误归一化、原始响应处理、实际传输
const plugins: RequestPlugin[] = [feedbackPlugin, sessionPlugin, retryPlugin, errorPlugin, rawPlugin]
const plugins: RequestPlugin[] = [
feedbackPlugin,
sessionPlugin,
retryPlugin,
errorPlugin,
rawPlugin,
]
function execute(config: RequestConfig, raw?: RawResponseOptions, stream = false) {
const context: RequestContext = { config, raw, stream, isCurrent: () => !config.signal?.aborted }
const context: RequestContext = {
config,
raw,
stream,
isCurrent: () => !config.signal?.aborted,
}
const transport: RequestNext = () => http.request<unknown>(context.config)
return plugins.reduceRight<RequestNext>((next, plugin) => () => plugin(context, next), transport)()
}
@@ -57,9 +68,16 @@ export async function requestRaw(config: RequestConfig, options: RawResponseOpti
export async function requestStream(config: RequestConfig, options: RawResponseOptions): Promise<Response> {
// 成功正文保持流式交付;宿主错误仍由同一管线识别和恢复,禁止网络错误重放写操作
const controller = new AbortController()
const signal = config.signal ? AbortSignal.any([config.signal, controller.signal]) : controller.signal
const signal = config.signal
? AbortSignal.any([config.signal, controller.signal])
: controller.signal
try {
const { data } = await execute({ ...config, adapter: 'fetch', retry: false, signal }, options, true)
const { data } = await execute({
...config,
adapter: 'fetch',
retry: false,
signal,
}, options, true)
return data as Response
}
catch (error) {
+18 -6
View File
@@ -19,7 +19,9 @@ export async function errorPlugin(_context: RequestContext, next: RequestNext) {
return response
}
catch (error) {
throw isAxiosError(error) ? normalizeApiError(error) : error
throw isAxiosError(error)
? normalizeApiError(error)
: error
}
}
@@ -64,7 +66,9 @@ export function normalizeApiResponseError(response: AxiosResponse<unknown>) {
const envelope = adminErrorEnvelope(response.data)
if (!envelope || envelope.code === 200)
return undefined
const message = adminErrorMessage(envelope) || httpFallback(response.status >= 400 ? response.status : 0)
const message = adminErrorMessage(envelope) || httpFallback(response.status >= 400
? response.status
: 0)
return new ApiError(
message,
response.status,
@@ -80,7 +84,9 @@ function adminErrorEnvelope(value: unknown): AdminErrorEnvelope | null {
const record = value as Record<string, unknown>
if (typeof record.code !== 'number' || !Number.isSafeInteger(record.code))
return null
const message = typeof record.message === 'string' ? record.message.trim() : ''
const message = typeof record.message === 'string'
? record.message.trim()
: ''
return { code: record.code, message }
}
@@ -109,8 +115,12 @@ function responseHeader(
) {
if (!headers)
return undefined
const value = typeof headers.get === 'function' ? headers.get(name) : headers[name]
return typeof value === 'string' && value ? value : undefined
const value = typeof headers.get === 'function'
? headers.get(name)
: headers[name]
return typeof value === 'string' && value
? value
: undefined
}
function httpFallback(status: number) {
@@ -144,7 +154,9 @@ function httpFallback(status: number) {
case 504:
return t('common.errors.upstreamTimeout')
default:
return status > 0 ? t('common.errors.http', { status }) : t('common.errors.request')
return status > 0
? t('common.errors.http', { status })
: t('common.errors.request')
}
}
+9 -3
View File
@@ -70,7 +70,9 @@ export async function rawPlugin(context: RequestContext, next: RequestNext) {
if (normalized)
throw normalized
throw new ApiError(
response.status >= 400 ? t('common.errors.http', { status: response.status }) : t('common.errors.unsupportedResponse'),
response.status >= 400
? t('common.errors.http', { status: response.status })
: t('common.errors.unsupportedResponse'),
response.status,
undefined,
rawHeader(response.headers, 'x-request-id'),
@@ -111,8 +113,12 @@ function rawHeader(headers: unknown, name: string) {
if (!headers || typeof headers !== 'object')
return undefined
const source = headers as { get?: (key: string) => unknown, [key: string]: unknown }
const value = typeof source.get === 'function' ? source.get(name) : source[name]
return typeof value === 'string' && value ? value : undefined
const value = typeof source.get === 'function'
? source.get(name)
: source[name]
return typeof value === 'string' && value
? value
: undefined
}
function decodeRawEnvelope(body: ArrayBuffer): unknown {
+3 -1
View File
@@ -84,7 +84,9 @@ export async function sessionPlugin(context: RequestContext, send: RequestNext)
if (!isSessionRequired(error) || config.signal?.aborted || generation !== sessionGeneration)
throw error
if (!config.skipSessionRecovery && sessionRecoveryHandler
&& await (sessionRecovery ?? (successfulRecoveries !== recovered ? true : recoverSession()))) {
&& await (sessionRecovery ?? (successfulRecoveries !== recovered
? true
: recoverSession()))) {
// 迟到的 40101 也须等待当前续期;Cookie 更新完成后原请求最多重放一次
if (config.signal?.aborted || generation !== sessionGeneration)
throw error
@@ -11,7 +11,9 @@ const props = defineProps<{
const { t } = useI18n({ useScope: 'global' })
const fullNames = computed(() =>
props.groups.map(group => (group.enabled ? group.name : t('common.status.disabledName', { name: group.name }))).join('、'),
props.groups.map(group => (group.enabled
? group.name
: t('common.status.disabledName', { name: group.name }))).join('、'),
)
</script>
@@ -24,7 +24,9 @@ const hasAnimatedValue = computed(
&& Number.isFinite(props.rawValue)
&& Boolean(props.formatter),
)
const sourceValue = computed(() => (hasAnimatedValue.value ? (props.rawValue ?? 0) : 0))
const sourceValue = computed(() => (hasAnimatedValue.value
? (props.rawValue ?? 0)
: 0))
const disabled = computed(() => preferredMotion.value === 'reduce' || !hasAnimatedValue.value)
const animatedValue = useTransition(sourceValue, {
duration: computed(() => props.duration),
@@ -33,7 +35,9 @@ const animatedValue = useTransition(sourceValue, {
})
const displayValue = computed(() =>
hasAnimatedValue.value && props.formatter ? props.formatter(animatedValue.value) : props.value,
hasAnimatedValue.value && props.formatter
? props.formatter(animatedValue.value)
: props.value,
)
</script>
+3 -1
View File
@@ -39,7 +39,9 @@ async function selectLocale(value: Locale, close: () => void) {
close()
}
catch {
notification.error({ message: t('navigation.languageLoadFailed') })
notification.error({
message: t('navigation.languageLoadFailed'),
})
}
finally {
if (pendingLocale.value === value)
+6 -1
View File
@@ -4,7 +4,12 @@ import { useI18n } from 'vue-i18n'
const props = defineProps<{ value: string | null, fullDisplay: string | null }>()
const { locale } = useI18n({ useScope: 'global' })
const shortTimeUnits: Record<string, string> = { sec: 's', min: 'min', hr: 'h', day: 'd' }
const shortTimeUnits: Record<string, string> = {
sec: 's',
min: 'min',
hr: 'h',
day: 'd',
}
const display = useTimeAgoIntl(() => props.value ?? 0, {
get locale() { return locale.value },
+10 -2
View File
@@ -13,8 +13,16 @@ const { t } = useI18n({ useScope: 'global' })
const provider = defineModel<string>({ required: true })
const options = computed(() => ([
{ label: t('common.providers.all'), value: '', icon: LayoutGrid },
...PROVIDER_IDS.map(value => ({ label: formatProviderLabel(value), value, icon: providerIcon(value) })),
{
label: t('common.providers.all'),
value: '',
icon: LayoutGrid,
},
...PROVIDER_IDS.map(value => ({
label: formatProviderLabel(value),
value,
icon: providerIcon(value),
})),
]))
</script>
+12 -4
View File
@@ -18,23 +18,31 @@ const props = withDefaults(
const { t } = useI18n({ useScope: 'global' })
const normalizedProvider = computed(() => (props.provider ?? '').trim().toLowerCase())
const providerIconComponent = computed(() => normalizedProvider.value ? providerIcon(normalizedProvider.value) : undefined)
const providerIconComponent = computed(() => normalizedProvider.value
? providerIcon(normalizedProvider.value)
: undefined)
const authenticationIconComponent = computed(() => authenticationIcon(props.authenticationKind))
const showAuthenticationKind = computed(() => props.authenticationKind !== undefined)
const groupGapClass = computed(() => {
if (props.size === 'xs')
return 'gap-0.75'
return props.size === 'sm' ? 'gap-1' : 'gap-2'
return props.size === 'sm'
? 'gap-1'
: 'gap-2'
})
const iconContainerClass = computed(() => {
if (props.size === 'xs')
return 'size-4.5 rounded-md'
return props.size === 'sm' ? 'size-5 rounded-md' : 'size-7 rounded-lg'
return props.size === 'sm'
? 'size-5 rounded-md'
: 'size-7 rounded-lg'
})
const iconClass = computed(() => {
if (props.size === 'xs')
return 'size-2.5'
return props.size === 'sm' ? 'size-3' : 'size-4'
return props.size === 'sm'
? 'size-3'
: 'size-4'
})
const providerLabel = computed(() => formatProviderLabel(props.provider, t('common.providers.unknown')))
@@ -30,8 +30,12 @@ const fallbackPalettes = [
'bg-cp-orange-container text-cp-orange-on-container',
] as const
const rawPlanType = computed(() => props.authenticationKind === 'api_key' ? 'api' : props.planType?.trim() || '')
const displayText = computed(() => props.authenticationKind === 'api_key' ? 'API' : props.planTypeDisplay)
const rawPlanType = computed(() => props.authenticationKind === 'api_key'
? 'api'
: props.planType?.trim() || '')
const displayText = computed(() => props.authenticationKind === 'api_key'
? 'API'
: props.planTypeDisplay)
const sizeClass = computed(() => {
if (props.size === 'xs')
@@ -130,7 +130,9 @@ export function resolveAccountUsageWindowPresentation(
const quota = input.window
? quotaWindowPresentation(input.window, definition.minimumBarWidth)
: null
const label = input.window ? quotaWindowLabel(input.window) : undefined
const label = input.window
? quotaWindowLabel(input.window)
: undefined
return {
mode,
@@ -205,11 +207,13 @@ function requestBuckets(value: unknown) {
) {
return []
}
return [{
bucketStart: item.bucketStart,
label: item.label,
requestCount: item.requestCount,
}]
return [
{
bucketStart: item.bucketStart,
label: item.label,
requestCount: item.requestCount,
},
]
})
}
@@ -241,11 +245,26 @@ export function quotaWindowLocalUsageDisplay(window: AccountQuotaWindow) {
}
const quotaWindowPeriods = [
{ seconds: 18_000, label: () => t('accounts.quota.windows.fiveHour') },
{ seconds: 86_400, label: () => t('accounts.quota.windows.daily') },
{ seconds: 604_800, label: () => t('accounts.quota.windows.weekly') },
{ seconds: 2_592_000, label: () => t('accounts.quota.windows.monthly') },
{ seconds: 31_536_000, label: () => t('accounts.quota.windows.annual') },
{
seconds: 18_000,
label: () => t('accounts.quota.windows.fiveHour'),
},
{
seconds: 86_400,
label: () => t('accounts.quota.windows.daily'),
},
{
seconds: 604_800,
label: () => t('accounts.quota.windows.weekly'),
},
{
seconds: 2_592_000,
label: () => t('accounts.quota.windows.monthly'),
},
{
seconds: 31_536_000,
label: () => t('accounts.quota.windows.annual'),
},
]
export function quotaWindowLabel(window: AccountQuotaWindow, includeLimitName = true) {
@@ -261,12 +280,21 @@ export function quotaWindowLabel(window: AccountQuotaWindow, includeLimitName =
label = period.label()
}
else if (seconds !== null && seconds > 0) {
if (seconds % 86_400 === 0)
label = t('accounts.quota.windows.days', { duration: formatInteger(seconds / 86_400) })
else if (seconds % 3_600 === 0)
label = t('accounts.quota.windows.hours', { duration: formatInteger(seconds / 3_600) })
else
label = t('accounts.quota.windows.minutes', { duration: formatInteger(Math.ceil(seconds / 60)) })
if (seconds % 86_400 === 0) {
label = t('accounts.quota.windows.days', {
duration: formatInteger(seconds / 86_400),
})
}
else if (seconds % 3_600 === 0) {
label = t('accounts.quota.windows.hours', {
duration: formatInteger(seconds / 3_600),
})
}
else {
label = t('accounts.quota.windows.minutes', {
duration: formatInteger(Math.ceil(seconds / 60)),
})
}
}
else if (window.role === 'monthly') {
label = t('accounts.quota.windows.monthly')
@@ -291,7 +319,9 @@ function requestCountDisplay(localUsage: AccountLocalUsage | null) {
if (typeof display === 'string' && display.trim())
return display.trim()
const count = localUsage?.requestCount
return typeof count === 'number' ? formatInteger(count) : '0'
return typeof count === 'number'
? formatInteger(count)
: '0'
}
function rollingWindowDurationDisplay(windowSeconds: number | null | undefined) {
@@ -300,13 +330,24 @@ function rollingWindowDurationDisplay(windowSeconds: number | null | undefined)
const hourSeconds = 60 * 60
const daySeconds = 24 * hourSeconds
if (windowSeconds <= daySeconds * 2 && windowSeconds % hourSeconds === 0)
return t('accounts.quota.h', { duration: formatInteger(windowSeconds / hourSeconds) })
if (windowSeconds % daySeconds === 0)
return t('accounts.quota.d', { duration: formatInteger(windowSeconds / daySeconds) })
if (windowSeconds % hourSeconds === 0)
return t('accounts.quota.h', { duration: formatInteger(windowSeconds / hourSeconds) })
return t('accounts.quota.s', { duration: formatInteger(windowSeconds) })
if (windowSeconds <= daySeconds * 2 && windowSeconds % hourSeconds === 0) {
return t('accounts.quota.h', {
duration: formatInteger(windowSeconds / hourSeconds),
})
}
if (windowSeconds % daySeconds === 0) {
return t('accounts.quota.d', {
duration: formatInteger(windowSeconds / daySeconds),
})
}
if (windowSeconds % hourSeconds === 0) {
return t('accounts.quota.h', {
duration: formatInteger(windowSeconds / hourSeconds),
})
}
return t('accounts.quota.s', {
duration: formatInteger(windowSeconds),
})
}
export function quotaWindowPresentation(window: AccountQuotaWindow, minimumWidth: string) {
@@ -315,7 +356,9 @@ export function quotaWindowPresentation(window: AccountQuotaWindow, minimumWidth
return {
barStyle: {
width: `${percent}%`,
minWidth: percent > 0 ? minimumWidth : '0',
minWidth: percent > 0
? minimumWidth
: '0',
},
barClass: quotaToneClasses[tone].bar,
percentTextClass: quotaToneClasses[tone].text,
@@ -337,7 +380,9 @@ function requestTimeline(buckets: NonNullable<AccountLocalUsage['requestBuckets'
return buckets.map(bucket => ({
key: bucket.bucketStart,
requestCount: bucket.requestCount,
height: bucket.requestCount === 0 ? '0' : `${Math.max(25, Math.round(bucket.requestCount / maximum * 100))}%`,
height: bucket.requestCount === 0
? '0'
: `${Math.max(25, Math.round(bucket.requestCount / maximum * 100))}%`,
title: bucket.label,
}))
}
@@ -76,7 +76,10 @@ export function requestActivityByBucket(
activity: Array<{ bucket: string, totalRequests: number }>,
) {
const requestsByBucket = new Map(
activity.map(point => [point.bucket, Math.max(0, point.totalRequests)]),
activity.map(point => [
point.bucket,
Math.max(0, point.totalRequests),
]),
)
return buckets.map(bucket => (requestsByBucket.get(bucket) ?? 0) > 0)
}
@@ -85,11 +88,15 @@ export function zeroInactiveValues(
values: TemporalValue[],
active: boolean[],
) {
return values.map((value, index) => normalizeValue(value) ?? (active[index] === false ? 0 : null))
return values.map((value, index) => normalizeValue(value) ?? (active[index] === false
? 0
: null))
}
function normalizeValue(value: TemporalValue) {
return typeof value === 'number' && Number.isFinite(value) ? value : null
return typeof value === 'number' && Number.isFinite(value)
? value
: null
}
function boundedGapRuns(values: Array<number | null>) {
+9 -3
View File
@@ -47,17 +47,23 @@ export function chartTooltipStyle(
}
export function chartTooltipContent(label: string, lines: string[]) {
const title = label ? `<span style="font-size:13px;line-height:20px">${escape(label)}</span>` : ''
const title = label
? `<span style="font-size:13px;line-height:20px">${escape(label)}</span>`
: ''
return [title, ...lines].filter(Boolean).join('<br/>')
}
export function tooltipRows(params: unknown): Record<string, unknown>[] {
const values = Array.isArray(params) ? params : [params]
const values = Array.isArray(params)
? params
: [params]
return values.filter(isRecord)
}
export function tooltipIndex(source: unknown) {
if (!isRecord(source))
return -1
return typeof source.dataIndex === 'number' ? source.dataIndex : -1
return typeof source.dataIndex === 'number'
? source.dataIndex
: -1
}
@@ -31,14 +31,18 @@ const previewing = shallowRef(false)
const independentDraft = shallowRef<ClientProfileSelection>()
const effective = computed(() => model.value ?? options.value?.globalConfiguration)
const inherited = computed(() => props.allowInherit && model.value === null)
const preset = computed(() => effective.value && !effective.value.mode ? effective.value : undefined)
const preset = computed(() => effective.value && !effective.value.mode
? effective.value
: undefined)
const custom = computed(() => effective.value?.mode === 'custom' || preset.value?.versionMode === 'fixed')
const needsInput = computed(() => effective.value?.mode === 'custom'
? !effective.value.userAgent.trim()
: preset.value?.versionMode === 'fixed'
&& (!preset.value.codexVersion || (preset.value.client === 'desktop' && (!preset.value.desktopVersion || !preset.value.desktopBuild))))
const profileSource = computed({
get: () => model.value === null ? 'global' : 'independent',
get: () => model.value === null
? 'global'
: 'independent',
set: (value: string) => {
if (value === 'global') {
if (model.value)
@@ -46,7 +50,9 @@ const profileSource = computed({
model.value = null
}
else if (independentDraft.value ?? options.value?.globalConfiguration) {
model.value = { ...(independentDraft.value ?? options.value!.globalConfiguration) }
model.value = {
...(independentDraft.value ?? options.value!.globalConfiguration),
}
}
},
})
@@ -64,7 +70,10 @@ async function load() {
}
}
watch([model, () => props.active], ([configuration, active], _, onCleanup) => {
watch([
model,
() => props.active,
], ([configuration, active], _, onCleanup) => {
if (!active)
return
let cancelled = false
@@ -21,12 +21,16 @@ const { t } = useI18n({ useScope: 'global' })
const model = defineModel<ClientProfileSelection | null>({ required: true })
const presetDraft = shallowRef<PresetClientProfileSelection>()
const customDraft = shallowRef<CustomClientProfileSelection>()
const preset = computed(() => model.value && !model.value.mode ? model.value : undefined)
const preset = computed(() => model.value && !model.value.mode
? model.value
: undefined)
const custom = computed(() => model.value?.mode === 'custom' || preset.value?.versionMode === 'fixed')
const platforms = { macos: 'MacOS', linux: 'Linux', windows: 'Windows' }
const presetOptions = computed(() => props.presets.map(({ configuration }) => ({
value: `${configuration.platform}-${configuration.client}`,
label: `${platforms[configuration.platform]} · ${configuration.client === 'desktop' ? 'Desktop' : 'CLI'}`,
label: `${platforms[configuration.platform]} · ${configuration.client === 'desktop'
? 'Desktop'
: 'CLI'}`,
})))
const currentPreset = computed(() => props.presets.find(({ configuration }) =>
configuration.client === preset.value?.client && configuration.platform === preset.value?.platform,
@@ -34,13 +38,18 @@ const currentPreset = computed(() => props.presets.find(({ configuration }) =>
const selectedPreset = computed({
get: () => {
const selection = preset.value ?? presetDraft.value
return selection ? `${selection.platform}-${selection.client}` : ''
return selection
? `${selection.platform}-${selection.client}`
: ''
},
set: (value: string) => {
const selected = props.presets.find(({ configuration }) => `${configuration.platform}-${configuration.client}` === value)
if (selected) {
customDraft.value = undefined
model.value = { ...selected.configuration, versionMode: selected.automaticAvailable ? 'latest' : 'fixed' }
model.value = {
...selected.configuration,
versionMode: selected.automaticAvailable ? 'latest' : 'fixed',
}
}
},
})
@@ -49,7 +58,12 @@ const cliEntry = computed({
set: (value: string) => {
if (preset.value && (value === 'core' || value === 'tui' || value === 'exec')) {
customDraft.value = undefined
model.value = { ...preset.value, cliEntry: value === 'core' ? null : value }
model.value = {
...preset.value,
cliEntry: value === 'core'
? null
: value,
}
}
},
})
@@ -61,23 +75,39 @@ const versionMode = computed({
return
if (preset.value)
presetDraft.value = { ...preset.value }
model.value = { ...(customDraft.value ?? { mode: 'custom', userAgent: props.preview!.userAgent }) }
model.value = {
...(customDraft.value ?? { mode: 'custom', userAgent: props.preview!.userAgent }),
}
}
else {
if (model.value?.mode === 'custom')
customDraft.value = { ...model.value }
const selection = preset.value ?? presetDraft.value ?? props.presets[0]?.configuration
if (selection) {
model.value = { ...selection, versionMode: 'latest', codexVersion: null, desktopVersion: null, desktopBuild: null }
model.value = {
...selection,
versionMode: 'latest',
codexVersion: null,
desktopVersion: null,
desktopBuild: null,
}
}
}
},
})
const versionLag = computed({
get: () => preset.value?.versionLag == null ? '' : String(preset.value.versionLag),
get: () => preset.value?.versionLag == null
? ''
: String(preset.value.versionLag),
set: (value: string) => {
if (preset.value)
model.value = { ...preset.value, versionLag: value === '' ? null : Number(value) }
if (preset.value) {
model.value = {
...preset.value,
versionLag: value === ''
? null
: Number(value),
}
}
},
})
const versionLagError = computed(() => {
@@ -89,21 +119,37 @@ const versionLagError = computed(() => {
const terminal = computed({
get: () => preset.value?.terminal ?? '',
set: (value: string) => {
if (preset.value)
model.value = { ...preset.value, terminal: value === '' ? null : value }
if (preset.value) {
model.value = {
...preset.value,
terminal: value === ''
? null
: value,
}
}
},
})
const osVersion = computed({
get: () => preset.value?.osVersion ?? '',
set: (value: string) => {
if (preset.value)
model.value = { ...preset.value, osVersion: value === '' ? null : value }
if (preset.value) {
model.value = {
...preset.value,
osVersion: value === ''
? null
: value,
}
}
},
})
const userAgent = computed({
get: () => model.value?.mode === 'custom' ? model.value.userAgent : props.preview?.userAgent ?? '',
get: () => model.value?.mode === 'custom'
? model.value.userAgent
: props.preview?.userAgent ?? '',
set: (value: string) => {
const previous = model.value?.mode === 'custom' ? model.value : undefined
const previous = model.value?.mode === 'custom'
? model.value
: undefined
if (preset.value)
presetDraft.value = { ...preset.value }
// 已知客户端的配套头重新派生,避免修改版本后继续携带旧值。
@@ -111,7 +157,9 @@ const userAgent = computed({
model.value = {
mode: 'custom',
userAgent: value,
...(!recognized && previous ? { originator: previous.originator, codexVersion: previous.codexVersion } : {}),
...(!recognized && previous
? { originator: previous.originator, codexVersion: previous.codexVersion }
: {}),
}
},
})
@@ -24,14 +24,28 @@ const previewError = shallowRef('')
const previewing = shallowRef(false)
const needsVersionInput = computed(() => model.value?.versionMode === 'fixed' && !model.value.clientVersion)
const fields = computed(() => ([
{ key: 'clientIdentifier', label: t('settings.clientProfile.clientId') },
{ key: 'clientMode', label: t('settings.clientProfile.operatingMode') },
{ key: 'targetOs', label: t('settings.clientProfile.operatingSystem') },
{ key: 'targetArch', label: t('settings.clientProfile.cpuArchitecture') },
{
key: 'clientIdentifier',
label: t('settings.clientProfile.clientId'),
},
{
key: 'clientMode',
label: t('settings.clientProfile.operatingMode'),
},
{
key: 'targetOs',
label: t('settings.clientProfile.operatingSystem'),
},
{
key: 'targetArch',
label: t('settings.clientProfile.cpuArchitecture'),
},
] as const))
const source = computed({
get: () => model.value === null ? 'global' : 'independent',
get: () => model.value === null
? 'global'
: 'independent',
set: (value: string) => {
if (value === 'global')
model.value = null
@@ -45,16 +59,26 @@ const versionMode = computed({
if (model.value) {
model.value = {
...model.value,
versionMode: value === 'fixed' ? 'fixed' : 'latest',
clientVersion: value === 'fixed' ? preview.value?.clientVersion ?? null : null,
versionMode: value === 'fixed'
? 'fixed'
: 'latest',
clientVersion: value === 'fixed'
? preview.value?.clientVersion ?? null
: null,
}
}
},
})
function updateField(key: keyof XaiClientProfileSelection, value: string) {
if (model.value)
model.value = { ...model.value, [key]: key === 'clientVersion' ? value || null : value }
if (model.value) {
model.value = {
...model.value,
[key]: key === 'clientVersion'
? value || null
: value,
}
}
}
async function load() {
@@ -67,7 +91,10 @@ async function load() {
}
}
watch([model, () => props.active], ([configuration, active], _, onCleanup) => {
watch([
model,
() => props.active,
], ([configuration, active], _, onCleanup) => {
if (!active)
return
let cancelled = false
+33 -7
View File
@@ -33,7 +33,9 @@ const incompatibleValue = computed(() => fields.value?.some((field) => {
return !field.values.some(option => option === item)
if (field.type === 'number' || field.type === 'integer')
return typeof item !== 'number' && typeof item !== 'string'
return field.type === 'boolean' ? typeof item !== 'boolean' : typeof item !== 'string'
return field.type === 'boolean'
? typeof item !== 'boolean'
: typeof item !== 'string'
}) ?? false)
const showJson = computed(() => jsonMode.value || fields.value === null || hasExtra.value || incompatibleValue.value || Boolean(error.value))
const empty = computed(() => fields.value?.length === 0 && props.schema.additionalProperties === false && !showJson.value)
@@ -48,14 +50,34 @@ function fieldValue(field: InputField) {
}
function inputValue(key: string) {
return value.value && Object.hasOwn(value.value, key) ? value.value[key] : undefined
return value.value && Object.hasOwn(value.value, key)
? value.value[key]
: undefined
}
function fieldOptions(field: InputField) {
const options = field.values
? field.values.map((item, index) => ({ value: String(index), label: String(item) }))
: [{ value: 'true', label: t('common.values.yes') }, { value: 'false', label: t('common.values.no') }]
return [{ value: '', label: t('common.values.unspecified') }, ...options]
? field.values.map((item, index) => ({
value: String(index),
label: String(item),
}))
: [
{
value: 'true',
label: t('common.values.yes'),
},
{
value: 'false',
label: t('common.values.no'),
},
]
return [
{
value: '',
label: t('common.values.unspecified'),
},
...options,
]
}
function update(field: InputField, input: string) {
@@ -71,7 +93,9 @@ function update(field: InputField, input: string) {
}
else if (field.type === 'number' || field.type === 'integer') {
// 不把尚未输入完整的数字静默替换成 0,保留原值供服务端校验。
next = Number.isFinite(Number(input)) ? Number(input) : input
next = Number.isFinite(Number(input))
? Number(input)
: input
}
else {
next = input
@@ -87,7 +111,9 @@ async function focusField(key?: string) {
await nextTick()
const control = editableField && !showJson.value
? [...(root.value?.querySelectorAll<HTMLElement>('[data-schema-control]') ?? [])]
? [
...(root.value?.querySelectorAll<HTMLElement>('[data-schema-control]') ?? []),
]
.find(element => element.dataset.schemaControl === key)
: root.value?.querySelector<HTMLElement>('[data-schema-json-control], textarea')
control?.focus()
+42 -9
View File
@@ -12,29 +12,62 @@ export interface InputField {
// 仅将能无损编辑的平面字段映射为控件,复杂 schema 保留 JSON 编辑,完整校验由服务端负责。
export function inputFields(schema: Record<string, unknown>): InputField[] | null {
if (['$ref', 'allOf', 'anyOf', 'oneOf', 'if', 'dependentSchemas'].some(key => key in schema))
if ([
'$ref',
'allOf',
'anyOf',
'oneOf',
'if',
'dependentSchemas',
].some(key => key in schema)) {
return null
if (!isRecord(schema.properties))
return schema.additionalProperties === false ? [] : null
const required = Array.isArray(schema.required) ? schema.required : []
}
if (!isRecord(schema.properties)) {
return schema.additionalProperties === false
? []
: null
}
const required = Array.isArray(schema.required)
? schema.required
: []
const fields: InputField[] = []
for (const [key, value] of Object.entries(schema.properties)) {
if (!isRecord(value) || !['string', 'number', 'integer', 'boolean'].includes(String(value.type)))
if (!isRecord(value) || ![
'string',
'number',
'integer',
'boolean',
].includes(String(value.type))) {
return null
if (['$ref', 'allOf', 'anyOf', 'oneOf', 'const', 'readOnly'].some(key => key in value))
}
if ([
'$ref',
'allOf',
'anyOf',
'oneOf',
'const',
'readOnly',
].some(key => key in value)) {
return null
}
const values = value.enum
if (values !== undefined && (!Array.isArray(values) || !values.every(item => item === null || ['string', 'number', 'boolean'].includes(typeof item))))
return null
fields.push({
key,
label: typeof value.title === 'string' ? value.title : key,
description: typeof value.description === 'string' ? value.description : undefined,
label: typeof value.title === 'string'
? value.title
: key,
description: typeof value.description === 'string'
? value.description
: undefined,
type: value.type as InputField['type'],
required: required.includes(key),
secret: value.writeOnly === true || value.format === 'password',
values: values as InputField['values'],
})
}
return fields.length || schema.additionalProperties === false ? fields : null
return fields.length || schema.additionalProperties === false
? fields
: null
}
@@ -118,7 +118,9 @@ function animatePointWave(centerIndex: number) {
const cellsByDistance: HTMLElement[][] = [[], [], []]
for (let distance = 0; distance <= 2; distance += 1) {
const candidateIndexes = distance === 0 ? [centerIndex] : [centerIndex - distance, centerIndex + distance]
const candidateIndexes = distance === 0
? [centerIndex]
: [centerIndex - distance, centerIndex + distance]
for (const index of candidateIndexes) {
const button = buttons[index]
@@ -140,7 +142,10 @@ function animatePointWave(centerIndex: number) {
if (preferredMotion.value === 'reduce') {
const cells = cellsByDistance.flat()
gsap.set([...cells, ...cellsToRelease], { clearProps: 'transform,willChange' })
gsap.set([
...cells,
...cellsToRelease,
], { clearProps: 'transform,willChange' })
wavedCellIndexes = nextCellIndexes
return
}
@@ -197,7 +202,12 @@ function releasePointWave() {
function pointAccessibilityLabel(point: DashboardHealthTimelinePoint) {
const eligibleRequests = healthEligibleRequestCount(point)
return t('usage.health.validRequestsAvailability', { time: point.time, label: healthStatusMeta.value[point.status].label, count: formatHealthCount(eligibleRequests), reliabilityDisplay: point.reliabilityDisplay })
return t('usage.health.validRequestsAvailability', {
time: point.time,
label: healthStatusMeta.value[point.status].label,
count: formatHealthCount(eligibleRequests),
reliabilityDisplay: point.reliabilityDisplay,
})
}
watch(popoverOpen, (open) => {
@@ -21,10 +21,26 @@ const billingItems = computed(() => {
return []
return [
{ label: t('usage.billing.serviceLevel'), value: value.serviceTierDisplay, tone: 'info' },
{ label: t('settings.pricing.multiplier'), value: value.multiplierDisplay, tone: 'info' },
{ label: t('usage.billing.totalCost'), value: value.totalAmountDisplay, tone: 'success' },
{ label: t('usage.billing.standardFee'), value: value.standardAmountDisplay, tone: 'default' },
{
label: t('usage.billing.serviceLevel'),
value: value.serviceTierDisplay,
tone: 'info',
},
{
label: t('settings.pricing.multiplier'),
value: value.multiplierDisplay,
tone: 'info',
},
{
label: t('usage.billing.totalCost'),
value: value.totalAmountDisplay,
tone: 'success',
},
{
label: t('usage.billing.standardFee'),
value: value.standardAmountDisplay,
tone: 'default',
},
]
})
@@ -34,21 +50,75 @@ const amountItems = computed(() => {
return []
return [
{ label: value.image ? t('usage.billing.textInputCost') : t('usage.billing.inputCost'), value: value.inputAmountDisplay, accent: false },
{
label: value.image
? t('usage.billing.textInputCost')
: t('usage.billing.inputCost'),
value: value.inputAmountDisplay,
accent: false,
},
...(value.image
? [
{ label: t('usage.billing.imageInputFee'), value: value.image.inputAmountDisplay, accent: false },
{ label: t('usage.billing.imageCachingCharges'), value: value.image.cacheReadAmountDisplay, accent: false },
{ label: t('usage.billing.imageInputUnitPrice'), value: value.image.inputPriceDisplay, accent: true },
{ label: t('usage.billing.imageCacheUnitPrice'), value: value.image.cacheReadPriceDisplay, accent: true },
{
label: t('usage.billing.imageInputFee'),
value: value.image.inputAmountDisplay,
accent: false,
},
{
label: t('usage.billing.imageCachingCharges'),
value: value.image.cacheReadAmountDisplay,
accent: false,
},
{
label: t('usage.billing.imageInputUnitPrice'),
value: value.image.inputPriceDisplay,
accent: true,
},
{
label: t('usage.billing.imageCacheUnitPrice'),
value: value.image.cacheReadPriceDisplay,
accent: true,
},
]
: []),
{ label: value.image ? t('usage.billing.imageOutputFee') : t('usage.billing.outputFee'), value: value.outputAmountDisplay, accent: false },
{ label: value.image ? t('usage.billing.textInputUnitPrice') : t('usage.billing.inputUnitPrice'), value: value.inputPriceDisplay, accent: true },
{ label: value.image ? t('usage.billing.imageOutputUnitPrice') : t('usage.billing.outputUnitPrice'), value: value.outputPriceDisplay, accent: true },
{ label: value.image ? t('usage.billing.textCacheReadCost') : t('usage.billing.cacheReadCost'), value: value.cacheReadAmountDisplay, accent: false },
{ label: t('usage.billing.cacheWriteCost'), value: value.cacheWriteAmountDisplay, accent: false },
{ label: t('usage.billing.cacheWriteUnitPrice'), value: value.cacheWritePriceDisplay, accent: true },
{
label: value.image
? t('usage.billing.imageOutputFee')
: t('usage.billing.outputFee'),
value: value.outputAmountDisplay,
accent: false,
},
{
label: value.image
? t('usage.billing.textInputUnitPrice')
: t('usage.billing.inputUnitPrice'),
value: value.inputPriceDisplay,
accent: true,
},
{
label: value.image
? t('usage.billing.imageOutputUnitPrice')
: t('usage.billing.outputUnitPrice'),
value: value.outputPriceDisplay,
accent: true,
},
{
label: value.image
? t('usage.billing.textCacheReadCost')
: t('usage.billing.cacheReadCost'),
value: value.cacheReadAmountDisplay,
accent: false,
},
{
label: t('usage.billing.cacheWriteCost'),
value: value.cacheWriteAmountDisplay,
accent: false,
},
{
label: t('usage.billing.cacheWriteUnitPrice'),
value: value.cacheWritePriceDisplay,
accent: true,
},
]
})
@@ -11,9 +11,21 @@ const { t } = useI18n({ useScope: 'global' })
const model = defineModel<UsageRecordStatus>({ required: true })
const options = computed(() => ([
{ label: t('common.status.success'), value: 'success', icon: Sun },
{ label: t('usage.filters.error'), value: 'error', icon: CloudRain },
{ label: t('common.values.all'), value: 'all', icon: List },
{
label: t('common.status.success'),
value: 'success',
icon: Sun,
},
{
label: t('usage.filters.error'),
value: 'error',
icon: CloudRain,
},
{
label: t('common.values.all'),
value: 'all',
icon: List,
},
]))
</script>
@@ -17,11 +17,26 @@ const { t } = useI18n({ useScope: 'global' })
const tokenDetails = computed(() => usageTokenDetails(props.record.tokenDetails))
const tokenItems = computed(() => [
{ label: t('usage.tokens.enterToken'), value: tokenDetails.value.inputTokensDisplay },
{ label: t('usage.tokens.outputToken'), value: tokenDetails.value.outputTokensDisplay },
{ label: t('usage.tokens.cacheReadTokens'), value: tokenDetails.value.cachedInputTokensDisplay },
{ label: t('usage.tokens.cacheWriteTokens'), value: tokenDetails.value.cacheWriteInputTokensDisplay },
{ label: t('usage.tokens.reasoningToken'), value: tokenDetails.value.reasoningOutputTokensDisplay },
{
label: t('usage.tokens.enterToken'),
value: tokenDetails.value.inputTokensDisplay,
},
{
label: t('usage.tokens.outputToken'),
value: tokenDetails.value.outputTokensDisplay,
},
{
label: t('usage.tokens.cacheReadTokens'),
value: tokenDetails.value.cachedInputTokensDisplay,
},
{
label: t('usage.tokens.cacheWriteTokens'),
value: tokenDetails.value.cacheWriteInputTokensDisplay,
},
{
label: t('usage.tokens.reasoningToken'),
value: tokenDetails.value.reasoningOutputTokensDisplay,
},
])
</script>
+93 -16
View File
@@ -6,22 +6,85 @@ import { t } from '@/i18n'
import { formatProviderLabel } from '@/utils/providers'
export const usageDataColumns = computed(() => ([
{ key: 'model', label: t('usage.fields.model'), kind: 'custom', size: 'lg' },
{ key: 'reasoningEffort', label: t('usage.fields.reasoningEffort'), kind: 'status', size: 'md' },
{ key: 'route', label: t('usage.fields.endpoint'), kind: 'mono' },
{ key: 'upstreamTransport', label: t('usage.fields.upstream'), kind: 'status', size: 'md' },
{ key: 'clientTransport', label: t('usage.fields.access'), kind: 'status', size: 'md' },
{ key: 'tokenDetails', label: 'TOKEN', kind: 'numeric', size: 'xl' },
{ key: 'billing', label: t('usage.fields.cost'), kind: 'numeric', size: 'xl' },
{ key: 'latency', label: t('usage.fields.latency'), kind: 'numeric', size: 'xl' },
{ key: 'performance', label: t('usage.outputThroughput'), kind: 'numeric', size: 'lg' },
{ key: 'createdAtDisplay', label: t('usage.fields.time'), kind: 'datetime' },
{ key: 'clientIp', label: 'IP', kind: 'custom', size: '3xl' },
{ key: 'userAgent', label: 'User-Agent', kind: 'custom', size: '4xl' },
{
key: 'model',
label: t('usage.fields.model'),
kind: 'custom',
size: 'lg',
},
{
key: 'reasoningEffort',
label: t('usage.fields.reasoningEffort'),
kind: 'status',
size: 'md',
},
{
key: 'route',
label: t('usage.fields.endpoint'),
kind: 'mono',
},
{
key: 'upstreamTransport',
label: t('usage.fields.upstream'),
kind: 'status',
size: 'md',
},
{
key: 'clientTransport',
label: t('usage.fields.access'),
kind: 'status',
size: 'md',
},
{
key: 'tokenDetails',
label: 'TOKEN',
kind: 'numeric',
size: 'xl',
},
{
key: 'billing',
label: t('usage.fields.cost'),
kind: 'numeric',
size: 'xl',
},
{
key: 'latency',
label: t('usage.fields.latency'),
kind: 'numeric',
size: 'xl',
},
{
key: 'performance',
label: t('usage.outputThroughput'),
kind: 'numeric',
size: 'lg',
},
{
key: 'createdAtDisplay',
label: t('usage.fields.time'),
kind: 'datetime',
},
{
key: 'clientIp',
label: 'IP',
kind: 'custom',
size: '3xl',
},
{
key: 'userAgent',
label: 'User-Agent',
kind: 'custom',
size: '4xl',
},
] satisfies TableColumn[]))
export const usageRecordColumns = computed(() => (defineTableColumns<UsageListRecord>([
{ key: 'clientApiKeyName', label: t('usage.fields.keyName'), kind: 'identity', size: 'xl' },
{
key: 'clientApiKeyName',
label: t('usage.fields.keyName'),
kind: 'identity',
size: 'xl',
},
{
key: 'accountEmail',
hideable: false,
@@ -29,15 +92,29 @@ export const usageRecordColumns = computed(() => (defineTableColumns<UsageListRe
kind: 'identity',
size: '3xl',
},
{ key: 'accountPlanType', label: t('usage.fields.subscription'), kind: 'status', size: 'sm', fixedWidth: true },
{
key: 'accountPlanType',
label: t('usage.fields.subscription'),
kind: 'status',
size: 'sm',
fixedWidth: true,
},
{
key: 'provider',
label: t('usage.fields.platformType'),
kind: 'status',
size: 'md',
fixedWidth: true,
formatter: (value: unknown) => formatProviderLabel(typeof value === 'string' ? value : null),
formatter: (value: unknown) => formatProviderLabel(typeof value === 'string'
? value
: null),
},
...usageDataColumns.value,
{ key: 'actions', label: t('common.fields.actions'), kind: 'actions', size: 'sm', hideable: false },
{
key: 'actions',
label: t('common.fields.actions'),
kind: 'actions',
size: 'sm',
hideable: false,
},
])))
+20 -5
View File
@@ -11,11 +11,26 @@ interface DashboardHealthStatusMeta {
}
export const healthLegend = computed(() => ([
{ status: 'no_data', label: t('usage.noValidSample') },
{ status: 'unavailable', label: t('usage.notReachable') },
{ status: 'unstable', label: t('usage.unstable') },
{ status: 'low_sample', label: t('usage.lowSample') },
{ status: 'stable', label: t('usage.stable') },
{
status: 'no_data',
label: t('usage.noValidSample'),
},
{
status: 'unavailable',
label: t('usage.notReachable'),
},
{
status: 'unstable',
label: t('usage.unstable'),
},
{
status: 'low_sample',
label: t('usage.lowSample'),
},
{
status: 'stable',
label: t('usage.stable'),
},
] satisfies { status: DashboardHealthStatus, label: string }[]))
export const healthStatusMeta = computed<Record<DashboardHealthStatus, DashboardHealthStatusMeta>>(() => ({
@@ -38,7 +38,9 @@ export function usageTokenDetails(details: UsageTokenDetails): UsageTokenDetails
return {
...details,
inputTokens,
inputTokensDisplay: inputTokens === null ? details.inputTokensDisplay : formatInteger(inputTokens),
inputTokensDisplay: inputTokens === null
? details.inputTokensDisplay
: formatInteger(inputTokens),
}
}
@@ -106,7 +108,9 @@ export function usagePerformanceDetails(record: UsagePerformanceRecord) {
: null
return {
throughputDisplay: throughput === null ? '—' : `${formatCompactNumber(throughput)} tok/s`,
throughputDisplay: throughput === null
? '—'
: `${formatCompactNumber(throughput)} tok/s`,
firstTokenDisplay: formatDuration(durationValue(record.firstTokenLatencyMs)),
}
}
@@ -121,17 +125,25 @@ export function usageLatencyDetails(record: UsageLatencyRecord) {
const requestItems: UsageTimingItem[] = []
if (firstTokenMs !== null && totalMs !== null && firstTokenMs <= totalMs) {
requestItems.push({ label: t('usage.status.firstOutputWaiting'), value: formatDuration(firstTokenMs) })
requestItems.push({
label: t('usage.status.firstOutputWaiting'),
value: formatDuration(firstTokenMs),
})
if (firstTextMs !== null && firstTextMs >= firstTokenMs && firstTextMs <= totalMs) {
const beforeTextMs = firstTextMs - firstTokenMs
if (beforeTextMs > 0) {
requestItems.push({
label: firstReasoningMs === firstTokenMs ? t('usage.status.reasoningToText') : t('usage.status.firstOneIsOutputToText'),
label: firstReasoningMs === firstTokenMs
? t('usage.status.reasoningToText')
: t('usage.status.firstOneIsOutputToText'),
value: formatDuration(beforeTextMs),
})
}
requestItems.push({ label: t('usage.status.textToCompletion'), value: formatDuration(totalMs - firstTextMs) })
requestItems.push({
label: t('usage.status.textToCompletion'),
value: formatDuration(totalMs - firstTextMs),
})
}
else {
requestItems.push({
@@ -141,21 +153,43 @@ export function usageLatencyDetails(record: UsageLatencyRecord) {
}
}
requestItems.push({ label: t('usage.latency.totalLatency'), value: formatDuration(totalMs), emphasized: true })
requestItems.push({
label: t('usage.latency.totalLatency'),
value: formatDuration(totalMs),
emphasized: true,
})
const transportItems: UsageTimingItem[] = [
{ label: t('usage.status.admissionDecision'), value: durationValue(latencyDetails?.admissionDecisionMs) },
{ label: t('usage.status.accountSelectionWaiting'), value: durationValue(latencyDetails?.accountSelectionWaitMs) },
{
label: t('usage.status.admissionDecision'),
value: durationValue(latencyDetails?.admissionDecisionMs),
},
{
label: t('usage.status.accountSelectionWaiting'),
value: durationValue(latencyDetails?.accountSelectionWaitMs),
},
{
label: t('usage.status.transferDecisionWaiting'),
value: durationValue(latencyDetails?.transportDecisionWaitMs),
},
{ label: t('usage.status.websocketConnection'), value: durationValue(latencyDetails?.wsConnectMs) },
{ label: t('usage.status.upstreamResponseHeader'), value: durationValue(latencyDetails?.upstreamHeadersMs) },
{ label: t('usage.status.firstUpstreamEvent'), value: durationValue(latencyDetails?.firstEventMs) },
{
label: t('usage.status.websocketConnection'),
value: durationValue(latencyDetails?.wsConnectMs),
},
{
label: t('usage.status.upstreamResponseHeader'),
value: durationValue(latencyDetails?.upstreamHeadersMs),
},
{
label: t('usage.status.firstUpstreamEvent'),
value: durationValue(latencyDetails?.firstEventMs),
},
]
.filter(item => item.value !== null)
.map(item => ({ ...item, value: formatDuration(item.value) }))
.map(item => ({
...item,
value: formatDuration(item.value),
}))
if (
latencyDetails?.capacityUsedSlots != null
@@ -169,19 +203,37 @@ export function usageLatencyDetails(record: UsageLatencyRecord) {
const upstreamDisplay = formatDuration(upstreamMs)
const upstreamItems: UsageTimingItem[] = []
if (upstreamMs !== null)
upstreamItems.push({ label: t('accounts.connectionTest.duration'), value: upstreamDisplay })
if (upstreamMs !== null) {
upstreamItems.push({
label: t('accounts.connectionTest.duration'),
value: upstreamDisplay,
})
}
const processingMs = durationValue(latencyDetails?.openaiProcessingMs)
if (processingMs !== null)
upstreamItems.push({ label: t('usage.status.processingTime'), value: formatDuration(processingMs) })
if (processingMs !== null) {
upstreamItems.push({
label: t('usage.status.processingTime'),
value: formatDuration(processingMs),
})
}
const performanceItems = [
{ label: t('usage.status.apiOverhead'), value: durationValue(latencyDetails?.upstreamApiOverheadMs) },
{ label: t('usage.status.engineTime'), value: durationValue(latencyDetails?.upstreamEngineMs) },
{
label: t('usage.status.apiOverhead'),
value: durationValue(latencyDetails?.upstreamApiOverheadMs),
},
{
label: t('usage.status.engineTime'),
value: durationValue(latencyDetails?.upstreamEngineMs),
},
]
for (const item of performanceItems) {
if (item.value !== null)
upstreamItems.push({ label: item.label, value: formatUpstreamDuration(item.value) })
if (item.value !== null) {
upstreamItems.push({
label: item.label,
value: formatUpstreamDuration(item.value),
})
}
}
const sections: UsageTimingSection[] = []
@@ -192,7 +244,11 @@ export function usageLatencyDetails(record: UsageLatencyRecord) {
items: upstreamItems,
})
}
sections.push({ title: t('usage.status.requestObservation'), source: 'local', items: requestItems })
sections.push({
title: t('usage.status.requestObservation'),
source: 'local',
items: requestItems,
})
if (transportItems.length) {
sections.push({
title: t('usage.status.transport'),
@@ -229,7 +285,9 @@ export function usageBillingText(record: Pick<UsageCommonRecord, 'billing'>) {
}
function durationValue(value: unknown) {
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null
return typeof value === 'number' && Number.isFinite(value) && value >= 0
? value
: null
}
function formatUpstreamDuration(value: number) {
@@ -242,5 +300,7 @@ function formatUpstreamDuration(value: number) {
function upstreamResponseMs(details: UsageCommonRecord['latencyDetails']) {
const value = durationValue(details?.upstreamResponseMs)
return value !== null && value > 0 ? value : null
return value !== null && value > 0
? value
: null
}
+4 -1
View File
@@ -31,7 +31,10 @@ export function useAsyncAction(defaults: AsyncActionRunOptions = {}) {
}
loading.value = true
const options = { ...defaults, ...overrides }
const options = {
...defaults,
...overrides,
}
try {
const execute = async () => task()
return options.minimumMs === undefined
+5 -1
View File
@@ -28,7 +28,11 @@ export function useCopyText() {
ZNotification.success({ message: options.successText })
}
catch (error: unknown) {
ZNotification.error({ message: options.errorFromException ? errorMessage(error, t('common.errors.copy')) : t('common.errors.copy') })
ZNotification.error({
message: options.errorFromException
? errorMessage(error, t('common.errors.copy'))
: t('common.errors.copy'),
})
}
}
}
+4 -1
View File
@@ -64,7 +64,10 @@ export function usePagedQuery<Result extends PageResult>(options: {
return false
if (result.items.length === 0 && result.page.total > 0 && result.page.page > 1) {
return execute({ ...execution, page: clamp(result.page.totalPages, 1, Number.POSITIVE_INFINITY) })
return execute({
...execution,
page: clamp(result.page.totalPages, 1, Number.POSITIVE_INFINITY),
})
}
items.value = result.items
+3 -1
View File
@@ -15,7 +15,9 @@ export function useTableSort<Row extends object>(
const { key, direction } = parsed
if (typeof key === 'string' && (direction === 'asc' || direction === 'desc')
&& toValue(source).some(column => column.sortable && (column.sortable === true ? column.key : column.sortable) === key)) {
&& toValue(source).some(column => column.sortable && (column.sortable === true
? column.key
: column.sortable) === key)) {
return { key, direction }
}
}
+3 -1
View File
@@ -20,7 +20,9 @@ export function normalizeLocale(language: string): Locale {
return 'zh-Hant'
if (script === 'Hans')
return 'zh-Hans'
return region && ['TW', 'HK', 'MO'].includes(region) ? 'zh-Hant' : 'zh-Hans'
return region && ['TW', 'HK', 'MO'].includes(region)
? 'zh-Hant'
: 'zh-Hans'
}
}
catch {
+81 -20
View File
@@ -74,15 +74,51 @@ const pluginMenuId = useId()
const pluginGroupExpanded = shallowRef(route.path.startsWith('/plugins'))
const navItems = computed(() => ([
{ label: t('navigation.overview'), icon: LayoutDashboard, path: '/' },
{ label: t('navigation.accounts'), icon: Users, path: '/accounts' },
{ label: t('navigation.proxies'), icon: Network, path: '/proxies' },
{ label: t('navigation.groups'), icon: FolderTree, path: '/groups' },
{ label: t('navigation.apiKeys'), icon: KeyRound, path: '/keys' },
{ label: t('navigation.usage'), icon: ChartNoAxesColumn, path: '/usage' },
{ label: t('navigation.plugins'), icon: Puzzle, path: '/plugins' },
{ label: t('navigation.appearance'), icon: Palette, path: '/theme' },
{ label: t('navigation.settings'), icon: Settings, path: '/settings' },
{
label: t('navigation.overview'),
icon: LayoutDashboard,
path: '/',
},
{
label: t('navigation.accounts'),
icon: Users,
path: '/accounts',
},
{
label: t('navigation.proxies'),
icon: Network,
path: '/proxies',
},
{
label: t('navigation.groups'),
icon: FolderTree,
path: '/groups',
},
{
label: t('navigation.apiKeys'),
icon: KeyRound,
path: '/keys',
},
{
label: t('navigation.usage'),
icon: ChartNoAxesColumn,
path: '/usage',
},
{
label: t('navigation.plugins'),
icon: Puzzle,
path: '/plugins',
},
{
label: t('navigation.appearance'),
icon: Palette,
path: '/theme',
},
{
label: t('navigation.settings'),
icon: Settings,
path: '/settings',
},
]))
const pluginNavIndex = navItems.value.findIndex(item => item.path === '/plugins')
const pluginMenuVisible = computed(() => pluginGroupExpanded.value && !isCollapsed.value)
@@ -106,15 +142,21 @@ const activeNavIndex = computed(() => {
return Math.max(0, index)
})
const activeNavIndicatorStyle = computed(() => ({
transform: `translate3d(0, ${activeNavIndex.value * 58 + (activeNavIndex.value > pluginNavIndex ? pluginChildrenHeight.value : 0)}px, 0)`,
opacity: isActive('/plugins') && pluginMenuVisible.value ? 0 : 1,
transform: `translate3d(0, ${activeNavIndex.value * 58 + (activeNavIndex.value > pluginNavIndex
? pluginChildrenHeight.value
: 0)}px, 0)`,
opacity: isActive('/plugins') && pluginMenuVisible.value
? 0
: 1,
}))
const activePluginIndex = computed(() => route.name === 'plugins'
? 0
: pluginPageViews.value.findIndex(isPluginViewActive) + 1)
const activePluginIndicatorStyle = computed(() => ({
transform: `translate3d(0, ${activePluginIndex.value * 44}px, 0)`,
opacity: isActive('/plugins') && pluginMenuVisible.value ? 1 : 0,
opacity: isActive('/plugins') && pluginMenuVisible.value
? 1
: 0,
}))
const navFeedbackMuted = shallowRef(false)
const { start: restoreNavFeedback, stop: stopNavFeedbackRestore } = useTimeoutFn(
@@ -153,7 +195,9 @@ function navigatePluginView(view: PluginManagementView) {
function isPluginViewActive(view: PluginManagementView) {
const value = route.params.instanceId
const routeInstanceId = Array.isArray(value) ? value[0] : value
const routeInstanceId = Array.isArray(value)
? value[0]
: value
return route.name === 'plugin-page' && routeInstanceId === view.target.instanceId
}
@@ -162,7 +206,9 @@ function pluginViewLabel(view: PluginManagementView) {
if (pluginViewNameCounts.value.get(title) === 1)
return title
const duplicateName = pluginPageViews.value.some(item => item !== view && item.name === view.name && item.pages[0]?.title === title)
return `${title} · ${duplicateName ? shortPluginInstanceId(view.target.instanceId) : view.name}`
return `${title} · ${duplicateName
? shortPluginInstanceId(view.target.instanceId)
: view.name}`
}
function openSystemUpdate() {
@@ -184,11 +230,15 @@ const collapsedSidebarWidth = 88
const expandedSidebarWidth = 251
const sidebarWidth = computed(() => (isCollapsed.value ? collapsedSidebarWidth : expandedSidebarWidth))
const brandLabelVisible = shallowRef(!isCollapsed.value)
const themeToggleLabel = computed(() => (effectiveTheme.value === 'dark' ? t('appearance.switchToLight') : t('appearance.switchToDark')))
const themeToggleLabel = computed(() => (effectiveTheme.value === 'dark'
? t('appearance.switchToLight')
: t('appearance.switchToDark')))
const versionText = computed(() => version.value?.version.trim() ?? '')
const hasVersionLabel = computed(() => versionText.value.length > 0)
const versionLabel = computed(() => `v${versionText.value}`)
const updateButtonLabel = computed(() => (hasUpdate.value ? t('navigation.update.available') : t('navigation.update.open')))
const updateButtonLabel = computed(() => (hasUpdate.value
? t('navigation.update.available')
: t('navigation.update.open')))
function prefersReducedMotion() {
return preferredMotion.value === 'reduce'
@@ -204,14 +254,18 @@ function animateSidebarLabels(collapsed: boolean) {
if (prefersReducedMotion()) {
gsap.set(labels, {
opacity: collapsed ? 0 : 1,
x: collapsed ? -6 : 0,
x: collapsed
? -6
: 0,
})
return
}
gsap.to(labels, {
opacity: collapsed ? 0 : 1,
x: collapsed ? -6 : 0,
x: collapsed
? -6
: 0,
duration: collapsed ? 0.16 : 0.2,
ease: collapsed ? 'power2.in' : 'power3.out',
stagger: collapsed ? 0 : 0.018,
@@ -295,7 +349,9 @@ function animateSidebarWidth(collapsed: boolean) {
}
function animateNavSignal() {
const signal = isActive('/plugins') && pluginMenuVisible.value ? pluginNavSignalEls.value?.[0] : navSignalEl.value
const signal = isActive('/plugins') && pluginMenuVisible.value
? pluginNavSignalEls.value?.[0]
: navSignalEl.value
if (!signal)
return
@@ -371,7 +427,12 @@ watch(
onBeforeUnmount(() => {
stopNavFeedbackRestore()
const targets = [sidebarEl.value, brandLabelEl.value, navSignalEl.value, ...(pluginNavSignalEls.value ?? [])].filter((target): target is HTMLElement =>
const targets = [
sidebarEl.value,
brandLabelEl.value,
navSignalEl.value,
...(pluginNavSignalEls.value ?? []),
].filter((target): target is HTMLElement =>
Boolean(target),
)
gsap.killTweensOf(targets)
@@ -148,7 +148,9 @@ function restorePosition() {
return
}
const sideX = storedPosition.value.side === 'right' ? bounds.value.maxX : bounds.value.minX
const sideX = storedPosition.value.side === 'right'
? bounds.value.maxX
: bounds.value.minX
applyPosition({
initialized: true,
side: storedPosition.value.side,
@@ -159,12 +161,16 @@ function restorePosition() {
function snapPosition(rawX: number, rawY: number) {
const { minX, maxX, minY, maxY } = bounds.value
const side = rawX <= (minX + maxX) / 2 ? 'left' : 'right'
const side = rawX <= (minX + maxX) / 2
? 'left'
: 'right'
return {
initialized: true,
side,
x: side === 'left' ? minX : maxX,
x: side === 'left'
? minX
: maxX,
y: clamp(rawY, minY, maxY),
} satisfies StoredPosition
}
@@ -184,12 +190,16 @@ function applyPosition(position: StoredPosition) {
}
function positiveNumber(value: number, fallback: number) {
return Number.isFinite(value) && value > 0 ? value : fallback
return Number.isFinite(value) && value > 0
? value
: fallback
}
function parseCssPixel(value: string) {
const parsed = Number.parseFloat(value)
return Number.isFinite(parsed) ? parsed : 0
return Number.isFinite(parsed)
? parsed
: 0
}
onMounted(async () => {
@@ -88,15 +88,36 @@ const updateLogRows = computed(() =>
)
const channelLabels = computed(() => ({
stable: { label: t('systemUpdate.channels.stable.label'), description: t('systemUpdate.channels.stable.description') },
rc: { label: t('systemUpdate.channels.rc.label'), description: t('systemUpdate.channels.rc.description') },
beta: { label: t('systemUpdate.channels.beta.label'), description: t('systemUpdate.channels.beta.description') },
alpha: { label: t('systemUpdate.channels.alpha.label'), description: t('systemUpdate.channels.alpha.description') },
exp: { label: t('systemUpdate.channels.experimental.label'), description: t('systemUpdate.channels.experimental.description') },
stable: {
label: t('systemUpdate.channels.stable.label'),
description: t('systemUpdate.channels.stable.description'),
},
rc: {
label: t('systemUpdate.channels.rc.label'),
description: t('systemUpdate.channels.rc.description'),
},
beta: {
label: t('systemUpdate.channels.beta.label'),
description: t('systemUpdate.channels.beta.description'),
},
alpha: {
label: t('systemUpdate.channels.alpha.label'),
description: t('systemUpdate.channels.alpha.description'),
},
exp: {
label: t('systemUpdate.channels.experimental.label'),
description: t('systemUpdate.channels.experimental.description'),
},
}))
const channelOptions = computed(() => availableChannels.value.map(value => ({
value,
label: { stable: 'Stable', rc: 'RC', beta: 'Beta', alpha: 'Alpha', exp: 'Exp' }[value],
label: {
stable: 'Stable',
rc: 'RC',
beta: 'Beta',
alpha: 'Alpha',
exp: 'Exp',
}[value],
})))
async function handleChannelChange(value: string) {
@@ -133,7 +154,11 @@ async function handleCheckUpdates(force = true) {
ZNotification.error({ message: data.warning })
return
}
ZNotification.success({ message: data?.hasUpdate ? t('systemUpdate.feedback.available') : t('systemUpdate.feedback.noUpdates') })
ZNotification.success({
message: data?.hasUpdate
? t('systemUpdate.feedback.available')
: t('systemUpdate.feedback.noUpdates'),
})
}
catch {}
}
@@ -151,12 +176,16 @@ async function handleUpdateRequest() {
return
}
if (!data?.hasUpdate || !canUpdate.value) {
ZNotification.success({ message: t('systemUpdate.feedback.noUpdates') })
ZNotification.success({
message: t('systemUpdate.feedback.noUpdates'),
})
return
}
const remoteTargetVersion = normalizeSystemVersion(data.latestVersion)
if (!remoteTargetVersion) {
ZNotification.error({ message: t('systemUpdate.errors.remoteTargetMissing') })
ZNotification.error({
message: t('systemUpdate.errors.remoteTargetMissing'),
})
return
}
if (previousTargetVersion && previousTargetVersion !== remoteTargetVersion) {
@@ -177,7 +206,9 @@ async function runConfirmedUpdate(targetVersion: string, channel: SystemUpdateCh
try {
const result = await updateNow(targetVersion, channel)
if (result) {
ZNotification.success({ message: t('systemUpdate.feedback.started') })
ZNotification.success({
message: t('systemUpdate.feedback.started'),
})
}
}
catch {}
@@ -209,7 +240,9 @@ async function handleRestart() {
await restartNow(restartPlan.value)
}
catch (error: unknown) {
ZNotification.error({ message: errorMessage(error, t('systemUpdate.errors.preRestartCheck')) })
ZNotification.error({
message: errorMessage(error, t('systemUpdate.errors.preRestartCheck')),
})
}
finally {
checkingRestart.value = false
@@ -225,7 +258,9 @@ async function handleConfirmRestart() {
}
catch (error: unknown) {
restartConfirmOpen.value = false
ZNotification.error({ message: errorMessage(error, t('systemUpdate.errors.restart')) })
ZNotification.error({
message: errorMessage(error, t('systemUpdate.errors.restart')),
})
}
}
@@ -26,8 +26,12 @@ export function resolveSystemUpdatePresentation(input: SystemUpdatePresentationI
releaseVersion: displayValue(input.updateInfo?.latestVersion),
streamStatusLabel: input.updateStreaming
? t('systemUpdate.logs.live')
: input.updateStreamError ? t('systemUpdate.logs.disconnectedState') : t('systemUpdate.logs.waiting'),
restartButtonLabel: input.restarting ? t('systemUpdate.status.restarting') : t('systemUpdate.actions.restart'),
: input.updateStreamError
? t('systemUpdate.logs.disconnectedState')
: t('systemUpdate.logs.waiting'),
restartButtonLabel: input.restarting
? t('systemUpdate.status.restarting')
: t('systemUpdate.actions.restart'),
}
}
@@ -47,7 +51,11 @@ export function resolveSystemUpdateLogClasses(level: string) {
function resolveStatus(input: SystemUpdatePresentationInput) {
if (input.restarting || input.updating || input.checking || input.loading) {
return {
label: input.restarting ? t('systemUpdate.status.restarting') : input.updating ? t('systemUpdate.status.updating') : t('systemUpdate.status.checking'),
label: input.restarting
? t('systemUpdate.status.restarting')
: input.updating
? t('systemUpdate.status.updating')
: t('systemUpdate.status.checking'),
icon: RefreshCw,
badge: 'bg-cp-info-container text-cp-info-on-container',
iconClass: 'text-cp-info',
@@ -63,7 +71,11 @@ function resolveStatus(input: SystemUpdatePresentationInput) {
}
if (input.needRestart || input.hasUpdate || input.updateInfo) {
return {
label: input.needRestart ? t('systemUpdate.status.restartRequired') : input.hasUpdate ? t('systemUpdate.status.available') : t('systemUpdate.status.upToDate'),
label: input.needRestart
? t('systemUpdate.status.restartRequired')
: input.hasUpdate
? t('systemUpdate.status.available')
: t('systemUpdate.status.upToDate'),
icon: input.hasUpdate ? ArrowUpCircle : CheckCircle2,
badge: 'bg-cp-success-container text-cp-success-on-container',
iconClass: 'text-cp-success',
@@ -82,7 +94,9 @@ function resolveSummaryItems(input: SystemUpdatePresentationInput) {
{
key: 'current',
label: t('systemUpdate.currentVersion'),
value: input.loading ? '...' : versionLabel(input.updateInfo?.currentVersion ?? input.version?.version),
value: input.loading
? '...'
: versionLabel(input.updateInfo?.currentVersion ?? input.version?.version),
title: input.updateInfo?.currentVersion ?? input.version?.version,
},
{
@@ -109,9 +123,21 @@ function resolveSummaryItems(input: SystemUpdatePresentationInput) {
function resolveConfirmRows(input: SystemUpdatePresentationInput) {
return [
{ key: 'current', label: t('systemUpdate.currentVersion'), value: versionLabel(input.version?.version) },
{ key: 'previous', label: t('systemUpdate.displayedTarget'), value: versionLabel(input.previousTargetVersion) },
{ key: 'target', label: t('systemUpdate.remoteTarget'), value: versionLabel(input.confirmedTargetVersion) },
{
key: 'current',
label: t('systemUpdate.currentVersion'),
value: versionLabel(input.version?.version),
},
{
key: 'previous',
label: t('systemUpdate.displayedTarget'),
value: versionLabel(input.previousTargetVersion),
},
{
key: 'target',
label: t('systemUpdate.remoteTarget'),
value: versionLabel(input.confirmedTargetVersion),
},
]
}
@@ -135,9 +161,13 @@ function deploymentModeLabel(value?: string) {
}
function versionLabel(value?: string | null) {
return value ? `v${value}` : '-'
return value
? `v${value}`
: '-'
}
function displayValue(value: unknown) {
return value ? String(value) : '-'
return value
? String(value)
: '-'
}
+6 -2
View File
@@ -11,8 +11,12 @@ export const authPlugin: Plugin = {
async function redirectToLogin(loginMode = authStore.session?.role ?? 'admin') {
authStore.invalidateSession()
if (router.currentRoute.value.name !== 'login')
await router.replace({ name: 'login', state: { loginMode } })
if (router.currentRoute.value.name !== 'login') {
await router.replace({
name: 'login',
state: { loginMode },
})
}
}
setUnauthorizedHandler(() => redirectToLogin())
+3 -1
View File
@@ -43,7 +43,9 @@ router.beforeEach(async (to, from) => {
}
catch {
// 首次进入时没有页面可保留,转到登录页;已有页面则保留,且不清除会话。
return from === START_LOCATION ? login : false
return from === START_LOCATION
? login
: false
}
}
+17 -2
View File
@@ -23,7 +23,9 @@ export const useAuthStore = defineStore('auth', () => {
return pendingCheck.value
const check = loadSession(async () => {
const status = await getAuthStatus()
return status.authenticated ? status : refreshAuthSession()
return status.authenticated
? status
: refreshAuthSession()
}).finally(() => {
if (pendingCheck.value === check)
pendingCheck.value = undefined
@@ -108,5 +110,18 @@ export const useAuthStore = defineStore('auth', () => {
resetUnauthorizedHandling()
}
return { session, sessionGeneration, isAuthenticated, isAdmin, sessionChecked, loading, checking, checkAuth, refreshSession, login, logout, invalidateSession }
return {
session,
sessionGeneration,
isAuthenticated,
isAdmin,
sessionChecked,
loading,
checking,
checkAuth,
refreshSession,
login,
logout,
invalidateSession,
}
})
+15 -3
View File
@@ -106,7 +106,9 @@ export const useSystemUpdateStore = defineStore('system-update', () => {
})
const needRestart = computed(() => Boolean(updateStatus.value?.needRestart))
const restartTargetVersion = computed(() => needRestart.value ? normalizeSystemVersion(updateStatus.value?.currentVersion) : '')
const restartTargetVersion = computed(() => needRestart.value
? normalizeSystemVersion(updateStatus.value?.currentVersion)
: '')
const selectedChannel = computed(() => policy.value?.channel ?? 'stable')
const availableChannels = computed(() => policy.value?.availableChannels ?? [])
const canChangeChannel = computed(() => availableChannels.value.length > 1
@@ -274,7 +276,11 @@ export const useSystemUpdateStore = defineStore('system-update', () => {
}
function settlePhase() {
setPhase(needRestart.value ? { kind: 'restart_required' } : updateError.value ? { kind: 'failed' } : { kind: 'ready' })
setPhase(needRestart.value
? { kind: 'restart_required' }
: updateError.value
? { kind: 'failed' }
: { kind: 'ready' })
}
async function loadDetail(refresh: boolean, generation: number, channel?: SystemUpdateChannel) {
@@ -289,7 +295,13 @@ export const useSystemUpdateStore = defineStore('system-update', () => {
return
if (version.value && detail.policy.channel === version.value.updateChannel) {
// 侧栏只提示当前运行通道的更新,临时查看其他通道不会改变全局提示。
version.value = { ...version.value, latestVersion: detail.latestVersion, hasUpdate: detail.hasUpdate, updateCached: detail.cached, updateWarning: detail.warning }
version.value = {
...version.value,
latestVersion: detail.latestVersion,
hasUpdate: detail.hasUpdate,
updateCached: detail.cached,
updateWarning: detail.warning,
}
}
}
+3 -1
View File
@@ -269,7 +269,9 @@ export const useThemeStore = defineStore(
if (themeTransitioning.value)
return
requestThemeTransition(event)
themeMode.value = effectiveTheme.value === 'dark' ? 'light' : 'dark'
themeMode.value = effectiveTheme.value === 'dark'
? 'light'
: 'dark'
}
function requestThemeTransition(event?: MouseEvent): void {
+7 -2
View File
@@ -91,7 +91,10 @@ export function buildCodexCcSwitchImportDeeplink(input: CodexCcSwitchImportInput
['endpoint', baseUrl],
['apiKey', input.apiKey],
['usageEnabled', 'true'],
['usageScript', encodeBase64(buildUsageScript(input.apiKey, baseUrl))],
[
'usageScript',
encodeBase64(buildUsageScript(input.apiKey, baseUrl)),
],
['usageAutoInterval', '30'],
]
@@ -116,5 +119,7 @@ export function resolveServiceRootUrl() {
const origin = window.location.origin.replace(/\/+$/, '')
if (!normalizedApiBase)
return origin
return `${origin}${normalizedApiBase.startsWith('/') ? normalizedApiBase : `/${normalizedApiBase}`}`
return `${origin}${normalizedApiBase.startsWith('/')
? normalizedApiBase
: `/${normalizedApiBase}`}`
}
+16 -5
View File
@@ -14,11 +14,22 @@ export function parseJsonObject(text: string, maximumBytes?: number): JsonObject
value = JSON.parse(text)
}
catch {
return { value: undefined, error: t('common.validation.invalidJson') }
return {
value: undefined,
error: t('common.validation.invalidJson'),
}
}
if (!isRecord(value)) {
return {
value: undefined,
error: t('common.validation.jsonObject'),
}
}
if (maximumBytes !== undefined && new TextEncoder().encode(JSON.stringify(value)).byteLength > maximumBytes) {
return {
value: undefined,
error: t('common.validation.sizeLimit', { value: maximumBytes / 1024 }),
}
}
if (!isRecord(value))
return { value: undefined, error: t('common.validation.jsonObject') }
if (maximumBytes !== undefined && new TextEncoder().encode(JSON.stringify(value)).byteLength > maximumBytes)
return { value: undefined, error: t('common.validation.sizeLimit', { value: maximumBytes / 1024 }) }
return { value, error: undefined }
}
+21 -7
View File
@@ -1,7 +1,9 @@
import { locale } from '@/i18n'
export function numberLocale() {
return locale.value === 'en' ? 'en-US' : locale.value
return locale.value === 'en'
? 'en-US'
: locale.value
}
const metricUnits = [
@@ -20,7 +22,9 @@ export function formatLocalizedCompactNumber(value: number) {
return new Intl.NumberFormat(numberLocale(), {
notation: 'compact',
maximumFractionDigits: 1,
}).format(Number.isFinite(value) ? value : 0)
}).format(Number.isFinite(value)
? value
: 0)
}
export function formatCompactNumber(value: number) {
@@ -33,7 +37,9 @@ export function formatCompactNumber(value: number) {
continue
const scaled = normalized / threshold
const rounded = scaled >= 10 ? scaled.toFixed(1) : scaled.toFixed(2)
const rounded = scaled >= 10
? scaled.toFixed(1)
: scaled.toFixed(2)
return `${rounded.replace(/\.?0+$/, '')}${unit}`
}
@@ -53,7 +59,9 @@ export function formatDuration(value?: number | null) {
return `${Math.round(value)} ms`
if (value < 60_000) {
const seconds = value / 1_000
return `${seconds.toFixed(seconds >= 10 ? 1 : 2).replace(/\.0+$|(?<=\.\d)0$/, '')} s`
return `${seconds.toFixed(seconds >= 10
? 1
: 2).replace(/\.0+$|(?<=\.\d)0$/, '')} s`
}
return `${(value / 60_000).toFixed(1).replace(/\.0$/, '')} min`
}
@@ -61,15 +69,21 @@ export function formatDuration(value?: number | null) {
export function decimalDisplayNumber(value?: string | number | null) {
if (value == null)
return null
const parsed = typeof value === 'number' ? value : Number(value)
return Number.isFinite(parsed) ? parsed : null
const parsed = typeof value === 'number'
? value
: Number(value)
return Number.isFinite(parsed)
? parsed
: null
}
export function formatUsd(value?: string | number | null, precise = false) {
const parsed = decimalDisplayNumber(value)
if (parsed == null)
return '—'
const fractionDigits = precise || (Math.abs(parsed) > 0 && Math.abs(parsed) < 0.01) ? 4 : 2
const fractionDigits = precise || (Math.abs(parsed) > 0 && Math.abs(parsed) < 0.01)
? 4
: 2
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
+6 -2
View File
@@ -10,7 +10,9 @@ export function errorMessage(error: unknown, fallback = t('common.errors.request
: error && typeof error === 'object' && 'message' in error
? (error as { message?: unknown }).message
: undefined
return typeof message === 'string' && message ? message : fallback
return typeof message === 'string' && message
? message
: fallback
}
export async function withMinimumDuration<T>(
@@ -19,7 +21,9 @@ export async function withMinimumDuration<T>(
): Promise<T> {
const startedAt = Date.now()
try {
return await (typeof task === 'function' ? task() : task)
return await (typeof task === 'function'
? task()
: task)
}
finally {
const remaining = minimumMs - (Date.now() - startedAt)
+6 -2
View File
@@ -19,7 +19,9 @@ export function isSupportedProvider(value: unknown): value is SupportedProvider
export function formatProviderLabel(value?: string | null, fallback = '—') {
const normalized = value?.trim()
const provider = normalized?.toLowerCase()
return isSupportedProvider(provider) ? PROVIDER_DISPLAY_NAMES[provider] : (normalized || fallback)
return isSupportedProvider(provider)
? PROVIDER_DISPLAY_NAMES[provider]
: (normalized || fallback)
}
export function providerIcon(value: string): Component {
@@ -35,7 +37,9 @@ export function formatAuthenticationLabel(value?: string | null) {
switch (normalized?.toLowerCase()) {
case 'oauth': return 'OAuth'
case 'api_key': return t('common.fields.apiKey')
default: return normalized ? t('common.providers.customAuth', { normalized }) : t('common.providers.unknownAuth')
default: return normalized
? t('common.providers.customAuth', { normalized })
: t('common.providers.unknownAuth')
}
}
@@ -13,7 +13,10 @@ const { t } = useI18n({ useScope: 'global' })
const form = defineModel<ApiKeyAccountForm>({ required: true })
const transportOptions = computed(() => ([
{ label: 'HTTP / SSE', value: 'http' },
{ label: t('accounts.credentials.wsFirst'), value: 'prefer_websocket' },
{
label: t('accounts.credentials.wsFirst'),
value: 'prefer_websocket',
},
]))
</script>
@@ -17,11 +17,37 @@ defineProps<{
const { t } = useI18n({ useScope: 'global' })
const columns = computed(() => (defineTableColumns<AccountConcurrencyEntry>([
{ key: 'key', label: t('accounts.concurrency.key'), kind: 'identity', size: '2xl' },
{ key: 'model', label: t('accounts.concurrency.model'), kind: 'mono', size: 'lg' },
{ key: 'clientIp', label: t('accounts.concurrency.sourceIp'), kind: 'custom', size: '4xl' },
{ key: 'startedAtDisplay', label: t('accounts.concurrency.startTime'), kind: 'datetime', size: 'xl' },
{ key: 'occupiedMs', label: t('accounts.concurrency.elapsed'), kind: 'numeric', size: 'lg', formatter: (_, row) => formatDuration(row.occupiedMs) },
{
key: 'key',
label: t('accounts.concurrency.key'),
kind: 'identity',
size: '2xl',
},
{
key: 'model',
label: t('accounts.concurrency.model'),
kind: 'mono',
size: 'lg',
},
{
key: 'clientIp',
label: t('accounts.concurrency.sourceIp'),
kind: 'custom',
size: '4xl',
},
{
key: 'startedAtDisplay',
label: t('accounts.concurrency.startTime'),
kind: 'datetime',
size: 'xl',
},
{
key: 'occupiedMs',
label: t('accounts.concurrency.elapsed'),
kind: 'numeric',
size: 'lg',
formatter: (_, row) => formatDuration(row.occupiedMs),
},
])))
</script>
@@ -35,10 +35,18 @@ const status = defineModel<string>('status', { required: true })
const provider = defineModel<string>('provider', { required: true })
const group = defineModel<string>('group', { required: true })
const groupOptions = computed(() => [
{ label: t('accounts.filters.allGroups'), value: '' },
{ label: t('accounts.filters.ungroupedAccounts'), value: 'ungrouped' },
{
label: t('accounts.filters.allGroups'),
value: '',
},
{
label: t('accounts.filters.ungroupedAccounts'),
value: 'ungrouped',
},
...props.groups.map(group => ({
label: group.enabled ? group.name : t('accounts.filters.disabled', { name: group.name }),
label: group.enabled
? group.name
: t('accounts.filters.disabled', { name: group.name }),
value: group.id,
})),
])
@@ -36,17 +36,23 @@ const emailText = computed(() => accountIdentityLabel(props.account))
const visibleNotes = computed(() => props.showNotes ? props.account.notes : undefined)
const displayTitle = computed(() =>
visibleNotes.value || props.titleMode === 'email' || props.account.authenticationKind === 'api_key' ? emailText.value : emailText.value.split('@')[0],
visibleNotes.value || props.titleMode === 'email' || props.account.authenticationKind === 'api_key'
? emailText.value
: emailText.value.split('@')[0],
)
const secondaryText = computed(() =>
props.titleMode === 'email' || props.account.authenticationKind === 'api_key' ? null : emailText.value,
props.titleMode === 'email' || props.account.authenticationKind === 'api_key'
? null
: emailText.value,
)
const initial = computed(() => displayTitle.value.slice(0, 1).toUpperCase())
const avatarSizeClass = computed(() =>
props.size === 'lg' ? 'size-10 text-cp-xl' : 'size-9 text-cp',
props.size === 'lg'
? 'size-10 text-cp-xl'
: 'size-9 text-cp',
)
const secondaryClass = computed(() =>
@@ -55,7 +61,9 @@ const secondaryClass = computed(() =>
: 'mt-0.5 font-mono text-cp-xs text-cp-text-quaternary',
)
const metaGapClass = computed(() => props.metaSize === 'xs' ? 'gap-1' : 'gap-1.5')
const metaGapClass = computed(() => props.metaSize === 'xs'
? 'gap-1'
: 'gap-1.5')
const avatarToneClass = computed(() => {
const identity = props.account.id || props.account.email || displayTitle.value
@@ -25,10 +25,26 @@ const inputError = ref('')
const request = useRequestState()
const { loading, error } = request
const modes = computed(() => [
...(props.allowPreserve ? [{ value: 'preserve', label: t('accounts.modelAccess.reserve') }] : []),
{ value: 'all', label: t('common.values.unlimited') },
{ value: 'allowlist', label: t('accounts.modelAccess.allowlist') },
{ value: 'denylist', label: t('accounts.modelAccess.blocklist') },
...(props.allowPreserve
? [
{
value: 'preserve',
label: t('accounts.modelAccess.reserve'),
},
]
: []),
{
value: 'all',
label: t('common.values.unlimited'),
},
{
value: 'allowlist',
label: t('accounts.modelAccess.allowlist'),
},
{
value: 'denylist',
label: t('accounts.modelAccess.blocklist'),
},
])
const mode = computed({
get: () => model.value?.mode ?? 'preserve',
@@ -38,19 +54,31 @@ const mode = computed({
model.value = undefined
return
}
model.value = { mode: value as AccountModelAccess['mode'], models: value === 'all' ? [] : [...(model.value?.models ?? [])] }
model.value = {
mode: value as AccountModelAccess['mode'],
models: value === 'all'
? []
: [
...(model.value?.models ?? []),
],
}
},
})
const restricted = computed(() => mode.value === 'allowlist' || mode.value === 'denylist')
const canAdd = computed(() => Boolean(search.value.trim()) && !model.value?.models.includes(search.value.trim()))
const models = computed(() => {
const entries = new Map(catalog.value.map(item => [item.id, { ...item, unavailable: false }]))
const entries = new Map(catalog.value.map(item => [
item.id,
{ ...item, unavailable: false },
]))
for (const id of model.value?.models ?? []) {
if (!entries.has(id))
entries.set(id, { id, label: id, unavailable: true })
}
const query = search.value.trim().toLowerCase()
return [...entries.values()].filter(item => `${item.id} ${item.label}`.toLowerCase().includes(query))
return [
...entries.values(),
].filter(item => `${item.id} ${item.label}`.toLowerCase().includes(query))
})
function select(id: string, selected: boolean) {
@@ -60,8 +88,13 @@ function select(id: string, selected: boolean) {
if (selected)
ids.add(id)
else ids.delete(id)
const next = { ...model.value, models: [...ids] }
inputError.value = selected ? accountModelAccessError(next) ?? '' : ''
const next = {
...model.value,
models: [...ids],
}
inputError.value = selected
? accountModelAccessError(next) ?? ''
: ''
if (!inputError.value)
model.value = next
}
@@ -99,7 +132,10 @@ async function load(refresh = false) {
}
}
watch([() => props.accountId, restricted], () => {
watch([
() => props.accountId,
restricted,
], () => {
request.invalidate()
catalog.value = []
search.value = ''
@@ -16,34 +16,56 @@ const { proxies, loading } = useProxyCatalog()
const currentSelection = computed(() => props.currentProxyId || (props.endpoint ? 'preserve' : 'direct'))
const options = computed(() => [
...(props.preserve && !props.accountId
? [{
label: t('accounts.proxy.keepCurrent'),
value: 'preserve',
description: t('accounts.proxy.keepExisting'),
}]
? [
{
label: t('accounts.proxy.keepCurrent'),
value: 'preserve',
description: t('accounts.proxy.keepExisting'),
},
]
: []),
...(props.preserve && props.accountId && props.endpoint && !proxies.value.some(proxy => proxy.id === props.currentProxyId)
? [{ label: props.endpoint, value: currentSelection.value }]
? [
{ label: props.endpoint, value: currentSelection.value },
]
: []),
{ label: t('accounts.proxy.directConnection'), value: 'direct', description: t('accounts.proxy.notUsingProxy') },
{
label: t('accounts.proxy.directConnection'),
value: 'direct',
description: t('accounts.proxy.notUsingProxy'),
},
...proxies.value.map(proxy => ({
label: `${proxy.name.trim() || proxy.endpoint}${!proxy.enabled ? t('accounts.proxy.disabledSuffix') : proxy.lastTest?.success ? '' : proxy.lastTest ? t('accounts.proxy.testFailed') : t('accounts.proxy.notTested')}`,
label: `${proxy.name.trim() || proxy.endpoint}${!proxy.enabled
? t('accounts.proxy.disabledSuffix')
: proxy.lastTest?.success
? ''
: proxy.lastTest
? t('accounts.proxy.testFailed')
: t('accounts.proxy.notTested')}`,
value: proxy.id,
description: proxy.name.trim() ? proxy.endpoint : undefined,
description: proxy.name.trim()
? proxy.endpoint
: undefined,
disabled: !proxy.enabled,
})),
])
const selection = computed({
get: () => mode.value === 'proxy'
? proxyId.value
: mode.value === 'preserve' && props.accountId ? currentSelection.value : mode.value,
: mode.value === 'preserve' && props.accountId
? currentSelection.value
: mode.value,
set: (value: string) => {
// 显示已绑定代理,未改选择时仍省略写入,保留禁用代理及仅地址配置
const nextMode = props.preserve && props.accountId && value === currentSelection.value
? 'preserve'
: value === 'preserve' || value === 'direct' ? value : 'proxy'
: value === 'preserve' || value === 'direct'
? value
: 'proxy'
mode.value = nextMode
proxyId.value = nextMode === 'proxy' ? value : ''
proxyId.value = nextMode === 'proxy'
? value
: ''
},
})
</script>
@@ -22,15 +22,61 @@ const totalBillingDisplay = computed(() => totalBilling.value?.estimatedAmountDi
const hasUsageSummary = computed(() => (props.account.usage.totalTokens ?? 0) > 0 || totalBilling.value !== undefined)
const modelUsageColumns = computed(() => (defineTableColumns<AccountModelUsage>([
{ key: 'model', label: t('accounts.concurrency.model'), kind: 'text', size: 'md' },
{ key: 'requestCountDisplay', label: t('accounts.usage.call'), kind: 'numeric', size: 'xs' },
{ key: 'successRateDisplay', label: t('accounts.usage.successRate'), kind: 'numeric', size: 'sm' },
{ key: 'inputTokensDisplay', label: t('accounts.usage.input'), kind: 'numeric', size: 'xs' },
{ key: 'outputTokensDisplay', label: t('accounts.usage.output'), kind: 'numeric', size: 'xs' },
{ key: 'cachedInputTokensDisplay', label: t('accounts.usage.cache'), kind: 'numeric', size: 'xs' },
{ key: 'totalTokensDisplay', label: t('accounts.usage.total'), kind: 'numeric', size: 'xs' },
{ key: 'billingAmountUsdDisplay', label: t('accounts.usage.cost'), kind: 'numeric', size: 'sm' },
{ key: 'lastUsedAt', label: t('accounts.fields.lastUsed'), kind: 'datetime', size: 'md', align: 'right' },
{
key: 'model',
label: t('accounts.concurrency.model'),
kind: 'text',
size: 'md',
},
{
key: 'requestCountDisplay',
label: t('accounts.usage.call'),
kind: 'numeric',
size: 'xs',
},
{
key: 'successRateDisplay',
label: t('accounts.usage.successRate'),
kind: 'numeric',
size: 'sm',
},
{
key: 'inputTokensDisplay',
label: t('accounts.usage.input'),
kind: 'numeric',
size: 'xs',
},
{
key: 'outputTokensDisplay',
label: t('accounts.usage.output'),
kind: 'numeric',
size: 'xs',
},
{
key: 'cachedInputTokensDisplay',
label: t('accounts.usage.cache'),
kind: 'numeric',
size: 'xs',
},
{
key: 'totalTokensDisplay',
label: t('accounts.usage.total'),
kind: 'numeric',
size: 'xs',
},
{
key: 'billingAmountUsdDisplay',
label: t('accounts.usage.cost'),
kind: 'numeric',
size: 'sm',
},
{
key: 'lastUsedAt',
label: t('accounts.fields.lastUsed'),
kind: 'datetime',
size: 'md',
align: 'right',
},
])))
</script>
@@ -13,7 +13,11 @@ const { t } = useI18n({ useScope: 'global' })
const source = defineModel<AccountCreateSource | null>({ required: true })
const options = computed(() => ([
{ value: 'bundle', label: t('accounts.onboarding.batchImport'), icon: LayoutGrid },
{
value: 'bundle',
label: t('accounts.onboarding.batchImport'),
icon: LayoutGrid,
},
...PROVIDER_IDS.map(provider => ({
value: `provider:${provider}`,
label: formatProviderLabel(provider),
@@ -21,11 +25,18 @@ const options = computed(() => ([
})),
]))
const selected = computed({
get: () => source.value?.kind === 'provider' ? `provider:${source.value.id}` : source.value?.kind ?? '',
get: () => source.value?.kind === 'provider'
? `provider:${source.value.id}`
: source.value?.kind ?? '',
set: (value: string) => {
if (!options.value.some(option => option.value === value))
return
source.value = value === 'bundle' ? { kind: 'bundle' } : { kind: 'provider', id: value.slice('provider:'.length) }
source.value = value === 'bundle'
? { kind: 'bundle' }
: {
kind: 'provider',
id: value.slice('provider:'.length),
}
},
})
</script>
@@ -30,7 +30,12 @@ const props = withDefaults(defineProps<{
reauthorizing?: boolean
account?: Account | null
authorization: AccountAuthorizationView
}>(), { saving: false, oauthLoading: false, reauthorizing: false, account: null })
}>(), {
saving: false,
oauthLoading: false,
reauthorizing: false,
account: null,
})
const emit = defineEmits<{ create: [], generateOauth: [] }>()
@@ -67,7 +72,9 @@ const mode = computed({
},
})
const importText = computed({
get: () => form.value.mode === 'oauth' || form.value.mode === 'api_key' ? '' : form.value.importTexts[form.value.mode],
get: () => form.value.mode === 'oauth' || form.value.mode === 'api_key'
? ''
: form.value.importTexts[form.value.mode],
set: (value: string) => {
if (form.value.mode !== 'oauth' && form.value.mode !== 'api_key')
form.value.importTexts[form.value.mode] = value
@@ -48,24 +48,56 @@ export function resolveAccountCreatePresentation(input: {
modeOptions: modes,
available,
modal: {
title: configuring ? t('accounts.onboarding.accountSettings') : input.reauthorizing ? t('accounts.onboarding.reauthorizeAccount') : t('accounts.filters.importAccount'),
description: configuring ? t('accounts.onboarding.settingsDescription') : input.reauthorizing ? t('accounts.onboarding.reauthorizeDescription') : description,
title: configuring
? t('accounts.onboarding.accountSettings')
: input.reauthorizing
? t('accounts.onboarding.reauthorizeAccount')
: t('accounts.filters.importAccount'),
description: configuring
? t('accounts.onboarding.settingsDescription')
: input.reauthorizing
? t('accounts.onboarding.reauthorizeDescription')
: description,
type: configuring ? 'default' as const : 'info' as const,
width: configuring ? '42rem' : '36rem',
},
oauth: {
callbackLabel: id === 'xai' ? t('accounts.onboarding.callbackLabel') : t('accounts.onboarding.callbackAddress'),
callbackPlaceholder: id === 'openai' ? 'http://localhost:1455/auth/callback?code=...&state=...' : t('accounts.onboarding.callbackPlaceholder'),
callbackLabel: id === 'xai'
? t('accounts.onboarding.callbackLabel')
: t('accounts.onboarding.callbackAddress'),
callbackPlaceholder: id === 'openai'
? 'http://localhost:1455/auth/callback?code=...&state=...'
: t('accounts.onboarding.callbackPlaceholder'),
description: t('accounts.onboarding.callbackHelp'),
},
importInput: form.mode === 'access_token'
? { label: 'Access Token', placeholder: t('accounts.onboarding.pasteOneAccessTokenPerLine'), uploadable: false }
? {
label: 'Access Token',
placeholder: t('accounts.onboarding.pasteOneAccessTokenPerLine'),
uploadable: false,
}
: form.mode === 'refresh_token'
? { label: 'Refresh Token', placeholder: t('accounts.onboarding.pasteOneRefreshTokenPerLine'), uploadable: false }
: { label: isBatch ? t('accounts.onboarding.batchAccountFiles') : t('accounts.onboarding.accountFile'), placeholder: isBatch ? t('accounts.onboarding.packagePlaceholder') : t('accounts.onboarding.pasteAccountJsonContent'), uploadable: true },
? {
label: 'Refresh Token',
placeholder: t('accounts.onboarding.pasteOneRefreshTokenPerLine'),
uploadable: false,
}
: {
label: isBatch
? t('accounts.onboarding.batchAccountFiles')
: t('accounts.onboarding.accountFile'),
placeholder: isBatch
? t('accounts.onboarding.packagePlaceholder')
: t('accounts.onboarding.pasteAccountJsonContent'),
uploadable: true,
},
canSubmit,
submitLabel: form.mode === 'oauth'
? input.reauthorizing ? t('accounts.onboarding.completeReauthorization') : t('accounts.onboarding.completeImport')
: isBatch ? t('accounts.onboarding.batchImport') : t('accounts.filters.importAccount'),
? input.reauthorizing
? t('accounts.onboarding.completeReauthorization')
: t('accounts.onboarding.completeImport')
: isBatch
? t('accounts.onboarding.batchImport')
: t('accounts.filters.importAccount'),
}
}
@@ -16,8 +16,14 @@ const { t } = useI18n({ useScope: 'global' })
const filter = shallowRef('all')
const filterOptions = computed(() => ([
{ label: t('common.values.all'), value: 'all' },
{ label: t('accounts.importTasks.needProcessing'), value: 'attention' },
{
label: t('common.values.all'),
value: 'all',
},
{
label: t('accounts.importTasks.needProcessing'),
value: 'attention',
},
]))
const hasAttention = computed(() => props.task.counts.failed + props.task.counts.unknown > 0)
const visibleItems = computed(() => filter.value === 'attention'
@@ -27,7 +27,10 @@ const retentionId = useId()
const taskOptions = computed(() => props.tasks.map(task => ({
value: task.taskId,
label: task.createdAtDisplay,
description: t('accounts.importTasks.items', { total: task.total, value: taskLabel(task) }),
description: t('accounts.importTasks.items', {
total: task.total,
value: taskLabel(task),
}),
})))
watch(open, () => {
@@ -3,22 +3,60 @@ import { computed } from 'vue'
import { t } from '@/i18n'
export const itemStates = computed<Record<ImportItemStatus, { label: string, color: string, fill: string }>>(() => ({
pending: { label: t('common.status.pending'), color: 'bg-cp-fill-alter text-cp-text-secondary', fill: 'bg-cp-fill-secondary' },
running: { label: t('accounts.importTasks.processing'), color: 'bg-cp-primary-container text-cp-primary-on-container', fill: 'bg-cp-primary' },
succeeded: { label: t('accounts.importTasks.alreadyInStock'), color: 'bg-cp-success-container text-cp-success-on-container', fill: 'bg-cp-success' },
failed: { label: t('common.status.failed'), color: 'bg-cp-error-container text-cp-error-on-container', fill: 'bg-cp-error' },
unknown: { label: t('accounts.importTasks.toBeVerified'), color: 'bg-cp-warning-container text-cp-warning-on-container', fill: 'bg-cp-warning' },
skipped: { label: t('accounts.importTasks.notExecuted'), color: 'bg-cp-fill-tertiary text-cp-text-secondary', fill: 'bg-cp-text-quaternary' },
pending: {
label: t('common.status.pending'),
color: 'bg-cp-fill-alter text-cp-text-secondary',
fill: 'bg-cp-fill-secondary',
},
running: {
label: t('accounts.importTasks.processing'),
color: 'bg-cp-primary-container text-cp-primary-on-container',
fill: 'bg-cp-primary',
},
succeeded: {
label: t('accounts.importTasks.alreadyInStock'),
color: 'bg-cp-success-container text-cp-success-on-container',
fill: 'bg-cp-success',
},
failed: {
label: t('common.status.failed'),
color: 'bg-cp-error-container text-cp-error-on-container',
fill: 'bg-cp-error',
},
unknown: {
label: t('accounts.importTasks.toBeVerified'),
color: 'bg-cp-warning-container text-cp-warning-on-container',
fill: 'bg-cp-warning',
},
skipped: {
label: t('accounts.importTasks.notExecuted'),
color: 'bg-cp-fill-tertiary text-cp-text-secondary',
fill: 'bg-cp-text-quaternary',
},
}))
export const outcomeOrder: ImportItemStatus[] = ['succeeded', 'failed', 'unknown', 'running', 'pending', 'skipped']
export const outcomeOrder: ImportItemStatus[] = [
'succeeded',
'failed',
'unknown',
'running',
'pending',
'skipped',
]
export function taskLabel(task: AccountImportTask) {
if (!task.finishedAt)
return task.stopRequested ? t('accounts.importTasks.stopping') : task.counts.running > 0 ? t('accounts.importTasks.importing') : t('accounts.importTasks.queuing')
if (!task.finishedAt) {
return task.stopRequested
? t('accounts.importTasks.stopping')
: task.counts.running > 0
? t('accounts.importTasks.importing')
: t('accounts.importTasks.queuing')
}
if (task.stopRequested)
return t('accounts.importTasks.stopped')
return task.counts.failed + task.counts.unknown > 0 ? t('accounts.importTasks.endedPendingItems') : t('accounts.importTasks.allDone')
return task.counts.failed + task.counts.unknown > 0
? t('accounts.importTasks.endedPendingItems')
: t('accounts.importTasks.allDone')
}
export function processed(task: AccountImportTask) {
@@ -14,14 +14,26 @@ const props = defineProps<{
const { t } = useI18n({ useScope: 'global' })
const insightRows = computed(() => [
{ label: t('accounts.profile.activity.quickMode'), value: formatPercent(props.insights.fastModePercent) },
{
label: t('accounts.profile.activity.quickMode'),
value: formatPercent(props.insights.fastModePercent),
},
{
label: t('accounts.profile.activity.reasoningEffort'),
value: formatReasoning(props.insights.reasoningEffort, props.insights.reasoningEffortPercent),
},
{ label: t('accounts.profile.activity.exploredSkills'), value: formatNumber(props.insights.skillsExplored) },
{ label: t('accounts.profile.activity.totalNumberOfSkillsUsed'), value: formatNumber(props.insights.totalSkillsUsed) },
{ label: t('accounts.profile.activity.totalNumberOfChats'), value: formatNumber(props.insights.totalThreads) },
{
label: t('accounts.profile.activity.exploredSkills'),
value: formatNumber(props.insights.skillsExplored),
},
{
label: t('accounts.profile.activity.totalNumberOfSkillsUsed'),
value: formatNumber(props.insights.totalSkillsUsed),
},
{
label: t('accounts.profile.activity.totalNumberOfChats'),
value: formatNumber(props.insights.totalThreads),
},
])
const invocations = computed(() =>
(props.insights.invocations ?? [])
@@ -30,7 +42,9 @@ const invocations = computed(() =>
)
function formatNumber(value: number | null) {
return value === null ? '—' : formatInteger(value)
return value === null
? '—'
: formatInteger(value)
}
function formatPercent(value: number | null) {
@@ -42,7 +56,9 @@ function formatPercent(value: number | null) {
function formatReasoning(effort: string | null, percent: number | null) {
if (!effort)
return '—'
return percent === null ? effort : `${effort} · ${formatPercent(percent)}`
return percent === null
? effort
: `${effort} · ${formatPercent(percent)}`
}
function invocationView(invocation: AccountProfileInvocation) {
@@ -39,15 +39,23 @@ const metrics = computed(() => [
])
function formatMetric(value: number | null | undefined) {
return value == null ? '—' : formatCompactNumber(value)
return value == null
? '—'
: formatCompactNumber(value)
}
function formatExact(value: number | null | undefined) {
return value == null ? undefined : `${formatInteger(value)} Tokens`
return value == null
? undefined
: `${formatInteger(value)} Tokens`
}
function formatDays(value: number | null | undefined) {
return value == null ? '—' : t('accounts.profile.metrics.dayCount', { integer: formatInteger(value) })
return value == null
? '—'
: t('accounts.profile.metrics.dayCount', {
integer: formatInteger(value),
})
}
function formatDuration(value: number | null | undefined) {
@@ -57,13 +65,21 @@ function formatDuration(value: number | null | undefined) {
const days = Math.floor(totalMinutes / (24 * 60))
const hours = Math.floor((totalMinutes % (24 * 60)) / 60)
const minutes = totalMinutes % 60
if (days > 0)
return hours > 0 ? t('accounts.profile.metrics.dH', { days, hours }) : t('accounts.profile.metrics.elapsedDays', { days })
if (hours > 0)
return minutes > 0 ? t('accounts.profile.metrics.hMin', { hours, minutes }) : t('accounts.profile.metrics.elapsedHours', { hours })
if (days > 0) {
return hours > 0
? t('accounts.profile.metrics.dH', { days, hours })
: t('accounts.profile.metrics.elapsedDays', { days })
}
if (hours > 0) {
return minutes > 0
? t('accounts.profile.metrics.hMin', { hours, minutes })
: t('accounts.profile.metrics.elapsedHours', { hours })
}
if (totalMinutes > 0)
return t('accounts.profile.metrics.min', { totalMinutes })
return t('accounts.profile.metrics.durationSeconds', { value: Math.max(0, Math.floor(value / 1000)) })
return t('accounts.profile.metrics.durationSeconds', {
value: Math.max(0, Math.floor(value / 1000)),
})
}
</script>
@@ -21,7 +21,9 @@ const imageFailed = shallowRef(false)
const displayName = computed(
() => props.profile?.displayName || props.profile?.username || props.account.email || props.account.name || t('accounts.profile.accountUser'),
)
const username = computed(() => (props.profile?.username ? `@${props.profile?.username.replace(/^@/, '')}` : null))
const username = computed(() => (props.profile?.username
? `@${props.profile?.username.replace(/^@/, '')}`
: null))
const accountIdentity = computed(() => props.account.email?.trim() || props.account.accountId?.trim())
const initial = computed(() => Array.from(displayName.value.trim())[0]?.toUpperCase() || null)
const avatarToneClass = computed(() =>
@@ -29,7 +31,9 @@ const avatarToneClass = computed(() =>
)
const avatarUrl = computed(() => {
const sourceUrl = props.profile?.imageUrl?.trim()
return props.account.capabilities.avatar && sourceUrl ? accountProfileAvatarUrl(props.account.id, sourceUrl) : null
return props.account.capabilities.avatar && sourceUrl
? accountProfileAvatarUrl(props.account.id, sourceUrl)
: null
})
watch(
avatarUrl,
@@ -21,13 +21,27 @@ const billingPeriod = computed(() => {
const period = props.subscription?.billingPeriod
if (!period)
return null
const labels: Record<string, string> = { month: t('accounts.profile.subscription.byMonth'), monthly: t('accounts.profile.subscription.byMonth'), year: t('accounts.profile.subscription.byYear'), yearly: t('accounts.profile.subscription.byYear'), annual: t('accounts.profile.subscription.byYear') }
const labels: Record<string, string> = {
month: t('accounts.profile.subscription.byMonth'),
monthly: t('accounts.profile.subscription.byMonth'),
year: t('accounts.profile.subscription.byYear'),
yearly: t('accounts.profile.subscription.byYear'),
annual: t('accounts.profile.subscription.byYear'),
}
return labels[period.toLowerCase()] ?? period
})
const billing = computed(() => [billingPeriod.value, props.subscription?.billingCurrency].filter(Boolean).join(' · ') || t('common.values.unknown'))
const dates = computed(() => [
{ label: t('accounts.profile.subscription.subscriptionStarts'), value: props.subscription?.startsAt, display: props.subscription?.startsAtDisplay },
{ label: t('accounts.profile.subscription.subscriptionEnds'), value: props.subscription?.expiresAt, display: props.subscription?.expiresAtDisplay },
{
label: t('accounts.profile.subscription.subscriptionStarts'),
value: props.subscription?.startsAt,
display: props.subscription?.startsAtDisplay,
},
{
label: t('accounts.profile.subscription.subscriptionEnds'),
value: props.subscription?.expiresAt,
display: props.subscription?.expiresAtDisplay,
},
])
</script>
@@ -16,9 +16,18 @@ const { t } = useI18n({ useScope: 'global' })
const mode = shallowRef<ProfileActivityMode>('daily')
const modeOptions = computed(() => ([
{ label: t('accounts.profile.activity.daily'), value: 'daily' },
{ label: t('accounts.profile.activity.weekly'), value: 'weekly' },
{ label: t('accounts.profile.activity.cumulative'), value: 'cumulative' },
{
label: t('accounts.profile.activity.daily'),
value: 'daily',
},
{
label: t('accounts.profile.activity.weekly'),
value: 'weekly',
},
{
label: t('accounts.profile.activity.cumulative'),
value: 'cumulative',
},
]))
const levelClasses: Record<ProfileActivityLevel, string> = {
0: 'bg-cp-activity-level-0',
@@ -31,7 +40,11 @@ const grid = computed(() => buildProfileActivityGrid(props.calendar, mode.value)
const rangeLabel = computed(() => grid.value.rangeLabel)
const monthLabels = computed(() =>
grid.value.weeks.flatMap((week, weekIndex) =>
week.monthLabel ? [{ label: week.monthLabel, weekIndex }] : [],
week.monthLabel
? [
{ label: week.monthLabel, weekIndex },
]
: [],
),
)
</script>
@@ -9,14 +9,28 @@ export function buildProfileActivityGrid(calendar: ProfileActivityCalendar | nul
let cumulative = 0
const weeks = (calendar?.weeks ?? []).map((week) => {
const weekly = week.cells.reduce((sum, cell) => sum + cell.tokens, 0)
return { ...week, cells: week.cells.map((cell) => {
cumulative += cell.isFuture ? 0 : cell.tokens
return { ...cell, value: mode === 'weekly' ? weekly : mode === 'cumulative' ? cumulative : cell.tokens, level: 0 as ProfileActivityLevel }
}) }
return {
...week,
cells: week.cells.map((cell) => {
cumulative += cell.isFuture ? 0 : cell.tokens
return {
...cell,
value: mode === 'weekly'
? weekly
: mode === 'cumulative'
? cumulative
: cell.tokens,
level: 0 as ProfileActivityLevel,
}
}),
}
})
const maximum = Math.max(0, ...weeks.flatMap(week => week.cells).filter(cell => !cell.isFuture).map(cell => cell.value))
for (const cell of weeks.flatMap(week => week.cells))
cell.level = cell.value <= 0 || maximum <= 0 ? 0 : Math.min(4, Math.max(1, Math.ceil(cell.value / maximum * 4))) as ProfileActivityLevel
for (const cell of weeks.flatMap(week => week.cells)) {
cell.level = cell.value <= 0 || maximum <= 0
? 0
: Math.min(4, Math.max(1, Math.ceil(cell.value / maximum * 4))) as ProfileActivityLevel
}
return { weeks, rangeLabel: calendar?.rangeLabel ?? '' }
}
@@ -33,11 +33,19 @@ const { now, pause, resume } = useNow({
scheduler: callback => useIntervalFn(callback, 30_000),
})
const options = computed(() => report.value?.forecasts.map(item => ({
label: item.period === 'weekly' ? t('accounts.quotaForecast.weeklyQuota') : t('accounts.quotaForecast.monthlyQuota'),
label: item.period === 'weekly'
? t('accounts.quotaForecast.weeklyQuota')
: t('accounts.quotaForecast.monthlyQuota'),
value: item.period,
})) ?? [
{ label: t('accounts.quotaForecast.weeklyQuota'), value: 'weekly' },
{ label: t('accounts.quotaForecast.monthlyQuota'), value: 'monthly' },
{
label: t('accounts.quotaForecast.weeklyQuota'),
value: 'weekly',
},
{
label: t('accounts.quotaForecast.monthlyQuota'),
value: 'monthly',
},
])
const forecast = computed(() => report.value?.forecasts.find(item => item.period === period.value))
const unavailableReason = computed(() => {
@@ -19,7 +19,9 @@ const balanceDisplay = computed(() => {
const [, sign, integer, fraction] = decimal
// 点数可能包含高精度小数,整数分组与小数文本分别处理,避免转成浮点数。
const grouped = `${sign}${new Intl.NumberFormat(numberLocale()).format(BigInt(integer!))}`
return fraction ? `${grouped}.${fraction}` : grouped
return fraction
? `${grouped}.${fraction}`
: grouped
})
const value = computed(() => {
if (props.credits?.unlimited)
@@ -23,7 +23,9 @@ const saveAction = useAsyncAction()
const busy = computed(() => loadAction.loading.value || saveAction.loading.value)
const resultText = computed(() => {
switch (status.value?.lastResult) {
case 'pending': return status.value?.enabled ? t('accounts.quotaReset.automation.confirming') : t('accounts.quotaReset.automation.unconfirmed')
case 'pending': return status.value?.enabled
? t('accounts.quotaReset.automation.confirming')
: t('accounts.quotaReset.automation.unconfirmed')
case 'expired_unknown': return t('accounts.quotaReset.automation.expiredUnconfirmed')
case 'discovery_failed': return t('accounts.quotaReset.automation.queryFailed')
case 'weekly_quota_above_threshold': return t('accounts.quotaReset.automation.aboveThreshold')
@@ -52,7 +52,9 @@ const {
const modalTitle = computed(() => {
if (!confirmation.value)
return t('accounts.quotaReset.limitReset')
return confirmation.value.ambiguous ? t('accounts.quotaReset.confirmLastReset') : t('accounts.quotaReset.confirmResetAmount')
return confirmation.value.ambiguous
? t('accounts.quotaReset.confirmLastReset')
: t('accounts.quotaReset.confirmResetAmount')
})
const triggerLabel = computed(() => {
if (ambiguous.value)
@@ -62,7 +64,9 @@ const triggerLabel = computed(() => {
? t('accounts.quotaReset.queryFailedWithCount', { count: availableCount.value })
: t('accounts.quotaReset.queryFailedLabel')
}
return hasSnapshot.value ? t('accounts.quotaReset.availableCount', { count: availableCount.value }) : t('accounts.quotaReset.viewCredits')
return hasSnapshot.value
? t('accounts.quotaReset.availableCount', { count: availableCount.value })
: t('accounts.quotaReset.viewCredits')
})
const showTriggerCount = computed(() => hasSnapshot.value && availableCount.value > 0)
const confirmCreditTitle = computed(() => confirmation.value?.credit
@@ -71,15 +75,20 @@ const confirmCreditTitle = computed(() => confirmation.value?.credit
const creditItems = computed(() => availableCredits.value.map(credit => ({
id: credit.id,
title: creditTitle(credit),
expiry: credit.expiresAtDisplay ? t('accounts.quotaReset.expires', { expiresAtDisplay: credit.expiresAtDisplay }) : t('accounts.quotaReset.expiryHelp'),
expiry: credit.expiresAtDisplay
? t('accounts.quotaReset.expires', { expiresAtDisplay: credit.expiresAtDisplay })
: t('accounts.quotaReset.expiryHelp'),
})))
const showCountOnlyAction = computed(() => !loadError.value
&& hasSnapshot.value
&& availableCount.value > 0
&& creditItems.value.length === 0)
const countLabel = computed(() => {
if (!hasSnapshot.value)
return loading.value ? t('accounts.quotaReset.querying') : t('accounts.quotaReset.toBeQueried')
if (!hasSnapshot.value) {
return loading.value
? t('accounts.quotaReset.querying')
: t('accounts.quotaReset.toBeQueried')
}
return t('accounts.quotaReset.usesAvailable', { count: availableCount.value })
})
@@ -94,8 +103,11 @@ watch(panelOpen, (isOpen) => {
})
watch(showConfirm, (confirming) => {
if (panelOpen.value)
confirmation.value = confirming ? { credit: consumptionCredit.value, ambiguous: ambiguous.value } : null
if (panelOpen.value) {
confirmation.value = confirming
? { credit: consumptionCredit.value, ambiguous: ambiguous.value }
: null
}
})
function creditTitle(credit: AccountResetCredit | undefined) {
@@ -25,7 +25,9 @@ const items = computed(() => orderedPanelQuotaWindows(props.windows)
label: quotaWindowLabel(window),
resetAtDisplay: window.resetAtDisplay,
remaining,
remainingDisplay: remaining === null ? t('accounts.quota.awaitingData') : t('accounts.quota.remaining', { remaining }),
remainingDisplay: remaining === null
? t('accounts.quota.awaitingData')
: t('accounts.quota.remaining', { remaining }),
barClass: presentation.barClass,
percentTextClass: presentation.percentTextClass,
barStyle: { width: `${remaining ?? 0}%` },
@@ -37,7 +37,9 @@ const forecastOpen = shallowRef(false)
const hasForecast = computed(() => props.account.authenticationKind !== 'api_key')
const hasPersonalInfo = computed(() => props.account.capabilities.profile || props.account.capabilities.subscription)
const hasActions = computed(() => hasPersonalInfo.value || hasForecast.value || props.account.capabilities.quotaRefresh || props.account.capabilities.resetCredits)
const lastRefreshed = computed(() => t('accounts.quota.lastRefreshed', { refreshedAtDisplay: formatRelativeTime(props.account.quota.refreshedAtRelative) }))
const lastRefreshed = computed(() => t('accounts.quota.lastRefreshed', {
refreshedAtDisplay: formatRelativeTime(props.account.quota.refreshedAtRelative),
}))
watch(hasPersonalInfo, (available) => {
if (!available)
profileOpen.value = false
@@ -22,8 +22,12 @@ const props = withDefaults(defineProps<{
const { t } = useI18n({ useScope: 'global' })
const detailHeading = computed(() => props.label)
const detailTitle = computed(() => detailHeading.value ?? (props.windows[0] ? quotaWindowLabel(props.windows[0]) : t('accounts.quota.quotaDetails')))
const summaryLabel = computed(() => props.label ?? (props.windows[0] ? quotaWindowLabel(props.windows[0]) : t('usage.quota')))
const detailTitle = computed(() => detailHeading.value ?? (props.windows[0]
? quotaWindowLabel(props.windows[0])
: t('accounts.quota.quotaDetails')))
const summaryLabel = computed(() => props.label ?? (props.windows[0]
? quotaWindowLabel(props.windows[0])
: t('usage.quota')))
const hasQuotaWindow = computed(() => props.windows.some(window => typeof window.usedPercent === 'number'))
const detailItems = computed(() =>
props.windows.map((window) => {
@@ -171,7 +171,9 @@ export function resolveAccountStatusPresentation(
? errorRecoveryHints.value[input.errorReason]
: definition.recoveryHint
const hasDetail = input.status === 'error' || mode === 'rate_limited' || isBackoff
const retry = nextRefreshDisplay ? t('accounts.status.nextAttempt', { nextRefreshDisplay }) : ''
const retry = nextRefreshDisplay
? t('accounts.status.nextAttempt', { nextRefreshDisplay })
: ''
const triggerLabel = t('accounts.status.clickOrFocusToViewDetails', { title, description: definition.description, retry })
return {
@@ -186,7 +188,9 @@ export function resolveAccountStatusPresentation(
errorText: input.errorMessage || null,
nextRefreshDisplay,
rateLimitRecovery,
recoveryTimeLabel: waitsForProbe ? t('accounts.status.resumeDetection') : t('accounts.status.expectedToRecover'),
recoveryTimeLabel: waitsForProbe
? t('accounts.status.resumeDetection')
: t('accounts.status.expectedToRecover'),
triggerLabel,
}
}
@@ -67,10 +67,12 @@ export function useAccountAuthorization(onComplete: () => Promise<void>) {
return
const expiresAt = Date.parse(result.expiresAt)
if (!Number.isFinite(expiresAt))
throw new Error(t('accounts.onboarding.invalidExpiry'))
throw new RangeError(t('accounts.onboarding.invalidExpiry'))
context = { provider: request.start.provider, settings: request.settings }
flow.value = result
status.value = expired() ? 'expired' : 'waiting'
status.value = expired()
? 'expired'
: 'waiting'
expiryTimer = setTimeout(() => {
// 不取消正在确认的提交,过期后仍接收服务端已经提交的结果。
if (status.value !== 'completing')
@@ -109,12 +111,22 @@ export function useAccountAuthorization(onComplete: () => Promise<void>) {
}
catch (cause) {
if (!current.signal.aborted) {
status.value = expired() ? 'expired' : 'paused'
status.value = expired()
? 'expired'
: 'paused'
error.value = errorMessage(cause, t('accounts.onboarding.authorizationFailed'))
}
}
}
onScopeDispose(reset)
return { view, callback, busy, start, complete, cancel, reset }
return {
view,
callback,
busy,
start,
complete,
cancel,
reset,
}
}
@@ -67,9 +67,15 @@ export function useAccountBatchEditor(options: {
ZNotification.warning({ message: modelError })
return
}
const scheduling = parseAccountSchedulingForm(editedFields.value.has('concurrencyLimit') ? concurrencyLimit.value : '', editedFields.value.has('weight') ? weight.value : '1')
const scheduling = parseAccountSchedulingForm(editedFields.value.has('concurrencyLimit')
? concurrencyLimit.value
: '', editedFields.value.has('weight')
? weight.value
: '1')
if (proxyMode.value === 'proxy' && !proxyId.value.trim()) {
ZNotification.warning({ message: t('accounts.batchEdit.proxyRequired') })
ZNotification.warning({
message: t('accounts.batchEdit.proxyRequired'),
})
return
}
if (!scheduling.valid) {
@@ -82,17 +88,36 @@ export function useAccountBatchEditor(options: {
await batchUpdateAccounts({
accountIds,
modelAccess: modelAccess.value,
outboundProxyId: proxyMode.value === 'preserve' ? undefined : proxyMode.value === 'direct' ? '' : proxyId.value.trim(),
outboundProxyId: proxyMode.value === 'preserve'
? undefined
: proxyMode.value === 'direct'
? ''
: proxyId.value.trim(),
enabled: schedulingEnabled.value,
concurrencyLimit: editedFields.value.has('concurrencyLimit') ? scheduling.values.concurrencyLimit : undefined,
weight: editedFields.value.has('weight') ? scheduling.values.weight : undefined,
groupIds: editedFields.value.has('groupIds') ? [...new Set(selectedGroupIds.value)] : undefined,
concurrencyLimit: editedFields.value.has('concurrencyLimit')
? scheduling.values.concurrencyLimit
: undefined,
weight: editedFields.value.has('weight')
? scheduling.values.weight
: undefined,
groupIds: editedFields.value.has('groupIds')
? [
...new Set(selectedGroupIds.value),
]
: undefined,
})
showBatchEditModal.value = false
options.selectedIds.value = new Set()
ZNotification.success({ message: t('accounts.batchEdit.saved', { count: accountIds.length }) })
void Promise.allSettled([options.reloadAccounts(), options.reloadGroups()])
}, { onError: () => void options.reloadAccounts() })
ZNotification.success({
message: t('accounts.batchEdit.saved', { count: accountIds.length }),
})
void Promise.allSettled([
options.reloadAccounts(),
options.reloadGroups(),
])
}, {
onError: () => void options.reloadAccounts(),
})
}
function selectedAccounts() {
@@ -145,7 +170,9 @@ function sharedConcurrencyLimit(accounts: Account[]) {
function sharedWeight(accounts: Account[]) {
const first = accounts[0]?.weight ?? 1
return accounts.every(account => account.weight === first) ? String(first) : '1'
return accounts.every(account => account.weight === first)
? String(first)
: '1'
}
function sharedGroupIds(accounts: Account[]) {
@@ -45,7 +45,10 @@ export function useAccountConcurrency(account: Ref<Account | null>, open: Ref<bo
if (!request.isCurrent(id))
return
const firstReceivedAt = performance.now()
const collected = new Map(result.entries.map(entry => [entry.leaseId, { entry, receivedAt: firstReceivedAt }]))
const collected = new Map(result.entries.map(entry => [
entry.leaseId,
{ entry, receivedAt: firstReceivedAt },
]))
// 按首次总数限定本轮读取,避免持续新增占用让刷新无界延长
const pages = Math.ceil(result.total / result.pageSize)
for (let page = 2; page <= pages; page++) {
@@ -60,9 +63,13 @@ export function useAccountConcurrency(account: Ref<Account | null>, open: Ref<bo
}
const snapshotAt = performance.now()
data.value = {
entries: [...collected.values()].map(({ entry, receivedAt }) => ({
entries: [
...collected.values(),
].map(({ entry, receivedAt }) => ({
...entry,
occupiedMs: entry.occupiedMs === null ? null : entry.occupiedMs + snapshotAt - receivedAt,
occupiedMs: entry.occupiedMs === null
? null
: entry.occupiedMs + snapshotAt - receivedAt,
})),
}
request.error.value = ''
@@ -80,7 +87,10 @@ export function useAccountConcurrency(account: Ref<Account | null>, open: Ref<bo
}
}
watch([active, () => account.value?.id], ([isActive, accountId], [, previousAccountId]) => {
watch([
active,
() => account.value?.id,
], ([isActive, accountId], [, previousAccountId]) => {
stop()
// 同一账号保留已确认内容,暂停轮询和弹窗退场不提前清空
if (accountId !== previousAccountId) {
@@ -94,5 +104,10 @@ export function useAccountConcurrency(account: Ref<Account | null>, open: Ref<bo
}, { immediate: true })
onScopeDispose(stop)
return { data, entries, loading: request.loading, error: request.error }
return {
data,
entries,
loading: request.loading,
error: request.error,
}
}
@@ -138,7 +138,9 @@ function connectionTestFailureText(event: ConnectionTestFailureEvent) {
}
function connectionTestFailureLabel(event: ConnectionTestFailureEvent) {
return event.source ? CONNECTION_TEST_SOURCE_LABEL.value[event.source] || t('accounts.connectionTest.failed') : t('accounts.connectionTest.failed')
return event.source
? CONNECTION_TEST_SOURCE_LABEL.value[event.source] || t('accounts.connectionTest.failed')
: t('accounts.connectionTest.failed')
}
function connectionTestFailureDiagnostics(event: ConnectionTestFailureEvent) {
@@ -308,7 +310,9 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
return
}
connectionTestLogs.value = connectionTestLogs.value.map((item, itemIndex) =>
itemIndex === index ? { ...next, time: item.time } : item,
itemIndex === index
? { ...next, time: item.time }
: item,
)
}
@@ -425,7 +429,9 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
return
applyConnectionTestModels(result, refresh)
if (refresh) {
ZNotification.success({ message: t('accounts.connectionTest.modelsRefreshed', { count: connectionTestModelOptions.value.length }) })
ZNotification.success({
message: t('accounts.connectionTest.modelsRefreshed', { count: connectionTestModelOptions.value.length }),
})
}
else if (!connectionTestSelectedModel.value) {
connectionTestError.value = t('accounts.connectionTest.noModelsAvailable')
@@ -537,7 +543,10 @@ export function useAccountConnectionTest(options: { reload: () => Promise<unknow
failConnectionTest(t('accounts.connectionTest.errors.disconnected'))
})
watch([showConnectionTestModal, () => testingAccount.value?.id], ([open]) => {
watch([
showConnectionTestModal,
() => testingAccount.value?.id,
], ([open]) => {
modelsRequest.invalidate({ resetLoading: open })
if (!open) {
abortConnectionTest()
@@ -28,7 +28,9 @@ export function useAccountEditor(options: {
const saveAction = useAsyncAction()
const saving = saveAction.loading
const apiKey = ref(emptyApiKeyAccountForm())
const configurationRequest = useRequestState(() => ZNotification.error({ message: t('accounts.editor.loadError') }))
const configurationRequest = useRequestState(() => ZNotification.error({
message: t('accounts.editor.loadError'),
}))
const savedConfiguration = shallowRef<ApiKeyConfiguration>()
const oauthTransport = shallowRef<ApiKeyConfiguration['transport'] | ''>('')
const savedOAuthTransport = shallowRef<ApiKeyConfiguration['transport']>()
@@ -51,7 +53,10 @@ export function useAccountEditor(options: {
const configuration = parseApiKeyConfiguration(detail.credentialConfiguration)
if (!configuration)
throw new Error(t('accounts.editor.apiKeyUnavailable'))
apiKey.value = { ...emptyApiKeyAccountForm(), ...configuration }
apiKey.value = {
...emptyApiKeyAccountForm(),
...configuration,
}
savedConfiguration.value = configuration
}
catch (error) {
@@ -71,7 +76,10 @@ export function useAccountEditor(options: {
schedulingEnabled.value = account.enabled
concurrencyLimit.value = concurrencyLimitInput(account.concurrencyLimit)
weight.value = String(account.weight)
modelAccess.value = { ...account.modelAccess, models: [...account.modelAccess.models] }
modelAccess.value = {
...account.modelAccess,
models: [...account.modelAccess.models],
}
selectedGroupIds.value = account.groups.map(group => group.id)
showEditModal.value = true
if (isOpenAiApiKeyAccount(account) || isOpenAiOAuthAccount(account))
@@ -84,7 +92,9 @@ export function useAccountEditor(options: {
return
const isApiKey = isOpenAiApiKeyAccount(editingAccount.value)
const isOAuth = isOpenAiOAuthAccount(editingAccount.value)
const configuration = isApiKey && configurationReady.value ? parseApiKeyConfiguration(apiKey.value) : undefined
const configuration = isApiKey && configurationReady.value
? parseApiKeyConfiguration(apiKey.value)
: undefined
if (isApiKey && !configuration)
return
if (isApiKey) {
@@ -101,7 +111,9 @@ export function useAccountEditor(options: {
}
const scheduling = parseAccountSchedulingForm(concurrencyLimit.value, weight.value)
if (proxyMode.value === 'proxy' && !proxyId.value.trim()) {
ZNotification.warning({ message: t('accounts.batchEdit.proxyRequired') })
ZNotification.warning({
message: t('accounts.batchEdit.proxyRequired'),
})
return
}
if (!scheduling.valid) {
@@ -113,12 +125,18 @@ export function useAccountEditor(options: {
const settings = {
accountId,
notes: notes.value,
outboundProxyId: proxyMode.value === 'preserve' ? undefined : proxyMode.value === 'direct' ? '' : proxyId.value.trim(),
outboundProxyId: proxyMode.value === 'preserve'
? undefined
: proxyMode.value === 'direct'
? ''
: proxyId.value.trim(),
enabled: schedulingEnabled.value,
concurrencyLimit: scheduling.values.concurrencyLimit,
weight: scheduling.values.weight,
modelAccess: modelAccess.value,
groupIds: [...new Set(selectedGroupIds.value)],
groupIds: [
...new Set(selectedGroupIds.value),
],
}
const connectionChanged = isApiKey && (
apiKey.value.apiKey !== ''
@@ -128,14 +146,23 @@ export function useAccountEditor(options: {
await updateAccount({
...settings,
connection: connectionChanged && configuration
? { baseUrl: configuration.base_url.trim(), transport: configuration.transport, apiKey: apiKey.value.apiKey || undefined }
? {
baseUrl: configuration.base_url.trim(),
transport: configuration.transport,
apiKey: apiKey.value.apiKey || undefined,
}
: isOAuth && configurationReady.value && oauthTransport.value && oauthTransport.value !== savedOAuthTransport.value
? { transport: oauthTransport.value }
: undefined,
})
showEditModal.value = false
ZNotification.success({ message: t('accounts.editor.saved') })
void Promise.allSettled([options.reloadAccounts(), options.reloadGroups()])
ZNotification.success({
message: t('accounts.editor.saved'),
})
void Promise.allSettled([
options.reloadAccounts(),
options.reloadGroups(),
])
})
}
@@ -146,7 +173,10 @@ export function useAccountEditor(options: {
function clearCredentials() {
configurationRequest.invalidate()
apiKey.value = { ...emptyApiKeyAccountForm(), transport: '' }
apiKey.value = {
...emptyApiKeyAccountForm(),
transport: '',
}
savedConfiguration.value = undefined
oauthTransport.value = ''
savedOAuthTransport.value = undefined
@@ -80,7 +80,10 @@ export function useAccountImportTasks(options: { reload: () => Promise<unknown>
}
function created(task: AccountImportTask, reveal = true) {
tasks.value = [task, ...tasks.value.filter(entry => entry.taskId !== task.taskId)]
tasks.value = [
task,
...tasks.value.filter(entry => entry.taskId !== task.taskId),
]
selectedId.value = task.taskId
detail.value = null
if (open.value || !reveal)
@@ -117,5 +120,18 @@ export function useAccountImportTasks(options: { reload: () => Promise<unknown>
clearTimeout(timer)
})
return { open, tasks, detail, selectedId, error, loading, activeCount, stopping: stopAction.loading, select, created, refresh, stop }
return {
open,
tasks,
detail,
selectedId,
error,
loading,
activeCount,
stopping: stopAction.loading,
select,
created,
refresh,
stop,
}
}
@@ -105,7 +105,9 @@ export function useAccountMutations(options: {
options.selectedIds.value = remaining
showSingleDeleteModal.value = false
await loadAccounts()
ZNotification.success({ message: t('accounts.actions.deleted') })
ZNotification.success({
message: t('accounts.actions.deleted'),
})
},
)
}
@@ -129,15 +131,22 @@ export function useAccountMutations(options: {
}
showDeleteModal.value = false
await loadAccounts()
ZNotification.success({ message: t('accounts.actions.deletedAccountDeletedAccounts', { count: deletedCount }) })
ZNotification.success({
message: t('accounts.actions.deletedAccountDeletedAccounts', { count: deletedCount }),
})
},
{
errorText: false,
onError: (error) => {
void loadAccounts().catch(() => undefined)
ZNotification.error({ message: deletedCount > 0
? t('accounts.actions.partialDeletion', { count: deletedCount, value: errorMessage(error, t('common.errors.operation')) })
: errorMessage(error, t('accounts.actions.batchDeletionFailed')) })
ZNotification.error({
message: deletedCount > 0
? t('accounts.actions.partialDeletion', {
count: deletedCount,
value: errorMessage(error, t('common.errors.operation')),
})
: errorMessage(error, t('accounts.actions.batchDeletionFailed')),
})
},
},
)
@@ -148,7 +157,9 @@ export function useAccountMutations(options: {
return
const selected = [...options.selectedIds.value]
if (selected.length === 0) {
ZNotification.warning({ message: t('accounts.actions.exportSelectionRequired') })
ZNotification.warning({
message: t('accounts.actions.exportSelectionRequired'),
})
return
}
if (exportDisabledReason.value) {
@@ -164,9 +175,13 @@ export function useAccountMutations(options: {
})
const fileName = payload.fileName
await downloadJson(payload, fileName)
ZNotification.success({ message: t('accounts.actions.exported', { count: selected.length }) })
ZNotification.success({
message: t('accounts.actions.exported', { count: selected.length }),
})
},
{
errorText: t('accounts.actions.exportFailed'),
},
{ errorText: t('accounts.actions.exportFailed') },
)
}
@@ -177,7 +192,9 @@ export function useAccountMutations(options: {
const plan = account.planTypeDisplay.trim().replace(/[^\p{L}\p{N}_-]/gu, '_') || 'unknown-plan'
const name = account.name.trim().replace(/[^\p{L}\p{N}_-]/gu, '_') || 'account'
await downloadJson(result.catalog, `cpr-model-catalog-${plan}-${name}.json`)
ZNotification.success({ message: t('accounts.actions.modelCatalogDownloadedModels', { count: result.modelCount }) })
ZNotification.success({
message: t('accounts.actions.modelCatalogDownloadedModels', { count: result.modelCount }),
})
}
catch {}
})
@@ -200,7 +217,9 @@ export function useAccountMutations(options: {
ZNotification.error({ message: result.error || t('accounts.actions.refreshFailed') })
return
}
ZNotification.success({ message: t('accounts.actions.tokenRefreshed') })
ZNotification.success({
message: t('accounts.actions.tokenRefreshed'),
})
}
catch {}
})
@@ -209,9 +228,16 @@ export function useAccountMutations(options: {
async function handleToggleScheduling(account: Account) {
await togglingSchedulingAccounts.run(account.id, async () => {
try {
await batchUpdateAccounts({ accountIds: [account.id], enabled: !account.enabled })
await batchUpdateAccounts({
accountIds: [account.id],
enabled: !account.enabled,
})
await loadAccounts()
ZNotification.success({ message: account.enabled ? t('accounts.actions.schedulingIsDisabled') : t('accounts.actions.schedulingIsEnabled') })
ZNotification.success({
message: account.enabled
? t('accounts.actions.schedulingIsDisabled')
: t('accounts.actions.schedulingIsEnabled'),
})
}
catch {}
})
@@ -227,7 +253,9 @@ export function useAccountMutations(options: {
selectedIds.delete(accountId)
options.selectedIds.value = selectedIds
}
ZNotification.success({ message: t('accounts.actions.quotaRefreshed') })
ZNotification.success({
message: t('accounts.actions.quotaRefreshed'),
})
}
catch {}
})
@@ -241,7 +269,12 @@ export function useAccountMutations(options: {
await options.replaceAccount(result.account)
}
catch (error: unknown) {
ZNotification.warning({ message: t('accounts.actions.resetRefreshFailed', { value: errorMessage(error, t('accounts.actions.refreshQuotaManually')) }), ...({ duration: 5000 }) })
ZNotification.warning({
message: t('accounts.actions.resetRefreshFailed', {
value: errorMessage(error, t('accounts.actions.refreshQuotaManually')),
}),
...({ duration: 5000 }),
})
}
}
@@ -255,7 +288,9 @@ export function useAccountMutations(options: {
selectedIds.delete(accountId)
options.selectedIds.value = selectedIds
}
ZNotification.success({ message: t('accounts.actions.statusRestored') })
ZNotification.success({
message: t('accounts.actions.statusRestored'),
})
}
catch {}
})
@@ -19,7 +19,9 @@ export function useAccountOnboarding(options: {
const reauthorizingAccount = shallowRef<Account | null>(null)
const creatingAccountAction = useAsyncAction()
const createForm = ref(emptyAccountCreateForm())
const authorization = useAccountAuthorization(() => finishCreate(reauthorizingAccount.value ? t('accounts.onboarding.reauthorized') : t('accounts.onboarding.accountAdded')))
const authorization = useAccountAuthorization(() => finishCreate(reauthorizingAccount.value
? t('accounts.onboarding.reauthorized')
: t('accounts.onboarding.accountAdded')))
let submissionIds: string[] = []
watch(createForm, () => {
submissionIds = []
@@ -60,7 +62,9 @@ export function useAccountOnboarding(options: {
if (proxyError)
throw new Error(proxyError)
const settings = accountImportSettings(form)
const outboundProxyId = form.proxyMode === 'proxy' ? form.proxyId.trim() : undefined
const outboundProxyId = form.proxyMode === 'proxy'
? form.proxyId.trim()
: undefined
const mode = form.mode
if (mode === 'oauth')
return
@@ -75,7 +79,14 @@ export function useAccountOnboarding(options: {
provider: 'openai',
settings,
outboundProxyId,
data: { provider: 'openai', authentication_kind: 'api_key', name: form.apiKey.name.trim(), base_url: form.apiKey.base_url.trim(), api_key: form.apiKey.apiKey, transport: form.apiKey.transport },
data: {
provider: 'openai',
authentication_kind: 'api_key',
name: form.apiKey.name.trim(),
base_url: form.apiKey.base_url.trim(),
api_key: form.apiKey.apiKey,
transport: form.apiKey.transport,
},
})
await finishCreate(t('accounts.onboarding.apiKeyAccountAdded'))
return
@@ -85,7 +96,12 @@ export function useAccountOnboarding(options: {
: accountImportDocuments(requireImportProvider(provider), mode, form.importTexts[mode])
for (const document of documents)
requireImportProvider(document.provider)
const batches = accountImportBatches(documents.map(entry => ({ provider: entry.provider, data: entry.document, settings, outboundProxyId })))
const batches = accountImportBatches(documents.map(entry => ({
provider: entry.provider,
data: entry.document,
settings,
outboundProxyId,
})))
// 同一输入重试时复用每批标识,恢复丢失响应或部分提交,不重复执行 RT 交换
const ids = submissionIds
for (const [index, items] of batches.entries()) {
@@ -96,11 +112,18 @@ export function useAccountOnboarding(options: {
showCreateModal.value = false
options.onImportTaskCreated(task, finished)
}
ZNotification.success({ message: submitted > 1 ? t('accounts.onboarding.importTasksCreated', { submitted }) : t('accounts.onboarding.importTaskCreated') })
ZNotification.success({
message: submitted > 1
? t('accounts.onboarding.importTasksCreated', { submitted })
: t('accounts.onboarding.importTaskCreated'),
})
}, {
onError: () => {
if (submitted)
ZNotification.warning({ message: t('accounts.onboarding.partialSubmission') })
if (submitted) {
ZNotification.warning({
message: t('accounts.onboarding.partialSubmission'),
})
}
},
})
}
@@ -120,15 +143,25 @@ export function useAccountOnboarding(options: {
await authorization.start({
start: {
provider,
name: account?.name || account?.email || t('accounts.onboarding.accountName', { provider: formatProviderLabel(provider) }),
name: account?.name || account?.email || t('accounts.onboarding.accountName', {
provider: formatProviderLabel(provider),
}),
accountId: account?.id,
outboundProxyId: account ? undefined : form.proxyMode === 'proxy' ? form.proxyId.trim() : undefined,
outboundProxyId: account
? undefined
: form.proxyMode === 'proxy'
? form.proxyId.trim()
: undefined,
},
settings: account ? undefined : accountImportSettings(form),
settings: account
? undefined
: accountImportSettings(form),
})
}
catch (cause) {
ZNotification.error({ message: errorMessage(cause, t('accounts.onboarding.failedToStartAuthorization')) })
ZNotification.error({
message: errorMessage(cause, t('accounts.onboarding.failedToStartAuthorization')),
})
}
}
@@ -144,7 +177,11 @@ export function useAccountOnboarding(options: {
return
authorization.reset()
reauthorizingAccount.value = account
createForm.value = { ...emptyAccountCreateForm(), source: { kind: 'provider', id: account.provider }, step: 'import' }
createForm.value = {
...emptyAccountCreateForm(),
source: { kind: 'provider', id: account.provider },
step: 'import',
}
showCreateModal.value = true
void handleAuthorizeOAuth()
}
@@ -158,7 +195,11 @@ export function useAccountOnboarding(options: {
watch(() => accountCreateSourceKey(createForm.value), () => {
createForm.value = {
...createForm.value,
mode: reauthorizingAccount.value ? 'oauth' : createForm.value.source?.kind === 'bundle' ? 'json' : accountImportModes(accountCreateProvider(createForm.value))[0]?.value ?? 'json',
mode: reauthorizingAccount.value
? 'oauth'
: createForm.value.source?.kind === 'bundle'
? 'json'
: accountImportModes(accountCreateProvider(createForm.value))[0]?.value ?? 'json',
apiKey: emptyApiKeyAccountForm(),
importTexts: { access_token: '', refresh_token: '', json: '' },
}
@@ -169,7 +210,9 @@ export function useAccountOnboarding(options: {
() => createForm.value.mode,
() => createForm.value.step,
() => createForm.value.proxyMode,
() => createForm.value.proxyMode === 'proxy' ? createForm.value.proxyId.trim() : '',
() => createForm.value.proxyMode === 'proxy'
? createForm.value.proxyId.trim()
: '',
], () => {
authorization.reset()
}, { flush: 'sync' })
@@ -13,8 +13,12 @@ export function useAccountPersonalInfo({ accountId, open, capabilities }: {
const info = shallowRef<AccountPersonalInfoResponse | null>(null)
const request = useRequestState()
const { loading } = request
const profile = computed(() => capabilities.value.profile ? info.value?.profile ?? null : null)
const subscription = computed(() => capabilities.value.subscription ? info.value?.subscription ?? null : null)
const profile = computed(() => capabilities.value.profile
? info.value?.profile ?? null
: null)
const subscription = computed(() => capabilities.value.subscription
? info.value?.subscription ?? null
: null)
const error = computed(() => request.error.value || (capabilities.value.profile ? info.value?.profileError : '') || '')
async function load() {
@@ -39,7 +43,12 @@ export function useAccountPersonalInfo({ accountId, open, capabilities }: {
}
// 打开或切换账号只请求一次;关闭取消等待,刷新按钮复用同一入口。
watch([open, accountId, () => capabilities.value.profile, () => capabilities.value.subscription], ([isOpen]) => {
watch([
open,
accountId,
() => capabilities.value.profile,
() => capabilities.value.subscription,
], ([isOpen]) => {
request.invalidate({ resetLoading: isOpen })
if (!isOpen)
return
@@ -51,8 +51,11 @@ export function useAccountQuotaForecast(
if (disposed)
return
onAccountUpdated(result.account)
if (accountId.value === targetAccountId && await load())
ZNotification.success({ message: t('accounts.actions.quotaRefreshed') })
if (accountId.value === targetAccountId && await load()) {
ZNotification.success({
message: t('accounts.actions.quotaRefreshed'),
})
}
}
catch (cause) {
if (!disposed && accountId.value === targetAccountId) {
@@ -78,5 +81,12 @@ export function useAccountQuotaForecast(
disposed = true
})
return { report, loading, refreshing, error, load, refresh }
return {
report,
loading,
refreshing,
error,
load,
refresh,
}
}
@@ -190,7 +190,9 @@ export function useAccountResetCredits(options: {
|| (result.code === 'already_redeemed' && operation.hasTransportFailure)
target.pendingOperation = null
if (!confirmed) {
ZNotification.error({ message: resetResultMessage(result.code) })
ZNotification.error({
message: resetResultMessage(result.code),
})
await loadSessionCredits(target, true)
return false
}
@@ -213,11 +215,16 @@ export function useAccountResetCredits(options: {
...operation,
hasTransportFailure: true,
}
ZNotification.warning({ message: t('accounts.quotaReset.resultUnknown'), ...({ duration: 5000 }) })
ZNotification.warning({
message: t('accounts.quotaReset.resultUnknown'),
...({ duration: 5000 }),
})
}
else {
target.pendingOperation = null
ZNotification.error({ message: errorMessage(error, t('accounts.quotaReset.quotaResetFailed')) })
ZNotification.error({
message: errorMessage(error, t('accounts.quotaReset.quotaResetFailed')),
})
await loadSessionCredits(target, true)
}
return false
@@ -62,7 +62,9 @@ export function useAccountsQuery() {
async function replaceAccount(updated: Account) {
// 先取消旧查询并应用接口返回的账号,避免旧响应覆盖最新行数据。
query.invalidate()
query.items.value = query.items.value.map(account => account.id === updated.id ? updated : account)
query.items.value = query.items.value.map(account => account.id === updated.id
? updated
: account)
// 筛选、排序、概览和末页回退仍由回读校准,但不触发整表加载。
if (!await query.execute({ background: true }))
+52 -10
View File
@@ -40,12 +40,37 @@ export const accountColumns = computed(() => (defineTableColumns<Account>([
kind: 'meta',
size: 'md',
align: 'center',
formatter: value => formatProviderLabel(typeof value === 'string' ? value : null),
formatter: value => formatProviderLabel(typeof value === 'string'
? value
: null),
},
{
key: 'status',
label: t('common.fields.status'),
kind: 'status',
align: 'center',
sortable: true,
},
{
key: 'planType',
label: t('accounts.profile.subscription.label'),
kind: 'status',
size: 'sm',
sortable: true,
},
{
key: 'usage',
label: t('usage.usage'),
kind: 'custom',
size: '2xl',
sortable: true,
},
{
key: 'groups',
label: t('accounts.fields.accountGrouping'),
kind: 'status',
size: 'sm',
},
{ key: 'status', label: t('common.fields.status'), kind: 'status', align: 'center', sortable: true },
{ key: 'planType', label: t('accounts.profile.subscription.label'), kind: 'status', size: 'sm', sortable: true },
{ key: 'usage', label: t('usage.usage'), kind: 'custom', size: '2xl', sortable: true },
{ key: 'groups', label: t('accounts.fields.accountGrouping'), kind: 'status', size: 'sm' },
{
key: 'lastUsedAt',
label: t('accounts.fields.lastUsed'),
@@ -62,7 +87,13 @@ export const accountColumns = computed(() => (defineTableColumns<Account>([
formatter: value => optionalAccountCell(value),
emptyText: '',
},
{ key: 'actions', label: t('common.fields.actions'), kind: 'actions', size: 'lg', hideable: false },
{
key: 'actions',
label: t('common.fields.actions'),
kind: 'actions',
size: 'lg',
hideable: false,
},
])))
export const statusLabels = computed<Record<AccountStatus, string>>(() => ({
@@ -82,7 +113,10 @@ export const statusTones: Record<AccountStatus, 'success' | 'danger' | 'warning'
}
export const accountStatusFilterOptions = computed(() => ([
{ label: t('common.status.all'), value: '' },
{
label: t('common.status.all'),
value: '',
},
{ label: statusLabels.value.normal, value: 'normal' },
{ label: statusLabels.value.quota_exhausted, value: 'quota_exhausted' },
{ label: statusLabels.value.rate_limited, value: 'rate_limited' },
@@ -103,7 +137,9 @@ export function visibleSummaryQuotaWindows(windows: AccountQuotaWindow[]) {
const known = [...windows]
.filter(window => window.group !== 'other')
.sort(compareQuotaWindows)
return known.length > 0 ? known : [...windows].sort(compareQuotaWindows)
return known.length > 0
? known
: [...windows].sort(compareQuotaWindows)
}
export function orderedPanelQuotaWindows(windows: AccountQuotaWindow[]) {
@@ -117,7 +153,11 @@ export function groupedAccountQuotaWindows(windows: AccountQuotaWindow[]) {
for (const window of windows) {
const limitLabel = quotaLimitLabel(window)
if (!window.limitId || !limitLabel) {
entries.push({ key: window.key, label: null, windows: [window] })
entries.push({
key: window.key,
label: null,
windows: [window],
})
continue
}
@@ -199,5 +239,7 @@ function quotaLimitLabel(window: AccountQuotaWindow) {
}
function optionalAccountCell(value: unknown) {
return value === '—' || value === '-' ? '' : value
return value === '—' || value === '-'
? ''
: value
}
+8 -2
View File
@@ -70,7 +70,10 @@ const {
} = useAccountGroupCatalog()
const importTasks = useAccountImportTasks({
reload: () => Promise.all([loadAccounts(), loadGroups()]),
reload: () => Promise.all([
loadAccounts(),
loadGroups(),
]),
})
const {
open: showImportTasks,
@@ -123,7 +126,10 @@ const {
onImportTaskCreated: importTasks.created,
accounts,
selectedIds,
reload: () => Promise.all([loadAccounts(), loadGroups()]),
reload: () => Promise.all([
loadAccounts(),
loadGroups(),
]),
replaceAccount,
})
@@ -43,11 +43,15 @@ export function emptyAccountCreateForm(): AccountCreateForm {
}
export function accountCreateProvider(form: AccountCreateForm) {
return form.source?.kind === 'provider' ? form.source.id : undefined
return form.source?.kind === 'provider'
? form.source.id
: undefined
}
export function accountCreateSourceKey(form: AccountCreateForm) {
return form.source?.kind === 'provider' ? `provider:${form.source.id}` : form.source?.kind ?? ''
return form.source?.kind === 'provider'
? `provider:${form.source.id}`
: form.source?.kind ?? ''
}
export function accountProxyError(form: AccountCreateForm): string | undefined {
@@ -66,10 +70,17 @@ export function accountImportSettings(form: AccountCreateForm) {
if (modelError)
throw new Error(modelError)
return {
modelAccess: form.modelAccess ? { ...form.modelAccess, models: [...form.modelAccess.models] } : undefined,
modelAccess: form.modelAccess
? {
...form.modelAccess,
models: [...form.modelAccess.models],
}
: undefined,
enabled: form.enabled,
...scheduling.values,
groupIds: [...new Set(form.groupIds)],
groupIds: [
...new Set(form.groupIds),
],
notes: form.notes.trim() || undefined,
}
}
@@ -78,13 +89,26 @@ export function accountImportModes(provider: string | undefined) {
if (provider === 'openai') {
return [
{ label: 'OAuth', value: 'oauth' as const },
{ label: t('common.fields.apiKey'), value: 'api_key' as const },
{
label: t('common.fields.apiKey'),
value: 'api_key' as const,
},
{ label: 'AT', value: 'access_token' as const },
{ label: 'RT', value: 'refresh_token' as const },
{ label: t('accounts.onboarding.accountFile'), value: 'json' as const },
{
label: t('accounts.onboarding.accountFile'),
value: 'json' as const,
},
]
}
if (provider === 'xai') {
return [
{ label: 'OAuth', value: 'oauth' as const },
{
label: t('accounts.onboarding.accountFile'),
value: 'json' as const,
},
]
}
if (provider === 'xai')
return [{ label: 'OAuth', value: 'oauth' as const }, { label: t('accounts.onboarding.accountFile'), value: 'json' as const }]
return []
}
@@ -30,13 +30,23 @@ function splitAccountDocuments(entry: AccountImportDocument): AccountImportDocum
if (!Array.isArray(data.accounts) || !data.accounts.length)
return [entry]
return data.accounts.map((account) => {
const item: Record<string, unknown> = { ...data, accounts: [account] }
const item: Record<string, unknown> = {
...data,
accounts: [account],
}
if (Array.isArray(data.proxies)) {
// 保留同键的全部记录,让 Provider 继续拒绝重复引用,不复制其他账号的代理
const key = isRecord(account) ? account.proxy_key : undefined
const key = isRecord(account)
? account.proxy_key
: undefined
item.proxies = data.proxies.filter(proxy => typeof key === 'string' && isRecord(proxy) && proxy.proxy_key === key)
}
return { provider: entry.provider, document: data === document ? item : { ...document, data: item } }
return {
provider: entry.provider,
document: data === document
? item
: { ...document, data: item },
}
})
}
@@ -81,18 +91,32 @@ export function mixedImportDocuments(text: string): AccountImportDocument[] {
export function accountImportDocuments(provider: string, mode: string, text: string): AccountImportDocument[] {
if (provider === 'openai' && (mode === 'access_token' || mode === 'refresh_token')) {
const tokens = text.split(/\r?\n/).map(token => token.trim()).filter(Boolean)
const label = mode === 'access_token' ? 'Access Token' : 'Refresh Token'
const label = mode === 'access_token'
? 'Access Token'
: 'Refresh Token'
if (!tokens.length)
throw new Error(t('accounts.import.pasteAtLeastOne', { label }))
const key = mode === 'access_token' ? 'accessToken' : 'refreshToken'
return tokens.map(token => ({ provider, document: { accounts: [{ [key]: token }] } }))
const key = mode === 'access_token'
? 'accessToken'
: 'refreshToken'
return tokens.map(token => ({
provider,
document: {
accounts: [
{ [key]: token },
],
},
}))
}
const value = parseJson(text)
// 原生快捷入口保留 CPR 文件展开,插件文档的字段含义由插件自己解释。
if (isSupportedProvider(provider) && isRecord(value) && Array.isArray(value.documents)) {
const documents = mixedImportDocuments(text).filter(entry => entry.provider === provider)
if (!documents.length)
throw new Error(t('accounts.import.missingProvider', { provider: formatProviderLabel(provider) }))
if (!documents.length) {
throw new Error(t('accounts.import.missingProvider', {
provider: formatProviderLabel(provider),
}))
}
return documents
}
if (!isRecord(value))
@@ -16,19 +16,27 @@ export function parseAccountSchedulingForm(
weight: string,
): AccountSchedulingParseResult {
const limitText = concurrencyLimit.trim()
const parsedLimit = limitText === '' ? null : Number(limitText)
const parsedLimit = limitText === ''
? null
: Number(limitText)
if (
parsedLimit !== null
&& (!Number.isSafeInteger(parsedLimit)
|| parsedLimit < 1
|| parsedLimit > MAX_ACCOUNT_CONCURRENCY)
) {
return { valid: false, message: t('accounts.scheduling.invalidConcurrency', { value: MAX_ACCOUNT_CONCURRENCY }) }
return {
valid: false,
message: t('accounts.scheduling.invalidConcurrency', { value: MAX_ACCOUNT_CONCURRENCY }),
}
}
const parsedWeight = Number(weight.trim())
if (!Number.isSafeInteger(parsedWeight) || parsedWeight < 1 || parsedWeight > 100) {
return { valid: false, message: t('accounts.scheduling.weightMustBeIntegerFromTo') }
return {
valid: false,
message: t('accounts.scheduling.weightMustBeIntegerFromTo'),
}
}
return {
@@ -41,5 +49,7 @@ export function parseAccountSchedulingForm(
}
export function concurrencyLimitInput(value: number | null) {
return value === null ? '' : String(value)
return value === null
? ''
: String(value)
}
@@ -16,7 +16,12 @@ export interface ApiKeyAccountForm extends Omit<ApiKeyConfiguration, 'transport'
}
export function emptyApiKeyAccountForm(): ApiKeyAccountForm {
return { name: '', base_url: '', apiKey: '', transport: 'http' }
return {
name: '',
base_url: '',
apiKey: '',
transport: 'http',
}
}
export function parseApiKeyConfiguration(value: Record<string, unknown> | undefined): ApiKeyConfiguration | undefined {
@@ -31,9 +31,24 @@ const scheduleStats = computed(() => {
? '—'
: formatCompactNumber(value)
return [
{ label: t('dashboard.scheduling.defaultConcurrency'), value: cap?.maxConcurrentPerAccount === 0 ? t('common.values.unlimited') : display(cap?.maxConcurrentPerAccount) },
{ label: t('dashboard.scheduling.totalSlots'), value: unlimitedCapacity.value ? t('common.values.unlimited') : display(cap?.totalSlots) },
{ label: t('dashboard.scheduling.freeSlots'), value: unlimitedCapacity.value ? t('common.values.unlimited') : display(cap?.availableSlots) },
{
label: t('dashboard.scheduling.defaultConcurrency'),
value: cap?.maxConcurrentPerAccount === 0
? t('common.values.unlimited')
: display(cap?.maxConcurrentPerAccount),
},
{
label: t('dashboard.scheduling.totalSlots'),
value: unlimitedCapacity.value
? t('common.values.unlimited')
: display(cap?.totalSlots),
},
{
label: t('dashboard.scheduling.freeSlots'),
value: unlimitedCapacity.value
? t('common.values.unlimited')
: display(cap?.availableSlots),
},
]
})
@@ -44,14 +59,19 @@ const usedProgressStyle = computed(() => {
return {
width: `${(capacity.usedSlots / capacity.totalSlots) * 100}%`,
minWidth: capacity.usedSlots > 0 ? '3px' : undefined,
minWidth: capacity.usedSlots > 0
? '3px'
: undefined,
}
})
const usedRatio = computed(() => {
const cap = props.capacity
if (unlimitedCapacity.value)
return `${cap?.usedSlots === null || cap?.usedSlots === undefined ? '—' : formatCompactNumber(cap.usedSlots)} / ∞`
if (unlimitedCapacity.value) {
return `${cap?.usedSlots === null || cap?.usedSlots === undefined
? '—'
: formatCompactNumber(cap.usedSlots)} / ∞`
}
if (!cap || cap.totalSlots === null || cap.totalSlots === undefined)
return '— / —'
if (cap.usedSlots === null || cap.usedSlots === undefined)
@@ -105,10 +125,34 @@ const statusRows = computed(() => {
const counts = accountStatusCounts.value
if (!p || !counts) {
return [
{ label: t('common.status.healthy'), description: t('dashboard.accountPool.empty'), value: '—', tone: 'success', icon: CircleCheck },
{ label: t('dashboard.accountPool.quotaExhausted'), description: t('dashboard.accountPool.empty'), value: '—', tone: 'warning', icon: TriangleAlert },
{ label: t('dashboard.accountPool.rateLimited'), description: t('dashboard.accountPool.empty'), value: '—', tone: 'normal', icon: RefreshCw },
{ label: t('dashboard.accountPool.pending'), description: t('dashboard.accountPool.issueCountsEmpty'), value: '—', tone: 'danger', icon: ShieldAlert },
{
label: t('common.status.healthy'),
description: t('dashboard.accountPool.empty'),
value: '—',
tone: 'success',
icon: CircleCheck,
},
{
label: t('dashboard.accountPool.quotaExhausted'),
description: t('dashboard.accountPool.empty'),
value: '—',
tone: 'warning',
icon: TriangleAlert,
},
{
label: t('dashboard.accountPool.rateLimited'),
description: t('dashboard.accountPool.empty'),
value: '—',
tone: 'normal',
icon: RefreshCw,
},
{
label: t('dashboard.accountPool.pending'),
description: t('dashboard.accountPool.issueCountsEmpty'),
value: '—',
tone: 'danger',
icon: ShieldAlert,
},
]
}
@@ -20,7 +20,9 @@ type DashboardAccount = DashboardSnapshot['accountUsage'][number]
const email = computed(() => props.account.email?.trim() || String(props.account.id))
const displayTitle = computed(() => email.value.split('@')[0] || email.value)
const normalizedProvider = computed(() => props.account.provider?.trim().toLowerCase() || '')
const providerIconComponent = computed(() => normalizedProvider.value ? providerIcon(normalizedProvider.value) : undefined)
const providerIconComponent = computed(() => normalizedProvider.value
? providerIcon(normalizedProvider.value)
: undefined)
const authenticationIconComponent = computed(() => authenticationIcon(props.account.authenticationKind))
const providerLabel = computed(() => formatProviderLabel(props.account.provider, t('common.providers.unknown')))
@@ -20,7 +20,9 @@ const preferredMotion = usePreferredReducedMotion()
const heartbeatDot = useTemplateRef<HTMLElement>('heartbeatDot')
const heartbeatRing = useTemplateRef<HTMLElement>('heartbeatRing')
const statusLabel = computed(() =>
props.updatedAt ? t('dashboard.refresh.lastUpdated', { updatedAt: props.updatedAt }) : t('dashboard.refresh.waiting'),
props.updatedAt
? t('dashboard.refresh.lastUpdated', { updatedAt: props.updatedAt })
: t('dashboard.refresh.waiting'),
)
function pulse() {
@@ -45,8 +45,17 @@ const sparklineOption = computed<EChartsOption | null>(() => {
return {
animation: false,
grid: { left: 0, right: 0, top: 4, bottom: 4 },
xAxis: { type: 'category', show: false, data: values.map((_, index) => index) },
yAxis: { type: 'value', show: false, min: 'dataMin', max: 'dataMax' },
xAxis: {
type: 'category',
show: false,
data: values.map((_, index) => index),
},
yAxis: {
type: 'value',
show: false,
min: 'dataMin',
max: 'dataMax',
},
series: [
{
type: 'line',
@@ -44,14 +44,18 @@ const releaseLabel = computed(() => {
const release = profile.value?.release
if (!release?.latestVersion)
return t('dashboard.clientIdentity.notChecked')
return release.latestBuild ? `${release.latestVersion} · Build ${release.latestBuild}` : release.latestVersion
return release.latestBuild
? `${release.latestVersion} · Build ${release.latestBuild}`
: release.latestVersion
})
const releaseStatus = computed(() => {
const current = profile.value
if (!current?.release) {
return {
label: current?.attributes.some(item => item.key === 'version_policy' && item.value === 'fixed') ? t('dashboard.clientIdentity.customVersion') : t('dashboard.clientIdentity.active'),
label: current?.attributes.some(item => item.key === 'version_policy' && item.value === 'fixed')
? t('dashboard.clientIdentity.customVersion')
: t('dashboard.clientIdentity.active'),
title: t('dashboard.clientIdentity.activeDescription'),
tone: 'bg-cp-info-container text-cp-info-on-container',
icon: ShieldCheck,
@@ -93,12 +97,16 @@ const releaseStatus = computed(() => {
const verifiedLabel = computed(() => {
const verifiedAt = profile.value?.verifiedAtDisplay
return verifiedAt ? t('dashboard.clientIdentity.verifiedAt', { verifiedAt }) : undefined
return verifiedAt
? t('dashboard.clientIdentity.verifiedAt', { verifiedAt })
: undefined
})
const checkedLabel = computed(() => {
const checkedAt = profile.value?.release?.checkedAtDisplay
return checkedAt ? t('dashboard.clientIdentity.checkedAt', { checkedAt }) : undefined
return checkedAt
? t('dashboard.clientIdentity.checkedAt', { checkedAt })
: undefined
})
function toPascalCase(value: string) {
@@ -116,7 +124,9 @@ const clientIdentity = computed(() => {
const value = current.attributes.find(attribute => attribute.key === 'client_identifier')?.value
if (!value)
return '—'
return current.provider === 'xai' ? toPascalCase(value) : value
return current.provider === 'xai'
? toPascalCase(value)
: value
})
const attributeLabels = computed<Record<string, string>>(() => ({
@@ -131,7 +141,12 @@ const identityAttributes = computed(() => {
if (!current)
return []
return isSupportedProvider(current.provider)
? [{ label: t('dashboard.clientIdentity.clientId'), value: clientIdentity.value }]
? [
{
label: t('dashboard.clientIdentity.clientId'),
value: clientIdentity.value,
},
]
: current.attributes.map(attribute => ({
label: attributeLabels.value[attribute.key] ?? attribute.key,
value: attribute.value,
@@ -33,7 +33,9 @@ export function useDashboard() {
const capacityInfo = computed(() => snapshot.value.capacityInfo)
const rotationStrategy = computed(() => snapshot.value.rotationStrategy)
const trendView = computed(() => dashboardTrendView(
trend.value?.kind === activeTrendKind.value ? trend.value : null,
trend.value?.kind === activeTrendKind.value
? trend.value
: null,
))
const trendPoints = computed(() => trendView.value.points)
const trendSummary = computed(() => trendView.value.summary)
@@ -43,9 +43,18 @@ interface BarSeriesOptions {
}
const tabs = computed(() => ([
{ label: t('usage.usage'), value: 'usage' },
{ label: t('dashboard.trends.latency'), value: 'latency' },
{ label: t('dashboard.trends.errors'), value: 'errors' },
{
label: t('usage.usage'),
value: 'usage',
},
{
label: t('dashboard.trends.latency'),
value: 'latency',
},
{
label: t('dashboard.trends.errors'),
value: 'errors',
},
]))
export function useRequestTrendChart(options: {
@@ -84,8 +93,12 @@ export function useRequestTrendChart(options: {
return {
...getCoordinateSystem(buckets),
series: getSeries(),
animationDuration: preferredMotion.value === 'reduce' ? 0 : 420,
animationDurationUpdate: preferredMotion.value === 'reduce' ? 0 : 220,
animationDuration: preferredMotion.value === 'reduce'
? 0
: 420,
animationDurationUpdate: preferredMotion.value === 'reduce'
? 0
: 220,
animationEasing: 'cubicOut',
animationEasingUpdate: 'cubicOut',
tooltip: {
@@ -108,7 +121,9 @@ export function useRequestTrendChart(options: {
return {
grid: {
left: 8,
right: activeKind.value === 'latency' ? 8 : 10,
right: activeKind.value === 'latency'
? 8
: 10,
top: 10,
bottom: 2,
outerBoundsMode: 'same' as const,
@@ -118,7 +133,10 @@ export function useRequestTrendChart(options: {
type: 'category' as const,
data: buckets,
boundaryGap: activeKind.value === 'errors',
axisLine: { show: true, lineStyle: { color: axisLine } },
axisLine: {
show: true,
lineStyle: { color: axisLine },
},
axisTick: { show: false },
axisLabel: {
color: muted,
@@ -149,7 +167,9 @@ export function useRequestTrendChart(options: {
{
type: 'value' as const,
min: 0,
max: activeKind.value === 'errors' ? 100 : undefined,
max: activeKind.value === 'errors'
? 100
: undefined,
splitNumber: 3,
axisLine: { show: false },
axisTick: { show: false },
@@ -170,7 +190,9 @@ export function useRequestTrendChart(options: {
function formatPrimaryAxisValue(value: number) {
if (activeKind.value === 'latency') {
return value >= 1000 ? `${formatAxisNumber(value / 1000)} s` : `${Math.round(value)} ms`
return value >= 1000
? `${formatAxisNumber(value / 1000)} s`
: `${Math.round(value)} ms`
}
return formatAxisCompact(value)
}
@@ -184,7 +206,9 @@ export function useRequestTrendChart(options: {
}
function formatTooltip(params: unknown) {
const rows = Array.isArray(params) ? params : [params]
const rows = Array.isArray(params)
? params
: [params]
const point = points.value[tooltipIndex(rows[0])]
const title = point?.label ?? ''
@@ -223,7 +247,9 @@ export function useRequestTrendChart(options: {
case 'latencyP95Ms':
return formatLatency(point?.latencyP95Ms, value)
case 'outputThroughputP50':
return point?.outputThroughputP50 == null ? value : `${point.outputThroughputP50} tok/s`
return point?.outputThroughputP50 == null
? value
: `${point.outputThroughputP50} tok/s`
case 'errors':
return point?.errors ?? value
case 'successRate':
@@ -275,19 +301,34 @@ export function useRequestTrendChart(options: {
'latencyP95Ms',
activeSeriesValues('latencyP95Ms'),
trendColor('latencyP95Ms'),
{ lineType: 'dashed', smooth: 0.2, width: 1.8, z: 2 },
{
lineType: 'dashed',
smooth: 0.2,
width: 1.8,
z: 2,
},
),
...gapLineSeries(
'firstTokenP95Ms',
activeSeriesValues('firstTokenP95Ms'),
trendColor('firstTokenP95Ms'),
{ area: true, smooth: 0.24, width: 2.5, z: 4 },
{
area: true,
smooth: 0.24,
width: 2.5,
z: 4,
},
),
...gapLineSeries(
'outputThroughputP50',
activeSeriesValues('outputThroughputP50'),
trendColor('outputThroughputP50'),
{ smooth: 0.2, width: 2, yAxisIndex: 1, z: 3 },
{
smooth: 0.2,
width: 2,
yAxisIndex: 1,
z: 3,
},
),
]
}
@@ -319,7 +360,9 @@ export function useRequestTrendChart(options: {
function activeSeriesValues(key: string) {
return points.value.map(point =>
Number(point.requestsValue) > 0 ? trendPointNumber(point, key) : 0,
Number(point.requestsValue) > 0
? trendPointNumber(point, key)
: 0,
)
}
@@ -351,7 +394,10 @@ export function useRequestTrendChart(options: {
width: lineOptions.width ?? 2.2,
opacity: muted ? 0.18 : 1,
},
itemStyle: { color, opacity: muted ? 0.18 : 1 },
itemStyle: {
color,
opacity: muted ? 0.18 : 1,
},
emphasis: { disabled: true },
areaStyle: area
? {
@@ -411,7 +457,9 @@ export function useRequestTrendChart(options: {
z: barOptions.z ?? 2,
itemStyle: {
color,
opacity: muted ? 0.14 : (barOptions.opacity ?? 0.72),
opacity: muted
? 0.14
: (barOptions.opacity ?? 0.72),
borderRadius: barOptions.borderRadius ?? [3, 3, 0, 0],
},
emphasis: { disabled: true },
@@ -419,7 +467,9 @@ export function useRequestTrendChart(options: {
}
function summaryMarkerStyle(item: TrendSummaryItem) {
return item.colorVar ? { backgroundColor: `var(${item.colorVar})` } : undefined
return item.colorVar
? { backgroundColor: `var(${item.colorVar})` }
: undefined
}
function trendColor(metric: DashboardTrendMetric) {
@@ -431,7 +481,9 @@ export function useRequestTrendChart(options: {
}
function toggleSummarySeries(metric: DashboardTrendMetric) {
pinnedSummaryMetric.value = pinnedSummaryMetric.value === metric ? undefined : metric
pinnedSummaryMetric.value = pinnedSummaryMetric.value === metric
? undefined
: metric
}
function isSeriesMuted(metric: DashboardTrendMetric) {
@@ -470,7 +522,9 @@ function formatAxisCompact(value: number) {
}
function formatAxisNumber(value: number) {
return value >= 10 ? value.toFixed(0) : value.toFixed(1).replace(/\.0$/, '')
return value >= 10
? value.toFixed(0)
: value.toFixed(1).replace(/\.0$/, '')
}
function formatLatency(value: number | null | undefined, fallback: string) {
@@ -494,7 +548,9 @@ function tooltipValue(source: unknown, key: string) {
if (typeof source !== 'object' || source === null || !(key in source))
return ''
const value = (source as Record<string, unknown>)[key]
return typeof value === 'number' || typeof value === 'string' ? String(value) : ''
return typeof value === 'number' || typeof value === 'string'
? String(value)
: ''
}
function trendPointProperty(point: TrendPoint | undefined, key: string) {
@@ -505,7 +561,9 @@ function trendPointProperty(point: TrendPoint | undefined, key: string) {
function trendPointText(point: TrendPoint | undefined, key: string) {
const value = trendPointProperty(point, key)
return typeof value === 'string' ? value : undefined
return typeof value === 'string'
? value
: undefined
}
function zeroAwareRateText(
@@ -521,5 +579,7 @@ function zeroAwareRateText(
function trendPointNumber(point: TrendPoint, key: string) {
const value = trendPointProperty(point, key)
return typeof value === 'number' && Number.isFinite(value) ? value : null
return typeof value === 'number' && Number.isFinite(value)
? value
: null
}

Some files were not shown because too many files have changed in this diff Show More