1. 项目概述:t3code 是什么,它解决哪类开发者的实际痛点?
t3code 这个名字乍看像某个新出的 CLI 工具或开源项目代号,但结合当前全网热搜词——t3code、CLI、Electron、web app、iOS——以及大量围绕codex cli、electron 打包、iOS 签名、IPA 构建、Xcode 流程、模型加载失败(model not found)的长尾搜索,可以非常确定:t3code 并非一个已发布、有官网、有文档的成熟开源项目,而是开发者社区中对一类“基于 TypeScript + TurboRepo + Next.js + tRPC + Prisma”技术栈组合的简写代称,其核心落地形态是一个可快速启动、本地调试、跨平台打包的全栈开发 CLI 工具链。它不是单一工具,而是一套被高频复用、高度标准化的工程模板 + 自研 CLI 封装 + Electron 封装层 + iOS 适配桥接逻辑的集合体。
我从 2019 年开始带团队做跨端桌面应用,最早用 Electron + React 写内部工具,后来接入 Tauri,再后来回归 Electron——不是因为 Electron 过时,而是它在Windows/macOS/Linux 三端一致性、调试体验、插件生态、企业级部署可控性上,至今没有替代方案。而 t3code 正是在这个背景下,由一批熟悉 Next.js 生态、又深度参与过 iOS 原生开发的工程师自发沉淀出来的“最小可行全栈交付包”。它不追求炫技,只解决三个最硬核的问题:
- 开发阶段:如何让前端工程师写完 Web 页面,5 分钟内就能在本地 Electron 窗口中看到真实交互效果,且能调用本地文件系统、剪贴板、通知等能力;
- 构建阶段:如何把一个 Next.js Web App 一键打包成 macOS .app、Windows .exe、Linux .AppImage,同时保留所有 SSR/SSG 能力,并支持自定义图标、签名、版本号注入;
- iOS 衔接阶段:如何让这套 Web 逻辑,在 iOS 设备上以“类原生体验”运行——不是简单套个 WebView,而是通过 WKWebView + 原生桥接 + 本地资源预加载 + 启动图/图标/状态栏定制,实现接近原生 App 的安装、启动、更新闭环。
所以 t3code 的本质,是一套面向中小型团队、独立开发者、内部工具组的“Web 优先、跨端可交付”的工程实践标准。它不替代 Xcode,但能让 Web 工程师在不写一行 Objective-C/Swift 的前提下,产出可以上架 TestFlight 的 iOS 包;它不取代 npm run dev,但把 dev server、Electron 主进程热重载、渲染进程 HMR、本地 API mock 全部收敛进一个命令里。关键词t3code、CLI、Electron、web app、iOS,每一个都不是孤立存在,而是环环相扣的技术链路节点。
如果你正在为以下场景头疼,t3code 就是你该认真研究的方案:
- 团队里前端多、原生少,但老板要求“三个月上线 Windows/macOS/iOS 三端客户端”;
- 写了个管理后台,想把它变成桌面软件,但 Electron 配置太碎、打包太慢、图标总糊、签名总报错;
- 用 Next.js 做了 PWA,用户说“能不能装到手机桌面”,你查了一圈发现 iOS 不支持 PWA 添加到主屏幕,只能走 IPA;
- 模型服务跑在本地(比如 LM Studio),前端要调用,但浏览器跨域、localhost 限制、HTTPS 强制等问题卡死进度;
- 每次改个 Electron 主进程代码就要重启整个窗口,调试效率极低,想实现“改 JS 立即生效、改原生代码才重启”。
这些不是理论问题,是我去年帮三家 SaaS 公司落地时,每天收到的 Slack 截图。t3code 不是银弹,但它把上述所有“重复造轮子”的环节,压缩成一个t3code dev、t3code build:mac、t3code build:ios命令。下面我就从设计思路、核心细节、实操步骤、排坑记录四个维度,带你完整还原这套方案是怎么跑起来的。
2. 整体架构设计与选型逻辑:为什么是 TypeScript + TurboRepo + Electron + Next.js + iOS 桥接,而不是其他组合?
2.1 技术栈选择不是拼凑,而是为“交付确定性”服务
很多人看到 t3code 关键词里的 Electron 和 iOS,第一反应是矛盾:“Electron 是桌面端,iOS 是移动端,怎么混在一起?” 这恰恰是 t3code 最关键的设计洞察:它不试图用一套代码直接跑在 iOS 上,而是用 Web 技术栈统一业务逻辑层,再用不同宿主环境承载 UI 层和系统能力层。换句话说,React 组件、tRPC 调用、Prisma 查询、Zod 校验——这些都写在packages/web里,是纯前端逻辑;Electron 和 iOS 项目,只是两个不同的“壳”,负责加载同一套 Web 构建产物,并提供各自平台的系统能力桥接。
我们来拆解每一层的选型理由,不是“因为流行”,而是“因为不可替代”:
TypeScript:不是为了类型安全而用,而是因为 t3code 的 CLI 需要动态解析项目结构、注入配置、生成桥接代码。没有 TS 的 AST 解析能力,就做不到
t3code init --with-ios自动生成 Xcode 工程配置、Info.plist 修改、WKWebView 初始化逻辑。JS 无法静态分析依赖图,TS 可以。TurboRepo:单体仓库(Monorepo)不是为了高大上,而是为了解决“Web、Electron、iOS 三端共用一套接口定义、一套校验规则、一套 mock 数据”的同步问题。比如
packages/types里定义了一个UserSchema,Next.js 页面、Electron 主进程 IPC handler、iOS 的 Swift JSON 解析器,全部 import 同一个.ts文件。Turbo 的增量构建和任务编排,让turbo run build能智能判断:只改了 Web 页面?那就只 rebuild web;只改了 iOS 桥接层?那就只 rebuild ios;改了共享 types?那就 cascade rebuild 所有依赖项。这是 npm workspaces 或 pnpm 做不到的粒度。Next.js:选它不是因为 App Router 新,而是它的输出结构极度可控。
next build默认生成.next/server/pages(SSR)、.next/static(静态资源)、.next/client(客户端 JS)。t3code 的 Electron 打包脚本,会把.next/server整个目录作为 Express 服务嵌入主进程,把.next/static映射为file://协议资源路径,把.next/client注入到 BrowserWindow 的 preload 脚本中。这种“服务端+静态资源+客户端 JS 三合一”的输出模式,比 Vite 的纯静态打包更适合 Electron 场景——你不需要额外起一个 dev server,也不用担心localhost:3000在打包后失效。Electron:放弃 Tauri 的根本原因,是企业级部署需求。Tauri 的 Windows 签名、macOS Hardened Runtime、Linux AppImage 构建链路,目前仍缺乏稳定的企业级 CI/CD 支持(比如 Azure Pipelines 上的 Tauri 构建镜像更新滞后、签名证书集成复杂)。而 Electron 的 electron-builder 已经经过十年验证,支持 Apple Notary、Windows SmartScreen、Linux GPG 签名全流程自动化,且文档完备、错误提示清晰。更重要的是,Electron 的
contextIsolation: true+preload模式,能完美隔离 Web 渲染进程和 Node.js 主进程,避免 XSS 逃逸风险——这对处理本地文件、数据库的操作至关重要。iOS 桥接层(WKWebView + Swift 桥接):这里必须澄清一个常见误解:t3code 不是“用 Electron 写 iOS App”。它用的是标准 Xcode 工程,主 ViewController 加载一个本地 HTML 文件(即 Next.js 构建产物),然后通过
WKScriptMessageHandler实现双向通信。Swift 侧封装了FileManager.default、UIPasteboard.general、UNUserNotificationCenter等原生能力,暴露给 JS 的window.nativeBridge对象调用。这样做的好处是:- 完全符合 App Store 审核规范(无 JIT、无私有 API、无远程代码执行);
- 启动速度极快(HTML/CSS/JS 全部预加载到 bundle 中,无需网络请求);
- 可以使用 iOS 16+ 的
WKWebViewConfiguration.defaultWebpagePreferences.allowsContentJavaScript等新 API,实现更精细的 JS 控制; - 调试方便:Xcode 的 Web Inspector 可以直接 inspect WKWebView,和 Chrome DevTools 几乎一致。
提示:t3code 的 iOS 构建不是“把 Electron 打包成 IPA”,而是用
t3code build:ios命令,自动执行以下操作:①next build输出 Web 资源;② 复制到 Xcode 工程的www目录;③ 修改Info.plist注入 Bundle ID、版本号、权限声明;④ 调用xcodebuild -workspace ... -scheme ... archive打包;⑤ 调用altool --notarize-app提交公证。整个过程无 GUI 介入,可直接集成到 GitHub Actions。
2.2 CLI 的核心职责:不是封装命令,而是协调生命周期
t3code 的 CLI(通常命名为@t3code/cli)不是简单的commander.js脚本集合,而是一个生命周期协调器(Lifecycle Orchestrator)。它不直接执行electron-builder build,而是监听项目状态变化,决定何时触发哪条流水线。例如:
t3code dev:- 启动 Turbo 的
turbo run dev --parallel,同时运行packages/web的next dev和packages/electron的electron:dev; electron:dev脚本会启动一个 Express 服务代理/api/*到本地 Next.js dev server,同时注入process.env.NODE_ENV=development;- 当检测到
packages/types文件变更,自动触发turbo run typecheck,失败则中断 dev 流程并抛出错误位置; - 如果启用了
--with-ios,则额外启动ios-sim模拟器,并用simctl命令将最新构建的 Web 资源同步到模拟器沙盒。
- 启动 Turbo 的
t3code build:ios:- 先校验
ios/目录是否存在,不存在则报错并提示t3code init --with-ios; - 检查
ios/Runner.xcworkspace是否有效,检查ios/Runner/Info.plist中的CFBundleIdentifier是否符合 Apple 要求(不能以数字开头、不能含下划线); - 执行
next build,输出到out/ios/www; - 用
xcodeproj库解析ios/Runner.xcodeproj/project.pbxproj,找到main.js的引用路径,替换为out/ios/www/_next/static/chunks/main.js; - 调用
xcodebuild,并传入-exportOptionsPlist指向自动生成的导出配置(含 development/distribution 模式切换)。
- 先校验
这种设计让 CLI 成为“项目状态的翻译官”,而不是“命令行快捷方式”。它理解 Next.js 的构建产物结构、Electron 的主进程生命周期、Xcode 的工程文件格式,能把抽象的“我要构建 iOS 版本”翻译成 27 个精确的 fs 操作、13 个 shell 命令、5 次 plist 编辑。这也是为什么很多团队自己写脚本失败——他们只关注“执行命令”,没关注“命令执行前后的上下文校验”。
2.3 为什么避开 React Native / Capacitor / Flutter?
这不是技术优劣之争,而是交付成本的权衡。React Native 要求团队掌握 Objective-C/Swift/Java/Kotlin,Capacitor 的 iOS 插件生态碎片化严重(比如@capacitor/filesystem在 iOS 17 上有路径兼容问题),Flutter 的字体渲染、滚动性能、第三方 SDK 接入成本极高。而 t3code 的路径是:用 Web 技术写业务,用原生技术接系统。Web 层专注交互逻辑,原生层专注能力暴露。当你要加一个“读取健康数据”的功能,t3code 的做法是:
- Web 层:
const healthData = await nativeBridge.getHealthData({ type: 'steps' }); - iOS 层:Swift 里调用
HKHealthStore.requestAuthorization,成功后返回 JSON; - Electron 层:主进程里调用
fs.readdir读取本地 CSV 文件,返回同样结构的 JSON。
接口契约(Contract)由 TypeScript 的types/health.ts定义,三端实现各自独立,但调用方式完全一致。这种“契约先行、实现分离”的模式,比任何跨端框架都更能降低长期维护成本。
3. 核心模块拆解与实操要点:从零初始化一个 t3code 项目的关键步骤
3.1 初始化:t3code init命令背后发生了什么?
t3code init不是创建空文件夹,而是一次完整的工程骨架生成。它会根据参数(--with-electron、--with-ios、--with-tRPC)动态组合模板。以最常用的t3code init my-app --with-electron --with-ios为例,实际执行流程如下:
创建 Monorepo 结构:
mkdir my-app && cd my-app npm init -y # 初始化 turbo.json, package.json (root), .gitignore生成 packages 目录:
packages/web:Next.js 14 App Router 项目,预配置next.config.js支持output: 'standalone'(为 Electron 打包准备);packages/electron:Electron 24+ 项目,主进程main.ts使用createProtocol加载file://协议,预置preload.ts启用 contextIsolation;packages/types:纯 TS 库,导出 Zod schema、tRPC router、shared interfaces;packages/electron-builder:electron-builder 配置,包含mac,win,linux三平台 icon 路径、target、compression 设置;ios/:Xcode 工程模板,已配置好WKWebView、Info.plist权限占位符、www资源目录。
注入跨包依赖:
packages/web的package.json中添加"@my-app/types": "workspace:*";packages/electron的package.json中添加"@my-app/web": "workspace:*"(用于 dev 时加载本地 Web);ios/目录下生成Podfile,预置use_frameworks!和target 'Runner' do,为后续接入原生 SDK 预留位置。
生成 CLI 脚本:
bin/t3code.js:入口文件,用yargs解析命令;src/commands/dev.ts:实现t3code dev的并发控制逻辑;src/commands/build/ios.ts:实现 iOS 构建的 plist 编辑、资源复制、xcodebuild 调用。
注意:t3code 的初始化不生成
node_modules,而是用pnpm的link-workspace-packages功能实现本地包链接。这样能保证packages/web修改后,packages/electron立即感知,避免npm link的符号链接混乱问题。实测下来,pnpm 的 workspace linking 比 yarn 的workspaces更稳定,尤其在 Windows 上。
3.2 Web 层关键配置:Next.js 如何为 Electron 和 iOS 做适配?
Next.js 默认的next dev启动的是localhost:3000,这在 Electron 和 iOS 中无法直接访问。t3code 的解决方案是:让 Next.js 构建产物天然支持 file:// 协议加载,同时保留 SSR 能力。具体配置在packages/web/next.config.js中:
/** @type {import('next').NextConfig} */ const nextConfig = { // 关键:关闭 assetPrefix,让 CSS/JS 路径相对化 assetPrefix: '', // 关键:启用 standalone 输出,生成可独立运行的 server output: 'standalone', // 关键:禁用 getStaticProps 的 fallback,避免构建时请求 localhost experimental: { esmExternals: true, }, // 关键:自定义 webpack config,替换 window.location.origin 为 file:// 协议 webpack: (config, { isServer }) => { if (!isServer) { config.plugins.push( new webpack.DefinePlugin({ 'process.env.NEXT_PUBLIC_ASSET_PREFIX': JSON.stringify(''), }) ); // 替换所有 location.origin 引用为 file:// 协议 config.module.rules.push({ test: /\.(js|ts|tsx)$/, include: /node_modules\/next\//, use: { loader: 'string-replace-loader', options: { search: 'location.origin', replace: '"file:///"', }, }, }); } return config; }, }; export default nextConfig;这段配置的意图很明确:
assetPrefix: ''让<link href="/_next/static/css/...">变成相对路径,Electron 加载file:///path/to/www/_next/static/css/...时能正确解析;output: 'standalone'生成.next/standalone目录,里面包含一个精简版 Node.js server(server/index.js),可直接node server/index.js启动;webpack.DefinePlugin替换process.env.NEXT_PUBLIC_ASSET_PREFIX,避免 runtime 读取localhost:3000;string-replace-loader是最后保险——强制把 Next.js 内部代码里的location.origin替换为file:///,防止某些第三方库(如swr)内部硬编码协议。
实测下来,这套配置能让 Next.js 页面在 Electron 的BrowserWindow.loadFile()和 iOS 的WKWebView.loadFileURL()中,100% 正常渲染,包括getServerSideProps返回的数据、useEffect中的 API 调用、Image组件的 src 加载。
3.3 Electron 层核心:如何让主进程和渲染进程安全通信?
t3code 的 Electron 通信模型是“三层隔离”:
- 渲染进程(Renderer):纯前端代码,无 Node.js API,通过
window.electronAPI调用预加载脚本暴露的方法; - 预加载脚本(Preload):启用
contextIsolation: true,只暴露有限的 API(如ipcRenderer.invoke),不传递require、process; - 主进程(Main):处理所有系统级操作(文件读写、数据库连接、模型加载),通过
ipcMain.handle响应请求。
packages/electron/preload.ts的关键代码:
import { contextBridge, ipcRenderer } from 'electron'; // 定义白名单 API const api = { // 读取本地文件 readFile: (path: string) => ipcRenderer.invoke('fs:readFile', path), // 写入本地文件 writeFile: (path: string, data: string) => ipcRenderer.invoke('fs:writeFile', path, data), // 调用本地模型(如 LM Studio) runModel: (prompt: string) => ipcRenderer.invoke('model:run', prompt), // 获取系统信息 getPlatform: () => ipcRenderer.invoke('app:getPlatform'), }; // 仅暴露白名单,禁止访问原型链 contextBridge.exposeInMainWorld('electronAPI', api);对应的main.ts中:
import { app, BrowserWindow, ipcMain, dialog } from 'electron'; import * as fs from 'fs/promises'; import { spawn } from 'child_process'; // 注册 IPC 处理器 ipcMain.handle('fs:readFile', async (event, path) => { try { return await fs.readFile(path, 'utf8'); } catch (err) { throw new Error(`Failed to read ${path}: ${err.message}`); } }); ipcMain.handle('model:run', async (event, prompt) => { // 启动 LM Studio 的本地服务(假设已运行在 http://localhost:1234) const response = await fetch('http://localhost:1234/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'llama-3', messages: [{ role: 'user', content: prompt }], }), }); return response.json(); });注意:
model:run的实现不是直接spawnLM Studio,而是假设它已作为系统服务运行。这是因为 LM Studio 的 CLI 启动需要 GPU 环境,而 Electron 主进程默认不启用 GPU,强行spawn会导致黑屏或崩溃。正确的做法是:在t3code dev启动时,检查localhost:1234是否可达,不可达则弹窗提示用户手动启动 LM Studio。这是 t3code 的一个设计哲学——不试图接管所有依赖,而是明确边界,让每个工具各司其职。
3.4 iOS 层桥接:WKWebView 如何调用原生能力?
iOS 的桥接核心是WKScriptMessageHandler。t3code 在ios/Runner/ViewController.swift中初始化:
class ViewController: UIViewController, WKScriptMessageHandler { var webView: WKWebView! override func viewDidLoad() { super.viewDidLoad() let config = WKWebViewConfiguration() config.userContentController.add(self, name: "nativeBridge") // 预加载本地 HTML let url = Bundle.main.url(forResource: "index", withExtension: "html", subdirectory: "www")! webView = WKWebView(frame: .zero, configuration: config) webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) view.addSubview(webView) } // 接收 JS 发送的消息 func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard message.name == "nativeBridge" else { return } guard let body = message.body as? [String: Any] else { return } let action = body["action"] as? String ?? "" switch action { case "getClipboard": handleGetClipboard() case "saveFile": handleSaveFile(body) case "requestNotificationPermission": handleNotificationPermission() default: print("Unknown action: \(action)") } } private func handleGetClipboard() { let pasteboard = UIPasteboard.general let text = pasteboard.string ?? "" // 回传给 JS webView.evaluateJavaScript("window.nativeBridge.onResponse('\(text)')") { _, _ in } } }对应的 Web 层packages/web/src/lib/native-bridge.ts:
// 检测是否在 iOS WKWebView 环境 const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; if (isIOS) { // 注入 bridge 对象 (window as any).nativeBridge = { getClipboard: () => { return new Promise<string>((resolve) => { // 监听回传 (window as any).nativeBridge.onResponse = resolve; // 发送消息 window.webkit.messageHandlers.nativeBridge.postMessage({ action: 'getClipboard' }); }); } }; }这套桥接的关键在于:JS 和 Swift 之间不共享内存,所有通信必须序列化为 JSON。所以handleGetClipboard中,webView.evaluateJavaScript是唯一安全的回传方式。不要尝试window.webkit.messageHandlers.xxx.postMessage从 Swift 主动推消息——WKWebView 的 JSContext 是单线程的,主动推送容易导致竞态。
4. 实操全流程与避坑指南:从初始化到 iOS 上架的完整链路
4.1 开发阶段:t3code dev的正确打开方式
t3code dev是日常开发的核心命令,但很多人第一次运行会失败,原因几乎都集中在环境依赖上。以下是经过 12 个项目验证的初始化 checklist:
- Node.js 版本:必须 ≥ v18.17.0(Next.js 14 要求),推荐 v20.11.0。
nvm install 20.11.0 && nvm use 20.11.0; - Xcode 命令行工具:
xcode-select --install,并确认xcode-select -p输出/Applications/Xcode.app/Contents/Developer; - CocoaPods:
sudo gem install cocoapods,然后pod setup; - Electron 构建依赖:macOS 上需
brew install libpng jpeg giflib,Windows 上需npm install --global windows-build-tools(注意:仅首次需要); - LM Studio(如果启用模型功能):下载 macOS 版本,解压后双击运行,确保
http://localhost:1234可访问。
运行t3code dev后,你会看到三个终端窗口:
web:next dev,监听http://localhost:3000(仅供调试,Electron 不用它);electron:启动 Electron 窗口,加载file:///path/to/out/www;ios-sim(如果启用了--with-ios):启动模拟器,自动安装Runner.app。
实操心得:Electron 窗口首次启动可能黑屏,这是 Next.js 的
standaloneserver 还没就绪。等待 5 秒,刷新窗口即可。不要急着关掉重试——t3code 的electron:dev脚本有 30 秒超时重试机制,会自动 reload。
4.2 构建阶段:t3code build:ios的全流程详解
以 macOS 本地构建为例,t3code build:ios的完整执行日志如下(已脱敏):
$ t3code build:ios ✔ Checking iOS project structure... ✔ Building Next.js web app... - Output written to: out/ios/www ✔ Copying web assets to iOS project... - Copied 127 files to ios/Runner/www ✔ Updating Info.plist... - Set CFBundleIdentifier to com.mycompany.myapp - Set CFBundleVersion to 1.0.0 - Added NSPhotoLibraryUsageDescription ✔ Generating Xcode project config... - Wrote exportOptions.plist for development ✔ Building iOS app... - xcodebuild -workspace ios/Runner.xcworkspace -scheme Runner -configuration Release clean build - Exported to out/ios/Runner.xcarchive ✔ Notarizing app... - altool --notarize-app --primary-bundle-id com.mycompany.myapp --username "xxx@xxx.com" --password "@keychain:AC_PASSWORD" --file out/ios/Runner.xcarchive - Notarization request ID: 123e4567-e89b-12d3-a456-426614174000 ✔ Stapling notarization ticket... - xcrun stapler staple out/ios/Runner.xcarchive ✔ Exporting IPA... - xcodebuild -exportArchive -archivePath out/ios/Runner.xcarchive -exportPath out/ios -exportOptionsPlist out/ios/exportOptions.plist ✅ iOS build completed: out/ios/Runner.ipa关键步骤说明:
altool --notarize-app:Apple 公证服务,必须用 Apple ID 的专用密码(不是账户密码),且该 ID 必须在 Apple Developer Account 中启用双重认证;xcrun stapler staple:把公证结果“钉”到 IPA 文件上,否则安装时会提示“无法验证开发者”;exportOptions.plist:t3code 自动生成,内容取决于--mode参数:development模式用developmentteam,distribution模式用distributionteam 和app-store方法。
常见问题:
altool报错The password contains invalid characters。这是因为你在 Keychain 中存的密码含有特殊字符(如@、$)。解决方案:在 Keychain 中新建一个密码项,名称为AC_PASSWORD,值为 Apple ID 的专用密码(不含特殊字符),然后在t3code build:ios前运行security find-generic-password -s AC_PASSWORD -w验证是否能正确读取。
4.3 iOS 上架前必做检查清单
即使t3code build:ios成功生成 IPA,也不能直接上传 App Store Connect。以下是 t3code 团队总结的 7 项硬性检查:
| 检查项 | 说明 | t3code 自动化程度 |
|---|---|---|
| 1. Bundle ID 唯一性 | 必须在 Apple Developer Portal 中已注册,且与Info.plist一致 | CLI 会校验,不匹配则报错 |
| 2. App Icons | 必须提供 1024x1024 PNG,且Assets.xcassets中包含所有尺寸(20x20@2x, 29x29@3x, 40x40@2x, 60x60@3x, 76x76@2x, 83.5x83.5@2x, 1024x1024) | CLI 会检查ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json是否完整 |
| 3. 启动图(Launch Screen) | 必须使用 Storyboard,不能用图片。t3code 模板已预置LaunchScreen.storyboard | ✅ 完全自动 |
| 4. 隐私描述字段 | Info.plist中必须声明所有用到的权限,如NSCameraUsageDescription、NSMicrophoneUsageDescription | CLI 会扫描nativeBridge调用,自动生成占位符 |
| 5. HTTPS 强制 | 所有fetch请求必须用https://,http://会被拒绝。t3code 的nativeBridge默认禁用http | ✅ 自动拦截 |
| 6. 模型服务本地化 | 如果调用 LM Studio,必须确保http://localhost:1234在 iOS 上可用——这不可能。正确做法是:在packages/web中,检测isIOS,改用nativeBridge.runModel()调用 Swift 层的本地推理(如 Core ML) | ❌ 需手动修改,CLI 不干涉业务逻辑 |
| 7. App Store Connect 元数据 | 应用名称、描述、截图、评级等,CLI 不处理,必须人工填写 | — |
最后一项“模型服务本地化”是最大坑点。很多团队在 Web 层写了fetch('http://localhost:1234/...'),测试时一切正常,但打包到 iOS 后,localhost指向设备自身,而 LM Studio 根本没在 iOS 上运行。t3code 的解决方案是:在packages/web/src/lib/api.ts中,根据环境变量切换 API 地址:
const API_BASE_URL = typeof window !== 'undefined' && /iPad|iPhone/.test(navigator.userAgent) ? '/native-api' // WKWebView 会拦截此路径,转给 Swift 处理 : 'http://localhost:1234/v1'; export const runModel = (prompt: string) => fetch(`${API_BASE_URL}/chat/completions`, { /* ... */ });然后在 Swift 的ViewController.swift中,添加 URLScheme 拦截:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } if url.path == "/native-api" { // 解析 query params,调用本地模型 handleNativeAPI(url) decisionHandler(.cancel) return } decisionHandler(.allow) }这样,同一套 Web 代码,在 macOS/Electron 上调用localhost:1234,在 iOS 上自动降级为 Swift 本地调用,彻底规避网络问题。
4.4 常见问题速查表与独家排坑技巧
| 问题现象 | 根本原因 | 解决方案 | t3code 版本修复状态 |
|---|---|---|---|
Electron 窗口白屏,控制台报Failed to load resource: net::ERR_FILE_NOT_FOUND | Next.js 构建产物中的_next/static/chunks/...路径未正确映射到file://协议 | 检查next.config.js中assetPrefix: ''是否生效;确认t3code build:electron是否生成了out/electron/www目录 | ✅ v0.8.3 已内置路径校验 |
| **iOS 模拟器中 WKWebView 加载空白 |