feat: 模型配置支持删除,岗位行动规划对接任务系统字典与批量新增 Skill
模型管理新增 DELETE 接口与前端删除能力(支持含斜杠的模型名);行动规划改为 9 字段 JSON,prepare 传入 taskId/字典并绑定 position-roundtable-action-create Skill。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
af509eb670
commit
c9c2dc7ea8
12 files changed
+697
-95
No files matched your search
@@ -96,6 +96,13 @@ export async function updateManagedModel(name: string, payload: ModelSavePayload
|
||||
return response.json() as Promise<ManagedModel>;
|
||||
}
|
||||
|
||||
export async function deleteManagedModel(name: string): Promise<void> {
|
||||
const response = await apiFetch(`${getBackendBaseURL()}/api/admin/models/${encodeURIComponent(name)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (!response.ok) return readError(response, "删除模型失败");
|
||||
}
|
||||
|
||||
export async function testManagedModel(model: ModelSavePayload): Promise<ModelConnectionTestResult> {
|
||||
const response = await apiFetch(`${getBackendBaseURL()}/api/admin/models/test`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import {
|
||||
createManagedModel,
|
||||
deleteManagedModel,
|
||||
loadManagedModels,
|
||||
testManagedModel,
|
||||
updateManagedModel,
|
||||
@@ -36,3 +37,16 @@ export function useSaveManagedModel() {
|
||||
export function useTestManagedModel() {
|
||||
return useMutation({ mutationFn: testManagedModel });
|
||||
}
|
||||
|
||||
export function useDeleteManagedModel() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: deleteManagedModel,
|
||||
onSuccess: async () => {
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: MANAGED_MODELS_QUERY_KEY }),
|
||||
queryClient.invalidateQueries({ queryKey: ["models"] }),
|
||||
]);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
RefreshCwIcon,
|
||||
Settings2Icon,
|
||||
SparklesIcon,
|
||||
Trash2Icon,
|
||||
WrenchIcon,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState, type ReactNode } from "react";
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
import {
|
||||
type ManagedModel,
|
||||
type ModelSavePayload,
|
||||
useDeleteManagedModel,
|
||||
useManagedModels,
|
||||
useSaveManagedModel,
|
||||
useTestManagedModel,
|
||||
@@ -151,6 +153,41 @@ function Field({ label, required = false, children }: { label: string; required?
|
||||
return <label className="space-y-1.5 text-sm font-medium">{label}{required && <span className="ml-1 text-destructive">*</span>}{children}</label>;
|
||||
}
|
||||
|
||||
function ModelCapabilityBadge({
|
||||
icon: Icon,
|
||||
label,
|
||||
className,
|
||||
}: {
|
||||
icon?: typeof KeyRoundIcon;
|
||||
label: string;
|
||||
className: string;
|
||||
}) {
|
||||
return (
|
||||
<span className={`inline-flex h-7 items-center gap-1 whitespace-nowrap rounded px-2 text-xs font-medium leading-none ${className}`}>
|
||||
{Icon && <Icon aria-hidden className="block size-3.5 shrink-0" />}
|
||||
<span className="block leading-none">{label}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function showGlobalMessage(
|
||||
theme: "success" | "error" | "warning",
|
||||
content: string,
|
||||
) {
|
||||
// Schedule after the modal state update so the message is rendered above
|
||||
// the page, rather than inside or behind a closing dialog.
|
||||
window.setTimeout(() => {
|
||||
MessagePlugin.closeAll();
|
||||
void MessagePlugin[theme]({
|
||||
content,
|
||||
duration: 5_000,
|
||||
placement: "top",
|
||||
zIndex: 10_000,
|
||||
attach: () => document.body,
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function ModelEditor({ model, onClose }: { model: ManagedModel | null; onClose: () => void }) {
|
||||
const isNew = model === null;
|
||||
const [draft, setDraft] = useState<ModelDraft>(() => model ? draftFromModel(model) : emptyDraft());
|
||||
@@ -165,10 +202,10 @@ function ModelEditor({ model, onClose }: { model: ManagedModel | null; onClose:
|
||||
const payload = buildPayload(draft);
|
||||
const result = await test.mutateAsync(payload);
|
||||
setTestResult({ success: result.success, latency: result.latency_ms, message: result.message, preview: result.preview });
|
||||
if (!result.success) return void MessagePlugin.error(`连接失败:${result.message}`);
|
||||
MessagePlugin.success(`连接成功${result.latency_ms ? `,${result.latency_ms} ms` : ""}`);
|
||||
if (!result.success) return void showGlobalMessage("error", `连接失败:${result.message}`);
|
||||
showGlobalMessage("success", `连接成功${result.latency_ms ? `,${result.latency_ms} ms` : ""}`);
|
||||
} catch (error) {
|
||||
MessagePlugin.error(error instanceof Error ? error.message : "连接测试失败");
|
||||
showGlobalMessage("error", error instanceof Error ? error.message : "连接测试失败");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -176,13 +213,18 @@ function ModelEditor({ model, onClose }: { model: ManagedModel | null; onClose:
|
||||
try {
|
||||
const payload = buildPayload(draft);
|
||||
if (!payload.name || !payload.use || !payload.model) {
|
||||
return void MessagePlugin.warning("请填写模型标识、驱动与实际模型名");
|
||||
return void showGlobalMessage("warning", "请填写模型标识、驱动与实际模型名");
|
||||
}
|
||||
await save.mutateAsync({ name: model?.name ?? payload.name, payload, isNew });
|
||||
MessagePlugin.success(isNew ? "模型已添加,所有页面的模型选择器已刷新" : "模型配置已保存并立即生效");
|
||||
onClose();
|
||||
showGlobalMessage(
|
||||
"success",
|
||||
isNew
|
||||
? `模型“${payload.name}”已添加,所有页面的模型选择器已刷新。`
|
||||
: `模型“${payload.name}”的配置已保存并立即生效。`,
|
||||
);
|
||||
} catch (error) {
|
||||
MessagePlugin.error(error instanceof Error ? error.message : "保存模型配置失败");
|
||||
showGlobalMessage("error", error instanceof Error ? error.message : "保存模型配置失败");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,6 +273,8 @@ export default function ModelManagementPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [filter, setFilter] = useState<"all" | "thinking" | "vision">("all");
|
||||
const [editor, setEditor] = useState<ManagedModel | null | "new">(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<ManagedModel | null>(null);
|
||||
const remove = useDeleteManagedModel();
|
||||
const models = useMemo(() => {
|
||||
const keyword = search.trim().toLowerCase();
|
||||
return (data?.models ?? []).filter((model) => {
|
||||
@@ -240,7 +284,208 @@ export default function ModelManagementPage() {
|
||||
});
|
||||
}, [data?.models, filter, search]);
|
||||
const stats = useMemo(() => ({ total: data?.models.length ?? 0, thinking: data?.models.filter((item) => item.supports_thinking).length ?? 0, vision: data?.models.filter((item) => item.supports_vision).length ?? 0 }), [data?.models]);
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget)
|
||||
return;
|
||||
try {
|
||||
await remove.mutateAsync(deleteTarget.name);
|
||||
const deletedName = deleteTarget.name;
|
||||
setDeleteTarget(null);
|
||||
showGlobalMessage("success", `模型“${deletedName}”已删除,配置已立即生效。`);
|
||||
}
|
||||
catch (error) {
|
||||
showGlobalMessage("error", error instanceof Error ? error.message : "删除模型失败,请稍后重试。");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<WorkspaceContainer><WorkspaceHeader /><WorkspaceBody><main className="flex size-full min-h-0 flex-col overflow-y-auto bg-background px-6 py-5 md:px-8"><div className="mx-auto flex w-full max-w-7xl flex-col gap-5"><header className="flex flex-wrap items-start justify-between gap-4"><div><div className="flex items-center gap-2"><span className="flex size-9 items-center justify-center rounded-lg bg-primary/10 text-primary"><CpuIcon className="size-5" /></span><h1 className="text-2xl font-semibold tracking-tight">模型配置</h1></div><p className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">管理当前部署 <code className="rounded bg-muted px-1 py-0.5">config.yaml</code> 中的对话模型。保存后所有页面立即可选;密钥仅可写入,永不回显。</p></div><div className="flex flex-wrap items-center gap-2"><TButton theme="default" variant="outline" loading={isFetching} onClick={() => void refetch()}><RefreshCwIcon className="mr-1.5 size-4" />刷新</TButton><TButton theme="primary" onClick={() => setEditor("new")}><PlusIcon className="mr-1.5 size-4" />添加模型</TButton></div></header><div className="grid gap-3 sm:grid-cols-3"><StatCard icon={BotIcon} label="已配置模型" value={stats.total} /><StatCard icon={SparklesIcon} label="支持思考" value={stats.thinking} /><StatCard icon={EyeIcon} label="支持视觉" value={stats.vision} /></div><section className="rounded-xl border border-border bg-card shadow-sm"><div className="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3"><div className="flex flex-wrap items-center gap-2">{([ ["all", "全部"], ["thinking", "支持思考"], ["vision", "支持视觉"] ] as const).map(([value, label]) => <TButton key={value} theme={filter === value ? "primary" : "default"} variant={filter === value ? "base" : "text"} size="small" onClick={() => setFilter(value)}>{label}</TButton>)}</div><Input value={search} placeholder="搜索名称、模型或驱动" clearable className="w-full sm:w-72" onChange={(value) => setSearch(String(value))} /></div>{isLoading && <div className="flex h-52 items-center justify-center gap-2 text-sm text-muted-foreground"><LoaderCircleIcon className="size-4 animate-spin" />加载模型配置…</div>}{error && <div className="m-4 flex items-start gap-2 rounded-lg border border-destructive/25 bg-destructive/5 p-3 text-sm text-destructive"><CircleAlertIcon className="mt-0.5 size-4" />{error.message}</div>}{!isLoading && !error && models.length === 0 && <div className="flex h-52 flex-col items-center justify-center gap-2 text-sm text-muted-foreground"><CpuIcon className="size-7" />没有匹配的模型配置</div>}{!isLoading && !error && models.length > 0 && <div className="grid gap-3 p-4 lg:grid-cols-2">{models.map((model) => <article key={model.name} className="rounded-lg border border-border bg-background p-4 transition-colors hover:border-primary/35"><div className="flex items-start justify-between gap-3"><div className="min-w-0"><div className="flex flex-wrap items-center gap-2"><h2 className="truncate font-semibold">{model.display_name || model.name}</h2><Tag theme="success" variant="light" size="small">config.yaml</Tag></div><p className="mt-1 truncate font-mono text-xs text-muted-foreground">{model.name}</p></div><TButton theme="default" variant="outline" size="small" onClick={() => setEditor(model)}>配置</TButton></div>{model.description ? <p className="mt-3 line-clamp-2 min-h-10 text-sm leading-5 text-muted-foreground">{model.description}</p> : <p className="mt-3 min-h-10 text-sm leading-5 text-muted-foreground">未填写描述</p>}<div className="mt-3 grid gap-2 border-t pt-3 text-xs text-muted-foreground sm:grid-cols-2"><span className="truncate" title={model.use}>驱动:{model.use}</span><span className="truncate" title={model.model}>模型:{model.model}</span></div><div className="mt-3 flex flex-wrap gap-1.5">{model.api_key_configured && <Tag size="small" variant="light"><KeyRoundIcon className="mr-1 size-3" />已配置密钥</Tag>}{model.supports_thinking && <Tag theme="primary" size="small" variant="light"><SparklesIcon className="mr-1 size-3" />思考</Tag>}{model.supports_vision && <Tag theme="warning" size="small" variant="light"><EyeIcon className="mr-1 size-3" />视觉</Tag>}{model.supports_reasoning_effort && <Tag size="small" variant="light">推理档位</Tag>}</div></article>)}</div>}</section>{data?.config_path && <p className="text-xs text-muted-foreground">当前配置文件:<code>{data.config_path}</code>{data.updated_at ? ` · 最近更新 ${new Date(data.updated_at).toLocaleString()}` : ""}</p>}</div></main></WorkspaceBody>{editor !== null && <ModelEditor key={editor === "new" ? "new" : editor.name} model={editor === "new" ? null : editor} onClose={() => setEditor(null)} />}</WorkspaceContainer>
|
||||
<WorkspaceContainer>
|
||||
<WorkspaceHeader />
|
||||
<WorkspaceBody>
|
||||
<main className="flex size-full min-h-0 flex-col overflow-y-auto bg-background px-6 py-5 md:px-8">
|
||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-5">
|
||||
<header className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex size-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
|
||||
<CpuIcon className="size-5" />
|
||||
</span>
|
||||
<h1 className="text-2xl font-semibold tracking-tight">模型配置</h1>
|
||||
</div>
|
||||
<p className="mt-2 max-w-3xl text-sm leading-6 text-muted-foreground">
|
||||
管理当前部署 <code className="rounded bg-muted px-1 py-0.5">config.yaml</code> 中的对话模型。保存后所有页面立即可选;密钥仅可写入,永不回显。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<TButton theme="default" variant="outline" loading={isFetching} onClick={() => void refetch()}>
|
||||
<RefreshCwIcon className="mr-1.5 size-4" />刷新
|
||||
</TButton>
|
||||
<TButton theme="primary" onClick={() => setEditor("new")}>
|
||||
<PlusIcon className="mr-1.5 size-4" />添加模型
|
||||
</TButton>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<StatCard icon={BotIcon} label="已配置模型" value={stats.total} />
|
||||
<StatCard icon={SparklesIcon} label="支持思考" value={stats.thinking} />
|
||||
<StatCard icon={EyeIcon} label="支持视觉" value={stats.vision} />
|
||||
</div>
|
||||
|
||||
<section className="rounded-xl border border-border bg-card shadow-sm">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b px-4 py-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{([
|
||||
["all", "全部"],
|
||||
["thinking", "支持思考"],
|
||||
["vision", "支持视觉"],
|
||||
] as const).map(([value, label]) => (
|
||||
<TButton
|
||||
key={value}
|
||||
theme={filter === value ? "primary" : "default"}
|
||||
variant={filter === value ? "base" : "text"}
|
||||
size="small"
|
||||
onClick={() => setFilter(value)}
|
||||
>
|
||||
{label}
|
||||
</TButton>
|
||||
))}
|
||||
</div>
|
||||
<Input
|
||||
value={search}
|
||||
placeholder="搜索名称、模型或驱动"
|
||||
clearable
|
||||
className="w-full sm:w-72"
|
||||
onChange={(value) => setSearch(String(value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex h-52 items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<LoaderCircleIcon className="size-4 animate-spin" />加载模型配置…
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="m-4 flex items-start gap-2 rounded-lg border border-destructive/25 bg-destructive/5 p-3 text-sm text-destructive">
|
||||
<CircleAlertIcon className="mt-0.5 size-4" />{error.message}
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && models.length === 0 && (
|
||||
<div className="flex h-52 flex-col items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<CpuIcon className="size-7" />没有匹配的模型配置
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !error && models.length > 0 && (
|
||||
<div className="grid gap-3 p-4 lg:grid-cols-2">
|
||||
{models.map((model) => (
|
||||
<article key={model.name} className="rounded-lg border border-border bg-background p-4 transition-colors hover:border-primary/35">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h2 className="truncate font-semibold">{model.display_name || model.name}</h2>
|
||||
<Tag theme="success" variant="light" size="small">config.yaml</Tag>
|
||||
</div>
|
||||
<p className="mt-1 truncate font-mono text-xs text-muted-foreground">{model.name}</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<TButton theme="default" variant="outline" size="small" onClick={() => setEditor(model)}>
|
||||
配置
|
||||
</TButton>
|
||||
<TButton
|
||||
theme="danger"
|
||||
variant="text"
|
||||
size="small"
|
||||
aria-label={"删除模型 " + (model.display_name || model.name)}
|
||||
onClick={() => setDeleteTarget(model)}
|
||||
>
|
||||
<Trash2Icon className="mr-1 size-3.5" />删除
|
||||
</TButton>
|
||||
</div>
|
||||
</div>
|
||||
{model.description ? (
|
||||
<p className="mt-3 line-clamp-2 min-h-10 text-sm leading-5 text-muted-foreground">{model.description}</p>
|
||||
) : (
|
||||
<p className="mt-3 min-h-10 text-sm leading-5 text-muted-foreground">未填写描述</p>
|
||||
)}
|
||||
<div className="mt-3 grid gap-2 border-t pt-3 text-xs text-muted-foreground sm:grid-cols-2">
|
||||
<span className="truncate" title={model.use}>驱动:{model.use}</span>
|
||||
<span className="truncate" title={model.model}>模型:{model.model}</span>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-1.5">
|
||||
{model.api_key_configured && (
|
||||
<ModelCapabilityBadge
|
||||
icon={KeyRoundIcon}
|
||||
label="已配置密钥"
|
||||
className="bg-muted text-foreground"
|
||||
/>
|
||||
)}
|
||||
{model.supports_thinking && (
|
||||
<ModelCapabilityBadge
|
||||
icon={SparklesIcon}
|
||||
label="思考"
|
||||
className="bg-primary/10 text-primary"
|
||||
/>
|
||||
)}
|
||||
{model.supports_vision && (
|
||||
<ModelCapabilityBadge
|
||||
icon={EyeIcon}
|
||||
label="视觉"
|
||||
className="bg-amber-500/10 text-amber-700 dark:text-amber-300"
|
||||
/>
|
||||
)}
|
||||
{model.supports_reasoning_effort && (
|
||||
<ModelCapabilityBadge
|
||||
label="推理档位"
|
||||
className="bg-muted text-muted-foreground"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{data?.config_path && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
当前配置文件:<code>{data.config_path}</code>
|
||||
{data.updated_at ? " · 最近更新 " + new Date(data.updated_at).toLocaleString() : ""}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</WorkspaceBody>
|
||||
|
||||
{editor !== null && (
|
||||
<ModelEditor
|
||||
key={editor === "new" ? "new" : editor.name}
|
||||
model={editor === "new" ? null : editor}
|
||||
onClose={() => setEditor(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{deleteTarget && (
|
||||
<Dialog
|
||||
visible
|
||||
header="删除模型"
|
||||
placement="center"
|
||||
width={440}
|
||||
zIndex={3200}
|
||||
confirmBtn={{ content: "删除", theme: "danger" }}
|
||||
cancelBtn="取消"
|
||||
confirmLoading={remove.isPending}
|
||||
onClose={() => {
|
||||
if (!remove.isPending) setDeleteTarget(null);
|
||||
}}
|
||||
onCancel={() => {
|
||||
if (!remove.isPending) setDeleteTarget(null);
|
||||
}}
|
||||
onConfirm={() => void handleDelete()}
|
||||
>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
确定要删除模型 <code className="rounded bg-muted px-1 py-0.5">{deleteTarget.name}</code> 吗?删除后会立即更新运行中的模型配置,依赖它的智能体或任务可能无法继续使用。
|
||||
</p>
|
||||
</Dialog>
|
||||
)}
|
||||
</WorkspaceContainer>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user