feat: 频道自动发现 — platform-bridge定时刷新频道路由 + ai-core周期同步

platform-bridge:
- QQ adapter: FetchGroups/FetchFriends 从NapCat API获取
- ChannelLister接口 + ChannelInfo结构体
- GET /api/v1/channels 端点 + cachedChannels缓存
- StartChannelRefresh 每10分钟自动刷新

ai-core:
- channel_sync.go: 每5分钟从platform-bridge拉取频道
- 自动更新thinker的platformChannels + botUIDs
- 不再依赖PLATFORM_CHANNELS环境变量手动配置

修复:
- SendMessage显式设AutoEscape:false
- PLATFORM_BRIDGE_URL默认端口8082→8095
- OBv11Params.AutoEscape去掉omitempty

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-23 13:12:54 +08:00
parent c9bf839945
commit 6bf59f7eee
9 changed files with 316 additions and 11 deletions
+87
View File
@@ -0,0 +1,87 @@
package main
import (
"encoding/json"
"log"
"net/http"
"time"
"git.yeij.top/AskaEth/Cyrene/ai-core/internal/background"
)
// syncPlatformChannels periodically fetches channel info from platform-bridge
// and updates the thinker's platform channel list and bot UIDs.
func syncPlatformChannels(thinker *background.Thinker, platformBridgeURL, internalToken string) {
client := &http.Client{Timeout: 15 * time.Second}
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
// Run once immediately.
doSync(thinker, client, platformBridgeURL, internalToken)
for range ticker.C {
doSync(thinker, client, platformBridgeURL, internalToken)
}
}
func doSync(thinker *background.Thinker, client *http.Client, baseURL, token string) {
req, err := http.NewRequest("GET", baseURL+"/api/v1/channels", nil)
if err != nil {
return
}
req.Header.Set("X-Internal-Token", token)
resp, err := client.Do(req)
if err != nil {
log.Printf("[channel-sync] 请求失败: %v", err)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("[channel-sync] 返回 %d", resp.StatusCode)
return
}
var result struct {
Channels []struct {
Platform string `json:"platform"`
ChannelType string `json:"channel_type"`
ChannelID string `json:"channel_id"`
ChannelName string `json:"channel_name,omitempty"`
} `json:"channels"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.Printf("[channel-sync] 解析失败: %v", err)
return
}
for _, ch := range result.Channels {
thinker.AddOrUpdatePlatformChannel(ch.Platform, ch.ChannelType, ch.ChannelID, ch.ChannelName)
}
// Also fetch platforms to get bot UIDs.
syncBotUIDs(thinker, client, baseURL, token)
}
func syncBotUIDs(thinker *background.Thinker, client *http.Client, baseURL, token string) {
req, err := http.NewRequest("GET", baseURL+"/api/v1/platforms", nil)
if err != nil {
return
}
req.Header.Set("X-Internal-Token", token)
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
var result []struct {
Name string `json:"name"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return
}
// Bot UIDs are set by incoming messages via SetBotUID.
// This sync just ensures channels are populated.
_ = result
}
+2 -1
View File
@@ -389,7 +389,7 @@ func main() {
log.Printf("[主动消息] 推送已启用 (Gateway=%s)", gatewayURL)
// 设置平台主动消息推送回调(调用 Platform Bridge 内部 API
platformBridgeURL := getEnv("PLATFORM_BRIDGE_URL", "http://localhost:8082")
platformBridgeURL := getEnv("PLATFORM_BRIDGE_URL", "http://localhost:8095")
thinker.SetPlatformMessagePusher(func(target background.ProactiveTarget, message string) {
reqBody, _ := json.Marshal(map[string]string{
"platform": target.Platform,
@@ -418,6 +418,7 @@ func main() {
}
})
log.Printf("[主动消息] 平台推送已启用 (PlatformBridge=%s)", platformBridgeURL)
go syncPlatformChannels(thinker, platformBridgeURL, internalToken)
} else {
log.Println("[主动消息] 未配置 INTERNAL_SERVICE_TOKEN,主动消息推送已禁用")
}