暗色模式

TeoNotify:不仅仅是通知,更是方便快速回复博客评论的 Typecho 插件

插件发布
2026-08-18
107
0

写在前面

TeoNotify 是 Teo 系列(与作者的 SEO 插件 TeoSeo 同系列)的 Typecho 1.3 评论通知插件,提供"邮件通知 + QQ 遥控回复评论"双通道。本文不止介绍功能,更讲清楚整个闭环怎么跑通:服务端怎么部署、安全的多层防护怎么写、以及踩过的坑。

它解决什么问题

传统"评论邮件提醒"插件只解决一件事:有人留言,站长收到一封邮件。但体验一般——邮件容易错过、不及时,而且你只能回后台才能回复评论

TeoNotify 更进一步:不仅通知,还能"遥控"回复。有人评论后你的手机 QQ 立即收到通知;你引用这条通知回复一句,博客上就自动发布你的博主回复,同时邮件通知原评论者。全程不用开电脑、不用进后台。

闭环原理

核心是一台常驻服务器 + 一个 QQ 机器人,靠两个 Python 服务配合:

访客评论 → TeoNotify 触发
  ├─ 邮件通知博主
  └─ 调服务器 /send → QQ 机器人 → 通知你QQ(消息带 评论ID=xxx)
                                         ↑ 你引用这条通知回复
qq-ws-listener WebSocket 监听 → 从引用内容解析出评论ID
  ├─ HMAC 签名(ts|nonce|cid|content)
  └─ POST 博客安全接口 → 自动发布博主回复 → 邮件通知评论者

关键点:

  1. 通知带 评论ID=xxx:引用回复能精确定位到哪条评论;
  2. 安全接口多层防护:IP 白名单 + API Key + HMAC 签名 + 时间戳防重放,防止垃圾评论借机涌入。

部署服务端(常驻服务器)

以一台 Ubuntu 服务器为例。装依赖,然后把 server/ 目录内容部署上去:

pip3 install websockets
mkdir -p /opt/teonotify
# 把 qq-server.py、qq-ws-listener.py、config.json 放到 /opt/teonotify/

配置文件 config.json(从 config.example.json 复制后填入你的机器人凭证):

{
  "appid": "你的QQ机器人AppID",
  "appsecret": "你的QQ机器人AppSecret",
  "api_key": "HTTP服务鉴权Key(自定义随机串,博客端也填同一个)",
  "blog_api": "https://你的博客域名/teonotify-qq-comment.php",
  "blog_key": "与博客安全接口一致的Key"
}

用 systemd 托管两个服务,保证崩溃自动拉起、开机自启。以 qq-server.service 为例:

[Unit]
Description=TeoNotify QQ server
After=network.target

[Service]
WorkingDirectory=/opt/teonotify
ExecStart=/usr/bin/python3 /opt/teonotify/qq-server.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
# ws_listener 用同样的方式写 qq-ws.service
systemctl daemon-reload
systemctl enable --now qq-server qq-ws
systemctl status qq-server   # 确认 active (running)

服务端起来后有健康检查接口,可以直接验证:

curl -s http://127.0.0.1:8970/health
# 返回 {"ok": true, "status": "up"}

qq-server.py 的核心是 access_token 管理:QQ 的 token 只有 2 小时有效期,需要自动换取并缓存、临期自动刷新,否则机器人发消息会 401:

TOKEN_URL = "https://api.bot.qq.com/app/getAppAccessToken"

class TokenManager:
    def get(self, force=False):
        # 未过期(留 120s 余量)直接用缓存, 否则重新换取
        if not force and self.token and time.time() < self.expire_at - 120:
            return self.token
        token, ttl = self._fetch()
        self.expire_at = time.time() + int(ttl)
        return token

    # 发消息遇 401/403 时强制刷新 token 重试一次

安全接口:多层防垃圾

引用回复要往博客插评论,这个口子如果不防好,垃圾评论就能借机涌入。博客侧接收接口 teonotify-qq-comment.php 做了五层校验:

  1. IP 白名单:只接受常驻服务器公网 IP(REMOTE_ADDR,不可伪造);
  2. API Key:请求头 X-Notify-Key 比对;
  3. HMAC-SHA256 签名sign = HMAC(key, ts|nonce|cid|content),防伪造请求;
  4. 时间戳 + nonce:防重放攻击(5 分钟容差 + 随机 nonce);
  5. 内容约束:长度限制 + 危险标签过滤 + cid 存在性校验。

服务端 POST 时生成签名:

ts    = str(int(time.time()))
nonce = f"{int(time.time()*1000)}{cid}"
msg   = f"{ts}|{nonce}|{cid}|{content}"
sign  = hmac.new(BLOG_KEY.encode(), msg.encode(), hashlib.sha256).hexdigest()

requests.post(BLOG_API, json={"cid": cid, "content": content},
    headers={
        "User-Agent": "Mozilla/5.0 (TeoNotify/1.0)",  # 关键: 别用 Python-urllib
        "X-Notify-Key": BLOG_KEY, "X-Notify-Ts": ts,
        "X-Notify-Nonce": nonce, "X-Notify-Sign": sign,
    })

博客侧校验(节选核心逻辑):

// 1. IP 白名单
if (!in_array($clientIp, $ALLOW_IPS)) fail(403, 'ip not allowed');

// 2. API Key
if (!hash_equals($NOTIFY_KEY, $_SERVER['HTTP_X_NOTIFY_KEY'] ?? '')) fail(401, 'invalid key');

// 3. ts + nonce
if (abs(time() - intval($ts)) > 300) fail(401, 'ts invalid');
if (strlen($nonce) < 8) fail(400, 'nonce invalid');

// 4. HMAC 签名比对(用 hash_equals 防时序攻击)
$expected = hash_hmac('sha256', "{$ts}|{$nonce}|{$cid}|{$content}", $NOTIFY_KEY);
if (!hash_equals($expected, $sign)) fail(401, 'sign invalid');

// 5. 内容约束 + cid 存在性
if (preg_match('/<\s*(script|iframe|object|embed|style)/i', $content)) fail(400, 'forbidden tags');
$row = $db->query("SELECT coid FROM {$PREFIX}comments WHERE coid=".intval($cid))->fetch_assoc();
if (!$row) fail(404, 'comment not found');

通过后插入一条博主回复(author=Astarryparent=原评论status=approved),再复用插件的 SMTP 通道给原评论者发"您的评论已回复"邮件。

踩过的坑(真实经验)

坑 1:博客在 CDN 后,REMOTE_ADDR 不是真实 IP

blog.astarry.cn 在腾讯云 EdgeOne CDN 后,PHP 的 REMOTE_ADDR 是 CDN 节点 IP(还会变),用它做 IP 白名单永远对不上。真实客户端 IP 要从 EdgeOne 注入的可信头 HTTP_EO_CONNECTING_IP 取,回退 X_REAL_IP / X_FORWARDED_FOR

$clientIp = $_SERVER['HTTP_EO_CONNECTING_IP']
         ?? $_SERVER['HTTP_X_REAL_IP']
         ?? (explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '')[0]
         ?? $_SERVER['REMOTE_ADDR'] ?? '');

坑 2:虚拟主机 WAF 拦截 Python 的请求

西部数码的 WTS-WAF 会把默认 Python-urllib/x 的请求直接 403。ws_listener 发请求必须带浏览器 UA:User-Agent: Mozilla/5.0 (TeoNotify/1.0),否则一直 403 找不到原因。

坑 3:接口地址一度指向测试站

旧版的 BLOG_API 硬编码指向了测试站(www.astarry.top),导致正式博客回复一直 comment not found / 403。改成 https://blog.astarry.cn/teonotify-qq-comment.php 后正常。凡接口类配置,务必确认指对了正式环境。

通道可扩展

插件把通知通道抽象成 ChannelInterface,想接 Server酱、钉钉、Webhook 只要实现接口放进 Channels/ 目录即可自动发现。示例——接一个 Server酱通道:

class TeoNotify_ServerChanChannel implements TeoNotify_ChannelInterface
{
    public function __construct(array $settings) { $this->sckey = $settings['sckey'] ?? ''; }
    public function isEnabled(): bool { return !empty($this->sckey); }
    public function sendNewCommentNotice(array $comment, array $context): void
    {
        // GET https://sctapi.ftqq.com/<sckey>.send?title=...&desp=...
    }
    public function sendReplyNotice(array $parent, array $reply, array $context): void {}
}

邮件通道(基础,人人可用)

  1. TeoNotify 文件夹上传到 usr/plugins/,后台启用;
  2. 填 SMTP 配置:服务器(如 smtp.qiye.aliyun.com)、端口(SSL 465 / STARTTLS 587)、账号、密码(建议授权码)、发件人、博主接收邮箱
  3. 有人评论 → 邮件通知你;你在后台回复 → 邮件通知评论者。

SMTP 用原生 stream_socket_client 实现,无第三方依赖,适配虚拟主机。

开源与相关链接

TeoNotify 已在 GitHub 开源,服务端与插件本体都在:Astarry-1127/TeoNotify

  • 纯 PHP 邮件通道(无第三方依赖,适配虚拟主机);
  • 服务端纯 Python(token 管理 + WebSocket 监听,依赖仅 websockets);
  • 插件可扩展通知通道(ChannelInterface)。

配套插件:TeoSeo(SEO/GEO 优化插件)。

发表评论

暂无评论,快来抢沙发吧!