首页
/ Multica 评论 @ 提及与委派机制深度解析:从 `mention://` 语法到触发器与权限门禁

Multica 评论 @ 提及与委派机制深度解析:从 `mention://` 语法到触发器与权限门禁

2026-09-07 18:36:37作者:廉彬冶Miranda

导读

在 Multica 中,一条 issue 评论里的 @某个Agent 不仅是把人"圈"出来,更会触发后端为该 Agent 排入一个真实运行任务。本文基于仓库内内置技能文档 multica-mentioningSKILL.md)及其源码映射(mentioning-source-map.md),完整讲解 mention 链接的语法契约、四种类型各自"入队了什么"、@all 广播语义、预览与抑制参数、以及"什么不会发生"的守卫与 outcome 编码,并下沉到 server/internal/util/mention.goserver/internal/handler/comment.go 的源码实现加以印证。读完你不仅能正确书写可用的 mention 链接,也能读懂 trigger_outcomes 里每一个状态码背后的判定顺序,避免把"折叠(coalesced)"误当成"提及失败"。

本文只讲后端层面一条 mention 链接意味着什么。是否应该提及(避免循环、对确认消息保持沉默)属于运行时指令(runtime brief)中 Mentions 章节的职责,不在本文范围内。

一、mention 链接由真实 UUID 构成

Multica 后端只通过唯一一种 Markdown 形态识别一条 mention:

@Label

其背后唯一的解析器是 mention.go 中导出的正则 util.MentionRe(第 16 行):

\[@?(.+?)\]\(mention://(member|agent|squad|issue|all)/([0-9a-fA-F-]+|all)\)

从中可以读出三条硬性约束:

  1. <type> 只接受四个值加一个哨兵memberagentsquadissue,以及字面量 all
  2. <id> 只接受十六进制字符与连字符[0-9a-fA-F-]+),或者字面量 all。这意味着典型的"人名"(含非十六进制字母)永远无法通过解析——链接目标必须是实体 UUID,绝不可能是显示名;
  3. 方括号里的 Label 是自由文本,采用非贪婪的 .+? 而不是排除 ] 的字符组,因此像 David[TF]Bot[v2][beta] 这类含方括号的标签也能被正确匹配(mention_test.go 的第 19-27 行用例专门钉死了这一行为)。@ 前缀是可选的,这是为了支持 issue 引用形如 MUL-123 的写法。

ParseMentions 会提取并去重 {Type, ID} 组合(同一 type:id 只保留一个),HasMentionAll 则报告是否存在 all 提及。上述四种类型 + @all 的完整形态全部由 Go 行为测试钉死——TestMentioningSkillTeachesTheParserContractbuiltin_skills_test.go)会把它们逐一喂给 util.ParseMentions:纯文本 @name 解析为空、真实 UUID 解析成功、@all 解析为 {all, all}、而用真实 UUID 搭配错误的 type 依然能解析成功——这正是"type 必须与 id 来源匹配"的原因。

唯一的例外:project 是纯渲染链接

Label 被刻意排除在解析器之外——project 不在上述 type 组中,因此后端永远不会解析它、也就永远无法入队任何任务。它的设计意图恰恰如此:项目引用永远不应触发一次运行。每个客户端都会把这种链接渲染成可点击对象:Web/桌面端渲染为 chip(RichLink,见 rich-content.tsx),移动端则是点击后打开项目的普通富链接。因此可以用它自由指向项目(相关用法见 multica-projects-and-resources 技能),而本文其余内容只讨论解析器真正认识的那四种类型(外加 all)。

二、Step 1:用 --output json 先查出 UUID

名字不是 UUID。写入链接之前必须先查表,从对应的 list 命令取 UUID:

目标 查询命令 取哪个字段
人(member) multica workspace member list --output json user_id
Agent multica agent list --output json id
Squad multica squad list --output json id

关键陷阱:人的 mention id 必须是 user_id,而不是成员关系行(membership row)的 id。后端自己的 roster 格式化函数在生成 mention 时就使用 user_id:见 squad_briefing.go 中"Mention syntax for humans uses the user_id (matches the rest of …)"的注释,以及 formatMention(user.Name, "member", userID) 的调用,formatMention 产出的正是 @<name> 形态(第 216-218 行附近)。CLI 侧的命令在 cmd_workspace.gocmd_agent.gocmd_squad.go 中注册,且 cmd_compat_test.go 显式回归验证了 workspace member list 命令仍然可用。

按显示名匹配。如果名字有歧义或查不到,不要猜——直接在评论里说明情况,而不是发出一个坏链接。

三、Step 2:四种类型各自入队了什么

格式:@Nametype 与 id 来源必须匹配,否则链接会解析到错误的实体(或什么都解析不到):

目的 type UUID 来源 后端行为
触发一个 Agent agent agent.id 为该 Agent 入队一次运行(EnqueueTaskForMention
把活交给一个 Squad squad squad.id 解析该 squad 的 leader_id,为 LEADER Agent 入队一次运行
链接一个人 member member.user_id 只渲染链接;不入队任何东西——不会启动 Agent 运行
引用一个 issue issue issue.id 只渲染链接;不入队任何东西——永远安全

源码印证:触发集如何计算

触发集由 computeMentionedAgentCommentTriggers(在 comment.goresolveMentionedAgentCommentTriggers 中体现,该函数名/职责与技能文档的描述一致)构建,评论路径把它折叠进 computeCommentAgentTriggerscomment.go),再由 enqueueCommentAgentTriggerscomment.go)入队。从源码可以清晰看到它只对两种类型采取行动:

  • squad 分支(comment.go):在工作区内解析该 squad → 读取 LeaderID → 把 leader 加入触发集 → 入队时走 leader 触发路径(对应 EnqueueTaskForSquadLeader 一类的 helper,源码中的注释写明 comment trigger source 为 MentionSquadLeader);
  • 之后 if m.Type != "agent" { continue }comment.go)会跳过所有非 agent 的提及;
  • agent 分支(comment.go):按工作区加载该 agent 并加入触发集。

因此一条 memberissue 提及两个分支都到不了,永远不会入队任务。补充一点去重细节:seen 映射按"解析后的执行 agent id"去重,多个提及(如 @Agent A 与 leader 同为 A 的 @Squad S)解析到同一执行 agent 时只入队一次任务;而 targets 按"用户点名的 type:id"逐条记录 outcome,保证每条显式提及即使运行被折叠也不会被静默吞掉。

需要特别说明的边界:本技能不声称 member 提及会通过 Go 评论 handler 投递通知——computeMentionedAgentCommentTriggers 只按 squad/agent 分支,对 comment.go 中 notif 的检索只能找到一个与"log spam"相关的无关注释。经验证的契约是:只有 agentsquad 提及会入队工作。

四、发布前预览与按评论抑制

较新客户端可以在创建或编辑评论之前调用 POST /api/issues/{id}/comments/trigger-preview(路由在 router.go 中注册)。预览端点与创建、编辑重触发共用同一个 computeCommentAgentTriggers,因此界面上展示的 agent chips 来自后端规则,而不是客户端另起炉灶的复刻。

  • 编辑预览传 editing_comment_id:服务端校验该评论属于同一工作区与同一 issue,校验或推导本次编辑的父评论上下文,并且只排除 trigger_comment_id 等于该评论本身的 pending 任务(走 agent.sql 中"exclude"变体查询,对应 helper 见 comment.gohasPendingTaskForIssueAndAgent)。同 issue 上来自其他评论的 pending 任务依然会对预览去重。原因在于:保存时先取消旧任务再重算触发集,所以被编辑的评论自己的历史任务不应让预览变空。它是"按评论作用域"的例外,不是绕开整个 agent 的旁路。
  • 创建/编辑时可选传 suppress_agent_ids:服务端仍然先计算完整触发集,然后把它作为后置过滤器移除这些 agent id。字段缺失或为空保持旧行为;传一个合法但不在触发集中的 UUID 是 no-op;格式非法的 UUID 会在请求边界被直接拒绝(comment.goparseUUIDSliceOrBadRequest 解析)。

对应的回归测试集中在 comment_trigger_preview_test.goTestPreviewCommentTriggers_EditExcludesSameCommentPendingTask(编辑预览排除同评论 pending 任务)、TestPreviewCommentTriggers_EditExclusionDoesNotIgnoreOtherCommentPendingTask(其他评论的 pending 任务仍去重预览)、TestPreviewCommentTriggers_ReturnsMentionedAgentsAndSuppressFiltersCreate(suppress 过滤)等。前端在 client.ts 中发送 editing_comment_idsuppress_agent_ids,编辑 UI(comment-card.tsx)会在预览时渲染触发 chips、记录被抑制的 agent,并在保存时一并提交。

五、@all 是广播类型

@all 使用字面量 all,从不使用 UUID:

@all

它面向 issue 上的所有人。它不会让任何特定 Agent 运行。它的特殊之处体现在触发时刻:一条携带 @all 的评论会被视为广播,从而抑制 issue 负责人的自动"评论即触发"(以及其他隐式路由兜底——thread parent / conversation owner)。用 @all 来公告,不要用 @all 向负责人要活。

@all 只抑制那些隐式路由。同一条评论中显式的 @agent / @squad 依然正常触发(MUL-5411):一条形如

@all heads up — @Preflight please take this

的评论只会入队 Preflight、不会入队其他人。显式提及优先于广播。源码依据在 comment.gocomputeCommentAgentTriggershasAgentOrSquadMention(mentions) 分支在 util.HasMentionAll(mentions) 短路之前被求值;且由于 all 既不是 agent 也不是 squad,它在 resolveMentionedAgentCommentTriggers 里同样被 if m.Type != "agent" { continue } 跳过,永远无法独自入队。测试 TestPreviewCommentTriggers_AllPlusExplicitAgentMentionStillTriggersAllPlusExplicitSquadMentionStillTriggersAllPlusMemberMentionStaysSuppressed(均在 comment_trigger_preview_test.go 中)分别验证了 @all 单独→0 个 agent、@all+@agent→仅该 agent、@all+@squad→leader、@all+@member→0 个 agent。

六、"什么不会发生":no-op、blocked、coalesced 与 deferred

下面这些情况都不会启动新运行,也都不会产生 error 响应——但它们是三种不同的事,且响应会告诉你属于哪一种。从未解析成功的提及是真正的静默 no-op;解析成功但被拒绝的会以 status: "blocked" + reason_code 的形式出现在 trigger_outcomes 里;目标正忙的会返回 coalesceddeferred。发帖后务必阅读这个数组——它是这些情况唯一会现身的地方。

1. 该放 UUID 的地方放了名字

mention://member/Alice 是死的。id 组只接受十六进制与连字符(或 all),普通名字里必然出现的非十六进制字母会让整个模式匹配失败,解析器返回空。无任何报错,也无人被触达。

2. 长得像 UUID 但是错的 UUID

一个格式良好却没有任何实体拥有的 UUID 能解析成功,然后在查找阶段 no-op:工作区作用域的查询找不到对应 agent,提及以 invocation_not_allowed 被标记为 blocked(见 comment.go,其中注释直言 "Do not reveal whether the id exists")。

这个错误码是刻意保持模糊的——打错的 UUID 与真正的权限拒绝看起来一模一样,因为你敲下的 id 可能指向另一个工作区的私有 agent,而 reason 绝不能证实它的存在。所以:看到 invocation_not_allowed 时,先对照线上名册(roster)核对 UUID,再去动任何可见性或调用设置(MUL-5548)。multica squad member list <squad-id> --output json 返回的 member_id 正是构造 mention 所用的字段。

反过来,一个匹配了模式却根本不是合法 UUID 的 id(如 mention://agent/-)会被带错误返回的 util.ParseUUID 拒绝(不是会 panic 的 Must 变体——评论文本不可信,后者会 panic 请求),并以 target_unavailable 被 block(comment.go)——非 UUID 在任何工作区都指不出实体,因此无需隐藏什么。两种情况都不会是 error 响应。测试 TestPreviewCommentTriggers_MalformedMentionIDDoesNotPanic 钉死了这一点。

3. 已存在 pending 任务

即使 @agent/@squad 完全正确,当目标在该 issue 上已有 pending 任务(hasPendingTaskForIssueAndAgentcomment.go)时也不会启动第二次运行。这是"折叠"而不是"丢弃":评论合并进那个任务,outcome 为 coalesced(同一个已评审 head)或 deferred(不同 head)——不要把它当成"提及没生效"而重发一遍。编辑预览是唯一例外:editing_comment_id 会忽略被编辑评论自身的 pending 任务(见本文第四节)。

4. 已归档的 agent / 未绑定 runtime 的 agent(或 leader 同样如此的 squad)

分别以 target_unavailableruntime_offline 被 block。两者都只在 invoke 门禁之后才检查,因此一个无权调用目标的调用者永远无法得知它的状态。

5. 无法调用的私有 agent

被 block。mention 路径对 @agent@squad 都门禁在 canInvokeAgent 上(comment.go 与第 3183 行)。这是**"运行"门禁,不是"看见"门禁**:自 MUL-3963 起,一个能在 UI 中打开私有 agent 的工作区管理员也不一定能触发它——能查看目标不等于能提及它。(顺带区分:canEnqueueSquadLeader 包装是 squad 指派/提升路径,不是本条 mention 路径;子任务完成的唤醒则不设门禁——见 multica-squads 技能。作为对照,canAccessPrivateAgent——"看见"门禁——定义在 agent_access.go,被刻意排除在 mention 路径之外。)

守卫顺序速查

守卫 Outcome
目标已归档 / 无 runtime 绑定 blocked target_unavailable / runtime_offline(invoke 门禁之后求值)
无法 invoke 的私有 agent / 私有 squad leader blocked invocation_not_allowed
格式良好但工作区内查无此 agent blocked invocation_not_allowed(与私有 agent 同码,防枚举)
非 UUID 的 id(mention://agent/- blocked target_unavailable(agent 与 squad 分支皆然)
目标已有该 issue 的 pending 任务 非丢弃:折叠进该任务 → coalesced / deferred(合并失败则 blocked

七、跨 issue 的链路与自动化委托的特殊规则

跨 issue 链路保留发起人(MUL-6490)

A2A(agent-to-agent)门禁判定的是你链路顶端的那个人,而那个人会跟着你写的评论走:评论记录下创作它的那次运行source_task_id),因此它唤醒的运行会继承你的 originator。这在你在不同于当前运行所在的 issue 上评论时依然成立——也就是常见的"先创建 issue Y,再到那边协调"流程——所以在自己 issue 上奏效的委托,在你新建的 issue 上继续奏效。反向则不成立:没有任何机制会替换成另一个不同的人(你的 agent 的 owner,或目标 issue 的 originator)。因此如果你的链路顶端没有人,member 作用域的 allow-list 无论你移到哪个 issue 都会保持关闭。

无归属 autopilot 运行的委托(MUL-4857)

当一个无归属(unattributed)的 autopilot 运行(定时调度/webhook 分发没有人形 originator,A2A 门禁无人可依)在其创建的 issue 上通过 @mention 委托时,invoke 门禁回退到 autopilot creator 作为有效调用用户——即最初放行该分发的同一主体。于是运行中途的 @agent/@squad 委托,恰好在 autopilot creator 本可以调用该目标时触发(owner / public_to 匹配),否则保持跳过。注意这只是授权——被入队运行的 originator/归属不变。

这一回退绑定在可验证的任务血缘上(见 comment.go 关于 effectiveInvoker 的注释,以及 agent_access.goautopilotDelegationAuthority 一族 helper):它在"正在委托的运行自身就是处理该 autopilot issue 的那个任务"(author == task agent、task.issue_id == 本 issue)时适用,因此在别处工作的运行永远无法通过在该 issue 上评论来借用另一个 autopilot creator 的权限。同一权威也承载在普通的"指派 squad leader 唤醒"上(worker 在 autopilot issue 上的结果评论仍可唤醒 leader),并且在目标繁忙时依然存活:若被提及的 agent 正在运行,委托会以同一权威在该运行完成时重放,因此永远不会丢失

编辑被视为全新的动作——它会从编辑动作重新推导评论血缘。只有 agent 作者编辑自己的评论才把血缘重新盖到当前编辑任务上;任何其他编辑者——包括以 owner/admin 身份编辑 agent 评论的工作区所有者/管理员——都会清除它。因此,编辑一条来自无关 issue 的旧 autopilot 评论,或管理员编辑 agent 的评论(拥有的是 manage 权限而非 invoke 权限),会在延后完成对账(deferred completion-reconcile)时以"关闭即失败"结束,而不是复用原运行权威。

八、错误写法 → 正确写法

错误:@alice please review → 纯文本,没有链接,解析为空,无人被触达。

错误:@Alice please review → "Alice"不是 UUID;id 组拒绝非十六进制字母,模式不匹配,链接静默死亡。

正确:

  1. multica workspace member list --output json → 得到 Alice 的 user_id = 7f3a…
  2. @Alice please review → 真实的 user_id 通过解析;链接渲染并解析到 Alice。

@all 广播:@all heads up——面向所有人、不运行任何特定 agent、并抑制 assignee 自动触发。

这些精确形态由 TestMentioningSkillTeachesTheParserContractbuiltin_skills_test.go)通过 util.ParseMentions 逐一钉死:名字形态解析为空、真实 UUID 形态可解析、@all 解析为 {all, all}、用真实 UUID 搭配错误 type 依然能解析(这正是 type 必须与 id 来源匹配的原因)。

参考资料与源码路径

提示:mentioning-source-map.md 中给出的行号是针对某一时刻树的指针,源码持续演进可能导致行号漂移(例如 resolveMentionedAgentCommentTriggers 已移动到 comment.go 更靠后的位置)。行为契约才是稳定的——如果 SKILL.md 所述行为与现状不一致,请回到 source map 重新核对。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
918
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.6 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
517
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389