feat(admin): 插件能力页按类型分组展示 — 工具/技能/渠道分开,渠道不再显示工具策略开关
- GET /admin/plugins 后端仍平铺返回(契约不变), 前端 Plugins.tsx 按 kind 分组渲染 - 工具+技能: 保留「群聊/陌生人」环境授权开关(ToolPolicy, owner可编辑)+卸载; 渠道: 仅名称/描述/『见渠道管理页』, 无策略开关(渠道走自身配置) - 渠道块文案tone=neutral, 工具技能accent; Miuix 原语复用
This commit is contained in:
1 parent
2dee372fb2
commit
f28995c1e5
1 file changed
+72
-16
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useAsyncData } from "../../lib/useAsyncData";
|
||||
import { adminListPlugins, adminSetPluginPolicy, adminPluginUninstall, getStoredRole } from "../../lib/api";
|
||||
import type { AdminPluginRecord } from "../../lib/types";
|
||||
@@ -8,7 +8,26 @@ import { AdminTable, Notice, PageHeader, type AdminColumn } from "../components"
|
||||
import { Button, Surface, Switch, Tag } from "../../components/primitives";
|
||||
import { InstalledPluginsPanel } from "./plugins/InstalledPlugins";
|
||||
|
||||
/** Columns for the registered-capability table (GET /admin/plugins). */
|
||||
type CapKind = "tool" | "skill" | "channel";
|
||||
|
||||
const KIND_LABELS: Record<CapKind, string> = {
|
||||
tool: "工具 Tool",
|
||||
skill: "技能 Skill",
|
||||
channel: "渠道 Channel",
|
||||
};
|
||||
|
||||
// Channel adapters are not gated by the tool policy (group/stranger markers are a
|
||||
// tool concept; a channel is configured via its own 渠道管理 page), so the
|
||||
// channel block shows identity + a pointer rather than the policy switches.
|
||||
function channelColumns(): AdminColumn<AdminPluginRecord>[] {
|
||||
return [
|
||||
{ key: "name", title: "名称", render: (p) => <span className="font-medium">{p.name}</span> },
|
||||
{ key: "desc", title: "描述", render: (p) => <span className="block max-w-md truncate text-[var(--mu-text-mid)]">{p.description || "—"}</span> },
|
||||
{ key: "admin", title: "配置", render: () => <span className="text-xs text-[var(--mu-text-lo)]">渠道配置见「渠道管理」页</span> },
|
||||
];
|
||||
}
|
||||
|
||||
/** Columns for a tool / skill row (has environment markers, owner can edit). */
|
||||
function registeredColumns(
|
||||
isOwner: boolean,
|
||||
busy: string | null,
|
||||
@@ -17,7 +36,7 @@ function registeredColumns(
|
||||
): AdminColumn<AdminPluginRecord>[] {
|
||||
return [
|
||||
{ key: "name", title: "名称", render: (p) => <span className="font-medium">{p.name}</span> },
|
||||
{ key: "kind", title: "类型", render: (p) => <Tag tone="accent">{p.kind}</Tag> },
|
||||
{ key: "kind", title: "类型", render: (p) => <Tag tone="accent">{(KIND_LABELS as Record<string, string>)[p.kind] ?? 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",
|
||||
@@ -97,13 +116,23 @@ export function PluginsModule(): React.ReactNode {
|
||||
.finally(() => setBusy(null));
|
||||
};
|
||||
|
||||
const columns = registeredColumns(isOwner, busy, setPolicy, uninstall);
|
||||
const byKind = useMemo(() => {
|
||||
const groups: Record<CapKind, AdminPluginRecord[]> = { tool: [], skill: [], channel: [] };
|
||||
for (const cap of data ?? []) {
|
||||
const k = cap.kind as CapKind;
|
||||
if (groups[k]) groups[k].push(cap);
|
||||
}
|
||||
return groups;
|
||||
}, [data]);
|
||||
|
||||
const toolSkillColumns = registeredColumns(isOwner, busy, setPolicy, uninstall);
|
||||
const channelCols = channelColumns();
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<PageHeader
|
||||
title="插件与能力"
|
||||
subtitle="GET /api/v1/admin/plugins · 已注册能力 + 运行时 MCP 安装/卸载 · POST /api/v1/admin/plugins/policy (owner-only)"
|
||||
subtitle="已注册能力(按工具 / 技能 / 渠道分组)+ 运行时 MCP 安装/卸载 · POST /api/v1/admin/plugins/policy (owner-only)"
|
||||
/>
|
||||
|
||||
{!isOwner ? <Notice tone="warn">当前角色为 {getStoredRole()},工具策略/插件安装仅 owner 可写(读取/查看可见)。</Notice> : null}
|
||||
@@ -112,22 +141,49 @@ export function PluginsModule(): React.ReactNode {
|
||||
|
||||
<InstalledPluginsPanel />
|
||||
|
||||
<Surface className="space-y-2">
|
||||
<Surface className="space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="text-sm font-semibold">已注册能力</h3>
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">GET /api/v1/admin/plugins</span>
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">GET /api/v1/admin/plugins · 按工具 / 技能 / 渠道分组</span>
|
||||
</div>
|
||||
|
||||
{(["tool", "skill"] as CapKind[]).map((kind) => {
|
||||
const rows = byKind[kind];
|
||||
return (
|
||||
<div key={kind} className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<Tag tone="accent">{KIND_LABELS[kind]}</Tag>
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">{rows.length} 个</span>
|
||||
</div>
|
||||
<AdminTable
|
||||
columns={toolSkillColumns}
|
||||
rows={loading ? null : rows}
|
||||
loading={loading}
|
||||
error={error}
|
||||
emptyTitle={`无${KIND_LABELS[kind]}`}
|
||||
rowKey={(p) => `${kind}:${p.name}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<Tag tone="neutral">{KIND_LABELS.channel}</Tag>
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">{byKind.channel.length} 个</span>
|
||||
</div>
|
||||
<AdminTable
|
||||
columns={channelCols}
|
||||
rows={loading ? null : byKind.channel}
|
||||
loading={loading}
|
||||
error={error}
|
||||
emptyTitle="无渠道"
|
||||
rowKey={(p) => `channel:${p.name}`}
|
||||
/>
|
||||
</div>
|
||||
<AdminTable
|
||||
columns={columns}
|
||||
rows={loading ? null : data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
emptyTitle="无插件数据"
|
||||
rowKey={(p) => p.name}
|
||||
/>
|
||||
</Surface>
|
||||
|
||||
<Notice>工具 schema / 技能为后端 M10+ 提供;此页展示已注册能力及其环境可见性标记(群聊 / 陌生人默认关闭)。</Notice>
|
||||
<Notice>工具 schema / 技能为后端 M10+ 提供;此页按类型分组展示已注册能力。群聊/陌生人环境可见性标记(默认关闭)仅对工具/技能生效。</Notice>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user