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 => ({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
}[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 = `
${escapeHTML(title)}
${current}/${total}
${escapeHTML(status)}
${percent}%
${escapeHTML(file)}
`;
}
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,
};