- 开发工具
- 接口测试
- 桌面应用
【免费下载链接】yaak
The most intuitive desktop API client. Organize and execute REST, GraphQL, WebSockets, Server Sent Events, and gRPC 🦬
导读
.claude/rules.md是 Yaak 桌面 API 客户端仓库(Rust 内核 + TypeScript 前端 + 插件生态的混合工程)专门面向 AI 辅助开发协作而维护的一份"契约式"规则文档。它浓缩了该仓库在多语言、多进程、多包(npm workspaces + Cargo crates)协作开发中最容易踩坑的六类硬约束:提交纪律、构建与静态检查、插件后端的UpdateSource语义、时间戳所有权、MCP Server 的上下文模型,以及 Rust 类型到 TypeScript bindings 的再生成流程。阅读本文后,你将掌握这些规则背后的源码级原理(对应UpdateSource::Plugin的调用链、upsert_date的时间戳策略、MCP 工具如何获取 workspace 上下文、ts-rs 如何驱动gen_*.ts生成),并能在开发或审查 AI 生成的代码时准确执行这些约束。
一、规则文档的定位:面向 AI 助手的协作契约
在 Yaak 仓库中,人类开发者与 AI 助手的协作规则被刻意拆成了三层,各司其职:
| 文件 | 定位 | 核心内容 |
|---|---|---|
| .claude/rules.md | 面向 AI 辅助开发的强制性执行规则 | 提交确认、lint/bootstrap 时机、插件后端约束、MCP 上下文限制、bindings 再生成 |
| AGENTS.md | 面向 Agent 的仓库级约束 | tag 安全(v*与yaak-api-*的区别)、未经明确批准不得 commit/push/tag |
| CONTRIBUTING.md | 面向社区贡献者的流程约定 | 仅接受 bug fix PR;非 bugfix 改动需先获得作者许可 |
三者共同构成"规则金字塔":CONTRIBUTING.md决定能否贡献,AGENTS.md约束 Git 操作安全,而.claude/rules.md则聚焦到每次修改后的技术动作。本文后续所有内容均以.claude/rules.md为主线展开,并用仓库源码逐一印证。
二、通用开发纪律与构建/静态检查流程
2.1 提交纪律:未经明确确认,绝不 commit / push
.claude/rules.md的第一条规则是NEVER commit or push without explicit confirmation,这与 AGENTS.md 中"未经明确批准不得 commit、push 或打 tag"的要求互为印证。之所以在 AI 协作语境下被反复强调,是因为 Yaak 的发布模型对 tag 极为敏感:应用与 CLI 共用v*tag(CLI 与应用版本锁定、随每次应用 tag 发布到 npm),而@yaakapp/api使用独立的yaak-api-*tag。AI 助手如果自主执行 Git 写操作,极易在错误的 tag 上引发发布事故。因此该规则要求:任何 Git 写操作都必须先获得人类开发者显式批准。
2.2 修改 TS/JS 后必须运行npm run lint
规则要求修改任何 TypeScript 或 JavaScript 文件后运行npm run lint。查看根目录 package.json 的 scripts 可以发现这条命令并非单一步骤,而是一个并行管道:
"lint": "run-p lint:*", "lint:vp": "vp lint", "lint:workspaces": "npm run --workspaces --if-present lint"即npm run lint同时执行两条链:
lint:vp:使用vp(Vite Plus 的命令行工具)对仓库根级配置与代码做静态检查;lint:workspaces:递归进入 package.jsonworkspaces字段声明的全部 workspace(包括packages/*、plugins/*、plugins-external/*、crates-tauri/*、crates/*、apps/*等近 60 个包),对各自声明了 lint 脚本的包逐一执行。
因此,AI 助手在修改任意一个插件(如plugins/template-function-json)或前端包(如apps/yaak-client)后,都必须通过这条命令确保整仓不引入 lint 错误。
2.3 修改插件运行时或 MCP Server 代码后必须运行npm run bootstrap
规则要求在修改plugin runtime 或 MCP server 代码后运行npm run bootstrap。bootstrap在 package.json 中定义为一组串行任务:
"bootstrap": "run-s bootstrap:*", "bootstrap:install-wasm-pack": "node scripts/install-wasm-pack.cjs", "bootstrap:build": "npm run build", "bootstrap:vendor": "npm run vendor"其链路为:安装wasm-pack(负责编译 Rust 到 WebAssembly)→ 构建所有 workspace → 执行 vendoring(vendor-plugins将插件打包为发布产物、vendor-protoc处理 gRPC 的 protoc 工具链,见 scripts/vendor-plugins.cjs 与 scripts/vendor-protoc.cjs)。
之所以这两类改动需要全量 bootstrap,是因为插件运行时依赖 Rust 编译出的 wasm 产物(如 crates/yaak-wasm 与 crates/yaak-templates 的pkg/目录),而 MCP Server 插件(plugins-external/mcp-server)运行时会通过 scripts/vendor-node.cjs 打入 Node 运行时。仅改前端文件时不需要走这条重链路,这也是规则把 bootstrap 单独列出的原因。
三、插件系统后端约束:UpdateSource::Plugin与时间戳所有权
这是.claude/rules.md中技术含量最高的部分,三条约束共同回答了同一个问题:插件进程写入数据库时,谁能决定哪些字段。
3.1 数据库写操作必须使用UpdateSource::Plugin
规则原文:Always useUpdateSource::Pluginwhen calling database methods from plugin events。
UpdateSource定义在 crates/yaak-models/src/util.rs,是一个带标签的枚举:
pub enum UpdateSource { Background, Import, Plugin, Sync, Window { label: String }, }它标明了每次数据变更的来源渠道。在 crates/yaak/src/plugin_events.rs 中,插件事件的UpsertModelRequest与DeleteModelRequest分派逻辑可以完整看到这一约束的落地:无论插件写入的是HttpRequest、GrpcRequest、WebsocketRequest、Folder、Environment还是Workspace,统一走with_tx(|tx| tx.upsert_xxx(m, &UpdateSource::Plugin));删除操作同理,例如delete_http_request_by_id(&req.id, &UpdateSource::Plugin)(crates/yaak/src/plugin_events.rs)。同文件的单元测试也严格遵循:种子数据用UpdateSource::Sync写入,而模拟插件写入时全部显式传入UpdateSource::Plugin,例如upsert_and_delete_model_are_shared_handled测试(crates/yaak/src/plugin_events.rs)。
为什么要区分来源:UpdateSource会直接影响后续的同步(sync)与冲突处理逻辑——来自Sync的写入需要走双向同步管道,来自Plugin的写入则需要被重新广播给同步层。如果 AI 助手在生成插件事件处理代码时漏掉UpdateSource::Plugin或错用UpdateSource::Sync,轻则导致本地变更无法正确上链同步,重则引发数据环回。
3.2 时间戳由 Rust 后端控制,TypeScript 永不发送
规则原文:Never send timestamps (createdAt,updatedAt) from TypeScript - Rust backend controls these。
这条规则的直接技术依据在 crates/common/yaak-database/src/traits.rs 的upsert_date函数:
pub fn upsert_date(update_source: &UpdateSource, dt: NaiveDateTime) -> SimpleExpr { match update_source { UpdateSource::Sync | UpdateSource::Import => { if dt.and_utc().timestamp() == 0 { Utc::now().naive_utc().into() } else { dt.into() } } _ => Utc::now().naive_utc().into(), } }其语义非常清晰:
- 对于
Sync/Import来源,保留传入的时间戳(仅在时间戳为 0、即未设置时回退为当前 UTC 时间),以保证跨设备同步时原始创建/修改时间不被覆盖; - 对于包括
Plugin在内的其他来源,一律由后端覆盖为当前 UTC 时间——插件传入的任何时间戳都会被丢弃。
因此从 TypeScript 侧发送createdAt/updatedAt不仅是多余的,还会造成误导:调用方以为时间戳由自己控制,实则后端_ => Utc::now().naive_utc().into()分支会直接忽略它们。正确做法是只携带业务字段(id、name、url等),让 Rust 后端统一生成时间戳。
3.3 后端使用NaiveDateTime(无时区),避免发送 ISO 时间戳字符串
规则原文:Backend usesNaiveDateTime(no timezone) so avoid sending ISO timestamp strings。
在 crates/yaak-models/src/models.rs 中,几乎所有模型的时间字段都声明为NaiveDateTime。以Settings为例:
#[ts(export, export_to = "gen_models.ts")] pub struct Settings { pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, ... }NaiveDateTime是 chrono 中不带时区信息的时间表示(仅含日期与时刻)。这意味着:
- 序列化到 TypeScript 侧时,时间字段的格式与带时区的 ISO 8601 字符串(如
2026-09-30T02:27:52Z或带+08:00偏移的形式)并不一致; - 如果插件或前端代码发送 ISO 字符串,后端在反序列化/存储时可能因格式不匹配产生解析偏差或失败;
- 仓库内所有时间均由
upsert_date统一以Utc::now().naive_utc()形式生成(见 3.2),天然是无时区的 UTC 时刻,前端如需展示本地时间再做转换即可。
实践结论:TypeScript/插件侧应当把时间字段视为"后端私有、只读"的字段——既不发送,也不假设其带时区语义;需要展示时,将其当作 UTC 时刻在 UI 层转换。
四、MCP Server 的上下文模型:没有"活动窗口"概念
规则原文:MCP server has no active window context - cannot callwindow.workspaceId()- Get workspace ID fromworkspaceCtx.yaak.workspace.list()instead。
4.1 为什么 MCP Server 没有活动窗口上下文
MCP Server 插件(plugins-external/mcp-server)是一个独立运行的后台服务。从其入口 plugins-external/mcp-server/src/index.ts 可以看到,插件在init后延迟 5 秒启动服务器,监听环境变量YAAK_PLUGIN_MCP_SERVER_PORT(默认端口64343):
const serverPort = parseInt(process.env.YAAK_PLUGIN_MCP_SERVER_PORT ?? "64343", 10);它通过 MCP(Model Context Protocol)标准协议与外部 AI 客户端通信,运行时并不依附于某个具体的桌面窗口。而window.workspaceId()这类 API 依赖"当前活动窗口"的 UI 状态——在无窗口的后台进程中,该调用无法可靠返回(可能为空或 undefined)。这就是规则禁止直接调用它的根本原因。
4.2 源码中的正确替代模式:显式解析 workspace 上下文
规则要求的替代路径在 MCP 工具实现中有完整的源码佐证:
- plugins-external/mcp-server/src/tools/workspace.ts 的
list_workspaces工具直接调用ctx.yaak.workspace.list(),返回当前打开的全部 workspace 及其 ID; - plugins-external/mcp-server/src/tools/helpers.ts 中的
getWorkspaceContext是核心辅助函数:先ctx.yaak.workspace.list()拿到 workspace 列表,若用户未指定且存在多个 workspace 则抛出带编号清单的错误提示,最后通过ctx.yaak.workspace.withContext(workspace)构造出带 workspace 上下文的引用:const workspaces = await ctx.yaak.workspace.list(); ... return { yaak: ctx.yaak.workspace.withContext(workspace) }; - 基于该上下文,plugins-external/mcp-server/src/tools/window.ts 的
get_workspace_id/get_environment_id工具才得以工作——它们在getWorkspaceContext返回的workspaceCtx上调用workspaceCtx.yaak.window.workspaceId()/workspaceCtx.yaak.window.environmentId()。
由此可以看到这条规则的精髓:MCP 场景下不要依赖隐式的"活动窗口",而要用 workspace 列表显式确定目标上下文(必要时让用户从列表中选择)。AI 助手在编写或审查 MCP 工具时,应优先复用getWorkspaceContext模式,而不是凭空假设存在活动窗口。
五、Rust 类型生成:cargo test驱动 TypeScript bindings 再生成
规则原文:Runcargo test --package yaak-plugins(and for other crates) to regenerate TypeScript bindings after modifying Rust event types。
5.1 机制:ts-rs 的测试期导出
Yaak 使用ts-rs库在 Rust 侧生成 TypeScript 类型声明。在 crates/yaak-plugins/src/events.rs 与 crates/yaak-plugins/src/api.rs 中,所有需要暴露给前端的 Rust 类型都标注了#[ts(export, export_to = "gen_events.ts")]、#[ts(export, export_to = "gen_api.ts")]等属性:
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[ts(export, export_to = "gen_events.ts")] pub struct InternalEvent { ... }ts_rs的export属性会在cargo test 执行期间触发文件写出,生成到 crates/yaak-plugins/bindings 目录下(gen_events.ts、gen_api.ts、gen_models.ts、gen_search.ts等)。随后 crates/yaak-plugins/index.ts 通过export * from "./bindings/gen_events"等语句把这些类型重新导出,供@yaakapp/api与前端消费。
5.2 为什么要用cargo test触发
这正是.claude/rules.md强调"修改 Rust 事件类型后必须运行cargo test --package yaak-plugins"的原因:只有运行测试,bindings 文件才会被重新生成。如果 AI 助手改动了crates/yaak-plugins中的事件结构(比如给某个InternalEventPayload变体增加字段),却忘记运行对应 crate 的测试,那么 crates/yaak-plugins/bindings 下的gen_*.ts仍是旧类型,TypeScript 侧会出现类型不匹配或缺失字段的编译错误,而且这类错误极难定位(错误发生在跨语言边界的另一端)。
同理,规则括号中的and for other crates说明:凡是用#[ts(export, ...)]声明了 bindings 的 crate(如 crates/yaak-models、crates/yaak-plugins 等,可在各 crate 的bindings/目录确认),改动其导出类型后都要跑对应 crate 的cargo test来刷新 bindings。作为快速自查手段,修改后应检查git status中bindings/gen_*.ts是否出现了预期的 diff。
六、实践落地:AI 协作修改的检查清单
综合.claude/rules.md全部条目,一份可直接执行的自检清单如下:
| 修改类型 | 必做动作 | 对应规则/源码 |
|---|---|---|
| 任意代码修改 | 不 commit、不 push,等待显式确认 | .claude/rules.md、AGENTS.md |
| 修改 TS/JS 文件 | npm run lint(含lint:vp+ 各 workspace lint) | package.json |
| 修改插件运行时 / MCP Server 代码 | npm run bootstrap(wasm-pack 安装 → 构建 → vendor) | package.json、scripts/install-wasm-pack.cjs |
| 编写插件事件中的数据库写入 | 一律传UpdateSource::Plugin,绝不使用其他来源 | crates/yaak/src/plugin_events.rs |
| 编写插件/前端数据写入 | 不发送createdAt/updatedAt,不发送 ISO 时间字符串 | crates/common/yaak-database/src/traits.rs |
| 编写 MCP 工具 | 不用window.workspaceId(),改用workspace.list()显式解析上下文 | plugins-external/mcp-server/src/tools/helpers.ts、plugins-external/mcp-server/src/tools/workspace.ts |
| 修改 Rust 事件/模型类型 | 运行cargo test --package <对应 crate>重新生成 bindings | crates/yaak-plugins/src/events.rs、crates/yaak-plugins/index.ts |
七、结语
.claude/rules.md虽然只有二十余行,却精准覆盖了 Yaak 这类"Rust 内核 + TypeScript 前端 + 插件生态"混合工程中 AI 协作的六大高风险区。它不是空泛的行为准则,而是可以从源码中逐条验证的工程约束:UpdateSource::Plugin在 crates/yaak/src/plugin_events.rs 中有完整的调用链与测试佐证;时间戳策略在 crates/common/yaak-database/src/traits.rs 的upsert_date中有明确的来源分支;MCP 上下文约束在 plugins-external/mcp-server/src/tools/helpers.ts 中有标准的替代实现;bindings 再生成则由 ts-rs 的测试期导出机制驱动。对于任何参与 Yaak 开发(尤其是通过 AI 助手提交代码)的工程师,把这七条规则内化为肌肉记忆,是避免跨语言、跨进程边界的隐性 bug 最有效的途径。
- 开发工具
- 接口测试
- 桌面应用
【免费下载链接】yaak
The most intuitive desktop API client. Organize and execute REST, GraphQL, WebSockets, Server Sent Events, and gRPC 🦬
相关推荐
深入解析 MoveFlow:Aptos 的 AI 辅助 Move 智能合约开发插件框架(MCP Server、插件生成器与编辑钩子)
深入解析 MoveFlow:Aptos 的 AI 辅助 Move 智能合约开发插件框架(MCP Server、插件生成器与编辑钩子) 导读 MoveFlow(
区块链Web3深入解析linshenkx/prompt-optimizer项目中的AI辅助开发最佳实践
深入解析linshenkx/prompt optimizer项目中的AI辅助开发最佳实践 项目概述 linshenkx/prompt optimizer项目提供
人工智能大模型提示工程AI 应用AI 评测pydantic-ai 编码规范深度解读:从代码风格到类型系统的源码级最佳实践
pydantic ai 编码规范深度解读:从代码风格到类型系统的源码级最佳实践 本指南基于 pydantic ai 仓库内部维护的 Coding Guideli
人工智能大模型AI Agent工具调用MCP Clients
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考