From 5cfb727ec6850ea2a501ae3cae4f76b7f6fdaf36 Mon Sep 17 00:00:00 2001 From: "koalazf.99" Date: Sun, 3 Aug 2025 21:08:47 +0800 Subject: [PATCH] save cli login info in system env folder --- packages/cli/src/config/settings.ts | 38 ++++++++- packages/cli/src/ui/components/AuthDialog.tsx | 24 ++++++ .../cli/src/ui/components/OpenAIKeyPrompt.tsx | 77 +++++++++++-------- 3 files changed, 107 insertions(+), 32 deletions(-) diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 31de5004..a2248974 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -79,6 +79,7 @@ export interface Settings { checkpointing?: CheckpointingSettings; autoConfigureMaxOldSpaceSize?: boolean; enableOpenAILogging?: boolean; + openaiConfig?: Record; // Git-aware file filtering settings fileFiltering?: { @@ -174,6 +175,11 @@ export class LoadedSettings { ...(workspace.mcpServers || {}), ...(system.mcpServers || {}), }, + openaiConfig: { + ...(user.openaiConfig || {}), + ...(workspace.openaiConfig || {}), + ...(system.openaiConfig || {}), + }, }; } @@ -307,6 +313,30 @@ export function loadEnvironment(): void { } } +/** + * Loads OpenAI configuration from user settings as fallback. + * Priority order: env vars > .env file > workspace settings.json > ~/.qwen/settings.json + */ +export function loadOpenAIConfigFromSettings(settings: Settings): void { + if (!settings.openaiConfig) { + return; + } + + // Only set environment variables if they're not already set + // This maintains the priority order + if (!process.env.OPENAI_API_KEY && settings.openaiConfig.OPENAI_API_KEY) { + process.env.OPENAI_API_KEY = settings.openaiConfig.OPENAI_API_KEY; + } + + if (!process.env.OPENAI_BASE_URL && settings.openaiConfig.OPENAI_BASE_URL) { + process.env.OPENAI_BASE_URL = settings.openaiConfig.OPENAI_BASE_URL; + } + + if (!process.env.OPENAI_MODEL && settings.openaiConfig.OPENAI_MODEL) { + process.env.OPENAI_MODEL = settings.openaiConfig.OPENAI_MODEL; + } +} + /** * Loads settings from user and workspace directories. * Project settings override user settings. @@ -386,7 +416,7 @@ export function loadSettings(workspaceDir: string): LoadedSettings { }); } - return new LoadedSettings( + const loadedSettings = new LoadedSettings( { path: systemSettingsPath, settings: systemSettings, @@ -401,6 +431,12 @@ export function loadSettings(workspaceDir: string): LoadedSettings { }, settingsErrors, ); + + // Load OpenAI config from settings as fallback + // Priority order: env vars > .env file > workspace settings > user settings + loadOpenAIConfigFromSettings(loadedSettings.merged); + + return loadedSettings; } export function saveSettings(settingsFile: SettingsFile): void { diff --git a/packages/cli/src/ui/components/AuthDialog.tsx b/packages/cli/src/ui/components/AuthDialog.tsx index ab5ddf81..74a6344f 100644 --- a/packages/cli/src/ui/components/AuthDialog.tsx +++ b/packages/cli/src/ui/components/AuthDialog.tsx @@ -92,6 +92,22 @@ export function AuthDialog({ setOpenAIApiKey(apiKey); setOpenAIBaseUrl(baseUrl); setOpenAIModel(model); + + // Save OpenAI configuration to user settings as fallback + // Priority order: .env > workspace settings.json > ~/.qwen/settings.json + try { + const openAIConfig: { [key: string]: string } = {}; + if (apiKey.trim()) openAIConfig.OPENAI_API_KEY = apiKey.trim(); + if (baseUrl.trim()) openAIConfig.OPENAI_BASE_URL = baseUrl.trim(); + if (model.trim()) openAIConfig.OPENAI_MODEL = model.trim(); + + // Save to user settings as environment variables for next time + settings.setValue(SettingScope.User, 'openaiConfig' as any, openAIConfig); + } catch (error) { + // Don't block authentication if saving fails + console.warn('Failed to save OpenAI config to settings:', error); + } + setShowOpenAIKeyPrompt(false); onSelect(AuthType.USE_OPENAI, SettingScope.User); }; @@ -124,10 +140,18 @@ export function AuthDialog({ }); if (showOpenAIKeyPrompt) { + // Load default values from settings + const defaultValues = { + apiKey: process.env.OPENAI_API_KEY || settings.merged.openaiConfig?.OPENAI_API_KEY || '', + baseUrl: process.env.OPENAI_BASE_URL || settings.merged.openaiConfig?.OPENAI_BASE_URL || '', + model: process.env.OPENAI_MODEL || settings.merged.openaiConfig?.OPENAI_MODEL || '', + }; + return ( ); } diff --git a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx index bf9f4bff..05ef4605 100644 --- a/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx +++ b/packages/cli/src/ui/components/OpenAIKeyPrompt.tsx @@ -11,63 +11,78 @@ import { Colors } from '../colors.js'; interface OpenAIKeyPromptProps { onSubmit: (apiKey: string, baseUrl: string, model: string) => void; onCancel: () => void; + defaultValues?: { + apiKey?: string; + baseUrl?: string; + model?: string; + }; } export function OpenAIKeyPrompt({ onSubmit, onCancel, + defaultValues, }: OpenAIKeyPromptProps): React.JSX.Element { - const [apiKey, setApiKey] = useState(''); - const [baseUrl, setBaseUrl] = useState(''); - const [model, setModel] = useState(''); + const [apiKey, setApiKey] = useState(defaultValues?.apiKey || ''); + const [baseUrl, setBaseUrl] = useState(defaultValues?.baseUrl || ''); + const [model, setModel] = useState(defaultValues?.model || ''); const [currentField, setCurrentField] = useState< 'apiKey' | 'baseUrl' | 'model' >('apiKey'); useInput((input, key) => { - // 过滤粘贴相关的控制序列 - let cleanInput = (input || '') - // 过滤 ESC 开头的控制序列(如 \u001b[200~、\u001b[201~ 等) - .replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '') // eslint-disable-line no-control-regex - // 过滤粘贴开始标记 [200~ - .replace(/\[200~/g, '') - // 过滤粘贴结束标记 [201~ - .replace(/\[201~/g, '') - // 过滤单独的 [ 和 ~ 字符(可能是粘贴标记的残留) - .replace(/^\[|~$/g, ''); - // 再过滤所有不可见字符(ASCII < 32,除了回车换行) - cleanInput = cleanInput - .split('') - .filter((ch) => ch.charCodeAt(0) >= 32) - .join(''); - - if (cleanInput.length > 0) { - if (currentField === 'apiKey') { - setApiKey((prev) => prev + cleanInput); - } else if (currentField === 'baseUrl') { - setBaseUrl((prev) => prev + cleanInput); - } else if (currentField === 'model') { - setModel((prev) => prev + cleanInput); - } + // Ignore control sequences like [I or [O from focus switching + if (input && (input === '[I' || input === '[O')) { return; } - // 检查是否是 Enter 键(通过检查输入是否包含换行符) + // 处理字符输入 + if (input && input.length > 0) { + // Filter paste-related control sequences + let cleanInput = (input || '') + // Filter ESC-based control sequences (like \u001b[200~, \u001b[201~, etc.) + .replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '') // eslint-disable-line no-control-regex + // Filter paste start marker [200~ + .replace(/\[200~/g, '') + // Filter paste end marker [201~ + .replace(/\[201~/g, '') + // Filter standalone [ and ~ characters (possible paste marker remnants) + .replace(/^\[|~$/g, ''); + + // Filter all invisible characters (ASCII < 32, except newlines) + cleanInput = cleanInput + .split('') + .filter((ch) => ch.charCodeAt(0) >= 32) + .join(''); + + if (cleanInput.length > 0) { + if (currentField === 'apiKey') { + setApiKey((prev) => prev + cleanInput); + } else if (currentField === 'baseUrl') { + setBaseUrl((prev) => prev + cleanInput); + } else if (currentField === 'model') { + setModel((prev) => prev + cleanInput); + } + return; + } + } + + // Check if Enter key was pressed (by checking for newline characters) if (input.includes('\n') || input.includes('\r')) { if (currentField === 'apiKey') { - // 允许空 API key 跳转到下一个字段,让用户稍后可以返回修改 + // Allow empty API key to jump to next field, user can return to modify later setCurrentField('baseUrl'); return; } else if (currentField === 'baseUrl') { setCurrentField('model'); return; } else if (currentField === 'model') { - // 只有在提交时才检查 API key 是否为空 + // Only check if API key is empty when submitting if (apiKey.trim()) { onSubmit(apiKey.trim(), baseUrl.trim(), model.trim()); } else { - // 如果 API key 为空,回到 API key 字段 + // If API key is empty, return to API key field setCurrentField('apiKey'); } }