# macOS Apple Silicon DMG 改造详细计划

## 1. 背景与目标

本计划基于当前 `dev` 分支代码事实制定，目标是把现有 Windows OpenClaw 客户端安装包链路改造成 **只面向 macOS Apple Silicon 的内部测试 DMG 包**。

本次改造不做双平台共存，不保留 Windows NSIS 安装器作为目标链路，也不纳入 Apple Developer ID 签名、公证、staple 等正式外部分发能力。阶段性目标是先让 M 芯片 Mac 上可以构建、安装、启动并跑通 bundled runtime 与 workspace execution。

### 当前代码事实

> **注意**：以下为改造前的初始状态，用于记录基线。实际改造进度见第 9 节。

- `apps/desktop/electron-builder.yml` 目前只配置了 `win` 和 `nsis`，产物命名为 `${productName}-Setup-${version}.${ext}`。
- `apps/desktop/electron-builder.yml` 引用了 `build/icons/brand-icon.ico`，但当前仓库未发现实际图标文件。
- 根 `package.json` 的 `materialize:runtime` 指向 `build/scripts/materialize-runtime-payload.ps1`，是 PowerShell/Windows-only 链路。
- 根 `package.json` 的大部分 smoke 命令都依赖 `.ps1`、`.exe`、NSIS installer 或 Windows Electron 路径。
- `packages/runtime-manager/src/index.ts` 写死了 `node/node.exe`、`python/python.exe`、`ffmpeg.exe`、`ffprobe.exe`、`python/Scripts`、PowerShell EPERM fallback 和 `taskkill`。
- `apps/desktop/src/main/services/project-workspace-executor.ts` 同样包含 PowerShell wrapper fallback、Windows PATH 扩展名逻辑和 `python/Scripts` 注入。
- `vendor/openclaw-runtime` 目前只有 README 占位，README 描述的是 Windows runtime 布局。
- packaged runtime 路径解析已经适合 macOS 包内资源：`apps/desktop/src/main/index.ts` 会在 packaged 模式使用 `process.resourcesPath/vendor/openclaw-runtime`。

### 总体成功标准

- 在 Apple Silicon macOS 上运行 `corepack pnpm run package:mac` 能产出 `.dmg`。
- DMG 内 `.app` 包含 `vendor/openclaw-runtime` mac arm64 runtime。
- `.app` 可启动并进入主界面。
- RuntimeManager 能把 bundled runtime 判定为 `ready`。
- managed OpenClaw Gateway 能启动并通过 readiness probe。
- workspace execution 能用 bundled Node/Python 跑通主路径。
- Playwright Chromium 能从包内 `playwright-browsers` 被 Python runtime 使用。
- 关闭应用后 managed runtime 能退出，不遗留 Gateway 占用端口。

## 2. 改造边界与默认决策

### 平台边界

- 唯一目标平台：`darwin-arm64`。
- 不支持 Windows。
- 不支持 Intel Mac。
- 不做 universal app。
- 不做 macOS x64 fallback。

### 发布边界

- 目标产物：内部测试 `.dmg`。
- 不配置正式签名。
- 不配置 notarization。
- 不配置 auto update。
- 允许首次启动时由测试人员手动绕过 Gatekeeper。

### 代码边界

- 保留 Electron 主进程、preload、UI、runtime-manager、workspace execution、OpenClaw package、Python skills。
- 废弃 Windows 打包目标、NSIS、installer.nsh、Windows installer smoke、PowerShell runtime materialize 主入口。
- Windows 相关历史脚本可暂时保留在仓库中，但 mac 分支的正式 scripts 不再引用它们。

### Runtime 目录标准

mac-only 分支统一采用以下布局：

```text
vendor/openclaw-runtime/
  node/bin/node
  openclaw/index.js
  openclaw/package/openclaw.mjs
  openclaw/package/package.json
  config/openclaw.json
  python/bin/python3
  python/python-manifest.json
  python/runtime-requirements.lock.txt
  ffmpeg/bin/ffmpeg
  ffmpeg/bin/ffprobe
  playwright-browsers/
  runtime-manifest.json
  README.md
```

## 3. 分阶段实施计划

### Phase 0: 建立实施基线

目标：在开始改代码前固定当前事实和验收边界，避免后续实施时重新决策。

实施内容：

- 确认当前分支为 `dev`，并从这里切出 mac-only 工作分支。
- 记录本计划中的平台边界：只做 `darwin-arm64`。
- 确认本次不处理正式签名、公证、自动更新。
- 确认 runtime 物料来源要求：
  - Node 使用本机 mac arm64 Node。
  - Python 使用可复制的 mac arm64 Python 根目录。
  - ffprobe 必须来自 PATH 或显式参数。
  - OpenClaw package 必须能从本机安装位置解析。

阶段性目标：

- 有一个明确的 mac-only 工作分支。
- 所有人对“不会保留 Windows 打包兼容”有一致预期。
- 后续实现不再为 Windows/NSIS/PowerShell 设计抽象层。

验收标准：

- `git status --short --branch` 显示工作分支清晰。
- 文档中已记录 mac-only、内部测试 DMG 两个关键决策。

### Phase 1: 改造 Electron Builder 打包配置

目标：把当前 Windows installer 配置改成只生成 macOS arm64 DMG。

涉及文件：

- `apps/desktop/electron-builder.yml`
- `apps/desktop/package.json`
- 根 `package.json`

实施内容：

- 重写 `apps/desktop/electron-builder.yml`：
  - 保留 `appId`、`productName`、`compression`、`asar`、`asarUnpack`、`directories.output`、`files`、`extraResources`。
  - 删除 `win` 配置。
  - 删除 `nsis` 配置。
  - 删除 `afterPack: build/hooks/after-pack-branding.cjs`。
  - 删除 `build/icons/brand-icon.ico` extraResource。
  - 将 `artifactName` 改成 `${productName}-${version}-mac-arm64.${ext}`。
  - 新增 `mac.target: dmg`。
  - 新增 `mac.arch: arm64`。
  - 新增 `mac.category: public.app-category.productivity`。
  - 内部测试阶段设置 `mac.identity: null` 或通过 electron-builder CLI 禁用 signing。
  - 暂不强制配置 `mac.icon`，避免当前缺少 `.icns` 阻塞。
- DMG 配置采用最小可用布局：
  - `dmg.title: ${productName} ${version}`
  - `dmg.contents` 包含 app 和 Applications link。
- 根 `package.json` 新增：
  - `materialize:runtime:mac`
  - `package:mac`
- `apps/desktop/package.json` 新增或调整：
  - `package:mac`

推荐命令语义：

```json
{
  "materialize:runtime:mac": "node build/scripts/materialize-runtime-payload.mjs",
  "package:mac": "corepack pnpm run materialize:runtime:mac && corepack pnpm build && corepack pnpm --filter @qjclaw/desktop exec electron-builder --config electron-builder.yml --mac dmg --arm64"
}
```

阶段性目标：

- electron-builder 不再尝试生成 `.exe` 或 NSIS installer。
- macOS 上可以进入 electron-builder 的 mac target 构建流程。
- 打包命令名称与产物目标清晰对应。

验收标准：

- `rg -n "nsis|win:|Setup|brand-icon.ico|after-pack-branding" apps/desktop/electron-builder.yml` 没有命中。
- `corepack pnpm --filter @qjclaw/desktop exec electron-builder --config electron-builder.yml --mac dmg --arm64 --dir` 至少能进入 mac packaging 阶段。

### Phase 2: 新建 mac runtime materialize 脚本

目标：替代 PowerShell runtime 物料生成链路，生成 mac arm64 可运行的 `vendor/openclaw-runtime`。

涉及文件：

- `build/scripts/materialize-runtime-payload.mjs`
- `build/runtime/python/runtime-requirements.lock.txt`
- `vendor/openclaw-runtime/README.md`
- 根 `package.json`

实施内容：

- 新建 Node 脚本 `build/scripts/materialize-runtime-payload.mjs`。
- 脚本启动时校验：
  - `process.platform === "darwin"`
  - `process.arch === "arm64"`
- 支持参数和环境变量：
  - `--runtime-dir`
  - `--source-config-path`
  - `--source-node`
  - `--source-python-root`
  - `--source-openclaw-entry`
  - `--source-ffprobe`
  - `--gateway-port`
  - `--gateway-token`
  - `QJCLAW_MAC_PYTHON_ROOT`
- 默认值：
  - runtime dir: `vendor/openclaw-runtime`
  - config: `~/.openclaw/openclaw.json`
  - requirements: `build/runtime/python/runtime-requirements.lock.txt`
  - gateway port: `18889`
  - gateway token: `qjc-bundled-runtime-token`
- 生成 staging 目录，全部成功后再原子替换 `vendor/openclaw-runtime`。
- 复制 Node：
  - 解析 `node` 命令。
  - 放入 `node/bin/node`。
  - `chmod 755`。
- 复制 OpenClaw package：
  - 优先从 `--source-openclaw-entry` 解析。
  - 否则从 PATH 中 `openclaw` 解析。
  - 拷贝 package 到 `openclaw/package`。
  - 生成 `openclaw/index.js` wrapper，按 manifest 载入 `openclaw/package/openclaw.mjs`。
- 复制并改写 config：
  - 读取源 `openclaw.json`。
  - 强制设置 `gateway.mode = "local"`。
  - 强制设置 `gateway.bind = "loopback"`。
  - 设置 `gateway.port` 和 token。
  - 写入 `config/openclaw.json`。
- 复制 Python：
  - 从 `--source-python-root` 或 `QJCLAW_MAC_PYTHON_ROOT` 获取完整 Python 根目录。
  - 要求存在 `bin/python3`。
  - 拷贝到 `python/`。
  - `chmod 755 python/bin/python3`。
  - 执行 `python/bin/python3 -m pip install --disable-pip-version-check --upgrade pip`。
  - 执行 `python/bin/python3 -m pip install --disable-pip-version-check -r build/runtime/python/runtime-requirements.lock.txt`。
  - 设置 `PLAYWRIGHT_BROWSERS_PATH` 后执行 `python/bin/python3 -m playwright install chromium`。
- 复制 ffmpeg/ffprobe：
  - `ffmpeg` 优先从 `imageio_ffmpeg` Python package 的 binaries 目录解析。
  - `ffprobe` 从 `--source-ffprobe` 或 PATH 解析。
  - 放入 `ffmpeg/bin/ffmpeg` 和 `ffmpeg/bin/ffprobe`。
  - 两者都 `chmod 755`。
- 生成 `python/python-manifest.json`：
  - Python 版本。
  - executable: `bin/python3`。
  - requirementsPath: `runtime-requirements.lock.txt`。
  - requestedPackages。
  - resolvedPackages。
- 生成 `runtime-manifest.json`：
  - platform: `darwin`
  - arch: `arm64`
  - bundledNodeExecutable: `node/bin/node`
  - packagedOpenClawEntry: `openclaw/package/openclaw.mjs`
  - defaultConfigPath: `config/openclaw.json`
  - pythonExecutable: `python/bin/python3`
  - pythonManifestPath: `python/python-manifest.json`
  - requirementsPath: `python/runtime-requirements.lock.txt`
  - ffmpegExecutable: `ffmpeg/bin/ffmpeg`
  - ffprobeExecutable: `ffmpeg/bin/ffprobe`
  - playwrightBrowsersPath: `playwright-browsers`
  - gatewayPort
  - gatewayToken
  - materializedAt
  - materializationKey
  - sizeBytes
  - fileCount
- 清理低风险冗余文件：
  - `__pycache__`
  - `*.pyc`
  - `*.pyo`
  - OpenClaw sourcemap
  - 非模板 docs，保留 workspace bootstrap 需要的 templates。

阶段性目标：

- macOS 上可以通过一个 Node 脚本生成完整 runtime payload。
- 生成的 runtime 布局与后续 runtime-manager 路径完全一致。
- PowerShell 不再是 mac runtime 物料生成入口。

验收标准：

- `corepack pnpm run materialize:runtime:mac` 成功退出。
- `vendor/openclaw-runtime/node/bin/node --version` 成功。
- `vendor/openclaw-runtime/python/bin/python3 --version` 成功。
- `vendor/openclaw-runtime/python/bin/python3 -m pip --version` 成功。
- `vendor/openclaw-runtime/runtime-manifest.json` 中 `platform` 是 `darwin`，`arch` 是 `arm64`。
- `vendor/openclaw-runtime/playwright-browsers` 存在且包含 Chromium。

### Phase 3: 改造 RuntimeManager 为 mac-only

目标：让 Electron 主进程能识别、启动、停止 mac bundled runtime。

涉及文件：

- `packages/runtime-manager/src/index.ts`

实施内容：

- `resolveBundledPaths()` 改为 mac 路径：
  - `node/bin/node`
  - `python/bin/python3`
  - `ffmpeg/bin/ffmpeg`
  - `ffmpeg/bin/ffprobe`
- 删除 Windows 专用工具：
  - `escapePowerShellSingleQuoted`
  - `resolveWindowsSystemExecutable`
  - `WINDOWS_POWERSHELL_PATH`
  - `WINDOWS_TASKKILL_PATH`
  - `buildWindowsChildWrapperScript`
- 删除 `execPythonInlineScript()` 中的 Windows EPERM PowerShell fallback。
- 删除 `detectRuntime()` 中 Python manifest EPERM fallback。
- `resolveExecutableOnPath()` 不再处理 PATHEXT，直接按 POSIX PATH 查找。
- `resolveBundledFfmpegExecutable()` 支持：
  - `ffmpeg/bin/ffmpeg`
  - `python/lib/python*/site-packages/imageio_ffmpeg/binaries/ffmpeg-*`
  - `playwright-browsers/ffmpeg-*` 下 mac ffmpeg 文件。
- `resolveBundledFfprobeExecutable()` 支持：
  - `ffmpeg/bin/ffprobe`
  - 与 ffmpeg 同目录的 `ffprobe`。
- `buildManagedChildEnv()` 改为 mac 环境：
  - 继承 `HOME`、`USER`、`TMPDIR`、`SHELL`、`PATH`、`LANG`、`LC_ALL` 等必要变量。
  - 设置 `HOME = runtimeDataDir`。
  - 设置 `OPENCLAW_HOME`、`OPENCLAW_STATE_DIR`、`OPENCLAW_CONFIG_PATH`。
  - 设置 `PLAYWRIGHT_BROWSERS_PATH`。
  - PATH 顺序为 `node/bin`、`python/bin`、`ffmpeg/bin`、系统 PATH。
- `performStart()` 直接 `spawn(paths.nodeExecutable, childArgs, spawnOptions)`。
- `stop()` 改为：
  - 优先对 tracked child 发 `SIGTERM`。
  - 等待短延迟。
  - 如仍未退出，发 `SIGKILL`。
  - 不调用 `taskkill`。
- Python probe 保持现有核心导入检查，并补充当前 lock 文件中已有但 probe 未覆盖的关键包：
  - `imageio-ffmpeg` 对应 `imageio_ffmpeg`
  - `qiniu`

阶段性目标：

- RuntimeManager 不再有 Windows runtime 路径假设。
- RuntimeManager 能读取 mac runtime manifest 并判定 payload ready。
- RuntimeManager 能启动 Gateway 并在停止时清理进程。

验收标准：

- `rg -n "powershell|taskkill|node.exe|python.exe|ffmpeg.exe|ffprobe.exe|python.*Scripts|win32" packages/runtime-manager/src/index.ts` 不再命中有效运行逻辑。
- `corepack pnpm --filter @qjclaw/runtime-manager typecheck` 通过。
- 在 dev 环境指定 `QJCLAW_SMOKE_RUNTIME_DIR=vendor/openclaw-runtime` 后，RuntimeManager status 中：
  - `payloadState` 为 `ready`。
  - `nodeExecutable` 指向 `node/bin/node`。
  - `pythonExecutable` 指向 `python/bin/python3`。

### Phase 4: 改造 workspace execution 为 mac-only

目标：保证项目自动化和 workspace agent runner 都能通过 mac bundled runtime 执行。

涉及文件：

- `apps/desktop/src/main/services/project-workspace-executor.ts`

实施内容：

- 删除 PowerShell helper：
  - `escapePowerShellSingleQuoted`
  - `getWindowsPowerShellPath`
- 删除 spawn EPERM 后 PowerShell wrapper retry。
- `resolveExecutableOnPath()` 简化为 POSIX PATH 查询。
- `resolveInjectedBinaryPath()` command candidates 改为：
  - ffmpeg: `["ffmpeg"]`
  - ffprobe: `["ffprobe"]`
- child env PATH 改为：
  - `path.dirname(paths.nodeExecutable)`
  - `path.dirname(paths.pythonExecutable)`
  - `path.dirname(paths.ffmpegExecutable)`
  - `path.dirname(paths.ffprobeExecutable)`
  - `process.env.PATH`
- 移除 `path.join(paths.runtimeDir, "python", "Scripts")`。
- 保留现有协议不变：
  - `workspaceAutomation.runtime = "python"`。
  - `workspaceAutomation.script` 模板解析。
  - `project-workspace-agent-runner.js` stdin payload。
  - stdout `QJC_WORKSPACE_EVENT` event stream。

阶段性目标：

- workspace automation 使用 `python/bin/python3`。
- Node runner 使用 `node/bin/node`。
- 所有执行路径都不依赖 shell wrapper。

验收标准：

- `rg -n "powershell|python.*Scripts|ffmpeg.exe|ffprobe.exe|win32" apps/desktop/src/main/services/project-workspace-executor.ts` 不再命中有效运行逻辑。
- `corepack pnpm --filter @qjclaw/desktop typecheck` 通过。
- 用一个最小 `project.json` 的 `workspaceAutomation.runtime = "python"` 项目验证 Python 脚本可启动。

### Phase 5: 调整 Electron mac 桌面行为

目标：让 `.app` 在 macOS 上以稳定、符合平台预期的方式启动和关闭。

涉及文件：

- `apps/desktop/src/main/create-window.ts`
- `apps/desktop/src/main/index.ts`

实施内容：

- `create-window.ts`：
  - 删除 Windows `.ico` 路径解析。
  - mac 内部测试阶段可让 Electron 使用默认 app icon。
  - 如果后续补充 `.icns`，只在 builder 配置中接入，不在 BrowserWindow 强行设置 `.ico`。
- 菜单：
  - mac 上不要完全 `Menu.setApplicationMenu(null)`。
  - 提供最小应用菜单：
    - app name
    - Quit
    - Edit: Copy/Paste/Select All
    - Window: Minimize/Close
  - Windows 菜单隐藏策略不再作为目标。
- 生命周期：
  - 保留 `window-all-closed` 中 mac 不直接 quit 的逻辑。
  - 保留 `activate` 恢复或创建主窗口。
  - 验证 Dock 点击能恢复窗口。

阶段性目标：

- mac `.app` 首次启动不会因窗口/菜单策略异常退出。
- 关闭窗口后 Dock 生命周期符合 mac 习惯。

验收标准：

- 启动应用出现主窗口。
- `Cmd+Q` 可退出。
- 关闭最后一个窗口后应用仍可通过 Dock 重新打开窗口。

### Phase 6: 清理 package scripts 和 Windows installer 入口

目标：让 mac-only 分支的正式命令不再引用 Windows installer 链路。

涉及文件：

- 根 `package.json`
- `apps/desktop/package.json`
- `build/scripts/README.md`

实施内容：

- 根 `package.json`：
  - 将 `package` 指向 mac 打包，或保留 `package` 但新增明确的 `package:mac`。
  - 新增 `materialize:runtime:mac`。
  - 新增 `smoke:mac:runtime`、`smoke:mac:package`、`smoke:mac:workspace-entry`。
  - 将 Windows installer smoke 从正式验收命令中移除。
- `apps/desktop/package.json`：
  - 新增 `package:mac`。
  - `package` 若保留，应避免调用 PowerShell。
- `build/scripts/README.md`：
  - 更新 runtime materialize 说明为 mac 脚本。
  - 标注旧 `.ps1` installer smoke 不属于 mac 分支验收入口。

阶段性目标：

- 新开发者只看 package scripts 就能知道 mac 打包入口。
- `corepack pnpm run package:mac` 是唯一正式 DMG 构建命令。

验收标准：

- `corepack pnpm run package:mac` 不调用 `powershell`。
- `rg -n "materialize-runtime-payload.ps1|installer-smoke.ps1|Setup-.*exe" package.json apps/desktop/package.json` 不命中正式 mac scripts。

### Phase 7: 建立 mac smoke 验证脚本

目标：替代 Windows installer smoke，提供发布前最小可信回归。

涉及文件：

- `build/scripts/mac-runtime-smoke.mjs`
- `build/scripts/mac-package-smoke.mjs`
- `build/scripts/mac-workspace-entry-smoke.mjs`
- 根 `package.json`

实施内容：

- `mac-runtime-smoke.mjs`：
  - 读取 `vendor/openclaw-runtime/runtime-manifest.json`。
  - 校验 manifest 中 platform/arch。
  - 校验 Node、Python、OpenClaw wrapper、config、ffmpeg、ffprobe、playwright-browsers 存在。
  - 执行 Node/Python version probe。
  - 执行 Python imports probe。
- `mac-package-smoke.mjs`：
  - 查找 `dist/installer/*.dmg`。
  - 查找 `dist/installer/mac-arm64/*.app` 或 electron-builder 实际输出目录。
  - 校验 `.app/Contents/Resources/vendor/openclaw-runtime` 存在。
  - 校验 `.app/Contents/Resources/app.asar` 存在。
- `mac-workspace-entry-smoke.mjs`：
  - 准备临时 project root。
  - 写入最小 `project.json`，包含 Python automation。
  - 通过 Electron smoke 或直接调用 executor 需要的主链路进行验证。
  - 第一版可先做 runtime Python automation 级别验证，第二版再补 Electron UI 驱动。
- package scripts：
  - `smoke:mac:runtime`
  - `smoke:mac:package`
  - `smoke:mac:workspace-entry`

阶段性目标：

- 能在不安装 DMG 的情况下验证 runtime payload。
- 能验证 electron-builder 产物资源布局。
- 能验证最小 workspace automation。

验收标准：

- `corepack pnpm run smoke:mac:runtime` 通过。
- `corepack pnpm run smoke:mac:package` 通过。
- `corepack pnpm run smoke:mac:workspace-entry` 通过。

### Phase 8: 端到端 DMG 验证

目标：验证最终测试包真实可用。

实施内容：

- 执行完整构建：

```bash
corepack pnpm run package:mac
```

- 打开 DMG。
- 将 `.app` 拖入 `/Applications` 或测试目录。
- 启动 `.app`。
- 验证：
  - 主窗口出现。
  - 设置页/聊天页不报主进程错误。
  - Runtime status 显示 bundled runtime ready/running。
  - managed Gateway readiness 成功。
  - 默认聊天可以发起。
  - 有 workspace automation 的项目可以调用 bundled Python。
  - Playwright Chromium 能启动。
  - 退出应用后 runtime 进程停止。

阶段性目标：

- 内部测试人员拿到 DMG 后可以安装并启动。
- 主要功能链路能完成一轮冒烟验证。

验收标准：

- `dist/installer/千匠问天-0.1.0-mac-arm64.dmg` 或对应版本文件存在。
- DMG 可挂载。
- `.app` 可启动。
- app 内 bundled runtime 可运行。
- 退出后无残留 managed Gateway 进程。

## 4. 推荐实施顺序

1. 在 `dev` 基础上创建 mac-only 工作分支。
2. 改 `electron-builder.yml`，先让 mac DMG target 配置成立。
3. 新建 `materialize-runtime-payload.mjs`，生成 mac runtime payload。
4. 更新 `vendor/openclaw-runtime/README.md` 为 mac 布局。
5. 改 `runtime-manager` 的路径、PATH、ffmpeg/ffprobe、stop/start 逻辑。
6. 改 `project-workspace-executor`，移除 PowerShell fallback 和 Windows PATH 假设。
7. 改 Electron 主窗口图标、菜单和 mac 生命周期细节。
8. 调整根 `package.json` 与 `apps/desktop/package.json` 的 mac 打包命令。
9. 增加 mac runtime/package/workspace smoke。
10. 跑 typecheck/build。
11. 跑 materialize runtime。
12. 跑 package:mac。
13. 挂载 DMG 做端到端人工验收。

## 5. 风险与处理策略

### Python runtime 可分发性

风险：直接复制系统 Python 或 Homebrew Python 可能存在动态库路径、venv、site-packages 可移植性问题。

处理策略：

- 第一阶段使用显式 `QJCLAW_MAC_PYTHON_ROOT`，要求输入是已验证可复制的 Python 根目录。
- materialize 后立即在 `vendor/openclaw-runtime/python/bin/python3` 上执行 import probe。
- 若复制式 Python 不稳定，下一阶段改为固定使用 standalone Python distribution。

### ffprobe 来源

风险：`imageio-ffmpeg` 通常提供 ffmpeg，不保证提供 ffprobe。

处理策略：

- materialize 阶段强制要求 `ffprobe` 可解析。
- 支持 `--source-ffprobe`。
- 缺失时直接失败，不生成半可用 runtime。

### Playwright 浏览器体积

风险：`playwright-browsers` 会显著增大 DMG。

处理策略：

- v1 保留 Chromium，优先保证功能完整。
- 后续再做瘦身，不在首版 DMG 改造里压缩能力。

### Gatekeeper

风险：内部测试未签名 app 首次打开可能被 macOS 阻止。

处理策略：

- 文档明确当前包是内部测试包。
- 验收时允许通过系统设置或 `xattr -dr com.apple.quarantine` 处理。
- 正式分发另开签名/公证阶段。

### 图标资源缺失

风险：当前仓库未发现 `brand-icon.ico/.icns/.png`，强行接入图标会阻塞。

处理策略：

- v1 不强制 `.icns`。
- electron-builder 使用默认 icon。
- 拿到品牌图后再补 `build/icons/brand-icon.icns` 和 DMG background。

## 6. 阶段性交付清单

### Milestone A: DMG 配置可构建

交付物：

- mac-only `apps/desktop/electron-builder.yml`
- `package:mac` 命令
- Windows NSIS 配置从正式打包链路移除

验收：

- electron-builder 能进入 mac arm64 packaging。

### Milestone B: mac runtime 可生成

交付物：

- `build/scripts/materialize-runtime-payload.mjs`
- mac 版 `vendor/openclaw-runtime`
- mac 版 runtime README

验收：

- `materialize:runtime:mac` 成功。
- Node/Python/Playwright/ffmpeg/ffprobe probe 成功。

### Milestone C: app 可启动 bundled runtime

交付物：

- mac-only RuntimeManager
- mac-only workspace executor
- mac child env 和 PATH 注入

验收：

- RuntimeManager `payloadState = ready`。
- Gateway readiness 成功。
- runtime stop 能清理进程。

### Milestone D: DMG 可安装可冒烟

交付物：

- `.dmg`
- mac smoke scripts
- 更新后的打包验证文档

验收：

- DMG 可挂载。
- `.app` 可启动。
- 主界面、runtime、workspace automation、Playwright 均通过冒烟。

## 7. 最终验收命令

```bash
corepack pnpm --filter @qjclaw/runtime-manager typecheck
corepack pnpm --filter @qjclaw/desktop typecheck
corepack pnpm build
corepack pnpm run materialize:runtime:mac
corepack pnpm run smoke:mac:runtime
corepack pnpm run package:mac
corepack pnpm run smoke:mac:package
corepack pnpm run smoke:mac:workspace-entry
```

## 8. 后续增强项

这些不进入首版 mac-only 内部测试 DMG 范围：

- Developer ID 签名。
- Notarization。
- Staple。
- Auto update feed。
- Universal app。
- Intel Mac 支持。
- 品牌 `.icns`、DMG background、Applications 拖拽窗口美化。
- runtime payload 瘦身。
- 双平台共存抽象。

## 9. 实施进度跟踪

工作分支：`feature/mac-arm64-dmg`

### Phase 0: 建立实施基线 — ✅ 完成

- 从 `dev` 切出 `feature/mac-arm64-dmg` 分支
- 平台边界、发布边界、代码边界已记录在本计划中

### Phase 1: 改造 Electron Builder 打包配置 — ✅ 代码完成，真实产物待验收

已修改文件：

- `apps/desktop/electron-builder.yml` — 删除 `win`/`nsis`/`ico`/`afterPack`，新增 `mac.target: dmg`/`mac.arch: arm64`/`mac.category`/`mac.identity: null`/`dmg.title`/`dmg.contents`，`artifactName` 改为 `${productName}-${version}-mac-arm64.${ext}`
- `apps/desktop/package.json` — 新增 `package:mac` 脚本
- `package.json` — 新增 `package:mac` 和 `materialize:runtime:mac` 脚本
- `apps/ui/src/assets/brand-icon.svg` — 已接入品牌图资源，当前不再依赖缺失的 Windows `.ico`

验收状态：

- `rg -n "nsis|win:|Setup|brand-icon.ico|after-pack-branding" apps/desktop/electron-builder.yml` 无命中 ✅
- `corepack pnpm build` 已通过 ✅
- `electron-builder --mac dmg --arm64 --dir` 已通过 ✅
  - 直连 GitHub 下载 Electron darwin arm64 zip 会长时间卡住；通过本地代理 `127.0.0.1:7897` 并提升权限后下载成功
  - 已产出 `dist/installer/mac-arm64/千匠问天.app`
  - 已产出 `dist/installer/千匠问天-0.1.0-mac-arm64.dmg`
  - `hdiutil verify dist/installer/千匠问天-0.1.0-mac-arm64.dmg` 通过 ✅
  - 当前产物内 `Resources/vendor/openclaw-runtime` 仅包含占位 README，尚不是最终可验收 runtime 包

### Phase 2: 新建 mac runtime materialize 脚本 — ✅ 代码完成，真实物料化待验收

新建文件：

- `build/scripts/materialize-runtime-payload.mjs` — 完整的 mac arm64 runtime 物料生成脚本
  - 平台校验 `darwin` + `arm64`
  - 参数解析：`--runtime-dir`、`--source-config-path`、`--source-node`、`--source-python-root`、`--source-openclaw-entry`、`--source-ffprobe`、`--requirements-path`、`--gateway-port`、`--gateway-token`
  - 环境变量 `QJCLAW_MAC_PYTHON_ROOT`
  - Node / OpenClaw / Python / ffmpeg / ffprobe 复制与 chmod
  - config 重写（gateway.mode=local, bind=loopback, port, token）
  - pip upgrade + requirements install + Playwright install
  - python-manifest.json 生成
  - runtime-manifest.json 生成（`platform: "darwin"`、`arch: "arm64"`；mac 路径：`node/bin/node`、`python/bin/python3`、`ffmpeg/bin/ffmpeg`、`ffmpeg/bin/ffprobe`）
  - openclaw/index.js wrapper 生成
  - README.md 生成（mac 布局）
  - cleanup（`__pycache__`、`.pyc`、`.pyo`、`.map`、Python Doc/Tools/test/idlelib/tkinter、OpenClaw docs 保留 templates）
  - 原子替换 vendor/openclaw-runtime

- `package.json` 中 `materialize:runtime:mac` 已从 placeholder 改为 `node build/scripts/materialize-runtime-payload.mjs`

验收状态：

- `node --check build/scripts/materialize-runtime-payload.mjs` 通过 ✅
- `corepack pnpm run materialize:runtime:mac` 已执行，当前失败在第一处硬前置：`ffprobe is required...` ❌
- 本机前置探测结果：
  - Node 是 `darwin arm64`，路径为 `/Users/edy/.nvm/versions/node/v24.15.0/bin/node` ✅
  - `QJCLAW_MAC_PYTHON_ROOT` 未设置 ❌
  - `command -v openclaw` 无结果 ❌
  - `command -v ffprobe` 无结果 ❌
  - `~/.openclaw/openclaw.json` 不存在 ❌
  - 仓库和 `node_modules` 中未找到可直接复用的 `openclaw.mjs`、`openclaw.json`、`ffprobe` ❌
- 后续仍需要：`QJCLAW_MAC_PYTHON_ROOT` 指向包含 `bin/python3` 的可复制 Python 根目录；用 `--source-openclaw-entry` 显式指定 OpenClaw entry；安装 ffmpeg 或用 `--source-ffprobe` 显式指定；准备 `~/.openclaw/openclaw.json` 或用 `--source-config-path` 指定

### Phase 3: 改造 RuntimeManager 为 mac-only — ✅ 代码完成，真实启动待验收

已修改文件：`packages/runtime-manager/src/index.ts`

具体改动：

- 路径改为 mac：`node/bin/node`、`python/bin/python3`、`ffmpeg/bin/ffmpeg`、`ffmpeg/bin/ffprobe`
- Runtime payload readiness probe 已补 `imageio_ffmpeg`、`qiniu`
- Playwright ffmpeg fallback 已补充到 `python/lib/*/site-packages/imageio_ffmpeg/binaries/ffmpeg-*`
- 删除 `escapePowerShellSingleQuoted`、`resolveWindowsSystemExecutable`、`WINDOWS_POWERSHELL_PATH`、`WINDOWS_TASKKILL_PATH`
- `execPythonInlineScript` 简化为直接执行（删除 EPERM PowerShell fallback）
- `detectRuntime` 中 Python probe fallback 改为通用 manifest fallback（不再限于 win32 EPERM）
- `resolveExecutableOnPath` 简化为 POSIX PATH 查询（删除 PATHEXT 逻辑）
- `resolveBundledFfmpegExecutable` 改为 mac 路径：`ffmpeg/bin/ffmpeg`、`python/lib/*/site-packages/imageio_ffmpeg/binaries/ffmpeg-*`
- `resolveBundledFfprobeExecutable` 改为 mac 路径：`ffmpeg/bin/ffprobe`
- `stop()` 改为 SIGTERM → 500ms → SIGKILL（删除 `taskkill`/`process.platform === "win32"` 分支）
- `performStart` 删除 EPERM PowerShell wrapper fallback（直接 spawn，失败即报错）
- `buildManagedChildEnv` 环境变量改为 macOS：HOME/USER/TMPDIR/SHELL/LANG/LC_ALL/PATH/XDG_* 等，删除 Windows 变量（ALLUSERSPROFILE/APPDATA/COMSPEC/HOMEDRIVE/HOMEPATH/PATHEXT/WINDIR 等）；PATH 删除 `python/Scripts`，加入 `node/bin`；ffmpeg/ffprobe 命令名改为 `["ffmpeg"]`/`["ffprobe"]`；删除 `USERPROFILE`
- 删除 `buildWindowsChildWrapperScript` 方法
- 删除 `windowsHide: false` spawn 选项

验收状态：

- `rg -n "powershell|taskkill|node.exe|python.exe|ffmpeg.exe|ffprobe.exe|python.*Scripts|win32" packages/runtime-manager/src/index.ts` 无命中 ✅
- `corepack pnpm --filter @qjclaw/runtime-manager typecheck` 通过 ✅
- bundled runtime 启动和 Gateway readiness 仍需在真实 runtime payload 生成后验收
- `corepack pnpm run smoke:mac:runtime` 已执行，当前因 `vendor/openclaw-runtime` 只有 README、缺 `runtime-manifest.json`/Node/Python/ffmpeg/ffprobe/Playwright 而失败 ❌

### Phase 4: 改造 workspace execution 为 mac-only — ✅ 代码完成，真实执行待验收

已修改文件：`apps/desktop/src/main/services/project-workspace-executor.ts`

具体改动：

- 删除 `escapePowerShellSingleQuoted`、`getWindowsPowerShellPath`
- `resolveExecutableOnPath` 简化为 POSIX PATH 查询（删除 PATHEXT / windowsExtensions）
- `resolveInjectedBinaryPath` 中 ffmpeg/ffprobe 命令名改为 `["ffmpeg"]`/`["ffprobe"]`（删除 `.exe`）
- child env PATH 删除 `path.join(paths.runtimeDir, "python", "Scripts")`，保留 `path.dirname(paths.pythonExecutable)`
- spawn 删除 EPERM PowerShell wrapper fallback（删除 `let child` + try/catch PowerShell retry，改为 `const child = spawn(...)`)
- 删除 `windowsHide: true` spawn 选项

验收状态：

- `rg -n "powershell|python.*Scripts|ffmpeg.exe|ffprobe.exe|win32" apps/desktop/src/main/services/project-workspace-executor.ts` 无命中 ✅
- `corepack pnpm --filter @qjclaw/desktop typecheck` 通过 ✅
- `smoke:mac:workspace-service` 已新增，用于通过 `ProjectWorkspaceExecutorService` 验证 bundled Python 调用；需在真实 runtime payload 生成后执行
- `corepack pnpm run smoke:mac:workspace-entry` 已执行，当前因缺 `vendor/openclaw-runtime/python/bin/python3` 失败 ❌
- `corepack pnpm run smoke:mac:workspace-service` 已执行，当前因缺 runtime manifest、bundled Python、bundled Node 失败 ❌

### Phase 5: 调整 Electron mac 桌面行为 — ✅ 代码完成，真实启动待验收

已修改文件：`apps/desktop/src/main/create-window.ts`

具体改动：

- 删除 `resolveWindowIcon()` 函数（win32 `.ico` 路径解析）
- 删除 `icon: resolveWindowIcon()` BrowserWindow 选项
- `Menu.setApplicationMenu(null)` 改为 `Menu.setApplicationMenu(buildApplicationMenu())`
- 新增 `buildApplicationMenu()` 函数，提供最小 mac 应用菜单：
  - App 菜单：About、Quit (Cmd+Q)
  - Edit 菜单：Copy (Cmd+C)、Paste (Cmd+V)、Select All (Cmd+A)
  - Window 菜单：Minimize (Cmd+M)、Close (Cmd+W)

`apps/desktop/src/main/index.ts` 无需修改（已有正确的 mac 生命周期逻辑：`window-all-closed` 中对 darwin 不 quit、`activate` 恢复窗口）

验收状态：

- `corepack pnpm --filter @qjclaw/desktop typecheck` 通过 ✅
- `.app` 手工启动、主窗口、设置页、聊天页仍需在 unpacked app/DMG 产出后验收

### Phase 6: 清理 package scripts — ✅ 代码完成

已修改文件：`package.json`

- 新增 `smoke:mac:runtime`：`node build/scripts/mac-runtime-smoke.mjs`
- 新增 `smoke:mac:package`：`node build/scripts/mac-package-smoke.mjs`
- 新增 `smoke:mac:workspace-entry`：`node build/scripts/mac-workspace-entry-smoke.mjs`
- 新增 `smoke:mac:workspace-service`：`node build/scripts/mac-workspace-service-smoke.mjs`
- Windows PowerShell smoke 脚本暂时保留在 package.json 中，但 mac 分支的正式验收命令不再引用它们

### Phase 7: 建立 mac smoke 验证脚本 — ✅ 代码完成，真实 smoke 待跑

新建文件：

- `build/scripts/mac-runtime-smoke.mjs` — 校验 runtime-manifest.json（platform/arch）、所有必需文件存在、Node/Python 执行 probe、Python 关键 import probe、config gateway 校验
- `build/scripts/mac-package-smoke.mjs` — 查找 `.app` 目录、校验 `vendor/openclaw-runtime` 在 Resources 中、校验 `app.asar`、校验 Info.plist
- `build/scripts/mac-workspace-entry-smoke.mjs` — 创建临时 project（含 Python automation）、执行 Python 脚本、校验 QJC_WORKSPACE_EVENT 事件流、校验 Node 可执行
- `build/scripts/mac-workspace-service-smoke.mjs` — 编译并调用 `ProjectWorkspaceExecutorService`，验证 workspace service 使用 bundled Python 和 runtime env

### Phase 8: 端到端 DMG 验证 — ❌ 未开始

待执行：

```bash
corepack pnpm build
corepack pnpm --filter @qjclaw/desktop exec electron-builder --config electron-builder.yml --mac dmg --arm64 --dir
corepack pnpm run materialize:runtime:mac -- \
  --source-python-root "$QJCLAW_MAC_PYTHON_ROOT" \
  --source-openclaw-entry "/path/to/openclaw.mjs" \
  --source-ffprobe "/path/to/ffprobe"
corepack pnpm run smoke:mac:runtime
corepack pnpm run smoke:mac:workspace-entry
corepack pnpm run smoke:mac:workspace-service
corepack pnpm run package:mac
corepack pnpm run smoke:mac:package
```

当前阻塞：

- `QJCLAW_MAC_PYTHON_ROOT` 未设置，且真实 `materialize:runtime:mac` 需要包含 `bin/python3` 的可复制 Python 根目录。
- `openclaw` 不在 PATH 时需通过 `--source-openclaw-entry /path/to/openclaw.mjs` 指定。
- `ffprobe` 不在 PATH 时需安装 ffmpeg 或通过 `--source-ffprobe /path/to/ffprobe` 指定。
- 默认 `~/.openclaw/openclaw.json` 不存在时需通过 `--source-config-path /path/to/openclaw.json` 指定。
- Electron darwin arm64 zip 直连 GitHub 会卡住；用 `HTTPS_PROXY=http://127.0.0.1:7897 HTTP_PROXY=http://127.0.0.1:7897 ALL_PROXY=http://127.0.0.1:7897 ELECTRON_GET_USE_PROXY=true` 可下载成功。
- `corepack pnpm run smoke:mac:package` 当前失败在 packaged runtime payload 不完整：`.app`、`app.asar`、DMG 均存在，但 `Resources/vendor/openclaw-runtime` 只有 README，缺 `runtime-manifest.json`、`node/bin/node`、`python/bin/python3`。

### Milestone 达成状态

| Milestone | 描述 | 状态 | 备注 |
|-----------|------|------|------|
| A | DMG 配置可构建 | ✅ 通过 | 使用本地代理后 `--dir` 产出 `.app` 和 DMG；DMG checksum 验证通过 |
| B | mac runtime 可生成 | ✅ 代码完成，待真实 materialize | 需要 Python/OpenClaw/ffprobe 前置 |
| C | app 可启动 bundled runtime | ✅ 代码完成，待 bundled runtime 启动验证 | typecheck 通过；需在有 runtime payload 时验证 RuntimeManager/Gateway |
| D | DMG 可安装可冒烟 | ❌ 未完成 | 已有无 runtime payload 的 DMG；最终 package smoke 依赖 B 生成完整 runtime 后重跑 |

### 改动文件汇总

已修改（7 个）：

- `apps/desktop/electron-builder.yml`
- `apps/desktop/package.json`
- `apps/desktop/src/main/create-window.ts`
- `apps/desktop/src/main/services/project-workspace-executor.ts`
- `package.json`
- `packages/runtime-manager/src/index.ts`
- `docs/dmg包改造方案.md`

新建（5 个）：

- `build/scripts/materialize-runtime-payload.mjs`
- `build/scripts/mac-runtime-smoke.mjs`
- `build/scripts/mac-package-smoke.mjs`
- `build/scripts/mac-workspace-entry-smoke.mjs`
- `build/scripts/mac-workspace-service-smoke.mjs`
