freeCodeCamp.org

How to Build an AI Support System That Automatically Routes Bugs to GitHub with Next.js and Jev

7.1内容质量
How to Build an AI Support System That Automatically Routes Bugs to GitHub with Next.js and Jev

TL;DR · AI 摘要

How to Build an AI Support System That Automatically Routes Bugs to GitHub with Next.js and Jev October 2, 2026 / AI And...

核心要点

  • 主题聚焦:How to Build an AI Support System That Automatic
  • 来源:freeCodeCamp.org,建议结合原文判断细节。
  • AI 分析暂不可用,本条为保底评分与摘要。
#AI#编程#前端#后端#安全
打开原文

如何使用 Next.js 和 Jev 构建自动将错误路由到 GitHub 的 AI 支持系统

2026年10月2日

/

#AI

Andrew Baisden

每个网站都会收到用户反馈,但大多数反馈最终都以尴尬的方式处理。访客发现按钮损坏后通过电子邮件联系你。另一个人在社交媒体上评论某页面在手机上无法加载。第三个人通过你的联系表单提交了一个功能建议,但这个建议却夹在新闻通讯和收据之间。

当你终于坐下来修复问题时,错误报告分散在三个不同的地方。其中一半缺少细节,而那些最终进入 GitHub 的报告都是手动复制的,有时甚至还粘贴了访客的电子邮件地址到公开的 issue 中。

我为自己的项目想要更好的解决方案,因此我构建了这个系统。IssueRelay 为任何 React 网站提供了一个小型支持组件,访客可以通过它提问、报告错误或提出功能建议。每个报告首先会保存到你自己的数据库中。然后由名为 Jev 的 AI 模型进行分类,一组代码中的简单规则决定其去向,你可以在私有仪表板中进行审核。

当你确认某个报告确实是一个错误时,IssueRelay 会为其创建一个干净的 GitHub issue,并移除访客的私人信息。当你之后在 GitHub 上关闭该 issue 时,支持工单也会同步关闭。

在本教程中,你将学习整个系统的运作方式,从浏览器中的组件到保持 GitHub 和仪表板同步的 webhook。你还将看到如何在大约 15 分钟内部署你自己的副本。

IssueRelay 在 GitHub 上的开源地址是 andrewbaisden/issuerelay,组件已发布在 npm 上,名为 @issuerelay/widget,目前正运行在我的作品集网站上。

我不会将整个代码库粘贴到本文中。仓库中包含所有文件,安装指南逐步讲解了安装过程。相反,我将展示承载关键概念的代码片段,解释每个部分的功能,并分享我在构建、测试和部署过程中学到的经验。

目录

  • 目录
  • 前提条件
  • AI 支持系统如何帮助任何网站
  • 我们将构建的内容
  • Jev 是什么?
  • 技术栈
  • 报告在系统中的传输过程
  • 如何部署你自己的 IssueRelay
  • 在真实网站上运行
  • 端到端测试(以及我的收获)
  • 构建过程:阶段与 AI 辅助开发
  • 将组件发布到 npm
  • 未来计划
  • 结论

前提条件

要跟随教程并部署你自己的副本,你应该具备:

  • React、Next.js 和 TypeScript 的实用知识:平台使用 Next.js App Router,组件是一个 React 组件。
  • Node.js 24 和 pnpm:如果你希望在本地运行项目或使用创建 GitHub App 的命令,需要安装这些工具。
  • GitHub 账号:以及你想要安装组件的网站或应用的仓库。确认的错误将在那里生成 issue。
  • Vercel 账号:免费的 Hobby 计划就足够了。你将通过 Vercel 的市场添加 Neon PostgreSQL 数据库,Neon 也有免费计划。
  • TypeSafe 账号:在 typesafe.ai 注册,用于 Jev(处理报告的 AI 模型)。在任何报告变成 GitHub issue 之前,你需要从 TypeSafe 控制台获取 API 密钥。
  • React 网站:你可以添加组件的地方。Next.js 网站是最简单的起点。
  • 可选:Resend 账号:如果你想发送账户邮件(如密码重置邮件)。

你不需要是人工智能专家也能跟上进度。通过一个小型的、类型化的SDK即可使用Jev,而大部分有趣的工作都是普通的网页工程:数据库、验证、身份认证和webhooks。

AI支持系统如何帮助任何网站

一个支持系统听起来像是只有大公司才需要的东西,但其解决的问题几乎出现在每个网站上:

  • 作品集网站会收到招聘人员的消息、关于项目的提问,以及报告某些浏览器上页面崩溃的问题。
  • SaaS产品会收到混合着计费问题和功能请求的错误报告,每个问题都需要不同的人或流程处理。
  • 文档网站会收到“这个示例无法运行”的报告,这些实际上都是产品中的错误。
  • 开源项目会遇到不愿自己在GitHub上提交问题,但会很乐意在网站上点击按钮的用户。
  • 为客户搭建的网站会收到客户几天后转达的反馈,但往往没有具体细节。

一个好的系统能为你提供一个统一的地方接收所有报告,即使其他服务失效时也能确保每个报告的安全存储,并对报告进行分类,让你专注于真正重要的问题。

AI部分的作用是协助分类,但它不应拥有最终决定权。模型可能会非常确定地犯错,而你并不希望基于猜测创建公开的GitHub问题。因此IssueRelay始终遵循一个简单的规则:AI提出建议,人工确认,代码强制执行规则。

我们将构建什么

这是IssueRelay中一个报告的完整处理流程:

访客在你的网站上打开小部件并选择主题:

他们描述问题,并可选择留下姓名和邮箱以便后续跟进:

小部件将报告发送到你的IssueRelay平台,平台会保存报告并回复一个访客之后可以引用的支持参考编号:

从那里开始,报告会成为你仪表板中的一个工单。Jev对其进行分类,你进行审核,如果确认是真正的错误,只需点击一次即可在你的仓库中创建GitHub问题。

完整功能列表如下:

  • 可嵌入的小部件:作为Shadow DOM内的React组件构建,无需CSS配置,永远不会与网站样式冲突。兼容Next.js App Router和严格的Content Security Policy。
  • 健壮的接收系统:每个报告在执行任何其他操作前都会先存储到PostgreSQL中,具备去重保护、按项目速率限制和允许站点地址列表。
  • 有限度的AI分类:Jev仅根据消息内容推荐类型和严重程度。
  • 私有仪表板:包含过滤器、分类历史记录和与AI输出分开存储的人工审核决策。
  • 审慎的GitHub升级:只有在所有者确认预览后,GitHub App才会创建问题,联系信息永远不会离开IssueRelay。
  • 双向同步:在GitHub上关闭或重新打开问题会通过签名webhooks更新工单状态。
  • 自托管支持:通过部署按钮、首次运行设置页面和设置页面,无需接触数据库即可运行自己的副本。

该形状正好符合支持分类的需求。一张工单可以是错误、问题、功能请求、计费问题或垃圾邮件。其优先级可以是低、中、高或关键。模型无需生成文本,没有提示注入使其编写问题标题,也没有需要后续清理的自由文本。输出仅包含标签和数字,你的代码可以同时验证两者。

它也非常经济且快速。撰写本文时,TypeSafe 将 Jev 的价格定为每十亿个输入令牌 42 美元,而一个支持消息仅包含几十个令牌。IssueRelay 集成只会向 Jev 发送访客消息和他们选择的主题。它永远不会发送姓名、电子邮件地址、工单 ID 或任何其他可识别个人的信息。

技术栈

IssueRelay 使用现代 TypeScript 技术栈构建,这也是我用于自己项目的相同技术栈。如果你阅读过我的其他文章,其中很多内容会显得很熟悉:

  • 平台和仪表板使用 Next.js 16(App 路由器)和 React 19
  • 所有地方都使用严格的 TypeScript,Zod 验证所有跨越信任边界的输入:公共 API 请求、环境变量、AI 输出和 GitHub Webhook 负载
  • PostgreSQL 配合 Drizzle ORM 和经过审查的 SQL 迁移
  • 使用 Better Auth 管理仪表板账户
  • 使用 TypeSafe 官方 Jev SDK 和 Octokit 构建 GitHub 应用
  • 使用 Vitest、React Testing Library 和 Playwright 进行测试,使用 Biome 进行代码检查和格式化
  • 使用 pnpm 工作区将所有内容存放在单一仓库中
  • 生产环境使用 Vercel、Neon 和 Resend

该单一仓库被拆分为多个小包,每个包都有明确职责:

| 包 | 职责 | |---|---| | apps/web | 平台:公共工单 API、仪表板、设置和 GitHub Webhook | | packages/widget | 发布到 npm 的浏览器小部件,永不导入服务器代码 | | packages/support-contracts | 小部件和 API 之间共享的请求和响应结构 | | packages/db | Drizzle 模式、迁移和所有数据库查询 | | packages/ai | Jev 适配器、分类服务和路由策略 | | packages/github | GitHub 应用客户端、问题草稿、隐私门禁和 Webhook 处理 | | packages/auth | Better Auth 设置、会话和工作区成员资格检查 |

这些边界比看起来更重要。React 组件不会直接与 GitHub、Jev 或数据库通信。浏览器代码中永远不会包含秘密。AI 包无法导入数据库。

保持这些界限的严格性使系统更容易测试和推理,这也是小部件可以发布到 npm 而无需携带任何服务器代码的原因。

报告如何在系统中流转

让我们跟踪一个报告从访客浏览器到 GitHub 闭合问题的完整流程。

第一步:小部件

小部件是一个普通的 React 组件,你可以从 npm 安装:

code
npm install @issuerelay/widget

然后你只需渲染一次,例如从根布局中的客户端组件:

code
"use client";

import {
  HttpSupportSubmissionClient,
  SupportWidget,
} from "@issuerelay/widget";

const submissionClient = new HttpSupportSubmissionClient({
  apiBaseUrl: "https://your-issuerelay.vercel.app",
});

export function Support() {
  return (
    <SupportWidget
      projectKey="pk_your_project_key"
      submissionClient={submissionClient}
      theme="system"
      position="bottom-right"
    />
  );
}

HttpSupportSubmissionClient 是与您的平台进行通信的部分。它不使用 cookie 或凭证,将每个报告直接提交到您的 IssueRelay API,并为每个报告分配一个提交 ID,这样在网络错误后重试时不会创建第二个工单。

SupportWidget 是访客看到的按钮和面板。projectKey 告诉平台该报告属于哪个项目。这是公开标识符而非密码,因此可以安全地添加到网站代码中。真正的防护在服务器端,服务器仅接受您为该项目列出的网站地址提交的报告。

"use client" 这一行存在是因为提交客户端是在浏览器中创建的。在 Next.js App Router 中,您需要用自定义的小型客户端组件包裹小部件,然后从布局中渲染该组件,例如这样实现:

在底层实现中,小部件在 Shadow DOM 中渲染,并自带捆绑样式,因此您的网站无需引入 Tailwind 或 CSS 文件,您的样式也不会意外地修改小部件外观。

您也不需要手动编写这些代码。IssueRelay 的项目设置页面会显示这个精确的代码片段,其中已预先填写好您的平台地址和项目密钥。

第二步:先保存,后处理

当报告到达 API 时,IssueRelay 首先要做的是保存它。不是进行分类,也不是发送到任何地方,只是在事务中将其存储到 PostgreSQL 数据库。

这是整个系统中最重要的设计决策。AI 提供商会出现故障,GitHub 也会出现故障。如果平台在保存报告前就调用 Jev 并且 Jev 超时,访客的消息将会丢失,而他们永远也不会知道。

因此规则很简单:只有在报告被安全存储后才会被接受,后续任何步骤的失败都不能删除它。如果 Jev 离线,工单会停留在仪表板中,直到您再次运行分类处理。

在保存之前,API 会检查以下内容:

  • 请求体符合共享的 Zod 合约,因此错误输入会被明确的错误拒绝。
  • projectKey 存在,且请求的来源是该项目允许的网站地址之一。
  • 项目未超出速率限制。
  • 提交 ID 之前未被使用过。重复提交会返回原始工单引用,而不是创建重复项。
code
const response = await this.client.systemOne({
  state: {
    message: input.message,
    category_hint: input.categoryHint ?? null,
  },
  questions: {
    ticket_type: choice(
      "What kind of support ticket is this? The visitor-selected category hint is a weak signal, not ground truth: judge from the message content.",
      {
        question: "The visitor asks how something works or what something is.",
        bug: "Something is broken, errors, or behaves incorrectly.",
        feature_request: "The visitor requests new functionality or an improvement.",
        spam: "Unsolicited advertising, scams, or irrelevant bulk content.",
        // ...account, billing, feedback, and other
      },
    ),
    severity: choice("How urgent is this ticket?", {
      low: "Minor inconvenience, cosmetic issue, or general question.",
      medium: "Broken functionality with a workaround, or a routine request.",
      high: "Major functionality unavailable, no workaround, time-sensitive.",
      critical: "Security breach, data loss, privacy exposure, or billing harm.",
    }),
  },
});

systemOne 是用于有限问题的 TypeSafe SDK 调用。state 对象包含 Jev 被允许查看的所有信息:消息和访客选择的主题。

注意缺失的部分。没有姓名、没有电子邮件、没有工单编号,因为这些信息对分类没有帮助,同时它们都会涉及离开平台的隐私数据。

每个 choice 定义一个问题及其可能的回答。每个标签旁边的描述告诉 Jev 该标签的含义。类型问题还指示 Jev 将访客选择的主题视为弱提示,因为人们经常为问题选择“报告错误”,或为明显损坏的问题选择“提问”。

返回的结果不会被自动信任。适配器会使用 Zod 模式验证响应,检查两个答案是否都来自允许的列表,并使用 Jev 给出的所选类型标签的概率作为置信度分数。

如果该概率缺失或不在 0 到 1 的范围内,结果会被拒绝,工单将留在审核状态而不是被赋予虚构的数值。

第 4 步:代码做出决策

Jev 会推荐一个类型和一个严重程度。它不会决定工单去向或是否成为 GitHub 问题。这些工作由 packages/ai/src/policy.ts 中的普通函数处理:

code
export function routeForType(type: TicketType): TicketRoute {
  switch (type) {
    case "bug":
      return "engineering";
    case "feature_request":
      return "product";
    case "spam":
      return "ignore";
    default:
      return "support";
  }
}

export function evaluateGitHubEscalation(input: {
  type: TicketType;
  route: TicketRoute;
  confidence: number;
}): EscalationEvaluation {
  const reasons: string[] = [];
  if (input.type !== "bug") reasons.push(`type is ${input.type}, not bug`);
  if (input.route !== "engineering") reasons.push(`route is ${input.route}, not engineering`);
  if (!(input.confidence >= GITHUB_ESCALATION_CONFIDENCE_THRESHOLD)) {
    reasons.push(`confidence ${input.confidence} is below ${GITHUB_ESCALATION_CONFIDENCE_THRESHOLD}`);
  }
  return { eligible: reasons.length === 0, reasons };
}

routeForType 将每种票证类型映射到对应的队列。错误归类到工程队列,功能请求归类到产品队列,垃圾信息会被隔离,其余所有内容归类到支持队列。由于这是一个普通的 switch 语句,你可以直接阅读、测试和修改它,而无需接触 AI。

evaluateGitHubEscalation 决定票证是否可以升级为 GitHub 问题。它必须是错误类型、必须在工程队列中,并且置信度必须至少达到 0.9。与其返回简单的 true 或 false,它会收集票证失败的原因,这些原因会在仪表板上显示,这样你始终可以知道为什么“创建 GitHub 问题”按钮会消失。

0.9 阈值存储在一个配置文件中,该文件包含一个注释说明这是一个未经校准的初始值,而非经过测量的准确度。我希望在代码中保持这种诚实:模型得分为 0.99 并不意味着模型 99% 的时间都是正确的。

第 5 步:仪表板中的人工审核

每个票证都会出现在一个私有仪表板中。项目页面会显示每个状态下的票证数量:

每个项目都有一个带有状态、路由、类型、严重性和参考信息过滤器的票证列表:

打开票证会显示访客的报告、当前 AI 的分类结果、完整的分类历史记录以及所有事件的时间线。你可以重新运行分类、解决票证,或记录一个更改路由、状态或 GitHub 推荐的审核决定。

我特别关注的一个细节是:人工决策会存储在自己的表格中,并记录作者和必填原因。AI 的历史记录永远不会被覆盖。如果你覆盖了 Jev 的判断,你仍然可以查看 Jev 原来的判断内容和时间,这对于了解模型实际表现非常重要。

仪表板通过 Better Auth 进行保护,所有读写操作都限定在工作区范围内。修改操作需要同源请求,且只有工作区所有者可以发布到 GitHub。

第 6 步:从错误报告到 GitHub 问题

当票证通过策略后,仪表板会显示即将创建的 GitHub 问题的预览:

仔细查看这张截图。访客留下了他们的姓名和电子邮件,这两项信息在仪表板上方可见,但都不会出现在问题预览中。

这不是巧合。在任何问题创建之前,报告会通过 packages/github/src/privacy.ts 中的隐私网关,该网关会查找电子邮件地址、电话号码、银行卡号、私钥、API 令牌、JSON Web 令牌和密码分配。它还会将报告与访客提交的联系信息进行比对,因此“嗨,我是访客 Sam”这样的内容不会泄露姓名到公开问题中。如果发现任何敏感信息,预览将被阻止且不会发布任何内容。

当你点击“创建 GitHub 问题”时,还会运行一些额外的保护措施:

  • 使用 GitHub App 而非个人令牌:App 仅安装在你选择的仓库中,拥有写入问题和读取元数据的权限,其他权限一概没有。
  • 先声明再创建:在调用 GitHub 之前,票证会在数据库中标记为“正在创建”,因此两次点击永远无法创建两个问题。
  • 隐藏标记:每个问题正文末尾都会附加一个与票证绑定的不透明 HTML 注释。如果请求超时且结果未知,IssueRelay 会先通过自己的 App 在仓库中搜索该标记,然后再尝试重新创建问题。它绝不会盲目重试一个可能已经创建过的问题。

以下是 IssueRelay 根据访客报告在我的投资组合公共仓库中创建的真实问题。该问题是通过 App 的机器人创建的,标记为 bug,包含报告和 AI 的分类,但没有联系方式:

第 7 步:保持 GitHub 和仪表板同步

最后一步形成了闭环。当你在 GitHub 上关闭问题时,GitHub 会向 IssueRelay 发送一个 webhook,问题状态会变为已解决。重新打开问题时,工单会重新进入队列。

由于 webhook 端点本质上是公开的,因此它的第一件事就是验证请求确实来自 GitHub。这是 packages/github/src/webhook-auth.ts 中的验证函数:

code
export function verifyWebhookSignature(input: {
  secret: string;
  rawBody: Uint8Array;
  signatureHeader: string | null;
}): boolean {
  const { secret, rawBody, signatureHeader } = input;
  if (!secret || !signatureHeader?.startsWith("sha256=")) {
    return false;
  }
  const hex = signatureHeader.slice("sha256=".length);
  if (!/^[0-9a-f]{64}$/.test(hex)) return false;
  const expected = createHmac("sha256", secret).update(rawBody).digest();
  const actual = Buffer.from(hex, "hex");
  if (expected.length !== actual.length) return false;
  return timingSafeEqual(expected, actual);
}

GitHub 使用只有 GitHub 和你的平台知道的密钥对每次交付进行签名,并在 X-Hub-Signature-256 请求头中发送签名。这个函数会针对原始请求字节计算自己的 HMAC SHA256 签名并进行比较。

有两个细节容易出错。首先,签名必须基于 GitHub 发送的原始字节计算,且在任何 JSON 解析之前,因为解析和重新格式化会改变字节内容。其次,比较使用 timingSafeEqual,无论第一个字节还是最后一个字节不同,耗时都相同,因此攻击者无法通过测量响应时间逐个字符猜测签名。该函数对所有类型的失败都返回 false,但不会说明具体原因,因此不会泄露任何信息。

签名验证后,IssueRelay 会存储每个交付的 ID,从而忽略重复的交付。它只会更新属于匹配 App 安装和仓库的问题,并按照 GitHub 上事件发生的顺序,而非接收顺序来应用事件。

如何部署你自己的 IssueRelay

你可以在 Vercel 和 Neon 上大约 15 分钟内部署你自己的 IssueRelay。完整的部署指南(包括故障排除)请参见 docs/SELF_HOSTING.md。以下是简要步骤:

第 1 步:部署

推荐的部署方式是 README 中的 "Deploy with Vercel" 按钮。它会将仓库复制到你的 GitHub 账户,添加一个 Neon 数据库,并请求三个随机密钥。

第一次构建故意失败,因为 Vercel 的克隆页面没有根目录设置,因此你需要在项目设置中将根目录设置为 apps/web,然后重新部署。生产构建会为你创建所有数据库表。

指南还描述了 "Fork and Import" 的替代路径,可以实现未来更新只需单击一次,但该路径尚未经过端到端测试。

第 2 步:运行设置页面

在你的新站点上打开 /setup。该页面仅在数据库中没有账户且你输入部署时创建的 SETUP_TOKEN 时才有效,因此第一个找到你 URL 的人无法声称你的平台。它会创建你的管理员账户和第一个项目,然后显示你的小部件密钥和准备粘贴的小部件代码。

站点地址字段以 http://localhost:3000 开头,这是 Next.js 应用在本地计算机上运行的地址。请同时添加你的生产环境地址,例如 https://my-site.vercel.app 或你自己的域名。如果忘记设置,小部件会礼貌地向访客显示"我们无法发送你的消息",因此当报告未到达时,这是首要检查项。

第 3 步:创建 GitHub App

手动设置 GitHub App 容易出现几个常见错误。最严重的是忘记订阅 Issues 事件,测试时我自己就犯过这个错误。因此 IssueRelay 提供了一个命令,可以根据清单自动创建 App:

code
pnpm github:create-app --platform https://your-issuerelay.vercel.app

该命令会在浏览器中打开 GitHub,并自动填写所有信息:一个具有写入 issues 和读取元数据权限的私有 App,已订阅 Issues 事件,其 Webhook 指向你的平台。

你点击 "Create GitHub App" 后,GitHub 会重定向到命令启动的临时本地服务器,命令会将 App ID、私钥和 Webhook 密钥写入 git 忽略的文件中。这些信息永远不会在终端中显示。随后终端会列出下一步的配置流程。

第 4 步:添加密钥并重新部署

将三个 GitHub App 参数和你的 TYPESAFE_API_KEY 添加到 Vercel 项目的环境变量中,然后重新部署。Jev 是处理 GitHub issues 的必要组件:没有它,报告仍会到达你的仪表板,但无法转化为 issues。

第 5 步:连接你的仓库

在将小部件部署的网站仓库中安装 App,然后在仪表板中打开项目设置页面并进行连接。该页面会询问 GitHub 哪个安装实例和仓库 ID 对应这个名称,因此拼写错误不会导致连接到错误的仓库。

第 6 步:安装小部件

通过设置页面提供的代码在你的网站上安装小部件,然后发送第一个报告。

之后要保持代码更新,可以从主仓库拉取更改。指南涵盖了使用部署按钮创建副本时需要的一次性步骤,因为这些副本不是 GitHub 的 fork。

在真实网站上运行

演示是一回事,但我想真正使用 IssueRelay,因此现在小部件已部署在我的作品集网站 andrewbaisden.com 上:

网站设计可能会变化,因此如果你在未来阅读这篇文章,之前的版本可以在我的 GitHub 上找到。

安装过程让我学到了一些东西。我的作品集当时仍在使用 React 18 进行测试,而 App Router 已经使用 React 19 渲染。因此我首先将项目升级到 React 19,并确保所有现有测试通过后才添加小部件。小部件与网站的浅色/深色主题匹配,位于右下角,并在作品集仓库中拥有自己的单元测试和浏览器测试。

然后我像访客一样进行了测试。我从生产网站发送了三个真实报告:一个问题、一个错误和一个功能请求。Jev 按我预期的方式对这三个报告进行了分类,评分在 0.95 到 1.00 之间,并根据策略将它们分别路由到支持团队、工程团队和产品团队。错误报告成为了我公开作品集仓库中的 issue #3,也就是之前截图中显示的 issue。我在该报告中包含了姓名和邮箱,但这些信息并未出现在公开的 issue 中。

端到端测试(以及我的收获)

我不想创建一个只能在本地运行的项目,因此测试是每个阶段的组成部分,而不是留到最后才处理。

测试套件包含多个层次:

  • 针对组件、API契约、AI策略、隐私门、设置页面等的单元测试,共有180个测试用例,均无需数据库支持。
  • 针对独立PostgreSQL测试数据库运行的数据库集成测试,包括证明两个点击无法创建两个GitHub问题的并发测试。
  • 使用Playwright进行的浏览器测试,会在独立端口启动自己的服务器,使用每次运行都会重新创建的独立数据库,确保测试永远无法接触真实数据。其中一个服务器连接空数据库以测试首次运行设置页面。
  • 包装检查:构建精确的npm tar包,并安装到带有严格内容安全策略的Vite应用和Next.js应用中(均在单体仓库外),然后在每个应用中提交报告。
  • 针对真实GitHub应用和临时仓库的实时旅程测试,包含20个检查步骤:提交报告、分类处理、预览内容、创建问题、验证未泄露隐私数据、在GitHub上关闭问题并等待webhook、重新打开问题并检查时间线。

我运行了三次实时旅程测试:第一次通过隧道连接本地机器,第二次针对生产环境,第三次针对我仅根据设置指南部署的全新副本。三次测试均通过了全部20项检查。

不过比测试通过更有价值的是各个阶段发现的问题:

  • Issues事件容易被遗忘:第一次手动创建GitHub应用时,没有设置事件订阅,导致GitHub从未通知IssueRelay问题关闭事件。这个错误就是create-app命令存在的原因。
  • 访客会提及自己的名字:名为Sarah的访客提交"Sarah here, the page is broken"这样的报告,会导致她的名字出现在公开问题中。隐私门现在会将每个报告与附带的联系信息进行比对。
  • Zod与严格CSP在浏览器中存在冲突:Zod 4短暂测试是否能使用new Function,这会被严格内容安全策略视为违规。我将Zod从组件中移除,改用小型验证检查,并通过测试证明这些检查与服务器端Zod模式在270种表单组合中保持一致。
  • Vercel的克隆流程没有根目录选项,且Vercel只会选择一次框架:我的全新部署失败了两次:一次是Vercel构建了仓库根目录,另一次是框架仍设置为"Other"。现在仓库在vercel.json中固定了Next.js,指南也警告了首次失败的情况。
  • 部署按钮复制的不是分支:从主仓库直接执行git pull会拒绝合并,因此指南现在包含一次性命令将复制仓库连接到主仓库。
  • GitHub问题需要Jev:我最初将Jev列为可选组件。仔细审查指南后发现,没有Jev就无法达到真实报告的置信度阈值。现在指南和设置页面都明确说明了这一点。
  • 日志噪声影响判断:每个数据库连接都会在错误级别记录SSL警告,导致健康部署看起来异常。解决方案是明确说明驱动程序已使用的SSL模式,这样警告消失但证书检查保持不变。

对我影响最深的教训是:逐字按照自己的文档部署,能发现任何测试都无法发现的漏洞。上述所有部署问题对自动化测试都是隐形的,但只要真实用户按照指南操作就会立刻显现。

构建过程:阶段与AI辅助开发

IssueRelay是通过多个小阶段逐步构建的,每个阶段结束前都会进行书面交接,之后才能开始下一个阶段:

阶段 | 成果 --- | --- 0 | 产品定义、架构、决策、安全和测试计划 1和2 | 单体仓库基础、领域模型、PostgreSQL模式和种子数据 3和4 | 小部件、演示站点和公共工单API 5和6 | Jev的AI分类和操作员仪表板 7和8 | 确认GitHub升级和签名的Webhook同步 9 | 在临时仓库中完整流程的实时验证 10 | 生产环境加固 11和12 | 验证并发布小部件到npm 部署 | Vercel、Neon和Resend在生产环境运行 13 | 在我的作品集上安装小部件 14和15 | 自用测试(持续进行中) 16 | 自托管:部署按钮、设置页面、项目设置和App清单命令

我的开发环境

我大部分工作都是在终端中完成的。我的配置如下:

  • 使用Ghostty作为终端,运行Claude Code、Codex和OpenCode
  • 使用Cursor作为编辑器
  • 使用ChatGPT、Claude和OpenCode的原生桌面应用

构建IssueRelay时,我主要使用的是Claude Code中的Claude Opus 5.5模型。在代码审查和签署阶段前检查时,我使用了其他模型,包括GPT-6 Sol和Grok,以及各种其他前沿和免费模型。

第二个模型用全新的视角阅读相同代码时发现了真实问题。例如,对第7和8阶段的Grok审查发现了15个问题。其中7个是有效的,包括在创建问题时的竞态条件和可能被猜测的问题标记,所有7个问题都在我继续之前修复了。另外3个部分有效,5个则因书面原因被推迟。

Anthropic新发布的Sonnet 5.5和OpenAI的GPT-6.1 Sol在此项目中未被使用。

更好的提示如何提升代码库质量

质量提升最大的来源不是更智能的模型,而是给模型更好的指令和更清晰的工作结构。以下方法证明有效:

  • 一次处理一个阶段:每个提示都要求精确完成一个阶段并明确成果,AI在获得批准前不得开始下一阶段。小而可审查的变更比一次性实现大功能更容易检查。
  • 任何代码前先制定计划:对于较大的阶段,我首先要求制定计划("先制定计划,获得批准后执行")。阅读计划只需2分钟,而纠正错误实现可能需要整个下午。
  • 存储在仓库中的规则:AGENTS.md文件保存项目规则,如"在调用外部AI或GitHub前持久化已接受的工单"、"绝不将联系信息发布到GitHub"和"不要盲目重试模糊的GitHub问题创建"。每个AI会话都会读取该文件,规则不再依赖我反复提醒。
  • 如实报告:指令规定绝不将未执行的检查报告为通过,每次交接都会记录实际运行的命令及其真实结果,包括失败情况。
  • 明确的提交条件:类似"当测试通过且没有其他问题时提交并推送"的提示确保完整测试套件在任何代码到达主分支前都已运行。
  • 要求验证而非承诺:我不问"自托管是否可行",而是要求AI通过在全新部署中遵循指南来验证。这一个要求就发现了7个文档和配置问题。
  • 将真实使用反馈纳入改进:当我亲自部署测试站点并记录所有困惑之处时,这些笔记直接被反馈到指南、设置页面和配置页面中。

将小部件发布到 npm

IssueRelay 中唯一对外发布的是小部件,以 @issuerelay/widget 的形式发布。其余所有内容都保留在 monorepo 的私有仓库中。

我不想发布一个仅限于自己工作区使用的组件,因此发布检查会构建 npm 实际接收到的精确 tarball 并进行检查。

它必须仅包含五个文件。它不能引用私有包、Node 内置模块、环境变量或任何类似密钥的内容。然后必须确保它能在仓库外的两个全新应用中安装运行,其中一个应用需在严格的 Content Security Policy 环境下运行,且不能出现任何策略违规。

发布通过 GitHub Actions 完成,使用 npm 的信任发布和出处验证,因此不存在长期有效的 npm 访问令牌泄露风险。

最终得到的压缩包大小约 10 KB,无需 CSS 配置,仅依赖 React 和 React Hook Form。小部件的 README 文档详细说明了所有属性。

后续计划

IssueRelay 已经完成自托管版本的开发,我每天都在个人作品集网站上使用它。接下来我希望探索的方向包括:

  • 提供托管版 IssueRelay,让用户无需自行部署即可注册并添加小部件
  • 测试 Fork 和 Import 部署路径的端到端流程,使其成为推荐的部署方式
  • 实现路线图中的功能,如重复报告检测、多个报告关联到同一问题、通知系统以及 GitHub 评论同步

结论

在本教程中,你学习了如何构建一个 AI 支持系统,将零散的网站反馈转化为已审核的工单,并将确认的错误路由到 GitHub。过程中你掌握了:

  • 构建一个无需 CSS 配置或样式冲突即可在任何网站运行的可嵌入 React 小部件
  • 在调用任何外部服务前先保存所有报告,确保服务中断时不会丢失数据
  • 使用 Jev 进行有界验证分类,返回标签和概率而非自由文本
  • 用可测试的明文代码实现路由和发布决策,并保持人工审核环节
  • 通过 GitHub App、隐私网关和防重复标记安全创建 GitHub 问题
  • 通过签名验证的 Webhook 保持 GitHub 和仪表盘数据同步
  • 在 Vercel 和 Neon 上部署自己的实例,并进行端到端测试(包括对自身文档的测试)

理解 IssueRelay 最好的方式是亲自尝试。你可以在 GitHub 上查看代码,通过自托管指南部署自己的实例,或通过 npm install @issuerelay/widget 在网站上添加小部件。如果这个项目对你有帮助,欢迎在仓库中点亮星标。

合作伙伴推荐课程

(在新标签页中打开)

ADVERTISEMENT

  • 在 Skillshare 学习 Google Data Studio - 免费创建惊人的数字营销报告 Sharon Hayward 4.4 星(18 人评分) 46 分钟 中级 查看课程 (在新标签页中打开)
  • 在 Udemy 学习企业级高级 React:面向高级工程师的 React Frantz Kati 4.5 星(2,765 人评分) 6 小时 3 分钟 中级 证书 查看课程 (在新标签页中打开)
  • 在 Skillshare 学习 React 101:绝对初学者的 React.js 教程 Kalob Taulien 4.4 星(22 人评分) 1 小时 27 分钟 初级 查看课程 (在新标签页中打开)

Class Central 上的更多 React 课程

→

全栈工程师 | React、Next.js、TypeScript、Node.js | AI 辅助工程

如果你读到这里,请感谢作者以表达你的支持。说声谢谢

免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人成为开发者。立即开始