Skip to content

feat(maafw): MFW 托管脚本类型(导入脱壳、共用运行环境、版本管理、远程更新、一键迁移) - #633

Open
qiyinxi wants to merge 13 commits into
AUTO-MAS-Project:devfrom
qiyinxi:feat/maafw-managed-20260908
Open

feat(maafw): MFW 托管脚本类型(导入脱壳、共用运行环境、版本管理、远程更新、一键迁移)#633
qiyinxi wants to merge 13 commits into
AUTO-MAS-Project:devfrom
qiyinxi:feat/maafw-managed-20260908

Conversation

@qiyinxi

@qiyinxi qiyinxi commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

接通三层规划的第三层。app/task/MaaFW/tools/{core/automas_maafw_project_store,embedded/managed} 那 9800 行早就落库但从未接线,本 PR 把它接上,并补齐未移植的插件宿主那半边。

用户拿到什么

新脚本类型「MFW 托管」:项目由 AUTO-MAS 导入并精简——按 interface.json 声明的路径做白名单投影,界面程序、自带 Python、自带 MaaFramework 都不落盘,agent 直接用运行池的解释器。实测 MaaYYs 225.51 MB → 75.51 MB(省 66.51%),M9A 652.6 MB → 71.7 MB(省 89.09%)。

同一项目的多个版本并存,可随时切换、删除;更新时下载整包作为新版本导入,旧版本留着可回退。现有的 MFW 脚本能一键转为托管,可选删掉原目录。

三个写错了不报错、只会出坏结果的点

  • 必须要整包。Store 导入的是一整棵树,差量包导进去是个跑不起来的版本。服务不支持 prefer_full_package 时直接报错,不静默降级。
  • 外壳提示要从 manifest 回填detect_maafw_project_shell_hint 靠根目录的 MFW.exe / maafw/ 判断外壳,而托管载荷正好把这些脱掉了;不回填,M9A 这种同时发 -MXU.zip-MFAA.zip 的项目会选错资产。
  • 运行前更新必须排在准备环境之前。准备那步会解析 Store 的当前版本并建 checkout,顺序反了这一轮仍跑旧版本,日志却说已更新。

顺带修一个既有缺陷:/maafw/updateisinstance(MaaFWConfig) 判类型,托管是它的子类拦不住,不分流就会拿 Store 的 checkout 去做原地改写。

取舍

不做两阶段 Managed.PendingUpgrade 升级——那条路要配套的配置迁移计划引擎,在未移植的 plugin.py 里。自选目录形态今天就是直接更新的:新 interface 里没有的任务由 run_plan 记成 skippedTasks,全没了才报错。托管沿用同一口径,而且旧版本留在 Store 里可回退,比原地更新更容易挽回。

迁移是原地换类型而不是「新建再删旧」:脚本 ID 不变,队列成员、计划表、通知绑定和 data/<uid>/ 下的用户数据全都留着。为此给 MultipleConfig 加了 convert

验证

本地 pytest tests -q 790 passed / 3 skipped / 162 subtests--collect-only 退出码 0;yarn typecheck / yarn lint 退出码 0;changelog.py check 退出码 0。

真机(开发后端 36164,真网络)跑完整链路:新建自选目录脚本 → 转托管 → 检查更新(发现 MaaYYs v3.15.2,未扣 CDK 额度)→ 执行更新(GitHub 实下 ~200 MB,导入并切换)→ 回退 → 删版本。临时下载包在成功与失败两条路径上都被释放,Store 未留半个版本。

真机跑出三个缺陷,均已修在本 PR 内,其中影响最大的一条:upgrade_project 曾把当前版本的 runtimeConstraint 套到新载荷上校验,凡是顺带升了 MaaFramework 的项目都更新不了(MaaYYs v3.10.2 自带 5.11.1、v3.15.2 自带 5.13.0b2)。

未验证:按环境约束没跑真实游戏,AutoProxy 真机运行仍待人工确认。

依赖 #632(同分支基线上摘出去的独立修复);两边都改了 CHANGELOG.md / res/version.json,先合的那个会让另一个需要 rebase。

🤖 Generated with Claude Code

qiyinxi and others added 13 commits September 8, 2026 22:30
第三层(MAS 托管)的服务层早已落库,缺的是宿主侧接线。本提交补第一批契约,
不改变自选目录形态的任何行为——两者并存。

- 新增 `MaaFWManagedConfig`,子类化 `MaaFWConfig`:既有的 `isinstance` 分发
  因此继续命中 `MaaFWEmbeddedManager`,无需为托管另造 manager。只补父类没有、
  而托管解析结果需要落盘的 `ImportProjectId` / `RunRootId` / `Status` 三个键。
- 注册进 `ScriptConfig` 列表、`CLASS_BOOK`、`TYPE_BOOK` 与两处 schema Literal。
  `MultipleConfig` 按类名索引,漏注册会在加载时**静默丢弃整条脚本配置**;
  `TYPE_BOOK` 漏了则会在按类名取显示名处 KeyError。
- 新增 `Config.get_script_records()`:托管环境服务要求 `type` 是 `CLASS_BOOK`
  的键而非类名,且 `Managed` / `ManagedRuntime` 下的 JSON 字段按 Mapping 读,
  而配置里存的是字符串,故在此解析。
- 新增 `Config.script_config_transaction()`:按脚本串行化配置写入者。**不复用**
  `ScriptConfig[uid].lock()`——那是运行期禁止界面改配置的闸门,会把事务体内
  自己的 `update_script` 一并拒掉。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
把第三层的服务层接进 `MaaFWEmbeddedManager`:托管脚本在任何用户任务之前先走
Store / Gateway 那条链解析出 checkout 与运行时,产出的可信 route 直接交给
inner task,`runner_task` 在 `managed_execution` 为真时不再自己算。

`check()` 对托管形态改判前置条件:项目目录是**环境准备的产物**而不是输入,
首次运行时 `Info.Path` 还是空的,用原来的「请设置项目路径」会把托管形态卡死;
按路径做的运行环境自检同理,改由准备那一步负责。

途中两处配置层缺陷会让托管形态根本跑不起来,一并修掉:

- `FolderValidator` 把工作目录及其子路径全列为禁区,而托管 checkout 本来就由
  AUTO-MAS 自己产出在 `data/maafw_project_runs/` 下,持久化绑定必然失败。把禁区
  列表提成可覆写的 `_forbidden_paths()`(默认行为不变),新增
  `ManagedFolderValidator` 只放开工作目录,系统目录与盘根仍禁;`MaaFWConfig`
  的 `Info.Path` 校验器改由 `_project_path_validator()` 提供,托管子类覆写它。
- `JSONValidator` 只接受 JSON 字符串,托管环境服务写的是 dict,于是 manifest 与
  runtime binding 被 `correct()` **静默丢成空**,运行时 route 反解必然报「缺少
  运行时 ID」。`correct()` 改为接受结构化输入并序列化;字符串输入行为不变。

实测(M9A v4.6.0 发行包,652.6 MB):导入脱壳后 checkout 71.2 MB,
Python agent 走共享 runtime `maafw-runtime-09379d…`(maafw==5.12.3),
不再建独立 venv;全量测试与基线一致。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`_safe_remove_tree` 用的是裸 `shutil.rmtree`。Windows 上删只读文件必然
`PermissionError`,而 git 仓的 pack 文件(`.git/objects/pack/*.idx`)就是只读的
—— 源目录里带 git 仓时,导入过程中任何一步失败,`finally` 的 staging 清理都会
再抛一个 `Access is denied`,把真正的失败原因整个盖住。

加一个只清只读位再重试的 `onexc` 钩子,非 `PermissionError` 一律照旧抛出。

实测:导入 M9A 源码仓(带 `.git`)从「Access is denied 到 pack-*.idx」变成真实
原因「Python Agent runtime ABI is unknown for indexes 0」——那才是它该被拒的理由。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`tests/task/test_maafw_managed_gate.py`(`137adfed` 落库、AUTO-MAS-Project#509 删除)当年把第三层
的「落库不接线」状态钉死;解 gate 的前提是第二层稳定,而第二层已合入 dev 并经真机
修复,所以这里把它的每一条断言反过来写:接线点必须在位。

覆盖四组,全部是纯逻辑、不碰文件系统也不联网:
- 托管脚本类型在 `CLASS_BOOK` / `TYPE_BOOK` / `ScriptConfig` 列表里都注册了,
  且三个新键只在子类上(漏注册会静默丢弃整条脚本配置);
- `Info.Path` 两种校验器各司其职:父类仍禁工作目录,托管放开、但盘根仍禁;
- `JSONValidator` 接受结构化输入并序列化,字符串输入行为不变;
- `get_script_records` 的 JSON 字段解析对坏值一律退化成空 Mapping。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
托管环境准备完成后,`_run_project_update` 与 `_ensure_project_environment` 会再跑一遍,
而它们走的是自选目录那套口径,结果把托管链路刚绑定好的东西整个覆盖掉。

真机实测:托管准备时 agent 已经是
`[Python环境] Agent python 使用 shared_runtime: …/maafw-runtime-09379da0…`,
被覆盖后变成 `[Python环境] Agent python 使用 external: python`,且换用了另一个
runtime `maafw-runtime-b800e2d9…`——依赖复用直接失效。

托管项目的版本由 Project Store 管,原地更新本就没有意义;环境也已由托管链路准备。
BeforeRun 与 AfterRun 两处都加上托管判据。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
M2 的后端部分。托管形态在此之前只能靠直接调服务层,界面无从接入。

新增端点(都在既有 `/api/scripts/maafw/` 下):
- `managed/import`    导入本地目录或 ZIP 并可选绑定到脚本
- `managed/versions`  列版本(含 current / pinned / references / 体积)
- `managed/switch`    切换当前版本,可同步更新脚本绑定
- `managed/version/delete`  删除版本
- `managed/inventory` Store 占用
- `managed/gc`        回收无人引用的版本,默认 dry-run

两条刻意的取舍:

- **绑定必须带 Store 身份**。只写 projectId/version 而不带 `StoreId` 与 manifest,
  运行时会被「脚本缺少可验证的 Project Store 身份」直接拒掉,所以导入端点在绑定时
  一次写全。
- **闸门与阻断的理由原样透出**。导入被拒(依赖不合规、ABI 未知、路径越界)和删除被
  阻断(current / pinned / references / lease)时,界面需要的正是那句原因,不吞成
  「操作失败」。

脱壳报告直接取 manifest 里现成的 `savedBytes` / `savedPercent` / `excludedReasons` /
`shells.families`,不另算一遍。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
从开发后端(36174)重新生成。生成物是 CRLF,`git add` 已按仓库的行尾规则归一化,
463 个被行尾差异标记的文件里只有 21 个真正有内容变化:15 个新模型、`MaaFwService`
与 `Service` 的六个方法、以及脚本类型与 `Managed` 三个新字段带来的三处增量。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
把 `MaaFWManaged` 加进前端各处按类型索引的表:`ScriptType` 联合、建项类型到后端枚举
的映射、配置类名到前端类型的反查(漏了会被 `resolveScriptType` 回落成 General、渲染成
通用脚本)、图标与短名、标签颜色,以及两处编辑页路径——托管复用 MaaFW 的编辑页,两者
配置结构相同,只多三个 Managed 键。

建项向导里托管选项排在 MaaFW 之前,描述写清它与自选目录的差别:由 AUTO-MAS 导入并
精简项目、去掉界面程序与自带 Python、多个脚本共用一套运行环境。三份词表同步。

`yarn typecheck` 与 `yarn lint` 通过。`yarn test` 419 passed / 1 failed,失败的
`electron/services/backendService.test.ts` 是既存问题(暂存本次改动后同样超时)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MaaFW 编辑页新增托管专属区块(只在 `MaaFWManaged` 类型出现,自选目录形态完全不受
影响),配 `useMaaFWManagedApi` 走生成的客户端,不手拼 axios。

三件事对应界面上的三块:

- **导入**:填本地项目目录或 ZIP 发行包,导入后自动绑定到本脚本。失败时用 Modal 展示
  后端给的原因全文——闸门拒绝的理由(依赖不合规、ABI 未知、路径越界)正是用户要看的
  东西,缩成「导入失败」等于什么都没说。
- **脱壳报告**:省了多少、百分比、移除的外壳家族、排除条数,可展开逐条看路径与原因。
  数值全部取自 manifest 里现成的 `savedBytes` / `savedPercent` / `shells` / `excludedReasons`,
  前端不重算。
- **版本管理**:表格列出版本、体积、最近使用,可切换与删除;current 行禁用这两个操作,
  删除被 pinned / references / lease 阻断时同样把原因原样弹出。

导入或切版本后重新拉一次配置——`Info.Path` 与 `Managed` 段都被后端改过了,否则界面还
停在旧 checkout 上。三份词表同步。

`yarn typecheck`、`yarn lint` 通过;`yarn test` 419 passed,唯一失败仍是既存的
`electron/services/backendService.test.ts` 超时。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
按仓库规矩改 CHANGELOG.md 后运行 scripts/changelog.py sync 同步生成物。

sync 顺带抹掉了 res/version.json 里两条既有条目的 by 署名:那两处署名是
机器人 97335f0 用 main 上的旧工作流直接写进生成物的,CHANGELOG.md 这个
唯一手写来源里从来没有,任何一次 sync 都会把它们擦掉。保留它们会让
check-changelog 闸门变红,这里不手改生成物。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
托管形态原本完全不更新:远程编排在未移植的插件宿主里,网关的 project_update
一直传 None。这里补上「下整包 → 导入为新版本 → 切过去」,与自选目录形态共用
同一个更新面板和同一套脚本级凭据。

三处托管特有、写错了不报错只出坏结果的地方:

- Store 导入的是一整棵树,差量包导进去就是个跑不起来的版本,所以恒要整包;
  服务不支持 prefer_full_package 时直接报错,不静默降级。
- 脱壳把根目录的 MFW.exe / maafw/ 之类标志全删了,外壳提示只能从 manifest
  回填,否则 M9A 这种同时发 -MXU.zip 与 -MFAA.zip 的项目会选错资产。
- upgrade_project 固定 inactive 导入,导完必须切版本,否则日志说更新成功、
  跑的还是旧版本。运行前更新也因此必须排在准备环境之前。

顺带修一个既有缺陷:/maafw/update 用 isinstance(MaaFWConfig) 判类型,托管是
它的子类,拦不住;不分流就会拿 Store 产出的 checkout 去做原地更新。

不做两阶段的 Managed.PendingUpgrade 升级——那条路要配套的配置迁移计划引擎。
自选目录形态今天就是直接更新的,新 interface 里没有的任务由 run_plan 记成
skippedTasks;托管沿用同一口径,且旧版本仍留在 Store 里可以切回去。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
迁移是**原地换类型**,不是"新建一个托管脚本再把旧的删掉":脚本 ID 保持不变,
队列成员、计划表、通知绑定和 data/<uid>/ 下的用户数据全都留着。走新建那条路
用户会发现脚本从队列里消失了,而这一步不会有任何报错。

为此给 MultipleConfig 加了 convert:类型不是单独存的字段,而是配置对象自己的
类名,换类型就是在原位换掉这个对象。用户 uuid 不重新生成——data/<script>/<user>
目录按它命名。

删原目录是不可撤销的,所以:只在用户显式勾选时做、一定排在导入与转换都成功
之后、删之前再过一遍 FolderValidator(拦驱动器根、系统目录和 AUTO-MAS 自己的
工作目录)。删除失败不回滚迁移——项目已经在 Store 里了,撤回去更糟,如实报告
让用户自己删。

迁移后清掉 Info.Path:项目已经不在那儿了,留着会把一个可能刚被删掉的目录当成
项目路径显示。真正的 checkout 由准备链路在首次运行时写回。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
真机验证 MaaYYs v3.10.2 -> v3.15.2 时逐个撞出来的:

1. 升级时不再沿用当前版本的 runtimeConstraint。它描述的是「这份载荷需要哪个
   MaaFramework」,是载荷的属性;套到新载荷上会被 Store 的一致性闸门拒成
   「==5.11.1 vs 5.13.0b2」——凡是顺带升了 MaaFramework 的项目都更新不了,
   而这几乎是每一次真实更新。不给时由 Store 从新包自行推导。

2. project_update 服务的 discover_update 补上 prefer_full_package 与
   version_only 两个透传参数。之前只有底层模块函数有,服务方法没暴露,网关按
   签名判断后直接拒掉,托管更新一次都跑不了。检查更新也因此会去换下载地址,
   白扣一次 Mirror 酱当日额度。

3. 版本体积与当前版本读错了键:体积在 summary.size.projectedBytes,项目的当前
   版本叫 currentVersion。读错不报错,界面上一律显示 0 B / 没有当前版本,看起来
   像「托管一点空间都没占」。实测该值是 74,653,605 B。

第 3 条能溜过去,是因为 M2 的两条路由测试用的是我臆造的返回形状而不是 Store
真实的输出;这次把那两个夹具按真实形状改了,并补上按签名校验网关依赖的断言。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @qiyinxi, your pull request is larger than the review limit of 150,000 diff characters

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant