diff --git a/data/skills/art_prompts/chinese_sweet_romance/images/2b1cb9c6-54f9-4e19-bf15-b546c80147bc.jpg b/data/skills/art_prompts/chinese_sweet_romance/images/2b1cb9c6-54f9-4e19-bf15-b546c80147bc.jpg new file mode 100644 index 0000000..1f5ad89 Binary files /dev/null and b/data/skills/art_prompts/chinese_sweet_romance/images/2b1cb9c6-54f9-4e19-bf15-b546c80147bc.jpg differ diff --git a/data/skills/art_prompts/chinese_sweet_romance/images/b5bd58ee-c304-477f-9c18-b40f814a5901.jpg b/data/skills/art_prompts/chinese_sweet_romance/images/b5bd58ee-c304-477f-9c18-b40f814a5901.jpg new file mode 100644 index 0000000..9ec69a9 Binary files /dev/null and b/data/skills/art_prompts/chinese_sweet_romance/images/b5bd58ee-c304-477f-9c18-b40f814a5901.jpg differ diff --git a/data/skills/art_prompts/chinese_sweet_romance/images/e4b20248-54f1-4e63-ab2d-cb72cd1886b8.jpg b/data/skills/art_prompts/chinese_sweet_romance/images/e4b20248-54f1-4e63-ab2d-cb72cd1886b8.jpg new file mode 100644 index 0000000..1f5ad89 Binary files /dev/null and b/data/skills/art_prompts/chinese_sweet_romance/images/e4b20248-54f1-4e63-ab2d-cb72cd1886b8.jpg differ diff --git a/data/skills/art_prompts/chinese_sweet_romance/images/f0f89ce4-b197-4bd6-9794-3d14d1c3ab1e.jpg b/data/skills/art_prompts/chinese_sweet_romance/images/f0f89ce4-b197-4bd6-9794-3d14d1c3ab1e.jpg new file mode 100644 index 0000000..9ec69a9 Binary files /dev/null and b/data/skills/art_prompts/chinese_sweet_romance/images/f0f89ce4-b197-4bd6-9794-3d14d1c3ab1e.jpg differ diff --git a/src/app.ts b/src/app.ts index 280cb98..cffa3c5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,6 +12,7 @@ import fs from "fs"; import u from "@/utils"; import jwt from "jsonwebtoken"; import socketInit from "@/socket/index"; +import path from "path"; const app = express(); const server = http.createServer(app); @@ -29,14 +30,28 @@ export default async function startServe(randomPort: Boolean = false) { app.use(express.json({ limit: "100mb" })); app.use(express.urlencoded({ extended: true, limit: "100mb" })); - // oss 静态资源 - const rootDir = u.getPath("oss"); - if (!fs.existsSync(rootDir)) { - fs.mkdirSync(rootDir, { recursive: true }); + const ossDir = u.getPath("oss"); + if (!fs.existsSync(ossDir)) { + fs.mkdirSync(ossDir, { recursive: true }); } - console.log("文件目录:", rootDir); - app.use(express.static(rootDir)); + console.log("文件目录:", ossDir); + app.use("/oss", express.static(ossDir)); + // skills 静态资源 + + const skillsDir = u.getPath("skills"); + if (!fs.existsSync(skillsDir)) { + fs.mkdirSync(skillsDir, { recursive: true }); + } + console.log("文件目录:", skillsDir); + // 只允许图片文件访问 + app.use( + "/skills", + (req, res, next) => { + /\.(jpe?g|png|gif|webp|svg|ico|bmp)$/i.test(req.path) ? next() : res.status(403).end(); + }, + express.static(skillsDir), + ); // data/web 静态网站 const webDir = u.getPath("web"); diff --git a/src/env.ts b/src/env.ts index e733138..d1af1b4 100644 --- a/src/env.ts +++ b/src/env.ts @@ -3,8 +3,8 @@ import path from "path"; // 默认环境变量(当 env 文件不存在时自动创建) const defaultEnvValues: Record = { - dev: `NODE_ENV=dev\nPORT=10588\nOSSURL=http://127.0.0.1:10588/`, - prod: `NODE_ENV=prod\nPORT=10588\nOSSURL=http://127.0.0.1:10588/`, + dev: `NODE_ENV=dev\nPORT=10588\nOSSURL=http://127.0.0.1:10588/oss/`, + prod: `NODE_ENV=prod\nPORT=10588\nOSSURL=http://127.0.0.1:10588/oss/`, }; // 判断是否为打包后的 Electron 环境 diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index aaf01c9..983add9 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -539,6 +539,7 @@ description: 专注于从剧本内容中提取所使用的资产(角色、场 table.text("reason"); table.text("prompt"); table.integer("selectVideoId"); + table.integer("duration"); table.primary(["id"]); table.unique(["id"]); }, diff --git a/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts b/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts index 72c9c07..79a375a 100644 --- a/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts +++ b/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts @@ -83,21 +83,47 @@ export default router.post( }); }); const result: ResultItem[] = Object.values(itemMap); - - const typeConfig: Record = { - role: { promptKey: "role-polish", itemType: "characters", label: "角色标准四视图", nameLabel: "角色", visualManual: "art_character" }, - scene: { promptKey: "scene-polish", itemType: "scenes", label: "场景图", nameLabel: "场景", visualManual: "art_scene" }, - tool: { promptKey: "tool-polish", itemType: "props", label: "道具图", nameLabel: "道具", visualManual: "art_prop" }, - }; // 批量更新所有 item 状态为生成中 const assetsIds = items.map((item: { assetsId: number }) => item.assetsId); await u.db("o_assets").whereIn("id", assetsIds).update({ promptState: "生成中" }); + //查询所有资产,用于判断每个资产是否是衍生资产 + const assetsDataList = await u.db("o_assets").whereIn("id", assetsIds).select("id", "assetsId"); + if (!assetsDataList || assetsDataList.length === 0) return res.status(500).send(error("资产不存在")); + const assetsDataMap = new Map(assetsDataList.map((a: any) => [a.id, a])); + + const getTypeConfig = ( + isDerivative: boolean, + ): Record => ({ + role: { + promptKey: "role-polish", + itemType: "characters", + label: "角色标准四视图", + nameLabel: "角色", + visualManual: isDerivative ? "art_character_derivative" : "art_character", + }, + scene: { + promptKey: "scene-polish", + itemType: "scenes", + label: "场景图", + nameLabel: "场景", + visualManual: isDerivative ? "art_scene_derivative" : "art_scene", + }, + tool: { + promptKey: "tool-polish", + itemType: "props", + label: "道具图", + nameLabel: "道具", + visualManual: isDerivative ? "art_prop_derivative" : "art_prop", + }, + }); // 后台异步并发生成,不阻塞响应 const limit = pLimit(concurrentCount ?? 1); - const tasks = items.map((item: { assetsId: number; type: string; name: string; describe: string }) => limit(async () => { + const assetData = assetsDataMap.get(item.assetsId); + if (!assetData) return; + const typeConfig = getTypeConfig(!!assetData.assetsId); const config = typeConfig[item.type]; if (!config) return; //获取到视觉手册 diff --git a/src/routes/assetsGenerate/polishAssetsPrompt.ts b/src/routes/assetsGenerate/polishAssetsPrompt.ts index 438af1e..ee96a7d 100644 --- a/src/routes/assetsGenerate/polishAssetsPrompt.ts +++ b/src/routes/assetsGenerate/polishAssetsPrompt.ts @@ -76,11 +76,31 @@ export default router.post( }); const result: ResultItem[] = Object.values(itemMap); - + //查询资产是否是衍生资产 + const assetsData = await u.db("o_assets").where("id", assetsId).select("assetsId").first(); + if (!assetsData) return { code: 500, message: "资产不存在" }; const typeConfig: Record = { - role: { promptKey: "role-polish", itemType: "characters", label: "角色标准四视图", nameLabel: "角色", visualManual: "art_character" }, - scene: { promptKey: "scene-polish", itemType: "scenes", label: "场景图", nameLabel: "场景", visualManual: "art_scene" }, - tool: { promptKey: "tool-polish", itemType: "props", label: "道具图", nameLabel: "道具", visualManual: "art_prop" }, + role: { + promptKey: "role-polish", + itemType: "characters", + label: "角色标准四视图", + nameLabel: "角色", + visualManual: assetsData.assetsId ? "art_character_derivative" : "art_character", + }, + scene: { + promptKey: "scene-polish", + itemType: "scenes", + label: "场景图", + nameLabel: "场景", + visualManual: assetsData.assetsId ? "art_scene_derivative" : "art_scene", + }, + tool: { + promptKey: "tool-polish", + itemType: "props", + label: "道具图", + nameLabel: "道具", + visualManual: assetsData.assetsId ? "art_prop_derivative" : "art_prop", + }, }; const config = typeConfig[type]; diff --git a/src/routes/production/workbench/getGenerateData.ts b/src/routes/production/workbench/getGenerateData.ts index b69f2d2..0a18465 100644 --- a/src/routes/production/workbench/getGenerateData.ts +++ b/src/routes/production/workbench/getGenerateData.ts @@ -23,6 +23,7 @@ interface TrackItem { prompt: string; state: "未生成" | "生成中" | "已完成" | "生成失败"; reason?: string; + duration?: number; selectVideoId?: number; medias: TrackMedia[]; videoList: VideoItem[]; @@ -53,6 +54,7 @@ export default router.post( const item = trackData.find((t) => t.id === trackId); trackList.push({ id: trackId, + duration: item?.duration ?? 0, prompt: item?.prompt || "", state: (item?.state as "未生成" | "生成中" | "已完成" | "生成失败") ?? "未生成", reason: item?.reason ?? "", diff --git a/src/routes/project/addVisualManual.ts b/src/routes/project/addVisualManual.ts index 434ed67..e8550fd 100644 --- a/src/routes/project/addVisualManual.ts +++ b/src/routes/project/addVisualManual.ts @@ -76,11 +76,11 @@ export default router.post( } fs.writeFileSync(filePath, item.data, "utf-8"); } - const ossImagesDir = u.getPath(["oss", stylePath]); + const imagesDir = path.join(mainPath, "images"); let existingFiles: string[] = []; try { - const allFiles = fs.readdirSync(ossImagesDir); + const allFiles = fs.readdirSync(imagesDir); existingFiles = allFiles.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f)); } catch {} @@ -88,12 +88,22 @@ export default router.post( for (const file of existingFiles) { if (!retainedFileNames.has(file)) { - await u.oss.deleteFile(`${stylePath}/${file}`); + const filePath = path.join(imagesDir, file); + if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } } + if (!fs.existsSync(imagesDir)) { + fs.mkdirSync(imagesDir, { recursive: true }); + } + for (const item of images) { - if (!item.startsWith("http")) await u.oss.writeFile(`${stylePath}/${u.uuid()}.jpg`, item); + if (!item.startsWith("http")) { + const fileName = `${u.uuid()}.jpg`; + const targetPath = path.join(imagesDir, fileName); + const buffer = Buffer.from(item.replace(/^data:[^;]+;base64,/, ""), "base64"); + fs.writeFileSync(targetPath, buffer); + } } res.status(200).send(success()); diff --git a/src/routes/project/deleteVisualManual.ts b/src/routes/project/deleteVisualManual.ts index 5d35333..b0a1244 100644 --- a/src/routes/project/deleteVisualManual.ts +++ b/src/routes/project/deleteVisualManual.ts @@ -33,14 +33,6 @@ export default router.post( } catch (e) { console.error("[删除视觉手册] 删除失败:", artPromptsDir, e); } - - // 2. 删除 oss 下的同名文件夹(存放图片),独立于 art_prompts 目录 - try { - await u.oss.deleteDirectory(name); - } catch (e) { - console.warn("[删除视觉手册] oss 目录删除失败:", name, e); - } - res.status(200).send(success({ message: "删除成功" })); } catch (err) { res.status(500).send(error(u.error(err).message || "删除失败")); diff --git a/src/routes/project/editVisualManual.ts b/src/routes/project/editVisualManual.ts index 77c7ea3..69997b0 100644 --- a/src/routes/project/editVisualManual.ts +++ b/src/routes/project/editVisualManual.ts @@ -77,11 +77,11 @@ export default router.post( const content = item.value === "README" ? `${name}\n${item.data}` : item.data; fs.writeFileSync(filePath, content, "utf-8"); } - const ossImagesDir = u.getPath(["oss", stylePath]); + const imagesDir = path.join(mainPath, "images"); let existingFiles: string[] = []; try { - const allFiles = fs.readdirSync(ossImagesDir); + const allFiles = fs.readdirSync(imagesDir); existingFiles = allFiles.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f)); } catch {} @@ -89,12 +89,22 @@ export default router.post( for (const file of existingFiles) { if (!retainedFileNames.has(file)) { - await u.oss.deleteFile(`${stylePath}/${file}`); + const filePath = path.join(imagesDir, file); + if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } } + if (!fs.existsSync(imagesDir)) { + fs.mkdirSync(imagesDir, { recursive: true }); + } + for (const item of images) { - if (!item.startsWith("http")) await u.oss.writeFile(`${stylePath}/${u.uuid()}.jpg`, item); + if (!item.startsWith("http")) { + const fileName = `${u.uuid()}.jpg`; + const targetPath = path.join(imagesDir, fileName); + const buffer = Buffer.from(item.replace(/^data:[^;]+;base64,/, ""), "base64"); + fs.writeFileSync(targetPath, buffer); + } } res.status(200).send(success()); diff --git a/src/routes/project/getVisualManual.ts b/src/routes/project/getVisualManual.ts index e227418..b997b96 100644 --- a/src/routes/project/getVisualManual.ts +++ b/src/routes/project/getVisualManual.ts @@ -33,11 +33,11 @@ function readMd(filePath: string): string { // 获取 images 文件夹下所有图片文件路径列表 async function readAllImages(imagesDir: string) { try { - const ossPath = u.getPath(["oss", imagesDir]); + const ossPath = u.getPath(path.join("skills", "art_prompts", imagesDir, "images")); const files = fs.readdirSync(ossPath); - const images = files.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f)).map((f) => path.join(imagesDir, f)); + const images = files.filter((f) => /\.(png|jpe?g|gif|webp|svg)$/i.test(f)).map((f) => path.join("art_prompts", imagesDir, "images", f)); if (images.length) { - return Promise.all(images.map(async (i) => await u.oss.getFileUrl(i))); + return Promise.all(images.map(async (i) => await u.oss.getFileUrl(i, "skills"))); } else { return []; } @@ -60,7 +60,6 @@ export default router.post("/", async (req, res) => { const result = await Promise.all( styleDirs.map(async (styleName) => { const styleDir = path.join(artPromptsDir, styleName); - const images = await readAllImages(styleName); const readmePath = path.join(styleDir, "README.md"); const readmeContent = fs.readFileSync(readmePath, "utf-8"); diff --git a/src/types/database.d.ts b/src/types/database.d.ts index 04139ce..2911895 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -1,6 +1,34 @@ -// @db-hash c0d74bd27b3a41b397705c93d1737a3b +// @db-hash a6017ee44d67db4a339664cfe7bacb76 //该文件由脚本自动生成,请勿手动修改 +export interface _o_vendorConfig_old_20260401 { + 'author'?: string | null; + 'code'?: string | null; + 'createTime'?: number | null; + 'description'?: string | null; + 'icon'?: string | null; + 'id'?: string; + 'inputs'?: string | null; + 'inputValues'?: string | null; + 'models'?: string | null; + 'name'?: string | null; +} +export interface _o_videoTrack_old_20260401 { + 'id'?: number; + 'projectId'?: number | null; + 'scriptId'?: number | null; + 'videoId'?: number | null; +} +export interface _o_videoTrack_old_20260401_1 { + 'id'?: number; + 'projectId'?: number | null; + 'prompt'?: string | null; + 'reason'?: string | null; + 'scriptId'?: number | null; + 'selectVideoId'?: number | null; + 'state'?: string | null; + 'videoId'?: number | null; +} export interface memories { 'content': string; 'createTime': number; @@ -216,6 +244,7 @@ export interface o_video { 'videoTrackId'?: number | null; } export interface o_videoTrack { + 'duration'?: number | null; 'id'?: number; 'projectId'?: number | null; 'prompt'?: string | null; @@ -227,6 +256,9 @@ export interface o_videoTrack { } export interface DB { + "_o_vendorConfig_old_20260401": _o_vendorConfig_old_20260401; + "_o_videoTrack_old_20260401": _o_videoTrack_old_20260401; + "_o_videoTrack_old_20260401_1": _o_videoTrack_old_20260401_1; "memories": memories; "o_agentDeploy": o_agentDeploy; "o_agentWorkData": o_agentWorkData; diff --git a/src/utils/oss.ts b/src/utils/oss.ts index e3e9f0c..8ae4927 100644 --- a/src/utils/oss.ts +++ b/src/utils/oss.ts @@ -45,12 +45,13 @@ class OSS { * @param userRelPath 用户传入的相对文件路径(使用 / 作为分隔符) * @returns 文件的 http 链接(本地服务地址) */ - async getFileUrl(userRelPath: string): Promise { + async getFileUrl(userRelPath: string, prefix?: string): Promise { + if (!prefix) prefix = "oss"; await this.ensureInit(); const safePath = normalizeUserPath(userRelPath); // URL 始终使用 /,所以这里需要将系统分隔符转回 / - let url = process.env.OSSURL || `http://127.0.0.1:10588/`; - if (isEletron()) url = `http://localhost:${process.env.PORT}/`; + let url = `${process.env.OSSURL}${prefix}/` || `http://127.0.0.1:10588/${prefix}/`; + if (isEletron()) url = `http://localhost:${process.env.PORT}/${prefix}/`; return `${url}${safePath.split(path.sep).join("/")}`; } @@ -146,10 +147,7 @@ class OSS { await fs.mkdir(path.dirname(absPath), { recursive: true }); // 如果 data 是 string,则视为 base64 编码,先解码再写入 // 自动去除可能存在的 Data URL 前缀(如 "data:image/png;base64,") - const buffer = - typeof data === "string" - ? Buffer.from(data.replace(/^data:[^;]+;base64,/, ""), "base64") - : data; + const buffer = typeof data === "string" ? Buffer.from(data.replace(/^data:[^;]+;base64,/, ""), "base64") : data; await fs.writeFile(absPath, buffer); }