
后端前端协同办公即时通讯企业应用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-harness 服务中客户端经常拿到一个会话 id却并不知道自己是否有权打开它——它可能来自一条 提及、一段聊天消息或一个文档链接。AgentSessionPreviewDto正是为这种“渲染前先探一眼”的场景设计的线上传输类型它把每个被请求的 id 解析为access、no_access、does_not_exist三种结果之一让前端用一个switch分支就能画出会话芯片chip。本文以生成的类型文档为骨架结合 Rust 服务端实现、SQL 查询与前端调用链完整讲清这个类型的结构、语义、访问判定顺序与实战用法。类型签名一个带type标签的三态联合关联文档给出的定义如下原始定义见 AgentSessionPreviewDto.mdAgentSessionPreviewDto AgentSessionPreviewData { type: access } | WithAgentSessionId { type: no_access } | WithAgentSessionId { type: does_not_exist }它的两个组成类型分别是AgentSessionPreviewData调用者有查看权限时返回的完整字段集用于渲染芯片内容WithAgentSessionId只有一个id: string用于另外两种“只告诉你这个 id 是什么状态、不泄露内容”的分支。对应的 Rust 定义位于 axum_router.rs关键是 serde 的标签序列化策略#[derive(Debug, Serialize, Deserialize, ToSchema)] #[serde(tag type, rename_all snake_case)] pub enum AgentSessionPreviewDto { /// 调用者可以查看该会话 Access(BoxAgentSessionPreviewData), /// 会话存在但调用者没有任何授权 NoAccess(WithAgentSessionId), /// 该 id 下不存在任何会话 DoesNotExist(WithAgentSessionId), }有三点值得注意内部标签internally tagged标签字段名是type值与变体名一起走snake_case因此线上取值就是文档写的access、no_access、does_not_exist。Access变体装箱BoxAgentSessionPreviewData让枚举体积保持稳定避免“一个大结构体撑大所有分支”。这一点与领域层的 AgentSessionPreview 一致那里同样用Box并附带了clippy::large_enum_variant的说明注释。两个方向都能反序列化源码注释明确写着 “Clients deserialize this, so both derives are used”因此Serialize与Deserialize都派生服务端序列化、客户端乃至测试夹具反序列化共用同一套形状。为什么用三态而不是错误码文档的核心动机写在注释里AgentSessionPreviewDto“Tagged the same way the chat and document preview endpoints tag theirs”也就是与聊天、文档的预览端点保持同一套标签语义客户端既然已经有渲染那些芯片的分支就能用同样的方式渲染会话芯片。更实际的原因在领域层注释中讲得更透model.rs预览存在的意义是客户端在还不知道自己能否打开会话时就先为一个引用提及、链接画出一个芯片因此每个 id 必须被回答为“三种事实之一”而不是一个错误只有第一种分支携带数据所以无权限的调用者除了“这个会话存在”之外学不到任何东西——这与GET /agent-sessions/{id}返回403已经泄露的信息量相同不构成额外泄漏。这也解释了一个容易被误读的设计预览端点没有按 id 的访问提取器no per-id access extractor。路由注释写得很直接axum_router.rs“a chip has to render for a session the caller cannot open, so access is answered per id in the body rather than enforced on the request”。如果像单条读取那样在请求层直接 403整个批量预览就会因为其中一条无权访问而失败。对比之下GET /agent-sessions/{session_id}是严格鉴权的源码注释指出该端点已把“无授权”401与“根本不存在”404区分开以便客户端分辨“创建尚未落库、应当重试的竞态”和“该拒绝的拒绝”axum_router.rs。预览端点把这个区分下沉到响应体里一次请求返回整批结果。端点契约POST /agent-sessions/preview类型永远依附于端点出现完整契约如下。请求体{ sessionIds: [ 3f2a9c1e-0000-4000-8000-000000000001, 3f2a9c1e-0000-4000-8000-000000000002 ] }字段名sessionIdscamelCaseDTO 上标了#[serde(rename_all camelCase)]服务端会折叠重复 id最多接受MAX_PREVIEW_SESSION_IDS 100个去重后的 idmodel.rs、请求结构体注释见 axum_router.rs。这个上限的注释解释得很务实“a mention menu never renders anywhere near this many chips at once”。响应体{ previews: [ { type: access, id: 3f2a9c1e-0000-4000-8000-000000000001, name: Fix flaky tests, ownerId: user|a1b2c3, botId: 9d8c7b6a-0000-4000-8000-000000000010, bot: { id: 9d8c7b6a-0000-4000-8000-000000000010, name: Test Agent, handle: test-agent, avatarUrl: null }, status: { kind: event, event: acp_ready }, createdAt: 2026-10-30T09:12:44Z, modifiedAt: 2026-10-30T09:40:02Z }, { type: no_access, id: 3f2a9c1e-0000-4000-8000-000000000002 } ] }响应外壳是PreviewAgentSessionsResponse只有一个previews数组文档明确说明它是“one entry per distinct requested id,in no particular order”PreviewAgentSessionsResponse.md。顺序不保证、也不与请求顺序对齐客户端必须按id做映射不能按下标取值。上面的示例值用于说明形状字段集合与类型与 OpenAPI schema 一致。状态码状态码含义依据200批量结果逐 id 回答axum_router.rs400去重后的 id 数超过 100TooManyPreviewIdsaxum_router.rs401调用者未通过认证同上注解500服务端错误同上注解注意400的语义是“这批请求本身太大”而不是“其中某个 id 有问题”单个 id 的问题一律以三态之一回答。字段语义AgentSessionPreviewData的九个属性access分支的字段全部来自 AgentSessionPreviewData.md服务端结构体见 axum_router.rs字段类型必填说明idstring(uuid)是会话 idnamestring是面向用户的会话名ownerIdstring是会话所属用户botIdstring(uuid)是运行该 agent 的 bot供头像使用botnull \| SessionBot否bot 的最小身份信息可用时服务端补全statusSessionStatusDto是会话最后已知状态用于在线状态指示createdAtstring(date-time)是创建时间modifiedAtstring(date-time)是最后修改时间bot为什么可选bot用的是#[serde(default, skip_serializing_if Option::is_none)]字段缺席时反序列化为None为None时干脆不序列化。这个设计不是随手写的——bot是在权限判定之后才补全的。仓储层返回候选数据时统一把bot置为Nonepostgres.rs服务层再对可访问会话按bot_id去重后水合service.rs。SessionBot自身定义在 model.rsid、name、handle不含前导、可选的avatarUrl。注释提示了一个容易踩坑的细节bot 发出的消息其 sender 形如bot|{id}。status的三态SessionStatusDto同样是内部标签联合标签字段为kind见 axum_router.rs{ kind: no_messages }—— 尚未收到任何状态更新领域模型里的默认值{ kind: event, event: acp_ready }—— 运行时上报的最后一条系统事件event是事件的线上名称{ kind: disconnected }—— 会话在未发送关闭事件的情况下断开。它的领域对应物是 SessionStatus。前端apps/web里还有一个把字符串归一化的工具函数normalizeAgentSessionStatusmention-types.ts说明该状态在既有链路中既可能以对象、也可能以纯字符串形式出现。服务端解析流程一次请求内部发生了什么preview_sessions是这套语义的唯一实现入口service.rs顺序如下去重与上限校验把 id 收进HashSet再展开长度超过MAX_PREVIEW_SESSION_IDS就返回TooManyPreviewIds空输入短路ids.is_empty()直接返回空数组不会打数据库批量取候选调用仓储preview(viewer, ids)结果按data.id建HashMap逐 id 裁决在 map 里找不到 →DoesNotExist(id)找到则判断visible candidate.has_grant || view_access.can_view(viewer, id)可见为Access否则NoAccess(id)回填 bot 身份只遍历Access变体按bot_id缓存session_bot()查询结果失败只记warn日志并降级为None不影响整批响应。权限的两条腿物化授权 视图端口第 4 步的||是全篇最关键的实现细节。has_grant来自数据库里真实存在的授权行view_access则负责那些“没有行也能继承”的访问权——路由注释点明了来源“Links and originating discussions can grant access without a materialized row”service.rs。视图端口是 SessionViewAccesscan_view返回一个装箱 futureobject-safe便于服务以擦除类型持有。默认实现NoInheritedSessionAccess恒返回false即“没有 entity-access 服务的进程或测试环境下只认物化授权”。这解释了为什么单测可以在不接完整权限系统的前提下验证行为。SQL谁算“有物化授权”仓储的 SQLpostgres.rs先构造调用者的授权来源集合再对entity_access做EXISTSWITH viewer_source_ids AS ( SELECT cp.channel_id::text AS source_id FROM comms_channel_participants cp WHERE cp.user_id $2 AND cp.left_at IS NULL UNION ALL SELECT t.team_id::text FROM team_user t WHERE t.user_id $2 UNION ALL SELECT $2 ) ... EXISTS ( SELECT 1 FROM entity_access ea WHERE ea.entity_id s.id AND ea.entity_type agent_session AND ea.source_id IN (SELECT source_id FROM viewer_source_ids) ) AS has_access!也就是说物化授权有三种来源用户本人、其尚未退出的频道、其所属团队。源码注释强调这三个来源“与 Soup 分支和访问提取器解析的是同样三个”从而保证预览结论与真正去读那条路由时的结论一致不会出现“芯片显示可点、点进去 403”的错位。同时查询还会带回thread_parent从comms_messages里取父实体类型与 id序列化为MessageParent这就是第 4 步视图端口需要的“继承线索”原料。友好降级而非整批失败两个降级点值得单独记住候选解析失败会整体作为错误抛出failed to preview agent sessions但bot 身份补全失败只记 warnfailed to hydrate session preview botbot退化为null。芯片仍能画出名称与状态只是少了头像/句柄。客户端用法TypeScript SDK 与分支渲染生成的 TS 类型与文档一一对应types.gen.tsexport type AgentSessionPreviewDto (AgentSessionPreviewData { type: access; }) | (WithAgentSessionId { type: no_access; }) | (WithAgentSessionId { type: does_not_exist; });SDK 方法previewAgentSessionssdk.gen.ts把POST /agent-sessions/preview与Content-Type: application/json封装好了const result await client.previewAgentSessions({ body: { sessionIds: ids }, }); for (const preview of result.previews) { switch (preview.type) { case access: // preview 此时已收窄为 AgentSessionPreviewData { type: access } renderChip({ id: preview.id, name: preview.name, bot: preview.bot, status: preview.status }); break; case no_access: renderChip({ id: preview.id, locked: true }); break; case does_not_exist: renderChip({ id: preview.id, missing: true }); break; } }type字段是判别式discriminantTypeScript 会自动收窄类型——这正是文档所说“同一个分支就能渲染”的收益。真实链路先查 Soup再用预览补齐apps/web的提及解析并没有无条件打这个端点而是做了两级策略mention-fetchers.ts先把 id 批量丢给 GraphQL 的 Soup 查询agentSessionMentionInput会按 id 数量构造or表达式或退化为include: true命中即为access剩下的在 Soup 里查不到的才调用agentHarnessServiceClient.preview。源码注释解释了原因“Absence in Soup cannot distinguish deletion from revoked access”——Soup 里缺失既可能是被删了也可能是权限被收回而这恰恰是预览端点三态存在的意义。底层 HTTP 客户端就是薄封装client.tspreview(sessionIds: string[]) { return fetchWithTokenPreviewAgentSessionsResponse( ${agentHarnessHost}/agent-sessions/preview, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ sessionIds }) } ); }再往上还有一层批处理器mention-batcher.tsAsyncBatcher以wait: 30毫秒聚合窗口、maxSize: 50把零散的单 id 请求合并成一次批量调用并在 fetch 前用[...new Set(...)]再去重一次。maxSize: 50与后端上限 100 之间留有安全余量。客户端的归一化前端把三态映射成自己的AgentSessionMentionPreviewmention-types.tsexport type AgentSessionMentionPreview | { access: access; data: AgentSessionMentionData } | { access: no_access | does_not_exist };并把modifiedAt重命名为updatedAt以对齐既有芯片组件mention-fetchers.ts中的updatedAt: preview.modifiedAt。如果你在自建客户端这种字段重命名要在适配层做不要直接改 DTO 的形状。边界与陷阱清单顺序不可依赖响应“in no particular order”且去重后条目数可能小于请求数。永远按id建索引。重复 id 会被折叠服务端用HashSet去重因此“请求 150 个含大量重复的 id”不一定报 400判定发生在去重之后。上限是 100 个去重 id不是 100 个原始元素超限返回400而非截断。no_access与does_not_exist都只有id不要期待在其中读到name或status也不要在前端逻辑里把两者混为“渲染失败”——它们对应不同的 UI 文案上锁 vs 已删除。bot可能为null这是服务端水合失败后的正常降级芯片要有无头像的兜底样式。不要用它做权限判决的唯一依据真正的写操作仍走AgentSessionAccessLevelExtractor例如重命名需要OwnerAccessLevel见 axum_router.rs预览只回答“能不能看个芯片”。测试如何验证这套语义服务层测试给了两条可执行的行为契约service/test.rspreviews_hydrate_bot_identity_only_for_accessible_sessions传入[session, session]两个相同 id断言结果长度为1证明去重且Access分支的bot.name已被水合为Test Agent换一个无关用户查询同一 id则精确得到vec![AgentSessionPreview::NoAccess(session)]。previews_resolve_discussion_and_link_access_through_the_view_port对“文档讨论派生”和“Initiative 派生”两种父实体分别验证——默认无视图端口时全部为NoAccess用GrantingViewAccess注入后对应会话变为Access。这直接证明了物化授权与继承授权是两条独立的通过路径。领域层还提供了一个便利方法AgentSessionPreview::id()model.rs无论三态如何都能取出所答的 id测试里正是用它做sort_by_key后再比较避免依赖服务端返回顺序。小结AgentSessionPreviewDto看起来只是一个三行 union但它承载的是 Macro 在“隐私最小暴露”和“UI 可用性”之间的一次明确取舍批量、逐 id、以事实代替错误。落到实践上记住四件事即可——用type做判别式分支、按id而非下标关联结果、bot允许缺席、100 个去重 id 是硬上限。服务端侧则由“物化授权 SQL SessionViewAccess视图端口”两条腿共同决定access与no_access的分界确保预览结论与真正打开会话时的权限判定保持一致。赞分享后端前端协同办公即时通讯企业应用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 AgentSessionPreviewData 详解Agent 会话预览数据类型、字段语义与访问控制实现Macro AgentSessionPreviewData 详解Agent 会话预览数据类型、字段语义与访问控制实现 在 Macro 的统一工作空间中Age后端前端协同办公即时通讯企业应用CRMAI Agent人工智能Macro Agent Harness 响应类型详解AgentSessionsForPullRequestsResponses 与批量 PR 关联会话查询Macro Agent Harness 响应类型详解 AgentSessionsForPullRequestsResponses 与批量 PR 关联会话查询后端前端协同办公即时通讯企业应用CRMAI Agent人工智能Macro agent-harness SDK 中 GetAgentSessionPermissionsData 类型详解请求 Agent 会话共享权限的类型契约Macro agent harness SDK 中 GetAgentSessionPermissionsData 类型详解请求 Agent 会话共享权限的类型后端前端协同办公即时通讯企业应用CRMAI Agent人工智能上一篇终极免费文档下载指南如何用kill-doc脚本一键获取30平台公开资源下一篇Audiveris乐谱识别教程5步将纸质乐谱转换为数字宝藏创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考