1 line
40 KiB
JSON
1 line
40 KiB
JSON
{"repo": "alexxxcoelho/ccswitcher", "files": {"src/lib/wizard.js": {"bac64abbae6f101b8439f98b74f9601566f21d9c": {"t": 1785746009.2039893, "n": 39458, "h": "b6c47214c7b2b862", "c": "import inquirer from 'inquirer';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport terminalLink from 'terminal-link';\nimport { configManager } from './config.js';\nimport { toolManager, SUPPORTED_TOOLS } from './tool-manager.js';\nimport { mcpManager } from './mcp-manager.js';\nimport { claudeCodeManager } from './claude-code-manager.js';\nimport { i18n } from './i18n.js';\nimport { createBorderLine, createContentLine } from '../utils/string-width.js';\nimport { execSync } from 'child_process';\nimport { validateApiKey } from './api-validator.js';\nexport class Wizard {\n static instance;\n BOX_WIDTH = 63; // Default box width for UI elements\n constructor() { }\n static getInstance() {\n if (!Wizard.instance) {\n Wizard.instance = new Wizard();\n }\n return Wizard.instance;\n }\n /**\n * Create a simple box with title using double-line border style\n */\n createBox(title) {\n console.log(chalk.cyan.bold('\\n' + createBorderLine('╔', '╗', '═', this.BOX_WIDTH)));\n console.log(chalk.cyan.bold(createContentLine(title, '║', '║', this.BOX_WIDTH, 'center')));\n console.log(chalk.cyan.bold(createBorderLine('╚', '╝', '═', this.BOX_WIDTH)));\n console.log('');\n }\n /**\n * Display operation hints\n */\n showOperationHints() {\n const hints = [\n chalk.gray(i18n.t('wizard.hint_navigate')),\n chalk.gray(i18n.t('wizard.hint_confirm'))\n ];\n console.log(chalk.gray('💡 ') + hints.join(chalk.gray(' | ')) + '\\n');\n }\n /**\n * Prompt wrapper that shows operation hints\n */\n async promptWithHints(questions) {\n this.showOperationHints();\n return inquirer.prompt(questions);\n }\n printBanner() {\n const BANNER_WIDTH = 75; \n \n const logoLines = [\n ' _|_|_| _|_|_| _|_|_| _| _| _|_|_| _|_|_|_|_| _|_|_| _| _| _|_|_|_| _|_|_| ',\n ' _| _| _| _| _| _| _| _| _| _| _| _| _| ',\n ' _| _| _|_| _| _| _| _| _| _| _|_|_|_| _|_|_| _|_|_| ',\n ' _| _| _| _| _| _| _| _| _| _| _| _| _| _| ',\n ' _|_|_| _|_|_| _|_|_| _| _| _|_|_| _| _|_|_| _| _| _|_|_|_| _| _| '\n ];\n\n const subtitle = i18n.t('wizard.banner_subtitle');\n const subtitleLine = createContentLine(subtitle, '║', '║', BANNER_WIDTH, 'center');\n const emptyLine = createContentLine('', '║', '║', BANNER_WIDTH, 'center');\n const versionLine = createContentLine('v0.0.2', '║', '║', BANNER_WIDTH, 'center');\n\n console.log(chalk.cyan.bold('\\n' + createBorderLine('╔', '╗', '═', BANNER_WIDTH)));\n console.log(chalk.cyan.bold(emptyLine));\n \n // Gradient Colors for Logo\n const colors = [chalk.hex('#00ffff'), chalk.hex('#00dfff'), chalk.hex('#00bfff'), chalk.hex('#009fff'), chalk.hex('#007fff')];\n logoLines.forEach((line, i) => {\n // Center the logo line within the box width manually or using helper\n // createContentLine adds borders, which we don't want to color with the gradient\n // So we construct the content line but color the text part\n const content = createContentLine(line, '║', '║', BANNER_WIDTH, 'center');\n // Hack: replace the text content with colored version. \n // Since createContentLine pads with spaces, simple coloring of the whole string inside borders works\n // But let's just color the *whole line* for simplicity as borders will be colored too.\n // Or better: print borders separately. \n // Let's stick to coloring the whole line for the cool effect.\n console.log(colors[i % colors.length].bold(content));\n });\n\n console.log(chalk.cyan.bold(emptyLine));\n console.log(chalk.white.bold(versionLine));\n console.log(chalk.gray(subtitleLine));\n console.log(chalk.cyan.bold(createBorderLine('╚', '╝', '═', BANNER_WIDTH)));\n console.log('');\n }\n\n resetScreen() {\n console.clear();\n this.printBanner();\n }\n\n async runFirstTimeSetup() {\n this.resetScreen();\n console.log(chalk.cyan.bold('\\n' + (i18n.t('wizard.welcome') || 'Welcome to Claude Code Switcher!')));\n console.log(chalk.gray(i18n.t('wizard.privacy_note') + '\\n'));\n \n await this.showMainMenu();\n }\n\n async showMainMenu() {\n while (true) {\n this.resetScreen();\n \n const providers = configManager.getProviders();\n const selectedId = configManager.getSelectedProviderId();\n\n this.createBox(i18n.t('wizard.select_provider'));\n \n if (providers.length === 0) {\n console.log(chalk.yellow(` ${i18n.t('wizard.not_configured')}\\n`));\n }\n\n const choices = providers.map(p => ({\n name: (p.id === selectedId ? chalk.green('● ') : '○ ') + \n `${chalk.bold(p.name)} ` + \n chalk.gray(`(${p.model || 'Auto'} - ${p.contextWindow || 'Default'} ctx)`),\n value: p.id\n }));\n\n choices.push(new inquirer.Separator());\n choices.push({ name: `+ ${i18n.t('wizard.add_provider')}`, value: 'add_new' });\n choices.push(new inquirer.Separator());\n choices.push({ name: 'x ' + i18n.t('wizard.action_exit'), value: 'exit' });\n\n // Visual Hints Footer\n console.log(\n ' ' + \n chalk.hex('#FFA500').bold('ENTER') + chalk.gray(' = Set/Manage Provider ') + \n chalk.hex('#FFA500').bold('TAB') + chalk.gray(' = Edit (in menu) ') + \n chalk.hex('#FFA500').bold('DEL') + chalk.gray(' = Delete (in menu)')\n );\n console.log('');\n\n const { action } = await this.promptWithHints([\n {\n type: 'list',\n name: 'action',\n message: i18n.t('wizard.select_action'),\n choices,\n pageSize: 10\n }\n ]);\n\n if (action === 'exit') {\n console.log(chalk.green('\\n👋 ' + i18n.t('wizard.goodbye')));\n process.exit(0);\n } else if (action === 'add_new') {\n await this.addNewProvider();\n } else {\n await this.manageProvider(action);\n }\n }\n }\n \n async addNewProvider() {\n this.resetScreen();\n this.createBox(i18n.t('wizard.add_provider'));\n \n const { type } = await this.promptWithHints([{\n type: 'list',\n name: 'type',\n message: i18n.t('wizard.select_provider_type'),\n choices: [\n { name: i18n.t('wizard.provider_type_zai'), value: 'zai' },\n { name: i18n.t('wizard.provider_type_deepseek'), value: 'deepseek' },\n { name: i18n.t('wizard.provider_type_custom'), value: 'custom' }\n ]\n }]);\n \n let defaultName = '';\n let defaultUrl = '';\n let defaultCtx = '128000';\n let defaultModel = '';\n let region = '';\n \n if (type === 'zai') {\n // Z.AI Specific Flow\n const { zaiRegion } = await this.promptWithHints([{\n type: 'list',\n name: 'zaiRegion',\n message: i18n.t('wizard.select_region'),\n choices: [\n { name: i18n.t('wizard.region_global'), value: 'global' },\n { name: i18n.t('wizard.region_china'), value: 'china' }\n ]\n }]);\n \n region = zaiRegion;\n if (region === 'global') {\n defaultName = 'Z.AI Global';\n defaultUrl = 'https://api.z.ai/api/anthropic';\n } else {\n defaultName = 'Z.AI China';\n defaultUrl = 'https://open.bigmodel.cn/api/anthropic';\n }\n defaultCtx = '128000';\n defaultModel = 'claude-3-5-sonnet-20240620';\n \n // For Z.AI, we only ask for API Key, other fields are fixed/hidden or confirmable?\n // User asked: \"c'est le seul qu on peut pas changer les endpoints et tout ça\"\n // So we ask for API Key only.\n \n const { apiKey } = await inquirer.prompt([\n {\n type: 'password',\n name: 'apiKey',\n message: i18n.t('wizard.input_api_key'),\n mask: '●',\n validate: input => input.length > 0 ? true : i18n.t('wizard.api_key_required')\n }\n ]);\n \n configManager.addProvider({\n id: Date.now().toString(),\n type,\n name: defaultName,\n baseUrl: defaultUrl,\n model: defaultModel,\n contextWindow: defaultCtx,\n apiKey: apiKey,\n region: region // Store region just in case\n });\n \n console.log(chalk.green(`\\n✅ ${i18n.t('wizard.provider_added')}`));\n await new Promise(resolve => setTimeout(resolve, 1000));\n return;\n } \n \n // DeepSeek & Custom Flow\n if (type === 'deepseek') {\n defaultName = 'DeepSeek';\n defaultUrl = 'https://api.deepseek.com';\n defaultCtx = '64000'; \n defaultModel = 'deepseek-coder';\n }\n \n const answers = await inquirer.prompt([\n {\n type: 'input',\n name: 'name',\n message: i18n.t('wizard.input_provider_name'),\n default: defaultName,\n validate: input => input.length > 0 ? true : 'Name is required'\n },\n {\n type: 'input',\n name: 'baseUrl',\n message: i18n.t('wizard.input_api_url'),\n default: defaultUrl,\n validate: input => input.startsWith('http') ? true : 'Must be a valid URL'\n },\n {\n type: 'input',\n name: 'model',\n message: i18n.t('wizard.input_model'),\n default: defaultModel\n },\n {\n type: 'input',\n name: 'contextWindow',\n message: i18n.t('wizard.input_context'),\n default: defaultCtx\n },\n {\n type: 'password',\n name: 'apiKey',\n message: i18n.t('wizard.input_api_key'),\n mask: '●',\n validate: input => input.length > 0 ? true : i18n.t('wizard.api_key_required')\n }\n ]);\n \n configManager.addProvider({\n id: Date.now().toString(),\n type,\n name: answers.name,\n baseUrl: answers.baseUrl,\n model: answers.model,\n contextWindow: answers.contextWindow,\n apiKey: answers.apiKey\n });\n \n console.log(chalk.green(`\\n✅ ${i18n.t('wizard.provider_added')}`));\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n async manageProvider(providerId) {\n const provider = configManager.getProviders().find(p => p.id === providerId);\n if (!provider) return;\n \n const isActive = configManager.getSelectedProviderId() === providerId;\n \n this.resetScreen();\n this.createBox(`${provider.name} (${isActive ? i18n.t('wizard.active') : ''})`);\n \n const choices = [];\n if (!isActive) {\n choices.push({ name: `> ${i18n.t('wizard.activate_provider')}`, value: 'activate' });\n }\n choices.push({ name: `> ${i18n.t('wizard.delete_provider')}`, value: 'delete' });\n choices.push(new inquirer.Separator());\n choices.push({ name: `< ${i18n.t('wizard.nav_return')}`, value: 'back' });\n \n const { action } = await this.promptWithHints([{\n type: 'list',\n name: 'action',\n message: i18n.t('wizard.select_action'),\n choices\n }]);\n \n if (action === 'back') return;\n \n if (action === 'activate') {\n await this.activateProvider(provider);\n } else if (action === 'delete') {\n configManager.removeProvider(providerId);\n console.log(chalk.yellow(`\\n🗑️ ${i18n.t('wizard.provider_removed')}`));\n await new Promise(resolve => setTimeout(resolve, 1000));\n }\n }\n \n async activateProvider(provider) {\n const spinner = ora(i18n.t('auth.reloading', { tool: 'Claude Code' })).start();\n \n try {\n // Use existing claudeCodeManager logic but mapped to new structure\n const options = {\n contextWindow: provider.contextWindow,\n baseUrl: provider.baseUrl,\n model: provider.model\n };\n \n // We reuse 'deepseek' plan name internally for custom logic in claude-code-manager\n // or we just call configurePlan with 'custom' and ensure tool manager handles it.\n // Earlier I changed tool-manager to handle 'custom'.\n \n claudeCodeManager.configurePlan('custom', provider.apiKey, options);\n configManager.setSelectedProviderId(provider.id);\n \n spinner.succeed(chalk.green(i18n.t('wizard.activated')));\n await new Promise(resolve => setTimeout(resolve, 1500));\n } catch (error) {\n spinner.fail(chalk.red(i18n.t('auth.reload_failed')));\n console.error(error);\n await new Promise(resolve => setTimeout(resolve, 2000));\n }\n }\n \n // Deprecated methods stubbed or removed\n async configLanguage() {}\n async configPlan() {}\n async configApiKey() {}\n async configCustomProviderOptions() {}\n async selectAndConfigureTool() {} \n async configureTool() {}\n async showToolMenu() {}\n async startTool() {}\n async loadGLMConfig() {}\n async unloadGLMConfig() {}\n async showMCPMenu() {}\n \n async showToolMenu(toolName) {\n while (true) {\n // for claude code now\n this.resetScreen();\n const title = `${SUPPORTED_TOOLS[toolName].displayName} ${i18n.t('wizard.menu_title')}`;\n this.createBox(title);\n if (toolName === 'claude-code') {\n console.log(chalk.yellow.bold(i18n.t('wizard.global_config_warning', { tool: 'Claude Code' })));\n console.log('');\n }\n let actionText = '';\n const chelperConfig = configManager.getConfig();\n const detectedConfig = claudeCodeManager.detectCurrentConfig();\n // 显示 chelper 配置\n console.log(chalk.cyan.bold(i18n.t('wizard.chelper_config_title') + ':'));\n if (chelperConfig.plan) {\n const planName = chelperConfig.plan === 'glm_coding_plan_global'\n ? i18n.t('wizard.plan_global')\n : i18n.t('wizard.plan_china');\n console.log(chalk.gray(' ' + i18n.t('wizard.config_plan') + ': ') + chalk.green(planName));\n }\n else {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_plan') + ': ') + chalk.red(i18n.t('wizard.not_set')));\n }\n if (chelperConfig.api_key) {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.gray(i18n.t('wizard.api_key_set') + ' (' + chelperConfig.api_key.slice(0, 4) + '****)'));\n }\n else {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.red(i18n.t('wizard.not_set')));\n }\n console.log('');\n // 显示 Claude Code 当前配置\n console.log(chalk.yellow.bold(i18n.t('wizard.claude_code_config_title') + ':'));\n if (detectedConfig.plan) {\n const planName = detectedConfig.plan === 'glm_coding_plan_global'\n ? i18n.t('wizard.plan_global')\n : i18n.t('wizard.plan_china');\n console.log(chalk.gray(' ' + i18n.t('wizard.config_plan') + ': ') + chalk.green(planName));\n }\n else {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_plan') + ': ') + chalk.red(i18n.t('wizard.not_set')));\n }\n if (detectedConfig.apiKey) {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.gray(i18n.t('wizard.api_key_set') + ' (' + detectedConfig.apiKey.slice(0, 4) + '****)'));\n }\n else {\n console.log(chalk.gray(' ' + i18n.t('wizard.config_api_key') + ': ') + chalk.red(i18n.t('wizard.not_set')));\n }\n console.log('');\n // 判断是否需要刷新配置\n const configMatches = detectedConfig.plan === chelperConfig.plan &&\n detectedConfig.apiKey === chelperConfig.api_key;\n if (detectedConfig.plan && detectedConfig.apiKey && configMatches) {\n // 配置已同步\n console.log(chalk.green('✅ ' + i18n.t('wizard.config_synced')));\n actionText = i18n.t('wizard.action_refresh_glm', { 'tool': SUPPORTED_TOOLS[toolName].displayName });\n }\n else if (detectedConfig.plan || detectedConfig.apiKey) {\n // 配置不一致,需要刷新\n console.log(chalk.yellow('⚠️ ' + i18n.t('wizard.config_out_of_sync')));\n actionText = i18n.t('wizard.action_refresh_glm', { 'tool': SUPPORTED_TOOLS[toolName].displayName });\n }\n else {\n // 未配置,需要装载\n console.log(chalk.blue('ℹ️ ' + i18n.t('wizard.config_not_loaded')));\n actionText = i18n.t('wizard.action_load_glm', { 'tool': SUPPORTED_TOOLS[toolName].displayName });\n }\n console.log('');\n const choices = [];\n choices.push({ name: '> ' + actionText, value: 'load_glm' });\n // 如果已经配置了 GLM Coding Plan,显示卸载选项\n if (detectedConfig.plan && detectedConfig.apiKey) {\n choices.push({ name: '> ' + i18n.t('wizard.action_unload_glm', { 'tool': SUPPORTED_TOOLS[toolName].displayName }), value: 'unload_glm' });\n }\n choices.push({ name: '> ' + i18n.t('wizard.action_mcp_config'), value: 'mcp_config' });\n // 如果已经配置了 GLM Coding Plan,显示启动选项\n if (detectedConfig.plan && detectedConfig.apiKey) {\n choices.push({ name: '> ' + i18n.t('wizard.start_tool', { 'tool': SUPPORTED_TOOLS[toolName].displayName, 'shell': SUPPORTED_TOOLS[toolName].command }), value: 'start_tool' });\n }\n choices.push(new inquirer.Separator(), { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' }, { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' });\n const { action } = await this.promptWithHints([\n {\n type: 'list',\n name: 'action',\n message: i18n.t('wizard.select_action'),\n choices\n }\n ]);\n if (action === 'exit') {\n console.log(chalk.green('\\n👋 ' + i18n.t('wizard.goodbye_message')));\n process.exit(0);\n }\n else if (action === 'back') {\n return;\n }\n else if (action === 'load_glm') {\n await this.loadGLMConfig(toolName);\n }\n else if (action === 'unload_glm') {\n await this.unloadGLMConfig(toolName);\n }\n else if (action === 'mcp_config') {\n await this.showMCPMenu(toolName);\n }\n else if (action === 'start_tool') {\n await this.startTool(toolName);\n }\n }\n }\n async startTool(toolName) {\n const tool = SUPPORTED_TOOLS[toolName];\n if (!tool) {\n throw new Error(`Unknown tool: ${toolName}`);\n }\n const spinner = ora({\n text: i18n.t('wizard.starting_tool'),\n spinner: 'star2'\n }).start();\n try {\n // 开启新窗口 执行 tool.command\n execSync(tool.command, { stdio: 'inherit' });\n spinner.succeed(i18n.t('wizard.tool_started'));\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.tool_start_failed'));\n throw error;\n }\n }\n async loadGLMConfig(toolName) {\n const spinner = ora({\n text: i18n.t('wizard.loading_glm_config'),\n spinner: 'star2'\n }).start();\n try {\n const config = configManager.getConfig();\n if (!config.plan || !config.api_key) {\n spinner.fail(i18n.t('wizard.missing_config'));\n await new Promise(resolve => setTimeout(resolve, 800));\n return;\n }\n toolManager.loadGLMConfig(toolName, config.plan, config.api_key);\n await new Promise(resolve => setTimeout(resolve, 800));\n spinner.succeed(chalk.green(i18n.t('wizard.glm_config_loaded', { tool: SUPPORTED_TOOLS[toolName].displayName })));\n await new Promise(resolve => setTimeout(resolve, 2000));\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.glm_config_failed'));\n await new Promise(resolve => setTimeout(resolve, 800));\n console.error(error);\n }\n }\n async unloadGLMConfig(toolName) {\n // 确认卸载操作\n const { confirm } = await this.promptWithHints([\n {\n type: 'confirm',\n name: 'confirm',\n message: i18n.t('wizard.confirm_unload_glm', { tool: SUPPORTED_TOOLS[toolName].displayName }),\n default: false\n }\n ]);\n if (!confirm) {\n return;\n }\n const spinner = ora({\n text: i18n.t('wizard.unloading_glm_config'),\n spinner: 'star2'\n }).start();\n try {\n if (toolName === 'claude-code') {\n // 添加短暂延迟,让动画效果更流畅\n await new Promise(resolve => setTimeout(resolve, 300));\n claudeCodeManager.unloadGLMConfig();\n await new Promise(resolve => setTimeout(resolve, 500));\n spinner.succeed(chalk.green(i18n.t('wizard.glm_config_unloaded')));\n await new Promise(resolve => setTimeout(resolve, 800));\n }\n else {\n spinner.fail(i18n.t('wizard.tool_not_supported'));\n await new Promise(resolve => setTimeout(resolve, 800));\n }\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.glm_config_unload_failed'));\n await new Promise(resolve => setTimeout(resolve, 800));\n console.error(error);\n }\n }\n async showMCPMenu(toolName) {\n while (true) {\n this.resetScreen();\n const presetServices = mcpManager.getPresetServices();\n const mcpStatus = mcpManager.getMCPStatus(toolName);\n const title = `${i18n.t('wizard.mcp_menu_title')}`;\n this.createBox(title);\n // 检查是否有任何套餐 MCP 已安装\n const hasInstalledPresetMCP = Array.from(mcpStatus.values()).some(installed => installed);\n // 如果没有安装任何套餐 MCP,显示提示信息\n if (!hasInstalledPresetMCP) {\n console.log(chalk.blue('ℹ️ ' + i18n.t('wizard.no_preset_mcp_installed')));\n console.log('');\n }\n // 内置 MCP 服务\n const choices = [];\n // 如果没有安装任何套餐 MCP,添加一键安装选项\n if (!hasInstalledPresetMCP) {\n choices.push({\n name: '> ' + chalk.green.bold(i18n.t('wizard.action_install_all_mcp')),\n value: 'install_all'\n });\n choices.push(new inquirer.Separator());\n }\n // 添加内置 MCP 服务标题\n choices.push({\n name: chalk.yellow.bold(`✨ ${i18n.t('wizard.builtin_mcp_services')}:`),\n value: 'builtin_header',\n disabled: true\n });\n const builtinChoices = presetServices.map(mcp => ({\n name: ` ${mcpStatus.get(mcp.id) ? '[+]' : '[ ]'} ${mcp.name} ${chalk.gray('(' + mcp.protocol + ')')} - ${chalk.gray(mcp.description)}`,\n value: `builtin:${mcp.id}`\n }));\n choices.push(...builtinChoices);\n // 如果是 Claude Code,显示其他 MCP 服务\n if (toolName === 'claude-code') {\n const builtinIds = presetServices.map(mcp => mcp.id);\n const otherMCPs = claudeCodeManager.getOtherMCPs(builtinIds);\n if (otherMCPs.length > 0) {\n // 添加空行分隔\n choices.push(new inquirer.Separator());\n // 添加其他 MCP 服务标题\n choices.push({\n name: chalk.yellow.bold(`* ${i18n.t('wizard.other_mcp_services')}:`),\n value: 'other_header',\n disabled: true\n });\n const otherChoices = otherMCPs.map(({ id, config }) => ({\n name: ` - ${id} ${chalk.gray('(' + config.type + ')')} ${chalk.blue('[' + i18n.t('wizard.other_mcp') + ']')}`,\n value: `other:${id}`\n }));\n choices.push(...otherChoices);\n }\n }\n choices.push(new inquirer.Separator(), { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' }, { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' });\n const { selectedMCP } = await this.promptWithHints([\n {\n type: 'list',\n name: 'selectedMCP',\n message: i18n.t('wizard.select_mcp'),\n choices,\n pageSize: 15\n }\n ]);\n if (selectedMCP === 'exit') {\n console.log(chalk.green('\\n👋 ' + i18n.t('wizard.goodbye_message')));\n process.exit(0);\n }\n else if (selectedMCP === 'back') {\n return;\n }\n else if (selectedMCP === 'install_all') {\n await this.installAllPresetMCPs(toolName);\n }\n else if (selectedMCP.startsWith('builtin:')) {\n const mcpId = selectedMCP.replace('builtin:', '');\n await this.showMCPDetail(toolName, mcpId);\n }\n else if (selectedMCP.startsWith('other:')) {\n const mcpId = selectedMCP.replace('other:', '');\n await this.showOtherMCPDetail(toolName, mcpId);\n }\n }\n }\n async showMCPDetail(toolName, mcpId) {\n const mcp = mcpManager.getPresetServices().find(m => m.id === mcpId);\n if (!mcp)\n return;\n this.resetScreen();\n const isInstalled = mcpManager.isMCPInstalled(toolName, mcpId);\n const title = `${mcp.name}`;\n this.createBox(title);\n console.log(chalk.gray(' ' + i18n.t('wizard.mcp_protocol') + ': ') + chalk.white(mcp.protocol));\n console.log(chalk.gray(' ' + i18n.t('wizard.mcp_type') + ': ') + chalk.white(mcp.type));\n // 显示套餐类型(如果该 MCP 支持多套餐)\n const config = configManager.getConfig();\n let planName = null;\n // 对于 Vision MCP (stdio 协议且有 envTemplate),根据已安装的环境变量判断\n if (mcp.protocol === 'stdio' && mcp.envTemplate && isInstalled) {\n // 读取已安装的 MCP 配置\n if (toolName === 'claude-code') {\n const allServers = claudeCodeManager.getAllMCPServers();\n const installedConfig = allServers[mcp.id];\n if (installedConfig && installedConfig.env) {\n const zaiMode = installedConfig.env.Z_AI_MODE;\n // 根据 Z_AI_MODE 判断套餐类型\n if (zaiMode === 'ZAI') {\n planName = i18n.t('wizard.plan_global');\n }\n else {\n // 'ZHIPU' 或无值都认为是中国版\n planName = i18n.t('wizard.plan_china');\n }\n }\n }\n }\n // 对于基于 URL 的 MCP (urlTemplate),根据当前配置判断\n if (!planName && mcp.urlTemplate) {\n const currentPlan = config.plan || 'glm_coding_plan_china';\n planName = currentPlan === 'glm_coding_plan_global'\n ? i18n.t('wizard.plan_global')\n : i18n.t('wizard.plan_china');\n }\n // 对于未安装的 Vision MCP,根据当前配置显示将要使用的套餐类型\n if (!planName && mcp.envTemplate && !isInstalled) {\n const currentPlan = config.plan || 'glm_coding_plan_china';\n planName = currentPlan === 'glm_coding_plan_global'\n ? i18n.t('wizard.plan_global')\n : i18n.t('wizard.plan_china');\n }\n // 显示套餐类型\n if (planName) {\n console.log(chalk.gray(' ' + i18n.t('wizard.mcp_plan_type') + ': ') + chalk.green(planName));\n }\n console.log(chalk.gray(' ' + i18n.t('wizard.mcp_description') + ': ') + chalk.white(mcp.description));\n // 显示 URL 信息\n if (mcp.protocol === 'sse' || mcp.protocol === 'streamable-http') {\n if (mcp.urlTemplate) {\n const currentPlan = config.plan || 'glm_coding_plan_china';\n const currentUrl = mcp.urlTemplate[currentPlan];\n console.log(chalk.gray(' URL (' + currentPlan + '): ') + chalk.white(currentUrl));\n }\n else if (mcp.url) {\n console.log(chalk.gray(' URL: ') + chalk.white(mcp.url));\n }\n }\n const statusIcon = isInstalled ? '[+]' : '[ ]';\n const statusText = isInstalled ? chalk.green(i18n.t('wizard.installed')) : chalk.gray(i18n.t('wizard.not_installed'));\n console.log(chalk.gray(' ' + statusIcon + ' ' + i18n.t('wizard.mcp_status') + ': ') + statusText);\n const choices = [];\n if (!isInstalled) {\n choices.push({ name: '> ' + i18n.t('wizard.action_install'), value: 'install' });\n }\n else {\n choices.push({ name: '* ' + i18n.t('wizard.action_uninstall'), value: 'uninstall' });\n }\n choices.push(new inquirer.Separator(), { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' }, { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' });\n const { action } = await this.promptWithHints([\n {\n type: 'list',\n name: 'action',\n message: i18n.t('wizard.select_action'),\n choices\n }\n ]);\n if (action === 'exit') {\n console.log(chalk.green('\\n👋 ' + i18n.t('wizard.goodbye_message')));\n process.exit(0);\n }\n else if (action === 'back') {\n return;\n }\n if (action === 'install') {\n const spinner = ora({\n text: i18n.t('wizard.installing_mcp'),\n spinner: 'star2'\n }).start();\n try {\n const config = configManager.getConfig();\n mcpManager.installMCP(toolName, mcp, config.api_key, config.plan);\n await new Promise(resolve => setTimeout(resolve, 800));\n spinner.succeed(chalk.green(i18n.t('wizard.mcp_installed')));\n await new Promise(resolve => setTimeout(resolve, 800));\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.mcp_install_failed'));\n await new Promise(resolve => setTimeout(resolve, 800));\n console.error(error);\n }\n }\n else if (action === 'uninstall') {\n const spinner = ora({\n text: i18n.t('wizard.uninstalling_mcp'),\n spinner: 'star2'\n }).start();\n try {\n mcpManager.uninstallMCP(toolName, mcpId);\n await new Promise(resolve => setTimeout(resolve, 800));\n spinner.succeed(chalk.green(i18n.t('wizard.mcp_uninstalled')));\n await new Promise(resolve => setTimeout(resolve, 800));\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.mcp_uninstall_failed'));\n await new Promise(resolve => setTimeout(resolve, 800));\n console.error(error);\n }\n }\n }\n async showOtherMCPDetail(toolName, mcpId) {\n if (toolName !== 'claude-code')\n return;\n const allServers = claudeCodeManager.getAllMCPServers();\n const mcpConfig = allServers[mcpId];\n if (!mcpConfig)\n return;\n this.resetScreen();\n const title = `${mcpId}`;\n this.createBox(title);\n console.log(chalk.gray(' ' + i18n.t('wizard.mcp_protocol') + ': ') + chalk.white(mcpConfig.type));\n if (mcpConfig.type === 'stdio') {\n console.log(chalk.gray(' Command: ') + chalk.white(mcpConfig.command || 'N/A'));\n if (mcpConfig.args && mcpConfig.args.length > 0) {\n console.log(chalk.gray(' Args: ') + chalk.white(mcpConfig.args.join(' ')));\n }\n }\n else if (mcpConfig.type === 'sse' || mcpConfig.type === 'http') {\n console.log(chalk.gray(' URL: ') + chalk.white(mcpConfig.url || 'N/A'));\n }\n console.log(chalk.blue('\\n [i] ' + i18n.t('wizard.other_mcp_info')));\n console.log('');\n const choices = [\n { name: '* ' + i18n.t('wizard.action_uninstall'), value: 'uninstall' },\n new inquirer.Separator(),\n { name: '<- ' + i18n.t('wizard.nav_return'), value: 'back' },\n { name: 'x ' + i18n.t('wizard.nav_exit'), value: 'exit' }\n ];\n const { action } = await this.promptWithHints([\n {\n type: 'list',\n name: 'action',\n message: i18n.t('wizard.select_action'),\n choices\n }\n ]);\n if (action === 'exit') {\n console.log(chalk.green('\\n👋 ' + i18n.t('wizard.goodbye_message')));\n process.exit(0);\n }\n else if (action === 'back') {\n return;\n }\n if (action === 'uninstall') {\n const { confirm } = await this.promptWithHints([\n {\n type: 'confirm',\n name: 'confirm',\n message: i18n.t('wizard.confirm_uninstall_other_mcp'),\n default: false\n }\n ]);\n if (confirm) {\n const spinner = ora(i18n.t('wizard.uninstalling_mcp')).start();\n try {\n claudeCodeManager.uninstallMCP(mcpId);\n spinner.succeed(i18n.t('wizard.mcp_uninstalled'));\n }\n catch (error) {\n spinner.fail(i18n.t('wizard.mcp_uninstall_failed'));\n console.error(error);\n }\n }\n }\n }\n async installAllPresetMCPs(toolName) {\n const config = configManager.getConfig();\n if (!config.plan || !config.api_key) {\n console.log(chalk.red('\\n[!] ' + i18n.t('wizard.missing_config')));\n await new Promise(resolve => setTimeout(resolve, 1500));\n return;\n }\n const presetServices = mcpManager.getPresetServices();\n // 显示将要安装的服务列表\n this.resetScreen();\n this.createBox(i18n.t('wizard.action_install_all_mcp'));\n console.log(chalk.yellow(i18n.t('wizard.install_all_mcp_confirm')));\n console.log('');\n presetServices.forEach(mcp => {\n console.log(chalk.gray(' • ') + chalk.white(mcp.name) + chalk.gray(` (${mcp.protocol}) - ${mcp.description}`));\n });\n console.log('');\n const { confirm } = await this.promptWithHints([\n {\n type: 'confirm',\n name: 'confirm',\n message: i18n.t('wizard.select_action'),\n default: true\n }\n ]);\n if (!confirm) {\n return;\n }\n const spinner = ora({\n text: i18n.t('wizard.installing_all_mcp'),\n spinner: 'star2'\n }).start();\n let successCount = 0;\n let failCount = 0;\n const errors = [];\n for (const mcp of presetServices) {\n try {\n spinner.text = i18n.t('wizard.installing_all_mcp') + ` [${successCount + failCount + 1}/${presetServices.length}] ${mcp.name}`;\n mcpManager.installMCP(toolName, mcp, config.api_key, config.plan);\n successCount++;\n await new Promise(resolve => setTimeout(resolve, 300));\n }\n catch (error) {\n failCount++;\n errors.push(`${mcp.name}: ${error}`);\n }\n }\n await new Promise(resolve => setTimeout(resolve, 500));\n if (failCount === 0) {\n spinner.succeed(chalk.green(i18n.t('wizard.all_mcp_installed') + ` (${successCount}/${presetServices.length})`));\n }\n else {\n spinner.warn(chalk.yellow(i18n.t('wizard.all_mcp_install_failed') + ` (${successCount}/${presetServices.length})`));\n if (errors.length > 0) {\n console.log(chalk.red('\\n[!] ' + i18n.t('wizard.mcp_install_failed') + ':'));\n errors.forEach(err => console.log(chalk.gray(' • ') + chalk.red(err)));\n }\n }\n await new Promise(resolve => setTimeout(resolve, 1500));\n }\n}\nexport const wizard = Wizard.getInstance();\n"}}}} |