docs/archives/118-desktop-auto-update-system/implementation.md
构建一个专业、跨平台、用户体验优先的桌面应用更新系统。系统应为非侵入式,将完整的控制权交给用户,同时确保更新流程的稳定性和数据的安全性。
目标: 自动化构建支持自动更新的安装包和供高级用户使用的便携包,并将其发布到 GitHub Releases。
packages/desktop/package.json.github/workflows/release.ymlpackage.json)electron-updater 到 dependencies。build 节点下,添加 publish 配置,指向项目的 GitHub 仓库(提供 owner 和 repo)。win.target: 设置为 ['nsis', 'zip'],同时生成 Windows 安装包和便携包。mac.target: 设置为 ['dmg', 'zip'],同时生成 macOS 安装包和便携包。linux.target: 设置为 ['AppImage', 'zip'],同时生成 Linux 安装包和便携包。release.yml)build-windows, build-macos, build-linux 这三个 job 中,修改 actions/upload-artifact 步骤,确保上传所有生成的文件(如 *.exe, *.dmg, *.AppImage, *.zip, *.yml),而不仅仅是 .zip。create-release job 中,修改 softprops/action-gh-release 的 files 参数,使用通配符(如 artifacts/**/*)将所有下载的 artifact 文件附加到 GitHub Release 中。目标: 编写健壮的主进程逻辑,作为整个交互式更新流程的后端引擎。
packages/desktop/main.jscheckUpdate 异步函数PreferenceService 异步读取 updater.allowPrerelease 和 updater.ignoredVersion 的值。autoUpdater.allowPrerelease。autoUpdater.autoDownload = false,将下载控制权交给用户。update-available 事件:
if (info.version === ignoredVersion) return;。如果发现的版本是用户忽略过的,则提前终止流程。package.json 中的 publish 配置和 info.version,动态构建出指向 GitHub Release 页面的 releaseUrl。update-available-info) 将包含版本信息和 releaseUrl 的对象发送给 UI 层。start-download-update: 调用 autoUpdater.downloadUpdate(),开始下载更新。install-update: 调用 autoUpdater.quitAndInstall(),安装更新并重启应用。ignore-update: 接收版本号参数,将其保存到 PreferenceService 的 updater.ignoredVersion 中。open-external-link: 接收 URL 参数,使用 shell.openExternal() 在用户的默认浏览器中打开链接。目标: 设计一个简洁、直观的用户界面,让用户能够轻松控制更新流程。
packages/ui/src/composables/useUpdater.tspackages/ui/src/components/UpdaterIcon.vuepackages/ui/src/components/UpdaterModal.vueuseUpdater ComposablehasUpdate, updateInfo, downloadProgress, isDownloading, isDownloaded, allowPrerelease 等响应式状态。checkUpdate, startDownload, installUpdate, ignoreUpdate, togglePrerelease 等方法。update-available-info, update-download-progress, update-downloaded 事件,并更新相应的状态。UpdaterIcon 组件isRunningInElectron() 进行环境检测。hasUpdate 状态显示更新提示(如小红点)。UpdaterModal 组件。UpdaterModal 组件目标: 确保更新功能仅在桌面环境中可见,对 Web 和 Extension 环境完全透明。
使用 @prompt-optimizer/core 包中的 isRunningInElectron() 函数进行环境检测:
import { isRunningInElectron } from '@prompt-optimizer/core'
// 仅在 Electron 环境中显示更新组件
<div v-if="isRunningInElectron()">
<UpdaterIcon />
</div>
UpdaterIcon 组件内部进行环境检测,非 Electron 环境直接返回空。useUpdater 中提供空实现,保持 API 一致性。App.vue 中条件性地包含更新组件。在 open-external-link IPC 处理器中,验证 URL 的协议,仅允许 http:// 和 https:// 链接:
if (!url.startsWith('http://') && !url.startsWith('https://')) {
throw new Error('Only HTTP and HTTPS URLs are allowed');
}
对接收到的版本号进行格式验证,防止恶意输入:
const versionRegex = /^v?\d+\.\d+\.\d+(-[\w.-]+)?(\+[\w.-]+)?$/;
if (!versionRegex.test(version)) {
throw new Error('Invalid version format');
}
使用配置文件管理敏感信息,避免硬编码:
const { buildReleaseUrl, validateVersion } = require('./config/update-config');
本技术方案实现了一个完整、安全、用户友好的桌面应用自动更新系统。通过多形态产品兼容性设计,确保了更新功能仅在需要的环境中可见。通过完善的错误处理和状态管理,保证了系统的稳定性和可靠性。
function createDetailedErrorResponse(error) {
const timestamp = new Date().toISOString();
let detailedMessage = `[${timestamp}] Error Details:\n\n`;
if (error instanceof Error) {
detailedMessage += `Message: ${error.message}\n`;
if (error.code) detailedMessage += `Code: ${error.code}\n`;
if (error.statusCode) detailedMessage += `HTTP Status: ${error.statusCode}\n`;
if (error.url) detailedMessage += `URL: ${error.url}\n`;
if (error.stack) detailedMessage += `\nStack Trace:\n${error.stack}\n`;
// 捕获其他属性和JSON兜底机制
const jsonError = JSON.stringify(error, Object.getOwnPropertyNames(error), 2);
if (jsonError && jsonError !== '{}') {
detailedMessage += `\nComplete Object Dump:\n${jsonError}`;
}
}
return { success: false, error: detailedMessage };
}
// 修复前:丢失详细信息
if (!result.success) {
throw new Error(result.error);
}
// 修复后:保留完整信息
if (!result.success) {
const error = new Error(result.error);
error.originalError = result.error;
error.detailedMessage = result.error;
throw error;
}
<!-- UpdaterModal.vue - 智能组件 -->
<script setup lang="ts">
// 内部管理所有更新逻辑
const {
state,
checkUpdate,
startDownload,
installUpdate,
ignoreUpdate,
togglePrerelease,
openReleaseUrl
} = useUpdater()
// 简化的接口
interface Props {
modelValue: boolean
}
const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>()
</script>
<!-- UpdaterIcon.vue - 简化组件 -->
<script setup lang="ts">
// 只获取状态用于图标显示
const { state } = useUpdater()
// 只管理模态框显示
const showModal = ref(false)
</script>
<template>
<!-- 极简调用 -->
<UpdaterModal v-model="showModal" />
</template>
// 开发模式下的更新检查配置
if (process.env.NODE_ENV === 'development' || !app.isPackaged) {
const fs = require('fs');
const devConfigPath = path.join(__dirname, 'dev-app-update.yml');
if (fs.existsSync(devConfigPath)) {
autoUpdater.forceDevUpdateConfig = true;
} else {
// 返回友好的开发环境提示
responseData.message = 'Development environment: Update checking is disabled';
return createSuccessResponse(responseData);
}
}
interface UpdaterState {
lastCheckResult: 'none' | 'available' | 'not-available' | 'error' | 'dev-disabled'
// ... 其他状态
}
if (checkData.hasUpdate && checkData.checkResult?.updateInfo) {
state.lastCheckResult = 'available'
} else if (checkData.remoteVersion && !checkData.hasUpdate) {
state.lastCheckResult = 'not-available'
} else if (checkData.message?.includes('Development environment')) {
state.lastCheckResult = 'dev-disabled'
} else {
state.lastCheckResult = 'error'
}
<template #footer>
<!-- 开发环境:只显示关闭按钮 -->
<div v-if="state.lastCheckResult === 'dev-disabled'">
<button @click="$emit('update:modelValue', false)">关闭</button>
</div>
<!-- 默认状态:关闭 + 立即检查 -->
<div v-else-if="!state.hasUpdate && !state.isCheckingUpdate">
<button @click="$emit('update:modelValue', false)">关闭</button>
<button @click="handleCheckUpdate">立即检查</button>
</div>
<!-- 有更新:多个操作按钮 -->
<div v-else-if="state.hasUpdate">
<button @click="handleStartDownload">下载更新</button>
</div>
</template>
关键特性: