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:
1 parent
217319eabc
commit
9eaed2883f
5 files changed
+478
-73
No files matched your search
+64
-40
@@ -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})
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user