首页
/ Linear × Claude Managed Agents:用无状态 Webhook 桥接实现「@提及即可召唤 Claude 回帖」的完整工程实践

Linear × Claude Managed Agents:用无状态 Webhook 桥接实现「@提及即可召唤 Claude 回帖」的完整工程实践

2026-09-07 21:11:55作者:郁楠烈Hubert

导读

本指南围绕 claude-cookbooks 仓库中 managed_agents/linear 示例,深入讲解如何把 Claude Managed Agent(CMA)接入 Linear 议题系统:当用户在任何 Linear Issue 里 @mention 智能体时,系统会在 Anthropic 托管的基础设施上运行 Claude 会话,并把最终回答以评论形式回贴到对应议题。文章核心剖析这座「桥」的完整数据流——Linear AgentSessionEvent → CMA 会话(携带路由 metadata)→ session.status_idled Webhook → createAgentActivity 回贴——同时给出环境变量、签名密钥、事件去重、10 秒 ack 规则等全部陷阱与本地调试清单,让读者能够自己跑通并扩展这条链路。

为什么需要一座「桥」

核心矛盾:Linear 的 Agent Platform 与 Anthropic 的 CMA 并不共享线格式(wire format)与凭证体系。

  • 入向:Linear 通过自己的 AgentSessionEvent Webhook 通知你「有人 @了智能体」,payload 携带 agentSession.idorganizationId 与议题上下文;
  • 出向:Anthropic 通过 session.status_idled Webhook 通知你「CMA 会话已空闲」,payload 只携带 CMA 会话 ID
  • 二者之间没有任何一方知道对方的"回调地址",也没有一方直接携带对方的输出。

因此必然需要一个翻译层:在入口把 "Linear @mention" 翻译成 sessions.create + user.message,在出口把 "CMA idle" 翻译成 Linear 评论,并同时持有两套密钥。这就是 src/ 中这座 stateless 桥的全部职责。

从仓库的目录结构与注释可以推断,这套实现刻意保持"薄":桥本身不保存任何业务数据,运行期会话路由完全依赖 CMA session 上的 metadatalinear_session_idlinear_org_id),这使得它天然适合部署成无状态函数或临时进程。

核心心智模型:Webhook 是门铃,不是快递

一个 Webhook 只是"某件事发生时调用我"

它是某个服务向你预先注册的 URL 发起的 HTTP POST,请求体是一个描述事件的小 JSON。注册一次 URL,之后每次事件触发都会收到回调:无轮询、无长连接。桥涉及两条 Webhook、一个中间层

  1. Linear → 桥@mention 时触发,payload 含 agentSession.idorganizationId 与议题上下文;
  2. Anthropic → 桥:会话空闲时触发,payload 仅含 CMA session ID。

两者都不携带"回调 URL",也不携带 Agent 的最终输出,本质都是"带 ID 的信号"。

Push 信号,Pull 数据

Anthropic 的 session.status_idled payload 被刻意做得极薄(形如 {type, id})。正确的做法永远是随后调用 sessions.retrieve(id) 取 metadata、调用 sessions.events.list(id) 取输出——推信号、拉数据。这样重试成本极低,数据也永远不会过期。

metadata 就是全部路由状态

当桥创建 CMA 会话时写入 metadata: {linear_session_id, linear_org_id}(见 src/agent.ts);当 idle Webhook 只带 session ID 到达时,桥 sessions.retrieve 读回这两个键,就知道该回贴到哪个 Linear 议题,无需在桥内存储任何会话状态——这正是 stateless 的关键设计。

文件职责:读懂一座桥的解剖图

文件 职责
setup/create-agent.ts 一次性任务:agents.create + environments.create,打印 ID 供写入 .env.local
src/main.ts Bun 服务器与路由注册,启动前做环境变量强校验
src/oauth.ts Linear OAuth(actor=app)+ 基于文件的 token 存储与自动刷新
src/agent.ts 收到 Linear 事件后:10 秒内 ack → sessions.create + user.message
src/cma-webhook.ts 验签 → 事件去重 → retrieve 过滤 → 读回复 → createAgentActivity 回贴
skill.md 完整设置清单、全部已知坑位与调试对照表
CLAUDE.md 面向智能体的操作手册:先读 skill、再跑、按需扩展

项目依赖见 package.json:核心是 @anthropic-ai/sdk(≥ 0.95.1,提供 beta.sessionsbeta.webhooks.unwrap 等 API)与 @linear/sdk(提供 LinearClientLinearWebhookClient 等),脚本仅有三个:bun run dev(watch 模式)、bun run startbun run setup

服务器骨架:四个路由与启动即校验

src/main.ts 启动时先对四个必需环境变量做强校验,任一缺失即 FATAL 退出:

LINEAR_WEBHOOK_SIGNING_SECRET
ANTHROPIC_WEBHOOK_SIGNING_KEY
CLAUDE_AGENT_ID
CLAUDE_ENVIRONMENT_ID

随后注册四个路由:

  • GET /:健康检查,返回 {status: "ok"}
  • GET /oauth/authorize:跳转 Linear OAuth 授权页(安装 Agent 的入口);
  • GET /oauth/callback:接收授权码、换 token、登记组织;
  • POST /linear-webhook:交给 LinearWebhookClient 的 handler,订阅 AgentSessionEvent
  • POST /cma-webhook:交给 handleCmaWebhook 处理 Anthropic 的会话状态事件。

其中 LinearWebhookClient(来自 @linear/sdk/webhooks)内置了对 Linear Webhook 请求体的验签与事件解析,事件回调里会收到完整的 AgentSessionEvent,随即调用 kickoffAgentSession(fire-and-forget 方式,异常仅打印日志,不阻塞 HTTP 响应)。

Linear 侧接入:OAuth、app user 与 10 秒 ack

actor=app 是承重墙参数

要创建"会出现在 @-picker 里"的 app user,必须在 OAuth 授权时带上 actor=app 与足够的 scope。见 src/oauth.ts

const params = new URLSearchParams({
  client_id: LINEAR_CLIENT_ID,
  redirect_uri: REDIRECT_URI,
  response_type: "code",
  scope: "read,write,app:assignable,app:mentionable",
  actor: "app",
});
return Response.redirect(`https://linear.app/oauth/authorize?${params}`);

个人 LINEAR_API_KEY 无法完成这件事——桥必须以 app 身份(通过 OAuth token)回贴。handleOAuthCallback 用授权码换取 token 后,还会查询 organization { id name },以组织 ID 为主键把 token 存入本地文件 .linear-tokens.json,完成"安装到某个工作区"的登记。

刷新逻辑:提前 5 分钟续期

getAccessToken(orgId)src/oauth.ts)在 token 距过期不足 5 分钟时自动用 refresh_token 换新并落盘,调用方无感知。需要注意 .linear-tokens.json 是本地文件方案,生产环境应替换为真正的密钥存储(见下文生产化)。

10 秒内先回一个 thought

Linear 要求在收到 AgentSessionEvent10 秒内至少写入一条 agentActivity,否则会话被标记失败。因此 src/agent.ts 在创建 CMA 会话之前,先同步回贴:

await linear.createAgentActivity({
  agentSessionId: agentSession.id,
  content: { type: "thought", body: "Thinking..." },
});

这让 Linear 界面立即显示"Thinking…",给后续耗时较长的 CMA 启动留出余量。

入向翻译:创建 CMA 会话并绑定路由元数据

sessions.create + user.message

const session = await anthropic.beta.sessions.create({
  agent: CLAUDE_AGENT_ID,
  environment_id: CLAUDE_ENVIRONMENT_ID,
  metadata: {
    linear_session_id: agentSession.id,
    linear_org_id: organizationId,
  },
});

await anthropic.beta.sessions.events.send(session.id, {
  events: [{ type: "user.message", content: [{ type: "text", text: buildPrompt(event) }] }],
});

session 上挂的 metadata 是整个路由系统的唯一事实来源——这是"stateless"得以成立的前提。随后用 events.send 注入 user.message,Claude 即开始在 Anthropic 基础设施上运行。

prompt 的组装策略

buildPromptsrc/agent.ts)优先使用 Linear 事件自带的 promptContext(若存在),否则拼装议题(identifier + title + description)、历史评论、以及本次 @ 消息,最后兜底为 "Hello! How can I help?"。这一兜底逻辑说明:即便某次事件缺失上下文,Agent 也不会空转。

出向回贴:验签、去重、过滤、拉取、回复

cma-webhook.ts 是返回路径的全部实现,也是坑位最密集的地方。

1. 验签并解包

event = anthropic.beta.webhooks.unwrap(rawBody, {
  headers: Object.fromEntries(req.headers),
});

unwrap 同时完成 HMAC 验签、时间戳校验与事件解析,读取环境变量 ANTHROPIC_WEBHOOK_SIGNING_KEY。验签失败返回 401。代码注释特别提醒:unwrap() 需要普通 header map,而非 fetch 的 Headers 对象。

2. 用 event.id 做幂等去重

Anthropic 对投递失败的请求会用相同的顶层 event.id 重试。代码用内存 Set<string> 去重:

if (seenEventIds.has(event.id)) return new Response(null, { status: 204 });
seenEventIds.add(event.id);

已处理或决定忽略的事件都应返回 2xx,否则会持续触发重试——约 20 次连续失败会自动禁用该 endpoint(详见 skill.md 的说明)。

3. 区分终止与空闲

session.status_terminatedpostTerminationError,向 Linear 回贴一条 {type: "error", body: "Agent session terminated unexpectedly."}session.status_idled 才进入回复流程;其余事件一律 204 忽略。

4. retrieve-then-filter:工作区共享 Webhook 的必修课

Anthropic 的 Webhook 是工作区级(workspace-scoped)的:Console 里注册的 endpoint 会收到该工作区内所有会话的事件,包括别人的 Agent、脚本或团队成员触发的会话。因此必须检索会话并按 metadata 过滤:

let session;
try {
  session = await anthropic.beta.sessions.retrieve(claudeSessionId);
} catch {
  return new Response(null, { status: 204 });
}

const linearSessionId = session.metadata?.linear_session_id;
const linearOrgId = session.metadata?.linear_org_id;
if (!linearSessionId || !linearOrgId) {
  return new Response(null, { status: 204 });
}

retrieve 抛 404/403 同样要捕获静默返回——同一工作区里由其他 API key 创建的会话,你的 key 本就不可读。这正是 skill.md 强调的 "retrieve-then-filter" 模式:先取数、再判断是不是自己的活。

5. 拉取回复并回贴

从事件历史中收集所有 agent.message 的文本块(对 sessions.events.list 返回的分页对象做 for await 迭代即自动翻页):

for await (const e of anthropic.beta.sessions.events.list(claudeSessionId)) {
  if (e.type === "agent.message") {
    for (const block of e.content ?? []) {
      if (block.type === "text") parts.push(block.text);
    }
  }
}

reply is empty 的常见根因之一正是分页:Agent 产出大量事件时列表需要翻页,迭代分页对象即可解决。收集到文本后:

await linear.createAgentActivity({
  agentSessionId: linearSessionId,
  content: { type: "response", body: responseText },
});

回复以 response 类型活动回贴,用户即可在 Linear 议题评论中看到 Claude 的最终答案。整个过程对无回复的会话以 204 静默收尾。

一次性初始化:agent 与 environment

bun run setup 执行 setup/create-agent.ts,一次性创建运行环境与 Agent:

const env = await anthropic.beta.environments.create({
  name: `linear-bridge-${Date.now()}`,
  config: { type: "cloud", networking: { type: "unrestricted" } },
});

const agent = await anthropic.beta.agents.create({
  name: "Linear Assistant",
  model: "claude-opus-4-7",
  system:
    "You are a helpful assistant embedded in Linear. Keep replies concise and actionable — they are posted as comments. Do not invent issue IDs, users, or project names.",
  tools: [{ type: "agent_toolset_20260401", default_config: { enabled: true } }],
});

值得注意的两点:environment 显式选择 type: "cloud"(即由 Anthropic 托管运行)与无限制网络;agent 的 system prompt 明确约束"回答会被贴成评论,保持简洁可执行,不得编造议题 ID/用户/项目名"——这是针对 Linear 场景的重要护栏。脚本最后打印两个 ID,复制进 .env.local 即可。

一键跑通:从安装到第一次 @ 提及

README.md 的 Quickstart 与 skill.md 的本地开发清单,最小落地路径为:

cd managed_agents/linear
bun install
claude          # 让 Claude 依据 CLAUDE.md / skill.md 引导你完成后续配置

随后在 Claude Code 中直接说 "walk me through setting this up",它会按 skill 中"实际能跑通的顺序"驱动你完成:Linear OAuth 应用 → Anthropic Agent + Webhook → env vars → bun run dev

如果不用 Claude 引导、完全手动,则依次执行:

  1. ngrok http 3000(或 cloudflared tunnel)得到公网 URL,后续所有地址都基于它;
  2. bun run setup,把 CLAUDE_AGENT_ID / CLAUDE_ENVIRONMENT_ID 复制进 .env.local
  3. Linear OAuth 应用linear.app/<workspace>/settings/api → 侧栏 Administration → API → OAuth Applications → Create new):Developer URL 填任意真实 https:// URL(仅展示用途,不影响功能);Callback 填 <url>/oauth/callback;Webhook 填 <url>/linear-webhook,事件选 Agent session events;复制 client ID/secret 与 webhook secret;
  4. Anthropic Console → Webhooks:填 <url>/cma-webhook,事件仅订阅 session.status_idledsession.status_terminated,复制 whsec_...务必与你的 API key 处于同一工作区
  5. 填好 .env.localbun run dev
  6. 浏览器访问 <url>/oauth/authorize → 同意授权 → 显示 "Agent installed.";
  7. 在某个 Issue 中 @ 它。

.env.local 中所需的完整变量(结合 main.tsoauth.ts 的读取点整理)包括:LINEAR_WEBHOOK_SIGNING_SECRETANTHROPIC_WEBHOOK_SIGNING_KEYCLAUDE_AGENT_IDCLAUDE_ENVIRONMENT_IDLINEAR_CLIENT_IDLINEAR_CLIENT_SECRETBASE_URL(可选,默认 http://localhost:3000)、PORT(可选,默认 3000)。

坑位清单:把容易浪费调试时间的地方一次说清

skill.md 花了大量篇幅记录"文档里没写、却极耗调试时间"的坑位,逐条对照如下:

  • Anthropic Webhook 是工作区级的:注册在 Console 的 endpoint 只收到同一工作区内会话的事件。若 API key 属于工作区 A、endpoint 注册在工作区 B,则会静默地一条都收不到。核对 API key 上的 Workspace 列与 Console Webhooks 页的工作区选择器一致。
  • 工作区 Webhook 会收到该工作区每一个会话:共享工作区下,每个 session.status_idled 都会打到你这里,handler 必须用 retrieve + metadata 过滤,无关事件回 204;retrieve 的 404/403 也要捕获。
  • 只订阅你需要的类型:宁可只订阅 session.status_idledsession.status_terminated,也不要选 "All events"。
  • Linear OAuth 应用仅限工作区管理员:非管理员可开一个免费个人工作区做测试,避免把实验性 Agent 装进生产。
  • 签名头名不一致:文档写 X-Webhook-Signature,实际线上走的是 Webhook-Signature / Webhook-Id / Webhook-Timestamp(Standard Webhooks 规范)。用 SDK 的 webhooks.unwrap() 即可自动处理,只有手工验签时才需要在意。
  • event.id 是幂等键:见上文去重逻辑。
  • metadata 缺失即忽略:别人工作区共享 Webhook 打到你的 endpoint、或会话不可读时,一律 204,不产生任何副作用。

静默失败的调试对照表

skill.md 给出了三条高发故障的快速定位路径:

现象 排查方向
Linear 里 "Thinking…" 从未出现 Linear Webhook 没到你这里:检查 Linear app 的 webhook URL、ngrok 是否存活
"Thinking…" 出现但没有回复 curl localhost:4040/api/requests/http 看 ngrok 请求日志;若无 POST /cma-webhook → Anthropic 侧工作区不匹配或 endpoint 未保存;若 POST 到达但返回 401 → 签名密钥不匹配
回复内容为空 sessions.events.list 可能分页,Agent 事件多时需迭代完整分页对象

扩展方向:从"只回帖"到"能干活"

基础桥跑通之后,CLAUDE.md 列出六个可选扩展(具体字段形状需查阅 Managed Agents API 参考):

  • GitHub 仓库:通过 sessions.create 上的 resources: [{type: "github_repository", ...}] 把仓库挂载进会话容器;
  • MCP 工具:例如 Linear 或 GitHub 的 MCP,让 Agent 不仅能回复、还能执行动作——在 agent 上配 mcp_servers + mcp_toolset,凭证经 vault 提供,会话上挂 vault_ids
  • Outcomes(结果评分):把 user.message 换成 user.define_outcome 事件,走 rubric 评分的迭代循环;
  • Multiagent(多智能体):在 agent 上配置 multiagent: {type: "coordinator", agents: [...]},形成协调者 + 子智能体阵容;
  • Memory store:通过 resources: [{type: "memory_store", ...}] 实现跨会话持久记忆;
  • 自定义工具:经 agent.custom_tool_use / user.custom_tool_result 由宿主机侧执行。

需要改动的位置集中在 setup/create-agent.tssrc/agent.ts 两处。由于这套桥高度 stateless,扩展大多只涉及请求体字段的增改,不动链路骨架。

生产化:从本地玩具到可上线服务

skill.md 与 CLAUDE.md 共同给出了四点收敛方向,原则是"链路不变、替换薄弱组件":

  • 用 Cloudflare Workers、Fly 等真实部署替换 ngrok,其余逻辑不变;
  • 用 Redis 或数据库替换内存 seenEventIds Set,实现多实例下的幂等去重;
  • 用真正的密钥存储替换 .linear-tokens.json 文件;
  • 把 Anthropic endpoint 的事件订阅收窄到与你实际处理的事件一致(session.status_idledsession.status_terminated)。

小结

Linear × Claude Managed Agents 桥的工程价值在于它示范了一种可复用的双向 Webhook 集成范式:用 session metadata 保存路由状态换取无状态实现,用 "push 信号、pull 数据" 换取可靠且廉价的回调处理,用 event.id 幂等换取安全的自动重试。这套「翻译层」模式同样适用于 Slack、GitHub Issues 或其他 IM/工单系统——仓库中 slack 目录即存在同构示例可供对照。若要深入理解每条 API 的精确字段,建议在实际开发时调用 Managed Agents 的完整 API 参考作为唯一事实来源,不要凭记忆猜测字段名。

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

项目优选

收起
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