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, };