@zhin.js/ai 0.0.1

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/src/index.ts ADDED
@@ -0,0 +1,1432 @@
1
+ /**
2
+ * @zhin.js/ai - AI Service for Zhin.js
3
+ *
4
+ * 多模型 AI 服务插件,支持:
5
+ * - 多模型提供商(OpenAI、Claude、DeepSeek、Ollama 等)
6
+ * - 工具调用(Function Calling)
7
+ * - 流式输出
8
+ * - 会话管理
9
+ * - Agent 能力
10
+ * - 独立的 AI 触发中间件(@机器人、前缀触发、私聊直接对话)
11
+ */
12
+
13
+ import {
14
+ usePlugin,
15
+ Logger,
16
+ // Tool Service 从 core 导入
17
+ createToolService,
18
+ ZhinTool,
19
+ // AI Trigger 工具函数从 core 导入
20
+ shouldTriggerAI,
21
+ inferSenderPermissions,
22
+ parseRichMediaContent,
23
+ extractTextContent,
24
+ mergeAITriggerConfig,
25
+ type Message,
26
+ type Plugin,
27
+ type Tool,
28
+ type ToolContext,
29
+ type AITriggerConfig,
30
+ } from '@zhin.js/core';
31
+
32
+ const aiLogger = new Logger(null, 'AI');
33
+ import type {
34
+ AIProvider,
35
+ AIConfig,
36
+ ChatMessage,
37
+ ChatCompletionRequest,
38
+ ChatCompletionResponse,
39
+ ChatCompletionChunk,
40
+ AgentTool,
41
+ } from './types.js';
42
+ import {
43
+ OpenAIProvider,
44
+ DeepSeekProvider,
45
+ MoonshotProvider,
46
+ ZhipuProvider,
47
+ AnthropicProvider,
48
+ OllamaProvider,
49
+ } from './providers/index.js';
50
+ import {
51
+ SessionManager,
52
+ createMemorySessionManager,
53
+ createDatabaseSessionManager,
54
+ AI_SESSION_MODEL,
55
+ type ISessionManager
56
+ } from './session.js';
57
+ import { Agent, createAgent } from './agent.js';
58
+ import { getBuiltinTools, getAllBuiltinTools } from './tools.js';
59
+
60
+ // ============================================================================
61
+ // 富媒体格式说明
62
+ // ============================================================================
63
+
64
+ /**
65
+ * 支持的富媒体消息格式说明
66
+ * AI 可以使用这些 XML-like 标签在回复中嵌入多媒体内容
67
+ */
68
+ const RICH_MEDIA_GUIDE = `
69
+
70
+ ## 富媒体输出格式
71
+
72
+ 你可以在回复中使用以下 XML 标签输出富媒体内容:
73
+
74
+ 1. **图片** - 展示图片
75
+ \`<image url="图片URL"/>\`
76
+ 示例:<image url="https://example.com/cat.jpg"/>
77
+
78
+ 2. **视频** - 展示视频
79
+ \`<video url="视频URL"/>\`
80
+ 示例:<video url="https://example.com/video.mp4"/>
81
+
82
+ 3. **音频** - 播放音频
83
+ \`<audio url="音频URL"/>\`
84
+ 示例:<audio url="https://example.com/song.mp3"/>
85
+
86
+ 4. **@用户** - 提及某人
87
+ \`<at user_id="用户ID"/>\`
88
+ 示例:<at user_id="123456"/>
89
+
90
+ 5. **表情** - 发送表情符号
91
+ \`<face id="表情ID"/>\`
92
+ 示例:<face id="178"/>
93
+
94
+ 注意事项:
95
+ - 富媒体标签可以与普通文本混合使用
96
+ - URL 必须是有效的、可访问的网络地址
97
+ - 图片建议使用 jpg/png/gif/webp 格式
98
+ - 适当使用图片和表情可以让回复更生动
99
+ `;
100
+ import {
101
+ ContextManager,
102
+ createContextManager,
103
+ CHAT_MESSAGE_MODEL,
104
+ CONTEXT_SUMMARY_MODEL,
105
+ type MessageRecord,
106
+ type ContextConfig,
107
+ } from './context-manager.js';
108
+
109
+ // ============================================================================
110
+ // 类型扩展
111
+ // ============================================================================
112
+
113
+ declare module '@zhin.js/core' {
114
+ namespace Plugin {
115
+ interface Contexts {
116
+ ai: AIService;
117
+ }
118
+ }
119
+ }
120
+
121
+ // ============================================================================
122
+ // AI Service 类
123
+ // ============================================================================
124
+
125
+ /**
126
+ * AI 服务
127
+ * 统一管理多个模型提供商,提供会话和 Agent 能力
128
+ */
129
+ export class AIService {
130
+ private providers: Map<string, AIProvider> = new Map();
131
+ private defaultProvider: string;
132
+ public sessions: SessionManager;
133
+ public contextManager?: ContextManager;
134
+ private builtinTools: AgentTool[];
135
+ private sessionConfig: { maxHistory?: number; expireMs?: number };
136
+ private contextConfig: ContextConfig;
137
+ private triggerConfig: AITriggerConfig;
138
+ private plugin?: Plugin;
139
+ /** 额外注册的自定义工具 */
140
+ private customTools: Map<string, AgentTool> = new Map();
141
+
142
+ constructor(config: AIConfig = {}) {
143
+ this.defaultProvider = config.defaultProvider || 'openai';
144
+ this.sessionConfig = config.sessions || {};
145
+ this.contextConfig = config.context || {};
146
+ this.triggerConfig = config.trigger || {};
147
+ // 先用内存会话管理器,后续通过 setSessionManager 切换到数据库
148
+ this.sessions = createMemorySessionManager(this.sessionConfig);
149
+ // 将 ZhinTool 转换为 Tool 格式(与 AgentTool 兼容)
150
+ this.builtinTools = getBuiltinTools().map(tool => tool.toTool());
151
+
152
+ // 初始化提供商
153
+ if (config.providers?.openai?.apiKey) {
154
+ this.registerProvider(new OpenAIProvider(config.providers.openai));
155
+ }
156
+
157
+ if (config.providers?.anthropic?.apiKey) {
158
+ this.registerProvider(new AnthropicProvider(config.providers.anthropic));
159
+ }
160
+
161
+ if (config.providers?.deepseek?.apiKey) {
162
+ this.registerProvider(new DeepSeekProvider(config.providers.deepseek));
163
+ }
164
+
165
+ if (config.providers?.moonshot?.apiKey) {
166
+ this.registerProvider(new MoonshotProvider(config.providers.moonshot));
167
+ }
168
+
169
+ if (config.providers?.zhipu?.apiKey) {
170
+ this.registerProvider(new ZhipuProvider(config.providers.zhipu));
171
+ }
172
+
173
+ if (config.providers?.ollama) {
174
+ this.registerProvider(new OllamaProvider(config.providers.ollama));
175
+ }
176
+ }
177
+
178
+ // ============================================================================
179
+ // AI 处理核心方法
180
+ // ============================================================================
181
+
182
+ /**
183
+ * 检查 AI 服务是否就绪
184
+ */
185
+ isReady(): boolean {
186
+ return this.providers.size > 0;
187
+ }
188
+
189
+ /**
190
+ * 处理 AI 请求
191
+ * 这是 AI 触发中间件调用的主入口
192
+ *
193
+ * 采用三步处理架构:
194
+ * 1. 意图分析:分析用户意图,筛选相关工具
195
+ * 2. 工具执行:用筛选的工具让模型决策和执行
196
+ * 3. 结果总结:基于结果生成友好回复
197
+ */
198
+ async process(
199
+ content: string,
200
+ context: ToolContext,
201
+ tools: Tool[]
202
+ ): Promise<string | AsyncIterable<string>> {
203
+ const { platform, senderId, sceneId } = context;
204
+
205
+ // 生成会话 ID
206
+ const sessionId = SessionManager.generateId(platform || '', senderId || '', sceneId);
207
+
208
+ // 收集所有可用工具
209
+ const allTools = this.collectAllToolsWithExternal(tools);
210
+
211
+ // 基础系统提示(包含富媒体格式说明)
212
+ const baseSystemPrompt = `你是一个友好的中文 AI 助手,请始终使用中文回复。
213
+ ${RICH_MEDIA_GUIDE}`;
214
+
215
+ // 如果没有工具,直接进入简单对话模式
216
+ if (allTools.length === 0) {
217
+ const response = await this.simpleChat(content, baseSystemPrompt);
218
+ await this.sessions.addMessage(sessionId, { role: 'user', content });
219
+ await this.sessions.addMessage(sessionId, { role: 'assistant', content: response });
220
+ return response;
221
+ }
222
+
223
+ aiLogger.debug(`处理开始,可用工具: ${allTools.length}`);
224
+
225
+ // ========== 第一步:工具匹配 ==========
226
+ // 先用关键词匹配(快速且准确)
227
+ let relevantTools = this.matchToolsByKeywords(content, allTools);
228
+ aiLogger.debug(`关键词匹配工具: ${relevantTools.length}`);
229
+
230
+ // 如果关键词没匹配到,再用 AI 分析
231
+ if (relevantTools.length === 0) {
232
+ relevantTools = await this.analyzeIntentAndSelectTools(content, allTools);
233
+ aiLogger.debug(`AI 意图分析工具: ${relevantTools.length}`);
234
+ }
235
+
236
+ // 如果仍然没有相关工具,直接对话
237
+ if (relevantTools.length === 0) {
238
+ const response = await this.simpleChat(content, baseSystemPrompt);
239
+ await this.sessions.addMessage(sessionId, { role: 'user', content });
240
+ await this.sessions.addMessage(sessionId, { role: 'assistant', content: response });
241
+ return response;
242
+ }
243
+
244
+ // ========== 第二步:工具执行 ==========
245
+ const toolSystemPrompt = `你是一个 AI 助手。根据用户的问题,决定是否需要调用工具来获取信息。
246
+ 如果需要调用工具,请直接调用,不要解释。
247
+ 如果不需要工具,请直接用中文回答。`;
248
+
249
+ const agent = this.createAgent({
250
+ systemPrompt: toolSystemPrompt,
251
+ tools: relevantTools,
252
+ useBuiltinTools: false,
253
+ collectExternalTools: false,
254
+ maxIterations: 3, // 限制迭代次数
255
+ });
256
+
257
+ const agentResult = await agent.run(content);
258
+
259
+ // ========== 第三步:结果总结 ==========
260
+ let finalResponse: string;
261
+
262
+ if (agentResult.toolCalls.length > 0) {
263
+ // 有工具调用,需要总结结果
264
+ aiLogger.debug(`工具调用: ${agentResult.toolCalls.map(t => t.tool).join(', ')}`);
265
+ finalResponse = await this.summarizeToolResults(content, agentResult.toolCalls);
266
+ } else {
267
+ // 没有工具调用,直接使用模型回复
268
+ finalResponse = agentResult.content || await this.simpleChat(content, baseSystemPrompt);
269
+ }
270
+
271
+ // 保存到会话
272
+ await this.sessions.addMessage(sessionId, { role: 'user', content });
273
+ await this.sessions.addMessage(sessionId, { role: 'assistant', content: finalResponse });
274
+
275
+ // 异步检查是否需要总结
276
+ if (this.contextManager && sceneId) {
277
+ this.contextManager.autoSummarizeIfNeeded(sceneId).catch(() => {});
278
+ }
279
+
280
+ return finalResponse;
281
+ }
282
+
283
+ /**
284
+ * 简单对话(不使用会话历史)
285
+ */
286
+ private async simpleChat(content: string, systemPrompt: string): Promise<string> {
287
+ const provider = this.getProvider();
288
+ const response = await this.chat({
289
+ model: provider.models[0],
290
+ messages: [
291
+ { role: 'system', content: systemPrompt },
292
+ { role: 'user', content },
293
+ ],
294
+ });
295
+ const msgContent = response.choices[0]?.message?.content;
296
+ return typeof msgContent === 'string' ? msgContent : '';
297
+ }
298
+
299
+ /**
300
+ * 第一步:意图分析,筛选相关工具
301
+ * 使用 AI 分析用户意图,选择最相关的工具
302
+ */
303
+ private async analyzeIntentAndSelectTools(
304
+ userContent: string,
305
+ allTools: AgentTool[]
306
+ ): Promise<AgentTool[]> {
307
+ // 构建工具列表(包含完整描述)
308
+ const toolList = allTools.map(t => `- ${t.name}: ${t.description}`).join('\n');
309
+
310
+ const analysisPrompt = `你是一个工具选择助手。分析用户的问题,选择可能需要的工具。
311
+
312
+ ## 重要说明
313
+ - 这些工具提供**本系统的真实数据**,不是通用知识
314
+ - 例如 "ai.models" 返回本系统配置的实际 AI 模型列表
315
+ - 优先使用工具获取实时数据,而不是依赖通用知识回答
316
+
317
+ ## 可用工具
318
+ ${toolList}
319
+
320
+ ## 用户问题
321
+ ${userContent}
322
+
323
+ ## 输出要求
324
+ - 只输出工具名称,用逗号分隔
325
+ - 最多选择 3 个最相关的工具
326
+ - 只有当用户的问题与任何工具都无关时,输出:无
327
+ - 不要输出任何解释
328
+
329
+ 需要的工具:`;
330
+
331
+ try {
332
+ const provider = this.getProvider();
333
+ const response = await this.chat({
334
+ model: provider.models[0],
335
+ messages: [
336
+ { role: 'system', content: '你是一个工具选择助手。根据用户问题分析需要哪些工具,只输出工具名称。' },
337
+ { role: 'user', content: analysisPrompt },
338
+ ],
339
+ temperature: 0.1, // 低温度,更确定性
340
+ });
341
+
342
+ const content = response.choices[0]?.message?.content;
343
+ const responseText = typeof content === 'string' ? content : '';
344
+
345
+ // 解析响应
346
+ if (!responseText || responseText.includes('无') || responseText.toLowerCase().includes('none')) {
347
+ return [];
348
+ }
349
+
350
+ // 提取工具名称
351
+ const toolNames = responseText
352
+ .replace(/[,、]/g, ',') // 中文逗号转英文
353
+ .split(',')
354
+ .map(s => s.trim().toLowerCase())
355
+ .filter(s => s && s !== '无' && s !== 'none');
356
+
357
+ // 匹配工具(精确匹配 + 模糊匹配)
358
+ const selectedTools = allTools.filter(tool => {
359
+ const toolNameLower = tool.name.toLowerCase();
360
+ return toolNames.some(name =>
361
+ toolNameLower === name ||
362
+ toolNameLower.includes(name) ||
363
+ name.includes(toolNameLower)
364
+ );
365
+ });
366
+
367
+ // 如果 AI 选择了但没匹配到,降级到关键词匹配
368
+ if (selectedTools.length === 0 && toolNames.length > 0) {
369
+ aiLogger.debug('AI 选择未匹配,降级到关键词匹配');
370
+ return this.matchToolsByKeywords(userContent, allTools);
371
+ }
372
+
373
+ return selectedTools.slice(0, 5);
374
+ } catch (error) {
375
+ aiLogger.warn('意图分析失败,降级到关键词匹配:', error);
376
+ return this.matchToolsByKeywords(userContent, allTools);
377
+ }
378
+ }
379
+
380
+ /**
381
+ * 基于关键词匹配工具(降级方案)
382
+ */
383
+ private matchToolsByKeywords(content: string, tools: AgentTool[]): AgentTool[] {
384
+ const keywords = content.toLowerCase();
385
+
386
+ // Debug: 输出可用工具列表
387
+ aiLogger.debug(`关键词匹配 - 输入: "${content}"`);
388
+ aiLogger.debug(`关键词匹配 - 可用工具: ${tools.map(t => t.name).join(', ')}`);
389
+
390
+ const keywordMap: Record<string, string[]> = {
391
+ '模型': ['ai.models', 'models'],
392
+ '可用模型': ['ai.models'],
393
+ 'ai模型': ['ai.models'],
394
+ '清除': ['ai.clear', 'clear'],
395
+ '清空': ['ai.clear', 'clear'],
396
+ '统计': ['ai.stats', 'stats'],
397
+ '工具': ['ai.tools', 'tools'],
398
+ '总结': ['ai.summary', 'summary'],
399
+ '健康': ['ai.health', 'health'],
400
+ '天气': ['weather'],
401
+ '热搜': ['weibo_hot', 'zhihu_hot', 'douyin_hot', 'toutiao_hot'],
402
+ '微博': ['weibo_hot'],
403
+ '知乎': ['zhihu_hot'],
404
+ '抖音': ['douyin_hot'],
405
+ '头条': ['toutiao_hot'],
406
+ '新闻': ['60s_news'],
407
+ '60': ['60s_news'],
408
+ '金价': ['gold_price'],
409
+ '黄金': ['gold_price'],
410
+ '油价': ['fuel_price'],
411
+ '汇率': ['exchange_rate'],
412
+ '翻译': ['translate_60s', 'translate'],
413
+ '历史': ['history_today', 'ai.clear', 'ai.stats'],
414
+ '一言': ['hitokoto'],
415
+ '摸鱼': ['moyu'],
416
+ '计算': ['calculator'],
417
+ '时间': ['get_time'],
418
+ '日期': ['get_time'],
419
+ 'kfc': ['kfc'],
420
+ '疯狂星期四': ['kfc'],
421
+ '段子': ['duanzi'],
422
+ '笑话': ['duanzi'],
423
+ 'ip': ['ip_query'],
424
+ '壁纸': ['bing_image'],
425
+ };
426
+
427
+ const matchedNames = new Set<string>();
428
+ for (const [keyword, toolNames] of Object.entries(keywordMap)) {
429
+ if (keywords.includes(keyword)) {
430
+ aiLogger.debug(`关键词匹配 - 匹配到关键词 "${keyword}" -> ${toolNames.join(', ')}`);
431
+ toolNames.forEach(name => matchedNames.add(name));
432
+ }
433
+ }
434
+
435
+ aiLogger.debug(`关键词匹配 - 需要的工具名: ${Array.from(matchedNames).join(', ')}`);
436
+
437
+ const matched = tools.filter(t => matchedNames.has(t.name));
438
+ aiLogger.debug(`关键词匹配 - 最终匹配: ${matched.map(t => t.name).join(', ') || '无'}`);
439
+
440
+ return matched.slice(0, 5);
441
+ }
442
+
443
+ /**
444
+ * 第三步:总结工具调用结果
445
+ */
446
+ private async summarizeToolResults(
447
+ userQuestion: string,
448
+ toolCalls: { tool: string; args: any; result: any }[]
449
+ ): Promise<string> {
450
+ // 构建工具结果描述
451
+ const resultsDesc = toolCalls.map(tc => {
452
+ const resultStr = typeof tc.result === 'string'
453
+ ? tc.result
454
+ : JSON.stringify(tc.result, null, 2);
455
+ return `工具 ${tc.tool} 的结果:\n${resultStr}`;
456
+ }).join('\n\n');
457
+
458
+ const summaryPrompt = `用户问题:${userQuestion}
459
+
460
+ 工具调用结果:
461
+ ${resultsDesc}
462
+
463
+ 请用友好的中文总结以上信息,回答用户的问题。要求:
464
+ 1. 使用自然语言,不要直接复制原始数据
465
+ 2. 突出重点信息
466
+ 3. 可以适当使用 emoji 增加趣味性
467
+ 4. 保持简洁明了
468
+ 5. 如果工具返回了图片/音频/视频 URL,请使用对应的标签展示`;
469
+
470
+ try {
471
+ const provider = this.getProvider();
472
+ const response = await this.chat({
473
+ model: provider.models[0],
474
+ messages: [
475
+ { role: 'system', content: `你是一个友好的中文助手,擅长用简洁生动的语言总结信息。
476
+ ${RICH_MEDIA_GUIDE}` },
477
+ { role: 'user', content: summaryPrompt },
478
+ ],
479
+ });
480
+ const msgContent = response.choices[0]?.message?.content;
481
+ return typeof msgContent === 'string' ? msgContent : resultsDesc;
482
+ } catch (error) {
483
+ aiLogger.warn('结果总结失败:', error);
484
+ // 降级:直接返回工具结果
485
+ const lastResult = toolCalls[toolCalls.length - 1]?.result;
486
+ return typeof lastResult === 'string' ? lastResult : JSON.stringify(lastResult, null, 2);
487
+ }
488
+ }
489
+
490
+ /**
491
+ * 收集所有工具(包括外部传入的)
492
+ * 注意:过滤掉命令转换的工具(cmd_xxx),避免工具过多影响模型性能
493
+ */
494
+ private collectAllToolsWithExternal(externalTools: Tool[]): AgentTool[] {
495
+ const tools: AgentTool[] = [];
496
+
497
+ // 1. 内置工具
498
+ tools.push(...this.builtinTools);
499
+
500
+ // 2. 自定义工具
501
+ tools.push(...this.customTools.values());
502
+
503
+ // 3. 外部工具(转换为 AgentTool,过滤掉命令工具)
504
+ for (const tool of externalTools) {
505
+ // 跳过命令转换的工具和进程相关工具,避免工具过多
506
+ if (tool.name.startsWith('cmd_') || tool.name.startsWith('process_')) {
507
+ continue;
508
+ }
509
+ tools.push(this.convertToolToAgentTool(tool));
510
+ }
511
+
512
+ // 限制工具数量,避免超出模型能力
513
+ const maxTools = 30;
514
+ if (tools.length > maxTools) {
515
+ aiLogger.debug(`工具数量 ${tools.length} 超过限制,截取前 ${maxTools} 个`);
516
+ return tools.slice(0, maxTools);
517
+ }
518
+
519
+ return tools;
520
+ }
521
+
522
+ /**
523
+ * 将 Tool 转换为 AgentTool
524
+ */
525
+ private convertToolToAgentTool(tool: Tool): AgentTool {
526
+ return {
527
+ name: tool.name,
528
+ description: tool.description,
529
+ parameters: tool.parameters as any,
530
+ execute: tool.execute,
531
+ };
532
+ }
533
+
534
+ // ============================================================================
535
+ // 原有方法
536
+ // ============================================================================
537
+
538
+ /**
539
+ * 设置会话管理器(用于切换到数据库存储)
540
+ */
541
+ setSessionManager(manager: SessionManager): void {
542
+ this.sessions.dispose();
543
+ this.sessions = manager;
544
+ }
545
+
546
+ /**
547
+ * 设置上下文管理器
548
+ */
549
+ setContextManager(manager: ContextManager): void {
550
+ this.contextManager = manager;
551
+ const defaultProvider = this.providers.get(this.defaultProvider);
552
+ if (defaultProvider) {
553
+ manager.setAIProvider(defaultProvider);
554
+ }
555
+ }
556
+
557
+ /**
558
+ * 设置插件引用(用于收集工具)
559
+ */
560
+ setPlugin(plugin: Plugin): void {
561
+ this.plugin = plugin;
562
+ }
563
+
564
+ /**
565
+ * 注册自定义工具到 AI 服务
566
+ */
567
+ registerTool(tool: AgentTool): () => void {
568
+ this.customTools.set(tool.name, tool);
569
+ return () => {
570
+ this.customTools.delete(tool.name);
571
+ };
572
+ }
573
+
574
+ /**
575
+ * 收集所有可用工具
576
+ */
577
+ collectAllTools(): AgentTool[] {
578
+ const tools: AgentTool[] = [];
579
+
580
+ tools.push(...this.builtinTools);
581
+ tools.push(...this.customTools.values());
582
+
583
+ if (this.plugin) {
584
+ const pluginTools = this.plugin.collectAllTools();
585
+ for (const tool of pluginTools) {
586
+ tools.push(this.convertToolToAgentTool(tool));
587
+ }
588
+ }
589
+
590
+ return tools;
591
+ }
592
+
593
+ getContextConfig(): ContextConfig {
594
+ return this.contextConfig;
595
+ }
596
+
597
+ getSessionConfig(): { maxHistory?: number; expireMs?: number } {
598
+ return this.sessionConfig;
599
+ }
600
+
601
+ getTriggerConfig(): AITriggerConfig {
602
+ return this.triggerConfig;
603
+ }
604
+
605
+ registerProvider(provider: AIProvider): void {
606
+ this.providers.set(provider.name, provider);
607
+ }
608
+
609
+ getProvider(name?: string): AIProvider {
610
+ const providerName = name || this.defaultProvider;
611
+ const provider = this.providers.get(providerName);
612
+ if (!provider) {
613
+ throw new Error(`AI Provider "${providerName}" not found. Available: ${this.listProviders().join(', ')}`);
614
+ }
615
+ return provider;
616
+ }
617
+
618
+ listProviders(): string[] {
619
+ return Array.from(this.providers.keys());
620
+ }
621
+
622
+ async listModels(providerName?: string): Promise<{ provider: string; models: string[] }[]> {
623
+ const result: { provider: string; models: string[] }[] = [];
624
+
625
+ if (providerName) {
626
+ const provider = this.getProvider(providerName);
627
+ const models = await provider.listModels?.() || provider.models;
628
+ result.push({ provider: providerName, models });
629
+ } else {
630
+ for (const [name, provider] of this.providers) {
631
+ const models = await provider.listModels?.() || provider.models;
632
+ result.push({ provider: name, models });
633
+ }
634
+ }
635
+
636
+ return result;
637
+ }
638
+
639
+ async chat(
640
+ request: ChatCompletionRequest,
641
+ providerName?: string
642
+ ): Promise<ChatCompletionResponse> {
643
+ const provider = this.getProvider(providerName);
644
+ return provider.chat(request);
645
+ }
646
+
647
+ async *chatStream(
648
+ request: ChatCompletionRequest,
649
+ providerName?: string
650
+ ): AsyncIterable<ChatCompletionChunk> {
651
+ const provider = this.getProvider(providerName);
652
+ yield* provider.chatStream(request);
653
+ }
654
+
655
+ async ask(
656
+ question: string,
657
+ options: {
658
+ provider?: string;
659
+ model?: string;
660
+ systemPrompt?: string;
661
+ temperature?: number;
662
+ } = {}
663
+ ): Promise<string> {
664
+ const messages: ChatMessage[] = [];
665
+
666
+ if (options.systemPrompt) {
667
+ messages.push({ role: 'system', content: options.systemPrompt });
668
+ }
669
+
670
+ messages.push({ role: 'user', content: question });
671
+
672
+ const provider = this.getProvider(options.provider);
673
+ const response = await provider.chat({
674
+ model: options.model || provider.models[0],
675
+ messages,
676
+ temperature: options.temperature,
677
+ });
678
+
679
+ const content = response.choices[0]?.message?.content;
680
+ return typeof content === 'string' ? content : '';
681
+ }
682
+
683
+ async chatWithSession(
684
+ sessionId: string,
685
+ message: string,
686
+ options: {
687
+ provider?: string;
688
+ model?: string;
689
+ systemPrompt?: string;
690
+ stream?: boolean;
691
+ } = {}
692
+ ): Promise<string | AsyncIterable<string>> {
693
+ const session = await this.sessions.get(sessionId, {
694
+ provider: options.provider || this.defaultProvider,
695
+ model: options.model,
696
+ systemPrompt: options.systemPrompt,
697
+ });
698
+
699
+ if (options.systemPrompt && !session.messages.some((m: ChatMessage) => m.role === 'system')) {
700
+ await this.sessions.setSystemPrompt(sessionId, options.systemPrompt);
701
+ }
702
+
703
+ await this.sessions.addMessage(sessionId, { role: 'user', content: message });
704
+
705
+ const provider = this.getProvider(options.provider);
706
+ const model = options.model || session.config.model || provider.models[0];
707
+
708
+ if (options.stream) {
709
+ const self = this;
710
+ async function* streamResponse(): AsyncIterable<string> {
711
+ let fullContent = '';
712
+ const messages = await self.sessions.getMessages(sessionId);
713
+
714
+ for await (const chunk of provider.chatStream({
715
+ model,
716
+ messages,
717
+ })) {
718
+ const content = chunk.choices[0]?.delta?.content;
719
+ if (content && typeof content === 'string') {
720
+ fullContent += content;
721
+ yield content;
722
+ }
723
+ }
724
+
725
+ await self.sessions.addMessage(sessionId, { role: 'assistant', content: fullContent });
726
+ }
727
+
728
+ return streamResponse();
729
+ }
730
+
731
+ const messages = await this.sessions.getMessages(sessionId);
732
+ const response = await provider.chat({
733
+ model,
734
+ messages,
735
+ });
736
+
737
+ const content = response.choices[0]?.message?.content;
738
+ const responseText = typeof content === 'string' ? content : '';
739
+
740
+ await this.sessions.addMessage(sessionId, { role: 'assistant', content: responseText });
741
+
742
+ return responseText;
743
+ }
744
+
745
+ createAgent(options: {
746
+ provider?: string;
747
+ model?: string;
748
+ systemPrompt?: string;
749
+ tools?: AgentTool[];
750
+ useBuiltinTools?: boolean;
751
+ collectExternalTools?: boolean;
752
+ maxIterations?: number;
753
+ } = {}): Agent {
754
+ const provider = this.getProvider(options.provider);
755
+
756
+ let tools: AgentTool[] = [];
757
+
758
+ if (options.useBuiltinTools !== false) {
759
+ tools.push(...this.builtinTools);
760
+ }
761
+
762
+ if (options.collectExternalTools !== false) {
763
+ tools.push(...this.customTools.values());
764
+
765
+ if (this.plugin) {
766
+ const pluginTools = this.plugin.collectAllTools();
767
+ for (const tool of pluginTools) {
768
+ tools.push(this.convertToolToAgentTool(tool));
769
+ }
770
+ }
771
+ }
772
+
773
+ if (options.tools?.length) {
774
+ tools.push(...options.tools);
775
+ }
776
+
777
+ return createAgent(provider, {
778
+ model: options.model,
779
+ systemPrompt: options.systemPrompt,
780
+ tools,
781
+ maxIterations: options.maxIterations,
782
+ });
783
+ }
784
+
785
+ async runAgent(
786
+ task: string,
787
+ options: {
788
+ provider?: string;
789
+ model?: string;
790
+ tools?: AgentTool[];
791
+ systemPrompt?: string;
792
+ } = {}
793
+ ): Promise<{ content: string; toolCalls: any[]; usage: any }> {
794
+ const agent = this.createAgent(options);
795
+ return agent.run(task);
796
+ }
797
+
798
+ async healthCheck(): Promise<Record<string, boolean>> {
799
+ const results: Record<string, boolean> = {};
800
+
801
+ for (const [name, provider] of this.providers) {
802
+ try {
803
+ results[name] = await provider.healthCheck?.() ?? true;
804
+ } catch {
805
+ results[name] = false;
806
+ }
807
+ }
808
+
809
+ return results;
810
+ }
811
+
812
+ dispose(): void {
813
+ this.sessions.dispose();
814
+ this.providers.clear();
815
+ }
816
+ }
817
+
818
+ // ============================================================================
819
+ // 插件入口
820
+ // ============================================================================
821
+
822
+ // 使用全局标志防止重复初始化
823
+ // 因为 Plugin.create 使用 ?t=timestamp 导入模块,导致模块被多次实例化
824
+ const AI_INIT_KEY = Symbol.for('@zhin.js/ai:initialized');
825
+ const globalState = globalThis as any;
826
+
827
+ const plugin = usePlugin();
828
+ const { provide, useContext, defineModel, root, logger } = plugin;
829
+
830
+ // 只在第一次加载时注册服务
831
+ if (!globalState[AI_INIT_KEY]) {
832
+ globalState[AI_INIT_KEY] = true;
833
+
834
+ // 注册数据模型(如果数据库服务可用)
835
+ if (typeof defineModel === 'function') {
836
+ defineModel('chat_messages', CHAT_MESSAGE_MODEL);
837
+ defineModel('context_summaries', CONTEXT_SUMMARY_MODEL);
838
+ defineModel('ai_sessions', AI_SESSION_MODEL);
839
+ }
840
+
841
+ // 注册 Tool Service
842
+ provide(createToolService());
843
+
844
+ logger.debug('AI plugin services registered (tool)');
845
+
846
+ // AI 服务实例
847
+ let aiServiceInstance: AIService | null = null;
848
+
849
+ // 注册 AI Context
850
+ provide({
851
+ name: 'ai',
852
+ description: 'AI Service - Multi-model LLM integration',
853
+ async mounted(p: Plugin) {
854
+ const configService = root.inject('config');
855
+ const appConfig = configService?.get<{ ai?: AIConfig }>('zhin.config.yml') || {};
856
+ const config = appConfig.ai || {};
857
+
858
+ if (config.enabled === false) {
859
+ logger.info('AI Service is disabled');
860
+ return null as any;
861
+ }
862
+
863
+ const service = new AIService(config);
864
+ aiServiceInstance = service;
865
+
866
+ service.setPlugin(root);
867
+
868
+ const providers = service.listProviders();
869
+ if (providers.length === 0) {
870
+ logger.warn('No AI providers configured. Please add API keys in zhin.config.yml');
871
+ } else {
872
+ logger.info(`AI Service started with providers: ${providers.join(', ')}`);
873
+ }
874
+
875
+ return service;
876
+ },
877
+ async dispose(service: AIService | null) {
878
+ if (service) {
879
+ service.dispose();
880
+ aiServiceInstance = null;
881
+ logger.info('AI Service stopped');
882
+ }
883
+ },
884
+ });
885
+
886
+ // ============================================================================
887
+ // AI 触发中间件(直接定义,无需单独服务)
888
+ // ============================================================================
889
+
890
+ // 当 AI 服务就绪时,注册 AI 触发中间件
891
+ useContext('ai', (ai: AIService) => {
892
+ const rawConfig = ai.getTriggerConfig();
893
+ const triggerConfig = mergeAITriggerConfig(rawConfig);
894
+
895
+ if (!triggerConfig.enabled) {
896
+ logger.info('AI Trigger is disabled');
897
+ return;
898
+ }
899
+
900
+ // 直接创建 AI 触发中间件
901
+ const aiTriggerMiddleware = async (message: Message<any>, next: () => Promise<void>) => {
902
+ // 检查消息是否已被命令处理(通过检查 $handled 标记)
903
+ if ((message as any).$handled) {
904
+ return await next();
905
+ }
906
+
907
+ const text = extractTextContent(message).trim();
908
+
909
+ // 检查是否匹配已注册的命令(避免与命令冲突)
910
+ const commandService = root.inject('command') as any;
911
+ if (commandService?.items) {
912
+ for (const cmd of commandService.items) {
913
+ // MessageCommand 的 name 或 pattern
914
+ const cmdName = cmd.name || cmd.pattern?.split(/\s/)[0];
915
+ if (cmdName && text.startsWith(cmdName)) {
916
+ // 消息匹配命令,跳过 AI 处理
917
+ logger.debug(`AI Trigger: 跳过命令 "${cmdName}"`);
918
+ return await next();
919
+ }
920
+ }
921
+ }
922
+
923
+ // 检查是否匹配工具生成的命令
924
+ const toolSvc = root.inject('tool') as any;
925
+ if (toolSvc?.toolCommands) {
926
+ for (const [toolName] of toolSvc.toolCommands) {
927
+ if (text.startsWith(toolName)) {
928
+ logger.debug(`AI Trigger: 跳过工具命令 "${toolName}"`);
929
+ return await next();
930
+ }
931
+ }
932
+ }
933
+
934
+ // 检查是否触发
935
+ const { triggered, content } = shouldTriggerAI(message, triggerConfig);
936
+
937
+ if (!triggered) {
938
+ return await next();
939
+ }
940
+
941
+ // 检查 AI 服务是否就绪
942
+ if (!ai.isReady()) {
943
+ return await next();
944
+ }
945
+
946
+ // 发送思考中提示
947
+ if (triggerConfig.thinkingMessage) {
948
+ await message.$reply(triggerConfig.thinkingMessage);
949
+ }
950
+
951
+ // 推断发送者权限
952
+ const permissions = inferSenderPermissions(message, triggerConfig);
953
+
954
+ // 构建工具上下文
955
+ const toolContext: ToolContext = {
956
+ platform: message.$adapter,
957
+ botId: message.$bot,
958
+ sceneId: message.$channel?.id || message.$sender.id,
959
+ senderId: message.$sender.id,
960
+ message,
961
+ scope: permissions.scope,
962
+ senderPermissionLevel: permissions.permissionLevel,
963
+ isGroupAdmin: permissions.isGroupAdmin,
964
+ isGroupOwner: permissions.isGroupOwner,
965
+ isBotAdmin: permissions.isBotAdmin,
966
+ isOwner: permissions.isOwner,
967
+ };
968
+
969
+ // 收集可用工具
970
+ const toolService = root.inject('tool');
971
+ let tools = toolService ? toolService.collectAll(root) : [];
972
+
973
+ // 根据上下文过滤工具
974
+ if (toolService && tools.length > 0) {
975
+ tools = toolService.filterByContext(tools, toolContext);
976
+ logger.debug(`AI Trigger: ${tools.length} tools available after filtering`);
977
+ }
978
+
979
+ try {
980
+ // 设置超时
981
+ const timeoutPromise = new Promise<never>((_, reject) => {
982
+ setTimeout(() => reject(new Error('AI 响应超时')), triggerConfig.timeout);
983
+ });
984
+
985
+ // 处理 AI 请求
986
+ const responsePromise = ai.process(content, toolContext, tools);
987
+ const response = await Promise.race([responsePromise, timeoutPromise]);
988
+
989
+ // 处理流式响应
990
+ if (response && typeof response === 'object' && Symbol.asyncIterator in response) {
991
+ let fullContent = '';
992
+ for await (const chunk of response as AsyncIterable<string>) {
993
+ fullContent += chunk;
994
+ }
995
+ if (fullContent) {
996
+ const elements = parseRichMediaContent(fullContent);
997
+ await message.$reply(elements);
998
+ }
999
+ } else if (response) {
1000
+ const elements = parseRichMediaContent(response as string);
1001
+ await message.$reply(elements);
1002
+ }
1003
+ } catch (error) {
1004
+ const errorMsg = error instanceof Error ? error.message : String(error);
1005
+ const errorResponse = triggerConfig.errorTemplate.replace('{error}', errorMsg);
1006
+ await message.$reply(errorResponse);
1007
+ }
1008
+
1009
+ await next();
1010
+ };
1011
+
1012
+ // 注册中间件
1013
+ const dispose = root.addMiddleware(aiTriggerMiddleware);
1014
+
1015
+ logger.info('AI Trigger middleware registered');
1016
+ logger.info(` - Prefixes: ${triggerConfig.prefixes.join(', ')}`);
1017
+ logger.info(` - Respond to @: ${triggerConfig.respondToAt}`);
1018
+ logger.info(` - Respond to private: ${triggerConfig.respondToPrivate}`);
1019
+
1020
+ return () => {
1021
+ dispose();
1022
+ logger.info('AI Trigger middleware unregistered');
1023
+ };
1024
+ });
1025
+
1026
+ // ============================================================================
1027
+ // 数据库集成
1028
+ // ============================================================================
1029
+
1030
+ useContext('database', (db) => {
1031
+ setTimeout(() => {
1032
+ if (!aiServiceInstance) {
1033
+ logger.debug('AI Service not ready, skipping database session manager setup');
1034
+ return;
1035
+ }
1036
+
1037
+ const configService = root.inject('config');
1038
+ const appConfig = configService?.get<{ ai?: AIConfig }>('zhin.config.yml') || {};
1039
+ const config = appConfig.ai || {};
1040
+
1041
+ if (config.sessions?.useDatabase === false) {
1042
+ logger.info('AI Session: Using memory storage (database disabled in config)');
1043
+ return;
1044
+ }
1045
+
1046
+ try {
1047
+
1048
+ const model = db.models.get('ai_sessions');
1049
+ if (!model) {
1050
+ logger.warn('AI Session: Failed to get model, falling back to memory storage');
1051
+ return;
1052
+ }
1053
+
1054
+ const dbSessionManager = createDatabaseSessionManager(model, aiServiceInstance.getSessionConfig());
1055
+ aiServiceInstance.setSessionManager(dbSessionManager);
1056
+
1057
+ logger.info('AI Session: Switched to database storage for persistent memory');
1058
+
1059
+ const contextConfig = aiServiceInstance.getContextConfig();
1060
+ if (contextConfig.enabled !== false) {
1061
+ try {
1062
+
1063
+ const messageModel = db.models.get('chat_messages');
1064
+ const summaryModel = db.models.get('context_summaries');
1065
+
1066
+ if (messageModel && summaryModel) {
1067
+ const contextManager = createContextManager(messageModel, summaryModel, contextConfig);
1068
+ aiServiceInstance.setContextManager(contextManager);
1069
+ logger.info('AI Context: Message recording and smart summary enabled');
1070
+ }
1071
+ } catch (error) {
1072
+ logger.error('AI Context: Failed to setup context manager:', error);
1073
+ }
1074
+ }
1075
+ } catch (error) {
1076
+ logger.error('AI Session: Failed to setup database storage:', error);
1077
+ logger.info('AI Session: Falling back to memory storage');
1078
+ }
1079
+ }, 100);
1080
+ });
1081
+
1082
+ // ============================================================================
1083
+ // 消息记录中间件
1084
+ // ============================================================================
1085
+
1086
+ root.addMiddleware(async (message: Message, next: () => Promise<void>) => {
1087
+ await next();
1088
+
1089
+ if (aiServiceInstance?.contextManager) {
1090
+ const record: MessageRecord = {
1091
+ platform: message.$adapter,
1092
+ scene_id: message.$channel?.id || message.$sender.id,
1093
+ scene_type: message.$channel?.type || 'private',
1094
+ scene_name: (message.$channel as any)?.name || '',
1095
+ sender_id: message.$sender.id,
1096
+ sender_name: message.$sender.name || message.$sender.id,
1097
+ message: typeof message.$raw === 'string' ? message.$raw : JSON.stringify(message.$raw),
1098
+ time: message.$timestamp || Date.now(),
1099
+ };
1100
+
1101
+ aiServiceInstance.contextManager.recordMessage(record).catch(err => {
1102
+ logger.debug('Failed to record message:', err);
1103
+ });
1104
+ }
1105
+ });
1106
+
1107
+ // ============================================================================
1108
+ // AI 管理工具 (使用 ZhinTool,同时支持 AI 调用和命令调用)
1109
+ // ============================================================================
1110
+
1111
+ useContext('ai', 'tool', (ai: AIService | undefined, toolService: any) => {
1112
+ if (!ai || !toolService) return;
1113
+
1114
+ // 列出模型工具
1115
+ const listModelsTool = new ZhinTool('ai.models')
1116
+ .desc('列出所有可用的 AI 模型')
1117
+ .tag('ai', 'management')
1118
+ .execute(async () => {
1119
+ const models = await ai.listModels();
1120
+ return {
1121
+ providers: models.map(({ provider, models: modelList }) => ({
1122
+ name: provider,
1123
+ models: modelList.slice(0, 10),
1124
+ total: modelList.length,
1125
+ })),
1126
+ };
1127
+ })
1128
+ .action(async () => {
1129
+ try {
1130
+ const models = await ai.listModels();
1131
+ let response = '🤖 可用模型:\n';
1132
+
1133
+ for (const { provider, models: modelList } of models) {
1134
+ response += `\n【${provider}】\n`;
1135
+ response += modelList.slice(0, 5).map((m: string) => ` • ${m}`).join('\n');
1136
+ if (modelList.length > 5) {
1137
+ response += `\n ... 还有 ${modelList.length - 5} 个`;
1138
+ }
1139
+ }
1140
+
1141
+ return response;
1142
+ } catch (error) {
1143
+ return `❌ 错误: ${error instanceof Error ? error.message : String(error)}`;
1144
+ }
1145
+ });
1146
+
1147
+ // 清除会话工具
1148
+ const clearSessionTool = new ZhinTool('ai.clear')
1149
+ .desc('清除当前对话的历史记录')
1150
+ .tag('ai', 'session')
1151
+ .execute(async (_args, context) => {
1152
+ if (!context?.message) return { success: false, error: '无法获取消息上下文' };
1153
+
1154
+ const message = context.message as Message;
1155
+ const sessionId = SessionManager.generateId(
1156
+ message.$adapter,
1157
+ message.$sender.id,
1158
+ message.$channel?.id
1159
+ );
1160
+
1161
+ await ai.sessions.reset(sessionId);
1162
+ return { success: true, message: '对话历史已清除' };
1163
+ })
1164
+ .action(async (message: Message) => {
1165
+ const sessionId = SessionManager.generateId(
1166
+ message.$adapter,
1167
+ message.$sender.id,
1168
+ message.$channel?.id
1169
+ );
1170
+
1171
+ await ai.sessions.reset(sessionId);
1172
+ return '✅ 对话历史已清除';
1173
+ });
1174
+
1175
+ // 场景统计工具
1176
+ const sceneStatsTool = new ZhinTool('ai.stats')
1177
+ .desc('查看当前场景的消息统计')
1178
+ .tag('ai', 'analytics')
1179
+ .execute(async (_args, context) => {
1180
+ if (!context?.message) return { error: '无法获取消息上下文' };
1181
+ if (!ai.contextManager) return { error: '上下文管理器未启用' };
1182
+
1183
+ const message = context.message as Message;
1184
+ const sceneId = message.$channel?.id || message.$sender.id;
1185
+ const stats = await ai.contextManager.getSceneStats(sceneId);
1186
+
1187
+ return {
1188
+ sceneId,
1189
+ messageCount: stats.messageCount,
1190
+ summaryCount: stats.summaryCount,
1191
+ firstMessageTime: stats.firstMessageTime,
1192
+ lastMessageTime: stats.lastMessageTime,
1193
+ };
1194
+ })
1195
+ .action(async (message: Message) => {
1196
+ const sceneId = message.$channel?.id || message.$sender.id;
1197
+
1198
+ if (!ai.contextManager) {
1199
+ return '⚠️ 上下文管理器未启用';
1200
+ }
1201
+
1202
+ try {
1203
+ const stats = await ai.contextManager.getSceneStats(sceneId);
1204
+ return [
1205
+ `📊 场景统计 (${sceneId})`,
1206
+ `• 消息数: ${stats.messageCount}`,
1207
+ `• 总结数: ${stats.summaryCount}`,
1208
+ stats.firstMessageTime ? `• 首条消息: ${new Date(stats.firstMessageTime).toLocaleString()}` : '',
1209
+ stats.lastMessageTime ? `• 最新消息: ${new Date(stats.lastMessageTime).toLocaleString()}` : '',
1210
+ ].filter(Boolean).join('\n');
1211
+ } catch (error) {
1212
+ return `❌ 错误: ${error instanceof Error ? error.message : String(error)}`;
1213
+ }
1214
+ });
1215
+
1216
+ // 列出工具工具
1217
+ const listToolsTool = new ZhinTool('ai.tools')
1218
+ .desc('列出所有可用的 AI 工具')
1219
+ .tag('ai', 'management')
1220
+ .execute(async () => {
1221
+ const allTools = ai.collectAllTools();
1222
+
1223
+ const groupedTools: Record<string, { name: string; description: string }[]> = {};
1224
+ for (const tool of allTools) {
1225
+ const source = (tool as any).source || 'builtin';
1226
+ if (!groupedTools[source]) {
1227
+ groupedTools[source] = [];
1228
+ }
1229
+ groupedTools[source].push({
1230
+ name: tool.name,
1231
+ description: tool.description,
1232
+ });
1233
+ }
1234
+
1235
+ return {
1236
+ total: allTools.length,
1237
+ groups: groupedTools,
1238
+ };
1239
+ })
1240
+ .action(async () => {
1241
+ try {
1242
+ const allTools = ai.collectAllTools();
1243
+
1244
+ if (allTools.length === 0) {
1245
+ return '📦 暂无可用工具';
1246
+ }
1247
+
1248
+ const groupedTools: Record<string, typeof allTools> = {};
1249
+ for (const tool of allTools) {
1250
+ const source = (tool as any).source || 'builtin';
1251
+ if (!groupedTools[source]) {
1252
+ groupedTools[source] = [];
1253
+ }
1254
+ groupedTools[source].push(tool);
1255
+ }
1256
+
1257
+ const lines: string[] = ['🔧 可用工具列表:\n'];
1258
+
1259
+ for (const [source, tools] of Object.entries(groupedTools)) {
1260
+ lines.push(`📁 ${source}:`);
1261
+ for (const tool of tools.slice(0, 10)) {
1262
+ lines.push(` • ${tool.name}: ${tool.description.substring(0, 50)}${tool.description.length > 50 ? '...' : ''}`);
1263
+ }
1264
+ if (tools.length > 10) {
1265
+ lines.push(` ... 还有 ${tools.length - 10} 个`);
1266
+ }
1267
+ lines.push('');
1268
+ }
1269
+
1270
+ lines.push(`总计: ${allTools.length} 个工具`);
1271
+
1272
+ return lines.join('\n');
1273
+ } catch (error) {
1274
+ return `❌ 错误: ${error instanceof Error ? error.message : String(error)}`;
1275
+ }
1276
+ });
1277
+
1278
+ // 对话总结工具
1279
+ const summarizeTool = new ZhinTool('ai.summary')
1280
+ .desc('生成当前场景的对话总结')
1281
+ .tag('ai', 'context')
1282
+ .execute(async (_args, context) => {
1283
+ if (!context?.message) return { error: '无法获取消息上下文' };
1284
+ if (!ai.contextManager) return { error: '上下文管理器未启用' };
1285
+
1286
+ const message = context.message as Message;
1287
+ const sceneId = message.$channel?.id || message.$sender.id;
1288
+ const summaryText = await ai.contextManager.summarize(sceneId);
1289
+
1290
+ return summaryText
1291
+ ? { success: true, summary: summaryText }
1292
+ : { success: false, error: '没有足够的历史消息进行总结' };
1293
+ })
1294
+ .action(async (message: Message) => {
1295
+ const sceneId = message.$channel?.id || message.$sender.id;
1296
+
1297
+ if (!ai.contextManager) {
1298
+ return '⚠️ 上下文管理器未启用';
1299
+ }
1300
+
1301
+ try {
1302
+ const summaryText = await ai.contextManager.summarize(sceneId);
1303
+ if (summaryText) {
1304
+ return `📝 对话总结:\n\n${summaryText}`;
1305
+ }
1306
+ return '⚠️ 没有足够的历史消息进行总结';
1307
+ } catch (error) {
1308
+ return `❌ 总结失败: ${error instanceof Error ? error.message : String(error)}`;
1309
+ }
1310
+ });
1311
+
1312
+ // 健康检查工具
1313
+ const healthCheckTool = new ZhinTool('ai.health')
1314
+ .desc('检查 AI 服务的健康状态')
1315
+ .tag('ai', 'management')
1316
+ .execute(async () => {
1317
+ const health = await ai.healthCheck();
1318
+ return {
1319
+ providers: Object.entries(health).map(([name, isHealthy]) => ({
1320
+ name,
1321
+ healthy: isHealthy,
1322
+ })),
1323
+ };
1324
+ })
1325
+ .action(async () => {
1326
+ try {
1327
+ const health = await ai.healthCheck();
1328
+ const lines = ['🏥 AI 服务健康状态:\n'];
1329
+
1330
+ for (const [provider, isHealthy] of Object.entries(health)) {
1331
+ lines.push(` ${isHealthy ? '✅' : '❌'} ${provider}`);
1332
+ }
1333
+
1334
+ return lines.join('\n');
1335
+ } catch (error) {
1336
+ return `❌ 健康检查失败: ${error instanceof Error ? error.message : String(error)}`;
1337
+ }
1338
+ });
1339
+
1340
+ // 注册所有工具
1341
+ const tools = [
1342
+ listModelsTool,
1343
+ clearSessionTool,
1344
+ sceneStatsTool,
1345
+ listToolsTool,
1346
+ summarizeTool,
1347
+ healthCheckTool,
1348
+ ];
1349
+
1350
+ const disposers: (() => void)[] = [];
1351
+ for (const tool of tools) {
1352
+ disposers.push(toolService.add(tool, root));
1353
+ }
1354
+
1355
+ logger.debug(`Registered ${tools.length} AI management tools`);
1356
+
1357
+ return () => {
1358
+ disposers.forEach(dispose => dispose());
1359
+ };
1360
+ });
1361
+
1362
+ } // 结束 if (!_initialized) 块
1363
+
1364
+ // ============================================================================
1365
+ // 创建 AI 服务(供 setup.ts 直接使用)
1366
+ // ============================================================================
1367
+
1368
+ /**
1369
+ * 创建 AI 服务 Context
1370
+ * 可在 setup.ts 中直接使用:provide(createAIService())
1371
+ */
1372
+ export function createAIService() {
1373
+ return {
1374
+ name: 'ai' as const,
1375
+ description: 'AI Service - Multi-model LLM integration',
1376
+ async mounted(p: Plugin) {
1377
+ const configService = p.root.inject('config');
1378
+ const appConfig = configService?.get<{ ai?: AIConfig }>('zhin.config.yml') || {};
1379
+ const config = appConfig.ai || {};
1380
+
1381
+ if (config.enabled === false) {
1382
+ p.logger.info('AI Service is disabled');
1383
+ return null as any;
1384
+ }
1385
+
1386
+ const service = new AIService(config);
1387
+ service.setPlugin(p.root);
1388
+
1389
+ const providers = service.listProviders();
1390
+ if (providers.length === 0) {
1391
+ p.logger.warn('No AI providers configured. Please add API keys in zhin.config.yml');
1392
+ } else {
1393
+ p.logger.info(`AI Service started with providers: ${providers.join(', ')}`);
1394
+ }
1395
+
1396
+ return service;
1397
+ },
1398
+ async dispose(service: AIService | null) {
1399
+ if (service) {
1400
+ service.dispose();
1401
+ }
1402
+ },
1403
+ };
1404
+ }
1405
+
1406
+ // ============================================================================
1407
+ // 导出
1408
+ // ============================================================================
1409
+
1410
+ // AIService 已通过 export class 导出,无需重复导出
1411
+ export { Agent, createAgent } from './agent.js';
1412
+ export { SessionManager, createMemorySessionManager, createDatabaseSessionManager } from './session.js';
1413
+ export { ContextManager, createContextManager, CHAT_MESSAGE_MODEL, CONTEXT_SUMMARY_MODEL } from './context-manager.js';
1414
+ export type * from './types.js';
1415
+ export * from './providers/index.js';
1416
+ export * from './tools.js';
1417
+
1418
+ // Tool Service 从 @zhin.js/core 重新导出
1419
+ export {
1420
+ createToolService,
1421
+ defineTool,
1422
+ ZhinTool,
1423
+ isZhinTool,
1424
+ // AI Trigger 工具函数
1425
+ shouldTriggerAI,
1426
+ inferSenderPermissions,
1427
+ parseRichMediaContent,
1428
+ extractTextContent,
1429
+ mergeAITriggerConfig,
1430
+ type ToolService,
1431
+ type AITriggerConfig,
1432
+ } from '@zhin.js/core';