mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
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:
co-authored by
Codex
Zyy
parent
dffc417b9f
commit
f0484c4e11
@@ -10,7 +10,9 @@ export function pluginBridge(): Plugin {
|
||||
const dependencies = new Set<string>()
|
||||
return {
|
||||
name: 'plugin-management-bridge',
|
||||
resolveId: source => source === id ? resolvedId : undefined,
|
||||
resolveId: source => source === id
|
||||
? resolvedId
|
||||
: undefined,
|
||||
async load(source) {
|
||||
if (source !== resolvedId)
|
||||
return
|
||||
@@ -20,11 +22,19 @@ export function pluginBridge(): Plugin {
|
||||
build: {
|
||||
write: false,
|
||||
minify: true,
|
||||
lib: { entry, name: 'CodexProxyPluginBridge', formats: ['iife'] },
|
||||
lib: {
|
||||
entry,
|
||||
name: 'CodexProxyPluginBridge',
|
||||
formats: ['iife'],
|
||||
},
|
||||
},
|
||||
})
|
||||
const outputs = (Array.isArray(result) ? result : [result])
|
||||
.flatMap(output => 'output' in output ? output.output : [])
|
||||
const outputs = (Array.isArray(result)
|
||||
? result
|
||||
: [result])
|
||||
.flatMap(output => 'output' in output
|
||||
? output.output
|
||||
: [])
|
||||
const [chunk] = outputs
|
||||
if (outputs.length !== 1 || chunk?.type !== 'chunk' || chunk.imports.length || chunk.dynamicImports.length)
|
||||
throw new Error('插件管理桥必须构建为单个独立脚本')
|
||||
@@ -38,7 +48,9 @@ export function pluginBridge(): Plugin {
|
||||
handleHotUpdate({ file, server }) {
|
||||
if (dependencies.has(file)) {
|
||||
const module = server.moduleGraph.getModuleById(resolvedId)
|
||||
return module ? [module] : []
|
||||
return module
|
||||
? [module]
|
||||
: []
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
+21
-10
@@ -88,15 +88,21 @@ export default antfu(
|
||||
},
|
||||
rules: {
|
||||
'template-style/single-line-simple-interpolation': 'error',
|
||||
'vue/html-closing-bracket-newline': ['error', {
|
||||
singleline: 'never',
|
||||
multiline: 'always',
|
||||
}],
|
||||
'vue/multiline-html-element-content-newline': ['error', {
|
||||
ignores: ['pre', 'textarea'],
|
||||
ignoreWhenEmpty: true,
|
||||
allowEmptyLines: false,
|
||||
}],
|
||||
'vue/html-closing-bracket-newline': [
|
||||
'error',
|
||||
{
|
||||
singleline: 'never',
|
||||
multiline: 'always',
|
||||
},
|
||||
],
|
||||
'vue/multiline-html-element-content-newline': [
|
||||
'error',
|
||||
{
|
||||
ignores: ['pre', 'textarea'],
|
||||
ignoreWhenEmpty: true,
|
||||
allowEmptyLines: false,
|
||||
},
|
||||
],
|
||||
'vue/mustache-interpolation-spacing': ['error', 'always'],
|
||||
},
|
||||
},
|
||||
@@ -118,7 +124,12 @@ export default antfu(
|
||||
'@intlify/vue-i18n/no-duplicate-keys-in-locale': 'error',
|
||||
'@intlify/vue-i18n/valid-message-syntax': 'error',
|
||||
'@intlify/vue-i18n/valid-plural-forms': 'error',
|
||||
'@intlify/vue-i18n/no-unused-keys': ['error', { extensions: ['.ts', '.vue'] }],
|
||||
'@intlify/vue-i18n/no-unused-keys': [
|
||||
'error',
|
||||
{
|
||||
extensions: ['.ts', '.vue'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"vue-router": "^5.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^9.5.1",
|
||||
"@antfu/eslint-config": "^10.0.0",
|
||||
"@intlify/eslint-plugin-vue-i18n": "4.5.1",
|
||||
"@intlify/unplugin-vue-i18n": "11.2.7",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
|
||||
Generated
+437
-220
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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 +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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
])))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
: '-'
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -43,7 +43,9 @@ router.beforeEach(async (to, from) => {
|
||||
}
|
||||
catch {
|
||||
// 首次进入时没有页面可保留,转到登录页;已有页面则保留,且不清除会话。
|
||||
return from === START_LOCATION ? login : false
|
||||
return from === START_LOCATION
|
||||
? login
|
||||
: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`}`
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
+14
-3
@@ -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)
|
||||
|
||||
+3
-1
@@ -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 }))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user