Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cmd/moonbridge/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,14 @@ func run(args []string, stdout io.Writer, stderr io.Writer) int {
if *addr != "" {
cfg.OverrideAddr(*addr)
}
// -print-* 子命令在 SQLite 覆盖前就会退出,这里先加载持久化中的有效配置,
// 使 -print-addr/-print-mode/-print-codex-config 等读到 WebUI 改动后的状态。
// 持久化未启用或加载失败时静默回退到 YAML 配置(YAML-only 部署是合法形态)。
if effectiveCfg, loadErr := app.TryLoadEffectiveConfig(context.Background(), cfg); loadErr == nil {
cfg = effectiveCfg
} else {
slog.Debug("跳过持久化配置加载,使用 YAML 配置", "error", loadErr)
}
if *printAddr {
fmt.Fprintln(stdout, cfg.Addr)
return exitOK
Expand Down
118 changes: 118 additions & 0 deletions cmd/moonbridge/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,121 @@ func TestPublishConfigFileDoesNotOverwriteExistingFinalFile(t *testing.T) {
t.Fatalf("temp file stat error = %v, want not exist", err)
}
}

// TestRunPrintCodexConfigReadsSQLiteOverride 是核心回归测试:
// 验证 -print-codex-config 能读到 SQLite 持久化覆盖后的配置,而非 YAML 源。
//
// 场景:config.yml 里是 placeholder provider/model,但 SQLite 中存的是 db-provider。
// 修复前 print 只读 YAML(半成品);修复后经 TryLoadEffectiveConfig 读到 DB 覆盖配置。
//
// 两阶段:
// 1. 用含 db-provider 的 config.yml 跑一次 → db_sqlite 自动 seed(库获得 db-provider)
// 2. 改写 config.yml 为 placeholder provider,再跑 -print-codex-config db-alias
// → 应输出 SQLite 里的 db-upstream-model(证明读到了覆盖配置)
func TestRunPrintCodexConfigReadsSQLiteOverride(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "moonbridge.db")
configPath := filepath.Join(dir, "config.yml")

// 第一阶段:含 db-provider 的配置,用于 seed SQLite。
seedConfig := []byte(`
mode: Transform
server:
addr: "127.0.0.1:0"
persistence:
active_provider: db_sqlite
extensions:
db_sqlite:
enabled: true
config:
path: ` + dbPath + `
models:
db-model:
context_window: 200000
providers:
db-provider:
base_url: "https://db.example.test"
api_key: "db-key"
protocol: anthropic
offers:
- model: db-model
upstream_name: db-upstream-model
routes:
db-alias:
provider: db-provider
model: db-model
`)
if err := os.WriteFile(configPath, seedConfig, 0o600); err != nil {
t.Fatalf("WriteFile(seed): %v", err)
}

// 第一次 run:触发 db_sqlite 初始化 + seed(-print-mode 提前退出,但持久化已 seed)。
var seedStdout, seedStderr bytes.Buffer
if code := run([]string{"-config", configPath, "-print-mode"}, &seedStdout, &seedStderr); code != exitOK {
t.Fatalf("seed run exit = %d, want %d; stderr:\n%s", code, exitOK, seedStderr.String())
}

// 确认 SQLite 文件已创建(seed 生效)。
if _, err := os.Stat(dbPath); err != nil {
t.Fatalf("seed did not create db file %s: %v", dbPath, err)
}

// 第二阶段:改写 config.yml 为完全不同的 placeholder provider/model。
// SQLite 中仍是第一阶段 seed 的 db-provider。print 应读 DB 而非这份 YAML。
placeholderConfig := []byte(`
mode: Transform
server:
addr: "127.0.0.1:0"
persistence:
active_provider: db_sqlite
extensions:
db_sqlite:
enabled: true
config:
path: ` + dbPath + `
models:
yaml-model:
context_window: 128000
providers:
yaml-placeholder:
base_url: "https://yaml.example.test"
api_key: "yaml-key"
protocol: anthropic
offers:
- model: yaml-model
routes:
yaml-alias:
provider: yaml-placeholder
model: yaml-model
`)
if err := os.WriteFile(configPath, placeholderConfig, 0o600); err != nil {
t.Fatalf("WriteFile(placeholder): %v", err)
}

// 核心断言:-print-codex-config 应输出 db-alias(DB 里的路由)而非 yaml-alias。
// db-alias 在 YAML 中已不存在,只有读到 SQLite 覆盖配置才能生成。
var stdout, stderr bytes.Buffer
code := run([]string{
"-config", configPath,
"-print-codex-config", "db-alias",
}, &stdout, &stderr)
if code != exitOK {
t.Fatalf("print run exit = %d, want %d; stderr:\n%s", code, exitOK, stderr.String())
}

output := stdout.String()
// db-alias 是 SQLite 中的路由别名;YAML(placeholder)里只有 yaml-alias。
// 输出含 db-alias 即证明 print 读到了 SQLite 覆盖配置。
if !strings.Contains(output, `model = "db-alias"`) {
t.Errorf("print output missing model=\"db-alias\" (proves it read YAML not SQLite):\n%s", output)
}
// context_window = 200000 来自 DB 的 db-model;YAML 的 yaml-model 是 128000。
// 这进一步证明读的是 SQLite 数据而非 YAML。
if !strings.Contains(output, "model_context_window = 200000") {
t.Errorf("print output missing model_context_window=200000 (DB value; YAML yaml-model is 128000):\n%s", output)
}
// 反向验证:YAML 的 placeholder 路由不应出现(证明 DB 完全覆盖 YAML)。
if strings.Contains(output, "yaml-alias") {
t.Errorf("print output contains yaml-alias (should not: DB should override YAML):\n%s", output)
}
}
100 changes: 28 additions & 72 deletions internal/service/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package app

import (
"context"
stderrors "errors"
"fmt"
"io"
"net/http"
Expand All @@ -11,7 +10,6 @@ import (

"log/slog"
"moonbridge/internal/config"
"moonbridge/internal/db"
"moonbridge/internal/extension/codextool"
"moonbridge/internal/format"
"moonbridge/internal/logger"
Expand All @@ -28,7 +26,6 @@ import (
"moonbridge/internal/service/server/trace"
"moonbridge/internal/service/server/usage"
"moonbridge/internal/service/stats"
"moonbridge/internal/service/store"
mbtrace "moonbridge/internal/service/trace"
)

Expand Down Expand Up @@ -122,78 +119,37 @@ func runTransform(ctx context.Context, cfg config.Config, errors io.Writer) erro
return plugins.ConsumeGlobalLog(entries)
})

// Initialize persistence layer (db.Registry).
dbRegistry := db.NewRegistry(slog.Default())
dbProviders := plugins.DBProviders()
providers := make([]db.Provider, 0, len(dbProviders))
for _, p := range dbProviders {
if prov := p.DBProvider(); prov != nil {
dbRegistry.RegisterProvider(prov)
providers = append(providers, prov)
}
}
for _, c := range plugins.DBConsumers() {
if cons := c.DBConsumer(); cons != nil {
dbRegistry.RegisterConsumer(cons)
// === Phase 2: Persistence bootstrap ===
// db.Registry 装配、ConfigStore.LoadAll 覆盖、空库 seed 等逻辑统一收敛到
// BootstrapPersistence,使 -print-* 子命令(经 TryLoadEffectiveConfig)与
// 本路径共享同一份覆盖语义。
result, err := BootstrapPersistence(ctx, cfg, plugins)
if err != nil {
return fmt.Errorf("bootstrap persistence: %w", err)
}
if result.Shutdown != nil {
defer result.Shutdown()
}
cfg = result.Cfg
cs := result.Store

// 如果 SQLite 覆盖了 cfg,用覆盖后的配置重建 provider 运行时派生状态。
if result.Overridden {
dbProviderCfg := config.ProviderFromGlobalConfig(&cfg)
providerDefs = provider.BuildProviderDefsFromConfig(dbProviderCfg)
modelRoutes = provider.BuildModelRoutesFromConfig(dbProviderCfg)
providerMgr, err = provider.NewProviderManager(providerDefs, modelRoutes)
if err != nil {
return fmt.Errorf("rebuild provider manager from DB: %w", err)
}
}
// Register the config_store consumer for configuration persistence.
configStoreConsumer := store.NewConfigStoreConsumer(logger.L())
configStoreConsumer.SetExtensionSpecs(BuiltinExtensions().ConfigSpecs())
dbRegistry.RegisterConsumer(configStoreConsumer)
activePersistenceProvider := ResolvePersistenceActiveProvider(cfg.Persistence.ActiveProvider, providers)
if err := dbRegistry.Init(ctx, activePersistenceProvider); err != nil {
return fmt.Errorf("init persistence: %w", err)
}
defer dbRegistry.Shutdown()

// === Phase 2: ConfigStore bootstrap ===
// Check if the store is available and has existing data.
cs := configStoreConsumer.Store()
if cs != nil {
if dbCfg, loadErr := cs.LoadAll(); loadErr == nil {
if len(dbCfg.ProviderDefs) > 0 || len(dbCfg.Routes) > 0 {
// DB has existing configuration: use it as the active config.
logger.Info("从持久化存储加载配置",
"providers", len(dbCfg.ProviderDefs),
"routes", len(dbCfg.Routes))
cfg = *dbCfg
dbProviderCfg := config.ProviderFromGlobalConfig(&cfg)

// Rebuild provider manager and pricing from DB-loaded config.
providerDefs = provider.BuildProviderDefsFromConfig(dbProviderCfg)
modelRoutes = provider.BuildModelRoutesFromConfig(dbProviderCfg)
providerMgr, err = provider.NewProviderManager(providerDefs, modelRoutes)
if err != nil {
return fmt.Errorf("rebuild provider manager from DB: %w", err)
}
_ = resolveDefaultClient(providerMgr, errors)
resolvePerProviderWebSearch(ctx, cfg, providerMgr, errors)
_ = resolveDefaultClient(providerMgr, errors)
resolvePerProviderWebSearch(ctx, cfg, providerMgr, errors)

pricing = provider.BuildPricingFromConfig(dbProviderCfg)
if len(pricing) > 0 {
sessionStats.SetPricing(pricing)
}
serverCfg = config.ServerFromGlobalConfig(&cfg)
} else {
// DB is empty: seed from YAML config.
logger.Info("持久化存储为空,从 YAML 导入种子配置")
if err := cs.SeedFromConfig(&cfg); err != nil {
logger.Warn("config store 种子导入失败", "error", err)
}
}
} else if loadErr != nil {
if stderrors.Is(loadErr, store.ErrConfigNotSeeded) {
logger.Info("持久化存储为空,从 YAML 导入种子配置")
if err := cs.SeedFromConfig(&cfg); err != nil {
return fmt.Errorf("seed config store from YAML: %w", err)
}
} else {
logger.Warn("config store 加载失败", "error", loadErr)
}
pricing = provider.BuildPricingFromConfig(dbProviderCfg)
if len(pricing) > 0 {
sessionStats.SetPricing(pricing)
}
} else {
logger.Warn("config store 不可用,跳过持久化引导")
serverCfg = config.ServerFromGlobalConfig(&cfg)
}

// === Phase 3: Build Runtime ===
Expand Down
Loading