前言
前一个月,我开发了一个让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 内置的 fs 和 path,连 node_modules 都没有。prepack 脚本让 npm publish 时自动构建,发版就是改代码 → npm version → npm 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 的锁语义让它现了形。
现在的形态
- GitHub:TypechoCli —
plugin/插件、cli/npm 包、skill/说明书 - npm:typecho-cli
- 使用:装插件 →
setApiKey→npm i -g typecho-cli→ 配两个环境变量 →typecho-cli status
上一篇文章的结尾我写:重复性的管理工作交给程序,人只负责创造。这一次升级让这句话的落地成本又降了一截——clone 不用了,tsx 不用了,一行 npm 命令,AI 的技能说明书念完就能上岗。
工具应该做工具该做的事,剩下的交给写博客的人。
没有评论