diff --git a/backend/internal/handler/admin/ops_handler.go b/backend/internal/handler/admin/ops_handler.go index 0ae93e65..b9558b97 100644 --- a/backend/internal/handler/admin/ops_handler.go +++ b/backend/internal/handler/admin/ops_handler.go @@ -110,6 +110,9 @@ func (h *OpsHandler) GetErrorLogs(c *gin.Context) { filter.Source = strings.TrimSpace(c.Query("error_source")) filter.Query = strings.TrimSpace(c.Query("q")) filter.UserQuery = strings.TrimSpace(c.Query("user_query")) + // Model 过滤:admin 走精确匹配(ModelFuzzy 默认 false,保持管理端语义)。 + // buildOpsErrorLogsWhere 以 COALESCE(requested_model, model) 比对。 + filter.Model = strings.TrimSpace(c.Query("model")) // Force request errors: client-visible status >= 400. // buildOpsErrorLogsWhere already applies this for non-upstream phase. @@ -227,6 +230,9 @@ func (h *OpsHandler) ListRequestErrors(c *gin.Context) { filter.Source = strings.TrimSpace(c.Query("error_source")) filter.Query = strings.TrimSpace(c.Query("q")) filter.UserQuery = strings.TrimSpace(c.Query("user_query")) + // Model 过滤:admin 走精确匹配(ModelFuzzy 默认 false,保持管理端语义)。 + // buildOpsErrorLogsWhere 以 COALESCE(requested_model, model) 比对。 + filter.Model = strings.TrimSpace(c.Query("model")) // Force request errors: client-visible status >= 400. // buildOpsErrorLogsWhere already applies this for non-upstream phase. diff --git a/frontend/src/api/admin/ops.ts b/frontend/src/api/admin/ops.ts index defdcf2e..a3a47c2c 100644 --- a/frontend/src/api/admin/ops.ts +++ b/frontend/src/api/admin/ops.ts @@ -907,6 +907,9 @@ export interface OpsErrorLog { user_id?: number | null user_email: string api_key_id?: number | null + // 关联 api_key 名称(后端 LEFT JOIN api_keys;软删保留 name,故已删 key 仍有原名)。 + api_key_name?: string + api_key_deleted?: boolean account_id?: number | null account_name: string group_id?: number | null @@ -1086,6 +1089,8 @@ export type OpsErrorListQueryParams = { account_id?: number | null user_id?: number api_key_id?: number + // 模型过滤:后端以 COALESCE(requested_model, model) 精确匹配(admin 路径)。 + model?: string phase?: string error_owner?: string diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 504b5dec..940a21ac 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4776,6 +4776,8 @@ export default { group: 'Group', user: 'User', userId: 'User ID', + apiKey: 'API Key', + keyDeletedBadge: 'Key Deleted', account: 'Account', accountId: 'Account ID', status: 'Status', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 423006d0..5ec4d688 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -4935,6 +4935,8 @@ export default { group: '分组', user: '用户', userId: '用户 ID', + apiKey: 'API Key', + keyDeletedBadge: 'Key 已删除', account: '账号', accountId: '账号 ID', status: '状态码', diff --git a/frontend/src/views/admin/UsageView.vue b/frontend/src/views/admin/UsageView.vue index 8bac1c50..4fb2caac 100644 --- a/frontend/src/views/admin/UsageView.vue +++ b/frontend/src/views/admin/UsageView.vue @@ -647,6 +647,9 @@ const loadAdminErrors = async () => { end_time: toRFC3339(filters.value.end_date, true), user_id: filters.value.user_id ?? undefined, api_key_id: filters.value.api_key_id ?? undefined, + account_id: filters.value.account_id ?? undefined, + group_id: filters.value.group_id ?? undefined, + model: filters.value.model || undefined, }) errRows.value = resp.items errTotal.value = resp.total diff --git a/frontend/src/views/admin/__tests__/UsageView.spec.ts b/frontend/src/views/admin/__tests__/UsageView.spec.ts index 8c644a75..1da0d09c 100644 --- a/frontend/src/views/admin/__tests__/UsageView.spec.ts +++ b/frontend/src/views/admin/__tests__/UsageView.spec.ts @@ -3,7 +3,7 @@ import { flushPromises, mount } from '@vue/test-utils' import UsageView from '../UsageView.vue' -const { list, getStats, getSnapshotV2, getById, getModelStats } = vi.hoisted(() => { +const { list, getStats, getSnapshotV2, getById, getModelStats, listErrorLogs } = vi.hoisted(() => { vi.stubGlobal('localStorage', { getItem: vi.fn(() => null), setItem: vi.fn(), @@ -16,6 +16,7 @@ const { list, getStats, getSnapshotV2, getById, getModelStats } = vi.hoisted(() getSnapshotV2: vi.fn(), getById: vi.fn(), getModelStats: vi.fn(), + listErrorLogs: vi.fn(), } }) @@ -55,6 +56,10 @@ vi.mock('@/api/admin/usage', () => ({ }, })) +vi.mock('@/api/admin/ops', () => ({ + listErrorLogs, +})) + vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError: vi.fn(), @@ -289,3 +294,61 @@ describe('admin UsageView handleUserClick', () => { expect(getById).toHaveBeenCalledWith(2, true) }) }) + +describe('admin UsageView errors tab filter forwarding', () => { + beforeEach(() => { + vi.useFakeTimers() + list.mockReset() + getStats.mockReset() + getSnapshotV2.mockReset() + getModelStats.mockReset() + listErrorLogs.mockReset() + + list.mockResolvedValue({ items: [], total: 0, pages: 0 }) + getStats.mockResolvedValue({ + total_requests: 0, total_input_tokens: 0, total_output_tokens: 0, + total_cache_tokens: 0, total_tokens: 0, total_cost: 0, total_actual_cost: 0, average_duration_ms: 0, + }) + getSnapshotV2.mockResolvedValue({ trend: [], models: [], groups: [] }) + getModelStats.mockResolvedValue({ models: [] }) + listErrorLogs.mockResolvedValue({ items: [], total: 0, pages: 0 }) + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('forwards model/account_id/group_id to listErrorLogs on the errors tab', async () => { + const wrapper = mount(UsageView, { + global: { stubs: { + AppLayout: AppLayoutStub, UsageStatsCards: true, UsageFilters: UsageFiltersStub, + UsageTable: true, UsageExportProgress: true, UsageCleanupDialog: true, + UserBalanceHistoryModal: true, AuditLogModal: true, Pagination: true, Select: true, + DateRangePicker: true, Icon: true, TokenUsageTrend: true, + ModelDistributionChart: true, GroupDistributionChart: true, EndpointDistributionChart: true, + OpsErrorLogTable: true, OpsErrorDetailModal: true, + } }, + }) + vi.advanceTimersByTime(120) + await flushPromises() + + // 模拟用户在过滤器里选择了模型/账户/分组 + const vm = wrapper.vm as any + vm.filters.model = 'gpt-5.3-codex' + vm.filters.account_id = 7 + vm.filters.group_id = 3 + await flushPromises() + + // 切换到「错误请求」标签(第二个 .tab 按钮)触发 loadAdminErrors + const tabs = wrapper.findAll('button.tab') + await tabs[1].trigger('click') + await flushPromises() + + expect(listErrorLogs).toHaveBeenCalledWith(expect.objectContaining({ + view: 'all', + model: 'gpt-5.3-codex', + account_id: 7, + group_id: 3, + })) + }) +}) diff --git a/frontend/src/views/admin/ops/components/OpsErrorLogTable.vue b/frontend/src/views/admin/ops/components/OpsErrorLogTable.vue index d779f7b1..becf6327 100644 --- a/frontend/src/views/admin/ops/components/OpsErrorLogTable.vue +++ b/frontend/src/views/admin/ops/components/OpsErrorLogTable.vue @@ -32,6 +32,12 @@