Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions ui/src/api/API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,18 +433,24 @@ API Key 按三个入口分别维护:
共用业务类型在 `types/api-key.ts`,`remark` 字段待后端补充返回与持久化;
两套系统接口沿用 V2 路径,仍需对应部署联调;头像菜单密钥与系统资源管理智能体密钥不能互相替代。

`admin/workspace/application/overview.ts` 独立维护概览接口,沿用 v2 的应用统计协议,在
`/workspace/<workspaceId>/application/<applicationId>` 下提供三个 GET 接口,均传入
`start_time`、`end_time`,由调用方管理 loading:
Workspace 与 System 资源管理的 `application/overview.ts` 独立维护概览接口,资源前缀分别为
`/workspace/<workspaceId>/application/<applicationId>` 与 `/system/resource/application/<applicationId>`。
`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/<currentPage>/<pageSize>` 或 `/top_questions/<currentPage>/<pageSize>` | `ResponsePage<HomeRankingRecord>` |
| `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 条;不使用旧全量数组做前端分页。

### 工作空间首页

Expand Down Expand Up @@ -701,7 +707,7 @@ System 资源管理 API,不包含 Workspace 共享和 System 共享。知识
智能体详情 context 仅装配 Workspace 和 System 资源管理的完整 Application、Overview、Access、ChatLog、
Workflow 与 ChatUser API。System 智能体接口统一使用 `/system/resource/application` 前缀,
详情 GET、配置 PUT 使用 `/<applicationId>`,发布 PUT 使用 `/<applicationId>/publish`;
概览统计、Tokens 排行、提问排行分别为 GET `application_stats`、`application_token_usage`、`top_questions`。
概览统计使用 GET `application_stats`;用户排行分页、汇总与导出约定见上文“智能体概览”,待后台对齐。
发布历史使用 GET `/<applicationId>/application_version`,版本编辑使用 PUT 追加版本 ID。
DebugPanel 与日志正文仍使用现有 Workspace conversation/聊天记录实现,尚未按 System 范围适配。

Expand Down
40 changes: 30 additions & 10 deletions ui/src/api/admin/system/resource-management/application/overview.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
/** 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'

/** 获取智能体每日对话、用户、Tokens 与满意度统计。 */
const getMonitoring = (_workspaceId: string, range: HomeDateRange, applicationId: string) =>
get<HomeMonitoringDay[]>(`${getPrefix()}/${applicationId}/application_stats`, { ...range })

/** 获取智能体按用户排序的 Tokens 消耗。 */
const getTokenUsage = (applicationId: string, range: HomeDateRange) =>
get<ApplicationTokenUsage[]>(`${getPrefix()}/${applicationId}/application_token_usage`, { ...range })
// TODO:后台按首页协议补齐用户排行分页、name 搜索、汇总和导出。
const rankingPaths = { userTokens: 'application_token_usage', questions: 'top_questions' }

/** 获取智能体按用户排序的提问次数。 */
const getTopQuestions = (applicationId: string, range: HomeDateRange) =>
get<ApplicationTopQuestion[]>(`${getPrefix()}/${applicationId}/top_questions`, { ...range })
/** 分页查询当前智能体的用户排行。 */
const getRanking = (applicationId: string, kind: ApplicationRankingKind, page: ParamsPage, range: HomeDateRange, name?: string) =>
get<ResponsePage<HomeRankingRecord>>(`${getPrefix()}/${applicationId}/${rankingPaths[kind]}/${page.currentPage}/${page.pageSize}`, {
...range,
...(name ? { name } : {}),
})

/** 获取当前智能体同期 Tokens 总量。 */
const getTokensAggregation = (applicationId: string, range: HomeDateRange) =>
get<number>(`${getPrefix()}/${applicationId}/tokens/aggregation`, { ...range })

/** 获取当前智能体同期对话次数。 */
const getChatRecordAggregation = (applicationId: string, range: HomeDateRange) =>
get<number>(`${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,
}
40 changes: 30 additions & 10 deletions ui/src/api/admin/workspace/application/overview.ts
Original file line number Diff line number Diff line change
@@ -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`
Expand All @@ -9,16 +10,35 @@ const getPrefix = (workspaceId = getWorkspaceId()) => `/workspace/${workspaceId}
const getMonitoring = (workspaceId: string, range: HomeDateRange, applicationId: string) =>
get<HomeMonitoringDay[]>(`${getPrefix(workspaceId)}/${applicationId}/application_stats`, { ...range })

/** 获取智能体按用户排序的 Tokens 消耗。 */
const getTokenUsage = (applicationId: string, range: HomeDateRange) =>
get<ApplicationTokenUsage[]>(`${getPrefix()}/${applicationId}/application_token_usage`, { ...range })
// TODO:后台按首页协议补齐用户排行分页、name 搜索、汇总和导出。
const rankingPaths = { userTokens: 'application_token_usage', questions: 'top_questions' }

/** 获取智能体按用户排序的提问次数。 */
const getTopQuestions = (applicationId: string, range: HomeDateRange) =>
get<ApplicationTopQuestion[]>(`${getPrefix()}/${applicationId}/top_questions`, { ...range })
/** 分页查询当前智能体的用户排行。 */
const getRanking = (applicationId: string, kind: ApplicationRankingKind, page: ParamsPage, range: HomeDateRange, name?: string) =>
get<ResponsePage<HomeRankingRecord>>(`${getPrefix()}/${applicationId}/${rankingPaths[kind]}/${page.currentPage}/${page.pageSize}`, {
...range,
...(name ? { name } : {}),
})

/** 获取当前智能体同期 Tokens 总量。 */
const getTokensAggregation = (applicationId: string, range: HomeDateRange) =>
get<number>(`${getPrefix()}/${applicationId}/tokens/aggregation`, { ...range })

/** 获取当前智能体同期对话次数。 */
const getChatRecordAggregation = (applicationId: string, range: HomeDateRange) =>
get<number>(`${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,
}
13 changes: 2 additions & 11 deletions ui/src/api/types/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
8 changes: 6 additions & 2 deletions ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 表头排序,切换时清空选择并回到第一页;搜索、筛选和分页保留排序。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ApplicationTokenUsage[]>()
const topQuestions = ref<ApplicationTopQuestion[]>()
const tokenUsage = ref<HomeRankingRecord[]>()
const topQuestions = ref<HomeRankingRecord[]>()
const tokenTotal = ref<number>()
const chatTotal = ref<number>()
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<ApplicationRankingKind>()
function handleOpenRankingDetail(kind: ApplicationRankingKind) {
detailKind.value = kind
}
const rankings = computed<Record<ApplicationRankingKind, HomeRankingRecord[] | undefined>>(() => ({
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
})
Expand All @@ -71,16 +91,25 @@ onMounted(loadRankings)
<MkDateRange @change="handleRangeChange" />
</div>
<div v-loading="loading" class="grid grid-cols-1 gap-4 xl:grid-cols-2">
<!-- 查看当前智能体的完整用户排行 -->
<RankingCard
v-for="option in rankingOptions"
:key="option.kind"
:title="option.title"
:kind="option.kind"
:records="rankings[option.kind]?.slice(0, 5)"
:records="rankings[option.kind]"
:loading="loading"
:total="totals[option.kind]"
@detail="handleOpenRankingDetail(option.kind)"
/>
</div>
<ApplicationRankingDrawer
v-if="detailKind"
:application-id="applicationId"
:initial-kind="detailKind"
:initial-range="range"
@closed="detailKind = undefined"
/>
</section>
</template>

Expand Down
Loading
Loading