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:
1 parent
4c48e94659
commit
f43eb17030
26 files changed
+2253
-4
No files matched your search
@@ -0,0 +1,99 @@
|
||||
import { useState } from "react";
|
||||
import { useAsyncData } from "../../lib/useAsyncData";
|
||||
import { adminListPlugins, adminSetPluginPolicy, getStoredRole } from "../../lib/api";
|
||||
import type { AdminPluginRecord } from "../../lib/types";
|
||||
import { useAutoReload } from "../hooks";
|
||||
import { useAuditStore } from "../audit";
|
||||
import { AdminTable, Notice, PageHeader, type AdminColumn } from "../components";
|
||||
import { Button, Surface, Switch, Tag } from "../../components/primitives";
|
||||
|
||||
export function PluginsModule(): React.ReactNode {
|
||||
const { data, loading, error, reload } = useAsyncData(adminListPlugins, []);
|
||||
useAutoReload(reload, 10000);
|
||||
const isOwner = getStoredRole() === "owner";
|
||||
const log = useAuditStore((s) => s.log);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
const [fail, setFail] = useState<string | null>(null);
|
||||
|
||||
const setPolicy = (name: string, allowInGroup: boolean, allowInStranger: boolean) => {
|
||||
setBusy(name);
|
||||
setFail(null);
|
||||
adminSetPluginPolicy(name, allowInGroup, allowInStranger)
|
||||
.then(() => {
|
||||
log(`policy.set:${name}`, "ok");
|
||||
reload();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
log(`policy.set:${name}`, "error");
|
||||
setFail(err instanceof Error ? err.message : "策略更新失败");
|
||||
})
|
||||
.finally(() => setBusy(null));
|
||||
};
|
||||
|
||||
const columns: AdminColumn<AdminPluginRecord>[] = [
|
||||
{ key: "name", title: "名称", render: (p) => <span className="font-medium">{p.name}</span> },
|
||||
{ key: "kind", title: "类型", render: (p) => <Tag tone="accent">{p.kind}</Tag> },
|
||||
{ key: "desc", title: "描述", render: (p) => <span className="block max-w-sm truncate text-[var(--mu-text-mid)]">{p.description || "—"}</span> },
|
||||
{
|
||||
key: "policy",
|
||||
title: "环境授权(owner)",
|
||||
render: (p) => (
|
||||
<span className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-1.5">
|
||||
<Switch
|
||||
checked={p.allowInGroup}
|
||||
disabled={!isOwner || busy === p.name}
|
||||
onChange={(v) => setPolicy(p.name, v, p.allowInStranger)}
|
||||
/>
|
||||
<span className="text-xs text-[var(--mu-text-mid)]">群聊</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5">
|
||||
<Switch
|
||||
checked={p.allowInStranger}
|
||||
disabled={!isOwner || busy === p.name}
|
||||
onChange={(v) => setPolicy(p.name, p.allowInGroup, v)}
|
||||
/>
|
||||
<span className="text-xs text-[var(--mu-text-mid)]">陌生人</span>
|
||||
</label>
|
||||
{busy === p.name ? <span className="text-xs text-[var(--mu-text-lo)]">保存中…</span> : null}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<PageHeader
|
||||
title="插件与能力"
|
||||
subtitle="GET /api/v1/admin/plugins · 启停/安装/卸载/MCP 连接 · POST /api/v1/admin/plugins/policy (owner-only)"
|
||||
actions={
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
log("plugin.install", "ok");
|
||||
alert("安装/卸载插件:后端 M10+ 提供,当前为占位操作。");
|
||||
}}
|
||||
>
|
||||
安装插件
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{!isOwner ? <Notice tone="warn">当前角色为 {getStoredRole()},工具策略仅 owner 可写(读取/查看可见)。</Notice> : null}
|
||||
{fail ? <Notice tone="err">策略更新失败:{fail}</Notice> : null}
|
||||
|
||||
<Surface className="space-y-2">
|
||||
<AdminTable
|
||||
columns={columns}
|
||||
rows={loading ? null : data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
emptyTitle="无插件数据"
|
||||
rowKey={(p) => p.name}
|
||||
/>
|
||||
</Surface>
|
||||
|
||||
<Notice>工具 schema / 技能 / MCP 服务器列表为后端 M10+ 提供;此页展示已注册能力及其环境可见性标记(群聊 / 陌生人默认关闭)。</Notice>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user