feat(*): 短剧创作工作台 - 完善章节分镜、资产管理、技能运行与生成任务恢复
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Express } from "express";
|
||||
|
||||
import { CanvasSession } from "./canvas-session.js";
|
||||
import { CONFIG_DIR, type LocalRuntimeConfig } from "./config.js";
|
||||
import { createLocalRuntimeApp } from "./local-runtime.js";
|
||||
@@ -21,7 +23,7 @@ type CanvasAgentHttpOptions = {
|
||||
dependencies: CanvasAgentHttpDependencies;
|
||||
};
|
||||
|
||||
export function startHttpServer(options?: CanvasAgentHttpOptions) {
|
||||
export function startHttpServer(options?: CanvasAgentHttpOptions): ReturnType<typeof startLocalRuntime> | Express {
|
||||
if (!options) return startLocalRuntime();
|
||||
return createHttpApp(options.config, options.session, options.dependencies);
|
||||
}
|
||||
@@ -30,7 +32,7 @@ export function createHttpApp(
|
||||
config: LocalRuntimeConfig,
|
||||
session: CanvasAgentSession = new CanvasSession(),
|
||||
dependencies: CanvasAgentHttpDependencies = {},
|
||||
) {
|
||||
): Express {
|
||||
const endpoint = config.url;
|
||||
const manager = new LocalRuntimeSessionManager({
|
||||
endpoint,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from "node:path";
|
||||
|
||||
import { createServer, type Server } from "node:http";
|
||||
import type { Express } from "express";
|
||||
|
||||
import {
|
||||
CONFIG_DIR,
|
||||
@@ -25,6 +26,14 @@ export type StartLocalRuntimeOptions = {
|
||||
persistConfig?: (config: LocalRuntimeConfig) => void;
|
||||
};
|
||||
|
||||
export type LocalRuntimeHandle = {
|
||||
app: Express;
|
||||
server: Server;
|
||||
sessions: LocalRuntimeSessionManager;
|
||||
ready: Promise<void>;
|
||||
close: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function createDefaultLocalRuntimeModules(config: LocalRuntimeConfig): LocalRuntimeModule[] {
|
||||
return [
|
||||
createCanvasAgentHttpModule(config),
|
||||
@@ -40,7 +49,7 @@ export function createDefaultLocalRuntimeModules(config: LocalRuntimeConfig): Lo
|
||||
];
|
||||
}
|
||||
|
||||
export function startLocalRuntime(options: StartLocalRuntimeOptions = {}) {
|
||||
export function startLocalRuntime(options: StartLocalRuntimeOptions = {}): LocalRuntimeHandle {
|
||||
const config = options.config ?? loadConfig(true);
|
||||
const persistConfig = options.persistConfig ?? saveConfig;
|
||||
const requestedPort = options.port ?? (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Request, RequestHandler, Response } from "express";
|
||||
import type { Express, Request, RequestHandler, Response } from "express";
|
||||
import express from "express";
|
||||
|
||||
import {
|
||||
@@ -55,14 +55,14 @@ export type CreateLocalRuntimeAppOptions = {
|
||||
legacyOrigins?: readonly string[];
|
||||
};
|
||||
|
||||
export function createLocalRuntimeApp(options: CreateLocalRuntimeAppOptions) {
|
||||
export function createLocalRuntimeApp(options: CreateLocalRuntimeAppOptions): Express {
|
||||
const modules = validateModules(options.modules);
|
||||
const policies = corsPolicies(modules, options.legacyOrigins ?? []);
|
||||
const app = express();
|
||||
app.disable("x-powered-by");
|
||||
app.use(express.raw({
|
||||
type: "application/json",
|
||||
limit: "30mb",
|
||||
limit: "64mb",
|
||||
}));
|
||||
app.use(noStore);
|
||||
app.use(exactAuthorityGuard(options.authority));
|
||||
|
||||
@@ -221,16 +221,54 @@ export function parseAgentSkills(value: unknown) {
|
||||
const input = item as Record<string, unknown>;
|
||||
const name = typeof input.name === "string" ? input.name.trim().slice(0, 120) : "";
|
||||
const instruction = typeof input.instruction === "string" ? input.instruction.trim().slice(0, 24_000) : "";
|
||||
if (!name || !instruction) return [];
|
||||
const files = parseAgentSkillFiles(input.files);
|
||||
if (!name || (!instruction && !files.length)) return [];
|
||||
return [{
|
||||
...(typeof input.skillId === "string" ? { skillId: input.skillId.trim().slice(0, 120) } : {}),
|
||||
name,
|
||||
...(typeof input.description === "string" ? { description: input.description.trim().slice(0, 500) } : {}),
|
||||
instruction,
|
||||
...(typeof input.version === "string" ? { version: input.version.trim().slice(0, 120) } : {}),
|
||||
...(files.length ? { files } : { instruction }),
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
function parseAgentSkillFiles(value: unknown) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const files: Array<{ path: string; mimeType?: string; contentBase64: string }> = [];
|
||||
const paths = new Set<string>();
|
||||
let totalBytes = 0;
|
||||
for (const item of value.slice(0, 512)) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) return [];
|
||||
const input = item as Record<string, unknown>;
|
||||
const filePath = typeof input.path === "string" ? normalizeAgentSkillPath(input.path) : "";
|
||||
const contentBase64 = typeof input.contentBase64 === "string" ? input.contentBase64.trim() : "";
|
||||
if (!filePath || paths.has(filePath) || !validBase64(contentBase64)) return [];
|
||||
const size = Buffer.from(contentBase64, "base64").byteLength;
|
||||
if (size > 8 * 1024 * 1024) return [];
|
||||
totalBytes += size;
|
||||
if (totalBytes > 20 * 1024 * 1024) return [];
|
||||
paths.add(filePath);
|
||||
files.push({
|
||||
path: filePath,
|
||||
...(typeof input.mimeType === "string" ? { mimeType: input.mimeType.trim().slice(0, 255) } : {}),
|
||||
contentBase64,
|
||||
});
|
||||
}
|
||||
return files.some((file) => file.path === "SKILL.md") ? files : [];
|
||||
}
|
||||
|
||||
function normalizeAgentSkillPath(value: string) {
|
||||
const normalized = value.trim().replace(/\\/g, "/");
|
||||
const segments = normalized.split("/");
|
||||
if (!normalized || normalized.startsWith("/") || normalized.length > 1000 || segments.some((segment) => !segment || segment === "." || segment === "..") || normalized.includes("\0")) return "";
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function validBase64(value: string) {
|
||||
return value.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(value);
|
||||
}
|
||||
|
||||
function queryValue(req: Request, key: string) {
|
||||
const value = req.query[key];
|
||||
return Array.isArray(value) ? String(value[0] ?? "") : String(value ?? "");
|
||||
|
||||
Reference in New Issue
Block a user