mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-21 05:21:20 +00:00
Merge pull request #5666 from Randark-JMT/feat/cn-providers-kimi-zhipu-deepseek
feat: 国产供应商多协议支持(Kimi/Zhipu/DeepSeek 原生 Anthropic 直通 + DeepSeek Responses)与配额/余额监控
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Admin CN providers (Kimi / Zhipu / DeepSeek) API endpoints.
|
||||
* Coding-plan rolling-window quota probe + payg balance probe.
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client'
|
||||
|
||||
/** 滚动用量窗口档(5 小时 / 每周),对齐后端 service.CNQuotaTier。 */
|
||||
export interface CNQuotaTier {
|
||||
window: '5h' | 'weekly'
|
||||
used_percent: number
|
||||
reset_at?: string
|
||||
}
|
||||
|
||||
/** Coding Plan 额度探测结果(kimi / zhipu),对齐后端 CNProviderQuotaProbeResult。 */
|
||||
export interface CNProviderQuotaProbeResult {
|
||||
provider: string
|
||||
source?: string
|
||||
success: boolean
|
||||
credential_valid: boolean
|
||||
tiers?: CNQuotaTier[]
|
||||
plan_level?: string
|
||||
status_code?: number
|
||||
fetched_at: number
|
||||
persisted: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
/** 单币种余额明细(deepseek 双币种账号含 CNY + USD 两条)。 */
|
||||
export interface CNProviderBalanceEntry {
|
||||
currency: string
|
||||
balance: number
|
||||
}
|
||||
|
||||
/** payg 余额探测结果(kimi / deepseek),对齐后端 CNProviderBalanceResult。 */
|
||||
export interface CNProviderBalanceResult {
|
||||
provider: string
|
||||
success: boolean
|
||||
/** 主币种余额(balances 首条,兼容单币种展示)。 */
|
||||
balance: number
|
||||
currency?: string
|
||||
/** 多币种明细;缺省时按主币种展示。 */
|
||||
balances?: CNProviderBalanceEntry[]
|
||||
available: boolean
|
||||
status_code?: number
|
||||
fetched_at: number
|
||||
persisted: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
/** 查询 Coding Plan 滚动窗口用量(5h + weekly)。 */
|
||||
export async function queryQuota(id: number): Promise<CNProviderQuotaProbeResult> {
|
||||
const { data } = await apiClient.get<CNProviderQuotaProbeResult>(
|
||||
`/admin/cn-providers/accounts/${id}/quota`
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
/** 查询 payg 账号余额。 */
|
||||
export async function queryBalance(id: number): Promise<CNProviderBalanceResult> {
|
||||
const { data } = await apiClient.get<CNProviderBalanceResult>(
|
||||
`/admin/cn-providers/accounts/${id}/balance`
|
||||
)
|
||||
return data
|
||||
}
|
||||
|
||||
export default {
|
||||
queryQuota,
|
||||
queryBalance
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import usageAPI from './usage'
|
||||
import geminiAPI from './gemini'
|
||||
import antigravityAPI from './antigravity'
|
||||
import grokAPI from './grok'
|
||||
import cnProvidersAPI from './cnProviders'
|
||||
import userAttributesAPI from './userAttributes'
|
||||
import opsAPI from './ops'
|
||||
import errorPassthroughAPI from './errorPassthrough'
|
||||
@@ -54,6 +55,7 @@ export const adminAPI = {
|
||||
gemini: geminiAPI,
|
||||
antigravity: antigravityAPI,
|
||||
grok: grokAPI,
|
||||
cnProviders: cnProvidersAPI,
|
||||
userAttributes: userAttributesAPI,
|
||||
ops: opsAPI,
|
||||
errorPassthrough: errorPassthroughAPI,
|
||||
@@ -88,6 +90,7 @@ export {
|
||||
geminiAPI,
|
||||
antigravityAPI,
|
||||
grokAPI,
|
||||
cnProvidersAPI,
|
||||
userAttributesAPI,
|
||||
opsAPI,
|
||||
errorPassthroughAPI,
|
||||
|
||||
@@ -36,13 +36,19 @@ export type SchedulingThresholdPlatformType =
|
||||
| "openai"
|
||||
| "anthropic"
|
||||
| "grok"
|
||||
| "kimi"
|
||||
| "zhipu"
|
||||
|
||||
export type AccountSchedulingThresholdsMap = Record<SchedulingThresholdPlatformType, number>
|
||||
|
||||
// 与后端 AllowedSchedulingThresholdPlatforms 保持一致(deepseek 为余额型,
|
||||
// 走余额检测而非用量阈值)。
|
||||
export const SCHEDULING_THRESHOLD_PLATFORMS: SchedulingThresholdPlatformType[] = [
|
||||
"openai",
|
||||
"anthropic",
|
||||
"grok",
|
||||
"kimi",
|
||||
"zhipu",
|
||||
]
|
||||
|
||||
export function normalizeAccountSchedulingThresholdsMap(
|
||||
|
||||
@@ -422,6 +422,21 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- CN providers (Kimi / Zhipu / DeepSeek): coding-plan quota or payg balance -->
|
||||
<template v-else-if="account.platform === 'kimi' || account.platform === 'zhipu' || account.platform === 'deepseek'">
|
||||
<div class="space-y-1">
|
||||
<!-- 子单元格各自按 模式×平台 判定可见;两者都不可见时(智谱 payg 无公开
|
||||
余额端点、coding 探测也不适用)才回落到占位符。 -->
|
||||
<div
|
||||
v-if="!cnQuotaCellVisible && !cnBalanceCellVisible"
|
||||
class="text-xs text-gray-400"
|
||||
:title="t('admin.accounts.cnProviders.noBalanceEndpoint')"
|
||||
>-</div>
|
||||
<CNProviderQuotaCell :account="account" />
|
||||
<CNProviderBalanceCell :account="account" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Gemini platform: show quota + local usage window -->
|
||||
<template v-else-if="account.platform === 'gemini'">
|
||||
<!-- Auth Type + Tier Badge (first line) -->
|
||||
@@ -625,7 +640,10 @@ import UsageProgressBar from './UsageProgressBar.vue'
|
||||
import AccountQuotaInfo from './AccountQuotaInfo.vue'
|
||||
import OpenAIQuotaResetCell from './OpenAIQuotaResetCell.vue'
|
||||
import GrokQuotaProbeCell from './GrokQuotaProbeCell.vue'
|
||||
import CNProviderQuotaCell from './CNProviderQuotaCell.vue'
|
||||
import CNProviderBalanceCell from './CNProviderBalanceCell.vue'
|
||||
import OllamaCloudUsageCell from './OllamaCloudUsageCell.vue'
|
||||
import { cnQuotaCellVisible as cnQuotaCellVisibleFn, cnBalanceCellVisible as cnBalanceCellVisibleFn } from './credentialsBuilder'
|
||||
|
||||
// Module-level cache shared across all AccountUsageCell instances
|
||||
const _usageCache = new Map<number, { data: AccountUsageInfo; ts: number }>()
|
||||
@@ -690,6 +708,15 @@ let visibilityObserver: IntersectionObserver | null = null
|
||||
const showUsageWindows = computed(() => {
|
||||
// Gemini: we can always compute local usage windows from DB logs (simulated quotas).
|
||||
if (props.account.platform === 'gemini') return true
|
||||
// CN providers: apikey 账号也有滚动用量窗口(coding plan)或余额(payg),
|
||||
// 由 CNProviderQuotaCell / CNProviderBalanceCell 自行探测与展示。
|
||||
if (
|
||||
props.account.platform === 'kimi' ||
|
||||
props.account.platform === 'zhipu' ||
|
||||
props.account.platform === 'deepseek'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return props.account.type === 'oauth' || props.account.type === 'setup-token'
|
||||
})
|
||||
|
||||
@@ -712,6 +739,15 @@ const shouldFetchUsage = computed(() => {
|
||||
return false
|
||||
})
|
||||
|
||||
// CN 供应商子单元格可见性(与 CNProviderQuotaCell / CNProviderBalanceCell 共用
|
||||
// credentialsBuilder 的单一实现):都不可见时显示 `-` 占位符。
|
||||
const cnAccountMode = computed(() => {
|
||||
const mode = props.account.credentials?.account_mode
|
||||
return typeof mode === 'string' ? mode : ''
|
||||
})
|
||||
const cnQuotaCellVisible = computed(() => cnQuotaCellVisibleFn(props.account.platform, cnAccountMode.value))
|
||||
const cnBalanceCellVisible = computed(() => cnBalanceCellVisibleFn(props.account.platform, cnAccountMode.value))
|
||||
|
||||
const isBatchManaged = computed(() => typeof props.requestBatchedUsage === 'function')
|
||||
|
||||
const showGeminiTodayStats = computed(() => {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div v-if="visible" class="space-y-1">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-dark-600',
|
||||
platformTextClass(account.platform)
|
||||
]"
|
||||
:disabled="loading"
|
||||
@click="handleProbe"
|
||||
>
|
||||
<svg
|
||||
class="h-2.5 w-2.5"
|
||||
:class="{ 'animate-spin': loading }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
{{ balanceLabel }}
|
||||
</button>
|
||||
|
||||
<!-- Low balance badge (reactive 402/429 marker or probe-detected) -->
|
||||
<span
|
||||
v-if="balanceLow"
|
||||
class="inline-flex items-center rounded bg-red-100 px-1 py-0.5 text-[10px] font-medium text-red-700 dark:bg-red-900/30 dark:text-red-300"
|
||||
>
|
||||
{{ t('admin.accounts.cnProviders.balanceLow') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="truncate text-[10px] text-red-600 dark:text-red-400" :title="error">
|
||||
{{ truncatedError }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { CNProviderBalanceEntry, CNProviderBalanceResult } from '@/api/admin/cnProviders'
|
||||
import type { Account } from '@/types'
|
||||
import { platformTextClass } from '@/utils/platformColors'
|
||||
import { cnBalanceCellVisible } from './credentialsBuilder'
|
||||
|
||||
const props = defineProps<{
|
||||
account: Account
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const readMode = (): string => {
|
||||
const mode = props.account.credentials?.account_mode
|
||||
return typeof mode === 'string' ? mode : ''
|
||||
}
|
||||
|
||||
// 仅 kimi / deepseek payg 账号有公开余额端点(智谱 payg 无)。
|
||||
const visible = computed(() => cnBalanceCellVisible(props.account.platform, readMode()))
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const data = ref<CNProviderBalanceResult | null>(null)
|
||||
|
||||
const extraKey = (suffix: string) => `${props.account.platform}_${suffix}`
|
||||
|
||||
// 落库快照(后端周期探测/响应式写入 account.Extra)。
|
||||
const snapshotBalance = computed(() => {
|
||||
const v = props.account.extra?.[extraKey('balance')]
|
||||
return typeof v === 'number' ? v : null
|
||||
})
|
||||
const snapshotCurrency = computed(() => {
|
||||
const v = props.account.extra?.[extraKey('balance_currency')]
|
||||
return typeof v === 'string' ? v : ''
|
||||
})
|
||||
// 多币种快照(后端写 <platform>_balances:[{currency, balance}],deepseek CNY+USD)。
|
||||
const snapshotBalances = computed<CNProviderBalanceEntry[]>(() => {
|
||||
const v = props.account.extra?.[extraKey('balances')]
|
||||
if (!Array.isArray(v)) return []
|
||||
return v.flatMap((item): CNProviderBalanceEntry[] => {
|
||||
if (!item || typeof item !== 'object') return []
|
||||
const { currency, balance } = item as Record<string, unknown>
|
||||
if (typeof currency !== 'string' || typeof balance !== 'number') return []
|
||||
return [{ currency, balance }]
|
||||
})
|
||||
})
|
||||
const balanceLow = computed(() => props.account.extra?.[extraKey('balance_low')] === true)
|
||||
|
||||
// 优先用探测结果,其次落库快照。多币种返回全部明细,否则主币种单条。
|
||||
const currentEntries = computed<CNProviderBalanceEntry[]>(() => {
|
||||
if (data.value && data.value.success) {
|
||||
if (data.value.balances && data.value.balances.length > 0) return data.value.balances
|
||||
return [{ currency: data.value.currency || '', balance: data.value.balance }]
|
||||
}
|
||||
if (snapshotBalances.value.length > 0) return snapshotBalances.value
|
||||
if (snapshotBalance.value != null) {
|
||||
return [{ currency: snapshotCurrency.value, balance: snapshotBalance.value }]
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const formatEntry = (entry: CNProviderBalanceEntry): string => {
|
||||
const fixed = entry.balance >= 100 ? entry.balance.toFixed(0) : entry.balance.toFixed(2)
|
||||
return `${entry.currency || '¥'} ${fixed}`
|
||||
}
|
||||
|
||||
const balanceLabel = computed(() => {
|
||||
if (currentEntries.value.length === 0) {
|
||||
return t('admin.accounts.grokBalance')?.trim() || 'Balance'
|
||||
}
|
||||
return currentEntries.value.map(formatEntry).join(' · ')
|
||||
})
|
||||
|
||||
const extractErrorMessage = (e: unknown): string => {
|
||||
const err = e as {
|
||||
message?: string
|
||||
reason?: string
|
||||
response?: { data?: { message?: string; error?: string } }
|
||||
}
|
||||
return (
|
||||
err?.message ||
|
||||
err?.reason ||
|
||||
err?.response?.data?.message ||
|
||||
err?.response?.data?.error ||
|
||||
t('common.error')
|
||||
)
|
||||
}
|
||||
|
||||
const truncatedError = computed(() => {
|
||||
if (!error.value) return ''
|
||||
return error.value.length > 80 ? `${error.value.slice(0, 80)}...` : error.value
|
||||
})
|
||||
|
||||
const handleProbe = async () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const result = await adminAPI.cnProviders.queryBalance(props.account.id)
|
||||
// 失败时保留快照展示(仅显示错误行),成功才覆盖。
|
||||
if (result.success) {
|
||||
data.value = result
|
||||
} else {
|
||||
error.value = result.error || t('common.error')
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = extractErrorMessage(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.account.id,
|
||||
() => {
|
||||
data.value = null
|
||||
error.value = null
|
||||
loading.value = false
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<div v-if="visible" class="space-y-1">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'inline-flex items-center gap-0.5 rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-dark-600',
|
||||
platformTextClass(account.platform)
|
||||
]"
|
||||
:disabled="loading"
|
||||
:title="t('admin.accounts.cnProviders.probeTooltip')"
|
||||
@click="handleProbe()"
|
||||
>
|
||||
<svg
|
||||
class="h-2.5 w-2.5"
|
||||
:class="{ 'animate-spin': loading }"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
{{ t('admin.accounts.cnProviders.window5h') }}/{{ t('admin.accounts.cnProviders.windowWeekly') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tier rows: 5h + weekly utilization bars -->
|
||||
<div v-if="data?.success && data.tiers?.length" class="space-y-1">
|
||||
<div v-for="tier in data.tiers" :key="tier.window" class="flex items-center gap-1.5 text-[10px]">
|
||||
<span class="w-10 shrink-0 text-gray-500 dark:text-gray-400">{{ windowLabel(tier.window) }}</span>
|
||||
<div class="h-1.5 w-16 shrink-0 overflow-hidden rounded-full bg-gray-200 dark:bg-dark-600">
|
||||
<div
|
||||
class="h-full rounded-full transition-all"
|
||||
:class="utilizationColor(tier.used_percent)"
|
||||
:style="{ width: `${Math.min(100, Math.max(0, tier.used_percent))}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span :class="['shrink-0 font-medium', utilizationTextColor(tier.used_percent)]">
|
||||
{{ Math.round(tier.used_percent) }}%
|
||||
</span>
|
||||
<span v-if="tier.reset_at" class="truncate text-gray-400 dark:text-gray-500" :title="tier.reset_at">
|
||||
· {{ formatReset(tier.reset_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="truncate text-[10px] text-red-600 dark:text-red-400" :title="error">
|
||||
{{ truncatedError }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { CNProviderQuotaProbeResult } from '@/api/admin/cnProviders'
|
||||
import type { Account } from '@/types'
|
||||
import { platformTextClass } from '@/utils/platformColors'
|
||||
import { cnQuotaCellVisible } from './credentialsBuilder'
|
||||
|
||||
const props = defineProps<{
|
||||
account: Account
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const readMode = (): string => {
|
||||
const mode = props.account.credentials?.account_mode
|
||||
return typeof mode === 'string' ? mode : ''
|
||||
}
|
||||
|
||||
const visible = computed(() => cnQuotaCellVisible(props.account.platform, readMode()))
|
||||
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const data = ref<CNProviderQuotaProbeResult | null>(null)
|
||||
|
||||
// 后端周期任务/手动探测写入的 extra 快照键(<provider>_ 前缀,与后端
|
||||
// cnQuotaExtraUpdates 对齐)。页面加载即有数据,无需等待探测。
|
||||
const SNAPSHOT_STALE_MS = 15 * 60 * 1000
|
||||
|
||||
// 自动探测去抖窗口与最近一次自动探测时间(模块级,跨实例共享)。
|
||||
const AUTO_PROBE_DEBOUNCE_MS = 5 * 60 * 1000
|
||||
const lastAutoProbeAt = new Map<number, number>()
|
||||
|
||||
const readExtraNumber = (key: string): number | null => {
|
||||
const v = (props.account.extra as Record<string, unknown> | undefined)?.[key]
|
||||
return typeof v === 'number' && Number.isFinite(v) ? v : null
|
||||
}
|
||||
|
||||
const readExtraString = (key: string): string => {
|
||||
const v = (props.account.extra as Record<string, unknown> | undefined)?.[key]
|
||||
return typeof v === 'string' ? v : ''
|
||||
}
|
||||
|
||||
// 从持久化快照构造展示数据(缺少 5h/weekly 两档键时返回 null)。
|
||||
const snapshotData = computed<CNProviderQuotaProbeResult | null>(() => {
|
||||
const platform = props.account.platform
|
||||
const used5h = readExtraNumber(`${platform}_5h_used_percent`)
|
||||
const usedWeekly = readExtraNumber(`${platform}_weekly_used_percent`)
|
||||
if (used5h == null && usedWeekly == null) return null
|
||||
const tiers: CNProviderQuotaProbeResult['tiers'] = []
|
||||
if (used5h != null) {
|
||||
tiers.push({ window: '5h', used_percent: used5h, reset_at: readExtraString(`${platform}_5h_reset_at`) || undefined })
|
||||
}
|
||||
if (usedWeekly != null) {
|
||||
tiers.push({ window: 'weekly', used_percent: usedWeekly, reset_at: readExtraString(`${platform}_weekly_reset_at`) || undefined })
|
||||
}
|
||||
return { success: true, tiers } as CNProviderQuotaProbeResult
|
||||
})
|
||||
|
||||
// 快照是否过期(无更新时间或超过 staleness 窗口)→ 挂载时需要自动探测。
|
||||
const snapshotIsStale = computed(() => {
|
||||
const updatedAt = readExtraString(`${props.account.platform}_usage_updated_at`)
|
||||
if (!updatedAt) return true
|
||||
const ts = new Date(updatedAt).getTime()
|
||||
return Number.isNaN(ts) || Date.now() - ts > SNAPSHOT_STALE_MS
|
||||
})
|
||||
|
||||
// 挂载时:先用持久化快照渲染;快照缺失或过期再自动探测一次(失败显示错误,
|
||||
// 避免静默失败导致单元格空白无提示)。
|
||||
onMounted(() => {
|
||||
if (!visible.value) return
|
||||
data.value = snapshotData.value
|
||||
if (!snapshotIsStale.value) return
|
||||
// 模块级去抖:列表页每行一个实例,翻页/筛选/刷新会重复挂载;同一账号
|
||||
// 短时间内已自动探测过则跳过,避免对上游形成探测风暴。
|
||||
const last = lastAutoProbeAt.get(props.account.id) ?? 0
|
||||
if (Date.now() - last < AUTO_PROBE_DEBOUNCE_MS) return
|
||||
lastAutoProbeAt.set(props.account.id, Date.now())
|
||||
handleProbe()
|
||||
})
|
||||
|
||||
const extractErrorMessage = (e: unknown): string => {
|
||||
const err = e as {
|
||||
message?: string
|
||||
reason?: string
|
||||
response?: { data?: { message?: string; error?: string } }
|
||||
}
|
||||
return (
|
||||
err?.message ||
|
||||
err?.reason ||
|
||||
err?.response?.data?.message ||
|
||||
err?.response?.data?.error ||
|
||||
t('common.error')
|
||||
)
|
||||
}
|
||||
|
||||
const truncatedError = computed(() => {
|
||||
if (!error.value) return ''
|
||||
return error.value.length > 80 ? `${error.value.slice(0, 80)}...` : error.value
|
||||
})
|
||||
|
||||
const windowLabel = (window: string) =>
|
||||
window === 'weekly'
|
||||
? t('admin.accounts.cnProviders.windowWeekly')
|
||||
: t('admin.accounts.cnProviders.window5h')
|
||||
|
||||
const utilizationColor = (pct: number) => {
|
||||
if (pct >= 90) return 'bg-red-500'
|
||||
if (pct >= 75) return 'bg-amber-500'
|
||||
return 'bg-emerald-500'
|
||||
}
|
||||
|
||||
const utilizationTextColor = (pct: number) => {
|
||||
if (pct >= 90) return 'text-red-600 dark:text-red-400'
|
||||
if (pct >= 75) return 'text-amber-600 dark:text-amber-400'
|
||||
return 'text-emerald-600 dark:text-emerald-400'
|
||||
}
|
||||
|
||||
// 重置时间相对/绝对简短显示。
|
||||
const formatReset = (iso: string) => {
|
||||
const d = new Date(iso)
|
||||
if (isNaN(d.getTime())) return iso
|
||||
const now = Date.now()
|
||||
const diffMs = d.getTime() - now
|
||||
if (diffMs <= 0) return t('admin.accounts.cnProviders.resetSoon')
|
||||
if (diffMs < 3_600_000) return `${Math.max(1, Math.round(diffMs / 60_000))}m`
|
||||
const hours = Math.round(diffMs / 3_600_000)
|
||||
if (hours < 48) return `${hours}h`
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(d.getDate()).padStart(2, '0')
|
||||
return `${mm}-${dd}`
|
||||
}
|
||||
|
||||
const handleProbe = async () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const result = await adminAPI.cnProviders.queryQuota(props.account.id)
|
||||
// 失败时保留已渲染的快照条形图(仅显示错误行),成功才覆盖。
|
||||
if (result.success) {
|
||||
data.value = result
|
||||
} else {
|
||||
error.value = result.error || t('common.error')
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = extractErrorMessage(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.account.id,
|
||||
() => {
|
||||
data.value = null
|
||||
error.value = null
|
||||
loading.value = false
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="preset in presets"
|
||||
:key="preset.mode + ':' + preset.protocol + ':' + preset.url"
|
||||
type="button"
|
||||
data-testid="cn-base-url-preset"
|
||||
:class="[
|
||||
'rounded-lg px-3 py-1 text-xs transition-colors',
|
||||
isActive(preset)
|
||||
? 'bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-primary-50 hover:text-primary-700 dark:bg-dark-600 dark:text-gray-300 dark:hover:bg-primary-900/30 dark:hover:text-primary-400'
|
||||
]"
|
||||
@click="emit('select', preset)"
|
||||
>
|
||||
{{ preset.label }} ({{ displayUrl(preset.url) }})
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { CN_BASE_URL_PRESETS, type CnBaseUrlPreset } from './credentialsBuilder'
|
||||
|
||||
// 国产供应商快捷端点:点击把预设地址(及对应账号类型/协议)回填到调用方。
|
||||
// 与 Grok 预设一致,仅作快速填充,输入框仍接受任意第三方转发地址。
|
||||
// 传入 protocol 时只显示该协议档的预设(协议 × 账号类型正交分档)。
|
||||
const props = defineProps<{
|
||||
platform: 'kimi' | 'zhipu' | 'deepseek'
|
||||
/** 当前已选账号类型,用于过滤和高亮匹配的预设 */
|
||||
mode?: 'payg' | 'coding'
|
||||
/** 当前已选 API 协议,用于过滤和高亮匹配的预设 */
|
||||
protocol?: 'chat_completions' | 'anthropic' | 'responses'
|
||||
/** 当前输入框中的 base url,用于高亮完全匹配项 */
|
||||
currentUrl?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', preset: CnBaseUrlPreset): void
|
||||
}>()
|
||||
|
||||
const presets = computed(() => {
|
||||
const all = CN_BASE_URL_PRESETS[props.platform] ?? []
|
||||
// 只按协议过滤:同协议下 payg/coding 两档都展示,点击即同时切换账号类型。
|
||||
if (props.protocol == null) return all
|
||||
return all.filter(p => p.protocol === props.protocol)
|
||||
})
|
||||
|
||||
const isActive = (preset: CnBaseUrlPreset) =>
|
||||
(props.mode != null && preset.mode === props.mode) || preset.url === props.currentUrl
|
||||
|
||||
const displayUrl = (url: string) => url.replace(/^https?:\/\//i, '')
|
||||
</script>
|
||||
@@ -161,6 +161,48 @@
|
||||
Grok
|
||||
</button>
|
||||
</div>
|
||||
<!-- CN providers row: Kimi / Zhipu GLM / DeepSeek -->
|
||||
<div class="mt-2 flex flex-wrap rounded-lg bg-gray-100 p-1 dark:bg-dark-700">
|
||||
<button
|
||||
type="button"
|
||||
@click="selectCNPlatform('kimi')"
|
||||
:class="[
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
|
||||
form.platform === 'kimi'
|
||||
? 'bg-white text-pink-600 shadow-sm dark:bg-dark-600 dark:text-pink-400'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
||||
]"
|
||||
>
|
||||
<PlatformIcon platform="kimi" size="sm" />
|
||||
Kimi
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="selectCNPlatform('zhipu')"
|
||||
:class="[
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
|
||||
form.platform === 'zhipu'
|
||||
? 'bg-white text-indigo-600 shadow-sm dark:bg-dark-600 dark:text-indigo-400'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
||||
]"
|
||||
>
|
||||
<PlatformIcon platform="zhipu" size="sm" />
|
||||
Zhipu GLM
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="selectCNPlatform('deepseek')"
|
||||
:class="[
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
|
||||
form.platform === 'deepseek'
|
||||
? 'bg-white text-teal-600 shadow-sm dark:bg-dark-600 dark:text-teal-400'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
||||
]"
|
||||
>
|
||||
<PlatformIcon platform="deepseek" size="sm" />
|
||||
DeepSeek
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Type Selection (Anthropic) -->
|
||||
@@ -411,6 +453,100 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Mode Selection (Kimi / Zhipu / DeepSeek) -->
|
||||
<div v-if="isCNPlatform">
|
||||
<label class="input-label">{{ t('admin.accounts.cnProviders.accountMode.title') }}</label>
|
||||
<div class="mt-2 grid grid-cols-1 gap-3 sm:grid-cols-2" data-tour="account-form-mode">
|
||||
<!-- Pay-as-you-go (token balance) -->
|
||||
<button
|
||||
type="button"
|
||||
@click="accountMode = 'payg'"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
accountMode === 'payg'
|
||||
? cnAccentActiveClass
|
||||
: 'border-gray-200 hover:border-gray-400 dark:border-dark-600 dark:hover:border-gray-600'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
|
||||
accountMode === 'payg'
|
||||
? cnAccentIconClass
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
|
||||
]"
|
||||
>
|
||||
<Icon name="creditCard" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{ t('admin.accounts.cnProviders.accountMode.payg') }}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.cnProviders.accountMode.paygDesc') }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<!-- Coding Plan (kimi / zhipu only — DeepSeek has no coding plan) -->
|
||||
<button
|
||||
v-if="form.platform !== 'deepseek'"
|
||||
type="button"
|
||||
@click="accountMode = 'coding'"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
accountMode === 'coding'
|
||||
? cnAccentActiveClass
|
||||
: 'border-gray-200 hover:border-gray-400 dark:border-dark-600 dark:hover:border-gray-600'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
|
||||
accountMode === 'coding'
|
||||
? cnAccentIconClass
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
|
||||
]"
|
||||
>
|
||||
<Icon name="bolt" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{ t('admin.accounts.cnProviders.accountMode.coding') }}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.cnProviders.accountMode.codingDesc') }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Protocol Selection (Kimi / Zhipu / DeepSeek) -->
|
||||
<div v-if="isCNPlatform" class="mt-4">
|
||||
<label class="input-label">{{ t('admin.accounts.cnProviders.apiProtocol.title') }}</label>
|
||||
<div class="mt-2 grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<button
|
||||
v-for="opt in cnProtocolOptions"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
@click="apiProtocol = opt.value"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
apiProtocol === opt.value
|
||||
? cnAccentActiveClass
|
||||
: 'border-gray-200 hover:border-gray-400 dark:border-dark-600 dark:hover:border-gray-600'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
|
||||
apiProtocol === opt.value
|
||||
? cnAccentIconClass
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
|
||||
]"
|
||||
>
|
||||
<Icon :name="opt.value === 'anthropic' ? 'sparkles' : opt.value === 'responses' ? 'terminal' : 'chat'" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">{{ t(`admin.accounts.cnProviders.apiProtocol.${opt.labelKey}`) }}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t(`admin.accounts.cnProviders.apiProtocol.${opt.labelKey}Desc`) }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Type Selection (Gemini) -->
|
||||
<div v-if="form.platform === 'gemini'">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -1120,15 +1256,7 @@
|
||||
v-model="apiKeyBaseUrl"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="
|
||||
form.platform === 'openai'
|
||||
? 'https://api.openai.com'
|
||||
: form.platform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: form.platform === 'grok'
|
||||
? 'https://api.x.ai/v1'
|
||||
: 'https://api.anthropic.com'
|
||||
"
|
||||
:placeholder="apiKeyBaseUrlPlaceholder"
|
||||
/>
|
||||
<p v-if="baseUrlHint" class="input-hint">{{ baseUrlHint }}</p>
|
||||
<GrokBaseUrlPresets
|
||||
@@ -1136,6 +1264,15 @@
|
||||
class="mt-2"
|
||||
@select="apiKeyBaseUrl = $event"
|
||||
/>
|
||||
<CnBaseUrlPresets
|
||||
v-if="isCNPlatform"
|
||||
class="mt-2"
|
||||
:platform="cnPresetPlatform"
|
||||
:mode="accountMode"
|
||||
:protocol="apiProtocol"
|
||||
:current-url="apiKeyBaseUrl"
|
||||
@select="onCnPresetSelect"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.apiKeyRequired') }}</label>
|
||||
@@ -1144,15 +1281,7 @@
|
||||
type="password"
|
||||
required
|
||||
class="input font-mono"
|
||||
:placeholder="
|
||||
form.platform === 'openai'
|
||||
? 'sk-proj-...'
|
||||
: form.platform === 'gemini'
|
||||
? 'AIza...'
|
||||
: form.platform === 'grok'
|
||||
? 'xai-...'
|
||||
: 'sk-ant-...'
|
||||
"
|
||||
:placeholder="apiKeyValuePlaceholder"
|
||||
/>
|
||||
<p v-if="apiKeyHint" class="input-hint">{{ apiKeyHint }}</p>
|
||||
</div>
|
||||
@@ -3609,13 +3738,17 @@ import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.
|
||||
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
|
||||
import Toggle from '@/components/common/Toggle.vue'
|
||||
import GrokBaseUrlPresets from '@/components/account/GrokBaseUrlPresets.vue'
|
||||
import CnBaseUrlPresets from '@/components/account/CnBaseUrlPresets.vue'
|
||||
import HeaderOverrideEditor from '@/components/account/HeaderOverrideEditor.vue'
|
||||
import {
|
||||
applyAntigravityProjectID,
|
||||
applyHeaderOverride,
|
||||
applyInterceptWarmup,
|
||||
defaultCNBaseUrl,
|
||||
isHeaderOverrideCapable,
|
||||
validateHeaderOverrideRows,
|
||||
type CnAccountMode,
|
||||
type CnApiProtocol,
|
||||
type HeaderOverrideRow
|
||||
} from '@/components/account/credentialsBuilder'
|
||||
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||
@@ -3674,6 +3807,42 @@ const apiKeyHint = computed(() => {
|
||||
return t('admin.accounts.apiKeyHint')
|
||||
})
|
||||
|
||||
// Base URL / API Key 占位符:国产供应商随账号类型变化。
|
||||
const apiKeyBaseUrlPlaceholder = computed(() => {
|
||||
if (isCNPlatform.value) {
|
||||
return defaultCNBaseUrl(form.platform, accountMode.value, apiProtocol.value) || 'https://api.example.com'
|
||||
}
|
||||
switch (form.platform) {
|
||||
case 'openai':
|
||||
return 'https://api.openai.com'
|
||||
case 'gemini':
|
||||
return 'https://generativelanguage.googleapis.com'
|
||||
case 'grok':
|
||||
return 'https://api.x.ai/v1'
|
||||
default:
|
||||
return 'https://api.anthropic.com'
|
||||
}
|
||||
})
|
||||
|
||||
const apiKeyValuePlaceholder = computed(() => {
|
||||
switch (form.platform) {
|
||||
case 'openai':
|
||||
return 'sk-proj-...'
|
||||
case 'gemini':
|
||||
return 'AIza...'
|
||||
case 'grok':
|
||||
return 'xai-...'
|
||||
case 'kimi':
|
||||
return 'sk-...'
|
||||
case 'zhipu':
|
||||
return '<api-key>.<secret>'
|
||||
case 'deepseek':
|
||||
return 'sk-...'
|
||||
default:
|
||||
return 'sk-ant-...'
|
||||
}
|
||||
})
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
proxies: Proxy[]
|
||||
@@ -3753,6 +3922,86 @@ const apiKeyBaseUrl = ref('https://api.anthropic.com')
|
||||
const apiKeyValue = ref('')
|
||||
const upstreamBillingAutoProbeEnabled = ref(true)
|
||||
|
||||
// ── 国产供应商(Kimi / Zhipu / DeepSeek)账号类型、API 协议与端点 ──
|
||||
const accountMode = ref<CnAccountMode>('payg')
|
||||
// API 协议决定转发端点与格式:cc=现有转换链,anthropic=原生直通(Claude Code),
|
||||
// responses=deepseek 原生 Responses 端点(Codex)。与账号类型正交。
|
||||
const apiProtocol = ref<CnApiProtocol>('chat_completions')
|
||||
const isCNPlatform = computed(
|
||||
() => form.platform === 'kimi' || form.platform === 'zhipu' || form.platform === 'deepseek'
|
||||
)
|
||||
// CnBaseUrlPresets 的 platform prop 是平台字面量联合类型,模板里不能写
|
||||
// `as` 断言(其中的 `|` 会被 eslint 误判为 Vue2 filter 语法),经此 computed 传递。
|
||||
const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
|
||||
if (form.platform === 'kimi' || form.platform === 'zhipu' || form.platform === 'deepseek') {
|
||||
return form.platform
|
||||
}
|
||||
return 'kimi'
|
||||
})
|
||||
// 当前平台可选的协议档(responses 仅 deepseek)。
|
||||
const cnProtocolOptions = computed<Array<{ value: CnApiProtocol; labelKey: string }>>(() => {
|
||||
const opts: Array<{ value: CnApiProtocol; labelKey: string }> = [
|
||||
{ value: 'chat_completions', labelKey: 'chatCompletions' },
|
||||
{ value: 'anthropic', labelKey: 'anthropic' }
|
||||
]
|
||||
if (form.platform === 'deepseek') {
|
||||
opts.push({ value: 'responses', labelKey: 'responses' })
|
||||
}
|
||||
return opts
|
||||
})
|
||||
// 当前选中平台的品牌色(选中卡片描边 / 图标底色),与 platformColors 取色一致。
|
||||
const cnAccentActiveClass = computed(() => {
|
||||
switch (form.platform) {
|
||||
case 'kimi':
|
||||
return 'border-pink-500 bg-pink-50 dark:bg-pink-900/20'
|
||||
case 'zhipu':
|
||||
return 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20'
|
||||
case 'deepseek':
|
||||
return 'border-teal-500 bg-teal-50 dark:bg-teal-900/20'
|
||||
default:
|
||||
return 'border-primary-500 bg-primary-50 dark:bg-primary-900/20'
|
||||
}
|
||||
})
|
||||
const cnAccentIconClass = computed(() => {
|
||||
switch (form.platform) {
|
||||
case 'kimi':
|
||||
return 'bg-pink-500 text-white'
|
||||
case 'zhipu':
|
||||
return 'bg-indigo-500 text-white'
|
||||
case 'deepseek':
|
||||
return 'bg-teal-500 text-white'
|
||||
default:
|
||||
return 'bg-primary-500 text-white'
|
||||
}
|
||||
})
|
||||
// 切换国产供应商平台:强制 apikey 类型,deepseek 无 coding 套餐故锁定 payg,
|
||||
// 协议回落 chat_completions,并把 base url 重置为该平台默认端点。
|
||||
function selectCNPlatform(platform: 'kimi' | 'zhipu' | 'deepseek') {
|
||||
form.platform = platform
|
||||
form.type = 'apikey'
|
||||
accountCategory.value = 'apikey'
|
||||
apiProtocol.value = 'chat_completions'
|
||||
if (platform === 'deepseek') {
|
||||
accountMode.value = 'payg'
|
||||
}
|
||||
apiKeyBaseUrl.value = defaultCNBaseUrl(platform, accountMode.value, apiProtocol.value)
|
||||
}
|
||||
// 账号类型 / 协议变更时同步默认 base url。
|
||||
watch(accountMode, (mode) => {
|
||||
if (!isCNPlatform.value) return
|
||||
apiKeyBaseUrl.value = defaultCNBaseUrl(form.platform, mode, apiProtocol.value)
|
||||
})
|
||||
watch(apiProtocol, (protocol) => {
|
||||
if (!isCNPlatform.value) return
|
||||
apiKeyBaseUrl.value = defaultCNBaseUrl(form.platform, accountMode.value, protocol)
|
||||
})
|
||||
// 点击预设端点:同时回填 base url、账号类型与协议。
|
||||
function onCnPresetSelect(preset: { mode: CnAccountMode; protocol: CnApiProtocol; url: string }) {
|
||||
accountMode.value = preset.mode
|
||||
apiProtocol.value = preset.protocol
|
||||
apiKeyBaseUrl.value = preset.url
|
||||
}
|
||||
|
||||
const syncPreviewCredentials = computed(() => {
|
||||
if (!apiKeyValue.value) return undefined
|
||||
return {
|
||||
@@ -4248,14 +4497,18 @@ watch(
|
||||
() => form.platform,
|
||||
(newPlatform) => {
|
||||
// Reset base URL based on platform
|
||||
apiKeyBaseUrl.value =
|
||||
(newPlatform === 'openai')
|
||||
? 'https://api.openai.com'
|
||||
: newPlatform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: newPlatform === 'grok'
|
||||
? 'https://api.x.ai/v1'
|
||||
: 'https://api.anthropic.com'
|
||||
if (newPlatform === 'kimi' || newPlatform === 'zhipu' || newPlatform === 'deepseek') {
|
||||
apiKeyBaseUrl.value = defaultCNBaseUrl(newPlatform, accountMode.value, apiProtocol.value)
|
||||
} else {
|
||||
apiKeyBaseUrl.value =
|
||||
(newPlatform === 'openai')
|
||||
? 'https://api.openai.com'
|
||||
: newPlatform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: newPlatform === 'grok'
|
||||
? 'https://api.x.ai/v1'
|
||||
: 'https://api.anthropic.com'
|
||||
}
|
||||
// Clear model-related settings
|
||||
allowedModels.value = []
|
||||
modelMappings.value = []
|
||||
@@ -4696,6 +4949,8 @@ const resetForm = () => {
|
||||
form.expires_at = null
|
||||
accountCategory.value = 'oauth-based'
|
||||
addMethod.value = 'oauth'
|
||||
accountMode.value = 'payg'
|
||||
apiProtocol.value = 'chat_completions'
|
||||
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
||||
apiKeyValue.value = ''
|
||||
upstreamBillingAutoProbeEnabled.value = true
|
||||
@@ -5158,6 +5413,20 @@ const handleSubmit = async () => {
|
||||
credentials.tier_id = geminiTierAIStudio.value
|
||||
}
|
||||
|
||||
// 国产供应商:账号模式 + 协议 + 对应端点写入凭据;后端按 account_mode 路由
|
||||
// 额度/余额探测,按 api_protocol 路由转发端点与格式。注意 CN apikey 走本函数
|
||||
// 的通用路径(直接 doCreateAccount),不经过 createAccountAndFinish。
|
||||
if (form.platform === 'kimi' || form.platform === 'zhipu' || form.platform === 'deepseek') {
|
||||
credentials.account_mode = accountMode.value
|
||||
credentials.api_protocol = apiProtocol.value
|
||||
const resolvedCNBase = (
|
||||
apiKeyBaseUrl.value.trim() || defaultCNBaseUrl(form.platform, accountMode.value, apiProtocol.value)
|
||||
).trim()
|
||||
if (resolvedCNBase) {
|
||||
credentials.base_url = resolvedCNBase
|
||||
}
|
||||
}
|
||||
|
||||
// Add model mapping if configured(OpenAI 开启自动透传时不应用)
|
||||
if (!isOpenAIModelRestrictionDisabled.value) {
|
||||
const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
|
||||
|
||||
@@ -52,6 +52,57 @@
|
||||
class="mt-2"
|
||||
@select="editBaseUrl = $event"
|
||||
/>
|
||||
<CnBaseUrlPresets
|
||||
v-if="isCNApiKeyAccount"
|
||||
class="mt-2"
|
||||
:platform="cnPresetPlatform"
|
||||
:mode="editAccountMode"
|
||||
:protocol="editApiProtocol"
|
||||
:current-url="editBaseUrl"
|
||||
@select="onCnPresetSelect"
|
||||
/>
|
||||
</div>
|
||||
<!-- Account Mode Selection (CN providers) -->
|
||||
<div v-if="isCNApiKeyAccount">
|
||||
<label class="input-label">{{ t('admin.accounts.cnProviders.accountMode.title') }}</label>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="opt in cnAccountModeOptions"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
:class="[
|
||||
'rounded-lg border-2 px-3 py-1.5 text-xs transition-all',
|
||||
editAccountMode === opt.value
|
||||
? 'border-primary-500 bg-primary-50 font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-300'
|
||||
: 'border-gray-200 text-gray-700 hover:border-gray-400 dark:border-dark-600 dark:text-gray-300 dark:hover:border-gray-600'
|
||||
]"
|
||||
@click="editAccountMode = opt.value"
|
||||
>
|
||||
{{ t(`admin.accounts.cnProviders.accountMode.${opt.labelKey}`) }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="input-hint">{{ t(`admin.accounts.cnProviders.accountMode.${editAccountMode}Desc`) }}</p>
|
||||
</div>
|
||||
<!-- API Protocol Selection (CN providers) -->
|
||||
<div v-if="isCNApiKeyAccount">
|
||||
<label class="input-label">{{ t('admin.accounts.cnProviders.apiProtocol.title') }}</label>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="opt in cnProtocolOptions"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
:class="[
|
||||
'rounded-lg border-2 px-3 py-1.5 text-xs transition-all',
|
||||
editApiProtocol === opt.value
|
||||
? 'border-primary-500 bg-primary-50 font-medium text-primary-700 dark:bg-primary-900/30 dark:text-primary-300'
|
||||
: 'border-gray-200 text-gray-700 hover:border-gray-400 dark:border-dark-600 dark:text-gray-300 dark:hover:border-gray-600'
|
||||
]"
|
||||
@click="editApiProtocol = opt.value"
|
||||
>
|
||||
{{ t(`admin.accounts.cnProviders.apiProtocol.${opt.labelKey}`) }}
|
||||
</button>
|
||||
</div>
|
||||
<p class="input-hint">{{ t(`admin.accounts.cnProviders.apiProtocol.${cnProtocolDescKey}Desc`) }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.apiKey') }}</label>
|
||||
@@ -2710,7 +2761,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { ref, reactive, computed, watch, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -2737,6 +2788,7 @@ import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||
import QuotaLimitCard from '@/components/account/QuotaLimitCard.vue'
|
||||
import GrokBaseUrlPresets from '@/components/account/GrokBaseUrlPresets.vue'
|
||||
import CnBaseUrlPresets from '@/components/account/CnBaseUrlPresets.vue'
|
||||
import HeaderOverrideEditor from '@/components/account/HeaderOverrideEditor.vue'
|
||||
import OllamaCloudUsageSettings from '@/components/account/OllamaCloudUsageSettings.vue'
|
||||
import {
|
||||
@@ -2750,8 +2802,11 @@ import {
|
||||
isHeaderOverrideCapable,
|
||||
splitHeaderOverridesObject,
|
||||
validateHeaderOverrideRows,
|
||||
defaultCNBaseUrl,
|
||||
HEADER_OVERRIDE_ENABLED_CREDENTIAL_KEY,
|
||||
HEADER_OVERRIDES_CREDENTIAL_KEY,
|
||||
type CnAccountMode,
|
||||
type CnApiProtocol,
|
||||
type HeaderOverrideRow
|
||||
} from '@/components/account/credentialsBuilder'
|
||||
import { formatDateTime, formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||
@@ -2829,6 +2884,78 @@ interface TempUnschedRuleForm {
|
||||
const submitting = ref(false)
|
||||
const editBaseUrl = ref('https://api.anthropic.com')
|
||||
const editApiKey = ref('')
|
||||
|
||||
// ── 国产供应商(Kimi / Zhipu / DeepSeek)account_mode / api_protocol 编辑 ──
|
||||
// account_mode 决定额度/余额监控路径,api_protocol 决定转发端点与格式;
|
||||
// 二者均可修正(早期创建的账号可能存错默认值),切换时重置 base_url 预置。
|
||||
const isCNApiKeyAccount = computed(
|
||||
() =>
|
||||
props.account?.type === 'apikey' &&
|
||||
(props.account.platform === 'kimi' ||
|
||||
props.account.platform === 'zhipu' ||
|
||||
props.account.platform === 'deepseek')
|
||||
)
|
||||
// CnBaseUrlPresets 的 platform prop 是平台字面量联合类型,模板里不能写
|
||||
// `as` 断言(其中的 `|` 会被 eslint 误判为 Vue2 filter 语法),经此 computed 传递。
|
||||
const cnPresetPlatform = computed<'kimi' | 'zhipu' | 'deepseek'>(() => {
|
||||
const platform = props.account?.platform
|
||||
if (platform === 'kimi' || platform === 'zhipu' || platform === 'deepseek') {
|
||||
return platform
|
||||
}
|
||||
return 'kimi'
|
||||
})
|
||||
const editApiProtocol = ref<CnApiProtocol>('chat_completions')
|
||||
const editAccountMode = ref<CnAccountMode>('payg')
|
||||
// 回填窗口标志:syncFormFromAccount 会同步改写 editAccountMode / editApiProtocol,
|
||||
// 而 watcher(pre-flush)在同步代码执行完之后才触发——若不抑制,会把刚恢复的
|
||||
// 存储版 base_url(可能是用户自定义/中转地址)覆盖为官方预设并在下次保存时持久化。
|
||||
// nextTick 后解除,此后用户主动切换模式/协议仍正常联动重置。
|
||||
const syncingForm = ref(false)
|
||||
const cnAccountModeOptions = computed<Array<{ value: CnAccountMode; labelKey: 'payg' | 'coding' }>>(
|
||||
() => {
|
||||
// DeepSeek 无 coding 套餐(与创建弹窗一致),仅保留按量付费。
|
||||
if (props.account?.platform === 'deepseek') {
|
||||
return [{ value: 'payg', labelKey: 'payg' }]
|
||||
}
|
||||
return [
|
||||
{ value: 'payg', labelKey: 'payg' },
|
||||
{ value: 'coding', labelKey: 'coding' }
|
||||
]
|
||||
}
|
||||
)
|
||||
const cnProtocolOptions = computed<Array<{ value: CnApiProtocol; labelKey: string }>>(() => {
|
||||
const opts: Array<{ value: CnApiProtocol; labelKey: string }> = [
|
||||
{ value: 'chat_completions', labelKey: 'chatCompletions' },
|
||||
{ value: 'anthropic', labelKey: 'anthropic' }
|
||||
]
|
||||
if (props.account?.platform === 'deepseek') {
|
||||
opts.push({ value: 'responses', labelKey: 'responses' })
|
||||
}
|
||||
return opts
|
||||
})
|
||||
watch(editApiProtocol, (protocol) => {
|
||||
if (!isCNApiKeyAccount.value || syncingForm.value) return
|
||||
editBaseUrl.value = defaultCNBaseUrl(props.account!.platform, editAccountMode.value, protocol)
|
||||
})
|
||||
watch(editAccountMode, (mode) => {
|
||||
if (!isCNApiKeyAccount.value || syncingForm.value) return
|
||||
// deepseek 无 coding 套餐:防御性回退(UI 已隐藏该选项)。
|
||||
const effectiveMode = props.account!.platform === 'deepseek' && mode === 'coding' ? 'payg' : mode
|
||||
if (effectiveMode !== mode) {
|
||||
editAccountMode.value = effectiveMode
|
||||
return
|
||||
}
|
||||
editBaseUrl.value = defaultCNBaseUrl(props.account!.platform, mode, editApiProtocol.value)
|
||||
})
|
||||
const cnProtocolDescKey = computed(
|
||||
() => cnProtocolOptions.value.find(o => o.value === editApiProtocol.value)?.labelKey ?? 'chatCompletions'
|
||||
)
|
||||
// 点击预设端点:回填 base url 与对应模式/协议。
|
||||
function onCnPresetSelect(preset: { mode: CnAccountMode; protocol: CnApiProtocol; url: string }) {
|
||||
editAccountMode.value = preset.mode
|
||||
editApiProtocol.value = preset.protocol
|
||||
editBaseUrl.value = preset.url
|
||||
}
|
||||
// Bedrock credentials
|
||||
const editBedrockAccessKeyId = ref('')
|
||||
const editBedrockSecretAccessKey = ref('')
|
||||
@@ -3272,6 +3399,15 @@ const defaultBaseUrl = computed(() => {
|
||||
if (props.account?.platform === 'openai') return 'https://api.openai.com'
|
||||
if (props.account?.platform === 'gemini') return 'https://generativelanguage.googleapis.com'
|
||||
if (props.account?.platform === 'grok') return 'https://api.x.ai/v1'
|
||||
// CN 供应商:按当前模式/协议回落到官方预设(清空输入框提交时使用),
|
||||
// 不能落到 anthropic 默认值(会被当 CC base 拼出错误端点)。
|
||||
if (
|
||||
props.account?.platform === 'kimi' ||
|
||||
props.account?.platform === 'zhipu' ||
|
||||
props.account?.platform === 'deepseek'
|
||||
) {
|
||||
return defaultCNBaseUrl(props.account.platform, editAccountMode.value, editApiProtocol.value)
|
||||
}
|
||||
return 'https://api.anthropic.com'
|
||||
})
|
||||
|
||||
@@ -3381,6 +3517,11 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
if (!newAccount) {
|
||||
return
|
||||
}
|
||||
// 进入回填窗口:抑制 CN 模式/协议 watcher 联动重置 base_url(见 syncingForm 注释)。
|
||||
syncingForm.value = true
|
||||
void nextTick(() => {
|
||||
syncingForm.value = false
|
||||
})
|
||||
antigravityMixedChannelConfirmed.value = false
|
||||
showMixedChannelWarning.value = false
|
||||
mixedChannelWarningDetails.value = null
|
||||
@@ -3622,6 +3763,17 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
// Initialize API Key fields for apikey type
|
||||
if (newAccount.type === 'apikey' && newAccount.credentials) {
|
||||
const credentials = newAccount.credentials as Record<string, unknown>
|
||||
// 国产供应商:读取 account_mode 与 api_protocol 作为可编辑初始值
|
||||
// (编辑弹窗允许修正两者,用于修复早期存错默认值的账号)。
|
||||
if (newAccount.platform === 'kimi' || newAccount.platform === 'zhipu' || newAccount.platform === 'deepseek') {
|
||||
editAccountMode.value = credentials.account_mode === 'coding' ? 'coding' : 'payg'
|
||||
const storedProtocol = credentials.api_protocol
|
||||
editApiProtocol.value =
|
||||
storedProtocol === 'anthropic' || storedProtocol === 'responses' ? storedProtocol : 'chat_completions'
|
||||
if (newAccount.platform !== 'deepseek' && editApiProtocol.value === 'responses') {
|
||||
editApiProtocol.value = 'chat_completions'
|
||||
}
|
||||
}
|
||||
const platformDefaultUrl =
|
||||
newAccount.platform === 'openai'
|
||||
? 'https://api.openai.com'
|
||||
@@ -3629,7 +3781,11 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: newAccount.platform === 'grok'
|
||||
? 'https://api.x.ai/v1'
|
||||
: 'https://api.anthropic.com'
|
||||
: newAccount.platform === 'kimi' ||
|
||||
newAccount.platform === 'zhipu' ||
|
||||
newAccount.platform === 'deepseek'
|
||||
? defaultCNBaseUrl(newAccount.platform, editAccountMode.value, editApiProtocol.value)
|
||||
: 'https://api.anthropic.com'
|
||||
editBaseUrl.value = (credentials.base_url as string) || platformDefaultUrl
|
||||
|
||||
// Load model mappings and detect mode
|
||||
@@ -4298,6 +4454,12 @@ const handleSubmit = async () => {
|
||||
base_url: newBaseUrl
|
||||
}
|
||||
|
||||
// 国产供应商:模式与协议写入凭据(决定额度/余额探测与转发端点/格式)。
|
||||
if (isCNApiKeyAccount.value) {
|
||||
newCredentials.account_mode = editAccountMode.value
|
||||
newCredentials.api_protocol = editApiProtocol.value
|
||||
}
|
||||
|
||||
// Handle API key
|
||||
// 后端响应已脱敏:currentCredentials 不会再包含 api_key 原文。
|
||||
// 用户填入新值则覆盖;留空时优先看 credentials_status.has_api_key;
|
||||
|
||||
@@ -199,7 +199,16 @@ const normalizedPlatforms = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const upstreamSyncPlatforms = new Set(['anthropic', 'openai', 'gemini', 'antigravity', 'grok'])
|
||||
const upstreamSyncPlatforms = new Set([
|
||||
'anthropic',
|
||||
'openai',
|
||||
'gemini',
|
||||
'antigravity',
|
||||
'grok',
|
||||
'kimi',
|
||||
'zhipu',
|
||||
'deepseek'
|
||||
])
|
||||
const canSyncUpstream = computed(() => {
|
||||
if (props.accountId) {
|
||||
if (normalizedPlatforms.value.length === 0) return true
|
||||
|
||||
@@ -242,6 +242,91 @@ export const GROK_BASE_URL_PRESETS: GrokBaseUrlPreset[] = [
|
||||
{ label: 'eu-west-1', url: 'https://eu-west-1.api.x.ai/v1' }
|
||||
]
|
||||
|
||||
// ========== 国产供应商(Kimi / Zhipu / DeepSeek)base_url 预设 ==========
|
||||
// 与后端 service/domain_constants.go 的默认 base url 保持一致。
|
||||
// 账号类型(payg 按量付费 / coding 编程套餐)决定额度监控方式;
|
||||
// API 协议(chat_completions / anthropic / responses)决定转发端点与格式,
|
||||
// 两者正交。同协议请求零转换直通,跨协议组合才走转换链。
|
||||
|
||||
export type CnAccountMode = 'payg' | 'coding'
|
||||
|
||||
/** 仅 deepseek 支持 responses 协议(官方原生 /responses 端点,适配 Codex)。 */
|
||||
export type CnApiProtocol = 'chat_completions' | 'anthropic' | 'responses'
|
||||
|
||||
export interface CnBaseUrlPreset {
|
||||
mode: CnAccountMode
|
||||
protocol: CnApiProtocol
|
||||
/** 专有名词,不参与 i18n */
|
||||
label: string
|
||||
url: string
|
||||
}
|
||||
|
||||
/** 各供应商按账号类型 × API 协议分档的快捷端点(点击快速填充,输入框仍可自由填写)。 */
|
||||
export const CN_BASE_URL_PRESETS: Record<'kimi' | 'zhipu' | 'deepseek', CnBaseUrlPreset[]> = {
|
||||
kimi: [
|
||||
{ mode: 'payg', protocol: 'chat_completions', label: 'Moonshot', url: 'https://api.moonshot.cn/v1' },
|
||||
{ mode: 'payg', protocol: 'anthropic', label: 'Moonshot Anthropic', url: 'https://api.moonshot.cn/anthropic' },
|
||||
{ mode: 'coding', protocol: 'chat_completions', label: 'Kimi For Coding', url: 'https://api.kimi.com/coding/v1' },
|
||||
{ mode: 'coding', protocol: 'anthropic', label: 'Kimi Coding Anthropic', url: 'https://api.kimi.com/coding' }
|
||||
],
|
||||
zhipu: [
|
||||
{ mode: 'payg', protocol: 'chat_completions', label: 'GLM PaaS', url: 'https://open.bigmodel.cn/api/paas/v4' },
|
||||
{ mode: 'payg', protocol: 'anthropic', label: 'GLM Anthropic', url: 'https://open.bigmodel.cn/api/anthropic' },
|
||||
{ mode: 'coding', protocol: 'chat_completions', label: 'GLM Coding', url: 'https://open.bigmodel.cn/api/coding/paas/v4' },
|
||||
{ mode: 'coding', protocol: 'anthropic', label: 'GLM Coding Anthropic', url: 'https://open.bigmodel.cn/api/anthropic' }
|
||||
],
|
||||
deepseek: [
|
||||
{ mode: 'payg', protocol: 'chat_completions', label: 'DeepSeek', url: 'https://api.deepseek.com' },
|
||||
{ mode: 'payg', protocol: 'anthropic', label: 'DeepSeek Anthropic', url: 'https://api.deepseek.com/anthropic' },
|
||||
{ mode: 'payg', protocol: 'responses', label: 'DeepSeek Responses', url: 'https://api.deepseek.com' }
|
||||
]
|
||||
}
|
||||
|
||||
/** 返回指定供应商 + 账号类型 + API 协议的默认 base url。 */
|
||||
export function defaultCNBaseUrl(
|
||||
platform: string,
|
||||
mode: CnAccountMode,
|
||||
protocol: CnApiProtocol = 'chat_completions'
|
||||
): string {
|
||||
if (protocol === 'anthropic') {
|
||||
switch (platform) {
|
||||
case 'kimi':
|
||||
return mode === 'coding' ? 'https://api.kimi.com/coding' : 'https://api.moonshot.cn/anthropic'
|
||||
case 'zhipu':
|
||||
return 'https://open.bigmodel.cn/api/anthropic'
|
||||
case 'deepseek':
|
||||
return 'https://api.deepseek.com/anthropic'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
// responses 仅 deepseek:base 与 chat_completions 相同(端点路径差异由后端处理)。
|
||||
switch (platform) {
|
||||
case 'kimi':
|
||||
return mode === 'coding' ? 'https://api.kimi.com/coding/v1' : 'https://api.moonshot.cn/v1'
|
||||
case 'zhipu':
|
||||
return mode === 'coding'
|
||||
? 'https://open.bigmodel.cn/api/coding/paas/v4'
|
||||
: 'https://open.bigmodel.cn/api/paas/v4'
|
||||
case 'deepseek':
|
||||
return 'https://api.deepseek.com'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 国产供应商用量单元格可见性(单一事实源) =====
|
||||
// CNProviderQuotaCell / CNProviderBalanceCell 与 AccountUsageCell 的占位符判定
|
||||
// 共用,避免多处复制条件后一处改另一处漏改。
|
||||
|
||||
export function cnQuotaCellVisible(platform: string, accountMode: string): boolean {
|
||||
return (platform === 'kimi' || platform === 'zhipu') && accountMode === 'coding'
|
||||
}
|
||||
|
||||
export function cnBalanceCellVisible(platform: string, accountMode: string): boolean {
|
||||
return (platform === 'kimi' || platform === 'deepseek') && accountMode !== 'coding'
|
||||
}
|
||||
|
||||
/**
|
||||
* 将请求头覆写写入 credentials。
|
||||
* create 模式:关闭时不写入任何字段;edit 模式:关闭时删除字段(全量替换语义)。
|
||||
|
||||
@@ -25,6 +25,25 @@
|
||||
d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"
|
||||
/>
|
||||
</svg>
|
||||
<!-- Kimi / Moonshot official logo mark (stylized K) -->
|
||||
<svg v-else-if="platform === 'kimi'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M21.765.351C22.998.351 24 1.353 24 2.586S22.998 4.82 21.765 4.82h-1.974c-.15 0-.26-.12-.26-.26V2.586A2.237 2.237 0 0 1 21.765.35M9.41 13.388l8.447-8.377c.16-.16.07-.471-.14-.471h-4.55s-.1.02-.14.06l-9.099 9.029c-.14.14-.35.02-.35-.21V4.81c0-.15-.1-.27-.221-.27H.22c-.12 0-.22.12-.22.27v18.57c0 .15.1.27.22.27h3.137c.12 0 .22-.12.22-.27v-3.79c0-.08.03-.16.08-.21l2.826-2.796c.07-.07.16-.08.241-.03l7.546 5.551a8.9 8.9 0 0 0 4.018 1.493c.12.01.23-.11.23-.27V19.76c0-.14-.08-.25-.19-.26a5.8 5.8 0 0 1-2.355-.942l-6.533-4.73c-.14-.09-.15-.32-.03-.441"
|
||||
/>
|
||||
</svg>
|
||||
<!-- Zhipu AI official logo mark (particle constellation) -->
|
||||
<svg v-else-if="platform === 'zhipu'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M11.991 23.503a.24.24 0 0 0-.244.248a.24.24 0 0 0 .244.249a.24.24 0 0 0 .245-.249a.24.24 0 0 0-.22-.247zM9.671 5.365a1.697 1.697 0 0 1 1.099 2.132l-.071.172l-.016.04l-.018.054c-.07.16-.104.32-.104.498c-.035.71.47 1.279 1.186 1.314h.366c1.309.053 2.338 1.173 2.286 2.523c-.052 1.332-1.152 2.38-2.478 2.327h-.174c-.715.018-1.274.64-1.239 1.368c0 .124.018.23.053.337c.209.373.54.658.96.8c.75.23 1.517-.125 1.9-.782l.018-.035c.402-.64 1.17-.96 1.92-.711c.854.284 1.378 1.226 1.099 2.167a1.66 1.66 0 0 1-2.077 1.102a1.7 1.7 0 0 1-.907-.711l-.017-.035c-.2-.323-.463-.58-.851-.711l-.056-.018a1.646 1.646 0 0 0-1.954.746a1.66 1.66 0 0 1-1.065.764a1.677 1.677 0 0 1-1.989-1.279c-.209-.906.332-1.83 1.257-2.043a1.5 1.5 0 0 1 .296-.035h.018c.68-.071 1.151-.622 1.116-1.333a1.3 1.3 0 0 0-.227-.693a2.5 2.5 0 0 1-.366-1.403a2.4 2.4 0 0 1 .366-1.208c.14-.195.21-.444.227-.693c.018-.71-.506-1.261-1.186-1.332l-.07-.018a1.4 1.4 0 0 1-.299-.07l-.05-.019a1.7 1.7 0 0 1-1.047-2.114a1.68 1.68 0 0 1 2.094-1.101m-5.575 10.11c.26-.264.639-.367.994-.27s.633.379.728.74c.095.362-.007.748-.267 1.013c-.402.41-1.053.41-1.455 0a1.06 1.06 0 0 1 0-1.482zm14.845-.294c.359-.09.738.024.992.297c.254.274.344.665.237 1.025s-.396.634-.756.718c-.551.128-1.1-.22-1.23-.781a1.05 1.05 0 0 1 .757-1.26zm-.064-4.39c.314.32.49.753.49 1.206s-.176.886-.49 1.206c-.315.32-.74.5-1.185.5c-.444 0-.87-.18-1.184-.5a1.727 1.727 0 0 1 0-2.412a1.654 1.654 0 0 1 2.369 0m-11.243.163c.364.484.447 1.128.218 1.691a1.665 1.665 0 0 1-2.188.923c-.855-.36-1.26-1.358-.907-2.228a1.68 1.68 0 0 1 1.33-1.038a1.66 1.66 0 0 1 1.547.652m11.545-4.221c.368 0 .708.2.892.524s.184.724 0 1.048a1.03 1.03 0 0 1-.892.524a1.04 1.04 0 0 1-1.03-1.048a1.04 1.04 0 0 1 1.03-1.048m-14.358 0c.368 0 .707.2.891.524s.184.724 0 1.048a1.03 1.03 0 0 1-.891.524a1.04 1.04 0 0 1-1.03-1.048c0-.579.461-1.048 1.03-1.048m10.031-1.475c.925 0 1.675.764 1.675 1.706s-.75 1.705-1.675 1.705s-1.674-.763-1.674-1.705s.75-1.706 1.674-1.706m-2.626-.684c.362-.082.653-.356.761-.718a1.06 1.06 0 0 0-.238-1.028a1.02 1.02 0 0 0-.996-.294c-.547.14-.881.7-.752 1.257c.13.558.675.907 1.225.783m0 16.876c.359-.087.644-.36.75-.72a1.06 1.06 0 0 0-.237-1.019a1.02 1.02 0 0 0-.985-.301a1.04 1.04 0 0 0-.762.717c-.108.361-.017.754.239 1.028c.245.263.606.377.953.305zM17.19 3.5a.63.63 0 0 0 .628-.64a.63.63 0 0 0-.628-.64a.63.63 0 0 0-.628.64c0 .355.28.64.628.64m-10.38 0a.63.63 0 0 0 .628-.64c0-.355-.28-.64-.628-.64a.63.63 0 0 0-.628.64c0 .355.279.64.628.64m-5.182 7.852a.63.63 0 0 0-.628.64c0 .354.28.639.628.639a.63.63 0 0 0 .627-.606l.001-.034a.62.62 0 0 0-.628-.64zm5.182 9.13a.63.63 0 0 0-.628.64c0 .355.279.64.628.64a.63.63 0 0 0 .628-.64c0-.355-.28-.64-.628-.64m10.38.018a.63.63 0 0 0-.628.64c0 .355.28.64.628.64a.63.63 0 0 0 .628-.64a.63.63 0 0 0-.628-.64m5.182-9.148a.63.63 0 0 0-.628.64c0 .354.279.639.628.639a.63.63 0 0 0 .628-.64c0-.355-.28-.64-.628-.64zm-.384-4.992a.24.24 0 0 0 .244-.249a.24.24 0 0 0-.244-.249a.24.24 0 0 0-.244.249c0 .142.122.249.244.249M11.991.497a.24.24 0 0 0 .245-.248A.24.24 0 0 0 11.99 0a.24.24 0 0 0-.244.249c0 .133.108.236.223.247zM2.011 6.36a.24.24 0 0 0 .245-.249a.24.24 0 0 0-.244-.249a.24.24 0 0 0-.244.249a.24.24 0 0 0 .244.249zm0 11.263a.24.24 0 0 0-.243.248a.24.24 0 0 0 .244.249a.24.24 0 0 0 .244-.249a.25.25 0 0 0-.244-.248zm19.995-.018a.24.24 0 0 0-.245.248a.24.24 0 0 0 .245.25a.24.24 0 0 0 .244-.25a.25.25 0 0 0-.244-.248z"
|
||||
/>
|
||||
</svg>
|
||||
<!-- DeepSeek official logo mark (whale) -->
|
||||
<svg v-else-if="platform === 'deepseek'" :class="sizeClass" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136a9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287a.3.3 0 0 1 .113.074a.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078a.253.253 0 0 1-.114-.358a1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45"
|
||||
/>
|
||||
</svg>
|
||||
<!-- Composite group icon -->
|
||||
<svg v-else-if="platform === 'composite'" :class="sizeClass" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="6" cy="12" r="3" />
|
||||
|
||||
@@ -90,6 +90,9 @@ const platformLabel = computed(() => {
|
||||
if (props.platform === 'openai') return 'OpenAI'
|
||||
if (props.platform === 'antigravity') return 'Antigravity'
|
||||
if (props.platform === 'grok') return 'Grok'
|
||||
if (props.platform === 'kimi') return 'Kimi'
|
||||
if (props.platform === 'zhipu') return 'Zhipu GLM'
|
||||
if (props.platform === 'deepseek') return 'DeepSeek'
|
||||
return 'Gemini'
|
||||
})
|
||||
|
||||
@@ -188,6 +191,15 @@ const platformClass = computed(() => {
|
||||
if (props.platform === 'grok') {
|
||||
return 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300'
|
||||
}
|
||||
if (props.platform === 'kimi') {
|
||||
return 'bg-pink-100 text-pink-700 dark:bg-pink-900/30 dark:text-pink-400'
|
||||
}
|
||||
if (props.platform === 'zhipu') {
|
||||
return 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400'
|
||||
}
|
||||
if (props.platform === 'deepseek') {
|
||||
return 'bg-teal-100 text-teal-700 dark:bg-teal-900/30 dark:text-teal-400'
|
||||
}
|
||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
})
|
||||
|
||||
@@ -204,6 +216,15 @@ const typeClass = computed(() => {
|
||||
if (props.platform === 'grok') {
|
||||
return 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300'
|
||||
}
|
||||
if (props.platform === 'kimi') {
|
||||
return 'bg-pink-100 text-pink-600 dark:bg-pink-900/30 dark:text-pink-400'
|
||||
}
|
||||
if (props.platform === 'zhipu') {
|
||||
return 'bg-indigo-100 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-400'
|
||||
}
|
||||
if (props.platform === 'deepseek') {
|
||||
return 'bg-teal-100 text-teal-600 dark:bg-teal-900/30 dark:text-teal-400'
|
||||
}
|
||||
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
})
|
||||
|
||||
|
||||
@@ -182,7 +182,9 @@ const yiModels = [
|
||||
// Moonshot/Kimi
|
||||
const moonshotModels = [
|
||||
'moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k',
|
||||
'kimi-latest'
|
||||
'kimi-latest',
|
||||
'kimi-for-coding',
|
||||
'kimi-k2'
|
||||
]
|
||||
|
||||
// 字节跳动 豆包
|
||||
@@ -428,7 +430,8 @@ export function getModelsByPlatform(platform: string): string[] {
|
||||
case 'grok': return xaiModels
|
||||
case 'cohere': return cohereModels
|
||||
case 'yi': return yiModels
|
||||
case 'moonshot': return moonshotModels
|
||||
case 'moonshot':
|
||||
case 'kimi': return moonshotModels
|
||||
case 'doubao': return doubaoModels
|
||||
case 'minimax': return minimaxModels
|
||||
case 'baidu': return baiduModels
|
||||
|
||||
@@ -104,6 +104,33 @@ export default {
|
||||
gemini: 'Gemini',
|
||||
antigravity: 'Antigravity',
|
||||
grok: 'Grok',
|
||||
kimi: 'Kimi',
|
||||
zhipu: 'Zhipu GLM',
|
||||
deepseek: 'DeepSeek',
|
||||
},
|
||||
cnProviders: {
|
||||
accountMode: {
|
||||
title: 'Account Type',
|
||||
payg: 'Pay-as-you-go',
|
||||
paygDesc: 'Consumes account balance, billed per token. Auto-cools down on low balance and recovers after top-up.',
|
||||
coding: 'Coding Plan',
|
||||
codingDesc: 'Subscription coding package, rate-limited by 5-hour / weekly rolling usage windows.',
|
||||
},
|
||||
apiProtocol: {
|
||||
title: 'API Protocol',
|
||||
chatCompletions: 'Chat Completions',
|
||||
chatCompletionsDesc: 'Standard OpenAI-compatible endpoint; requests in other formats are converted.',
|
||||
anthropic: 'Anthropic',
|
||||
anthropicDesc: 'Native passthrough to the provider’s Anthropic endpoint — ideal for Claude Code.',
|
||||
responses: 'Responses',
|
||||
responsesDesc: 'Provider’s native Responses endpoint — ideal for Codex.',
|
||||
},
|
||||
window5h: '5-hour window',
|
||||
windowWeekly: 'Weekly window',
|
||||
probeTooltip: 'Query the provider quota endpoint for 5-hour / weekly rolling window usage',
|
||||
balanceLow: 'Insufficient balance',
|
||||
noBalanceEndpoint: 'This platform has no balance query endpoint',
|
||||
resetSoon: 'reset soon',
|
||||
},
|
||||
types: {
|
||||
oauth: 'OAuth',
|
||||
|
||||
@@ -307,6 +307,33 @@ export default {
|
||||
gemini: 'Gemini',
|
||||
antigravity: 'Antigravity',
|
||||
grok: 'Grok',
|
||||
kimi: 'Kimi',
|
||||
zhipu: 'Zhipu GLM',
|
||||
deepseek: 'DeepSeek',
|
||||
},
|
||||
cnProviders: {
|
||||
accountMode: {
|
||||
title: '账号类型',
|
||||
payg: '按量付费',
|
||||
paygDesc: '消耗账户余额,按 Token 计费。余额不足自动冷却,充值后恢复。',
|
||||
coding: 'Coding Plan',
|
||||
codingDesc: '订阅制编程套餐,按 5 小时 / 每周滚动用量窗口限流。',
|
||||
},
|
||||
apiProtocol: {
|
||||
title: 'API 协议',
|
||||
chatCompletions: 'Chat Completions',
|
||||
chatCompletionsDesc: '标准 OpenAI 兼容端点,其他格式请求将被转换。',
|
||||
anthropic: 'Anthropic',
|
||||
anthropicDesc: '直通供应商原生 Anthropic 端点,零转换,适配 Claude Code。',
|
||||
responses: 'Responses',
|
||||
responsesDesc: '供应商原生 Responses 端点,适配 Codex。',
|
||||
},
|
||||
window5h: '5 小时窗口',
|
||||
windowWeekly: '每周窗口',
|
||||
probeTooltip: '请求供应商额度端点,查询 5 小时 / 每周滚动窗口用量',
|
||||
balanceLow: '余额不足',
|
||||
noBalanceEndpoint: '该平台暂无余额查询接口',
|
||||
resetSoon: '即将重置',
|
||||
},
|
||||
types: {
|
||||
oauth: 'OAuth',
|
||||
|
||||
@@ -525,7 +525,7 @@ export interface PaginationConfig {
|
||||
|
||||
// ==================== API Key & Group Types ====================
|
||||
|
||||
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'grok' | 'composite'
|
||||
export type GroupPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'grok' | 'kimi' | 'zhipu' | 'deepseek' | 'composite'
|
||||
|
||||
export type VideoModelPrices = Record<string, Record<string, number>>
|
||||
|
||||
@@ -882,7 +882,7 @@ export interface UpdateGroupRequest {
|
||||
|
||||
// ==================== Account & Proxy Types ====================
|
||||
|
||||
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'grok'
|
||||
export type AccountPlatform = 'anthropic' | 'openai' | 'gemini' | 'antigravity' | 'grok' | 'kimi' | 'zhipu' | 'deepseek'
|
||||
export type AccountType = 'oauth' | 'setup-token' | 'apikey' | 'upstream' | 'bedrock' | 'service_account'
|
||||
export type OAuthAddMethod = 'oauth' | 'setup-token'
|
||||
export type ProxyProtocol = 'http' | 'https' | 'socks5' | 'socks5h'
|
||||
|
||||
@@ -5,7 +5,16 @@
|
||||
* instead of defining their own color mappings.
|
||||
*/
|
||||
|
||||
export type Platform = 'anthropic' | 'openai' | 'antigravity' | 'gemini' | 'grok' | 'composite'
|
||||
export type Platform =
|
||||
| 'anthropic'
|
||||
| 'openai'
|
||||
| 'antigravity'
|
||||
| 'gemini'
|
||||
| 'grok'
|
||||
| 'kimi'
|
||||
| 'zhipu'
|
||||
| 'deepseek'
|
||||
| 'composite'
|
||||
|
||||
// ── Badge (bg + text + border, for inline badges with border) ───────
|
||||
const BADGE: Record<Platform, string> = {
|
||||
@@ -14,6 +23,9 @@ const BADGE: Record<Platform, string> = {
|
||||
antigravity: 'bg-purple-500/10 text-purple-600 border-purple-500/30 dark:text-purple-400',
|
||||
gemini: 'bg-blue-500/10 text-blue-600 border-blue-500/30 dark:text-blue-400',
|
||||
grok: 'bg-zinc-800/10 text-zinc-800 border-zinc-800/30 dark:bg-zinc-500/10 dark:text-zinc-200 dark:border-zinc-500/30',
|
||||
kimi: 'bg-pink-500/10 text-pink-600 border-pink-500/30 dark:text-pink-400',
|
||||
zhipu: 'bg-indigo-500/10 text-indigo-600 border-indigo-500/30 dark:text-indigo-400',
|
||||
deepseek: 'bg-teal-500/10 text-teal-600 border-teal-500/30 dark:text-teal-400',
|
||||
composite: 'bg-cyan-500/10 text-cyan-700 border-cyan-500/30 dark:text-cyan-300',
|
||||
}
|
||||
const BADGE_DEFAULT = 'bg-slate-500/10 text-slate-600 border-slate-500/30 dark:text-slate-400'
|
||||
@@ -25,6 +37,9 @@ const BADGE_LIGHT: Record<Platform, string> = {
|
||||
antigravity: 'bg-purple-500/10 text-purple-600 dark:bg-purple-500/10 dark:text-purple-300',
|
||||
gemini: 'bg-blue-500/10 text-blue-600 dark:bg-blue-500/10 dark:text-blue-300',
|
||||
grok: 'bg-zinc-800/10 text-zinc-800 dark:bg-zinc-500/10 dark:text-zinc-200',
|
||||
kimi: 'bg-pink-500/10 text-pink-600 dark:bg-pink-500/10 dark:text-pink-300',
|
||||
zhipu: 'bg-indigo-500/10 text-indigo-600 dark:bg-indigo-500/10 dark:text-indigo-300',
|
||||
deepseek: 'bg-teal-500/10 text-teal-600 dark:bg-teal-500/10 dark:text-teal-300',
|
||||
composite: 'bg-cyan-500/10 text-cyan-700 dark:bg-cyan-500/10 dark:text-cyan-300',
|
||||
}
|
||||
|
||||
@@ -35,6 +50,9 @@ const BORDER: Record<Platform, string> = {
|
||||
antigravity: 'border-purple-500/20 dark:border-purple-500/20',
|
||||
gemini: 'border-blue-500/20 dark:border-blue-500/20',
|
||||
grok: 'border-zinc-800/20 dark:border-zinc-500/20',
|
||||
kimi: 'border-pink-500/20 dark:border-pink-500/20',
|
||||
zhipu: 'border-indigo-500/20 dark:border-indigo-500/20',
|
||||
deepseek: 'border-teal-500/20 dark:border-teal-500/20',
|
||||
composite: 'border-cyan-500/20 dark:border-cyan-500/20',
|
||||
}
|
||||
const BORDER_DEFAULT = 'border-gray-200 dark:border-dark-700'
|
||||
@@ -46,6 +64,9 @@ const BORDER_STRONG: Record<Platform, string> = {
|
||||
antigravity: 'border-purple-500/35 dark:border-purple-500/30',
|
||||
gemini: 'border-blue-500/35 dark:border-blue-500/30',
|
||||
grok: 'border-zinc-800/35 dark:border-zinc-500/35',
|
||||
kimi: 'border-pink-500/35 dark:border-pink-500/30',
|
||||
zhipu: 'border-indigo-500/35 dark:border-indigo-500/30',
|
||||
deepseek: 'border-teal-500/35 dark:border-teal-500/30',
|
||||
composite: 'border-cyan-500/35 dark:border-cyan-500/30',
|
||||
}
|
||||
const BORDER_STRONG_DEFAULT = 'border-gray-300 dark:border-dark-600'
|
||||
@@ -58,6 +79,9 @@ const ACCENT: Record<Platform, string> = {
|
||||
antigravity: '#a855f7', // purple-500
|
||||
gemini: '#3b82f6', // blue-500
|
||||
grok: '#71717a', // zinc-500
|
||||
kimi: '#ec4899', // pink-500
|
||||
zhipu: '#6366f1', // indigo-500
|
||||
deepseek: '#14b8a6', // teal-500
|
||||
composite: '#06b6d4', // cyan-500
|
||||
}
|
||||
const ACCENT_DEFAULT = '#14b8a6' // primary-500 (teal)
|
||||
@@ -69,6 +93,9 @@ const ACCENT_BAR: Record<Platform, string> = {
|
||||
antigravity: 'bg-gradient-to-r from-purple-400 to-purple-500',
|
||||
gemini: 'bg-gradient-to-r from-blue-400 to-blue-500',
|
||||
grok: 'bg-gradient-to-r from-zinc-700 to-zinc-900',
|
||||
kimi: 'bg-gradient-to-r from-pink-400 to-pink-500',
|
||||
zhipu: 'bg-gradient-to-r from-indigo-400 to-indigo-500',
|
||||
deepseek: 'bg-gradient-to-r from-teal-400 to-teal-500',
|
||||
composite: 'bg-gradient-to-r from-slate-500 to-cyan-500',
|
||||
}
|
||||
const ACCENT_BAR_DEFAULT = 'bg-gradient-to-r from-primary-400 to-primary-500'
|
||||
@@ -80,6 +107,9 @@ const TEXT: Record<Platform, string> = {
|
||||
antigravity: 'text-purple-600 dark:text-purple-400',
|
||||
gemini: 'text-blue-600 dark:text-blue-400',
|
||||
grok: 'text-zinc-800 dark:text-zinc-200',
|
||||
kimi: 'text-pink-600 dark:text-pink-400',
|
||||
zhipu: 'text-indigo-600 dark:text-indigo-400',
|
||||
deepseek: 'text-teal-600 dark:text-teal-400',
|
||||
composite: 'text-cyan-700 dark:text-cyan-300',
|
||||
}
|
||||
const TEXT_DEFAULT = 'text-primary-600 dark:text-primary-400'
|
||||
@@ -91,6 +121,9 @@ const ICON: Record<Platform, string> = {
|
||||
antigravity: 'text-purple-500 dark:text-purple-400',
|
||||
gemini: 'text-blue-500 dark:text-blue-400',
|
||||
grok: 'text-zinc-800 dark:text-zinc-200',
|
||||
kimi: 'text-pink-500 dark:text-pink-400',
|
||||
zhipu: 'text-indigo-500 dark:text-indigo-400',
|
||||
deepseek: 'text-teal-500 dark:text-teal-400',
|
||||
composite: 'text-cyan-600 dark:text-cyan-300',
|
||||
}
|
||||
const ICON_DEFAULT = 'text-primary-500 dark:text-primary-400'
|
||||
@@ -102,6 +135,9 @@ const BUTTON: Record<Platform, string> = {
|
||||
antigravity: 'bg-purple-500 text-white hover:bg-purple-600 active:bg-purple-700 dark:bg-purple-500/80 dark:hover:bg-purple-500',
|
||||
gemini: 'bg-blue-500 text-white hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-500/80 dark:hover:bg-blue-500',
|
||||
grok: 'bg-zinc-800 text-white hover:bg-zinc-900 active:bg-black dark:bg-zinc-700 dark:hover:bg-zinc-600',
|
||||
kimi: 'bg-pink-500 text-white hover:bg-pink-600 active:bg-pink-700 dark:bg-pink-500/80 dark:hover:bg-pink-500',
|
||||
zhipu: 'bg-indigo-500 text-white hover:bg-indigo-600 active:bg-indigo-700 dark:bg-indigo-500/80 dark:hover:bg-indigo-500',
|
||||
deepseek: 'bg-teal-500 text-white hover:bg-teal-600 active:bg-teal-700 dark:bg-teal-500/80 dark:hover:bg-teal-500',
|
||||
composite: 'bg-cyan-700 text-white hover:bg-cyan-800 active:bg-cyan-900 dark:bg-cyan-600 dark:hover:bg-cyan-500',
|
||||
}
|
||||
const BUTTON_DEFAULT = 'bg-primary-500 text-white hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-500'
|
||||
@@ -113,6 +149,9 @@ const DISCOUNT: Record<Platform, string> = {
|
||||
antigravity: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
|
||||
gemini: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
|
||||
grok: 'bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200',
|
||||
kimi: 'bg-pink-100 text-pink-700 dark:bg-pink-900/40 dark:text-pink-300',
|
||||
zhipu: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300',
|
||||
deepseek: 'bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300',
|
||||
composite: 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/40 dark:text-cyan-300',
|
||||
}
|
||||
const DISCOUNT_DEFAULT = 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
|
||||
@@ -124,6 +163,9 @@ const GRADIENT: Record<Platform, string> = {
|
||||
antigravity: 'from-purple-500 to-purple-600',
|
||||
gemini: 'from-blue-500 to-blue-600',
|
||||
grok: 'from-zinc-700 to-zinc-900',
|
||||
kimi: 'from-pink-500 to-pink-600',
|
||||
zhipu: 'from-indigo-500 to-indigo-600',
|
||||
deepseek: 'from-teal-500 to-teal-600',
|
||||
composite: 'from-slate-600 to-cyan-600',
|
||||
}
|
||||
const GRADIENT_DEFAULT = 'from-primary-500 to-primary-600'
|
||||
@@ -135,6 +177,9 @@ const GRADIENT_TEXT: Record<Platform, string> = {
|
||||
antigravity: 'text-purple-100',
|
||||
gemini: 'text-blue-100',
|
||||
grok: 'text-zinc-100',
|
||||
kimi: 'text-pink-100',
|
||||
zhipu: 'text-indigo-100',
|
||||
deepseek: 'text-teal-100',
|
||||
composite: 'text-cyan-100',
|
||||
}
|
||||
const GRADIENT_TEXT_DEFAULT = 'text-primary-100'
|
||||
@@ -145,6 +190,9 @@ const GRADIENT_SUBTEXT: Record<Platform, string> = {
|
||||
antigravity: 'text-purple-200',
|
||||
gemini: 'text-blue-200',
|
||||
grok: 'text-zinc-300',
|
||||
kimi: 'text-pink-200',
|
||||
zhipu: 'text-indigo-200',
|
||||
deepseek: 'text-teal-200',
|
||||
composite: 'text-cyan-200',
|
||||
}
|
||||
const GRADIENT_SUBTEXT_DEFAULT = 'text-primary-200'
|
||||
@@ -152,7 +200,17 @@ const GRADIENT_SUBTEXT_DEFAULT = 'text-primary-200'
|
||||
// ── Public API ──────────────────────────────────────────────────────
|
||||
|
||||
function isPlatform(p: string): p is Platform {
|
||||
return p === 'anthropic' || p === 'openai' || p === 'antigravity' || p === 'gemini' || p === 'grok' || p === 'composite'
|
||||
return (
|
||||
p === 'anthropic' ||
|
||||
p === 'openai' ||
|
||||
p === 'antigravity' ||
|
||||
p === 'gemini' ||
|
||||
p === 'grok' ||
|
||||
p === 'kimi' ||
|
||||
p === 'zhipu' ||
|
||||
p === 'deepseek' ||
|
||||
p === 'composite'
|
||||
)
|
||||
}
|
||||
|
||||
export function platformBadgeClass(p: string): string {
|
||||
@@ -214,6 +272,9 @@ export function platformLabel(p: string): string {
|
||||
case 'antigravity': return 'Antigravity'
|
||||
case 'gemini': return 'Gemini'
|
||||
case 'grok': return 'Grok'
|
||||
case 'kimi': return 'Kimi'
|
||||
case 'zhipu': return 'Zhipu GLM'
|
||||
case 'deepseek': return 'DeepSeek'
|
||||
case 'composite': return 'Composite'
|
||||
default: return p || 'API'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user