feat(config): 运行时配置体系②——渠道配置落 channels.json + 面板落盘 + 热更新
- runtimeconfig/channels.go: channels.json 承载 map[platform][]ChannelAccount(token/baseURL/黑白名单/群聊限流), LoadChannels 优先文件/回退env, SaveChannels(0600+tmp原子写), Upsert/Delete/Masked脱敏
- channelbridge/reload.go: adapterManager 热替换——工厂重建新adapter先连接, 再原子断旧adapter(独立readCtx/进行中不中断); Bridge.Reload(platform) 逐账号增删
- 面板 PUT/DELETE /admin/channels/{platform}/config(owner+审计) 落盘channels.json+Reload热更新; GET /admin/channels 附账号掩码不泄裸token
- 前端 Channels.tsx 配置表单(id/token/baseURL/黑白名单/限流+config JSON)+保存热更新提示+掩码展示/删除
- .gitignore 加 channels.json; .env 退兜底(.env.example注明); token不打印/Config()脱敏
- channelbridge.go 拆 logic→reload.go(742→731); 全仓 go build/vet/test绿+前端build绿
- 未做真token端到端(需渠道凭据,不读.env)——如实标注待凭据
This commit is contained in:
1 parent
2d7151f979
commit
aedae2190b
18 files changed
+1743
-126
No files matched your search
@@ -30,10 +30,14 @@ type PluginSummary struct {
|
||||
AllowInStranger bool `json:"allowInStranger"`
|
||||
}
|
||||
|
||||
// ChannelSummary is the wire shape of a registered channel adapter.
|
||||
// ChannelSummary is the wire shape of a registered channel adapter. Accounts
|
||||
// carries that platform's configured accounts (masked, no raw tokens) so the
|
||||
// panel can show and edit the account config; it is omitted when s.channels is
|
||||
// not wired or the platform has no configured account.
|
||||
type ChannelSummary struct {
|
||||
Name string `json:"name"`
|
||||
Connected bool `json:"connected"`
|
||||
Name string `json:"name"`
|
||||
Connected bool `json:"connected"`
|
||||
Accounts []AdminChannelAccount `json:"accounts,omitempty"`
|
||||
}
|
||||
|
||||
// policyRequest is the POST /api/v1/admin/plugins/policy body.
|
||||
@@ -133,8 +137,11 @@ func (s *Server) adminListPlugins(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// adminListChannels lists the registered channel adapters (admin read).
|
||||
// adminListChannels lists the registered channel adapters (admin read). When the
|
||||
// channel config store is wired it also attaches each platform's configured
|
||||
// accounts, masked so a raw token never reaches the client.
|
||||
func (s *Server) adminListChannels(w http.ResponseWriter, r *http.Request) {
|
||||
accountsByPlatform := s.maskedChannelAccounts()
|
||||
out := make([]ChannelSummary, 0)
|
||||
if s.caps != nil {
|
||||
for _, cap := range s.caps.List(capreg.KindChannel) {
|
||||
@@ -142,12 +149,57 @@ func (s *Server) adminListChannels(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, ChannelSummary{Name: ch.Name(), Connected: ch.IsConnected()})
|
||||
sum := ChannelSummary{Name: ch.Name(), Connected: ch.IsConnected()}
|
||||
if accts, ok := accountsByPlatform[channelBasePlatform(ch.Name())]; ok && len(accts) > 0 {
|
||||
sum.Accounts = accts
|
||||
}
|
||||
out = append(out, sum)
|
||||
}
|
||||
}
|
||||
// A platform that has configured accounts but no registered adapter (e.g. the
|
||||
// account was just saved) still surfaces, so its config is visible.
|
||||
for platform, accts := range accountsByPlatform {
|
||||
if len(accts) == 0 || s.hasChannelSummary(out, platform) {
|
||||
continue
|
||||
}
|
||||
out = append(out, ChannelSummary{Name: platform, Accounts: accts})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// maskedChannelAccounts returns the effective per-platform configured accounts,
|
||||
// masked (no raw tokens). It is empty when the channel config store is not wired.
|
||||
func (s *Server) maskedChannelAccounts() map[string][]AdminChannelAccount {
|
||||
if s.channels == nil {
|
||||
return map[string][]AdminChannelAccount{}
|
||||
}
|
||||
out := make(map[string][]AdminChannelAccount, 8)
|
||||
for platform, accts := range s.channels.Load() {
|
||||
list := make([]AdminChannelAccount, 0, len(accts))
|
||||
for _, a := range accts {
|
||||
if !configuredAccount(a) {
|
||||
continue
|
||||
}
|
||||
list = append(list, adminChannelAccountOf(runtimeconfig.Masked(a)))
|
||||
}
|
||||
if len(list) > 0 {
|
||||
out[platform] = list
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// hasChannelSummary reports whether out already contains a summary for platform
|
||||
// (so a platform with a registered adapter is not duplicated).
|
||||
func (s *Server) hasChannelSummary(out []ChannelSummary, platform string) bool {
|
||||
for _, c := range out {
|
||||
if channelBasePlatform(c.Name) == platform {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// adminListIoT lists IoT devices (admin read).
|
||||
func (s *Server) adminListIoT(w http.ResponseWriter, r *http.Request) {
|
||||
out := make([]iotDeviceSummary, 0)
|
||||
|
||||
Reference in new issue
Block a user