refactor(CodeToolsPage): streamline CLI tool management and enhance p… (#9386)

* refactor(CodeToolsPage): streamline CLI tool management and enhance provider filtering logic

- Removed hardcoded CLI tool options and supported providers, replacing them with imported constants for better maintainability.
- Optimized provider filtering to include additional providers for Claude and Gemini tools.
- Updated environment variable handling for CLI tools to utilize a centralized API base URL function.

* refactor(CodeToolsPage): enhance CLI tool management and environment variable handling

- Updated provider filtering logic to utilize a centralized mapping for CLI tools, improving maintainability and extensibility.
- Refactored environment variable generation and parsing to streamline the launch process for different CLI tools.
- Simplified state management for tool selection and directory handling, enhancing code clarity.
This commit is contained in:
亢奋猫 2025-08-22 12:42:27 +08:00 committed by GitHub
parent b4a3a483e9
commit 3501d377f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 198 additions and 135 deletions

View File

@ -11,22 +11,19 @@ import { getModelUniqId } from '@renderer/services/ModelService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsBunInstalled } from '@renderer/store/mcp'
import { Model } from '@renderer/types'
import { codeTools } from '@shared/config/constant'
import { Alert, Button, Checkbox, Input, Select, Space } from 'antd'
import { Download, Terminal, X } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
// CLI 工具选项
const CLI_TOOLS = [
{ value: codeTools.qwenCode, label: 'Qwen Code' },
{ value: codeTools.claudeCode, label: 'Claude Code' },
{ value: codeTools.geminiCli, label: 'Gemini CLI' },
{ value: codeTools.openaiCodex, label: 'OpenAI Codex' }
]
const SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
import {
CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS,
CLI_TOOL_PROVIDER_MAP,
CLI_TOOLS,
generateToolEnvironment,
parseEnvironmentVariables
} from '.'
const logger = loggerService.withContext('CodeToolsPage')
@ -51,25 +48,17 @@ const CodeToolsPage: FC = () => {
} = useCodeTools()
const { setTimeoutTimer } = useTimer()
// 状态管理
const [isLaunching, setIsLaunching] = useState(false)
const [isInstallingBun, setIsInstallingBun] = useState(false)
const [autoUpdateToLatest, setAutoUpdateToLatest] = useState(false)
const handleCliToolChange = (value: codeTools) => setCliTool(value)
const openAiCompatibleProviders = providers.filter((p) => p.type.includes('openai'))
const openAiProviders = providers.filter((p) => p.id === 'openai')
const geminiProviders = providers.filter((p) => p.type === 'gemini' || SUPPORTED_PROVIDERS.includes(p.id))
const claudeProviders = providers.filter((p) => p.type === 'anthropic' || SUPPORTED_PROVIDERS.includes(p.id))
const modelPredicate = useCallback(
(m: Model) => {
if (isEmbeddingModel(m) || isRerankModel(m) || isTextToImageModel(m)) {
return false
}
if (selectedCliTool === 'claude-code') {
return m.id.includes('claude')
return m.id.includes('claude') || CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS.includes(m.provider)
}
if (selectedCliTool === 'gemini-cli') {
return m.id.includes('gemini')
@ -80,20 +69,9 @@ const CodeToolsPage: FC = () => {
)
const availableProviders = useMemo(() => {
if (selectedCliTool === codeTools.claudeCode) {
return claudeProviders
}
if (selectedCliTool === codeTools.geminiCli) {
return geminiProviders
}
if (selectedCliTool === codeTools.qwenCode) {
return openAiCompatibleProviders
}
if (selectedCliTool === codeTools.openaiCodex) {
return openAiProviders
}
return []
}, [claudeProviders, geminiProviders, openAiCompatibleProviders, openAiProviders, selectedCliTool])
const filterFn = CLI_TOOL_PROVIDER_MAP[selectedCliTool]
return filterFn ? filterFn(providers) : []
}, [providers, selectedCliTool])
const handleModelChange = (value: string) => {
if (!value) {
@ -111,25 +89,6 @@ const CodeToolsPage: FC = () => {
}
}
// 处理环境变量更改
const handleEnvVarsChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setEnvVars(e.target.value)
}
// 处理文件夹选择
const handleFolderSelect = async () => {
try {
await selectFolder()
} catch (error) {
logger.error('选择文件夹失败:', error as Error)
}
}
// 处理目录选择
const handleDirectoryChange = (value: string) => {
setCurrentDir(value)
}
// 处理删除目录
const handleRemoveDirectory = (directory: string, e: React.MouseEvent) => {
e.stopPropagation()
@ -170,104 +129,74 @@ const CodeToolsPage: FC = () => {
}
}
// 处理启动
const handleLaunch = async () => {
// 验证启动条件
const validateLaunch = (): { isValid: boolean; message?: string } => {
if (!canLaunch || !isBunInstalled) {
if (!isBunInstalled) {
window.message.warning({
content: t('code.launch.bun_required'),
key: 'code-launch-message'
})
} else {
window.message.warning({
content: t('code.launch.validation_error'),
key: 'code-launch-message'
})
return {
isValid: false,
message: !isBunInstalled ? t('code.launch.bun_required') : t('code.launch.validation_error')
}
return
}
setIsLaunching(true)
if (!selectedModel) {
window.message.error({
content: t('code.model_required'),
key: 'code-launch-message'
})
return
return { isValid: false, message: t('code.model_required') }
}
return { isValid: true }
}
// 准备启动环境
const prepareLaunchEnvironment = async (): Promise<Record<string, string> | null> => {
if (!selectedModel) return null
const modelProvider = getProviderByModel(selectedModel)
const aiProvider = new AiProvider(modelProvider)
const baseUrl = await aiProvider.getBaseURL()
const apiKey = await aiProvider.getApiKey()
let env: Record<string, string> = {}
if (selectedCliTool === codeTools.claudeCode) {
env = {
ANTHROPIC_API_KEY: apiKey,
ANTHROPIC_BASE_URL: modelProvider.apiHost,
ANTHROPIC_MODEL: selectedModel.id
}
// 生成工具特定的环境变量
const toolEnv = generateToolEnvironment({
tool: selectedCliTool,
model: selectedModel,
modelProvider,
apiKey,
baseUrl
})
// 合并用户自定义的环境变量
const userEnv = parseEnvironmentVariables(environmentVariables)
return { ...toolEnv, ...userEnv }
}
// 执行启动操作
const executeLaunch = async (env: Record<string, string>) => {
window.api.codeTools.run(selectedCliTool, selectedModel?.id!, currentDirectory, env, { autoUpdateToLatest })
window.message.success({ content: t('code.launch.success'), key: 'code-launch-message' })
}
// 处理启动
const handleLaunch = async () => {
const validation = validateLaunch()
if (!validation.isValid) {
window.message.warning({ content: validation.message, key: 'code-launch-message' })
return
}
if (selectedCliTool === codeTools.geminiCli) {
const apiSuffix = modelProvider.id === 'aihubmix' ? '/gemini' : ''
const apiBaseUrl = modelProvider.apiHost + apiSuffix
env = {
GEMINI_API_KEY: apiKey,
GEMINI_BASE_URL: apiBaseUrl,
GOOGLE_GEMINI_BASE_URL: apiBaseUrl,
GEMINI_MODEL: selectedModel.id
}
}
if (selectedCliTool === codeTools.qwenCode || selectedCliTool === codeTools.openaiCodex) {
env = {
OPENAI_API_KEY: apiKey,
OPENAI_BASE_URL: baseUrl,
OPENAI_MODEL: selectedModel.id
}
}
// 解析用户自定义的环境变量
if (environmentVariables) {
const lines = environmentVariables.split('\n')
for (const line of lines) {
const trimmedLine = line.trim()
if (trimmedLine && trimmedLine.includes('=')) {
const [key, ...valueParts] = trimmedLine.split('=')
const trimmedKey = key.trim()
const value = valueParts.join('=').trim()
if (trimmedKey) {
env[trimmedKey] = value
}
}
}
}
setIsLaunching(true)
try {
// 这里可以添加实际的启动逻辑
logger.info('启动配置:', {
cliTool: selectedCliTool,
model: selectedModel,
folder: currentDirectory
})
const env = await prepareLaunchEnvironment()
if (!env) {
window.message.error({ content: t('code.model_required'), key: 'code-launch-message' })
return
}
window.api.codeTools.run(selectedCliTool, selectedModel?.id, currentDirectory, env, {
autoUpdateToLatest
})
window.message.success({
content: t('code.launch.success'),
key: 'code-launch-message'
})
await executeLaunch(env)
} catch (error) {
logger.error('启动失败:', error as Error)
window.message.error({
content: t('code.launch.error'),
key: 'code-launch-message'
})
window.message.error({ content: t('code.launch.error'), key: 'code-launch-message' })
} finally {
setIsLaunching(false)
}
@ -320,7 +249,7 @@ const CodeToolsPage: FC = () => {
style={{ width: '100%' }}
placeholder={t('code.cli_tool_placeholder')}
value={selectedCliTool}
onChange={handleCliToolChange}
onChange={setCliTool}
options={CLI_TOOLS}
/>
</SettingsItem>
@ -345,7 +274,7 @@ const CodeToolsPage: FC = () => {
style={{ flex: 1, width: 480 }}
placeholder={t('code.folder_placeholder')}
value={currentDirectory || undefined}
onChange={handleDirectoryChange}
onChange={setCurrentDir}
allowClear
showSearch
filterOption={(input, option) => {
@ -366,7 +295,7 @@ const CodeToolsPage: FC = () => {
)
}))}
/>
<Button onClick={handleFolderSelect} style={{ width: 120 }}>
<Button onClick={selectFolder} style={{ width: 120 }}>
{t('code.select_folder')}
</Button>
</Space.Compact>
@ -377,7 +306,7 @@ const CodeToolsPage: FC = () => {
<Input.TextArea
placeholder={`KEY1=value1\nKEY2=value2`}
value={environmentVariables}
onChange={handleEnvVarsChange}
onChange={(e) => setEnvVars(e.target.value)}
rows={2}
style={{ fontFamily: 'monospace' }}
/>

View File

@ -1 +1,135 @@
import { EndpointType, Model, Provider } from '@renderer/types'
import { codeTools } from '@shared/config/constant'
export interface LaunchValidationResult {
isValid: boolean
message?: string
}
export interface ToolEnvironmentConfig {
tool: codeTools
model: Model
modelProvider: Provider
apiKey: string
baseUrl: string
}
// CLI 工具选项
export const CLI_TOOLS = [
{ value: codeTools.qwenCode, label: 'Qwen Code' },
{ value: codeTools.claudeCode, label: 'Claude Code' },
{ value: codeTools.geminiCli, label: 'Gemini CLI' },
{ value: codeTools.openaiCodex, label: 'OpenAI Codex' }
]
export const GEMINI_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api']
export const CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS = ['deepseek', 'moonshot', 'zhipu']
export const CLAUDE_SUPPORTED_PROVIDERS = ['aihubmix', 'dmxapi', 'new-api', ...CLAUDE_OFFICIAL_SUPPORTED_PROVIDERS]
// Provider 过滤映射
export const CLI_TOOL_PROVIDER_MAP: Record<string, (providers: Provider[]) => Provider[]> = {
[codeTools.claudeCode]: (providers) =>
providers.filter((p) => p.type === 'anthropic' || CLAUDE_SUPPORTED_PROVIDERS.includes(p.id)),
[codeTools.geminiCli]: (providers) =>
providers.filter((p) => p.type === 'gemini' || GEMINI_SUPPORTED_PROVIDERS.includes(p.id)),
[codeTools.qwenCode]: (providers) => providers.filter((p) => p.type.includes('openai')),
[codeTools.openaiCodex]: (providers) => providers.filter((p) => p.id === 'openai')
}
export const getCodeToolsApiBaseUrl = (model: Model, type: EndpointType) => {
const CODE_TOOLS_API_ENDPOINTS = {
aihubmix: {
gemini: {
api_base_url: 'https://api.aihubmix.com/gemini'
}
},
deepseek: {
anthropic: {
api_base_url: 'https://api.deepseek.com/anthropic'
}
},
moonshot: {
anthropic: {
api_base_url: 'https://api.moonshot.cn/anthropic'
}
},
zhipu: {
anthropic: {
api_base_url: 'https://open.bigmodel.cn/api/anthropic'
}
}
}
const provider = model.provider
return CODE_TOOLS_API_ENDPOINTS[provider]?.[type]?.api_base_url
}
// 解析环境变量字符串为对象
export const parseEnvironmentVariables = (envVars: string): Record<string, string> => {
const env: Record<string, string> = {}
if (!envVars) return env
const lines = envVars.split('\n')
for (const line of lines) {
const trimmedLine = line.trim()
if (trimmedLine && trimmedLine.includes('=')) {
const [key, ...valueParts] = trimmedLine.split('=')
const trimmedKey = key.trim()
const value = valueParts.join('=').trim()
if (trimmedKey) {
env[trimmedKey] = value
}
}
}
return env
}
// 为不同 CLI 工具生成环境变量配置
export const generateToolEnvironment = ({
tool,
model,
modelProvider,
apiKey,
baseUrl
}: {
tool: codeTools
model: Model
modelProvider: Provider
apiKey: string
baseUrl: string
}): Record<string, string> => {
const env: Record<string, string> = {}
switch (tool) {
case codeTools.claudeCode:
env.ANTHROPIC_BASE_URL = getCodeToolsApiBaseUrl(model, 'anthropic') || modelProvider.apiHost
env.ANTHROPIC_MODEL = model.id
if (modelProvider.type === 'anthropic') {
env.ANTHROPIC_API_KEY = apiKey
} else {
env.ANTHROPIC_AUTH_TOKEN = apiKey
}
break
case codeTools.geminiCli: {
const apiBaseUrl = getCodeToolsApiBaseUrl(model, 'gemini') || modelProvider.apiHost
env.GEMINI_API_KEY = apiKey
env.GEMINI_BASE_URL = apiBaseUrl
env.GOOGLE_GEMINI_BASE_URL = apiBaseUrl
env.GEMINI_MODEL = model.id
break
}
case codeTools.qwenCode:
case codeTools.openaiCodex:
env.OPENAI_API_KEY = apiKey
env.OPENAI_BASE_URL = baseUrl
env.OPENAI_MODEL = model.id
break
}
return env
}
export { default } from './CodeToolsPage'