T3 Code 非官方中文翻译不是官方产品,也不是 T3 Tools 官方文档。源提交 d2c25950b4d9英文原文GitHub 源文件
Skip to content

T3 Code ​

T3 Code 是给编程智能体用的一个极简图形界面。一台 Node WebSocket 服务器把各个提供方(provider)的 CLI 和智能体包起来(Codex、Claude Code、Cursor、Grok、OpenCode、Antigravity、Kiro),并服务于 Web、桌面和移动客户端(client)。

可以把 T3 Code 看成一种开源的「自带订阅」替代品,用来替代 Claude Desktop、Codex App、Cursor Glass 和 Conductor 这一类应用。

T3 Code 特别在哪里? ​

喜爱 T3 Code 的用户已经超过 500,000。我们继续迭代这个产品的时候,保住他们所喜爱的那些东西,这一点很重要。下面简短列出让我们绝不能退让的几件事。

1. 内核开放 ​

T3 Code 是真正开放的。我们把路线图分享出来,把我们怎么看问题分享出来,当然,也把全部代码分享出来。我们的用户里,有相当多的人在跑自己的分叉版本。我们的工作是公开进行的,也应该努力保持这样。

2. 性能绝不妥协 ​

不少应用已经被糟糕的技术决策和「注水代码」拖住了。我们没有,而且我们为 T3 Code 的性能感到自豪。我们会定期审计性能是否回退。回退常常来自这些地方:经 WebSocket 送出了太多数据,CSS 动画把 GPU 顶出尖峰,列表变得难渲染,以及别的一些原因。每一项改动都要顾及它对性能的影响。

3. 远程就绪 ​

T3 Code 的 WebSocket 层架构(t3)使许多非常好用的远程功能成为可能。这些功能已经成了产品的核心。不论用户是在自己的本地网络上直接连接,还是使用 Tailscale,还是彻底靠上 T3 Connect(我们的隧道方案,同样在这个仓库里),我们都要确保新功能得到恰当的支持。

4. 多界面 ​

T3 Code 有 3 个关键的应用界面:Web、桌面和移动。

Web 差不多算两个界面,因为我们既有面向公众的 “app.t3.codes”,也有通过 t3 命令(command)在本地把 Web 应用托管起来的方式。在合理的范围内,所有新功能都要同时支持这两者。

桌面是大多数用户最先安装的那个主界面。它是一个完整的 Electron 应用,服务器运行器也一并打包在内。桌面应用还可以充当宿主服务器,让 app.t3.codes 或移动应用从远端连上来。

移动是一套同时覆盖 iOS 和 Android 的 React Native 应用,可以在 App Store 和 Google Play 上获得。移动应用能够连上任何一台 T3 Code 服务器,从而在远端控制工作。

Theo 的一则说明 ​

我喜欢有野心的想法、简单的系统,以及用起来让人觉得理所当然的软件。不要只是因为复杂已经在那里,就把它保留下来。不要因为一套机制看上去在架构上很了不起,就把它引进来。先把真正的约束弄明白,然后去争取那个最小的模型,让正确的行为变得毫不意外。

「量两次,再切一刀」和「YAGNI(你不会需要它)」这两条,都要贯彻。抵制范围一点点胀大。尽量用既小又现实的方式,去尊重开发者的意图。

这份文档剩下的部分,是为了帮你在代码库里找到路,并把改动做得到位。这些说明,与其当成「硬性规则」,不如当成「良好的默认做法」。开发者自己的偏好,应当能够压过这里的任何一条。

有一点要特别说清:T3 Code 的大多数贡献会来自 T3 Code 自己,而且常常是被人在远端操控着的。因此,访问数据、关掉开发服务器,以及其他可能把贡献者正在用的那个 T3 Code 实例弄坏的事情,你都要小心。

小型术语表 ​

我们需要在用词上站在同一边。沟通的时候,使用下面这套说法:

  • 你(you) 指正在读这份文件、并且在改 T3 Code 的智能体。
  • 我们、我们这边和维护者(we、us 和 maintainers) 指 Theo、Julius,以及正在构建 T3 Code 的那些人。你现在说话的对象就是他们。
  • 用户(user) 指那个用 T3 Code 来指挥编程智能体的人。
  • 智能体(agent) 指用户在 T3 Code 里面跑起来的编程智能体。看上下文,这里面也可能包括你。
  • 提供方(provider) 指 T3 Code 去与之对话的智能体运行时或驾驭层(agent harness),例如 Codex、Claude、Cursor 或 OpenCode。
  • 客户端(client) 指 Web、桌面或移动端的界面。
  • 环境(environment) 指一台正在运行的 T3 服务器,加上这台服务器所拥有的机器、文件系统、提供方凭据和状态。
  • 项目(project) 指一条属于某个环境、以一个目录为根的工作区记录。
  • 对话线程(thread) 指一个项目上持久保存下来的对话和工作历史。
  • 回合(turn) 指从用户到智能体的一轮循环,包括建立检查点(checkpoint)这样的后续工作。
  • T3 home 指基础数据目录。运行时状态通常放在它的 userdata 目录下面。

三种会让自己受伤的方式 ​

  1. 靠模式去杀进程。 绝不要对一个靠匹配名称、路径或工作树(worktree)字符串才找到的 PID,去执行 pkill -f、pgrep | kill 或 kill。你自己这个智能体进程的 argv 里,就带着这个工作树的路径,而这台机器上同时还有另外好几个开发服务器在跑。要杀,只杀你在进程拉起时亲手记下的那个 PID,或者只杀占用你端口的那个进程:它由 ss -H -ltnp 给出,并且要先确认 /proc/<pid>/cwd 就是你的工作树。
  2. 往正在使用的安装里写。 ~/.t3/userdata 是开发者真实的 T3 Code 数据库,你干活的时候它正被用着。读它,以及从里面复制,都没问题,这也是拿到真实测试数据的一个好办法(见「测试数据」)。绝不要对着它把服务器开起来,绝不要以可读写的方式打开它,绝不要把它清理掉。
  3. 把源地址烤进产物。 做开发时,绝不要去设置 VITE_HTTP_URL 或 VITE_WS_URL。开发是单源的,Vite 会代理 /api、/ws、/oauth 和 /.well-known。把它们设上,就会把 localhost 烤进打包结果里,然后悄无声息地弄坏每一个远程浏览器。

每个界面都要覆盖到 ​

本仓库里最常见的缺陷,是一处改动在你测过的那条路径上能工作,换到别处就没了。在把前端工作称为完成之前,把下面这份清单走一遍,并说明哪些条目用得上:

  • 入口。 从聊天视图能够到达的行为,通常从设置、命令面板和快捷键也能到达。只修好其中一处,并不等于把这个功能修好了。
  • 客户端(client)。 Web、桌面(在 Web 外包一层,并加上 Electron 外壳与 IPC)以及移动(React Native,导航是分开的)。共享逻辑位于 packages/client-runtime。
  • 提供方(provider)。 Codex、Claude、Cursor、Grok、OpenCode、Antigravity 和 Kiro 各自都有一个适配器(adapter)。凡是跟着提供方走的功能,每一个适配器都要有一个决定,哪怕这个决定是「这里不支持」。
  • 智能体。 用户能够触发的能力,通常也应该让智能体经由 MCP 工具触达,定时任务走的也是这些路径。这件事只有在它是一个服务方法、而不是写在处理函数里的代码时,才做得成。
  • 契约。 任何跨过线路传递的内容,类型都写在 packages/contracts 里。schema 一改,服务器、Web、移动和桌面都会跟着走。
  • 反向状态。 如果加了进去的路,就要加上出来的路,以及看见它的路。能搁置,就要能取消搁置。能关闭,就要能重新打开。只进不出的门就是缺陷。
  • 连接模式。 本地、远程/中继(relay)和隧道,行为各不相同。多设备、多环境的情况是真实存在的。
  • 文档。 检查这次改动有没有让已有的指导变得不准确。添加任何内容之前,先应用文档规则。

开发服务器 ​

  • vp i 负责安装。工作树(worktree)从 t3.json 的设置脚本得到这一步;如果模块解析看起来是坏的,多半是它没有跑。
  • 设置在后台运行。当 .t3/setup-running 里写着一个仍活着的 PID 时,等它结束,不要去跑 vp i。它收场时会留下 .t3/setup-done,或者留下 .t3/setup-failed(里面带着错误的尾部)。如果一个标记都没有,说明设置在这里根本没跑过,那就自己运行 vp i。
  • vp run dev 会把服务器和 Web 启动起来。在工作树里,状态默认落在这个工作树自己的、已被 git 忽略的 .t3 上。它被故意排在周围环境里的 T3CODE_HOME 前面,这样你就不会意外踩进共享状态。显式指定的 --home-dir 仍然更优先。
  • 端口由工作树路径推导而来,重启之后保持稳定;但已被占用的端口会让实际端口发生偏移,所以真实端口要从 [dev-runner] 那一行去读。
  • 通过 tailnet 共享,分三步:在后台运行 vp run dev --share,等它的输出里出现 pairingUrl: 那一行,然后把那个完整的 URL 交给一个尚未配对的浏览器。不要手工去接 tailscale serve,不要自己把这个 URL 打开,也不要去消耗用户的配对链接。带着可复用开发 cookie 的浏览器,可以使用不带额外路径的源地址。如果普通的一次性令牌已经被用掉,就用 node apps/server/src/bin.ts pair 重新签发一个。它带的是标准权限范围,而启动 URL 带的是「连接」设置所需要的管理员权限范围。
  • 想在各个工作树之间复用 Web 的开发认证,就配置一个固定的 T3CODE_DEV_AUTH_TOKEN,放在主检出里被 git 忽略的 .env 中。t3.json 的设置会把这个文件链接进各个工作树。绝不要提交或公开这个令牌,也不要提交或公开启动 URL。参见可复用的开发凭据。
  • 停掉你启动的东西,用的是你跟踪下来的那个 PID。见规则 1。
  • 需要第二份检出?用 git worktree add 把它放在这个工作树旁边,不要放进 /tmp。做完就把它删掉。

测试数据 ​

空数据库是糟糕的测试。用一份真实数据的副本,给你工作树的 .t3 做种子,不要直接指向正在使用中的状态:

  • 工作树里的第一次 vp run dev,会在数据库还不存在、且来源不超过 500 MiB 时,替你把这件事做完。来源再大,就会发出警告,开发服务器改为以空数据库启动。--no-seed 或 T3CODE_DEV_SEED=0 会跳过做种子。手动迁移仍然接受更大的来源,并且在裁剪之前,可能用掉相当多的磁盘空间。
  • 刷新时要先把开发服务器停掉,再运行 vp run migrate-dev-db。它会重建 <worktree>/.t3/userdata/statev2.sqlite,材料来自 ~/.t3/userdata/statev2.sqlite 的一份只读快照,那就是开发者的真实数据。它保留最近的项目(project),以及这些项目里已经停止的对话线程(thread),并丢掉定时任务、尚未完成的工作、认证会话,以及指向其他环境(environment)的链接,这样你的开发服务器就不会去跑开发者的智能体,也不会连到他们的其他机器。把 --projects 和 --threads-per-project 调高,就能得到更多数据。
  • 要刷新的是 statev2.sqlite,不是 state.sqlite。服务器会复制 V1 的 state.sqlite,但只有在 statev2.sqlite 缺失时才这样做。
  • 只有当前被测的流程需要它们时,才一并带上 secrets 和 settings.json。
  • 用复制放进去,绝不要用符号链接。数据只朝一个方向流动:流进你的沙箱,绝不流出去。

验证 ​

  • 推送之前,先运行 vp run check:changed。它会对已更改的文件跑 CI 的那套快速检查:格式、lint、knip、受影响的包以及依赖这些包的包的类型检查、紧挨着改动的测试,还有前面这些检查覆盖不到的跨文件检查。CI 仍然会跑完整的测试套件。

  • 你在迭代时,运行 vp test run <files>。遇到大文件,再加上 -t "<name>"。

  • 使用 vp run typecheck;它按包做缓存。每个包运行 effect-tsgo tsrs --noEmit,这是把 Effect 的诊断内建在内的 Rust 版 TypeScript 7 检查器。没有一个 typescript 包可以拿来运行。

  • node -e 片段要从 apps/server 或拥有它的那个包里运行。effect 不能从仓库根目录解析。

  • 要测试那些可能坏掉的行为,例如编排与事件溯源、提供方协议的转译、认证与权限守卫、持久化与迁移、外部格式的解析、竞态条件的修复,以及某次缺陷修复所改动的那部分逻辑。下列内容不要写测试:类型已经保证了的东西、琐碎的逻辑、文案、冒烟跑一遍、把模拟调用再镜像一遍、回调是怎么接上的,以及渲染成静态标记的 props。

  • 能扩展就先扩展,不要急着新增。已有测试或表格覆盖了这个行为时,就在那里加一行,或加一条断言。只有没有任何一个用例对得上时,才新写一个 it。

  • 只在一个层次上测试:在 main 下会失败的最低那一层。不要把同一个用例在包装测试里再写一遍,不要在每个调用点再写一遍,也不要在这份逻辑的 Web 副本和移动副本里再写一遍。

  • 测试 把同样的测试规则,再加上评审方面的指导,提供给评审机器人。改一边的时候,另一边一起改。

  • 不要跑覆盖整个仓库的检查。 不要跑 vp check,不要跑 vp run -r test,也不要跑 vp run -r typecheck,除非我提出要求。完整的那一套归 CI 管。

  • 后端行为有变化时,要连同针对该行为的聚焦测试一起交付。

  • 缺陷修复拿什么当证据,取决于这个缺陷是哪一种。逻辑、数据、状态和协议方面的缺陷,要先有一个失败的测试(在该模块的测试文件里,如果已有用例合适,就用那个用例;取那些会因为这个原因在 main 上失败的测试,数量要尽量少),然后再写修复。纯粹的视觉缺陷(间距、颜色、对齐、布局挪动)以修复前和修复后的截取画面为证据,不写测试:不要为吹毛求疵的小问题添加界面测试;截取画面已经能说明问题时,也不要在评审里再要求补一个测试。只改服务器、没有可见界面的修复,交付内容是测试,再加上一段简短的运行时记录。如果用户能在 Web 客户端里复现这个缺陷,vp run scenarios:prove 要在真实应用中展示:它在基线上失败,在修复上通过;参见场景。

  • 服务器是事件溯源的,副作用在命令提交之后才运行。在测试中,把效应工作器排空(OrchestrationEffectWorkerV2.drain),或者等待那个标志着里程碑的、已经持久化的特定事件,或等待 Deferred。绝不要靠休眠或轮询来等。一个测试如果必须靠超时才能通过,那它就是错的。具体来说:

    • it.effect 跑在 TestClock 上;Effect.sleep 只能经由 TestClock.adjust 往前走。it.live 表示的是墙上时钟,只用于子进程或网络的测试夹具。
    • 不要写 yieldNow 或 sleep 的轮询循环。去等待 Deferred,从 Queue 里取,或者把工作器排空。
    • 不要给单个测试传超时参数,也不要使用 retry。项目设定的超时是上限。不稳定的测试要修掉,而不是重试。
    • 不要用 performance.now() 或 Date.now() 去比时间差。断言应落在事件和计数上,不要落在经过了多久上。
    • 浏览器测试通过 expect.element 或 vi.waitFor 来断言,绝不要通过 requestAnimationFrame 或 transitionend。
    • vi.mock 的工厂不得展开一个真实模块,如果那个模块会把 Shiki 或 diff worker 拉进来。
    • it.skip 以及和它类似的写法,上一行需要有一个跟踪用的 issue:// quarantine: #1234 <why>。
    • Lint 会强制执行这些:t3code/no-test-timeout-or-unreasoned-skip、t3code/no-frame-waits-in-browser-tests、t3code/no-wall-clock-in-tests、t3code/no-real-time-wait-in-tests、t3code/no-polling-loop-in-tests。
  • Web 端的浏览器测试是 *.browser.test.tsx 或 *.browser.test.ts,渲染时使用 await using screen = await render(...),它来自 apps/web/test/render.tsx。t3code/require-disposable-render 遇到旧的写法会报错。

    • 凡是测试在挂载完成之后才去驱动的东西(它会往前拨的假定时器、它在挂载之后才装上的 spy),都要在渲染之后再去取得,这样这些东西会在卸载之前被释放掉。第一次渲染就要读的任何东西,都放在这次渲染之前。
    • 如果一个辅助函数先渲染,然后再等待或点击,它会把画面放进一个 AsyncDisposableStack,并且只在成功时才对它做 move(),因此即使等待失败,卸载仍然会发生。
    • rerender 每一次调用都需要一个新的元素。
    • 不要用生命周期钩子,也不要有模块一级的可变状态。vi.mock 工厂会去读的那些共享模拟状态,要在测试的最上方用一次 setup() 调用重置。
    • router 默认是打开的。只有同时用一行注释说明原因,才可以传入 router: false。
    tsx
    it("saves on Enter", async () => {
      const onSave = vi.fn();
      using _now = vi.spyOn(Date, "now").mockReturnValue(0);
      await using screen = await render(<TitleEditor onSave={onSave} />, { router: "/threads/1" });
      await userEvent.type(screen.getByRole("textbox"), "Title{Enter}");
      expect(onSave).toHaveBeenCalledWith("Title");
    });
  • 用户看得到的前端改动,应当在真实客户端里完整走一遍:Web 用 test-t3-app,移动端用 test-t3-mobile。这件事由主智能体在集成完成之后做一次。子智能体不要自己去启动开发服务器。

  • 真实的智能体回合是要花钱的。如果想看着一个回合流过整条流水线和界面,就用免费的 Replay (dev) 提供方,它是 vp run dev 注册的;如何启动一个,test-t3-app 里有说明。只有当这次改动确实依赖某个提供方自己的行为时,才去用真实的提供方,并且选它最便宜的模型,比如 Claude 用 Haiku,Codex 用 Luna。

做移动端验证时,原生客户端缺失或版本过旧,只是一个安装步骤,并不是阻碍。在启动 Metro 之前,于设备宿主上运行 vp run mobile:install <ios|android> [device-id];当 id 被省略时,它会以那台唯一已启动的模拟器,或那台已连接的 Android 设备为目标。它会在几秒内从共享缓存装上与这次检出的原生指纹对应的开发客户端,并且只有在还没有任何一台机器构建过它的时候才会去构建,所以优先用它,而不是 expo run:*、xcodebuild 或 gradlew,后面这三个会从头重新构建。它的标准输出是单独的一行 JSON;构建过程的输出写进它所指出的那份日志。完整的工作流程见 test-t3-mobile。

评测 ​

evals/ 用来检查真实模型在读到 T3 Code 面向模型的文本时会怎么做:智能体指令、MCP 工具的描述和 schema,以及生成出来的标题。改这些文本之前,先读 evals/README.md,并运行其中点到名的那些评测。如果你修的是模型行为上的缺陷,就加一个在修复之前会失败的用例。

拉取请求 ​

  • 除非开发者明确要求,否则绝不要创建拉取请求(pull request,PR)。
  • 如果 origin 是 git.cafe,就遵循GitCafe 镜像:推送到 origin,用 cafe 以及它们的 git.cafe URL 来打开并跟踪 PR,合并则继续使用 gh。
  • 提交标题用约定式提交,语言平实:fix(web): new threads no longer spike CPU。
  • 正文先用一两句话写出问题,再写出你是怎么修的。结尾注明完成这项工作的模型与驾驭层(agent harness)。
  • 界面上的改动需要修复前和修复后的图片。涉及运动或时序的,需要一段短视频。
  • 把 PR 的证据上传到 GitHub。绝不要把只为 PR 准备的截图或资源提交进仓库,例如 .github/pr-assets/。
  • 一个请求对应一个 PR。只有维护者要求拆开时才拆。外部贡献遵循更严格的一个 PR 只解决一个问题规则。
  • 值守的时候:调用 watch_pull_request,然后结束你这个回合(turn)。检查结果、评论或冲突一出现,T3 Code 就会把你唤醒。不要去轮询,也不要去休眠等待。机器人给出的每一条发现,都要对照源码核实;真实的问题就修掉,误报则写明理由后驳回。没有新情况时保持安静。当这些机器人在最新的那次提交上都变成绿色,就可以停下来。

文档 ​

大多数代码改动,并不需要跟着改内部文档。智能体自己读得了代码。

  • docs/internals/ 用来写架构上的决定以及这么决定的理由、横跨多个组件的约束,还有那些很难直接从源码里看出来的实现陷阱。加一段文字之前先问:若是没有这段,维护者会在什么地方搞错。如果把相关代码读一遍就能回答,那就别写。
  • 不要把每个功能都写成文档,不要把字段或方法逐个列出来,不要把控制流叙述一遍,不要维护一份文件目录,也不要在后面追加 PR 的摘要。类型、测试和代码已经把实现记下来了。术语表定义的是大家共用的词汇;它不是一份功能索引。
  • 如果有一个局部的理由是代码本身展示不出来的,就在旁边写一条短的代码注释。当这番理由跨越了边界,或者它需要的上下文是代码难以好好装下的,这时再用内部文档。链到相关的源码,不要把源码复制进来。
  • 已经写进文档的决定或约束变了,就把受影响的文字重写或删掉。不要另外再附一篇,把新行为再讲一遍。新开一个内部页面,得有一个与众不同、而且站得住的理由。
  • docs/user/ 是帮助用户把事情做成的。每个主要功能给一节简明的说明,讲它做什么、怎样开始,以及哪里不那么直观。写明设置项在哪一条路径上是有用的;去描述看得见的按钮、图标、布局、动画,或者每一种界面状态,则没有用。加文字之前先问:它帮助用户完成的是哪件任务,或者帮助用户做的是哪个决定。
  • 用户文档保持和已经发布的产品一致的口气,不要写实现细节,也不要写贡献者才用的工具。使用方式变了,就更新对应的那一节功能说明。界面上微调一下,不需要为此加一条文档;新加一个控件,也不需要单独给它一页。
  • docs/operations/ 放的是维护者的搭建、发布和调试步骤。至于怎样操作一台已经装好的 T3 Code 服务器,这类说明留在用户指南里。

计划与工作产物 ​

  • 不要把实现计划、研究笔记或智能体的草稿纸提交进去。临时用的工作材料放在工作树(worktree)外面。.plans/ 之所以被 git 忽略,只是给遗留的工具留一张安全网。
  • 维护者手头正在做的工作,放在拥有这项工作的 GitHub issue 或项目条目里跟踪。外部来的提案遵循 CONTRIBUTING.md,并且应该出现在 Ideas 讨论里。
  • 合并之后的 PR 就是这份实现的记录。工作落地时,把它的跟踪条目关掉或更新掉;不要在仓库里再保存第二份核对清单。

工作原理 ​

客户端发送带有类型的 WebSocket 请求。服务器把这些请求变成_命令(command)。编排器(orchestrator)(apps/server/src/orchestration-v2/Orchestrator.ts)把命令排成串行,并决定_事件(event),这中间不做任何 I/O。_事件汇(event sink)在同一个事务里提交这些事件、界面要读的_投影、命令回执,以及_发件箱(outbox)效应。随后_效应工作器_去运行这些效应,例如开始一次提供方的回合,或捕获一个_检查点(checkpoint),再把结果以命令的形式送回来。提供方的 CLI 作为子进程运行;按提供方分开的_适配器(adapter)_把它们各自的原生协议翻译成编排用的事件。每一个回合都以一个_检查点_收尾,那是一个隐藏的 git 引用,这样应用才能做差异比较,也才能恢复。

架构及其约束:docs/internals/overview.md。术语表:docs/internals/glossary.md

代码放在哪里 ​

  • apps/server - WebSocket、编排、提供方与检查点(checkpoint)。这里重度使用 Effect:添加服务器代码之前,先读 Effect 服务,Effect 这个库本身则看 .repos/effect/LLMS.md。
  • apps/web - React/Vite 的界面。apps/desktop 在外面包一层,apps/mobile 是 React Native,apps/marketing 是站点。
  • packages/contracts - Effect/Schema 的契约,加上一些小的派生辅助函数。这里不含重的运行时逻辑。
  • packages/shared - 共享的运行时工具函数,按子路径导出,没有桶文件(barrel)。
  • packages/client-runtime - Web 和移动端共用的客户端代码。
  • .repos/ - 以只读形式放进来的参考代码。优先沿用它们已有的模式,不要自己另发明一套。绝不要去编辑,也绝不要从中导入。提升与之对应的依赖版本时,用 vpr sync:repos 来同步。如果它不在(稀疏检出),就用 git show origin/main:.repos/<path> 去读文件。

品味 ​

  • 复杂性应当留在适配器(adapter)的边界上。编排保持纯粹,界面保持单纯。
  • 客户端一侧的修改走 createEnvironmentRpcCommand。把受保护的方法添加到契约中的 CLIENT_GUARDED_RPC_SCOPES,并用这条命令的 permissionAtom 来决定界面上是否可用。授予的权限在执行时对照目标环境做检查;说了算的仍然是服务器。原始的 RPC 客户端留在 rpc/ 里面,每增加一个受保护方法,就把权限行为的测试一起扩展。
  • 服务器上的功能做成服务;各种传输保持薄。不论是 ws.ts 的 RPC 处理函数、一条 HTTP 路由,还是一个 MCP 工具,都只做这些:解码输入,调用一个服务方法,再把错误映射出去。参见 Effect 服务。
  • apps/web/src/components/ui 导出的东西,外观由它们自己负责。挑选一个 variant,或者挑选一个 size;不要用 className 把其中一个重新打扮一遍。如果没有一个合适,而这副外观表达的是通用概念,就在组件上加一个变体;只属于某一个功能的外观,留在这个功能自己的组件中,不要放进 components/ui。布局用的类(宽度、flex、外边距、定位)写在父级上。一旦违反,shadcn/no-restyle 会让 lint 失败。参见 Web 界面。
  • 可复用的 Effect 函数使用 Effect.fn("Service.operation"),并带上一个稳定的 span 名称,而不是用一个函数把 Effect.gen 包起来。Effect.gen 留给内联的程序,以及层的构建。 只有在 span 会密得过分的时候(流里的一个个分块、逐个事件的投影、很紧的循环),或者它会和一条已经存在的追踪边界重复时,才使用 Effect.fnUntraced;这个选择要在近处解释清楚。 带名字的 Effect.fn 会创建一个 span;不带名字的那种形式则不会。已有的 span 名称和选项予以保留,组合子作为额外的函数构建器参数传进去。
  • 优先用推断出来的类型,而不是类型标注。any 是敌人。
  • 注释默认不写。只在需要指出一项约束、一处来自外部的怪癖,或一条代码自己表现不出来的不变量时才写;能写成一行就写成一行,最多四行。不要把名称或签名再讲一遍,不要把步骤叙述出来,不要记录来龙去脉,也不要解释你刚刚做出的那个修复;这些东西由 PR 带着。t3code/no-low-value-comments 负责强制执行长度上限,以及那些可以机械判定的情况。
  • React 的 effect 是为了和 React 之外的某样东西同步。能在渲染时推导出来的就在渲染时推导,该在处理函数里做的动作就在处理函数里做,而不是把状态同步来同步去;可是也不要为了躲开某一个 effect,就把真正属于生命周期的工作藏到 ref 或 store 里面。t3code/no-unreasoned-state-sync 这条 lint 规则会把每一个 effect 都报出来;留下来的那一个,要用 // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- <why> 写明它的理由。
  • 我们的用户整天都在驱使智能体工作,少掉的一帧、撒谎的加载转圈、过期的标签,他们都会注意到。不要使用持续重绘的动画;在高刷新率的显示器上,它们会把 GPU 打满。
  • 如果这里的某一条规则,和你眼前的任务打架,就明确说出来,并且在打破这条规则之前,先得到一个人的签字认可。

补充提示 ​

  • 安全很重要,但不应被放到过高的位置上,对开发模式和只给维护者用的功能来说更是这样。

非官方译文 · 源提交 d2c25950b4d9 · 不代表 T3 Tools