Files
noname-shuying/update.js
T
Diuse dd562ae627 fixed # 修复手机端地图滑动异常的问题
fixed # 重构更新框架,支持选择是否下载梦三模式
fixed # 补充部分意图的资源图片与逻辑
2026-09-29 17:44:21 +08:00

1295 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { lib, game } from "../../noname.js";
const giteaRepositoryApi = "https://gitea.diuse.work/api/v1/repos/Diuse/noname-shuying";
const githubRepositoryApi = "https://api.github.com/repos/Shu-Ying/noname-shuying";
const githubRawBase = "https://raw.githubusercontent.com/Shu-Ying/noname-shuying";
const coreTempDir = "extension/术樱包/.update_tmp";
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 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;
}
function getUpdateChannel() {
return lib.config.shuYing_update_channel
|| (/-rc\.\d+$/.test(lib.config.shuYing_local_version || "")
? "preview" : "stable");
}
function getUpdateSource() {
return lib.config.shuYing_update_source == "github" ? "github" : "gitea";
}
function getRemoteUrl(tag, path, source = getUpdateSource()) {
const filePath = path.split("/").map(encodeURIComponent).join("/");
if (source == "github") {
return `${githubRawBase}/${encodeURIComponent(tag)}/${filePath}`;
}
return `${giteaRepositoryApi}/raw/${filePath}?ref=${encodeURIComponent(tag)}`;
}
async function getLatestTag(channel = getUpdateChannel(), source = getUpdateSource()) {
let latest = null;
for (let page = 1; ; page++) {
const tagsUrl = source == "github"
? `${githubRepositoryApi}/tags?page=${page}&per_page=50`
: `${giteaRepositoryApi}/tags?page=${page}&limit=50`;
const response = await fetch(tagsUrl);
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(`更新源尚无${channel == "preview" ? "测试版" : "正式版"}标签`);
return latest;
}
function getByteLength(data) {
if (typeof data == "string") return new TextEncoder().encode(data).byteLength;
if (data instanceof ArrayBuffer) return data.byteLength;
if (ArrayBuffer.isView(data)) return data.byteLength;
return data?.length || 0;
}
function toUint8Array(data) {
if (data instanceof Uint8Array) return data;
if (data instanceof ArrayBuffer) return new Uint8Array(data);
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
if (typeof data == "string") return new TextEncoder().encode(data);
throw new Error("无法识别的文件数据格式");
}
function bytesToHex(bytes) {
return Array.from(bytes, byte => byte.toString(16).padStart(2, "0")).join("");
}
async function sha256(data) {
const bytes = toUint8Array(data);
if (globalThis.crypto?.subtle) {
const hash = await globalThis.crypto.subtle.digest("SHA-256", bytes);
return bytesToHex(new Uint8Array(hash));
}
if (lib.node) {
const crypto = window.require("crypto");
return crypto.createHash("sha256").update(bytes).digest("hex");
}
throw new Error("当前环境不支持 SHA-256 校验");
}
function setBusy(shuYing, busy) {
shuYing.m_bIsDownload = !busy;
}
function ensureProgressNode(shuYing) {
if (!shuYing.text.parentNode) {
document.body.appendChild(shuYing.text);
}
}
function removeProgressNode(shuYing) {
if (shuYing.text.parentNode) {
shuYing.text.parentNode.removeChild(shuYing.text);
}
}
function getErrorMessage(error) {
if (!error) return "未知错误";
if (typeof error == "string") return error;
return error.message || error.stack || String(error);
}
function getLogText(shuYing) {
return (shuYing?._updateLogs || []).join("\n");
}
async function writeLog(shuYing) {
if (!shuYing || typeof game.writeFile != "function") return;
await writeFile(getLogText(shuYing), "extension/术樱包", "log.txt");
}
async function addLog(shuYing, message) {
if (!shuYing) return;
if (!Array.isArray(shuYing._updateLogs)) {
shuYing._updateLogs = [];
}
const time = new Date().toLocaleString();
shuYing._updateLogs.push(`[${time}] ${message}`);
try {
await writeLog(shuYing);
}
catch (error) {
console.error("写入术樱包日志失败:", error);
}
}
function escapeHTML(text) {
return String(text ?? "").replace(/[&<>"']/g, char => ({
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
}[char]));
}
function formatBytes(bytes) {
const value = Math.max(0, Number(bytes) || 0);
if (value < 1024 * 1024) return `${Math.ceil(value / 1024)} KB`;
return `${(value / 1024 / 1024).toFixed(1)} MB`;
}
function setProgress(shuYing, options = {}) {
if (!shuYing?.text) return;
const current = Math.max(0, Number(options.current) || 0);
const total = Math.max(0, Number(options.total) || 0);
const percent = total > 0 ? Math.min(100, Math.round(current / total * 100)) : 0;
const title = options.title || "资源更新";
const status = options.status || "准备中";
const file = options.file || "";
shuYing.text.innerHTML = `
<div style="position:static;box-sizing:border-box;writing-mode:horizontal-tb;text-orientation:mixed;display:flex;align-items:center;justify-content:space-between;gap:12px;min-width:0;width:100%;">
<div style="position:static;box-sizing:border-box;font-size:16px;font-weight:700;color:#fff;white-space:nowrap;word-break:keep-all;overflow:hidden;text-overflow:ellipsis;min-width:0;">${escapeHTML(title)}</div>
<div style="position:static;box-sizing:border-box;font-size:12px;color:rgba(245,241,232,0.72);white-space:nowrap;word-break:keep-all;flex:0 0 auto;">${current}/${total}</div>
</div>
<div style="position:static;box-sizing:border-box;display:block;width:100%;height:8px;min-height:8px;border-radius:999px;background:linear-gradient(90deg,#6ee7b7 0%,#60a5fa ${percent}%,rgba(255,255,255,0.14) ${percent}%,rgba(255,255,255,0.14) 100%);overflow:hidden;"></div>
<div style="position:static;box-sizing:border-box;writing-mode:horizontal-tb;text-orientation:mixed;display:flex;justify-content:space-between;gap:10px;font-size:12px;color:rgba(245,241,232,0.82);min-width:0;width:100%;">
<span style="position:static;box-sizing:border-box;white-space:nowrap;word-break:keep-all;overflow:hidden;text-overflow:ellipsis;min-width:0;">${escapeHTML(status)}</span>
<span style="position:static;box-sizing:border-box;white-space:nowrap;word-break:keep-all;flex:0 0 auto;">${percent}%</span>
</div>
<div style="position:static;box-sizing:border-box;width:100%;writing-mode:horizontal-tb;text-orientation:mixed;font-size:12px;color:rgba(245,241,232,0.58);white-space:nowrap;word-break:keep-all;overflow:hidden;text-overflow:ellipsis;">${escapeHTML(file)}</div>
`;
}
function splitPath(path) {
const index = path.lastIndexOf("/");
if (index == -1) {
return {
dir: "",
name: path,
};
}
return {
dir: path.slice(0, index),
name: path.slice(index + 1),
};
}
async function fetchFile(path, tag, source) {
const response = await fetch(getRemoteUrl(tag, path, source));
if (!response.ok) throw new Error(`${path} 下载失败:${response.status}`);
return await response.arrayBuffer();
}
async function writeTargetFile(data, target) {
if (typeof game.writeFile != "function") {
throw new Error("当前环境不支持写入文件");
}
const targetInfo = splitPath(target);
await writeFile(data, targetInfo.dir, targetInfo.name);
}
async function downloadFile(path, target, manifest) {
const data = await fetchFile(path, manifest.tag_name, manifest.source);
if (manifest?.files?.[path]) {
await verifyFile(path, data, manifest);
}
await writeTargetFile(data, target);
return data;
}
async function getManifestFromTag(tag, requireChannels = true,
source = getUpdateSource()) {
const response = await fetch(getRemoteUrl(tag.name, "dist/manifest.json", source));
if (!response.ok) throw new Error(`校验清单请求失败:${response.status}`);
const manifest = await response.json();
if (requireChannels && manifest?.update_channels !== true) {
throw new Error("目标标签尚未包含新版更新器,请发布支持通道切换的版本");
}
validateManifest(manifest, tag.version);
manifest.tag_name = tag.name;
manifest.source = source;
return manifest;
}
function validateManifest(manifest, version, moduleId = "") {
if (!manifest || manifest.algorithm != "sha256"
|| !manifest.files || Array.isArray(manifest.files)
|| typeof manifest.files != "object"
|| manifest.version != version
|| (moduleId && manifest.module_id != moduleId)) {
throw new Error("校验清单格式错误");
}
const files = getManifestFileKeys(manifest);
if (moduleId && files.some(file => !file.startsWith(`${moduleId}/`))) {
throw new Error(`校验清单包含越界的 ${moduleId} 模块文件`);
}
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);
return manifest;
}
async function getModuleManifest(tag, rootManifest, moduleId,
source = rootManifest?.source || getUpdateSource()) {
const descriptor = rootManifest?.modules?.[moduleId];
if (!descriptor || !isSafeManifestPath(descriptor.manifest)) {
throw new Error(`更新清单未提供 ${moduleId} 模块`);
}
if (typeof descriptor.entry != "string"
|| !isSafeManifestPath(descriptor.entry)
|| !descriptor.entry.startsWith(`${moduleId}/`)) {
throw new Error(`${moduleId} 模块入口配置无效`);
}
const response = await fetch(getRemoteUrl(tag.name, descriptor.manifest, source));
if (!response.ok) throw new Error(`${moduleId} 模块清单请求失败:${response.status}`);
const manifest = await response.json();
validateManifest(manifest, tag.version, moduleId);
if (!manifest.files[descriptor.entry]) {
throw new Error(`${moduleId} 模块清单缺少入口文件`);
}
manifest.tag_name = tag.name;
manifest.source = source;
return manifest;
}
async function getManifest(channel = getUpdateChannel(), source = getUpdateSource()) {
return getManifestFromTag(await getLatestTag(channel, source), true, source);
}
function normalizeManifestPath(path) {
return String(path || "").replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
}
function uniqueManifestList(files) {
const result = [];
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);
result.push(path);
}
return result;
}
function getManifestFileKeys(manifest) {
return uniqueManifestList(Object.keys(manifest?.files || {})).sort();
}
function getManifestCoreFiles(manifest) {
const files = uniqueManifestList(manifest?.core || []);
const manifestFiles = manifest?.files || {};
const missing = files.filter(file => !manifestFiles[file]);
if (missing.length) {
throw new Error(`校验清单 core 字段包含不存在的文件:${missing.join("、")}`);
}
return files;
}
function getManifestAssetFiles(manifest, coreFiles) {
const manifestFiles = manifest?.files || {};
const coreSet = new Set(coreFiles);
const assets = Array.isArray(manifest?.assets)
? uniqueManifestList(manifest.assets)
: getManifestFileKeys(manifest).filter(file => !coreSet.has(file));
const missing = assets.filter(file => !manifestFiles[file]);
if (missing.length) {
throw new Error(`校验清单 assets 字段包含不存在的文件:${missing.join("、")}`);
}
return assets.filter(file => !coreSet.has(file));
}
function getManifestRemoveFiles(manifest) {
const files = uniqueManifestList(manifest?.remove || []);
const unsafe = files.filter(file => !isSafeManifestPath(file));
if (unsafe.length) {
throw new Error(`校验清单 remove 包含不安全路径:${unsafe.join("、")}`);
}
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("、")}`);
}
return files;
}
function isSafeManifestPath(path) {
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(":"));
}
function getManifestRemoveDirectories(manifest) {
const directories = uniqueManifestList(manifest?.removeDirectories || []);
const unsafe = directories.filter(directory => !isSafeManifestPath(directory));
if (unsafe.length) {
throw new Error(`校验清单 removeDirectories 包含不安全路径:${unsafe.join("、")}`);
}
const manifestFiles = getManifestFileKeys(manifest);
const conflicts = directories.filter(directory =>
manifestFiles.some(file => file == directory || file.startsWith(`${directory}/`))
);
if (conflicts.length) {
throw new Error(`待删除目录仍包含新版文件:${conflicts.join("、")}`);
}
return directories.sort((a, b) => b.split("/").length - a.split("/").length || b.length - a.length);
}
function getManifestDownloadFiles(manifest) {
const coreFiles = getManifestCoreFiles(manifest);
const assetFiles = getManifestAssetFiles(manifest, coreFiles);
const removeSet = new Set(getManifestRemoveFiles(manifest));
const files = coreFiles.length || assetFiles.length
? [...coreFiles, ...assetFiles]
: getManifestFileKeys(manifest);
return uniqueManifestList(files).filter(file => manifest.files[file] && !removeSet.has(file));
}
function getFoldersFromFiles(files) {
const folders = new Set();
for (const file of files) {
const parts = normalizeManifestPath(file).split("/");
parts.pop();
for (let i = 1; i <= parts.length; i++) {
folders.add(parts.slice(0, i).join("/"));
}
}
return Array.from(folders).filter(Boolean).sort();
}
async function verifyFile(path, data, manifest) {
const info = manifest.files[path];
if (!info) throw new Error(`校验清单缺少文件:${path}`);
const size = getByteLength(data);
if (size != info.size) {
throw new Error(`${path} 大小校验失败:${size}/${info.size}`);
}
const hash = await sha256(data);
if (hash != info.sha256) {
throw new Error(`${path} SHA-256 校验失败:${hash}/${info.sha256}`);
}
}
function checkFile(path) {
return new Promise((resolve, reject) => {
game.checkFile(path, resolve, reject);
});
}
async function safeCheckFile(path) {
try {
return await checkFile(path);
}
catch (error) {
console.error("检查文件失败,按缺失处理:", path, error);
return -1;
}
}
function readFile(path) {
return new Promise((resolve, reject) => {
game.readFile(path, resolve, reject);
});
}
function writeFile(data, path, name) {
return new Promise((resolve, reject) => {
game.writeFile(data, path, name, result => {
if (result instanceof Error) {
reject(result);
}
else {
resolve(result);
}
});
});
}
function removeFile(path) {
return new Promise(resolve => {
if (typeof game.removeFile != "function") {
resolve();
return;
}
game.removeFile(path, () => resolve());
});
}
function removeDir(path) {
return new Promise(resolve => {
if (typeof game.removeDir != "function") {
resolve({ removed: false, error: new Error("当前环境不支持删除文件夹") });
return;
}
game.removeDir(
path,
() => resolve({ removed: true }),
error => resolve({ removed: false, error })
);
});
}
function createDir(path) {
return new Promise((resolve, reject) => {
game.createDir(path, resolve, reject);
});
}
async function createFolders(prefix, files, shuYing) {
for (const folder of getFoldersFromFiles(files)) {
const path = `${prefix}${folder}/`;
if (shuYing) await addLog(shuYing, `创建目录:${path}`);
await createDir(path);
}
}
async function removeManifestFiles(shuYing, manifest) {
const files = getManifestRemoveFiles(manifest);
for (const file of files) {
await addLog(shuYing, `删除旧文件:${file}`);
const target = `extension/术樱包/${file}`;
await removeFile(target);
if (typeof game.checkFile == "function"
&& await safeCheckFile(target) == 1) {
throw new Error(`旧文件删除失败:${file}`);
}
}
}
async function removeManifestDirectories(shuYing, manifest) {
const directories = getManifestRemoveDirectories(manifest);
for (const directory of directories) {
const target = `extension/术樱包/${directory}`;
await addLog(shuYing, `删除废弃目录:${directory}`);
const result = await removeDir(target);
if (result.removed) {
await addLog(shuYing, `废弃目录已删除:${directory}`);
}
else {
await addLog(shuYing, `废弃目录不存在或删除失败,已跳过:${directory};${getErrorMessage(result.error)}`);
}
}
}
async function removeManifestOldPaths(shuYing, manifest) {
await removeManifestFiles(shuYing, manifest);
await removeManifestDirectories(shuYing, manifest);
}
async function downloadCoreFiles(shuYing, manifest) {
if (typeof game.readFile != "function" || typeof game.writeFile != "function") {
throw new Error("当前环境不支持安全替换核心文件");
}
let coreFiles = [];
try {
ensureProgressNode(shuYing);
setProgress(shuYing, { title: "核心文件修复", status: "获取校验清单", current: 0, total: 1 });
manifest = manifest || await getManifest();
coreFiles = getManifestCoreFiles(manifest);
if (!coreFiles.length) throw new Error("校验清单缺少 core 核心文件列表");
await createDir(`${coreTempDir}/`);
await createFolders(`${coreTempDir}/`, coreFiles);
await createFolders("extension/术樱包/", coreFiles);
let progress = 0;
const fileData = {};
for (const file of coreFiles) {
setProgress(shuYing, { title: "核心文件修复", status: "下载并校验", file, current: progress, total: coreFiles.length * 2 });
fileData[file] = await downloadFile(file, `${coreTempDir}/${file}`, manifest);
progress++;
}
const backupData = {};
for (const file of coreFiles) {
try {
backupData[file] = await readFile(`extension/术樱包/${file}`);
}
catch (error) {
backupData[file] = null;
}
}
try {
for (const file of coreFiles) {
setProgress(shuYing, { title: "核心文件修复", status: "写入中", file, current: progress, total: coreFiles.length * 2 });
await writeTargetFile(fileData[file], `extension/术樱包/${file}`);
progress++;
}
await removeManifestOldPaths(shuYing, manifest);
setProgress(shuYing, { title: "核心文件修复", status: "完成", current: coreFiles.length * 2, total: coreFiles.length * 2 });
}
catch (error) {
for (const file of coreFiles) {
try {
if (backupData[file]) {
await writeTargetFile(backupData[file], `extension/术樱包/${file}`);
}
else {
await removeFile(`extension/术樱包/${file}`);
}
}
catch (rollbackError) {
console.error("核心文件回滚失败:", file, rollbackError);
}
}
throw error;
}
}
finally {
for (const file of coreFiles) {
try {
await removeFile(`${coreTempDir}/${file}`);
}
catch (error) {
console.error("临时文件清理失败:", file, error);
}
}
}
}
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;
}
function makeLegacyModuleManifest(manifest, moduleId) {
const prefix = `${moduleId}/`;
const files = Object.fromEntries(Object.entries(manifest.files || {})
.filter(([file]) => file.startsWith(prefix)));
if (!Object.keys(files).length) return null;
const entry = `${moduleId}/register.js`;
return {
...manifest,
module_id: moduleId,
files,
core: files[entry] ? [entry] : [],
assets: Object.keys(files).filter(file => file != entry),
remove: [],
removeDirectories: [],
};
}
async function getInstalledModuleManifest(moduleId, version, source) {
const tag = { name: `v${version}`, version };
const oldRoot = await getManifestFromTag(tag, false, source);
if (oldRoot.modules?.[moduleId]) {
return await getModuleManifest(tag, oldRoot, moduleId, source);
}
return makeLegacyModuleManifest(oldRoot, moduleId);
}
async function getLegacyModuleManifest(moduleId, rootManifest) {
const path = rootManifest?.modules?.[moduleId]?.legacy_manifest;
if (!path) return null;
if (!isSafeManifestPath(path)) {
throw new Error(`${moduleId} 迁移清单路径无效`);
}
const response = await fetch(getRemoteUrl(
rootManifest.tag_name, path, rootManifest.source
));
if (!response.ok) throw new Error(`${moduleId} 迁移清单请求失败:${response.status}`);
const snapshot = await response.json();
if (snapshot.version != rootManifest.version
|| snapshot.module_id != moduleId
|| snapshot.snapshot != "legacy"
|| !Array.isArray(snapshot.files)) {
throw new Error(`${moduleId} 迁移清单格式错误`);
}
const files = uniqueManifestList(snapshot.files);
if (files.some(file => !file.startsWith(`${moduleId}/`))) {
throw new Error(`${moduleId} 迁移清单包含越界文件`);
}
return { files: Object.fromEntries(files.map(file => [file, {}])) };
}
async function isMengsanInstalled(localVersion, previousManifest,
currentManifest) {
const saved = lib.config.shuYing_mengsan_installed_version;
if (saved == "disabled") return false;
if (saved) return true;
const entryPath = "extension/术樱包/mengsan/register.js";
if (typeof game.checkFile == "function"
&& await safeCheckFile(entryPath) == 1) {
return true;
}
if (typeof game.checkFile != "function" && typeof game.readFile == "function") {
try {
await readFile(entryPath);
return true;
}
catch (error) { }
}
const oldManifest = previousManifest || (parseVersion(localVersion)
? await getManifestFromTag({
name: `v${localVersion}`, version: localVersion,
}, false, currentManifest.source).catch(() => null)
: null);
return !!oldManifest?.files?.["mengsan/register.js"];
}
function mergeRemovedFiles(manifest, oldFiles, retainedFiles = []) {
const current = new Set(getManifestFileKeys(manifest));
const retained = new Set(retainedFiles);
manifest.remove = [...new Set([...(manifest.remove || []),
...oldFiles.filter(file => !current.has(file) && !retained.has(file))])];
getManifestRemoveFiles(manifest);
}
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 = [];
const obsoleteBackups = [];
const obsoleteFiles = getManifestRemoveFiles(manifest);
let filesInstalled = false;
try {
ensureProgressNode(shuYing);
await createDir(`${newRoot}/`);
await createDir(`${oldRoot}/`);
await createFolders(`${newRoot}/`, files);
await createFolders(`${oldRoot}/`, [...files, ...obsoleteFiles]);
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);
}
for (const file of obsoleteFiles) {
const target = `extension/术樱包/${file}`;
try {
const original = await readFile(target);
await writeTargetFile(original, `${oldRoot}/${file}`);
backedUp.add(file);
obsoleteBackups.push(file);
}
catch (error) {
if (await safeCheckFile(target) == 1) throw error;
}
}
await removeManifestOldPaths(shuYing, manifest);
filesInstalled = true;
}
catch (error) {
const rollbackFiles = filesInstalled ? []
: [...installed, ...obsoleteBackups].reverse();
for (const file of rollbackFiles) {
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 new Set([...files, ...backedUp])) {
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;
for (const file of files) {
const target = `extension/术樱包/${file}`;
try {
await addLog(shuYing, `检查:${file}`);
setProgress(shuYing, { title: "查漏补缺", status: "检查本地文件", file: target, current: finished, total });
const exists = typeof game.checkFile == "function" ? await safeCheckFile(target) : 0;
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}`);
setProgress(shuYing, { title: "查漏补缺", status: "下载中", file: target, current: finished, total });
await downloadFile(file, target, manifest);
if (manifest?.files?.[file]) {
await addLog(shuYing, `校验通过:${file}`);
}
finished++;
await addLog(shuYing, `完成:${file}`);
setProgress(shuYing, { title: "查漏补缺", status: "已完成", file: target, current: finished, total });
}
catch (error) {
await addLog(shuYing, `失败:${file};${getErrorMessage(error)}`);
throw new Error(`${file} 处理失败:${getErrorMessage(error)}`);
}
}
}
async function getOnlineVersion(channel = getUpdateChannel(),
source = getUpdateSource()) {
const manifest = await getManifest(channel, source);
return { online_version: manifest.version, manifest };
}
async function checkVersion(shuYing, options = {}) {
try {
shuYing._updateLogs = [];
await addLog(shuYing, "版本检查开始");
const onlineVersion = await getOnlineVersion(options.channel, options.source);
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("目标更新源的版本落后于本地版本,请稍后重试");
}
let previousManifest = null;
if (parseVersion(localVersion) && localVersion != manifest.version) {
previousManifest = await getManifestFromTag({
name: `v${localVersion}`, version: localVersion,
}, false, manifest.source).catch(error => {
console.warn("读取本地旧版清单失败,跳过旧文件差集清理:", error);
return null;
});
}
const installedMengsan = await isMengsanInstalled(
localVersion, previousManifest, manifest
);
if (installedMengsan
&& !lib.config.shuYing_mengsan_installed_version
&& parseVersion(localVersion)) {
game.saveConfig("shuYing_mengsan_installed_version",
localVersion == manifest.version ? "legacy" : localVersion);
}
if (previousManifest) {
const retainedModuleFiles = manifest.modules?.mengsan
? getManifestFileKeys(previousManifest)
.filter(file => file.startsWith("mengsan/"))
: [];
mergeRemovedFiles(
manifest,
getManifestFileKeys(previousManifest),
retainedModuleFiles
);
}
const changed = await getChangedFiles(manifest);
const obsolete = await getObsoleteFiles(manifest);
let mengsanManifest = null;
let mengsanChanged = [];
let mengsanObsolete = [];
let mengsanUpdateError = null;
let previousMengsan = null;
if (installedMengsan && manifest.modules?.mengsan) {
try {
mengsanManifest = await getModuleManifest({
name: manifest.tag_name, version: manifest.version,
}, manifest, "mengsan");
const installedModuleVersion =
lib.config.shuYing_mengsan_installed_version;
if (installedModuleVersion
&& installedModuleVersion != "disabled"
&& parseVersion(installedModuleVersion)
&& installedModuleVersion != manifest.version) {
previousMengsan = await getInstalledModuleManifest(
"mengsan", installedModuleVersion, manifest.source
);
}
else if (previousManifest && previousManifest.modules?.mengsan) {
previousMengsan = await getModuleManifest({
name: `v${localVersion}`, version: localVersion,
}, previousManifest, "mengsan", manifest.source);
}
else if (previousManifest) {
previousMengsan = makeLegacyModuleManifest(
previousManifest, "mengsan"
);
}
if (!previousMengsan
&& (!installedModuleVersion || installedModuleVersion == "legacy")) {
previousMengsan = await getLegacyModuleManifest(
"mengsan", manifest
);
}
if (previousMengsan) {
mergeRemovedFiles(
mengsanManifest,
getManifestFileKeys(previousMengsan)
);
}
mengsanChanged = await getChangedFiles(mengsanManifest);
mengsanObsolete = await getObsoleteFiles(mengsanManifest);
}
catch (error) {
mengsanUpdateError = error;
await addLog(shuYing, `梦三模块更新暂不可用:${getErrorMessage(error)}`);
}
}
const allChangedCount = changed.length + mengsanChanged.length;
const allObsoleteCount = obsolete.length + mengsanObsolete.length;
await addLog(shuYing, `更新源:${manifest.source};标签:${manifest.tag_name};待更新文件:${allChangedCount};待删除文件:${allObsoleteCount}`);
if (localVersion == onlineVersion.online_version
&& !allChangedCount && !allObsoleteCount && !mengsanUpdateError) {
game.saveConfig("shuYing_online_version", onlineVersion.online_version);
alert("本地版本为最新版");
return onlineVersion.online_version;
}
const message = localVersion == onlineVersion.online_version
? `当前版本有 ${allChangedCount} 个文件需修复、${allObsoleteCount} 个旧文件需删除,是否继续?`
: `检测到最新版本为:${onlineVersion.online_version}, 本地版本为:${localVersion},是否更新?`;
if (!options.switchChannel && !confirm(message)) {
return null;
}
await updateAllFiles(shuYing, manifest, changed);
if (mengsanManifest && !mengsanUpdateError) {
try {
await updateAllFiles(shuYing, mengsanManifest, mengsanChanged);
game.saveConfig("shuYing_mengsan_installed_version", manifest.version);
}
catch (error) {
mengsanUpdateError = error;
await addLog(shuYing, `梦三模块更新失败:${getErrorMessage(error)}`);
}
}
game.saveConfig("shuYing_local_version", onlineVersion.online_version);
game.saveConfig("shuYing_online_version", onlineVersion.online_version);
await addLog(shuYing, "版本更新完成");
alert(mengsanUpdateError
? "主包更新完成;梦三模块未完成更新,可稍后重试。"
: "下载完成,重启生效");
return onlineVersion.online_version;
}
catch (error) {
console.error(error);
await addLog(shuYing, `版本更新失败:${getErrorMessage(error)}`);
alert("版本检测或下载失败,请检查网络或服务器配置。");
return null;
}
finally {
removeProgressNode(shuYing);
setBusy(shuYing, false);
}
}
async function repairMissingFiles(shuYing) {
if (typeof game.checkFile != "function") {
alert("此端暂不支持查缺补漏!");
setBusy(shuYing, false);
return;
}
try {
shuYing._updateLogs = [];
await addLog(shuYing, "查漏补缺开始");
ensureProgressNode(shuYing);
setProgress(shuYing, { title: "查漏补缺", status: "获取校验清单", current: 0, total: 1 });
const manifest = await getManifest();
const files = getManifestDownloadFiles(manifest);
const localVersion = lib.config.shuYing_local_version || "0.0.0.0";
let previousManifest = null;
if (parseVersion(localVersion) && localVersion != manifest.version) {
previousManifest = await getManifestFromTag({
name: `v${localVersion}`, version: localVersion,
}, false, manifest.source).catch(() => null);
if (previousManifest) {
const retainedModuleFiles = manifest.modules?.mengsan
? getManifestFileKeys(previousManifest)
.filter(file => file.startsWith("mengsan/"))
: [];
mergeRemovedFiles(
manifest,
getManifestFileKeys(previousManifest),
retainedModuleFiles
);
}
}
const installedMengsan = await isMengsanInstalled(
localVersion, previousManifest, manifest
);
let mengsanManifest = null;
let previousMengsan = null;
if (installedMengsan && manifest.modules?.mengsan) {
mengsanManifest = await getModuleManifest({
name: manifest.tag_name, version: manifest.version,
}, manifest, "mengsan");
const installedModuleVersion =
lib.config.shuYing_mengsan_installed_version;
if (installedModuleVersion == "legacy") {
previousMengsan = await getLegacyModuleManifest(
"mengsan", manifest
);
}
else if (installedModuleVersion
&& parseVersion(installedModuleVersion)
&& installedModuleVersion != manifest.version) {
previousMengsan = await getInstalledModuleManifest(
"mengsan", installedModuleVersion, manifest.source
);
}
else if (previousManifest?.modules?.mengsan) {
previousMengsan = await getModuleManifest({
name: `v${localVersion}`, version: localVersion,
}, previousManifest, "mengsan", manifest.source);
}
else if (previousManifest) {
previousMengsan = makeLegacyModuleManifest(
previousManifest, "mengsan"
);
}
else if (!installedModuleVersion) {
previousMengsan = await getLegacyModuleManifest(
"mengsan", manifest
);
}
if (previousMengsan) {
mergeRemovedFiles(
mengsanManifest,
getManifestFileKeys(previousMengsan)
);
}
}
await addLog(shuYing, `待检查文件数:${files.length}`);
await addLog(shuYing, `校验清单版本:${manifest.version || "未知"};清单文件数:${Object.keys(manifest.files || {}).length}`);
setProgress(shuYing, { title: "查漏补缺", status: "正在创建资源目录", current: 0, total: files.length });
await createFolders("extension/术樱包/", files, shuYing);
await downloadList(shuYing, files, manifest);
await removeManifestOldPaths(shuYing, manifest);
if (mengsanManifest) {
const moduleChanged = await getChangedFiles(mengsanManifest);
await updateAllFiles(shuYing, mengsanManifest, moduleChanged);
game.saveConfig("shuYing_mengsan_installed_version", manifest.version);
}
setProgress(shuYing, { title: "查漏补缺", status: "完成", current: files.length, total: files.length });
await addLog(shuYing, "查漏补缺完成");
alert("查漏补缺完毕!");
}
catch (error) {
console.error(error);
await addLog(shuYing, `查漏补缺失败:${getErrorMessage(error)}`);
alert("查漏补缺失败,请查看 extension/术樱包/log.txt");
}
finally {
removeProgressNode(shuYing);
setBusy(shuYing, false);
}
}
async function manageMengsanModule(shuYing) {
setBusy(shuYing, true);
try {
shuYing._updateLogs = [];
const localVersion = lib.config.shuYing_local_version || "0.0.0.0";
const rootManifest = await getManifest();
const installed = await isMengsanInstalled(
localVersion, null, rootManifest
);
if (installed) {
if (!confirm("确认卸载梦三模式?只删除梦三模块清单拥有的文件。")) return;
let moduleManifest;
const moduleVersion = lib.config.shuYing_mengsan_installed_version;
if (moduleVersion == "legacy" || !moduleVersion) {
const legacyManifest = await getLegacyModuleManifest(
"mengsan", rootManifest
).catch(() => null);
const currentManifest = await getModuleManifest({
name: rootManifest.tag_name, version: rootManifest.version,
}, rootManifest, "mengsan");
let localManifest = null;
if (parseVersion(localVersion)
&& localVersion != rootManifest.version) {
localManifest = await getInstalledModuleManifest(
"mengsan", localVersion, rootManifest.source
).catch(() => null);
}
moduleManifest = {
files: Object.fromEntries([...new Set([
...getManifestFileKeys(legacyManifest || { files: {} }),
...getManifestFileKeys(localManifest || { files: {} }),
...getManifestFileKeys(currentManifest),
])].map(file => [file, {}])),
};
}
else if (parseVersion(moduleVersion || "")) {
moduleManifest = await getInstalledModuleManifest(
"mengsan", moduleVersion, rootManifest.source
).catch(async error => {
await addLog(shuYing, `读取已安装模块清单失败:${getErrorMessage(error)}`);
return null;
});
}
if (!moduleManifest && parseVersion(localVersion)) {
moduleManifest = await getInstalledModuleManifest(
"mengsan", localVersion, rootManifest.source
).catch(() => null);
}
if (!moduleManifest) {
moduleManifest = await getModuleManifest({
name: rootManifest.tag_name, version: rootManifest.version,
}, rootManifest, "mengsan");
}
const files = getManifestFileKeys(moduleManifest);
if (typeof game.removeFile != "function") {
throw new Error("当前环境不支持安全卸载文件");
}
for (const file of files) {
await addLog(shuYing, `卸载梦三文件:${file}`);
await removeFile(`extension/术樱包/${file}`);
if (typeof game.checkFile == "function"
&& await safeCheckFile(`extension/术樱包/${file}`) == 1) {
throw new Error(`梦三文件删除失败:${file}`);
}
}
game.saveConfig("shuYing_mengsan_installed_version", "disabled");
alert("梦三模式文件已卸载,请重启游戏生效。");
return;
}
const moduleInfo = rootManifest.modules?.mengsan;
const sizeText = moduleInfo?.size_bytes
? `,约 ${formatBytes(moduleInfo.size_bytes)}` : "";
if (!confirm(`梦三模式尚未安装,是否下载并校验全部梦三资源${sizeText}?`)) return;
const moduleManifest = await getModuleManifest({
name: rootManifest.tag_name, version: rootManifest.version,
}, rootManifest, "mengsan");
const files = getManifestFileKeys(moduleManifest);
ensureProgressNode(shuYing);
setProgress(shuYing, {
title: "安装梦三模式", status: "准备下载", current: 0,
total: files.length * 2,
});
await updateAllFiles(shuYing, moduleManifest, files);
game.saveConfig("shuYing_mengsan_installed_version", rootManifest.version);
alert("梦三模式下载完成,请重启游戏生效。");
}
catch (error) {
console.error(error);
await addLog(shuYing, `梦三模块操作失败:${getErrorMessage(error)}`);
alert("梦三模式操作失败,请检查网络或 extension/术樱包/log.txt。");
}
finally {
removeProgressNode(shuYing);
setBusy(shuYing, false);
}
}
async function repairCoreFiles(shuYing) {
let shouldDownload = false;
let manifest;
let coreFiles;
try {
manifest = await getManifest();
coreFiles = getManifestCoreFiles(manifest);
}
catch (error) {
console.error(error);
alert("获取服务器校验清单失败,请检查网络或 manifest.json。");
setBusy(shuYing, false);
return;
}
if (typeof game.readFile != "function") {
shouldDownload = confirm("此端暂不支持本地资源检查。如有问题,点击确定重新下载核心文件。");
}
else {
try {
for (const file of coreFiles) {
await readFile(`extension/术樱包/${file}`);
}
shouldDownload = confirm("本地资源文件检测无误,若有问题点击确定重新下载主要js文件(包含关键js文件, 请勿关机、断网等操作,注意备份!)");
}
catch (error) {
console.error(error);
shouldDownload = confirm("本地资源不完整!点击确认重新获取!");
}
}
if (!shouldDownload) {
setBusy(shuYing, false);
return;
}
try {
await downloadCoreFiles(shuYing, manifest);
alert("下载完成,重启生效");
}
catch (error) {
console.error(error);
alert("核心文件下载或校验失败,请检查服务器文件与 manifest.json 是否一致。");
}
finally {
removeProgressNode(shuYing);
setBusy(shuYing, false);
}
}
export default {
getOnlineVersion,
getUpdateChannel,
getUpdateSource,
checkVersion,
repairMissingFiles,
repairCoreFiles,
manageMengsanModule,
};