Merge remote-tracking branch 'origin/main' into feat/grok-sso-device-oauth

# Conflicts:
#	frontend/src/api/admin/grok.ts
This commit is contained in:
shaw
2026-07-14 10:19:16 +08:00
147 changed files with 8735 additions and 597 deletions
@@ -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)
})
})
+50
View File
@@ -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()
})
})
// --- 响应拦截器 ---
+10 -9
View File
@@ -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 {
+3
View File
@@ -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
+27
View File
@@ -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)
}
+5
View File
@@ -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) => {