文章12
标签11
分类3

Typecho插件排查实录

Typecho 插件排查实录:从装不上到发不出邮件

环境:Typecho 1.3.0 · PHP 7.4.33 · 虚拟主机(美国节点)
记录一次把插件从「全挂」修到「能用」的完整过程,含可直接复用的排查方法。

引言

一次集中排查了 5 个插件的三类问题:

  1. 装不上 —— 点启用要么「无法启用插件」,要么直接 500
  2. 发不出邮件 —— CommentNotifier 报 MAIL FROM command failed, 502 Invalid parameters
  3. 头像不显示 —— 评论区头像全是破图

三类问题看起来无关,其实根因只有两个: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.7Links_Plugin\TypechoPlugin\Links-1.2.7\Plugin✗
typecho-Comment2MailComment2Mail_Plugin\TypechoPlugin\typecho-Comment2Mail\Plugin✗
ColorHighlight-for-typecho-masterColorHighlight_Plugin...\ColorHighlight-for-typecho-master\Plugin✗
wzTypechoCodeHighlight_Plugin\TypechoPlugin\wz\Plugin✗
LinksLinks_Plugin\TypechoPlugin\Links\Plugin✓

规律:GitHub 打包下载的仓库,文件夹名往往带 -master、-1.2.7、-main 后缀,必须去掉。

1.4 正确写法(Typecho 1.2 / 1.3)

目录结构

usr/plugins/
└── MyPlugin/              ← 文件夹名 = 类名
    └── Plugin.php

Plugin.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) {}   // 可选但建议保留
}

⚠️ 三个易错点:

  1. 不要用老接口 Typecho_Plugin_Interface —— 1.2 起已换成命名空间写法,用错直接 500
  2. 插件文件省略结尾 ?> 是标准做法,不要加
  3. 文件夹名不要加版本后缀。我在这上面栽过:把插件目录命名成 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_ExceptionTypecho\Plugin\Exception
Typecho_Plugin_InterfaceTypecho\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: 502

3.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.com200 ✓备选
cravatar.cn时好时坏不稳定
secure.gravatar.com(官方)000 ✗国内被墙
www.gravatar.com(官方)000 ✗国内被墙
avatar.dawnlab.me000 ✗服务已停

关键结论: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=mp

d=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 排查顺序建议

遇到插件问题,按这个顺序走,能少走弯路:

  1. 文件夹名对不对(占一半以上的问题)
  2. 看 PHP 错误日志(500 的具体原因都在里面)
  3. 用诊断脚本读真实配置(后台看到的不一定是实际值)
  4. 实测外部依赖是否可达(curl -o /dev/null -w "%{http_code}")
  5. 实测页面真实渲染(别靠猜)

附:常用的几行命令

# 测外部服务可达性
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 评论

  1. 苏 10月7日 回复
    123