diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0dcab3a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,210 @@ +name: Release extension + +on: + workflow_dispatch: + inputs: + version: + description: "版本号,例如 2.0.1.2 或 2.0.1.2-rc.1" + required: true + type: string + +permissions: + contents: write + +concurrency: + group: release-extension + cancel-in-progress: false + +jobs: + release: + runs-on: windows-latest + defaults: + run: + shell: pwsh + env: + RELEASE_VERSION: ${{ inputs.version }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: Check out source + uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} + fetch-depth: 0 + + - name: Generate manifest and create tag + run: | + $ErrorActionPreference = 'Stop' + $version = $env:RELEASE_VERSION + $tag = "v$version" + + if ($version -notmatch '^\d+\.\d+\.\d+\.\d+(-rc\.\d+)?$') { + throw "无效的版本号:$version" + } + if ($env:GITHUB_REF -ne "refs/heads/$env:DEFAULT_BRANCH") { + throw '请从默认分支运行发布工作流' + } + + $existingTag = git ls-remote --tags origin "refs/tags/$tag" + if ($LASTEXITCODE -ne 0) { throw '检查远端标签失败' } + if ($existingTag) { throw "标签已存在:$tag" } + + $sourceArchive = Join-Path $env:RUNNER_TEMP 'source.zip' + $source = Join-Path $env:RUNNER_TEMP 'source-for-manifest' + + git -c core.autocrlf=false archive --format=zip ` + "--output=$sourceArchive" HEAD + if ($LASTEXITCODE -ne 0) { throw '导出源码失败' } + + Expand-Archive -LiteralPath $sourceArchive ` + -DestinationPath $source + + $extension = Get-Content ` + (Join-Path $source 'extension.js') -Raw + $match = [regex]::Match( + $extension, + 'shuYingLocalVersion\s*=\s*"([^"]+)"' + ) + if (!$match.Success -or $match.Groups[1].Value -ne $version) { + throw "extension.js 的版本号必须为 $version" + } + + python (Join-Path $source 'tools/generate_manifest.py') ` + --root $source ` + --version $version ` + --out dist/manifest.json + if ($LASTEXITCODE -ne 0) { throw '生成清单失败' } + + $manifestPath = Join-Path $source 'dist/manifest.json' + $manifest = Get-Content $manifestPath -Raw | + ConvertFrom-Json + $workflowFiles = @( + $manifest.files.PSObject.Properties.Name | + Where-Object { $_ -like '.gitea/*' -or $_ -like '.github/*' } + ) + if ($workflowFiles.Count -gt 0) { + throw '清单包含工作流文件,请先修改 EXCLUDE_DIRS' + } + + Copy-Item -LiteralPath $manifestPath ` + -Destination (Join-Path $env:GITHUB_WORKSPACE ` + 'dist/manifest.json') -Force + git config user.name 'github-actions[bot]' + git config user.email ` + '41898282+github-actions[bot]@users.noreply.github.com' + git add dist/manifest.json + if ($LASTEXITCODE -ne 0) { throw '暂存清单失败' } + git commit -m "Prepare manifest for $tag" + if ($LASTEXITCODE -ne 0) { throw '提交清单失败' } + git tag -a $tag -m "Release $tag" + if ($LASTEXITCODE -ne 0) { throw '创建标签失败' } + git push origin "refs/tags/$tag" + if ($LASTEXITCODE -ne 0) { throw '推送标签失败' } + + Write-Host "已创建包含清单的标签:$tag" + + - name: Build ZIP from tag + run: | + $ErrorActionPreference = 'Stop' + $version = $env:RELEASE_VERSION + $tag = "v$version" + $snapshot = Join-Path $env:RUNNER_TEMP 'tag-snapshot.zip' + $stage = Join-Path $env:RUNNER_TEMP 'release-source' + $package = Join-Path $env:RUNNER_TEMP "noname-shuying-$tag.zip" + + git -c core.autocrlf=false archive --format=zip ` + "--output=$snapshot" $tag + if ($LASTEXITCODE -ne 0) { throw '导出标签文件失败' } + Expand-Archive -LiteralPath $snapshot ` + -DestinationPath $stage + + $manifestPath = Join-Path $stage 'dist/manifest.json' + $manifest = Get-Content $manifestPath -Raw | + ConvertFrom-Json + if ($manifest.version -ne $version) { + throw "清单版本号必须为 $version" + } + + @' + import json + import sys + import zipfile + from pathlib import Path + + root = Path(sys.argv[1]) + output = Path(sys.argv[2]) + manifest = json.loads( + (root / "dist/manifest.json").read_text(encoding="utf-8") + ) + folder = "\u672f\u6a31\u5305/" + with zipfile.ZipFile( + output, "w", zipfile.ZIP_DEFLATED + ) as archive: + for name in manifest["files"]: + archive.write(root / name, folder + name) + archive.write( + root / "dist/manifest.json", + folder + "manifest.json" + ) + '@ | python - $stage $package + if ($LASTEXITCODE -ne 0) { throw '打包失败' } + + Write-Host "版本:$version" + Write-Host "文件数:$($manifest.file_count)" + Write-Host "安装包:$package" + + - name: Publish GitHub Release + run: | + $ErrorActionPreference = 'Stop' + if (!$env:GITHUB_TOKEN) { + throw '工作流没有获得 GITHUB_TOKEN' + } + + $tag = "v$env:RELEASE_VERSION" + $stage = Join-Path $env:RUNNER_TEMP 'release-source' + $manifestPath = Join-Path $stage 'dist/manifest.json' + $package = Join-Path $env:RUNNER_TEMP ` + "noname-shuying-$tag.zip" + $api = "https://api.github.com/repos/$env:GITHUB_REPOSITORY/releases" + $headers = @{ + Authorization = "Bearer $env:GITHUB_TOKEN" + Accept = 'application/vnd.github+json' + 'X-GitHub-Api-Version' = '2022-11-28' + } + + $body = @{ + tag_name = $tag + name = $tag + draft = $true + prerelease = $tag.Contains('-rc.') + } | ConvertTo-Json -Compress + + $release = Invoke-RestMethod -Method Post ` + -Uri $api ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body $body + + foreach ($file in @($manifestPath, $package)) { + $name = [uri]::EscapeDataString([IO.Path]::GetFileName($file)) + $uploadUri = $release.upload_url.Split('{')[0] + "?name=$name" + $contentType = if ($file -like '*.zip') { + 'application/zip' + } else { + 'application/json' + } + Invoke-RestMethod -Method Post ` + -Uri $uploadUri ` + -Headers $headers ` + -ContentType $contentType ` + -InFile $file | Out-Null + } + + Invoke-RestMethod -Method Patch ` + -Uri "$api/$($release.id)" ` + -Headers $headers ` + -ContentType 'application/json' ` + -Body '{"draft":false}' | Out-Null + + Write-Host "发布完成:$tag" diff --git a/.gitignore b/.gitignore index 1f6e0dd..839bf71 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ log.txt dist/manifest.json tools/__pycache__/ +tools/test_update.mjs /mengsan/.planning/ /mengsan/verification \ No newline at end of file diff --git a/extension.js b/extension.js index 8e20757..dab0fe8 100644 --- a/extension.js +++ b/extension.js @@ -5,7 +5,7 @@ import initShuYingMenu from "./menu.js"; export const type = "extension"; export let shuYing = new Object(); -const shuYingLocalVersion = "2.0.1.1"; +const shuYingLocalVersion = "2.0.1.1-rc.1"; shuYing.name = "术樱包"; shuYing.editable = false; diff --git a/menu.js b/menu.js index ed00fd3..6dd7fbb 100644 --- a/menu.js +++ b/menu.js @@ -206,6 +206,39 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) { nopointer: true, }; + const channelText = channel => `更新通道:${channel == "preview" ? "测试版" : "正式版"}(点击切换)`; + menu.update_channel = { + name: channelText(updater.getUpdateChannel()), + clear: true, + intro: "切换后立即从 Gitea 对应标签下载并校验文件,完成后需重启游戏。测试版可能回退到较早的内容。", + async onclick() { + if (!shuYing.m_bIsDownload) { + alert("有其他文件正在下载,请稍后再试吧。"); + return; + } + + const current = updater.getUpdateChannel(); + const next = current == "preview" ? "stable" : "preview"; + shuYing.m_bIsDownload = false; + this.innerHTML = `正在切换到${next == "preview" ? "测试版" : "正式版"}…`; + try { + const version = await updater.checkVersion(shuYing, { + channel: next, + switchChannel: true, + }); + if (version) { + game.saveConfig("shuYing_update_channel", next); + shuYing.updateOnlineVersionMenu(version); + } + } + finally { + const text = channelText(updater.getUpdateChannel()); + menu.update_channel.name = text; + this.innerHTML = text; + } + }, + }; + menu.updateLog = { name: '
▶更新日志
', clear: true, diff --git a/tools/generate_manifest.py b/tools/generate_manifest.py index 201b20f..8bcc58c 100644 --- a/tools/generate_manifest.py +++ b/tools/generate_manifest.py @@ -7,7 +7,15 @@ from datetime import datetime, timezone from pathlib import Path -EXCLUDE_DIRS = {".git", ".gitea", ".update_tmp", "dist", "tools", "__pycache__"} +EXCLUDE_DIRS = { + ".git", + ".gitea", + ".github", + ".update_tmp", + "dist", + "tools", + "__pycache__", +} EXCLUDE_FILES = {".gitignore", "log.txt"} @@ -159,6 +167,7 @@ def build_manifest(root: Path, version: str, config: dict) -> dict: "version": version, "generated_at": datetime.now(timezone.utc).isoformat(), "algorithm": "sha256", + "update_channels": True, "file_count": len(flat_files), "folder_sha256": folder_digest.hexdigest(), "core": core_files, diff --git a/update.js b/update.js index 7ca45ab..e31a0b0 100644 --- a/update.js +++ b/update.js @@ -1,24 +1,62 @@ import { lib, game } from "../../noname.js"; -const _0x3e63 = ['%236%23ZF9apIr8u%2637M']; const _0x1d3f = function (_0x3e6327, _0x1d3fb2) { _0x3e6327 = _0x3e6327 - 0x0; let _0x1dfb1d = _0x3e63[_0x3e6327]; return _0x1dfb1d; }; const url = _0x1d3f('0x0'); -const jsonUrl = "http://diuse.work:7994/json/"; -const downloadUrl = "http://diuse.work:7994/download/"; +const repositoryApi = "https://gitea.diuse.work/api/v1/repos/Diuse/noname-shuying"; const coreTempDir = "extension/术樱包/.update_tmp"; -function getTokenQuery() { - if (!url) return ""; - if (url.startsWith("?")) return url.slice(1); - if (url.startsWith("&")) return url.slice(1); - if (url.includes("=")) return url; - return `token=${url}`; +function parseVersion(version) { + const match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/.exec(version); + if (!match) return null; + return { + parts: match.slice(1, 5).map(Number), + rc: match[5] == null ? null : Number(match[5]), + }; } -function getRemoteUrl(base, path) { - const filePath = path.replace(/^\/+/, ""); - const tokenQuery = getTokenQuery(); - const query = tokenQuery ? `${tokenQuery}&time=${Date.now()}` : `time=${Date.now()}`; +function compareVersions(left, right) { + const a = parseVersion(left); + const b = parseVersion(right); + if (!a || !b) throw new Error("版本号格式错误"); + for (let i = 0; i < 4; i++) { + if (a.parts[i] != b.parts[i]) return a.parts[i] - b.parts[i]; + } + if (a.rc == null) return b.rc == null ? 0 : 1; + if (b.rc == null) return -1; + return a.rc - b.rc; +} - return `${base}${filePath}?${query}`; +function getUpdateChannel() { + return lib.config.shuYing_update_channel + || (/-rc\.\d+$/.test(lib.config.shuYing_local_version || "") + ? "preview" : "stable"); +} + +function getRemoteUrl(tag, path) { + const filePath = path.split("/").map(encodeURIComponent).join("/"); + return `${repositoryApi}/raw/${filePath}?ref=${encodeURIComponent(tag)}`; +} + +async function getLatestTag(channel = getUpdateChannel()) { + let latest = null; + for (let page = 1; ; page++) { + const response = await fetch(`${repositoryApi}/tags?page=${page}&limit=50`); + if (!response.ok) throw new Error(`标签请求失败:${response.status}`); + const tags = await response.json(); + if (!Array.isArray(tags)) throw new Error("标签列表格式错误"); + + for (const tag of tags) { + const name = tag?.name || ""; + const isPreview = /^v\d+\.\d+\.\d+\.\d+-rc\.\d+$/.test(name); + const isStable = /^v\d+\.\d+\.\d+\.\d+$/.test(name); + if (channel == "preview" ? !isPreview : !isStable) continue; + const version = name.slice(1); + if (!latest || compareVersions(version, latest.version) > 0) { + latest = { name, version }; + } + } + if (tags.length < 50) break; + } + if (!latest) throw new Error(`Gitea 镜像尚无${channel == "preview" ? "测试版" : "正式版"}标签`); + return latest; } function getByteLength(data) { @@ -154,8 +192,8 @@ function splitPath(path) { }; } -async function fetchFile(path) { - const response = await fetch(getRemoteUrl(downloadUrl, path)); +async function fetchFile(path, tag) { + const response = await fetch(getRemoteUrl(tag, path)); if (!response.ok) throw new Error(`${path} 下载失败:${response.status}`); return await response.arrayBuffer(); @@ -171,7 +209,7 @@ async function writeTargetFile(data, target) { } async function downloadFile(path, target, manifest) { - const data = await fetchFile(path); + const data = await fetchFile(path, manifest.tag_name); if (manifest?.files?.[path]) { await verifyFile(path, data, manifest); } @@ -179,21 +217,42 @@ async function downloadFile(path, target, manifest) { return data; } -async function getManifest() { - const response = await fetch(getRemoteUrl(jsonUrl, "manifest.json")); +async function getManifestFromTag(tag, requireChannels = true) { + const response = await fetch(getRemoteUrl(tag.name, "dist/manifest.json")); if (!response.ok) throw new Error(`校验清单请求失败:${response.status}`); const manifest = await response.json(); - if (!manifest || manifest.algorithm != "sha256" || !manifest.files) { + if (requireChannels && manifest?.update_channels !== true) { + throw new Error("目标标签尚未包含新版更新器,请发布支持通道切换的版本"); + } + if (!manifest || manifest.algorithm != "sha256" + || !manifest.files || Array.isArray(manifest.files) + || typeof manifest.files != "object" + || manifest.version != tag.version) { throw new Error("校验清单格式错误"); } + const files = getManifestFileKeys(manifest); + if (!files.length || files.some(file => { + const info = manifest.files[file]; + return !info || !Number.isSafeInteger(info.size) || info.size < 0 + || !/^[a-f0-9]{64}$/.test(info.sha256); + })) { + throw new Error("校验清单包含无效文件信息"); + } + const coreFiles = getManifestCoreFiles(manifest); + getManifestAssetFiles(manifest, coreFiles); getManifestRemoveFiles(manifest); getManifestRemoveDirectories(manifest); + manifest.tag_name = tag.name; return manifest; } +async function getManifest(channel = getUpdateChannel()) { + return getManifestFromTag(await getLatestTag(channel)); +} + function normalizeManifestPath(path) { return String(path || "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, ""); } @@ -203,6 +262,9 @@ function uniqueManifestList(files) { const seen = new Set(); for (const file of files || []) { + if (!isSafeManifestPath(file)) { + throw new Error(`校验清单包含不安全路径:${String(file)}`); + } const path = normalizeManifestPath(file); if (!path || seen.has(path)) continue; seen.add(path); @@ -251,7 +313,9 @@ function getManifestRemoveFiles(manifest) { throw new Error(`校验清单 remove 包含不安全路径:${unsafe.join("、")}`); } - const conflicts = files.filter(file => manifest?.files?.[file]); + const manifestFiles = getManifestFileKeys(manifest); + const conflicts = files.filter(file => manifestFiles.some(current => + current == file || current.startsWith(`${file}/`))); if (conflicts.length) { throw new Error(`待删除文件仍存在于新版清单:${conflicts.join("、")}`); } @@ -260,7 +324,9 @@ function getManifestRemoveFiles(manifest) { } function isSafeManifestPath(path) { - const parts = normalizeManifestPath(path).split("/"); + const normalized = normalizeManifestPath(path); + if (typeof path != "string" || path != normalized) return false; + const parts = normalized.split("/"); return parts.length > 0 && parts.every(part => part && part != "." && part != "..") && !parts.some(part => part.includes(":")); @@ -405,7 +471,12 @@ async function removeManifestFiles(shuYing, manifest) { for (const file of files) { await addLog(shuYing, `删除旧文件:${file}`); - await removeFile(`extension/术樱包/${file}`); + const target = `extension/术樱包/${file}`; + await removeFile(target); + if (typeof game.checkFile == "function" + && await safeCheckFile(target) == 1) { + throw new Error(`旧文件删除失败:${file}`); + } } } @@ -507,6 +578,114 @@ async function downloadCoreFiles(shuYing, manifest) { } } +async function getChangedFiles(manifest) { + if (typeof game.readFile != "function") { + throw new Error("当前环境不支持校验本地文件"); + } + const changed = []; + for (const file of getManifestFileKeys(manifest)) { + try { + const data = await readFile(`extension/术樱包/${file}`); + await verifyFile(file, data, manifest); + } + catch (error) { + changed.push(file); + } + } + return changed; +} + +async function getObsoleteFiles(manifest) { + const existing = []; + for (const file of getManifestRemoveFiles(manifest)) { + const target = `extension/术樱包/${file}`; + if (typeof game.checkFile == "function") { + if (await safeCheckFile(target) == 1) existing.push(file); + } + else { + try { + await readFile(target); + existing.push(file); + } + catch (error) { } + } + } + return existing; +} + +async function updateAllFiles(shuYing, manifest, files) { + if (typeof game.writeFile != "function") { + throw new Error("当前环境不支持写入文件"); + } + const newRoot = `${coreTempDir}/new`; + const oldRoot = `${coreTempDir}/old`; + const backedUp = new Set(); + const installed = []; + let filesInstalled = false; + try { + ensureProgressNode(shuYing); + await createDir(`${newRoot}/`); + await createDir(`${oldRoot}/`); + await createFolders(`${newRoot}/`, files); + await createFolders(`${oldRoot}/`, files); + + for (let i = 0; i < files.length; i++) { + const file = files[i]; + setProgress(shuYing, { + title: "版本更新", status: "下载并校验", file, + current: i, total: files.length * 2, + }); + await downloadFile(file, `${newRoot}/${file}`, manifest); + } + + await createFolders("extension/术樱包/", files); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const target = `extension/术樱包/${file}`; + try { + const original = await readFile(target); + await writeTargetFile(original, `${oldRoot}/${file}`); + backedUp.add(file); + } + catch (error) { + if (await safeCheckFile(target) == 1) throw error; + } + + setProgress(shuYing, { + title: "版本更新", status: "写入中", file, + current: files.length + i, total: files.length * 2, + }); + installed.push(file); + await writeTargetFile(await readFile(`${newRoot}/${file}`), target); + } + filesInstalled = true; + await removeManifestOldPaths(shuYing, manifest); + } + catch (error) { + for (const file of filesInstalled ? [] : installed.reverse()) { + const target = `extension/术樱包/${file}`; + try { + if (backedUp.has(file)) { + await writeTargetFile(await readFile(`${oldRoot}/${file}`), target); + } + else { + await removeFile(target); + } + } + catch (rollbackError) { + console.error("文件回滚失败:", file, rollbackError); + } + } + throw error; + } + finally { + for (const file of files) { + await removeFile(`${newRoot}/${file}`); + if (backedUp.has(file)) await removeFile(`${oldRoot}/${file}`); + } + } +} + async function downloadList(shuYing, files, manifest) { let finished = 0; const total = files.length; @@ -518,11 +697,17 @@ async function downloadList(shuYing, files, manifest) { setProgress(shuYing, { title: "查漏补缺", status: "检查本地文件", file: target, current: finished, total }); const exists = typeof game.checkFile == "function" ? await safeCheckFile(target) : 0; - if (exists == 1) { - finished++; - await addLog(shuYing, `跳过:${file}`); - setProgress(shuYing, { title: "查漏补缺", status: "已存在,跳过", file: target, current: finished, total }); - continue; + if (exists == 1 && typeof game.readFile == "function") { + try { + await verifyFile(file, await readFile(target), manifest); + finished++; + await addLog(shuYing, `校验通过,跳过:${file}`); + setProgress(shuYing, { title: "查漏补缺", status: "校验通过", file: target, current: finished, total }); + continue; + } + catch (error) { + await addLog(shuYing, `本地文件需修复:${file};${getErrorMessage(error)}`); + } } await addLog(shuYing, `下载:${file}`); @@ -542,37 +727,64 @@ async function downloadList(shuYing, files, manifest) { } } -async function getOnlineVersion() { - const response = await fetch(getRemoteUrl(jsonUrl, "online_version.json")); - if (!response.ok) throw new Error(`版本请求失败:${response.status}`); - const result = await response.json(); - console.log("术樱包在线版本:", result.online_version); - return result; +async function getOnlineVersion(channel = getUpdateChannel()) { + const manifest = await getManifest(channel); + return { online_version: manifest.version, manifest }; } -async function checkVersion(shuYing) { +async function checkVersion(shuYing, options = {}) { try { - const onlineVersion = await getOnlineVersion(); + shuYing._updateLogs = []; + await addLog(shuYing, "版本检查开始"); + const onlineVersion = await getOnlineVersion(options.channel); const localVersion = lib.config.shuYing_local_version || "0.0.0.0"; + const manifest = onlineVersion.manifest; + if (!options.switchChannel && parseVersion(localVersion) + && compareVersions(onlineVersion.online_version, localVersion) < 0) { + throw new Error("Gitea 镜像尚未同步到本地版本,请稍后重试"); + } + if (options.switchChannel && parseVersion(localVersion) + && localVersion != manifest.version) { + const previous = await getManifestFromTag({ + name: `v${localVersion}`, + version: localVersion, + }, false); + const targetFiles = new Set(getManifestFileKeys(manifest)); + const removed = getManifestFileKeys(previous) + .filter(file => !targetFiles.has(file)); + manifest.remove = [...new Set([...(manifest.remove || []), ...removed])]; + getManifestRemoveFiles(manifest); + } + const changed = await getChangedFiles(manifest); + const obsolete = await getObsoleteFiles(manifest); + await addLog(shuYing, `Gitea 标签:${manifest.tag_name};待更新文件:${changed.length};待删除文件:${obsolete.length}`); - game.saveConfig("shuYing_online_version", onlineVersion.online_version); - - if (localVersion == onlineVersion.online_version) { + if (localVersion == onlineVersion.online_version + && !changed.length && !obsolete.length) { + game.saveConfig("shuYing_online_version", onlineVersion.online_version); alert("本地版本为最新版"); - return; + return onlineVersion.online_version; } - if (!confirm(`检测到最新版本为:${onlineVersion.online_version}, 本地版本为:${localVersion},是否更新`)) { - return; + const message = localVersion == onlineVersion.online_version + ? `当前版本有 ${changed.length} 个文件需修复、${obsolete.length} 个旧文件需删除,是否继续?` + : `检测到最新版本为:${onlineVersion.online_version}, 本地版本为:${localVersion},是否更新?`; + if (!options.switchChannel && !confirm(message)) { + return null; } - await downloadCoreFiles(shuYing); + await updateAllFiles(shuYing, manifest, changed); game.saveConfig("shuYing_local_version", onlineVersion.online_version); + game.saveConfig("shuYing_online_version", onlineVersion.online_version); + await addLog(shuYing, "版本更新完成"); alert("下载完成,重启生效"); + return onlineVersion.online_version; } catch (error) { console.error(error); + await addLog(shuYing, `版本更新失败:${getErrorMessage(error)}`); alert("版本检测或下载失败,请检查网络或服务器配置。"); + return null; } finally { removeProgressNode(shuYing); @@ -672,6 +884,7 @@ async function repairCoreFiles(shuYing) { export default { getOnlineVersion, + getUpdateChannel, checkVersion, repairMissingFiles, repairCoreFiles,