feat: 公开用户排行榜页面 + 修复 checkpointer 连接与前端依赖

- 新增公开排行榜页面与后端 public_leaderboard 路由
- 完善 skills/agents 路由
- checkpointer Postgres 改用连接池(自愈), 解决连接关闭后定时任务连环失败
- 修复前端 mermaid 依赖未声明导致 Vite 解析失败 (新增 pnpm-lock)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-08 17:09:28 +08:00
1 parent 8113b822c5
commit 80d2db6315
26 files changed
+1877 -967

No files matched your search

@@ -14,6 +14,7 @@ import {
StarIcon,
TagsIcon,
Trash2Icon,
UserIcon,
} from "lucide-react";
import { useRouter } from "next/navigation";
import {
@@ -497,6 +498,18 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
</>
);
// 发布者(仅自定义/已发布的智能体显示;内置无 owner 不显示)。
const publisherNode =
!isBuiltin && agent.owner_username ? (
<div
className="text-muted-foreground flex items-center gap-1 text-xs"
title={`发布者:${desensitize(agent.owner_username)}`}
>
<UserIcon className="size-3 shrink-0" />
<span className="truncate">发布者:{desensitize(agent.owner_username)}</span>
</div>
) : null;
const skillBadgeItems = [
...(agent.tool_groups ?? []).map((group) => ({
key: `tg:${group}`,
@@ -585,15 +598,15 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
<SparklesIcon className="h-3.5 w-3.5" />
</Button>
)}
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
onClick={() => setTagPickerOpen(true)}
title={t.adminTags.manageTags}
>
<TagsIcon className="h-3.5 w-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
onClick={() => setTagPickerOpen(true)}
title={t.adminTags.manageTags}
>
<TagsIcon className="h-3.5 w-3.5" />
</Button>
{isOwned && (
<Button
size="icon"
@@ -702,6 +715,7 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
{desensitize(agent.description)}
</CardDescription>
)}
{publisherNode && <div className="mt-2">{publisherNode}</div>}
</CardHeader>
{(agent.tags?.length ?? 0) > 0 && (
@@ -747,6 +761,7 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
{desensitize(agent.description)}
</p>
)}
{publisherNode && <div className="mt-0.5">{publisherNode}</div>}
</div>
<div className="flex shrink-0 items-center gap-1">
{showActionRow && actionButtons}
@@ -934,13 +949,13 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {
</DialogContent>
</Dialog>
<TagPickerDialog
open={tagPickerOpen}
onOpenChange={setTagPickerOpen}
targetType="agent"
targetId={agent.id}
currentTags={agent.tags ?? []}
/>
<TagPickerDialog
open={tagPickerOpen}
onOpenChange={setTagPickerOpen}
targetType="agent"
targetId={agent.id}
currentTags={agent.tags ?? []}
/>
<SkillDetailDialog
@@ -30,6 +30,10 @@ import {
} from "@/core/system-settings/hooks";
import { cn } from "@/lib/utils";
import PrivateSquareSettingsPage from "@/pages/PrivateSquareSettingsPage";
import PromptPrefixSettingsPage from "@/pages/PromptPrefixSettingsPage";
import RecommendedQuestionsPage from "@/pages/RecommendedQuestionsPage";
import { SettingsSection } from "./settings-section";
const languageOptions: { value: Locale; label: string }[] = [
@@ -336,6 +340,17 @@ export function AppearanceSettingsPage() {
</div>
)}
</SettingsSection>
{isAdmin && (
<>
<Separator />
<RecommendedQuestionsPage embedded />
<Separator />
<PromptPrefixSettingsPage embedded />
<Separator />
<PrivateSquareSettingsPage embedded />
</>
)}
</div>
);
}
@@ -1,6 +1,6 @@
"use client";
import { PenLineIcon, PlusIcon, SearchIcon, Trash2Icon, Heart, BookOpen, Map, Zap, Target, ShieldAlert } from "lucide-react";
import { PenLineIcon, PlusIcon, SearchIcon, Trash2Icon, Heart, BookOpen, Map, Zap, Target, ShieldAlert, Briefcase, UserRound, Sparkles, Clock, History, Archive } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
@@ -458,12 +458,24 @@ const CTX_LABELS: Record<string, string> = {
topOfMind: "当前重点",
};
const CTX_ICONS: Record<string, React.ElementType> = {
workContext: Briefcase,
personalContext: UserRound,
topOfMind: Sparkles,
};
const HISTORY_LABELS: Record<string, string> = {
recentMonths: "近期",
earlierContext: "早期背景",
longTermBackground: "长期背景",
};
const HISTORY_ICONS: Record<string, React.ElementType> = {
recentMonths: Clock,
earlierContext: History,
longTermBackground: Archive,
};
const CATEGORY_LABELS: Record<string, Record<string, string>> = {
zh: {
preference: "偏好",
@@ -572,12 +584,15 @@ function MemoryV1View({
<Icon className="w-3.5 h-3.5" />
{categoryLabel}
</span>
<div className="flex flex-col items-end gap-1">
<span className="text-[11px] text-muted-foreground font-semibold tracking-wider">
{Math.round(fact.confidence * 100)}%
<div
className="flex flex-col items-end gap-1"
title={`置信度:系统对这条记忆准确性的把握程度,${Math.round(fact.confidence * 100)}%`}
>
<span className="text-[11px] text-muted-foreground font-semibold tracking-wide">
置信度 {Math.round(fact.confidence * 100)}%
</span>
{/* Confidence Progress Bar */}
<div className="h-1 w-12 rounded-full bg-muted overflow-hidden">
<div className="h-1 w-16 rounded-full bg-muted overflow-hidden">
<div
className="h-full rounded-full bg-primary transition-all duration-500"
style={{ width: `${Math.round(fact.confidence * 100)}%` }}
@@ -611,44 +626,77 @@ function MemoryV1View({
</TabsContent>
{/* 用户画像 & 历史摘要 Tab */}
<TabsContent value="profile" className="mt-3 space-y-4">
<TabsContent value="profile" className="mt-3 space-y-5">
{/* 用户画像 (userContext) */}
<div className="rounded-lg border p-4 space-y-2 dark:border-[#213D5E] dark:bg-[#022248]">
<h3 className="text-base font-medium">用户画像</h3>
<section className="rounded-xl border border-border/60 bg-card/40 p-4 space-y-3 dark:border-[#213D5E] dark:bg-[#022248]/50">
<div className="flex items-center gap-2">
<span className="inline-flex h-7 w-7 items-center justify-center rounded-lg bg-primary/10 text-primary dark:bg-primary/20">
<UserRound className="h-4 w-4" />
</span>
<h3 className="text-base font-semibold">用户画像</h3>
</div>
{ctxEntries.length === 0 ? (
<p className="text-sm text-muted-foreground">暂无画像信息,对话后系统会自动归纳。</p>
<p className="rounded-lg border border-dashed p-3 text-sm text-muted-foreground dark:border-[#213D5E]">
暂无画像信息,对话后系统会自动归纳。
</p>
) : (
<dl className="space-y-2">
{ctxEntries.map(([key, value]) => (
<div key={key} className="text-sm">
<dt className="font-medium text-muted-foreground mb-0.5">
{CTX_LABELS[key] ?? key}
</dt>
<dd className="whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(value)}</dd>
</div>
))}
</dl>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{ctxEntries.map(([key, value]) => {
const Icon = CTX_ICONS[key] ?? UserRound;
return (
<div
key={key}
className="group rounded-lg border border-border/50 bg-gradient-to-br from-card to-card/40 p-3.5 shadow-sm transition-all hover:border-primary/30 hover:shadow-md dark:border-[#213D5E]/60 dark:from-[#022248]/40 dark:to-[#022248]/10"
>
<div className="mb-1.5 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
<Icon className="h-3.5 w-3.5 text-primary/80" />
{CTX_LABELS[key] ?? key}
</div>
<p className="whitespace-pre-wrap text-sm leading-relaxed text-foreground/90 [overflow-wrap:anywhere]">
{desensitize(value)}
</p>
</div>
);
})}
</div>
)}
</div>
</section>
{/* 历史摘要 (history) */}
<div className="rounded-lg border p-4 space-y-2 dark:border-[#213D5E] dark:bg-[#022248]">
<h3 className="text-base font-medium">历史摘要</h3>
<section className="rounded-xl border border-border/60 bg-card/40 p-4 space-y-3 dark:border-[#213D5E] dark:bg-[#022248]/50">
<div className="flex items-center gap-2">
<span className="inline-flex h-7 w-7 items-center justify-center rounded-lg bg-primary/10 text-primary dark:bg-primary/20">
<History className="h-4 w-4" />
</span>
<h3 className="text-base font-semibold">历史摘要</h3>
</div>
{historyEntries.length === 0 ? (
<p className="text-sm text-muted-foreground">暂无历史摘要。</p>
<p className="rounded-lg border border-dashed p-3 text-sm text-muted-foreground dark:border-[#213D5E]">
暂无历史摘要。
</p>
) : (
<dl className="space-y-2">
{historyEntries.map(([key, value]) => (
<div key={key} className="text-sm">
<dt className="font-medium text-muted-foreground mb-0.5">
{HISTORY_LABELS[key] ?? key}
</dt>
<dd className="whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(value)}</dd>
</div>
))}
</dl>
<div className="space-y-3">
{historyEntries.map(([key, value]) => {
const Icon = HISTORY_ICONS[key] ?? History;
return (
<div
key={key}
className="relative rounded-lg border border-border/50 bg-card/60 p-3.5 pl-4 shadow-sm transition-all hover:border-primary/30 hover:shadow-md dark:border-[#213D5E]/60 dark:bg-[#022248]/30"
>
<span className="absolute inset-y-2 left-0 w-1 rounded-full bg-primary/30" />
<div className="mb-1.5 flex items-center gap-1.5 text-xs font-semibold text-muted-foreground">
<Icon className="h-3.5 w-3.5 text-primary/80" />
{HISTORY_LABELS[key] ?? key}
</div>
<p className="whitespace-pre-wrap text-sm leading-relaxed text-foreground/90 [overflow-wrap:anywhere]">
{desensitize(value)}
</p>
</div>
);
})}
</div>
)}
</div>
</section>
</TabsContent>
</Tabs>
</div>
@@ -22,6 +22,7 @@ import {
Trash2Icon,
TrendingUpIcon,
Upload,
UserIcon,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -631,6 +632,12 @@ export function SkillRow({
const metaNodes = (
<>
{!isBuiltin && skill.owner_username && (
<div className="text-muted-foreground mt-1 flex items-center gap-1 text-xs">
<UserIcon className="size-3" />
发布者:{desensitize(skill.owner_username)}
</div>
)}
{skill.use_count !== undefined && (
<div className="text-muted-foreground mt-1 text-xs">
{skill.use_count > 0
@@ -649,15 +656,15 @@ export function SkillRow({
const actionButtons = (
<>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
onClick={() => setTagPickerOpen(true)}
title={t.adminTags.manageTags}
>
<TagsIcon className="size-3.5" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
onClick={() => setTagPickerOpen(true)}
title={t.adminTags.manageTags}
>
<TagsIcon className="size-3.5" />
</Button>
{canManage && (
<Button
size="icon"
@@ -842,13 +849,13 @@ export function SkillRow({
</DialogFooter>
</DialogContent>
</Dialog>
<TagPickerDialog
open={tagPickerOpen}
onOpenChange={setTagPickerOpen}
targetType="skill"
targetId={skill.name}
currentTags={skill.tags ?? []}
/>
<TagPickerDialog
open={tagPickerOpen}
onOpenChange={setTagPickerOpen}
targetType="skill"
targetId={skill.name}
currentTags={skill.tags ?? []}
/>
<SkillDetailDialog
skillName={skill.name}
open={detailOpen}
@@ -109,13 +109,20 @@ export function SkillDetailDialog({
const canManage = useMemo(() => canManageSkill(skill), [skill]);
const isOwner = useMemo(() => isSkillOwner(skill), [skill]);
// Admins may browse any skill's files (the backend authorizes this); owners
// may browse their own custom skill. Edit-ability is decided separately by
// the backend's `filesData.editable` flag.
const canViewFiles = useMemo(() => {
if (!skill) return false;
return isOwner || getStoredAuth()?.system_role === "admin";
}, [skill, isOwner]);
const {
data: contentData,
isLoading: isContentLoading,
error: contentError,
} = useSkillContent(open ? skillName : null);
const filesEnabled = open && isOwner && activeTab === "files";
const filesEnabled = open && canViewFiles && activeTab === "files";
const {
data: filesData,
isLoading: isFilesLoading,
@@ -227,7 +234,7 @@ export function SkillDetailDialog({
展示适配
</TabsTrigger>
)}
{isOwner && (
{canViewFiles && (
<TabsTrigger value="files" className="text-xs">
<FilesIcon className="mr-1 size-3.5" />
{tx.filesSectionTitle}
@@ -349,7 +356,7 @@ export function SkillDetailDialog({
</TabsContent>
)}
{isOwner && (
{canViewFiles && (
<TabsContent
value="files"
className="m-0 min-h-0 flex-1 overscroll-contain px-6 py-4 data-[state=inactive]:hidden"
@@ -9,16 +9,12 @@ import {
DatabaseZapIcon,
GaugeIcon,
HistoryIcon,
MessageSquareCodeIcon,
MessageSquarePlusIcon,
ScrollTextIcon,
Settings2Icon,
SettingsIcon,
ShieldCheckIcon,
SparklesIcon,
TagsIcon,
TrophyIcon,
UsersIcon,
} from "lucide-react";
import { useEffect, useState } from "react";
@@ -149,36 +145,6 @@ export function WorkspaceNavMenu() {
<Settings2Icon />
{t.common.settings}
</DropdownMenuItem>
{isAdmin && (
<DropdownMenuItem
onClick={() =>
navigate("/page/workspace/settings/recommended-questions")
}
>
<MessageSquarePlusIcon />
推荐问题配置
</DropdownMenuItem>
)}
{isAdmin && (
<DropdownMenuItem
onClick={() =>
navigate("/page/workspace/settings/prompt-prefix")
}
>
<MessageSquareCodeIcon />
问答前缀配置
</DropdownMenuItem>
)}
{isAdmin && (
<DropdownMenuItem
onClick={() =>
navigate("/page/workspace/settings/private-square")
}
>
<ShieldCheckIcon />
私密广场配置
</DropdownMenuItem>
)}
{isAdmin && (
<DropdownMenuItem
onClick={() =>
@@ -199,14 +165,6 @@ export function WorkspaceNavMenu() {
Checkpoint 迁移
</DropdownMenuItem>
)}
{isAdmin && (
<DropdownMenuItem
onClick={() => navigate("/page/workspace/admin")}
>
<UsersIcon />
用户中心
</DropdownMenuItem>
)}
{isAdmin && (
<DropdownMenuItem
onClick={() =>