$kernelink route --hydrate --safe

页面加载 /
跳到正文
auth://account/session

登录工作区

使用 Emlog 账户继续访问你的内容与互动记录。

忘记密码?

打开 Emlog 原生登录页

5.md
workspace / posts
~/posts/5.md 阅读中

给博客写百度推送插件,记点过程中的事

起因是问了一句 EMlog 的插件会不会写,然后就成了这个把文章链接定时推给百度的东西。这篇记详细一点,把关键代码贴出来,免得以后自己还得翻仓库。

还没动手就撞上一个拦路的

百度给的接口调用地址长这样:http://data.zz.baidu.com/urls?site=https://www.binliyou.com&token=xxx,里面的 site 是带 www 的。而站点实际用的是裸域,www 根本没做解析,curl 一试就露了:

https://binliyou.com/       200
https://www.binliyou.com/   连不上

麻烦的地方在于,域名不一致时百度不会报错。接口照样返回 200、照样给你 success 字段,只是提交的链接全部落在 not_same_site 数组里,一条都不入队。要是先装上跑一轮,当日配额花了、日志看着还挺正常,回头再查域名就晚了。

所以插件里第一个功能不是推送,是比对:

$want = parse_url($query['site'], PHP_URL_HOST);
if (!$want) {
    $want = $query['site'];
}
$mine = parse_url(BLOG_URL, PHP_URL_HOST);
if ($mine && strcasecmp($want, $mine) !== 0) {
    return sprintf(
        '接口地址里的 site 域名是 %s,站点当前域名是 %s。两者不一致时百度会把链接全部退回 not_same_site,一条都不会入队;请先在资源平台改用站点真实域名的接口地址。',
        $want,
        $mine
    );
}

不一致就在设置页顶上挂黄条,推送按钮点了也不执行。整件事里花的时间最少、价值最大的一处是这里。

接口调用本身没什么花样

POST 纯文本,一行一个 URL,Content-Type: text/plain:

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $body,          // implode("\n", $urls)
    CURLOPT_HTTPHEADER => ['Content-Type: text/plain', 'Expect:'],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 30,
]);

成功返回 remain(当天剩余配额)、success(成功条数)、not_same_site、not_valid;失败是 4xx 带 error 和 message。两个处理原则:remain <= 0 立刻停止,配额当日有效不累计,继续打只是浪费;接口报错也停止,不要在下一次重试时把同一批反复推。

链接用 EMlog 自己的 Url::log($gid) 生成,这样站点哪天开了伪静态或改了 URL 模式,推出去的地址跟着变,不用插件操心。

EMlog 插件的机制是翻源码确认的

几条硬约定,名字写错一个就加载不上:

  • 入口 baidu_push/baidu_push.php,头部注释块是插件元信息(Plugin Name / Version / Description)。
  • 配置读写走 Storage::getInstance('baidu_push'),数据落在 storage 表。插件名和键名都限 16 个字符、只能 [\w_],超了会被静默替换成 DefaultPlugin / DefaultName,这个坑够阴的。
  • 设置页文件名必须是 baidu_push_setting.php,里面要有 plugin_setting_view() 和 plugin_setting() 两个函数,路由是 admin/plugin.php?plugin=baidu_push。
  • 启用/升级/删除的回调在 baidu_push_callback.php,可选。我用了删除回调,卸载时把配置清掉,因为里面存着百度准入密钥:
function callback_rm()
{
    $storage = Storage::getInstance(BaiduPush::PLUGIN);
    $storage->deleteAllName('YES');
}

后台那条路由过了 loginAuth::checkLogin() 和角色校验,确认只有管理员能进,才敢把密钥存这儿。

定时怎么触发

EMlog 没有常驻进程,"定时"得借外力。三条路:crontab 直接调 php、crontab curl 一个带密钥的 URL、前台访问时顺带触发。

选了第一条为主、第二条为辅。理由是密钥进 URL 就会进 nginx 访问日志,等于天天在日志里写一遍。第三条没做,因为没人访问就永远不跑,跑起来还拖慢那一次请求。

一个文件里分两种入口:

if (PHP_SAPI === 'cli') {
    $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
    $_SERVER['REQUEST_METHOD'] = 'GET';
    $_SERVER['HTTP_USER_AGENT'] = 'emlog-baidu-push-cron';
}

require_once dirname(__DIR__, 2) . '/init.php';
require_once __DIR__ . '/baidu_push_core.php';

// init.php 已经把 Content-Type 设成 text/html,这里必须在其之后覆盖
if (PHP_SAPI !== 'cli') {
    header('Content-Type: text/plain; charset=UTF-8');
}

那个注释是踩出来的:EMlog 的 init.php 第二行就 header('Content-Type: text/html'),你要是引导之前就设自己的头,会被它盖掉。CLI 那几行赋值是给 getIp() / getUA() 用的,虽然它们内部有 isset 兜底不会炸,但补上更干净。

后台那几个按钮限了最多五批(BaiduPush::run($mode, 5)),那是同步请求,让一次点击把全站扫完会把请求挂死。完整推送交给 crontab:

30 8 * * * php /app/emlog/content/plugins/baidu_push/run.php increment >> /var/log/emlog-baidu-push.log 2>&1
0 4 * * 0  php /app/emlog/content/plugins/baidu_push/run.php full      >> /var/log/emlog-baidu-push.log 2>&1

站点在 Docker 里就得走 docker exec <php容器> php ...。

增量为什么扫 date,不挂钩子

本来想在 save_log 钩子上记一笔"这篇待推",后来改成直接扫数据库:

-- 增量:水位之后发布或更新过的可见文章
SELECT gid, date FROM blog
 WHERE type='blog' AND hide='n' AND checked='y' AND date > <水位>
 ORDER BY date ASC, gid ASC;

-- 全量:按主键游标分批,可断点续推
SELECT gid FROM blog
 WHERE type='blog' AND hide='n' AND checked='y' AND gid > <游标>
 ORDER BY gid ASC LIMIT <批大小>;

关键前提是 EMlog 更新文章时会刷新 blog.date,所以"改过的"和"新增的"都会被这条查询带出来,不需要额外记账。好处是插件在正常请求链路上一点开销都不占——入口文件除了 require_once 什么都不做,不挂任何前台钩子。

草稿和待审核的推不出去,靠 hide='n' AND checked='y' 挡。

人工读代码抓出的一个真 bug

游标推进原来是这么写的,在拼 URL 的循环里顺手往前推:

foreach ($rows as $row) {
    $cursor = $row['gid'];          // 错:还没推就先走了
    $urls[] = Url::log($row['gid']);
}
self::pushBatch($api, $urls, $stat);

这样一批推送失败、百度没收下,游标照样走过了那批 gid,那些链接就永久漏推了,除非手动把游标归零重扫。改成先记在局部变量里,确认这批真送出去了才推进:

$lastGid = $cursor;
foreach ($rows as $row) {
    $urls[] = Url::log($row['gid']);
    $lastGid = $row['gid'];
}
$before = $stat['batches'];
self::pushBatch($api, $urls, $stat);
// 只有这一批真正送出去了才推进游标,失败的那批下次仍会重试
if ($stat['batches'] > $before) {
    $cursor = $lastGid;
}

增量水位同理。这类问题不在任何报错里,跑起来一切正常,只有读代码能看见——写完通读一遍这个动作,值。

并发就一个 flock

$file = sys_get_temp_dir() . '/emlog_baidu_push.lock';
$fp = @fopen($file, 'c');
if (!flock($fp, LOCK_EX | LOCK_NB)) {
    $age = time() - (int)@filemtime($file);
    fclose($fp);
    return $age > self::LOCK_TTL ? self::forceLock($file) : false;
}
@touch($file);

拿不到锁就这次跳过并记录原因,不排队不重试。锁文件超过三十分钟视为异常残留会尝试强制接管,但接管用的还是非阻塞 flock,原来那个进程活着的时候抢不到,所以最坏情况只是多跳过一次,不会出现两个任务同时往百度推。

打包传后台也有坑

后台「安装插件」要求上传 zip,解压前的校验逻辑是这样的:

$r = explode('/', $zip->getNameIndex(0), 2);
$dir = isset($r[0]) ? $r[0] . '/' : '';
// type == 'plugin' 时
$plugin_name = substr($dir, 0, -1);
$re = $zip->getFromName($dir . $plugin_name . '.php');
if (false === $re) {
    return -1;   // 前端提示"插件包格式不正确"
}

它拿 zip 第一个条目的顶层目录名当插件名,再要求那个目录下有同名 php。所以文件平铺在压缩包根目录会直接失败,必须是一层目录包着。

另一个坑在打包工具:Windows PowerShell 5.1 的 Compress-Archive 会把条目名写成反斜杠(baidu_push\baidu_push.php),上面那句 explode('/') 就切不出目录了,报错还只告诉你"格式不正确"。改用 bsdtar:

C:\Windows\System32\tar.exe -a -cf baidu_push.zip baidu_push

打完用 tar -tf 回读确认条目是正斜杠:

baidu_push/
baidu_push/baidu_push.php
baidu_push/baidu_push_core.php
baidu_push/baidu_push_setting.php
baidu_push/baidu_push_callback.php
baidu_push/run.php
baidu_push/README.md
🤞 分享
comments.cmd 可写入
guest@kernelink:~/posts/5$ comment --compose
identity.env 访客信息
插入 访客会话 Text + UBB · UTF-8 · LF 0 字符