跳转到内容

做一个工作流通知 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)

worker.ts
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

wrangler.toml
name = "hashee-notif-bot"
main = "worker.ts"
compatibility_date = "2024-09-23"
compatibility_flags = ["nodejs_compat"]
Terminal window
wrangler secret put HASHEE_AGENT_ID
wrangler secret put HASHEE_AGENT_TOKEN
wrangler secret put GITHUB_WEBHOOK_SECRET
wrangler secret put TEAM_CONV_ID
wrangler secret put ONCALL_CONV_ID
wrangler 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 issueartifact_response handler 调 GitHub API

相关页面