Skip to content

feat(update): 支持 MAA、MaaEnd 与 MFW 家族选择全量或增量更新 - #1949

Merged
youngslin merged 5 commits into
AUTO-MAS-Project:devfrom
youngslin:feat/incremental-update-mode-local
Nov 13, 2026
Merged

youngslin merged 5 commits into
AUTO-MAS-Project:devfrom
youngslin:feat/incremental-update-mode-local

Conversation

@youngslin

@youngslin youngslin commented Nov 12, 2026 •

Copy link
Copy Markdown

摘要

  • MAA、MaaEnd、MFW、M9A、MSS 在接管更新旁新增“全量 / 增量”,默认全量;选择增量时提示风险,必要时回退全量。
  • MAA 复用官方 OTA 安装入口,支持 GitHub 匹配 OTA 和 Mirror酱增量;资源增量先合并为校验后的完整共享缓存。MFW 家族按选择控制 Mirror酱差量与 GitHub 区间下载,在安全边界内回退全量。
  • MaaEnd 专用更新器接入自建站字节增量,由 MAS 重建归档并核对独立从 GitHub 获取的官方整包大小、SHA256,再走现有可恢复安装器;无需 CDK。首次无可信旧包、版本对不匹配、站点不可用或校验失败时回退原有全量源。仅选择增量时保存下一次可复用的归档。
  • 增量生成与上传 Action 在配套数据仓库 AUTO-MAS-Copilot-Data #4,不在本 PR。MaaEnd 每架构/通道使用独立站点文件;源未部署时仍可回退全量。前端 API 类型通过生成器更新,只保留一个更新日志碎片。

审查修复

  • 合法资源基线带根目录或子目录 .gitignore 时,重建增量缓存采用与分发相同的忽略规则;保留未改文件及摘要校验,不再误触发全量。
  • MaaFW 识别真实 UpdateApplyError → PayloadError → MaaFWProjectUpdateError 链;登记前仅回退一次全量。真实坏指纹、取消、两种磁盘错误、登记后回调失败及全量请求仍返回坏增量均有回归;旧载荷内容全程保持不变。只隔离 HTTP 和磁盘故障,不替换构建、校验或登记逻辑。
  • MAA 成功入缓存后,持同架构/通道锁清理旧基线,保留最近三份和 24 小时内缓存,避免同日下载额度重复消耗;不动完整包、其他通道、安装目录待更新包或链接指向的目录。测试覆盖最近三份及当天多实例缓存保护。
  • 以上故障先在原逻辑复现再修复;更新保存与执行结果继续复用已有遥测入口,不新增逐包事件。

测试与验证

  • Windows,项目锁定 Python 环境、Yarn 4、Edge;审查修复验证提交 c62ecffd557f5c3597eb5e53d7e79fc93fafa39f,已合并上游 dev 的 5dc79aebd 并解决三种语言与统计覆盖文档冲突。
  • 审查专项回归:uv run --no-sync python -m pytest tests/task/test_maa_fluxdown_update.py tests/task/test_maafw_update_fallback.py -k 'delta_fallback_uses_real or resource_delta_rebuilds or prunes_old_baselines' -q -p no:cacheprovider --tb=short:9 passed。
  • 前端 corepack yarn test --run:358 passed;corepack yarn typecheck、lint、format:check 通过,lint 仅有原有 QueueDeleteButton 属性默认值警告。ruff format --check .、相关文件 Ruff 检查、uv lock --check、更新日志碎片与 diff 检查通过。check_reverts 仅提示增量接口签名与夹具的对应调整;同日新版突破旧额度限制的原回归仍保留并通过。
  • corepack yarn e2e --grep 'package mode persists':2 passed,隔离配置、不填写 CDK,覆盖 MAA/MaaEnd 禁用状态、选择增量、保存、风险提示、刷新保留。产出截图、HTML 报告和 trace;corepack yarn e2e:check 通过。
  • 此前真实官方 x64 ZIP v3.1.0-beta.4 → v3.1.0-beta.5:两包核对官方 SHA256 后,用数据仓库生成器制包,再用本 PR 的客户端解码。全量 316,003,772 字节,增量 11,172,712 字节,节省约 96.5%;重建的官方整包 SHA256 为 05b516305872c8b8a41fa0235d5ecfedef0157d1822dbe7147cc3112c00477a4。
  • 上述重建包经 MAS 真实 apply_update 入口安装到独立临时目录,安装回执、目标版本/架构及用户配置保留均通过;没有启动 MaaEnd 或游戏,没有修改用户实际安装。
  • 新增回归随实现提交:MaaEnd 站点有效增量、损坏包/基线不匹配回退、取消不触发全量,及前一提交的 MAA OTA、软件缓存、资源合并等场景。

已知验证边界

  • 本次完整后端回归(c62ecffd5,Windows):uv run --no-sync python -m pytest tests -q -p no:cacheprovider --tb=short:2076 passed、1 skipped、1 failed,17 subtests passed。唯一失败仍为 tests/utils/test_fluxdown_detect_managed_priority.py::test_incomplete_managed_fluxdown_falls_back:本机完整 res/fluxdown-cli 优先于夹具的外部目录。此前未修改上游 5aa609284 独立工作树在相同工作目录已复现同一失败;该检测实现及测试从 5aa609284 到本次合并的 5dc79aebd 均无变化,本 PR 也未修改。没有新增测试失败。
  • 此前原生 Electron 开发版真实 Computer Use 验证通过:使用 Windows 桌面工具实际操作 MaaEnd 页面,确认接管关闭时更新方式禁用、开启后可切换全量/增量、增量风险与回退提示正确显示、切回全量时提示消失;刷新页面及隔离后端重启后,选择仍保留。只操作 36174 端口的隔离测试数据,正式 MAS 队列始终为空;未启动游戏或真实账号任务。本地已保存全量/增量两张桌面截图。
  • 尚无 arm64 真实包和正式下载站发布/回读证据。数据仓库 README 已列出独立文件、Access Key 和四个文件 ID 变量的启用要求;本次不合并、不触发生产发布。

选项名称已统一为“全量更新 / 增量更新”,默认仍为全量;文案调整后类型检查、格式检查与两个更新设置组件测试通过。

@github-actions github-actions Bot added 模块: 更新 按更新日志碎片自动设置 类型: 新增 按更新日志碎片自动设置 labels Nov 12, 2026
Comment on lines +490 to 500
raise _DownloadError("资源增量基线已失效,必须重新下载全量包")
complete = stage_tmp / "complete"
shutil.copytree(_STAGE_DIR / "resource", complete)
merge_package_tree(root, complete)
root = complete
expected = len(resource_tree_hashes(root))
else:
expected = count_zip_entries(zip_path, prefix=prefix)
file_hashes = resource_tree_hashes(root)
_MANIFEST_FILE.unlink(missing_ok=True)
if _STAGE_DIR.exists():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Discrepancy in counting .gitignore files during incremental stage validation causes _DownloadError and forces unexpected fallbacks to full downloads.
Severity: MEDIUM

Suggested Fix

Align file counting between expected and actual. Either ignore .gitignore files when calculating actual via rglob, or exclude .gitignore during copytree when building the incremental stage tree.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: app/task/MAA/tools/resource_update.py#L484-L500

Potential issue: In `_build_stage`, the expected file count for incremental packages is
calculated via `len(resource_tree_hashes(root))`, which ignores `.gitignore` files.
However, `actual` is calculated by counting all files in `_STAGE_DIR / 'resource'`,
including `.gitignore` files copied from the baseline tree during `copytree`. If
`.gitignore` files exist in the baseline directory, `actual` exceeds `expected`, raising
`_DownloadError` and causing incremental updates to fail and fall back to full package
downloads every time.

Did we get this right? 👍 / 👎 to inform future reviews.

Comment on lines +81 to 91
) -> Path:
"""本体更新共享缓存目录:按系统、架构、通道隔离,互不覆盖。"""
return Path.cwd() / "data" / "maa_update" / "software" / os_name / arch / channel
root = Path.cwd() / "data" / "maa_update" / "software" / os_name / arch / channel
return (
root / ("base-" + hashlib.sha256(base_version.encode()).hexdigest()[:16])
if base_version
else root
)


def load_cache_manifest(cache_dir: Path) -> dict[str, str] | None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Versioned base-* cache directories for incremental software updates are never cleaned up, resulting in unbounded disk usage.
Severity: LOW

Suggested Fix

Implement a cleanup routine to remove obsolete base-* directories within {channel} when a new software package is cached or after an update completes successfully.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: app/task/MAA/tools/software_package.py#L76-L91

Potential issue: `software_cache_dir` creates versioned cache directories prefixed with
`base-<hash>` when a base version is specified. Because there is no pruning mechanism or
cleanup routine for old `base-*` directories, each incremental update leaves past
package caches behind, causing cumulative unbounded disk space consumption over multiple
updates.

Also affects:

  • app/task/MAA/tools/software_update.py:466~491
  • app/task/MAA/tools/software_package.py:287~298

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Copy Markdown
Contributor

总体结论:修改后合并

AUTO-MAS-Project/AUTO-MAS #1949

宝宝,这次没有另起一套安装器,MAA 继续交给官方更新入口,MFW 家族也沿用了现有的新版本构建与登记流程,这个边界保留得好喵。不过增量路径还漏了两处:一处把合法资源缓存误判成损坏,另一处没接到真正需要回退全量的异常,先补好再合啦。

配置和前端负责保存全量/增量偏好;MAA 软件模块准备更新包,资源模块先拼出完整缓存再分发;MaaFW 更新模块则在临时目录构建、校验新版本,通过后才登记并切换。下面两项都是静态源码推演,未执行更新或复现。

MAA 资源更新

[逻辑 · P2] 基线含 .gitignore 时,合法增量会被误判损坏

位置:resource_update.py:487–514

启动/预取进入资源更新后,_build_stage 用 copytree 复制完整基线,再覆盖增量。这里会保留基线中的 .gitignore,但新增的 expected = len(resource_tree_hashes(root)) 不统计它,后面的 actual 却统计所有文件,所以两者必然不相等。

这不是假设缓存里随便多放了文件:官方 MaaResource 的资源目录确实包含 .gitignore。先使用 GitHub 全量资源包,再切换 Mirror 酱并选择增量,就能复用这样的合法基线。现有基线校验同样忽略 .gitignore,不会提前拦住它;原全量流程按 ZIP 条目计数,也能正常通过。

结果是先下载增量,再报“暂存树不完整”并重新下载全量;全量回退也失败时,本轮资源更新失败。旧安装目录的保护仍在,不是安装内容被破坏,但这条路径省流量的目的就落空啦。

请在资源暂存模块统一文件集合:复制基线时同样排除 .gitignore,或让预期数和实际数采用一致规则,同时保留内容摘要校验。现有增量重建测试加一个含 .gitignore 的有效基线,确认增量成功、未改文件保留且没有请求全量,就能抓住这只漏网 bug。

MaaFW / M9A / MSS 更新

[逻辑 · P2] 新增全量回退只看一层异常,接不到差量校验失败

位置:updater.py:860–864

选择优先增量、通过 Mirror 酱取得差量包后,若包的基线版本、指纹或文件哈希校验失败,手动和自动更新都会走到 apply_maafw_project_update → build_from_package。底层抛出的 UpdateApplyError 先被 payloads.py:761–768 包成 PayloadError,再被 updater.py:1797–1814 包成 MaaFWProjectUpdateError。

因此,新增判断看到的直接 __cause__ 是 PayloadError,不是 UpdateApplyError,不会进入全量回退,而是直接结束更新。当前版本仍受临时目录和登记前保护,不会因此损坏;但用户得手动切全量重试。这里指出的是本次新增回退分支未接通,原有校验拒绝坏差量本身没有错,别为了回退放宽校验喵。

请由构建/应用边界保留明确的可回退错误类别,或准确识别现有异常链,继续排除取消、登记后错误及无关磁盘错误,不要一律重试。补一个从真实 build_from_package 校验失败开始的测试,确认只回退一次全量且旧版本保持不变;只模拟一层异常包装会漏掉实际调用链。

已读取现有 CI 日志:Ubuntu 为 1973 passed、39 skipped,Windows 为 2011 passed、2 skipped,两者另有 17 subtests。它们不能替代上述缺失场景;dot酱未独立运行项目代码、测试或真实安装验证。

—— dot酱拍拍爪敬上 🐾

@youngslin youngslin changed the title feat(update): 支持 MAA 与 MFW 家族选择全量或增量更新 feat(update): 支持 MAA、MaaEnd 与 MFW 家族选择全量或增量更新 Nov 13, 2026

Copy link
Copy Markdown
Contributor

总体结论:可以合并

宝宝,上一轮两处问题都补好了喵:资源增量复制已统一排除 .gitignore;MFW 全量回退也接到了真实异常链,而且保留了取消、磁盘故障和登记后的保护,没有为了重试放宽校验。

新增缓存清理也核对过了:与取包、复制共用通道锁,保留近期和当前缓存,不会顺手删掉安装目录里的待更新包。合入 dev 后的配置衔接未发现新的必须修改项,这次可以放行啦。

dot酱只做了静态复核,未执行测试或真实安装更新。

—— 把增量回退这条线检查完的 dot酱敬上 🐾

@youngslin
youngslin merged commit ad3de27 into AUTO-MAS-Project:dev Nov 13, 2026
9 checks passed
qiyinxi added a commit that referenced this pull request Nov 14, 2026
release 分支没有 dev 上「差量构建失败回退全量」的通用回退(#1949),这里只针对
DeltaNativeRelocationError 补一次:新版本尚未登记、当前版本未动,按全量包重新更新。
qiyinxi added a commit that referenced this pull request Nov 14, 2026
release 分支没有 dev 上「差量构建失败回退全量」的通用回退(#1949),这里只针对
DeltaNativeRelocationError 补一次:新版本尚未登记、当前版本未动,按全量包重新更新。

(cherry picked from commit 59c61b2)
qiyinxi added a commit that referenced this pull request Nov 14, 2026
## 摘要
- 用户报障:MAA_bbb 发行包同时带 `maafw/`(5.14.3)和 `runtimes/win-x64/` 两份原生库,导入时只留了
`maafw/`。v1.13.20 只升级了 `runtimes/win-x64/`(5.16.0),Mirror 酱差量包里只有改过的十几个
DLL。runner 按版本取高换到 `runtimes/win-x64`,opencv、onnxruntime
等没改过的依赖都不在,每次运行都报 `Could not find module
...\runtimes\win-x64\MaaFramework.dll (or one of its dependencies)`。
- 差量包套用完后,如果 runner 要用的原生库目录换到了旧版本里没有的地方,就抛
`DeltaNativeRelocationError`(原因链上是 `UpdateApplyError`),在登记前由 #1949
的回退改要全量包。
- 预检在独立子进程里按 runner 的方式加载一次新版本的
`MaaFramework.dll`。确定加载不了就放弃这次更新、继续用当前版本;检查本身出问题(起不来、超时、非 Windows)一律放行。

## 测试与验证
- 自动化测试:`.venv\Scripts\python.exe -m pytest tests -q`:2087 passed、1
skipped(跑完后只给错误单独定义了类型、去掉报错末尾的句号,之后 MaaFW 相关测试重跑通过)。改动文件 `ruff format` /
`ruff check` 通过。
- 测试提交:没有随 PR 提交测试。本地回归用例按问题包实际布局造了一个只有 `maafw/` 的当前版本和只带
`runtimes/win-x64`
改动文件的差量包:去掉修复时只下差量包、新版本缺依赖(红),修复后先下差量、再下全量,新版本原生库目录完整(绿)。预检的加载检查另有一条
Windows 专属用例。
- 环境与提交:`15fffce99`,Windows 11,Python 3.12(项目锁定环境)。
- 未覆盖范围:没有在真实 MAS 上走一遍 MAA_bbb v1.13.18 → v1.13.20 的 Mirror 酱差量更新。
- 作者本机真实流程:预检加载检查用 MAA_bbb 自带的真实 MaaFramework 5.14.3 原生库试过:完整目录放行;只留
`MaaFramework.dll` + `MaaToolkit.dll` 时被拒,报错与用户日志一致。
youngslin added a commit that referenced this pull request Nov 14, 2026
## 摘要

- MaaEnd 增量更新优先复用 MFW 的 HTTP Range 读取器,直接从官方全量 ZIP 重建原始 ZIP;复用的是读取器,不是
MFW 文件树安装流程。公共重建工具没有宿主依赖,本次不为其他专项开启该能力。
- GitHub 路径不限起始版本,以实际读取量 40%、变化条目 3000 和网络条件决定是否回退;失败后尝试 [数据仓库
#4](AUTO-MAS-Project/AUTO-MAS-Copilot-Data#4) 的
Action 增量,最后沿用全量下载。默认仍为全量,停止操作不会触发备用下载。
- Action 备用包覆盖目标之前最多三个正式版;beta 必须有同号正式基准。客户端保留最近安装 ZIP 和最近正式基准,避免 beta
更新覆盖后续备用包需要的基准;两路都必须通过独立 GitHub 官方整包大小和 SHA256,之后才交给原有事务安装器。
- 说明见 [MaaEnd
双路径增量更新](https://github.com/youngslin/AUTO-MAS/blob/feat/maaend-multi-base-delta/docs/maaend-incremental-update.md),包含范围、限制、缓存、重复/删除/回退文件及发布边界。保存与执行复用现有遥测结果入口,不新增逐区间事件。

## 路径与限制

| 路径 | 允许的起点 | 失败后的处理 |
| --- | --- | --- |
| GitHub Range | 任意跨度的可信旧缓存 ZIP,包括跨多个 beta;不需要版本范围索引 | 再试 Action 自建站包 |
| Action 正式版 | 如 `3.0.0 / 2.9.0 / 2.8.0 → 3.1.0`;更早版本不使用本站增量 | 全量 |
| Action beta | `3.1.0` 正式版或 `3.1.0b1…b7 → b8`,实际重建使用缓存的 `3.1.0` 原始 ZIP
| 缺同号正式版、缺可信正式缓存、站点未发布或不匹配时全量 |

GitHub 只接受范围正确且完整的 206;返回 200 不读取整包响应体。实际读取预算包含头、中央目录和对齐开销,超过整包 40%
即退出;变化条目超过 3000 也退出。单次网络超时 30 秒。CDK 提供完整包备用源时,复用 MFW 网速保护,以 40% 读取上界保守估算
120 秒预算、180 秒实际网络耗时上限;没有较快备用源时不以这两项阈值打断持续传输的直连。

ZIP
是完整目标快照,不在当前安装上逐版叠补丁。重复数据复用,删除、改名和内容回退以目标快照为准;用户配置仍由现有安装器保护。CRC/大小只挑选复用候选,不能替代最终整包
SHA256;压缩字节不同导致 SHA256 不一致时丢弃重建结果。缓存最多两份原始
ZIP,约占两个整包大小;无缓存时允许先全量一次,不额外下载正式基准来伪装节省流量。

## 验证

- Windows,锁定 Python 环境;实现提交
`57651c30569e247731b9aa762c7480dfe7e6ee24`,基于上游 `70376570f`。
- 真实官方 x64 `v3.1.0-beta.4 → beta.5`:官方完整 ZIP **316,003,772** 字节,Range
实际读取 **38,917,565** 字节(约节省 **87.7%**),复用 **3035** 个条目、取回 **11** 个变化条目。重建
SHA256
与官方完全一致:`05b516305872c8b8a41fa0235d5ecfedef0157d1822dbe7147cc3112c00477a4`。原型耗时
73.9 秒;生产读取路径再次复现同一结果。
- 同一真实包由数据仓库新生成器产生 **11,172,784** 字节 v2 包,再由主程序实际 v2 解码器重建,亦通过上述官方
SHA256。此项是跨仓库字节协议验证,未绕过正式发布规则去声称没有同号正式基准也能使用 Action。
- Range 重建包通过真实 `apply_update`
入口安装到独立临时目录,目标版本、安装回执和用户配置保留通过;不启动游戏、不更改用户实际安装。
- 相关回归 **107 passed**:真实更新入口覆盖跨 beta/旧正式版的 Range、文件删除/回退/复用、200
响应后自建站接续、正式基准跨 beta 保留、损坏与错误基线回退、取消,以及既有 MFW 核心与预取回归。
- `ruff format --check .`、相关文件 Ruff、`uv lock --check`、diff
和更新日志检查通过。完整后端回归 `uv run --no-sync python -m pytest tests -q -p
no:cacheprovider --tb=short`:**2097 passed、1 skipped、1 failed,17
subtests passed**。唯一失败仍是此前已在未修改上游复现的
`tests/utils/test_fluxdown_detect_managed_priority.py::test_incomplete_managed_fluxdown_falls_back`,由本机已有完整
`res/fluxdown-cli` 优先于测试夹具外部目录触发;本 PR 未修改该实现或测试,没有新增失败。

## 发布边界

没有合并或触发站点生产发布;arm64 实包未验证。Action 仍需四个独立文件 ID 和授权密钥,旧 v1
发布包在新客户端上全量回退,旧客户端不识别 v2 也全量回退。

本次属于 #1949 同一 beta 未发布功能的补充,按仓库规则使用 `skip-changelog`,不重复增加碎片,不修改版本号或
CHANGELOG。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

模块: 更新 按更新日志碎片自动设置 类型: 新增 按更新日志碎片自动设置

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants