diff --git a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx
index 6fdec3c5..1f224cd2 100644
--- a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx
+++ b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx
@@ -12,11 +12,10 @@ import {
FileArchive,
GlobeIcon,
InfoIcon,
- Loader2,
- LockIcon,
- PinIcon,
- PinOffIcon,
- RefreshCwIcon,
+ Loader2,
+ LockIcon,
+ PinIcon,
+ RefreshCwIcon,
SparklesIcon,
TagsIcon,
Trash2Icon,
@@ -27,15 +26,8 @@ import { useRouter } from "next/navigation";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import {
- Card,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-} from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
@@ -203,11 +195,22 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
};
}, [skills, currentUserId]);
- const filteredSkills = filter === "mine" ? activeMine : activeAll;
- const staleCount = staleVisible.length;
- const archivedCount = archivedVisible.length;
-
- const totalPages = Math.max(1, Math.ceil(filteredSkills.length / PAGE_SIZE));
+ const filteredSkills = filter === "mine" ? activeMine : activeAll;
+ const orderedSkills = useMemo(
+ () =>
+ filteredSkills
+ .map((skill, index) => ({ skill, index }))
+ .sort((a, b) => {
+ const pinnedDelta = Number(!!b.skill.pinned) - Number(!!a.skill.pinned);
+ return pinnedDelta || a.index - b.index;
+ })
+ .map(({ skill }) => skill),
+ [filteredSkills],
+ );
+ const staleCount = staleVisible.length;
+ const archivedCount = archivedVisible.length;
+
+ const totalPages = Math.max(1, Math.ceil(orderedSkills.length / PAGE_SIZE));
// Filter / search / tag changes may shrink the result set — pull the page
// back into range without forcing a reset on every keystroke.
useEffect(() => {
@@ -216,10 +219,10 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
useEffect(() => {
setPage(1);
}, [search, tagIds, sort]);
- const pagedSkills = useMemo(
- () => filteredSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
- [filteredSkills, page],
- );
+ const pagedSkills = useMemo(
+ () => orderedSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE),
+ [orderedSkills, page],
+ );
const handleRescan = async () => {
try {
@@ -356,32 +359,18 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
{!isLoading && !error && filteredSkills.length === 0 && (
)}
-
- {/* —— 原纵向列表布局(保留备查)——
- {!isLoading && !error &&
- filteredSkills.length > 0 &&
- pagedSkills.map((skill) => (
-
- ))} */}
- {/* card */}
- {!isLoading && !error && filteredSkills.length > 0 && (
-
- {pagedSkills.map((skill) => (
-
- ))}
-
- )}
+ {!isLoading && !error && filteredSkills.length > 0 && (
+
+ {pagedSkills.map((skill) => (
+
+ ))}
+
+ )}
{!isLoading && !error && totalPages > 1 && (
)}
{canManage && (
-
+ )}
{canTogglePublish && (
)}
- */}
-
- setDetailOpen(true)}
- >
-
-
-
-
-
-
-
- {desensitize(skill.name)}
-
-
- {isBuiltin
- ? t.settings.skills.badgeBuiltin
- : skill.category}
-
-
-
-
-
- {isOwned && (
-
- {t.settings.skills.badgeMine}
-
- )}
- {isFromOthers && (
-
- {t.settings.skills.badgeFromOthers}
-
- )}
- {isOwned && skill.published && (
-
-
- {t.settings.skills.badgePublished}
-
- )}
- {isCustom && !isBuiltin && skill.source === "agent" && (
-
- {t.settings.skills.badgeSourceAgent}
-
- )}
- {isCustom && !isBuiltin && skill.source === "upload" && (
-
- {t.settings.skills.badgeSourceUpload}
-
- )}
- {skill.pinned && (
-
- {t.settings.skills.badgePinned}
-
- )}
-
-
- {skill.description ? (
-
- {desensitize(skill.description)}
-
- ) : null}
-
-
- {skill.use_count !== undefined && skill.use_count > 0
- ? t.settings.skills.useCountInline.replace(
- "{count}",
- String(skill.use_count),
- )
- : t.settings.skills.useCountInlineZero}
-
- 0 ? "pt-2.5" : "min-h-4",
- )}
- >
- {(skill.tags?.length ?? 0) > 0 && (
-
- )}
-
-
-
- e.stopPropagation()}
- >
-
- {isAdmin && (
-
setTagPickerOpen(true)}
- title={t.adminTags.manageTags}
- >
-
-
- )}
- {canManage && (
-
void handleTogglePinned()}
- title={
- skill.pinned
- ? t.settings.skills.unpinAction
- : t.settings.skills.pinAction
- }
- >
- {skill.pinned ? (
-
- ) : (
-
- )}
-
- )}
- {canTogglePublish && (
-
void handleTogglePublished(!skill.published)}
- title={
- skill.published
- ? t.settings.skills.unpublishAction
- : t.settings.skills.publishAction
- }
- >
- {skill.published ? (
-
- ) : (
-
- )}
-
- )}
-
-
-
- {canManage && (
-
setConfirmDeleteOpen(true)}
- title={t.settings.skills.deleteAction}
- >
-
-
- )}
-
-
-
-
- {data && (
+ {data && reportStatus === "idle" && !reportText && (
diff --git a/offline-backend-20260512/backend/app/gateway/routers/admin_users.py b/offline-backend-20260512/backend/app/gateway/routers/admin_users.py
index b34597b6..c7ef2b53 100644
--- a/offline-backend-20260512/backend/app/gateway/routers/admin_users.py
+++ b/offline-backend-20260512/backend/app/gateway/routers/admin_users.py
@@ -318,7 +318,7 @@ def _report_cache_key(
settings,
) -> str:
payload = {
- "version": 4,
+ "version": 5,
"since": _iso(since),
"until": _iso(until),
"limit": limit,
@@ -1091,15 +1091,16 @@ async def _build_leaderboard_report_prompt(
"trends": [t.model_dump() for t in trends[-21:]],
}
return (
- "请基于以下管理员排行榜统计数据,生成一份中文运营分析报告。"
+ "请基于以下管理员排行榜统计数据,生成一份中文用户行为分析报告。"
"报告需要有结论,不要只罗列数字;请使用 Markdown 标题、列表和表格,让前端可以直接渲染。"
"重点要求:"
- "1) 优先突出用户最近关注的实时问题、事件、主题和业务方向,必须引用 recent_questions 中的近期问题作为依据;"
- "2) 单独分析 frequent_questions,归纳高频关注簇,说明这些高频问题反映出的需求强度;"
- "3) 结合 top_users、top_skills、top_tools、top_models 和 trends,说明使用频率、使用增长、常用能力和重点用户画像;"
- "4) 可以提及使用强度、关注变化和运营机会,但不要把报告重点放在系统本身的问题、故障、失败率或回答质量缺陷上;"
- "5) 给管理员 3-5 条可执行建议,建议应围绕用户关注主题、资源配置、内容供给、培训引导和高频能力优化。"
- "输出结构:摘要、最近关注、高频问题与使用频率、重点用户与常用能力、趋势观察、行动建议。"
+ "1) 你是专门负责分析用户行为的专家,不要自称企业 AI 应用运营分析师;"
+ "2) 优先突出用户最近常用问题、最近关注的实时问题、事件、主题和业务方向,必须引用 recent_questions 中的近期问题作为依据;"
+ "3) 单独分析 frequent_questions,归纳高频关注簇,说明这些高频问题反映出的需求强度和重复频率;"
+ "4) 结合 top_users、top_skills、top_tools、top_models 和 trends,说明使用频率、使用增长、常用能力和重点用户行为画像;"
+ "5) 可以提及使用强度、关注变化和行为机会,但不要把报告重点放在系统本身的问题、故障、失败率或回答质量缺陷上;"
+ "6) 给管理员 3-5 条可执行建议,建议应围绕用户关注主题、资源配置、内容供给、培训引导和高频能力优化。"
+ "输出结构:摘要、最近关注、高频问题与频率、重点用户行为、常用能力、趋势观察、行动建议。"
"\n\n统计数据如下:\n"
f"{json.dumps(context, ensure_ascii=False, default=str)}"
)
@@ -1141,7 +1142,7 @@ async def generate_user_leaderboard_report(
model = create_chat_model(name=resolved_model_name, thinking_enabled=thinking_enabled, app_config=config)
response = await model.ainvoke(
[
- SystemMessage(content="你是企业 AI 应用的运营分析师,擅长从用户行为、问题文本和调用指标中生成管理报告;报告聚焦用户关注主题和使用频率,不讨论系统本身问题。"),
+ SystemMessage(content="你是专门负责分析用户行为的专家,擅长从用户最近常用问题、关注主题、使用频率和调用指标中生成管理报告;报告聚焦用户行为,不讨论系统本身问题。"),
HumanMessage(content=prompt),
],
config={"run_name": "admin_user_leaderboard_report"},
@@ -1206,7 +1207,7 @@ async def stream_user_leaderboard_report(
model = create_chat_model(name=resolved_model_name, thinking_enabled=thinking_enabled, app_config=config)
async for chunk in model.astream(
[
- SystemMessage(content="你是企业 AI 应用的运营分析师,擅长从用户行为、问题文本和调用指标中生成管理报告;报告聚焦用户关注主题和使用频率,不讨论系统本身问题。"),
+ SystemMessage(content="你是专门负责分析用户行为的专家,擅长从用户最近常用问题、关注主题、使用频率和调用指标中生成管理报告;报告聚焦用户行为,不讨论系统本身问题。"),
HumanMessage(content=prompt),
],
config={"run_name": "admin_user_leaderboard_report"},