逐行精读源码生成。行号基于当前工作区版本,仅用于快速定位。 覆盖文件:
src-tauri/src/services/game_launcher_service.rs(共 4636 行)。 「进程启动/检测/杀死」「磁盘空间」「渠道特征识别」不在本文件,位于src-tauri/src/commands/launcher.rs,下文关键流程中单独标注其行号。
文件职责:游戏启动器后端核心服务,负责远程版本检查、文件清单下载解密、安装/更新(补丁包优先 + 并发下载 + 断点续传 + MD5 校验 + 暂存原子应用)、完整性校验与修复、预下载(preload)与补丁包应用、渠道切换(5 阶段带回滚)、Banner/公告/背景媒体获取,并维护全局下载/切换取消标志与「活跃操作」状态。同一套链路同时服务《明日方舟:终末地》与《明日方舟》——两个游戏的接口、清单加密方式与补丁协议相同,差异(appcode、CDN 路径、exe 名、数据目录名)全部收敛在 GameChannel 里。
状态注册方式:lib.rs:379 创建 Arc<GameLauncherService> 并 app.manage(...) 注入 Tauri State;command(commands/launcher.rs)以 tauri::State<'_, Arc<GameLauncherService>> 直接取用。服务不再被一把外层锁包住:内部可变状态只有 active_operation: Arc<Mutex<Option<ActiveOperation>>>,全部方法都只需 &self,所以读写命令可以并发。历史上这里用 Arc<tokio::sync::Mutex<GameLauncherService>> 并把锁握满整段操作,于是一次几十分钟的下载会把公告、背景图、状态查询全部堵在锁上——前端刷新后启动流程的任务里正好有公告和背景图,于是卡死在「正在加载初始数据…」。需要互斥的只剩「会写安装目录」的长任务:launcher_install_or_update / launcher_verify_and_repair / launcher_preload_download / launcher_install_preload / launcher_switch_channel 先经 commands/launcher.rs 的 acquire_write_gate(svc) 取得 write_gate(容量 1 的 Semaphore)permit,permit 随函数结束自动释放;取消类接口(cancel_download 等)是模块级自由函数,command 直接调用,不取锁。
主要依赖:reqwest::Client(API 超时 30s、下载客户端连接 30s/读 120s,UA EndProtocol/0.1.0);futures_util::StreamExt(字节流);tokio::sync::{Mutex, Semaphore} + tokio::task::JoinSet(并发下载/校验);walkdir::WalkDir(目录遍历);zip::ZipArchive + 自定义 MultipartZipReader(多分卷 ZIP 解密解压);tauri::Emitter(事件 emit);crate::models::game::*(全部数据结构,定义在 models/game.rs);crate::utils::capture(send/send_stream,网络录制包装);crate::utils::hg_crypto(AES-256-CBC 解密、MD5/SHA-256);crate::utils::paths::app_data_dir();uuid::Uuid、chrono、serde_json、tracing。
以下结论来自对官方 Games.exe / hgdownloadsdk.dll / Patch.exe 的静态逆向与本机实测,代码按此实现:
| 官方行为 | 逆向坐标 / 证据 | 本服务的对应实现 |
|---|---|---|
cd_key 属于 patch 对象(不在 pkg 里) |
Games.exe!CLibNetworkUtils::ParseLatestGameJson @ 0x002f1e80;线上 get_latest_game 响应实测 |
PatchInfo::cd_key(pkg.cd_key 仅作兜底) |
补丁分卷是 WinZip AES(AE-2/AES-256)加密 ZIP,密码 = cd_key |
本机 7zg.exe + cd_key 解出包内全部文件且 MD5 与清单一致;错误密码失败;zip crate 复核一致 |
zip::ZipArchive::by_index_decrypt(..., cd_key.as_bytes()) |
官方用 7zg.exe x -y -o"…" "<分卷>" [-p"<cd_key>"] -qtw<w> -qtfw<w> 解压 |
Games.exe!CDecompressAssist::Start @ 0x002b47e0(-p"%1" @ 0x002b4a2f、模板 @ 0x002b4af0);日志 qep StartProcess … 7zg.exe + LogUnzipFinishEvent type:update_unzip |
extract_encrypted_archive(等价解密解压) |
包内 package_files / delete_files.txt / patch.json 是控制文件,不落盘;game_files 是正式文件 |
Games.exe!CCopyGameFile 跳过名单 @ 0x00278dc0、delete_files.txt 路径构造 @ 0x00277c50;实测官方安装目录只有 game_files |
ARCHIVE_CONTROL_FILES + apply_extracted_files 过滤;game_files 照常应用 |
官方按 delete_files.txt 删除旧文件 |
同上(拼出 <目录>\delete_files.txt) |
remove_deleted_files + parse_delete_list |
package_files 是本包文件清单(与 game_files 同格式) |
实测阿米娅安装目录那份 510,176 B / 3115 条,固定 AES 解密后为 {"path","md5","size"} NDJSON |
verify_archive_package_files(只校验解压目录里存在的条目,坏包不铺) |
预下载产物在正式更新时被直接消费(bUsePreDownloadZip) |
日志 cga QueryAllBrokenState … bUsePreDownloadZip:{} / ClearDownloadFilesIfNeed … bUsePreDownloadPath:{} |
download_parts_to_staging 按「同名 + MD5」从 .staging.preload.* 复用 |
常规更新走补丁包(下载分卷 → 解压 → 覆盖);官方历史日志里解压事件全部是 update_unzip |
日志 LogUnzipFinishEvent … vecFiles=[j9HZmer9RNws8Oly_1_6_4_1_6_3.zip.001](= patch.patches[0]);type: 取值统计 4/4 为 update_unzip |
install_or_update 优先 install_from_archive_parts |
差量包(含 hpatch _patch 条目)必须由 Patch.exe 结合本地 base 打补丁 |
Patch.exe!CPatchCotroller::DoPatch @ 0x000729f0;patch.json 的 patch[] 数组语义;线上实测:终末地 1.6.3→1.6.4 的 v2_patch_info_url 为空(整文件包),明日方舟 78.0.0→79.0.0 非空(差量包) |
选路阶段用 PatchInfo::is_delta(v2_patch_info_url 非空)识别差量包;解压阶段仍兜底检测 _patch 条目并返回 ERR_DELTA_ARCHIVE |
差量包之外,服务端还提供 patch.url:未加密的整补丁 zip,条目就是最终游戏文件 |
实测明日方舟 79.0.0 这份 3,518,344,483 B / 3616 条目 / 0 加密条目 / 0 _patch 条目,抽样 config.ini、WebviewConfig/WebPortalSign.gryph 的 MD5 与清单一致 |
install_from_whole_patch_zip(L2647):差量包场景下先下载明文整包铺盘,失败才回退清单逐文件 |
total_size 是"预留空间估计",不是解压后大小 |
实测倍率 1.66×~2.5×(补丁/整包各自不同) | 进度分母用实际解压条目之和,total_size 仅兜底 |
| 两个游戏共用同一条下载/更新协议 | 明日方舟 appcode GzD1CpaWgmSq1wew(官服/B 服同 appcode,靠 channel 1/2 区分),清单同样是"固定 AES 加密 NDJSON",ak.hycdn.cn 免签名直取(抽样实测 200 + Content-Length 与清单一致) |
GameChannel 承担游戏维度:appcode / exe 名(Arknights.exe)/ 数据目录(Arknights_Data)都从渠道取;下载、补丁、预下载、修复链路完全复用 |
| 区间 | 内容 |
|---|---|
| 1-17 | use 导入(std/fs/io/path/atomic、futures_util、reqwest、tauri、tokio、walkdir、zip、models、utils) |
| 19-46 | 模块级静态取消标志 DOWNLOAD_CANCELLED(L19)、SWITCH_CANCELLED(L20)、CANCEL_CLEANUP_REQUESTED(L22) 与暂存/归档常量(L25/27/29/31/42/44/46) |
| 49-101 | 归档辅助:is_archive_control_file(L49)、parse_delete_list(L63) |
| 103-122 | 5 个取消标志自由函数 |
| 125-210 | cleanup_staging_files(L125)、dir_has_files(L173)、has_download_cache(L181) |
| 213-306 | MultipartZipReader:把多个 ZIP 分卷当作一个可随机访问的归档读取器 |
| 309-528 | GameLauncherService 初始化、版本检查(get_latest_package / _with_cd_key) |
| 530-740 | check_status、scan_install_dir、本地版本解析、版本比较 |
| 741-870 | 清单下载/解密/解析、路径安全校验 |
| 872-956 | plan_updates(更新计划:小文件按 MD5、大文件按大小) |
| 958-1120 | 下载原语:download_file_with_resume、download_and_verify(保留但无调用方) |
| 1122-1582 | install_or_update(补丁包优先 → 回退清单逐文件)与 apply_staged_files |
| 1584-2046 | 完整性校验与修复:verify_and_repair |
| L2112 | 预下载:preload_download(只下载分卷,不解压) |
| L2370 | install_preload(取密钥后进入归档应用) |
| L2484 | install_from_archive_parts:分卷 → 解密解压 → 自检 → 应用 → 落状态 |
| L2647 | install_from_whole_patch_zip:差量包兜底,下载明文整补丁 zip → 解压 → 自检 → 应用 → 落状态 |
| L2760 | extract_plain_archive:明文整补丁 zip 解压(仍拒绝 _patch 条目) |
| L2870 | download_parts_to_staging:分卷并发下载与跨暂存复用 |
| L3024 | extract_encrypted_archive:WinZip AES 分卷解密解压 + 差量形态检测 |
| L3152 | apply_extracted_files:控制文件过滤、原子替换 |
| L3257 | remove_deleted_files:按 delete_files.txt 删除旧文件 |
| L3303 | read_archive_manifest:优先使用包内 game_files |
| L3324 | verify_archive_package_files:包内清单自检 |
| L3376 | 分卷命名、预下载暂存目录的创建/定位/清理 |
| 3140-3233 | payload 状态与预下载状态的读写(game_state/{channel}.json) |
| 3235-3732 | Web 批量代理、Banner/公告/侧边栏/背景媒体 |
| 3734-4061 | 渠道切换 switch_channel(5 阶段 + 回滚) |
| 4063-4328 | 模块级 subtract_download_progress(L4063) 与 download_single_file(L4073) |
| — | 文件内不包含进程启动/检测/杀死、磁盘空间、渠道 DLL 识别(见 commands/launcher.rs) |
| 字段 | 类型 | 含义 |
|---|---|---|
http_client |
reqwest::Client |
通用 API 客户端,超时 30s,UA EndProtocol/0.1.0 |
download_client |
reqwest::Client |
下载客户端:连接超时 30s、读超时 120s、不设总超时(支持大文件) |
active_operation |
Arc<Mutex<Option<ActiveOperation>>> |
当前活跃操作快照;仅 switch_channel 会写入/清空 |
write_gate |
Arc<Semaphore>(容量 1) |
只串行化「会写安装目录」的长任务:command 层 acquire_write_gate 取 permit 后再调用 install_or_update / verify_and_repair / preload_download / install_preload / switch_channel。不再用外层 Mutex<GameLauncherService> 把整个服务锁住——那会让下载堵死公告/背景图/状态查询 |
| 符号 | 位置 | 含义 |
|---|---|---|
DOWNLOAD_CANCELLED |
L19 | 全局下载/校验/归档应用取消标志 |
SWITCH_CANCELLED |
L20 | 切服取消标志,仅 switch_channel 各阶段检查 |
CANCEL_CLEANUP_REQUESTED |
L22 | 带路径取消时置位,服务在操作结束时据此删除续传暂存 |
INSTALL_STAGING_SUFFIX |
L25 | 清单安装暂存目录后缀 .staging.resume |
INSTALL_STAGING_VERSION_SUFFIX |
L27 | 暂存版本标记后缀 .staging.resume.version |
STAGED_REUSE_HASH_LIMIT |
L29 | 暂存小文件复用前重新校验 MD5 的尺寸上限(16 MB) |
PRELOAD_VERSION_MARKER |
L31 | 预下载暂存目录内的目标版本标记文件名 .preload_version |
ARCHIVE_CONTROL_FILES |
L42 | 包内控制文件名单:package_files / delete_files.txt / patch.json(不落盘) |
ARCHIVE_DELETE_LIST |
L44 | 包内删除清单文件名 delete_files.txt |
ERR_DELTA_ARCHIVE |
L46 | 差量包错误前缀 DELTA_ARCHIVE:,调用方据此回退清单路径 |
| 结构 | 关键字段 | 说明 |
|---|---|---|
RemotePackage (game.rs:358) |
version、resource_base_url、has_preload、preload_version、preload_parts、preload_total_bytes、preload_expanded_bytes、has_patch、patch_parts、patch_total_bytes、has_patch_whole |
远程版本、资源基址、补丁分卷与预下载分卷;不含解密密钥 |
PatchInfo (game.rs:244) |
cd_key、url、patches、v2_* |
版本补丁包;cd_key 是分卷 zip 密码(仅后端持有) |
PrePatch / PatchPart (game.rs:302 / 329) |
PrePatch:version、client_version、package_size、total_size、patches;PatchPart:url、md5、package_size |
预下载补丁包与分卷条目(pre_patch 无密钥) |
ManifestFile (game.rs:350) |
path、md5(32 hex)、size |
解密后 NDJSON 清单单条 |
FilePlan (game.rs:452) |
manifest、source_path |
本地已匹配则给源路径(可跳过下载) |
DownloadProgress (game.rs:385) |
downloaded、total、stage、current_file、file_index、file_count、verified_bytes |
安装/校验/预下载进度 payload |
InstallStage (game.rs:412) |
checking/comparing/downloading/verifying/repairing/applying/preloading/completed/error |
DownloadProgress.stage 取值 |
ActiveOperation (game.rs:112) |
type:installing/verifying/repairing/switching |
统一活跃操作枚举 |
GameStatus (game.rs:151) |
is_installed、has_update、local_version、remote_version、has_preload、preload_installable、preload_version、preload_total_bytes、preload_completed、active_operation |
check_status 输出 |
FileScanResult (game.rs:640) |
channel_detected、total_files、valid_files、corrupted_files、missing_files、existing_bytes、download_bytes、corrupted_file_list(≤20)、missing_file_list(≤20) |
scan_install_dir 输出 |
PayloadState (game.rs:459) |
channel、version、manifest_sha256、file_count、total_bytes、updated_at、preload_version?、preload_completed |
落盘的安装状态 JSON |
| 方法 | 位置 | 说明(输入 → 做什么 → 输出) |
|---|---|---|
is_archive_control_file(私有) |
L49 | 输入相对路径 → 判断是否包内控制文件(仅匹配包根下的 package_files/delete_files.txt/patch.json) → bool |
parse_delete_list(私有) |
L63 | 输入文本 → 按行解析相对路径,同时兼容 JSON 数组/对象形态 → Vec<String> |
pub fn cancel_download |
L103 | 无输入 → 置 DOWNLOAD_CANCELLED → 无返回 |
pub fn request_cancel_cleanup |
L108 | 无输入 → 置 CANCEL_CLEANUP_REQUESTED(须在 cancel_download 前调用) → 无返回 |
pub fn reset_download_cancel |
L112 | 无输入 → 复位下载取消标志 → 无返回 |
pub fn cancel_switch |
L116 | 无输入 → 置 SWITCH_CANCELLED → 无返回 |
pub fn reset_switch_cancel |
L120 | 无输入 → 复位切服取消标志 → 无返回 |
pub fn cleanup_staging_files |
L125 | 输入 install_path → 删除 {path}.switch.download、父目录下 {stem}.staging.*/{stem}.switch.*/{stem}.backup.* 目录、安装目录内所有 .download 文件 → 无返回(失败忽略) |
dir_has_files(私有) |
L173 | 输入目录 → 递归判断是否存在文件 → bool |
pub fn has_download_cache |
L181 | 输入 install_path → 扫父目录 {stem}.staging.* 与安装目录内 .download 文件 → bool |
subtract_download_progress(私有) |
L4371 | 输入进度计数与字节数 → 饱和回退(重试/服务端忽略 Range 时修正计数) → 无返回 |
async fn download_single_file(私有) |
L4381 | 输入共享 Client、url、dest_path、expected_md5、共享 dl_bytes、file_size、resume → 流式写盘 + 流式 MD5 校验;resume 为真时保留 .download 分片并按 Range 续传 → Result<(),String> |
| 方法 | 位置 | 说明(输入 → 做什么 → 输出) |
|---|---|---|
MultipartZipReader::new / Read::read / Seek::seek |
L221 | 将多个分卷文件作为一个可随机读取的 ZIP 输入流,无需拼接出完整归档副本 |
pub fn new |
L221 | 无输入 → 构造 API 客户端、下载客户端、空 active_operation 与容量 1 的 write_gate → Self |
pub fn write_gate |
L340 | 无输入 → 克隆 Arc<Semaphore>,供命令层串行化写安装目录的长任务 |
pub async fn has_active_operation |
L344 | 无输入 → 读锁判断 active_operation.is_some() → bool |
pub async fn set_active_operation |
L348 | 输入 Option<ActiveOperation> → 覆盖写入 → 无返回 |
pub async fn get_active_operation |
L352 | 无输入 → 克隆当前活跃操作 → Option<ActiveOperation> |
pub async fn get_latest_package |
L363 | 输入 channel 与可选本地版本 → 请求 get_latest_game 并只返回公开 RemotePackage(密钥不暴露给前端) |
async fn get_latest_package_with_cd_key(私有) |
L373 | 解析远端包、patch(分卷/整包/V2 字段)与 pre_patch,校验 URL 为 HTTPS → (RemotePackage, Option<String>);密钥取 patch.cd_key,pkg.cd_key 仅兜底;同时算出 patch_is_delta(v2_patch_info_url 非空)供上层选路 |
pub async fn check_status |
L548 | 输入 channel + install_path → 获取补丁与密钥;preload_installable = has_preload && cd_key 非空,可安装时把 has_update 置真 → GameStatus |
pub async fn scan_install_dir |
L620 | 输入 channel + install_path → 取远程清单并比对本地文件存在性/大小/MD5(核心文件判定用 channel.executable_name() 与 channel.data_dir_name()) → FileScanResult |
fn read_local_version(私有) |
L718 | 解密 config.ini 并解析版本行 → Result<String,_> |
fn versions_equal(私有关联 fn) |
L740 | 规范化两个版本串并按数字段比较 → bool |
pub async fn fetch_manifest |
L759 | GET {resource_base_url}/game_files,计算 SHA-256 并解密 NDJSON → (Vec<ManifestFile>, manifest_sha256) |
fn parse_manifest(私有关联 fn) |
L796 | 解析并校验清单路径、MD5、size 与条目数 → Vec<ManifestFile> |
fn validate_path(私有关联 fn) |
L849 | 拒绝绝对路径、./.. 路径段及 Windows 保留名 → Result<(),String> |
async fn plan_updates(私有) |
L890 | 比对清单与本地文件并生成更新计划:先按「存在 + 大小一致」筛选,再对其中 ≤16MB 的文件并发计算 MD5,大文件仅按大小判断 → Vec<FilePlan> |
pub async fn install_or_update |
L1140 | 常规安装/更新:补丁包优先(加密分卷)→ 差量包兜底(明文整补丁 zip)→ 清单比对逐文件;输入 max_concurrent、resume |
fn apply_staged_files(私有) |
L1623 | 将清单暂存内容逐文件复制到目标目录 |
pub async fn verify_and_repair |
L1648 | 并发检查文件并重新下载损坏文件;resume 控制修复下载的分片保留;返回校验结果 JSON |
pub async fn preload_download |
L2112 | 只下载 pre_patch 分卷到版本标记暂存目录并逐卷校验 MD5(不解压、不应用,与官方预下载阶段一致) |
pub async fn install_preload |
L2370 | 取 patch.cd_key 后进入归档应用;pre_patch 缺失时退回 patch 分卷(正式发版后的兜底);遇到差量包时把 ERR_DELTA_ARCHIVE 转成可读提示(不泄漏内部前缀) |
async fn install_from_archive_parts(私有) |
L2484 | 加密分卷链路:分卷下载(含预下载复用)→ 解密解压 → 包内清单自检 → 控制文件过滤/delete 清单/原子替换 → 落状态 → 清理暂存 |
async fn install_from_whole_patch_zip(私有) |
L2647 | 差量包兜底链路:下载明文整补丁 zip(patch.url,可断点续传 + MD5)→ 解压 → 自检 → 应用 → 落状态;暂存 {install_path}.staging.wholepatch |
fn extract_plain_archive(私有关联 fn) |
L2760 | 解压明文整补丁 zip(不需要密码);仍拒绝 _patch / vfs_files/vfs_patch 条目 |
async fn download_parts_to_staging(私有) |
L2870 | 分卷并发下载;已存在且 MD5 一致跳过;可按「同名 + MD5」从 .staging.preload.* 直接搬用 |
fn extract_encrypted_archive(私有关联 fn) |
L3024 | 用 MultipartZipReader + cd_key 解密解压分卷 ZIP;发现 hpatch _patch/vfs_files/vfs_patch 条目即返回 ERR_DELTA_ARCHIVE |
fn apply_extracted_files(私有) |
L3152 | 应用解压产物:跳过控制文件、<目标>.hgtmp 原子替换、校验写入长度 |
fn remove_deleted_files(私有) |
L3257 | 读取 delete_files.txt(先试固定 AES 解密、再按明文)→ 删除安装目录中的对应文件 |
fn read_archive_manifest(私有关联 fn) |
L3303 | 读取包内 game_files 并解密解析为新版清单 → Option<(Vec<ManifestFile>, String)> |
fn verify_archive_package_files(私有关联 fn) |
L3324 | 读取包内 package_files(加密 NDJSON)→ 对解压目录里存在的条目逐条校验 MD5,坏包返回 Err → Result<usize,String>(返回校验条目数) |
fn preload_part_file_name(私有) |
L3376 | 根据分卷 URL 提取安全文件名,非法时回退到编号名 |
fn create_preload_staging(私有关联 fn) |
L3389 | 创建版本标记的预下载暂存目录 |
fn preload_staging_version(私有关联 fn) |
L3399 | 读取预下载暂存目录记录的目标版本 |
fn preload_staging_dir / preload_staging_dirs / cleanup_preload_staging(私有) |
L3405 | 定位(单个/全部,按修改时间新到旧)与清理预下载暂存目录 |
fn save_payload_state / load_payload_state / get_payload_state / save_preload_state |
L3448 | 读写 game_state/{channel}.json 与预下载状态(键就是渠道名,两个游戏天然分开) |
async fn batch_proxy_web(私有) |
L3554 | 批量代理请求并解析响应(含公告结构调试输出) |
pub async fn get_banners / get_announcements |
L3629 | 获取 Banner 与公告(各自带回落,见「公告内容回落」) |
async fn batch_proxy_web_raw(私有) |
L3668 | 批量代理并返回原始 serde_json::Value |
pub async fn get_notice_content / get_background_image / classify_media_type |
L3700 | 公告 + Banner + 侧边栏三合一请求(带回落)、背景媒体与媒体类型判定 |
async fn fetch_notice_parts(私有) |
L3767 | 按 kinds(get_banner / get_announcement / get_sidebar)发一次 batch_proxy_web_raw 请求 |
fn parse_notice_parts(私有关联 fn) |
L3797 | 从原始响应解析出 NoticeParts(banner / announcements / sidebars 三项) |
fn parse_sidebars(私有关联 fn) |
L3917 | 把 get_sidebar_rsp 的分组结构合并成扁平 Vec<SidebarItem>;解析失败只记日志并返回空表 |
pub async fn switch_channel |
L4051 | 输入 from_channel、to_channel、install_path、app → 计算清单差异、下载差异文件、备份并应用(备份含将被覆盖的 MD5 差异文件),支持取消回滚 → Ok(to_remote.version) / Err |
模块级另有私有结构体
NoticeParts(L66):banners/announcements/sidebars三项的解析容器,让三个取数入口共用同一套解析与回落逻辑。
方法总数 56:模块级 12(含 2 个归档辅助)、
MultipartZipReader3、GameLauncherServiceimpl 内 41(其中公开方法 19)。download_file_with_resume/download_and_verify仍保留在代码中,但没有任何调用方(死代码)。
get_latest_package_with_cd_key请求体:seq+proxy_reqs[{kind:"get_latest_game", get_latest_game_req{appcode, launcher_appcode, channel, sub_channel, version:<本地版本>}}]。- 必须上报本地版本:不带版本时服务端返回
patch: null、pre_patch: null(全新安装只能走清单逐文件);带版本时才可能给出补丁包与预下载包。 - 解析
pkg.file_path(资源根)、patch(分卷 +cd_key+ V2 字段)、pre_patch(分卷,无密钥);资源、补丁、预下载 URL 均强制 HTTPS。 - 密钥取值顺序:
patch.cd_key→pkg.cd_key(历史兜底);只留在后端,RemotePackage不含密钥。
checking阶段 → 读取本地版本 → 带版本请求远端包。- 补丁包优先:
has_patch && patch_parts 非空 && !patch_is_delta && cd_key 存在→install_from_archive_parts(加密分卷 → 解密解压 → 自检 → 应用)。- 解压阶段仍会兜底检测
_patch/vfs_files/vfs_patch条目(ERR_DELTA_ARCHIVE)→ 回退下一档;其它错误直接失败(暂存按resume保留)。 - 没有密钥时只记警告并继续下一档(不静默失败)。
- 解压阶段仍会兜底检测
- 差量包兜底:
patch_is_delta && patch_whole_url 非空→install_from_whole_patch_zip(L2647):下载明文整补丁 zip →extract_plain_archive→ 自检 → 应用。失败再回退下一档。明日方舟的补丁都是这种 V2 差量包,靠这一档省掉按清单重下十几 GB。 - 清单路径:
fetch_manifest→plan_updates(先大小、再对 ≤16MB 文件算 MD5)→ 下载到固定暂存{install_path}.staging.resume(带.staging.resume.version标记)→ 全部成功后apply_staged_files应用 → 保存状态。- 这条路径与游戏无关:终末地清单 1688 条 / 63.6 GiB,明日方舟清单 14,020 条 / 24.27 GiB(实测官服 79.0.0),条目都是"游戏目录相对路径 + md5 + size",
{file_path}/{path}直取。
- 这条路径与游戏无关:终末地清单 1688 条 / 63.6 GiB,明日方舟清单 14,020 条 / 24.27 GiB(实测官服 79.0.0),条目都是"游戏目录相对路径 + md5 + size",
- 预下载产物(加密分卷)不在清单路径里按
manifest.path复用(官方产物也不是游戏文件);它的复用发生在加密分卷路径。 - 失败/取消时:
resume为真且未要求清理 → 保留暂存与分片供下次续传。
- 暂存:加密分卷用
{install_path}.staging.patch(分卷 +.extracted);明文整包用{install_path}.staging.wholepatch(patch.zip+.extracted)。 - 分卷复用顺序:本暂存已完整 →
.staging.preload.*里同名且 MD5 一致(rename,跨卷失败则copy+ 复验)→ 重新下载。 - 解压:
MultipartZipReader顺序拼接分卷 →ZipArchive::by_index_decrypt(cd_key);进度分母用实际条目解压总量;未加密条目会被自动按无密码读取(实测by_index_decrypt对明文条目忽略密码)。 - 自检:用包内
package_files核对解压产物 MD5(只校验存在的条目),失败即拒绝应用。 - 应用:跳过
ARCHIVE_CONTROL_FILES;每个文件先写<目标>.hgtmp,长度校验通过后rename覆盖;最后按delete_files.txt删除旧文件。 - 落状态:优先用包内
game_files(解密 NDJSON)作为新版清单与 SHA-256,缺失时回退远端fetch_manifest。 - 成功后清理补丁暂存与预下载暂存,并把预下载完成标记复位(
save_preload_state(channel, version, false))。
- 扫描安装清单中的缺失、大小不符及 MD5 不符文件,并并发下载修复;
quick=false时执行完整 MD5 校验。 - 修复下载同样使用
download_single_file,resume决定取消后是否保留.download分片。
preload_download:读取本地版本 → 取/复用pre_patch分卷信息 → 版本标记暂存目录 → 并发下载并逐卷 MD5 校验 → 写save_preload_state(completed=true);不做解压。- 阶段取值:进行中
stage = "preloading"(InstallStage::Preloading),终态completed/error;前端按stage === "preloading"把进度画在预下载按钮上。 - 可安装条件:
has_preload && cd_key 非空(即正式发版、patch下发后);后端install_preload会再次校验密钥,缺失即拒绝。 - 预下载分卷是 WinZip AES 加密的官方补丁分卷(
.zip.001…),与应用阶段消费的是同一批文件。
get_notice_content一次batch_proxy_web_raw请求带上get_banner/get_announcement/get_sidebar,分别解析为banners/announcements/sidebars(解析集中在parse_notice_parts,右侧栏由parse_sidebars解析)。parse_sidebars抹平官方数据的两种形态(pic.url+jump_url与sidebar_labels[0]),规则是「条目自身优先、缺失回退第一条 label」。
线上实测(web/batch_proxy,2026-11-08)各渠道三项内容条数:
| 渠道 | banner | 公告 tabs/条数 | 右侧栏 |
|---|---|---|---|
| 终末地 官服 | 9 | 3 / 20 | 9 |
| 终末地 B服 | 0 | 0 / 0 | 0 |
| 终末地 国际服 | 8 | 3 / 15 | 7 |
| 终末地 GooglePlay | 0 | 3 / 15 | 7 |
| 明日方舟 官服 | 8 | 2 / 9 | 7 |
| 明日方舟 B服 | 0 | 0 / 0 | 0 |
B 服三项全空(官服参数与 B 服参数都查过),因此取到空结果时用回落渠道补:
- 回落渠道由
GameChannel::notice_fallback_channel给出:B服 → 同游戏官服(bilibili→official、arknights_bilibili→arknights)、GooglePlay → Global;主渠道返回None。 - 三项各自独立判断:只补缺的那一项,不整份替换(万一某渠道只有公告没有右侧栏,另一项仍保留本渠道的内容)。
- 请求参数不做预先替换:先用渠道自己的 appcode/channel/sub_channel 请求,拿到空才回落到回落渠道再请求一次。
get_banners/get_announcements走同一套逻辑,各自只补自己那项。- 背景图(
get_main_bg_image)不走回落:实测 B 服背景图是有的(与官服不同的图),Global 与 GooglePlay 的背景图也不同,保持本渠道的。 - 回落请求失败只记日志(
safe_eprintln!),不覆盖已取到的内容、不让整个请求失败。
switch_channel 依次做游戏进程预检(Phase 0)、比较两渠道清单、下载差异文件、备份并移除旧渠道文件、应用新渠道文件、清理暂存;切换期间检查取消标志并尝试回滚。
Phase 0 进程预检:游戏进程在跑(含伴生组件占用文件)时 Phase 4 覆盖写入会拿到 os error 32,所以任何阶段开始前先用 process::exists_once 检查游戏进程,在跑即拒绝并返回可读错误。
终态事件:Phase 1 起(set_active_operation 已置切换态)的所有失败/取消路径都先 emit phase: "error" 的 launcher://switch-progress / launcher://progress 再返回 Err——前端 localActiveOp 只在收到终态或组件重挂时清空,缺这一步界面会永远卡在「切换中」。Phase 0 的拒绝发生在置位之前,不发终态。
两个游戏都支持换服(实测 2026-11-08,明日方舟官服 vs B 服清单 diff):
| 差异类别 | 文件数 | 体积 | 内容 |
|---|---|---|---|
| B 服独有 | 71 | 186 MB | BLPlatform64/(B 服渠道 SDK + CEF 内嵌浏览器) |
| 官服独有 | 4 | 1.9 MB | hgsdk.dll、devicehelper.dll、sdkdata/sdk_hgsdk_config.* |
| 同路径不同 MD5 | 32 | 520 MB | Arknights.exe、config.ini、U8SDK.dll、u8_channel.dll、GameAssembly.dll 等 |
即明日方舟换服一次约下载 706 MB、删除 1.9 MB;切换后 config.ini 的 channel 随包更新,launcher_detect_channel 的识别自动跟随。旧结论「明日方舟渠道差异不在清单里」系推断错误,已被实测推翻。
备份范围(Phase 3):备份「对方渠道没有的文件」+「将被覆盖且本地存在的 MD5 差异文件」(覆盖型只留副本不删原文件,删除型照旧移走)。反向切换时 Phase 2 可命中「Reusing backup」快速路径,官↔B 来回切第二次起只需下载对方独有的 ~186 MB。
应用失败回滚(Phase 4):任一文件覆盖失败(典型:os error 32 文件被占用)时整体回滚——已覆盖文件从备份还原、已删除文件从备份放回,目录回到 from_channel 完整状态,错误信息提示关闭占用程序后重试。
启动/检测/终止游戏、检测渠道、检查可执行文件和查询磁盘空间由对应 launcher command 处理,不属于本服务。
POST https://launcher.hypergryph.com/api/proxy/batch_proxy(国际服launcher.gryphline.com):请求get_latest_game,读取pkg.file_path、patch、pre_patch。POST .../api/proxy/web/batch_proxy:Banner、公告、右侧栏(get_sidebar)与背景信息。GET {resource_base_url}/game_files:加密清单;GET {resource_base_url}/{manifest.path}:安装/修复文件(免签名直取)。GET {patch.patches[].url}:补丁/预下载分卷(WinZip AES,需要cd_key)。- 要取到
patch/pre_patch,get_latest_game请求必须携带本地版本。
- 清单安装暂存:
{install_path}.staging.resume+ 版本标记{install_path}.staging.resume.version。 - 补丁归档暂存:
{install_path}.staging.patch(含.extracted解压目录)。 - 预下载暂存:
{install_path}.staging.preload.{uuid},含.preload_version标记与分卷;正式更新时被补丁路径复用。 - Payload 状态:
%LOCALAPPDATA%\cn.msk-network.endprotocol\game_state\{channel}.json。 - 普通下载临时文件为
{dest}.download;应用阶段临时文件为{dest}.hgtmp。 - 本服务不读写
app_config.json;前端设置保存在launcher_install_path、launcher_channel、launcher_verify_threads、launcher_download_threads(默认 8)、launcher_download_resume(默认 true)等 localStorage 键中。
- 服务内渠道切换发出
launcher://switch-progress(SwitchProgress)和launcher://progress(ActiveOperation)。 - 安装、校验、预下载通过回调由 command 发出
launcher-progress(DownloadProgress);前端监听封装在src/utils/launcherService.ts。
- 调用本服务:
launcher_check_status、launcher_install_or_update、launcher_verify_and_repair、launcher_get_remote_version、launcher_preload_download、launcher_install_preload、launcher_get_payload_state、launcher_get_banners、launcher_get_announcements、launcher_get_notice_content、launcher_get_background_image、launcher_scan_install_dir、launcher_switch_channel。 - 直接使用模块级下载/切换控制函数:
launcher_cancel_download、launcher_has_download_cache、launcher_reset_download_cancel、launcher_cancel_switch、launcher_cancel_all。