Template — 客服 Bot
中等复杂度模板:意图分类 → FAQ 检索 → 订单查询 (tool call) → 投诉转人工 (Approval artifact)。基于 客服 bot tutorial。
目录结构
hashee-cs-bot/├── src/│ ├── index.ts # 入口 + handler 注册│ ├── intents.ts # 意图分类│ ├── faq.ts # FAQ 检索│ ├── order.ts # 订单查询│ ├── escalation.ts # 投诉转人工│ └── db.ts # PG 连接├── sql/│ └── schema.sql # DB 表├── .env.example├── package.json├── tsconfig.json├── docker-compose.yml # 含 PG└── README.mdpackage.json
{ "name": "hashee-cs-bot", "version": "0.1.0", "private": true, "type": "module", "scripts": { "dev": "tsx --env-file=.env src/index.ts", "build": "tsc", "start": "node --env-file=.env dist/index.js", "db:setup": "psql $DATABASE_URL < sql/schema.sql" }, "dependencies": { "@hasheeai/agent-sdk-ts": "^0.2.0", "openai": "^4.0.0", "pg": "^8.13.0" }, "devDependencies": { "@types/node": "^22.0.0", "@types/pg": "^8.11.0", "tsx": "^4.20.0", "typescript": "^5.7.0" }}src/index.ts
import { HasheeAgent } from "@hasheeai/agent-sdk-ts";import { classifyIntent } from "./intents";import { handleFaq } from "./faq";import { handleOrderQuery } from "./order";import { handleComplaint, handleOncallResponse } from "./escalation";import { archiveMessage } from "./db";
const agent = await HasheeAgent.init({ agentId: process.env.HASHEE_AGENT_ID!, token: process.env.HASHEE_AGENT_TOKEN!, baseUrl: "https://api.hashee.ai", connectionMode: "websocket",});
agent.addMessageHandler(async (msg) => { if (msg.payload?.type !== "text") return; await agent.typing(msg.conversation_id);
const intent = await classifyIntent(msg.payload.text); console.log(`[intent] ${msg.sender_id}: ${intent}`);
switch (intent) { case "faq": await handleFaq(agent, msg); break; case "order_query": await handleOrderQuery(agent, msg); break; case "complaint": await handleComplaint(agent, msg); break; default: await agent.send(msg.conversation_id, { type: "text", text: "抱歉没听懂。是想:(a) 查询订单 (b) 报告问题 (c) 看常见问题?", }); }
await archiveMessage(msg, intent);});
agent.addEventHandler(handleOncallResponse(agent));agent.addStatusHandler((s) => console.log(`[hashee] ${s}`));console.log("[cs-bot] up");src/intents.ts
import OpenAI from "openai";const openai = new OpenAI();
export async function classifyIntent(text: string): Promise<string> { const r = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: "分类:faq / order_query / complaint / general。只回类别词。" }, { role: "user", content: text }, ], temperature: 0, }); return r.choices[0]?.message?.content?.trim().toLowerCase() ?? "general";}src/faq.ts
import OpenAI from "openai";import { Pool } from "pg";import type { HasheeAgent } from "@hasheeai/agent-sdk-ts";
const openai = new OpenAI();const db = new Pool({ connectionString: process.env.DATABASE_URL });
export async function handleFaq(agent: HasheeAgent, msg: any) { const r = await db.query(` SELECT question, answer FROM faq_entries WHERE to_tsvector('simple', question) @@ plainto_tsquery('simple', $1) ORDER BY ts_rank(to_tsvector('simple', question), plainto_tsquery('simple', $1)) DESC LIMIT 3 `, [msg.payload.text]);
const context = r.rows.map((m) => `Q: ${m.question}\nA: ${m.answer}`).join("\n\n"); const c = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: `你是客服。基于 FAQ 回答:\n\n${context}` }, { role: "user", content: msg.payload.text }, ], }); await agent.send(msg.conversation_id, { type: "text", text: c.choices[0]?.message?.content ?? "(空)", });}src/order.ts
import type { HasheeAgent } from "@hasheeai/agent-sdk-ts";
export async function handleOrderQuery(agent: HasheeAgent, msg: any) { const orderId = msg.payload.text.match(/\b\d{13}\b/)?.[0]; if (!orderId) { await agent.send(msg.conversation_id, { type: "text", text: "麻烦提供 13 位订单号。", }); return; } // 替换为你的真实 API const order = { status: "已发货", amount: 99.99, eta: "2026-05-18" }; await agent.sendArtifact(msg.conversation_id, { artifact: { subtype: "info", title: `订单 #${orderId}`, payload: { body: `**状态**:${order.status}\n**金额**:$${order.amount}\n**预计**:${order.eta}`, }, }, });}src/escalation.ts
import type { HasheeAgent } from "@hasheeai/agent-sdk-ts";import { ulid } from "ulid";
export async function handleComplaint(agent: HasheeAgent, msg: any) { await agent.send(msg.conversation_id, { type: "text", text: "我理解你的不满。让我转给人工。", }); await agent.sendArtifact(process.env.ONCALL_GROUP_CONV_ID!, { artifact: { artifact_id: `esc-${msg.message_id}`, subtype: "approval", title: "客户投诉转人工", payload: { request: { from_user: msg.sender_id, conversation_id: msg.conversation_id, issue: msg.payload.text.slice(0, 500), }, approve_label: "我来接", reject_label: "驳回", timeout_s: 600, }, }, });}
export function handleOncallResponse(agent: HasheeAgent) { return async (event: any) => { if (event.type !== "artifact_response") return; if (!event.payload.ref_artifact?.startsWith("esc-")) return; if (event.payload.action === "approve") { const claimer = event.sender_id; const customerConv = event.payload.payload.conversation_id; await agent.send(customerConv, { type: "text", text: `已为你接通 @<U:${claimer}>,稍后他会跟进。`, }); } };}src/db.ts
import { Pool } from "pg";const db = new Pool({ connectionString: process.env.DATABASE_URL });
export async function archiveMessage(msg: any, intent: string) { await db.query( "INSERT INTO cs_messages (msg_id, user_id, conv_id, content, intent, ts) VALUES ($1, $2, $3, $4, $5, NOW())", [msg.message_id, msg.sender_id, msg.conversation_id, msg.payload.text, intent], );}sql/schema.sql
CREATE TABLE IF NOT EXISTS faq_entries ( id SERIAL PRIMARY KEY, question TEXT NOT NULL, answer TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW());CREATE INDEX IF NOT EXISTS faq_search_idx ON faq_entries USING gin(to_tsvector('simple', question));
CREATE TABLE IF NOT EXISTS cs_messages ( msg_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, conv_id TEXT NOT NULL, content TEXT NOT NULL, intent TEXT NOT NULL, ts TIMESTAMPTZ NOT NULL);CREATE INDEX IF NOT EXISTS cs_user_idx ON cs_messages(user_id);docker-compose.yml
services: pg: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_PASSWORD: dev POSTGRES_DB: cs_bot ports: ["5432:5432"] volumes: [pgdata:/var/lib/postgresql/data]
agent: build: . restart: unless-stopped env_file: .env depends_on: [pg]
volumes: pgdata:.env.example
HASHEE_AGENT_ID=HASHEE_AGENT_TOKEN=HASHEE_X25519_PRIVATE_BASE64=HASHEE_ED25519_PRIVATE_BASE64=DATABASE_URL=postgres://postgres:dev@pg:5432/cs_botOPENAI_API_KEY=ONCALL_GROUP_CONV_ID=跑
docker compose up -d pgnpm installnpm run db:setup# 填几条 FAQpsql $DATABASE_URL -c "INSERT INTO faq_entries (question, answer) VALUES ('退款多久', '7 个工作日');"npm run dev