重构agent结构前提交
This commit is contained in:
parent
68f771b7bc
commit
81eb0395a0
@ -23,10 +23,16 @@ function parseFrontmatter(content: string): { name: string; description: string
|
|||||||
|
|
||||||
for (let i = 0; i < lines.length; ) {
|
for (let i = 0; i < lines.length; ) {
|
||||||
const colonIndex = lines[i].indexOf(":");
|
const colonIndex = lines[i].indexOf(":");
|
||||||
if (colonIndex === -1) { i++; continue; }
|
if (colonIndex === -1) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const key = lines[i].slice(0, colonIndex).trim();
|
const key = lines[i].slice(0, colonIndex).trim();
|
||||||
if (!key) { i++; continue; }
|
if (!key) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let value = lines[i].slice(colonIndex + 1).trim();
|
let value = lines[i].slice(colonIndex + 1).trim();
|
||||||
i++;
|
i++;
|
||||||
@ -44,8 +50,7 @@ function parseFrontmatter(content: string): { name: string; description: string
|
|||||||
result[key] = value;
|
result[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.name) throw new Error("Frontmatter missing required field: name");
|
if (!result.name || !result.description) throw new Error("Frontmatter missing required field: name or description");
|
||||||
if (!result.description) throw new Error("Frontmatter missing required field: description");
|
|
||||||
return { name: result.name, description: result.description };
|
return { name: result.name, description: result.description };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,13 +62,17 @@ function stripFrontmatter(content: string): string {
|
|||||||
|
|
||||||
async function listResources(dir: string, base = ""): Promise<string[]> {
|
async function listResources(dir: string, base = ""): Promise<string[]> {
|
||||||
let entries;
|
let entries;
|
||||||
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { return []; }
|
try {
|
||||||
|
entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const files: string[] = [];
|
const files: string[] = [];
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const rel = base ? `${base}/${entry.name}` : entry.name;
|
const rel = base ? `${base}/${entry.name}` : entry.name;
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
files.push(...await listResources(path.join(dir, entry.name), rel));
|
files.push(...(await listResources(path.join(dir, entry.name), rel)));
|
||||||
} else if (entry.name !== "SKILL.md") {
|
} else if (entry.name !== "SKILL.md") {
|
||||||
files.push(rel);
|
files.push(rel);
|
||||||
}
|
}
|
||||||
@ -71,22 +80,39 @@ async function listResources(dir: string, base = ""): Promise<string[]> {
|
|||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 读取单个技能 ====================
|
||||||
|
|
||||||
|
async function readSkillFromDir(skillDir: string): Promise<SkillRecord | null> {
|
||||||
|
const location = path.join(skillDir, "SKILL.md");
|
||||||
|
let content: string;
|
||||||
|
try {
|
||||||
|
content = await fs.readFile(location, "utf-8");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const meta = parseFrontmatter(content);
|
||||||
|
console.log(`[Skill] ✅ 发现技能:${meta.name} — ${meta.description}`);
|
||||||
|
return { ...meta, location, baseDir: skillDir };
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[Skill] ⚠️ 解析失败 "${skillDir}":${(e as Error).message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 构建技能目录 ====================
|
// ==================== 构建技能目录 ====================
|
||||||
|
|
||||||
function buildCatalog(skill: SkillRecord): string {
|
function buildCatalog(skills: SkillRecord[]): string {
|
||||||
return [
|
const entries = skills.map((s) => ` <skill>\n <name>${s.name}</name>\n <description>${s.description}</description>\n </skill>`).join("\n");
|
||||||
"## Skills",
|
|
||||||
"以下技能提供了专业任务的专用指令。",
|
return `## Skills
|
||||||
"当任务与某个技能的描述匹配时,调用 activate_skill 工具并传入技能名称来加载完整指令。",
|
以下技能提供了专业任务的专用指令。
|
||||||
"加载后遵循技能指令执行任务,需要时调用 read_skill_file 读取资源文件内容。",
|
当任务与某个技能的描述匹配时,调用 activate_skill 工具并传入技能名称来加载完整指令。
|
||||||
"",
|
加载后遵循技能指令执行任务,需要时调用 read_skill_file 读取资源文件内容。
|
||||||
"<available_skills>",
|
|
||||||
` <skill>`,
|
<available_skills>
|
||||||
` <name>${skill.name}</name>`,
|
${entries}
|
||||||
` <description>${skill.description}</description>`,
|
</available_skills>`;
|
||||||
` </skill>`,
|
|
||||||
"</available_skills>",
|
|
||||||
].join("\n");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 激活 + 执行工具 ====================
|
// ==================== 激活 + 执行工具 ====================
|
||||||
@ -103,19 +129,13 @@ function createSkillTools(skills: SkillRecord[]) {
|
|||||||
}),
|
}),
|
||||||
execute: async ({ name }) => {
|
execute: async ({ name }) => {
|
||||||
const skill = skills.find((s) => s.name === name);
|
const skill = skills.find((s) => s.name === name);
|
||||||
if (!skill) {
|
if (!skill) return { error: `Skill '${name}' not found` };
|
||||||
console.log(`[Skill] ❌ 激活失败:未找到技能 "${name}"`);
|
if (activated.has(name)) return { already_active: true, message: `技能 "${name}" 已激活,无需重复加载` };
|
||||||
return { error: `Skill '${name}' not found` };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activated.has(name)) {
|
|
||||||
console.log(`[Skill] ℹ️ 技能 "${name}" 已在当前会话中激活,跳过重复注入`);
|
|
||||||
return { already_active: true, message: `技能 "${name}" 已激活,无需重复加载` };
|
|
||||||
}
|
|
||||||
|
|
||||||
let content: string;
|
let content: string;
|
||||||
try { content = await fs.readFile(skill.location, "utf-8"); } catch {
|
try {
|
||||||
console.log(`[Skill] ❌ 激活失败:无法读取 ${skill.location}`);
|
content = await fs.readFile(skill.location, "utf-8");
|
||||||
|
} catch {
|
||||||
return { error: `Failed to read SKILL.md for '${name}'` };
|
return { error: `Failed to read SKILL.md for '${name}'` };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,22 +143,17 @@ function createSkillTools(skills: SkillRecord[]) {
|
|||||||
const resources = await listResources(skill.baseDir);
|
const resources = await listResources(skill.baseDir);
|
||||||
activated.add(name);
|
activated.add(name);
|
||||||
|
|
||||||
console.log(`[Skill] 📖 已激活技能:${skill.name}(${body.length} 字符,${resources.length} 个资源文件)`);
|
const resourcesXml =
|
||||||
|
resources.length > 0 ? `\n<skill_resources>\n${resources.map((f) => ` <file>${f}</file>`).join("\n")}\n</skill_resources>` : "";
|
||||||
const resourcesXml = resources.length > 0
|
|
||||||
? `\n<skill_resources>\n${resources.map((f) => ` <file>${f}</file>`).join("\n")}\n</skill_resources>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: `<skill_content name="${skill.name}">
|
||||||
`<skill_content name="${skill.name}">`,
|
${body}
|
||||||
body,
|
|
||||||
"",
|
Skill directory: ${skill.baseDir}
|
||||||
`Skill directory: ${skill.baseDir}`,
|
相对路径基于此技能目录解析,使用 read_skill_file 工具读取资源文件。
|
||||||
`相对路径基于此技能目录解析,使用 read_skill_file 工具读取资源文件。`,
|
${resourcesXml}
|
||||||
resourcesXml,
|
</skill_content>`,
|
||||||
`</skill_content>`,
|
|
||||||
].join("\n"),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -151,23 +166,14 @@ function createSkillTools(skills: SkillRecord[]) {
|
|||||||
}),
|
}),
|
||||||
execute: async ({ skillName, filePath: relPath }) => {
|
execute: async ({ skillName, filePath: relPath }) => {
|
||||||
const skill = skills.find((s) => s.name === skillName);
|
const skill = skills.find((s) => s.name === skillName);
|
||||||
if (!skill) {
|
if (!skill) return { error: `Skill '${skillName}' not found` };
|
||||||
console.log(`[Skill] ❌ 读取失败:未找到技能 "${skillName}"`);
|
|
||||||
return { error: `Skill '${skillName}' not found` };
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullPath = path.resolve(path.join(skill.baseDir, relPath));
|
const fullPath = path.resolve(path.join(skill.baseDir, relPath));
|
||||||
if (!isPathInside(fullPath, skill.baseDir)) {
|
if (!isPathInside(fullPath, skill.baseDir)) return { error: "Access denied: path is outside skill directory" };
|
||||||
console.log(`[Skill] 🚫 路径越界已拦截:"${relPath}" 超出技能目录范围`);
|
|
||||||
return { error: "Access denied: path is outside skill directory" };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fileContent = await fs.readFile(fullPath, "utf-8");
|
return { content: await fs.readFile(fullPath, "utf-8") };
|
||||||
console.log(`[Skill] 📄 已读取文件:${skillName}/${relPath}(${fileContent.length} 字符)`);
|
|
||||||
return { content: fileContent };
|
|
||||||
} catch {
|
} catch {
|
||||||
console.log(`[Skill] ❌ 读取失败:未找到文件 "${relPath}"`);
|
|
||||||
return { error: `File not found: ${relPath}` };
|
return { error: `File not found: ${relPath}` };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -180,26 +186,25 @@ function createSkillTools(skills: SkillRecord[]) {
|
|||||||
export async function useSkill(...segments: string[]) {
|
export async function useSkill(...segments: string[]) {
|
||||||
if (segments.length === 0) return { prompt: "", tools: {} };
|
if (segments.length === 0) return { prompt: "", tools: {} };
|
||||||
|
|
||||||
const baseDir = path.join(getPath("skills"), ...segments);
|
const skills = new Map<string, SkillRecord>();
|
||||||
const location = path.join(baseDir, "SKILL.md");
|
|
||||||
|
|
||||||
let content: string;
|
const primary = await readSkillFromDir(path.join(getPath("skills"), ...segments));
|
||||||
try { content = await fs.readFile(location, "utf-8"); } catch {
|
if (primary) skills.set(primary.name, primary);
|
||||||
console.log(`[Skill] ⚠️ 未发现技能:${segments.join("/")}`);
|
|
||||||
return { prompt: "", tools: {} };
|
const publicDir = path.join(getPath("skills"), "public");
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(publicDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory()) continue;
|
||||||
|
const skill = await readSkillFromDir(path.join(publicDir, entry.name));
|
||||||
|
if (skill && !skills.has(skill.name)) skills.set(skill.name, skill);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* public dir not found */
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata: { name: string; description: string };
|
if (skills.size === 0) return { prompt: "", tools: {} };
|
||||||
try { metadata = parseFrontmatter(content); } catch (e) {
|
|
||||||
console.log(`[Skill] ⚠️ 解析失败 "${segments.join("/")}":${(e as Error).message}`);
|
|
||||||
return { prompt: "", tools: {} };
|
|
||||||
}
|
|
||||||
|
|
||||||
const skill: SkillRecord = { ...metadata, location, baseDir };
|
const allSkills = [...skills.values()];
|
||||||
console.log(`[Skill] ✅ 发现技能:${skill.name} — ${skill.description}`);
|
return { prompt: buildCatalog(allSkills), tools: createSkillTools(allSkills) };
|
||||||
|
|
||||||
return {
|
|
||||||
prompt: buildCatalog(skill),
|
|
||||||
tools: createSkillTools([skill]),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user