参与贡献
面向开发者。从开发者手册选择 API 接入、App 发布或项目贡献。
本页写给修改 OpenBot 本身的人。它介绍如何准备你的电脑,如何运行服务器和检查,以及如何在代码中找到方向。
你需要
devenv。devenv 为项目提供确切版本的工具:Node.js、pnpm、PostgreSQL 等。你还需要
Docker。开发数据库和开发用的对象存储在 Docker 中运行,构建和检查镜像也要用 Docker。devenv 提供 Docker 命令行工具,你的电脑只需要一个正在运行的 Docker 服务。请看部署。每条命令都通过
devenv shell -- <command> 运行,这样你用的总是这些工具。
git clone <repository-url> openbotcd openbot创建本地设置
Section titled “创建本地设置”devenv shell -- pnpm dev:setupdev:setup 创建 .env.development,并在 OPENBOT_SECRET_KEYS
中放入一个新的随机密钥。如果你的 .env.development
来自旧版本,请删除它,然后再次运行
dev:setup。你也可以重置开发数据库。请看配置。
启动开发服务器
Section titled “启动开发服务器”devenv shell -- pnpm devpnpm dev 运行 devenv up。它执行以下步骤:
- 在
127.0.0.1上启动 PostgreSQL 18,用户为openbot_dev。它在 Docker 中运行,使用和生产 Compose 文件相同的镜像。它的数据存放在 Docker 卷openbot-dev-postgres中。要重置数据库,先停止pnpm dev,再运行docker volume rm openbot-dev-postgres。 - 在
127.0.0.1上启动 RustFS,即文件的对象存储。它同样在 Docker 中运行。它的数据存放在 Docker 卷openbot-dev-rustfs中。要删除存储的文件,先停止pnpm dev,再运行docker volume rm openbot-dev-rustfs。 - 用
pnpm install --frozen-lockfile安装包。 - 构建 OpenBot 及其需要的包。
- 运行数据库迁移。
- 在
http://localhost启动开发服务器。服务器会自己为这个源写入平台设置,所以不需要设置步骤。
devenv 会为每个服务选择一个空闲端口。它从基础端口开始:PostgreSQL 是
5433,RustFS 是 9000,OpenBot 是
3000。它会在进程列表中显示这些端口。不管 OpenBot 得到哪个端口,devenv 的代理都在 80 端口上给它一个固定的地址
http://localhost。在 Linux 上,代理第一次会用 sudo
要求你输入密码,这样它才能监听 80 端口。
当你修改 packages/*/src 中的文件时,devenv 会重新启动 OpenBot。
开发服务器使用本地测试账户、测试模型和一个测试 App。不需要任何外部服务。如何登录并创建用户令牌,请看快速开始。
| 命令 | 作用 |
|---|---|
devenv shell -- pnpm install |
安装所有包。 |
devenv shell -- pnpm build |
用 tsdown 构建每个包。 |
devenv shell -- pnpm typecheck |
检查每个包的类型和 Effect 规则。 |
devenv shell -- pnpm rules |
只检查 scripts/effect-rules.ts 中的 Effect 规则。 |
devenv shell -- pnpm -r test |
运行每个包的测试。 |
devenv shell -- pnpm test |
在一次 Vitest 运行中运行所有包的测试,不构建,也不检查类型。 |
devenv shell -- pnpm --filter @openbot-build/core test |
运行一个包的测试。 |
devenv shell -- treefmt |
用 Prettier 和 nixfmt 格式化代码。 |
先运行 pnpm build,再运行 pnpm typecheck。包之间通过构建出的文件互相导入。
测试使用 Vitest 和
@effect/vitest。运行 Effect 的测试使用
it.effect。它给测试一个测试时钟:只有测试推动时,时间才会前进。需要真实时间的测试,例如带有真实服务器的测试,使用
it.live。OpenBot 的测试会用 devenv 中的工具启动自己的临时 PostgreSQL 服务器。它们不需要密钥,也不需要外部服务。GitHub
Actions 运行同样的 build、typecheck 和 test
命令。请看部署。
| 路径 | 内容 |
|---|---|
packages/ |
工作区中的包。请看包。 |
packages/docs/ |
本文档网站。 |
packages/template/ |
新包的起点。它不属于工作区。 |
protocol-test/ |
App Protocol 的公开测试服务。请看协议测试。 |
deploy/ |
部署 OpenBot 的文件。每个目标有自己的文件夹。请看部署。 |
deploy/docker/ |
Dockerfile、Bake 文件、Compose 文件、生产环境模板和镜像检查。 |
.env.development.example |
.env.development 的模板。 |
patches/ |
针对外部包特定版本的修复。 |
references/ |
其他开源项目的源代码,以 Git 子模块的形式存在。只供阅读。 |
devenv.nix |
开发工具、设置和进程。 |
references/ 文件夹不参与构建。要阅读其中的代码,请用
git submodule update --init 获取它。
要新建一个包,请把 packages/template/ 复制到
packages/<name>/。然后修改它的名称、入口和依赖。
文档网站是一个用 Starlight 构建的 Astro 网站。构建只生成静态文件。页眉中的 Logo 链接到 openbot.build。用以下命令启动它:
devenv shell -- pnpm --filter @openbot-build/docs dev然后打开 http://localhost:4321。英文手册从 / 开始,中文从 /zh-cn/
开始,德文从 /de/ 开始。
packages/docs/ 中的路径 |
是什么 |
|---|---|
content/docs/*.mdx |
页面。文件名就是 URL:deploy.mdx 对应 /deploy。 |
content/docs/<locale>/ |
翻译:zh-cn 是简体中文,de 是德语。 |
astro.config.ts |
网站设置,以及页面在侧边栏中的顺序。 |
src/components/ |
页面不用导入就能使用的组件:Callout、Steps 等。 |
src/i18n.ts |
文档支持的三种语言。 |
src/pages/openapi.json.ts |
在 /openapi.json 提供 openapi.json,方便大家下载。 |
scripts/api.ts |
根据 API 契约生成 openapi.json。 |
/api 上的 API 参考由
starlight-openapi 插件根据
openapi.json 生成。不要手写它。这个脚本在每次 dev 和 build
之前运行。当你修改 @openbot-build/api
中的 API 契约时,参考也会随之改变。当你在契约中添加一个分组时,请在
scripts/api.ts
中为它写一个标题和描述。在你写好之前,脚本会报错停止。构建还会生成 /llms.txt
和
/llms-full.txt,以纯文本形式为 AI 工具提供文档。要检查每个页面都能构建,请运行
devenv shell -- pnpm --filter @openbot-build/docs build。
每次推送到 main,都会把网站发布到
docs.openbot.build。请看发布文档网站。
面向一个读者编写
Section titled “面向一个读者编写”用户手册说明通过已有客户端或 Channel 完成工作的方法,写清任务、结果和求助方式。安装、API 字段和源码不放进用户页。
管理员手册说明配置和运维任务,明确需要组织权限、平台权限还是服务器访问权。客户端契约、App 发布和源码修改放在开发者手册。任务需要交给另一角色时,链接到该角色的说明。
代码和字段细节只保留一份。译文保持相同章节,并链接到自己的语言。自动生成的 API 参考保留英文。
Basic English
Section titled “Basic English”所有文字都用 Basic English(基本英语)书写。这包括文档、代码注释、错误消息、日志、提示词和测试文字。尽量使用 Basic English 的 850 个词,再加上需要的技术词。使用短句。每个技术词第一次出现时,都要解释它。
英文是所有文字的源头。文档还有简体中文和德语版本。修改英文页面时,请在同一次改动中修改它的翻译。翻译要保留相同的标题,顺序也相同,并且它的链接要指向同一语言的页面,例如
/de/deploy。API 参考只有英文版。
OpenBot 没有用户界面。不要向仓库添加页面、前端或客户端应用。文档网站是唯一的例外。每个新功能都从
@openbot-build/api
契约中的一个操作开始。然后由服务器处理它。每个操作都必须既能用用户令牌,也能用登录 cookie。它还必须用
Required 或 GroupRequired
注解说明自己需要对哪个资源的哪个操作。packages/api/src/Authorization.ts
中的辅助函数 own、organization、channel 和 channels
用来生成这些注解。一个要求只能使用它的端点有的路径参数。packages/api
中的一个测试会检查这一点。请看权限。
授权代码来自 ability 库,也就是 npm 包
@nmnmcc/ability。package.json
固定使用 3.1.0。HTTP 和存储边界使用 JSON 文本表示策略。这个版本在类型声明中直接导出错误类,让 TypeScript 无需本地补丁就能生成 OpenBot 的声明文件。
调用方与数据的变更
Section titled “调用方与数据的变更”变更需要处理旧调用方、客户端或已有数据时,先询问用户。不要自行决定采用破坏性变更还是保留旧行为。
不要修改基线迁移。每次数据库结构变更,都通过
devenv shell -- drizzle-kit generate --name <change>
添加新迁移,让已有数据库可以升级。
绕过外部包或工具的缺陷时,在相关代码旁用注释解释缺陷。
UUID 是一个 16 字节的 ID。当时间顺序有利于已存记录或索引时,使用 UUID
v7。用 Effect 的 Crypto.randomUUIDv7() 生成。如果下一步需要字节,使用 bytes
格式。随机密钥,以及无法从时间顺序获益的短期 ID,保留 UUID v4。
UUID v7 的开头是毫秒时间。同一毫秒内生成的 ID 顺序是随机的。需要精确顺序时,保留时间字段和序号。请看 RFC 9562。
数据允许时,以字节存储 UUID。PostgreSQL 使用原生
uuid 类型。它占 16 字节,同时接受 v4 和 v7。API 仍可使用 UUID 文本。不要把混合格式、外部 ID 或带前缀的 ID 直接转为
uuid。修改存储前,检查旧值、引用和加密数据中的用途。添加新迁移,并用旧 v4 值测试。
优先使用 Effect 数据
Section titled “优先使用 Effect 数据”使用 Effect 的不可变数据类型,例如 HashMap、Chunk 和
Option。只有真正需要时,才使用 JavaScript 的可变类型。
先写类型,再写执行工作的代码。服务契约说明输入、输出、错误和依赖。
- 用 Schema 定义领域值和外部数据。用
typeof Name.Type共享 Schema 的类型,不要在另一个类型中重复字段。 - 为每个
Context.Service写明确的接口或对象类型,放在make之前。不要用Effect.Success<typeof make>或ReturnType<typeof make>从实现反推服务类型。 - 用
Effect.Effect<Value, Error, Needs>或有类型的 Stream 声明方法。错误要有名字。泛型错误保留为E,不要改成any或unknown。在构建服务时取得依赖,调用方提供的上下文除外。 - 用
Service.of检查实现,再构建 Layer。局部辅助函数可以使用类型推导。共享类型可以引用另一个已声明的契约,但不能依赖构造函数推导出的结果。
service-contract 检查也会跟踪导入的类型别名,阻止从 make
推导服务类型。编译器还会拒绝错误和依赖通道中的 any 或
unknown、不安全的 Effect 类型断言、缺失的服务依赖,以及在允许边界之外提供服务。测试可以提供自己的 Layer。
Effect 规则
Section titled “Effect 规则”同一检查也验证模块依赖,包括类型导入。domain 可以使用
core,但不能使用 HTTP、SQL 或工作流适配器。core 和 protocol
不依赖产品包。openbot 中的功能模块不能导入 app
装配目录。各包通过公开入口互相使用。
业务代码和存储不能导入 HTTP 契约。这包括 PostgreSQL 适配器、设置、模型、账户、角色和 SSO。这些模块使用领域值和领域错误。API 使用同一份领域定义,再添加 HTTP 状态码。只有 HTTP 处理器、响应映射和应用装配可以导入 HTTP 契约。App 适配器在边界处加入协议数据。
业务代码也不能导入 HTTP 响应映射。共享错误映射使用领域错误。HTTP 处理器添加响应结构。
服务器通过共同执行引擎运行工作。检查禁止服务器使用本地
ChatRuntime、Operations.make 或
Operations.layer。只有执行引擎、资源传输和应用装配可以导入 Effect 集群或 RPC 模块。领域代码不能使用 core 的执行或存储适配器。检查也识别导入别名和包子路径。
OpenBot 用 Effect
编写。有两项检查让代码保持 Effect 的写法。pnpm typecheck
会运行这两项检查,GitHub Actions 会对每个改动运行 pnpm typecheck。
第一项检查是 Effect 语言服务。它是 TypeScript 编译器的插件,配置在
tsconfig.base.json 中。它的规则都是错误,所以 tsc
会在这些地方停下。这些规则要求的内容包括:
- 用 Effect 的
Array、String和Record模块代替原生方法,导入名分别是Arr、Str和Rec; - 时间用
Clock和DateTime,随机值和 ID 用Random和Crypto服务,环境变量用Config; - 用
HttpClient代替fetch,用FileSystem、Path和ChildProcess服务代替 Node.js 模块; - JSON 和外部数据用 Effect Schema,通过 Effect 解码,不用
*Sync函数; - 错误用
Schema.TaggedError,每个 Schema 类都带 brand; - 用
Effect.gen和Effect.fn代替async函数和new Promise; - 测试中用
@effect/vitest的it.effect、it.live和it.layer。
有三条规则是关闭的。apiStabilityLeak 和 unstableApiUsage
关闭,是因为 API 契约建立在 effect/http-api 上,而 Effect 把它标为不稳定。
missingPipeableSignature
关闭,是因为给每个导出函数加上可管道的形式只对库有用,对应用没有用。
第二项检查是
scripts/effect-rules.ts。它读取每个 TypeScript 文件,检查语言服务看不到的规则:
| 规则 | 它禁止什么 |
|---|---|
service-contract |
从构造函数反推的服务类型,包括通过导入的别名推导。先写服务契约。 |
blind-error-map |
Effect.mapError(() => ...)、Effect.orElseFail(() => ...) 和 catch: () => ...,它们会丢掉失败的原因。按名字传入的处理函数也算在内,只要同一文件把它声明为没有参数的函数。 |
throw |
throw。用有类型的错误失败;代码自身的规则被破坏时用 Effect.die;测试中用 assert.fail。 |
run-outside-main |
在程序入口(*Main.ts 文件)之外使用 Effect.runPromise 和其他 run 函数,包括 runForkWith。外部回调应使用受作用域管理的 FiberSet runtime。 |
fork-daemon |
Effect.forkDetach。应在 scope 中 fork fiber,或把它放进 FiberSet 或 FiberMap。 |
promise-then |
用 .then 串联 Promise。应把 Promise 转成 Effect。 |
directive-reason |
没有写原因的规则指令。 |
有些代码必须保持原生写法,因为外部 API 要求这样,或者 Effect 没有对应的 API。例如必须返回
Date
的 Drizzle 回调、SDK 的 Promise 回调,以及 HMAC 签名。这时只对一行关闭规则,并在
-- 后写明原因:
// @effect-diagnostics-next-line globalDate:off -- Drizzle calls this outside Effect and needs a Date.// @effect-rules-next-line throw -- The SDK reads a thrown error as a failed call.指令绝不是省事的办法。测试是程序入口,所以 test/ 文件夹中关闭了
strictEffectProvide。
小而真实的改动
Section titled “小而真实的改动”只构建能带来真正价值的东西。让每次改动保持专注,并测试那些真的可能出错的部分。
给贡献者的完整规则在仓库根目录的 AGENTS.md 中。
OpenBot 使用 GNU
Affero 通用公共许可证第 3 版,且仅限该版本(AGPL-3.0-only)。全文在仓库根目录的
LICENSE 文件中。当你提交改动时,即表示你同意它使用相同的许可证。
客户端包使用 MIT 协议。packages/client/LICENSE
也为客户端打包的 OpenBot 代码提供 MIT 授权。其他包仍使用 AGPL。客户端在
@openbot-build/client/Api 导出 API 定义,在 @openbot-build/client/Core
导出聊天数据结构。应用无需安装其他 OpenBot 包。
CI 先构建、检查类型并运行测试,再将 npm 包发布到 GitHub Packages。八个 OpenBot 发布包使用同一个版本号。客户端保留 MIT 授权,其他包使用 AGPL-3.0-only。
- 推送到
main时发布0.0.0-commit-<完整哈希>,标签为sha-<完整哈希>,不改动latest。 v0.1.0这样的标签发布0.1.0,并设置latest。- Pull request 只打包并检查,不发布。
运行 devenv shell -- pnpm packages:pack 在 .release 中生成本地包。设置
RELEASE_REF=v0.1.0
可检查正式版本。内部包依赖固定为同一个精确版本。打包结束后恢复源码清单,不改动工作区锁文件。发布重试会跳过内容相同的版本;同一版本的内容不同时会失败。
在 GitHub
Packages 中,将所需包的 Actions 读取权限授予使用这些包的仓库。工作流使用带
packages: read 的 GITHUB_TOKEN。如需独立凭证,将具有 read:packages
的 classic PAT 存为 PACKAGES_READ_TOKEN secret。发布使用源码仓库带
packages: write 的
GITHUB_TOKEN。本地安装公开包也需要令牌。参见客户端设置。