From 1a49e2f8d5f14513b98423d0c2f9af77e8e5b978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ACT=E4=B8=B6=E6=B5=81=E6=98=9F=E9=9B=A8?= <1340145680@qq.com> Date: Thu, 2 Apr 2026 17:28:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router.ts | 132 ++++++------ src/routes/setting/about/checkUpdate.ts | 67 +++++-- src/routes/setting/about/downloadApp.ts | 255 ++++-------------------- 3 files changed, 151 insertions(+), 303 deletions(-) diff --git a/src/router.ts b/src/router.ts index 5880361..e48b32a 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,4 @@ -// @routes-hash 6fee4152cf981edb9229a3dcfafcb1a7 +// @routes-hash b96beb71b18fbc98620917e465530e0c import { Express } from "express"; import route1 from "./routes/agents/clearMemory"; @@ -99,38 +99,39 @@ import route95 from "./routes/scriptAgent/getPlanData"; import route96 from "./routes/scriptAgent/setPlanData"; import route97 from "./routes/scriptAgent/updateData"; import route98 from "./routes/setting/about/checkUpdate"; -import route99 from "./routes/setting/about/downloadApp"; -import route100 from "./routes/setting/agentDeploy/agentSetKey"; -import route101 from "./routes/setting/agentDeploy/deployAgentModel"; -import route102 from "./routes/setting/agentDeploy/getAgentDeploy"; -import route103 from "./routes/setting/dbConfig/clearData"; -import route104 from "./routes/setting/dev/getSwitchAiDevTool"; -import route105 from "./routes/setting/dev/updateSwitchAiDevTool"; -import route106 from "./routes/setting/fileManagement/openFolder"; -import route107 from "./routes/setting/getTextModel"; -import route108 from "./routes/setting/loginConfig/getUser"; -import route109 from "./routes/setting/loginConfig/updateUserPwd"; -import route110 from "./routes/setting/memoryConfig/delAllMemory"; -import route111 from "./routes/setting/memoryConfig/getMemory"; -import route112 from "./routes/setting/memoryConfig/sureMemory"; -import route113 from "./routes/setting/promptManage/getPrompt"; -import route114 from "./routes/setting/promptManage/updatePrompt"; -import route115 from "./routes/setting/skillManagement/getSkillContent"; -import route116 from "./routes/setting/skillManagement/getSkillList"; -import route117 from "./routes/setting/skillManagement/saveSkillContent"; -import route118 from "./routes/setting/vendorConfig/addVendor"; -import route119 from "./routes/setting/vendorConfig/deleteVendor"; -import route120 from "./routes/setting/vendorConfig/enableVendor"; -import route121 from "./routes/setting/vendorConfig/getCodeByLink"; -import route122 from "./routes/setting/vendorConfig/getVendorList"; -import route123 from "./routes/setting/vendorConfig/modelTest"; -import route124 from "./routes/setting/vendorConfig/updateCode"; -import route125 from "./routes/setting/vendorConfig/updateVendor"; -import route126 from "./routes/task/getProject"; -import route127 from "./routes/task/getTaskApi"; -import route128 from "./routes/task/getTaskCategories"; -import route129 from "./routes/task/taskDetails"; -import route130 from "./routes/test/test"; +import route99 from "./routes/setting/about/downloadApp copy"; +import route100 from "./routes/setting/about/downloadApp"; +import route101 from "./routes/setting/agentDeploy/agentSetKey"; +import route102 from "./routes/setting/agentDeploy/deployAgentModel"; +import route103 from "./routes/setting/agentDeploy/getAgentDeploy"; +import route104 from "./routes/setting/dbConfig/clearData"; +import route105 from "./routes/setting/dev/getSwitchAiDevTool"; +import route106 from "./routes/setting/dev/updateSwitchAiDevTool"; +import route107 from "./routes/setting/fileManagement/openFolder"; +import route108 from "./routes/setting/getTextModel"; +import route109 from "./routes/setting/loginConfig/getUser"; +import route110 from "./routes/setting/loginConfig/updateUserPwd"; +import route111 from "./routes/setting/memoryConfig/delAllMemory"; +import route112 from "./routes/setting/memoryConfig/getMemory"; +import route113 from "./routes/setting/memoryConfig/sureMemory"; +import route114 from "./routes/setting/promptManage/getPrompt"; +import route115 from "./routes/setting/promptManage/updatePrompt"; +import route116 from "./routes/setting/skillManagement/getSkillContent"; +import route117 from "./routes/setting/skillManagement/getSkillList"; +import route118 from "./routes/setting/skillManagement/saveSkillContent"; +import route119 from "./routes/setting/vendorConfig/addVendor"; +import route120 from "./routes/setting/vendorConfig/deleteVendor"; +import route121 from "./routes/setting/vendorConfig/enableVendor"; +import route122 from "./routes/setting/vendorConfig/getCodeByLink"; +import route123 from "./routes/setting/vendorConfig/getVendorList"; +import route124 from "./routes/setting/vendorConfig/modelTest"; +import route125 from "./routes/setting/vendorConfig/updateCode"; +import route126 from "./routes/setting/vendorConfig/updateVendor"; +import route127 from "./routes/task/getProject"; +import route128 from "./routes/task/getTaskApi"; +import route129 from "./routes/task/getTaskCategories"; +import route130 from "./routes/task/taskDetails"; +import route131 from "./routes/test/test"; export default async (app: Express) => { app.use("/api/agents/clearMemory", route1); @@ -231,36 +232,37 @@ export default async (app: Express) => { app.use("/api/scriptAgent/setPlanData", route96); app.use("/api/scriptAgent/updateData", route97); app.use("/api/setting/about/checkUpdate", route98); - app.use("/api/setting/about/downloadApp", route99); - app.use("/api/setting/agentDeploy/agentSetKey", route100); - app.use("/api/setting/agentDeploy/deployAgentModel", route101); - app.use("/api/setting/agentDeploy/getAgentDeploy", route102); - app.use("/api/setting/dbConfig/clearData", route103); - app.use("/api/setting/dev/getSwitchAiDevTool", route104); - app.use("/api/setting/dev/updateSwitchAiDevTool", route105); - app.use("/api/setting/fileManagement/openFolder", route106); - app.use("/api/setting/getTextModel", route107); - app.use("/api/setting/loginConfig/getUser", route108); - app.use("/api/setting/loginConfig/updateUserPwd", route109); - app.use("/api/setting/memoryConfig/delAllMemory", route110); - app.use("/api/setting/memoryConfig/getMemory", route111); - app.use("/api/setting/memoryConfig/sureMemory", route112); - app.use("/api/setting/promptManage/getPrompt", route113); - app.use("/api/setting/promptManage/updatePrompt", route114); - app.use("/api/setting/skillManagement/getSkillContent", route115); - app.use("/api/setting/skillManagement/getSkillList", route116); - app.use("/api/setting/skillManagement/saveSkillContent", route117); - app.use("/api/setting/vendorConfig/addVendor", route118); - app.use("/api/setting/vendorConfig/deleteVendor", route119); - app.use("/api/setting/vendorConfig/enableVendor", route120); - app.use("/api/setting/vendorConfig/getCodeByLink", route121); - app.use("/api/setting/vendorConfig/getVendorList", route122); - app.use("/api/setting/vendorConfig/modelTest", route123); - app.use("/api/setting/vendorConfig/updateCode", route124); - app.use("/api/setting/vendorConfig/updateVendor", route125); - app.use("/api/task/getProject", route126); - app.use("/api/task/getTaskApi", route127); - app.use("/api/task/getTaskCategories", route128); - app.use("/api/task/taskDetails", route129); - app.use("/api/test/test", route130); + app.use("/api/setting/about/downloadApp copy", route99); + app.use("/api/setting/about/downloadApp", route100); + app.use("/api/setting/agentDeploy/agentSetKey", route101); + app.use("/api/setting/agentDeploy/deployAgentModel", route102); + app.use("/api/setting/agentDeploy/getAgentDeploy", route103); + app.use("/api/setting/dbConfig/clearData", route104); + app.use("/api/setting/dev/getSwitchAiDevTool", route105); + app.use("/api/setting/dev/updateSwitchAiDevTool", route106); + app.use("/api/setting/fileManagement/openFolder", route107); + app.use("/api/setting/getTextModel", route108); + app.use("/api/setting/loginConfig/getUser", route109); + app.use("/api/setting/loginConfig/updateUserPwd", route110); + app.use("/api/setting/memoryConfig/delAllMemory", route111); + app.use("/api/setting/memoryConfig/getMemory", route112); + app.use("/api/setting/memoryConfig/sureMemory", route113); + app.use("/api/setting/promptManage/getPrompt", route114); + app.use("/api/setting/promptManage/updatePrompt", route115); + app.use("/api/setting/skillManagement/getSkillContent", route116); + app.use("/api/setting/skillManagement/getSkillList", route117); + app.use("/api/setting/skillManagement/saveSkillContent", route118); + app.use("/api/setting/vendorConfig/addVendor", route119); + app.use("/api/setting/vendorConfig/deleteVendor", route120); + app.use("/api/setting/vendorConfig/enableVendor", route121); + app.use("/api/setting/vendorConfig/getCodeByLink", route122); + app.use("/api/setting/vendorConfig/getVendorList", route123); + app.use("/api/setting/vendorConfig/modelTest", route124); + app.use("/api/setting/vendorConfig/updateCode", route125); + app.use("/api/setting/vendorConfig/updateVendor", route126); + app.use("/api/task/getProject", route127); + app.use("/api/task/getTaskApi", route128); + app.use("/api/task/getTaskCategories", route129); + app.use("/api/task/taskDetails", route130); + app.use("/api/test/test", route131); } diff --git a/src/routes/setting/about/checkUpdate.ts b/src/routes/setting/about/checkUpdate.ts index b229f1c..207df54 100644 --- a/src/routes/setting/about/checkUpdate.ts +++ b/src/routes/setting/about/checkUpdate.ts @@ -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,48 @@ 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: any = { + toonflow: "http://localhost:5173/update.json", + github: "https://api.github.com/repos/toonflow/toonflow/releases/latest", + gitee: "https://gitee.com/api/v5/repos/toonflow/toonflow/releases/latest", + atomgit: "https://api.github.com/repos/atomgit/atomgit/releases/latest", + }; + + const vsersion = await fetch(getUrl[source]).then((res) => res.json()); + if (!vsersion) return res.status(400).send(error("无法获取版本信息")); + const { version: tagger, time, data } = vsersion; + + const platformType: Record = { + win32: "windows", + darwin: "macos", + linux: "linux", + }; + + const zipItem = data.find((d: any) => d.type === "zip"); + const installerItem = data.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]) { + return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: true, time, url: installerItem?.url })); + } + //对比Minor + if (taggerList[1] > currentVersionList[1]) { + return res.status(200).send(success({ needUpdate: true, latestVersion: tagger, reinstall: true, time, url: installerItem?.url })); + } + //Patch + if (taggerList[2] > currentVersionList[2]) { + 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 })); + }, +); diff --git a/src/routes/setting/about/downloadApp.ts b/src/routes/setting/about/downloadApp.ts index 5b146c1..4e64d7c 100644 --- a/src/routes/setting/about/downloadApp.ts +++ b/src/routes/setting/about/downloadApp.ts @@ -1,238 +1,55 @@ 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 = { - 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 { - 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"]); + const webPath = u.getPath(["temp", "web"]); + if (!fs.existsSync(tempServerPath) || !fs.existsSync(webPath)) { + fs.rmSync(rootDir, { recursive: true, force: true }); + return res.status(400).send(error("服务器文件不存在")); } - - 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 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 { - // ═══════════════ 模式 B:data 补丁热更新 ═══════════════ - 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(); - await new Promise((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, - }), - ); - } - } catch (err: any) { - console.error("[downloadApp] 更新失败:", err); - const message = err?.response?.status === 404 ? "未找到更新资源,请检查版本号或稍后重试" : (err?.message ?? "更新失败,请稍后重试"); - return res.status(500).send(error(message)); + fs.cpSync(tempServerPath, u.getPath(["serve"]), { recursive: true }); + fs.cpSync(webPath, u.getPath(["web"]), { recursive: true }); + fs.rmSync(rootDir, { recursive: true, force: true }); + res.status(200).send(success("更新成功,5秒后重启")); } }, );