@zhin.js/core 1.0.25 → 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 +10 -0
- package/README.md +84 -342
- package/lib/adapter.d.ts +17 -0
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +84 -2
- 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/ai-trigger.d.ts.map +1 -1
- package/lib/built/ai-trigger.js +7 -3
- package/lib/built/ai-trigger.js.map +1 -1
- 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 +71 -164
- package/lib/built/tool.d.ts.map +1 -1
- package/lib/built/tool.js +212 -297
- package/lib/built/tool.js.map +1 -1
- 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 +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin.d.ts +25 -17
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +180 -20
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +4 -9
- package/lib/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/adapter.ts +101 -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/ai-trigger.ts +6 -3
- 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 +245 -366
- package/src/feature.ts +113 -0
- package/src/index.ts +7 -0
- package/src/plugin.ts +198 -33
- package/src/types.ts +6 -10
- 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/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
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZhinAgent — 全局持久 AI 大脑
|
|
3
|
+
*
|
|
4
|
+
* 取代旧的 AIService.process() 临时创建 Agent 的方式。
|
|
5
|
+
*
|
|
6
|
+
* 核心能力:
|
|
7
|
+
* 1. 全局单例,应用生命周期内常驻
|
|
8
|
+
* 2. Skill 感知:两级过滤 Skill → Tool
|
|
9
|
+
* 3. 双层记忆:per-scene(对话上下文)+ per-user(长期偏好)
|
|
10
|
+
* 4. 任务规划:复杂请求自动分解为子步骤
|
|
11
|
+
* 5. 多模态输出:结构化 OutputElement[]
|
|
12
|
+
* 6. 智能路径选择:纯闲聊走轻量路径,工具请求走完整路径
|
|
13
|
+
* 7. 用户画像:跨会话个性化记忆
|
|
14
|
+
* 8. 速率限制:防止单用户过度消耗资源
|
|
15
|
+
* 9. 流式输出:onChunk 回调实时推送部分文本
|
|
16
|
+
* 10. 情绪感知:根据用户语气调整回复风格
|
|
17
|
+
* 11. 主动跟进:schedule_followup 定时回查
|
|
18
|
+
* 12. 多模态输入:图片/音频直接传给视觉模型
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Logger } from '@zhin.js/logger';
|
|
22
|
+
import type { Tool, ToolContext } from '../types.js';
|
|
23
|
+
import type { SkillFeature } from '../built/skill.js';
|
|
24
|
+
import type {
|
|
25
|
+
AIProvider,
|
|
26
|
+
AgentTool,
|
|
27
|
+
ChatMessage,
|
|
28
|
+
ContentPart,
|
|
29
|
+
} from './types.js';
|
|
30
|
+
import { Agent, createAgent } from './agent.js';
|
|
31
|
+
import { SessionManager, createMemorySessionManager } from './session.js';
|
|
32
|
+
import type { ContextManager } from './context-manager.js';
|
|
33
|
+
import { ConversationMemory } from './conversation-memory.js';
|
|
34
|
+
import type { OutputElement } from './output.js';
|
|
35
|
+
import { parseOutput } from './output.js';
|
|
36
|
+
import { UserProfileStore } from './user-profile.js';
|
|
37
|
+
import { RateLimiter, type RateLimitConfig } from './rate-limiter.js';
|
|
38
|
+
import { detectTone } from './tone-detector.js';
|
|
39
|
+
import { FollowUpManager, type FollowUpSender } from './follow-up.js';
|
|
40
|
+
|
|
41
|
+
const logger = new Logger(null, 'ZhinAgent');
|
|
42
|
+
|
|
43
|
+
/** 高精度计时 */
|
|
44
|
+
const now = () => performance.now();
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// 配置
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
export interface ZhinAgentConfig {
|
|
51
|
+
/** 默认系统人格 */
|
|
52
|
+
persona?: string;
|
|
53
|
+
/** 最大工具调用轮数 */
|
|
54
|
+
maxIterations?: number;
|
|
55
|
+
/** 单次请求超时 (ms) */
|
|
56
|
+
timeout?: number;
|
|
57
|
+
/** 预执行超时 (ms) */
|
|
58
|
+
preExecTimeout?: number;
|
|
59
|
+
/** Skill 选择最大数量 */
|
|
60
|
+
maxSkills?: number;
|
|
61
|
+
/** Tool 选择最大数量 */
|
|
62
|
+
maxTools?: number;
|
|
63
|
+
/** 一个话题至少持续多少轮才触发摘要(默认 5) */
|
|
64
|
+
minTopicRounds?: number;
|
|
65
|
+
/** 滑动窗口大小:最近 N 轮消息(默认 5) */
|
|
66
|
+
slidingWindowSize?: number;
|
|
67
|
+
/** 话题切换检测阈值(0-1,值越低越敏感,默认 0.15) */
|
|
68
|
+
topicChangeThreshold?: number;
|
|
69
|
+
/** 速率限制配置 */
|
|
70
|
+
rateLimit?: RateLimitConfig;
|
|
71
|
+
/** 是否启用情绪感知(默认 true) */
|
|
72
|
+
toneAwareness?: boolean;
|
|
73
|
+
/** 视觉模型名称(如 llava, bakllava),留空则不启用视觉 */
|
|
74
|
+
visionModel?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const DEFAULT_CONFIG: Required<ZhinAgentConfig> = {
|
|
78
|
+
persona: '你是一个友好的中文 AI 助手,擅长使用工具帮助用户解决问题。',
|
|
79
|
+
maxIterations: 5,
|
|
80
|
+
timeout: 60_000,
|
|
81
|
+
preExecTimeout: 10_000,
|
|
82
|
+
maxSkills: 3,
|
|
83
|
+
maxTools: 8,
|
|
84
|
+
minTopicRounds: 5,
|
|
85
|
+
slidingWindowSize: 5,
|
|
86
|
+
topicChangeThreshold: 0.15,
|
|
87
|
+
rateLimit: {},
|
|
88
|
+
toneAwareness: true,
|
|
89
|
+
visionModel: '',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// 流式回调
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 流式输出回调 — 适配器可通过此回调实时更新消息
|
|
98
|
+
*
|
|
99
|
+
* @param chunk 增量文本片段
|
|
100
|
+
* @param full 到目前为止的完整文本
|
|
101
|
+
*/
|
|
102
|
+
export type OnChunkCallback = (chunk: string, full: string) => void;
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// 权限映射
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
const PERM_MAP: Record<string, number> = {
|
|
109
|
+
user: 0,
|
|
110
|
+
group_admin: 1,
|
|
111
|
+
group_owner: 2,
|
|
112
|
+
bot_admin: 3,
|
|
113
|
+
owner: 4,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// ZhinAgent
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
export class ZhinAgent {
|
|
121
|
+
private provider: AIProvider;
|
|
122
|
+
private config: Required<ZhinAgentConfig>;
|
|
123
|
+
private skillRegistry: SkillFeature | null = null;
|
|
124
|
+
private sessions: SessionManager;
|
|
125
|
+
private contextManager: ContextManager | null = null;
|
|
126
|
+
private memory: ConversationMemory;
|
|
127
|
+
private externalTools: Map<string, AgentTool> = new Map();
|
|
128
|
+
private userProfiles: UserProfileStore;
|
|
129
|
+
private rateLimiter: RateLimiter;
|
|
130
|
+
private followUps: FollowUpManager;
|
|
131
|
+
|
|
132
|
+
constructor(provider: AIProvider, config?: ZhinAgentConfig) {
|
|
133
|
+
this.provider = provider;
|
|
134
|
+
this.config = { ...DEFAULT_CONFIG, ...config } as Required<ZhinAgentConfig>;
|
|
135
|
+
this.sessions = createMemorySessionManager();
|
|
136
|
+
this.memory = new ConversationMemory({
|
|
137
|
+
minTopicRounds: this.config.minTopicRounds,
|
|
138
|
+
slidingWindowSize: this.config.slidingWindowSize,
|
|
139
|
+
topicChangeThreshold: this.config.topicChangeThreshold,
|
|
140
|
+
});
|
|
141
|
+
this.memory.setProvider(provider);
|
|
142
|
+
this.userProfiles = new UserProfileStore();
|
|
143
|
+
this.rateLimiter = new RateLimiter(this.config.rateLimit);
|
|
144
|
+
this.followUps = new FollowUpManager();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── 依赖注入 ─────────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
setSkillRegistry(registry: SkillFeature): void {
|
|
150
|
+
this.skillRegistry = registry;
|
|
151
|
+
logger.debug(`SkillRegistry connected (${registry.size} skills)`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
setSessionManager(manager: SessionManager): void {
|
|
155
|
+
this.sessions.dispose();
|
|
156
|
+
this.sessions = manager;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
setContextManager(manager: ContextManager): void {
|
|
160
|
+
this.contextManager = manager;
|
|
161
|
+
manager.setAIProvider(this.provider);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 将 ConversationMemory 升级为数据库存储 */
|
|
165
|
+
upgradeMemoryToDatabase(msgModel: any, sumModel: any): void {
|
|
166
|
+
this.memory.upgradeToDatabase(msgModel, sumModel);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 将 UserProfileStore 升级为数据库存储 */
|
|
170
|
+
upgradeProfilesToDatabase(model: any): void {
|
|
171
|
+
this.userProfiles.upgradeToDatabase(model);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 将 FollowUpManager 升级为数据库存储 */
|
|
175
|
+
upgradeFollowUpsToDatabase(model: any): void {
|
|
176
|
+
this.followUps.upgradeToDatabase(model);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** 注入提醒消息发送回调(由 init.ts 在适配器就绪后调用) */
|
|
180
|
+
setFollowUpSender(sender: FollowUpSender): void {
|
|
181
|
+
this.followUps.setSender(sender);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 从数据库恢复未完成的跟进任务(启动时调用)
|
|
186
|
+
* @returns 恢复的任务数量
|
|
187
|
+
*/
|
|
188
|
+
async restoreFollowUps(): Promise<number> {
|
|
189
|
+
return this.followUps.restore();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 获取 UserProfileStore(用于外部注册) */
|
|
193
|
+
getUserProfiles(): UserProfileStore {
|
|
194
|
+
return this.userProfiles;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
registerTool(tool: AgentTool): () => void {
|
|
198
|
+
this.externalTools.set(tool.name, tool);
|
|
199
|
+
return () => { this.externalTools.delete(tool.name); };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── 核心处理入口 ─────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 处理用户消息 — 唯一的公开入口
|
|
206
|
+
*
|
|
207
|
+
* @param content 用户消息文本
|
|
208
|
+
* @param context 工具上下文(平台、发送者、权限等)
|
|
209
|
+
* @param externalTools 外部传入的工具列表
|
|
210
|
+
* @param onChunk 流式输出回调(可选,适配器支持时传入)
|
|
211
|
+
*
|
|
212
|
+
* 路径选择策略(按开销从低到高):
|
|
213
|
+
*
|
|
214
|
+
* ┌─ 闲聊路径(最快)────────────────────────────────────────────┐
|
|
215
|
+
* │ 工具过滤 = 0 → 仅 persona prompt → 流式 1 次 LLM 调用 │
|
|
216
|
+
* └──────────────────────────────────────────────────────────────┘
|
|
217
|
+
* ┌─ 快速路径(1 轮 LLM)───────────────────────────────────────┐
|
|
218
|
+
* │ 全部命中无参数工具 → 预执行 → 数据注入 prompt → 1 次 LLM │
|
|
219
|
+
* └──────────────────────────────────────────────────────────────┘
|
|
220
|
+
* ┌─ Agent 路径(多轮 LLM)─────────────────────────────────────┐
|
|
221
|
+
* │ 存在需参数工具 → Agent tool-calling → 多轮 LLM │
|
|
222
|
+
* └──────────────────────────────────────────────────────────────┘
|
|
223
|
+
*/
|
|
224
|
+
async process(
|
|
225
|
+
content: string,
|
|
226
|
+
context: ToolContext,
|
|
227
|
+
externalTools: Tool[] = [],
|
|
228
|
+
onChunk?: OnChunkCallback,
|
|
229
|
+
): Promise<OutputElement[]> {
|
|
230
|
+
const t0 = now();
|
|
231
|
+
const { senderId, sceneId, platform } = context;
|
|
232
|
+
const sessionId = SessionManager.generateId(platform || '', senderId || '', sceneId);
|
|
233
|
+
const userId = senderId || 'unknown';
|
|
234
|
+
|
|
235
|
+
// ══════ 0. 速率限制检查 ══════
|
|
236
|
+
const rateCheck = this.rateLimiter.check(userId);
|
|
237
|
+
if (!rateCheck.allowed) {
|
|
238
|
+
logger.debug(`[速率限制] 用户 ${userId} 被限制: ${rateCheck.message}`);
|
|
239
|
+
return parseOutput(rateCheck.message || '请稍后再试');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ══════ 1. 收集工具 — 两级过滤 ══════
|
|
243
|
+
const tFilter = now();
|
|
244
|
+
const allTools = this.collectTools(content, context, externalTools);
|
|
245
|
+
|
|
246
|
+
// 注入内置工具
|
|
247
|
+
allTools.push(this.createChatHistoryTool(sessionId));
|
|
248
|
+
allTools.push(this.createUserProfileTool(userId));
|
|
249
|
+
allTools.push(this.createScheduleFollowUpTool(sessionId, context));
|
|
250
|
+
|
|
251
|
+
const filterMs = (now() - tFilter).toFixed(0);
|
|
252
|
+
|
|
253
|
+
// ══════ 2. 构建会话记忆 + 用户画像 ══════
|
|
254
|
+
const tMem = now();
|
|
255
|
+
const historyMessages = await this.buildHistoryMessages(sessionId);
|
|
256
|
+
const memMs = (now() - tMem).toFixed(0);
|
|
257
|
+
|
|
258
|
+
// ══════ 2.5 用户画像 & 情绪感知 ══════
|
|
259
|
+
const profileSummary = await this.userProfiles.buildProfileSummary(userId);
|
|
260
|
+
const toneHint = this.config.toneAwareness ? detectTone(content).hint : '';
|
|
261
|
+
const personaEnhanced = this.buildEnhancedPersona(profileSummary, toneHint);
|
|
262
|
+
|
|
263
|
+
// ══════ 3. 无工具 → 闲聊路径 (轻量 prompt + 历史) ══════
|
|
264
|
+
if (allTools.length === 0) {
|
|
265
|
+
logger.debug(`[闲聊路径] 过滤=${filterMs}ms, 记忆=${memMs}ms (${historyMessages.length}条), 0 工具`);
|
|
266
|
+
const tLLM = now();
|
|
267
|
+
const reply = await this.streamChatWithHistory(content, personaEnhanced, historyMessages, onChunk);
|
|
268
|
+
const llmMs = (now() - tLLM).toFixed(0);
|
|
269
|
+
logger.info(`[闲聊路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, LLM=${llmMs}ms, 总=${(now() - t0).toFixed(0)}ms`);
|
|
270
|
+
await this.saveToSession(sessionId, content, reply, sceneId);
|
|
271
|
+
return parseOutput(reply);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
logger.debug(`[工具路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, ${allTools.length} 工具 (${allTools.map(t => t.name).join(', ')})`);
|
|
275
|
+
|
|
276
|
+
// ══════ 4. 拆分无参数 / 有参数工具 ══════
|
|
277
|
+
const noParamTools: AgentTool[] = [];
|
|
278
|
+
const paramTools: AgentTool[] = [];
|
|
279
|
+
for (const tool of allTools) {
|
|
280
|
+
const required = tool.parameters?.required;
|
|
281
|
+
(!required || required.length === 0) ? noParamTools.push(tool) : paramTools.push(tool);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ══════ 5. 预执行无参数工具 ══════
|
|
285
|
+
let preData = '';
|
|
286
|
+
if (noParamTools.length > 0) {
|
|
287
|
+
const tPre = now();
|
|
288
|
+
logger.debug(`预执行: ${noParamTools.map(t => t.name).join(', ')}`);
|
|
289
|
+
const results = await Promise.allSettled(
|
|
290
|
+
noParamTools.map(async (tool) => {
|
|
291
|
+
const result = await Promise.race([
|
|
292
|
+
tool.execute({}),
|
|
293
|
+
new Promise<never>((_, rej) =>
|
|
294
|
+
setTimeout(() => rej(new Error('超时')), this.config.preExecTimeout)),
|
|
295
|
+
]);
|
|
296
|
+
return { name: tool.name, result };
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
for (const r of results) {
|
|
300
|
+
if (r.status === 'fulfilled') {
|
|
301
|
+
const s = typeof r.value.result === 'string' ? r.value.result : JSON.stringify(r.value.result);
|
|
302
|
+
preData += `\n【${r.value.name}】${s}`;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
logger.debug(`预执行耗时: ${(now() - tPre).toFixed(0)}ms`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ══════ 6. 路径选择 ══════
|
|
309
|
+
let reply: string;
|
|
310
|
+
|
|
311
|
+
if (paramTools.length === 0 && preData) {
|
|
312
|
+
// ── 快速路径: 只有预执行数据 → 1 轮 AI ──
|
|
313
|
+
const tLLM = now();
|
|
314
|
+
const prompt = `${personaEnhanced}
|
|
315
|
+
|
|
316
|
+
以下是根据用户问题自动获取的实时数据:
|
|
317
|
+
${preData}
|
|
318
|
+
|
|
319
|
+
请基于以上数据,用自然流畅的中文回答用户问题。突出重点,适当使用 emoji。`;
|
|
320
|
+
reply = await this.streamChatWithHistory(content, prompt, historyMessages, onChunk);
|
|
321
|
+
logger.info(`[快速路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, LLM=${(now() - tLLM).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
|
|
322
|
+
} else {
|
|
323
|
+
// ── Agent 路径: 需要参数的工具 → 多轮 ──
|
|
324
|
+
const tAgent = now();
|
|
325
|
+
logger.debug(`Agent 路径: ${paramTools.length} 个参数工具`);
|
|
326
|
+
const contextHint = this.buildContextHint(context, content);
|
|
327
|
+
const systemPrompt = `${personaEnhanced}
|
|
328
|
+
${contextHint}
|
|
329
|
+
${preData ? `\n已自动获取的数据:${preData}\n` : ''}
|
|
330
|
+
## 工作流程
|
|
331
|
+
1. 分析用户的问题
|
|
332
|
+
2. 如果已获取的数据能回答问题,直接作答
|
|
333
|
+
3. 如果还需要更多信息,调用工具获取(直接调用,不要解释)
|
|
334
|
+
4. 获取工具结果后,**务必**生成一条完整、自然的中文回答
|
|
335
|
+
|
|
336
|
+
## 关键要求
|
|
337
|
+
- 调用工具后你**必须**基于结果给出完整回答,绝不能返回空内容
|
|
338
|
+
- 用自然语言总结工具结果,突出关键信息
|
|
339
|
+
- 适当使用 emoji 让回答更生动`;
|
|
340
|
+
|
|
341
|
+
const agentTools = paramTools.length > 0 ? paramTools : allTools;
|
|
342
|
+
const agent = createAgent(this.provider, {
|
|
343
|
+
systemPrompt,
|
|
344
|
+
tools: agentTools,
|
|
345
|
+
maxIterations: this.config.maxIterations,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Agent 路径也注入历史上下文
|
|
349
|
+
const result = await agent.run(content, historyMessages);
|
|
350
|
+
reply = result.content || this.fallbackFormat(result.toolCalls);
|
|
351
|
+
logger.info(`[Agent 路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, Agent=${(now() - tAgent).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
await this.saveToSession(sessionId, content, reply, sceneId);
|
|
355
|
+
return parseOutput(reply);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 处理多模态消息(图片+文字)
|
|
360
|
+
*
|
|
361
|
+
* 当用户发送图片时,走视觉模型路径。
|
|
362
|
+
*/
|
|
363
|
+
async processMultimodal(
|
|
364
|
+
parts: ContentPart[],
|
|
365
|
+
context: ToolContext,
|
|
366
|
+
onChunk?: OnChunkCallback,
|
|
367
|
+
): Promise<OutputElement[]> {
|
|
368
|
+
const { senderId, sceneId, platform } = context;
|
|
369
|
+
const sessionId = SessionManager.generateId(platform || '', senderId || '', sceneId);
|
|
370
|
+
const userId = senderId || 'unknown';
|
|
371
|
+
|
|
372
|
+
// 速率限制
|
|
373
|
+
const rateCheck = this.rateLimiter.check(userId);
|
|
374
|
+
if (!rateCheck.allowed) {
|
|
375
|
+
return parseOutput(rateCheck.message || '请稍后再试');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 构建记忆
|
|
379
|
+
const historyMessages = await this.buildHistoryMessages(sessionId);
|
|
380
|
+
const profileSummary = await this.userProfiles.buildProfileSummary(userId);
|
|
381
|
+
const personaEnhanced = this.buildEnhancedPersona(profileSummary, '');
|
|
382
|
+
|
|
383
|
+
// 提取文本部分用于保存
|
|
384
|
+
const textContent = parts
|
|
385
|
+
.filter((p): p is Extract<ContentPart, { type: 'text' }> => p.type === 'text')
|
|
386
|
+
.map(p => p.text)
|
|
387
|
+
.join(' ') || '[多模态消息]';
|
|
388
|
+
|
|
389
|
+
// 选择模型:优先视觉模型
|
|
390
|
+
const visionModel = this.config.visionModel || this.provider.models[0];
|
|
391
|
+
|
|
392
|
+
const messages: ChatMessage[] = [
|
|
393
|
+
{ role: 'system', content: personaEnhanced },
|
|
394
|
+
...historyMessages,
|
|
395
|
+
{ role: 'user', content: parts },
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
let reply = '';
|
|
399
|
+
try {
|
|
400
|
+
for await (const chunk of this.provider.chatStream({ model: visionModel, messages })) {
|
|
401
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
402
|
+
if (delta && typeof delta === 'string') {
|
|
403
|
+
reply += delta;
|
|
404
|
+
if (onChunk) onChunk(delta, reply);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} catch {
|
|
408
|
+
// fallback 非流式
|
|
409
|
+
const response = await this.provider.chat({ model: visionModel, messages });
|
|
410
|
+
const msg = response.choices[0]?.message?.content;
|
|
411
|
+
reply = typeof msg === 'string' ? msg : '';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!reply) reply = '抱歉,我无法理解这张图片。';
|
|
415
|
+
await this.saveToSession(sessionId, textContent, reply, sceneId);
|
|
416
|
+
return parseOutput(reply);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ── 增强人格(注入画像 + 情绪 hint) ────────────────────────────────
|
|
420
|
+
|
|
421
|
+
private buildEnhancedPersona(profileSummary: string, toneHint: string): string {
|
|
422
|
+
let persona = this.config.persona;
|
|
423
|
+
if (profileSummary) {
|
|
424
|
+
persona += `\n\n${profileSummary}`;
|
|
425
|
+
}
|
|
426
|
+
if (toneHint) {
|
|
427
|
+
persona += `\n\n[语气提示] ${toneHint}`;
|
|
428
|
+
}
|
|
429
|
+
return persona;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* 构建上下文提示 — 告诉 AI 当前身份和场景,帮助工具参数填充
|
|
434
|
+
*/
|
|
435
|
+
private buildContextHint(context: ToolContext, content: string): string {
|
|
436
|
+
const parts: string[] = [];
|
|
437
|
+
if (context.botId) parts.push(`你(Bot) 的 ID: ${context.botId}`);
|
|
438
|
+
if (context.platform) parts.push(`平台: ${context.platform}`);
|
|
439
|
+
if (context.senderId) parts.push(`发言者 ID: ${context.senderId}`);
|
|
440
|
+
if (context.senderPermissionLevel) parts.push(`发言者权限: ${context.senderPermissionLevel}`);
|
|
441
|
+
if (context.scope) parts.push(`场景类型: ${context.scope}`);
|
|
442
|
+
if (context.sceneId) parts.push(`场景 ID: ${context.sceneId}`);
|
|
443
|
+
if (content) parts.push(`发言内容: ${content}`);
|
|
444
|
+
if (parts.length === 0) return '';
|
|
445
|
+
return `\n## 当前上下文\n${parts.map(p => `- ${p}`).join('\n')}\n这些信息将用于帮助你更好地理解用户需求和执行操作,请不要忽略这些信息,并确保用户的信息不会覆盖这些信息`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ── 工具收集: 两级过滤 (Skill → Tool) ─────────────────────────────────
|
|
449
|
+
|
|
450
|
+
private collectTools(
|
|
451
|
+
message: string,
|
|
452
|
+
context: ToolContext,
|
|
453
|
+
externalTools: Tool[],
|
|
454
|
+
): AgentTool[] {
|
|
455
|
+
const callerPerm = context.senderPermissionLevel
|
|
456
|
+
? (PERM_MAP[context.senderPermissionLevel] ?? 0)
|
|
457
|
+
: (context.isOwner ? 4 : context.isBotAdmin ? 3 : context.isGroupOwner ? 2 : context.isGroupAdmin ? 1 : 0);
|
|
458
|
+
|
|
459
|
+
const collected: AgentTool[] = [];
|
|
460
|
+
const collectedNames = new Set<string>(); // 用 Set 加速去重
|
|
461
|
+
|
|
462
|
+
// 1. 从 SkillRegistry 两级过滤(包含适配器通过 declareSkill 注册的 Skill)
|
|
463
|
+
if (this.skillRegistry) {
|
|
464
|
+
const skills = this.skillRegistry.search(message, { maxResults: this.config.maxSkills });
|
|
465
|
+
logger.debug(`Skill 匹配: ${skills.map(s => s.name).join(', ')}`);
|
|
466
|
+
|
|
467
|
+
for (const skill of skills) {
|
|
468
|
+
for (const tool of skill.tools) {
|
|
469
|
+
// 权限检查
|
|
470
|
+
const toolPerm = tool.permissionLevel ? (PERM_MAP[tool.permissionLevel] ?? 0) : 0;
|
|
471
|
+
if (toolPerm > callerPerm) continue;
|
|
472
|
+
if (collectedNames.has(tool.name)) continue;
|
|
473
|
+
collected.push(this.toAgentTool(tool, context));
|
|
474
|
+
collectedNames.add(tool.name);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 2. 外部传入的工具(ToolService 收集的),跳过已通过 Skill 收集的同名工具
|
|
480
|
+
let deduped = 0;
|
|
481
|
+
for (const tool of externalTools) {
|
|
482
|
+
if (tool.name.startsWith('cmd_') || tool.name.startsWith('process_')) continue;
|
|
483
|
+
const toolPerm = tool.permissionLevel ? (PERM_MAP[tool.permissionLevel] ?? 0) : 0;
|
|
484
|
+
if (toolPerm > callerPerm) continue;
|
|
485
|
+
if (collectedNames.has(tool.name)) {
|
|
486
|
+
deduped++;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
collected.push(this.toAgentTool(tool, context));
|
|
490
|
+
collectedNames.add(tool.name);
|
|
491
|
+
}
|
|
492
|
+
if (deduped > 0) {
|
|
493
|
+
logger.debug(`externalTools 去重: 跳过 ${deduped} 个已由 Skill 提供的工具`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 3. 额外注册的工具
|
|
497
|
+
for (const tool of this.externalTools.values()) {
|
|
498
|
+
if (tool.permissionLevel != null && tool.permissionLevel > callerPerm) continue;
|
|
499
|
+
if (collectedNames.has(tool.name)) continue;
|
|
500
|
+
collected.push(tool);
|
|
501
|
+
collectedNames.add(tool.name);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 4. 用 Agent.filterTools 做最终相关性排序
|
|
505
|
+
return Agent.filterTools(message, collected, {
|
|
506
|
+
callerPermissionLevel: callerPerm,
|
|
507
|
+
maxTools: this.config.maxTools,
|
|
508
|
+
minScore: 0.1,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ── 辅助方法 ─────────────────────────────────────────────────────────
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 将 Tool 转为 AgentTool,注入 ToolContext 以确保执行时鉴权生效
|
|
516
|
+
*/
|
|
517
|
+
private toAgentTool(tool: Tool, context?: ToolContext): AgentTool {
|
|
518
|
+
const originalExecute = tool.execute;
|
|
519
|
+
const at: AgentTool = {
|
|
520
|
+
name: tool.name,
|
|
521
|
+
description: tool.description,
|
|
522
|
+
parameters: tool.parameters as any,
|
|
523
|
+
// 包装 execute,将 ToolContext 注入第二参数,确保工具内部的鉴权逻辑能正常执行
|
|
524
|
+
execute: context
|
|
525
|
+
? (args: Record<string, any>) => originalExecute(args, context)
|
|
526
|
+
: originalExecute,
|
|
527
|
+
};
|
|
528
|
+
if (tool.tags?.length) at.tags = tool.tags;
|
|
529
|
+
if (tool.keywords?.length) at.keywords = tool.keywords;
|
|
530
|
+
if (tool.permissionLevel) at.permissionLevel = PERM_MAP[tool.permissionLevel] ?? 0;
|
|
531
|
+
return at;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* 构建 Skill 增强的 system prompt(仅在工具路径使用,闲聊不走这里)
|
|
536
|
+
*/
|
|
537
|
+
private buildRichSystemPrompt(): string {
|
|
538
|
+
let prompt = this.config.persona;
|
|
539
|
+
if (this.skillRegistry && this.skillRegistry.size > 0) {
|
|
540
|
+
const skills = this.skillRegistry.getAll();
|
|
541
|
+
prompt += '\n\n## 我的能力\n';
|
|
542
|
+
for (const skill of skills) {
|
|
543
|
+
prompt += `- **${skill.name}**: ${skill.description}\n`;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return prompt;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ── 内置工具 ─────────────────────────────────────────────────────────
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* 创建 chat_history 工具 — 让 AI 能主动搜索历史聊天记录
|
|
553
|
+
*/
|
|
554
|
+
private createChatHistoryTool(sessionId: string): AgentTool {
|
|
555
|
+
const memory = this.memory;
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
name: 'chat_history',
|
|
559
|
+
description: '搜索与用户的历史聊天记录。可以按关键词搜索,也可以按对话轮次范围查询。当用户问到"之前聊过什么""我们讨论过什么"等回忆类问题时使用。',
|
|
560
|
+
parameters: {
|
|
561
|
+
type: 'object',
|
|
562
|
+
properties: {
|
|
563
|
+
keyword: {
|
|
564
|
+
type: 'string',
|
|
565
|
+
description: '搜索关键词(模糊匹配消息内容和摘要)',
|
|
566
|
+
},
|
|
567
|
+
from_round: {
|
|
568
|
+
type: 'number',
|
|
569
|
+
description: '起始轮次(与 to_round 配合使用,精确查询某段对话)',
|
|
570
|
+
},
|
|
571
|
+
to_round: {
|
|
572
|
+
type: 'number',
|
|
573
|
+
description: '结束轮次',
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
tags: ['memory', 'history', '聊天记录', '回忆', '之前'],
|
|
578
|
+
keywords: ['之前', '历史', '聊过', '讨论过', '记得', '上次', '以前', '回忆'],
|
|
579
|
+
async execute(args: Record<string, any>) {
|
|
580
|
+
const { keyword, from_round, to_round } = args;
|
|
581
|
+
|
|
582
|
+
// 获取当前轮次用于提示
|
|
583
|
+
const currentRound = await memory.getCurrentRound(sessionId);
|
|
584
|
+
|
|
585
|
+
if (keyword) {
|
|
586
|
+
const result = await memory.traceByKeyword(sessionId, keyword);
|
|
587
|
+
const msgs = result.messages.map(m => {
|
|
588
|
+
const role = m.role === 'user' ? '用户' : '助手';
|
|
589
|
+
const time = new Date(m.time).toLocaleString('zh-CN');
|
|
590
|
+
return `[第${m.round}轮 ${time}] ${role}: ${m.content}`;
|
|
591
|
+
}).join('\n');
|
|
592
|
+
|
|
593
|
+
let output = `当前是第 ${currentRound} 轮对话。\n\n`;
|
|
594
|
+
if (result.summary) {
|
|
595
|
+
output += `📋 找到相关摘要(覆盖第${result.summary.fromRound}-${result.summary.toRound}轮):\n${result.summary.summary}\n\n`;
|
|
596
|
+
}
|
|
597
|
+
output += msgs ? `💬 相关聊天记录:\n${msgs}` : '未找到包含该关键词的聊天记录。';
|
|
598
|
+
return output;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (from_round != null && to_round != null) {
|
|
602
|
+
const messages = await memory.getMessagesByRound(sessionId, from_round, to_round);
|
|
603
|
+
if (messages.length === 0) {
|
|
604
|
+
return `第 ${from_round}-${to_round} 轮没有聊天记录。当前是第 ${currentRound} 轮。`;
|
|
605
|
+
}
|
|
606
|
+
const msgs = messages.map(m => {
|
|
607
|
+
const role = m.role === 'user' ? '用户' : '助手';
|
|
608
|
+
const time = new Date(m.time).toLocaleString('zh-CN');
|
|
609
|
+
return `[第${m.round}轮 ${time}] ${role}: ${m.content}`;
|
|
610
|
+
}).join('\n');
|
|
611
|
+
return `第 ${from_round}-${to_round} 轮聊天记录(当前第 ${currentRound} 轮):\n${msgs}`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// 无参数 → 返回最近几轮
|
|
615
|
+
const messages = await memory.getMessagesByRound(
|
|
616
|
+
sessionId,
|
|
617
|
+
Math.max(1, currentRound - 4),
|
|
618
|
+
currentRound,
|
|
619
|
+
);
|
|
620
|
+
if (messages.length === 0) {
|
|
621
|
+
return '暂无聊天记录。';
|
|
622
|
+
}
|
|
623
|
+
const msgs = messages.map(m => {
|
|
624
|
+
const role = m.role === 'user' ? '用户' : '助手';
|
|
625
|
+
return `[第${m.round}轮] ${role}: ${m.content}`;
|
|
626
|
+
}).join('\n');
|
|
627
|
+
return `最近的聊天记录(当前第 ${currentRound} 轮):\n${msgs}`;
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* 创建 user_profile 工具 — 让 AI 读写用户画像
|
|
634
|
+
*/
|
|
635
|
+
private createUserProfileTool(userId: string): AgentTool {
|
|
636
|
+
const profiles = this.userProfiles;
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
name: 'user_profile',
|
|
640
|
+
description: '读取或保存用户的个人偏好和信息。当用户告诉你他的名字、偏好、兴趣、习惯等个人信息时,用 set 操作保存。当需要了解用户偏好时,用 get 操作读取。',
|
|
641
|
+
parameters: {
|
|
642
|
+
type: 'object',
|
|
643
|
+
properties: {
|
|
644
|
+
action: {
|
|
645
|
+
type: 'string',
|
|
646
|
+
description: '操作类型: get(读取所有偏好), set(保存偏好), delete(删除偏好)',
|
|
647
|
+
enum: ['get', 'set', 'delete'],
|
|
648
|
+
},
|
|
649
|
+
key: {
|
|
650
|
+
type: 'string',
|
|
651
|
+
description: '偏好名称,如: name, style, interests, timezone, language 等',
|
|
652
|
+
},
|
|
653
|
+
value: {
|
|
654
|
+
type: 'string',
|
|
655
|
+
description: '偏好值(仅 set 操作需要)',
|
|
656
|
+
},
|
|
657
|
+
},
|
|
658
|
+
required: ['action'],
|
|
659
|
+
},
|
|
660
|
+
tags: ['profile', '偏好', '用户', '个性化', '记住'],
|
|
661
|
+
keywords: ['我叫', '我的名字', '记住我', '我喜欢', '我偏好', '我习惯', '叫我', '我是'],
|
|
662
|
+
async execute(args: Record<string, any>) {
|
|
663
|
+
const { action, key, value } = args;
|
|
664
|
+
|
|
665
|
+
switch (action) {
|
|
666
|
+
case 'get': {
|
|
667
|
+
const all = await profiles.getAll(userId);
|
|
668
|
+
const entries = Object.entries(all);
|
|
669
|
+
if (entries.length === 0) return '暂无保存的用户偏好。';
|
|
670
|
+
return '用户偏好:\n' + entries.map(([k, v]) => ` ${k}: ${v}`).join('\n');
|
|
671
|
+
}
|
|
672
|
+
case 'set': {
|
|
673
|
+
if (!key || !value) return '需要提供 key 和 value';
|
|
674
|
+
await profiles.set(userId, key, value);
|
|
675
|
+
return `已保存: ${key} = ${value}`;
|
|
676
|
+
}
|
|
677
|
+
case 'delete': {
|
|
678
|
+
if (!key) return '需要提供 key';
|
|
679
|
+
const deleted = await profiles.delete(userId, key);
|
|
680
|
+
return deleted ? `已删除: ${key}` : `未找到偏好: ${key}`;
|
|
681
|
+
}
|
|
682
|
+
default:
|
|
683
|
+
return '不支持的操作,请使用 get/set/delete';
|
|
684
|
+
}
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* 创建 schedule_followup 工具 — 让 AI 主动安排跟进
|
|
691
|
+
*
|
|
692
|
+
* 任务持久化到数据库,机器人重启后自动恢复。
|
|
693
|
+
* 同一会话创建新提醒时,旧的 pending 提醒会被自动取消。
|
|
694
|
+
*/
|
|
695
|
+
private createScheduleFollowUpTool(sessionId: string, context: ToolContext): AgentTool {
|
|
696
|
+
const followUps = this.followUps;
|
|
697
|
+
const platform = context.platform || '';
|
|
698
|
+
const botId = context.botId || '';
|
|
699
|
+
const senderId = context.senderId || '';
|
|
700
|
+
const sceneId = context.sceneId || '';
|
|
701
|
+
const sceneType = (context.message as any)?.$channel?.type || 'private';
|
|
702
|
+
|
|
703
|
+
return {
|
|
704
|
+
name: 'schedule_followup',
|
|
705
|
+
description: '安排或取消定时跟进提醒。创建新提醒会自动取消之前的提醒。提醒持久保存,重启不丢失。',
|
|
706
|
+
parameters: {
|
|
707
|
+
type: 'object',
|
|
708
|
+
properties: {
|
|
709
|
+
action: {
|
|
710
|
+
type: 'string',
|
|
711
|
+
description: '操作类型: create(创建提醒,默认)或 cancel(取消当前会话所有提醒)',
|
|
712
|
+
enum: ['create', 'cancel'],
|
|
713
|
+
},
|
|
714
|
+
delay_minutes: {
|
|
715
|
+
type: 'number',
|
|
716
|
+
description: '延迟时间,单位是分钟。注意:3 就是 3 分钟,不是 3 小时。举例: 3 = 3分钟后, 60 = 1小时后, 1440 = 1天后',
|
|
717
|
+
},
|
|
718
|
+
message: {
|
|
719
|
+
type: 'string',
|
|
720
|
+
description: '提醒消息内容',
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
required: ['action'],
|
|
724
|
+
},
|
|
725
|
+
tags: ['reminder', '提醒', '跟进', '定时'],
|
|
726
|
+
keywords: ['提醒', '提醒我', '过一会', '过一小时', '明天', '跟进', '别忘了', '记得提醒', '取消提醒'],
|
|
727
|
+
async execute(args: Record<string, any>) {
|
|
728
|
+
const { action = 'create', delay_minutes, message: msg } = args;
|
|
729
|
+
|
|
730
|
+
if (action === 'cancel') {
|
|
731
|
+
const count = await followUps.cancelBySession(sessionId);
|
|
732
|
+
return count > 0
|
|
733
|
+
? `✅ 已取消 ${count} 个待执行的提醒`
|
|
734
|
+
: '当前没有待执行的提醒';
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// create
|
|
738
|
+
if (!delay_minutes || delay_minutes <= 0) return '延迟时间必须大于 0 分钟';
|
|
739
|
+
if (!msg) return '请提供提醒内容';
|
|
740
|
+
|
|
741
|
+
return followUps.schedule({
|
|
742
|
+
sessionId,
|
|
743
|
+
platform,
|
|
744
|
+
botId,
|
|
745
|
+
senderId,
|
|
746
|
+
sceneId,
|
|
747
|
+
sceneType,
|
|
748
|
+
message: msg,
|
|
749
|
+
delayMinutes: delay_minutes,
|
|
750
|
+
});
|
|
751
|
+
},
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ── 会话记忆(基于 ConversationMemory) ─────────────────────────────
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* 从 ConversationMemory 构建上下文
|
|
759
|
+
*/
|
|
760
|
+
private async buildHistoryMessages(sessionId: string): Promise<ChatMessage[]> {
|
|
761
|
+
return this.memory.buildContext(sessionId);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* 流式聊天(带历史记忆) — 利用 chatStream 减少 TTFT
|
|
766
|
+
*
|
|
767
|
+
* 新增 onChunk 回调:每收到一个 token 立即通知调用方,
|
|
768
|
+
* 支持适配器(Telegram/Discord/Kook)实时编辑消息。
|
|
769
|
+
*/
|
|
770
|
+
private async streamChatWithHistory(
|
|
771
|
+
content: string,
|
|
772
|
+
systemPrompt: string,
|
|
773
|
+
history: ChatMessage[],
|
|
774
|
+
onChunk?: OnChunkCallback,
|
|
775
|
+
): Promise<string> {
|
|
776
|
+
const model = this.provider.models[0];
|
|
777
|
+
const messages: ChatMessage[] = [
|
|
778
|
+
{ role: 'system', content: systemPrompt },
|
|
779
|
+
...history,
|
|
780
|
+
{ role: 'user', content },
|
|
781
|
+
];
|
|
782
|
+
|
|
783
|
+
// 优先流式(对 Ollama 等本地模型有明显提速)
|
|
784
|
+
try {
|
|
785
|
+
let result = '';
|
|
786
|
+
for await (const chunk of this.provider.chatStream({ model, messages })) {
|
|
787
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
788
|
+
if (delta && typeof delta === 'string') {
|
|
789
|
+
result += delta;
|
|
790
|
+
if (onChunk) onChunk(delta, result);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return result;
|
|
794
|
+
} catch {
|
|
795
|
+
// fallback 非流式
|
|
796
|
+
const response = await this.provider.chat({ model, messages });
|
|
797
|
+
const msg = response.choices[0]?.message?.content;
|
|
798
|
+
const result = typeof msg === 'string' ? msg : '';
|
|
799
|
+
if (onChunk && result) onChunk(result, result);
|
|
800
|
+
return result;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
private async saveToSession(
|
|
805
|
+
sessionId: string,
|
|
806
|
+
userContent: string,
|
|
807
|
+
assistantContent: string,
|
|
808
|
+
sceneId?: string,
|
|
809
|
+
): Promise<void> {
|
|
810
|
+
// 1. 保存到 ConversationMemory(含异步摘要判断)
|
|
811
|
+
await this.memory.saveRound(sessionId, userContent, assistantContent);
|
|
812
|
+
|
|
813
|
+
// 2. 保存到 SessionManager(兼容旧逻辑)
|
|
814
|
+
await this.sessions.addMessage(sessionId, { role: 'user', content: userContent });
|
|
815
|
+
await this.sessions.addMessage(sessionId, { role: 'assistant', content: assistantContent });
|
|
816
|
+
|
|
817
|
+
// 3. ContextManager 场景摘要(如有)
|
|
818
|
+
if (this.contextManager && sceneId) {
|
|
819
|
+
this.contextManager.autoSummarizeIfNeeded(sceneId).catch(() => {});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private fallbackFormat(toolCalls: { tool: string; args: any; result: any }[]): string {
|
|
824
|
+
if (toolCalls.length === 0) return '处理完成。';
|
|
825
|
+
return toolCalls.map(tc => {
|
|
826
|
+
const s = typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result, null, 2);
|
|
827
|
+
return `【${tc.tool}】\n${s}`;
|
|
828
|
+
}).join('\n\n');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// ── 生命周期 ─────────────────────────────────────────────────────────
|
|
832
|
+
|
|
833
|
+
isReady(): boolean {
|
|
834
|
+
return true; // provider is required in constructor
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
dispose(): void {
|
|
838
|
+
this.memory.dispose();
|
|
839
|
+
this.sessions.dispose();
|
|
840
|
+
this.externalTools.clear();
|
|
841
|
+
this.userProfiles.dispose();
|
|
842
|
+
this.rateLimiter.dispose();
|
|
843
|
+
this.followUps.dispose();
|
|
844
|
+
}
|
|
845
|
+
}
|