Skip to content

Commit 94dc809

Browse files
SummerRay160HappySummercoderabbitai[bot]
authored
Fix UI improvements and AI configuration error handling (AmintaCCCP#114)
* feat: 改进侧栏滚动行为并修复主题持久化 修复持久化状态中的主题设置问题,不再强制使用暗色主题 改进发现页面的滚动行为,添加侧栏固定功能 * Update src/store/useAppStore.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * refactor(DiscoveryView): 移除未使用的scrollContainerRef引用 * refactor(DiscoveryView): 使用 window.scrollY 替代 scrollContainerRef 处理滚动位置 移除对 scrollContainerRef 的依赖,统一使用 window 的滚动位置 API 简化滚动位置保存和恢复逻辑,提高代码一致性 * style(ReleaseTimeline): 为暗黑模式添加背景和边框颜色 添加暗黑模式下的背景和边框颜色样式,提升夜间使用的视觉体验 * style: 统一组件颜色样式为中性灰调 调整多个组件中的颜色样式,将品牌色和状态色统一为中性灰调色板,提升视觉一致性。主要修改包括: - 替换品牌色和状态色为中性灰 - 统一按钮、图标、卡片等元素的颜色样式 - 优化暗黑模式下的颜色对比度 * refactor(数据管理面板): 优化数据删除操作的类型定义和状态管理 - 更新类型定义,使用 SubscriptionRepo 和 SubscriptionChannel 替代原有类型 - 重构 assetFilters 的状态管理,改用 useAppStore.setState - 添加 searchHistoryVersion 状态用于跟踪搜索历史变化 - 优化删除操作的确认提示文案和翻译 - 改进 UI 交互细节和样式 * style(ui): 优化多个组件的样式和颜色方案 - 为 SliderInput 组件添加 showMarks 属性控制标记显示 - 更新 BulkActionToolbar 按钮颜色以符合新设计规范 - 调整 BackToTop 和 ScrollToBottom 组件的背景和边框样式 - 简化 SearchBar 的 applyFilters 函数参数 - 移除 DiscoveryView 中未使用的 currentCount 和 isSidebarFixed - 改进 MarkdownRenderer 的代码块样式和终端模拟器外观 - 修复 MarkdownImage 组件中文本透明度问题 * fix: 修复代码块背景色和优化状态管理 调整Markdown渲染器中代码块的背景色透明度表示方式 优化数据管理面板中搜索历史版本的状态管理,移除不必要的依赖 * fix(ReadmeModal): 修正暗黑模式下链接文本颜色问题 * fix(api): 处理空API密钥和连接测试错误 - 在多个组件中添加对空API密钥状态的检查 - 改进AI服务连接测试的错误处理和反馈 - 优化API密钥批量更新时的空值处理 - 调整图片加载失败时的UI样式 * Update src/components/settings/AIConfigPanel.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: HappySummer <zjhao233@126.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 6fba089 commit 94dc809

9 files changed

Lines changed: 177 additions & 41 deletions

File tree

server/src/routes/configs.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ router.put('/api/configs/ai/bulk', (req, res) => {
125125
}
126126

127127
const bulkSync = db.transaction(() => {
128-
// Read existing keys BEFORE delete
129128
const existingKeys = new Map<string, string>();
130129
const existingRows = db.prepare('SELECT id, api_key_encrypted FROM ai_configs').all() as Array<{ id: string; api_key_encrypted: string }>;
131130
for (const row of existingRows) {
@@ -139,19 +138,37 @@ router.put('/api/configs/ai/bulk', (req, res) => {
139138
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
140139
`);
141140

141+
const skippedConfigs: Array<{ id: string; name: string; reason: string }> = [];
142+
142143
for (const c of configs) {
143144
let encryptedKey = '';
144145
if (c.apiKey && !c.apiKey.startsWith('***')) {
145146
encryptedKey = encrypt(c.apiKey, config.encryptionKey);
146147
} else {
147148
encryptedKey = existingKeys.get(String(c.id)) ?? '';
148149
}
150+
151+
if (!encryptedKey) {
152+
skippedConfigs.push({
153+
id: c.id,
154+
name: c.name ?? '',
155+
reason: c.apiKey?.startsWith('***')
156+
? 'API key is masked and no existing key found'
157+
: 'API key is empty',
158+
});
159+
continue;
160+
}
161+
149162
stmt.run(
150163
c.id, c.name ?? '', c.apiType ?? 'openai', c.baseUrl ?? '',
151164
encryptedKey, c.model ?? '', c.isActive ? 1 : 0,
152165
c.customPrompt ?? null, c.useCustomPrompt ? 1 : 0, c.concurrency ?? 1, c.reasoningEffort ?? null
153166
);
154167
}
168+
169+
if (skippedConfigs.length > 0) {
170+
console.warn('[configs] Skipped AI configs with missing keys:', skippedConfigs);
171+
}
155172
});
156173

157174
bulkSync();

src/components/DiscoveryView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,8 +664,8 @@ export const DiscoveryView: React.FC = React.memo(() => {
664664
return;
665665
}
666666

667-
if (activeConfig.apiKeyStatus === 'decrypt_failed') {
668-
alert(t('AI服务的API密钥无法解密,请在设置中重新输入并保存该配置。', 'The AI service API key could not be decrypted. Please re-enter and save the configuration in settings.'));
667+
if (activeConfig.apiKeyStatus === 'decrypt_failed' || activeConfig.apiKeyStatus === 'empty') {
668+
alert(t('AI服务的API密钥无法解密或为空,请在设置中重新输入并保存该配置。', 'The AI service API key could not be decrypted or is empty. Please re-enter and save the configuration in settings.'));
669669
return;
670670
}
671671

src/components/MarkdownRenderer.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -493,19 +493,17 @@ const MarkdownImage: React.FC<{ src?: string; alt?: string; baseUrl?: string }>
493493

494494
if (hasError) {
495495
return (
496-
<div className="my-4 p-4 bg-gray-100 dark:bg-white/[0.04] rounded-lg border border-black/[0.06] dark:border-white/[0.04] flex items-center gap-3">
497-
<svg className="w-5 h-5 text-gray-700 dark:text-text-secondary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
496+
<div className="my-2 px-3 py-2 bg-gray-100 dark:bg-white/[0.04] rounded border border-black/[0.06] dark:border-white/[0.04] flex items-center gap-2 text-xs">
497+
<svg className="w-4 h-4 text-gray-500 dark:text-text-tertiary flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
498498
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
499499
</svg>
500-
<div className="flex-1 min-w-0">
501-
<p className="text-sm text-gray-700 dark:text-text-secondary font-medium">
502-
{language === 'zh' ? '图片加载失败' : 'Image failed to load'}
503-
</p>
504-
{alt && <p className="text-xs text-gray-700 dark:text-text-secondary truncate">{alt}</p>}
505-
</div>
500+
<span className="text-gray-500 dark:text-text-tertiary">
501+
{language === 'zh' ? '图片加载失败' : 'Image failed'}
502+
</span>
503+
{alt && <span className="text-gray-400 dark:text-text-quaternary truncate max-w-[120px]">{alt}</span>}
506504
<button
507505
onClick={handleRetry}
508-
className="px-2 py-1 text-xs bg-gray-100 dark:bg-white/[0.04] text-gray-700 dark:text-text-secondary rounded hover:bg-gray-200 dark:hover:bg-white/[0.08] transition-colors flex-shrink-0"
506+
className="ml-auto px-2 py-0.5 text-xs text-brand-violet hover:text-brand-violet/80 transition-colors flex-shrink-0"
509507
>
510508
{language === 'zh' ? '重试' : 'Retry'}
511509
</button>
@@ -548,11 +546,11 @@ const MarkdownImage: React.FC<{ src?: string; alt?: string; baseUrl?: string }>
548546
) : (
549547
<div className="my-4 flex flex-col items-center group/img">
550548
{isLoading && (
551-
<div className="w-full max-w-md h-48 bg-light-surface dark:bg-panel-dark rounded-xl flex flex-col items-center justify-center animate-pulse gap-2">
552-
<svg className="w-8 h-8 text-gray-300 dark:text-text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
549+
<div className="w-full max-w-md h-16 bg-light-surface dark:bg-panel-dark rounded-lg flex items-center justify-center animate-pulse gap-2">
550+
<svg className="w-5 h-5 text-gray-300 dark:text-text-tertiary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
553551
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
554552
</svg>
555-
<span className="text-xs text-gray-400 dark:text-text-tertiary">{language === 'zh' ? '加载中...' : 'Loading...'}</span>
553+
<span className="text-xs text-gray-400 dark:text-text-quaternary">{language === 'zh' ? '加载中...' : 'Loading...'}</span>
556554
</div>
557555
)}
558556

src/components/ReadmeModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ export const ReadmeModal: React.FC<ReadmeModalProps> = ({
402402
href={repository.html_url}
403403
target="_blank"
404404
rel="noopener noreferrer"
405-
className="flex items-center space-x-1 px-3 py-2 text-sm text-gray-700 dark:text-text-secondary hover:text-gray-900 dark:hover:text-gray-900 hover:bg-light-surface dark:hover:bg-white/10 rounded-lg transition-colors"
405+
className="flex items-center space-x-1 px-3 py-2 text-sm text-gray-700 dark:text-text-primary hover:text-gray-900 dark:hover:text-white hover:bg-light-surface dark:hover:bg-white/10 rounded-lg transition-colors"
406406
title={t('在 GitHub 上查看', 'View on GitHub')}
407407
>
408408
<ExternalLink className="w-4 h-4" />

src/components/RepositoryCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,8 @@ const RepositoryCardComponent: React.FC<RepositoryCardProps> = ({
279279
return;
280280
}
281281

282-
if (activeConfig.apiKeyStatus === 'decrypt_failed') {
283-
alert(language === 'zh' ? 'AI服务的API密钥无法解密,请在设置中重新输入并保存该配置。' : 'The AI service API key could not be decrypted. Please re-enter and save the configuration in settings.');
282+
if (activeConfig.apiKeyStatus === 'decrypt_failed' || activeConfig.apiKeyStatus === 'empty') {
283+
alert(language === 'zh' ? 'AI服务的API密钥无法解密或为空,请在设置中重新输入并保存该配置。' : 'The AI service API key could not be decrypted or is empty. Please re-enter and save the configuration in settings.');
284284
return;
285285
}
286286

src/components/RepositoryList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ export const RepositoryList: React.FC<RepositoryListProps> = ({
277277
return;
278278
}
279279

280-
if (activeConfig.apiKeyStatus === 'decrypt_failed') {
281-
alert(language === 'zh' ? 'AI服务的API密钥无法解密,请在设置中重新输入并保存该配置。' : 'The AI service API key could not be decrypted. Please re-enter and save the configuration in settings.');
280+
if (activeConfig.apiKeyStatus === 'decrypt_failed' || activeConfig.apiKeyStatus === 'empty') {
281+
alert(language === 'zh' ? 'AI服务的API密钥无法解密或为空,请在设置中重新输入并保存该配置。' : 'The AI service API key could not be decrypted or is empty. Please re-enter and save the configuration in settings.');
282282
return;
283283
}
284284

src/components/SubscriptionRepoCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,8 @@ export const SubscriptionRepoCard: React.FC<SubscriptionRepoCardProps> = ({ repo
209209
return;
210210
}
211211

212-
if (activeConfig.apiKeyStatus === 'decrypt_failed') {
213-
alert(t('AI服务的API密钥无法解密,请在设置中重新输入并保存该配置。', 'The AI service API key could not be decrypted. Please re-enter and save the configuration in settings.'));
212+
if (activeConfig.apiKeyStatus === 'decrypt_failed' || activeConfig.apiKeyStatus === 'empty') {
213+
alert(t('AI服务的API密钥无法解密或为空,请在设置中重新输入并保存该配置。', 'The AI service API key could not be decrypted or is empty. Please re-enter and save the configuration in settings.'));
214214
return;
215215
}
216216

src/components/settings/AIConfigPanel.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,12 @@ export const AIConfigPanel: React.FC<AIConfigPanelProps> = ({ t }) => {
152152
setTestingId(config.id);
153153
try {
154154
const aiService = new AIService(config, language);
155-
const isConnected = await aiService.testConnection();
156-
157-
if (isConnected) {
155+
const result = await aiService.testConnection();
156+
157+
if (result.success) {
158158
alert(t('AI服务连接成功!', 'AI service connection successful!'));
159159
} else {
160-
alert(t('AI服务连接失败,请检查配置。', 'AI service connection failed. Please check configuration.'));
160+
alert(result.message);
161161
}
162162
} catch (error) {
163163
console.error('AI test failed:', error);
@@ -190,12 +190,12 @@ export const AIConfigPanel: React.FC<AIConfigPanelProps> = ({ t }) => {
190190
};
191191

192192
const aiService = new AIService(tempConfig, language);
193-
const isConnected = await aiService.testConnection();
194-
195-
if (isConnected) {
196-
alert(t('AI服务连接成功!', 'AI service connection successful!'));
193+
const result = await aiService.testConnection();
194+
195+
if (result.success) {
196+
alert(t('AI服务连接成功!', 'AI service connection successful!'));
197197
} else {
198-
alert(t('AI服务连接失败,请检查配置。', 'AI service connection failed. Please check configuration.'));
198+
alert(result.message);
199199
}
200200
} catch (error) {
201201
console.error('AI test failed:', error);
@@ -630,11 +630,11 @@ Focus on practicality and accurate categorization to help users quickly understa
630630
{(config.apiType || 'openai').toUpperCase()}{config.baseUrl}{config.model}{t('并发数', 'Concurrency')}: {config.concurrency || 1}
631631
{config.reasoningEffort ? ` • reasoning: ${config.reasoningEffort}` : ''}
632632
</p>
633-
{config.apiKeyStatus === 'decrypt_failed' && (
633+
{(config.apiKeyStatus === 'decrypt_failed' || config.apiKeyStatus === 'empty') && (
634634
<p className="mt-1 text-sm text-gray-700 dark:text-text-secondary ">
635635
{t(
636-
'存储的 API Key 无法解密,请重新输入并保存该配置。',
637-
'The stored API key could not be decrypted. Please re-enter and save this configuration.'
636+
'存储的 API Key 无法解密或为空,请重新输入并保存该配置。',
637+
'The stored API key could not be decrypted or is empty. Please re-enter and save this configuration.'
638638
)}
639639
</p>
640640
)}

src/services/aiService.ts

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,38 @@ interface OpenAIResponse {
2525
choices?: OpenAIResponseChoice[];
2626
}
2727

28+
export interface ConnectionTestResult {
29+
success: boolean;
30+
statusCode?: number;
31+
statusText?: string;
32+
errorType?: 'network' | 'auth' | 'timeout' | 'server' | 'unknown';
33+
message: string;
34+
}
35+
36+
function getStatusCodeMeaning(statusCode: number, language: string): string {
37+
const meanings: Record<number, { zh: string; en: string }> = {
38+
400: { zh: '请求参数错误', en: 'Bad Request' },
39+
401: { zh: 'API密钥无效或已过期', en: 'Invalid or expired API key' },
40+
403: { zh: '没有权限访问该资源', en: 'Forbidden - no permission' },
41+
404: { zh: 'API端点或模型不存在', en: 'API endpoint or model not found' },
42+
408: { zh: '请求超时', en: 'Request timeout' },
43+
429: { zh: '请求过于频繁,已达到速率限制', en: 'Rate limit exceeded' },
44+
500: { zh: '服务器内部错误', en: 'Internal server error' },
45+
502: { zh: '网关错误,服务器暂时不可用', en: 'Bad Gateway' },
46+
503: { zh: '服务暂时不可用,请稍后重试', en: 'Service unavailable' },
47+
504: { zh: '网关超时', en: 'Gateway timeout' },
48+
};
49+
return meanings[statusCode]?.[language as 'zh' | 'en'] || (language === 'zh' ? '未知错误' : 'Unknown error');
50+
}
51+
52+
function getErrorTypeFromStatus(statusCode: number): ConnectionTestResult['errorType'] {
53+
if (statusCode === 401 || statusCode === 403) return 'auth';
54+
if (statusCode === 408 || statusCode === 504) return 'timeout';
55+
if (statusCode >= 500) return 'server';
56+
if (statusCode >= 400) return 'unknown';
57+
return 'unknown';
58+
}
59+
2860
export class AIService {
2961
private config: AIConfig;
3062
private language: string;
@@ -442,14 +474,23 @@ Focus on practicality and accurate categorization to help users quickly understa
442474
}
443475
}
444476

445-
async testConnection(): Promise<boolean> {
477+
async testConnection(): Promise<ConnectionTestResult> {
478+
const apiType = this.getApiType();
479+
const timeoutMs = apiType === 'openai-responses' || this.config.reasoningEffort ? 30000 : 10000;
480+
446481
try {
447482
const base = new URL(this.config.baseUrl);
448-
if (base.protocol !== 'http:' && base.protocol !== 'https:') return false;
483+
if (base.protocol !== 'http:' && base.protocol !== 'https:') {
484+
return {
485+
success: false,
486+
errorType: 'unknown',
487+
message: this.language === 'zh'
488+
? '无效的协议,请使用 http:// 或 https://'
489+
: 'Invalid protocol, please use http:// or https://',
490+
};
491+
}
449492

450493
const controller = new AbortController();
451-
const apiType = this.getApiType();
452-
const timeoutMs = apiType === 'openai-responses' || this.config.reasoningEffort ? 30000 : 10000;
453494
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
454495
try {
455496
const content = await this.requestText({
@@ -459,12 +500,92 @@ Focus on practicality and accurate categorization to help users quickly understa
459500
maxTokens: 50,
460501
signal: controller.signal,
461502
});
462-
return !!content;
503+
if (content) {
504+
return {
505+
success: true,
506+
message: this.language === 'zh' ? '连接成功' : 'Connection successful',
507+
};
508+
}
509+
return {
510+
success: false,
511+
errorType: 'unknown',
512+
message: this.language === 'zh' ? '未收到响应内容' : 'No content received',
513+
};
463514
} finally {
464515
clearTimeout(timeoutId);
465516
}
466-
} catch {
467-
return false;
517+
} catch (error) {
518+
const err = error as Error;
519+
const errorMessage = err.message || '';
520+
521+
// 解析状态码
522+
const statusMatch = errorMessage.match(/(\d{3})/);
523+
const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : undefined;
524+
525+
// 处理超时错误
526+
if (errorMessage.includes('timeout') || errorMessage.includes('abort') || err.name === 'AbortError') {
527+
return {
528+
success: false,
529+
errorType: 'timeout',
530+
message: this.language === 'zh'
531+
? `连接超时(${timeoutMs / 1000}秒)。请检查:1. 网络连接是否正常 2. API端点是否正确 3. 服务器是否响应缓慢`
532+
: `Connection timeout (${timeoutMs / 1000}s). Please check: 1. Network connection 2. API endpoint 3. Server response time`,
533+
};
534+
}
535+
536+
// 处理网络错误
537+
if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('Failed to fetch')) {
538+
return {
539+
success: false,
540+
errorType: 'network',
541+
message: this.language === 'zh'
542+
? '网络连接失败。请检查:1. 网络连接是否正常 2. API端点地址是否正确 3. 防火墙或代理设置'
543+
: 'Network connection failed. Please check: 1. Network connection 2. API endpoint 3. Firewall or proxy settings',
544+
};
545+
}
546+
547+
// 如果有状态码,提供详细的错误信息
548+
if (statusCode) {
549+
const meaning = getStatusCodeMeaning(statusCode, this.language);
550+
const errorType = getErrorTypeFromStatus(statusCode) ?? 'unknown';
551+
const suggestions: Record<string, { zh: string; en: string }> = {
552+
auth: {
553+
zh: '请检查 API 密钥是否正确,或密钥是否已过期',
554+
en: 'Please check if the API key is correct or expired',
555+
},
556+
timeout: {
557+
zh: '请求超时,请稍后重试或检查网络连接',
558+
en: 'Request timeout, please retry later or check network',
559+
},
560+
server: {
561+
zh: '服务器端错误,请稍后重试或联系服务提供商',
562+
en: 'Server error, please retry later or contact provider',
563+
},
564+
unknown: {
565+
zh: '请检查 API 端点、模型名称和请求参数是否正确',
566+
en: 'Please check API endpoint, model name and request parameters',
567+
},
568+
};
569+
570+
return {
571+
success: false,
572+
statusCode,
573+
statusText: meaning,
574+
errorType,
575+
message: this.language === 'zh'
576+
? `HTTP ${statusCode} - ${meaning}\n建议:${suggestions[errorType].zh}`
577+
: `HTTP ${statusCode} - ${meaning}\nSuggestion: ${suggestions[errorType].en}`,
578+
};
579+
}
580+
581+
// 默认错误
582+
return {
583+
success: false,
584+
errorType: 'unknown',
585+
message: this.language === 'zh'
586+
? `连接失败:${errorMessage || '未知错误'}\n请检查 API 端点、API 密钥和模型名称是否正确`
587+
: `Connection failed: ${errorMessage || 'Unknown error'}\nPlease check API endpoint, API key and model name`,
588+
};
468589
}
469590
}
470591

0 commit comments

Comments
 (0)