diff --git a/server/src/routes/configs.ts b/server/src/routes/configs.ts index 29020948..f3838a59 100644 --- a/server/src/routes/configs.ts +++ b/server/src/routes/configs.ts @@ -125,7 +125,6 @@ router.put('/api/configs/ai/bulk', (req, res) => { } const bulkSync = db.transaction(() => { - // Read existing keys BEFORE delete const existingKeys = new Map(); const existingRows = db.prepare('SELECT id, api_key_encrypted FROM ai_configs').all() as Array<{ id: string; api_key_encrypted: string }>; for (const row of existingRows) { @@ -139,6 +138,8 @@ router.put('/api/configs/ai/bulk', (req, res) => { VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); + const skippedConfigs: Array<{ id: string; name: string; reason: string }> = []; + for (const c of configs) { let encryptedKey = ''; if (c.apiKey && !c.apiKey.startsWith('***')) { @@ -146,12 +147,28 @@ router.put('/api/configs/ai/bulk', (req, res) => { } else { encryptedKey = existingKeys.get(String(c.id)) ?? ''; } + + if (!encryptedKey) { + skippedConfigs.push({ + id: c.id, + name: c.name ?? '', + reason: c.apiKey?.startsWith('***') + ? 'API key is masked and no existing key found' + : 'API key is empty', + }); + continue; + } + stmt.run( c.id, c.name ?? '', c.apiType ?? 'openai', c.baseUrl ?? '', encryptedKey, c.model ?? '', c.isActive ? 1 : 0, c.customPrompt ?? null, c.useCustomPrompt ? 1 : 0, c.concurrency ?? 1, c.reasoningEffort ?? null ); } + + if (skippedConfigs.length > 0) { + console.warn('[configs] Skipped AI configs with missing keys:', skippedConfigs); + } }); bulkSync(); diff --git a/src/components/DiscoveryView.tsx b/src/components/DiscoveryView.tsx index 8b5c0d38..745fcb1f 100644 --- a/src/components/DiscoveryView.tsx +++ b/src/components/DiscoveryView.tsx @@ -664,8 +664,8 @@ export const DiscoveryView: React.FC = React.memo(() => { return; } - if (activeConfig.apiKeyStatus === 'decrypt_failed') { - alert(t('AI服务的API密钥无法解密,请在设置中重新输入并保存该配置。', 'The AI service API key could not be decrypted. Please re-enter and save the configuration in settings.')); + if (activeConfig.apiKeyStatus === 'decrypt_failed' || activeConfig.apiKeyStatus === 'empty') { + 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.')); return; } diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx index b96faf44..00f8e3c2 100644 --- a/src/components/MarkdownRenderer.tsx +++ b/src/components/MarkdownRenderer.tsx @@ -493,19 +493,17 @@ const MarkdownImage: React.FC<{ src?: string; alt?: string; baseUrl?: string }> if (hasError) { return ( -
- +
+ -
-

- {language === 'zh' ? '图片加载失败' : 'Image failed to load'} -

- {alt &&

{alt}

} -
+ + {language === 'zh' ? '图片加载失败' : 'Image failed'} + + {alt && {alt}} @@ -548,11 +546,11 @@ const MarkdownImage: React.FC<{ src?: string; alt?: string; baseUrl?: string }> ) : (
{isLoading && ( -
- +
+ - {language === 'zh' ? '加载中...' : 'Loading...'} + {language === 'zh' ? '加载中...' : 'Loading...'}
)} diff --git a/src/components/ReadmeModal.tsx b/src/components/ReadmeModal.tsx index e183a94e..71726cc6 100644 --- a/src/components/ReadmeModal.tsx +++ b/src/components/ReadmeModal.tsx @@ -402,7 +402,7 @@ export const ReadmeModal: React.FC = ({ href={repository.html_url} target="_blank" rel="noopener noreferrer" - 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" + 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" title={t('在 GitHub 上查看', 'View on GitHub')} > diff --git a/src/components/RepositoryCard.tsx b/src/components/RepositoryCard.tsx index 88002f86..175250df 100644 --- a/src/components/RepositoryCard.tsx +++ b/src/components/RepositoryCard.tsx @@ -279,8 +279,8 @@ const RepositoryCardComponent: React.FC = ({ return; } - if (activeConfig.apiKeyStatus === 'decrypt_failed') { - alert(language === 'zh' ? 'AI服务的API密钥无法解密,请在设置中重新输入并保存该配置。' : 'The AI service API key could not be decrypted. Please re-enter and save the configuration in settings.'); + if (activeConfig.apiKeyStatus === 'decrypt_failed' || activeConfig.apiKeyStatus === 'empty') { + 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.'); return; } diff --git a/src/components/RepositoryList.tsx b/src/components/RepositoryList.tsx index 1a7ea5d3..27322f6c 100644 --- a/src/components/RepositoryList.tsx +++ b/src/components/RepositoryList.tsx @@ -277,8 +277,8 @@ export const RepositoryList: React.FC = ({ return; } - if (activeConfig.apiKeyStatus === 'decrypt_failed') { - alert(language === 'zh' ? 'AI服务的API密钥无法解密,请在设置中重新输入并保存该配置。' : 'The AI service API key could not be decrypted. Please re-enter and save the configuration in settings.'); + if (activeConfig.apiKeyStatus === 'decrypt_failed' || activeConfig.apiKeyStatus === 'empty') { + 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.'); return; } diff --git a/src/components/SubscriptionRepoCard.tsx b/src/components/SubscriptionRepoCard.tsx index 308f3c90..b57d42c9 100644 --- a/src/components/SubscriptionRepoCard.tsx +++ b/src/components/SubscriptionRepoCard.tsx @@ -209,8 +209,8 @@ export const SubscriptionRepoCard: React.FC = ({ repo return; } - if (activeConfig.apiKeyStatus === 'decrypt_failed') { - alert(t('AI服务的API密钥无法解密,请在设置中重新输入并保存该配置。', 'The AI service API key could not be decrypted. Please re-enter and save the configuration in settings.')); + if (activeConfig.apiKeyStatus === 'decrypt_failed' || activeConfig.apiKeyStatus === 'empty') { + 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.')); return; } diff --git a/src/components/settings/AIConfigPanel.tsx b/src/components/settings/AIConfigPanel.tsx index b7df7033..18c1f29d 100644 --- a/src/components/settings/AIConfigPanel.tsx +++ b/src/components/settings/AIConfigPanel.tsx @@ -152,12 +152,12 @@ export const AIConfigPanel: React.FC = ({ t }) => { setTestingId(config.id); try { const aiService = new AIService(config, language); - const isConnected = await aiService.testConnection(); - - if (isConnected) { + const result = await aiService.testConnection(); + + if (result.success) { alert(t('AI服务连接成功!', 'AI service connection successful!')); } else { - alert(t('AI服务连接失败,请检查配置。', 'AI service connection failed. Please check configuration.')); + alert(result.message); } } catch (error) { console.error('AI test failed:', error); @@ -190,12 +190,12 @@ export const AIConfigPanel: React.FC = ({ t }) => { }; const aiService = new AIService(tempConfig, language); - const isConnected = await aiService.testConnection(); - - if (isConnected) { - alert(t('AI服务连接成功!', 'AI service connection successful!')); + const result = await aiService.testConnection(); + + if (result.success) { + alert(t('✅ AI服务连接成功!', '✅ AI service connection successful!')); } else { - alert(t('AI服务连接失败,请检查配置。', 'AI service connection failed. Please check configuration.')); + alert(result.message); } } catch (error) { console.error('AI test failed:', error); @@ -630,11 +630,11 @@ Focus on practicality and accurate categorization to help users quickly understa {(config.apiType || 'openai').toUpperCase()} • {config.baseUrl} • {config.model} • {t('并发数', 'Concurrency')}: {config.concurrency || 1} {config.reasoningEffort ? ` • reasoning: ${config.reasoningEffort}` : ''}

- {config.apiKeyStatus === 'decrypt_failed' && ( + {(config.apiKeyStatus === 'decrypt_failed' || config.apiKeyStatus === 'empty') && (

{t( - '存储的 API Key 无法解密,请重新输入并保存该配置。', - 'The stored API key could not be decrypted. Please re-enter and save this configuration.' + '存储的 API Key 无法解密或为空,请重新输入并保存该配置。', + 'The stored API key could not be decrypted or is empty. Please re-enter and save this configuration.' )}

)} diff --git a/src/services/aiService.ts b/src/services/aiService.ts index ef5e7b50..876fcf22 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -25,6 +25,38 @@ interface OpenAIResponse { choices?: OpenAIResponseChoice[]; } +export interface ConnectionTestResult { + success: boolean; + statusCode?: number; + statusText?: string; + errorType?: 'network' | 'auth' | 'timeout' | 'server' | 'unknown'; + message: string; +} + +function getStatusCodeMeaning(statusCode: number, language: string): string { + const meanings: Record = { + 400: { zh: '请求参数错误', en: 'Bad Request' }, + 401: { zh: 'API密钥无效或已过期', en: 'Invalid or expired API key' }, + 403: { zh: '没有权限访问该资源', en: 'Forbidden - no permission' }, + 404: { zh: 'API端点或模型不存在', en: 'API endpoint or model not found' }, + 408: { zh: '请求超时', en: 'Request timeout' }, + 429: { zh: '请求过于频繁,已达到速率限制', en: 'Rate limit exceeded' }, + 500: { zh: '服务器内部错误', en: 'Internal server error' }, + 502: { zh: '网关错误,服务器暂时不可用', en: 'Bad Gateway' }, + 503: { zh: '服务暂时不可用,请稍后重试', en: 'Service unavailable' }, + 504: { zh: '网关超时', en: 'Gateway timeout' }, + }; + return meanings[statusCode]?.[language as 'zh' | 'en'] || (language === 'zh' ? '未知错误' : 'Unknown error'); +} + +function getErrorTypeFromStatus(statusCode: number): ConnectionTestResult['errorType'] { + if (statusCode === 401 || statusCode === 403) return 'auth'; + if (statusCode === 408 || statusCode === 504) return 'timeout'; + if (statusCode >= 500) return 'server'; + if (statusCode >= 400) return 'unknown'; + return 'unknown'; +} + export class AIService { private config: AIConfig; private language: string; @@ -442,14 +474,23 @@ Focus on practicality and accurate categorization to help users quickly understa } } - async testConnection(): Promise { + async testConnection(): Promise { + const apiType = this.getApiType(); + const timeoutMs = apiType === 'openai-responses' || this.config.reasoningEffort ? 30000 : 10000; + try { const base = new URL(this.config.baseUrl); - if (base.protocol !== 'http:' && base.protocol !== 'https:') return false; + if (base.protocol !== 'http:' && base.protocol !== 'https:') { + return { + success: false, + errorType: 'unknown', + message: this.language === 'zh' + ? '无效的协议,请使用 http:// 或 https://' + : 'Invalid protocol, please use http:// or https://', + }; + } const controller = new AbortController(); - const apiType = this.getApiType(); - const timeoutMs = apiType === 'openai-responses' || this.config.reasoningEffort ? 30000 : 10000; const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const content = await this.requestText({ @@ -459,12 +500,92 @@ Focus on practicality and accurate categorization to help users quickly understa maxTokens: 50, signal: controller.signal, }); - return !!content; + if (content) { + return { + success: true, + message: this.language === 'zh' ? '连接成功' : 'Connection successful', + }; + } + return { + success: false, + errorType: 'unknown', + message: this.language === 'zh' ? '未收到响应内容' : 'No content received', + }; } finally { clearTimeout(timeoutId); } - } catch { - return false; + } catch (error) { + const err = error as Error; + const errorMessage = err.message || ''; + + // 解析状态码 + const statusMatch = errorMessage.match(/(\d{3})/); + const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : undefined; + + // 处理超时错误 + if (errorMessage.includes('timeout') || errorMessage.includes('abort') || err.name === 'AbortError') { + return { + success: false, + errorType: 'timeout', + message: this.language === 'zh' + ? `连接超时(${timeoutMs / 1000}秒)。请检查:1. 网络连接是否正常 2. API端点是否正确 3. 服务器是否响应缓慢` + : `Connection timeout (${timeoutMs / 1000}s). Please check: 1. Network connection 2. API endpoint 3. Server response time`, + }; + } + + // 处理网络错误 + if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('Failed to fetch')) { + return { + success: false, + errorType: 'network', + message: this.language === 'zh' + ? '网络连接失败。请检查:1. 网络连接是否正常 2. API端点地址是否正确 3. 防火墙或代理设置' + : 'Network connection failed. Please check: 1. Network connection 2. API endpoint 3. Firewall or proxy settings', + }; + } + + // 如果有状态码,提供详细的错误信息 + if (statusCode) { + const meaning = getStatusCodeMeaning(statusCode, this.language); + const errorType = getErrorTypeFromStatus(statusCode) ?? 'unknown'; + const suggestions: Record = { + auth: { + zh: '请检查 API 密钥是否正确,或密钥是否已过期', + en: 'Please check if the API key is correct or expired', + }, + timeout: { + zh: '请求超时,请稍后重试或检查网络连接', + en: 'Request timeout, please retry later or check network', + }, + server: { + zh: '服务器端错误,请稍后重试或联系服务提供商', + en: 'Server error, please retry later or contact provider', + }, + unknown: { + zh: '请检查 API 端点、模型名称和请求参数是否正确', + en: 'Please check API endpoint, model name and request parameters', + }, + }; + + return { + success: false, + statusCode, + statusText: meaning, + errorType, + message: this.language === 'zh' + ? `HTTP ${statusCode} - ${meaning}\n建议:${suggestions[errorType].zh}` + : `HTTP ${statusCode} - ${meaning}\nSuggestion: ${suggestions[errorType].en}`, + }; + } + + // 默认错误 + return { + success: false, + errorType: 'unknown', + message: this.language === 'zh' + ? `连接失败:${errorMessage || '未知错误'}\n请检查 API 端点、API 密钥和模型名称是否正确` + : `Connection failed: ${errorMessage || 'Unknown error'}\nPlease check API endpoint, API key and model name`, + }; } }