import { useMemo, useState } from "react"; import { useAsyncData } from "../../lib/useAsyncData"; import { adminListPlugins, adminSetPluginPolicy, adminPluginUninstall, 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"; import { InstalledPluginsPanel } from "./plugins/InstalledPlugins"; type CapKind = "tool" | "skill" | "channel"; const KIND_LABELS: Record = { 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[] { return [ { key: "name", title: "名称", render: (p) => {p.name} }, { key: "desc", title: "描述", render: (p) => {p.description || "—"} }, { key: "admin", title: "配置", render: () => 渠道配置见「渠道管理」页 }, ]; } /** Columns for a tool / skill row (has environment markers, owner can edit). */ function registeredColumns( isOwner: boolean, busy: string | null, setPolicy: (name: string, allowInGroup: boolean, allowInStranger: boolean) => void, uninstall: (name: string) => void, ): AdminColumn[] { return [ { key: "name", title: "名称", render: (p) => {p.name} }, { key: "kind", title: "类型", render: (p) => {(KIND_LABELS as Record)[p.kind] ?? p.kind} }, { key: "desc", title: "描述", render: (p) => {p.description || "—"} }, { key: "policy", title: "环境授权(owner)", render: (p) => ( {busy === p.name ? 保存中… : null} ), }, { key: "actions", title: "操作", render: (p) => ( ), }, ]; } 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(null); const [fail, setFail] = useState(null); const [msg, setMsg] = useState(null); const uninstall = (name: string) => { setBusy(name); setFail(null); setMsg(null); adminPluginUninstall(name) .then(() => { log("plugin.uninstall", "ok"); setMsg(`已卸载 ${name}`); reload(); }) .catch((err: unknown) => { log("plugin.uninstall", "error"); setFail(err instanceof Error ? err.message : "卸载失败"); }) .finally(() => setBusy(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 byKind = useMemo(() => { const groups: Record = { 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 (
{!isOwner ? 当前角色为 {getStoredRole()},工具策略/插件安装仅 owner 可写(读取/查看可见)。 : null} {fail ? {fail} : null} {msg ? {msg} : null}

已注册能力

GET /api/v1/admin/plugins · 按工具 / 技能 / 渠道分组
{(["tool", "skill"] as CapKind[]).map((kind) => { const rows = byKind[kind]; return (
{KIND_LABELS[kind]} {rows.length} 个
`${kind}:${p.name}`} />
); })}
{KIND_LABELS.channel} {byKind.channel.length} 个
`channel:${p.name}`} />
工具 schema / 技能为后端 M10+ 提供;此页按类型分组展示已注册能力。群聊/陌生人环境可见性标记(默认关闭)仅对工具/技能生效。
); }