fix(boot): 后台初始化各步各自容错,可选步骤失败不再连带跳过主定时器 - #835
beichen24a1 wants to merge 12 commits into
Conversation
同一套「按 ConfigPathMode 在两个目录间拷贝」的逻辑在通用脚本里写了 4 份,只有 ScriptConfig.set_general 做了存在性判断,另外三处源路径不存在时直接抛异常: - AutoProxy.set_general(下发):新建用户后没跑过「脚本设置」,data/<脚本>/<用户>/ ConfigFile 还不存在,自动代理一启动就崩、任务判成异常(Sentry AUTO-MAS-BACKEND-4Q)。 ScriptConfig.set_general 在同样情况下会跳过拷贝、让脚本用自己的配置 —— 两条路径 对同一件事行为不一致,有判断的那份是预期行为。 - AutoProxy.update_config(回写):脚本配置路径不存在时抛异常;File 分支还缺目标目录, copy 不像 copytree 会自建目录,会再抛一次。 - ScriptConfig.final_task(回写):先 rmtree 掉 MAS 侧副本再拷,源不存在时异常发生在 清空之后,用户会以为配置丢了(4Y / 4W)。 三处统一成「先判源存在,不存在就跳过并给一条可读 warning」,与 ScriptConfig.set_general 同口径。 验证: - 三处源不存在路径的前后走查:修复前分别是 swap_in_dir 的 RuntimeError 与 shutil 的 FileNotFoundError,修复后都是 warning + 跳过 - python -m pytest tests/task -q:804 passed / 17 failed,与改前基线逐条一致(既有失败) - python -m pytest tests --collect-only -q:1406 collected,exit 0 - ruff check / ruff format --check:通过
copyToRoot() 对每个条目先 rmSync 掉 appRoot 下的目标、再逐文件 copyFileSync。删完之后、 复制完成之前的任何中断(进程被杀、断电、单个文件撞杀软或占用)都会留下残缺的 app/, 缺哪些模块取决于复制停在哪一步:落在 lifespan 里的让后端完全起不来,落在 initialize_background_services 里的静默失效(Sentry AUTO-MAS-BACKEND-56 / 51)。 改为 replaceItem():先整体复制到 <目标>.new,再改名换入,成功后删掉旧副本;换入失败把 旧目标改名回去 —— 宁可停在旧版本,也不让目标处于缺失或半新半旧的状态。 顺带把 .git 挪到复制列表最后:它是「当前部署的是哪个版本」的依据,先复制会让中断后的 残缺源码看起来已经是最新版,标题栏不再提示更新,用户也就失去重新拉取代码的入口。 验证: - 新增 4 个 vitest 用例(真实临时目录):整目录替换后无 .new/.old 残留、目标不存在时 自建(顺带暴露并修掉 copyFileSync 不自建父目录的边界)、清掉上次中断残留、源不存在 时抛错且目标原样保留 - yarn vitest run electron/services/repositoryService.test.ts:4 passed - yarn tsc -p tsconfig.electron.json --noEmit:通过 - yarn lint:0 errors
list_js_scripts() 逐个读取 {RootPath}/User/JsScript/*/manifest.json 取显示名,任意一个
解析失败都会让函数抛出、接口 500(Sentry AUTO-MAS-BACKEND-9B 的现场是一个尾逗号),
一条龙表单的「JS 脚本」候选列表整体拿不到 —— 而那个坏文件只属于某一个脚本。
manifest 是玩家从社区订阅或手工放置的第三方文件,格式未必严格。解析失败改为记一条
warning、显示名退回目录名、继续列下一个;函数本来就有 display = folder 的兜底,只是
没兜住异常。
验证:
- 新增 4 个纯逻辑用例:坏 manifest 只影响自己(用线上那条事件的尾逗号形态)、manifest
缺 name、目录没有 manifest、JS 脚本目录不存在
- python -m pytest tests/task/test_bettergi_js_script_manifest.py -q:4 passed
- python -m pytest tests --collect-only -q:exit 0
- ruff check:通过
initializationHandlers.ts 里 12 处 event.sender.send(...) 都没有守卫。这些回调活得比 一次 IPC 调用长:窗口关闭、渲染进程被 kill 之后,Runtime 子进程还在往 stdout 吐进度 (Sentry AUTO-MAS-DESKTOP-16),后端子进程随后仍会退出并触发状态回调 (AUTO-MAS-DESKTOP-11),拿着已销毁的 WebContents 去 send 就是主进程的未捕获异常 —— 抛在 Node 的 exit 回调里,没有任何 try/catch 兜住,level 直接是 fatal。 统一走新的 sendToRenderer():sender 已销毁就跳过。main.ts 里一直有这道守卫(12 处), 这个文件是漏的。 验证: - yarn tsc -p tsconfig.electron.json --noEmit:通过 - yarn lint:0 errors - 12 处调用点全部改到统一出口,文件内不再有裸 event.sender.send
雷电的 dnconsole.exe 崩溃时返回码是 3221225480,而 stdout / stderr 都是空的,原来 只回 stdout 的写法让界面上只剩「命令执行失败: 」加一个空串,用户分不清是路径配置错、 实例不存在,还是模拟器自身挂了。 给 ProcessResult 加 failure_detail()(returncode + repr 过的 stdout/stderr),ldplayer 与 mumu 里 8 处 raise RuntimeError(f"命令执行失败: ...") 统一改用它。 验证: - 新增 2 个纯逻辑用例:线上那条事件的形态(返回码非零、两个输出都为空)与多行输出 - python -m pytest tests/tools/test_process_result_failure_detail.py -q:2 passed - ruff check / ruff format --check:通过
dependencyService 只在「Installing collected packages:」与「Successfully installed」两个 节点推进进度:下载阶段解析出的 Collecting 只累加 totalPackages,从不推进 installedPackages。而下载恰恰是耗时最长的一段 —— 非大陆用户实测初始化 7 分 36 秒里有 7 分 27 秒停在 40%,体感是彻底卡死(issue AUTO-MAS-Project#499)。 pip 在这个阶段并不需要知道总包数,所以给出 estimateDownloadProgress():按已见到的包数 渐近推进(40% + 30% × (1 - e^(-n/25))),越接近 70% 越慢、封顶 70%,把 70-80 留给「开始 安装」那一刻的跳动。 验证: - 新增 4 个 vitest 用例:起点 40%、单调不减、封顶 70% 且能到 69% 以上、见到 20 个包时 已经过半 - yarn vitest run electron/services/dependencyService.test.ts:4 passed
initialize_background_services() 把 MCP 挂载、历史清理、适配器导入、显示输出守卫、通知 管理器、Koishi 全放在一个 try 里,前面任何一步抛异常就整体结束,排在其后的 MainTimer.start() 一起没起 —— 界面一切正常、手动运行队列也正常,定时任务却整夜不触发; /api/core/health 虽然回了 backgroundStatus: failed 与 backgroundError,但前端不消费这 两个字段,用户看不到任何提示(issue AUTO-MAS-Project#738)。 改成 _optional_step() 逐个包住:失败只记一条 exception 日志、继续下一步。主定时器仍排在 显示输出守卫之后(原有顺序要求不变),它自己的失败依旧算整体失败。各步骤之间的先后顺序 没有调整。 验证: - 真机启动(把 MainTimer.start() 临时换成一行日志,避免触发用户的计划任务)并在「后台 清理」里注入 RuntimeError:日志依序出现「MCP 服务已挂载」→「后台清理失败, 已跳过: 模拟可选步骤失败」(带完整 traceback)→「主业务定时器启动」→「后端后台初始化完成」, 即可选步骤失败后流程照常走到定时器;修复前同样的注入会让这一步永远不执行 - python -m py_compile main.py:通过 - python -m pytest tests --collect-only -q:1412 collected,exit 0 - ruff format --check main.py:通过(ruff check 报的 7 处 import 排序 / E402 是既有问题, 不在本次改动范围)
审查者指南本 PR 以后台初始化步骤级容错为核心,保证可选服务失败时主定时器仍能启动,同时补充配置、脚本、模拟器、部署、安装进度和 IPC 生命周期相关的独立可靠性修复,并为关键行为增加测试。 具备容错能力的后台初始化时序图sequenceDiagram
participant BackgroundInit as initialize_background_services
participant OptionalStep as _optional_step
participant MCP as _mount_mcp
participant Cleanup as _cleanup
participant Arknight as _init_arknight
participant Guard as DesktopGuard
participant Timer as MainTimer
participant Claw as _start_openclaw
participant Koishi as _init_koishi
BackgroundInit->>OptionalStep: _optional_step 挂载 MCP 服务
OptionalStep->>MCP: _mount_mcp
MCP-->>OptionalStep: 成功或异常
OptionalStep-->>BackgroundInit: 继续
BackgroundInit->>OptionalStep: _optional_step 后台清理
OptionalStep->>Cleanup: _cleanup
Cleanup-->>OptionalStep: 成功或异常
OptionalStep-->>BackgroundInit: 继续
BackgroundInit->>OptionalStep: _optional_step 初始化 ArknightWin32 适配器
OptionalStep->>Arknight: _init_arknight
Arknight-->>OptionalStep: 成功或异常
OptionalStep-->>BackgroundInit: 继续
BackgroundInit->>OptionalStep: _optional_step 启动显示输出守卫
OptionalStep->>Guard: DesktopGuard.start
Guard-->>OptionalStep: 成功或异常
OptionalStep-->>BackgroundInit: 继续
BackgroundInit->>Timer: MainTimer.start
Timer-->>BackgroundInit: 成功或异常
BackgroundInit->>OptionalStep: _optional_step 启动 Claw 通知管理器
OptionalStep->>Claw: _start_openclaw
Claw-->>OptionalStep: 成功或异常
OptionalStep-->>BackgroundInit: 继续
BackgroundInit->>OptionalStep: _optional_step 初始化 Koishi 系统客户端
OptionalStep->>Koishi: _init_koishi
Koishi-->>OptionalStep: 成功或异常
OptionalStep-->>BackgroundInit: 初始化完成
原子部署替换流程图flowchart TD
Start[replaceItem] --> Prepare[创建父目录]
Prepare --> Cleanup[移除残留的 .new 和 .old]
Cleanup --> Stage[将源复制到 target.new]
Stage -->|复制失败| Preserve[保持现有 target 不变]
Stage --> RenameOld[将现有 target 重命名为 target.old]
RenameOld --> RenameNew[将 target.new 重命名为 target]
RenameNew -->|重命名失败| Restore[恢复 target.old]
RenameNew --> Success[移除 target.old]
Restore --> Failure[在不存在部分 target 的情况下抛出错误]
Preserve --> Failure
Success --> Done[部署条目替换完成]
文件级变更
可能关联的问题
提示和命令与 Sourcery 交互
自定义使用体验访问你的控制面板以:
获取帮助Original review guide in EnglishReviewer's Guide本 PR 以后台初始化步骤级容错为核心,保证可选服务失败时主定时器仍能启动,同时补充配置、脚本、模拟器、部署、安装进度和 IPC 生命周期相关的独立可靠性修复,并为关键行为增加测试。 Sequence diagram for resilient background initializationsequenceDiagram
participant BackgroundInit as initialize_background_services
participant OptionalStep as _optional_step
participant MCP as _mount_mcp
participant Cleanup as _cleanup
participant Arknight as _init_arknight
participant Guard as DesktopGuard
participant Timer as MainTimer
participant Claw as _start_openclaw
participant Koishi as _init_koishi
BackgroundInit->>OptionalStep: _optional_step MCP 服务挂载
OptionalStep->>MCP: _mount_mcp
MCP-->>OptionalStep: success or exception
OptionalStep-->>BackgroundInit: continue
BackgroundInit->>OptionalStep: _optional_step 后台清理
OptionalStep->>Cleanup: _cleanup
Cleanup-->>OptionalStep: success or exception
OptionalStep-->>BackgroundInit: continue
BackgroundInit->>OptionalStep: _optional_step ArknightWin32 适配器初始化
OptionalStep->>Arknight: _init_arknight
Arknight-->>OptionalStep: success or exception
OptionalStep-->>BackgroundInit: continue
BackgroundInit->>OptionalStep: _optional_step 显示输出守卫启动
OptionalStep->>Guard: DesktopGuard.start
Guard-->>OptionalStep: success or exception
OptionalStep-->>BackgroundInit: continue
BackgroundInit->>Timer: MainTimer.start
Timer-->>BackgroundInit: success or exception
BackgroundInit->>OptionalStep: _optional_step Claw 通知管理器启动
OptionalStep->>Claw: _start_openclaw
Claw-->>OptionalStep: success or exception
OptionalStep-->>BackgroundInit: continue
BackgroundInit->>OptionalStep: _optional_step Koishi 系统客户端初始化
OptionalStep->>Koishi: _init_koishi
Koishi-->>OptionalStep: success or exception
OptionalStep-->>BackgroundInit: initialization complete
Flow diagram for atomic deployment replacementflowchart TD
Start[replaceItem] --> Prepare[Create parent directory]
Prepare --> Cleanup[Remove stale .new and .old]
Cleanup --> Stage[Copy source to target.new]
Stage -->|copy failed| Preserve[Keep existing target unchanged]
Stage --> RenameOld[Rename existing target to target.old]
RenameOld --> RenameNew[Rename target.new to target]
RenameNew -->|rename failed| Restore[Restore target.old]
RenameNew --> Success[Remove target.old]
Restore --> Failure[Raise error without partial target]
Preserve --> Failure
Success --> Done[Deployment item replaced]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
你好——我发现了 2 个问题
面向 AI Agent 的提示
请处理本次代码审查中的评论:
## 个别评论
### 评论 1
<location path="frontend/electron/services/repositoryService.ts" line_range="713-716" />
<code_context>
+ // 2. 换入:目标先让位,再把暂存改名成目标
+ const hadTarget = fs.existsSync(dstPath)
+ if (hadTarget) {
+ fs.renameSync(dstPath, backup)
+ }
+ try {
+ fs.renameSync(staged, dstPath)
+ } catch (error) {
+ if (hadTarget) {
</code_context>
<issue_to_address>
**问题 (bug_risk):** `replaceItem` 首先将现有目标重命名为 `.old`,然后才将 `.new` 重命名到目标位置。因此,如果在这两次重命名之间进程被终止或发生断电,目标就会缺失。在下一次部署时,启动清理会无条件删除 `.old`,从而永久丢失之前可正常工作的副本。
**触发条件:** 部署在旧目标完成重命名、但暂存目标尚未安装时被中断。
**建议修复:** 在可用的情况下使用原子替换原语,或者让启动恢复逻辑在目标缺失时先恢复 `.old`,然后再删除任何暂存文件。
</issue_to_address>
### 评论 2
<location path="main.py" line_range="406-408" />
<code_context>
from app.core.desktop_guard import DesktopGuard
- await DesktopGuard.start()
+ await _optional_step("显示输出守卫启动", DesktopGuard.start())
await MainTimer.start()
</code_context>
<issue_to_address>
**问题 (broader_impact):** `DesktopGuard` 模块的导入位于 `_optional_step` 外部,因此导入时异常仍会在到达 `MainTimer.start()` 之前逃逸出外层初始化代码块。这违反了既定保证:显示守卫失败时只跳过守卫,不应阻止主计时器启动。
**触发条件:** 由于缺少依赖项或其他模块级初始化错误,导入 `app.core.desktop_guard` 失败时。
**建议修复:** 将 `DesktopGuard` 的导入移入传递给 `_optional_step` 的异步包装器中,并与 `DesktopGuard.start()` 的调用放在一起。
```suggestion
async def _start_desktop_guard() -> None:
from app.core.desktop_guard import DesktopGuard
await DesktopGuard.start()
await _optional_step("显示输出守卫启动", _start_desktop_guard())
```
</issue_to_address>Sourcery 评估
需要人工审查。 需要先处理 2 个发现的问题;此外,此改动会影响多个子系统中的启动顺序、配置持久化、渲染器 IPC 以及自更新替换路径。缺陷可能导致替换失败后更新的安装无法使用,或者在没有可选守卫的情况下启动计划任务;回滚代码也不一定能修复已经中断的部署,或恢复被跳过的配置同步。
阻塞性发现:frontend/electron/services/repositoryService.ts:716、main.py:408
Original comment in English
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="frontend/electron/services/repositoryService.ts" line_range="713-716" />
<code_context>
+ // 2. 换入:目标先让位,再把暂存改名成目标
+ const hadTarget = fs.existsSync(dstPath)
+ if (hadTarget) {
+ fs.renameSync(dstPath, backup)
+ }
+ try {
+ fs.renameSync(staged, dstPath)
+ } catch (error) {
+ if (hadTarget) {
</code_context>
<issue_to_address>
**issue (bug_risk):** `replaceItem` first renames the existing target to `.old` and only then renames `.new` into place, so a process kill or power loss between those two renames leaves the target missing. On the next deployment, the startup cleanup unconditionally deletes `.old`, permanently losing the previously working copy.
**Triggers:** When deployment is interrupted after the old target is renamed but before the staged target is installed.
**Suggested fix:** Use an atomic replacement primitive where available, or make startup recovery restore `.old` when the target is absent before deleting any staging artifacts.
</issue_to_address>
### Comment 2
<location path="main.py" line_range="406-408" />
<code_context>
from app.core.desktop_guard import DesktopGuard
- await DesktopGuard.start()
+ await _optional_step("显示输出守卫启动", DesktopGuard.start())
await MainTimer.start()
</code_context>
<issue_to_address>
**issue (broader_impact):** The `DesktopGuard` module is imported outside `_optional_step`, so an import-time exception still escapes the outer initialization block before `MainTimer.start()` is reached. This violates the stated guarantee that display-guard failure only skips the guard and does not prevent the main timer from starting.
**Triggers:** When importing `app.core.desktop_guard` fails because of a missing dependency or another module-level initialization error.
**Suggested fix:** Move the `DesktopGuard` import into an async wrapper passed to `_optional_step`, alongside the call to `DesktopGuard.start()`.
```suggestion
async def _start_desktop_guard() -> None:
from app.core.desktop_guard import DesktopGuard
await DesktopGuard.start()
await _optional_step("显示输出守卫启动", _start_desktop_guard())
```
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and this changes startup sequencing, configuration persistence, renderer IPC, and the self-update replacement path across several subsystems. A defect could leave an updated installation unusable after a failed replacement, or start scheduled work without an optional guard; reverting the code would not necessarily repair an already interrupted deployment or restore skipped configuration synchronization.
Blocking findings: frontend/electron/services/repositoryService.ts:716, main.py:408
上一版把守卫启动包进了 _optional_step,但 `from app.core.desktop_guard import DesktopGuard` 仍写在 await 之外:模块导入本身抛异常时(依赖缺失、模块自身出错) 异常会直接冒泡到外层 except,主定时器跟着被跳过 —— 正是这次要修的那个症状。 把 import 移进被 _optional_step 包住的协程体,导入失败与启动失败同一条出路: 只记一条日志、跳过守卫,MainTimer.start() 照常执行。相对顺序不变,守卫仍在 主定时器之前。 验证: - python -m py_compile main.py:通过 - 从 main.py 抽出 _optional_step 与 _start_desktop_guard 真实函数体执行,并用 sys.meta_path 钩子让 desktop_guard 导入抛 ImportError:旧写法该异常冒泡、 新写法被捕获并继续走到 MainTimer.start();AST 顺序检查守卫步骤(第 412 行) 早于 MainTimer.start()(第 413 行) - ruff format --check main.py:通过(ruff check 报的 7 处 import 排序 / E402 是 既有问题,均不在本次改动的行) - python -m pytest tests/services -q:72 passed;tests --collect-only -q:exit 0
|
@sourcery-ai review |
|
Sorry @HarcoChen, your pull request is larger than the review limit of 150,000 diff characters |
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes #738initialize_background_services()把 MCP 挂载、历史清理、适配器导入、显示输出守卫、通知管理器、Koishi 全放在一个try里,前面任何一步抛异常就整体结束,排在其后的MainTimer.start()一起没起——界面一切正常、手动运行队列也正常,定时任务却整夜不触发;/api/core/health虽然回了backgroundStatus: failed与backgroundError,但前端不消费这两个字段,用户看不到任何提示。_optional_step(label, step):失败只记一条 exception 日志、继续下一步MainTimer.start()仍排在守卫之后(原有顺序要求不变),它自己的失败依旧算整体失败验证
真机启动验证。为不触发用户环境里已排期的计划任务,启动前把
MainTimer.start()临时换成一行日志,并在「后台清理」里注入RuntimeError:可选步骤失败后流程照常走到定时器那一步;修复前同样的注入会让这一步永远不执行、
background_status停在failed。临时改动验证后已撤销。其余检查:
python -m py_compile main.py:通过python -m pytest tests --collect-only -q:1412 collected,exit 0ruff format --check main.py:通过(ruff check报的 7 处 import 排序 / E402 为既有问题,不在本次改动范围)没做的部分
issue 期望的第二条——「前端消费
/api/core/health的backgroundStatus/backgroundError,failed时给出可见提示」——没做:后端字段已经备好,但那属于前端提示的交互设计(在哪个页面提示、是否阻塞、要不要给「重试后台初始化」入口),留给维护者定。Sourcery 总结
提高后台初始化、配置同步、部署更新、模拟器错误报告和 Electron IPC 通信的可靠性,确保局部故障不会扩大为整体功能中断。
错误修复:
改进:
测试:
Original summary in English
Sourcery 摘要
通过隔离可选失败、保留可恢复的部署状态,并让后端和 Electron 工作流中的错误与进度更加透明,提升应用可靠性。
错误修复:
增强功能:
部署:
测试:
Original summary in English
Summary by Sourcery
Improve application reliability by isolating optional failures, preserving recoverable deployment state, and making errors and progress visible across backend and Electron workflows.
Bug Fixes:
Enhancements:
Deployment:
Tests:
Original summary in English