/** * TODO: 更新失败的回滚操作 */ const isAlphaVersion = Plugins.APP_VERSION.includes('Alpha') const RollingReleasePath = `data/rolling-release` /* 触发器 手动触发 */ const onRun = async () => { await Rolling() } /* 触发器 启动APP时 */ const onStartup = async () => { if (Plugin.AutoRollingMode === 'onStartup') { doCheck() } } /* 触发器 APP就绪后 */ const onReady = async () => { if (Plugin.AutoRollingMode === 'onReady') { doCheck() } addRollingReleaseTagToTitleBar() } const doCheck = async () => { await Plugins.sleep(3000) // 至少延迟3s,以便GUI完成对核心状态的恢复 const run = () => Rolling(false, Plugin.AutoRollingReload) const { running } = Plugins.useKernelApiStore() if (running) { run() } else { // 延迟检测,确保内核已经启动 setTimeout(run, (Plugin.AutoRollingDelay || 10) * 1000) } } /* * 右键菜单 - 滚动版本 * params: confirm 是否进行交互式确认 */ const Rolling = async (confirm = true, autoReload = false) => { await checkRollingReleaseEnabled() const isLatest = await checkLatestVersion() if (!isLatest) { Plugins.useAppStore().showAbout = true return } const GFC_URL = 'https://api.github.com/repos/GUI-for-Cores/GUI.for.Clash/releases/tags/rolling-release' const GFS_URL = 'https://api.github.com/repos/GUI-for-Cores/GUI.for.SingBox/releases/tags/rolling-release' const url = Plugins.APP_TITLE.includes('Clash') ? GFC_URL : GFS_URL const { destroy } = Plugins.message.info(`[${Plugin.name}] 检测中...`, 999999) const { body } = await Plugins.HttpGet(url, { Authorization: Plugins.getGitHubApiAuthorization() }) if (body.message) { destroy() if (body.status == '404') { throw '当前没有可用的滚动更新' } throw body.message } const ZipFile = `data/.cache/rolling-release.zip` const rollingReleaseAsset = body.assets.find((v) => v.name === `rolling-release.zip`) const ZipUrl = rollingReleaseAsset?.browser_download_url const VersionUrl = body.assets.find((v) => v.name === 'version.txt')?.browser_download_url if (!ZipUrl || !VersionUrl) { destroy() throw '出现一些错误,无法找到更新资源包' } let localVersion = '' let remoteVersion = '' try { const { body } = await Plugins.HttpGet(VersionUrl) remoteVersion = body const res = await fetch('/version.txt') localVersion = await res.text() } catch (err) {} if (!remoteVersion) { destroy() throw '无法获取远程版本信息' } if (localVersion === remoteVersion) { Plugins.message.success(`[${Plugin.name}] 当前版本已是最新`) destroy() return } destroy() confirm && (await Plugins.confirm('', await fetchChangeLog(), { type: 'markdown' })) const { update, destroy: destroy2, error } = Plugins.message.info('正在更新...') try { await Plugins.Download(ZipUrl, ZipFile, {}, (progress, total) => { update('正在更新...' + ((progress / total) * 100).toFixed(2) + '%') }) await Plugins.UnzipZIPFile(ZipFile, 'data') await Plugins.RemoveFile(ZipFile) await Plugins.WriteFile(`${RollingReleasePath}/updated_at.txt`, Plugins.formatDate(rollingReleaseAsset.updated_at, 'YYYY.MM.DD')) destroy2() const ok = autoReload || (await Plugins.confirm(Plugin.name, '更新成功,是否立即重载界面?').catch(() => 0)) ok && Plugins.WindowReloadApp() } catch (err) { error(err.message || err) } finally { Plugins.sleep(1500).then(() => destroy2()) } } /** * 右键菜单 - 恢复版本 */ const Recovery = async () => { await checkRollingReleaseEnabled() if (!(await Plugins.FileExists(RollingReleasePath))) { Plugins.message.info('无需恢复,此版本已是默认版本。') return } await Plugins.confirm( Plugin.name, '滚动发行过程中,相关配置文件会自动升级,因此回滚会造成配置文件不兼容,导致出现意外状况,确定要回滚吗?', { type: 'markdown' } ) await Plugins.RemoveFile(RollingReleasePath) await Plugins.confirm(Plugin.name, '回滚成功,即将重载界面!').catch(() => true) await Plugins.WindowReloadApp() } /** * 右键菜单 - 更新日志 */ const Changelog = async () => { Plugins.alert('', await fetchChangeLog(), { type: 'markdown' }) } /** * 右键菜单 - 版本统计 */ const Statistics = async () => { const clientMap = { 'GUI.for.Clash': 'https://api.github.com/repos/GUI-for-Cores/GUI.for.Clash/releases', 'GUI.for.SingBox': 'https://api.github.com/repos/GUI-for-Cores/GUI.for.SingBox/releases' } const url = clientMap[Plugins.APP_TITLE] const { body } = await Plugins.HttpGet(url, { Authorization: Plugins.getGitHubApiAuthorization() }) const { message } = body if (message) throw message const records = body .sort((a, b) => b.name.startsWith('v') - a.name.startsWith('rolling-release')) .map((r) => { const count = r.assets.reduce((p, c) => { if (c.name.includes('darwin')) { p.darwin = (p.darwin || 0) + c.download_count } else if (c.name.includes('windows')) { p.windows = (p.windows || 0) + c.download_count } else if (c.name.includes('linux')) { p.linux = (p.linux || 0) + c.download_count } else { p.other = (p.other || 0) + c.download_count } return p }, {}) const size = r.assets.reduce((p, c) => p + c.size, 0) const name = r.name + (r.name === Plugins.APP_VERSION ? '`我的版本`' : '') const download = count.other ?? `${windows_icon} ${count.windows} / ${darwin_icon} ${count.darwin} / ${linux_icon} ${count.linux}` const fileSize = Plugins.formatBytes(size / r.assets.filter((v) => v.name.startsWith('GUI') || v.name.startsWith('rolling-')).length) const uploader = [...new Set(r.assets.map((asset) => asset.uploader.login))].join('、') const createTime = Plugins.formatRelativeTime(r.assets[0].updated_at) return `| ${name} | ${download} | ${fileSize} | ${uploader} | ${createTime} |` }) const table = ['|版本名称|下载次数|文件大小|发布者|更新时间|', '|-|-|-|-|-|', records.join('\n')] await Plugins.alert('信息统计如下', table.join('\n'), { type: 'markdown'}) } const checkRollingReleaseEnabled = async () => { const appSettings = Plugins.useAppSettingsStore() if (!appSettings.app.rollingRelease) { throw '请在【设置】中,开启【启用滚动发行】功能。' } } const checkLatestVersion = async () => { if (isAlphaVersion) { throw 'v1.9.8-Alpha已转正,请至 设置 - 关于 更新到最新版本!' } const GFC_URL = `https://api.github.com/repos/GUI-for-Cores/GUI.for.Clash/releases/latest` const GFS_URL = `https://api.github.com/repos/GUI-for-Cores/GUI.for.SingBox/releases/latest` const url = Plugins.APP_TITLE.includes('Clash') ? GFC_URL : GFS_URL const { body } = await Plugins.HttpGet(url, { Authorization: Plugins.getGitHubApiAuthorization() }) const { tag_name, message } = body if (message) throw message return tag_name === Plugins.APP_VERSION } const fetchChangeLog = async () => { const { body } = await Plugins.HttpGet(`https://api.github.com/repos/GUI-for-Cores/${Plugins.APP_TITLE}/commits`, { Authorization: Plugins.getGitHubApiAuthorization() }) const releaseIndex = body.findIndex((v) => v.commit.message.startsWith('Release v')) let currentVersion try { currentVersion = await (await fetch('/version.txt')).text() } catch (error) { console.log(`[${Plugin.name}]`, '当前不是滚动发行版本') } const history = body.slice(0, releaseIndex).map((v) => ({ message: v.commit.message .replaceAll('\n\n', '\n - ') .replaceAll(/#(\d+)/g, `[#$1](https://github.com/GUI-for-Cores/${Plugins.APP_TITLE}/pull/$1 "#$1")`), time: Plugins.formatRelativeTime(v.commit.committer.date), isCurrent: v.sha.slice(0, 7) === currentVersion, html_url: v.html_url })) if (history.length === 0) { return `暂无滚动发行日志` } let tip = '' if (!currentVersion) { tip = '\n\n注意:你当前使用的不是滚动发行版本,请执行本插件以获取上述更新特性。' } const changelog = '## 滚动发行日志\n\n' + history.map((v) => ` - ${v.isCurrent ? '`你的版本`' : ''}${v.message} 【[${v.time}](${v.html_url} "${v.time}")】`).join('\n') + tip return changelog } const addRollingReleaseTagToTitleBar = async () => { let rollingReleaseVersion const res = await fetch('/version.txt') const txt = await res.text() if (txt && txt.length === 7) { rollingReleaseVersion = `Rolling Release (${txt})` } if (!rollingReleaseVersion) return const res2 = await fetch('/updated_at.txt') const txt2 = await res2.text() if (txt2) { rollingReleaseVersion = `Rolling Release (${txt2})` } const kernelApiStore = Plugins.useKernelApiStore() Plugins.useAppStore().addCustomActions('title_bar', ({ h, ref }) => { const loading = ref(false) const check = async () => { loading.value = true await Rolling().catch((err) => Plugins.message.error(err)) loading.value = false } return { component: 'div', componentSlots: { default: () => { return h( 'Button', { type: kernelApiStore.running ? 'link' : 'text', style: kernelApiStore.running ? { color: 'var(--primary-color)' } : {}, size: 'small', loading: loading.value, onClick: check }, () => rollingReleaseVersion ) } } } }) } const windows_icon = `` const darwin_icon = `` const linux_icon = ``