feat(web): 渠道配置表单排版+引导——核心必填(token)+高级折叠+业务策略独立区
agent-118 排版引导: - 核心必填区: 高亮 Callout(各平台'只需填X'头条)+Token必填badge(红底)+自动获取旁注, 不再藏段落 - 高级选项折叠(Miuix arrow折叠): base_url/平台特定字段/额外 config(连接配置), 收起默认 - 各平台引导头条: kook'只需填bot_token其余自动获取'/telegram/qq(mode接入方式)/weixin/webhook 我补(主人原则: 连接配置 vs 业务策略分开): - 黑白名单(allow/deny groups/users)+限流(groupMsgPerMin/msgPerMin/maxMessages) 从'高级选项'抽离成独立'业务策略'折叠区(不参与端到端连接) - openConfig 重置 polOpen; 业务策略默认收起 - 拆 channelFields.ts(PlatformField/PLATFORM_FIELDS/PLATFORM_GUIDE) → Channels.tsx 454行<500红线合规 前端 build 绿; 敏感字段password掩码/不回显
This commit is contained in:
1 parent
65091f6497
commit
5398486fdf
2 files changed
+186
-126
No files matched your search
@@ -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<string, PlatformField[]> = {
|
||||
// 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<string, unknown> = {};
|
||||
for (const f of PLATFORM_FIELDS[cfgPlatform] ?? []) {
|
||||
const v = (platVals[f.key] ?? "").trim();
|
||||
@@ -333,90 +295,92 @@ export function ChannelsModule(): React.ReactNode {
|
||||
{cfgPlatform ? (
|
||||
<Surface className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">配置账号 → {cfgPlatform}</h3>
|
||||
{cfgPlatform === "kook" ? (
|
||||
<p className="text-xs text-[var(--mu-text-lo)]">
|
||||
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 身份。
|
||||
</p>
|
||||
) : null}
|
||||
<p className="text-xs text-[var(--mu-text-lo)]">
|
||||
保存后写入 channels.json(本地、已 gitignore)并热更新 {cfgPlatform} 的渠道 adapter,无需重启进程。token 只在写文件时出现,列表/响应不回显裸值。
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">账号 ID(留空自动生成)</span>
|
||||
<input value={cfgId} onChange={(e) => setCfgId(e.target.value)} placeholder="conf0" className={inputCls} />
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">Token(bot_token)</span>
|
||||
<input type="password" value={token} onChange={(e) => setToken(e.target.value)} placeholder="机器人 token" className={inputCls} />
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">baseURL(地址)</span>
|
||||
<input value={baseURL} onChange={(e) => setBaseURL(e.target.value)} placeholder="https://…" className={inputCls} />
|
||||
</label>
|
||||
{(PLATFORM_FIELDS[cfgPlatform] ?? []).map((f) => (
|
||||
<label key={f.key} className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">{f.label}</span>
|
||||
<input
|
||||
type={f.secret ? "password" : "text"}
|
||||
value={platVals[f.key] ?? ""}
|
||||
onChange={(e) => setPlatVals((prev) => ({ ...prev, [f.key]: e.target.value }))}
|
||||
placeholder={f.placeholder}
|
||||
className={inputCls}
|
||||
/>
|
||||
|
||||
{/* 核心必填:最小必备字段 + 醒目 Callout / 高亮 Tag,不平铺全量 */}
|
||||
<div className="space-y-2 rounded-[var(--mu-radius)] border border-[var(--mu-accent)]/25 bg-[var(--mu-surface-4)] p-3">
|
||||
<div className="flex items-center gap-1.5 text-sm font-semibold text-[var(--mu-accent-hi)]">
|
||||
<Icon name="check" size={16} className="text-[var(--mu-accent)]" />
|
||||
核心必填
|
||||
</div>
|
||||
<div className="rounded-[var(--mu-radius)] bg-[var(--mu-accent)]/15 px-3 py-2 text-xs leading-relaxed text-[var(--mu-accent-hi)]">
|
||||
{PLATFORM_GUIDE[cfgPlatform].guide}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<Field label="账号 ID(留空自动生成)" value={cfgId} onChange={setCfgId} placeholder="conf0" />
|
||||
<label className="block space-y-1">
|
||||
<span className="flex items-center gap-1.5 text-xs text-[var(--mu-text-lo)]">
|
||||
Token(bot_token)
|
||||
{PLATFORM_GUIDE[cfgPlatform].tokenRequired !== false ? (
|
||||
<span className="rounded-[var(--mu-radius-xs)] bg-[var(--mu-err)]/15 px-1.5 py-0.5 text-[10px] font-medium text-[var(--mu-err)]">必填</span>
|
||||
) : null}
|
||||
</span>
|
||||
<input type="password" value={token} onChange={(e) => setToken(e.target.value)} placeholder="机器人 token" className={`${inputCls} border border-[var(--mu-accent)]/25`} />
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{PLATFORM_GUIDE[cfgPlatform].hint ? (
|
||||
<p className="text-xs leading-relaxed text-[var(--mu-text-lo)]">{PLATFORM_GUIDE[cfgPlatform].hint}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">allowGroups(逗号分隔白名单)</span>
|
||||
<input value={allowGroups} onChange={(e) => setAllowGroups(e.target.value)} className={inputCls} />
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">denyGroups(黑名单)</span>
|
||||
<input value={denyGroups} onChange={(e) => setDenyGroups(e.target.value)} className={inputCls} />
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">allowUsers(白名单)</span>
|
||||
<input value={allowUsers} onChange={(e) => setAllowUsers(e.target.value)} className={inputCls} />
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">denyUsers(黑名单)</span>
|
||||
<input value={denyUsers} onChange={(e) => setDenyUsers(e.target.value)} className={inputCls} />
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">groupMsgPerMin(群聊限流,≤0 关闭)</span>
|
||||
<input value={groupMsgPerMin} onChange={(e) => setGroupMsgPerMin(e.target.value)} inputMode="numeric" className={inputCls} />
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">msgPerMin</span>
|
||||
<input value={msgPerMin} onChange={(e) => setMsgPerMin(e.target.value)} inputMode="numeric" className={inputCls} />
|
||||
</label>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">maxMessages</span>
|
||||
<input value={maxMessages} onChange={(e) => setMaxMessages(e.target.value)} inputMode="numeric" className={inputCls} />
|
||||
</label>
|
||||
|
||||
{/* 高级选项(连接配置):base_url / 平台特定字段 / 额外 config。业务策略单独一区。 */}
|
||||
<div className="overflow-hidden rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-2)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAdvOpen((v) => !v)}
|
||||
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs font-medium text-[var(--mu-text-mid)] transition-colors duration-200 hover:bg-[var(--mu-surface-4)]/40"
|
||||
>
|
||||
<span className={`shrink-0 transition-transform duration-200 ease-[var(--mu-spring)] ${advOpen ? "rotate-90" : ""}`}>
|
||||
<Icon name="arrow" size={13} className="text-[var(--mu-text-lo)]" />
|
||||
</span>
|
||||
高级选项(base_url / 平台特定字段 / 额外 config)
|
||||
</button>
|
||||
{advOpen ? (
|
||||
<div className="space-y-3 px-3 pb-3 pt-1">
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<Field label="baseURL(地址)" value={baseURL} onChange={setBaseURL} placeholder="https://…" />
|
||||
{(PLATFORM_FIELDS[cfgPlatform] ?? []).map((f) => (
|
||||
<Field key={f.key} label={f.label} value={platVals[f.key] ?? ""} onChange={(v) => setPlatVals((prev) => ({ ...prev, [f.key]: v }))} placeholder={f.placeholder} secret={f.secret} />
|
||||
))}
|
||||
</div>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">额外 config(可选 JSON,覆盖平台特有字段如 webhook_url/endpointPath/secret)</span>
|
||||
<textarea value={extraConfig} onChange={(e) => setExtraConfig(e.target.value)} rows={2} placeholder='{"webhook_url":"…"}' className="w-full rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-[var(--mu-accent)]" />
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">额外 config(可选 JSON,覆盖平台特有字段如 webhook_url/endpointPath/secret)</span>
|
||||
<textarea
|
||||
value={extraConfig}
|
||||
onChange={(e) => setExtraConfig(e.target.value)}
|
||||
rows={2}
|
||||
placeholder='{"webhook_url":"…"}'
|
||||
className="w-full rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-[var(--mu-accent)]"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* 业务策略(独立于连接配置):黑白名单 / 限流——管准入与降噪,不参与端到端连接。 */}
|
||||
<div className="overflow-hidden rounded-[var(--mu-radius)] border border-[var(--mu-glass-border)] bg-[var(--mu-surface-2)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPolOpen((v) => !v)}
|
||||
className="flex w-full items-center gap-2 px-3 py-2.5 text-xs font-medium text-[var(--mu-text-mid)] transition-colors duration-200 hover:bg-[var(--mu-surface-4)]/40"
|
||||
>
|
||||
<span className={`shrink-0 transition-transform duration-200 ease-[var(--mu-spring)] ${polOpen ? "rotate-90" : ""}`}>
|
||||
<Icon name="arrow" size={13} className="text-[var(--mu-text-lo)]" />
|
||||
</span>
|
||||
业务策略(黑白名单 / 群聊限流——不参与连接配置)
|
||||
</button>
|
||||
{polOpen ? (
|
||||
<div className="grid grid-cols-1 gap-2 px-3 pb-3 pt-1 sm:grid-cols-2">
|
||||
<Field label="allowGroups(逗号分隔白名单)" value={allowGroups} onChange={setAllowGroups} />
|
||||
<Field label="denyGroups(黑名单)" value={denyGroups} onChange={setDenyGroups} />
|
||||
<Field label="allowUsers(白名单)" value={allowUsers} onChange={setAllowUsers} />
|
||||
<Field label="denyUsers(黑名单)" value={denyUsers} onChange={setDenyUsers} />
|
||||
<Field label="groupMsgPerMin(群聊限流,≤0 关闭)" value={groupMsgPerMin} onChange={setGroupMsgPerMin} inputMode="numeric" />
|
||||
<Field label="msgPerMin" value={msgPerMin} onChange={setMsgPerMin} inputMode="numeric" />
|
||||
<Field label="maxMessages" value={maxMessages} onChange={setMaxMessages} inputMode="numeric" />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="primary" disabled={busy !== null} onClick={save}>
|
||||
保存配置
|
||||
</Button>
|
||||
<Button variant="ghost" disabled={busy !== null} onClick={() => setCfgPlatform("")}>
|
||||
取消
|
||||
</Button>
|
||||
<Button variant="primary" disabled={busy !== null} onClick={save}>保存配置</Button>
|
||||
<Button variant="ghost" disabled={busy !== null} onClick={() => setCfgPlatform("")}>取消</Button>
|
||||
</div>
|
||||
</Surface>
|
||||
) : null}
|
||||
@@ -462,3 +426,29 @@ export function ChannelsModule(): React.ReactNode {
|
||||
|
||||
const inputCls =
|
||||
"w-full rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-[var(--mu-accent)]";
|
||||
|
||||
/** Compact labeled input for the config form; `secret` masks the field. */
|
||||
interface FieldProps {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
placeholder?: string;
|
||||
secret?: boolean;
|
||||
inputMode?: "text" | "numeric";
|
||||
}
|
||||
|
||||
function Field({ label, value, onChange, placeholder, secret, inputMode = "text" }: FieldProps): React.ReactNode {
|
||||
return (
|
||||
<label className="block space-y-1">
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">{label}</span>
|
||||
<input
|
||||
type={secret ? "password" : "text"}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
inputMode={inputMode}
|
||||
className={inputCls}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/** Channel config field metadata + per-platform guidance (design/16 §4.4-4.6).
|
||||
* Separated from Channels.tsx so the module stays under the 500-line bound and a
|
||||
* platform only needs a new entry here. */
|
||||
|
||||
/** One curated config input; `key` is the adapter Config json tag the backend decodes. */
|
||||
export interface PlatformField {
|
||||
key: string;
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
/** Masked [type=password] input (token / verify / encrypt / secret). */
|
||||
secret?: boolean;
|
||||
}
|
||||
|
||||
/** Platform-specific config fields only (the COMMON fields are rendered separately).
|
||||
* Keys are the adapter Config json tags verbatim. */
|
||||
export const PLATFORM_FIELDS: Record<string, PlatformField[]> = {
|
||||
kook: [], // KOOK is "token 即用、其余自动获取"—advanced values go in the extra JSON.
|
||||
"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" },
|
||||
],
|
||||
};
|
||||
|
||||
/** Core-required guidance per platform: headline `guide` + auto-fetch `hint`. */
|
||||
export interface PlatformGuide {
|
||||
guide: string;
|
||||
hint?: string;
|
||||
tokenRequired?: boolean;
|
||||
}
|
||||
|
||||
export const PLATFORM_GUIDE: Record<string, PlatformGuide> = {
|
||||
kook: {
|
||||
guide: "只需填 bot_token(必填),其余自动获取",
|
||||
hint: "base_url 留空即用官方地址;receive_mode 默认 ws、webhook_path 默认 /webhook/kook,均无需填写;连接后渠道列表自动显示该 bot 身份。如需 webhook(receive_mode=webhook)或 webhook_verify_token / webhook_encrypt_key,在「高级选项 → 额外 config」里以 JSON 设置。",
|
||||
},
|
||||
"qq-obv11": {
|
||||
guide: "填 token(必填)并选择接入方式(mode:client / server)",
|
||||
hint: "mode=server 填 listen_addr(本地监听端口);mode=client 填 remote_url(连接上游)。mode / listen_addr / remote_url / access_token 在「高级选项」里配置。",
|
||||
},
|
||||
telegram: {
|
||||
guide: "只需填 bot_token(必填),其余自动获取",
|
||||
hint: "webhook_url(公网回调,可选)在「高级选项」里配置;留空则走长轮询。",
|
||||
},
|
||||
"weixin-ilink": {
|
||||
guide: "填 bot_token(必填)与 bot_id / owner_id",
|
||||
hint: "bot_id / owner_id / cdn_base_url / cursor 在「高级选项」里配置;媒体 CDN 可留空自动。",
|
||||
},
|
||||
webhook: {
|
||||
guide: "填 endpointPath 与 secret(必填)",
|
||||
hint: "endpointPath / secret / channelId / signatureHeader 在「高级选项」里配置,用于接收平台回调。",
|
||||
tokenRequired: false,
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user