mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
fix(plugins): 新内置插件要能落地 —— SeedBundled 改为按文件补齐,deploy.sh 补插件同步
真实事故:channels.lua(多平台账号接入插件)在仓库里、在 2.0 二进制里 (go:embed),生产却完全看不到它 —— /api/plugins 只有 billing/quota/usage。 根因是两处叠加: 1. SeedBundled 只在 plugin_dir **不存在**时才写内置插件。生产 plugin_dir 从 首次安装起就存在,于是后来加入二进制的新插件永远不会落盘。目录存在不等于 内容是最新的 —— 这个假设把"新增内置插件"整条路径堵死了。 改为**按文件补齐**:只在该文件缺失时才 seed,已存在的一律不动(运维自己改 过、或运行时新增的插件必须跨升级存活 —— 与适配器同一条规则)。 2. deploy.sh 完全不处理 plugins(只同步 adapters/)。即使内核修好了,部署脚本 也没有把插件内容与仓库对齐的动作。新增 sync_plugins:备份、只补缺、对已有 文件零改动、末尾逐个校验内嵌插件都在目标目录,摘要里打印插件目录。 测试(internal/lua/vm_test.go): - TestSeedBundledFillsMissingFilesInExistingDir:构造确切事故条件(目录已存在、 只有部分插件),断言缺失的被补齐、运维自己的 billing.lua 不被覆盖、补齐后 channels 确实出现在插件表里。 - TestSeedBundledIdempotent:二次 seed 不覆盖已就位文件。 deploy.sh 通过 bash -n;go test ./internal/lua/ 全绿。
This commit is contained in:
@ -641,15 +641,23 @@ func (ps *Plugins) LoadDir() error {
|
||||
// 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
|
||||
}
|
||||
if _, err := os.Stat(ps.dir); err == nil {
|
||||
return nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return writeBundledPlugins(ps.dir)
|
||||
}
|
||||
|
||||
|
||||
@ -744,11 +744,21 @@ func writeBundledPlugins(dir string) error {
|
||||
if e.IsDir() || filepath.Ext(e.Name()) != ".lua" {
|
||||
continue
|
||||
}
|
||||
dst := filepath.Join(dir, e.Name())
|
||||
// Per-file seeding: a plugin already present is left ALONE. That is what
|
||||
// makes an operator's own copy (or a locally edited one) survive an
|
||||
// upgrade, and it is also why a newly embedded plugin still arrives —
|
||||
// absence of THAT file, not of the directory, is the trigger.
|
||||
if _, err := os.Stat(dst); err == nil {
|
||||
continue
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
data, err := bundledAdapters.ReadFile(bundledPluginsDir + "/" + e.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, e.Name()), data, 0644); err != nil {
|
||||
if err := os.WriteFile(dst, data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@ -1868,3 +1868,97 @@ func TestAnthropicUsageCacheMapping(t *testing.T) {
|
||||
t.Errorf("usage-less chunk must not emit a usage object: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSeedBundledFillsMissingFilesInExistingDir 是真实事故的回归测试。
|
||||
//
|
||||
// 事故:SeedBundled 原本"目录已存在就什么都不做"。生产 plugin_dir 从首次安装
|
||||
// 起就存在,于是后来加入二进制的新内置插件(channels.lua)永远不会落盘 ——
|
||||
// 它在 go:embed 里、却完全不出现在 /api/plugins(线上只加载 billing/quota/usage)。
|
||||
//
|
||||
// 本测试构造那个确切条件:目录已存在且只装了部分内置插件,断言缺失的被补齐、
|
||||
// 已存在的(模拟运维自定义)**不被覆盖**。
|
||||
func TestSeedBundledFillsMissingFilesInExistingDir(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)
|
||||
}
|
||||
|
||||
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.Fatalf("seed: %v", err)
|
||||
}
|
||||
|
||||
// ① 已存在的运维文件不被覆盖
|
||||
got, err := os.ReadFile(filepath.Join(dir, "billing.lua"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != customBilling {
|
||||
t.Errorf("运维自己的 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)
|
||||
}
|
||||
names := map[string]bool{}
|
||||
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 覆盖了已存在的文件")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user