feat(square): 新增发布广场功能,智能体/技能按广场分类展示
- 后端新增 PublishSquare/PublishSquaresSettings 配置与读写接口 - agents/skills 增加 square_id 字段及对应迁移脚本 - 前端新增发布广场设置页、技能转移选择器,画廊按广场分 Tab 展示 - lead_agent prompt 及若干持久化层适配调整 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3845d5090b
commit
f0aec4cd7f
39 files changed
+1707
-226
No files matched your search
@@ -233,6 +233,7 @@ export default function AgentChatPage() {
|
||||
<InputBox
|
||||
className="w-full"
|
||||
isNewThread
|
||||
hideAdminRecommendedQuestions
|
||||
threadId={threadId}
|
||||
assistantId={agent_id}
|
||||
autoFocus
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2Icon, PlusIcon, StarIcon, Trash2Icon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
usePublishSquares,
|
||||
useUpdatePublishSquares,
|
||||
} from "@/core/system-settings/hooks";
|
||||
import type { PublishSquare } from "@/core/system-settings/api";
|
||||
|
||||
/** Local draft row — `id` is stable (server- or client-generated) so the
|
||||
* "default" radio can reference it before the first save. */
|
||||
type Draft = PublishSquare;
|
||||
|
||||
function genId(): string {
|
||||
// Frontend-only id; the backend also accepts/normalizes these.
|
||||
return `sq_${Math.random().toString(36).slice(2, 10)}`;
|
||||
}
|
||||
|
||||
export default function PublishSquaresSettingsPage({
|
||||
embedded = false,
|
||||
}: {
|
||||
embedded?: boolean;
|
||||
} = {}) {
|
||||
const { data, isLoading, error } = usePublishSquares();
|
||||
const update = useUpdatePublishSquares();
|
||||
|
||||
const [squares, setSquares] = useState<Draft[]>([]);
|
||||
const [defaultId, setDefaultId] = useState<string>("");
|
||||
const initializedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || initializedRef.current) return;
|
||||
setSquares(data.squares.map((s) => ({ ...s })));
|
||||
setDefaultId(data.default_square_id);
|
||||
initializedRef.current = true;
|
||||
}, [data]);
|
||||
|
||||
function addSquare() {
|
||||
setSquares((prev) => [...prev, { id: genId(), name: "", description: "" }]);
|
||||
}
|
||||
|
||||
function patchSquare(id: string, patch: Partial<Draft>) {
|
||||
setSquares((prev) =>
|
||||
prev.map((s) => (s.id === id ? { ...s, ...patch } : s)),
|
||||
);
|
||||
}
|
||||
|
||||
function removeSquare(id: string) {
|
||||
setSquares((prev) => prev.filter((s) => s.id !== id));
|
||||
if (defaultId === id) setDefaultId("");
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const cleaned = squares
|
||||
.map((s) => ({ ...s, name: s.name.trim(), description: s.description.trim() }))
|
||||
.filter((s) => s.name.length > 0);
|
||||
if (cleaned.length === 0) {
|
||||
toast.error("请至少配置一个广场(名称不能为空)");
|
||||
return;
|
||||
}
|
||||
// Re-anchor the default to a surviving square if the picked one was removed.
|
||||
const nextDefault = cleaned.some((s) => s.id === defaultId)
|
||||
? defaultId
|
||||
: cleaned[0].id;
|
||||
try {
|
||||
const saved = await update.mutateAsync({
|
||||
squares: cleaned,
|
||||
default_square_id: nextDefault,
|
||||
});
|
||||
setSquares(saved.squares.map((s) => ({ ...s })));
|
||||
setDefaultId(saved.default_square_id);
|
||||
toast.success("已保存");
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
embedded
|
||||
? "flex w-full flex-col gap-6"
|
||||
: "mx-auto flex w-full max-w-2xl flex-col gap-6 px-6 py-10"
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
className={embedded ? "text-base font-semibold" : "text-xl font-semibold"}
|
||||
>
|
||||
发布广场配置
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm">
|
||||
管理员可在此配置多个「发布广场」。发布智能体或技能时,未选择广场则归入默认广场,也可选择发布到其他广场;
|
||||
每个智能体或技能只能归属于一个广场,不会重复出现在多个广场中。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<Loader2Icon className="text-muted-foreground size-6 animate-spin" />
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-destructive rounded-xl border border-dashed py-16 text-center text-sm">
|
||||
加载失败:{error.message}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 rounded-xl border bg-white px-5 py-5 dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white">
|
||||
<div className="flex flex-col gap-3">
|
||||
{squares.length === 0 ? (
|
||||
<p className="text-muted-foreground py-4 text-center text-sm">
|
||||
暂无广场,点击下方「添加广场」创建第一个。
|
||||
</p>
|
||||
) : (
|
||||
squares.map((s) => {
|
||||
const isDefault = defaultId === s.id;
|
||||
return (
|
||||
<div
|
||||
key={s.id}
|
||||
className="flex flex-col gap-2 rounded-lg border p-3 dark:border-[#1B477E]"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
title={isDefault ? "默认广场" : "设为默认广场"}
|
||||
className={
|
||||
isDefault ? "text-amber-500 hover:text-amber-600" : ""
|
||||
}
|
||||
onClick={() => setDefaultId(s.id)}
|
||||
>
|
||||
<StarIcon
|
||||
className={isDefault ? "size-4 fill-current" : "size-4"}
|
||||
/>
|
||||
</Button>
|
||||
<Input
|
||||
value={s.name}
|
||||
placeholder="广场名称(如:公开广场 / 合作伙伴 / 内测)"
|
||||
onChange={(e) => patchSquare(s.id, { name: e.target.value })}
|
||||
className="h-9 flex-1 text-sm"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="text-destructive hover:text-destructive"
|
||||
title="删除广场"
|
||||
onClick={() => removeSquare(s.id)}
|
||||
>
|
||||
<Trash2Icon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={s.description}
|
||||
placeholder="广场描述(选填)"
|
||||
onChange={(e) =>
|
||||
patchSquare(s.id, { description: e.target.value })
|
||||
}
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
{isDefault && (
|
||||
<span className="text-muted-foreground text-xs">
|
||||
默认广场:发布时未选择广场的智能体/技能会归入此处。
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Button type="button" variant="outline" size="sm" onClick={addSquare}>
|
||||
<PlusIcon className="mr-1.5 size-4" />
|
||||
添加广场
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={update.isPending}>
|
||||
{update.isPending && (
|
||||
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
||||
)}
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import CheckpointMigrationPage from './CheckpointMigrationPage';
|
||||
import MemoryPage from './MemoryPage';
|
||||
import PromptPrefixSettingsPage from './PromptPrefixSettingsPage';
|
||||
import PrivateSquareSettingsPage from './PrivateSquareSettingsPage';
|
||||
import PublishSquaresSettingsPage from './PublishSquaresSettingsPage';
|
||||
import SkillEvolutionPage from './SkillEvolutionPage';
|
||||
import SkillsPage from './SkillsPage';
|
||||
import UsersPage from './UsersPage';
|
||||
@@ -496,6 +497,18 @@ export default function WorkspaceRoutes({
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/publish-squares"
|
||||
element={
|
||||
getStoredAuth()?.system_role === 'admin' ? (
|
||||
<WorkspaceLayout {...layoutProps}>
|
||||
<PublishSquaresSettingsPage />
|
||||
</WorkspaceLayout>
|
||||
) : (
|
||||
<Navigate to="/page/workspace/chats/new" replace />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="settings/llm-metrics"
|
||||
element={
|
||||
|
||||
Reference in new issue
Block a user