从零构建 GitHub Issues 集成:HagiCode 的前端直连实践
本文记录了在 HagiCode 平台中集成 GitHub Issues 的全过程。我们将探讨如何通过"前端直连 + 后端最小化"的架构,在保持后端轻量的同时,实现安全的 OAuth 认证与高效的 Issues 同步。
背景:为什么要集成 GitHub?
HagiCode 作为一个 AI 辅助开发平台,核心价值在于连接想法与实现。但在实际使用中,我们发现用户在 HagiCode 中完成了 Proposal(提案)后,往往需要手动将内容复制到 GitHub Issues 中进行项目跟踪。
这带来了几个明显的痛点:
- 工作流割裂:用户需要在两个系统之间来回切换,体验不仅不流畅,还容易导致关键信息在复制粘贴的过程中丢失。
- 协作不便:团队其他成员习惯在 GitHub 上查看任务,无法直接看到 HagiCode 中的提案进展。
- 重复劳动:每当提案更新,就要人工去 GitHub 更新对应的 Issue,增加不必要的维护成本。
为了解决这个问题,我们决定引入GitHub Issues Integration功能,打通 HagiCode 会话与 GitHub 仓库的连接,实现"一键同步"。
关于 HagiCode
嘿,介绍一下我们正在做的东西
我们正在开发HagiCode—— 一款 AI 驱动的代码智能助手,让开发体验变得更智能、更便捷、更有趣。
智能—— AI 全程辅助,从想法到代码,让编码效率提升数倍。便捷—— 多线程并发操作,充分利用资源,开发流程顺畅无阻。有趣—— 游戏化机制和成就系统,让编码不再枯燥,充满成就感。
项目正在快速迭代中,如果你对技术写作、知识管理或者 AI 辅助开发感兴趣,欢迎来 GitHub 看看~
技术选型:前端直连 vs 后端代理
在设计集成方案时,摆在我们面前的有两条路:传统的"后端代理模式"和更激进的"前端直连模式"。
方案对比
在传统的后端代理模式中,前端所有的请求都要先经过我们的后端,再由后端去调用 GitHub API。这虽然逻辑集中,但给后端带来了不小的负担:
- 后端臃肿:需要编写专门的 GitHub API 客户端封装,还要处理 OAuth 的复杂状态机。
- Token 风险:用户的 GitHub Token 必须存储在后端数据库中,虽然可以加密,但毕竟增加了安全风险面。
- 开发成本:需要数据库迁移来存储 Token,还需要维护一套额外的同步服务。
而前端直连模式则要轻量得多。在这个方案中,我们只利用后端来处理最敏感的"密钥交换"环节(OAuth callback),获取到 Token 后,直接存在浏览器的 localStorage 里。后续创建 Issue、更新评论等操作,直接由前端发 HTTP 请求到 GitHub。
| 对比维度 | 后端代理模式 | 前端直连模式 |
|---|---|---|
| 后端复杂度 | 需要完整的 OAuth 服务和 GitHub API 客户端 | 仅需一个 OAuth 回调端点 |
| Token 管理 | 需加密存储在数据库,有泄露风险 | 存储在浏览器,仅用户自己可见 |
| 实施成本 | 需数据库迁移、多服务开发 | 主要是前端工作量 |
| 用户体验 | 逻辑统一,但服务器延迟可能稍高 | 响应极快,直接与 GitHub 交互 |
考虑到我们要的是快速集成和最小化后端改动,最终我们采用了"前端直连模式"。这就像给浏览器发了一张"临时通行证",拿到证之后,浏览器就可以自己去 GitHub 办事了,不需要每次都找后端管理员批准。
核心设计:数据流与安全
在确定架构后,我们需要设计具体的数据流。整个同步流程的核心在于如何安全地获取 Token 并高效地利用它。
整体架构图
整个系统可以抽象为三个角色:浏览器(前端)、HagiCode 后端、GitHub。
/* by yours.tools - online tools website : yours.tools/zh/unicode.html */ +--------------+ +--------------+ +--------------+ | 前端 React | | 后端 | | GitHub | | | | ASP.NET | | REST API | | +--------+ | | | | | | | OAuth |--+--------> /callback | | | | | 流程 | | | | | | | +--------+ | | | | | | | | | | | | +--------+ | | +--------+ | | +--------+ | | |GitHub | +------------>Session | +----------> Issues | | | |API | | | |Metadata| | | | | | | |直连 | | | +--------+ | | +--------+ | | +--------+ | | | | | +--------------+ +--------------+ +--------------+关键点在于:只有 OAuth 的一小步(获取 code 换 token)需要经过后端,之后的粗活累活(创建 Issue)都是前端直接跟 GitHub 打交道。
同步数据流详解
当用户点击 HagiCode 界面上的"Sync to GitHub"按钮时,会发生一系列复杂的动作:
/* by yours.tools - online tools website : yours.tools/zh/unicode.html */ 用户点击 "Sync to GitHub" │ ▼ 1. 前端检查 localStorage 获取 GitHub Token │ ▼ 2. 格式化 Issue 内容(将 Proposal 转换为 Markdown) │ ▼ 3. 前端直接调用 GitHub API 创建/更新 Issue │ ▼ 4. 调用 HagiCode 后端 API 更新 Session.metadata (存储 Issue URL 等信息) │ ▼ 5. 后端通过 SignalR 广播 SessionUpdated 事件 │ ▼ 6. 前端接收事件,更新 UI 显示"已同步"状态安全设计
安全问题始终是集成第三方服务的重中之重。我们做了以下考量:
- 防 CSRF 攻击:在 OAuth 跳转时,生成随机的
state参数并存入 sessionStorage。回调时严格验证 state,防止请求被伪造。 - Token 存储隔离:Token 仅存储在浏览器的
localStorage中,利用同源策略(Same-Origin Policy),只有 HagiCode 的脚本才能读取,避免了服务器端数据库泄露波及用户。 - 错误边界:针对 GitHub API 常见的错误(如 401 Token 过期、422 验证失败、429 速率限制),设计了专门的错误处理逻辑,给用户以友好的提示。
实践:代码实现细节
纸上得来终觉浅,咱们来看看具体的代码是怎么实现的。
1. 后端最小化改动
后端只需要做两件事:存储同步信息、处理 OAuth 回调。
数据库变更
我们只需要在Sessions表增加一个Metadata列,用来存储 JSON 格式的扩展信息。
-- 添加 metadata 列到 Sessions 表 ALTER TABLE "Sessions" ADD COLUMN "Metadata" text NULL;实体与 DTO 定义
// src/HagiCode.DomainServices.Contracts/Entities/Session.cs public class Session : AuditedAggregateRoot<SessionId> { // ... 其他属性 ... /// <summary> /// JSON metadata for storing extension data like GitHub integration /// </summary> public string? Metadata { get; set; } } // DTO 定义,方便前端序列化 public class GitHubIssueMetadata { public required string Owner { get; set; } public required string Repo { get; set; } public int IssueNumber { get; set; } public required string IssueUrl { get; set; } public DateTime SyncedAt { get; set; } public string LastSyncStatus { get; set; } = "success"; } public class SessionMetadata { public GitHubIssueMetadata? GitHubIssue { get; set; } }2. 前端 OAuth 流程
这是连接的入口。我们使用标准的 Authorization Code Flow。
// src/HagiCode.Client/src/services/githubOAuth.ts // 生成授权 URL 并跳转 export async function generateAuthUrl(): Promise<string> { const state = generateRandomString(); // 生成防 CSRF 的随机串 sessionStorage.setItem('hagicode_github_state', state); const params = new URLSearchParams({ client_id: clientId, redirect_uri: window.location.origin + '/settings?tab=github&oauth=callback', scope: ['repo', 'public_repo'].join(' '), state: state, }); return `https://github.com/login/oauth/authorize?${params.toString()}`; } // 在回调页面处理 Code 换取 Token export async function exchangeCodeForToken(code: string, state: string): Promise<GitHubToken> { // 1. 验证 State 防止 CSRF const savedState = sessionStorage.getItem('hagicode_github_state'); if (state !== savedState) throw new Error('Invalid state parameter'); // 2. 调用后端 API 进行 Token 交换 // 注意:这里必须经过后端,因为需要 ClientSecret,不能暴露在前端 const response = await fetch('/api/GitHubOAuth/callback', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, state, redirectUri: window.location.origin + '/settings?tab=github&oauth=callback' }), }); if (!response.ok) throw new Error('Failed to exchange token'); const token = await response.json(); // 3. 存入 LocalStorage saveToken(token); return token; }3. GitHub API 客户端封装
有了 Token 之后,我们就需要一个强有力的工具来调 GitHub API。
// src/HagiCode.Client/src/services/githubApiClient.ts const GITHUB_API_BASE = 'https://api.github.com'; // 核心请求封装 async function githubApi<T>(endpoint: string, options: RequestInit = {}): Promise<T> { const token = localStorage.getItem('gh_token'); if (!token) throw new Error('Not connected to GitHub'); const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, { ...options, headers: { ...options.headers, Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json', // 指定 API 版本 }, }); // 错误处理逻辑 if (!response.ok) { if (response.status === 401) throw new Error('GitHub Token 失效,请重新连接'); if (response.status === 403) throw new Error('无权访问该仓库或超出速率限制'); if (response.status === 422) throw new Error('Issue 验证失败,可能标题重复'); throw new Error(`GitHub API Error: ${response.statusText}`); } return response.json(); } // 创建 Issue export async function createIssue(owner: string, repo: string, data: { title: string, body: string, labels: string[] }) { return githubApi(`/repos/${owner}/${repo}/issues`, { method: 'POST', body: JSON.stringify(data), }); }4. 内容格式化与同步
最后一步,就是把 HagiCode 的 Session 数据转换成 GitHub Issue 的格式。这有点像"翻译"工作。
// 将 Session 对象转换为 Markdown 字符串 function formatIssueForSession(session: Session): string { let content = `# ${session.title}\n\n`; content += `**> HagiCode Session:** #${session.code}\n`; content += `**> Status:** ${session.status}\n\n`; content += `## Description\n\n${session.description || 'No description provided.'}\n\n`; // 如果是 Proposal 类型,添加额外字段 if (session.type === 'proposal') { content += `## Chief Complaint\n\n${session.chiefComplaint || ''}\n\n`; // 添加一个深链接,方便从 GitHub 跳回 HagiCode content += `---\n\n**[View in HagiCode](hagicode://sessions/${session.id})**\n`; } return content; } // 点击同步按钮的主逻辑 const handleSync = async (session: Session) => { try { const repoInfo = parseRepositoryFromUrl(session.repoUrl); // 解析仓库 URL if (!repoInfo) throw new Error('Invalid repository URL'); toast.loading('正在同步到 GitHub...'); // 1. 格式化内容 const issueBody = formatIssueForSession(session); // 2. 调用 API const issue = await githubApiClient.createIssue(repoInfo.owner, repoInfo.repo, { title: `[HagiCode] ${session.title}`, body: issueBody, labels: ['hagicode', 'proposal', `status:${session.status}`], }); // 3. 更新 Session Metadata (保存 Issue 链接) await SessionsService.patchApiSessionsSessionId(session.id, { metadata: { githubIssue: { owner: repoInfo.owner, repo: repoInfo.repo, issueNumber: issue.number, issueUrl: issue.html_url, syncedAt: new Date().toISOString(), } } }); toast.success('同步成功!'); } catch (err) { console.error(err); toast.error('同步失败,请检查 Token 或网络'); } };总结与展望
通过这套"前端直连"方案,我们用最少的后端代码实现了 GitHub Issues 的无缝集成。
收获
- 开发效率高:后端改动极小,主要是数据库加一个字段和一个简单的 OAuth 回调接口,大部分逻辑都在前端完成。
- 安全性好:Token 不经过服务器数据库,降低了泄露风险。
- 用户体验佳:直接从前端发起请求,响应速度快,不需要经过后端中转。
注意事项
在实际部署时,有几个坑大家要注意:
- OAuth App 设置:记得在 GitHub OAuth App 设置里填正确的
Authorization callback URL(通常是http://localhost:3000/settings?tab=github&oauth=callback)。 - 速率限制:GitHub API 对未认证请求限制较严,但用 Token 后通常足够(5000次/小时)。
- URL 解析:用户输入的 Repo URL 千奇百怪,记得正则要匹配
.git后缀、SSH 格式等情况。
后续增强
目前的功能还是单向同步(HagiCode -> GitHub)。未来我们计划通过 GitHub Webhooks 实现双向同步,比如在 GitHub 里关闭 Issue,HagiCode 这边的会话状态也能自动更新。这需要我们在后端暴露一个 Webhook 接收端点,这也是下一步要做的有趣工作。
希望这篇文章能给你的第三方集成开发带来一点灵感!如果有问题,欢迎在 HagiCode GitHub 上提 Issue 讨论。