diff --git a/frontend/web/src/admin/modules/Channels.tsx b/frontend/web/src/admin/modules/Channels.tsx index 902b68d..522febc 100644 --- a/frontend/web/src/admin/modules/Channels.tsx +++ b/frontend/web/src/admin/modules/Channels.tsx @@ -19,6 +19,50 @@ 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: [ + { key: "receive_mode", label: "receive_mode(收包模式 ws/webhook)", placeholder: "ws" }, + { key: "webhook_path", label: "webhook_path(webhook 挂载路由)", placeholder: "/webhook/kook" }, + { key: "webhook_verify_token", label: "webhook_verify_token(校验 token)", secret: true }, + { key: "webhook_encrypt_key", label: "webhook_encrypt_key(AES 加密密钥)", secret: true }, + ], + "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); @@ -37,9 +81,10 @@ export function ChannelsModule(): React.ReactNode { const [cfgId, setCfgId] = useState(""); const [token, setToken] = useState(""); const [baseURL, setBaseURL] = useState(""); - const [remoteURL, setRemoteURL] = useState(""); - const [listenAddr, setListenAddr] = useState(""); - const [mode, setMode] = useState(""); + // Platform-specific config values keyed by the adapter Config json tag + // (receive_mode/webhook_path/mode/listen_addr/webhook_url/... ). Rendered only + // for the currently selected platform's fields. + const [platVals, setPlatVals] = useState>({}); const [allowGroups, setAllowGroups] = useState(""); const [denyGroups, setDenyGroups] = useState(""); const [allowUsers, setAllowUsers] = useState(""); @@ -91,9 +136,7 @@ export function ChannelsModule(): React.ReactNode { setCfgId(""); setToken(""); setBaseURL(""); - setRemoteURL(""); - setListenAddr(""); - setMode(""); + setPlatVals({}); setAllowGroups(""); setDenyGroups(""); setAllowUsers(""); @@ -110,23 +153,38 @@ 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. + const specificMap: Record = {}; + for (const f of PLATFORM_FIELDS[cfgPlatform] ?? []) { + const v = (platVals[f.key] ?? "").trim(); + if (v) specificMap[f.key] = v; + } let configMap: Record | undefined; if (extraConfig.trim()) { + let extra: unknown; try { - configMap = JSON.parse(extraConfig); + extra = JSON.parse(extraConfig); } catch { setErr("额外配置 JSON 格式有误"); setBusy(null); return; } + if (typeof extra !== "object" || extra === null || Array.isArray(extra)) { + setErr("额外配置 JSON 需为对象(如 {\"receive_mode\":\"ws\"})"); + setBusy(null); + return; + } + configMap = { ...specificMap, ...(extra as Record) }; + } else if (Object.keys(specificMap).length) { + configMap = specificMap; } adminSaveChannelConfig(cfgPlatform, { id: cfgId.trim() || undefined, token: token.trim() || undefined, baseURL: baseURL.trim() || undefined, - remoteURL: remoteURL.trim() || undefined, - listenAddr: listenAddr.trim() || undefined, - mode: mode.trim() || undefined, config: configMap, allowGroups: toList(allowGroups), denyGroups: toList(denyGroups), @@ -142,9 +200,7 @@ export function ChannelsModule(): React.ReactNode { setCfgId(""); setToken(""); setBaseURL(""); - setRemoteURL(""); - setListenAddr(""); - setMode(""); + setPlatVals({}); setAllowGroups(""); setDenyGroups(""); setAllowUsers(""); @@ -270,24 +326,24 @@ export function ChannelsModule(): React.ReactNode { - - - + {(PLATFORM_FIELDS[cfgPlatform] ?? []).map((f) => ( + + ))}