跳转到内容

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.md

package.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

Terminal window
HASHEE_AGENT_ID=
HASHEE_AGENT_TOKEN=
HASHEE_X25519_PRIVATE_BASE64=
HASHEE_ED25519_PRIVATE_BASE64=
DATABASE_URL=postgres://postgres:dev@pg:5432/cs_bot
OPENAI_API_KEY=
ONCALL_GROUP_CONV_ID=

Terminal window
docker compose up -d pg
npm install
npm run db:setup
# 填几条 FAQ
psql $DATABASE_URL -c "INSERT INTO faq_entries (question, answer) VALUES ('退款多久', '7 个工作日');"
npm run dev

相关