From 80d2db6315ff327cbed7295187d027b456a18ec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Mon, 8 Jun 2026 17:09:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=AC=E5=BC=80=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=8E=92=E8=A1=8C=E6=A6=9C=E9=A1=B5=E9=9D=A2=20+=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20checkpointer=20=E8=BF=9E=E6=8E=A5=E4=B8=8E=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增公开排行榜页面与后端 public_leaderboard 路由 - 完善 skills/agents 路由 - checkpointer Postgres 改用连接池(自愈), 解决连接关闭后定时任务连环失败 - 修复前端 mermaid 依赖未声明导致 Vite 解析失败 (新增 pnpm-lock) Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 5 +- frontend-web/package.json | 1 + frontend-web/src/App.tsx | 3 + .../workspace/agents/agent-card.tsx | 47 +- .../settings/appearance-settings-page.tsx | 15 + .../settings/memory-settings-page.tsx | 116 ++- .../settings/skill-settings-page.tsx | 39 +- .../workspace/skills/skill-detail-dialog.tsx | 13 +- .../workspace/workspace-nav-menu.tsx | 42 - frontend-web/src/core/agents/types.ts | 22 +- .../src/core/page-layout/sidebar-menu.ts | 23 - frontend-web/src/core/skills/type.ts | 3 + .../src/pages/AdminUserLeaderboardPage.tsx | 251 +++--- .../AdminUserLeaderboardSettingsPage.tsx | 38 +- .../src/pages/PrivateSquareSettingsPage.tsx | 16 +- .../src/pages/PromptPrefixSettingsPage.tsx | 16 +- .../src/pages/PublicLeaderboardPage.tsx | 350 ++++++++ .../src/pages/RecommendedQuestionsPage.tsx | 16 +- offline-backend-20260512/backend/CLAUDE.md | 1 + .../backend/app/gateway/app.py | 794 +++++++++--------- .../backend/app/gateway/routers/agents.py | 343 ++++---- .../app/gateway/routers/public_leaderboard.py | 97 +++ .../backend/app/gateway/routers/skills.py | 41 + .../runtime/checkpointer/async_provider.py | 304 ++++--- offline-backend-20260512/config.yaml | 2 +- pnpm-lock.yaml | 246 ++++++ 26 files changed, 1877 insertions(+), 967 deletions(-) create mode 100644 frontend-web/src/pages/PublicLeaderboardPage.tsx create mode 100644 offline-backend-20260512/backend/app/gateway/routers/public_leaderboard.py create mode 100644 pnpm-lock.yaml diff --git a/.gitignore b/.gitignore index 4aebc103..de6dbc5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Workspace/editor .vscode/ frontend-web/.vscode/ - +node_modules/ # Claude Code local-only settings (per-user permissions, not shared) .claude/settings.local.json @@ -84,6 +84,9 @@ offline-backend-20260512/backend/tests/* deploy .history +# Cloned third-party repos (local only) +git-clone/ + # Repo-root offline dependency pack (local only) /dist/ diff --git a/frontend-web/package.json b/frontend-web/package.json index e54c5f66..c74784bd 100644 --- a/frontend-web/package.json +++ b/frontend-web/package.json @@ -74,6 +74,7 @@ "hast": "^1.0.0", "katex": "^0.16.28", "lucide-react": "^0.562.0", + "mermaid": "11.14.0", "motion": "^12.26.2", "nanoid": "^5.1.6", "next-themes": "^0.4.6", diff --git a/frontend-web/src/App.tsx b/frontend-web/src/App.tsx index 4ca83b8e..752afc7a 100644 --- a/frontend-web/src/App.tsx +++ b/frontend-web/src/App.tsx @@ -19,6 +19,7 @@ import PublicScheduledTaskPage, { PublicScheduledTaskFullscreenPage, } from "./pages/PublicScheduledTaskPage"; import PublicSharePage from "./pages/PublicSharePage"; +import PublicLeaderboardPage from "./pages/PublicLeaderboardPage"; import PublicRoundtableSharePage from "./pages/PublicRoundtableSharePage"; import EmbedChatPage from "./pages/EmbedChatPage"; import { ChatRuntime } from "./pages/WorkspaceRoutes"; @@ -66,6 +67,8 @@ export function App() { } /> {/* Public read-only conversation viewer — no authentication. */} } /> + {/* Public read-only user leaderboard publish page — no authentication. */} + } /> {/* Public read-only roundtable share — landing + full result report. */} ); + // 发布者(仅自定义/已发布的智能体显示;内置无 owner 不显示)。 + const publisherNode = + !isBuiltin && agent.owner_username ? ( +
+ + 发布者:{desensitize(agent.owner_username)} +
+ ) : null; + const skillBadgeItems = [ ...(agent.tool_groups ?? []).map((group) => ({ key: `tg:${group}`, @@ -585,15 +598,15 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) { )} - + {isOwned && ( + {canManage && ( @@ -438,8 +439,8 @@ function PodiumCard({ if (!user) { return ( -
-
第 {rank} 名
+
+
第 {rank} 名
暂无数据
); @@ -450,8 +451,8 @@ function PodiumCard({ type="button" onClick={() => onSelect(user)} className={cn( - "flex min-h-40 flex-col justify-between rounded-lg border bg-white p-4 text-left shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:bg-[#230D0F]", - active ? "border-[#C1121F] ring-2 ring-[#F1D6D6] dark:border-[#F2B8BB] dark:ring-[#C1121F]/20" : "border-[#DADCE0] dark:border-[#462123]", + "flex min-h-40 flex-col justify-between rounded-lg border bg-white p-4 text-left shadow-sm transition hover:-translate-y-0.5 hover:shadow-md dark:bg-[#02244C]", + active ? "border-[#C1121F] dark:border-[#2563EB] ring-2 ring-[#F1D6D6] dark:border-[#7FACF2] dark:ring-[#2563EB]/20" : "border-[#DADCE0] dark:border-[#213D5E]", )} >
@@ -462,11 +463,11 @@ function PodiumCard({ {user.system_role === "admin" && 管理员}
-
+
{initials(user)}
{user.email || user.user_id}
-
{scoreLabel(user, tab)}
+
{scoreLabel(user, tab)}
运行 {fmtNumber(user.run_count)} 问答 {fmtNumber(user.qa_count)} @@ -482,15 +483,15 @@ function TrendBars({ points, compact = false }: { points: AdminLeaderboardTrendP const [metric, setMetric] = useState<"all" | TrendMetric>("all"); const chartId = useId().replace(/:/g, ""); if (points.length === 0) { - return
暂无趋势数据
; + return
暂无趋势数据
; } const visible = points.slice(-32); const series = [ - { key: "runs", label: "运行", color: "#C1121F", soft: "bg-[#FFF1F1] text-[#C1121F]", description: "每天发起的运行次数" }, - { key: "questions", label: "问答", color: "#0F766E", soft: "bg-[#ECFDF5] text-[#0F766E]", description: "每天产生的有效提问次数" }, - { key: "users", label: "活跃", color: "#B45309", soft: "bg-[#FFFBEB] text-[#B45309]", description: "每天有行为的用户数量" }, - { key: "tokens", label: "Token", color: "#7C3AED", soft: "bg-[#F5F3FF] text-[#7C3AED]", description: "每天模型输入输出 Token 总量" }, + { key: "runs", label: "运行", color: "#C1121F", soft: "bg-[#FFF1F1] text-[#C1121F] dark:text-[#7FACF2] dark:bg-[#2563EB]/15 dark:text-[#7FACF2]", description: "每天发起的运行次数" }, + { key: "questions", label: "问答", color: "#0F766E", soft: "bg-[#ECFDF5] text-[#0F766E] dark:bg-teal-300/15 dark:text-teal-100", description: "每天产生的有效提问次数" }, + { key: "users", label: "活跃", color: "#B45309", soft: "bg-[#FFFBEB] text-[#B45309] dark:bg-amber-300/15 dark:text-amber-100", description: "每天有行为的用户数量" }, + { key: "tokens", label: "Token", color: "#7C3AED", soft: "bg-[#F5F3FF] text-[#7C3AED] dark:bg-violet-300/15 dark:text-violet-100", description: "每天模型输入输出 Token 总量" }, ] as const; const activeMetric: TrendMetric = metric === "all" ? "runs" : metric; const activeSeries = series.find((item) => item.key === activeMetric) ?? series[0]; @@ -533,15 +534,15 @@ function TrendBars({ points, compact = false }: { points: AdminLeaderboardTrendP "min-w-0 rounded-lg border px-3 py-2 text-left transition hover:-translate-y-0.5 hover:shadow-sm", large && "px-4 py-3", metric === "all" - ? "border-[#C1121F] bg-[#FFF1F1] shadow-sm dark:border-[#F2B8BB]/40 dark:bg-[#3A1215]" - : "border-[#F1D6D6] bg-white dark:border-[#462123] dark:bg-[#230D0F]", + ? "border-[#C1121F] dark:border-[#2563EB] bg-[#FFF1F1] shadow-sm dark:border-[#7FACF2]/40 dark:bg-[#0D2D54]" + : "border-[#F1D6D6] bg-white dark:border-[#213D5E] dark:bg-[#02244C]", )} > - 全部趋势 + 全部趋势 {fmtNumber(visible.reduce((sum, point) => sum + point.runs + point.questions + point.users, 0))} - {large && 同时显示运行、问答、活跃与 Token} + {large && 同时显示运行、问答、活跃与 Token} )} {series.map((item) => ( @@ -553,8 +554,8 @@ function TrendBars({ points, compact = false }: { points: AdminLeaderboardTrendP "min-w-0 rounded-lg border px-3 py-2 text-left transition hover:-translate-y-0.5 hover:shadow-sm", large && "px-4 py-3", metric === item.key - ? "border-[#C1121F] bg-[#FFF1F1] shadow-sm dark:border-[#F2B8BB]/40 dark:bg-[#3A1215]" - : "border-[#F1D6D6] bg-white dark:border-[#462123] dark:bg-[#230D0F]", + ? "border-[#C1121F] dark:border-[#2563EB] bg-[#FFF1F1] shadow-sm dark:border-[#7FACF2]/40 dark:bg-[#0D2D54]" + : "border-[#F1D6D6] bg-white dark:border-[#213D5E] dark:bg-[#02244C]", )} title={item.description} > @@ -562,7 +563,7 @@ function TrendBars({ points, compact = false }: { points: AdminLeaderboardTrendP {fmtNumber(totalFor(item.key))} - {(large || compact) && {item.description}} + {(large || compact) && {item.description}} ))}
@@ -583,15 +584,15 @@ function TrendBars({ points, compact = false }: { points: AdminLeaderboardTrendP
{metricCards({ includeAll: true, large: true })}
-
-
当前查看
+
+
当前查看
{metric === "all" ? "全部指标" : activeSeries.label}
-
最新 {metric === "all" ? "运行" : activeSeries.label}
+
最新 {metric === "all" ? "运行" : activeSeries.label}
{fmtNumber(latestValue)}
-
= 0 ? "text-[#C1121F]" : "text-[#7F1D1D] dark:text-[#FCA5A5]")}> +
= 0 ? "text-[#C1121F] dark:text-[#7FACF2]" : "text-[#7F1D1D] dark:text-[#FCA5A5]")}> 较上一日 {delta >= 0 ? "+" : ""}{fmtNumber(delta)}
-
+
峰值 {peak.date} / {fmtNumber(Number(peak[activeMetric] ?? 0))}
@@ -679,7 +680,7 @@ function TrendBars({ points, compact = false }: { points: AdminLeaderboardTrendP return (
{metricCards({ includeAll: false })} -
+
每个点代表 1 天;当前显示 {metric === "all" ? "全部指标" : activeSeries.label},覆盖 {activeDays} 个活跃日。 {analysisDialog( "个人使用趋势", @@ -693,7 +694,7 @@ function TrendBars({ points, compact = false }: { points: AdminLeaderboardTrendP return (
-

+

每个折线点代表一天;点击指标卡可单独查看运行、问答、活跃或 Token,全部趋势会同时展示四条多色折线。

@@ -704,13 +705,13 @@ function TrendBars({ points, compact = false }: { points: AdminLeaderboardTrendP )}
-
-
最新 {metric === "all" ? "运行" : activeSeries.label}
+
+
最新 {metric === "all" ? "运行" : activeSeries.label}
{fmtNumber(latestValue)}
-
= 0 ? "text-[#C1121F]" : "text-[#7F1D1D] dark:text-[#FCA5A5]")}> +
= 0 ? "text-[#C1121F] dark:text-[#7FACF2]" : "text-[#7F1D1D] dark:text-[#FCA5A5]")}> 较上一日 {delta >= 0 ? "+" : ""}{fmtNumber(delta)}
-
+
峰值 {peak.date.slice(5)} / {fmtNumber(Number(peak[activeMetric] ?? 0))}
@@ -735,11 +736,11 @@ function UserTable({ }) { const max = Math.max(1, ...users.map((user) => userScore(user, tab))); if (users.length === 0) { - return
暂无用户统计
; + return
暂无用户统计
; } return ( -
+
{users.map((user, index) => { const score = userScore(user, tab); return ( @@ -748,11 +749,11 @@ function UserTable({ type="button" onClick={() => onSelect(user)} className={cn( - "grid w-full grid-cols-[44px_minmax(0,1fr)_minmax(160px,240px)_120px] items-center gap-4 border-b border-[#F1D6D6] px-4 py-3 text-left last:border-b-0 hover:bg-[#FFF7F7] dark:border-[#462123] dark:hover:bg-[#19090A]", - selectedUserId === user.user_id && "bg-[#FFF1F1] dark:bg-[#C1121F]/10", + "grid w-full grid-cols-[44px_minmax(0,1fr)_minmax(160px,240px)_120px] items-center gap-4 border-b border-[#F1D6D6] px-4 py-3 text-left last:border-b-0 hover:bg-[#FFF7F7] dark:border-[#213D5E] dark:hover:bg-[#011226]", + selectedUserId === user.user_id && "bg-[#FFF1F1] dark:bg-[#2563EB]/10", )} > -
+
{rankOffset + index + 1}
@@ -765,9 +766,9 @@ function UserTable({
-
+
@@ -801,7 +802,7 @@ function SkillRanking({ }) { const max = Math.max(1, ...items.map((item) => item.call_count)); return ( -
+
@@ -810,7 +811,7 @@ function SkillRanking({ {items.length} 项
- {items.length === 0 &&
{empty}
} + {items.length === 0 &&
{empty}
} {items.slice(0, 10).map((item, index) => (
{index + 1} @@ -821,7 +822,7 @@ function SkillRanking({ {fmtNumber(item.user_count)} 人
-
+
item.count)); return ( -
+
@@ -868,7 +869,7 @@ function DimensionRanking({ {items.length} 项
- {items.length === 0 &&
{empty}
} + {items.length === 0 &&
{empty}
} {items.slice(0, 8).map((item, index) => (
{index + 1} @@ -879,7 +880,7 @@ function DimensionRanking({ {fmtNumber(item.user_count)} 人
-
+
-
+
+

{title}

@@ -922,8 +923,8 @@ function QuestionPanel({ {items.length}
-
- {items.length === 0 &&
暂无问题记录
} +
+ {items.length === 0 &&
暂无问题记录
} {items.map((item, index) => (
{clampText(item.text, 180)}
@@ -942,8 +943,8 @@ function QuestionPanel({ function Metric({ label, value }: { label: string; value: number }) { return ( -
-
{label}
+
+
{label}
{fmtNumber(value)}
); @@ -971,7 +972,7 @@ function UserDetailPanel({ if (isLoading || !data) { return ( -
+
加载用户详情
@@ -981,13 +982,13 @@ function UserDetailPanel({ return (
-
+
{data.user.email || data.user.user_id}
-
+
最近活跃 {fmtTime(data.user.last_active_at)}
@@ -996,7 +997,7 @@ function UserDetailPanel({
-
+
@@ -1016,7 +1017,7 @@ function UserDetailPanel({
- + 个人使用趋势
@@ -1226,31 +1227,36 @@ export default function AdminUserLeaderboardPage() { }, [apiRange.since, apiRange.until, reportModelName]); return ( -
+
-
-
-
-
- - 管理员分析 +
+
+ {/* 标题维度:仅承载页面标题与说明 */} +
+
+
+ + 管理员分析 +
+

用户排行榜

+

+ 统计用户活跃、问答、Token 消耗、技能热度和近期问题,用于运营和审计。 +

-

用户排行榜

-

- 统计用户活跃、问答、Token 消耗、技能热度和近期问题,用于运营和审计。 -

-
- -
+
+ + {/* 操作维度:与标题区分开的控制工具条(操作按钮 + 时间筛选) */} +
+
+ - + AI 榜单分析报告 基于当前时间段的问答、用户、技能、模型和趋势统计生成运营分析。 -
+
{reportStatus === "cached" && 已读取缓存} {reportResolvedModel && 模型 {reportResolvedModel}} - {reportGeneratedAt && 生成于 {fmtTime(reportGeneratedAt)}} + {reportGeneratedAt && 生成于 {fmtTime(reportGeneratedAt)}}
{data && reportStatus === "idle" && !reportText && (
-
+
- + 最近关注
{data.recent_questions.slice(0, 3).map((item, index) => (
- {fmtTime(item.asked_at)} + {fmtTime(item.asked_at)} {clampText(item.text, 58)}
))} - {data.recent_questions.length === 0 &&
暂无数据
} + {data.recent_questions.length === 0 &&
暂无数据
}
-
+
- + 高频问题
@@ -1337,34 +1353,34 @@ export default function AdminUserLeaderboardPage() { {index + 1}. {clampText(item.text, 72)}
))} - {data.frequent_questions.length === 0 &&
暂无数据
} + {data.frequent_questions.length === 0 &&
暂无数据
}
-
+
- + 使用频率
{[...data.skills.slice(0, 2), ...data.tools.slice(0, 1)].map((item, index) => (
{item.name} - {fmtNumber(item.call_count)} 次 + {fmtNumber(item.call_count)} 次
))} - {data.skills.length + data.tools.length === 0 &&
暂无数据
} + {data.skills.length + data.tools.length === 0 &&
暂无数据
}
)} - + {reportStatus === "idle" && !reportText && ( -
+
选择模型后点击生成;同一时间段、同一模型和同一统计设置会直接读取已保存报告。
)} {reportGenerating && !reportText && ( -
+
正在调用大模型生成报告,内容会流式显示
@@ -1375,13 +1391,13 @@ export default function AdminUserLeaderboardPage() {
)} {reportText && ( -
+
)} {reportText && ( -
+
+
event.currentTarget.showPicker?.()} onFocus={(event) => event.currentTarget.showPicker?.()} onChange={(event) => setRange((prev) => ({ ...prev, since: event.target.value }))} - className="h-9 w-36 bg-white dark:bg-[#19090A]" + className="h-9 w-36 bg-white dark:bg-[#011226]" />
@@ -1413,7 +1431,7 @@ export default function AdminUserLeaderboardPage() { onClick={(event) => event.currentTarget.showPicker?.()} onFocus={(event) => event.currentTarget.showPicker?.()} onChange={(event) => setRange((prev) => ({ ...prev, until: event.target.value }))} - className="h-9 w-36 bg-white dark:bg-[#19090A]" + className="h-9 w-36 bg-white dark:bg-[#011226]" />
+
@@ -1432,12 +1451,12 @@ export default function AdminUserLeaderboardPage() {
{isLoading && ( -
+
正在统计排行榜数据
-
+
线上用户较多或时间范围较大时,统计时间可能较长,请耐心等待。为避免影响其他接口,系统会限制排行榜统计的并发数量。
@@ -1450,12 +1469,12 @@ export default function AdminUserLeaderboardPage() { )} {!isLoading && !error && leaderboardPending && !leaderboardHasRows && ( -
+
- + 排行榜正在后台统计
-
+
{data?.message || "统计任务已放到后台执行,页面会自动刷新结果。这样排行榜慢慢算,不会长时间占住当前接口请求。"}
@@ -1495,7 +1514,7 @@ export default function AdminUserLeaderboardPage() { /> )} -
+

榜单前三

@@ -1534,7 +1553,7 @@ export default function AdminUserLeaderboardPage() { value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索用户邮箱或 ID" - className="h-9 w-64 bg-white pl-8 dark:bg-[#19090A]" + className="h-9 w-64 bg-white pl-8 dark:bg-[#011226]" />
@@ -1558,8 +1577,8 @@ export default function AdminUserLeaderboardPage() {
-
-
+
+

用户详情

@@ -1575,7 +1594,7 @@ export default function AdminUserLeaderboardPage() {
-
+
diff --git a/frontend-web/src/pages/AdminUserLeaderboardSettingsPage.tsx b/frontend-web/src/pages/AdminUserLeaderboardSettingsPage.tsx index 4113c4c1..712c97d1 100644 --- a/frontend-web/src/pages/AdminUserLeaderboardSettingsPage.tsx +++ b/frontend-web/src/pages/AdminUserLeaderboardSettingsPage.tsx @@ -61,17 +61,17 @@ export default function AdminUserLeaderboardSettingsPage() { }; return ( -
+
-
+
-
+
管理员设置

排行榜设置

-

+

这里的规则会影响用户排行榜、趋势、技能、模型、问题与个人详情统计。

@@ -86,14 +86,14 @@ export default function AdminUserLeaderboardSettingsPage() {
-
+
- + 统计排除名单
-

+

名单中的账号不会进入排行榜、趋势、问题、模型和技能统计。

@@ -111,21 +111,21 @@ export default function AdminUserLeaderboardSettingsPage() { value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索账号邮箱或 ID" - className="h-10 border-[#F1D6D6] bg-white pl-9" + className="h-10 border-[#F1D6D6] bg-white pl-9 dark:border-[#213D5E] dark:bg-[#011226] dark:text-white" /> {query.trim() && candidates.length > 0 && ( -
+
{candidates.map((user) => ( @@ -134,15 +134,15 @@ export default function AdminUserLeaderboardSettingsPage() {
)}
-
+
{selectedUsers.length === 0 ? ( - 暂无排除账号 + 暂无排除账号 ) : (
{selectedUsers.map((user) => ( {userLabel(user)} @@ -157,14 +157,14 @@ export default function AdminUserLeaderboardSettingsPage() {
-
+
- + 统计规则
-

+

关闭某项后,该类数据会从榜单、趋势、问题列表和个人统计中移除。

@@ -180,11 +180,11 @@ export default function AdminUserLeaderboardSettingsPage() { ].map(([key, title, desc]) => (