This commit is contained in:
小帅 2026-04-03 00:33:35 +08:00
commit 898aa5054c
19 changed files with 1322 additions and 1436 deletions

BIN
data/latest.zip Normal file

Binary file not shown.

27
data/update.json Normal file
View File

@ -0,0 +1,27 @@
{
"version": "1.0.8",
"time": 1775118545494,
"data": {
"toonflow": [
{
"type": "zip",
"url": "https://toonflow.oss-cn-beijing.aliyuncs.com/latest/latest.zip"
},
{
"type": "windows",
"url": "https://toonflow.oss-cn-beijing.aliyuncs.com/latest/latest.exe"
},
{
"type": "linux",
"url": "https://toonflow.oss-cn-beijing.aliyuncs.com/latest/latest.AppImage"
},
{
"type": "macos",
"url": "https://toonflow.oss-cn-beijing.aliyuncs.com/latest/latest.dmg"
}
],
"github": [],
"atomgit": [],
"gitee": []
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 169 KiB

File diff suppressed because one or more lines are too long

BIN
docs/g-star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@ -13,19 +13,16 @@ declare const __APP_VERSION__: string | undefined;
* extraResources data
*/
function getVersionFromFile(filePath: string): string | null {
function getVersionFromUpdateJson(filePath: string): string | null {
try {
if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath, "utf8").trim();
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
return data.version ?? null;
}
} catch {}
return null;
}
function writeVersionToFile(filePath: string, version: string): void {
fs.writeFileSync(filePath, version, { encoding: "utf8" });
}
function copyDirForce(src: string, dest: string): void {
if (!fs.existsSync(src)) return;
if (fs.existsSync(dest)) {
@ -37,14 +34,13 @@ function copyDirForce(src: string, dest: string): void {
function initializeData(): void {
const srcDir = path.join(process.resourcesPath, "data");
const destDir = path.join(app.getPath("userData"), "data");
const versionFile = path.join(destDir, "version.txt");
const updateJsonFile = path.join(destDir, "update.json");
const currentVersion = typeof __APP_VERSION__ !== "undefined" ? __APP_VERSION__ : "0.0.0";
const userVersion = getVersionFromFile(versionFile);
const userVersion = getVersionFromUpdateJson(updateJsonFile);
// 首次安装或无version.txt,直接全量拷贝
// 首次安装或无update.json,直接全量拷贝
if (!fs.existsSync(destDir) || !userVersion) {
copyDirRecursive(srcDir, destDir);
writeVersionToFile(versionFile, currentVersion);
return;
}
@ -52,7 +48,6 @@ function initializeData(): void {
if (userVersion !== currentVersion) {
copyDirForce(path.join(srcDir, "serve"), path.join(destDir, "serve"));
copyDirForce(path.join(srcDir, "web"), path.join(destDir, "web"));
writeVersionToFile(versionFile, currentVersion);
}
}
@ -61,6 +56,7 @@ function copyDirRecursive(src: string, dest: string): void {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
// 跳过 oss 文件夹和 db2.sqlite 文件
if (entry.isDirectory() && entry.name === "logs") continue;
if (entry.isDirectory() && entry.name === "oss") continue;
if (!entry.isDirectory() && entry.name === "db2.sqlite") continue;
const srcPath = path.join(src, entry.name);

View File

@ -43,12 +43,23 @@ export async function decisionAI(ctx: AgentContext) {
const skill = path.join(u.getPath("skills"), "production_agent_decision.md");
const prompt = await fs.promises.readFile(skill, "utf-8");
const projectInfo = await u.db("o_project").where("id", ctx.resTool.data.projectId).first();
if (!projectInfo) throw new Error(`项目不存在ID: ${ctx.resTool.data.projectId}`);
const [_, imageModelName] = projectInfo.imageModel!.split(":");
const [id, videoModelName] = projectInfo.videoModel!.split(":");
const data = await u.db("o_vendorConfig").where("id", id).select("models").first();
const models = JSON.parse(data!.models!);
const findData = models.find((i: any) => i.modelName == name);
const isRef = findData.mode.every((i: any) => Array.isArray(i));
const modelInfo = `项目使用的模型如下:\n图像模型${imageModelName}\n视频模型${videoModelName}\n多参${isRef ? "是" : "否"}`;
const mem = buildMemPrompt(await memory.get(text));
const { textStream } = await u.Ai.Text("productionAgent").stream({
messages: [
{ role: "system", content: prompt },
{ role: "assistant", content: mem },
{ role: "assistant", content: mem + "\n" + modelInfo },
{ role: "user", content: text },
],
abortSignal,
@ -138,13 +149,20 @@ function createSubAgent(parentCtx: AgentContext) {
"分镜面板:<storyboardItem videoDesc='视频描述' prompt=提示词内容 track='分组' duration='视频推荐时间' associateAssetsIds='[该分镜所需的资产ID列表]'></storyboardItem>",
"```",
].join("\n");
const projectData = await u.db("o_project").where("id", resTool.data.projectId).first();
const modelInfo = `项目使用的模型如下:\n图像模型${projectData?.imageModel}\n视频模型${projectData?.videoModel}`;
const projectInfo = await u.db("o_project").where("id", resTool.data.projectId).first();
if (!projectInfo) throw new Error(`项目不存在ID: ${resTool.data.projectId}`);
const artSkills = await createArtSkills(projectInfo?.artStyle!);
const [_, imageModelName] = projectInfo.imageModel!.split(":");
const [id, videoModelName] = projectInfo.videoModel!.split(":");
const data = await u.db("o_vendorConfig").where("id", id).select("models").first();
const models = JSON.parse(data!.models!);
const findData = models.find((i: any) => i.modelName == name);
const isRef = findData.mode.every((i: any) => Array.isArray(i));
const modelInfo = `项目使用的模型如下:\n图像模型${imageModelName}\n视频模型${videoModelName}\n多参${isRef ? "是" : "否"}`;
return runAgent({
prompt,
system: systemPrompt + addPrompt,
@ -194,7 +212,6 @@ async function createArtSkills(artName: string) {
return res;
}
function removeAllXmlTags(text: string): string {
text = text.replace(/<([a-zA-Z][\w-]*)(\s+[^>]*)?>([\s\S]*?)<\/\1>/g, "");
text = text.replace(/<([a-zA-Z][\w-]*)(\s+[^>]*)?\/>/g, "");

View File

@ -16,7 +16,7 @@ export default router.post(
async (req, res) => {
const { id, url, flowId } = req.body;
const [imageId] = await u.db("o_image").insert({
filePath: new URL(url).pathname,
filePath: u.replaceUrl(url),
state: "已完成",
assetsId: id,
});

View File

@ -13,7 +13,15 @@ export default router.post(
}),
async (req, res) => {
const { edges, nodes } = req.body;
nodes.forEach((node: any) => {
if (node.type == "upload") {
node.data.image = node.data.image ? u.replaceUrl(node.data.image) : "";
}
if (node.type == "generated") {
node.data.generatedImage = node.data.generatedImage ? u.replaceUrl(node.data.generatedImage) : "";
}
});
const [insertFlowId] = await u.db("o_imageFlow").insert({
flowData: JSON.stringify({ edges, nodes }),
});

View File

@ -10,39 +10,19 @@ export default router.post(
validateFields({
edges: z.any(),
nodes: z.any(),
id: z.number(),
imageUrl: z.string(),
flowId: z.number(),
episodesId: z.number(),
}),
async (req, res) => {
const { edges, nodes, flowId } = req.body;
nodes.forEach((node: any) => {
if (node.type == "upload") {
node.data.image = node.data.image ? new URL(node.data.image).pathname : "";
node.data.image = node.data.image ? u.replaceUrl(node.data.image) : "";
}
if (node.type == "generated") {
node.data.generatedImage = node.data.generatedImage ? new URL(node.data.generatedImage).pathname : "";
node.data.generatedImage = node.data.generatedImage ? u.replaceUrl(node.data.generatedImage) : "";
}
});
// let imagePath = "";
// try {
// imagePath = new URL(imageUrl).pathname;
// } catch (e) {}
// if (imagePath) {
// if (type == "storyboard") {
// await u.db("o_storyboard").where("id", id).update({
// filePath: imagePath,
// });
// } else {
// const [imageId] = await u.db("o_image").insert({
// filePath: imagePath,
// assetsId: id,
// state: "已完成",
// });
// await u.db("o_assets").where("id", id).update({ imageId });
// }
// }
await u
.db("o_imageFlow")

View File

@ -116,6 +116,7 @@ export default router.post(
prompt: item.prompt ?? "",
desc: item.describe ?? "",
src: item.filePath && (await u.oss.getFileUrl(item.filePath!)),
flowId: item.flowId,
derive: await Promise.all(
childAssetsData
.filter((child) => child.assetsId === item.id)
@ -129,6 +130,7 @@ export default router.post(
src: child.filePath && (await u.oss.getFileUrl(child.filePath!)),
state: child.state ?? "未生成",
errorReason: child?.errorReason ?? "",
flowId: child.flowId,
})),
),
})),
@ -145,6 +147,7 @@ export default router.post(
videoDesc: i.videoDesc,
shouldGenerateImage: i.shouldGenerateImage,
reason: i?.reason ?? "",
flowId: i.flowId,
}))
.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
res.status(200).send(success(flowData));

View File

@ -35,7 +35,7 @@ export default router.post(
prompt,
duration,
state,
filePath: new URL(src).pathname,
filePath: u.replaceUrl(src),
trackId,
videoDesc,
shouldGenerateImage: src ? 1 : 0,

View File

@ -19,7 +19,7 @@ export default router.post(
.db("o_storyboard")
.where({ id })
.update({
filePath: new URL(url).pathname,
filePath: u.replaceUrl(url),
flowId,
state: "已完成",
shouldGenerateImage:url ? 1 : 0

View File

@ -1,5 +1,7 @@
import express from "express";
import { success } from "@/lib/responseFormat";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { z } from "zod";
const router = express.Router();
import fs from "fs";
@ -17,21 +19,49 @@ const APP_VERSION: string = (() => {
return pkg.version;
})();
export default router.post("/", async (req, res) => {
const tagger = "1.1.0";
const taggerList = tagger.split(".").map(Number);
const currentVersionList = APP_VERSION.split(".").map(Number);
//对比Major
if (taggerList[0] > currentVersionList[0]) {
return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: false }));
}
//对比Minor
if (taggerList[1] > currentVersionList[1]) {
return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: false }));
}
//Patch
if (taggerList[2] > currentVersionList[2]) {
return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: false }));
}
return res.status(200).send(success({ needUpdate: false, latestVersion: tagger, reinstall: false }));
});
export default router.post(
"/",
validateFields({
source: z.enum(["toonflow", "github", "gitee", "atomgit"]),
}),
async (req, res) => {
const { source } = req.body;
const getUrl = "https://toonflow.oss-cn-beijing.aliyuncs.com/update.json";
const versionInfo = await fetch(getUrl).then((res) => res.json());
if (!versionInfo) return res.status(400).send(error("无法获取版本信息"));
const { version: tagger, time, data } = versionInfo;
const sourceData = data[source];
if (!sourceData) return res.status(400).send(error("无法获取该源的下载信息"));
const platformType: Record<string, string> = {
win32: "windows",
darwin: "macos",
linux: "linux",
};
const zipItem = sourceData.find((d: any) => d.type === "zip");
const installerItem = sourceData.find((d: any) => d.type === platformType[process.platform]);
const taggerList = tagger.split(".").map(Number);
const currentVersionList = APP_VERSION.split(".").map(Number);
//对比Major
if (taggerList[0] > currentVersionList[0]) {
if (!installerItem) return res.status(400).send(error("该源暂无适用于当前系统的安装包"));
return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: true, time, url: installerItem.url }));
}
//对比Minor
if (taggerList[1] > currentVersionList[1]) {
if (!installerItem) return res.status(400).send(error("该源暂无适用于当前系统的安装包"));
return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: true, time, url: installerItem.url }));
}
//Patch
if (taggerList[2] > currentVersionList[2]) {
if (!zipItem) return res.status(400).send(error("该源暂无增量更新包"));
return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: false, time, url: zipItem.url }));
}
return res.status(200).send(success({ needUpdate: false, latestVersion: tagger, reinstall: false, time }));
},
);

View File

@ -1,238 +1,64 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import getPath from "@/utils/getPath";
import z from "zod";
import { validateFields } from "@/middleware/middleware";
import u from "@/utils";
import fs from "fs";
import path from "path";
import axios from "axios";
import compressing from "compressing";
import { validateFields } from "@/middleware/middleware";
import { spawn } from "child_process";
import path from "path";
import { success, error } from "@/lib/responseFormat";
const router = express.Router();
/** 仓库源配置 */
const REPO_SOURCES = {
github: {
repo: "HBAI-Ltd/Toonflow-app",
api: "https://api.github.com/repos/HBAI-Ltd/Toonflow-app/releases/latest",
headers: { Accept: "application/vnd.github.v3+json" },
},
gitee: {
repo: "HBAI-Ltd/Toonflow-app",
api: "https://gitee.com/api/v5/repos/HBAI-Ltd/Toonflow-app/releases/latest",
headers: {},
},
} as const;
type SourceType = keyof typeof REPO_SOURCES;
function normalizeAssets(source: SourceType, release: any): { name: string; browser_download_url: string }[] {
if (source === "github") {
return (release.assets ?? []).map((a: any) => ({
name: a.name,
browser_download_url: a.browser_download_url,
}));
const runInstaller = (installerPath: string) => {
const { exec } = require("child_process");
if (process.platform === "darwin") {
exec(`open "${installerPath}"`);
} else {
if (process.platform !== "win32") fs.chmodSync(installerPath, 0o755);
exec(`"${installerPath}"`);
}
return (release.assets ?? []).map((a: any) => ({
name: a.name,
browser_download_url: a.browser_download_url,
}));
}
};
/** 获取当前系统平台和架构标识,用于匹配安装包文件名 */
function getPlatformArch(): { platform: string; arch: string } {
const platform = process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : "linux";
const arch = process.arch === "arm64" ? "arm64" : "x64";
return { platform, arch };
}
/** 匹配安装包资产(.exe / .dmg / .AppImage / .portable.exe */
function findInstallerAsset(assets: any[]): any | null {
const { platform, arch } = getPlatformArch();
const installerExtensions: Record<string, string[]> = {
win: [".exe"],
mac: [".dmg"],
linux: [".AppImage"],
};
const exts = installerExtensions[platform] || [".exe"];
// 优先找 nsis 安装包(排除 portable如果没有再找 portable
return (
assets.find(
(a: any) =>
exts.some((ext) => a.name.endsWith(ext)) &&
a.name.includes(arch) &&
!a.name.toLowerCase().includes("portable") &&
!a.name.endsWith(".blockmap"),
) ??
assets.find((a: any) => exts.some((ext) => a.name.endsWith(ext)) && a.name.includes(arch) && !a.name.endsWith(".blockmap")) ??
null
);
}
/**
*
*/
async function downloadFile(url: string, destPath: string): Promise<void> {
const dir = path.dirname(destPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const response = await axios.get(url, {
responseType: "stream",
headers: { Accept: "application/octet-stream" },
timeout: 600_000, // 10 分钟超时
});
const writer = fs.createWriteStream(destPath);
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on("finish", resolve);
writer.on("error", reject);
});
}
export default router.post(
"/",
validateFields({
source: z.enum(["github", "gitee"]),
url: z.url(),
reinstall: z.boolean(),
latestVersion: z.string(),
}),
async (req, res) => {
try {
const { reinstall, latestVersion, source } = req.body as {
reinstall: boolean;
latestVersion: string;
source: string;
};
if (!latestVersion) {
return res.status(400).send(error("缺少目标版本号 latestVersion"));
const { reinstall, url } = req.body;
const rootDir = u.getPath(["temp"]);
fs.mkdirSync(rootDir, { recursive: true });
if (reinstall) {
const response = await axios.get(url, { responseType: "arraybuffer" });
const ext =
path.extname(new URL(url).pathname) || (process.platform === "win32" ? ".exe" : process.platform === "darwin" ? ".dmg" : ".AppImage");
const installerPath = path.join(rootDir, `latest${ext}`);
fs.writeFileSync(installerPath, response.data);
runInstaller(installerPath);
res.status(200).send(success("安装包已下载并启动"));
} else {
const zip = await axios.get(url, { responseType: "arraybuffer" }).then((res) => res.data);
fs.writeFileSync(`${rootDir}/latest.zip`, zip);
await compressing.zip.uncompress(`${rootDir}/latest.zip`, rootDir);
const tempServerPath = u.getPath(["temp", "serve"]);
if (fs.existsSync(tempServerPath)) {
fs.cpSync(tempServerPath, u.getPath(["serve"]), { recursive: true });
}
const sourceConfig = REPO_SOURCES[source as SourceType] ?? REPO_SOURCES.github;
// ─── 获取 Release 信息(支持 GitHub / Gitee ──────────────────────
let releaseRes;
try {
releaseRes = await axios.get(sourceConfig.api, {
headers: sourceConfig.headers,
timeout: 30_000,
});
} catch (e) {
return res.status(500).send(error(`获取 ${source} Release 信息失败`));
const webPath = u.getPath(["temp", "web"]);
if (fs.existsSync(webPath)) {
fs.cpSync(webPath, u.getPath(["web"]), { recursive: true });
}
const release = releaseRes.data;
const assets = normalizeAssets(source as SourceType, release);
if (reinstall) {
// ═══════════════ 模式 A下载完整安装包 ═══════════════
const installerAsset = findInstallerAsset(assets);
if (!installerAsset) {
return res.status(404).send(error("未找到当前平台的安装包,请前往 GitHub Releases 手动下载"));
}
const tempDir = getPath(["temp"]);
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
const installerPath = path.join(tempDir, installerAsset.name);
// 如果已经下载过相同文件,跳过下载
if (!fs.existsSync(installerPath)) {
await downloadFile(installerAsset.browser_download_url, installerPath);
}
// 使用 shell 打开安装程序
const sub = spawn("cmd", ["/c", `${installerPath}`], {
cwd: tempDir,
detached: true,
stdio: "ignore",
windowsHide: false,
});
sub.unref();
return res.status(200).send(
success({
type: "reinstall",
version: latestVersion,
filePath: installerPath,
message: "安装包已下载并打开,请按照安装向导完成更新",
}),
);
} else {
// ═══════════════ 模式 Bdata 补丁热更新 ═══════════════
const patchAsset = assets.find((a: any) => a.name.startsWith(latestVersion) && a.name.endsWith(".zip")) ?? null;
if (!patchAsset) {
return res.status(404).send(error("未找到 data 补丁包,请前往 GitHub Releases 手动下载"));
}
//
const tempDir = getPath(["temp"]);
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
const patchZipPath = path.join(tempDir, `${latestVersion}.zip`);
// 下载补丁 zip
await downloadFile(patchAsset.browser_download_url, patchZipPath);
// 解压覆盖到 data 目录(同名文件夹先删除再解压,确保完全替换)
const dataDir = getPath();
// 先读取 zip 内的顶层文件夹/文件列表,删除 data 目录下的同名项
const zipStream = new compressing.zip.UncompressStream({ source: patchZipPath, zipFileNameEncoding: "utf8" });
const topLevelEntries = new Set<string>();
await new Promise<void>((resolve, reject) => {
zipStream.on("entry", (_header: any, stream: any, next: () => void) => {
const entryName: string = _header.name || "";
// 取顶层名称(第一个 / 之前的部分)
const topName = entryName.split("/")[0];
if (topName) topLevelEntries.add(topName);
stream.resume();
next();
});
zipStream.on("finish", resolve);
zipStream.on("error", reject);
});
// 删除 data 目录下与 zip 顶层同名的文件夹/文件
for (const name of topLevelEntries) {
const targetPath = path.join(dataDir, name);
if (fs.existsSync(targetPath)) {
const stat = fs.statSync(targetPath);
if (stat.isDirectory()) {
fs.rmSync(targetPath, { recursive: true, force: true });
} else {
fs.unlinkSync(targetPath);
}
}
}
await compressing.zip.uncompress(patchZipPath, dataDir, { zipFileNameEncoding: "utf8" });
// 清理临时文件
try {
fs.unlinkSync(patchZipPath);
} catch {
// 忽略清理失败
}
return res.status(200).send(
success({
type: "patch",
version: latestVersion,
message: "补丁更新完成,请重启应用以使更新生效",
restartRequired: true,
}),
);
const tempSkillsPath = u.getPath(["temp", "skills"]);
if (fs.existsSync(tempSkillsPath)) {
fs.cpSync(tempSkillsPath, u.getPath(["skills"]), { recursive: true, force: false });
}
} catch (err: any) {
console.error("[downloadApp] 更新失败:", err);
const message = err?.response?.status === 404 ? "未找到更新资源,请检查版本号或稍后重试" : (err?.message ?? "更新失败,请稍后重试");
return res.status(500).send(error(message));
const tempModelsPath = u.getPath(["temp", "models"]);
if (fs.existsSync(tempModelsPath)) {
fs.cpSync(tempModelsPath, u.getPath(["models"]), { recursive: true, force: false });
}
fs.rmSync(rootDir, { recursive: true, force: true });
res.status(200).send(success("更新成功5秒后重启"));
}
},
);

View File

@ -14,7 +14,5 @@ export default router.post("/", async (req, res) => {
onlyFiles: true,
});
console.log("%c Line:15 🍺 entries", "background:#e41a6a", entries);
res.status(200).send(success(entries));
});

View File

@ -1,21 +1,6 @@
// @db-hash 35cf00f711e9d4df398703de70511684
// @db-hash 7af86e2bafe5cab7d175eb68cf76ed7a
//该文件由脚本自动生成,请勿手动修改
export interface _o_project_old_20260402 {
'artStyle'?: string | null;
'createTime'?: number | null;
'id'?: number | null;
'imageModel'?: string | null;
'imageQuality'?: string | null;
'intro'?: string | null;
'mode'?: string | null;
'name'?: string | null;
'projectType'?: string | null;
'type'?: string | null;
'userId'?: number | null;
'videoModel'?: string | null;
'videoRatio'?: string | null;
}
export interface _o_storyboard_old_20260402 {
'createTime'?: number | null;
'duration'?: string | null;
@ -180,7 +165,6 @@ export interface o_outlineNovel {
export interface o_project {
'artStyle'?: string | null;
'createTime'?: number | null;
'directorManual'?: string | null;
'id'?: number | null;
'imageModel'?: string | null;
'imageQuality'?: string | null;
@ -301,7 +285,6 @@ export interface o_videoTrack {
}
export interface DB {
"_o_project_old_20260402": _o_project_old_20260402;
"_o_storyboard_old_20260402": _o_storyboard_old_20260402;
"_o_storyboard_old_20260402_1": _o_storyboard_old_20260402_1;
"_o_vendorConfig_old_20260401": _o_vendorConfig_old_20260401;

View File

@ -10,6 +10,7 @@ import task from "@/utils/taskRecord";
import Ai from "@/utils/ai";
import { getPrompts } from "@/utils/getPrompts";
import { getArtPrompt } from "@/utils/getArtPrompt";
import replaceUrl from "@/utils/replaceUrl";
export default {
db,
@ -24,4 +25,5 @@ export default {
task,
getPrompts,
getArtPrompt,
replaceUrl
};

12
src/utils/replaceUrl.ts Normal file
View File

@ -0,0 +1,12 @@
export default function replaceUrl(url: string): string {
if (typeof url !== 'string' || !url.trim()) return '';
let cleanedPath = '';
try {
const pathname = new URL(url).pathname;
cleanedPath = pathname.replace(/^\/oss/, '');
} catch (e) {
// 如果不是有效的URL则直接返回原字符串
cleanedPath = url;
}
return cleanedPath;
}