fix(config): 优雅处理 Provider 未配置错误
- 添加 ConfigurationError 类替代 process.exit(1) - Server 端捕获配置错误并返回友好消息 - UI 端支持 config_error 类型的 WebSocket 消息 - 服务器不再因配置缺失而崩溃
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
export { Agent } from './core/agent.js';
|
export { Agent } from './core/agent.js';
|
||||||
export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.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';
|
export type { VisionConfig } from './utils/config.js';
|
||||||
|
|
||||||
// Context compression
|
// Context compression
|
||||||
|
|||||||
@@ -4,6 +4,23 @@ import * as os from 'os';
|
|||||||
import type { AgentConfig, ProviderType } from '../types/index.js';
|
import type { AgentConfig, ProviderType } from '../types/index.js';
|
||||||
import { providerRegistry, resolveApiKey } from '../provider/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_DIR = path.join(os.homedir(), '.ai-terminal-assistant');
|
||||||
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
||||||
|
|
||||||
@@ -90,9 +107,11 @@ export function loadConfig(): AgentConfig {
|
|||||||
const finalApiKey = resolveApiKey(providerConfig);
|
const finalApiKey = resolveApiKey(providerConfig);
|
||||||
|
|
||||||
if (!finalApiKey) {
|
if (!finalApiKey) {
|
||||||
console.error(`❌ 错误: 未配置 API Key`);
|
throw new ConfigurationError(
|
||||||
console.error(`请在设置中配置 ${finalProvider} 的 API Key`);
|
`未配置 ${finalProvider} 的 API Key,请在 Provider 设置中配置`,
|
||||||
process.exit(1);
|
finalProvider,
|
||||||
|
'apiKey'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定模型
|
// 确定模型
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ let coreModule: CoreModule | null = null;
|
|||||||
// Agent 实例缓存(每个 session 一个)
|
// Agent 实例缓存(每个 session 一个)
|
||||||
const agentCache: Map<string, AgentInstance> = new Map();
|
const agentCache: Map<string, AgentInstance> = new Map();
|
||||||
|
|
||||||
|
// 配置错误缓存(用于向客户端返回友好错误)
|
||||||
|
let lastConfigError: { provider: string; message: string } | null = null;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 公共 API
|
// 公共 API
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -135,8 +138,13 @@ export function isCoreAvailable(): boolean {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取或创建 Agent 实例
|
* 获取或创建 Agent 实例
|
||||||
|
*
|
||||||
|
* @returns Agent 实例,或 null(Core 不可用或配置错误)
|
||||||
*/
|
*/
|
||||||
export function getOrCreateAgent(sessionId: string): AgentInstance | null {
|
export function getOrCreateAgent(sessionId: string): AgentInstance | null {
|
||||||
|
// 清除之前的配置错误
|
||||||
|
lastConfigError = null;
|
||||||
|
|
||||||
if (!coreModule) {
|
if (!coreModule) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -146,17 +154,32 @@ export function getOrCreateAgent(sessionId: string): AgentInstance | null {
|
|||||||
return agentCache.get(sessionId)!;
|
return agentCache.get(sessionId)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新 Agent
|
try {
|
||||||
const config = coreModule.loadConfig();
|
// 创建新 Agent(可能抛出 ConfigurationError)
|
||||||
const agent = new coreModule.Agent(config);
|
const config = coreModule.loadConfig();
|
||||||
agent.setRegistry(coreModule.toolRegistry);
|
const agent = new coreModule.Agent(config);
|
||||||
|
agent.setRegistry(coreModule.toolRegistry);
|
||||||
|
|
||||||
// 设置权限回调,通过 WebSocket 请求用户确认
|
// 设置权限回调,通过 WebSocket 请求用户确认
|
||||||
const permissionManager = coreModule.getPermissionManager();
|
const permissionManager = coreModule.getPermissionManager();
|
||||||
permissionManager.setAskCallback(createServerPermissionCallback(sessionId));
|
permissionManager.setAskCallback(createServerPermissionCallback(sessionId));
|
||||||
|
|
||||||
agentCache.set(sessionId, agent);
|
agentCache.set(sessionId, agent);
|
||||||
return 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);
|
const agent = getOrCreateAgent(sessionId);
|
||||||
|
|
||||||
if (!agent) {
|
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 模块不可用,返回占位响应
|
// Core 模块不可用,返回占位响应
|
||||||
broadcastToSession(sessionId, {
|
broadcastToSession(sessionId, {
|
||||||
type: 'chunk',
|
type: 'chunk',
|
||||||
|
|||||||
@@ -122,6 +122,8 @@ export type {
|
|||||||
CompressionStatus,
|
CompressionStatus,
|
||||||
CompressionType,
|
CompressionType,
|
||||||
CompressionResult,
|
CompressionResult,
|
||||||
|
// WebSocket error types
|
||||||
|
ConfigErrorPayload,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
// API Configuration
|
// API Configuration
|
||||||
|
|||||||
@@ -756,3 +756,21 @@ export interface CompressionResult {
|
|||||||
summaryTokens?: number;
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,15 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { createWebSocket, getMessages, type Message } from '../api/client.js';
|
import { createWebSocket, getMessages, type Message } from '../api/client.js';
|
||||||
import type { PermissionRequest } from '../components/PermissionDialog.js';
|
import type { PermissionRequest } from '../components/PermissionDialog.js';
|
||||||
|
import type { ConfigErrorPayload } from '../api/types.js';
|
||||||
|
|
||||||
interface UseChatOptions {
|
interface UseChatOptions {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
onError?: (error: Error) => void;
|
onError?: (error: Error) => void;
|
||||||
onSessionNotFound?: () => void;
|
onSessionNotFound?: () => void;
|
||||||
onSessionUpdated?: (sessionId: string, name: string) => void;
|
onSessionUpdated?: (sessionId: string, name: string) => void;
|
||||||
|
/** 配置错误回调(如 API Key 未配置) */
|
||||||
|
onConfigError?: (error: ConfigErrorPayload) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatState {
|
interface ChatState {
|
||||||
@@ -23,7 +26,7 @@ interface ChatState {
|
|||||||
permissionRequest: PermissionRequest | null;
|
permissionRequest: PermissionRequest | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdated }: UseChatOptions) {
|
export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdated, onConfigError }: UseChatOptions) {
|
||||||
const [state, setState] = useState<ChatState>({
|
const [state, setState] = useState<ChatState>({
|
||||||
messages: [],
|
messages: [],
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
@@ -43,9 +46,11 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
|||||||
const onErrorRef = useRef(onError);
|
const onErrorRef = useRef(onError);
|
||||||
const onSessionNotFoundRef = useRef(onSessionNotFound);
|
const onSessionNotFoundRef = useRef(onSessionNotFound);
|
||||||
const onSessionUpdatedRef = useRef(onSessionUpdated);
|
const onSessionUpdatedRef = useRef(onSessionUpdated);
|
||||||
|
const onConfigErrorRef = useRef(onConfigError);
|
||||||
onErrorRef.current = onError;
|
onErrorRef.current = onError;
|
||||||
onSessionNotFoundRef.current = onSessionNotFound;
|
onSessionNotFoundRef.current = onSessionNotFound;
|
||||||
onSessionUpdatedRef.current = onSessionUpdated;
|
onSessionUpdatedRef.current = onSessionUpdated;
|
||||||
|
onConfigErrorRef.current = onConfigError;
|
||||||
|
|
||||||
// 加载历史消息
|
// 加载历史消息
|
||||||
const loadMessages = useCallback(async () => {
|
const loadMessages = useCallback(async () => {
|
||||||
@@ -137,7 +142,12 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'error':
|
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: '' }));
|
setState((prev) => ({ ...prev, isLoading: false, streamingContent: '' }));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user