Skip to content

Conversation

@zouyonghe
Copy link
Collaborator

@zouyonghe zouyonghe commented Feb 10, 2026

  • 修复了 Windows 打包版在“设置 -> 重启”场景下,首次启动正常但重启后崩溃的问题。
  • 在 frozen(onefile) 重启前重置 PyInstaller 运行时环境:
    • 设置 PYINSTALLER_RESET_ENVIRONMENT=1
    • 清理继承的 PYI* 环境变量
  • 将重启执行改为 os.execv(...),避免 Windows 下参数/引号处理导致的二次启动异常。
  • 不影响源码运行路径(非 frozen 模式行为保持兼容)。

由 Sourcery 提供的摘要

提高在 Windows 打包环境下后端重启的可靠性,并在桌面端、后端和控制面板之间统一重启处理逻辑。

错误修复:

  • 通过重置冻结的运行时环境并切换到基于 exec 的重启语义,确保 Windows 单文件(PyInstaller)后端能够可靠重启。
  • 在 Windows 桌面应用关闭或重启时,尝试停止绑定到配置端口的任意后端进程(包括未受管实例),以避免留下孤立的后端进程。
  • 使控制面板的重启等待逻辑更加健壮,即使在初始后端启动时间不可用的情况下,也能检测到重启是否成功。

功能增强:

  • 向 Electron 运行时暴露后端端口和 Web UI 目录,以支持基于端口的后端清理,并实现一致的 Web UI 解析。
  • 优化冻结模式和 CLI 模式下的后端重启行为,在重新执行进程时保留或重建诸如 Web UI 目录等关键参数。
  • 将前端重启行为集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以提供一致的重启体验。
Original summary in English

Summary by Sourcery

在 Windows 打包后端、Electron 桌面应用和 Web 仪表盘之间,提高后端重启行为的可靠性和一致性。

Bug 修复:

  • 通过重置冻结运行时环境并切换为基于 exec 的重启语义,确保 Windows PyInstaller 单文件后端在重启时不会崩溃。
  • 当 Windows 桌面应用退出或重启时,停止绑定到配置端口的任何后端进程(包括未托管实例),以避免产生“孤儿”后端。
  • 使仪表盘在等待重启的逻辑更加健壮,即使无法获取初始后端启动时间,也能检测到重启是否成功。

增强功能:

  • 将后端端口和 Web UI 目录暴露给 Electron 运行时,并用它们来进行基于端口的后端清理以及一致的 Web UI 解析。
  • 通过在桌面后端管理器中添加优雅的进程内重启支持来统一并强化重启流程,并在需要时回退到托管的停止/启动方式。
  • 在冻结模式和 CLI 模式下,在重启过程中保留或重建 Web UI 目录等关键参数,并将前端的重启行为集中到共享的 restartAstrBot 工具中,以便在设置、备份、迁移和配置视图中统一使用。
Original summary in English

Summary by Sourcery

Improve reliability and consistency of backend restart behavior across Windows packaged backend, Electron desktop app, and web dashboard.

Bug Fixes:

  • Ensure Windows PyInstaller one-file backend can restart without crashing by resetting the frozen runtime environment and switching to exec-based restart semantics.
  • Stop any backend processes bound to the configured port when the Windows desktop app exits or restarts, including unmanaged instances, to avoid orphaned backends.
  • Make dashboard restart-waiting logic more robust so it can detect successful restarts even when the initial backend start time cannot be obtained.

Enhancements:

  • Expose backend port and Web UI directory to the Electron runtime and use them for port-based backend cleanup and consistent Web UI resolution.
  • Unify and harden restart flows by adding graceful in-process restart support in the desktop backend manager, falling back to managed stop/start when needed.
  • Preserve or reconstruct key arguments such as the Web UI directory across restarts for both frozen and CLI modes, and centralize frontend restart behavior into a shared restartAstrBot utility used across settings, backup, migration, and config views.

Bug 修复:

  • 通过重置 PyInstaller 运行时环境并使用基于 exec 的重启语义,确保 Windows 单文件打包的后端能够可靠地重启。
  • 在 Windows 桌面关闭时,停止所有绑定到配置端口的后端进程(包括未受管理的实例),以避免产生孤立后端进程。
  • 改进控制面板在等待重启时的行为,即使无法获取初始启动时间,也能正确检测后端是否已重启。

增强:

  • 将后端端口和 Web UI 目录暴露给 Electron 环境,以支持基于端口的后端清理以及一致的 Web UI 解析。
  • 优化在冻结模式和 CLI 模式下的后端重启参数处理,以保留或重建 Web UI 目录等关键参数。
  • 将前端重启逻辑集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以实现一致的重启行为。
Original summary in English

由 Sourcery 提供的摘要

提高在 Windows 打包环境下后端重启的可靠性,并在桌面端、后端和控制面板之间统一重启处理逻辑。

错误修复:

  • 通过重置冻结的运行时环境并切换到基于 exec 的重启语义,确保 Windows 单文件(PyInstaller)后端能够可靠重启。
  • 在 Windows 桌面应用关闭或重启时,尝试停止绑定到配置端口的任意后端进程(包括未受管实例),以避免留下孤立的后端进程。
  • 使控制面板的重启等待逻辑更加健壮,即使在初始后端启动时间不可用的情况下,也能检测到重启是否成功。

功能增强:

  • 向 Electron 运行时暴露后端端口和 Web UI 目录,以支持基于端口的后端清理,并实现一致的 Web UI 解析。
  • 优化冻结模式和 CLI 模式下的后端重启行为,在重新执行进程时保留或重建诸如 Web UI 目录等关键参数。
  • 将前端重启行为集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以提供一致的重启体验。
Original summary in English

Summary by Sourcery

在 Windows 打包后端、Electron 桌面应用和 Web 仪表盘之间,提高后端重启行为的可靠性和一致性。

Bug 修复:

  • 通过重置冻结运行时环境并切换为基于 exec 的重启语义,确保 Windows PyInstaller 单文件后端在重启时不会崩溃。
  • 当 Windows 桌面应用退出或重启时,停止绑定到配置端口的任何后端进程(包括未托管实例),以避免产生“孤儿”后端。
  • 使仪表盘在等待重启的逻辑更加健壮,即使无法获取初始后端启动时间,也能检测到重启是否成功。

增强功能:

  • 将后端端口和 Web UI 目录暴露给 Electron 运行时,并用它们来进行基于端口的后端清理以及一致的 Web UI 解析。
  • 通过在桌面后端管理器中添加优雅的进程内重启支持来统一并强化重启流程,并在需要时回退到托管的停止/启动方式。
  • 在冻结模式和 CLI 模式下,在重启过程中保留或重建 Web UI 目录等关键参数,并将前端的重启行为集中到共享的 restartAstrBot 工具中,以便在设置、备份、迁移和配置视图中统一使用。
Original summary in English

Summary by Sourcery

Improve reliability and consistency of backend restart behavior across Windows packaged backend, Electron desktop app, and web dashboard.

Bug Fixes:

  • Ensure Windows PyInstaller one-file backend can restart without crashing by resetting the frozen runtime environment and switching to exec-based restart semantics.
  • Stop any backend processes bound to the configured port when the Windows desktop app exits or restarts, including unmanaged instances, to avoid orphaned backends.
  • Make dashboard restart-waiting logic more robust so it can detect successful restarts even when the initial backend start time cannot be obtained.

Enhancements:

  • Expose backend port and Web UI directory to the Electron runtime and use them for port-based backend cleanup and consistent Web UI resolution.
  • Unify and harden restart flows by adding graceful in-process restart support in the desktop backend manager, falling back to managed stop/start when needed.
  • Preserve or reconstruct key arguments such as the Web UI directory across restarts for both frozen and CLI modes, and centralize frontend restart behavior into a shared restartAstrBot utility used across settings, backup, migration, and config views.

Bug 修复:

  • 修复在重启 Windows 单文件打包后端时的崩溃问题:通过正确重置 PyInstaller 运行时环境,并使用基于 exec 的重启语义。
  • 确保 Windows 桌面端在关闭时,会尝试停止绑定到配置端口的任意后端实例(无论是否受管理),避免在重启或退出时遗留孤立的后端进程。
  • 改进仪表盘重启等待逻辑,即使初始启动时间获取失败,也能正确检测到后端已重启。

增强功能:

  • 将后端端口和 Web UI 目录暴露给 Electron 环境,以支持基于端口的后端清理,并实现一致的 Web UI 解析路径。
  • 优化后端重启行为,使用 os.execv,并在 CLI 和冻结(打包)模式下,在重启过程中保留或重建关键参数(例如 --webui-dir)。
  • 将前端重启逻辑集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一使用,以实现一致的行为。
Original summary in English

由 Sourcery 提供的摘要

提高在 Windows 打包环境下后端重启的可靠性,并在桌面端、后端和控制面板之间统一重启处理逻辑。

错误修复:

  • 通过重置冻结的运行时环境并切换到基于 exec 的重启语义,确保 Windows 单文件(PyInstaller)后端能够可靠重启。
  • 在 Windows 桌面应用关闭或重启时,尝试停止绑定到配置端口的任意后端进程(包括未受管实例),以避免留下孤立的后端进程。
  • 使控制面板的重启等待逻辑更加健壮,即使在初始后端启动时间不可用的情况下,也能检测到重启是否成功。

功能增强:

  • 向 Electron 运行时暴露后端端口和 Web UI 目录,以支持基于端口的后端清理,并实现一致的 Web UI 解析。
  • 优化冻结模式和 CLI 模式下的后端重启行为,在重新执行进程时保留或重建诸如 Web UI 目录等关键参数。
  • 将前端重启行为集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以提供一致的重启体验。
Original summary in English

Summary by Sourcery

在 Windows 打包后端、Electron 桌面应用和 Web 仪表盘之间,提高后端重启行为的可靠性和一致性。

Bug 修复:

  • 通过重置冻结运行时环境并切换为基于 exec 的重启语义,确保 Windows PyInstaller 单文件后端在重启时不会崩溃。
  • 当 Windows 桌面应用退出或重启时,停止绑定到配置端口的任何后端进程(包括未托管实例),以避免产生“孤儿”后端。
  • 使仪表盘在等待重启的逻辑更加健壮,即使无法获取初始后端启动时间,也能检测到重启是否成功。

增强功能:

  • 将后端端口和 Web UI 目录暴露给 Electron 运行时,并用它们来进行基于端口的后端清理以及一致的 Web UI 解析。
  • 通过在桌面后端管理器中添加优雅的进程内重启支持来统一并强化重启流程,并在需要时回退到托管的停止/启动方式。
  • 在冻结模式和 CLI 模式下,在重启过程中保留或重建 Web UI 目录等关键参数,并将前端的重启行为集中到共享的 restartAstrBot 工具中,以便在设置、备份、迁移和配置视图中统一使用。
Original summary in English

Summary by Sourcery

Improve reliability and consistency of backend restart behavior across Windows packaged backend, Electron desktop app, and web dashboard.

Bug Fixes:

  • Ensure Windows PyInstaller one-file backend can restart without crashing by resetting the frozen runtime environment and switching to exec-based restart semantics.
  • Stop any backend processes bound to the configured port when the Windows desktop app exits or restarts, including unmanaged instances, to avoid orphaned backends.
  • Make dashboard restart-waiting logic more robust so it can detect successful restarts even when the initial backend start time cannot be obtained.

Enhancements:

  • Expose backend port and Web UI directory to the Electron runtime and use them for port-based backend cleanup and consistent Web UI resolution.
  • Unify and harden restart flows by adding graceful in-process restart support in the desktop backend manager, falling back to managed stop/start when needed.
  • Preserve or reconstruct key arguments such as the Web UI directory across restarts for both frozen and CLI modes, and centralize frontend restart behavior into a shared restartAstrBot utility used across settings, backup, migration, and config views.

Bug 修复:

  • 通过重置 PyInstaller 运行时环境并使用基于 exec 的重启语义,确保 Windows 单文件打包的后端能够可靠地重启。
  • 在 Windows 桌面关闭时,停止所有绑定到配置端口的后端进程(包括未受管理的实例),以避免产生孤立后端进程。
  • 改进控制面板在等待重启时的行为,即使无法获取初始启动时间,也能正确检测后端是否已重启。

增强:

  • 将后端端口和 Web UI 目录暴露给 Electron 环境,以支持基于端口的后端清理以及一致的 Web UI 解析。
  • 优化在冻结模式和 CLI 模式下的后端重启参数处理,以保留或重建 Web UI 目录等关键参数。
  • 将前端重启逻辑集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以实现一致的重启行为。
Original summary in English

由 Sourcery 提供的摘要

提高在 Windows 打包环境下后端重启的可靠性,并在桌面端、后端和控制面板之间统一重启处理逻辑。

错误修复:

  • 通过重置冻结的运行时环境并切换到基于 exec 的重启语义,确保 Windows 单文件(PyInstaller)后端能够可靠重启。
  • 在 Windows 桌面应用关闭或重启时,尝试停止绑定到配置端口的任意后端进程(包括未受管实例),以避免留下孤立的后端进程。
  • 使控制面板的重启等待逻辑更加健壮,即使在初始后端启动时间不可用的情况下,也能检测到重启是否成功。

功能增强:

  • 向 Electron 运行时暴露后端端口和 Web UI 目录,以支持基于端口的后端清理,并实现一致的 Web UI 解析。
  • 优化冻结模式和 CLI 模式下的后端重启行为,在重新执行进程时保留或重建诸如 Web UI 目录等关键参数。
  • 将前端重启行为集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以提供一致的重启体验。
Original summary in English

Summary by Sourcery

在 Windows 打包后端、Electron 桌面应用和 Web 仪表盘之间,提高后端重启行为的可靠性和一致性。

Bug 修复:

  • 通过重置冻结运行时环境并切换为基于 exec 的重启语义,确保 Windows PyInstaller 单文件后端在重启时不会崩溃。
  • 当 Windows 桌面应用退出或重启时,停止绑定到配置端口的任何后端进程(包括未托管实例),以避免产生“孤儿”后端。
  • 使仪表盘在等待重启的逻辑更加健壮,即使无法获取初始后端启动时间,也能检测到重启是否成功。

增强功能:

  • 将后端端口和 Web UI 目录暴露给 Electron 运行时,并用它们来进行基于端口的后端清理以及一致的 Web UI 解析。
  • 通过在桌面后端管理器中添加优雅的进程内重启支持来统一并强化重启流程,并在需要时回退到托管的停止/启动方式。
  • 在冻结模式和 CLI 模式下,在重启过程中保留或重建 Web UI 目录等关键参数,并将前端的重启行为集中到共享的 restartAstrBot 工具中,以便在设置、备份、迁移和配置视图中统一使用。
Original summary in English

Summary by Sourcery

Improve reliability and consistency of backend restart behavior across Windows packaged backend, Electron desktop app, and web dashboard.

Bug Fixes:

  • Ensure Windows PyInstaller one-file backend can restart without crashing by resetting the frozen runtime environment and switching to exec-based restart semantics.
  • Stop any backend processes bound to the configured port when the Windows desktop app exits or restarts, including unmanaged instances, to avoid orphaned backends.
  • Make dashboard restart-waiting logic more robust so it can detect successful restarts even when the initial backend start time cannot be obtained.

Enhancements:

  • Expose backend port and Web UI directory to the Electron runtime and use them for port-based backend cleanup and consistent Web UI resolution.
  • Unify and harden restart flows by adding graceful in-process restart support in the desktop backend manager, falling back to managed stop/start when needed.
  • Preserve or reconstruct key arguments such as the Web UI directory across restarts for both frozen and CLI modes, and centralize frontend restart behavior into a shared restartAstrBot utility used across settings, backup, migration, and config views.
Original summary in English

由 Sourcery 提供的摘要

提高在 Windows 打包环境下后端重启的可靠性,并在桌面端、后端和控制面板之间统一重启处理逻辑。

错误修复:

  • 通过重置冻结的运行时环境并切换到基于 exec 的重启语义,确保 Windows 单文件(PyInstaller)后端能够可靠重启。
  • 在 Windows 桌面应用关闭或重启时,尝试停止绑定到配置端口的任意后端进程(包括未受管实例),以避免留下孤立的后端进程。
  • 使控制面板的重启等待逻辑更加健壮,即使在初始后端启动时间不可用的情况下,也能检测到重启是否成功。

功能增强:

  • 向 Electron 运行时暴露后端端口和 Web UI 目录,以支持基于端口的后端清理,并实现一致的 Web UI 解析。
  • 优化冻结模式和 CLI 模式下的后端重启行为,在重新执行进程时保留或重建诸如 Web UI 目录等关键参数。
  • 将前端重启行为集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以提供一致的重启体验。
Original summary in English

Summary by Sourcery

在 Windows 打包后端、Electron 桌面应用和 Web 仪表盘之间,提高后端重启行为的可靠性和一致性。

Bug 修复:

  • 通过重置冻结运行时环境并切换为基于 exec 的重启语义,确保 Windows PyInstaller 单文件后端在重启时不会崩溃。
  • 当 Windows 桌面应用退出或重启时,停止绑定到配置端口的任何后端进程(包括未托管实例),以避免产生“孤儿”后端。
  • 使仪表盘在等待重启的逻辑更加健壮,即使无法获取初始后端启动时间,也能检测到重启是否成功。

增强功能:

  • 将后端端口和 Web UI 目录暴露给 Electron 运行时,并用它们来进行基于端口的后端清理以及一致的 Web UI 解析。
  • 通过在桌面后端管理器中添加优雅的进程内重启支持来统一并强化重启流程,并在需要时回退到托管的停止/启动方式。
  • 在冻结模式和 CLI 模式下,在重启过程中保留或重建 Web UI 目录等关键参数,并将前端的重启行为集中到共享的 restartAstrBot 工具中,以便在设置、备份、迁移和配置视图中统一使用。
Original summary in English

Summary by Sourcery

Improve reliability and consistency of backend restart behavior across Windows packaged backend, Electron desktop app, and web dashboard.

Bug Fixes:

  • Ensure Windows PyInstaller one-file backend can restart without crashing by resetting the frozen runtime environment and switching to exec-based restart semantics.
  • Stop any backend processes bound to the configured port when the Windows desktop app exits or restarts, including unmanaged instances, to avoid orphaned backends.
  • Make dashboard restart-waiting logic more robust so it can detect successful restarts even when the initial backend start time cannot be obtained.

Enhancements:

  • Expose backend port and Web UI directory to the Electron runtime and use them for port-based backend cleanup and consistent Web UI resolution.
  • Unify and harden restart flows by adding graceful in-process restart support in the desktop backend manager, falling back to managed stop/start when needed.
  • Preserve or reconstruct key arguments such as the Web UI directory across restarts for both frozen and CLI modes, and centralize frontend restart behavior into a shared restartAstrBot utility used across settings, backup, migration, and config views.

Bug 修复:

  • 通过重置 PyInstaller 运行时环境并使用基于 exec 的重启语义,确保 Windows 单文件打包的后端能够可靠地重启。
  • 在 Windows 桌面关闭时,停止所有绑定到配置端口的后端进程(包括未受管理的实例),以避免产生孤立后端进程。
  • 改进控制面板在等待重启时的行为,即使无法获取初始启动时间,也能正确检测后端是否已重启。

增强:

  • 将后端端口和 Web UI 目录暴露给 Electron 环境,以支持基于端口的后端清理以及一致的 Web UI 解析。
  • 优化在冻结模式和 CLI 模式下的后端重启参数处理,以保留或重建 Web UI 目录等关键参数。
  • 将前端重启逻辑集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以实现一致的重启行为。
Original summary in English

由 Sourcery 提供的摘要

提高在 Windows 打包环境下后端重启的可靠性,并在桌面端、后端和控制面板之间统一重启处理逻辑。

错误修复:

  • 通过重置冻结的运行时环境并切换到基于 exec 的重启语义,确保 Windows 单文件(PyInstaller)后端能够可靠重启。
  • 在 Windows 桌面应用关闭或重启时,尝试停止绑定到配置端口的任意后端进程(包括未受管实例),以避免留下孤立的后端进程。
  • 使控制面板的重启等待逻辑更加健壮,即使在初始后端启动时间不可用的情况下,也能检测到重启是否成功。

功能增强:

  • 向 Electron 运行时暴露后端端口和 Web UI 目录,以支持基于端口的后端清理,并实现一致的 Web UI 解析。
  • 优化冻结模式和 CLI 模式下的后端重启行为,在重新执行进程时保留或重建诸如 Web UI 目录等关键参数。
  • 将前端重启行为集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以提供一致的重启体验。
Original summary in English

Summary by Sourcery

在 Windows 打包后端、Electron 桌面应用和 Web 仪表盘之间,提高后端重启行为的可靠性和一致性。

Bug 修复:

  • 通过重置冻结运行时环境并切换为基于 exec 的重启语义,确保 Windows PyInstaller 单文件后端在重启时不会崩溃。
  • 当 Windows 桌面应用退出或重启时,停止绑定到配置端口的任何后端进程(包括未托管实例),以避免产生“孤儿”后端。
  • 使仪表盘在等待重启的逻辑更加健壮,即使无法获取初始后端启动时间,也能检测到重启是否成功。

增强功能:

  • 将后端端口和 Web UI 目录暴露给 Electron 运行时,并用它们来进行基于端口的后端清理以及一致的 Web UI 解析。
  • 通过在桌面后端管理器中添加优雅的进程内重启支持来统一并强化重启流程,并在需要时回退到托管的停止/启动方式。
  • 在冻结模式和 CLI 模式下,在重启过程中保留或重建 Web UI 目录等关键参数,并将前端的重启行为集中到共享的 restartAstrBot 工具中,以便在设置、备份、迁移和配置视图中统一使用。
Original summary in English

Summary by Sourcery

Improve reliability and consistency of backend restart behavior across Windows packaged backend, Electron desktop app, and web dashboard.

Bug Fixes:

  • Ensure Windows PyInstaller one-file backend can restart without crashing by resetting the frozen runtime environment and switching to exec-based restart semantics.
  • Stop any backend processes bound to the configured port when the Windows desktop app exits or restarts, including unmanaged instances, to avoid orphaned backends.
  • Make dashboard restart-waiting logic more robust so it can detect successful restarts even when the initial backend start time cannot be obtained.

Enhancements:

  • Expose backend port and Web UI directory to the Electron runtime and use them for port-based backend cleanup and consistent Web UI resolution.
  • Unify and harden restart flows by adding graceful in-process restart support in the desktop backend manager, falling back to managed stop/start when needed.
  • Preserve or reconstruct key arguments such as the Web UI directory across restarts for both frozen and CLI modes, and centralize frontend restart behavior into a shared restartAstrBot utility used across settings, backup, migration, and config views.

Bug 修复:

  • 修复在重启 Windows 单文件打包后端时的崩溃问题:通过正确重置 PyInstaller 运行时环境,并使用基于 exec 的重启语义。
  • 确保 Windows 桌面端在关闭时,会尝试停止绑定到配置端口的任意后端实例(无论是否受管理),避免在重启或退出时遗留孤立的后端进程。
  • 改进仪表盘重启等待逻辑,即使初始启动时间获取失败,也能正确检测到后端已重启。

增强功能:

  • 将后端端口和 Web UI 目录暴露给 Electron 环境,以支持基于端口的后端清理,并实现一致的 Web UI 解析路径。
  • 优化后端重启行为,使用 os.execv,并在 CLI 和冻结(打包)模式下,在重启过程中保留或重建关键参数(例如 --webui-dir)。
  • 将前端重启逻辑集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一使用,以实现一致的行为。
Original summary in English

由 Sourcery 提供的摘要

提高在 Windows 打包环境下后端重启的可靠性,并在桌面端、后端和控制面板之间统一重启处理逻辑。

错误修复:

  • 通过重置冻结的运行时环境并切换到基于 exec 的重启语义,确保 Windows 单文件(PyInstaller)后端能够可靠重启。
  • 在 Windows 桌面应用关闭或重启时,尝试停止绑定到配置端口的任意后端进程(包括未受管实例),以避免留下孤立的后端进程。
  • 使控制面板的重启等待逻辑更加健壮,即使在初始后端启动时间不可用的情况下,也能检测到重启是否成功。

功能增强:

  • 向 Electron 运行时暴露后端端口和 Web UI 目录,以支持基于端口的后端清理,并实现一致的 Web UI 解析。
  • 优化冻结模式和 CLI 模式下的后端重启行为,在重新执行进程时保留或重建诸如 Web UI 目录等关键参数。
  • 将前端重启行为集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以提供一致的重启体验。
Original summary in English

Summary by Sourcery

在 Windows 打包后端、Electron 桌面应用和 Web 仪表盘之间,提高后端重启行为的可靠性和一致性。

Bug 修复:

  • 通过重置冻结运行时环境并切换为基于 exec 的重启语义,确保 Windows PyInstaller 单文件后端在重启时不会崩溃。
  • 当 Windows 桌面应用退出或重启时,停止绑定到配置端口的任何后端进程(包括未托管实例),以避免产生“孤儿”后端。
  • 使仪表盘在等待重启的逻辑更加健壮,即使无法获取初始后端启动时间,也能检测到重启是否成功。

增强功能:

  • 将后端端口和 Web UI 目录暴露给 Electron 运行时,并用它们来进行基于端口的后端清理以及一致的 Web UI 解析。
  • 通过在桌面后端管理器中添加优雅的进程内重启支持来统一并强化重启流程,并在需要时回退到托管的停止/启动方式。
  • 在冻结模式和 CLI 模式下,在重启过程中保留或重建 Web UI 目录等关键参数,并将前端的重启行为集中到共享的 restartAstrBot 工具中,以便在设置、备份、迁移和配置视图中统一使用。
Original summary in English

Summary by Sourcery

Improve reliability and consistency of backend restart behavior across Windows packaged backend, Electron desktop app, and web dashboard.

Bug Fixes:

  • Ensure Windows PyInstaller one-file backend can restart without crashing by resetting the frozen runtime environment and switching to exec-based restart semantics.
  • Stop any backend processes bound to the configured port when the Windows desktop app exits or restarts, including unmanaged instances, to avoid orphaned backends.
  • Make dashboard restart-waiting logic more robust so it can detect successful restarts even when the initial backend start time cannot be obtained.

Enhancements:

  • Expose backend port and Web UI directory to the Electron runtime and use them for port-based backend cleanup and consistent Web UI resolution.
  • Unify and harden restart flows by adding graceful in-process restart support in the desktop backend manager, falling back to managed stop/start when needed.
  • Preserve or reconstruct key arguments such as the Web UI directory across restarts for both frozen and CLI modes, and centralize frontend restart behavior into a shared restartAstrBot utility used across settings, backup, migration, and config views.

Bug 修复:

  • 通过重置 PyInstaller 运行时环境并使用基于 exec 的重启语义,确保 Windows 单文件打包的后端能够可靠地重启。
  • 在 Windows 桌面关闭时,停止所有绑定到配置端口的后端进程(包括未受管理的实例),以避免产生孤立后端进程。
  • 改进控制面板在等待重启时的行为,即使无法获取初始启动时间,也能正确检测后端是否已重启。

增强:

  • 将后端端口和 Web UI 目录暴露给 Electron 环境,以支持基于端口的后端清理以及一致的 Web UI 解析。
  • 优化在冻结模式和 CLI 模式下的后端重启参数处理,以保留或重建 Web UI 目录等关键参数。
  • 将前端重启逻辑集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以实现一致的重启行为。
Original summary in English

由 Sourcery 提供的摘要

提高在 Windows 打包环境下后端重启的可靠性,并在桌面端、后端和控制面板之间统一重启处理逻辑。

错误修复:

  • 通过重置冻结的运行时环境并切换到基于 exec 的重启语义,确保 Windows 单文件(PyInstaller)后端能够可靠重启。
  • 在 Windows 桌面应用关闭或重启时,尝试停止绑定到配置端口的任意后端进程(包括未受管实例),以避免留下孤立的后端进程。
  • 使控制面板的重启等待逻辑更加健壮,即使在初始后端启动时间不可用的情况下,也能检测到重启是否成功。

功能增强:

  • 向 Electron 运行时暴露后端端口和 Web UI 目录,以支持基于端口的后端清理,并实现一致的 Web UI 解析。
  • 优化冻结模式和 CLI 模式下的后端重启行为,在重新执行进程时保留或重建诸如 Web UI 目录等关键参数。
  • 将前端重启行为集中到共享的 restartAstrBot 工具中,并在设置、备份、迁移和配置视图中统一采用,以提供一致的重启体验。
Original summary in English

Summary by Sourcery

在 Windows 打包后端、Electron 桌面应用和 Web 仪表盘之间,提高后端重启行为的可靠性和一致性。

Bug 修复:

  • 通过重置冻结运行时环境并切换为基于 exec 的重启语义,确保 Windows PyInstaller 单文件后端在重启时不会崩溃。
  • 当 Windows 桌面应用退出或重启时,停止绑定到配置端口的任何后端进程(包括未托管实例),以避免产生“孤儿”后端。
  • 使仪表盘在等待重启的逻辑更加健壮,即使无法获取初始后端启动时间,也能检测到重启是否成功。

增强功能:

  • 将后端端口和 Web UI 目录暴露给 Electron 运行时,并用它们来进行基于端口的后端清理以及一致的 Web UI 解析。
  • 通过在桌面后端管理器中添加优雅的进程内重启支持来统一并强化重启流程,并在需要时回退到托管的停止/启动方式。
  • 在冻结模式和 CLI 模式下,在重启过程中保留或重建 Web UI 目录等关键参数,并将前端的重启行为集中到共享的 restartAstrBot 工具中,以便在设置、备份、迁移和配置视图中统一使用。
Original summary in English

Summary by Sourcery

Improve reliability and consistency of backend restart behavior across Windows packaged backend, Electron desktop app, and web dashboard.

Bug Fixes:

  • Ensure Windows PyInstaller one-file backend can restart without crashing by resetting the frozen runtime environment and switching to exec-based restart semantics.
  • Stop any backend processes bound to the configured port when the Windows desktop app exits or restarts, including unmanaged instances, to avoid orphaned backends.
  • Make dashboard restart-waiting logic more robust so it can detect successful restarts even when the initial backend start time cannot be obtained.

Enhancements:

  • Expose backend port and Web UI directory to the Electron runtime and use them for port-based backend cleanup and consistent Web UI resolution.
  • Unify and harden restart flows by adding graceful in-process restart support in the desktop backend manager, falling back to managed stop/start when needed.
  • Preserve or reconstruct key arguments such as the Web UI directory across restarts for both frozen and CLI modes, and centralize frontend restart behavior into a shared restartAstrBot utility used across settings, backup, migration, and config views.

zouyonghe and others added 22 commits February 10, 2026 01:30
- Add shared confirm helper in dashboard/src/utils/confirmDialog.ts for async dialog usage with safe fallback.

- Migrate provider, chat, config, session, platform, persona, MCP, backup, and knowledge-base delete/close confirmations to use the shared helper.

- Remove scattered inline confirm handling to keep behavior consistent and avoid native blocking dialog focus/caret issues in Electron.
- Add bootstrap record buffer in runtime_bootstrap for early TLS patch logs before logger is ready.

- Flush buffered bootstrap logs to astrbot logger at process startup in main.py.

- Include concrete exception details for TLS bootstrap failures to improve diagnosis.
- Simplify bootstrap log buffering and add a public initialize hook for non-main startup paths.

- Guard aiohttp TLS patching with feature/type checks and keep graceful fallback when internals are unavailable.

- Standardize dashboard confirmation flow via shared confirm helpers across composition and options API components.
@dosubot dosubot bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Feb 10, 2026
@dosubot
Copy link

dosubot bot commented Feb 10, 2026

Related Documentation

Checked 1 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我已经审查了你的更改,一切看起来都很棒!


Sourcery 对开源项目是免费的——如果你喜欢我们的评审,请考虑分享给更多人 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进以后的评审。
Original comment in English

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@zouyonghe zouyonghe requested a review from Raven95676 February 10, 2026 08:39
@dosubot dosubot bot added the feature:updater The bug / feature is about astrbot updater system label Feb 10, 2026
@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Feb 10, 2026
@zouyonghe
Copy link
Collaborator Author

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了两个问题,并给出了一些整体性的反馈:

  • _resolve_webui_dir_arg 中,连续出现的两个 if arg.startswith("--") 判断都会执行 break,可以合并为一个 if arg.startswith("--"): 条件,这会简化控制流并让意图更清晰。
  • Windows 特定路径 findListeningPidsOnWindows/stopUnmanagedBackendByPortnetstattaskkill 使用了 spawnSync;由于这些是阻塞调用,建议加一个简要注释解释这里同步行为是可以接受的(因为它只在关闭/重启时运行),以避免未来的重构不小心把它移到对延迟敏感的路径中。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_resolve_webui_dir_arg`, the two consecutive `if arg.startswith("--")` checks both break and can be merged into a single `if arg.startswith("--"):` condition, which would simplify the control flow and make the intent clearer.
- The Windows-specific `findListeningPidsOnWindows`/`stopUnmanagedBackendByPort` path uses `spawnSync` for `netstat` and `taskkill`; since these are blocking calls, consider adding a brief comment explaining that the synchronous behavior is acceptable here (because it only runs during shutdown/restart) to avoid future refactors accidentally moving it into latency-sensitive paths.

## Individual Comments

### Comment 1
<location> `desktop/lib/backend-manager.js:435-440` </location>
<code_context>
+  findListeningPidsOnWindows(port) {
</code_context>

<issue_to_address>
**suggestion:** 考虑收紧对 `netstat` 输出的解析逻辑,以避免在边界情况中错误识别端口。

当前在 `findListeningPidsOnWindows` 中的解析逻辑:

```js
if (
  localAddress.endsWith(`:${port}`) ||
  localAddress.endsWith(`]:${port}`)
) {
  pids.add(pid);
}
```

依赖 `endsWith`,如果 `netstat` 输出中包含 IPv6 区域索引或其他格式(例如 `fe80::1%12:8080`),就可能比较脆弱。同时它忽略了状态列,因此你可能会收集到 `ESTABLISHED`/`TIME_WAIT` 等连接状态的 PID,而不仅仅是监听中的端口。

为了让这一逻辑更加健壮,你可以显式检查状态并解析端口:

```js
const state = parts[3];
if (state !== 'LISTENING') continue;

const [host, portStr] = localAddress.replace(/\]$/, '').split(':').slice(-2);
if (Number(portStr) === port) {
  pids.add(pid);
}
```

这样可以将结果限制为真正处于监听状态的套接字,并避免因为地址字符串格式而产生的误匹配。

```suggestion
    const state = parts[3];
    if (state !== 'LISTENING') {
      continue;
    }

    const cleanedLocalAddress = localAddress.replace(/\]$/, '');
    const segments = cleanedLocalAddress.split(':');
    const portStr = segments[segments.length - 1];
    const portNum = Number(portStr);

    if (Number.isInteger(portNum) && portNum === Number(port)) {
      pids.add(pid);
    }
```
</issue_to_address>

### Comment 2
<location> `astrbot/core/updator.py:47` </location>
<code_context>
         except psutil.NoSuchProcess:
             pass

+    @staticmethod
+    def _resolve_webui_dir_arg(argv: list[str]) -> str | None:
+        try:
</code_context>

<issue_to_address>
**issue (complexity):** 建议将 `_resolve_webui_dir_arg` 简化为基于索引与边界检查的实现,因为目前它实际上不会累积多个路径片段,只需要 `--webui-dir` 之后的单个参数即可。

你可以在不丢失当前行为的前提下简化 `_resolve_webui_dir_arg`,同时让意图更清晰。

目前的循环是:

```python
value_parts: list[str] = []
for arg in argv[start:]:
    if arg.startswith("--") and value_parts:
        break
    if arg.startswith("--") and not value_parts:
        break
    value_parts.append(arg)
```

它无论 `value_parts` 是否为空,在遇到第一个以 `--` 开头的参数时都会 `break`,因此实际上最多只会返回紧跟在 `--webui-dir` 后面的单个参数。由于 shell/操作系统会保证带空格且被引号包裹的路径作为一个 argv 元素保留,你不需要在这里做多段值的累积。

你可以用一个简单的索引查找和边界检查来替换这个辅助函数:

```python
@staticmethod
def _resolve_webui_dir_arg(argv: list[str]) -> str | None:
    try:
        idx = argv.index("--webui-dir")
    except ValueError:
        return None

    # 没有值或下一个参数仍是一个选项 → 视为缺失
    if idx + 1 >= len(argv) or argv[idx + 1].startswith("--"):
        return None

    return argv[idx + 1] or None
```

`_build_frozen_reboot_args` 可以保持不变:

```python
def _build_frozen_reboot_args(self) -> list[str]:
    argv = list(sys.argv[1:])
    webui_dir = self._resolve_webui_dir_arg(argv)
    if not webui_dir:
        webui_dir = os.environ.get("ASTRBOT_WEBUI_DIR")

    if webui_dir:
        return ["--webui-dir", webui_dir]
    return []
```

这样既保留了当前所有行为(包括环境变量兜底,以及仅在冻结重启时透传 `--webui-dir`),又降低了理解成本,使参数解析逻辑更易于推理。
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得我们的评审有帮助,请考虑分享 ✨
帮我变得更好用!请在每条评论上点击 👍 或 👎,我会根据反馈改进后续的评审。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • In _resolve_webui_dir_arg, the two consecutive if arg.startswith("--") checks both break and can be merged into a single if arg.startswith("--"): condition, which would simplify the control flow and make the intent clearer.
  • The Windows-specific findListeningPidsOnWindows/stopUnmanagedBackendByPort path uses spawnSync for netstat and taskkill; since these are blocking calls, consider adding a brief comment explaining that the synchronous behavior is acceptable here (because it only runs during shutdown/restart) to avoid future refactors accidentally moving it into latency-sensitive paths.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_resolve_webui_dir_arg`, the two consecutive `if arg.startswith("--")` checks both break and can be merged into a single `if arg.startswith("--"):` condition, which would simplify the control flow and make the intent clearer.
- The Windows-specific `findListeningPidsOnWindows`/`stopUnmanagedBackendByPort` path uses `spawnSync` for `netstat` and `taskkill`; since these are blocking calls, consider adding a brief comment explaining that the synchronous behavior is acceptable here (because it only runs during shutdown/restart) to avoid future refactors accidentally moving it into latency-sensitive paths.

## Individual Comments

### Comment 1
<location> `desktop/lib/backend-manager.js:435-440` </location>
<code_context>
+  findListeningPidsOnWindows(port) {
</code_context>

<issue_to_address>
**suggestion:** Consider tightening the `netstat` parsing to avoid misidentifying ports in edge cases.

The current parsing in `findListeningPidsOnWindows`:

```js
if (
  localAddress.endsWith(`:${port}`) ||
  localAddress.endsWith(`]:${port}`)
) {
  pids.add(pid);
}
```

relies on `endsWith`, which can be fragile if `netstat` output includes IPv6 zone indices or other formatting (e.g. `fe80::1%12:8080`). It also ignores the state column, so you may collect PIDs for `ESTABLISHED`/`TIME_WAIT` connections instead of only listeners.

To make this more robust, you could explicitly check the state and parse the port:

```js
const state = parts[3];
if (state !== 'LISTENING') continue;

const [host, portStr] = localAddress.replace(/\]$/, '').split(':').slice(-2);
if (Number(portStr) === port) {
  pids.add(pid);
}
```

This limits results to actual listening sockets and avoids accidental matches on the address string format.

```suggestion
    const state = parts[3];
    if (state !== 'LISTENING') {
      continue;
    }

    const cleanedLocalAddress = localAddress.replace(/\]$/, '');
    const segments = cleanedLocalAddress.split(':');
    const portStr = segments[segments.length - 1];
    const portNum = Number(portStr);

    if (Number.isInteger(portNum) && portNum === Number(port)) {
      pids.add(pid);
    }
```
</issue_to_address>

### Comment 2
<location> `astrbot/core/updator.py:47` </location>
<code_context>
         except psutil.NoSuchProcess:
             pass

+    @staticmethod
+    def _resolve_webui_dir_arg(argv: list[str]) -> str | None:
+        try:
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying `_resolve_webui_dir_arg` to a straightforward index-and-bounds check since it never actually accumulates multiple path segments and only needs the single token after `--webui-dir`.

You can simplify `_resolve_webui_dir_arg` without losing any behavior currently provided, and make the intent clearer.

Right now the loop:

```python
value_parts: list[str] = []
for arg in argv[start:]:
    if arg.startswith("--") and value_parts:
        break
    if arg.startswith("--") and not value_parts:
        break
    value_parts.append(arg)
```

always breaks on the first `--` regardless of `value_parts`, so it effectively only ever returns at most a single token after `--webui-dir`. Since shells/OSes already keep quoted paths with spaces in a single argv element, you don’t need multi-part accumulation here.

You can replace the helper with a simple index lookup and bounds check:

```python
@staticmethod
def _resolve_webui_dir_arg(argv: list[str]) -> str | None:
    try:
        idx = argv.index("--webui-dir")
    except ValueError:
        return None

    # No value or next token is another option → treat as missing
    if idx + 1 >= len(argv) or argv[idx + 1].startswith("--"):
        return None

    return argv[idx + 1] or None
```

`_build_frozen_reboot_args` remains unchanged:

```python
def _build_frozen_reboot_args(self) -> list[str]:
    argv = list(sys.argv[1:])
    webui_dir = self._resolve_webui_dir_arg(argv)
    if not webui_dir:
        webui_dir = os.environ.get("ASTRBOT_WEBUI_DIR")

    if webui_dir:
        return ["--webui-dir", webui_dir]
    return []
```

This keeps all the current behavior (including env fallback and only passing through `--webui-dir` for frozen restarts) while reducing cognitive load and making the argument parsing easier to reason about.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@zouyonghe
Copy link
Collaborator Author

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 2 个问题,并给出了一些整体性的反馈:

  • BackendManager.stopUnmanagedBackendByPort 中,delay(500)pingBackend(1200) 目前是魔法数字;建议将它们提取为具名常量或配置项,以便更清晰地表达意图并方便后续调优。
  • findListeningPidsOnWindows 的实现依赖解析 netstat 输出;可能需要显式记录预期的输出格式(IPv4/IPv6、本地化输出等),并在解析失败时增加基本的校验/日志,以减少在不同 Windows 语言环境或版本上出现静默异常行为的风险。
  • _resolve_webui_dir_arg 中,用于重建可能由多段组成的 --webui-dir 值的自定义逻辑有些微妙;通过添加简短注释,或简化控制流(例如在该标志之后遇到第一个 --* 时就停止)会让它在未来更易于维护和理解。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `BackendManager.stopUnmanagedBackendByPort`, the `delay(500)` and `pingBackend(1200)` values are currently magic numbers; consider extracting them into named constants or configuration to make the intent and tuning clearer.
- The `findListeningPidsOnWindows` implementation relies on parsing `netstat` output; it may be worth explicitly documenting the expected formats (IPv4/IPv6, localized output) and adding basic validation/logging when parsing fails to reduce the risk of silent misbehaviour on different Windows locales or versions.
- In `_resolve_webui_dir_arg`, the custom logic to reconstruct a possibly multi-part `--webui-dir` value is a bit subtle; adding a brief comment or simplifying the control flow (e.g., by stopping at the first `--*` after the flag) would make it easier to maintain and reason about in the future.

## Individual Comments

### Comment 1
<location> `desktop/main.js:361-362` </location>
<code_context>
-            error instanceof Error ? error.message : String(error)
-          }`,
-        );
+      backendManager.stopAnyBackend().then((result) => {
+        if (!result.ok) {
+          logElectron(`stopBackend failed: ${result.reason || 'unknown reason'}`);
+        }
</code_context>

<issue_to_address>
**issue (bug_risk):** stopAnyBackend errors are no longer caught in before-quit, which can lead to unhandled promise rejections.

This used to use `stopManagedBackend().catch(...)` to log any errors; after switching to `stopAnyBackend().then(...)` there’s no rejection handler. If `stopAnyBackend` rejects (e.g. spawn failures), the rejection will be unhandled. Please add an explicit `.catch` or use `try/catch` with `async/await` so shutdown-time errors are handled and logged as before.
</issue_to_address>

### Comment 2
<location> `astrbot/core/updator.py:47` </location>
<code_context>
         except psutil.NoSuchProcess:
             pass

+    @staticmethod
+    def _resolve_webui_dir_arg(argv: list[str]) -> str | None:
+        try:
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying `_resolve_webui_dir_arg` to read only the single argument following `--webui-dir` instead of accumulating multiple parts and breaking on flag-like tokens.

The `_resolve_webui_dir_arg` helper is more complex than necessary for its actual usage. Because shells already split arguments, a `--webui-dir` value with spaces must be quoted and will still appear as a single element in `argv`. The multi-part accumulation and double `arg.startswith("--")` checks add state and branches without a real-world benefit.

You can simplify it by reading exactly the next argument after `--webui-dir` and trimming it. This keeps the feature (CLI arg overrides env var, and empty/whitespace-only values are treated as missing) while reducing control flow complexity:

```python
@staticmethod
def _resolve_webui_dir_arg(argv: list[str]) -> str | None:
    try:
        idx = argv.index("--webui-dir")
    except ValueError:
        return None

    # no value provided after the flag
    if idx + 1 >= len(argv):
        return None

    value = argv[idx + 1].strip()
    return value or None
```

`_build_frozen_reboot_args` can stay as-is and will work the same:

```python
def _build_frozen_reboot_args(self) -> list[str]:
    argv = list(sys.argv[1:])
    webui_dir = self._resolve_webui_dir_arg(argv)
    if not webui_dir:
        webui_dir = os.environ.get("ASTRBOT_WEBUI_DIR")

    if webui_dir:
        return ["--webui-dir", webui_dir]
    return []
```

This removes the unnecessary loop and `value_parts` state, making the parsing easier to understand and maintain.
</issue_to_address>

Sourcery 对开源项目是免费的——如果你觉得我们的评论有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的代码审查。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • In BackendManager.stopUnmanagedBackendByPort, the delay(500) and pingBackend(1200) values are currently magic numbers; consider extracting them into named constants or configuration to make the intent and tuning clearer.
  • The findListeningPidsOnWindows implementation relies on parsing netstat output; it may be worth explicitly documenting the expected formats (IPv4/IPv6, localized output) and adding basic validation/logging when parsing fails to reduce the risk of silent misbehaviour on different Windows locales or versions.
  • In _resolve_webui_dir_arg, the custom logic to reconstruct a possibly multi-part --webui-dir value is a bit subtle; adding a brief comment or simplifying the control flow (e.g., by stopping at the first --* after the flag) would make it easier to maintain and reason about in the future.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `BackendManager.stopUnmanagedBackendByPort`, the `delay(500)` and `pingBackend(1200)` values are currently magic numbers; consider extracting them into named constants or configuration to make the intent and tuning clearer.
- The `findListeningPidsOnWindows` implementation relies on parsing `netstat` output; it may be worth explicitly documenting the expected formats (IPv4/IPv6, localized output) and adding basic validation/logging when parsing fails to reduce the risk of silent misbehaviour on different Windows locales or versions.
- In `_resolve_webui_dir_arg`, the custom logic to reconstruct a possibly multi-part `--webui-dir` value is a bit subtle; adding a brief comment or simplifying the control flow (e.g., by stopping at the first `--*` after the flag) would make it easier to maintain and reason about in the future.

## Individual Comments

### Comment 1
<location> `desktop/main.js:361-362` </location>
<code_context>
-            error instanceof Error ? error.message : String(error)
-          }`,
-        );
+      backendManager.stopAnyBackend().then((result) => {
+        if (!result.ok) {
+          logElectron(`stopBackend failed: ${result.reason || 'unknown reason'}`);
+        }
</code_context>

<issue_to_address>
**issue (bug_risk):** stopAnyBackend errors are no longer caught in before-quit, which can lead to unhandled promise rejections.

This used to use `stopManagedBackend().catch(...)` to log any errors; after switching to `stopAnyBackend().then(...)` there’s no rejection handler. If `stopAnyBackend` rejects (e.g. spawn failures), the rejection will be unhandled. Please add an explicit `.catch` or use `try/catch` with `async/await` so shutdown-time errors are handled and logged as before.
</issue_to_address>

### Comment 2
<location> `astrbot/core/updator.py:47` </location>
<code_context>
         except psutil.NoSuchProcess:
             pass

+    @staticmethod
+    def _resolve_webui_dir_arg(argv: list[str]) -> str | None:
+        try:
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying `_resolve_webui_dir_arg` to read only the single argument following `--webui-dir` instead of accumulating multiple parts and breaking on flag-like tokens.

The `_resolve_webui_dir_arg` helper is more complex than necessary for its actual usage. Because shells already split arguments, a `--webui-dir` value with spaces must be quoted and will still appear as a single element in `argv`. The multi-part accumulation and double `arg.startswith("--")` checks add state and branches without a real-world benefit.

You can simplify it by reading exactly the next argument after `--webui-dir` and trimming it. This keeps the feature (CLI arg overrides env var, and empty/whitespace-only values are treated as missing) while reducing control flow complexity:

```python
@staticmethod
def _resolve_webui_dir_arg(argv: list[str]) -> str | None:
    try:
        idx = argv.index("--webui-dir")
    except ValueError:
        return None

    # no value provided after the flag
    if idx + 1 >= len(argv):
        return None

    value = argv[idx + 1].strip()
    return value or None
```

`_build_frozen_reboot_args` can stay as-is and will work the same:

```python
def _build_frozen_reboot_args(self) -> list[str]:
    argv = list(sys.argv[1:])
    webui_dir = self._resolve_webui_dir_arg(argv)
    if not webui_dir:
        webui_dir = os.environ.get("ASTRBOT_WEBUI_DIR")

    if webui_dir:
        return ["--webui-dir", webui_dir]
    return []
```

This removes the unnecessary loop and `value_parts` state, making the parsing easier to understand and maintain.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +361 to +362
backendManager.stopAnyBackend().then((result) => {
if (!result.ok) {
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): stopAnyBackend 的错误在 before-quit 中不再被捕获,这可能导致未处理的 Promise 拒绝。

之前使用的是 stopManagedBackend().catch(...) 来记录所有错误;切换到 stopAnyBackend().then(...) 之后,就没有拒绝处理函数了。如果 stopAnyBackend 被拒绝(例如进程启动失败),这个拒绝将不会被处理。请显式添加一个 .catch,或者使用带 async/awaittry/catch,以便像之前一样在关闭阶段捕获并记录错误。

Original comment in English

issue (bug_risk): stopAnyBackend errors are no longer caught in before-quit, which can lead to unhandled promise rejections.

This used to use stopManagedBackend().catch(...) to log any errors; after switching to stopAnyBackend().then(...) there’s no rejection handler. If stopAnyBackend rejects (e.g. spawn failures), the rejection will be unhandled. Please add an explicit .catch or use try/catch with async/await so shutdown-time errors are handled and logged as before.

@zouyonghe
Copy link
Collaborator Author

@sourcery-ai review

Copy link

@SourceryAI SourceryAI left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 3 个问题,并留下了一些高层次的反馈:

  • waitForGracefulRestart 中,当 maxWaitMs 为 0 且应用未打包时,循环可能会无限运行;建议强制设置一个硬性的上限,或者在超过合理时长时进行日志记录,以避免在开发/CLI 环境中出现静默的无限等待。
  • 新增的后端 HTTP 辅助方法(requestBackendJsongetBackendStartTimerequestGracefulRestart)使用了多个硬编码的超时时间(1800/2000/2500ms);建议将这些超时时间集中到常量或配置中,以保持行为一致并便于调优。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `waitForGracefulRestart`, when `maxWaitMs` is 0 and the app is not packaged, the loop can run indefinitely; consider enforcing a hard upper bound or logging when it exceeds a reasonable duration to avoid silent infinite waits in dev/CLI environments.
- The new backend HTTP helpers (`requestBackendJson`, `getBackendStartTime`, `requestGracefulRestart`) use several hard-coded timeout values (1800/2000/2500ms); consider centralizing these timeouts into constants or configuration to keep behavior consistent and easier to tune.

## Individual Comments

### Comment 1
<location> `desktop/lib/backend-manager.js:269-277` </location>
<code_context>
+    }
+  }
+
+  async getBackendStartTime() {
+    const result = await this.requestBackendJson('/api/stat/start-time', {
+      timeoutMs: 1800,
+      method: 'GET',
+    });
+    if (!result.ok || !result.data) {
+      return null;
+    }
+    const startTime = result.data?.data?.start_time;
+    return Number.isFinite(startTime) ? startTime : null;
+  }
</code_context>

<issue_to_address>
**issue (bug_risk):** Backend start time parsing assumes a numeric value and may silently fail if a string or other type is returned.

`data.start_time` is passed directly to `Number.isFinite`, which only returns `true` for values of type `number`. If the API starts returning a string timestamp (e.g. "1700000000") or another numeric-like type, this will always resolve to `null` and could break `waitForGracefulRestart`. Consider normalizing first, e.g.:

```ts
const startTimeNum = Number(result.data?.data?.start_time);
return Number.isFinite(startTimeNum) ? startTimeNum : null;
```

so both numeric and numeric-string values are handled correctly.
</issue_to_address>

### Comment 2
<location> `astrbot/core/updator.py:69-78` </location>
<code_context>
+
+        return " ".join(value_parts).strip() or None
+
+    def _build_frozen_reboot_args(self) -> list[str]:
+        argv = list(sys.argv[1:])
+        webui_dir = self._resolve_webui_dir_arg(argv)
</code_context>

<issue_to_address>
**issue (bug_risk):** Frozen reboot drops all non-webui CLI arguments, which may regress existing configuration behaviors.

Currently `_build_frozen_reboot_args` only carries over `--webui-dir`/`ASTRBOT_WEBUI_DIR`, and `_reboot` then calls `os.execv(executable, [executable, *args])`. As a result, any other flags to the frozen binary (config path, ports, feature toggles, etc.) are lost on restart, whereas before we forwarded `sys.argv[1:]`. If you only need to fix `--webui-dir` handling, consider starting from a copy of `sys.argv[1:]` and updating or injecting the `--webui-dir` argument instead of discarding the rest.
</issue_to_address>

### Comment 3
<location> `dashboard/src/utils/restartAstrBot.ts:18-21` </location>
<code_context>
+): Promise<void> {
+  const desktopBridge = window.astrbotDesktop
+
+  if (desktopBridge?.isElectron) {
+    await triggerWaiting(waitingRef)
+    const authToken = localStorage.getItem('token')
+    const result = await desktopBridge.restartBackend(authToken)
+    if (!result.ok) {
+      waitingRef?.stop?.()
</code_context>

<issue_to_address>
**issue:** Electron restart flow can leave the waiting UI stuck if the IPC call throws before returning a result.

In the Electron branch, if `desktopBridge.restartBackend(authToken)` throws (IPC error, main-process exception, etc.), the function exits before `waitingRef?.stop?.()` is called and the `WaitingForRestart` UI can stay visible indefinitely. Callers currently just log and don’t clean up the UI. You can avoid this by wrapping the Electron path in a `try/finally` that always calls `waitingRef?.stop?.()`, or by catching the error, stopping the wait state, then rethrowing.
</issue_to_address>

Hi @zouyonghe! 👋

感谢你通过评论 @sourcery-ai review 来试用 Sourcery!🚀

安装 sourcery-ai bot 以在每个 Pull Request 上获得自动代码审查 ✨

帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进后续的审查。
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • In waitForGracefulRestart, when maxWaitMs is 0 and the app is not packaged, the loop can run indefinitely; consider enforcing a hard upper bound or logging when it exceeds a reasonable duration to avoid silent infinite waits in dev/CLI environments.
  • The new backend HTTP helpers (requestBackendJson, getBackendStartTime, requestGracefulRestart) use several hard-coded timeout values (1800/2000/2500ms); consider centralizing these timeouts into constants or configuration to keep behavior consistent and easier to tune.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `waitForGracefulRestart`, when `maxWaitMs` is 0 and the app is not packaged, the loop can run indefinitely; consider enforcing a hard upper bound or logging when it exceeds a reasonable duration to avoid silent infinite waits in dev/CLI environments.
- The new backend HTTP helpers (`requestBackendJson`, `getBackendStartTime`, `requestGracefulRestart`) use several hard-coded timeout values (1800/2000/2500ms); consider centralizing these timeouts into constants or configuration to keep behavior consistent and easier to tune.

## Individual Comments

### Comment 1
<location> `desktop/lib/backend-manager.js:269-277` </location>
<code_context>
+    }
+  }
+
+  async getBackendStartTime() {
+    const result = await this.requestBackendJson('/api/stat/start-time', {
+      timeoutMs: 1800,
+      method: 'GET',
+    });
+    if (!result.ok || !result.data) {
+      return null;
+    }
+    const startTime = result.data?.data?.start_time;
+    return Number.isFinite(startTime) ? startTime : null;
+  }
</code_context>

<issue_to_address>
**issue (bug_risk):** Backend start time parsing assumes a numeric value and may silently fail if a string or other type is returned.

`data.start_time` is passed directly to `Number.isFinite`, which only returns `true` for values of type `number`. If the API starts returning a string timestamp (e.g. "1700000000") or another numeric-like type, this will always resolve to `null` and could break `waitForGracefulRestart`. Consider normalizing first, e.g.:

```ts
const startTimeNum = Number(result.data?.data?.start_time);
return Number.isFinite(startTimeNum) ? startTimeNum : null;
```

so both numeric and numeric-string values are handled correctly.
</issue_to_address>

### Comment 2
<location> `astrbot/core/updator.py:69-78` </location>
<code_context>
+
+        return " ".join(value_parts).strip() or None
+
+    def _build_frozen_reboot_args(self) -> list[str]:
+        argv = list(sys.argv[1:])
+        webui_dir = self._resolve_webui_dir_arg(argv)
</code_context>

<issue_to_address>
**issue (bug_risk):** Frozen reboot drops all non-webui CLI arguments, which may regress existing configuration behaviors.

Currently `_build_frozen_reboot_args` only carries over `--webui-dir`/`ASTRBOT_WEBUI_DIR`, and `_reboot` then calls `os.execv(executable, [executable, *args])`. As a result, any other flags to the frozen binary (config path, ports, feature toggles, etc.) are lost on restart, whereas before we forwarded `sys.argv[1:]`. If you only need to fix `--webui-dir` handling, consider starting from a copy of `sys.argv[1:]` and updating or injecting the `--webui-dir` argument instead of discarding the rest.
</issue_to_address>

### Comment 3
<location> `dashboard/src/utils/restartAstrBot.ts:18-21` </location>
<code_context>
+): Promise<void> {
+  const desktopBridge = window.astrbotDesktop
+
+  if (desktopBridge?.isElectron) {
+    await triggerWaiting(waitingRef)
+    const authToken = localStorage.getItem('token')
+    const result = await desktopBridge.restartBackend(authToken)
+    if (!result.ok) {
+      waitingRef?.stop?.()
</code_context>

<issue_to_address>
**issue:** Electron restart flow can leave the waiting UI stuck if the IPC call throws before returning a result.

In the Electron branch, if `desktopBridge.restartBackend(authToken)` throws (IPC error, main-process exception, etc.), the function exits before `waitingRef?.stop?.()` is called and the `WaitingForRestart` UI can stay visible indefinitely. Callers currently just log and don’t clean up the UI. You can avoid this by wrapping the Electron path in a `try/finally` that always calls `waitingRef?.stop?.()`, or by catching the error, stopping the wait state, then rethrowing.
</issue_to_address>

Hi @zouyonghe! 👋

Thanks for trying out Sourcery by commenting with @sourcery-ai review! 🚀

Install the sourcery-ai bot to get automatic code reviews on every pull request ✨

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +69 to +78
def _build_frozen_reboot_args(self) -> list[str]:
argv = list(sys.argv[1:])
webui_dir = self._resolve_webui_dir_arg(argv)
if not webui_dir:
webui_dir = os.environ.get("ASTRBOT_WEBUI_DIR")

if webui_dir:
return ["--webui-dir", webui_dir]
return []

Choose a reason for hiding this comment

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

issue (bug_risk): 冷冻(frozen)重启会丢弃所有非 webui 的 CLI 参数,可能导致现有配置行为回归。

当前 _build_frozen_reboot_args 只保留了 --webui-dir/ASTRBOT_WEBUI_DIR,而 _reboot 随后调用 os.execv(executable, [executable, *args])。其结果是,传给该 frozen 可执行文件的其他所有参数(配置路径、端口、功能开关等)在重启时都会丢失,而之前我们是直接转发 sys.argv[1:]。如果你只需要修复 --webui-dir 的处理方式,建议从 sys.argv[1:] 的拷贝开始,然后更新或注入 --webui-dir 参数,而不是丢弃其余参数。

Original comment in English

issue (bug_risk): Frozen reboot drops all non-webui CLI arguments, which may regress existing configuration behaviors.

Currently _build_frozen_reboot_args only carries over --webui-dir/ASTRBOT_WEBUI_DIR, and _reboot then calls os.execv(executable, [executable, *args]). As a result, any other flags to the frozen binary (config path, ports, feature toggles, etc.) are lost on restart, whereas before we forwarded sys.argv[1:]. If you only need to fix --webui-dir handling, consider starting from a copy of sys.argv[1:] and updating or injecting the --webui-dir argument instead of discarding the rest.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 3 个问题,并给出了一些高层次的反馈:

  • waitForGracefulRestart 中,如果 backendTimeoutMs(或传入的 maxWaitMs)为 0 或负数,循环可能会无限运行;建议即使在开发环境中也强制设置一个硬性的上限或合理的默认超时时间,以避免潜在的卡死。
  • updator.py 中,_resolve_webui_dir_arg 辅助函数会把 --webui-dir 后面所有非 -- 开头的参数都当作目录的一部分,这可能会意外吞掉后续的短参数或位置参数;你可能需要在遇到任何看起来像选项的参数(例如以 - 开头)时就停止,而不是只在 -- 处停止。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `waitForGracefulRestart`, if `backendTimeoutMs` (or the passed `maxWaitMs`) is zero or negative, the loop can run indefinitely; consider enforcing a hard upper bound or a sensible default timeout even in dev to avoid a potential hang.
- The `_resolve_webui_dir_arg` helper in `updator.py` treats all non-`--` arguments after `--webui-dir` as part of the directory, which could accidentally consume subsequent short/positional arguments; you might want to stop on any argument that looks like an option (e.g. starting with `-`) rather than just `--`.

## Individual Comments

### Comment 1
<location> `desktop/lib/backend-manager.js:293-302` </location>
<code_context>
+  async waitForGracefulRestart(previousStartTime, maxWaitMs = 0) {
</code_context>

<issue_to_address>
**issue (bug_risk):** waitForGracefulRestart can loop indefinitely when effectiveMaxWaitMs is 0 (e.g., dev mode).

When `maxWaitMs` is 0 on a non-packaged build, `getEffectiveWaitMs(0)` returns 0, so `effectiveMaxWaitMs` is 0 and the `while (true)` in `waitForGracefulRestart` has no timeout-based exit. If the backend never restarts (e.g., restart endpoint is broken), this loop runs forever. Please either enforce a minimum timeout for graceful restart, or treat `0` as “no graceful wait” and fall back to the managed restart path after a bounded period.
</issue_to_address>

### Comment 2
<location> `dashboard/src/utils/restartAstrBot.ts:16-27` </location>
<code_context>
+): Promise<void> {
+  const desktopBridge = window.astrbotDesktop
+
+  if (desktopBridge?.isElectron) {
+    await triggerWaiting(waitingRef)
+    const authToken = localStorage.getItem('token')
+    const result = await desktopBridge.restartBackend(authToken)
+    if (!result.ok) {
+      waitingRef?.stop?.()
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Electron restart calls WaitingForRestart.check() before initiating restart and doesn’t stop it on IPC failures.

Two things to tighten up in the Electron branch:

1. `triggerWaiting(waitingRef)` is awaited before `desktopBridge.restartBackend(authToken)`, so the waiting UI starts polling before the restart is actually requested. Consider moving `triggerWaiting(waitingRef)` to run only after a successful restart call.

2. If `desktopBridge.restartBackend` rejects (e.g. IPC error), the exception will bypass the `result.ok` check and the waiting UI will never be stopped. Wrapping this block in a `try/catch` and calling `waitingRef?.stop?.()` in the catch before rethrowing would prevent the UI from getting stuck.

```suggestion
  const desktopBridge = window.astrbotDesktop

  if (desktopBridge?.isElectron) {
    const authToken = localStorage.getItem('token')
    try {
      const result = await desktopBridge.restartBackend(authToken)
      if (!result.ok) {
        waitingRef?.stop?.()
        throw new Error(result.reason || 'Failed to restart backend.')
      }
      await triggerWaiting(waitingRef)
    } catch (error) {
      waitingRef?.stop?.()
      throw error
    }
    return
  }
```
</issue_to_address>

### Comment 3
<location> `astrbot/core/updator.py:47` </location>
<code_context>
         except psutil.NoSuchProcess:
             pass

+    @staticmethod
+    def _resolve_webui_dir_arg(argv: list[str]) -> str | None:
+        try:
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying the new reboot argument handling by using generic flag-parsing helpers and small extraction functions so `_reboot` just orchestrates high-level behavior.

You can reduce the added complexity without changing behavior by:

## 1. Simplify and generalize `--webui-dir` parsing

Unless you *truly* need multi-part values for `--webui-dir` (which is unusual; shells keep quoted values in a single arg), you can replace the mini-parser with a simple "next arg" helper that’s reusable for other flags.

```py
@staticmethod
def _find_flag_value(argv: list[str], flag: str) -> str | None:
    try:
        idx = argv.index(flag)
    except ValueError:
        return None
    if idx + 1 >= len(argv):
        return None
    value = argv[idx + 1].strip()
    return value or None

def _build_frozen_reboot_args(self) -> list[str]:
    argv = sys.argv[1:]
    webui_dir = self._find_flag_value(argv, "--webui-dir")
    if not webui_dir:
        webui_dir = os.environ.get("ASTRBOT_WEBUI_DIR")

    return ["--webui-dir", webui_dir] if webui_dir else []
```

If you *do* need multi-part support, at least isolate and document it as a generic helper instead of a one-off:

```py
@staticmethod
def _collect_flag_values(argv: list[str], flag: str) -> str | None:
    """
    Collects all positional values after `flag` until the next `--...` option.
    Intended for flags whose value may be split across multiple argv entries.
    """
    try:
        idx = argv.index(flag)
    except ValueError:
        return None
    parts: list[str] = []
    for arg in argv[idx + 1:]:
        if arg.startswith("--"):
            break
        if arg:
            parts.append(arg)
    value = " ".join(parts).strip()
    return value or None

def _build_frozen_reboot_args(self) -> list[str]:
    argv = sys.argv[1:]
    webui_dir = self._collect_flag_values(argv, "--webui-dir") or os.environ.get("ASTRBOT_WEBUI_DIR")
    return ["--webui-dir", webui_dir] if webui_dir else []
```

This makes the intent clear and re-usable.

## 2. Flatten `_reboot` by extracting helpers

You can keep the same behavior but make the control flow easier to scan by:

- Extracting PyInstaller env scrubbing into `_reset_pyinstaller_env`.
- Centralizing argv construction into `_build_reboot_argv`.
- Letting `_reboot` just orchestrate.

```py
def _reset_pyinstaller_env(self) -> None:
    if not getattr(sys, "frozen", False):
        return
    os.environ["PYINSTALLER_RESET_ENVIRONMENT"] = "1"
    for key in list(os.environ.keys()):
        if key.startswith("_PYI_"):
            os.environ.pop(key, None)

def _build_reboot_argv(self, executable: str) -> list[str]:
    if os.environ.get("ASTRBOT_CLI") == "1":
        args = sys.argv[1:]
        return [executable, "-m", "astrbot.cli.__main__", *args]

    if getattr(sys, "frozen", False):
        args = self._build_frozen_reboot_args()
        return [executable, *args]

    return [executable, *sys.argv]

def _reboot(self, delay: int = 3) -> None:
    """重启当前程序."""
    time.sleep(delay)
    self.terminate_child_processes()
    executable = sys.executable

    try:
        self._reset_pyinstaller_env()
        argv = self._build_reboot_argv(executable)
        os.execv(executable, argv)
    except Exception as e:
        logger.error(f"重启失败({executable}, {e}),请尝试手动重启。")
        raise
```

This keeps all current semantics (CLI vs frozen vs normal, PyInstaller reset, `--webui-dir` handling) but reduces nesting and local complexity in `_reboot`.
</issue_to_address>

Sourcery 对开源项目是免费的——如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • In waitForGracefulRestart, if backendTimeoutMs (or the passed maxWaitMs) is zero or negative, the loop can run indefinitely; consider enforcing a hard upper bound or a sensible default timeout even in dev to avoid a potential hang.
  • The _resolve_webui_dir_arg helper in updator.py treats all non--- arguments after --webui-dir as part of the directory, which could accidentally consume subsequent short/positional arguments; you might want to stop on any argument that looks like an option (e.g. starting with -) rather than just --.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `waitForGracefulRestart`, if `backendTimeoutMs` (or the passed `maxWaitMs`) is zero or negative, the loop can run indefinitely; consider enforcing a hard upper bound or a sensible default timeout even in dev to avoid a potential hang.
- The `_resolve_webui_dir_arg` helper in `updator.py` treats all non-`--` arguments after `--webui-dir` as part of the directory, which could accidentally consume subsequent short/positional arguments; you might want to stop on any argument that looks like an option (e.g. starting with `-`) rather than just `--`.

## Individual Comments

### Comment 1
<location> `desktop/lib/backend-manager.js:293-302` </location>
<code_context>
+  async waitForGracefulRestart(previousStartTime, maxWaitMs = 0) {
</code_context>

<issue_to_address>
**issue (bug_risk):** waitForGracefulRestart can loop indefinitely when effectiveMaxWaitMs is 0 (e.g., dev mode).

When `maxWaitMs` is 0 on a non-packaged build, `getEffectiveWaitMs(0)` returns 0, so `effectiveMaxWaitMs` is 0 and the `while (true)` in `waitForGracefulRestart` has no timeout-based exit. If the backend never restarts (e.g., restart endpoint is broken), this loop runs forever. Please either enforce a minimum timeout for graceful restart, or treat `0` as “no graceful wait” and fall back to the managed restart path after a bounded period.
</issue_to_address>

### Comment 2
<location> `dashboard/src/utils/restartAstrBot.ts:16-27` </location>
<code_context>
+): Promise<void> {
+  const desktopBridge = window.astrbotDesktop
+
+  if (desktopBridge?.isElectron) {
+    await triggerWaiting(waitingRef)
+    const authToken = localStorage.getItem('token')
+    const result = await desktopBridge.restartBackend(authToken)
+    if (!result.ok) {
+      waitingRef?.stop?.()
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Electron restart calls WaitingForRestart.check() before initiating restart and doesn’t stop it on IPC failures.

Two things to tighten up in the Electron branch:

1. `triggerWaiting(waitingRef)` is awaited before `desktopBridge.restartBackend(authToken)`, so the waiting UI starts polling before the restart is actually requested. Consider moving `triggerWaiting(waitingRef)` to run only after a successful restart call.

2. If `desktopBridge.restartBackend` rejects (e.g. IPC error), the exception will bypass the `result.ok` check and the waiting UI will never be stopped. Wrapping this block in a `try/catch` and calling `waitingRef?.stop?.()` in the catch before rethrowing would prevent the UI from getting stuck.

```suggestion
  const desktopBridge = window.astrbotDesktop

  if (desktopBridge?.isElectron) {
    const authToken = localStorage.getItem('token')
    try {
      const result = await desktopBridge.restartBackend(authToken)
      if (!result.ok) {
        waitingRef?.stop?.()
        throw new Error(result.reason || 'Failed to restart backend.')
      }
      await triggerWaiting(waitingRef)
    } catch (error) {
      waitingRef?.stop?.()
      throw error
    }
    return
  }
```
</issue_to_address>

### Comment 3
<location> `astrbot/core/updator.py:47` </location>
<code_context>
         except psutil.NoSuchProcess:
             pass

+    @staticmethod
+    def _resolve_webui_dir_arg(argv: list[str]) -> str | None:
+        try:
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying the new reboot argument handling by using generic flag-parsing helpers and small extraction functions so `_reboot` just orchestrates high-level behavior.

You can reduce the added complexity without changing behavior by:

## 1. Simplify and generalize `--webui-dir` parsing

Unless you *truly* need multi-part values for `--webui-dir` (which is unusual; shells keep quoted values in a single arg), you can replace the mini-parser with a simple "next arg" helper that’s reusable for other flags.

```py
@staticmethod
def _find_flag_value(argv: list[str], flag: str) -> str | None:
    try:
        idx = argv.index(flag)
    except ValueError:
        return None
    if idx + 1 >= len(argv):
        return None
    value = argv[idx + 1].strip()
    return value or None

def _build_frozen_reboot_args(self) -> list[str]:
    argv = sys.argv[1:]
    webui_dir = self._find_flag_value(argv, "--webui-dir")
    if not webui_dir:
        webui_dir = os.environ.get("ASTRBOT_WEBUI_DIR")

    return ["--webui-dir", webui_dir] if webui_dir else []
```

If you *do* need multi-part support, at least isolate and document it as a generic helper instead of a one-off:

```py
@staticmethod
def _collect_flag_values(argv: list[str], flag: str) -> str | None:
    """
    Collects all positional values after `flag` until the next `--...` option.
    Intended for flags whose value may be split across multiple argv entries.
    """
    try:
        idx = argv.index(flag)
    except ValueError:
        return None
    parts: list[str] = []
    for arg in argv[idx + 1:]:
        if arg.startswith("--"):
            break
        if arg:
            parts.append(arg)
    value = " ".join(parts).strip()
    return value or None

def _build_frozen_reboot_args(self) -> list[str]:
    argv = sys.argv[1:]
    webui_dir = self._collect_flag_values(argv, "--webui-dir") or os.environ.get("ASTRBOT_WEBUI_DIR")
    return ["--webui-dir", webui_dir] if webui_dir else []
```

This makes the intent clear and re-usable.

## 2. Flatten `_reboot` by extracting helpers

You can keep the same behavior but make the control flow easier to scan by:

- Extracting PyInstaller env scrubbing into `_reset_pyinstaller_env`.
- Centralizing argv construction into `_build_reboot_argv`.
- Letting `_reboot` just orchestrate.

```py
def _reset_pyinstaller_env(self) -> None:
    if not getattr(sys, "frozen", False):
        return
    os.environ["PYINSTALLER_RESET_ENVIRONMENT"] = "1"
    for key in list(os.environ.keys()):
        if key.startswith("_PYI_"):
            os.environ.pop(key, None)

def _build_reboot_argv(self, executable: str) -> list[str]:
    if os.environ.get("ASTRBOT_CLI") == "1":
        args = sys.argv[1:]
        return [executable, "-m", "astrbot.cli.__main__", *args]

    if getattr(sys, "frozen", False):
        args = self._build_frozen_reboot_args()
        return [executable, *args]

    return [executable, *sys.argv]

def _reboot(self, delay: int = 3) -> None:
    """重启当前程序."""
    time.sleep(delay)
    self.terminate_child_processes()
    executable = sys.executable

    try:
        self._reset_pyinstaller_env()
        argv = self._build_reboot_argv(executable)
        os.execv(executable, argv)
    except Exception as e:
        logger.error(f"重启失败({executable}, {e}),请尝试手动重启。")
        raise
```

This keeps all current semantics (CLI vs frozen vs normal, PyInstaller reset, `--webui-dir` handling) but reduces nesting and local complexity in `_reboot`.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@zouyonghe
Copy link
Collaborator Author

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - 我发现了 3 个问题

给 AI Agents 的提示
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `desktop/lib/backend-manager.js:275-280` </location>
<code_context>
+    if (!result.ok || !result.data) {
+      return null;
+    }
+    const startTime = result.data?.data?.start_time;
+    return Number.isFinite(startTime) ? startTime : null;
+  }
+
</code_context>

<issue_to_address>
**suggestion:** 在执行 `Number.isFinite` 检查之前,先将 `start_time` 强制转换为数字,以处理字符串编码的时间戳。

如果 `start_time` 返回的是字符串(例如 JSON 编码的 Unix 时间戳),`Number.isFinite(startTime)` 将始终返回 false。为了同时处理数字和字符串类型的时间戳,应先进行强制转换:

```js
const rawStartTime = result.data?.data?.start_time;
const numericStartTime = Number(rawStartTime);
return Number.isFinite(numericStartTime) ? numericStartTime : null;
```

```suggestion
    if (!result.ok || !result.data) {
      return null;
    }
    const rawStartTime = result.data?.data?.start_time;
    const numericStartTime = Number(rawStartTime);
    return Number.isFinite(numericStartTime) ? numericStartTime : null;
  }
```
</issue_to_address>

### Comment 2
<location> `desktop/lib/backend-manager.js:294` </location>
<code_context>
+    return result.ok;
+  }
+
+  async waitForGracefulRestart(previousStartTime, maxWaitMs = 0) {
+    const effectiveMaxWaitMs = this.getEffectiveWaitMs(maxWaitMs);
+    const gracefulWaitMs =
</code_context>

<issue_to_address>
**issue (complexity):** 建议将重启和关闭逻辑重构为更清晰的“阶段化”辅助方法(优雅重启、超时和停止流程),这样可以在一眼浏览时更容易理解控制流。

你可以保持所有现有功能,但通过让重启和清理流程更明显地“分阶段”和线性化,从而降低认知负担。

### 1. 让 `waitForGracefulRestart` 明确分为两个阶段

目前的成功条件在一个循环中混合了 `sawBackendDown`、可空的 `previousStartTime` 和可空的 `currentStartTime`。你可以在保持语义不变的前提下,将其拆分为:

1. 等待后端停止(或达到超时时间)
2. 等待后端恢复,并在可用时通过 `start_time` 确认已重启

这样可以让条件更容易推理,同时允许复用 `getEffectiveWaitMs`,而不需要额外的“回退叠加”:

```js
async waitForGracefulRestart(previousStartTime, maxWaitMs = 0) {
  const totalWaitMs =
    this.getEffectiveWaitMs(maxWaitMs) || GRACEFUL_RESTART_WAIT_FALLBACK_MS;
  const phaseDeadline = Date.now() + totalWaitMs;

  // Phase 1: wait for backend to go down (if it ever does).
  while (await this.pingBackend(700)) {
    if (Date.now() >= phaseDeadline) {
      return {
        ok: false,
        reason: `Timed out after ${totalWaitMs}ms waiting for backend to go down.`,
      };
    }
    await delay(350);
  }

  // Phase 2: wait for backend to come back and verify restart
  while (true) {
    if (Date.now() >= phaseDeadline) {
      return {
        ok: false,
        reason: `Timed out after ${totalWaitMs}ms waiting for backend to come back up.`,
      };
    }

    const reachable = await this.pingBackend(700);
    if (!reachable) {
      await delay(350);
      continue;
    }

    const currentStartTime = await this.getBackendStartTime();

    // both known and changed
    if (
      previousStartTime !== null &&
      currentStartTime !== null &&
      currentStartTime !== previousStartTime
    ) {
      return { ok: true, reason: null };
    }

    // start_time not exposed before, now backend is up -> treat as success
    if (previousStartTime === null && currentStartTime !== null) {
      return { ok: true, reason: null };
    }

    // both null: backend went down and came back, but no start_time exposed
    if (previousStartTime === null && currentStartTime === null) {
      return { ok: true, reason: null };
    }

    await delay(350);
  }
}
```

要点:

- 使用一个清晰的超时时间(`totalWaitMs`)贯穿两个阶段。
- 不再在一个分支中混合 `sawBackendDown` / 各种 `null` 检查。
- 退出条件按阶段分别表达,而不是交织在一起。

你可以根据当前语义调整具体的 “null/null” 规则,但按阶段分组会让行为更容易检查。

### 2. 将超时计算保持在调用方本地

`getEffectiveWaitMs` 非常小,但目前需要调用方额外封装(例如在其上叠加 `GRACEFUL_RESTART_WAIT_FALLBACK_MS`)。你可以保留该辅助函数,但通过一个可选提示把重启特定的回退逻辑移动到内部,这样每个调用方看起来更线性:

```js
getEffectiveWaitMs(maxWaitMs = 0, { forGracefulRestart = false } = {}) {
  if (maxWaitMs > 0) return maxWaitMs;
  if (this.app.isPackaged) {
    return forGracefulRestart
      ? GRACEFUL_RESTART_WAIT_FALLBACK_MS
      : PACKAGED_BACKEND_TIMEOUT_FALLBACK_MS;
  }
  return 0;
}

// usage:
async waitForGracefulRestart(previousStartTime, maxWaitMs = 0) {
  const totalWaitMs = this.getEffectiveWaitMs(maxWaitMs, {
    forGracefulRestart: true,
  });
  // ...
}
```

这样可以移除 `waitForGracefulRestart` 中“在回退之上再做回退”的额外层级,并把策略集中到一个地方。

### 3. 让 `stopAnyBackend` 更线性

`stopAnyBackend` 目前根据 `backendProcess` 做分支,并在不同分支中执行多次 ping。你可以在保留非托管清理行为的前提下,让流程更加线性、易读:

```js
async stopAnyBackend() {
  // Quick check: nothing listening -> done
  if (!(await this.pingBackend())) {
    return { ok: true, reason: null };
  }

  // If we manage a process, try stopping it first
  if (this.backendProcess) {
    await this.stopManagedBackend();
    if (!(await this.pingBackend())) {
      return { ok: true, reason: null };
    }
  }

  // Still reachable: attempt unmanaged cleanup on Windows packaged builds
  const cleaned = await this.stopUnmanagedBackendByPort();
  if (cleaned) {
    return { ok: true, reason: null };
  }

  return {
    ok: false,
    reason: 'Backend is running but not managed by Electron.',
  };
}
```

收益:

- 高层算法一目了然:ping → 停托管进程 → 非托管清理。
- 不再在两个分支中复制“ping-then-return”的逻辑。
- `stopUnmanagedBackendByPort` 继续作为封装“细节繁多”的辅助函数。

### 4. 在 `restartBackend` 中将优雅重启与回退逻辑分离

当前重启路径在一个方法中交织了日志、ping 以及回退逻辑。抽取一个小的辅助函数可以保持 `restartBackend` 的可读性:

```js
async attemptGracefulRestart(authToken) {
  const backendRunning = await this.pingBackend(900);
  if (!backendRunning) {
    return { ok: false, reason: 'Backend not running; skip graceful restart.' };
  }

  const previousStartTime = await this.getBackendStartTime();
  const gracefulRequested = await this.requestGracefulRestart(authToken);
  if (!gracefulRequested) {
    return { ok: false, reason: 'Request failed' };
  }

  const gracefulResult = await this.waitForGracefulRestart(
    previousStartTime,
    this.backendTimeoutMs,
  );
  return gracefulResult;
}

async restartBackend(authToken = null) {
  if (!this.canManageBackend()) {
    return { ok: false, reason: 'Backend command is not configured.' };
  }
  if (this.backendSpawning || this.backendRestarting) {
    return { ok: false, reason: 'Backend action already in progress.' };
  }

  this.backendRestarting = true;
  try {
    const graceful = await this.attemptGracefulRestart(authToken);
    if (graceful.ok) {
      return { ok: true, reason: null };
    }
    if (graceful.reason) {
      this.log(`Graceful restart did not complete: ${graceful.reason}`);
    }

    await this.stopManagedBackend();
    const startResult = await this.startBackendAndWait(this.backendTimeoutMs);
    if (!startResult.ok) {
      return {
        ok: false,
        reason: startResult.reason || 'Failed to restart backend.',
      };
    }
    return { ok: true, reason: null };
  } finally {
    this.backendRestarting = false;
  }
}
```

这样既保持了现有行为不变,又让优雅重启路径成为一个命名清晰、自洽的阶段,同时回退逻辑也更容易一眼看清。
</issue_to_address>

### Comment 3
<location> `astrbot/core/updator.py:47` </location>
<code_context>
         except psutil.NoSuchProcess:
             pass

+    @staticmethod
+    def _is_option_arg(arg: str) -> bool:
+        return arg.startswith("-")
</code_context>

<issue_to_address>
**issue (complexity):** 建议通过将标志解析内联到一个辅助函数中,并按模式拆分 argv 构造逻辑,来简化重启逻辑,使每条路径更清晰、间接性更少。

你可以在不改变行为的情况下,通过合并一些非常小的辅助函数,让每种重启模式的 argv 构造都自成一体,从而减少间接。

### 1. 将迷你 flag 解析器内联到一个聚焦的辅助函数中

`_is_option_arg` + `_collect_flag_values` + `_resolve_webui_dir_arg` 可以合并成一个方法,它仍然支持多段值,并在遇到下一个 flag 时停止,但更容易在一个地方完整阅读:

```python
@staticmethod
def _get_webui_dir_from_argv(argv: list[str]) -> str | None:
    try:
        idx = argv.index("--webui-dir")
    except ValueError:
        return None

    if idx + 1 >= len(argv):
        return None

    value_parts: list[str] = []
    for arg in argv[idx + 1:]:
        if arg.startswith("-"):  # next option starts
            break
        if arg:
            value_parts.append(arg)

    if not value_parts:
        return None

    return " ".join(value_parts).strip() or None
```

然后简化 `_build_frozen_reboot_args`,直接使用该方法:

```python
def _build_frozen_reboot_args(self) -> list[str]:
    argv = list(sys.argv[1:])
    webui_dir = self._get_webui_dir_from_argv(argv) or os.environ.get("ASTRBOT_WEBUI_DIR")
    return ["--webui-dir", webui_dir] if webui_dir else []
```

这会移除两个额外的辅助函数,同时保持现有行为不变。

### 2. 按模式拆分重启 argv 构造逻辑

你可以保留 `_build_reboot_argv` 作为分发器,但把每种模式的逻辑移动到命名清晰的辅助函数中。这样每种模式最终构造出的 argv 都一目了然,而不改变行为:

```python
def _build_cli_reboot_argv(self, executable: str) -> list[str]:
    args = sys.argv[1:]
    return [executable, "-m", "astrbot.cli.__main__", *args]

def _build_frozen_reboot_argv(self, executable: str) -> list[str]:
    args = self._build_frozen_reboot_args()
    return [executable, *args]

def _build_normal_reboot_argv(self, executable: str) -> list[str]:
    return [executable, *sys.argv]

def _build_reboot_argv(self, executable: str) -> list[str]:
    if os.environ.get("ASTRBOT_CLI") == "1":
        return self._build_cli_reboot_argv(executable)
    if getattr(sys, "frozen", False):
        return self._build_frozen_reboot_argv(executable)
    return self._build_normal_reboot_argv(executable)
```

现在重启行为仍然保持不变(包括“冻结模式只保留 `--webui-dir`,丢弃其他原始参数”的行为),但每种模式的行为都是局部、自包含且更易审查的。
</issue_to_address>

Sourcery 对开源项目免费——如果你喜欢我们的 Review,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进你的代码审查体验。
Original comment in English

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `desktop/lib/backend-manager.js:275-280` </location>
<code_context>
+    if (!result.ok || !result.data) {
+      return null;
+    }
+    const startTime = result.data?.data?.start_time;
+    return Number.isFinite(startTime) ? startTime : null;
+  }
+
</code_context>

<issue_to_address>
**suggestion:** Coerce `start_time` to a number before the `Number.isFinite` check to handle string-encoded timestamps.

If `start_time` ever comes back as a string (e.g. a JSON-encoded Unix timestamp), `Number.isFinite(startTime)` will always return false. To handle both numeric and string timestamps, coerce first:

```js
const rawStartTime = result.data?.data?.start_time;
const numericStartTime = Number(rawStartTime);
return Number.isFinite(numericStartTime) ? numericStartTime : null;
```

```suggestion
    if (!result.ok || !result.data) {
      return null;
    }
    const rawStartTime = result.data?.data?.start_time;
    const numericStartTime = Number(rawStartTime);
    return Number.isFinite(numericStartTime) ? numericStartTime : null;
  }
```
</issue_to_address>

### Comment 2
<location> `desktop/lib/backend-manager.js:294` </location>
<code_context>
+    return result.ok;
+  }
+
+  async waitForGracefulRestart(previousStartTime, maxWaitMs = 0) {
+    const effectiveMaxWaitMs = this.getEffectiveWaitMs(maxWaitMs);
+    const gracefulWaitMs =
</code_context>

<issue_to_address>
**issue (complexity):** Consider restructuring the restart and shutdown logic into clearer, phase-based helpers (graceful restart, timeouts, and stop flows) so the control flow is easier to follow at a glance.

You can keep all the functionality but reduce cognitive load by making the restart and cleanup flows more obviously “phase-based” and linear.

### 1. Make `waitForGracefulRestart` explicitly two-phase

Right now the success conditions mix `sawBackendDown`, nullable `previousStartTime`, and nullable `currentStartTime` in one loop. You can keep the same semantics while splitting into:

1. wait for backend to go down (or hit timeout)
2. wait for backend to come back and confirm restart via `start_time` when available

This makes the conditions easier to reason about and allows re-use of `getEffectiveWaitMs` without extra fallback layering:

```js
async waitForGracefulRestart(previousStartTime, maxWaitMs = 0) {
  const totalWaitMs =
    this.getEffectiveWaitMs(maxWaitMs) || GRACEFUL_RESTART_WAIT_FALLBACK_MS;
  const phaseDeadline = Date.now() + totalWaitMs;

  // Phase 1: wait for backend to go down (if it ever does).
  while (await this.pingBackend(700)) {
    if (Date.now() >= phaseDeadline) {
      return {
        ok: false,
        reason: `Timed out after ${totalWaitMs}ms waiting for backend to go down.`,
      };
    }
    await delay(350);
  }

  // Phase 2: wait for backend to come back and verify restart
  while (true) {
    if (Date.now() >= phaseDeadline) {
      return {
        ok: false,
        reason: `Timed out after ${totalWaitMs}ms waiting for backend to come back up.`,
      };
    }

    const reachable = await this.pingBackend(700);
    if (!reachable) {
      await delay(350);
      continue;
    }

    const currentStartTime = await this.getBackendStartTime();

    // both known and changed
    if (
      previousStartTime !== null &&
      currentStartTime !== null &&
      currentStartTime !== previousStartTime
    ) {
      return { ok: true, reason: null };
    }

    // start_time not exposed before, now backend is up -> treat as success
    if (previousStartTime === null && currentStartTime !== null) {
      return { ok: true, reason: null };
    }

    // both null: backend went down and came back, but no start_time exposed
    if (previousStartTime === null && currentStartTime === null) {
      return { ok: true, reason: null };
    }

    await delay(350);
  }
}
```

Key points:

- One clear timeout (`totalWaitMs`) shared across both phases.
- No `sawBackendDown` / mixed `null` checks in a single branch.
- Exit conditions are expressed per phase instead of interleaved.

You can adjust the specific “null/null” rules to match your current semantics, but keeping them grouped by phase will make the behavior much easier to inspect.

### 2. Keep timeout computation local to the caller

`getEffectiveWaitMs` is very small and currently requires per-caller layering (e.g., `GRACEFUL_RESTART_WAIT_FALLBACK_MS` on top). You can keep the helper but move the restart-specific fallback inside it via an optional hint, so each caller is linear:

```js
getEffectiveWaitMs(maxWaitMs = 0, { forGracefulRestart = false } = {}) {
  if (maxWaitMs > 0) return maxWaitMs;
  if (this.app.isPackaged) {
    return forGracefulRestart
      ? GRACEFUL_RESTART_WAIT_FALLBACK_MS
      : PACKAGED_BACKEND_TIMEOUT_FALLBACK_MS;
  }
  return 0;
}

// usage:
async waitForGracefulRestart(previousStartTime, maxWaitMs = 0) {
  const totalWaitMs = this.getEffectiveWaitMs(maxWaitMs, {
    forGracefulRestart: true,
  });
  // ...
}
```

This removes the extra “fallback on top of a fallback” layer from `waitForGracefulRestart` and centralizes the policy.

### 3. Linearize `stopAnyBackend`

`stopAnyBackend` currently branches on `backendProcess` and does multiple pings in different branches. You can keep unmanaged cleanup while making the flow more obviously linear:

```js
async stopAnyBackend() {
  // Quick check: nothing listening -> done
  if (!(await this.pingBackend())) {
    return { ok: true, reason: null };
  }

  // If we manage a process, try stopping it first
  if (this.backendProcess) {
    await this.stopManagedBackend();
    if (!(await this.pingBackend())) {
      return { ok: true, reason: null };
    }
  }

  // Still reachable: attempt unmanaged cleanup on Windows packaged builds
  const cleaned = await this.stopUnmanagedBackendByPort();
  if (cleaned) {
    return { ok: true, reason: null };
  }

  return {
    ok: false,
    reason: 'Backend is running but not managed by Electron.',
  };
}
```

Benefits:

- High-level algorithm visible in one read: ping → stop managed → unmanaged cleanup.
- No duplicated “ping-then-return” logic in two branches.
- `stopUnmanagedBackendByPort` remains encapsulated as the “detail-heavy” helper.

### 4. Separate graceful restart attempt from fallback in `restartBackend`

The restart path is functionally solid but interleaves logging, pings, and fallbacks in one method. Extracting a small helper keeps `restartBackend` readable:

```js
async attemptGracefulRestart(authToken) {
  const backendRunning = await this.pingBackend(900);
  if (!backendRunning) {
    return { ok: false, reason: 'Backend not running; skip graceful restart.' };
  }

  const previousStartTime = await this.getBackendStartTime();
  const gracefulRequested = await this.requestGracefulRestart(authToken);
  if (!gracefulRequested) {
    return { ok: false, reason: 'Request failed' };
  }

  const gracefulResult = await this.waitForGracefulRestart(
    previousStartTime,
    this.backendTimeoutMs,
  );
  return gracefulResult;
}

async restartBackend(authToken = null) {
  if (!this.canManageBackend()) {
    return { ok: false, reason: 'Backend command is not configured.' };
  }
  if (this.backendSpawning || this.backendRestarting) {
    return { ok: false, reason: 'Backend action already in progress.' };
  }

  this.backendRestarting = true;
  try {
    const graceful = await this.attemptGracefulRestart(authToken);
    if (graceful.ok) {
      return { ok: true, reason: null };
    }
    if (graceful.reason) {
      this.log(`Graceful restart did not complete: ${graceful.reason}`);
    }

    await this.stopManagedBackend();
    const startResult = await this.startBackendAndWait(this.backendTimeoutMs);
    if (!startResult.ok) {
      return {
        ok: false,
        reason: startResult.reason || 'Failed to restart backend.',
      };
    }
    return { ok: true, reason: null };
  } finally {
    this.backendRestarting = false;
  }
}
```

This keeps your behavior identical but makes the graceful path a clearly named, self-contained phase, and the fallback is easy to see at a glance.
</issue_to_address>

### Comment 3
<location> `astrbot/core/updator.py:47` </location>
<code_context>
         except psutil.NoSuchProcess:
             pass

+    @staticmethod
+    def _is_option_arg(arg: str) -> bool:
+        return arg.startswith("-")
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying the reboot logic by inlining the flag parsing into one helper and splitting argv construction into per-mode helpers so each path is clearer and less indirect.

You can reduce indirection without changing behavior by collapsing some of the tiny helpers and making each reboot mode’s argv construction self-contained.

### 1. Inline the mini flag parser into a single, focused helper

`_is_option_arg` + `_collect_flag_values` + `_resolve_webui_dir_arg` can be merged into one method that still supports multi-part values and stops at the next flag, but is easier to read in one place:

```python
@staticmethod
def _get_webui_dir_from_argv(argv: list[str]) -> str | None:
    try:
        idx = argv.index("--webui-dir")
    except ValueError:
        return None

    if idx + 1 >= len(argv):
        return None

    value_parts: list[str] = []
    for arg in argv[idx + 1:]:
        if arg.startswith("-"):  # next option starts
            break
        if arg:
            value_parts.append(arg)

    if not value_parts:
        return None

    return " ".join(value_parts).strip() or None
```

Then simplify `_build_frozen_reboot_args` to use this directly:

```python
def _build_frozen_reboot_args(self) -> list[str]:
    argv = list(sys.argv[1:])
    webui_dir = self._get_webui_dir_from_argv(argv) or os.environ.get("ASTRBOT_WEBUI_DIR")
    return ["--webui-dir", webui_dir] if webui_dir else []
```

This removes two extra helpers and keeps the existing behavior intact.

### 2. Split reboot argv construction by mode

You can keep `_build_reboot_argv` as the dispatcher but move the per-mode logic into clearly named helpers. This makes the final argv for each mode obvious without changing behavior:

```python
def _build_cli_reboot_argv(self, executable: str) -> list[str]:
    args = sys.argv[1:]
    return [executable, "-m", "astrbot.cli.__main__", *args]

def _build_frozen_reboot_argv(self, executable: str) -> list[str]:
    args = self._build_frozen_reboot_args()
    return [executable, *args]

def _build_normal_reboot_argv(self, executable: str) -> list[str]:
    return [executable, *sys.argv]

def _build_reboot_argv(self, executable: str) -> list[str]:
    if os.environ.get("ASTRBOT_CLI") == "1":
        return self._build_cli_reboot_argv(executable)
    if getattr(sys, "frozen", False):
        return self._build_frozen_reboot_argv(executable)
    return self._build_normal_reboot_argv(executable)
```

Now the reboot behavior is still the same (including the “frozen discards all original args except `--webui-dir`” behavior), but each mode’s behavior is local and easier to audit.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

except psutil.NoSuchProcess:
pass

@staticmethod
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (complexity): 建议通过将标志解析内联到一个辅助函数中,并按模式拆分 argv 构造逻辑,来简化重启逻辑,使每条路径更清晰、间接性更少。

你可以在不改变行为的情况下,通过合并一些非常小的辅助函数,让每种重启模式的 argv 构造都自成一体,从而减少间接。

1. 将迷你 flag 解析器内联到一个聚焦的辅助函数中

_is_option_arg + _collect_flag_values + _resolve_webui_dir_arg 可以合并成一个方法,它仍然支持多段值,并在遇到下一个 flag 时停止,但更容易在一个地方完整阅读:

@staticmethod
def _get_webui_dir_from_argv(argv: list[str]) -> str | None:
    try:
        idx = argv.index("--webui-dir")
    except ValueError:
        return None

    if idx + 1 >= len(argv):
        return None

    value_parts: list[str] = []
    for arg in argv[idx + 1:]:
        if arg.startswith("-"):  # next option starts
            break
        if arg:
            value_parts.append(arg)

    if not value_parts:
        return None

    return " ".join(value_parts).strip() or None

然后简化 _build_frozen_reboot_args,直接使用该方法:

def _build_frozen_reboot_args(self) -> list[str]:
    argv = list(sys.argv[1:])
    webui_dir = self._get_webui_dir_from_argv(argv) or os.environ.get("ASTRBOT_WEBUI_DIR")
    return ["--webui-dir", webui_dir] if webui_dir else []

这会移除两个额外的辅助函数,同时保持现有行为不变。

2. 按模式拆分重启 argv 构造逻辑

你可以保留 _build_reboot_argv 作为分发器,但把每种模式的逻辑移动到命名清晰的辅助函数中。这样每种模式最终构造出的 argv 都一目了然,而不改变行为:

def _build_cli_reboot_argv(self, executable: str) -> list[str]:
    args = sys.argv[1:]
    return [executable, "-m", "astrbot.cli.__main__", *args]

def _build_frozen_reboot_argv(self, executable: str) -> list[str]:
    args = self._build_frozen_reboot_args()
    return [executable, *args]

def _build_normal_reboot_argv(self, executable: str) -> list[str]:
    return [executable, *sys.argv]

def _build_reboot_argv(self, executable: str) -> list[str]:
    if os.environ.get("ASTRBOT_CLI") == "1":
        return self._build_cli_reboot_argv(executable)
    if getattr(sys, "frozen", False):
        return self._build_frozen_reboot_argv(executable)
    return self._build_normal_reboot_argv(executable)

现在重启行为仍然保持不变(包括“冻结模式只保留 --webui-dir,丢弃其他原始参数”的行为),但每种模式的行为都是局部、自包含且更易审查的。

Original comment in English

issue (complexity): Consider simplifying the reboot logic by inlining the flag parsing into one helper and splitting argv construction into per-mode helpers so each path is clearer and less indirect.

You can reduce indirection without changing behavior by collapsing some of the tiny helpers and making each reboot mode’s argv construction self-contained.

1. Inline the mini flag parser into a single, focused helper

_is_option_arg + _collect_flag_values + _resolve_webui_dir_arg can be merged into one method that still supports multi-part values and stops at the next flag, but is easier to read in one place:

@staticmethod
def _get_webui_dir_from_argv(argv: list[str]) -> str | None:
    try:
        idx = argv.index("--webui-dir")
    except ValueError:
        return None

    if idx + 1 >= len(argv):
        return None

    value_parts: list[str] = []
    for arg in argv[idx + 1:]:
        if arg.startswith("-"):  # next option starts
            break
        if arg:
            value_parts.append(arg)

    if not value_parts:
        return None

    return " ".join(value_parts).strip() or None

Then simplify _build_frozen_reboot_args to use this directly:

def _build_frozen_reboot_args(self) -> list[str]:
    argv = list(sys.argv[1:])
    webui_dir = self._get_webui_dir_from_argv(argv) or os.environ.get("ASTRBOT_WEBUI_DIR")
    return ["--webui-dir", webui_dir] if webui_dir else []

This removes two extra helpers and keeps the existing behavior intact.

2. Split reboot argv construction by mode

You can keep _build_reboot_argv as the dispatcher but move the per-mode logic into clearly named helpers. This makes the final argv for each mode obvious without changing behavior:

def _build_cli_reboot_argv(self, executable: str) -> list[str]:
    args = sys.argv[1:]
    return [executable, "-m", "astrbot.cli.__main__", *args]

def _build_frozen_reboot_argv(self, executable: str) -> list[str]:
    args = self._build_frozen_reboot_args()
    return [executable, *args]

def _build_normal_reboot_argv(self, executable: str) -> list[str]:
    return [executable, *sys.argv]

def _build_reboot_argv(self, executable: str) -> list[str]:
    if os.environ.get("ASTRBOT_CLI") == "1":
        return self._build_cli_reboot_argv(executable)
    if getattr(sys, "frozen", False):
        return self._build_frozen_reboot_argv(executable)
    return self._build_normal_reboot_argv(executable)

Now the reboot behavior is still the same (including the “frozen discards all original args except --webui-dir” behavior), but each mode’s behavior is local and easier to audit.

@zouyonghe zouyonghe merged commit 64de474 into AstrBotDevs:master Feb 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature:updater The bug / feature is about astrbot updater system size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants