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:
@ -1869,20 +1869,16 @@ func TestAnthropicUsageCacheMapping(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSeedBundledFillsMissingFilesInExistingDir 是真实事故的回归测试。
|
||||
// TestSeedBundledNeverWritesFiles 钉住"插件不由网关安装"这条契约。
|
||||
//
|
||||
// 事故:SeedBundled 原本"目录已存在就什么都不做"。生产 plugin_dir 从首次安装
|
||||
// 起就存在,于是后来加入二进制的新内置插件(channels.lua)永远不会落盘 ——
|
||||
// 它在 go:embed 里、却完全不出现在 /api/plugins(线上只加载 billing/quota/usage)。
|
||||
//
|
||||
// 本测试构造那个确切条件:目录已存在且只装了部分内置插件,断言缺失的被补齐、
|
||||
// 已存在的(模拟运维自定义)**不被覆盖**。
|
||||
func TestSeedBundledFillsMissingFilesInExistingDir(t *testing.T) {
|
||||
// 历史:SeedBundled 曾把 go:embed 的 billing/channels/quota/usage 写进
|
||||
// plugin_dir,于是插件与 Go 二进制耦合、升级还会覆盖或删除运维文件。
|
||||
// 现在插件是纯外置资产:网关不创建、不补齐、不覆盖,只加载目录里已有的文件。
|
||||
func TestSeedBundledNeverWritesFiles(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "plugins")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 模拟:目录已存在,里面只有运维自己的一份 billing.lua(内容故意不同)
|
||||
const customBilling = "-- operator's own billing plugin\nreturn { name='billing', version='9.9.9' }"
|
||||
if err := os.WriteFile(filepath.Join(dir, "billing.lua"), []byte(customBilling), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
@ -1896,33 +1892,29 @@ func TestSeedBundledFillsMissingFilesInExistingDir(t *testing.T) {
|
||||
ps := NewPlugins(vm, dir)
|
||||
|
||||
if err := ps.SeedBundled(); err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
t.Fatalf("SeedBundled must be a no-op, got: %v", err)
|
||||
}
|
||||
|
||||
// ① 已存在的运维文件不被覆盖
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(entries) != 1 || entries[0].Name() != "billing.lua" {
|
||||
var names []string
|
||||
for _, e := range entries {
|
||||
names = append(names, e.Name())
|
||||
}
|
||||
t.Fatalf("网关不得写入任何插件文件, 目录应为 [billing.lua], 实得 %v", names)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(dir, "billing.lua"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != customBilling {
|
||||
t.Errorf("运维自己的 billing.lua 被覆盖了 —— 升级会抹掉运维的改动")
|
||||
t.Error("运维自己的 billing.lua 被改写了")
|
||||
}
|
||||
|
||||
// ② 每个内置插件现在都在目录里(这正是 channels.lua 缺失的那个洞)
|
||||
entries, err := bundledAdapters.ReadDir(bundledPluginsDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || filepath.Ext(e.Name()) != ".lua" {
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, e.Name())); err != nil {
|
||||
t.Errorf("内置插件 %s 未被补齐(目录已存在时被跳过)", e.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// ③ 补齐后确实能被加载(channels 必须真的出现在插件表里)
|
||||
// 手工放入的插件仍然可以加载 —— 外置不等于不可用。
|
||||
if err := ps.LoadDir(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -1930,35 +1922,7 @@ func TestSeedBundledFillsMissingFilesInExistingDir(t *testing.T) {
|
||||
for _, p := range ps.List() {
|
||||
names[p["name"].(string)] = true
|
||||
}
|
||||
if !names["channels"] {
|
||||
t.Errorf("channels 插件应被 seed 并加载, 实得 %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSeedBundledIdempotent 二次调用不应改变已就位的文件。
|
||||
func TestSeedBundledIdempotent(t *testing.T) {
|
||||
dir := filepath.Join(t.TempDir(), "plugins")
|
||||
vm := NewVM(filepath.Join(t.TempDir(), "adapters"))
|
||||
if err := vm.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer vm.Stop()
|
||||
ps := NewPlugins(vm, dir)
|
||||
|
||||
if err := ps.SeedBundled(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before, _ := os.ReadFile(filepath.Join(dir, "channels.lua"))
|
||||
// 运维改一笔
|
||||
edited := append(before, []byte("\n-- operator edit\n")...)
|
||||
if err := os.WriteFile(filepath.Join(dir, "channels.lua"), edited, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ps.SeedBundled(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after, _ := os.ReadFile(filepath.Join(dir, "channels.lua"))
|
||||
if string(after) != string(edited) {
|
||||
t.Error("二次 seed 覆盖了已存在的文件")
|
||||
if !names["billing"] {
|
||||
t.Errorf("plugin_dir 中手工安装的插件应被加载, 实得 %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user