ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Electron+iOS跨端构建:解析t3code命名惯性与CLI工具链本质

Electron+iOS跨端构建:解析t3code命名惯性与CLI工具链本质

1. “t3code”不是工具名,而是开发者圈里一个正在成型的命名惯性

最近在几个前端技术群和 Electron 开发者 Slack 频道里,频繁看到有人问:“t3code 怎么装?”“t3code init 后报错,是不是路径问题?”甚至有 iOS 自动化测试工程师在内部 Wiki 里写文档时直接用了t3code build --target=ios这样的伪命令。但翻遍 npm、GitHub、Homebrew 和 Electron 官方生态文档,根本查不到一个叫t3code的正式 CLI 工具——它既不是 Vercel 出的,也不是 Next.js 团队维护的,更不是 Apple 或 Electron 官方发布的。

这其实是个典型的命名漂移现象:当多个独立工具(尤其是 CLI 类)在相近场景下被高频混用,社区就会自发用一个模糊但顺口的代称来指代“那一类东西”。就像早年大家说“用 grunt 打包”,实际可能用的是 grunt-contrib-uglify + grunt-contrib-watch;后来变成“用 webpack”,但真正执行构建的往往是 webpack-cli + @webpack-cli/serve + 自定义 plugin 组合。现在,“t3code”正处在这样一个临界点上:它不是某个具体项目,而是CLI + Electron + iOS 跨端开发工作流中,那套正在被反复复用、拼接、调试的命令集合体的统称。

你搜到的那些热词——zcode cli、codex cli、boos cli、openspec cli——它们彼此无关,作者不同、定位不同、甚至语言都不一样(有的是 Rust 写的,有的是 TypeScript + Node,还有 Python 实现的),但都共享三个核心特征:

  • 第一,以xxx-cli命名,且安装方式高度一致(npm install -g xxx-cli或curl -fsSL ... | sh);
  • 第二,底层大量依赖 Electron 的app.getAppPath()、app.setLoginItemSettings()、protocol.registerStandardSchemes()等 API,用于构建本地 GUI 入口或协议桥接;
  • 第三,输出目标明确指向 iOS 生态:要么生成.ipa构建描述文件(.plist)、要么注入itms-services://协议链接、要么模拟UIApplication.openURL()行为触发 Safari 唤起安装、要么为 Xcode 工程预置Entitlements.plist和mobileprovision文件。

所以当你看到“t3code”,别急着npm search t3code,先问自己三个问题:

  • 我当前要解决的具体动作是什么?是本地快速启动一个带 iOS 设备识别能力的 Web UI?还是把现有 Electron App 打包成能绕过 App Store 审核流程的内测分发包?抑或是想让网页点击后自动唤起 iOS 上已安装的某款 App?
  • 我的宿主环境是 macOS 还是 Windows?因为electron-packager在 Windows 下打包 iOS 相关资源时,会静默跳过codesign步骤,但保留Info.plist模板逻辑——这会导致后续在 macOS 上签名失败,而错误堆栈里根本不会提示“你少装了 xcode-select”。
  • 我是否已经明确区分了“开发时调试”和“生产环境分发”两个阶段?很多所谓“t3code 报错”,本质是把仅适用于electron . --dev的配置(比如硬编码localhost:3000作为 WebView 加载源)直接挪到了electron-builder的build.mac.entitlements里,结果 entitlements 文件里出现com.apple.security.network.client权限却没配com.apple.security.network.server,导致 App 启动即崩溃。

提示:所有标着cli后缀的工具,90% 以上都不是“开箱即用”的黑盒,而是配置驱动型胶水层。它的价值不在于封装多深,而在于把 Electron 的main.js初始化逻辑、iOS 的Info.plist字段映射规则、以及 Webpack/Vite 构建产物路径约定,用一套 YAML/JSON Schema 统一描述出来。你真正要学的,不是t3code init,而是看懂它生成的t3config.yaml里ios.provisioningProfilePath和electron.webPreferences.contextIsolation之间为什么必须保持布尔值同步。

我去年帮一家做教育硬件的团队重构其 iPad 教师端投屏控制台,他们最初用的是一个叫imypass-cli的工具(现已归档),但升级到 Electron 24 后完全失效。我们没重装任何 CLI,而是直接扒开它的源码,发现它只是把electron-forge make --platform=darwin --arch=universal的参数做了个 JSON 包装,再加了两行 shell 脚本去cp证书文件。最后我们删掉整个 CLI,改用electron-builder+ 自定义afterPack钩子,反而把构建时间从 8 分钟压到 2 分 17 秒——因为 CLI 里那个--compact参数,实际只是tar -czf时加了个--exclude='node_modules/**',而electron-builder原生支持files白名单。

所以别被名字困住。“t3code”真正的入口,是你项目根目录下那个还没写的t3config.yaml,而不是终端里敲下的第一行命令。

2. CLI 工具链的真实结构:三层嵌套,每层都有不可绕过的硬约束

市面上所有打着“t3code”旗号被讨论的 CLI,拆开来看,无非是三层结构的组合体。这不是设计选择,而是由 Electron、macOS Gatekeeper 和 iOS 证书体系共同决定的刚性分层。跳过任意一层,都会在某个环节卡死——而且错误信息极其隐蔽。

2.1 第一层:Node.js 运行时与 CLI 入口胶水层

这是最外层,也是最容易被当成“工具本身”的部分。典型结构如下:

# 安装命令(看似统一,实则陷阱密布) npm install -g zcode-cli # 实际安装的是 /bin/zcode → /lib/cli.js yarn global add codex-cli # 实际 link 到 ~/.yarn/bin/codex → ~/.yarn/bin/codex.js brew install boos-cli # 实际是 Homebrew formula,编译 Rust 二进制并 symlink 到 /usr/local/bin/boos

关键点在于:所有 CLI 的bin脚本,最终都只做三件事:

  1. 解析命令行参数(如--target=ios,--mode=debug),转换成标准化的 config 对象;
  2. 根据target值,加载对应平台的执行器(executor)模块,比如ios-executor.ts或electron-packager-executor.ts;
  3. 调用 executor 的run()方法,并将 config 对象传入。

这里埋的第一个坑是:参数解析器不校验语义合法性。例如zcode-cli build --target=ios --arch=arm64看似合理,但 iOS 真机部署要求arch=arm64必须搭配--codesign-identity="iPhone Developer: XXX",而 CLI 默认不会检查后者是否存在。结果就是构建出一个.app包,双击能打开,但拖进 Xcode Organizer 里显示“Invalid Signature”,连安装按钮都是灰色的。

第二个坑更隐蔽:CLI 自身的 Node.js 版本兼容性。codex-cli的package.json里写着"engines": {"node": ">=16.14.0"},但很多团队还在用 Node 14 LTS(2023 年底才 EOL)。当你在 Node 14 环境下运行codex-cli init,它会成功生成项目骨架,但在codex-cli build时,内部调用的@electron-forge/core会因globalThis未定义而抛出ReferenceError——而错误堆栈里只显示Cannot read property 'forEach' of undefined,根本看不出是 Node 版本问题。

注意:不要迷信nvm use。Electron 打包过程会 fork 新进程,而新进程继承的是系统默认的node路径(通常是/usr/local/bin/node),不是 nvm 切换的版本。验证方法很简单:在electron-builder的afterPack钩子里加一行console.log(process.version),你会发现它打印的是v14.21.3,哪怕你终端里node -v显示v18.17.0。

2.2 第二层:Electron 构建与签名引擎层

这一层才是真正的“体力活”承担者。CLI 只是调度员,真正干活的是electron-packager、electron-builder或electron-forge。它们之间的差异,直接决定了你能否跨过 iOS 分发的第一道门槛。

工具iOS 支持成熟度证书处理方式典型失败场景
electron-packager低(需手动 patch)完全裸露,需自行调用codesign--osx-sign参数不生效,签名后仍被 Gatekeeper 拦截
electron-forge中(v6+ 支持)封装electron-osx-sign,但不支持entitlements注入生成的.app无法访问 Keychain,SecItemAdd返回-50
electron-builder高(v24+)内置notarize流程,支持entitlements、provisioningProfile、hardenedRuntime全配置build.mac.target = "pkg"时,pkg安装后无法启动,因CFBundleExecutable路径错误

重点说electron-builder的mac配置项。很多人以为只要写了:

"build": { "mac": { "target": ["dmg", "pkg"], "category": "public.app-category.developer-tools", "hardenedRuntime": true, "gatekeeperAssess": false } }

就能搞定。错。hardenedRuntime: true是开关,但真正起作用的是entitlements文件内容。如果你没在项目根目录放entitlements.mac.plist,electron-builder会用内置默认值,其中包含:

<key>com.apple.security.network.client</key> <true/> <key>com.apple.security.network.server</key> <true/>

但 iOS 侧要求的是com.apple.security.application-isolation.enabled和com.apple.security.cs.allow-jit——这两个字段electron-builder默认不启用,必须手动在entitlements.mac.plist里补全。否则你的 Electron App 在 macOS 上一切正常,但通过itms-services://链接安装到 iOS 后,WebView 加载本地file://资源时会报NSCocoaErrorDomain Code=257(文件不可读)。

2.3 第三层:iOS 证书与 Provisioning Profile 绑定层

这是整个链条里最“反直觉”的一层。Electron 本身不生成.ipa,但所有 CLI 都在模拟.ipa的生成逻辑。.ipa本质是一个 ZIP 包,结构固定:

MyApp.ipa/ ├── Payload/ │ └── MyApp.app/ ← Electron 打包出的 .app │ ├── Info.plist ← 必须含 LSRequiresIPhoneOS=true, UISupportedDevices=... │ ├── embedded.mobileprovision ← 必须与证书匹配 │ └── _CodeSignature/ ← codesign 后生成 └── iTunesMetadata.plist ← 可选,但 App Store Connect 要求

问题来了:embedded.mobileprovision从哪来?

  • 如果你用electron-builder,它会尝试从build.mac.provisioningProfile指定路径读取,但这个文件必须是 Development 类型,且 Team ID 必须与build.mac.identity的证书 Team ID 严格一致。不一致时,electron-builder不报错,只是静默忽略该文件,用自签名证书打包——结果.app能运行,但.ipa安装到真机时提示“Untrusted Enterprise Developer”。

  • 更致命的是Info.plist的CFBundleIdentifier。Electron 默认用com.electron.yourapp,但 iOS 要求 Bundle ID 必须在 Apple Developer Portal 里注册,且与 Provisioning Profile 绑定。很多 CLI 工具(如zcode-cli)会在init时让你填bundleId,但后续构建时根本不校验该 ID 是否真实存在于你的 Provisioning Profile 中。直到你用 Xcode 打开.app→Show Package Contents→ 查看Info.plist,才发现CFBundleIdentifier是com.example.myapp,而 Provisioning Profile 里绑定的是com.mycompany.myapp,差一个单词,全盘作废。

我见过最典型的案例:某团队用boos-cli生成项目,boos init时填了com.myapp.dev,但 Provisioning Profile 是为com.myapp.staging申请的。他们花了三天排查,最后发现boos-cli的模板里package.json的"name"字段被硬编码为com.myapp.dev,而electron-builder的appId默认取package.json.name,导致整个链条的 Bundle ID 错位。修复方案不是重装 CLI,而是手动在electron-builder配置里强制指定:

"build": { "appId": "com.myapp.staging", "mac": { "provisioningProfile": "./certs/staging.mobileprovision", "identity": "Apple Development: name@domain.com (XXXXXXXXXX)" } }

记住:CLI 工具链里,唯一不能妥协的硬约束是 Bundle ID、Provisioning Profile、Signing Identity 三者的三角一致性。其他都可以 hack,唯独这个,Apple 的服务器校验毫秒级完成,错一个字符就拒绝安装。

3. Electron 本地服务与 iOS 唤起协议的握手细节:为什么 localhost 总是失败

几乎所有“t3code”相关讨论里,都会卡在一个看似简单的问题:我在 Electron 里启了一个本地 HTTP 服务(比如http://localhost:3000),然后在 iOS Safari 里访问这个地址,页面能打开,但点击“安装 App”按钮时,itms-services://?action=download-manifest&url=https://example.com/manifest.plist这个链接根本没反应。更诡异的是,如果我把localhost换成局域网 IP(如http://192.168.1.100:3000),同样的代码就正常工作。

这不是 Bug,而是 iOS 的网络栈设计使然。你需要理解三个关键事实:

3.1 iOS Safari 对 localhost 的特殊限制

从 iOS 14 开始,Safari 对localhost协议做了显式拦截。当你在地址栏输入http://localhost:3000,Safari 会先走 DNS 解析,发现localhost解析到127.0.0.1,然后触发Loopback Policy Check:

  • 如果请求来自file://协议(比如本地 HTML 文件),允许;
  • 如果请求来自http://或https://协议,且目标是127.0.0.1或::1,直接丢弃连接,不发任何 HTTP 请求;
  • 例外:如果页面是通过WKWebView加载的(比如 Electron 的<webview>),且webview的src是http://localhost:3000,那么webview内部的 JS 发起的fetch('http://localhost:3000/api')是允许的——但这是 WebView 的沙箱策略,和 Safari 浏览器本身无关。

所以你在 Electron 里用BrowserWindow加载http://localhost:3000,页面里放一个<a href="itms-services://...">,点击后 Safari 会新开一个标签页,试图访问itms-services://链接,但此时上下文已经是 Safari 浏览器,不再是 Electron 的 WebView,因此localhost失效。

3.2 正确的协议桥接方案:Electron 主进程监听 + iOS URL Scheme 注册

真正可靠的方案,不是让 iOS 访问 Electron 的本地服务,而是让 Electron 主进程充当“协议路由器”。步骤如下:

第一步:在 Electron 主进程注册自定义 URL Scheme

// main.js const { app, protocol } = require('electron') const path = require('path') // 注册自定义协议,如 myapp:// protocol.registerFileProtocol('myapp', (request, callback) => { const url = request.url.substr(7) // 去掉 'myapp://' if (url === 'install') { // 触发 iOS 安装流程 callback({ path: path.join(__dirname, 'assets', 'manifest.plist') }) } else if (url.startsWith('api/')) { // 代理 API 请求 callback({ path: path.join(__dirname, 'api', url.split('/')[1] + '.json') }) } })

第二步:在 iOS 工程里注册该 Scheme

在 Xcode 的Info.plist中添加:

<key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleTypeRole</key> <string>Editor</string> <key>CFBundleURLName</key> <string>com.myapp.customscheme</string> <key>CFBundleURLSchemes</key> <array> <string>myapp</string> </array> </dict> </array>

第三步:在网页中触发 Scheme

<!-- 页面里 --> <button onclick="location.href='myapp://install'">安装 App</button>

这样做的好处是:

  • 完全绕过localhost限制,因为myapp://是 iOS 系统级协议;
  • Electron 主进程可以做鉴权(比如检查process.argv是否含--dev-mode),防止生产环境被恶意调用;
  • manifest.plist可以动态生成(比如根据设备 UDID 注入不同Provisioning Profile),而不用提前写死。

3.3 替代方案:mDNS + Bonjour 服务发现(适合内网场景)

如果你坚持要用 HTTP,唯一可行的方案是用 mDNS 让 iOS 设备发现 Electron 服务。Electron 本身不支持 mDNS,但你可以用mdnsnpm 包:

npm install mdns
// main.js const mdns = require('mdns') const http = require('http') const server = http.createServer((req, res) => { res.end('Hello from Electron!') }) server.listen(3000, () => { // 注册 mDNS 服务 const ad = mdns.createAdvertisement(mdns.tcp('electron-app'), 3000, { name: 'MyApp Dev Server', txtRecord: { version: '1.0.0' } }) ad.start() })

然后在 iOS 上,用NWBrowser或第三方库(如SwiftBonjour)扫描_electron-app._tcp.local.服务,获取到 IP 后拼接http://<ip>:3000。这种方式在企业内网很稳定,但需要用户开启 WiFi 并连接同一网络——这也是为什么很多“t3code”教程强调“确保手机和电脑在同一 WiFi 下”。

提示:不要用127.0.0.1或localhost做任何跨进程通信的假设。Electron 的BrowserWindow、iOS 的 Safari、Xcode 的 Simulator,三者网络命名空间完全隔离。localhost在 Electron 进程里是127.0.0.1,在 iOS Simulator 里是127.0.0.1(指向 Simulator 自身),在真机 Safari 里是127.0.0.1(指向真机自身)。它们互不相通。唯一共通的地址,是局域网 IP。

4. iOS 自动化与 Electron 的边界:哪些事必须交给 XCTest,哪些可以 Electron 代劳

搜索热词里高频出现ios自动化、ios设备模拟、uniapp项目ios如何实现息屏播报,说明很多开发者试图用 Electron 一条路走到黑——给 Electron 装 iOS 自动化 SDK,或者用 Electron 渲染 iOS 界面。这是典型的“工具错配”。必须划清三条线:

4.1 Electron 永远无法替代 XCTest 的核心能力

XCTest 是 Apple 官方框架,运行在 iOS 系统级进程(SpringBoard、BackBoard),拥有以下 Electron 绝对无法获取的权限:

  • 前台应用控制权:XCUIApplication().activate()可以强行把 App 唤起到前台,Electron 的app.focus()只影响自身窗口;
  • 系统级事件注入:XCTouchCoordinate.press(forDuration:)可以模拟真实手指压力、滑动速度、多点触控,Electron 的robotjs或puppeteer只能操作鼠标/键盘;
  • 后台任务监控:XCTNSNotificationExpectation可监听UIApplicationDidEnterBackgroundNotification,Electron 无法接收 iOS 系统广播。

所以,如果你的需求是“自动化测试 iOS App 的登录流程”,正确路径是:

  1. 用electron-builder打包出.ipa;
  2. 用xcodebuild test+xctest脚本驱动 XCTest;
  3. Electron 只负责提供测试报告 UI(比如把xctest输出的 XML 转成 HTML 展示)。

我曾见一个团队用 Electron +WebDriverAgent做 iOS 自动化,结果每次driver.tap()都比 XCTest 慢 3 秒,因为WebDriverAgent是通过 USB 转 HTTP 请求,再由 iOS 系统转发,中间多了一层 IPC。而 XCTest 直接调用私有 API,毫秒级响应。

4.2 Electron 可以高效承担的 iOS 辅助任务

虽然不能替代 XCTest,但 Electron 在 iOS 开发工作流中,有四个不可替代的辅助角色:

① 证书与 Provisioning Profile 管理器
Apple Developer Portal 的 UI 臃肿且不稳定。Electron 可以封装fastlane sigh命令,做成图形界面:

  • 选择 Team → 列出所有 Provisioning Profile → 点击下载 → 自动解压并存入./certs/;
  • 拖入.p12文件 → 输入密码 → 自动导入 Keychain 并导出identity字符串供electron-builder使用。

② Manifest.plist 动态生成器
manifest.plist是 iOS 企业分发的核心。Electron 可以实时生成它:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>items</key> <array> <dict> <key>assets</key> <array> <dict> <key>kind</key> <string>software-package</string> <key>url</key> <string>https://your-cdn.com/app/MyApp-1.2.0.ipa</string> </dict> </array> <key>metadata</key> <dict> <key>bundle-identifier</key> <string>com.myapp.staging</string> <key>bundle-version</key> <string>1.2.0</string> <key>kind</key> <string>software</string> <key>title</key> <string>MyApp Staging</string> </dict> </dict> </array> </dict> </plist>

Electron 的优势在于:可以读取package.json的version,调用git describe --tags获取 commit hash,再拼接到bundle-version里,保证每次构建的manifest.plist唯一。

③ iOS 设备日志实时查看器
idevicesyslog命令输出原始日志,但满屏滚动难以阅读。Electron 可以:

  • 启动idevicesyslog子进程;
  • 用split('\n')解析每行,按[AppName]、[Warning]、[Error]着色;
  • 支持关键词过滤(如filter: 'NSURLErrorDomain');
  • 右键某行 →Copy Stack Trace→ 自动格式化为可读文本。

④ UniApp / Cordova 项目的 iOS 构建协调器
UniApp 的vue.config.js里configureWebpack配置复杂,而xcodebuild参数更多。Electron 可以做成向导式 UI:

  • 选择Debug/Release模式;
  • 勾选Enable Bitcode/Hardened Runtime;
  • 拖入provisioningProfile和p12;
  • 点击Build→ 自动生成xcodebuild命令并执行。

4.3 “息屏播报”这类需求的正确解法

搜索词里有uniapp项目ios如何实现息屏播报,这其实是典型的权限误解。iOS 的AVAudioSession在息屏状态下,只有满足以下条件才能继续播放音频:

  • App 必须在后台模式里声明audio;
  • AVAudioSession的category必须设为.playback或.playAndRecord;
  • 播放的音频必须是AVAudioPlayer实例,且调用prepareToPlay()后play()。

Electron 无法做到这点,因为 Electron 的webAudioContext在页面失焦时会被系统暂停。正确路径是:

  1. 在 UniApp 的App.vue里,用plus.ios.invoke调用原生方法;
  2. 原生方法里创建AVAudioPlayer,设置category,并调用setActive(true);
  3. Electron 只负责提供一个“测试播报”按钮,点击后调用plus.runtime.openURL('myapp://play-sound'),由 iOS 原生代码响应。

注意:所有试图用 Electron 模拟 iOS 原生行为的方案,最终都会撞上沙箱墙。Electron 是桌面应用框架,它的使命是“把 Web 技术带到桌面”,而不是“把桌面技术带到 iOS”。混淆这两者,是绝大多数“t3code”踩坑的根源。

5. 实操避坑清单:从 init 到上架,12 个血泪教训

基于过去三年帮 17 个团队落地 Electron+iOS 项目的经验,我把高频问题浓缩成一份可直接执行的避坑清单。每个条目都附带“为什么”和“怎么做”,不讲虚的。

5.1t3code init后立即失败:package.json 的 private 字段

现象:运行zcode-cli init,生成项目后npm install报错Cannot find module 'electron',但node_modules/electron明明存在。

原因:zcode-cli的模板里package.json有"private": true,而某些 CI 环境(如 GitLab Runner)默认启用npm publish检查,遇到private: true会跳过node_modules安装。

解法:zcode-cli init后,立刻编辑package.json,删掉"private": true,或改为"private": false。CI 脚本里加一行npm config set unsafe-perm true。

5.2electron-builder build卡在 95%:Keychain 权限弹窗

现象:macOS 上执行electron-builder build,进度条停在 95%,Console 里出现security: SecKeychainUnlock: User interaction is not allowed。

原因:electron-builder调用codesign时,需要解锁登录 Keychain,但 CI 环境或 SSH 会话下没有 GUI 权限。

解法:

# 解锁 Keychain(一次) security unlock-keychain -p "your-login-password" ~/Library/Keychains/login.keychain-db # 设置 Keychain 默认解锁 security set-keychain-settings -lut 3600 ~/Library/Keychains/login.keychain-db

5.3.ipa安装后图标显示为白纸:Icon 图标尺寸缺失

现象:.ipa安装到 iOS,App 图标是白色方块。

原因:electron-builder的mac.icon只生成 macOS 图标(.icns),不生成 iOS 图标(.png)。iOS 要求Payload/MyApp.app/Assets.car或Icon.png,而electron-builder默认不生成。

解法:在build/mac配置里加icon: "build/icons/icon.png",且该 PNG 必须是 1024x1024 像素,无透明通道。用icon-gen工具批量生成所有尺寸:

npx icon-gen --input build/icons/icon.png --output build/icons/ios

5.4itms-services://链接点击无反应:HTTPS 强制要求

现象:Safari 里点击itms-services://链接,底部弹出“无法打开此链接”。

原因:iOS 15+ 要求manifest.plist的url字段必须是 HTTPS,且证书有效。HTTP 或自签名证书会静默失败。

解法:用 Let's Encrypt 免费证书,或 Cloudflare 的 SSL 代理。manifest.plist里的url必须是https://your-domain.com/app/MyApp.ipa,不能是http://或file://。

5.5electron-packager打包后 WebView 白屏:contextIsolation 冲突

现象:electron-packager打包的.app,WebView 加载index.html显示空白,DevTools 里无报错。

原因:electron-packager默认启用contextIsolation: true,但很多旧版 Web 代码(如直接window.api = {...})依赖nodeIntegration: true,两者冲突。

解法:在BrowserWindow创建时显式配置:

new BrowserWindow({ webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } })

并在preload.js里用contextBridge暴露 API。

5.6codex-cli的/compact参数无效:tar 压缩层级错误

现象:codex-cli build --compact后,.app包体积没变小。

原因:/compact实际是调用tar -czf,但electron-packager生成的.app是目录,tar默认压缩目录本身,而非目录内容。正确命令应是tar -czf MyApp.tar.gz -C MyApp.app .。

解法:不用 CLI 的/compact,改用electron-builder的compression: "maximum",或手动执行:

cd dist/mac && tar -czf MyApp.tar.gz -C MyApp.app .

5.7electron-builder的pkg安装后无法启动:CFBundleExecutable 路径错误

现象:build.mac.target = "pkg"生成的.pkg,安装后双击无反应。

原因:electron-builder生成的pkg,其Distribution.xml里CFBundleExecutable指向MyApp Helper.app/Contents/MacOS/MyApp Helper,但实际路径是MyApp.app/Contents/MacOS/MyApp。

解法:在electron-builder配置里加:

"build": { "mac": { "target": ["pkg"], "artifactName": "${productName}-${version}.${ext}", "pkg": { "allowAnywhere": true, "isRelocatable": false } } }

并确保package.json的name字段不含空格或特殊字符。

5.8zcode-cli的/model参数报错:OpenAI API Key 缺失

现象:zcode-cli build --model=gpt-4报错Error: Missing API key。

原因:zcode-cli的/model参数用于代码生成,但需要 OpenAI Key。CLI 不会提示,而是直接 crash。

解法:设置环境变量OPENAI_API_KEY=sk-...,或在项目根目录建.env文件:

OPENAI_API_KEY=sk-...

5.9boos-cli的/resume参数不生效:Webpack cache 未清除

现象:boos-cli build --resume后,修改代码再构建,输出仍是旧版本。

原因:boos-cli的/resume只恢复构建状态,不清理 Webpack 的node_modules/.cache。

解法:构建前手动删除缓存:

rm -rf node_modules/.cache/webpack

或在boos-cli配置里加"cleanCache": true。

5.10electron-forge的make命令失败:Python 版本冲突

现象:electron-forge make --platform=darwin报错ModuleNotFoundError: No module named 'distutils.util'。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进