尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Sim 应用开发指南:Apps/Sim 的架构边界、API 契约体系与编码规范全解

Sim 应用开发指南:Apps/Sim 的架构边界、API 契约体系与编码规范全解 Sim 应用开发指南Apps/Sim 的架构边界、API 契约体系与编码规范全解【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000 builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim本篇技术指南以 Sim 仓库中apps/sim/AGENTS.md为核心骨架系统讲解 Sim 应用Next.jsUI API 路由 工作流编辑器的架构分层原则、应用操作边界Application Operation Boundary、以defineRouteContract为中心的 API 契约体系、声明式路由构建器、React Query 客户端边界、边界注解机制以及一整套可被 CI 强制执行的编码规范。读者读完将掌握如何在 Sim 中端到端新增一条受保护的 API 边界功能契约 → 应用用例 → 路由适配器 → 查询 Hook → 组件如何正确书写边界注解以通过审计脚本以及如何在 PR 评审中像审 DB 迁移一样审查 Schema 契约。说明本文所有结论均以当前仓库为证据来源涉及的源码、配置与脚本路径均可在仓库中直接打开核对。一、文档定位与适用范围apps/sim/AGENTS.md是 Sim 主应用Next.js 应用的开发者协作契约其规则叠加在仓库根目录的 AGENTS.md 之上仅约束apps/sim/目录下的文件。它面向两类读者人类开发者在 Sim 上新增功能、重构代码、评审 PR 时遵循的架构与编码标准AI 编码 AgentLLM文档刻意以规则化的条目撰写让 Agent 能低成本、确定性地遵循同样的架构约束避免生成能编译但结构松散的代码。文档末尾还嵌入了一段由next dev自动维护的nextjs-agent-rules区块见 AGENTS.md明确警告这个版本的 Next.js 与训练数据中的版本存在破坏性差异写代码前必须先阅读node_modules/next/dist/docs/中的对应指南该文件由next包的generate-agent-files.js生成在 monorepo 中next包可能不在仓库根目录可见需从 AGENTS.md 所在目录向上解析。这段区块由next dev自动写入/重写提交时保留它反而能让工作树保持干净。二、架构核心原则文档为apps/sim/的代码组织定义了四条核心原则单一职责Single Responsibility每个组件、Hook、Store 只承担一个清晰目的组合优于复杂Composition Over Complexity把复杂逻辑拆解为更小的单元类型安全优先Type Safety First所有 props、state、返回类型都必须定义 TypeScript 接口状态可预测Predictable State全局状态用 Zustand仅 UI 局部状态用useState。从仓库结构看这四条原则直接映射到apps/sim/的目录组织stores/集中存放 Zustand stores共 120 个文件hooks/下再细分为queries/React Query hooks与各类功能 Hookcomponents/按emcn/、ui/等子目录组织共享 UI。应用操作边界Application Operation Boundary这是整个文档中最关键的架构约束核心思想是把领域行为与传输层适配彻底分离受保护操作必须经由应用用例use case进入每一次受保护读、受保护写、规范资源查询canonical resource lookup或需要鉴权解析的引用都必须穿过一个有授权的应用用例。一个语义操作semantic operation只定义一次包含角色role、工作区键策略workspace-key policy、主体类型principal kinds与委托服务delegated services。表面适配器surface adapters只做薄的事认证并构建Principal、限流、解析、映射、呈现present。适配器禁止查询受保护数据、授权资源、实现业务事务或记录语义审计。应用用例承担重的职责规范化 scope、授权当前访问、执行 managers/repositories、投影语义审计、触发共享领域效果。应用代码保持表面中立surface-neutral永远不导入app/api/**、next/server、路由契约/presenters 或 Copilot handlers。Copilot 走同一扇门Copilot 通过createCopilotApplicationAdapter复用同一套应用用例不拥有独立的受保护业务逻辑复合的受保护变更必须对应一个顶层的语义应用操作。身份不可偷换绝不把计费归属billing attribution、上传者、创建者或 API-key 所有者当作实际执行主体acting principal当身份或策略无法表示时快速失败fail fast。新端点、工具命令和资源方法必须使用migrate-application-operation技能进行迁移。这套边界的底层实现证据分散在多个目录应用用例集中在apps/sim/lib/domain/application/表面适配器分布在apps/sim/app/api/**路由文件与apps/sim/hooks/**客户端代码中Principal类型与认证策略如internalSessionAuth则定义在 internal-json-route.ts 及sim/auth包中。仓库还通过scripts/check-actorless-executor-operations.ts等脚本从 CI 层面约束无 Actor 的 executor 操作。根级目录结构与包依赖方向apps/sim/AGENTS.md给出了整个 monorepo 的根级结构关键信息如下apps/ ├── sim/ # 本应用 (Next.js: UI API 路由 工作流编辑器) │ ├── app/ # Next.js App Router页面、API 路由 │ ├── blocks/ # Block 定义与注册表 │ ├── components/ # 共享 UIemcn/, ui/ │ ├── executor/ # 工作流执行引擎 │ ├── hooks/ # 共享 Hooksqueries/, selectors/ │ ├── lib/ # 应用级工具 │ ├── providers/ # LLM Provider 集成 │ ├── stores/ # Zustand stores │ ├── tools/ # 工具定义 │ └── triggers/ # 触发器定义 └── realtime/ # Bun Socket.IO 服务器协作画布 packages/ # sim/* — audit, auth, db, logger, realtime-protocol, # security, tsconfig, utils, platform-authz, # workflow-persistence, workflow-types依赖方向是单向的apps/* → packages/*packages 永远不能 importapps/*。特别是任何被apps/realtime消费的包都不得引入/lib/webhooks/providers/*、/executor/*、/blocks/*或/tools/*——这些重量级注册表只属于apps/sim。apps/realtime只能通过内部 HTTP 携带INTERNAL_API_SECRET回调进本应用。CI 通过两个脚本强制这些边界scripts/check-monorepo-boundaries.ts检查整个 monorepo 的包依赖边界scripts/check-realtime-prune-graph.ts确保 realtime 工作区不会意外引入被剪枝的重型依赖。功能组织Feature Organization业务功能统一放在app/workspace/[workspaceId]/下每个功能一个目录标准结构为feature/ ├── components/ # 功能组件 ├── hooks/ # 功能级 Hooks ├── utils/ # 功能级工具2 个及以上消费者 ├── feature.tsx # 主组件 └── page.tsx # Next.js 页面入口注意utils/目录的创建门槛只有当2 个及以上文件需要同一辅助函数时才建立utils.ts单一消费者直接内联。工具函数的落位优先级为lib/应用级→feature/utils/功能级→ 内联单次使用。命名约定对象约定示例组件PascalCaseWorkflowListHooksuse前缀useWorkflowOperations文件kebab-caseworkflow-list.tsxStoresstores/feature/store.ts—常量SCREAMING_SNAKE_CASE—接口PascalCase 后缀WorkflowListProps三、导入与类型规则apps/sim/AGENTS.md对导入与类型的使用有一组硬性约束一律使用/...绝对导入禁止相对导入Barrel 导出仅在文件夹有 3 个及以上导出时使用不得通过非 barrel 文件再导出类型专用导入必须用import type禁止使用any优先精确类型必要时用unknown配合类型守卫。四、组件与样式仅当需要使用 hooks 或浏览器专属 API 时才添加use client每个组件必须定义 props 接口适合的地方用as const提取常量使用 Tailwind 类与cn()做条件类名除非 CSS 变量是预期机制否则避免内联样式样式保持组件局部不得为功能开发修改全局样式。五、API 契约体系一次定义两端共用契约文件的位置与形式apps/sim/app/api/**下所有路由的 HTTP 请求/响应边界形状都集中在 apps/sim/lib/api/contracts/每个资源族一个文件大型领域使用子目录如knowledge/、selectors/、tools/。其约束是路由绝不定义路由局部的边界 Zod schema客户端绝不定义临时的 wire 类型两端消费同一个契约。每个契约用defineRouteContract构建签名如下见 types.tsdefineRouteContract({ method, // HttpMethod: GET | POST | PUT | PATCH | DELETE path, // 路由路径模板 params?, // 路径参数 schema query?, // 查询参数 schema body?, // 请求体 schema headers?, // 请求头 schema response: { mode: json, schema }, // 响应模式 schema error?, // 错误 schema })契约必须同时导出命名 schema与命名 TypeScript 类型别名例如export type CreateFolderBody z.inputtypeof createFolderBodySchema客户端 hooks 只能 import 这些命名别名绝不在 hooks 中写z.input.../z.output...。共享的标识符 schema如workspaceIdSchema、workflowIdSchema统一放在 apps/sim/lib/api/contracts/primitives.ts。从源码看defineRouteContract是一个恒等函数identity function它的价值在于类型推断它把泛型参数固定为ApiRouteContract...结构从而让ContractParams、ContractQuery、ContractBody、ContractJsonResponse等条件类型可以从契约实例反推出输入/输出类型——这正是客户端requestJson和路由构建器能做到全类型安全的基石。types.ts中的注释还揭示了一条重要规则/api/v2/契约必须显式声明query不声明意味着永不查看查询串?bogus1也会返回 200不接收任何查询参数的端点用noInputSchemaz.object({}).strict()显式表达该规则由contracts/v2/__tests__/query-declaration.test.ts中的 sweep 强制。边界注解Boundary Annotations合法例外的显式声明少量合理的边界例外可以带注解容忍审计脚本识别四种注解形式每种都必须紧贴在对应调用/转换的上一行reason 非空最多容忍上方 3 行非空注释注解适用位置允许场景// boundary-raw-fetch: reasonapps/sim/hooks/queries/**、hooks/selectors/**等客户端源码中指向同源/api/...的裸fetch(上方流式响应、二进制下载、multipart 上传、签名 URL 流程、OAuth 重定向、外部源请求// double-cast-allowed: reason测试文件之外的as unknown as X转换上方遗留 provider 类型缺少判别字段等// boundary-raw-json: reason路由 handler 中裸await request.json()/await req.json()含多行await request.clone().json()shim 变体上方JSON-RPC 信封、容忍的.catch(() ({}))解析、无法走parseRequest的情形// untyped-response: reason契约文件中schema: z.unknown()/z.object({}).passthrough()/z.record(...)响应声明上方响应体确实不透明用户提供的数据、第三方透传文档给出的三个真实示例流式 SSE 的 raw fetch、遗留 provider 的 double-cast、mothership 信封的 raw json与untyped-response的 firecrawl/v2/parse透传示例一起共同展示了例外必须显式化的设计哲学——让所有越过边界的行为在代码审查中可见、可审计。整文件级别的路由白名单合法的非边界或鉴权已处理路由通过 scripts/check-api-validation-contracts.ts 中的INDIRECT_ZOD_ROUTES配置而不是逐行注解。契约审计脚本bun run check:api-validation强制执行边界策略输出路由 Zod imports、路由局部 schema 构造器、路由ZodError引用、客户端 hook Zod imports 及相关计数器的 ratchet 指标。PR 必须通过。bun run check:api-validation:strict更严格的 CI 门禁额外对空 reason 的注解失败annotationsMissingReason。注意边界规则只约束 HTTP 边界。非边界的领域校验器——tools、blocks、triggers、connectors、realtime handlers 与内部 helpers——仍可直接使用 Zod。六、API 路由模式声明式构建器每个路由方法必须跑在withRouteHandler内普通内部与 v2 JSON/二进制路由使用defineInternalJsonRoute、defineV2JsonRoute或对应的 binary/stream 构建器——这些构建器内部已经应用了withRouteHandler绝不能再包裹一次。裸withRouteHandler只用于明确的协议或生命周期例外流式、multipart 控制、大体量请求准入、OAuth、公开执行。永远不要导出裸的async function GET/POST/...——要么导出共享构建器的结果要么对文档化的特殊路由导出withRouteHandler(...)。普通受保护 JSON 路由的标准形态文档给出了完整示例见 AGENTS.mdexport const PATCH defineInternalJsonRoute({ contract: renameWidgetContract, auth: internalSessionAuth, operation: widgetOperations.rename, rateLimit: internalRateLimits.none({ reason: Preserve existing internal behavior }), errorPolicy: internalWidgetErrorPolicy, mapInput: ({ params, body }) ({ widgetId: params.widgetId, assertedWorkspaceId: params.workspaceId, name: body.name, }), useCase: renameWidget, present: ({ widget }) ({ success: true, widget }), })各字段职责从源码 internal-json-route.ts 可逐一验证contract本路由对应的边界契约路由构建器用它驱动解析与响应验证auth认证策略如internalSessionAuth——基于 session 构建SessionPrincipal另有createInternalSessionOrExecutorAuth支持 executor 委托Bearer delegation token audience resourceScope 绑定operation语义操作标识与契约、用例在定义期保持一致rateLimit限流策略internalRateLimits.none({ reason })或基于用户/令牌桶enforceUserRateLimit、TokenBucketConfig的策略errorPolicy错误投影策略可用extendInternalErrorPolicy在基类之上扩展mapInput把解析后的{ params, body }映射为用例输入useCase应用用例负责规范加载、断言 scope、授权、业务行为、语义审计与共享领域效果present呈现器只返回表面成功体可用internalJsonPresenters.withSuccess/successFrom辅助。执行顺序是固定的认证与请求级限流发生在解析之前规范加载与授权发生在应用用例内解析、用例执行、响应验证、错误投影全部由声明式构建器接管。这正是适配器薄、用例重边界原则在路由层的落地。apps/sim/app/api/v1/**下的路由使用共享中间件 apps/sim/app/api/v1/middleware.ts 处理 auth、限流与工作区访问契约校验组合进该中间件绝不在每个路由里重复实现 auth/限流。特殊路由的规范解析辅助特殊路由裸withRouteHandler在认证与廉价准入之后用 apps/sim/lib/api/server 中的规范助手消费同一份契约并完成验证parseRequest(contract, request, context, options?)完整契约绑定路由一次调用解析 params、query、body、headers。无路由参数的路由传{}作为context有路由参数时传路由的context参数。返回判别联合检查parsed.success失败时返回parsed.responsevalidationErrorResponse(error)/getValidationErrorMessage(error, fallback)由ZodError生成 400 响应validationErrorResponseFromError(error)处理未知捕获错误可能是也可能不是ZodErrorisZodError(error)类型守卫。路由绝不使用instanceof z.ZodError。七、端到端新增一条边界功能五步标准流程apps/sim/AGENTS.md明确规定了新增路由 客户端表面的完成顺序每一步都有唯一归属地先写契约在apps/sim/lib/api/contracts/domain.ts大型领域用子目录为每个请求切片params、query、body、headers与响应各定义一个 schema再用defineRouteContract包裹导出命名类型别名输入用z.input输出用z.output。定义语义操作与应用用例在apps/sim/lib/domain/application/下实现。用例负责规范加载、断言 scope 检查、当前授权、业务行为、语义审计与共享领域效果。实现路由适配器在apps/sim/app/api/path/route.ts用合适的共享构建器声明 auth、operation、rate policy、error policy、input mapping、use case 与 presenter。auth 永远在解析之前仅对明确的特殊路由使用裸withRouteHandler且受保护工作保留在应用用例中。添加 React Query Hook在apps/sim/hooks/queries/domain.ts用requestJson(contract, input)发起调用构建层级化 query-key 工厂all→lists()→list(workspaceId)→details()→detail(id)使失效invalidation能精确命中前缀。在组件中使用 Hookmutation 的data与error完全由契约推导类型直接展示error.messagerequestJson已从响应体的error或message字段提取。客户端侧的契约消费requestJson所有同源 JSON 调用必须走 apps/sim/lib/api/client/request.ts 的requestJson(contract, ...)而非裸fetchHooks 从/lib/api/contracts/**import 命名类型别名不在 hooks 中写z.input/z.output不在客户端代码import { z } from zodrequestJson在发出请求时按契约解析 params、query、body、headers在返回时验证 JSON 响应hooks 始终转发signal支持取消裸fetch的文档化例外与边界注解一致流式响应、二进制下载、multipart 上传、签名 URL 流程、OAuth 重定向、外部源请求且每个裸fetch必须带 TSDoc 注释说明适用哪个例外。从源码看requestJson的输入类型ApiClientRequestC是精心设计的条件类型MaybeField用[Value] extends [undefined]的元组包裹形式避免判别联合被分发展开源码注释明确解释了这一点并在request.test.ts中有复现与论证未定义的切片自动变为可选never字段从而让调用方在类型层面就不可能传错切片。路径参数通过replacePathParams用正则\[\[?(\.\.\.)?([^\][])\]\]?处理含可选 catch-all[[...slug]]与编码这也是契约路径模板与 Next.js 动态路由语法对齐的原因。文档给出了完整可运行的 Hook 示例见 AGENTS.mdimport { keepPreviousData, useQuery } from tanstack/react-query import { requestJson } from /lib/api/client/request import { listEntitiesContract, type EntityList } from /lib/api/contracts/entities async function fetchEntities(workspaceId: string, signal?: AbortSignal): PromiseEntityList { const data await requestJson(listEntitiesContract, { query: { workspaceId }, signal, }) return data.entities } export function useEntityList(workspaceId?: string) { return useQuery({ queryKey: entityKeys.list(workspaceId), queryFn: ({ signal }) fetchEntities(workspaceId as string, signal), enabled: Boolean(workspaceId), staleTime: 60 * 1000, placeholderData: keepPreviousData, }) }其中entityKeys即第 4 步要求的分层 query-key 工厂产物keepPreviousDatastaleTime的组合保证列表翻页/切换工作区时界面不闪烁。八、Schema 审查清单像审 DB 迁移一样审契约 diff文档用一段非常醒目的标题强调LLM 写出的契约往往能编译但很潦草人工评审应把注意力集中在这些判断性问题上CI 的结构性检查抓不到它们requiredvsoptionalvsnullable必须正确optional()允许省略nullable()允许null两者链式叠加会产生三态几乎从不是你想要的响应 schema 必须匹配路由的真实 JSON 输出这是最常见的漂移 bug——路由输出了 schema 未声明的字段或漏掉了必填字段。逐个对照每个NextResponse.json(...)调用点与 schema错误消息要可读fileName cannot be empty远胜Required使用min(1, ...)、nonempty(...)的第二个参数跨字段 refine 用superRefine配合path和能指明失败字段的消息边界必须设置数组.min(1)/.max(N)字符串ID/名称.min(1).max(N)数字limit/size.min().max()z.unknown()是坏味道除非数据确实任意provider 透传、用户自定义工具结果、JSON-RPC 信封保留时必须带// untyped-response: 具体原因注解优先判别联合discriminated unions而非普通联合wire 上有判别字段时用判别联合让客户端获得穷尽式窄化。CIbun run check:api-validation:strict只捕获结构性违规路由中的 Zod imports、裸request.json()、双重转换、缺失注解不捕获上述 schema 质量判断——那是 PR review 中人的职责。九、测试与工具规则Testing使用 Vitest除非需要 DOM API优先vitest-environment node用vi.hoisted()vi.mock() 静态导入除真正的模块级单例外不用vi.resetModules()vi.doMock() 动态导入不使用vi.importActual()优先使用sim/testing提供的 mocks 与 factories。Utils 规则绝不为单一消费者创建utils.ts——直接内联当 2 个及以上文件需要同一辅助函数时才创建utils.ts复制前先检查既有实现lib/下已有大量工具落位顺序lib/应用级→feature/utils/功能级→ 内联单次使用。十、给仓库贡献者的快速自查清单结合全文为在apps/sim/下工作的开发者整理一份提交前自查新端点是否先写了契约且契约同时导出命名 schema 与命名类型别名受保护逻辑是否全部下沉到lib/domain/application/的应用用例适配器是否保持薄路由是否用共享构建器defineInternalJsonRoute/defineV2JsonRoute而非裸 handler是否重复包裹了withRouteHandler路由与客户端是否都没有import { z } from zod客户端是否只用命名别名 requestJson每个合法例外raw fetch、double cast、raw json、untyped response是否都带非空 reason 的注解且紧贴代码行bun run check:api-validation:strict是否通过是否按契约 → 用例 → 路由 → hook → 组件的顺序完成了端到端闭环且 query-key 工厂是分层的命名PascalCase 组件 / use 前缀 hook / kebab-case 文件 / SCREAMING_SNAKE 常量 / Props 后缀接口是否合规新增辅助函数是否先检查了lib/与feature/utils/且满足 2 消费者门槛测试是否遵循 Vitest sim/testing的约定遵循这套规范不仅能保证新代码与 Sim 现有的 1000 契约路由、553 个lib/api文件、141 个查询 hooks 保持一致的架构风格也能让人类评审与 AI 编码 Agent 在同一套语言体系下高效协作。更多细节可继续阅读仓库根目录的 AGENTS.md 与 CLAUDE.md。【免费下载链接】simSim is the collaborative workspace to build, deploy, and monitor AI agents and workflows. Used by 100,000 builders.项目地址: https://gitcode.com/GitHub_Trending/sim16/sim创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表