feat(lua): 插件长期运行环境(R4-b)—— 内核驱动调度 + 独立 VM

用户需求:插件需要长期运行环境(独立 LuaVM 线程化),以支持自动签到、
token 轮换这类"没有浏览器、没有人操作时也要发生"的能力。

旧模型做不到:插件的周期工作只能靠前端 pluginAPI.call 驱动,页面没开就
一次都不跑。这正是自动签到的死穴。

新增 internal/lua/plugin_runtime.go:插件声明,内核驱动。

    PLUGIN.schedule = {
      { fn = "refresh_token", every = 3600 },
      { fn = "daily_checkin",  at = "03:30" },
      { fn = "poll_quota",     every = 300, jitter = 30 },
    }

- 每个声明了 schedule 的插件获得**私有 VM**(不是共享 plugin.state 的那个:
  钩子要累加,任务要长跑且网络阻塞,两者对隔离的要求相反)
- 内核单个 ticker 驱动所有任务;每个任务**独立 goroutine**,一个长任务
  不推迟另一个的 due
- `at="HH:MM"` 每日固定时刻;`jitter` 打散首触发,避免整点齐射
- 任务失败计数进入 hook_errors 面(静默坏任务必须可见)
- Unload 先停调度再关状态(顺序反了会让在跑的任务写进已关闭的 Lua 状态)

明确边界:**适配器不参与调度**。适配器仍是无状态请求级池;token 轮换靠
插件(这里)按周期调用 Phase 3 的 UpdateAdapter。

修掉两个把进程直接搞崩的缺陷:
1. 私有 VM 的池我给 sync.Cond 传了新 mutex 而非池自己的 mu →
   "unlock of unlocked mutex",fatal error 而非可恢复错误。
2. Transform 硬编码 adapterGlobal,而插件模式的池把表存在 pluginGlobal →
   健康的状态也报 "has no table"。加 poolGlobal(p) 按模式选择。

测试 8 个:无任何触发自动执行(核心)/真正执行了(计数读回)/无声明则零开销/
at 解析与畸形退化/不完整条目被忽略/失败隔离/卸载停 goroutine/6 插件×3 任务压力。
This commit is contained in:
JianFeeeee
2026-10-11 10:26:39 +08:00
parent 5f6939b590
commit 4d1407bbb4
4 changed files with 801 additions and 1 deletions

View File

@ -889,6 +889,17 @@ func (v *VM) RemoveAdapter(name string) {
}
}
// poolGlobal returns the Lua global holding a pool's returned table: plugins
// store theirs under one global, adapters under another. Transform and friends
// must read the SAME one boot() wrote, or a plugin-mode pool answers
// "has no table" even though the state is perfectly healthy.
func poolGlobal(p *adapterPool) string {
if p != nil && p.pluginMode {
return pluginGlobal
}
return adapterGlobal
}
func (v *VM) pool(name string) *adapterPool {
v.mu.RLock()
defer v.mu.RUnlock()
@ -966,7 +977,7 @@ func (v *VM) Transform(name, fn, raw string) (string, error) {
L.SetTop(0)
defer L.SetTop(0)
L.GetGlobal(adapterGlobal)
L.GetGlobal(poolGlobal(p))
if L.IsNil(-1) {
return "", fmt.Errorf("adapter %s has no table", name)
}