Compare commits

...

2 Commits

Author SHA1 Message Date
koalazf.99
05238b4f90 fix test:ci 2025-08-03 21:28:07 +08:00
koalazf.99
5cfb727ec6 save cli login info in system env folder 2025-08-03 21:08:47 +08:00
5 changed files with 129 additions and 44 deletions

View File

@@ -98,6 +98,7 @@ describe('Settings Loading and Merging', () => {
expect(settings.merged).toEqual({
customThemes: {},
mcpServers: {},
openaiConfig: {},
});
expect(settings.errors.length).toBe(0);
});
@@ -131,6 +132,7 @@ describe('Settings Loading and Merging', () => {
...systemSettingsContent,
customThemes: {},
mcpServers: {},
openaiConfig: {},
});
});
@@ -164,6 +166,7 @@ describe('Settings Loading and Merging', () => {
...userSettingsContent,
customThemes: {},
mcpServers: {},
openaiConfig: {},
});
});
@@ -195,6 +198,7 @@ describe('Settings Loading and Merging', () => {
...workspaceSettingsContent,
customThemes: {},
mcpServers: {},
openaiConfig: {},
});
});
@@ -232,6 +236,7 @@ describe('Settings Loading and Merging', () => {
contextFileName: 'WORKSPACE_CONTEXT.md',
customThemes: {},
mcpServers: {},
openaiConfig: {},
});
});
@@ -281,6 +286,7 @@ describe('Settings Loading and Merging', () => {
allowMCPServers: ['server1', 'server2'],
customThemes: {},
mcpServers: {},
openaiConfig: {},
});
});
@@ -560,6 +566,7 @@ describe('Settings Loading and Merging', () => {
expect(settings.merged).toEqual({
customThemes: {},
mcpServers: {},
openaiConfig: {},
});
// Check that error objects are populated in settings.errors
@@ -954,6 +961,7 @@ describe('Settings Loading and Merging', () => {
...systemSettingsContent,
customThemes: {},
mcpServers: {},
openaiConfig: {},
});
});
});

View File

@@ -79,6 +79,7 @@ export interface Settings {
checkpointing?: CheckpointingSettings;
autoConfigureMaxOldSpaceSize?: boolean;
enableOpenAILogging?: boolean;
openaiConfig?: Record<string, string>;
// 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 {

View File

@@ -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', 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,27 @@ 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 (
<OpenAIKeyPrompt
onSubmit={handleOpenAIKeySubmit}
onCancel={handleOpenAIKeyCancel}
defaultValues={defaultValues}
/>
);
}
@@ -165,7 +198,7 @@ export function AuthDialog({
</Box>
<Box marginTop={1}>
<Text color={Colors.AccentBlue}>
{'https://github.com/QwenLM/Qwen3-Coder/blob/main/README.md'}
{'https://github.com/QwenLM/qwen-code/blob/main/README.md'}
</Text>
</Box>
</Box>

View File

@@ -17,8 +17,7 @@ describe('OpenAIKeyPrompt', () => {
<OpenAIKeyPrompt onSubmit={onSubmit} onCancel={onCancel} />,
);
expect(lastFrame()).toContain('OpenAI Configuration Required');
expect(lastFrame()).toContain('https://platform.openai.com/api-keys');
expect(lastFrame()).toContain('API Configuration Required');
expect(lastFrame()).toContain(
'Press Enter to continue, Tab/↑↓ to navigate, Esc to cancel',
);
@@ -33,7 +32,7 @@ describe('OpenAIKeyPrompt', () => {
);
const output = lastFrame();
expect(output).toContain('OpenAI Configuration Required');
expect(output).toContain('API Configuration Required');
expect(output).toContain('API Key:');
expect(output).toContain('Base URL:');
expect(output).toContain('Model:');

View File

@@ -11,32 +11,45 @@ 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) => {
// 过滤粘贴相关的控制序列
// Ignore control sequences like [I or [O from focus switching
if (input && (input === '[I' || input === '[O')) {
return;
}
// 处理字符输入
if (input && input.length > 0) {
// Filter paste-related control sequences
let cleanInput = (input || '')
// 过滤 ESC 开头的控制序列(如 \u001b[200~\u001b[201~ 等)
// 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
// 过滤粘贴开始标记 [200~
// Filter paste start marker [200~
.replace(/\[200~/g, '')
// 过滤粘贴结束标记 [201~
// Filter paste end marker [201~
.replace(/\[201~/g, '')
// 过滤单独的 [ 和 ~ 字符(可能是粘贴标记的残留)
// Filter standalone [ and ~ characters (possible paste marker remnants)
.replace(/^\[|~$/g, '');
// 再过滤所有不可见字符ASCII < 32除了回车换行
// Filter all invisible characters (ASCII < 32, except newlines)
cleanInput = cleanInput
.split('')
.filter((ch) => ch.charCodeAt(0) >= 32)
@@ -52,22 +65,23 @@ export function OpenAIKeyPrompt({
}
return;
}
}
// 检查是否是 Enter 键(通过检查输入是否包含换行符)
// 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');
}
}
@@ -132,15 +146,10 @@ export function OpenAIKeyPrompt({
width="100%"
>
<Text bold color={Colors.AccentBlue}>
OpenAI Configuration Required
API Configuration Required
</Text>
<Box marginTop={1}>
<Text>
Please enter your OpenAI configuration. You can get an API key from{' '}
<Text color={Colors.AccentBlue}>
https://platform.openai.com/api-keys
</Text>
</Text>
<Text>Please enter your API configuration.</Text>
</Box>
<Box marginTop={1} flexDirection="row">
<Box width={12}>