暗色模式

TeoSeo 插件开发踩坑实录:从白屏到 JSON-LD 注入的八个坑

技术教程
2026-08-07
25
0

前言

TeoSeo 从 v1.0(sitemap + 推送)迭代到 v1.2.0(AI 内容优化),大部分时间花在了"修坑"而不是"写功能"上。这篇把踩过的坑按"症状 → 根因 → 修复"整理出来,九成坑都出在虚拟主机环境的特殊性,另外几成是 Typecho 1.3 的 API 变化和 AI 接口的行为差异。

坑一:虚拟主机 max_execution_time=30s,页面直接白屏

症状:后台面板点「生成」按钮,页面白屏;刷新后摘要其实已经生成成功(推送历史里能看到记录)。

根因:单篇生成是同步调用 AI 接口的,接口一慢(20~30s),PHP 执行到 30 秒上限被直接掐死。此时字段和日志可能已经写完,但最后的跳转响应永远发不出去——浏览器就停在白屏上。

修复:把生成从"同步等接口"改成"响应先返回,AI 挂到请求收尾(shutdown)执行"。发布文章的钩子里也做了同样的异步化,发布不用干等 AI。

register_shutdown_function(function () use ($cid, $force) {
    TeoSeo_Plugin::applyAiToCid($cid, $force);
});
header('Location: ...');  // 先跳走
exit;

教训:虚拟主机上任何"等外部接口"的操作,都默认按 30 秒上限设计,能异步就异步。

坑二:output_buffering=4096 把 302 憋在缓冲区——白屏第二层

症状:改完异步后还是白屏,但地址栏 URL 已经变成跳转目标了。

根因:虚拟主机 output_buffering=4096exit 之后 PHP 会先跑 shutdown 函数(AI 生成 2~30s)再刷输出缓冲区——302 响应被憋在缓冲区里,等 AI 跑完才发给浏览器。异步是异步了,响应还是没发出去。

修复:header 之后显式把响应发出去,PHP-FPM 环境用 fastcgi_finish_request()(立即发送 + 释放 worker,跳转后的页面不用排队):

if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request();   // 立刻发 302, worker 释放
} else {
    while (ob_get_level() > 0) { ob_end_flush(); }
    flush();
}

教训:虚拟主机的 output_buffering 默认是开的,exit 不等于"响应已发出"。

坑三:终极方案——面板生成改 AJAX,页面永不跳转

症状:前两个坑都修了,但"跳转 → 重载页面"这条路本身就有各种边界问题(会话、缓存、刷新竞态),体验始终不干脆。

根因:用"表单 POST + 302 跳转"的模式,浏览器必然经历"提交 → 等响应 → 跳转 → 重载",任何一环慢都是白屏。

修复:面板生成全部改 AJAX(fetch + JSON 接口)。点击后页面原地不动,按钮变"生成中…",接口在后台跑(最长超时时间内),完成后弹消息 + 刷新列表。批量生成也改成 JS 循环逐篇 fetch。

fetch(TEOSEO_BASE + '&ajax=1&cid=' + cid + '&_=' + token)
    .then(r => r.json())
    .then(j => { showMsg(j.msg, j.ok); location.reload(); });

教训:后台面板里任何超过几秒的操作,AJAX 是最省心的模式——白屏这个类别的问题直接不存在了。

坑四:typecho_fields.type 存的是字符串,不是数字

症状:文章编辑页的自定义字段区域刷出 Warning: Undefined array key "0_value",且主题的「SEO 关键词 / SEO 描述」输入框取不到值(后台能看到字段存在,但值是空的)。

根因typecho_fields.type 列是 varchar(8),存的是类型名'str' / 'int' / 'float' / 'json')。插件写入时填了数字 0,Typecho 1.3 读字段时按 $row[$row['type'] . '_value'] 拼列名——type 是 '0' 就去取不存在的 0_value 列。

// 错误: type 填数字 0
'type' => 0,
// 正确: 存类型名字符串
'type' => 'str',

教训:Typecho 1.3 的自定义字段 type 是字符串枚举,不是 int。这个坑的症状很隐蔽——前台一切正常,只有编辑页报警告。

坑五:插件升级不是幂等的——钩子翻倍、菜单累积

症状:升级插件后,前台文章的 JSON-LD 结构化数据输出了 3 份;后台左侧菜单出现7 个 TeoSeo

根因:Typecho 的 Widget\Init::alloc() 会先把 options.plugins 里的旧钩子导入内存,此时再执行插件的 activate(),每个钩子又被注册一遍——重复升级几次,注册表里就有 N 份。菜单同理:Helper::addPanel 每次激活都往 panelTable追加一条,从不检查是否已存在。

修复:升级脚本不要调用 Init::alloc(),先清空注册表再激活一次:

// 1. UPDATE options SET value='{"handles":[],"activated":[]}' WHERE name='plugins'
// 2. 清空 panelTable
// 3. require Plugin.php → TeoSeo_Plugin::activate() → \Typecho\Plugin::activate('TeoSeo')
// 4. 写回 \Typecho\Plugin::export()\n\n> ⚠️ **注意**:上面的 SQL 会**清空整个插件注册表**——仅适用于站点只有 TeoSeo 一个插件(或其余插件都已停用)的升级场景。多插件并存时别照抄,请只删 `options.plugins` 里对应插件的条目,或者直接用官方后台的"停用 → 启用"流程。

教训:Typecho 插件的"停用再启用"不是幂等操作。涉及 activate() 的升级脚本,先清注册表再激活,永远只跑一次。

坑六:JSON-LD 输出存在注入风险

症状:审查代码时发现,AI 生成的摘要是外部接口返回的不可信内容,直接拼进 <script type="application/ld+json">——如果摘要里混进 </script>,会提前闭合标签变成 HTML 注入点。

根因json_encode() 默认不转义 < / >(只对引号做处理),</script> 原样输出。

修复:加 JSON_HEX_TAG | JSON_HEX_AMP

json_encode($jsonLd, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP)

教训:JSON-LD 也是"输出 HTML 上下文",json_encode 的默认转义级别不够,凡是塞进 <script> 的 JSON 都要 HEX_TAG。

坑七:SSL 校验全关 = API Key 裸奔

症状:为兼容虚拟主机(CA 链残缺导致 curl 校验收失败),把 CURLOPT_SSL_VERIFYPEERVERIFYHOST 全关了——每个请求都带着 AI 接口的 API Key 走可被中间人嗅探的链路。

修复:默认严格校验,勾选「跳过 SSL 校验」开关时才允许失败降级重试一次:

// 先严格模式试一次
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
// 失败且配置允许 → 降级重试
if (false === $resp && $skipSsl) {
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
    $resp = curl_exec($ch);
}

教训:虚拟主机 CA 问题只影响特定域名,不能一刀切全关——默认严格,按需降级

坑八:推理模型的 content 为空 + prompt 泄露

症状:换用智谱的推理模型 glm-4.7-flash 后,接口返回成功但 content 永远是空——输出全耗在思考过程上了。另一次用 glm-4-flash-250414 时,模型把 prompt 里的说明文字原样抄进了摘要("适合用作搜索引擎的 meta description。")。

修复:① 模型默认值改用非推理模型 glm-4-flash-250414,并在设置页注明推理模型的坑;② prompt 里加一句禁令:"summary 只写文章内容本身,禁止出现说明性文字"。

教训:调 AI 接口时,推理模型 ≠ 输出模型,生成类任务要避开 reasoning 系列;prompt 里的"要求"会被模型当成"可引用的素材",该加禁令的地方必须加。

总结:给 Typecho 1.3 插件开发者的检查清单

  1. 涉及外部接口的操作:默认按 30s 执行上限设计,能异步就异步;
  2. 面板操作:优先 AJAX,别用表单 + 跳转;
  3. 自定义字段:type字符串类型名
  4. 升级脚本:先清注册表再激活,且只跑一次;
  5. 输出到 <script> 的 JSON:必须 HEX_TAG
  6. curl 调外部 HTTPS:默认严格校验,降级要有开关;
  7. 调 AI:避开推理模型,prompt 里的要求要防泄露;
  8. fastcgi_finish_request() 是虚拟主机异步的好朋友,但前提是 output_buffering 别把响应憋住。

参考资料

发表评论

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