feat: add OpenClaw key usage config

This commit is contained in:
kone
2026-05-12 05:25:28 +08:00
parent 908715ae9b
commit 6461356047
6 changed files with 187 additions and 15 deletions
+119 -4
View File
@@ -275,23 +275,27 @@ const clientTabs = computed((): TabConfig[] => {
tabs.push({ id: 'claude', label: t('keys.useKeyModal.cliTabs.claudeCode'), icon: TerminalIcon })
}
tabs.push({ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon })
tabs.push({ id: 'openclaw', label: t('keys.useKeyModal.cliTabs.openclaw'), icon: TerminalIcon })
return tabs
}
case 'gemini':
return [
{ id: 'gemini', label: t('keys.useKeyModal.cliTabs.geminiCli'), icon: SparkleIcon },
{ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon }
{ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon },
{ id: 'openclaw', label: t('keys.useKeyModal.cliTabs.openclaw'), icon: TerminalIcon }
]
case 'antigravity':
return [
{ id: 'claude', label: t('keys.useKeyModal.cliTabs.claudeCode'), icon: TerminalIcon },
{ id: 'gemini', label: t('keys.useKeyModal.cliTabs.geminiCli'), icon: SparkleIcon },
{ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon }
{ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon },
{ id: 'openclaw', label: t('keys.useKeyModal.cliTabs.openclaw'), icon: TerminalIcon }
]
default:
return [
{ id: 'claude', label: t('keys.useKeyModal.cliTabs.claudeCode'), icon: TerminalIcon },
{ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon }
{ id: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon },
{ id: 'openclaw', label: t('keys.useKeyModal.cliTabs.openclaw'), icon: TerminalIcon }
]
}
})
@@ -309,7 +313,7 @@ const openaiTabs: TabConfig[] = [
{ id: 'windows', label: 'Windows', icon: WindowsIcon }
]
const showShellTabs = computed(() => activeClientTab.value !== 'opencode')
const showShellTabs = computed(() => activeClientTab.value !== 'opencode' && activeClientTab.value !== 'openclaw')
const currentTabs = computed(() => {
if (!showShellTabs.value) return []
@@ -412,6 +416,21 @@ const currentFiles = computed((): FileConfig[] => {
}
}
if (activeClientTab.value === 'openclaw') {
switch (props.platform) {
case 'anthropic':
return [generateOpenClawConfig('anthropic', apiBase, apiKey)]
case 'openai':
return [generateOpenClawConfig('openai', apiBase, apiKey)]
case 'gemini':
return [generateOpenClawConfig('gemini', geminiBase, apiKey)]
case 'antigravity':
return [generateOpenClawConfig('antigravity', antigravityBase, apiKey, antigravityGeminiBase)]
default:
return [generateOpenClawConfig('anthropic', apiBase, apiKey)]
}
}
switch (props.platform) {
case 'openai':
if (activeClientTab.value === 'claude') {
@@ -1041,6 +1060,102 @@ function generateOpenCodeConfig(platform: string, baseUrl: string, apiKey: strin
}
}
function generateOpenClawConfig(platform: GroupPlatform, baseUrl: string, apiKey: string, geminiBaseUrl?: string): FileConfig {
const providerId = `sub2api-${platform}`
const env = {
SUB2API_API_KEY: apiKey
}
let primaryModel = ''
const providers: Record<string, any> = {}
if (platform === 'openai') {
primaryModel = `${providerId}/gpt-5.4`
providers[providerId] = {
baseUrl,
apiKey: '${SUB2API_API_KEY}',
api: 'openai-responses',
models: [
{ id: 'gpt-5.4', name: 'GPT-5.4' },
{ id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini' },
{ id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex' },
{ id: 'gpt-5.3-codex-spark', name: 'GPT-5.3 Codex Spark' }
]
}
} else if (platform === 'gemini') {
primaryModel = `${providerId}/gemini-2.5-pro`
providers[providerId] = {
baseUrl,
apiKey: '${SUB2API_API_KEY}',
api: 'google-generative-ai',
models: [
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro' },
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' },
{ id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro Preview' },
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview' }
]
}
} else if (platform === 'antigravity') {
const claudeProviderId = 'sub2api-antigravity-claude'
const geminiProviderId = 'sub2api-antigravity-gemini'
primaryModel = `${claudeProviderId}/claude-sonnet-4-6`
providers[claudeProviderId] = {
baseUrl,
apiKey: '${SUB2API_API_KEY}',
api: 'anthropic-messages',
models: [
{ id: 'claude-sonnet-4-6', name: 'Claude 4.6 Sonnet' },
{ id: 'claude-opus-4-6-thinking', name: 'Claude 4.6 Opus (Thinking)' }
]
}
providers[geminiProviderId] = {
baseUrl: geminiBaseUrl ?? baseUrl,
apiKey: '${SUB2API_API_KEY}',
api: 'google-generative-ai',
models: [
{ id: 'gemini-3.1-pro-high', name: 'Gemini 3.1 Pro High' },
{ id: 'gemini-3.1-pro-low', name: 'Gemini 3.1 Pro Low' },
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' }
]
}
} else {
primaryModel = `${providerId}/claude-sonnet-4-6`
providers[providerId] = {
baseUrl,
apiKey: '${SUB2API_API_KEY}',
api: 'anthropic-messages',
models: [
{ id: 'claude-sonnet-4-6', name: 'Claude 4.6 Sonnet' },
{ id: 'claude-opus-4-6-thinking', name: 'Claude 4.6 Opus (Thinking)' }
]
}
}
const content = JSON.stringify(
{
env,
agents: {
defaults: {
model: {
primary: primaryModel
}
}
},
models: {
mode: 'merge',
providers
}
},
null,
2
)
return {
path: '~/.openclaw/openclaw.json',
content,
hint: t('keys.useKeyModal.openclaw.hint')
}
}
const copyContent = async (content: string, index: number) => {
const success = await clipboardCopy(content, t('keys.copied'))
if (success) {
@@ -50,4 +50,53 @@ describe('UseKeyModal', () => {
expect(codeBlock.text()).toContain('"name": "GPT-5.4 Mini"')
expect(codeBlock.text()).not.toContain('"name": "GPT-5.4 Nano"')
})
it.each(['anthropic', 'openai', 'gemini', 'antigravity'] as const)(
'renders OpenClaw config for %s keys',
async (platform) => {
const wrapper = mount(UseKeyModal, {
props: {
show: true,
apiKey: 'sk-test',
baseUrl: 'https://example.com/v1',
platform
},
global: {
stubs: {
BaseDialog: {
template: '<div><slot /><slot name="footer" /></div>'
},
Icon: {
template: '<span />'
}
}
}
})
const openclawTab = wrapper.findAll('button').find((button) =>
button.text().includes('keys.useKeyModal.cliTabs.openclaw')
)
expect(openclawTab).toBeDefined()
await openclawTab!.trigger('click')
await nextTick()
expect(wrapper.text()).toContain('~/.openclaw/openclaw.json')
const codeBlock = wrapper.find('pre code')
expect(codeBlock.exists()).toBe(true)
expect(codeBlock.text()).toContain('"models":')
if (platform === 'openai') {
expect(codeBlock.text()).toContain('"api": "openai-responses"')
} else if (platform === 'gemini') {
expect(codeBlock.text()).toContain('"api": "google-generative-ai"')
} else if (platform === 'antigravity') {
expect(codeBlock.text()).toContain('sub2api-antigravity-claude')
expect(codeBlock.text()).toContain('sub2api-antigravity-gemini')
} else {
expect(codeBlock.text()).toContain('"api": "anthropic-messages"')
}
}
)
})
+4
View File
@@ -705,6 +705,7 @@ export default {
codexCli: 'Codex CLI',
codexCliWs: 'Codex CLI (WebSocket)',
opencode: 'OpenCode',
openclaw: 'OpenClaw',
},
antigravity: {
description: 'Configure API access for Antigravity group. Select the configuration method based on your client.',
@@ -723,6 +724,9 @@ export default {
subtitle: 'opencode.json',
hint: 'Config path: ~/.config/opencode/opencode.json (or opencode.jsonc), create if not exists. Use default providers (openai/anthropic/google) or custom provider_id. API Key can be configured directly or via /connect command. This is an example, adjust models and options as needed.',
},
openclaw: {
hint: 'Config path: ~/.openclaw/openclaw.json, create if it does not exist. This example is generated for the current key group platform; adjust models and options as needed.',
},
},
customKeyLabel: 'Custom Key',
customKeyPlaceholder: 'Enter your custom key (min 16 chars)',
+5 -1
View File
@@ -704,7 +704,8 @@ export default {
geminiCli: 'Gemini CLI',
codexCli: 'Codex CLI',
codexCliWs: 'Codex CLI (WebSocket)',
opencode: 'OpenCode'
opencode: 'OpenCode',
openclaw: 'OpenClaw'
},
antigravity: {
description: '为 Antigravity 分组配置 API 访问。请根据您使用的客户端选择对应的配置方式。',
@@ -725,6 +726,9 @@ export default {
title: 'OpenCode 配置示例',
subtitle: 'opencode.json',
hint: '配置文件路径:~/.config/opencode/opencode.json(或 opencode.jsonc),不存在需手动创建。可使用默认 provideropenai/anthropic/google)或自定义 provider_id。API Key 支持直接配置或通过客户端 /connect 命令配置。示例仅供参考,模型与选项可按需调整。'
},
openclaw: {
hint: '配置文件路径:~/.openclaw/openclaw.json,不存在需手动创建。配置按当前密钥所属分组生成,模型与选项可按需调整。'
}
},
customKeyLabel: '自定义密钥',
+9 -9
View File
@@ -9035,7 +9035,7 @@ watch(
}
.settings-tab {
@apply relative isolate flex h-10 min-w-[6.75rem] shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-xl border border-transparent px-3 text-sm font-medium text-gray-600 outline-none transition-colors duration-200 ease-out dark:text-gray-300;
@apply relative isolate flex h-10 min-w-[6.75rem] shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-xl border border-transparent px-3 text-sm font-medium text-slate-700 outline-none transition-colors duration-200 ease-out dark:text-slate-300;
}
@media (min-width: 768px) {
@@ -9055,7 +9055,7 @@ watch(
.settings-tab::before {
@apply absolute inset-0 -z-10 rounded-xl opacity-0 transition-opacity duration-200;
content: "";
background: linear-gradient(135deg, rgb(248 250 252 / 0.95), rgb(241 245 249 / 0.8));
background: linear-gradient(135deg, rgb(241 245 249 / 0.98), rgb(226 232 240 / 0.9));
}
.settings-tab:hover::before,
@@ -9064,7 +9064,7 @@ watch(
}
:global(.dark) .settings-tab::before {
background: linear-gradient(135deg, rgb(30 41 59 / 0.9), rgb(51 65 85 / 0.62));
background: linear-gradient(135deg, rgb(30 41 59 / 0.96), rgb(51 65 85 / 0.82));
}
.settings-tab:focus-visible {
@@ -9072,16 +9072,16 @@ watch(
}
.settings-tab-active {
@apply border-primary-200/80 bg-white text-primary-700 shadow-sm dark:border-primary-400/30 dark:bg-dark-700/95 dark:text-primary-200;
@apply border-primary-500/30 bg-primary-600 text-white shadow-sm dark:border-primary-400/30 dark:bg-primary-500 dark:text-white;
box-shadow:
0 8px 18px rgb(15 23 42 / 0.08),
0 1px 0 rgb(255 255 255 / 0.92) inset;
0 10px 22px rgb(2 132 199 / 0.26),
0 1px 0 rgb(255 255 255 / 0.18) inset;
}
:global(.dark) .settings-tab-active {
box-shadow:
0 12px 26px rgb(0 0 0 / 0.22),
0 1px 0 rgb(255 255 255 / 0.08) inset;
0 12px 26px rgb(2 132 199 / 0.28),
0 1px 0 rgb(255 255 255 / 0.1) inset;
}
.settings-tab-active::before {
@@ -9109,7 +9109,7 @@ watch(
}
.settings-tab-active .settings-tab-icon {
@apply bg-primary-50 text-primary-600 dark:bg-primary-400/10 dark:text-primary-300;
@apply bg-white/15 text-white dark:bg-white/15 dark:text-white;
}
.settings-tab-label {