mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +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:
@ -215,9 +215,9 @@ type Plugin struct {
|
||||
exports []string
|
||||
// script is the plugin's source, kept so a reload can rebuild its state.
|
||||
script string
|
||||
// Builtin marks a plugin that shipped with the gateway. The UI shows it as
|
||||
// such so an operator can tell "example I can read" from "mine", and a
|
||||
// delete of a builtin is allowed but re-seeds on a fresh plugin dir.
|
||||
// Builtin is retained in the API for compatibility. Business plugins are
|
||||
// operator-owned files; the gateway never classifies, replaces, or seeds
|
||||
// them based on their contents.
|
||||
Builtin bool
|
||||
|
||||
// state is the plugin's SINGLE authoritative Lua state, guarded by mu.
|
||||
@ -746,29 +746,9 @@ func (ps *Plugins) LoadDir() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeedBundled writes the plugins shipped with the gateway into dir when the
|
||||
// directory does not exist yet, mirroring the adapter seeding rule: once the
|
||||
// directory exists it is authoritative, so deleting or editing a shipped plugin
|
||||
// is a real action that survives restarts.
|
||||
// SeedBundled writes the plugins shipped with the gateway into dir when they are
|
||||
// MISSING.
|
||||
//
|
||||
// This used to be "write everything only when the directory does not exist",
|
||||
// which silently stopped delivering plugins added to the binary LATER: the
|
||||
// directory had existed since the first install, so a newly embedded plugin
|
||||
// (e.g. channels.lua) never landed on an upgraded deployment — it was in the
|
||||
// binary and completely invisible in /api/plugins. The directory's mere
|
||||
// existence is not evidence that its contents are current.
|
||||
//
|
||||
// The rule is now per-file: seed a bundled plugin only when a file of that name
|
||||
// is absent. An OPERATOR'S file always wins, so customising or replacing a
|
||||
// bundled plugin survives an upgrade — the same rule adapters already use.
|
||||
func (ps *Plugins) SeedBundled() error {
|
||||
if ps.dir == "" {
|
||||
return nil
|
||||
}
|
||||
return writeBundledPlugins(ps.dir)
|
||||
}
|
||||
// Plugins are optional scripts managed independently in plugin_dir. This method
|
||||
// is retained for API compatibility and deliberately performs no seeding.
|
||||
func (ps *Plugins) SeedBundled() error { return nil }
|
||||
|
||||
// bootPluginState compiles one plugin into a fresh Lua state and stores its
|
||||
// returned table under pluginGlobal. It is the plugin counterpart of
|
||||
@ -797,6 +777,9 @@ func bootPluginState(code, name string, allow *httpAllowlist, ps *Plugins) (*wor
|
||||
registerKernelBridge(L, ps, name)
|
||||
// The plugin's own directory, as ground truth (see pluginDirGlobal).
|
||||
registerPluginDirBridge(L, ps.dir)
|
||||
// Publish the public SDK table. Must be LAST: it shares the bridges
|
||||
// above rather than duplicating them.
|
||||
registerSDK(L)
|
||||
}
|
||||
if err := L.DoString(code); err != nil {
|
||||
L.Close()
|
||||
@ -861,13 +844,9 @@ func (ps *Plugins) LoadSource(name, code string) error {
|
||||
}
|
||||
p.script = code
|
||||
p.state = w
|
||||
// Builtin = the shipped source is byte-identical to what this file was
|
||||
// loaded from. That is stronger than "the name matches a bundled plugin": an
|
||||
// operator who EDITED billing.lua must not be told their copy is builtin,
|
||||
// or the UI would offer to overwrite their changes.
|
||||
if orig, err := ReadBundledPlugin(name); err == nil && orig == code {
|
||||
p.Builtin = true
|
||||
}
|
||||
// Plugins are external operator-owned files. Do not compare their source
|
||||
// with repository examples: doing so couples runtime behavior to the source
|
||||
// tree and falsely labels a user file as gateway-owned.
|
||||
|
||||
// ---- manifest ----
|
||||
w.L.GetGlobal(pluginGlobal)
|
||||
@ -1229,8 +1208,11 @@ type DiskEntry struct {
|
||||
Size int64 `json:"size"`
|
||||
Loaded bool `json:"loaded"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Builtin bool `json:"builtin"`
|
||||
Error string `json:"error,omitempty"`
|
||||
// Builtin is retained in the JSON shape for compatibility with older
|
||||
// clients; it is always false because plugins are external operator-owned
|
||||
// files, never gateway-owned resources.
|
||||
Builtin bool `json:"builtin"`
|
||||
Error string `json:"error,omitempty"`
|
||||
// Version/Description are read from the loaded plugin when available.
|
||||
Version string `json:"version,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
@ -1299,25 +1281,15 @@ func (ps *Plugins) OnDisk() []DiskEntry {
|
||||
// field covers either.
|
||||
de.Pages = pageNames(p)
|
||||
} else {
|
||||
// On disk but not in the running set: either it failed so badly
|
||||
// that LoadSource never produced a record, or the dir was written
|
||||
// after startup. Mark it by comparing with the bundled source.
|
||||
if code, err := os.ReadFile(filepath.Join(ps.dir, e.Name())); err == nil {
|
||||
if orig, err := ReadBundledPlugin(name); err == nil && orig == string(code) {
|
||||
de.Builtin = true
|
||||
}
|
||||
}
|
||||
// The file may have been added after startup. It remains an external
|
||||
// operator-owned plugin; never inspect repository source to classify it.
|
||||
}
|
||||
ps.mu.RUnlock()
|
||||
out = append(out, de)
|
||||
}
|
||||
// External plugins are listed alphabetically; no file is privileged by the
|
||||
// gateway based on where its source happens to exist.
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
// builtins first, then alphabetical: the example plugin an operator
|
||||
// is most likely to want to read should not be buried under whatever
|
||||
// they installed most recently.
|
||||
if out[i].Builtin != out[j].Builtin {
|
||||
return out[i].Builtin
|
||||
}
|
||||
return out[i].Name < out[j].Name
|
||||
})
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user