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) {
|
||||
|
||||
@@ -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`;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user