部署到 Cloudflare Workers
Cloudflare Workers 是 Hashee 推荐的 serverless 部署目标——和 Hashee 自己的 后端用同一套基础设施,延迟、成本、可观测性都最好。本页讲完整的 Worker 部署, 含 Durable Objects 持久状态 + Queues 异步处理。
架构
Hashee Mobile / Desktop │ ▼Hashee API (盲管道路由) │ HTTP POST {webhook_url} (1 MB body, 10 s deadline) │ HMAC-SHA256 签名 ▼你的 Worker (this guide) │ ├─ 立即 verify + 解密 → onMessage │ ├─ 短任务直接处理 + 200 OK │ └─ 长任务 enqueue 到 Cloudflare Queues ↓ Queue Consumer Worker ↓ 调 LLM / DB / 外部 API ↓ restRequest POST /agents/:id/messages 回写 Hashee项目脚手架
mkdir my-agent && cd my-agentnpm create cloudflare@latest . -- --type=hello-world --ts --no-deploynpm install @hasheeai/agent-sdk-tswrangler.toml
name = "my-hashee-agent"main = "src/worker.ts"compatibility_date = "2024-09-23"compatibility_flags = ["nodejs_compat"]
[observability]enabled = true
# Durable Object 用来存 per-conversation 状态[[durable_objects.bindings]]name = "CONVO"class_name = "ConversationState"
[[migrations]]tag = "v1"new_classes = ["ConversationState"]
# Queue 用来异步处理长任务[[queues.producers]]queue = "agent-jobs"binding = "JOBS"
[[queues.consumers]]queue = "agent-jobs"max_batch_size = 5max_batch_timeout = 30Worker 主入口
import { createWebhookDispatcher, restRequest } from "@hasheeai/agent-sdk-ts";
interface Env { HASHEE_AGENT_ID: string; HASHEE_AGENT_TOKEN: string; HASHEE_WEBHOOK_SECRET: string; HASHEE_X25519_PRIVATE_BASE64: string; HASHEE_ED25519_PRIVATE_BASE64: string; CONVO: DurableObjectNamespace; JOBS: Queue<Job>;}
interface Job { conversation_id: string; text: string; sender_id: string;}
export default { // 接收 Hashee webhook async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { const url = new URL(request.url); if (request.method !== "POST" || url.pathname !== "/hashee/webhook") { return new Response("not found", { status: 404 }); }
const dispatcher = createWebhookDispatcher({ agentId: env.HASHEE_AGENT_ID, token: env.HASHEE_AGENT_TOKEN, secret: env.HASHEE_WEBHOOK_SECRET, privateKeyBase64: env.HASHEE_X25519_PRIVATE_BASE64, signingPrivateKeyBase64: env.HASHEE_ED25519_PRIVATE_BASE64,
onMessage: async (msg) => { if (msg.payload?.type !== "text") return;
// 短问候直接 ack;长任务 enqueue if (msg.payload.text.length < 50) { ctx.waitUntil( dispatcher.sendReply(msg.conversation_id, { type: "text", text: "收到,正在处理...", }), ); }
await env.JOBS.send({ conversation_id: msg.conversation_id, text: msg.payload.text, sender_id: msg.sender_id, }); },
onEvent: async (event) => { if (event.type === "relation.revoked") { // 清 DO 里这个用户的状态 const id = env.CONVO.idFromName(event.payload.conversation_id); await env.CONVO.get(id).fetch("/cleanup", { method: "POST" }); } }, });
try { await dispatcher.handle( Object.fromEntries(request.headers), await request.text(), ); return new Response("ok", { status: 200 }); } catch (err: any) { console.error("[hashee] dispatcher error:", err); return new Response("error", { status: err?.statusCode ?? 500 }); } },
// Queue 消费者 async queue(batch: MessageBatch<Job>, env: Env, ctx: ExecutionContext): Promise<void> { for (const m of batch.messages) { try { await processJob(m.body, env, ctx); m.ack(); } catch (e) { console.error("[job] failed:", e); m.retry(); // 走 Cloudflare Queues 重试 } } },};
async function processJob(job: Job, env: Env, ctx: ExecutionContext): Promise<void> { // 调 LLM (这里用 Cloudflare Workers AI 作为示例) const reply = await fetch("https://api.cloudflare.com/client/v4/accounts/.../ai/run/@cf/meta/llama-3.3-70b-instruct-fp8-fast", { method: "POST", headers: { Authorization: `Bearer ${env.CF_API_TOKEN}` }, body: JSON.stringify({ messages: [{ role: "user", content: job.text }] }), }).then((r) => r.json());
await restRequest({ method: "POST", baseUrl: "https://api.hashee.ai", path: `/agents/${env.HASHEE_AGENT_ID}/messages`, token: env.HASHEE_AGENT_TOKEN, body: { conversation_id: job.conversation_id, payload: { type: "text", text: reply.result?.response ?? "(empty)" }, }, });}
// Durable Object 存 per-conversation 状态export class ConversationState { constructor(private state: DurableObjectState, private env: Env) {}
async fetch(req: Request): Promise<Response> { const url = new URL(req.url); if (url.pathname === "/get") { const data = await this.state.storage.get<unknown>("data"); return Response.json(data ?? null); } if (url.pathname === "/set" && req.method === "POST") { const body = await req.json(); await this.state.storage.put("data", body); return new Response("ok"); } if (url.pathname === "/cleanup" && req.method === "POST") { await this.state.storage.deleteAll(); return new Response("ok"); } return new Response("not found", { status: 404 }); }}部署
# 设置 secretwrangler secret put HASHEE_AGENT_IDwrangler secret put HASHEE_AGENT_TOKENwrangler secret put HASHEE_WEBHOOK_SECRETwrangler secret put HASHEE_X25519_PRIVATE_BASE64wrangler secret put HASHEE_ED25519_PRIVATE_BASE64wrangler secret put CF_API_TOKEN
# 创建 Queuewrangler queues create agent-jobs
# 部署wrangler deploy部署成功后会显示:
Deployed my-hashee-agent triggers (X events) https://my-hashee-agent.<subdomain>.workers.dev把 Webhook URL 注册到 Agent
在 Hashee app 跟系统 Agent Hashee 说:
把 DemoBot 的连接模式改成 webhookURL: https://my-hashee-agent.<subdomain>.workers.dev/hashee/webhooksecret: <你前面 wrangler secret 里设的 HASHEE_WEBHOOK_SECRET 值>System Agent 会显示 Artifact 表单二次确认;提交后立即生效。
验证
在 Hashee app 给 DemoBot 发”你好”,几秒内应该收到回复。
wrangler tail my-hashee-agent# POST /hashee/webhook 200 in 320ms# [job] processing conv=01HZ...# POST https://api.hashee.ai/agents/01906abc/.../messages 200 in 180ms性能与限制
| 项 | Workers 限 | Hashee 限 |
|---|---|---|
| Webhook 同步响应 | 30 s(CPU 时间) | 10 s |
| Webhook payload | — | 1 MB(超走 R2) |
| 子请求数 | 50 / request | — |
| Queue message size | 128 KB / msg | — |
| Queue retries | 默认 3 + DLQ | — |
| Durable Object req/s | 1000 / instance | — |
关键:Webhook handler 必须 ≤ 10 s 完成(Hashee 这一侧的硬限)。 超过会被 Hashee 视为失败 + 触发重试 + 7 次后进入 unreachable。 所有长任务必须 enqueue 到 Queue 异步处理,handler 立即 200。
主动推送
// 在 cron trigger / 其他 Worker 里主动给 Hashee 用户推消息import { restRequest } from "@hasheeai/agent-sdk-ts";
await restRequest({ method: "POST", baseUrl: "https://api.hashee.ai", path: `/agents/${env.HASHEE_AGENT_ID}/messages`, token: env.HASHEE_AGENT_TOKEN, body: { conversation_id: targetConvId, payload: { type: "text", text: "提醒:你的部署完成了" }, },});可以加到 cron trigger(wrangler.toml 里的 [triggers] crons = ["0 * * * *"])做定时通知。
持久状态:Durable Objects 还是 KV / D1?
| 用 DO | 用 KV | 用 D1 |
|---|---|---|
| per-conversation 状态机(游戏 / 投票) | 配置 / 缓存(不需要强一致) | 关系数据 / 跨会话查询 |
| 强一致写入 | 全球分布式读 | SQL 查询 |
Hashee 后端自己的 ConversationDO / DeliveryShardDO 也是这种用法。
私钥旋转
# 1. 在 Hashee app 让系统 Agent "重新生成 DemoBot Token + 重新生成密钥"# 2. 拿到新 token + 新 base64 私钥# 3. 重新 put secretwrangler secret put HASHEE_AGENT_TOKENwrangler secret put HASHEE_X25519_PRIVATE_BASE64wrangler secret put HASHEE_ED25519_PRIVATE_BASE64# 4. 自动生效(下次 webhook 调用拿新 secret)故障排查
| 症状 | 排查 |
|---|---|
wrangler tail 看不到 webhook 调用 | 检查 Hashee Agent 详情页”连接状态”是否 webhook + URL 是否正确 |
401 invalid signature | secret 错;重 put 一次确保和 Hashee 后端一致 |
409 AGENT_KEYS_MISMATCH | 私钥与后端公钥不匹配;重做 keys/register |
| Worker 超时 | 长任务必须 enqueue;handler 立即 200 |
| Queue 累积 | 看 consumer worker 是否能跟上;增 max_batch_size |
演示视频
视频文字版逐节描述
- 00:00 – 00:15 — 终端
wrangler deploy→ 输出 deployment URL。 - 00:15 – 00:30 — 在 Hashee app 跟系统 Agent 说”把 DemoBot 改成 webhook, url=…secret=…”;提交确认。
- 00:30 – 00:45 — 给 DemoBot 发”你好”,几秒后收到回复”收到,正在处理…”。 紧接着收到 LLM 完整回复”你好!需要帮忙吗?”。
- 00:45 – 01:00 —
wrangler tail显示POST /hashee/webhook 200 in 290ms[job] processing+POST .../messages 200 in 180ms。
- 01:00 – 01:15 — 演示长任务:用户问”详细介绍 React 18”,Worker handler 立即 200 ack(看到第一个 “收到” 回复秒回),LLM 30 秒后完成生成, 完整回复才显示。Queue 异步处理避免了 webhook timeout。
下一步
- 部署到 Vercel Edge
- 部署到 AWS Lambda
- 部署到自托管 VPS — 走 WebSocket 模式
- Webhook 协议参考