mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
用户指出「接口不全」——此前插件只有 __llmsproxy_adapters 三个手写闭包,
够不到内核。对照参考插件(dsh-our-free-model,13 个账号渠道)的真实能力,
缺的正是这两块:
插件自己驱动链接认证 → 存凭据 → 定时领额度 → 把源接进内核
## R4-c 内核动作面:__llmsproxy_kernel
18 个动作,全部映射到既有 Core 方法(不是新造能力):
只读 6:status / list_sources / list_models / list_adapters /
list_auto_rules / adapter_owner
写 8:reload / reset_health / probe_all / save_auto_rules /
save_key_auto / add_source / create_key / update_key
破坏性 4:remove_source / delete_key / remove_adapter / set_plugin_enabled
**add_source 是「插件注册自己的上游」的落点**——插件拿到凭据后可以自己把
端点接进网关,无需运维改 config.yaml。已端到端验证:插件调它 → 内核源列表
出现 → 模型进入可路由列表。
依赖方向:lua 不能 import core,所以接口定义在 lua、实现放 core
(kernel_actions.go),走 SetKernelAPI 注入——与 SetOutboundHosts 同一先例。
**凭据边界**:动作面**有意排除** list_keys/find_key。Core.ListKeys() 返回
明文网关密钥,交给第三方 Lua 等于交出整个部署的凭据面。有回归测试锁死
这条(TestKernelActionSurfaceExcludesPlaintextKeys)。list_sources 返回的
api_key 也强制掩码(只给 ***+后4位)。
**权限**(用户选「全开+设置页可配」):18 个动作全部可达,但**默认拒绝**,
授权落盘 .kernel-perms.json(独立文件,不并进 .access.json——后者是
map[string]string,塞进去会破坏旧二进制能读的格式)。danger 级不接受通配符。
插件可在源码 plugin.kernel={...} **申请**,设置页显示申请 vs 已授予。
**Go 侧 deadline**(用户选定):每个动作有超时预算,超时把 (nil,err) 还给
Lua。这可行的原因:Lua 只是等返回值,不占 Lua 栈执行权——与「Lua 死循环
不可中断」是两件事。
## R4-d 凭据保险库:__llmsproxy_secrets
此前插件存 token 只能写 plugin.state:0755 目录、明文、与计费累计量同文件、
会被 PUT /state 整体替换、会进 WebUI。现在独立文件:
<plugin_dir>/.secrets/<plugin>.json 文件 0600,目录 0700
临时文件**先 chmod 再 rename**,全程不存在宽权限窗口。读取身份在插件 boot
时绑定(不从 Lua 状态现读)⇒ 跨插件读不可行是机制而非约定。插件删除时清空
(DeleteAllSecrets + 启动 PurgeOrphanVaults)。
## 开发中被测试抓出的两个真实缺陷(我自己引入的)
写了 TestNoKernelActionDeadlocksFromLua 对**每个**动作断言「在插件 Lua 调用
(此时持有 p.mu)内不得死锁」,抓出:
1. **自死锁**:set_plugin_enabled → SetEnabled → p.mu.Lock(),而死锁发生在
调用者已持有的同一把锁上。
2. **更隐蔽的迟到副作用**:超时把 error 返回给插件后,动作仍在幕后执行完成——
插件以为失败,实际已生效。
修法:标注 Deferred 的动作在本次调用返回后异步执行,如实返回 accepted:true
而不是谎报成功。18 个动作全部复验无死锁,另有单独测试确认副作用真的落地
(只测「不死锁」的话,一个全空转的 dispatch 也能通过)。
测试 11 个(core 8 + lua 3):动作可用/密钥掩码/默认拒绝/授权计数/
插件注册源并变为可路由/授权落盘重启保留/未知动作名被拒/凭据面排除明文/
全动作无死锁/破坏性动作生效/Lua 侧桥接可用。
118 lines
5.4 KiB
Markdown
118 lines
5.4 KiB
Markdown
# 插件可调用内核动作(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:<plugin>` → 逗号分隔的动作名或 `*`。
|
||
|
||
- **默认拒绝**:插件不显式获得授权时,`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 生效、危险动作需显式授权
|