Merge upstream/main: v0.1.77-v0.1.84 updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
"dependencies": {
|
||||
"@lobehub/icons": "^4.0.2",
|
||||
"@vueuse/core": "^10.7.0",
|
||||
"axios": "^1.6.2",
|
||||
"axios": "^1.13.5",
|
||||
"chart.js": "^4.4.1",
|
||||
"dompurify": "^3.3.1",
|
||||
"driver.js": "^1.4.0",
|
||||
|
||||
21
frontend/pnpm-lock.yaml
generated
21
frontend/pnpm-lock.yaml
generated
@@ -15,8 +15,8 @@ importers:
|
||||
specifier: ^10.7.0
|
||||
version: 10.11.1(vue@3.5.26(typescript@5.6.3))
|
||||
axios:
|
||||
specifier: ^1.6.2
|
||||
version: 1.13.2
|
||||
specifier: ^1.13.5
|
||||
version: 1.13.5
|
||||
chart.js:
|
||||
specifier: ^4.4.1
|
||||
version: 4.5.1
|
||||
@@ -1257,56 +1257,67 @@ packages:
|
||||
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
|
||||
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.54.0':
|
||||
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
|
||||
@@ -1805,8 +1816,8 @@ packages:
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
axios@1.13.2:
|
||||
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
|
||||
axios@1.13.5:
|
||||
resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==}
|
||||
|
||||
babel-plugin-macros@3.1.0:
|
||||
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
|
||||
@@ -6387,7 +6398,7 @@ snapshots:
|
||||
postcss: 8.5.6
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
axios@1.13.2:
|
||||
axios@1.13.5:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
form-data: 4.0.5
|
||||
|
||||
@@ -32,6 +32,7 @@ export async function list(
|
||||
platform?: string
|
||||
type?: string
|
||||
status?: string
|
||||
group?: string
|
||||
search?: string
|
||||
},
|
||||
options?: {
|
||||
|
||||
@@ -53,4 +53,18 @@ export async function exchangeCode(
|
||||
return data
|
||||
}
|
||||
|
||||
export default { generateAuthUrl, exchangeCode }
|
||||
export async function refreshAntigravityToken(
|
||||
refreshToken: string,
|
||||
proxyId?: number | null
|
||||
): Promise<AntigravityTokenInfo> {
|
||||
const payload: Record<string, any> = { refresh_token: refreshToken }
|
||||
if (proxyId) payload.proxy_id = proxyId
|
||||
|
||||
const { data } = await apiClient.post<AntigravityTokenInfo>(
|
||||
'/admin/antigravity/oauth/refresh-token',
|
||||
payload
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
export default { generateAuthUrl, exchangeCode, refreshAntigravityToken }
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface ErrorPassthroughRule {
|
||||
response_code: number | null
|
||||
passthrough_body: boolean
|
||||
custom_message: string | null
|
||||
skip_monitoring: boolean
|
||||
description: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
@@ -41,6 +42,7 @@ export interface CreateRuleRequest {
|
||||
response_code?: number | null
|
||||
passthrough_body?: boolean
|
||||
custom_message?: string | null
|
||||
skip_monitoring?: boolean
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
@@ -59,6 +61,7 @@ export interface UpdateRuleRequest {
|
||||
response_code?: number | null
|
||||
passthrough_body?: boolean
|
||||
custom_message?: string | null
|
||||
skip_monitoring?: boolean
|
||||
description?: string | null
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.allGroups', { count: groups.length }) }}
|
||||
{{ t('admin.accounts.groupCountTotal', { count: groups.length }) }}
|
||||
</span>
|
||||
<button
|
||||
@click="showPopover = false"
|
||||
|
||||
@@ -708,6 +708,7 @@ const groupIds = ref<number[]>([])
|
||||
// All models list (combined Anthropic + OpenAI)
|
||||
const allModels = [
|
||||
{ value: 'claude-opus-4-6', label: 'Claude Opus 4.6' },
|
||||
{ value: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6' },
|
||||
{ value: 'claude-opus-4-5-20251101', label: 'Claude Opus 4.5' },
|
||||
{ value: 'claude-sonnet-4-20250514', label: 'Claude Sonnet 4' },
|
||||
{ value: 'claude-sonnet-4-5-20250929', label: 'Claude Sonnet 4.5' },
|
||||
@@ -754,6 +755,13 @@ const presetMappings = [
|
||||
color:
|
||||
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
},
|
||||
{
|
||||
label: 'Sonnet 4.6',
|
||||
from: 'claude-sonnet-4-6',
|
||||
to: 'claude-sonnet-4-6',
|
||||
color:
|
||||
'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
},
|
||||
{
|
||||
label: 'Opus->Sonnet',
|
||||
from: 'claude-opus-4-5-20251101',
|
||||
|
||||
@@ -665,8 +665,8 @@
|
||||
<Icon name="cloud" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{ t('admin.accounts.types.upstream') }}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.upstreamDesc') }}</span>
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">API Key</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.types.antigravityApikey') }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -681,7 +681,7 @@
|
||||
type="text"
|
||||
required
|
||||
class="input"
|
||||
placeholder="https://s.konstants.xyz"
|
||||
placeholder="https://cloudcode-pa.googleapis.com"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.upstream.baseUrlHint') }}</p>
|
||||
</div>
|
||||
@@ -816,8 +816,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Key input (only for apikey type) -->
|
||||
<div v-if="form.type === 'apikey'" class="space-y-4">
|
||||
<!-- API Key input (only for apikey type, excluding Antigravity which has its own fields) -->
|
||||
<div v-if="form.type === 'apikey' && form.platform !== 'antigravity'" class="space-y-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.baseUrl') }}</label>
|
||||
<input
|
||||
@@ -862,7 +862,7 @@
|
||||
<p class="input-hint">{{ t('admin.accounts.gemini.tier.aiStudioHint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Model Restriction Section (不适用于 Gemini) -->
|
||||
<!-- Model Restriction Section (不适用于 Gemini,Antigravity 已在上层条件排除) -->
|
||||
<div v-if="form.platform !== 'gemini'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||
|
||||
@@ -1527,6 +1527,46 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cache TTL Override -->
|
||||
<div class="rounded-lg border border-gray-200 p-4 dark:border-dark-600">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.quotaControl.cacheTTLOverride.label') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.quotaControl.cacheTTLOverride.hint') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="cacheTTLOverrideEnabled = !cacheTTLOverrideEnabled"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||
cacheTTLOverrideEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
cacheTTLOverrideEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="cacheTTLOverrideEnabled" class="mt-3">
|
||||
<label class="input-label text-xs">{{ t('admin.accounts.quotaControl.cacheTTLOverride.target') }}</label>
|
||||
<select
|
||||
v-model="cacheTTLOverrideTarget"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-dark-500 dark:bg-dark-700 dark:text-white"
|
||||
>
|
||||
<option value="5m">5m</option>
|
||||
<option value="1h">1h</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.quotaControl.cacheTTLOverride.targetHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -1647,12 +1687,12 @@
|
||||
:show-proxy-warning="form.platform !== 'openai' && !!form.proxy_id"
|
||||
:allow-multiple="form.platform === 'anthropic'"
|
||||
:show-cookie-option="form.platform === 'anthropic'"
|
||||
:show-refresh-token-option="form.platform === 'openai'"
|
||||
:show-refresh-token-option="form.platform === 'openai' || form.platform === 'antigravity'"
|
||||
:platform="form.platform"
|
||||
:show-project-id="geminiOAuthType === 'code_assist'"
|
||||
@generate-url="handleGenerateUrl"
|
||||
@cookie-auth="handleCookieAuth"
|
||||
@validate-refresh-token="handleOpenAIValidateRT"
|
||||
@validate-refresh-token="handleValidateRefreshToken"
|
||||
/>
|
||||
|
||||
</div>
|
||||
@@ -2146,6 +2186,8 @@ const maxSessions = ref<number | null>(null)
|
||||
const sessionIdleTimeout = ref<number | null>(null)
|
||||
const tlsFingerprintEnabled = ref(false)
|
||||
const sessionIdMaskingEnabled = ref(false)
|
||||
const cacheTTLOverrideEnabled = ref(false)
|
||||
const cacheTTLOverrideTarget = ref<string>('5m')
|
||||
|
||||
// Gemini tier selection (used as fallback when auto-detection is unavailable/fails)
|
||||
const geminiTierGoogleOne = ref<'google_one_free' | 'google_ai_pro' | 'google_ai_ultra'>('google_one_free')
|
||||
@@ -2597,6 +2639,8 @@ const resetForm = () => {
|
||||
sessionIdleTimeout.value = null
|
||||
tlsFingerprintEnabled.value = false
|
||||
sessionIdMaskingEnabled.value = false
|
||||
cacheTTLOverrideEnabled.value = false
|
||||
cacheTTLOverrideTarget.value = '5m'
|
||||
antigravityAccountType.value = 'oauth'
|
||||
upstreamBaseUrl.value = ''
|
||||
upstreamApiKey.value = ''
|
||||
@@ -2802,6 +2846,14 @@ const handleGenerateUrl = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleValidateRefreshToken = (rt: string) => {
|
||||
if (form.platform === 'openai') {
|
||||
handleOpenAIValidateRT(rt)
|
||||
} else if (form.platform === 'antigravity') {
|
||||
handleAntigravityValidateRT(rt)
|
||||
}
|
||||
}
|
||||
|
||||
const formatDateTimeLocal = formatDateTimeLocalInput
|
||||
const parseDateTimeLocal = parseDateTimeLocalInput
|
||||
|
||||
@@ -2950,6 +3002,95 @@ const handleOpenAIValidateRT = async (refreshTokenInput: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Antigravity 手动 RT 批量验证和创建
|
||||
const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
|
||||
if (!refreshTokenInput.trim()) return
|
||||
|
||||
// Parse multiple refresh tokens (one per line)
|
||||
const refreshTokens = refreshTokenInput
|
||||
.split('\n')
|
||||
.map((rt) => rt.trim())
|
||||
.filter((rt) => rt)
|
||||
|
||||
if (refreshTokens.length === 0) {
|
||||
antigravityOAuth.error.value = t('admin.accounts.oauth.antigravity.pleaseEnterRefreshToken')
|
||||
return
|
||||
}
|
||||
|
||||
antigravityOAuth.loading.value = true
|
||||
antigravityOAuth.error.value = ''
|
||||
|
||||
let successCount = 0
|
||||
let failedCount = 0
|
||||
const errors: string[] = []
|
||||
|
||||
try {
|
||||
for (let i = 0; i < refreshTokens.length; i++) {
|
||||
try {
|
||||
const tokenInfo = await antigravityOAuth.validateRefreshToken(
|
||||
refreshTokens[i],
|
||||
form.proxy_id
|
||||
)
|
||||
if (!tokenInfo) {
|
||||
failedCount++
|
||||
errors.push(`#${i + 1}: ${antigravityOAuth.error.value || 'Validation failed'}`)
|
||||
antigravityOAuth.error.value = ''
|
||||
continue
|
||||
}
|
||||
|
||||
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
|
||||
|
||||
// Generate account name with index for batch
|
||||
const accountName = refreshTokens.length > 1 ? `${form.name} #${i + 1}` : form.name
|
||||
|
||||
// Note: Antigravity doesn't have buildExtraInfo, so we pass empty extra or rely on credentials
|
||||
await adminAPI.accounts.create({
|
||||
name: accountName,
|
||||
notes: form.notes,
|
||||
platform: 'antigravity',
|
||||
type: 'oauth',
|
||||
credentials,
|
||||
extra: {},
|
||||
proxy_id: form.proxy_id,
|
||||
concurrency: form.concurrency,
|
||||
priority: form.priority,
|
||||
rate_multiplier: form.rate_multiplier,
|
||||
group_ids: form.group_ids,
|
||||
expires_at: form.expires_at,
|
||||
auto_pause_on_expired: autoPauseOnExpired.value
|
||||
})
|
||||
successCount++
|
||||
} catch (error: any) {
|
||||
failedCount++
|
||||
const errMsg = error.response?.data?.detail || error.message || 'Unknown error'
|
||||
errors.push(`#${i + 1}: ${errMsg}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Show results
|
||||
if (successCount > 0 && failedCount === 0) {
|
||||
appStore.showSuccess(
|
||||
refreshTokens.length > 1
|
||||
? t('admin.accounts.oauth.batchSuccess', { count: successCount })
|
||||
: t('admin.accounts.accountCreated')
|
||||
)
|
||||
emit('created')
|
||||
handleClose()
|
||||
} else if (successCount > 0 && failedCount > 0) {
|
||||
appStore.showWarning(
|
||||
t('admin.accounts.oauth.batchPartialSuccess', { success: successCount, failed: failedCount })
|
||||
)
|
||||
antigravityOAuth.error.value = errors.join('\n')
|
||||
emit('created')
|
||||
} else {
|
||||
antigravityOAuth.error.value = errors.join('\n')
|
||||
appStore.showError(t('admin.accounts.oauth.batchFailed'))
|
||||
}
|
||||
} finally {
|
||||
antigravityOAuth.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini OAuth 授权码兑换
|
||||
const handleGeminiExchange = async (authCode: string) => {
|
||||
if (!authCode.trim() || !geminiOAuth.sessionId.value) return
|
||||
@@ -3077,6 +3218,12 @@ const handleAnthropicExchange = async (authCode: string) => {
|
||||
extra.session_id_masking_enabled = true
|
||||
}
|
||||
|
||||
// Add cache TTL override settings
|
||||
if (cacheTTLOverrideEnabled.value) {
|
||||
extra.cache_ttl_override_enabled = true
|
||||
extra.cache_ttl_override_target = cacheTTLOverrideTarget.value
|
||||
}
|
||||
|
||||
const credentials = {
|
||||
...tokenInfo,
|
||||
...(interceptWarmupRequests.value ? { intercept_warmup_requests: true } : {})
|
||||
@@ -3170,6 +3317,12 @@ const handleCookieAuth = async (sessionKey: string) => {
|
||||
extra.session_id_masking_enabled = true
|
||||
}
|
||||
|
||||
// Add cache TTL override settings
|
||||
if (cacheTTLOverrideEnabled.value) {
|
||||
extra.cache_ttl_override_enabled = true
|
||||
extra.cache_ttl_override_target = cacheTTLOverrideTarget.value
|
||||
}
|
||||
|
||||
const accountName = keys.length > 1 ? `${form.name} #${i + 1}` : form.name
|
||||
|
||||
// Merge interceptWarmupRequests into credentials
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
? 'https://api.openai.com'
|
||||
: account.platform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: 'https://api.anthropic.com'
|
||||
: account.platform === 'antigravity'
|
||||
? 'https://cloudcode-pa.googleapis.com'
|
||||
: 'https://api.anthropic.com'
|
||||
"
|
||||
/>
|
||||
<p class="input-hint">{{ baseUrlHint }}</p>
|
||||
@@ -55,14 +57,16 @@
|
||||
? 'sk-proj-...'
|
||||
: account.platform === 'gemini'
|
||||
? 'AIza...'
|
||||
: 'sk-ant-...'
|
||||
: account.platform === 'antigravity'
|
||||
? 'sk-...'
|
||||
: 'sk-ant-...'
|
||||
"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.leaveEmptyToKeep') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Model Restriction Section (不适用于 Gemini) -->
|
||||
<div v-if="account.platform !== 'gemini'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<!-- Model Restriction Section (不适用于 Gemini 和 Antigravity) -->
|
||||
<div v-if="account.platform !== 'gemini' && account.platform !== 'antigravity'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||
|
||||
<!-- Mode Toggle -->
|
||||
@@ -372,7 +376,7 @@
|
||||
v-model="editBaseUrl"
|
||||
type="text"
|
||||
class="input"
|
||||
placeholder="https://s.konstants.xyz"
|
||||
placeholder="https://cloudcode-pa.googleapis.com"
|
||||
/>
|
||||
<p class="input-hint">{{ t('admin.accounts.upstream.baseUrlHint') }}</p>
|
||||
</div>
|
||||
@@ -900,6 +904,46 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cache TTL Override -->
|
||||
<div class="rounded-lg border border-gray-200 p-4 dark:border-dark-600">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.quotaControl.cacheTTLOverride.label') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.quotaControl.cacheTTLOverride.hint') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="cacheTTLOverrideEnabled = !cacheTTLOverrideEnabled"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||
cacheTTLOverrideEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
cacheTTLOverrideEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="cacheTTLOverrideEnabled" class="mt-3">
|
||||
<label class="input-label text-xs">{{ t('admin.accounts.quotaControl.cacheTTLOverride.target') }}</label>
|
||||
<select
|
||||
v-model="cacheTTLOverrideTarget"
|
||||
class="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-dark-500 dark:bg-dark-700 dark:text-white"
|
||||
>
|
||||
<option value="5m">5m</option>
|
||||
<option value="1h">1h</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.quotaControl.cacheTTLOverride.targetHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
@@ -1098,6 +1142,8 @@ const maxSessions = ref<number | null>(null)
|
||||
const sessionIdleTimeout = ref<number | null>(null)
|
||||
const tlsFingerprintEnabled = ref(false)
|
||||
const sessionIdMaskingEnabled = ref(false)
|
||||
const cacheTTLOverrideEnabled = ref(false)
|
||||
const cacheTTLOverrideTarget = ref<string>('5m')
|
||||
|
||||
// Computed: current preset mappings based on platform
|
||||
const presetMappings = computed(() => getPresetMappingsByPlatform(props.account?.platform || 'anthropic'))
|
||||
@@ -1485,6 +1531,8 @@ function loadQuotaControlSettings(account: Account) {
|
||||
sessionIdleTimeout.value = null
|
||||
tlsFingerprintEnabled.value = false
|
||||
sessionIdMaskingEnabled.value = false
|
||||
cacheTTLOverrideEnabled.value = false
|
||||
cacheTTLOverrideTarget.value = '5m'
|
||||
|
||||
// Only applies to Anthropic OAuth/SetupToken accounts
|
||||
if (account.platform !== 'anthropic' || (account.type !== 'oauth' && account.type !== 'setup-token')) {
|
||||
@@ -1513,6 +1561,12 @@ function loadQuotaControlSettings(account: Account) {
|
||||
if (account.session_id_masking_enabled === true) {
|
||||
sessionIdMaskingEnabled.value = true
|
||||
}
|
||||
|
||||
// Load cache TTL override setting
|
||||
if (account.cache_ttl_override_enabled === true) {
|
||||
cacheTTLOverrideEnabled.value = true
|
||||
cacheTTLOverrideTarget.value = account.cache_ttl_override_target || '5m'
|
||||
}
|
||||
}
|
||||
|
||||
function formatTempUnschedKeywords(value: unknown) {
|
||||
@@ -1719,6 +1773,15 @@ const handleSubmit = async () => {
|
||||
delete newExtra.session_id_masking_enabled
|
||||
}
|
||||
|
||||
// Cache TTL override setting
|
||||
if (cacheTTLOverrideEnabled.value) {
|
||||
newExtra.cache_ttl_override_enabled = true
|
||||
newExtra.cache_ttl_override_target = cacheTTLOverrideTarget.value
|
||||
} else {
|
||||
delete newExtra.cache_ttl_override_enabled
|
||||
delete newExtra.cache_ttl_override_target
|
||||
}
|
||||
|
||||
updatePayload.extra = newExtra
|
||||
}
|
||||
|
||||
|
||||
@@ -45,19 +45,19 @@
|
||||
class="text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span class="text-sm text-blue-900 dark:text-blue-200">{{
|
||||
t('admin.accounts.oauth.openai.refreshTokenAuth')
|
||||
t(getOAuthKey('refreshTokenAuth'))
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Refresh Token Input (OpenAI only) -->
|
||||
<!-- Refresh Token Input (OpenAI / Antigravity) -->
|
||||
<div v-if="inputMethod === 'refresh_token'" class="space-y-4">
|
||||
<div
|
||||
class="rounded-lg border border-blue-300 bg-white/80 p-4 dark:border-blue-600 dark:bg-gray-800/80"
|
||||
>
|
||||
<p class="mb-3 text-sm text-blue-700 dark:text-blue-300">
|
||||
{{ t('admin.accounts.oauth.openai.refreshTokenDesc') }}
|
||||
{{ t(getOAuthKey('refreshTokenDesc')) }}
|
||||
</p>
|
||||
|
||||
<!-- Refresh Token Input -->
|
||||
@@ -78,7 +78,7 @@
|
||||
v-model="refreshTokenInput"
|
||||
rows="3"
|
||||
class="input w-full resize-y font-mono text-sm"
|
||||
:placeholder="t('admin.accounts.oauth.openai.refreshTokenPlaceholder')"
|
||||
:placeholder="t(getOAuthKey('refreshTokenPlaceholder'))"
|
||||
></textarea>
|
||||
<p
|
||||
v-if="parsedRefreshTokenCount > 1"
|
||||
@@ -128,8 +128,8 @@
|
||||
<Icon v-else name="sparkles" size="sm" class="mr-2" />
|
||||
{{
|
||||
loading
|
||||
? t('admin.accounts.oauth.openai.validating')
|
||||
: t('admin.accounts.oauth.openai.validateAndCreate')
|
||||
? t(getOAuthKey('validating'))
|
||||
: t(getOAuthKey('validateAndCreate'))
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -148,6 +148,16 @@
|
||||
{{ rule.passthrough_body ? t('admin.errorPassthrough.passthrough') : t('admin.errorPassthrough.custom') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="rule.skip_monitoring" class="flex items-center gap-1">
|
||||
<Icon
|
||||
name="checkCircle"
|
||||
size="xs"
|
||||
class="text-yellow-500"
|
||||
/>
|
||||
<span class="text-gray-600 dark:text-gray-400">
|
||||
{{ t('admin.errorPassthrough.skipMonitoring') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
@@ -366,6 +376,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skip Monitoring -->
|
||||
<div class="flex items-center gap-1.5">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="form.skip_monitoring"
|
||||
class="h-3.5 w-3.5 rounded border-gray-300 text-yellow-600 focus:ring-yellow-500"
|
||||
/>
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.errorPassthrough.form.skipMonitoring') }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="input-hint text-xs -mt-3">{{ t('admin.errorPassthrough.form.skipMonitoringHint') }}</p>
|
||||
|
||||
<!-- Enabled -->
|
||||
<div class="flex items-center gap-1.5">
|
||||
<input
|
||||
@@ -453,6 +476,7 @@ const form = reactive({
|
||||
response_code: null as number | null,
|
||||
passthrough_body: true,
|
||||
custom_message: null as string | null,
|
||||
skip_monitoring: false,
|
||||
description: null as string | null
|
||||
})
|
||||
|
||||
@@ -497,6 +521,7 @@ const resetForm = () => {
|
||||
form.response_code = null
|
||||
form.passthrough_body = true
|
||||
form.custom_message = null
|
||||
form.skip_monitoring = false
|
||||
form.description = null
|
||||
errorCodesInput.value = ''
|
||||
keywordsInput.value = ''
|
||||
@@ -520,6 +545,7 @@ const handleEdit = (rule: ErrorPassthroughRule) => {
|
||||
form.response_code = rule.response_code
|
||||
form.passthrough_body = rule.passthrough_body
|
||||
form.custom_message = rule.custom_message
|
||||
form.skip_monitoring = rule.skip_monitoring
|
||||
form.description = rule.description
|
||||
errorCodesInput.value = rule.error_codes.join(', ')
|
||||
keywordsInput.value = rule.keywords.join('\n')
|
||||
@@ -575,6 +601,7 @@ const handleSubmit = async () => {
|
||||
response_code: form.passthrough_code ? null : form.response_code,
|
||||
passthrough_body: form.passthrough_body,
|
||||
custom_message: form.passthrough_body ? null : form.custom_message,
|
||||
skip_monitoring: form.skip_monitoring,
|
||||
description: form.description?.trim() || null
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,19 @@ import type { Account } from '@/types'
|
||||
const props = defineProps<{ show: boolean; account: Account | null; position: { top: number; left: number } | null }>()
|
||||
const emit = defineEmits(['close', 'test', 'stats', 'reauth', 'refresh-token', 'reset-status', 'clear-rate-limit'])
|
||||
const { t } = useI18n()
|
||||
const isRateLimited = computed(() => props.account?.rate_limit_reset_at && new Date(props.account.rate_limit_reset_at) > new Date())
|
||||
const isRateLimited = computed(() => {
|
||||
if (props.account?.rate_limit_reset_at && new Date(props.account.rate_limit_reset_at) > new Date()) {
|
||||
return true
|
||||
}
|
||||
const modelLimits = (props.account?.extra as Record<string, unknown> | undefined)?.model_rate_limits as
|
||||
| Record<string, { rate_limit_reset_at: string }>
|
||||
| undefined
|
||||
if (modelLimits) {
|
||||
const now = new Date()
|
||||
return Object.values(modelLimits).some(info => new Date(info.rate_limit_reset_at) > now)
|
||||
}
|
||||
return false
|
||||
})
|
||||
const isOverloaded = computed(() => props.account?.overload_until && new Date(props.account.overload_until) > new Date())
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
|
||||
@@ -10,16 +10,21 @@
|
||||
<Select :model-value="filters.platform" class="w-40" :options="pOpts" @update:model-value="updatePlatform" @change="$emit('change')" />
|
||||
<Select :model-value="filters.type" class="w-40" :options="tOpts" @update:model-value="updateType" @change="$emit('change')" />
|
||||
<Select :model-value="filters.status" class="w-40" :options="sOpts" @update:model-value="updateStatus" @change="$emit('change')" />
|
||||
<Select :model-value="filters.group" class="w-40" :options="gOpts" @update:model-value="updateGroup" @change="$emit('change')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import Select from '@/components/common/Select.vue'; import SearchInput from '@/components/common/SearchInput.vue'
|
||||
const props = defineProps(['searchQuery', 'filters']); const emit = defineEmits(['update:searchQuery', 'update:filters', 'change']); const { t } = useI18n()
|
||||
import type { AdminGroup } from '@/types'
|
||||
const props = defineProps<{ searchQuery: string; filters: Record<string, any>; groups?: AdminGroup[] }>()
|
||||
const emit = defineEmits(['update:searchQuery', 'update:filters', 'change']); const { t } = useI18n()
|
||||
const updatePlatform = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, platform: value }) }
|
||||
const updateType = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, type: value }) }
|
||||
const updateStatus = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, status: value }) }
|
||||
const updateGroup = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, group: value }) }
|
||||
const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }])
|
||||
const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }])
|
||||
const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }])
|
||||
const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }, { value: 'rate_limited', label: t('admin.accounts.status.rateLimited') }])
|
||||
const gOpts = computed(() => [{ value: '', label: t('admin.accounts.allGroups') }, ...(props.groups || []).map(g => ({ value: String(g.id), label: g.name }))])
|
||||
</script>
|
||||
|
||||
@@ -70,6 +70,8 @@
|
||||
<div v-if="row.cache_creation_tokens > 0" class="inline-flex items-center gap-1">
|
||||
<svg class="h-3.5 w-3.5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
|
||||
<span class="font-medium text-amber-600 dark:text-amber-400">{{ formatCacheTokens(row.cache_creation_tokens) }}</span>
|
||||
<span v-if="row.cache_creation_1h_tokens > 0" class="inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-orange-100 text-orange-600 ring-1 ring-inset ring-orange-200 dark:bg-orange-500/20 dark:text-orange-400 dark:ring-orange-500/30">1h</span>
|
||||
<span v-if="row.cache_ttl_overridden" :title="t('usage.cacheTtlOverriddenHint')" class="inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-rose-100 text-rose-600 ring-1 ring-inset ring-rose-200 dark:bg-rose-500/20 dark:text-rose-400 dark:ring-rose-500/30 cursor-help">R</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,9 +159,36 @@
|
||||
<span class="text-gray-400">{{ t('admin.usage.outputTokens') }}</span>
|
||||
<span class="font-medium text-white">{{ tokenTooltipData.output_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div v-if="tokenTooltipData && tokenTooltipData.cache_creation_tokens > 0" class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400">{{ t('admin.usage.cacheCreationTokens') }}</span>
|
||||
<span class="font-medium text-white">{{ tokenTooltipData.cache_creation_tokens.toLocaleString() }}</span>
|
||||
<div v-if="tokenTooltipData && tokenTooltipData.cache_creation_tokens > 0">
|
||||
<!-- 有 5m/1h 明细时,展开显示 -->
|
||||
<template v-if="tokenTooltipData.cache_creation_5m_tokens > 0 || tokenTooltipData.cache_creation_1h_tokens > 0">
|
||||
<div v-if="tokenTooltipData.cache_creation_5m_tokens > 0" class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400 flex items-center gap-1.5">
|
||||
{{ t('admin.usage.cacheCreation5mTokens') }}
|
||||
<span class="inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-amber-500/20 text-amber-400 ring-1 ring-inset ring-amber-500/30">5m</span>
|
||||
</span>
|
||||
<span class="font-medium text-white">{{ tokenTooltipData.cache_creation_5m_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div v-if="tokenTooltipData.cache_creation_1h_tokens > 0" class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400 flex items-center gap-1.5">
|
||||
{{ t('admin.usage.cacheCreation1hTokens') }}
|
||||
<span class="inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-orange-500/20 text-orange-400 ring-1 ring-inset ring-orange-500/30">1h</span>
|
||||
</span>
|
||||
<span class="font-medium text-white">{{ tokenTooltipData.cache_creation_1h_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 无明细时,只显示聚合值 -->
|
||||
<div v-else class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400">{{ t('admin.usage.cacheCreationTokens') }}</span>
|
||||
<span class="font-medium text-white">{{ tokenTooltipData.cache_creation_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tokenTooltipData && tokenTooltipData.cache_ttl_overridden" class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400 flex items-center gap-1.5">
|
||||
{{ t('usage.cacheTtlOverriddenLabel') }}
|
||||
<span class="inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-rose-500/20 text-rose-400 ring-1 ring-inset ring-rose-500/30">R-{{ tokenTooltipData.cache_creation_1h_tokens > 0 ? '5m' : '1H' }}</span>
|
||||
</span>
|
||||
<span class="font-medium text-rose-400">{{ tokenTooltipData.cache_creation_1h_tokens > 0 ? t('usage.cacheTtlOverridden1h') : t('usage.cacheTtlOverridden5m') }}</span>
|
||||
</div>
|
||||
<div v-if="tokenTooltipData && tokenTooltipData.cache_read_tokens > 0" class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400">{{ t('admin.usage.cacheReadTokens') }}</span>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="stat-label truncate">{{ title }}</p>
|
||||
<div class="mt-1 flex items-baseline gap-2">
|
||||
<p class="stat-value">{{ formattedValue }}</p>
|
||||
<p class="stat-value" :title="String(formattedValue)">{{ formattedValue }}</p>
|
||||
<span v-if="change !== undefined" :class="['stat-trend', trendClass]">
|
||||
<Icon
|
||||
v-if="changeType !== 'neutral'"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="sidebar-header">
|
||||
<!-- Custom Logo or Default Logo -->
|
||||
<div class="flex h-9 w-9 items-center justify-center overflow-hidden rounded-xl shadow-glow">
|
||||
<img :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" />
|
||||
<img v-if="settingsLoaded" :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" />
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="!sidebarCollapsed" class="flex flex-col">
|
||||
@@ -167,6 +167,7 @@ const isDark = ref(document.documentElement.classList.contains('dark'))
|
||||
const siteName = computed(() => appStore.siteName)
|
||||
const siteLogo = computed(() => appStore.siteLogo)
|
||||
const siteVersion = computed(() => appStore.siteVersion)
|
||||
const settingsLoaded = computed(() => appStore.publicSettingsLoaded)
|
||||
|
||||
// SVG Icon Components
|
||||
const DashboardIcon = {
|
||||
|
||||
@@ -29,17 +29,19 @@
|
||||
<!-- Logo/Brand -->
|
||||
<div class="mb-8 text-center">
|
||||
<!-- Custom Logo or Default Logo -->
|
||||
<div
|
||||
class="mb-4 inline-flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl shadow-lg shadow-primary-500/30"
|
||||
>
|
||||
<img :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" />
|
||||
</div>
|
||||
<h1 class="text-gradient mb-2 text-3xl font-bold">
|
||||
{{ siteName }}
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-dark-400">
|
||||
{{ siteSubtitle }}
|
||||
</p>
|
||||
<template v-if="settingsLoaded">
|
||||
<div
|
||||
class="mb-4 inline-flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl shadow-lg shadow-primary-500/30"
|
||||
>
|
||||
<img :src="siteLogo || '/logo.png'" alt="Logo" class="h-full w-full object-contain" />
|
||||
</div>
|
||||
<h1 class="text-gradient mb-2 text-3xl font-bold">
|
||||
{{ siteName }}
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-dark-400">
|
||||
{{ siteSubtitle }}
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Card Container -->
|
||||
@@ -61,25 +63,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { getPublicSettings } from '@/api/auth'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useAppStore } from '@/stores'
|
||||
import { sanitizeUrl } from '@/utils/url'
|
||||
|
||||
const siteName = ref('TianShuAPI')
|
||||
const siteLogo = ref('')
|
||||
const siteSubtitle = ref('Subscription to API Conversion Platform')
|
||||
const appStore = useAppStore()
|
||||
|
||||
const siteName = computed(() => appStore.siteName || 'TianShuAPI')
|
||||
const siteLogo = computed(() => sanitizeUrl(appStore.siteLogo || '', { allowRelative: true, allowDataUrl: true }))
|
||||
const siteSubtitle = computed(() => appStore.cachedPublicSettings?.site_subtitle || 'Subscription to API Conversion Platform')
|
||||
const settingsLoaded = computed(() => appStore.publicSettingsLoaded)
|
||||
|
||||
const currentYear = computed(() => new Date().getFullYear())
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const settings = await getPublicSettings()
|
||||
siteName.value = settings.site_name || 'TianShuAPI'
|
||||
siteLogo.value = sanitizeUrl(settings.site_logo || '', { allowRelative: true })
|
||||
siteSubtitle.value = settings.site_subtitle || 'Subscription to API Conversion Platform'
|
||||
} catch (error) {
|
||||
console.error('Failed to load public settings:', error)
|
||||
}
|
||||
onMounted(() => {
|
||||
appStore.fetchPublicSettings()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -83,6 +83,35 @@ export function useAntigravityOAuth() {
|
||||
}
|
||||
}
|
||||
|
||||
const validateRefreshToken = async (
|
||||
refreshToken: string,
|
||||
proxyId?: number | null
|
||||
): Promise<AntigravityTokenInfo | null> => {
|
||||
if (!refreshToken.trim()) {
|
||||
error.value = t('admin.accounts.oauth.antigravity.pleaseEnterRefreshToken')
|
||||
return null
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const tokenInfo = await adminAPI.antigravity.refreshAntigravityToken(
|
||||
refreshToken.trim(),
|
||||
proxyId
|
||||
)
|
||||
return tokenInfo as AntigravityTokenInfo
|
||||
} catch (err: any) {
|
||||
error.value =
|
||||
err.response?.data?.detail || t('admin.accounts.oauth.antigravity.failedToValidateRT')
|
||||
// Don't show global error toast for batch validation to avoid spamming
|
||||
// appStore.showError(error.value)
|
||||
return null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const buildCredentials = (tokenInfo: AntigravityTokenInfo): Record<string, unknown> => {
|
||||
let expiresAt: string | undefined
|
||||
if (typeof tokenInfo.expires_at === 'number' && Number.isFinite(tokenInfo.expires_at)) {
|
||||
@@ -110,6 +139,7 @@ export function useAntigravityOAuth() {
|
||||
resetState,
|
||||
generateAuthUrl,
|
||||
exchangeAuthCode,
|
||||
validateRefreshToken,
|
||||
buildCredentials
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export const claudeModels = [
|
||||
'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001',
|
||||
'claude-opus-4-5-20251101',
|
||||
'claude-opus-4-6',
|
||||
'claude-sonnet-4-6',
|
||||
'claude-2.1', 'claude-2.0', 'claude-instant-1.2'
|
||||
]
|
||||
|
||||
@@ -233,6 +234,7 @@ export const allModels = allModelsList.map(m => ({ value: m, label: m }))
|
||||
const anthropicPresetMappings = [
|
||||
{ label: 'Sonnet 4', from: 'claude-sonnet-4-20250514', to: 'claude-sonnet-4-20250514', color: 'bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400' },
|
||||
{ label: 'Sonnet 4.5', from: 'claude-sonnet-4-5-20250929', to: 'claude-sonnet-4-5-20250929', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
|
||||
{ label: 'Sonnet 4.6', from: 'claude-sonnet-4-6', to: 'claude-sonnet-4-6', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
|
||||
{ label: 'Opus 4.5', from: 'claude-opus-4-5-20251101', to: 'claude-opus-4-5-20251101', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
|
||||
{ label: 'Opus 4.6', from: 'claude-opus-4-6', to: 'claude-opus-4-6', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
|
||||
{ label: 'Haiku 3.5', from: 'claude-3-5-haiku-20241022', to: 'claude-3-5-haiku-20241022', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' },
|
||||
|
||||
@@ -576,6 +576,10 @@ export default {
|
||||
description: 'View and analyze your API usage history',
|
||||
costDetails: 'Cost Breakdown',
|
||||
tokenDetails: 'Token Breakdown',
|
||||
cacheTtlOverriddenHint: 'Cache TTL Override enabled',
|
||||
cacheTtlOverriddenLabel: 'TTL Override',
|
||||
cacheTtlOverridden5m: 'Billed as 5m',
|
||||
cacheTtlOverridden1h: 'Billed as 1h',
|
||||
totalRequests: 'Total Requests',
|
||||
totalTokens: 'Total Tokens',
|
||||
totalCost: 'Total Cost',
|
||||
@@ -841,7 +845,7 @@ export default {
|
||||
createUser: 'Create User',
|
||||
editUser: 'Edit User',
|
||||
deleteUser: 'Delete User',
|
||||
searchUsers: 'Search users...',
|
||||
searchUsers: 'Search by email, username, notes, or API key...',
|
||||
allRoles: 'All Roles',
|
||||
allStatus: 'All Status',
|
||||
admin: 'Admin',
|
||||
@@ -1335,6 +1339,7 @@ export default {
|
||||
allPlatforms: 'All Platforms',
|
||||
allTypes: 'All Types',
|
||||
allStatus: 'All Status',
|
||||
allGroups: 'All Groups',
|
||||
oauthType: 'OAuth',
|
||||
setupToken: 'Setup Token',
|
||||
apiKey: 'API Key',
|
||||
@@ -1344,7 +1349,7 @@ export default {
|
||||
schedulableEnabled: 'Scheduling enabled',
|
||||
schedulableDisabled: 'Scheduling disabled',
|
||||
failedToToggleSchedulable: 'Failed to toggle scheduling status',
|
||||
allGroups: '{count} groups total',
|
||||
groupCountTotal: '{count} groups total',
|
||||
platforms: {
|
||||
anthropic: 'Anthropic',
|
||||
claude: 'Claude',
|
||||
@@ -1359,6 +1364,7 @@ export default {
|
||||
googleOauth: 'Google OAuth',
|
||||
codeAssist: 'Code Assist',
|
||||
antigravityOauth: 'Antigravity OAuth',
|
||||
antigravityApikey: 'Connect via Base URL + API Key',
|
||||
upstream: 'Upstream',
|
||||
upstreamDesc: 'Connect via Base URL + API Key'
|
||||
},
|
||||
@@ -1593,6 +1599,12 @@ export default {
|
||||
sessionIdMasking: {
|
||||
label: 'Session ID Masking',
|
||||
hint: 'When enabled, fixes the session ID in metadata.user_id for 15 minutes, making upstream think requests come from the same session'
|
||||
},
|
||||
cacheTTLOverride: {
|
||||
label: 'Cache TTL Override',
|
||||
hint: 'Force all cache creation tokens to be billed as the selected TTL tier (5m or 1h)',
|
||||
target: 'Target TTL',
|
||||
targetHint: 'Select the TTL tier for billing'
|
||||
}
|
||||
},
|
||||
expired: 'Expired',
|
||||
@@ -1625,7 +1637,7 @@ export default {
|
||||
// Upstream type
|
||||
upstream: {
|
||||
baseUrl: 'Upstream Base URL',
|
||||
baseUrlHint: 'The address of the upstream Antigravity service, e.g., https://s.konstants.xyz',
|
||||
baseUrlHint: 'The address of the upstream Antigravity service, e.g., https://cloudcode-pa.googleapis.com',
|
||||
apiKey: 'Upstream API Key',
|
||||
apiKeyHint: 'API Key for the upstream service',
|
||||
pleaseEnterBaseUrl: 'Please enter upstream Base URL',
|
||||
@@ -1773,13 +1785,20 @@ export default {
|
||||
authCode: 'Authorization URL or Code',
|
||||
authCodePlaceholder:
|
||||
'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value',
|
||||
authCodeHint: 'You can copy the entire URL or just the code parameter value, the system will auto-detect',
|
||||
failedToGenerateUrl: 'Failed to generate Antigravity auth URL',
|
||||
missingExchangeParams: 'Missing code, session ID, or state',
|
||||
failedToExchangeCode: 'Failed to exchange Antigravity auth code'
|
||||
}
|
||||
},
|
||||
// Gemini specific (platform-wide)
|
||||
authCodeHint: 'You can copy the entire URL or just the code parameter value, the system will auto-detect',
|
||||
failedToGenerateUrl: 'Failed to generate Antigravity auth URL',
|
||||
missingExchangeParams: 'Missing code, session ID, or state',
|
||||
failedToExchangeCode: 'Failed to exchange Antigravity auth code',
|
||||
// Refresh Token auth
|
||||
refreshTokenAuth: 'Manual RT',
|
||||
refreshTokenDesc: 'Enter your existing Antigravity Refresh Token. Supports batch input (one per line). The system will automatically validate and create accounts.',
|
||||
refreshTokenPlaceholder: 'Paste your Antigravity Refresh Token...\nSupports multiple tokens, one per line',
|
||||
validating: 'Validating...',
|
||||
validateAndCreate: 'Validate & Create',
|
||||
pleaseEnterRefreshToken: 'Please enter Refresh Token',
|
||||
failedToValidateRT: 'Failed to validate Refresh Token'
|
||||
}
|
||||
}, // Gemini specific (platform-wide)
|
||||
gemini: {
|
||||
helpButton: 'Help',
|
||||
helpDialog: {
|
||||
@@ -2128,7 +2147,7 @@ export default {
|
||||
title: 'Redeem Code Management',
|
||||
description: 'Generate and manage redeem codes',
|
||||
generateCodes: 'Generate Codes',
|
||||
searchCodes: 'Search codes...',
|
||||
searchCodes: 'Search codes or email...',
|
||||
allTypes: 'All Types',
|
||||
allStatus: 'All Status',
|
||||
balance: 'Balance',
|
||||
@@ -2351,6 +2370,8 @@ export default {
|
||||
inputTokens: 'Input Tokens',
|
||||
outputTokens: 'Output Tokens',
|
||||
cacheCreationTokens: 'Cache Creation Tokens',
|
||||
cacheCreation5mTokens: 'Cache Write',
|
||||
cacheCreation1hTokens: 'Cache Write',
|
||||
cacheReadTokens: 'Cache Read Tokens',
|
||||
failedToLoad: 'Failed to load usage records',
|
||||
billingType: 'Billing Type',
|
||||
@@ -3352,6 +3373,7 @@ export default {
|
||||
custom: 'Custom',
|
||||
code: 'Code',
|
||||
body: 'Body',
|
||||
skipMonitoring: 'Skip Monitoring',
|
||||
|
||||
// Columns
|
||||
columns: {
|
||||
@@ -3396,6 +3418,8 @@ export default {
|
||||
passthroughBody: 'Passthrough upstream error message',
|
||||
customMessage: 'Custom error message',
|
||||
customMessagePlaceholder: 'Error message to return to client...',
|
||||
skipMonitoring: 'Skip monitoring',
|
||||
skipMonitoringHint: 'When enabled, errors matching this rule will not be recorded in ops monitoring',
|
||||
enabled: 'Enable this rule'
|
||||
},
|
||||
|
||||
|
||||
@@ -582,6 +582,10 @@ export default {
|
||||
description: '查看和分析您的 API 使用历史',
|
||||
costDetails: '成本明细',
|
||||
tokenDetails: 'Token 明细',
|
||||
cacheTtlOverriddenHint: '缓存 TTL Override 已启用',
|
||||
cacheTtlOverriddenLabel: 'TTL 替换',
|
||||
cacheTtlOverridden5m: '按 5m 计费',
|
||||
cacheTtlOverridden1h: '按 1h 计费',
|
||||
totalRequests: '总请求数',
|
||||
totalTokens: '总 Token',
|
||||
totalCost: '总消费',
|
||||
@@ -865,8 +869,8 @@ export default {
|
||||
editUser: '编辑用户',
|
||||
deleteUser: '删除用户',
|
||||
deleteConfirmMessage: "确定要删除用户 '{email}' 吗?此操作无法撤销。",
|
||||
searchPlaceholder: '搜索用户邮箱或用户名、备注、支持模糊查询...',
|
||||
searchUsers: '搜索用户邮箱或用户名、备注、支持模糊查询',
|
||||
searchPlaceholder: '邮箱/用户名/备注/API Key 模糊搜索...',
|
||||
searchUsers: '邮箱/用户名/备注/API Key 模糊搜索',
|
||||
roleFilter: '角色筛选',
|
||||
allRoles: '全部角色',
|
||||
allStatus: '全部状态',
|
||||
@@ -1426,6 +1430,7 @@ export default {
|
||||
allPlatforms: '全部平台',
|
||||
allTypes: '全部类型',
|
||||
allStatus: '全部状态',
|
||||
allGroups: '全部分组',
|
||||
oauthType: 'OAuth',
|
||||
// Schedulable toggle
|
||||
schedulable: '参与调度',
|
||||
@@ -1433,7 +1438,7 @@ export default {
|
||||
schedulableEnabled: '调度已开启',
|
||||
schedulableDisabled: '调度已关闭',
|
||||
failedToToggleSchedulable: '切换调度状态失败',
|
||||
allGroups: '共 {count} 个分组',
|
||||
groupCountTotal: '共 {count} 个分组',
|
||||
columns: {
|
||||
name: '名称',
|
||||
platformType: '平台/类型',
|
||||
@@ -1493,6 +1498,7 @@ export default {
|
||||
googleOauth: 'Google OAuth',
|
||||
codeAssist: 'Code Assist',
|
||||
antigravityOauth: 'Antigravity OAuth',
|
||||
antigravityApikey: '通过 Base URL + API Key 连接',
|
||||
upstream: '对接上游',
|
||||
upstreamDesc: '通过 Base URL + API Key 连接上游',
|
||||
api_key: 'API Key',
|
||||
@@ -1739,6 +1745,12 @@ export default {
|
||||
sessionIdMasking: {
|
||||
label: '会话 ID 伪装',
|
||||
hint: '启用后将在 15 分钟内固定 metadata.user_id 中的 session ID,使上游认为请求来自同一会话'
|
||||
},
|
||||
cacheTTLOverride: {
|
||||
label: '缓存 TTL 强制替换',
|
||||
hint: '将所有缓存创建 token 强制按指定的 TTL 类型(5分钟或1小时)计费',
|
||||
target: '目标 TTL',
|
||||
targetHint: '选择计费使用的 TTL 类型'
|
||||
}
|
||||
},
|
||||
expired: '已过期',
|
||||
@@ -1771,7 +1783,7 @@ export default {
|
||||
// Upstream type
|
||||
upstream: {
|
||||
baseUrl: '上游 Base URL',
|
||||
baseUrlHint: '上游 Antigravity 服务的地址,例如:https://s.konstants.xyz',
|
||||
baseUrlHint: '上游 Antigravity 服务的地址,例如:https://cloudcode-pa.googleapis.com',
|
||||
apiKey: '上游 API Key',
|
||||
apiKeyHint: '上游服务的 API Key',
|
||||
pleaseEnterBaseUrl: '请输入上游 Base URL',
|
||||
@@ -1912,7 +1924,15 @@ export default {
|
||||
authCodeHint: '您可以直接复制整个链接或仅复制 code 参数值,系统会自动识别',
|
||||
failedToGenerateUrl: '生成 Antigravity 授权链接失败',
|
||||
missingExchangeParams: '缺少 code / session_id / state',
|
||||
failedToExchangeCode: 'Antigravity 授权码兑换失败'
|
||||
failedToExchangeCode: 'Antigravity 授权码兑换失败',
|
||||
// Refresh Token auth
|
||||
refreshTokenAuth: '手动输入 RT',
|
||||
refreshTokenDesc: '输入您已有的 Antigravity Refresh Token,支持批量输入(每行一个),系统将自动验证并创建账号。',
|
||||
refreshTokenPlaceholder: '粘贴您的 Antigravity Refresh Token...\n支持多个,每行一个',
|
||||
validating: '验证中...',
|
||||
validateAndCreate: '验证并创建账号',
|
||||
pleaseEnterRefreshToken: '请输入 Refresh Token',
|
||||
failedToValidateRT: '验证 Refresh Token 失败'
|
||||
}
|
||||
},
|
||||
// Gemini specific (platform-wide)
|
||||
@@ -2291,7 +2311,7 @@ export default {
|
||||
allStatus: '全部状态',
|
||||
unused: '未使用',
|
||||
used: '已使用',
|
||||
searchCodes: '搜索兑换码...',
|
||||
searchCodes: '搜索兑换码或邮箱...',
|
||||
exportCsv: '导出 CSV',
|
||||
deleteAllUnused: '删除全部未使用',
|
||||
deleteCodeConfirm: '确定要删除此兑换码吗?此操作无法撤销。',
|
||||
@@ -2517,6 +2537,8 @@ export default {
|
||||
inputTokens: '输入 Token',
|
||||
outputTokens: '输出 Token',
|
||||
cacheCreationTokens: '缓存创建 Token',
|
||||
cacheCreation5mTokens: '缓存创建',
|
||||
cacheCreation1hTokens: '缓存创建',
|
||||
cacheReadTokens: '缓存读取 Token',
|
||||
failedToLoad: '加载使用记录失败',
|
||||
billingType: '计费类型',
|
||||
@@ -3526,6 +3548,7 @@ export default {
|
||||
custom: '自定义',
|
||||
code: '状态码',
|
||||
body: '消息体',
|
||||
skipMonitoring: '跳过监控',
|
||||
|
||||
// Columns
|
||||
columns: {
|
||||
@@ -3570,6 +3593,8 @@ export default {
|
||||
passthroughBody: '透传上游错误信息',
|
||||
customMessage: '自定义错误信息',
|
||||
customMessagePlaceholder: '返回给客户端的错误信息...',
|
||||
skipMonitoring: '跳过运维监控记录',
|
||||
skipMonitoringHint: '开启后,匹配此规则的错误不会被记录到运维监控中',
|
||||
enabled: '启用此规则'
|
||||
},
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
@apply text-2xl font-bold text-gray-900 dark:text-white;
|
||||
@apply text-2xl font-bold text-gray-900 dark:text-white truncate;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
|
||||
@@ -614,6 +614,10 @@ export interface Account {
|
||||
// 启用后将在15分钟内固定 metadata.user_id 中的 session ID
|
||||
session_id_masking_enabled?: boolean | null
|
||||
|
||||
// 缓存 TTL 强制替换(仅 Anthropic OAuth/SetupToken 账号有效)
|
||||
cache_ttl_override_enabled?: boolean | null
|
||||
cache_ttl_override_target?: string | null
|
||||
|
||||
// 运行时状态(仅当启用对应限制时返回)
|
||||
current_window_cost?: number | null // 当前窗口费用
|
||||
active_sessions?: number | null // 当前活跃会话数
|
||||
@@ -827,6 +831,9 @@ export interface UsageLog {
|
||||
// User-Agent
|
||||
user_agent: string | null
|
||||
|
||||
// Cache TTL Override
|
||||
cache_ttl_overridden: boolean
|
||||
|
||||
created_at: string
|
||||
|
||||
user?: User
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
type SanitizeOptions = {
|
||||
allowRelative?: boolean
|
||||
allowDataUrl?: boolean
|
||||
}
|
||||
|
||||
export function sanitizeUrl(value: string, options: SanitizeOptions = {}): string {
|
||||
@@ -18,6 +19,11 @@ export function sanitizeUrl(value: string, options: SanitizeOptions = {}): strin
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// 允许 data:image/ 开头的 data URL(仅限图片类型)
|
||||
if (options.allowDataUrl && trimmed.startsWith('data:image/')) {
|
||||
return trimmed
|
||||
}
|
||||
|
||||
// 只接受绝对 URL,不使用 base URL 来避免相对路径被解析为当前域名
|
||||
// 检查是否以 http:// 或 https:// 开头
|
||||
if (!trimmed.match(/^https?:\/\//i)) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<AccountTableFilters
|
||||
v-model:searchQuery="params.search"
|
||||
:filters="params"
|
||||
:groups="groups"
|
||||
@update:filters="(newFilters) => Object.assign(params, newFilters)"
|
||||
@change="debouncedReload"
|
||||
@update:searchQuery="debouncedReload"
|
||||
@@ -439,7 +440,7 @@ const isColumnVisible = (key: string) => !hiddenColumns.has(key)
|
||||
|
||||
const { items: accounts, loading, params, pagination, load, reload, debouncedReload, handlePageChange, handlePageSizeChange } = useTableLoader<Account, any>({
|
||||
fetchFn: adminAPI.accounts.list,
|
||||
initialParams: { platform: '', type: '', status: '', search: '' }
|
||||
initialParams: { platform: '', type: '', status: '', group: '', search: '' }
|
||||
})
|
||||
|
||||
const isAnyModalOpen = computed(() => {
|
||||
|
||||
@@ -117,9 +117,9 @@
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #cell-used_by="{ value }">
|
||||
<template #cell-used_by="{ value, row }">
|
||||
<span class="text-sm text-gray-500 dark:text-dark-400">
|
||||
{{ value ? t('admin.redeem.userPrefix', { id: value }) : '-' }}
|
||||
{{ row.user?.email || (value ? t('admin.redeem.userPrefix', { id: value }) : '-') }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -233,6 +233,8 @@
|
||||
<span class="font-medium text-amber-600 dark:text-amber-400">{{
|
||||
formatCacheTokens(row.cache_creation_tokens)
|
||||
}}</span>
|
||||
<span v-if="row.cache_creation_1h_tokens > 0" class="inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-orange-100 text-orange-600 ring-1 ring-inset ring-orange-200 dark:bg-orange-500/20 dark:text-orange-400 dark:ring-orange-500/30">1h</span>
|
||||
<span v-if="row.cache_ttl_overridden" :title="t('usage.cacheTtlOverriddenHint')" class="inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-rose-100 text-rose-600 ring-1 ring-inset ring-rose-200 dark:bg-rose-500/20 dark:text-rose-400 dark:ring-rose-500/30 cursor-help">R</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -350,9 +352,36 @@
|
||||
<span class="text-gray-400">{{ t('admin.usage.outputTokens') }}</span>
|
||||
<span class="font-medium text-white">{{ tokenTooltipData.output_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div v-if="tokenTooltipData && tokenTooltipData.cache_creation_tokens > 0" class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400">{{ t('admin.usage.cacheCreationTokens') }}</span>
|
||||
<span class="font-medium text-white">{{ tokenTooltipData.cache_creation_tokens.toLocaleString() }}</span>
|
||||
<div v-if="tokenTooltipData && tokenTooltipData.cache_creation_tokens > 0">
|
||||
<!-- 有 5m/1h 明细时,展开显示 -->
|
||||
<template v-if="tokenTooltipData.cache_creation_5m_tokens > 0 || tokenTooltipData.cache_creation_1h_tokens > 0">
|
||||
<div v-if="tokenTooltipData.cache_creation_5m_tokens > 0" class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400 flex items-center gap-1.5">
|
||||
{{ t('admin.usage.cacheCreation5mTokens') }}
|
||||
<span class="inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-amber-500/20 text-amber-400 ring-1 ring-inset ring-amber-500/30">5m</span>
|
||||
</span>
|
||||
<span class="font-medium text-white">{{ tokenTooltipData.cache_creation_5m_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
<div v-if="tokenTooltipData.cache_creation_1h_tokens > 0" class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400 flex items-center gap-1.5">
|
||||
{{ t('admin.usage.cacheCreation1hTokens') }}
|
||||
<span class="inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-orange-500/20 text-orange-400 ring-1 ring-inset ring-orange-500/30">1h</span>
|
||||
</span>
|
||||
<span class="font-medium text-white">{{ tokenTooltipData.cache_creation_1h_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 无明细时,只显示聚合值 -->
|
||||
<div v-else class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400">{{ t('admin.usage.cacheCreationTokens') }}</span>
|
||||
<span class="font-medium text-white">{{ tokenTooltipData.cache_creation_tokens.toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tokenTooltipData && tokenTooltipData.cache_ttl_overridden" class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400 flex items-center gap-1.5">
|
||||
{{ t('usage.cacheTtlOverriddenLabel') }}
|
||||
<span class="inline-flex items-center rounded px-1 py-px text-[10px] font-medium leading-tight bg-rose-500/20 text-rose-400 ring-1 ring-inset ring-rose-500/30">R-{{ tokenTooltipData.cache_creation_1h_tokens > 0 ? '5m' : '1H' }}</span>
|
||||
</span>
|
||||
<span class="font-medium text-rose-400">{{ tokenTooltipData.cache_creation_1h_tokens > 0 ? t('usage.cacheTtlOverridden1h') : t('usage.cacheTtlOverridden5m') }}</span>
|
||||
</div>
|
||||
<div v-if="tokenTooltipData && tokenTooltipData.cache_read_tokens > 0" class="flex items-center justify-between gap-4">
|
||||
<span class="text-gray-400">{{ t('admin.usage.cacheReadTokens') }}</span>
|
||||
|
||||
Reference in New Issue
Block a user