feat: 渠道管理分两块(.env必要回退+删除同步+可用类型/已保存分开)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

agent-120 .env只留必要回退:
- .env.example KOOK移4可选业务字段(RECEIVE_MODE/WEBHOOK_PATH/VERIFY_TOKEN/ENCRYPT_KEY),只留token/base_url; channelAccountsFromEnv不读这4个; 说明业务字段走channelConfigForm.channels.json
- 黑名单/限流/receive_mode等走管理面板热更新, env仅必要连接回退

agent-121 删除渠道列表同步(根因: adminListChannels枚举caps,删除不同步caps):
- adminListChannels 以channels.json为准: 遍历caps时已删账号adapter跳过(s.channels注入时), 删除/保存后列表=channels.json, 删除即消失
- (capReg Remove后续可加,范围外标注)

agent-122 渠道管理分两块显示(主人:不割裂):
- ①可用适配器类型(顶部PLATFORM_TYPES全集grid卡片,已配置/未配置Tag,点新建配置)
- ②已保存配置(下方AdminTable过滤accounts>0,账号+bot身份+掩码token提示+连接/断开/删除)
- 拆 channelConfigForm.tsx(248行) 表单抽出; Channels.tsx 463→306行; token只回填hint不写值(保存留空不改secret)
全仓 build/vet/test绿+前端build绿(356KB)
This commit is contained in:
Aska committed 2026-09-07 16:16:50 +08:00
1 parent b614633432
commit 0ed7984de3
8 files changed
+578 -290

No files matched your search

+12 -7
View File
@@ -87,7 +87,11 @@ func buildPlatformAdapters(platform string, accts []config.ChannelAccount, log *
// channelAccountsFromEnv synthesizes the env fallback: the single-account
// CHANNEL_<NAME>_* adapters plus any CHANNELS_JSON multi-account entries. It is
// the .env fallback passed to runtimeconfig.LoadChannels when channels.json is
// absent or invalid.
// absent or invalid. .env only carries the necessary connection fallback
// (token/base_url/...); business and advanced per-account fields (receive_mode,
// webhook path/verify_token/encrypt_key, allow/deny lists, rate limits) are
// configured in the panel's channel management and live in channels.json, never
// in .env.
//
// A token-requiring platform (kook/telegram/weixin-ilink) is synthesized only
// when its token env var is set; with an unset token there is no account at all,
@@ -96,16 +100,17 @@ func buildPlatformAdapters(platform string, accts []config.ChannelAccount, log *
// gate decides whether to dial) so the channel list stays stable.
func channelAccountsFromEnv() map[string][]config.ChannelAccount {
out := map[string][]config.ChannelAccount{}
// KOOK: only the necessary connection fields. receive_mode / webhook_path /
// webhook_verify_token / webhook_encrypt_key are business·advanced fields that
// go to channels.json via the panel — .env does not fall back on them.
if tok := os.Getenv("CHANNEL_KOOK_TOKEN"); tok != "" {
out["kook"] = append(out["kook"], config.ChannelAccount{Config: map[string]any{
"bot_token": tok,
"base_url": os.Getenv("CHANNEL_KOOK_BASE_URL"),
"receive_mode": os.Getenv("CHANNEL_KOOK_RECEIVE_MODE"),
"webhook_path": os.Getenv("CHANNEL_KOOK_WEBHOOK_PATH"),
"webhook_verify_token": os.Getenv("CHANNEL_KOOK_WEBHOOK_VERIFY_TOKEN"),
"webhook_encrypt_key": os.Getenv("CHANNEL_KOOK_WEBHOOK_ENCRYPT_KEY"),
"bot_token": tok,
"base_url": os.Getenv("CHANNEL_KOOK_BASE_URL"),
}})
}
// Telegram: token (necessary) + optional webhook_url (connection). Business
// tuning goes to channels.json.
if tok := os.Getenv("CHANNEL_TELEGRAM_BOT_TOKEN"); tok != "" {
out["telegram"] = append(out["telegram"], config.ChannelAccount{Config: map[string]any{
"bot_token": tok,
+26 -13
View File
@@ -151,22 +151,29 @@ func TestChannelFactoryBuildFromFile(t *testing.T) {
// TestNamedAdapterForwardsWebhook verifies the production wrapper forwards a
// kook webhook-mode adapter's InboundHandler/WebhookPath to the channel bridge,
// so bridge.WebhookHandlers() mounts the KOOK callback route on the gateway.
// so bridge.WebhookHandlers() mounts the KOOK callback route on the gateway. The
// webhook business fields (receive_mode/webhook_path) are sourced from
// channels.json — .env only carries the necessary connection fallback.
func TestNamedAdapterForwardsWebhook(t *testing.T) {
t.Setenv("CHANNEL_KOOK_TOKEN", "kook-secret")
t.Setenv("CHANNEL_KOOK_RECEIVE_MODE", "webhook")
t.Setenv("CHANNEL_KOOK_WEBHOOK_PATH", "/webhook/kook")
adapters := buildChannelAdapters(testFactory(t))
path := filepath.Join(t.TempDir(), "channels.json")
raw := `{"kook":[{"id":"conf0","config":{"bot_token":"kook-secret","receive_mode":"webhook","webhook_path":"/webhook/kook"}}]}`
if err := os.WriteFile(path, []byte(raw), 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)
}
var found *namedAdapter
for _, a := range adapters {
for _, a := range adpts {
if nwa, ok := a.(*namedAdapter); ok && barePlatform(nwa.Name()) == "kook" {
found = nwa
break
}
}
if found == nil {
t.Fatalf("kook named adapter not found, got %v", names(adapters))
t.Fatalf("kook named adapter not found, got %v", names(adpts))
}
if p := found.WebhookPath(); p != "/webhook/kook" {
t.Errorf("WebhookPath = %q, want /webhook/kook", p)
@@ -179,11 +186,17 @@ func TestNamedAdapterForwardsWebhook(t *testing.T) {
// TestNamedAdapterNoWebhookInWSMode verifies a ws-mode adapter (all non-webhook
// platforms) does not surface a webhook handler through the wrapper.
func TestNamedAdapterNoWebhookInWSMode(t *testing.T) {
t.Setenv("CHANNEL_KOOK_TOKEN", "kook-secret")
t.Setenv("CHANNEL_KOOK_RECEIVE_MODE", "ws")
adapters := buildChannelAdapters(testFactory(t))
for _, a := range adapters {
path := filepath.Join(t.TempDir(), "channels.json")
raw := `{"kook":[{"id":"conf0","config":{"bot_token":"kook-secret","receive_mode":"ws"}}]}`
if err := os.WriteFile(path, []byte(raw), 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)
}
for _, a := range adpts {
if barePlatform(a.Name()) != "kook" {
continue
}