@zhin.js/agent 0.1.0 → 0.1.2

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 (114) hide show
  1. package/lib/cron-engine.d.ts +16 -1
  2. package/lib/cron-engine.d.ts.map +1 -1
  3. package/lib/cron-engine.js +47 -13
  4. package/lib/cron-engine.js.map +1 -1
  5. package/lib/discover-skills.d.ts +3 -1
  6. package/lib/discover-skills.d.ts.map +1 -1
  7. package/lib/discover-skills.js +7 -9
  8. package/lib/discover-skills.js.map +1 -1
  9. package/lib/discover-tools.d.ts +1 -6
  10. package/lib/discover-tools.d.ts.map +1 -1
  11. package/lib/discover-tools.js +2 -6
  12. package/lib/discover-tools.js.map +1 -1
  13. package/lib/index.d.ts +2 -4
  14. package/lib/index.d.ts.map +1 -1
  15. package/lib/index.js +1 -2
  16. package/lib/index.js.map +1 -1
  17. package/lib/init/create-zhin-agent.d.ts.map +1 -1
  18. package/lib/init/create-zhin-agent.js +31 -20
  19. package/lib/init/create-zhin-agent.js.map +1 -1
  20. package/lib/init/register-ai-trigger.d.ts.map +1 -1
  21. package/lib/init/register-ai-trigger.js +10 -3
  22. package/lib/init/register-ai-trigger.js.map +1 -1
  23. package/lib/init/register-builtin-tools.d.ts.map +1 -1
  24. package/lib/init/register-builtin-tools.js +46 -14
  25. package/lib/init/register-builtin-tools.js.map +1 -1
  26. package/lib/init/register-db-models.d.ts.map +1 -1
  27. package/lib/init/register-db-models.js +1 -3
  28. package/lib/init/register-db-models.js.map +1 -1
  29. package/lib/init/register-db-upgrade.d.ts.map +1 -1
  30. package/lib/init/register-db-upgrade.js +1 -8
  31. package/lib/init/register-db-upgrade.js.map +1 -1
  32. package/lib/init/register-management-tools.d.ts.map +1 -1
  33. package/lib/init/register-management-tools.js +33 -20
  34. package/lib/init/register-management-tools.js.map +1 -1
  35. package/lib/service.d.ts.map +1 -1
  36. package/lib/service.js +0 -8
  37. package/lib/service.js.map +1 -1
  38. package/lib/zhin-agent/builtin-tools.d.ts +0 -2
  39. package/lib/zhin-agent/builtin-tools.d.ts.map +1 -1
  40. package/lib/zhin-agent/builtin-tools.js +0 -55
  41. package/lib/zhin-agent/builtin-tools.js.map +1 -1
  42. package/lib/zhin-agent/config.d.ts +2 -1
  43. package/lib/zhin-agent/config.d.ts.map +1 -1
  44. package/lib/zhin-agent/config.js +1 -1
  45. package/lib/zhin-agent/config.js.map +1 -1
  46. package/lib/zhin-agent/index.d.ts +1 -6
  47. package/lib/zhin-agent/index.d.ts.map +1 -1
  48. package/lib/zhin-agent/index.js +26 -34
  49. package/lib/zhin-agent/index.js.map +1 -1
  50. package/lib/zhin-agent/prompt.d.ts.map +1 -1
  51. package/lib/zhin-agent/prompt.js +31 -76
  52. package/lib/zhin-agent/prompt.js.map +1 -1
  53. package/lib/zhin-agent/tool-collector.d.ts.map +1 -1
  54. package/lib/zhin-agent/tool-collector.js +7 -7
  55. package/lib/zhin-agent/tool-collector.js.map +1 -1
  56. package/package.json +7 -4
  57. package/CHANGELOG.md +0 -190
  58. package/lib/follow-up.d.ts +0 -131
  59. package/lib/follow-up.d.ts.map +0 -1
  60. package/lib/follow-up.js +0 -265
  61. package/lib/follow-up.js.map +0 -1
  62. package/src/agent.ts +0 -6
  63. package/src/bootstrap.ts +0 -309
  64. package/src/builtin-tools.ts +0 -958
  65. package/src/compaction.ts +0 -28
  66. package/src/context-manager.ts +0 -15
  67. package/src/conversation-memory.ts +0 -5
  68. package/src/cron-engine.ts +0 -338
  69. package/src/discover-agents.ts +0 -138
  70. package/src/discover-skills.ts +0 -325
  71. package/src/discover-tools.ts +0 -302
  72. package/src/discovery-utils.ts +0 -96
  73. package/src/file-policy.ts +0 -333
  74. package/src/follow-up.ts +0 -357
  75. package/src/hooks.ts +0 -223
  76. package/src/index.ts +0 -183
  77. package/src/init/create-zhin-agent.ts +0 -161
  78. package/src/init/register-ai-service.ts +0 -53
  79. package/src/init/register-ai-trigger.ts +0 -253
  80. package/src/init/register-builtin-tools.ts +0 -308
  81. package/src/init/register-db-models.ts +0 -31
  82. package/src/init/register-db-upgrade.ts +0 -77
  83. package/src/init/register-management-tools.ts +0 -71
  84. package/src/init/register-message-recorder.ts +0 -31
  85. package/src/init/register-tool-service.ts +0 -9
  86. package/src/init/shared-refs.ts +0 -20
  87. package/src/init/types.ts +0 -18
  88. package/src/init.ts +0 -50
  89. package/src/output.ts +0 -15
  90. package/src/rate-limiter.ts +0 -5
  91. package/src/service.ts +0 -228
  92. package/src/session.ts +0 -13
  93. package/src/storage.ts +0 -9
  94. package/src/subagent.ts +0 -209
  95. package/src/tone-detector.ts +0 -5
  96. package/src/tools.ts +0 -214
  97. package/src/user-profile.ts +0 -182
  98. package/src/zhin-agent/builtin-tools.ts +0 -247
  99. package/src/zhin-agent/config.ts +0 -124
  100. package/src/zhin-agent/exec-policy.ts +0 -285
  101. package/src/zhin-agent/index.ts +0 -633
  102. package/src/zhin-agent/prompt.ts +0 -305
  103. package/src/zhin-agent/tool-collector.ts +0 -249
  104. package/tests/ai/follow-up.test.ts +0 -175
  105. package/tests/ai/integration.test.ts +0 -582
  106. package/tests/ai/multimodal.test.ts +0 -106
  107. package/tests/ai/setup.ts +0 -186
  108. package/tests/ai/subagent.test.ts +0 -270
  109. package/tests/ai/tools-builtin.test.ts +0 -310
  110. package/tests/ai/user-profile.test.ts +0 -73
  111. package/tests/ai/zhin-agent.test.ts +0 -306
  112. package/tests/exec-policy.test.ts +0 -355
  113. package/tests/file-policy.test.ts +0 -405
  114. package/tsconfig.json +0 -22
@@ -1,325 +0,0 @@
1
- /**
2
- * 技能发现(SKILL.md 文件扫描与加载)
3
- *
4
- * 加载顺序:Workspace(cwd/skills)> Local(~/.zhin/skills)> data/skills > 已加载插件包 skills/
5
- * 同名先发现者优先,支持平台/依赖兼容性过滤
6
- */
7
-
8
- import * as fs from 'fs';
9
- import * as path from 'path';
10
- import { exec } from 'child_process';
11
- import { promisify } from 'util';
12
- import { Logger, type Plugin } from '@zhin.js/core';
13
- import { getSkillSearchDirectories, getDataDir } from './discovery-utils.js';
14
-
15
- const execAsync = promisify(exec);
16
- const logger = new Logger(null, 'builtin-tools');
17
-
18
- // ============================================================================
19
- // 类型
20
- // ============================================================================
21
-
22
- export interface SkillMeta {
23
- name: string;
24
- description: string;
25
- keywords?: string[];
26
- tags?: string[];
27
- /** SKILL.md frontmatter 中声明的关联工具名列表 */
28
- toolNames?: string[];
29
- filePath: string;
30
- /** 是否常驻注入 system prompt(frontmatter always: true) */
31
- always?: boolean;
32
- /** 当前环境是否满足依赖(bins/env) */
33
- available?: boolean;
34
- /** 缺失的依赖描述(如 "CLI: ffmpeg", "ENV: API_KEY") */
35
- requiresMissing?: string[];
36
- }
37
-
38
- // ============================================================================
39
- // 发现
40
- // ============================================================================
41
-
42
- /**
43
- * 扫描技能目录,发现 SKILL.md 技能文件
44
- * @param root 根插件(可选):用于追加插件包内 `skills/` 扫描,与 `activate_skill` 查找路径一致
45
- */
46
- export async function discoverWorkspaceSkills(root?: Plugin | null): Promise<SkillMeta[]> {
47
- const skills: SkillMeta[] = [];
48
- const seenNames = new Set<string>();
49
- const dataDir = getDataDir();
50
- const skillDirs = getSkillSearchDirectories(root ?? undefined);
51
-
52
- // 确保 data/skills 目录存在
53
- const defaultSkillDir = path.join(dataDir, 'skills');
54
- if (!fs.existsSync(defaultSkillDir)) {
55
- fs.mkdirSync(defaultSkillDir, { recursive: true });
56
- logger.debug(`Created skill directory: ${defaultSkillDir}`);
57
- }
58
-
59
- for (const skillsDir of skillDirs) {
60
- if (!fs.existsSync(skillsDir)) continue;
61
-
62
- let entries: fs.Dirent[];
63
- try {
64
- entries = await fs.promises.readdir(skillsDir, { withFileTypes: true });
65
- } catch {
66
- continue;
67
- }
68
-
69
- for (const entry of entries) {
70
- if (!entry.isDirectory()) continue;
71
- const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md');
72
- if (!fs.existsSync(skillMdPath)) continue;
73
-
74
- try {
75
- const content = await fs.promises.readFile(skillMdPath, 'utf-8');
76
- const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
77
- if (!match) {
78
- logger.debug(`Skill文件 ${skillMdPath} 没有有效的frontmatter格式`);
79
- continue;
80
- }
81
-
82
- let jsYaml: any;
83
- try {
84
- jsYaml = await import('js-yaml');
85
- if (jsYaml.default) jsYaml = jsYaml.default;
86
- } catch (e) {
87
- logger.warn(`Unable to import js-yaml module: ${e}`);
88
- continue;
89
- }
90
-
91
- const metadata = jsYaml.load(match[1]);
92
- if (!metadata || !metadata.name || !metadata.description) {
93
- logger.debug(`Skill文件 ${skillMdPath} 缺少必需的 name/description 字段`);
94
- continue;
95
- }
96
-
97
- // 平台兼容检查
98
- const compat = metadata.compatibility || {};
99
- if (compat.os && Array.isArray(compat.os)) {
100
- const currentOs = process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'windows' : 'linux';
101
- if (!compat.os.includes(currentOs)) {
102
- logger.debug(`Skipping skill '${metadata.name}' (unsupported OS)`);
103
- continue;
104
- }
105
- }
106
-
107
- // 依赖检查
108
- const requiresBins: string[] = metadata.requires?.bins || compat.deps || metadata.deps || [];
109
- const requiresEnv: string[] = metadata.requires?.env || [];
110
- const binsToCheck = Array.isArray(requiresBins) ? requiresBins : [];
111
- const envToCheck = Array.isArray(requiresEnv) ? requiresEnv : [];
112
- const requiresMissing: string[] = [];
113
- for (const bin of binsToCheck) {
114
- try {
115
- await execAsync(`which ${bin} 2>/dev/null`);
116
- } catch {
117
- requiresMissing.push(`CLI: ${bin}`);
118
- }
119
- }
120
- for (const envKey of envToCheck) {
121
- if (!process.env[envKey]) {
122
- requiresMissing.push(`ENV: ${envKey}`);
123
- }
124
- }
125
- const available = requiresMissing.length === 0;
126
-
127
- if (seenNames.has(metadata.name)) {
128
- logger.debug(`Skill '${metadata.name}' 已由先序目录加载,跳过: ${skillMdPath}`);
129
- continue;
130
- }
131
- seenNames.add(metadata.name);
132
-
133
- skills.push({
134
- name: metadata.name,
135
- description: metadata.description,
136
- keywords: metadata.keywords || [],
137
- tags: [...(metadata.tags || []), 'workspace-skill'],
138
- toolNames: Array.isArray(metadata.tools) ? metadata.tools : [],
139
- filePath: skillMdPath,
140
- always: Boolean(metadata.always),
141
- available,
142
- requiresMissing: requiresMissing.length > 0 ? requiresMissing : undefined,
143
- });
144
- logger.debug(`Skill发现成功: ${metadata.name}, tools: ${JSON.stringify(metadata.tools || [])}`);
145
- } catch (e) {
146
- logger.warn(`Failed to parse SKILL.md in ${skillMdPath}:`, e);
147
- }
148
- }
149
- }
150
-
151
- return skills;
152
- }
153
-
154
- // ============================================================================
155
- // 辅助函数
156
- // ============================================================================
157
-
158
- /**
159
- * 获取 frontmatter 中 always: true 的技能名列表(用于常驻注入 system prompt)
160
- */
161
- export function getAlwaysSkillNames(skills: SkillMeta[]): string[] {
162
- return skills.filter(s => s.always && s.available).map(s => s.name);
163
- }
164
-
165
- function stripFrontmatter(content: string): string {
166
- const match = content.match(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/);
167
- if (match) return content.slice(match[0].length).trim();
168
- return content.trim();
169
- }
170
-
171
- /**
172
- * 加载 always 技能的正文内容并拼接为「Active Skills」段
173
- */
174
- export async function loadAlwaysSkillsContent(skills: SkillMeta[]): Promise<string> {
175
- const always = skills.filter(s => s.always && s.available);
176
- if (always.length === 0) return '';
177
- const parts: string[] = [];
178
- for (const s of always) {
179
- try {
180
- const content = await fs.promises.readFile(s.filePath, 'utf-8');
181
- const body = stripFrontmatter(content);
182
- parts.push(`### Skill: ${s.name}\n\n${body}`);
183
- } catch (e) {
184
- logger.warn(`Failed to load always skill ${s.name}: ${(e as Error).message}`);
185
- }
186
- }
187
- return parts.join('\n\n---\n\n');
188
- }
189
-
190
- function escapeXml(s: string): string {
191
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
192
- }
193
-
194
- /**
195
- * 构建技能列表的 XML 摘要,供 model 区分可用/不可用及缺失依赖
196
- */
197
- export function buildSkillsSummaryXML(skills: SkillMeta[]): string {
198
- if (skills.length === 0) return '';
199
- const lines = ['<skills>'];
200
- for (const s of skills) {
201
- const available = s.available !== false;
202
- lines.push(` <skill available="${available}">`);
203
- lines.push(` <name>${escapeXml(s.name)}</name>`);
204
- lines.push(` <description>${escapeXml(s.description)}</description>`);
205
- lines.push(` <location>${escapeXml(s.filePath)}</location>`);
206
- if (!available && s.requiresMissing && s.requiresMissing.length > 0) {
207
- lines.push(` <requires>${escapeXml(s.requiresMissing.join(', '))}</requires>`);
208
- }
209
- lines.push(' </skill>');
210
- }
211
- lines.push('</skills>');
212
- return lines.join('\n');
213
- }
214
-
215
- // ============================================================================
216
- // 技能内容解析(activate_skill 使用)
217
- // ============================================================================
218
-
219
- /**
220
- * 检查技能声明的依赖是否在环境中可用;若有缺失返回提示文案,否则返回空字符串
221
- */
222
- export async function checkSkillDeps(content: string): Promise<string> {
223
- const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
224
- if (!fmMatch) return '';
225
- let jsYaml: any;
226
- try {
227
- jsYaml = await import('js-yaml');
228
- if (jsYaml.default) jsYaml = jsYaml.default;
229
- } catch {
230
- return '';
231
- }
232
- const metadata = jsYaml.load(fmMatch[1]);
233
- if (!metadata) return '';
234
- const compat = metadata.compatibility || {};
235
- const deps = compat.deps || metadata.deps;
236
- if (!deps || !Array.isArray(deps)) return '';
237
- const missing: string[] = [];
238
- for (const dep of deps) {
239
- try {
240
- await execAsync(`which ${dep} 2>/dev/null`);
241
- } catch {
242
- missing.push(dep);
243
- }
244
- }
245
- if (missing.length === 0) return '';
246
- return `⚠️ 当前环境缺少以下依赖,请先安装后再使用本技能:${missing.join(', ')}`;
247
- }
248
-
249
- /**
250
- * 从 SKILL.md 全文中提取精简的执行指令
251
- * 只保留 frontmatter(工具列表)和执行规则,去掉示例、测试场景等冗余内容
252
- */
253
- export function extractSkillInstructions(name: string, content: string, maxBodyLen: number = 4000): string {
254
- const lines: string[] = [];
255
- lines.push(`Skill '${name}' activated. 请立即根据以下指导执行工具调用:`);
256
- lines.push('');
257
-
258
- const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
259
- if (fmMatch) {
260
- const fmContent = fmMatch[1];
261
- const toolsMatch = fmContent.match(/tools:\s*\n((?:\s+-\s+.+\n?)+)/);
262
- if (toolsMatch) {
263
- lines.push('## 可用工具');
264
- lines.push(toolsMatch[0].trim());
265
- lines.push('');
266
- }
267
- }
268
-
269
- const bodyAfterFm = fmMatch && fmMatch.index !== undefined
270
- ? content.slice(fmMatch.index + fmMatch[0].length).replace(/^\s+/, '')
271
- : content;
272
-
273
- // Priority: "## 快速操作" / "## Quick Actions" summary for small models
274
- const quickActionsMatch = bodyAfterFm.match(/## (?:快速操作|Quick\s*Actions)[\s\S]*?(?=\n## [^\s]|$)/i);
275
- if (quickActionsMatch && maxBodyLen <= 2000) {
276
- lines.push(quickActionsMatch[0].trim());
277
- lines.push('');
278
- lines.push('## 立即行动');
279
- lines.push('根据上面的指导,立即调用工具完成用户请求。禁止重复调用 activate_skill,禁止用文本描述代替实际工具调用。');
280
- return lines.join('\n');
281
- }
282
-
283
- const rulesMatch = content.match(/## 执行规则[\s\S]*?(?=\n## [^\s]|$)/);
284
- const workflowMatch = content.match(/## (?:Workflow|Instructions|使用说明)[\s\S]*?(?=\n## [^\s]|$)/);
285
-
286
- if (rulesMatch) {
287
- lines.push(rulesMatch[0].trim());
288
- lines.push('');
289
- } else if (workflowMatch) {
290
- lines.push(workflowMatch[0].trim());
291
- lines.push('');
292
- } else if (bodyAfterFm.trim()) {
293
- const firstH2 = bodyAfterFm.match(/\n## [^\s]/);
294
- const intro = firstH2 ? bodyAfterFm.slice(0, firstH2.index).trim() : bodyAfterFm.trim();
295
-
296
- const quickStartMatch = bodyAfterFm.match(/## (?:快速开始|Quick\s*Start|Getting\s*Started)[\s\S]*?(?=\n## [^\s]|$)/i);
297
- const authMatch = bodyAfterFm.match(/## (?:认证|Authentication|Auth)[\s\S]*?(?=\n## [^\s]|$)/i);
298
-
299
- if (quickStartMatch || (intro.length < 200 && bodyAfterFm.length > intro.length)) {
300
- lines.push('## 指导');
301
- lines.push(intro);
302
- lines.push('');
303
- const extra: string[] = [];
304
- if (quickStartMatch) extra.push(quickStartMatch[0].trim());
305
- if (authMatch) extra.push(authMatch[0].trim());
306
- if (extra.length > 0) {
307
- const joined = extra.join('\n\n');
308
- lines.push(joined.length > maxBodyLen ? joined.slice(0, maxBodyLen) + '\n...(truncated)' : joined);
309
- } else {
310
- const rest = bodyAfterFm.slice(intro.length).trim();
311
- lines.push(rest.length > maxBodyLen ? rest.slice(0, maxBodyLen) + '\n...(truncated)' : rest);
312
- }
313
- lines.push('');
314
- } else if (intro) {
315
- lines.push('## 指导');
316
- lines.push(intro);
317
- lines.push('');
318
- }
319
- }
320
-
321
- lines.push('## 立即行动');
322
- lines.push('根据上面的指导,立即调用工具完成用户请求。禁止重复调用 activate_skill,禁止用文本描述代替实际工具调用。');
323
-
324
- return lines.join('\n');
325
- }
@@ -1,302 +0,0 @@
1
- /**
2
- * 文件化 Tool 发现(*.tool.md 文件扫描与构建)
3
- *
4
- * 加载顺序与 skills/agents 一致:Workspace > ~/.zhin > data > 插件包
5
- * 同名先发现者优先
6
- */
7
-
8
- import * as fs from 'fs';
9
- import * as os from 'os';
10
- import * as path from 'path';
11
- import { spawn } from 'child_process';
12
- import { Logger, type Plugin, type ToolParametersSchema } from '@zhin.js/core';
13
- import { getDataDir } from './discovery-utils.js';
14
-
15
- const logger = new Logger(null, 'builtin-tools');
16
-
17
- // ============================================================================
18
- // 类型
19
- // ============================================================================
20
-
21
- export interface ToolParamShorthand {
22
- type: string;
23
- description?: string;
24
- required?: boolean;
25
- enum?: string[];
26
- default?: any;
27
- }
28
-
29
- export interface ToolMeta {
30
- name: string;
31
- description: string;
32
- /** 简写参数定义(frontmatter 格式) */
33
- parameters?: Record<string, ToolParamShorthand>;
34
- /** 命令配置 */
35
- command?: {
36
- pattern?: string;
37
- alias?: string[];
38
- examples?: string[];
39
- };
40
- platforms?: string[];
41
- scopes?: string[];
42
- permissionLevel?: string;
43
- tags?: string[];
44
- keywords?: string[];
45
- kind?: string;
46
- hidden?: boolean;
47
- /** handler 文件路径(相对于 .tool.md) */
48
- handler?: string;
49
- /** *.tool.md 文件的绝对路径 */
50
- filePath: string;
51
- /** body 内容(无 handler 时作为 prompt 模板) */
52
- templateBody?: string;
53
- }
54
-
55
- // ============================================================================
56
- // 目录收集
57
- // ============================================================================
58
-
59
- /**
60
- * 从根插件树收集:根插件与直接子插件包目录下的 `tools/`
61
- */
62
- export function collectPluginToolSearchRoots(root: Plugin | null | undefined): string[] {
63
- if (!root) return [];
64
- const dirs: string[] = [];
65
- const push = (d: string) => { if (d && !dirs.includes(d)) dirs.push(d); };
66
- const fromPlugin = (p: Plugin) => {
67
- if (!p?.filePath) return;
68
- const dir = path.dirname(p.filePath);
69
- push(path.join(dir, 'tools'));
70
- const dirName = path.basename(dir);
71
- if (dirName === 'src' || dirName === 'lib') {
72
- push(path.join(path.dirname(dir), 'tools'));
73
- }
74
- };
75
- fromPlugin(root);
76
- for (const child of root.children || []) fromPlugin(child);
77
- return dirs;
78
- }
79
-
80
- /**
81
- * 获取所有 tool 搜索目录(标准目录 + 插件包 tools/)
82
- */
83
- export function getToolSearchDirectories(root?: Plugin | null): string[] {
84
- const list = [
85
- path.join(process.cwd(), 'tools'),
86
- path.join(os.homedir(), '.zhin', 'tools'),
87
- path.join(getDataDir(), 'tools'),
88
- ];
89
- for (const d of collectPluginToolSearchRoots(root ?? undefined)) {
90
- if (!list.includes(d)) list.push(d);
91
- }
92
- return list;
93
- }
94
-
95
- // ============================================================================
96
- // 发现
97
- // ============================================================================
98
-
99
- /**
100
- * 扫描 tools/ 目录,发现 *.tool.md 文件
101
- */
102
- export async function discoverWorkspaceTools(root?: Plugin | null): Promise<ToolMeta[]> {
103
- const tools: ToolMeta[] = [];
104
- const seenNames = new Set<string>();
105
- const toolDirs = getToolSearchDirectories(root);
106
-
107
- for (const toolsDir of toolDirs) {
108
- if (!fs.existsSync(toolsDir)) continue;
109
-
110
- let entries: fs.Dirent[];
111
- try {
112
- entries = await fs.promises.readdir(toolsDir, { withFileTypes: true });
113
- } catch {
114
- continue;
115
- }
116
-
117
- for (const entry of entries) {
118
- let toolMdPath: string | undefined;
119
- if (entry.isFile() && entry.name.endsWith('.tool.md')) {
120
- toolMdPath = path.join(toolsDir, entry.name);
121
- } else if (entry.isDirectory()) {
122
- const nested = path.join(toolsDir, entry.name, `${entry.name}.tool.md`);
123
- if (fs.existsSync(nested)) toolMdPath = nested;
124
- }
125
- if (!toolMdPath) continue;
126
-
127
- try {
128
- const content = await fs.promises.readFile(toolMdPath, 'utf-8');
129
- const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/);
130
- if (!match) {
131
- logger.debug(`Tool文件 ${toolMdPath} 没有有效的frontmatter格式`);
132
- continue;
133
- }
134
-
135
- let jsYaml: any;
136
- try {
137
- jsYaml = await import('js-yaml');
138
- if (jsYaml.default) jsYaml = jsYaml.default;
139
- } catch (e) {
140
- logger.warn(`Unable to import js-yaml module: ${e}`);
141
- continue;
142
- }
143
-
144
- const metadata = jsYaml.load(match[1]);
145
- if (!metadata || !metadata.name || !metadata.description) {
146
- logger.debug(`Tool文件 ${toolMdPath} 缺少必需的 name/description 字段`);
147
- continue;
148
- }
149
-
150
- if (seenNames.has(metadata.name)) {
151
- logger.debug(`Tool '${metadata.name}' 已由先序目录加载,跳过: ${toolMdPath}`);
152
- continue;
153
- }
154
- seenNames.add(metadata.name);
155
-
156
- const body = content.replace(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/, '').trim();
157
-
158
- tools.push({
159
- name: metadata.name,
160
- description: metadata.description,
161
- parameters: metadata.parameters || undefined,
162
- command: metadata.command || undefined,
163
- platforms: metadata.platforms,
164
- scopes: metadata.scopes,
165
- permissionLevel: metadata.permissionLevel,
166
- tags: metadata.tags || [],
167
- keywords: metadata.keywords || [],
168
- kind: metadata.kind,
169
- hidden: metadata.hidden,
170
- handler: metadata.handler,
171
- filePath: toolMdPath,
172
- templateBody: !metadata.handler && body ? body : undefined,
173
- });
174
- logger.debug(`Tool发现成功: ${metadata.name}`);
175
- } catch (e) {
176
- logger.warn(`Failed to parse tool.md in ${toolMdPath}:`, e);
177
- }
178
- }
179
- }
180
- return tools;
181
- }
182
-
183
- // ============================================================================
184
- // 构建
185
- // ============================================================================
186
-
187
- function shorthandToSchema(params: Record<string, ToolParamShorthand>): ToolParametersSchema {
188
- const properties: Record<string, any> = {};
189
- const required: string[] = [];
190
- for (const [key, param] of Object.entries(params)) {
191
- properties[key] = {
192
- type: param.type || 'string',
193
- description: param.description || key,
194
- };
195
- if (param.enum) properties[key].enum = param.enum;
196
- if (param.default !== undefined) properties[key].default = param.default;
197
- if (param.required) required.push(key);
198
- }
199
- return { type: 'object', properties, required: required.length > 0 ? required : undefined };
200
- }
201
-
202
- async function loadToolHandler(handlerPath: string, toolMdPath: string): Promise<((args: any, context?: any) => any) | undefined> {
203
- const resolved = path.resolve(path.dirname(toolMdPath), handlerPath);
204
- if (!fs.existsSync(resolved)) {
205
- logger.warn(`Tool handler 文件不存在: ${resolved}`);
206
- return undefined;
207
- }
208
- const ext = path.extname(resolved).toLowerCase();
209
-
210
- // Python script handler: spawn subprocess, pass args via JSON stdin, return stdout
211
- if (ext === '.py') {
212
- return async (args: any) => {
213
- const input = JSON.stringify(args);
214
- const pythonBin = process.env.PYTHON_BIN || 'python3';
215
- try {
216
- const result = await new Promise<string>((resolve, reject) => {
217
- const child = spawn(pythonBin, [resolved], {
218
- stdio: ['pipe', 'pipe', 'pipe'],
219
- timeout: 30_000,
220
- });
221
- let stdout = '';
222
- let stderr = '';
223
- child.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
224
- child.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
225
- child.on('close', (code) => {
226
- if (stderr) logger.debug(`[Python handler] ${resolved} stderr: ${stderr.trim()}`);
227
- if (code !== 0) reject(new Error(`Python exited with code ${code}: ${stderr.trim()}`));
228
- else resolve(stdout.trim());
229
- });
230
- child.on('error', reject);
231
- child.stdin.write(input);
232
- child.stdin.end();
233
- });
234
- return result;
235
- } catch (e: any) {
236
- return `Error running Python handler: ${e.message ?? String(e)}`;
237
- }
238
- };
239
- }
240
-
241
- // JS/TS handler: ESM import
242
- try {
243
- const fileUrl = `file://${resolved}?t=${Date.now()}`;
244
- const mod = await import(fileUrl);
245
- const fn = mod.default || mod;
246
- if (typeof fn !== 'function') {
247
- logger.warn(`Tool handler 未导出函数: ${resolved}`);
248
- return undefined;
249
- }
250
- return fn;
251
- } catch (e) {
252
- logger.warn(`Tool handler 加载失败 (${resolved}): ${e instanceof Error ? e.message : String(e)}`);
253
- return undefined;
254
- }
255
- }
256
-
257
- function buildTemplateExecute(body: string): (args: Record<string, any>) => string {
258
- return (args: Record<string, any>) => body.replace(/\{\{(\w+)\}\}/g, (_, k) => {
259
- const val = args[k];
260
- return val !== undefined && val !== null ? String(val) : '';
261
- });
262
- }
263
-
264
- /**
265
- * 将 ToolMeta 转换为 Tool 对象(包含 execute 函数)
266
- */
267
- export async function buildToolFromMeta(meta: ToolMeta): Promise<import('@zhin.js/core').Tool | null> {
268
- let execute: ((args: any, context?: any) => any) | undefined;
269
-
270
- if (meta.handler) {
271
- execute = await loadToolHandler(meta.handler, meta.filePath);
272
- if (!execute) return null;
273
- } else if (meta.templateBody) {
274
- execute = buildTemplateExecute(meta.templateBody);
275
- } else {
276
- logger.warn(`Tool '${meta.name}' 既没有 handler 也没有模板 body,跳过`);
277
- return null;
278
- }
279
-
280
- const parameters = meta.parameters
281
- ? shorthandToSchema(meta.parameters)
282
- : { type: 'object' as const, properties: {} };
283
-
284
- return {
285
- name: meta.name,
286
- description: meta.description,
287
- parameters,
288
- execute,
289
- tags: meta.tags,
290
- keywords: meta.keywords,
291
- platforms: meta.platforms,
292
- scopes: meta.scopes as any,
293
- permissionLevel: meta.permissionLevel as any,
294
- hidden: meta.hidden,
295
- kind: meta.kind,
296
- command: meta.command ? {
297
- pattern: meta.command.pattern,
298
- alias: meta.command.alias,
299
- examples: meta.command.examples,
300
- } : undefined,
301
- };
302
- }