Skip to content

fix(boot): 后台初始化各步各自容错,可选步骤失败不再连带跳过主定时器 - #835

Open
beichen24a1 wants to merge 12 commits into
AUTO-MAS-Project:devfrom
beichen24a1:fix/background-init-independent
Open

beichen24a1 wants to merge 12 commits into
AUTO-MAS-Project:devfrom
beichen24a1:fix/background-init-independent

Conversation

@beichen24a1

@beichen24a1 beichen24a1 commented Sep 16, 2026

Copy link
Copy Markdown

Closes #738

initialize_background_services() 把 MCP 挂载、历史清理、适配器导入、显示输出守卫、通知管理器、Koishi 全放在一个 try 里,前面任何一步抛异常就整体结束,排在其后的 MainTimer.start() 一起没起——界面一切正常、手动运行队列也正常,定时任务却整夜不触发;/api/core/health 虽然回了 backgroundStatus: failedbackgroundError,但前端不消费这两个字段,用户看不到任何提示。

  • 新增 _optional_step(label, step):失败只记一条 exception 日志、继续下一步
  • 五个可选步骤(MCP 挂载、后台清理、ArknightWin32 适配器、Claw 通知管理器、Koishi 客户端)逐个包住;显示输出守卫的失败也只跳过自己
  • MainTimer.start() 仍排在守卫之后(原有顺序要求不变),它自己的失败依旧算整体失败
  • 各步骤之间的先后顺序没有调整

验证

真机启动验证。为不触发用户环境里已排期的计划任务,启动前把 MainTimer.start() 临时换成一行日志,并在「后台清理」里注入 RuntimeError

INFO  | 主程序 | MCP 服务已挂载
ERROR | 主程序 | 后台清理失败, 已跳过: 模拟可选步骤失败
                 (完整 traceback)
INFO  | 主程序 | 主业务定时器启动(验证模式, 不真启动)
INFO  | 主程序 | 后端后台初始化完成

可选步骤失败后流程照常走到定时器那一步;修复前同样的注入会让这一步永远不执行、background_status 停在 failed。临时改动验证后已撤销。

其余检查:

  • 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 为既有问题,不在本次改动范围)

没做的部分

issue 期望的第二条——「前端消费 /api/core/healthbackgroundStatus / backgroundErrorfailed 时给出可见提示」——没做:后端字段已经备好,但那属于前端提示的交互设计(在哪个页面提示、是否阻塞、要不要给「重试后台初始化」入口),留给维护者定。

Sourcery 总结

提高后台初始化、配置同步、部署更新、模拟器错误报告和 Electron IPC 通信的可靠性,确保局部故障不会扩大为整体功能中断。

错误修复:

  • 让后台初始化中的可选服务彼此隔离,避免单个步骤失败导致主定时器未启动。
  • 让缺失或损坏的脚本配置、BetterGI manifest 及模拟器命令失败信息得到更安全且更具可读性的处理。
  • 防止部署更新中断留下残缺文件,并避免 Electron 在渲染进程销毁后继续发送 IPC 事件。
  • 修复依赖安装下载阶段进度长期停滞的问题。

改进:

  • 改进后台初始化、配置同步、脚本发现、部署替换和进程错误报告的容错能力。

测试:

  • 新增 BetterGI manifest、进程失败详情、依赖下载进度和部署替换行为的测试。
Original summary in English

Sourcery 摘要

通过隔离可选失败、保留可恢复的部署状态,并让后端和 Electron 工作流中的错误与进度更加透明,提升应用可靠性。

错误修复:

  • 隔离可选的后台初始化步骤,避免其失败导致主调度器无法启动。
  • 处理缺失或格式错误的脚本配置和 BetterGI 清单,避免相关操作失败。
  • 提供完整的模拟器命令失败详情,包括返回码和标准错误输出。
  • 防止被中断的部署留下不完整的应用文件。
  • 避免向已销毁的渲染器进程发送 Electron IPC 事件。
  • 在下载软件包期间推进依赖安装进度。

增强功能:

  • 提升后台初始化、配置同步、脚本发现、部署替换、模拟器操作和桌面 IPC 通信的容错能力与诊断能力。

部署:

  • 使用分阶段且可恢复的部署替换机制,在更新失败时保留之前的安装。

测试:

  • 增加对格式错误的 BetterGI 清单、进程失败详情、依赖下载进度和部署替换行为的测试覆盖。
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:

  • Isolate optional background initialization steps so failures do not prevent the main scheduler from starting.
  • Handle missing or malformed script configuration and BetterGI manifests without failing related operations.
  • Provide complete emulator command failure details including return codes and standard error output.
  • Prevent interrupted deployments from leaving incomplete application files.
  • Avoid sending Electron IPC events to destroyed renderer processes.
  • Make dependency installation progress advance during package downloads.

Enhancements:

  • Improve fault tolerance and diagnostics across background initialization, configuration synchronization, script discovery, deployment replacement, emulator operations, and desktop IPC communication.

Deployment:

  • Use staged, recoverable deployment replacements to preserve the previous installation when updates fail.

Tests:

  • Add coverage for malformed BetterGI manifests, process failure details, dependency download progress, and deployment replacement behavior.
Original summary in English

同一套「按 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 是既有问题,
  不在本次改动范围)
@sourcery-ai

sourcery-ai Bot commented Sep 16, 2026

Copy link
Copy Markdown

审查者指南

本 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: 初始化完成
Loading

原子部署替换流程图

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[部署条目替换完成]
Loading

文件级变更

变更 详情 文件
将后台初始化拆分为相互隔离的可选步骤,确保单步失败不会阻断主定时器启动。
  • 新增统一的可选步骤包装器,记录完整异常并继续执行,同时保留取消异常传播。
  • 分别隔离 MCP、后台清理、ArknightWin32、显示输出守卫、Claw 和 Koishi 初始化。
  • 保持既有步骤顺序,并让 MainTimer.start() 继续位于显示输出守卫之后;定时器自身失败仍使整体初始化失败。
main.py
changelog.d/fix-background-init-independent.fix.md
增强用户配置、脚本清单和模拟器命令失败场景下的容错与诊断信息。
  • 缺失或无效的脚本配置文件不再破坏配置回写或下发流程。
  • BetterGI 单个损坏 manifest 回退到目录名,不影响其他脚本。
  • 模拟器失败信息统一包含返回码、标准输出和标准错误。
app/task/BetterGI/tools/one_dragon.py
app/task/general/AutoProxy.py
app/task/general/ScriptConfig.py
app/utils/emulator/ldplayer.py
app/utils/emulator/mumu.py
app/utils/platform/common/process_runner.py
tests/task/test_bettergi_js_script_manifest.py
tests/tools/test_process_result_failure_detail.py
changelog.d/fix-bettergi-js-manifest-robust.fix.md
changelog.d/fix-emulator-failure-detail.fix.md
changelog.d/fix-general-config-copy-exists.fix.md
提升部署替换和初始化进度反馈的可靠性,并防止渲染进程销毁后的 IPC 崩溃。
  • 依赖下载阶段根据已发现包数推进进度,并用测试覆盖单调性和上限。
  • 部署条目先复制到 .new,再通过备份与重命名替换目标,失败时保留旧版本并清理残留;将 .git 放到最后复制。
  • 统一通过销毁检查后向渲染进程发送进度和状态事件。
frontend/electron/services/dependencyService.ts
frontend/electron/services/dependencyService.test.ts
frontend/electron/services/repositoryService.ts
frontend/electron/services/repositoryService.test.ts
frontend/electron/ipc/initializationHandlers.ts
changelog.d/fix-dependency-progress-advance.fix.md
changelog.d/fix-deploy-atomic-replace.fix.md
changelog.d/fix-ipc-send-destroyed-guard.fix.md

可能关联的问题


提示和命令

与 Sourcery 交互

  • 触发新的审查: 在拉取请求中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 回复审查评论,请 Sourcery 根据该评论创建 issue。你也可以回复审查评论并使用 @sourcery-ai issue,从中创建 issue。
  • 生成拉取请求标题: 在拉取请求标题的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在拉取请求中评论 @sourcery-ai title,随时重新生成标题。
  • 生成拉取请求摘要: 在拉取请求正文中任意位置写入 @sourcery-ai summary,即可在指定位置随时生成 PR 摘要。你也可以在拉取请求中评论 @sourcery-ai summary,随时重新生成摘要。
  • 生成审查者指南: 在拉取请求中评论 @sourcery-ai guide,即可随时重新生成审查者指南。
  • 解决所有 Sourcery 评论: 在拉取请求中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,此功能会很有用。
  • 忽略所有 Sourcery 审查: 在拉取请求中评论 @sourcery-ai dismiss,即可忽略所有现有的 Sourcery 审查。如果你想从新的审查开始,这尤其有用——别忘了评论 @sourcery-ai review 以触发新的审查!

自定义使用体验

访问你的控制面板以:

  • 启用或禁用审查功能,例如 Sourcery 生成的拉取请求摘要、审查者指南及其他功能。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

本 PR 以后台初始化步骤级容错为核心,保证可选服务失败时主定时器仍能启动,同时补充配置、脚本、模拟器、部署、安装进度和 IPC 生命周期相关的独立可靠性修复,并为关键行为增加测试。

Sequence diagram for resilient background initialization

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: 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
Loading

Flow diagram for atomic deployment replacement

flowchart 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]
Loading

File-Level Changes

Change Details Files
将后台初始化拆分为相互隔离的可选步骤,确保单步失败不会阻断主定时器启动。
  • 新增统一的可选步骤包装器,记录完整异常并继续执行,同时保留取消异常传播。
  • 分别隔离 MCP、后台清理、ArknightWin32、显示输出守卫、Claw 和 Koishi 初始化。
  • 保持既有步骤顺序,并让 MainTimer.start() 继续位于显示输出守卫之后;定时器自身失败仍使整体初始化失败。
main.py
changelog.d/fix-background-init-independent.fix.md
增强用户配置、脚本清单和模拟器命令失败场景下的容错与诊断信息。
  • 缺失或无效的脚本配置文件不再破坏配置回写或下发流程。
  • BetterGI 单个损坏 manifest 回退到目录名,不影响其他脚本。
  • 模拟器失败信息统一包含返回码、标准输出和标准错误。
app/task/BetterGI/tools/one_dragon.py
app/task/general/AutoProxy.py
app/task/general/ScriptConfig.py
app/utils/emulator/ldplayer.py
app/utils/emulator/mumu.py
app/utils/platform/common/process_runner.py
tests/task/test_bettergi_js_script_manifest.py
tests/tools/test_process_result_failure_detail.py
changelog.d/fix-bettergi-js-manifest-robust.fix.md
changelog.d/fix-emulator-failure-detail.fix.md
changelog.d/fix-general-config-copy-exists.fix.md
提升部署替换和初始化进度反馈的可靠性,并防止渲染进程销毁后的 IPC 崩溃。
  • 依赖下载阶段根据已发现包数推进进度,并用测试覆盖单调性和上限。
  • 部署条目先复制到 .new,再通过备份与重命名替换目标,失败时保留旧版本并清理残留;将 .git 放到最后复制。
  • 统一通过销毁检查后向渲染进程发送进度和状态事件。
frontend/electron/services/dependencyService.ts
frontend/electron/services/dependencyService.test.ts
frontend/electron/services/repositoryService.ts
frontend/electron/services/repositoryService.test.ts
frontend/electron/ipc/initializationHandlers.ts
changelog.d/fix-dependency-progress-advance.fix.md
changelog.d/fix-deploy-atomic-replace.fix.md
changelog.d/fix-ipc-send-destroyed-guard.fix.md

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@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.

你好——我发现了 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:716main.py:408


Sourcery 对开源项目免费——如果您喜欢我们的审查结果,请考虑分享它们 ✨
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


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread frontend/electron/services/repositoryService.ts Outdated
Comment thread main.py Outdated
上一版把守卫启动包进了 _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
@HarcoChen

Copy link
Copy Markdown
Contributor

@sourcery-ai review

@sourcery-ai

sourcery-ai Bot commented Sep 17, 2026

Copy link
Copy Markdown

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>
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.

3 participants