feat(admin): 面板补全 — 插件安装表单+已装列表 / LLM组maxTokens展示编辑 / POST admin/llm 部分编辑合并
- 前端: api.ts(adminPluginInstall带config/uninstall/listInstalledPlugins) + types + plugins/InstalledPlugins.tsx(已装列表+安装表单, Miuix) + llm/LlmProviderForm.tsx(maxTokens输入) ; Plugins.tsx去假安装按钮 - 后端 admin_write_llm_channels.go: llmProviderRequest字段改指针, mergeLlmProvider部分合并(省略保留原值), runtimeconfig/models.go Find - 新增测试 admin_write_test TestAdminAddLlmProviderPartialEditPreservesFields - 前端 build + 后端 build/vet/test 全绿; Docs/api-client v0.29 - (面板无法实机登录, 用 react-dom/server 冒烟渲染 LlmModule/PluginsModule)
This commit is contained in:
1 parent
70c9fba529
commit
18b2040c95
10 files changed
+812
-220
No files matched your search
@@ -1,71 +1,21 @@
|
||||
import { useState } from "react";
|
||||
import { useAsyncData } from "../../lib/useAsyncData";
|
||||
import { adminListPlugins, adminSetPluginPolicy, adminPluginInstall, adminPluginUninstall, getStoredRole } from "../../lib/api";
|
||||
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";
|
||||
|
||||
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 [msg, setMsg] = useState<string | null>(null);
|
||||
|
||||
const install = () => {
|
||||
setBusy("__install__");
|
||||
setFail(null);
|
||||
setMsg(null);
|
||||
adminPluginInstall({ name: "sample-plugin", kind: "tool", description: "手工安装示例" })
|
||||
.then(() => {
|
||||
log("plugin.install", "ok");
|
||||
setMsg("插件安装成功");
|
||||
reload();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
log("plugin.install", "error");
|
||||
setFail(err instanceof Error ? err.message : "插件安装失败");
|
||||
})
|
||||
.finally(() => setBusy(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 columns: AdminColumn<AdminPluginRecord>[] = [
|
||||
/** Columns for the registered-capability table (GET /admin/plugins). */
|
||||
function registeredColumns(
|
||||
isOwner: boolean,
|
||||
busy: string | null,
|
||||
setPolicy: (name: string, allowInGroup: boolean, allowInStranger: boolean) => void,
|
||||
uninstall: (name: string) => void,
|
||||
): 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: "desc", title: "描述", render: (p) => <span className="block max-w-sm truncate text-[var(--mu-text-mid)]">{p.description || "—"}</span> },
|
||||
@@ -104,24 +54,69 @@ export function PluginsModule(): React.ReactNode {
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
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 [msg, setMsg] = useState<string | null>(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 columns = registeredColumns(isOwner, busy, setPolicy, uninstall);
|
||||
|
||||
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" disabled={busy !== null} onClick={install}>
|
||||
安装插件
|
||||
</Button>
|
||||
}
|
||||
subtitle="GET /api/v1/admin/plugins · 已注册能力 + 运行时 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}
|
||||
{fail ? <Notice tone="err">{fail}</Notice> : null}
|
||||
{msg ? <Notice tone="ok">{msg}</Notice> : null}
|
||||
|
||||
<InstalledPluginsPanel />
|
||||
|
||||
<Surface className="space-y-2">
|
||||
<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>
|
||||
</div>
|
||||
<AdminTable
|
||||
columns={columns}
|
||||
rows={loading ? null : data}
|
||||
@@ -132,7 +127,7 @@ export function PluginsModule(): React.ReactNode {
|
||||
/>
|
||||
</Surface>
|
||||
|
||||
<Notice>工具 schema / 技能 / MCP 服务器列表为后端 M10+ 提供;此页展示已注册能力及其环境可见性标记(群聊 / 陌生人默认关闭)。</Notice>
|
||||
<Notice>工具 schema / 技能为后端 M10+ 提供;此页展示已注册能力及其环境可见性标记(群聊 / 陌生人默认关闭)。</Notice>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user