feat: config.yaml only, OpenRouter free models, remove runtime.json config

- Move all config (auto rules, keys) from runtime.json to config.yaml
- Store now only holds runtime sources (WebUI-created)
- Add OpenRouter free models to config.yaml
- One-time migration from legacy runtime.json on startup
- Fix gateway tests for new config structure
- Update core.go with migrateFromRuntime, saveConfig, seedKeys/seedAuto
- Remove SaveKey/KeyByValue/AutoRules from Store
- Add Config.Save() with YAML marshaling
- Update WebUI admin keys visibility (show all keys including admin)
- Bump binary to 11MB with luajit
This commit is contained in:
JianFeeeee
2026-08-13 10:18:19 +08:00
parent 084c9fee2b
commit d06210204b
10 changed files with 307 additions and 225 deletions

View File

@ -37,9 +37,13 @@ func mockUpstream() *httptest.Server {
func newTestGateway(t *testing.T, srcs ...config.Source) *Gateway {
t.Helper()
td := t.TempDir()
cfgPath := filepath.Join(td, "config.yaml")
os.WriteFile(cfgPath, []byte("listen: :0"), 0644)
cfg := &config.Config{
AdapterDir: filepath.Join(t.TempDir(), "adapters"),
RuntimeFile: filepath.Join(t.TempDir(), "runtime.json"),
Path: cfgPath,
AdapterDir: filepath.Join(td, "adapters"),
RuntimeFile: filepath.Join(td, "runtime.json"),
GatewayKeys: []string{"sk-test"},
Sources: srcs,
}
@ -543,9 +547,9 @@ func TestSourcesAPIAddAndPersist(t *testing.T) {
if !strings.Contains(rr.Body.String(), "new-m") {
t.Fatalf("new model not live: %s", rr.Body.String())
}
// verify persistence file exists
if _, err := os.Stat(g.core.Config().RuntimeFile); err != nil {
t.Fatalf("runtime file not written: %v", err)
// verify source persisted to config.yaml
if _, err := os.Stat(g.core.Config().Path); err != nil {
t.Fatalf("config file not written: %v", err)
}
}

View File

@ -203,9 +203,10 @@ textarea{min-height:200px;resize:vertical;font-family:'JetBrains Mono',ui-monosp
select{cursor:pointer}
label{display:block;font-size:12px;color:var(--muted);margin:12px 0 5px}
.row{display:flex;gap:12px}.row>div{flex:1}
.model-row{display:flex;gap:8px;align-items:center}
.model-row input,.model-row select{margin:0 0 8px}
.model-row .del{flex:0 0 auto;padding:5px 9px}
.model-row{display:flex;gap:6px;align-items:center;width:100%}
.model-row .m-id{flex:1;min-width:0;width:0}
.model-row .m-kind{flex:0 0 70px;width:70px}
.model-row .del{flex:0 0 auto;padding:4px 8px}
.muted{color:var(--muted)}
.hidden,.hidden#tab-chat{display:none}
#toast{position:fixed;bottom:24px;right:24px;z-index:100;background:var(--card-solid);border:1px solid var(--line);
@ -2179,7 +2180,7 @@ async function renderKeysAdmin() {
async function loadKeys() {
const el = $('#k-list'); if (!el) return;
const j = await api('/api/keys');
const ks = (j.keys || []).filter(k => k.role === 'user');
const ks = (j.keys || []);
el.innerHTML = ks.length ? ks.map(k => keyCanvasHtml(k)).join('') : `<div class="muted">${t('kEmpty')}</div>`;
el.querySelectorAll('.kc-blocks').forEach(cv => bindCanvasDrop(cv));
el.querySelectorAll('.mb').forEach(b => bindBrickDrag(b));