diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index 2bf71897c47..f9f5855b76d 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -433,18 +433,24 @@ API Key 按三个入口分别维护: 共用业务类型在 `types/api-key.ts`,`remark` 字段待后端补充返回与持久化; 两套系统接口沿用 V2 路径,仍需对应部署联调;头像菜单密钥与系统资源管理智能体密钥不能互相替代。 -`admin/workspace/application/overview.ts` 独立维护概览接口,沿用 v2 的应用统计协议,在 -`/workspace//application/` 下提供三个 GET 接口,均传入 -`start_time`、`end_time`,由调用方管理 loading: +Workspace 与 System 资源管理的 `application/overview.ts` 独立维护概览接口,资源前缀分别为 +`/workspace//application/` 与 `/system/resource/application/`。 +`getMonitoring(workspaceId, range, applicationId)` 继续请求 `/application_stats`,返回 `HomeMonitoringDay[]`。 -- `getMonitoring(workspaceId, range, applicationId)` 请求 `/application_stats`,返回 `HomeMonitoringDay[]`,供概览复用首页监控。 -- `getTokenUsage` 请求 `/application_token_usage`,返回 `ApplicationTokenUsage[]`,字段为 - `username`、`token_usage`。 -- `getTopQuestions` 请求 `/top_questions`,返回 `ApplicationTopQuestion[]`,字段为 - `username`、`chat_record_count`。 +用户排行按首页协议预接入,以下分页、汇总及导出约定待后台实现,两套 API 均保留 TODO: -两类用户排行返回按使用量降序排列的完整数组,不使用首页排行的分页、总量或导出接口。 -业务类型经 `@/api/types` 导出;Tokens 接口不包含用户对话次数,不据此展示每轮平均消耗。 +| 方法 | GET 路径(相对资源前缀) | 返回 | +| ---- | ----------------------- | ---- | +| `getRanking` | `/application_token_usage//` 或 `/top_questions//` | `ResponsePage` | +| `getTokensAggregation` | `/tokens/aggregation` | 数值 | +| `getChatRecordAggregation` | `/chat_record/aggregation` | 数值 | +| `exportRanking` | `/application_token_usage/export` 或 `/top_questions/export` | Excel 文件 | + +排行类型 `ApplicationRankingKind` 为 `userTokens`/`questions`,两者均按用户统计,分别按 Tokens/对话次数降序。 +分页返回 `records`、`total`、`current`、`size`;记录沿用首页字段,需提供唯一 `chat_user_id`、 +`asker.username`、`total_tokens`、`chat_record_count`。均 Tokens/次由前端计算,不按用户名拼接两个接口。 +所有查询传入 `start_time`、`end_time`,分页和导出额外支持 `name`;汇总返回当前智能体同期总量,不受用户名搜索影响。 +导出不传分页参数,返回全部匹配记录。TOP5 请求第一页 5 条,详情默认每页 20 条;不使用旧全量数组做前端分页。 ### 工作空间首页 @@ -701,7 +707,7 @@ System 资源管理 API,不包含 Workspace 共享和 System 共享。知识 智能体详情 context 仅装配 Workspace 和 System 资源管理的完整 Application、Overview、Access、ChatLog、 Workflow 与 ChatUser API。System 智能体接口统一使用 `/system/resource/application` 前缀, 详情 GET、配置 PUT 使用 `/`,发布 PUT 使用 `//publish`; -概览统计、Tokens 排行、提问排行分别为 GET `application_stats`、`application_token_usage`、`top_questions`。 +概览统计使用 GET `application_stats`;用户排行分页、汇总与导出约定见上文“智能体概览”,待后台对齐。 发布历史使用 GET `//application_version`,版本编辑使用 PUT 追加版本 ID。 DebugPanel 与日志正文仍使用现有 Workspace conversation/聊天记录实现,尚未按 System 范围适配。 diff --git a/ui/src/api/admin/system/resource-management/application/overview.ts b/ui/src/api/admin/system/resource-management/application/overview.ts index d4faf68636d..a768589a968 100644 --- a/ui/src/api/admin/system/resource-management/application/overview.ts +++ b/ui/src/api/admin/system/resource-management/application/overview.ts @@ -1,6 +1,7 @@ /** System 资源管理智能体概览的使用统计与用户排行接口。 */ -import { get } from '@/api/admin/core/request' -import type { ApplicationTokenUsage, ApplicationTopQuestion, HomeDateRange, HomeMonitoringDay } from '@/api/types' +import { get, getExportFile } from '@/api/admin/core/request' +import type { ParamsPage, ResponsePage } from '@/api/admin/core/types' +import type { ApplicationRankingKind, HomeDateRange, HomeMonitoringDay, HomeRankingRecord } from '@/api/types' const getPrefix = () => '/system/resource/application' @@ -8,16 +9,35 @@ const getPrefix = () => '/system/resource/application' const getMonitoring = (_workspaceId: string, range: HomeDateRange, applicationId: string) => get(`${getPrefix()}/${applicationId}/application_stats`, { ...range }) -/** 获取智能体按用户排序的 Tokens 消耗。 */ -const getTokenUsage = (applicationId: string, range: HomeDateRange) => - get(`${getPrefix()}/${applicationId}/application_token_usage`, { ...range }) +// TODO:后台按首页协议补齐用户排行分页、name 搜索、汇总和导出。 +const rankingPaths = { userTokens: 'application_token_usage', questions: 'top_questions' } -/** 获取智能体按用户排序的提问次数。 */ -const getTopQuestions = (applicationId: string, range: HomeDateRange) => - get(`${getPrefix()}/${applicationId}/top_questions`, { ...range }) +/** 分页查询当前智能体的用户排行。 */ +const getRanking = (applicationId: string, kind: ApplicationRankingKind, page: ParamsPage, range: HomeDateRange, name?: string) => + get>(`${getPrefix()}/${applicationId}/${rankingPaths[kind]}/${page.currentPage}/${page.pageSize}`, { + ...range, + ...(name ? { name } : {}), + }) + +/** 获取当前智能体同期 Tokens 总量。 */ +const getTokensAggregation = (applicationId: string, range: HomeDateRange) => + get(`${getPrefix()}/${applicationId}/tokens/aggregation`, { ...range }) + +/** 获取当前智能体同期对话次数。 */ +const getChatRecordAggregation = (applicationId: string, range: HomeDateRange) => + get(`${getPrefix()}/${applicationId}/chat_record/aggregation`, { ...range }) + +/** 按日期与用户名筛选导出完整用户排行。 */ +const exportRanking = (applicationId: string, kind: ApplicationRankingKind, range: HomeDateRange, name?: string) => + getExportFile(`${rankingPaths[kind]}.xlsx`, `${getPrefix()}/${applicationId}/${rankingPaths[kind]}/export`, { + ...range, + ...(name ? { name } : {}), + }) export default { getMonitoring, - getTokenUsage, - getTopQuestions, + getRanking, + getTokensAggregation, + getChatRecordAggregation, + exportRanking, } diff --git a/ui/src/api/admin/workspace/application/overview.ts b/ui/src/api/admin/workspace/application/overview.ts index 83d8c1ec2de..510ded36360 100644 --- a/ui/src/api/admin/workspace/application/overview.ts +++ b/ui/src/api/admin/workspace/application/overview.ts @@ -1,6 +1,7 @@ /** 工作空间智能体概览的使用统计与用户排行接口。 */ -import { get } from '../../core/request' -import type { ApplicationTokenUsage, ApplicationTopQuestion, HomeDateRange, HomeMonitoringDay } from '@/api/types' +import { get, getExportFile } from '../../core/request' +import type { ParamsPage, ResponsePage } from '../../core/types' +import type { ApplicationRankingKind, HomeDateRange, HomeMonitoringDay, HomeRankingRecord } from '@/api/types' import { getWorkspaceId } from '@/utils/resource-context' const getPrefix = (workspaceId = getWorkspaceId()) => `/workspace/${workspaceId}/application` @@ -9,16 +10,35 @@ const getPrefix = (workspaceId = getWorkspaceId()) => `/workspace/${workspaceId} const getMonitoring = (workspaceId: string, range: HomeDateRange, applicationId: string) => get(`${getPrefix(workspaceId)}/${applicationId}/application_stats`, { ...range }) -/** 获取智能体按用户排序的 Tokens 消耗。 */ -const getTokenUsage = (applicationId: string, range: HomeDateRange) => - get(`${getPrefix()}/${applicationId}/application_token_usage`, { ...range }) +// TODO:后台按首页协议补齐用户排行分页、name 搜索、汇总和导出。 +const rankingPaths = { userTokens: 'application_token_usage', questions: 'top_questions' } -/** 获取智能体按用户排序的提问次数。 */ -const getTopQuestions = (applicationId: string, range: HomeDateRange) => - get(`${getPrefix()}/${applicationId}/top_questions`, { ...range }) +/** 分页查询当前智能体的用户排行。 */ +const getRanking = (applicationId: string, kind: ApplicationRankingKind, page: ParamsPage, range: HomeDateRange, name?: string) => + get>(`${getPrefix()}/${applicationId}/${rankingPaths[kind]}/${page.currentPage}/${page.pageSize}`, { + ...range, + ...(name ? { name } : {}), + }) + +/** 获取当前智能体同期 Tokens 总量。 */ +const getTokensAggregation = (applicationId: string, range: HomeDateRange) => + get(`${getPrefix()}/${applicationId}/tokens/aggregation`, { ...range }) + +/** 获取当前智能体同期对话次数。 */ +const getChatRecordAggregation = (applicationId: string, range: HomeDateRange) => + get(`${getPrefix()}/${applicationId}/chat_record/aggregation`, { ...range }) + +/** 按日期与用户名筛选导出完整用户排行。 */ +const exportRanking = (applicationId: string, kind: ApplicationRankingKind, range: HomeDateRange, name?: string) => + getExportFile(`${rankingPaths[kind]}.xlsx`, `${getPrefix()}/${applicationId}/${rankingPaths[kind]}/export`, { + ...range, + ...(name ? { name } : {}), + }) export default { getMonitoring, - getTokenUsage, - getTopQuestions, + getRanking, + getTokensAggregation, + getChatRecordAggregation, + exportRanking, } diff --git a/ui/src/api/types/application.ts b/ui/src/api/types/application.ts index 8011a7b646d..cef0bb08296 100644 --- a/ui/src/api/types/application.ts +++ b/ui/src/api/types/application.ts @@ -194,14 +194,5 @@ export interface ApplicationStoreResponse { apps: ApplicationStoreTemplate[] } -/** 智能体概览的用户 Tokens 消耗,接口返回完整排行。 */ -export interface ApplicationTokenUsage { - username: string - token_usage: number | null -} - -/** 智能体概览的用户提问次数,接口返回完整排行。 */ -export interface ApplicationTopQuestion { - username: string - chat_record_count: number -} +/** 智能体概览的两类用户排行。 */ +export type ApplicationRankingKind = 'userTokens' | 'questions' diff --git a/ui/src/views/VIEW_README.md b/ui/src/views/VIEW_README.md index 9cbca9d1b04..62cba9964a3 100644 --- a/ui/src/views/VIEW_README.md +++ b/ui/src/views/VIEW_README.md @@ -389,8 +389,12 @@ View 负责路由、资源与模式、图加载、保存/发布/调试编排 三类场景统一调用 `getMonitoring(workspaceId, range, applicationId)`;全部智能体使用空 ID, 两类首页不提交智能体筛选,概览传当前智能体 ID。页面直接传入完整 API,不包装接口。 排行由概览本地 `components/ApplicationRanking.vue` 调用 Overview API,展示用户 Tokens 消耗与提问次数两类 TOP5。 - 应用排行复用首页 `RankingCard`,由概览转换展示字段;占比使用当前应用同期完整排行总量。 - 概览内暂时隐藏卡片详情入口,不接入排行抽屉、搜索、分页或导出。 + 应用排行复用首页 `RankingCard`,TOP5 请求第一页 5 条,占比使用独立查询的当前智能体同期总量。 + 两类卡片详情共用概览本地排行抽屉,标题为“排行榜详情”,两个用户排行页签默认选中入口对应类型。 + 打开时继承概览日期,抽屉内日期独立筛选;用户名搜索、分页与导出走当前资源范围的 Overview API,默认每页 20 条。 + 搜索与日期变更回第一页,切换页签保留日期、清空搜索并回第一页;导出当前筛选下的全部记录。 + 表格展示排名、用户、当前指标、占比、另一指标及均 Tokens/次,排名跨页连续;搜索不改变同期占比分母。 + 两类排行均需返回用户 Tokens 和对话次数,不按用户名合并数据;新接口约定待后台对齐,见 API 文档。 - 触发器执行记录的执行时间列使用服务端排序,独立 `orderBy` 经 `toOrderBy` 添加 `ett.` 前缀, 通过查询参数 `order` 传入。切换排序回第一页;搜索、分页和详情跨页保留排序,重新打开抽屉恢复默认排序。 - 触发器列表的下次执行时间、创建时间使用 `order_by` 表头排序,切换时清空选择并回到第一页;搜索、筛选和分页保留排序。 diff --git a/ui/src/views/application-detail/overview/components/ApplicationRanking.vue b/ui/src/views/application-detail/overview/components/ApplicationRanking.vue index de46706e97b..24496112388 100644 --- a/ui/src/views/application-detail/overview/components/ApplicationRanking.vue +++ b/ui/src/views/application-detail/overview/components/ApplicationRanking.vue @@ -2,57 +2,77 @@ import { useApplicationDetailContext } from '../../context' import { computed, onMounted, ref } from 'vue' -import type { ApplicationTokenUsage, ApplicationTopQuestion, HomeRankingRecord } from '@/api/types' +import type { ApplicationRankingKind, HomeRankingRecord } from '@/api/types' import MkDateRange from '@/components/mk-date-range/index.vue' import type { MkDateRangeValue } from '@/components/mk-date-range/types' import { beforeDay } from '@/utils/time' import RankingCard from '@/views/home/components/ranking/RankingCard.vue' +import ApplicationRankingDrawer from './ApplicationRankingDrawer.vue' const { resourceApi } = useApplicationDetailContext() defineOptions({ name: 'ApplicationRanking' }) const props = defineProps<{ applicationId: string }>() -/* 当前智能体的用户排行,接口返回完整数组。 */ +/* TOP5 使用分页首屏,汇总数据独立查询。 */ const range = ref({ start_time: beforeDay(7), end_time: beforeDay(0) }) const loading = ref(false) -const tokenUsage = ref() -const topQuestions = ref() +const tokenUsage = ref() +const topQuestions = ref() +const tokenTotal = ref() +const chatTotal = ref() const rankingOptions = [ { kind: 'userTokens', title: 'Tokens 消耗 · Top 用户' }, { kind: 'questions', title: '对话次数 · Top 用户' }, ] as const -const rankings = computed<{ userTokens: HomeRankingRecord[] | undefined; questions: HomeRankingRecord[] | undefined }>(() => ({ - userTokens: tokenUsage.value?.map((record) => ({ - asker: { username: record.username }, - total_tokens: record.token_usage ?? 0, - chat_record_count: 0, - })), - questions: topQuestions.value?.map((record) => ({ name: record.username, chat_record_count: record.chat_record_count })), +const detailKind = ref() +function handleOpenRankingDetail(kind: ApplicationRankingKind) { + detailKind.value = kind +} +const rankings = computed>(() => ({ + userTokens: tokenUsage.value, + questions: topQuestions.value?.map((record) => ({ ...record, name: record.asker?.username })), })) const totals = computed(() => ({ - userTokens: tokenUsage.value?.reduce((total, record) => total + (record.token_usage ?? 0), 0), - questions: topQuestions.value?.reduce((total, record) => total + record.chat_record_count, 0), + userTokens: tokenTotal.value, + questions: chatTotal.value, })) function loadRankings() { loading.value = true + const query = { ...range.value } return Promise.allSettled([ resourceApi.overview - .getTokenUsage(props.applicationId, range.value) - .then((records) => { - tokenUsage.value = records + .getRanking(props.applicationId, 'userTokens', { currentPage: 1, pageSize: 5 }, query) + .then((result) => { + tokenUsage.value = result.records }) .catch(() => { tokenUsage.value = undefined }), resourceApi.overview - .getTopQuestions(props.applicationId, range.value) - .then((records) => { - topQuestions.value = records + .getRanking(props.applicationId, 'questions', { currentPage: 1, pageSize: 5 }, query) + .then((result) => { + topQuestions.value = result.records }) .catch(() => { topQuestions.value = undefined }), + resourceApi.overview + .getTokensAggregation(props.applicationId, query) + .then((value) => { + tokenTotal.value = value + }) + .catch(() => { + tokenTotal.value = undefined + }), + resourceApi.overview + .getChatRecordAggregation(props.applicationId, query) + .then((value) => { + chatTotal.value = value + }) + .catch(() => { + chatTotal.value = undefined + }), ]).finally(() => { loading.value = false }) @@ -71,16 +91,25 @@ onMounted(loadRankings)
+
+ diff --git a/ui/src/views/application-detail/overview/components/ApplicationRankingDrawer.vue b/ui/src/views/application-detail/overview/components/ApplicationRankingDrawer.vue new file mode 100644 index 00000000000..0fc02bda2fe --- /dev/null +++ b/ui/src/views/application-detail/overview/components/ApplicationRankingDrawer.vue @@ -0,0 +1,160 @@ + + +