refactor(main): 解除对Win32的强依赖 - #423
Conversation
审查者指南本 PR 通过平台能力抽象、Windows/common 双实现和按需导入,将后端启动、系统服务、进程/窗口操作及安全存储与 Win32 解耦;Windows 继续使用原生能力,非 Windows 则可启动并对不支持的功能进行安全跳过或明确拒绝,同时将 Windows 依赖改为条件安装并补充平台兼容性测试。 平台特定电源操作的时序图sequenceDiagram
participant System as System
participant Power as power
participant WindowsPower as WindowsPowerController
participant CommonPower as CommonPowerController
participant OS as OperatingSystem
System->>Power: execute(action)
alt Windows
Power->>WindowsPower: execute(action)
WindowsPower->>OS: subprocess.run(command)
else non-Windows
Power->>CommonPower: execute(action)
CommonPower-->>System: UnsupportedPlatformError
end
延迟加载 Windows 适配器的时序图sequenceDiagram
participant Main as main
participant Platform as IS_WINDOWS
participant Importer as importlib
participant Adapter as ArknightWin32Toolkit
participant Timer as MainTimer
Main->>Platform: check platform
alt Windows
Main->>Importer: import_module(app.MaaFW.ArknightWin32)
Main->>Adapter: init()
Main->>Timer: start()
else non-Windows
Main->>Timer: start()
end
文件级变更
提示和命令与 Sourcery 交互
自定义使用体验访问你的控制面板:
获取帮助Original review guide in EnglishReviewer's Guide本 PR 通过平台能力抽象、Windows/common 双实现和按需导入,将后端启动、系统服务、进程/窗口操作及安全存储与 Win32 解耦;Windows 继续使用原生能力,非 Windows 则可启动并对不支持的功能进行安全跳过或明确拒绝,同时将 Windows 依赖改为条件安装并补充平台兼容性测试。 Sequence diagram for platform-specific power operationssequenceDiagram
participant System as System
participant Power as power
participant WindowsPower as WindowsPowerController
participant CommonPower as CommonPowerController
participant OS as OperatingSystem
System->>Power: execute(action)
alt Windows
Power->>WindowsPower: execute(action)
WindowsPower->>OS: subprocess.run(command)
else non-Windows
Power->>CommonPower: execute(action)
CommonPower-->>System: UnsupportedPlatformError
end
Sequence diagram for deferred Windows adapter loadingsequenceDiagram
participant Main as main
participant Platform as IS_WINDOWS
participant Importer as importlib
participant Adapter as ArknightWin32Toolkit
participant Timer as MainTimer
Main->>Platform: check platform
alt Windows
Main->>Importer: import_module(app.MaaFW.ArknightWin32)
Main->>Adapter: init()
Main->>Timer: start()
else non-Windows
Main->>Timer: start()
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
2d482e3 to
1087b38
Compare
1087b38 to
fe9bd17
Compare
There was a problem hiding this comment.
嘿——我发现了 1 个安全问题,以及另外 4 个问题
安全问题:
- 检测到子进程函数
run未使用静态字符串。如果此数据可由恶意行为者控制,则可能构成命令注入。请审查此调用的使用方式,确保它不受外部资源控制。可以考虑使用shlex.escape()。(链接)
面向 AI Agent 的提示
请处理本次代码审查中的评论:
## 单独评论
### 评论 1
<location path="main.py" line_range="182" />
<code_context>
- )
- from app.MaaFW import ArknightWin32Toolkit
+ if IS_WINDOWS:
+ for adapter in (
+ "app.MaaFW.ArknightWin32",
+ "app.MaaFW.EndFieldPCWin32",
+ ):
+ await asyncio.to_thread(importlib.import_module, adapter)
- await ArknightWin32Toolkit.init()
</code_context>
<issue_to_address>
**issue (broader_impact):** Windows 启动时会导入 `app.MaaFW.EndFieldPCWin32`,尽管该 Win32 适配器已在重构中移除,因此 `initialize_background_services()` 会在后端完成启动前引发 `ModuleNotFoundError`。
**触发条件:** Windows 上进行后台服务初始化时。
**建议修复:** 从适配器列表中移除 `app.MaaFW.EndFieldPCWin32`,或者在所有启动导入都移除之前保留该模块。
```suggestion
```
</issue_to_address>
### 评论 2
<location path="app/services/platform/common/system.py" line_range="265-270" />
<code_context>
+ """
+
+ logger.info(f"开始中止进程 PID: {pid}")
+ args = ["taskkill", "/F"]
+ if kill_tree:
+ args.append("/T")
+ args.extend(["/PID", str(pid)])
+ result = await ProcessRunner.run_process(
+ *args,
+ )
+ if result.returncode != 0:
</code_context>
<issue_to_address>
**issue (bug_risk):** `kill_process_by_pid` 始终调用仅限 Windows 使用的 `taskkill` 可执行文件,因此在 Linux 上进行进程清理时会引发 `FileNotFoundError`,而不是终止指定的进程。
**触发条件:** 在非 Windows 平台上调用 `System.kill_process_by_pid` 或 `System.kill_process` 时。
**建议修复:** 实现平台特定的进程终止逻辑,例如在非 Windows 平台上使用 `psutil.Process(pid).kill()`。
</issue_to_address>
### 评论 3
<location path="app/utils/platform/common/process.py" line_range="77" />
<code_context>
+ with suppress(psutil.NoSuchProcess, psutil.AccessDenied):
+ if proc.info.get("name") == process_name:
+ for hwnd in get_window_handles(proc.pid):
+ if window_service.is_visible(hwnd):
+ return True
+ return False
</code_context>
<issue_to_address>
**issue (bug_risk):** 非 Windows 实现仅在赋值 `window` 时捕获 `UnsupportedPlatformError`,但 `window_service.is_visible(hwnd)` 在该保护范围之外调用,并会引发 `UnsupportedPlatformError`;因此,在不受支持的平台上,`is_process_running` 会崩溃,而不是返回 `False`。
**触发条件:** 在非 Windows 平台上找到匹配的进程时。
**建议修复:** 在窗口可见性调用周围捕获 `UnsupportedPlatformError`,或者在窗口功能不受支持时立即返回 `False`。
```suggestion
try:
if window_service.is_visible(hwnd):
return True
except UnsupportedPlatformError:
return False
```
</issue_to_address>
### 评论 4
<location path="app/utils/platform/windows/window.py" line_range="146-156" />
<code_context>
- except Exception:
- # 某些系统策略下 SetForegroundWindow 可能被拒绝, 尝试焦点切换降级路径
- try:
- win32gui.SetWindowPos(
- hwnd,
win32con.HWND_TOPMOST,
0,
</code_context>
<issue_to_address>
**issue (bug_risk):** `activate_window` 的回退逻辑在两次 `SetWindowPos` 调用中都将 `hwnd` 作为 `hWndInsertAfter` 参数传入,而不是分别使用 `HWND_TOPMOST` 和 `HWND_NOTOPMOST`,因此无法执行预期的置顶切换;当 `SetForegroundWindow` 被拒绝时,激活操作仍可能失败。
**触发条件:** Windows 拒绝初始的 `SetForegroundWindow` 调用并执行回退路径时。
**建议修复:** 第一次调用传入 `win32con.HWND_TOPMOST`,第二次调用传入 `win32con.HWND_NOTOPMOST`。
</issue_to_address>
### 评论 5
<location path="app/services/platform/windows/power.py" line_range="19" />
<code_context>
subprocess.run(commands[action])
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** 检测到子进程函数 `run` 未使用静态字符串。如果此数据可由恶意行为者控制,则可能构成命令注入。请审查此调用的使用方式,确保它不受外部资源控制。可以考虑使用 `shlex.escape()`。
*来源:opengrep*
</issue_to_address>Sourcery 评估
需要人工审查。 首先需要处理 5 个发现的问题;此外,该重构还改变了启动任务、电源命令、进程终止以及基于 DPAPI 的密钥存储的平台分派。错误的分派可能创建或遗留自动启动任务、终止模拟器进程,或关闭/重启机器,而这些影响无法通过还原代码完全撤销。
阻塞性发现:main.py:182、app/services/platform/common/system.py:270、app/utils/platform/common/process.py:77、app/utils/platform/windows/window.py:156、app/services/platform/windows/power.py:19
请帮助我变得更有用!请在每条评论上点击 👍 或 👎,我会利用反馈来改进审查结果。
Original comment in English
Hey - I've found 1 security issue, and 4 other issues
Security issues:
- Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="main.py" line_range="182" />
<code_context>
- )
- from app.MaaFW import ArknightWin32Toolkit
+ if IS_WINDOWS:
+ for adapter in (
+ "app.MaaFW.ArknightWin32",
+ "app.MaaFW.EndFieldPCWin32",
+ ):
+ await asyncio.to_thread(importlib.import_module, adapter)
- await ArknightWin32Toolkit.init()
</code_context>
<issue_to_address>
**issue (broader_impact):** Windows startup imports `app.MaaFW.EndFieldPCWin32` even though this Win32 adapter is removed by the refactor, so `initialize_background_services()` raises `ModuleNotFoundError` before the backend finishes starting.
**Triggers:** On Windows during background-service initialization.
**Suggested fix:** Remove `app.MaaFW.EndFieldPCWin32` from the adapter list, or retain the module until all startup imports are removed.
```suggestion
```
</issue_to_address>
### Comment 2
<location path="app/services/platform/common/system.py" line_range="265-270" />
<code_context>
+ """
+
+ logger.info(f"开始中止进程 PID: {pid}")
+ args = ["taskkill", "/F"]
+ if kill_tree:
+ args.append("/T")
+ args.extend(["/PID", str(pid)])
+ result = await ProcessRunner.run_process(
+ *args,
+ )
+ if result.returncode != 0:
</code_context>
<issue_to_address>
**issue (bug_risk):** `kill_process_by_pid` always invokes the Windows-only `taskkill` executable, so process cleanup on Linux raises `FileNotFoundError` instead of terminating the requested process.
**Triggers:** When `System.kill_process_by_pid` or `System.kill_process` is called on a non-Windows platform.
**Suggested fix:** Implement platform-specific process termination, for example with `psutil.Process(pid).kill()` on non-Windows platforms.
</issue_to_address>
### Comment 3
<location path="app/utils/platform/common/process.py" line_range="77" />
<code_context>
+ with suppress(psutil.NoSuchProcess, psutil.AccessDenied):
+ if proc.info.get("name") == process_name:
+ for hwnd in get_window_handles(proc.pid):
+ if window_service.is_visible(hwnd):
+ return True
+ return False
</code_context>
<issue_to_address>
**issue (bug_risk):** The non-Windows implementation catches `UnsupportedPlatformError` only while assigning `window`, but `window_service.is_visible(hwnd)` is called outside that protection and raises `UnsupportedPlatformError`; therefore `is_process_running` crashes instead of returning `False` on unsupported platforms.
**Triggers:** When a matching process is found on a non-Windows platform.
**Suggested fix:** Catch `UnsupportedPlatformError` around the window-visibility call or return `False` immediately when the window capability is unsupported.
```suggestion
try:
if window_service.is_visible(hwnd):
return True
except UnsupportedPlatformError:
return False
```
</issue_to_address>
### Comment 4
<location path="app/utils/platform/windows/window.py" line_range="146-156" />
<code_context>
- except Exception:
- # 某些系统策略下 SetForegroundWindow 可能被拒绝, 尝试焦点切换降级路径
- try:
- win32gui.SetWindowPos(
- hwnd,
- win32con.HWND_TOPMOST,
- 0,
</code_context>
<issue_to_address>
**issue (bug_risk):** The `activate_window` fallback passes `hwnd` as the `hWndInsertAfter` argument to both `SetWindowPos` calls instead of `HWND_TOPMOST` and `HWND_NOTOPMOST`, so the intended topmost toggle is not performed and activation can still fail when `SetForegroundWindow` is rejected.
**Triggers:** When Windows refuses the initial `SetForegroundWindow` call and the fallback path runs.
**Suggested fix:** Pass `win32con.HWND_TOPMOST` for the first call and `win32con.HWND_NOTOPMOST` for the second call.
</issue_to_address>
### Comment 5
<location path="app/services/platform/windows/power.py" line_range="19" />
<code_context>
subprocess.run(commands[action])
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.
*Source: opengrep*
</issue_to_address>Sourcery assessment
Needs a human reviewer. 5 findings to address first, and the refactor changes platform dispatch for startup tasks, power commands, process termination, and DPAPI-backed secret storage. A wrong dispatch can create or leave behind an auto-start task, terminate emulator processes, or shut down/reboot the machine, and those effects are not fully undone by reverting the code.
Blocking findings: main.py:182, app/services/platform/common/system.py:270, app/utils/platform/common/process.py:77, app/utils/platform/windows/window.py:156, app/services/platform/windows/power.py:19
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- pywin32/keyboard 改为 sys_platform == "win32" 条件依赖,代码已不 硬依赖 Win32,非 Windows 环境安装依赖不应再失败 - 修复 res/version.json 中 AUTO-MAS-Project#433 引入的缺失逗号;该文件被 frontend/vite.config.ts require,非法 JSON 会导致前端构建失败 - 补充平台层相关更新日志条目 - tests/AGENTS.md 补充 tests/platform/ 目录归属说明
50ab8b3 to
cac5e67
Compare
|
@sourcery-ai review |
|
Sorry @HarcoChen, you've used your own review budget of 250,000 diff characters for the last 7 days. You can request another review in 2 days and 11 hours by commenting |
审查结论本次改动方向合理,但目前仍有两项会阻断“Linux 等非 Windows 平台启动后端”目标的问题,建议合并前处理。 需要修复
循环导入验证静态依赖图显示,PR 与
PR 没有新增静态可见的强连通分量。另以 验证结果
当前 Windows 环境会跳过真正的非 Windows 导入路径,因此以上结果不能覆盖 Linux 无 |
循环依赖重构建议当前通过函数内导入和模块级 建议方案
推荐落地顺序前五步即可从根本上消除本 PR 涉及的动态循环依赖; |
后续计划:阶段三:参数化 power_signSystem 不再自己读 Config,改由调用方传入,消除 services → core 的剩余延迟导入。 阶段四:KillSelf 移出电源能力关闭 WebSocket、设置 should_exit 是应用生命周期,不是操作系统电源能力。平台电源接口只保留 shutdown / reboot / sleep。 阶段五:包级聚合导入改为具体模块from app.core import Config 改为 from app.core.config import Config,让 init.py 不再主动加载业务模块,导入结果不再取决于包初始化顺序。 记录:两个更大的环models ↔ utils双向有顶层导入(models→utils 4 条,utils→models 6 条)现在不炸只是因为初始化顺序恰好成立。 app.utils.emulator.{general,ldplayer,mumu} → app.models.{config,emulator} [顶层] task ↔ core 有 32 条反向边,但只需断一条。 task → core 有 32 条顶层导入,方向极其一致;反向只有 core/task_manager.py 一条。真要治,断那一条就够,不必动 32 条。task ↔ models 同理:93 对 2。 |
|
对本 PR 做了一轮较深入的审查(多角度查找 + 每条结论独立复核,含对 CPython 行为的实测验证)。整体重构思路和平台分层是好的,迁移过程也很克制,但目前跨平台启动这个核心目标尚未真正达成,另有若干需要处理的问题。按严重程度排列: 阻断核心目标的问题
其他正确性问题
设计与一致性
规范
附注(基线相关): 本 PR 的基线快照里, 本审查由 AI(Claude)辅助完成;以上每条均经独立验证(代码交叉核对,关键语义在 CPython 3.12 上实测)。 |
关联 #295(本 PR 将 dev_v2 已合入的 #296 重构移植到 `dev`,是否随本 PR 关闭该 issue 请维护者定夺)。 ## 摘要 1. **后端**新增 `app/core/ws/`(MainConnection 唯一主连接、Dispatcher 按 id+type 分发、Publisher 统一出口、强类型 WSEnvelope/WS*Data);`@ws_command` 反射执行器换为显式参数注册表;出站 Koishi 客户端改用协议层 ping/pong,命令执行器由 main.py 显式注入;新增 `/api/core/ws_meta` 与任务运行时、电源倒计时、更新下载三个 HTTP 初始快照端点。 2. **前端**重写连接层(状态机、单一重连计时器、有限退避、id+type 分发、请求响应关联)与订阅注册表;新增 `useAppLifecycle` 生命周期协调器(常驻订阅、正常关闭 30s taskkill 兜底、异常自动重启后端上限 3 次、后端驱动电源倒计时、休眠恢复检查);Electron 主进程改为协调退出(`app-close-requested` → renderer closeApp → 25s 兜底 scoped 强杀),**删除 `taskkill /f /im python.exe` 全量误杀**。 3. **删除**应用层心跳、Broadcast(全仓零订阅者)、消息缓存/重放/历史、反射式 ws_command、`app/api/ws_debug.py` 全部调试路由与 WSdev 页面、连接原因白名单、`schedulerHandlers` localStorage 重放。 4. **dev 独有面补齐迁移**(#296 未覆盖):HSR/OkNte/Okww 专项共 21 处调用、GameSign 结果广播(新增 `id=GameSign` / `gamesign.result.updated` 路由,并顺带修复该调用在 dev_v2 上已静默失效的问题)、托盘 TrayAction/全局停止快捷键与协调退出汇合、首页卫星状态改由任务运行时常驻订阅驱动。 5. 已包含 dev_v2 上 #296 的全部后续修复(30s 关闭超时、模拟器/明日方舟错误通知展示、断线后任务停止契约、/stop 完成事件窗口)。 ## 范围调整(相对 dev_v2 版本) - **不移植**:插件系统/插件市场全部通道(dev 无该子系统)、MaaFW 前端适配、uv/pyproject 工具链。 - **弹窗 Dialogs 通道未移植**:dev 已移除人工排查模式(其唯一生产方),且 dev 上旧 `Message/Question` 通道全仓无发送方;对应 `WebSocketMessageListener` 一并删除。 - **开发模式判定改用 `AUTO_MAS_ENV`**:dev 分支前端拉起的后端始终携带 `AUTO_MAS_DEV=1`(跳过自行提权的宿主标记),不能沿用 dev_v2 的判定,否则生产环境后端会被误判为开发模式导致关闭流程失效。 - 行为说明:Koishi 远程 `core.close` 现执行完整 teardown 并经主 WS 发 `backend.shutdown.ready`(原为关闭 socket + KillSelf);如需远程关闭前端仍可走电源 KillSelf 路径(`frontend.close.requested`)。 - `yarn openapi` 再生成同时收敛了此前后端 schema 变更后未再生的存量差异(森空岛内置签到、人工排查移除、OkNte `IfUseMasConfig`、MAA 游戏更新配置等字段),生成目录未手改。 ## 验证 - 后端 pytest 135 passed(2 项失败为既存基线:telemetry 测试断言旧 `_start_sentry` 签名、SRC 测试依赖的系统临时目录残留,均与本 PR 无关);合并门槛 `pytest --collect-only` 退出码 0。 - 前端 vitest 120/120;`yarn typecheck` 0 错误;vite build 与 electron 主进程构建通过;改动文件 ESLint 全绿。 - 真实主 WS 冒烟:单连接替换旧连接、非法信封丢弃不掉线、`/close` 后依次收到 `power.sign.updated` 与 `backend.shutdown.ready`、开发模式后端保留复用、三个快照端点返回正确。 - 全仓搜索 `send_websocket_message`/`Config.websocket`/`Broadcast`/`sendRaw`/`ExternalWSHandlers`/`scheduler-pending-tabs` 等旧机制 0 残留。 - 待人工复核(需图形界面实机):Electron 正常/异常退出全路径(✕、托盘退出/重启、25s 兜底、失败保留前端)、休眠恢复、任务全流程与音频、断线自动重启弹窗。 ## 协作提示 - 与在途 PR #423(main.py Win32 依赖)与 #417(通知中间层)存在相邻改动,合并顺序靠后的一方需 rebase。 - WS 调试 REST 路由删除会同步影响 MCP 工具面(自动生成),属预期的对外契约变化。 - 协议速查文档新增于 `res/docs/WebSocket管理器快速上手.md`。 🤖 Generated with [Claude Code](https://claude.com/claude-code)
P0-1,Headless Linux前端怎么启动,而且和架构改动关系不大 P2-10 确实是问题,后面提PR改 |
…-phase1 # Conflicts: # app/core/__init__.py # app/services/system.py # app/services/update.py # res/version.json
解除了后端对Win32的强依赖,现在支持跨平台启动后端
Sourcery 摘要
通过引入平台能力层并隔离 Windows 专属功能,解除后端对 Win32 的强依赖并实现跨平台启动。
新功能:
错误修复:
改进:
构建:
杂项:
Original summary in English
Sourcery 总结
将后端启动和核心服务与 Win32 依赖解耦,以支持跨平台运行,同时保留 Windows 特有功能。
新功能:
错误修复:
增强功能:
构建:
测试:
杂项:
Original summary in English
Sourcery 摘要
将后端启动和核心服务与 Win32 解耦,以支持跨平台运行,同时保留 Windows 特有的功能。
新功能:
错误修复:
增强功能:
构建:
测试:
日常维护:
Original summary in English
Sourcery 摘要
通过引入平台能力层并隔离 Windows 专属功能,使后端支持跨平台启动。
新功能:
错误修复:
改进:
构建:
测试:
杂项:
Original summary in English
Sourcery 摘要
通过引入平台能力抽象并隔离 Windows 专属功能,使后端支持跨平台启动,同时保留 Windows 平台能力。
新功能:
错误修复:
改进:
构建:
测试:
杂项:
Original summary in English
Sourcery 摘要
通过平台能力抽象解耦后端与 Win32 依赖,使应用支持跨平台启动并保留 Windows 专属功能。
新功能:
错误修复:
增强功能:
构建:
测试:
维护:
Original summary in English
Sourcery 摘要
将后端核心启动流程与 Win32 解耦,以支持跨平台运行并保留 Windows 专属功能。
新功能:
错误修复:
增强:
构建:
测试:
杂项:
Original summary in English
Sourcery 摘要
通过平台能力抽象解耦后端启动流程与 Win32 依赖,同时保留 Windows 专属功能。
新功能:
错误修复:
改进:
构建:
测试:
杂项:
Original summary in English
Summary by Sourcery
通过平台能力抽象解耦后端启动流程与 Win32 依赖,同时保留 Windows 专属功能。
New Features:
Bug Fixes:
Enhancements:
Build:
Tests:
Chores: