chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -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 chainWindows 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-commandclaude-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 级别(受限 tokensandbox 未启用时命令以用户权限运行。
### 无命令白名单/黑名单
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 源码

View File

@@ -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` 自动判断。

View File

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

View File

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

View File

@@ -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。
---
## 一、端口与服务的对应关系(以最终配置为准)
检查端口时应使用 **当前配置的端口**,而不是写死的默认值。各服务端口来源如下:
| 服务 | 说明 | 配置来源 | 默认端口 |
|------|------|----------|----------|
| **AgentComputerServer** | /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 一致或使用 60002Agent Runner 代理默认) | 远程常 10076本地 60002 |
- 若未在应用内改过端口,则按上表**默认端口**检查即可。
- 若改过端口,请以**设置页或下面「获取当前配置端口」**得到的值为准。
---
## 二、获取当前配置端口(可选)
**方式一:应用内**
打开 Qiming Agent → 设置 → 查看「Agent 端口 / 后端端口」「File Server 端口」「MCP 代理端口」「Lanproxy 服务端端口」(远程)及本地代理相关端口(若可见)。开发时 Vite 端口见项目根下 `vite.config.ts`
**方式二:从 SQLite 读取(应用未运行时)**
```bash
DB="$HOME/.qimingclaw/qimingclaw.db"
# step1_config 内含 agentPort、fileServerPortJSON
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>` 或浏览器访问,确认可达。 |
---
## 八、检查清单小结(可打印或保存)
- [ ] 以**当前配置端口**检查AgentComputerServer、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*

View File

@@ -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 / 脚本无 .shmain 用 `execSync` + cmd脚本用 `execSync` + shell: true或 lsof行为与之前一致。
---
## 3. 平台与环境
### 3.1 Windows
- **有 prepare-git**`getBundledGitBashPath()` 返回 `resources/git/bin/bash.exe`(或 usr/binmain 与 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 端口合法性
- 当前未校验端口范围165535及 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。*

View File

@@ -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` 一致,避免键名硬编码。 |
**可选增强**若将来需要严格校验可对端口做范围检查165535并在解析后统一校验。
---
### 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 ...')` 抛错(如未安装 sqlite3main() 的 try/catch 会捕获并回退默认端口 + 提示“读取 DB 失败”。 |
**已做修改**:删除未使用的 `projectRoot`;为 key 做 `String(key)` + 引号转义并加注释DB 存在但读取失败时统一走默认端口并打明确日志。
---
### 2.4 调用方
| 位置 | 用法 | 结论 |
|------|------|------|
| **startup.ts** | `getConfiguredPorts().agent` 作为 ComputerServer 端口 | 正确,在 DB 已初始化后调用。 |
| **serviceManager.ts** | `getConfiguredPorts().fileServer` 启动 File Server | 正确;其余仍用 step1ConfigworkspaceDir 等)无重复。 |
| **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 1execSync 抛错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 存在但读取失败时的回退与日志。其余为可选增强,可按需再做。