Corsair Jira 插件接入指南:32 个类型化接口、本地数据同步与 Webhook 事件
【免费下载链接】corsairConnect your users to their apps项目地址: https://gitcode.com/GitHub_Trending/corsa/corsair
导读
@corsair-dev/jira是 Corsair 生态中的 Jira 官方插件,它把 Jira Cloud 的 REST API v3 与 Agile REST API v1.0 封装成 32 个类型安全的jira.api.*操作、6 张本地同步数据表与 3 种入站 Webhook 事件,让 AI Agent 可以直接通过一个客户端完成"查问题、改状态、建项目、排迭代、回评论"等完整工作流。读完本文,你将掌握该插件的安装、认证、端点调用、本地数据查询与 Webhook 接入的全套实战方法,并理解其底层 HTTP 客户端与错误处理机制。
插件概览:一个 Jira 连接层
@corsair-dev/jira位于仓库 packages/jira 目录,是 Corsair 标准插件形态的参考实现之一。从源码结构看,插件由四层组成(见 packages/jira/index.ts):
- 端点层:
endpoints/目录按业务域拆分为 issues.ts、comments.ts、projects.ts、sprints.ts、users.ts,覆盖问题、评论、项目、迭代、用户与用户组六大域; - 客户端层:client.ts 封装 Jira REST API v3 与 Agile API v1.0 两类请求通道,以及附件上传;
- 数据模型层:schema/database.ts 定义了 6 个可本地同步的实体(
boards、comments、issues、projects、sprints、users); - Webhook 层:webhooks/ 提供 3 种事件的匹配、HMAC 签名校验与多租户识别。
所有端点的输入/输出均通过 Zod 模式定义于 endpoints/types.ts,并在插件注册时暴露为endpointSchemas,这是"类型安全"的来源——调用方与响应方共用同一套模式,非法字段在编译期即被拦截。
安装与环境要求
根据 packages/jira/README.md,使用 pnpm 安装:
pnpm add @corsair-dev/jira也可以使用你习惯的包管理器(npm / yarn / bun)同时安装corsair与插件本体:
npm install corsair @corsair-dev/jira插件在 packages/jira/package.json 中声明了两个 peer 依赖:
| 依赖 | 版本要求 | 用途 |
|---|---|---|
corsair | >=0.1.0 | 提供插件运行时、HTTP 请求、数据库与 Webhook 基础设施 |
zod | ^4.1.13 | 端点输入/输出与 Webhook 负载的模式校验 |
当前仓库中该插件版本为0.1.5,类型入口为./dist/index.d.ts,产物为 ESM 格式。
快速接入:注册插件与连接租户
在 docs/plugins/jira/overview.mdx 中给出了标准的接入流程。首先创建一个corsair.ts并注册插件:
import Database from 'better-sqlite3'; import { createCorsair } from 'corsair'; import { jira } from '@corsair-dev/jira'; export const corsair = createCorsair({ plugins: [ jira(), ], database: new Database('corsair.db'), kek: process.env.CORSAIR_KEK!, hub: { projectApiKey: process.env.CORSAIR_API_KEY!, signingSecret: process.env.CORSAIR_SIGNING_SECRET!, }, });Corsair 默认支持多租户(multi-tenancy),用corsair.withTenant(id)隔离不同用户/Jira 站点的凭据与数据。接入方通过 Hub 生成 connect 链接,让租户在浏览器中完成授权,并把结果回传给应用:
const { connectUrl } = await corsair.manage.connect.createLink({ plugin: 'jira', tenantId: 'acme', }); // redirect the user's browser to connectUrl首次调用时,Corsair 会提示租户提供 API Key(详见下文认证章节)。关于 KEK、Hub 密钥与租户隔离的细节,可分别参考 docs/getting-started/set-up-with-your-agent.mdx 与 docs/concepts/multi-tenancy.mdx。
插件工厂选项
jira()工厂函数接受 JiraPluginOptions 配置:
| 选项 | 类型 | 说明 |
|---|---|---|
authType | 'api_key' | 认证方式,默认为api_key |
key | string | 全局 API Key,优先级高于数据库中的租户凭据 |
webhookSecret | string | 全局 Webhook 签名密钥(也可由租户单独存储) |
cloudUrl | string | Jira Cloud 地址,如https://your-domain.atlassian.net,所有 API 调用必需 |
hooks | object | 端点执行前/后的生命周期钩子 |
webhookHooks | object | Webhook 处理前/后的钩子 |
errorHandlers | CorsairErrorHandler | 自定义错误处理器,覆盖插件默认行为 |
permissions | PluginPermissionsConfig | 权限配置,使用 Jira 端点树的点分路径,非法路径会在编译期报错 |
认证:API Key 与租户凭据
packages/jira/README.md 明确:Auth: API key,Corsair 会在租户首次使用时提示输入凭据。Jira Cloud 使用 Basic Auth,即邮箱 + API Token的组合(email:apiToken),客户端会将其 Base64 编码后放入Authorization头(见 client.ts)。
获取并存储凭据
根据 docs/plugins/jira/get-credentials.mdx:
- 登录 Atlassian 账号,进入Security → API tokens页面;
- 点击Create API token,命名(如 "Corsair Integration")后创建;
- 立即复制 Token(只显示一次),妥善保存。
随后通过 Corsair CLI 存储为租户凭据:
pnpm corsair setup --plugin=jira api_key=your-api-tokenJira Cloud URL(如https://yourcompany.atlassian.net)会作为独立账号字段cloud_url存储。这一点在 index.ts 的jiraAuthConfig中有专门设计——它在基础api_key配置上扩展了account: ['cloud_url']字段,使云端地址可以通过corsair auth动态设置,而无需硬编码在插件选项里。
keyBuilder 的取 key 优先级
从 index.ts 的keyBuilder实现可以梳理出凭据解析顺序:
- Webhook 来源:优先使用
options.webhookSecret;否则从ctx.keys.get_webhook_signature()读取租户的webhook_signature,缺失则抛出[auth-missing:jira:webhook_signature]错误; - 端点来源:优先使用
options.key(全局配置);否则从ctx.keys.get_api_key()读取租户api_key,缺失则抛出AuthMissingError('jira', 'api_key')。
每个端点处理器还会通过ctx.keys.get_cloud_url()获取目标站点地址,二者共同构成一次完整调用的认证上下文。
OAuth 配置说明
尽管文档推荐并使用 API Key,源码中的oauthConfig仍内置了 Atlassian OAuth 端点与授权范围(read:jira-work、write:jira-work、read:jira-user、offline_access,audience 为api.atlassian.com)。可以推断这是为未来或自托管场景预留的 OAuth 能力;当前默认与文档化认证方式以 API Key 为准。
端点总览:32 个类型化操作
下表完整列出了插件暴露的全部操作(来自 packages/jira/README.md,同时与 index.ts 中的jiraEndpointsNested、jiraEndpointMeta一致)。操作 ID 为jira.api.<operation>的完整路径,风险级别分为read、write、destructive三档:
| Operation | Operation ID | Risk | Description |
|---|---|---|---|
comments.add | jira.api.comments.add | write | Add a comment to a Jira issue |
comments.delete | jira.api.comments.delete | destructive | Delete a comment from a Jira issue [DESTRUCTIVE] |
comments.get | jira.api.comments.get | read | Get a specific comment on a Jira issue |
comments.list | jira.api.comments.list | read | List all comments on a Jira issue |
comments.update | jira.api.comments.update | write | Update a comment on a Jira issue |
groups.create | jira.api.groups.create | write | Create a new Jira group |
groups.getAll | jira.api.groups.getAll | read | Get all Jira groups |
issues.addAttachment | jira.api.issues.addAttachment | write | Add an attachment to a Jira issue |
issues.addWatcher | jira.api.issues.addWatcher | write | Add a watcher to a Jira issue |
issues.assign | jira.api.issues.assign | write | Assign a Jira issue to a user |
issues.bulkCreate | jira.api.issues.bulkCreate | write | Bulk create multiple Jira issues |
issues.bulkFetch | jira.api.issues.bulkFetch | read | Bulk fetch multiple Jira issues by ID or key |
issues.create | jira.api.issues.create | write | Create a new Jira issue |
issues.delete | jira.api.issues.delete | destructive | Delete a Jira issue [DESTRUCTIVE] |
issues.edit | jira.api.issues.edit | write | Edit an existing Jira issue |
issues.get | jira.api.issues.get | read | Get a Jira issue by ID or key |
issues.getTransitions | jira.api.issues.getTransitions | read | Get available transitions for a Jira issue |
issues.linkIssues | jira.api.issues.linkIssues | write | Link two Jira issues together |
issues.removeWatcher | jira.api.issues.removeWatcher | write | Remove a watcher from a Jira issue |
issues.search | jira.api.issues.search | read | Search issues using JQL |
issues.transition | jira.api.issues.transition | write | Transition a Jira issue to a new status |
projects.create | jira.api.projects.create | write | Create a new Jira project |
projects.get | jira.api.projects.get | read | Get a Jira project by ID or key |
projects.getRoles | jira.api.projects.getRoles | read | Get project roles for a Jira project |
projects.list | jira.api.projects.list | read | List Jira projects |
sprints.create | jira.api.sprints.create | write | Create a new sprint on a Jira board |
sprints.list | jira.api.sprints.list | read | List sprints for a Jira board |
sprints.listBoards | jira.api.sprints.listBoards | read | List Jira boards |
sprints.moveIssues | jira.api.sprints.moveIssues | write | Move issues to a sprint |
users.find | jira.api.users.find | read | Search for Jira users |
users.getAll | jira.api.users.getAll | read | Get all Jira users |
users.getCurrent | jira.api.users.getCurrent | read | Get the currently authenticated Jira user |
每个操作的完整输入/输出字段、类型与必填性,均可在 docs/plugins/jira/api.mdx 中查阅(该页由插件 Zod 模式生成)。
核心操作实战
以下示例均基于corsair.withTenant('acme')获取的租户实例调用,参数采用 Zod 模式中的snake_case命名(见 endpoints/types.ts)。
问题(Issues)域
创建问题——最小参数为project_key与summary,其余可选:
const tenant = corsair.withTenant('acme'); await tenant.jira.api.issues.create({ project_key: 'DEMO', summary: 'Fix login redirect bug', issue_type: 'Task', // 默认为 'Task' description: 'Users are redirected to /home instead of /dashboard', assignee: '712020:abc123', // accountId priority: 'High', labels: ['frontend', 'auth'], due_date: '2026-09-30', });从 issues.ts 的实现可见,插件会把输入映射为 Jira REST v3 的fields结构,其中description会通过makeAdf()自动包装为 Atlassian Document Format(ADF),即{ version: 1, type: 'doc', content: [{ type: 'paragraph', content: [{ type: 'text', text }] }] }——你无需关心 ADF 细节,传普通字符串即可。
查询与搜索:
// 按 ID 或 Key 获取 await tenant.jira.api.issues.get({ issue_id_or_key: 'DEMO-42', fields: 'summary,status,assignee', expand: 'renderedFields', }); // JQL 搜索 await tenant.jira.api.issues.search({ jql: 'project = DEMO AND status = "In Progress" ORDER BY updated DESC', start_at: 0, max_results: 50, });状态流转——通常先取可用流转,再执行:
const { transitions } = await tenant.jira.api.issues.getTransitions({ issue_id_or_key: 'DEMO-42', }); await tenant.jira.api.issues.transition({ issue_id_or_key: 'DEMO-42', transition_id: transitions![0].id!, comment: 'Moving to Done per sprint review', });transition支持附带评论,插件会将其同样包装为 ADF 追加到变更记录中。
批量操作:
await tenant.jira.api.issues.bulkCreate({ issues: [ { project_key: 'DEMO', summary: 'Issue A', issue_type: 'Task' }, { project_key: 'DEMO', summary: 'Issue B', priority: 'High' }, ], }); await tenant.jira.api.issues.bulkFetch({ issue_ids_or_keys: ['DEMO-1', 'DEMO-2'], fields: ['summary', 'status'], });附件上传——两种方式二选一:file_content(Base64 内容)或file_url(远程地址),Zod 模式通过refine强制至少提供其一:
// 方式一:Base64 内容 await tenant.jira.api.issues.addAttachment({ issue_id_or_key: 'DEMO-42', file_name: 'screenshot.png', file_content: 'iVBORw0KGgoAAAANSUhEUg...', mime_type: 'image/png', }); // 方式二:远程 URL await tenant.jira.api.issues.addAttachment({ issue_id_or_key: 'DEMO-42', file_name: 'design.pdf', file_url: 'https://cdn.example.com/design.pdf', });评论(Comments)域
// 添加评论(支持 visibility 限制到角色/群组) await tenant.jira.api.comments.add({ issue_id_or_key: 'DEMO-42', comment: 'Fixed in build #1201, please verify', visibility_type: 'role', visibility_value: 'Developers', }); // 分页列出评论 await tenant.jira.api.comments.list({ issue_id_or_key: 'DEMO-42', start_at: 0, max_results: 20, order_by: '-created', });项目、迭代、用户与用户组
// 创建项目(默认 software 类型、UNASSIGNED 指派策略) await tenant.jira.api.projects.create({ key: 'DEMO', name: 'Demo Project', project_type_key: 'software', description: 'Demo project for the AI assistant', }); // 列出项目 / 获取角色 await tenant.jira.api.projects.list({ query: 'demo', max_results: 10 }); await tenant.jira.api.projects.getRoles({ project_id_or_key: 'DEMO' }); // 迭代域(走 Agile API v1.0) const { values: boards } = await tenant.jira.api.sprints.listBoards({ project_key_or_id: 'DEMO', }); await tenant.jira.api.sprints.create({ origin_board_id: boards![0]!.id!, name: 'Sprint 24', goal: 'Ship onboarding v2', }); await tenant.jira.api.sprints.moveIssues({ sprint_id: 124, issue_keys: ['DEMO-42', 'DEMO-43'], }); // 用户域 await tenant.jira.api.users.getCurrent({}); await tenant.jira.api.users.find({ query: 'alice', max_results: 5 }); // 用户组域 await tenant.jira.api.groups.getAll({}); await tenant.jira.api.groups.create({ name: 'support-team' });注意:项目、迭代、用户等端点同样会在成功后把返回实体写入本地数据库(详见"本地数据同步"章节),实现"远端操作 + 本地可查询"的双通道一致性。
源码级原理:HTTP 客户端与错误处理
双 API 通道
client.ts 提供了两个请求入口:
makeJiraRequest:基础路径为${cloudUrl}/rest/api/3,服务所有非迭代端点;makeJiraAgileRequest:基础路径为${cloudUrl}/rest/agile/1.0,服务看板/迭代端点(sprints.*)。
两者统一采用 Basic Auth(Basic base64(email:apiToken)),cloudUrl会先经过sanitizeCloudUrl去除尾部斜杠再拼接。GET/DELETE 支持 query 参数,POST/PUT/PATCH 携带 JSON body。
限流与重试
插件内置了 Jira 限流配置JIRA_RATE_LIMIT_CONFIG:启用限流、最多重试 3 次、初始退避 1 秒、退避倍率 2,并读取Retry-After响应头。当请求经过corsair/http的request()时,该配置会被传入以驱动自动重试。
附件上传的特殊处理
uploadJiraAttachment不使用 JSON,而是构造multipart/form-data:
- 若提供
file_url,先fetch拉取内容,mime 类型从响应头自动识别; - 否则把
file_content(Base64)解码为 Buffer; - 请求携带
X-Atlassian-Token: no-check头(Jira 附件接口的防 CSRF 要求),且故意不设置 Content-Type,由 fetch 自动生成 multipart boundary。
分层错误处理
error-handlers.ts 定义了四个错误处理器,与corsair/http的ApiError协同:
| 处理器 | 匹配条件 | 行为 |
|---|---|---|
RATE_LIMIT_ERROR | HTTP 429 或消息含rate_limit/ratelimited/429 | 最多重试 5 次,尊重Retry-After |
AUTH_ERROR | HTTP 401 或unauthorized/authentication failed/invalid_auth | 不重试,提示检查email:apiToken格式 |
PERMISSION_ERROR | HTTP 403 或permission_denied/forbidden/access_denied | 不重试,记录告警 |
DEFAULT | 兜底 | 记录错误,不重试 |
你可以在jira({ errorHandlers: ... })中覆盖或扩展这些默认行为(index.ts中通过{ ...errorHandlers, ...options.errorHandlers }合并)。
本地数据同步:6 个可搜索实体
插件会把调用结果与 Webhook 事件持续同步到本地数据库。6 个实体及其 Zod 模型定义在 schema/database.ts:
| 实体 | 主要字段 |
|---|---|
boards | id(number)、name、type、projectId、projectKey、projectName |
comments | id、issueKey、body、authorAccountId、authorDisplayName、created、updated |
issues | id、key、summary、description、status、assignee*、reporter*、priority、issueType、projectKey、projectId、labels |
projects | id、key、name、description、projectTypeKey、leadAccountId、leadDisplayName |
sprints | id(number)、name、state、goal、startDate、endDate、originBoardId |
users | accountId、displayName、emailAddress、active、timeZone、locale |
同步写入通过ctx.db.<entity>.upsertByEntityId(...)完成,例如 issues.ts 在create、get、search、bulkCreate、bulkFetch成功后都会落库;Webhook 处理器也会在收到事件时更新对应行(见 webhooks/new-issue.ts)。edit、assign等只持有issue_id_or_key(可能是 Key 而非数字 ID)的操作会刻意跳过落库,交由下一次issues.get刷新数据——源码注释中明确记录了这一设计取舍。
查询本地数据
const rows = await corsair.jira.db.issues.search({ data: { status: 'In Progress', projectKey: 'DEMO' }, limit: 100, offset: 0, });每个实体的可过滤字段与操作符各不相同,docs/plugins/jira/database.mdx 给出了完整矩阵,规律如下:
- 字符串字段:支持
equals、contains、startsWith、endsWith、in; - 数字字段(如
boards.id、sprints.id、sprints.originBoardId):支持equals、gt、gte、lt、lte、in; createdAt日期字段:支持equals、before、after、between;users.active布尔字段仅支持equals。
所有.search()均接受limit与offset做分页。
Webhook:3 种事件与签名验证
packages/jira/README.md 说明插件处理 3 种 Webhook 事件,映射关系见 index.ts 的jiraWebhooksNested:
| 事件路径 | Webhook Event 值 | 触发时机 |
|---|---|---|
issues.newIssue | jira:issue_created | 新建问题 |
issues.updatedIssue | jira:issue_updated | 问题被更新(含 changelog 变更明细) |
projects.newProject | project_created | 新建项目 |
接收 Webhook 的 HTTP Handler
把 Jira 的订阅 URL 指向你的 Corsair HTTP 处理端点(Next.js 路由示例来自 docs/plugins/jira/webhooks.mdx):
import { processWebhook } from 'corsair'; import { corsair } from '@/server/corsair'; export async function POST(request: Request) { const headers = Object.fromEntries(request.headers); const body = await request.json(); const result = await processWebhook(corsair, headers, body); return result.response; }匹配与签名验证
- 事件匹配:
createJiraMatch(webhookEvent)(webhooks/types.ts)解析请求体并比对webhookEvent字段;插件级匹配器则检查请求头中是否含x-atlassian-webhook-identifier(index.ts 的pluginWebhookMatcher)。 - 签名验证:
verifyJiraWebhookSignature使用 HMAC-SHA256 校验x-hub-signature头(格式为sha256=<hash>),并用crypto.timingSafeEqual做常数时间比较,防止时序攻击。签名密钥来自options.webhookSecret或租户的webhook_signature。验证失败时返回 HTTP 401。 - 多租户识别:tenant-matcher.ts 从负载的
issue.self、issue.fields.project.self、project.self、user.self中提取站点 host,得到cloud_url后按此维度路由到对应租户。
使用 webhookHooks 处理事件
jira({ webhookHooks: { issues: { newIssue: { before(ctx, args) { // 事件处理前:记录、鉴权、去重 return { ctx, args }; }, after(ctx, response) { // 事件处理后:通知、审计 }, }, updatedIssue: { before(ctx, args) { return { ctx, args }; }, after(ctx, response) {}, }, }, projects: { newProject: { before(ctx, args) { return { ctx, args }; }, after(ctx, response) {}, }, }, }, })各事件的完整负载结构(issue、user、changelog、project的嵌套类型)可在 docs/plugins/jira/webhooks.mdx 查阅,Zod 模式定义见 webhooks/types.ts。收到事件后,插件默认行为是同步对应实体到本地数据库(如newIssue会 upsert 问题与用户行),随后触发after钩子。
权限配置:限制 Agent 能做什么
JiraPluginOptions.permissions用于控制 AI Agent 允许执行的操作,采用点分路径引用端点树(如'issues.delete'、'comments.delete'),路径非法时会产生编译期类型错误:
jira({ permissions: { 'issues.delete': false, // 禁止删除问题 'issues.transition': true, // 允许状态流转 'comments.add': true, // 允许添加评论 }, })该机制与 docs/concepts/permissions.mdx 中的权限模型一致,配合端点元数据中的riskLevel(read/write/destructive),可对不同风险等级的操作做精细化放行。
测试与验证
插件自带两套测试,可作为验证行为与理解调用链的参考:
- api.test.ts:针对真实 Jira Cloud 的集成型类型测试,通过环境变量
JIRA_API_KEY、JIRA_CLOUD_URL驱动,覆盖users、projects、issues、comments等域,每个响应都会用JiraEndpointOutputSchemas.*.parse()做运行时校验; - client.test.ts 与 webhooks/types.test.ts:客户端与 Webhook 类型层面的测试。
本地运行测试与构建:
pnpm --filter @corsair-dev/jira test pnpm --filter @corsair-dev/jira build # tsc --build --force && tsup许可与参考
插件以Apache-2.0许可发布(见 packages/jira/README.md 与 packages/jira/package.json)。更完整的参考文档(含每个端点的输入/输出类型、数据库过滤操作符、Webhook 负载示例)位于 docs/plugins/jira 目录:
- overview.mdx:接入总览与快速开始
- api.mdx:全部
jira.api.*操作与类型参考 - database.mdx:同步实体与搜索过滤操作符
- webhooks.mdx:事件路径、负载与
webhookHooks示例 - get-credentials.mdx:API Token 与 Webhook 密钥获取步骤
若要将这些操作暴露给 AI 助手调用,可参考 docs/mcp-adapters/mcp-adapters.mdx 的 MCP 适配方案,把插件能力直接映射为 MCP 工具。
【免费下载链接】corsairConnect your users to their apps项目地址: https://gitcode.com/GitHub_Trending/corsa/corsair
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考