@zhin.js/core 1.0.27 → 1.0.29
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 +117 -0
- 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 +33 -2
- package/lib/ai/zhin-agent.d.ts.map +1 -1
- package/lib/ai/zhin-agent.js +259 -60
- package/lib/ai/zhin-agent.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 +17 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +4 -4
- 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 +124 -0
- package/src/ai/types.ts +2 -0
- package/src/ai/zhin-agent.ts +293 -58
- package/src/built/tool.ts +12 -0
- package/src/cron.ts +28 -27
- package/src/types-generator.ts +1 -1
- package/src/types.ts +19 -0
- package/tests/adapter.test.ts +1 -1
- package/tests/config.test.ts +2 -2
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,25 @@ ${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.senderId) parts.push(
|
|
440
|
-
if (context.
|
|
441
|
-
if (context.
|
|
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.botId) parts.push(`Bot:${context.botId}`);
|
|
505
|
+
if (context.senderId) parts.push(`用户:${context.senderId}`);
|
|
506
|
+
if (context.scope) parts.push(`场景类型:${context.scope}`);
|
|
507
|
+
if (context.sceneId) parts.push(`场景ID:${context.sceneId}`);
|
|
444
508
|
if (parts.length === 0) return '';
|
|
445
|
-
return `\n
|
|
509
|
+
return `\n上下文: ${parts.join(' | ')}`;
|
|
446
510
|
}
|
|
447
511
|
|
|
448
512
|
// ── 工具收集: 两级过滤 (Skill → Tool) ─────────────────────────────────
|
|
@@ -459,13 +523,48 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
|
|
|
459
523
|
const collected: AgentTool[] = [];
|
|
460
524
|
const collectedNames = new Set<string>(); // 用 Set 加速去重
|
|
461
525
|
|
|
526
|
+
// 0. 检测用户是否明确提到了已知技能名称
|
|
527
|
+
// 若是,优先包含 activate_skill 以确保 Agent 可以激活该技能
|
|
528
|
+
let mentionedSkill: string | null = null;
|
|
529
|
+
if (this.skillRegistry && this.skillRegistry.size > 0) {
|
|
530
|
+
const msgLower = message.toLowerCase();
|
|
531
|
+
for (const skill of this.skillRegistry.getAll()) {
|
|
532
|
+
// 检查用户消息是否包含技能名称(精确或模糊匹配)
|
|
533
|
+
if (msgLower.includes(skill.name.toLowerCase())) {
|
|
534
|
+
mentionedSkill = skill.name;
|
|
535
|
+
logger.debug(`[技能检测] 用户提到技能: ${mentionedSkill}`);
|
|
536
|
+
break; // 只检测第一个匹配的技能
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 如果检测到技能名称,从 externalTools 中找 activate_skill 并优先加入
|
|
542
|
+
if (mentionedSkill) {
|
|
543
|
+
const activateSkillTool = externalTools.find(t => t.name === 'activate_skill');
|
|
544
|
+
if (activateSkillTool) {
|
|
545
|
+
const toolPerm = activateSkillTool.permissionLevel ? (PERM_MAP[activateSkillTool.permissionLevel] ?? 0) : 0;
|
|
546
|
+
if (toolPerm <= callerPerm) {
|
|
547
|
+
collected.push(this.toAgentTool(activateSkillTool, context));
|
|
548
|
+
collectedNames.add('activate_skill');
|
|
549
|
+
logger.debug(`[技能激活] 已提前加入 activate_skill 工具(优先级最高)`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
462
554
|
// 1. 从 SkillRegistry 两级过滤(包含适配器通过 declareSkill 注册的 Skill)
|
|
463
555
|
if (this.skillRegistry) {
|
|
464
556
|
const skills = this.skillRegistry.search(message, { maxResults: this.config.maxSkills });
|
|
465
|
-
|
|
557
|
+
const skillStr = skills.length > 0
|
|
558
|
+
? skills.map(s => `${s.name}(${s.tools?.length || 0}工具)`).join(', ')
|
|
559
|
+
: '(无匹配技能)';
|
|
560
|
+
logger.debug(`[Skill 匹配] ${skillStr}`);
|
|
466
561
|
|
|
467
562
|
for (const skill of skills) {
|
|
468
563
|
for (const tool of skill.tools) {
|
|
564
|
+
// 平台过滤:确保 Skill 中的工具也只保留当前平台支持的
|
|
565
|
+
if (tool.platforms?.length && context.platform && !tool.platforms.includes(context.platform)) continue;
|
|
566
|
+
// 场景过滤
|
|
567
|
+
if (tool.scopes?.length && context.scope && !tool.scopes.includes(context.scope)) continue;
|
|
469
568
|
// 权限检查
|
|
470
569
|
const toolPerm = tool.permissionLevel ? (PERM_MAP[tool.permissionLevel] ?? 0) : 0;
|
|
471
570
|
if (toolPerm > callerPerm) continue;
|
|
@@ -501,49 +600,178 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
|
|
|
501
600
|
collectedNames.add(tool.name);
|
|
502
601
|
}
|
|
503
602
|
|
|
504
|
-
// 4. 用 Agent.filterTools
|
|
505
|
-
|
|
603
|
+
// 4. 用 Agent.filterTools 做最终相关性排序(阈值 0.3 减少噪音)
|
|
604
|
+
const filtered = Agent.filterTools(message, collected, {
|
|
506
605
|
callerPermissionLevel: callerPerm,
|
|
507
606
|
maxTools: this.config.maxTools,
|
|
508
|
-
minScore: 0.
|
|
607
|
+
minScore: 0.3,
|
|
509
608
|
});
|
|
609
|
+
|
|
610
|
+
// 特殊处理:如果检测到了技能名称,确保 activate_skill 排在最前面
|
|
611
|
+
if (mentionedSkill && filtered.length > 0) {
|
|
612
|
+
const activateSkillIdx = filtered.findIndex(t => t.name === 'activate_skill');
|
|
613
|
+
if (activateSkillIdx > 0) { // 若存在但不在最前
|
|
614
|
+
// 将 activate_skill 移到最前面
|
|
615
|
+
const activateSkillTool = filtered[activateSkillIdx];
|
|
616
|
+
filtered.splice(activateSkillIdx, 1);
|
|
617
|
+
filtered.unshift(activateSkillTool);
|
|
618
|
+
logger.debug(`[工具排序] activate_skill 提升至首位(因检测到技能: ${mentionedSkill})`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// 诊断日志:显示收集的工具总数、过滤后的数量、以及列表
|
|
623
|
+
if (filtered.length > 0) {
|
|
624
|
+
logger.debug(
|
|
625
|
+
`[工具收集] 收集了 ${collected.length} 个工具,过滤后 ${filtered.length} 个,` +
|
|
626
|
+
`用户消息相关性最高的: ${filtered.slice(0, 3).map(t => t.name).join(', ')}`
|
|
627
|
+
);
|
|
628
|
+
} else {
|
|
629
|
+
logger.debug(`[工具收集] 收集了 ${collected.length} 个工具,但过滤后 0 个(没有超过相关性阈值的)`);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return filtered;
|
|
510
633
|
}
|
|
511
634
|
|
|
512
635
|
// ── 辅助方法 ─────────────────────────────────────────────────────────
|
|
513
636
|
|
|
514
637
|
/**
|
|
515
|
-
* 将 Tool 转为 AgentTool,注入 ToolContext
|
|
638
|
+
* 将 Tool 转为 AgentTool,注入 ToolContext 以确保执行时鉴权生效。
|
|
639
|
+
*
|
|
640
|
+
* 当参数定义了 contextKey 时:
|
|
641
|
+
* 1. 从 AI 可见的 parameters 中移除该参数(减少 token、避免填错)
|
|
642
|
+
* 2. 执行时自动从 ToolContext 注入对应值,并按声明类型做类型转换
|
|
516
643
|
*/
|
|
517
644
|
private toAgentTool(tool: Tool, context?: ToolContext): AgentTool {
|
|
518
645
|
const originalExecute = tool.execute;
|
|
646
|
+
|
|
647
|
+
// ── 收集需要自动注入的参数 ──────────────────────────────────
|
|
648
|
+
const contextInjections: Array<{
|
|
649
|
+
paramName: string;
|
|
650
|
+
contextKey: string;
|
|
651
|
+
paramType: string; // 目标参数的 JSON Schema type,用于类型转换
|
|
652
|
+
}> = [];
|
|
653
|
+
let cleanParameters: any = tool.parameters;
|
|
654
|
+
|
|
655
|
+
if (context && tool.parameters?.properties) {
|
|
656
|
+
const props = tool.parameters.properties as Record<string, any>;
|
|
657
|
+
const filteredProps: Record<string, any> = {};
|
|
658
|
+
const filteredRequired: string[] = [];
|
|
659
|
+
|
|
660
|
+
for (const [key, schema] of Object.entries(props)) {
|
|
661
|
+
if (schema.contextKey && (context as any)[schema.contextKey] != null) {
|
|
662
|
+
// 记录需要注入的映射
|
|
663
|
+
contextInjections.push({
|
|
664
|
+
paramName: key,
|
|
665
|
+
contextKey: schema.contextKey,
|
|
666
|
+
paramType: schema.type || 'string',
|
|
667
|
+
});
|
|
668
|
+
} else {
|
|
669
|
+
// 保留给 AI 的参数
|
|
670
|
+
filteredProps[key] = schema;
|
|
671
|
+
if (tool.parameters.required?.includes(key)) {
|
|
672
|
+
filteredRequired.push(key);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (contextInjections.length > 0) {
|
|
678
|
+
cleanParameters = {
|
|
679
|
+
...tool.parameters,
|
|
680
|
+
properties: filteredProps,
|
|
681
|
+
required: filteredRequired.length > 0 ? filteredRequired : undefined,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ── 组装 AgentTool ──────────────────────────────────────────
|
|
519
687
|
const at: AgentTool = {
|
|
520
688
|
name: tool.name,
|
|
521
689
|
description: tool.description,
|
|
522
|
-
parameters:
|
|
523
|
-
// 包装 execute,将 ToolContext 注入第二参数,确保工具内部的鉴权逻辑能正常执行
|
|
690
|
+
parameters: cleanParameters as any,
|
|
524
691
|
execute: context
|
|
525
|
-
? (args: Record<string, any>) =>
|
|
692
|
+
? (args: Record<string, any>) => {
|
|
693
|
+
// 自动注入 context 值,按目标 type 做类型转换
|
|
694
|
+
const enrichedArgs = { ...args };
|
|
695
|
+
for (const { paramName, contextKey, paramType } of contextInjections) {
|
|
696
|
+
let value = (context as any)[contextKey];
|
|
697
|
+
if (paramType === 'number' && typeof value === 'string') {
|
|
698
|
+
value = Number(value);
|
|
699
|
+
} else if (paramType === 'string' && typeof value !== 'string') {
|
|
700
|
+
value = String(value);
|
|
701
|
+
}
|
|
702
|
+
enrichedArgs[paramName] = value;
|
|
703
|
+
}
|
|
704
|
+
return originalExecute(enrichedArgs, context);
|
|
705
|
+
}
|
|
526
706
|
: originalExecute,
|
|
527
707
|
};
|
|
528
708
|
if (tool.tags?.length) at.tags = tool.tags;
|
|
529
709
|
if (tool.keywords?.length) at.keywords = tool.keywords;
|
|
530
710
|
if (tool.permissionLevel) at.permissionLevel = PERM_MAP[tool.permissionLevel] ?? 0;
|
|
711
|
+
if (tool.preExecutable) at.preExecutable = true;
|
|
531
712
|
return at;
|
|
532
713
|
}
|
|
533
714
|
|
|
534
715
|
/**
|
|
535
|
-
*
|
|
716
|
+
* 构建结构化 System Prompt(借鉴 OpenClaw 的分段式设计)
|
|
717
|
+
*
|
|
718
|
+
* 段落结构:
|
|
719
|
+
* 1. 身份 + 人格
|
|
720
|
+
* 2. 安全准则
|
|
721
|
+
* 3. 工具调用风格
|
|
722
|
+
* 4. 技能列表(XML 格式)
|
|
723
|
+
* 5. 当前时间
|
|
724
|
+
* 6. 引导文件上下文(SOUL.md, TOOLS.md, AGENTS.md)
|
|
725
|
+
*/
|
|
726
|
+
/**
|
|
727
|
+
* 构建精简的 System Prompt — 专为小模型(8B/14B 级)优化
|
|
728
|
+
*
|
|
729
|
+
* 设计原则:
|
|
730
|
+
* - 控制在 300-500 token 内,为工具定义和历史留足空间
|
|
731
|
+
* - 规则用短句,不用段落
|
|
732
|
+
* - 不重复,不举例(模型能从工具定义中推断用法)
|
|
536
733
|
*/
|
|
537
734
|
private buildRichSystemPrompt(): string {
|
|
538
|
-
|
|
735
|
+
const lines: string[] = [];
|
|
736
|
+
|
|
737
|
+
// §1 身份
|
|
738
|
+
lines.push(this.config.persona);
|
|
739
|
+
lines.push('');
|
|
740
|
+
|
|
741
|
+
// §2 核心规则(精简为 6 条短句)
|
|
742
|
+
lines.push('## 规则');
|
|
743
|
+
lines.push('1. 直接调用工具执行操作,不要描述步骤或解释意图');
|
|
744
|
+
lines.push('2. 时间/日期问题:直接用下方"当前时间"回答,不调工具');
|
|
745
|
+
lines.push('3. 修改文件必须调用 edit_file/write_file,禁止给手动教程');
|
|
746
|
+
lines.push('4. activate_skill 返回后,必须继续调用其中指导的工具,不要停');
|
|
747
|
+
lines.push('5. 所有回答必须基于工具返回的实际数据');
|
|
748
|
+
lines.push('6. 工具失败时尝试替代方案,不要直接把错误丢给用户');
|
|
749
|
+
lines.push('');
|
|
750
|
+
|
|
751
|
+
// §3 技能列表(紧凑格式)
|
|
539
752
|
if (this.skillRegistry && this.skillRegistry.size > 0) {
|
|
540
753
|
const skills = this.skillRegistry.getAll();
|
|
541
|
-
|
|
754
|
+
lines.push('## 可用技能');
|
|
542
755
|
for (const skill of skills) {
|
|
543
|
-
|
|
756
|
+
lines.push(`- ${skill.name}: ${skill.description}`);
|
|
544
757
|
}
|
|
758
|
+
lines.push('用户提到技能名 → 调用 activate_skill(name) → 按返回的指导执行工具');
|
|
759
|
+
lines.push('');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// §4 当前时间
|
|
763
|
+
const now = new Date();
|
|
764
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
765
|
+
const timeStr = now.toLocaleString('zh-CN', { timeZone: tz });
|
|
766
|
+
lines.push(`当前时间: ${timeStr} (${tz})`);
|
|
767
|
+
lines.push('');
|
|
768
|
+
|
|
769
|
+
// §5 引导文件上下文(SOUL.md, TOOLS.md, AGENTS.md)
|
|
770
|
+
if (this.bootstrapContext) {
|
|
771
|
+
lines.push(this.bootstrapContext);
|
|
545
772
|
}
|
|
546
|
-
|
|
773
|
+
|
|
774
|
+
return lines.filter(Boolean).join('\n');
|
|
547
775
|
}
|
|
548
776
|
|
|
549
777
|
// ── 内置工具 ─────────────────────────────────────────────────────────
|
|
@@ -562,7 +790,7 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
|
|
|
562
790
|
properties: {
|
|
563
791
|
keyword: {
|
|
564
792
|
type: 'string',
|
|
565
|
-
description: '
|
|
793
|
+
description: '搜索关键词(模糊匹配消息内容和摘要)。留空则返回最近几轮记录',
|
|
566
794
|
},
|
|
567
795
|
from_round: {
|
|
568
796
|
type: 'number',
|
|
@@ -573,6 +801,7 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
|
|
|
573
801
|
description: '结束轮次',
|
|
574
802
|
},
|
|
575
803
|
},
|
|
804
|
+
required: ['keyword'],
|
|
576
805
|
},
|
|
577
806
|
tags: ['memory', 'history', '聊天记录', '回忆', '之前'],
|
|
578
807
|
keywords: ['之前', '历史', '聊过', '讨论过', '记得', '上次', '以前', '回忆'],
|
|
@@ -822,7 +1051,13 @@ ${preData ? `\n已自动获取的数据:${preData}\n` : ''}
|
|
|
822
1051
|
|
|
823
1052
|
private fallbackFormat(toolCalls: { tool: string; args: any; result: any }[]): string {
|
|
824
1053
|
if (toolCalls.length === 0) return '处理完成。';
|
|
825
|
-
|
|
1054
|
+
// 过滤掉 activate_skill 的结果(是 SKILL.md 指令,不应暴露给用户)
|
|
1055
|
+
const userFacing = toolCalls.filter(tc => tc.tool !== 'activate_skill');
|
|
1056
|
+
if (userFacing.length === 0) {
|
|
1057
|
+
// 只有 activate_skill 被调用但后续工具未执行 — 说明技能激活后流程中断
|
|
1058
|
+
return '技能已激活但未能完成后续操作,请重试或换一种方式描述你的需求。';
|
|
1059
|
+
}
|
|
1060
|
+
return userFacing.map(tc => {
|
|
826
1061
|
const s = typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result, null, 2);
|
|
827
1062
|
return `【${tc.tool}】\n${s}`;
|
|
828
1063
|
}).join('\n\n');
|
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');
|