Browse Source

feat(updater): 实现热更新功能并优化命令处理

添加热更新功能,包括下载更新包、解压、标记更新状态及触发更新流程。优化命令处理逻辑,支持返回命令是否存在及执行结果。更新本地化配置,添加热更新相关文案。删除不再使用的热更新生成脚本。
feat/icon
npmrun 3 weeks ago
parent
commit
bd9ac214c6
  1. 1
      package.json
  2. 6
      packages/locales/languages/zh.json
  3. 20
      packages/locales/main.ts
  4. 5
      packages/locales/package.json
  5. 118
      pnpm-lock.yaml
  6. 2
      src/common/event/common.ts
  7. 6
      src/common/event/update/main.ts
  8. 10
      src/main/commands/UpdateCommand.ts
  9. 4
      src/main/commands/_ioc.ts
  10. 8
      src/main/modules/commands/index.ts
  11. 62
      src/main/modules/updater/hot/gen.ts
  12. 118
      src/main/modules/updater/hot/index.ts
  13. 21
      src/main/modules/updater/index.ts
  14. 2
      tsconfig.web.json

1
package.json

@ -53,6 +53,7 @@
"electron-vite": "^2.3.0",
"eslint": "^8.57.1",
"eslint-plugin-vue": "^9.32.0",
"extract-zip": "^2.0.1",
"locales": "workspace:*",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.52.2",

6
packages/locales/languages/zh.json

@ -1,6 +1,12 @@
{
"title": "aaaaaa2 {name} bbbb",
"update": {
"ready": {
"hot": {
"title": "提示",
"desc": "新版本 v{version} 已经准备好更新, 下次启动程序即可自动更新"
}
},
"status": {
"IDLE": "检查更新",
"InitCheckingUpdate": "初始化检查更新",

20
packages/locales/main.ts

@ -15,7 +15,7 @@ type FlattenKeys<T> = FlattenObject<T>
type TranslationKey = FlattenKeys<typeof zh>
class Locale {
locale: string = "en"
locale: string = "zh"
constructor() {
try {
@ -29,8 +29,22 @@ class Locale {
return this.locale.startsWith("zh")
}
t(key: TranslationKey): string {
return this.isCN() ? get(zh, key) : get(en, key)
t(key: TranslationKey, replacements?: Record<string, string>): string {
let text: string = this.isCN() ? get(zh, key) : get(en, key)
if (!text) {
text = get(zh, key)
if (!text) {
return key
}
}
if (replacements) {
// 替换所有形如 {key} 的占位符
Object.entries(replacements).forEach(([key, value]) => {
console.log(text)
text = text.replace(new RegExp(`{${key}}`, "g"), value)
})
}
return text
}
}

5
packages/locales/package.json

@ -1,11 +1,6 @@
{
"name": "locales",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"

118
pnpm-lock.yaml

@ -14,15 +14,6 @@ importers:
'@electron-toolkit/utils':
specifier: ^3.0.0
version: 3.0.0(electron@31.7.7)
'@types/debug':
specifier: ^4.1.12
version: 4.1.12
'@unocss/reset':
specifier: ^0.64.1
version: 0.64.1
'@vueuse/core':
specifier: ^12.7.0
version: 12.7.0(typescript@5.7.3)
electron-updater:
specifier: ^6.3.9
version: 6.3.9
@ -35,24 +26,6 @@ importers:
reflect-metadata:
specifier: ^0.2.2
version: 0.2.2
sass:
specifier: ^1.85.0
version: 1.85.0
unplugin-auto-import:
specifier: ^19.1.0
version: 19.1.0(@vueuse/core@12.7.0(typescript@5.7.3))
unplugin-vue-components:
specifier: ^28.4.0
version: 28.4.0(@babel/parser@7.26.9)(vue@3.5.13(typescript@5.7.3))
unplugin-vue-macros:
specifier: ^2.14.2
version: 2.14.2(@vueuse/core@12.7.0(typescript@5.7.3))(esbuild@0.23.1)(rollup@4.26.0)(typescript@5.7.3)(vite@5.4.14(@types/node@20.17.19)(sass@1.85.0))(vue-tsc@2.1.10(typescript@5.7.3))(vue@3.5.13(typescript@5.7.3))
unplugin-vue-router:
specifier: ^0.11.2
version: 0.11.2(rollup@4.26.0)(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
vue-router:
specifier: ^4.5.0
version: 4.5.0(vue@3.5.13(typescript@5.7.3))
devDependencies:
'@electron-toolkit/eslint-config':
specifier: ^1.0.2
@ -69,12 +42,18 @@ importers:
'@rushstack/eslint-patch':
specifier: ^1.10.5
version: 1.10.5
'@types/debug':
specifier: ^4.1.12
version: 4.1.12
'@types/node':
specifier: ^20.17.19
version: 20.17.19
'@unocss/preset-rem-to-px':
specifier: ^0.64.1
version: 0.64.1
'@unocss/reset':
specifier: ^0.64.1
version: 0.64.1
'@vitejs/plugin-vue':
specifier: ^5.2.1
version: 5.2.1(vite@5.4.14(@types/node@20.17.19)(sass@1.85.0))(vue@3.5.13(typescript@5.7.3))
@ -87,6 +66,9 @@ importers:
'@vue/eslint-config-typescript':
specifier: ^13.0.0
version: 13.0.0(eslint-plugin-vue@9.32.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.7.3)
'@vueuse/core':
specifier: ^12.7.0
version: 12.7.0(typescript@5.7.3)
debug:
specifier: ^4.4.0
version: 4.4.0
@ -105,6 +87,9 @@ importers:
eslint-plugin-vue:
specifier: ^9.32.0
version: 9.32.0(eslint@8.57.1)
extract-zip:
specifier: ^2.0.1
version: 2.0.1
locales:
specifier: workspace:*
version: link:packages/locales
@ -120,6 +105,9 @@ importers:
rotating-file-stream:
specifier: ^3.2.6
version: 3.2.6
sass:
specifier: ^1.85.0
version: 1.85.0
simplebar-vue:
specifier: ^2.4.0
version: 2.4.0(vue@3.5.13(typescript@5.7.3))
@ -129,6 +117,18 @@ importers:
unocss:
specifier: ^0.64.1
version: 0.64.1(postcss@8.4.49)(rollup@4.26.0)(vite@5.4.14(@types/node@20.17.19)(sass@1.85.0))(vue@3.5.13(typescript@5.7.3))
unplugin-auto-import:
specifier: ^19.1.0
version: 19.1.0(@vueuse/core@12.7.0(typescript@5.7.3))
unplugin-vue-components:
specifier: ^28.4.0
version: 28.4.0(@babel/parser@7.26.9)(vue@3.5.13(typescript@5.7.3))
unplugin-vue-macros:
specifier: ^2.14.2
version: 2.14.2(@vueuse/core@12.7.0(typescript@5.7.3))(esbuild@0.23.1)(rollup@4.26.0)(typescript@5.7.3)(vite@5.4.14(@types/node@20.17.19)(sass@1.85.0))(vue-tsc@2.1.10(typescript@5.7.3))(vue@3.5.13(typescript@5.7.3))
unplugin-vue-router:
specifier: ^0.11.2
version: 0.11.2(rollup@4.26.0)(vue-router@4.5.0(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
vite:
specifier: ^5.4.14
version: 5.4.14(@types/node@20.17.19)(sass@1.85.0)
@ -144,6 +144,9 @@ importers:
vue-i18n:
specifier: ^11.1.1
version: 11.1.1(vue@3.5.13(typescript@5.7.3))
vue-router:
specifier: ^4.5.0
version: 4.5.0(vue@3.5.13(typescript@5.7.3))
vue-tsc:
specifier: ^2.1.10
version: 2.1.10(typescript@5.7.3)
@ -711,22 +714,26 @@ packages:
resolution: {integrity: sha512-bb8gZvoeKExCI2r/NVCK9E4YyOkvYGaSCPxVZe8T0jz8aX+dHEOZWxK06Z/Y9mWRkJfBiCH4aOhDF1yr1t5J8Q==}
engines: {node: '>= 16'}
'@intlify/message-compiler@11.0.0-rc.1':
resolution: {integrity: sha512-TGw2uBfuTFTegZf/BHtUQBEKxl7Q/dVGLoqRIdw8lFsp9g/53sYn5iD+0HxIzdYjbWL6BTJMXCPUHp9PxDTRPw==}
engines: {node: '>= 16'}
'@intlify/message-compiler@11.1.1':
resolution: {integrity: sha512-4iEsUZ3aF7jXY19CJFN5VP+pPyLITD9FVsjB13z9TU1UxaZLlFsmNhvRxlPDSOfHAP5RpNF2QKKdZ3DHVf4Yzw==}
engines: {node: '>= 16'}
'@intlify/shared@11.0.0-rc.1':
resolution: {integrity: sha512-8tR1xe7ZEbkabTuE/tNhzpolygUn9OaYp9yuYAF4MgDNZg06C3Qny80bes2/e9/Wm3aVkPUlCw6WgU7mQd0yEg==}
'@intlify/message-compiler@12.0.0-alpha.2':
resolution: {integrity: sha512-PD9C+oQbb7BF52hec0+vLnScaFkvnfX+R7zSbODYuRo/E2niAtGmHd0wPvEMsDhf9Z9b8f/qyDsVeZnD/ya9Ug==}
engines: {node: '>= 16'}
'@intlify/shared@11.1.1':
resolution: {integrity: sha512-2kGiWoXaeV8HZlhU/Nml12oTbhv7j2ufsJ5vQaa0VTjzUmZVdd/nmKFRAOJ/FtjO90Qba5AnZDwsrY7ZND5udA==}
engines: {node: '>= 16'}
'@intlify/shared@11.1.2':
resolution: {integrity: sha512-dF2iMMy8P9uKVHV/20LA1ulFLL+MKSbfMiixSmn6fpwqzvix38OIc7ebgnFbBqElvghZCW9ACtzKTGKsTGTWGA==}
engines: {node: '>= 16'}
'@intlify/shared@12.0.0-alpha.2':
resolution: {integrity: sha512-P2DULVX9nz3y8zKNqLw9Es1aAgQ1JGC+kgpx5q7yLmrnAKkPR5MybQWoEhxanefNJgUY5ehsgo+GKif59SrncA==}
engines: {node: '>= 16'}
'@intlify/unplugin-vue-i18n@6.0.3':
resolution: {integrity: sha512-9ZDjBlhUHtgjRl23TVcgfJttgu8cNepwVhWvOv3mUMRDAhjW0pur1mWKEUKr1I8PNwE4Gvv2IQ1xcl4RL0nG0g==}
engines: {node: '>= 18'}
@ -838,25 +845,21 @@ packages:
resolution: {integrity: sha512-eEwxY+0Cf76HnQwr1+Qy48qwf4dAebTHaKhzEgxLqLK6szbglnK6SThjY95YHrYWwsH1GujWiFoX51jwZNYfSw==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-arm64-musl@3.0.3':
resolution: {integrity: sha512-LdxbLv8qVkzro4/ZoP9MuytIL6NOVsbhoZ5Wl1KXOa/2DSxBiksrAPMSChCTyeLy6P3ebSHxQSb52ku18t1LBA==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@oxc-resolver/binding-linux-x64-gnu@3.0.3':
resolution: {integrity: sha512-bN8elR9AV/DZZPdcteOWWElkz8KyxLtOvmfVl7Dnehcs6f9e+fWYKyqiKvva1jsxG4znGKCPT1gfMhpYW8QuKg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@oxc-resolver/binding-linux-x64-musl@3.0.3':
resolution: {integrity: sha512-Zy1U49BjriwbAds2ho6CGjZIk2KVn0+lrc/G5bvhQg7UJYxEkAueMGBuA5rULIhx9xVtIPsT9Q+J5Xhb4ffVNw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@oxc-resolver/binding-wasm32-wasi@3.0.3':
resolution: {integrity: sha512-7rteQnn7i5f9nkFZs1VRdBqFhvOx3zWavyKkWjXYVxc9vsSLTg0moh2MRZw5dw5m/bEi1u/p3YAKJ9gdHyBhNQ==}
@ -902,42 +905,36 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.0':
resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.0':
resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.0':
resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.0':
resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.0':
resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.0':
resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==}
@ -1024,55 +1021,46 @@ packages:
resolution: {integrity: sha512-paHF1bMXKDuizaMODm2bBTjRiHxESWiIyIdMugKeLnjuS1TCS54MF5+Y5Dx8Ui/1RBPVRE09i5OUlaLnv8OGnA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.26.0':
resolution: {integrity: sha512-cwxiHZU1GAs+TMxvgPfUDtVZjdBdTsQwVnNlzRXC5QzIJ6nhfB4I1ahKoe9yPmoaA/Vhf7m9dB1chGPpDRdGXg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.26.0':
resolution: {integrity: sha512-4daeEUQutGRCW/9zEo8JtdAgtJ1q2g5oHaoQaZbMSKaIWKDQwQ3Yx0/3jJNmpzrsScIPtx/V+1AfibLisb3AMQ==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.26.0':
resolution: {integrity: sha512-eGkX7zzkNxvvS05ROzJ/cO/AKqNvR/7t1jA3VZDi2vRniLKwAWxUr85fH3NsvtxU5vnUUKFHKh8flIBdlo2b3Q==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.26.0':
resolution: {integrity: sha512-Odp/lgHbW/mAqw/pU21goo5ruWsytP7/HCC/liOt0zcGG0llYWKrd10k9Fj0pdj3prQ63N5yQLCLiE7HTX+MYw==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.26.0':
resolution: {integrity: sha512-MBR2ZhCTzUgVD0OJdTzNeF4+zsVogIR1U/FsyuFerwcqjZGvg2nYe24SAHp8O5sN8ZkRVbHwlYeHqcSQ8tcYew==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.26.0':
resolution: {integrity: sha512-YYcg8MkbN17fMbRMZuxwmxWqsmQufh3ZJFxFGoHjrE7bv0X+T6l3glcdzd7IKLiwhT+PZOJCblpnNlz1/C3kGQ==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.26.0':
resolution: {integrity: sha512-ZuwpfjCwjPkAOxpjAEjabg6LRSfL7cAJb6gSQGZYjGhadlzKKywDkCUnJ+KEfrNY1jH5EEoSIKLCb572jSiglA==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.26.0':
resolution: {integrity: sha512-+HJD2lFS86qkeF8kNu0kALtifMpPCZU80HvwztIKnYwym3KnA1os6nsX4BGSTLtS2QVAGG1P3guRgsYyMA0Yhg==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.26.0':
resolution: {integrity: sha512-WUQzVFWPSw2uJzX4j6YEbMAiLbs0BUysgysh8s817doAYhR5ybqTI1wtKARQKo6cGop3pHnrUJPFCsXdoFaimQ==}
@ -4016,8 +4004,8 @@ snapshots:
'@intlify/bundle-utils@10.0.0(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))':
dependencies:
'@intlify/message-compiler': 11.0.0-rc.1
'@intlify/shared': 11.0.0-rc.1
'@intlify/message-compiler': 12.0.0-alpha.2
'@intlify/shared': 12.0.0-alpha.2
acorn: 8.14.0
escodegen: 2.1.0
estree-walker: 2.0.2
@ -4033,26 +4021,28 @@ snapshots:
'@intlify/message-compiler': 11.1.1
'@intlify/shared': 11.1.1
'@intlify/message-compiler@11.0.0-rc.1':
dependencies:
'@intlify/shared': 11.0.0-rc.1
source-map-js: 1.2.1
'@intlify/message-compiler@11.1.1':
dependencies:
'@intlify/shared': 11.1.1
source-map-js: 1.2.1
'@intlify/shared@11.0.0-rc.1': {}
'@intlify/message-compiler@12.0.0-alpha.2':
dependencies:
'@intlify/shared': 12.0.0-alpha.2
source-map-js: 1.2.1
'@intlify/shared@11.1.1': {}
'@intlify/shared@11.1.2': {}
'@intlify/shared@12.0.0-alpha.2': {}
'@intlify/unplugin-vue-i18n@6.0.3(@vue/compiler-dom@3.5.13)(eslint@8.57.1)(rollup@4.26.0)(typescript@5.7.3)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@8.57.1)
'@intlify/bundle-utils': 10.0.0(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))
'@intlify/shared': 11.1.1
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.1)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@intlify/shared': 11.1.2
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.1.2)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))
'@rollup/pluginutils': 5.1.4(rollup@4.26.0)
'@typescript-eslint/scope-manager': 8.25.0
'@typescript-eslint/typescript-estree': 8.25.0(typescript@5.7.3)
@ -4074,11 +4064,11 @@ snapshots:
- supports-color
- typescript
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.1)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
'@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.1.2)(@vue/compiler-dom@3.5.13)(vue-i18n@11.1.1(vue@3.5.13(typescript@5.7.3)))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@babel/parser': 7.26.9
optionalDependencies:
'@intlify/shared': 11.1.1
'@intlify/shared': 11.1.2
'@vue/compiler-dom': 3.5.13
vue: 3.5.13(typescript@5.7.3)
vue-i18n: 11.1.1(vue@3.5.13(typescript@5.7.3))

2
src/common/event/common.ts

@ -1,4 +1,4 @@
const keys = ["progress"] as const
const keys = ["hot-update-ready"] as const
type AllKeys = (typeof keys)[number]

6
src/common/event/update/main.ts

@ -1,8 +1,8 @@
import { broadcast } from "main/utils"
import { AllKeys } from "../common"
function emitProgress(...argu) {
broadcast<AllKeys>("progress", ...argu)
function emitHotUpdateReady(...argu) {
broadcast<AllKeys>("hot-update-ready", ...argu)
}
export { emitProgress }
export { emitHotUpdateReady }

10
src/main/commands/UpdateCommand.ts

@ -0,0 +1,10 @@
import { inject } from "inversify"
import Updater from "main/modules/updater"
export default class BasicCommand {
constructor(@inject(Updater) private _Updater: Updater) {}
async triggerHotUpdate() {
await this._Updater.triggerHotUpdate()
}
}

4
src/main/commands/_ioc.ts

@ -1,10 +1,14 @@
import { Container, ContainerModule } from "inversify"
import BasicCommand from "./BasicCommand"
import TabsCommand from "./TabsCommand"
import UpdateCommand from "./UpdateCommand"
// TODO 考虑迁移,将所有命令都注册common/event中
const modules = new ContainerModule(bind => {
bind("BasicCommand").to(BasicCommand).inSingletonScope()
bind("TabsCommand").to(TabsCommand).inSingletonScope()
bind("UpdateCommand").to(UpdateCommand).inSingletonScope()
})
async function destroyAllCommand(ioc: Container) {

8
src/main/modules/commands/index.ts

@ -23,9 +23,9 @@ export default class Commands extends BaseClass {
const run = await this._IOC.getAsync<any>(splitClass[0])
if (run) {
const result: Promise<any> | any = run[splitClass[1]](...argus)
return result
return [true, result]
}
return null
return [false]
}
public async invoke(command, ...argus) {
@ -37,8 +37,8 @@ export default class Commands extends BaseClass {
ipcMain.addListener("command", async (event, key, command: string, ...argus) => {
// console.log(event.sender);
try {
const result = await this.handleCommand(command, ...argus)
if (result) {
const [isExist, result] = await this.handleCommand(command, ...argus)
if (isExist) {
if (isPromise(result)) {
result
.then((res: any) => {

62
src/main/modules/updater/hot/gen.ts

@ -1,62 +0,0 @@
import { spawn } from "node:child_process"
import fs from "node:fs"
import path from "node:path"
import os from "node:os"
import { app } from "electron"
function getUpdateScriptTemplate() {
return process.platform === "win32"
? `
@echo off
timeout /t 2
taskkill /IM "{{EXE_NAME}}" /F
xcopy /Y /E "{{UPDATE_DIR}}\\*" "{{APP_PATH}}"
start "" "{{EXE_PATH}}"
`
: `
#!/bin/bash
sleep 2
pkill -f "{{EXE_NAME}}"
cp -Rf "{{UPDATE_DIR}}/*" "{{APP_PATH}}/"
open "{{EXE_PATH}}"
`
}
function generateUpdateScript() {
const scriptContent = getUpdateScriptTemplate()
.replace(/{{APP_PATH}}/g, process.platform === "win32" ? "%APP_PATH%" : "$APP_PATH")
.replace(/{{UPDATE_DIR}}/g, process.platform === "win32" ? "%UPDATE_DIR%" : "$UPDATE_DIR")
.replace(/{{EXE_PATH}}/g, process.platform === "win32" ? "%EXE_PATH%" : "$EXE_PATH")
.replace(/{{EXE_NAME}}/g, process.platform === "win32" ? "%EXE_NAME%" : "$EXE_NAME")
const scriptPath = path.join(os.tmpdir(), `update.${process.platform === "win32" ? "bat" : "sh"}`)
fs.writeFileSync(scriptPath, scriptContent)
return scriptPath
}
app.on("will-quit", event => {
event.preventDefault()
// 假设已下载更新到临时目录
const updateTempDir = path.join(os.tmpdir(), "app-update")
const appPath = app.getAppPath()
const appExePath = process.execPath
// 生成动态脚本
const scriptPath = generateUpdateScript()
fs.chmodSync(scriptPath, 0o755)
// 执行脚本
const child = spawn(scriptPath, [], {
detached: true,
shell: true,
env: {
APP_PATH: appPath,
UPDATE_DIR: updateTempDir,
EXE_PATH: appExePath,
},
})
child.unref()
app.exit()
})

118
src/main/modules/updater/hot/index.ts

@ -0,0 +1,118 @@
import { spawn } from "node:child_process"
import fs from "node:fs"
import path from "node:path"
import os from "node:os"
import { app } from "electron"
import extract from "extract-zip"
import { emitHotUpdateReady } from "common/event/Update/main"
import _debug from "debug"
const debug = _debug("app:hot-updater")
function getUpdateScriptTemplate() {
return process.platform === "win32"
? `
@echo off
timeout /t 2
taskkill /IM "{{EXE_NAME}}" /F
xcopy /Y /E "{{UPDATE_DIR}}\\*" "{{APP_PATH}}"
start "" "{{EXE_PATH}}"
`
: `
#!/bin/bash
sleep 2
pkill -f "{{EXE_NAME}}"
cp -Rf "{{UPDATE_DIR}}/*" "{{APP_PATH}}/"
open "{{EXE_PATH}}"
`
}
function generateUpdateScript() {
const scriptContent = getUpdateScriptTemplate()
.replace(/{{APP_PATH}}/g, process.platform === "win32" ? "%APP_PATH%" : "$APP_PATH")
.replace(/{{UPDATE_DIR}}/g, process.platform === "win32" ? "%UPDATE_DIR%" : "$UPDATE_DIR")
.replace(/{{EXE_PATH}}/g, process.platform === "win32" ? "%EXE_PATH%" : "$EXE_PATH")
.replace(/{{EXE_NAME}}/g, process.platform === "win32" ? "%EXE_NAME%" : "$EXE_NAME")
const scriptPath = path.join(os.tmpdir(), `update.${process.platform === "win32" ? "bat" : "sh"}`)
fs.writeFileSync(scriptPath, scriptContent)
return scriptPath
}
// 标记是否需要热更新
let shouldPerformHotUpdate = false
let isReadyUpdate = false
// 更新临时目录路径
// 使用应用名称和随机字符串创建唯一的临时目录
const updateTempDirPath = path.join(os.tmpdir(), `${app.getName()}-update-${Math.random().toString(36).substring(2, 15)}`)
app.once("will-quit", event => {
if (!shouldPerformHotUpdate) return
event.preventDefault()
const appPath = app.getAppPath()
const appExePath = process.execPath
const exeName = path.basename(appExePath)
// 生成动态脚本
const scriptPath = generateUpdateScript()
fs.chmodSync(scriptPath, 0o755)
// 执行脚本
const child = spawn(scriptPath, [], {
detached: true,
shell: true,
env: {
APP_PATH: appPath,
UPDATE_DIR: updateTempDirPath,
EXE_PATH: appExePath,
EXE_NAME: exeName,
},
})
child.unref()
app.exit()
})
// 下载热更新包
export async function fetchHotUpdatePackage(updatePackageUrl: string = "https://example.com/updates/latest.zip") {
if (isReadyUpdate) return
// 清除临时目录
clearUpdateTempDir()
// 创建临时目录
if (!fs.existsSync(updateTempDirPath)) {
fs.mkdirSync(updateTempDirPath, { recursive: true })
}
// 下载文件的本地保存路径
const downloadPath = path.join(updateTempDirPath, "update.zip")
try {
// 使用 fetch 下载更新包
const response = await fetch(updatePackageUrl)
if (!response.ok) {
throw new Error(`下载失败: ${response.status} ${response.statusText}`)
}
// 将下载内容写入文件
const arrayBuffer = await response.arrayBuffer()
fs.writeFileSync(downloadPath, Buffer.from(arrayBuffer))
// 解压更新包
await extract(downloadPath, { dir: updateTempDirPath })
// 删除下载的zip文件
fs.unlinkSync(downloadPath)
isReadyUpdate = true
emitHotUpdateReady()
} catch (error) {
debug("热更新包下载失败:", error)
throw error
}
}
function clearUpdateTempDir() {
if (!fs.existsSync(updateTempDirPath)) return
fs.rmSync(updateTempDirPath, { recursive: true })
}
export function flagNeedUpdate() {
shouldPerformHotUpdate = true
}

21
src/main/modules/updater/index.ts

@ -5,6 +5,8 @@ import BaseClass from "main/base/base"
// import { Setting } from "../setting"
import _debug from "debug"
import EventEmitter from "events"
import { fetchHotUpdatePackage, flagNeedUpdate } from "./hot"
import Locales from "locales/main"
const debug = _debug("app:updater")
const { autoUpdater } = pkg
@ -13,10 +15,21 @@ const { autoUpdater } = pkg
export class Updater extends BaseClass {
public events = new EventEmitter()
private timer: ReturnType<typeof setInterval> | null = null
// autoReplace = false
async triggerHotUpdate(autoReplace = false) {
await fetchHotUpdatePackage()
flagNeedUpdate()
if (!autoReplace) {
dialog.showMessageBox({
title: Locales.t("update.ready.hot.title"),
message: Locales.t("update.ready.hot.desc", { version: app.getVersion() }),
})
} else {
app.quit()
}
}
constructor(
// @inject(Setting) private _Setting: Setting
) {
constructor() {
super()
// 配置自动更新
@ -72,7 +85,7 @@ export class Updater extends BaseClass {
destroy() {
// 清理工作
if(this.timer){
if (this.timer) {
clearInterval(this.timer)
this.timer = null
}

2
tsconfig.web.json

@ -6,6 +6,7 @@
"src/renderer/src/env.d.ts",
"src/renderer/src/**/*",
"src/renderer/src/**/*.vue",
"packages/locales/**/*.ts",
"src/preload/*.d.ts",
"src/types/**/*",
"config/**/*",
@ -13,6 +14,7 @@
"src/common/**/*"
],
"exclude": [
"packages/locales/main.ts",
"src/common/**/*.main.ts",
"src/common/**/main.ts"
],

Loading…
Cancel
Save