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 分析暂不可用,本条为保底评分与摘要。
如何使用 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 安装:
npm install @issuerelay/widget然后你只需渲染一次,例如从根布局中的客户端组件:
"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 之前未被使用过。重复提交会返回原始工单引用,而不是创建重复项。
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 中的普通函数处理:
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 中的验证函数:
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:
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 分钟 初级 查看课程 (在新标签页中打开)
- 在 Udemy 上 理解 NPM - Node.js 包管理器 Bogdan Stashchuk 4.6(满分 5.0)(1,808 人评分)来自 1,808 条评分 2 小时 20 分 初学者 证书 查看课程(在新标签页中打开)
Class Central 上的更多 React 课程
→
全栈工程师 | React、Next.js、TypeScript、Node.js | AI 辅助工程
如果你读到这里,请感谢作者以表达你的支持。说声谢谢
免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人成为开发者。立即开始