- 任务1: 用户管理/定时任务管理移入左下角菜单, 从侧边栏删除原入口
- 任务2: 管理员查看用户问答(读 LangGraph checkpoint)与记忆, 新增 admin_users 路由与审计页
- 任务3: 标签筛选改为顶部多选标签栏(OR), agents/skills/scheduled_tasks 三端
- 任务4: 新增 tool_call_metrics 表 + ToolMetricsMiddleware + tool-metrics 路由,
工具/技能调用统计并入大模型统计页, 含错误分类
- 技能识别: lead agent 经 read_file 加载 /mnt/skills/<cat>/<name>/SKILL.md 调用技能,
中间件扫描工具参数里的 skills/{public,custom}/{name}/ 路径还原 skill_name
- 单元测试 tests/test_tool_metrics_middleware.py (18 项全过)
- 附: SQLite->MySQL 切换后清理脚本
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
149 lines
4.6 KiB
TypeScript
149 lines
4.6 KiB
TypeScript
import { DownloadIcon, LoaderIcon, PackageIcon } from "lucide-react";
|
|
import { useCallback, useState } from "react";
|
|
import { toast } from "sonner";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardAction,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
import { downloadArtifactFile } from "@/core/artifacts/download";
|
|
import { useI18n } from "@/core/i18n/hooks";
|
|
import { installSkill } from "@/core/skills/api";
|
|
import {
|
|
getFileExtensionDisplayName,
|
|
getFileIcon,
|
|
getFileName,
|
|
} from "@/core/utils/files";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
import { useThread } from "../messages/context";
|
|
import { useArtifacts } from "./context";
|
|
|
|
export function ArtifactFileList({
|
|
className,
|
|
files,
|
|
threadId,
|
|
}: {
|
|
className?: string;
|
|
files: string[];
|
|
threadId: string;
|
|
}) {
|
|
const { t } = useI18n();
|
|
const { isMock, publicShareCode } = useThread();
|
|
const { select: selectArtifact, setOpen } = useArtifacts();
|
|
const [installingFile, setInstallingFile] = useState<string | null>(null);
|
|
const [downloadingFile, setDownloadingFile] = useState<string | null>(null);
|
|
|
|
const handleClick = useCallback(
|
|
(filepath: string) => {
|
|
selectArtifact(filepath);
|
|
setOpen(true);
|
|
},
|
|
[selectArtifact, setOpen],
|
|
);
|
|
|
|
const handleInstallSkill = useCallback(
|
|
async (e: React.MouseEvent, filepath: string) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
if (installingFile) return;
|
|
|
|
setInstallingFile(filepath);
|
|
try {
|
|
const result = await installSkill({
|
|
thread_id: threadId,
|
|
path: filepath,
|
|
});
|
|
if (result.success) {
|
|
toast.success(result.message);
|
|
} else {
|
|
toast.error(result.message || "Failed to install skill");
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to install skill:", error);
|
|
toast.error("Failed to install skill");
|
|
} finally {
|
|
setInstallingFile(null);
|
|
}
|
|
},
|
|
[threadId, installingFile],
|
|
);
|
|
|
|
const handleDownload = useCallback(
|
|
async (e: React.MouseEvent, filepath: string) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
if (downloadingFile) return;
|
|
setDownloadingFile(filepath);
|
|
try {
|
|
await downloadArtifactFile({ filepath, threadId, isMock, publicShareCode });
|
|
} catch (error) {
|
|
console.error(error);
|
|
toast.error(
|
|
error instanceof Error ? error.message : "Download failed",
|
|
);
|
|
} finally {
|
|
setDownloadingFile(null);
|
|
}
|
|
},
|
|
[threadId, isMock, publicShareCode, downloadingFile],
|
|
);
|
|
|
|
return (
|
|
<ul className={cn("flex w-full flex-col gap-4", className)}>
|
|
{files.map((file) => (
|
|
<Card
|
|
key={file}
|
|
className="relative cursor-pointer p-3"
|
|
onClick={() => handleClick(file)}
|
|
>
|
|
<CardHeader className="grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 pr-2 pl-1">
|
|
<CardTitle className="relative min-w-0 pl-8 leading-tight [overflow-wrap:anywhere] break-words">
|
|
<div className="min-w-0">{getFileName(file)}</div>
|
|
<div className="absolute top-2 -left-0.5">
|
|
{getFileIcon(file, "size-6")}
|
|
</div>
|
|
</CardTitle>
|
|
<CardDescription className="min-w-0 pl-8 text-xs">
|
|
{getFileExtensionDisplayName(file)} file
|
|
</CardDescription>
|
|
<CardAction className="row-span-1 self-center">
|
|
{file.endsWith(".skill") && (
|
|
<Button
|
|
variant="ghost"
|
|
disabled={installingFile === file}
|
|
onClick={(e) => handleInstallSkill(e, file)}
|
|
>
|
|
{installingFile === file ? (
|
|
<LoaderIcon className="size-4 animate-spin" />
|
|
) : (
|
|
<PackageIcon className="size-4" />
|
|
)}
|
|
{t.common.install}
|
|
</Button>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
disabled={downloadingFile === file}
|
|
onClick={(e) => handleDownload(e, file)}
|
|
>
|
|
{downloadingFile === file ? (
|
|
<LoaderIcon className="size-4 animate-spin" />
|
|
) : (
|
|
<DownloadIcon className="size-4" />
|
|
)}
|
|
{t.common.download}
|
|
</Button>
|
|
</CardAction>
|
|
</CardHeader>
|
|
</Card>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|