chore: initialize qiming workspace repository
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# Electron 客户端 Review(2026-02)
|
||||
|
||||
## 1. 已做修正
|
||||
|
||||
### 1.1 Windows CI 上传步骤
|
||||
- **问题**:`Upload artifacts to Release` 使用 bash 语法,Windows runner 默认 shell 为 PowerShell,会导致上传失败。
|
||||
- **修正**:为该 step 增加 `shell: bash`,三平台统一用 bash 执行上传脚本。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构与脚本
|
||||
|
||||
### 2.1 打包脚本(package.json)
|
||||
- **dist:mac** / **dist:win** / **dist:linux**:单平台、Mac 走签名/公证(需证书与 env)。
|
||||
- **dist:mac:unsigned**:仅 Mac、无签名,用于无证书环境。
|
||||
- **dist:unsigned:local**:当前平台无签名,避免在 Mac 上同时打 Win/Linux 时的 node-gyp 交叉编译。
|
||||
- **dist:all** / **dist:unsigned**:一次传 `--mac --win --linux`,在 arm64 Mac 上会因 better-sqlite3 交叉编译失败,仅适合在无 native 依赖或非交叉编译场景使用;当前推荐用 CI 分平台打。
|
||||
|
||||
### 2.2 prepare 脚本
|
||||
- **prepare:uv**:多平台,从 GitHub 下载或使用已有 uv,正常。
|
||||
- **prepare:sign-uv**:仅 macOS 执行签名,其他平台跳过,正常。
|
||||
- **prepare-lanproxy**:缺二进制时改为**仅警告 + 创建目录**,不 `process.exit(1)`,CI 可正常产出 Win/Linux 安装包,该平台下 lanproxy 功能不可用。
|
||||
- **prepare-sdk**:若 `vendors/qimingcode-sdk` 不存在则 `process.exit(0)`;若仓库未包含或未 clone 该路径,后续 electron-builder 可能因 `file:` 依赖或符号链接报错,需保证 CI 与本地均有该目录(或等效依赖)。
|
||||
|
||||
### 2.3 主进程 import 路径
|
||||
- 已按 `tsconfig.main.json` 的 rootDir 修正:`acpClient`、`engineManager`、`unifiedAgent`、`dependencies` 中对 `constants` / `@shared/constants` 的相对路径,`build:main` 可正常通过。
|
||||
|
||||
---
|
||||
|
||||
## 3. CI(release-electron.yml)
|
||||
|
||||
- **触发**:tag `electron-v*`,与 Tauri 的 `v*` 分离,设计合理。
|
||||
- **prepare**:解析版本、读取 release notes、创建 Release,逻辑清晰。
|
||||
- **build-electron**:matrix 三平台(macos / windows / ubuntu),各跑对应 `dist:*`,fail-fast: false 可接受。
|
||||
- **macOS**:可选导入证书与公证 key,未配置时走 `dist:mac:unsigned`,合理。
|
||||
- **上传**:已改为 `shell: bash`,Windows 上也可正确执行。
|
||||
|
||||
---
|
||||
|
||||
## 4. 风险与建议
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| **vendors/qimingcode-sdk** | 若未提交或 CI 未包含,prepare-sdk 会跳过,可能导致构建或打包失败。建议:CI 确保该路径存在或使用可选的 file 依赖策略。 |
|
||||
| **lanproxy 缺失** | 已做:主进程返回友好错误文案;新增 `lanproxy:isAvailable` IPC;设置页在无二进制时展示「当前平台暂不支持内网穿透」并禁用启动。 |
|
||||
| **dist:unsigned:local 语义** | 当前为「当前平台 + 无签名」。若希望本地默认走签名,保留 `dist:mac` 为默认 Mac 打包方式即可;`dist:unsigned:local` 明确为「快速无签名构建」即可。 |
|
||||
| **Release 已存在** | 当前逻辑为 `gh release view` 存在则只打印 "Release already exists" 不报错,后续上传会覆盖同名的 asset(--clobber)。若需避免覆盖,可改为上传前检查或使用不同 artifact 名称。 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 日志设计(logConfig)
|
||||
|
||||
### 5.1 实现要点
|
||||
- **logConfig.ts**:`initLogging()` 在 main 入口最早调用,设置 electron-log 的 file transport:路径、maxSize、level、自定义 `archiveLogFn`(轮转时重命名为 `main.YYYY-MM-DD-HHmmss.log`),并在启动时执行 TTL 清理。
|
||||
- **开发 vs 正式**:`isDev()` 用 `!app.isPackaged` 与 `NODE_ENV` 判断;开发 5MB/30 天、正式 2MB/7 天,文件级别分别为 debug / info。
|
||||
- **cleanupOldLogs**:只删 `main`/`renderer` 的 `.old.log` 与 `.YYYY-MM-DD-*.log`,不删 `main.log`、`renderer.log` 及其他文件(如 app.json、mcp/ 等)。
|
||||
|
||||
### 5.2 审查结论
|
||||
- 轮转与 TTL 逻辑清晰,归档命名可区分且便于按时间删除。
|
||||
- `archiveLogFn` 内调用 `log.info` 会写入当前 main.log(轮转后为新文件),不会造成递归轮转。
|
||||
- 已扩展 TTL 清理:对同一 logDir 下的 `renderer.old.log`、`renderer.YYYY-MM-DD-*.log` 应用相同 TTL,与 main 归档一并清理。
|
||||
- **LOGGING.md** 已说明目录、行为与配置入口。
|
||||
|
||||
### 5.3 latest.log 统一入口(多平台)
|
||||
- **实现**:`updateLatestLog(logDir)` 在 init 结束时通过 `setImmediate` 调用(保证 main.log 已存在);轮转时仅在 Windows 下通过 `setImmediate` 再次调用,因硬链接指向 inode 需重新指向新 main.log。
|
||||
- **macOS/Linux**:`fs.symlinkSync('main.log', latestPath, 'file')`,相对路径,轮转后无需更新。
|
||||
- **Windows**:`fs.linkSync(mainPath, latestPath)` 硬链接,无需管理员/开发者模式;轮转后 setImmediate 时新 main.log 已由 transport 同步写入,时序正确。
|
||||
- **log:list**:优先读 `latest.log`,不存在则回退 `getFile().path`;`log:getDir` / `log:openDir` 仍为目录,用户打开目录后可见 `latest.log`。
|
||||
- **cleanupOldLogs** 不删 `latest.log`(`isArchiveLogName` 不匹配),符合预期。
|
||||
- **可选改进已做**:`log:openDir` 在 macOS 上 `open -R <latest.log>`、在 Windows 上 `explorer /select,<latest.log>`,打开目录并选中当前日志入口;失败时回退为仅打开目录;Linux 仍为仅打开目录。
|
||||
|
||||
---
|
||||
|
||||
## 6. 文档与约定
|
||||
|
||||
- **SIGNING.md**:记录 Mac 签名/公证与 env,便于本地与 CI 配置。
|
||||
- **release-notes/**:支持 `release-notes/electron-v<version>.md` 作为 Release 说明,与 workflow 一致。
|
||||
- 应用名与目录:使用 `APP_DISPLAY_NAME` / `APP_NAME_IDENTIFIER`(及 `APP_DATA_DIR_NAME`)统一,避免硬编码 `.qimingclaw` / `qiming-agent`。
|
||||
- **LOGGING.md**:日志轮转、TTL、开发/正式差异说明。
|
||||
|
||||
---
|
||||
|
||||
*Review 完成;Windows 上传步骤已修正;日志设计与 latest.log 多平台方案已纳入审查。*
|
||||
@@ -0,0 +1,76 @@
|
||||
# Review:Node 24 与 Git 仅 Windows 集成
|
||||
|
||||
## 变更范围
|
||||
|
||||
1. **prepare-git.js** — 对齐 LobsterAI 方案(7z + 7zip-bin),并明确「仅 Windows 客户端」
|
||||
2. **prepare-node.js** — 非 Windows 直接跳过,仅 Windows 下载/解压 Node 24
|
||||
3. **package.json** — node/git 的 extraResources 仅挂在 `win` 下,Mac/Linux 不再打包
|
||||
4. **dependencies.ts** — `getBundledNodeBinDir` 按 prepare-node 实际输出路径查找(见下)
|
||||
|
||||
---
|
||||
|
||||
## 1. prepare-git.js ✅
|
||||
|
||||
| 项 | 结论 |
|
||||
|----|------|
|
||||
| **仅 Windows** | 非 Windows 默认跳过;`--required` / `QIMING_SETUP_GIT_FORCE=1` 可强制(如 macOS 上为 Windows 打包) |
|
||||
| **7z + 7zip-bin** | 与 LobsterAI 一致,解压稳定、可跨平台 |
|
||||
| **环境变量** | `QIMING_PORTABLE_GIT_ARCHIVE`、`QIMING_GIT_URL` 文档完整 |
|
||||
| **bash 查找** | `findPortableGitBash` 检查 bin、usr/bin、mingw64/bin、mingw64/usr/bin,覆盖常见解压结构 |
|
||||
| **导出** | `ensurePortableGit`、`findPortableGitBash`、`GIT_VERSION`、`GIT_ROOT` 可供主进程/CLI 复用 |
|
||||
|
||||
**建议**:CI 若 GitHub 下载不稳,可在 workflow 中设置 `QIMING_PORTABLE_GIT_ARCHIVE` 或使用缓存步骤。
|
||||
|
||||
---
|
||||
|
||||
## 2. prepare-node.js ✅
|
||||
|
||||
| 项 | 结论 |
|
||||
|----|------|
|
||||
| **仅 Windows** | `main()` 开头 `process.platform !== 'win32'` 则直接 return |
|
||||
| **输出路径** | `resources/node/win32-x64` 或 `resources/node/win32-arm64`,与 dependencies 约定一致 |
|
||||
| **NODE_ASSET_SUFFIX** | 仍含 darwin/linux 键,但不会执行到(已提前 return),可后续删减以去混淆(非必须) |
|
||||
|
||||
**注意**:`getBundledNodeBinDir()` 已改为按 `node/win32-x64`(或 win32-arm64)查找,与 prepare-node 输出一致。
|
||||
|
||||
---
|
||||
|
||||
## 3. package.json 打包 ✅
|
||||
|
||||
| 项 | 结论 |
|
||||
|----|------|
|
||||
| **全局 extraResources** | 已移除 `resources/node`、`resources/git`,Mac/Linux 包不再包含 |
|
||||
| **win.extraResources** | 仅 Windows 安装包包含 node、git 两项 |
|
||||
| **electron-builder** | 平台专属 `win.extraResources` 会与顶层 extraResources 合并,行为符合预期 |
|
||||
|
||||
---
|
||||
|
||||
## 4. dependencies.ts ✅(已修)
|
||||
|
||||
| 项 | 结论 |
|
||||
|----|------|
|
||||
| **getBundledNodeBinDir** | **已修复**:由原来的 `node/bin` 改为 `node/win32-x64` 或 `node/win32-arm64`(与 prepare-node 输出一致) |
|
||||
| **getBundledGitBinDir / getBundledGitBashPath** | 仅 Windows 返回非空;查找路径与 prepare-git 的 bin/usr/bin 一致 |
|
||||
| **getAppEnv PATH** | 继续使用 `bundledNodeBinDir`、`bundledGitBinDir`,逻辑正确 |
|
||||
|
||||
此前存在 **路径不一致**:prepare-node 写入 `resources/node/win32-x64`,而运行时查找 `resources/node/bin`,会导致 Windows 上内置 Node 从未被用上。现已统一为按平台键查找。
|
||||
|
||||
---
|
||||
|
||||
## 5. 可选后续优化
|
||||
|
||||
1. **prepare-node.js**:可删除 `NODE_ASSET_SUFFIX` 中的 darwin/linux 键,仅保留 win32-x64、win32-arm64,减少误解。
|
||||
2. **getBundledGitBinDir**:若 PortableGit 某版本解压后只有 `mingw64/bin`,可增加对 `resources/git/mingw64/bin` 的回退(与 `findPortableGitBash` 一致)。
|
||||
3. **文档**:`electron-spawn-no-window-solution.md`、`CHANGELOG.md` 中若仍写 `resources/node/bin`,可改为 `resources/node/win32-x64` 等以与实现一致。
|
||||
|
||||
---
|
||||
|
||||
## 6. 测试建议
|
||||
|
||||
- **Windows**:在干净 clone 下执行 `npm run prepare:git`、`npm run prepare:node`,确认 `resources/git`、`resources/node/win32-x64` 生成;再打 Windows 包,运行时确认 PATH 中能用到内置 node/npm 与 git-bash。
|
||||
- **macOS/Linux**:执行 `prepare:all` 或打 Mac/Linux 包,确认 prepare-node/prepare-git 跳过且安装包内无 node、git 目录。
|
||||
- **CI**:Windows 构建若遇下载失败,可设 `QIMING_PORTABLE_GIT_ARCHIVE` 或 `QIMING_GIT_URL`,或为 Git 归档加缓存。
|
||||
|
||||
---
|
||||
|
||||
*Review 完成;getBundledNodeBinDir 路径已与 prepare-node 对齐并落地修改。*
|
||||
@@ -0,0 +1,49 @@
|
||||
# 敏感信息与安全自查(agent-electron-client)
|
||||
|
||||
## 结论
|
||||
|
||||
**当前代码未发现硬编码密钥或明显敏感信息泄露。** 已对以下点做了修正与约定说明。
|
||||
|
||||
---
|
||||
|
||||
## 已修正
|
||||
|
||||
### 1. 日志中泄露 API Key(processHandlers.ts)
|
||||
|
||||
- **问题**:`agentRunner:start` 中 `log.info(..., args.join(' '))` 会输出完整命令行参数,包含 `--api-key <真实 key>`。
|
||||
- **修正**:仅记录 `binPath`、`backendPort`、`proxyPort`、`apiBaseUrl`,不再记录含 apiKey 的 args。
|
||||
|
||||
### 2. 登录失败时打印完整 error(auth.ts)
|
||||
|
||||
- **问题**:`console.error('[Auth] 登录失败:', error)` 可能把含请求体(如 password)的 error 对象输出到控制台/日志。
|
||||
- **修正**:改为只输出 `console.error('[Auth] 登录失败:', errorMessage)`,不再打印整个 error。
|
||||
|
||||
### 3. 登录成功时打印 configKey(auth.ts)
|
||||
|
||||
- **问题**:`console.log('[Auth] 登录成功:', { configKey: response.configKey, ... })` 会把客户端 configKey 写入日志。
|
||||
- **修正**:改为 `configKeySet: !!response.configKey`,只记录是否设置,不记录内容。
|
||||
|
||||
---
|
||||
|
||||
## 已确认安全或低风险
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| **API Key 占位符** | UI 中 `placeholder="sk-ant-..."` 仅为占位文案,非真实密钥。 |
|
||||
| **acpClient 日志** | `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` 仅打印前若干字符 + `...`,已脱敏。 |
|
||||
| **凭证存储** | API key、password、configKey、saved_key 等均通过 IPC 存 SQLite(settings),未写进代码或仓库。 |
|
||||
| **环境变量** | `dist:mac:unsigned` 等脚本中 `APPLE_API_KEY=` / `APPLE_ISSUER_ID=` 等置空,用于关闭签名与公证,不涉及真实密钥。 |
|
||||
| **文档** | SIGNING.md、verify-sign.js 仅说明 env 名称,无密钥内容。 |
|
||||
| **公开 URL** | `DEFAULT_SERVER_HOST`、npm/pypi 镜像等为公开默认配置,非敏感。 |
|
||||
|
||||
---
|
||||
|
||||
## 建议
|
||||
|
||||
1. **生产构建**:若使用 electron-log 等写文件,确保日志轮转与权限控制,避免日志文件被未授权访问。
|
||||
2. **configKey / saved_key**:已不在日志中输出全文;若后续有调试需要,仅建议在开发环境且脱敏后使用。
|
||||
3. **依赖**:定期 `npm audit`,及时升级存在已知漏洞的依赖。
|
||||
|
||||
---
|
||||
|
||||
*自查日期:2026-02*
|
||||
@@ -0,0 +1,99 @@
|
||||
# Electron 相关方案留存(2026-03)
|
||||
|
||||
本文档固化若干**已落地或评审通过**的设计与修复,便于后续合并分支、排查回归时对照。实现位置以 `qiming-agent` 主仓库为准;独立 worktree 中的实验以「附录」说明。
|
||||
|
||||
---
|
||||
|
||||
## 1. FirstTokenTrace 请求级采样(均匀分布)
|
||||
|
||||
### 问题
|
||||
|
||||
按 `requestId` 的 MD5 前 8 个十六进制字符得到 32 位无符号整数,若除以 `0xFFFFFFFF`(即 \(2^{32}-1\)),则最大哈希对应比值为 **1.0**,严格说不属于半开区间 \([0, 1)\),且与「将 \(2^{32}\) 个整点均匀映射到单位区间」的常规定义不一致,在 `sampleRate` 接近 1 时会产生可观测偏差。
|
||||
|
||||
### 方案
|
||||
|
||||
- 除数使用 **\(2^{32}\)**,即十六进制字面量 `0x100000000`(或等价 `2 ** 32`)。
|
||||
- 判定仍为 `v < sampleRate`,其中 `v ∈ [0, 1)`。
|
||||
|
||||
### 代码位置
|
||||
|
||||
- `src/main/services/engines/perf/firstTokenTrace.ts` → `shouldSample()`
|
||||
|
||||
### 与性能报告的关系
|
||||
|
||||
首条消息/首 token 相关分析见 `docs/optimization/MACOS-FIRST-MESSAGE-PERF-REPORT-2026-03-31.md` 等;采样用于控制磁盘与日志量,不影响功能正确性,但影响统计代表性。
|
||||
|
||||
---
|
||||
|
||||
## 2. `sync-oss.sh` 与 GitHub Actions 运行记录
|
||||
|
||||
### 背景
|
||||
|
||||
脚本在**本机**通过 `gh` 触发 **目标仓库**(默认 `qiming-ai/qimingclaw`)的 `sync-electron-to-oss.yml`,此前存在两类风险:
|
||||
|
||||
1. **`gh run list --limit 1`**:取的是「列表中最新一条」,并发或他人刚触发的 run 会导致**跟错 run**。
|
||||
2. **未指定 `--repo`**:`gh` 默认指向当前 checkout 的仓库;若在 `qiming-agent` 目录执行而实际 dispatch 到 `qimingclaw`,会列到**错误仓库**的 run。
|
||||
|
||||
### 方案
|
||||
|
||||
1. 在发起 `workflow_dispatch` 的 HTTP 请求**之前**,记录 `START_EPOCH`(当前 UTC 时间戳减约 10 秒,抵消客户端与 GitHub 时钟差)。
|
||||
2. 触发成功后轮询 `gh run list --repo "$REPO" --workflow=sync-electron-to-oss.yml`,在结果中筛选:
|
||||
- `event == "workflow_dispatch"`;
|
||||
- `createdAt` 解析为 epoch 后 **≥ `START_EPOCH`**;
|
||||
- 取其中按时间**最新**一条的 `databaseId`。
|
||||
3. 所有 `gh run view` / 输出日志 URL 均带 **`--repo "$REPO"`**。
|
||||
4. 若在超时轮询内仍无法解析 run,脚本失败并提示用户用 `gh run list --repo "$REPO"` 人工核对。
|
||||
|
||||
### 残余边界
|
||||
|
||||
同一秒内对**同一仓库**多次 `workflow_dispatch`,仍可能歧义;若未来需要,可在 workflow `inputs` 中增加显式关联 ID。
|
||||
|
||||
### 相关文档
|
||||
|
||||
- `docs/QIMINGCLAW-WORKFLOW-DISPATCH.md`:在 qimingclaw 侧启用 workflow、Secrets、一次性复制路径说明。
|
||||
|
||||
---
|
||||
|
||||
## 3. CI 中版本号校验:POSIX `grep` 与 `ripgrep`
|
||||
|
||||
### 问题
|
||||
|
||||
在 `ubuntu-latest` 上若使用 `rg -q` 做 `x.y.z` 校验,**默认镜像未必安装 ripgrep**,校验步骤可能静默失败或整条 step 失败,与「MVP 仅允许纯数字三段版本」的意图不符。
|
||||
|
||||
### 方案
|
||||
|
||||
在 shell 中使用 **`grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'`**,依赖 POSIX 环境即可,与 `sync-electron-to-oss.yml` / `release-electron.yml` 中生成 `latest.json` 的版本字段策略一致。
|
||||
|
||||
### 落地说明
|
||||
|
||||
带 `channel`(stable/beta)与上述校验的 workflow 变更可能位于独立分支或 worktree;合并到主分支后,**qiming-agent 与 qimingclaw 两侧 workflow 文件应保持同步**(以实际执行 release/OSS 的仓库为准)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 附录:stable / beta 更新通道(规划/分支)
|
||||
|
||||
目标与 OSS 路径约定(先 beta 再 stable、beta 不写 `latest/latest.json` 等)见独立说明文档草案;若已合入主仓库,路径为:
|
||||
|
||||
- `docs/RELEASE-CHANNELS.md`(若不存在,可参考 worktree `feat/stable-beta-update-channel` 内同名文件)
|
||||
|
||||
客户端侧要点(实现以合入后的代码为准):
|
||||
|
||||
- SQLite `update_channel` 决定拉取的 `latest.json` URL(stable 与 beta 不同路径)。
|
||||
- 旧版客户端仅读 `latest/`,行为保持不变。
|
||||
|
||||
---
|
||||
|
||||
## 5. 文档索引
|
||||
|
||||
| 主题 | 路径 |
|
||||
|------|------|
|
||||
| OSS 仅同步 workflow 启用说明 | [../QIMINGCLAW-WORKFLOW-DISPATCH.md](../QIMINGCLAW-WORKFLOW-DISPATCH.md) |
|
||||
| 自动更新架构 | [../architecture/auto-update.md](../architecture/auto-update.md) |
|
||||
| qimingcode 二进制签名说明 | [../QIMINGCODE-BINARY-SIGNING-2026-03-28.md](../QIMINGCODE-BINARY-SIGNING-2026-03-28.md) |
|
||||
| macOS 首条消息性能报告 | [../optimization/MACOS-FIRST-MESSAGE-PERF-REPORT-2026-03-31.md](../optimization/MACOS-FIRST-MESSAGE-PERF-REPORT-2026-03-31.md) |
|
||||
|
||||
---
|
||||
|
||||
*文档状态:留存归档;若实现迁移或策略变更,请在本文件顶部更新「最后修订」并保留变更摘要。*
|
||||
|
||||
**最后修订:2026-03-31**
|
||||
@@ -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 PATH(macOS/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 开发团队*
|
||||
Reference in New Issue
Block a user