mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
feat(update): 实现更新通道管理系统与更新调试页面
本次提交完成以下变更: 1. 实现完整的更新通道管理功能,支持慢走/跑步两种更新模式,可在设置页切换且配置持久化生效 2. 新增更新调试页面,集成检查更新、版本比对、设置测试版本等调试工具 3. 新增快速打开Chromium DevTools的按钮,方便开发调试 4. 完善自动更新后端逻辑,新增版本比对、测试版本覆盖、通道动态获取等IPC接口 5. 调整GitHub Release构建脚本,修复prerelease版本的命名与发布问题 6. 添加semver依赖与类型定义,完善全量类型声明 7. 更新自动更新文档,补充更新通道相关的设计说明 8. 微调默认配置,添加主题暗黑模式的初始配置
This commit is contained in:
@@ -105,14 +105,22 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# BUILD ID = GitHub Run Number(严格递增,无分钟级冲突)
|
# BUILD ID = GitHub Run Number(严格递增,无分钟级冲突)
|
||||||
# base 自动读 package.json(版本单一事实源,消除本地/CI 版本双轨)
|
# base 自动读 package.json(版本单一事实源)
|
||||||
|
# 版本格式(electron-updater GitHub provider 的频道逻辑只认 alpha/beta 字符串标识):
|
||||||
|
# beta → {base}-beta.{buildId}(如 1.2.1-beta.13,频道 beta)
|
||||||
|
# run → {base}(稳定版,如 1.2.1)
|
||||||
|
# 数字标识(如 1.2.1-13)会被当作"自定义频道"导致更新无法识别,故弃用
|
||||||
- name: Generate BUILD ID & set version
|
- name: Generate BUILD ID & set version
|
||||||
id: version
|
id: version
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
$buildId = "$env:GITHUB_RUN_NUMBER"
|
$buildId = "$env:GITHUB_RUN_NUMBER"
|
||||||
$base = node scripts/version.js get
|
$base = node scripts/version.js get
|
||||||
$full = node scripts/version.js build ci $buildId
|
if ("${{ inputs.mode }}" -eq "beta") {
|
||||||
|
$full = node scripts/version.js build ci "beta.$buildId"
|
||||||
|
} else {
|
||||||
|
$full = node scripts/version.js build local
|
||||||
|
}
|
||||||
"build_id=$buildId" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
"build_id=$buildId" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||||
"base=$base" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
"base=$base" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||||
"full=$full" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
"full=$full" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||||
@@ -167,13 +175,20 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
$tag = "v${{ steps.version.outputs.full }}"
|
$tag = "v${{ steps.version.outputs.full }}"
|
||||||
$prerelease = if ("${{ inputs.mode }}" -eq "beta") { "--prerelease" } else { "" }
|
$prerelease = if ("${{ inputs.mode }}" -eq "beta") { "--prerelease" } else { "" }
|
||||||
# Release 标题命名(仅展示名;tag / 真实版本号仍是 {base}-{buildId},更新机制不变):
|
$extraAssets = @()
|
||||||
|
# beta 额外上传 latest-beta.yml:GitHub provider 频道取件(latest-beta)不再 404 回退
|
||||||
|
if ("${{ inputs.mode }}" -eq "beta") {
|
||||||
|
Copy-Item "dist-electron/latest.yml" "dist-electron/latest-beta.yml" -Force
|
||||||
|
$extraAssets += "dist-electron/latest-beta.yml"
|
||||||
|
}
|
||||||
|
# Release 标题命名(仅展示名;tag / 真实版本号仍是 {base}-beta.{id} 或 {base},更新机制不变):
|
||||||
# 正式/预览版 → "1.2.1";测试版 → "BETA 1.2.1"
|
# 正式/预览版 → "1.2.1";测试版 → "BETA 1.2.1"
|
||||||
$title = if ("${{ inputs.mode }}" -eq "beta") { "BETA ${{ steps.version.outputs.base }}" } else { "${{ steps.version.outputs.base }}" }
|
$title = if ("${{ inputs.mode }}" -eq "beta") { "BETA ${{ steps.version.outputs.base }}" } else { "${{ steps.version.outputs.base }}" }
|
||||||
gh release create $tag `
|
gh release create $tag `
|
||||||
"dist-electron/koring-launcher-${{ steps.version.outputs.full }}-setup.exe" `
|
"dist-electron/koring-launcher-${{ steps.version.outputs.full }}-setup.exe" `
|
||||||
"dist-electron/latest.yml" `
|
"dist-electron/latest.yml" `
|
||||||
"release-notes.md" `
|
"release-notes.md" `
|
||||||
|
@extraAssets `
|
||||||
--title $title `
|
--title $title `
|
||||||
--notes-file release-notes.md `
|
--notes-file release-notes.md `
|
||||||
$prerelease
|
$prerelease
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
oobe: false
|
oobe: false
|
||||||
|
theme:
|
||||||
|
darkMode: light
|
||||||
|
|||||||
+33
-14
@@ -296,10 +296,16 @@ src/
|
|||||||
- `sign`:是否使用 SignPath 签名
|
- `sign`:是否使用 SignPath 签名
|
||||||
- **无 `version` 输入**:base 自动读 `package.json`(版本单一事实源,消除本地/CI 版本双轨)
|
- **无 `version` 输入**:base 自动读 `package.json`(版本单一事实源,消除本地/CI 版本双轨)
|
||||||
|
|
||||||
**版本号与 BUILD ID(2026-08-30 起改为 GitHub Run Number)**:
|
**版本号与 BUILD ID(2026-08-30 起)**:
|
||||||
- BUILD ID = `github.run_number`(严格递增、无分钟级冲突)
|
- BUILD ID = `github.run_number`(严格递增、无分钟级冲突)
|
||||||
- 最终版本 = `{base}-{buildId}`(如 `1.2.0-12`),tag = `v{base}-{buildId}`,
|
- **beta**:`{base}-beta.{buildId}`(如 `1.2.1-beta.13`,tag `v1.2.1-beta.13`,GitHub 标记 prerelease)
|
||||||
electron-builder 产物 = `koring-launcher-{base}-{buildId}-setup.exe`,`latest.yml` 同步更新
|
- **run**:`{base}`(稳定版,如 `1.2.1`,tag `v1.2.1`)
|
||||||
|
- ⚠️ **为什么弃用数字标识**:electron-updater 的 GitHub provider 频道逻辑只认 `alpha`/`beta`
|
||||||
|
字符串频道;`1.2.1-13` 的数字 prerelease 会被当作"自定义频道"导致更新检查
|
||||||
|
`No published versions on GitHub`(无法识别当前/最新版本)。字母标识 `beta` 解决此问题。
|
||||||
|
- **兼容性**(semver 数字 < 字母):`1.2.1-beta.13 > 1.2.1-12 > 1.2.1-2608271921 > 1.2.1`,
|
||||||
|
所有旧数字版本(含时间 ID 与 Run Number 时代)都能平滑升级到新格式;beta 用户可被正式版(稳定)覆盖
|
||||||
|
- beta release 额外上传 `latest-beta.yml`(GitHub provider 频道取件用,避免 404 回退)
|
||||||
- 构建元数据(commit / buildId)由 `scripts/gen-build-info.js` 写入 `src/lib/buildInfo.ts` →
|
- 构建元数据(commit / buildId)由 `scripts/gen-build-info.js` 写入 `src/lib/buildInfo.ts` →
|
||||||
打包进渲染层,VersionCard / 关于页显示**构建来源 commit**(`scripts/version.js build ci` 负责统一设版本)
|
打包进渲染层,VersionCard / 关于页显示**构建来源 commit**(`scripts/version.js build ci` 负责统一设版本)
|
||||||
|
|
||||||
@@ -311,23 +317,27 @@ src/
|
|||||||
- **Release 标题命名(仅展示名,tag/真实版本号不变)**:run → `{base}`(如 `1.2.1`),
|
- **Release 标题命名(仅展示名,tag/真实版本号不变)**:run → `{base}`(如 `1.2.1`),
|
||||||
beta → `BETA {base}`(如 `BETA 1.2.1`)
|
beta → `BETA {base}`(如 `BETA 1.2.1`)
|
||||||
|
|
||||||
**⚠️ 版本语义注意(electron-updater)**:
|
**⚠️ 版本语义注意(electron-updater,2026-08-30 已修复并落地)**:
|
||||||
- `{base}-{buildId}` 属 semver prerelease;**electron-updater 默认 `allowPrerelease=false` 会直接跳过带
|
- **根因**:GitHub provider 用 Atom feed + 频道逻辑选版本,频道只认 `alpha`/`beta` 字符串标识。
|
||||||
prerelease 后缀的新版本**(GitHub provider 过滤 prerelease release + isUpdateAvailable 的
|
数字 prerelease(`1.2.1-4`)会被当作"自定义频道" → 永远选不出版本(`No published versions on GitHub`),
|
||||||
`semver.prerelease` 校验)——**必须**在 `electron/updater.ts` 设置 `autoUpdater.allowPrerelease = true`,
|
且 `allowPrerelease` 由当前版本自动决定(含 prerelease 即开启)。
|
||||||
否则如 v1.2.1-4 永远检测不到 v1.2.1-5(该问题已修复并落地)。
|
- **修复**:beta 用 `{base}-beta.{buildId}`(频道 `beta`,allowPrerelease 自动开启 → 正常识别);
|
||||||
- 开启后:同格式版本之间可正常升级(buildId 更大者胜,数值比较,如 `1.2.1-5 > 1.2.1-4`)
|
run 用稳定版 `{base}`(allowPrerelease=false → 走 releases/latest,正常识别)。
|
||||||
- 若未来发布**不带** buildId 的稳定版本(如 `1.2.0`),稳定版用户不会自动升级到带 buildId 的构建
|
- 同格式升级:`1.2.1-beta.14 > 1.2.1-beta.13`(数值比较)✓;`1.2.1 > 1.2.1-beta.x`(稳定版覆盖 beta)✓
|
||||||
- **迁移注意**:从时间 ID(`2608271921`)切换到 Run Number 后,旧格式数值更大(`2608271921 > 12`),
|
- **无需手动设置 allowPrerelease**:electron-updater 按当前版本自动判定(见 AppUpdater.js:218)。
|
||||||
老用户不会自动升级到新格式——切换时应同时提升 base(如 `1.3.0-12 > 1.2.0-2608271921`)
|
- 所有旧数字版本(`1.2.1-12` / `1.2.1-2608271921`)都小于 `1.2.1-beta.13`,平滑升级,无需提升 base
|
||||||
|
|
||||||
## 14. M2 主进程更新模块(2026-08-28,UI 待做)
|
## 14. M2 主进程更新模块(2026-08-28,UI 待做)
|
||||||
|
|
||||||
**新增/改动**:
|
**新增/改动**:
|
||||||
- `electron/updater.ts` — 更新服务:electron-updater(GitHub provider)优先,失败后加速源兜底;
|
- `electron/updater.ts` — 更新服务:electron-updater(GitHub provider)优先,失败后加速源兜底;
|
||||||
状态机 idle/checking/available/not-available/downloading/downloaded/error,进度事件,`quitAndInstall`
|
状态机 idle/checking/available/not-available/downloading/downloaded/error,进度事件,`quitAndInstall`
|
||||||
- `electron/handlers/update.ts` — IPC:`update:check` / `update:download` / `update:quitAndInstall` / `update:getState`,
|
- `electron/handlers/update.ts` — IPC:`update:check` / `update:download` / `update:pause` / `update:resume` /
|
||||||
|
`update:cancel` / `update:quitAndInstall` / `update:getState` / `update:getReleaseNotes` /
|
||||||
|
`update:getChannels` / `update:setChannel` / `update:setTestVersion` / `update:compareVersions`,
|
||||||
状态变化广播 `update:status` 到所有窗口
|
状态变化广播 `update:status` 到所有窗口
|
||||||
|
- 开发者工具:`window:openDevTools`(打开 Chromium DevTools)+ 更新功能测试页
|
||||||
|
(`debug-update`:设置测试版本号 / 检查更新 / 获取发布说明 / 版本识别列表 / 版本比对 / 下载)
|
||||||
- `electron/main.ts` — 注册 handler + 启动后 12s 延迟静默检查(开发模式自动跳过)
|
- `electron/main.ts` — 注册 handler + 启动后 12s 延迟静默检查(开发模式自动跳过)
|
||||||
- `electron/preload.ts` + `src/types/electron.d.ts` — 暴露更新 API(UI 未接,待 M3)
|
- `electron/preload.ts` + `src/types/electron.d.ts` — 暴露更新 API(UI 未接,待 M3)
|
||||||
|
|
||||||
@@ -342,7 +352,16 @@ src/
|
|||||||
idle → checking → available → downloading → downloaded → quitAndInstall()
|
idle → checking → available → downloading → downloaded → quitAndInstall()
|
||||||
└─not-available→ idle └─ error → idle(可重试)
|
└─not-available→ idle └─ error → idle(可重试)
|
||||||
```
|
```
|
||||||
`update:status` payload:`{ state, manual, version?, currentVersion?, percent?, transferred?, total?, bytesPerSecond?, source?, error? }`
|
`update:status` payload:`{ state, manual, version?, currentVersion?, percent?, transferred?, total?, bytesPerSecond?, source?, channel?, error? }`
|
||||||
|
|
||||||
|
## 16. 更新通道(woker / runner,2026-08-30)
|
||||||
|
|
||||||
|
- **可扩展通道注册表**(`electron/updater.ts` 的 `UPDATE_CHANNELS`):新增通道只需追加一项,
|
||||||
|
UI 通过 `update:getChannels` 动态渲染
|
||||||
|
- **woker(慢走模式,默认)**:`allowPrerelease=false` → 仅获取正式版(稳定)更新
|
||||||
|
- **runner(跑步模式)**:`allowPrerelease=true` → 可获取预览版(测试版)更新
|
||||||
|
- 通道持久化在 `Koring.yml` 的 `update.channel`;`update:setChannel` 即时切换,下次检查生效
|
||||||
|
- UI:设置 → 关于 → **更新设置**区块的「更新通道」**下拉框**(选项由 `update:getChannels` 动态渲染)
|
||||||
|
|
||||||
## 15. 更新日志独立页面(2026-08-28)
|
## 15. 更新日志独立页面(2026-08-28)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+3
-1
@@ -124,6 +124,8 @@ export interface UpdateConfig {
|
|||||||
total: number;
|
total: number;
|
||||||
/** 更新源:github / 加速源域名 */
|
/** 更新源:github / 加速源域名 */
|
||||||
source: string;
|
source: string;
|
||||||
|
/** 更新通道:woker(慢走,仅正式版)/ runner(跑步,含预览版) */
|
||||||
|
channel: string;
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +159,7 @@ const DEFAULTS: AppConfig = {
|
|||||||
download: { fileSource: 'mirror', versionSource: 'mirror', threads: 16, speedLimit: 0 },
|
download: { fileSource: 'mirror', versionSource: 'mirror', threads: 16, speedLimit: 0 },
|
||||||
network: { securityId: { enabled: false, authUrl: '' } },
|
network: { securityId: { enabled: false, authUrl: '' } },
|
||||||
ui: { showInstanceTitle: true, showTaskButton: true },
|
ui: { showInstanceTitle: true, showTaskButton: true },
|
||||||
update: { state: 'idle', version: '', percent: 0, transferred: 0, total: 0, source: 'github', error: '' },
|
update: { state: 'idle', version: '', percent: 0, transferred: 0, total: 0, source: 'github', channel: 'woker', error: '' },
|
||||||
instances: [],
|
instances: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -88,4 +88,43 @@ export function registerUpdateHandlers() {
|
|||||||
return { success: false, data: null, error: String(e) };
|
return { success: false, data: null, error: String(e) };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 更新通道定义列表(woker/runner…,UI 动态渲染)
|
||||||
|
ipcMain.handle('update:getChannels', () => {
|
||||||
|
try {
|
||||||
|
return { success: true, data: updateService.getChannels(), error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 切换更新通道(持久化并立即生效)
|
||||||
|
ipcMain.handle('update:setChannel', (_event, payload?: { channel?: string }) => {
|
||||||
|
try {
|
||||||
|
const state = updateService.setChannel(payload?.channel ?? '');
|
||||||
|
return { success: true, data: state, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开发者工具:设置测试版本号(覆盖当前识别版本)
|
||||||
|
ipcMain.handle('update:setTestVersion', (_event, payload?: { version?: string }) => {
|
||||||
|
try {
|
||||||
|
const state = updateService.setTestVersion(payload?.version ?? '');
|
||||||
|
return { success: true, data: state, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 开发者工具:版本比对(semver)
|
||||||
|
ipcMain.handle('update:compareVersions', (_event, payload?: { a?: string; b?: string }) => {
|
||||||
|
try {
|
||||||
|
const data = updateService.compareVersions(payload?.a ?? '', payload?.b ?? '');
|
||||||
|
return { success: true, data, error: null };
|
||||||
|
} catch (e: unknown) {
|
||||||
|
return { success: false, data: null, error: String(e) };
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ export function registerWindowHandlers(win: WinRef) {
|
|||||||
return (win.mainWindow as any)?.themeSource ?? null;
|
return (win.mainWindow as any)?.themeSource ?? null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 打开浏览器调试工具(DevTools,开发者工具测试用)
|
||||||
|
ipcMain.handle('window:openDevTools', () => {
|
||||||
|
win.mainWindow?.webContents.openDevTools({ mode: 'detach' });
|
||||||
|
return { success: true };
|
||||||
|
});
|
||||||
|
|
||||||
// Splash window management
|
// Splash window management
|
||||||
ipcMain.handle('window:openSplash', () => {
|
ipcMain.handle('window:openSplash', () => {
|
||||||
if (win.splashWindow && !win.splashWindow.isDestroyed()) {
|
if (win.splashWindow && !win.splashWindow.isDestroyed()) {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
maximize: () => ipcRenderer.invoke('window:maximize'),
|
maximize: () => ipcRenderer.invoke('window:maximize'),
|
||||||
close: () => ipcRenderer.invoke('window:close'),
|
close: () => ipcRenderer.invoke('window:close'),
|
||||||
isMaximized: () => ipcRenderer.invoke('window:isMaximized'),
|
isMaximized: () => ipcRenderer.invoke('window:isMaximized'),
|
||||||
|
openDevTools: () => ipcRenderer.invoke('window:openDevTools'),
|
||||||
onResized: (callback: () => void) => {
|
onResized: (callback: () => void) => {
|
||||||
const handler = () => callback();
|
const handler = () => callback();
|
||||||
ipcRenderer.on('window:resized', handler);
|
ipcRenderer.on('window:resized', handler);
|
||||||
@@ -104,6 +105,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
quitAndInstall: () => ipcRenderer.invoke('update:quitAndInstall'),
|
quitAndInstall: () => ipcRenderer.invoke('update:quitAndInstall'),
|
||||||
getUpdateState: () => ipcRenderer.invoke('update:getState'),
|
getUpdateState: () => ipcRenderer.invoke('update:getState'),
|
||||||
getReleaseNotes: (tag?: string) => ipcRenderer.invoke('update:getReleaseNotes', { tag }),
|
getReleaseNotes: (tag?: string) => ipcRenderer.invoke('update:getReleaseNotes', { tag }),
|
||||||
|
getUpdateChannels: () => ipcRenderer.invoke('update:getChannels'),
|
||||||
|
setUpdateChannel: (channel: string) => ipcRenderer.invoke('update:setChannel', { channel }),
|
||||||
|
setTestVersion: (version: string) => ipcRenderer.invoke('update:setTestVersion', { version }),
|
||||||
|
compareVersions: (a: string, b: string) => ipcRenderer.invoke('update:compareVersions', { a, b }),
|
||||||
onUpdateStatus: (callback: (data: unknown) => void) => {
|
onUpdateStatus: (callback: (data: unknown) => void) => {
|
||||||
const handler = (_event: Electron.IpcRendererEvent, data: unknown) => callback(data);
|
const handler = (_event: Electron.IpcRendererEvent, data: unknown) => callback(data);
|
||||||
ipcRenderer.on('update:status', handler);
|
ipcRenderer.on('update:status', handler);
|
||||||
|
|||||||
+119
-7
@@ -1,6 +1,7 @@
|
|||||||
import { autoUpdater, CancellationToken, type ProgressInfo } from 'electron-updater';
|
import { autoUpdater, CancellationToken, type ProgressInfo } from 'electron-updater';
|
||||||
import electron from 'electron';
|
import electron from 'electron';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
import semver from 'semver';
|
||||||
import { getConfig, updateConfig, flushConfig } from './config';
|
import { getConfig, updateConfig, flushConfig } from './config';
|
||||||
|
|
||||||
const { app } = electron;
|
const { app } = electron;
|
||||||
@@ -30,9 +31,38 @@ export interface UpdateStatusPayload {
|
|||||||
bytesPerSecond?: number;
|
bytesPerSecond?: number;
|
||||||
/** 当前使用的更新源(github=官方 / 加速源域名) */
|
/** 当前使用的更新源(github=官方 / 加速源域名) */
|
||||||
source?: string;
|
source?: string;
|
||||||
|
/** 当前更新通道(woker / runner) */
|
||||||
|
channel?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 更新通道 key(可扩展:新增通道只需在 UPDATE_CHANNELS 注册) */
|
||||||
|
export type UpdateChannelKey = 'woker' | 'runner';
|
||||||
|
|
||||||
|
export interface UpdateChannelDef {
|
||||||
|
key: UpdateChannelKey;
|
||||||
|
/** 显示名 */
|
||||||
|
label: string;
|
||||||
|
/** 说明 */
|
||||||
|
desc: string;
|
||||||
|
/** 是否接收预览版(runner 可收 beta;woker 只收正式版) */
|
||||||
|
allowPrerelease: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新通道注册表(后期扩展新通道:在此追加一项即可,UI 通过 update:getChannels 动态渲染)。
|
||||||
|
* - woker(慢走模式,默认):仅检查/获取正式版(稳定)更新
|
||||||
|
* - runner(跑步模式):可获取预览版(测试版)更新
|
||||||
|
*/
|
||||||
|
const UPDATE_CHANNELS: UpdateChannelDef[] = [
|
||||||
|
{ key: 'woker', label: '慢走模式', desc: '仅获取正式版更新(稳定)', allowPrerelease: false },
|
||||||
|
{ key: 'runner', label: '跑步模式', desc: '可获取预览版(测试版)更新', allowPrerelease: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getChannelDef(key: string): UpdateChannelDef {
|
||||||
|
return UPDATE_CHANNELS.find((c) => c.key === key) ?? UPDATE_CHANNELS[0];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReleaseNotesResult {
|
export interface ReleaseNotesResult {
|
||||||
/** release tag,如 v1.2.0-2608271921 */
|
/** release tag,如 v1.2.0-2608271921 */
|
||||||
tag: string;
|
tag: string;
|
||||||
@@ -108,6 +138,8 @@ class UpdateService {
|
|||||||
private suppressErrors = false;
|
private suppressErrors = false;
|
||||||
/** 当前下载的取消令牌(暂停/取消时 cancel) */
|
/** 当前下载的取消令牌(暂停/取消时 cancel) */
|
||||||
private downloadToken: CancellationToken | null = null;
|
private downloadToken: CancellationToken | null = null;
|
||||||
|
/** 当前更新通道(woker 慢走 / runner 跑步;从配置读取,可运行时切换) */
|
||||||
|
private channelKey: UpdateChannelKey = 'woker';
|
||||||
|
|
||||||
init(listener: (payload: UpdateStatusPayload) => void): void {
|
init(listener: (payload: UpdateStatusPayload) => void): void {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
@@ -119,6 +151,12 @@ class UpdateService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 恢复持久化的更新通道
|
||||||
|
const persistedChannel = getConfig().update?.channel;
|
||||||
|
if (persistedChannel && UPDATE_CHANNELS.some((c) => c.key === persistedChannel)) {
|
||||||
|
this.channelKey = persistedChannel as UpdateChannelKey;
|
||||||
|
}
|
||||||
|
|
||||||
// 应用能启动即说明上次安装已完成/已结束,清理持久化的进行中状态
|
// 应用能启动即说明上次安装已完成/已结束,清理持久化的进行中状态
|
||||||
const persisted = getConfig().update;
|
const persisted = getConfig().update;
|
||||||
if (persisted && persisted.state && persisted.state !== 'idle') {
|
if (persisted && persisted.state && persisted.state !== 'idle') {
|
||||||
@@ -128,11 +166,7 @@ class UpdateService {
|
|||||||
|
|
||||||
autoUpdater.autoDownload = false;
|
autoUpdater.autoDownload = false;
|
||||||
autoUpdater.autoInstallOnAppQuit = true;
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
// 项目版本方案为 {base}-{buildId}(如 1.2.1-5),属 semver prerelease(带 - 后缀)。
|
this.applyChannel();
|
||||||
// electron-updater 默认 allowPrerelease=false:GitHub provider 会过滤 prerelease release,
|
|
||||||
// isUpdateAvailable 也会拒绝带 prerelease 后缀的新版本 → v1.2.1-4 永远检测不到 v1.2.1-5。
|
|
||||||
// 必须显式开启(本项目的每个发布版本都是 prerelease 形式,不存在误升级稳定版的问题)。
|
|
||||||
autoUpdater.allowPrerelease = true;
|
|
||||||
autoUpdater.logger = console;
|
autoUpdater.logger = console;
|
||||||
autoUpdater.on('checking-for-update', () => {
|
autoUpdater.on('checking-for-update', () => {
|
||||||
this.state = 'checking';
|
this.state = 'checking';
|
||||||
@@ -189,6 +223,7 @@ class UpdateService {
|
|||||||
total: this.progress?.total,
|
total: this.progress?.total,
|
||||||
bytesPerSecond: this.progress?.bytesPerSecond,
|
bytesPerSecond: this.progress?.bytesPerSecond,
|
||||||
source: this.source,
|
source: this.source,
|
||||||
|
channel: this.channelKey,
|
||||||
error: this.error,
|
error: this.error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -204,6 +239,7 @@ class UpdateService {
|
|||||||
transferred: payload.transferred ?? 0,
|
transferred: payload.transferred ?? 0,
|
||||||
total: payload.total ?? 0,
|
total: payload.total ?? 0,
|
||||||
source: payload.source ?? 'github',
|
source: payload.source ?? 'github',
|
||||||
|
channel: this.channelKey,
|
||||||
error: payload.error ?? '',
|
error: payload.error ?? '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -215,17 +251,92 @@ class UpdateService {
|
|||||||
private persistIdleConfig(): void {
|
private persistIdleConfig(): void {
|
||||||
try {
|
try {
|
||||||
updateConfig({
|
updateConfig({
|
||||||
update: { state: 'idle', version: '', percent: 0, transferred: 0, total: 0, source: 'github', error: '' },
|
update: { state: 'idle', version: '', percent: 0, transferred: 0, total: 0, source: 'github', channel: this.channelKey, error: '' },
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按当前通道应用 electron-updater 的 allowPrerelease(woker=只收正式版 / runner=可收预览版) */
|
||||||
|
private applyChannel(): void {
|
||||||
|
const def = getChannelDef(this.channelKey);
|
||||||
|
// runner(跑步) 强制开启 allowPrerelease → GitHub provider 走 Atom feed 频道逻辑可收 beta;
|
||||||
|
// woker(慢走) 关闭 → 走 releases/latest 只认稳定版,不被预览版污染。
|
||||||
|
autoUpdater.allowPrerelease = def.allowPrerelease;
|
||||||
|
console.log(`[updater] 更新通道: ${def.label}(${def.key},allowPrerelease=${def.allowPrerelease})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 通道定义列表(UI 动态渲染;可扩展) */
|
||||||
|
getChannels(): UpdateChannelDef[] {
|
||||||
|
return UPDATE_CHANNELS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换更新通道(校验 + 持久化 + 立即生效,下次检查生效) */
|
||||||
|
setChannel(key: string): UpdateStatusPayload {
|
||||||
|
if (!UPDATE_CHANNELS.some((c) => c.key === key)) {
|
||||||
|
console.warn(`[updater] 未知更新通道: ${key}`);
|
||||||
|
return this.buildPayload();
|
||||||
|
}
|
||||||
|
if (this.channelKey === key) return this.buildPayload();
|
||||||
|
this.channelKey = key as UpdateChannelKey;
|
||||||
|
this.applyChannel();
|
||||||
|
try {
|
||||||
|
updateConfig({ update: { channel: key } });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[updater] 通道写入配置失败:', e);
|
||||||
|
}
|
||||||
|
this.emit();
|
||||||
|
return this.buildPayload();
|
||||||
|
}
|
||||||
|
|
||||||
getState(): UpdateStatusPayload {
|
getState(): UpdateStatusPayload {
|
||||||
return this.buildPayload();
|
return this.buildPayload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试用:覆盖当前识别到的版本号(影响 update:getState 与后续更新检查的比对)。
|
||||||
|
* 传入非法版本时忽略并返回当前状态。
|
||||||
|
*/
|
||||||
|
setTestVersion(version: string): UpdateStatusPayload {
|
||||||
|
const v = semver.valid(version.trim());
|
||||||
|
if (!v) {
|
||||||
|
console.warn(`[updater] 无效测试版本号: ${version}`);
|
||||||
|
return this.buildPayload();
|
||||||
|
}
|
||||||
|
this.currentVersion = v;
|
||||||
|
try {
|
||||||
|
// currentVersion 在类型声明中为 readonly,但运行时可直接赋值(测试工具用)
|
||||||
|
(autoUpdater as unknown as { currentVersion: unknown }).currentVersion = semver.parse(v);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[updater] 设置 autoUpdater.currentVersion 失败:', e);
|
||||||
|
}
|
||||||
|
console.log(`[updater] 测试版本号 → ${v}`);
|
||||||
|
this.emit();
|
||||||
|
return this.buildPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 版本比对(semver 规则,支持 v 前缀与 prerelease) */
|
||||||
|
compareVersions(a: string, b: string): { a: string; b: string; result: string; detail: string } {
|
||||||
|
const va = semver.valid(a.trim());
|
||||||
|
const vb = semver.valid(b.trim());
|
||||||
|
if (!va || !vb) {
|
||||||
|
return {
|
||||||
|
a: a.trim(),
|
||||||
|
b: b.trim(),
|
||||||
|
result: 'invalid',
|
||||||
|
detail: `无效版本:${!va ? `「${a.trim()}」` : ''}${!va && !vb ? ' / ' : ''}${!vb ? `「${b.trim()}」` : ''}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const c = semver.compare(va, vb);
|
||||||
|
return {
|
||||||
|
a: va,
|
||||||
|
b: vb,
|
||||||
|
result: c > 0 ? 'a>b' : c < 0 ? 'a<b' : 'a==b',
|
||||||
|
detail: `${va} ${c > 0 ? '>' : c < 0 ? '<' : '=='} ${vb}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查更新:GitHub 官方优先,失败后依次尝试加速源。
|
* 检查更新:GitHub 官方优先,失败后依次尝试加速源。
|
||||||
*/
|
*/
|
||||||
@@ -268,7 +379,8 @@ class UpdateService {
|
|||||||
this.suppressErrors = false;
|
this.suppressErrors = false;
|
||||||
// 镜像若反馈无更新(可能发现的是旧 tag / latest.yml 不匹配),
|
// 镜像若反馈无更新(可能发现的是旧 tag / latest.yml 不匹配),
|
||||||
// 不要就此返回 not-available,继续尝试下一个源
|
// 不要就此返回 not-available,继续尝试下一个源
|
||||||
if (this.state !== 'not-available') return this.buildPayload();
|
const mirrorResult = this.buildPayload();
|
||||||
|
if (mirrorResult.state !== 'not-available') return mirrorResult;
|
||||||
console.warn(`[updater] ${mirror} 反馈无可用更新,尝试下一个源`);
|
console.warn(`[updater] ${mirror} 反馈无可用更新,尝试下一个源`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`[updater] 加速源 ${mirror} 检查失败: ${String((err as Error)?.message ?? err)}`);
|
console.warn(`[updater] 加速源 ${mirror} 检查失败: ${String((err as Error)?.message ?? err)}`);
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"rehype-raw": "^7.0.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
|
"semver": "^7.8.5",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"three": "^0.184.0",
|
"three": "^0.184.0",
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@types/semver": "^7.8.0",
|
||||||
"@vitejs/plugin-react": "^4.6.0",
|
"@vitejs/plugin-react": "^4.6.0",
|
||||||
"concurrently": "^9.1.0",
|
"concurrently": "^9.1.0",
|
||||||
"electron": "^33.0.0",
|
"electron": "^33.0.0",
|
||||||
|
|||||||
Generated
+24
-6
@@ -92,6 +92,9 @@ importers:
|
|||||||
remark-gfm:
|
remark-gfm:
|
||||||
specifier: ^4.0.1
|
specifier: ^4.0.1
|
||||||
version: 4.0.1
|
version: 4.0.1
|
||||||
|
semver:
|
||||||
|
specifier: ^7.8.5
|
||||||
|
version: 7.8.5
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.7
|
specifier: ^2.0.7
|
||||||
version: 2.0.8(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 2.0.8(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
@@ -120,6 +123,9 @@ importers:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^19.1.6
|
specifier: ^19.1.6
|
||||||
version: 19.2.5(@types/react@19.2.17)
|
version: 19.2.5(@types/react@19.2.17)
|
||||||
|
'@types/semver':
|
||||||
|
specifier: ^7.8.0
|
||||||
|
version: 7.8.0
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^4.6.0
|
specifier: ^4.6.0
|
||||||
version: 4.7.0(vite@7.3.5(@types/node@26.0.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))
|
version: 4.7.0(vite@7.3.5(@types/node@26.0.1)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4))
|
||||||
@@ -1359,6 +1365,9 @@ packages:
|
|||||||
'@types/responselike@1.0.3':
|
'@types/responselike@1.0.3':
|
||||||
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
|
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
|
||||||
|
|
||||||
|
'@types/semver@7.8.0':
|
||||||
|
resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==}
|
||||||
|
|
||||||
'@types/stats.js@0.17.4':
|
'@types/stats.js@0.17.4':
|
||||||
resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
|
resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
|
||||||
|
|
||||||
@@ -3723,6 +3732,11 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
semver@7.8.5:
|
||||||
|
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
send@1.2.1:
|
send@1.2.1:
|
||||||
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
|
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
@@ -4613,7 +4627,7 @@ snapshots:
|
|||||||
node-gyp: 9.4.1
|
node-gyp: 9.4.1
|
||||||
ora: 5.4.1
|
ora: 5.4.1
|
||||||
read-binary-file-arch: 1.0.6
|
read-binary-file-arch: 1.0.6
|
||||||
semver: 7.8.4
|
semver: 7.8.5
|
||||||
tar: 6.2.1
|
tar: 6.2.1
|
||||||
yargs: 17.7.3
|
yargs: 17.7.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -4985,7 +4999,7 @@ snapshots:
|
|||||||
'@npmcli/fs@2.1.2':
|
'@npmcli/fs@2.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@gar/promisify': 1.1.3
|
'@gar/promisify': 1.1.3
|
||||||
semver: 7.8.4
|
semver: 7.8.5
|
||||||
|
|
||||||
'@npmcli/move-file@2.0.1':
|
'@npmcli/move-file@2.0.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5425,6 +5439,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 26.0.1
|
'@types/node': 26.0.1
|
||||||
|
|
||||||
|
'@types/semver@7.8.0': {}
|
||||||
|
|
||||||
'@types/stats.js@0.17.4': {}
|
'@types/stats.js@0.17.4': {}
|
||||||
|
|
||||||
'@types/three@0.184.1':
|
'@types/three@0.184.1':
|
||||||
@@ -5601,7 +5617,7 @@ snapshots:
|
|||||||
minimatch: 10.2.5
|
minimatch: 10.2.5
|
||||||
resedit: 1.7.2
|
resedit: 1.7.2
|
||||||
sanitize-filename: 1.6.4
|
sanitize-filename: 1.6.4
|
||||||
semver: 7.8.4
|
semver: 7.8.5
|
||||||
tar: 6.2.1
|
tar: 6.2.1
|
||||||
temp-file: 3.4.0
|
temp-file: 3.4.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -7697,14 +7713,14 @@ snapshots:
|
|||||||
|
|
||||||
node-abi@3.92.0:
|
node-abi@3.92.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.8.4
|
semver: 7.8.5
|
||||||
|
|
||||||
node-addon-api@1.7.2:
|
node-addon-api@1.7.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
node-api-version@0.2.1:
|
node-api-version@0.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.8.4
|
semver: 7.8.5
|
||||||
|
|
||||||
node-domexception@1.0.0: {}
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
@@ -7724,7 +7740,7 @@ snapshots:
|
|||||||
nopt: 6.0.0
|
nopt: 6.0.0
|
||||||
npmlog: 6.0.2
|
npmlog: 6.0.2
|
||||||
rimraf: 3.0.2
|
rimraf: 3.0.2
|
||||||
semver: 7.8.4
|
semver: 7.8.5
|
||||||
tar: 6.2.1
|
tar: 6.2.1
|
||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -8267,6 +8283,8 @@ snapshots:
|
|||||||
|
|
||||||
semver@7.8.4: {}
|
semver@7.8.4: {}
|
||||||
|
|
||||||
|
semver@7.8.5: {}
|
||||||
|
|
||||||
send@1.2.1(supports-color@8.1.1):
|
send@1.2.1(supports-color@8.1.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@8.1.1)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { Debug } from "./pages/debug";
|
|||||||
import { SplashDebug } from "./pages/debug/splash-debug";
|
import { SplashDebug } from "./pages/debug/splash-debug";
|
||||||
import { DisplayDebug } from "./pages/debug/display-debug";
|
import { DisplayDebug } from "./pages/debug/display-debug";
|
||||||
import { VersionCardDebug } from "./pages/debug/version-card-debug";
|
import { VersionCardDebug } from "./pages/debug/version-card-debug";
|
||||||
|
import { UpdateDebug } from "./pages/debug/update-debug";
|
||||||
import { TaskDebug } from "./pages/debug/task-debug";
|
import { TaskDebug } from "./pages/debug/task-debug";
|
||||||
import { CrashDebug } from "./pages/debug/crash-debug";
|
import { CrashDebug } from "./pages/debug/crash-debug";
|
||||||
import { Oobe } from "./pages/oobe";
|
import { Oobe } from "./pages/oobe";
|
||||||
@@ -59,6 +60,7 @@ const pageMap = {
|
|||||||
"debug-splash": SplashDebug,
|
"debug-splash": SplashDebug,
|
||||||
"debug-display": DisplayDebug,
|
"debug-display": DisplayDebug,
|
||||||
"debug-version-card": VersionCardDebug,
|
"debug-version-card": VersionCardDebug,
|
||||||
|
"debug-update": UpdateDebug,
|
||||||
"debug-task": TaskDebug,
|
"debug-task": TaskDebug,
|
||||||
"debug-crash": CrashDebug,
|
"debug-crash": CrashDebug,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export interface UpdateConfig {
|
|||||||
transferred: number;
|
transferred: number;
|
||||||
total: number;
|
total: number;
|
||||||
source: string;
|
source: string;
|
||||||
|
channel: string;
|
||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ export type UpdateState =
|
|||||||
| "installing"
|
| "installing"
|
||||||
| "error";
|
| "error";
|
||||||
|
|
||||||
|
/** 更新通道定义(主进程注册表,可扩展) */
|
||||||
|
export interface UpdateChannelDef {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
desc: string;
|
||||||
|
allowPrerelease: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** 主进程广播的更新状态 payload(update:status) */
|
/** 主进程广播的更新状态 payload(update:status) */
|
||||||
export interface UpdateStatusPayload {
|
export interface UpdateStatusPayload {
|
||||||
state: UpdateState;
|
state: UpdateState;
|
||||||
@@ -27,6 +35,8 @@ export interface UpdateStatusPayload {
|
|||||||
bytesPerSecond?: number;
|
bytesPerSecond?: number;
|
||||||
/** 当前使用的更新源(github / 加速源域名) */
|
/** 当前使用的更新源(github / 加速源域名) */
|
||||||
source?: string;
|
source?: string;
|
||||||
|
/** 当前更新通道(woker / runner) */
|
||||||
|
channel?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,3 +96,27 @@ export const getReleaseNotes = (tag?: string): Promise<ReleaseNotesResult | null
|
|||||||
/** 订阅更新状态变化 */
|
/** 订阅更新状态变化 */
|
||||||
export const onUpdateStatus = (cb: (status: UpdateStatusPayload) => void): (() => void) =>
|
export const onUpdateStatus = (cb: (status: UpdateStatusPayload) => void): (() => void) =>
|
||||||
onIpcEvent<UpdateStatusPayload>("update:status", cb);
|
onIpcEvent<UpdateStatusPayload>("update:status", cb);
|
||||||
|
|
||||||
|
/** 获取更新通道定义列表(UI 动态渲染,可扩展) */
|
||||||
|
export const getUpdateChannels = (): Promise<UpdateChannelDef[]> =>
|
||||||
|
ipcInvoke<UpdateChannelDef[]>("update:getChannels");
|
||||||
|
|
||||||
|
/** 切换更新通道(woker 慢走 / runner 跑步;持久化并立即生效) */
|
||||||
|
export const setUpdateChannel = (channel: string): Promise<UpdateStatusPayload> =>
|
||||||
|
ipcInvoke<UpdateStatusPayload>("update:setChannel", { channel });
|
||||||
|
|
||||||
|
/** 开发者工具:设置测试版本号(覆盖当前识别版本) */
|
||||||
|
export const setTestVersion = (version: string): Promise<UpdateStatusPayload> =>
|
||||||
|
ipcInvoke<UpdateStatusPayload>("update:setTestVersion", { version });
|
||||||
|
|
||||||
|
/** 开发者工具:版本比对结果 */
|
||||||
|
export interface VersionCompareResult {
|
||||||
|
a: string;
|
||||||
|
b: string;
|
||||||
|
result: "a>b" | "a<b" | "a==b" | "invalid";
|
||||||
|
detail: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开发者工具:版本比对(semver) */
|
||||||
|
export const compareVersions = (a: string, b: string): Promise<VersionCompareResult> =>
|
||||||
|
ipcInvoke<VersionCompareResult>("update:compareVersions", { a, b });
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { useRouteStore } from "@/stores/routeStore";
|
import { useRouteStore } from "@/stores/routeStore";
|
||||||
import { Monitor, Paintbrush, CreditCard, ListTodo, ChevronRight, FlaskConical, Rocket, AlertTriangle } from "lucide-react";
|
import { Monitor, Paintbrush, CreditCard, ListTodo, ChevronRight, FlaskConical, Rocket, AlertTriangle, RefreshCw, SquareTerminal } from "lucide-react";
|
||||||
|
|
||||||
const debugPages = [
|
const debugPages = [
|
||||||
|
{
|
||||||
|
key: "debug-update" as const,
|
||||||
|
icon: RefreshCw,
|
||||||
|
title: "更新功能测试",
|
||||||
|
desc: "设置测试版本号、检查更新、获取发布说明、版本比对与下载",
|
||||||
|
color: "text-emerald-500",
|
||||||
|
bg: "bg-emerald-500/10",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "debug-crash" as const,
|
key: "debug-crash" as const,
|
||||||
icon: AlertTriangle,
|
icon: AlertTriangle,
|
||||||
@@ -67,6 +75,23 @@ export function Debug() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 打开浏览器调试工具(DevTools) */}
|
||||||
|
<button
|
||||||
|
onClick={() => window.electronAPI?.openDevTools()}
|
||||||
|
className="glass-card w-full px-5 py-4 text-left hover:scale-[1.01] active:scale-[0.99] transition-transform cursor-pointer group mb-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="p-2.5 rounded-xl bg-foreground/[0.08]">
|
||||||
|
<SquareTerminal className="w-5 h-5 text-foreground/60" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-foreground">打开开发者工具</p>
|
||||||
|
<p className="text-[13px] text-muted-foreground mt-0.5">打开浏览器调试工具(Chromium DevTools),查看控制台与网络请求</p>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="w-4 h-4 text-foreground/20 group-hover:text-foreground/40 transition-colors shrink-0" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{debugPages.map((p) => (
|
{debugPages.map((p) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { GlassCard, PageHeader } from "./components";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
cancelUpdate,
|
||||||
|
checkForUpdates,
|
||||||
|
compareVersions,
|
||||||
|
downloadUpdate,
|
||||||
|
getReleaseNotes,
|
||||||
|
getUpdateState,
|
||||||
|
onUpdateStatus,
|
||||||
|
pauseUpdate,
|
||||||
|
quitAndInstall,
|
||||||
|
resumeUpdate,
|
||||||
|
setTestVersion,
|
||||||
|
type ReleaseNotesResult,
|
||||||
|
type UpdateStatusPayload,
|
||||||
|
type VersionCompareResult,
|
||||||
|
} from "@/api/update";
|
||||||
|
|
||||||
|
/** 更新功能测试:版本识别 / 检查 / 介绍 / 比对 / 下载 */
|
||||||
|
export function UpdateDebug() {
|
||||||
|
const [status, setStatus] = useState<UpdateStatusPayload | null>(null);
|
||||||
|
const [testVersion, setTestVersionInput] = useState("");
|
||||||
|
const [versionMsg, setVersionMsg] = useState("");
|
||||||
|
const [notesTag, setNotesTag] = useState("");
|
||||||
|
const [notes, setNotes] = useState<ReleaseNotesResult | null>(null);
|
||||||
|
const [notesMsg, setNotesMsg] = useState("");
|
||||||
|
const [cmpA, setCmpA] = useState("");
|
||||||
|
const [cmpB, setCmpB] = useState("");
|
||||||
|
const [cmpResult, setCmpResult] = useState<VersionCompareResult | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = onUpdateStatus(setStatus);
|
||||||
|
getUpdateState().then(setStatus).catch(() => {});
|
||||||
|
return unsub;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const s = status;
|
||||||
|
const pct = s?.percent ?? 0;
|
||||||
|
|
||||||
|
const run = async (fn: () => Promise<unknown>, setMsg: (m: string) => void) => {
|
||||||
|
try {
|
||||||
|
setMsg("执行中...");
|
||||||
|
const r = await fn();
|
||||||
|
setMsg(JSON.stringify(r));
|
||||||
|
} catch (e) {
|
||||||
|
setMsg(`失败:${e instanceof Error ? e.message : String(e)}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-2xl mx-auto p-8 space-y-6">
|
||||||
|
<PageHeader title="更新功能测试" desc="测试更新检查、版本识别、发布说明、版本比对与下载" />
|
||||||
|
|
||||||
|
{/* 1. 版本识别列表 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
|
||||||
|
版本识别列表
|
||||||
|
</h3>
|
||||||
|
<GlassCard>
|
||||||
|
<div className="space-y-1.5 text-[13px] font-mono">
|
||||||
|
<p className="flex justify-between"><span className="text-muted-foreground">当前版本</span><span>{s?.currentVersion ?? "-"}</span></p>
|
||||||
|
<p className="flex justify-between"><span className="text-muted-foreground">目标/最新版本</span><span>{s?.version ?? "-"}</span></p>
|
||||||
|
<p className="flex justify-between"><span className="text-muted-foreground">更新状态</span><span>{s?.state ?? "-"}</span></p>
|
||||||
|
<p className="flex justify-between"><span className="text-muted-foreground">更新通道</span><span>{s?.channel ?? "-"}</span></p>
|
||||||
|
<p className="flex justify-between"><span className="text-muted-foreground">更新源</span><span className="max-w-[60%] truncate">{s?.source ?? "-"}</span></p>
|
||||||
|
{s?.error && <p className="flex justify-between text-red-500"><span className="text-muted-foreground">错误</span><span className="max-w-[60%] truncate">{s.error}</span></p>}
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2. 设置版本号 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
|
||||||
|
设置测试版本号
|
||||||
|
</h3>
|
||||||
|
<GlassCard>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={testVersion}
|
||||||
|
onChange={(e) => setTestVersionInput(e.target.value)}
|
||||||
|
placeholder="如 1.2.1-beta.13"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button size="sm" onClick={() => run(async () => {
|
||||||
|
const r = await setTestVersion(testVersion.trim());
|
||||||
|
setStatus(r);
|
||||||
|
return r;
|
||||||
|
}, setVersionMsg)}>
|
||||||
|
设置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{versionMsg && <p className="mt-2 text-[12px] text-muted-foreground font-mono break-all">{versionMsg}</p>}
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 3. 检查更新 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
|
||||||
|
检查更新
|
||||||
|
</h3>
|
||||||
|
<GlassCard>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button size="sm" onClick={() => run(async () => {
|
||||||
|
const r = await checkForUpdates(true);
|
||||||
|
setStatus(r);
|
||||||
|
return r;
|
||||||
|
}, setVersionMsg)}>
|
||||||
|
检查更新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 4. 获取更新介绍 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
|
||||||
|
获取更新介绍
|
||||||
|
</h3>
|
||||||
|
<GlassCard>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
value={notesTag}
|
||||||
|
onChange={(e) => setNotesTag(e.target.value)}
|
||||||
|
placeholder="留空=当前版本;或填 tag 如 v1.2.1-beta.13"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button size="sm" onClick={() => run(async () => {
|
||||||
|
const r = await getReleaseNotes(notesTag.trim() || undefined);
|
||||||
|
setNotes(r);
|
||||||
|
return r ? { tag: r.tag, source: r.source, version: r.version, len: r.notes.length } : null;
|
||||||
|
}, setNotesMsg)}>
|
||||||
|
获取
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{notes && (
|
||||||
|
<div className="mt-3 text-[12px] text-muted-foreground font-mono break-all">
|
||||||
|
<p>tag: {notes.tag} · 来源: {notes.source} · 长度: {notes.notes.length}</p>
|
||||||
|
<div className="mt-1 max-h-40 overflow-y-auto border border-border/40 rounded-lg p-2 bg-foreground/[0.03] whitespace-pre-wrap">
|
||||||
|
{notes.notes.slice(0, 600)}{notes.notes.length > 600 ? "…" : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{notesMsg && <p className="mt-2 text-[12px] text-muted-foreground font-mono break-all">{notesMsg}</p>}
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 5. 版本比对 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
|
||||||
|
版本比对
|
||||||
|
</h3>
|
||||||
|
<GlassCard>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input value={cmpA} onChange={(e) => setCmpA(e.target.value)} placeholder="版本 A" className="flex-1" />
|
||||||
|
<span className="self-center text-muted-foreground text-[13px]">vs</span>
|
||||||
|
<Input value={cmpB} onChange={(e) => setCmpB(e.target.value)} placeholder="版本 B" className="flex-1" />
|
||||||
|
<Button size="sm" onClick={() => run(async () => {
|
||||||
|
const r = await compareVersions(cmpA.trim(), cmpB.trim());
|
||||||
|
setCmpResult(r);
|
||||||
|
return r;
|
||||||
|
}, setNotesMsg)}>
|
||||||
|
比对
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{cmpResult && (
|
||||||
|
<p className="mt-2 text-[13px] font-mono">
|
||||||
|
<span className={cmpResult.result === "a>b" ? "text-emerald-500" : cmpResult.result === "a<b" ? "text-red-500" : "text-foreground"}>
|
||||||
|
{cmpResult.detail}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 6. 下载版本 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
|
||||||
|
下载版本
|
||||||
|
</h3>
|
||||||
|
<GlassCard>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Button size="sm" onClick={() => run(downloadUpdate, setNotesMsg)}>下载</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => run(pauseUpdate, setNotesMsg)}>暂停</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => run(resumeUpdate, setNotesMsg)}>继续</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => run(cancelUpdate, setNotesMsg)}>取消</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={() => run(quitAndInstall, setNotesMsg)}>安装</Button>
|
||||||
|
</div>
|
||||||
|
{(s?.state === "downloading" || s?.state === "paused") && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<div className="h-1.5 w-full rounded-full bg-foreground/10 overflow-hidden">
|
||||||
|
<div className="h-full bg-blue-500 transition-all" style={{ width: `${pct}%` }} />
|
||||||
|
</div>
|
||||||
|
<p className="mt-1.5 text-[12px] text-muted-foreground font-mono">
|
||||||
|
{pct.toFixed(1)}% · {s.state}
|
||||||
|
{s.transferred != null && s.total ? ` · ${(s.transferred / 1024 / 1024).toFixed(1)} / ${(s.total / 1024 / 1024).toFixed(1)} MB` : ""}
|
||||||
|
{s.bytesPerSecond ? ` · ${(s.bytesPerSecond / 1024 / 1024).toFixed(1)} MB/s` : ""}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</GlassCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[12px] text-muted-foreground/50 text-center">
|
||||||
|
仅用于开发调试;下载/安装会真实执行,请谨慎操作
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,10 +2,18 @@ import { useState, useEffect } from "react";
|
|||||||
import { VersionCard } from "@/components/VersionCard";
|
import { VersionCard } from "@/components/VersionCard";
|
||||||
import { BUILD_MODE } from "@/lib/mode";
|
import { BUILD_MODE } from "@/lib/mode";
|
||||||
import { BUILD_COMMIT, BUILD_ID } from "@/lib/buildInfo";
|
import { BUILD_COMMIT, BUILD_ID } from "@/lib/buildInfo";
|
||||||
import { ExternalLink, GitFork, RotateCcw } from "lucide-react";
|
import { ExternalLink, GitFork, RotateCcw, ChevronDown } from "lucide-react";
|
||||||
import { Link } from "@heroui/react";
|
import { Link, Select, ListBox, ListBoxItem } from "@heroui/react";
|
||||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
getUpdateChannels,
|
||||||
|
getUpdateState,
|
||||||
|
onUpdateStatus,
|
||||||
|
setUpdateChannel,
|
||||||
|
type UpdateChannelDef,
|
||||||
|
} from "@/api/update";
|
||||||
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
@@ -27,11 +35,50 @@ const modeLabels: Record<string, string> = {
|
|||||||
const GITHUB_URL = "https://github.com/lingke-net/koring-launcher";
|
const GITHUB_URL = "https://github.com/lingke-net/koring-launcher";
|
||||||
const OFFICIAL_URL = "https://koring.space";
|
const OFFICIAL_URL = "https://koring.space";
|
||||||
|
|
||||||
|
/** 兜底通道列表:主进程 update:getChannels 不可用时使用,保证下拉框始终有选项 */
|
||||||
|
const FALLBACK_CHANNELS: UpdateChannelDef[] = [
|
||||||
|
{ key: "woker", label: "慢走模式", desc: "仅获取正式版更新(稳定)", allowPrerelease: false },
|
||||||
|
{ key: "runner", label: "跑步模式", desc: "可获取预览版(测试版)更新", allowPrerelease: true },
|
||||||
|
];
|
||||||
|
|
||||||
export function AboutSetting() {
|
export function AboutSetting() {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [countdown, setCountdown] = useState(5);
|
const [countdown, setCountdown] = useState(5);
|
||||||
const canConfirm = countdown <= 0;
|
const canConfirm = countdown <= 0;
|
||||||
|
|
||||||
|
// 更新通道(下拉框,选项来自主进程通道注册表)
|
||||||
|
const [channels, setChannels] = useState<UpdateChannelDef[]>([]);
|
||||||
|
const [activeChannel, setActiveChannel] = useState("woker");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getUpdateChannels()
|
||||||
|
.then((list) => setChannels(list.length ? list : FALLBACK_CHANNELS))
|
||||||
|
.catch((e) => {
|
||||||
|
console.error("[update] 获取更新通道失败,使用内置列表:", e);
|
||||||
|
setChannels(FALLBACK_CHANNELS);
|
||||||
|
});
|
||||||
|
const unsub = onUpdateStatus((s) => {
|
||||||
|
if (s.channel) setActiveChannel(s.channel);
|
||||||
|
});
|
||||||
|
getUpdateState()
|
||||||
|
.then((s) => {
|
||||||
|
if (s.channel) setActiveChannel(s.channel);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
return unsub;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChannelChange = async (key: unknown) => {
|
||||||
|
if (typeof key !== "string" || !key || key === activeChannel) return;
|
||||||
|
try {
|
||||||
|
await setUpdateChannel(key);
|
||||||
|
setActiveChannel(key);
|
||||||
|
toast.success("更新通道已切换,下次检查更新生效");
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof Error ? e.message : String(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
setCountdown(5);
|
setCountdown(5);
|
||||||
@@ -87,6 +134,48 @@ export function AboutSetting() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<SectionTitle>更新设置</SectionTitle>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<SettingCard>
|
||||||
|
<SettingRow label="更新通道" desc="慢走模式仅获取正式版;跑步模式可获取预览版">
|
||||||
|
<Select.Root
|
||||||
|
selectedKey={activeChannel}
|
||||||
|
onSelectionChange={handleChannelChange}
|
||||||
|
aria-label="更新通道"
|
||||||
|
className="w-48"
|
||||||
|
>
|
||||||
|
<Select.Trigger className="h-8 rounded-lg border border-border/40 dark:border-white/[0.08] bg-white/60 dark:bg-black/30 px-3 hover:border-primary/30 transition-colors">
|
||||||
|
<Select.Value className="text-[13px] text-foreground">
|
||||||
|
{channels.find((c) => c.key === activeChannel)?.label ?? "慢走模式"}
|
||||||
|
</Select.Value>
|
||||||
|
<Select.Indicator>
|
||||||
|
<ChevronDown className="w-3.5 h-3.5 text-muted-foreground" />
|
||||||
|
</Select.Indicator>
|
||||||
|
</Select.Trigger>
|
||||||
|
<Select.Popover className="z-50 rounded-xl border border-border/50 dark:border-white/[0.08] bg-background shadow-xl p-1.5 w-52">
|
||||||
|
<ListBox className="outline-none">
|
||||||
|
{channels.map((c) => (
|
||||||
|
<ListBoxItem
|
||||||
|
key={c.key}
|
||||||
|
id={c.key}
|
||||||
|
textValue={c.label}
|
||||||
|
className="text-[13px] py-1.5 px-2.5 rounded-lg data-[selected=true]:bg-primary/10 data-[selected=true]:text-primary outline-none cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span>{c.label}</span>
|
||||||
|
<span className="text-[12px] text-muted-foreground">{c.desc}</span>
|
||||||
|
</div>
|
||||||
|
</ListBoxItem>
|
||||||
|
))}
|
||||||
|
</ListBox>
|
||||||
|
</Select.Popover>
|
||||||
|
</Select.Root>
|
||||||
|
</SettingRow>
|
||||||
|
</SettingCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>相关链接</SectionTitle>
|
<SectionTitle>相关链接</SectionTitle>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const DEFAULT_CONFIG: AppConfig = {
|
|||||||
download: { fileSource: "mirror", versionSource: "mirror", threads: 16, speedLimit: 0 },
|
download: { fileSource: "mirror", versionSource: "mirror", threads: 16, speedLimit: 0 },
|
||||||
network: { securityId: { enabled: false, authUrl: "" } },
|
network: { securityId: { enabled: false, authUrl: "" } },
|
||||||
ui: { showInstanceTitle: true, showTaskButton: true },
|
ui: { showInstanceTitle: true, showTaskButton: true },
|
||||||
update: { state: "idle", version: "", percent: 0, transferred: 0, total: 0, source: "github", error: "" },
|
update: { state: "idle", version: "", percent: 0, transferred: 0, total: 0, source: "github", channel: "woker", error: "" },
|
||||||
instances: [],
|
instances: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ interface KoringAuthState {
|
|||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useKoringAuthStore = create<KoringAuthState>((set) => ({
|
export const useKoringAuthStore = create<KoringAuthState>((set, get) => ({
|
||||||
user: null,
|
user: null,
|
||||||
authData: null,
|
authData: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type RouteKey =
|
|||||||
| "debug-splash"
|
| "debug-splash"
|
||||||
| "debug-display"
|
| "debug-display"
|
||||||
| "debug-version-card"
|
| "debug-version-card"
|
||||||
|
| "debug-update"
|
||||||
| "debug-task"
|
| "debug-task"
|
||||||
| "debug-crash";
|
| "debug-crash";
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ export const allRoutes: RouteItem[] = [
|
|||||||
{ key: "debug-splash", label: "启动动画调试", path: "/debug/splash", hidden: true },
|
{ key: "debug-splash", label: "启动动画调试", path: "/debug/splash", hidden: true },
|
||||||
{ key: "debug-display", label: "显示效果调试", path: "/debug/display", hidden: true },
|
{ key: "debug-display", label: "显示效果调试", path: "/debug/display", hidden: true },
|
||||||
{ key: "debug-version-card", label: "版本卡片调试", path: "/debug/version-card", hidden: true },
|
{ key: "debug-version-card", label: "版本卡片调试", path: "/debug/version-card", hidden: true },
|
||||||
|
{ key: "debug-update", label: "更新功能测试", path: "/debug/update", hidden: true },
|
||||||
{ key: "debug-task", label: "任务队列调试", path: "/debug/task", hidden: true },
|
{ key: "debug-task", label: "任务队列调试", path: "/debug/task", hidden: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Vendored
+5
@@ -7,6 +7,7 @@ interface ElectronAPI {
|
|||||||
maximize: () => Promise<void>;
|
maximize: () => Promise<void>;
|
||||||
close: () => Promise<void>;
|
close: () => Promise<void>;
|
||||||
isMaximized: () => Promise<boolean>;
|
isMaximized: () => Promise<boolean>;
|
||||||
|
openDevTools: () => Promise<unknown>;
|
||||||
onResized: (callback: () => void) => () => void;
|
onResized: (callback: () => void) => () => void;
|
||||||
|
|
||||||
getTheme: () => Promise<'light' | 'dark' | 'system' | null>;
|
getTheme: () => Promise<'light' | 'dark' | 'system' | null>;
|
||||||
@@ -33,6 +34,10 @@ interface ElectronAPI {
|
|||||||
quitAndInstall: () => Promise<unknown>;
|
quitAndInstall: () => Promise<unknown>;
|
||||||
getUpdateState: () => Promise<unknown>;
|
getUpdateState: () => Promise<unknown>;
|
||||||
getReleaseNotes: (tag?: string) => Promise<unknown>;
|
getReleaseNotes: (tag?: string) => Promise<unknown>;
|
||||||
|
getUpdateChannels: () => Promise<unknown>;
|
||||||
|
setUpdateChannel: (channel: string) => Promise<unknown>;
|
||||||
|
setTestVersion: (version: string) => Promise<unknown>;
|
||||||
|
compareVersions: (a: string, b: string) => Promise<unknown>;
|
||||||
onUpdateStatus: (callback: (data: unknown) => void) => () => void;
|
onUpdateStatus: (callback: (data: unknown) => void) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user