@zhin.js/core 1.0.24 → 1.0.26
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/CHANGELOG.md +22 -0
- package/README.md +84 -342
- package/lib/adapter.d.ts +45 -1
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +182 -1
- package/lib/adapter.js.map +1 -1
- package/lib/ai/agent.d.ts +126 -0
- package/lib/ai/agent.d.ts.map +1 -0
- package/lib/ai/agent.js +645 -0
- package/lib/ai/agent.js.map +1 -0
- package/lib/ai/context-manager.d.ts +213 -0
- package/lib/ai/context-manager.d.ts.map +1 -0
- package/lib/ai/context-manager.js +313 -0
- package/lib/ai/context-manager.js.map +1 -0
- package/lib/ai/conversation-memory.d.ts +181 -0
- package/lib/ai/conversation-memory.d.ts.map +1 -0
- package/lib/ai/conversation-memory.js +581 -0
- package/lib/ai/conversation-memory.js.map +1 -0
- package/lib/ai/follow-up.d.ts +131 -0
- package/lib/ai/follow-up.d.ts.map +1 -0
- package/lib/ai/follow-up.js +265 -0
- package/lib/ai/follow-up.js.map +1 -0
- package/lib/ai/index.d.ts +29 -0
- package/lib/ai/index.d.ts.map +1 -0
- package/lib/ai/index.js +34 -0
- package/lib/ai/index.js.map +1 -0
- package/lib/ai/init.d.ts +30 -0
- package/lib/ai/init.d.ts.map +1 -0
- package/lib/ai/init.js +424 -0
- package/lib/ai/init.js.map +1 -0
- package/lib/ai/output.d.ts +93 -0
- package/lib/ai/output.d.ts.map +1 -0
- package/lib/ai/output.js +176 -0
- package/lib/ai/output.js.map +1 -0
- package/lib/ai/providers/anthropic.d.ts +23 -0
- package/lib/ai/providers/anthropic.d.ts.map +1 -0
- package/lib/ai/providers/anthropic.js +322 -0
- package/lib/ai/providers/anthropic.js.map +1 -0
- package/lib/ai/providers/base.d.ts +43 -0
- package/lib/ai/providers/base.d.ts.map +1 -0
- package/lib/ai/providers/base.js +135 -0
- package/lib/ai/providers/base.js.map +1 -0
- package/lib/ai/providers/index.d.ts +12 -0
- package/lib/ai/providers/index.d.ts.map +1 -0
- package/lib/ai/providers/index.js +9 -0
- package/lib/ai/providers/index.js.map +1 -0
- package/lib/ai/providers/ollama.d.ts +25 -0
- package/lib/ai/providers/ollama.d.ts.map +1 -0
- package/lib/ai/providers/ollama.js +243 -0
- package/lib/ai/providers/ollama.js.map +1 -0
- package/lib/ai/providers/openai.d.ts +46 -0
- package/lib/ai/providers/openai.d.ts.map +1 -0
- package/lib/ai/providers/openai.js +132 -0
- package/lib/ai/providers/openai.js.map +1 -0
- package/lib/ai/rate-limiter.d.ts +38 -0
- package/lib/ai/rate-limiter.d.ts.map +1 -0
- package/lib/ai/rate-limiter.js +86 -0
- package/lib/ai/rate-limiter.js.map +1 -0
- package/lib/ai/service.d.ts +81 -0
- package/lib/ai/service.d.ts.map +1 -0
- package/lib/ai/service.js +274 -0
- package/lib/ai/service.js.map +1 -0
- package/lib/ai/session.d.ts +186 -0
- package/lib/ai/session.d.ts.map +1 -0
- package/lib/ai/session.js +443 -0
- package/lib/ai/session.js.map +1 -0
- package/lib/ai/tone-detector.d.ts +19 -0
- package/lib/ai/tone-detector.d.ts.map +1 -0
- package/lib/ai/tone-detector.js +72 -0
- package/lib/ai/tone-detector.js.map +1 -0
- package/lib/ai/tools.d.ts +45 -0
- package/lib/ai/tools.d.ts.map +1 -0
- package/lib/ai/tools.js +206 -0
- package/lib/ai/tools.js.map +1 -0
- package/lib/ai/types.d.ts +264 -0
- package/lib/ai/types.d.ts.map +1 -0
- package/lib/ai/types.js +6 -0
- package/lib/ai/types.js.map +1 -0
- package/lib/ai/user-profile.d.ts +56 -0
- package/lib/ai/user-profile.d.ts.map +1 -0
- package/lib/ai/user-profile.js +130 -0
- package/lib/ai/user-profile.js.map +1 -0
- package/lib/ai/zhin-agent.d.ts +165 -0
- package/lib/ai/zhin-agent.d.ts.map +1 -0
- package/lib/ai/zhin-agent.js +707 -0
- package/lib/ai/zhin-agent.js.map +1 -0
- package/lib/built/adapter-process.d.ts +4 -0
- package/lib/built/adapter-process.d.ts.map +1 -1
- package/lib/built/adapter-process.js +94 -0
- package/lib/built/adapter-process.js.map +1 -1
- package/lib/built/ai-trigger.d.ts +89 -0
- package/lib/built/ai-trigger.d.ts.map +1 -0
- package/lib/built/ai-trigger.js +166 -0
- package/lib/built/ai-trigger.js.map +1 -0
- package/lib/built/command.d.ts +33 -17
- package/lib/built/command.d.ts.map +1 -1
- package/lib/built/command.js +71 -44
- package/lib/built/command.js.map +1 -1
- package/lib/built/component.d.ts +42 -15
- package/lib/built/component.d.ts.map +1 -1
- package/lib/built/component.js +84 -52
- package/lib/built/component.js.map +1 -1
- package/lib/built/config.d.ts +54 -5
- package/lib/built/config.d.ts.map +1 -1
- package/lib/built/config.js +76 -10
- package/lib/built/config.js.map +1 -1
- package/lib/built/cron.d.ts +41 -18
- package/lib/built/cron.d.ts.map +1 -1
- package/lib/built/cron.js +106 -63
- package/lib/built/cron.js.map +1 -1
- package/lib/built/database.d.ts +55 -6
- package/lib/built/database.d.ts.map +1 -1
- package/lib/built/database.js +93 -22
- package/lib/built/database.js.map +1 -1
- package/lib/built/dispatcher.d.ts +118 -0
- package/lib/built/dispatcher.d.ts.map +1 -0
- package/lib/built/dispatcher.js +196 -0
- package/lib/built/dispatcher.js.map +1 -0
- package/lib/built/permission.d.ts +45 -5
- package/lib/built/permission.d.ts.map +1 -1
- package/lib/built/permission.js +56 -11
- package/lib/built/permission.js.map +1 -1
- package/lib/built/skill.d.ts +117 -0
- package/lib/built/skill.d.ts.map +1 -0
- package/lib/built/skill.js +191 -0
- package/lib/built/skill.js.map +1 -0
- package/lib/built/tool.d.ts +188 -0
- package/lib/built/tool.d.ts.map +1 -0
- package/lib/built/tool.js +749 -0
- package/lib/built/tool.js.map +1 -0
- package/lib/feature.d.ts +75 -0
- package/lib/feature.d.ts.map +1 -0
- package/lib/feature.js +69 -0
- package/lib/feature.js.map +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin.d.ts +53 -18
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +301 -31
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +248 -9
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +38 -12
- package/lib/utils.js.map +1 -1
- package/package.json +4 -4
- package/src/adapter.ts +206 -2
- package/src/ai/agent.ts +772 -0
- package/src/ai/context-manager.ts +440 -0
- package/src/ai/conversation-memory.ts +774 -0
- package/src/ai/follow-up.ts +357 -0
- package/src/ai/index.ts +128 -0
- package/src/ai/init.ts +502 -0
- package/src/ai/output.ts +261 -0
- package/src/ai/providers/anthropic.ts +375 -0
- package/src/ai/providers/base.ts +173 -0
- package/src/ai/providers/index.ts +13 -0
- package/src/ai/providers/ollama.ts +292 -0
- package/src/ai/providers/openai.ts +167 -0
- package/src/ai/rate-limiter.ts +129 -0
- package/src/ai/service.ts +319 -0
- package/src/ai/session.ts +544 -0
- package/src/ai/tone-detector.ts +89 -0
- package/src/ai/tools.ts +218 -0
- package/src/ai/types.ts +296 -0
- package/src/ai/user-profile.ts +181 -0
- package/src/ai/zhin-agent.ts +845 -0
- package/src/built/adapter-process.ts +99 -0
- package/src/built/ai-trigger.ts +259 -0
- package/src/built/command.ts +75 -69
- package/src/built/component.ts +94 -76
- package/src/built/config.ts +238 -128
- package/src/built/cron.ts +117 -101
- package/src/built/database.ts +128 -33
- package/src/built/dispatcher.ts +332 -0
- package/src/built/permission.ts +146 -54
- package/src/built/skill.ts +280 -0
- package/src/built/tool.ts +928 -0
- package/src/feature.ts +113 -0
- package/src/index.ts +11 -0
- package/src/plugin.ts +359 -69
- package/src/types.ts +306 -11
- package/src/utils.ts +37 -13
- package/tests/adapter.test.ts +153 -1
- package/tests/ai/agent.test.ts +614 -0
- package/tests/ai/ai-trigger.test.ts +368 -0
- package/tests/ai/context-manager.test.ts +413 -0
- package/tests/ai/conversation-memory.test.ts +128 -0
- package/tests/ai/follow-up.test.ts +175 -0
- package/tests/ai/integration.test.ts +584 -0
- package/tests/ai/output.test.ts +128 -0
- package/tests/ai/providers.integration.test.ts +227 -0
- package/tests/ai/rate-limiter.test.ts +108 -0
- package/tests/ai/session.test.ts +375 -0
- package/tests/ai/setup.ts +308 -0
- package/tests/ai/tone-detector.test.ts +80 -0
- package/tests/ai/tool.test.ts +800 -0
- package/tests/ai/tools-builtin.test.ts +346 -0
- package/tests/ai/user-profile.test.ts +73 -0
- package/tests/ai/zhin-agent.test.ts +177 -0
- package/tests/component-new.test.ts +17 -6
- package/tests/config.test.ts +46 -0
- package/tests/cron.test.ts +94 -5
- package/tests/dispatcher.test.ts +146 -0
- package/tests/feature.test.ts +145 -0
- package/tests/features-builtin.test.ts +191 -0
- package/tests/plugin.test.ts +88 -14
- package/tests/skill-feature.test.ts +179 -0
- package/tests/tool-feature.test.ts +254 -0
package/src/ai/init.ts
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI 模块初始化
|
|
3
|
+
*
|
|
4
|
+
* 将 AI 服务注册到 Zhin 插件系统中:
|
|
5
|
+
* - AIService context
|
|
6
|
+
* - ZhinAgent 全局大脑
|
|
7
|
+
* - AI 触发处理器 (via MessageDispatcher)
|
|
8
|
+
* - 数据库会话/上下文持久化
|
|
9
|
+
* - 消息记录中间件
|
|
10
|
+
* - AI 管理工具
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Logger } from '@zhin.js/logger';
|
|
14
|
+
import { getPlugin, type Plugin } from '../plugin.js';
|
|
15
|
+
import { Message } from '../message.js';
|
|
16
|
+
import type { Tool, ToolContext } from '../types.js';
|
|
17
|
+
import type { AITriggerConfig } from '../built/ai-trigger.js';
|
|
18
|
+
import { ZhinTool, ToolFeature } from '../built/tool.js';
|
|
19
|
+
import {
|
|
20
|
+
shouldTriggerAI,
|
|
21
|
+
inferSenderPermissions,
|
|
22
|
+
parseRichMediaContent,
|
|
23
|
+
mergeAITriggerConfig,
|
|
24
|
+
} from '../built/ai-trigger.js';
|
|
25
|
+
import type { MessageDispatcherService } from '../built/dispatcher.js';
|
|
26
|
+
import type { SkillFeature } from '../built/skill.js';
|
|
27
|
+
import { AIService } from './service.js';
|
|
28
|
+
import { ZhinAgent } from './zhin-agent.js';
|
|
29
|
+
import { SessionManager, createDatabaseSessionManager } from './session.js';
|
|
30
|
+
import { AI_SESSION_MODEL } from './session.js';
|
|
31
|
+
import {
|
|
32
|
+
createContextManager,
|
|
33
|
+
CHAT_MESSAGE_MODEL,
|
|
34
|
+
CONTEXT_SUMMARY_MODEL,
|
|
35
|
+
type MessageRecord,
|
|
36
|
+
} from './context-manager.js';
|
|
37
|
+
import { AI_MESSAGE_MODEL, AI_SUMMARY_MODEL } from './conversation-memory.js';
|
|
38
|
+
import { AI_USER_PROFILE_MODEL } from './user-profile.js';
|
|
39
|
+
import { AI_FOLLOWUP_MODEL } from './follow-up.js';
|
|
40
|
+
import { renderToPlainText, type OutputElement } from './output.js';
|
|
41
|
+
import type { AIConfig, ContentPart } from './types.js';
|
|
42
|
+
|
|
43
|
+
// ============================================================================
|
|
44
|
+
// 类型扩展
|
|
45
|
+
// ============================================================================
|
|
46
|
+
|
|
47
|
+
declare module '../plugin.js' {
|
|
48
|
+
namespace Plugin {
|
|
49
|
+
interface Contexts {
|
|
50
|
+
ai: AIService;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// 辅助函数
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 从消息中提取图片 URL(支持 XML 标签格式和 raw 对象格式)
|
|
61
|
+
*/
|
|
62
|
+
function extractImageUrls(message: Message<any>): string[] {
|
|
63
|
+
const urls: string[] = [];
|
|
64
|
+
const raw = typeof message.$raw === 'string' ? message.$raw : JSON.stringify(message.$raw || '');
|
|
65
|
+
|
|
66
|
+
// 匹配 <image url="..." /> 格式
|
|
67
|
+
const xmlMatches = raw.match(/<image[^>]+url="([^"]+)"/g);
|
|
68
|
+
if (xmlMatches) {
|
|
69
|
+
for (const m of xmlMatches) {
|
|
70
|
+
const urlMatch = m.match(/url="([^"]+)"/);
|
|
71
|
+
if (urlMatch) urls.push(urlMatch[1]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 匹配 [CQ:image,url=...] 格式 (OneBot)
|
|
76
|
+
const cqMatches = raw.match(/\[CQ:image[^\]]*url=([^\],]+)/g);
|
|
77
|
+
if (cqMatches) {
|
|
78
|
+
for (const m of cqMatches) {
|
|
79
|
+
const urlMatch = m.match(/url=([^\],]+)/);
|
|
80
|
+
if (urlMatch) urls.push(urlMatch[1]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return urls;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// 初始化函数
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 初始化 AI 模块
|
|
93
|
+
*
|
|
94
|
+
* 在 setup.ts 中调用:
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { initAIModule } from '@zhin.js/core';
|
|
97
|
+
* initAIModule();
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function initAIModule(): void {
|
|
101
|
+
const plugin = getPlugin();
|
|
102
|
+
const { provide, useContext, root, logger } = plugin;
|
|
103
|
+
// ── 工具服务 ──
|
|
104
|
+
provide(new ToolFeature());
|
|
105
|
+
|
|
106
|
+
// ── 数据库模型定义 ──
|
|
107
|
+
// provide(defineDatabaseService) 之后 defineModel 即可用,直接在顶层定义
|
|
108
|
+
const defineModel = (plugin as any).defineModel as
|
|
109
|
+
| ((name: string, def: any) => void)
|
|
110
|
+
| undefined;
|
|
111
|
+
if (typeof defineModel === 'function') {
|
|
112
|
+
defineModel('chat_messages', CHAT_MESSAGE_MODEL);
|
|
113
|
+
defineModel('context_summaries', CONTEXT_SUMMARY_MODEL);
|
|
114
|
+
defineModel('ai_sessions', AI_SESSION_MODEL);
|
|
115
|
+
defineModel('ai_messages', AI_MESSAGE_MODEL);
|
|
116
|
+
defineModel('ai_summaries', AI_SUMMARY_MODEL);
|
|
117
|
+
defineModel('ai_user_profiles', AI_USER_PROFILE_MODEL);
|
|
118
|
+
defineModel('ai_followups', AI_FOLLOWUP_MODEL);
|
|
119
|
+
logger.debug('AI database models registered (7 tables)');
|
|
120
|
+
} else {
|
|
121
|
+
logger.debug('defineModel not available, AI will use in-memory storage');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── AI 服务实例 ──
|
|
125
|
+
let aiServiceInstance: AIService | null = null;
|
|
126
|
+
let zhinAgentInstance: ZhinAgent | null = null;
|
|
127
|
+
|
|
128
|
+
provide({
|
|
129
|
+
name: 'ai' as any,
|
|
130
|
+
description: 'AI Service - Multi-model LLM integration',
|
|
131
|
+
async mounted(p: Plugin) {
|
|
132
|
+
const configService = root.inject('config');
|
|
133
|
+
const appConfig =
|
|
134
|
+
configService?.get<{ ai?: AIConfig }>('zhin.config.yml') || {};
|
|
135
|
+
const config = appConfig.ai || {};
|
|
136
|
+
|
|
137
|
+
if (config.enabled === false) {
|
|
138
|
+
logger.info('AI Service is disabled');
|
|
139
|
+
return null as any;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const service = new AIService(config);
|
|
143
|
+
aiServiceInstance = service;
|
|
144
|
+
service.setPlugin(root);
|
|
145
|
+
|
|
146
|
+
const providers = service.listProviders();
|
|
147
|
+
if (providers.length === 0) {
|
|
148
|
+
logger.warn(
|
|
149
|
+
'No AI providers configured. Please add API keys in zhin.config.yml',
|
|
150
|
+
);
|
|
151
|
+
} else {
|
|
152
|
+
logger.info(
|
|
153
|
+
`AI Service started with providers: ${providers.join(', ')}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return service;
|
|
158
|
+
},
|
|
159
|
+
async dispose(service: AIService | null) {
|
|
160
|
+
if (service) {
|
|
161
|
+
service.dispose();
|
|
162
|
+
aiServiceInstance = null;
|
|
163
|
+
logger.info('AI Service stopped');
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── ZhinAgent 全局大脑 ──
|
|
169
|
+
useContext('ai' as any, (ai: AIService) => {
|
|
170
|
+
if (!ai.isReady()) {
|
|
171
|
+
logger.warn('AI Service not ready, ZhinAgent not created');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const provider = ai.getProvider();
|
|
176
|
+
const agent = new ZhinAgent(provider);
|
|
177
|
+
zhinAgentInstance = agent;
|
|
178
|
+
|
|
179
|
+
const skillRegistry = root.inject('skill' as any) as
|
|
180
|
+
| SkillFeature
|
|
181
|
+
| undefined;
|
|
182
|
+
if (skillRegistry) agent.setSkillRegistry(skillRegistry);
|
|
183
|
+
|
|
184
|
+
// 注入跟进提醒的发送回调(不依赖数据库,内存模式也能发)
|
|
185
|
+
agent.setFollowUpSender(async (record) => {
|
|
186
|
+
const adapter = root.inject(record.platform as any) as any;
|
|
187
|
+
if (!adapter || typeof adapter.sendMessage !== 'function') {
|
|
188
|
+
logger.warn(`[跟进提醒] 找不到适配器: ${record.platform}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const content = `⏰ 定时提醒:${record.message}`;
|
|
192
|
+
await adapter.sendMessage({
|
|
193
|
+
context: record.platform,
|
|
194
|
+
bot: record.bot_id,
|
|
195
|
+
id: record.scene_id,
|
|
196
|
+
type: record.scene_type as any,
|
|
197
|
+
content,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
logger.debug('ZhinAgent created');
|
|
202
|
+
return () => {
|
|
203
|
+
agent.dispose();
|
|
204
|
+
zhinAgentInstance = null;
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// ── AI 触发处理器 ──
|
|
209
|
+
useContext('ai' as any, (ai: AIService) => {
|
|
210
|
+
const rawConfig = ai.getTriggerConfig();
|
|
211
|
+
const triggerConfig = mergeAITriggerConfig(rawConfig);
|
|
212
|
+
if (!triggerConfig.enabled) {
|
|
213
|
+
logger.info('AI Trigger is disabled');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const renderOutput = (elements: OutputElement[]): string => {
|
|
218
|
+
const parts: string[] = [];
|
|
219
|
+
for (const el of elements) {
|
|
220
|
+
switch (el.type) {
|
|
221
|
+
case 'text':
|
|
222
|
+
if (el.content) parts.push(el.content);
|
|
223
|
+
break;
|
|
224
|
+
case 'image':
|
|
225
|
+
parts.push(`<image url="${el.url}"/>`);
|
|
226
|
+
break;
|
|
227
|
+
case 'audio':
|
|
228
|
+
parts.push(`<audio url="${el.url}"/>`);
|
|
229
|
+
break;
|
|
230
|
+
case 'video':
|
|
231
|
+
parts.push(`<video url="${el.url}"/>`);
|
|
232
|
+
break;
|
|
233
|
+
case 'card': {
|
|
234
|
+
const cp = [`📋 ${el.title}`];
|
|
235
|
+
if (el.description) cp.push(el.description);
|
|
236
|
+
if (el.fields?.length)
|
|
237
|
+
for (const f of el.fields) cp.push(` ${f.label}: ${f.value}`);
|
|
238
|
+
if (el.imageUrl) cp.push(`<image url="${el.imageUrl}"/>`);
|
|
239
|
+
parts.push(cp.join('\n'));
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case 'file':
|
|
243
|
+
parts.push(`📎 ${el.name}: ${el.url}`);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return parts.join('\n') || '';
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const handleAIMessage = async (
|
|
251
|
+
message: Message<any>,
|
|
252
|
+
content: string,
|
|
253
|
+
) => {
|
|
254
|
+
const t0 = performance.now();
|
|
255
|
+
if (!ai.isReady()) return;
|
|
256
|
+
if (triggerConfig.thinkingMessage)
|
|
257
|
+
await message.$reply(triggerConfig.thinkingMessage);
|
|
258
|
+
|
|
259
|
+
const permissions = inferSenderPermissions(message, triggerConfig);
|
|
260
|
+
const toolContext: ToolContext = {
|
|
261
|
+
platform: message.$adapter,
|
|
262
|
+
botId: message.$bot,
|
|
263
|
+
sceneId: message.$channel?.id || message.$sender.id,
|
|
264
|
+
senderId: message.$sender.id,
|
|
265
|
+
message,
|
|
266
|
+
scope: permissions.scope,
|
|
267
|
+
senderPermissionLevel: permissions.permissionLevel,
|
|
268
|
+
isGroupAdmin: permissions.isGroupAdmin,
|
|
269
|
+
isGroupOwner: permissions.isGroupOwner,
|
|
270
|
+
isBotAdmin: permissions.isBotAdmin,
|
|
271
|
+
isOwner: permissions.isOwner,
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const tCollect = performance.now();
|
|
275
|
+
const toolService = root.inject('tool');
|
|
276
|
+
let externalTools: Tool[] = [];
|
|
277
|
+
if (toolService) {
|
|
278
|
+
externalTools = toolService.collectAll(root);
|
|
279
|
+
externalTools = toolService.filterByContext(externalTools, toolContext);
|
|
280
|
+
}
|
|
281
|
+
logger.debug(`[AI Handler] 工具收集: ${externalTools.length} 个, ${(performance.now() - tCollect).toFixed(0)}ms`);
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const timeout = new Promise<never>((_, rej) =>
|
|
285
|
+
setTimeout(() => rej(new Error('AI 响应超时')), triggerConfig.timeout),
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
let responseText: string;
|
|
289
|
+
if (zhinAgentInstance) {
|
|
290
|
+
// 检查消息是否包含图片(多模态路由)
|
|
291
|
+
const imageUrls = extractImageUrls(message);
|
|
292
|
+
let elements: OutputElement[];
|
|
293
|
+
if (imageUrls.length > 0) {
|
|
294
|
+
const parts: ContentPart[] = [];
|
|
295
|
+
if (content) parts.push({ type: 'text', text: content });
|
|
296
|
+
for (const url of imageUrls) {
|
|
297
|
+
parts.push({ type: 'image_url', image_url: { url } });
|
|
298
|
+
}
|
|
299
|
+
elements = await Promise.race([
|
|
300
|
+
zhinAgentInstance.processMultimodal(parts, toolContext),
|
|
301
|
+
timeout,
|
|
302
|
+
]);
|
|
303
|
+
} else {
|
|
304
|
+
elements = await Promise.race([
|
|
305
|
+
zhinAgentInstance.process(content, toolContext, externalTools),
|
|
306
|
+
timeout,
|
|
307
|
+
]);
|
|
308
|
+
}
|
|
309
|
+
responseText = renderOutput(elements);
|
|
310
|
+
} else {
|
|
311
|
+
const response = await Promise.race([
|
|
312
|
+
ai.process(content, toolContext, externalTools),
|
|
313
|
+
timeout,
|
|
314
|
+
]);
|
|
315
|
+
responseText = typeof response === 'string' ? response : '';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (responseText) await message.$reply(parseRichMediaContent(responseText));
|
|
319
|
+
logger.info(`[AI Handler] 总耗时: ${(performance.now() - t0).toFixed(0)}ms`);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
322
|
+
logger.warn(`[AI Handler] 失败 (${(performance.now() - t0).toFixed(0)}ms): ${msg}`);
|
|
323
|
+
await message.$reply(triggerConfig.errorTemplate.replace('{error}', msg));
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const dispatcher = root.inject('dispatcher' as any) as
|
|
328
|
+
| MessageDispatcherService
|
|
329
|
+
| undefined;
|
|
330
|
+
|
|
331
|
+
if (dispatcher && typeof dispatcher.setAIHandler === 'function') {
|
|
332
|
+
dispatcher.setAITriggerMatcher((message: Message<any>) =>
|
|
333
|
+
shouldTriggerAI(message, triggerConfig),
|
|
334
|
+
);
|
|
335
|
+
dispatcher.setAIHandler(handleAIMessage);
|
|
336
|
+
logger.debug('AI Handler registered via MessageDispatcher');
|
|
337
|
+
return () => { logger.info('AI Handler unregistered'); };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 回退中间件
|
|
341
|
+
const aiMw = async (
|
|
342
|
+
message: Message<any>,
|
|
343
|
+
next: () => Promise<void>,
|
|
344
|
+
) => {
|
|
345
|
+
const { triggered, content } = shouldTriggerAI(message, triggerConfig);
|
|
346
|
+
if (!triggered) return await next();
|
|
347
|
+
await handleAIMessage(message, content);
|
|
348
|
+
await next();
|
|
349
|
+
};
|
|
350
|
+
const dispose = root.addMiddleware(aiMw);
|
|
351
|
+
logger.debug('AI Trigger middleware registered (fallback mode)');
|
|
352
|
+
return () => { dispose(); };
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ── 数据库集成(db 就绪后升级各组件到数据库存储)──
|
|
356
|
+
useContext('database' as any, (db: any) => {
|
|
357
|
+
setTimeout(() => {
|
|
358
|
+
if (!aiServiceInstance) return;
|
|
359
|
+
const configService = root.inject('config');
|
|
360
|
+
const appConfig =
|
|
361
|
+
configService?.get<{ ai?: AIConfig }>('zhin.config.yml') || {};
|
|
362
|
+
const config = appConfig.ai || {};
|
|
363
|
+
|
|
364
|
+
if (config.sessions?.useDatabase === false) return;
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const model = db.models.get('ai_sessions');
|
|
368
|
+
if (!model) return;
|
|
369
|
+
|
|
370
|
+
const dbSession = createDatabaseSessionManager(
|
|
371
|
+
model,
|
|
372
|
+
aiServiceInstance.getSessionConfig(),
|
|
373
|
+
);
|
|
374
|
+
aiServiceInstance.setSessionManager(dbSession);
|
|
375
|
+
if (zhinAgentInstance) zhinAgentInstance.setSessionManager(dbSession);
|
|
376
|
+
|
|
377
|
+
const ctxCfg = aiServiceInstance.getContextConfig();
|
|
378
|
+
if (ctxCfg.enabled !== false) {
|
|
379
|
+
const msgModel = db.models.get('chat_messages');
|
|
380
|
+
const sumModel = db.models.get('context_summaries');
|
|
381
|
+
if (msgModel && sumModel) {
|
|
382
|
+
const ctxMgr = createContextManager(msgModel, sumModel, ctxCfg);
|
|
383
|
+
aiServiceInstance.setContextManager(ctxMgr);
|
|
384
|
+
if (zhinAgentInstance) zhinAgentInstance.setContextManager(ctxMgr);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ConversationMemory 升级到数据库
|
|
389
|
+
if (zhinAgentInstance) {
|
|
390
|
+
const aiMsgModel = db.models.get('ai_messages');
|
|
391
|
+
const aiSumModel = db.models.get('ai_summaries');
|
|
392
|
+
if (aiMsgModel && aiSumModel) {
|
|
393
|
+
zhinAgentInstance.upgradeMemoryToDatabase(aiMsgModel, aiSumModel);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// UserProfile 升级到数据库
|
|
397
|
+
const profileModel = db.models.get('ai_user_profiles');
|
|
398
|
+
if (profileModel) {
|
|
399
|
+
zhinAgentInstance.upgradeProfilesToDatabase(profileModel);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// FollowUp 升级到数据库 + 恢复未完成任务
|
|
403
|
+
const followUpModel = db.models.get('ai_followups');
|
|
404
|
+
if (followUpModel) {
|
|
405
|
+
zhinAgentInstance.upgradeFollowUpsToDatabase(followUpModel);
|
|
406
|
+
|
|
407
|
+
// 从数据库恢复未完成的跟进任务
|
|
408
|
+
zhinAgentInstance.restoreFollowUps().catch(e => {
|
|
409
|
+
logger.warn('FollowUp restore failed:', e);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
logger.debug('AI database storage activated (session, memory, profile, followup)');
|
|
415
|
+
} catch (e) {
|
|
416
|
+
logger.error('AI Session: database setup failed:', e);
|
|
417
|
+
}
|
|
418
|
+
}, 100);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ── 消息记录中间件 ──
|
|
422
|
+
root.addMiddleware(async (message: Message, next: () => Promise<void>) => {
|
|
423
|
+
await next();
|
|
424
|
+
if (aiServiceInstance?.contextManager) {
|
|
425
|
+
const record: MessageRecord = {
|
|
426
|
+
platform: message.$adapter,
|
|
427
|
+
scene_id: message.$channel?.id || message.$sender.id,
|
|
428
|
+
scene_type: message.$channel?.type || 'private',
|
|
429
|
+
scene_name: (message.$channel as any)?.name || '',
|
|
430
|
+
sender_id: message.$sender.id,
|
|
431
|
+
sender_name: message.$sender.name || message.$sender.id,
|
|
432
|
+
message:
|
|
433
|
+
typeof message.$raw === 'string'
|
|
434
|
+
? message.$raw
|
|
435
|
+
: JSON.stringify(message.$raw),
|
|
436
|
+
time: message.$timestamp || Date.now(),
|
|
437
|
+
};
|
|
438
|
+
aiServiceInstance.contextManager.recordMessage(record).catch(() => {});
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// ── AI 管理工具 ──
|
|
443
|
+
useContext('ai' as any, 'tool' as any, (ai: AIService | undefined, toolService: any) => {
|
|
444
|
+
if (!ai || !toolService) return;
|
|
445
|
+
|
|
446
|
+
const listModelsTool = new ZhinTool('ai.models')
|
|
447
|
+
.desc('列出所有可用的 AI 模型')
|
|
448
|
+
.keyword('模型', '可用模型', 'ai模型', 'model', 'models')
|
|
449
|
+
.tag('ai', 'management')
|
|
450
|
+
.execute(async () => {
|
|
451
|
+
const models = await ai.listModels();
|
|
452
|
+
return { providers: models.map(({ provider, models: ml }) => ({ name: provider, models: ml.slice(0, 10), total: ml.length })) };
|
|
453
|
+
})
|
|
454
|
+
.action(async () => {
|
|
455
|
+
const models = await ai.listModels();
|
|
456
|
+
let r = '🤖 可用模型:\n';
|
|
457
|
+
for (const { provider, models: ml } of models) {
|
|
458
|
+
r += `\n【${provider}】\n` + ml.slice(0, 5).map(m => ` • ${m}`).join('\n');
|
|
459
|
+
if (ml.length > 5) r += `\n ... 还有 ${ml.length - 5} 个`;
|
|
460
|
+
}
|
|
461
|
+
return r;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const clearSessionTool = new ZhinTool('ai.clear')
|
|
465
|
+
.desc('清除当前对话的历史记录')
|
|
466
|
+
.keyword('清除', '清空', '重置', 'clear', 'reset')
|
|
467
|
+
.tag('ai', 'session')
|
|
468
|
+
.execute(async (_args, context) => {
|
|
469
|
+
if (!context?.message) return { success: false, error: '无消息上下文' };
|
|
470
|
+
const msg = context.message as Message;
|
|
471
|
+
const sid = SessionManager.generateId(msg.$adapter, msg.$sender.id, msg.$channel?.id);
|
|
472
|
+
await ai.sessions.reset(sid);
|
|
473
|
+
return { success: true, message: '对话历史已清除' };
|
|
474
|
+
})
|
|
475
|
+
.action(async (message: Message) => {
|
|
476
|
+
const sid = SessionManager.generateId(message.$adapter, message.$sender.id, message.$channel?.id);
|
|
477
|
+
await ai.sessions.reset(sid);
|
|
478
|
+
return '✅ 对话历史已清除';
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const healthCheckTool = new ZhinTool('ai.health')
|
|
482
|
+
.desc('检查 AI 服务的健康状态')
|
|
483
|
+
.keyword('健康', '状态', '检查', 'health', 'status')
|
|
484
|
+
.tag('ai', 'management')
|
|
485
|
+
.execute(async () => {
|
|
486
|
+
const h = await ai.healthCheck();
|
|
487
|
+
return { providers: Object.entries(h).map(([n, ok]) => ({ name: n, healthy: ok })) };
|
|
488
|
+
})
|
|
489
|
+
.action(async () => {
|
|
490
|
+
const h = await ai.healthCheck();
|
|
491
|
+
return ['🏥 AI 服务健康状态:'].concat(
|
|
492
|
+
Object.entries(h).map(([p, ok]) => ` ${ok ? '✅' : '❌'} ${p}`),
|
|
493
|
+
).join('\n');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const tools = [listModelsTool, clearSessionTool, healthCheckTool];
|
|
497
|
+
const disposers: (() => void)[] = [];
|
|
498
|
+
for (const tool of tools) disposers.push(toolService.addTool(tool, root.name));
|
|
499
|
+
logger.debug(`Registered ${tools.length} AI management tools`);
|
|
500
|
+
return () => disposers.forEach(d => d());
|
|
501
|
+
});
|
|
502
|
+
}
|