Agent开发系列-10分钟快速利用Cloudflare AIChatAgent 开发 聊天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);
},
};
修正后的标准本次官方标准继承写法,除基础对话能力外,额外实现三大核心能力:
人设自定义:通过 system 指令定义Agent专属身份,精准适配技术问答场景
持久记忆:通过state存储完整对话历史,同一会话下Agent可记住上下文对话内容
轻量化模型调用:使用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:
为什么推荐使用Cloudflare Workers AI 服务来开发 Agent?
因为提供了大量的封装,包括StreamText,SQLite,历史,工具等
提供了便捷的前端页面,前端只要设置 name:会话ID即可实现多会话
Agent 支持设置工具给大模型使用吗?
支持,可以定义服务端工具(server-side)
甚至可以设置 客户端工具(client-side),进行调用
支持持久化存储吗?
支持,可以在Agent中直接使用* this.sql* 来执行SQLite语句
Cloudflare Agent框架中默认使用 DurableObject 来实现数据库访问(会话级)
利用 Cloudflare D1 实现持久化数据库存储
支持自定义大模型接口吗?
支持 AI Gateway 模式调用大模型
支持 AI Search (AutoRAG) 调用知识库查询
支持调用第三方库,如 Vercel AI SDK
支持自定义 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>;