mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-02 18:10:02 +08:00
merge
This commit is contained in:
@@ -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"
|
||||
@@ -1,5 +1,6 @@
|
||||
log.txt
|
||||
dist/manifest.json
|
||||
tools/__pycache__/
|
||||
tools/test_update.mjs
|
||||
/mengsan/.planning/
|
||||
/mengsan/verification
|
||||
+1
-1
@@ -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;
|
||||
|
||||
@@ -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: '<div class="hth_menu">▶更新日志</div>',
|
||||
clear: true,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user