@zhin.js/agent 0.0.17 → 0.0.18
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 +9 -0
- package/lib/builtin-tools.d.ts +2 -138
- package/lib/builtin-tools.d.ts.map +1 -1
- package/lib/builtin-tools.js +4 -723
- package/lib/builtin-tools.js.map +1 -1
- package/lib/discover-agents.d.ts +28 -0
- package/lib/discover-agents.d.ts.map +1 -0
- package/lib/discover-agents.js +116 -0
- package/lib/discover-agents.js.map +1 -0
- package/lib/discover-skills.d.ts +49 -0
- package/lib/discover-skills.d.ts.map +1 -0
- package/lib/discover-skills.js +297 -0
- package/lib/discover-skills.js.map +1 -0
- package/lib/discover-tools.d.ts +56 -0
- package/lib/discover-tools.d.ts.map +1 -0
- package/lib/discover-tools.js +263 -0
- package/lib/discover-tools.js.map +1 -0
- package/lib/discovery-utils.d.ts +27 -0
- package/lib/discovery-utils.d.ts.map +1 -0
- package/lib/discovery-utils.js +96 -0
- package/lib/discovery-utils.js.map +1 -0
- package/lib/init/create-zhin-agent.d.ts.map +1 -1
- package/lib/init/create-zhin-agent.js +2 -1
- package/lib/init/create-zhin-agent.js.map +1 -1
- package/lib/init/register-builtin-tools.d.ts.map +1 -1
- package/lib/init/register-builtin-tools.js +50 -54
- package/lib/init/register-builtin-tools.js.map +1 -1
- package/package.json +3 -3
- package/src/builtin-tools.ts +7 -830
- package/src/discover-agents.ts +138 -0
- package/src/discover-skills.ts +325 -0
- package/src/discover-tools.ts +302 -0
- package/src/discovery-utils.ts +96 -0
- package/src/init/create-zhin-agent.ts +2 -1
- package/src/init/register-builtin-tools.ts +50 -62
package/lib/builtin-tools.js
CHANGED
|
@@ -1,117 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AI 内置系统工具
|
|
3
3
|
*
|
|
4
|
-
* 借鉴 OpenClaw/MicroClaw 的实用工具设计,为 ZhinAgent 提供:
|
|
5
|
-
*
|
|
6
4
|
* 文件工具: read_file, write_file, edit_file, list_dir, glob, grep
|
|
7
5
|
* Shell: bash
|
|
8
6
|
* 网络: web_search, web_fetch
|
|
9
7
|
* 计划: todo_read, todo_write
|
|
10
8
|
* 记忆: read_memory, write_memory (AGENTS.md)
|
|
11
9
|
* 技能: activate_skill, install_skill
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 引导文件: SOUL.md, TOOLS.md, AGENTS.md 自动加载
|
|
10
|
+
*
|
|
11
|
+
* 发现逻辑已拆分到 discover-skills.ts / discover-agents.ts / discover-tools.ts
|
|
15
12
|
*/
|
|
16
13
|
import * as fs from 'fs';
|
|
17
|
-
import * as os from 'os';
|
|
18
14
|
import * as path from 'path';
|
|
19
15
|
import { exec } from 'child_process';
|
|
20
16
|
import { promisify } from 'util';
|
|
21
17
|
import { Logger } from '@zhin.js/core';
|
|
22
18
|
import { ZhinTool } from '@zhin.js/core';
|
|
23
19
|
import { assertFileAccess, checkBashCommandSafety, shellEscape } from './file-policy.js';
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
import { errMsg, expandHome, getDataDir, mergeSkillDirsWithResolver, nodeErrToFileMessage, } from './discovery-utils.js';
|
|
21
|
+
import { checkSkillDeps, extractSkillInstructions } from './discover-skills.js';
|
|
26
22
|
const execAsync = promisify(exec);
|
|
27
23
|
const logger = new Logger(null, 'builtin-tools');
|
|
28
|
-
function errMsg(e) {
|
|
29
|
-
return e instanceof Error ? e.message : String(e);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* 获取数据目录路径
|
|
33
|
-
*/
|
|
34
|
-
function getDataDir() {
|
|
35
|
-
const dir = path.join(process.cwd(), 'data');
|
|
36
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
-
return dir;
|
|
38
|
-
}
|
|
39
|
-
/** Workspace / ~/.zhin / data 下 skills 根目录(与 activate_skill 扫描顺序一致的前缀) */
|
|
40
|
-
function buildStandardSkillDirs() {
|
|
41
|
-
return [
|
|
42
|
-
path.join(process.cwd(), 'skills'),
|
|
43
|
-
path.join(os.homedir(), '.zhin', 'skills'),
|
|
44
|
-
path.join(getDataDir(), 'skills'),
|
|
45
|
-
];
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* 从根插件树收集:根插件与**直接子插件**包目录下的 `skills/`(其下为 `<name>/SKILL.md`)
|
|
49
|
-
*/
|
|
50
|
-
export function collectPluginSkillSearchRoots(root) {
|
|
51
|
-
if (!root)
|
|
52
|
-
return [];
|
|
53
|
-
const dirs = [];
|
|
54
|
-
const push = (d) => {
|
|
55
|
-
if (d && !dirs.includes(d))
|
|
56
|
-
dirs.push(d);
|
|
57
|
-
};
|
|
58
|
-
const fromPlugin = (p) => {
|
|
59
|
-
if (!p?.filePath)
|
|
60
|
-
return;
|
|
61
|
-
const dir = path.dirname(p.filePath);
|
|
62
|
-
push(path.join(dir, 'skills'));
|
|
63
|
-
// Also check package root when filePath is under src/ or lib/
|
|
64
|
-
const dirName = path.basename(dir);
|
|
65
|
-
if (dirName === 'src' || dirName === 'lib') {
|
|
66
|
-
push(path.join(path.dirname(dir), 'skills'));
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
fromPlugin(root);
|
|
70
|
-
for (const child of root.children || []) {
|
|
71
|
-
fromPlugin(child);
|
|
72
|
-
}
|
|
73
|
-
return dirs;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* 技能发现与 activate_skill 查找共用:标准目录 + 已加载插件包 skills/
|
|
77
|
-
*/
|
|
78
|
-
export function getSkillSearchDirectories(root) {
|
|
79
|
-
const list = [...buildStandardSkillDirs()];
|
|
80
|
-
for (const d of collectPluginSkillSearchRoots(root ?? undefined)) {
|
|
81
|
-
if (!list.includes(d))
|
|
82
|
-
list.push(d);
|
|
83
|
-
}
|
|
84
|
-
return list;
|
|
85
|
-
}
|
|
86
|
-
function mergeSkillDirsWithResolver(resolver) {
|
|
87
|
-
const list = [...buildStandardSkillDirs()];
|
|
88
|
-
for (const d of resolver?.() ?? []) {
|
|
89
|
-
if (d && !list.includes(d))
|
|
90
|
-
list.push(d);
|
|
91
|
-
}
|
|
92
|
-
return list;
|
|
93
|
-
}
|
|
94
|
-
/** 展开路径中的 ~ 为实际 home 目录 */
|
|
95
|
-
function expandHome(p) {
|
|
96
|
-
if (p === '~')
|
|
97
|
-
return os.homedir();
|
|
98
|
-
if (p.startsWith('~/') || p.startsWith('~\\'))
|
|
99
|
-
return path.join(os.homedir(), p.slice(2));
|
|
100
|
-
return p;
|
|
101
|
-
}
|
|
102
|
-
/** 将 Node 文件错误转为 miniclawd 风格的结构化短句,便于模型区分并重试 */
|
|
103
|
-
function nodeErrToFileMessage(err, filePath, kind) {
|
|
104
|
-
const e = err;
|
|
105
|
-
if (e?.code === 'ENOENT') {
|
|
106
|
-
if (kind === 'list')
|
|
107
|
-
return `Error: Directory not found: ${filePath}`;
|
|
108
|
-
return `Error: File not found: ${filePath}`;
|
|
109
|
-
}
|
|
110
|
-
if (e?.code === 'EACCES')
|
|
111
|
-
return `Error: Permission denied: ${filePath}`;
|
|
112
|
-
const action = kind === 'read' ? 'reading file' : kind === 'write' ? 'writing file' : kind === 'edit' ? 'editing file' : 'listing directory';
|
|
113
|
-
return `Error ${action}: ${e?.message ?? String(err)}`;
|
|
114
|
-
}
|
|
115
24
|
/**
|
|
116
25
|
* 创建所有内置系统工具
|
|
117
26
|
*/
|
|
@@ -502,7 +411,6 @@ export function createBuiltinTools(options) {
|
|
|
502
411
|
const instructions = extractSkillInstructions(args.name, fullContent, skillMaxChars);
|
|
503
412
|
return depWarning ? `${depWarning}\n\n${instructions}` : instructions;
|
|
504
413
|
}
|
|
505
|
-
// 回退到目录扫描(与 discoverWorkspaceSkills 顺序一致)
|
|
506
414
|
for (const dir of skillDirList()) {
|
|
507
415
|
const skillPath = path.join(dir, args.name, 'SKILL.md');
|
|
508
416
|
if (fs.existsSync(skillPath)) {
|
|
@@ -563,631 +471,4 @@ export function createBuiltinTools(options) {
|
|
|
563
471
|
}));
|
|
564
472
|
return tools;
|
|
565
473
|
}
|
|
566
|
-
/**
|
|
567
|
-
* 检查技能声明的依赖是否在环境中可用;若有缺失返回提示文案,否则返回空字符串
|
|
568
|
-
*/
|
|
569
|
-
async function checkSkillDeps(content) {
|
|
570
|
-
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
571
|
-
if (!fmMatch)
|
|
572
|
-
return '';
|
|
573
|
-
let jsYaml;
|
|
574
|
-
try {
|
|
575
|
-
jsYaml = await import('js-yaml');
|
|
576
|
-
if (jsYaml.default)
|
|
577
|
-
jsYaml = jsYaml.default;
|
|
578
|
-
}
|
|
579
|
-
catch {
|
|
580
|
-
return '';
|
|
581
|
-
}
|
|
582
|
-
const metadata = jsYaml.load(fmMatch[1]);
|
|
583
|
-
if (!metadata)
|
|
584
|
-
return '';
|
|
585
|
-
const compat = metadata.compatibility || {};
|
|
586
|
-
const deps = compat.deps || metadata.deps;
|
|
587
|
-
if (!deps || !Array.isArray(deps))
|
|
588
|
-
return '';
|
|
589
|
-
const missing = [];
|
|
590
|
-
for (const dep of deps) {
|
|
591
|
-
try {
|
|
592
|
-
await execAsync(`which ${dep} 2>/dev/null`);
|
|
593
|
-
}
|
|
594
|
-
catch {
|
|
595
|
-
missing.push(dep);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
if (missing.length === 0)
|
|
599
|
-
return '';
|
|
600
|
-
return `⚠️ 当前环境缺少以下依赖,请先安装后再使用本技能:${missing.join(', ')}`;
|
|
601
|
-
}
|
|
602
|
-
/**
|
|
603
|
-
* 从 SKILL.md 全文中提取精简的执行指令
|
|
604
|
-
* 只保留 frontmatter(工具列表)和执行规则,去掉示例、测试场景等冗余内容
|
|
605
|
-
* 这样可以大幅减少 token 占用,让小模型能有足够空间继续调用工具
|
|
606
|
-
*/
|
|
607
|
-
function extractSkillInstructions(name, content, maxBodyLen = 4000) {
|
|
608
|
-
const lines = [];
|
|
609
|
-
lines.push(`Skill '${name}' activated. 请立即根据以下指导执行工具调用:`);
|
|
610
|
-
lines.push('');
|
|
611
|
-
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
612
|
-
if (fmMatch) {
|
|
613
|
-
const fmContent = fmMatch[1];
|
|
614
|
-
const toolsMatch = fmContent.match(/tools:\s*\n((?:\s+-\s+.+\n?)+)/);
|
|
615
|
-
if (toolsMatch) {
|
|
616
|
-
lines.push('## 可用工具');
|
|
617
|
-
lines.push(toolsMatch[0].trim());
|
|
618
|
-
lines.push('');
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
const bodyAfterFm = fmMatch && fmMatch.index !== undefined
|
|
622
|
-
? content.slice(fmMatch.index + fmMatch[0].length).replace(/^\s+/, '')
|
|
623
|
-
: content;
|
|
624
|
-
// Priority: "## 快速操作" / "## Quick Actions" summary for small models
|
|
625
|
-
const quickActionsMatch = bodyAfterFm.match(/## (?:快速操作|Quick\s*Actions)[\s\S]*?(?=\n## [^\s]|$)/i);
|
|
626
|
-
if (quickActionsMatch && maxBodyLen <= 2000) {
|
|
627
|
-
lines.push(quickActionsMatch[0].trim());
|
|
628
|
-
lines.push('');
|
|
629
|
-
lines.push('## 立即行动');
|
|
630
|
-
lines.push('根据上面的指导,立即调用工具完成用户请求。禁止重复调用 activate_skill,禁止用文本描述代替实际工具调用。');
|
|
631
|
-
return lines.join('\n');
|
|
632
|
-
}
|
|
633
|
-
const rulesMatch = content.match(/## 执行规则[\s\S]*?(?=\n## [^\s]|$)/);
|
|
634
|
-
const workflowMatch = content.match(/## (?:Workflow|Instructions|使用说明)[\s\S]*?(?=\n## [^\s]|$)/);
|
|
635
|
-
if (rulesMatch) {
|
|
636
|
-
lines.push(rulesMatch[0].trim());
|
|
637
|
-
lines.push('');
|
|
638
|
-
}
|
|
639
|
-
else if (workflowMatch) {
|
|
640
|
-
lines.push(workflowMatch[0].trim());
|
|
641
|
-
lines.push('');
|
|
642
|
-
}
|
|
643
|
-
else if (bodyAfterFm.trim()) {
|
|
644
|
-
const firstH2 = bodyAfterFm.match(/\n## [^\s]/);
|
|
645
|
-
const intro = firstH2 ? bodyAfterFm.slice(0, firstH2.index).trim() : bodyAfterFm.trim();
|
|
646
|
-
const quickStartMatch = bodyAfterFm.match(/## (?:快速开始|Quick\s*Start|Getting\s*Started)[\s\S]*?(?=\n## [^\s]|$)/i);
|
|
647
|
-
const authMatch = bodyAfterFm.match(/## (?:认证|Authentication|Auth)[\s\S]*?(?=\n## [^\s]|$)/i);
|
|
648
|
-
if (quickStartMatch || (intro.length < 200 && bodyAfterFm.length > intro.length)) {
|
|
649
|
-
lines.push('## 指导');
|
|
650
|
-
lines.push(intro);
|
|
651
|
-
lines.push('');
|
|
652
|
-
const extra = [];
|
|
653
|
-
if (quickStartMatch)
|
|
654
|
-
extra.push(quickStartMatch[0].trim());
|
|
655
|
-
if (authMatch)
|
|
656
|
-
extra.push(authMatch[0].trim());
|
|
657
|
-
if (extra.length > 0) {
|
|
658
|
-
const joined = extra.join('\n\n');
|
|
659
|
-
lines.push(joined.length > maxBodyLen ? joined.slice(0, maxBodyLen) + '\n...(truncated)' : joined);
|
|
660
|
-
}
|
|
661
|
-
else {
|
|
662
|
-
const rest = bodyAfterFm.slice(intro.length).trim();
|
|
663
|
-
lines.push(rest.length > maxBodyLen ? rest.slice(0, maxBodyLen) + '\n...(truncated)' : rest);
|
|
664
|
-
}
|
|
665
|
-
lines.push('');
|
|
666
|
-
}
|
|
667
|
-
else if (intro) {
|
|
668
|
-
lines.push('## 指导');
|
|
669
|
-
lines.push(intro);
|
|
670
|
-
lines.push('');
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
lines.push('## 立即行动');
|
|
674
|
-
lines.push('根据上面的指导,立即调用工具完成用户请求。禁止重复调用 activate_skill,禁止用文本描述代替实际工具调用。');
|
|
675
|
-
return lines.join('\n');
|
|
676
|
-
}
|
|
677
|
-
/**
|
|
678
|
-
* 扫描技能目录,发现 SKILL.md 技能文件
|
|
679
|
-
* 加载顺序:Workspace(cwd/skills)> Local(~/.zhin/skills)> data/skills > 已加载插件包 skills/,同名先发现者优先
|
|
680
|
-
* 支持平台/依赖兼容性过滤。内置技能由 create-zhin 在创建项目时写入 skills/summarize 等。
|
|
681
|
-
*
|
|
682
|
-
* @param root 根插件(可选):用于追加插件包内 `skills/` 扫描,与 `activate_skill` 查找路径一致
|
|
683
|
-
*/
|
|
684
|
-
export async function discoverWorkspaceSkills(root) {
|
|
685
|
-
const skills = [];
|
|
686
|
-
const seenNames = new Set();
|
|
687
|
-
const dataDir = getDataDir();
|
|
688
|
-
const skillDirs = getSkillSearchDirectories(root ?? undefined);
|
|
689
|
-
// 确保 data/skills 目录存在
|
|
690
|
-
const defaultSkillDir = path.join(dataDir, 'skills');
|
|
691
|
-
if (!fs.existsSync(defaultSkillDir)) {
|
|
692
|
-
fs.mkdirSync(defaultSkillDir, { recursive: true });
|
|
693
|
-
logger.debug(`Created skill directory: ${defaultSkillDir}`);
|
|
694
|
-
}
|
|
695
|
-
for (const skillsDir of skillDirs) {
|
|
696
|
-
if (!fs.existsSync(skillsDir))
|
|
697
|
-
continue;
|
|
698
|
-
let entries;
|
|
699
|
-
try {
|
|
700
|
-
entries = await fs.promises.readdir(skillsDir, { withFileTypes: true });
|
|
701
|
-
}
|
|
702
|
-
catch {
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
for (const entry of entries) {
|
|
706
|
-
if (!entry.isDirectory())
|
|
707
|
-
continue;
|
|
708
|
-
const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
709
|
-
if (!fs.existsSync(skillMdPath))
|
|
710
|
-
continue;
|
|
711
|
-
try {
|
|
712
|
-
const content = await fs.promises.readFile(skillMdPath, 'utf-8');
|
|
713
|
-
// 改进的 frontmatter 正则:支持多种换行符、可选的尾部空白
|
|
714
|
-
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
|
|
715
|
-
if (!match) {
|
|
716
|
-
logger.debug(`Skill文件 ${skillMdPath} 没有有效的frontmatter格式`);
|
|
717
|
-
continue;
|
|
718
|
-
}
|
|
719
|
-
let jsYaml;
|
|
720
|
-
try {
|
|
721
|
-
jsYaml = await import('js-yaml');
|
|
722
|
-
if (jsYaml.default)
|
|
723
|
-
jsYaml = jsYaml.default;
|
|
724
|
-
}
|
|
725
|
-
catch (e) {
|
|
726
|
-
logger.warn(`Unable to import js-yaml module: ${e}`);
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
const metadata = jsYaml.load(match[1]);
|
|
730
|
-
if (!metadata || !metadata.name || !metadata.description) {
|
|
731
|
-
logger.debug(`Skill文件 ${skillMdPath} 缺少必需的 name/description 字段`);
|
|
732
|
-
continue;
|
|
733
|
-
}
|
|
734
|
-
// 平台兼容检查
|
|
735
|
-
const compat = metadata.compatibility || {};
|
|
736
|
-
if (compat.os && Array.isArray(compat.os)) {
|
|
737
|
-
const currentOs = process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'windows' : 'linux';
|
|
738
|
-
if (!compat.os.includes(currentOs)) {
|
|
739
|
-
logger.debug(`Skipping skill '${metadata.name}' (unsupported OS)`);
|
|
740
|
-
continue;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
// 依赖检查:支持 metadata.requires.bins / requires.env 或 compat.deps / metadata.deps
|
|
744
|
-
const requiresBins = metadata.requires?.bins || compat.deps || metadata.deps || [];
|
|
745
|
-
const requiresEnv = metadata.requires?.env || [];
|
|
746
|
-
const binsToCheck = Array.isArray(requiresBins) ? requiresBins : [];
|
|
747
|
-
const envToCheck = Array.isArray(requiresEnv) ? requiresEnv : [];
|
|
748
|
-
const requiresMissing = [];
|
|
749
|
-
for (const bin of binsToCheck) {
|
|
750
|
-
try {
|
|
751
|
-
await execAsync(`which ${bin} 2>/dev/null`);
|
|
752
|
-
}
|
|
753
|
-
catch {
|
|
754
|
-
requiresMissing.push(`CLI: ${bin}`);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
for (const envKey of envToCheck) {
|
|
758
|
-
if (!process.env[envKey]) {
|
|
759
|
-
requiresMissing.push(`ENV: ${envKey}`);
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
const available = requiresMissing.length === 0;
|
|
763
|
-
if (seenNames.has(metadata.name)) {
|
|
764
|
-
logger.debug(`Skill '${metadata.name}' 已由先序目录加载,跳过: ${skillMdPath}`);
|
|
765
|
-
continue;
|
|
766
|
-
}
|
|
767
|
-
seenNames.add(metadata.name);
|
|
768
|
-
skills.push({
|
|
769
|
-
name: metadata.name,
|
|
770
|
-
description: metadata.description,
|
|
771
|
-
keywords: metadata.keywords || [],
|
|
772
|
-
tags: [...(metadata.tags || []), 'workspace-skill'],
|
|
773
|
-
toolNames: Array.isArray(metadata.tools) ? metadata.tools : [],
|
|
774
|
-
filePath: skillMdPath,
|
|
775
|
-
always: Boolean(metadata.always),
|
|
776
|
-
available,
|
|
777
|
-
requiresMissing: requiresMissing.length > 0 ? requiresMissing : undefined,
|
|
778
|
-
});
|
|
779
|
-
logger.debug(`Skill发现成功: ${metadata.name}, tools: ${JSON.stringify(metadata.tools || [])}`);
|
|
780
|
-
}
|
|
781
|
-
catch (e) {
|
|
782
|
-
logger.warn(`Failed to parse SKILL.md in ${skillMdPath}:`, e);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
if (skills.length > 0) {
|
|
787
|
-
logger.info(`发现 ${skills.length} 个工作区技能: ${skills.map(s => `${s.name}(tools:${(s.toolNames || []).join(',')})`).join(', ')}`);
|
|
788
|
-
}
|
|
789
|
-
return skills;
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* 获取 frontmatter 中 always: true 的技能名列表(用于常驻注入 system prompt)
|
|
793
|
-
*/
|
|
794
|
-
export function getAlwaysSkillNames(skills) {
|
|
795
|
-
return skills.filter(s => s.always && s.available).map(s => s.name);
|
|
796
|
-
}
|
|
797
|
-
/**
|
|
798
|
-
* 去除 frontmatter,返回正文
|
|
799
|
-
*/
|
|
800
|
-
function stripFrontmatter(content) {
|
|
801
|
-
const match = content.match(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/);
|
|
802
|
-
if (match) {
|
|
803
|
-
return content.slice(match[0].length).trim();
|
|
804
|
-
}
|
|
805
|
-
return content.trim();
|
|
806
|
-
}
|
|
807
|
-
/**
|
|
808
|
-
* 加载 always 技能的正文内容并拼接为「Active Skills」段
|
|
809
|
-
*/
|
|
810
|
-
export async function loadAlwaysSkillsContent(skills) {
|
|
811
|
-
const always = skills.filter(s => s.always && s.available);
|
|
812
|
-
if (always.length === 0)
|
|
813
|
-
return '';
|
|
814
|
-
const parts = [];
|
|
815
|
-
for (const s of always) {
|
|
816
|
-
try {
|
|
817
|
-
const content = await fs.promises.readFile(s.filePath, 'utf-8');
|
|
818
|
-
const body = stripFrontmatter(content);
|
|
819
|
-
parts.push(`### Skill: ${s.name}\n\n${body}`);
|
|
820
|
-
}
|
|
821
|
-
catch (e) {
|
|
822
|
-
logger.warn(`Failed to load always skill ${s.name}: ${e.message}`);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
return parts.join('\n\n---\n\n');
|
|
826
|
-
}
|
|
827
|
-
/** 转义 XML 特殊字符 */
|
|
828
|
-
function escapeXml(s) {
|
|
829
|
-
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
830
|
-
}
|
|
831
|
-
/**
|
|
832
|
-
* 构建技能列表的 XML 摘要,供 model 区分可用/不可用及缺失依赖
|
|
833
|
-
*/
|
|
834
|
-
export function buildSkillsSummaryXML(skills) {
|
|
835
|
-
if (skills.length === 0)
|
|
836
|
-
return '';
|
|
837
|
-
const lines = ['<skills>'];
|
|
838
|
-
for (const s of skills) {
|
|
839
|
-
const available = s.available !== false;
|
|
840
|
-
lines.push(` <skill available="${available}">`);
|
|
841
|
-
lines.push(` <name>${escapeXml(s.name)}</name>`);
|
|
842
|
-
lines.push(` <description>${escapeXml(s.description)}</description>`);
|
|
843
|
-
lines.push(` <location>${escapeXml(s.filePath)}</location>`);
|
|
844
|
-
if (!available && s.requiresMissing && s.requiresMissing.length > 0) {
|
|
845
|
-
lines.push(` <requires>${escapeXml(s.requiresMissing.join(', '))}</requires>`);
|
|
846
|
-
}
|
|
847
|
-
lines.push(' </skill>');
|
|
848
|
-
}
|
|
849
|
-
lines.push('</skills>');
|
|
850
|
-
return lines.join('\n');
|
|
851
|
-
}
|
|
852
|
-
/**
|
|
853
|
-
* 扫描 agents/ 目录,发现 *.agent.md 文件
|
|
854
|
-
* 加载顺序与 skills 一致:Workspace > ~/.zhin > data > 插件包
|
|
855
|
-
* 同名先发现者优先
|
|
856
|
-
*/
|
|
857
|
-
export async function discoverWorkspaceAgents(root) {
|
|
858
|
-
const agents = [];
|
|
859
|
-
const seenNames = new Set();
|
|
860
|
-
// 构建扫描目录:标准目录的 agents/ + 插件包的 agents/
|
|
861
|
-
const agentDirs = [
|
|
862
|
-
path.join(process.cwd(), 'agents'),
|
|
863
|
-
path.join(os.homedir(), '.zhin', 'agents'),
|
|
864
|
-
path.join(getDataDir(), 'agents'),
|
|
865
|
-
];
|
|
866
|
-
if (root) {
|
|
867
|
-
const addPluginDir = (p) => {
|
|
868
|
-
if (!p?.filePath)
|
|
869
|
-
return;
|
|
870
|
-
const dir = path.dirname(p.filePath);
|
|
871
|
-
const d = path.join(dir, 'agents');
|
|
872
|
-
if (!agentDirs.includes(d))
|
|
873
|
-
agentDirs.push(d);
|
|
874
|
-
// Also check package root when filePath is under src/ or lib/
|
|
875
|
-
const dirName = path.basename(dir);
|
|
876
|
-
if (dirName === 'src' || dirName === 'lib') {
|
|
877
|
-
const d2 = path.join(path.dirname(dir), 'agents');
|
|
878
|
-
if (!agentDirs.includes(d2))
|
|
879
|
-
agentDirs.push(d2);
|
|
880
|
-
}
|
|
881
|
-
};
|
|
882
|
-
addPluginDir(root);
|
|
883
|
-
for (const child of root.children || []) {
|
|
884
|
-
addPluginDir(child);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
for (const agentsDir of agentDirs) {
|
|
888
|
-
if (!fs.existsSync(agentsDir))
|
|
889
|
-
continue;
|
|
890
|
-
let entries;
|
|
891
|
-
try {
|
|
892
|
-
entries = await fs.promises.readdir(agentsDir, { withFileTypes: true });
|
|
893
|
-
}
|
|
894
|
-
catch {
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
for (const entry of entries) {
|
|
898
|
-
// 支持两种结构:
|
|
899
|
-
// 1. agents/<name>.agent.md(扁平文件)
|
|
900
|
-
// 2. agents/<name>/<name>.agent.md(目录结构)
|
|
901
|
-
let agentMdPath;
|
|
902
|
-
if (entry.isFile() && entry.name.endsWith('.agent.md')) {
|
|
903
|
-
agentMdPath = path.join(agentsDir, entry.name);
|
|
904
|
-
}
|
|
905
|
-
else if (entry.isDirectory()) {
|
|
906
|
-
const nested = path.join(agentsDir, entry.name, `${entry.name}.agent.md`);
|
|
907
|
-
if (fs.existsSync(nested)) {
|
|
908
|
-
agentMdPath = nested;
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
if (!agentMdPath)
|
|
912
|
-
continue;
|
|
913
|
-
try {
|
|
914
|
-
const content = await fs.promises.readFile(agentMdPath, 'utf-8');
|
|
915
|
-
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
|
|
916
|
-
if (!match) {
|
|
917
|
-
logger.debug(`Agent文件 ${agentMdPath} 没有有效的frontmatter格式`);
|
|
918
|
-
continue;
|
|
919
|
-
}
|
|
920
|
-
let jsYaml;
|
|
921
|
-
try {
|
|
922
|
-
jsYaml = await import('js-yaml');
|
|
923
|
-
if (jsYaml.default)
|
|
924
|
-
jsYaml = jsYaml.default;
|
|
925
|
-
}
|
|
926
|
-
catch (e) {
|
|
927
|
-
logger.warn(`Unable to import js-yaml module: ${e}`);
|
|
928
|
-
continue;
|
|
929
|
-
}
|
|
930
|
-
const metadata = jsYaml.load(match[1]);
|
|
931
|
-
if (!metadata || !metadata.name || !metadata.description) {
|
|
932
|
-
logger.debug(`Agent文件 ${agentMdPath} 缺少必需的 name/description 字段`);
|
|
933
|
-
continue;
|
|
934
|
-
}
|
|
935
|
-
if (seenNames.has(metadata.name)) {
|
|
936
|
-
logger.debug(`Agent '${metadata.name}' 已由先序目录加载,跳过: ${agentMdPath}`);
|
|
937
|
-
continue;
|
|
938
|
-
}
|
|
939
|
-
seenNames.add(metadata.name);
|
|
940
|
-
agents.push({
|
|
941
|
-
name: metadata.name,
|
|
942
|
-
description: metadata.description,
|
|
943
|
-
keywords: metadata.keywords || [],
|
|
944
|
-
tags: metadata.tags || [],
|
|
945
|
-
toolNames: Array.isArray(metadata.tools) ? metadata.tools : [],
|
|
946
|
-
filePath: agentMdPath,
|
|
947
|
-
model: metadata.model,
|
|
948
|
-
provider: metadata.provider,
|
|
949
|
-
maxIterations: typeof metadata.maxIterations === 'number' ? metadata.maxIterations : undefined,
|
|
950
|
-
});
|
|
951
|
-
logger.debug(`Agent发现成功: ${metadata.name}`);
|
|
952
|
-
}
|
|
953
|
-
catch (e) {
|
|
954
|
-
logger.warn(`Failed to parse agent.md in ${agentMdPath}:`, e);
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
if (agents.length > 0) {
|
|
959
|
-
logger.info(`发现 ${agents.length} 个工作区 Agent 预设: ${agents.map(a => a.name).join(', ')}`);
|
|
960
|
-
}
|
|
961
|
-
return agents;
|
|
962
|
-
}
|
|
963
|
-
/**
|
|
964
|
-
* 从根插件树收集:根插件与直接子插件包目录下的 `tools/`
|
|
965
|
-
*/
|
|
966
|
-
export function collectPluginToolSearchRoots(root) {
|
|
967
|
-
if (!root)
|
|
968
|
-
return [];
|
|
969
|
-
const dirs = [];
|
|
970
|
-
const push = (d) => {
|
|
971
|
-
if (d && !dirs.includes(d))
|
|
972
|
-
dirs.push(d);
|
|
973
|
-
};
|
|
974
|
-
const fromPlugin = (p) => {
|
|
975
|
-
if (!p?.filePath)
|
|
976
|
-
return;
|
|
977
|
-
const dir = path.dirname(p.filePath);
|
|
978
|
-
push(path.join(dir, 'tools'));
|
|
979
|
-
// Also check package root when filePath is under src/ or lib/
|
|
980
|
-
const dirName = path.basename(dir);
|
|
981
|
-
if (dirName === 'src' || dirName === 'lib') {
|
|
982
|
-
push(path.join(path.dirname(dir), 'tools'));
|
|
983
|
-
}
|
|
984
|
-
};
|
|
985
|
-
fromPlugin(root);
|
|
986
|
-
for (const child of root.children || []) {
|
|
987
|
-
fromPlugin(child);
|
|
988
|
-
}
|
|
989
|
-
return dirs;
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* 获取所有 tool 搜索目录(标准目录 + 插件包 tools/)
|
|
993
|
-
*/
|
|
994
|
-
export function getToolSearchDirectories(root) {
|
|
995
|
-
const list = [
|
|
996
|
-
path.join(process.cwd(), 'tools'),
|
|
997
|
-
path.join(os.homedir(), '.zhin', 'tools'),
|
|
998
|
-
path.join(getDataDir(), 'tools'),
|
|
999
|
-
];
|
|
1000
|
-
for (const d of collectPluginToolSearchRoots(root ?? undefined)) {
|
|
1001
|
-
if (!list.includes(d))
|
|
1002
|
-
list.push(d);
|
|
1003
|
-
}
|
|
1004
|
-
return list;
|
|
1005
|
-
}
|
|
1006
|
-
/**
|
|
1007
|
-
* 将简写参数定义转换为 ToolParametersSchema
|
|
1008
|
-
*/
|
|
1009
|
-
function shorthandToSchema(params) {
|
|
1010
|
-
const properties = {};
|
|
1011
|
-
const required = [];
|
|
1012
|
-
for (const [key, param] of Object.entries(params)) {
|
|
1013
|
-
properties[key] = {
|
|
1014
|
-
type: param.type || 'string',
|
|
1015
|
-
description: param.description || key,
|
|
1016
|
-
};
|
|
1017
|
-
if (param.enum)
|
|
1018
|
-
properties[key].enum = param.enum;
|
|
1019
|
-
if (param.default !== undefined)
|
|
1020
|
-
properties[key].default = param.default;
|
|
1021
|
-
if (param.required)
|
|
1022
|
-
required.push(key);
|
|
1023
|
-
}
|
|
1024
|
-
return { type: 'object', properties, required: required.length > 0 ? required : undefined };
|
|
1025
|
-
}
|
|
1026
|
-
/**
|
|
1027
|
-
* 加载 handler 文件(动态 import)
|
|
1028
|
-
* @returns execute 函数, 或 undefined(加载失败)
|
|
1029
|
-
*/
|
|
1030
|
-
async function loadToolHandler(handlerPath, toolMdPath) {
|
|
1031
|
-
const resolved = path.resolve(path.dirname(toolMdPath), handlerPath);
|
|
1032
|
-
if (!fs.existsSync(resolved)) {
|
|
1033
|
-
logger.warn(`Tool handler 文件不存在: ${resolved}`);
|
|
1034
|
-
return undefined;
|
|
1035
|
-
}
|
|
1036
|
-
try {
|
|
1037
|
-
const fileUrl = `file://${resolved}?t=${Date.now()}`;
|
|
1038
|
-
const mod = await import(fileUrl);
|
|
1039
|
-
const fn = mod.default || mod;
|
|
1040
|
-
if (typeof fn !== 'function') {
|
|
1041
|
-
logger.warn(`Tool handler 未导出函数: ${resolved}`);
|
|
1042
|
-
return undefined;
|
|
1043
|
-
}
|
|
1044
|
-
return fn;
|
|
1045
|
-
}
|
|
1046
|
-
catch (e) {
|
|
1047
|
-
logger.warn(`Tool handler 加载失败 (${resolved}): ${errMsg(e)}`);
|
|
1048
|
-
return undefined;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
/**
|
|
1052
|
-
* 从 body 构建 prompt 模板执行函数
|
|
1053
|
-
*/
|
|
1054
|
-
function buildTemplateExecute(body) {
|
|
1055
|
-
return (args) => body.replace(/\{\{(\w+)\}\}/g, (_, k) => {
|
|
1056
|
-
const val = args[k];
|
|
1057
|
-
return val !== undefined && val !== null ? String(val) : '';
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
/**
|
|
1061
|
-
* 扫描 tools/ 目录,发现 *.tool.md 文件
|
|
1062
|
-
* 加载顺序与 skills/agents 一致:Workspace > ~/.zhin > data > 插件包
|
|
1063
|
-
* 同名先发现者优先
|
|
1064
|
-
*/
|
|
1065
|
-
export async function discoverWorkspaceTools(root) {
|
|
1066
|
-
const tools = [];
|
|
1067
|
-
const seenNames = new Set();
|
|
1068
|
-
const toolDirs = getToolSearchDirectories(root);
|
|
1069
|
-
for (const toolsDir of toolDirs) {
|
|
1070
|
-
if (!fs.existsSync(toolsDir))
|
|
1071
|
-
continue;
|
|
1072
|
-
let entries;
|
|
1073
|
-
try {
|
|
1074
|
-
entries = await fs.promises.readdir(toolsDir, { withFileTypes: true });
|
|
1075
|
-
}
|
|
1076
|
-
catch {
|
|
1077
|
-
continue;
|
|
1078
|
-
}
|
|
1079
|
-
for (const entry of entries) {
|
|
1080
|
-
// 支持两种结构:
|
|
1081
|
-
// 1. tools/<name>.tool.md(扁平文件)
|
|
1082
|
-
// 2. tools/<name>/<name>.tool.md(目录结构,允许放 handler.ts)
|
|
1083
|
-
let toolMdPath;
|
|
1084
|
-
if (entry.isFile() && entry.name.endsWith('.tool.md')) {
|
|
1085
|
-
toolMdPath = path.join(toolsDir, entry.name);
|
|
1086
|
-
}
|
|
1087
|
-
else if (entry.isDirectory()) {
|
|
1088
|
-
const nested = path.join(toolsDir, entry.name, `${entry.name}.tool.md`);
|
|
1089
|
-
if (fs.existsSync(nested)) {
|
|
1090
|
-
toolMdPath = nested;
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
if (!toolMdPath)
|
|
1094
|
-
continue;
|
|
1095
|
-
try {
|
|
1096
|
-
const content = await fs.promises.readFile(toolMdPath, 'utf-8');
|
|
1097
|
-
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
|
|
1098
|
-
if (!match) {
|
|
1099
|
-
logger.debug(`Tool文件 ${toolMdPath} 没有有效的frontmatter格式`);
|
|
1100
|
-
continue;
|
|
1101
|
-
}
|
|
1102
|
-
let jsYaml;
|
|
1103
|
-
try {
|
|
1104
|
-
jsYaml = await import('js-yaml');
|
|
1105
|
-
if (jsYaml.default)
|
|
1106
|
-
jsYaml = jsYaml.default;
|
|
1107
|
-
}
|
|
1108
|
-
catch (e) {
|
|
1109
|
-
logger.warn(`Unable to import js-yaml module: ${e}`);
|
|
1110
|
-
continue;
|
|
1111
|
-
}
|
|
1112
|
-
const metadata = jsYaml.load(match[1]);
|
|
1113
|
-
if (!metadata || !metadata.name || !metadata.description) {
|
|
1114
|
-
logger.debug(`Tool文件 ${toolMdPath} 缺少必需的 name/description 字段`);
|
|
1115
|
-
continue;
|
|
1116
|
-
}
|
|
1117
|
-
if (seenNames.has(metadata.name)) {
|
|
1118
|
-
logger.debug(`Tool '${metadata.name}' 已由先序目录加载,跳过: ${toolMdPath}`);
|
|
1119
|
-
continue;
|
|
1120
|
-
}
|
|
1121
|
-
seenNames.add(metadata.name);
|
|
1122
|
-
// 提取 body(frontmatter 之后的内容)
|
|
1123
|
-
const body = content.replace(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/, '').trim();
|
|
1124
|
-
tools.push({
|
|
1125
|
-
name: metadata.name,
|
|
1126
|
-
description: metadata.description,
|
|
1127
|
-
parameters: metadata.parameters || undefined,
|
|
1128
|
-
command: metadata.command || undefined,
|
|
1129
|
-
platforms: metadata.platforms,
|
|
1130
|
-
scopes: metadata.scopes,
|
|
1131
|
-
permissionLevel: metadata.permissionLevel,
|
|
1132
|
-
tags: metadata.tags || [],
|
|
1133
|
-
keywords: metadata.keywords || [],
|
|
1134
|
-
kind: metadata.kind,
|
|
1135
|
-
hidden: metadata.hidden,
|
|
1136
|
-
handler: metadata.handler,
|
|
1137
|
-
filePath: toolMdPath,
|
|
1138
|
-
templateBody: !metadata.handler && body ? body : undefined,
|
|
1139
|
-
});
|
|
1140
|
-
logger.debug(`Tool发现成功: ${metadata.name}`);
|
|
1141
|
-
}
|
|
1142
|
-
catch (e) {
|
|
1143
|
-
logger.warn(`Failed to parse tool.md in ${toolMdPath}:`, e);
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
if (tools.length > 0) {
|
|
1148
|
-
logger.info(`发现 ${tools.length} 个工作区 Tool: ${tools.map(t => t.name).join(', ')}`);
|
|
1149
|
-
}
|
|
1150
|
-
return tools;
|
|
1151
|
-
}
|
|
1152
|
-
/**
|
|
1153
|
-
* 将 ToolMeta 转换为 Tool 对象(包含 execute 函数)
|
|
1154
|
-
*/
|
|
1155
|
-
export async function buildToolFromMeta(meta) {
|
|
1156
|
-
// 构建 execute 函数
|
|
1157
|
-
let execute;
|
|
1158
|
-
if (meta.handler) {
|
|
1159
|
-
execute = await loadToolHandler(meta.handler, meta.filePath);
|
|
1160
|
-
if (!execute)
|
|
1161
|
-
return null;
|
|
1162
|
-
}
|
|
1163
|
-
else if (meta.templateBody) {
|
|
1164
|
-
execute = buildTemplateExecute(meta.templateBody);
|
|
1165
|
-
}
|
|
1166
|
-
else {
|
|
1167
|
-
logger.warn(`Tool '${meta.name}' 既没有 handler 也没有模板 body,跳过`);
|
|
1168
|
-
return null;
|
|
1169
|
-
}
|
|
1170
|
-
// 构建参数 schema
|
|
1171
|
-
const parameters = meta.parameters
|
|
1172
|
-
? shorthandToSchema(meta.parameters)
|
|
1173
|
-
: { type: 'object', properties: {} };
|
|
1174
|
-
return {
|
|
1175
|
-
name: meta.name,
|
|
1176
|
-
description: meta.description,
|
|
1177
|
-
parameters,
|
|
1178
|
-
execute,
|
|
1179
|
-
tags: meta.tags,
|
|
1180
|
-
keywords: meta.keywords,
|
|
1181
|
-
platforms: meta.platforms,
|
|
1182
|
-
scopes: meta.scopes,
|
|
1183
|
-
permissionLevel: meta.permissionLevel,
|
|
1184
|
-
hidden: meta.hidden,
|
|
1185
|
-
kind: meta.kind,
|
|
1186
|
-
command: meta.command ? {
|
|
1187
|
-
pattern: meta.command.pattern,
|
|
1188
|
-
alias: meta.command.alias,
|
|
1189
|
-
examples: meta.command.examples,
|
|
1190
|
-
} : undefined,
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
474
|
//# sourceMappingURL=builtin-tools.js.map
|