Skip to content

Commit 66fd01d

Browse files
Wing900claude
andcommitted
feat: 添加自定义 API 配置功能并修复安全问题
- 添加统一的自定义 API 配置开关,允许用户覆盖环境变量 - 修复环境变量暴露问题:未启用自定义时不在 settings 中存储环境变量值 - 调整设置项顺序:开关 → API Base URL → API Key - 优化设置界面颜色对比度:使用主题色替代低对比度的灰色 - 实际调用 API 时根据 useCustomApi 动态选择环境变量或用户配置 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent cf94415 commit 66fd01d

File tree

8 files changed

+83
-34
lines changed

8 files changed

+83
-34
lines changed

components/settings/AboutSettings.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,19 @@ export const AboutSettings: React.FC<AboutSettingsProps> = ({ versionInfo }) =>
9696
<div className="border-t border-[var(--glass-border)] pt-6 space-y-4">
9797
<h3 className="font-bold text-lg text-[var(--text-color)]">{t('usefulLinks')}</h3>
9898
<div className="flex flex-wrap gap-4">
99-
<a href="https://github.com/Wing900/KChat" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-[var(--accent-color)] hover:underline">
99+
<a href="https://github.com/Wing900/KChat" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-[var(--md-sys-color-primary)] hover:underline">
100100
<Icon icon="github" className="w-4 h-4" />
101101
<span>{t('sourceCode')}</span>
102102
</a>
103-
<a href="https://github.com/Wing900/KChat/issues" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-[var(--accent-color)] hover:underline">
103+
<a href="https://github.com/Wing900/KChat/issues" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-[var(--md-sys-color-primary)] hover:underline">
104104
<Icon icon="bug" className="w-4 h-4" />
105105
<span>{t('reportBug')}</span>
106106
</a>
107-
<a href="https://github.com/Wing900/KChat/discussions" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-[var(--accent-color)] hover:underline">
107+
<a href="https://github.com/Wing900/KChat/discussions" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-[var(--md-sys-color-primary)] hover:underline">
108108
<Icon icon="message-square" className="w-4 h-4" />
109109
<span>{t('discussions')}</span>
110110
</a>
111-
<a href="https://iambin.qzz.io" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-[var(--accent-color)] hover:underline">
111+
<a href="https://iambin.qzz.io" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-[var(--md-sys-color-primary)] hover:underline">
112112
<Icon icon="chicken" className="w-4 h-4" />
113113
<span>iambin.qzz.io</span>
114114
</a>

components/settings/AdvancedSettings.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,27 +40,28 @@ export const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ settings, on
4040
/>
4141
</SettingsItem>
4242
)}
43-
{visibleIds.has('apiKey') && (
44-
<SettingsItem label={t('apiKey')} description={t('apiKeyDesc')}>
45-
<textarea
46-
value={isApiKeySetByEnv ? '' : (settings.apiKey || []).join('\n')}
47-
onChange={handleApiKeyChange}
48-
disabled={isApiKeySetByEnv}
49-
placeholder={isApiKeySetByEnv ? t('apiKeyEnvVar') : t('apiKeyPlaceholder')}
50-
className="input-glass max-w-60 min-h-24"
51-
rows={3}
52-
/>
43+
44+
{/* 使用自定义 API 配置的开关(同时控制 URL 和 Key) */}
45+
{(isApiBaseUrlSetByEnv || isApiKeySetByEnv) && (
46+
<SettingsItem label={t('useCustomApi')} description={t('useCustomApiDesc')}>
47+
<Switch
48+
size="sm"
49+
checked={settings.useCustomApi || false}
50+
onChange={e => onSettingsChange({ useCustomApi: e.target.checked })}
51+
/>
5352
</SettingsItem>
5453
)}
54+
55+
{/* API Base URL */}
5556
{visibleIds.has('apiBaseUrl') && (
5657
<SettingsItem label={t('apiBaseUrl')} description={t('apiBaseUrlDesc')}>
5758
<input
5859
type="text"
59-
value={isApiBaseUrlSetByEnv ? '' : (settings.apiBaseUrl || '')}
60+
value={(isApiBaseUrlSetByEnv || isApiKeySetByEnv) && !settings.useCustomApi ? '' : (settings.apiBaseUrl || '')}
6061
onChange={e => onSettingsChange({ apiBaseUrl: e.target.value })}
61-
disabled={isApiBaseUrlSetByEnv}
62+
disabled={(isApiBaseUrlSetByEnv || isApiKeySetByEnv) && !settings.useCustomApi}
6263
placeholder={
63-
isApiBaseUrlSetByEnv
64+
(isApiBaseUrlSetByEnv || isApiKeySetByEnv) && !settings.useCustomApi
6465
? t('apiKeyEnvVar')
6566
: settings.llmProvider === 'openai'
6667
? 'https://api.openai.com'
@@ -70,6 +71,21 @@ export const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ settings, on
7071
/>
7172
</SettingsItem>
7273
)}
74+
75+
{/* API Key */}
76+
{visibleIds.has('apiKey') && (
77+
<SettingsItem label={t('apiKey')} description={t('apiKeyDesc')}>
78+
<textarea
79+
value={(isApiBaseUrlSetByEnv || isApiKeySetByEnv) && !settings.useCustomApi ? '' : (settings.apiKey || []).join('\n')}
80+
onChange={handleApiKeyChange}
81+
disabled={(isApiBaseUrlSetByEnv || isApiKeySetByEnv) && !settings.useCustomApi}
82+
placeholder={(isApiBaseUrlSetByEnv || isApiKeySetByEnv) && !settings.useCustomApi ? t('apiKeyEnvVar') : t('apiKeyPlaceholder')}
83+
className="input-glass max-w-60 min-h-24"
84+
rows={3}
85+
/>
86+
</SettingsItem>
87+
)}
88+
7389
{visibleIds.has('temperature') && (
7490
<SettingsItem label={t('temperature')} description={t('temperatureDesc')}>
7591
<div className="flex items-center gap-4 w-60">

components/settings/SettingsModal.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,31 +62,31 @@ export const SettingsModal: React.FC<SettingsModalProps> = ({ versionInfo, ...pr
6262
{!showAllSections && (
6363
<div className="flex border-b border-[var(--glass-border)] mb-4 -mx-6 px-6">
6464
<button
65-
className={`px-4 py-2 font-medium text-sm ${activeTab === 'general' ? 'text-[var(--accent-color)] border-b-2 border-[var(--accent-color)]' : 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'}`}
65+
className={`px-4 py-2 font-medium text-sm ${activeTab === 'general' ? 'text-[var(--md-sys-color-primary)] border-b-2 border-[var(--md-sys-color-primary)]' : 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'}`}
6666
onClick={() => setActiveTab('general')}
6767
>
6868
{t('general')}
6969
</button>
7070
<button
71-
className={`px-4 py-2 font-medium text-sm ${activeTab === 'behavior' ? 'text-[var(--accent-color)] border-b-2 border-[var(--accent-color)]' : 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'}`}
71+
className={`px-4 py-2 font-medium text-sm ${activeTab === 'behavior' ? 'text-[var(--md-sys-color-primary)] border-b-2 border-[var(--md-sys-color-primary)]' : 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'}`}
7272
onClick={() => setActiveTab('behavior')}
7373
>
7474
{t('behavior')}
7575
</button>
7676
<button
77-
className={`px-4 py-2 font-medium text-sm ${activeTab === 'advanced' ? 'text-[var(--accent-color)] border-b-2 border-[var(--accent-color)]' : 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'}`}
77+
className={`px-4 py-2 font-medium text-sm ${activeTab === 'advanced' ? 'text-[var(--md-sys-color-primary)] border-b-2 border-[var(--md-sys-color-primary)]' : 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'}`}
7878
onClick={() => setActiveTab('advanced')}
7979
>
8080
{t('advanced')}
8181
</button>
8282
<button
83-
className={`px-4 py-2 font-medium text-sm ${activeTab === 'data' ? 'text-[var(--accent-color)] border-b-2 border-[var(--accent-color)]' : 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'}`}
83+
className={`px-4 py-2 font-medium text-sm ${activeTab === 'data' ? 'text-[var(--md-sys-color-primary)] border-b-2 border-[var(--md-sys-color-primary)]' : 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'}`}
8484
onClick={() => setActiveTab('data')}
8585
>
8686
{t('data')}
8787
</button>
8888
<button
89-
className={`px-4 py-2 font-medium text-sm ${activeTab === 'about' ? 'text-[var(--accent-color)] border-b-2 border-[var(--accent-color)]' : 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'}`}
89+
className={`px-4 py-2 font-medium text-sm ${activeTab === 'about' ? 'text-[var(--md-sys-color-primary)] border-b-2 border-[var(--md-sys-color-primary)]' : 'text-[var(--text-color-secondary)] hover:text-[var(--text-color)]'}`}
9090
onClick={() => setActiveTab('about')}
9191
>
9292
{t('about')}

contexts/locales/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export const en = {
7979
apiKeyEnvVar: 'Key set by environment variable',
8080
apiBaseUrl: 'API Base URL',
8181
apiBaseUrlDesc: 'Optional. Use a proxy or a different API endpoint.',
82+
useCustomApi: 'Use Custom API Configuration',
83+
useCustomApiDesc: 'Override environment variables and use custom API URL and Key.',
8284
llmProvider: 'Model Service Provider',
8385
llmProviderDesc: 'Choose the backend large language model service for chat.',
8486
langDetectModel: 'Language Detection Model',

contexts/locales/zh.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export const zh = {
7979
apiKeyEnvVar: '密钥已由环境变量设置',
8080
apiBaseUrl: 'API Base URL',
8181
apiBaseUrlDesc: '可选。使用代理或不同的 API 端点。',
82+
useCustomApi: '使用自定义 API 配置',
83+
useCustomApiDesc: '覆盖环境变量,使用自定义的 API 地址和密钥。',
8284
llmProvider: '模型服务商',
8385
llmProviderDesc: '选择用于聊天的后端大语言模型服务。',
8486
langDetectModel: '语言检测模型',

hooks/useChatMessaging.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,19 @@ export const useChatMessaging = ({ settings, activeChat, personas, setChats, set
3030
}, []);
3131

3232
const _initiateStream = useCallback(async (chatId: string, historyForAPI: Message[], personaId: string | null | undefined, titleGenerationMode: 'INITIAL' | 'RECURRING' | null = null) => {
33-
const apiKeys = settings.apiKey && settings.apiKey.length > 0
34-
? settings.apiKey
35-
: (process.env.API_KEY ? [process.env.API_KEY] : []);
36-
33+
// 获取 API Key:如果用户启用了自定义,使用用户的配置;否则使用环境变量
34+
let apiKeys: string[] = [];
35+
if (settings.useCustomApi) {
36+
// 用户启用了自定义配置,使用用户输入的 API Key
37+
apiKeys = settings.apiKey && settings.apiKey.length > 0 ? settings.apiKey : [];
38+
} else {
39+
// 用户未启用自定义,使用环境变量
40+
const envKey = settings.llmProvider === 'openai'
41+
? process.env.OPENAI_API_KEY
42+
: process.env.GEMINI_API_KEY || process.env.API_KEY;
43+
apiKeys = envKey ? [envKey] : [];
44+
}
45+
3746
if (apiKeys.length === 0) {
3847
const providerName = settings.llmProvider === 'openai' ? 'OpenAI' : 'Gemini';
3948
addToast(`Please set your ${providerName} API key in Settings.`, 'error');
@@ -67,7 +76,19 @@ export const useChatMessaging = ({ settings, activeChat, personas, setChats, set
6776

6877
try {
6978
const llmService = createLLMService(settings);
70-
79+
80+
// 获取 API Base URL:如果用户启用了自定义,使用用户的配置;否则使用环境变量
81+
let apiBaseUrl = '';
82+
if (settings.useCustomApi) {
83+
// 用户启用了自定义配置,使用用户输入的 API Base URL
84+
apiBaseUrl = settings.apiBaseUrl || '';
85+
} else {
86+
// 用户未启用自定义,使用环境变量
87+
apiBaseUrl = settings.llmProvider === 'openai'
88+
? (process.env.OPENAI_API_BASE_URL || '')
89+
: (process.env.API_BASE_URL || '');
90+
}
91+
7192
const chatRequest: ChatRequest = {
7293
messages: historyForAPI,
7394
model: chatSession.model,
@@ -78,7 +99,7 @@ export const useChatMessaging = ({ settings, activeChat, personas, setChats, set
7899
contextLength: settings.contextLength,
79100
},
80101
apiKey: apiKeys[0], // 服务内部目前只处理单个key
81-
apiBaseUrl: settings.apiBaseUrl,
102+
apiBaseUrl: apiBaseUrl,
82103
showThoughts: settings.showThoughts,
83104
enableSearch: settings.enableSearch,
84105
};

hooks/useSettings.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,18 @@ export const useSettings = () => {
8787
const loadedSettings = loadSettings();
8888
const initialSettings = { ...defaultSettings, ...loadedSettings };
8989

90-
// 优先使用环境变量配置(每次启动都重新读取)
90+
// 如果有环境变量配置
9191
if (envConfig) {
9292
initialSettings.llmProvider = envConfig.provider;
93-
initialSettings.apiKey = [envConfig.apiKey];
94-
initialSettings.apiBaseUrl = envConfig.apiBaseUrl;
93+
94+
// 关键修改:只有在用户启用了自定义 API 配置时,才保留用户的设置
95+
// 否则,清空 apiKey 和 apiBaseUrl,避免暴露环境变量
96+
if (!initialSettings.useCustomApi) {
97+
// 用户未启用自定义,清空这些字段,实际使用时从环境变量读取
98+
initialSettings.apiKey = [];
99+
initialSettings.apiBaseUrl = '';
100+
}
101+
// 如果用户启用了自定义,保留 loadedSettings 中的值
95102
}
96103

97104
// 如果没有环境变量配置,保持用户之前的手动配置或默认值
@@ -107,11 +114,11 @@ export const useSettings = () => {
107114
useEffect(() => {
108115
if (!isStorageLoaded) return;
109116

110-
// 保存设置时,排除 apiKey 和 apiBaseUrl(如果来自环境变量
117+
// 保存设置时,排除 apiKey 和 apiBaseUrl(如果来自环境变量且用户未启用自定义
111118
// 这样环境变量不会被缓存到 localStorage
112119
const settingsToSave = { ...settings };
113-
if (hasEnvApiKey) {
114-
// 环境变量有配置,不保存敏感信息到 localStorage
120+
if ((hasEnvApiKey || isApiBaseUrlSetByEnv) && !settings.useCustomApi) {
121+
// 环境变量有配置且用户未启用自定义,不保存 API 配置到 localStorage
115122
delete (settingsToSave as Record<string, unknown>).apiKey;
116123
delete (settingsToSave as Record<string, unknown>).apiBaseUrl;
117124
}

types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export interface Settings {
8989
thinkDeeper: boolean;
9090
enableSearch: boolean;
9191
apiBaseUrl?: string;
92+
useCustomApi?: boolean; // 是否使用自定义 API 配置(包括 URL 和 Key,覆盖环境变量)
9293
temperature?: number;
9394
maxOutputTokens?: number;
9495
contextLength?: number;

0 commit comments

Comments
 (0)