Skip to content

结合 后端接口 实现自动更新

简介

通过使用 electron-builder 与和 electron-updater,结合 后端接口 可以实现自动更新。

步骤 1: 创建对应后端接口

步骤 2: 配置 electron-builder.yml 文件

js
publish:
  - provider: generic
    url: 'your .exe file download_url 目录'
    updaterCacheDirName: 'file name'

步骤 3: 配置 upload.ts 文件

js
iimport { app, BrowserWindow, ipcMain } from 'electron'
import { autoUpdater } from 'electron-updater'
import log from 'electron-log'
import axios from 'axios'
// 全局变量,用于存储主窗口引用
let mainWindow: BrowserWindow | null = null

// 版本信息接口地址
const VERSION_API_URL = 'your-version-api-url'

// 版本信息接口返回的数据结构
interface VersionInfo {
  current_version: string
  description: string
  download_url: string
  release_date: string
  update_available: boolean
}

/**
 * 设置主窗口引用
 * @param window 主窗口实例
 */
export function setMainWindow(window: BrowserWindow | null) {
  mainWindow = window
}

/**
 * 发送日志消息到渲染进程
 * @param type 消息类型
 * @param msg 消息内容
 */
function sendLogMessage(type: 'info' | 'success' | 'warning' | 'error', msg: string) {
  if (mainWindow) {
    mainWindow.webContents.send('log-message', { type, msg })
  }
}

/**
 * 设置自动更新
 */
export function setupAutoUpdater() {
  // 配置autoUpdater
  autoUpdater.autoDownload = false
  autoUpdater.autoInstallOnAppQuit = false

  /**
   * 获取更新内容文本
   * @param updataUrl 基础URL
   * @returns 更新内容文本
   */
  async function fetchUpdateContent(updataUrl: string): Promise<string> {
    try {
      sendLogMessage('info', '正在获取更新内容: ' + updataUrl)

      const response = await axios.get(updataUrl)

      const content = response.data
      sendLogMessage('info', '成功获取更新内容')
      return content.trim()
    } catch (error) {
      sendLogMessage('error', '获取更新内容失败: ' + error.message)
      return ''
    }
  }

  // 检查更新
  ipcMain.handle('check-for-updates', async () => {
    try {
      sendLogMessage('info', '正在请求版本信息: ' + VERSION_API_URL)
      const response = await axios.post(
        VERSION_API_URL,
        {
          type: 'win10'
        },
        {
          method: 'post',
          headers: {
            'Content-Type': 'application/json'
          }
        }
      )
      sendLogMessage('info', '服务器响应状态: ' + response.status + ' ' + response.statusText)

      const versionInfo: VersionInfo = response.data
      const currentVersion = app.getVersion()
      sendLogMessage('info', '当前版本: ' + currentVersion)

      // 获取更新内容
      let updateContent = ''
      if (versionInfo.update_available) {
        const updataUrl = versionInfo.release_date

        updateContent = await fetchUpdateContent(updataUrl)
      }

      // 构造与electron-updater兼容的返回格式
      const updateInfo = {
        version: versionInfo.current_version,
        releaseNotes: versionInfo.description ? [versionInfo.description] : [],
        releaseDate: versionInfo.release_date,
        updateAvailable: versionInfo.update_available,
        updateContent: updateContent
      }

      if (versionInfo.update_available) {
        // 配置autoUpdater的下载URL
        if (versionInfo.download_url) {
          const baseUrl = versionInfo.download_url.substring(
            0,
            versionInfo.download_url.lastIndexOf('/')
          )

          sendLogMessage('info', '设置下载基础URL: ' + baseUrl)

          // 测试latest.yml是否可访问
          try {
            const testYmlUrl = `${baseUrl}/latest.yml`
            sendLogMessage('info', '测试latest.yml URL: ' + testYmlUrl)
            axios
              .head(testYmlUrl, { method: 'HEAD' })
              .then((response) => {
                if (response.status === 200) {
                  sendLogMessage('info', 'latest.yml可访问,状态码: ' + response.status)
                } else {
                  sendLogMessage('warning', 'latest.yml不可访问,状态码: ' + response.status)
                }
              })
              .catch((err) => {
                sendLogMessage('error', '测试latest.yml失败: ' + err.message)
              })
          } catch (testError) {
            sendLogMessage('error', '测试latest.yml出错: ' + testError.message)
          }

          autoUpdater.setFeedURL({
            provider: 'generic',
            url: baseUrl
          })
        } else {
          // 使用默认下载URL
          autoUpdater.setFeedURL({
            provider: 'generic',
            url: 'your latest.yml file download_url 目录'
          })
        }

        // 通知渲染进程有可用更新
        if (mainWindow) {
          mainWindow.webContents.send('update-available', updateInfo)
        }
      }

      return { updateInfo }
    } catch (error) {
      sendLogMessage('error', '检查更新失败: ' + error.message)
      return { error: error.message }
    }
  })

  // 添加获取应用版本号的处理程序
  ipcMain.handle('get-app-version', () => {
    return app.getVersion()
  })

  // 添加下载更新处理程序
  ipcMain.handle('download-update', async () => {
    try {
      // 清除之前的所有监听器,避免重复
      autoUpdater.removeAllListeners()
      // 记录当前的下载URL配置
      sendLogMessage('info', '开始准备下载更新...')
      // 设置下载进度监听
      autoUpdater.on('download-progress', (progressObj) => {
        sendLogMessage('info', '下载进度: ' + progressObj.percent + '%')
        if (mainWindow) {
          mainWindow.webContents.send('download-progress', { percent: progressObj.percent })
        }
      })

      // 设置下载完成监听
      autoUpdater.on('update-downloaded', (info) => {
        sendLogMessage('success', '更新下载完成: ' + info.version)
        if (mainWindow) {
          mainWindow.webContents.send('update-downloaded', {
            version: info.version,
            releaseDate: info.releaseDate
          })
        }
      })

      // 添加错误监听
      autoUpdater.on('error', (error) => {
        sendLogMessage('error', '更新下载错误: ' + error.message)
        // 将错误信息发送到渲染进程
        if (mainWindow) {
          mainWindow.webContents.send('update-error', { message: error.message })
        }
      })

      // 添加日志监听
      autoUpdater.logger = log
      autoUpdater.logger.transports.file.level = 'debug'

      // 开始下载
      sendLogMessage('info', '开始下载更新...')
      // 尝试手动检查更新文件
      try {
        const url = 'your latest.yml download_url 目录'
        const latestYmlUrl = `${url}/latest.yml`
        sendLogMessage('info', '尝试获取latest.yml: ' + latestYmlUrl)
        const response = await axios.get(latestYmlUrl)

        if (response.status === 200) {
          const ymlContent = await response.data
          sendLogMessage('info', `latest.yml内容前100字符: ${ymlContent.substring(0, 100)}...`)
          // 验证yml内容是否包含必要的字段
          if (!ymlContent.includes('path:') || !ymlContent.includes('sha512:')) {
            sendLogMessage('error', 'latest.yml格式不正确,缺少必要字段')
            throw new Error('latest.yml格式不正确,缺少必要字段')
          }
        } else {
          sendLogMessage('warning', `无法获取latest.yml,状态码: ${response.status}`)
          throw new Error(`无法获取latest.yml,状态码: ${response.status}`)
        }
      } catch (checkError) {
        sendLogMessage('error', `检查latest.yml失败: ${checkError.message}`)
        return { error: checkError.message }
      }

      // 确保autoUpdater配置正确
      autoUpdater.setFeedURL({
        provider: 'generic',
        url: 'your latest.yml download_url 目录'
      })

      // 添加这一行:先检查更新
      sendLogMessage('info', '检查更新...')
      await autoUpdater.checkForUpdates()

      autoUpdater.downloadUpdate().catch((err) => {
        sendLogMessage('error', `下载更新失败: ${err.message}`)
        if (mainWindow) {
          mainWindow.webContents.send('update-error', { message: err.message })
        }
        throw err
      })

      return { success: true }
    } catch (error) {
      sendLogMessage('error', '下载更新失败: ' + error.message)
      return { error: error.message }
    }
  })

  // 添加安装并重启应用处理程序
  ipcMain.on('install-and-restart', () => {
    autoUpdater.quitAndInstall(false, true)
  })
}

步骤 4: 配置 index.ts 文件

js
import { app, shell, BrowserWindow, ipcMain, Tray, Menu } from "electron";
import { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import { setupUpdate } from "./update";

let mainWindow: BrowserWindow | null = null;

function createWindow(): void {
  const iconPath = join(__dirname, "../../resources/icon.png");
  mainWindow = new BrowserWindow({
    width: 900,
    height: 670,
    show: false,
    autoHideMenuBar: true,
    icon: iconPath,
    ...(process.platform === "linux" ? { iconPath } : {}),
    webPreferences: {
      preload: join(__dirname, "../preload/index.js"),
      sandbox: false,
    },
    frame: false,
    transparent: true,
  });

  mainWindow.on('ready-to-show', () => {
    setMainWindow(mainWindow)
  })

  // 获取应用更新
  export function initWindows(): void {
    setupAutoUpdater()
  };
}

步骤 5: 配置 package.json 文件

js
"scripts": {
    "build": "electron-vite build",
    "build:win": "npm run build && electron-builder --win --config --publish always",
  }

步骤 6: 打包后会在本地release目录下生成一个新的版本目录, 将其中的.exe 文件和latest.yml 文件通过脚本打包上传到服务器或者腾讯云储存桶.

js
const fs = require("fs");
const path = require("path");
const archiver = require("archiver");
const axios = require("axios");
const FormData = require("form-data");
const http = require("http");
const https = require("https");
const COS = require("cos-nodejs-sdk-v5");
// 配置
const RELEASE_DIR = "./release";
const UPDATE_SERVER_URL = "your update server url";
const TEMP_ZIP_PATH = "./update-files.zip";

// 腾讯云COS配置
const COS_CONFIG = {
  SecretId: "", // SecretId
  SecretKey: "", // SecretKey
  Region: "", // 存储桶地区
  Bucket: "", // 存储桶名称
};

const cos = new COS({
  SecretId: COS_CONFIG.SecretId,
  SecretKey: COS_CONFIG.SecretKey,
});

// 创建自定义的 HTTP Agent 配置
const httpAgent = new http.Agent({
  keepAlive: true,
  keepAliveMsecs: 30000,
  maxSockets: 5,
  maxFreeSockets: 2,
  timeout: 300000, // 5分钟超时
});

const httpsAgent = new https.Agent({
  keepAlive: true,
  keepAliveMsecs: 30000,
  maxSockets: 5,
  maxFreeSockets: 2,
  timeout: 300000, // 5分钟超时
});

/**
 * 获取最新版本的文件夹
 * @returns {string} 最新版本的文件夹路径
 */
const getLatestVersionFolder = () => {
  // 读取release目录下的所有文件夹
  const versionFolders = fs
    .readdirSync(RELEASE_DIR)
    .filter((folder) => {
      const folderPath = path.join(RELEASE_DIR, folder);
      return (
        fs.statSync(folderPath).isDirectory() && /^\d+\.\d+\.\d+$/.test(folder)
      );
    })
    .sort((a, b) => {
      // 按版本号排序(假设版本号格式为x.y.z)
      const versionA = a.split(".").map(Number);
      const versionB = b.split(".").map(Number);

      for (let i = 0; i < 3; i++) {
        if (versionA[i] !== versionB[i]) {
          return versionB[i] - versionA[i]; // 降序排列,最新版本在前
        }
      }
      return 0;
    });

  if (versionFolders.length === 0) {
    throw new Error("没有找到任何版本文件夹");
  }

  const latestVersion = versionFolders[0];
  console.info(`找到最新版本: ${latestVersion}`);
  return path.join(RELEASE_DIR, latestVersion);
};

/**
 * 创建包含.exe和latest.yml文件的压缩包
 * @param {string} sourceFolder 源文件夹路径
 * @param {string} targetZip 目标压缩包路径
 * @returns {Promise<void>}
 */
const createUpdatePackage = (sourceFolder, targetZip) => {
  return new Promise((resolve, reject) => {
    console.info(`从 ${sourceFolder} 创建更新包...`);

    // 检查文件是否存在
    const exeFiles = fs
      .readdirSync(sourceFolder)
      .filter((file) => file.endsWith(".exe") && !file.endsWith(".blockmap"));
    if (exeFiles.length === 0) {
      return reject(new Error("没有找到.exe安装文件"));
    }

    const latestYmlPath = path.join(sourceFolder, "latest.yml");
    if (!fs.existsSync(latestYmlPath)) {
      return reject(new Error("没有找到latest.yml文件"));
    }

    // 检查根目录下的updata.txt文件
    const updataTxtPath = path.join(process.cwd(), "updata.txt");
    if (!fs.existsSync(updataTxtPath)) {
      console.warn("警告: 没有找到updata.txt文件,将跳过该文件");
    }

    // 创建压缩包
    const output = fs.createWriteStream(targetZip);
    const archive = archiver("zip", {
      zlib: { level: 9 }, // 最高压缩级别
    });

    output.on("close", () => {
      console.info(
        `压缩完成,文件大小: ${(archive.pointer() / 1024 / 1024).toFixed(2)} MB`
      );
      resolve();
    });

    archive.on("error", (err) => reject(err));
    archive.pipe(output);

    // 添加.exe文件
    exeFiles.forEach((exeFile) => {
      const exeFilePath = path.join(sourceFolder, exeFile);
      archive.file(exeFilePath, { name: exeFile });
      console.info(`添加文件: ${exeFile}`);
    });

    // 添加latest.yml文件
    archive.file(latestYmlPath, { name: "latest.yml" });
    console.info("添加文件: latest.yml");

    // 添加updata.txt文件(如果存在)
    if (fs.existsSync(updataTxtPath)) {
      archive.file(updataTxtPath, { name: "updata.txt" });
      console.info("添加文件: updata.txt");
    }

    archive.finalize();
  });
};

/**
 * 上传文件到更新服务器(带重试机制)
 * @param {string} filePath 要上传的文件路径
 * @param {number} maxRetries 最大重试次数
 * @returns {Promise<boolean>} 上传是否成功
 */
const uploadToServer = async (filePath, maxRetries = 3) => {
  console.info(`开始上传 ${filePath} 到 ${UPDATE_SERVER_URL}...`);

  const fileStats = fs.statSync(filePath);
  const fileSizeInMB = (fileStats.size / 1024 / 1024).toFixed(2);
  console.info(`文件大小: ${fileSizeInMB} MB`);

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    console.info(`尝试上传 (${attempt}/${maxRetries})...`);

    try {
      const formData = new FormData();
      const fileStream = fs.createReadStream(filePath);

      // 监听文件流错误
      fileStream.on("error", (err) => {
        console.error("文件流错误:", err.message);
      });

      formData.append("file", fileStream);
      formData.append("type", "win10");

      // 配置请求选项
      const config = {
        headers: {
          ...formData.getHeaders(),
          Connection: "close",
          "User-Agent": "Upload-Script/1.0",
        },
        timeout: 120000, // 2分钟超时
        maxContentLength: Infinity,
        maxBodyLength: Infinity,
        validateStatus: function (status) {
          return status >= 200 && status < 300;
        },
        // 上传进度监控
        onUploadProgress: (progressEvent) => {
          if (progressEvent.total) {
            const percentCompleted = Math.round(
              (progressEvent.loaded * 100) / progressEvent.total
            );
            if (percentCompleted % 10 === 0) {
              console.info(`上传进度: ${percentCompleted}%`);
            }
          }
        },
      };

      const response = await axios.post(UPDATE_SERVER_URL, formData, config);

      // 完善响应处理逻辑
      console.info("服务器响应状态:", response.status);
      console.info("服务器响应数据:", response.data);

      // 检查响应格式和成功状态
      if (response.data && typeof response.data === "object") {
        if (response.data.success === true) {
          console.info("✅ 上传成功!");
          if (response.data.message) {
            console.info("📝 服务器消息:", response.data.message);
          }
          return true;
        } else {
          // 处理 success: false 的情况
          const errorMessage =
            response.data.message ||
            response.data.error ||
            "服务器返回失败状态";
          console.error("❌ 上传失败:", errorMessage);
          if (attempt === maxRetries) {
            return false;
          }
        }
      } else {
        // 处理响应格式不正确的情况
        console.error("❌ 服务器响应格式异常:", response.data);
        if (attempt === maxRetries) {
          return false;
        }
      }

      // 重试前等待
      if (attempt < maxRetries) {
        console.info(`等待 ${attempt * 2} 秒后重试...`);
        await new Promise((resolve) => setTimeout(resolve, attempt * 2000));
      }
    } catch (error) {
      console.error(
        `❌ 上传出错 (尝试 ${attempt}/${maxRetries}):`,
        error.message
      );

      // 详细错误信息
      if (error.code) {
        console.error("错误代码:", error.code);
      }

      if (error.response) {
        console.error(
          "服务器响应状态:",
          error.response.status,
          error.response.statusText
        );
        if (error.response.data) {
          console.error("服务器响应数据:", error.response.data);

          // 尝试解析错误响应中的消息
          if (
            typeof error.response.data === "object" &&
            error.response.data.message
          ) {
            console.error("服务器错误消息:", error.response.data.message);
          }
        }
      } else if (error.request) {
        console.error("请求配置:", {
          url: error.config?.url,
          method: error.config?.method,
          timeout: error.config?.timeout,
        });
        console.error("网络错误: 无法连接到服务器或请求超时");
      } else {
        console.error("请求设置错误:", error.message);
      }

      // 如果是最后一次尝试,返回失败
      if (attempt === maxRetries) {
        console.error("🔄 已达到最大重试次数,上传失败");
        return false;
      }

      // 等待后重试,等待时间递增
      const waitTime = attempt * 5000;
      console.info(`等待 ${waitTime / 1000} 秒后重试...`);
      await new Promise((resolve) => setTimeout(resolve, waitTime));
    }
  }

  return false;
};

/**
 * 上传文件到腾讯云COS(带重试机制)
 * @param {string} filePath 要上传的文件路径
 * @param {string} key COS中的文件路径
 * @param {number} maxRetries 最大重试次数
 * @returns {Promise<boolean>} 上传是否成功
 */
const uploadToCOS = async (filePath, key, maxRetries = 3) => {
  console.info(`开始上传 ${filePath} 到腾讯云COS...`);

  const fileStats = fs.statSync(filePath);
  const fileSizeInMB = (fileStats.size / 1024 / 1024).toFixed(2);
  console.info(`文件大小: ${fileSizeInMB} MB`);

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    console.info(`尝试上传到COS (${attempt}/${maxRetries})...`);

    try {
      const result = await new Promise((resolve, reject) => {
        cos.putObject(
          {
            Bucket: COS_CONFIG.Bucket,
            Region: COS_CONFIG.Region,
            Key: key,
            Body: fs.createReadStream(filePath),
            ContentLength: fileStats.size,
            onProgress: (progressData) => {
              const percentCompleted = Math.round(
                (progressData.loaded * 100) / progressData.total
              );
              if (percentCompleted % 10 === 0) {
                console.info(`COS上传进度: ${percentCompleted}%`);
              }
            },
          },
          (err, data) => {
            if (err) {
              reject(err);
            } else {
              resolve(data);
            }
          }
        );
      });

      if (result.statusCode === 200) {
        console.info("✅ 腾讯云COS上传成功!");
        console.info(
          "📝 COS文件地址:",
          `https://${COS_CONFIG.Bucket}.cos.${COS_CONFIG.Region}.myqcloud.com/${key}`
        );
        return true;
      } else {
        console.error("❌ 腾讯云COS上传失败!");
        return false;
      }
    } catch (error) {
      console.error(
        `❌ COS上传出错 (尝试 ${attempt}/${maxRetries}):`,
        error.message
      );

      if (error.code) {
        console.error("COS错误代码:", error.code);
      }

      // 如果是最后一次尝试,返回失败
      if (attempt === maxRetries) {
        console.error("🔄 COS已达到最大重试次数,上传失败");
        return false;
      }

      // 等待后重试,等待时间递增
      const waitTime = attempt * 3000;
      console.info(`等待 ${waitTime / 1000} 秒后重试COS上传...`);
      await new Promise((resolve) => setTimeout(resolve, waitTime));
    }
  }

  return false;
};

/**
 * 上传.exe文件到腾讯云COS
 * @param {string} sourceFolder 源文件夹路径
 * @returns {Promise<boolean>} 上传是否成功
 */
const uploadExeFilesToCOS = async (sourceFolder) => {
  console.info("🔍 查找.exe文件...");

  // 获取所有.exe文件
  const exeFiles = fs
    .readdirSync(sourceFolder)
    .filter((file) => file.endsWith(".exe") && !file.endsWith(".blockmap"));

  if (exeFiles.length === 0) {
    console.error("❌ 没有找到.exe文件");
    return false;
  }

  const latestYmlPath = path.join(sourceFolder, "latest.yml");
  if (!fs.existsSync(latestYmlPath)) {
    console.error("❌ 没有找到latest.yml文件");
    return false;
  }

  console.info(`找到 ${exeFiles.length} 个.exe文件:`, exeFiles);

  // 上传每个.exe文件
  for (const exeFile of exeFiles) {
    const exeFilePath = path.join(sourceFolder, exeFile);
    const cosKey = `win10/${exeFile}`; // COS中的文件路径

    console.info(`📤 上传 ${exeFile} 到腾讯云COS...`);
    const uploadSuccess = await uploadToCOS(exeFilePath, cosKey);

    if (!uploadSuccess) {
      console.error(`❌ ${exeFile} 上传失败`);
      return false;
    }

    console.info(`✅ ${exeFile} 上传成功`);
  }

  // 上传latest.yml文件
  console.info("📤 上传 latest.yml 到腾讯云COS...");
  const ymlUploadSuccess = await uploadToCOS(latestYmlPath, "win10/latest.yml");

  if (!ymlUploadSuccess) {
    console.error("❌ latest.yml 上传失败");
    return false;
  }

  console.info("✅ latest.yml 上传成功");

  // 检查并上传updata.txt文件(如果存在)
  const updataTxtPath = path.join(process.cwd(), "updata.txt");
  if (fs.existsSync(updataTxtPath)) {
    console.info("📤 上传 updata.txt 到腾讯云COS...");
    const txtUploadSuccess = await uploadToCOS(
      updataTxtPath,
      "win10/updata.txt"
    );

    if (txtUploadSuccess) {
      console.info("✅ updata.txt 上传成功");
    } else {
      console.warn("⚠️ updata.txt 上传失败,但不影响主要功能");
    }
  }

  return true;
};

/**
 * 清理临时文件
 */
const cleanup = () => {
  if (fs.existsSync(TEMP_ZIP_PATH)) {
    fs.unlinkSync(TEMP_ZIP_PATH);
    console.info(`已删除临时文件: ${TEMP_ZIP_PATH}`);
  }
};

/**
 * 主函数
 */
const main = async () => {
  try {
    console.info("🚀 开始构建和上传更新包...");

    // 清理之前的临时文件
    cleanup();

    // 获取最新版本文件夹
    const latestVersionFolder = getLatestVersionFolder();

    // 创建更新包
    console.info("📦 创建更新包...");
    await createUpdatePackage(latestVersionFolder, TEMP_ZIP_PATH);

    // 上传到服务器
    console.info("⬆️ 上传到服务器...");
    const uploadSuccess = await uploadToServer(TEMP_ZIP_PATH);

    if (uploadSuccess) {
      console.info("✅ 更新文件已成功上传到服务器");
      // 上传到腾讯云COS
      console.info("☁️ 上传到腾讯云COS...");
      const cosUploadSuccess = await uploadExeFilesToCOS(latestVersionFolder);

      if (cosUploadSuccess) {
        console.info("✅ .exe文件已成功上传到腾讯云COS");
        cleanup();
        console.info("🗑️ 已清理临时文件");
        process.exit(0);
      } else {
        console.error("❌ .exe文件上传到腾讯云COS失败");
        process.exit(1);
      }
    } else {
      console.error("❌ 更新文件上传失败");
      process.exit(1);
    }
  } catch (error) {
    console.error("💥 发生错误:", error.message);
    console.error("错误堆栈:", error.stack);
    process.exit(1);
  } finally {
    // 清理临时文件
    cleanup();

    // 清理连接池
    httpAgent.destroy();
    httpsAgent.destroy();
  }
};

// 处理未捕获的异常
process.on("uncaughtException", (error) => {
  console.error("未捕获的异常:", error);
  cleanup();
  process.exit(1);
});

process.on("unhandledRejection", (reason) => {
  console.error("未处理的 Promise 拒绝:", reason);
  cleanup();
  process.exit(1);
});

// 执行主函数
main();

总结

总体思路就是:

  1. 创建对应的后端上传文件接口, 获取文件接口.
  2. 配置 electron-builder.yml 文件
  3. 配置 upload.ts 文件,监听用户的更新请求,下载更新,安装更新
  4. 配置 index.ts 文件,监听用户的更新请求,获取应用更新
  5. 配置 package.json 文件,打包命令,发布命令
  6. 打包后将release目录下的文件上传到服务器

贡献者

unthapy
unthapy

用知识点燃技术的火山