feat(admin): 插件能力页按类型分组展示 — 工具/技能/渠道分开,渠道不再显示工具策略开关
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- GET /admin/plugins 后端仍平铺返回(契约不变), 前端 Plugins.tsx 按 kind 分组渲染
- 工具+技能: 保留「群聊/陌生人」环境授权开关(ToolPolicy, owner可编辑)+卸载; 渠道: 仅名称/描述/『见渠道管理页』, 无策略开关(渠道走自身配置)
- 渠道块文案tone=neutral, 工具技能accent; Miuix 原语复用
This commit is contained in:
AskaEth committed 2026-09-09 02:57:13 +08:00
1 parent 2dee372fb2
commit f28995c1e5
1 file changed
+72 -16
+72 -16
View File
@@ -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>
);
}