From 32064a353192e14ffe04dae64d700590ba584fa8 Mon Sep 17 00:00:00 2001 From: kurihada Date: Sun, 14 Dec 2025 22:24:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(config):=20=E4=BC=98=E9=9B=85=E5=A4=84?= =?UTF-8?q?=E7=90=86=20Provider=20=E6=9C=AA=E9=85=8D=E7=BD=AE=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 ConfigurationError 类替代 process.exit(1) - Server 端捕获配置错误并返回友好消息 - UI 端支持 config_error 类型的 WebSocket 消息 - 服务器不再因配置缺失而崩溃 --- packages/core/src/index.ts | 2 +- packages/core/src/utils/config.ts | 25 ++++++++++-- packages/server/src/agent/adapter.ts | 61 ++++++++++++++++++++++++---- packages/ui/src/api/client.ts | 2 + packages/ui/src/api/types.ts | 18 ++++++++ packages/ui/src/hooks/useChat.ts | 14 ++++++- 6 files changed, 107 insertions(+), 15 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5852784..fc81ec4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,6 @@ export { Agent } from './core/agent.js'; export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js'; -export { loadConfig, saveConfig, getConfig, loadVisionConfig } from './utils/config.js'; +export { loadConfig, saveConfig, getConfig, loadVisionConfig, ConfigurationError } from './utils/config.js'; export type { VisionConfig } from './utils/config.js'; // Context compression diff --git a/packages/core/src/utils/config.ts b/packages/core/src/utils/config.ts index 4125523..6b56a00 100644 --- a/packages/core/src/utils/config.ts +++ b/packages/core/src/utils/config.ts @@ -4,6 +4,23 @@ import * as os from 'os'; import type { AgentConfig, ProviderType } from '../types/index.js'; import { providerRegistry, resolveApiKey } from '../provider/index.js'; +/** + * 配置错误异常 + * + * 当配置缺失或无效时抛出,用于替代 process.exit() + * 允许调用方优雅地处理错误并向用户显示友好提示 + */ +export class ConfigurationError extends Error { + constructor( + message: string, + public readonly provider: string, + public readonly missingKey: 'apiKey' | 'provider' + ) { + super(message); + this.name = 'ConfigurationError'; + } +} + const CONFIG_DIR = path.join(os.homedir(), '.ai-terminal-assistant'); const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json'); @@ -90,9 +107,11 @@ export function loadConfig(): AgentConfig { const finalApiKey = resolveApiKey(providerConfig); if (!finalApiKey) { - console.error(`❌ 错误: 未配置 API Key`); - console.error(`请在设置中配置 ${finalProvider} 的 API Key`); - process.exit(1); + throw new ConfigurationError( + `未配置 ${finalProvider} 的 API Key,请在 Provider 设置中配置`, + finalProvider, + 'apiKey' + ); } // 确定模型 diff --git a/packages/server/src/agent/adapter.ts b/packages/server/src/agent/adapter.ts index 5565cd9..dd4c1ed 100644 --- a/packages/server/src/agent/adapter.ts +++ b/packages/server/src/agent/adapter.ts @@ -97,6 +97,9 @@ let coreModule: CoreModule | null = null; // Agent 实例缓存(每个 session 一个) const agentCache: Map = new Map(); +// 配置错误缓存(用于向客户端返回友好错误) +let lastConfigError: { provider: string; message: string } | null = null; + // ============================================================================ // 公共 API // ============================================================================ @@ -135,8 +138,13 @@ export function isCoreAvailable(): boolean { /** * 获取或创建 Agent 实例 + * + * @returns Agent 实例,或 null(Core 不可用或配置错误) */ export function getOrCreateAgent(sessionId: string): AgentInstance | null { + // 清除之前的配置错误 + lastConfigError = null; + if (!coreModule) { return null; } @@ -146,17 +154,32 @@ export function getOrCreateAgent(sessionId: string): AgentInstance | null { return agentCache.get(sessionId)!; } - // 创建新 Agent - const config = coreModule.loadConfig(); - const agent = new coreModule.Agent(config); - agent.setRegistry(coreModule.toolRegistry); + try { + // 创建新 Agent(可能抛出 ConfigurationError) + const config = coreModule.loadConfig(); + const agent = new coreModule.Agent(config); + agent.setRegistry(coreModule.toolRegistry); - // 设置权限回调,通过 WebSocket 请求用户确认 - const permissionManager = coreModule.getPermissionManager(); - permissionManager.setAskCallback(createServerPermissionCallback(sessionId)); + // 设置权限回调,通过 WebSocket 请求用户确认 + const permissionManager = coreModule.getPermissionManager(); + permissionManager.setAskCallback(createServerPermissionCallback(sessionId)); - agentCache.set(sessionId, agent); - return agent; + agentCache.set(sessionId, agent); + return agent; + } catch (error) { + // 检测配置错误(通过 error.name 识别,避免直接依赖 Core 类型) + if (error instanceof Error && error.name === 'ConfigurationError') { + const configError = error as Error & { provider?: string }; + lastConfigError = { + provider: configError.provider || 'unknown', + message: error.message, + }; + console.warn(`[Agent] Configuration error: ${error.message}`); + return null; + } + // 其他错误继续抛出 + throw error; + } } /** @@ -180,6 +203,26 @@ export async function processMessage(sessionId: string, content: string): Promis const agent = getOrCreateAgent(sessionId); if (!agent) { + // 检查是否为配置错误 + if (lastConfigError) { + // 返回配置错误,引导用户配置 Provider + broadcastToSession(sessionId, { + type: 'error', + sessionId, + payload: { + type: 'config_error', + message: lastConfigError.message, + provider: lastConfigError.provider, + action: 'open_providers_panel', + }, + }); + + emitLogEvent(sessionId, 'error', `配置错误: ${lastConfigError.message}`); + sessionManager.updateStatus(sessionId, 'idle' as SessionStatus); + emitStatusEvent(sessionId, 'idle'); + return; + } + // Core 模块不可用,返回占位响应 broadcastToSession(sessionId, { type: 'chunk', diff --git a/packages/ui/src/api/client.ts b/packages/ui/src/api/client.ts index 5c776c1..5aa9615 100644 --- a/packages/ui/src/api/client.ts +++ b/packages/ui/src/api/client.ts @@ -122,6 +122,8 @@ export type { CompressionStatus, CompressionType, CompressionResult, + // WebSocket error types + ConfigErrorPayload, } from './types.js'; // API Configuration diff --git a/packages/ui/src/api/types.ts b/packages/ui/src/api/types.ts index 553bff6..58904e3 100644 --- a/packages/ui/src/api/types.ts +++ b/packages/ui/src/api/types.ts @@ -756,3 +756,21 @@ export interface CompressionResult { summaryTokens?: number; } +// ============ WebSocket 错误相关 ============ + +/** + * 配置错误 Payload + * + * 当 Provider 未配置 API Key 时,通过 WebSocket 返回此错误 + */ +export interface ConfigErrorPayload { + /** 错误类型标识 */ + type: 'config_error'; + /** 错误消息 */ + message: string; + /** 缺失配置的 Provider */ + provider?: string; + /** 建议的操作 */ + action: 'open_providers_panel' | 'open_settings'; +} + diff --git a/packages/ui/src/hooks/useChat.ts b/packages/ui/src/hooks/useChat.ts index a861fb6..6d3db58 100644 --- a/packages/ui/src/hooks/useChat.ts +++ b/packages/ui/src/hooks/useChat.ts @@ -7,12 +7,15 @@ import { useState, useEffect, useCallback, useRef } from 'react'; import { createWebSocket, getMessages, type Message } from '../api/client.js'; import type { PermissionRequest } from '../components/PermissionDialog.js'; +import type { ConfigErrorPayload } from '../api/types.js'; interface UseChatOptions { sessionId: string; onError?: (error: Error) => void; onSessionNotFound?: () => void; onSessionUpdated?: (sessionId: string, name: string) => void; + /** 配置错误回调(如 API Key 未配置) */ + onConfigError?: (error: ConfigErrorPayload) => void; } interface ChatState { @@ -23,7 +26,7 @@ interface ChatState { permissionRequest: PermissionRequest | null; } -export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdated }: UseChatOptions) { +export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdated, onConfigError }: UseChatOptions) { const [state, setState] = useState({ messages: [], isConnected: false, @@ -43,9 +46,11 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate const onErrorRef = useRef(onError); const onSessionNotFoundRef = useRef(onSessionNotFound); const onSessionUpdatedRef = useRef(onSessionUpdated); + const onConfigErrorRef = useRef(onConfigError); onErrorRef.current = onError; onSessionNotFoundRef.current = onSessionNotFound; onSessionUpdatedRef.current = onSessionUpdated; + onConfigErrorRef.current = onConfigError; // 加载历史消息 const loadMessages = useCallback(async () => { @@ -137,7 +142,12 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate break; case 'error': - onErrorRef.current?.(new Error(message.payload?.message || 'Unknown error')); + // 检查是否为配置错误 + if (message.payload?.type === 'config_error') { + onConfigErrorRef.current?.(message.payload as ConfigErrorPayload); + } else { + onErrorRef.current?.(new Error(message.payload?.message || 'Unknown error')); + } setState((prev) => ({ ...prev, isLoading: false, streamingContent: '' })); break;