前言

前一个月,我开发了一个让AI管理Typecho的插件与skill和脚本,现在我对此进行升级了。

从插件到 CLI:一个月真实使用之后,它从「能跑通」进化到「装上就能用」——npm 一行命令,零依赖进终端。

上个月的让AI管理你的博客:我开发了一款Typecho JSON API插件解决的是"有没有"的问题:一套覆盖博客管理全场景的 JSON API,让 AI 能直接发文章、审评论、管分类。但那套东西用起来还偏"开发者向"——AI 得拿着 TypeScript 客户端现拼代码,skill 里塞着一堆源码副本。

这一个多月,我把它用成了日常工具,也借这次重构把它打磨成了完整的形态:TypechoCli。这篇文章讲讲用了什么、改了什么,以及过程中踩到的最有趣的一个坑。

一个多月用下来

先说结论:管博客这件事,已经彻底从后台搬进了终端。

现在这个博客有 30 篇文章、24 条评论,发文、改稿、删稿、评论审核、状态巡检,全部在命令行里完成,一个后台都不用进。核心是一条命令:

typecho-cli status
=== 连通性 ===
ping: pong

=== 总体统计 ===
文章 30 篇 | 页面 3 个 | 分类 2 | 标签 33
评论 共 24 条(通过 24 / 待审 0 / 垃圾 0)
媒体 0 个 | 用户: Young143
最新文章: 《又到了这种时候》

=== 分类分布 ===
  千言万语: 10 篇
  技术笔记: 20 篇

一条命令看到连通性、统计、待审核评论、页面列表——这就是每天管理博客的全部界面。

本地为源:Markdown 工作流

这次值得单独一提的是写作流。所有文章以本地 Markdown 为主副本,按 {编号}.{标题}.md 命名,front matter 记录元信息:

---
title: 又到了这种时候
date: 2026-09-06
cid: 50
category: 千言万语
tags:
  - 随记
  - 千言万语
---

发新文章时 create-post 做三件事:读 front matter → 调 API 创建 → 把线上返回的 cid 自动回写到本地文件。此后 update-post <cid> 以本地文件为准同步,本地永远是源,线上只是镜像。regen-index 则扫描全目录,按分类分组、日期倒序重建 文章/_index.md

于是写作流程变成:编辑器里写 Markdown → 一条命令发布 → 索引自动更新。human 写,AI 管,各干各的。

这次升级了什么

1. 更名:TypechoAgent → TypechoCli

当初叫 TypechoAgent,是因为核心是那个插件。现在 CLI 成了主要使用入口,名字跟着定位走。顺手把 API 端点从 /action/ta 换成 /action/tc,插件目录、类名、配置键全部对齐。

代价是插件端改动要连动:GitHub 仓库改名、npm 换包名、服务器上卸旧插件装新插件、API Key 重设一遍。教训是起名字要考虑未来形态——好在这次是主动改,成本可控。

2. CLI 发布 npm:零依赖,装上就能用

npm i -g typecho-cli

这是本次最重要的升级。之前想用得 clone 仓库、装 tsx、跑 TypeScript;现在 CLI 打包成了 npm 包,装完直接用,Node 18+ 即可。

打包用 esbuild 把 CLI 和客户端打成一个文件(dist/cli.js),零运行时依赖——整个包只用到 Node 内置的 fspath,连 node_modules 都没有。prepack 脚本让 npm publish 时自动构建,发版就是改代码 → npm versionnpm publish 三步。

3. skill 只留说明书

这是个有意思的取舍。之前 agent skill 目录里塞着客户端源码、CLI 脚本、配置文件,和 GitHub 仓库里的副本是两份代码,每次改都要同步。这次干脆把它删到只剩一个 SKILL.md 说明书:

.agent/skills/typecho-cli/
└── SKILL.md     # 操作手册

AI 读说明书知道怎么用命令;命令本身来自 npm 全局安装。代码单一来源,升级就是 npm update -g typecho-cli。对想自己搭一套的人来说,仓库结构也因此变得清爽:plugin/ 是插件,cli/ 是 npm 包根,skill/ 是说明书,各管各的。

4. API 端点:/action/tc

随更名一起,端点从 /action/ta 换成了 /action/tc。功能不变,配置键从 plugin:TypechoAgent 换成 plugin:TypechoCli——卸载旧插件时会自动清掉旧配置。

最大的坑:SQLite 事务死锁

升级期间我顺手把博客数据库从 MySQL 迁到了 SQLite。这个过程里踩到了一个足够有趣、值得单独一节的坑。

为什么迁 SQLite

博客跑在一台小内存 VPS 上,MySQL 常驻占着几百 MB,而这博客一年也吃不到多少写入。SQLite 就一个文件,备份是 cp,迁移是转换脚本,何乐不为。

转换没有想象中简单

网上"MySQL 转 SQLite"的教程一抓一大把,但踩坑的人也一抓一大把:类型对不上、自增主键失效导致"转换完发不了新文章"。我的做法是绕开各种第三方转换脚本,直接用 Typecho 官方安装包里的 SQLite.sql 模板建表(官方写法用 INTEGER PRIMARY KEY,规避了自增的坑),再用 Node 的 node:sqlite 逐行参数化写入。这里还有个反直觉的发现:用 sqlite3 命令行导 SQL 文件,字符串里的 \r 会被静默剥掉——走文本通道必然丢字节,所以转换必须走参数化直写,最后按字节校验一致性。

死锁现场

数据库迁完,全站正常,博客照常访问。然后一测插件——发文章偶发卡死,请求超时,重试几次才能成功。

根因:两条连接,两把锁

翻 Typecho 1.3 源码发现一个隐蔽的设计:selectDb 即使只配置一个数据库,也会按读写分出两个独立的 PDO 连接(READ 和 WRITE)。平时相安无事,但插件里的事务代码是这样写的:

$db->query('BEGIN');          // 裸字符串 → 默认走 READ 连接
// ... 在 READ 连接的事务里读数据,持有 SHARED 锁
$db->insert('contents')->rows([...]);   // 走 WRITE 连接,需要 RESERVED 锁
$db->query('COMMIT');

读连接的事务持着 SHARED 锁没放,写连接的 INSERT 等锁——自己等自己,直到超时。同一个进程里,两个连接,互相是对方的地狱。

修复

方向不是去掉事务(原子性必须保),而是事务内所有语句——包括读——统一走写连接

$db->query('BEGIN', Db::WRITE);         // 显式指定写连接
$this->fetchAllW(...);                   // 事务内读也走写连接
$db->insert('contents')->rows([...]);   // 同一条连接
$db->query('COMMIT', Db::WRITE);

改造完成后死锁消失,事务内读到的数据和写入的一致性也有了保证。顺带还修了一个潜伏的 bug:批量操作后分类文章计数不对——也是读写分离在背锅。

这个坑的通用教训:用连接池的框架里开事务,先搞清楚语句落在哪条连接上。MySQL 下这问题被 InnoDB 的锁机制掩盖了,SQLite 的锁语义让它现了形。

现在的形态

  • GitHubTypechoCliplugin/ 插件、cli/ npm 包、skill/ 说明书
  • npmtypecho-cli
  • 使用:装插件 → setApiKeynpm i -g typecho-cli → 配两个环境变量 → typecho-cli status

上一篇文章的结尾我写:重复性的管理工作交给程序,人只负责创造。这一次升级让这句话的落地成本又降了一截——clone 不用了,tsx 不用了,一行 npm 命令,AI 的技能说明书念完就能上岗。

工具应该做工具该做的事,剩下的交给写博客的人。