
【免费下载链接】OpenBotOpen-source AI coworkers that each get a computer of their own: a browser, files and tools, with every action decided before it happens and recorded after. Bring any AG-UI agent.项目地址https://gitcode.com/GitHub_Trending/openb/OpenBot点击查看免费下载一个租户包里要新增一位 AI 同事coworker过去只能去改agents.yaml这个所有人都在编辑的文件要把某位同事交给别人也只能从中间撕一段 YAML 出来。OpenBot 的 fintech 示例包给出的解法是在agents.yaml旁边放一个agents/目录一个文件声明一位同事加载器把两处一起读。这既解决了“多人改同一个文件”的冲突也让一位同事变成可复制、可删除、可转交的独立单元。读完本文你将掌握agents/目录的加载规则、文件格式与全部校验行为能据此从catalog/里挑选同事落地到自己的部署并清楚知道删除文件之后会发生什么。这个目录要解决的具体问题examples/fintech/agents/README.md用三句话说明了设计意图和约束目录里的一个文件声明一位同事加载器会把它和../agents.yaml一起读两种写法并存只把内容全放在agents.yaml里的包行为不变包默认从../agents.yaml启动两位同事General Assistant 与 Knowledge因此全新部署打开时是一个简短花名册而不是十几个没人要的 Bot另外两条记录只有在端点配置好之后才会注册增加一位同事从../catalog复制一个文件进来删除一位同事删掉它的文件。配套的examples/fintech/catalog/README.md进一步说明了为什么“一个文件一位同事”值得做catalog/里的东西不会被加载它是一份“可以拿走的同事”清单——把文件复制进agents/下次启动就注册。而agents.yaml单文件模式的问题在于新增一位同事意味着修改别人也在改的文件转交一位同事意味着给对方一段要粘贴进自己文件中间的碎片。目录模式下一位同事就是一个文件。需要提前记住一条边界这些文件不是授权grant。同事只声明技能skill技能再声明工具而它实际能调用什么取决于管理员授予了什么。因此往agents/里复制文件增加的是“指令”不是“能力”。这一点在examples/fintech/skills.yaml的头部注释里被反复强调也是整个包模型的安全底座。启动时的加载链路从源码结构看加载发生在服务启动早期顺序是“先读包、后同步进库”server/src/index.ts调用loadTenantPackage(config.tenantPackageDirectory)。注意它被提前到线程标识创建之前——包决定了部署名而频道存储需要这个名字才能生成 thread id见该行上方的注释。server/src/index.ts调用synchronizeTenantPackage(database, tenantPackage)在一个事务里把同事、频道、技能和包授予写进数据库。包目录由环境变量TENANT_PACKAGE_DIR指定默认值是../examples/fintech相对server/解析见server/src/config.ts。因此默认部署跑的就是本文讨论的这个 fintech 包。读取目录的函数在server/src/tenant-package.ts的readAgentFiles它决定了目录语义的全部细节const directory join(sourcePath, agents); const entries await readdir(directory).catch( (error: NodeJS.ErrnoException) { if (error.code ENOENT || error.code ENOTDIR) return []; throw error; }, ); const filenames entries .filter((entry) entry.endsWith(.yaml) || entry.endsWith(.yml)) .sort();四条规则可以直接从这段代码读出目录可以不存在没有agents/就是“所有同事都在一个文件里”的老包继续正常加载只读.yaml和.yml所以在agents/旁边放一个 README 不会被当作同事去解析按文件名排序注释里说明了原因——顺序决定了重复 id 会被归咎于哪个文件而“在另一台机器上报出另一个文件”的拒绝信息没人能据此修复${NAME}在这里同样展开与agents.yaml完全一致。排序行为有对应测试server/tests/tenant-package-agent-files.test.ts写入b-second.yaml、a-first.yml和一个README.md断言新增同事的顺序是[first, second]README 被忽略。单个文件的写法独立文档或agents:列表agents/下的一个文件支持两种形态。第一种是直接写一位同事# examples/fintech/agents/expense-review.yaml从 catalog/ 复制而来 id: expense-review name: Expense Review title: Finance Operations role_description: Check one expense claim at a time against the policy as it is written. avatar_seed: expense-review type: built-in system_prompt: Quote the clause you relied on, and leave the decision to a person. skills: - find-a-document第二种是把agents:列表照搬过来和agents.yaml一样agents: - id: expense-review name: Expense Review title: Finance Operations role_description: Check one expense claim at a time against the policy as it is written. type: built-in system_prompt: Quote the clause you relied on, and leave the decision to a person.第二种形态存在的理由是迁移体验有人拆分现有agents.yaml时会把列表语法一起带过来“一种能解析、另一种不能”会把一次搬移变成一道谜题。两种写法都有测试覆盖见server/tests/tenant-package-agent-files.test.ts。合并逻辑在server/src/tenant-package.ts的collectAgents先用agents.yaml的agents:列表再逐个文件文件里若没有agents键就把整个文档当作一位同事[document]。这也解释了为什么独立文档里不能缺字段——整个文档都会被当成同事记录去校验。字段与类型与agents.yaml完全一致目录里的文件走的是同一个parseAgents所以字段规则没有第二套。每个同事必须有id、name、title、role_description、type字段必填性说明id必填非空字符串且不能是部署路由名或“个人 OpenBot 浏览器”形态的 idname必填非空字符串title必填字符串最长 120 字符role_description必填非空字符串远程同事唯一的指令来源type必填只能是built-in、remote-ag-ui、remote-mastraavatar_seed可选缺省时回落到idskills可选slug 数组必须是本包skills.yaml里声明的按type分叉的字段是类型必需字段额外说明built-insystem_prompt会被写成configuration.systemPromptremote-ag-uiendpoint端点为空则该同事被丢弃而非报错remote-mastraendpoint可选remote_agent_id指定 Mastra 花名册中的某一个type的映射与校验见server/src/tenant-package.tsbuilt-in→built_in、remote-ag-ui→remote_ag_ui、remote-mastra→remote_mastra其他值直接抛出agent.type must be built-in, remote-ag-ui or remote-mastra。一个容易忽略的细节built-in同事拿到的是system_prompt而远程同事根本没有这个字段role_description是它从包里收到的唯一指令。所以远程同事的那句话要写成完整的工作简报而不是一个列表标签——这一点在docs/configuration.md里有明确说明。id的两条硬性约束id不是随便起的。parseAgents里有两道检查if (DEPLOYMENT_ROUTES.has(id)) { throw new Error( ${source}: agent.id ${id} is reserved for a deployment route and cannot name a Bot, ); } if (isPersonBrowserId(id)) { throw new Error( ${source}: agent.id ${id} is the shape of a persons OpenBot browser and cannot name a Bot, ); }第一道防的是权限绕过computer 路由的 bot-access 守卫会对部署路由名让步而请求无法区分“名叫policy的 Bot”和/policy这个路由本身这样的 Bot 会被提供给任何能登录的人。第二道防的是“撞电脑”个人 OpenBot 浏览器的 id 与 Bot 的 id 同形重名会让 Bot 与那位用户共用一台电脑。由于包里的id是唯一能自定义 Bot 标识的途径通过 API 创建的都是agent_uuid在校验阶段拒绝比事后迁移更彻底。校验拒绝时要能指出是哪个文件agents/的整个价值在于文件可以被复制来复制去所以“复制错了”必须是一个能被立刻定位的错误。collectAgents的注释写得很直白偏好某一个会让“部署运行哪个同事”取决于目录被读取的顺序而一个把同一位同事用不同文件名复制了两次的克隆将永远不会发现问题。因此重复id是报错并且两个文件都会被点名。测试里的期望字符串是agent knowledge is declared in both agents.yaml and agents/knowledge.yaml其余校验同样会带上agents/filename前缀例如把type写成smokeagents/broken.yaml: agent.type must be built-in这来自collectAgents里const source \agents/${file.filename}的传参方式以及parseAgents把source 拼进每一条错误消息的做法。除了类型与重复 id目录里的文件还要过这些关技能必须属于本包skills:里写了本包skills.yaml没有的 slug会得到agent expense-review names skill quote-the-expense-policy, which this package does not ship。这条检查是刻意的两个文件由同一个人一起交付匹配不上就是拼写错误而“静默不挂载任何技能”的拼写错误最难被发现——Bot 只是永远不做工具收窄部署看起来还在正常工作同一技能不能写两次agent x names skill triage twice。原因是同步时一次多行INSERT ... ON CONFLICTPostgres 遇到重复行会直接拒绝服务器会在启动时死于一条 SQL 错误slug 形状^[a-z0-9][a-z0-9-]{0,38}[a-z0-9]$即小写字母、数字和连字符2–40 字符首尾必须是字母或数字。它和 skills API、前端表单用的是同一个模式否则包能种出用户既打不出来也编辑不了的/命令。空端点不是错误是“还没配置”agents.yaml里的risk-analyst和picked-harness都用了${VAR:-}形式。解析后端点为空字符串时parseAgents不会报错而是把 id 收进omittedAgentIds并跳过这条记录if (type remote_ag_ui || type remote_mastra) { const endpoint typeof agent.endpoint string ? agent.endpoint.trim() : ; if (!endpoint) { omittedAgentIds.add(id); return []; } }随后validateTenantPackage把这个集合用于两件事从频道的permitted_agents里过滤掉这些 id否则“频道引用了未知 agent”会误伤以及把它们交给同步函数去停用对应的 profile。这就是“一条记录在没人挑选之前自己消失”的实现方式——注册不需要 API选择变成一个设置项剩下的交给种子同步。还有一个反过来的规则测试位于server/tests/tenant-package-duplicates.test.ts某个 id 在agents.yaml里端点为空白但在agents/risk.yaml里被正式声明那它就是“已声明”不会进omittedAgentIds也不会被从频道里剔除。真实声明胜过空白且与目录读取顺序无关for (const id of agentIds) omittedAgentIds.delete(id);环境变量插值包描述部署地址属于环境loadTenantPackage对五个必需文件、skills.yaml和每个agents/文件都调用expandEnvironment。规则在server/src/tenant-package.ts${NAME}取环境变量${NAME:-fallback}在未设置或为空串时取 fallback既没有值也没有 fallback 时抛错并指明是哪个文件在要这个变量${filename} refers to ${NAME}, which is not set in this environment.这解释了picked-harness那一行的设计type: ${PICKED_HARNESS_KIND:-remote-ag-ui}的类型本身也是插值的。如果写成字面量remote-mastra任何早于该类型的服务器都会因为“未知agent.type”拒绝整个文件——而拒绝一位同事就是拒绝整个文件部署根本起不来。插值后旧服务器看到的是remote-ag-ui只有在真的有人选了 Mastra 时才会变化。校验和删除和新增都是“包变更”agents/里的文件参与包的 SHA-256 校验和checksum: createHash(sha256) .update( [ ...contents, skills, ...agentFiles.map((file) ${file.filename}\n${file.contents}), ].join(\n), ) .digest(hex),注释说明了原因agents/进校验和的理由与skills.yaml相同——在这里新增、修改或删除一位同事都是包变更没注意到的部署会继续跑着它启动时的花名册而仓库里写的已经是另一回事。server/tests/tenant-package-agent-files.test.ts直接改一个title断言校验和随之改变。实战从 catalog 里落地一位同事catalog/里有十份可直接复制的同事覆盖财务、会议、发布、支持、入职、研究、招聘、可靠性、采购和产品反馈。它们的共同点是role_description的写法写清这份工作是什么、绝对不能做什么、以及找不到东西时该说什么。catalog/README.md的判断是——一句话的描述只会得到一个回答含糊的同事。以examples/fintech/catalog/ticket-triage.yaml为例它的约束写得非常具体“你起草人发送。绝不回复客户、绝不关闭工单、绝不承诺日期、退款或修复。如果工具报错或说未连接说出来并停下而不是猜工单系统会说什么。”它的system_prompt则规定了输出形状ask、urgency、归属团队每条都要附上工单里导致这个判断的那句话。落地步骤# 1. 复制一位同事进 agents/目录不存在就创建 mkdir -p examples/fintech/agents cp examples/fintech/catalog/ticket-triage.yaml examples/fintech/agents/ # 2. 确认它声明了 skills.yaml 里存在的技能 grep -A3 ^skills: examples/fintech/agents/ticket-triage.yaml # 3. 只跑解析层测试验证加载与校验行为 cd server bun test tests/tenant-package-agent-files.test.ts tests/tenant-package-duplicates.test.tsticket-triage.yaml声明了find-a-document和who-owns-this两者都在examples/fintech/skills.yaml中定义因此校验会通过。反过来说如果你从外部拿来一位同事、它声明了本包没有的 slug服务会在启动时拒绝并说出是哪个文件——这正是你想要的失败方式。复制完之后别忘了频道新增的同事如果不在任何channels.yaml的permitted_agents里它虽然注册了但没有对话入口。频道里引用未知 id 也会报channel references unknown agent ...。一个可以带走的检查清单文件必须落在包的agents/下扩展名.yaml或.yml独立文档形态要写全id、name、title、role_description、type以及按类型分叉的system_prompt或endpointid不能与agents.yaml或其他文件重复也不能是部署路由名skills:里的每个 slug 都必须在同一包的skills.yaml里存在且不能重复引用它的频道要同步更新permitted_agents需要端点时用${VAR:-}让未配置的部署自动丢弃这条记录而不是启动失败。删除文件之后会发生什么这是“删一位同事”最容易踩空的地方。删除文件在下一次启动时生效包不再声明它校验和变化同步逻辑也不再 upsert 它。但已经注册过的同事不会自动消失——它的行还在仍归这个包所有因此仍会出现在列表里并且仍然受删除保护。真正会被自动退休的只有一种端点为空的远程同事即进omittedAgentIds的那些同步函数会为它们设置deleted_atawait transaction .update(agentProfiles) .set({ deletedAt: now, updatedAt: now }) .where( and( isNull(agentProfiles.ownerUserId), isNull(agentProfiles.deletedAt), inArray( agentProfiles.agentId, transaction.select({ id: agentTable.id }).from(agentTable).where(/* 属于本包且在被省略的 id 中 */), ), ), );要退休一个已经从包里删掉的同事只能把它的agent_profiles行的deleted_at设上——这也正是“删除一位由人创建的 Bot”所做的事。断言deletedAt的条件里有两个isNull意味着这个操作只会作用在“包拥有、且尚未被删除”的行上不会碰到用户自己的 profile。同步阶段文件声明如何变成数据库行校验通过之后synchronizeTenantPackage在一个事务里完成写入顺序值得了解因为它决定了“改文件”和“改界面”的边界先检查保留路由名是否已经作为 Bot 存在存在则拒绝启动不回滚地半同步比拒绝更糟upsert 部署包记录含sourcePath与checksum停用被显式省略的远程同事但保留会话成员关系与可读历史逐位 upsertagents与agent_profilessetWhere保证只更新本包拥有的行如果撞上用户自己创建的 agent 或 profile会抛出collides with a user-created agent/collides with a user-owned profile逐频道 upsert 并重建channel_agents关联技能按 slug upsertorigin记为catalogue、owner_user_id为空因此所有人都能在/菜单里看到。如果这个 slug 已经被某个人写的技能占用包会让位只打一条package-skill-skipped警告并跳过——因为任何人都能写技能若在那里抛错一个人就能靠抢名字让整个部署起不来包授予的技能以grantedBy tenant-package写入且每次同步先删除“上一次由包授予的全部记录”所以包停止授予某个技能时会收回它但管理员在 Skills 页面手工授予的那条带着自己的用户 id会被保留。这也解释了为什么agents/里的skills:是安全的它种下的是技能授予而不是 MCP 授予。技能是指令、不带来能力实际可调用的工具永远是“声明”与“授予”的交集。两种写法怎么选场景建议包很小、同事固定、单人维护全部放agents.yaml行为与旧包完全一致多人协作、要频繁增删同事用agents/一位同事一个文件减少编辑冲突要把某位同事交给另一个部署用agents/直接给文件不必让对方粘贴片段只想看看有哪些现成的同事看catalog/它不会被加载两种方式不是二选一agents.yaml先读agents/逐个追加重复 id 会被拒绝。因此你可以保留general-assistant与knowledge这两个“开箱即有”的同事在agents.yaml里把后续按业务线加入的同事都放进agents/让花名册保持初始化时的简短形状——这正是 fintech 包选择的布局。小结examples/fintech/agents/README.md描述的机制很小一个目录、一个文件一位同事、与agents.yaml并存。但它背后是三条一致的工程判断同事应当是文件可复制、可删除、可转交歧义应当在启动时拒绝重复 id 点名两个文件、技能拼写错误点名文件、错误信息永远带agents/filename以及声明不等于授权技能只写指令工具仍由管理员授予。理解这三点之后catalog/的十份模板就不只是可以复制的样例而是一套你可以照着写、照着改动、照着交接的租户包工作方式。赞分享【免费下载链接】OpenBotOpen-source AI coworkers that each get a computer of their own: a browser, files and tools, with every action decided before it happens and recorded after. Bring any AG-UI agent.项目地址https://gitcode.com/GitHub_Trending/openb/OpenBot点击查看免费下载相关推荐OpenBot Agent Designer 技能实战用 create_agent 与 read_agent 造一个只做一件事的持久 AgentOpenBot Agent Designer 技能实战用 create_agent 与 read_agent 造一个只做一件事的持久 Agent 在 Op桌面应用AI Agent人工智能多智能体代码智能体浏览器控制本地部署MCP 服务WiX Toolset v3 实战使用 DirectorySearchRef 复用 DirectorySearch在同一目录下定位多个文件WiX Toolset v3 实战使用 DirectorySearchRef 复用 DirectorySearch在同一目录下定位多个文件 在 WiX To开发工具构建工具WeKan 多租户设计全解析从每租户一个进程到以组织为租户的落地实践WeKan 多租户设计全解析从每租户一个进程到以组织为租户的落地实践 导读 本文以 docs/Design/Multitenancy/Multiten后端前端协同办公项目管理上一篇为什么有人用了 humanizer 反而更假语料指纹不一致的隐形坑下一篇一周三巨头下场OpenAI、Cloudflare、StartLux 围剿 Jev决策模型要变红海创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考