做一个工作流通知 bot
把 GitHub Actions / Jenkins / Sentry / Grafana 等系统的事件通过 Hashee 推 送给团队 — 替代 Slack 通知 bot,但带端到端加密 + 用户可控撤销。
用户故事
“团队不想用 Slack/Teams(隐私顾虑 + 内部敏感)。要把 CI 失败 / 部署 完成 / 监控告警发到 Hashee 群里,并能 @mention 责任人 + 一键 ack 关单。“
架构
GitHub Actions / Sentry / Grafana │ │ HTTP POST (其他系统的 webhook) ▼我们的 notification-bot (Cloudflare Worker) │ ├─ 验证来源(GitHub HMAC / Sentry token / etc) ├─ 解析事件 → 决定推送给哪个会话 ├─ 渲染消息 (Markdown + artifact) │ ▼restRequest POST /agents/:id/messages → Hashee注意:这是两套 webhook:
- 外部系统 → 你的 Worker(GitHub / Sentry / 等的 webhook)
- 你的 Worker → Hashee(用 SDK restRequest 主动推送)
完整代码(Cloudflare Worker)
import { restRequest } from "@hasheeai/agent-sdk-ts";
interface Env { HASHEE_AGENT_ID: string; HASHEE_AGENT_TOKEN: string; GITHUB_WEBHOOK_SECRET: string; TEAM_CONV_ID: string; ONCALL_CONV_ID: string;}
export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url);
if (request.method !== "POST") return new Response("not found", { status: 404 });
switch (url.pathname) { case "/github": return handleGitHub(request, env); case "/sentry": return handleSentry(request, env); case "/grafana": return handleGrafana(request, env); default: return new Response("not found", { status: 404 }); } },};
// === GitHub Webhook ===
async function handleGitHub(req: Request, env: Env): Promise<Response> { const body = await req.text(); if (!await verifyGitHubSignature(env.GITHUB_WEBHOOK_SECRET, req.headers, body)) { return new Response("invalid signature", { status: 401 }); } const event = req.headers.get("X-GitHub-Event"); const payload = JSON.parse(body);
if (event === "workflow_run" && payload.action === "completed") { const run = payload.workflow_run; const status = run.conclusion; // "success" / "failure" / "cancelled" const repo = payload.repository.full_name; const actor = run.actor.login; const branch = run.head_branch; const url = run.html_url;
const emoji = status === "success" ? "✅" : status === "failure" ? "❌" : "⚪"; const text = `${emoji} **${repo}** workflow \`${run.name}\` on \`${branch}\` — **${status}** (by ${actor})\n[查看](${url})`;
// 失败 → 推 oncall + @ 责任人 const convId = status === "failure" ? env.ONCALL_CONV_ID : env.TEAM_CONV_ID; const mentions = status === "failure" ? [await findHasheeUserByGitHubLogin(actor)] : undefined;
await pushToHashee(env, convId, { type: "text", text, mentions: mentions?.filter(Boolean), }); }
return new Response("ok");}
async function verifyGitHubSignature(secret: string, headers: Headers, body: string): Promise<boolean> { const sig = headers.get("X-Hub-Signature-256"); if (!sig?.startsWith("sha256=")) return false; const expected = "sha256=" + await hmacSha256Hex(secret, body); return safeCompare(sig, expected);}
// === Sentry Webhook ===
async function handleSentry(req: Request, env: Env): Promise<Response> { const payload = await req.json(); if (payload.action !== "created") return new Response("ignored"); const issue = payload.data.issue; const level = issue.level; // "error" / "warning" / "info" const title = issue.title; const url = issue.permalink; const project = issue.project.slug; const count = issue.count;
// P0 错误 → artifact approval(让 oncall 一键 claim) const isP0 = level === "error" && count > 100;
if (isP0) { await pushArtifactToHashee(env, env.ONCALL_CONV_ID, { artifact_id: `sentry-${issue.id}`, subtype: "approval", title: `🚨 P0: ${title}`, payload: { request: { project, count, url, time: payload.data.event.received }, approve_label: "我来接 (claim)", reject_label: "误报", timeout_s: 1800, }, }); } else { await pushToHashee(env, env.TEAM_CONV_ID, { type: "text", text: `${level === "error" ? "🔴" : "🟡"} **${title}** in \`${project}\` (${count} 次)\n[查看](${url})`, }); } return new Response("ok");}
// === Grafana Webhook ===
async function handleGrafana(req: Request, env: Env): Promise<Response> { const payload = await req.json(); for (const alert of payload.alerts ?? []) { const status = alert.status; // "firing" / "resolved" const name = alert.labels.alertname; const summary = alert.annotations.summary; const emoji = status === "firing" ? "🔥" : "✅";
await pushToHashee(env, env.ONCALL_CONV_ID, { type: "text", text: `${emoji} **${name}** — ${status}\n${summary}`, }); } return new Response("ok");}
// === 推 Hashee 的统一封装 ===
async function pushToHashee(env: Env, convId: string, payload: any) { await restRequest({ method: "POST", baseUrl: "https://api.hashee.ai", path: `/agents/${env.HASHEE_AGENT_ID}/messages`, token: env.HASHEE_AGENT_TOKEN, body: { conversation_id: convId, payload }, });}
async function pushArtifactToHashee(env: Env, convId: string, artifact: any) { await restRequest({ method: "POST", baseUrl: "https://api.hashee.ai", path: `/agents/${env.HASHEE_AGENT_ID}/messages`, token: env.HASHEE_AGENT_TOKEN, body: { conversation_id: convId, payload: { type: "artifact", artifact }, }, });}
// === 工具 ===
async function hmacSha256Hex(secret: string, msg: string): Promise<string> { const key = await crypto.subtle.importKey( "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"], ); const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(msg)); return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");}
function safeCompare(a: string, b: string): boolean { if (a.length !== b.length) return false; let r = 0; for (let i = 0; i < a.length; i++) r |= a.charCodeAt(i) ^ b.charCodeAt(i); return r === 0;}
async function findHasheeUserByGitHubLogin(login: string): Promise<string | null> { // 你侧 DB / KV 维护 GitHub login → Hashee user_id 的映射 // 实际实现略 return null;}部署到 Cloudflare Workers
name = "hashee-notif-bot"main = "worker.ts"compatibility_date = "2024-09-23"compatibility_flags = ["nodejs_compat"]wrangler secret put HASHEE_AGENT_IDwrangler secret put HASHEE_AGENT_TOKENwrangler secret put GITHUB_WEBHOOK_SECRETwrangler secret put TEAM_CONV_IDwrangler secret put ONCALL_CONV_IDwrangler deploy部署后注册 webhook:
- GitHub repo settings → Webhooks →
https://hashee-notif-bot.example.workers.dev/github(secret:GITHUB_WEBHOOK_SECRET) - Sentry project settings → Webhooks →
https://...workers.dev/sentry - Grafana contact point →
https://...workers.dev/grafana
处理 approval claim(responder)
// 同一 Worker 里再接一个 endpoint 收 Hashee → 你的 webhook(如果 Agent 是 webhook 模式)// 或者另起一个长驻 service 接 WebSocket 收 artifact_response// 见 [Hello World — Webhook 模式](/zh-cn/sdk/hello-world-webhook)监控指标
| 指标 | 目标 |
|---|---|
| GitHub → Hashee 推送延迟 (P95) | < 500 ms |
| Sentry → Hashee 推送延迟 | < 1 s |
| Webhook 验签失败率 | 0% |
| pushToHashee 失败率 | < 0.1% |
升级路径
| 想做 | 改哪 |
|---|---|
| 加 Linear / Jira / Asana | 新 endpoint /linear 解 payload + 同样 push |
| 加批量去重(5 分钟同 alert 合并) | KV 记录最近 alert hash + cooldown |
| 加 SLO 看板 | 每天 cron 跑 → agent.sendArtifact({ subtype: "info", payload: { stats } }) |
| 收到 ack 后自动 close GitHub issue | artifact_response handler 调 GitHub API |