利用 Trigger.dev 和Gemini AI构建Next.js文本摘要器
现代网页应用越来越依赖AI功能,这些功能可能需要几秒甚至几分钟才能完成。直接在Next.js路由或服务器操作中运行这些操作,一旦接近平台执行极限,可能会变得不可靠。

Trigger.dev 将长期运行的工作转移到专用的后台运行时。你的应用程序不是在工作完成时保持 HTTP 请求开启,而是排队任务,跟踪其进度,并在处理完成时获取结果。
在本教程中,我们将用Next.js、Trigger.dev 和Google Gemini API构建文本摘要器。在这个过程中,你将学会如何定义持久的后台任务,修复:Windows 中“发送到”菜单缺少“邮件收件人” - 致知笔记 - 专注教程知识分享 如何从服务器操作触发它们,配置自动重试,并将实时运行更新发送回React前端。
同样的模式适用于较长的AI工作流程、媒体处理、Webhook处理、定时工作以及其他不应依赖单一请求存活的任务。
什么是 Trigger.dev?
Trigger.dev 是一个开源的后台作业平台,用于运行异步和长时间运行的工作流程,无需维护自己的队列和工作者基础设施。
它提供:
-
应用程序请求生命周期之外的长期任务执行
-
可配置自动重试
-
队列、延迟作业与调度
-
实时日志与运行可观察性
-
弹性任务执行
-
托管云托管或自托管,使用 Docker 和 Kubernetes
在开始之前,有三个概念值得理解:
| 概念 | 它的代表意义 |
|---|---|
| 任务 | 定义背景工作单位的函数 |
| 跑 | 任务的一次执行,包括状态、日志、输出和重试 |
| 触发器 | 启动任务的事件,如服务器动作、webhook、HTTP 请求或调度 |
在我们的应用程序中,摘要逻辑是任务,每提交一个文档创建一个运行,Next.js服务器动作提供触发条件。
前提条件
你需要:
-
Node.js 20岁或更晚
-
对Next.js和React有基本熟悉
-
Gemini 的 Google AI Studio API 密钥
-
一个 Trigger.dev 的记载
在Next.js中设置 Trigger.dev
我们将利用 Trigger.dev 将双子座请求从Next.js请求生命周期中移出。
如果你还没有Next.js应用,可以用TypeScript、Tailwind CSS和应用路由器创建一个:
npx create-next-app@latest text-summarizer --typescript --tailwind --app
进入项目目录:
cd text-summarizer
创建 Trigger.dev 项目
打开 Trigger.dev 仪表盘,创建一个新项目。如何在 iPhone 和 Mac 上查找 Wi-Fi 密码 - 致知笔记 - 专注教程知识分享在这个例子中,我们称之为AI摘要器。
项目作为你后台任务的工作区。Trigger.dev 记录每次运行,包括执行历史、日志、重试、元数据和输出。
创建项目后,仪表盘会为你的应用程序提供初始化命令:
复制命令,从你Next.js项目的根执行:
按照提示用你的 Trigger.dev 账户授权CLI。
初始化会将 Trigger.dev 配置和任务目录添加到你的项目中:
-
src/trigger/包含 Trigger.dev 在开发和部署过程中发现的背景任务
-
trigger.config.ts包含该项目的SDK配置
我们还会使用 Trigger.dev 的 React 钩子订阅客户端的更新。如果你用的是pnpm或其他包管理器,请相应调整安装命令:
npm install @trigger.dev/react-hooks
同时安装Google GenAI SDK:
npm install @google/genai
一起运行Next.js和 Trigger.dev
在本地开发过程中,Next.js服务器和 Trigger.dev 开发工作者都需要同时运行。
你可以在不同的终端里运行它们。为了让工作流程集中在同一个终端,安装:concurrently
npm install concurrently
然后更新 的部分:scripts``package.json
{
"scripts": {
"trigger:dev": "npx trigger.dev@latest dev",
"dev": "concurrently --kill-others --names \"next,trigger\" --prefix-colors \"yellow,blue\" \"next dev\" \"npm run trigger:dev\""
}
}
两个开发流程的起点是:
npm run dev
Next.js应用和 Trigger.dev 工作者现在并行运行。
构建摘要器
我们将分三步构建该应用:
-
定义后台任务
-
通过服务器操作触发它
-
订阅客户端的运行
首先,添加所需的环境变量。
添加环境变量
在项目根创建:.env.local
TRIGGER_SECRET_KEY=tr_dev_your_trigger_secret
GEMINI_API_KEY=AIzaSy_your_gemini_api_key
服务器端保持两个数值。如何检查手机是否已解锁 - 致知笔记 - 专注教程知识分享浏览器将获得一个范围限定的公共访问令牌,用于订阅后续的单个 Trigger.dev 运行。
创建后台任务
创作:src/trigger/summarize.ts
import { schemaTask, logger, metadata } from "@trigger.dev/sdk";
import { GoogleGenAI } from "@google/genai";
export interface SummarizePayload {
text: string;
}
export interface SummarizeOutput {
summary: string;
}
export const summarizeTask = schemaTask({
id: "summarize-text",
retry: {
maxAttempts: 5,
minTimeoutInMs: 2000,
maxTimeoutInMs: 10000,
factor: 2,
randomize: true,
},
schema: (data: unknown): SummarizePayload => {
if (!data || typeof data !== "object") {
throw new Error("Payload must be an object");
}
const { text } = data as { text?: unknown };
if (typeof text !== "string") {
throw new Error("Text must be a string");
}
const trimmed = text.trim();
if (trimmed.length < 20) {
throw new Error(
"Text is too short. Minimum length is 20 characters."
);
}
if (trimmed.length > 10000) {
throw new Error(
"Text is too long. Maximum length is 10,000 characters."
);
}
return { text: trimmed };
},
run: async (
payload: SummarizePayload,
{ ctx }
): Promise<SummarizeOutput> => {
logger.log("Starting text summarization task", {
textLength: payload.text.length,
attempt: ctx.attempt.number,
});
metadata.set("progress", {
percentage: 10,
status: "Initializing task",
});
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey) {
metadata.set("progress", {
percentage: 10,
status: "Configuration error",
});
throw new Error(
"GEMINI_API_KEY environment variable is not configured."
);
}
metadata.set("progress", {
percentage: 30,
status: "Contacting Gemini AI",
});
const ai = new GoogleGenAI({ apiKey });
const response = await ai.models.generateContent({
model: "gemini-3-flash-preview",
contents:
`Summarize the following text for a busy developer. ` +
`Keep it concise and actionable.\n\n` +
`Text to summarize:\n${payload.text}`,
});
metadata.set("progress", {
percentage: 80,
status: "Processing response",
});
const summary = response.text?.trim();
if (!summary) {
throw new Error("Gemini returned an empty response.");
}
metadata.set("progress", {
percentage: 100,
status: "Done",
});
return { summary };
},
});
该任务注册为唯一ID 。summarize-text
其重试政策允许最多五次尝试。重试间隔从最小2秒指数增长到最多10秒,采用随机化以防止多个失败作业同时重试。
在任务运行前,它会验证收到的有效载荷。在这个例子中,输入必须包含一个介于20到10,000字符之间的字符串。schema``text
在 中,任务初始化 Gemini,发送摘要请求,并记录 Trigger.dev 元数据的进展。如果 Gemini 返回空结果或请求失败,任务会抛出错误,Trigger.dev 可以根据上述策略重新尝试。run
关键在于该函数执行的位置。Gemini请求运行在Trigger.dev的工作环境中,而不是在发起它的Next.js请求中。这正是使多回合AI代理实用的原理——将执行与原始HTTP请求解耦。
从服务器操作触发任务
接下来,创建 。app/actions.ts
服务器操作验证用户输入并排队 Trigger.dev 任务:
"use server";
import { tasks } from "@trigger.dev/sdk";
import type { summarizeTask } from "../src/trigger/summarize";
function getErrorMessage(error: unknown): string {
if (error instanceof Error && error.message) {
return error.message;
}
return "An unexpected error occurred.";
}
export async function triggerSummaryAction(text: string) {
if (!text || typeof text !== "string") {
return {
success: false,
error: "Text must be a valid string.",
};
}
const trimmed = text.trim();
if (trimmed.length < 20) {
return {
success: false,
error: "Text is too short. Please provide at least 20 characters.",
};
}
if (trimmed.length > 10000) {
return {
success: false,
error: "Text is too long. Please limit your input to 10,000 characters.",
};
}
try {
const handle = await tasks.trigger<typeof summarizeTask>(
"summarize-text",
{
text: trimmed,
}
);
return {
success: true,
runId: handle.id,
publicAccessToken: handle.publicAccessToken,
};
} catch (error: unknown) {
console.error("Failed to trigger background task:", error);
return {
success: false,
error: getErrorMessage(error),
};
}
}
服务器操作在调用前执行快速验证。tasks.trigger()
触发任务不会等双子完成。相反,Trigger.dev 返回包含新运行ID和公共访问令牌的句柄。
客户端用这些值订阅运行,而不暴露项目的 Trigger.dev 秘密密钥。
你会注意到输入会被验证两次:一次在服务器操作中,一次在任务内部。服务器操作可以在排队不必要工作前返回有用的验证错误,同时任务仍会验证自身有效载荷,确保后续从其他来源触发时安全。
将实时更新连接到 React 客户端
现在构建客户端界面。在优化 React 应用性能时,最小化发送给客户端的 JavaScript 很重要——这里轮询逻辑存在于 Trigger.dev 的钩子中,而非自定义客户端代码。
替换为:app/page.tsx
"use client";
import { useState } from "react";
import { useRealtimeRun } from "@trigger.dev/react-hooks";
import type { summarizeTask } from "../src/trigger/summarize";
import { triggerSummaryAction } from "./actions";
export default function Home() {
const [text, setText] = useState("");
const [runId, setRunId] = useState<string | null>(null);
const [publicAccessToken, setPublicAccessToken] =
useState<string | null>(null);
const [triggerError, setTriggerError] =
useState<string | null>(null);
const {
run,
error: realtimeError
} = useRealtimeRun<typeof summarizeTask>(
runId ?? "",
{
accessToken: publicAccessToken ?? "",
enabled: Boolean(runId && publicAccessToken),
}
);
const handleSummarize = async () => {
const result = await triggerSummaryAction(text);
if (
!result.success ||
!result.runId ||
!result.publicAccessToken
) {
setTriggerError(
result.error || "Failed to queue job."
);
return;
}
setRunId(result.runId);
setPublicAccessToken(result.publicAccessToken);
setTriggerError(null);
};
return (
<main>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button onClick={handleSummarize}>
Summarize Text
</button>
{triggerError && <p>{triggerError}</p>}
{realtimeError && <p>{realtimeError.message}</p>}
{run?.status === "COMPLETED" && (
<pre>{run.output?.summary}</pre>
)}
</main>
);
}
useRealtimeRun`订阅浏览器 Trigger.dev 运行,表示为 。`runId
随着后台作业生命周期的进行,钩子会收到更新的运行状态,并导致组件重新渲染。一旦状态变为 ,结果可通过 获得。COMPLETED``run.output
这将工作流程分为四个部分:
-
服务器动作启动了工作
-
Trigger.dev 执行了
-
useRealtimeRun看着它
-
React 会渲染结果
显示进度元数据
我们已经在任务中的运行元数据中写入百分比和状态值:
metadata.set("progress", {
percentage: 30,
status: "Contacting Gemini AI",
});
当前界面只显示已完成的结果。
测试背景工作
如果开发环境还没有运行,可以从以下方式开始:
npm run dev
Next.js在 ,而 Trigger.dev 开发过程处理后台任务。http://localhost:3000
打开申请表并提交摘要:
你可以在 Trigger.dev 仪表盘中查看对应的任务运行:
仪表盘让你能看到运行状态、日志、元数据、重试和输出,这在任务在超出正常请求生命周期时尤为有用。
将 Trigger.dev 任务部署到生产环境
当你准备好部署后台任务时,运行:
npx trigger.dev@latest deploy
Trigger.dev 将任务打包并部署到其云运行时。
成功的部署应类似于以下内容:
然后配置生产环境。
通过 Trigger.dev 仪表盘连接应用仓库,并将所需的生产凭证添加到你的主机提供商中。在这个演示中,Next.js应用部署到了Vercel,因此 Trigger.dev 键和Gemini键被配置为环境变量。如果你需要保护项目免受供应链风险,也值得回顾为什么 NPM 依赖可能比你自己的代码更安全。
已部署的应用程序看起来如下:
生产应用的批量现显示在 Trigger.dev:
你可以尝试部署的文本摘要工具,或者在GitHub仓库查看完整项目。
什么时候应该使用 Trigger.dev?
将工作迁移到后台运行时会在应用架构中引入另一项服务,因此并非所有异步操作都需要它。
一个简单的规则是将简短且受请求束缚的工作保持在Next.js中,并将生命周期应与用户请求独立的工作转移到后台作业中。这一决定与React Server Components将工作转移到服务器的方式相似——目标是避免昂贵的操作影响客户端请求。
| 保持同步 | 使用背景工作 |
|---|---|
| 快速的数据库读写 | 长期AI生成 |
| 必须立即返回的操作 | 可能超出请求限制的任务 |
| 简单的API调用,延迟可预测 | 需要自动重试的呼叫 |
| 小用户驱动突变 | 计划执行或延迟执行 |
| 工作已经由内部队列处理 | 多步骤工作流程 |
| 需要可靠处理的Webhook | |
| 需要执行历史和进度跟踪的作业 |
Trigger.dev 在失败应独立于原始网页请求重试,或用户需要离开页面而不取消底层工作时尤其有用。对于构建更复杂流水线的团队来说,比较AI代理沙箱平台有助于识别 Trigger.dev 与其他执行环境的定位。
结论
在本教程中,我们构建了一个Next.js文本摘要器,将长期运行的Gemini请求发送给 Trigger.dev,而不是将其绑定到应用的请求生命周期。
后台任务会验证输入,通过元数据跟踪进度,并重试暂时性失败。服务器操作会排队任务并返回运行ID,同时让React客户端跟随执行完成。useRealtimeRun
更广泛的模式比摘要本身更重要。昂贵或易失败的工作不需要共享发起该请求的生命周期。将这些工作迁移到专用的后台运行时,可以赋予它自己的重试行为、执行历史、日志和状态,同时保持Next.js请求的简短。

浙公网安备 33010602011771号