feat(keys): 适配 Antigravity 和 Gemini 平台的使用教程与 CCS 导入

- UseKeyModal: 添加 Antigravity 两级 Tab (Claude Code / Gemini CLI)
- UseKeyModal: 添加 Gemini 平台的 Gemini CLI 教程
- UseKeyModal: Antigravity 平台统一使用 /antigravity 后缀
- KeysView: CCS 导入支持 Antigravity (询问客户端) / Gemini / OpenAI
- i18n: 添加相关中英文翻译
This commit is contained in:
shaw
2026-01-02 15:53:05 +08:00
parent b1528e9dec
commit 9d3ec9e627
4 changed files with 307 additions and 30 deletions

View File

@@ -28,7 +28,29 @@
{{ platformDescription }}
</p>
<!-- OS Tabs -->
<!-- Client Tabs (only for Antigravity platform) -->
<div v-if="platform === 'antigravity'" class="border-b border-gray-200 dark:border-dark-700">
<nav class="-mb-px flex space-x-6" aria-label="Client">
<button
v-for="tab in clientTabs"
:key="tab.id"
@click="activeClientTab = tab.id"
:class="[
'whitespace-nowrap py-2.5 px-1 border-b-2 font-medium text-sm transition-colors',
activeClientTab === tab.id
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
]"
>
<span class="flex items-center gap-2">
<component :is="tab.icon" class="w-4 h-4" />
{{ tab.label }}
</span>
</button>
</nav>
</div>
<!-- OS/Shell Tabs -->
<div class="border-b border-gray-200 dark:border-dark-700">
<nav class="-mb-px flex space-x-4" aria-label="Tabs">
<button
@@ -154,16 +176,21 @@ const { copyToClipboard: clipboardCopy } = useClipboard()
const copiedIndex = ref<number | null>(null)
const activeTab = ref<string>('unix')
const activeClientTab = ref<string>('claude') // Level 1 tab for antigravity platform
// Reset active tab when platform changes
// Reset tabs when platform changes
watch(() => props.platform, (newPlatform) => {
if (newPlatform === 'openai') {
activeTab.value = 'unix'
} else {
activeTab.value = 'unix'
activeTab.value = 'unix'
if (newPlatform === 'antigravity') {
activeClientTab.value = 'claude'
}
})
// Reset shell tab when client changes (for antigravity)
watch(activeClientTab, () => {
activeTab.value = 'unix'
})
// Icon components
const AppleIcon = {
render() {
@@ -189,8 +216,52 @@ const WindowsIcon = {
}
}
// Anthropic tabs (3 shell types)
const anthropicTabs: TabConfig[] = [
// Terminal icon for Claude Code
const TerminalIcon = {
render() {
return h('svg', {
fill: 'none',
stroke: 'currentColor',
viewBox: '0 0 24 24',
'stroke-width': '1.5',
class: 'w-4 h-4'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
d: 'm6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 17.25V6.75A2.25 2.25 0 0 0 18.75 4.5H5.25A2.25 2.25 0 0 0 3 6.75v10.5A2.25 2.25 0 0 0 5.25 20.25Z'
})
])
}
}
// Sparkle icon for Gemini
const SparkleIcon = {
render() {
return h('svg', {
fill: 'none',
stroke: 'currentColor',
viewBox: '0 0 24 24',
'stroke-width': '1.5',
class: 'w-4 h-4'
}, [
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
d: 'M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z'
})
])
}
}
// Client tabs for Antigravity platform (Level 1)
const clientTabs = computed((): TabConfig[] => [
{ id: 'claude', label: t('keys.useKeyModal.antigravity.claudeCode'), icon: TerminalIcon },
{ id: 'gemini', label: t('keys.useKeyModal.antigravity.geminiCli'), icon: SparkleIcon }
])
// Shell tabs (3 types for environment variable based configs)
const shellTabs: TabConfig[] = [
{ id: 'unix', label: 'macOS / Linux', icon: AppleIcon },
{ id: 'cmd', label: 'Windows CMD', icon: WindowsIcon },
{ id: 'powershell', label: 'PowerShell', icon: WindowsIcon }
@@ -204,26 +275,40 @@ const openaiTabs: TabConfig[] = [
const currentTabs = computed(() => {
if (props.platform === 'openai') {
return openaiTabs
return openaiTabs // 2 tabs: unix, windows
}
return anthropicTabs
// All other platforms (anthropic, gemini, antigravity) use shell tabs
return shellTabs
})
const platformDescription = computed(() => {
if (props.platform === 'openai') {
return t('keys.useKeyModal.openai.description')
switch (props.platform) {
case 'openai':
return t('keys.useKeyModal.openai.description')
case 'gemini':
return t('keys.useKeyModal.gemini.description')
case 'antigravity':
return t('keys.useKeyModal.antigravity.description')
default:
return t('keys.useKeyModal.description')
}
return t('keys.useKeyModal.description')
})
const platformNote = computed(() => {
if (props.platform === 'openai') {
if (activeTab.value === 'windows') {
return t('keys.useKeyModal.openai.noteWindows')
}
return t('keys.useKeyModal.openai.note')
switch (props.platform) {
case 'openai':
return activeTab.value === 'windows'
? t('keys.useKeyModal.openai.noteWindows')
: t('keys.useKeyModal.openai.note')
case 'gemini':
return t('keys.useKeyModal.gemini.note')
case 'antigravity':
return activeClientTab.value === 'claude'
? t('keys.useKeyModal.antigravity.claudeNote')
: t('keys.useKeyModal.antigravity.geminiNote')
default:
return t('keys.useKeyModal.note')
}
return t('keys.useKeyModal.note')
})
// Syntax highlighting helpers
@@ -239,11 +324,20 @@ const currentFiles = computed((): FileConfig[] => {
const baseUrl = props.baseUrl || window.location.origin
const apiKey = props.apiKey
if (props.platform === 'openai') {
return generateOpenAIFiles(baseUrl, apiKey)
switch (props.platform) {
case 'openai':
return generateOpenAIFiles(baseUrl, apiKey)
case 'gemini':
return [generateGeminiCliContent(baseUrl, apiKey)]
case 'antigravity':
// Both Claude Code and Gemini CLI need /antigravity suffix for antigravity platform
if (activeClientTab.value === 'claude') {
return generateAnthropicFiles(`${baseUrl}/antigravity`, apiKey)
}
return [generateGeminiCliContent(`${baseUrl}/antigravity`, apiKey)]
default: // anthropic
return generateAnthropicFiles(baseUrl, apiKey)
}
return generateAnthropicFiles(baseUrl, apiKey)
})
function generateAnthropicFiles(baseUrl: string, apiKey: string): FileConfig[] {
@@ -282,6 +376,51 @@ ${keyword('$env:')}${variable('ANTHROPIC_AUTH_TOKEN')}${operator('=')}${string(`
return [{ path, content, highlighted }]
}
function generateGeminiCliContent(baseUrl: string, apiKey: string): FileConfig {
const model = 'gemini-2.5-pro'
const modelComment = t('keys.useKeyModal.gemini.modelComment')
let path: string
let content: string
let highlighted: string
switch (activeTab.value) {
case 'unix':
path = 'Terminal'
content = `export GOOGLE_GEMINI_BASE_URL="${baseUrl}"
export GEMINI_API_KEY="${apiKey}"
export GEMINI_MODEL="${model}" # ${modelComment}`
highlighted = `${keyword('export')} ${variable('GOOGLE_GEMINI_BASE_URL')}${operator('=')}${string(`"${baseUrl}"`)}
${keyword('export')} ${variable('GEMINI_API_KEY')}${operator('=')}${string(`"${apiKey}"`)}
${keyword('export')} ${variable('GEMINI_MODEL')}${operator('=')}${string(`"${model}"`)} ${comment(`# ${modelComment}`)}`
break
case 'cmd':
path = 'Command Prompt'
content = `set GOOGLE_GEMINI_BASE_URL=${baseUrl}
set GEMINI_API_KEY=${apiKey}
set GEMINI_MODEL=${model}`
highlighted = `${keyword('set')} ${variable('GOOGLE_GEMINI_BASE_URL')}${operator('=')}${baseUrl}
${keyword('set')} ${variable('GEMINI_API_KEY')}${operator('=')}${apiKey}
${keyword('set')} ${variable('GEMINI_MODEL')}${operator('=')}${model}
${comment(`REM ${modelComment}`)}`
break
case 'powershell':
path = 'PowerShell'
content = `$env:GOOGLE_GEMINI_BASE_URL="${baseUrl}"
$env:GEMINI_API_KEY="${apiKey}"
$env:GEMINI_MODEL="${model}" # ${modelComment}`
highlighted = `${keyword('$env:')}${variable('GOOGLE_GEMINI_BASE_URL')}${operator('=')}${string(`"${baseUrl}"`)}
${keyword('$env:')}${variable('GEMINI_API_KEY')}${operator('=')}${string(`"${apiKey}"`)}
${keyword('$env:')}${variable('GEMINI_MODEL')}${operator('=')}${string(`"${model}"`)} ${comment(`# ${modelComment}`)}`
break
default:
path = 'Terminal'
content = ''
highlighted = ''
}
return { path, content, highlighted }
}
function generateOpenAIFiles(baseUrl: string, apiKey: string): FileConfig[] {
const isWindows = activeTab.value === 'windows'
const configDir = isWindows ? '%userprofile%\\.codex' : '~/.codex'

View File

@@ -321,6 +321,18 @@ export default {
note: 'Make sure the config directory exists. macOS/Linux users can run mkdir -p ~/.codex to create it.',
noteWindows: 'Press Win+R and enter %userprofile%\\.codex to open the config directory. Create it manually if it does not exist.',
},
antigravity: {
description: 'Configure API access for Antigravity group. Select the configuration method based on your client.',
claudeCode: 'Claude Code',
geminiCli: 'Gemini CLI',
claudeNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.',
geminiNote: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.',
},
gemini: {
description: 'Add the following environment variables to your terminal profile or run directly in terminal to configure Gemini CLI access.',
modelComment: 'If you have Gemini 3 access, you can use: gemini-3-pro-preview',
note: 'These environment variables will be active in the current terminal session. For permanent configuration, add them to ~/.bashrc, ~/.zshrc, or the appropriate configuration file.',
},
},
customKeyLabel: 'Custom Key',
customKeyPlaceholder: 'Enter your custom key (min 16 chars)',
@@ -328,7 +340,15 @@ export default {
customKeyTooShort: 'Custom key must be at least 16 characters',
customKeyInvalidChars: 'Custom key can only contain letters, numbers, underscores, and hyphens',
customKeyRequired: 'Please enter a custom key',
ccSwitchNotInstalled: 'CC-Switch is not installed or the protocol handler is not registered. Please install CC-Switch first or manually copy the API key.'
ccSwitchNotInstalled: 'CC-Switch is not installed or the protocol handler is not registered. Please install CC-Switch first or manually copy the API key.',
ccsClientSelect: {
title: 'Select Client',
description: 'Please select the client type to import to CC-Switch:',
claudeCode: 'Claude Code',
claudeCodeDesc: 'Import as Claude Code configuration',
geminiCli: 'Gemini CLI',
geminiCliDesc: 'Import as Gemini CLI configuration',
},
},
// Usage

View File

@@ -317,6 +317,18 @@ export default {
note: '请确保配置目录存在。macOS/Linux 用户可运行 mkdir -p ~/.codex 创建目录。',
noteWindows: '按 Win+R输入 %userprofile%\\.codex 打开配置目录。如目录不存在,请先手动创建。',
},
antigravity: {
description: '为 Antigravity 分组配置 API 访问。请根据您使用的客户端选择对应的配置方式。',
claudeCode: 'Claude Code',
geminiCli: 'Gemini CLI',
claudeNote: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。',
geminiNote: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。',
},
gemini: {
description: '将以下环境变量添加到您的终端配置文件或直接在终端中运行,以配置 Gemini CLI 访问。',
modelComment: '如果你有 Gemini 3 权限可以填gemini-3-pro-preview',
note: '这些环境变量将在当前终端会话中生效。如需永久配置,请将其添加到 ~/.bashrc、~/.zshrc 或相应的配置文件中。',
},
},
customKeyLabel: '自定义密钥',
customKeyPlaceholder: '输入自定义密钥至少16个字符',
@@ -324,7 +336,15 @@ export default {
customKeyTooShort: '自定义密钥至少需要16个字符',
customKeyInvalidChars: '自定义密钥只能包含字母、数字、下划线和连字符',
customKeyRequired: '请输入自定义密钥',
ccSwitchNotInstalled: 'CC-Switch 未安装或协议处理程序未注册。请先安装 CC-Switch 或手动复制 API 密钥。'
ccSwitchNotInstalled: 'CC-Switch 未安装或协议处理程序未注册。请先安装 CC-Switch 或手动复制 API 密钥。',
ccsClientSelect: {
title: '选择客户端',
description: '请选择您要导入到 CC-Switch 的客户端类型:',
claudeCode: 'Claude Code',
claudeCodeDesc: '导入为 Claude Code 配置',
geminiCli: 'Gemini CLI',
geminiCliDesc: '导入为 Gemini CLI 配置',
},
},
// Usage

View File

@@ -173,7 +173,7 @@
</button>
<!-- Import to CC Switch Button -->
<button
@click="importToCcswitch(row.key)"
@click="importToCcswitch(row)"
class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
>
<svg
@@ -453,6 +453,49 @@
@close="closeUseKeyModal"
/>
<!-- CCS Client Selection Dialog for Antigravity -->
<BaseDialog
:show="showCcsClientSelect"
:title="t('keys.ccsClientSelect.title')"
width="narrow"
@close="closeCcsClientSelect"
>
<div class="space-y-4">
<p class="text-sm text-gray-600 dark:text-gray-400">
{{ t('keys.ccsClientSelect.description') }}
</p>
<div class="grid grid-cols-2 gap-3">
<button
@click="handleCcsClientSelect('claude')"
class="flex flex-col items-center gap-2 p-4 rounded-xl border-2 border-gray-200 dark:border-dark-600 hover:border-primary-500 dark:hover:border-primary-500 hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-all"
>
<svg class="w-8 h-8 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 17.25V6.75A2.25 2.25 0 0 0 18.75 4.5H5.25A2.25 2.25 0 0 0 3 6.75v10.5A2.25 2.25 0 0 0 5.25 20.25Z" />
</svg>
<span class="font-medium text-gray-900 dark:text-white">{{ t('keys.ccsClientSelect.claudeCode') }}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('keys.ccsClientSelect.claudeCodeDesc') }}</span>
</button>
<button
@click="handleCcsClientSelect('gemini')"
class="flex flex-col items-center gap-2 p-4 rounded-xl border-2 border-gray-200 dark:border-dark-600 hover:border-primary-500 dark:hover:border-primary-500 hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-all"
>
<svg class="w-8 h-8 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" />
</svg>
<span class="font-medium text-gray-900 dark:text-white">{{ t('keys.ccsClientSelect.geminiCli') }}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('keys.ccsClientSelect.geminiCliDesc') }}</span>
</button>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<button @click="closeCcsClientSelect" class="btn btn-secondary">
{{ t('common.cancel') }}
</button>
</div>
</template>
</BaseDialog>
<!-- Group Selector Dropdown (Teleported to body to avoid overflow clipping) -->
<Teleport to="body">
<div
@@ -563,6 +606,8 @@ const showCreateModal = ref(false)
const showEditModal = ref(false)
const showDeleteDialog = ref(false)
const showUseKeyModal = ref(false)
const showCcsClientSelect = ref(false)
const pendingCcsRow = ref<ApiKey | null>(null)
const selectedKey = ref<ApiKey | null>(null)
const copiedKeyId = ref<number | null>(null)
const groupSelectorKeyId = ref<number | null>(null)
@@ -871,8 +916,48 @@ const closeModals = () => {
}
}
const importToCcswitch = (apiKey: string) => {
const importToCcswitch = (row: ApiKey) => {
const platform = row.group?.platform || 'anthropic'
// For antigravity platform, show client selection dialog
if (platform === 'antigravity') {
pendingCcsRow.value = row
showCcsClientSelect.value = true
return
}
// For other platforms, execute directly
executeCcsImport(row, platform === 'gemini' ? 'gemini' : 'claude')
}
const executeCcsImport = (row: ApiKey, clientType: 'claude' | 'gemini') => {
const baseUrl = publicSettings.value?.api_base_url || window.location.origin
const platform = row.group?.platform || 'anthropic'
// Determine app name and endpoint based on platform and client type
let app: string
let endpoint: string
if (platform === 'antigravity') {
// Antigravity always uses /antigravity suffix
app = clientType === 'gemini' ? 'gemini' : 'claude'
endpoint = `${baseUrl}/antigravity`
} else {
switch (platform) {
case 'openai':
app = 'codex'
endpoint = baseUrl
break
case 'gemini':
app = 'gemini'
endpoint = baseUrl
break
default: // anthropic
app = 'claude'
endpoint = baseUrl
}
}
const usageScript = `({
request: {
url: "{{baseUrl}}/v1/usage",
@@ -889,11 +974,11 @@ const importToCcswitch = (apiKey: string) => {
})`
const params = new URLSearchParams({
resource: 'provider',
app: 'claude',
app: app,
name: 'sub2api',
homepage: baseUrl,
endpoint: baseUrl,
apiKey: apiKey,
endpoint: endpoint,
apiKey: row.key,
configFormat: 'json',
usageEnabled: 'true',
usageScript: btoa(usageScript),
@@ -916,6 +1001,19 @@ const importToCcswitch = (apiKey: string) => {
}
}
const handleCcsClientSelect = (clientType: 'claude' | 'gemini') => {
if (pendingCcsRow.value) {
executeCcsImport(pendingCcsRow.value, clientType)
}
showCcsClientSelect.value = false
pendingCcsRow.value = null
}
const closeCcsClientSelect = () => {
showCcsClientSelect.value = false
pendingCcsRow.value = null
}
onMounted(() => {
loadApiKeys()
loadGroups()