# 插件可调用内核动作(R4-c)设计 > 用户决策(2026-10-11):**全开**(13 个内核动作全部可调)+ **设置页可配权限** + > **Go 侧 deadline 兜底**。 ## 1. 要解决的问题 用户原话:"现在这个接口不全,可以多给一些接口让 lua 调用内核。" 现状(`__llmsproxy_adapters`)是我手写的**三个闭包**(install/update/remove), 只能管适配器。内核还有 13 个公开动作(Reload / ResetHealth / AddSource / RemoveSource / SaveAutoRules / key 增删改 / 列表读取…),插件一个都调不到。 而"自动孵化 / 自愈 / 联动"这类插件能力(例如:探测到上游全挂 → 触发重探 + 清冷却;发现额度变化 → 重排 AUTO 链)恰恰需要它们。 ## 2. 关键认识:Go 调用可以超时,Lua 死循环不能 这决定了整个设计,也是用户提"阻塞 + Go 兜底"的原因: | 形态 | 能否中断 | 处置方式 | |---|---|---| | Lua 里 `while true do end` | **不能**(LuaJIT 限制,已实测三次) | 只能停用插件 | | Lua 里 `__llmsproxy_kernel.call("reload")` | **能** | Go 侧 `select` + deadline,超时把错误返回给 Lua | 因为 Lua 只是在等 Go 的返回值,**不占用 Lua 栈的执行**。所以给每次内核调用加 Go 侧 deadline 是可行的、干净的 —— 不需要"Lua 投递请求、Go 队列执行":那样 还要定义"超时后队列里积压的命令怎么办",而 deadline 直接返回 `(nil, err)`, 语义闭合。 ## 3. 依赖方向 `internal/lua` **不能** import `internal/core`(方向是 core → lua)。 按既有先例(`SetOutboundHosts`)注入一个**接口**,由 core 实现: ```go // internal/lua/kernel_api.go type KernelAction struct { Name string // "reload" / "reset_health" / ... Title string // 设置页展示用 Description string Danger string // ""=只读, "write"=改配置, "danger"=破坏性 Params []string // 参数名(设置页/文档用) } type KernelAPI interface { // Actions 列出可调用动作(设置页据此渲染权限开关) Actions() []KernelAction // Invoke 执行一个动作,args 为已解码参数。**必须**尊重 ctx 的 deadline。 Invoke(ctx context.Context, name string, args map[string]any) (any, error) } ``` core 侧实现 `Invoke` 时用**现有的 Core 方法**,本身不持 `p.mu`,因此不会与 插件锁成环(已核实:所有 `ps.mu` → `p.mu` 都是先释放 `ps.mu`)。 ## 4. 权限模型 复用 `visibilityOverrides` 的**同一套持久化机制**(`.access.json`), 但用独立的键空间:`kernel:` → 逗号分隔的动作名或 `*`。 - **默认拒绝**:插件不显式获得授权时,`call` 返回 `(nil, "not permitted")`。 理由:全开是"能力存在",不是"默认授予"。一个插件能删密钥这件事必须由 运维在设置页点过确认。 - **`danger` 类必须显式逐个授权**,不接受 `*`。这一条是防止运维图省事写 通配符,把"删密钥"和"读状态"等同起来。 - 插件可以在自己源码里**声明**它想要的动作(`plugin.kernel = {...}`), 设置页把它显示为"申请",运维批准后才生效。声明不等于授权。 ## 5. 动作清单(13 个 + 适配器 3 个) | 动作 | 危险级 | 对应 Core 方法 | 超时 | |---|---|---|---| | `status` | 只读 | 读 AutoSlotStates/Registry | 2s | | `list_sources` | 只读 | Sources() | 2s | | `list_models` | 只读 | Registry().ModelList() | 2s | | `list_adapters` | 只读 | ListAdapters() | 2s | | `adapter_owner` | 只读 | AdapterOwnerOf | 2s | | `reload` | write | Reload() | 30s | | `reset_health` | write | ResetHealth() | 5s | | `probe_all` | write | Registry().ProbeAll(ctx) | 60s | | `save_auto_rules` | write | SaveAutoRules() | 10s | | `save_key_auto` | write | SaveKeyAuto() | 10s | | `add_source` | write | AddSource() | 30s | | `remove_source` | danger | RemoveSource() | 30s | | `create_key` | write | CreateKey() | 10s | | `update_key` | write | UpdateKey() | 10s | | `delete_key` | danger | DeleteKey() | 10s | | `install_adapter` | write | UploadAdapter() | 10s | | `remove_adapter` | danger | RemoveAdapter() | 30s | | `set_plugin_enabled` | danger | SetEnabled() | 5s | **绝不开放**:`ListKeys()` / `FindKey()` —— 返回**明文密钥**。给插件读密钥等于 把网关的凭据面交给第三方 Lua。要用 key 相关动作就只用带掩码的形态。 ## 6. Lua 调用面 ```lua -- 探测:拿到可用动作与自己的授权情况 local acts = __llmsproxy_kernel.actions() -- 调用:统一入口,返回 (result, err) local ok, err = __llmsproxy_kernel.call("reset_health", {}) local st, err = __llmsproxy_kernel.call("status", {}) ``` 统一入口(而不是每个动作一个全局函数)的理由:动作集合将来会变,而 `__llmsproxy_kernel` 的签名不变 —— 否则每加一个动作就要动 `bootPluginState`,而那里正是历史上出过 cgo 崩溃的地方。 ## 7. 实施顺序 1. `internal/lua/kernel_api.go`:接口 + 动作元数据 + 权限检查 + deadline 包装 2. `internal/lua/plugin_kernel_bridge.go`:Lua 绑定(`actions` / `call`) 3. `internal/core/kernel_actions.go`:实现 `KernelAPI`(core 侧) 4. 权限持久化:复用 `.access.json`,新键空间 5. 设置页:插件行加"内核动作"列 + 授权编辑 6. 测试:Go 侧全动作、Lua 侧调用、权限拒绝、deadline 生效、危险动作需显式授权