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 { useAsyncData } from "../../lib/useAsyncData";
|
||||||
import { adminListPlugins, adminSetPluginPolicy, adminPluginUninstall, getStoredRole } from "../../lib/api";
|
import { adminListPlugins, adminSetPluginPolicy, adminPluginUninstall, getStoredRole } from "../../lib/api";
|
||||||
import type { AdminPluginRecord } from "../../lib/types";
|
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 { Button, Surface, Switch, Tag } from "../../components/primitives";
|
||||||
import { InstalledPluginsPanel } from "./plugins/InstalledPlugins";
|
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(
|
function registeredColumns(
|
||||||
isOwner: boolean,
|
isOwner: boolean,
|
||||||
busy: string | null,
|
busy: string | null,
|
||||||
@@ -17,7 +36,7 @@ function registeredColumns(
|
|||||||
): AdminColumn<AdminPluginRecord>[] {
|
): AdminColumn<AdminPluginRecord>[] {
|
||||||
return [
|
return [
|
||||||
{ key: "name", title: "名称", render: (p) => <span className="font-medium">{p.name}</span> },
|
{ 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: "desc", title: "描述", render: (p) => <span className="block max-w-sm truncate text-[var(--mu-text-mid)]">{p.description || "—"}</span> },
|
||||||
{
|
{
|
||||||
key: "policy",
|
key: "policy",
|
||||||
@@ -97,13 +116,23 @@ export function PluginsModule(): React.ReactNode {
|
|||||||
.finally(() => setBusy(null));
|
.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 (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="插件与能力"
|
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}
|
{!isOwner ? <Notice tone="warn">当前角色为 {getStoredRole()},工具策略/插件安装仅 owner 可写(读取/查看可见)。</Notice> : null}
|
||||||
@@ -112,22 +141,49 @@ export function PluginsModule(): React.ReactNode {
|
|||||||
|
|
||||||
<InstalledPluginsPanel />
|
<InstalledPluginsPanel />
|
||||||
|
|
||||||
<Surface className="space-y-2">
|
<Surface className="space-y-3">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<h3 className="text-sm font-semibold">已注册能力</h3>
|
<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>
|
</div>
|
||||||
<AdminTable
|
|
||||||
columns={columns}
|
|
||||||
rows={loading ? null : data}
|
|
||||||
loading={loading}
|
|
||||||
error={error}
|
|
||||||
emptyTitle="无插件数据"
|
|
||||||
rowKey={(p) => p.name}
|
|
||||||
/>
|
|
||||||
</Surface>
|
</Surface>
|
||||||
|
|
||||||
<Notice>工具 schema / 技能为后端 M10+ 提供;此页展示已注册能力及其环境可见性标记(群聊 / 陌生人默认关闭)。</Notice>
|
<Notice>工具 schema / 技能为后端 M10+ 提供;此页按类型分组展示已注册能力。群聊/陌生人环境可见性标记(默认关闭)仅对工具/技能生效。</Notice>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user