feat(admin): 面板补全 — 插件安装表单+已装列表 / LLM组maxTokens展示编辑 / POST admin/llm 部分编辑合并
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 前端: 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:
AskaEth committed 2026-09-08 23:47:09 +08:00
1 parent 70c9fba529
commit 18b2040c95
10 files changed
+812 -220

No files matched your search

+63 -68
View File
@@ -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>
);
}