feat(bridge): separate bridge config from .env, web panel + hot-reload

- BridgeConfigStore: file-backed JSON config (configs/bridge.json)
- Auto-creates config file only when user saves via admin panel
- Hot-reload: watches config file every 5s, reloads adapters live
- Admin API: GET/PUT/DELETE /api/v1/admin/bridge/config/:name
- Frontend: full CRUD page with platform type selector + toggle
- Removed bridge env vars from .env.example
This commit is contained in:
Aska committed 2026-08-12 17:55:15 +08:00
1 parent 217319eabc
commit 9eaed2883f
5 files changed
+478 -73

No files matched your search

+64 -40
View File
@@ -185,49 +185,21 @@ func main() {
log.Printf("[orchestrator] pipeline ready")
// === 11.5 Platform Bridge ===
bridgeCfgPath := "configs/bridge.json"
bridgeRouter := bridge.NewRouter()
// QQ adapter — enabled when OBv11 WebSocket URL is configured
if qqURL := os.Getenv("QQ_OBV11_WS_URL"); qqURL != "" {
qqAdapter := bridgeqq.New(bridgeqq.Config{
Name: "qq-main", WSURL: qqURL,
AccessToken: os.Getenv("QQ_ACCESS_TOKEN"),
BotQQ: os.Getenv("QQ_BOT_QQ"),
})
bridgeRouter.Register(qqAdapter)
}
// Telegram
if tgToken := os.Getenv("TG_BOT_TOKEN"); tgToken != "" {
bridgeRouter.Register(bridgetg.New(bridgetg.Config{Name: "tg-main", Token: tgToken}))
}
// Discord
if dcToken := os.Getenv("DC_BOT_TOKEN"); dcToken != "" {
bridgeRouter.Register(bridgedc.New(bridgedc.Config{Name: "dc-main", Token: dcToken}))
}
// Webhook
if whAddr := os.Getenv("WEBHOOK_LISTEN_ADDR"); whAddr != "" {
bridgeRouter.Register(bridgewh.New(bridgewh.Config{Name: "webhook-main", ListenAddr: whAddr}))
}
// Route incoming messages to orchestrator
bridgeRouter.SetMessageHandler(func(msg *bridge.UnifiedMessage) {
log.Printf("[bridge] incoming %s from %s: %s", msg.Platform, msg.SenderName, msg.Content)
tokenCh, _, err := orch.ProcessInput(context.Background(), "bridge-"+msg.Source, msg.Content)
if err != nil {
log.Printf("[bridge] orchestrator error: %v", err)
return
}
var fullContent string
for token := range tokenCh {
fullContent += token.Content
}
if fullContent != "" {
bridgeRouter.SendTo(msg.Platform, msg.Source, fullContent, bridge.SendOptions{
MaxLength: 500, MinGap: 3 * time.Second,
})
}
bridgeStore := bridge.NewConfigStore(bridgeCfgPath, func(cfg *bridge.BridgeConfig) {
bridgeRouter.StopAll()
bridgeRouter = bridge.NewRouter()
loadAdaptersFromConfig(bridgeRouter, cfg, orch)
bridgeRouter.StartAll()
})
bridgeStore.StartWatching(5 * time.Second)
bridgeRouter.StartAll(); log.Printf("[bridge] platform bridge ready")
// Load initial adapters
loadAdaptersFromConfig(bridgeRouter, &bridge.BridgeConfig{Adapters: bridgeStore.GetAdapters()}, orch)
bridgeRouter.StartAll()
log.Printf("[bridge] config store ready (path=%s, %d adapters)", bridgeCfgPath, bridgeStore.Count())
// === 12. Gateway ===
userStore := api.NewMemUserStore(cfg.AdminPassword)
@@ -246,6 +218,7 @@ func main() {
TraceLog: &traceAdapter{Logger: traceLogger},
PerfMon: &perfAdapter{Monitor: perfMon},
ModelConfig: api.NewModelConfigStore(),
BridgeCfg: &bridgeConfigAdapter{ConfigStore: bridgeStore},
PluginList: pluginNamesToMap(reg.ListNames()),
}
if memSvc != nil {
@@ -340,3 +313,54 @@ func (a *perfAdapter) History() []map[string]any {
}
return result
}
// bridgeConfigAdapter adapts bridge.ConfigStore to api.BridgeConfigManager.
type bridgeConfigAdapter struct{ *bridge.ConfigStore }
func (a *bridgeConfigAdapter) GetAdapters() []map[string]any {
adapters := a.ConfigStore.GetAdapters()
result := make([]map[string]any, len(adapters))
for i, ac := range adapters {
result[i] = map[string]any{
"name": ac.Name, "type": ac.Type, "enabled": ac.Enabled,
"settings": ac.Settings, "created_at": ac.CreatedAt, "updated_at": ac.UpdatedAt,
}
}
return result
}
func (a *bridgeConfigAdapter) SaveAdapter(name, adapterType string, enabled bool, settings map[string]string) error {
return a.ConfigStore.SaveAdapter(bridge.AdapterConfig{
Name: name, Type: adapterType, Enabled: enabled, Settings: settings,
})
}
func (a *bridgeConfigAdapter) DeleteAdapter(name string) error {
return a.ConfigStore.DeleteAdapter(name)
}
func loadAdaptersFromConfig(router *bridge.Router, cfg *bridge.BridgeConfig, orch *orchestrator.Pipeline) {
for _, ac := range cfg.Adapters {
if !ac.Enabled { continue }
switch ac.Type {
case "qq-obv11":
router.Register(bridgeqq.New(bridgeqq.Config{
Name: ac.Name, WSURL: ac.Settings["ws_url"],
AccessToken: ac.Settings["access_token"], BotQQ: ac.Settings["bot_qq"],
}))
case "telegram":
router.Register(bridgetg.New(bridgetg.Config{Name: ac.Name, Token: ac.Settings["token"]}))
case "discord":
router.Register(bridgedc.New(bridgedc.Config{Name: ac.Name, Token: ac.Settings["token"]}))
case "webhook":
router.Register(bridgewh.New(bridgewh.Config{Name: ac.Name, ListenAddr: ac.Settings["listen_addr"]}))
}
}
router.SetMessageHandler(func(msg *bridge.UnifiedMessage) {
tokenCh, _, err := orch.ProcessInput(context.Background(), "bridge-"+msg.Source, msg.Content)
if err != nil { return }
var fc string
for t := range tokenCh { fc += t.Content }
if fc != "" {
router.SendTo(msg.Platform, msg.Source, fc, bridge.SendOptions{MaxLength: 500, MinGap: 3 * time.Second})
}
})
}