feat(*): 短剧创作工作台 - 完善章节分镜、资产管理、技能运行与生成任务恢复

This commit is contained in:
ddCat
2026-08-29 23:34:46 +08:00
parent 4f07daae9e
commit 2f6832f99c
113 changed files with 7016 additions and 723 deletions

View File

@@ -13,7 +13,8 @@ import type { AgentAttachment, AgentEmit } from "./types.js";
type Json = Record<string, unknown>;
type AgentEvent = Json & { type: string; usage?: unknown };
type PendingRequest = { resolve: (value: unknown) => void; reject: (error: Error) => void };
export type AgentSkillReference = { skillId?: string; name: string; description?: string; instruction?: string };
export type AgentSkillFile = { path: string; mimeType?: string; contentBase64: string };
export type AgentSkillReference = { skillId?: string; name: string; description?: string; version?: string; files?: AgentSkillFile[]; instruction?: string };
export type CodexSkillInput = { type: "skill"; name: string; path: string };
type CodexRunOptions = { threadId?: string; cwd?: string; skills?: AgentSkillReference[]; onThreadId?: (threadId: string) => void };
type AgentHistoryMessage = { id: string; role: "user" | "assistant" | "tool" | "error"; title?: string; text: string; detail?: unknown; streamId?: string };
@@ -471,17 +472,32 @@ export async function writeSkillFiles(skills: AgentSkillReference[]) {
const usedNames = new Set<string>();
try {
for (const skill of skills.slice(0, 8)) {
const instruction = String(skill.instruction || "").trim().slice(0, 24_000);
if (!instruction) continue;
const baseName = `canvas-${safeSkillSegment(skill.skillId || skill.name)}`;
const name = uniqueSkillName(baseName, usedNames);
usedNames.add(name);
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "infinite-canvas-skill-"));
directories.push(directory);
const temporaryRoot = await fs.mkdtemp(path.join(os.tmpdir(), "infinite-canvas-skill-"));
directories.push(temporaryRoot);
const directory = path.join(temporaryRoot, name);
await fs.mkdir(directory, { recursive: true });
const packageFiles = Array.isArray(skill.files) ? skill.files : [];
if (packageFiles.length) {
for (const item of packageFiles) {
const relativePath = safeSkillFilePath(item.path);
const target = path.join(directory, ...relativePath.split("/"));
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.writeFile(target, Buffer.from(item.contentBase64, "base64"));
}
} else {
const instruction = String(skill.instruction || "").trim().slice(0, 24_000);
if (!instruction) continue;
await fs.writeFile(path.join(directory, "SKILL.md"), skillMarkdown(name, skill.name, skill.description, instruction), "utf8");
}
const file = path.join(directory, "SKILL.md");
const description = String(skill.description || skill.name).trim().slice(0, 500).replace(/[\r\n]+/g, " ");
const body = [`---`, `name: ${name}`, `description: ${JSON.stringify(description)}`, `---`, ``, `# ${skill.name}`, ``, instruction, ``].join("\n");
await fs.writeFile(file, body, "utf8");
let entry = await fs.readFile(file, "utf8");
if (!hasSkillFrontmatter(entry)) {
entry = skillMarkdown(name, skill.name, skill.description, entry);
await fs.writeFile(file, entry, "utf8");
}
inputs.push({ type: "skill", name, path: file });
}
return { directories, inputs };
@@ -491,6 +507,29 @@ export async function writeSkillFiles(skills: AgentSkillReference[]) {
}
}
function skillMarkdown(name: string, title: string, description: string | undefined, instruction: string) {
const summary = String(description || title).trim().slice(0, 500).replace(/[\r\n]+/g, " ");
return [`---`, `name: ${name}`, `description: ${JSON.stringify(summary)}`, `---`, ``, `# ${title}`, ``, instruction, ``].join("\n");
}
function hasSkillFrontmatter(value: string) {
const normalized = value.replace(/^\uFEFF/, "");
if (!normalized.startsWith("---\n") && !normalized.startsWith("---\r\n")) return false;
const end = normalized.indexOf("\n---", 4);
if (end < 0) return false;
const frontmatter = normalized.slice(0, end);
return /^name\s*:/m.test(frontmatter) && /^description\s*:/m.test(frontmatter);
}
function safeSkillFilePath(value: string) {
const normalized = String(value || "").trim().replace(/\\/g, "/");
const segments = normalized.split("/");
if (!normalized || normalized.startsWith("/") || segments.some((segment) => !segment || segment === "." || segment === "..") || normalized.includes("\0")) {
throw new Error(`技能文件路径无效:${normalized || "空路径"}`);
}
return normalized;
}
function uniqueSkillName(baseName: string, usedNames: Set<string>) {
if (!usedNames.has(baseName)) return baseName;
let suffix = 2;