feat(channels): 其它适配器专属通用壳(design/17 §2)
- 新增 GenericAdapterPanel.tsx: 非KOOK平台专属壳(平台标签+能力徽标+平台专属功能区诚实占位+ 主动推送能力提示),读 record.accounts[].capabilities,未实装区标「待实现」不伪造数据 - 新增 channelCapabilities.ts: 能力标签元数据+辅助(friends/user_chats/channels/guilds/ message_test 与 Markdown/Image/Voice/Reaction/TypingHint/Proactive 中文映射;未知回退原值) - AdapterDetailPage.tsx: platform==='kook' 渲染 KookPanel, 否则渲染 GenericAdapterPanel - 前向兼容: 后端后续为其它平台补 capabilities 声明即点亮; 零新依赖; 移动端flex-wrap/overflow - 验收: npm run build(tsc+vite) 99模块全绿; 文件均<500行
This commit is contained in:
1 parent
5ec4c9814d
commit
c43b2f1d5d
3 files changed
+167
-2
No files matched your search
@@ -7,6 +7,7 @@ import { PLATFORM_LABEL } from "../channelFields";
|
||||
import { ChannelLogList } from "./channelLogList";
|
||||
import { ChannelIdentityTest } from "./channelIdentityTest";
|
||||
import { KookPanel } from "./KookPanel";
|
||||
import { GenericAdapterPanel } from "./GenericAdapterPanel";
|
||||
import { platformOf } from "./channelUtils";
|
||||
import { useChannelActions } from "./useChannelActions";
|
||||
|
||||
@@ -35,7 +36,7 @@ export function AdapterDetailPage({ adapter, onBack }: { adapter: string; onBack
|
||||
subtitle={
|
||||
platform === "kook"
|
||||
? "该配置专属页 · 连接状态 + KOOK 平台能力(好友/频道/私信/测试)+ 实时日志"
|
||||
: "该配置专属页 · 实时日志 + 功能操作(完整配置字段见 P2)"
|
||||
: "该配置专属页 · 实时日志 + 功能操作 + 平台能力壳(专属功能后续接入)"
|
||||
}
|
||||
actions={
|
||||
<Button size="sm" variant="ghost" disabled={busy !== null} onClick={onBack}>
|
||||
@@ -74,7 +75,11 @@ export function AdapterDetailPage({ adapter, onBack }: { adapter: string; onBack
|
||||
|
||||
<ChannelIdentityTest defaultPlatform={platform} disabled={busy !== null} />
|
||||
|
||||
{platform === "kook" ? <KookPanel platform={platform} accountId={accountId} /> : null}
|
||||
{platform === "kook" ? (
|
||||
<KookPanel platform={platform} accountId={accountId} />
|
||||
) : (
|
||||
<GenericAdapterPanel platform={platform} accountId={accountId} record={record} />
|
||||
)}
|
||||
|
||||
<div className="rounded-[var(--mu-radius-lg)] bg-[var(--mu-surface-3)] border border-[var(--mu-glass-border)] shadow-[var(--mu-shadow-1)] p-5">
|
||||
<ChannelLogList adapter={adapter} limit={200} />
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { AdminChannelRecord } from "../../../lib/types";
|
||||
import { Notice } from "../../components";
|
||||
import { Surface, Tag } from "../../../components/primitives";
|
||||
import { PLATFORM_LABEL } from "../channelFields";
|
||||
import { adminOpsOf, capabilityMeta, hasCapability } from "./channelCapabilities";
|
||||
|
||||
/** Generic per-adapter shell for platforms without a specialised panel (design/17
|
||||
* §2). KOOK gets KookPanel; every other platform renders this honest shell. It
|
||||
* shows the platform label + account, the capabilities the adapter declares
|
||||
* (from `record.accounts[].capabilities`), a placeholder for platform-specific
|
||||
* feature areas (still to be wired), and a proactive-push note. It never
|
||||
* fabricates data, buttons, or friend/channel lists — non-KOOK platforms have no
|
||||
* backend /view endpoint yet, so the page only displays capability + placeholders
|
||||
* + the generic connect/disconnect/log/identity actions rendered above it. */
|
||||
export function GenericAdapterPanel({
|
||||
platform,
|
||||
accountId,
|
||||
record,
|
||||
}: {
|
||||
platform: string;
|
||||
accountId?: string;
|
||||
record?: AdminChannelRecord;
|
||||
}): React.ReactNode {
|
||||
const label = PLATFORM_LABEL[platform] ?? platform;
|
||||
const account = record?.accounts?.find((a) =>
|
||||
accountId === undefined ? (!a.id || a.id === "default") : a.id === accountId,
|
||||
);
|
||||
const caps = account?.capabilities ?? [];
|
||||
const accountName =
|
||||
account?.botNickname ||
|
||||
(account?.botUsername ? `@${account.botUsername}` : accountId ?? "default");
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Tag tone="accent">{label} 平台能力</Tag>
|
||||
<Tag tone="neutral">{accountName}</Tag>
|
||||
<span className="ml-auto text-xs text-[var(--mu-text-lo)]">通用壳 · 平台专属端点未接入</span>
|
||||
</div>
|
||||
|
||||
<CapabilityBadges caps={caps} />
|
||||
<FeaturePlaceholder caps={caps} />
|
||||
<ProactiveNote caps={caps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Capability badges: what this adapter declares it supports. Empty → honest note. */
|
||||
function CapabilityBadges({ caps }: { caps: readonly string[] }): React.ReactNode {
|
||||
return (
|
||||
<Surface gap="sm" className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">平台能力</h3>
|
||||
{caps.length === 0 ? (
|
||||
<Notice>该适配器尚未声明能力列表;平台专属端点未接入,当前仅提供通用操作(连接 / 断开 / 消息日志 / 身份测试)。</Notice>
|
||||
) : (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{caps.map((c) => {
|
||||
const m = capabilityMeta(c);
|
||||
return (
|
||||
<Tag key={c} tone={m.tone}>
|
||||
{m.label}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
/** Placeholder for future platform-specific feature areas (friends/channels/members/
|
||||
* test-send). Only the admin-operations the adapter actually declares surface here,
|
||||
* each marked as not yet wired. */
|
||||
function FeaturePlaceholder({ caps }: { caps: readonly string[] }): React.ReactNode {
|
||||
const adminOps = adminOpsOf(caps);
|
||||
return (
|
||||
<Surface gap="sm" className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">平台专属功能区(占位)</h3>
|
||||
{adminOps.length === 0 ? (
|
||||
<Notice>该平台暂无已声明的专属功能模块。频道 / 成员 / 发送测试等平台能力将随后续接入后在此显示。</Notice>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{adminOps.map((op) => (
|
||||
<Tag key={op} tone={capabilityMeta(op).tone}>
|
||||
{capabilityMeta(op).label} · 后续接入
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
<Notice tone="warn">以上模块为占位展示,对应后端专属端点尚未接入,当前不提供真实数据 / 操作。</Notice>
|
||||
</>
|
||||
)}
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
|
||||
/** Proactive-push note: declared → placeholder; otherwise an honest "not supported". */
|
||||
function ProactiveNote({ caps }: { caps: readonly string[] }): React.ReactNode {
|
||||
const supported = hasCapability(caps, "Proactive");
|
||||
return (
|
||||
<Surface gap="sm" className="space-y-3">
|
||||
<h3 className="text-sm font-semibold">主动推送</h3>
|
||||
{supported ? (
|
||||
<Notice tone="warn">主动推送(占位,后端待接):该适配器声明了主动推送能力,定时 / 主动消息功能待后端接入。</Notice>
|
||||
) : (
|
||||
<Notice>当前适配器未声明主动推送能力(Proactive),不支持主动 / 定时消息;仅在被对话触发时响应。</Notice>
|
||||
)}
|
||||
</Surface>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { TagTone } from "../../../components/primitives";
|
||||
|
||||
/** Per-account capability names the admin panel may receive (design/17 §2).
|
||||
*
|
||||
* Two flavours arrive over the wire (both as a string[]):
|
||||
* - account-level admin operations a platform adapter exposes (kook reports
|
||||
* friends/user_chats/channels/guilds/message_test; other platforms report none yet);
|
||||
* - channel *transport* capabilities from the adapter (Markdown / Image / Voice /
|
||||
* Reaction / TypingHint / Proactive).
|
||||
*
|
||||
* The generic shell renders only the names the backend actually reports: an
|
||||
* unknown name falls back to a neutral tag with its raw value, never a
|
||||
* fabricated label (red line: no invented data). */
|
||||
export type CapabilityKind = "adminOp" | "channelCap";
|
||||
|
||||
export interface CapabilityMeta {
|
||||
label: string;
|
||||
tone: TagTone;
|
||||
kind: CapabilityKind;
|
||||
}
|
||||
|
||||
const KNOWN: Record<string, CapabilityMeta> = {
|
||||
friends: { label: "好友", tone: "accent", kind: "adminOp" },
|
||||
user_chats: { label: "私信会话", tone: "accent", kind: "adminOp" },
|
||||
channels: { label: "频道", tone: "accent", kind: "adminOp" },
|
||||
guilds: { label: "服务器", tone: "accent", kind: "adminOp" },
|
||||
message_test: { label: "发消息测试", tone: "ok", kind: "adminOp" },
|
||||
Proactive: { label: "主动推送", tone: "warn", kind: "channelCap" },
|
||||
Markdown: { label: "Markdown 渲染", tone: "neutral", kind: "channelCap" },
|
||||
Image: { label: "图片", tone: "neutral", kind: "channelCap" },
|
||||
Voice: { label: "语音", tone: "neutral", kind: "channelCap" },
|
||||
Reaction: { label: "表情回应", tone: "neutral", kind: "channelCap" },
|
||||
TypingHint: { label: "输入提示", tone: "neutral", kind: "channelCap" },
|
||||
};
|
||||
|
||||
/** Metadata for a capability name; unknown names keep their raw value. */
|
||||
export function capabilityMeta(name: string): CapabilityMeta {
|
||||
return KNOWN[name] ?? { label: name, tone: "neutral", kind: "channelCap" };
|
||||
}
|
||||
|
||||
/** The account-level admin operations present in a capability list, preserving the
|
||||
* order the backend reports (empty for platforms without a specialized surface). */
|
||||
export function adminOpsOf(caps: readonly string[]): string[] {
|
||||
return caps.filter((c) => capabilityMeta(c).kind === "adminOp");
|
||||
}
|
||||
|
||||
/** True when the capability list declares the given name. */
|
||||
export function hasCapability(caps: readonly string[], name: string): boolean {
|
||||
return caps.includes(name);
|
||||
}
|
||||
Reference in new issue
Block a user