skill manager 添加脚本管理功能,支持写入和删除可复用脚本

This commit is contained in:
2026-05-15 17:07:52 +08:00
parent 4ec6cbed16
commit 2f83add134
7 changed files with 127 additions and 8 deletions
+73
View File
@@ -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) {
+19 -2
View File
@@ -14,12 +14,15 @@ const FORBIDDEN_PERSISTENCE_PATTERNS = [
/eyJ[a-zA-Z0-9_-]{8,}\.[a-zA-Z0-9._-]{8,}\.[a-zA-Z0-9._-]{8,}/,
];
export const containsForbiddenPersistentContent = (content: string) =>
FORBIDDEN_PERSISTENCE_PATTERNS.some((pattern) => pattern.test(content));
export const sanitizePersistentLine = (content: string, maxLength: number) => {
const normalized = content.replace(/\s+/g, " ").trim();
if (!normalized) {
return "";
}
if (FORBIDDEN_PERSISTENCE_PATTERNS.some((pattern) => pattern.test(normalized))) {
if (containsForbiddenPersistentContent(normalized)) {
return "";
}
if (normalized.length > maxLength) {
@@ -39,7 +42,7 @@ export const sanitizePersistentDocument = (content: string, maxLength: number) =
if (!normalized) {
return "";
}
if (FORBIDDEN_PERSISTENCE_PATTERNS.some((pattern) => pattern.test(normalized))) {
if (containsForbiddenPersistentContent(normalized)) {
return "";
}
if (normalized.length > maxLength) {
@@ -47,3 +50,17 @@ export const sanitizePersistentDocument = (content: string, maxLength: number) =
}
return normalized;
};
export const sanitizePersistentScript = (content: string, maxLength: number) => {
const normalized = content.replace(/\r\n/g, "\n").replace(/\t/g, " ").trim();
if (!normalized) {
return "";
}
if (containsForbiddenPersistentContent(normalized)) {
return "";
}
if (normalized.length > maxLength) {
return "";
}
return `${normalized}\n`;
};