写在前面
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 博客安全接口 → 自动发布博主回复 → 邮件通知评论者
关键点:
- 通知带
评论ID=xxx:引用回复能精确定位到哪条评论; - 安全接口多层防护: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 做了五层校验:
- IP 白名单:只接受常驻服务器公网 IP(
REMOTE_ADDR,不可伪造); - API Key:请求头
X-Notify-Key比对; - HMAC-SHA256 签名:
sign = HMAC(key, ts|nonce|cid|content),防伪造请求; - 时间戳 + nonce:防重放攻击(5 分钟容差 + 随机 nonce);
- 内容约束:长度限制 + 危险标签过滤 + 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=Astarry、parent=原评论、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 {}
}
邮件通道(基础,人人可用)
- 把
TeoNotify文件夹上传到usr/plugins/,后台启用; - 填 SMTP 配置:服务器(如
smtp.qiye.aliyun.com)、端口(SSL 465 / STARTTLS 587)、账号、密码(建议授权码)、发件人、博主接收邮箱; - 有人评论 → 邮件通知你;你在后台回复 → 邮件通知评论者。
SMTP 用原生 stream_socket_client 实现,无第三方依赖,适配虚拟主机。
开源与相关链接
TeoNotify 已在 GitHub 开源,服务端与插件本体都在:Astarry-1127/TeoNotify。
- 纯 PHP 邮件通道(无第三方依赖,适配虚拟主机);
- 服务端纯 Python(token 管理 + WebSocket 监听,依赖仅 websockets);
- 插件可扩展通知通道(
ChannelInterface)。
配套插件:TeoSeo(SEO/GEO 优化插件)。
评论 (0)
暂无评论,快来抢沙发吧!