标签 GitHub API 下的文章

从零构建 GitHub Issues 集成:HagiCode 的前端直连实践

本文记录了在 HagiCode 平台中集成 GitHub Issues 的全过程。我们将探讨如何通过"前端直连 + 后端最小化"的架构,在保持后端轻量的同时,实现安全的 OAuth 认证与高效的 Issues 同步。

背景:为什么要集成 GitHub?

HagiCode 作为一个 AI 辅助开发平台,核心价值在于连接想法与实现。但在实际使用中,我们发现用户在 HagiCode 中完成了 Proposal(提案)后,往往需要手动将内容复制到 GitHub Issues 中进行项目跟踪。

这带来了几个明显的痛点:

  1. 工作流割裂:用户需要在两个系统之间来回切换,体验不仅不流畅,还容易导致关键信息在复制粘贴的过程中丢失。
  2. 协作不便:团队其他成员习惯在 GitHub 上查看任务,无法直接看到 HagiCode 中的提案进展。
  3. 重复劳动:每当提案更新,就要人工去 GitHub 更新对应的 Issue,增加不必要的维护成本。

为了解决这个问题,我们决定引入 GitHub Issues Integration 功能,打通 HagiCode 会话与 GitHub 仓库的连接,实现"一键同步"。

关于 HagiCode

嘿,介绍一下我们正在做的东西

我们正在开发 HagiCode —— 一款 AI 驱动的代码智能助手,让开发体验变得更智能、更便捷、更有趣。

智能 —— AI 全程辅助,从想法到代码,让编码效率提升数倍。便捷 —— 多线程并发操作,充分利用资源,开发流程顺畅无阻。有趣 —— 游戏化机制和成就系统,让编码不再枯燥,充满成就感。

项目正在快速迭代中,如果你对技术写作、知识管理或者 AI 辅助开发感兴趣,欢迎来 GitHub 看看~


技术选型:前端直连 vs 后端代理

在设计集成方案时,摆在我们面前的有两条路:传统的"后端代理模式"和更激进的"前端直连模式"。

方案对比

在传统的后端代理模式中,前端所有的请求都要先经过我们的后端,再由后端去调用 GitHub API。这虽然逻辑集中,但给后端带来了不小的负担:

  1. 后端臃肿:需要编写专门的 GitHub API 客户端封装,还要处理 OAuth 的复杂状态机。
  2. Token 风险:用户的 GitHub Token 必须存储在后端数据库中,虽然可以加密,但毕竟增加了安全风险面。
  3. 开发成本:需要数据库迁移来存储 Token,还需要维护一套额外的同步服务。

前端直连模式则要轻量得多。在这个方案中,我们只利用后端来处理最敏感的"密钥交换"环节(OAuth callback),获取到 Token 后,直接存在浏览器的 localStorage 里。后续创建 Issue、更新评论等操作,直接由前端发 HTTP 请求到 GitHub。

对比维度后端代理模式前端直连模式
后端复杂度需要完整的 OAuth 服务和 GitHub API 客户端仅需一个 OAuth 回调端点
Token 管理需加密存储在数据库,有泄露风险存储在浏览器,仅用户自己可见
实施成本需数据库迁移、多服务开发主要是前端工作量
用户体验逻辑统一,但服务器延迟可能稍高响应极快,直接与 GitHub 交互

考虑到我们要的是快速集成和最小化后端改动,最终我们采用了"前端直连模式"。这就像给浏览器发了一张"临时通行证",拿到证之后,浏览器就可以自己去 GitHub 办事了,不需要每次都找后端管理员批准。


核心设计:数据流与安全

在确定架构后,我们需要设计具体的数据流。整个同步流程的核心在于如何安全地获取 Token 并高效地利用它。

整体架构图

整个系统可以抽象为三个角色:浏览器(前端)、HagiCode 后端、GitHub。

+--------------+        +--------------+        +--------------+
|  前端 React  |        |    后端      |        |    GitHub    |
|              |        |   ASP.NET    |        |    REST API  |
|  +--------+  |        |              |        |              |
|  |  OAuth |--+--------> /callback    |        |              |
|  |  流程  |  |        |              |        |              |
|  +--------+  |        |              |        |              |
|              |        |              |        |              |
|  +--------+  |        |  +--------+  |        |  +--------+  |
|  |GitHub  |  +------------>Session |  +----------> Issues |  |
|  |API     |  |        |  |Metadata|  |        |  |        |  |
|  |直连    |  |        |  +--------+  |        |  +--------+  |
|  +--------+  |        |              |        |              |
+--------------+        +--------------+        +--------------+

关键点在于:只有 OAuth 的一小步(获取 code 换 token)需要经过后端,之后的粗活累活(创建 Issue)都是前端直接跟 GitHub 打交道。

同步数据流详解

当用户点击 HagiCode 界面上的"Sync to GitHub"按钮时,会发生一系列复杂的动作:

用户点击 "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 显示"已同步"状态

安全设计

安全问题始终是集成第三方服务的重中之重。我们做了以下考量:

  1. 防 CSRF 攻击:在 OAuth 跳转时,生成随机的 state 参数并存入 sessionStorage。回调时严格验证 state,防止请求被伪造。
  2. Token 存储隔离:Token 仅存储在浏览器的 localStorage 中,利用同源策略(Same-Origin Policy),只有 HagiCode 的脚本才能读取,避免了服务器端数据库泄露波及用户。
  3. 错误边界:针对 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 的无缝集成。

收获

  1. 开发效率高:后端改动极小,主要是数据库加一个字段和一个简单的 OAuth 回调接口,大部分逻辑都在前端完成。
  2. 安全性好:Token 不经过服务器数据库,降低了泄露风险。
  3. 用户体验佳:直接从前端发起请求,响应速度快,不需要经过后端中转。

注意事项

在实际部署时,有几个坑大家要注意:

  • 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 讨论。

前因:

上次有佬友问如何自动定时同步上游仓库,当时我随手糊了一段脚本,结果发现 bug 满天飞,于是删除了。同时也推荐了 pull 这个工具,但是这个工具的同步比较随机,不可控。
于是就搞了现在这个脚本,支持多仓库、多用户、多分支、多平台通知

食用方法:
fork 仓库,然后根据 README.md 进行配置。
上游仓库:可以是任意公开仓库
目标仓库:可以是任意用户的仓库(需要具备 repo 权限的 token)

目标仓库支持你 fork 别人的,不影响 pr、创建分支等。也可以你自己创建一个空仓然后搬运。

该脚本运行于 GitHub Aactions,运行后的 actions 日志会显示上游仓库地址、目标仓库 owner/repo,但是不会暴露各种 token 等私密信息。可以把仓库设置为私密,不影响同步功能和效果。

推送的消息如下:


📌 转载信息
原作者:
binghe
转载时间:
2026/1/16 17:41:31

cliproxy 更新有点快,来个脚本启动前更新,或者安装
配置好 config.yaml 后命令行运行

@'
@echo off
setlocal enabledelayedexpansion

echo ============================================
echo   CLIProxyAPI Auto Update Script
echo ============================================
echo.

set "REPO=router-for-me/CLIProxyAPI"
set "API_URL=https://api.github.com/repos/%REPO%/releases/latest"
set "DOWNLOAD_DIR=%~dp0"
set "TEMP_DIR=%DOWNLOAD_DIR%temp_update"
set "ZIP_FILE=%DOWNLOAD_DIR%CLIProxyAPI_latest.zip"
set "EXE_NAME=cli-proxy-api.exe"
set "VERSION_FILE=%DOWNLOAD_DIR%current_version.txt"

echo [1/4] Checking for updates...

REM Get latest version from GitHub
for /f "usebackq tokens=*" %%i in (`curl -s "%API_URL%" ^| powershell -Command "$input | ConvertFrom-Json | Select-Object -ExpandProperty tag_name"`) do set "LATEST_VERSION=%%i"

if "%LATEST_VERSION%"=="" (
    echo [ERROR] Failed to get version info
    echo [INFO] Starting existing program...
    goto :start_program
)

echo [INFO] Latest version: %LATEST_VERSION%

REM Get current local version
set "CURRENT_VERSION="
if exist "%VERSION_FILE%" (
    set /p CURRENT_VERSION=<"%VERSION_FILE%"
)

if "%CURRENT_VERSION%"=="" (
    echo [INFO] No local version found
) else (
    echo [INFO] Current version: %CURRENT_VERSION%
)

REM Compare versions
if "%CURRENT_VERSION%"=="%LATEST_VERSION%" (
    echo.
    echo [INFO] Already up to date!
    goto :start_program
)

echo.
echo [2/4] New version available! Downloading...

set "VERSION_NUM=%LATEST_VERSION:~1%"
set "DOWNLOAD_URL=https://github.com/%REPO%/releases/download/%LATEST_VERSION%/CLIProxyAPI_%VERSION_NUM%_windows_amd64.zip"
echo URL: %DOWNLOAD_URL%

curl -L -o "%ZIP_FILE%" "%DOWNLOAD_URL%" --progress-bar

if not exist "%ZIP_FILE%" (
    echo [ERROR] Download failed
    goto :start_program
)

echo.
echo [3/4] Extracting and updating %EXE_NAME%...

if exist "%TEMP_DIR%" rd /s /q "%TEMP_DIR%"
mkdir "%TEMP_DIR%"

powershell -Command "Expand-Archive -Path '%ZIP_FILE%' -DestinationPath '%TEMP_DIR%' -Force"

REM Stop running process
tasklist /FI "IMAGENAME eq %EXE_NAME%" 2>nul | find /I "%EXE_NAME%" >nul
if !errorlevel! equ 0 (
    echo [INFO] Stopping running process...
    taskkill /F /IM "%EXE_NAME%" >nul 2>&1
    timeout /t 2 /nobreak >nul
)

if exist "%TEMP_DIR%\%EXE_NAME%" (
    copy /y "%TEMP_DIR%\%EXE_NAME%" "%DOWNLOAD_DIR%\%EXE_NAME%" >nul
    echo %LATEST_VERSION%>"%VERSION_FILE%"
    echo [INFO] Updated to %LATEST_VERSION%
) else (
    echo [ERROR] %EXE_NAME% not found in archive
)

echo.
echo [4/4] Cleaning up...
del /f /q "%ZIP_FILE%" 2>nul
rd /s /q "%TEMP_DIR%" 2>nul

:start_program
echo.
if not exist "%DOWNLOAD_DIR%%EXE_NAME%" (
    echo [ERROR] %EXE_NAME% not found
    pause
    exit /b 1
)

echo ============================================
echo   Starting %EXE_NAME%
echo ============================================
echo.

cd /d "%DOWNLOAD_DIR%"
"%EXE_NAME%"
'@ | Out-File -FilePath "cliproxy.bat" -Encoding ASCII

再运行

.\cliproxy.bat

📌 转载信息
转载时间:
2026/1/16 12:50:59

Who Starred My Repo

是否曾经好奇,在那些给你仓库点过 Star 的用户中,是否有隐藏的开源大佬?本项目来帮助你找到答案!

  • 影响力排行: 基于多维度指标综合计算用户的「开源影响力」并进行排序

  • 单维度排序: 同时也支持按单一指标排序:


    • 🌟 Star 总数
    • 📅 Star 日期
    • 👥 粉丝 (Follower) 数量
    • 📦 公开仓库数量

在线预览: Who Starred My Repo

(你可能会遇到 GitHub API 的速率限制导致请求失败,此时你可以 Clone 本项目并在本地使用自己的 GitHub Token 来运行,启动方法见 ReadMe )

开源地址: https://github.com/wy-luke/who-starred-my-repo 欢迎 Star ~

screenshot

用 manus 搓了个挺好玩的开源小工具

输入 GitHub 仓库地址

自动分析这个项目的 Star 都来自哪些国家 / 地区

世界地图 + 排名 + 统计一目了然

在线体验:

GitHub:

看着自己项目的 Star 在地图上亮起来,还挺有成就感的

settings 可以设置 github token 可以提升接口调用次数,建议对小型项目进行分析,大型项目数据分析时间太长了,setting 页面需要 manus 登陆,建议大家去白嫖一个,站里全是邀请链接

效果图:



📌 转载信息
原作者:
tianchang_ovo
转载时间:
2026/1/9 18:02:09