clearlove2.1
1// goja 引擎实现:脚本加载、函数调用、Host API 装配。
2package plugin
3
4import (
5 "encoding/json"
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "strconv"
11 "strings"
12
13 "github.com/dop251/goja"
14
15 "clearlove/internal/config"
16 "clearlove/internal/models"
17 "clearlove/internal/util"
18)
19
20// jsRuntime 单个插件的脚本运行时(只允许通过 Worker 访问)
21type jsRuntime struct {
22 vm *goja.Runtime
23 dir string
24 plugin *Plugin
25 app *App // 注册阶段回填,Host API 用它做权限校验
26
27 cur *reqContext // 当前请求上下文(worker 串行,同一时刻仅一个)
28 fnSeq int // 内联函数自增序号,用于生成 __slot_1 之类的内部函数名
29}
30
31// newJSRuntime 创建运行时并装配 Host API
32func newJSRuntime(dir string, p *Plugin) (*jsRuntime, error) {
33 vm := goja.New()
34 vm.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
35 rt := &jsRuntime{vm: vm, dir: dir, plugin: p}
36 if err := rt.installAPI(); err != nil {
37 return nil, err
38 }
39 return rt, nil
40}
41
42// Stop 中断当前执行(超时保护)
43func (rt *jsRuntime) Stop() {
44 if rt == nil || rt.vm == nil {
45 return
46 }
47 defer func() { _ = recover() }()
48 rt.vm.Interrupt("timeout")
49}
50
51// loadEntry 读取并执行入口脚本(顶层代码只做定义,不应有副作用)
52func (rt *jsRuntime) loadEntry() error {
53 entry := rt.plugin.entryPath()
54 if entry == "" {
55 return errors.New("runtime.entry 缺失或非法(不允许绝对路径与 .. 穿越)")
56 }
57 full := filepath.Join(rt.dir, filepath.FromSlash(entry))
58 data, err := os.ReadFile(full)
59 if err != nil {
60 return fmt.Errorf("读取入口脚本失败: %w", err)
61 }
62 if _, err := rt.vm.RunScript(entry, string(data)); err != nil {
63 return fmt.Errorf("执行入口脚本失败: %w", err)
64 }
65 return nil
66}
67
68// hasFn 脚本是否定义了某函数
69func (rt *jsRuntime) hasFn(name string) bool {
70 _, ok := goja.AssertFunction(rt.vm.Get(name))
71 return ok
72}
73
74// callFn 调用脚本函数,参数与返回值均为可 JSON 化的 Go 值
75func (rt *jsRuntime) callFn(fn string, args ...any) (any, error) {
76 f, ok := goja.AssertFunction(rt.vm.Get(fn))
77 if !ok {
78 return nil, fmt.Errorf("插件未定义函数 %s", fn)
79 }
80 vals := make([]goja.Value, 0, len(args))
81 for _, a := range args {
82 vals = append(vals, rt.vm.ToValue(a))
83 }
84 v, err := f(goja.Undefined(), vals...)
85 if err != nil {
86 return nil, err
87 }
88 return exportJS(v), nil
89}
90
91// ---------- 值转换 ----------
92
93// exportJS goja 值 -> 纯 Go 值
94func exportJS(v goja.Value) any {
95 if v == nil || goja.IsUndefined(v) || goja.IsNull(v) {
96 return nil
97 }
98 return v.Export()
99}
100
101// argsOf 导出全部实参
102func argsOf(call goja.FunctionCall) []any {
103 out := make([]any, 0, len(call.Arguments))
104 for _, a := range call.Arguments {
105 out = append(out, exportJS(a))
106 }
107 return out
108}
109
110// objOf 导出对象参数字典
111func objOf(v goja.Value) map[string]any {
112 if v == nil || goja.IsUndefined(v) || goja.IsNull(v) {
113 return nil
114 }
115 if m, ok := v.Export().(map[string]any); ok {
116 return m
117 }
118 return nil
119}
120
121func strOf(v any) string {
122 switch t := v.(type) {
123 case nil:
124 return ""
125 case string:
126 return t
127 case int64:
128 return strconv.FormatInt(t, 10)
129 case float64:
130 return strconv.FormatFloat(t, 'f', -1, 64)
131 case bool:
132 if t {
133 return "1"
134 }
135 return "0"
136 default:
137 return fmt.Sprint(t)
138 }
139}
140
141func intOf(v any) int {
142 switch t := v.(type) {
143 case int64:
144 return int(t)
145 case int:
146 return t
147 case float64:
148 return int(t)
149 case string:
150 n, _ := strconv.Atoi(strings.TrimSpace(t))
151 return n
152 default:
153 return 0
154 }
155}
156
157func boolOf(v any) bool {
158 switch t := v.(type) {
159 case bool:
160 return t
161 case string:
162 return t == "1" || strings.EqualFold(t, "true")
163 case int64:
164 return t != 0
165 case float64:
166 return t != 0
167 default:
168 return false
169 }
170}
171
172// jsFn 包装 Go 函数为 goja 函数:错误以 JS 异常抛出(插件可 try/catch)
173func (rt *jsRuntime) jsFn(fn func(call goja.FunctionCall) (any, error)) func(goja.FunctionCall) goja.Value {
174 return func(call goja.FunctionCall) goja.Value {
175 v, err := fn(call)
176 if err != nil {
177 panic(rt.vm.NewGoError(err))
178 }
179 if v == nil {
180 return goja.Undefined()
181 }
182 return rt.vm.ToValue(v)
183 }
184}
185
186// ---------- Host API 装配 ----------
187
188func (rt *jsRuntime) installAPI() error {
189 clv := rt.vm.NewObject()
190 if err := rt.installCore(clv); err != nil {
191 return err
192 }
193 if err := rt.installDB(clv); err != nil {
194 return err
195 }
196 if err := rt.installSite(clv); err != nil {
197 return err
198 }
199 if err := rt.installView(clv); err != nil {
200 return err
201 }
202 if err := rt.installNet(clv); err != nil {
203 return err
204 }
205 rt.vm.Set("clv", clv)
206 return nil
207}
208
209// installCore clv 的基础部分:元信息、日志、JSON、配置
210func (rt *jsRuntime) installCore(clv *goja.Object) error {
211 p := rt.plugin
212
213 // clv.version
214 if err := clv.Set("version", AppVersion); err != nil {
215 return err
216 }
217
218 // clv.plugin.{name,slug,dir,version,config,configs}
219 pl := rt.vm.NewObject()
220 _ = pl.Set("name", p.Name)
221 _ = pl.Set("slug", p.SlugOf())
222 _ = pl.Set("dir", rt.dir)
223 _ = pl.Set("version", p.Version)
224 _ = pl.Set("config", rt.jsFn(func(call goja.FunctionCall) (any, error) {
225 args := argsOf(call)
226 if len(args) == 0 {
227 return nil, errors.New("clv.plugin.config(key) 需要一个参数")
228 }
229 return pluginConfigValue(p, strOf(args[0])), nil
230 }))
231 _ = pl.Set("configs", rt.jsFn(func(call goja.FunctionCall) (any, error) {
232 cfg := GetConfig(p)
233 out := make(map[string]any, len(cfg))
234 for k, v := range cfg {
235 out[k] = v
236 }
237 return out, nil
238 }))
239 _ = clv.Set("plugin", pl)
240
241 // clv.log.{info,warn,error}
242 logObj := rt.vm.NewObject()
243 for _, lv := range []string{"info", "warn", "error"} {
244 level := lv
245 _ = logObj.Set(level, rt.jsFn(func(call goja.FunctionCall) (any, error) {
246 args := argsOf(call)
247 parts := make([]string, 0, len(args))
248 for _, a := range args {
249 parts = append(parts, strOf(a))
250 }
251 msg := strings.Join(parts, " ")
252 util.Log(level, "[插件 %s] %s", p.SlugOf(), msg)
253 logPlugin(p.SlugOf(), "log."+level, msg, 0)
254 return nil, nil
255 }))
256 }
257 _ = clv.Set("log", logObj)
258
259 // clv.json.{encode,decode}
260 jsonObj := rt.vm.NewObject()
261 _ = jsonObj.Set("encode", rt.jsFn(func(call goja.FunctionCall) (any, error) {
262 args := argsOf(call)
263 if len(args) == 0 {
264 return "", nil
265 }
266 return jsonEncode(args[0])
267 }))
268 _ = jsonObj.Set("decode", rt.jsFn(func(call goja.FunctionCall) (any, error) {
269 args := argsOf(call)
270 if len(args) == 0 {
271 return nil, nil
272 }
273 return jsonDecode(strOf(args[0]))
274 }))
275 _ = clv.Set("json", jsonObj)
276
277 return nil
278}
279
280// pluginConfigValue 读取插件配置(未设置时回落到清单里的 default)
281func pluginConfigValue(p *Plugin, key string) string {
282 if key == "" {
283 return ""
284 }
285 if v := models.GetSetting(configKey(p.Name, key)); v != "" {
286 return v
287 }
288 for _, f := range p.Config {
289 if f.Key == key {
290 return f.Default
291 }
292 }
293 return ""
294}
295
296// ---------- 小工具 ----------
297
298// AppVersion 内核版本(config 包不依赖 plugin,直接引用是安全的)
299var AppVersion = config.Version
300
301func jsonEncode(v any) (string, error) {
302 b, err := json.Marshal(v)
303 if err != nil {
304 return "", err
305 }
306 return string(b), nil
307}
308
309func jsonDecode(s string) (any, error) {
310 var v any
311 if err := json.Unmarshal([]byte(s), &v); err != nil {
312 return nil, err
313 }
314 return v, nil
315}