@zhin.js/agent 0.1.0 → 0.1.3
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/lib/cron-engine.d.ts +20 -2
- package/lib/cron-engine.d.ts.map +1 -1
- package/lib/cron-engine.js +59 -18
- package/lib/cron-engine.js.map +1 -1
- package/lib/discover-skills.d.ts +3 -1
- package/lib/discover-skills.d.ts.map +1 -1
- package/lib/discover-skills.js +7 -9
- package/lib/discover-skills.js.map +1 -1
- package/lib/discover-tools.d.ts +1 -6
- package/lib/discover-tools.d.ts.map +1 -1
- package/lib/discover-tools.js +2 -6
- package/lib/discover-tools.js.map +1 -1
- package/lib/index.d.ts +2 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -2
- package/lib/index.js.map +1 -1
- package/lib/init/create-zhin-agent.d.ts.map +1 -1
- package/lib/init/create-zhin-agent.js +42 -20
- package/lib/init/create-zhin-agent.js.map +1 -1
- package/lib/init/register-ai-trigger.d.ts.map +1 -1
- package/lib/init/register-ai-trigger.js +10 -3
- package/lib/init/register-ai-trigger.js.map +1 -1
- package/lib/init/register-builtin-tools.d.ts.map +1 -1
- package/lib/init/register-builtin-tools.js +85 -15
- package/lib/init/register-builtin-tools.js.map +1 -1
- package/lib/init/register-db-models.d.ts.map +1 -1
- package/lib/init/register-db-models.js +1 -3
- package/lib/init/register-db-models.js.map +1 -1
- package/lib/init/register-db-upgrade.d.ts.map +1 -1
- package/lib/init/register-db-upgrade.js +1 -8
- package/lib/init/register-db-upgrade.js.map +1 -1
- package/lib/init/register-management-tools.d.ts.map +1 -1
- package/lib/init/register-management-tools.js +33 -20
- package/lib/init/register-management-tools.js.map +1 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +0 -8
- package/lib/service.js.map +1 -1
- package/lib/zhin-agent/builtin-tools.d.ts +0 -2
- package/lib/zhin-agent/builtin-tools.d.ts.map +1 -1
- package/lib/zhin-agent/builtin-tools.js +0 -55
- package/lib/zhin-agent/builtin-tools.js.map +1 -1
- package/lib/zhin-agent/config.d.ts +2 -1
- package/lib/zhin-agent/config.d.ts.map +1 -1
- package/lib/zhin-agent/config.js +1 -1
- package/lib/zhin-agent/config.js.map +1 -1
- package/lib/zhin-agent/index.d.ts +1 -6
- package/lib/zhin-agent/index.d.ts.map +1 -1
- package/lib/zhin-agent/index.js +26 -34
- package/lib/zhin-agent/index.js.map +1 -1
- package/lib/zhin-agent/prompt.d.ts.map +1 -1
- package/lib/zhin-agent/prompt.js +31 -76
- package/lib/zhin-agent/prompt.js.map +1 -1
- package/lib/zhin-agent/tool-collector.d.ts.map +1 -1
- package/lib/zhin-agent/tool-collector.js +7 -7
- package/lib/zhin-agent/tool-collector.js.map +1 -1
- package/package.json +7 -4
- package/CHANGELOG.md +0 -190
- package/lib/follow-up.d.ts +0 -131
- package/lib/follow-up.d.ts.map +0 -1
- package/lib/follow-up.js +0 -265
- package/lib/follow-up.js.map +0 -1
- package/src/agent.ts +0 -6
- package/src/bootstrap.ts +0 -309
- package/src/builtin-tools.ts +0 -958
- package/src/compaction.ts +0 -28
- package/src/context-manager.ts +0 -15
- package/src/conversation-memory.ts +0 -5
- package/src/cron-engine.ts +0 -338
- package/src/discover-agents.ts +0 -138
- package/src/discover-skills.ts +0 -325
- package/src/discover-tools.ts +0 -302
- package/src/discovery-utils.ts +0 -96
- package/src/file-policy.ts +0 -333
- package/src/follow-up.ts +0 -357
- package/src/hooks.ts +0 -223
- package/src/index.ts +0 -183
- package/src/init/create-zhin-agent.ts +0 -161
- package/src/init/register-ai-service.ts +0 -53
- package/src/init/register-ai-trigger.ts +0 -253
- package/src/init/register-builtin-tools.ts +0 -308
- package/src/init/register-db-models.ts +0 -31
- package/src/init/register-db-upgrade.ts +0 -77
- package/src/init/register-management-tools.ts +0 -71
- package/src/init/register-message-recorder.ts +0 -31
- package/src/init/register-tool-service.ts +0 -9
- package/src/init/shared-refs.ts +0 -20
- package/src/init/types.ts +0 -18
- package/src/init.ts +0 -50
- package/src/output.ts +0 -15
- package/src/rate-limiter.ts +0 -5
- package/src/service.ts +0 -228
- package/src/session.ts +0 -13
- package/src/storage.ts +0 -9
- package/src/subagent.ts +0 -209
- package/src/tone-detector.ts +0 -5
- package/src/tools.ts +0 -214
- package/src/user-profile.ts +0 -182
- package/src/zhin-agent/builtin-tools.ts +0 -247
- package/src/zhin-agent/config.ts +0 -124
- package/src/zhin-agent/exec-policy.ts +0 -285
- package/src/zhin-agent/index.ts +0 -633
- package/src/zhin-agent/prompt.ts +0 -305
- package/src/zhin-agent/tool-collector.ts +0 -249
- package/tests/ai/follow-up.test.ts +0 -175
- package/tests/ai/integration.test.ts +0 -582
- package/tests/ai/multimodal.test.ts +0 -106
- package/tests/ai/setup.ts +0 -186
- package/tests/ai/subagent.test.ts +0 -270
- package/tests/ai/tools-builtin.test.ts +0 -310
- package/tests/ai/user-profile.test.ts +0 -73
- package/tests/ai/zhin-agent.test.ts +0 -306
- package/tests/exec-policy.test.ts +0 -355
- package/tests/file-policy.test.ts +0 -405
- package/tsconfig.json +0 -22
package/src/zhin-agent/index.ts
DELETED
|
@@ -1,633 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ZhinAgent — 全局持久 AI 大脑
|
|
3
|
-
*
|
|
4
|
-
* 核心能力:
|
|
5
|
-
* 1. 全局单例,应用生命周期内常驻
|
|
6
|
-
* 2. Skill 感知:两级过滤 Skill → Tool
|
|
7
|
-
* 3. 双层记忆:per-scene(对话上下文)+ per-user(长期偏好)
|
|
8
|
-
* 4. 任务规划:复杂请求自动分解为子步骤
|
|
9
|
-
* 5. 多模态输出:结构化 OutputElement[]
|
|
10
|
-
* 6. 智能路径选择:纯闲聊走轻量路径,工具请求走完整路径
|
|
11
|
-
* 7. 用户画像:跨会话个性化记忆
|
|
12
|
-
* 8. 速率限制:防止单用户过度消耗资源
|
|
13
|
-
* 9. 流式输出:onChunk 回调实时推送部分文本
|
|
14
|
-
* 10. 情绪感知:根据用户语气调整回复风格
|
|
15
|
-
* 11. 主动跟进:schedule_followup 定时回查
|
|
16
|
-
* 12. 多模态输入:图片/音频直接传给视觉模型
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { Logger } from '@zhin.js/core';
|
|
20
|
-
import type { Tool, ToolContext, SkillFeature, AIProvider, AgentTool, ChatMessage, ContentPart } from '@zhin.js/core';
|
|
21
|
-
import { createAgent } from '@zhin.js/ai';
|
|
22
|
-
import { SessionManager, createMemorySessionManager } from '@zhin.js/ai';
|
|
23
|
-
import type { ContextManager } from '@zhin.js/ai';
|
|
24
|
-
import { ConversationMemory } from '@zhin.js/ai';
|
|
25
|
-
import type { OutputElement } from '@zhin.js/ai';
|
|
26
|
-
import { parseOutput } from '@zhin.js/ai';
|
|
27
|
-
import type { ModelRegistry } from '@zhin.js/ai';
|
|
28
|
-
import { UserProfileStore } from '../user-profile.js';
|
|
29
|
-
import { RateLimiter } from '@zhin.js/ai';
|
|
30
|
-
import { detectTone } from '@zhin.js/ai';
|
|
31
|
-
import { FollowUpManager, type FollowUpSender } from '../follow-up.js';
|
|
32
|
-
import { SubagentManager, type SubagentResultSender } from '../subagent.js';
|
|
33
|
-
import {
|
|
34
|
-
pruneHistoryForContext,
|
|
35
|
-
DEFAULT_CONTEXT_TOKENS,
|
|
36
|
-
} from '@zhin.js/ai';
|
|
37
|
-
import { triggerAIHook, createAIHookEvent } from '../hooks.js';
|
|
38
|
-
|
|
39
|
-
// ── Sub-modules ─────────────────────────────────────────────────────
|
|
40
|
-
import {
|
|
41
|
-
type ZhinAgentConfig,
|
|
42
|
-
type OnChunkCallback,
|
|
43
|
-
DEFAULT_CONFIG,
|
|
44
|
-
KEYWORD_TRIGGERS,
|
|
45
|
-
} from './config.js';
|
|
46
|
-
import { applyExecPolicyToTools } from './exec-policy.js';
|
|
47
|
-
import { collectRelevantTools } from './tool-collector.js';
|
|
48
|
-
import {
|
|
49
|
-
buildEnhancedPersona,
|
|
50
|
-
buildContextHint,
|
|
51
|
-
buildRichSystemPrompt,
|
|
52
|
-
buildUserMessageWithHistory,
|
|
53
|
-
} from './prompt.js';
|
|
54
|
-
import {
|
|
55
|
-
createChatHistoryTool,
|
|
56
|
-
createUserProfileTool,
|
|
57
|
-
createScheduleFollowUpTool,
|
|
58
|
-
createSpawnTaskTool,
|
|
59
|
-
} from './builtin-tools.js';
|
|
60
|
-
|
|
61
|
-
// Re-export public types for backward compat
|
|
62
|
-
export type { ZhinAgentConfig, OnChunkCallback } from './config.js';
|
|
63
|
-
|
|
64
|
-
const logger = new Logger(null, 'ZhinAgent');
|
|
65
|
-
const now = () => performance.now();
|
|
66
|
-
|
|
67
|
-
/** Strip `<think>…</think>` blocks that some reasoning models embed in content. */
|
|
68
|
-
function stripThinkBlocks(text: string): string {
|
|
69
|
-
return text.replace(/<think>[\s\S]*?<\/think>\s*/g, '').trim();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ============================================================================
|
|
73
|
-
// ZhinAgent
|
|
74
|
-
// ============================================================================
|
|
75
|
-
|
|
76
|
-
export class ZhinAgent {
|
|
77
|
-
private provider: AIProvider;
|
|
78
|
-
private config: Required<ZhinAgentConfig>;
|
|
79
|
-
private skillRegistry: SkillFeature | null = null;
|
|
80
|
-
private sessions: SessionManager;
|
|
81
|
-
private contextManager: ContextManager | null = null;
|
|
82
|
-
private memory: ConversationMemory;
|
|
83
|
-
private externalTools: Map<string, AgentTool> = new Map();
|
|
84
|
-
private userProfiles: UserProfileStore;
|
|
85
|
-
private rateLimiter: RateLimiter;
|
|
86
|
-
private followUps: FollowUpManager;
|
|
87
|
-
private subagentManager: SubagentManager | null = null;
|
|
88
|
-
private bootstrapContext: string = '';
|
|
89
|
-
private activeSkillsContext: string = '';
|
|
90
|
-
private skillsSummaryXML: string = '';
|
|
91
|
-
private modelRegistry: ModelRegistry | null = null;
|
|
92
|
-
|
|
93
|
-
constructor(provider: AIProvider, config?: ZhinAgentConfig) {
|
|
94
|
-
this.provider = provider;
|
|
95
|
-
this.config = { ...DEFAULT_CONFIG, ...config } as Required<ZhinAgentConfig>;
|
|
96
|
-
this.sessions = createMemorySessionManager();
|
|
97
|
-
this.memory = new ConversationMemory({
|
|
98
|
-
minTopicRounds: this.config.minTopicRounds,
|
|
99
|
-
slidingWindowSize: this.config.slidingWindowSize,
|
|
100
|
-
topicChangeThreshold: this.config.topicChangeThreshold,
|
|
101
|
-
});
|
|
102
|
-
this.memory.setProvider(provider);
|
|
103
|
-
this.userProfiles = new UserProfileStore();
|
|
104
|
-
this.rateLimiter = new RateLimiter(this.config.rateLimit);
|
|
105
|
-
this.followUps = new FollowUpManager();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ── DI setters ──────────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
setSkillRegistry(registry: SkillFeature): void {
|
|
111
|
-
this.skillRegistry = registry;
|
|
112
|
-
logger.debug(`SkillRegistry connected (${registry.size} skills)`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
setSessionManager(manager: SessionManager): void {
|
|
116
|
-
this.sessions.dispose();
|
|
117
|
-
this.sessions = manager;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
setContextManager(manager: ContextManager): void {
|
|
121
|
-
this.contextManager = manager;
|
|
122
|
-
manager.setAIProvider(this.provider);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
setModelRegistry(registry: ModelRegistry): void {
|
|
126
|
-
this.modelRegistry = registry;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
upgradeMemoryToDatabase(msgModel: any, sumModel: any): void {
|
|
130
|
-
this.memory.upgradeToDatabase(msgModel, sumModel);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
upgradeProfilesToDatabase(model: any): void {
|
|
134
|
-
this.userProfiles.upgradeToDatabase(model);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
upgradeFollowUpsToDatabase(model: any): void {
|
|
138
|
-
this.followUps.upgradeToDatabase(model);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
setFollowUpSender(sender: FollowUpSender): void {
|
|
142
|
-
this.followUps.setSender(sender);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async restoreFollowUps(): Promise<number> {
|
|
146
|
-
return this.followUps.restore();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
initSubagentManager(createTools: () => AgentTool[]): void {
|
|
150
|
-
this.subagentManager = new SubagentManager({
|
|
151
|
-
provider: this.provider,
|
|
152
|
-
workspace: process.cwd(),
|
|
153
|
-
createTools,
|
|
154
|
-
maxIterations: this.config.maxSubagentIterations,
|
|
155
|
-
execPolicyConfig: this.config,
|
|
156
|
-
});
|
|
157
|
-
logger.debug('SubagentManager initialized');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
setSubagentSender(sender: SubagentResultSender): void {
|
|
161
|
-
if (this.subagentManager) {
|
|
162
|
-
this.subagentManager.setSender(sender);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
getSubagentManager(): SubagentManager | null {
|
|
167
|
-
return this.subagentManager;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
getUserProfiles(): UserProfileStore {
|
|
171
|
-
return this.userProfiles;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** 根据任务类型选择最合适的模型,优先使用 config 显式指定,其次 ModelRegistry,回退到 provider.models[0] */
|
|
175
|
-
private resolveModel(task: 'chat' | 'vision' | 'tool_call' | 'summary' = 'chat', preferred?: string): string {
|
|
176
|
-
return this.resolveModelCandidates(task, preferred)[0];
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* 返回按优先级排序的候选模型列表(用于自动降级)。
|
|
181
|
-
* 第一个是最优选择,失败后依次尝试后续模型。
|
|
182
|
-
*/
|
|
183
|
-
private resolveModelCandidates(task: 'chat' | 'vision' | 'tool_call' | 'summary' = 'chat', preferred?: string): string[] {
|
|
184
|
-
const candidates: string[] = [];
|
|
185
|
-
|
|
186
|
-
// 1. 用户显式指定 / 配置指定优先
|
|
187
|
-
if (preferred) candidates.push(preferred);
|
|
188
|
-
if (task === 'chat' && this.config.chatModel && !candidates.includes(this.config.chatModel)) {
|
|
189
|
-
candidates.push(this.config.chatModel);
|
|
190
|
-
}
|
|
191
|
-
if (task === 'vision' && this.config.visionModel && !candidates.includes(this.config.visionModel)) {
|
|
192
|
-
candidates.push(this.config.visionModel);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// 2. ModelRegistry 自动排序的候选列表
|
|
196
|
-
if (this.modelRegistry) {
|
|
197
|
-
for (const id of this.modelRegistry.selectModels(this.provider.name, task, 5)) {
|
|
198
|
-
if (!candidates.includes(id)) candidates.push(id);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// 3. 兜底: provider.models[0]
|
|
203
|
-
const fallback = this.provider.models[0];
|
|
204
|
-
if (fallback && !candidates.includes(fallback)) candidates.push(fallback);
|
|
205
|
-
|
|
206
|
-
return candidates;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
registerTool(tool: AgentTool): () => void {
|
|
210
|
-
this.externalTools.set(tool.name, tool);
|
|
211
|
-
return () => { this.externalTools.delete(tool.name); };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
setBootstrapContext(context: string): void {
|
|
215
|
-
this.bootstrapContext = context;
|
|
216
|
-
logger.debug(`Bootstrap context set (${context.length} chars)`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
setActiveSkillsContext(content: string): void {
|
|
220
|
-
this.activeSkillsContext = content || '';
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
setSkillsSummaryXML(xml: string): void {
|
|
224
|
-
this.skillsSummaryXML = xml || '';
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ── Core processing ─────────────────────────────────────────────────
|
|
228
|
-
|
|
229
|
-
async process(
|
|
230
|
-
content: string,
|
|
231
|
-
context: ToolContext,
|
|
232
|
-
externalTools: Tool[] = [],
|
|
233
|
-
onChunk?: OnChunkCallback,
|
|
234
|
-
): Promise<OutputElement[]> {
|
|
235
|
-
const t0 = now();
|
|
236
|
-
const { senderId, sceneId, platform } = context;
|
|
237
|
-
const sessionId = SessionManager.generateId(platform || '', senderId || '', sceneId);
|
|
238
|
-
const userId = senderId || 'unknown';
|
|
239
|
-
|
|
240
|
-
// 0. Rate limit
|
|
241
|
-
const rateCheck = this.rateLimiter.check(userId);
|
|
242
|
-
if (!rateCheck.allowed) {
|
|
243
|
-
logger.debug(`[速率限制] 用户 ${userId} 被限制: ${rateCheck.message}`);
|
|
244
|
-
return parseOutput(rateCheck.message || '请稍后再试');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
triggerAIHook(createAIHookEvent('message', 'received', sessionId, {
|
|
248
|
-
userId,
|
|
249
|
-
content,
|
|
250
|
-
platform: platform || '',
|
|
251
|
-
})).catch(() => {});
|
|
252
|
-
|
|
253
|
-
// 1. Collect tools
|
|
254
|
-
const tFilter = now();
|
|
255
|
-
const allTools = collectRelevantTools(content, context, externalTools, {
|
|
256
|
-
config: this.config,
|
|
257
|
-
skillRegistry: this.skillRegistry,
|
|
258
|
-
externalRegistered: this.externalTools,
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// Inject context-aware built-in tools on keyword match
|
|
262
|
-
if (KEYWORD_TRIGGERS.chatHistory.test(content)) {
|
|
263
|
-
allTools.push(createChatHistoryTool(sessionId, this.memory));
|
|
264
|
-
}
|
|
265
|
-
if (KEYWORD_TRIGGERS.userProfile.test(content)) {
|
|
266
|
-
allTools.push(createUserProfileTool(userId, this.userProfiles));
|
|
267
|
-
}
|
|
268
|
-
if (KEYWORD_TRIGGERS.scheduleFollowUp.test(content)) {
|
|
269
|
-
allTools.push(createScheduleFollowUpTool(sessionId, context, this.followUps));
|
|
270
|
-
}
|
|
271
|
-
if (this.subagentManager && KEYWORD_TRIGGERS.spawnTask.test(content)) {
|
|
272
|
-
allTools.push(createSpawnTaskTool(context, this.subagentManager));
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const filterMs = (now() - tFilter).toFixed(0);
|
|
276
|
-
|
|
277
|
-
// 2. History + profile
|
|
278
|
-
const tMem = now();
|
|
279
|
-
let historyMessages = await this.buildHistoryMessages(sessionId);
|
|
280
|
-
|
|
281
|
-
const contextTokens = this.config.contextTokens ?? DEFAULT_CONTEXT_TOKENS;
|
|
282
|
-
const maxHistoryShare = this.config.maxHistoryShare ?? 0.5;
|
|
283
|
-
const pruneResult = pruneHistoryForContext({
|
|
284
|
-
messages: historyMessages,
|
|
285
|
-
maxContextTokens: contextTokens,
|
|
286
|
-
maxHistoryShare,
|
|
287
|
-
});
|
|
288
|
-
historyMessages = pruneResult.messages;
|
|
289
|
-
if (pruneResult.droppedCount > 0) {
|
|
290
|
-
logger.debug(`[上下文窗口] 丢弃 ${pruneResult.droppedCount} 条历史消息 (${pruneResult.droppedTokens} tokens)`);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const memMs = (now() - tMem).toFixed(0);
|
|
294
|
-
|
|
295
|
-
// 2.5 Profile + tone
|
|
296
|
-
const profileSummary = await this.userProfiles.buildProfileSummary(userId);
|
|
297
|
-
const toneHint = this.config.toneAwareness ? detectTone(content).hint : '';
|
|
298
|
-
const personaEnhanced = buildEnhancedPersona(this.config, profileSummary, toneHint);
|
|
299
|
-
|
|
300
|
-
// 3. No tools → chat path
|
|
301
|
-
if (allTools.length === 0) {
|
|
302
|
-
logger.info(`[System Prompt] chat-path: ${personaEnhanced.length} chars ≈ ${Math.ceil(personaEnhanced.length / 2.5)} tokens`);
|
|
303
|
-
logger.debug(`[闲聊路径] 过滤=${filterMs}ms, 记忆=${memMs}ms (${historyMessages.length}条), 0 工具`);
|
|
304
|
-
const tLLM = now();
|
|
305
|
-
const reply = await this.streamChatWithHistory(content, personaEnhanced, historyMessages, onChunk);
|
|
306
|
-
const llmMs = (now() - tLLM).toFixed(0);
|
|
307
|
-
logger.info(`[闲聊路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, LLM=${llmMs}ms, 总=${(now() - t0).toFixed(0)}ms`);
|
|
308
|
-
await this.saveToSession(sessionId, content, reply, sceneId);
|
|
309
|
-
return parseOutput(reply);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
logger.debug(`[工具路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, ${allTools.length} 工具 (${allTools.map(t => t.name).join(', ')})`);
|
|
313
|
-
|
|
314
|
-
// 4. Pre-executable tools
|
|
315
|
-
const preExecTools: AgentTool[] = [];
|
|
316
|
-
for (const tool of allTools) {
|
|
317
|
-
if (tool.preExecutable) preExecTools.push(tool);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// 5. Pre-execution
|
|
321
|
-
let preData = '';
|
|
322
|
-
if (preExecTools.length > 0) {
|
|
323
|
-
const tPre = now();
|
|
324
|
-
logger.debug(`预执行: ${preExecTools.map(t => t.name).join(', ')}`);
|
|
325
|
-
const results = await Promise.allSettled(
|
|
326
|
-
preExecTools.map(async (tool) => {
|
|
327
|
-
const result = await Promise.race([
|
|
328
|
-
tool.execute({}),
|
|
329
|
-
new Promise<never>((_, rej) =>
|
|
330
|
-
setTimeout(() => rej(new Error('超时')), this.config.preExecTimeout)),
|
|
331
|
-
]);
|
|
332
|
-
return { name: tool.name, result };
|
|
333
|
-
}),
|
|
334
|
-
);
|
|
335
|
-
for (const r of results) {
|
|
336
|
-
if (r.status === 'fulfilled') {
|
|
337
|
-
let s = typeof r.value.result === 'string' ? r.value.result : JSON.stringify(r.value.result);
|
|
338
|
-
if (s.length > 500) {
|
|
339
|
-
s = s.slice(0, 500) + `\n... (truncated, ${s.length} chars total)`;
|
|
340
|
-
}
|
|
341
|
-
preData += `\n【${r.value.name}】${s}`;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
logger.debug(`预执行耗时: ${(now() - tPre).toFixed(0)}ms`);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// 6. Path selection
|
|
348
|
-
let reply: string;
|
|
349
|
-
const hasNonPreExecTools = allTools.some(t => !t.preExecutable);
|
|
350
|
-
|
|
351
|
-
if (!hasNonPreExecTools && preData) {
|
|
352
|
-
// Fast path
|
|
353
|
-
const tLLM = now();
|
|
354
|
-
const prompt = `${personaEnhanced}
|
|
355
|
-
|
|
356
|
-
Pre-fetched data (from user's question):
|
|
357
|
-
${preData}
|
|
358
|
-
|
|
359
|
-
Answer the user's question based on the data above. Be clear and concise; use emoji when appropriate.`;
|
|
360
|
-
logger.info(`[System Prompt] fast-path: ${prompt.length} chars ≈ ${Math.ceil(prompt.length / 2.5)} tokens`);
|
|
361
|
-
reply = await this.streamChatWithHistory(content, prompt, historyMessages, onChunk);
|
|
362
|
-
logger.info(`[快速路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, LLM=${(now() - tLLM).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
|
|
363
|
-
} else {
|
|
364
|
-
// Agent path
|
|
365
|
-
const tAgent = now();
|
|
366
|
-
logger.debug(`Agent 路径: ${allTools.length} 个工具`);
|
|
367
|
-
const contextHint = buildContextHint(context, content);
|
|
368
|
-
|
|
369
|
-
const richPrompt = buildRichSystemPrompt({
|
|
370
|
-
config: this.config,
|
|
371
|
-
skillRegistry: this.skillRegistry,
|
|
372
|
-
skillsSummaryXML: this.skillsSummaryXML,
|
|
373
|
-
activeSkillsContext: this.activeSkillsContext,
|
|
374
|
-
bootstrapContext: this.bootstrapContext,
|
|
375
|
-
});
|
|
376
|
-
const systemPrompt = `${richPrompt}
|
|
377
|
-
${contextHint}
|
|
378
|
-
${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
|
|
379
|
-
|
|
380
|
-
const promptChars = systemPrompt.length;
|
|
381
|
-
const estimatedTokens = Math.ceil(promptChars / 2.5);
|
|
382
|
-
logger.info(`[System Prompt] ${promptChars} chars ≈ ${estimatedTokens} tokens`);
|
|
383
|
-
logger.debug(`[System Prompt Preview]\n${systemPrompt.slice(0, 500)}...\n---END PREVIEW---`);
|
|
384
|
-
|
|
385
|
-
const agentTools = applyExecPolicyToTools(this.config, allTools);
|
|
386
|
-
|
|
387
|
-
// Adaptive maxIterations: boost when skills are active (multi-step skill flows)
|
|
388
|
-
const SKILL_ITERATION_BOOST = 3;
|
|
389
|
-
const hasSkillActivation = agentTools.some(t => t.name === 'activate_skill' || t.name === 'install_skill');
|
|
390
|
-
const effectiveMaxIterations = hasSkillActivation
|
|
391
|
-
? this.config.maxIterations + SKILL_ITERATION_BOOST
|
|
392
|
-
: this.config.maxIterations;
|
|
393
|
-
|
|
394
|
-
const chatCandidates = this.resolveModelCandidates('chat');
|
|
395
|
-
const agent = createAgent(this.provider, {
|
|
396
|
-
model: chatCandidates[0],
|
|
397
|
-
modelFallbacks: chatCandidates.slice(1),
|
|
398
|
-
systemPrompt,
|
|
399
|
-
tools: agentTools,
|
|
400
|
-
maxIterations: effectiveMaxIterations,
|
|
401
|
-
});
|
|
402
|
-
|
|
403
|
-
const userMessageWithHistory = buildUserMessageWithHistory(historyMessages, content);
|
|
404
|
-
const result = await agent.run(userMessageWithHistory, []);
|
|
405
|
-
reply = stripThinkBlocks(result.content) || this.fallbackFormat(result.toolCalls);
|
|
406
|
-
logger.info(`[Agent 路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, Agent=${(now() - tAgent).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
await this.saveToSession(sessionId, content, reply, sceneId);
|
|
410
|
-
|
|
411
|
-
triggerAIHook(createAIHookEvent('message', 'sent', sessionId, {
|
|
412
|
-
userId,
|
|
413
|
-
content: reply,
|
|
414
|
-
platform: platform || '',
|
|
415
|
-
})).catch(() => {});
|
|
416
|
-
|
|
417
|
-
return parseOutput(reply);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
async processMultimodal(
|
|
421
|
-
parts: ContentPart[],
|
|
422
|
-
context: ToolContext,
|
|
423
|
-
onChunk?: OnChunkCallback,
|
|
424
|
-
): Promise<OutputElement[]> {
|
|
425
|
-
const { senderId, sceneId, platform } = context;
|
|
426
|
-
const sessionId = SessionManager.generateId(platform || '', senderId || '', sceneId);
|
|
427
|
-
const userId = senderId || 'unknown';
|
|
428
|
-
|
|
429
|
-
const rateCheck = this.rateLimiter.check(userId);
|
|
430
|
-
if (!rateCheck.allowed) {
|
|
431
|
-
return parseOutput(rateCheck.message || '请稍后再试');
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const historyMessages = await this.buildHistoryMessages(sessionId);
|
|
435
|
-
const profileSummary = await this.userProfiles.buildProfileSummary(userId);
|
|
436
|
-
const personaEnhanced = buildEnhancedPersona(this.config, profileSummary, '');
|
|
437
|
-
|
|
438
|
-
// Build text summary describing the multimodal content
|
|
439
|
-
const textFragments: string[] = [];
|
|
440
|
-
const llmParts: ContentPart[] = [];
|
|
441
|
-
|
|
442
|
-
/** Full multimodal ContentPart union (core/ai may export a narrower type in some builds) */
|
|
443
|
-
type MultimodalPart =
|
|
444
|
-
| ContentPart
|
|
445
|
-
| { type: 'video_url'; video_url: { url: string } }
|
|
446
|
-
| { type: 'face'; face: { id: string; text?: string } };
|
|
447
|
-
|
|
448
|
-
for (const p of parts as MultimodalPart[]) {
|
|
449
|
-
switch (p.type) {
|
|
450
|
-
case 'text':
|
|
451
|
-
textFragments.push(p.text);
|
|
452
|
-
llmParts.push(p);
|
|
453
|
-
break;
|
|
454
|
-
case 'image_url':
|
|
455
|
-
textFragments.push('[图片]');
|
|
456
|
-
llmParts.push(p);
|
|
457
|
-
break;
|
|
458
|
-
case 'video_url':
|
|
459
|
-
textFragments.push('[视频]');
|
|
460
|
-
// Most LLMs don't support video natively; describe it as a URL for context
|
|
461
|
-
llmParts.push({ type: 'text', text: `[用户发送了一个视频: ${p.video_url.url}]` });
|
|
462
|
-
break;
|
|
463
|
-
case 'audio':
|
|
464
|
-
textFragments.push('[音频]');
|
|
465
|
-
llmParts.push(p);
|
|
466
|
-
break;
|
|
467
|
-
case 'face':
|
|
468
|
-
textFragments.push(p.face.text || `[表情:${p.face.id}]`);
|
|
469
|
-
llmParts.push({ type: 'text', text: p.face.text ? `[表情: ${p.face.text}]` : `[表情ID: ${p.face.id}]` });
|
|
470
|
-
break;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const textContent = textFragments.join(' ') || '[多模态消息]';
|
|
475
|
-
const visionCandidates = this.resolveModelCandidates('vision');
|
|
476
|
-
|
|
477
|
-
const messages: ChatMessage[] = [
|
|
478
|
-
{ role: 'system', content: personaEnhanced },
|
|
479
|
-
...historyMessages,
|
|
480
|
-
{ role: 'user', content: llmParts },
|
|
481
|
-
];
|
|
482
|
-
|
|
483
|
-
let reply = '';
|
|
484
|
-
for (let i = 0; i < visionCandidates.length; i++) {
|
|
485
|
-
const visionModel = visionCandidates[i];
|
|
486
|
-
try {
|
|
487
|
-
reply = '';
|
|
488
|
-
for await (const chunk of this.provider.chatStream({ model: visionModel, messages })) {
|
|
489
|
-
const delta = chunk.choices?.[0]?.delta;
|
|
490
|
-
if (!delta) continue;
|
|
491
|
-
const text = typeof delta.content === 'string' ? delta.content : '';
|
|
492
|
-
if (text) {
|
|
493
|
-
reply += text;
|
|
494
|
-
if (onChunk) onChunk(text, reply);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
reply = stripThinkBlocks(reply);
|
|
498
|
-
if (!reply) {
|
|
499
|
-
logger.warn(`[processMultimodal] ${visionModel} 流式为空,尝试非流式`);
|
|
500
|
-
const response = await this.provider.chat({ model: visionModel, messages });
|
|
501
|
-
const msg = response.choices[0]?.message?.content;
|
|
502
|
-
reply = stripThinkBlocks(typeof msg === 'string' ? msg : '');
|
|
503
|
-
}
|
|
504
|
-
if (reply) break; // 成功,退出循环
|
|
505
|
-
} catch (err) {
|
|
506
|
-
const isLast = i === visionCandidates.length - 1;
|
|
507
|
-
if (isLast) {
|
|
508
|
-
try {
|
|
509
|
-
const response = await this.provider.chat({ model: visionModel, messages });
|
|
510
|
-
const msg = response.choices[0]?.message?.content;
|
|
511
|
-
reply = stripThinkBlocks(typeof msg === 'string' ? msg : '');
|
|
512
|
-
} catch { /* all candidates exhausted */ }
|
|
513
|
-
} else {
|
|
514
|
-
logger.warn(`[processMultimodal] ${visionModel} 失败,降级到 ${visionCandidates[i + 1]}: ${(err as Error).message}`);
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (!reply) reply = '抱歉,我无法理解这条消息。';
|
|
520
|
-
await this.saveToSession(sessionId, textContent, reply, sceneId);
|
|
521
|
-
return parseOutput(reply);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// ── Internal helpers ────────────────────────────────────────────────
|
|
525
|
-
|
|
526
|
-
private async buildHistoryMessages(sessionId: string): Promise<ChatMessage[]> {
|
|
527
|
-
return this.memory.buildContext(sessionId);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
private async streamChatWithHistory(
|
|
531
|
-
content: string,
|
|
532
|
-
systemPrompt: string,
|
|
533
|
-
history: ChatMessage[],
|
|
534
|
-
onChunk?: OnChunkCallback,
|
|
535
|
-
): Promise<string> {
|
|
536
|
-
const candidates = this.resolveModelCandidates('chat');
|
|
537
|
-
const userContent = history.length > 0
|
|
538
|
-
? buildUserMessageWithHistory(history, content)
|
|
539
|
-
: content;
|
|
540
|
-
const messages: ChatMessage[] = [
|
|
541
|
-
{ role: 'system', content: systemPrompt },
|
|
542
|
-
{ role: 'user', content: userContent },
|
|
543
|
-
];
|
|
544
|
-
|
|
545
|
-
for (let i = 0; i < candidates.length; i++) {
|
|
546
|
-
const model = candidates[i];
|
|
547
|
-
try {
|
|
548
|
-
let result = '';
|
|
549
|
-
for await (const chunk of this.provider.chatStream({ model, messages })) {
|
|
550
|
-
const delta = chunk.choices?.[0]?.delta;
|
|
551
|
-
if (!delta) continue;
|
|
552
|
-
const text = typeof delta.content === 'string' ? delta.content : '';
|
|
553
|
-
if (text) {
|
|
554
|
-
result += text;
|
|
555
|
-
if (onChunk) onChunk(text, result);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
result = stripThinkBlocks(result);
|
|
559
|
-
if (result) return result;
|
|
560
|
-
// Streaming returned empty content — try non-streaming with same model
|
|
561
|
-
logger.warn(`[streamChat] ${model} 流式响应为空,尝试非流式`);
|
|
562
|
-
const response = await this.provider.chat({ model, messages });
|
|
563
|
-
const msg = response.choices[0]?.message?.content;
|
|
564
|
-
result = stripThinkBlocks(typeof msg === 'string' ? msg : '');
|
|
565
|
-
if (result) {
|
|
566
|
-
if (onChunk) onChunk(result, result);
|
|
567
|
-
return result;
|
|
568
|
-
}
|
|
569
|
-
} catch (err) {
|
|
570
|
-
const isLast = i === candidates.length - 1;
|
|
571
|
-
if (isLast) {
|
|
572
|
-
// No more candidates — try non-streaming as final attempt
|
|
573
|
-
try {
|
|
574
|
-
const response = await this.provider.chat({ model, messages });
|
|
575
|
-
const msg = response.choices[0]?.message?.content;
|
|
576
|
-
let result = stripThinkBlocks(typeof msg === 'string' ? msg : '');
|
|
577
|
-
if (onChunk && result) onChunk(result, result);
|
|
578
|
-
return result;
|
|
579
|
-
} catch {
|
|
580
|
-
return '';
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
logger.warn(`[streamChat] ${model} 失败,降级到 ${candidates[i + 1]}: ${(err as Error).message}`);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
return '';
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
private async saveToSession(
|
|
590
|
-
sessionId: string,
|
|
591
|
-
userContent: string,
|
|
592
|
-
assistantContent: string,
|
|
593
|
-
sceneId?: string,
|
|
594
|
-
): Promise<void> {
|
|
595
|
-
await this.memory.saveRound(sessionId, userContent, assistantContent);
|
|
596
|
-
await this.sessions.addMessage(sessionId, { role: 'user', content: userContent });
|
|
597
|
-
await this.sessions.addMessage(sessionId, { role: 'assistant', content: assistantContent });
|
|
598
|
-
if (this.contextManager && sceneId) {
|
|
599
|
-
this.contextManager.autoSummarizeIfNeeded(sceneId).catch(() => {});
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
private fallbackFormat(toolCalls: { tool: string; args: any; result: any }[]): string {
|
|
604
|
-
if (toolCalls.length === 0) return 'Done.';
|
|
605
|
-
const userFacing = toolCalls.filter(tc => tc.tool !== 'activate_skill');
|
|
606
|
-
if (userFacing.length === 0) {
|
|
607
|
-
return '技能已激活但未能完成后续操作,请重试或换一种方式描述你的需求。';
|
|
608
|
-
}
|
|
609
|
-
return userFacing.map(tc => {
|
|
610
|
-
const s = typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result, null, 2);
|
|
611
|
-
return `【${tc.tool}】\n${s}`;
|
|
612
|
-
}).join('\n\n');
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
616
|
-
|
|
617
|
-
isReady(): boolean {
|
|
618
|
-
return true;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
dispose(): void {
|
|
622
|
-
this.memory.dispose();
|
|
623
|
-
this.sessions.dispose();
|
|
624
|
-
this.externalTools.clear();
|
|
625
|
-
this.userProfiles.dispose();
|
|
626
|
-
this.rateLimiter.dispose();
|
|
627
|
-
this.followUps.dispose();
|
|
628
|
-
if (this.subagentManager) {
|
|
629
|
-
this.subagentManager.dispose();
|
|
630
|
-
this.subagentManager = null;
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|