@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.
- package/CHANGELOG.md +18 -0
- package/lib/ai/agent.d.ts +4 -0
- package/lib/ai/agent.d.ts.map +1 -1
- package/lib/ai/agent.js +22 -0
- package/lib/ai/agent.js.map +1 -1
- package/lib/ai/builtin-tools.d.ts +2 -1
- package/lib/ai/builtin-tools.d.ts.map +1 -1
- package/lib/ai/builtin-tools.js +86 -7
- package/lib/ai/builtin-tools.js.map +1 -1
- package/lib/ai/cron-engine.d.ts +92 -0
- package/lib/ai/cron-engine.d.ts.map +1 -0
- package/lib/ai/cron-engine.js +278 -0
- package/lib/ai/cron-engine.js.map +1 -0
- package/lib/ai/index.d.ts +3 -1
- package/lib/ai/index.d.ts.map +1 -1
- package/lib/ai/index.js +3 -1
- package/lib/ai/index.js.map +1 -1
- package/lib/ai/init.d.ts.map +1 -1
- package/lib/ai/init.js +115 -51
- package/lib/ai/init.js.map +1 -1
- package/lib/ai/service.d.ts +3 -0
- package/lib/ai/service.d.ts.map +1 -1
- package/lib/ai/service.js +4 -0
- package/lib/ai/service.js.map +1 -1
- package/lib/ai/types.d.ts +15 -0
- package/lib/ai/types.d.ts.map +1 -1
- package/lib/ai/zhin-agent.d.ts +18 -0
- package/lib/ai/zhin-agent.d.ts.map +1 -1
- package/lib/ai/zhin-agent.js +102 -10
- package/lib/ai/zhin-agent.js.map +1 -1
- package/lib/built/tool.d.ts +2 -0
- package/lib/built/tool.d.ts.map +1 -1
- package/lib/built/tool.js +8 -0
- package/lib/built/tool.js.map +1 -1
- package/lib/plugin.js +2 -2
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +2 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ai/agent.ts +21 -0
- package/src/ai/builtin-tools.ts +79 -7
- package/src/ai/cron-engine.ts +337 -0
- package/src/ai/index.ts +21 -1
- package/src/ai/init.ts +109 -55
- package/src/ai/service.ts +4 -0
- package/src/ai/types.ts +15 -0
- package/src/ai/zhin-agent.ts +114 -10
- package/src/built/tool.ts +8 -0
- package/src/plugin.ts +2 -2
- 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'
|
|
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
|
|
183
|
+
const agentConfig = ai.getAgentConfig();
|
|
184
|
+
const agent = new ZhinAgent(provider, agentConfig);
|
|
180
185
|
zhinAgentInstance = agent;
|
|
181
186
|
|
|
182
|
-
const skillRegistry = root.inject('skill'
|
|
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
|
-
|
|
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
|
|
586
|
+
const count = await syncWorkspaceSkills();
|
|
521
587
|
const skillFeature = root.inject?.('skill') as SkillFeature | undefined;
|
|
522
|
-
if (
|
|
523
|
-
logger.
|
|
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 () =>
|
|
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) */
|
package/src/ai/zhin-agent.ts
CHANGED
|
@@ -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
|
-
|
|
403
|
-
const result = await agent.run(
|
|
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 (
|
|
675
|
+
if (final.length > 0) {
|
|
624
676
|
logger.debug(
|
|
625
|
-
`[工具收集] 收集了 ${collected.length} 个工具,过滤后 ${
|
|
626
|
-
`用户消息相关性最高的: ${
|
|
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
|
|
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 核心规则(精简为
|
|
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
|
-
|
|
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(`${
|
|
930
|
+
await import(`${pathToFileURL(entryFile).href}?t=${Date.now()}`);
|
|
931
931
|
});
|
|
932
932
|
|
|
933
933
|
return plugin;
|