---
title: 通知
description: 针对需要关注的讨论的邮件汇总和 Webhook，包括签名验证。
---

Hearl 可以在讨论需要关注时第一时间通知你的团队。在 **通知** 页面里设置。只有所有者和管理员可以修改这些设置。

## 何时发送通知 [#when]

Hearl 每 15 分钟检查一次。满足以下条件的讨论会被包含进来：

- 平台状态为 **还没人回复** 或 **用户有新回复**
- 进度仍为 **未处理**
- 自动判断认为它与你的产品有关，且没有不确定的答案
- 优先级不低于你设置的最低优先级：全部、中及以上，或只要高

每条讨论在每种状态下只通知一次。如果它以 **还没人回复** 通知过，之后用户又跟进了，就会再以 **用户有新回复** 通知一次。

## 邮件 [#email]

打开 **发送邮件** 并添加收件人。列表留空则发给工作空间的所有成员。任何邮箱地址都可以，比如共用的客服邮箱，最多 20 个收件人。

每次检查会发送一封汇总邮件，最多列出 10 条讨论，其余的附有收件箱链接。在 **通知语言** 下选择英文或中文。

## Webhook [#webhook]

填写 **推送地址**。它必须是公开的 `https` 网址。Hearl 会根据地址识别常见的聊天机器人，并发送它们需要的消息格式：

| 地址 | 发送格式 |
| --- | --- |
| `hooks.slack.com` | Slack 消息 |
| `feishu.cn`, `larksuite.com` | 飞书文本消息 |
| `qyapi.weixin.qq.com` | 企业微信 Markdown 消息 |
| `oapi.dingtalk.com` | 钉钉 Markdown 消息 |
| `discord.com`, `discordapp.com` | Discord 消息 |
| 其他任何网址 | 通用 JSON（见下） |

页面会显示它识别出的类型。**发送测试** 会发送一条示例，页面还会显示最近一次推送的结果。请求超过 8 秒算作超时。

### 通用 JSON [#json]

```json
{
  "event": "conversations.needs_attention",
  "workspace": { "id": "org_123", "name": "Acme" },
  "text": "*2 conversations need attention* · Acme\n• [reddit] Sync stopped working after 2.3 — No reply yet <https://…>",
  "conversations": [
    {
      "id": "c_8f2a",
      "platform": "reddit",
      "product": "Acme",
      "title": "Sync stopped working after 2.3",
      "author": "some_user",
      "url": "https://www.reddit.com/r/…",
      "status": "待回复",
      "priority": "高",
      "category": "故障",
      "inbox_url": "https://hearl.org/app?c=c_8f2a"
    }
  ],
  "sent_at": "2026-11-08T09:15:00.000Z"
}
```

| 字段 | 说明 |
| --- | --- |
| `event` | `conversations.needs_attention`，测试时为 `test`（来自 **发送测试**）。 |
| `workspace` | 工作空间的 `id` 和 `name`。 |
| `text` | 可直接发布的摘要。Slack 只读取这个字段。 |
| `conversations` | 本次推送包含的每条讨论。 |
| `sent_at` | 推送发送的时间，ISO 8601 格式。 |

`platform` 的值是 `x`、`reddit` 或 `hn`。`status`、`priority` 和 `category` 是固定的中文代码，所以无论工作空间使用什么语言，你的代码都可以按它们匹配：

| 字段 | 代码 | 含义 |
| --- | --- | --- |
| `status` | `待回复` | 还没人回复 |
| `status` | `用户有新回复` | 用户有新回复 |
| `priority` | `高` / `中` / `低` | 高 / 中 / 低 |
| `category` | `故障` | 故障 |
| `category` | `求助` | 求助 |
| `category` | `功能建议` | 功能建议 |
| `category` | `价格` | 价格 |
| `category` | `负面反馈` | 负面反馈 |
| `category` | `正面反馈` | 正面反馈 |
| `category` | `购买咨询` | 购买咨询 |
| `category` | `其他` | 其他 |

### 验证签名 [#signature]

保存 Webhook 推送地址时，Hearl 会为它创建一个 **签名密钥**。你可以在通知页面显示或更换它。请求会带上这个请求头：

```txt
X-Hearloop-Signature: sha256=<hex>
```

`<hex>` 是用你的签名密钥对原始请求体计算的 HMAC-SHA256，以小写十六进制表示。请对收到的原始字节计算，在解析 JSON 之前进行，并用常量时间比较。

```js Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();
const secret = process.env.HEARL_WEBHOOK_SECRET;

app.post("/hearl", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + createHmac("sha256", secret).update(req.body).digest("hex");
  const received = req.get("x-hearloop-signature") ?? "";
  const ok =
    received.length === expected.length &&
    timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!ok) return res.status(401).end();

  const payload = JSON.parse(req.body.toString("utf8"));
  console.log(payload.event, payload.conversations.length);
  res.status(200).end();
});

app.listen(3000);
```

```python Python
import hashlib
import hmac
import json
import os

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["HEARL_WEBHOOK_SECRET"].encode()


@app.post("/hearl")
def hearl():
    body = request.get_data()
    expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    received = request.headers.get("X-Hearloop-Signature", "")
    if not hmac.compare_digest(received, expected):
        abort(401)

    payload = json.loads(body)
    print(payload["event"], len(payload["conversations"]))
    return "", 200
```

返回任意 `2xx` 状态码即表示已确认收到推送。
