← 返回蜂巢洞察

如何使用 Next.js 和 Jev 构建一个能够自动将错误信息发送到 GitHub 的人工智能支持系统

每个网站都会收到用户的反馈,而其中大部分反馈最终都会被搁置在某个不太合适的地方。有的访客会发现某个按钮无法正常使用,然后会通过电子邮件联系你;还有人会在社交媒体上留言,反映他们的手机无法打开某个页面;再有人则会填写你的联系表格,提出一些功能改进的建议,而这些建议就会和新闻通讯、收据之类的信息一起堆在你的收件箱里。 当你终于坐下来准备处理这些反馈时,你会发现那些错误报告分散在三个不同的地方。其中有一半的报告缺少必要的详细信息,而那些被上传到GitHub上的报告,也是有人手动复制过去的,有时甚至还会把访客的电子邮件地址也粘贴在里面。 因为我想为自己的项目提供更好的支持系统,所以我决定自己动手开发它

每个网站都会收到用户的反馈,而其中大部分反馈最终都会被搁置在某个不太合适的地方。有的访客会发现某个按钮无法正常使用,然后会通过电子邮件联系你;还有人会在社交媒体上留言,反映他们的手机无法打开某个页面;再有人则会填写你的联系表格,提出一些功能改进的建议,而这些建议就会和新闻通讯、收据之类的信息一起堆在你的收件箱里。

当你终于坐下来准备处理这些反馈时,你会发现那些错误报告分散在三个不同的地方。其中有一半的报告缺少必要的详细信息,而那些被上传到GitHub上的报告,也是有人手动复制过去的,有时甚至还会把访客的电子邮件地址也粘贴在里面。

因为我想为自己的项目提供更好的支持系统,所以我决定自己动手开发它。IssueRelay可以为任何使用React技术的网站添加一个辅助工具栏,让访客可以通过这个工具栏提出问题、报告错误或建议新的功能。每条反馈信息首先都会被保存在你的数据库中,然后会由一个名为Jev的人工智能模型对它们进行分类,再有一组编写在代码中的规则会决定这些信息应该被存放到哪里,最后你可以在私有的控制面板中查看这些信息。

当你确认某条反馈确实属于错误报告时,IssueRelay会为它创建一个格式规范的GitHub问题报告,同时会删除访客的私人信息。之后当你在GitHub上关闭这个问题报告时,相应的支持工单也会被关闭。

在这个教程中,你将了解到整个系统的运作原理,从浏览器中的辅助工具栏开始,到确保GitHub与控制面板保持同步的Webhook机制。此外,你还能在大约15分钟内学会如何自己部署这个系统。

IssueRelay是在GitHub上以开源形式发布的,地址是andrewbaisden/issuerelay;而用于浏览器的辅助工具栏则可以通过npm包进行安装,地址是@issuerlay/widget。目前,我的个人作品网站上就已经在使用这个系统了。

我不会在这篇文章中粘贴所有的代码。因为源代码库里已经包含了所有文件,而且安装指南也会一步步指导你完成安装过程。相反,我会向你展示那些能够体现核心功能的代码片段,解释每段代码的作用,并分享我在开发、测试和部署这个系统的过程中所学到的东西。

网站上的IssueRelay辅助工具栏,展示了‘提出问题’、‘报告错误’和‘建议新功能’等选项

目录

先决条件

若要跟随步骤自行部署该功能,您需要具备以下条件:

  • 掌握React、Next.js和TypeScript的相关知识:该系统使用Next.js的应用路由机制,而相关组件也是基于React开发的。

  • 安装Node.js 24及pnpm:如果您希望在本地运行该项目或使用用于生成GitHub应用的命令,这些工具是必不可少的。

  • 拥有GitHub账户:此外,您还需要一个用于存储网站或应用相关代码的仓库。所有确认的错误都会被记录在该仓库中。

  • 拥有Vercel账户:免费的Hobby计划即可满足需求。您可以通过Vercel的市场平台添加Neon PostgreSQL数据库,而Neon本身也提供免费方案。

  • 拥有TypeSafe账户:在typesafe.ai平台上注册账户,以便使用用于处理报告的Jev人工智能模型。在任何报告被提交为GitHub问题之前,您都需要从TypeSafe控制台获取API密钥。

  • 拥有一个React开发的网站:您可以在该网站上添加相关组件。使用Next.js构建的网站是最适合开始使用的平台。

  • 可选:拥有Resend账户:如果您需要接收关于密码重置等操作的邮件通知,那么这个账户是必要的。

您并不需要成为人工智能专家才能使用该功能。Jev是通过一个简单的SDK来实现的,而大部分相关工作其实都属于常规的网页开发范畴,比如数据库管理、数据验证、身份认证以及Web钩子的配置等。

人工智能支持系统如何帮助任何网站

很多人可能会认为,只有大型企业才需要支持系统,但实际上几乎所有网站都会遇到类似的问题:

  • 作品集网站会收到招聘人员的咨询邮件、关于项目的相关问题,以及关于某些页面在特定浏览器中出现故障的反馈。

  • SaaS产品会收到各种错误报告,其中既包括技术问题,也包含账单相关的问题或功能需求请求,而每种问题通常都需要由不同的人员或流程来处理。

  • 文档网站会收到用户反馈称“这个示例无法正常使用”,而这些实际上往往就是产品中的缺陷。

  • 开源项目的用户通常不会自己提交问题报告,而是会通过网站上的按钮来反馈问题。

  • 为他人开发的客户网站,客户往往会在几天后才将反馈信息转发给您,而且往往不会提供详细的背景信息。

一个优秀的支持系统能够将所有报告集中到一个地方进行处理,确保即使其他服务出现故障,这些报告也能得到妥善处理;同时,它还能对报告进行分类,让您把时间花在真正重要的问题上。

人工智能技术有助于这些报告的分类工作,但它绝不应该完全取代人类的判断。因为模型有时也会犯错,而公开在GitHub上提交问题报告显然不是基于猜测来进行的操作。因此,IssueRelay始终遵循这样一个简单的原则:由人工智能提供建议,人类进行最终确认,然后通过代码来确保规则得到严格执行。

我们将构建什么

以下是报告在IssueRelay中处理的全过程:

报告在IssueRelay中的处理流程

访问者打开您网站上的该插件,并选择了一个主题:

演示网站上显示的插件界面,其中包含三个选项:提问、报告错误或建议新增功能

访问者描述了所遇到的问题,还可以选择留下姓名和电子邮件地址,以便您后续跟进处理:

插件中的报告错误表格,已填写了问题描述、姓名及电子邮件地址

该插件会将报告发送到您的IssueRelay平台,平台会保存这份报告,并回复一个支持编号,供访问者日后参考:

之后,这份报告会成为您仪表板中的一张工单。Jev会对其进行分类,您也会进行审核;如果确实是一个错误,只需点击一下按钮,就会在您的GitHub仓库中生成一个新的问题。

以下是完整的功能列表:

  • 可嵌入的插件:该插件作为React组件被构建在Shadow DOM中,因此无需进行任何CSS配置,也不会与您网站的样式发生冲突。它既适用于Next.js应用路由器,也兼容严格的Content Security Policy规则。

  • 可靠的数据存储:所有报告在进入其他处理流程之前都会先被保存到PostgreSQL数据库中,系统会防止数据重复,同时还会根据项目设置限制报告的数量,并只允许来自指定网站的报告被提交。

  • 智能的分类系统:Jev仅通过分析用户提交的信息,就能自动判断问题的类型和严重程度。

  • 私密的仪表板:该仪表板提供过滤功能、分类记录以及人工审核结果,这些信息会与AI生成的分类结果分开存储。

  • 谨慎的GitHub问题提交流程:只有当问题所有者确认了预览版本后,才会通过GitHub应用生成正式的问题工单,而且用户的联系信息也不会离开IssueRelay平台。

  • 双向同步:在GitHub上关闭或重新打开问题时,系统会通过签名的webhook自动更新仪表板中的工单状态。

  • 自主托管选项:提供了“部署”按钮、初次使用设置页面以及配置页面,让您能够轻松搭建自己的解决方案,而无需修改数据库结构。

Jev是什么?

Jev是TypeSafe开发的一款模型,专为TypeSafe所称的“系统一”类任务而设计:这类任务需要快速、有明确界限的判断,而非冗长、开放式的文本编写。

与其让模型先写一段文字然后再尝试解析它,不如向Jev提供一些初始状态以及一系列问题,每个问题都对应着固定的答案选项。Jev会为每个问题选择一个答案,并返回它为每个选项分配的概率值。

这种处理方式恰恰符合支持请求分类处理的实际需求。一张工单可能代表一个漏洞、一个问题、一个功能请求、一个账单问题,或者是一封垃圾邮件。这些工单可以被划分为低优先级、中优先级、高优先级或紧急级别。模型不需要编写任何文本,也不需要输入提示来生成问题标题,更不需要后续处理任何自由格式的文字内容。它的输出结果只是一个标签和一个数字,而你的代码可以直接使用这些信息来进行判断。

此外,Jev的使用成本也很低且运行速度很快。在撰写本文时,TypeSafe表示每十亿个输入token使用Jev的成本为42美元,而一条支持请求的信息通常只需要几十个token即可处理。IssueRelay集成机制只会向Jev传递访问者的消息以及他们选择的主题,绝不会发送任何能够识别个人身份的信息,如姓名、电子邮件地址或工单ID等。

技术架构

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

  • Next.js 16 (App Router)和React 19被用于构建平台和仪表盘界面。

  • 在整个系统中严格使用TypeScript,并通过Zod来检查所有跨越信任边界的输入数据,包括公共API请求、环境变量、AI输出结果以及GitHub Webhook的数据。

  • 使用Drizzle ORM与PostgreSQL数据库,并对SQL迁移脚本进行严格审核。

  • 为仪表盘账户提供了更强大的认证机制。

  • 对于Jev模型,使用了官方TypeSafe SDK;而对于GitHub App,则使用了Octokit。

  • 测试工具包括Vitest、React Testing Library和Playwright,代码格式检查工具则使用Biome。

  • 所有代码都存储在pnpm工作区中,实现了代码的统一管理。

  • 在生产环境中使用了Vercel、Neon和Resend等工具来加速部署流程。

这个单仓库系统被拆分成了多个独立的包,每个包都有明确的职责:

包名 负责的功能
apps/web 平台相关功能:公共工单API、仪表盘界面、系统配置以及GitHub Webhook处理。
packages/widget 用于浏览器端的小工具,该工具不会导入任何服务器代码。
packages/support-contracts 为小工具和API提供统一的请求与响应结构。
packages/db 负责数据库相关的schema设计、迁移操作以及所有数据库查询语句的处理。
packages/ai 包含Jev模型适配器、问题分类处理服务以及路由规则配置。
packages/github GitHub App客户端相关功能,包括工单草稿编辑、隐私设置管理以及Webhook事件处理。
packages/auth 提供更强大的认证机制,包括会话管理及工作区成员身份验证。

这些界限的实际重要性可能比看上去要大得多。React组件从来不会直接与GitHub、Jev或数据库进行交互;浏览器代码中也不存在任何敏感信息,而AI插件同样无法访问数据库。

严格遵守这些规则使得系统更易于测试和理解,也正是因此,该小部件才能被发布到npm上,而无需携带任何服务器端代码。

报告在系统中的流转过程

让我们追踪一条报告从访客的浏览器开始,直到最终被保存到GitHub上的某个问题中为止。

步骤1:小部件

这个小部件其实就是一个普通的React组件,你可以通过npm来安装它:

npm install @issuerelay/widget

然后你可以在页面中渲染它,比如在根布局中的某个客户端组件里:

"use client";

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

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

export function Support() {
  return (
    
  );
}

HttpSupportSubmissionClient负责与你的平台进行交互。它会将每条报告发送到IssueRelay API中,而且这个过程不需要使用cookie或密码;同时,每条报告都会被分配一个提交ID,这样在网络出现故障时重新尝试提交时,就不会生成新的工单。

SupportWidget就是访客们看到的那个按钮和面板。projectKey用于告诉平台这条报告属于哪个项目。这个键是公开的识别信息,并非密码,因此将其放入网站代码中是安全的。真正的安全保障在于服务器端——只有你为该项目指定的网站地址才能提交报告。

"use client"这一行代码的存在是因为提交客户端是在浏览器中创建的。在Next.js的应用路由系统中,你需要将这个小部件包装在一个自定义的客户端组件中,然后再从页面布局中渲染它。

实际上,这个小部件会在一个独立的Shadow DOM中被渲染出来,并且会使用自己单独的样式文件,因此你的网站不需要依赖Tailwind CSS或导入其他CSS文件;同时,其他样式也不会意外地影响到这个小部件的外观。

你也不必手动编写这些代码。IssueRelay的项目设置页面会直接提供已经填好平台地址和项目键的完整代码片段。

步骤2:先保存,后再处理

当报告到达API后,IssueRelay首先会将其保存下来。它不会对报告进行分类,也不会将其发送到其他地方,只是简单地将其存储在PostgreSQL数据库中,并确保这个操作是在一个事务框架内完成的。

这是整个系统中最重要的设计决策。AI服务可能会出现故障,GitHub也可能出现故障。如果平台在保存报告之前先调用了Jev服务,而Jev服务发生了超时,那么访客提交的消息就会丢失,而他们永远也不会知道这一点。

因此,规则很简单:只有当报告被安全地存储之后,它才会被接受;而后续环节中的任何错误都永远无法删除该报告。如果Jev系统出现故障,该报告就会一直保留在控制面板中,直到你再次进行分类处理。

在保存报告之前,API会检查几项内容:

  • 请求的内容必须符合预先定义的Zod契约格式,因此不符合要求的输入会被立即拒绝,并会显示明确的错误信息。

  • 项目键必须存在,并且请求的来源地址必须是该项目允许的访问地址之一。

  • 该报告的提交次数必须在项目的限制范围内。

  • 该报告的提交ID不能已经被使用过;如果重复提交,系统会返回原来的报告编号,而不会创建新的报告。

步骤3:使用Jev进行分类处理

一旦报告被存储起来,分类系统就会请求Jev对其进行分类。以下是Jev适配器的核心代码片段,来自packages/ai/src/jev-classifier.ts文件(为节省空间稍作删减):

const response = await this.client.systemOne({
  state: {
    message: input.message,
    category_hint: input.categoryHint ?? null,
  },
  questions: {
    ticket_type: choice(
      "这是一份什么样的支持请求?访客选择的分类提示仅供参考,并不能作为最终依据,应根据请求内容来判断。",
      {
        question: "访客询问某项功能的用途或性质。",
        bug: "系统出现故障、出现错误或行为异常。",
        feature_request: "访客请求添加新功能或对现有功能进行改进。",
        spam: "未经请求的广告、诈骗信息或无关内容。",
        // ...还有其他类别
      },
    ),
    severity: choice("这份请求的紧急程度如何?", {
      low: "属于小问题,只是外观上的瑕疵或一般性咨询。",
      medium: "功能出现故障,但仍有解决办法;或者是常规性的请求。",
      high: "关键功能无法使用,没有解决办法,且处理时间紧迫。",
      critical: "存在安全漏洞、数据丢失、隐私泄露或财务损失的风险。"
    }),
  },
});

systemOne是TypeSafe SDK中用于处理这类分类请求的接口。state对象包含了Jev能够看到的所有信息:请求内容以及访客选择的分类选项。

需要注意的是,这些信息中并不包含姓名、电子邮件地址或报告编号,因为这些信息既无助于分类处理,而且属于用户的隐私数据,不应被泄露。

每个choice对象定义了一个问题及其可能的答案。标签旁边的说明告诉Jev这些标签的具体含义。对于“请求类型”这个问题,系统还会提示Jev将访客选择的分类选项视为参考信息,因为人们有时会将“报告故障”选为与实际问题无关的类别。

系统收到的响应并不会被直接视为有效信息。适配器会使用Zod规范来验证这些响应内容,确保所有答案都属于允许的范围,并且会根据Jev给出的各选项的优先级来计算出最终的置信度分数。

如果该概率值缺失,或者不在0到1的范围内,那么该请求将会被拒绝,相应的工单也会继续处于待审核状态,而不会被赋予一个人为设定的数值。

经过分类后的工单在控制面板中的显示效果:Jev将这个工单归类为中等严重程度的错误,并给出了1.00的置信度评分,同时建议将其提交到GitHub上

步骤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(`类型为${input.type},不属于错误类别`);
  if (input.route !== "engineering") reasons.push(`分配路径为${input.route},不属于工程处理流程`);
  if (!(input.confidence >= GITHUB_ESCALATION_CONFIDENCE_THRESHOLD)) {
    reasons.push(`置信度为${input.confidence},未达到最低要求${GITHUB_ESCALATION_CONFIDENCE_threshold}`);
  }
  return { eligible: reasons.length === 0, reasons };
}

routeForType函数会将每种类型的工单分配到相应的处理队列中:错误会被送入工程处理流程,功能请求会进入产品开发环节,垃圾信息会被隔离处理,其余所有工单则会被送往支持部门。由于这是一个普通的`switch`语句,因此你可以直接阅读、测试或修改这些代码,而无需对人工智能系统进行任何调整。

evaluateGitHubEscalation函数会判断某个工单是否具备被提交到GitHub上的条件:该工单必须属于错误类型,且必须处于工程处理队列中,其置信度值也必须至少达到0.9。这个函数并不会直接返回`true`或`false`,而是会列出导致工单无法被提交的理由,这些信息会在控制面板上显示出来,这样你就能清楚地知道为什么“创建GitHub问题”按钮会处于不可使用状态。

0.9这个阈值被设置在一个配置文件中,文件中明确说明这是一个未经校准的初始值,并非实际测得的准确数值。我在代码中明确写明了这一点:即使模型的评分达到了0.99,也不意味着它的预测准确率真的有99%。

步骤5:在控制面板中进行人工审核

每张工单都会被记录在一个私有的控制面板中。项目页面会显示每种状态下的工单数量:

控制面板上的项目页面,展示了两个项目中各类工作流程状态的工单数量

每个项目都有一份带有筛选功能的工单列表,这些筛选条件涵盖状态、处理路径、类型、严重程度以及参考信息:

项目的工单列表,包含筛选选项以及显示各工单的状态、处理路径、人工智能分类类型、严重程度和置信度的表格

打开某张工单后,系统会显示访客提交的报告、当前的人工智能分类结果、完整的分类历史记录,以及所有相关事件的时间线。你可以重新进行问题优先级评估,解决该工单,或者记录下某种决策——这种决策可能会改变问题的处理路径、状态或GitHub上的推荐方案。

有一点我很在意:人类做出的决策会被存储在专门的表格中,其中会注明决策者以及做出决策的原因。人工智能的处理记录永远不会被篡改。即使你覆盖了Jev的判断结果,仍然可以清楚地看到Jev当初说了什么、什么时候说的,这一点在想要了解该模型的实际表现时非常重要。

控制面板采用了Better Auth进行保护,所有的读写操作都局限于特定的工作空间范围内。进行任何修改都需要使用相同的来源请求,而且只有工作空间的所有者才能将内容发布到GitHub上。

步骤6:从缺陷报告转化为GitHub问题

当某张工单符合相关规则时,控制面板会显示即将创建的问题的预览版本:

GitHub问题升级流程中的预览界面,其中会显示问题的标题和内容,但不会显示访客的姓名和电子邮件地址

仔细观察这张截图就会发现:访客虽然留下了自己的姓名和电子邮件地址,但这些信息在上面的控制面板中确实显示出来了,但在问题的预览版本中却完全看不到。

这绝非巧合。在任何问题被创建之前,相关报告都会先通过packages/github/src/privacy.ts文件中的隐私审核机制。该机制会检查报告中是否包含电子邮件地址、电话号码、银行卡号、私钥、API令牌、JSON Web令牌或密码等信息。同时,系统还会将报告中的信息与访客提交的联系详情进行比对,这样就可以确保“嗨,我是Sam访客”这样的表述不会导致访客的姓名被泄露到公开的问题中。如果发现任何违规内容,问题的预览版本就会被阻止显示,相应的内容也不会被发布。

当你点击创建GitHub问题按钮时,还会有一些额外的安全措施被触发:

  • 使用GitHub应用而非个人令牌:该应用仅会被安装在你选择的仓库上,它只拥有编写问题内容和读取元数据的权限,没有其他任何功能。

  • 先进行声明,然后再创建问题:在真正调用GitHub接口之前,系统会先将工单的状态标记为creating,因此即使点击两次按钮,也不会创建两个重复的问题。

  • 隐藏的标识符:每个问题的内容结尾都会包含一个与该问题相关联的HTML注释。如果某个请求超时且结果未知,IssueRelay会先在自己的应用中搜索仓库中是否存在这个特定的标识符,然后再尝试再次处理该问题。它绝不会盲目地重复尝试已经创建过的问题。

问题升级后的处理记录,其中显示了关联的GitHub问题以及时间线上的升级事件

这是IssueRelay根据访问者的报告,在我的项目代码库的公共仓库中创建的一个真实问题。这个问题是由应用程序的机器人生成的,被标记为bug,其中包含了报告内容以及人工智能给出的分类结果,但并没有联系方式:

由IssueRelay机器人在一个公共仓库中创建的真实GitHub问题,其中包含问题的摘要、报告内容、相关背景信息,以及“联系方式永远不会被公开”这一说明

步骤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会使用只有它自己和你的平台才知道的密钥来为每一条传输信息生成签名,并将这个签名放在X-Hub-Signature-256头部字段中。而这个函数则会根据原始请求数据字节重新计算出一个HMAC SHA256签名,然后对比这两个签名是否一致。

有两条细节很容易被忽略或弄错:首先,签名必须是根据GitHub实际发送的原始数据字节来计算的,任何形式的JSON解析都会改变这些字节的内容;其次,进行比较时使用了timingSafeEqual这个函数,无论第一个字节还是最后一个字节有所不同,这个函数计算所需的时间都是相同的,因此攻击者无法通过测量响应时间来逐个字符地猜测签名内容。此外,这个函数在遇到任何错误时都会返回false,而不会具体说明是哪种错误,这样一来就没有任何信息会被泄露。

在完成签名验证后,IssueRelay会为每一条传输记录保存一个唯一的ID,这样重复的传输请求就会被忽略。它只会更新与特定应用程序安装版本和代码库相关的问题,并且会按照这些问题在GitHub上实际发生的顺序来处理它们,而不是按照接收到的顺序。

如何部署你自己的IssueRelay

你可以在大约15分钟内通过Vercel和Neon来部署你自己的IssueRelay。包括故障排除在内的完整操作指南请参见docs/SELF_hosting.md。以下是简要说明。

步骤1:部署

推荐的做法是使用README文件中的通过Vercel进行部署选项。该选项会将代码仓库复制到你的GitHub账户中,同时创建一个Neon数据库,并要求你提供三个随机生成的密钥。

第一次构建时会故意出现错误,因为Vercel的克隆界面没有设置“根目录”的选项,所以你需要在项目设置中将“根目录”设置为apps/web,然后再重新进行部署。这样,在生产环境中构建时,系统会自动为你创建所有的数据库表。

指南中还提到了另一种分支并导入的方法,这种方法可以让你以后只需点击一次按钮就能完成更新操作,但这种方法目前还没有经过全面测试。

步骤2:运行设置页面

在新建的网站中打开/setup路径。这个页面只有在数据库中还没有任何账户信息,并且你输入了在部署过程中生成的SETUP_TOKEN时才会生效。这样,如果有人先找到了你的网站地址,也无法占用你的平台资源。该流程会为你创建所有者账户和第一个项目,同时还会显示你的插件密钥以及可以直接粘贴使用的插件代码。

首次运行设置页面时需要输入的字段包括设置令牌、所有者账户、工作空间名称、站点名称及站点地址

站点地址字段的格式为http://localhost:3000,这是因为Next.js应用是在你的计算机上运行的。你也可以添加你的实际域名,比如https://my-site.vercel.app。如果你忘记了设置这个地址,插件会向访问者显示“我们无法发送您的消息”,因此当报告没有按时到达时,首先检查这个地址是非常重要的。

步骤3:创建GitHub应用

手动配置GitHub应用的过程中很容易出现一些错误。最常见的问题就是忘记订阅“Issues”事件,我在测试过程中就犯过这个错误。因此,IssueRelay提供了一个命令,可以帮助你根据配置文件自动创建GitHub应用:

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

执行这条命令后,GitHub会自动在浏览器中打开相关页面,所有必要的信息都已经填好:这个应用是私有的,具有提交问题以及查看元数据的权限,并且已经订阅了“Issues”事件,其Webhook也会指向你的平台。

你只需点击创建GitHub应用按钮,GitHub就会将你重定向到由该命令启动的临时本地服务器上。此时,系统会把应用ID、私钥以及Webhook密钥写入一个git不会识别的文件中,这些信息永远不会在终端窗口中显示出来。之后,终端会提示你进入下一步设置环节。

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

将GitHub应用所需的三个参数以及你的TYPESAFE_API_KEY添加到Vercel项目的环境变量中,然后重新部署。对于处理GitHub问题而言,Jev是必不可少的;如果没有它,报告仍然会显示在仪表板上,但这些报告无法被转化为正式的问题。

步骤5:连接你的仓库

将该应用安装在托管 Widget 的网站的仓库中,然后打开仪表板中项目的设置页面进行连接。系统会询问GitHub,哪个安装版本以及哪个仓库ID与当前名称对应,这样就能确保不会错误地链接到错误的仓库。

项目设置页面,其中包含Widget密钥、可粘贴的Widget代码、允许访问的网站地址以及已连接的GitHub仓库

步骤6:安装Widget

使用设置页面中的代码将Widget安装在你的网站上,然后发送第一份报告。

为了确保后续使用的副本始终是最新的版本,请从主仓库拉取最新的变更。本指南主要介绍了使用“部署”按钮创建副本时所需进行的一次性操作,因为这些副本并不是GitHub上的分支。

在真实网站上运行它

虽然演示只是为了说明功能,但我还是决定真正将IssueRelay应用到实际项目中。现在,这个Widget已经在我个人网站andrewbaisden.com上的作品集中开始运行了:

IssueRelay Widget显示在作者个人网站的角落处,背景是伦敦的街道场景

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

在安装和测试这个Widget的过程中,我学到了一些东西。我的作品集在测试时仍然使用的是React 18,而应用路由系统已经使用了React 19。因此,我首先将作品集升级到了React 19,并确保所有现有的测试都能通过,之后才添加了Widget。这个Widget能够适应网站的不同主题设置,会显示在页面的右下角,并且在我的作品集仓库中还有针对它的单元测试和浏览器测试。

随后,我像普通用户一样测试了这个Widget。我从实际运行的网站上发送了三份报告:一份是疑问,一份是漏洞报告,还有一份是功能需求请求。Jev对这三份报告的分类完全符合我的预期,评分都在0.95到1.00之间,系统也正确地将它们分派到了相应的支持团队、工程团队和产品开发团队。其中那份漏洞报告在我的公开作品集仓库中被标记为问题#3,也就是之前截图中显示的那个问题。我在报告中附上了自己的姓名和电子邮件地址,但这些信息并没有出现在公开的issue页面上。

端到端测试及其收获

我并不想要一个只在我的机器上才能正常运行的项目,因此测试是每个开发阶段的必备环节,而不是被留到最后才进行的事情。

我们的测试套件包含多个层次:

  • 单元测试涵盖了组件、API接口、人工智能相关逻辑、隐私保护机制以及设置页面等功能。这些测试一共有180个,而且其中没有任何一个需要使用数据库。

  • 数据库集成测试是在专门的PostgreSQL测试数据库上进行的,其中包括一些并发性测试,用于确保用户两次点击操作不会导致在GitHub上创建两个问题。

  • 使用Playwright进行的浏览器测试:这些测试会在不同的端口上启动独立的服务器,并且每次运行都会重新创建数据库,因此测试过程永远不会接触到真实数据。其中有一台服务器会使用空数据库来测试初始设置页面的功能。

  • 包检查:该步骤会生成精确的npm压缩包,并将其安装到遵循严格内容安全策略的Vite项目中,同时也安装在Next.js项目中。这些操作都是在单独的仓库之外进行的,完成后会在每个项目中生成相应的报告。

  • 实时运行流程测试:这项测试会针对真实的GitHub应用以及临时创建的仓库进行20项检查。包括提交报告、问题分类处理、预览结果、创建问题记录、确认没有私人信息被公开披露、在GitHub上关闭问题记录,然后等待Webhook通知,重新打开问题页面并检查整个流程是否正常。

我总共进行了三次这样的实时运行流程测试:第一次是在本地机器上通过隧道连接进行的;第二次是在生产环境中进行的;最后一次则是使用仅按照设置指南创建的全新副本进行的。三次测试都通过了全部20项检测。

不过,比测试是否通过更有趣的是,每个测试阶段所暴露出来的问题:

  • “Issues事件”容易被忽略:当我第一次手动创建GitHub应用时,没有配置任何事件订阅机制,因此GitHub从未向IssueRelay发送过问题关闭的通知。正是这个错误导致了create-app命令的存在。

  • 访问者会在报告中提到自己的名字:如果有人名叫Sarah却提交了“页面出错了”这样的报告,那么她的名字就会出现在公开的 issue 中。现在,隐私保护机制会将每份报告与其中提供的联系信息进行比对。

  • Zod框架与严格的内容安全策略在浏览器中无法兼容:Zod 4会在运行时尝试使用new Function函数,而那些采用严格内容安全策略的网站会将这种行为视为违规。我最终去掉了组件中的Zod框架,转而编写了简单的验证代码,并通过测试确认这些验证代码与服务器上的Zod架构是匹配的。

  • Vercel的部署流程中没有“根目录”选项,且框架的选择只能进行一次:我的两次全新部署尝试都失败了:第一次是因为Vercel错误地构建了仓库的根目录结构;第二次则是因为框架设置仍然被选为“其他”。现在,我们在vercel.json文件中明确指定了要使用的Next.js框架,而且指南中也对这种可能导致的错误进行了提示。

  • 部署按钮生成的副本并不是分支版本:直接从主仓库执行git pull操作是无法完成合并操作的,因此现在指南中提供了专门的命令,用于将副本与主仓库连接起来。

  • :最初我将Jev插件设置为可选选项,但经过仔细检查后发现,如果没有这个插件,就无法生成满足要求的测试报告。现在,指南和设置页面都明确指出了这一点。

  • <日志信息中的冗余内容也会影响测试结果

    :每次数据库连接操作都会在错误级别记录SSL警告信息,这使得正常的部署过程看起来似乎出现了问题。为了解决这个问题,我们明确了驱动程序实际使用的SSL模式,这样一来,警告信息虽然仍然会出现,但证书验证的过程却没有任何变化。

最让我印象深刻的一点是:严格按照自己的文档进行部署,往往能够发现那些测试程序无法发现的错误。上面提到的所有部署问题,在自动化测试中都是无法被检测出来的;而只有当真正有人按照文档中的步骤来操作时,这些问题才会暴露出来。

开发过程:阶段划分与人工智能辅助开发

IssueRelay是分阶段开发的,每个阶段在开始下一个阶段之前都会进行书面交接:

阶段 成果
0 产品定义、架构设计、相关决策的制定、安全措施的实施以及测试计划的编制
1和2 单仓库基础架构的建设、领域模型的构建、PostgreSQL数据库模式的设计以及初始数据的准备
3和4 小工具的开发、演示网站的搭建以及公共工单API的实现
5和6 利用Jev进行人工智能辅助问题分类处理,同时开发操作员控制面板
7和8 确认在GitHub上进行问题上报流程的设置,并完成webhook同步功能的开发
9 在一个临时仓库中全面验证整个系统的运行流程
10 系统进行生产环境下的优化调整
11和12 验证小工具的功能,并将其发布到npm仓库中
部署 在生产环境中使用Vercel、Neon及Resend等工具进行部署
13 将开发好的小工具添加到我的个人作品集中
14和15 进行内部测试(目前仍在进行中)
16 实现自我托管功能:包括部署按钮的设计、设置页面的配置、项目参数的调整以及App manifest命令的使用

我的开发环境配置

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

  • 使用Ghostty作为终端工具,同时运行Claude Code、Codex和OpenCode程序

  • 使用Cursor作为代码编辑器

  • 同时使用ChatGPT、Claude和OpenCode的原生桌面应用程序

在开发IssueRelay的过程中,我主要使用了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不得开始下一阶段。对于那些规模较小、易于审查的修改来说,进行检查要容易得多,而试图修改一个复杂的功能则会带来巨大的麻烦。

  • 在编写代码之前先制定计划:对于规模较大的项目阶段,我会要求先制定一份计划(“先制定计划,待我批准后再开始执行”)。阅读一份计划只需要两分钟,而纠正一个错误的实现方案却可能需要花费一整个下午的时间。

  • 将规则明确记录在代码仓库中:`AGENTS.md`文件中明确了项目的各项规则,例如“在外部AI或GitHub发起请求之前,必须先处理已接受的请求”、“永远不要将联系信息发布到GitHub上”,以及“对于那些描述模糊的GitHub问题,切勿盲目尝试重新提交”。每次AI运行时都会读取这些规则,因此这些规则不会因为我忘记提醒而失效。

  • 如实汇报结果:相关说明明确规定,绝不能将未执行的检查报告为通过结果,而且每次交接工作时都会记录实际执行的命令及其真实结果,包括失败情况。

  • 明确提交代码的条件:像“当测试通过且没有其他问题时再提交代码”这样的要求,意味着在任何代码被推送到主分支之前,都必须先运行完整的测试套件。

  • 要求提供证据而非空口承诺:我不会直接问“自我托管功能是否可行?”,而是会让AI按照新的部署指南来验证这一功能。正是这个简单的请求,帮助我们发现了七处与文档或配置相关的问题。

  • 将实际使用中的反馈纳入开发流程:

    当我自己部署测试环境并记录下所有遇到的问题时,这些信息都会被直接添加到指南、设置页面中。

将插件发布到npm

在IssueRelay项目中,只有这个插件被发布了,其包名为@issuerelay/widget,可以通过进行下载。项目的其他部分都保留在私有代码仓库中。

我并不想发布一个只在我自己的工作环境中才能正常使用的插件,因此发布流程会生成一个npm能够接收的压缩包,并对其进行严格检查。

这个压缩包必须只包含五个文件,且不得引用任何私有包、Node内置模块、环境变量,或者任何看起来像密钥的文件。此外,这个插件必须在两个全新的应用程序中正常运行,其中一个应用程序还遵循严格的Content Security Policy政策,且不能有任何违规行为。

发布过程是通过GitHub Actions来完成的,同时使用了npm提供的安全发布机制和来源验证功能,因此不存在长期有效的npm访问令牌泄露的风险。

最终生成的插件压缩包大小约为10KB,使用时不需要进行任何CSS配置,它仅依赖于React和React Hook Form框架。通过这个插件的README文件可以了解其所有的使用参数和配置方法。

下一步计划

IssueRelay已经完全可以用于自主托管了,我每天都在我的作品集中使用它。接下来,我有几件事情想要尝试:

  • 推出IssueRelay的托管版本,这样用户就可以直接注册并添加该插件,而无需自己进行任何部署操作。

  • 对“分叉与导入路径”的功能进行端到端的测试,以便将其确立为推荐的部署方式。

  • 实现路线图中规划的一些功能,比如检测重复的报告、将多份报告关联到同一个问题上、发送通知以及同步GitHub上的评论等内容。

总结

通过本教程,您了解了如何构建这样一个AI支持系统:它能够将分散在各个网站上的用户反馈整理成经过审核的工单,并将确认存在的漏洞信息发送到GitHub上。在这个过程中,您学会了以下方法:

  • 如何创建一个可嵌入到任何网站中的React插件,这种插件无需进行任何CSS配置,也不会导致样式冲突。

  • 在调用任何外部服务之前,先保存所有的用户反馈信息,这样即使外部服务出现故障,数据也不会丢失。

  • 如何使用Jev来进行有约束条件的分类分析,该工具能够返回标签和概率值,而非纯文本形式的结果。

  • 如何将路由决策和发布逻辑编写成清晰、可测试的代码,并确保有人会定期审查这些代码。

  • 如何利用GitHub App、隐私保护机制以及防止重复提交的机制,来安全地创建GitHub工单。

  • 如何通过经过签名验证的Webhook,让GitHub与您的仪表板保持数据同步。

  • 如何将自己开发的版本部署到Vercel和Neon平台上,并进行端到端的测试,包括对照您自己的文档进行验证。

  • 了解IssueRelay的最佳方式就是亲自尝试使用它。您可以通过在GitHub上查看源代码,按照自主托管指南来部署自己的版本,也可以通过npm install @issuerelay/widget(从npm仓库下载)将该插件添加到您的网站中。如果这对您有帮助的话,在该代码库上点个星标也是非常有意义的哦。

相关文章

技术实践

如何利用Claude API构建一个可靠的人工智能助手

大型语言模型能够回答问题、总结文档、编写代码,还能与外部系统进行交互。但构建一个可靠的人工智能应用,仅仅发送指令并显示响应是远远不够的。 一个可用于实际生产的应用程序必须能够管理对话历史记录、提供相关的上下文信息、安全地使用各种工具、处理不同类型的响应,并判断生成的内容是否有用。 在本教程中,我们将构建 ShopHelper ——这个为虚构在线商店设计的客户支持助手。完成制作后,ShopHelper将能够做到以下几件事: 以统一的语气回答常见问题 记住顾客之前说过的话 通过调用代码中的函数来查询订单状态 安全地处理Claude给出的多段式响应 利用工作流程来处理客户支持请求 评估修改提示语后效

阅读全文
技术实践

如何使用Next.js、Supabase以及TypeSafe Jev来构建一个用于筛选人工智能相关简历的工具

每当我们发布一份工程招聘启事时,一周内就会收到300到400份简历。仔细阅读每一份简历大约需要两分钟的时间。因此,仅仅为了处理一个职位空缺,我们在开始任何面试之前就已经要花费11个小时来处理这些简历了。 但实际上,并没有人会真的去阅读每一份简历。相反,人力资源部门会进行快速的筛选工作。他们会快速浏览应聘者的职位名称、工作经验年限以及所掌握的技术框架,然后在大约15秒的时间内,将简历分为“需要进一步查看”和“可能不符合要求”两类。当他们看到第40份简历时,他们对这份简历的关注程度就已经远远低于对第4份简历时的关注了。 我们的目标就是取代这种快速筛选的过程,而不是放弃阅读简历这一环节。当阅读简历确

阅读全文
技术实践

iOS NFC使用指南:如何使用React Native读取、写入NFC标签以及锁定这些标签

将iPhone靠近贴纸,就会发生一些奇妙的事情:名片会自动添加到联系人列表中,某个聚焦操作会结束,或者某扇门会自动打开。这种芯片的成本大约为20便士,其存储容量约为130字节。 读取一条NFC信息需要执行两次函数调用;而要获得执行这些调用的权限,则需要花费更长的时间。之后,CoreNFC还会要求你再次完成这个流程。 第一个障碍来自苹果公司:你需要拥有一个付费开发者账户,在某个网站平台上注册应用ID,勾选相关选项,并重新生成配置文件。如果其中任何一步出错,构建过程就会因为代码签名错误而失败,而这些错误信息中根本不会提到“NFC”这个词。 第二个障碍则来自CoreNFC本身,而且没有人会提醒你注意

阅读全文
技术实践

如何利用功能标志来实现安全、渐进式的功能推出

功能开关是团队在部署过程中最强大的工具之一。它们将 部署 与 发布 分离开来,这意味着你的持续集成/持续交付流程可以在每次代码合并时都将更新推送到生产服务器上,但只有当你明确启用这些功能开关时,用户才会看到新的变化。 “部署”是一个技术性操作,而“发布”则是一项产品决策。正是这种分离机制,使得本文中讨论的诸多内容成为可能。 然而,如果实现不当,功能开关反而会带来技术债务、测试难题以及运行时的复杂性。在这篇文章中,你将学习到实施功能开关的核心方法——从简单的布尔值切换,到基于百分比的比例化部署方案,再到针对不同用户群体的功能启用策略,同时还会了解相关的生命周期管理方法及应避免的错误做法。以下就是

阅读全文