欢迎访问 AI Skills Video ! 海量优质视频教程,助你提升技能。

Agent开发系列-10分钟快速利用Cloudflare AIChatAgent 开发 聊天Agent

老张 2026年4月28日 11 次阅读
Agent开发系列。本文介绍利用Cloudflare AIChatAgent 快速开发聊天助手Agent。包括工具定义、会话数据存储、数据库操作、会话历史支持。文章最后附带完整的聊天助手Agent源码直接进行部署体验。

一、前言

当下AI Agent已经成为大模型落地的核心形态,区别于传统问答模型,具备状态记忆、工具调用、持久化运行、云端部署的智能Agent,能够自主完成复杂任务。但对于大多数开发者而言,从零搭建Agent存在环境配置复杂、模型对接繁琐、部署成本高、服务器运维麻烦等诸多问题。

而Cloudflare推出的AIChatAgent,基于Cloudflare Agents SDK与Workers AI能力,无需本地部署大模型、无需购买云服务器、无需复杂环境配置,依托Cloudflare全球边缘网络,开箱即用。本文属于Agent开发系列入门第一篇,全程仅需10分钟,零基础开发者也能快速搭建、调试、部署专属私有AI聊天Agent,快速上手Agent开发核心逻辑。

二、Cloudflare AIChatAgent核心优势

在正式开发之前,我们先梳理Cloudflare AIChatAgent的核心亮点,也是它适合新手入门Agent开发的关键原因:

  • 零模型成本:内置Cloudflare Workers AI通用大模型,无需对接第三方API、无需配置密钥,开箱即可调用LLM能力

  • 极简部署:基于Cloudflare Workers边缘函数,一键部署至全球节点,毫秒级响应,永久在线

  • 自带状态管理:原生支持对话记忆、数据持久化,无需额外搭建数据库存储对话记录

  • 轻量化开发:官方提供完整脚手架模板,封装底层复杂逻辑,开发者只需聚焦Agent业务能力开发

  • 小额启动:Workers AI 每月5美元订阅

三、开发前置准备

本次开发仅需两个基础环境,安装简单、门槛极低,所有工具均为跨平台通用:

1. 环境依赖

  • Node.js 20及以上版本(适配Cloudflare脚手架运行)

  • npm 包管理器(Node.js自带,无需额外安装)

  • Cloudflare账号(免费注册,用于项目云端部署)

2. 前置知识

无需精通AI算法、无需掌握复杂架构,仅需基础的JavaScript/TypeScript语法,即可完成本次Agent开发。

四、10分钟极速开发实操

我们将按照项目初始化→自定义Agent逻辑→本地调试→云端部署四个步骤,全程落地开发,每一步均可直接复制代码运行。

步骤1:脚手架初始化项目(2分钟)

Cloudflare官方提供了成熟的Agents启动模板,内置聊天Agent基础架构、前端交互页面、模型调用逻辑,无需从零搭建项目结构。打开终端,执行以下命令快速初始化项目:

# 初始化官方Agent模板项目
npm create cloudflare@latest my-ai-agent -- --template=cloudflare/agents-starter

# 进入项目目录
cd my-ai-agent

# 安装项目依赖
npm install

命令执行完成后,项目目录结构自动生成,核心目录说明:

  • src/server.ts:Agent后端核心文件,用于定义Agent能力、模型调用、状态管理

  • src/client:前端聊天页面,无需修改,自带对话交互界面

  • wrangler.toml:Cloudflare项目配置文件,管理部署、模型、环境参数

步骤2:自定义专属Agent逻辑(5分钟)

默认模板仅提供基础对话能力,我们将基于Cloudflare Agents官方标准规范,自定义一个带对话记忆、专属人设、简洁应答的私有Agent。核心规范:我们将遵循 Cloudflare Agents 最新官方开发规范:SDK 内置成熟的 AIChatAgent 聊天基类,无需直接继承底层 Agent。我们自定义的业务 ChatAgent 直接继承 AIChatAgent,并重写官方标准 onChatMessage 钩子函数,快速实现带对话记忆、专属人设、简洁应答的私有Agent。打开 src/server.ts,替换默认代码,完整可运行代码如下:

import { Agent, AIChatAgent, routeAgentRequest } from "agents";

// 定义Agent状态:存储历史对话,实现记忆能力
type AgentState = {
  history: Array<{ role: string; content: string }>;
};

/**
 * 官方标准继承方式
 * AIChatAgent 为 SDK 内置官方聊天基类,封装了基础聊天会话、websocket通信、消息接收能力
 * 业务 Agent 直接继承 AIChatAgent,只需重写 onChatMessage 钩子即可自定义业务逻辑
 */
export class ChatAgent extends AIChatAgent<Env, AgentState> {
  // 初始化状态:空对话记录
  initialState: AgentState = {
    history: [],
  };

  // 官方标准聊天消息钩子:接收前端用户消息,自定义应答逻辑
  async onChatMessage(message: string) {
    // 拼接用户最新消息到对话历史
    this.state.history.push({
      role: "user",
      content: message,
    });

    // 调用Cloudflare内置Llama3模型
    const res = await this.env.AI.run("@cf/meta/llama-3-8b-instruct", {
      messages: [
        // 自定义Agent人设
        {
          role: "system",
          content: "以恋爱导师的角色来对用户描述的",
        },
        ...this.state.history,
      ],
    });

    // 保存模型应答到对话历史,实现上下文记忆
    this.state.history.push({
      role: "assistant",
      content: res.response,
    });

    return res.response;
  }
}

// 路由转发:接收前端请求,对接Agent服务
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    return routeAgentRequest(request, env, ctx);
  },
};

修正后的标准本次官方标准继承写法,除基础对话能力外,额外实现三大核心能力:

  1. 人设自定义:通过 system 指令定义Agent专属身份,精准适配技术问答场景

  2. 持久记忆:通过state存储完整对话历史,同一会话下Agent可记住上下文对话内容

  3. 轻量化模型调用:使用Cloudflare免费开源的llama-3-8b模型,兼顾响应速度与应答质量

步骤3:本地调试验证(2分钟)

代码编写完成后,我们可以启动本地开发服务验证Agent基础功能。需要重点注意:Cloudflare Workers AI 属于远程云端服务,本地调试并不会完全本地化运行模型,依旧需要联网访问Cloudflare远程AI接口。如果本地网络环境不佳、无法正常连接Cloudflare远程服务,会出现模型调用失败、对话无响应、接口报错等问题。

针对网络异常场景,无需纠结本地调试,可直接跳过本地验证,跳转至下方步骤4完成云端部署,云端部署成功后绑定自定义域名,依托Cloudflare全球边缘网络,可完美规避本地网络问题,直接完成Agent功能验证与上线。

网络正常的情况下,终端执行以下命令启动本地服务:

npm run dev

启动成功后,终端会输出本地访问地址 http://localhost:5173,打开浏览器访问该地址,即可看到自带的聊天界面。输入问题测试,可验证:上下文记忆生效、人设匹配、模型正常应答,本地网络通畅时即可完成本地Agent功能校验。若测试失败,基本为网络连通性问题,建议直接进行云端部署验证。

步骤4:云端一键部署(1分钟)

本地调试无误后,一键部署至Cloudflare全球边缘网络,实现永久在线、公网可访问。执行部署命令:

npx wrangler deploy

首次部署需要按照终端提示,登录绑定Cloudflare账号,绑定完成后自动完成打包、部署。部署成功后,终端会生成公网访问链接,任意设备均可访问使用你的专属AI Agent。

五、能力进阶拓展

基础聊天Agent开发完成后,我们可以基于该模板快速拓展更多高级Agent能力,适配更多业务场景:

1. 新增工具调用能力

Cloudflare Agent支持自定义工具函数,可拓展天气查询、计算器、文本解析、接口调用等能力,让Agent不再局限于问答,可自主完成功能性任务。

2. 更换高阶模型

除了llama-3-8b,Cloudflare Workers AI内置数十种开源模型,可直接替换模型标识,适配代码生成、文案创作、多模态识别等场景。

3. 持久化数据存储

依托Cloudflare Durable Objects,可实现跨会话记忆,用户退出重进后依然保留历史对话,打造完整商用级Agent体验。

六、常见问题排查

  • 本地启动失败:检查Node.js版本是否≥20,删除node_modules与lock文件后重新安装依赖

  • 部署报错:确认Cloudflare账号登录正常,网络通畅,免费额度未耗尽

  • Agent无记忆:检查state状态存储逻辑,确认对话历史是否正常写入数组

七、总结与预告

本文通过Cloudflare AIChatAgent,仅用10分钟完成了项目初始化、自定义人设、上下文记忆、本地调试、云端部署全流程,无需服务器、无需模型训练、无需复杂配置,极低门槛实现专属AI Agent落地。

Cloudflare Agents SDK 极大降低了AI Agent的开发和部署门槛,非常适合新手入门学习、个人项目开发、小型工具落地。后续本系列将持续更新,讲解Agent工具调用、多模态能力、会话持久化、私有化改造等进阶教程,带你系统性掌握AI Agent开发体系。

FAQ:

  1. 为什么推荐使用Cloudflare Workers AI 服务来开发 Agent?

    1. 因为提供了大量的封装,包括StreamText,SQLite,历史,工具等

    2. 提供了便捷的前端页面,前端只要设置 name:会话ID即可实现多会话

  2. Agent 支持设置工具给大模型使用吗?

    1. 支持,可以定义服务端工具(server-side)

    2. 甚至可以设置 客户端工具(client-side),进行调用

  3. 支持持久化存储吗?

    1. 支持,可以在Agent中直接使用* this.sql* 来执行SQLite语句

    2. Cloudflare Agent框架中默认使用 DurableObject 来实现数据库访问(会话级)

    3. 利用 Cloudflare D1 实现持久化数据库存储

  4. 支持自定义大模型接口吗?

    1. 支持 AI Gateway 模式调用大模型

    2. 支持 AI Search (AutoRAG) 调用知识库查询

    3. 支持调用第三方库,如 Vercel AI SDK

    4. 支持自定义 fetch

八、附录-聊天恋爱助手Agent源码

基于Cloudflare Agents,利用 Vercel AI SDK 实现流式文本输出的聊天辅助Agent。涵盖星座、节日、聊天

对象持久化支持。



/**
 * 本项目源码由 https://www.ai-skills.video 提供。
 * 由于本项目暂未接入 OAuth 访问控制,因此暂不提供实际的测试网址,避免大模型用量超限
 * 本Agent支持的功能:
 * 1. 支持会话级数据存储,可以通过工具进行聊天对象添加和更新
 * 2. 支持星座查询,在聊天对象添加时,会根据生日进行查询
 * 3. 支持节日查询,根据当前日期或者日期范围,来提示用户进行节日提醒
 * 4. 可以利用大模型进行聊天技巧推荐,能够根据聊天记录,来给出回复建议
 */
import { createWorkersAI } from "workers-ai-provider";
import { callable, routeAgentRequest, type Connection, type Schedule, type WSMessage } from "agents";
import { getSchedulePrompt, scheduleSchema } from "agents/schedule";
import { AIChatAgent, type OnChatMessageOptions } from "@cloudflare/ai-chat";
import {
  convertToModelMessages,
  pruneMessages,
  stepCountIs,
  streamText,
  tool,
  type ModelMessage
} from "ai";
import { z } from "zod";

/**
 * 本项目源码由 https://www.ai-skills.video 提供
 * The AI SDK's downloadAssets step runs `new URL(data)` on every file
 * part's string data. Data URIs parse as valid URLs, so it tries to
 * HTTP-fetch them and fails. Decode to Uint8Array so the SDK treats
 * them as inline data instead.
 */
function inlineDataUrls(messages: ModelMessage[]): ModelMessage[] {
  return messages.map((msg) => {
    if (msg.role !== "user" || typeof msg.content === "string") return msg;
    return {
      ...msg,
      content: msg.content.map((part) => {
        if (part.type !== "file" || typeof part.data !== "string") return part;
        const match = part.data.match(/^data:([^;]+);base64,(.+)$/);
        if (!match) return part;
        const bytes = Uint8Array.from(atob(match[2]), (c) => c.charCodeAt(0));
        return { ...part, data: bytes, mediaType: match[1] };
      })
    };
  });
}

// ── dateQuery helpers ──────────────────────────────────────────────

interface Holiday {
  month: number;
  day: number;
  name: string;
}

/** Common holidays (solar calendar, fixed dates). */
const HOLIDAYS: Holiday[] = [
  { month: 1, day: 1, name: "元旦" },
  { month: 2, day: 14, name: "情人节" },
  { month: 3, day: 8, name: "妇女节" },
  { month: 3, day: 12, name: "植树节" },
  { month: 3, day: 14, name: "白色情人节" },
  { month: 4, day: 1, name: "愚人节" },
  { month: 4, day: 5, name: "清明节(约)" },
  { month: 5, day: 1, name: "劳动节" },
  { month: 5, day: 4, name: "青年节" },
  { month: 5, day: 11, name: "母亲节(约,5月第二个周日)" },
  { month: 5, day: 20, name: "520表白日" },
  { month: 6, day: 1, name: "儿童节" },
  { month: 6, day: 15, name: "父亲节(约,6月第三个周日)" },
  { month: 7, day: 1, name: "建党节" },
  { month: 8, day: 1, name: "建军节" },
  { month: 9, day: 10, name: "教师节" },
  { month: 10, day: 1, name: "国庆节" },
  { month: 10, day: 31, name: "万圣夜" },
  { month: 11, day: 1, name: "万圣节" },
  { month: 11, day: 11, name: "双十一/光棍节" },
  { month: 11, day: 27, name: "感恩节(约,11月第四个周四)" },
  { month: 12, day: 12, name: "双十二" },
  { month: 12, day: 24, name: "平安夜" },
  { month: 12, day: 25, name: "圣诞节" },
  { month: 12, day: 31, name: "跨年夜" }
];

/**
 * Parse a date string in various formats:
 *   yyyy-MM-dd / yyyy.MM.dd / yyyy/MM/dd / yyyy年MM月dd日
 *   MM-dd / MM.dd / MM/dd / MM月dd日
 * Falls back to the current date when the input is empty or unparseable.
 */
function parseDateInput(input?: string): { month: number; day: number } {
  if (!input || input.trim() === "") {
    const now = new Date();
    return { month: now.getMonth() + 1, day: now.getDate() };
  }

  const s = input.trim();

  // yyyy[-./年]MM[-./月]dd[日]
  const fullMatch = s.match(
    /^(\d{4})[-./年](\d{1,2})[-./月](\d{1,2})日?$/
  );
  if (fullMatch) {
    return { month: Number(fullMatch[2]), day: Number(fullMatch[3]) };
  }

  // MM[-./月]dd[日]
  const shortMatch = s.match(/^(\d{1,2})[-./月](\d{1,2})日?$/);
  if (shortMatch) {
    return { month: Number(shortMatch[1]), day: Number(shortMatch[2]) };
  }

  // Fallback: try native Date parsing
  const parsed = new Date(s);
  if (!Number.isNaN(parsed.getTime())) {
    return { month: parsed.getMonth() + 1, day: parsed.getDate() };
  }

  // Ultimate fallback
  const now = new Date();
  return { month: now.getMonth() + 1, day: now.getDate() };
}

/** Return holiday names that fall on the given month/day. */
function queryHolidays(month: number, day: number): string[] {
  return HOLIDAYS.filter((h) => h.month === month && h.day === day).map(
    (h) => h.name
  );
}

/** Day-of-year helper (1-indexed, non-leap aware is fine for lookups). */
function dayOfYear(month: number, day: number): number {
  const daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  let total = day;
  for (let m = 1; m < month; m++) total += daysInMonth[m];
  return total;
}

/**
 * Return holidays within the next `range` days (wraps around year-end).
 * Excludes holidays that fall on the query date itself.
 */
function queryUpcomingHolidays(
  month: number,
  day: number,
  range: number
): Holiday[] {
  const today = dayOfYear(month, day);
  return HOLIDAYS.filter((h) => {
    if (h.month === month && h.day === day) return false;
    let diff = dayOfYear(h.month, h.day) - today;
    if (diff < 0) diff += 365;
    return diff > 0 && diff <= range;
  }).sort((a, b) => {
    let da = dayOfYear(a.month, a.day) - today;
    let db = dayOfYear(b.month, b.day) - today;
    if (da <= 0) da += 365;
    if (db <= 0) db += 365;
    return da - db;
  });
}

// ── xingzuo (zodiac) helper ────────────────────────────────────────

interface ZodiacSign {
  name: string;
  startMonth: number;
  startDay: number;
  endMonth: number;
  endDay: number;
  element: string;
  traits: string;
}

const ZODIAC_SIGNS: ZodiacSign[] = [
  { name: "摩羯座", startMonth: 12, startDay: 22, endMonth: 1, endDay: 19, element: "土", traits: "务实、有耐心、有责任心、稳重" },
  { name: "水瓶座", startMonth: 1, startDay: 20, endMonth: 2, endDay: 18, element: "风", traits: "独立、创新、理性、博爱" },
  { name: "双鱼座", startMonth: 2, startDay: 19, endMonth: 3, endDay: 20, element: "水", traits: "浪漫、温柔、富有同情心、直觉强" },
  { name: "白羊座", startMonth: 3, startDay: 21, endMonth: 4, endDay: 19, element: "火", traits: "热情、勇敢、直率、行动力强" },
  { name: "金牛座", startMonth: 4, startDay: 20, endMonth: 5, endDay: 20, element: "土", traits: "踏实、忠诚、有耐心、重感官享受" },
  { name: "双子座", startMonth: 5, startDay: 21, endMonth: 6, endDay: 21, element: "风", traits: "聪明、善变、好奇心强、善于沟通" },
  { name: "巨蟹座", startMonth: 6, startDay: 22, endMonth: 7, endDay: 22, element: "水", traits: "温柔、顾家、敏感、重感情" },
  { name: "狮子座", startMonth: 7, startDay: 23, endMonth: 8, endDay: 22, element: "火", traits: "自信、大方、有领导力、热情" },
  { name: "处女座", startMonth: 8, startDay: 23, endMonth: 9, endDay: 22, element: "土", traits: "细心、追求完美、务实、善于分析" },
  { name: "天秤座", startMonth: 9, startDay: 23, endMonth: 10, endDay: 23, element: "风", traits: "优雅、公正、善于社交、追求和谐" },
  { name: "天蝎座", startMonth: 10, startDay: 24, endMonth: 11, endDay: 22, element: "水", traits: "神秘、专注、意志力强、洞察力强" },
  { name: "射手座", startMonth: 11, startDay: 23, endMonth: 12, endDay: 21, element: "火", traits: "乐观、自由、热爱冒险、坦率" },
];

/** Look up the zodiac sign for a given month/day. */
function getZodiacSign(month: number, day: number): ZodiacSign | undefined {
  return ZODIAC_SIGNS.find((sign) => {
    // Handle Capricorn which spans year boundary (12/22 – 1/19)
    if (sign.startMonth > sign.endMonth) {
      return (
        (month === sign.startMonth && day >= sign.startDay) ||
        (month === sign.endMonth && day <= sign.endDay)
      );
    }
    return (
      (month === sign.startMonth && day >= sign.startDay) ||
      (month === sign.endMonth && day <= sign.endDay) ||
      (month > sign.startMonth && month < sign.endMonth)
    );
  });
}

export class ChatAgent extends AIChatAgent<Env> {
  maxPersistedMessages = 100;

  onStart() {
    // Configure OAuth popup behavior for MCP servers that require authentication
    this.mcp.configureOAuthCallback({
      customHandler: (result) => {
        if (result.authSuccess) {
          return new Response("<script>window.close();</script>", {
            headers: { "content-type": "text/html" },
            status: 200
          });
        }
        return new Response(
          `Authentication Failed: ${result.authError || "Unknown error"}`,
          { headers: { "content-type": "text/plain" }, status: 400 }
        );
      }
    });

    this.sql`CREATE TABLE IF NOT EXISTS targets (
        id       INTEGER PRIMARY KEY AUTOINCREMENT,
        name     TEXT    UNIQUE
                        NOT NULL,
        birthDay TEXT,
        addDate  TEXT,
        gendar   TEXT    DEFAULT ('女'),
        info     TEXT
    );
    `;
  }

  @callable()
  async addServer(name: string, url: string) {
    return await this.addMcpServer(name, url);
  }

  @callable()
  async removeServer(serverId: string) {
    await this.removeMcpServer(serverId);
  }

  async onChatMessage(_onFinish: unknown, options?: OnChatMessageOptions) {
    const mcpTools = this.mcp.getAITools();
    const workersai = createWorkersAI({ binding: this.env.AI });

    const result = streamText({
      model: workersai("@cf/moonshotai/kimi-k2.5", {
        sessionAffinity: this.sessionAffinity
      }),
      system: `利用恋爱导师的思路和方式进行聊天对话分析辅助, 可以查询聊天目标的基本信息、性格、对话习惯。同时能够根据聊天内容进行回复建议。角色包含:“聊天目标”,“用户”三种。
      系统内置:节日查询(dateQuery)、聊天对象信息查询(targetInfo)、保存聊天对象信息(saveTargetInfo), 列出聊天对象列表(listTargets), 星座查询(xingzuo) 工具。

${getSchedulePrompt({ date: new Date() })}

If the user asks to schedule a task, use the schedule tool to schedule the task.`,
      // Prune old tool calls to save tokens on long conversations
      messages: pruneMessages({
        messages: inlineDataUrls(await convertToModelMessages(this.messages)),
        toolCalls: "before-last-2-messages"
      }),
      tools: {
        // MCP tools from connected servers
        ...mcpTools,

        // Server-side tool: runs automatically on the server
        dateQuery: tool({
          description: "查询当天或者近期的节日信息",
          inputSchema: z.object({
            date: z.string().describe("date").optional()
          }),
          execute: async ({ date }) => {
            const { month, day } = parseDateInput(date);
            const holidays = queryHolidays(month, day);
            const upcoming = queryUpcomingHolidays(month, day, 30);
            return {
              date: `${month}月${day}日`,
              holidays:
                holidays.length > 0
                  ? holidays
                  : ["今天没有特定节日"],
              upcoming:
                upcoming.length > 0
                  ? upcoming.map((h) => `${h.month}月${h.day}日 - ${h.name}`)
                  : ["近期没有节日"]
            };
          }
        }),

        xingzuo: tool({
          description: "查询日期对应的星座",
          inputSchema: z.object({
            date: z.string().describe("日期")
          }),
          execute: async ({ date }) => {
            const { month, day } = parseDateInput(date);
            const sign = getZodiacSign(month, day);
            if (!sign) {
              return { error: "无法识别该日期对应的星座", date: `${month}月${day}日` };
            }
            return {
              date: `${month}月${day}日`,
              xingzuo: sign.name,
              element: sign.element,
              traits: sign.traits,
              period: `${sign.startMonth}月${sign.startDay}日 - ${sign.endMonth}月${sign.endDay}日`
            };
          }
        }),

        saveTargetInfo: tool({
          description: "保存聊天目标的信息",
          inputSchema: z.object({
            name: z.string().describe("聊天目标的姓名/昵称"),
            birthDay: z.string().describe("生日").default(''),
            addDate: z.string().describe("添加日期").default(''),
            gendar: z.string().describe("性别").optional().default('女'),
            info: z.string().describe("其他信息").optional()
          }),
          execute: async (target)=>{
            let status = "添加失败";
            try{
              this.sql`INSERT INTO targets (name, birthDay, addDate, gendar, info) VALUES ( ${target.name}, ${target.birthDay}, ${target.addDate||new Date().toString()}, ${target.gendar}, ${target.info||''} )
              ON CONFLICT (name)
              DO UPDATE
              SET info = ${target.info||''}
              `;
              status = "添加成功";
            }catch(err){

            }
            return {
              "status": status,
            };
          },
        }),

        listTargets: tool({
          description: "列出所有的聊天目标对象",
          inputSchema: z.object(),
          execute: async () => {
            const rows = this.sql`SELECT * FROM targets`;
            return {
              count: rows.length,
              results: rows
            };
          }
        }),

        targetInfo: tool({
          description:
            "查询聊天对象/目标的基本信息",
          inputSchema: z.object({
            name: z.string().describe("聊天对象/目标的姓名"),
            birthDay: z.string().describe("生日").optional(),
            addDate: z.string().describe("添加日期").optional(),
          }),
          execute: async ({ name, birthDay, addDate }) => {
            try {
              // Build conditions and collect values
              const conditions: string[] = [];
              const params: (string | number | boolean | null)[] = [];

              if (name) {
                conditions.push("name LIKE ?");
                params.push(`%${name}%`);
              }
              if (birthDay) {
                conditions.push("birthDay = ?");
                params.push(birthDay);
              }
              if (addDate) {
                conditions.push("addDate = ?");
                params.push(addDate);
              }

              // this.sql is a tagged template — build the query dynamically
              // based on which parameters were provided
              let rows: Record<string, string | number | boolean | null>[];

              if (conditions.length === 0) {
                rows = this.sql`SELECT * FROM targets`;
              } else if (conditions.length === 1) {
                if (name) {
                  rows = this.sql`SELECT * FROM targets WHERE name LIKE ${`%${name}%`}`;
                } else if (birthDay) {
                  rows = this.sql`SELECT * FROM targets WHERE birthDay = ${birthDay}`;
                } else {
                  rows = this.sql`SELECT * FROM targets WHERE addDate = ${addDate!}`;
                }
              } else if (conditions.length === 2) {
                if (name && birthDay) {
                  rows = this.sql`SELECT * FROM targets WHERE name LIKE ${`%${name}%`} AND birthDay = ${birthDay}`;
                } else if (name && addDate) {
                  rows = this.sql`SELECT * FROM targets WHERE name LIKE ${`%${name}%`} AND addDate = ${addDate!}`;
                } else {
                  rows = this.sql`SELECT * FROM targets WHERE birthDay = ${birthDay!} AND addDate = ${addDate!}`;
                }
              } else {
                rows = this.sql`SELECT * FROM targets WHERE name LIKE ${`%${name}%`} AND birthDay = ${birthDay!} AND addDate = ${addDate!}`;
              }

              if (rows.length === 0) {
                return { status: "未找到", results: [] };
              }
              return { status: "查询成功", results: rows };
            } catch (err) {
              return { status: "查询失败", error: String(err) };
            }
          },
        }),
      },
      stopWhen: stepCountIs(5),
      abortSignal: options?.abortSignal
    });

    return result.toUIMessageStreamResponse();
  }

  async executeTask(description: string, _task: Schedule<string>) {
    // Do the actual work here (send email, call API, etc.)
    console.log(`Executing scheduled task: ${description}`);

    // Notify connected clients via a broadcast event.
    // We use broadcast() instead of saveMessages() to avoid injecting
    // into chat history — that would cause the AI to see the notification
    // as new context and potentially loop.
    this.broadcast(
      JSON.stringify({
        type: "scheduled-task",
        description,
        timestamp: new Date().toISOString()
      })
    );
  }
}

export default {
  async fetch(request: Request, env: Env) {
    return (
      (await routeAgentRequest(request, env)) ||
      new Response("Not found", { status: 404 })
    );
  }
} satisfies ExportedHandler<Env>;