跳到主内容
创建时间:最后更新:作者:白川

Tauri + React 搭建 monorepo 项目

桌面应用既需要用户看到的页面,也需要承载页面的窗口。搭建 AI 工作台时,可以用 React 编写聊天、项目列表和设置等界面,再交给 Tauri 放进桌面窗口中运行。

随着功能增加,一些代码会被不同部分共同使用。例如,页面和本机服务需要使用相同的数据格式,适合放在共享代码包,用 pnpm workspace 将桌面应用与共享代码放在同一个仓库中管理,建立项目的基础结构。

为什么使用这些技术

React 适合把界面拆成可以组合的组件。 例如,聊天中的消息、输入框和项目列表可以分别编写,再组合成完整页面。同一种按钮或消息样式也可以重复使用,减少在多个地方维护相似界面的工作。React 还会根据数据变化更新页面,适合展示不断增加的聊天消息和任务状态。

Tauri 让使用网页技术编写的界面进入桌面应用。 它支持 Windows、macOS 和 Linux,并提供调用原生能力的入口。Tauri 使用操作系统提供的网页渲染组件,不必随应用携带完整的浏览器引擎,有利于控制分发体积。不同系统的开发依赖和构建仍需分别处理,本文先配置本机桌面开发。

页面开发还需要工具支持。这里使用 Vite:开发时提供本地页面服务,修改代码后通过热更新反馈到页面,减少反复手动重启的操作;构建时将前端代码处理为可分发的静态文件,供 Tauri 加载。React 负责页面内容,Vite 负责页面的开发和构建过程。

pnpm workspace 用来管理仓库中的多个代码包。 桌面应用和共享包各自声明依赖,再由工作区建立本地连接。修改共享代码后,应用可以直接使用它,无需先把这个内部包发布到 npm。

这种把多个相关项目放在同一个代码仓库中管理的方式,称为 monorepo。本次将桌面应用放在 apps/desktop,共享代码放在 packages/contracts,再让页面引用共享包中的内容。它们属于同一个工程,但各自保留明确的用途。

Corepack 用来选择和运行项目指定版本的包管理器。 pnpm 负责安装依赖和管理工作区,Corepack 则负责按项目要求调用哪一个版本的 pnpm。例如,项目声明使用 pnpm 10.26.0,执行 corepack pnpm install 时,Corepack 会使用这个版本;如果本机尚未缓存它,会先下载。这样切换项目时,不必反复手动安装不同版本的 pnpm。

1. 准备开发环境

本文使用 Node.js 22.20.0 或更新的 22.x 版本、pnpm 10.26.0,以及 Rust stable。先在终端检查:

bash
node --version
corepack --version
rustup --version
rustc --version
cargo --version

Node.js 用来运行前端工具,Rust 和 Cargo 用来编译桌面程序。尚未安装时,先安装 Node.js 22.x,再通过 Rust 的工具链管理器 rustup 安装 Rust 与 Cargo,完成后重新打开终端,运行上面的命令确认工具可用。

如果只有 corepack 不存在,可以安装它:

bash
npm install --global corepack

后面统一使用 corepack pnpm 调用 pnpm。Corepack 本身有独立的版本号,corepack --version 显示的是它自己的版本;在项目目录中执行 corepack pnpm --version,显示的才是当前项目使用的 pnpm 版本。

桌面开发还依赖操作系统提供的工具,按自己的系统准备即可:

系统需要准备什么
macOS
仅开发桌面应用时,可以安装 Xcode Command Line Tools
Windows
Microsoft C++ Build Tools,勾选“使用 C++ 的桌面开发”;确认 WebView2 可用
Linux
按发行版安装 WebKitGTK 及编译依赖

macOS 可以使用 xcode-select --install 安装命令行工具。Windows 安装 C++ Build Tools 时需要选中对应的桌面开发工作负载;Linux 的依赖包名称随发行版不同,应匹配自己的系统版本。系统依赖没有准备好时,网页可能能打开,桌面程序仍然无法编译。

2. 创建工作区根目录

在你存放练习项目的位置执行:

bash
mkdir aiwork
cd aiwork
mkdir apps packages

接下来用编辑器打开 aiwork 文件夹。下文提到的路径都相对于这个文件夹;新建文件时,如果父目录不存在,就先创建父目录。

在根目录新建 package.json,写入完整内容:

json
{
  "name": "aiwork",
  "private": true,
  "version": "0.1.0",
  "packageManager": "pnpm@10.26.0",
  "engines": {
    "node": ">=22.20.0"
  },
  "scripts": {
    "dev": "pnpm --filter @aiwork/desktop exec tauri dev",
    "dev:web": "pnpm --filter @aiwork/desktop run dev",
    "build:web": "pnpm --filter @aiwork/desktop run build",
    "typecheck": "pnpm --filter @aiwork/desktop exec tsc --noEmit",
    "build:desktop": "pnpm --filter @aiwork/desktop exec tauri build"
  }
}

packageManager 中的 pnpm@10.26.0 是给 Corepack 的版本声明。将这个文件随项目一起保存,其他人在项目目录中执行 corepack pnpm 时,也会按相同声明选择版本。pnpm-lock.yaml 则记录项目依赖的解析结果,两者分别固定包管理工具和依赖安装依据。

private: true 表示这个根包不准备发布到 npm。scripts 是从根目录操作桌面应用的快捷入口;桌面应用还没生成,这些命令稍后才能运行。

继续新建 pnpm-workspace.yaml:

yaml
packages:
  - "apps/*"
  - "packages/*"

它告诉 pnpm:去这两组目录里寻找包含 package.json 的包。仅仅创建文件夹,还不会产生应用或依赖关系。

再新建 rust-toolchain.toml:

toml
[toolchain]
channel = "stable"
profile = "minimal"

这样进入项目时会选择 stable 工具链。如果电脑上还没有安装它,执行:

bash
rustup toolchain install stable --profile minimal

根目录还需要一份 .gitignore,避免之后把依赖和构建产物加入版本管理:

text
node_modules/
dist/
target/
.env
.env.*
!.env.example
.DS_Store

pnpm-lock.yaml 和桌面应用生成的 Cargo.lock 应当保留,它们记录实际解析的依赖版本。

3. 在 apps 中生成桌面应用

现在让官方脚手架一次生成 React、TypeScript 和 Tauri 的基础文件。在根目录的终端执行:

bash
cd apps
corepack pnpm dlx create-tauri-app@4.7.5 desktop --template react-ts --manager pnpm --identifier cn.example.aiwork --tauri-version 2 --yes
cd ..

这里固定使用 create-tauri-app@4.7.5,减少脚手架升级造成的步骤差异。生成器版本与 Tauri 版本是两回事:--tauri-version 2 指定的是 Tauri 的主版本。--template react-ts 选择 React + TypeScript 模板,--manager pnpm 指定包管理器,--yes 使用默认选项并跳过交互提问。

desktop 是生成目录名,cn.example.aiwork 是练习用的应用标识;以后准备分发自己的应用时,应换成自己使用的唯一标识。不要在已有工作台仓库里重复执行生成命令,也不要用强制覆盖参数处理目录冲突。

生成完成后,打开 apps/desktop/package.json,只把 name 字段改成:

json
"name": "@aiwork/desktop"

保留其他字段。这一行是局部修改,不是整个文件的内容。根目录命令中的 --filter @aiwork/desktop,正是通过这个名字找到桌面应用。

此时的主要目录应当类似下面这样,辅助文件已省略:

生成后的最小工程

使用上下方向键浏览目录,左右方向键展开或收起,Enter 或空格切换展开状态。

  • aiworkAI 工作台的工程根目录
    • package.json项目信息、工具版本与统一命令
    • pnpm-workspace.yaml声明哪些目录属于工作区代码包
    • rust-toolchain.toml指定桌面程序使用的 Rust 工具链
    • apps存放可独立运行的应用
      • desktopAI 工作台桌面应用
        • package.json桌面应用的依赖与开发命令
        • index.html加载界面的页面入口
        • vite.config.ts界面开发服务与构建配置
        • src用户看到的页面与交互代码
          • main.tsx将应用界面挂载到页面
          • App.tsx组合应用的主要界面
        • src-tauri桌面窗口与原生能力相关代码
          • Cargo.toml桌面程序的包信息与 Rust 依赖
          • tauri.conf.json窗口、应用身份与打包配置
          • src桌面程序入口与原生功能实现
    • packages存放共享代码包,稍后添加 contracts

src 中放 React 页面,src-tauri 中放桌面程序的 Rust 工程和 Tauri 配置。此时不必修改 Rust 代码,模板已经提供了打开窗口的基本入口。

4. 对齐页面地址与桌面启动命令

桌面窗口需要知道去哪里加载开发页面。本文统一使用 http://127.0.0.1:17420,与参考项目的前端开发地址一致。

将 apps/desktop/vite.config.ts 替换为以下完整内容:

ts
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"

export default defineConfig({
  plugins: [react()],
  clearScreen: false,
  server: {
    host: "127.0.0.1",
    port: 17420,
    strictPort: true,
    watch: {
      ignored: ["**/src-tauri/**"]
    }
  }
})

这份配置面向本机桌面开发。strictPort: true 表示端口被占用时直接报错,避免 Vite 自动换到另一个端口、桌面窗口还在访问原地址。

然后打开 apps/desktop/src-tauri/tauri.conf.json,将其中的 build 对象替换为:

json
"build": {
  "beforeDevCommand": "pnpm dev",
  "devUrl": "http://127.0.0.1:17420",
  "beforeBuildCommand": "pnpm build",
  "frontendDist": "../dist"
}

保留文件中的其他配置。还可以把 productName 和 app.windows 中第一个窗口的 title 都改成 useAiWork,方便辨认应用。

这里有两组需要对应起来的关系:

  • 开发时,beforeDevCommand 启动桌面包中的 Vite,窗口访问 devUrl。
  • 构建时,beforeBuildCommand 生成前端文件,Tauri 从 frontendDist 读取它们。

../dist 相对于 tauri.conf.json 所在目录,指向 apps/desktop/dist,而不是根目录下的 dist。

IMPORTANT

根目录的 dev 启动 Tauri;桌面包中的 dev 启动 Vite。不要把桌面包的 dev 也改成 tauri dev,否则 Tauri 启动前执行这个命令时,会再次启动自己。

5. 创建共享包,让页面用上它

接下来验证 monorepo 中最重要的一件事:一个包声明依赖后,能够使用另一个本地包的代码。

新建 packages/contracts/package.json:

json
{
  "name": "@aiwork/contracts",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "exports": {
    ".": "./src/index.ts"
  }
}

再新建 packages/contracts/src/index.ts:

ts
export interface WorkbenchInfo {
  name: string
  stage: string
}

export const workbenchInfo: WorkbenchInfo = {
  name: "useAiWork",
  stage: "桌面工程已就绪"
}

这里用一个对象及其类型演示共享导出。后续应用和本机服务需要约定数据格式时,可以在这个包中维护实际共享的定义。

exports 声明包的入口。本次直接导出 TypeScript 源文件,由桌面应用的工具处理,因此共享包不需要先单独构建。这是当前练习采用的方式;以后让 Node.js 直接运行某个包时,还需要结合其运行环境确定输出方式。

回到 apps/desktop/package.json,在原有 dependencies 中增加这一项,保留已有依赖:

json
"@aiwork/contracts": "workspace:*"

workspace:* 明确要求使用当前工作区里的包。它不是从 npm 下载一个同名包;本地找不到对应名称时,安装会失败。

最后,将 apps/desktop/src/App.tsx 替换为:

tsx
import { workbenchInfo } from "@aiwork/contracts"

export default function App() {
  return (
    <main>
      <h1>{workbenchInfo.name}</h1>
      <p>{workbenchInfo.stage}</p>
    </main>
  )
}

这次没有使用 ../../packages/... 这样的相对路径,而是通过包名导入。以后移动应用内部的组件时,也不必重新计算到共享目录有几层距离。

6. 安装依赖,启动第一个窗口

确认终端已经回到 aiwork 根目录,再执行:

bash
corepack pnpm install
corepack pnpm run dev

依赖统一从根目录安装,pnpm 会处理工作区内各个包,并生成根目录的 pnpm-lock.yaml。之后重新安装已有工程时,可以使用 corepack pnpm install --frozen-lockfile,要求依赖声明与锁文件保持一致。

启动过程会先运行 Vite,再编译 Rust 桌面程序。第一次编译可能需要下载 Rust 依赖,终端暂时没有出现窗口时,先查看它是否仍在下载或编译。

预期结果是出现一个标题为 useAiWork 的桌面窗口,正文显示:

text
useAiWork
桌面工程已就绪

要检查共享包是否真的参与了运行,可以修改 packages/contracts/src/index.ts 中的 stage,保存后观察页面文字是否更新。这比只确认窗口能打开,多检查了一层本地包之间的联系。

结束开发时,在启动命令所在的终端按 Ctrl+C,并确认本次窗口和开发进程已经退出。

如果只想先看 React 页面,可以从根目录运行:

bash
corepack pnpm run dev:web

再用浏览器打开 http://127.0.0.1:17420。这一命令只启动网页开发服务;检查过网页以后,先停止它,再运行桌面开发命令,避免两个 Vite 进程争用同一个端口。

7. 检查工程是否具备继续开发的条件

关闭开发进程后,在根目录执行:

bash
corepack pnpm run typecheck
corepack pnpm run build:web

第一条检查桌面页面及其引用代码的 TypeScript 类型,第二条生成前端构建产物。成功后,应能在 apps/desktop/dist 中找到 index.html 和静态资源。这两项检查都不代表桌面程序已经编译成功。

环境准备齐全时,还可以执行:

bash
corepack pnpm run build:desktop

它会进入 Tauri 的本机构建与打包流程。结果受当前操作系统及其依赖影响,不能把某个平台的成功构建当成其他平台也已通过。签名、安装包分发和自动更新留到后续处理。

遇到问题时,先根据现象缩小范围:

现象优先检查什么
找不到 corepack、rustc 或 cargo
工具是否安装,重新打开终端后是否进入命令搜索路径
提示没有匹配 @aiwork/desktop 的项目
桌面包的 name 与工作区目录规则是否一致
找不到 @aiwork/contracts
共享包名称、桌面依赖声明是否正确,是否重新在根目录安装依赖
17420 端口被占用
是否还开着上一次的网页或桌面开发进程
Vite 已启动,但窗口加载失败
devUrl 与 Vite 的主机、端口是否完全一致
网页能打开,Rust 编译失败
查看 Cargo 的具体报错,核对工具链和操作系统依赖
前端构建成功,Tauri 找不到前端产物
frontendDist 是否指向 apps/desktop/dist

到这里,工程就有了三个可以分别核对的结果:React 页面能够构建,Tauri 能够加载页面,共享包能够被桌面应用引用。后续加入项目管理、聊天和本机服务时,可以在这份基础上逐项增加能力。

8. 约定各部分负责什么

把应用放进 apps、共享代码放进 packages,只是分好了目录。继续加功能时,还需要回答:一段代码放到哪里,它可以使用哪些其他代码,又由谁保存数据和执行操作。这些决定合起来,就是项目的架构约定。

先划清当前工程的边界

现在的工程只有一个 React 页面、Tauri 外壳和共享包,职责可以保持简单:

部分当前职责设计要点
apps/desktop/src
显示页面,接收用户操作
页面从数据生成界面,不在组件中混入工具链和系统操作
apps/desktop/src-tauri
创建窗口,配置桌面能力
原生能力通过明确入口提供,不向页面开放任意系统操作
packages/contracts
提供应用需要的共享定义
保持独立,不导入桌面组件或应用内部状态

本篇的依赖方向是 desktop → contracts,意思是桌面应用可以导入共享包。反过来,contracts 不应导入 desktop:一旦共享包需要知道某个页面的内部写法,页面调整就会牵动原本独立的包,其他应用也难以复用它。

workbenchInfo 是为了检查包连接而放入的教学数据。后续真正接入服务时,contracts 应围绕双方共同使用的数据格式组织,例如请求、响应和错误类型,不应逐渐变成存放项目配置、聊天状态和各种帮助函数的杂物箱。

共享类型不等于执行请求。 页面导入一个请求类型,只是让 TypeScript 检查数据结构;它不会因此向服务发送消息。以后还需要实际的通信代码,并在接收请求时检查数据。TypeScript 类型本身不能验证来自网络或磁盘的内容。

功能增加以后,再拆页面内部的职责

现在的 App.tsx 只有几行,保持原样即可。等加入项目选择、聊天和设置后,可以按下面的职责拆分。这是后续组织方式,不要求本次创建空目录:

目录适合放什么判断方法
src/app
应用启动、全局依赖组装和导航协调
是否需要把多个功能连在一起?
src/pages
页面入口,组合相应功能
是否主要负责决定这一页展示哪些功能?
src/features
项目、聊天、设置各自的组件与交互
是否只围绕一个业务功能工作?
src/lib
客户端通信和平台调用的封装
是否负责连接外部能力,而不是决定页面布局?

例如,聊天消息组件先放在聊天功能内;确实被多个功能共同使用的基础控件,再提取到公共位置。只因两个按钮长得相似,就提前建立庞大的共享组件包,反而可能让简单修改需要跨多个目录。

依赖也应沿职责组织:app 组装页面与功能,功能通过参数或明确的接口接收所需数据,不反过来导入 app 的内部实现。这样修改应用入口时,不必同时修改每个功能。

接入本机服务时,明确执行与数据归属

后续如果加入 apps/server,可以让它负责模型连接、项目配置以及文件操作,把 React 留在展示和交互一侧。Tauri 则继续负责窗口和必要的原生能力。这个划分是本工作台的设计选择,Tauri 本身并不要求所有项目都增加 Node 服务。

以“读取项目说明”为例,拟采用的流程是:页面提交项目标识和用户要求,本机服务找到该项目允许访问的目录,检查路径并读取文件,再把结果返回页面。页面传入的标识只能用于查找,不能直接变成拥有任意磁盘访问权的路径。

模型密钥也由执行请求的一侧读取和使用,不返回页面、不写入前端构建变量。页面只需要得到“是否已配置”和可以展示的错误信息。

这时可以形成 desktop → contracts、server → contracts 两条代码依赖。客户端和服务之间另通过接口通信,不能直接把服务端源码导入 React。共享包供双方理解数据,接口负责实际传递数据,两件事各有入口。

同样要明确每份状态由谁更新。例如,输入框里尚未发送的文字由客户端维护;将来的文件保存结果由实际执行保存的一方确认。页面应在收到成功结果后显示“已保存”,不能以按钮已经点击作为依据。目录分层和 AGENTS.md 可以帮助遵守这些规则,真正的访问限制仍要由执行代码检查。

9. 把约定留在项目中

只在聊天里说过的规则,不容易在下一次开发时完整保留。可以把规则分成两份文件:docs/architecture.md 解释当前设计及理由,根目录 AGENTS.md 说明人和 Codex 修改项目时应遵守什么。

架构文档记录已经作出的决定

在练习工程中新建 docs/architecture.md,写入下面这份起点:

md
# 工程架构约定

## 当前范围

当前工程包含 Tauri + React 桌面应用和 contracts 共享包。
共享包导出 TypeScript 源码,由桌面应用的工具处理。
本机服务、模型请求和文件工具尚未实现。

## 依赖与职责

- desktop 使用 contracts,contracts 不依赖任何应用。
- React 负责展示和交互,Tauri 负责窗口及明确接入的原生能力。
- contracts 保存实际共享的数据定义,不保存页面状态或执行文件操作。

## 后续增加服务时

本机服务负责配置、模型请求和文件操作,客户端通过接口访问。
服务与客户端可以共同依赖 contracts,不能相互导入内部实现。
新能力接入时,需要补充数据归属、失败反馈和访问范围。

## 为什么先保持简单

当前只验证页面、窗口和共享包之间的连接。
功能增加后再拆模块,不提前生成没有实现的服务或运行时包。

这里把“当前已有”和“后续打算”分开,避免下一次让 Codex 读文档时,把尚未存在的服务当成已经可以调用的能力。以后改变依赖方向或增加数据存储,也在这里写明变动理由,而不只更新目录树。

AGENTS.md 给出可以执行的修改规则

继续在练习工程根目录新建 AGENTS.md:

md
# 项目开发约定

## 开始修改前

- 阅读现有实现、package.json 和 docs/architecture.md,检查 Git 状态。
- 只修改当前任务需要的文件,保留其他未提交改动。
- 已有规则有冲突时先指出具体冲突,不自行重建整个工程。

## 代码组织

- 手写代码采用 TypeScript、两空格缩进、LF 换行。
- 新增文件使用英文 kebab-case;现有脚手架文件名不必为此改名。
- 功能代码先放在所属功能内,实际有跨功能使用时再提取共享代码。
- 应用可以依赖共享包,共享包不得反向依赖 apps。
- 不把服务实现、模型密钥或任意文件操作放进 React 页面。

## 依赖与验证

- 使用 corepack pnpm,从根目录安装依赖并维护同一份 pnpm 锁文件。
- 新依赖声明在实际使用它的包中;跨包引用通过包名和 workspace 协议。
- 修改后运行根目录 typecheck 和 build:web,按变更补充针对性验证。
- 窗口、系统能力或打包配置改变时,补充对应的桌面验证。
- 报告实际检查结果及未验证项;未经要求不提交、推送或发布。

规则应能对应具体行为。例如,“注意代码质量”很难检查;“共享包不得导入 apps”可以通过依赖声明和 import 直接核对。规范也不必罗列所有未来功能,新增一种实际能力后,再补充它需要遵守的规则。

两空格和换行约定还可以交给编辑器。新建根目录 .editorconfig:

ini
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true

支持 EditorConfig 的编辑器会读取这些设置。它不负责检查 React 用法和包依赖方向,也不替代格式化、类型检查或测试。本篇先记录最小约定;后续接入质量工具时,应让配置与这些规则一致。

10. 交给 Codex 搭建的提示词模板

也可以让 Codex 按照相同要求创建工程。在 Codex 中打开存放练习项目的父文件夹,再复制下面整段提示词。默认在当前目录下新建 aiwork;如果想换名字,发送前修改提示词中的目标目录即可。

提示词把前面的操作整理成明确的搭建要求,同时要求 Codex 实际检查结果。它生成的仍是这一篇的基础工程,聊天和文件处理功能后续再加入。

text
请直接在当前目录下创建 aiwork 文件夹,搭建一个 Tauri 2 + React + TypeScript 的 pnpm monorepo 工程。请完成文件创建、依赖安装和必要验证,不要只给方案或命令清单。

一、先检查工作位置和环境

1. 阅读适用的 AGENTS.md,检查当前目录、Git 状态和 aiwork 是否存在,保留已有文件和未提交修改。
2. 默认新建 aiwork。若该目录已存在且非空,先检查内容;可以继续完善同一练习工程,但不能强制覆盖或重新初始化无关项目。无法判断时再询问我。
3. 检查 Node.js、Corepack、Rust stable、Cargo 和当前系统的 Tauri 桌面依赖。Node.js 使用 22.20.0 或更新的兼容版本,pnpm 固定为 10.26.0。
4. 项目命令使用 corepack pnpm。缺少系统工具或需要管理员权限时,说明缺少什么以及安装办法,不要自行修改全局工具链或安装系统组件。

二、创建最小目录结构

aiwork/
  package.json
  pnpm-workspace.yaml
  rust-toolchain.toml
  .gitignore
  .editorconfig
  AGENTS.md
  docs/
    architecture.md
  apps/
    desktop/
      package.json
      vite.config.ts
      src/App.tsx
      src-tauri/tauri.conf.json
  packages/
    contracts/
      package.json
      src/index.ts

这只是关键文件清单,请保留脚手架生成的其他必要文件。

三、配置工作区和桌面应用

1. 根 package.json 设置 private: true、packageManager: pnpm@10.26.0,以及 engines.node: >=22.20.0。
2. pnpm-workspace.yaml 包含 apps/* 和 packages/*。
3. rust-toolchain.toml 选择 stable,profile 为 minimal。
4. 在 apps 目录中执行官方脚手架:
   corepack pnpm dlx create-tauri-app@4.7.5 desktop --template react-ts --manager pnpm --identifier cn.example.aiwork --tauri-version 2 --yes
5. 将桌面包名改为 @aiwork/desktop,保留生成的依赖和必要脚本。桌面包 dev 运行 vite,build 运行 TypeScript 检查和 vite build。
6. Vite 保留 React 插件,设置 clearScreen: false;开发服务 host 为 127.0.0.1,port 为 17420,strictPort 为 true,忽略监听 **/src-tauri/**。
7. Tauri 的 productName 和窗口 title 使用 useAiWork。build.devUrl 为 http://127.0.0.1:17420,frontendDist 为 ../dist;beforeDevCommand 运行桌面包的 pnpm dev,beforeBuildCommand 运行桌面包的 pnpm build。不要在这些钩子中调用根目录的桌面启动命令,避免递归启动。
8. .gitignore 忽略 node_modules、dist、target 和本地环境文件,保留 .env.example。保留生成的 pnpm-lock.yaml 和 Cargo.lock。

四、加入共享包,并让页面实际引用

1. packages/contracts 的包名为 @aiwork/contracts,设置 private: true、type: module,exports 的入口指向 ./src/index.ts。
2. 导出 WorkbenchInfo 类型,包含字符串字段 name 和 stage;再导出符合该类型的 workbenchInfo 对象,name 为 useAiWork,stage 为“桌面工程已就绪”。
3. 桌面包在 dependencies 中声明 @aiwork/contracts: workspace:*。
4. App.tsx 通过 @aiwork/contracts 包名导入 workbenchInfo,分别用标题和段落显示 name、stage,不在页面中重复写死这两段文字。
5. 共享包本次由 Vite 和 TypeScript 直接处理源文件,不增加单独的构建流程。

五、提供根目录命令

dev:pnpm --filter @aiwork/desktop exec tauri dev
dev:web:pnpm --filter @aiwork/desktop run dev
build:web:pnpm --filter @aiwork/desktop run build
typecheck:pnpm --filter @aiwork/desktop exec tsc --noEmit
build:desktop:pnpm --filter @aiwork/desktop exec tauri build

六、记录项目约定与架构规范

1. 创建 docs/architecture.md,分别写明当前实现、依赖方向、设计理由和后续计划。当前只有 desktop 和 contracts,本机服务尚未实现,不生成空的服务或运行时包。
2. 记录 desktop 可以依赖 contracts,而 contracts 不能依赖 apps;contracts 不放页面状态、组件、密钥或系统执行代码。当前 workbenchInfo 只是检查共享导出的教学示例。
3. 后续 React 功能增加时,app 负责组装,pages 作为薄入口,features 按功能组织,lib 封装客户端与平台调用。现在 App.tsx 仍简单,不为此创建空目录或多余转发层。
4. 后续接入服务时,页面通过接口提交项目标识和用户要求;执行端解析允许目录、校验请求、读取密钥和执行操作。共享类型不代替运行时校验。页面状态与执行结果分别明确归属,避免重复维护。
5. 创建根 AGENTS.md,包含修改前检查、保留其他改动、两空格与 LF、TypeScript、依赖声明位置、跨包边界、验证命令以及交付规则。已有适用规则需要保留,不复制父仓库无关业务约定。
6. 创建 .editorconfig:root = true;所有文件使用 UTF-8、空格缩进、两空格、LF,并保留文件末尾换行。不额外安装格式或架构检查工具;说明 EditorConfig 的作用范围。

七、执行验证并报告证据

1. 在 aiwork 根目录运行 corepack pnpm install,确认工作区依赖连接成功。
2. 运行 corepack pnpm run typecheck 和 corepack pnpm run build:web,修复本次搭建产生的问题;确认 apps/desktop/dist/index.html 已生成。
3. 环境支持时,运行 corepack pnpm run dev 检查开发启动。能查看窗口时,确认标题和页面文字;无法查看时,区分“进程已启动”和“窗口已验证”。缺少系统依赖时,保留已完成的工程,报告具体阻塞原因。
4. 验证结束后清理自己启动的开发进程,不停止其他项目的进程。不要求本次执行桌面打包。
5. 最后列出创建的结构、启动命令、实际执行的检查、失败项和未验证项,不把网页构建通过说成桌面验收通过。

范围仅限工程搭建和共享包示例。不要添加 AI 接口、聊天业务、本机服务、UI 组件库、签名或自动更新,也不要提交、推送或发布。

Codex 完成后,可以先看它报告的检查结果,再在新工程根目录执行 corepack pnpm run dev。如果它报告原生环境缺失,先处理对应依赖;只完成前端构建时,仍需补上桌面窗口的检查。