clearlove2.1
1// 应用型插件注册表。
2//
3// 所有扩展点(路由 / 后台页面与菜单 / Slot / 事件 / 过滤器 / 定时任务)
4// 统一登记在此,热路径通过不可变快照读取,避免每次请求加锁。
5package plugin
6
7import (
8 "context"
9 "sort"
10 "sync"
11 "sync/atomic"
12 "time"
13)
14
15// RouteReg 应用型插件注册的 HTTP 路由(path 为相对路径)
16type RouteReg struct {
17 Method string // GET / POST / ...
18 Path string // 相对 "/x/<slug>" 或 "/admin/plugins/<slug>" 的路径
19 Fn string // 脚本函数名
20 Auth string // none | user | admin
21 Perm string // 后台页面所需的内核权限组
22 JSON bool // JSON 接口(错误以 JSON 返回)
23}
24
25// MenuReg 后台菜单项
26type MenuReg struct {
27 Slug string // 所属插件 slug(重建快照时填充)
28 Label string
29 Path string
30 Perm string
31 Order int
32}
33
34// PageReg 后台页面
35type PageReg struct {
36 Path string
37 Fn string
38 Perm string
39}
40
41// FilterReg 过滤器注册项
42type FilterReg struct {
43 Fn string
44 Priority int
45}
46
47// MiddlewareReg HTTP 中间件注册项(phase: http.before / http.after)
48type MiddlewareReg struct {
49 Phase string
50 Fn string
51}
52
53// JobReg 定时任务
54type JobReg struct {
55 Name string
56 Every time.Duration
57 Fn string
58}
59
60// App 一个已加载的应用型插件实例
61type App struct {
62 Dir string // 目录名(生命周期操作的键)
63 Plugin *Plugin
64 Worker *Worker
65
66 Routes []RouteReg
67 Pages []PageReg
68 Menus []MenuReg
69 Slots map[string][]string
70 Events map[string][]string
71 Filters map[string][]FilterReg
72 Jobs []JobReg
73 Middlewares []MiddlewareReg
74}
75
76func newApp(dir string, p *Plugin, w *Worker) *App {
77 return &App{
78 Dir: dir, Plugin: p, Worker: w,
79 Slots: map[string][]string{}, Events: map[string][]string{},
80 Filters: map[string][]FilterReg{},
81 }
82}
83
84// ---------- 供热路径读取的不可变快照 ----------
85
86// SlotHook 一个 Slot 处理器
87type SlotHook struct {
88 App *App
89 Fn string
90}
91
92// EventHook 一个事件处理器
93type EventHook struct {
94 App *App
95 Fn string
96}
97
98// FilterHook 一个过滤器
99type FilterHook struct {
100 App *App
101 Fn string
102 Priority int
103}
104
105// MiddlewareHook 一个 HTTP 中间件
106type MiddlewareHook struct {
107 App *App
108 Fn string
109}
110
111// RouteHook 一条路由
112type RouteHook struct {
113 App *App
114 Route RouteReg
115}
116
117type snapshot struct {
118 apps map[string]*App // dir -> app
119 bySlug map[string]*App // slug -> app
120 slots map[string][]SlotHook
121 events map[string][]EventHook
122 filters map[string][]FilterHook
123 middlewares map[string][]MiddlewareHook
124 routes []RouteHook
125 menus []MenuReg
126 pages map[string]string // "<slug><path>" -> fn 名
127 empty bool
128}
129
130var (
131 regMu sync.Mutex
132 regApps = map[string]*App{} // dir -> app
133 regSnap atomic.Pointer[snapshot]
134 regInitOne sync.Once
135)
136
137func init() {
138 regInitOne.Do(func() { regSnap.Store(&snapshot{empty: true}) })
139}
140
141// RegisterApp 登记一个已 setup 完成的应用型插件
142func RegisterApp(a *App) {
143 regMu.Lock()
144 regApps[a.Dir] = a
145 regMu.Unlock()
146 rebuildSnapshot()
147}
148
149// UnregisterApp 移除应用型插件(禁用 / 卸载)
150func UnregisterApp(dir string) {
151 regMu.Lock()
152 delete(regApps, dir)
153 regMu.Unlock()
154 rebuildSnapshot()
155}
156
157// GetApp 按目录名取已加载的应用型插件
158func GetApp(dir string) *App {
159 regMu.Lock()
160 defer regMu.Unlock()
161 return regApps[dir]
162}
163
164// AppBySlug 按 slug 取已加载的应用型插件
165func AppBySlug(slug string) *App {
166 s := regSnap.Load()
167 if s == nil {
168 return nil
169 }
170 return s.bySlug[slug]
171}
172
173// CallFn 调用某个应用型插件的脚本函数(内核与测试使用,参数与返回值可 JSON 化)
174func CallFn(slug, fn string, args ...any) (any, error) {
175 a := AppBySlug(slug)
176 if a == nil || a.Worker == nil {
177 return nil, errNoApp
178 }
179 ctx, cancel := context.WithTimeout(context.Background(), a.Plugin.Timeout())
180 defer cancel()
181 return a.Worker.Do(ctx, func(rt *jsRuntime) (any, error) {
182 rt.cur = nil
183 return rt.callFn(fn, args...)
184 })
185}
186
187// LoadedApps 全部已加载的应用型插件
188func LoadedApps() []*App {
189 regMu.Lock()
190 defer regMu.Unlock()
191 out := make([]*App, 0, len(regApps))
192 for _, a := range regApps {
193 out = append(out, a)
194 }
195 return out
196}
197
198// rebuildSnapshot 重建不可变快照(注册变更时调用,非热路径)
199func rebuildSnapshot() {
200 regMu.Lock()
201 defer regMu.Unlock()
202 s := &snapshot{
203 apps: map[string]*App{},
204 bySlug: map[string]*App{},
205 slots: map[string][]SlotHook{},
206 events: map[string][]EventHook{},
207 filters: map[string][]FilterHook{},
208 middlewares: map[string][]MiddlewareHook{},
209 pages: map[string]string{},
210 }
211 for dir, a := range regApps {
212 s.apps[dir] = a
213 s.bySlug[a.Plugin.SlugOf()] = a
214 for slot, fns := range a.Slots {
215 for _, fn := range fns {
216 s.slots[slot] = append(s.slots[slot], SlotHook{App: a, Fn: fn})
217 }
218 }
219 for ev, fns := range a.Events {
220 for _, fn := range fns {
221 s.events[ev] = append(s.events[ev], EventHook{App: a, Fn: fn})
222 }
223 }
224 for fname, list := range a.Filters {
225 for _, f := range list {
226 s.filters[fname] = append(s.filters[fname], FilterHook{App: a, Fn: f.Fn, Priority: f.Priority})
227 }
228 }
229 for _, mw := range a.Middlewares {
230 s.middlewares[mw.Phase] = append(s.middlewares[mw.Phase], MiddlewareHook{App: a, Fn: mw.Fn})
231 }
232 for _, r := range a.Routes {
233 s.routes = append(s.routes, RouteHook{App: a, Route: r})
234 }
235 for _, m := range a.Menus {
236 m.Slug = a.Plugin.SlugOf()
237 s.menus = append(s.menus, m)
238 }
239 for _, p := range a.Pages {
240 s.pages[a.Plugin.SlugOf()+p.Path] = p.Fn
241 }
242 }
243 // 过滤器按优先级升序(小的先执行)
244 for k := range s.filters {
245 list := s.filters[k]
246 sort.SliceStable(list, func(i, j int) bool { return list[i].Priority < list[j].Priority })
247 s.filters[k] = list
248 }
249 sort.SliceStable(s.menus, func(i, j int) bool { return s.menus[i].Order < s.menus[j].Order })
250 s.empty = len(s.apps) == 0
251 regSnap.Store(s)
252}
253
254func currentSnapshot() *snapshot {
255 s := regSnap.Load()
256 if s == nil {
257 return &snapshot{empty: true}
258 }
259 return s
260}
261
262// HasApps 是否有任何应用型插件在运行(热路径零开销判断)
263func HasApps() bool { return !currentSnapshot().empty }
264
265// ---------- 注册表查询(内核侧使用) ----------
266
267// SlotHooks 取某 Slot 的全部处理器
268func SlotHooks(name string) []SlotHook { return currentSnapshot().slots[name] }
269
270// EventHooks 取某事件的全部处理器
271func EventHooks(event string) []EventHook { return currentSnapshot().events[event] }
272
273// FilterHooks 取某过滤器的全部处理器(已按优先级排序)
274func FilterHooks(name string) []FilterHook { return currentSnapshot().filters[name] }
275
276// AdminMenus 全部插件后台菜单
277func AdminMenus() []MenuReg { return currentSnapshot().menus }
278
279// MiddlewareHooks 取某阶段(http.before / http.after)的中间件
280func MiddlewareHooks(phase string) []MiddlewareHook { return currentSnapshot().middlewares[phase] }
281
282// FindRoute 匹配插件路由:返回 App、路由定义与剩余路径
283func FindRoute(prefix string, method, path string) (*App, *RouteReg, bool) {
284 s := currentSnapshot()
285 for i := range s.routes {
286 r := &s.routes[i]
287 if r.App.Plugin.SlugOf() != prefix {
288 continue
289 }
290 if !methodEqual(r.Route.Method, method) {
291 continue
292 }
293 if pathEqual(r.Route.Path, path) {
294 return r.App, &r.Route, true
295 }
296 }
297 return nil, nil, false
298}
299
300// AdminPageFn 查后台页面处理器
301func AdminPageFn(slug, path string) (fn string, ok bool) {
302 fn, ok = currentSnapshot().pages[slug+path]
303 return
304}
305
306func methodEqual(reg, got string) bool {
307 if reg == "" || reg == "*" {
308 return true
309 }
310 return equalFoldASCII(reg, got)
311}
312
313func equalFoldASCII(a, b string) bool {
314 if len(a) != len(b) {
315 return false
316 }
317 for i := 0; i < len(a); i++ {
318 ca, cb := a[i], b[i]
319 if 'a' <= ca && ca <= 'z' {
320 ca -= 32
321 }
322 if 'a' <= cb && cb <= 'z' {
323 cb -= 32
324 }
325 if ca != cb {
326 return false
327 }
328 }
329 return true
330}
331
332// pathEqual 路径匹配,支持 "/*" 后缀通配
333func pathEqual(reg, got string) bool {
334 if reg == got {
335 return true
336 }
337 if len(reg) > 2 && reg[len(reg)-2:] == "/*" {
338 return len(got) >= len(reg)-2 && got[:len(reg)-2] == reg[:len(reg)-2]
339 }
340 return false
341}