Files
noname-shuying/update.js
T
Diuse 9e1732d2f8 fixed # 补全部分卡牌
fixed # 修复部分已知BUG
2026-10-04 19:32:38 +08:00

1450 lines
59 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";
let fileMutationUncertain = false;
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);
if (moduleId && [...getManifestRemoveFiles(manifest), ...getManifestRemoveDirectories(manifest)]
.some(file => !file.startsWith(`${moduleId}/`))) {
throw new Error(`校验清单包含越界的 ${moduleId} 模块删除项`);
}
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) {
assertLocalPath(path);
return fileOperation(`检查文件:${path}`, (resolve, reject) => {
if (lib.node?.fs && typeof window.__dirname == "string") {
lib.node.fs.stat(lib.node.path.join(window.__dirname, path), (error, stat) => {
if (error?.code == "ENOENT") resolve(-1);
else if (error) reject(error);
else resolve(stat.isFile() ? 1 : 0);
});
}
else if (hasCordovaFiles()) {
cordovaEntry(path).then(entry => resolve(entry.isFile ? 1 : 0), error => {
if (error.code == 1) resolve(-1);
else reject(error);
});
}
else if (typeof game.checkFile == "function") game.checkFile(path, resolve, reject);
else reject(new Error("当前环境不支持检查文件"));
});
}
async function safeCheckFile(path) {
// An unreadable file must never be treated as a missing file during replacement.
return await checkFile(path);
}
function assertLocalPath(path) {
if (!isSafeManifestPath(path) || !path.startsWith("extension/术樱包/")) {
throw new Error(`拒绝访问扩展目录外的路径:${path}`);
}
}
function operationError(value) {
if (!value) return null;
if (value instanceof Error) return value;
const error = value.target?.error || value.error;
if (error) return operationError(error);
if (typeof value == "string" || value.code || value.message) {
return Object.assign(new Error(value.message || (value.code ? `文件操作错误:${value.code}` : String(value))), { code: value.code });
}
return null; // A successful Cordova writeend event is not an error.
}
function assertMutationReady() {
if (fileMutationUncertain) {
const error = new Error("上次文件操作中断,结果尚未确认,请重启游戏后再修复或卸载");
error.operationUncertain = true;
throw error;
}
}
function fileOperation(label, start, mutation = false) {
if (mutation) {
try { assertMutationReady(); }
catch (error) { return Promise.reject(error); }
}
return new Promise((resolve, reject) => {
let settled = false;
const finish = (error, result) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (error) {
const failure = operationError(error) || new Error(`${label}失败`);
if (mutation && !lib.node && !hasCordovaFiles()
&& (failure instanceof TypeError || failure instanceof SyntaxError)) failure.operationUncertain = true;
if (mutation && failure.operationUncertain) fileMutationUncertain = true;
reject(failure);
}
else resolve(result);
};
const timer = setTimeout(() => {
const error = new Error(`${label}超时,请重启后检查或修复文件`);
error.operationUncertain = mutation;
if (mutation) fileMutationUncertain = true;
finish(error);
}, 60000);
try { start(result => finish(null, result), error => finish(error || new Error(`${label}失败`))); }
catch (error) { finish(error); }
});
}
function hasCordovaFiles() {
return !!lib.device && typeof window.resolveLocalFileSystemURL == "function";
}
function cordovaEntry(path, create = false) {
return fileOperation(`打开文件:${path}`, (resolve, reject) => {
const root = localStorage.getItem("noname_inited");
if (!root) throw new Error("无法确定游戏文件目录");
window.resolveLocalFileSystemURL(root, entry => {
try { entry.getFile(path, { create }, resolve, reject); }
catch (error) { reject(error); }
}, reject);
}, create);
}
function readFile(path) {
assertLocalPath(path);
return fileOperation(`读取文件:${path}`, (resolve, reject) => {
if (hasCordovaFiles()) {
cordovaEntry(path).then(entry => entry.file(file => {
try {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
}
catch (error) { reject(error); }
}, reject)).catch(reject);
}
else game.readFile(path, resolve, reject);
});
}
async function writeFile(data, path, name) {
const target = `${path.replace(/\/$/, "")}/${name}`;
assertLocalPath(target);
await fileOperation(`写入文件:${target}`, (resolve, reject) => {
if (hasCordovaFiles()) {
cordovaEntry(target, true).then(entry => entry.createWriter(writer => {
let truncated = false;
writer.onerror = () => reject(writer.error || new Error("写入失败"));
writer.onwriteend = () => {
if (writer.error) { reject(writer.error); return; }
if (truncated) { resolve(); return; }
truncated = true;
try { writer.write(new Blob([toUint8Array(data)])); }
catch (error) { reject(error); }
};
try { writer.truncate(0); }
catch (error) { reject(error); }
}, reject)).catch(reject);
}
else if (!lib.node) {
// The browser adapter omits fetch's rejection callback; use its API directly.
fetch("/writeFile", {
method: "post", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data: typeof data == "string" ? data : Array.from(toUint8Array(data)), path: target }),
}).then(response => {
if (!response.ok) throw new Error(`文件服务请求失败:${response.status}`);
return response.json();
}).then(result => {
if (result?.success) resolve();
else reject(new Error(result?.errorMsg || "文件服务写入失败"));
}).catch(error => {
// Losing the response cannot prove that the server stopped writing.
error.operationUncertain = true;
reject(error);
});
}
else game.writeFile(data, path, name, result => {
const error = operationError(result);
if (error) reject(error);
else resolve();
});
}, true);
const actual = toUint8Array(await readFile(target));
const expected = toUint8Array(data);
let equal = actual.length == expected.length;
for (let i = 0; equal && i < actual.length; i++) equal = actual[i] == expected[i];
if (!equal) {
throw new Error(`文件落盘校验失败:${target}`);
}
}
async function removeFile(path) {
assertMutationReady();
assertLocalPath(path);
const exists = await checkFile(path);
if (exists == -1) return;
if (exists != 1) throw new Error(`待删除路径不是文件:${path}`);
await fileOperation(`删除文件:${path}`, (resolve, reject) => {
if (hasCordovaFiles()) {
cordovaEntry(path).then(entry => entry.remove(resolve, reject)).catch(reject);
}
else {
if (typeof game.removeFile != "function") throw new Error("当前环境不支持删除文件");
game.removeFile(path, error => error ? reject(error) : resolve(), reject);
}
}, true);
if (await checkFile(path) != -1) throw new Error(`文件删除后仍存在:${path}`);
}
async function removeDir(path) {
assertLocalPath(path);
try {
await fileOperation(`删除目录:${path}`, (resolve, reject) => {
if (typeof game.removeDir != "function") throw new Error("当前环境不支持删除文件夹");
game.removeDir(path, resolve, reject);
}, true);
return { removed: true };
}
catch (error) {
if (error.operationUncertain) throw error;
return { removed: false, error };
}
}
function createDir(path) {
return fileOperation(`创建目录:${path}`, (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) {
manifest = manifest || await getManifest();
const files = getManifestCoreFiles(manifest);
if (!files.length) throw new Error("校验清单缺少 core 核心文件列表");
await updateAllFiles(shuYing, manifest, files);
}
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, {}])) };
}
const mengsanRecordKey = "shuYing_mengsan_installation";
const mengsanVersionKey = "shuYing_mengsan_installed_version";
function getMengsanRecord() {
const record = lib.config[mengsanRecordKey];
if (record == null) return null;
if (!["installed", "installing", "repair", "uninstalling", "disabled"].includes(record.status)
|| !Array.isArray(record.files)) throw new Error("梦三本地安装记录无效,请保留文件并检查日志");
const files = uniqueManifestList(record.files);
if (files.some(file => !file.startsWith("mengsan/"))) throw new Error("梦三安装记录包含越界文件");
if (record.status != "disabled" && !files.includes("mengsan/register.js")) {
throw new Error("梦三安装记录缺少模块入口");
}
return { ...record, files };
}
async function saveMengsanRecord(record) {
const version = record.status == "disabled" ? "disabled" : record.version || "legacy";
// Both keys commit together; saveConfig/putDB request success is too early.
lib.status.reload++;
try {
if (lib.db) {
await new Promise((resolve, reject) => {
let transaction;
let requestError;
try {
transaction = lib.db.transaction(["config"], "readwrite");
transaction.oncomplete = () => resolve();
transaction.onerror = event => { requestError = event.target?.error; };
transaction.onabort = () => reject(transaction.error || requestError || new Error("梦三安装记录未能保存"));
const store = transaction.objectStore("config");
store.put(record, mengsanRecordKey);
store.put(version, mengsanVersionKey);
}
catch (error) {
try { transaction?.abort(); } catch {}
reject(error);
}
});
}
else {
const key = `${lib.configprefix}config`;
const config = JSON.parse(localStorage.getItem(key) || "{}");
if (!config || typeof config != "object" || Array.isArray(config)) throw new Error("本地配置格式无效");
config[mengsanRecordKey] = record;
config[mengsanVersionKey] = version;
localStorage.setItem(key, JSON.stringify(config));
}
lib.config[mengsanRecordKey] = record;
lib.config[mengsanVersionKey] = version;
}
finally { game.reload2(); }
}
async function getPreviousMengsanManifest(rootManifest = null) {
const record = getMengsanRecord();
if (record && record.status != "disabled") {
return { files: Object.fromEntries(record.files.map(file => [file, {}])) };
}
const savedVersion = lib.config[mengsanVersionKey];
const receiptPath = "extension/术樱包/dist/modules/mengsan.json";
if (await checkFile(receiptPath) == 1) {
const receipt = JSON.parse(new TextDecoder().decode(toUint8Array(await readFile(receiptPath))));
if (parseVersion(receipt.version || "")
&& (!parseVersion(savedVersion || "") || receipt.version == savedVersion)) {
validateManifest(receipt, receipt.version, "mengsan");
if (!receipt.files["mengsan/register.js"]) throw new Error("本地梦三清单缺少入口");
// ZIP installs carry their own receipt, so first uninstall also works offline.
if (!parseVersion(savedVersion || "")) {
const legacyPath = "extension/术樱包/dist/modules/mengsan.legacy.json";
if (await checkFile(legacyPath) == 1) {
const legacy = JSON.parse(new TextDecoder().decode(toUint8Array(await readFile(legacyPath))));
if (legacy.version != receipt.version || legacy.module_id != "mengsan"
|| legacy.snapshot != "legacy" || !Array.isArray(legacy.files)) throw new Error("本地梦三迁移清单无效");
const owned = uniqueManifestList(legacy.files);
if (owned.some(file => !file.startsWith("mengsan/"))) throw new Error("本地梦三迁移清单包含越界文件");
return { files: Object.fromEntries([...getManifestFileKeys(receipt), ...owned].map(file => [file, {}])) };
}
}
return receipt;
}
}
let previous;
if (parseVersion(savedVersion || "")) {
// Never substitute the latest manifest for an unavailable installed version.
previous = await getInstalledModuleManifest("mengsan", savedVersion, rootManifest?.source || getUpdateSource());
}
else {
previous = await getLegacyModuleManifest("mengsan", rootManifest || await getManifest());
}
if (!previous || !getManifestFileKeys(previous).includes("mengsan/register.js")) {
throw new Error("无法确认已安装梦三模块的完整文件清单,未改动模块文件;请恢复对应版本清单后重试");
}
return previous;
}
async function installMengsanFiles(shuYing, manifest, files, previousManifest = null) {
assertMutationReady();
const previousRecord = getMengsanRecord();
const currentFiles = getManifestFileKeys(manifest);
const oldFiles = previousManifest ? getManifestFileKeys(previousManifest) : previousRecord?.files || [];
const operationRoot = newOperationRoot();
const pending = {
status: "installing", version: manifest.version, source: manifest.source,
files: uniqueManifestList([...oldFiles, ...currentFiles, ...getManifestRemoveFiles(manifest)]),
recoveryDirectory: operationRoot,
};
await saveMengsanRecord(pending); // Disable loading before the first filesystem mutation.
let filesApplied = false;
try {
await updateAllFiles(shuYing, manifest, files, operationRoot);
filesApplied = true;
for (const file of currentFiles) {
await verifyFile(file, await readFile(`extension/术樱包/${file}`), manifest);
}
await saveMengsanRecord({ status: "installed", version: manifest.version,
source: manifest.source, files: currentFiles });
}
catch (error) {
let recovery = { ...pending, status: "repair", recoveryDirectory: error.recoveryDirectory || null };
if (!filesApplied && !error.recoveryIncomplete && !error.operationUncertain) {
// A complete rollback may restore a previously committed installation.
if (previousRecord?.status == "installed" || previousRecord?.status == "disabled") recovery = previousRecord;
else if (!previousManifest && !oldFiles.length) recovery = { status: "disabled", files: [], version: "disabled" };
}
try { await saveMengsanRecord(recovery); }
catch (saveError) { console.error("安装中断记录仍保持待处理状态:", saveError); }
throw error;
}
}
async function isMengsanInstalled(localVersion, previousManifest,
currentManifest) {
const record = getMengsanRecord();
if (record) return !["disabled", "uninstalling"].includes(record.status);
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") {
return await safeCheckFile(entryPath) == 1;
}
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;
}
function newOperationRoot() {
return `${coreTempDir}/${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
async function updateAllFiles(shuYing, manifest, files, operationRoot = newOperationRoot()) {
assertMutationReady();
if (typeof game.writeFile != "function") {
throw new Error("当前环境不支持写入文件");
}
// Failed recovery directories must not be overwritten by a later operation.
const newRoot = `${operationRoot}/new`;
const oldRoot = `${operationRoot}/old`;
const backedUp = new Set();
const staged = new Set();
const backupAttempts = new Set();
const installed = [];
const obsoleteBackups = [];
const obsoleteFiles = getManifestRemoveFiles(manifest);
let filesInstalled = false;
let retainRecovery = 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,
});
staged.add(file);
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}`;
const exists = await checkFile(target);
if (exists == 0) throw new Error(`待替换路径不是文件:${file}`);
if (exists == 1) {
const original = await readFile(target);
backupAttempts.add(file);
await writeTargetFile(original, `${oldRoot}/${file}`);
backedUp.add(file);
}
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}`;
if (await checkFile(target) == 1) {
const original = await readFile(target);
backupAttempts.add(file);
await writeTargetFile(original, `${oldRoot}/${file}`);
backedUp.add(file);
obsoleteBackups.push(file);
}
}
await removeManifestOldPaths(shuYing, manifest);
filesInstalled = true;
}
catch (error) {
const rollbackFiles = filesInstalled || error.operationUncertain ? []
: [...installed, ...obsoleteBackups].reverse();
const failedRollback = [];
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) {
failedRollback.push(file);
console.error("文件回滚失败:", file, rollbackError);
}
}
retainRecovery = !!failedRollback.length || !!error.operationUncertain;
if (retainRecovery) {
error.recoveryIncomplete = true;
error.recoveryDirectory = operationRoot;
try {
await writeTargetFile(JSON.stringify({
version: manifest.version, files, installed, obsoleteBackups,
backedUp: [...backedUp], failedRollback,
operationUncertain: !!error.operationUncertain,
error: getErrorMessage(error),
}, null, 2), `${operationRoot}/recovery.json`);
}
catch (saveError) { console.error("恢复说明写入失败,旧文件仍保留:", operationRoot, saveError); }
await addLog(shuYing, `回滚未完成,恢复文件已保留在:${operationRoot}`);
}
throw error;
}
finally {
if (!retainRecovery) {
for (const file of new Set([...staged, ...backupAttempts])) {
try {
if (staged.has(file)) await removeFile(`${newRoot}/${file}`);
if (backupAttempts.has(file)) await removeFile(`${oldRoot}/${file}`);
}
catch (cleanupError) {
console.error("临时文件清理失败,不改变安装结果:", file, cleanupError);
break; // An unavailable backend must not cause one timeout per 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 (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");
previousMengsan = await getPreviousMengsanManifest(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) {
if (mengsanManifest && getMengsanRecord()?.status != "installed") {
await installMengsanFiles(shuYing, mengsanManifest, [], previousMengsan);
}
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 installMengsanFiles(shuYing, mengsanManifest, mengsanChanged, previousMengsan);
}
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");
previousMengsan = await getPreviousMengsanManifest(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 installMengsanFiles(shuYing, mengsanManifest, moduleChanged, previousMengsan);
}
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 = [];
assertMutationReady();
const record = getMengsanRecord();
const localVersion = lib.config.shuYing_local_version || "0.0.0.0";
const pendingInstall = record && ["installing", "repair"].includes(record.status);
const installed = record?.status == "uninstalling"
|| await isMengsanInstalled(localVersion, null, null);
if (installed && !pendingInstall) {
const message = record?.status == "uninstalling"
? "上次卸载未完成,是否继续卸载梦三模式?"
: "确认卸载梦三模式?只删除安装清单拥有的文件,保留存档。";
if (!confirm(message)) return;
// Cached ownership allows uninstalling without a network connection.
const previous = record ? { files: Object.fromEntries(record.files.map(file => [file, {}])) }
: await getPreviousMengsanManifest();
const files = getManifestFileKeys(previous);
const pending = {
status: "uninstalling", version: record?.version || lib.config[mengsanVersionKey] || "legacy",
source: record?.source || getUpdateSource(), files,
};
await saveMengsanRecord(pending);
ensureProgressNode(shuYing);
for (let i = 0; i < files.length; i++) {
const file = files[i];
setProgress(shuYing, { title: "卸载梦三模式", status: "删除并检查", file, current: i, total: files.length });
await addLog(shuYing, `卸载梦三文件:${file}`);
await removeFile(`extension/术樱包/${file}`);
}
await saveMengsanRecord({ status: "disabled", version: "disabled", files: [] });
alert("梦三模式文件已卸载,存档已保留,请重启游戏生效。");
return;
}
const rootManifest = await getManifest();
const moduleInfo = rootManifest.modules?.mengsan;
const sizeText = moduleInfo?.size_bytes ? `,约 ${formatBytes(moduleInfo.size_bytes)}` : "";
const message = pendingInstall
? `上次安装或更新未完成,是否重新下载并修复梦三模式${sizeText}?`
: `梦三模式尚未安装,是否下载并校验全部梦三资源${sizeText}?`;
if (!confirm(message)) return;
const moduleManifest = await getModuleManifest({
name: rootManifest.tag_name, version: rootManifest.version,
}, rootManifest, "mengsan");
const previous = pendingInstall ? await getPreviousMengsanManifest(rootManifest) : null;
if (previous) mergeRemovedFiles(moduleManifest, getManifestFileKeys(previous));
const files = getManifestFileKeys(moduleManifest);
ensureProgressNode(shuYing);
setProgress(shuYing, {
title: "安装梦三模式", status: "准备下载", current: 0, total: files.length * 2,
});
await installMengsanFiles(shuYing, moduleManifest, files, previous);
alert("梦三模式安装完成,请重启游戏生效。");
}
catch (error) {
console.error(error);
await addLog(shuYing, `梦三模块操作失败:${getErrorMessage(error)}`);
const status = lib.config[mengsanRecordKey]?.status;
const hint = status == "uninstalling" ? "卸载未完成,重启后梦三暂停加载,可再次点击此按钮继续卸载。"
: ["installing", "repair"].includes(status) ? "安装未完成,重启后梦三暂停加载,可再次点击此按钮修复。" : "模块文件未能完成操作。";
alert(`${hint}\n${getErrorMessage(error)}\n请查看 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,
};