mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 04:07:25 +00:00
feat(lua)!: 插件只通过 modelrouter SDK 与内核交互,且不再随内核安装
- 新增唯一公开接口表 `modelrouter`(sdk_version=1.0):http / secrets / kernel / adapters / plugin_dir。成员与内部桥是**同一对象**而非副本 —— 复制会让凭据保险库分叉(插件写 A、内核读 B)。 - `llmsproxy` 与 `__llmsproxy_*` 降为同对象兼容别名,只为已装插件继续可用。 仓库内 4 个插件(channels / billing / quota / usage)全部改用 `modelrouter.*`:此前它们直接依赖内核私有全局,内核无法在不炸插件的前提下 改实现(重命名、加守卫、换实现都算破坏性变更)。 - 插件不再由网关安装:`SeedBundled()` 变彻底 no-op,`core.New` 不再调用; `ReadBundledPlugin` 改为按源文件定位(脱离 cwd),只服务测试与开发工具。 - 桌面端删除 `BUILTIN_PLUGINS` 与 `syncBundledPluginsOnUpgrade`:升级不再 删除、改名或覆盖用户插件文件。插件是运维资产,不是内核资源。 - `Builtin` 不再靠"源码与仓库示例逐字节相同"推断,插件一律视为外部文件。 - 判据:SDK 存在/版本/形状、别名与内部桥同一张表、适配器 VM 拿不到 SDK、 经 SDK 真实写读凭据并驱动内核动作、GUI 升级不动插件文件、release 包不含插件。 说明:channels.lua / quota.lua 同时带有此前的渠道与额度改动(含 OpenCode 免费池 监控),与 SDK 改名同处一个文件,无法文件级拆分,故一并纳入。
This commit is contained in:
@ -8,27 +8,82 @@ import (
|
||||
golua "github.com/aarzilli/golua/lua"
|
||||
)
|
||||
|
||||
// Package lua — Lua 绑定:凭据保险库 + 内核动作面
|
||||
// Package lua — the PLUGIN SDK surface.
|
||||
//
|
||||
// 这两个全局把"插件自己主导链接认证"所需的两块补齐:
|
||||
// A plugin is an external Lua file that talks to the gateway through exactly
|
||||
// one published table, `modelrouter`. Nothing else in this package is part of
|
||||
// the plugin contract: the underlying globals below are private implementation
|
||||
// and may be renamed or removed. They are still installed, as deprecated
|
||||
// aliases, so plugins written against the pre-SDK names keep working.
|
||||
//
|
||||
// __llmsproxy_secrets —— 安全存放 OAuth token / session / 设备指纹
|
||||
// __llmsproxy_kernel —— 触发内核动作(注册源、探测、清冷却、改链路…)
|
||||
// modelrouter.sdk_version — string, bumped when the SDK shape changes
|
||||
// modelrouter.http(spec) — allowlisted outbound HTTP
|
||||
// modelrouter.secrets — this plugin's credential vault
|
||||
// modelrouter.kernel — kernel actions (grant-gated)
|
||||
// modelrouter.adapters — install/update/remove this plugin's adapters
|
||||
// modelrouter.plugin_dir — this plugin's own directory
|
||||
//
|
||||
// 注册点与 __llmsproxy_http 相同:只在 PLUGIN 的 bootPluginState 里装,
|
||||
// 因为适配器共用 setupGlobals 而不该拿到这些能力。
|
||||
//
|
||||
// 两张表都用 buildJSONTable 的既有写法(NewTable + PushGoClosure + SetField),
|
||||
// 该写法在 json 表上长期稳定,也是内核自己的用法。
|
||||
// Why a single table instead of individual globals: the gateway must be able to
|
||||
// change how a capability is implemented (rename a global, add a guard, swap a
|
||||
// closure for a table) without breaking every installed plugin. A versioned
|
||||
// table is the seam that makes that possible; scattered private globals are not.
|
||||
|
||||
// secretGlobal 是凭据保险库的 Lua 名字。
|
||||
// SDKVersion is the published plugin SDK version. Bump it whenever a field is
|
||||
// removed or its calling convention changes. Additive fields do not bump it.
|
||||
const SDKVersion = "1.0"
|
||||
|
||||
// sdkGlobal is the ONLY public entry point for plugins. It carries the product
|
||||
// name, not the historical repository name: a plugin author writes
|
||||
// `modelrouter.http(...)`, and the name they learn is the one on the box.
|
||||
const sdkGlobal = "modelrouter"
|
||||
|
||||
// legacySDKGlobal is the pre-rename spelling, kept as an alias to the SAME table
|
||||
// so already-installed plugins keep working. It is not documented and no new
|
||||
// plugin should use it.
|
||||
const legacySDKGlobal = "llmsproxy"
|
||||
|
||||
// registerSDK publishes the SDK table. It must run AFTER every bridge is
|
||||
// installed, and it shares the already-registered values rather than re-creating
|
||||
// them, so a plugin cannot end up with two divergent credential vaults.
|
||||
func registerSDK(L *golua.State) {
|
||||
L.NewTable()
|
||||
L.PushString(SDKVersion)
|
||||
L.SetField(-2, "sdk_version")
|
||||
|
||||
members := []struct{ global, field string }{
|
||||
{httpGlobal, "http"},
|
||||
{secretGlobal, "secrets"},
|
||||
{kernelGlobal, "kernel"},
|
||||
{adapterBridgeGlobal, "adapters"},
|
||||
{pluginDirGlobal, "plugin_dir"},
|
||||
}
|
||||
for _, m := range members {
|
||||
L.GetGlobal(m.global)
|
||||
if L.IsNil(-1) {
|
||||
// Capability not granted for this state (e.g. deny-all outbound
|
||||
// HTTP). Leave the field absent instead of publishing nil, so a
|
||||
// plugin sees a missing member rather than a callable that fails
|
||||
// in an unexpected way.
|
||||
L.Pop(1)
|
||||
continue
|
||||
}
|
||||
L.SetField(-2, m.field)
|
||||
}
|
||||
L.SetGlobal(sdkGlobal)
|
||||
// Same table object under the historical name: a plugin that mixes
|
||||
// `modelrouter.secrets` and `llmsproxy.secrets` still hits one vault.
|
||||
L.GetGlobal(sdkGlobal)
|
||||
L.SetGlobal(legacySDKGlobal)
|
||||
}
|
||||
|
||||
// secretGlobal is the deprecated alias for llmsproxy.secrets.
|
||||
const secretGlobal = "__llmsproxy_secrets"
|
||||
|
||||
// pluginDirGlobal carries the plugin's OWN directory as ground truth.
|
||||
//
|
||||
// Why it must come from the kernel (live defect): the channels plugin decides
|
||||
// where to keep its state by reading /etc/llmsproxy/config.yaml when
|
||||
// LLMSPROXY_PLUGIN_DIR is unset. Tests load the real plugin but never set that
|
||||
// where to keep its state by reading /etc/modelrouter/config.yaml when
|
||||
// MODELROUTER_PLUGIN_DIR is unset. Tests load the real plugin but never set that
|
||||
// variable, so they read AND WROTE the production store — one full
|
||||
// `go test ./internal/lua` replaced the production account pool with the test's
|
||||
// empty one (13 accounts → 0; the credentials stayed in the vault, so the
|
||||
|
||||
Reference in New Issue
Block a user