mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-15 19:50:08 +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
|
log.txt
|
||||||
dist/manifest.json
|
dist/manifest.json
|
||||||
tools/__pycache__/
|
tools/__pycache__/
|
||||||
|
tools/test_update.mjs
|
||||||
/mengsan/.planning/
|
/mengsan/.planning/
|
||||||
/mengsan/verification
|
/mengsan/verification
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import initShuYingMenu from "./menu.js";
|
|||||||
|
|
||||||
export const type = "extension";
|
export const type = "extension";
|
||||||
export let shuYing = new Object();
|
export let shuYing = new Object();
|
||||||
const shuYingLocalVersion = "2.0.1.1";
|
const shuYingLocalVersion = "2.0.1.1-rc.1";
|
||||||
|
|
||||||
shuYing.name = "术樱包";
|
shuYing.name = "术樱包";
|
||||||
shuYing.editable = false;
|
shuYing.editable = false;
|
||||||
|
|||||||
@@ -206,6 +206,39 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) {
|
|||||||
nopointer: true,
|
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 = {
|
menu.updateLog = {
|
||||||
name: '<div class="hth_menu">▶更新日志</div>',
|
name: '<div class="hth_menu">▶更新日志</div>',
|
||||||
clear: true,
|
clear: true,
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
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"}
|
EXCLUDE_FILES = {".gitignore", "log.txt"}
|
||||||
|
|
||||||
|
|
||||||
@@ -159,6 +167,7 @@ def build_manifest(root: Path, version: str, config: dict) -> dict:
|
|||||||
"version": version,
|
"version": version,
|
||||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||||
"algorithm": "sha256",
|
"algorithm": "sha256",
|
||||||
|
"update_channels": True,
|
||||||
"file_count": len(flat_files),
|
"file_count": len(flat_files),
|
||||||
"folder_sha256": folder_digest.hexdigest(),
|
"folder_sha256": folder_digest.hexdigest(),
|
||||||
"core": core_files,
|
"core": core_files,
|
||||||
|
|||||||
@@ -1,24 +1,62 @@
|
|||||||
import { lib, game } from "../../noname.js";
|
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 repositoryApi = "https://gitea.diuse.work/api/v1/repos/Diuse/noname-shuying";
|
||||||
const jsonUrl = "http://diuse.work:7994/json/";
|
|
||||||
const downloadUrl = "http://diuse.work:7994/download/";
|
|
||||||
const coreTempDir = "extension/术樱包/.update_tmp";
|
const coreTempDir = "extension/术樱包/.update_tmp";
|
||||||
|
|
||||||
function getTokenQuery() {
|
function parseVersion(version) {
|
||||||
if (!url) return "";
|
const match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/.exec(version);
|
||||||
if (url.startsWith("?")) return url.slice(1);
|
if (!match) return null;
|
||||||
if (url.startsWith("&")) return url.slice(1);
|
return {
|
||||||
if (url.includes("=")) return url;
|
parts: match.slice(1, 5).map(Number),
|
||||||
return `token=${url}`;
|
rc: match[5] == null ? null : Number(match[5]),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRemoteUrl(base, path) {
|
function compareVersions(left, right) {
|
||||||
const filePath = path.replace(/^\/+/, "");
|
const a = parseVersion(left);
|
||||||
const tokenQuery = getTokenQuery();
|
const b = parseVersion(right);
|
||||||
const query = tokenQuery ? `${tokenQuery}&time=${Date.now()}` : `time=${Date.now()}`;
|
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) {
|
function getByteLength(data) {
|
||||||
@@ -154,8 +192,8 @@ function splitPath(path) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFile(path) {
|
async function fetchFile(path, tag) {
|
||||||
const response = await fetch(getRemoteUrl(downloadUrl, path));
|
const response = await fetch(getRemoteUrl(tag, path));
|
||||||
if (!response.ok) throw new Error(`${path} 下载失败:${response.status}`);
|
if (!response.ok) throw new Error(`${path} 下载失败:${response.status}`);
|
||||||
|
|
||||||
return await response.arrayBuffer();
|
return await response.arrayBuffer();
|
||||||
@@ -171,7 +209,7 @@ async function writeTargetFile(data, target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function downloadFile(path, target, manifest) {
|
async function downloadFile(path, target, manifest) {
|
||||||
const data = await fetchFile(path);
|
const data = await fetchFile(path, manifest.tag_name);
|
||||||
if (manifest?.files?.[path]) {
|
if (manifest?.files?.[path]) {
|
||||||
await verifyFile(path, data, manifest);
|
await verifyFile(path, data, manifest);
|
||||||
}
|
}
|
||||||
@@ -179,21 +217,42 @@ async function downloadFile(path, target, manifest) {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getManifest() {
|
async function getManifestFromTag(tag, requireChannels = true) {
|
||||||
const response = await fetch(getRemoteUrl(jsonUrl, "manifest.json"));
|
const response = await fetch(getRemoteUrl(tag.name, "dist/manifest.json"));
|
||||||
if (!response.ok) throw new Error(`校验清单请求失败:${response.status}`);
|
if (!response.ok) throw new Error(`校验清单请求失败:${response.status}`);
|
||||||
const manifest = await response.json();
|
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("校验清单格式错误");
|
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);
|
getManifestRemoveFiles(manifest);
|
||||||
getManifestRemoveDirectories(manifest);
|
getManifestRemoveDirectories(manifest);
|
||||||
|
|
||||||
|
manifest.tag_name = tag.name;
|
||||||
return manifest;
|
return manifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getManifest(channel = getUpdateChannel()) {
|
||||||
|
return getManifestFromTag(await getLatestTag(channel));
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeManifestPath(path) {
|
function normalizeManifestPath(path) {
|
||||||
return String(path || "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
return String(path || "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
||||||
}
|
}
|
||||||
@@ -203,6 +262,9 @@ function uniqueManifestList(files) {
|
|||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
||||||
for (const file of files || []) {
|
for (const file of files || []) {
|
||||||
|
if (!isSafeManifestPath(file)) {
|
||||||
|
throw new Error(`校验清单包含不安全路径:${String(file)}`);
|
||||||
|
}
|
||||||
const path = normalizeManifestPath(file);
|
const path = normalizeManifestPath(file);
|
||||||
if (!path || seen.has(path)) continue;
|
if (!path || seen.has(path)) continue;
|
||||||
seen.add(path);
|
seen.add(path);
|
||||||
@@ -251,7 +313,9 @@ function getManifestRemoveFiles(manifest) {
|
|||||||
throw new Error(`校验清单 remove 包含不安全路径:${unsafe.join("、")}`);
|
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) {
|
if (conflicts.length) {
|
||||||
throw new Error(`待删除文件仍存在于新版清单:${conflicts.join("、")}`);
|
throw new Error(`待删除文件仍存在于新版清单:${conflicts.join("、")}`);
|
||||||
}
|
}
|
||||||
@@ -260,7 +324,9 @@ function getManifestRemoveFiles(manifest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isSafeManifestPath(path) {
|
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
|
return parts.length > 0
|
||||||
&& parts.every(part => part && part != "." && part != "..")
|
&& parts.every(part => part && part != "." && part != "..")
|
||||||
&& !parts.some(part => part.includes(":"));
|
&& !parts.some(part => part.includes(":"));
|
||||||
@@ -405,7 +471,12 @@ async function removeManifestFiles(shuYing, manifest) {
|
|||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
await addLog(shuYing, `删除旧文件:${file}`);
|
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) {
|
async function downloadList(shuYing, files, manifest) {
|
||||||
let finished = 0;
|
let finished = 0;
|
||||||
const total = files.length;
|
const total = files.length;
|
||||||
@@ -518,11 +697,17 @@ async function downloadList(shuYing, files, manifest) {
|
|||||||
setProgress(shuYing, { title: "查漏补缺", status: "检查本地文件", file: target, current: finished, total });
|
setProgress(shuYing, { title: "查漏补缺", status: "检查本地文件", file: target, current: finished, total });
|
||||||
const exists = typeof game.checkFile == "function" ? await safeCheckFile(target) : 0;
|
const exists = typeof game.checkFile == "function" ? await safeCheckFile(target) : 0;
|
||||||
|
|
||||||
if (exists == 1) {
|
if (exists == 1 && typeof game.readFile == "function") {
|
||||||
finished++;
|
try {
|
||||||
await addLog(shuYing, `跳过:${file}`);
|
await verifyFile(file, await readFile(target), manifest);
|
||||||
setProgress(shuYing, { title: "查漏补缺", status: "已存在,跳过", file: target, current: finished, total });
|
finished++;
|
||||||
continue;
|
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}`);
|
await addLog(shuYing, `下载:${file}`);
|
||||||
@@ -542,37 +727,64 @@ async function downloadList(shuYing, files, manifest) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOnlineVersion() {
|
async function getOnlineVersion(channel = getUpdateChannel()) {
|
||||||
const response = await fetch(getRemoteUrl(jsonUrl, "online_version.json"));
|
const manifest = await getManifest(channel);
|
||||||
if (!response.ok) throw new Error(`版本请求失败:${response.status}`);
|
return { online_version: manifest.version, manifest };
|
||||||
const result = await response.json();
|
|
||||||
console.log("术樱包在线版本:", result.online_version);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkVersion(shuYing) {
|
async function checkVersion(shuYing, options = {}) {
|
||||||
try {
|
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 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
|
||||||
|
&& !changed.length && !obsolete.length) {
|
||||||
if (localVersion == onlineVersion.online_version) {
|
game.saveConfig("shuYing_online_version", onlineVersion.online_version);
|
||||||
alert("本地版本为最新版");
|
alert("本地版本为最新版");
|
||||||
return;
|
return onlineVersion.online_version;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!confirm(`检测到最新版本为:${onlineVersion.online_version}, 本地版本为:${localVersion},是否更新`)) {
|
const message = localVersion == onlineVersion.online_version
|
||||||
return;
|
? `当前版本有 ${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_local_version", onlineVersion.online_version);
|
||||||
|
game.saveConfig("shuYing_online_version", onlineVersion.online_version);
|
||||||
|
await addLog(shuYing, "版本更新完成");
|
||||||
alert("下载完成,重启生效");
|
alert("下载完成,重启生效");
|
||||||
|
return onlineVersion.online_version;
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
await addLog(shuYing, `版本更新失败:${getErrorMessage(error)}`);
|
||||||
alert("版本检测或下载失败,请检查网络或服务器配置。");
|
alert("版本检测或下载失败,请检查网络或服务器配置。");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
removeProgressNode(shuYing);
|
removeProgressNode(shuYing);
|
||||||
@@ -672,6 +884,7 @@ async function repairCoreFiles(shuYing) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
getOnlineVersion,
|
getOnlineVersion,
|
||||||
|
getUpdateChannel,
|
||||||
checkVersion,
|
checkVersion,
|
||||||
repairMissingFiles,
|
repairMissingFiles,
|
||||||
repairCoreFiles,
|
repairCoreFiles,
|
||||||
|
|||||||
Reference in New Issue
Block a user