@zhin.js/core 1.0.32 → 1.0.34
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 +18 -0
- package/lib/ai/agent.d.ts.map +1 -1
- package/lib/ai/agent.js +15 -2
- package/lib/ai/agent.js.map +1 -1
- package/lib/ai/bootstrap.d.ts +11 -2
- package/lib/ai/bootstrap.d.ts.map +1 -1
- package/lib/ai/bootstrap.js +46 -2
- package/lib/ai/bootstrap.js.map +1 -1
- package/lib/ai/builtin-tools.d.ts +28 -6
- package/lib/ai/builtin-tools.d.ts.map +1 -1
- package/lib/ai/builtin-tools.js +265 -76
- package/lib/ai/builtin-tools.js.map +1 -1
- package/lib/ai/index.d.ts +9 -1
- package/lib/ai/index.d.ts.map +1 -1
- package/lib/ai/index.js +8 -0
- package/lib/ai/index.js.map +1 -1
- package/lib/ai/init.d.ts.map +1 -1
- package/lib/ai/init.js +84 -3
- package/lib/ai/init.js.map +1 -1
- package/lib/ai/providers/anthropic.d.ts +7 -0
- package/lib/ai/providers/anthropic.d.ts.map +1 -1
- package/lib/ai/providers/anthropic.js +3 -0
- package/lib/ai/providers/anthropic.js.map +1 -1
- package/lib/ai/providers/ollama.d.ts +10 -0
- package/lib/ai/providers/ollama.d.ts.map +1 -1
- package/lib/ai/providers/ollama.js +11 -3
- package/lib/ai/providers/ollama.js.map +1 -1
- package/lib/ai/providers/openai.d.ts +7 -0
- package/lib/ai/providers/openai.d.ts.map +1 -1
- package/lib/ai/providers/openai.js +3 -0
- package/lib/ai/providers/openai.js.map +1 -1
- package/lib/ai/service.d.ts +4 -0
- package/lib/ai/service.d.ts.map +1 -1
- package/lib/ai/service.js +7 -0
- package/lib/ai/service.js.map +1 -1
- package/lib/ai/subagent.d.ts +50 -0
- package/lib/ai/subagent.d.ts.map +1 -0
- package/lib/ai/subagent.js +144 -0
- package/lib/ai/subagent.js.map +1 -0
- package/lib/ai/types.d.ts +25 -5
- package/lib/ai/types.d.ts.map +1 -1
- package/lib/ai/zhin-agent-builtin-tools.d.ts +17 -0
- package/lib/ai/zhin-agent-builtin-tools.d.ts.map +1 -0
- package/lib/ai/zhin-agent-builtin-tools.js +220 -0
- package/lib/ai/zhin-agent-builtin-tools.js.map +1 -0
- package/lib/ai/zhin-agent-config.d.ts +54 -0
- package/lib/ai/zhin-agent-config.d.ts.map +1 -0
- package/lib/ai/zhin-agent-config.js +76 -0
- package/lib/ai/zhin-agent-config.js.map +1 -0
- package/lib/ai/zhin-agent-exec-policy.d.ts +20 -0
- package/lib/ai/zhin-agent-exec-policy.d.ts.map +1 -0
- package/lib/ai/zhin-agent-exec-policy.js +71 -0
- package/lib/ai/zhin-agent-exec-policy.js.map +1 -0
- package/lib/ai/zhin-agent-prompt.d.ts +21 -0
- package/lib/ai/zhin-agent-prompt.d.ts.map +1 -0
- package/lib/ai/zhin-agent-prompt.js +116 -0
- package/lib/ai/zhin-agent-prompt.js.map +1 -0
- package/lib/ai/zhin-agent-tool-collector.d.ts +22 -0
- package/lib/ai/zhin-agent-tool-collector.d.ts.map +1 -0
- package/lib/ai/zhin-agent-tool-collector.js +218 -0
- package/lib/ai/zhin-agent-tool-collector.js.map +1 -0
- package/lib/ai/zhin-agent.d.ts +11 -155
- package/lib/ai/zhin-agent.d.ts.map +1 -1
- package/lib/ai/zhin-agent.js +84 -684
- package/lib/ai/zhin-agent.js.map +1 -1
- package/lib/component.d.ts.map +1 -1
- package/lib/component.js +19 -19
- package/lib/component.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/scheduler/index.d.ts +10 -0
- package/lib/scheduler/index.d.ts.map +1 -0
- package/lib/scheduler/index.js +12 -0
- package/lib/scheduler/index.js.map +1 -0
- package/lib/scheduler/scheduler.d.ts +49 -0
- package/lib/scheduler/scheduler.d.ts.map +1 -0
- package/lib/scheduler/scheduler.js +352 -0
- package/lib/scheduler/scheduler.js.map +1 -0
- package/lib/scheduler/types.d.ts +71 -0
- package/lib/scheduler/types.d.ts.map +1 -0
- package/lib/scheduler/types.js +8 -0
- package/lib/scheduler/types.js.map +1 -0
- package/lib/tool-zod.d.ts +28 -0
- package/lib/tool-zod.d.ts.map +1 -0
- package/lib/tool-zod.js +98 -0
- package/lib/tool-zod.js.map +1 -0
- package/package.json +9 -4
- package/src/ai/agent.ts +15 -2
- package/src/ai/bootstrap.ts +48 -2
- package/src/ai/builtin-tools.ts +283 -75
- package/src/ai/index.ts +19 -1
- package/src/ai/init.ts +85 -3
- package/src/ai/providers/anthropic.ts +3 -0
- package/src/ai/providers/ollama.ts +13 -3
- package/src/ai/providers/openai.ts +3 -0
- package/src/ai/service.ts +8 -0
- package/src/ai/subagent.ts +209 -0
- package/src/ai/types.ts +29 -2
- package/src/ai/zhin-agent-builtin-tools.ts +247 -0
- package/src/ai/zhin-agent-config.ts +113 -0
- package/src/ai/zhin-agent-exec-policy.ts +78 -0
- package/src/ai/zhin-agent-prompt.ts +136 -0
- package/src/ai/zhin-agent-tool-collector.ts +243 -0
- package/src/ai/zhin-agent.ts +113 -791
- package/src/component.ts +29 -28
- package/src/index.ts +1 -0
- package/src/scheduler/index.ts +28 -0
- package/src/scheduler/scheduler.ts +372 -0
- package/src/scheduler/types.ts +74 -0
- package/src/tool-zod.ts +115 -0
- package/tests/ai/subagent.test.ts +270 -0
package/lib/ai/zhin-agent.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ZhinAgent — 全局持久 AI 大脑
|
|
3
3
|
*
|
|
4
|
-
* 取代旧的 AIService.process() 临时创建 Agent 的方式。
|
|
5
|
-
*
|
|
6
4
|
* 核心能力:
|
|
7
5
|
* 1. 全局单例,应用生命周期内常驻
|
|
8
6
|
* 2. Skill 感知:两级过滤 Skill → Tool
|
|
@@ -18,7 +16,7 @@
|
|
|
18
16
|
* 12. 多模态输入:图片/音频直接传给视觉模型
|
|
19
17
|
*/
|
|
20
18
|
import { Logger } from '@zhin.js/logger';
|
|
21
|
-
import {
|
|
19
|
+
import { createAgent } from './agent.js';
|
|
22
20
|
import { SessionManager, createMemorySessionManager } from './session.js';
|
|
23
21
|
import { ConversationMemory } from './conversation-memory.js';
|
|
24
22
|
import { parseOutput } from './output.js';
|
|
@@ -26,59 +24,17 @@ import { UserProfileStore } from './user-profile.js';
|
|
|
26
24
|
import { RateLimiter } from './rate-limiter.js';
|
|
27
25
|
import { detectTone } from './tone-detector.js';
|
|
28
26
|
import { FollowUpManager } from './follow-up.js';
|
|
27
|
+
import { SubagentManager } from './subagent.js';
|
|
29
28
|
import { pruneHistoryForContext, DEFAULT_CONTEXT_TOKENS, } from './compaction.js';
|
|
30
29
|
import { triggerAIHook, createAIHookEvent } from './hooks.js';
|
|
30
|
+
// ── Extracted modules ───────────────────────────────────────────────
|
|
31
|
+
import { DEFAULT_CONFIG, } from './zhin-agent-config.js';
|
|
32
|
+
import { applyExecPolicyToTools } from './zhin-agent-exec-policy.js';
|
|
33
|
+
import { collectRelevantTools } from './zhin-agent-tool-collector.js';
|
|
34
|
+
import { buildEnhancedPersona, buildContextHint, buildRichSystemPrompt, buildUserMessageWithHistory, } from './zhin-agent-prompt.js';
|
|
35
|
+
import { createChatHistoryTool, createUserProfileTool, createScheduleFollowUpTool, createSpawnTaskTool, } from './zhin-agent-builtin-tools.js';
|
|
31
36
|
const logger = new Logger(null, 'ZhinAgent');
|
|
32
|
-
/** 高精度计时 */
|
|
33
37
|
const now = () => performance.now();
|
|
34
|
-
const HISTORY_CONTEXT_MARKER = '[Chat messages since your last reply - for context]';
|
|
35
|
-
const CURRENT_MESSAGE_MARKER = '[Current message - respond to this]';
|
|
36
|
-
function contentToText(c) {
|
|
37
|
-
if (typeof c === 'string')
|
|
38
|
-
return c;
|
|
39
|
-
return c.map(p => (p.type === 'text' ? p.text : '')).join('');
|
|
40
|
-
}
|
|
41
|
-
function buildUserMessageWithHistory(history, currentContent) {
|
|
42
|
-
if (history.length === 0)
|
|
43
|
-
return currentContent;
|
|
44
|
-
const roleLabel = (role) => (role === 'user' ? '用户' : role === 'assistant' ? '助手' : '系统');
|
|
45
|
-
const lines = history
|
|
46
|
-
.filter(m => m.role === 'user' || m.role === 'assistant' || m.role === 'system')
|
|
47
|
-
.map(m => `${roleLabel(m.role)}: ${contentToText(m.content)}`);
|
|
48
|
-
const historyBlock = lines.join('\n');
|
|
49
|
-
return `${HISTORY_CONTEXT_MARKER}\n${historyBlock}\n\n${CURRENT_MESSAGE_MARKER}\n${currentContent}`;
|
|
50
|
-
}
|
|
51
|
-
const DEFAULT_CONFIG = {
|
|
52
|
-
persona: '你是一个友好的中文 AI 助手,擅长使用工具帮助用户解决问题。',
|
|
53
|
-
maxIterations: 5,
|
|
54
|
-
timeout: 60_000,
|
|
55
|
-
preExecTimeout: 10_000,
|
|
56
|
-
maxSkills: 3,
|
|
57
|
-
maxTools: 8,
|
|
58
|
-
minTopicRounds: 5,
|
|
59
|
-
slidingWindowSize: 5,
|
|
60
|
-
topicChangeThreshold: 0.15,
|
|
61
|
-
rateLimit: {},
|
|
62
|
-
toneAwareness: true,
|
|
63
|
-
visionModel: '',
|
|
64
|
-
contextTokens: DEFAULT_CONTEXT_TOKENS,
|
|
65
|
-
maxHistoryShare: 0.5,
|
|
66
|
-
disabledTools: [],
|
|
67
|
-
allowedTools: [], // 空数组表示不限制;非空时仅允许列表中的工具
|
|
68
|
-
execSecurity: 'deny', // 默认禁止 bash,避免误用
|
|
69
|
-
execAllowlist: [],
|
|
70
|
-
execAsk: false,
|
|
71
|
-
};
|
|
72
|
-
// ============================================================================
|
|
73
|
-
// 权限映射
|
|
74
|
-
// ============================================================================
|
|
75
|
-
const PERM_MAP = {
|
|
76
|
-
user: 0,
|
|
77
|
-
group_admin: 1,
|
|
78
|
-
group_owner: 2,
|
|
79
|
-
bot_admin: 3,
|
|
80
|
-
owner: 4,
|
|
81
|
-
};
|
|
82
38
|
// ============================================================================
|
|
83
39
|
// ZhinAgent
|
|
84
40
|
// ============================================================================
|
|
@@ -93,8 +49,10 @@ export class ZhinAgent {
|
|
|
93
49
|
userProfiles;
|
|
94
50
|
rateLimiter;
|
|
95
51
|
followUps;
|
|
96
|
-
|
|
52
|
+
subagentManager = null;
|
|
97
53
|
bootstrapContext = '';
|
|
54
|
+
activeSkillsContext = '';
|
|
55
|
+
skillsSummaryXML = '';
|
|
98
56
|
constructor(provider, config) {
|
|
99
57
|
this.provider = provider;
|
|
100
58
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
@@ -109,7 +67,7 @@ export class ZhinAgent {
|
|
|
109
67
|
this.rateLimiter = new RateLimiter(this.config.rateLimit);
|
|
110
68
|
this.followUps = new FollowUpManager();
|
|
111
69
|
}
|
|
112
|
-
// ──
|
|
70
|
+
// ── DI setters ──────────────────────────────────────────────────────
|
|
113
71
|
setSkillRegistry(registry) {
|
|
114
72
|
this.skillRegistry = registry;
|
|
115
73
|
logger.debug(`SkillRegistry connected (${registry.size} skills)`);
|
|
@@ -122,30 +80,39 @@ export class ZhinAgent {
|
|
|
122
80
|
this.contextManager = manager;
|
|
123
81
|
manager.setAIProvider(this.provider);
|
|
124
82
|
}
|
|
125
|
-
/** 将 ConversationMemory 升级为数据库存储 */
|
|
126
83
|
upgradeMemoryToDatabase(msgModel, sumModel) {
|
|
127
84
|
this.memory.upgradeToDatabase(msgModel, sumModel);
|
|
128
85
|
}
|
|
129
|
-
/** 将 UserProfileStore 升级为数据库存储 */
|
|
130
86
|
upgradeProfilesToDatabase(model) {
|
|
131
87
|
this.userProfiles.upgradeToDatabase(model);
|
|
132
88
|
}
|
|
133
|
-
/** 将 FollowUpManager 升级为数据库存储 */
|
|
134
89
|
upgradeFollowUpsToDatabase(model) {
|
|
135
90
|
this.followUps.upgradeToDatabase(model);
|
|
136
91
|
}
|
|
137
|
-
/** 注入提醒消息发送回调(由 init.ts 在适配器就绪后调用) */
|
|
138
92
|
setFollowUpSender(sender) {
|
|
139
93
|
this.followUps.setSender(sender);
|
|
140
94
|
}
|
|
141
|
-
/**
|
|
142
|
-
* 从数据库恢复未完成的跟进任务(启动时调用)
|
|
143
|
-
* @returns 恢复的任务数量
|
|
144
|
-
*/
|
|
145
95
|
async restoreFollowUps() {
|
|
146
96
|
return this.followUps.restore();
|
|
147
97
|
}
|
|
148
|
-
|
|
98
|
+
initSubagentManager(createTools) {
|
|
99
|
+
this.subagentManager = new SubagentManager({
|
|
100
|
+
provider: this.provider,
|
|
101
|
+
workspace: process.cwd(),
|
|
102
|
+
createTools,
|
|
103
|
+
maxIterations: this.config.maxSubagentIterations,
|
|
104
|
+
execPolicyConfig: this.config,
|
|
105
|
+
});
|
|
106
|
+
logger.debug('SubagentManager initialized');
|
|
107
|
+
}
|
|
108
|
+
setSubagentSender(sender) {
|
|
109
|
+
if (this.subagentManager) {
|
|
110
|
+
this.subagentManager.setSender(sender);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
getSubagentManager() {
|
|
114
|
+
return this.subagentManager;
|
|
115
|
+
}
|
|
149
116
|
getUserProfiles() {
|
|
150
117
|
return this.userProfiles;
|
|
151
118
|
}
|
|
@@ -153,70 +120,57 @@ export class ZhinAgent {
|
|
|
153
120
|
this.externalTools.set(tool.name, tool);
|
|
154
121
|
return () => { this.externalTools.delete(tool.name); };
|
|
155
122
|
}
|
|
156
|
-
/**
|
|
157
|
-
* 注入引导文件上下文(SOUL.md + TOOLS.md + AGENTS.md 的合并内容)
|
|
158
|
-
* 由 init.ts 在加载引导文件后调用
|
|
159
|
-
*/
|
|
160
123
|
setBootstrapContext(context) {
|
|
161
124
|
this.bootstrapContext = context;
|
|
162
125
|
logger.debug(`Bootstrap context set (${context.length} chars)`);
|
|
163
126
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
* @param onChunk 流式输出回调(可选,适配器支持时传入)
|
|
172
|
-
*
|
|
173
|
-
* 路径选择策略(按开销从低到高):
|
|
174
|
-
*
|
|
175
|
-
* ┌─ 闲聊路径(最快)────────────────────────────────────────────┐
|
|
176
|
-
* │ 工具过滤 = 0 → 仅 persona prompt → 流式 1 次 LLM 调用 │
|
|
177
|
-
* └──────────────────────────────────────────────────────────────┘
|
|
178
|
-
* ┌─ 快速路径(1 轮 LLM)───────────────────────────────────────┐
|
|
179
|
-
* │ 全部命中无参数工具 → 预执行 → 数据注入 prompt → 1 次 LLM │
|
|
180
|
-
* └──────────────────────────────────────────────────────────────┘
|
|
181
|
-
* ┌─ Agent 路径(多轮 LLM)─────────────────────────────────────┐
|
|
182
|
-
* │ 存在需参数工具 → Agent tool-calling → 多轮 LLM │
|
|
183
|
-
* └──────────────────────────────────────────────────────────────┘
|
|
184
|
-
*/
|
|
127
|
+
setActiveSkillsContext(content) {
|
|
128
|
+
this.activeSkillsContext = content || '';
|
|
129
|
+
}
|
|
130
|
+
setSkillsSummaryXML(xml) {
|
|
131
|
+
this.skillsSummaryXML = xml || '';
|
|
132
|
+
}
|
|
133
|
+
// ── Core processing ─────────────────────────────────────────────────
|
|
185
134
|
async process(content, context, externalTools = [], onChunk) {
|
|
186
135
|
const t0 = now();
|
|
187
136
|
const { senderId, sceneId, platform } = context;
|
|
188
137
|
const sessionId = SessionManager.generateId(platform || '', senderId || '', sceneId);
|
|
189
138
|
const userId = senderId || 'unknown';
|
|
190
|
-
//
|
|
139
|
+
// 0. Rate limit
|
|
191
140
|
const rateCheck = this.rateLimiter.check(userId);
|
|
192
141
|
if (!rateCheck.allowed) {
|
|
193
142
|
logger.debug(`[速率限制] 用户 ${userId} 被限制: ${rateCheck.message}`);
|
|
194
143
|
return parseOutput(rateCheck.message || '请稍后再试');
|
|
195
144
|
}
|
|
196
|
-
// 触发 message:received hook
|
|
197
145
|
triggerAIHook(createAIHookEvent('message', 'received', sessionId, {
|
|
198
146
|
userId,
|
|
199
147
|
content,
|
|
200
148
|
platform: platform || '',
|
|
201
149
|
})).catch(() => { });
|
|
202
|
-
//
|
|
150
|
+
// 1. Collect tools
|
|
203
151
|
const tFilter = now();
|
|
204
|
-
const allTools =
|
|
205
|
-
|
|
152
|
+
const allTools = collectRelevantTools(content, context, externalTools, {
|
|
153
|
+
config: this.config,
|
|
154
|
+
skillRegistry: this.skillRegistry,
|
|
155
|
+
externalRegistered: this.externalTools,
|
|
156
|
+
});
|
|
157
|
+
// Inject context-aware built-in tools on keyword match
|
|
206
158
|
if (/之前|上次|历史|回忆|聊过|记录|还记得|曾经/i.test(content)) {
|
|
207
|
-
allTools.push(
|
|
159
|
+
allTools.push(createChatHistoryTool(sessionId, this.memory));
|
|
208
160
|
}
|
|
209
161
|
if (/偏好|设置|配置|档案|资料|时区|timezone|profile|喜好|我叫|叫我|记住我/i.test(content)) {
|
|
210
|
-
allTools.push(
|
|
162
|
+
allTools.push(createUserProfileTool(userId, this.userProfiles));
|
|
211
163
|
}
|
|
212
164
|
if (/提醒|定时|过一会|跟进|别忘|取消提醒|reminder|分钟后|小时后/i.test(content)) {
|
|
213
|
-
allTools.push(
|
|
165
|
+
allTools.push(createScheduleFollowUpTool(sessionId, context, this.followUps));
|
|
166
|
+
}
|
|
167
|
+
if (this.subagentManager && /后台|子任务|spawn|异步|background|并行|独立处理/i.test(content)) {
|
|
168
|
+
allTools.push(createSpawnTaskTool(context, this.subagentManager));
|
|
214
169
|
}
|
|
215
170
|
const filterMs = (now() - tFilter).toFixed(0);
|
|
216
|
-
//
|
|
171
|
+
// 2. History + profile
|
|
217
172
|
const tMem = now();
|
|
218
173
|
let historyMessages = await this.buildHistoryMessages(sessionId);
|
|
219
|
-
// 上下文窗口保护:按 token 预算修剪历史(借鉴 OpenClaw context-window-guard)
|
|
220
174
|
const contextTokens = this.config.contextTokens ?? DEFAULT_CONTEXT_TOKENS;
|
|
221
175
|
const maxHistoryShare = this.config.maxHistoryShare ?? 0.5;
|
|
222
176
|
const pruneResult = pruneHistoryForContext({
|
|
@@ -229,11 +183,11 @@ export class ZhinAgent {
|
|
|
229
183
|
logger.debug(`[上下文窗口] 丢弃 ${pruneResult.droppedCount} 条历史消息 (${pruneResult.droppedTokens} tokens)`);
|
|
230
184
|
}
|
|
231
185
|
const memMs = (now() - tMem).toFixed(0);
|
|
232
|
-
//
|
|
186
|
+
// 2.5 Profile + tone
|
|
233
187
|
const profileSummary = await this.userProfiles.buildProfileSummary(userId);
|
|
234
188
|
const toneHint = this.config.toneAwareness ? detectTone(content).hint : '';
|
|
235
|
-
const personaEnhanced = this.
|
|
236
|
-
//
|
|
189
|
+
const personaEnhanced = buildEnhancedPersona(this.config, profileSummary, toneHint);
|
|
190
|
+
// 3. No tools → chat path
|
|
237
191
|
if (allTools.length === 0) {
|
|
238
192
|
logger.debug(`[闲聊路径] 过滤=${filterMs}ms, 记忆=${memMs}ms (${historyMessages.length}条), 0 工具`);
|
|
239
193
|
const tLLM = now();
|
|
@@ -244,14 +198,13 @@ export class ZhinAgent {
|
|
|
244
198
|
return parseOutput(reply);
|
|
245
199
|
}
|
|
246
200
|
logger.debug(`[工具路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, ${allTools.length} 工具 (${allTools.map(t => t.name).join(', ')})`);
|
|
247
|
-
//
|
|
248
|
-
// 只有显式标记 preExecutable=true 的工具才会被预执行(opt-in 模式)
|
|
201
|
+
// 4. Pre-executable tools
|
|
249
202
|
const preExecTools = [];
|
|
250
203
|
for (const tool of allTools) {
|
|
251
204
|
if (tool.preExecutable)
|
|
252
205
|
preExecTools.push(tool);
|
|
253
206
|
}
|
|
254
|
-
//
|
|
207
|
+
// 5. Pre-execution
|
|
255
208
|
let preData = '';
|
|
256
209
|
if (preExecTools.length > 0) {
|
|
257
210
|
const tPre = now();
|
|
@@ -266,7 +219,6 @@ export class ZhinAgent {
|
|
|
266
219
|
for (const r of results) {
|
|
267
220
|
if (r.status === 'fulfilled') {
|
|
268
221
|
let s = typeof r.value.result === 'string' ? r.value.result : JSON.stringify(r.value.result);
|
|
269
|
-
// 限制单条预执行结果的长度,防止注入过多数据干扰模型
|
|
270
222
|
if (s.length > 500) {
|
|
271
223
|
s = s.slice(0, 500) + `\n... (truncated, ${s.length} chars total)`;
|
|
272
224
|
}
|
|
@@ -275,12 +227,11 @@ export class ZhinAgent {
|
|
|
275
227
|
}
|
|
276
228
|
logger.debug(`预执行耗时: ${(now() - tPre).toFixed(0)}ms`);
|
|
277
229
|
}
|
|
278
|
-
//
|
|
230
|
+
// 6. Path selection
|
|
279
231
|
let reply;
|
|
280
|
-
// 判断是否所有工具都已被预执行(即没有非预执行工具)
|
|
281
232
|
const hasNonPreExecTools = allTools.some(t => !t.preExecutable);
|
|
282
233
|
if (!hasNonPreExecTools && preData) {
|
|
283
|
-
//
|
|
234
|
+
// Fast path
|
|
284
235
|
const tLLM = now();
|
|
285
236
|
const prompt = `${personaEnhanced}
|
|
286
237
|
|
|
@@ -292,22 +243,31 @@ ${preData}
|
|
|
292
243
|
logger.info(`[快速路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, LLM=${(now() - tLLM).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
|
|
293
244
|
}
|
|
294
245
|
else {
|
|
295
|
-
//
|
|
246
|
+
// Agent path
|
|
296
247
|
const tAgent = now();
|
|
297
248
|
logger.debug(`Agent 路径: ${allTools.length} 个工具`);
|
|
298
|
-
const contextHint =
|
|
299
|
-
|
|
300
|
-
|
|
249
|
+
const contextHint = buildContextHint(context, content);
|
|
250
|
+
const richPrompt = buildRichSystemPrompt({
|
|
251
|
+
config: this.config,
|
|
252
|
+
skillRegistry: this.skillRegistry,
|
|
253
|
+
skillsSummaryXML: this.skillsSummaryXML,
|
|
254
|
+
activeSkillsContext: this.activeSkillsContext,
|
|
255
|
+
bootstrapContext: this.bootstrapContext,
|
|
256
|
+
});
|
|
301
257
|
const systemPrompt = `${richPrompt}
|
|
302
258
|
${contextHint}
|
|
303
259
|
${preData ? `\n已获取数据:${preData}\n` : ''}`;
|
|
304
|
-
|
|
305
|
-
//
|
|
306
|
-
const
|
|
260
|
+
const agentTools = applyExecPolicyToTools(this.config, allTools);
|
|
261
|
+
// Adaptive maxIterations: boost when skills are active (multi-step skill flows)
|
|
262
|
+
const SKILL_ITERATION_BOOST = 3;
|
|
263
|
+
const hasSkillActivation = agentTools.some(t => t.name === 'activate_skill' || t.name === 'install_skill');
|
|
264
|
+
const effectiveMaxIterations = hasSkillActivation
|
|
265
|
+
? this.config.maxIterations + SKILL_ITERATION_BOOST
|
|
266
|
+
: this.config.maxIterations;
|
|
307
267
|
const agent = createAgent(this.provider, {
|
|
308
268
|
systemPrompt,
|
|
309
269
|
tools: agentTools,
|
|
310
|
-
maxIterations:
|
|
270
|
+
maxIterations: effectiveMaxIterations,
|
|
311
271
|
});
|
|
312
272
|
const userMessageWithHistory = buildUserMessageWithHistory(historyMessages, content);
|
|
313
273
|
const result = await agent.run(userMessageWithHistory, []);
|
|
@@ -315,7 +275,6 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
|
|
|
315
275
|
logger.info(`[Agent 路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, Agent=${(now() - tAgent).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
|
|
316
276
|
}
|
|
317
277
|
await this.saveToSession(sessionId, content, reply, sceneId);
|
|
318
|
-
// 触发 message:sent hook
|
|
319
278
|
triggerAIHook(createAIHookEvent('message', 'sent', sessionId, {
|
|
320
279
|
userId,
|
|
321
280
|
content: reply,
|
|
@@ -323,30 +282,21 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
|
|
|
323
282
|
})).catch(() => { });
|
|
324
283
|
return parseOutput(reply);
|
|
325
284
|
}
|
|
326
|
-
/**
|
|
327
|
-
* 处理多模态消息(图片+文字)
|
|
328
|
-
*
|
|
329
|
-
* 当用户发送图片时,走视觉模型路径。
|
|
330
|
-
*/
|
|
331
285
|
async processMultimodal(parts, context, onChunk) {
|
|
332
286
|
const { senderId, sceneId, platform } = context;
|
|
333
287
|
const sessionId = SessionManager.generateId(platform || '', senderId || '', sceneId);
|
|
334
288
|
const userId = senderId || 'unknown';
|
|
335
|
-
// 速率限制
|
|
336
289
|
const rateCheck = this.rateLimiter.check(userId);
|
|
337
290
|
if (!rateCheck.allowed) {
|
|
338
291
|
return parseOutput(rateCheck.message || '请稍后再试');
|
|
339
292
|
}
|
|
340
|
-
// 构建记忆
|
|
341
293
|
const historyMessages = await this.buildHistoryMessages(sessionId);
|
|
342
294
|
const profileSummary = await this.userProfiles.buildProfileSummary(userId);
|
|
343
|
-
const personaEnhanced = this.
|
|
344
|
-
// 提取文本部分用于保存
|
|
295
|
+
const personaEnhanced = buildEnhancedPersona(this.config, profileSummary, '');
|
|
345
296
|
const textContent = parts
|
|
346
297
|
.filter((p) => p.type === 'text')
|
|
347
298
|
.map(p => p.text)
|
|
348
299
|
.join(' ') || '[多模态消息]';
|
|
349
|
-
// 选择模型:优先视觉模型
|
|
350
300
|
const visionModel = this.config.visionModel || this.provider.models[0];
|
|
351
301
|
const messages = [
|
|
352
302
|
{ role: 'system', content: personaEnhanced },
|
|
@@ -365,7 +315,6 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
|
|
|
365
315
|
}
|
|
366
316
|
}
|
|
367
317
|
catch {
|
|
368
|
-
// fallback 非流式
|
|
369
318
|
const response = await this.provider.chat({ model: visionModel, messages });
|
|
370
319
|
const msg = response.choices[0]?.message?.content;
|
|
371
320
|
reply = typeof msg === 'string' ? msg : '';
|
|
@@ -375,556 +324,10 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
|
|
|
375
324
|
await this.saveToSession(sessionId, textContent, reply, sceneId);
|
|
376
325
|
return parseOutput(reply);
|
|
377
326
|
}
|
|
378
|
-
// ──
|
|
379
|
-
buildEnhancedPersona(profileSummary, toneHint) {
|
|
380
|
-
let persona = this.config.persona;
|
|
381
|
-
if (profileSummary) {
|
|
382
|
-
persona += `\n\n${profileSummary}`;
|
|
383
|
-
}
|
|
384
|
-
if (toneHint) {
|
|
385
|
-
persona += `\n\n[语气提示] ${toneHint}`;
|
|
386
|
-
}
|
|
387
|
-
// 注入当前时间(所有路径都需要,闲聊/快速/Agent 路径共用)
|
|
388
|
-
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
389
|
-
const timeStr = new Date().toLocaleString('zh-CN', { timeZone: tz });
|
|
390
|
-
persona += `\n\n当前时间: ${timeStr} (${tz})`;
|
|
391
|
-
return persona;
|
|
392
|
-
}
|
|
393
|
-
/**
|
|
394
|
-
* 构建上下文提示 — 告诉 AI 当前身份和场景,帮助工具参数填充
|
|
395
|
-
*/
|
|
396
|
-
buildContextHint(context, _content) {
|
|
397
|
-
const parts = [];
|
|
398
|
-
if (context.platform)
|
|
399
|
-
parts.push(`平台:${context.platform}`);
|
|
400
|
-
if (context.botId)
|
|
401
|
-
parts.push(`Bot:${context.botId}`);
|
|
402
|
-
if (context.senderId)
|
|
403
|
-
parts.push(`用户:${context.senderId}`);
|
|
404
|
-
if (context.scope)
|
|
405
|
-
parts.push(`场景类型:${context.scope}`);
|
|
406
|
-
if (context.sceneId)
|
|
407
|
-
parts.push(`场景ID:${context.sceneId}`);
|
|
408
|
-
if (parts.length === 0)
|
|
409
|
-
return '';
|
|
410
|
-
return `\n上下文: ${parts.join(' | ')}`;
|
|
411
|
-
}
|
|
412
|
-
// ── 工具收集: 两级过滤 (Skill → Tool) ─────────────────────────────────
|
|
413
|
-
collectTools(message, context, externalTools) {
|
|
414
|
-
const callerPerm = context.senderPermissionLevel
|
|
415
|
-
? (PERM_MAP[context.senderPermissionLevel] ?? 0)
|
|
416
|
-
: (context.isOwner ? 4 : context.isBotAdmin ? 3 : context.isGroupOwner ? 2 : context.isGroupAdmin ? 1 : 0);
|
|
417
|
-
const collected = [];
|
|
418
|
-
const collectedNames = new Set(); // 用 Set 加速去重
|
|
419
|
-
// 0. 检测用户是否明确提到了已知技能名称
|
|
420
|
-
// 若是,优先包含 activate_skill 以确保 Agent 可以激活该技能
|
|
421
|
-
let mentionedSkill = null;
|
|
422
|
-
if (this.skillRegistry && this.skillRegistry.size > 0) {
|
|
423
|
-
const msgLower = message.toLowerCase();
|
|
424
|
-
for (const skill of this.skillRegistry.getAll()) {
|
|
425
|
-
// 检查用户消息是否包含技能名称(精确或模糊匹配)
|
|
426
|
-
if (msgLower.includes(skill.name.toLowerCase())) {
|
|
427
|
-
mentionedSkill = skill.name;
|
|
428
|
-
logger.debug(`[技能检测] 用户提到技能: ${mentionedSkill}`);
|
|
429
|
-
break; // 只检测第一个匹配的技能
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
// 如果检测到技能名称,从 externalTools 中找 activate_skill 并优先加入
|
|
434
|
-
if (mentionedSkill) {
|
|
435
|
-
const activateSkillTool = externalTools.find(t => t.name === 'activate_skill');
|
|
436
|
-
if (activateSkillTool) {
|
|
437
|
-
const toolPerm = activateSkillTool.permissionLevel ? (PERM_MAP[activateSkillTool.permissionLevel] ?? 0) : 0;
|
|
438
|
-
if (toolPerm <= callerPerm) {
|
|
439
|
-
collected.push(this.toAgentTool(activateSkillTool, context));
|
|
440
|
-
collectedNames.add('activate_skill');
|
|
441
|
-
logger.debug(`[技能激活] 已提前加入 activate_skill 工具(优先级最高)`);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
// 1. 从 SkillRegistry 两级过滤(包含适配器通过 declareSkill 注册的 Skill)
|
|
446
|
-
if (this.skillRegistry) {
|
|
447
|
-
const skills = this.skillRegistry.search(message, { maxResults: this.config.maxSkills });
|
|
448
|
-
const skillStr = skills.length > 0
|
|
449
|
-
? skills.map(s => `${s.name}(${s.tools?.length || 0}工具)`).join(', ')
|
|
450
|
-
: '(无匹配技能)';
|
|
451
|
-
logger.debug(`[Skill 匹配] ${skillStr}`);
|
|
452
|
-
for (const skill of skills) {
|
|
453
|
-
for (const tool of skill.tools) {
|
|
454
|
-
// 平台过滤:确保 Skill 中的工具也只保留当前平台支持的
|
|
455
|
-
if (tool.platforms?.length && context.platform && !tool.platforms.includes(context.platform))
|
|
456
|
-
continue;
|
|
457
|
-
// 场景过滤
|
|
458
|
-
if (tool.scopes?.length && context.scope && !tool.scopes.includes(context.scope))
|
|
459
|
-
continue;
|
|
460
|
-
// 权限检查
|
|
461
|
-
const toolPerm = tool.permissionLevel ? (PERM_MAP[tool.permissionLevel] ?? 0) : 0;
|
|
462
|
-
if (toolPerm > callerPerm)
|
|
463
|
-
continue;
|
|
464
|
-
if (collectedNames.has(tool.name))
|
|
465
|
-
continue;
|
|
466
|
-
collected.push(this.toAgentTool(tool, context));
|
|
467
|
-
collectedNames.add(tool.name);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
// 2. 外部传入的工具(ToolService 收集的),跳过已通过 Skill 收集的同名工具
|
|
472
|
-
let deduped = 0;
|
|
473
|
-
for (const tool of externalTools) {
|
|
474
|
-
if (tool.name.startsWith('cmd_') || tool.name.startsWith('process_'))
|
|
475
|
-
continue;
|
|
476
|
-
const toolPerm = tool.permissionLevel ? (PERM_MAP[tool.permissionLevel] ?? 0) : 0;
|
|
477
|
-
if (toolPerm > callerPerm)
|
|
478
|
-
continue;
|
|
479
|
-
if (collectedNames.has(tool.name)) {
|
|
480
|
-
deduped++;
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
collected.push(this.toAgentTool(tool, context));
|
|
484
|
-
collectedNames.add(tool.name);
|
|
485
|
-
}
|
|
486
|
-
if (deduped > 0) {
|
|
487
|
-
logger.debug(`externalTools 去重: 跳过 ${deduped} 个已由 Skill 提供的工具`);
|
|
488
|
-
}
|
|
489
|
-
// 3. 额外注册的工具
|
|
490
|
-
for (const tool of this.externalTools.values()) {
|
|
491
|
-
if (tool.permissionLevel != null && tool.permissionLevel > callerPerm)
|
|
492
|
-
continue;
|
|
493
|
-
if (collectedNames.has(tool.name))
|
|
494
|
-
continue;
|
|
495
|
-
collected.push(tool);
|
|
496
|
-
collectedNames.add(tool.name);
|
|
497
|
-
}
|
|
498
|
-
// 4. 用 Agent.filterTools 做最终相关性排序(阈值 0.3 减少噪音)
|
|
499
|
-
const filtered = Agent.filterTools(message, collected, {
|
|
500
|
-
callerPermissionLevel: callerPerm,
|
|
501
|
-
maxTools: this.config.maxTools,
|
|
502
|
-
minScore: 0.3,
|
|
503
|
-
});
|
|
504
|
-
// 特殊处理:如果检测到了技能名称,确保 activate_skill 排在最前面
|
|
505
|
-
if (mentionedSkill && filtered.length > 0) {
|
|
506
|
-
const activateSkillIdx = filtered.findIndex(t => t.name === 'activate_skill');
|
|
507
|
-
if (activateSkillIdx > 0) { // 若存在但不在最前
|
|
508
|
-
// 将 activate_skill 移到最前面
|
|
509
|
-
const activateSkillTool = filtered[activateSkillIdx];
|
|
510
|
-
filtered.splice(activateSkillIdx, 1);
|
|
511
|
-
filtered.unshift(activateSkillTool);
|
|
512
|
-
logger.debug(`[工具排序] activate_skill 提升至首位(因检测到技能: ${mentionedSkill})`);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
// 5. 配置级工具开关:disabledTools / allowedTools(权限与安全)
|
|
516
|
-
let final = filtered;
|
|
517
|
-
const allowed = this.config.allowedTools;
|
|
518
|
-
const disabled = this.config.disabledTools ?? [];
|
|
519
|
-
if (allowed && allowed.length > 0) {
|
|
520
|
-
const allowSet = new Set(allowed.map(n => n.toLowerCase()));
|
|
521
|
-
final = final.filter(t => allowSet.has(t.name.toLowerCase()));
|
|
522
|
-
if (final.length < filtered.length) {
|
|
523
|
-
logger.debug(`[工具开关] allowedTools 限制: ${filtered.length} -> ${final.length}`);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
else if (disabled.length > 0) {
|
|
527
|
-
const disabledSet = new Set(disabled.map(n => n.toLowerCase()));
|
|
528
|
-
final = final.filter(t => !disabledSet.has(t.name.toLowerCase()));
|
|
529
|
-
if (final.length < filtered.length) {
|
|
530
|
-
logger.debug(`[工具开关] disabledTools 过滤: ${filtered.length} -> ${final.length}`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
// 诊断日志:显示收集的工具总数、过滤后的数量、以及列表
|
|
534
|
-
if (final.length > 0) {
|
|
535
|
-
logger.debug(`[工具收集] 收集了 ${collected.length} 个工具,过滤后 ${final.length} 个,` +
|
|
536
|
-
`用户消息相关性最高的: ${final.slice(0, 3).map(t => t.name).join(', ')}`);
|
|
537
|
-
}
|
|
538
|
-
else {
|
|
539
|
-
logger.debug(`[工具收集] 收集了 ${collected.length} 个工具,但过滤后 0 个(没有超过相关性阈值的)`);
|
|
540
|
-
}
|
|
541
|
-
return final;
|
|
542
|
-
}
|
|
543
|
-
/**
|
|
544
|
-
* bash 执行策略检查:未通过时抛出 Error(供上层返回给用户)。
|
|
545
|
-
*/
|
|
546
|
-
checkExecPolicy(command) {
|
|
547
|
-
const security = this.config.execSecurity ?? 'deny';
|
|
548
|
-
if (security === 'full')
|
|
549
|
-
return;
|
|
550
|
-
if (security === 'deny') {
|
|
551
|
-
throw new Error('当前配置禁止执行 Shell 命令(execSecurity=deny)。如需开放请在配置中设置 ai.agent.execSecurity。');
|
|
552
|
-
}
|
|
553
|
-
// allowlist
|
|
554
|
-
const list = this.config.execAllowlist ?? [];
|
|
555
|
-
const cmd = (command || '').trim();
|
|
556
|
-
const allowed = list.some(pattern => {
|
|
557
|
-
try {
|
|
558
|
-
const re = new RegExp(pattern);
|
|
559
|
-
return re.test(cmd);
|
|
560
|
-
}
|
|
561
|
-
catch {
|
|
562
|
-
return cmd === pattern || cmd.startsWith(pattern);
|
|
563
|
-
}
|
|
564
|
-
});
|
|
565
|
-
if (!allowed) {
|
|
566
|
-
const ask = this.config.execAsk;
|
|
567
|
-
throw new Error(ask
|
|
568
|
-
? '该命令不在允许列表中,需要审批后执行。当前版本请将命令加入 ai.agent.execAllowlist 或联系管理员。'
|
|
569
|
-
: '该命令不在允许列表中,已被拒绝执行。可将允许的命令模式加入 ai.agent.execAllowlist。');
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
/**
|
|
573
|
-
* 对传入的 Agent 工具列表应用 bash 执行策略(仅包装 bash 工具)。
|
|
574
|
-
*/
|
|
575
|
-
applyExecPolicyToTools(tools) {
|
|
576
|
-
return tools.map(t => {
|
|
577
|
-
if (t.name !== 'bash')
|
|
578
|
-
return t;
|
|
579
|
-
const original = t.execute;
|
|
580
|
-
return {
|
|
581
|
-
...t,
|
|
582
|
-
execute: async (args) => {
|
|
583
|
-
const cmd = args?.command != null ? String(args.command) : '';
|
|
584
|
-
this.checkExecPolicy(cmd);
|
|
585
|
-
return original(args);
|
|
586
|
-
},
|
|
587
|
-
};
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
// ── 辅助方法 ─────────────────────────────────────────────────────────
|
|
591
|
-
/**
|
|
592
|
-
* 将 Tool 转为 AgentTool,注入 ToolContext 以确保执行时鉴权生效。
|
|
593
|
-
*
|
|
594
|
-
* 当参数定义了 contextKey 时:
|
|
595
|
-
* 1. 从 AI 可见的 parameters 中移除该参数(减少 token、避免填错)
|
|
596
|
-
* 2. 执行时自动从 ToolContext 注入对应值,并按声明类型做类型转换
|
|
597
|
-
*/
|
|
598
|
-
toAgentTool(tool, context) {
|
|
599
|
-
const originalExecute = tool.execute;
|
|
600
|
-
// ── 收集需要自动注入的参数 ──────────────────────────────────
|
|
601
|
-
const contextInjections = [];
|
|
602
|
-
let cleanParameters = tool.parameters;
|
|
603
|
-
if (context && tool.parameters?.properties) {
|
|
604
|
-
const props = tool.parameters.properties;
|
|
605
|
-
const filteredProps = {};
|
|
606
|
-
const filteredRequired = [];
|
|
607
|
-
for (const [key, schema] of Object.entries(props)) {
|
|
608
|
-
if (schema.contextKey && context[schema.contextKey] != null) {
|
|
609
|
-
// 记录需要注入的映射
|
|
610
|
-
contextInjections.push({
|
|
611
|
-
paramName: key,
|
|
612
|
-
contextKey: schema.contextKey,
|
|
613
|
-
paramType: schema.type || 'string',
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
// 保留给 AI 的参数
|
|
618
|
-
filteredProps[key] = schema;
|
|
619
|
-
if (tool.parameters.required?.includes(key)) {
|
|
620
|
-
filteredRequired.push(key);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
if (contextInjections.length > 0) {
|
|
625
|
-
cleanParameters = {
|
|
626
|
-
...tool.parameters,
|
|
627
|
-
properties: filteredProps,
|
|
628
|
-
required: filteredRequired.length > 0 ? filteredRequired : undefined,
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
// ── 组装 AgentTool ──────────────────────────────────────────
|
|
633
|
-
const at = {
|
|
634
|
-
name: tool.name,
|
|
635
|
-
description: tool.description,
|
|
636
|
-
parameters: cleanParameters,
|
|
637
|
-
execute: context
|
|
638
|
-
? (args) => {
|
|
639
|
-
// 自动注入 context 值,按目标 type 做类型转换
|
|
640
|
-
const enrichedArgs = { ...args };
|
|
641
|
-
for (const { paramName, contextKey, paramType } of contextInjections) {
|
|
642
|
-
let value = context[contextKey];
|
|
643
|
-
if (paramType === 'number' && typeof value === 'string') {
|
|
644
|
-
value = Number(value);
|
|
645
|
-
}
|
|
646
|
-
else if (paramType === 'string' && typeof value !== 'string') {
|
|
647
|
-
value = String(value);
|
|
648
|
-
}
|
|
649
|
-
enrichedArgs[paramName] = value;
|
|
650
|
-
}
|
|
651
|
-
return originalExecute(enrichedArgs, context);
|
|
652
|
-
}
|
|
653
|
-
: originalExecute,
|
|
654
|
-
};
|
|
655
|
-
if (tool.tags?.length)
|
|
656
|
-
at.tags = tool.tags;
|
|
657
|
-
if (tool.keywords?.length)
|
|
658
|
-
at.keywords = tool.keywords;
|
|
659
|
-
if (tool.permissionLevel)
|
|
660
|
-
at.permissionLevel = PERM_MAP[tool.permissionLevel] ?? 0;
|
|
661
|
-
if (tool.preExecutable)
|
|
662
|
-
at.preExecutable = true;
|
|
663
|
-
if (tool.kind)
|
|
664
|
-
at.kind = tool.kind;
|
|
665
|
-
return at;
|
|
666
|
-
}
|
|
667
|
-
/**
|
|
668
|
-
* 构建结构化 System Prompt(借鉴 OpenClaw 的分段式设计)
|
|
669
|
-
*
|
|
670
|
-
* 段落结构:
|
|
671
|
-
* 1. 身份 + 人格
|
|
672
|
-
* 2. 安全准则
|
|
673
|
-
* 3. 工具调用风格
|
|
674
|
-
* 4. 技能列表(XML 格式)
|
|
675
|
-
* 5. 当前时间
|
|
676
|
-
* 6. 引导文件上下文(SOUL.md, TOOLS.md, AGENTS.md)
|
|
677
|
-
*/
|
|
678
|
-
/**
|
|
679
|
-
* 构建精简的 System Prompt — 专为小模型(8B/14B 级)优化
|
|
680
|
-
*
|
|
681
|
-
* 设计原则:
|
|
682
|
-
* - 控制在 300-500 token 内,为工具定义和历史留足空间
|
|
683
|
-
* - 规则用短句,不用段落
|
|
684
|
-
* - 不重复,不举例(模型能从工具定义中推断用法)
|
|
685
|
-
*/
|
|
686
|
-
buildRichSystemPrompt() {
|
|
687
|
-
const lines = [];
|
|
688
|
-
// §1 身份
|
|
689
|
-
lines.push(this.config.persona);
|
|
690
|
-
lines.push('');
|
|
691
|
-
// §2 核心规则(精简为 7 条短句)
|
|
692
|
-
lines.push('## 规则');
|
|
693
|
-
lines.push('1. 直接调用工具执行操作,不要描述步骤或解释意图');
|
|
694
|
-
lines.push('2. 时间/日期问题:直接用下方"当前时间"回答,不调工具');
|
|
695
|
-
lines.push('3. 修改文件必须调用 edit_file/write_file,禁止给手动教程');
|
|
696
|
-
lines.push('4. activate_skill 返回后,必须继续调用其中指导的工具,不要停');
|
|
697
|
-
lines.push('5. 所有回答必须基于工具返回的实际数据');
|
|
698
|
-
lines.push('6. 工具失败时尝试替代方案,不要直接把错误丢给用户');
|
|
699
|
-
lines.push('7. 只根据用户**最后一条**消息作答,前面的对话仅作背景');
|
|
700
|
-
lines.push('');
|
|
701
|
-
// §3 技能列表(紧凑格式)
|
|
702
|
-
if (this.skillRegistry && this.skillRegistry.size > 0) {
|
|
703
|
-
const skills = this.skillRegistry.getAll();
|
|
704
|
-
lines.push('## 可用技能');
|
|
705
|
-
for (const skill of skills) {
|
|
706
|
-
lines.push(`- ${skill.name}: ${skill.description}`);
|
|
707
|
-
}
|
|
708
|
-
lines.push('用户提到技能名 → 调用 activate_skill(name) → 按返回的指导执行工具');
|
|
709
|
-
lines.push('');
|
|
710
|
-
}
|
|
711
|
-
// §4 当前时间
|
|
712
|
-
const now = new Date();
|
|
713
|
-
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
714
|
-
const timeStr = now.toLocaleString('zh-CN', { timeZone: tz });
|
|
715
|
-
lines.push(`当前时间: ${timeStr} (${tz})`);
|
|
716
|
-
lines.push('');
|
|
717
|
-
// §5 引导文件上下文(SOUL.md, TOOLS.md, AGENTS.md)
|
|
718
|
-
if (this.bootstrapContext) {
|
|
719
|
-
lines.push(this.bootstrapContext);
|
|
720
|
-
}
|
|
721
|
-
return lines.filter(Boolean).join('\n');
|
|
722
|
-
}
|
|
723
|
-
// ── 内置工具 ─────────────────────────────────────────────────────────
|
|
724
|
-
/**
|
|
725
|
-
* 创建 chat_history 工具 — 让 AI 能主动搜索历史聊天记录
|
|
726
|
-
*/
|
|
727
|
-
createChatHistoryTool(sessionId) {
|
|
728
|
-
const memory = this.memory;
|
|
729
|
-
return {
|
|
730
|
-
name: 'chat_history',
|
|
731
|
-
description: '搜索与用户的历史聊天记录。可以按关键词搜索,也可以按对话轮次范围查询。当用户问到"之前聊过什么""我们讨论过什么"等回忆类问题时使用。',
|
|
732
|
-
parameters: {
|
|
733
|
-
type: 'object',
|
|
734
|
-
properties: {
|
|
735
|
-
keyword: {
|
|
736
|
-
type: 'string',
|
|
737
|
-
description: '搜索关键词(模糊匹配消息内容和摘要)。留空则返回最近几轮记录',
|
|
738
|
-
},
|
|
739
|
-
from_round: {
|
|
740
|
-
type: 'number',
|
|
741
|
-
description: '起始轮次(与 to_round 配合使用,精确查询某段对话)',
|
|
742
|
-
},
|
|
743
|
-
to_round: {
|
|
744
|
-
type: 'number',
|
|
745
|
-
description: '结束轮次',
|
|
746
|
-
},
|
|
747
|
-
},
|
|
748
|
-
required: ['keyword'],
|
|
749
|
-
},
|
|
750
|
-
tags: ['memory', 'history', '聊天记录', '回忆', '之前'],
|
|
751
|
-
keywords: ['之前', '历史', '聊过', '讨论过', '记得', '上次', '以前', '回忆'],
|
|
752
|
-
async execute(args) {
|
|
753
|
-
const { keyword, from_round, to_round } = args;
|
|
754
|
-
// 获取当前轮次用于提示
|
|
755
|
-
const currentRound = await memory.getCurrentRound(sessionId);
|
|
756
|
-
if (keyword) {
|
|
757
|
-
const result = await memory.traceByKeyword(sessionId, keyword);
|
|
758
|
-
const msgs = result.messages.map(m => {
|
|
759
|
-
const role = m.role === 'user' ? '用户' : '助手';
|
|
760
|
-
const time = new Date(m.time).toLocaleString('zh-CN');
|
|
761
|
-
return `[第${m.round}轮 ${time}] ${role}: ${m.content}`;
|
|
762
|
-
}).join('\n');
|
|
763
|
-
let output = `当前是第 ${currentRound} 轮对话。\n\n`;
|
|
764
|
-
if (result.summary) {
|
|
765
|
-
output += `📋 找到相关摘要(覆盖第${result.summary.fromRound}-${result.summary.toRound}轮):\n${result.summary.summary}\n\n`;
|
|
766
|
-
}
|
|
767
|
-
output += msgs ? `💬 相关聊天记录:\n${msgs}` : '未找到包含该关键词的聊天记录。';
|
|
768
|
-
return output;
|
|
769
|
-
}
|
|
770
|
-
if (from_round != null && to_round != null) {
|
|
771
|
-
const messages = await memory.getMessagesByRound(sessionId, from_round, to_round);
|
|
772
|
-
if (messages.length === 0) {
|
|
773
|
-
return `第 ${from_round}-${to_round} 轮没有聊天记录。当前是第 ${currentRound} 轮。`;
|
|
774
|
-
}
|
|
775
|
-
const msgs = messages.map(m => {
|
|
776
|
-
const role = m.role === 'user' ? '用户' : '助手';
|
|
777
|
-
const time = new Date(m.time).toLocaleString('zh-CN');
|
|
778
|
-
return `[第${m.round}轮 ${time}] ${role}: ${m.content}`;
|
|
779
|
-
}).join('\n');
|
|
780
|
-
return `第 ${from_round}-${to_round} 轮聊天记录(当前第 ${currentRound} 轮):\n${msgs}`;
|
|
781
|
-
}
|
|
782
|
-
// 无参数 → 返回最近几轮
|
|
783
|
-
const messages = await memory.getMessagesByRound(sessionId, Math.max(1, currentRound - 4), currentRound);
|
|
784
|
-
if (messages.length === 0) {
|
|
785
|
-
return '暂无聊天记录。';
|
|
786
|
-
}
|
|
787
|
-
const msgs = messages.map(m => {
|
|
788
|
-
const role = m.role === 'user' ? '用户' : '助手';
|
|
789
|
-
return `[第${m.round}轮] ${role}: ${m.content}`;
|
|
790
|
-
}).join('\n');
|
|
791
|
-
return `最近的聊天记录(当前第 ${currentRound} 轮):\n${msgs}`;
|
|
792
|
-
},
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
/**
|
|
796
|
-
* 创建 user_profile 工具 — 让 AI 读写用户画像
|
|
797
|
-
*/
|
|
798
|
-
createUserProfileTool(userId) {
|
|
799
|
-
const profiles = this.userProfiles;
|
|
800
|
-
return {
|
|
801
|
-
name: 'user_profile',
|
|
802
|
-
description: '读取或保存用户的个人偏好和信息。当用户告诉你他的名字、偏好、兴趣、习惯等个人信息时,用 set 操作保存。当需要了解用户偏好时,用 get 操作读取。',
|
|
803
|
-
parameters: {
|
|
804
|
-
type: 'object',
|
|
805
|
-
properties: {
|
|
806
|
-
action: {
|
|
807
|
-
type: 'string',
|
|
808
|
-
description: '操作类型: get(读取所有偏好), set(保存偏好), delete(删除偏好)',
|
|
809
|
-
enum: ['get', 'set', 'delete'],
|
|
810
|
-
},
|
|
811
|
-
key: {
|
|
812
|
-
type: 'string',
|
|
813
|
-
description: '偏好名称,如: name, style, interests, timezone, language 等',
|
|
814
|
-
},
|
|
815
|
-
value: {
|
|
816
|
-
type: 'string',
|
|
817
|
-
description: '偏好值(仅 set 操作需要)',
|
|
818
|
-
},
|
|
819
|
-
},
|
|
820
|
-
required: ['action'],
|
|
821
|
-
},
|
|
822
|
-
tags: ['profile', '偏好', '用户', '个性化', '记住'],
|
|
823
|
-
keywords: ['我叫', '我的名字', '记住我', '我喜欢', '我偏好', '我习惯', '叫我', '我是'],
|
|
824
|
-
async execute(args) {
|
|
825
|
-
const { action, key, value } = args;
|
|
826
|
-
switch (action) {
|
|
827
|
-
case 'get': {
|
|
828
|
-
const all = await profiles.getAll(userId);
|
|
829
|
-
const entries = Object.entries(all);
|
|
830
|
-
if (entries.length === 0)
|
|
831
|
-
return '暂无保存的用户偏好。';
|
|
832
|
-
return '用户偏好:\n' + entries.map(([k, v]) => ` ${k}: ${v}`).join('\n');
|
|
833
|
-
}
|
|
834
|
-
case 'set': {
|
|
835
|
-
if (!key || !value)
|
|
836
|
-
return '需要提供 key 和 value';
|
|
837
|
-
await profiles.set(userId, key, value);
|
|
838
|
-
return `已保存: ${key} = ${value}`;
|
|
839
|
-
}
|
|
840
|
-
case 'delete': {
|
|
841
|
-
if (!key)
|
|
842
|
-
return '需要提供 key';
|
|
843
|
-
const deleted = await profiles.delete(userId, key);
|
|
844
|
-
return deleted ? `已删除: ${key}` : `未找到偏好: ${key}`;
|
|
845
|
-
}
|
|
846
|
-
default:
|
|
847
|
-
return '不支持的操作,请使用 get/set/delete';
|
|
848
|
-
}
|
|
849
|
-
},
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* 创建 schedule_followup 工具 — 让 AI 主动安排跟进
|
|
854
|
-
*
|
|
855
|
-
* 任务持久化到数据库,机器人重启后自动恢复。
|
|
856
|
-
* 同一会话创建新提醒时,旧的 pending 提醒会被自动取消。
|
|
857
|
-
*/
|
|
858
|
-
createScheduleFollowUpTool(sessionId, context) {
|
|
859
|
-
const followUps = this.followUps;
|
|
860
|
-
const platform = context.platform || '';
|
|
861
|
-
const botId = context.botId || '';
|
|
862
|
-
const senderId = context.senderId || '';
|
|
863
|
-
const sceneId = context.sceneId || '';
|
|
864
|
-
const sceneType = context.message?.$channel?.type || 'private';
|
|
865
|
-
return {
|
|
866
|
-
name: 'schedule_followup',
|
|
867
|
-
description: '安排或取消定时跟进提醒。创建新提醒会自动取消之前的提醒。提醒持久保存,重启不丢失。',
|
|
868
|
-
parameters: {
|
|
869
|
-
type: 'object',
|
|
870
|
-
properties: {
|
|
871
|
-
action: {
|
|
872
|
-
type: 'string',
|
|
873
|
-
description: '操作类型: create(创建提醒,默认)或 cancel(取消当前会话所有提醒)',
|
|
874
|
-
enum: ['create', 'cancel'],
|
|
875
|
-
},
|
|
876
|
-
delay_minutes: {
|
|
877
|
-
type: 'number',
|
|
878
|
-
description: '延迟时间,单位是分钟。注意:3 就是 3 分钟,不是 3 小时。举例: 3 = 3分钟后, 60 = 1小时后, 1440 = 1天后',
|
|
879
|
-
},
|
|
880
|
-
message: {
|
|
881
|
-
type: 'string',
|
|
882
|
-
description: '提醒消息内容',
|
|
883
|
-
},
|
|
884
|
-
},
|
|
885
|
-
required: ['action'],
|
|
886
|
-
},
|
|
887
|
-
tags: ['reminder', '提醒', '跟进', '定时'],
|
|
888
|
-
keywords: ['提醒', '提醒我', '过一会', '过一小时', '明天', '跟进', '别忘了', '记得提醒', '取消提醒'],
|
|
889
|
-
async execute(args) {
|
|
890
|
-
const { action = 'create', delay_minutes, message: msg } = args;
|
|
891
|
-
if (action === 'cancel') {
|
|
892
|
-
const count = await followUps.cancelBySession(sessionId);
|
|
893
|
-
return count > 0
|
|
894
|
-
? `✅ 已取消 ${count} 个待执行的提醒`
|
|
895
|
-
: '当前没有待执行的提醒';
|
|
896
|
-
}
|
|
897
|
-
// create
|
|
898
|
-
if (!delay_minutes || delay_minutes <= 0)
|
|
899
|
-
return '延迟时间必须大于 0 分钟';
|
|
900
|
-
if (!msg)
|
|
901
|
-
return '请提供提醒内容';
|
|
902
|
-
return followUps.schedule({
|
|
903
|
-
sessionId,
|
|
904
|
-
platform,
|
|
905
|
-
botId,
|
|
906
|
-
senderId,
|
|
907
|
-
sceneId,
|
|
908
|
-
sceneType,
|
|
909
|
-
message: msg,
|
|
910
|
-
delayMinutes: delay_minutes,
|
|
911
|
-
});
|
|
912
|
-
},
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
// ── 会话记忆(基于 ConversationMemory) ─────────────────────────────
|
|
916
|
-
/**
|
|
917
|
-
* 从 ConversationMemory 构建上下文
|
|
918
|
-
*/
|
|
327
|
+
// ── Internal helpers ────────────────────────────────────────────────
|
|
919
328
|
async buildHistoryMessages(sessionId) {
|
|
920
329
|
return this.memory.buildContext(sessionId);
|
|
921
330
|
}
|
|
922
|
-
/**
|
|
923
|
-
* 流式聊天(带历史记忆) — 利用 chatStream 减少 TTFT
|
|
924
|
-
*
|
|
925
|
-
* 新增 onChunk 回调:每收到一个 token 立即通知调用方,
|
|
926
|
-
* 支持适配器(Telegram/Discord/Kook)实时编辑消息。
|
|
927
|
-
*/
|
|
928
331
|
async streamChatWithHistory(content, systemPrompt, history, onChunk) {
|
|
929
332
|
const model = this.provider.models[0];
|
|
930
333
|
const userContent = history.length > 0
|
|
@@ -934,7 +337,6 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
|
|
|
934
337
|
{ role: 'system', content: systemPrompt },
|
|
935
338
|
{ role: 'user', content: userContent },
|
|
936
339
|
];
|
|
937
|
-
// 优先流式(对 Ollama 等本地模型有明显提速)
|
|
938
340
|
try {
|
|
939
341
|
let result = '';
|
|
940
342
|
for await (const chunk of this.provider.chatStream({ model, messages })) {
|
|
@@ -948,7 +350,6 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
|
|
|
948
350
|
return result;
|
|
949
351
|
}
|
|
950
352
|
catch {
|
|
951
|
-
// fallback 非流式
|
|
952
353
|
const response = await this.provider.chat({ model, messages });
|
|
953
354
|
const msg = response.choices[0]?.message?.content;
|
|
954
355
|
const result = typeof msg === 'string' ? msg : '';
|
|
@@ -958,12 +359,9 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
|
|
|
958
359
|
}
|
|
959
360
|
}
|
|
960
361
|
async saveToSession(sessionId, userContent, assistantContent, sceneId) {
|
|
961
|
-
// 1. 保存到 ConversationMemory(含异步摘要判断)
|
|
962
362
|
await this.memory.saveRound(sessionId, userContent, assistantContent);
|
|
963
|
-
// 2. 保存到 SessionManager(兼容旧逻辑)
|
|
964
363
|
await this.sessions.addMessage(sessionId, { role: 'user', content: userContent });
|
|
965
364
|
await this.sessions.addMessage(sessionId, { role: 'assistant', content: assistantContent });
|
|
966
|
-
// 3. ContextManager 场景摘要(如有)
|
|
967
365
|
if (this.contextManager && sceneId) {
|
|
968
366
|
this.contextManager.autoSummarizeIfNeeded(sceneId).catch(() => { });
|
|
969
367
|
}
|
|
@@ -971,10 +369,8 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
|
|
|
971
369
|
fallbackFormat(toolCalls) {
|
|
972
370
|
if (toolCalls.length === 0)
|
|
973
371
|
return '处理完成。';
|
|
974
|
-
// 过滤掉 activate_skill 的结果(是 SKILL.md 指令,不应暴露给用户)
|
|
975
372
|
const userFacing = toolCalls.filter(tc => tc.tool !== 'activate_skill');
|
|
976
373
|
if (userFacing.length === 0) {
|
|
977
|
-
// 只有 activate_skill 被调用但后续工具未执行 — 说明技能激活后流程中断
|
|
978
374
|
return '技能已激活但未能完成后续操作,请重试或换一种方式描述你的需求。';
|
|
979
375
|
}
|
|
980
376
|
return userFacing.map(tc => {
|
|
@@ -982,9 +378,9 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
|
|
|
982
378
|
return `【${tc.tool}】\n${s}`;
|
|
983
379
|
}).join('\n\n');
|
|
984
380
|
}
|
|
985
|
-
// ──
|
|
381
|
+
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
986
382
|
isReady() {
|
|
987
|
-
return true;
|
|
383
|
+
return true;
|
|
988
384
|
}
|
|
989
385
|
dispose() {
|
|
990
386
|
this.memory.dispose();
|
|
@@ -993,6 +389,10 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
|
|
|
993
389
|
this.userProfiles.dispose();
|
|
994
390
|
this.rateLimiter.dispose();
|
|
995
391
|
this.followUps.dispose();
|
|
392
|
+
if (this.subagentManager) {
|
|
393
|
+
this.subagentManager.dispose();
|
|
394
|
+
this.subagentManager = null;
|
|
395
|
+
}
|
|
996
396
|
}
|
|
997
397
|
}
|
|
998
398
|
//# sourceMappingURL=zhin-agent.js.map
|