作者:Andrea Griffiths
排版:Alan Wang
学习如何将 Copilot SDK 集成到 React Native 应用中,以生成由 AI 驱动的 issue 摘要,并采用适用于生产环境的优雅降级和缓存模式。

在这里插入图片描述

Copilot SDK 让你可以将驱动 Copilot Chat 的同款 AI 添加到你自己的应用中。我想看看它在实际中的效果如何,于是我构建了一个名为 IssueCrush 的 issue 分类应用。下面是我的一些经验,以及你可以如何开始。

如果你曾维护过开源项目,或者在一个拥有活跃仓库的团队中工作过,你一定熟悉这种感觉。你打开 GitHub,看到通知角标显示:47 个 issues。其中一些是 bug,一些是功能请求,一些其实应该放到讨论区,还有一些是三年前就已出现的重复 issue。

对 issue 进行分类所带来的精力消耗是真实存在的。每一个 issue 都需要上下文切换:阅读标题、浏览描述、检查标签、思考优先级、决定如何处理。当多个仓库中有数十个 issue 需要逐一处理时,你的大脑很快就会不堪重负。

我想让这个过程变得更高效。借助 GitHub Copilot SDK,我找到了实现的方法。

IssueCrush 登场:右滑即处理

IssueCrush 将你的 GitHub issues 以可滑动卡片的形式展示。向左滑表示关闭,向右滑表示保留。当你点击 “Get AI Summary” 时,Copilot 会读取该 issue,并告诉你它的内容以及应该如何处理。这样一来,维护者无需逐条阅读冗长的描述,就能获得即时、可操作的上下文,从而更快地做出分类决策。下面是我如何集成 GitHub Copilot SDK 来实现这一点的。

在这里插入图片描述

架构挑战

第一个技术决策是弄清楚在哪里运行 Copilot SDK。React Native 应用无法直接使用 Node.js 包,而 Copilot SDK 需要 Node.js 运行时。在内部,SDK 会管理一个本地的 Copilot CLI 进程,并通过 JSON-RPC 与其通信。由于依赖 CLI 可执行文件和 Node 环境,这种集成必须在服务端运行,而不能直接在 React Native 应用中运行。这意味着服务器必须安装 Copilot CLI,并且该命令需要在系统的 PATH 中可用。

我最终选择了一种服务端集成模式:
在这里插入图片描述

以下是这种架构之所以有效的原因:

  • 在所有客户端之间共享单个 SDK 实例,这样就不需要为每个移动客户端都创建一个新的连接。服务器为所有请求管理一个实例。开销更低、认证握手更少、清理也更简单。

  • 在服务器端存储 Copilot 认证密钥,以确保凭证安全。你的 API token 永远不会接触客户端。它们保存在服务器上——这是它们应在的位置,而不是放在一个可以被反编译的 React Native 包中。

  • 当 AI 不可用时具备优雅降级能力,这样即使 Copilot 服务宕机或超时,你仍然可以对 issues 进行分类。应用会回退到一个基础摘要。AI 可以让分类更快,但不应该成为单点故障。

  • 对请求进行日志记录以便调试和监控,因为每一次提示和响应都会经过你的服务器。你可以跟踪延迟、捕获失败,并调试提示问题,而无需在移动客户端中额外添加监控代码。

在构建类似系统之前,你需要:

  1. 在你的服务器上安装 Copilot CLI

  2. GitHub Copilot 订阅服务,或使用自有 API 密钥的 BYOK 配置

  3. 完成 Copilot CLI 的认证。在服务器上运行 copilot auth,或者设置 COPILOT_GITHUB_TOKEN 环境变量。

如何实现 Copilot SDK 集成

Copilot SDK 使用基于会话的模型。你需要先启动一个客户端(这会拉起 CLI 进程),创建一个会话,发送消息,然后进行清理。

const { CopilotClient, approveAll } = await import('@github/copilot-sdk');
 
let client = null; 
let session = null; 
 
try { 
   // 1. Initialize the client (spawns Copilot CLI in server mode) 
   client = new CopilotClient(); 
   await client.start(); 
 
  // 2. Create a session with your preferred model 
  session = await client.createSession({
  model: 'gpt-4.1',
  onPermissionRequest: approveAll,
});
 
  // 3. Send your prompt and wait for response 
   const response = await session.sendAndWait({ prompt }); 
 
  // 4. Extract the content 
   if (response && response.data && response.data.content) { 
     const summary = response.data.content; 
     // Use the summary... 
   } 
 
} finally { 
   // 5. Always clean up 
   if (session) await session.disconnect().catch(() => {}); 
   if (client) await client.stop().catch(() => {}); 
}

关键 SDK 模式

生命周期管理

SDK 遵循严格的生命周期:start() → createSession() → sendAndWait() → disconnect() → stop()

这里有一个我吃过亏才学到的教训:如果没有正确清理会话,会导致资源泄漏。我花了两个小时排查内存问题,最后才发现自己忘了调用disconnect()。因此务必将所有会话交互都包裹在 try/finally 语句中。在清理操作上使用 .catch(() => {}) 可以避免清理过程中的错误覆盖原始错误。

用于分类的提示工程

提示词结构为模型提供了充足的上下文信息,使其能够完成任务。我会提供与 issue 相关的结构化信息,而非直接堆砌原始文本:

const prompt = `You are analyzing a GitHub issue to help a developer quickly understand it and decide how to handle it. 
 
Issue Details: 
- Title: ${issue.title} 
- Number: #${issue.number} 
- Repository: ${issue.repository?.full_name || 'Unknown'} 
- State: ${issue.state} 
- Labels: ${issue.labels?.length ? issue.labels.map(l => l.name).join(', ') : 'None'} 
- Created: ${issue.created_at} 
- Author: ${issue.user?.login || 'Unknown'} 
 
Issue Body: 
${issue.body || 'No description provided.'} 
 
Provide a concise 2-3 sentence summary that: 
1. Explains what the issue is about 
2. Identifies the key problem or request 
3. Suggests a recommended action (e.g., "needs investigation", "ready to implement", "assign to backend team", "close as duplicate") 
 
Keep it clear, actionable, and helpful for quick triage. No markdown formatting.`;

标签和作者相关信息的重要性远超你的想象。首次贡献者提出的 issue,其处理方式与核心维护者提出的 issue 截然不同,而 AI 会利用这些信息来调整其生成的摘要内容。

响应处理

sendAndWait() 方法会在会话进入空闲状态后返回助手的响应。在访问嵌套属性之前,务必验证响应链是否存在。

const response = await session.sendAndWait({ prompt }, 30000); // 30 second timeout 
 
let summary; 
if (response && response.data && response.data.content) { 
   summary = response.data.content; 
} else { 
   throw new Error('No content received from Copilot'); 
}

sendAndWait() 的第二个参数是超时时间(毫秒)。我们需要将其设置得足够长以处理复杂 issue,但又不能太长,以免用户一直盯着加载状态。我见过太多 “undefined is not an object” 的错误,所以我可以肯定:永远不要省略对响应链的空值校验。

客户端服务层

在 React Native 端,我将 API 调用封装在一个服务类中,用于处理初始化和错误状态:

// src/lib/copilotService.ts 
import type { GitHubIssue } from '../api/github'; 
import { getToken } from './tokenStorage'; 
export interface SummaryResult { 
   summary: string; 
   fallback?: boolean; 
   requiresCopilot?: boolean; 
} 
 
export class CopilotService { 
   private backendUrl = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:3000'; 
 
  async initialize(): Promise<{ copilotMode: string }> { 
     try { 
       const response = await fetch(`${this.backendUrl}/health`); 
       const data = await response.json(); 
 
      console.log('Backend health check:', data); 
       return { copilotMode: data.copilotMode || 'unknown' }; 
     } catch (error) { 
       console.error('Failed to connect to backend:', error); 
       throw new Error('Backend server not available'); 
     } 
   } 
 
  async summarizeIssue(issue: GitHubIssue): Promise<SummaryResult> { 
     try { 
       const token = await getToken(); 
 
      if (!token) { 
         throw new Error('No GitHub token available'); 
       } 
 
      const response = await fetch(`${this.backendUrl}/api/ai-summary`, { 
         method: 'POST', 
         headers: { 
           'Content-Type': 'application/json', 
         }, 
         body: JSON.stringify({ issue, token }), 
       }); 
 
      const data = await response.json(); 
 
      if (!response.ok) { 
         if (response.status === 403 && data.requiresCopilot) { 
           return { 
             summary: data.message || 'AI summaries require a GitHub Copilot subscription.', 
             requiresCopilot: true, 
           }; 
         } 
         throw new Error(data.error || 'Failed to generate summary'); 
       } 
 
      return { 
         summary: data.summary || 'Unable to generate summary', 
         fallback: data.fallback || false, 
       }; 
     } catch (error) { 
       console.error('Copilot summarization error:', error); 
       throw error; 
     } 
   } 
} 
 
export const copilotService = new CopilotService();

React Native 集成

UI 使用的是非常直接的 React 状态管理。点击按钮,调用服务,并缓存结果:

const [loadingAiSummary, setLoadingAiSummary] = useState(false); 
 
const handleGetAiSummary = async () => { 
   const issue = issues[currentIndex]; 
   if (!issue || issue.aiSummary) return; 
 
  setLoadingAiSummary(true); 
   try { 
     const result = await copilotService.summarizeIssue(issue); 
     setIssues(prevIssues => 
       prevIssues.map((item, index) => 
         index === currentIndex ? { ...item, aiSummary: result.summary } : item 
       ) 
     ); 
   } catch (error) { 
     console.error('AI Summary error:', error); 
   } finally { 
     setLoadingAiSummary(false); 
   } 
};

一旦 issue 对象上存在摘要,卡片就会将按钮替换为摘要文本。如果用户滑走后再回来,缓存的版本会立即渲染出来,无需再次发起 API 请求。

优雅降级

AI 服务可能会失败。网络问题、速率限制以及服务中断都会发生。服务器处理两种失败模式:订阅错误会返回 403 状态码,从而让客户端展示清晰的提示信息;而其他所有情况都会回退到基于 issue 元数据生成的摘要。

} catch (error) { 
   // Clean up on error 
   try { 
     if (session) await session.disconnect().catch(() => {}); 
     if (client) await client.stop().catch(() => {}); 
   } catch (cleanupError) { 
     // Ignore cleanup errors 
   } 
 
  const errorMessage = error.message.toLowerCase(); 
 
  // Copilot subscription errors get a clear 403 
   if (errorMessage.includes('unauthorized') || 
       errorMessage.includes('forbidden') || 
       errorMessage.includes('copilot') || 
       errorMessage.includes('subscription')) { 
     return res.status(403).json({ 
       error: 'Copilot access required', 
       message: 'AI summaries require a GitHub Copilot subscription.', 
       requiresCopilot: true 
     }); 
   } 
 
  // Everything else falls back to a metadata-based summary 
   const fallbackSummary = generateFallbackSummary(issue); 
   res.json({ summary: fallbackSummary, fallback: true }); 
}

回退机制会基于我们已有的信息生成一个有用的摘要:

function generateFallbackSummary(issue) { 
   const parts = [issue.title]; 
 
  if (issue.labels?.length) { 
     parts.push(`\nLabels: ${issue.labels.map(l => l.name).join(', ')}`); 
   } 
 
  if (issue.body) { 
     const firstSentence = issue.body.split(/[.!?]\s/)[0]; 
     if (firstSentence && firstSentence.length < 200) { 
       parts.push(`\n\n${firstSentence}.`); 
     } 
   } 
 
  parts.push('\n\nReview the full issue details to determine next steps.'); 
   return parts.join(''); 
}

其他值得注意的模式

服务器提供了一个 /health 端点,用于指示 AI 是否可用。客户端在启动时会检查该端点,如果后端无法支持 AI 功能,就会直接隐藏摘要按钮,避免出现不可用的按钮。

摘要是按需生成的,而不是预先生成的。这样可以降低 API 成本,并避免用户只是快速滑过 issue 时产生不必要的调用浪费。

SDK 使用 await import('@github/copilot-sdk') 动态加载,而不是在顶层使用 require。这样即使 SDK 本身存在问题,服务器也能正常启动,从而让部署和调试更加顺畅。

依赖项

{ 
   "dependencies": { 
     "@github/copilot-sdk": "^0.1.14", 
     "express": "^5.2.1" 
   } 
}

SDK 通过 JSON-RPC 与 Copilot CLI 进程进行通信。你需要在系统中安装 Copilot CLI,并确保它可以在 PATH 环境变量中被访问。请查看 SDK 的包依赖要求,以确认所需的最低 Node.js 版本。

我在构建这个项目时学到的

**服务端是正确的选择。**SDK 依赖 Copilot CLI 二进制文件,而你不可能在手机上安装它。把它运行在服务端可以将 AI 逻辑集中管理,简化移动端客户端,同时确保凭证永远不会离开后端。

**提示词结构比提示词长度更重要。**向模型提供结构化的元数据(例如标题、标签、作者)比直接把整个 issue 内容当作原始文本效果更好。给模型合适的输入,它才能返回更有价值的结果。

**永远要有回退机制。**AI 服务可能会宕机,也可能遇到速率限制。从一开始就设计优雅降级机制。即使 AI 部分不可用,用户仍然应该能够继续处理 issue。

**一定要清理会话。**SDK 要求显式清理:先 disconnect(),再 stop()。我曾经漏掉一次 disconnect(),结果花了两个小时排查内存泄漏。每次都要用 try/finally

**缓存结果。**一旦生成摘要,就把它存到 issue 对象上。如果用户滑走再回来,可以立即显示缓存内容。没有第二次 API 调用,没有额外成本,也没有延迟。

**AI 可以让维护工作变得可持续。**issue 分类是一种“隐形工作”,非常消耗精力。它不会被感谢,但会不断堆积。如果能把处理 50 个 issue 的时间减半,就意味着可以把时间投入到代码审查、指导他人,或者至少不用再害怕通知数量。Copilot SDK 只是一个工具,但更重要的是这个思路:找出那些让你疲惫的维护工作,然后问问 AI 是否可以先做第一步处理。

尝试一下

@github/copilot-sdk 为构建智能开发者工具提供了真正的可能性。结合 React Native 的跨平台能力,你可以以一种原生且流畅的方式,将 AI 工作流带到移动端。

如果你正在构建类似的东西,可以从我这里介绍的服务端模式开始。这是最简单的可行路径,而且可以随着应用规模扩展。源代码已发布在 GitHub:AndreaGriffiths11/IssueCrush

开始使用 Copilot SDK,看看你还能构建什么。入门指南会带你用大约五行代码完成第一次集成。如果你有反馈或想法,可以加入 SDK 讨论区一起交流。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐