feat: add OpenClaw key usage config
This commit is contained in:
@@ -1 +1 @@
|
|||||||
0.1.128
|
0.1.129
|
||||||
|
|||||||
@@ -275,23 +275,27 @@ const clientTabs = computed((): TabConfig[] => {
|
|||||||
tabs.push({ id: 'claude', label: t('keys.useKeyModal.cliTabs.claudeCode'), icon: TerminalIcon })
|
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: 'opencode', label: t('keys.useKeyModal.cliTabs.opencode'), icon: TerminalIcon })
|
||||||
|
tabs.push({ id: 'openclaw', label: t('keys.useKeyModal.cliTabs.openclaw'), icon: TerminalIcon })
|
||||||
return tabs
|
return tabs
|
||||||
}
|
}
|
||||||
case 'gemini':
|
case 'gemini':
|
||||||
return [
|
return [
|
||||||
{ id: 'gemini', label: t('keys.useKeyModal.cliTabs.geminiCli'), icon: SparkleIcon },
|
{ 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':
|
case 'antigravity':
|
||||||
return [
|
return [
|
||||||
{ id: 'claude', label: t('keys.useKeyModal.cliTabs.claudeCode'), icon: TerminalIcon },
|
{ id: 'claude', label: t('keys.useKeyModal.cliTabs.claudeCode'), icon: TerminalIcon },
|
||||||
{ id: 'gemini', label: t('keys.useKeyModal.cliTabs.geminiCli'), icon: SparkleIcon },
|
{ 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:
|
default:
|
||||||
return [
|
return [
|
||||||
{ id: 'claude', label: t('keys.useKeyModal.cliTabs.claudeCode'), icon: TerminalIcon },
|
{ 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 }
|
{ id: 'windows', label: 'Windows', icon: WindowsIcon }
|
||||||
]
|
]
|
||||||
|
|
||||||
const showShellTabs = computed(() => activeClientTab.value !== 'opencode')
|
const showShellTabs = computed(() => activeClientTab.value !== 'opencode' && activeClientTab.value !== 'openclaw')
|
||||||
|
|
||||||
const currentTabs = computed(() => {
|
const currentTabs = computed(() => {
|
||||||
if (!showShellTabs.value) return []
|
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) {
|
switch (props.platform) {
|
||||||
case 'openai':
|
case 'openai':
|
||||||
if (activeClientTab.value === 'claude') {
|
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 copyContent = async (content: string, index: number) => {
|
||||||
const success = await clipboardCopy(content, t('keys.copied'))
|
const success = await clipboardCopy(content, t('keys.copied'))
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|||||||
@@ -50,4 +50,53 @@ describe('UseKeyModal', () => {
|
|||||||
expect(codeBlock.text()).toContain('"name": "GPT-5.4 Mini"')
|
expect(codeBlock.text()).toContain('"name": "GPT-5.4 Mini"')
|
||||||
expect(codeBlock.text()).not.toContain('"name": "GPT-5.4 Nano"')
|
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"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -705,6 +705,7 @@ export default {
|
|||||||
codexCli: 'Codex CLI',
|
codexCli: 'Codex CLI',
|
||||||
codexCliWs: 'Codex CLI (WebSocket)',
|
codexCliWs: 'Codex CLI (WebSocket)',
|
||||||
opencode: 'OpenCode',
|
opencode: 'OpenCode',
|
||||||
|
openclaw: 'OpenClaw',
|
||||||
},
|
},
|
||||||
antigravity: {
|
antigravity: {
|
||||||
description: 'Configure API access for Antigravity group. Select the configuration method based on your client.',
|
description: 'Configure API access for Antigravity group. Select the configuration method based on your client.',
|
||||||
@@ -723,6 +724,9 @@ export default {
|
|||||||
subtitle: 'opencode.json',
|
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.',
|
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',
|
customKeyLabel: 'Custom Key',
|
||||||
customKeyPlaceholder: 'Enter your custom key (min 16 chars)',
|
customKeyPlaceholder: 'Enter your custom key (min 16 chars)',
|
||||||
|
|||||||
@@ -704,7 +704,8 @@ export default {
|
|||||||
geminiCli: 'Gemini CLI',
|
geminiCli: 'Gemini CLI',
|
||||||
codexCli: 'Codex CLI',
|
codexCli: 'Codex CLI',
|
||||||
codexCliWs: 'Codex CLI (WebSocket)',
|
codexCliWs: 'Codex CLI (WebSocket)',
|
||||||
opencode: 'OpenCode'
|
opencode: 'OpenCode',
|
||||||
|
openclaw: 'OpenClaw'
|
||||||
},
|
},
|
||||||
antigravity: {
|
antigravity: {
|
||||||
description: '为 Antigravity 分组配置 API 访问。请根据您使用的客户端选择对应的配置方式。',
|
description: '为 Antigravity 分组配置 API 访问。请根据您使用的客户端选择对应的配置方式。',
|
||||||
@@ -725,6 +726,9 @@ export default {
|
|||||||
title: 'OpenCode 配置示例',
|
title: 'OpenCode 配置示例',
|
||||||
subtitle: 'opencode.json',
|
subtitle: 'opencode.json',
|
||||||
hint: '配置文件路径:~/.config/opencode/opencode.json(或 opencode.jsonc),不存在需手动创建。可使用默认 provider(openai/anthropic/google)或自定义 provider_id。API Key 支持直接配置或通过客户端 /connect 命令配置。示例仅供参考,模型与选项可按需调整。'
|
hint: '配置文件路径:~/.config/opencode/opencode.json(或 opencode.jsonc),不存在需手动创建。可使用默认 provider(openai/anthropic/google)或自定义 provider_id。API Key 支持直接配置或通过客户端 /connect 命令配置。示例仅供参考,模型与选项可按需调整。'
|
||||||
|
},
|
||||||
|
openclaw: {
|
||||||
|
hint: '配置文件路径:~/.openclaw/openclaw.json,不存在需手动创建。配置按当前密钥所属分组生成,模型与选项可按需调整。'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
customKeyLabel: '自定义密钥',
|
customKeyLabel: '自定义密钥',
|
||||||
|
|||||||
@@ -9035,7 +9035,7 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab {
|
.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) {
|
@media (min-width: 768px) {
|
||||||
@@ -9055,7 +9055,7 @@ watch(
|
|||||||
.settings-tab::before {
|
.settings-tab::before {
|
||||||
@apply absolute inset-0 -z-10 rounded-xl opacity-0 transition-opacity duration-200;
|
@apply absolute inset-0 -z-10 rounded-xl opacity-0 transition-opacity duration-200;
|
||||||
content: "";
|
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,
|
.settings-tab:hover::before,
|
||||||
@@ -9064,7 +9064,7 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .settings-tab::before {
|
: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 {
|
.settings-tab:focus-visible {
|
||||||
@@ -9072,16 +9072,16 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab-active {
|
.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:
|
box-shadow:
|
||||||
0 8px 18px rgb(15 23 42 / 0.08),
|
0 10px 22px rgb(2 132 199 / 0.26),
|
||||||
0 1px 0 rgb(255 255 255 / 0.92) inset;
|
0 1px 0 rgb(255 255 255 / 0.18) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .settings-tab-active {
|
:global(.dark) .settings-tab-active {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 12px 26px rgb(0 0 0 / 0.22),
|
0 12px 26px rgb(2 132 199 / 0.28),
|
||||||
0 1px 0 rgb(255 255 255 / 0.08) inset;
|
0 1px 0 rgb(255 255 255 / 0.1) inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab-active::before {
|
.settings-tab-active::before {
|
||||||
@@ -9109,7 +9109,7 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab-active .settings-tab-icon {
|
.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 {
|
.settings-tab-label {
|
||||||
|
|||||||
Reference in New Issue
Block a user