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,513 @@
# Electron 子进程跨平台无窗口启动方案
> 本文档记录了 Qiming Agent Electron 客户端在子进程启动时的跨平台兼容性问题及解决方案。
## 问题描述
在 Electron 应用中使用 `child_process.spawn()` 启动 Node.js 子进程时,会遇到以下平台特定问题:
| 平台 | 问题 | 影响 |
|------|------|------|
| **Windows** | CMD 控制台窗口闪烁/弹出 | 用户体验差,看起来像恶意软件 |
| **macOS** | 子进程出现在 Dock 栏并跳动 | 用户体验差,显示为独立应用 |
| **Linux** | 可能显示在任务栏 | 取决于桌面环境 |
## 问题根因
### Windows CMD 弹窗
1. npm 安装的包在 `node_modules/.bin/` 目录下生成 `.cmd``.ps1` 脚本
2. 这些 `.cmd` 文件会调用 `cmd.exe` 执行
3. 即使设置了 `windowsHide: true``cmd.exe` 可能忽略父进程的窗口标志
4. 结果:控制台窗口短暂闪烁或持续显示
### macOS Dock 图标
1. 使用 `spawn(process.execPath, ...)` + `ELECTRON_RUN_AS_NODE=1` 启动子进程
2. `process.execPath` 指向 Electron 可执行文件
3. macOS 将其识别为**新的应用实例**
4. 即使以 Node.js 模式运行,仍会出现在 Dock 中
## 社区讨论
这是一个**广泛存在的社区问题**,多个项目和 issue 都有相关讨论:
### 相关 GitHub Issues
| Issue | 项目 | 描述 |
|-------|------|------|
| [#480](https://github.com/anthropics/claude-agent-sdk-python/issues/480) | claude-agent-sdk-python | Windows subprocess 终端窗口隐藏 |
| [#2634](https://github.com/electron/electron/issues/2634) | Electron | Electron spawn 子进程问题 |
| [#4179](https://github.com/tauri-apps/tauri/discussions/4179) | Tauri | 启动后隐藏 CMD 窗口 |
### Node.js 官方文档
> `windowsHide <boolean>` - Hide the subprocess console window that would normally be created on Windows systems. **Default: false**
>
> — [Node.js child_process 文档](https://nodejs.org/api/child_process.html)
**注意**Node.js v11.2.0 将 `windowsHide` 默认值恢复为 `false`,这意味着 GUI 应用需要**显式设置** `windowsHide: true`
### 常见方案对比
| 方案 | 社区使用度 | 优点 | 缺点 |
|------|-----------|------|------|
| `windowsHide: true` | ⭐⭐⭐⭐⭐ | 简单,官方支持 | 对 `.cmd` 文件可能无效 |
| `cross-spawn` 模块 | ⭐⭐⭐⭐ | 跨平台自动处理 | 仍使用 `.cmd` 文件 |
| 绕过 `.cmd` 直接执行 JS | ⭐⭐⭐ | 彻底解决 | 需要解析入口 |
| `CREATE_NO_WINDOW` (Rust) | ⭐⭐⭐ | Windows API 级别 | 仅限 Rust/Native |
### 为什么 `windowsHide: true` 对 `.cmd` 文件可能无效
```
┌─────────────────────────────────────────────────────────────┐
│ spawn('npm', [...], { windowsHide: true }) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ npm.cmd (batch file) │ │
│ │ │ │ │
│ │ ▼ (创建新的 cmd.exe 进程) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ cmd.exe /c "node npm-cli.js" │ │ │
│ │ │ │ │ │ │
│ │ │ │ ⚠️ cmd.exe 可能忽略父进程的 │ │ │
│ │ │ │ windowsHide 标志 │ │ │
│ │ │ ▼ │ │ │
│ │ │ [控制台窗口弹出] │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
**解决方案**:绕过 `.cmd` 文件,直接执行 JS 入口文件。
## 解决方案
### 最终方案:平台差异化策略
```
┌─────────────┬────────────────────────────────────────────────┐
│ 平台 │ 策略 │
├─────────────┼────────────────────────────────────────────────┤
│ Windows │ Electron bundled Node + ELECTRON_RUN_AS_NODE=1 │
│ │ + windowsHide: true + 绕过 .cmd 文件 │
├─────────────┼────────────────────────────────────────────────┤
│ macOS │ 系统 node从用户 shell PATH 解析) │
├─────────────┼────────────────────────────────────────────────┤
│ Linux │ 系统 node从用户 shell PATH 解析) │
└─────────────┴────────────────────────────────────────────────┘
```
### 实现代码
#### 1. 核心工具模块 (`spawnNoWindow.ts`)
```typescript
import { spawn, ChildProcess, execSync } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
/**
* 解析用户 shell PATHmacOS/Linux
*
* 打包后的 Electron 应用不会继承用户的 shell profile
* 所以 node/npm 等工具不会在 PATH 中,需要手动解析。
*/
function resolveUserShellPath(): string | null {
if (process.platform === 'win32') return null;
try {
const shell = process.env.SHELL || '/bin/bash';
// 使用 login shell (-il) 加载用户的 profile 文件
const result = execSync(`${shell} -ilc 'echo __PATH__=$PATH'`, {
encoding: 'utf-8',
timeout: 5000,
});
const match = result.match(/__PATH__=(.+)/);
return match ? match[1].trim() : null;
} catch {
return null;
}
}
/**
* 查找系统 node 可执行文件macOS/Linux
*
* 支持多种 node 安装方式:
* - Homebrew: /opt/homebrew/bin/node, /usr/local/bin/node
* - nvm: ~/.nvm/versions/node/...
* - volta: ~/.volta/bin/node
* - fnm: ~/.fnm/...
*/
function findSystemNode(): string {
if (process.platform === 'win32') {
return process.execPath; // Windows 使用 Electron bundled Node
}
const userPath = resolveUserShellPath();
if (userPath) {
// 在用户 PATH 中搜索 node
for (const dir of userPath.split(path.delimiter)) {
const nodePath = path.join(dir, 'node');
if (fs.existsSync(nodePath)) return nodePath;
}
}
// 回退:尝试常见安装路径
const home = process.env.HOME || '';
const commonPaths = [
'/usr/local/bin/node',
'/opt/homebrew/bin/node',
'/usr/bin/node',
path.join(home, '.nvm/versions/node/current/bin/node'),
path.join(home, '.volta/bin/node'),
];
for (const p of commonPaths) {
if (fs.existsSync(p)) return p;
}
return 'node'; // 最终回退
}
/**
* 解析 npm 包的 JS 入口文件
*
* 绕过 .cmd 文件,直接执行 JS 文件
*/
function resolveNpmPackageEntry(packageDir: string, binName?: string): string | null {
const pkgJsonPath = path.join(packageDir, 'package.json');
if (!fs.existsSync(pkgJsonPath)) return null;
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
// 从 package.json 的 bin 字段获取入口
let binPath: string | undefined;
if (typeof pkg.bin === 'string') {
binPath = pkg.bin;
} else if (pkg.bin && typeof pkg.bin === 'object') {
const name = binName || pkg.name;
binPath = name ? pkg.bin[name] : Object.values(pkg.bin)[0] as string;
}
if (binPath) {
const entryPath = path.join(packageDir, binPath);
if (fs.existsSync(entryPath)) return entryPath;
}
return null;
}
/**
* 跨平台无窗口启动 JS 文件
*/
export function spawnJsFile(
jsFile: string,
args: string[] = [],
options: SpawnOptions = {},
): ChildProcess {
let node: string;
let env: Record<string, string | undefined>;
if (process.platform === 'win32') {
// Windows: 使用 Electron bundled Node
node = process.execPath;
env = {
...process.env,
...options.env,
ELECTRON_RUN_AS_NODE: '1', // 让 Electron 以 Node.js 模式运行
};
} else {
// macOS/Linux: 使用系统 node
node = findSystemNode();
env = {
...process.env,
...options.env,
PATH: resolveUserShellPath() || process.env.PATH,
};
}
return spawn(node, [jsFile, ...args], {
...options,
env,
windowsHide: true, // 防止 Windows 控制台窗口
});
}
/**
* 启动 npm 包(自动解析入口)
*/
export function spawnNpmPackage(
packageDir: string,
args: string[] = [],
options: SpawnOptions = {},
binName?: string,
): ChildProcess | null {
const entryFile = resolveNpmPackageEntry(packageDir, binName);
if (!entryFile) return null;
return spawnJsFile(entryFile, args, options);
}
```
#### 2. 使用示例
```typescript
import { spawnJsFile, spawnNpmPackage } from './utils/spawnNoWindow';
// 启动 JS 文件
const child1 = spawnJsFile('/path/to/script.js', ['--port', '8080']);
// 启动 npm 包
const child2 = spawnNpmPackage(
'/path/to/node_modules/mcp-stdio-proxy',
['proxy', '--port', '9000'],
{ env: { CUSTOM_VAR: 'value' } },
'mcp-proxy' // bin 名称(可选)
);
// 处理输出
child2.stdout?.on('data', (data) => {
console.log('stdout:', data.toString());
});
child2.stderr?.on('data', (data) => {
console.error('stderr:', data.toString());
});
```
## 方案对比
| 方案 | Windows 弹窗 | macOS Dock | API 兼容 | 依赖 |
|------|:------------:|:----------:|:--------:|------|
| **系统 node + spawn** | ❌ CMD 弹窗 | ✅ 无 Dock | ✅ spawn | 系统安装 node |
| **Electron + ELECTRON_RUN_AS_NODE** | ✅ 无弹窗 | ❌ 有 Dock | ✅ spawn | 无 |
| **平台差异化(本方案)** | ✅ 无弹窗 | ✅ 无 Dock | ✅ spawn | macOS/Linux 需系统 node |
| **child_process.fork()** | ✅ 无弹窗 | ✅ 无 Dock | ⚠️ IPC | 无 |
| **utilityProcess.fork()** | ✅ 无弹窗 | ✅ 无 Dock | ⚠️ MessagePort | Electron 22+ |
### 为什么选择平台差异化方案
1. **保持 spawn API** - 支持 stdio 流式通信ACP 协议需要 NDJSON
2. **兼容原生模块** - fork() 在 Electron 中对原生模块有兼容问题
3. **无需修改 Info.plist** - 不影响主应用的 Dock 行为
4. **参考 LobsterAI** - 已经过生产环境验证
## 替代方案
### 1. Electron utilityProcess官方推荐
Electron 22+ 提供的官方 API使用 Chromium Services API
```typescript
const { utilityProcess } = require('electron');
const child = utilityProcess.fork('./worker.js', ['--arg'], {
stdio: 'pipe',
serviceName: 'MyService'
});
// 使用 MessagePort 通信
child.postMessage({ data: 'hello' });
child.on('message', (msg) => console.log(msg));
```
**限制**
- 只能在 Electron 主进程使用
- 使用 MessagePort 而非 stdio
- 需要重构现有通信层
### 2. child_process.fork()
```typescript
const { fork } = require('child_process');
const child = fork('./worker.js', ['--arg']);
// 使用 IPC 通信
child.send({ data: 'hello' });
child.on('message', (msg) => console.log(msg));
```
**限制**
- 使用 IPC 而非 stdio
- 在 Electron 中对原生模块有兼容问题
- 依赖 ELECTRON_RUN_AS_NODE可能被 Fuse 禁用)
### 3. Info.plist 配置(仅 macOS
对于纯后台应用,可以在打包时配置:
```json
// electron-builder 配置
{
"mac": {
"extendInfo": {
"LSUIElement": 1 // 不显示 Dock 图标
}
}
}
```
**限制**
- 只影响 macOS
- 会影响整个应用(包括主应用)
## 测试验证
### 测试用例
```typescript
describe('spawnNoWindow', () => {
describe('Windows 行为', () => {
it('应使用 ELECTRON_RUN_AS_NODE=1', () => {
// 模拟 Windows 平台
Object.defineProperty(process, 'platform', { value: 'win32' });
spawnJsFile('/path/to/script.js');
expect(callArgs[0]).toBe(process.execPath);
expect(callArgs[2].env.ELECTRON_RUN_AS_NODE).toBe('1');
});
});
describe('macOS/Linux 行为', () => {
it('应使用系统 node不设置 ELECTRON_RUN_AS_NODE', () => {
Object.defineProperty(process, 'platform', { value: 'darwin' });
spawnJsFile('/path/to/script.js');
expect(callArgs[0]).toBe('/usr/local/bin/node'); // 系统 node
expect(callArgs[2].env.ELECTRON_RUN_AS_NODE).toBeUndefined();
});
});
});
```
### 验证步骤
1. **Windows 验证**
- 运行应用,触发子进程启动
- 确认无 CMD 窗口弹出
2. **macOS 验证**
- 运行应用,触发子进程启动
- 确认 Dock 中无新图标出现
3. **Linux 验证**
- 运行应用,触发子进程启动
- 确认无意外的任务栏图标
## 相关文件
| 文件 | 说明 |
|------|------|
| `src/main/services/utils/spawnNoWindow.ts` | 核心工具模块 |
| `src/main/services/utils/spawnNoWindow.test.ts` | 单元测试 |
| `src/main/services/packages/mcp.ts` | MCP Proxy 启动(使用此工具) |
---
## 内置 Node.js 24 和 Git 集成2026-02-27
### 背景
参考 [LobsterAI 方案](https://github.com/netease-youdao/LobsterAI)
- Electron 内置的 Node.js **不包含 npm/npx**
- Windows 用户通常没有预装 Node.js
- 需要集成独立的 Node.js 和 Git
### 集成方案
#### 1. 脚本准备
| 脚本 | 功能 |
|------|------|
| `scripts/prepare/prepare-node.js` | 下载 Node.js 24 到 resources/node/ |
| `scripts/prepare/prepare-git.js` | 下载 PortableGit 到 resources/git/(仅 Windows |
#### 2. 打包配置
```json
// package.json
"extraResources": [
{ "from": "resources/node", "to": "node" },
{ "from": "resources/git", "to": "git" }
]
```
#### 3. PATH 优先级Windows
```
1. resources/node/bin ← 内置 Node.js 24最高
2. Electron 内置 Node
3. resources/git/bin ← 内置 Git
4. 应用内 node_modules
5. uv
6. Windows 系统目录System32, Wbem, PowerShell
7. 注册表最新 PATH ← 用户后安装的工具
```
#### 4. 环境变量
| 变量 | 说明 |
|------|------|
| `QIMINGCODE_NODE_DIR` | qimingcode-acp 使用 |
| `CLAUDE_CODE_NODE_DIR` | claude-code-acp-ts 使用 |
| `QIMINGCODE_GIT_BASH_PATH` | qimingcode-acp 使用 |
| `CLAUDE_CODE_GIT_BASH_PATH` | claude-code-acp-ts 使用 |
| `MSYS2_PATH_TYPE=inherit` | git-bash 正确继承 PATH |
| `ORIGINAL_PATH` | POSIX 格式 PATH |
#### 5. Windows 特殊优化
- **关键系统变量**SystemRoot, windir, COMSPEC, SYSTEMDRIVE
- **系统目录 PATH**System32, System32\Wbem, WindowsPowerShell, OpenSSH
- **注册表读取**:从注册表读取最新 PATH解决后安装工具不在 PATH 问题
- **Electron Node Shim**:创建 node/npm 桥接脚本
### 相关文件
| 文件 | 说明 |
|------|------|
| `scripts/prepare/prepare-node.js` | Node.js 下载脚本 |
| `scripts/prepare/prepare-git.js` | Git 下载脚本 |
| `src/main/services/system/dependencies.ts` | 环境变量配置 |
| `src/main/services/engines/engineManager.ts` | 引擎启动(使用此工具) |
| `src/main/services/engines/acp/acpClient.ts` | ACP 客户端(使用此工具) |
## 参考资料
### 官方文档
- [Node.js child_process 文档](https://nodejs.org/api/child_process.html) - `windowsHide` 选项说明
- [Electron utilityProcess API](https://www.electronjs.org/docs/latest/api/utility-process) - Electron 22+ 推荐方案
- [Electron Dock API](https://www.electronjs.org/docs/latest/api/dock) - macOS Dock 控制
- [Electron Fuses - runAsNode](https://www.electronjs.org/docs/latest/tutorial/fuses) - ELECTRON_RUN_AS_NODE 安全控制
### GitHub Issues
- [electron/electron #2634](https://github.com/electron/electron/issues/2634) - Electron spawn 子进程问题
- [electron/electron #8727](https://github.com/electron/electron/issues/8727) - fork 和原生模块兼容问题
- [anthropics/claude-agent-sdk-python #480](https://github.com/anthropics/claude-agent-sdk-python/issues/480) - Windows subprocess 终端窗口隐藏
- [tauri-apps/tauri #4179](https://github.com/tauri-apps/tauri/discussions/4179) - Tauri 启动后隐藏 CMD 窗口
### 社区方案
- [LobsterAI coworkUtil.ts](https://github.com/LobsterAI/LobsterAI) - shell PATH 解析参考(本方案灵感来源)
- [cross-spawn](https://www.npmjs.com/package/cross-spawn) - 跨平台 spawn 封装(仍使用 .cmd 文件)
- [nano-spawn](https://www.npmjs.com/package/nano-spawn) - 现代 spawn 替代方案
## 更新历史
| 日期 | 版本 | 变更 |
|------|------|------|
| 2026-02-25 | 1.0 | 初始版本,记录完整解决方案 |
| 2026-02-25 | 1.1 | 添加社区讨论和 GitHub Issues 参考 |
---
*文档维护Qiming Agent 开发团队*