@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.
Files changed (113) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/lib/ai/agent.d.ts.map +1 -1
  3. package/lib/ai/agent.js +15 -2
  4. package/lib/ai/agent.js.map +1 -1
  5. package/lib/ai/bootstrap.d.ts +11 -2
  6. package/lib/ai/bootstrap.d.ts.map +1 -1
  7. package/lib/ai/bootstrap.js +46 -2
  8. package/lib/ai/bootstrap.js.map +1 -1
  9. package/lib/ai/builtin-tools.d.ts +28 -6
  10. package/lib/ai/builtin-tools.d.ts.map +1 -1
  11. package/lib/ai/builtin-tools.js +265 -76
  12. package/lib/ai/builtin-tools.js.map +1 -1
  13. package/lib/ai/index.d.ts +9 -1
  14. package/lib/ai/index.d.ts.map +1 -1
  15. package/lib/ai/index.js +8 -0
  16. package/lib/ai/index.js.map +1 -1
  17. package/lib/ai/init.d.ts.map +1 -1
  18. package/lib/ai/init.js +84 -3
  19. package/lib/ai/init.js.map +1 -1
  20. package/lib/ai/providers/anthropic.d.ts +7 -0
  21. package/lib/ai/providers/anthropic.d.ts.map +1 -1
  22. package/lib/ai/providers/anthropic.js +3 -0
  23. package/lib/ai/providers/anthropic.js.map +1 -1
  24. package/lib/ai/providers/ollama.d.ts +10 -0
  25. package/lib/ai/providers/ollama.d.ts.map +1 -1
  26. package/lib/ai/providers/ollama.js +11 -3
  27. package/lib/ai/providers/ollama.js.map +1 -1
  28. package/lib/ai/providers/openai.d.ts +7 -0
  29. package/lib/ai/providers/openai.d.ts.map +1 -1
  30. package/lib/ai/providers/openai.js +3 -0
  31. package/lib/ai/providers/openai.js.map +1 -1
  32. package/lib/ai/service.d.ts +4 -0
  33. package/lib/ai/service.d.ts.map +1 -1
  34. package/lib/ai/service.js +7 -0
  35. package/lib/ai/service.js.map +1 -1
  36. package/lib/ai/subagent.d.ts +50 -0
  37. package/lib/ai/subagent.d.ts.map +1 -0
  38. package/lib/ai/subagent.js +144 -0
  39. package/lib/ai/subagent.js.map +1 -0
  40. package/lib/ai/types.d.ts +25 -5
  41. package/lib/ai/types.d.ts.map +1 -1
  42. package/lib/ai/zhin-agent-builtin-tools.d.ts +17 -0
  43. package/lib/ai/zhin-agent-builtin-tools.d.ts.map +1 -0
  44. package/lib/ai/zhin-agent-builtin-tools.js +220 -0
  45. package/lib/ai/zhin-agent-builtin-tools.js.map +1 -0
  46. package/lib/ai/zhin-agent-config.d.ts +54 -0
  47. package/lib/ai/zhin-agent-config.d.ts.map +1 -0
  48. package/lib/ai/zhin-agent-config.js +76 -0
  49. package/lib/ai/zhin-agent-config.js.map +1 -0
  50. package/lib/ai/zhin-agent-exec-policy.d.ts +20 -0
  51. package/lib/ai/zhin-agent-exec-policy.d.ts.map +1 -0
  52. package/lib/ai/zhin-agent-exec-policy.js +71 -0
  53. package/lib/ai/zhin-agent-exec-policy.js.map +1 -0
  54. package/lib/ai/zhin-agent-prompt.d.ts +21 -0
  55. package/lib/ai/zhin-agent-prompt.d.ts.map +1 -0
  56. package/lib/ai/zhin-agent-prompt.js +116 -0
  57. package/lib/ai/zhin-agent-prompt.js.map +1 -0
  58. package/lib/ai/zhin-agent-tool-collector.d.ts +22 -0
  59. package/lib/ai/zhin-agent-tool-collector.d.ts.map +1 -0
  60. package/lib/ai/zhin-agent-tool-collector.js +218 -0
  61. package/lib/ai/zhin-agent-tool-collector.js.map +1 -0
  62. package/lib/ai/zhin-agent.d.ts +11 -155
  63. package/lib/ai/zhin-agent.d.ts.map +1 -1
  64. package/lib/ai/zhin-agent.js +84 -684
  65. package/lib/ai/zhin-agent.js.map +1 -1
  66. package/lib/component.d.ts.map +1 -1
  67. package/lib/component.js +19 -19
  68. package/lib/component.js.map +1 -1
  69. package/lib/index.d.ts +1 -0
  70. package/lib/index.d.ts.map +1 -1
  71. package/lib/index.js +1 -0
  72. package/lib/index.js.map +1 -1
  73. package/lib/scheduler/index.d.ts +10 -0
  74. package/lib/scheduler/index.d.ts.map +1 -0
  75. package/lib/scheduler/index.js +12 -0
  76. package/lib/scheduler/index.js.map +1 -0
  77. package/lib/scheduler/scheduler.d.ts +49 -0
  78. package/lib/scheduler/scheduler.d.ts.map +1 -0
  79. package/lib/scheduler/scheduler.js +352 -0
  80. package/lib/scheduler/scheduler.js.map +1 -0
  81. package/lib/scheduler/types.d.ts +71 -0
  82. package/lib/scheduler/types.d.ts.map +1 -0
  83. package/lib/scheduler/types.js +8 -0
  84. package/lib/scheduler/types.js.map +1 -0
  85. package/lib/tool-zod.d.ts +28 -0
  86. package/lib/tool-zod.d.ts.map +1 -0
  87. package/lib/tool-zod.js +98 -0
  88. package/lib/tool-zod.js.map +1 -0
  89. package/package.json +9 -4
  90. package/src/ai/agent.ts +15 -2
  91. package/src/ai/bootstrap.ts +48 -2
  92. package/src/ai/builtin-tools.ts +283 -75
  93. package/src/ai/index.ts +19 -1
  94. package/src/ai/init.ts +85 -3
  95. package/src/ai/providers/anthropic.ts +3 -0
  96. package/src/ai/providers/ollama.ts +13 -3
  97. package/src/ai/providers/openai.ts +3 -0
  98. package/src/ai/service.ts +8 -0
  99. package/src/ai/subagent.ts +209 -0
  100. package/src/ai/types.ts +29 -2
  101. package/src/ai/zhin-agent-builtin-tools.ts +247 -0
  102. package/src/ai/zhin-agent-config.ts +113 -0
  103. package/src/ai/zhin-agent-exec-policy.ts +78 -0
  104. package/src/ai/zhin-agent-prompt.ts +136 -0
  105. package/src/ai/zhin-agent-tool-collector.ts +243 -0
  106. package/src/ai/zhin-agent.ts +113 -791
  107. package/src/component.ts +29 -28
  108. package/src/index.ts +1 -0
  109. package/src/scheduler/index.ts +28 -0
  110. package/src/scheduler/scheduler.ts +372 -0
  111. package/src/scheduler/types.ts +74 -0
  112. package/src/tool-zod.ts +115 -0
  113. package/tests/ai/subagent.test.ts +270 -0
@@ -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 { Agent, createAgent } from './agent.js';
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
- /** 引导文件上下文(SOUL.md + TOOLS.md + AGENTS.md) */
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
- /** 获取 UserProfileStore(用于外部注册) */
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
- * @param content 用户消息文本
169
- * @param context 工具上下文(平台、发送者、权限等)
170
- * @param externalTools 外部传入的工具列表
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
- // ══════ 0. 速率限制检查 ══════
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
- // ══════ 1. 收集工具 — 两级过滤 ══════
150
+ // 1. Collect tools
203
151
  const tFilter = now();
204
- const allTools = this.collectTools(content, context, externalTools);
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(this.createChatHistoryTool(sessionId));
159
+ allTools.push(createChatHistoryTool(sessionId, this.memory));
208
160
  }
209
161
  if (/偏好|设置|配置|档案|资料|时区|timezone|profile|喜好|我叫|叫我|记住我/i.test(content)) {
210
- allTools.push(this.createUserProfileTool(userId));
162
+ allTools.push(createUserProfileTool(userId, this.userProfiles));
211
163
  }
212
164
  if (/提醒|定时|过一会|跟进|别忘|取消提醒|reminder|分钟后|小时后/i.test(content)) {
213
- allTools.push(this.createScheduleFollowUpTool(sessionId, context));
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
- // ══════ 2. 构建会话记忆 + 用户画像 ══════
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
- // ══════ 2.5 用户画像 & 情绪感知 ══════
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.buildEnhancedPersona(profileSummary, toneHint);
236
- // ══════ 3. 无工具闲聊路径 (轻量 prompt + 历史) ══════
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
- // ══════ 4. 拆分可预执行 / 普通工具 ══════
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
- // ══════ 5. 预执行标记的工具 ══════
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
- // ══════ 6. 路径选择 ══════
230
+ // 6. Path selection
279
231
  let reply;
280
- // 判断是否所有工具都已被预执行(即没有非预执行工具)
281
232
  const hasNonPreExecTools = allTools.some(t => !t.preExecutable);
282
233
  if (!hasNonPreExecTools && preData) {
283
- // ── 快速路径: 所有工具都已预执行 → 1 轮 AI ──
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
- // ── Agent 路径: 需要 LLM 决策调用哪些工具 → 多轮 ──
246
+ // Agent path
296
247
  const tAgent = now();
297
248
  logger.debug(`Agent 路径: ${allTools.length} 个工具`);
298
- const contextHint = this.buildContextHint(context, content);
299
- // 使用结构化系统提示(包含时间、安全准则、技能列表等)
300
- const richPrompt = this.buildRichSystemPrompt();
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
- // 始终传递所有工具给 Agent,因为 activate_skill 激活后可能需要调用
305
- // 之前被分类为 noParamTools 的工具(确保技能中引用的所有工具都可用)
306
- const agentTools = this.applyExecPolicyToTools(allTools);
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: this.config.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.buildEnhancedPersona(profileSummary, '');
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
- // ── 增强人格(注入画像 + 情绪 hint + 引导上下文) ──────────────────
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; // provider is required in constructor
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