Linear
快速开始
通过 Linear 蓝图,将经过验证的 Linear 资源和智能体会话 webhook 以及项目自有的出站 Linear API 访问添加到现有 Flue 项目中。在终端或你选择的编码智能体中运行以下命令:
flue add channel linear
概述
该蓝图安装 @flue/linear 和官方 @linear/sdk,在源码根目录创建 channels/linear.ts 模块,包含命名的 channel 和项目自有的 client 导出,并修改选定的智能体以绑定生成的消息工具。
import { createLinearChannel } from '@flue/linear';
import { dispatch } from '@flue/runtime';
import { LinearClient } from '@linear/sdk';
import { Assistant } from '../agents/assistant.ts';
export const client = new LinearClient({
apiKey: process.env.LINEAR_API_KEY!,
});
export const channel = createLinearChannel({
webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,
async webhook({ payload, deliveryId }) {
if (payload.type !== 'Comment' || !('body' in payload.data)) return;
const comment = payload.data;
if (payload.action !== 'create' || !comment.issueId) return;
await dispatch(Assistant, {
id: channel.instanceId({
type: 'issue',
organizationId: payload.organizationId,
issueId: comment.issueId,
...(comment.parentId ? { threadCommentId: comment.parentId } : {}),
}),
message: {
kind: 'signal',
type: 'linear.comment.created',
body: comment.body,
attributes: {
deliveryId,
...(payload.actor ? { actorId: payload.actor.id } : {}),
...(payload.actor && 'name' in payload.actor ? { actorName: payload.actor.name } : {}),
},
},
});
},
});节选示例展示了生成的评论路径,省略了智能体会话分支、可复用的类型守卫和消息工具。配置完成后,新的 issue 评论会继续该 issue 或评论线程的智能体实例,而绑定的 SDK 工具会向同一 Linear 对话发布回复。官方 SDK 还支持生成的智能体会话路径,并在 Cloudflare Workers 上使用 Flue 的 nodejs_compat 设置运行。
挂载渠道
渠道仅在 app.ts 挂载的位置提供 HTTP 路由。挂载模块中命名导出的 channel:
import { channel as linear } from './channels/linear.ts';
app.route('/channels/linear', linear.route());channel.route() 是一个纯路由器工厂,相对于挂载路径提供渠道声明的路由。本指南中的 webhook 路径假定使用常规的 /channels/linear 挂载;不同的挂载路径会相应调整。分发目标智能体模块携带 'use agent' 指令——该指令负责注册,因此仅用于分发的智能体不需要自己的 HTTP 挂载。
配置
| 变量 | 用途 |
|---|---|
LINEAR_WEBHOOK_SECRET | 必需 — 验证入站 webhook 交付。 |
LINEAR_API_KEY | 必需 — 认证示例的出站 SDK 客户端。 |
LINEAR_ORGANIZATION_ID | 可选 — 将入站交付限制到一个 Linear 组织。 |
LINEAR_WEBHOOK_ID | 可选 — 将入站交付限制到一个配置的 webhook。 |
它安装 @flue/linear 用于经过验证的入口,以及官方 @linear/sdk 用于项目自有的出站 API 访问。Linear 在其自己的 Cloudflare Workers 智能体示例中使用该 SDK 和 nodejs_compat,Flue 的 Cloudflare 目标已经启用了该设置。
将 webhook URL 设置为:
https://example.com/channels/linear/webhook
渠道模块
import { createLinearChannel, type LinearWebhookPayload } from '@flue/linear';
import { defineTool, dispatch } from '@flue/runtime';
import { LinearClient } from '@linear/sdk';
import * as v from 'valibot';
import type {
AgentSessionEventWebhookPayload,
EntityWebhookPayloadWithCommentData,
} from '@linear/sdk/webhooks';
import { Assistant } from '../agents/assistant.ts';
const organizationId = process.env.LINEAR_ORGANIZATION_ID;
export const client = new LinearClient({
apiKey: process.env.LINEAR_API_KEY!,
});
export const channel = createLinearChannel({
webhookSecret: process.env.LINEAR_WEBHOOK_SECRET!,
...(organizationId ? { organizationId } : {}),
// 路径:/channels/linear/webhook
async webhook({ payload, deliveryId }) {
if (isCommentEvent(payload)) {
const comment = payload.data;
if (payload.action !== 'create' || !comment.issueId) return;
await dispatch(Assistant, {
id: channel.instanceId({
type: 'issue',
organizationId: payload.organizationId,
issueId: comment.issueId,
...(comment.parentId ? { threadCommentId: comment.parentId } : {}),
}),
// 在此事件创建实例时记录一次;之后忽略。
initialData: {
type: 'issue',
issueId: comment.issueId,
...(comment.parentId ? { threadCommentId: comment.parentId } : {}),
...(comment.issue?.title ? { issueTitle: comment.issue.title } : {}),
},
message: {
kind: 'signal',
type: 'linear.comment.created',
body: comment.body,
attributes: {
deliveryId,
...(payload.actor ? { actorId: payload.actor.id } : {}),
...(payload.actor && 'name' in payload.actor ? { actorName: payload.actor.name } : {}),
},
},
});
return;
}
if (isAgentSessionEvent(payload)) {
await dispatch(Assistant, {
id: channel.instanceId({
type: 'agent-session',
organizationId: payload.organizationId,
agentSessionId: payload.agentSession.id,
}),
// 在此事件创建实例时记录一次;之后忽略。
initialData: {
type: 'agent-session',
agentSessionId: payload.agentSession.id,
...(payload.agentSession.issue?.title
? { issueTitle: payload.agentSession.issue.title }
: {}),
},
message: {
kind: 'signal',
type: `linear.agent_session.${payload.action}`,
body: JSON.stringify({
promptContext: payload.promptContext,
activity: payload.agentActivity,
session: payload.agentSession,
}),
attributes: { deliveryId },
},
});
}
},
});
// Linear 的原生联合类型有一个 catch-all 成员,使 `type` 保持宽泛,因此仅使用
// 字面量 `type` 检查无法缩窄。将其与嵌套字段组合使用。
function isCommentEvent(
payload: LinearWebhookPayload,
): payload is EntityWebhookPayloadWithCommentData {
return payload.type === 'Comment' && 'body' in payload.data;
}
function isAgentSessionEvent(
payload: LinearWebhookPayload,
): payload is AgentSessionEventWebhookPayload {
return payload.type === 'AgentSessionEvent' && 'agentSession' in payload;
}
/** 实际发布消息所需的 `LinearConversationRef` 子集。 */
export type LinearMessageRef =
| { type: 'agent-session'; agentSessionId: string }
| { type: 'issue'; issueId: string; threadCommentId?: string };
export function postMessage(ref: LinearMessageRef) {
return defineTool({
name: 'post_linear_message',
description: '向绑定到此智能体的 Linear 对话发布消息。',
input: v.object({ text: v.pipe(v.string(), v.minLength(1)) }),
async run({ data }) {
const { text } = data;
if (ref.type === 'agent-session') {
const result = await client.createAgentActivity({
agentSessionId: ref.agentSessionId,
content: { type: 'response', body: text },
});
return { output: { success: result.success } };
}
const result = await client.createComment({
issueId: ref.issueId,
...(ref.threadCommentId ? { parentId: ref.threadCommentId } : {}),
body: text,
});
return { output: { commentId: result.id } };
},
});
}每个经过验证的非 ping 交付都会以其原生的 Linear webhook 负载转发。使用 payload.type 和嵌套字段进行缩窄。Linear 的主题目录广泛,因此渠道保持 payload 开放,仅在需要时验证字段。选择对哪些事件采取行动是应用策略——在 Linear 中配置 webhook,并在处理程序中根据 payload.type 和 payload.action 进行分支。Linear ping 在内部确认,永远不会到达回调。
绑定工具
'use agent';
import { useInitialData, useModel, useTool } from '@flue/runtime';
import * as v from 'valibot';
import { postMessage } from '../channels/linear.ts';
const initialData = v.object({
type: v.union([v.literal('issue'), v.literal('agent-session')]),
issueId: v.optional(v.string()),
agentSessionId: v.optional(v.string()),
threadCommentId: v.optional(v.string()),
});
export function Assistant() {
useModel('anthropic/claude-haiku-4-5');
const data = useInitialData<v.InferOutput<typeof initialData>>();
if (!data) throw new Error('此智能体由 Linear 渠道分发创建。');
useTool(postMessage(data));
return '审查入站的 Linear 评论,并在适当时发布简洁的回复。';
}
Assistant.initialData = initialData;模型选择消息内容。它不选择任意的 Linear 对话、凭据或 API 方法。此工具创建普通的评论或智能体活动。
交付和运行时行为
Linear 交付是至少一次的。该渠道是无状态的,不会对交付 ID 进行去重。在不允许重复接收时,在分发之前在应用自有的持久存储中声明 deliveryId。
该包在 Node 和 Cloudflare Workers 上运行,需要 Flue 的 nodejs_compat 设置。