diff --git a/data/vendor/yunwu.ts b/data/vendor/yunwu.ts new file mode 100644 index 0000000..5c372d1 --- /dev/null +++ b/data/vendor/yunwu.ts @@ -0,0 +1,427 @@ +// ==================== 类型定义 ==================== +// 文本模型 +interface TextModel { + name: string; + modelName: string; + type: "text"; + think: boolean; +} + +// 图像模型 +interface ImageModel { + name: string; + modelName: string; + type: "image"; + mode: ("text" | "singleImage" | "multiReference")[]; + associationSkills?: string; +} + +// 视频模型 +interface VideoModel { + name: string; + modelName: string; + type: "video"; + mode: ( + | "singleImage" + | "startEndRequired" + | "endFrameOptional" + | "startFrameOptional" + | "text" + | ("videoReference" | "imageReference" | "audioReference" | "textReference")[] + )[]; + associationSkills?: string; + audio: "optional" | false | true; + durationResolutionMap: { duration: number[]; resolution: string[] }[]; +} + +interface TTSModel { + name: string; + modelName: string; + type: "tts"; + voices: { title: string; voice: string }[]; +} + +interface VendorConfig { + id: string; + author: string; + description?: string; + name: string; + icon?: string; + inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[]; + inputValues: Record; + models: (TextModel | ImageModel | VideoModel)[]; +} + +// ==================== 全局工具函数声明 ==================== +declare const zipImage: (completeBase64: string, size: number) => Promise; +declare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise; +declare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise; +declare const urlToBase64: (url: string) => Promise; +declare const pollTask: ( + fn: () => Promise<{ completed: boolean; data?: string; error?: string }>, + interval?: number, + timeout?: number, +) => Promise<{ completed: boolean; data?: string; error?: string }>; +declare const axios: any; +declare const createOpenAI: any; +declare const createDeepSeek: any; +declare const createZhipu: any; +declare const createQwen: any; +declare const createAnthropic: any; +declare const createOpenAICompatible: any; +declare const createXai: any; +declare const createMinimax: any; +declare const createGoogleGenerativeAI: any; +declare const logger: (logstring: string) => void; +declare const jsonwebtoken: any; + +// ==================== 供应商数据 ==================== +const vendor: VendorConfig = { + id: "yunwu", + author: "Toonflow", + description: "OpenAI标准格式接口,您可以修改请求地址并手动添加缺失的模型。", + name: "云雾中转", + icon: "", + inputs: [ + { key: "apiKey", label: "API密钥", type: "password", required: true }, + { key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "以v1结束,示例:https://yunwu.ai/v1" }, + ], + inputValues: { + apiKey: "", + baseUrl: "https://yunwu.ai/v1", + }, + models: [ + { + name: "Doubao-Seedream-5.0-lite", + type: "image", + modelName: "doubao-seedream-5-0-260128", + mode: ["text", "singleImage", "multiReference"], + }, + { + name: "Gemini-3-Pro-Image-Preview", + type: "image", + modelName: "gemini-3.1-flash-image-preview", + mode: ["text", "singleImage", "multiReference"], + associationSkills: "高质量图像生成,支持文本生成图像、图像编辑", + }, + { + name: "Claude-sonnet-4.6", + type: "text", + modelName: "claude-sonnet-4-6", + think: false, + }, + { + name: "Claude-haiku-4.5-20251001", + type: "text", + modelName: "claude-haiku-4-5-20251001", + think: false, + }, + { + name: "Grok-Video-3", + type: "video", + modelName: "grok-video-3", + mode: ["text", "singleImage"], + audio: false, + durationResolutionMap: [ + { duration: [6, 10], resolution: ["720P", "1080P"] } + ], + associationSkills: "文本生成视频,支持图片垫图" + } + ], +}; +exports.vendor = vendor; + +// ==================== 适配器函数 ==================== + +// 文本请求函数 +const textRequest = (textModel: TextModel) => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + if (!vendor.inputValues.baseUrl) throw new Error("缺少请求地址(baseUrl)"); + + const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, ""); + const baseURL = vendor.inputValues.baseUrl; + + return createOpenAI({ + baseURL: baseURL, + apiKey: apiKey, + }).chat(textModel.modelName); +}; +exports.textRequest = textRequest; + +// 图片请求函数(修正版:使用 /v1/chat/completions 兼容接口) +interface ImageConfig { + prompt: string; + imageBase64: string[]; + size: "1K" | "2K" | "4K"; + aspectRatio: `${number}:${number}`; +} + +const imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => { + const { apiKey, baseUrl } = vendor.inputValues; + if (!apiKey) throw new Error("缺少API Key"); + if (!baseUrl) throw new Error("缺少请求地址(baseUrl)"); + + const cleanApiKey = apiKey.replace(/^Bearer\s+/i, ""); + const baseURL = baseUrl.replace(/\/$/, ""); + const endpoint = baseURL + "/chat/completions"; + + // 构建用户消息内容(支持多图垫图) + const content: any[] = [ + { + type: "text", + text: imageConfig.prompt, + }, + ]; + + // 添加参考图片(垫图) + if (imageConfig.imageBase64 && imageConfig.imageBase64.length > 0) { + for (const imgBase64 of imageConfig.imageBase64) { + let dataUrl = imgBase64; + if (!imgBase64.startsWith("data:image")) { + dataUrl = `data:image/png;base64,${imgBase64}`; + } + content.push({ + type: "image_url", + image_url: { url: dataUrl }, + }); + } + } + + // 注意:云雾中转站可能支持通过额外参数传递图像尺寸/比例, + // 若不确定,可将 size 和 aspectRatio 拼接到 prompt 中(推荐)。 + // 这里采用追加提示词的方式,确保模型理解期望的分辨率和比例。 + let finalPrompt = imageConfig.prompt; + const sizeMap: Record = { "1K": "1024x1024", "2K": "2048x2048", "4K": "4096x4096" }; + const resolution = sizeMap[imageConfig.size] || "1024x1024"; + finalPrompt += `\n请生成一张比例为 ${imageConfig.aspectRatio}、分辨率不低于 ${resolution} 的图片。`; + content[0].text = finalPrompt; + + const requestBody = { + model: imageModel.modelName, + messages: [ + { + role: "user", + content: content, + }, + ], + max_tokens: 4096, // 防止输出截断 + response_format: { type: "json_object" }, // 部分中转站需要 JSON 输出 + }; + + logger(`[图像生成] 请求URL: ${endpoint}`); + logger(`[图像生成] 模型: ${imageModel.modelName}`); + logger(`[图像生成] 参考图片数量: ${imageConfig.imageBase64?.length || 0}`); + + try { + const response = await axios.post(endpoint, requestBody, { + headers: { + "Authorization": `Bearer ${cleanApiKey}`, + "Content-Type": "application/json", + }, + timeout: 120000, + }); + + if (response.status !== 200) { + throw new Error(`HTTP ${response.status}: ${JSON.stringify(response.data)}`); + } + + const assistantMessage = response.data?.choices?.[0]?.message?.content; + if (!assistantMessage) { + throw new Error("响应中没有 assistant 消息内容"); + } + + // 提取图像数据(支持直接返回 base64 data URL 或普通 URL) + let imageBase64: string | null = null; + // 情况1:消息内容本身就是 data:image 开头 + if (assistantMessage.startsWith("data:image")) { + imageBase64 = assistantMessage; + } + // 情况2:包含 Markdown 图片链接 ![alt](url) + else { + const markdownMatch = assistantMessage.match(/!\[.*?\]\((.*?)\)/); + if (markdownMatch && markdownMatch[1]) { + const url = markdownMatch[1]; + if (url.startsWith("data:image")) { + imageBase64 = url; + } else { + imageBase64 = await urlToBase64(url); + } + } + // 情况3:直接是纯文本 URL + else if (assistantMessage.match(/^https?:\/\/[^\s]+\.(png|jpg|jpeg|gif|webp)/i)) { + imageBase64 = await urlToBase64(assistantMessage); + } + } + + if (!imageBase64) { + // 最后尝试:也许整个 content 就是 base64 字符串(无前缀) + if (/^[A-Za-z0-9+/=]+$/.test(assistantMessage) && assistantMessage.length > 100) { + imageBase64 = `data:image/png;base64,${assistantMessage}`; + } else { + throw new Error(`无法从响应中提取图像数据: ${assistantMessage.substring(0, 200)}`); + } + } + + logger(`[图像生成] 成功,图片大小: ${(imageBase64.length / 1024).toFixed(2)} KB`); + return imageBase64; + } catch (error: any) { + logger(`[图像生成] 失败: ${error.message}`); + if (error.response) { + logger(`[图像生成] API 错误详情: ${JSON.stringify(error.response.data)}`); + throw new Error(`图像生成失败: ${error.response.data?.error?.message || error.message}`); + } + throw error; + } +}; +exports.imageRequest = imageRequest; + +// 视频请求函数(保持原有实现,若云雾中转站有专用视频接口可按需修改) +interface VideoConfig { + duration: number; + resolution: string; + aspectRatio: "16:9" | "9:16"; + prompt: string; + imageBase64?: string[]; + audio?: boolean; + mode: + | "singleImage" + | "multiImage" + | "gridImage" + | "startEndRequired" + | "endFrameOptional" + | "startFrameOptional" + | "text" + | ("video" | "image" | "audio" | "text")[]; +} + +const videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => { + const { apiKey, baseUrl: rawBaseUrl } = vendor.inputValues; + if (!apiKey) throw new Error("缺少API Key"); + const baseUrl = rawBaseUrl?.trim(); + if (!baseUrl) throw new Error("缺少请求地址(baseUrl)"); + + const createEndpoint = baseUrl.replace(/\/$/, "") + "/video/create"; + const queryEndpoint = baseUrl.replace(/\/$/, "") + "/video/query"; + + let images: string[] | undefined; + if (videoConfig.imageBase64 && videoConfig.imageBase64.length > 0) { + logger(`[视频生成] 原始图片数组: ${JSON.stringify(videoConfig.imageBase64)}`); + images = videoConfig.imageBase64 + .filter(img => img && typeof img === 'string' && img.length > 0) + .map(img => { + if (img.startsWith("data:image")) return img; + return `data:image/png;base64,${img}`; + }); + if (images.length === 0) { + logger(`[视频生成] 警告: 所有图片都无效,将忽略图片参数`); + images = undefined; + } else { + logger(`[视频生成] 有效图片数量: ${images.length}`); + } + } + + let aspectRatioParam: string; + switch (videoConfig.aspectRatio) { + case "16:9": aspectRatioParam = "3:2"; break; + case "9:16": aspectRatioParam = "2:3"; break; + default: aspectRatioParam = "1:1"; + } + + let sizeParam: string = "720P"; + if (videoConfig.resolution && videoConfig.resolution.includes("1080")) sizeParam = "1080P"; + + const createBody: any = { + model: videoModel.modelName, + prompt: videoConfig.prompt, + aspect_ratio: aspectRatioParam, + size: sizeParam, + }; + if (images && images.length > 0) createBody.images = images; + + try { + logger(`[视频生成] 创建请求体: ${JSON.stringify({ ...createBody, images: images ? `${images.length}张图片` : undefined })}`); + const createResp = await axios.post(createEndpoint, createBody, { + headers: { + "Authorization": `Bearer ${apiKey.replace(/^Bearer\s+/i, "")}`, + "Content-Type": "application/json", + }, + timeout: 30000, + }); + + if (createResp.status !== 200 || !createResp.data?.id) { + throw new Error(`创建任务失败: ${JSON.stringify(createResp.data)}`); + } + + const taskId = createResp.data.id; + logger(`[视频生成] 任务已创建,ID: ${taskId}`); + + const pollResult = await pollTask( + async () => { + try { + const queryResp = await axios.get(queryEndpoint, { + params: { id: taskId }, + headers: { + "Authorization": `Bearer ${apiKey.replace(/^Bearer\s+/i, "")}`, + "Content-Type": "application/json", + }, + timeout: 15000, + }); + + if (queryResp.status !== 200) { + return { completed: false, error: `查询失败: HTTP ${queryResp.status}` }; + } + + const data = queryResp.data; + const status = data.status; + logger(`[视频生成] 任务状态: ${status}`); + + if (status === "succeeded" || status === "completed" || status === "success") { + if (data.video_url) { + return { completed: true, data: data.video_url }; + } else { + return { completed: false, error: "任务成功但未返回视频URL" }; + } + } else if (status === "failed" || status === "error") { + return { completed: false, error: `视频生成失败: ${data.error || "未知错误"}` }; + } else { + return { completed: false }; + } + } catch (err: any) { + logger(`[视频生成] 轮询出错: ${err.message}`); + return { completed: false, error: err.message }; + } + }, + 3000, + 300000 + ); + + if (!pollResult.completed) { + throw new Error(pollResult.error || "视频生成超时或失败"); + } + + const videoUrl = pollResult.data; + logger(`[视频生成] 成功,视频URL: ${videoUrl}`); + return videoUrl; + } catch (error: any) { + logger(`[视频生成] 失败: ${error.message}`); + if (error.response) { + logger(`[视频生成] API 错误详情: ${JSON.stringify(error.response.data)}`); + throw new Error(`视频生成失败: ${error.response.data?.error?.message || error.message}`); + } + throw error; + } +}; +exports.videoRequest = videoRequest; + +// TTS 请求函数(占位) +interface TTSConfig { + text: string; + voice: string; + speechRate: number; + pitchRate: number; + volume: number; +} +const ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => { + return null; +}; +exports.ttsRequest = ttsRequest; \ No newline at end of file diff --git a/src/agents/productionAgent/index.ts b/src/agents/productionAgent/index.ts index 9480688..0edd578 100644 --- a/src/agents/productionAgent/index.ts +++ b/src/agents/productionAgent/index.ts @@ -61,8 +61,10 @@ export async function runDecisionAI(ctx: AgentContext) { videoMode = projectInfo.mode ?? ""; } const isRef = Array.isArray(videoMode) ? true : false; + // console.log("%c Line:64 🍯 isRef", "background:#b03734", isRef); // const findData = models.find((i: any) => i.modelName == videoModelName); // const isRef = findData.mode.every((i: any) => Array.isArray(i)); + console.log("%c Line:67 🍪 isRef", "background:#fca650", isRef); const modelInfo = `项目使用的模型如下:\n图像模型:${imageModelName}\n视频模型:${videoModelName}\n多参:${isRef ? "是" : "否"}`; const mem = buildMemPrompt(await memory.get(text)); @@ -148,8 +150,16 @@ async function createSubAgent(parentCtx: AgentContext) { const [id, videoModelName] = projectInfo.videoModel!.split(/:(.+)/); const models = await u.vendor.getModelList(id); if (!models.length) throw new Error(`项目使用的模型不存在,ID: ${projectInfo.videoModel}`); - const findData = models.find((i: any) => i.modelName == videoModelName); - const isRef = findData.mode.every((i: any) => Array.isArray(i)); + // const findData = models.find((i: any) => i.modelName == videoModelName); + // console.log("%c Line:153 🍿 findData.mode", "background:#93c0a4", findData.mode); + let videoMode = ""; + try { + videoMode = JSON.parse(projectInfo.mode ?? ""); + } catch (e) { + videoMode = projectInfo.mode ?? ""; + } + const isRef = Array.isArray(videoMode) ? true : false; + console.log("%c Line:153 🥤 isRef", "background:#42b983", isRef); const modelInfo = `项目使用的模型如下:\n图像模型:${imageModelName}\n视频模型:${videoModelName}\n多参:${isRef ? "是" : "否"}`; // const run_sub_agent_execution = tool({ @@ -294,7 +304,7 @@ async function createSubAgent(parentCtx: AgentContext) { const systemPrompt = await fs.promises.readFile(skill, "utf-8"); const addPrompt = - "\n你必须使用如下XML格式写入工作区:\n```\n\n```"; + "\n你必须使用如下XML格式写入工作区:\n```\n\n```"; return runAgent({ key: "productionAgent:storyboardPanelAgent", diff --git a/src/router.ts b/src/router.ts index 6bd2cbb..9f999b3 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,4 @@ -// @routes-hash 341ae5b09b9564f4ea847d0edead3c77 +// @routes-hash 8fa7db9525fe2ff96ba91e9bd6c22b65 import { Express } from "express"; import route1 from "./routes/agents/clearMemory"; @@ -27,32 +27,32 @@ import route23 from "./routes/assetsGenerate/batchPolishAssetsPrompt"; import route24 from "./routes/assetsGenerate/cancelGenerate"; import route25 from "./routes/assetsGenerate/generateAssets"; import route26 from "./routes/assetsGenerate/polishAssetsPrompt"; -import route27 from "./routes/cornerScape/batchBindAudio"; -import route28 from "./routes/cornerScape/getAllAssets"; -import route29 from "./routes/cornerScape/updateAssetsAudio"; -import route30 from "./routes/general/generalStatistics"; -import route31 from "./routes/general/getSingleProject"; -import route32 from "./routes/general/updateProject"; -import route33 from "./routes/login/login"; -import route34 from "./routes/modelSelect/getModelDetail"; -import route35 from "./routes/modelSelect/getModelList"; -import route36 from "./routes/novel/addNovel"; -import route37 from "./routes/novel/batchDeleteNovel"; -import route38 from "./routes/novel/delNovel"; -import route39 from "./routes/novel/event/batchDeleteEvent"; -import route40 from "./routes/novel/event/deletEvent"; -import route41 from "./routes/novel/event/generateEvents"; -import route42 from "./routes/novel/event/getEvent"; -import route43 from "./routes/novel/getNovel"; -import route44 from "./routes/novel/getNovelData"; -import route45 from "./routes/novel/getNovelEventState"; -import route46 from "./routes/novel/getNovelIndex"; -import route47 from "./routes/novel/updateNovel"; -import route48 from "./routes/other/deleteAllData"; -import route49 from "./routes/other/getVersion"; -import route50 from "./routes/production/assets/batchGenerateAssetsImage"; -import route51 from "./routes/production/assets/deleteAssetsDireve"; -import route52 from "./routes/production/assets/getAssetsData"; +import route27 from "./routes/common/getBigImage"; +import route28 from "./routes/cornerScape/batchBindAudio"; +import route29 from "./routes/cornerScape/getAllAssets"; +import route30 from "./routes/cornerScape/updateAssetsAudio"; +import route31 from "./routes/general/generalStatistics"; +import route32 from "./routes/general/getSingleProject"; +import route33 from "./routes/general/updateProject"; +import route34 from "./routes/login/login"; +import route35 from "./routes/modelSelect/getModelDetail"; +import route36 from "./routes/modelSelect/getModelList"; +import route37 from "./routes/novel/addNovel"; +import route38 from "./routes/novel/batchDeleteNovel"; +import route39 from "./routes/novel/delNovel"; +import route40 from "./routes/novel/event/batchDeleteEvent"; +import route41 from "./routes/novel/event/deletEvent"; +import route42 from "./routes/novel/event/generateEvents"; +import route43 from "./routes/novel/event/getEvent"; +import route44 from "./routes/novel/getNovel"; +import route45 from "./routes/novel/getNovelData"; +import route46 from "./routes/novel/getNovelEventState"; +import route47 from "./routes/novel/getNovelIndex"; +import route48 from "./routes/novel/updateNovel"; +import route49 from "./routes/other/deleteAllData"; +import route50 from "./routes/other/getVersion"; +import route51 from "./routes/production/assets/batchGenerateAssetsImage"; +import route52 from "./routes/production/assets/deleteAssetsDireve"; import route53 from "./routes/production/assets/pollingImage"; import route54 from "./routes/production/assets/updateAssetsUrl"; import route55 from "./routes/production/editImage/generateFlowImage"; @@ -182,32 +182,32 @@ export default async (app: Express) => { app.use("/api/assetsGenerate/cancelGenerate", route24); app.use("/api/assetsGenerate/generateAssets", route25); app.use("/api/assetsGenerate/polishAssetsPrompt", route26); - app.use("/api/cornerScape/batchBindAudio", route27); - app.use("/api/cornerScape/getAllAssets", route28); - app.use("/api/cornerScape/updateAssetsAudio", route29); - app.use("/api/general/generalStatistics", route30); - app.use("/api/general/getSingleProject", route31); - app.use("/api/general/updateProject", route32); - app.use("/api/login/login", route33); - app.use("/api/modelSelect/getModelDetail", route34); - app.use("/api/modelSelect/getModelList", route35); - app.use("/api/novel/addNovel", route36); - app.use("/api/novel/batchDeleteNovel", route37); - app.use("/api/novel/delNovel", route38); - app.use("/api/novel/event/batchDeleteEvent", route39); - app.use("/api/novel/event/deletEvent", route40); - app.use("/api/novel/event/generateEvents", route41); - app.use("/api/novel/event/getEvent", route42); - app.use("/api/novel/getNovel", route43); - app.use("/api/novel/getNovelData", route44); - app.use("/api/novel/getNovelEventState", route45); - app.use("/api/novel/getNovelIndex", route46); - app.use("/api/novel/updateNovel", route47); - app.use("/api/other/deleteAllData", route48); - app.use("/api/other/getVersion", route49); - app.use("/api/production/assets/batchGenerateAssetsImage", route50); - app.use("/api/production/assets/deleteAssetsDireve", route51); - app.use("/api/production/assets/getAssetsData", route52); + app.use("/api/common/getBigImage", route27); + app.use("/api/cornerScape/batchBindAudio", route28); + app.use("/api/cornerScape/getAllAssets", route29); + app.use("/api/cornerScape/updateAssetsAudio", route30); + app.use("/api/general/generalStatistics", route31); + app.use("/api/general/getSingleProject", route32); + app.use("/api/general/updateProject", route33); + app.use("/api/login/login", route34); + app.use("/api/modelSelect/getModelDetail", route35); + app.use("/api/modelSelect/getModelList", route36); + app.use("/api/novel/addNovel", route37); + app.use("/api/novel/batchDeleteNovel", route38); + app.use("/api/novel/delNovel", route39); + app.use("/api/novel/event/batchDeleteEvent", route40); + app.use("/api/novel/event/deletEvent", route41); + app.use("/api/novel/event/generateEvents", route42); + app.use("/api/novel/event/getEvent", route43); + app.use("/api/novel/getNovel", route44); + app.use("/api/novel/getNovelData", route45); + app.use("/api/novel/getNovelEventState", route46); + app.use("/api/novel/getNovelIndex", route47); + app.use("/api/novel/updateNovel", route48); + app.use("/api/other/deleteAllData", route49); + app.use("/api/other/getVersion", route50); + app.use("/api/production/assets/batchGenerateAssetsImage", route51); + app.use("/api/production/assets/deleteAssetsDireve", route52); app.use("/api/production/assets/pollingImage", route53); app.use("/api/production/assets/updateAssetsUrl", route54); app.use("/api/production/editImage/generateFlowImage", route55); diff --git a/src/routes/artStyle/getArtStyle.ts b/src/routes/artStyle/getArtStyle.ts index 789e03c..4c791fa 100644 --- a/src/routes/artStyle/getArtStyle.ts +++ b/src/routes/artStyle/getArtStyle.ts @@ -7,7 +7,7 @@ export default router.post("/", async (req, res) => { const list = await u.db("o_artStyle").select("*"); const data = await Promise.all( list.map(async (item: any) => { - const fileUrl = await u.oss.getFileUrl(item.fileUrl); + const fileUrl = await u.oss.getSmallImageUrl(item.fileUrl); return { ...item, fileUrl }; }), ); diff --git a/src/routes/assets/getAssetsApi.ts b/src/routes/assets/getAssetsApi.ts index c117998..81aa063 100644 --- a/src/routes/assets/getAssetsApi.ts +++ b/src/routes/assets/getAssetsApi.ts @@ -47,7 +47,7 @@ export default router.post( const childAssetsWithSrc = await Promise.all( childAssets.map(async (child) => ({ ...child, - src: child.filePath && (await u.oss.getFileUrl(child.filePath!)), + src: child.filePath && (await filterTypeGetFileUrl(child.filePath!, child.type)), })), ); @@ -56,7 +56,7 @@ export default router.post( parentAssets.map(async (parent) => ({ ...parent, sonAssets: childAssetsWithSrc.filter((child) => child.assetsId === parent.id), - src: parent.filePath && (await u.oss.getFileUrl(parent.filePath!)), + src: parent.filePath && (await filterTypeGetFileUrl(parent.filePath!, parent.type)), ...(parent.type == "audio" ? { sex: parent.describe?.split("|")[0], describe: parent.describe?.split("|")[1] } : {}), })), ); @@ -77,3 +77,11 @@ export default router.post( res.status(200).send(success({ data: result, total: totalQuery?.total })); }, ); + +async function filterTypeGetFileUrl(url: string, type: string) { + if (type == 'role' || type == 'tool' || type == 'scene') { + return await u.oss.getSmallImageUrl(url) + } else { + return await u.oss.getFileUrl(url) + } +} \ No newline at end of file diff --git a/src/routes/assets/getImage.ts b/src/routes/assets/getImage.ts index 56d1a4c..0a8660c 100644 --- a/src/routes/assets/getImage.ts +++ b/src/routes/assets/getImage.ts @@ -21,7 +21,7 @@ export default router.post( const tempAssets = await Promise.all( rawTempAssets.map(async (item) => ({ ...item, - filePath: item.filePath ? await u.oss.getFileUrl(item.filePath) : "", + filePath: item.filePath ? await u.oss.getSmallImageUrl(item.filePath) : "", selected: assets?.imageId != null && Number(item.id) === Number(assets.imageId), })), ); diff --git a/src/routes/assets/pollingImageAssets.ts b/src/routes/assets/pollingImageAssets.ts index 428cb6a..1a55065 100644 --- a/src/routes/assets/pollingImageAssets.ts +++ b/src/routes/assets/pollingImageAssets.ts @@ -21,7 +21,7 @@ export default router.post( const result = await Promise.all( data.map(async (item: any) => ({ ...item, - filePath: item.filePath ? await u.oss.getFileUrl(item.filePath) : null, + filePath: item.filePath ? await u.oss.getSmallImageUrl(item.filePath) : null, })), ); res.status(200).send(success(result)); diff --git a/src/routes/assetsGenerate/generateAssets.ts b/src/routes/assetsGenerate/generateAssets.ts index 9f180f9..7c48412 100644 --- a/src/routes/assetsGenerate/generateAssets.ts +++ b/src/routes/assetsGenerate/generateAssets.ts @@ -127,7 +127,7 @@ export default router.post("/", validateFields(requestSchema), async (req, res) resolution, }); - const path = await u.oss.getFileUrl(imagePath); + const path = await u.oss.getSmallImageUrl(imagePath); await u.db("o_assets").where("id", id).update({ imageId }); return res.status(200).send(success({ path, assetsId: id })); diff --git a/src/routes/common/getBigImage.ts b/src/routes/common/getBigImage.ts new file mode 100644 index 0000000..3c9217c --- /dev/null +++ b/src/routes/common/getBigImage.ts @@ -0,0 +1,19 @@ +import express from "express"; +import u from "@/utils"; +import { success } from "@/lib/responseFormat"; +import { z } from "zod"; +import { validateFields } from "@/middleware/middleware"; +const router = express.Router(); + +// 获取生成图片 +export default router.post( + "/", + validateFields({ + url: z.string() + }), + async (req, res) => { + const { url } = req.body + const bigImageUrl = await u.oss.getFileUrl(u.replaceUrl(url)) + res.status(200).send(success(bigImageUrl)); + }, +); diff --git a/src/routes/cornerScape/getAllAssets.ts b/src/routes/cornerScape/getAllAssets.ts index a34cd50..8f2b00a 100644 --- a/src/routes/cornerScape/getAllAssets.ts +++ b/src/routes/cornerScape/getAllAssets.ts @@ -51,12 +51,12 @@ export default router.post( const historyImagesWithUrl = await Promise.all( historyImages.map(async (img: any) => ({ id: img.id, - filePath: img.filePath && (await u.oss.getFileUrl(img.filePath)), + filePath: img.filePath && (await u.oss.getSmallImageUrl(img.filePath)), })), ); return { ...parent, - filePath: parent.filePath && (await u.oss.getFileUrl(parent.filePath!)), + filePath: parent.filePath && (await u.oss.getSmallImageUrl(parent.filePath!)), historyImages: historyImagesWithUrl, relepedAudio:repleAssets[parent.id] ?? [] }; diff --git a/src/routes/production/assets/batchGenerateAssetsImage.ts b/src/routes/production/assets/batchGenerateAssetsImage.ts index 1b391e5..b2c3af0 100644 --- a/src/routes/production/assets/batchGenerateAssetsImage.ts +++ b/src/routes/production/assets/batchGenerateAssetsImage.ts @@ -109,7 +109,7 @@ export default router.post( return { id: item.id!, state: "已完成", - src: await u.oss.getFileUrl(savePath), + src: await u.oss.getSmallImageUrl(savePath), }; } catch (e) { await u diff --git a/src/routes/production/assets/getAssetsData.ts b/src/routes/production/assets/getAssetsData.ts deleted file mode 100644 index 234866f..0000000 --- a/src/routes/production/assets/getAssetsData.ts +++ /dev/null @@ -1,59 +0,0 @@ -import express from "express"; -import u from "@/utils"; -import { z } from "zod"; -import { success } from "@/lib/responseFormat"; -import { validateFields } from "@/middleware/middleware"; -import { o_assets } from "@/types/database"; -const router = express.Router(); - -export default router.post( - "/", - validateFields({ - projectId: z.number(), - }), - async (req, res) => { - const { projectId } = req.body; - const parentAssetsData = await u.db("o_assets").where("projectId", projectId).whereNotNull("assetsId"); - const parentIds = parentAssetsData.map((i) => i.id); - const sonAssetsData = await u.db("o_assets").whereIn("assetsId", parentIds); - const sonAssetsMap: Record = {}; - - const imageIds = [...parentAssetsData.map((i) => i.imageId).concat(sonAssetsData.map((i) => i.imageId))].filter(Boolean); - const imagePaths = await u - .db("o_image") - .whereIn("id", imageIds as unknown as string[]) - .select("id", "filePath"); - const imageSignUrls = await Promise.all( - imagePaths.map(async (i) => { - return { id: i.id, src: i.filePath ? await u.oss.getFileUrl(i.filePath) : null }; - }), - ); - const imageUrlMap: Record = {}; - imageSignUrls.forEach((i, index) => { - imageUrlMap[i.id!] = i.src; - }); - sonAssetsData.forEach((i) => { - if (!sonAssetsMap[i.assetsId!]) { - sonAssetsMap[i.assetsId!] = []; - } - const obj = { - assetsId: i.id, - name: i.name, - desc: i.describe, - src: imageUrlMap[i.imageId!] ?? null, - derive: sonAssetsMap[i.id!] ?? [], - }; - sonAssetsMap[i.assetsId!].push(obj); - }); - const returnData = parentAssetsData.map((i) => { - return { - assetsId: i.id, - name: i.name, - desc: i.describe, - src: imageUrlMap[i.imageId!] ?? null, - derive: sonAssetsMap[i.id!] ?? [], - }; - }); - res.status(200).send(success(returnData)); - }, -); diff --git a/src/routes/production/assets/pollingImage.ts b/src/routes/production/assets/pollingImage.ts index c1fb8c9..e1e1f1f 100644 --- a/src/routes/production/assets/pollingImage.ts +++ b/src/routes/production/assets/pollingImage.ts @@ -21,7 +21,7 @@ export default router.post( const result = await Promise.all( data.map(async (item: any) => ({ ...item, - src: item.filePath ? await u.oss.getFileUrl(item.filePath) : null, + src: item.filePath ? await u.oss.getSmallImageUrl(item.filePath) : null, })), ); res.status(200).send(success(result)); diff --git a/src/routes/production/editImage/generateFlowImage.ts b/src/routes/production/editImage/generateFlowImage.ts index e62affc..0b508ad 100644 --- a/src/routes/production/editImage/generateFlowImage.ts +++ b/src/routes/production/editImage/generateFlowImage.ts @@ -8,8 +8,9 @@ const router = express.Router(); async function urlToBase64(imageUrl: string): Promise { if (imageUrl.startsWith("/oss/")) { - return await u.oss.getImageBase64(u.replaceUrl(imageUrl)); + return await u.oss.getImageBase64(u.replaceUrl(imageUrl).replace("/smallImage", "")); } + imageUrl = await u.oss.getFileUrl(u.replaceUrl(imageUrl)) const response = await axios.get(imageUrl, { responseType: "arraybuffer" }); const contentType = response.headers["content-type"] || "image/png"; const base64 = Buffer.from(response.data, "binary").toString("base64"); @@ -51,7 +52,7 @@ export default router.post( const savePath = `${projectId}/workFlow/${u.uuid()}.jpg`; await imageClass.save(savePath); - const url = await u.oss.getFileUrl(savePath); + const url = await u.oss.getSmallImageUrl(savePath); return res.status(200).send(success({ url })); } catch (e) { res.status(400).send(error(u.error(e).message)) diff --git a/src/routes/production/editImage/getImageFlow.ts b/src/routes/production/editImage/getImageFlow.ts index e9c5fee..13d79dd 100644 --- a/src/routes/production/editImage/getImageFlow.ts +++ b/src/routes/production/editImage/getImageFlow.ts @@ -18,14 +18,13 @@ export default router.post( await Promise.all( parseFlow.nodes.map(async (node: any) => { if (node.type === "upload") { - node.data.image = node.data.image ? await u.oss.getFileUrl(node.data.image) : ""; + node.data.image = node.data.image ? await u.oss.getSmallImageUrl(node.data.image) : ""; } else if (node.type === "generated") { - node.data.generatedImage = node.data.generatedImage ? await u.oss.getFileUrl(node.data.generatedImage) : ""; - console.log("%c Line:25 🍋 node.data.references", "background:#42b983", node.data.references); + node.data.generatedImage = node.data.generatedImage ? await u.oss.getSmallImageUrl(node.data.generatedImage) : ""; node.data.references = await Promise.all(node.data.references.map(async (item: { image: string }) => { return { - image: await u.oss.getFileUrl(item.image) + image: await u.oss.getSmallImageUrl(item.image) } })); } diff --git a/src/routes/production/editImage/uploadImage.ts b/src/routes/production/editImage/uploadImage.ts index c0b353c..0d797e0 100644 --- a/src/routes/production/editImage/uploadImage.ts +++ b/src/routes/production/editImage/uploadImage.ts @@ -40,7 +40,7 @@ export default router.post( const savePath = `/${projectId}/imageFlow/${scriptId}/${uuid()}.${ext}`; await u.oss.writeFile(savePath, Buffer.from(base64Data.match(/base64,([A-Za-z0-9+/=]+)/)[1] ?? "", "base64")); - const url = await u.oss.getFileUrl(savePath); + const url = await u.oss.getSmallImageUrl(savePath); res.status(200).send(success(url)); }, ); diff --git a/src/routes/production/getFlowData.ts b/src/routes/production/getFlowData.ts index 0de1f22..2cbe2c7 100644 --- a/src/routes/production/getFlowData.ts +++ b/src/routes/production/getFlowData.ts @@ -53,7 +53,7 @@ export default router.post( type: item.type ?? "", prompt: item.prompt ?? "", desc: item.describe ?? "", - src: item.filePath && (await u.oss.getFileUrl(item.filePath!)), + src: item.filePath && (await u.oss.getSmallImageUrl(item.filePath!)), derive: await Promise.all( childAssetsData .filter((child) => child.assetsId === item.id) @@ -64,7 +64,7 @@ export default router.post( type: child.type, prompt: child.prompt, desc: child.describe ?? "", - src: child.filePath && (await u.oss.getFileUrl(child.filePath!)), + src: child.filePath && (await u.oss.getSmallImageUrl(child.filePath!)), state: child.state ?? "未生成", //todo:矫正状态值 })), ), @@ -91,7 +91,7 @@ export default router.post( storyboardData.map(async (i) => { if (i.filePath) { try { - i.filePath = await u.oss.getFileUrl(i.filePath); + i.filePath = await u.oss.getSmallImageUrl(i.filePath); } catch { i.filePath = ""; } @@ -118,7 +118,7 @@ export default router.post( type: item.type ?? "", prompt: item.prompt ?? "", desc: item.describe ?? "", - src: item.filePath && (await u.oss.getFileUrl(item.filePath!)), + src: item.filePath && (await u.oss.getSmallImageUrl(item.filePath!)), flowId: item.flowId, derive: await Promise.all( childAssetsData @@ -130,7 +130,7 @@ export default router.post( prompt: child.prompt, type: child.type, desc: child.describe ?? "", - src: child.filePath && (await u.oss.getFileUrl(child.filePath!)), + src: child.filePath && (await u.oss.getSmallImageUrl(child.filePath!)), state: child.state ?? "未生成", errorReason: child?.errorReason ?? "", flowId: child.flowId, diff --git a/src/routes/production/getStoryboardData.ts b/src/routes/production/getStoryboardData.ts index 62466a6..e1c2ca8 100644 --- a/src/routes/production/getStoryboardData.ts +++ b/src/routes/production/getStoryboardData.ts @@ -17,7 +17,7 @@ export default router.post( storyboardData.map(async (i) => { return { ...i, - filePath: i.filePath ? await u.oss.getFileUrl(i.filePath!) : "", + filePath: i.filePath ? await u.oss.getSmallImageUrl(i.filePath!) : "", }; }), ); @@ -58,7 +58,7 @@ export default router.post( const charactersWithUrl = await Promise.all( characters.map(async (c) => { if (c.avatar) { - return { ...c, avatar: await u.oss.getFileUrl(c.avatar) }; + return { ...c, avatar: await u.oss.getSmallImageUrl(c.avatar) }; } return c; }), diff --git a/src/routes/production/storyboard/batchAddStoryboardInfo.ts b/src/routes/production/storyboard/batchAddStoryboardInfo.ts index c4565cc..dff273b 100644 --- a/src/routes/production/storyboard/batchAddStoryboardInfo.ts +++ b/src/routes/production/storyboard/batchAddStoryboardInfo.ts @@ -94,7 +94,7 @@ export default router.post( lastStoryboard.map(async (i) => { return { associateAssetsIds: await u.db("o_assets2Storyboard").where("storyboardId", i.id).orderBy("rowid").select("assetId").pluck("assetId"), - src: i.filePath ? await u.oss.getFileUrl(i.filePath) : "", + src: i.filePath ? await u.oss.getSmallImageUrl(i.filePath) : "", id: i.id, trackId: i.trackId, prompt: i.prompt, diff --git a/src/routes/production/storyboard/getStoryboardData.ts b/src/routes/production/storyboard/getStoryboardData.ts index 4125257..a922ff9 100644 --- a/src/routes/production/storyboard/getStoryboardData.ts +++ b/src/routes/production/storyboard/getStoryboardData.ts @@ -33,7 +33,7 @@ export default router.post( id: i.id, prompt: i.prompt, state: i.state, - src: i.filePath ? await u.oss.getFileUrl(i.filePath!) : "", + src: i.filePath ? await u.oss.getSmallImageUrl(i.filePath!) : "", }; }), ); diff --git a/src/routes/production/storyboard/pollingImage.ts b/src/routes/production/storyboard/pollingImage.ts index 0850501..7ecdc00 100644 --- a/src/routes/production/storyboard/pollingImage.ts +++ b/src/routes/production/storyboard/pollingImage.ts @@ -16,7 +16,7 @@ export default router.post( const result = await Promise.all( data.map(async (item: any) => ({ ...item, - src: item.filePath ? await u.oss.getFileUrl(item.filePath) : null, + src: item.filePath ? await u.oss.getSmallImageUrl(item.filePath) : null, })), ); res.status(200).send(success(result)); diff --git a/src/routes/production/workbench/getFileUrl.ts b/src/routes/production/workbench/getFileUrl.ts index a3bca4e..900ff08 100644 --- a/src/routes/production/workbench/getFileUrl.ts +++ b/src/routes/production/workbench/getFileUrl.ts @@ -31,7 +31,7 @@ export default router.post( await Promise.all( totalFilePaths.map(async (item: { id: string, filePath: string, sources: string }) => { - result[`${item.id}:${item.sources}`] = item.filePath ? await u.oss.getFileUrl(item.filePath) : ""; + result[`${item.id}:${item.sources}`] = item.filePath ? await u.oss.getSmallImageUrl(item.filePath) : ""; })) res.status(200).send(success({ data: result })); diff --git a/src/routes/production/workbench/getGenerateData.ts b/src/routes/production/workbench/getGenerateData.ts index fe463b9..3492155 100644 --- a/src/routes/production/workbench/getGenerateData.ts +++ b/src/routes/production/workbench/getGenerateData.ts @@ -53,7 +53,7 @@ export default router.post( const storyboardList = await u.db("o_storyboard").where({ scriptId, projectId }).orderBy("index", "asc"); await Promise.all( storyboardList.map(async (i) => { - i.filePath = i.filePath ? await u.oss.getFileUrl(i.filePath) : ""; + i.filePath = i.filePath ? await u.oss.getSmallImageUrl(i.filePath) : ""; }), ); const storyboardTrackRecord: Record = {}; @@ -100,7 +100,7 @@ export default router.post( type: i.type, fileType: "image" as const, sources: "assets", - src: i.filePath ? await u.oss.getFileUrl(i.filePath) : "", + src: i.filePath ? await u.oss.getSmallImageUrl(i.filePath) : "", }; const sid = i.storyboardId as number; if (!otherDataMap[sid]) otherDataMap[sid] = []; diff --git a/src/routes/production/workbench/getVideoList.ts b/src/routes/production/workbench/getVideoList.ts index 26461a2..2f408b0 100644 --- a/src/routes/production/workbench/getVideoList.ts +++ b/src/routes/production/workbench/getVideoList.ts @@ -23,7 +23,7 @@ export default router.post( await Promise.all( videoList.map(async (s) => ({ ...s, - src: s.filePath ? await u.oss.getFileUrl(s.filePath) : "", + src: s.filePath ? await u.oss.getSmallImageUrl(s.filePath) : "", })), ), ), diff --git a/src/routes/scriptAgent/getPlanData.ts b/src/routes/scriptAgent/getPlanData.ts index 608af9b..a441949 100644 --- a/src/routes/scriptAgent/getPlanData.ts +++ b/src/routes/scriptAgent/getPlanData.ts @@ -16,7 +16,7 @@ export default router.post( const row = await u.db("o_agentWorkData").where({ projectId: projectId, key: agentType }).first(); if (!row) { - await u.db("o_agentWorkData").insert({ + const [id] = await u.db("o_agentWorkData").insert({ projectId: projectId, key: agentType, data: JSON.stringify({ @@ -26,8 +26,11 @@ export default router.post( }); return res.status(200).send( success({ - storySkeleton: "", - adaptationStrategy: "", + data: { + storySkeleton: "", + adaptationStrategy: "", + }, + id }), ); } diff --git a/src/utils/oss.ts b/src/utils/oss.ts index e95ed21..d34ed05 100644 --- a/src/utils/oss.ts +++ b/src/utils/oss.ts @@ -2,6 +2,7 @@ import isPathInside from "is-path-inside"; import getPath, { isEletron } from "@/utils/getPath"; import fs from "node:fs/promises"; import path from "node:path"; +import sharp from "sharp"; // 规范化路径:去除前导斜杠,并将路径分隔符统一转换为系统分隔符 function normalizeUserPath(userPath: string): string { @@ -169,6 +170,43 @@ class OSS { return false; } } + + /** + * 获取图片的缩略图 URL(最长边不超过 512px,等比缩放)。 + * 缩略图保存在原路径同目录下的 smallImage 子文件夹中。 + * 若缩略图已存在则直接返回其 URL;若不存在则同步生成并保存后返回缩略图 URL, + * 生成失败时返回原图 URL。 + * @param userRelPath 用户传入的相对文件路径(使用 / 作为分隔符) + * @returns 缩略图 URL(已存在或生成成功)或原图 URL(生成失败时) + */ + async getSmallImageUrl(userRelPath: string): Promise { + // 构造缩略图相对路径:在原路径的目录层级前插入 smallImage 目录 + // 例如:123/abc.jpg => smallImage/123/abc.jpg + const smallImageRelPath = `smallImage/${userRelPath.replace(/^[/\\]+/, "")}`; + + if (await this.fileExists(smallImageRelPath)) { + return this.getFileUrl(smallImageRelPath); + } + + // 缩略图不存在:同步生成,生成失败则返回原图 URL + const originalUrl = await this.getFileUrl(userRelPath); + + try { + await this.ensureInit(); + const srcAbsPath = resolveSafeLocalPath(userRelPath, this.rootDir); + const dstAbsPath = resolveSafeLocalPath(smallImageRelPath, this.rootDir); + await fs.mkdir(path.dirname(dstAbsPath), { recursive: true }); + await sharp(srcAbsPath) + .resize(512, 512, { fit: "inside", withoutEnlargement: true }) + .toFile(dstAbsPath); + console.info(`[${dstAbsPath}]小图写入成功`); + return this.getFileUrl(smallImageRelPath); + } catch (e) { + // 生成失败返回原图 + console.warn("[OSS] 生成缩略图失败:", e); + return originalUrl; + } + } } export default new OSS(); diff --git a/src/utils/replaceUrl.ts b/src/utils/replaceUrl.ts index bcdfec8..4f95cb2 100644 --- a/src/utils/replaceUrl.ts +++ b/src/utils/replaceUrl.ts @@ -3,7 +3,7 @@ export default function replaceUrl(url: string): string { let cleanedPath = ''; try { const pathname = new URL(url).pathname; - cleanedPath = pathname.replace(/^\/oss/, ''); + cleanedPath = pathname.replace(/^\/oss/, '').replace(/^\/smallImage/, ''); } catch (e) { // 如果不是有效的URL,则直接返回原字符串 cleanedPath = url;