@zhin.js/core 1.0.26 → 1.0.28

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 (68) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/lib/ai/agent.d.ts.map +1 -1
  3. package/lib/ai/agent.js +4 -0
  4. package/lib/ai/agent.js.map +1 -1
  5. package/lib/ai/bootstrap.d.ts +82 -0
  6. package/lib/ai/bootstrap.d.ts.map +1 -0
  7. package/lib/ai/bootstrap.js +199 -0
  8. package/lib/ai/bootstrap.js.map +1 -0
  9. package/lib/ai/builtin-tools.d.ts +36 -0
  10. package/lib/ai/builtin-tools.d.ts.map +1 -0
  11. package/lib/ai/builtin-tools.js +509 -0
  12. package/lib/ai/builtin-tools.js.map +1 -0
  13. package/lib/ai/compaction.d.ts +132 -0
  14. package/lib/ai/compaction.d.ts.map +1 -0
  15. package/lib/ai/compaction.js +370 -0
  16. package/lib/ai/compaction.js.map +1 -0
  17. package/lib/ai/hooks.d.ts +143 -0
  18. package/lib/ai/hooks.d.ts.map +1 -0
  19. package/lib/ai/hooks.js +108 -0
  20. package/lib/ai/hooks.js.map +1 -0
  21. package/lib/ai/index.d.ts +6 -0
  22. package/lib/ai/index.d.ts.map +1 -1
  23. package/lib/ai/index.js +6 -0
  24. package/lib/ai/index.js.map +1 -1
  25. package/lib/ai/init.d.ts.map +1 -1
  26. package/lib/ai/init.js +120 -3
  27. package/lib/ai/init.js.map +1 -1
  28. package/lib/ai/types.d.ts +2 -0
  29. package/lib/ai/types.d.ts.map +1 -1
  30. package/lib/ai/zhin-agent.d.ts +28 -1
  31. package/lib/ai/zhin-agent.d.ts.map +1 -1
  32. package/lib/ai/zhin-agent.js +196 -57
  33. package/lib/ai/zhin-agent.js.map +1 -1
  34. package/lib/built/config.d.ts +10 -0
  35. package/lib/built/config.d.ts.map +1 -1
  36. package/lib/built/config.js +54 -3
  37. package/lib/built/config.js.map +1 -1
  38. package/lib/built/tool.d.ts +6 -0
  39. package/lib/built/tool.d.ts.map +1 -1
  40. package/lib/built/tool.js +12 -0
  41. package/lib/built/tool.js.map +1 -1
  42. package/lib/cron.d.ts +0 -27
  43. package/lib/cron.d.ts.map +1 -1
  44. package/lib/cron.js +28 -27
  45. package/lib/cron.js.map +1 -1
  46. package/lib/types-generator.js +1 -1
  47. package/lib/types-generator.js.map +1 -1
  48. package/lib/types.d.ts +7 -0
  49. package/lib/types.d.ts.map +1 -1
  50. package/package.json +6 -6
  51. package/src/ai/agent.ts +6 -0
  52. package/src/ai/bootstrap.ts +263 -0
  53. package/src/ai/builtin-tools.ts +569 -0
  54. package/src/ai/compaction.ts +529 -0
  55. package/src/ai/hooks.ts +223 -0
  56. package/src/ai/index.ts +58 -0
  57. package/src/ai/init.ts +127 -3
  58. package/src/ai/types.ts +2 -0
  59. package/src/ai/zhin-agent.ts +226 -54
  60. package/src/built/config.ts +53 -3
  61. package/src/built/tool.ts +12 -0
  62. package/src/cron.ts +28 -27
  63. package/src/types-generator.ts +1 -1
  64. package/src/types.ts +8 -0
  65. package/tests/adapter.test.ts +1 -1
  66. package/tests/config.test.ts +2 -2
  67. package/test/minimal-bot.ts +0 -31
  68. package/test/stress-test.ts +0 -123
@@ -37,6 +37,15 @@ import { UserProfileStore } from './user-profile.js';
37
37
  import { RateLimiter, type RateLimitConfig } from './rate-limiter.js';
38
38
  import { detectTone } from './tone-detector.js';
39
39
  import { FollowUpManager, type FollowUpSender } from './follow-up.js';
40
+ import {
41
+ compactSession,
42
+ estimateMessagesTokens,
43
+ pruneHistoryForContext,
44
+ resolveContextWindowTokens,
45
+ evaluateContextWindowGuard,
46
+ DEFAULT_CONTEXT_TOKENS,
47
+ } from './compaction.js';
48
+ import { triggerAIHook, createAIHookEvent } from './hooks.js';
40
49
 
41
50
  const logger = new Logger(null, 'ZhinAgent');
42
51
 
@@ -72,6 +81,10 @@ export interface ZhinAgentConfig {
72
81
  toneAwareness?: boolean;
73
82
  /** 视觉模型名称(如 llava, bakllava),留空则不启用视觉 */
74
83
  visionModel?: string;
84
+ /** 上下文窗口 token 数(默认 128000) */
85
+ contextTokens?: number;
86
+ /** 历史记录最大占比(默认 0.5 = 50%) */
87
+ maxHistoryShare?: number;
75
88
  }
76
89
 
77
90
  const DEFAULT_CONFIG: Required<ZhinAgentConfig> = {
@@ -87,6 +100,8 @@ const DEFAULT_CONFIG: Required<ZhinAgentConfig> = {
87
100
  rateLimit: {},
88
101
  toneAwareness: true,
89
102
  visionModel: '',
103
+ contextTokens: DEFAULT_CONTEXT_TOKENS,
104
+ maxHistoryShare: 0.5,
90
105
  };
91
106
 
92
107
  // ============================================================================
@@ -128,6 +143,8 @@ export class ZhinAgent {
128
143
  private userProfiles: UserProfileStore;
129
144
  private rateLimiter: RateLimiter;
130
145
  private followUps: FollowUpManager;
146
+ /** 引导文件上下文(SOUL.md + TOOLS.md + AGENTS.md) */
147
+ private bootstrapContext: string = '';
131
148
 
132
149
  constructor(provider: AIProvider, config?: ZhinAgentConfig) {
133
150
  this.provider = provider;
@@ -199,6 +216,15 @@ export class ZhinAgent {
199
216
  return () => { this.externalTools.delete(tool.name); };
200
217
  }
201
218
 
219
+ /**
220
+ * 注入引导文件上下文(SOUL.md + TOOLS.md + AGENTS.md 的合并内容)
221
+ * 由 init.ts 在加载引导文件后调用
222
+ */
223
+ setBootstrapContext(context: string): void {
224
+ this.bootstrapContext = context;
225
+ logger.debug(`Bootstrap context set (${context.length} chars)`);
226
+ }
227
+
202
228
  // ── 核心处理入口 ─────────────────────────────────────────────────────
203
229
 
204
230
  /**
@@ -239,20 +265,47 @@ export class ZhinAgent {
239
265
  return parseOutput(rateCheck.message || '请稍后再试');
240
266
  }
241
267
 
268
+ // 触发 message:received hook
269
+ triggerAIHook(createAIHookEvent('message', 'received', sessionId, {
270
+ userId,
271
+ content,
272
+ platform: platform || '',
273
+ })).catch(() => {});
274
+
242
275
  // ══════ 1. 收集工具 — 两级过滤 ══════
243
276
  const tFilter = now();
244
277
  const allTools = this.collectTools(content, context, externalTools);
245
278
 
246
- // 注入内置工具
247
- allTools.push(this.createChatHistoryTool(sessionId));
248
- allTools.push(this.createUserProfileTool(userId));
249
- allTools.push(this.createScheduleFollowUpTool(sessionId, context));
279
+ // 按需注入内置工具 — 只在消息匹配关键词时注入,避免污染小模型的上下文
280
+ if (/之前|上次|历史|回忆|聊过|记录|还记得|曾经/i.test(content)) {
281
+ allTools.push(this.createChatHistoryTool(sessionId));
282
+ }
283
+ if (/偏好|设置|配置|档案|资料|时区|timezone|profile|喜好|我叫|叫我|记住我/i.test(content)) {
284
+ allTools.push(this.createUserProfileTool(userId));
285
+ }
286
+ if (/提醒|定时|过一会|跟进|别忘|取消提醒|reminder|分钟后|小时后/i.test(content)) {
287
+ allTools.push(this.createScheduleFollowUpTool(sessionId, context));
288
+ }
250
289
 
251
290
  const filterMs = (now() - tFilter).toFixed(0);
252
291
 
253
292
  // ══════ 2. 构建会话记忆 + 用户画像 ══════
254
293
  const tMem = now();
255
- const historyMessages = await this.buildHistoryMessages(sessionId);
294
+ let historyMessages = await this.buildHistoryMessages(sessionId);
295
+
296
+ // 上下文窗口保护:按 token 预算修剪历史(借鉴 OpenClaw context-window-guard)
297
+ const contextTokens = this.config.contextTokens ?? DEFAULT_CONTEXT_TOKENS;
298
+ const maxHistoryShare = this.config.maxHistoryShare ?? 0.5;
299
+ const pruneResult = pruneHistoryForContext({
300
+ messages: historyMessages,
301
+ maxContextTokens: contextTokens,
302
+ maxHistoryShare,
303
+ });
304
+ historyMessages = pruneResult.messages;
305
+ if (pruneResult.droppedCount > 0) {
306
+ logger.debug(`[上下文窗口] 丢弃 ${pruneResult.droppedCount} 条历史消息 (${pruneResult.droppedTokens} tokens)`);
307
+ }
308
+
256
309
  const memMs = (now() - tMem).toFixed(0);
257
310
 
258
311
  // ══════ 2.5 用户画像 & 情绪感知 ══════
@@ -273,21 +326,20 @@ export class ZhinAgent {
273
326
 
274
327
  logger.debug(`[工具路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, ${allTools.length} 工具 (${allTools.map(t => t.name).join(', ')})`);
275
328
 
276
- // ══════ 4. 拆分无参数 / 有参数工具 ══════
277
- const noParamTools: AgentTool[] = [];
278
- const paramTools: AgentTool[] = [];
329
+ // ══════ 4. 拆分可预执行 / 普通工具 ══════
330
+ // 只有显式标记 preExecutable=true 的工具才会被预执行(opt-in 模式)
331
+ const preExecTools: AgentTool[] = [];
279
332
  for (const tool of allTools) {
280
- const required = tool.parameters?.required;
281
- (!required || required.length === 0) ? noParamTools.push(tool) : paramTools.push(tool);
333
+ if (tool.preExecutable) preExecTools.push(tool);
282
334
  }
283
335
 
284
- // ══════ 5. 预执行无参数工具 ══════
336
+ // ══════ 5. 预执行标记的工具 ══════
285
337
  let preData = '';
286
- if (noParamTools.length > 0) {
338
+ if (preExecTools.length > 0) {
287
339
  const tPre = now();
288
- logger.debug(`预执行: ${noParamTools.map(t => t.name).join(', ')}`);
340
+ logger.debug(`预执行: ${preExecTools.map(t => t.name).join(', ')}`);
289
341
  const results = await Promise.allSettled(
290
- noParamTools.map(async (tool) => {
342
+ preExecTools.map(async (tool) => {
291
343
  const result = await Promise.race([
292
344
  tool.execute({}),
293
345
  new Promise<never>((_, rej) =>
@@ -298,7 +350,11 @@ export class ZhinAgent {
298
350
  );
299
351
  for (const r of results) {
300
352
  if (r.status === 'fulfilled') {
301
- const s = typeof r.value.result === 'string' ? r.value.result : JSON.stringify(r.value.result);
353
+ let s = typeof r.value.result === 'string' ? r.value.result : JSON.stringify(r.value.result);
354
+ // 限制单条预执行结果的长度,防止注入过多数据干扰模型
355
+ if (s.length > 500) {
356
+ s = s.slice(0, 500) + `\n... (truncated, ${s.length} chars total)`;
357
+ }
302
358
  preData += `\n【${r.value.name}】${s}`;
303
359
  }
304
360
  }
@@ -308,8 +364,11 @@ export class ZhinAgent {
308
364
  // ══════ 6. 路径选择 ══════
309
365
  let reply: string;
310
366
 
311
- if (paramTools.length === 0 && preData) {
312
- // ── 快速路径: 只有预执行数据 1 轮 AI ──
367
+ // 判断是否所有工具都已被预执行(即没有非预执行工具)
368
+ const hasNonPreExecTools = allTools.some(t => !t.preExecutable);
369
+
370
+ if (!hasNonPreExecTools && preData) {
371
+ // ── 快速路径: 所有工具都已预执行 → 1 轮 AI ──
313
372
  const tLLM = now();
314
373
  const prompt = `${personaEnhanced}
315
374
 
@@ -320,25 +379,20 @@ ${preData}
320
379
  reply = await this.streamChatWithHistory(content, prompt, historyMessages, onChunk);
321
380
  logger.info(`[快速路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, LLM=${(now() - tLLM).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
322
381
  } else {
323
- // ── Agent 路径: 需要参数的工具 → 多轮 ──
382
+ // ── Agent 路径: 需要 LLM 决策调用哪些工具 → 多轮 ──
324
383
  const tAgent = now();
325
- logger.debug(`Agent 路径: ${paramTools.length} 个参数工具`);
384
+ logger.debug(`Agent 路径: ${allTools.length} 个工具`);
326
385
  const contextHint = this.buildContextHint(context, content);
327
- const systemPrompt = `${personaEnhanced}
386
+
387
+ // 使用结构化系统提示(包含时间、安全准则、技能列表等)
388
+ const richPrompt = this.buildRichSystemPrompt();
389
+ const systemPrompt = `${richPrompt}
328
390
  ${contextHint}
329
- ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
330
- ## 工作流程
331
- 1. 分析用户的问题
332
- 2. 如果已获取的数据能回答问题,直接作答
333
- 3. 如果还需要更多信息,调用工具获取(直接调用,不要解释)
334
- 4. 获取工具结果后,**务必**生成一条完整、自然的中文回答
335
-
336
- ## 关键要求
337
- - 调用工具后你**必须**基于结果给出完整回答,绝不能返回空内容
338
- - 用自然语言总结工具结果,突出关键信息
339
- - 适当使用 emoji 让回答更生动`;
340
-
341
- const agentTools = paramTools.length > 0 ? paramTools : allTools;
391
+ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
392
+
393
+ // 始终传递所有工具给 Agent,因为 activate_skill 激活后可能需要调用
394
+ // 之前被分类为 noParamTools 的工具(确保技能中引用的所有工具都可用)
395
+ const agentTools = allTools;
342
396
  const agent = createAgent(this.provider, {
343
397
  systemPrompt,
344
398
  tools: agentTools,
@@ -352,6 +406,14 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
352
406
  }
353
407
 
354
408
  await this.saveToSession(sessionId, content, reply, sceneId);
409
+
410
+ // 触发 message:sent hook
411
+ triggerAIHook(createAIHookEvent('message', 'sent', sessionId, {
412
+ userId,
413
+ content: reply,
414
+ platform: platform || '',
415
+ })).catch(() => {});
416
+
355
417
  return parseOutput(reply);
356
418
  }
357
419
 
@@ -416,7 +478,7 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
416
478
  return parseOutput(reply);
417
479
  }
418
480
 
419
- // ── 增强人格(注入画像 + 情绪 hint ────────────────────────────────
481
+ // ── 增强人格(注入画像 + 情绪 hint + 引导上下文) ──────────────────
420
482
 
421
483
  private buildEnhancedPersona(profileSummary: string, toneHint: string): string {
422
484
  let persona = this.config.persona;
@@ -426,23 +488,23 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
426
488
  if (toneHint) {
427
489
  persona += `\n\n[语气提示] ${toneHint}`;
428
490
  }
491
+ // 注入当前时间(所有路径都需要,闲聊/快速/Agent 路径共用)
492
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
493
+ const timeStr = new Date().toLocaleString('zh-CN', { timeZone: tz });
494
+ persona += `\n\n当前时间: ${timeStr} (${tz})`;
429
495
  return persona;
430
496
  }
431
497
 
432
498
  /**
433
499
  * 构建上下文提示 — 告诉 AI 当前身份和场景,帮助工具参数填充
434
500
  */
435
- private buildContextHint(context: ToolContext, content: string): string {
501
+ private buildContextHint(context: ToolContext, _content: string): string {
436
502
  const parts: string[] = [];
437
- if (context.botId) parts.push(`你(Bot) 的 ID: ${context.botId}`);
438
- if (context.platform) parts.push(`平台: ${context.platform}`);
439
- if (context.senderId) parts.push(`发言者 ID: ${context.senderId}`);
440
- if (context.senderPermissionLevel) parts.push(`发言者权限: ${context.senderPermissionLevel}`);
441
- if (context.scope) parts.push(`场景类型: ${context.scope}`);
442
- if (context.sceneId) parts.push(`场景 ID: ${context.sceneId}`);
443
- if (content) parts.push(`发言内容: ${content}`);
503
+ if (context.platform) parts.push(`平台:${context.platform}`);
504
+ if (context.senderId) parts.push(`用户:${context.senderId}`);
505
+ if (context.scope) parts.push(`场景:${context.scope}`);
444
506
  if (parts.length === 0) return '';
445
- return `\n## 当前上下文\n${parts.map(p => `- ${p}`).join('\n')}\n这些信息将用于帮助你更好地理解用户需求和执行操作,请不要忽略这些信息,并确保用户的信息不会覆盖这些信息`;
507
+ return `\n上下文: ${parts.join(' | ')}`;
446
508
  }
447
509
 
448
510
  // ── 工具收集: 两级过滤 (Skill → Tool) ─────────────────────────────────
@@ -459,10 +521,41 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
459
521
  const collected: AgentTool[] = [];
460
522
  const collectedNames = new Set<string>(); // 用 Set 加速去重
461
523
 
524
+ // 0. 检测用户是否明确提到了已知技能名称
525
+ // 若是,优先包含 activate_skill 以确保 Agent 可以激活该技能
526
+ let mentionedSkill: string | null = null;
527
+ if (this.skillRegistry && this.skillRegistry.size > 0) {
528
+ const msgLower = message.toLowerCase();
529
+ for (const skill of this.skillRegistry.getAll()) {
530
+ // 检查用户消息是否包含技能名称(精确或模糊匹配)
531
+ if (msgLower.includes(skill.name.toLowerCase())) {
532
+ mentionedSkill = skill.name;
533
+ logger.debug(`[技能检测] 用户提到技能: ${mentionedSkill}`);
534
+ break; // 只检测第一个匹配的技能
535
+ }
536
+ }
537
+ }
538
+
539
+ // 如果检测到技能名称,从 externalTools 中找 activate_skill 并优先加入
540
+ if (mentionedSkill) {
541
+ const activateSkillTool = externalTools.find(t => t.name === 'activate_skill');
542
+ if (activateSkillTool) {
543
+ const toolPerm = activateSkillTool.permissionLevel ? (PERM_MAP[activateSkillTool.permissionLevel] ?? 0) : 0;
544
+ if (toolPerm <= callerPerm) {
545
+ collected.push(this.toAgentTool(activateSkillTool, context));
546
+ collectedNames.add('activate_skill');
547
+ logger.debug(`[技能激活] 已提前加入 activate_skill 工具(优先级最高)`);
548
+ }
549
+ }
550
+ }
551
+
462
552
  // 1. 从 SkillRegistry 两级过滤(包含适配器通过 declareSkill 注册的 Skill)
463
553
  if (this.skillRegistry) {
464
554
  const skills = this.skillRegistry.search(message, { maxResults: this.config.maxSkills });
465
- logger.debug(`Skill 匹配: ${skills.map(s => s.name).join(', ')}`);
555
+ const skillStr = skills.length > 0
556
+ ? skills.map(s => `${s.name}(${s.tools?.length || 0}工具)`).join(', ')
557
+ : '(无匹配技能)';
558
+ logger.debug(`[Skill 匹配] ${skillStr}`);
466
559
 
467
560
  for (const skill of skills) {
468
561
  for (const tool of skill.tools) {
@@ -501,12 +594,36 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
501
594
  collectedNames.add(tool.name);
502
595
  }
503
596
 
504
- // 4. 用 Agent.filterTools 做最终相关性排序
505
- return Agent.filterTools(message, collected, {
597
+ // 4. 用 Agent.filterTools 做最终相关性排序(阈值 0.3 减少噪音)
598
+ const filtered = Agent.filterTools(message, collected, {
506
599
  callerPermissionLevel: callerPerm,
507
600
  maxTools: this.config.maxTools,
508
- minScore: 0.1,
601
+ minScore: 0.3,
509
602
  });
603
+
604
+ // 特殊处理:如果检测到了技能名称,确保 activate_skill 排在最前面
605
+ if (mentionedSkill && filtered.length > 0) {
606
+ const activateSkillIdx = filtered.findIndex(t => t.name === 'activate_skill');
607
+ if (activateSkillIdx > 0) { // 若存在但不在最前
608
+ // 将 activate_skill 移到最前面
609
+ const activateSkillTool = filtered[activateSkillIdx];
610
+ filtered.splice(activateSkillIdx, 1);
611
+ filtered.unshift(activateSkillTool);
612
+ logger.debug(`[工具排序] activate_skill 提升至首位(因检测到技能: ${mentionedSkill})`);
613
+ }
614
+ }
615
+
616
+ // 诊断日志:显示收集的工具总数、过滤后的数量、以及列表
617
+ if (filtered.length > 0) {
618
+ logger.debug(
619
+ `[工具收集] 收集了 ${collected.length} 个工具,过滤后 ${filtered.length} 个,` +
620
+ `用户消息相关性最高的: ${filtered.slice(0, 3).map(t => t.name).join(', ')}`
621
+ );
622
+ } else {
623
+ logger.debug(`[工具收集] 收集了 ${collected.length} 个工具,但过滤后 0 个(没有超过相关性阈值的)`);
624
+ }
625
+
626
+ return filtered;
510
627
  }
511
628
 
512
629
  // ── 辅助方法 ─────────────────────────────────────────────────────────
@@ -528,22 +645,70 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
528
645
  if (tool.tags?.length) at.tags = tool.tags;
529
646
  if (tool.keywords?.length) at.keywords = tool.keywords;
530
647
  if (tool.permissionLevel) at.permissionLevel = PERM_MAP[tool.permissionLevel] ?? 0;
648
+ if (tool.preExecutable) at.preExecutable = true;
531
649
  return at;
532
650
  }
533
651
 
534
652
  /**
535
- * 构建 Skill 增强的 system prompt(仅在工具路径使用,闲聊不走这里)
653
+ * 构建结构化 System Prompt(借鉴 OpenClaw 的分段式设计)
654
+ *
655
+ * 段落结构:
656
+ * 1. 身份 + 人格
657
+ * 2. 安全准则
658
+ * 3. 工具调用风格
659
+ * 4. 技能列表(XML 格式)
660
+ * 5. 当前时间
661
+ * 6. 引导文件上下文(SOUL.md, TOOLS.md, AGENTS.md)
662
+ */
663
+ /**
664
+ * 构建精简的 System Prompt — 专为小模型(8B/14B 级)优化
665
+ *
666
+ * 设计原则:
667
+ * - 控制在 300-500 token 内,为工具定义和历史留足空间
668
+ * - 规则用短句,不用段落
669
+ * - 不重复,不举例(模型能从工具定义中推断用法)
536
670
  */
537
671
  private buildRichSystemPrompt(): string {
538
- let prompt = this.config.persona;
672
+ const lines: string[] = [];
673
+
674
+ // §1 身份
675
+ lines.push(this.config.persona);
676
+ lines.push('');
677
+
678
+ // §2 核心规则(精简为 6 条短句)
679
+ lines.push('## 规则');
680
+ lines.push('1. 直接调用工具执行操作,不要描述步骤或解释意图');
681
+ lines.push('2. 时间/日期问题:直接用下方"当前时间"回答,不调工具');
682
+ lines.push('3. 修改文件必须调用 edit_file/write_file,禁止给手动教程');
683
+ lines.push('4. activate_skill 返回后,必须继续调用其中指导的工具,不要停');
684
+ lines.push('5. 所有回答必须基于工具返回的实际数据');
685
+ lines.push('6. 工具失败时尝试替代方案,不要直接把错误丢给用户');
686
+ lines.push('');
687
+
688
+ // §3 技能列表(紧凑格式)
539
689
  if (this.skillRegistry && this.skillRegistry.size > 0) {
540
690
  const skills = this.skillRegistry.getAll();
541
- prompt += '\n\n## 我的能力\n';
691
+ lines.push('## 可用技能');
542
692
  for (const skill of skills) {
543
- prompt += `- **${skill.name}**: ${skill.description}\n`;
693
+ lines.push(`- ${skill.name}: ${skill.description}`);
544
694
  }
695
+ lines.push('用户提到技能名 → 调用 activate_skill(name) → 按返回的指导执行工具');
696
+ lines.push('');
545
697
  }
546
- return prompt;
698
+
699
+ // §4 当前时间
700
+ const now = new Date();
701
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
702
+ const timeStr = now.toLocaleString('zh-CN', { timeZone: tz });
703
+ lines.push(`当前时间: ${timeStr} (${tz})`);
704
+ lines.push('');
705
+
706
+ // §5 引导文件上下文(SOUL.md, TOOLS.md, AGENTS.md)
707
+ if (this.bootstrapContext) {
708
+ lines.push(this.bootstrapContext);
709
+ }
710
+
711
+ return lines.filter(Boolean).join('\n');
547
712
  }
548
713
 
549
714
  // ── 内置工具 ─────────────────────────────────────────────────────────
@@ -562,7 +727,7 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
562
727
  properties: {
563
728
  keyword: {
564
729
  type: 'string',
565
- description: '搜索关键词(模糊匹配消息内容和摘要)',
730
+ description: '搜索关键词(模糊匹配消息内容和摘要)。留空则返回最近几轮记录',
566
731
  },
567
732
  from_round: {
568
733
  type: 'number',
@@ -573,6 +738,7 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
573
738
  description: '结束轮次',
574
739
  },
575
740
  },
741
+ required: ['keyword'],
576
742
  },
577
743
  tags: ['memory', 'history', '聊天记录', '回忆', '之前'],
578
744
  keywords: ['之前', '历史', '聊过', '讨论过', '记得', '上次', '以前', '回忆'],
@@ -822,7 +988,13 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
822
988
 
823
989
  private fallbackFormat(toolCalls: { tool: string; args: any; result: any }[]): string {
824
990
  if (toolCalls.length === 0) return '处理完成。';
825
- return toolCalls.map(tc => {
991
+ // 过滤掉 activate_skill 的结果(是 SKILL.md 指令,不应暴露给用户)
992
+ const userFacing = toolCalls.filter(tc => tc.tool !== 'activate_skill');
993
+ if (userFacing.length === 0) {
994
+ // 只有 activate_skill 被调用但后续工具未执行 — 说明技能激活后流程中断
995
+ return '技能已激活但未能完成后续操作,请重试或换一种方式描述你的需求。';
996
+ }
997
+ return userFacing.map(tc => {
826
998
  const s = typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result, null, 2);
827
999
  return `【${tc.tool}】\n${s}`;
828
1000
  }).join('\n\n');
@@ -6,6 +6,7 @@
6
6
  import path from "node:path";
7
7
  import fs from "node:fs";
8
8
  import { stringify as stringifyYaml, parse as parseYaml } from "yaml";
9
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
9
10
  import { Schema } from "@zhin.js/schema";
10
11
  import { Feature, FeatureJSON } from "../feature.js";
11
12
  import { getPlugin } from "../plugin.js";
@@ -49,8 +50,24 @@ export class ConfigLoader<T extends object> {
49
50
  if (result.startsWith('\\${') && result.endsWith('}')) return result.slice(1);
50
51
  if (/^\$\{(.*)\}$/.test(result)) {
51
52
  const content = result.slice(2, -1);
52
- const [key, ...rest] = content.split(':');
53
- const defaultValue = rest.length > 0 ? rest.join(':') : undefined;
53
+ // 支持 bash 风格的默认值语法:${VAR:-default} 和 ${VAR:=default}
54
+ // 同时兼容简单语法 ${VAR:default}
55
+ let key: string;
56
+ let defaultValue: string | undefined;
57
+ const bashDefaultMatch = content.match(/^([^:}]+):[-=](.*)$/);
58
+ if (bashDefaultMatch) {
59
+ // ${VAR:-default} 或 ${VAR:=default}
60
+ key = bashDefaultMatch[1];
61
+ defaultValue = bashDefaultMatch[2];
62
+ } else if (content.includes(':')) {
63
+ // ${VAR:default}(旧的简单语法)
64
+ const [k, ...rest] = content.split(':');
65
+ key = k;
66
+ defaultValue = rest.join(':');
67
+ } else {
68
+ key = content;
69
+ defaultValue = undefined;
70
+ }
54
71
  return process.env[key] ?? defaultValue ?? (loader.initial as any)[key] ?? result;
55
72
  }
56
73
  }
@@ -83,6 +100,9 @@ export class ConfigLoader<T extends object> {
83
100
  case ".yml":
84
101
  rawConfig = parseYaml(content);
85
102
  break;
103
+ case ".toml":
104
+ rawConfig = parseToml(content);
105
+ break;
86
106
  }
87
107
  if (this.schema) {
88
108
  this.#data = this.schema(rawConfig || this.initial) as T;
@@ -99,12 +119,29 @@ export class ConfigLoader<T extends object> {
99
119
  case ".yml":
100
120
  fs.writeFileSync(fullPath, stringifyYaml(this.#data));
101
121
  break;
122
+ case ".toml":
123
+ fs.writeFileSync(fullPath, stringifyToml(this.#data as Record<string, any>));
124
+ break;
102
125
  }
103
126
  }
104
127
  }
105
128
 
106
129
  export namespace ConfigLoader {
107
- export const supportedExtensions = [".json", ".yaml", ".yml"];
130
+ export const supportedExtensions = [".json", ".yaml", ".yml", ".toml"];
131
+
132
+ /**
133
+ * 自动发现配置文件(按优先级:yml > yaml > json > toml)
134
+ */
135
+ export function discover(basename: string): string | null {
136
+ const cwd = process.cwd();
137
+ for (const ext of ['.yml', '.yaml', '.json', '.toml']) {
138
+ const filename = `${basename}${ext}`;
139
+ if (fs.existsSync(path.resolve(cwd, filename))) {
140
+ return filename;
141
+ }
142
+ }
143
+ return null;
144
+ }
108
145
  export function load<T extends object>(filename: string, initial?: T, schema?: Schema<T>) {
109
146
  const result = new ConfigLoader<T>(filename, initial ?? {} as T, schema);
110
147
  result.load();
@@ -152,6 +189,11 @@ export class ConfigFeature extends Feature<ConfigRecord> {
152
189
  /** 主配置文件名(第一个加载的配置文件) */
153
190
  #primaryConfigFile: string = '';
154
191
 
192
+ /** 获取主配置文件名 */
193
+ get primaryFile(): string {
194
+ return this.#primaryConfigFile;
195
+ }
196
+
155
197
  /**
156
198
  * 加载配置文件
157
199
  */
@@ -178,6 +220,14 @@ export class ConfigFeature extends Feature<ConfigRecord> {
178
220
  return config.data as T;
179
221
  }
180
222
 
223
+ /**
224
+ * 获取主配置文件数据(第一个加载的配置文件)
225
+ */
226
+ getPrimary<T extends object>(): T {
227
+ if (!this.#primaryConfigFile) throw new Error('没有加载任何配置文件');
228
+ return this.get<T>(this.#primaryConfigFile);
229
+ }
230
+
181
231
  /**
182
232
  * 获取原始配置数据
183
233
  */
package/src/built/tool.ts CHANGED
@@ -187,6 +187,7 @@ export class ZhinTool {
187
187
  #commandConfig: Omit<Tool.CommandConfig, 'enabled'> = {};
188
188
  #hidden: boolean = false;
189
189
  #source?: string;
190
+ #preExecutable: boolean = false;
190
191
 
191
192
  constructor(name: string) {
192
193
  this.#name = name;
@@ -254,6 +255,16 @@ export class ZhinTool {
254
255
  return this;
255
256
  }
256
257
 
258
+ /**
259
+ * 标记此工具允许被预执行(opt-in)。
260
+ * 仅适用于无副作用的只读工具(如获取系统状态、读取配置等)。
261
+ * 默认为 false,即不预执行。
262
+ */
263
+ preExec(value: boolean = true): this {
264
+ this.#preExecutable = value;
265
+ return this;
266
+ }
267
+
257
268
  usage(...usage: string[]): this {
258
269
  this.#commandConfig.usage = [...(this.#commandConfig.usage || []), ...usage];
259
270
  return this;
@@ -353,6 +364,7 @@ export class ZhinTool {
353
364
  if (this.#hidden) tool.hidden = this.#hidden;
354
365
  if (this.#source) tool.source = this.#source;
355
366
  if (this.#keywords.length > 0) tool.keywords = this.#keywords;
367
+ if (this.#preExecutable) tool.preExecutable = true;
356
368
 
357
369
  if (!this.#commandCallback) {
358
370
  tool.command = false;
package/src/cron.ts CHANGED
@@ -112,30 +112,31 @@ export class Cron {
112
112
  }
113
113
  }
114
114
 
115
- /**
116
- * Cron 表达式格式说明:
117
- *
118
- * 标准格式: "分 时 日 月 周"
119
- *
120
- * 字段说明:
121
- * - 秒: 0-59
122
- * - 分: 0-59
123
- * - 时: 0-23
124
- * - 日: 1-31
125
- * - 月: 1-12 ( JAN-DEC)
126
- * - 周: 0-7 (0和7都表示周日,或 SUN-SAT)
127
- *
128
- * 特殊字符:
129
- * - 星号: 匹配任意值
130
- * - 问号: 用于日和周字段,表示不指定值
131
- * - 横线: 表示范围,如 1-5
132
- * - 逗号: 表示列表,如 1,3,5
133
- * - 斜杠: 表示步长,如 0/15 表示每15分钟
134
- *
135
- * 常用示例:
136
- * - "0 0 0 * * *": 每天午夜执行
137
- * - "0 0/15 * * * *": 每15分钟执行
138
- * - "0 0 12 * * *": 每天中午12点执行
139
- * - "0 0 0 1 * *": 每月1号午夜执行
140
- * - "0 0 0 * * 0": 每周日午夜执行
141
- */
115
+ //
116
+ // Cron 表达式格式说明:
117
+ //
118
+ // 标准格式: "分 时 日 月 周" (5 字段)
119
+ //
120
+ // 字段说明:
121
+ // - 分: 0-59
122
+ // - 时: 0-23
123
+ // - 日: 1-31
124
+ // - 月: 1-12 (或 JAN-DEC)
125
+ // - 周: 0-7 (0和7都表示周日,或 SUN-SAT)
126
+ //
127
+ // > croner 也支持 6 字段格式 "秒 分 时 日 月 周",但推荐使用 5 字段格式。
128
+ //
129
+ // 特殊字符:
130
+ // - 星号: 匹配任意值
131
+ // - 问号: 用于日和周字段,表示不指定值
132
+ // - 横线: 表示范围,如 1-5
133
+ // - 逗号: 表示列表,如 1,3,5
134
+ // - 斜杠: 表示步长,如 */15 表示每15分钟
135
+ //
136
+ // 常用示例:
137
+ // - "0 0 * * *": 每天午夜执行
138
+ // - "*/15 * * * *": 每15分钟执行
139
+ // - "0 12 * * *": 每天中午12点执行
140
+ // - "0 0 1 * *": 每月1号午夜执行
141
+ // - "0 0 * * 0": 每周日午夜执行
142
+ //
@@ -11,7 +11,7 @@ const logger = getLogger('TypesGenerator');
11
11
  export async function generateEnvTypes(cwd: string): Promise<void> {
12
12
  try {
13
13
  // 基础类型集合
14
- const types = new Set(['@zhin.js/types']);
14
+ const types = new Set(['@types/node']);
15
15
 
16
16
  // 检查 package.json 中的依赖
17
17
  const pkgPath = path.join(cwd, 'package.json');
package/src/types.ts CHANGED
@@ -361,6 +361,14 @@ export interface Tool {
361
361
  * 隐藏的工具不会出现在帮助列表中,但仍可被调用
362
362
  */
363
363
  hidden?: boolean;
364
+
365
+ /**
366
+ * 是否允许预执行(opt-in)
367
+ * 仅当设置为 true 时,Agent 才会在 LLM 调用前自动预执行此工具并将结果注入上下文。
368
+ * 适用于无副作用的只读工具(如获取系统状态、读取配置等)。
369
+ * 默认为 false,即不预执行。
370
+ */
371
+ preExecutable?: boolean;
364
372
  }
365
373
 
366
374
  /**