chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
# ACP Terminal API 沙箱化方案
|
||||
|
||||
> 版本: 1.3.1 | 日期: 2026-04-10 | 状态: 已实现(含 sandbox 抽象层同步)
|
||||
|
||||
## 问题背景
|
||||
|
||||
Windows 受限 token (Restricted Token) 沙箱无法在受限进程内创建子进程(EPERM),
|
||||
而 ACP 引擎(claude-code-acp-ts、qimingcode)需要 spawn 子进程来执行 bash 命令和 MCP 服务器。
|
||||
进程级沙箱化(`serve` 模式)在 Windows 上不可行。
|
||||
|
||||
## Deep Research 关键发现
|
||||
|
||||
### claude-code-acp-ts 使用 ACP Terminal API
|
||||
|
||||
**claude-code-acp-ts 的 bash 工具通过 ACP `terminal/create` 协议方法执行命令**,
|
||||
而非直接使用 Node.js `child_process`。
|
||||
|
||||
源码证据(`~/.qimingclaw/node_modules/claude-code-acp-ts/dist/mcp-server.js:424-432`):
|
||||
|
||||
```javascript
|
||||
if (!agent.clientCapabilities?.terminal || !agent.client.createTerminal) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
const handle = await agent.client.createTerminal({
|
||||
command: input.command,
|
||||
env: [{ name: "CLAUDECODE", value: "1" }],
|
||||
sessionId,
|
||||
outputByteLimit: 32000,
|
||||
});
|
||||
```
|
||||
|
||||
### qimingcode 使用内部 bash 执行
|
||||
|
||||
qimingcode(基于 opencode)不使用 Terminal API,而是内部直接执行 bash 命令。
|
||||
通过 `OPENCODE_CONFIG_CONTENT.sandbox` 配置实现逐命令沙箱化(已在之前 PR 中实现)。
|
||||
|
||||
### 结论
|
||||
|
||||
| 引擎 | Bash 执行方式 | 沙箱化方案 |
|
||||
|------|-------------|-----------|
|
||||
| claude-code | ACP `terminal/create` | Client 端 Terminal Manager + helper.exe |
|
||||
| qimingcode | 内部 child_process | `OPENCODE_CONFIG_CONTENT.sandbox` |
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Electron Client (AcpEngine) │
|
||||
│ │
|
||||
│ clientCapabilities: { terminal: true } │
|
||||
│ │
|
||||
│ buildClientHandler(): │
|
||||
│ ├─ sessionUpdate ──→ handleAcpSessionUpdate() │
|
||||
│ ├─ requestPermission ──→ handlePermissionRequest() │
|
||||
│ │ └─ strictPermissionGuard.ts │
|
||||
│ └─ ...terminalManager.getClientHandlers() │
|
||||
│ ├─ createTerminal → SandboxInvoker + helper/direct │
|
||||
│ ├─ terminalOutput → output buffer │
|
||||
│ ├─ waitForExit → exit promise │
|
||||
│ ├─ killTerminal → process.kill() │
|
||||
│ └─ releaseTerminal → cleanup │
|
||||
│ │
|
||||
│ qimingcode: OPENCODE_CONFIG_CONTENT.sandbox (独立路径) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
claude-code-acp-ts qimingcode acp
|
||||
(terminal/create) (internal bash)
|
||||
```
|
||||
|
||||
### ACP Terminal API 协议
|
||||
|
||||
| 方法 | 方向 | 说明 |
|
||||
|------|------|------|
|
||||
| `terminal/create` | Agent → Client | 创建终端,执行命令 |
|
||||
| `terminal/output` | Agent → Client | 获取当前输出 |
|
||||
| `terminal/wait_for_exit` | Agent → Client | 等待命令完成 |
|
||||
| `terminal/kill` | Agent → Client | 终止命令 |
|
||||
| `terminal/release` | Agent → Client | 释放资源 |
|
||||
|
||||
Agent 必须检查 `clientCapabilities.terminal === true` 才能使用。
|
||||
|
||||
### 执行流程 (claude-code on Windows)
|
||||
|
||||
1. Agent 调用 `terminal/create` 发送命令(如 `npm test`)
|
||||
2. `AcpTerminalManager.createTerminal()` 接收请求
|
||||
3. Windows 下:通过 `SandboxInvoker.buildInvocation()` 构建包装命令
|
||||
```
|
||||
qiming-sandbox-helper.exe run --mode read-only --cwd <cwd> --policy-json {...} -- npm test
|
||||
```
|
||||
4. macOS/Linux 下:直接 `spawn(command, args)`
|
||||
5. 返回 `terminalId` 给 Agent
|
||||
6. Agent 调用 `terminal/output` 获取输出(从 buffer 读取)
|
||||
7. Agent 调用 `terminal/wait_for_exit` 等待完成
|
||||
8. Agent 调用 `terminal/release` 释放资源
|
||||
|
||||
### Sandbox Helper JSON 输出
|
||||
|
||||
`qiming-sandbox-helper.exe run` 返回 JSON:
|
||||
```json
|
||||
{
|
||||
"exit_code": 0,
|
||||
"stdout": "test output...",
|
||||
"stderr": "",
|
||||
"timed_out": false
|
||||
}
|
||||
```
|
||||
|
||||
`AcpTerminalManager` 解析此 JSON,提取 `stdout` + `stderr` 放入 output buffer。
|
||||
|
||||
## 变更文件
|
||||
|
||||
| 文件 | 变更类型 | 说明 |
|
||||
|------|----------|------|
|
||||
| `acpTerminalManager.ts` | 修改 | Terminal 生命周期管理;Windows 走 `SandboxInvoker(run)`;平台判断改为 `PlatformAdapter` |
|
||||
| `acpClient.ts` | 修改 | ACP 侧 sandbox 启动链细节同步(含平台分支统一入口) |
|
||||
| `strictPermissionGuard.ts` | 修改 | strict 写入路径门控继续生效,平台来源统一 |
|
||||
| `SandboxInvoker.ts` | 修改 | strict 语义对齐:macOS strict 不含 startup chain;Windows run strict 仅首个 writable root |
|
||||
| `platformAdapter.ts` | **新建** | 跨平台统一抽象层(平台能力、命令探测、helper/bwrap 路径解析、推荐后端) |
|
||||
| `platformAdapter.test.ts` | **新建** | `PlatformAdapter` 单测覆盖 |
|
||||
| `shellEnv.ts` | 修改 | 命令探测与 PATH 分隔符统一走 `PlatformAdapter` |
|
||||
| `policy.ts` | 修改 | 平台/后端推荐与 helper 路径解析统一走 `PlatformAdapter` |
|
||||
| `SandboxManager.ts` | 修改 | 平台分支入口统一走 `PlatformAdapter` |
|
||||
| `serviceBootstrap.ts` | 修改 | 平台识别统一走 `PlatformAdapter` |
|
||||
| `processTree.ts` | 修改 | 进程树清理平台分支统一走 `PlatformAdapter` |
|
||||
|
||||
### 不变的文件
|
||||
|
||||
| 文件 | 原因 |
|
||||
|------|------|
|
||||
| `windows-sandbox-helper/main.rs` | `run` 模式已可用 |
|
||||
| `sandboxProcessWrapper.ts` | 仍用于 qimingcode env-var 注入 |
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
### 1. getClientHandlers() 模式
|
||||
|
||||
`AcpTerminalManager` 提供 `getClientHandlers()` 方法返回 handler 对象,
|
||||
`acpEngine.ts` 通过 spread 操作符注入:
|
||||
|
||||
```typescript
|
||||
private buildClientHandler(): AcpClientHandler {
|
||||
return {
|
||||
sessionUpdate: ...,
|
||||
requestPermission: ...,
|
||||
// 一行注入所有 terminal handlers
|
||||
...(this.terminalManager?.getClientHandlers() ?? {}),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**优点**:减少代码侵入,terminal 逻辑完全封装在 manager 中。
|
||||
|
||||
### 2. JSON 解析 vs 直接输出
|
||||
|
||||
Windows sandbox helper `run` 模式返回 JSON 封装的输出。
|
||||
`AcpTerminalManager` 在 `spawnProcess()` 中区分两种模式:
|
||||
- `parseJson=true`(Windows sandbox):收集完整 JSON,解析后提取 stdout/stderr
|
||||
- `parseJson=false`(直接执行):实时流式收集 stdout/stderr
|
||||
|
||||
### 3. outputByteLimit
|
||||
|
||||
遵循 ACP 规范:当输出超过 `outputByteLimit` 时,从开头截断保留最新输出。
|
||||
标记 `truncated: true` 以通知 Agent。
|
||||
|
||||
> **注意**:当前实现使用 `string.length` 而非字节数。对于 ASCII 输出两者一致,
|
||||
> 对于 CJK 多字节字符,`string.length`(UTF-16 code units)可能小于实际字节数。
|
||||
> 实际影响有限,因为 claude-code 设置 `outputByteLimit: 32000`。
|
||||
|
||||
### 4. 沙箱参数来源
|
||||
|
||||
`writablePaths`、`networkEnabled` 和 `mode` 从 sandbox 配置传入,而非硬编码:
|
||||
|
||||
```typescript
|
||||
this.terminalManager = new AcpTerminalManager({
|
||||
windowsSandboxHelperPath: sandboxConfig.windowsSandboxHelperPath,
|
||||
windowsSandboxMode: sandboxConfig.windowsSandboxMode,
|
||||
networkEnabled: sandboxConfig.networkEnabled ?? true,
|
||||
writablePaths: sandboxConfig.projectWorkspaceDir
|
||||
? [sandboxConfig.projectWorkspaceDir]
|
||||
: [],
|
||||
mode: sandboxConfig.mode,
|
||||
});
|
||||
```
|
||||
|
||||
`createTerminal` 不会把 `cwd` 追加到 `writablePaths`;而是执行以下策略:
|
||||
- `cwd` 在可写根内:直接使用
|
||||
- `cwd` 缺失或越界:回退到首个可写根(workspace-first)
|
||||
|
||||
### 5. 竞态防护
|
||||
|
||||
Terminal 在 spawn 之前注册到 Map,避免快速退出的进程导致 "Terminal not found" 错误:
|
||||
|
||||
```typescript
|
||||
// 注册在前,spawn 在后
|
||||
this.terminals.set(terminalId, session);
|
||||
this.spawnProcess(session, ...);
|
||||
```
|
||||
|
||||
### 6. 会话级终端清理
|
||||
|
||||
当 `abortSession()` 取消会话时,自动终止该会话关联的所有终端进程:
|
||||
|
||||
```typescript
|
||||
if (this.terminalManager) {
|
||||
await this.terminalManager.releaseForSession(sessionId);
|
||||
}
|
||||
```
|
||||
|
||||
`releaseForSession()` 遍历所有 terminal,按 sessionId 过滤后逐一 kill + release。
|
||||
|
||||
### 7. 并发限制
|
||||
|
||||
默认最多 50 个并发终端,防止失控的 Agent 耗尽系统资源:
|
||||
|
||||
```typescript
|
||||
private static readonly MAX_CONCURRENT = 50;
|
||||
|
||||
async createTerminal(...) {
|
||||
if (this.terminals.size >= AcpTerminalManager.MAX_CONCURRENT) {
|
||||
throw new Error(
|
||||
`Terminal limit reached (${AcpTerminalManager.MAX_CONCURRENT}). Release existing terminals first.`
|
||||
);
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 与现有系统的关系
|
||||
|
||||
### macOS/Linux 进程级沙箱
|
||||
|
||||
在 macOS/Linux 上,ACP 引擎进程仍通过 seatbelt/bwrap 进行进程级沙箱化。
|
||||
Terminal API 执行的命令也在此沙箱内运行,无需额外包装。
|
||||
|
||||
### qimingcode env-var 沙箱
|
||||
|
||||
qimingcode 不使用 Terminal API,其 bash 执行通过 `OPENCODE_CONFIG_CONTENT.sandbox`
|
||||
配置路由到 sandbox helper。这是独立的代码路径,不受 Terminal API 实现影响。
|
||||
|
||||
另外,在 `strict` 模式下,`qimingcode` 的 ACP `requestPermission` 路径会额外经过
|
||||
`strictPermissionGuard.ts`:
|
||||
- 仅识别写入类请求应用路径门控(非写入请求跳过)
|
||||
- 允许写入根目录:`workspace` + `temp(TMP/TEMP/TMPDIR)` + `isolatedHome` + `isolatedHome/tmp`(strict 下不含 `appData`)
|
||||
- 路径缺失时 fail-closed(拒绝)
|
||||
- 写入类权限仅选择 `allow_once`(不走 `allow_always`)
|
||||
|
||||
同时,`qimingcode` warmup 复用现在对 sandbox policy 变更敏感:
|
||||
- warmup 启动时记录 `QIMING_AGENT_WARMUP_SANDBOX_POLICY_FP`
|
||||
- 复用前比对当前 policy 指纹
|
||||
- 旧 warmup 缺少该标记或指纹不一致时,立即放弃复用并冷启动
|
||||
- 确保 sandbox mode/policy 配置修改后对“下一次新会话”立即生效
|
||||
|
||||
## 验证步骤
|
||||
|
||||
1. 启动 Electron 开发模式
|
||||
2. 使用 claude-code 引擎发送包含 bash 命令的 prompt
|
||||
3. 检查日志中 `createTerminal` 调用(而非 EPERM 错误)
|
||||
4. 确认 Windows 下命令通过 `qiming-sandbox-helper.exe run` 执行
|
||||
5. 确认 `terminalOutput` 返回正确输出
|
||||
6. 确认 `waitForExit` 返回正确退出码
|
||||
7. 确认 qimingcode 行为不受影响
|
||||
8. 确认 macOS/Linux 沙箱行为不受影响
|
||||
9. 验证 abort 后终端进程被正确清理
|
||||
10. 验证并发超过 50 时抛出限制错误
|
||||
11. (qimingcode + strict)验证 ACP 调试日志:
|
||||
- `strict writable roots snapshot`(每个 session 一次)
|
||||
- `strict permission evaluation`
|
||||
- `strict permission skipped (non-write request)`
|
||||
- `strict write permission blocked`
|
||||
- `strict write permission allowed_once`
|
||||
12. (warmup)验证 sandbox policy 相关调试日志:
|
||||
- `warmup sandbox policy snapshot`
|
||||
- `warmup sandbox policy compatibility check`
|
||||
- 发生策略变更时出现:`Sandbox policy changed, skipping warmup reuse`
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
### SandboxMode 对 Windows per-command 的影响
|
||||
|
||||
`SandboxMode`(strict / compat / permissive)影响 Windows 沙箱行为,包括 `run` 和 `serve` 子命令:
|
||||
|
||||
#### `run` 子命令(per-command,claude-code bash)
|
||||
|
||||
| Mode | `writable_roots` | Token 限制 | APPDATA |
|
||||
|------|-----------------|-----------|---------|
|
||||
| **strict** | 仅首个传入路径(workspace-first) | WRITE_RESTRICTED 保持启用 | 不包含 |
|
||||
| **compat**(默认) | 全部传入路径 | WRITE_RESTRICTED 保持启用 | 包含 |
|
||||
| **permissive** | 全部传入路径 | `--no-write-restricted` 放松 token | 包含 |
|
||||
|
||||
#### `serve` 子命令(进程级,qimingcode 整个进程)
|
||||
|
||||
| Mode | WRITE_RESTRICTED | 可写路径 | 说明 |
|
||||
|------|-----------------|---------|------|
|
||||
| **strict** | `--write-restricted` 启用 | workspace + TEMP/TMP | 最小写入面,APPDATA 不可写 |
|
||||
| **compat** | `--write-restricted` 启用 | workspace + TEMP/TMP + APPDATA/LOCALAPPDATA | 引擎基础设施可写 |
|
||||
| **permissive** | 不传递 flag(默认 false) | 无限制 | 仅受限 token,无写入保护 |
|
||||
|
||||
> **关键变更 (v1.2.0)**: `serve` 子命令新增 `--write-restricted` CLI flag。
|
||||
> strict/compat 模式下启用,permissive 模式下禁用。
|
||||
> 这修复了 qimingcode 内部 bash 工具可绕过沙箱写入 Desktop 的问题。
|
||||
> APPDATA/LOCALAPPDATA 的包含/排除由 Rust helper 的 `compute_allow_paths()`
|
||||
> 根据 policy JSON 中的 `sandbox_mode` 字段统一控制。
|
||||
|
||||
> 对 `qimingcode` 来说,strict 下除了 helper 的 `writable_roots` 外,ACP 权限层还会执行
|
||||
> `strictPermissionGuard` 二次门控(写入路径必须落在 workspace/temp/isolatedHome)。
|
||||
|
||||
### macOS/Linux 上的 per-command 沙箱
|
||||
|
||||
当前 `AcpTerminalManager` 仅在 Windows 上启用 per-command 沙箱包装。
|
||||
macOS/Linux 上 terminal 命令直接执行(进程级沙箱由 seatbelt/bwrap 在引擎级别提供)。
|
||||
|
||||
### 跨平台 SandboxMode 差异
|
||||
|
||||
| 平台 | strict | compat | permissive |
|
||||
|------|--------|--------|-----------|
|
||||
| Linux (bwrap) | 最小 ro-bind(`/usr`, `/bin`, `/sbin`, `/lib`, `/lib64`, `/etc`, `/opt`, `/usr/local`) | 全局 ro-bind `/` | 完整 rw bind,无 namespace 隔离 |
|
||||
| macOS (seatbelt) | 仅命令本身在 exec allowlist(不含 startup chain) | 命令 + startup chain 在 exec allowlist | 全局 file-write + unrestricted process-exec |
|
||||
| Windows (helper `run`) | `writable_roots` 仅首个路径 + WRITE_RESTRICTED | `writable_roots` 全部路径 + WRITE_RESTRICTED | `writable_roots` 全部路径 + `--no-write-restricted` |
|
||||
| Windows (helper `serve`) | `--write-restricted`,仅 workspace + TEMP/TMP | `--write-restricted`,workspace + TEMP/TMP + APPDATA | 无 WRITE_RESTRICTED(进程级不限制写入) |
|
||||
|
||||
### Windows 非 sandbox 路径的 shell 注入
|
||||
|
||||
当 sandbox 未启用且平台为 Windows 时,`spawnProcess` 使用 `shell: true`。
|
||||
如果 Agent 发送的命令包含 shell 元字符,它们会被解释。
|
||||
安全边界在 sandbox helper 级别(受限 token),sandbox 未启用时命令以用户权限运行。
|
||||
|
||||
### 无命令白名单/黑名单
|
||||
|
||||
Terminal Manager 执行 Agent 发送的所有命令,安全边界在 sandbox helper 级别。
|
||||
如果 sandbox 不可用,命令以完整用户权限运行。
|
||||
|
||||
## 参考
|
||||
|
||||
- [ACP Terminal API 规范](https://agentclientprotocol.com/protocol/terminals)
|
||||
- [ACP SDK 源码](node_modules/@agentclientprotocol/sdk/dist/acp.js)
|
||||
- [Codex Windows Sandbox](https://github.com/openai/codex) — 参考架构
|
||||
- [windows-sandbox-helper](../../crates/windows-sandbox-helper/src/main.rs) — Rust helper 源码
|
||||
@@ -0,0 +1,40 @@
|
||||
# 日志设计说明
|
||||
|
||||
主进程日志目录:`~/.qimingclaw/logs/`(开发与正式共用该路径,行为由是否打包区分)。
|
||||
|
||||
## 问题与目标
|
||||
|
||||
- **问题**:所有日志只写 `main.log` 会导致文件无限膨胀。
|
||||
- **目标**:单文件有大小上限、按时间轮转、过期自动清理,且开发与正式环境策略不同。
|
||||
|
||||
## 实现(logConfig.ts)
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| **按大小轮转** | 单文件超过 `maxSize` 时,将当前 `main.log` 归档为 `main.YYYY-MM-DD-HHmmss.log`,再继续写新的 `main.log`。 |
|
||||
| **按时间清理(TTL)** | 每次启动时扫描 `logs/`,删除超过有效期的归档文件(`main`/`renderer` 的 `.old.log`、`.YYYY-MM-DD-*.log`),不删当前 `main.log`、`renderer.log`。 |
|
||||
| **开发 vs 正式** | 开发:文件级别 `debug`、maxSize 5MB、保留 30 天;正式:文件级别 `info`、maxSize 2MB、保留 7 天。控制台始终为 `debug`。 |
|
||||
|
||||
## latest.log:统一入口(多平台兼容)
|
||||
|
||||
用户与客户端只需关注 **latest.log**,其始终指向当前正在写入的主进程日志(即 main.log 的“当前版本”)。
|
||||
|
||||
| 平台 | 实现方式 | 说明 |
|
||||
|------|----------|------|
|
||||
| **macOS / Linux** | 符号链接 `latest.log` → `main.log`(相对路径) | 轮转后新生成的 `main.log` 自动被指向,无需更新链接。 |
|
||||
| **Windows** | 硬链接 `latest.log`(与 `main.log` 同 inode) | 不依赖管理员或开发者模式;轮转后会在下一拍更新 `latest.log` 指向新的 `main.log`。 |
|
||||
|
||||
- 客户端内「日志列表」展示(`log:list` IPC)优先读取 `latest.log`,若不存在则回退到 `main.log`。
|
||||
- 客户端内「打开日志目录」:**macOS** 使用 Finder 打开目录并选中 `latest.log`(或 `main.log`);**Windows** 使用资源管理器打开目录并选中该文件;**Linux** 仅打开日志目录。用户也可在终端中 `tail -f latest.log` 查看实时主进程日志。
|
||||
|
||||
## 目录与文件
|
||||
|
||||
- **latest.log**:始终指向当前主进程日志的入口(见上表;不参与 TTL 清理)。
|
||||
- **main.log** / **renderer.log**:当前主进程/渲染进程日志(electron-log 文件 transport)。
|
||||
- **main.YYYY-MM-DD-HHmmss.log**、**renderer.YYYY-MM-DD-HHmmss.log**:轮转后的历史日志,超过 TTL 会被自动删除。
|
||||
- **app.json**:渲染进程 LogViewer 用的应用内日志(logService),最多保留 100 条,与上述 TTL 独立。
|
||||
- **mcp/**、**project_logs/**、**computer_logs/**:由各服务自行写入,本方案不改变其逻辑。
|
||||
|
||||
## 配置入口
|
||||
|
||||
主进程入口 `main.ts` 在启动时调用 `initLogging()`,无需额外配置;环境通过 `app.isPackaged` 与 `NODE_ENV` 自动判断。
|
||||
@@ -0,0 +1,62 @@
|
||||
# macOS 26 Tahoe 兼容性修复
|
||||
|
||||
## 问题描述
|
||||
|
||||
在 macOS 26 Tahoe 上,Electron 应用启动时崩溃:
|
||||
|
||||
```
|
||||
Exception Type: EXC_BREAKPOINT (SIGTRAP)
|
||||
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
|
||||
0 Electron Framework cxxbridge1$box$rust_png$ResultOfReader$drop
|
||||
1 Electron Framework ElectronMain + 84
|
||||
```
|
||||
|
||||
## 根本原因
|
||||
|
||||
**macOS 26 Tahoe 上存在 Electron 兼容性问题:**
|
||||
|
||||
- **崩溃位置**: `ElectronMain` 初始化阶段
|
||||
- **相关组件**: Chromium 的 Rust 组件(Fontations 字体后端、rust_png)
|
||||
- **触发时机**: 崩溃发生在 **任何应用代码执行之前**
|
||||
- **影响范围**: Electron 38+ 版本(Chrome 140+)
|
||||
|
||||
## 解决方案
|
||||
|
||||
在 `src/main/main.ts` 文件开头添加:
|
||||
|
||||
```typescript
|
||||
import { app } from 'electron';
|
||||
|
||||
// macOS 26 Tahoe 兼容性:禁用 Fontations 字体后端
|
||||
// 参考: https://github.com/electron/electron/issues/49522
|
||||
if (process.platform === 'darwin') {
|
||||
app.commandLine.appendSwitch('disable-features', 'FontationsFontBackend');
|
||||
}
|
||||
```
|
||||
|
||||
## 版本历史
|
||||
|
||||
| 版本 | 修复内容 |
|
||||
|------|----------|
|
||||
| 0.4.4 | 降级到 Electron 37 (临时方案) |
|
||||
| **0.4.5** | 升级到 Electron 39 + 添加 workaround |
|
||||
|
||||
## 验证方法
|
||||
|
||||
打包后的应用进程参数中应包含:
|
||||
|
||||
```
|
||||
--disable-features=FontationsFontBackend
|
||||
```
|
||||
|
||||
```bash
|
||||
ps aux | grep -i "Qiming Agent" | grep -v grep
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Electron Issue #49522](https://github.com/electron/electron/issues/49522)
|
||||
|
||||
---
|
||||
|
||||
*最后更新: 2026-02-25*
|
||||
@@ -0,0 +1,41 @@
|
||||
# Sandbox 子模块升级 Runbook
|
||||
|
||||
## 适用范围
|
||||
|
||||
- 主仓: `qiming-agent`
|
||||
- Electron 客户端: `crates/agent-electron-client`
|
||||
- 子模块: `crates/agent-sandbox-runtime`
|
||||
|
||||
## 升级步骤
|
||||
|
||||
1. 更新子模块到目标 commit。
|
||||
2. 检查 `manifest.json` 是否包含目标平台产物和 `sha256`。
|
||||
3. 在 Electron 客户端执行:
|
||||
|
||||
```bash
|
||||
npm run prepare:sandbox-runtime
|
||||
```
|
||||
|
||||
4. 验证产物落地:
|
||||
- `resources/sandbox-runtime/bin`
|
||||
- `resources/sandbox-runtime/resolved-manifest.json`
|
||||
5. 运行构建并验证 `after-sign` 日志包含 `sandbox-runtime`(Windows 另含 `sandbox-helper`)。
|
||||
|
||||
## 快速校验
|
||||
|
||||
```bash
|
||||
node scripts/prepare/prepare-sandbox-runtime.js
|
||||
npm run build:main
|
||||
```
|
||||
|
||||
## 回滚步骤
|
||||
|
||||
1. 将子模块回退到上一个稳定 commit。
|
||||
2. 重新执行 `npm run prepare:sandbox-runtime`。
|
||||
3. 重新构建安装包并验证。
|
||||
|
||||
## 常见故障
|
||||
|
||||
1. `manifest 不存在`:子模块未初始化或路径错误。
|
||||
2. `sha256 mismatch`:产物与清单不一致,拒绝继续。
|
||||
3. `helper not found`(Windows):未构建内置 helper(`npm run build:sandbox-helper`)且未通过子模块 `prepare:sandbox-runtime` 同步产物,或安装包未包含 `sandbox-helper` / `sandbox-runtime` 下的 exe。
|
||||
@@ -0,0 +1,216 @@
|
||||
# Qiming Agent 启动前检查清单
|
||||
|
||||
适用于 **开发模式** 与 **打包运行**,按顺序执行可避免常见端口冲突与 native 模块错误。
|
||||
|
||||
**聚合实现**:端口定义与解析集中在 `src/shared/startupPorts.ts` 与 `src/main/services/startupPorts.ts`,启动时 ComputerServer / File Server 等均通过 `getConfiguredPorts()` 取端口;CLI 检查可运行 `npm run check-ports`(开发时加 Vite:`npm run check-ports:dev`)。**端口占用检查**:优先统一走 **bash** 执行 `scripts/tools/check-port.sh`(与 prepare-git 集成一致):Windows 使用 prepare-git 集成的 Git Bash(`resources/git/bin/bash.exe`),macOS/Linux 使用系统 bash;无 bash 或脚本时回退到 Node 内联 netstat/lsof。
|
||||
|
||||
---
|
||||
|
||||
## 一、端口与服务的对应关系(以最终配置为准)
|
||||
|
||||
检查端口时应使用 **当前配置的端口**,而不是写死的默认值。各服务端口来源如下:
|
||||
|
||||
| 服务 | 说明 | 配置来源 | 默认端口 |
|
||||
|------|------|----------|----------|
|
||||
| **Agent(ComputerServer)** | /computer/* API,供后端与 lanproxy 隧道访问 | 设置 → step1 配置 → `agentPort`(存 SQLite `step1_config`) | 60001 |
|
||||
| **File Server** | 本地文件服务、项目上传等 | 设置 → step1 配置 → `fileServerPort`(存 SQLite `step1_config`) | 60000 |
|
||||
| **MCP Proxy** | MCP 服务统一入口 | 设置 → MCP → 代理端口(存 SQLite `mcp_proxy_port`) | 18099 |
|
||||
| **Vite 开发服务器** | 仅开发模式,前端热更新 | `vite.config.ts` 的 `server.port` | 60173 |
|
||||
| **Lanproxy** | 内网穿透客户端;连接远程 `server_port`,隧道落到本机 Agent 或本地代理端口 | 设置 → Lanproxy → 服务端端口(存 `lanproxy_config` / `lanproxy.server_port`,为**远程**端口);**本地**端口依实现,常见与 Agent 一致或使用 60002(Agent Runner 代理默认) | 远程常 10076;本地 60002 |
|
||||
|
||||
- 若未在应用内改过端口,则按上表**默认端口**检查即可。
|
||||
- 若改过端口,请以**设置页或下面「获取当前配置端口」**得到的值为准。
|
||||
|
||||
---
|
||||
|
||||
## 二、获取当前配置端口(可选)
|
||||
|
||||
**方式一:应用内**
|
||||
打开 Qiming Agent → 设置 → 查看「Agent 端口 / 后端端口」「File Server 端口」「MCP 代理端口」「Lanproxy 服务端端口」(远程)及本地代理相关端口(若可见)。开发时 Vite 端口见项目根下 `vite.config.ts`。
|
||||
|
||||
**方式二:从 SQLite 读取(应用未运行时)**
|
||||
|
||||
```bash
|
||||
DB="$HOME/.qimingclaw/qimingclaw.db"
|
||||
|
||||
# step1_config 内含 agentPort、fileServerPort(JSON)
|
||||
sqlite3 "$DB" "SELECT value FROM settings WHERE key='step1_config';"
|
||||
|
||||
# MCP 代理端口(单独键)
|
||||
sqlite3 "$DB" "SELECT value FROM settings WHERE key='mcp_proxy_port';"
|
||||
|
||||
# Lanproxy 远程服务端端口(lanproxy_config 或 lanproxy.server_port)
|
||||
sqlite3 "$DB" "SELECT value FROM settings WHERE key='lanproxy_config';"
|
||||
sqlite3 "$DB" "SELECT value FROM settings WHERE key='lanproxy.server_port';"
|
||||
```
|
||||
|
||||
解析 `step1_config` 的 JSON 可得 `agentPort`、`fileServerPort`;`mcp_proxy_port` 无则表示用默认 18099。Lanproxy 本地检查端口通常用默认 60002(与 Agent Runner 代理端口一致)。
|
||||
|
||||
---
|
||||
|
||||
## 三、快速检查(按配置端口执行)
|
||||
|
||||
确认上述**实际使用的端口**未被占用(无输出表示端口空闲):
|
||||
|
||||
```bash
|
||||
# 替换为你的实际端口(未改过则用默认)
|
||||
AGENT_PORT=60001 # Agent / ComputerServer
|
||||
FILE_SERVER_PORT=60000
|
||||
MCP_PROXY_PORT=18099
|
||||
LANPROXY_LOCAL_PORT=60002 # Lanproxy 相关本地端口(如 Agent Runner 代理)
|
||||
VITE_PORT=60173 # 仅开发模式需要
|
||||
|
||||
lsof -i :$AGENT_PORT
|
||||
lsof -i :$FILE_SERVER_PORT
|
||||
lsof -i :$MCP_PROXY_PORT
|
||||
lsof -i :$LANPROXY_LOCAL_PORT
|
||||
# 开发时追加:
|
||||
# lsof -i :$VITE_PORT
|
||||
```
|
||||
|
||||
若某端口有输出,说明已被占用,需先结束占用进程或只保留一个 Agent 实例。
|
||||
|
||||
**一键检查(推荐,与应用内聚合逻辑一致):**
|
||||
|
||||
```bash
|
||||
# 从 ~/.qimingclaw/qimingclaw.db 读配置并检查占用(需系统有 sqlite3)
|
||||
npm run check-ports
|
||||
# 开发模式含 Vite 端口
|
||||
npm run check-ports:dev
|
||||
```
|
||||
|
||||
**或手动从 SQLite 读取后检查(需已安装 sqlite3):**
|
||||
|
||||
```bash
|
||||
DB="$HOME/.qimingclaw/qimingclaw.db"
|
||||
get_port() { sqlite3 "$DB" "SELECT value FROM settings WHERE key='$1';" 2>/dev/null; }
|
||||
step1=$(get_port 'step1_config')
|
||||
agent_port=$(echo "$step1" | sed -n 's/.*"agentPort":\([0-9]*\).*/\1/p'); agent_port=${agent_port:-60001}
|
||||
file_port=$(echo "$step1" | sed -n 's/.*"fileServerPort":\([0-9]*\).*/\1/p'); file_port=${file_port:-60000}
|
||||
mcp_port=$(get_port 'mcp_proxy_port'); mcp_port=${mcp_port:-18099}
|
||||
lanproxy_local_port=60002 # Lanproxy 相关本地端口,默认 60002
|
||||
|
||||
echo "检查端口: Agent=$agent_port FileServer=$file_port MCP=$mcp_port Lanproxy本地=$lanproxy_local_port (Vite=60173 仅开发)"
|
||||
for p in $agent_port $file_port $mcp_port $lanproxy_local_port; do
|
||||
lsof -i :$p 2>/dev/null && echo " -> 端口 $p 已被占用"
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、端口被占用时的处理步骤
|
||||
|
||||
### 1. 查看占用进程
|
||||
|
||||
```bash
|
||||
# 替换 <PORT> 为实际端口号(以「一」中配置为准),例如 60001
|
||||
lsof -i :<PORT>
|
||||
```
|
||||
|
||||
示例输出:
|
||||
```
|
||||
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
|
||||
Electron 123 apple 23u IPv4 xxx 0t0 TCP *:60001 (LISTEN)
|
||||
```
|
||||
|
||||
记下 **PID**(第二列)。
|
||||
|
||||
### 2. 结束占用进程
|
||||
|
||||
```bash
|
||||
# 替换 123 为上面看到的 PID
|
||||
kill 123
|
||||
```
|
||||
|
||||
若进程无法结束,可强制结束:
|
||||
```bash
|
||||
kill -9 123
|
||||
```
|
||||
|
||||
### 3. 一次性释放 Agent / File Server / MCP / Vite 端口(谨慎使用)
|
||||
|
||||
以下会结束占用**当前配置端口**的进程。若你未改过端口,可直接运行;若改过,请先把 `agent_port`、`file_port`、`mcp_port`、`lanproxy_local_port`、`vite_port` 改成你在设置里配置的值,并确认没有其他重要服务使用这些端口:
|
||||
|
||||
```bash
|
||||
# 使用默认端口示例;若已修改,请改为你配置的端口
|
||||
agent_port=60001
|
||||
file_port=60000
|
||||
mcp_port=18099
|
||||
lanproxy_local_port=60002 # Lanproxy 相关本地端口
|
||||
vite_port=60173 # 仅开发模式需要
|
||||
|
||||
for port in $agent_port $file_port $mcp_port $lanproxy_local_port $vite_port; do
|
||||
pid=$(lsof -t -i :$port 2>/dev/null)
|
||||
[ -n "$pid" ] && echo "Killing PID $pid (port $port)" && kill $pid
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、开发模式启动流程
|
||||
|
||||
在项目根目录或 `crates/agent-electron-client` 下执行:
|
||||
|
||||
```bash
|
||||
cd /Users/apple/workspace/qiming-agent/crates/agent-electron-client
|
||||
|
||||
# 1. 安装依赖(首次或 package.json 变更后)
|
||||
npm install
|
||||
|
||||
# 2. 若曾出现 better-sqlite3 与 Electron Node 版本不匹配,先重建 native 模块
|
||||
npx @electron/rebuild
|
||||
|
||||
# 3. 启动开发环境(Vite + Electron)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**开发模式注意:**
|
||||
|
||||
- 若 Vite 报错 `Port 60173 is already in use`,说明上次 dev 未完全退出。先执行「四、端口被占用时的处理步骤」释放 Vite 配置端口(默认 60173),或执行「四」中的「一次性释放」脚本后再 `npm run dev`。
|
||||
- 确保只运行一个 `npm run dev`,避免多开导致 Agent / File Server / MCP 端口冲突。
|
||||
|
||||
---
|
||||
|
||||
## 六、打包后运行(正式/测试包)
|
||||
|
||||
1. **完全退出已有实例**
|
||||
从菜单退出并确认托盘图标已消失,避免 Agent / File Server / MCP 等配置端口被旧进程占用。
|
||||
|
||||
2. **首次运行或更换 Electron/Node 后**
|
||||
若出现「Database initialization failed」且与 better-sqlite3 相关,在**开发目录**执行一次:
|
||||
```bash
|
||||
cd crates/agent-electron-client && npx @electron/rebuild
|
||||
```
|
||||
然后重新打包再运行。
|
||||
|
||||
3. **启动应用**
|
||||
直接打开打包好的应用即可;无需再执行 npm。
|
||||
|
||||
---
|
||||
|
||||
## 七、API 连接失败(UND_ERR_SOCKET)时
|
||||
|
||||
日志中出现 `Unable to connect to API (UND_ERR_SOCKET)` 时,表示引擎无法连上配置的模型 API,请依次检查:
|
||||
|
||||
| 检查项 | 说明 |
|
||||
|--------|------|
|
||||
| 网络 | 本机能否访问外网或企业代理(如用代理,确认代理可用)。 |
|
||||
| Base URL | 设置中的 API Base URL 是否正确(含协议与端口)。 |
|
||||
| 防火墙/安全软件 | 是否拦截了 Electron 或 Node 的出站连接。 |
|
||||
| 本地验证 | 在终端用 `curl -I <你的 Base URL>` 或浏览器访问,确认可达。 |
|
||||
|
||||
---
|
||||
|
||||
## 八、检查清单小结(可打印或保存)
|
||||
|
||||
- [ ] 以**当前配置端口**检查:Agent(ComputerServer)、File Server、MCP Proxy、Lanproxy 相关本地端口(默认 60002)及开发时 Vite 无冲突或已释放(参见「一」「三」)
|
||||
- [ ] 只保留一个 Qiming Agent 进程(含托盘)
|
||||
- [ ] 开发模式:已执行 `npm install`,必要时执行 `npx @electron/rebuild`
|
||||
- [ ] 设置中已配置默认模型和 API Key(避免依赖内置默认)
|
||||
- [ ] 启动后语言初始化正常:`navigator.language`(如 `en-US` / `zh-TW`)能命中期望语言;`zh-TW` / `zh-HK` 的 antd 组件文案为繁体;主进程托盘文案与页面语言一致
|
||||
- [ ] 若需内网穿透:Lanproxy 服务端与网络正常,且本地 Lanproxy/Agent Runner 所用端口(如 60002)无冲突;否则可忽略 Lanproxy 相关告警
|
||||
- [ ] 出现 API 连接错误时,已按「七」检查网络与 Base URL
|
||||
|
||||
---
|
||||
|
||||
*文档依据 `~/.qimingclaw/logs/latest.log` 常见错误整理,最后更新:2026-04-08*
|
||||
@@ -0,0 +1,112 @@
|
||||
# Deepcheck:启动端口与统一 Bash 实现
|
||||
|
||||
## 1. 实现范围
|
||||
|
||||
- **shared/startupPorts.ts**:端口默认值、配置键、`resolvePortsFromSettings(getSetting)`、`getPortsToCheck(ports, includeVite)`
|
||||
- **main/services/startupPorts.ts**:`getConfiguredPorts()`、`getPortList()`、`checkPortsInUse()`、`isPortInUse()`(bash 优先,Windows 回退 cmd+netstat)
|
||||
- **scripts/tools/check-port.sh**:单端口占用检查(netstat/lsof),供 bash 执行
|
||||
- **scripts/tools/check-startup-ports.js**:CLI,读 DB、解析端口、调用 bash+check-port.sh 或回退 Node 内联
|
||||
|
||||
---
|
||||
|
||||
## 2. 正确性
|
||||
|
||||
### 2.1 三处脚本逻辑一致性
|
||||
|
||||
| 位置 | 来源 | 端口参数 | Windows 分支 | Unix 分支 | 输出/exit |
|
||||
|------|------|----------|--------------|-----------|-----------|
|
||||
| **check-port.sh** | 文件 | `$1` | cmd //c "netstat -ano \| findstr \":${port} \"" | lsof -t -i ":${port}" | 占用: exit 0 + echo PID;未占用: exit 1 |
|
||||
| **main CHECK_PORT_SCRIPT** | 内嵌字符串 | `$1` | 同上(\\":$1 \\" 在 JS 中产生 \":$1 \") | lsof -t -i ":$1" | 同左 |
|
||||
| **脚本** | 读 check-port.sh | spawnSync(..., '_', port) | 由 .sh 决定 | 由 .sh 决定 | 同左 |
|
||||
|
||||
- **结论**:.sh 与内嵌脚本语义一致;端口均通过 `$1` 传入,无注入(端口为数字的 String(port))。
|
||||
- **已修复**:CLI 原先用 `execSync(bashPath, [args], options)`,Node 的 `execSync` 仅支持 `(command, options)`,第二个参数被当成 options,导致 bash 未按参数执行。已改为 `spawnSync(bashPath, ['-c', scriptContent, '_', String(port)], options)`。
|
||||
|
||||
### 2.2 配置解析(shared + 脚本)
|
||||
|
||||
- **step1_config**:`step1?.agentPort ?? default`、`step1?.fileServerPort ?? default`。若 DB 存的是 `{}` 或缺失,则用默认值;若 `agentPort: 0`,会得到 0(端口 0 在部分系统上表示“任选”,可能不符合预期,见 4.1)。
|
||||
- **mcp_proxy_port**:兼容 number 与 string;`parseInt` 失败或非整数时用默认值。
|
||||
- **脚本 DEFAULTS**:与 shared/constants 数值一致,需人工与 shared 同步(见 4.2)。
|
||||
|
||||
### 2.3 端口占用结果
|
||||
|
||||
- **spawnSync**:`status === 0` 表示脚本 exit 0(端口占用);stdout 取首行/最后一列解析 PID;空 stdout 时 `inUse: true, pid: undefined`,行为合理。
|
||||
- **回退路径**(Windows 无 bash / 脚本无 .sh):main 用 `execSync` + cmd;脚本用 `execSync` + shell: true(或 lsof),行为与之前一致。
|
||||
|
||||
---
|
||||
|
||||
## 3. 平台与环境
|
||||
|
||||
### 3.1 Windows
|
||||
|
||||
- **有 prepare-git**:`getBundledGitBashPath()` 返回 `resources/git/bin/bash.exe`(或 usr/bin),main 与 CLI 均用该 bash 执行脚本;脚本内 `OSTYPE=msys*`,走 cmd+netstat+findstr,一致。
|
||||
- **无 prepare-git**:main 回退 cmd+netstat;脚本 `winBashPath` 不存在则回退 Node netstat,逻辑一致。
|
||||
- **路径**:DB 路径 `%USERPROFILE%\.qimingclaw\`;脚本中 sqlite3 路径用正斜杠归一化,避免反斜杠转义问题。
|
||||
|
||||
### 3.2 macOS / Linux
|
||||
|
||||
- main 与脚本均用系统 `bash`;脚本来自文件或内嵌,均包含 `$OSTYPE` 判断,非 msys/cygwin 走 lsof。
|
||||
- **无 bash**(如 Alpine 仅 ash):`spawnSync('bash', ...)` 会失败,当前实现将 `status !== 0` 视为未占用并返回 `inUse: false`,属保守回退,可接受;若需严格“未知”可再区分 error/status。
|
||||
|
||||
### 3.3 check-port.sh 被读入后的执行
|
||||
|
||||
- 通过 `bash -c "<scriptContent>"` 执行时,首行 `#!/usr/bin/env bash` 被当作注释(`#` 起头),不影响执行,无需 strip。
|
||||
|
||||
---
|
||||
|
||||
## 4. 边界与改进建议
|
||||
|
||||
### 4.1 端口合法性
|
||||
|
||||
- 当前未校验端口范围(1–65535)及 0。若配置了 `agentPort: 0` 或错误大数,会直接传给 `startComputerServer`/lsof/netstat。
|
||||
- **建议**:在 `resolvePortsFromSettings` 或调用方对端口做范围校验,非法时回退默认或报错。
|
||||
|
||||
### 4.2 脚本与 shared 同步
|
||||
|
||||
- **check-startup-ports.js** 的 DEFAULTS/LABELS 与 shared/constants、shared/startupPorts 需人工同步;脚本无法 require 编译后的 shared。
|
||||
- **建议**:在脚本顶部注释中写明“与 shared/startupPorts.ts、constants 保持一致”;或考虑 build 时生成一份 ports-defaults.json 供脚本读取(可选)。
|
||||
|
||||
### 4.3 错误与超时
|
||||
|
||||
- `spawnSync` 未设 timeout;若 bash 或子命令卡住会阻塞。
|
||||
- **建议**:若需在 UI 或敏感路径调用,可加 `timeout` 选项或异步封装。
|
||||
|
||||
### 4.4 安全性
|
||||
|
||||
- 端口来自配置或常量,`String(port)` 后传入 spawn,无用户自由输入,无命令注入风险;脚本内 `$1` 仅用于端口,且由调用方控制。
|
||||
- sqlite3 的 key 做单引号转义,key 来自内部常量,风险可控。
|
||||
|
||||
---
|
||||
|
||||
## 5. 调用链与依赖
|
||||
|
||||
- **startup.ts**:`getConfiguredPorts().agent` → ComputerServer 端口。
|
||||
- **serviceManager.ts**:`getConfiguredPorts().fileServer` → File Server 端口。
|
||||
- **processHandlers.ts**:动态 import `getConfiguredPorts`,取 `fileServer` → 重启时的 File Server 端口。
|
||||
- **dependencies.ts**:导出 `getBundledGitBashPath()`,供 main startupPorts 使用;prepare-git 仅 Windows 下载并解压 Git 到 `resources/git/`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 测试建议
|
||||
|
||||
- **单元**:`resolvePortsFromSettings` 用 mock getSetting 测默认值、step1 部分键、mcp 为 number/string/非法。
|
||||
- **集成**:在 macOS 上运行 `npm run check-ports` 与 `npm run check-ports:dev`,确认有 bash + check-port.sh 时输出与 exit code 正确;关闭应用后再次检查,确认“未占用”与 exit 0。
|
||||
- **Windows**:在装有 prepare-git 与未装两种情况下各跑一次 CLI,确认有 bash 时走脚本、无时走 netstat 回退。
|
||||
|
||||
---
|
||||
|
||||
## 7. 小结
|
||||
|
||||
| 项 | 状态 |
|
||||
|----|------|
|
||||
| 三处脚本逻辑一致(.sh / 内嵌 / CLI 调用方式) | ✅ 一致 |
|
||||
| CLI 使用 spawnSync 传参(原 execSync 误用已修复) | ✅ 已修复 |
|
||||
| 配置解析与默认值 | ✅ 合理,0 与范围未校验见 4.1 |
|
||||
| Windows 有/无 bash 回退 | ✅ 正确 |
|
||||
| Unix 使用 bash,无 bash 时保守回退 | ✅ 可接受 |
|
||||
| 安全与注入 | ✅ 端口与 key 受控 |
|
||||
| 改进点 | 端口范围校验、DEFAULTS 同步方式、可选 timeout |
|
||||
|
||||
---
|
||||
|
||||
*Deepcheck 完成;CLI 中 execSync 误用已改为 spawnSync。*
|
||||
@@ -0,0 +1,93 @@
|
||||
# Code Review:启动端口聚合实现
|
||||
|
||||
## 1. 总体评价
|
||||
|
||||
- **聚合度**:端口默认值、配置键、解析规则集中在 `shared/startupPorts.ts`;主进程通过 `main/services/startupPorts.ts` 对接 DB 与 lsof/netstat,启动与重启均使用 `getConfiguredPorts()`,逻辑集中、无散落硬编码。
|
||||
- **可维护性**:新增/修改端口只需改 shared 默认值与解析,main 与脚本行为一致。
|
||||
- **安全性**:端口来自配置或常量,`execSync` 仅传入数字端口,无 shell 注入风险;脚本内 SQL 的 key 仅用内部常量并做引号转义。
|
||||
|
||||
---
|
||||
|
||||
## 2. 文件审阅
|
||||
|
||||
### 2.1 `src/shared/startupPorts.ts`
|
||||
|
||||
| 项 | 结论 |
|
||||
|----|------|
|
||||
| **step1 解析** | `step1?.agentPort ?? default` 正确;若 DB 存的是字符串或空对象会回退默认值。 |
|
||||
| **mcp 解析** | 兼容 `number` / `string`,`parseInt` 失败用默认值,逻辑正确。 |
|
||||
| **类型** | `GetSettingFn`、`StartupPorts` 清晰,便于 main/测试复用。 |
|
||||
| **STORAGE_KEYS** | 与 `constants.ts` 一致,避免键名硬编码。 |
|
||||
|
||||
**可选增强**:若将来需要严格校验,可对端口做范围检查(1–65535)并在解析后统一校验。
|
||||
|
||||
---
|
||||
|
||||
### 2.2 `src/main/services/startupPorts.ts`
|
||||
|
||||
| 项 | 结论 |
|
||||
|----|------|
|
||||
| **getConfiguredPorts()** | 在 `readSetting` 可用后调用(startup 在 DB 初始化之后),无时序问题。 |
|
||||
| **isPortInUse** | `port` 为 number,拼接进命令安全;Windows 使用 `:${port} ` 带空格,避免 60001 误匹配 600011。 |
|
||||
| **多 PID** | `lsof -t` 可能多行,只取第一行 PID 足够用于“是否占用”判断。 |
|
||||
| **异常** | `execSync` 失败时 catch 返回 `{ inUse: false }`,避免进程崩溃。 |
|
||||
|
||||
**跨平台**:Windows 用 netstat + findstr,且必须传 `shell: process.env.ComSpec || 'cmd.exe'` 才能正确解析管道与 `2>nul`;macOS/Linux 用 lsof(单命令无需 shell)。netstat 输出取首行最后一列作为 PID;行尾为 `\r\n` 时用 `split(/\r?\n/)` 兼容。
|
||||
|
||||
---
|
||||
|
||||
### 2.3 `scripts/tools/check-startup-ports.js`
|
||||
|
||||
| 项 | 结论 |
|
||||
|----|------|
|
||||
| **与 shared 一致** | 默认值、键名、解析顺序与 `shared/startupPorts.ts` 一致,注释已说明需同步修改。 |
|
||||
| **getSetting** | 使用 `key.replace(/'/g, "''")` 转义,key 仅来自内部常量,安全。 |
|
||||
| **DB 不存在** | 使用默认端口并打日志,行为合理。 |
|
||||
| **sqlite3 不可用** | 若 `resolvePortsFromSettings` 内调用 getSetting 时 `execSync('sqlite3 ...')` 抛错(如未安装 sqlite3),main() 的 try/catch 会捕获并回退默认端口 + 提示“读取 DB 失败”。 |
|
||||
|
||||
**已做修改**:删除未使用的 `projectRoot`;为 key 做 `String(key)` + 引号转义并加注释;DB 存在但读取失败时统一走默认端口并打明确日志。
|
||||
|
||||
---
|
||||
|
||||
### 2.4 调用方
|
||||
|
||||
| 位置 | 用法 | 结论 |
|
||||
|------|------|------|
|
||||
| **startup.ts** | `getConfiguredPorts().agent` 作为 ComputerServer 端口 | 正确,在 DB 已初始化后调用。 |
|
||||
| **serviceManager.ts** | `getConfiguredPorts().fileServer` 启动 File Server | 正确;其余仍用 step1Config(workspaceDir 等)无重复。 |
|
||||
| **processHandlers.ts** | 动态 `import('../services/startupPorts')` 后取 `fileServer` | 正确,避免循环依赖。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 边界情况
|
||||
|
||||
- **step1_config 为 null/undefined**:解析得到全部默认端口,符合预期。
|
||||
- **mcp_proxy_port 为非法字符串**:`parseInt` 得 NaN,使用默认 18099。
|
||||
- **端口被占用**:由各服务自己的 `listen()` 报 EADDRINUSE;`checkPortsInUse` / 脚本仅做启动前检查与提示。
|
||||
- **Windows 无 lsof**:使用 netstat,逻辑正确;脚本与 main 一致。
|
||||
|
||||
---
|
||||
|
||||
## 4. Windows 兼容性(已落实)
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| **管道与 shell** | 主进程与脚本在 Windows 上执行 netstat 与 findstr 管道命令时,必须传入 `shell: true`(脚本)或 `shell: process.env.ComSpec \|\| 'cmd.exe'`(main,满足 @types/node 的 shell: string),否则管道不生效。 |
|
||||
| **PID 解析** | netstat 输出可能多行(IPv4/IPv6),取首行并用 `split(/\s+/).pop()` 取最后一列作为 PID;行分隔用 `/\r?\n/` 兼容 CRLF。 |
|
||||
| **findstr 无匹配** | 无匹配时 exit code 1,execSync 抛错,catch 后返回 inUse: false,行为正确。 |
|
||||
| **DB 路径** | 脚本用 `path.join(home, '.qimingclaw', 'qimingclaw.db')`,Windows 为 `%USERPROFILE%\\.qimingclaw\\qimingclaw.db`;调用 sqlite3 时路径可转为正斜杠以规避反斜杠转义,且 Windows 下传 `shell: true` 便于执行 sqlite3.cmd。 |
|
||||
| **sqlite3** | Windows 上多数环境未预装 sqlite3,脚本读 DB 失败时已回退到默认端口并提示。 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 建议(可选)
|
||||
|
||||
1. **端口范围**:若需严格校验,可在 `resolvePortsFromSettings` 返回前或 `startComputerServer`/`startFileServer` 前统一校验 `1 <= port <= 65535`。
|
||||
2. **脚本与 shared 同步**:修改 `shared/startupPorts.ts` 的默认值或键时,记得同步改 `scripts/tools/check-startup-ports.js` 的 DEFAULTS/LABELS 和键名;必要时可考虑从 build 产物 require 解析逻辑以彻底单源(需解决 path alias 与运行环境)。
|
||||
3. **IPC 暴露**:若需要“设置页一键检查端口”,可在 preload 暴露 `startup:getConfiguredPortsWithStatus`,由渲染进程调 main 的 `getConfiguredPortsWithStatus(includeVite)` 并展示结果。
|
||||
|
||||
---
|
||||
|
||||
## 6. 小结
|
||||
|
||||
实现满足“逻辑尽量聚合”的目标,类型与异常处理合理,调用关系清晰。本次 review 中已做的小改动:脚本去掉未使用变量、key 转义与注释、DB 存在但读取失败时的回退与日志。其余为可选增强,可按需再做。
|
||||
Reference in New Issue
Block a user