Merge branch 'develop' of https://github.com/HBAI-Ltd/Toonflow-app into develop

This commit is contained in:
ACT丶流星雨 2026-04-19 11:26:23 +08:00
commit c5b18a1285
27 changed files with 596 additions and 150 deletions

427
data/vendor/yunwu.ts vendored Normal file
View File

@ -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<string, string>;
models: (TextModel | ImageModel | VideoModel)[];
}
// ==================== 全局工具函数声明 ====================
declare const zipImage: (completeBase64: string, size: number) => Promise<string>;
declare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise<string>;
declare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise<string>;
declare const urlToBase64: (url: string) => Promise<string>;
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<string, string> = { "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;

View File

@ -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<storyboardItem videoDesc='视频描述' prompt=提示词内容 track='分组' duration='视频推荐时间' associateAssetsIds='[该分镜所需的资产ID列表]'></storyboardItem>\n```";
"\n你必须使用如下XML格式写入工作区\n```\n<storyboardItem videoDesc='视频描述' prompt=提示词内容 track='分组' shouldGenerateImage='true/false' duration='视频推荐时间' associateAssetsIds='[该分镜所需的资产ID列表]'></storyboardItem>\n```";
return runAgent({
key: "productionAgent:storyboardPanelAgent",

View File

@ -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);

View File

@ -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 };
}),
);

View File

@ -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)
}
}

View File

@ -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),
})),
);

View File

@ -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));

View File

@ -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 }));

View File

@ -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));
},
);

View File

@ -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] ?? []
};

View File

@ -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

View File

@ -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<number, o_assets[]> = {};
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<number, string | null> = {};
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));
},
);

View File

@ -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));

View File

@ -8,8 +8,9 @@ const router = express.Router();
async function urlToBase64(imageUrl: string): Promise<string> {
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))

View File

@ -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)
}
}));
}

View File

@ -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));
},
);

View File

@ -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,

View File

@ -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;
}),

View File

@ -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,

View File

@ -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!) : "",
};
}),
);

View File

@ -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));

View File

@ -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 }));

View File

@ -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<number, any[]> = {};
@ -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] = [];

View File

@ -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) : "",
})),
),
),

View File

@ -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
}),
);
}

View File

@ -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<string> {
// 构造缩略图相对路径:在原路径的目录层级前插入 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();

View File

@ -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;