feat(frontend/admin): M10 后台管理面板(/admin hash 路由 + 15 大模块 + RBAC(owner/admin/viewer) + 审计 + Miuix);Docs/api-client.md v0.3 契约补全;tsc+vite build 通过

This commit is contained in:
Aska committed 2026-09-06 07:55:04 +08:00
1 parent 4c48e94659
commit f43eb17030
26 files changed
+2253 -4

No files matched your search

@@ -0,0 +1,97 @@
import { useState } from "react";
import { useAsyncData } from "../../lib/useAsyncData";
import { adminListChannels } from "../../lib/api";
import type { AdminChannelRecord } from "../../lib/types";
import { useAutoReload } from "../hooks";
import { useAuditStore } from "../audit";
import { AdminTable, Notice, PageHeader, type AdminColumn } from "../components";
import { Button, Surface, Tag } from "../../components/primitives";
const columns: AdminColumn<AdminChannelRecord>[] = [
{ key: "name", title: "渠道", render: (c) => <span className="font-medium">{c.name}</span> },
{
key: "connected",
title: "状态",
render: (c) => (
<Tag tone={c.connected ? "ok" : "err"}>
<span className={`mr-1 inline-block h-1.5 w-1.5 rounded-full ${c.connected ? "bg-[var(--mu-ok)]" : "bg-[var(--mu-err)]"}`} />
{c.connected ? "已连接" : "未连接"}
</Tag>
),
},
];
export function ChannelsModule(): React.ReactNode {
const { data, loading, error, reload } = useAsyncData(adminListChannels, []);
useAutoReload(reload, 10000);
const log = useAuditStore((s) => s.log);
const [ownerId, setOwnerId] = useState("");
const [group, setGroup] = useState(false);
const [verdict, setVerdict] = useState<string | null>(null);
return (
<div className="space-y-5">
<PageHeader
title="渠道管理"
subtitle="GET /api/v1/admin/channels · 类型/账号/状态/健康 · 身份识别基于 actor 元数据"
actions={
<Button
variant="soft"
onClick={() => {
log("channel.reconnect", "ok");
alert("热重载/重连(占位):后端 M10+ 提供。");
}}
>
热重载
</Button>
}
/>
<Surface className="space-y-2">
<AdminTable
columns={columns}
rows={loading ? null : data}
loading={loading}
error={error}
emptyTitle="无渠道数据"
rowKey={(c) => c.name}
/>
</Surface>
<Surface className="space-y-3">
<h3 className="text-sm font-semibold">身份识别测试(占位 · 策略引擎 M10 接入)</h3>
<p className="text-xs text-[var(--mu-text-lo)]">
输入渠道确认的发送者账号 id,模拟 channel 层 actor 元数据 → 策略引擎判定;判定绝不依赖提示词 / LLM 自我判断。
</p>
<div className="flex flex-wrap items-center gap-2">
<input
value={ownerId}
onChange={(e) => setOwnerId(e.target.value)}
placeholder="渠道账号 id(如 weixin-ilink 的 id)"
className="min-w-0 flex-1 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 className="flex items-center gap-1.5 text-xs text-[var(--mu-text-mid)]">
<input type="checkbox" checked={group} onChange={(e) => setGroup(e.target.checked)} className="accent-[var(--mu-accent)]" />
群聊
</label>
<Button
variant="primary"
onClick={() => {
log("channel.identity-test", "ok");
setVerdict(
group
? "群聊上下文 → 隔离;记忆只读不写;仅可调用标记允许的工具"
: "匹配主人白名单 → owner;若未命中则按陌生人处理(default deny)"
);
}}
>
测试
</Button>
</div>
{verdict ? <div className="rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-xs text-[var(--mu-text-mid)]">判定:{verdict}</div> : null}
</Surface>
<Notice>账号配置(扫码/token)、消息流预览、推送测试为后端 M10+ 提供;此页当前展示渠道注册与连接健康。</Notice>
</div>
);
}