refactor: migrate desktop backend packaging from PyInstaller to CPython runtime#5170
refactor: migrate desktop backend packaging from PyInstaller to CPython runtime#5170zouyonghe wants to merge 32 commits intoAstrBotDevs:masterfrom
Conversation
There was a problem hiding this comment.
嗨——我已经查看了你的更改,看起来非常棒!
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的代码审查。
Original comment in English
Hey - I've reviewed your changes and they look great!
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了两个问题,并给出了一些高层次的反馈:
- 在
desktop/scripts/build-backend.mjs中,建议校验所选的 CPython 运行时是否与预期的 Python 主/次版本号匹配,并且至少能够执行一个简单命令(例如python -c 'import pip'),这样配置错误或不兼容的运行时可以在构建阶段快速失败,而不是等到应用运行时才暴露问题。 - Windows 上的后端进程识别现在依赖通过 PowerShell(
getWindowsProcessCommandLine)解析命令行;可以考虑约束调用的时机/频率,或者按 PID 缓存结果,以避免在存在大量 PID 的关机阶段重复调用 PowerShell。
给 AI Agent 的提示
Please address the comments from this code review:
## Overall Comments
- In `desktop/scripts/build-backend.mjs`, consider validating that the selected CPython runtime matches the expected Python major/minor version and can at least execute a trivial command (e.g., `python -c 'import pip'`) so misconfigured or incompatible runtimes fail fast at build time instead of only at app runtime.
- The backend process identification on Windows now relies on parsing the command line via PowerShell (`getWindowsProcessCommandLine`); it may be worth constraining when/how often this is called or caching results per PID to avoid repeated PowerShell invocations during shutdown when many PIDs are present.
## Individual Comments
### Comment 1
<location> `desktop/README.md:156` </location>
<code_context>
+
+- `Missing CPython runtime source`: set `ASTRBOT_DESKTOP_CPYTHON_HOME` (or `ASTRBOT_DESKTOP_BACKEND_RUNTIME`).
+- `Cannot find Python executable in runtime`: runtime directory is invalid or incomplete.
+- `Failed to detect purelib from runtime python`: runtime Python cannot run correctly.
+
If Electron download times out on restricted networks, configure mirrors before install:
</code_context>
<issue_to_address>
**nitpick (typo):** Capitalize "Python" for consistency and correctness.
Here, "runtime python" should be "runtime Python" to match the earlier "Python executable" and the later "runtime Python cannot run correctly."
```suggestion
- `Failed to detect purelib from runtime Python`: runtime Python cannot run correctly.
```
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:175` </location>
<code_context>
+ return this.packagedBackendManifest;
+ }
+
+ getPackagedBackendAppDir() {
+ const backendDir = this.getPackagedBackendDir();
+ if (!backendDir) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the new manifest-based helpers and Windows PID matching logic into shared utilities to centralize behavior and reduce duplication.
You can reduce the added complexity in two focused places without changing behavior: manifest-driven path resolution and Windows PID matching.
---
### 1. Consolidate manifest-based path helpers
`getPackagedBackendAppDir`, `getPackagedBackendLaunchScriptPath`, and `getPackagedRuntimePythonPath` all:
- recompute `backendDir`
- call `getPackagedBackendManifest`
- compute a relative path with a default
- join + `existsSync`
This can be centralized so you only resolve the manifest once and reduce duplication.
**Example refactor (internal helper + cohesive config):**
```js
// Small internal helper
resolveManifestPath(fieldName, defaultValue) {
const backendDir = this.getPackagedBackendDir();
if (!backendDir) return null;
const manifest = this.getPackagedBackendManifest() || {};
const rel = typeof manifest[fieldName] === 'string' && manifest[fieldName]
? manifest[fieldName]
: defaultValue;
const candidate = path.join(backendDir, rel);
return fs.existsSync(candidate) ? candidate : null;
}
// Single cohesive config getter
getPackagedRuntimeConfig() {
if (!this.app.isPackaged) return null;
const backendDir = this.getPackagedBackendDir();
if (!backendDir) return null;
const pythonRelativeDefault =
process.platform === 'win32'
? path.join('python', 'Scripts', 'python.exe')
: path.join('python', 'bin', 'python3');
const appDir = this.resolveManifestPath('app', 'app');
const entry = this.resolveManifestPath('entrypoint', 'launch_backend.py');
const python = this.resolveManifestPath('python', pythonRelativeDefault);
if (!python || !entry) return null;
return { backendDir, appDir, entry, python };
}
```
Then the existing helpers become thin wrappers (or can be inlined where used):
```js
getPackagedBackendAppDir() {
const cfg = this.getPackagedRuntimeConfig();
return cfg ? cfg.appDir : null;
}
getPackagedBackendLaunchScriptPath() {
const cfg = this.getPackagedRuntimeConfig();
return cfg ? cfg.entry : null;
}
getPackagedRuntimePythonPath() {
const cfg = this.getPackagedRuntimeConfig();
return cfg ? cfg.python : null;
}
buildPackagedBackendLaunch(webuiDir) {
const cfg = this.getPackagedRuntimeConfig();
if (!cfg) return null;
const args = [cfg.entry];
if (webuiDir) args.push('--webui-dir', webuiDir);
return { cmd: cfg.python, args, shell: false };
}
```
This keeps all existing behavior intact but:
- removes repeated `backendDir` / manifest reads
- gives a single “runtime config” mental model
- makes future fields easier to add.
---
### 2. Encapsulate Windows PID matching logic
`stopUnmanagedBackendByPort` is handling:
- backendConfig extraction
- image-name matching
- deciding if strict command line check is required
- resolving/normalizing markers
- per-PID log decisions and `taskkill`
You already added `normalizeWindowsPathForMatch`, `isGenericWindowsPythonImage`, and `getWindowsProcessCommandLine`; you can push the branching into a single predicate like “does this PID look like our backend?” to linearize `stopUnmanagedBackendByPort`.
**Example refactor:**
```js
// Internal helper: decide whether a PID is our backend
isMatchingBackendPid(pid, backendConfig, expectedImageName, markers, requireStrict) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`);
return false;
}
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
if (!requireStrict) return true;
if (!markers.length) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`,
);
return false;
}
const commandLine = this.getWindowsProcessCommandLine(pid);
if (!commandLine) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`,
);
return false;
}
const normalizedCommandLine = this.normalizeWindowsPathForMatch(commandLine);
const markerMatched = markers.some(
(marker) => marker && normalizedCommandLine.includes(marker),
);
if (!markerMatched) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
return false;
}
return true;
}
```
Then `stopUnmanagedBackendByPort` becomes mostly setup + a simple loop:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') return false;
const port = this.getBackendPort();
if (!port) return false;
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) return false;
this.log(
`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`,
);
const backendConfig = this.getBackendConfig();
const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
const requireStrict = this.isGenericWindowsPythonImage(expectedImageName);
const markers = [];
if (Array.isArray(backendConfig.args) && backendConfig.args.length > 0) {
const primaryArg = backendConfig.args[0];
if (typeof primaryArg === 'string' && primaryArg) {
const resolvedPrimaryArg = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(backendConfig.cwd || process.cwd(), primaryArg);
markers.push(this.normalizeWindowsPathForMatch(resolvedPrimaryArg));
markers.push(this.normalizeWindowsPathForMatch(path.basename(primaryArg)));
}
}
for (const pid of pids) {
if (!this.isMatchingBackendPid(pid, backendConfig, expectedImageName, markers, requireStrict)) {
continue;
}
try {
spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
} catch {}
}
return true;
}
```
This preserves all the safety logic you’ve added, but:
- keeps the main method’s control flow linear
- keeps PID-matching responsibilities in one place
- makes it easier to reason about / test PID matching separately.
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- In
desktop/scripts/build-backend.mjs, consider validating that the selected CPython runtime matches the expected Python major/minor version and can at least execute a trivial command (e.g.,python -c 'import pip') so misconfigured or incompatible runtimes fail fast at build time instead of only at app runtime. - The backend process identification on Windows now relies on parsing the command line via PowerShell (
getWindowsProcessCommandLine); it may be worth constraining when/how often this is called or caching results per PID to avoid repeated PowerShell invocations during shutdown when many PIDs are present.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `desktop/scripts/build-backend.mjs`, consider validating that the selected CPython runtime matches the expected Python major/minor version and can at least execute a trivial command (e.g., `python -c 'import pip'`) so misconfigured or incompatible runtimes fail fast at build time instead of only at app runtime.
- The backend process identification on Windows now relies on parsing the command line via PowerShell (`getWindowsProcessCommandLine`); it may be worth constraining when/how often this is called or caching results per PID to avoid repeated PowerShell invocations during shutdown when many PIDs are present.
## Individual Comments
### Comment 1
<location> `desktop/README.md:156` </location>
<code_context>
+
+- `Missing CPython runtime source`: set `ASTRBOT_DESKTOP_CPYTHON_HOME` (or `ASTRBOT_DESKTOP_BACKEND_RUNTIME`).
+- `Cannot find Python executable in runtime`: runtime directory is invalid or incomplete.
+- `Failed to detect purelib from runtime python`: runtime Python cannot run correctly.
+
If Electron download times out on restricted networks, configure mirrors before install:
</code_context>
<issue_to_address>
**nitpick (typo):** Capitalize "Python" for consistency and correctness.
Here, "runtime python" should be "runtime Python" to match the earlier "Python executable" and the later "runtime Python cannot run correctly."
```suggestion
- `Failed to detect purelib from runtime Python`: runtime Python cannot run correctly.
```
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:175` </location>
<code_context>
+ return this.packagedBackendManifest;
+ }
+
+ getPackagedBackendAppDir() {
+ const backendDir = this.getPackagedBackendDir();
+ if (!backendDir) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the new manifest-based helpers and Windows PID matching logic into shared utilities to centralize behavior and reduce duplication.
You can reduce the added complexity in two focused places without changing behavior: manifest-driven path resolution and Windows PID matching.
---
### 1. Consolidate manifest-based path helpers
`getPackagedBackendAppDir`, `getPackagedBackendLaunchScriptPath`, and `getPackagedRuntimePythonPath` all:
- recompute `backendDir`
- call `getPackagedBackendManifest`
- compute a relative path with a default
- join + `existsSync`
This can be centralized so you only resolve the manifest once and reduce duplication.
**Example refactor (internal helper + cohesive config):**
```js
// Small internal helper
resolveManifestPath(fieldName, defaultValue) {
const backendDir = this.getPackagedBackendDir();
if (!backendDir) return null;
const manifest = this.getPackagedBackendManifest() || {};
const rel = typeof manifest[fieldName] === 'string' && manifest[fieldName]
? manifest[fieldName]
: defaultValue;
const candidate = path.join(backendDir, rel);
return fs.existsSync(candidate) ? candidate : null;
}
// Single cohesive config getter
getPackagedRuntimeConfig() {
if (!this.app.isPackaged) return null;
const backendDir = this.getPackagedBackendDir();
if (!backendDir) return null;
const pythonRelativeDefault =
process.platform === 'win32'
? path.join('python', 'Scripts', 'python.exe')
: path.join('python', 'bin', 'python3');
const appDir = this.resolveManifestPath('app', 'app');
const entry = this.resolveManifestPath('entrypoint', 'launch_backend.py');
const python = this.resolveManifestPath('python', pythonRelativeDefault);
if (!python || !entry) return null;
return { backendDir, appDir, entry, python };
}
```
Then the existing helpers become thin wrappers (or can be inlined where used):
```js
getPackagedBackendAppDir() {
const cfg = this.getPackagedRuntimeConfig();
return cfg ? cfg.appDir : null;
}
getPackagedBackendLaunchScriptPath() {
const cfg = this.getPackagedRuntimeConfig();
return cfg ? cfg.entry : null;
}
getPackagedRuntimePythonPath() {
const cfg = this.getPackagedRuntimeConfig();
return cfg ? cfg.python : null;
}
buildPackagedBackendLaunch(webuiDir) {
const cfg = this.getPackagedRuntimeConfig();
if (!cfg) return null;
const args = [cfg.entry];
if (webuiDir) args.push('--webui-dir', webuiDir);
return { cmd: cfg.python, args, shell: false };
}
```
This keeps all existing behavior intact but:
- removes repeated `backendDir` / manifest reads
- gives a single “runtime config” mental model
- makes future fields easier to add.
---
### 2. Encapsulate Windows PID matching logic
`stopUnmanagedBackendByPort` is handling:
- backendConfig extraction
- image-name matching
- deciding if strict command line check is required
- resolving/normalizing markers
- per-PID log decisions and `taskkill`
You already added `normalizeWindowsPathForMatch`, `isGenericWindowsPythonImage`, and `getWindowsProcessCommandLine`; you can push the branching into a single predicate like “does this PID look like our backend?” to linearize `stopUnmanagedBackendByPort`.
**Example refactor:**
```js
// Internal helper: decide whether a PID is our backend
isMatchingBackendPid(pid, backendConfig, expectedImageName, markers, requireStrict) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`);
return false;
}
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
if (!requireStrict) return true;
if (!markers.length) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`,
);
return false;
}
const commandLine = this.getWindowsProcessCommandLine(pid);
if (!commandLine) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`,
);
return false;
}
const normalizedCommandLine = this.normalizeWindowsPathForMatch(commandLine);
const markerMatched = markers.some(
(marker) => marker && normalizedCommandLine.includes(marker),
);
if (!markerMatched) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
return false;
}
return true;
}
```
Then `stopUnmanagedBackendByPort` becomes mostly setup + a simple loop:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') return false;
const port = this.getBackendPort();
if (!port) return false;
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) return false;
this.log(
`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`,
);
const backendConfig = this.getBackendConfig();
const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
const requireStrict = this.isGenericWindowsPythonImage(expectedImageName);
const markers = [];
if (Array.isArray(backendConfig.args) && backendConfig.args.length > 0) {
const primaryArg = backendConfig.args[0];
if (typeof primaryArg === 'string' && primaryArg) {
const resolvedPrimaryArg = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(backendConfig.cwd || process.cwd(), primaryArg);
markers.push(this.normalizeWindowsPathForMatch(resolvedPrimaryArg));
markers.push(this.normalizeWindowsPathForMatch(path.basename(primaryArg)));
}
}
for (const pid of pids) {
if (!this.isMatchingBackendPid(pid, backendConfig, expectedImageName, markers, requireStrict)) {
continue;
}
try {
spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
} catch {}
}
return true;
}
```
This preserves all the safety logic you’ve added, but:
- keeps the main method’s control flow linear
- keeps PID-matching responsibilities in one place
- makes it easier to reason about / test PID matching separately.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey,我发现了 4 个问题,并给出了一些总体反馈:
- 在 Windows 的非托管 backend 清理逻辑中,
getWindowsProcessCommandLine会为每个 PID 调用一次同步的 PowerShell 命令,而且没有设置超时或回退机制;建议增加一个超时时间,并在 PowerShell 不可用或很慢的环境下提供一个优雅的回退方案,以避免在关闭应用时发生卡死。 - 在
desktop/scripts/build-backend.mjs中,即使源目录和目标目录有重叠,构建流程仍会递归地将 runtime 目录cpSync到desktop/resources/backend/python;可以增加一个保护条件,阻止ASTRBOT_DESKTOP_CPYTHON_HOME指向desktop/resources/backend内部,以避免构建产物被意外地复制到自身目录中。
AI 代理用提示词
Please address the comments from this code review:
## Overall Comments
- On Windows unmanaged-backend cleanup, `getWindowsProcessCommandLine` invokes a synchronous PowerShell command per PID without a timeout or fallback; consider adding a timeout and a graceful fallback for environments where PowerShell is unavailable or slow to avoid hangs during shutdown.
- In `desktop/scripts/build-backend.mjs`, the build will recursively `cpSync` the runtime directory into `desktop/resources/backend/python` even if the source and destination overlap; adding a guard to prevent `ASTRBOT_DESKTOP_CPYTHON_HOME` pointing inside `desktop/resources/backend` would avoid accidentally copying build artifacts into themselves.
## Individual Comments
### Comment 1
<location> `desktop/lib/backend-manager.js:728-753` </location>
<code_context>
+ );
+ }
+
+ getWindowsProcessCommandLine(pid) {
+ const numericPid = Number.parseInt(`${pid}`, 10);
+ if (!Number.isInteger(numericPid)) {
+ return null;
+ }
+
+ const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${numericPid}"; if ($null -ne $p) { $p.CommandLine }`;
+ const result = spawnSync(
+ 'powershell',
+ ['-NoProfile', '-NonInteractive', '-Command', query],
+ {
+ stdio: ['ignore', 'pipe', 'ignore'],
+ encoding: 'utf8',
+ windowsHide: true,
+ },
+ );
+ if (result.status !== 0 || !result.stdout) {
+ return null;
+ }
+
+ const line = result.stdout
+ .split(/\r?\n/)
+ .map((item) => item.trim())
+ .find((item) => item.length > 0);
+ return line || null;
+ }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 考虑为 PowerShell 的 `spawnSync` 调用添加一个超时时间,以避免在关闭时可能出现的卡死。
由于这段代码跑在关闭/清理路径上,使用同步调用是合理的,但如果 PowerShell 进程变慢或卡住,仍然会阻塞应用关闭。通过为 `spawnSync` 添加一个合理的 `timeout`,并在超时时将结果视为 `null`/命令行不可用,可以在不改变清理行为的前提下提高健壮性。
```suggestion
getWindowsProcessCommandLine(pid) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) {
return null;
}
const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${numericPid}"; if ($null -ne $p) { $p.CommandLine }`;
const result = spawnSync(
'powershell',
['-NoProfile', '-NonInteractive', '-Command', query],
{
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
windowsHide: true,
// Avoid hangs during shutdown: fail fast if PowerShell is slow or stuck.
timeout: 2000, // milliseconds
},
);
const timedOut = result.error && result.error.code === 'ETIMEDOUT';
if (timedOut || result.status !== 0 || !result.stdout) {
return null;
}
const line = result.stdout
.split(/\r?\n/)
.map((item) => item.trim())
.find((item) => item.length > 0);
return line || null;
}
```
</issue_to_address>
### Comment 2
<location> `astrbot/core/utils/pip_installer.py:441` </location>
<code_context>
+ wheel_only: bool = False,
) -> None:
args = ["install"]
+ pip_install_args = self.pip_install_arg.split() if self.pip_install_arg else []
requested_requirements: set[str] = set()
if package_name:
</code_context>
<issue_to_address>
**suggestion:** 直接对 `pip_install_arg` 使用普通的 `str.split()` 会错误处理带引号的参数,并影响对 `wheel_only` 的检测。
由于 `pip_install_args` 是用一个简单的 `.split()` 构造的,任何带引号的片段或带空格的值(例如 extra-index URL 或复合选项)都可能被错误拆分,因此用于检测 `--only-binary` / `--prefer-binary` 的逻辑可能看不到真实的参数 token。为了更健壮,建议使用 `shlex.split(self.pip_install_arg)`,它会正确尊重引号,同时依然返回一个参数列表。
建议实现:
```python
import shlex
class PipInstaller:
```
```python
pip_install_args = shlex.split(self.pip_install_arg) if self.pip_install_arg else []
```
如果这个文件在顶部已经有 import 区块,建议把 `import shlex` 移动到上面,与其他标准库 import 放在一起,以符合项目的 import 排序约定。
</issue_to_address>
### Comment 3
<location> `desktop/README.md:43` </location>
<code_context>
pnpm --dir desktop run dist:full
```
+If you are already developing in this repository, you can directly reuse the local virtual environment as runtime:
+
+```bash
</code_context>
<issue_to_address>
**nitpick (typo):** 建议将 "as runtime" 改为 "as the runtime",语气会更自然一些。
例如:`reuse the local virtual environment as the runtime` 会更符合英文习惯表达。
</issue_to_address>
### Comment 4
<location> `desktop/lib/backend-manager.js:133` </location>
<code_context>
}
- getPackagedBackendPath() {
+ getPackagedBackendDir() {
if (!this.app.isPackaged) {
return null;
</code_context>
<issue_to_address>
**issue (complexity):** 建议将打包 backend 的 manifest/路径处理逻辑集中起来,并把 Windows 非托管 backend 清理的启发式逻辑抽取到独立的帮助方法中,以简化新增逻辑,让每个方法更易于理解。
你可以在不改变行为的情况下,通过以下方式降低新增复杂度:
---
### 1. 集中处理打包 backend 的 manifest/路径逻辑
目前每个 helper 都会:
- 重新解析 `backendDir`
- 重新获取 `manifest`
- 重复执行“相对路径 → join → existsSync → null”的模式
- 反复对 `manifest.<field>` 做类型保护
你可以保持对外 API 不变,但将这些逻辑重构为:
1. 一个带缓存的单一加载器,它:
- 解析 `backendDir`
- 加载并解析 manifest,一次完成
- 应用默认值
- 预先解析出绝对路径
2. 一个用于“相对路径 → join → existsSync”模式的小工具函数。
例如:
```js
// Small utility for path fields
resolveManifestPath(backendDir, manifest, key, defaultRelative) {
const rel = manifest && typeof manifest[key] === 'string' && manifest[key]
? manifest[key]
: defaultRelative;
const candidate = path.join(backendDir, rel);
return fs.existsSync(candidate) ? candidate : null;
}
// Centralized loader
loadPackagedBackendConfig() {
if (!this.app.isPackaged) return null;
if (this._packagedBackendConfig) return this._packagedBackendConfig;
const backendDir = path.join(process.resourcesPath, 'backend');
if (!fs.existsSync(backendDir)) return null;
let manifest = {};
const manifestPath = path.join(backendDir, 'runtime-manifest.json');
if (fs.existsSync(manifestPath)) {
try {
const raw = fs.readFileSync(manifestPath, 'utf8');
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') manifest = parsed;
} catch (error) {
this.log(
`Failed to parse packaged backend manifest: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
const appDir = this.resolveManifestPath(backendDir, manifest, 'app', 'app');
const launchScriptPath = this.resolveManifestPath(
backendDir,
manifest,
'entrypoint',
'launch_backend.py',
);
const defaultPythonRel =
process.platform === 'win32'
? path.join('python', 'Scripts', 'python.exe')
: path.join('python', 'bin', 'python3');
const pythonPath = this.resolveManifestPath(
backendDir,
manifest,
'python',
defaultPythonRel,
);
this._packagedBackendConfig = Object.freeze({
backendDir,
appDir,
launchScriptPath,
pythonPath,
});
return this._packagedBackendConfig;
}
```
然后,其他 helpers 就可以变成很薄的一层包装(或者在使用处直接内联):
```js
getPackagedBackendAppDir() {
const cfg = this.loadPackagedBackendConfig();
return cfg ? cfg.appDir : null;
}
getPackagedBackendLaunchScriptPath() {
const cfg = this.loadPackagedBackendConfig();
return cfg ? cfg.launchScriptPath : null;
}
getPackagedRuntimePythonPath() {
const cfg = this.loadPackagedBackendConfig();
return cfg ? cfg.pythonPath : null;
}
buildPackagedBackendLaunch(webuiDir) {
const cfg = this.loadPackagedBackendConfig();
if (!cfg || !cfg.pythonPath || !cfg.launchScriptPath) return null;
const args = [cfg.launchScriptPath];
if (webuiDir) args.push('--webui-dir', webuiDir);
return { cmd: cfg.pythonPath, args, shell: false };
}
```
这样可以:
- 去掉重复的 null 检查以及 `backendDir`/manifest 管道逻辑。
- 将 manifest 解析、默认值和缓存集中在一个地方。
- 让依赖这些信息的方法变得非常简单、易于理解。
---
### 2. 抽取 Windows 非托管清理的启发式逻辑
`stopUnmanagedBackendByPort` 现在在循环中混合了很多不同的关注点。你可以把“这个 pid 是否应该被杀掉?”的逻辑提取为独立 helper,让端口查找 + 循环遍历保持更纯粹。
例如:
```js
shouldKillUnmanagedBackendProcess(pid, processInfo, backendConfig) {
const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
const requireStrictCommandLineCheck =
this.isGenericWindowsPythonImage(expectedImageName);
if (!requireStrictCommandLineCheck) {
return true;
}
const expectedCommandLineMarkers = [];
if (Array.isArray(backendConfig.args) && backendConfig.args.length > 0) {
const primaryArg = backendConfig.args[0];
if (typeof primaryArg === 'string' && primaryArg) {
const resolvedPrimaryArg = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(backendConfig.cwd || process.cwd(), primaryArg);
expectedCommandLineMarkers.push(
this.normalizeWindowsPathForMatch(resolvedPrimaryArg),
);
expectedCommandLineMarkers.push(
this.normalizeWindowsPathForMatch(path.basename(primaryArg)),
);
}
}
if (!expectedCommandLineMarkers.length) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`,
);
return false;
}
const commandLine = this.getWindowsProcessCommandLine(pid);
if (!commandLine) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`,
);
return false;
}
const normalizedCommandLine = this.normalizeWindowsPathForMatch(commandLine);
const markerMatched = expectedCommandLineMarkers.some(
(marker) => marker && normalizedCommandLine.includes(marker),
);
if (!markerMatched) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
return false;
}
return true;
}
```
并简化主方法:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') return false;
const port = this.getBackendPort();
if (!port) return false;
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) return false;
this.log(
`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`,
);
const backendConfig = this.getBackendConfig();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`,
);
continue;
}
if (!this.shouldKillUnmanagedBackendProcess(pid, processInfo, backendConfig)) {
continue;
}
try {
spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
} catch {}
}
return true;
}
```
这样可以保留所有现有检查和日志记录,但:
- 让 `stopUnmanagedBackendByPort` 主要负责流程编排。
- 将“什么算是 AstrBot backend 进程”的判断逻辑集中在一个 helper 中。
- 减少循环中的分支和嵌套,在不损失新行为的情况下,解决复杂度问题。
</issue_to_address>请帮我变得更有用!欢迎在每条评论上点 👍 或 👎,我会根据反馈改进后续评审。
Original comment in English
Hey - I've found 4 issues, and left some high level feedback:
- On Windows unmanaged-backend cleanup,
getWindowsProcessCommandLineinvokes a synchronous PowerShell command per PID without a timeout or fallback; consider adding a timeout and a graceful fallback for environments where PowerShell is unavailable or slow to avoid hangs during shutdown. - In
desktop/scripts/build-backend.mjs, the build will recursivelycpSyncthe runtime directory intodesktop/resources/backend/pythoneven if the source and destination overlap; adding a guard to preventASTRBOT_DESKTOP_CPYTHON_HOMEpointing insidedesktop/resources/backendwould avoid accidentally copying build artifacts into themselves.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- On Windows unmanaged-backend cleanup, `getWindowsProcessCommandLine` invokes a synchronous PowerShell command per PID without a timeout or fallback; consider adding a timeout and a graceful fallback for environments where PowerShell is unavailable or slow to avoid hangs during shutdown.
- In `desktop/scripts/build-backend.mjs`, the build will recursively `cpSync` the runtime directory into `desktop/resources/backend/python` even if the source and destination overlap; adding a guard to prevent `ASTRBOT_DESKTOP_CPYTHON_HOME` pointing inside `desktop/resources/backend` would avoid accidentally copying build artifacts into themselves.
## Individual Comments
### Comment 1
<location> `desktop/lib/backend-manager.js:728-753` </location>
<code_context>
+ );
+ }
+
+ getWindowsProcessCommandLine(pid) {
+ const numericPid = Number.parseInt(`${pid}`, 10);
+ if (!Number.isInteger(numericPid)) {
+ return null;
+ }
+
+ const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${numericPid}"; if ($null -ne $p) { $p.CommandLine }`;
+ const result = spawnSync(
+ 'powershell',
+ ['-NoProfile', '-NonInteractive', '-Command', query],
+ {
+ stdio: ['ignore', 'pipe', 'ignore'],
+ encoding: 'utf8',
+ windowsHide: true,
+ },
+ );
+ if (result.status !== 0 || !result.stdout) {
+ return null;
+ }
+
+ const line = result.stdout
+ .split(/\r?\n/)
+ .map((item) => item.trim())
+ .find((item) => item.length > 0);
+ return line || null;
+ }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider adding a timeout to the PowerShell `spawnSync` call to avoid potential hangs during shutdown.
Since this runs in shutdown/cleanup paths, a synchronous call is reasonable, but a slow or stuck PowerShell process would still block app shutdown. Adding a reasonable `timeout` to `spawnSync` and treating timeouts as `null`/unavailable command lines would improve robustness without changing the cleanup behavior.
```suggestion
getWindowsProcessCommandLine(pid) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) {
return null;
}
const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${numericPid}"; if ($null -ne $p) { $p.CommandLine }`;
const result = spawnSync(
'powershell',
['-NoProfile', '-NonInteractive', '-Command', query],
{
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
windowsHide: true,
// Avoid hangs during shutdown: fail fast if PowerShell is slow or stuck.
timeout: 2000, // milliseconds
},
);
const timedOut = result.error && result.error.code === 'ETIMEDOUT';
if (timedOut || result.status !== 0 || !result.stdout) {
return null;
}
const line = result.stdout
.split(/\r?\n/)
.map((item) => item.trim())
.find((item) => item.length > 0);
return line || null;
}
```
</issue_to_address>
### Comment 2
<location> `astrbot/core/utils/pip_installer.py:441` </location>
<code_context>
+ wheel_only: bool = False,
) -> None:
args = ["install"]
+ pip_install_args = self.pip_install_arg.split() if self.pip_install_arg else []
requested_requirements: set[str] = set()
if package_name:
</code_context>
<issue_to_address>
**suggestion:** Using a plain `str.split()` for `pip_install_arg` can mis-handle quoted arguments and affect `wheel_only` detection.
Because `pip_install_args` is built with a naive `.split()`, any quoted segments or values with spaces (e.g., extra-index URLs or composite options) may be split incorrectly, so the `wheel_only` detection for `--only-binary` / `--prefer-binary` may not see the real tokens. To make this robust, consider using `shlex.split(self.pip_install_arg)` instead, which will respect quoting while still returning a list of args.
Suggested implementation:
```python
import shlex
class PipInstaller:
```
```python
pip_install_args = shlex.split(self.pip_install_arg) if self.pip_install_arg else []
```
If this file already has an import section at the top, you may want to move `import shlex` up to keep it grouped with the other standard library imports, following your project's import ordering conventions.
</issue_to_address>
### Comment 3
<location> `desktop/README.md:43` </location>
<code_context>
pnpm --dir desktop run dist:full
```
+If you are already developing in this repository, you can directly reuse the local virtual environment as runtime:
+
+```bash
</code_context>
<issue_to_address>
**nitpick (typo):** Consider "as the runtime" instead of "as runtime" for natural phrasing.
For example: `reuse the local virtual environment as the runtime` reads more naturally.
</issue_to_address>
### Comment 4
<location> `desktop/lib/backend-manager.js:133` </location>
<code_context>
}
- getPackagedBackendPath() {
+ getPackagedBackendDir() {
if (!this.app.isPackaged) {
return null;
</code_context>
<issue_to_address>
**issue (complexity):** Consider centralizing the packaged backend manifest/path handling and extracting the Windows unmanaged-backend cleanup heuristics into dedicated helpers to simplify the new logic and make each method easier to reason about.
You can reduce the added complexity without changing behavior by:
---
### 1. Centralizing packaged backend manifest/path logic
Right now each helper:
- Re-resolves `backendDir`
- Re-fetches `manifest`
- Repeats “relative path → join → existsSync → null” pattern
- Repeats type guards on `manifest.<field>`
You can keep the same externally-visible API but factor this into:
1. A single cached loader that:
- Resolves `backendDir`
- Loads + parses the manifest once
- Applies defaults
- Pre-resolves absolute paths
2. A tiny utility for the “relative → joined → existsSync” pattern.
For example:
```js
// Small utility for path fields
resolveManifestPath(backendDir, manifest, key, defaultRelative) {
const rel = manifest && typeof manifest[key] === 'string' && manifest[key]
? manifest[key]
: defaultRelative;
const candidate = path.join(backendDir, rel);
return fs.existsSync(candidate) ? candidate : null;
}
// Centralized loader
loadPackagedBackendConfig() {
if (!this.app.isPackaged) return null;
if (this._packagedBackendConfig) return this._packagedBackendConfig;
const backendDir = path.join(process.resourcesPath, 'backend');
if (!fs.existsSync(backendDir)) return null;
let manifest = {};
const manifestPath = path.join(backendDir, 'runtime-manifest.json');
if (fs.existsSync(manifestPath)) {
try {
const raw = fs.readFileSync(manifestPath, 'utf8');
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') manifest = parsed;
} catch (error) {
this.log(
`Failed to parse packaged backend manifest: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
const appDir = this.resolveManifestPath(backendDir, manifest, 'app', 'app');
const launchScriptPath = this.resolveManifestPath(
backendDir,
manifest,
'entrypoint',
'launch_backend.py',
);
const defaultPythonRel =
process.platform === 'win32'
? path.join('python', 'Scripts', 'python.exe')
: path.join('python', 'bin', 'python3');
const pythonPath = this.resolveManifestPath(
backendDir,
manifest,
'python',
defaultPythonRel,
);
this._packagedBackendConfig = Object.freeze({
backendDir,
appDir,
launchScriptPath,
pythonPath,
});
return this._packagedBackendConfig;
}
```
Then the other helpers become thin wrappers (or can be inlined where used):
```js
getPackagedBackendAppDir() {
const cfg = this.loadPackagedBackendConfig();
return cfg ? cfg.appDir : null;
}
getPackagedBackendLaunchScriptPath() {
const cfg = this.loadPackagedBackendConfig();
return cfg ? cfg.launchScriptPath : null;
}
getPackagedRuntimePythonPath() {
const cfg = this.loadPackagedBackendConfig();
return cfg ? cfg.pythonPath : null;
}
buildPackagedBackendLaunch(webuiDir) {
const cfg = this.loadPackagedBackendConfig();
if (!cfg || !cfg.pythonPath || !cfg.launchScriptPath) return null;
const args = [cfg.launchScriptPath];
if (webuiDir) args.push('--webui-dir', webuiDir);
return { cmd: cfg.pythonPath, args, shell: false };
}
```
This:
- Removes repeated null checks and `backendDir`/manifest plumbing.
- Keeps manifest parsing, defaults, and caching in one place.
- Makes the dependent methods trivial and easier to reason about.
---
### 2. Extracting unmanaged Windows cleanup heuristics
`stopUnmanagedBackendByPort` now mixes a lot of concerns inside the loop. You can pull out the “should this pid be killed?” logic into a dedicated helper and keep port lookup + iteration separate.
For example:
```js
shouldKillUnmanagedBackendProcess(pid, processInfo, backendConfig) {
const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
const requireStrictCommandLineCheck =
this.isGenericWindowsPythonImage(expectedImageName);
if (!requireStrictCommandLineCheck) {
return true;
}
const expectedCommandLineMarkers = [];
if (Array.isArray(backendConfig.args) && backendConfig.args.length > 0) {
const primaryArg = backendConfig.args[0];
if (typeof primaryArg === 'string' && primaryArg) {
const resolvedPrimaryArg = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(backendConfig.cwd || process.cwd(), primaryArg);
expectedCommandLineMarkers.push(
this.normalizeWindowsPathForMatch(resolvedPrimaryArg),
);
expectedCommandLineMarkers.push(
this.normalizeWindowsPathForMatch(path.basename(primaryArg)),
);
}
}
if (!expectedCommandLineMarkers.length) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`,
);
return false;
}
const commandLine = this.getWindowsProcessCommandLine(pid);
if (!commandLine) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`,
);
return false;
}
const normalizedCommandLine = this.normalizeWindowsPathForMatch(commandLine);
const markerMatched = expectedCommandLineMarkers.some(
(marker) => marker && normalizedCommandLine.includes(marker),
);
if (!markerMatched) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
return false;
}
return true;
}
```
And simplify the main method:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') return false;
const port = this.getBackendPort();
if (!port) return false;
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) return false;
this.log(
`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`,
);
const backendConfig = this.getBackendConfig();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`,
);
continue;
}
if (!this.shouldKillUnmanagedBackendProcess(pid, processInfo, backendConfig)) {
continue;
}
try {
spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
} catch {}
}
return true;
}
```
This keeps all existing checks and logging, but:
- Makes `stopUnmanagedBackendByPort` mostly orchestration.
- Localizes “what qualifies as an AstrBot backend process” into one helper.
- Reduces in-loop branching and nesting, addressing the complexity concern without losing the new behavior.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
@sourcery-ai review |
1 similar comment
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了 3 个问题,并给出了一些整体反馈:
build-backend.mjs中的readProjectRequiresPythonLowerBound/resolveExpectedRuntimeVersion逻辑依赖一个非常具体的requires-python = "..."行格式;建议考虑使用一个最小的 TOML 解析器,或者更灵活的正则,这样在格式发生变化时(单引号、额外空白、或放在工具专用的 table 下)不会悄悄导致运行时版本检测失效。- 在
BackendManager.stopUnmanagedBackendByPort中,getWindowsProcessCommandLine里的同步 PowerShell(powershell/pwsh)调用,每个 PID 最长可以阻塞事件循环达 2 秒;如果这段逻辑运行在 UI / 关键路径上,建议把命令行探测迁移到异步 / worker 进程中,或者缩短超时时间并收紧这个路径被调用的条件。
给 AI Agent 的提示
Please address the comments from this code review:
## Overall Comments
- `build-backend.mjs` 中的 `readProjectRequiresPythonLowerBound` / `resolveExpectedRuntimeVersion` 逻辑依赖一个非常具体的 `requires-python = "..."` 行格式;建议考虑使用一个最小的 TOML 解析器,或者更灵活的正则,这样在格式发生变化时(单引号、额外空白、或放在工具专用的 table 下)不会悄悄导致运行时版本检测失效。
- 在 `BackendManager.stopUnmanagedBackendByPort` 中,`getWindowsProcessCommandLine` 里的同步 PowerShell(`powershell` / `pwsh`)调用,每个 PID 最长可以阻塞事件循环达 2 秒;如果这段逻辑运行在 UI / 关键路径上,建议把命令行探测迁移到异步 / worker 进程中,或者缩短超时时间并收紧这个路径被调用的条件。
## Individual Comments
### Comment 1
<location> `desktop/lib/backend-manager.js:860-869` </location>
<code_context>
+ return null;
+ }
+
+ buildWindowsUnmanagedBackendMatcher(backendConfig) {
+ const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
+ const requireStrictCommandLineCheck =
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against missing or incomplete backendConfig when building the unmanaged Windows backend matcher.
In `stopUnmanagedBackendByPort`, `this.getBackendConfig()` is passed straight into `buildWindowsUnmanagedBackendMatcher` without checking for `null`/`undefined` or expected shape. If `getBackendConfig()` ever returns a missing/partial config (e.g. no `cmd`/`cwd`/`args`), accessing `backendConfig.cmd`/`.cwd` will throw. Consider either early-returning from `stopUnmanagedBackendByPort` when the config is invalid, or having `buildWindowsUnmanagedBackendMatcher` handle missing/partial config (e.g. return `null`) and skipping cleanup in that case.
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:136` </location>
<code_context>
- getPackagedBackendPath() {
- if (!this.app.isPackaged) {
+ getPackagedBackendDir() {
+ const packagedBackendConfig = this.loadPackagedBackendConfig();
+ return packagedBackendConfig ? packagedBackendConfig.backendDir : null;
</code_context>
<issue_to_address>
**issue (complexity):** 考虑简化新的 packaged-backend 配置和 Windows 进程匹配辅助函数,通过内联逻辑并整合这些小的包装器,让控制流更加线性、易于阅读。
- 新增的 packaged-backend 配置辅助函数都是非常薄的一层包装,并且扩展了类的公共接口。你可以把配置的使用集中在一起,这样既能把配置结构局部化,又能减少方法数量。
```js
// 保留一个统一入口
getPackagedBackendConfig() {
return this.loadPackagedBackendConfig();
}
// 在调用点直接使用,而不是多个很小的 getter
resolveBackendCwd() {
if (!this.app.isPackaged) {
return path.resolve(this.baseDir, '..');
}
const cfg = this.getPackagedBackendConfig();
return (cfg && cfg.appDir) || this.resolveBackendRoot();
}
buildPackagedBackendLaunch(webuiDir) {
const cfg = this.getPackagedBackendConfig();
if (!cfg || !cfg.runtimePythonPath || !cfg.launchScriptPath) {
return null;
}
const args = [cfg.launchScriptPath];
if (webuiDir) args.push('--webui-dir', webuiDir);
return { cmd: cfg.runtimePythonPath, args, shell: false };
}
```
这样你就可以删除 `getPackagedBackendManifest`、`getPackagedBackendAppDir`、`getPackagedBackendLaunchScriptPath`、`getPackagedRuntimePythonPath` 等方法,只在 `loadPackagedBackendConfig()` 加一个统一 getter 来持有配置结构的知识。
- `resolveManifestPath` 加上在 `loadPackagedBackendConfig()` 中的三个调用,其实可以改成更显式的写法而不是通用方法,这样更容易一眼看懂:
```js
loadPackagedBackendConfig() {
// ...
const manifest = this.parsePackagedBackendManifest(backendDir) || {};
const defaultPythonRelative =
process.platform === 'win32'
? path.join('python', 'Scripts', 'python.exe')
: path.join('python', 'bin', 'python3');
const resolve = (key, fallback) => {
const rel = (typeof manifest[key] === 'string' && manifest[key]) || fallback;
const candidate = path.join(backendDir, rel);
return fs.existsSync(candidate) ? candidate : null;
};
this.packagedBackendConfig = Object.freeze({
backendDir,
manifest: Object.keys(manifest).length ? manifest : null,
appDir: resolve('app', 'app'),
launchScriptPath: resolve('entrypoint', 'launch_backend.py'),
runtimePythonPath: resolve('python', defaultPythonRelative),
});
return this.packagedBackendConfig;
}
```
这样所有逻辑都集中在一个地方,而不需要单独的 `resolveManifestPath` 方法。
- PowerShell / Pwsh 查询辅助函数几乎完全相同;你可以把它们合并成一个可传参的函数,并把 `parseWindowsProcessCommandLine` 内联掉,以减少间接跳转:
```js
queryWindowsProcessCommandLine(pid, shellName) {
const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($null -ne $p) { $p.CommandLine }`;
return spawnSync(
shellName,
['-NoProfile', '-NonInteractive', '-Command', query],
{
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
windowsHide: true,
timeout: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
},
);
}
getWindowsProcessCommandLine(pid) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) return null;
const cached = this.windowsProcessCommandLineCache.get(numericPid);
if (cached && Date.now() - cached.timestampMs <= WINDOWS_PROCESS_COMMAND_LINE_CACHE_TTL_MS) {
return cached.commandLine;
}
this.windowsProcessCommandLineCache.delete(numericPid);
for (const shellName of ['powershell', 'pwsh']) {
let result;
try {
result = this.queryWindowsProcessCommandLine(numericPid, shellName);
} catch (error) {
// existing logging
continue;
}
// existing error/timeout handling...
if (result.status === 0 && result.stdout) {
const commandLine =
result.stdout
.split(/\r?\n/)
.map((s) => s.trim())
.find((s) => s.length > 0) || null;
this.windowsProcessCommandLineCache.set(numericPid, {
commandLine,
timestampMs: Date.now(),
});
return commandLine;
}
}
this.windowsProcessCommandLineCache.set(numericPid, { commandLine: null, timestampMs: Date.now() });
return null;
}
```
- 匹配器对象(`buildWindowsUnmanagedBackendMatcher` + `shouldKillUnmanagedBackendProcess`)只在一个调用点被使用。你可以用一个函数同时做 image 检查和命令行检查,让流程更线性:
```js
isUnmanagedBackendProcess(pid, processInfo, backendConfig) {
const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
if (!this.isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
const args = Array.isArray(backendConfig.args) ? backendConfig.args : [];
const primaryArg = args[0];
if (typeof primaryArg !== 'string' || !primaryArg) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`,
);
return false;
}
const cwd = backendConfig.cwd || process.cwd();
const markers = [
this.normalizeWindowsPathForMatch(
path.isAbsolute(primaryArg) ? primaryArg : path.resolve(cwd, primaryArg),
),
this.normalizeWindowsPathForMatch(path.basename(primaryArg)),
];
const commandLine = this.getWindowsProcessCommandLine(pid);
if (!commandLine) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`,
);
return false;
}
const normalized = this.normalizeWindowsPathForMatch(commandLine);
const matched = markers.some((m) => m && normalized.includes(m));
if (!matched) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
return false;
}
return true;
}
async stopUnmanagedBackendByPort() {
// ...
const backendConfig = this.getBackendConfig();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
// existing log
continue;
}
if (!this.isUnmanagedBackendProcess(pid, processInfo, backendConfig)) {
continue;
}
// taskkill...
}
}
```
这样可以完全移除 `processMatcher` 对象 / 类型,同时保留所有检查逻辑。
- 考虑到 `stopUnmanagedBackendByPort` 是一个不常被调用的操作,你可能不需要带 TTL 和清理逻辑的长生命周期缓存。一个更简单的按调用缓存(没有时间戳,也没有清理)就能保持行为一致,同时移除全局状态:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') {
return false;
}
const commandLineCache = new Map(); // pid -> string|null
const getCommandLineOnce = (pid) => {
if (commandLineCache.has(pid)) return commandLineCache.get(pid);
const value = this.getWindowsProcessCommandLine(pid); // simplified version w/o TTL
commandLineCache.set(pid, value);
return value;
};
// 在 isUnmanagedBackendProcess 中使用 getCommandLineOnce,或者直接内联
}
```
如果你想保留现有的 TTL 语义,也可以把缓存范围收窄到 `stopUnmanagedBackendByPort`(例如把一个 `getCommandLine` 函数传给 `isUnmanagedBackendProcess`),从而移除 `this.windowsProcessCommandLineCache` 和 `pruneWindowsProcessCommandLineCache`。
</issue_to_address>
### Comment 3
<location> `desktop/scripts/build-backend.mjs:32` </location>
<code_context>
+ process.exit(1);
+}
+
+const normalizePathForCompare = (targetPath) => {
+ const resolved = path.resolve(targetPath).replace(/[\\/]+$/, '');
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
</code_context>
<issue_to_address>
**issue (complexity):** 考虑简化新的运行时校验辅助函数,通过合并重叠的函数,并使用更清晰的内联工具函数(用于路径重叠检查、版本解析、Python 校验),从而让控制流更直观、更不容易出错。
- 路径重叠检查的辅助函数只是为单个安全检查增加了一层抽象,而且当前还有一个逻辑 bug(`if (! isSameOrSubPath(...))`)。你可以直接用 `path.relative` 配合一个非常小的内联工具,同时实现简化与修复:
```js
const pathsOverlap = (a, b) => {
const relA = path.relative(a, b);
const relB = path.relative(b, a);
const isSub = (rel) => rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
return isSub(relA) || isSub(relB);
};
if (pathsOverlap(runtimeSourceReal, outputDir)) {
console.error(
`CPython runtime source overlaps with backend output directory. ` +
`runtime=${runtimeSourceReal}, output=${outputDir}. ` +
'Please set ASTRBOT_DESKTOP_CPYTHON_HOME to a separate runtime directory.',
);
process.exit(1);
}
```
这样就可以完全移除 `normalizePathForCompare` 和 `isSameOrSubPath`,并且让意图(“这些路径是否有重叠?”)一目了然。
- 版本解析流水线可以通过把 `parseExpectedRuntimeVersion` 和 `readProjectRequiresPythonLowerBound` 合并进 `resolveExpectedRuntimeVersion` 来简化,只保留一个辅助函数:
```js
const resolveExpectedRuntimeVersion = () => {
const parse = (raw, sourceName) => {
const match = /^(\d+)\.(\d+)$/.exec(String(raw).trim());
if (!match) {
console.error(
`Invalid expected Python version from ${sourceName}: ${raw}. ` +
'Expected format <major>.<minor>.',
);
process.exit(1);
}
return { major: Number(match[1]), minor: Number(match[2]) };
};
if (process.env.ASTRBOT_DESKTOP_EXPECTED_PYTHON) {
return parse(process.env.ASTRBOT_DESKTOP_EXPECTED_PYTHON, 'ASTRBOT_DESKTOP_EXPECTED_PYTHON');
}
const pyprojectPath = path.join(rootDir, 'pyproject.toml');
if (fs.existsSync(pyprojectPath)) {
const content = fs.readFileSync(pyprojectPath, 'utf8');
const requires = /^\s*requires-python\s*=\s*"([^"]+)"/m.exec(content)?.[1];
const lower = requires && />=\s*(\d+)\.(\d+)/.exec(requires);
if (lower) {
return parse(`${lower[1]}.${lower[2]}`, 'pyproject.toml requires-python');
}
}
console.error(
'Unable to determine expected runtime Python version. ' +
'Set ASTRBOT_DESKTOP_EXPECTED_PYTHON or declare project.requires-python in pyproject.toml.',
);
process.exit(1);
};
```
这样可以移除两个全局函数,并把所有错误处理放在一个地方。
- 通过把版本比较逻辑抽取成一个明确的条件,并复用一个通用的退出辅助函数,可以简化 `validateRuntimePython`,并修复当前比较逻辑中的 bug:
```js
const fatal = (msg) => {
console.error(msg);
process.exit(1);
};
const validateRuntimePython = (pythonExecutable) => {
const probe = spawnSync(
pythonExecutable,
['-c', 'import sys, pip; print(f"{sys.version_info.major}.{sys.version_info.minor}")'],
{
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
windowsHide: true,
timeout: 5000,
},
);
if (probe.error) {
const reason =
probe.error.code === 'ETIMEDOUT'
? 'runtime Python probe timed out'
: probe.error.message || String(probe.error);
fatal(`Runtime Python probe failed: ${reason}`);
}
if (probe.status !== 0) {
const stderrText = (probe.stderr || '').trim();
fatal(
`Runtime Python probe failed with exit code ${probe.status}.` +
(stderrText ? ` stderr: ${stderrText}` : ''),
);
}
const versionMatch = /(\d+)\.(\d+)/.exec((probe.stdout || '').trim());
if (!versionMatch) {
fatal(
`Runtime Python probe did not report a valid version. Output: ${(probe.stdout || '').trim()}`,
);
}
const actual = { major: Number(versionMatch[1]), minor: Number(versionMatch[2]) };
if (actual.major !== expectedRuntimeVersion.major || actual.minor !== expectedRuntimeVersion.minor) {
fatal(
`Runtime Python version mismatch: expected ${expectedRuntimeVersion.major}.${expectedRuntimeVersion.minor}, ` +
`got ${actual.major}.${actual.minor}.`,
);
}
};
```
这就消除了 `! actualVersion.major !== ...` 这种易混淆的写法,并把 `console.error` / `process.exit(1)` 的模式集中到一起,从而减少分支噪音。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈持续改进代码审查质量。
Original comment in English
Hey - I've found 3 issues, and left some high level feedback:
- The
readProjectRequiresPythonLowerBound/resolveExpectedRuntimeVersionlogic inbuild-backend.mjsrelies on a very specificrequires-python = "..."line format; consider using a minimal TOML parser or more flexible regexes so that alternate formatting (single quotes, extra whitespace, or tool-specific tables) doesn’t silently break runtime version detection. - In
BackendManager.stopUnmanagedBackendByPortthe synchronous PowerShell (powershell/pwsh) calls ingetWindowsProcessCommandLinecan block the event loop for up to 2 seconds per PID; if this runs on the UI/critical path, consider moving command-line probing to an async/worker process or lowering the timeout and tightening when this path is invoked.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `readProjectRequiresPythonLowerBound`/`resolveExpectedRuntimeVersion` logic in `build-backend.mjs` relies on a very specific `requires-python = "..."` line format; consider using a minimal TOML parser or more flexible regexes so that alternate formatting (single quotes, extra whitespace, or tool-specific tables) doesn’t silently break runtime version detection.
- In `BackendManager.stopUnmanagedBackendByPort` the synchronous PowerShell (`powershell`/`pwsh`) calls in `getWindowsProcessCommandLine` can block the event loop for up to 2 seconds per PID; if this runs on the UI/critical path, consider moving command-line probing to an async/worker process or lowering the timeout and tightening when this path is invoked.
## Individual Comments
### Comment 1
<location> `desktop/lib/backend-manager.js:860-869` </location>
<code_context>
+ return null;
+ }
+
+ buildWindowsUnmanagedBackendMatcher(backendConfig) {
+ const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
+ const requireStrictCommandLineCheck =
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against missing or incomplete backendConfig when building the unmanaged Windows backend matcher.
In `stopUnmanagedBackendByPort`, `this.getBackendConfig()` is passed straight into `buildWindowsUnmanagedBackendMatcher` without checking for `null`/`undefined` or expected shape. If `getBackendConfig()` ever returns a missing/partial config (e.g. no `cmd`/`cwd`/`args`), accessing `backendConfig.cmd`/`.cwd` will throw. Consider either early-returning from `stopUnmanagedBackendByPort` when the config is invalid, or having `buildWindowsUnmanagedBackendMatcher` handle missing/partial config (e.g. return `null`) and skipping cleanup in that case.
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:136` </location>
<code_context>
- getPackagedBackendPath() {
- if (!this.app.isPackaged) {
+ getPackagedBackendDir() {
+ const packagedBackendConfig = this.loadPackagedBackendConfig();
+ return packagedBackendConfig ? packagedBackendConfig.backendDir : null;
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new packaged-backend configuration and Windows process-matching helpers by inlining logic and consolidating small wrappers so the control flow is more linear and easier to scan.
- The new packaged-backend config helpers are very thin wrappers and widen the class surface. You can centralize config usage to keep the shape local and reduce method count.
```js
// Keep a single accessor
getPackagedBackendConfig() {
return this.loadPackagedBackendConfig();
}
// Use it at call sites instead of multiple tiny getters
resolveBackendCwd() {
if (!this.app.isPackaged) {
return path.resolve(this.baseDir, '..');
}
const cfg = this.getPackagedBackendConfig();
return (cfg && cfg.appDir) || this.resolveBackendRoot();
}
buildPackagedBackendLaunch(webuiDir) {
const cfg = this.getPackagedBackendConfig();
if (!cfg || !cfg.runtimePythonPath || !cfg.launchScriptPath) {
return null;
}
const args = [cfg.launchScriptPath];
if (webuiDir) args.push('--webui-dir', webuiDir);
return { cmd: cfg.runtimePythonPath, args, shell: false };
}
```
This lets you drop `getPackagedBackendManifest`, `getPackagedBackendAppDir`, `getPackagedBackendLaunchScriptPath`, `getPackagedRuntimePythonPath`, and keep config shape knowledge in `loadPackagedBackendConfig()` plus a single getter.
- `resolveManifestPath` + the three calls in `loadPackagedBackendConfig()` can be made more explicit instead of generic, which makes it easier to scan:
```js
loadPackagedBackendConfig() {
// ...
const manifest = this.parsePackagedBackendManifest(backendDir) || {};
const defaultPythonRelative =
process.platform === 'win32'
? path.join('python', 'Scripts', 'python.exe')
: path.join('python', 'bin', 'python3');
const resolve = (key, fallback) => {
const rel = (typeof manifest[key] === 'string' && manifest[key]) || fallback;
const candidate = path.join(backendDir, rel);
return fs.existsSync(candidate) ? candidate : null;
};
this.packagedBackendConfig = Object.freeze({
backendDir,
manifest: Object.keys(manifest).length ? manifest : null,
appDir: resolve('app', 'app'),
launchScriptPath: resolve('entrypoint', 'launch_backend.py'),
runtimePythonPath: resolve('python', defaultPythonRelative),
});
return this.packagedBackendConfig;
}
```
This keeps the logic in one place without a separate `resolveManifestPath` method.
- The PowerShell/Pwsh query helpers are almost identical; you can collapse them into one parameterized function and inline `parseWindowsProcessCommandLine` to reduce indirection:
```js
queryWindowsProcessCommandLine(pid, shellName) {
const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($null -ne $p) { $p.CommandLine }`;
return spawnSync(
shellName,
['-NoProfile', '-NonInteractive', '-Command', query],
{
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
windowsHide: true,
timeout: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
},
);
}
getWindowsProcessCommandLine(pid) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) return null;
const cached = this.windowsProcessCommandLineCache.get(numericPid);
if (cached && Date.now() - cached.timestampMs <= WINDOWS_PROCESS_COMMAND_LINE_CACHE_TTL_MS) {
return cached.commandLine;
}
this.windowsProcessCommandLineCache.delete(numericPid);
for (const shellName of ['powershell', 'pwsh']) {
let result;
try {
result = this.queryWindowsProcessCommandLine(numericPid, shellName);
} catch (error) {
// existing logging
continue;
}
// existing error/timeout handling...
if (result.status === 0 && result.stdout) {
const commandLine =
result.stdout
.split(/\r?\n/)
.map((s) => s.trim())
.find((s) => s.length > 0) || null;
this.windowsProcessCommandLineCache.set(numericPid, {
commandLine,
timestampMs: Date.now(),
});
return commandLine;
}
}
this.windowsProcessCommandLineCache.set(numericPid, { commandLine: null, timestampMs: Date.now() });
return null;
}
```
- The matcher object (`buildWindowsUnmanagedBackendMatcher` + `shouldKillUnmanagedBackendProcess`) is only used at one call site. You can make the flow more linear with a single function that does both the image and command-line checks:
```js
isUnmanagedBackendProcess(pid, processInfo, backendConfig) {
const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
if (!this.isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
const args = Array.isArray(backendConfig.args) ? backendConfig.args : [];
const primaryArg = args[0];
if (typeof primaryArg !== 'string' || !primaryArg) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`,
);
return false;
}
const cwd = backendConfig.cwd || process.cwd();
const markers = [
this.normalizeWindowsPathForMatch(
path.isAbsolute(primaryArg) ? primaryArg : path.resolve(cwd, primaryArg),
),
this.normalizeWindowsPathForMatch(path.basename(primaryArg)),
];
const commandLine = this.getWindowsProcessCommandLine(pid);
if (!commandLine) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`,
);
return false;
}
const normalized = this.normalizeWindowsPathForMatch(commandLine);
const matched = markers.some((m) => m && normalized.includes(m));
if (!matched) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
return false;
}
return true;
}
async stopUnmanagedBackendByPort() {
// ...
const backendConfig = this.getBackendConfig();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
// existing log
continue;
}
if (!this.isUnmanagedBackendProcess(pid, processInfo, backendConfig)) {
continue;
}
// taskkill...
}
}
```
This removes the `processMatcher` object/type entirely while preserving all checks.
- Given `stopUnmanagedBackendByPort` is an infrequent operation, you may not need a long-lived cache with TTL and pruning. A simpler per-invocation cache (no timestamps, no pruning) keeps the behavior but removes global state:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') {
return false;
}
const commandLineCache = new Map(); // pid -> string|null
const getCommandLineOnce = (pid) => {
if (commandLineCache.has(pid)) return commandLineCache.get(pid);
const value = this.getWindowsProcessCommandLine(pid); // simplified version w/o TTL
commandLineCache.set(pid, value);
return value;
};
// use getCommandLineOnce inside isUnmanagedBackendProcess or inline it there
}
```
If you want to keep the existing TTL semantics, you can still narrow the cache to `stopUnmanagedBackendByPort` (pass a `getCommandLine` function into `isUnmanagedBackendProcess`) and remove `this.windowsProcessCommandLineCache` and `pruneWindowsProcessCommandLineCache`.
</issue_to_address>
### Comment 3
<location> `desktop/scripts/build-backend.mjs:32` </location>
<code_context>
+ process.exit(1);
+}
+
+const normalizePathForCompare = (targetPath) => {
+ const resolved = path.resolve(targetPath).replace(/[\\/]+$/, '');
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new runtime validation helpers by collapsing overlapping functions and using clearer inline utilities for path overlap checks, version resolution, and Python validation to make the control flow more obvious and less error-prone.
- The path-overlap helpers add abstraction for a single safety check and currently have a logic bug (`if (! isSameOrSubPath(...))`). You can both simplify and fix this by using `path.relative` directly with a tiny inline helper:
```js
const pathsOverlap = (a, b) => {
const relA = path.relative(a, b);
const relB = path.relative(b, a);
const isSub = (rel) => rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
return isSub(relA) || isSub(relB);
};
if (pathsOverlap(runtimeSourceReal, outputDir)) {
console.error(
`CPython runtime source overlaps with backend output directory. ` +
`runtime=${runtimeSourceReal}, output=${outputDir}. ` +
'Please set ASTRBOT_DESKTOP_CPYTHON_HOME to a separate runtime directory.',
);
process.exit(1);
}
```
This removes `normalizePathForCompare` and `isSameOrSubPath` entirely and makes the intent (“do these paths overlap?”) immediately obvious.
- The version resolution pipeline can be made easier to follow by collapsing `parseExpectedRuntimeVersion` and `readProjectRequiresPythonLowerBound` into `resolveExpectedRuntimeVersion`, keeping only one helper in play:
```js
const resolveExpectedRuntimeVersion = () => {
const parse = (raw, sourceName) => {
const match = /^(\d+)\.(\d+)$/.exec(String(raw).trim());
if (!match) {
console.error(
`Invalid expected Python version from ${sourceName}: ${raw}. ` +
'Expected format <major>.<minor>.',
);
process.exit(1);
}
return { major: Number(match[1]), minor: Number(match[2]) };
};
if (process.env.ASTRBOT_DESKTOP_EXPECTED_PYTHON) {
return parse(process.env.ASTRBOT_DESKTOP_EXPECTED_PYTHON, 'ASTRBOT_DESKTOP_EXPECTED_PYTHON');
}
const pyprojectPath = path.join(rootDir, 'pyproject.toml');
if (fs.existsSync(pyprojectPath)) {
const content = fs.readFileSync(pyprojectPath, 'utf8');
const requires = /^\s*requires-python\s*=\s*"([^"]+)"/m.exec(content)?.[1];
const lower = requires && />=\s*(\d+)\.(\d+)/.exec(requires);
if (lower) {
return parse(`${lower[1]}.${lower[2]}`, 'pyproject.toml requires-python');
}
}
console.error(
'Unable to determine expected runtime Python version. ' +
'Set ASTRBOT_DESKTOP_EXPECTED_PYTHON or declare project.requires-python in pyproject.toml.',
);
process.exit(1);
};
```
That eliminates two globals and keeps all error handling in one place.
- `validateRuntimePython` can be simplified and have its comparison bug fixed by extracting the version comparison into a single obvious condition and reusing a common exit helper:
```js
const fatal = (msg) => {
console.error(msg);
process.exit(1);
};
const validateRuntimePython = (pythonExecutable) => {
const probe = spawnSync(
pythonExecutable,
['-c', 'import sys, pip; print(f"{sys.version_info.major}.{sys.version_info.minor}")'],
{
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
windowsHide: true,
timeout: 5000,
},
);
if (probe.error) {
const reason =
probe.error.code === 'ETIMEDOUT'
? 'runtime Python probe timed out'
: probe.error.message || String(probe.error);
fatal(`Runtime Python probe failed: ${reason}`);
}
if (probe.status !== 0) {
const stderrText = (probe.stderr || '').trim();
fatal(
`Runtime Python probe failed with exit code ${probe.status}.` +
(stderrText ? ` stderr: ${stderrText}` : ''),
);
}
const versionMatch = /(\d+)\.(\d+)/.exec((probe.stdout || '').trim());
if (!versionMatch) {
fatal(
`Runtime Python probe did not report a valid version. Output: ${(probe.stdout || '').trim()}`,
);
}
const actual = { major: Number(versionMatch[1]), minor: Number(versionMatch[2]) };
if (actual.major !== expectedRuntimeVersion.major || actual.minor !== expectedRuntimeVersion.minor) {
fatal(
`Runtime Python version mismatch: expected ${expectedRuntimeVersion.major}.${expectedRuntimeVersion.minor}, ` +
`got ${actual.major}.${actual.minor}.`,
);
}
};
```
This removes the `! actualVersion.major !== ...` confusion and centralizes the `console.error`/`process.exit(1)` pattern, reducing branching noise.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
SourceryAI
left a comment
There was a problem hiding this comment.
Hey - 我发现了 3 个问题,并给出了一些整体层面的反馈:
- 在
BackendManager.stopUnmanagedBackendByPort中,buildWindowsUnmanagedBackendMatcher无条件地用this.getBackendConfig()调用,而如果当前会话从未启动过后端,这个值可能为null;建议在缺少配置时先做保护,并回退到一个更简单的基于镜像名的匹配方式,以避免在清理过程中出现运行时错误。 build-backend.mjs里的readProjectRequiresPythonLowerBound辅助函数通过正则解析pyproject.toml,并假设requires-python约束是简单形式;使用 TOML 解析器(或者至少处理更复杂的约束/分段)会让不同pyproject布局下的版本检测更健壮。
给 AI Agent 的提示
Please address the comments from this code review:
## Overall Comments
- In `BackendManager.stopUnmanagedBackendByPort`, `buildWindowsUnmanagedBackendMatcher` is called unconditionally with `this.getBackendConfig()`, which can be `null` if the backend was never started in the current session; consider guarding for a missing config and falling back to a simpler image-name-based match to avoid runtime errors during cleanup.
- The `readProjectRequiresPythonLowerBound` helper in `build-backend.mjs` parses `pyproject.toml` with regex and assumes a simple `requires-python` constraint; using a TOML parser (or at least handling more complex specifiers/sections) would make version detection more robust across different `pyproject` layouts.
## Individual Comments
### Comment 1
<location> `astrbot/core/utils/pip_installer.py:442-443` </location>
<code_context>
+ wheel_only: bool = False,
) -> None:
args = ["install"]
+ pip_install_args = (
+ shlex.split(self.pip_install_arg) if self.pip_install_arg else []
+ )
requested_requirements: set[str] = set()
</code_context>
<issue_to_address>
**question (bug_risk):** Consider whether changing from str.split to shlex.split for pip_install_arg might alter behavior for existing configurations.
shlex.split improves handling of quoted arguments but does change behavior versus .split(). Configs that depended on literal spaces or unusual quoting in pip_install_arg may now be parsed differently or even error. If backward compatibility is important, consider gating this behind a feature flag or catching shlex errors and falling back to .split(), with a warning.
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:136` </location>
<code_context>
- getPackagedBackendPath() {
- if (!this.app.isPackaged) {
+ getPackagedBackendDir() {
+ const packagedBackendConfig = this.loadPackagedBackendConfig();
+ return packagedBackendConfig ? packagedBackendConfig.backendDir : null;
</code_context>
<issue_to_address>
**issue (complexity):** Consider inlining the small helper methods and collapsing duplicated logic to reduce indirection and make the backend config and Windows process-handling code easier to follow.
You can trim a fair bit of indirection here without changing behavior.
### 1. Packaged backend config: collapse trivial getters / helpers
You already have `loadPackagedBackendConfig` as a single source of truth; the tiny getters are just pass‑throughs and increase call depth.
Instead of:
```js
getPackagedBackendAppDir() {
const packagedBackendConfig = this.loadPackagedBackendConfig();
return packagedBackendConfig ? packagedBackendConfig.appDir : null;
}
resolveBackendCwd() {
if (!this.app.isPackaged) {
return path.resolve(this.baseDir, '..');
}
return this.getPackagedBackendAppDir() || this.resolveBackendRoot();
}
```
use the config object directly:
```js
resolveBackendCwd() {
if (!this.app.isPackaged) {
return path.resolve(this.baseDir, '..');
}
const cfg = this.loadPackagedBackendConfig();
return (cfg && cfg.appDir) || this.resolveBackendRoot();
}
```
and similarly in `buildPackagedBackendLaunch`:
```js
buildPackagedBackendLaunch(webuiDir) {
const cfg = this.loadPackagedBackendConfig();
if (!cfg || !cfg.runtimePythonPath || !cfg.launchScriptPath) {
return null;
}
const args = [cfg.launchScriptPath];
if (webuiDir) {
args.push('--webui-dir', webuiDir);
}
return {
cmd: cfg.runtimePythonPath,
args,
shell: false,
};
}
```
Then you can remove:
- `getPackagedBackendDir`
- `getPackagedBackendManifest`
- `getPackagedBackendAppDir`
- `getPackagedBackendLaunchScriptPath`
- `getPackagedRuntimePythonPath`
If `parsePackagedBackendManifest` / `resolveManifestPath` are only used in `loadPackagedBackendConfig`, consider inlining them as inner functions to keep the public surface smaller:
```js
loadPackagedBackendConfig() {
if (!this.app.isPackaged) return null;
if (this.packagedBackendConfig) return this.packagedBackendConfig;
const backendDir = path.join(process.resourcesPath, 'backend');
if (!fs.existsSync(backendDir)) return null;
const parseManifest = (dir) => { /* current body */ };
const resolveManifestPath = (dir, manifest, key, def) => { /* current body */ };
const manifest = parseManifest(backendDir);
const manifestForPathResolve = manifest || {};
// ... build frozen config as you already do ...
}
```
### 2. Windows process command line: unify PowerShell helpers and cache pruning
The two query functions are identical except for the executable name; you can replace both with one parametrized helper:
```js
queryWindowsProcessCommandLine(shellName, pid) {
const query =
`$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}";` +
' if ($null -ne $p) { $p.CommandLine }';
return spawnSync(
shellName,
['-NoProfile', '-NonInteractive', '-Command', query],
{
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
windowsHide: true,
timeout: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
},
);
}
```
and simplify `getWindowsProcessCommandLine`:
```js
getWindowsProcessCommandLine(pid) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) return null;
const now = Date.now();
// prune old entries as you go
for (const [cachedPid, cached] of this.windowsProcessCommandLineCache.entries()) {
if (!cached || now - cached.timestampMs > WINDOWS_PROCESS_COMMAND_LINE_CACHE_TTL_MS) {
this.windowsProcessCommandLineCache.delete(cachedPid);
}
}
const cached = this.windowsProcessCommandLineCache.get(numericPid);
if (cached) return cached.commandLine;
const shells = ['powershell', 'pwsh'];
for (const shellName of shells) {
let result;
try {
result = this.queryWindowsProcessCommandLine(shellName, numericPid);
} catch (error) {
// existing logging
continue;
}
// existing error/timeout handling...
if (result.status === 0) {
const commandLine = this.parseWindowsProcessCommandLine(result);
this.windowsProcessCommandLineCache.set(numericPid, {
commandLine,
timestampMs: now,
});
return commandLine;
}
}
this.windowsProcessCommandLineCache.set(numericPid, {
commandLine: null,
timestampMs: now,
});
return null;
}
```
With this, you can drop:
- `queryWindowsProcessCommandLineByPowerShell`
- `queryWindowsProcessCommandLineByPwsh`
- `pruneWindowsProcessCommandLineCache`
- the explicit `this.pruneWindowsProcessCommandLineCache()` call in `stopUnmanagedBackendByPort`.
`parseWindowsProcessCommandLine` can also be made a small inner function in `getWindowsProcessCommandLine` if it’s not reused elsewhere.
### 3. Windows unmanaged backend matcher: inline the matcher object
`buildWindowsUnmanagedBackendMatcher` + `shouldKillUnmanagedBackendProcess` introduces an extra struct and flag that mostly serves a single call site. You can fold them into one clearly named helper, keeping behavior the same:
```js
isLikelyBackendProcess(pid, processInfo, backendConfig) {
const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
if (!this.isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
const args = Array.isArray(backendConfig.args) ? backendConfig.args : [];
const primaryArg = args[0];
if (typeof primaryArg !== 'string' || !primaryArg) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`,
);
return false;
}
const resolvedPrimaryArg = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(backendConfig.cwd || process.cwd(), primaryArg);
const markers = [
this.normalizeWindowsPathForMatch(resolvedPrimaryArg),
this.normalizeWindowsPathForMatch(path.basename(primaryArg)),
];
const commandLine = this.getWindowsProcessCommandLine(pid);
if (!commandLine) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`,
);
return false;
}
const normalizedCommandLine = this.normalizeWindowsPathForMatch(commandLine);
const matched = markers.some(
(marker) => marker && normalizedCommandLine.includes(marker),
);
if (!matched) {
this.log(
'Skip unmanaged cleanup for pid=' +
pid +
': command line does not match AstrBot backend launch marker.',
);
return false;
}
return true;
}
```
Then `stopUnmanagedBackendByPort` becomes:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') {
return false;
}
const port = this.getBackendPort();
if (!port) return false;
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) return false;
this.log(
`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`,
);
const backendConfig = this.getBackendConfig();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`,
);
continue;
}
if (!this.isLikelyBackendProcess(pid, processInfo, backendConfig)) {
continue;
}
// existing taskkill call...
}
}
```
This keeps the safety semantics (image check + generic-python command‑line markers) but removes the extra `processMatcher` layer and boolean flag, making the flow easier to follow from `stopUnmanagedBackendByPort` down.
</issue_to_address>
### Comment 3
<location> `desktop/scripts/build-backend.mjs:14` </location>
<code_context>
+const manifestPath = path.join(outputDir, 'runtime-manifest.json');
+const launcherPath = path.join(outputDir, 'launch_backend.py');
+
+const runtimeSource =
+ process.env.ASTRBOT_DESKTOP_CPYTHON_HOME ||
+ process.env.ASTRBOT_DESKTOP_BACKEND_RUNTIME;
</code_context>
<issue_to_address>
**issue (complexity):** Consider centralizing error handling and organizing the script around a small `main()`-style orchestration with cohesive helpers to make the control flow clearer and less repetitive.
You can reduce complexity without changing behavior by (1) consolidating error handling and (2) grouping the high‑level flow into a `main()` plus a couple of cohesive helpers.
### 1. Centralize error handling
Right now many helpers call `console.error(...); process.exit(1);`, which scatters control flow. Instead, throw `Error` from helpers and handle `process.exit` in one place:
```js
const fail = (message) => new Error(message);
const parseExpectedRuntimeVersion = (rawVersion, sourceName) => {
const match = /^(\d+)\.(\d+)$/.exec(String(rawVersion).trim());
if (!match) {
throw fail(
`Invalid expected Python version from ${sourceName}: ${rawVersion}. ` +
'Expected format <major>.<minor>.'
);
}
return {
major: Number.parseInt(match[1], 10),
minor: Number.parseInt(match[2], 10),
};
};
// example: replace process.exit in one of the callers
const readProjectRequiresPythonLowerBound = () => {
const pyprojectPath = path.join(rootDir, 'pyproject.toml');
if (!fs.existsSync(pyprojectPath)) {
return null;
}
const content = fs.readFileSync(pyprojectPath, 'utf8');
const requiresPythonMatch = /^\s*requires-python\s*=\s*"([^"]+)"/m.exec(content);
if (!requiresPythonMatch) {
return null;
}
const lowerBoundMatch = />=\s*(\d+)\.(\d+)/.exec(requiresPythonMatch[1]);
if (!lowerBoundMatch) {
return null;
}
return `${lowerBoundMatch[1]}.${lowerBoundMatch[2]}`;
};
```
Top-level:
```js
const main = () => {
const runtimeSourceReal = resolveAndValidateRuntimeSource();
const expectedRuntimeVersion = resolveExpectedRuntimeVersion();
prepareBackendAppTree();
const runtimePython = prepareRuntimeTree(runtimeSourceReal, expectedRuntimeVersion);
writeLauncherAndManifest(runtimePython);
console.log(`Prepared CPython backend runtime in ${outputDir}`);
};
main();
```
with a single exit point:
```js
try {
main();
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
```
This keeps error behavior the same (message + exit 1) but removes repeated `process.exit` and makes control flow clearer.
### 2. Group high‑level responsibilities
You already have good low‑level helpers (`copyTree`, `resolveRuntimePython`, etc.), but the script still reads as a long, linear sequence. You can wrap existing logic into a few cohesive helpers without changing what they do.
For example:
```js
const resolveAndValidateRuntimeSource = () => {
const runtimeSource =
process.env.ASTRBOT_DESKTOP_CPYTHON_HOME ||
process.env.ASTRBOT_DESKTOP_BACKEND_RUNTIME;
if (!runtimeSource) {
throw fail(
'Missing CPython runtime source. Set ASTRBOT_DESKTOP_CPYTHON_HOME ' +
'(recommended) or ASTRBOT_DESKTOP_BACKEND_RUNTIME.'
);
}
const runtimeSourceReal = path.resolve(rootDir, runtimeSource);
if (!fs.existsSync(runtimeSourceReal)) {
throw fail(`CPython runtime source does not exist: ${runtimeSourceReal}`);
}
assertNoOverlap(runtimeSourceReal, outputDir);
return runtimeSourceReal;
};
const prepareBackendAppTree = () => {
fs.rmSync(outputDir, { recursive: true, force: true });
fs.mkdirSync(outputDir, { recursive: true });
fs.mkdirSync(appDir, { recursive: true });
for (const [srcRelative, destRelative] of sourceEntries) {
const sourcePath = path.join(rootDir, srcRelative);
const targetPath = path.join(appDir, destRelative);
if (!fs.existsSync(sourcePath)) {
throw fail(`Backend source path does not exist: ${sourcePath}`);
}
copyTree(sourcePath, targetPath);
}
};
```
And encapsulate the runtime copy + validation:
```js
const prepareRuntimeTree = (runtimeSourceReal, expectedRuntimeVersion) => {
const sourceRuntimePython = resolveRuntimePython(runtimeSourceReal);
if (!sourceRuntimePython) {
throw fail(
`Cannot find Python executable in runtime source: ${runtimeSourceReal}. ` +
'Expected python under bin/ or Scripts/.'
);
}
validateRuntimePython(sourceRuntimePython.absolute, expectedRuntimeVersion);
copyTree(runtimeSourceReal, runtimeDir, { dereference: true });
const runtimePython = resolveRuntimePython(runtimeDir);
if (!runtimePython) {
throw fail(
`Cannot find Python executable in runtime: ${runtimeDir}. ` +
'Expected python under bin/ or Scripts/.'
);
}
return runtimePython;
};
```
### 3. Simplify the overlap check
`normalizePathForCompare` and `isSameOrSubPath` are only used once; wrapping the whole concern in a single helper makes the intent clearer:
```js
const assertNoOverlap = (runtimeSourceReal, outputDir) => {
const normalize = (p) => {
const resolved = path.resolve(p).replace(/[\\/]+$/, '');
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
};
const sourceNorm = normalize(runtimeSourceReal);
const outNorm = normalize(outputDir);
const isSub = (a, b) => a === b || a.startsWith(`${b}${path.sep}`);
if (isSub(sourceNorm, outNorm) || isSub(outNorm, sourceNorm)) {
throw fail(
`CPython runtime source overlaps with backend output directory. ` +
`runtime=${runtimeSourceReal}, output=${outputDir}. ` +
'Please set ASTRBOT_DESKTOP_CPYTHON_HOME to a separate runtime directory.'
);
}
};
```
This keeps all functionality but reduces the number of top‑level “utility” concepts and makes the top-level flow (`main`) much easier to read.
</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
BackendManager.stopUnmanagedBackendByPort,buildWindowsUnmanagedBackendMatcheris called unconditionally withthis.getBackendConfig(), which can benullif the backend was never started in the current session; consider guarding for a missing config and falling back to a simpler image-name-based match to avoid runtime errors during cleanup. - The
readProjectRequiresPythonLowerBoundhelper inbuild-backend.mjsparsespyproject.tomlwith regex and assumes a simplerequires-pythonconstraint; using a TOML parser (or at least handling more complex specifiers/sections) would make version detection more robust across differentpyprojectlayouts.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `BackendManager.stopUnmanagedBackendByPort`, `buildWindowsUnmanagedBackendMatcher` is called unconditionally with `this.getBackendConfig()`, which can be `null` if the backend was never started in the current session; consider guarding for a missing config and falling back to a simpler image-name-based match to avoid runtime errors during cleanup.
- The `readProjectRequiresPythonLowerBound` helper in `build-backend.mjs` parses `pyproject.toml` with regex and assumes a simple `requires-python` constraint; using a TOML parser (or at least handling more complex specifiers/sections) would make version detection more robust across different `pyproject` layouts.
## Individual Comments
### Comment 1
<location> `astrbot/core/utils/pip_installer.py:442-443` </location>
<code_context>
+ wheel_only: bool = False,
) -> None:
args = ["install"]
+ pip_install_args = (
+ shlex.split(self.pip_install_arg) if self.pip_install_arg else []
+ )
requested_requirements: set[str] = set()
</code_context>
<issue_to_address>
**question (bug_risk):** Consider whether changing from str.split to shlex.split for pip_install_arg might alter behavior for existing configurations.
shlex.split improves handling of quoted arguments but does change behavior versus .split(). Configs that depended on literal spaces or unusual quoting in pip_install_arg may now be parsed differently or even error. If backward compatibility is important, consider gating this behind a feature flag or catching shlex errors and falling back to .split(), with a warning.
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:136` </location>
<code_context>
- getPackagedBackendPath() {
- if (!this.app.isPackaged) {
+ getPackagedBackendDir() {
+ const packagedBackendConfig = this.loadPackagedBackendConfig();
+ return packagedBackendConfig ? packagedBackendConfig.backendDir : null;
</code_context>
<issue_to_address>
**issue (complexity):** Consider inlining the small helper methods and collapsing duplicated logic to reduce indirection and make the backend config and Windows process-handling code easier to follow.
You can trim a fair bit of indirection here without changing behavior.
### 1. Packaged backend config: collapse trivial getters / helpers
You already have `loadPackagedBackendConfig` as a single source of truth; the tiny getters are just pass‑throughs and increase call depth.
Instead of:
```js
getPackagedBackendAppDir() {
const packagedBackendConfig = this.loadPackagedBackendConfig();
return packagedBackendConfig ? packagedBackendConfig.appDir : null;
}
resolveBackendCwd() {
if (!this.app.isPackaged) {
return path.resolve(this.baseDir, '..');
}
return this.getPackagedBackendAppDir() || this.resolveBackendRoot();
}
```
use the config object directly:
```js
resolveBackendCwd() {
if (!this.app.isPackaged) {
return path.resolve(this.baseDir, '..');
}
const cfg = this.loadPackagedBackendConfig();
return (cfg && cfg.appDir) || this.resolveBackendRoot();
}
```
and similarly in `buildPackagedBackendLaunch`:
```js
buildPackagedBackendLaunch(webuiDir) {
const cfg = this.loadPackagedBackendConfig();
if (!cfg || !cfg.runtimePythonPath || !cfg.launchScriptPath) {
return null;
}
const args = [cfg.launchScriptPath];
if (webuiDir) {
args.push('--webui-dir', webuiDir);
}
return {
cmd: cfg.runtimePythonPath,
args,
shell: false,
};
}
```
Then you can remove:
- `getPackagedBackendDir`
- `getPackagedBackendManifest`
- `getPackagedBackendAppDir`
- `getPackagedBackendLaunchScriptPath`
- `getPackagedRuntimePythonPath`
If `parsePackagedBackendManifest` / `resolveManifestPath` are only used in `loadPackagedBackendConfig`, consider inlining them as inner functions to keep the public surface smaller:
```js
loadPackagedBackendConfig() {
if (!this.app.isPackaged) return null;
if (this.packagedBackendConfig) return this.packagedBackendConfig;
const backendDir = path.join(process.resourcesPath, 'backend');
if (!fs.existsSync(backendDir)) return null;
const parseManifest = (dir) => { /* current body */ };
const resolveManifestPath = (dir, manifest, key, def) => { /* current body */ };
const manifest = parseManifest(backendDir);
const manifestForPathResolve = manifest || {};
// ... build frozen config as you already do ...
}
```
### 2. Windows process command line: unify PowerShell helpers and cache pruning
The two query functions are identical except for the executable name; you can replace both with one parametrized helper:
```js
queryWindowsProcessCommandLine(shellName, pid) {
const query =
`$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}";` +
' if ($null -ne $p) { $p.CommandLine }';
return spawnSync(
shellName,
['-NoProfile', '-NonInteractive', '-Command', query],
{
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
windowsHide: true,
timeout: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
},
);
}
```
and simplify `getWindowsProcessCommandLine`:
```js
getWindowsProcessCommandLine(pid) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) return null;
const now = Date.now();
// prune old entries as you go
for (const [cachedPid, cached] of this.windowsProcessCommandLineCache.entries()) {
if (!cached || now - cached.timestampMs > WINDOWS_PROCESS_COMMAND_LINE_CACHE_TTL_MS) {
this.windowsProcessCommandLineCache.delete(cachedPid);
}
}
const cached = this.windowsProcessCommandLineCache.get(numericPid);
if (cached) return cached.commandLine;
const shells = ['powershell', 'pwsh'];
for (const shellName of shells) {
let result;
try {
result = this.queryWindowsProcessCommandLine(shellName, numericPid);
} catch (error) {
// existing logging
continue;
}
// existing error/timeout handling...
if (result.status === 0) {
const commandLine = this.parseWindowsProcessCommandLine(result);
this.windowsProcessCommandLineCache.set(numericPid, {
commandLine,
timestampMs: now,
});
return commandLine;
}
}
this.windowsProcessCommandLineCache.set(numericPid, {
commandLine: null,
timestampMs: now,
});
return null;
}
```
With this, you can drop:
- `queryWindowsProcessCommandLineByPowerShell`
- `queryWindowsProcessCommandLineByPwsh`
- `pruneWindowsProcessCommandLineCache`
- the explicit `this.pruneWindowsProcessCommandLineCache()` call in `stopUnmanagedBackendByPort`.
`parseWindowsProcessCommandLine` can also be made a small inner function in `getWindowsProcessCommandLine` if it’s not reused elsewhere.
### 3. Windows unmanaged backend matcher: inline the matcher object
`buildWindowsUnmanagedBackendMatcher` + `shouldKillUnmanagedBackendProcess` introduces an extra struct and flag that mostly serves a single call site. You can fold them into one clearly named helper, keeping behavior the same:
```js
isLikelyBackendProcess(pid, processInfo, backendConfig) {
const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
if (!this.isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
const args = Array.isArray(backendConfig.args) ? backendConfig.args : [];
const primaryArg = args[0];
if (typeof primaryArg !== 'string' || !primaryArg) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`,
);
return false;
}
const resolvedPrimaryArg = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(backendConfig.cwd || process.cwd(), primaryArg);
const markers = [
this.normalizeWindowsPathForMatch(resolvedPrimaryArg),
this.normalizeWindowsPathForMatch(path.basename(primaryArg)),
];
const commandLine = this.getWindowsProcessCommandLine(pid);
if (!commandLine) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`,
);
return false;
}
const normalizedCommandLine = this.normalizeWindowsPathForMatch(commandLine);
const matched = markers.some(
(marker) => marker && normalizedCommandLine.includes(marker),
);
if (!matched) {
this.log(
'Skip unmanaged cleanup for pid=' +
pid +
': command line does not match AstrBot backend launch marker.',
);
return false;
}
return true;
}
```
Then `stopUnmanagedBackendByPort` becomes:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') {
return false;
}
const port = this.getBackendPort();
if (!port) return false;
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) return false;
this.log(
`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`,
);
const backendConfig = this.getBackendConfig();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`,
);
continue;
}
if (!this.isLikelyBackendProcess(pid, processInfo, backendConfig)) {
continue;
}
// existing taskkill call...
}
}
```
This keeps the safety semantics (image check + generic-python command‑line markers) but removes the extra `processMatcher` layer and boolean flag, making the flow easier to follow from `stopUnmanagedBackendByPort` down.
</issue_to_address>
### Comment 3
<location> `desktop/scripts/build-backend.mjs:14` </location>
<code_context>
+const manifestPath = path.join(outputDir, 'runtime-manifest.json');
+const launcherPath = path.join(outputDir, 'launch_backend.py');
+
+const runtimeSource =
+ process.env.ASTRBOT_DESKTOP_CPYTHON_HOME ||
+ process.env.ASTRBOT_DESKTOP_BACKEND_RUNTIME;
</code_context>
<issue_to_address>
**issue (complexity):** Consider centralizing error handling and organizing the script around a small `main()`-style orchestration with cohesive helpers to make the control flow clearer and less repetitive.
You can reduce complexity without changing behavior by (1) consolidating error handling and (2) grouping the high‑level flow into a `main()` plus a couple of cohesive helpers.
### 1. Centralize error handling
Right now many helpers call `console.error(...); process.exit(1);`, which scatters control flow. Instead, throw `Error` from helpers and handle `process.exit` in one place:
```js
const fail = (message) => new Error(message);
const parseExpectedRuntimeVersion = (rawVersion, sourceName) => {
const match = /^(\d+)\.(\d+)$/.exec(String(rawVersion).trim());
if (!match) {
throw fail(
`Invalid expected Python version from ${sourceName}: ${rawVersion}. ` +
'Expected format <major>.<minor>.'
);
}
return {
major: Number.parseInt(match[1], 10),
minor: Number.parseInt(match[2], 10),
};
};
// example: replace process.exit in one of the callers
const readProjectRequiresPythonLowerBound = () => {
const pyprojectPath = path.join(rootDir, 'pyproject.toml');
if (!fs.existsSync(pyprojectPath)) {
return null;
}
const content = fs.readFileSync(pyprojectPath, 'utf8');
const requiresPythonMatch = /^\s*requires-python\s*=\s*"([^"]+)"/m.exec(content);
if (!requiresPythonMatch) {
return null;
}
const lowerBoundMatch = />=\s*(\d+)\.(\d+)/.exec(requiresPythonMatch[1]);
if (!lowerBoundMatch) {
return null;
}
return `${lowerBoundMatch[1]}.${lowerBoundMatch[2]}`;
};
```
Top-level:
```js
const main = () => {
const runtimeSourceReal = resolveAndValidateRuntimeSource();
const expectedRuntimeVersion = resolveExpectedRuntimeVersion();
prepareBackendAppTree();
const runtimePython = prepareRuntimeTree(runtimeSourceReal, expectedRuntimeVersion);
writeLauncherAndManifest(runtimePython);
console.log(`Prepared CPython backend runtime in ${outputDir}`);
};
main();
```
with a single exit point:
```js
try {
main();
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
```
This keeps error behavior the same (message + exit 1) but removes repeated `process.exit` and makes control flow clearer.
### 2. Group high‑level responsibilities
You already have good low‑level helpers (`copyTree`, `resolveRuntimePython`, etc.), but the script still reads as a long, linear sequence. You can wrap existing logic into a few cohesive helpers without changing what they do.
For example:
```js
const resolveAndValidateRuntimeSource = () => {
const runtimeSource =
process.env.ASTRBOT_DESKTOP_CPYTHON_HOME ||
process.env.ASTRBOT_DESKTOP_BACKEND_RUNTIME;
if (!runtimeSource) {
throw fail(
'Missing CPython runtime source. Set ASTRBOT_DESKTOP_CPYTHON_HOME ' +
'(recommended) or ASTRBOT_DESKTOP_BACKEND_RUNTIME.'
);
}
const runtimeSourceReal = path.resolve(rootDir, runtimeSource);
if (!fs.existsSync(runtimeSourceReal)) {
throw fail(`CPython runtime source does not exist: ${runtimeSourceReal}`);
}
assertNoOverlap(runtimeSourceReal, outputDir);
return runtimeSourceReal;
};
const prepareBackendAppTree = () => {
fs.rmSync(outputDir, { recursive: true, force: true });
fs.mkdirSync(outputDir, { recursive: true });
fs.mkdirSync(appDir, { recursive: true });
for (const [srcRelative, destRelative] of sourceEntries) {
const sourcePath = path.join(rootDir, srcRelative);
const targetPath = path.join(appDir, destRelative);
if (!fs.existsSync(sourcePath)) {
throw fail(`Backend source path does not exist: ${sourcePath}`);
}
copyTree(sourcePath, targetPath);
}
};
```
And encapsulate the runtime copy + validation:
```js
const prepareRuntimeTree = (runtimeSourceReal, expectedRuntimeVersion) => {
const sourceRuntimePython = resolveRuntimePython(runtimeSourceReal);
if (!sourceRuntimePython) {
throw fail(
`Cannot find Python executable in runtime source: ${runtimeSourceReal}. ` +
'Expected python under bin/ or Scripts/.'
);
}
validateRuntimePython(sourceRuntimePython.absolute, expectedRuntimeVersion);
copyTree(runtimeSourceReal, runtimeDir, { dereference: true });
const runtimePython = resolveRuntimePython(runtimeDir);
if (!runtimePython) {
throw fail(
`Cannot find Python executable in runtime: ${runtimeDir}. ` +
'Expected python under bin/ or Scripts/.'
);
}
return runtimePython;
};
```
### 3. Simplify the overlap check
`normalizePathForCompare` and `isSameOrSubPath` are only used once; wrapping the whole concern in a single helper makes the intent clearer:
```js
const assertNoOverlap = (runtimeSourceReal, outputDir) => {
const normalize = (p) => {
const resolved = path.resolve(p).replace(/[\\/]+$/, '');
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
};
const sourceNorm = normalize(runtimeSourceReal);
const outNorm = normalize(outputDir);
const isSub = (a, b) => a === b || a.startsWith(`${b}${path.sep}`);
if (isSub(sourceNorm, outNorm) || isSub(outNorm, sourceNorm)) {
throw fail(
`CPython runtime source overlaps with backend output directory. ` +
`runtime=${runtimeSourceReal}, output=${outputDir}. ` +
'Please set ASTRBOT_DESKTOP_CPYTHON_HOME to a separate runtime directory.'
);
}
};
```
This keeps all functionality but reduces the number of top‑level “utility” concepts and makes the top-level flow (`main`) much easier to read.
</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.There was a problem hiding this comment.
你好——我这边发现了 3 个问题,并留下了一些高层面的反馈:
- 在
desktop/scripts/build-backend.mjs中,即使是从requires-python >=x.y推导出来的预期 Python 运行时版本,当前也被强制要求精确匹配<major>.<minor>。建议将其放宽为接受>=下限的运行时版本(同时仍然拒绝不支持的主版本),这样就不会因为较新的、实际上可用的次版本导致构建不必要地失败。 - 目前在
build-backend.mjs中对pyproject.toml的解析是通过对原始文本使用正则实现的;如果改用 TOML 解析器(例如通过一个短小的辅助脚本调用tomllib,或使用 Node 的 TOML 库),会让requires-python的检测对格式变化和新增元数据更具鲁棒性。 - Windows 上的非托管后端清理现在会针对每个 PID 同步运行 PowerShell/Pwsh 命令以查询命令行;如果存在大量 PID,可能会让 Electron 主进程在可感知的时间内卡住,因此建议在候选过多时短路跳出,或者将这部分逻辑移动到单独的进程/worker 中。
面向 AI 代理的提示词
Please address the comments from this code review:
## Overall Comments
- In `desktop/scripts/build-backend.mjs`, the expected runtime Python version is enforced as an exact `<major>.<minor>` match even when derived from `requires-python >=x.y`; consider relaxing this to accept runtimes `>=` the lower bound (while still rejecting unsupported majors) so that a valid newer minor version doesn't cause the build to fail unnecessarily.
- The current `pyproject.toml` parsing in `build-backend.mjs` uses regex on raw text; using a TOML parser (e.g., `tomllib` via a short helper script or a Node TOML library) would make `requires-python` detection more robust to formatting changes and additional metadata.
- The Windows unmanaged backend cleanup now runs synchronous PowerShell/Pwsh commands per PID to query command lines; if many PIDs are present this could freeze the Electron main process for noticeable time, so consider short‑circuiting when there are many candidates or moving this logic to a separate process/worker.
## Individual Comments
### Comment 1
<location> `desktop/README.md:84` </location>
<code_context>
+ app/ # AstrBot backend source snapshot used in packaged mode
+ python/ # Bundled CPython runtime directory
+ launch_backend.py # Launcher executed by Electron
+ runtime-manifest.json # Runtime metadata (python path, entrypoint, app path)
+```
+
</code_context>
<issue_to_address>
**nitpick (typo):** Consider capitalizing "Python" in the inline comment for consistency.
To match the rest of the documentation (e.g., CPython, runtime Python), please change "python path" to "Python path" in this comment.
```suggestion
runtime-manifest.json # Runtime metadata (Python path, entrypoint, app path)
```
</issue_to_address>
### Comment 2
<location> `desktop/README.md:87` </location>
<code_context>
+ runtime-manifest.json # Runtime metadata (python path, entrypoint, app path)
+```
+
+Electron reads `runtime-manifest.json` and starts backend with:
+- `python` from `python/`
+- `launch_backend.py` as entrypoint
</code_context>
<issue_to_address>
**nitpick (typo):** Minor grammar tweak: add "the" before "backend".
Suggested wording: "Electron reads `runtime-manifest.json` and starts the backend with:"
```suggestion
Electron reads `runtime-manifest.json` and starts the backend with:
```
</issue_to_address>
### Comment 3
<location> `desktop/lib/backend-manager.js:742` </location>
<code_context>
+ );
+ }
+
+ queryWindowsProcessCommandLineByPowerShell(pid) {
+ const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($null -ne $p) { $p.CommandLine }`;
+ return spawnSync(
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the new Windows process-handling helpers (PowerShell querying, command-line cache, and matcher) to reduce duplication and indirection while preserving behavior.
You can trim a fair bit of structural complexity without changing behavior by:
---
### 1. Unifying PowerShell / pwsh command‑line querying
`queryWindowsProcessCommandLineByPowerShell` and `queryWindowsProcessCommandLineByPwsh` are identical except for the binary name. You can collapse them into a single helper and simplify `getWindowsProcessCommandLine`’s `queryAttempts`:
```js
queryWindowsProcessCommandLine(shellName, pid) {
const query =
`$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; ` +
'if ($null -ne $p) { $p.CommandLine }';
return spawnSync(
shellName,
['-NoProfile', '-NonInteractive', '-Command', query],
{
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
windowsHide: true,
timeout: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
},
);
}
```
Then:
```js
const queryAttempts = [
{ shellName: 'powershell' },
{ shellName: 'pwsh' },
];
for (const { shellName } of queryAttempts) {
let result;
try {
result = this.queryWindowsProcessCommandLine(shellName, numericPid);
} catch (error) {
if (error instanceof Error && error.message) {
this.log(
`Failed to query process command line by ${shellName} for pid=${numericPid}: ${error.message}`,
);
}
continue;
}
// ... existing ENOENT / ETIMEDOUT / status handling ...
}
```
This keeps all current behavior while reducing duplication and the surface area you need to touch for future changes (e.g., timeouts or flags).
---
### 2. Simplifying the command‑line cache
Right now you have:
- TTL on each entry,
- explicit pruning on every call,
- and negative caching (`commandLine: null`).
Given this is only used during unmanaged cleanup, you can simplify the cache without changing externally observable behavior:
- Drop `pruneWindowsProcessCommandLineCache` and the per‑call prune.
- Cache only successful lookups.
- Treat failures as “no cache entry” so they can be retried if needed.
Example:
```js
getWindowsProcessCommandLine(pid) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) {
return null;
}
const cached = this.windowsProcessCommandLineCache.get(numericPid);
if (cached) {
return cached; // just the command line string
}
const queryAttempts = [
{ shellName: 'powershell' },
{ shellName: 'pwsh' },
];
for (const { shellName } of queryAttempts) {
let result;
try {
result = this.queryWindowsProcessCommandLine(shellName, numericPid);
} catch (error) {
if (error instanceof Error && error.message) {
this.log(
`Failed to query process command line by ${shellName} for pid=${numericPid}: ${error.message}`,
);
}
continue;
}
if (result.error && result.error.code === 'ENOENT') {
continue;
}
if (result.error && result.error.code === 'ETIMEDOUT') {
this.log(
`Timed out (${WINDOWS_PROCESS_QUERY_TIMEOUT_MS}ms) querying process command line by ${shellName} for pid=${numericPid}.`,
);
continue;
}
if (result.status === 0) {
const commandLine = this.parseWindowsProcessCommandLine(result);
if (commandLine) {
this.windowsProcessCommandLineCache.set(numericPid, commandLine);
}
return commandLine;
}
}
return null;
}
```
This removes timestamp/TTL bookkeeping and negative entries while preserving semantics: callers still get `null` for “cannot resolve command line,” but the internal state is much easier to reason about.
---
### 3. Inlining the matcher to avoid an extra abstraction layer (optional)
If you want to further reduce indirection, you can compute the matcher fields directly inside `shouldKillUnmanagedBackendProcess` and pass `backendConfig` instead of a separate matcher object.
For example, instead of:
```js
const backendConfig = this.getBackendConfig();
const processMatcher = this.buildWindowsUnmanagedBackendMatcher(backendConfig);
if (!this.shouldKillUnmanagedBackendProcess(pid, processInfo, processMatcher)) {
continue;
}
```
You can make `shouldKillUnmanagedBackendProcess` accept `backendConfig` and build what it needs inside:
```js
shouldKillUnmanagedBackendProcess(pid, processInfo, backendConfig) {
const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
const requireStrictCommandLineCheck =
this.isGenericWindowsPythonImage(expectedImageName);
if (!requireStrictCommandLineCheck) {
return true;
}
// ...build markers from backendConfig.args here and run the existing checks...
}
```
This keeps behavior intact but removes the extra “matcher” type and its construction step, making the control flow easier to follow from `stopUnmanagedBackendByPort` downwards.
</issue_to_address>帮我变得更加有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进之后的 Review。
Original comment in English
Hey - I've found 3 issues, and left some high level feedback:
- In
desktop/scripts/build-backend.mjs, the expected runtime Python version is enforced as an exact<major>.<minor>match even when derived fromrequires-python >=x.y; consider relaxing this to accept runtimes>=the lower bound (while still rejecting unsupported majors) so that a valid newer minor version doesn't cause the build to fail unnecessarily. - The current
pyproject.tomlparsing inbuild-backend.mjsuses regex on raw text; using a TOML parser (e.g.,tomllibvia a short helper script or a Node TOML library) would makerequires-pythondetection more robust to formatting changes and additional metadata. - The Windows unmanaged backend cleanup now runs synchronous PowerShell/Pwsh commands per PID to query command lines; if many PIDs are present this could freeze the Electron main process for noticeable time, so consider short‑circuiting when there are many candidates or moving this logic to a separate process/worker.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `desktop/scripts/build-backend.mjs`, the expected runtime Python version is enforced as an exact `<major>.<minor>` match even when derived from `requires-python >=x.y`; consider relaxing this to accept runtimes `>=` the lower bound (while still rejecting unsupported majors) so that a valid newer minor version doesn't cause the build to fail unnecessarily.
- The current `pyproject.toml` parsing in `build-backend.mjs` uses regex on raw text; using a TOML parser (e.g., `tomllib` via a short helper script or a Node TOML library) would make `requires-python` detection more robust to formatting changes and additional metadata.
- The Windows unmanaged backend cleanup now runs synchronous PowerShell/Pwsh commands per PID to query command lines; if many PIDs are present this could freeze the Electron main process for noticeable time, so consider short‑circuiting when there are many candidates or moving this logic to a separate process/worker.
## Individual Comments
### Comment 1
<location> `desktop/README.md:84` </location>
<code_context>
+ app/ # AstrBot backend source snapshot used in packaged mode
+ python/ # Bundled CPython runtime directory
+ launch_backend.py # Launcher executed by Electron
+ runtime-manifest.json # Runtime metadata (python path, entrypoint, app path)
+```
+
</code_context>
<issue_to_address>
**nitpick (typo):** Consider capitalizing "Python" in the inline comment for consistency.
To match the rest of the documentation (e.g., CPython, runtime Python), please change "python path" to "Python path" in this comment.
```suggestion
runtime-manifest.json # Runtime metadata (Python path, entrypoint, app path)
```
</issue_to_address>
### Comment 2
<location> `desktop/README.md:87` </location>
<code_context>
+ runtime-manifest.json # Runtime metadata (python path, entrypoint, app path)
+```
+
+Electron reads `runtime-manifest.json` and starts backend with:
+- `python` from `python/`
+- `launch_backend.py` as entrypoint
</code_context>
<issue_to_address>
**nitpick (typo):** Minor grammar tweak: add "the" before "backend".
Suggested wording: "Electron reads `runtime-manifest.json` and starts the backend with:"
```suggestion
Electron reads `runtime-manifest.json` and starts the backend with:
```
</issue_to_address>
### Comment 3
<location> `desktop/lib/backend-manager.js:742` </location>
<code_context>
+ );
+ }
+
+ queryWindowsProcessCommandLineByPowerShell(pid) {
+ const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($null -ne $p) { $p.CommandLine }`;
+ return spawnSync(
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the new Windows process-handling helpers (PowerShell querying, command-line cache, and matcher) to reduce duplication and indirection while preserving behavior.
You can trim a fair bit of structural complexity without changing behavior by:
---
### 1. Unifying PowerShell / pwsh command‑line querying
`queryWindowsProcessCommandLineByPowerShell` and `queryWindowsProcessCommandLineByPwsh` are identical except for the binary name. You can collapse them into a single helper and simplify `getWindowsProcessCommandLine`’s `queryAttempts`:
```js
queryWindowsProcessCommandLine(shellName, pid) {
const query =
`$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; ` +
'if ($null -ne $p) { $p.CommandLine }';
return spawnSync(
shellName,
['-NoProfile', '-NonInteractive', '-Command', query],
{
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
windowsHide: true,
timeout: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
},
);
}
```
Then:
```js
const queryAttempts = [
{ shellName: 'powershell' },
{ shellName: 'pwsh' },
];
for (const { shellName } of queryAttempts) {
let result;
try {
result = this.queryWindowsProcessCommandLine(shellName, numericPid);
} catch (error) {
if (error instanceof Error && error.message) {
this.log(
`Failed to query process command line by ${shellName} for pid=${numericPid}: ${error.message}`,
);
}
continue;
}
// ... existing ENOENT / ETIMEDOUT / status handling ...
}
```
This keeps all current behavior while reducing duplication and the surface area you need to touch for future changes (e.g., timeouts or flags).
---
### 2. Simplifying the command‑line cache
Right now you have:
- TTL on each entry,
- explicit pruning on every call,
- and negative caching (`commandLine: null`).
Given this is only used during unmanaged cleanup, you can simplify the cache without changing externally observable behavior:
- Drop `pruneWindowsProcessCommandLineCache` and the per‑call prune.
- Cache only successful lookups.
- Treat failures as “no cache entry” so they can be retried if needed.
Example:
```js
getWindowsProcessCommandLine(pid) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) {
return null;
}
const cached = this.windowsProcessCommandLineCache.get(numericPid);
if (cached) {
return cached; // just the command line string
}
const queryAttempts = [
{ shellName: 'powershell' },
{ shellName: 'pwsh' },
];
for (const { shellName } of queryAttempts) {
let result;
try {
result = this.queryWindowsProcessCommandLine(shellName, numericPid);
} catch (error) {
if (error instanceof Error && error.message) {
this.log(
`Failed to query process command line by ${shellName} for pid=${numericPid}: ${error.message}`,
);
}
continue;
}
if (result.error && result.error.code === 'ENOENT') {
continue;
}
if (result.error && result.error.code === 'ETIMEDOUT') {
this.log(
`Timed out (${WINDOWS_PROCESS_QUERY_TIMEOUT_MS}ms) querying process command line by ${shellName} for pid=${numericPid}.`,
);
continue;
}
if (result.status === 0) {
const commandLine = this.parseWindowsProcessCommandLine(result);
if (commandLine) {
this.windowsProcessCommandLineCache.set(numericPid, commandLine);
}
return commandLine;
}
}
return null;
}
```
This removes timestamp/TTL bookkeeping and negative entries while preserving semantics: callers still get `null` for “cannot resolve command line,” but the internal state is much easier to reason about.
---
### 3. Inlining the matcher to avoid an extra abstraction layer (optional)
If you want to further reduce indirection, you can compute the matcher fields directly inside `shouldKillUnmanagedBackendProcess` and pass `backendConfig` instead of a separate matcher object.
For example, instead of:
```js
const backendConfig = this.getBackendConfig();
const processMatcher = this.buildWindowsUnmanagedBackendMatcher(backendConfig);
if (!this.shouldKillUnmanagedBackendProcess(pid, processInfo, processMatcher)) {
continue;
}
```
You can make `shouldKillUnmanagedBackendProcess` accept `backendConfig` and build what it needs inside:
```js
shouldKillUnmanagedBackendProcess(pid, processInfo, backendConfig) {
const expectedImageName = path.basename(backendConfig.cmd || 'python.exe').toLowerCase();
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
const requireStrictCommandLineCheck =
this.isGenericWindowsPythonImage(expectedImageName);
if (!requireStrictCommandLineCheck) {
return true;
}
// ...build markers from backendConfig.args here and run the existing checks...
}
```
This keeps behavior intact but removes the extra “matcher” type and its construction step, making the control flow easier to follow from `stopUnmanagedBackendByPort` downwards.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Hey - 我发现了 3 个问题,并留下了一些整体性的反馈:
- 在
desktop/scripts/build-backend.mjs中,当前的运行时探测要求import pip成功;如果有人提供的是不带 pip 的精简 CPython 运行时,这里会直接硬失败,即使后端实际上仍然可以工作——建议只探测sys.version_info,或者把 pip 检查做成可选,并在失败时给出更清晰的错误路径。 - 在
PipInstaller.install中,当wheel_only=True时,你无条件添加了--only-binary :all:和--prefer-binary,但没有防护可能已经在pip_install_arg中出现的--no-binary之类的冲突参数;在构造最终参数列表前,对不兼容选项做归一化处理或直接拒绝会更安全。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- 在 `desktop/scripts/build-backend.mjs` 中,当前的运行时探测要求 `import pip` 成功;如果有人提供的是不带 pip 的精简 CPython 运行时,这里会直接硬失败,即使后端实际上仍然可以工作——建议只探测 `sys.version_info`,或者把 pip 检查做成可选,并在失败时给出更清晰的错误路径。
- 在 `PipInstaller.install` 中,当 `wheel_only=True` 时,你无条件添加了 `--only-binary :all:` 和 `--prefer-binary`,但没有防护可能已经在 `pip_install_arg` 中出现的 `--no-binary` 之类的冲突参数;在构造最终参数列表前,对不兼容选项做归一化处理或直接拒绝会更安全。
## Individual Comments
### Comment 1
<location> `desktop/scripts/build-backend.mjs:107-43` </location>
<code_context>
+const expectedRuntimeVersion = resolveExpectedRuntimeVersion();
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 当前的运行时 Python 版本检查比实际需要更严格,可能会拒绝兼容的更高版本。
这里 `resolveExpectedRuntimeVersion` 实际上被当成一个严格的 `major.minor` 固定版本来处理。如果它是从 `requires-python >= X.Y` 这样的约束推导出来,那就会错误地拒绝兼容的更高版本运行时(例如要求 `>=3.10` 时拒绝 3.11),从而导致构建失败。
为了与“最低版本”约束对齐,检查逻辑应该在 `major.minor` 上使用 `>=` 语义(只在运行时版本低于最低版本时失败)。你仍然可以在明确设置了 `ASTRBOT_DESKTOP_EXPECTED_PYTHON` 时保持严格相等检查,而对从 `pyproject.toml` 推导出的值只使用“下界”逻辑。
建议的实现方式:
```javascript
const { expectedRuntimeVersion, isLowerBoundRuntimeVersion } = resolveExpectedRuntimeVersion();
```
为了完成你描述的行为变更,你还需要同时调整 `resolveExpectedRuntimeVersion` 的实现以及运行时检查逻辑:
1. 更新 `resolveExpectedRuntimeVersion`,让它返回一个对象,例如:
```js
// 当来源于环境变量时:
return { expectedRuntimeVersion: parseExpectedRuntimeVersion(envValue, 'ASTRBOT_DESKTOP_EXPECTED_PYTHON'), isLowerBoundRuntimeVersion: false };
// 当来源于 pyproject requires-python >=X.Y 时:
return { expectedRuntimeVersion: parseExpectedRuntimeVersion(projectLowerBound, 'pyproject.toml requires-python'), isLowerBoundRuntimeVersion: true };
```
2. 找到当前校验 Python 运行时版本的地方(很可能类似 `if (runtimeVersion !== expectedRuntimeVersion) { ... }`),并将其修改为:
```js
if (isLowerBoundRuntimeVersion) {
// 仅当运行时版本 *低于* expectedRuntimeVersion 的 major.minor 时失败
if (compareMajorMinor(runtimeVersion, expectedRuntimeVersion) < 0) {
// fail
}
} else {
// 当通过环境变量显式设置时使用严格相等
if (runtimeVersion !== expectedRuntimeVersion) {
// fail
}
}
```
你需要一个类似 `compareMajorMinor(a, b)` 的辅助函数,用来把 `major.minor` 解析成整数并返回 -1/0/1。
3. 确保所有 `resolveExpectedRuntimeVersion` 的调用点都已经更新为使用新的 `{ expectedRuntimeVersion, isLowerBoundRuntimeVersion }` 结构。
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:742` </location>
<code_context>
+ );
+ }
+
+ queryWindowsProcessCommandLineByPowerShell(pid) {
+ const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($null -ne $p) { $p.CommandLine }`;
+ return spawnSync(
</code_context>
<issue_to_address>
**issue (complexity):** 建议重构新的 Windows 进程检查和清理逻辑,以消除重复、简化缓存,并将“是否终止”的决策收敛到单一辅助函数中,便于推理和维护。
你可以保留所有新的安全特性,同时在几个关键位置降低复杂度:
---
### 1. 合并两个 PowerShell 辅助函数
`queryWindowsProcessCommandLineByPowerShell` 和 `queryWindowsProcessCommandLineByPwsh` 除了可执行文件名不同外完全相同。你可以用一个单一的辅助函数替换这两个函数,并简化 `getWindowsProcessCommandLine`:
```js
// 用下面的函数替换 queryWindowsProcessCommandLineByPowerShell/Pwsh:
queryWindowsProcessCommandLine(shellName, pid) {
const query =
`$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; ` +
'if ($null -ne $p) { $p.CommandLine }';
return spawnSync(
shellName,
['-NoProfile', '-NonInteractive', '-Command', query],
{
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
windowsHide: true,
timeout: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
},
);
}
```
然后 `getWindowsProcessCommandLine` 可以重写为:
```js
const queryAttempts = ['powershell', 'pwsh'];
for (const shellName of queryAttempts) {
let result;
try {
result = this.queryWindowsProcessCommandLine(shellName, numericPid);
} catch (error) {
if (error instanceof Error && error.message) {
this.log(
`Failed to query process command line by ${shellName} for pid=${numericPid}: ${error.message}`,
);
}
continue;
}
if (result.error && result.error.code === 'ENOENT') continue;
if (result.error && result.error.code === 'ETIMEDOUT') {
this.log(
`Timed out (${WINDOWS_PROCESS_QUERY_TIMEOUT_MS}ms) querying process command line by ${shellName} for pid=${numericPid}.`,
);
continue;
}
if (result.status === 0) {
const commandLine = this.parseWindowsProcessCommandLine(result);
this.windowsProcessCommandLineCache.set(numericPid, {
commandLine,
timestampMs: Date.now(),
});
return commandLine;
}
}
```
---
### 2. 去掉缓存中的显式清理和时间戳
由于该缓存只在“非托管清理”路径中使用,你可以将其简化为一个普通的 `Map<pid, commandLine>`,并彻底移除 TTL/清理逻辑。这可以删除 `WINDOWS_PROCESS_COMMAND_LINE_CACHE_TTL_MS`、`pruneWindowsProcessCommandLineCache` 以及所有时间戳处理,而不会改变清理流程的可观察行为。
```js
// 构造函数中:
this.windowsProcessCommandLineCache = new Map(); // pid -> commandLine
// 完全删除 pruneWindowsProcessCommandLineCache 以及在 stopUnmanagedBackendByPort 中的调用
getWindowsProcessCommandLine(pid) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) {
return null;
}
if (this.windowsProcessCommandLineCache.has(numericPid)) {
return this.windowsProcessCommandLineCache.get(numericPid);
}
// ... 保持原有的查询逻辑 ...
if (result.status === 0) {
const commandLine = this.parseWindowsProcessCommandLine(result);
this.windowsProcessCommandLineCache.set(numericPid, commandLine);
return commandLine;
}
this.windowsProcessCommandLineCache.set(numericPid, null);
return null;
}
```
这样可以保留缓存语义(同一次清理运行中不会重复查询),但去掉生命周期管理和额外分支。
---
### 3. 将 matcher 对象内联为单一决策辅助函数
当前的 `buildWindowsUnmanagedBackendMatcher` 加上 `shouldKillUnmanagedBackendProcess` 可以合并为单一、便于审计的辅助函数,直接基于 `backendConfig` 进行操作。这样就可以移除中间的“matcher 对象”抽象以及额外的间接层。
```js
shouldKillUnmanagedBackendProcess(pid, processInfo, backendConfig) {
const expectedImageName = path
.basename(backendConfig.cmd || 'python.exe')
.toLowerCase();
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
if (!this.isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
const expectedCommandLineMarkers = [];
if (Array.isArray(backendConfig.args) && backendConfig.args.length > 0) {
const primaryArg = backendConfig.args[0];
if (typeof primaryArg === 'string' && primaryArg) {
const resolvedPrimaryArg = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(backendConfig.cwd || process.cwd(), primaryArg);
expectedCommandLineMarkers.push(
this.normalizeWindowsPathForMatch(resolvedPrimaryArg),
);
expectedCommandLineMarkers.push(
this.normalizeWindowsPathForMatch(path.basename(primaryArg)),
);
}
}
if (!expectedCommandLineMarkers.length) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`,
);
return false;
}
const commandLine = this.getWindowsProcessCommandLine(pid);
if (!commandLine) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`,
);
return false;
}
const normalizedCommandLine = this.normalizeWindowsPathForMatch(commandLine);
const markerMatched = expectedCommandLineMarkers.some(
(marker) => marker && normalizedCommandLine.includes(marker),
);
if (!markerMatched) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
return false;
}
return true;
}
```
这样 `stopUnmanagedBackendByPort` 也会更简单:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') {
return false;
}
const port = this.getBackendPort();
if (!port) {
return false;
}
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) {
return false;
}
this.log(
`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`,
);
const backendConfig = this.getBackendConfig();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`,
);
continue;
}
if (!this.shouldKillUnmanagedBackendProcess(pid, processInfo, backendConfig)) {
continue;
}
// taskkill 逻辑保持不变...
}
return true;
}
```
这样可以保留所有现有检查(镜像名匹配 + 可选的严格命令行标记),但把“是否终止”的决策集中在一个容易追踪的地方。
</issue_to_address>
### Comment 3
<location> `desktop/scripts/build-backend.mjs:14` </location>
<code_context>
+const manifestPath = path.join(outputDir, 'runtime-manifest.json');
+const launcherPath = path.join(outputDir, 'launch_backend.py');
+
+const runtimeSource =
+ process.env.ASTRBOT_DESKTOP_CPYTHON_HOME ||
+ process.env.ASTRBOT_DESKTOP_BACKEND_RUNTIME;
</code_context>
<issue_to_address>
**issue (complexity):** 建议重构这个脚本,将失败处理集中起来,简化路径/运行时相关的辅助函数,并稍微收紧过滤逻辑,使主构建流程更短、更易阅读,同时保持行为不变。
你可以在不删除任何现有行为的前提下明显降低复杂度:
---
### 1. 集中错误处理
目前你在很多辅助函数中重复使用 `console.error` + `process.exit(1)`。可以增加一个简单的 `fail` 辅助函数,保持行为一致的同时简化所有调用点:
```ts
// 在文件顶部附近
const fail = (message: string): never => {
console.error(message);
process.exit(1);
};
```
然后把类似下面的模式:
```ts
if (!runtimeSource) {
console.error(
'Missing CPython runtime source. Set ASTRBOT_DESKTOP_CPYTHON_HOME ' +
'(recommended) or ASTRBOT_DESKTOP_BACKEND_RUNTIME.',
);
process.exit(1);
}
```
替换为:
```ts
if (!runtimeSource) {
fail(
'Missing CPython runtime source. Set ASTRBOT_DESKTOP_CPYTHON_HOME ' +
'(recommended) or ASTRBOT_DESKTOP_BACKEND_RUNTIME.',
);
}
```
在 `parseExpectedRuntimeVersion`、`resolveExpectedRuntimeVersion`、`validateRuntimePython` 等函数中同样进行替换。光是这一步就能去掉大量分支噪音。
---
### 2. 简化路径重叠逻辑
你可以保留路径重叠保护,但用更直接的方式实现,不需要在多处散落单独的“normalize”辅助函数:
```ts
const normalize = (p: string) => {
const resolved = path.resolve(p).replace(/[\\/]+$/, '');
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
};
const pathsOverlap = (a: string, b: string) => {
const na = normalize(a);
const nb = normalize(b);
return na === nb || na.startsWith(nb + path.sep) || nb.startsWith(na + path.sep);
};
const runtimeSourceReal = path.resolve(rootDir, runtimeSource);
if (!fs.existsSync(runtimeSourceReal)) {
fail(`CPython runtime source does not exist: ${runtimeSourceReal}`);
}
if (pathsOverlap(runtimeSourceReal, outputDir)) {
fail(
`CPython runtime source overlaps with backend output directory. ` +
`runtime=${runtimeSourceReal}, output=${outputDir}. ` +
'Please set ASTRBOT_DESKTOP_CPYTHON_HOME to a separate runtime directory.',
);
}
```
这样可以保持跨平台行为和重叠检查,但主流程会更容易阅读。
---
### 3. 将运行时解析 + 校验合并为单一辅助函数
你现在是先调用 `resolveRuntimePython`,然后立刻对结果进行校验。把这两个步骤合并到一个函数中,可以更清晰地表达“查找 + 断言”的意图,并减少调用图深度:
```ts
const findAndValidateRuntimePython = (runtimeRoot: string) => {
const candidates =
process.platform === 'win32'
? ['python.exe', path.join('Scripts', 'python.exe')]
: [path.join('bin', 'python3'), path.join('bin', 'python')];
for (const rel of candidates) {
const abs = path.join(runtimeRoot, rel);
if (fs.existsSync(abs)) {
validateRuntimePython(abs);
return {
absolute: abs,
relative: path.relative(outputDir, abs),
};
}
}
fail(
`Cannot find Python executable in runtime: ${runtimeRoot}. ` +
'Expected python under bin/ or Scripts/.',
);
};
```
然后主流程可以写成:
```ts
const sourceRuntimePython = findAndValidateRuntimePython(runtimeSourceReal);
// ...
const runtimePython = findAndValidateRuntimePython(runtimeDir);
```
---
### 4. 简化 `validateRuntimePython` 的版本解析
你可以保留 `pip` 的导入(用来确认它存在),但去掉格式化字符串 + 正则匹配的往返,只打印两个数字再 `split` 即可:
```ts
const validateRuntimePython = (pythonExecutable: string) => {
const probe = spawnSync(
pythonExecutable,
[
'-c',
'import sys, pip; print(sys.version_info[0], sys.version_info[1])',
],
{
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
windowsHide: true,
timeout: 5000,
},
);
if (probe.error) {
const reason =
probe.error.code === 'ETIMEDOUT'
? 'runtime Python probe timed out'
: probe.error.message || String(probe.error);
fail(`Runtime Python probe failed: ${reason}`);
}
if (probe.status !== 0) {
const stderrText = (probe.stderr || '').trim();
fail(
`Runtime Python probe failed with exit code ${probe.status}.` +
(stderrText ? ` stderr: ${stderrText}` : ''),
);
}
const parts = (probe.stdout || '').trim().split(/\s+/);
if (parts.length < 2) {
fail(
`Runtime Python probe did not report a valid version. Output: ${(probe.stdout || '').trim()}`,
);
}
const actual = {
major: Number.parseInt(parts[0], 10),
minor: Number.parseInt(parts[1], 10),
};
if (
actual.major !== expectedRuntimeVersion.major ||
actual.minor !== expectedRuntimeVersion.minor
) {
fail(
`Runtime Python version mismatch: expected ${expectedRuntimeVersion.major}.${expectedRuntimeVersion.minor}, ` +
`got ${actual.major}.${actual.minor}.`,
);
}
};
```
这样可以保留所有功能(包括验证 `pip` 可被导入),同时简化探测逻辑并避免使用正则。
---
### 5. 在不改变行为的前提下略微收紧拷贝过滤
当前的 `shouldCopy` 没问题,但可以稍微合并一下条件,减轻视觉负担:
```ts
const IGNORED_NAMES = new Set([
'__pycache__',
'.pytest_cache',
'.ruff_cache',
'.git',
'.mypy_cache',
'.DS_Store',
]);
const shouldCopy = (srcPath: string) => {
const base = path.basename(srcPath);
if (IGNORED_NAMES.has(base)) return false;
return !base.endsWith('.pyc') && !base.endsWith('.pyo');
};
```
`copyTree` 的调用点可以完全保持不变,但意图会更清晰、代码也更紧凑。
---
应用这些针对性的改动,可以保留所有新增行为(环境变量/pyproject 配置、路径重叠检查、运行时校验、过滤器、manifest/launcher 等),同时让脚本的主流程和辅助函数更易读、更易维护。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进后续评审。
Original comment in English
Hey - I've found 3 issues, and left some high level feedback:
- In
desktop/scripts/build-backend.mjs, the runtime probe requiresimport pipto succeed; if someone provides a minimal CPython runtime without pip this will hard-fail even though the backend might still work—consider probing onlysys.version_infoor making the pip check optional with a clearer error path. - In
PipInstaller.install, whenwheel_only=Trueyou unconditionally add--only-binary :all:and--prefer-binarybut do not guard against conflicting flags like--no-binarythat may be present inpip_install_arg; it would be safer to normalize or reject incompatible options before constructing the final argument list.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `desktop/scripts/build-backend.mjs`, the runtime probe requires `import pip` to succeed; if someone provides a minimal CPython runtime without pip this will hard-fail even though the backend might still work—consider probing only `sys.version_info` or making the pip check optional with a clearer error path.
- In `PipInstaller.install`, when `wheel_only=True` you unconditionally add `--only-binary :all:` and `--prefer-binary` but do not guard against conflicting flags like `--no-binary` that may be present in `pip_install_arg`; it would be safer to normalize or reject incompatible options before constructing the final argument list.
## Individual Comments
### Comment 1
<location> `desktop/scripts/build-backend.mjs:107-43` </location>
<code_context>
+const expectedRuntimeVersion = resolveExpectedRuntimeVersion();
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The runtime Python version check is stricter than necessary and may reject compatible newer runtimes.
Here `resolveExpectedRuntimeVersion` is effectively treated as a strict `major.minor` pin. If it’s derived from a `requires-python >= X.Y` constraint, that will incorrectly reject newer compatible runtimes (e.g. 3.11 when `>=3.10`) and fail the build.
To align with a minimum-version constraint, the check should use `>=` semantics on `major.minor` (only fail when the runtime is older than the minimum). You could still keep the strict equality check when `ASTRBOT_DESKTOP_EXPECTED_PYTHON` is explicitly set, and only use the lower-bound logic for values inferred from `pyproject.toml`.
Suggested implementation:
```javascript
const { expectedRuntimeVersion, isLowerBoundRuntimeVersion } = resolveExpectedRuntimeVersion();
```
To complete the behavior change you described, you will also need to adjust the implementation of `resolveExpectedRuntimeVersion` and the runtime check:
1. Update `resolveExpectedRuntimeVersion` so it returns an object, e.g.
```js
// when coming from env:
return { expectedRuntimeVersion: parseExpectedRuntimeVersion(envValue, 'ASTRBOT_DESKTOP_EXPECTED_PYTHON'), isLowerBoundRuntimeVersion: false };
// when coming from pyproject requires-python >=X.Y:
return { expectedRuntimeVersion: parseExpectedRuntimeVersion(projectLowerBound, 'pyproject.toml requires-python'), isLowerBoundRuntimeVersion: true };
```
2. Locate where the current Python runtime version is validated (likely something like `if (runtimeVersion !== expectedRuntimeVersion) { ... }`) and change it to:
```js
if (isLowerBoundRuntimeVersion) {
// fail only when runtime is *older* than expectedRuntimeVersion major.minor
if (compareMajorMinor(runtimeVersion, expectedRuntimeVersion) < 0) {
// fail
}
} else {
// strict equality when explicitly set via env
if (runtimeVersion !== expectedRuntimeVersion) {
// fail
}
}
```
You will need a helper like `compareMajorMinor(a, b)` that parses the `major.minor` segments into integers and returns -1/0/1.
3. Ensure all call sites of `resolveExpectedRuntimeVersion` are updated to expect the new `{ expectedRuntimeVersion, isLowerBoundRuntimeVersion }` shape.
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:742` </location>
<code_context>
+ );
+ }
+
+ queryWindowsProcessCommandLineByPowerShell(pid) {
+ const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($null -ne $p) { $p.CommandLine }`;
+ return spawnSync(
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring the new Windows process-inspection and cleanup logic to remove duplication, simplify caching, and consolidate the kill decision into a single helper for easier reasoning and maintenance.
You can keep all the new safety features and still reduce complexity in a few focused spots:
---
### 1. Collapse the two PowerShell helpers
`queryWindowsProcessCommandLineByPowerShell` and `queryWindowsProcessCommandLineByPwsh` are identical apart from the executable name. You can replace both with a single helper and simplify `getWindowsProcessCommandLine`:
```js
// replace both queryWindowsProcessCommandLineByPowerShell/Pwsh with:
queryWindowsProcessCommandLine(shellName, pid) {
const query =
`$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; ` +
'if ($null -ne $p) { $p.CommandLine }';
return spawnSync(
shellName,
['-NoProfile', '-NonInteractive', '-Command', query],
{
stdio: ['ignore', 'pipe', 'ignore'],
encoding: 'utf8',
windowsHide: true,
timeout: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
},
);
}
```
Then `getWindowsProcessCommandLine` becomes:
```js
const queryAttempts = ['powershell', 'pwsh'];
for (const shellName of queryAttempts) {
let result;
try {
result = this.queryWindowsProcessCommandLine(shellName, numericPid);
} catch (error) {
if (error instanceof Error && error.message) {
this.log(
`Failed to query process command line by ${shellName} for pid=${numericPid}: ${error.message}`,
);
}
continue;
}
if (result.error && result.error.code === 'ENOENT') continue;
if (result.error && result.error.code === 'ETIMEDOUT') {
this.log(
`Timed out (${WINDOWS_PROCESS_QUERY_TIMEOUT_MS}ms) querying process command line by ${shellName} for pid=${numericPid}.`,
);
continue;
}
if (result.status === 0) {
const commandLine = this.parseWindowsProcessCommandLine(result);
this.windowsProcessCommandLineCache.set(numericPid, {
commandLine,
timestampMs: Date.now(),
});
return commandLine;
}
}
```
---
### 2. Drop explicit pruning and timestamps from the cache
Given this cache is used only in the unmanaged cleanup path, you can simplify it to a plain `Map<pid, commandLine>` and remove TTL/pruning logic altogether. This eliminates `WINDOWS_PROCESS_COMMAND_LINE_CACHE_TTL_MS`, `pruneWindowsProcessCommandLineCache`, and timestamp handling without changing observable behavior for the cleanup flow.
```js
// constructor:
this.windowsProcessCommandLineCache = new Map(); // pid -> commandLine
// remove pruneWindowsProcessCommandLineCache entirely and its call in stopUnmanagedBackendByPort
getWindowsProcessCommandLine(pid) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) {
return null;
}
if (this.windowsProcessCommandLineCache.has(numericPid)) {
return this.windowsProcessCommandLineCache.get(numericPid);
}
// ... query logic as before ...
if (result.status === 0) {
const commandLine = this.parseWindowsProcessCommandLine(result);
this.windowsProcessCommandLineCache.set(numericPid, commandLine);
return commandLine;
}
this.windowsProcessCommandLineCache.set(numericPid, null);
return null;
}
```
This keeps caching semantics (no repeated queries within a cleanup run) but removes lifecycle management and extra branching.
---
### 3. Inline the matcher object into a single decision helper
`buildWindowsUnmanagedBackendMatcher` + `shouldKillUnmanagedBackendProcess` can be merged into a single, audit-friendly helper that operates directly on `backendConfig`. That removes the intermediary “matcher object” abstraction and the extra indirection.
```js
shouldKillUnmanagedBackendProcess(pid, processInfo, backendConfig) {
const expectedImageName = path
.basename(backendConfig.cmd || 'python.exe')
.toLowerCase();
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
if (!this.isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
const expectedCommandLineMarkers = [];
if (Array.isArray(backendConfig.args) && backendConfig.args.length > 0) {
const primaryArg = backendConfig.args[0];
if (typeof primaryArg === 'string' && primaryArg) {
const resolvedPrimaryArg = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(backendConfig.cwd || process.cwd(), primaryArg);
expectedCommandLineMarkers.push(
this.normalizeWindowsPathForMatch(resolvedPrimaryArg),
);
expectedCommandLineMarkers.push(
this.normalizeWindowsPathForMatch(path.basename(primaryArg)),
);
}
}
if (!expectedCommandLineMarkers.length) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`,
);
return false;
}
const commandLine = this.getWindowsProcessCommandLine(pid);
if (!commandLine) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`,
);
return false;
}
const normalizedCommandLine = this.normalizeWindowsPathForMatch(commandLine);
const markerMatched = expectedCommandLineMarkers.some(
(marker) => marker && normalizedCommandLine.includes(marker),
);
if (!markerMatched) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
return false;
}
return true;
}
```
And `stopUnmanagedBackendByPort` becomes simpler:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') {
return false;
}
const port = this.getBackendPort();
if (!port) {
return false;
}
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) {
return false;
}
this.log(
`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`,
);
const backendConfig = this.getBackendConfig();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`,
);
continue;
}
if (!this.shouldKillUnmanagedBackendProcess(pid, processInfo, backendConfig)) {
continue;
}
// taskkill as before...
}
return true;
}
```
This keeps all existing checks (image match + optional strict command line markers) but makes the kill decision traceable in a single place.
</issue_to_address>
### Comment 3
<location> `desktop/scripts/build-backend.mjs:14` </location>
<code_context>
+const manifestPath = path.join(outputDir, 'runtime-manifest.json');
+const launcherPath = path.join(outputDir, 'launch_backend.py');
+
+const runtimeSource =
+ process.env.ASTRBOT_DESKTOP_CPYTHON_HOME ||
+ process.env.ASTRBOT_DESKTOP_BACKEND_RUNTIME;
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring this script to centralize failures, streamline path/runtime helpers, and tighten filters so the main build flow is shorter and easier to follow without changing behavior.
You can reduce the complexity noticeably without dropping any behavior by:
---
### 1. Centralize error handling
You currently repeat `console.error` + `process.exit(1)` in many helpers. A small `fail` helper keeps behavior identical but simplifies all callers:
```ts
// Near the top
const fail = (message: string): never => {
console.error(message);
process.exit(1);
};
```
Then replace patterns like:
```ts
if (!runtimeSource) {
console.error(
'Missing CPython runtime source. Set ASTRBOT_DESKTOP_CPYTHON_HOME ' +
'(recommended) or ASTRBOT_DESKTOP_BACKEND_RUNTIME.',
);
process.exit(1);
}
```
with:
```ts
if (!runtimeSource) {
fail(
'Missing CPython runtime source. Set ASTRBOT_DESKTOP_CPYTHON_HOME ' +
'(recommended) or ASTRBOT_DESKTOP_BACKEND_RUNTIME.',
);
}
```
Do the same in `parseExpectedRuntimeVersion`, `resolveExpectedRuntimeVersion`, `validateRuntimePython`, etc. This alone removes a lot of branching noise.
---
### 2. Simplify path overlap logic
You can keep the overlap protection but make it more direct, without separate “normalize” helpers spread around:
```ts
const normalize = (p: string) => {
const resolved = path.resolve(p).replace(/[\\/]+$/, '');
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
};
const pathsOverlap = (a: string, b: string) => {
const na = normalize(a);
const nb = normalize(b);
return na === nb || na.startsWith(nb + path.sep) || nb.startsWith(na + path.sep);
};
const runtimeSourceReal = path.resolve(rootDir, runtimeSource);
if (!fs.existsSync(runtimeSourceReal)) {
fail(`CPython runtime source does not exist: ${runtimeSourceReal}`);
}
if (pathsOverlap(runtimeSourceReal, outputDir)) {
fail(
`CPython runtime source overlaps with backend output directory. ` +
`runtime=${runtimeSourceReal}, output=${outputDir}. ` +
'Please set ASTRBOT_DESKTOP_CPYTHON_HOME to a separate runtime directory.',
);
}
```
This keeps the cross‑platform behavior and overlap check, but the main flow is easier to read.
---
### 3. Make runtime resolution + validation a single helper
You call `resolveRuntimePython` and then immediately validate it. Merging them into one function clarifies the “find + assert” intent and reduces call graph depth:
```ts
const findAndValidateRuntimePython = (runtimeRoot: string) => {
const candidates =
process.platform === 'win32'
? ['python.exe', path.join('Scripts', 'python.exe')]
: [path.join('bin', 'python3'), path.join('bin', 'python')];
for (const rel of candidates) {
const abs = path.join(runtimeRoot, rel);
if (fs.existsSync(abs)) {
validateRuntimePython(abs);
return {
absolute: abs,
relative: path.relative(outputDir, abs),
};
}
}
fail(
`Cannot find Python executable in runtime: ${runtimeRoot}. ` +
'Expected python under bin/ or Scripts/.',
);
};
```
Then the main flow becomes:
```ts
const sourceRuntimePython = findAndValidateRuntimePython(runtimeSourceReal);
// ...
const runtimePython = findAndValidateRuntimePython(runtimeDir);
```
---
### 4. Simplify `validateRuntimePython` version parsing
You can keep the `pip` import (to assert it’s present) but remove the formatted string + regex round‑trip; just print two numbers and split:
```ts
const validateRuntimePython = (pythonExecutable: string) => {
const probe = spawnSync(
pythonExecutable,
[
'-c',
'import sys, pip; print(sys.version_info[0], sys.version_info[1])',
],
{
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
windowsHide: true,
timeout: 5000,
},
);
if (probe.error) {
const reason =
probe.error.code === 'ETIMEDOUT'
? 'runtime Python probe timed out'
: probe.error.message || String(probe.error);
fail(`Runtime Python probe failed: ${reason}`);
}
if (probe.status !== 0) {
const stderrText = (probe.stderr || '').trim();
fail(
`Runtime Python probe failed with exit code ${probe.status}.` +
(stderrText ? ` stderr: ${stderrText}` : ''),
);
}
const parts = (probe.stdout || '').trim().split(/\s+/);
if (parts.length < 2) {
fail(
`Runtime Python probe did not report a valid version. Output: ${(probe.stdout || '').trim()}`,
);
}
const actual = {
major: Number.parseInt(parts[0], 10),
minor: Number.parseInt(parts[1], 10),
};
if (
actual.major !== expectedRuntimeVersion.major ||
actual.minor !== expectedRuntimeVersion.minor
) {
fail(
`Runtime Python version mismatch: expected ${expectedRuntimeVersion.major}.${expectedRuntimeVersion.minor}, ` +
`got ${actual.major}.${actual.minor}.`,
);
}
};
```
This preserves all functionality (including verifying `pip` is importable) but trims the probe logic and avoids regex.
---
### 5. Slightly tighten copy filtering without changing behavior
`shouldCopy` is fine, but you can collapse the conditionals a bit to reduce visual weight:
```ts
const IGNORED_NAMES = new Set([
'__pycache__',
'.pytest_cache',
'.ruff_cache',
'.git',
'.mypy_cache',
'.DS_Store',
]);
const shouldCopy = (srcPath: string) => {
const base = path.basename(srcPath);
if (IGNORED_NAMES.has(base)) return false;
return !base.endsWith('.pyc') && !base.endsWith('.pyo');
};
```
The call site of `copyTree` stays exactly the same, but the intent is clearer and more compact.
---
Applying these targeted changes would keep all of the new behavior (env/pyproject config, overlap checks, runtime validation, filters, manifest/launcher) while making the script’s main flow and helpers noticeably simpler to read and maintain.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Hey - 我发现了两个问题
面向 AI Agents 的提示
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/lib/backend-manager.js:165` </location>
<code_context>
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
</code_context>
<issue_to_address>
**issue (complexity):** 建议将后端启动规划和非托管清理决策提取成一些小而纯粹的辅助对象/工厂,这样主方法只需要组合它们,逻辑会更容易理解。
你可以通过引入小的值对象和值工厂函数来集中新的逻辑,而不改变行为,再把现有代码接入这些工厂即可。
### 1. 将后端启动解析整合为单一计划
目前 `resolveBackendConfig`、`canManageBackend`、`getBackendCommandUnavailableReason` 以及打包状态都在参与“我要运行什么、以及为什么可能不可用?”这类决策。
你可以将这些逻辑放入一个 `BackendLaunchPlan` 工厂中,并把 `resolveBackendConfig` 保持为一个很薄的封装:
```js
// keep this near the top of the file
function createBackendLaunchPlan({
app,
env,
resourcesPath,
webuiDir,
loadPackagedBackendState,
log,
}) {
const customCmd = env.ASTRBOT_BACKEND_CMD;
let cmd = null;
let args = [];
let shell = true;
let failureReason = null;
if (customCmd) {
cmd = customCmd;
shell = true;
} else if (app.isPackaged) {
const packagedBackendState = loadPackagedBackendState();
if (packagedBackendState?.ok && packagedBackendState.config) {
cmd = packagedBackendState.config.runtimePythonPath;
args = [
packagedBackendState.config.launchScriptPath,
...(webuiDir ? ['--webui-dir', webuiDir] : []),
];
shell = false;
} else {
failureReason =
packagedBackendState?.failureReason || 'Backend command is not configured.';
log(failureReason);
}
} else {
// dev default
args = ['run', 'main.py'];
if (webuiDir) {
args.push('--webui-dir', webuiDir);
}
cmd = 'uv';
shell = process.platform === 'win32';
}
return { cmd, args, shell, failureReason };
}
```
然后 `resolveBackendConfig` 就变成了一个简单的组合:
```js
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
const { cmd, args, shell, failureReason } = createBackendLaunchPlan({
app: this.app,
env: process.env,
resourcesPath: process.resourcesPath,
webuiDir,
loadPackagedBackendState: () => this.getPackagedBackendState(),
log: (m) => this.log(m),
});
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
this.backendConfig = {
cmd,
args,
shell,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
然后所有关于“这个配置是否可用 / 如果不可用,原因是什么?”的逻辑都可以集中到两个小的辅助函数中,而不是分散在各处:
```js
isBackendConfigUsable() {
const cfg = this.getBackendConfig();
return Boolean(cfg.cmd && !cfg.failureReason);
}
getBackendConfigFailureReason() {
const cfg = this.getBackendConfig();
return cfg.failureReason || 'Backend command is not configured.';
}
```
这样调用端的代码会更简单、更一致:
```js
async startBackendAndWait(maxWaitMs = this.backendTimeoutMs) {
if (!this.isBackendConfigUsable()) {
return { ok: false, reason: this.getBackendConfigFailureReason() };
}
// ...
}
async ensureBackend() {
this.backendStartupFailureReason = null;
if (await this.pingBackend()) return true;
if (!this.backendAutoStart) {
this.backendStartupFailureReason = 'Backend auto-start is disabled.';
return false;
}
if (!this.isBackendConfigUsable()) {
this.backendStartupFailureReason = this.getBackendConfigFailureReason();
return false;
}
// ...
}
```
这样就不再需要 `canManageBackend()`、`getBackendCommandUnavailableReason()`,以及各个调用点上临时写的字符串字面量了。
---
### 2. 为非托管后端清理引入显式的清理策略对象
`cleanupUnmanagedBackendOnWindows` 目前在每次循环中都会重新计算/防御性检查 `backendConfig`、`allowImageOnlyMatch` 和 `fallbackCmdRaw`。你可以预先计算一个显式的策略对象,并把它传给 `shouldKillUnmanagedBackendProcess`。
```js
function createUnmanagedCleanupStrategy({ getBackendConfig, log, env }) {
let backendConfig = null;
try {
backendConfig = getBackendConfig();
} catch (error) {
log(
`Failed to resolve backend config during unmanaged cleanup: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
const hasBackendConfig = backendConfig && typeof backendConfig === 'object';
if (!hasBackendConfig) {
log(
'Backend config is unavailable during unmanaged cleanup; falling back to image-name-only matching.',
);
}
return {
backendConfig: hasBackendConfig ? backendConfig : null,
allowImageOnlyMatch: !hasBackendConfig,
fallbackCmdRaw: env.ASTRBOT_BACKEND_CMD || 'python.exe',
};
}
```
然后 `cleanupUnmanagedBackendOnWindows` 可以简化为:
```js
cleanupUnmanagedBackendOnWindows(port) {
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) return false;
this.log(`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`);
const strategy = createUnmanagedCleanupStrategy({
getBackendConfig: () => this.getBackendConfig(),
log: (m) => this.log(m),
env: process.env,
});
const commandLineCache = new Map();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`);
continue;
}
const shouldKill = shouldKillUnmanagedBackendProcess({
pid,
processInfo,
commandLineCache,
spawnSync,
log: (m) => this.log(m),
...strategy,
});
if (!shouldKill) continue;
try {
spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
} catch (error) {
this.log(/* unchanged error log */);
}
}
return true;
}
```
这样在保留所有现有行为(包括回退逻辑)的前提下,把分支和防御式检查放进了一个小而可测试的“策略”构造函数中,使 `cleanupUnmanagedBackendOnWindows` 和 `shouldKillUnmanagedBackendProcess` 更容易理解。
</issue_to_address>
### Comment 2
<location> `desktop/lib/windows-backend-cleanup.js:138` </location>
<code_context>
+ ];
+}
+
+function shouldKillUnmanagedBackendProcess({
+ pid,
+ processInfo,
</code_context>
<issue_to_address>
**issue (complexity):** 建议提取一个预先计算好的小型匹配配置对象,并增加一个专门的命令行解析辅助函数,这样 `shouldKillUnmanagedBackendProcess` 就可以只专注于决策逻辑,而不是配置和探测细节。
你可以在保持行为不变的情况下,通过两点来降低感知复杂度:(1) 把配置折叠到一个预先计算的对象里;(2) 把 PowerShell 探测逻辑隔离到一个更窄的辅助函数里。
### 1. 预先计算一个小型匹配配置
目前 `shouldKillUnmanagedBackendProcess` 接收很多参数,并在每次调用时重复计算派生值。你可以把这些逻辑移到一个专门的配置工厂里,从而简化调用点和函数签名:
```js
function createBackendCleanupMatchConfig({ backendConfig, fallbackCmdRaw, allowImageOnlyMatch }) {
const expectedImageName = getExpectedWindowsBackendImageName(backendConfig, fallbackCmdRaw);
const markers = buildBackendCommandLineMarkers(backendConfig);
return {
expectedImageName,
markers,
allowImageOnlyMatch,
};
}
function shouldKillUnmanagedBackendProcess({
pid,
processInfo,
matchConfig,
commandLineCache,
spawnSync,
log,
}) {
const { expectedImageName, markers, allowImageOnlyMatch } = matchConfig;
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
log(`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`);
return false;
}
if (allowImageOnlyMatch || !isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
if (!markers.length) {
log(`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`);
return false;
}
const commandLine = getWindowsProcessCommandLine({
pid,
commandLineCache,
spawnSync,
log,
timeoutMs: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
});
if (!commandLine) {
log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`);
return false;
}
const normalizedCommandLine = normalizeWindowsPathForMatch(commandLine);
const matched = markers.some((marker) => marker && normalizedCommandLine.includes(marker));
if (!matched) {
log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
}
return matched;
}
```
调用端示例:
```js
const matchConfig = createBackendCleanupMatchConfig({
backendConfig,
fallbackCmdRaw,
allowImageOnlyMatch,
});
// 后续在每个进程上:
shouldKillUnmanagedBackendProcess({
pid,
processInfo,
matchConfig,
commandLineCache,
spawnSync,
log,
});
```
这样可以消除重复计算,让 `shouldKillUnmanagedBackendProcess` 更专注于“决策逻辑”,而不是配置构造。
### 2. 收窄并简化 PowerShell 探测
当前的双 shell 逻辑和缓存接线嵌入在主决策路径中。你可以在保持行为不变的前提下,把复杂性隐藏在一个单一的辅助函数中,使调用图更浅、更语义化:
```js
function resolveWindowsProcessCommandLine({ pid, commandLineCache, spawnSync, log }) {
return getWindowsProcessCommandLine({
pid,
commandLineCache,
spawnSync,
log,
timeoutMs: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
});
}
// 在 shouldKillUnmanagedBackendProcess 中:
const commandLine = resolveWindowsProcessCommandLine({
pid,
commandLineCache,
spawnSync,
log,
});
```
如果你愿意在行为不变的前提下再进一步,还可以把双 shell 重试封装到 `resolveWindowsProcessCommandLine` 中,并将 `getWindowsProcessCommandLine` 作为内部实现,这样 `shouldKillUnmanagedBackendProcess` 只需要处理“获取命令行或 null”这一单一职责。
这两处改动在保留你已有实现的同时,压平了公共 API,使调用方和主决策函数不再需要了解探测/归一化细节。
</issue_to_address>帮我变得更有用!请对每条评论点 👍 或 👎,我会根据这些反馈改进后续评审。
Original comment in English
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/lib/backend-manager.js:165` </location>
<code_context>
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting backend launch planning and unmanaged cleanup decisions into small, pure helper objects/factories so the main methods just compose them and become easier to follow.
You can centralize the new logic without changing behaviour by introducing small value objects and pure factory functions, then wiring existing code through them.
### 1. Consolidate backend launch resolution into a single plan
Right now `resolveBackendConfig`, `canManageBackend`, `getBackendCommandUnavailableReason`, and packaged state all participate in “what do I run and why might it be unavailable?”.
You can move this into a single `BackendLaunchPlan` factory and keep `resolveBackendConfig` as a thin wrapper:
```js
// keep this near the top of the file
function createBackendLaunchPlan({
app,
env,
resourcesPath,
webuiDir,
loadPackagedBackendState,
log,
}) {
const customCmd = env.ASTRBOT_BACKEND_CMD;
let cmd = null;
let args = [];
let shell = true;
let failureReason = null;
if (customCmd) {
cmd = customCmd;
shell = true;
} else if (app.isPackaged) {
const packagedBackendState = loadPackagedBackendState();
if (packagedBackendState?.ok && packagedBackendState.config) {
cmd = packagedBackendState.config.runtimePythonPath;
args = [
packagedBackendState.config.launchScriptPath,
...(webuiDir ? ['--webui-dir', webuiDir] : []),
];
shell = false;
} else {
failureReason =
packagedBackendState?.failureReason || 'Backend command is not configured.';
log(failureReason);
}
} else {
// dev default
args = ['run', 'main.py'];
if (webuiDir) {
args.push('--webui-dir', webuiDir);
}
cmd = 'uv';
shell = process.platform === 'win32';
}
return { cmd, args, shell, failureReason };
}
```
Then `resolveBackendConfig` becomes a simple composition:
```js
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
const { cmd, args, shell, failureReason } = createBackendLaunchPlan({
app: this.app,
env: process.env,
resourcesPath: process.resourcesPath,
webuiDir,
loadPackagedBackendState: () => this.getPackagedBackendState(),
log: (m) => this.log(m),
});
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
this.backendConfig = {
cmd,
args,
shell,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
And all the “is this usable / why not?” logic can be centralized in two small helpers instead of scattered checks:
```js
isBackendConfigUsable() {
const cfg = this.getBackendConfig();
return Boolean(cfg.cmd && !cfg.failureReason);
}
getBackendConfigFailureReason() {
const cfg = this.getBackendConfig();
return cfg.failureReason || 'Backend command is not configured.';
}
```
Then call sites become simpler and consistent:
```js
async startBackendAndWait(maxWaitMs = this.backendTimeoutMs) {
if (!this.isBackendConfigUsable()) {
return { ok: false, reason: this.getBackendConfigFailureReason() };
}
// ...
}
async ensureBackend() {
this.backendStartupFailureReason = null;
if (await this.pingBackend()) return true;
if (!this.backendAutoStart) {
this.backendStartupFailureReason = 'Backend auto-start is disabled.';
return false;
}
if (!this.isBackendConfigUsable()) {
this.backendStartupFailureReason = this.getBackendConfigFailureReason();
return false;
}
// ...
}
```
This removes the need for `canManageBackend()` + `getBackendCommandUnavailableReason()` + ad hoc string literals at each call site.
---
### 2. Use an explicit cleanup strategy object for unmanaged backend cleanup
`cleanupUnmanagedBackendOnWindows` currently recomputes/guards over `backendConfig`, `allowImageOnlyMatch`, and `fallbackCmdRaw` on every loop. You can precompute an explicit strategy once and pass that into `shouldKillUnmanagedBackendProcess`.
```js
function createUnmanagedCleanupStrategy({ getBackendConfig, log, env }) {
let backendConfig = null;
try {
backendConfig = getBackendConfig();
} catch (error) {
log(
`Failed to resolve backend config during unmanaged cleanup: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
const hasBackendConfig = backendConfig && typeof backendConfig === 'object';
if (!hasBackendConfig) {
log(
'Backend config is unavailable during unmanaged cleanup; falling back to image-name-only matching.',
);
}
return {
backendConfig: hasBackendConfig ? backendConfig : null,
allowImageOnlyMatch: !hasBackendConfig,
fallbackCmdRaw: env.ASTRBOT_BACKEND_CMD || 'python.exe',
};
}
```
Then `cleanupUnmanagedBackendOnWindows` simplifies to:
```js
cleanupUnmanagedBackendOnWindows(port) {
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) return false;
this.log(`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`);
const strategy = createUnmanagedCleanupStrategy({
getBackendConfig: () => this.getBackendConfig(),
log: (m) => this.log(m),
env: process.env,
});
const commandLineCache = new Map();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`);
continue;
}
const shouldKill = shouldKillUnmanagedBackendProcess({
pid,
processInfo,
commandLineCache,
spawnSync,
log: (m) => this.log(m),
...strategy,
});
if (!shouldKill) continue;
try {
spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
} catch (error) {
this.log(/* unchanged error log */);
}
}
return true;
}
```
This keeps all existing behaviour (including fallbacks) but puts the branching and defensive checks into a small, testable “strategy” constructor, making `cleanupUnmanagedBackendOnWindows` and `shouldKillUnmanagedBackendProcess` much easier to reason about.
</issue_to_address>
### Comment 2
<location> `desktop/lib/windows-backend-cleanup.js:138` </location>
<code_context>
+ ];
+}
+
+function shouldKillUnmanagedBackendProcess({
+ pid,
+ processInfo,
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting a small precomputed match configuration and a dedicated command-line resolution helper so that `shouldKillUnmanagedBackendProcess` focuses only on decision logic instead of configuration and probing details.
You can keep the behavior but reduce perceived complexity by (1) collapsing configuration into a precomputed object, and (2) isolating the PowerShell probing to a single, narrower helper.
### 1. Precompute a small match config
Right now `shouldKillUnmanagedBackendProcess` takes many parameters and recomputes derived values on every call. You can move those into a dedicated config factory and simplify the call site and function signature:
```js
function createBackendCleanupMatchConfig({ backendConfig, fallbackCmdRaw, allowImageOnlyMatch }) {
const expectedImageName = getExpectedWindowsBackendImageName(backendConfig, fallbackCmdRaw);
const markers = buildBackendCommandLineMarkers(backendConfig);
return {
expectedImageName,
markers,
allowImageOnlyMatch,
};
}
function shouldKillUnmanagedBackendProcess({
pid,
processInfo,
matchConfig,
commandLineCache,
spawnSync,
log,
}) {
const { expectedImageName, markers, allowImageOnlyMatch } = matchConfig;
const actualImageName = processInfo.imageName.toLowerCase();
if (actualImageName !== expectedImageName) {
log(`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`);
return false;
}
if (allowImageOnlyMatch || !isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
if (!markers.length) {
log(`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`);
return false;
}
const commandLine = getWindowsProcessCommandLine({
pid,
commandLineCache,
spawnSync,
log,
timeoutMs: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
});
if (!commandLine) {
log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`);
return false;
}
const normalizedCommandLine = normalizeWindowsPathForMatch(commandLine);
const matched = markers.some((marker) => marker && normalizedCommandLine.includes(marker));
if (!matched) {
log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
}
return matched;
}
```
Caller side:
```js
const matchConfig = createBackendCleanupMatchConfig({
backendConfig,
fallbackCmdRaw,
allowImageOnlyMatch,
});
// later, per-process:
shouldKillUnmanagedBackendProcess({
pid,
processInfo,
matchConfig,
commandLineCache,
spawnSync,
log,
});
```
This removes repeated computation and makes `shouldKillUnmanagedBackendProcess` about “decision logic” instead of config construction.
### 2. Narrow and simplify PowerShell probing
The dual-shell logic and cache wiring are currently embedded in the main decision path. You can keep the behavior but hide the complexity in a single helper, making the call graph shallower and more semantic:
```js
function resolveWindowsProcessCommandLine({ pid, commandLineCache, spawnSync, log }) {
return getWindowsProcessCommandLine({
pid,
commandLineCache,
spawnSync,
log,
timeoutMs: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
});
}
// in shouldKillUnmanagedBackendProcess:
const commandLine = resolveWindowsProcessCommandLine({
pid,
commandLineCache,
spawnSync,
log,
});
```
If you want to go a bit further without changing behavior, you can also encapsulate the dual-shell retry within `resolveWindowsProcessCommandLine` and keep `getWindowsProcessCommandLine` internal, so `shouldKillUnmanagedBackendProcess` only deals with “get command line or null”.
These two changes keep everything you’ve implemented but flatten the public API and reduce how much the caller and main decision function need to know about the probing/normalization details.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了 1 个问题
给 AI 代理的提示
Please address the comments from this code review:
## 单独评论
### 评论 1
<location> `desktop/scripts/runtime-version-utils.mjs:171` </location>
<code_context>
+ );
+};
+
+export const validateRuntimePython = ({
+ pythonExecutable,
+ expectedRuntimeConstraint,
</code_context>
<issue_to_address>
**issue (complexity):** 考虑将 `validateRuntimePython` 中的探测执行逻辑和版本解析逻辑提取到单独的辅助函数中,以简化控制流并集中处理错误。
你可以通过职责分离的方式,最大程度地降低 `validateRuntimePython` 的复杂度:一个函数负责运行探测(包括错误处理),另一个函数负责解析版本。这样可以减少分支嵌套,让主验证函数在保持原有行为的同时更易读。
例如:
```js
const runPythonProbe = (pythonExecutable, requirePipProbe) => {
const probeScript = requirePipProbe
? 'import sys, pip; print(sys.version_info[0], sys.version_info[1])'
: 'import sys; print(sys.version_info[0], sys.version_info[1])';
const probe = spawnSync(pythonExecutable, ['-c', probeScript], {
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
windowsHide: true,
timeout: 5000,
});
if (probe.error) {
const reason =
probe.error.code === 'ETIMEDOUT'
? 'runtime Python probe timed out'
: probe.error.message || String(probe.error);
throw new Error(`Runtime Python probe failed: ${reason}`);
}
if (probe.status !== 0) {
const stderrText = (probe.stderr || '').trim();
const base = `Runtime Python probe failed with exit code ${probe.status}. `;
const pipNote = requirePipProbe
? 'pip import check is enabled by ASTRBOT_DESKTOP_REQUIRE_PIP=1. '
: '';
throw new Error(base + pipNote + (stderrText ? `stderr: ${stderrText}` : ''));
}
return probe.stdout || '';
};
const parseProbeVersion = (stdout) => {
const parts = String(stdout).trim().split(/\s+/);
if (parts.length < 2) {
throw new Error(
`Runtime Python probe did not report a valid version. Output: ${String(stdout).trim()}`,
);
}
return {
major: Number.parseInt(parts[0], 10),
minor: Number.parseInt(parts[1], 10),
};
};
```
然后,`validateRuntimePython` 就变成了一条直线式的流程:
```js
export const validateRuntimePython = ({
pythonExecutable,
expectedRuntimeConstraint,
requirePipProbe,
}) => {
const stdout = runPythonProbe(pythonExecutable, requirePipProbe);
const actualVersion = parseProbeVersion(stdout);
const expected = expectedRuntimeConstraint.expectedRuntimeVersion;
const cmp = compareMajorMinor(actualVersion, expected);
if (expectedRuntimeConstraint.isLowerBoundRuntimeVersion) {
if (cmp < 0) {
throw new Error(
`Runtime Python version is too low for ${expectedRuntimeConstraint.source}: ` +
`expected >= ${expected.major}.${expected.minor}, ` +
`got ${actualVersion.major}.${actualVersion.minor}.`,
);
}
return;
}
if (cmp !== 0) {
throw new Error(
`Runtime Python version mismatch for ${expectedRuntimeConstraint.source}: ` +
`expected ${expected.major}.${expected.minor}, ` +
`got ${actualVersion.major}.${actualVersion.minor}.`,
);
}
};
```
这样可以在保留当前所有行为(包括超时处理和 pip 相关的错误信息)的前提下,大幅减少嵌套,并将错误处理逻辑集中在一个地方。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进之后的评审。
Original comment in English
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/scripts/runtime-version-utils.mjs:171` </location>
<code_context>
+ );
+};
+
+export const validateRuntimePython = ({
+ pythonExecutable,
+ expectedRuntimeConstraint,
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the probe execution and version parsing in `validateRuntimePython` into separate helper functions to simplify the control flow and centralize error handling.
You can reduce complexity most effectively in `validateRuntimePython` by separating concerns: one function that runs the probe (including error handling) and one that parses the version. This flattens the branching and makes the main validator easier to read while preserving behavior.
For example:
```js
const runPythonProbe = (pythonExecutable, requirePipProbe) => {
const probeScript = requirePipProbe
? 'import sys, pip; print(sys.version_info[0], sys.version_info[1])'
: 'import sys; print(sys.version_info[0], sys.version_info[1])';
const probe = spawnSync(pythonExecutable, ['-c', probeScript], {
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
windowsHide: true,
timeout: 5000,
});
if (probe.error) {
const reason =
probe.error.code === 'ETIMEDOUT'
? 'runtime Python probe timed out'
: probe.error.message || String(probe.error);
throw new Error(`Runtime Python probe failed: ${reason}`);
}
if (probe.status !== 0) {
const stderrText = (probe.stderr || '').trim();
const base = `Runtime Python probe failed with exit code ${probe.status}. `;
const pipNote = requirePipProbe
? 'pip import check is enabled by ASTRBOT_DESKTOP_REQUIRE_PIP=1. '
: '';
throw new Error(base + pipNote + (stderrText ? `stderr: ${stderrText}` : ''));
}
return probe.stdout || '';
};
const parseProbeVersion = (stdout) => {
const parts = String(stdout).trim().split(/\s+/);
if (parts.length < 2) {
throw new Error(
`Runtime Python probe did not report a valid version. Output: ${String(stdout).trim()}`,
);
}
return {
major: Number.parseInt(parts[0], 10),
minor: Number.parseInt(parts[1], 10),
};
};
```
Then `validateRuntimePython` becomes a straight‑line flow:
```js
export const validateRuntimePython = ({
pythonExecutable,
expectedRuntimeConstraint,
requirePipProbe,
}) => {
const stdout = runPythonProbe(pythonExecutable, requirePipProbe);
const actualVersion = parseProbeVersion(stdout);
const expected = expectedRuntimeConstraint.expectedRuntimeVersion;
const cmp = compareMajorMinor(actualVersion, expected);
if (expectedRuntimeConstraint.isLowerBoundRuntimeVersion) {
if (cmp < 0) {
throw new Error(
`Runtime Python version is too low for ${expectedRuntimeConstraint.source}: ` +
`expected >= ${expected.major}.${expected.minor}, ` +
`got ${actualVersion.major}.${actualVersion.minor}.`,
);
}
return;
}
if (cmp !== 0) {
throw new Error(
`Runtime Python version mismatch for ${expectedRuntimeConstraint.source}: ` +
`expected ${expected.major}.${expected.minor}, ` +
`got ${actualVersion.major}.${actualVersion.minor}.`,
);
}
};
```
This preserves all current behavior (including timeout handling and the pip-specific error message) but significantly reduces nesting and concentrates the error‑handling logic in one place.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了 1 个问题,并给出了一些整体性的反馈:
dashboard/vite.config.ts中的 PostCSSnormalize-nested-type-selector插件当前会对所有 CSS 生效;建议将它只作用于你自己的源码(例如通过include/exclude或选择器启发式规则),以避免意外重写第三方样式。- 在新的桌面后端打包流程中,
runtime-layout-utils.resolveAndValidateRuntimeSource目前只检查运行时目录和输出目录之间的路径重叠;你可能还需要显式防止将ASTRBOT_DESKTOP_CPYTHON_HOME指向项目根目录(或其他大型目录树),以避免不小心把整个仓库复制到打包后的运行时中。
给 AI Agent 的提示词
Please address the comments from this code review:
## Overall Comments
- The PostCSS `normalize-nested-type-selector` plugin in `dashboard/vite.config.ts` currently runs on all CSS; consider scoping it to only your own source (e.g. via `include`/`exclude` or selector heuristics) to avoid unexpectedly rewriting third‑party styles.
- In the new desktop backend packaging flow, `runtime-layout-utils.resolveAndValidateRuntimeSource` only checks for path overlap between runtime and output; you may also want to explicitly guard against pointing `ASTRBOT_DESKTOP_CPYTHON_HOME` at the project root (or other large trees) to prevent accidentally copying the entire repo into the packaged runtime.
## Individual Comments
### Comment 1
<location> `desktop/lib/backend-manager.js:132` </location>
<code_context>
return fs.existsSync(indexPath) ? candidate : null;
}
- getPackagedBackendPath() {
+ loadPackagedBackendState() {
if (!this.app.isPackaged) {
</code_context>
<issue_to_address>
**issue (complexity):** 建议通过合并冗余的辅助函数、让启动解析保持基于分支的结构、并内联过薄的包装器来简化后端管理流程,从而减少额外的间接层。
- 你可以把 `loadPackagedBackendState` 和 `getPackagedBackendState` 折叠为一个惰性 getter,在不改变行为的前提下减少一层间接调用:
```js
// replace both loadPackagedBackendState/getPackagedBackendState with:
getPackagedBackendState() {
if (!this.app.isPackaged) {
return null;
}
if (!this.packagedBackendState) {
this.packagedBackendState = resolvePackagedBackendState(
process.resourcesPath,
(message) => this.log(message),
);
}
return this.packagedBackendState;
}
```
- `resolveLaunchCommand` 和 `resolveBackendConfig` 之间的拆分可以通过让启动解析清晰地基于分支,并将打包相关逻辑移动到一个聚焦的辅助函数中来简化。这样既能保持 `resolveBackendConfig` 的线性结构,又能将“打包场景”的行为隔离开来:
```js
buildLaunchForPackagedBackend(packagedBackendState, webuiDir) {
if (!packagedBackendState?.ok || !packagedBackendState.config) {
return {
launch: null,
failureReason:
packagedBackendState?.failureReason ||
'Backend command is not configured.',
};
}
const { runtimePythonPath, launchScriptPath } = packagedBackendState.config;
const args = [launchScriptPath];
if (webuiDir) {
args.push('--webui-dir', webuiDir);
}
return {
launch: { cmd: runtimePythonPath, args, shell: false },
failureReason: null,
};
}
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
let launch = null;
let failureReason = null;
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
if (customCmd) {
launch = { cmd: customCmd, args: [], shell: true };
} else if (this.app.isPackaged) {
({ launch, failureReason } = this.buildLaunchForPackagedBackend(
this.getPackagedBackendState(),
webuiDir,
));
} else {
launch = this.buildDefaultBackendLaunch(webuiDir);
}
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
if (failureReason) {
this.log(failureReason);
}
this.backendConfig = {
cmd: launch ? launch.cmd : null,
args: launch ? launch.args : [],
shell: launch ? launch.shell : true,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
- 实例方法包装器 `shouldKillUnmanagedProcess` 相比 `shouldKillUnmanagedBackendProcess` 带来的价值有限。你可以在 `stopUnmanagedBackendByPort` 中直接内联它的调用,并把参数构造逻辑保持在本地,从而去掉一层间接调用:
```js
async stopUnmanagedBackendByPort() {
// ... existing checks ...
let backendConfig = null;
try {
backendConfig = this.getBackendConfig();
} catch (error) {
this.log(
`Failed to resolve backend config during unmanaged cleanup: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
const hasBackendConfig = backendConfig && typeof backendConfig === 'object';
if (!hasBackendConfig) {
this.log(
'Backend config is unavailable during unmanaged cleanup; falling back to image-name-only matching.',
);
}
const commandLineCache = new Map();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`,
);
continue;
}
const shouldKill = shouldKillUnmanagedBackendProcess({
pid,
processInfo,
backendConfig,
allowImageOnlyMatch: !hasBackendConfig,
commandLineCache,
spawnSync,
log: (message) => this.log(message),
fallbackCmdRaw: process.env.ASTRBOT_BACKEND_CMD || 'python.exe',
});
if (!shouldKill) {
continue;
}
// existing taskkill block...
}
}
```
这些调整在保留当前所有行为的同时,删减了一些让控制流更难理解的分层。
</issue_to_address>帮我变得更有用!请对每条评论点 👍 或 👎,我会根据这些反馈改进后续评审。
Original comment in English
Hey - I've found 1 issue, and left some high level feedback:
- The PostCSS
normalize-nested-type-selectorplugin indashboard/vite.config.tscurrently runs on all CSS; consider scoping it to only your own source (e.g. viainclude/excludeor selector heuristics) to avoid unexpectedly rewriting third‑party styles. - In the new desktop backend packaging flow,
runtime-layout-utils.resolveAndValidateRuntimeSourceonly checks for path overlap between runtime and output; you may also want to explicitly guard against pointingASTRBOT_DESKTOP_CPYTHON_HOMEat the project root (or other large trees) to prevent accidentally copying the entire repo into the packaged runtime.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The PostCSS `normalize-nested-type-selector` plugin in `dashboard/vite.config.ts` currently runs on all CSS; consider scoping it to only your own source (e.g. via `include`/`exclude` or selector heuristics) to avoid unexpectedly rewriting third‑party styles.
- In the new desktop backend packaging flow, `runtime-layout-utils.resolveAndValidateRuntimeSource` only checks for path overlap between runtime and output; you may also want to explicitly guard against pointing `ASTRBOT_DESKTOP_CPYTHON_HOME` at the project root (or other large trees) to prevent accidentally copying the entire repo into the packaged runtime.
## Individual Comments
### Comment 1
<location> `desktop/lib/backend-manager.js:132` </location>
<code_context>
return fs.existsSync(indexPath) ? candidate : null;
}
- getPackagedBackendPath() {
+ loadPackagedBackendState() {
if (!this.app.isPackaged) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the backend management flow by collapsing redundant helpers, keeping launch resolution branch-based, and inlining thin wrappers to reduce indirection.
- You can fold `loadPackagedBackendState` and `getPackagedBackendState` into a single lazy getter to reduce one layer of indirection without changing behavior:
```js
// replace both loadPackagedBackendState/getPackagedBackendState with:
getPackagedBackendState() {
if (!this.app.isPackaged) {
return null;
}
if (!this.packagedBackendState) {
this.packagedBackendState = resolvePackagedBackendState(
process.resourcesPath,
(message) => this.log(message),
);
}
return this.packagedBackendState;
}
```
- The split between `resolveLaunchCommand` and `resolveBackendConfig` could be simplified by making the launch resolution clearly branch-based and moving the packaged-specific logic into a focused helper. That keeps `resolveBackendConfig` linear while still isolating the “packaged” behavior:
```js
buildLaunchForPackagedBackend(packagedBackendState, webuiDir) {
if (!packagedBackendState?.ok || !packagedBackendState.config) {
return {
launch: null,
failureReason:
packagedBackendState?.failureReason ||
'Backend command is not configured.',
};
}
const { runtimePythonPath, launchScriptPath } = packagedBackendState.config;
const args = [launchScriptPath];
if (webuiDir) {
args.push('--webui-dir', webuiDir);
}
return {
launch: { cmd: runtimePythonPath, args, shell: false },
failureReason: null,
};
}
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
let launch = null;
let failureReason = null;
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
if (customCmd) {
launch = { cmd: customCmd, args: [], shell: true };
} else if (this.app.isPackaged) {
({ launch, failureReason } = this.buildLaunchForPackagedBackend(
this.getPackagedBackendState(),
webuiDir,
));
} else {
launch = this.buildDefaultBackendLaunch(webuiDir);
}
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
if (failureReason) {
this.log(failureReason);
}
this.backendConfig = {
cmd: launch ? launch.cmd : null,
args: launch ? launch.args : [],
shell: launch ? launch.shell : true,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
- The instance wrapper `shouldKillUnmanagedProcess` adds limited value on top of `shouldKillUnmanagedBackendProcess`. You can inline its use in `stopUnmanagedBackendByPort` and keep the argument shaping local, which removes one level of indirection:
```js
async stopUnmanagedBackendByPort() {
// ... existing checks ...
let backendConfig = null;
try {
backendConfig = this.getBackendConfig();
} catch (error) {
this.log(
`Failed to resolve backend config during unmanaged cleanup: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
const hasBackendConfig = backendConfig && typeof backendConfig === 'object';
if (!hasBackendConfig) {
this.log(
'Backend config is unavailable during unmanaged cleanup; falling back to image-name-only matching.',
);
}
const commandLineCache = new Map();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(
`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`,
);
continue;
}
const shouldKill = shouldKillUnmanagedBackendProcess({
pid,
processInfo,
backendConfig,
allowImageOnlyMatch: !hasBackendConfig,
commandLineCache,
spawnSync,
log: (message) => this.log(message),
fallbackCmdRaw: process.env.ASTRBOT_BACKEND_CMD || 'python.exe',
});
if (!shouldKill) {
continue;
}
// existing taskkill block...
}
}
```
These tweaks preserve all current behavior while trimming some of the layering that makes the control flow harder to follow.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了 1 个问题
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/scripts/runtime-version-utils.mjs:171` </location>
<code_context>
+ );
+};
+
+const runPythonProbe = ({ pythonExecutable, requirePipProbe }) => {
+ const probeScript = requirePipProbe
+ ? 'import sys, pip; print(sys.version_info[0], sys.version_info[1])'
</code_context>
<issue_to_address>
**issue (complexity):** 请考虑通过内联探测/解析辅助函数,并收窄支持的 `requires-python` 形式来简化运行时探测和版本规范解析,从而降低认知负担。
你可以在不改变行为的前提下,扁平化一些辅助函数的分层和版本处理的管道。
**1. 合并 `runPythonProbe` 和 `parseProbeVersion`**
这两个函数只在一起使用才有意义;将它们合并可以减少调用栈,并减少作为字符串在各处传递的数据量:
```js
const probePythonVersion = ({ pythonExecutable, requirePipProbe }) => {
const probeScript = requirePipProbe
? 'import sys, pip; print(sys.version_info[0], sys.version_info[1])'
: 'import sys; print(sys.version_info[0], sys.version_info[1])';
const probe = spawnSync(pythonExecutable, ['-c', probeScript], {
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
windowsHide: true,
timeout: 5000,
});
if (probe.error) {
const reason =
probe.error.code === 'ETIMEDOUT'
? 'runtime Python probe timed out'
: probe.error.message || String(probe.error);
throw new Error(`Runtime Python probe failed: ${reason}`);
}
if (probe.status !== 0) {
const stderrText = (probe.stderr || '').trim();
throw new Error(
`Runtime Python probe failed with exit code ${probe.status}. ` +
(requirePipProbe ? 'pip import check is enabled by ASTRBOT_DESKTOP_REQUIRE_PIP=1. ' : '') +
(stderrText ? `stderr: ${stderrText}` : ''),
);
}
const trimmedOutput = String(probe.stdout || '').trim();
const parts = trimmedOutput.split(/\s+/);
if (parts.length < 2) {
throw new Error(
`Runtime Python probe did not report a valid version. Output: ${trimmedOutput}`,
);
}
const major = Number.parseInt(parts[0], 10);
const minor = Number.parseInt(parts[1], 10);
if (!Number.isInteger(major) || !Number.isInteger(minor)) {
throw new Error(
`Runtime Python probe did not report a valid version. Output: ${trimmedOutput}`,
);
}
return { major, minor };
};
```
然后 `validateRuntimePython` 就会变得更简单:
```js
export const validateRuntimePython = ({
pythonExecutable,
expectedRuntimeConstraint,
requirePipProbe,
}) => {
const actualVersion = probePythonVersion({ pythonExecutable, requirePipProbe });
const { expectedRuntimeVersion, isLowerBoundRuntimeVersion, source } =
expectedRuntimeConstraint;
const compareResult = compareMajorMinor(actualVersion, expectedRuntimeVersion);
if (isLowerBoundRuntimeVersion) {
if (compareResult < 0) {
throw new Error(
`Runtime Python version is too low for ${source}: ` +
`expected >= ${expectedRuntimeVersion.major}.${expectedRuntimeVersion.minor}, ` +
`got ${actualVersion.major}.${actualVersion.minor}.`,
);
}
return;
}
if (compareResult !== 0) {
throw new Error(
`Runtime Python version mismatch for ${source}: ` +
`expected ${expectedRuntimeVersion.major}.${expectedRuntimeVersion.minor}, ` +
`got ${actualVersion.major}.${actualVersion.minor}.`,
);
}
};
```
这样可以移除一个导出的辅助函数以及一个基于字符串的中间结果。
**2. 可选地收窄 `extractLowerBoundFromPythonSpecifier`**
如果产品需求允许你只支持一小部分已文档化的 `requires-python` 形式(例如单一的 `>=` 子句),就可以用更简单的逻辑替换当前的自定义子句循环,并对其他情况快速失败:
```js
const extractLowerBoundFromPythonSpecifier = (rawSpecifier) => {
if (typeof rawSpecifier !== 'string') return null;
const m = /^\s*>=\s*(\d+)(?:\.(\d+))?\s*$/.exec(rawSpecifier);
if (!m) {
// Unsupported/complex specifier; caller can ignore and fall back to env
return null;
}
const major = Number.parseInt(m[1], 10);
const minor = Number.parseInt(m[2] ?? '0', 10);
return `${major}.${minor}`;
};
```
这样可以保留“下界”行为,但去掉对 `>`, `==`, `~=` 以及逗号分隔子句的自定义处理。如果你确实需要支持这些形式,那么你现在的实现是正确的;否则,这样可以减少认知负担和 PEP 440 的涉及面。
</issue_to_address>请帮我变得更有用!请对每条评论点 👍 或 👎,我会根据反馈改进后续的评审。
Original comment in English
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/scripts/runtime-version-utils.mjs:171` </location>
<code_context>
+ );
+};
+
+const runPythonProbe = ({ pythonExecutable, requirePipProbe }) => {
+ const probeScript = requirePipProbe
+ ? 'import sys, pip; print(sys.version_info[0], sys.version_info[1])'
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the runtime probing and version-spec parsing by inlining the probe/parse helpers and narrowing the supported `requires-python` forms to reduce cognitive load.
You can flatten some of the helper layering and version-handling plumbing without changing behavior.
**1. Combine `runPythonProbe` and `parseProbeVersion`**
These two are only meaningful together; combining them reduces the call stack and the amount of data passed around as strings:
```js
const probePythonVersion = ({ pythonExecutable, requirePipProbe }) => {
const probeScript = requirePipProbe
? 'import sys, pip; print(sys.version_info[0], sys.version_info[1])'
: 'import sys; print(sys.version_info[0], sys.version_info[1])';
const probe = spawnSync(pythonExecutable, ['-c', probeScript], {
stdio: ['ignore', 'pipe', 'pipe'],
encoding: 'utf8',
windowsHide: true,
timeout: 5000,
});
if (probe.error) {
const reason =
probe.error.code === 'ETIMEDOUT'
? 'runtime Python probe timed out'
: probe.error.message || String(probe.error);
throw new Error(`Runtime Python probe failed: ${reason}`);
}
if (probe.status !== 0) {
const stderrText = (probe.stderr || '').trim();
throw new Error(
`Runtime Python probe failed with exit code ${probe.status}. ` +
(requirePipProbe ? 'pip import check is enabled by ASTRBOT_DESKTOP_REQUIRE_PIP=1. ' : '') +
(stderrText ? `stderr: ${stderrText}` : ''),
);
}
const trimmedOutput = String(probe.stdout || '').trim();
const parts = trimmedOutput.split(/\s+/);
if (parts.length < 2) {
throw new Error(
`Runtime Python probe did not report a valid version. Output: ${trimmedOutput}`,
);
}
const major = Number.parseInt(parts[0], 10);
const minor = Number.parseInt(parts[1], 10);
if (!Number.isInteger(major) || !Number.isInteger(minor)) {
throw new Error(
`Runtime Python probe did not report a valid version. Output: ${trimmedOutput}`,
);
}
return { major, minor };
};
```
Then `validateRuntimePython` becomes simpler:
```js
export const validateRuntimePython = ({
pythonExecutable,
expectedRuntimeConstraint,
requirePipProbe,
}) => {
const actualVersion = probePythonVersion({ pythonExecutable, requirePipProbe });
const { expectedRuntimeVersion, isLowerBoundRuntimeVersion, source } =
expectedRuntimeConstraint;
const compareResult = compareMajorMinor(actualVersion, expectedRuntimeVersion);
if (isLowerBoundRuntimeVersion) {
if (compareResult < 0) {
throw new Error(
`Runtime Python version is too low for ${source}: ` +
`expected >= ${expectedRuntimeVersion.major}.${expectedRuntimeVersion.minor}, ` +
`got ${actualVersion.major}.${actualVersion.minor}.`,
);
}
return;
}
if (compareResult !== 0) {
throw new Error(
`Runtime Python version mismatch for ${source}: ` +
`expected ${expectedRuntimeVersion.major}.${expectedRuntimeVersion.minor}, ` +
`got ${actualVersion.major}.${actualVersion.minor}.`,
);
}
};
```
That removes one exported helper and a stringly-typed intermediate.
**2. Optionally narrow `extractLowerBoundFromPythonSpecifier`**
If product requirements allow you to only support a minimal, documented subset of `requires-python` forms (e.g. a single `>=` clause), you can replace the custom clause loop with something much simpler and fail-fast for others:
```js
const extractLowerBoundFromPythonSpecifier = (rawSpecifier) => {
if (typeof rawSpecifier !== 'string') return null;
const m = /^\s*>=\s*(\d+)(?:\.(\d+))?\s*$/.exec(rawSpecifier);
if (!m) {
// Unsupported/complex specifier; caller can ignore and fall back to env
return null;
}
const major = Number.parseInt(m[1], 10);
const minor = Number.parseInt(m[2] ?? '0', 10);
return `${major}.${minor}`;
};
```
This keeps the “lower bound” behavior but drops the custom handling of `>`, `==`, `~=`, and comma-separated clauses. If you need those forms, what you have is correct; otherwise, this reduces cognitive load and PEP 440 surface area.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
SourceryAI
left a comment
There was a problem hiding this comment.
Hey - 我已经审查了你的更改,一切看起来都很好!
Hi @zouyonghe! 👋
感谢你通过评论 @sourcery-ai review 来体验 Sourcery!🚀
安装 sourcery-ai 机器人,即可在每个拉取请求上获得自动代码审查 ✨
帮我变得更有用吧!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进我的审查。Original comment in English
Hey - I've reviewed your changes and they look great!
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.|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了 3 个问题
给 AI Agent 的提示
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/scripts/build-backend.mjs:42` </location>
<code_context>
- stdio: 'inherit',
- shell: process.platform === 'win32',
-});
+const resolveRuntimePythonOrThrow = ({ runtimeRoot, errorSubject }) => {
+ const runtimePython = resolveRuntimePython({
+ runtimeRoot,
</code_context>
<issue_to_address>
**issue (complexity):** 建议内联这个小的工具函数并简化 source entries 列表,以减少间接调用,让控制流更直接。
你可以在不损失结构和功能的情况下减少一些间接层级。
**1. 内联 `resolveRuntimePythonOrThrow`**
它只被使用了两次,而且只是给 `resolveRuntimePython` 包了一层错误信息。把检查逻辑直接内联到使用的地方,可以减少跳转:
```js
const main = () => {
const runtimeSourceReal = resolveAndValidateRuntimeSource({
rootDir,
outputDir,
runtimeSource,
});
const expectedRuntimeConstraint = resolveExpectedRuntimeVersion({ rootDir });
const sourceRuntimePython = resolveRuntimePython({
runtimeRoot: runtimeSourceReal,
outputDir,
});
if (!sourceRuntimePython) {
throw new Error(
`Cannot find Python executable in runtime source: ${runtimeSourceReal}. ` +
'Expected python under bin/ or Scripts/.',
);
}
validateRuntimePython({
pythonExecutable: sourceRuntimePython.absolute,
expectedRuntimeConstraint,
requirePipProbe,
});
prepareOutputDirs();
copyAppSources();
const runtimePython = prepareRuntimeExecutable(runtimeSourceReal);
writeLauncherScript();
writeRuntimeManifest(runtimePython);
console.log(`Prepared CPython backend runtime in ${outputDir}`);
console.log(`Runtime source: ${runtimeSourceReal}`);
console.log(`Python executable: ${runtimePython.relative}`);
};
```
然后在 `prepareRuntimeExecutable` 中:
```js
const prepareRuntimeExecutable = (runtimeSourceReal) => {
copyTree(runtimeSourceReal, runtimeDir, { dereference: true });
const runtimePython = resolveRuntimePython({
runtimeRoot: runtimeDir,
outputDir,
});
if (!runtimePython) {
throw new Error(
`Cannot find Python executable in runtime: ${runtimeDir}. ` +
'Expected python under bin/ or Scripts/.',
);
}
return runtimePython;
};
```
这样既能保持错误信息清晰,又能去掉一层间接调用。
**2. 在保持可重构性的前提下简化 `sourceEntries`**
目前 `[src, dest]` 的二元组抽象并没有带来太多收益,因为二者是相同的。在保留未来灵活性的同时,可以把循环逻辑简化一下:
```js
const sourceEntries = [
'astrbot',
'main.py',
'runtime_bootstrap.py',
'requirements.txt',
];
const copyAppSources = () => {
for (const relativePath of sourceEntries) {
const sourcePath = path.join(rootDir, relativePath);
const targetPath = path.join(appDir, relativePath);
if (!fs.existsSync(sourcePath)) {
throw new Error(`Backend source path does not exist: ${sourcePath}`);
}
copyTree(sourcePath, targetPath);
}
};
```
如果 / 当你以后需要路径映射时,可以再换回二元组结构,但在当前需求下这样会让意图更简单、更明显。
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:180` </location>
<code_context>
+ };
+ }
+
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
+ let launch = null;
</code_context>
<issue_to_address>
**issue (complexity):** 建议将 `resolveBackendConfig` 中关于 launch/failureReason 的分支逻辑提取到单独的 `resolveLaunchStrategy` 工具函数中,让它返回一个归一化的结果对象,从而保持主方法线性、易读。
你可以通过把关于 launch/failureReason 的所有分支逻辑封装到一个策略工具函数里,并返回一个归一化的结果,来降低 `resolveBackendConfig` 新增的复杂度。这样可以让 `resolveBackendConfig` 保持线性,并避免通过从 `buildLaunchForPackagedBackend` 解构来跨字段修改状态。
例如:
```js
// 新的 helper:集中所有“如何启动”的分支逻辑
resolveLaunchStrategy(webuiDir) {
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
if (customCmd) {
return {
launch: { cmd: customCmd, args: [], shell: true },
failureReason: null,
};
}
if (this.app.isPackaged) {
return this.buildLaunchForPackagedBackend(
this.getPackagedBackendState(),
webuiDir,
);
}
// 开发环境下的 `uv run` 回退
return {
launch: this.buildDefaultBackendLaunch(webuiDir),
failureReason: null,
};
}
```
然后 `resolveBackendConfig` 会变得更简单、更易读:
```js
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
const { launch, failureReason } = this.resolveLaunchStrategy(webuiDir);
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
if (failureReason) {
this.log(failureReason);
}
this.backendConfig = {
cmd: launch ? launch.cmd : null,
args: launch ? launch.args : [],
shell: launch ? launch.shell : true,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
功能保持不变(同样的优先级:环境变量 cmd → 打包后的后端 → 开发环境 `uv run`;同样的日志,同样的 `failureReason`),但所有分支逻辑都集中在一个地方,而 `resolveBackendConfig` 只需要消费一个类型清晰的 `{ launch, failureReason }` 结果。这也让单独测试该策略更容易。
</issue_to_address>
### Comment 3
<location> `desktop/lib/windows-backend-cleanup.js:20` </location>
<code_context>
+ return normalized === 'python.exe' || normalized === 'pythonw.exe' || normalized === 'py.exe';
+}
+
+function getWindowsProcessCommandLine({ pid, commandLineCache, spawnSync, log, timeoutMs }) {
+ const numericPid = Number.parseInt(`${pid}`, 10);
+ if (!Number.isInteger(numericPid)) {
</code_context>
<issue_to_address>
**issue (complexity):** 建议封装缓存键(cache key)的访问,并将 `shouldKillUnmanagedBackendProcess` 拆分成更小的工具函数,以在不改变行为的前提下降低认知负担。
你可以在不改变行为的前提下,通过以下方式降低认知负担:
1. 将魔法缓存键隐藏在小工具函数后面。
2. 将 `shouldKillUnmanagedBackendProcess` 拆分成更小的谓词函数。
### 1) 封装魔法缓存键
目前该模块的调用方需要了解 `COMMAND_LINE_QUERY_UNAVAILABLE_KEY` 和 `COMMAND_LINE_FALLBACK_LOGGED_KEY`。你可以继续使用普通的 `Map`,但把键的访问集中起来,让其他代码只处理布尔或值,而不是字符串键。
```js
const COMMAND_LINE_QUERY_UNAVAILABLE_KEY = '__command_line_query_unavailable__';
const COMMAND_LINE_FALLBACK_LOGGED_KEY = '__command_line_fallback_logged__';
function isQueryUnavailable(cache) {
return !!(cache && cache.get(COMMAND_LINE_QUERY_UNAVAILABLE_KEY));
}
function setQueryUnavailable(cache, value) {
if (cache) cache.set(COMMAND_LINE_QUERY_UNAVAILABLE_KEY, !!value);
}
function wasFallbackLogged(cache) {
return !!(cache && cache.get(COMMAND_LINE_FALLBACK_LOGGED_KEY));
}
function markFallbackLogged(cache) {
if (cache) cache.set(COMMAND_LINE_FALLBACK_LOGGED_KEY, true);
}
```
然后 `getWindowsProcessCommandLine` 和 `shouldKillUnmanagedBackendProcess` 会更易理解:
```js
function getWindowsProcessCommandLine({ pid, commandLineCache, spawnSync, log, timeoutMs }) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) {
return { commandLine: null, commandLineQueryUnavailable: false };
}
if (isQueryUnavailable(commandLineCache)) {
return { commandLine: null, commandLineQueryUnavailable: true };
}
if (commandLineCache && commandLineCache.has(numericPid)) {
return {
commandLine: commandLineCache.get(numericPid),
commandLineQueryUnavailable: false,
};
}
// ... 未修改的查询逻辑 ...
if (!hasAvailableShell) {
setQueryUnavailable(commandLineCache, true);
return { commandLine: null, commandLineQueryUnavailable: true };
}
if (commandLineCache) {
commandLineCache.set(numericPid, null);
setQueryUnavailable(commandLineCache, false);
}
return { commandLine: null, commandLineQueryUnavailable: false };
}
```
以及:
```js
if (!commandLine) {
if (commandLineQueryUnavailable) {
if (!wasFallbackLogged(commandLineCache)) {
markFallbackLogged(commandLineCache);
log(
'Neither powershell nor pwsh is available. ' +
'Falling back to image-name-only matching for generic Python backend cleanup.',
);
}
return true;
}
log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`);
return false;
}
```
这样可以保持缓存内部表示完全一致(仍然使用相同的键),但大多数代码不需要直接记住或处理这些键。
### 2) 将 `shouldKillUnmanagedBackendProcess` 拆分为多个谓词函数
当前函数混合了多个职责。你可以把镜像名称匹配和基于标记的决策拆分成小工具函数,从而让主流程更易阅读和测试。
```js
function getExpectedImageName(backendConfig, fallbackCmdRaw) {
const safeBackendConfig = backendConfig && typeof backendConfig === 'object' ? backendConfig : {};
const fallbackCmd = String(fallbackCmdRaw || 'python.exe').trim().split(/\s+/, 1)[0];
return path.basename(safeBackendConfig.cmd || fallbackCmd || 'python.exe').toLowerCase();
}
function matchesExpectedImage(processInfo, expectedImageName) {
return processInfo.imageName.toLowerCase() === expectedImageName;
}
function matchesBackendMarkers({ pid, backendConfig, commandLineCache, spawnSync, log }) {
const markers = buildBackendCommandLineMarkers(backendConfig);
if (!markers.length) {
log(`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`);
return false;
}
const { commandLine, commandLineQueryUnavailable } = getWindowsProcessCommandLine({
pid,
commandLineCache,
spawnSync,
log,
timeoutMs: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
});
if (!commandLine) {
if (commandLineQueryUnavailable) {
if (!wasFallbackLogged(commandLineCache)) {
markFallbackLogged(commandLineCache);
log(
'Neither powershell nor pwsh is available. ' +
'Falling back to image-name-only matching for generic Python backend cleanup.',
);
}
return true;
}
log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`);
return false;
}
const normalizedCommandLine = normalizeWindowsPathForMatch(commandLine);
const matched = markers.some(
(marker) => marker && normalizedCommandLine.includes(marker),
);
if (!matched) {
log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
}
return matched;
}
```
然后 `shouldKillUnmanagedBackendProcess` 可以写成:
```js
function shouldKillUnmanagedBackendProcess({
pid,
processInfo,
backendConfig,
allowImageOnlyMatch,
commandLineCache,
spawnSync,
log,
fallbackCmdRaw,
}) {
const expectedImageName = getExpectedImageName(backendConfig, fallbackCmdRaw);
if (!matchesExpectedImage(processInfo, expectedImageName)) {
log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
if (allowImageOnlyMatch || !isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
return matchesBackendMarkers({
pid,
backendConfig,
commandLineCache,
spawnSync,
log,
});
}
```
行为保持不变,但逻辑被拆分成更加聚焦的单元,更易于单独推理和测试。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据反馈改进后续的 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/scripts/build-backend.mjs:42` </location>
<code_context>
- stdio: 'inherit',
- shell: process.platform === 'win32',
-});
+const resolveRuntimePythonOrThrow = ({ runtimeRoot, errorSubject }) => {
+ const runtimePython = resolveRuntimePython({
+ runtimeRoot,
</code_context>
<issue_to_address>
**issue (complexity):** Consider inlining the small helper and simplifying the source entries list to reduce indirection and make the control flow more straightforward.
You can trim some indirection here without losing structure or functionality.
**1. Inline `resolveRuntimePythonOrThrow`**
It’s only used twice and just wraps `resolveRuntimePython` with an error message. Inlining the check where it’s used reduces jumping:
```js
const main = () => {
const runtimeSourceReal = resolveAndValidateRuntimeSource({
rootDir,
outputDir,
runtimeSource,
});
const expectedRuntimeConstraint = resolveExpectedRuntimeVersion({ rootDir });
const sourceRuntimePython = resolveRuntimePython({
runtimeRoot: runtimeSourceReal,
outputDir,
});
if (!sourceRuntimePython) {
throw new Error(
`Cannot find Python executable in runtime source: ${runtimeSourceReal}. ` +
'Expected python under bin/ or Scripts/.',
);
}
validateRuntimePython({
pythonExecutable: sourceRuntimePython.absolute,
expectedRuntimeConstraint,
requirePipProbe,
});
prepareOutputDirs();
copyAppSources();
const runtimePython = prepareRuntimeExecutable(runtimeSourceReal);
writeLauncherScript();
writeRuntimeManifest(runtimePython);
console.log(`Prepared CPython backend runtime in ${outputDir}`);
console.log(`Runtime source: ${runtimeSourceReal}`);
console.log(`Python executable: ${runtimePython.relative}`);
};
```
And in `prepareRuntimeExecutable`:
```js
const prepareRuntimeExecutable = (runtimeSourceReal) => {
copyTree(runtimeSourceReal, runtimeDir, { dereference: true });
const runtimePython = resolveRuntimePython({
runtimeRoot: runtimeDir,
outputDir,
});
if (!runtimePython) {
throw new Error(
`Cannot find Python executable in runtime: ${runtimeDir}. ` +
'Expected python under bin/ or Scripts/.',
);
}
return runtimePython;
};
```
This keeps the error messages clear but removes a level of indirection.
**2. Simplify `sourceEntries` while keeping refactorability**
Right now the `[src, dest]` pair abstraction isn’t buying much since they’re identical. You can keep future flexibility while simplifying the loop:
```js
const sourceEntries = [
'astrbot',
'main.py',
'runtime_bootstrap.py',
'requirements.txt',
];
const copyAppSources = () => {
for (const relativePath of sourceEntries) {
const sourcePath = path.join(rootDir, relativePath);
const targetPath = path.join(appDir, relativePath);
if (!fs.existsSync(sourcePath)) {
throw new Error(`Backend source path does not exist: ${sourcePath}`);
}
copyTree(sourcePath, targetPath);
}
};
```
If/when you need remapping, you can switch back to tuples, but this keeps the current intent simpler and more obvious.
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:180` </location>
<code_context>
+ };
+ }
+
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
+ let launch = null;
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the launch/failureReason branching in `resolveBackendConfig` into a separate `resolveLaunchStrategy` helper that returns a normalized result object to keep the main method linear and easier to read.
You can reduce the new complexity in `resolveBackendConfig` by encapsulating all branching around launch/failureReason into a dedicated strategy helper and returning a normalized result. This keeps `resolveBackendConfig` linear and avoids the cross-field mutation via destructuring from `buildLaunchForPackagedBackend`.
For example:
```js
// New helper: centralizes all "how do we launch" branching
resolveLaunchStrategy(webuiDir) {
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
if (customCmd) {
return {
launch: { cmd: customCmd, args: [], shell: true },
failureReason: null,
};
}
if (this.app.isPackaged) {
return this.buildLaunchForPackagedBackend(
this.getPackagedBackendState(),
webuiDir,
);
}
// dev `uv run` fallback
return {
launch: this.buildDefaultBackendLaunch(webuiDir),
failureReason: null,
};
}
```
Then `resolveBackendConfig` becomes simpler and easier to read:
```js
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
const { launch, failureReason } = this.resolveLaunchStrategy(webuiDir);
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
if (failureReason) {
this.log(failureReason);
}
this.backendConfig = {
cmd: launch ? launch.cmd : null,
args: launch ? launch.args : [],
shell: launch ? launch.shell : true,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
Functionality remains the same (same precedence: env cmd → packaged backend → dev `uv run`, same logging, same `failureReason`), but all the branching is localized in one place and `resolveBackendConfig` only consumes a clearly-typed `{ launch, failureReason }` result. This also makes it easier to test the strategy in isolation.
</issue_to_address>
### Comment 3
<location> `desktop/lib/windows-backend-cleanup.js:20` </location>
<code_context>
+ return normalized === 'python.exe' || normalized === 'pythonw.exe' || normalized === 'py.exe';
+}
+
+function getWindowsProcessCommandLine({ pid, commandLineCache, spawnSync, log, timeoutMs }) {
+ const numericPid = Number.parseInt(`${pid}`, 10);
+ if (!Number.isInteger(numericPid)) {
</code_context>
<issue_to_address>
**issue (complexity):** Consider encapsulating cache-key access and decomposing `shouldKillUnmanagedBackendProcess` into smaller helpers to lower cognitive overhead without changing behavior.
You can reduce the cognitive load without changing behavior by:
1. Hiding the magic cache keys behind small helpers.
2. Splitting `shouldKillUnmanagedBackendProcess` into smaller predicates.
### 1) Encapsulate magic cache keys
Right now callers in this module need to know about `COMMAND_LINE_QUERY_UNAVAILABLE_KEY` and `COMMAND_LINE_FALLBACK_LOGGED_KEY`. You can keep using a plain `Map` but centralize the key access so the rest of the code talks in booleans/values instead of string keys.
```js
const COMMAND_LINE_QUERY_UNAVAILABLE_KEY = '__command_line_query_unavailable__';
const COMMAND_LINE_FALLBACK_LOGGED_KEY = '__command_line_fallback_logged__';
function isQueryUnavailable(cache) {
return !!(cache && cache.get(COMMAND_LINE_QUERY_UNAVAILABLE_KEY));
}
function setQueryUnavailable(cache, value) {
if (cache) cache.set(COMMAND_LINE_QUERY_UNAVAILABLE_KEY, !!value);
}
function wasFallbackLogged(cache) {
return !!(cache && cache.get(COMMAND_LINE_FALLBACK_LOGGED_KEY));
}
function markFallbackLogged(cache) {
if (cache) cache.set(COMMAND_LINE_FALLBACK_LOGGED_KEY, true);
}
```
Then `getWindowsProcessCommandLine` and `shouldKillUnmanagedBackendProcess` become easier to follow:
```js
function getWindowsProcessCommandLine({ pid, commandLineCache, spawnSync, log, timeoutMs }) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) {
return { commandLine: null, commandLineQueryUnavailable: false };
}
if (isQueryUnavailable(commandLineCache)) {
return { commandLine: null, commandLineQueryUnavailable: true };
}
if (commandLineCache && commandLineCache.has(numericPid)) {
return {
commandLine: commandLineCache.get(numericPid),
commandLineQueryUnavailable: false,
};
}
// ... unchanged query logic ...
if (!hasAvailableShell) {
setQueryUnavailable(commandLineCache, true);
return { commandLine: null, commandLineQueryUnavailable: true };
}
if (commandLineCache) {
commandLineCache.set(numericPid, null);
setQueryUnavailable(commandLineCache, false);
}
return { commandLine: null, commandLineQueryUnavailable: false };
}
```
And:
```js
if (!commandLine) {
if (commandLineQueryUnavailable) {
if (!wasFallbackLogged(commandLineCache)) {
markFallbackLogged(commandLineCache);
log(
'Neither powershell nor pwsh is available. ' +
'Falling back to image-name-only matching for generic Python backend cleanup.',
);
}
return true;
}
log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`);
return false;
}
```
This keeps the cache representation identical (still using the same keys) but removes the need for most of the code to remember/handle those keys directly.
### 2) Split `shouldKillUnmanagedBackendProcess` into predicates
The current function mixes several responsibilities. You can factor out the image-name match and the marker-based decision into small helpers, making the main flow easier to read and test.
```js
function getExpectedImageName(backendConfig, fallbackCmdRaw) {
const safeBackendConfig = backendConfig && typeof backendConfig === 'object' ? backendConfig : {};
const fallbackCmd = String(fallbackCmdRaw || 'python.exe').trim().split(/\s+/, 1)[0];
return path.basename(safeBackendConfig.cmd || fallbackCmd || 'python.exe').toLowerCase();
}
function matchesExpectedImage(processInfo, expectedImageName) {
return processInfo.imageName.toLowerCase() === expectedImageName;
}
function matchesBackendMarkers({ pid, backendConfig, commandLineCache, spawnSync, log }) {
const markers = buildBackendCommandLineMarkers(backendConfig);
if (!markers.length) {
log(`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`);
return false;
}
const { commandLine, commandLineQueryUnavailable } = getWindowsProcessCommandLine({
pid,
commandLineCache,
spawnSync,
log,
timeoutMs: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
});
if (!commandLine) {
if (commandLineQueryUnavailable) {
if (!wasFallbackLogged(commandLineCache)) {
markFallbackLogged(commandLineCache);
log(
'Neither powershell nor pwsh is available. ' +
'Falling back to image-name-only matching for generic Python backend cleanup.',
);
}
return true;
}
log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`);
return false;
}
const normalizedCommandLine = normalizeWindowsPathForMatch(commandLine);
const matched = markers.some(
(marker) => marker && normalizedCommandLine.includes(marker),
);
if (!matched) {
log(
`Skip unmanaged cleanup for pid=${pid}: command line does not match AstrBot backend launch marker.`,
);
}
return matched;
}
```
Then `shouldKillUnmanagedBackendProcess` reads as:
```js
function shouldKillUnmanagedBackendProcess({
pid,
processInfo,
backendConfig,
allowImageOnlyMatch,
commandLineCache,
spawnSync,
log,
fallbackCmdRaw,
}) {
const expectedImageName = getExpectedImageName(backendConfig, fallbackCmdRaw);
if (!matchesExpectedImage(processInfo, expectedImageName)) {
log(
`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`,
);
return false;
}
if (allowImageOnlyMatch || !isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
return matchesBackendMarkers({
pid,
backendConfig,
commandLineCache,
spawnSync,
log,
});
}
```
Behavior remains the same, but the logic is decomposed into focused units that are easier to reason about and test individually.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了 2 个问题
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/scripts/read-requires-python.py:9-14` </location>
<code_context>
return plugin_info
except Exception:
- if cleanup_required and dir_name and plugin_path:
- await self._cleanup_failed_plugin_install(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 将 `pyproject.toml` 的解析失败默认为「没有 requires_python」会静默地隐藏配置错误。
当前解析失败会返回 `{"requires_python": null}` 且退出码为 0,这与一个合法但未声明 `project.requires-python` 字段的文件在行为上完全一致。这样会掩盖格式错误的 `pyproject.toml` 文件,并导致系统静默地退回到基于环境的配置。
请为解析错误添加一个清晰的信号(例如在 stderr 输出一条简短消息,或者在 JSON 中额外加入 `{"requires_python": null, "error": "parse_failed"}` 这样的字段),以便调用方能够区分「解析失败」和「字段确实不存在」。
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:202` </location>
<code_context>
+ };
+ }
+
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
- const customCmd = process.env.ASTRBOT_BACKEND_CMD;
</code_context>
<issue_to_address>
**issue (complexity):** 建议将启动策略内联进 `resolveBackendConfig`,并将非托管清理时的上下文构建抽取到一个辅助函数中,以简化控制流并减少间接层级。
你可以在不改变行为的前提下简化两个部分:启动方式解析,以及 Windows 上的非托管清理逻辑。
### 1. 将 `resolveLaunchStrategy` 折叠进 `resolveBackendConfig`
目前,启动解析逻辑分散在 `buildDefaultBackendLaunch`、`buildLaunchForPackagedBackend` 和 `resolveLaunchStrategy` 中,而 `{ launch, failureReason }` 这个封装只在一个地方被使用。
你可以把策略逻辑内联进 `resolveBackendConfig`,并完全移除 `resolveLaunchStrategy` / `buildLaunchForPackagedBackend`,只保留 `buildDefaultBackendLaunch` 作为一个小而纯的辅助函数:
```js
// remove resolveLaunchStrategy + buildLaunchForPackagedBackend
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
let launch = null;
let failureReason = null;
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
if (customCmd) {
launch = { cmd: customCmd, args: [], shell: true };
} else if (this.app.isPackaged) {
const packagedBackendState = this.getPackagedBackendState();
if (!packagedBackendState?.ok || !packagedBackendState.config) {
failureReason =
packagedBackendState?.failureReason ||
'Backend command is not configured.';
} else {
const { runtimePythonPath, launchScriptPath } = packagedBackendState.config;
const args = [launchScriptPath];
if (webuiDir) {
args.push('--webui-dir', webuiDir);
}
launch = { cmd: runtimePythonPath, args, shell: false };
}
} else {
launch = this.buildDefaultBackendLaunch(webuiDir);
}
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
if (failureReason) {
this.log(failureReason);
}
this.backendConfig = {
cmd: launch ? launch.cmd : null,
args: launch ? launch.args : [],
shell: launch ? launch.shell : true,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
这样可以保留所有原有行为(自定义命令、打包清单、失败信息),但让 `resolveBackendConfig` 成为唯一决定 `cmd/args/failureReason` 的地方,并去掉额外的 `{ launch, failureReason }` 传递管线。
### 2. 让 `cleanupUnmanagedBackendOnWindows` 呈线性结构,并把细节推到一个小的辅助函数里
当前实现把以下几个方面混杂在一起:
- 对 `getBackendConfig()` 的防御性处理;
- 何时退回到仅基于镜像名匹配的决策;
- 为 `shouldKillUnmanagedBackendProcess` 组装庞大的 options 对象。
你可以把「上下文构建」封装进一个小的辅助函数,使主方法更易于阅读:
```js
// new helper
buildUnmanagedCleanupContext() {
let backendConfig = null;
try {
backendConfig = this.getBackendConfig();
} catch (error) {
this.log(
`Failed to resolve backend config during unmanaged cleanup: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
const hasBackendConfig = backendConfig && typeof backendConfig === 'object';
if (!hasBackendConfig) {
this.log(
'Backend config is unavailable during unmanaged cleanup; ' +
'falling back to image-name-only matching.',
);
}
return {
backendConfig,
allowImageOnlyMatch: !hasBackendConfig,
commandLineCache: new Map(),
fallbackCmdRaw: process.env.ASTRBOT_BACKEND_CMD || 'python.exe',
};
}
```
然后 `cleanupUnmanagedBackendOnWindows` 基本上就只剩下循环和调用了:
```js
cleanupUnmanagedBackendOnWindows(port) {
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) {
return false;
}
this.log(`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`);
const context = this.buildUnmanagedCleanupContext();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`);
continue;
}
const shouldKill = shouldKillUnmanagedBackendProcess({
pid,
processInfo,
spawnSync,
log: (message) => this.log(message),
...context,
});
if (!shouldKill) {
continue;
}
try {
spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
this.log(`Successfully killed unmanaged backend pid=${pid}.`);
} catch (error) {
this.log(`Failed unmanaged cleanup for pid=${pid}: ${error instanceof Error ? error.message : String(error)}`);
}
}
return true;
}
```
在这种重构下,行为保持不变,但 `cleanupUnmanagedBackendOnWindows` 的控制流变得线性,「做什么」(遍历并视情况 kill)与「怎么做」(构建匹配所需的上下文)清晰分离,后者集中在一个相对纯粹的辅助函数中。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的审查意见。
Original comment in English
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/scripts/read-requires-python.py:9-14` </location>
<code_context>
return plugin_info
except Exception:
- if cleanup_required and dir_name and plugin_path:
- await self._cleanup_failed_plugin_install(
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Silently treating pyproject.toml parse failures as "no requires_python" may hide configuration errors.
A parse failure currently results in `{"requires_python": null}` with exit code 0, which is indistinguishable from a valid file that simply omits `project.requires-python`. This can mask malformed `pyproject.toml` files and cause silent fallback to environment-based configuration.
Please add a clear signal for parse errors (e.g., a short stderr message or an extra JSON field like `{"requires_python": null, "error": "parse_failed"}`) so callers can distinguish failures from genuine absence of the field.
</issue_to_address>
### Comment 2
<location> `desktop/lib/backend-manager.js:202` </location>
<code_context>
+ };
+ }
+
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
- const customCmd = process.env.ASTRBOT_BACKEND_CMD;
</code_context>
<issue_to_address>
**issue (complexity):** Consider inlining launch strategy into `resolveBackendConfig` and extracting unmanaged cleanup context-building into a helper to streamline control flow and reduce indirection.
You can simplify two areas without changing behavior: launch resolution and unmanaged cleanup on Windows.
### 1. Collapse `resolveLaunchStrategy` into `resolveBackendConfig`
Right now launch resolution is spread across `buildDefaultBackendLaunch`, `buildLaunchForPackagedBackend`, and `resolveLaunchStrategy`, and the `{ launch, failureReason }` wrapper is only used once.
You can inline the strategy logic into `resolveBackendConfig` and drop `resolveLaunchStrategy` / `buildLaunchForPackagedBackend` entirely, keeping `buildDefaultBackendLaunch` as a small pure helper:
```js
// remove resolveLaunchStrategy + buildLaunchForPackagedBackend
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
let launch = null;
let failureReason = null;
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
if (customCmd) {
launch = { cmd: customCmd, args: [], shell: true };
} else if (this.app.isPackaged) {
const packagedBackendState = this.getPackagedBackendState();
if (!packagedBackendState?.ok || !packagedBackendState.config) {
failureReason =
packagedBackendState?.failureReason ||
'Backend command is not configured.';
} else {
const { runtimePythonPath, launchScriptPath } = packagedBackendState.config;
const args = [launchScriptPath];
if (webuiDir) {
args.push('--webui-dir', webuiDir);
}
launch = { cmd: runtimePythonPath, args, shell: false };
}
} else {
launch = this.buildDefaultBackendLaunch(webuiDir);
}
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
if (failureReason) {
this.log(failureReason);
}
this.backendConfig = {
cmd: launch ? launch.cmd : null,
args: launch ? launch.args : [],
shell: launch ? launch.shell : true,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
This keeps all behavior (custom command, packaged manifest, failure messages) but makes `resolveBackendConfig` the single place that decides `cmd/args/failureReason`, and removes the extra `{ launch, failureReason }` plumbing.
### 2. Make `cleanupUnmanagedBackendOnWindows` linear and push detail into a small helper
The current implementation mixes:
- defensive `getBackendConfig()` handling,
- decision of when to fall back to image-only matching, and
- assembling the large options object for `shouldKillUnmanagedBackendProcess`.
You can encapsulate the “context building” in a small helper to make the main method easier to scan:
```js
// new helper
buildUnmanagedCleanupContext() {
let backendConfig = null;
try {
backendConfig = this.getBackendConfig();
} catch (error) {
this.log(
`Failed to resolve backend config during unmanaged cleanup: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
const hasBackendConfig = backendConfig && typeof backendConfig === 'object';
if (!hasBackendConfig) {
this.log(
'Backend config is unavailable during unmanaged cleanup; ' +
'falling back to image-name-only matching.',
);
}
return {
backendConfig,
allowImageOnlyMatch: !hasBackendConfig,
commandLineCache: new Map(),
fallbackCmdRaw: process.env.ASTRBOT_BACKEND_CMD || 'python.exe',
};
}
```
Then `cleanupUnmanagedBackendOnWindows` becomes mostly the loop + call:
```js
cleanupUnmanagedBackendOnWindows(port) {
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) {
return false;
}
this.log(`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`);
const context = this.buildUnmanagedCleanupContext();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`);
continue;
}
const shouldKill = shouldKillUnmanagedBackendProcess({
pid,
processInfo,
spawnSync,
log: (message) => this.log(message),
...context,
});
if (!shouldKill) {
continue;
}
try {
spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
this.log(`Successfully killed unmanaged backend pid=${pid}.`);
} catch (error) {
this.log(`Failed unmanaged cleanup for pid=${pid}: ${error instanceof Error ? error.message : String(error)}`);
}
}
return true;
}
```
Behavior stays the same, but the control flow in `cleanupUnmanagedBackendOnWindows` is now linear and the “what” (iterate and maybe kill) is clearly separated from the “how” (building context for matching), which lives in a single pure-ish helper.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了 1 个问题
给 AI 代理的提示词
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/lib/backend-manager.js:649` </location>
<code_context>
return { imageName, pid: parsedPid };
}
+ buildUnmanagedCleanupContext() {
+ let backendConfig = null;
+ try {
</code_context>
<issue_to_address>
**issue (complexity):** 考虑通过内联一次性使用的上下文 helper,并将“纯配置构建”与“包含副作用的解析和缓存”分离,来简化新的后端配置和非托管清理逻辑。
你可以在不改变行为的前提下,通过收紧几个抽象层级来降低新增的复杂度。
### 1. 内联非托管清理上下文(单一调用点)
`buildUnmanagedCleanupContext` 新增了一层只在一个地方使用的封装,迫使读者在代码间来回跳转。你可以保持相同逻辑,把它内联到 `stopUnmanagedBackendByPort` 中,只向 `shouldKillUnmanagedBackendProcess` 传入一个更精简的对象。
当前实现:
```js
buildUnmanagedCleanupContext() {
let backendConfig = null;
try {
backendConfig = this.getBackendConfig();
} catch (error) {
this.log(
`Failed to resolve backend config during unmanaged cleanup: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
const hasBackendConfig = backendConfig && typeof backendConfig === 'object';
if (!hasBackendConfig) {
this.log(
'Backend config is unavailable during unmanaged cleanup; falling back to image-name-only matching.',
);
}
return {
backendConfig,
allowImageOnlyMatch: !hasBackendConfig,
commandLineCache: new Map(),
fallbackCmdRaw: process.env.ASTRBOT_BACKEND_CMD || 'python.exe',
};
}
async stopUnmanagedBackendByPort() {
// ...
const context = this.buildUnmanagedCleanupContext();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
// ...
const shouldKill = shouldKillUnmanagedBackendProcess({
pid,
processInfo,
...context,
spawnSync,
log: (message) => this.log(message),
});
// ...
}
}
```
建议的简化方式:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') {
return false;
}
const port = this.getBackendPort();
if (!port) {
return false;
}
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) {
return false;
}
this.log(
`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`,
);
let backendConfig = null;
try {
backendConfig = this.getBackendConfig();
} catch (error) {
this.log(
`Failed to resolve backend config during unmanaged cleanup: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
const hasBackendConfig = backendConfig && typeof backendConfig === 'object';
if (!hasBackendConfig) {
this.log(
'Backend config is unavailable during unmanaged cleanup; falling back to image-name-only matching.',
);
}
const contextBase = {
backendConfig,
allowImageOnlyMatch: !hasBackendConfig,
commandLineCache: new Map(),
fallbackCmdRaw: process.env.ASTRBOT_BACKEND_CMD || 'python.exe',
spawnSync,
log: (message) => this.log(message),
};
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`);
continue;
}
const shouldKill = shouldKillUnmanagedBackendProcess({
pid,
processInfo,
...contextBase,
});
if (!shouldKill) {
continue;
}
try {
spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
this.log(`Successfully killed unmanaged backend pid=${pid}.`);
} catch (error) {
this.log(
`Failed to kill unmanaged backend pid=${pid}: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
return true;
}
```
这样可以保持原有行为(日志、回退逻辑、缓存),但去掉了只被构造一次的额外 helper 和“上下文”对象。
---
### 2. 拆分 `resolveBackendConfig` 的职责
`resolveBackendConfig` 现在负责:环境变量覆盖解析、已打包后端解析、启动配置构建、记录失败原因、目录创建以及状态更新。你可以把“构建启动配置”和“产生副作用 + 缓存”分开,使逻辑更容易理解。
当前实现:
```js
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
let launch = null;
let failureReason = null;
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
if (customCmd) {
launch = { cmd: customCmd, args: [], shell: true };
} else if (this.app.isPackaged) {
({ launch, failureReason } = this.buildLaunchForPackagedBackend(
this.getPackagedBackendState(),
webuiDir,
));
} else {
launch = this.buildDefaultBackendLaunch(webuiDir);
}
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
if (failureReason) {
this.log(failureReason);
}
this.backendConfig = {
cmd: launch ? launch.cmd : null,
args: launch ? launch.args : [],
shell: launch ? launch.shell : true,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
建议拆分为(纯“构建” + “应用副作用并缓存”):
```js
buildBackendConfig() {
const webuiDir = this.resolveWebuiDir();
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
let launch = null;
let failureReason = null;
if (customCmd) {
launch = { cmd: customCmd, args: [], shell: true };
} else if (this.app.isPackaged) {
({ launch, failureReason } = this.buildLaunchForPackagedBackend(
this.getPackagedBackendState(),
webuiDir,
));
} else {
launch = this.buildDefaultBackendLaunch(webuiDir);
}
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
return {
launch,
cwd,
rootDir,
webuiDir,
failureReason,
};
}
resolveBackendConfig() {
const { launch, cwd, rootDir, webuiDir, failureReason } =
this.buildBackendConfig();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
if (failureReason) {
this.log(failureReason);
}
this.backendConfig = {
cmd: launch ? launch.cmd : null,
args: launch ? launch.args : [],
shell: launch ? launch.shell : true,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
这样既保留了 `backendConfig` 和 `failureReason` 的缓存行为,又让“配置是什么”这部分逻辑在没有副作用干扰的情况下更易于测试和阅读。
</issue_to_address>帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/lib/backend-manager.js:649` </location>
<code_context>
return { imageName, pid: parsedPid };
}
+ buildUnmanagedCleanupContext() {
+ let backendConfig = null;
+ try {
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the new backend config and unmanaged cleanup logic by inlining the one‑off context helper and separating pure config construction from side‑effectful resolution and caching.
You can reduce the new complexity without changing behavior by tightening a couple of the abstractions.
### 1. Inline the unmanaged cleanup context (single call site)
`buildUnmanagedCleanupContext` adds a layer that’s only used once and forces readers to jump around. You can keep the same logic but inline it into `stopUnmanagedBackendByPort` and pass a narrow object into `shouldKillUnmanagedBackendProcess`.
Current:
```js
buildUnmanagedCleanupContext() {
let backendConfig = null;
try {
backendConfig = this.getBackendConfig();
} catch (error) {
this.log(
`Failed to resolve backend config during unmanaged cleanup: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
const hasBackendConfig = backendConfig && typeof backendConfig === 'object';
if (!hasBackendConfig) {
this.log(
'Backend config is unavailable during unmanaged cleanup; falling back to image-name-only matching.',
);
}
return {
backendConfig,
allowImageOnlyMatch: !hasBackendConfig,
commandLineCache: new Map(),
fallbackCmdRaw: process.env.ASTRBOT_BACKEND_CMD || 'python.exe',
};
}
async stopUnmanagedBackendByPort() {
// ...
const context = this.buildUnmanagedCleanupContext();
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
// ...
const shouldKill = shouldKillUnmanagedBackendProcess({
pid,
processInfo,
...context,
spawnSync,
log: (message) => this.log(message),
});
// ...
}
}
```
Suggested simplification:
```js
async stopUnmanagedBackendByPort() {
if (!this.app.isPackaged || process.platform !== 'win32') {
return false;
}
const port = this.getBackendPort();
if (!port) {
return false;
}
const pids = this.findListeningPidsOnWindows(port);
if (!pids.length) {
return false;
}
this.log(
`Attempting unmanaged backend cleanup by port=${port} pids=${pids.join(',')}`,
);
let backendConfig = null;
try {
backendConfig = this.getBackendConfig();
} catch (error) {
this.log(
`Failed to resolve backend config during unmanaged cleanup: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
const hasBackendConfig = backendConfig && typeof backendConfig === 'object';
if (!hasBackendConfig) {
this.log(
'Backend config is unavailable during unmanaged cleanup; falling back to image-name-only matching.',
);
}
const contextBase = {
backendConfig,
allowImageOnlyMatch: !hasBackendConfig,
commandLineCache: new Map(),
fallbackCmdRaw: process.env.ASTRBOT_BACKEND_CMD || 'python.exe',
spawnSync,
log: (message) => this.log(message),
};
for (const pid of pids) {
const processInfo = this.getWindowsProcessInfo(pid);
if (!processInfo) {
this.log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process info.`);
continue;
}
const shouldKill = shouldKillUnmanagedBackendProcess({
pid,
processInfo,
...contextBase,
});
if (!shouldKill) {
continue;
}
try {
spawnSync('taskkill', ['/pid', `${pid}`, '/t', '/f'], {
stdio: 'ignore',
windowsHide: true,
});
this.log(`Successfully killed unmanaged backend pid=${pid}.`);
} catch (error) {
this.log(
`Failed to kill unmanaged backend pid=${pid}: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
return true;
}
```
This keeps the behavior (logging, fallbacks, caching) but removes an extra helper and “context” object that’s only ever constructed once.
---
### 2. Split `resolveBackendConfig` responsibilities
`resolveBackendConfig` is now doing: env override resolution, packaged backend resolution, launch construction, logging failure reason, directory creation, and state update. You can separate “build launch config” from “side effects + caching” to make it easier to follow.
Current:
```js
resolveBackendConfig() {
const webuiDir = this.resolveWebuiDir();
let launch = null;
let failureReason = null;
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
if (customCmd) {
launch = { cmd: customCmd, args: [], shell: true };
} else if (this.app.isPackaged) {
({ launch, failureReason } = this.buildLaunchForPackagedBackend(
this.getPackagedBackendState(),
webuiDir,
));
} else {
launch = this.buildDefaultBackendLaunch(webuiDir);
}
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
if (failureReason) {
this.log(failureReason);
}
this.backendConfig = {
cmd: launch ? launch.cmd : null,
args: launch ? launch.args : [],
shell: launch ? launch.shell : true,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
Suggested split (pure “build” + “apply side effects and cache”):
```js
buildBackendConfig() {
const webuiDir = this.resolveWebuiDir();
const customCmd = process.env.ASTRBOT_BACKEND_CMD;
let launch = null;
let failureReason = null;
if (customCmd) {
launch = { cmd: customCmd, args: [], shell: true };
} else if (this.app.isPackaged) {
({ launch, failureReason } = this.buildLaunchForPackagedBackend(
this.getPackagedBackendState(),
webuiDir,
));
} else {
launch = this.buildDefaultBackendLaunch(webuiDir);
}
const cwd = process.env.ASTRBOT_BACKEND_CWD || this.resolveBackendCwd();
const rootDir = process.env.ASTRBOT_ROOT || this.resolveBackendRoot();
return {
launch,
cwd,
rootDir,
webuiDir,
failureReason,
};
}
resolveBackendConfig() {
const { launch, cwd, rootDir, webuiDir, failureReason } =
this.buildBackendConfig();
ensureDir(cwd);
if (rootDir) {
ensureDir(rootDir);
}
if (failureReason) {
this.log(failureReason);
}
this.backendConfig = {
cmd: launch ? launch.cmd : null,
args: launch ? launch.args : [],
shell: launch ? launch.shell : true,
cwd,
webuiDir,
rootDir,
failureReason,
};
return this.backendConfig;
}
```
This keeps the cached `backendConfig` and `failureReason` behavior intact, but makes the “what is the config?” logic testable and readable without the side effects mixed in.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
SourceryAI
left a comment
There was a problem hiding this comment.
Hey - 我发现了 1 个问题
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/lib/windows-backend-cleanup.js:6` </location>
<code_context>
+const path = require('path');
+
+const WINDOWS_PROCESS_QUERY_TIMEOUT_MS = 2000;
+const COMMAND_LINE_QUERY_UNAVAILABLE_KEY = '__command_line_query_unavailable__';
+const COMMAND_LINE_FALLBACK_LOGGED_KEY = '__command_line_fallback_logged__';
+
</code_context>
<issue_to_address>
**issue (complexity):** 考虑重构这个模块,使用一个显式的上下文对象、更简单的命令行探测辅助函数,以及单一的后端标记,从而避免“魔法”的缓存键和嵌套分支。
在保持现有行为不变的前提下,你可以去掉大量间接层和隐式状态编码。具体来说:
### 1. 用显式字段替代“魔法”缓存键
与其把 `__command_line_query_unavailable__` / `__command_line_fallback_logged__` 存在和 PID 相同的 `Map` 中,不如让缓存保持纯粹的 `pid → commandLine` 映射,并在上下文对象上增加显式的标志位。
**之前(简化版):**
```js
const COMMAND_LINE_QUERY_UNAVAILABLE_KEY = '__command_line_query_unavailable__';
const COMMAND_LINE_FALLBACK_LOGGED_KEY = '__command_line_fallback_logged__';
function isQueryUnavailable(cache) {
return !!(cache && cache.get(COMMAND_LINE_QUERY_UNAVAILABLE_KEY));
}
function setQueryUnavailable(cache, value) {
if (cache) {
cache.set(COMMAND_LINE_QUERY_UNAVAILABLE_KEY, !!value);
}
}
function wasFallbackLogged(cache) {
return !!(cache && cache.get(COMMAND_LINE_FALLBACK_LOGGED_KEY));
}
function markFallbackLogged(cache) {
if (cache) {
cache.set(COMMAND_LINE_FALLBACK_LOGGED_KEY, true);
}
}
```
**之后:**
在上下文中添加显式字段,并保持缓存为 `Map<number, string | null>`:
```js
// context builder
function buildUnmanagedCleanupContext(backendConfig, fallbackCmdRaw) {
return {
backendConfig,
expectedImageName: getExpectedImageName(backendConfig, fallbackCmdRaw),
commandLineCache: new Map(), // pid -> string|null
commandLineQueryUnavailable: false, // globally no powershell/pwsh
fallbackLogged: false, // logged image-only fallback
};
}
```
在辅助函数中使用这些字段,而不是“魔法”键:
```js
function getWindowsProcessCommandLine({ pid, ctx, spawnSync, log, timeoutMs }) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) {
return null;
}
if (ctx.commandLineQueryUnavailable) {
return null;
}
if (ctx.commandLineCache.has(numericPid)) {
return ctx.commandLineCache.get(numericPid);
}
// ... spawn powershell/pwsh ...
// on success:
ctx.commandLineCache.set(numericPid, commandLine);
return commandLine;
// on ENOENT for all shells:
ctx.commandLineQueryUnavailable = true;
return null;
// on other failures:
ctx.commandLineCache.set(numericPid, null);
return null;
}
```
以及:
```js
if (!commandLine && ctx.commandLineQueryUnavailable) {
if (!ctx.fallbackLogged) {
ctx.fallbackLogged = true;
log('Neither powershell nor pwsh is available. Falling back to image-name-only matching...');
}
return true; // fallback path
}
```
这样可以移除 `COMMAND_LINE_*` 常量以及 `isQueryUnavailable` / `setQueryUnavailable` / `wasFallbackLogged` / `markFallbackLogged` 这些函数,并让缓存状态更易于理解。
---
### 2. 简化命令行查询逻辑
在保持相同语义——“先尝试 powershell,再尝试 pwsh,将 ENOENT 视为不可用,记录超时/其他错误”的前提下,你可以通过一个辅助函数折叠循环,从而减少分支。
**之前(核心结构):**
```js
const queryAttempts = ['powershell', 'pwsh'];
let hasAvailableShell = false;
for (const shellName of queryAttempts) {
let result = null;
try {
result = spawnSync(shellName, args, options);
} catch (error) {
// log and continue
}
if (result.error && result.error.code === 'ENOENT') {
continue;
}
hasAvailableShell = true;
if (result.error && result.error.code === 'ETIMEDOUT') {
// log and continue
}
if (result.error) {
// log and continue
}
if (result.status === 0) {
// parse stdout, cache, setQueryUnavailable(false)
return { commandLine, commandLineQueryUnavailable: false };
}
}
if (!hasAvailableShell) {
setQueryUnavailable(commandLineCache, true);
return { commandLine: null, commandLineQueryUnavailable: true };
}
// cache null, setQueryUnavailable(false)
```
**之后(相同行为,更小的表面面积):**
```js
function tryShellForCommandLine(shellName, args, options, log, pid, timeoutMs) {
let result;
try {
result = spawnSync(shellName, args, options);
} catch (error) {
if (error instanceof Error && error.message) {
log(`Failed to query process command line via ${shellName} for pid=${pid}: ${error.message}`);
}
return { available: false, commandLine: null };
}
if (result.error?.code === 'ENOENT') {
return { available: false, commandLine: null };
}
if (result.error?.code === 'ETIMEDOUT') {
log(`Timed out (${timeoutMs}ms) querying process command line via ${shellName} for pid=${pid}.`);
return { available: true, commandLine: null };
}
if (result.error?.message) {
log(`Failed to query process command line via ${shellName} for pid=${pid}: ${result.error.message}`);
return { available: true, commandLine: null };
}
if (result.status === 0) {
const commandLine =
result.stdout
.split(/\r?\n/)
.map((item) => item.trim())
.find((item) => item.length > 0) || null;
return { available: true, commandLine };
}
return { available: true, commandLine: null };
}
function getWindowsProcessCommandLine({ pid, ctx, spawnSync, log, timeoutMs }) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid) || ctx.commandLineQueryUnavailable) {
return null;
}
if (ctx.commandLineCache.has(numericPid)) {
return ctx.commandLineCache.get(numericPid);
}
const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${numericPid}"; if ($null -ne $p) { $p.CommandLine }`;
const args = ['-NoProfile', '-NonInteractive', '-Command', query];
const options = { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', windowsHide: true, timeout: timeoutMs };
let anyAvailable = false;
for (const shell of ['powershell', 'pwsh']) {
const { available, commandLine } = tryShellForCommandLine(shell, args, options, log, numericPid, timeoutMs);
anyAvailable ||= available;
if (commandLine !== null) {
ctx.commandLineCache.set(numericPid, commandLine);
return commandLine;
}
}
if (!anyAvailable) {
ctx.commandLineQueryUnavailable = true;
} else {
ctx.commandLineCache.set(numericPid, null);
}
return null;
}
```
这样可以保留所有功能边界条件(ENOENT ⇒ 不可用、超时日志、按 shell 的错误处理、按 pid 缓存),但去掉了 `{ commandLine, commandLineQueryUnavailable }` 这个二元组以及相关的哨兵更新逻辑。
---
### 3. 将标记逻辑收窄为单一稳定标记
与其返回两个标记(绝对路径 + 基名)并对二者分别做 `includes` 检查,你可以用解析后的主参数作为单一标记。这样既保持了严格的匹配语义,又简化了推理。
**之前:**
```js
function buildBackendCommandLineMarkers(backendConfig) {
// ... validation ...
const resolvedPrimaryArg = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(safeBackendConfig.cwd || process.cwd(), primaryArg);
return [
normalizeWindowsPathForMatch(resolvedPrimaryArg),
normalizeWindowsPathForMatch(path.basename(primaryArg)),
];
}
const matched = markers.some((marker) => marker && normalizedCommandLine.includes(marker));
```
**之后:**
```js
function buildBackendCommandLineMarker(backendConfig) {
const cfg = backendConfig && typeof backendConfig === 'object' ? backendConfig : {};
if (!Array.isArray(cfg.args) || cfg.args.length === 0) {
return null;
}
const primaryArg = cfg.args[0];
if (typeof primaryArg !== 'string' || !primaryArg) {
return null;
}
const resolved = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(cfg.cwd || process.cwd(), primaryArg);
return normalizeWindowsPathForMatch(resolved);
}
function matchesBackendMarker({ pid, backendConfig, ctx, spawnSync, log }) {
const marker = buildBackendCommandLineMarker(backendConfig);
if (!marker) {
log(`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`);
return false;
}
const commandLine = getWindowsProcessCommandLine({
pid,
ctx,
spawnSync,
log,
timeoutMs: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
});
if (!commandLine) {
if (ctx.commandLineQueryUnavailable) {
if (!ctx.fallbackLogged) {
ctx.fallbackLogged = true;
log('Neither powershell nor pwsh is available. Falling back to image-name-only matching for generic Python backend cleanup.');
}
return true; // image-only fallback
}
log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`);
return false;
}
const normalizedCommandLine = normalizeWindowsPathForMatch(commandLine);
const matched = normalizedCommandLine.includes(marker);
if (!matched) {
log(`Skip unmanaged cleanup for pid=${pid}: command line does not match backend launch marker.`);
}
return matched;
}
```
这消除了标记数组和 `some()` 的调用,同时仍然使用基于绝对路径的强标识来识别后端进程。
---
### 4. 简化通用镜像逻辑
你可以去掉 `allowImageOnlyMatch` 和 `isGenericWindowsPythonImage`,改用更简单的规则:当可以获取命令行时,始终同时要求满足镜像和标记;如果命令行在全局范围内不可用,则回退到仅基于镜像匹配(并进行一次性日志记录)。
**之前:**
```js
function shouldKillUnmanagedBackendProcess({
pid,
processInfo,
backendConfig,
allowImageOnlyMatch,
commandLineCache,
spawnSync,
log,
fallbackCmdRaw,
}) {
const expectedImageName = getExpectedImageName(backendConfig, fallbackCmdRaw);
if (!matchesExpectedImage(processInfo, expectedImageName)) {
// log and return false
}
if (allowImageOnlyMatch || !isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
return matchesBackendMarkers({
pid,
backendConfig,
commandLineCache,
spawnSync,
log,
});
}
```
**之后:**
```js
function shouldKillUnmanagedBackendProcess({
pid,
processInfo,
backendConfig,
ctx,
spawnSync,
log,
}) {
if (processInfo.imageName.toLowerCase() !== ctx.expectedImageName) {
log(`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`);
return false;
}
// If we know command line is unavailable, we already logged and decided to allow image-only.
if (ctx.commandLineQueryUnavailable) {
return true;
}
return matchesBackendMarker({
pid,
backendConfig,
ctx,
spawnSync,
log,
});
}
```
这样既保留了防护机制,避免误杀无关的 `python.exe` 进程——在可以检查命令行时,总是要求匹配标记——又能在无法获取命令行时自动退回到仅基于镜像的匹配。
---
这些修改在保留所有功能行为(缓存、日志、回退、安全检查)的同时,通过以下方式显著降低了复杂度:
* 移除了“魔法”键及其相关的辅助函数。
* 将状态扁平化到一个小型上下文对象上的显式字段。
* 围绕单一稳定的命令行标记收紧匹配逻辑。
* 将 Windows 命令行探测路径简化为更清晰、更小的辅助函数。
</issue_to_address>Hi @zouyonghe! 👋
感谢你通过评论 @sourcery-ai review 来试用 Sourcery!🚀
安装 sourcery-ai bot,即可在每个 pull request 上获得自动代码审查 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据这些反馈改进之后的审查。Original comment in English
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location> `desktop/lib/windows-backend-cleanup.js:6` </location>
<code_context>
+const path = require('path');
+
+const WINDOWS_PROCESS_QUERY_TIMEOUT_MS = 2000;
+const COMMAND_LINE_QUERY_UNAVAILABLE_KEY = '__command_line_query_unavailable__';
+const COMMAND_LINE_FALLBACK_LOGGED_KEY = '__command_line_fallback_logged__';
+
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring this module to use an explicit context object, a simpler command-line probing helper, and a single backend marker to avoid magic cache keys and nested branching.
You can keep the same behavior while dropping a lot of indirection and magic-state encoding. Concretely:
### 1. Replace magic cache keys with explicit fields
Instead of storing `__command_line_query_unavailable__` / `__command_line_fallback_logged__` in the same `Map` as PIDs, you can keep the cache purely `pid → commandLine` and add explicit flags to the context.
**Before (simplified):**
```js
const COMMAND_LINE_QUERY_UNAVAILABLE_KEY = '__command_line_query_unavailable__';
const COMMAND_LINE_FALLBACK_LOGGED_KEY = '__command_line_fallback_logged__';
function isQueryUnavailable(cache) {
return !!(cache && cache.get(COMMAND_LINE_QUERY_UNAVAILABLE_KEY));
}
function setQueryUnavailable(cache, value) {
if (cache) {
cache.set(COMMAND_LINE_QUERY_UNAVAILABLE_KEY, !!value);
}
}
function wasFallbackLogged(cache) {
return !!(cache && cache.get(COMMAND_LINE_FALLBACK_LOGGED_KEY));
}
function markFallbackLogged(cache) {
if (cache) {
cache.set(COMMAND_LINE_FALLBACK_LOGGED_KEY, true);
}
}
```
**After:**
Add explicit fields to the context and keep the cache as `Map<number, string | null>`:
```js
// context builder
function buildUnmanagedCleanupContext(backendConfig, fallbackCmdRaw) {
return {
backendConfig,
expectedImageName: getExpectedImageName(backendConfig, fallbackCmdRaw),
commandLineCache: new Map(), // pid -> string|null
commandLineQueryUnavailable: false, // globally no powershell/pwsh
fallbackLogged: false, // logged image-only fallback
};
}
```
Use them in helpers instead of magic keys:
```js
function getWindowsProcessCommandLine({ pid, ctx, spawnSync, log, timeoutMs }) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid)) {
return null;
}
if (ctx.commandLineQueryUnavailable) {
return null;
}
if (ctx.commandLineCache.has(numericPid)) {
return ctx.commandLineCache.get(numericPid);
}
// ... spawn powershell/pwsh ...
// on success:
ctx.commandLineCache.set(numericPid, commandLine);
return commandLine;
// on ENOENT for all shells:
ctx.commandLineQueryUnavailable = true;
return null;
// on other failures:
ctx.commandLineCache.set(numericPid, null);
return null;
}
```
And:
```js
if (!commandLine && ctx.commandLineQueryUnavailable) {
if (!ctx.fallbackLogged) {
ctx.fallbackLogged = true;
log('Neither powershell nor pwsh is available. Falling back to image-name-only matching...');
}
return true; // fallback path
}
```
This removes `COMMAND_LINE_*` constants + `isQueryUnavailable`/`setQueryUnavailable`/`wasFallbackLogged`/`markFallbackLogged` and makes cache state much easier to reason about.
---
### 2. Simplify command-line querying logic
You can keep the same “try powershell, then pwsh, treat ENOENT as unavailable, log timeouts/other errors” semantics with less branching by collapsing the loop into a helper.
**Before (core structure):**
```js
const queryAttempts = ['powershell', 'pwsh'];
let hasAvailableShell = false;
for (const shellName of queryAttempts) {
let result = null;
try {
result = spawnSync(shellName, args, options);
} catch (error) {
// log and continue
}
if (result.error && result.error.code === 'ENOENT') {
continue;
}
hasAvailableShell = true;
if (result.error && result.error.code === 'ETIMEDOUT') {
// log and continue
}
if (result.error) {
// log and continue
}
if (result.status === 0) {
// parse stdout, cache, setQueryUnavailable(false)
return { commandLine, commandLineQueryUnavailable: false };
}
}
if (!hasAvailableShell) {
setQueryUnavailable(commandLineCache, true);
return { commandLine: null, commandLineQueryUnavailable: true };
}
// cache null, setQueryUnavailable(false)
```
**After (same behavior, less surface):**
```js
function tryShellForCommandLine(shellName, args, options, log, pid, timeoutMs) {
let result;
try {
result = spawnSync(shellName, args, options);
} catch (error) {
if (error instanceof Error && error.message) {
log(`Failed to query process command line via ${shellName} for pid=${pid}: ${error.message}`);
}
return { available: false, commandLine: null };
}
if (result.error?.code === 'ENOENT') {
return { available: false, commandLine: null };
}
if (result.error?.code === 'ETIMEDOUT') {
log(`Timed out (${timeoutMs}ms) querying process command line via ${shellName} for pid=${pid}.`);
return { available: true, commandLine: null };
}
if (result.error?.message) {
log(`Failed to query process command line via ${shellName} for pid=${pid}: ${result.error.message}`);
return { available: true, commandLine: null };
}
if (result.status === 0) {
const commandLine =
result.stdout
.split(/\r?\n/)
.map((item) => item.trim())
.find((item) => item.length > 0) || null;
return { available: true, commandLine };
}
return { available: true, commandLine: null };
}
function getWindowsProcessCommandLine({ pid, ctx, spawnSync, log, timeoutMs }) {
const numericPid = Number.parseInt(`${pid}`, 10);
if (!Number.isInteger(numericPid) || ctx.commandLineQueryUnavailable) {
return null;
}
if (ctx.commandLineCache.has(numericPid)) {
return ctx.commandLineCache.get(numericPid);
}
const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${numericPid}"; if ($null -ne $p) { $p.CommandLine }`;
const args = ['-NoProfile', '-NonInteractive', '-Command', query];
const options = { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', windowsHide: true, timeout: timeoutMs };
let anyAvailable = false;
for (const shell of ['powershell', 'pwsh']) {
const { available, commandLine } = tryShellForCommandLine(shell, args, options, log, numericPid, timeoutMs);
anyAvailable ||= available;
if (commandLine !== null) {
ctx.commandLineCache.set(numericPid, commandLine);
return commandLine;
}
}
if (!anyAvailable) {
ctx.commandLineQueryUnavailable = true;
} else {
ctx.commandLineCache.set(numericPid, null);
}
return null;
}
```
This maintains all functional edges (ENOENT ⇒ unavailable, timeout logging, per-shell errors, per-pid caching) but removes the tuple `{ commandLine, commandLineQueryUnavailable }` and associated sentinel updates.
---
### 3. Narrow the marker logic to a single stable marker
Instead of returning two markers (absolute path + basename) and `includes`-checking both, you can use the resolved primary arg as the single marker. This keeps matching semantics strict while simplifying reasoning.
**Before:**
```js
function buildBackendCommandLineMarkers(backendConfig) {
// ... validation ...
const resolvedPrimaryArg = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(safeBackendConfig.cwd || process.cwd(), primaryArg);
return [
normalizeWindowsPathForMatch(resolvedPrimaryArg),
normalizeWindowsPathForMatch(path.basename(primaryArg)),
];
}
const matched = markers.some((marker) => marker && normalizedCommandLine.includes(marker));
```
**After:**
```js
function buildBackendCommandLineMarker(backendConfig) {
const cfg = backendConfig && typeof backendConfig === 'object' ? backendConfig : {};
if (!Array.isArray(cfg.args) || cfg.args.length === 0) {
return null;
}
const primaryArg = cfg.args[0];
if (typeof primaryArg !== 'string' || !primaryArg) {
return null;
}
const resolved = path.isAbsolute(primaryArg)
? primaryArg
: path.resolve(cfg.cwd || process.cwd(), primaryArg);
return normalizeWindowsPathForMatch(resolved);
}
function matchesBackendMarker({ pid, backendConfig, ctx, spawnSync, log }) {
const marker = buildBackendCommandLineMarker(backendConfig);
if (!marker) {
log(`Skip unmanaged cleanup for pid=${pid}: backend launch marker is unavailable.`);
return false;
}
const commandLine = getWindowsProcessCommandLine({
pid,
ctx,
spawnSync,
log,
timeoutMs: WINDOWS_PROCESS_QUERY_TIMEOUT_MS,
});
if (!commandLine) {
if (ctx.commandLineQueryUnavailable) {
if (!ctx.fallbackLogged) {
ctx.fallbackLogged = true;
log('Neither powershell nor pwsh is available. Falling back to image-name-only matching for generic Python backend cleanup.');
}
return true; // image-only fallback
}
log(`Skip unmanaged cleanup for pid=${pid}: unable to resolve process command line.`);
return false;
}
const normalizedCommandLine = normalizeWindowsPathForMatch(commandLine);
const matched = normalizedCommandLine.includes(marker);
if (!matched) {
log(`Skip unmanaged cleanup for pid=${pid}: command line does not match backend launch marker.`);
}
return matched;
}
```
This eliminates the array of markers and `some()` call, while still using a strong, absolute-path-based identifier for the backend process.
---
### 4. Simplify generic image logic
You can drop `allowImageOnlyMatch` and `isGenericWindowsPythonImage` in favor of a simpler rule: always require (image && marker) when command line is available; if command line is globally unavailable, fall back to image-only (with a one-time log).
**Before:**
```js
function shouldKillUnmanagedBackendProcess({
pid,
processInfo,
backendConfig,
allowImageOnlyMatch,
commandLineCache,
spawnSync,
log,
fallbackCmdRaw,
}) {
const expectedImageName = getExpectedImageName(backendConfig, fallbackCmdRaw);
if (!matchesExpectedImage(processInfo, expectedImageName)) {
// log and return false
}
if (allowImageOnlyMatch || !isGenericWindowsPythonImage(expectedImageName)) {
return true;
}
return matchesBackendMarkers({
pid,
backendConfig,
commandLineCache,
spawnSync,
log,
});
}
```
**After:**
```js
function shouldKillUnmanagedBackendProcess({
pid,
processInfo,
backendConfig,
ctx,
spawnSync,
log,
}) {
if (processInfo.imageName.toLowerCase() !== ctx.expectedImageName) {
log(`Skip unmanaged cleanup for pid=${pid}: unexpected process image ${processInfo.imageName}.`);
return false;
}
// If we know command line is unavailable, we already logged and decided to allow image-only.
if (ctx.commandLineQueryUnavailable) {
return true;
}
return matchesBackendMarker({
pid,
backendConfig,
ctx,
spawnSync,
log,
});
}
```
This keeps the protection against accidentally killing unrelated `python.exe` processes by requiring the marker whenever command line inspection is available, and automatically falls back to image-only matching when it is not.
---
These changes keep all functional behaviors (caching, logging, fallbacks, safety checks) but substantially reduce complexity by:
* Removing magic keys and associated helpers.
* Flattening state into explicit fields on a small context object.
* Tightening matching logic around a single stable command-line marker.
* Simplifying the Windows command-line probing path into clearer, smaller helpers.
</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.
概述
主要修改
变更动机
验证情况
额外说明
ASTRBOT_DESKTOP_CPYTHON_HOME(例如 .venv)。
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Bug 修复:
ASTRBOT_ELECTRON_CLIENT标志,而不再依赖sys.frozen,以便在新的运行时模型下改进环境检测。增强:
site-packages目录的支持。Build:
.gitignore,涵盖新的后端构建产物。CI:
ASTRBOT_DESKTOP_CPYTHON_HOME,以便后端打包在 CI 构建期间能够找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Bug 修复:
ASTRBOT_ELECTRON_CLIENT标志,而不再依赖sys.frozen,以便在新的运行时模型下改进环境检测。增强:
site-packages目录的支持。Build:
.gitignore,涵盖新的后端构建产物。CI:
ASTRBOT_DESKTOP_CPYTHON_HOME,以便后端打包在 CI 构建期间能够找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
由 Sourcery 提供的总结
将 Electron 桌面端后端从使用 PyInstaller 构建的可执行文件迁移为嵌入式 CPython 运行时与源码快照,并更新后端启动方式、插件处理逻辑以及环境/运行时检测,以适配新的打包模型。
错误修复:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,以与新的运行时模型保持一致。功能增强:
site-packages中。构建:
.gitignore,以涵盖新的后端构建产物。CI:
workflow_dispatch中要求显式 tag;同时导出ASTRBOT_DESKTOP_CPYTHON_HOME,以便 CI 构建能够找到 CPython 运行时。文档:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation:
Original summary in English
Summary by Sourcery
将 Electron 桌面端后端从基于 PyInstaller 构建的可执行文件迁移为“打包的 CPython 运行时 + 源码快照”的形式,并更新启动链路、环境检测逻辑和数据路径以适配新的打包模型。
New Features:
Bug Fixes:
ASTRBOT_ELECTRON_CLIENT标志而非sys.frozen,避免对运行环境做出错误假设。Enhancements:
shlex解析,使pip install参数在 shell 中更加健壮。Build:
ASTRBOT_DESKTOP_CPYTHON_HOME或ASTRBOT_DESKTOP_BACKEND_RUNTIME指定其位置。.gitignore条目以覆盖新的后端构建产物。CI:
github.ref_name解析引用;对workflow_dispatch触发的运行要求显式标签;并导出ASTRBOT_DESKTOP_CPYTHON_HOME以便 CI 构建能找到 CPython 运行时。Documentation:
ASTRBOT_DESKTOP_CPYTHON_HOME。Original summary in English
Summary by Sourcery
Migrate the Electron desktop backend from a PyInstaller-built executable to a bundled CPython runtime plus source snapshot, updating the startup chain, environment detection, and data paths to fit the new packaging model.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Documentation: