feat(config): 运行时配置体系②——渠道配置落 channels.json + 面板落盘 + 热更新
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 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:
Aska committed 2026-09-07 13:29:41 +08:00
1 parent 2d7151f979
commit aedae2190b
18 files changed
+1743 -126

No files matched your search

+57 -5
View File
@@ -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)