Merge remote-tracking branch 'origin/main' into feat/grok-sso-device-oauth
# Conflicts: # frontend/src/api/admin/grok.ts
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
ADMIN_UI_REQUEST_HEADER,
|
||||
shouldMarkAdminUIRequest,
|
||||
} from '@/api/adminUIRequest'
|
||||
|
||||
describe('Admin UI request marker', () => {
|
||||
it('uses the stable request header name', () => {
|
||||
expect(ADMIN_UI_REQUEST_HEADER).toBe('X-Admin-UI-Request')
|
||||
})
|
||||
|
||||
it.each([
|
||||
'/admin',
|
||||
'/admin/users',
|
||||
'/api/v1/admin',
|
||||
'/api/v1/admin/accounts?status=active',
|
||||
'https://api.example.test/api/v1/admin/dashboard',
|
||||
])('marks Admin API request %s before page navigation', (requestURL) => {
|
||||
expect(shouldMarkAdminUIRequest(requestURL, '/login')).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['/keys', '/groups/available', '/auth/me', '/announcements'])(
|
||||
'marks shared request %s while an Admin page is active',
|
||||
(requestURL) => {
|
||||
expect(shouldMarkAdminUIRequest(requestURL, '/admin/dashboard')).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
['/keys', '/dashboard'],
|
||||
['/api/v1/administer', '/dashboard'],
|
||||
['/keys', '/administrator'],
|
||||
['', '/'],
|
||||
])('does not mark request %s on page %s', (requestURL, pagePath) => {
|
||||
expect(shouldMarkAdminUIRequest(requestURL, pagePath)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -12,6 +12,7 @@ describe('API Client', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
localStorage.clear()
|
||||
window.history.replaceState({}, '', '/')
|
||||
// 每次测试重新导入以获取干净的模块状态
|
||||
vi.resetModules()
|
||||
const mod = await import('@/api/client')
|
||||
@@ -120,6 +121,55 @@ describe('API Client', () => {
|
||||
const config = adapter.mock.calls[0][0]
|
||||
expect(config.withCredentials).toBe(true)
|
||||
})
|
||||
|
||||
it('Admin API 在进入管理页面前也带 Admin UI 标记', async () => {
|
||||
const adapter = vi.fn().mockResolvedValue({
|
||||
status: 200,
|
||||
data: { code: 0, data: {} },
|
||||
headers: {},
|
||||
config: {},
|
||||
statusText: 'OK',
|
||||
})
|
||||
apiClient.defaults.adapter = adapter
|
||||
|
||||
await apiClient.get('/admin/users')
|
||||
|
||||
const config = adapter.mock.calls[0][0]
|
||||
expect(config.headers.get('X-Admin-UI-Request')).toBe('1')
|
||||
})
|
||||
|
||||
it('管理页面调用共享 API 时带 Admin UI 标记', async () => {
|
||||
window.history.replaceState({}, '', '/admin/dashboard')
|
||||
const adapter = vi.fn().mockResolvedValue({
|
||||
status: 200,
|
||||
data: { code: 0, data: {} },
|
||||
headers: {},
|
||||
config: {},
|
||||
statusText: 'OK',
|
||||
})
|
||||
apiClient.defaults.adapter = adapter
|
||||
|
||||
await apiClient.get('/groups/available')
|
||||
|
||||
const config = adapter.mock.calls[0][0]
|
||||
expect(config.headers.get('X-Admin-UI-Request')).toBe('1')
|
||||
})
|
||||
|
||||
it('普通用户页面调用共享 API 时不带 Admin UI 标记', async () => {
|
||||
const adapter = vi.fn().mockResolvedValue({
|
||||
status: 200,
|
||||
data: { code: 0, data: {} },
|
||||
headers: {},
|
||||
config: {},
|
||||
statusText: 'OK',
|
||||
})
|
||||
apiClient.defaults.adapter = adapter
|
||||
|
||||
await apiClient.get('/groups/available')
|
||||
|
||||
const config = adapter.mock.calls[0][0]
|
||||
expect(config.headers.get('X-Admin-UI-Request')).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
// --- 响应拦截器 ---
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
*/
|
||||
|
||||
import { apiClient } from '../client'
|
||||
import type { GrokBillingSummary, GrokQuotaWindow, WindowStats } from '@/types'
|
||||
|
||||
export type { GrokBillingSummary, GrokQuotaWindow } from '@/types'
|
||||
|
||||
export interface GrokAuthUrlResponse {
|
||||
auth_url: string
|
||||
@@ -79,13 +82,6 @@ export function getGrokSSOImportTimeout(keyCount: number): number {
|
||||
return batches * GROK_SSO_IMPORT_TIMEOUT_PER_BATCH_MS + GROK_SSO_IMPORT_TIMEOUT_BUFFER_MS
|
||||
}
|
||||
|
||||
export interface GrokQuotaWindow {
|
||||
limit?: number | null
|
||||
remaining?: number | null
|
||||
reset_unix?: number | null
|
||||
reset_at?: string | null
|
||||
}
|
||||
|
||||
export interface GrokQuotaSnapshot {
|
||||
requests?: GrokQuotaWindow | null
|
||||
tokens?: GrokQuotaWindow | null
|
||||
@@ -102,13 +98,18 @@ export interface GrokQuotaSnapshot {
|
||||
}
|
||||
|
||||
export interface GrokQuotaProbeResult {
|
||||
source: 'active_probe'
|
||||
model: string
|
||||
source: 'active_probe' | 'billing_probe' | 'hybrid_probe'
|
||||
model?: string
|
||||
billing?: GrokBillingSummary | null
|
||||
snapshot?: GrokQuotaSnapshot | null
|
||||
local_usage_7d?: WindowStats | null
|
||||
local_usage_monthly?: WindowStats | null
|
||||
status_code?: number
|
||||
headers_observed: boolean
|
||||
reset_supported: boolean
|
||||
fetched_at: number
|
||||
persisted?: boolean
|
||||
probe_error?: string
|
||||
}
|
||||
|
||||
export interface GrokQuotaResetResult {
|
||||
|
||||
@@ -828,6 +828,7 @@ export interface OpsRuntimeLogConfig {
|
||||
export interface OpsSystemLog {
|
||||
id: number
|
||||
created_at: string
|
||||
host: string
|
||||
level: string
|
||||
component: string
|
||||
message: string
|
||||
@@ -849,6 +850,7 @@ export interface OpsSystemLogQuery {
|
||||
time_range?: '5m' | '30m' | '1h' | '6h' | '24h' | '7d' | '30d'
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
host?: string
|
||||
level?: string
|
||||
component?: string
|
||||
request_id?: string
|
||||
@@ -864,6 +866,7 @@ export interface OpsSystemLogQuery {
|
||||
export interface OpsSystemLogCleanupRequest {
|
||||
start_time?: string
|
||||
end_time?: string
|
||||
host?: string
|
||||
level?: string
|
||||
component?: string
|
||||
request_id?: string
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
export const ADMIN_UI_REQUEST_HEADER = 'X-Admin-UI-Request'
|
||||
|
||||
function isAdminPath(path: string): boolean {
|
||||
return (
|
||||
path === '/admin' ||
|
||||
path.startsWith('/admin/') ||
|
||||
path === '/api/v1/admin' ||
|
||||
path.startsWith('/api/v1/admin/')
|
||||
)
|
||||
}
|
||||
|
||||
function requestPath(rawURL: string): string {
|
||||
const value = rawURL.trim()
|
||||
if (!value) return ''
|
||||
try {
|
||||
const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
|
||||
return new URL(value, origin).pathname
|
||||
} catch {
|
||||
return value.split(/[?#]/, 1)[0]
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldMarkAdminUIRequest(requestURL: string, pagePath?: string): boolean {
|
||||
const currentPath =
|
||||
pagePath ?? (typeof window !== 'undefined' ? window.location.pathname : '')
|
||||
return isAdminPath(requestPath(requestURL)) || isAdminPath(currentPath)
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import type { ApiResponse } from '@/types'
|
||||
import { getLocale } from '@/i18n'
|
||||
import { ADMIN_UI_REQUEST_HEADER, shouldMarkAdminUIRequest } from './adminUIRequest'
|
||||
import { getAPIBaseURL } from './url'
|
||||
export { buildApiUrl, buildGatewayUrl } from './url'
|
||||
|
||||
@@ -74,6 +75,10 @@ apiClient.interceptors.request.use(
|
||||
config.params.timezone = getUserTimezone()
|
||||
}
|
||||
|
||||
if (config.headers && shouldMarkAdminUIRequest(String(config.url || ''))) {
|
||||
config.headers[ADMIN_UI_REQUEST_HEADER] = '1'
|
||||
}
|
||||
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
|
||||
Reference in New Issue
Block a user