skill manager 添加脚本管理功能,支持写入和删除可复用脚本
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
toStableId,
|
||||
} from "../utils/fileStore.js";
|
||||
import {
|
||||
sanitizePersistentScript,
|
||||
sanitizePersistentDocument,
|
||||
sanitizePersistentLine,
|
||||
} from "../utils/persistencePolicy.js";
|
||||
@@ -36,6 +37,7 @@ export class SkillStore {
|
||||
const current = (await readTextFile(target)) ?? defaultLearnedSkill(normalizedSkillPath);
|
||||
return {
|
||||
references: await this.listReferenceFiles(normalizedSkillPath),
|
||||
scripts: await this.listScriptFiles(normalizedSkillPath),
|
||||
skillPath: normalizedSkillPath,
|
||||
target,
|
||||
patterns: extractLearnedPatterns(current),
|
||||
@@ -146,12 +148,62 @@ export class SkillStore {
|
||||
});
|
||||
}
|
||||
|
||||
async writeScript(skillPath: string, filePath: string, content: string) {
|
||||
const normalizedSkillPath = normalizeSkillPath(skillPath);
|
||||
const normalizedScriptPath = normalizeScriptPath(filePath);
|
||||
if (!normalizedSkillPath) {
|
||||
return { changed: false, detail: "invalid skill_path", target: "" };
|
||||
}
|
||||
if (!normalizedScriptPath) {
|
||||
return { changed: false, detail: "invalid script file_path", target: "" };
|
||||
}
|
||||
const sanitizedContent = sanitizePersistentScript(content, 20000);
|
||||
if (!sanitizedContent) {
|
||||
return { changed: false, detail: "script content rejected by persistence policy", target: "" };
|
||||
}
|
||||
return this.serializeWrite(async () => {
|
||||
const target = join(SKILLS_ROOT_DIR, normalizedSkillPath, normalizedScriptPath);
|
||||
await ensureDirectory(dirname(target));
|
||||
await atomicWriteFileWithHistory(target, sanitizedContent, {
|
||||
historyDir: SKILLS_HISTORY_DIR,
|
||||
rootDir: SKILLS_ROOT_DIR,
|
||||
});
|
||||
return { changed: true, detail: "script written", target };
|
||||
});
|
||||
}
|
||||
|
||||
async removeScript(skillPath: string, filePath: string) {
|
||||
const normalizedSkillPath = normalizeSkillPath(skillPath);
|
||||
const normalizedScriptPath = normalizeScriptPath(filePath);
|
||||
if (!normalizedSkillPath) {
|
||||
return { changed: false, detail: "invalid skill_path", target: "" };
|
||||
}
|
||||
if (!normalizedScriptPath) {
|
||||
return { changed: false, detail: "invalid script file_path", target: "" };
|
||||
}
|
||||
return this.serializeWrite(async () => {
|
||||
const target = join(SKILLS_ROOT_DIR, normalizedSkillPath, normalizedScriptPath);
|
||||
const previous = await readTextFile(target);
|
||||
if (previous === null) {
|
||||
return { changed: false, detail: "script not found", target };
|
||||
}
|
||||
await removeFileIfExists(target);
|
||||
return { changed: true, detail: "script removed", target };
|
||||
});
|
||||
}
|
||||
|
||||
private async listReferenceFiles(skillPath: string) {
|
||||
const referenceDir = join(SKILLS_ROOT_DIR, skillPath, "references");
|
||||
const files = await listFiles(referenceDir);
|
||||
return files.map((file) => file.slice(referenceDir.length + 1));
|
||||
}
|
||||
|
||||
private async listScriptFiles(skillPath: string) {
|
||||
const scriptDir = join(SKILLS_ROOT_DIR, skillPath, "scripts");
|
||||
const files = await listFiles(scriptDir);
|
||||
return files.map((file) => file.slice(scriptDir.length + 1));
|
||||
}
|
||||
|
||||
private skillFilePath(skillPath: string) {
|
||||
return join(SKILLS_ROOT_DIR, skillPath, "SKILL.md");
|
||||
}
|
||||
@@ -201,6 +253,27 @@ const normalizeReferencePath = (rawFilePath: string) => {
|
||||
return [...segments, `${normalizedStem}.md`].join("/");
|
||||
};
|
||||
|
||||
const normalizeScriptPath = (rawFilePath: string) => {
|
||||
const normalized = posix.normalize(rawFilePath.trim().replace(/^\/+|\/+$/g, ""));
|
||||
if (!normalized || normalized.startsWith("..")) {
|
||||
return null;
|
||||
}
|
||||
if (!normalized.startsWith("scripts/")) {
|
||||
return null;
|
||||
}
|
||||
if (!normalized.endsWith(".py")) {
|
||||
return null;
|
||||
}
|
||||
const segments = normalized.split("/");
|
||||
const last = segments.pop();
|
||||
if (!last) {
|
||||
return null;
|
||||
}
|
||||
const stem = last.replace(/\.py$/i, "");
|
||||
const normalizedStem = slugify(stem);
|
||||
return [...segments, `${normalizedStem}.py`].join("/");
|
||||
};
|
||||
|
||||
export const extractLearnedPatterns = (content: string): SkillPatternRecord[] => {
|
||||
const section = extractLearnedPatternsSection(content);
|
||||
if (!section) {
|
||||
|
||||
Reference in New Issue
Block a user