
后端前端协同办公即时通讯企业应用CRMAI Agent人工智能【免费下载链接】macroMacro is a unified workspace for teams: email, chat, docs, tasks, agents, calls, and CRM — -linked together with shared AI memory.项目地址https://gitcode.com/GitHub_Trending/macro3/macro点击查看免费下载在 macro 这样把邮箱、聊天、文档、任务、Agent 与 CRM 打通的工作区里这条数据属于谁是一个被几乎所有领域 crate 反复追问的问题。model_owner就是为这个问题提供统一答案的共享基础设施它把归属者建模为封闭的三值类型用户、机器人、团队把谁创建与谁拥有分离并用一套 Rust 函数与 PostgreSQL 函数双实现来定义团队链接的可见范围。读完之后你将掌握该 crate 的值类型语义、principal 字符串的严格解析规则、创建者解析链路以及 Rust 与 SQL 两侧必须保持同步的那条可见性策略。一、crate 定位做什么不做什么crates/model_owner/AGENTS.md对该 crate 的定位给出了三条硬边界这也是理解后续所有设计的前提维度内容是什么被几乎所有带类型化归属的领域 crate 共享的基础设施提供Owner/OwnerType这一类型化归属引用区别于实际操作者拥有什么Owner、OwnerType、CreationPrincipal三个值类型以及团队可见范围策略函数不负责什么归属者是否真的能访问某实体 → 交由models_permissions、entity_access判定依赖仅依赖bot_id、macro_user_id两个底层标识 crate被谁使用agent_session、agent_trigger、bots、channels、databases_sql、documents、entity_access、entity_registry、model、model_notifications、models_permissions、models_soup、projects、scheduled_action、soup、teams等这条不负责鉴权的边界在代码里被反复强调。例如 team.rs 的模块文档开头就写明解析受众从不授予归属权而 entity_registry 的 owner_grant_policy.rs 也说明把归属展开为授权来源并不等于创建授权。整个 crate 的依赖面极小见 Cargo.toml除bot_id、macro_user_id外只有serde、thiserror、uuidsqlx是可选的 feature。二、Owner三种主体与三种 principal 字符串2.1 枚举定义src/lib.rs 定义了归属者枚举每个变体都携带一个已解析的强类型标识而不是裸字符串/// Who owns an entity. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(into String, try_from String)] pub enum Owner { /// A Macro user (macro|email). User(MacroUserIdStrstatic), /// A bot (bot|uuid). Bot(BotId), /// A team (hyphenated UUID, no prefix). Team(Uuid), }三种主体的存储形式与示例变体存储形式前缀示例Owner::Usermacro|emailmacro\|macro\|hutchmacro.comOwner::Botbot|uuidbot\|bot\|00000000-0000-0000-0000-00000000a1a1Owner::Team带连字符的 36 位 UUID无01234567-89ab-cdef-0123-456789abcdef注意团队不带前缀用户与机器人必须带前缀。前缀常量定义在 lib.rs团队 UUID 长度常量TEAM_UUID_HYPHENATED_LEN 36也在同处。2.2 先有类型再解析Owner提供两个入口Owner::parse(owner_type, value)在已知类型下解析严格匹配该类型的存储形式lib.rs#L105-L119Owner::from_principal_str(value)按前缀猜类型macro|走用户、bot|走机器人其余一律按团队处理lib.rs#L121-L130。pub fn from_principal_str(value: str) - ResultSelf, OwnerParseError { if value.starts_with(USER_PRINCIPAL_PREFIX) { Self::parse(OwnerType::User, value) } else if value.starts_with(BOT_PRINCIPAL_PREFIX) { Self::parse(OwnerType::Bot, value) } else { Self::parse(OwnerType::Team, value) } }其余一律按团队意味着任何非macro|、非bot|的输入都要先过长 UUID 校验。团队解析函数 parse_hyphenated_uuid 做了三重检查长度必须等于 36、字符必须是 ASCII 十六进制或-、最后才交给Uuid::parse_str。这套前置校验让错误尽早暴露避免把非 UUID 的字符串当作团队 ID 传播出去。反向操作由principal_id()与Display提供二者语义一致/// Canonical principal string for this owner. pub fn principal_id(self) - String { match self { Self::User(user_id) user_id.to_string(), Self::Bot(bot_id) bot_id.into_storage_id().to_string(), Self::Team(team_id) team_id.hyphenated().to_string(), } }见 lib.rs#L95-L103 与 lib.rs#L152-L160。机器人的into_storage_id()来自 bot_id它会重新拼出bot|uuid字符串团队的hyphenated()保证输出始终是带连字符形式而不是 32 位紧凑形式。2.3 严格解析的反面用例src/test.rs 中的测试把严格落实为可执行断言这些用例正是接入方最容易踩的坑测试断言内容parse_rejects_bot_principal_as_userOwner::parse(User, bot\|…)必须失败parse_rejects_user_principal_as_botOwner::parse(Bot, macro\|…)必须失败parse_rejects_bare_uuid_as_bot裸 UUID 不能被当作机器人parse_rejects_bot_principal_as_teambot\|…不能被当作团队parse_rejects_malformed_uuidsbot\|not-a-uuid、not-a-uuid均失败parse_rejects_non_canonical_uuids32 位无连字符 UUID 被拒绝其中非规范 UUID 被拒绝这一点尤其值得强调0123456789abcdef0123456789abcdef在语义上是合法 UUID但因为不满足 36 位带连字符的长度约束Owner::parse(Team, …)仍然报错。这保证了同一个归属者在系统里只有一种字符串表示避免同一团队出现两种 ID 形态导致索引与比较失效。2.4 序列化透明字符串 反序列化时解析Owner上的#[serde(into String, try_from String)]lib.rs#L41-L42意味着它在 JSON 里就是一个普通字符串而不是结构体。这一选择对 API 与 GraphQL 层非常友好同时通过impl TryFromStringlib.rs#L174-L180在反序列化时强制走from_principal_str从而把校验前移到边界。测试 user_round_trip 明确断言serde_json::to_value(owner)等于Value::String(macro|hutchmacro.com)机器人与团队同样如此test.rs#L19-L50。2.5 数据库侧OwnerType 与entity_owner_typeOwnerType是为sqlx类型entity_owner_type准备的封闭字符串集合同时充当 wire 层判别符lib.rs#L52-L67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all lowercase)] #[cfg_attr(feature sqlx, derive(sqlx::Type))] #[cfg_attr( feature sqlx, sqlx(type_name entity_owner_type, rename_all lowercase) )] pub enum OwnerType { User, Bot, Team }它有三条互相印证的约束serde 输出小写字符串user/bot/teamDisplay/FromStr也一样只在三个小写值之间往返其他输入返回OwnerParseErrorlib.rs#L182-L203测试见 test.rs#L84-L105只有启用sqlxfeature 才派生sqlx::Type并绑定到 PG 枚举名entity_owner_type。数据库侧的枚举与约束在 迁移 20260910144347_add_entity_table.sql 中定义CREATE TYPE entity_owner_type AS ENUM (user, bot, team); CREATE TABLE entity ( id UUID PRIMARY KEY, entity_type TEXT NOT NULL CHECK (entity_type IN (project,document,chat,agent_session,scheduled_action)), owner_type entity_owner_type NOT NULL, owner_id TEXT NOT NULL, ... CONSTRAINT entity_owner_id_matches_type CHECK ( (owner_type user AND owner_id LIKE macro|%) OR (owner_type bot AND owner_id LIKE bot|%) OR (owner_type team AND owner_id ~ ^[0-9a-f-]{36}$) ) );entity_owner_id_matches_type这条 CHECK 是 Rust 端三套 principal 格式在数据库侧的镜像。需要注意两者的严格程度不同Rust 侧团队 ID 会做长度 十六进制字符 真实 UUID 解析三重校验而 SQL 侧只做^[0-9a-f-]{36}$的形状匹配。因此写库时不应绕过 Rust 的Owner类型直接把字符串塞进owner_id。三、CreationPrincipal谁创建、谁拥有、谁执行3.1 三种创建场景CreationPrincipal定义在 src/creation_principal.rs其核心设计动机写在类型文档里Owner、actor、acting user 三者都从这一个值派生因此不可能互相矛盾。变体语义拥有者执行用户执行机器人User(user)用户为自己创建该用户Some(user)NoneBotForUser { bot, user }机器人代表已验证用户创建该用户Some(user)Some(bot)TeamBot { bot, team }团队作用域机器人无执行用户该机器人NoneSome(bot)派生逻辑非常直白creation_principal.rs#L36-L63pub fn owner(self) - Owner { match self { Self::User(user) | Self::BotForUser { user, .. } Owner::User(user.clone()), Self::TeamBot { bot, .. } Owner::Bot(bot.get()), } }TeamBot使用NonSystemBotId而非裸BotId这是一个重要约束bot_id 中的NonSystemBotId::new只在 ID 不属于第一方系统机器人时返回Some。换言之系统机器人不能作为无用户归属的拥有者它必须始终代表某个用户行事。测试 src/creation_principal/test.rs 三个用例分别验证了这三条路径其中team_bot_owns_without_a_user断言principal.user()为None、owner().principal_id()等于bot|00000000-0000-0000-0000-00000000b07a。3.2 从鉴权结果到创建主体CreationPrincipal的构造并不在model_owner内部完成而是在entity_registry的 resolve_creation_principal 中由MacroAuthorization推导pub fn resolve_creation_principal( authorization: MacroAuthorization, non_user_owners: NonUserOwners, ) - ResultCreationPrincipal, CreationPrincipalError { match authorization { MacroAuthorization::User(user) | MacroAuthorization::Internal(Some(user)) { Ok(CreationPrincipal::User(user.macro_user_id.clone())) } MacroAuthorization::Bot(bot) resolve_bot(bot, non_user_owners), MacroAuthorization::Internal(None) Err(CreationPrincipalError::InternalWithoutUser), MacroAuthorization::Harness(_) Err(CreationPrincipalError::Harness), } }机器人分支的判定顺序同文件 #L81-L102值得逐条记住有acting_user→ 直接产出BotForUser不检查开关无执行用户时必须是非系统机器人否则SystemBotWithoutUserBotScope::User→UserScopeWithoutUserBotScope::Team但无已验证团队 →MissingTeam团队作用域 有团队 NonUserOwners::Disabled→NonUserOwnersDisabled全部满足才产出TeamBot。NonUserOwners开关的配置解析刻意不读环境变量creation_principal.rs#L24-L37true开启false或缺失关闭其他值报ParseBoolError。这一点让开关的来源配置系统与语义解耦也方便测试。错误枚举的六个变体见 #L40-L60每个都带一句可直接作为日志文案的错误信息例如a team-scoped bot has no verified team。3.3 归属展开从 Owner 到授权来源创建主体确定归属后entity_registry的 OwnerGrantPolicy::grants_for 把Owner展开为若干(source_type, source_id)授权来源用户与团队只产生一条自身授权普通机器人产生机器人自身授权其 sponsor赞助者授权系统机器人只产生自身授权不要求存在持久化的 bot 行sponsor 缺失或指向另一个机器人直接InvalidBotSponsor失败关闭fail closed。这段逻辑与Owner/BotId的组合直接相关也是类型化归属能安全支撑共享与可见性计算的原因。四、owner_team团队链接可见范围的双实现策略4.1 Rust 侧OwnerTeamFacts 与纯函数src/team.rs是AGENTS.md里唯一被标注为 Gotcha 的模块。它接收一个Owner和三份由仓储派生、而非调用方传入的事实team.rs#L13-L22/// Repository-derived membership and bot ownership facts, not caller-supplied teams. #[derive(Debug, Default, Clone, Copy)] pub struct OwnerTeamFacts { /// The users membership for a user owner. pub user_team: OptionUuid, /// The bots explicit team, if it is team-owned. pub bot_team: OptionUuid, /// The owning users membership, if the bot is user-owned. pub bot_user_team: OptionUuid, }策略函数只有五行team.rs#L24-L35pub fn owner_team(owner: Owner, facts: OwnerTeamFacts) - OptionUuid { match owner { Owner::User(_) facts.user_team, Owner::Team(team) Some(*team), Owner::Bot(_) facts.bot_team.or(facts.bot_user_team), } }三条规则的完整语义Owner 类型可见受众来源边界行为Userfacts.user_team该用户当前所属团队无团队 →None即没有团队链接受众Team团队自身恒为Some(team)即使 facts 全空Bot优先facts.bot_team回退facts.bot_user_team两者皆空缺失机器人 / 无团队用户→None注意用户保留其当前成员关系这一表述OwnerTeamFacts不是调用方随便传的入参而是必须从仓储读取的事实避免上层伪造团队归属。文档注释还特别声明了两点限制解析受众从不授予归属且不授权编辑共享设置。src/team/test.rs 用一个精心构造的 facts 覆盖所有分支user_team Some(other)、bot_team Some(team)、bot_user_team Some(other)从而可以区分机器人显式团队优先与回退到所属用户团队同时断言Owner::Team(team)在OwnerTeamFacts::default()下依然返回Some(team)。4.2 SQL 侧owner_team 集合函数同一策略在 PostgreSQL 中以集合函数实现定义在 迁移 20260922200142_resolve_typed_owner_team.sqlCREATE FUNCTION public.owner_team(principal text) RETURNS TABLE (team_id uuid) LANGUAGE sql STABLE PARALLEL SAFE AS $$ SELECT tu.team_id FROM public.team_user tu WHERE tu.user_id principal AND principal NOT LIKE bot|% AND principal !~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ UNION ALL SELECT CASE WHEN principal ~ ^[0-9a-fA-F]{8}-...$ THEN principal::uuid END WHERE principal ~ ^[0-9a-fA-F]{8}-...$ UNION ALL SELECT COALESCE(b.team_id, tu.team_id) FROM public.bots b LEFT JOIN public.team_user tu ON b.team_id IS NULL AND tu.user_id b.owner_user_id WHERE b.id CASE WHEN principal ~ ^bot\|...$ THEN substring(principal FROM 5)::uuid END AND COALESCE(b.team_id, tu.team_id) IS NOT NULL $$;上面为便于阅读省略了重复的 UUID 正则完整正则见迁移文件第 14、16–18、23 行。三段UNION ALL与 Rust 的三个 match 分支一一对应用户分支按user_id查team_user但排除了形如bot|…的 principal 和形如 UUID 的 principal。注释里写明这是保留历史用户 ID但绝不把带类型的 bot/team 当作用户。团队分支当 principal 自身是 UUID 时直接把它作为team_id返回且不依赖team_user表——正对应 Rust 的Owner::Team(team) Some(*team)。机器人分支以b.id substring(principal FROM 5)::uuid定位机器人行用COALESCE(b.team_id, tu.team_id)实现显式团队优先、回退到 owner 用户团队再用IS NOT NULL过滤掉两者皆空的情况——与facts.bot_team.or(facts.bot_user_team)完全同构。函数声明为STABLE PARALLEL SAFE并且注释强调要保持可内联inlinable用户归属的查询依旧走team_user上的索引不会退化成逐实体往返。这条性能取向是它被写成 SQL 函数而非应用层循环的关键原因。4.3 为什么必须两侧锁步AGENTS.md的 Gotcha 明确指出OwnerTeamFacts必须与owner_team的集合逻辑保持锁步任何行为变更都需要在两侧补上对应的适配测试。仓库里确实存在跨语言的对拍测试crates/macro_db_client/src/share_permission/access_level/chat_tests.rs 中的typed_owner_team_links_match_batch_access它加载 typed_owner_team 夹具对三个用户与七份实体31–37逐一断言member has_team才拥有Comment级别。夹具中的实体覆盖了全部归属形态typed_owner_team.sql#L35-L42文档 ID 后缀名称owner团队链接是否应命中031Usermacro\|typed-ownerexample.com是该用户属于 owner 团队032Teamless usermacro\|typed-teamlessexample.com否无团队033Team90000000-…-011是团队自身即受众034Team botbot\|…021team_id 011是035User botbot\|…022owner_user_id typed-owner是036Teamless botbot\|…023owner 用户无团队否037Missing botbot\|…024bots表无此行否夹具还刻意插入了一批历史 principal 行#L19-L34例如让团队 UUID 与 bot principal 在team_user里拥有另一团队的成员关系并在注释中说明历史 principal 行的成员关系不得覆盖其类型化归属。这正是 SQL 用户分支里那两条排除条件存在的原因。4.4 真实调用点会话批量访问级别owner_team函数在查询里被用于判断linkShare TEAM的链接是否对当前访问者生效见 chat.rsSELECT 1 FROM owner_team(c.userId) owner_team WHERE owner_team.team_id::text ANY( SELECT source_id FROM user_source_ids )这里的c.userId是被查询会话或文档/项目的归属者 principal而user_source_ids是访问者自己的来源集合参与的信道、所属团队、自身 ID。也就是说函数把归属者映射成受众团队再与访问者的来源集合求交——owner_team只负责前半段身份比对由调用方完成。这与模块文档Resolving an audience never confers ownership完全一致。五、在领域模型中的落地形态modelcrate 直接以Owner作为字段类型把类型化归属推进到 API 层。以文档为例crates/model/src/document.rs 中/// The owner of the document #[schema(value_type String)] #[sqlx(try_from String)] pub owner: Owner,两个属性各有作用#[sqlx(try_from String)]让数据库里的text列在映射时自动走TryFromString即from_principal_str。由于Owner的 serde 也是字符串读写两侧的表示完全统一#[schema(value_type String)]让 OpenAPI schema 把该字段描述为字符串与 JSON 实际形态一致避免生成嵌套对象模型。同样的模式出现在BasicDocument、项目模型等位置Owner在modelcrate 中被activity/map_item.rs、chat.rs、document.rs、item/map_item.rs、project.rs等多处引用。这也是AGENTS.md所说被几乎所有带类型化归属的领域 crate 使用的具体体现。另一处具体落地是活动归属activitycrate 把CreationPrincipal映射为Attributioncrates/activity/src/domain/models.rs#L91-L101CreationPrincipal::User(user) Self::direct(Actor::new_from_user(user.clone())), CreationPrincipal::BotForUser { bot, user } { Self::delegated(Actor::new_from_bot(*bot), user.clone()) } CreationPrincipal::TeamBot { bot, .. } Self::direct(Actor::new_from_bot(bot.get())),BotForUser映射为delegated机器人执行、用户为发起人TeamBot映射为direct机器人自己就是直接执行者。这条映射说明CreationPrincipal不只是归属计算工具也是审计与活动流的语义来源——它保证记录的操作者与记录的所有者不会互相冲突。六、验证与运行AGENTS.md给出的检查命令很简短但有一个容易忽略的前提# 基础单元测试SQLX_OFFLINE 需未设置 cargo test -p model_owner # 覆盖 Postgres 类型实现sqlx feature cargo test -p model_owner --features sqlxSQLX_OFFLINE之所以要取消是因为该 crate 本身不含查询、但--features sqlx会拉起 sqlx 的类型派生离线元数据环境会干扰编译流程。若只想验证纯逻辑解析规则、把握团队受众第一条命令即可只有在需要确认OwnerType与entity_owner_type的映射时才需要开启 feature。涉及 SQL 对拍的测试属于macro_db_client需要真实 Postgres 实例sqlx test 宏会为每个用例建库。例如cargo test -p macro_db_client typed_owner_team_links_match_batch_access修改owner_team策略时建议的验证顺序是先跑model_owner::team的单元测试确认 Rust 语义再跑上述 SQL 对拍测试确认集合实现最后检查 chat_tests.rs 中基于 TEAM 链接的批量访问用例是否仍然通过。七、易错点清单不要用裸 UUID 表示机器人。Owner::parse(Bot, 0123…)必然失败反之裸 UUID 只会被解释为团队。不要用 32 位紧凑 UUID 表示团队。长度检查会直接拒绝即使Uuid::parse_str本身能接受。不要混淆受众与鉴权。owner_team只回答这个归属者的团队链接面向哪些团队最终访问级别由entity_access、models_permissions决定。不要让调用方传入OwnerTeamFacts的团队值。三个字段必须来自仓储读取否则等于把归属伪造权交给请求方。改 SQL 不改 Rust或反之。team.rs与owner_team(principal)是同一策略的两种实现typed_owner_team.sql夹具中的历史 principal 行就是为捕捉不对称行为而存在的。系统机器人不是合法的无用户归属者。NonSystemBotId在类型层面就挡住了若绕过它直接构造Owner::BotOwnerGrantPolicy会走系统机器人只发自身授权的分支与CreationPrincipal的语义不一致。数据库 CHECK 比 Rust 宽松。entity_owner_id_matches_type的正则只是形状匹配写库前必须经过Owner的类型化解析。八、小结model_owner看起来只是一个包含Owner、OwnerType、CreationPrincipal的小 crate但它把三件容易混淆的事拆得足够干净值层面谁拥有 —— 用户、机器人、团队三值封闭类型配一套严格且唯一的 principal 字符串规范行为层面谁创建、谁执行、谁拥有 ——CreationPrincipal让三者从同一个值派生消除不一致可见性层面团队链接面向谁 —— 一个纯函数加一个 SQL 集合函数两条实现被夹具强制对齐。理解了这三层再去看entity表的owner_type/owner_id约束、model里#[sqlx(try_from String)]的Owner字段以及share_permission中owner_team(c.userId)的用法就能把它们串成一条完整的归属 → 授权来源 → 可见范围链路而不会在拥有者是否等于操作者团队链接是否等于编辑权限这类边界问题上出错。赞分享后端前端协同办公即时通讯企业应用CRMAI Agent人工智能【免费下载链接】macroMacro is a unified workspace for teams: email, chat, docs, tasks, agents, calls, and CRM — -linked together with shared AI memory.项目地址https://gitcode.com/GitHub_Trending/macro3/macro点击查看免费下载相关推荐Macro model_owner 深度解析类型化所有权、CreationPrincipal 与团队链接受众策略Macro model_owner 深度解析类型化所有权、CreationPrincipal 与团队链接受众策略 model_owner 是 Macro 仓库后端前端协同办公即时通讯企业应用CRMAI Agent人工智能Macro channel_labels 模块解析团队共享与私有的聊天频道标签、智能标签与可见性策略Macro channel_labels 模块解析团队共享与私有的聊天频道标签、智能标签与可见性策略 channel_labels 是 Macro 统一工作空后端前端协同办公即时通讯企业应用CRMAI Agent人工智能Kiln 工单路由 Playbook团队归属、优先级与升级判定的一体化 Triage 决策体系Kiln 工单路由 Playbook团队归属、优先级与升级判定的一体化 Triage 决策体系 本指南围绕 Kiln 开源仓库中面向支持工单分诊triage人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用MLOps上一篇LeetCode 857 雇佣 K 名工人的最低成本贪心 堆优先队列完整题解 | AlgoNote 算法通关手册下一篇devenv 中 path: 本地输入变更的评估缓存失效机制详解创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考