From 3501d377f61b8b136b1de71c6f3b52dd1ac3df89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=A2=E5=A5=8B=E7=8C=AB?= Date: Fri, 22 Aug 2025 12:42:27 +0800 Subject: [PATCH] =?UTF-8?q?refactor(CodeToolsPage):=20streamline=20CLI=20t?= =?UTF-8?q?ool=20management=20and=20enhance=20p=E2=80=A6=20(#9386)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- src/renderer/src/pages/code/CodeToolsPage.tsx | 199 ++++++------------ src/renderer/src/pages/code/index.ts | 134 ++++++++++++ 2 files changed, 198 insertions(+), 135 deletions(-) diff --git a/src/renderer/src/pages/code/CodeToolsPage.tsx b/src/renderer/src/pages/code/CodeToolsPage.tsx index 115dab30ef..2e6c87b7ca 100644 --- a/src/renderer/src/pages/code/CodeToolsPage.tsx +++ b/src/renderer/src/pages/code/CodeToolsPage.tsx @@ -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) => { - 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 | 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 = {} - 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) => { + 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} /> @@ -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 = () => { ) }))} /> - @@ -377,7 +306,7 @@ const CodeToolsPage: FC = () => { setEnvVars(e.target.value)} rows={2} style={{ fontFamily: 'monospace' }} /> diff --git a/src/renderer/src/pages/code/index.ts b/src/renderer/src/pages/code/index.ts index 434a3f0a45..c94e8bc867 100644 --- a/src/renderer/src/pages/code/index.ts +++ b/src/renderer/src/pages/code/index.ts @@ -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 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 => { + const env: Record = {} + 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 => { + const env: Record = {} + + 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'