Merge branch 'develop' of https://github.com/HBAI-Ltd/Toonflow-app into develop
This commit is contained in:
commit
c5b18a1285
427
data/vendor/yunwu.ts
vendored
Normal file
427
data/vendor/yunwu.ts
vendored
Normal 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 图片链接 
|
||||
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;
|
||||
@ -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",
|
||||
|
||||
106
src/router.ts
106
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);
|
||||
|
||||
@ -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 };
|
||||
}),
|
||||
);
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
})),
|
||||
);
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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 }));
|
||||
|
||||
19
src/routes/common/getBigImage.ts
Normal file
19
src/routes/common/getBigImage.ts
Normal 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));
|
||||
},
|
||||
);
|
||||
@ -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] ?? []
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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));
|
||||
},
|
||||
);
|
||||
@ -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));
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@ -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));
|
||||
},
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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!) : "",
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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 }));
|
||||
|
||||
@ -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] = [];
|
||||
|
||||
@ -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) : "",
|
||||
})),
|
||||
),
|
||||
),
|
||||
|
||||
@ -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
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user