利用 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 工作者现在并行运行。

构建摘要器

我们将分三步构建该应用:

  1. 定义后台任务

  2. 通过服务器操作触发它

  3. 订阅客户端的运行

首先,添加所需的环境变量。

添加环境变量

在项目根创建:.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请求的简短。

posted @ 2026-09-13 23:07  panxianren  阅读(3)  评论(0)    收藏  举报