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
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
@@ -13,9 +15,20 @@ func testLog() *slog.Logger {
|
||||
return logger.New(logger.Options{Out: io.Discard})
|
||||
}
|
||||
|
||||
// testFactory builds a channelFactory pointing at a temp (absent) channels.json,
|
||||
// seeded by the env fallback, so the test reads the env the same way boot does.
|
||||
func testFactory(t *testing.T) *channelFactory {
|
||||
t.Helper()
|
||||
return &channelFactory{
|
||||
path: filepath.Join(t.TempDir(), "channels.json"),
|
||||
fallback: channelAccountsFromEnv(),
|
||||
log: testLog(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildChannelAdapters_IncludesKook(t *testing.T) {
|
||||
t.Setenv("CHANNEL_KOOK_TOKEN", "kook-secret")
|
||||
adapters := buildChannelAdapters(testLog())
|
||||
adapters := buildChannelAdapters(testFactory(t))
|
||||
if !hasAdapter(adapters, "kook") {
|
||||
t.Fatalf("expected a kook adapter, got %v", names(adapters))
|
||||
}
|
||||
@@ -23,7 +36,7 @@ func TestBuildChannelAdapters_IncludesKook(t *testing.T) {
|
||||
|
||||
func TestBuildChannelAdapters_KookMultiAccount(t *testing.T) {
|
||||
t.Setenv("CHANNELS_JSON", `{"kook":[{"id":"conf0","config":{"bot_token":"kook-secret"},"allowUsers":["u-1"],"denyGroups":["g-9"]}]}`)
|
||||
adapters := buildChannelAdapters(testLog())
|
||||
adapters := buildChannelAdapters(testFactory(t))
|
||||
if !hasAdapter(adapters, "kook:conf0") {
|
||||
t.Fatalf("expected a kook:conf0 adapter, got %v", names(adapters))
|
||||
}
|
||||
@@ -47,6 +60,43 @@ func hasAdapter(adapters []adapter.PlatformAdapter, name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// TestChannelFactoryBuildFromFile exercises the reload factory against a real
|
||||
// channels.json file: the factory reads the file (file beats env) and builds a
|
||||
// named adapter carrying the account's token and black-or-white lists, so a panel
|
||||
// save → Reload path uses exactly this builder.
|
||||
func TestChannelFactoryBuildFromFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "channels.json")
|
||||
if err := os.WriteFile(path, []byte(`{"kook":[{"id":"conf0","config":{"bot_token":"bt"},"denyGroups":["g-1"],"allowUsers":["u-1"],"groupMsgPerMin":3}]}`), 0o600); err != nil {
|
||||
t.Fatalf("write channels.json: %v", err)
|
||||
}
|
||||
f := &channelFactory{path: path, fallback: channelAccountsFromEnv(), log: testLog()}
|
||||
adpts, err := f.Build("kook")
|
||||
if err != nil {
|
||||
t.Fatalf("Build: %v", err)
|
||||
}
|
||||
if !hasAdapter(adpts, "kook:conf0") {
|
||||
t.Fatalf("kook:conf0 not built, got %v", names(adpts))
|
||||
}
|
||||
for _, a := range adpts {
|
||||
if a.Name() != "kook:conf0" {
|
||||
continue
|
||||
}
|
||||
cfg := a.Config()
|
||||
if cfg["bot_token"] != "bt" {
|
||||
t.Errorf("bot_token = %v, want bt", cfg["bot_token"])
|
||||
}
|
||||
if got, _ := cfg["denyGroups"].([]string); len(got) != 1 || got[0] != "g-1" {
|
||||
t.Errorf("denyGroups = %v, want [g-1]", cfg["denyGroups"])
|
||||
}
|
||||
if got, _ := cfg["allowUsers"].([]string); len(got) != 1 || got[0] != "u-1" {
|
||||
t.Errorf("allowUsers = %v, want [u-1]", cfg["allowUsers"])
|
||||
}
|
||||
if got, _ := cfg["groupMsgPerMin"].(int); got != 3 {
|
||||
t.Errorf("groupMsgPerMin = %v, want 3", cfg["groupMsgPerMin"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func names(adapters []adapter.PlatformAdapter) []string {
|
||||
out := make([]string, 0, len(adapters))
|
||||
for _, a := range adapters {
|
||||
|
||||
Reference in new issue
Block a user