Typecho 插件排查实录:从装不上到发不出邮件
环境:Typecho 1.3.0 · PHP 7.4.33 · 虚拟主机(美国节点)
记录一次把插件从「全挂」修到「能用」的完整过程,含可直接复用的排查方法。
引言
一次集中排查了 5 个插件的三类问题:
- 装不上 —— 点启用要么「无法启用插件」,要么直接 500
- 发不出邮件 —— CommentNotifier 报
MAIL FROM command failed, 502 Invalid parameters - 头像不显示 —— 评论区头像全是破图
三类问题看起来无关,其实根因只有两个:Typecho 的插件命名规则,和外部服务的可用性。
一、插件装不上:文件夹名必须等于类名
1.1 现象
后台点「启用」,跳转到:
/index.php/action/plugins-edit?activate=Links-1.2.7然后报「无法启用插件」,或者「页面没有找到」。
1.2 原因
Typecho 加载插件时,用文件夹名去拼期望的类名。源码在 var/Typecho/Plugin.php 的 portal() 方法:
public static function portal(string $pluginName, string $path): array
{
switch (true) {
case file_exists($pluginFileName = $path . '/' . $pluginName . '/Plugin.php'):
$className = "\\" . PLUGIN_NAMESPACE . "\\{$pluginName}\\Plugin";
break;
case file_exists($pluginFileName = $path . '/' . $pluginName . '.php'):
$className = "\\" . PLUGIN_NAMESPACE . "\\" . $pluginName;
break;
default:
throw new PluginException('Missing Plugin ' . $pluginName, 404);
}
return [$pluginFileName, $className];
}关键:$pluginName 来自 URL 的 activate 参数,也就是文件夹名。
- 文件夹叫
Links→ 期望类\TypechoPlugin\Links\Plugin - 文件夹叫
Links-1.2.7→ 期望类\TypechoPlugin\Links-1.2.7\Plugin← 含连字符,根本不是合法类名
文件里写的类名叫什么,完全不影响。找不到期望的类名,class_exists() 返回 false,就是「无法启用插件」或 500。
1.3 常见踩坑对照
| 你下载的文件夹名 | 里面的类名 | Typecho 期望的类名 | 结果 |
|---|---|---|---|
| Links-1.2.7 | Links_Plugin | \TypechoPlugin\Links-1.2.7\Plugin | ✗ |
| typecho-Comment2Mail | Comment2Mail_Plugin | \TypechoPlugin\typecho-Comment2Mail\Plugin | ✗ |
| ColorHighlight-for-typecho-master | ColorHighlight_Plugin | ...\ColorHighlight-for-typecho-master\Plugin | ✗ |
| wz | TypechoCodeHighlight_Plugin | \TypechoPlugin\wz\Plugin | ✗ |
| Links | Links_Plugin | \TypechoPlugin\Links\Plugin | ✓ |
规律:GitHub 打包下载的仓库,文件夹名往往带 -master、-1.2.7、-main 后缀,必须去掉。
1.4 正确写法(Typecho 1.2 / 1.3)
目录结构
usr/plugins/
└── MyPlugin/ ← 文件夹名 = 类名
└── Plugin.phpPlugin.php 完整示例
<?php
namespace TypechoPlugin\MyPlugin; // ← 必须与文件夹名一致
use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
class Plugin implements PluginInterface // ← 类名就叫 Plugin
{
public static function activate() { /* ... */ }
public static function deactivate() { /* ... */ }
public static function config(Form $form) { /* ... */ }
public static function personalConfig(Form $form) {} // 可选但建议保留
}⚠️ 三个易错点:
- 不要用老接口
Typecho_Plugin_Interface—— 1.2 起已换成命名空间写法,用错直接 500 - 插件文件省略结尾
?>是标准做法,不要加 - 文件夹名不要加版本后缀。我在这上面栽过:把插件目录命名成
Lantern_1.2,结果点启用直接「页面没有找到」
二、启用时报 500:一个藏在源码里的真 bug
有些老插件即使文件夹名改对了,启用仍然 500。以下是排查出的两类原因。
2.1 PHP 里 || 返回的不是布尔值
某访问统计插件的建表失败分支:
if (('Mysql' == $type && $code == (1050 || '42S01'))) {1050 || '42S01' 在 PHP 里恒等于 1 —— || 返回第一个真值,而 1050 是真值。所以这个判断等价于 $code == 1,「数据表已存在」的判断永远不成立,必然走进 else 抛异常 → 500。
原意是「错误码是 1050(表已存在)或 42S01(表不存在)就当作成功」,想用 || 表达「或」,写错了。
正确改法(不依赖错误码,更稳):
// 先探测表是否存在
try {
$db->query("SELECT 1 FROM `$table` LIMIT 1", \Typecho\Db::READ);
return '数据表已存在,插件启用成功';
} catch (\Throwable $e) {
// 不存在,继续建表
}2.2 老 API 类名在 1.3 下的对应关系
| Typecho 1.1 老写法 | Typecho 1.2 / 1.3 写法 |
|---|---|
| Typecho_Db::get() | Typecho\Db::get() |
| Typecho_Widget::widget('Widget_Options') | Widget\Options::alloc() |
| Typecho_Request::getInstance() | Typecho\Request::getInstance() |
| Typecho_Plugin_Exception | Typecho\Plugin\Exception |
| Typecho_Plugin_Interface | Typecho\Plugin\PluginInterface |
1.3 保留了一部分兼容别名,所以有些老代码「看起来能跑」,但迟早出问题,建议一并改掉。
三、邮件发不出去:发件邮箱填成了服务器地址
3.1 现象
CommentNotifier 报错:
The following From address failed: : MAIL FROM command failed,
Invalid paramenters ,502,SMTP server error: MAIL FROM command failed
Detail: Invalid paramenters SMTP code: 5023.2 定位方法:用一次性诊断脚本读真实配置
邮件插件的配置存在数据库里,从后台看不出全貌。可以上传一个带 token 的只读 PHP 脚本,读完立即删除。
脚本要点:
// 1. 用 SHOW TABLES 自动识别表前缀(不要用正则去猜 config.inc.php)
$r = mysqli_query($conn, "SHOW TABLES");
while ($row = mysqli_fetch_array($r)) { $tables[] = $row[0]; }
foreach ($tables as $t) {
if (substr($t, -7) === 'options') { $prefix = substr($t, 0, -7); break; }
}
// 2. 读各插件配置,密码字段掩码
$r = mysqli_query($conn, "SELECT value FROM `{$prefix}options`
WHERE name='plugin:CommentNotifier' LIMIT 1");
// 3. URL 带 token,只有自己知道
if ($_GET['k'] !== '你的随机token') { http_response_code(404); exit; }查出 CommentNotifier 的实际配置:
STMPHost = smtp.qq.com ✓
SMTPUserName = 2404999487@qq.com ✓
SMTPPassword = (16位) ✓
SMTPSecure = ssl / SMTPPort = 465 ✓
from = smtp.qq.com ✗ ← 问题在这里「SMTP邮箱地址」被填成了服务器地址 smtp.qq.com,而不是邮箱地址。
3.3 复现验证
诊断脚本里真实走一遍 SMTP 握手,只到 MAIL FROM 就断开(不发信):
✓ 登录: 235 Authentication successful
✓ MAIL FROM:<2404999487@qq.com> → 250 OK
【复现】MAIL FROM:<smtp.qq.com> → 502 Invalid input报错完全一致,根因确认。
3.4 QQ 邮箱的正确配置
| 项目 | 值 |
|---|---|
| 邮件推送方式 | SMTP |
| SMTP服务器地址 | smtp.qq.com |
| SMTP登录用户 | 你的QQ号@qq.com |
| SMTP邮箱地址(发件邮箱) | 你的QQ号@qq.com,必须与登录用户一致 |
| SMTP登录密码 | 授权码(不是 QQ 密码) |
| 加密方式 | SSL,端口 465;或 TLS,端口 587 |
| 发信方案 | 传统直发(直连 SMTP) |
授权码获取:QQ邮箱 → 设置 → 账号 → 开启 POP3/SMTP 服务 → 生成授权码。
要点:QQ 邮箱要求 MAIL FROM 的地址必须等于认证账号,写别的(哪怕是同域的别名)都会被拒。
四、评论头像不显示
4.1 原因:主题依赖的第三方服务已经死了
主题的 comments.php 里写死了头像来源:
$host = '//avatar.dawnlab.me';
// QQ 邮箱 → //avatar.dawnlab.me/qq/{qq}
// 其他 → //avatar.dawnlab.me/gravatar/{md5}实测 avatar.dawnlab.me 返回 HTTP 000(连不上),服务已停。所以所有评论头像都是破图。
4.2 各头像源的国内可达性实测
头像 URL 是访客浏览器去加载的,所以国内可达性是首要指标:
| 头像源 | HTTP 状态 | 备注 |
|---|---|---|
| q1.qlogo.cn(QQ官方) | 200 ✓ | 稳定 |
| gravatar.loli.net(Gravatar 国内镜像) | 200 ✓ | 推荐 |
| weavatar.com | 200 ✓ | 备选 |
| cravatar.cn | 时好时坏 | 不稳定 |
| secure.gravatar.com(官方) | 000 ✗ | 国内被墙 |
| www.gravatar.com(官方) | 000 ✗ | 国内被墙 |
| avatar.dawnlab.me | 000 ✗ | 服务已停 |
关键结论:Gravatar 官方域名在国内打不通,必须用镜像。
4.3 方案:d=404 + onerror
需求是「有 Gravatar 就用,没有就用随机头像」。不需要在后端做判断,用 HTTP 状态码 + JS 回落即可:
<img src="https://gravatar.loli.net/avatar/{md5}?s=100&r=G&d=404"
onerror="this.onerror=null;this.src='{随机头像API}'">- 该邮箱有 Gravatar → 正常返回图片,显示
- 该邮箱没有 Gravatar → Gravatar 返回 404 → 触发 onerror → 换成随机头像
- 镜像本身挂了 → 同样触发 onerror → 换成随机头像
三种情况全覆盖。d=404 是关键 —— 不加它,Gravatar 会返回一张默认图(200),onerror 就不会触发。
4.4 邮件里不能用这套方案
邮件客户端会过滤 JavaScript,onerror 无效。所以邮件模板里的头像必须用「总能出图」的地址:
https://gravatar.loli.net/avatar/{md5}?s=96&r=G&d=mpd=mp(mystery person)保证没有 Gravatar 时也返回一张图,不会破图。
五、哪些插件该淘汰
排查后,站上这几个插件建议处理:
| 插件 | 状态 | 建议 |
|---|---|---|
| CommentNotifier | 唯一配置正确、可用的邮件提醒 | 保留,改 from 一处即可 |
| CommentToMail | 配置 port=25 + ssl 不匹配 | 停用 |
| Comment2Mail | 无配置记录 | 停用 |
| CommentsByQQ | 依赖 pxwei.com/php_api.php,实测 404;且用 1.1 老接口 | 停用 |
| AvatarQQ | 用 1.1 老接口,1.3 下不可用;功能已被主题头像逻辑覆盖 | 停用 |
关于 CommentsByQQ 的真相
它的实现是:
curl_setopt($ch, CURLOPT_URL,
"http://www.pxwei.com/php_api.php?qq=" . $options->plugin('CommentsByQQ')->qq
. "&url=" . $comment->permalink);它把请求转发给第三方的 pxwei.com,由那个服务推送 QQ 消息。
- 域名还活着,但
php_api.php返回 404,接口已下线 → 插件彻底失效 - 它还是个通知类插件,不是头像插件,容易和邮件提醒功能重复
网上流传的「QQ 机器人打通」版本是另一个实现(基于 NoneBot + go‑cqhttp),需要你自己长期运行一个 QQ 机器人:
- go‑cqhttp 已于 2024 年停止维护
- 替代方案是 NapCat / Lagrange.OneBot(OneBot 协议)
- 需要一台常开的设备跑机器人,且有账号被风控/冻结的风险
如果不想折腾,用邮件提醒就足够了。
六、经验总结
6.1 交付插件前,先核对命名
# 看插件里的命名空间最后一段
grep -n "^namespace" usr/plugins/*/Plugin.php文件夹名必须与命名空间最后一段完全一致,不能加任何后缀。
6.2 「CSS 写了却不生效」先实测,别猜
用 Chrome DevTools Protocol 直接读元素的真实渲染值,不要靠推断反复上传试错:
chrome.exe --headless --remote-debugging-port=9333 --window-size=1440,900 about:blank
# 然后用 Node 内置 WebSocket 连上去发 Runtime.evaluate关键 JS:
var r = el.getBoundingClientRect(); // 真实渲染尺寸
var cs = getComputedStyle(el); // 实际生效的样式
img.naturalWidth + 'x' + img.naturalHeight; // 图片原始尺寸
// 列出所有匹配的选择器,判断哪条规则真的生效
document.styleSheets[i].cssRules[j].selectorText这次就是靠它才发现:CSS 规则完全正确,但选择器方向写反了 —— 把类加在 <body> 上,却写成 #content .那个类 ...(要求它是 #content 的后代),而实际它是祖先,选择器一次都没匹配上。
6.3 改数据库前必须先备份
用脚本批量停用插件时要小心:Typecho\Plugin::export() 在没有先 Plugin::init($data) 的情况下返回空数组,直接写回数据库会把所有插件配置清空。
正确做法:
$data = json_decode($row['value'], true); // 先从库里读出来
\Typecho\Plugin::init($data); // 喂给 Plugin
\Typecho\Plugin::deactivate($name); // 再操作
$db->query($db->update('table.options')
->rows(['value' => json_encode(\Typecho\Plugin::export())])
->where('name = ?', 'plugins'));并且操作前先把原值存一份,出问题能立刻回滚。
6.4 外部依赖是最大的不确定性
这次三个问题里,两个来自外部服务:
- 主题头像依赖的
avatar.dawnlab.me—— 服务已停 - CommentsByQQ 依赖的
pxwei.com/php_api.php—— 接口已下线
凡是把功能寄托在别人的免费服务上的,都要有失效预案。 Gravatar 官方域名在国内被墙这件事,也说明「用官方源」不等于「更可靠」。
6.5 排查顺序建议
遇到插件问题,按这个顺序走,能少走弯路:
- 文件夹名对不对(占一半以上的问题)
- 看 PHP 错误日志(500 的具体原因都在里面)
- 用诊断脚本读真实配置(后台看到的不一定是实际值)
- 实测外部依赖是否可达(
curl -o /dev/null -w "%{http_code}") - 实测页面真实渲染(别靠猜)
附:常用的几行命令
# 测外部服务可达性
curl -sL --connect-timeout 8 -o /dev/null -w "%{http_code} %{content_type}\n" "https://example.com/img.png"
# 看插件命名空间
grep -n "^namespace" usr/plugins/*/Plugin.php
# 看某个文件是否被改过(对比原始包)
diff <(unzip -p plugin.zip path/file) path/file
1 评论