本文档是 Typecho-Workers 插件开发的完整参考。以
typecho-plugin-antispam/目录为示例。
typecho-plugin-example/
├── package.json # npm 包声明(keywords 必须包含 typecho + plugin,typecho.plugin 包含元数据)
└── index.ts # 入口文件(ESM,export default init 函数)
插件加载器优先发现
index.ts,然后才回退到index.js/index.mjs/plugin.ts/plugin.js。本仓库内置插件统一使用 TypeScript;发布为独立 npm 包时,可将 TS 编译为 JS 并在package.json的typecho.plugin.entry中指向编译产物。
插件元数据统一放在 typecho.plugin 字段中,不再使用独立的 plugin.json 文件。
{
"name": "typecho-plugin-example",
"version": "1.0.0",
"description": "插件描述",
"keywords": ["typecho", "plugin"],
"author": "Your Name",
"license": "MIT",
"type": "module",
"main": "index.ts",
"typecho": {
"plugin": {
"id": "typecho-plugin-example",
"name": "示例插件",
"description": "插件功能描述",
"author": "Your Name",
"authorUrl": "https://example.com",
"version": "1.0.0",
"homepage": "https://github.com/...",
"license": "MIT",
"tags": ["example"],
"adminPage": "example",
"config": {
"fieldName": {
"type": "text",
"label": "字段标签",
"default": "",
"description": "帮助文本(支持 HTML)"
}
}
}
}
}adminPage 是可选字段。声明后,通用路由 /admin/plugin/<adminPage> 会自动读取
该插件的 name(或 adminPageTitle)作为后台页面标题;插件仍需通过 admin:page filter 渲染对应页面内容。
若自定义页面即插件设置 UI(且不使用声明式 config),可设 adminPageIsSettings: true:
插件列表的「设置」会指向 /admin/plugin/<adminPage>(Notes / Cache 等运维页不要设此标志)。
关键约束:
keywords必须同时包含"typecho"和"plugin",否则构建时不会被发现"type": "module"— 使用 ESM(export default,不用module.exports)main指向入口文件;本仓库本地插件使用index.tstypecho.plugin包含插件元数据(id、name、config 等)
| 类型 | 说明 | 额外字段 |
|---|---|---|
text |
单行文本 | — |
textarea |
多行文本 | — |
password |
密码(掩码显示) | — |
hidden |
隐藏字段 | — |
select |
下拉选择 | options: { value: label } |
radio |
单选按钮 | options: { value: label } |
checkbox |
多选框 | options: { value: label },default 为数组 |
repeatable |
可重复配置组 | itemFields: { fieldName: fieldDef },default 为对象数组 |
声明了 config 后,管理插件列表中自动显示「设置」链接,跳转到 /admin/plugin-config?id=<pluginId>。
repeatable 用于多个同结构配置项,例如多个后端存储挂载:
{
"type": "repeatable",
"label": "后端存储挂载",
"default": [{ "mount": "media", "provider": "r2" }],
"itemFields": {
"mount": { "type": "text", "label": "挂载目录", "default": "media" },
"provider": {
"type": "select",
"label": "存储类型",
"default": "r2",
"options": { "r2": "Cloudflare R2", "s3": "Amazon S3 兼容" }
}
}
}字段可选 showWhen 做条件显示,可选 optionsSource: "r2Bindings" 将下拉选项填充为当前 Worker 环境中的 R2 bucket binding。
/**
* 插件入口函数,由系统在构建时调用。
* 所有 Hook 注册通过 addHook 完成,此处不要执行 I/O。
*/
import type { PluginInitContext } from 'typecho/plugin-sdk';
export default function init({ addHook, pluginId }: PluginInitContext): void {
// filter 钩子:修改数据并返回
addHook('content:content', pluginId, (html: string) => {
return html + '<!-- powered by example plugin -->';
});
// call 钩子:执行副作用,不需要返回值
addHook('feedback:finishComment', pluginId, (comment: { coid?: number }) => {
console.log('新评论:', comment.coid);
});
}| 参数 | 类型 | 说明 |
|---|---|---|
addHook(point, pluginId, handler, priority?) |
function | 注册钩子处理函数。priority 默认 10,越小越早执行 |
pluginId |
string | 当前插件 ID(来自 package.json 的 typecho.plugin.id 字段) |
在 filter/call 处理函数中,从传入的 extra.options 读取配置:
import { loadPluginConfig } from 'typecho/plugin-sdk';
addHook('feedback:comment', pluginId, async (commentData: { _rejected?: string }, extra?: { options?: Record<string, unknown> }) => {
if (!extra?.options) return commentData;
// 读取本插件配置(已与 typecho.plugin.config 默认值合并)
const config = loadPluginConfig(extra.options, pluginId);
if (!config.apiKey) return commentData; // 未配置,跳过
// ... 业务逻辑
return commentData;
});主项目内插件也可直接解析
extra.options[\plugin:${pluginId}`](JSON 字符串)。独立 npm 插件推荐使用 SDK 提供的loadPluginConfig`。
配置存储:typecho_options 表,name = "plugin:<pluginId>",值为 JSON 字符串。
完整 Hook 点清单以 AGENTS.md 的 6.6 为准。
| Hook | 触发位置 | 参数 |
|---|---|---|
system:begin |
每次请求初始化 | (context) |
admin:header |
管理后台 <head> |
— |
admin:footer |
管理后台底部 | — |
admin:navBar |
管理后台导航 | — |
admin:writePost:option |
文章编辑器侧边栏选项 | (post) |
admin:writePost:advanceOption |
文章编辑器高级选项 | (post) |
admin:writePost:bottom |
文章编辑器底部区域 | (post) |
admin:writePage:option |
页面编辑器侧边栏选项 | (page) |
admin:writePage:advanceOption |
页面编辑器高级选项 | (page) |
admin:writePage:bottom |
页面编辑器底部区域 | (page) |
post:finishPublish |
文章发布后 | (post, extra?) |
post:finishSave |
文章保存后 | (post, extra?) |
post:delete |
文章删除前 | (post) |
post:finishDelete |
文章删除后 | (cid) |
page:finishPublish |
页面发布后 | (page, extra?) |
page:finishSave |
页面保存后 | (page, extra?) |
page:delete |
页面删除前 | (page) |
page:finishDelete |
页面删除后 | (cid) |
feedback:finishComment |
评论保存后 | (comment, extra) |
upload:beforeUpload |
文件上传前 | (file) |
upload:upload |
文件上传后 | (file) |
upload:delete |
文件删除 | (path) |
| Hook | 触发位置 | 参数 | 说明 |
|---|---|---|---|
content:markdown |
Markdown 渲染前 | (markdown, post) |
过滤原始 Markdown 文本 |
content:content |
Markdown 渲染后 | (html, post) |
过滤输出 HTML |
content:title |
标题输出 | (title, post) |
过滤文章标题 |
content:excerpt |
摘要输出 | (excerpt, post) |
过滤文章摘要 |
comment:content |
评论内容输出 | (html, comment) |
过滤评论 HTML |
comment:markdown |
评论 Markdown | (markdown, comment) |
过滤评论原始文本 |
comment:allowContent |
评论目标验证 | (allowed, extra) |
扩展自定义内容类型的评论资格;返回 true 前必须自行校验公开状态 |
comment:avatarMap |
公开评论 API 输出 | (avatarMap, { request, options }) |
过滤以评论 ID 为键的头像 URL 映射;异常时保留原映射 |
gravatar:url |
头像 URL 生成 | (url, { request?, options? }) |
过滤单个 Gravatar URL(如 Edge Cache 插件按 avatarCdnUrl 改写);异常时保留原 URL |
comment:list |
公开评论列表 API 响应 | (payload, { request, options, cid }) |
向评论列表 JSON 附加插件专属字段(如反垃圾时间令牌);发生在共享缓存之外,每次请求执行;返回原对象或变换后的对象 |
post:write |
文章保存前 | (data, extra) |
过滤文章写入数据 |
page:write |
页面保存前 | (data, extra) |
过滤页面写入数据 |
admin:managePosts:titleActions |
文章列表标题操作区 | (html, extra) |
在每篇文章标题旁追加管理操作 |
feedback:comment |
评论保存前 | (commentData, extra) |
验证/修改评论,设置 _rejected 可拒绝 |
feed:item |
RSS/Atom 生成 | (item, post) |
过滤 feed 条目 |
widget:sidebar |
侧边栏渲染 | (sidebarData, context) |
过滤侧边栏数据 |
plugin:config:beforeSave |
插件配置保存前 | (result, extra) |
校验或规范化插件配置,返回 { success, settings?, error? } |
mail:send |
邮件发送(适配器) | (result, { payload, ctx }) |
首个返回 sent: true 的 handler 胜出;ctx.reason 区分 password-reset / comment / comment-reply / test |
applyFilter 默认会传播插件异常。业务链路(保存内容、评论、登录、插件配置等)会因此中止并暴露错误。纯展示注入点可由系统使用 applyFilterSafely 包裹,单个插件失败时跳过该插件输出并继续渲染。
部分 call hook(如
feedback:finishComment、post/page:finishPublish、post/page:finishSave)在业务数据后追加extra对象:{ request, options, db, pluginCtx?, waitUntil?, siteUrl?, permalinkPattern?, pagePattern? },供插件读取配置、查询数据库、构造链接、调用其它 filter(如gravatar:url)或后台异步任务。旧插件忽略多余参数,完全向后兼容。
在 feedback:comment filter 中,设置 commentData._rejected 可拒绝评论:
addHook('feedback:comment', pluginId, async (commentData, extra) => {
if (spamDetected) {
commentData._rejected = '检测到垃圾评论'; // 非空字符串 = 拒绝,返回 403
}
return commentData;
});插件可通过 archive:header 和 archive:footer filter 自动向前端页面注入 HTML/JS,无需主题手动适配:
// index.ts
import type { PluginInitContext } from 'typecho/plugin-sdk';
import { loadPluginConfig } from 'typecho/plugin-sdk';
export default function init({ addHook, pluginId }: PluginInitContext): void {
// 注入 <head> 内容(如 SDK 脚本)
addHook('archive:header', pluginId, (headHtml: string, extra?: { options?: Record<string, unknown> }) => {
const config = loadPluginConfig(extra?.options, pluginId);
if (!config.sitekey) return headHtml;
return headHtml + '<script src="..."></script>';
});
// 注入 </body> 前内容(如交互脚本)
addHook('archive:footer', pluginId, (bodyHtml: string, extra?: { options?: Record<string, unknown> }) => {
const config = loadPluginConfig(extra?.options, pluginId);
if (!config.sitekey) return bodyHtml;
return bodyHtml + '<script>/* ... */</script>';
});
}Base.astro 布局会自动调用 getClientSnippets(options) 收集所有激活插件的注入内容,主题无需任何额外代码。
本仓库内的插件通过 typecho/plugin-sdk 导入公共 API。SDK 使用 barrel export 模式,集中 re-export 插件常用的类型和函数。
// 在 monorepo 内(通过 tsconfig paths + Vite alias 解析)
import { parsePluginOption, escapeAttr, fetchWithTimeout } from 'typecho/plugin-sdk';
import type { PluginInitContext } from 'typecho/plugin-sdk';
// 需要直接访问数据库 Schema 的插件
import type { Database } from 'typecho/db';
import { schema } from 'typecho/db';插件发布为独立 npm 包时,需在 package.json 中将 typecho 声明为 peerDependencies:
{
"name": "typecho-plugin-example",
"peerDependencies": {
"typecho": ">=0.1.0"
}
}安装时宿主项目会自动提供 typecho 包,typecho/plugin-sdk 通过 package.json 的 exports 字段解析。
| 类别 | 导出 |
|---|---|
| 类型 | PluginInitContext, PluginRouteResult, PluginManifest, PluginConfigField, AttachmentMeta, Database |
| 插件系统 | HookPoints, parsePluginOption, parsePluginConfigFormData, loadPluginConfig, escapeAttr, getClientIp |
| 认证 | hasPermission, verifyPassword |
| 内容 | buildPermalink, formatDate, buildAuthorLink, buildCategoryLink |
| Markdown/HTML | escapeHtml, renderMarkdown, renderMarkdownFiltered, renderContentExcerpt, generateExcerpt, autop, stripTypechoMarkers, stripHtmlTags |
| 网络 | fetchWithTimeout |
| 附件 | parseAttachmentMeta |
| URL | normalizeHttpUrl |
| 选项 | getOption, setOption |
- 将插件目录放在
src/plugins/下 - 在根
package.json的dependencies中添加"<packageName>": "workspace:*"(根workspaces已包含src/plugins/*) - 运行
bun install - 重新执行
bun run build
bun add typecho-plugin-example
bun run build独立发布的插件必须将
typecho声明为peerDependencies。宿主项目安装插件时,typecho包会自动提供 SDK 解析。
每个插件必须包含 index.test.ts,与 index.ts 同目录,使用 vitest。
import { afterEach, describe, expect, it, vi } from 'vitest';
import init from './index';
// 通过 mock PluginInitContext 收集注册的 hooks
function collectHooks() {
const hooks = new Map<string, Function>();
init({
pluginId: 'typecho-plugin-<name>',
HookPoints: {} as any,
addHook: (point: string, _pluginId: string, handler: Function) => {
hooks.set(point, handler);
},
});
return hooks;
}
// 构造插件运行时读取的 options 对象
function options(settings: Record<string, unknown>) {
return {
'plugin:typecho-plugin-<name>': JSON.stringify(settings),
// 可包含插件读取的站点级配置(如 siteUrl、secret)
};
}- Hook 注册 — 验证
hooks.keys()与预期的 hook 集合一致 - 守卫分支 — 未配置时跳过、已登录时跳过、pageContext 跳过
- 正常路径 — 有效输入下各项功能正常工作
- 拒绝路径 — 各守卫/检查正确拒绝
- 模式分发 — 若插件支持多种模式(如 spam/waiting/discard),覆盖每种模式
- 边界情况 — 零值、空字符串、缺失 token、API 故障
- 外部 API mock — 使用
vi.stubGlobal('fetch', mock),在afterEach中调用vi.unstubAllGlobals() - 配置验证 —
plugin:config:beforeSave接受合法配置,拒绝非法配置,忽略其他插件
index.test.ts— 与index.ts同目录
npx vitest run src/plugins/<插件名>/index.test.ts-
index.test.ts存在 - 所有注册的 hook 已验证
- 每个检查分支至少有 1 个测试
- 默认模式/spam 路径已覆盖
- discard/reject 路径已覆盖(如适用)
- 已登录用户跳过已测试
- 缺少配置时跳过已测试
- API 故障(mock)不会导致 handler 崩溃
-
pageContext守卫已测试(针对archive:header/archive:footerhook)
typecho-plugin-antispam/ 目录演示了:
package.json配置声明(在typecho.plugin中)feedback:commentfilter 钩子(反垃圾评论)- 读取插件配置
- 正确提取客户端 IP(优先 CF-Connecting-IP)