仰望星辰工作室

feat(plugin): 新增插件兼容性自检提示

F fqh 提交于 2026-09-12 23:43 · 5be97f3 ·父提交 a37daa9
feat(plugin): 新增插件兼容性自检提示

- 后台插件列表展示 ⚠ 提示:使用了当前内核不支持的钩子类型,或 requires 版本不满足
- 版本比较按 x.y.z 逐段数值比较,避免字符串比较误判
- 文档补充「插件装了但没反应」的排查顺序(启用状态 / 兼容性提示 / data-admin-nicks)与 BOM 说明
3 个文件变更 +54 -4 fangqihang1717@163.com
•docs/PLUGIN.md +6 -1
•internal/plugin/plugin.go +47 -2
•web/templates/admin.html +1 -1
变更内容
diff --git a/docs/PLUGIN.md b/docs/PLUGIN.md
index 212158b..df94029 100644
--- a/docs/PLUGIN.md
+++ b/docs/PLUGIN.md
@@ -249,8 +249,13 @@ zip -r official-guard.zip plugin.json
 
 | 场景 | 建议 |
 |---|---|
+| **插件装了但完全没反应** | 按顺序检查:① 是否已在「插件管理」点**启用**(安装 ≠ 启用);② 后台插件列表里插件名下方是否出现 ⚠ 提示 —— 有提示说明正在运行的表白墙版本过旧,不认识插件的钩子类型,**升级二进制即可**;③ 浏览器打开 `/compose` 查看网页源码,若没有 `data-admin-nicks` 属性,同样说明服务端是旧版本 |
 | 改了 plugin.json 不生效 | 插件清单每次请求都会重新读取磁盘,但 `content_filter` 规则有 30 秒缓存 |
 | 配置改了没生效 | 配置存在 `settings` 表并带内存缓存,保存后立即刷新,无需重启 |
 | 钩子没触发 | 确认插件已在「插件管理」中**启用**(安装 ≠ 启用) |
 | 前台注入的 JS 报错 | 打开浏览器控制台查看;注入内容会在渲染时直接输出,注意转义引号 |
-| 想看当前插件与配置 | 后台「插件管理」页可查看全部插件、状态与配置表单 |
+| 保存 plugin.json 后插件不出现 | 确认文件为 **UTF-8 无 BOM**(带 BOM 会导致 JSON 解析失败而静默跳过),可用 `jq . plugin.json` 验证 |
+| 想看当前插件与配置 | 后台「插件管理」页可查看全部插件、状态、兼容性提示与配置表单 |
+
+> 兼容性原则:插件使用了当前内核不支持的钩子类型时,**该钩子会被跳过而不是报错**,
+> 因此请务必留意后台插件列表中的 ⚠ 兼容性提示。
diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go
index 92c0280..e0c24d7 100644
--- a/internal/plugin/plugin.go
+++ b/internal/plugin/plugin.go
@@ -18,6 +18,7 @@ import (
 	"os"
 	"path/filepath"
 	"regexp"
+	"strconv"
 	"strings"
 	"sync"
 	"time"
@@ -70,6 +71,50 @@ type Plugin struct {
 type Meta struct {
 	Plugin
 	Enabled bool
+	Warn    string // 兼容性提示:使用了当前版本不支持的钩子 / 版本要求不满足
+}
+
+// supportedTypes 当前内核支持的钩子类型
+var supportedTypes = map[string]bool{
+	"html": true, "filter": true, "http": true, "guard": true, "badge": true,
+}
+
+// checkCompat 返回插件的兼容性提示(空串表示没有问题)
+func (p *Plugin) checkCompat() string {
+	var bad []string
+	seen := map[string]bool{}
+	for _, h := range p.Hooks {
+		if h.Type == "" || supportedTypes[h.Type] || seen[h.Type] {
+			continue
+		}
+		seen[h.Type] = true
+		bad = append(bad, h.Type)
+	}
+	if len(bad) > 0 {
+		return "该插件使用了当前版本不支持的钩子类型:" + strings.Join(bad, "、") + ",请升级表白墙后再启用"
+	}
+	if p.Requires != "" && versionLess(config.Version, p.Requires) {
+		return "该插件要求表白墙版本 ≥ " + p.Requires + ",当前版本为 " + config.Version
+	}
+	return ""
+}
+
+// versionLess 按 x.y.z 逐段比较版本号(缺失段按 0 处理)
+func versionLess(a, b string) bool {
+	pa, pb := strings.Split(a, "."), strings.Split(b, ".")
+	for i := 0; i < 3; i++ {
+		x, y := 0, 0
+		if i < len(pa) {
+			x, _ = strconv.Atoi(strings.TrimSpace(pa[i]))
+		}
+		if i < len(pb) {
+			y, _ = strconv.Atoi(strings.TrimSpace(pb[i]))
+		}
+		if x != y {
+			return x < y
+		}
+	}
+	return false
 }
 
 // dir 插件目录(跟随数据目录配置,支持 CLEARLOVE_DATA_DIR 自定义)
@@ -120,12 +165,12 @@ func saveEnabled(set map[string]bool) {
 	_ = models.SetSetting("enabled_plugins", string(b))
 }
 
-// List 管理页用:全部插件及启用状态
+// List 管理页用:全部插件、启用状态与兼容性提示
 func List() []Meta {
 	enabled := enabledSet()
 	var out []Meta
 	for _, p := range Load() {
-		out = append(out, Meta{Plugin: *p, Enabled: enabled[p.Name]})
+		out = append(out, Meta{Plugin: *p, Enabled: enabled[p.Name], Warn: p.checkCompat()})
 	}
 	return out
 }
diff --git a/web/templates/admin.html b/web/templates/admin.html
index 384f3a9..ff7c214 100644
--- a/web/templates/admin.html
+++ b/web/templates/admin.html
@@ -503,7 +503,7 @@
       <tbody>
       {{range .plugins}}
       <tr>
-        <td><b>{{.Name}}</b></td>
+        <td><b>{{.Name}}</b>{{if .Warn}}<br><small class="warn">⚠ {{.Warn}}</small>{{end}}</td>
         <td>{{.Version}}</td>
         <td>{{.Author}}</td>
         <td class="cell-content">{{.Description}}</td>