diff --git a/frontend/web/src/admin/modules/Channels.tsx b/frontend/web/src/admin/modules/Channels.tsx index 06c7165..9b7ceb6 100644 --- a/frontend/web/src/admin/modules/Channels.tsx +++ b/frontend/web/src/admin/modules/Channels.tsx @@ -13,57 +13,14 @@ import { useAutoReload } from "../hooks"; import { useAuditStore } from "../audit"; import { AdminTable, Notice, PageHeader, type AdminColumn } from "../components"; import { Button, Surface, Tag } from "../../components/primitives"; +import { Icon } from "../../components/Icon"; +import { PLATFORM_FIELDS, PLATFORM_GUIDE } from "./channelFields"; /** Platform part of an adapter name ("kook:conf0" → "kook"). */ function platformOf(name: string): string { return name.split(":")[0]; } -/** One curated config input for a platform. `key` is the config-map JSON key - * the backend decodes (see plugins/channels/* adapter Config json tags). */ -interface PlatformField { - key: string; - label: string; - placeholder?: string; - /** Render as a masked [type=password] input (token / verify / encrypt / secret). */ - secret?: boolean; -} - -/** Platform-specific config fields shown for a platform. The COMMON fields - * (account id, token/bot_token, baseURL/base_url, allow/deny lists, flow limits) - * are rendered for every platform separately; this map holds only what a given - * adapter additionally needs. Keys are the adapter Config json tags verbatim. */ -const PLATFORM_FIELDS: Record = { - // KOOK is "token 即用、其余自动获取": only the token is required. base_url - // defaults to the official root, receive_mode defaults to "ws" and webhook_path - // to "/webhook/kook", so none of them need a form field. Advanced values - // (receive_mode="webhook", webhook_path, webhook_verify_token, webhook_encrypt_key) - // are set through the "额外 config" JSON instead. We keep the (empty) entry so - // the panel still renders the KOOK-specific hint below. - kook: [], - "qq-obv11": [ - { key: "mode", label: "mode(client/server)", placeholder: "server" }, - { key: "listen_addr", label: "listen_addr(server 监听)", placeholder: "0.0.0.0:6700" }, - { key: "remote_url", label: "remote_url(client 连接)", placeholder: "ws://…" }, - { key: "access_token", label: "access_token(OneBot 访问 token)", secret: true }, - { key: "self_id", label: "self_id(机器人 QQ 号)", placeholder: "123456789" }, - ], - telegram: [{ key: "webhook_url", label: "webhook_url(公网回调地址)", placeholder: "https://…" }], - "weixin-ilink": [ - { key: "cdn_base_url", label: "cdn_base_url(媒体 CDN)", placeholder: "https://…" }, - { key: "bot_id", label: "bot_id(ilink_bot_id)", placeholder: "wx…" }, - { key: "owner_id", label: "owner_id(绑定主人 uid)", placeholder: "wx…" }, - { key: "cursor", label: "cursor(get_updates_buf)", placeholder: "…" }, - ], - webhook: [ - { key: "endpointPath", label: "endpointPath(挂载路由)", placeholder: "/webhook" }, - { key: "channelId", label: "channelId(渠道 id)", placeholder: "…" }, - { key: "channelName", label: "channelName(渠道名)", placeholder: "…" }, - { key: "secret", label: "secret(HMAC 签名密钥)", secret: true }, - { key: "signatureHeader", label: "signatureHeader(签名头)", placeholder: "X-Webhook-Signature" }, - ], -}; - export function ChannelsModule(): React.ReactNode { const { data, loading, error, reload } = useAsyncData(adminListChannels, []); useAutoReload(reload, 10000); @@ -94,6 +51,11 @@ export function ChannelsModule(): React.ReactNode { const [msgPerMin, setMsgPerMin] = useState(""); const [maxMessages, setMaxMessages] = useState(""); const [extraConfig, setExtraConfig] = useState(""); + // Advanced-options fold; default closed (values persist in state regardless). + const [advOpen, setAdvOpen] = useState(false); + // Business-policy fold (blacklist/allow + rate limits) is separate from the + // connection config — it governs admission/de-noise, not the endpoint. + const [polOpen, setPolOpen] = useState(false); const toggle = (name: string, connect: boolean) => { setBusy(name); @@ -157,6 +119,8 @@ export function ChannelsModule(): React.ReactNode { setMsgPerMin(cfg.msgPerMin == null ? "" : String(cfg.msgPerMin)); setMaxMessages(cfg.maxMessages == null ? "" : String(cfg.maxMessages)); setExtraConfig(""); + setAdvOpen(false); // fold stays closed on open; advanced values are prefilled. + setPolOpen(false); // business-policy fold also starts closed. }; const save = () => { @@ -165,10 +129,8 @@ export function ChannelsModule(): React.ReactNode { setErr(null); setNote(null); const toList = (s: string) => s.split(/[\n,]/).map((x) => x.trim()).filter(Boolean); - // Curate this platform's specific config keys (e.g. kook receive_mode / - // webhook_path, qq mode / listen_addr), then let an explicit extraConfig JSON - // override any of them. Keys are the adapter Config json tags the backend - // DecodeConfig reads, so the saved config lands in the right struct fields. + // Curate platform-specific config keys, then let an explicit extraConfig JSON + // override any of them; these json tags are what the backend DecodeConfig reads. const specificMap: Record = {}; for (const f of PLATFORM_FIELDS[cfgPlatform] ?? []) { const v = (platVals[f.key] ?? "").trim(); @@ -333,90 +295,92 @@ export function ChannelsModule(): React.ReactNode { {cfgPlatform ? (

配置账号 → {cfgPlatform}

- {cfgPlatform === "kook" ? ( -

- KOOK 为「token 即用、其余自动获取」:仅需填 token(必填);base_url 留空即用官方默认;receive_mode 默认 ws、webhook_path 默认 /webhook/kook,均无需填写。若改用 webhook 或配校验/加密,在下方「额外 config」JSON 里设置,如 - {" "} - {"{"}"receive_mode":"webhook","webhook_path":"/webhook/kook","webhook_verify_token":"...","webhook_encrypt_key":"..."{"}"} - (verify_token / encrypt_key 仅在 webhook 模式需要)。连接后渠道列表会显示该 KOOK bot 身份。 -

- ) : null}

保存后写入 channels.json(本地、已 gitignore)并热更新 {cfgPlatform} 的渠道 adapter,无需重启进程。token 只在写文件时出现,列表/响应不回显裸值。

-
- - - - {(PLATFORM_FIELDS[cfgPlatform] ?? []).map((f) => ( -