本插件为安知鱼(Anheyu)内容管理平台提供统一的 CDN 运维能力:将多家 CDN 的缓存清除、缓存预热、指标查询封装为一套 HTTP API,并在站点内容变更时自动清除缓存。
维护者:白夜
插件类型:
eventhook(订阅站点事件)+ 自带 HTTP 服务(供前端消费)插件版本:
1.5.3(端口9800)
1. 概述
站点若同时使用多家 CDN,日常运维(清缓存、预热、看流量/命中率)需要在各厂商控制台之间来回切换,且平台自带的清除能力覆盖有限。cdn-console 把这些操作收敛到一个控制台,并支持:
多实例分开配置:站点、图片等不同域名可分别归属不同 CDN 账号,各自保存凭证。
统一操作:一次提交可同时下发到多个已启用实例。
事件自动清除:文章创建/发布/更新/删除时,自动向启用自动清除的实例提交 URL/标签清除。
控制台不占主题路由:管理界面由插件自带的
console.js注入框架「自定义页面」,或开启serve_ui由插件自己托管一个独立页。附带两个运维动作:扫描主题静态资源生成预热清单、把内嵌官方前端的静态产物并入主题目录。
2. 功能特性
分类 | 能力 |
|---|---|
缓存清除 | URL / 目录 / 首页(整站) / 标签,按厂商支持度暴露 |
缓存预热 | 提交 URL 预热,可附带请求头(套餐不支持则不提供);服务端按厂商上限自动分批 |
指标查询 | 流量 / 带宽 / 请求数 / 命中率 / 状态码分布 / 平均响应,含时序曲线与区间汇总 |
事件自动清除 | 订阅 |
实例管理 | 多厂商、多实例增删改查,凭证脱敏存储、连通性测试 |
静态资源扫描 | 定位运行中的主题,按后缀扫描 |
后台静态同步 | 把内嵌官方前端的 |
公开流量摘要 | 免鉴权只读接口,供侧边栏展示固定站点的近 24 小时流量 |
安全防护 | 管理员鉴权、写/读/登录/公开四类限流、单次目标数量上限 |
性能 | 指标结果内存缓存 + 并发合并(single-flight),可选 Redis 共享二级缓存 |
3. 支持的 CDN 厂商与能力矩阵
当前支持五家厂商,能力存在差异,插件以"能力矩阵"驱动前端控件显隐。
3.1 能力矩阵
能力 | 腾讯云 EdgeOne | 腾讯云 CDN | 阿里云 CDN | 阿里云 ESA | Cloudflare |
|---|---|---|---|---|---|
URL 清除 | ✅ | ✅ | ✅ | ✅ | ✅ |
目录清除 | ✅ | ✅ | ✅ | ✅ | — |
首页/整站清除 | ✅ | — | — | ✅ | ✅ |
标签(Cache-Tag)清除 | ✅ | — | — | ✅ | — |
通配符清除 | — | — | — | — | — |
预热 | ✅ | ✅ | ✅ | ✅ | —(标准套餐无预热 API) |
流量 | ✅ | ✅ | ✅ | ✅ | ✅ |
带宽 | ✅ | ✅ | ✅ | ✅ | ✅ |
请求数 | ✅ | ✅ | ✅ | ✅ | ✅ |
命中率 | ✅ | ✅ | ✅ | — | — |
状态码分布 | ✅ | ✅ | ✅ | — | ✅ |
平均响应 | ✅ | — | — | — | — |
其中 EdgeOne 的命中率由 l7Flow_hit_outFlux / l7Flow_outFlux 逐点相除得出,状态码分布按 statusCode 过滤 l7Flow_request 后聚合;Cloudflare 的带宽由区间字节数换算,均非厂商直接提供的字段。
3.2 厂商标识与所需凭证
厂商 |
| 凭证字段 | 其它必填 | 是否需要 | 是否需要 |
|---|---|---|---|---|---|
腾讯云 EdgeOne |
|
|
| 是(ZoneId) | 否 |
腾讯云 CDN |
|
|
| 否 | 是 |
阿里云 CDN |
|
| — | 否 | 是(取首个域名) |
阿里云 ESA |
|
| — | 是(SiteId) | 否 |
Cloudflare |
|
| — | 是(Zone ID) | 否 |
凭证字段属敏感数据:落盘文件权限为
0600,接口回显时脱敏为********,再次提交占位值将保留原密钥。
4. 工作原理
4.1 进程模型
插件是 Anheyu 主程序以 go-plugin 拉起的独立子进程,与主程序拥有同等系统权限。main() 先启动自带 HTTP 服务(默认监听 :9800),再通过 SDK 注册 eventhook 能力。
绑定失败不会退出进程,而是按 1s→30s 指数退避重试,避免热重载时旧进程未释放端口导致插件起不来。
4.2 鉴权链路
所有业务 API 默认要求"管理员"身份,插件本身不持有 JWT 密钥:
PLAINTEXT
浏览器 ──(携带 Authorization: Bearer <JWT>)──► 插件 /api/*
│
插件把该 Bearer 转发到后端 admin_check_path
│
HTTP 200 且响应体 code==200 → 管理员,放行
HTTP 401 → 提示重新登录;其余 → 403
判定为管理员的结果按 admin_cache_ttl(默认 30s)缓存,避免一次页面加载反复回源后端;只缓存通过,降权或令牌失效最迟 30 秒内生效。
4.3 事件自动清除
订阅上述四类文章事件;命中后向所有 auto_purge=true 且启用的实例提交清除。有 site_url 则拼绝对 URL,否则提交相对路径。标签清除仅对支持该能力的厂商执行,标签为 purge_tag_prefix(默认 article-)加文章 ID,另附 article-detail、home-page、article-list 三个固定标签。
OnEvent 返回的错误仅被主程序记录日志,不影响文章发布等业务流程。
4.4 控制台形态
注入自定义页面:在框架「页面管理」新建页面,把
console.js注入正文。外观跟随主题配色。内置独立页:开启
serve_ui后,插件在 HTTP 服务根路径托管一个薄壳页,整页自绘外观。
两种形态共用同一份 console.js,接口地址由脚本自定位,或由配置项 console_entry 指定。
5. 环境要求
Anheyu(社区版或 PRO)开启插件系统;建议 App 版本 ≥
1.0.0(详见plugin.json的min_app_version)。插件与后端在同一容器 / Docker 内网可达(用于回源管理员校验接口)。
可选:一个插件进程网络可达的 Redis 服务(启用共享缓存时所需)。
静态资源扫描与后台静态同步依赖 Linux
/proc定位进程;非 Linux 环境需手动填写目录。
6. 安装
后台进入「插件管理」。
上传安装包
cdn-console.zip(内含plugin.json与各平台二进制)。安装成功后状态变为「运行中」。升级同 ID 插件会保留既有配置与禁用状态。
说明:插件走本地 zip 上传;这与 SSR 主题的
downloadUrl直装是两条不同的通道,请勿混淆。安装包内plugin.json必须位于压缩包根目录。
7. 快速开始
配置引导参数:至少填写
site_url(用于自动清除拼绝对 URL、扫描生成绝对 URL);backend_url/admin_check_path在同容器部署时可保持默认。新建 CDN 实例:选择厂商,填入凭证与
zone_id/domains,点击「测试连接」验证。接入控制台:按第 10 节任选一种方式打开面板。
在控制台执行清除/预热,或依赖事件自动清除。
8. 配置参数参考
以下参数在 plugin.json 的 config_schema 中声明,由后台保存后以环境变量注入插件进程。
参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| number |
| 插件 HTTP 服务端口。仅内网可达,禁止公网暴露。 |
| string | 空 | 监听地址。空=所有网卡;同机部署可填 |
| string |
| 用于管理员校验的后端基地址。 |
| string |
| 一个"仅管理员可访问"的后端接口路径,返回 200 视为放行。 |
| string | 空 | 站点访问地址,自动清除与扫描时拼接绝对 URL。 |
| string | 空 | 实例配置与任务历史落盘目录。留空则使用插件安装目录之外、与 |
| boolean |
| 是否启用事件自动清除。 |
| boolean |
| 是否由插件自带 HTTP 服务托管内置控制台页面。 |
| string | 空 | 面板入口地址,接口走该地址下的 |
| number |
| 每调用者每分钟清除/预热/扫描/同步写操作上限(0=不限)。 |
| number |
| 每调用者每分钟指标读取上限(0=不限)。 |
| number |
| 每来源 IP 每分钟登录代理上限(0=不限)。 |
| number |
| 指标结果内存缓存秒数(0=不缓存,仍合并并发)。 |
| number |
| 管理员"通过"结果缓存秒数(0=不缓存)。 |
| boolean |
| 是否启用 Redis 共享二级缓存。 |
| string | 空 | Redis 地址 |
| string | 空 | Redis AUTH 密码(表单为密码框,不回显)。 |
| number |
| Redis 库编号,建议使用独立库。 |
| number |
| Redis 中指标缓存秒数(0=沿用 |
| boolean |
| 是否开启免鉴权的公开流量摘要接口。关闭时返回 404。 |
| string | 空 | 公开摘要允许暴露的实例,逗号分隔(名称或 ID)。留空=全部已启用实例,最多聚合 12 个。 |
| number |
| 公开摘要每来源 IP 每分钟限流(0=不限)。 |
| number |
| 扫描时用于定位"当前生效主题"的端口。 |
| string |
| 额外探测端口。仅当启用 |
| string | 空 | 手动指定主题根目录,填写后跳过自动探测。 |
| string |
| 默认扫描后缀,逗号分隔。前端可临时覆盖。 |
| number |
| 单次扫描返回 URL 上限,超出会在结果中标记 |
| boolean |
| 自动推导扫描根时,是否默认一并扫描主题 |
| boolean |
| 是否启用后台静态同步(会向主题目录写文件)。 |
| number |
| 同步来源:内嵌官方前端进程端口。 |
| string | 空 | 手动指定同步目标的主题根,主题停止时用。 |
9. 使用指南
9.1 管理 CDN 实例
实例是操作的基本单位。字段含义:name(显示名)、provider(厂商)、enabled(是否参与"全部启用实例"的批量操作与自动清除)、zone_id、domains、auto_purge(是否参与事件自动清除)、purge_tag_prefix(Cache-Tag 前缀,默认 article-)及对应厂商的 credentials。
建议:为不同用途(如站点 HTML、图片资源)分别建立实例,便于按需选择清除目标。
9.2 清除缓存
选择清除类型并提交目标列表。清除类型取值:
| 含义 | 目标写法 |
|---|---|---|
| 按 URL 清除 | 每行一个资源 URL |
| 按目录清除 | 目录 URL(部分厂商为前缀匹配) |
| 首页/整站清除 | 无需目标 |
| 按 Cache-Tag 清除 | 标签值(可配合 |
| 通配符清除 | 当前各家均不支持,控件不显示 |
未指定实例时,默认下发到全部已启用实例;单次目标数量上限为 500。响应按实例逐条返回回执,部分失败不影响其余实例。
9.3 缓存预热
提交需要提前回源的 URL 列表,可为 Cloudflare 外的厂商附带请求头 headers。单次上限同为 500。
服务端会按每批 100 条向厂商提交;若某批被厂商以"单次条数超上限"拒绝(如腾讯云 LimitExceeded.BatchQuota),会对半拆分重试,自动收敛到当前套餐允许的上限。回执里的 已分 N 批提交厂商,单批≤M 条 可用于确认是否发生了拆分。
9.4 扫描静态资源
主题每次构建产出新的 /_next/static/<BUILD_ID>/...,这些文件带哈希、体量大,是预热的主要对象。点「扫描资源」即按后缀遍历当前生效主题的构建产物,把结果填入预热框,确认后再提交。
扫描根的确定顺序:请求显式传入的 roots → scan_theme_dir → 在 /proc 中查找监听 scan_theme_port 且注入了 PORT=<该值> 的 node 进程,取其工作目录。
主题目录里常堆积历次更新残留的构建目录,扫描只遍历 .next/BUILD_ID 指向的那一份,过期残留整体跳过;探测不到 BUILD_ID 时不过滤,保守全量保留。返回的 roots[].matched / exists 可用于确认目录是否找对。
9.5 后台静态同步
把内嵌官方前端的 .next/static 产物并入主题目录,替代在容器里执行 sync-admin-static.sh。
PRO 的后台与主题是两次独立构建,chunk 名对不上,而 Next standalone 不会在 /_next/static 上触发反向代理,只能把后台专属的静态文件物理补进主题目录,否则后台 /admin 会因缺 chunk 白屏。
该能力默认关闭,需开启 sync_enabled。来源与目标均由 /proc 探测或配置指定,请求体不接受路径。勾选「顺带清理过期构建目录」会删除目标 /_next/static 下非 chunks/media、且不属于任何活跃构建的一级目录;活跃集合为空时不清理。
同步只合并文件,不重启主题进程。若后台仍报缺 chunk,请手动重启容器,让主题重新索引。
9.6 查看指标
选择实例、指标与时间范围查询。指标取值:traffic、bandwidth、requests、hit_rate、status_code、avg_response(不支持的组合由能力矩阵挡掉)。粒度 granularity 取 minute/hour/day。结果含时序曲线 series 与区间汇总 summary。
9.7 事件自动清除
开启 auto_purge_enabled 且实例 auto_purge=true 时,相应文章事件会触发自动清除。若需关闭某实例的自动清除,取消该实例的 auto_purge;需全站停用时关掉总开关即可。
10. 前端接入
10.1 注入自定义页面
后台「页面管理」新建一个页面,在「自定义 JS」里粘贴下面这段纯 JS:
JAVASCRIPT
(function () {
if (window.__CDN_CONSOLE_INJECTED__) return;
window.__CDN_CONSOLE_INJECTED__ = true;
var host = document.querySelector("article") || document.body;
host.style.minHeight = "0";
var hd = host.previousElementSibling;
if (hd && hd.tagName === "HEADER") hd.style.display = "none";
var s = document.createElement("script");
s.id = "cdn-console-entry";
s.src = "https://你的域名/cdn/api/console.js";
host.appendChild(s);
})();
「自定义 JS」按 JS 源码执行,不能直接粘贴 <script> 标签。脚本带 id="cdn-console-entry",console.js 会据此把控制台挂进正文 <article>,与导航栏版心对齐。
页面地址与反代前缀需一致:若把插件反代到 /cdn/,上面写 /cdn/api/console.js;换挂载路径则同步改这里。
10.2 脚本加载器
/api/console.js 是一个固定名加载器,控制台代码本体在 /api/console.<hash>.js:
路径 | 内容 | 缓存 |
|---|---|---|
| 加载器,注释里注明当前指向 |
|
| 实现体,内容变则文件名变 | 一年 immutable |
因此粘贴地址长期不变,升级插件也无需清 CDN 缓存。想确认线上版本,curl https://你的域名/cdn/api/console.js | head -3 即可看到指向。
CDN 侧只需保证固定名加载器能回源;带哈希的实现体可以放心长缓存。
10.3 内置控制台(serve_ui)
将 serve_ui 置为 true 后,访问插件服务的根路径即可打开内置控制台。
控制台不内置登录框,直接复用主题登录态(localStorage 的 auth-storage),因此页面需与站点同源。若部署在独立域名,可在页面里先设 window.ANHEYU_SITE_ORIGIN = "https://你的站点域名"。未登录或令牌失效时会跳转 /login?redirect=<当前地址>。
控制台配色跟随站点主题:读 <html> 的 dark 类或 data-theme 属性,切换时实时生效;站点未声明暗色时退回系统 prefers-color-scheme。因此「站点深色 + 系统浅色」的组合也是深色面板。
10.4 侧边栏显示
在插件管理中启用 CDN 流量公开摘要,然后在公开站点列表填写你在控制台设置的站点名称,最后在后台的侧边栏设置中添加侧边栏,不用输入标题,内容如下:
JAVASCRIPT
<div id="cdn-widget" class="cdnw"><div class="cdnw-tip">加载中…</div></div>
<style>
.cdnw{display:flex;flex-direction:column;gap:1rem;font-size:.8125rem;line-height:1.6}
.cdnw-tip{color:var(--home-ink-muted,#8a8a8a);font-size:.8125rem}
/* 不再画外框:贴着侧边栏卡片渲染;多站点用顶部分隔线区分 */
.cdnw-site + .cdnw-site{padding-top:1rem;border-top:1px solid var(--home-line,rgba(120,120,120,.16))}
.cdnw-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem}
.cdnw-name{display:flex;align-items:center;gap:.4rem;font-weight:700;color:var(--home-ink,#222)}
.cdnw-name svg{width:16px;height:16px;color:var(--home-accent,#3b82f6)}
.cdnw-badge{font-size:.68rem;padding:.1rem .5rem;border-radius:999px;white-space:nowrap;
color:var(--home-accent,#3b82f6);border:1px solid var(--home-accent,#3b82f6)55}
.cdnw-sub{margin-top:.15rem;color:var(--home-ink-muted,#8a8a8a);font-size:.72rem}
.cdnw-spark{margin-top:.4rem;height:44px}
.cdnw-spark svg{width:100%;height:100%;display:block;color:var(--home-accent,#3b82f6)}
.cdnw-big{margin-top:.1rem;font-size:1.6rem;font-weight:800;letter-spacing:.5px;
color:var(--home-ink,#222);font-variant-numeric:tabular-nums}
.cdnw-grid{display:grid;grid-template-columns:1fr 1fr;gap:.5rem;margin-top:.55rem}
.cdnw-cell{padding:.4rem .55rem;border-radius:10px;background:var(--home-accent,#3b82f6)0a;
border:1px solid var(--home-line,rgba(120,120,120,.14))}
.cdnw-cell .k{font-size:.7rem;color:var(--home-ink-muted,#8a8a8a)}
.cdnw-cell .v{font-weight:700;color:var(--home-ink,#222);font-variant-numeric:tabular-nums}
</style>
<script>
(function () {
var root = document.getElementById("cdn-widget");
if (!root || root.dataset.init === "1") return; // 防重复执行
root.dataset.init = "1";
function fmtBytes(b){ if(b==null||isNaN(b))return "—"; if(b===0)return "0 B";
var u=["B","KB","MB","GB","TB","PB"],i=Math.min(u.length-1,Math.floor(Math.log(b)/Math.log(1024))),v=b/Math.pow(1024,i);
return v.toFixed(v>=100||i===0?0:2)+" "+u[i]; }
function fmtNum(n){ return n==null||isNaN(n)?"—":Math.round(n).toLocaleString("zh-CN"); }
function fmtMs(n){ return n==null||isNaN(n)?"—":Math.round(n)+" ms"; }
function fmtPct(n){ return n==null||isNaN(n)?null:n.toFixed(n>=10?0:1)+"%"; }
function spark(vals){ if(!vals||!vals.length)return "";
var W=100,H=30,min=Math.min.apply(null,vals),max=Math.max.apply(null,vals),span=(max-min)||1,n=vals.length;
return vals.map(function(v,i){var x=n===1?W/2:(i/(n-1))*W,y=H-((v-min)/span)*(H-2)-1;
return (i===0?"M":"L")+x.toFixed(2)+","+y.toFixed(2);}).join(" "); }
function esc(s){return String(s==null?"":s).replace(/[&<>"]/g,function(c){return{"&":"&","<":"<",">":">",'"':"""}[c];});}
function site(it){
var vals=(it.series||[]).map(function(p){return p.v;});
var hr=it.hit_rate!=null?fmtPct(it.hit_rate):null;
var path=spark(vals);
var svg='<svg viewBox="0 0 100 30" preserveAspectRatio="none">'+
(path?'<path d="'+path+' L100,30 L0,30 Z" fill="currentColor" fill-opacity="0.12" stroke="none"/>'+
'<path d="'+path+'" fill="none" stroke="currentColor" stroke-width="1.5" vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round"/>'
:'<line x1="0" y1="15" x2="100" y2="15" stroke="currentColor" stroke-opacity="0.15" stroke-width="0.5"/>')+'</svg>';
return '<div class="cdnw-site">'+
'<div class="cdnw-head"><div class="cdnw-name">'+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 17l5-5 4 4 8-8"/><path d="M16 8h4v4"/></svg>'+
esc(it.name)+'</div>'+
'<span class="cdnw-badge">'+(hr? "命中率 "+hr : "缓存")+'</span></div>'+
'<div class="cdnw-sub">近 24 小时 CDN 流量</div>'+
'<div class="cdnw-spark">'+svg+'</div>'+
'<div class="cdnw-big">'+fmtBytes(it.traffic_bytes)+'</div>'+
'<div class="cdnw-grid">'+
'<div class="cdnw-cell"><div class="k">请求数</div><div class="v">'+fmtNum(it.requests)+'</div></div>'+
'<div class="cdnw-cell"><div class="k">平均耗时</div><div class="v">'+fmtMs(it.avg_response_ms)+'</div></div>'+
'</div></div>';
}
fetch("/cdn/api/public/cdn-summary").then(function(r){
if(!r.ok){ root.innerHTML=""; return; } // 未开启(404)等:静默隐藏
return r.json().then(function(j){
if(j.code!==200){ root.innerHTML=""; return; }
var items=(j.data&&j.data.items)||[];
if(!items.length){ root.innerHTML='<div class="cdnw-tip">暂无公开的 CDN 站点</div>'; return; }
root.innerHTML = items.map(site).join("");
});
}).catch(function(){ root.innerHTML=""; });
})();
</script>
其中 fetch("/cdn/api/public/cdn-summary") 的 /cdn 即插件反代前缀,与第 10.1 节保持一致。接口未开启时静默隐藏,不会在侧边栏留下错误提示。
公开摘要只返回聚合数据,不含实例 ID、凭证与加速域名;按来源 IP 限流并复用指标缓存。
11. HTTP API 参考
直连插件时前缀为
/api/...。插件会自行裁掉/api/之前的挂载前缀,因此反代保留前缀时同样可用。
统一响应封装(业务成功以 code=200 判定):
JSON
{ "code": 200, "message": "ok", "data": { } }
接口一览:
方法 | 路径 | 鉴权 | 说明 |
|---|---|---|---|
GET |
| 否 | 健康检查 |
POST |
| 否 | 登录代理,透传后端 |
GET |
| 是 | 各厂商能力矩阵与表单字段元信息 |
GET |
| 是 | 实例列表(凭证脱敏) |
POST |
| 是 | 新增或更新实例(body 为 |
DELETE |
| 是 | 删除实例 |
POST |
| 是 | 测试该实例连通性 |
POST |
| 是 | 提交清除 |
POST |
| 是 | 提交预热 |
POST |
| 是 | 扫描主题静态资源 |
POST |
| 是 | 后台静态同步(需 |
GET |
| 是 | 返回 |
GET |
| 是 | 查询指标 |
GET |
| 是 | 清除/预热/同步历史( |
GET |
| 否 | 公开流量摘要(需 |
提交清除 POST /api/purge:
JSON
{
"instance_ids": ["inst-1", "inst-2"],
"type": "url",
"targets": ["https://anheyu.com/post/hello"]
}
instance_ids 为空时下发到全部已启用实例。响应 data.results 为逐实例回执 {provider, instance_id, task_id, accepted, message}。
查询指标 GET /api/metrics:
查询参数 | 必填 | 默认 | 说明 |
|---|---|---|---|
| 是 | — | 目标实例 |
| 否 |
| 指标类型,见 §9.6 |
| 否 |
|
|
| 否 | 24 小时前 | Unix 毫秒时间戳 |
| 否 | 当前 | Unix 毫秒时间戳 |
响应 data 结构:
JSON
{
"supported": true,
"summary": { "traffic_bytes": 123, "requests": 456, "hit_rate": 0.92 },
"series": [ { "timestamp": 1700000000, "value": 123 } ],
"status_codes": [ { "label": "2xx", "value": 100 } ],
"message": ""
}
summary 中缺失的字段表示该厂商不支持对应指标;supported=false 时 message 说明原因。
扫描资源 POST /api/scan-assets:
JSON
{
"extensions": [".js", ".css"],
"include_public": false,
"max_files": 0
}
返回 urls、roots(每个扫描根的 matched / exists)、detected_theme、total 与 truncated。
后台静态同步 POST /api/sync-admin-static:
JSON
{ "prune": true }
返回来源与目标目录、复制与跳过数量、被清理的构建目录名。定位失败时返回 code=200 并附带 error 与部分结果,便于对照路径排查。
12. 安全说明
谁能触发清除/预热:默认仅管理员。插件将调用者 Bearer 透传给
admin_check_path,返回 200 且业务码 200 才放行,否则 401/403。不要把插件端口(默认 9800)发布到公网。它应只在 Docker 内网/本机可达;外部访问统一经反向代理。同机部署可将
listen_host设为127.0.0.1进一步收敛。限流:写操作(
rate_limit_per_min)、指标读取(read_limit_per_min)、登录代理(login_limit_per_min)、公开摘要(public_limit_per_min)分别按调用者或来源 IP 限流,取X-Real-Ip→X-Forwarded-For首项 →RemoteAddr判定来源。目标上限:单次清除/预热目标数不超过 500,避免放大对厂商接口的调用。
文件写入面:扫描只读且拒绝非绝对路径;同步的来源与目标只来自探测或配置,请求体不接受路径。
13. 指标缓存与 Redis
指标查询按 (实例/指标/量化到 minute|hour|day 时间窗) 组织两级缓存。时间按粒度向下取整到桶边界,逻辑相同的区间共享同一条缓存。
L1 内存(默认启用):
metrics_cache_ttl秒内复用结果;并发相同请求合并为一次上游调用(防击穿);失败/不支持结果用更短 TTL(≤15s)负缓存。L2 Redis(
redis_enabled=true时启用):作为跨进程/多副本的共享缓存。链路为「内存未命中 → 查 Redis → 命中则回填 → 否则回源并写回 Redis」,且仅写"成功"结果。
Redis 连接要点:插件为独立进程,无法复用主程序内存中的 Redis 客户端对象,而是按
redis_addr自行拨号连接同一 Redis 服务,需确保插件进程网络可达该地址。Redis 客户端基于 Go 标准库实现(极简 RESP2:GET/SET ... EX/AUTH/SELECT/PING),不引入第三方依赖;Redis 不可达或出错时自动降级为纯内存缓存,不影响可用性。
14. 故障排查
现象 | 可能原因 | 处理 |
|---|---|---|
页面里看不到控制台 | 注入片段报语法错误 | 「自定义 JS」须粘纯 JS,不能粘 |
提示"需要管理员权限" | 非管理员登录态,或与站点不同源 | 以管理员账号登录;异构源时设 |
接口全部 404 | 片段里的前缀与实际反代不一致 | 统一改成实际挂载路径 |
升级后仍是旧版控制台 | 固定名加载器被缓存 | 让 |
测试连接报鉴权错误 | 密钥错误 / 缺 | 核对凭证;EdgeOne/ESA/Cloudflare 需 Zone/Site ID,腾讯/阿里 CDN 需已接入域名 |
指标一片空白 | 厂商不支持该指标(见能力矩阵)或字段口径/套餐差异 | 换受支持指标;确认为空时按"无数据/需核对"理解 |
扫描结果为 0 | 未填 | 补 |
扫描结果与主题无关 | 扫到了内嵌前端而非主题 | 确认 |
同步报"未能定位内嵌前端" | 3010 端口没跑内嵌前端或端口改过 | 对齐 |
同步完成但后台仍白屏 | 主题未重新索引 | 手动重启容器 |
启用了 Redis 却未共享 | 地址不可达 / 密码或库号错误 | 检查插件进程到 Redis 的网络;查看插件日志(输出于 stderr) |
清除提示单次目标过多 | 超过 500 | 分批提交 |
插件端口对外可达导致风险 | 误将 9800 公网映射 | 关闭公网发布,仅经反向代理访问 |
15. 常见问题
Q:不用主题也能管理吗? A:能。开启 serve_ui=true,直接访问插件服务根路径的内置控制台即可。
Q:控制台的登录框去哪了? A:没有内置登录框,直接复用主题登录态。因此控制台需与站点同源,独立域名下要设 window.ANHEYU_SITE_ORIGIN。
Q:插件能连你们后端已在用的 Redis 吗? A:能连同一个 Redis 服务,但需填 redis_addr(如 redis:6379)且保证插件进程可达;不能复用后端进程内的客户端对象。
Q:自动清除会影响发布流程吗? A:不会。事件钩子内的错误只记录日志,不阻断文章创建/发布。
Q:扫描出来的 URL 能直接预热吗? A:能,但先看 total 与 truncated。默认上限 2000 条,主题大的站点可能被截断,可调大 scan_max_files 或收窄后缀。
Q:同步会覆盖主题自己的文件吗? A:不会。静态产物按内容哈希命名,同名即同内容;合并前还会比体积与首尾 1KB,相同则跳过。
Q:某厂商指标口径与文档不符怎么办? A:各厂商 API 字段以官方文档为准,解析失败会降级为"该区间暂无可用指标数据",不影响清除/预热主功能。
16. 限制与已知事项
平均响应仅 EdgeOne 提供;Cloudflare 不支持命中率与预热;阿里云 ESA 暂不提供命中率/状态码/平均响应。
通配符清除当前五家均未开放,故不提供该控件。
指标数值以厂商返回为准,个别厂商为近似聚合值。
扫描与同步依赖 Linux
/proc定位进程,非 Linux 环境需手动指定目录。同步不重启主题进程;需要重新索引时请手动重启容器。
数据目录默认落在插件安装目录之外,可跨插件更新保留;若旧数据在安装目录内,首次启动会自动迁移。
17. 附录:取值枚举
provider:edgeone/tencent_cdn/aliyun/aliyun_esa/cloudflare清除
type:url/directory/homepage/tag(wildcard暂不支持)metric:traffic/bandwidth/requests/hit_rate/status_code/avg_responsegranularity:minute/hour/day任务
kind:purge/preheat/sync能力矩阵 JSON 键:
purge_url、purge_directory、purge_homepage、purge_wildcard、purge_tag、preheat、metrics_traffic、metrics_bandwidth、metrics_requests、metrics_hit_rate、metrics_status_code、metrics_avg_response


