@zhin.js/core 1.0.30 → 1.0.32

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 (50) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/lib/ai/agent.d.ts +4 -0
  3. package/lib/ai/agent.d.ts.map +1 -1
  4. package/lib/ai/agent.js +22 -0
  5. package/lib/ai/agent.js.map +1 -1
  6. package/lib/ai/builtin-tools.d.ts +2 -1
  7. package/lib/ai/builtin-tools.d.ts.map +1 -1
  8. package/lib/ai/builtin-tools.js +86 -7
  9. package/lib/ai/builtin-tools.js.map +1 -1
  10. package/lib/ai/cron-engine.d.ts +92 -0
  11. package/lib/ai/cron-engine.d.ts.map +1 -0
  12. package/lib/ai/cron-engine.js +278 -0
  13. package/lib/ai/cron-engine.js.map +1 -0
  14. package/lib/ai/index.d.ts +3 -1
  15. package/lib/ai/index.d.ts.map +1 -1
  16. package/lib/ai/index.js +3 -1
  17. package/lib/ai/index.js.map +1 -1
  18. package/lib/ai/init.d.ts.map +1 -1
  19. package/lib/ai/init.js +115 -51
  20. package/lib/ai/init.js.map +1 -1
  21. package/lib/ai/service.d.ts +3 -0
  22. package/lib/ai/service.d.ts.map +1 -1
  23. package/lib/ai/service.js +4 -0
  24. package/lib/ai/service.js.map +1 -1
  25. package/lib/ai/types.d.ts +15 -0
  26. package/lib/ai/types.d.ts.map +1 -1
  27. package/lib/ai/zhin-agent.d.ts +18 -0
  28. package/lib/ai/zhin-agent.d.ts.map +1 -1
  29. package/lib/ai/zhin-agent.js +102 -10
  30. package/lib/ai/zhin-agent.js.map +1 -1
  31. package/lib/built/tool.d.ts +2 -0
  32. package/lib/built/tool.d.ts.map +1 -1
  33. package/lib/built/tool.js +8 -0
  34. package/lib/built/tool.js.map +1 -1
  35. package/lib/plugin.js +2 -2
  36. package/lib/plugin.js.map +1 -1
  37. package/lib/types.d.ts +2 -0
  38. package/lib/types.d.ts.map +1 -1
  39. package/package.json +4 -4
  40. package/src/ai/agent.ts +21 -0
  41. package/src/ai/builtin-tools.ts +79 -7
  42. package/src/ai/cron-engine.ts +337 -0
  43. package/src/ai/index.ts +21 -1
  44. package/src/ai/init.ts +109 -55
  45. package/src/ai/service.ts +4 -0
  46. package/src/ai/types.ts +15 -0
  47. package/src/ai/zhin-agent.ts +114 -10
  48. package/src/built/tool.ts +8 -0
  49. package/src/plugin.ts +2 -2
  50. package/src/types.ts +3 -0
package/src/ai/init.ts CHANGED
@@ -10,6 +10,9 @@
10
10
  * - AI 管理工具
11
11
  */
12
12
 
13
+ import * as fs from 'fs';
14
+ import * as os from 'os';
15
+ import * as path from 'path';
13
16
  import { Logger } from '@zhin.js/logger';
14
17
  import { getPlugin, type Plugin } from '../plugin.js';
15
18
  import { Message } from '../message.js';
@@ -40,6 +43,7 @@ import {
40
43
  import { AI_MESSAGE_MODEL, AI_SUMMARY_MODEL } from './conversation-memory.js';
41
44
  import { AI_USER_PROFILE_MODEL } from './user-profile.js';
42
45
  import { AI_FOLLOWUP_MODEL } from './follow-up.js';
46
+ import { PersistentCronEngine, setCronManager, createCronTools } from './cron-engine.js';
43
47
  import { renderToPlainText, type OutputElement } from './output.js';
44
48
  import type { AIConfig, ContentPart } from './types.js';
45
49
 
@@ -169,19 +173,18 @@ export function initAIModule(): void {
169
173
  });
170
174
 
171
175
  // ── ZhinAgent 全局大脑 ──
172
- useContext('ai' as any, (ai: AIService) => {
176
+ useContext('ai', (ai) => {
173
177
  if (!ai.isReady()) {
174
178
  logger.warn('AI Service not ready, ZhinAgent not created');
175
179
  return;
176
180
  }
177
181
 
178
182
  const provider = ai.getProvider();
179
- const agent = new ZhinAgent(provider);
183
+ const agentConfig = ai.getAgentConfig();
184
+ const agent = new ZhinAgent(provider, agentConfig);
180
185
  zhinAgentInstance = agent;
181
186
 
182
- const skillRegistry = root.inject('skill' as any) as
183
- | SkillFeature
184
- | undefined;
187
+ const skillRegistry = root.inject('skill');
185
188
  if (skillRegistry) agent.setSkillRegistry(skillRegistry);
186
189
 
187
190
  // 注入跟进提醒的发送回调(不依赖数据库,内存模式也能发)
@@ -201,8 +204,32 @@ export function initAIModule(): void {
201
204
  });
202
205
  });
203
206
 
207
+ // 持久化定时任务引擎:加载 data/cron-jobs.json,到点用 prompt 调用 Agent;并暴露给 AI 管理(list/add/remove/pause/resume)
208
+ let cronEngine: PersistentCronEngine | null = null;
209
+ const cronFeature = root.inject('cron' as any);
210
+ if (cronFeature && typeof cronFeature.add === 'function') {
211
+ const dataDir = path.join(process.cwd(), 'data');
212
+ const addCron = (c: any) => cronFeature.add(c, 'cron-engine');
213
+ const runner = async (prompt: string) => {
214
+ if (!zhinAgentInstance) return;
215
+ await zhinAgentInstance.process(prompt, {
216
+ platform: 'cron',
217
+ senderId: 'system',
218
+ sceneId: 'cron',
219
+ });
220
+ };
221
+ cronEngine = new PersistentCronEngine({ dataDir, addCron, runner });
222
+ cronEngine.load();
223
+ setCronManager({ cronFeature, engine: cronEngine });
224
+ }
225
+
204
226
  logger.debug('ZhinAgent created');
205
227
  return () => {
228
+ setCronManager(null);
229
+ if (cronEngine) {
230
+ cronEngine.unload();
231
+ cronEngine = null;
232
+ }
206
233
  agent.dispose();
207
234
  zhinAgentInstance = null;
208
235
  };
@@ -511,61 +538,55 @@ export function initAIModule(): void {
511
538
  const builtinTools = createBuiltinTools();
512
539
  const disposers: (() => void)[] = [];
513
540
  for (const tool of builtinTools) disposers.push(toolService.addTool(tool, root.name));
514
- logger.info(`Registered ${builtinTools.length} built-in system tools`);
541
+ const cronTools = createCronTools();
542
+ for (const tool of cronTools) disposers.push(toolService.addTool(tool, root.name));
543
+ logger.info(`Registered ${builtinTools.length} built-in + ${cronTools.length} cron tools`);
544
+
545
+ let skillWatchers: fs.FSWatcher[] = [];
546
+ let skillReloadDebounce: ReturnType<typeof setTimeout> | null = null;
547
+
548
+ async function syncWorkspaceSkills(): Promise<number> {
549
+ const skillFeature = root.inject?.('skill') as SkillFeature | undefined;
550
+ if (!skillFeature) return 0;
551
+ // 先移除当前插件注册的所有工作区技能(增量更新)
552
+ const existing = skillFeature.getByPlugin(root.name);
553
+ for (const s of existing) skillFeature.remove(s);
554
+ const skills = await discoverWorkspaceSkills();
555
+ if (skills.length === 0) return 0;
556
+ const allRegisteredTools = toolService.getAll();
557
+ const toolNameIndex = new Map<string, Tool>();
558
+ for (const t of allRegisteredTools) {
559
+ toolNameIndex.set(t.name, t);
560
+ const parts = t.name.split('_');
561
+ if (parts.length === 2) toolNameIndex.set(`${parts[1]}_${parts[0]}`, t);
562
+ }
563
+ for (const s of skills) {
564
+ const associatedTools: Tool[] = [];
565
+ const toolNames = s.toolNames || [];
566
+ for (const toolName of toolNames) {
567
+ let tool = toolService.get(toolName) || toolNameIndex.get(toolName);
568
+ if (tool) associatedTools.push(tool);
569
+ }
570
+ skillFeature.add({
571
+ name: s.name,
572
+ description: s.description,
573
+ tools: associatedTools,
574
+ keywords: s.keywords || [],
575
+ tags: s.tags || [],
576
+ pluginName: root.name,
577
+ }, root.name);
578
+ }
579
+ return skills.length;
580
+ }
515
581
 
516
582
  // 异步发现工作区技能 + 加载引导文件(不阻塞注册流程)
517
583
  (async () => {
518
584
  // ── 第一步:发现和注册工作区技能 ──
519
585
  try {
520
- const skills = await discoverWorkspaceSkills();
586
+ const count = await syncWorkspaceSkills();
521
587
  const skillFeature = root.inject?.('skill') as SkillFeature | undefined;
522
- if (skillFeature && skills.length > 0) {
523
- logger.debug(`[技能注册] 开始注册 ${skills.length} 个技能...`);
524
- // 构建所有已注册工具名的索引(用于模糊匹配)
525
- const allRegisteredTools = toolService.getAll();
526
- const toolNameIndex = new Map<string, Tool>();
527
- for (const t of allRegisteredTools) {
528
- toolNameIndex.set(t.name, t);
529
- // 建立反向别名索引:read_file ↔ file_read 等
530
- const parts = t.name.split('_');
531
- if (parts.length === 2) {
532
- toolNameIndex.set(`${parts[1]}_${parts[0]}`, t);
533
- }
534
- }
535
-
536
- for (const s of skills) {
537
- // 从 toolService 中查找技能声明的关联工具(支持模糊匹配)
538
- const associatedTools: Tool[] = [];
539
- const toolNames = s.toolNames || [];
540
- if (toolNames.length > 0 && toolService) {
541
- logger.debug(`[技能注册] 技能 '${s.name}' 声明的工具: ${toolNames.join(', ')}`);
542
- for (const toolName of toolNames) {
543
- // 精确匹配
544
- let tool = toolService.get(toolName);
545
- // 若精确匹配失败,尝试反向别名(file_read → read_file)
546
- if (!tool) {
547
- tool = toolNameIndex.get(toolName) || undefined;
548
- }
549
- if (tool) {
550
- associatedTools.push(tool);
551
- const matchType = toolService.get(toolName) ? '精确' : '模糊';
552
- logger.debug(`[技能注册] ✅ 找到工具: ${toolName}${matchType === '模糊' ? ` → ${tool.name} (模糊匹配)` : ''}`);
553
- } else {
554
- logger.warn(`[技能注册] ❌ 工具 '${toolName}' 未找到(已注册: ${allRegisteredTools.map(t => t.name).join(', ')})`);
555
- }
556
- }
557
- }
558
- skillFeature.add({
559
- name: s.name,
560
- description: s.description,
561
- tools: associatedTools,
562
- keywords: s.keywords || [],
563
- tags: s.tags || [],
564
- pluginName: root.name,
565
- }, root.name);
566
- logger.debug(`[技能注册] 技能 '${s.name}' 已注册 (${associatedTools.length} 个工具)`);
567
- }
568
- logger.info(`✅ Registered ${skills.length} workspace skills with ${skills.reduce((sum, s) => sum + ((s.toolNames || []).length), 0)} total tool references`);
588
+ if (count > 0 && skillFeature) {
589
+ logger.info(`✅ Registered ${count} workspace skills`);
569
590
  }
570
591
  } catch (e: any) {
571
592
  logger.warn(`Failed to discover workspace skills: ${e.message}`);
@@ -619,8 +640,41 @@ export function initAIModule(): void {
619
640
  skillCount: skillFeature2?.size ?? 0,
620
641
  bootstrapFiles: loadedFiles,
621
642
  }));
643
+
644
+ // ── 技能目录热重载:监听 workspace + local 技能目录,防抖后重新发现并更新 ──
645
+ const workspaceSkillDir = path.join(process.cwd(), 'skills');
646
+ const localSkillDir = path.join(os.homedir(), '.zhin', 'skills');
647
+ const onSkillDirChange = () => {
648
+ if (skillReloadDebounce) clearTimeout(skillReloadDebounce);
649
+ skillReloadDebounce = setTimeout(async () => {
650
+ skillReloadDebounce = null;
651
+ try {
652
+ const count = await syncWorkspaceSkills();
653
+ await triggerAIHook(createAIHookEvent('agent', 'skills-reloaded', undefined, { skillCount: count }));
654
+ if (count >= 0) logger.info(`[技能热重载] 已更新,当前工作区技能数: ${count}`);
655
+ } catch (e: any) {
656
+ logger.warn(`[技能热重载] 失败: ${e.message}`);
657
+ }
658
+ }, 400);
659
+ };
660
+ for (const dir of [workspaceSkillDir, localSkillDir]) {
661
+ if (fs.existsSync(dir)) {
662
+ try {
663
+ const w = fs.watch(dir, { recursive: true }, onSkillDirChange);
664
+ skillWatchers.push(w);
665
+ logger.debug(`[技能热重载] 监听目录: ${dir}`);
666
+ } catch (e: any) {
667
+ logger.debug(`[技能热重载] 无法监听 ${dir}: ${e.message}`);
668
+ }
669
+ }
670
+ }
622
671
  })();
623
672
 
624
- return () => disposers.forEach(d => d());
673
+ return () => {
674
+ disposers.forEach(d => d());
675
+ skillWatchers.forEach(w => w.close());
676
+ skillWatchers = [];
677
+ if (skillReloadDebounce) clearTimeout(skillReloadDebounce);
678
+ };
625
679
  });
626
680
  }
package/src/ai/service.ts CHANGED
@@ -44,6 +44,7 @@ export class AIService {
44
44
  private sessionConfig: { maxHistory?: number; expireMs?: number };
45
45
  private contextConfig: ContextConfig;
46
46
  private triggerConfig: AITriggerConfig;
47
+ private agentConfig: AIConfig['agent'];
47
48
  private plugin?: Plugin;
48
49
  private customTools: Map<string, AgentTool> = new Map();
49
50
 
@@ -52,6 +53,7 @@ export class AIService {
52
53
  this.sessionConfig = config.sessions || {};
53
54
  this.contextConfig = config.context || {};
54
55
  this.triggerConfig = config.trigger || {};
56
+ this.agentConfig = config.agent;
55
57
  this.sessions = createMemorySessionManager(this.sessionConfig);
56
58
  this.builtinTools = getBuiltinTools().map(tool => this.convertToolToAgentTool(tool.toTool()));
57
59
 
@@ -253,6 +255,8 @@ ${preExecutedData ? `\n已自动获取的数据:${preExecutedData}\n` : ''}
253
255
  getContextConfig(): ContextConfig { return this.contextConfig; }
254
256
  getSessionConfig() { return this.sessionConfig; }
255
257
  getTriggerConfig(): AITriggerConfig { return this.triggerConfig; }
258
+ /** Agent 配置(如 disabledTools / allowedTools),供 ZhinAgent 使用 */
259
+ getAgentConfig(): AIConfig['agent'] { return this.agentConfig; }
256
260
 
257
261
  registerProvider(provider: AIProvider): void { this.providers.set(provider.name, provider); }
258
262
  getProvider(name?: string): AIProvider {
package/src/ai/types.ts CHANGED
@@ -174,6 +174,8 @@ export interface AgentTool {
174
174
  permissionLevel?: number;
175
175
  /** 是否允许预执行(opt-in),默认 false */
176
176
  preExecutable?: boolean;
177
+ /** 工具分类(如 file / shell / web),用于 formatToolTitle 等展示 */
178
+ kind?: string;
177
179
  }
178
180
 
179
181
  /**
@@ -274,6 +276,19 @@ export interface AIConfig {
274
276
  /** 自定义总结提示词 */
275
277
  summaryPrompt?: string;
276
278
  };
279
+ /** Agent 工具开关与执行安全 */
280
+ agent?: {
281
+ /** 禁用的工具名列表,这些工具不会下发给 AI */
282
+ disabledTools?: string[];
283
+ /** 仅允许的工具名列表;若设置则只下发列表中的工具(与 disabledTools 二选一,allowedTools 优先) */
284
+ allowedTools?: string[];
285
+ /** bash 执行策略:deny=禁止执行,allowlist=仅允许列表内命令,full=不限制 */
286
+ execSecurity?: 'deny' | 'allowlist' | 'full';
287
+ /** allowlist 模式下允许的命令(支持正则字符串,如 "^ls "、"^cat ") */
288
+ execAllowlist?: string[];
289
+ /** allowlist 未命中时:true=需审批(当前实现为拒绝并提示),false=直接拒绝 */
290
+ execAsk?: boolean;
291
+ };
277
292
  /** AI 触发配置 */
278
293
  trigger?: {
279
294
  /** 是否启用(默认 true) */
@@ -52,6 +52,25 @@ const logger = new Logger(null, 'ZhinAgent');
52
52
  /** 高精度计时 */
53
53
  const now = () => performance.now();
54
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
+
55
74
  // ============================================================================
56
75
  // 配置
57
76
  // ============================================================================
@@ -85,6 +104,16 @@ export interface ZhinAgentConfig {
85
104
  contextTokens?: number;
86
105
  /** 历史记录最大占比(默认 0.5 = 50%) */
87
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;
88
117
  }
89
118
 
90
119
  const DEFAULT_CONFIG: Required<ZhinAgentConfig> = {
@@ -102,6 +131,11 @@ const DEFAULT_CONFIG: Required<ZhinAgentConfig> = {
102
131
  visionModel: '',
103
132
  contextTokens: DEFAULT_CONTEXT_TOKENS,
104
133
  maxHistoryShare: 0.5,
134
+ disabledTools: [],
135
+ allowedTools: [], // 空数组表示不限制;非空时仅允许列表中的工具
136
+ execSecurity: 'deny', // 默认禁止 bash,避免误用
137
+ execAllowlist: [],
138
+ execAsk: false,
105
139
  };
106
140
 
107
141
  // ============================================================================
@@ -392,15 +426,15 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
392
426
 
393
427
  // 始终传递所有工具给 Agent,因为 activate_skill 激活后可能需要调用
394
428
  // 之前被分类为 noParamTools 的工具(确保技能中引用的所有工具都可用)
395
- const agentTools = allTools;
429
+ const agentTools = this.applyExecPolicyToTools(allTools);
396
430
  const agent = createAgent(this.provider, {
397
431
  systemPrompt,
398
432
  tools: agentTools,
399
433
  maxIterations: this.config.maxIterations,
400
434
  });
401
435
 
402
- // Agent 路径也注入历史上下文
403
- const result = await agent.run(content, historyMessages);
436
+ const userMessageWithHistory = buildUserMessageWithHistory(historyMessages, content);
437
+ const result = await agent.run(userMessageWithHistory, []);
404
438
  reply = result.content || this.fallbackFormat(result.toolCalls);
405
439
  logger.info(`[Agent 路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, Agent=${(now() - tAgent).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
406
440
  }
@@ -619,17 +653,83 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
619
653
  }
620
654
  }
621
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
+
622
674
  // 诊断日志:显示收集的工具总数、过滤后的数量、以及列表
623
- if (filtered.length > 0) {
675
+ if (final.length > 0) {
624
676
  logger.debug(
625
- `[工具收集] 收集了 ${collected.length} 个工具,过滤后 ${filtered.length} 个,` +
626
- `用户消息相关性最高的: ${filtered.slice(0, 3).map(t => t.name).join(', ')}`
677
+ `[工具收集] 收集了 ${collected.length} 个工具,过滤后 ${final.length} 个,` +
678
+ `用户消息相关性最高的: ${final.slice(0, 3).map(t => t.name).join(', ')}`
627
679
  );
628
680
  } else {
629
681
  logger.debug(`[工具收集] 收集了 ${collected.length} 个工具,但过滤后 0 个(没有超过相关性阈值的)`);
630
682
  }
631
683
 
632
- return filtered;
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
+ });
633
733
  }
634
734
 
635
735
  // ── 辅助方法 ─────────────────────────────────────────────────────────
@@ -709,6 +809,7 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
709
809
  if (tool.keywords?.length) at.keywords = tool.keywords;
710
810
  if (tool.permissionLevel) at.permissionLevel = PERM_MAP[tool.permissionLevel] ?? 0;
711
811
  if (tool.preExecutable) at.preExecutable = true;
812
+ if ((tool as any).kind) at.kind = (tool as any).kind;
712
813
  return at;
713
814
  }
714
815
 
@@ -738,7 +839,7 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
738
839
  lines.push(this.config.persona);
739
840
  lines.push('');
740
841
 
741
- // §2 核心规则(精简为 6 条短句)
842
+ // §2 核心规则(精简为 7 条短句)
742
843
  lines.push('## 规则');
743
844
  lines.push('1. 直接调用工具执行操作,不要描述步骤或解释意图');
744
845
  lines.push('2. 时间/日期问题:直接用下方"当前时间"回答,不调工具');
@@ -746,6 +847,7 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
746
847
  lines.push('4. activate_skill 返回后,必须继续调用其中指导的工具,不要停');
747
848
  lines.push('5. 所有回答必须基于工具返回的实际数据');
748
849
  lines.push('6. 工具失败时尝试替代方案,不要直接把错误丢给用户');
850
+ lines.push('7. 只根据用户**最后一条**消息作答,前面的对话仅作背景');
749
851
  lines.push('');
750
852
 
751
853
  // §3 技能列表(紧凑格式)
@@ -1003,10 +1105,12 @@ ${preData ? `\n已获取数据:${preData}\n` : ''}`;
1003
1105
  onChunk?: OnChunkCallback,
1004
1106
  ): Promise<string> {
1005
1107
  const model = this.provider.models[0];
1108
+ const userContent = history.length > 0
1109
+ ? buildUserMessageWithHistory(history, content)
1110
+ : content;
1006
1111
  const messages: ChatMessage[] = [
1007
1112
  { role: 'system', content: systemPrompt },
1008
- ...history,
1009
- { role: 'user', content },
1113
+ { role: 'user', content: userContent },
1010
1114
  ];
1011
1115
 
1012
1116
  // 优先流式(对 Ollama 等本地模型有明显提速)
package/src/built/tool.ts CHANGED
@@ -188,6 +188,7 @@ export class ZhinTool {
188
188
  #hidden: boolean = false;
189
189
  #source?: string;
190
190
  #preExecutable: boolean = false;
191
+ #kind?: string;
191
192
 
192
193
  constructor(name: string) {
193
194
  this.#name = name;
@@ -265,6 +266,12 @@ export class ZhinTool {
265
266
  return this;
266
267
  }
267
268
 
269
+ /** 设置工具分类(如 file / shell / web),用于展示与 TOOLS.md 协同 */
270
+ kind(value: string): this {
271
+ this.#kind = value;
272
+ return this;
273
+ }
274
+
268
275
  usage(...usage: string[]): this {
269
276
  this.#commandConfig.usage = [...(this.#commandConfig.usage || []), ...usage];
270
277
  return this;
@@ -365,6 +372,7 @@ export class ZhinTool {
365
372
  if (this.#source) tool.source = this.#source;
366
373
  if (this.#keywords.length > 0) tool.keywords = this.#keywords;
367
374
  if (this.#preExecutable) tool.preExecutable = true;
375
+ if (this.#kind) tool.kind = this.#kind;
368
376
 
369
377
  if (!this.#commandCallback) {
370
378
  tool.command = false;
package/src/plugin.ts CHANGED
@@ -11,7 +11,7 @@ import { Schema } from "@zhin.js/schema";
11
11
  import type { Models, RegisteredAdapters, Tool, ToolContext } from "./types.js";
12
12
  import * as fs from "fs";
13
13
  import * as path from "path";
14
- import { fileURLToPath } from "url";
14
+ import { fileURLToPath, pathToFileURL } from "url";
15
15
  import logger, { Logger } from "@zhin.js/logger";
16
16
  import { compose, remove, resolveEntry } from "./utils.js";
17
17
  import { MessageMiddleware, RegisteredAdapter, MaybePromise, ArrayItem, SendOptions } from "./types.js";
@@ -927,7 +927,7 @@ export class Plugin extends EventEmitter<Plugin.Lifecycle> {
927
927
  loadedModules.set(realPath, plugin);
928
928
 
929
929
  await storage.run(plugin, async () => {
930
- await import(`${import.meta.resolve(entryFile)}?t=${Date.now()}`);
930
+ await import(`${pathToFileURL(entryFile).href}?t=${Date.now()}`);
931
931
  });
932
932
 
933
933
  return plugin;
package/src/types.ts CHANGED
@@ -380,6 +380,9 @@ export interface Tool {
380
380
  * 默认为 false,即不预执行。
381
381
  */
382
382
  preExecutable?: boolean;
383
+
384
+ /** 工具分类(如 file / shell / web),用于展示与 TOOLS.md 协同 */
385
+ kind?: string;
383
386
  }
384
387
 
385
388
  /**