mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-09 18:40:19 +08:00
fixed # 补全部分卡牌
fixed # 修复部分已知BUG
This commit is contained in:
@@ -4,6 +4,7 @@ const giteaRepositoryApi = "https://gitea.diuse.work/api/v1/repos/Diuse/noname-s
|
||||
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);
|
||||
@@ -275,6 +276,10 @@ function validateManifest(manifest, version, moduleId = "") {
|
||||
getManifestAssetFiles(manifest, coreFiles);
|
||||
getManifestRemoveFiles(manifest);
|
||||
getManifestRemoveDirectories(manifest);
|
||||
if (moduleId && [...getManifestRemoveFiles(manifest), ...getManifestRemoveDirectories(manifest)]
|
||||
.some(file => !file.startsWith(`${moduleId}/`))) {
|
||||
throw new Error(`校验清单包含越界的 ${moduleId} 模块删除项`);
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
@@ -446,66 +451,206 @@ async function verifyFile(path, data, manifest) {
|
||||
}
|
||||
|
||||
function checkFile(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
game.checkFile(path, resolve, reject);
|
||||
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) {
|
||||
try {
|
||||
return await checkFile(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}`);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("检查文件失败,按缺失处理:", path, error);
|
||||
return -1;
|
||||
}
|
||||
|
||||
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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
game.readFile(path, resolve, reject);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
function removeFile(path) {
|
||||
return new Promise(resolve => {
|
||||
if (typeof game.removeFile != "function") {
|
||||
resolve();
|
||||
return;
|
||||
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);
|
||||
}
|
||||
game.removeFile(path, () => resolve());
|
||||
});
|
||||
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}`);
|
||||
}
|
||||
|
||||
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 })
|
||||
);
|
||||
});
|
||||
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 new Promise((resolve, reject) => {
|
||||
return fileOperation(`创建目录:${path}`, (resolve, reject) => {
|
||||
game.createDir(path, resolve, reject);
|
||||
});
|
||||
}
|
||||
@@ -555,79 +700,10 @@ async function removeManifestOldPaths(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
manifest = manifest || await getManifest();
|
||||
const files = getManifestCoreFiles(manifest);
|
||||
if (!files.length) throw new Error("校验清单缺少 core 核心文件列表");
|
||||
await updateAllFiles(shuYing, manifest, files);
|
||||
}
|
||||
|
||||
async function getChangedFiles(manifest) {
|
||||
@@ -697,15 +773,147 @@ async function getLegacyModuleManifest(moduleId, rootManifest) {
|
||||
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"
|
||||
&& await safeCheckFile(entryPath) == 1) {
|
||||
return true;
|
||||
if (typeof game.checkFile == "function") {
|
||||
return await safeCheckFile(entryPath) == 1;
|
||||
}
|
||||
if (typeof game.checkFile != "function" && typeof game.readFile == "function") {
|
||||
try {
|
||||
@@ -717,7 +925,7 @@ async function isMengsanInstalled(localVersion, previousManifest,
|
||||
const oldManifest = previousManifest || (parseVersion(localVersion)
|
||||
? await getManifestFromTag({
|
||||
name: `v${localVersion}`, version: localVersion,
|
||||
}, false, currentManifest.source).catch(() => null)
|
||||
}, false, currentManifest?.source).catch(() => null)
|
||||
: null);
|
||||
return !!oldManifest?.files?.["mengsan/register.js"];
|
||||
}
|
||||
@@ -748,17 +956,26 @@ async function getObsoleteFiles(manifest) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
async function updateAllFiles(shuYing, manifest, files) {
|
||||
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("当前环境不支持写入文件");
|
||||
}
|
||||
const newRoot = `${coreTempDir}/new`;
|
||||
const oldRoot = `${coreTempDir}/old`;
|
||||
// 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}/`);
|
||||
@@ -772,6 +989,7 @@ async function updateAllFiles(shuYing, manifest, files) {
|
||||
title: "版本更新", status: "下载并校验", file,
|
||||
current: i, total: files.length * 2,
|
||||
});
|
||||
staged.add(file);
|
||||
await downloadFile(file, `${newRoot}/${file}`, manifest);
|
||||
}
|
||||
|
||||
@@ -779,14 +997,14 @@ async function updateAllFiles(shuYing, manifest, files) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const target = `extension/术樱包/${file}`;
|
||||
try {
|
||||
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);
|
||||
}
|
||||
catch (error) {
|
||||
if (await safeCheckFile(target) == 1) throw error;
|
||||
}
|
||||
|
||||
setProgress(shuYing, {
|
||||
title: "版本更新", status: "写入中", file,
|
||||
@@ -797,22 +1015,21 @@ async function updateAllFiles(shuYing, manifest, files) {
|
||||
}
|
||||
for (const file of obsoleteFiles) {
|
||||
const target = `extension/术樱包/${file}`;
|
||||
try {
|
||||
if (await checkFile(target) == 1) {
|
||||
const original = await readFile(target);
|
||||
backupAttempts.add(file);
|
||||
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 ? []
|
||||
const rollbackFiles = filesInstalled || error.operationUncertain ? []
|
||||
: [...installed, ...obsoleteBackups].reverse();
|
||||
const failedRollback = [];
|
||||
for (const file of rollbackFiles) {
|
||||
const target = `extension/术樱包/${file}`;
|
||||
try {
|
||||
@@ -824,15 +1041,39 @@ async function updateAllFiles(shuYing, manifest, files) {
|
||||
}
|
||||
}
|
||||
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 {
|
||||
for (const file of new Set([...files, ...backedUp])) {
|
||||
await removeFile(`${newRoot}/${file}`);
|
||||
if (backedUp.has(file)) await removeFile(`${oldRoot}/${file}`);
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -908,12 +1149,6 @@ async function checkVersion(shuYing, options = {}) {
|
||||
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)
|
||||
@@ -938,32 +1173,7 @@ async function checkVersion(shuYing, options = {}) {
|
||||
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
|
||||
);
|
||||
}
|
||||
previousMengsan = await getPreviousMengsanManifest(manifest);
|
||||
if (previousMengsan) {
|
||||
mergeRemovedFiles(
|
||||
mengsanManifest,
|
||||
@@ -984,6 +1194,9 @@ async function checkVersion(shuYing, options = {}) {
|
||||
|
||||
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;
|
||||
@@ -999,8 +1212,7 @@ async function checkVersion(shuYing, options = {}) {
|
||||
await updateAllFiles(shuYing, manifest, changed);
|
||||
if (mengsanManifest && !mengsanUpdateError) {
|
||||
try {
|
||||
await updateAllFiles(shuYing, mengsanManifest, mengsanChanged);
|
||||
game.saveConfig("shuYing_mengsan_installed_version", manifest.version);
|
||||
await installMengsanFiles(shuYing, mengsanManifest, mengsanChanged, previousMengsan);
|
||||
}
|
||||
catch (error) {
|
||||
mengsanUpdateError = error;
|
||||
@@ -1068,35 +1280,7 @@ async function repairMissingFiles(shuYing) {
|
||||
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
|
||||
);
|
||||
}
|
||||
previousMengsan = await getPreviousMengsanManifest(manifest);
|
||||
if (previousMengsan) {
|
||||
mergeRemovedFiles(
|
||||
mengsanManifest,
|
||||
@@ -1115,8 +1299,7 @@ async function repairMissingFiles(shuYing) {
|
||||
await removeManifestOldPaths(shuYing, manifest);
|
||||
if (mengsanManifest) {
|
||||
const moduleChanged = await getChangedFiles(mengsanManifest);
|
||||
await updateAllFiles(shuYing, mengsanManifest, moduleChanged);
|
||||
game.saveConfig("shuYing_mengsan_installed_version", manifest.version);
|
||||
await installMengsanFiles(shuYing, mengsanManifest, moduleChanged, previousMengsan);
|
||||
}
|
||||
|
||||
setProgress(shuYing, { title: "查漏补缺", status: "完成", current: files.length, total: files.length });
|
||||
@@ -1138,93 +1321,65 @@ 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 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) {
|
||||
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}`);
|
||||
if (typeof game.checkFile == "function"
|
||||
&& await safeCheckFile(`extension/术樱包/${file}`) == 1) {
|
||||
throw new Error(`梦三文件删除失败:${file}`);
|
||||
}
|
||||
}
|
||||
game.saveConfig("shuYing_mengsan_installed_version", "disabled");
|
||||
alert("梦三模式文件已卸载,请重启游戏生效。");
|
||||
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)}` : "";
|
||||
if (!confirm(`梦三模式尚未安装,是否下载并校验全部梦三资源${sizeText}?`)) return;
|
||||
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,
|
||||
title: "安装梦三模式", status: "准备下载", current: 0, total: files.length * 2,
|
||||
});
|
||||
await updateAllFiles(shuYing, moduleManifest, files);
|
||||
game.saveConfig("shuYing_mengsan_installed_version", rootManifest.version);
|
||||
alert("梦三模式下载完成,请重启游戏生效。");
|
||||
await installMengsanFiles(shuYing, moduleManifest, files, previous);
|
||||
alert("梦三模式安装完成,请重启游戏生效。");
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
await addLog(shuYing, `梦三模块操作失败:${getErrorMessage(error)}`);
|
||||
alert("梦三模式操作失败,请检查网络或 extension/术樱包/log.txt。");
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user