@huyooo/ai-chat-core 0.2.19 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/events.d.ts +452 -0
- package/dist/events.js +1 -0
- package/dist/index.d.ts +202 -550
- package/dist/index.js +1 -1
- package/package.json +23 -4
- package/src/agent.ts +399 -0
- package/src/constants.ts +125 -0
- package/src/events.ts +797 -0
- package/src/index.ts +309 -0
- package/src/internal/update-plan.ts +2 -0
- package/src/internal/web-search.ts +78 -0
- package/src/mcp/client-manager.ts +301 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/types.ts +43 -0
- package/src/providers/context-compressor.ts +149 -0
- package/src/providers/index.ts +120 -0
- package/src/providers/model-registry.ts +320 -0
- package/src/providers/orchestrator.ts +761 -0
- package/src/providers/protocols/anthropic.ts +406 -0
- package/src/providers/protocols/ark.ts +362 -0
- package/src/providers/protocols/deepseek.ts +344 -0
- package/src/providers/protocols/error-utils.ts +74 -0
- package/src/providers/protocols/gemini.ts +350 -0
- package/src/providers/protocols/index.ts +36 -0
- package/src/providers/protocols/openai.ts +420 -0
- package/src/providers/protocols/qwen.ts +326 -0
- package/src/providers/protocols/types.ts +189 -0
- package/src/providers/types.ts +272 -0
- package/src/providers/unified-adapter.ts +367 -0
- package/src/router.ts +72 -0
- package/src/test-utils/mock-sse.ts +32 -0
- package/src/tools.ts +162 -0
- package/src/types.ts +531 -0
- package/src/utils.ts +86 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @huyooo/ai-chat-core
|
|
3
|
+
*
|
|
4
|
+
* AI Chat 核心包 - 纯 Agent 逻辑,无框架依赖
|
|
5
|
+
*
|
|
6
|
+
* 新架构(Protocol + Family 分离):
|
|
7
|
+
* - Model Registry: 模型注册表,管理所有模型配置
|
|
8
|
+
* - Protocol Layer: 只负责 HTTP/SSE 通信
|
|
9
|
+
* - Family Config: 统一管理模型行为差异
|
|
10
|
+
* - UnifiedAdapter: 组合 Protocol + Family
|
|
11
|
+
* - ChatOrchestrator: 统一处理工具调用循环
|
|
12
|
+
*
|
|
13
|
+
* 设计优势:
|
|
14
|
+
* 1. Protocol 只负责 HTTP/SSE 通信
|
|
15
|
+
* 2. FamilyConfig 统一管理行为差异
|
|
16
|
+
* 3. 新增模型只需在 Registry 配置,无需修改代码
|
|
17
|
+
*
|
|
18
|
+
* 支持模型:
|
|
19
|
+
* - ARK (豆包/DeepSeek) - 火山引擎
|
|
20
|
+
* - Qwen (通义千问) - DashScope
|
|
21
|
+
* - Gemini - Google AI
|
|
22
|
+
* - OpenRouter - Claude/GPT 等
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// ==================== Agent ====================
|
|
26
|
+
|
|
27
|
+
export { HybridAgent, type RuntimeConfig } from './agent';
|
|
28
|
+
|
|
29
|
+
// ==================== 模型注册表 ====================
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
// 家族配置
|
|
33
|
+
MODEL_FAMILIES,
|
|
34
|
+
DOUBAO_FAMILY,
|
|
35
|
+
DEEPSEEK_FAMILY,
|
|
36
|
+
QWEN_FAMILY,
|
|
37
|
+
GEMINI_FAMILY,
|
|
38
|
+
GPT_FAMILY,
|
|
39
|
+
CLAUDE_FAMILY,
|
|
40
|
+
|
|
41
|
+
// 模型注册表
|
|
42
|
+
MODEL_REGISTRY,
|
|
43
|
+
|
|
44
|
+
// 查询函数
|
|
45
|
+
getModelEntry,
|
|
46
|
+
getModelFamily,
|
|
47
|
+
getModelProtocol,
|
|
48
|
+
getVisibleModels,
|
|
49
|
+
getModelsByFamily,
|
|
50
|
+
getModelsByProtocol,
|
|
51
|
+
modelSupportsThinking,
|
|
52
|
+
modelSupportsNativeSearch,
|
|
53
|
+
getModelSearchStrategy,
|
|
54
|
+
} from './providers/model-registry';
|
|
55
|
+
|
|
56
|
+
export type {
|
|
57
|
+
ModelFamilyId,
|
|
58
|
+
ProtocolId,
|
|
59
|
+
ThinkingFormat,
|
|
60
|
+
SearchStrategy,
|
|
61
|
+
ToolCallFormat,
|
|
62
|
+
ModelFamilyConfig,
|
|
63
|
+
ModelRegistryEntry,
|
|
64
|
+
} from './providers/model-registry';
|
|
65
|
+
|
|
66
|
+
// ==================== 新架构:UnifiedAdapter + Protocol ====================
|
|
67
|
+
|
|
68
|
+
// UnifiedAdapter - 统一适配器
|
|
69
|
+
export { UnifiedAdapter, createUnifiedAdapter } from './providers/unified-adapter';
|
|
70
|
+
export type { UnifiedAdapterConfig, StreamOptions } from './providers/unified-adapter';
|
|
71
|
+
|
|
72
|
+
// Protocol Layer
|
|
73
|
+
export {
|
|
74
|
+
ArkProtocol,
|
|
75
|
+
createArkProtocol,
|
|
76
|
+
DeepSeekProtocol,
|
|
77
|
+
createDeepSeekProtocol,
|
|
78
|
+
QwenProtocol,
|
|
79
|
+
createQwenProtocol,
|
|
80
|
+
GeminiProtocol,
|
|
81
|
+
createGeminiProtocol,
|
|
82
|
+
OpenAIProtocol,
|
|
83
|
+
createOpenAIProtocol,
|
|
84
|
+
AnthropicProtocol,
|
|
85
|
+
createAnthropicProtocol,
|
|
86
|
+
} from './providers/protocols';
|
|
87
|
+
|
|
88
|
+
export type {
|
|
89
|
+
Protocol,
|
|
90
|
+
ProtocolConfig,
|
|
91
|
+
ProtocolMessage,
|
|
92
|
+
ProtocolToolDefinition,
|
|
93
|
+
ProtocolToolCall,
|
|
94
|
+
RawEvent,
|
|
95
|
+
RawEventType,
|
|
96
|
+
RawToolCall,
|
|
97
|
+
RawSearchResult,
|
|
98
|
+
} from './providers/protocols';
|
|
99
|
+
|
|
100
|
+
// Orchestrator - 统一的工具调用处理
|
|
101
|
+
export { ChatOrchestrator, createOrchestrator } from './providers/orchestrator';
|
|
102
|
+
|
|
103
|
+
// 新架构类型
|
|
104
|
+
export type {
|
|
105
|
+
StreamChunk,
|
|
106
|
+
StreamChunkType,
|
|
107
|
+
ToolCallRequest,
|
|
108
|
+
SearchResultItem,
|
|
109
|
+
StandardMessage,
|
|
110
|
+
ProviderAdapter,
|
|
111
|
+
AdapterConfig,
|
|
112
|
+
StreamOnceOptions,
|
|
113
|
+
OrchestratorConfig,
|
|
114
|
+
OrchestratorContext,
|
|
115
|
+
OrchestratorOptions,
|
|
116
|
+
SimpleToolDefinition,
|
|
117
|
+
ToolExecutor as OrchestratorToolExecutor,
|
|
118
|
+
} from './providers/types';
|
|
119
|
+
|
|
120
|
+
// Legacy Adapters 已删除,使用 UnifiedAdapter + Protocol 新架构
|
|
121
|
+
|
|
122
|
+
// ==================== 配置与类型 ====================
|
|
123
|
+
|
|
124
|
+
export type {
|
|
125
|
+
AgentConfig,
|
|
126
|
+
ChatOptions,
|
|
127
|
+
ChatMode,
|
|
128
|
+
ThinkingMode,
|
|
129
|
+
AutoRunMode,
|
|
130
|
+
AutoRunConfig,
|
|
131
|
+
ToolExecutor,
|
|
132
|
+
ToolDefinition,
|
|
133
|
+
ChatMessage,
|
|
134
|
+
ResponsesApiTool,
|
|
135
|
+
// 模型类型
|
|
136
|
+
ModelOption,
|
|
137
|
+
ProviderType,
|
|
138
|
+
// 工具接口
|
|
139
|
+
Tool,
|
|
140
|
+
ToolContext,
|
|
141
|
+
ToolResult,
|
|
142
|
+
// 副作用类型
|
|
143
|
+
SideEffect,
|
|
144
|
+
// 工具插件(Vite 风格)
|
|
145
|
+
ToolPlugin,
|
|
146
|
+
ToolConfigItem,
|
|
147
|
+
// 用户工具(透传模式)
|
|
148
|
+
UserToolDefinition,
|
|
149
|
+
} from './types';
|
|
150
|
+
|
|
151
|
+
export {
|
|
152
|
+
// 模型列表
|
|
153
|
+
MODELS,
|
|
154
|
+
getModelByModelId,
|
|
155
|
+
// 工具解析和辅助函数
|
|
156
|
+
resolveTools,
|
|
157
|
+
tool,
|
|
158
|
+
tools,
|
|
159
|
+
} from './types';
|
|
160
|
+
|
|
161
|
+
// 常量
|
|
162
|
+
export { DEFAULT_MODEL } from './constants';
|
|
163
|
+
|
|
164
|
+
// ==================== 事件系统 ====================
|
|
165
|
+
|
|
166
|
+
// 基础类型
|
|
167
|
+
export type {
|
|
168
|
+
SearchResult,
|
|
169
|
+
ToolCallStatus,
|
|
170
|
+
ToolCallInfo,
|
|
171
|
+
TokenUsage,
|
|
172
|
+
ErrorCategory,
|
|
173
|
+
ErrorDetails
|
|
174
|
+
} from './events';
|
|
175
|
+
|
|
176
|
+
// 思考事件
|
|
177
|
+
export type {
|
|
178
|
+
ThinkingStartEvent,
|
|
179
|
+
ThinkingDeltaEvent,
|
|
180
|
+
ThinkingEndEvent,
|
|
181
|
+
ThinkingEvent
|
|
182
|
+
} from './events';
|
|
183
|
+
|
|
184
|
+
// 搜索事件
|
|
185
|
+
export type {
|
|
186
|
+
SearchStartEvent,
|
|
187
|
+
SearchResultEvent,
|
|
188
|
+
SearchEndEvent,
|
|
189
|
+
SearchEvent
|
|
190
|
+
} from './events';
|
|
191
|
+
|
|
192
|
+
// 工具事件
|
|
193
|
+
export type {
|
|
194
|
+
ToolCallStartEvent,
|
|
195
|
+
ToolCallResultEvent,
|
|
196
|
+
ToolCallOutputEvent,
|
|
197
|
+
ToolApprovalRequestEvent,
|
|
198
|
+
ToolCallRequestEvent,
|
|
199
|
+
ToolEvent
|
|
200
|
+
} from './events';
|
|
201
|
+
|
|
202
|
+
// 文本事件
|
|
203
|
+
export type {
|
|
204
|
+
TextDeltaEvent,
|
|
205
|
+
TextEvent
|
|
206
|
+
} from './events';
|
|
207
|
+
|
|
208
|
+
// 计划类型(update_plan 工具通过 resultType:'plan' 渲染,无专用事件)
|
|
209
|
+
export type {
|
|
210
|
+
PlanStep,
|
|
211
|
+
PlanStepStatus,
|
|
212
|
+
} from './events';
|
|
213
|
+
|
|
214
|
+
// 状态事件
|
|
215
|
+
export type {
|
|
216
|
+
DoneEvent,
|
|
217
|
+
ErrorEvent,
|
|
218
|
+
AbortEvent,
|
|
219
|
+
StatusEvent
|
|
220
|
+
} from './events';
|
|
221
|
+
|
|
222
|
+
// 步骤事件
|
|
223
|
+
export type {
|
|
224
|
+
StepStartEvent,
|
|
225
|
+
StepEndEvent,
|
|
226
|
+
StepEvent
|
|
227
|
+
} from './events';
|
|
228
|
+
|
|
229
|
+
// 聚合类型
|
|
230
|
+
export type {
|
|
231
|
+
ChatEvent,
|
|
232
|
+
ChatEventType
|
|
233
|
+
} from './events';
|
|
234
|
+
|
|
235
|
+
// 事件创建函数
|
|
236
|
+
export {
|
|
237
|
+
createThinkingStart,
|
|
238
|
+
createThinkingDelta,
|
|
239
|
+
createThinkingEnd,
|
|
240
|
+
createSearchStart,
|
|
241
|
+
createSearchResult,
|
|
242
|
+
createSearchEnd,
|
|
243
|
+
createToolCallStart,
|
|
244
|
+
createToolCallResult,
|
|
245
|
+
createToolCallOutput,
|
|
246
|
+
createToolCallRequest,
|
|
247
|
+
createTextDelta,
|
|
248
|
+
createDone,
|
|
249
|
+
// 错误创建函数
|
|
250
|
+
createError,
|
|
251
|
+
createApiError,
|
|
252
|
+
createRateLimitError,
|
|
253
|
+
createToolError,
|
|
254
|
+
createTimeoutError,
|
|
255
|
+
createParseError,
|
|
256
|
+
createAbort,
|
|
257
|
+
// 步骤事件
|
|
258
|
+
createStepStart,
|
|
259
|
+
createStepEnd,
|
|
260
|
+
// 类型守卫
|
|
261
|
+
isThinkingEvent,
|
|
262
|
+
isSearchEvent,
|
|
263
|
+
isToolEvent,
|
|
264
|
+
isTextEvent,
|
|
265
|
+
isStatusEvent,
|
|
266
|
+
isErrorEvent,
|
|
267
|
+
isAbortEvent,
|
|
268
|
+
isStepEvent,
|
|
269
|
+
isRetryableError,
|
|
270
|
+
CHAT_EVENT_TYPES
|
|
271
|
+
} from './events';
|
|
272
|
+
|
|
273
|
+
// ==================== 工具执行器 ====================
|
|
274
|
+
|
|
275
|
+
export { createDefaultToolExecutor } from './tools';
|
|
276
|
+
|
|
277
|
+
// ==================== 工具已迁移 ====================
|
|
278
|
+
//
|
|
279
|
+
// 计划工具(update_plan)请使用:
|
|
280
|
+
// import { createUpdatePlanTool } from '@huyooo/ai-chat-tools-local';
|
|
281
|
+
//
|
|
282
|
+
// 云端工具(weather, web-search)请使用:
|
|
283
|
+
// import { getWeatherTool, createWebSearchTool } from '@huyooo/ai-chat-tools-cloud';
|
|
284
|
+
//
|
|
285
|
+
// 本地工具(execute-command, ui-actions 等)请使用:
|
|
286
|
+
// import { getCwdTool, executeCommandTool, ... } from '@huyooo/ai-chat-tools-local';
|
|
287
|
+
//
|
|
288
|
+
|
|
289
|
+
// ==================== 路由 ====================
|
|
290
|
+
|
|
291
|
+
export type { RouteResult } from './router';
|
|
292
|
+
|
|
293
|
+
export {
|
|
294
|
+
routeModelToProvider,
|
|
295
|
+
routeModelWithDetails,
|
|
296
|
+
getDefaultProvider,
|
|
297
|
+
isModelForProvider
|
|
298
|
+
} from './router';
|
|
299
|
+
|
|
300
|
+
// ==================== MCP ====================
|
|
301
|
+
// McpClientManager 只在内部使用(agent.ts 直接 import),不从主入口导出
|
|
302
|
+
// 避免在 renderer 中因 barrel file 加载整个模块图而引入 Node.js-only 的 MCP SDK
|
|
303
|
+
// 如需外部使用,请通过 @huyooo/ai-chat-core/mcp 子路径导入
|
|
304
|
+
|
|
305
|
+
export type { McpServerConfig, McpConnectionStatus, McpConnectionInfo } from './mcp';
|
|
306
|
+
|
|
307
|
+
// ==================== 调试工具 ====================
|
|
308
|
+
|
|
309
|
+
export { DebugLogger } from './utils';
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Search 工具(内部使用,Tavily 实现)
|
|
3
|
+
*
|
|
4
|
+
* 对外统一使用工具名 web_search_ai,各协议一致使用,与厂商内置 web_search 不冲突。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Tool } from '../types';
|
|
8
|
+
|
|
9
|
+
/** 联网搜索工具名(Tavily 实现,对外统一) */
|
|
10
|
+
export const WEB_SEARCH_TOOL_NAME = 'web_search_ai';
|
|
11
|
+
|
|
12
|
+
export function createWebSearchTool(tavilyApiKey: string): Tool {
|
|
13
|
+
return {
|
|
14
|
+
name: WEB_SEARCH_TOOL_NAME,
|
|
15
|
+
description:
|
|
16
|
+
'联网搜索工具。输入 query(搜索关键词/问题),返回搜索结果列表(title/url/snippet)。用于获取实时信息与可引用来源。',
|
|
17
|
+
parameters: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
query: { type: 'string', description: '搜索关键词或问题(必填)' },
|
|
21
|
+
max_results: { type: 'number', description: '最大返回结果数(可选,默认 5)' },
|
|
22
|
+
},
|
|
23
|
+
required: ['query'],
|
|
24
|
+
},
|
|
25
|
+
resultType: 'search_results',
|
|
26
|
+
execute: async (args, ctx) => {
|
|
27
|
+
const query = typeof args.query === 'string' ? args.query : '';
|
|
28
|
+
const maxResults = typeof args.max_results === 'number' && Number.isFinite(args.max_results)
|
|
29
|
+
? Math.max(1, Math.min(10, Math.floor(args.max_results)))
|
|
30
|
+
: 5;
|
|
31
|
+
|
|
32
|
+
if (!query.trim()) {
|
|
33
|
+
return JSON.stringify({ query: '', results: [], error: '缺少 query' });
|
|
34
|
+
}
|
|
35
|
+
if (!tavilyApiKey) {
|
|
36
|
+
return JSON.stringify({ query, results: [], error: '缺少 Tavily API Key' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const resp = await fetch('https://api.tavily.com/search', {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: {
|
|
42
|
+
Authorization: `Bearer ${tavilyApiKey}`,
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
query,
|
|
47
|
+
max_results: maxResults,
|
|
48
|
+
search_depth: 'basic',
|
|
49
|
+
include_answer: false,
|
|
50
|
+
include_raw_content: false,
|
|
51
|
+
}),
|
|
52
|
+
signal: ctx.signal,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!resp.ok) {
|
|
56
|
+
const t = await resp.text().catch(() => '');
|
|
57
|
+
return JSON.stringify({ query, results: [], error: `Tavily /search 错误: ${resp.status} ${t}`.trim() });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data: unknown = await resp.json().catch(() => null);
|
|
61
|
+
|
|
62
|
+
const results: Array<{ title: string; url: string; snippet: string }> = [];
|
|
63
|
+
const arr = (data && typeof data === 'object' && Array.isArray((data as Record<string, unknown>).results))
|
|
64
|
+
? (data as Record<string, unknown>).results as Array<Record<string, unknown>>
|
|
65
|
+
: [];
|
|
66
|
+
for (const r of arr) {
|
|
67
|
+
const url = typeof r?.url === 'string' ? r.url : '';
|
|
68
|
+
if (!url) continue;
|
|
69
|
+
const title = typeof r?.title === 'string' ? r.title : '';
|
|
70
|
+
const snippet = typeof r?.content === 'string' ? r.content : '';
|
|
71
|
+
results.push({ title, url, snippet });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return JSON.stringify({ query, results });
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Client Manager
|
|
3
|
+
*
|
|
4
|
+
* 管理多个 MCP Server 连接,自动发现工具并转换为 Tool 接口。
|
|
5
|
+
*
|
|
6
|
+
* 职责:
|
|
7
|
+
* 1. 连接管理:根据配置连接/断开 MCP Server
|
|
8
|
+
* 2. 工具发现:通过 MCP 协议自动发现 Server 提供的工具
|
|
9
|
+
* 3. 工具适配:将 MCP 工具转换为我们的 Tool 接口
|
|
10
|
+
* 4. 生命周期:优雅关闭所有连接
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
14
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
15
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
16
|
+
import type { Tool, JsonSchemaObject, JsonSchemaProperty } from '../types';
|
|
17
|
+
import type { McpServerConfig, McpConnectionInfo, McpConnectionStatus } from './types';
|
|
18
|
+
import { DebugLogger } from '../utils';
|
|
19
|
+
|
|
20
|
+
const logger = DebugLogger.module('MCP');
|
|
21
|
+
|
|
22
|
+
/** 单个 MCP Server 连接 */
|
|
23
|
+
interface McpConnection {
|
|
24
|
+
config: McpServerConfig;
|
|
25
|
+
client: Client;
|
|
26
|
+
transport: StdioClientTransport | SSEClientTransport;
|
|
27
|
+
status: McpConnectionStatus;
|
|
28
|
+
tools: Tool[];
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* MCP Client Manager
|
|
34
|
+
*
|
|
35
|
+
* 使用方式:
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const manager = new McpClientManager();
|
|
38
|
+
* await manager.connectAll(configs);
|
|
39
|
+
* const tools = manager.getAllTools(); // 返回 Tool[] 直接注册到 Agent
|
|
40
|
+
* // ... 使用完毕
|
|
41
|
+
* await manager.disconnectAll();
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export class McpClientManager {
|
|
45
|
+
private connections = new Map<string, McpConnection>();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 连接到所有配置的 MCP Server
|
|
49
|
+
*
|
|
50
|
+
* 并行连接,单个失败不影响其他。
|
|
51
|
+
*/
|
|
52
|
+
async connectAll(configs: McpServerConfig[]): Promise<void> {
|
|
53
|
+
const results = await Promise.allSettled(
|
|
54
|
+
configs.map(config => this.connect(config))
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// 记录连接结果
|
|
58
|
+
for (let i = 0; i < results.length; i++) {
|
|
59
|
+
const result = results[i];
|
|
60
|
+
const name = configs[i].name;
|
|
61
|
+
if (result.status === 'rejected') {
|
|
62
|
+
logger.error(`[${name}] 连接失败:`, result.reason);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 连接单个 MCP Server
|
|
69
|
+
*/
|
|
70
|
+
async connect(config: McpServerConfig): Promise<void> {
|
|
71
|
+
const { name } = config;
|
|
72
|
+
|
|
73
|
+
// 如果已连接,先断开
|
|
74
|
+
if (this.connections.has(name)) {
|
|
75
|
+
await this.disconnect(name);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
logger.info(`[${name}] 正在连接...`);
|
|
79
|
+
|
|
80
|
+
const client = new Client(
|
|
81
|
+
{ name: 'ai-chat-mcp-client', version: '1.0.0' },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
let transport: StdioClientTransport | SSEClientTransport;
|
|
85
|
+
|
|
86
|
+
if (config.transport === 'stdio') {
|
|
87
|
+
if (!config.command) {
|
|
88
|
+
throw new Error(`[${name}] stdio 模式必须指定 command`);
|
|
89
|
+
}
|
|
90
|
+
transport = new StdioClientTransport({
|
|
91
|
+
command: config.command,
|
|
92
|
+
args: config.args,
|
|
93
|
+
env: config.env,
|
|
94
|
+
cwd: config.cwd,
|
|
95
|
+
});
|
|
96
|
+
} else if (config.transport === 'sse') {
|
|
97
|
+
if (!config.url) {
|
|
98
|
+
throw new Error(`[${name}] sse 模式必须指定 url`);
|
|
99
|
+
}
|
|
100
|
+
transport = new SSEClientTransport(new URL(config.url));
|
|
101
|
+
} else {
|
|
102
|
+
throw new Error(`[${name}] 不支持的传输方式: ${config.transport}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const connection: McpConnection = {
|
|
106
|
+
config,
|
|
107
|
+
client,
|
|
108
|
+
transport,
|
|
109
|
+
status: 'connecting',
|
|
110
|
+
tools: [],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.connections.set(name, connection);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await client.connect(transport);
|
|
117
|
+
connection.status = 'connected';
|
|
118
|
+
logger.info(`[${name}] 已连接`);
|
|
119
|
+
|
|
120
|
+
// 发现工具
|
|
121
|
+
const tools = await this.discoverTools(connection);
|
|
122
|
+
connection.tools = tools;
|
|
123
|
+
logger.info(`[${name}] 发现 ${tools.length} 个工具:`, tools.map(t => t.name));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
connection.status = 'error';
|
|
126
|
+
connection.error = error instanceof Error ? error.message : String(error);
|
|
127
|
+
logger.error(`[${name}] 连接失败:`, connection.error);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 从 MCP Server 发现工具并转换为 Tool 接口
|
|
134
|
+
*/
|
|
135
|
+
private async discoverTools(connection: McpConnection): Promise<Tool[]> {
|
|
136
|
+
const { client, config } = connection;
|
|
137
|
+
const { tools: mcpTools } = await client.listTools();
|
|
138
|
+
|
|
139
|
+
return mcpTools.map(mcpTool => this.adaptTool(mcpTool, config.name, client));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 将 MCP 工具转换为我们的 Tool 接口
|
|
144
|
+
*
|
|
145
|
+
* 关键转换:
|
|
146
|
+
* - MCP inputSchema → 我们的 parameters (JsonSchemaObject)
|
|
147
|
+
* - MCP callTool() → 我们的 execute()
|
|
148
|
+
*/
|
|
149
|
+
private adaptTool(
|
|
150
|
+
mcpTool: { name: string; description?: string; inputSchema: Record<string, unknown> },
|
|
151
|
+
serverName: string,
|
|
152
|
+
client: Client
|
|
153
|
+
): Tool {
|
|
154
|
+
// 转换 inputSchema 为我们的 JsonSchemaObject
|
|
155
|
+
const parameters = this.convertSchema(mcpTool.inputSchema);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
name: mcpTool.name,
|
|
159
|
+
description: mcpTool.description || `MCP tool from ${serverName}`,
|
|
160
|
+
parameters,
|
|
161
|
+
execute: async (args: Record<string, unknown>) => {
|
|
162
|
+
const result = await client.callTool({
|
|
163
|
+
name: mcpTool.name,
|
|
164
|
+
arguments: args,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// MCP 返回 content 数组,拼接为字符串
|
|
168
|
+
const texts: string[] = [];
|
|
169
|
+
if (Array.isArray(result.content)) {
|
|
170
|
+
for (const item of result.content) {
|
|
171
|
+
if (typeof item === 'object' && item !== null) {
|
|
172
|
+
const content = item as Record<string, unknown>;
|
|
173
|
+
if (content.type === 'text' && typeof content.text === 'string') {
|
|
174
|
+
texts.push(content.text);
|
|
175
|
+
} else if (content.type === 'image') {
|
|
176
|
+
texts.push(`[image: ${content.mimeType || 'unknown'}]`);
|
|
177
|
+
} else if (content.type === 'resource') {
|
|
178
|
+
texts.push(JSON.stringify(content));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return texts.join('\n') || JSON.stringify(result.content);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 将 MCP inputSchema 转换为 JsonSchemaObject
|
|
191
|
+
*/
|
|
192
|
+
private convertSchema(inputSchema: Record<string, unknown>): JsonSchemaObject {
|
|
193
|
+
const properties: Record<string, JsonSchemaProperty> = {};
|
|
194
|
+
const required: string[] = [];
|
|
195
|
+
|
|
196
|
+
const schemaProperties = inputSchema.properties as Record<string, Record<string, unknown>> | undefined;
|
|
197
|
+
if (schemaProperties) {
|
|
198
|
+
for (const [key, value] of Object.entries(schemaProperties)) {
|
|
199
|
+
properties[key] = this.convertProperty(value);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (Array.isArray(inputSchema.required)) {
|
|
204
|
+
required.push(...inputSchema.required.filter((r): r is string => typeof r === 'string'));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties,
|
|
210
|
+
required: required.length > 0 ? required : undefined,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 递归转换 JSON Schema 属性
|
|
216
|
+
*/
|
|
217
|
+
private convertProperty(prop: Record<string, unknown>): JsonSchemaProperty {
|
|
218
|
+
const result: JsonSchemaProperty = {
|
|
219
|
+
type: typeof prop.type === 'string' ? prop.type : 'string',
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (typeof prop.description === 'string') {
|
|
223
|
+
result.description = prop.description;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (Array.isArray(prop.enum)) {
|
|
227
|
+
result.enum = prop.enum;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 递归处理 items(数组类型)
|
|
231
|
+
if (prop.items && typeof prop.items === 'object') {
|
|
232
|
+
result.items = this.convertProperty(prop.items as Record<string, unknown>);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 递归处理 properties(对象类型)
|
|
236
|
+
if (prop.properties && typeof prop.properties === 'object') {
|
|
237
|
+
result.properties = {};
|
|
238
|
+
for (const [key, value] of Object.entries(prop.properties as Record<string, Record<string, unknown>>)) {
|
|
239
|
+
result.properties[key] = this.convertProperty(value);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (Array.isArray(prop.required)) {
|
|
244
|
+
result.required = prop.required.filter((r): r is string => typeof r === 'string');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 获取所有已连接 Server 的工具
|
|
252
|
+
*/
|
|
253
|
+
getAllTools(): Tool[] {
|
|
254
|
+
const tools: Tool[] = [];
|
|
255
|
+
for (const connection of this.connections.values()) {
|
|
256
|
+
if (connection.status === 'connected') {
|
|
257
|
+
tools.push(...connection.tools);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return tools;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 获取所有连接状态
|
|
265
|
+
*/
|
|
266
|
+
getConnectionInfos(): McpConnectionInfo[] {
|
|
267
|
+
return Array.from(this.connections.values()).map(conn => ({
|
|
268
|
+
name: conn.config.name,
|
|
269
|
+
status: conn.status,
|
|
270
|
+
toolCount: conn.tools.length,
|
|
271
|
+
error: conn.error,
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 断开单个 Server
|
|
277
|
+
*/
|
|
278
|
+
async disconnect(name: string): Promise<void> {
|
|
279
|
+
const connection = this.connections.get(name);
|
|
280
|
+
if (!connection) return;
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
await connection.client.close();
|
|
284
|
+
logger.info(`[${name}] 已断开`);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
logger.error(`[${name}] 断开失败:`, error);
|
|
287
|
+
} finally {
|
|
288
|
+
connection.status = 'disconnected';
|
|
289
|
+
connection.tools = [];
|
|
290
|
+
this.connections.delete(name);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 断开所有连接
|
|
296
|
+
*/
|
|
297
|
+
async disconnectAll(): Promise<void> {
|
|
298
|
+
const names = Array.from(this.connections.keys());
|
|
299
|
+
await Promise.allSettled(names.map(name => this.disconnect(name)));
|
|
300
|
+
}
|
|
301
|
+
}
|
package/src/mcp/index.ts
ADDED