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