Просмотр исходного кода

📦 添加自动化部署脚本

imengyu недель назад: 2
Родитель
Сommit
e0c58f9d35

+ 2 - 1
package.json

@@ -8,7 +8,8 @@
     "build": "run-p type-check \"build-only {@}\" --",
     "preview": "vite preview",
     "build-only": "vite build",
-    "type-check": "vue-tsc --build"
+    "type-check": "vue-tsc --build",
+    "updater": "node src/scripts/UpdateScript/index.mjs"
   },
   "dependencies": {
     "@imengyu/js-request-transform": "^0.3.3",

+ 4 - 0
src/scripts/.gitignore

@@ -0,0 +1,4 @@
+node_modules
+_config.json
+_token.json
+upload.zip

+ 22 - 0
src/scripts/UpdateScript/deprecate.html

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>提示</title>
+    <style>.global-error{position:fixed;left:0;right:0;bottom:100px;top:0;display:flex;flex-direction:column;justify-content:center;align-items:center;background-color:#fff;-webkit-app-region:drag}.global-error span{margin-top:20px;font-size:16px}button{margin-top:20px;padding:0;display:inline-block;-webkit-app-region:no-drag;height:40px;width:120px;appearance:none;outline:none;border:none;background-color:#0083da;color:#fff;cursor:pointer}button:hover{background-color:#005c99}</style>
+  </head>
+  <body>
+    <script>
+      function relaunch() {
+        if (typeof api.relaunch == 'function') 
+          api.relaunch();
+        else
+          location.reload();
+      }
+    </script>
+    <div class="global-error">
+      <span>系统已更新,请重启</span>
+      <button id="load-confirm-button" onclick="relaunch()">确定</button>
+    </div>
+</html>

+ 524 - 0
src/scripts/UpdateScript/index.mjs

@@ -0,0 +1,524 @@
+/**
+ * 更新发布工具
+ * 
+ * Copyright © 2025 imengyu.top imengyu-update-server
+ */
+
+import { program } from 'commander';
+import { password, input, confirm, select } from '@inquirer/prompts';
+import { writeFile, readFile } from 'node:fs/promises';
+import { postAppUpdate, postWebUpdate } from './postUpdate.mjs';
+import Table from 'cli-table3';
+import md5 from 'md5';
+import axios from 'axios';
+import path from 'path';
+import { dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+//基础配置
+//========================================
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+const constant = {
+  ServerUrl: 'http://update-server1.imengyu.top/',
+  TokenSave: path.resolve(__dirname, './_token.json'),
+};
+let currentData = {
+  token: '',
+  identifier: '',
+};
+
+readFile(constant.TokenSave).then((res) => {
+  currentData = JSON.parse(res);
+  if (!currentData.identifier) 
+    currentData.identifier = `commandClient${Math.floor(Math.random() * 1000)}`;
+  start();
+}).catch(() => {
+  start();
+})
+
+const axiosInstance = axios.create({
+  baseURL: constant.ServerUrl,
+  timeoutErrorMessage: '请求超时,请检查网络连接',
+  responseType: 'json',
+  withCredentials: false,
+  validateStatus: () => true,
+});
+
+axiosInstance.interceptors.request.use((value) => {
+  value.headers['authorization'] = JSON.stringify({
+    auth: currentData?.token?.authName,
+    validity: currentData?.token?.authKey,
+    nonce: "aaaaaaaaaa",
+    identifier: currentData.identifier,
+    key: 'abc123',
+  });  
+  value.url = value.url + (value.url.includes('?') ? '&' : '?' ) + `identifier=${currentData.identifier}`
+  return value;
+});
+axiosInstance.interceptors.response.use((value) => {
+  if (value.data.success) 
+    return value.data;
+  else
+    return Promise.reject(value.data);
+});
+
+function getErrorMessage(e) {
+  return e instanceof Error ? e.message : (typeof e === 'object' ? e : ('' + e));
+}
+
+//登录相关
+//========================================
+
+async function checkLogged() {
+  try {
+    await axiosInstance.get('/auth');
+    console.log('已登录');
+  } catch (e) {
+    console.error('获取状态失败:', getErrorMessage(e));
+  }
+}
+async function login(user) {
+  try {
+    const pass = await password({ message: '输入密码' });
+    const res = await axiosInstance.post('/auth?rember=true', {
+      method: 'key',
+      key: `${user}@${md5(pass)}`,
+    })
+    currentData.token = {
+      authName: res.data.authName,
+      authKey: res.data.authKey,
+    };
+    writeFile(constant.TokenSave, JSON.stringify(currentData));
+    console.log('登录成功');
+  } catch (e) {
+    console.error('登录失败', getErrorMessage(e));
+  }
+}
+async function logout() {
+  currentToken = '';
+  writeFile(constant.TokenSave, JSON.stringify(currentData));
+  try {
+    await axiosInstance.delete('/auth');
+    console.log('退出登录成功');
+  } catch(e) {
+    console.error('退出登录失败', getErrorMessage(e));
+  }
+}
+
+//版本相关
+//========================================
+
+async function viewVersion(type) {
+  if (type === 'all' || !type) {
+    const data = await axiosInstance.get('/version/list');
+    const table = new Table({
+      head: ['ID', '版本'], 
+      colWidths: [10, 20 ]
+    });
+
+    data.data.forEach((d) => {
+      table.push([ d.id, d.version ]);
+    })
+    console.log(table.toString());
+  } else {
+    let data = null;
+    try {
+      if (Number.isNaN(Number(type)))
+        data = await axiosInstance.get('/version/get-by-name?name=' + type);
+      else
+        data = await axiosInstance.get('/version/' + type);
+    }
+    catch (e) {
+      console.error('Failed to load version info', e);
+      return;
+    }
+    const table = new Table({
+      head: ['key', 'data'], 
+      colWidths: [30, 60]
+    });
+    table.push(
+      [ 'ID', data.data.id ],
+      [ '状态', stateConstant[data.data.status] ],
+      [ '版本号', data.data.version ],
+      [ '创建时间', new Date(data.data.createAt).toString() ],
+      [ '设置', data.data.config ],
+      [ '激活的Web更新ID', data.data.webUpdateId ],
+      [ '激活的App更新ID', data.data.appUpdateId ],
+      [ '激活的下一个App更新ID', data.data.appUpdateNextId ],
+    );
+    console.log(table.toString());
+  }
+}
+async function getVersion(action, type) {
+  switch(action) {
+    case 'view':
+      await viewVersion(type);
+      break;
+    case 'new': {
+      const version = await input({ message: 'Enter version name, like (1.0.0)' });
+      try {
+        await axiosInstance.post('/version', {
+          version: version,
+          status: 1,
+          config: "{}",
+        });
+        console.log('Add version success');
+      } catch (e) {
+        console.error('Failed to add version', e);
+      }
+      break;
+    }
+    case 'delete': {
+      const versionId = type ? type : await input({ message: '输入版本ID' });
+      if (!await confirm({ message: `确定删除版本 ${versionId}?`, default: false }))
+        return;
+      if (!await confirm({ message: '确认删除版本?此操作会删除所属版本的所有更新项目、存储等,无法恢复,是否确定删除?', default: false }))
+        return;
+      try {
+        await axiosInstance.delete('/version/' + versionId);
+        console.log('删除版本成功');
+      } catch (e) {
+        console.error('删除版本失败', e);
+      }
+      break;
+    }
+    case 'set-state': {
+      const versionId = type ? type : await input({ message: '输入版本ID' });
+      const state = await select({
+        message: '设置状态',
+        choices: [
+          {
+            name: 'NotEnable',
+            value: 0,
+          },
+          {
+            name: 'Normal',
+            value: 1,
+          },
+          {
+            name: 'Deprecated',
+            value: 2,
+          },
+        ],
+      });
+
+      try {
+        await axiosInstance.put('/version/' + versionId, {
+          status: state
+        });
+        console.log('设置状态成功');
+      } catch (e) {
+        console.error('设置状态失败', e);
+      }
+      break;
+    }
+    case 'set-config': {
+      const versionId = type ? type : await input({ message: '输入版本ID' });
+      const config = await input({ message: '输入配置Json' });
+      try {
+        await axiosInstance.put('/version/' + versionId, {
+          config: config,
+        });
+        console.log('设置配置成功');
+      } catch (e) {
+        console.error('设置配置失败', e);
+      }
+      break;
+    }
+    case 'set-active-app-update': {
+      const versionId = await input({ message: '输入版本ID' });
+      const updateId = await input({ message: '输入更新ID' });
+      const isNext = await confirm({ message: 'Set as next active?', default: false });
+
+      try {
+        await axiosInstance.post('/update/active/app', { versionId, updateId, isNext });
+        console.log('成功');
+      } catch (e) {
+        console.error('失败', e);
+      }
+      break;
+    }
+    case 'set-active-web-update': {
+      const versionId = await input({ message: '输入版本ID' });
+      const updateId = await input({ message: '输入更新ID' });
+
+      try {
+        await axiosInstance.post('/update/active/web', { versionId, updateId });
+        console.log('成功');
+      } catch (e) {
+        console.error('失败', e);
+      }
+      break;
+    }
+    default:
+      console.error('未知参数', action);
+      break;
+  }
+}
+
+//选择方法
+//========================================
+
+export async function selectVersion(requireString = false, defaultVersionId = null) {
+  const data = (await axiosInstance.get('/version/list')).data;
+  if (data.length === 0) {
+    console.error('没有版本');
+    return;
+  }
+  const resultId = (await select({
+    choices: data.map(p => ({
+      value: p.id,
+      name: p.version,
+    })),
+    default: defaultVersionId,
+    message: '选择一个版本',
+  }));
+  if (requireString) {
+    return data.find(p => p.id === resultId).version
+  }
+  return resultId;
+}
+export async function selectUpdate() {
+  const versionId = await selectVersion();
+  const data = (await axiosInstance.get('/version/update?search=' + JSON.stringify({ versionId }))).data;
+  if (data.length === 0) {
+    console.error('没有更新');
+    return;
+  }
+  const resultId = (await select({
+    choices: data.map(p => ({
+      value: p.id,
+      name: p.version,
+    })),
+    default: defaultVersionId,
+    message: '选择一个更新',
+  }));
+  return resultId;
+}
+
+//更新相关
+//========================================
+
+const stateConstant = [ 'Deleted', 'Normal', 'Deprecated' ];
+const typeConstant = [ 'Unknow', 'Web', 'app' ];
+const storageTypeConstant = [ 'Unknow', 'LocalStorage', 'AliOSS' ];
+
+async function postUpdate(type, options) {
+  switch (type) {
+    case 'web': {
+      await postWebUpdate(axiosInstance, options);
+      break;
+    }
+    case 'app': {
+      await postAppUpdate(axiosInstance, options);
+      break;
+    }
+    default:
+      console.error('Unknow type', type);
+      break;
+  }
+}
+async function deprecateOrDeleteUpdate(updateId) {
+  if (!updateId)
+    updateId = await selectUpdate();
+
+  const { currentUpdateInfo, currentVersionInfo } = await viewUpdate(updateId);
+
+  const deprecate = (await select({
+    choices: [
+      {
+        name: 'Deprecate',
+        value: 0,
+      },
+      {
+        name: 'Delete',
+        value: 1,
+      },
+    ],
+    message: '删除或弃用?',
+  })) === 0;
+
+  if (deprecate && currentUpdateInfo.type !== 1) {
+    console.log('只有Web更新允许弃用');
+    return;
+  }
+  if (currentUpdateInfo.status === 0) {
+    console.log(`当前状态 ${stateConstant[currentUpdateInfo.status]} 无法弃用`);
+    return;
+  }
+
+  if (deprecate) {
+    if (!await confirm({ message: `确定弃用当前版本 ${currentUpdateInfo.versionCode} ?弃用会删除存储文件以及备份。此操作无法恢复!`, default: false }))
+      return;
+  } else {
+    if (!await confirm({ message: `确定删除当前版本 ${currentUpdateInfo.versionCode} ?此操作无法恢复!`, default: false }))
+      return;
+  }
+
+  try {
+    await axiosInstance.post(`/update/${deprecate ? 'deprecate' : 'delete'}`, { updateId });
+    console.log(`${deprecate ? '弃用' : '删除'} 成功`);
+  } catch (e) {
+    console.error(`操作失败`, e);
+  }
+}
+async function viewUpdate(updateId) {
+  
+  let currentUpdateInfo = null
+  let currentVersionInfo = null
+
+  try {
+    currentUpdateInfo = (await axiosInstance.get('/update/' + updateId)).data;
+  } catch (e) {
+    console.error('加载更新信息失败', updateId);
+  }
+  try {
+    currentVersionInfo = (await axiosInstance.get('/version/' + currentUpdateInfo.versionId)).data;
+  } catch (e) {
+    console.error('加载版本信息失败', currentUpdateInfo.versionId);
+  }
+
+  const table = new Table({
+    head: ['key', 'data'], 
+    colWidths: [20, 40]
+  });
+  table.push(
+    [ 'ID', currentUpdateInfo.id ],
+    [ '所属应用', currentUpdateInfo.name ],
+    [ '更新信息', currentUpdateInfo.updateInfo ],
+    [ '版本号', currentUpdateInfo.versionCode ],
+    [ '创建时间', new Date(currentUpdateInfo.createAt).toString() ],
+    [ '类型', typeConstant[currentUpdateInfo.type] ],
+    [ '状态', stateConstant[currentUpdateInfo.status] ],
+    [ '强制更新', currentUpdateInfo.force ],
+    [ '公共访问路径', currentUpdateInfo.publicUrl ],
+    [ '存储类型', storageTypeConstant[currentUpdateInfo.storageType] ],
+    [ '存储路径', currentUpdateInfo.storagePath ],
+  );
+
+  console.log(table.toString());
+
+  if (currentUpdateInfo.activeWebVersionName) {
+    table.push(
+      [ '使用中的Web版本', currentUpdateInfo.activeWebVersionName ],
+    );
+  }
+  if (currentUpdateInfo.activeAppVersionName) {
+    table.push(
+      [ '使用中的App版本', currentUpdateInfo.activeAppVersionName ],
+    );
+  }
+
+  return {
+    currentUpdateInfo,
+    currentVersionInfo,
+  }
+}
+async function getUpdate(action, type, all, options) {
+  const typeNotANumber = isNaN(new Number(type)); 
+
+  if (action === 'view' && (!type || typeNotANumber)) {
+
+    let hasSerch = false;
+    const search = {};
+    const sort = {
+      field: "createAt",
+      order: "descend"
+    }
+    if (typeNotANumber && type !== 'all') {
+      hasSerch = true;
+      search.version = type;
+    }
+
+    const data = await axiosInstance.get('/update/list?full=true' + (hasSerch ? ('&search=' + JSON.stringify(search)) : '') + '&sort=' + JSON.stringify(sort));
+    const table = new Table({
+      head: ['ID', '版本', '版本号', '类型', '状态'], 
+      colWidths: [10, 10, 20, 10, 15 ]
+    });
+
+    if (type !== 'all' && all !== 'all' && data.data.length > 10) {
+      data.data = data.data.slice(0, 10);
+      console.log('filter!', all);
+    }
+
+    data.data.forEach((d) => {
+      table.push([ 
+        d.id, 
+        (d.activeAppVersionName ? `${d.activeAppVersionName} (App)` : (
+          d.activeWebVersionName? `${d.activeWebVersionName} (Web)` : '无'
+        )),  
+        d.versionCode, typeConstant[d.type], stateConstant[d.status] 
+      ]);
+    })
+    
+    console.log(table.toString());
+  } else {
+    switch (action) {
+      case 'post': {
+        await postUpdate(type, options);
+        break;
+      }
+      case 'delete': {
+        await deprecateOrDeleteUpdate(type);
+        break;
+      }
+      case 'view': {
+        viewUpdate(type);
+        break;
+      }
+      default: 
+        console.error('Unknow action', action);
+        break;
+    }
+  }
+}
+async function testVersion(version) {
+  try {
+    const data = await axiosInstance.get('/update-get-info?version=' + version);
+    console.log('版本信息', data.data);
+  } catch(e) {
+    console.log('获取失败', e);
+  }
+}
+
+//程序入口
+//============================================
+
+program
+  .command('login <user>')
+  .description('登录')
+  .action(login);
+program
+  .command('logstate')
+  .description('检查登录状态')
+  .action(checkLogged);
+program
+  .command('logout')
+  .description('退出登录')
+  .action(logout);
+
+program
+  .command('test <version>')
+  .description('测试更新入口')
+  .action(testVersion);
+
+program
+  .command('version <action> [type]')
+  .description('查看版本信息/发布版本/删除版本/设置版本, action 可选 view/new/delete/set-web-update/set-app-update/set-next-app-update')
+  .action(getVersion);
+program
+  .command('update <action> [type] [all]')
+  .description('查看更新信息/发布更新, action 可选 view/post, view type 可选 id/all; post type 可选 web/app')
+  .option('--skip', '跳过构建')
+  .option('--ndelete', '不删除构建文件')
+  .action(getUpdate);
+
+function start() {
+  program.parse(process.argv);
+}
+
+process.on('unhandledRejection', (reason, p) => {
+  console.error('Promise: ', p, 'Reason: ', reason)
+})

+ 63 - 0
src/scripts/UpdateScript/postConfig.mjs

@@ -0,0 +1,63 @@
+import { readFileSync } from 'node:fs';
+import { pad } from './postUpdate.mjs';;
+import { dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import path from 'node:path';
+
+//基础配置
+//========================================
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+//提交更新配置
+//========================================
+
+export const config = {
+  submitKey: '',
+  uploadWebConfig: {
+    storageAction: 'override',
+    storageProps: {
+      overrideMode: 'overrideFiles',
+      overrideFiles: [
+        'index.html',
+      ],
+      newFolderNameGenerateType: 'hash',
+    },
+    deprecateConfig: {
+      indexFile: 'index.html',
+      updateHtml: readFileSync(path.resolve(__dirname, './deprecate.html'), 'utf8')
+    },
+  },
+  buildWebCommand: 'npm run build-only', //构建命令
+  buildWebOutDir: '../../../dist', //构建输出目录。相对于当前文件目录
+  buildWebOutVersionPath: '', //版本号输出目录,输出版本号至文件以供项目使用。相对于当前文件目录
+  /**
+   * 自定义生成Web版本号的方法。
+   * @param {Date} now 当前日期
+   * @param {Number} lastTodaySubVersion 今天上传的之前版本数量
+   * @returns 
+   */
+  buildWebVersionGenerateCommand: async (now, lastTodaySubVersion) => {
+    //生成Web版本号
+    const version = `${now.getFullYear().toString().substring(2)}${pad(now.getMonth() + 1, 2)}${pad(now.getDate(), 2)}.${pad(lastTodaySubVersion, 2)}`;
+    return version;
+  },
+  buildAppCallback: async (param, versionCode, lastTodaySubVersion) => {
+    //构建App
+    throw new Error('未实现buildAppCallback方法');
+  }, 
+  buildAppGetUploadFile: async (param) => {
+    //获取上传文件路径
+    throw new Error('未实现buildAppGetUploadFile方法');
+  }, 
+  buildAppGetOSSFileName: async (param) => {
+    //生成OSS保存路径
+    throw new Error('buildAppGetOSSFileName');
+  },//构建命令
+  buildAppOutDir: './dist', //构建输出目录。相对于当前文件目录
+  buildAppGetVersion: async () => {
+    //获取版本号
+    throw new Error('未实现buildAppGetVersion方法');
+  },
+} 

+ 397 - 0
src/scripts/UpdateScript/postUpdate.mjs

@@ -0,0 +1,397 @@
+/**
+ * 更新发布工具
+ * 
+ * Copyright © 2025 imengyu.top imengyu-update-server
+ */
+
+import { confirm , input } from '@inquirer/prompts';
+import { writeFile, readFile, access, unlink, readdir, stat, constants } from 'node:fs/promises';
+import { exec } from 'node:child_process';
+import fs from 'fs';
+import archiver from 'archiver';
+import path from 'node:path';
+import OSS from 'ali-oss';
+import cliProgress from 'cli-progress';
+import { selectVersion } from './index.mjs';
+import { config } from './postConfig.mjs';
+import { dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+//基础配置
+//========================================
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+function readFileRange(file, start, length) {
+  return new Promise((resolve, reject) => {
+    fs.open(file, 'r', (err, fd) => {
+      if (err) {
+        reject('Error opening file:', er);
+        return;
+      }
+      const buffer = Buffer.alloc(length);
+      fs.read(fd, buffer, 0, length, start, (err, bytesRead, buffer) => {
+        if (err) {
+          reject('Error reading file:', err)
+          return;
+        }
+        fs.close(fd, (err) => {
+          if (err) {
+            reject('Error closing file:', err)
+            return ;
+          }
+          resolve(buffer);
+        });
+      });
+    });
+  })
+  
+}
+export function pad(num, n) {
+  var strNum = num.toString();
+  var len = strNum.length;
+  while (len < n) {
+    strNum = "0" + strNum;
+    len++;
+  }
+  return strNum;
+}
+async function getConfig() {
+  let postConfig = {
+    lastVersion: '',
+    lastUpdateInfo: '',
+    lastSubmitDay: '',
+    lastTodaySubVersion: 0,
+  };
+  try {
+    postConfig = JSON.parse(await readFile(path.resolve(__dirname, './_config.json')));
+  } catch {
+    //
+  }
+  if (postConfig.lastSubmitDay != new Date().getDate()) 
+    postConfig.lastTodaySubVersion = 0;
+  return postConfig;
+}
+async function saveConfig(postConfig) {
+  postConfig.lastSubmitDay = new Date().getDate();
+  await writeFile(path.resolve(__dirname, './_config.json'), JSON.stringify(postConfig));
+}
+
+//App更新与提交
+//========================================
+
+export async function postAppUpdate(axiosInstance, param) {
+  const postConfig = await getConfig();
+  const versionId = await selectVersion(false, postConfig.lastVersion);
+  const updateInfo = await input({ message: '输入更新信息', default: postConfig.lastUpdateInfo });
+
+  await axiosInstance.post('/update-post', { config: { type: 2, test: true, versionId, uploadWebConfig: config.uploadWebConfig, submitKey: config.submitKey } });
+
+  postConfig.lastVersion = versionId;
+  postConfig.lastUpdateInfo = updateInfo;
+  postConfig.lastTodaySubVersion++;
+
+  const updateSource = (await select({
+    choices: [
+      {
+        name: '重新构建',
+        value: 'rebuild',
+      },
+      {
+        name: '已上传的阿里OSS文件路径',
+        value: 'uploaded-alioss',
+      },
+    ],
+    message: '选择上传来源',
+    default: 'rebuild',
+  }));
+
+  const force = await confirm({ message: '强制更新?', default: false });
+  const updateNext = hotfix ? false : await confirm({ message: '作为下个版本?', default: false });
+  const versionCode = config.buildAppGetVersion();
+
+  await saveConfig(postConfig);
+
+  if (updateSource === 'rebuild') 
+    await config.buildAppCallback(param, versionCode, postConfig.lastTodaySubVersion);
+  else if (updateSource === 'uploaded-alioss') {
+    const fileName = await input({ message: '输入已上传的阿里OSS文件路径' });
+    try {
+      const result = (await axiosInstance.post('/update-post', {
+        type: 2,
+        ossConfig: {
+          ossPath: fileName,
+          ossPublic: '',
+        },
+        uploadAppConfig: {
+          updateAsNext: updateNext,
+        },
+        versionId,
+        updateInfo,
+        versionCode: versionCode
+      })).data;
+      console.log('上传成功');
+      console.log('新更新ID: ' + result.updateId);
+    } catch (e) {
+      console.error('上传失败', e);
+    }
+    return;
+  }
+  else {
+    console.error('错误的选择');
+    return;
+  }
+
+  const uploadFile = await config.buildAppGetUploadFile(param);
+  try {
+    await access(uploadFile, constants.R_OK)
+  } catch {
+    console.error(`Failed to access ${uploadFile}, did you created it?`);
+    return;
+  }
+
+  console.log('开始上传');
+
+  //小于8mb则小文件上传,否则使用阿里OSS上传
+  const fileInfo = await stat(uploadFile);
+  if (fileInfo.size < 8 * 1024 * 1024) {
+    const appData = (await readFile(uploadFile));
+
+    const formData = new FormData();
+    formData.append("file", new Blob([ appData ], { type : 'application/zip' }));
+    formData.append("type", 2);
+    formData.append("versionId", versionId);
+    formData.append("updateInfo", updateInfo);
+    formData.append("uploadAppConfig", { updateAsNext: updateNext });
+    formData.append("force", force);
+    formData.append("versionCode", versionCode);
+
+    try {
+      const result = (await axiosInstance.post('/update-post', formData, {
+        headers: { 'Content-Type': 'multipart/form-data' }
+      })).data;
+      console.log('上传成功');
+      console.log('新更新ID: ' + result.updateId);
+      console.log('删除构建文件');
+
+      await unlink(uploadFile);
+    } catch (e) {
+      console.error('上传失败', e);
+    }
+
+  } else {
+    //阿里OSS上传
+    //请求STS进行临时授权
+    const stsToken = (await axiosInstance.post('/update-ali-oss-sts')).data;
+    const client = new OSS({
+      region: stsToken.Region,
+      accessKeyId: stsToken.AccessKeyId,
+      accessKeySecret: stsToken.AccessKeySecret,
+      stsToken: stsToken.SecurityToken,
+      bucket: stsToken.Bucket,
+      refreshSTSToken: async () => {
+        const refreshToken = (await axiosInstance.get("/update-ali-oss-sts")).data;
+        return {
+          accessKeyId: refreshToken.AccessKeyId,
+          accessKeySecret: refreshToken.AccessKeySecret,
+          stsToken: refreshToken.SecurityToken,
+        };
+      },
+    });
+    
+    //小于96mb则直接上传,否则分片上传
+    const fileName = `/${await config.buildAppGetOSSFileName(param)}`;
+
+    console.log('Start upload to ali oss');
+
+    let returnUrl = '';
+    if (fileInfo.size < 96 * 1024 * 1024) {
+      const result = await client.put(fileName, uploadFile);
+      returnUrl = result.url;
+    } else {
+      const bar1 = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
+      bar1.start(100, 0);
+      await aliOSSMultipartUpload(client, fileName, uploadFile, (p) => {
+        bar1.update(p * 100);
+      });
+      bar1.stop();
+    }
+
+    console.log('Upload to ali oss done');
+  
+    try {
+      const result = (await axiosInstance.post('/update-post', {
+        type: 2,
+        ossConfig: {
+          ossPath: fileName,
+          ossPublic: returnUrl,
+        },
+        uploadAppConfig: {
+          updateAsNext: updateNext,
+        },
+        versionId,
+        updateInfo,
+        versionCode: versionCode
+      })).data;
+      console.log('上传成功');
+      console.log('新更新ID: ' + result.updateId);
+    } catch (e) {
+      console.error('上传失败', e);
+    }
+  }
+}
+
+//Web更新与提交
+//========================================
+
+export async function postWebUpdate(axiosInstance, param) {
+  const skipBuild = param.skip
+  const noDelete = param.ndelete;
+  
+  const postConfig = await getConfig();
+  const versionId = await selectVersion(false, postConfig.lastVersion);
+  const updateInfo = await input({ message: '输入更新信息', default: postConfig.lastUpdateInfo });
+
+  postConfig.lastVersion = versionId;
+  postConfig.lastUpdateInfo = updateInfo;
+  postConfig.lastTodaySubVersion++;
+
+  await axiosInstance.post('/update-post', { config: { type: 1, test: true, versionId, uploadWebConfig: config.uploadWebConfig, submitKey: config.submitKey } });
+
+  const now = new Date();
+  const versionCode = await config.buildWebVersionGenerateCommand(now, postConfig.lastTodaySubVersion);
+
+  if (config.buildWebOutVersionPath)
+    await writeFile(path.resolve(__dirname, config.buildWebOutVersionPath), versionCode);
+
+  await saveConfig(postConfig);
+  
+  if (!skipBuild && config.buildWebCommand) {
+    console.log('正在执行构建...');
+
+    await new Promise((resolve, reject) => {
+      exec(config.buildWebCommand, function(err, stdout) {
+        if (err)
+          reject(err);
+        else 
+          resolve();
+      });
+    });
+
+    console.log('构建完成');
+  }
+
+  const distDir = path.resolve(__dirname, config.buildWebOutDir);
+
+  try {
+    await access(distDir, constants.R_OK)
+  } catch {
+    console.error(`Failed to access ${distDir}`);
+    return;
+  }
+
+  const outputPath = __dirname + '/upload.zip';
+
+  if (!skipBuild) {
+    console.log('开始压缩zip...');
+
+    const output = fs.createWriteStream(outputPath);
+    const archive = archiver('zip', {
+      zlib: { level: 9 } // Sets the compression level.
+    });
+    archive.pipe(output);
+
+    const files = await readdir(distDir);
+    for (const file of files) {
+      const filestat = await stat(distDir + '/' + file);
+      if (filestat.isDirectory()) {
+        archive.directory(distDir + '/' + file, file);
+      } else {
+        archive.file(distDir + '/' + file, { name: file });
+      }
+    }
+
+    console.log('等待压缩zip...');
+
+    const waitArchive = new Promise((resolve, reject) => {
+      output.on('close', function() {
+        console.log(archive.pointer() + ' total bytes');
+        console.log('archiver has been finalized and the output file descriptor has closed.');
+        resolve();
+      });
+      archive.on('error', function(err) {
+        reject(err);
+      });
+    })
+
+    await archive.finalize();
+    await waitArchive;
+  }
+
+  console.log('开始上传zip');
+
+  let success = false;
+
+  //小于8mb则小文件上传,否则分片上传
+  const fileInfo = await stat(outputPath);
+  const formData = new FormData();
+  const submitConfig = {
+    type: 1,
+    versionId,
+    updateInfo,
+    versionCode,
+    uploadWebConfig: config.uploadWebConfig,
+  }
+
+  if (fileInfo.size < 8 * 1024 * 1024) {
+    const uploadZipContent = await readFile(outputPath);
+    formData.append("file", new Blob([ uploadZipContent ], { type : 'application/zip' }), 'upload.zip');
+  } else {
+    const multuploadInfo = (await axiosInstance.post('/update-large-token', {
+      fileSize: fileInfo.size,
+      fileName: path.basename(outputPath)
+    })).data;
+    const chunkSize = multuploadInfo.splitPartSize;
+
+    const bar1 = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic);
+    bar1.start(100, 0);
+
+    for (let i = 0; i < multuploadInfo.allChunks; i++) {
+      const start = i * chunkSize;
+      const len = Math.min(start + chunkSize, fileInfo.size) - start;
+      const uploadZipContent = await readFileRange(outputPath, start, len);
+      const subFormData = new FormData();
+      subFormData.append("file", new Blob([ uploadZipContent ], { type : 'application/zip' }), 'upload.zip');
+      subFormData.append("key", multuploadInfo.key);
+      subFormData.append("info", JSON.stringify({}));
+      
+      (await axiosInstance.post('/update-large', subFormData, {
+        headers: { 'Content-Type': 'multipart/form-data' }
+      })).data;
+      
+      bar1.update(Math.floor(i / multuploadInfo.allChunks * 100));
+    }
+
+    bar1.stop();
+
+    submitConfig.multuploadedKey = multuploadInfo.key;
+  }
+
+  try {
+    formData.append("config", JSON.stringify(submitConfig));
+    const result = (await axiosInstance.post('/update-post', formData, {
+      headers: { 'Content-Type': 'multipart/form-data' }
+    })).data;
+    console.log('上传成功');
+    console.log('新更新ID: ' + result.updateId);
+    success = true;
+  } catch (e) {
+    console.error('上传失败', e);
+  }
+  if (!success || noDelete) 
+    return;
+    
+  console.log('删除zip');
+  await unlink(outputPath);
+}