@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
|
@@ -0,0 +1,302 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发现模块共用的工具函数
|
|
3
|
+
*
|
|
4
|
+
* 被 builtin-tools / discover-skills / discover-agents / discover-tools 共同依赖,
|
|
5
|
+
* 独立出来以避免循环导入。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import type { Plugin } from '@zhin.js/core';
|
|
12
|
+
|
|
13
|
+
/** 将 unknown 错误转为字符串 */
|
|
14
|
+
export function errMsg(e: unknown): string {
|
|
15
|
+
return e instanceof Error ? e.message : String(e);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 获取 data/ 目录路径,自动创建 */
|
|
19
|
+
export function getDataDir(): string {
|
|
20
|
+
const dir = path.join(process.cwd(), 'data');
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 展开路径中的 ~ 为实际 home 目录 */
|
|
26
|
+
export function expandHome(p: string): string {
|
|
27
|
+
if (p === '~') return os.homedir();
|
|
28
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) return path.join(os.homedir(), p.slice(2));
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Workspace / ~/.zhin / data 下 skills 根目录(与 activate_skill 扫描顺序一致的前缀) */
|
|
33
|
+
export function buildStandardSkillDirs(): string[] {
|
|
34
|
+
return [
|
|
35
|
+
path.join(process.cwd(), 'skills'),
|
|
36
|
+
path.join(os.homedir(), '.zhin', 'skills'),
|
|
37
|
+
path.join(getDataDir(), 'skills'),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 从根插件树收集:根插件与**直接子插件**包目录下的 `skills/`(其下为 `<name>/SKILL.md`)
|
|
43
|
+
*/
|
|
44
|
+
export function collectPluginSkillSearchRoots(root: Plugin | null | undefined): string[] {
|
|
45
|
+
if (!root) return [];
|
|
46
|
+
const dirs: string[] = [];
|
|
47
|
+
const push = (d: string) => {
|
|
48
|
+
if (d && !dirs.includes(d)) dirs.push(d);
|
|
49
|
+
};
|
|
50
|
+
const fromPlugin = (p: Plugin) => {
|
|
51
|
+
if (!p?.filePath) return;
|
|
52
|
+
const dir = path.dirname(p.filePath);
|
|
53
|
+
push(path.join(dir, 'skills'));
|
|
54
|
+
// Also check package root when filePath is under src/ or lib/
|
|
55
|
+
const dirName = path.basename(dir);
|
|
56
|
+
if (dirName === 'src' || dirName === 'lib') {
|
|
57
|
+
push(path.join(path.dirname(dir), 'skills'));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
fromPlugin(root);
|
|
61
|
+
for (const child of root.children || []) {
|
|
62
|
+
fromPlugin(child);
|
|
63
|
+
}
|
|
64
|
+
return dirs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 技能发现与 activate_skill 查找共用:标准目录 + 已加载插件包 skills/
|
|
69
|
+
*/
|
|
70
|
+
export function getSkillSearchDirectories(root?: Plugin | null): string[] {
|
|
71
|
+
const list = [...buildStandardSkillDirs()];
|
|
72
|
+
for (const d of collectPluginSkillSearchRoots(root ?? undefined)) {
|
|
73
|
+
if (!list.includes(d)) list.push(d);
|
|
74
|
+
}
|
|
75
|
+
return list;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function mergeSkillDirsWithResolver(resolver?: () => string[]): string[] {
|
|
79
|
+
const list = [...buildStandardSkillDirs()];
|
|
80
|
+
for (const d of resolver?.() ?? []) {
|
|
81
|
+
if (d && !list.includes(d)) list.push(d);
|
|
82
|
+
}
|
|
83
|
+
return list;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 将 Node 文件错误转为 miniclawd 风格的结构化短句,便于模型区分并重试 */
|
|
87
|
+
export function nodeErrToFileMessage(err: unknown, filePath: string, kind: 'read' | 'write' | 'edit' | 'list'): string {
|
|
88
|
+
const e = err as NodeJS.ErrnoException;
|
|
89
|
+
if (e?.code === 'ENOENT') {
|
|
90
|
+
if (kind === 'list') return `Error: Directory not found: ${filePath}`;
|
|
91
|
+
return `Error: File not found: ${filePath}`;
|
|
92
|
+
}
|
|
93
|
+
if (e?.code === 'EACCES') return `Error: Permission denied: ${filePath}`;
|
|
94
|
+
const action = kind === 'read' ? 'reading file' : kind === 'write' ? 'writing file' : kind === 'edit' ? 'editing file' : 'listing directory';
|
|
95
|
+
return `Error ${action}: ${e?.message ?? String(err)}`;
|
|
96
|
+
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import { getPlugin, Scheduler, getScheduler, setScheduler, type MessageType, type SendOptions } from '@zhin.js/core';
|
|
7
7
|
import { ZhinAgent } from '../zhin-agent/index.js';
|
|
8
|
-
import {
|
|
8
|
+
import { createBuiltinTools } from '../builtin-tools.js';
|
|
9
|
+
import { collectPluginSkillSearchRoots } from '../discovery-utils.js';
|
|
9
10
|
import { resolveSkillInstructionMaxChars, DEFAULT_CONFIG } from '../zhin-agent/config.js';
|
|
10
11
|
import { PersistentCronEngine, setCronManager } from '../cron-engine.js';
|
|
11
12
|
import type { AIServiceRefs } from './shared-refs.js';
|
|
@@ -5,17 +5,12 @@
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as os from 'os';
|
|
7
7
|
import * as path from 'path';
|
|
8
|
-
import { getPlugin, type Tool, type SkillFeature, type
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
discoverWorkspaceTools,
|
|
15
|
-
buildToolFromMeta,
|
|
16
|
-
loadAlwaysSkillsContent,
|
|
17
|
-
buildSkillsSummaryXML,
|
|
18
|
-
} from '../builtin-tools.js';
|
|
8
|
+
import { getPlugin, type Tool, type SkillFeature, type AgentPresetFeature } from '@zhin.js/core';
|
|
9
|
+
import { createBuiltinTools } from '../builtin-tools.js';
|
|
10
|
+
import { collectPluginSkillSearchRoots } from '../discovery-utils.js';
|
|
11
|
+
import { discoverWorkspaceSkills, loadAlwaysSkillsContent, buildSkillsSummaryXML } from '../discover-skills.js';
|
|
12
|
+
import { discoverWorkspaceAgents } from '../discover-agents.js';
|
|
13
|
+
import { discoverWorkspaceTools, buildToolFromMeta } from '../discover-tools.js';
|
|
19
14
|
import { resolveSkillInstructionMaxChars, DEFAULT_CONFIG } from '../zhin-agent/config.js';
|
|
20
15
|
import { loadBootstrapFiles, buildContextFiles, buildBootstrapContextSection } from '../bootstrap.js';
|
|
21
16
|
import { triggerAIHook, createAIHookEvent } from '../hooks.js';
|
|
@@ -57,31 +52,39 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
|
|
|
57
52
|
const existing = skillFeature.getByPlugin(root.name);
|
|
58
53
|
for (const s of existing) skillFeature.remove(s);
|
|
59
54
|
const skills = await discoverWorkspaceSkills(root);
|
|
60
|
-
if (skills.length
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
for (const s of skills) {
|
|
69
|
-
const associatedTools: Tool[] = [];
|
|
70
|
-
const toolNames = s.toolNames || [];
|
|
71
|
-
for (const toolName of toolNames) {
|
|
72
|
-
let tool = toolService.get(toolName) || toolNameIndex.get(toolName);
|
|
73
|
-
if (tool) associatedTools.push(tool);
|
|
55
|
+
if (skills.length > 0) {
|
|
56
|
+
const allRegisteredTools = toolService.getAll();
|
|
57
|
+
const toolNameIndex = new Map<string, Tool>();
|
|
58
|
+
for (const t of allRegisteredTools) {
|
|
59
|
+
toolNameIndex.set(t.name, t);
|
|
60
|
+
const parts = t.name.split('_');
|
|
61
|
+
if (parts.length === 2) toolNameIndex.set(`${parts[1]}_${parts[0]}`, t);
|
|
74
62
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
63
|
+
for (const s of skills) {
|
|
64
|
+
const associatedTools: Tool[] = [];
|
|
65
|
+
const toolNames = s.toolNames || [];
|
|
66
|
+
for (const toolName of toolNames) {
|
|
67
|
+
let tool = toolService.get(toolName) || toolNameIndex.get(toolName);
|
|
68
|
+
if (tool) associatedTools.push(tool);
|
|
69
|
+
}
|
|
70
|
+
skillFeature.add({
|
|
71
|
+
name: s.name,
|
|
72
|
+
description: s.description,
|
|
73
|
+
tools: associatedTools,
|
|
74
|
+
keywords: s.keywords || [],
|
|
75
|
+
tags: s.tags || [],
|
|
76
|
+
pluginName: root.name,
|
|
77
|
+
filePath: s.filePath,
|
|
78
|
+
always: s.always,
|
|
79
|
+
}, root.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Inject always-on skills content + XML summary into agent
|
|
83
|
+
if (refs.zhinAgent) {
|
|
84
|
+
const alwaysContent = await loadAlwaysSkillsContent(skills);
|
|
85
|
+
const skillsXml = buildSkillsSummaryXML(skills);
|
|
86
|
+
refs.zhinAgent.setActiveSkillsContext(alwaysContent);
|
|
87
|
+
refs.zhinAgent.setSkillsSummaryXML(skillsXml);
|
|
85
88
|
}
|
|
86
89
|
return skills.length;
|
|
87
90
|
}
|
|
@@ -116,15 +119,20 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
|
|
|
116
119
|
return added;
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
// 已发现的 Agent 预设(本地缓存,无需存储到 Plugin 树)
|
|
120
|
-
const discoveredAgents = new Map<string, AgentPreset>();
|
|
121
|
-
|
|
122
122
|
/**
|
|
123
|
-
* Discover *.agent.md files and register agent presets.
|
|
123
|
+
* Discover *.agent.md files and register agent presets into AgentPresetFeature.
|
|
124
124
|
*/
|
|
125
125
|
async function syncWorkspaceAgents(): Promise<number> {
|
|
126
|
+
const agentPresetFeature = root.inject?.('agentPreset') as AgentPresetFeature | undefined;
|
|
127
|
+
if (!agentPresetFeature) return 0;
|
|
128
|
+
|
|
129
|
+
// Remove previously discovered presets for this plugin
|
|
130
|
+
const existing = agentPresetFeature.getByPlugin(root.name);
|
|
131
|
+
for (const p of existing) agentPresetFeature.remove(p);
|
|
132
|
+
|
|
126
133
|
const agentMetas = await discoverWorkspaceAgents(root);
|
|
127
134
|
if (agentMetas.length === 0) return 0;
|
|
135
|
+
|
|
128
136
|
const allRegisteredTools = toolService.getAll();
|
|
129
137
|
const toolNameIndex = new Map<string, import('@zhin.js/core').Tool>();
|
|
130
138
|
for (const t of allRegisteredTools) {
|
|
@@ -132,7 +140,7 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
|
|
|
132
140
|
}
|
|
133
141
|
let added = 0;
|
|
134
142
|
for (const meta of agentMetas) {
|
|
135
|
-
if (
|
|
143
|
+
if (agentPresetFeature.get(meta.name)) continue;
|
|
136
144
|
const associatedTools: import('@zhin.js/core').Tool[] = [];
|
|
137
145
|
for (const toolName of meta.toolNames || []) {
|
|
138
146
|
const tool = toolService.get(toolName) || toolNameIndex.get(toolName);
|
|
@@ -145,7 +153,7 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
|
|
|
145
153
|
const body = content.replace(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/, '').trim();
|
|
146
154
|
if (body) systemPrompt = body;
|
|
147
155
|
} catch { /* ignore */ }
|
|
148
|
-
|
|
156
|
+
agentPresetFeature.add({
|
|
149
157
|
name: meta.name,
|
|
150
158
|
description: meta.description,
|
|
151
159
|
keywords: meta.keywords,
|
|
@@ -156,7 +164,7 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
|
|
|
156
164
|
provider: meta.provider,
|
|
157
165
|
maxIterations: meta.maxIterations,
|
|
158
166
|
filePath: meta.filePath,
|
|
159
|
-
});
|
|
167
|
+
}, root.name);
|
|
160
168
|
added++;
|
|
161
169
|
}
|
|
162
170
|
return added;
|
|
@@ -224,19 +232,6 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
|
|
|
224
232
|
logger.debug(`Bootstrap files not loaded: ${e.message}`);
|
|
225
233
|
}
|
|
226
234
|
|
|
227
|
-
// Step 3: inject always-on skills content + XML summary
|
|
228
|
-
try {
|
|
229
|
-
const skillsForContext = await discoverWorkspaceSkills(root);
|
|
230
|
-
const alwaysContent = await loadAlwaysSkillsContent(skillsForContext);
|
|
231
|
-
const skillsXml = buildSkillsSummaryXML(skillsForContext);
|
|
232
|
-
if (refs.zhinAgent) {
|
|
233
|
-
refs.zhinAgent.setActiveSkillsContext(alwaysContent);
|
|
234
|
-
refs.zhinAgent.setSkillsSummaryXML(skillsXml);
|
|
235
|
-
}
|
|
236
|
-
} catch (e: unknown) {
|
|
237
|
-
logger.debug(`Skills context not set: ${e instanceof Error ? e.message : String(e)}`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
235
|
// Trigger agent:bootstrap hook
|
|
241
236
|
const skillFeature2 = root.inject('skill') as SkillFeature | undefined;
|
|
242
237
|
await triggerAIHook(createAIHookEvent('agent', 'bootstrap', undefined, {
|
|
@@ -279,13 +274,6 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
|
|
|
279
274
|
skillReloadDebounce = null;
|
|
280
275
|
try {
|
|
281
276
|
const count = await syncWorkspaceSkills();
|
|
282
|
-
const skillsForContext = await discoverWorkspaceSkills(root);
|
|
283
|
-
const alwaysContent = await loadAlwaysSkillsContent(skillsForContext);
|
|
284
|
-
const skillsXml = buildSkillsSummaryXML(skillsForContext);
|
|
285
|
-
if (refs.zhinAgent) {
|
|
286
|
-
refs.zhinAgent.setActiveSkillsContext(alwaysContent);
|
|
287
|
-
refs.zhinAgent.setSkillsSummaryXML(skillsXml);
|
|
288
|
-
}
|
|
289
277
|
await triggerAIHook(createAIHookEvent('agent', 'skills-reloaded', undefined, { skillCount: count }));
|
|
290
278
|
if (count >= 0) logger.info(`[技能热重载] 已更新,工作区技能: ${count}`);
|
|
291
279
|
} catch (e: any) {
|