@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.
- package/CHANGELOG.md +18 -0
- package/lib/ai/agent.d.ts.map +1 -1
- package/lib/ai/agent.js +4 -0
- package/lib/ai/agent.js.map +1 -1
- package/lib/ai/bootstrap.d.ts +82 -0
- package/lib/ai/bootstrap.d.ts.map +1 -0
- package/lib/ai/bootstrap.js +199 -0
- package/lib/ai/bootstrap.js.map +1 -0
- package/lib/ai/builtin-tools.d.ts +36 -0
- package/lib/ai/builtin-tools.d.ts.map +1 -0
- package/lib/ai/builtin-tools.js +509 -0
- package/lib/ai/builtin-tools.js.map +1 -0
- package/lib/ai/compaction.d.ts +132 -0
- package/lib/ai/compaction.d.ts.map +1 -0
- package/lib/ai/compaction.js +370 -0
- package/lib/ai/compaction.js.map +1 -0
- package/lib/ai/hooks.d.ts +143 -0
- package/lib/ai/hooks.d.ts.map +1 -0
- package/lib/ai/hooks.js +108 -0
- package/lib/ai/hooks.js.map +1 -0
- package/lib/ai/index.d.ts +6 -0
- package/lib/ai/index.d.ts.map +1 -1
- package/lib/ai/index.js +6 -0
- package/lib/ai/index.js.map +1 -1
- package/lib/ai/init.d.ts.map +1 -1
- package/lib/ai/init.js +120 -3
- package/lib/ai/init.js.map +1 -1
- package/lib/ai/types.d.ts +2 -0
- package/lib/ai/types.d.ts.map +1 -1
- package/lib/ai/zhin-agent.d.ts +28 -1
- package/lib/ai/zhin-agent.d.ts.map +1 -1
- package/lib/ai/zhin-agent.js +196 -57
- package/lib/ai/zhin-agent.js.map +1 -1
- package/lib/built/config.d.ts +10 -0
- package/lib/built/config.d.ts.map +1 -1
- package/lib/built/config.js +54 -3
- package/lib/built/config.js.map +1 -1
- package/lib/built/tool.d.ts +6 -0
- package/lib/built/tool.d.ts.map +1 -1
- package/lib/built/tool.js +12 -0
- package/lib/built/tool.js.map +1 -1
- package/lib/cron.d.ts +0 -27
- package/lib/cron.d.ts.map +1 -1
- package/lib/cron.js +28 -27
- package/lib/cron.js.map +1 -1
- package/lib/types-generator.js +1 -1
- package/lib/types-generator.js.map +1 -1
- package/lib/types.d.ts +7 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/ai/agent.ts +6 -0
- package/src/ai/bootstrap.ts +263 -0
- package/src/ai/builtin-tools.ts +569 -0
- package/src/ai/compaction.ts +529 -0
- package/src/ai/hooks.ts +223 -0
- package/src/ai/index.ts +58 -0
- package/src/ai/init.ts +127 -3
- package/src/ai/types.ts +2 -0
- package/src/ai/zhin-agent.ts +226 -54
- package/src/built/config.ts +53 -3
- package/src/built/tool.ts +12 -0
- package/src/cron.ts +28 -27
- package/src/types-generator.ts +1 -1
- package/src/types.ts +8 -0
- package/tests/adapter.test.ts +1 -1
- package/tests/config.test.ts +2 -2
- package/test/minimal-bot.ts +0 -31
- package/test/stress-test.ts +0 -123
package/src/ai/zhin-agent.ts
CHANGED
|
@@ -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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
const
|
|
329
|
+
// ══════ 4. 拆分可预执行 / 普通工具 ══════
|
|
330
|
+
// 只有显式标记 preExecutable=true 的工具才会被预执行(opt-in 模式)
|
|
331
|
+
const preExecTools: AgentTool[] = [];
|
|
279
332
|
for (const tool of allTools) {
|
|
280
|
-
|
|
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 (
|
|
338
|
+
if (preExecTools.length > 0) {
|
|
287
339
|
const tPre = now();
|
|
288
|
-
logger.debug(`预执行: ${
|
|
340
|
+
logger.debug(`预执行: ${preExecTools.map(t => t.name).join(', ')}`);
|
|
289
341
|
const results = await Promise.allSettled(
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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 路径: ${
|
|
384
|
+
logger.debug(`Agent 路径: ${allTools.length} 个工具`);
|
|
326
385
|
const contextHint = this.buildContextHint(context, content);
|
|
327
|
-
|
|
386
|
+
|
|
387
|
+
// 使用结构化系统提示(包含时间、安全准则、技能列表等)
|
|
388
|
+
const richPrompt = this.buildRichSystemPrompt();
|
|
389
|
+
const systemPrompt = `${richPrompt}
|
|
328
390
|
${contextHint}
|
|
329
|
-
${preData ? `\n
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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,
|
|
501
|
+
private buildContextHint(context: ToolContext, _content: string): string {
|
|
436
502
|
const parts: string[] = [];
|
|
437
|
-
if (context.
|
|
438
|
-
if (context.
|
|
439
|
-
if (context.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
691
|
+
lines.push('## 可用技能');
|
|
542
692
|
for (const skill of skills) {
|
|
543
|
-
|
|
693
|
+
lines.push(`- ${skill.name}: ${skill.description}`);
|
|
544
694
|
}
|
|
695
|
+
lines.push('用户提到技能名 → 调用 activate_skill(name) → 按返回的指导执行工具');
|
|
696
|
+
lines.push('');
|
|
545
697
|
}
|
|
546
|
-
|
|
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
|
-
|
|
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');
|
package/src/built/config.ts
CHANGED
|
@@ -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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
//
|
package/src/types-generator.ts
CHANGED
|
@@ -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(['@
|
|
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
|
/**
|