
Jev
Jev 驱动的网站支持系统搭建教程:从访客反馈到 GitHub Issue 的完整链路
访客的反馈常常散落在邮箱、社交平台和表单里,等你动手修的时候已经缺东少西。这套方案把反馈先存进自己的数据库,再由 Jev 做有边界的分类判断,人工确认后才生成 GitHub Issue,并在 Issue 关闭时反向同步工单状态。本文讲清前置条件、各步骤的真实代码、一个能跑通的最小示例,以及部署与测试中容易踩的坑。
网站只要对外开放,就一定会收到反馈,而这些反馈的落点往往很尴尬:有人发现按钮坏了,发邮件告诉你;有人在社交平台上留言说某个页面在手机上打不开;还有人通过联系表单提了一个功能想法,它静静躺在收件箱里,夹在一封订阅邮件和一张收据之间。等你终于坐下来处理时,缺陷报告散落在三个地方,一半缺关键信息,少数被手工复制进 GitHub 的还带着访客的邮箱地址。这套支持系统的目标就是解决这个问题:给 React 网站挂一个轻量反馈组件,访客可以提问、报缺陷、提功能建议;每条报告先落进你自己的数据库,再由 Jev 分类,由代码里的明文规则决定去向,最后在私有面板里人工复核。确认是真缺陷后,才生成一条干净的 GitHub Issue,访客的私人信息不会跟着出去;之后在 GitHub 上关闭该 Issue,对应工单也会同步关闭。站内有一个相关工具页可以参考:Jev。
准备工作
要跟着部署一套自己的副本,需要先确认下面这些条件。它们大多不是 AI 相关的东西,而是普通的 Web 工程依赖。
- React、Next.js 与 TypeScript 的实操经验:平台侧使用 Next.js App Router,反馈组件本身是一个 React 组件。不熟悉 App Router 的客户端组件边界,会在挂载组件时卡住。
- Node.js 24 与 pnpm:本地运行项目、以及使用那条创建 GitHub App 的命令都需要它们。
- 一个 GitHub 账号:以及你打算安装反馈组件的那个网站或应用的仓库。确认后的缺陷最终会变成这个仓库里的 Issue。
- 一个 Vercel 账号:免费套餐就够用。PostgreSQL 数据库通过 Vercel 的应用市场接入,Neon 本身也有免费方案。
- 一个 TypeSafe 账号:用于调用 Jev。需要先在 TypeSafe 控制台拿到 API key,否则任何报告都无法变成 GitHub Issue。
- 一个 React 网站:能加组件即可,Next.js 站点是最省事的起点。
- 可选:一个 Resend 账号:如果你需要密码重置这类账号邮件。
不需要是 AI 专家。Jev 通过一个小而类型化的 SDK 调用,整套系统里真正有意思的部分都是常规 Web 工程:数据库、输入校验、认证、Webhook。
这套系统适合谁
支持系统听起来像是大公司才需要的东西,但它解决的问题几乎出现在每一类网站上:
- 作品集站点会收到招聘方的留言、关于项目的提问,以及某个浏览器上页面坏掉的报告。
- SaaS 产品会把缺陷报告、账单问题和功能请求混在一起,每一类都需要不同的处理路径。
- 文档站点会收到「这个示例跑不通」的报告,而它其实是产品本身的缺陷。
- 开源项目会遇到那种不愿意自己开 GitHub Issue、但很乐意在网站上点一下按钮的用户。
- 你替客户做的站点,反馈会先到客户手里,几天后才被转发给你,而且没有任何细节。
一套好的系统应该做到三件事:所有报告汇到一个地方;即使其他服务挂了,每条报告也安全留存;报告被分好类,让你的时间花在真正重要的那些上。AI 只负责分类这一环,而且绝不能让它做主。模型可以非常自信地给出错误结论,而公开的 GitHub Issue 不是可以靠猜测创建的东西。所以整套系统贯穿一条规则:AI 只做推荐,人来做确认,代码来执行规则。
操作步骤
第一步:在站点里挂载反馈组件
反馈组件是一个普通的 React 组件,从 npm 安装:
npm install @issuerelay/widget
然后在根布局里渲染一次,例如放在你自己的客户端组件中:
"use client";
import {
HttpSupportSubmissionClient,
SupportWidget,
} from "@issuerelay/widget";
const submissionClient = new HttpSupportSubmissionClient({
apiBaseUrl: "",
});
export function Support() {
return (
<SupportWidget
projectKey="pk_your_project_key"
submissionClient={submissionClient}
theme="system"
position="bottom-right"
/>
);
}
这里有两个角色要分清。HttpSupportSubmissionClient 负责和你的平台通信:它把每条报告 POST 到你的接口,不带 cookie、不带凭据,并且给每条报告一个提交 ID,这样网络出错后重试不会产生第二张工单。SupportWidget 是访客看到的按钮和面板。projectKey 告诉平台这条报告属于哪个项目,它是公开标识而不是密码,所以放在站点代码里是安全的;真正的防护在服务端,服务端只接受来自该项目已登记站点地址的报告。
"use client" 这一行是必需的,因为提交客户端在浏览器里创建。在 Next.js App Router 中,做法就是像上面这样把组件包进你自己的小客户端组件,再从布局里渲染它。组件内部渲染在 Shadow DOM 中并自带样式,所以你的站点不需要 Tailwind,也不需要引入 CSS,你的样式也不会意外改到它。
这段代码不必手写。项目设置页会直接给出填好平台地址和项目 key 的同一段代码,复制即可。
第二步:先落库,再思考
报告到达接口后,第一件事是保存,不是分类,也不是转发,而是在一个事务里写进 PostgreSQL。这是整套系统里最重要的设计决定。
AI 服务会宕机,GitHub 也会宕机。如果平台在保存之前先调用 Jev,而 Jev 超时了,访客的消息就丢了,而且他们永远不会知道。所以规则很简单:只有安全存储之后,报告才算被接受,后续任何一步失败都不能把它抹掉。如果 Jev 不可用,工单就在面板里等着,等你重新跑一次分类。
保存之前,接口会检查几件事:
- 请求体符合共享的 Zod 契约,非法输入会被明确报错拒绝。
- 项目 key 存在,且请求来源属于该项目登记的允许站点地址之一。
- 项目未超出配额限制。
此外还有针对重复提交的防护和按项目维度的速率限制。这些检查都发生在写库之前,所以被拒绝的请求不会留下垃圾数据。
第三步:让 Jev 做有边界的分类
Jev 是 TypeSafe 提供的模型,面向的是所谓「System One」类任务:快速、有边界的判断,而不是长篇开放式写作。用法不是让模型写一段话再去解析,而是给它一些状态和一组问题,每个问题都有固定的候选答案。Jev 为每个问题挑一个答案,并返回它给每个选项分配的概率。
这个形状恰好就是支持工单分类需要的:一张工单是缺陷、提问、功能请求、账单问题还是垃圾信息;严重程度是低、中、高还是紧急。没有文本可供模型自由发挥,没有提示注入能让它去写 Issue 标题,也没有自由文本需要事后清理。输出就是一个标签加一个数字,代码可以同时校验两者。
它同时也便宜且快。按撰写时的公开信息,TypeSafe 列出 Jev 的价格是每十亿输入 token 42 美元,而一条支持消息只有几十个 token。价格与配额随时可能调整,请以官网当前信息为准。
集成时只把访客的消息和他们选择的主题发给 Jev,绝不发送姓名、邮箱、工单 ID 或任何可以识别到人的内容。这一点是硬约束,不是可选项。
第四步:人工复核与 GitHub 升级
分类结果进入私有面板,面板提供筛选、分类历史,以及和 AI 输出分开存储的人工复核决定。分开存储很关键:AI 的判断和人的判断是两条记录,事后可以对比、可以追责,也不会因为重新跑一次分类就把人的结论覆盖掉。
只有当所有者确认了预览之后,GitHub App 才会创建 Issue。创建前会经过一道隐私闸门,访客的联系方式不会离开平台。生成的 Issue 是干净的:标题、正文、标签都来自模板和已确认的内容,不包含私人信息。
第五步:双向同步
在 GitHub 上关闭或重新打开 Issue,会通过带签名的 Webhook 更新工单状态。签名校验是必须的,否则任何人都能伪造一个请求来改你的工单。Webhook 载荷同样要过 Zod 校验,因为它也是一条跨越信任边界的输入。
一个完整示例
下面把一条报告从浏览器走到 GitHub Issue 关闭的全过程串一遍,这是能跑通的最小路径。
- 访客打开组件,在站点右下角看到按钮,点开后选择一个主题,描述问题,可选地留下姓名和邮箱以便你跟进。
- 组件提交报告。提交客户端把报告 POST 到你的平台,不带凭据,并带上一个提交 ID。
- 平台校验并落库。请求体过 Zod 契约,项目 key 与来源地址校验通过,速率与重复检查通过,然后在事务里写入 PostgreSQL,并返回一个支持编号,访客之后可以引用它。
- 报告变成工单,出现在面板里。此时即使 Jev 不可用,这条记录也已经安全了。
- Jev 分类。平台把访客的消息和所选主题发给 Jev,拿回类型标签与严重程度,以及每个选项的概率。
- 你复核。在面板里看到分类结果、原始消息和概率,做出自己的判断。你的决定和 AI 的输出分开存储。
- 确认是真缺陷后创建 Issue。你确认预览,GitHub App 在你的仓库里创建一条 Issue,访客的联系方式被隐私闸门拦下,不会出现在 Issue 里。
- 你在 GitHub 上关闭 Issue。带签名的 Webhook 打到平台,校验通过后,对应工单同步关闭。
整个部署过程按素材的说法大约 15 分钟可以完成,前提是账号和密钥都已经准备好。
技术栈与代码边界
平台使用一套现代 TypeScript 栈:
- Next.js 16(App Router)与 React 19,用于平台和面板。
- 严格模式的 TypeScript,所有跨越信任边界的输入都用 Zod 校验:公开 API 请求、环境变量、AI 输出、GitHub Webhook 载荷。
- PostgreSQL 搭配 Drizzle ORM,迁移脚本经过人工审阅。
- Better Auth 负责面板账号。
- TypeSafe 官方 SDK 调用 Jev,Octokit 处理 GitHub App。
- Vitest、React Testing Library、Playwright 负责测试,Biome 负责 lint 与格式化。
- pnpm workspaces 把一切放在一个 monorepo 里。
- 生产环境使用 Vercel、Neon 与 Resend。
monorepo 被拆成若干职责单一的小包:
| 包 | 职责 |
|---|---|
| apps/web | 平台本体:公开工单接口、面板、初始化设置、GitHub Webhook |
| packages/widget | 发布到 npm 的浏览器组件,绝不引入服务端代码 |
| packages/support-contracts | 组件与接口共享的请求和响应结构 |
| packages/db | Drizzle schema、迁移,以及所有数据库查询 |
| packages/ai | Jev 适配器、分类服务与路由策略 |
| packages/github | GitHub App 客户端、Issue 草稿、隐私闸门与 Webhook 处理 |
| packages/auth | Better Auth 配置、会话与工作区成员校验 |
这些边界比看上去更重要:React 组件从不直接和 GitHub、Jev 或数据库对话;浏览器代码里永远不含密钥;AI 包不能引入数据库。把这些线划严之后,系统更容易测试,也更容易推理,这也是组件能发布到 npm 而不拖带任何服务端代码的原因。
注意事项
- AI 不能做主。模型可以非常自信地给出错误结论,而公开的 GitHub Issue 不该靠猜测创建。整套流程必须保持「AI 推荐、人工确认、代码执行规则」。
- 先落库再调用外部服务。如果先调 Jev 再保存,Jev 超时就会永久丢失访客的消息,而且访客毫无感知。任何后续步骤的失败都不能抹掉已接受的报告。
- projectKey 不是密钥。它是公开标识,放在前端是设计如此。真正的防护是服务端只接受该项目登记的站点地址发来的报告,不要指望靠隐藏 key 来防护。
- 只把必要内容发给模型。姓名、邮箱、工单 ID 等可识别个人的信息不应进入 AI 调用。
- Webhook 必须验签。否则任何人都能伪造请求改动工单状态;载荷也要做结构校验。
- 价格、配额与套餐随时可能变化。文中提到的模型定价、免费方案等信息请以各服务官网当前信息为准。
- 版本与依赖会演进。Node.js、Next.js、React 等版本要求以项目仓库当前的说明为准。