refactor: TypeScript 类型体系重构 + 状态管理 + 环境配置
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 13s
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 13s
## 类型安全 - tsconfig: 开启 strict: true(豁免 noImplicitAny + strictNullChecks,待后续收紧) - 新建 15 个领域类型文件 (src/types/),共 ~80 个类型定义 - 统一 barrel export (src/types/index.ts) - 消除 9 个 [key: string]: any 接口 - 消除 22 个 API data: any 参数 → 替换为具体 DTO - 消除 36 个 Promise<any> → 使用具体泛型 - 消除重复 PaginatedResponse,统一使用 PaginatedResult - API 内联类型全部迁至 types/ ## 状态管理 - 引入 Zustand (zustand + persist) - 新建 src/stores/ui-store.ts — 侧边栏折叠状态持久化 - AdminLayout 接入 useUIStore ## 环境配置 - 新建 .env.example — 环境变量模板 - 新建 .env.production — 生产架构文档 - 新建 src/vite-env.d.ts — IDE 类型声明 ## 通用 UI 类型 - 新建 src/types/ui.ts — TypedColumn<T>, SortState, FilterState 等 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f291f39049
commit
91698a2f3b
13
.env.example
Normal file
13
.env.example
Normal file
@ -0,0 +1,13 @@
|
||||
# ============================================
|
||||
# 知习 Admin 前端 — 环境变量
|
||||
# ============================================
|
||||
# 复制此文件为 .env 用于本地开发
|
||||
# cp .env.example .env
|
||||
#
|
||||
# 生产环境:不需要配置,Nginx 自动代理 API 请求
|
||||
# ============================================
|
||||
|
||||
# API 后端地址(仅本地开发使用)
|
||||
# - 本地开发:http://127.0.0.1:3000
|
||||
# - 连接远程 API:https://api.longde.cloud
|
||||
VITE_API_BASE_URL=http://127.0.0.1:3000
|
||||
9
.env.production
Normal file
9
.env.production
Normal file
@ -0,0 +1,9 @@
|
||||
# ============================================
|
||||
# 知习 Admin 前端 — 生产环境
|
||||
# ============================================
|
||||
# VITE_API_BASE_URL 不需要设置
|
||||
# 生产环境通过 Nginx 反向代理 API 请求
|
||||
# Nginx 配置: /etc/nginx/sites-enabled/admin
|
||||
# 部署路径: /opt/zhixi/admin/dist/
|
||||
# API 代理: /admin-api/ → http://127.0.0.1:3000
|
||||
# ============================================
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@ -21,7 +21,8 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.15.1",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"remark-gfm": "^4.0.1"
|
||||
"remark-gfm": "^4.0.1",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
@ -7075,6 +7076,35 @@
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.14",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
|
||||
"integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz",
|
||||
|
||||
@ -23,7 +23,8 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router-dom": "^7.15.1",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"remark-gfm": "^4.0.1"
|
||||
"remark-gfm": "^4.0.1",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { AiRoute } from '@/types'
|
||||
|
||||
export type CreateRouteDto = Omit<AiRoute, 'id' | 'createdAt'>
|
||||
|
||||
export const aiGatewayAPI = {
|
||||
status() { return api.get('/admin-api/ai-gateway/status') },
|
||||
routes() { return api.get('/admin-api/ai-gateway/routes') },
|
||||
providers() { return api.get('/admin-api/ai-gateway/providers') },
|
||||
fallbackEvents() { return api.get('/admin-api/ai-gateway/fallback-events') },
|
||||
createRoute(data: any) { return api.post('/admin-api/ai-gateway/routes', data) },
|
||||
createRoute(data: CreateRouteDto) { return api.post('/admin-api/ai-gateway/routes', data) },
|
||||
deleteRoute(id: string) { return api.delete(`/admin-api/ai-gateway/routes/${id}`) },
|
||||
toggleProvider(name: string, enabled: boolean) { return api.put(`/admin-api/ai-gateway/providers/${name}`, { enabled }) },
|
||||
}
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { BillingInfo } from '@/types'
|
||||
|
||||
export interface BillingInfo {
|
||||
name: string; model: string; balance: string; currency: string
|
||||
status: 'ok' | 'unknown'; consoleUrl: string; note: string
|
||||
}
|
||||
export type { BillingInfo } from '@/types'
|
||||
|
||||
export const billingAPI = {
|
||||
get() { return api.get<{ providers: BillingInfo[] }>('/admin-api/billing') },
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
|
||||
export interface PrivacyDto { version: string; title: string; content: string; effectiveAt: string; published?: boolean }
|
||||
export interface AgreementDto { version: string; title: string; content: string; effectiveAt: string; published?: boolean }
|
||||
export interface FilingDto { type: string; title: string; description?: string; status?: string }
|
||||
|
||||
export const complianceAPI = {
|
||||
privacyPolicies: () => api.get('/admin-api/compliance/privacy-policies'),
|
||||
createPrivacy: (data: any) => api.post('/admin-api/compliance/privacy-policies', data),
|
||||
updatePrivacy: (id: string, data: any) => api.patch(`/admin-api/compliance/privacy-policies/${id}`, data),
|
||||
createPrivacy: (data: PrivacyDto) => api.post('/admin-api/compliance/privacy-policies', data),
|
||||
updatePrivacy: (id: string, data: Partial<PrivacyDto>) => api.patch(`/admin-api/compliance/privacy-policies/${id}`, data),
|
||||
userAgreements: () => api.get('/admin-api/compliance/user-agreements'),
|
||||
createAgreement: (data: any) => api.post('/admin-api/compliance/user-agreements', data),
|
||||
updateAgreement: (id: string, data: any) => api.patch(`/admin-api/compliance/user-agreements/${id}`, data),
|
||||
createAgreement: (data: AgreementDto) => api.post('/admin-api/compliance/user-agreements', data),
|
||||
updateAgreement: (id: string, data: Partial<AgreementDto>) => api.patch(`/admin-api/compliance/user-agreements/${id}`, data),
|
||||
filings: () => api.get('/admin-api/compliance/filings'),
|
||||
createFiling: (data: any) => api.post('/admin-api/compliance/filings', data),
|
||||
createFiling: (data: FilingDto) => api.post('/admin-api/compliance/filings', data),
|
||||
deletionRequests: () => api.get('/admin-api/compliance/deletion-requests'),
|
||||
approveDeletion: (id: string) => api.post(`/admin-api/compliance/deletion-requests/${id}/approve`),
|
||||
exportRequests: () => api.get('/admin-api/compliance/export-requests'),
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { AppConfig, FeatureFlag, ConfigChangeLog } from '@/types'
|
||||
|
||||
export const configAPI = {
|
||||
get() { return api.get<{ configs: any[]; flags: any[] }>('/admin-api/config') },
|
||||
get() { return api.get<{ configs: AppConfig[]; flags: FeatureFlag[] }>('/admin-api/config') },
|
||||
set(key: string, value: string) { return api.post('/admin-api/config', { key, value }) },
|
||||
update(key: string, value: string) { return api.patch(`/admin-api/config/${key}`, { value }) },
|
||||
remove(key: string) { return api.delete(`/admin-api/config/${key}`) },
|
||||
toggleFlag(name: string, enabled: boolean) { return api.post(`/admin-api/config/flags/${name}`, { enabled }) },
|
||||
setFlagWhitelist(name: string, whitelist: string) { return api.post(`/admin-api/config/flags/${name}`, { whitelist }) },
|
||||
changeLog() { return api.get<any[]>('/admin-api/config/changelog') },
|
||||
changeLog() { return api.get<ConfigChangeLog[]>('/admin-api/config/changelog') },
|
||||
}
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
|
||||
export interface AddWordDto { word: string; category?: string; riskLevel?: string }
|
||||
export interface HandleReportDto { action?: string; note?: string }
|
||||
export interface PenaltyDto { penalty: string }
|
||||
export interface SafetyQueryParams { page?: number; limit?: number; status?: string; type?: string }
|
||||
|
||||
export const contentSafetyAPI = {
|
||||
words() { return api.get('/admin-api/content-safety/words') },
|
||||
addWord(data: any) { return api.post('/admin-api/content-safety/words', data) },
|
||||
addWord(data: AddWordDto) { return api.post('/admin-api/content-safety/words', data) },
|
||||
deleteWord(id: string) { return api.delete(`/admin-api/content-safety/words/${id}`) },
|
||||
checks(params?: any) { return api.get('/admin-api/content-safety/checks', params) },
|
||||
reports(params?: any) { return api.get('/admin-api/content-safety/reports', params) },
|
||||
violations(params?: any) { return api.get('/admin-api/content-safety/violations', params) },
|
||||
handleReport(id: string, data: any) { return api.post(`/admin-api/content-safety/reports/${id}/handle`, data) },
|
||||
penaltyViolation(id: string, data: any) { return api.post(`/admin-api/content-safety/violations/${id}/penalty`, data) },
|
||||
checks(params?: SafetyQueryParams) { return api.get('/admin-api/content-safety/checks', params) },
|
||||
reports(params?: SafetyQueryParams) { return api.get('/admin-api/content-safety/reports', params) },
|
||||
violations(params?: SafetyQueryParams) { return api.get('/admin-api/content-safety/violations', params) },
|
||||
handleReport(id: string, data: HandleReportDto) { return api.post(`/admin-api/content-safety/reports/${id}/handle`, data) },
|
||||
penaltyViolation(id: string, data: PenaltyDto) { return api.post(`/admin-api/content-safety/violations/${id}/penalty`, data) },
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { Conversation, MessageRecord } from '@/types'
|
||||
|
||||
export interface Conversation { id: string; title: string; [key: string]: any }
|
||||
export interface MessageRecord { id: string; content: string; role: string; [key: string]: any }
|
||||
export type { Conversation, MessageRecord } from '@/types'
|
||||
|
||||
export const conversationsAPI = {
|
||||
list() { return api.get<Conversation[]>('/admin-api/conversations') },
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { CostItem, CostSummary } from '@/types'
|
||||
|
||||
export interface CostItem { id: string; name: string; currency: string; amount: number; purchaseDate?: string; expiryDate?: string; [key: string]: any }
|
||||
export interface CostSummary { totalMonthly: number; totalYearly: number; totalOneTime: number; items: CostItem[]; byMonth: any[]; expiringSoon: any[] }
|
||||
export type { CostItem, CostSummary, CostByMonth } from '@/types'
|
||||
|
||||
export const costsAPI = {
|
||||
summary() { return api.get<CostSummary>('/admin-api/costs/summary') },
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { QueueInfo, FailedJob, JobDetail } from '@/types'
|
||||
|
||||
export interface QueueInfo { name: string; waiting: number; active: number; failed: number; [key: string]: any }
|
||||
export interface FailedJob { id: string; name: string; failedReason: string; [key: string]: any }
|
||||
export interface JobDetail { id: string; data: any; [key: string]: any }
|
||||
export type { QueueInfo, FailedJob, JobDetail, QueueOverview } from '@/types'
|
||||
|
||||
export const eventsQueueAPI = {
|
||||
overview() { return api.get<{ queues: QueueInfo[] }>('/admin-api/events') },
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { ImportJob, ImportJobDetail } from '@/types'
|
||||
|
||||
export const importsAdminAPI = {
|
||||
list: (params: string) => api.get(`/admin-api/imports?${params}`),
|
||||
detail: (id: string) => api.get(`/admin-api/imports/${id}`),
|
||||
list: (params: string) => api.get<{ items: ImportJob[] }>(`/admin-api/imports?${params}`),
|
||||
detail: (id: string) => api.get<ImportJobDetail>(`/admin-api/imports/${id}`),
|
||||
retry: (id: string) => api.post(`/admin-api/imports/${id}/retry`),
|
||||
batchRetry: (ids: string[]) => api.post('/admin-api/imports/batch-retry', { ids }),
|
||||
}
|
||||
|
||||
@ -2,16 +2,22 @@ export { authAPI } from './auth'
|
||||
export { usersAPI } from './users'
|
||||
export { dashboardAPI } from './dashboard'
|
||||
export { auditAPI } from './audit'
|
||||
export { billingAPI, type BillingInfo } from './billing'
|
||||
export { costsAPI, type CostItem, type CostSummary } from './costs'
|
||||
export { billingAPI } from './billing'
|
||||
export { costsAPI } from './costs'
|
||||
export { configAPI } from './config'
|
||||
export { conversationsAPI, type Conversation } from './conversations'
|
||||
export { conversationsAPI } from './conversations'
|
||||
export { eventsQueueAPI } from './events-queue'
|
||||
export { knowledgeAPI } from './knowledge'
|
||||
export { serversAPI, type ServerInfo, type ProcessInfo, type ServerHealth, type HealthService } from './servers'
|
||||
export { serversAPI } from './servers'
|
||||
export { learningAPI } from './learning'
|
||||
export { aiAPI } from './ai'
|
||||
|
||||
// 类型统一从 @/types 导出(源)
|
||||
export type { BillingInfo } from '@/types'
|
||||
export type { CostItem, CostSummary } from '@/types'
|
||||
export type { Conversation, MessageRecord } from '@/types'
|
||||
export type { ServerInfo, ProcessInfo, ServerHealth, HealthService } from '@/types'
|
||||
|
||||
// ── 新增 ──
|
||||
export { aiGatewayAPI } from './ai-gateway'
|
||||
export { cacheAPI } from './cache'
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { KnowledgeBase, KnowledgeSource, PaginatedResult, SourceReference, KnowledgeCandidate, KnowledgeChunk } from '@/types'
|
||||
|
||||
export const knowledgeAPI = {
|
||||
list(page = 1, limit = 20) {
|
||||
return api.get<{ items: any[]; total: number; page: number; limit: number; totalPages: number }>(`/admin-api/knowledge-bases?page=${page}&limit=${limit}`)
|
||||
return api.get<PaginatedResult<KnowledgeBase>>(`/admin-api/knowledge-bases?page=${page}&limit=${limit}`)
|
||||
},
|
||||
getById(id: string) { return api.get<any>(`/admin-api/knowledge-bases/${id}`) },
|
||||
getById(id: string) { return api.get<KnowledgeBase>(`/admin-api/knowledge-bases/${id}`) },
|
||||
remove(id: string) { return api.delete<void>(`/admin-api/knowledge-bases/${id}`) },
|
||||
candidates() { return api.get('/admin-api/knowledge-bases/candidates?limit=50') },
|
||||
chunks(sourceId: string) { return api.get(`/admin-api/knowledge-bases/chunks?sourceId=${sourceId}&limit=50`) },
|
||||
sources(kbId: string) { return api.get(`/admin-api/knowledge-bases/${kbId}/sources`) },
|
||||
sourceReferences(sourceId: string) { return api.get(`/admin-api/knowledge-bases/sources/${sourceId}/references`) },
|
||||
allSources(params: string) { return api.get(`/admin-api/knowledge-bases/all-sources?${params}`) },
|
||||
search(params: string) { return api.get(`/admin-api/knowledge-bases?${params}`) },
|
||||
candidates() { return api.get<KnowledgeCandidate[]>('/admin-api/knowledge-bases/candidates?limit=50') },
|
||||
chunks(sourceId: string) { return api.get<KnowledgeChunk[]>(`/admin-api/knowledge-bases/chunks?sourceId=${sourceId}&limit=50`) },
|
||||
sources(kbId: string) { return api.get<KnowledgeSource[]>(`/admin-api/knowledge-bases/${kbId}/sources`) },
|
||||
sourceReferences(sourceId: string) { return api.get<SourceReference[]>(`/admin-api/knowledge-bases/sources/${sourceId}/references`) },
|
||||
allSources(params: string) { return api.get<PaginatedResult<KnowledgeSource>>(`/admin-api/knowledge-bases/all-sources?${params}`) },
|
||||
search(params: string) { return api.get<PaginatedResult<KnowledgeBase>>(`/admin-api/knowledge-bases?${params}`) },
|
||||
}
|
||||
|
||||
@ -1,48 +1,64 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type {
|
||||
PaginatedResult,
|
||||
ReadingEvent,
|
||||
LearningSession,
|
||||
MaterialReadingProgress,
|
||||
DailyLearningActivity,
|
||||
LearningRecord,
|
||||
AdminDashboard,
|
||||
AnomalyData,
|
||||
UserTimeline,
|
||||
UserDiagnose,
|
||||
MaterialDiagnose,
|
||||
} from '@/types'
|
||||
|
||||
const BASE = '/admin-api/learning'
|
||||
|
||||
function qs(params?: Record<string, any>): string {
|
||||
type QueryParams = Record<string, string | number | undefined>
|
||||
|
||||
function qs(params?: QueryParams): string {
|
||||
if (!params) return ''
|
||||
const p = Object.entries(params).filter(([, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
|
||||
const p = Object.entries(params)
|
||||
.filter(([, v]) => v !== undefined && v !== '')
|
||||
.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`)
|
||||
.join('&')
|
||||
return p ? `?${p}` : ''
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> { items: T[]; total: number }
|
||||
|
||||
export const learningAPI = {
|
||||
// dashboard
|
||||
getDashboard: (params?: Record<string, any>) => api.get<any>(`${BASE}/dashboard${qs(params)}`),
|
||||
getDashboard: (params?: QueryParams) => api.get<AdminDashboard>(`${BASE}/dashboard${qs(params)}`),
|
||||
getKnowledgeBases: () => api.get<Array<{ id: string; name: string }>>(`${BASE}/knowledge-bases`),
|
||||
|
||||
// reading events
|
||||
getReadingEvents: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/reading-events${qs(params)}`),
|
||||
getReadingEventDetail: (id: string) => api.get<any>(`${BASE}/reading-events/${id}`),
|
||||
getFailedEvents: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/reading-events/failed${qs(params)}`),
|
||||
getReadingEvents: (params?: QueryParams) => api.get<PaginatedResult<ReadingEvent>>(`${BASE}/reading-events${qs(params)}`),
|
||||
getReadingEventDetail: (id: string) => api.get<ReadingEvent>(`${BASE}/reading-events/${id}`),
|
||||
getFailedEvents: (params?: QueryParams) => api.get<PaginatedResult<ReadingEvent>>(`${BASE}/reading-events/failed${qs(params)}`),
|
||||
|
||||
// sessions
|
||||
getSessions: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/sessions${qs(params)}`),
|
||||
getSessionDetail: (id: string) => api.get<any>(`${BASE}/sessions/${id}`),
|
||||
getSessions: (params?: QueryParams) => api.get<PaginatedResult<LearningSession>>(`${BASE}/sessions${qs(params)}`),
|
||||
getSessionDetail: (id: string) => api.get<LearningSession>(`${BASE}/sessions/${id}`),
|
||||
deleteSessions: (ids: string[]) => api.post<{ deleted: number; message: string }>(`${BASE}/sessions/batch-delete`, { ids }),
|
||||
|
||||
// progress & activities
|
||||
getProgress: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/progress${qs(params)}`),
|
||||
getDailyActivities: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/daily-activities${qs(params)}`),
|
||||
getRecords: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/records${qs(params)}`),
|
||||
getProgress: (params?: QueryParams) => api.get<PaginatedResult<MaterialReadingProgress>>(`${BASE}/progress${qs(params)}`),
|
||||
getDailyActivities: (params?: QueryParams) => api.get<PaginatedResult<DailyLearningActivity>>(`${BASE}/daily-activities${qs(params)}`),
|
||||
getRecords: (params?: QueryParams) => api.get<PaginatedResult<LearningRecord>>(`${BASE}/records${qs(params)}`),
|
||||
|
||||
// user/material
|
||||
getUserTimeline: (userId: string) => api.get(`${BASE}/user-timeline?userId=${encodeURIComponent(userId)}`),
|
||||
getUserDiagnose: (userId: string) => api.get(`${BASE}/user-diagnose?userId=${encodeURIComponent(userId)}`),
|
||||
getMaterialDiagnose: (materialId: string) => api.get(`${BASE}/material-diagnose?materialId=${encodeURIComponent(materialId)}`),
|
||||
getUserTimeline: (userId: string) => api.get<UserTimeline>(`${BASE}/user-timeline?userId=${encodeURIComponent(userId)}`),
|
||||
getUserDiagnose: (userId: string) => api.get<UserDiagnose>(`${BASE}/user-diagnose?userId=${encodeURIComponent(userId)}`),
|
||||
getMaterialDiagnose: (materialId: string) => api.get<MaterialDiagnose>(`${BASE}/material-diagnose?materialId=${encodeURIComponent(materialId)}`),
|
||||
|
||||
// misc
|
||||
getAnomalies: () => api.get<any>(`${BASE}/anomalies`),
|
||||
getTemporaryMaterials: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/temporary-materials${qs(params)}`),
|
||||
getAnomalies: () => api.get<AnomalyData>(`${BASE}/anomalies`),
|
||||
getTemporaryMaterials: (params?: QueryParams) => api.get<PaginatedResult<unknown>>(`${BASE}/temporary-materials${qs(params)}`),
|
||||
|
||||
// actions
|
||||
recalculate: () => api.post(`${BASE}/recalculate`),
|
||||
replayEvent: (eventId: string) => api.post<{ success: boolean; message: string }>(`${BASE}/reading-events/${eventId}/replay`),
|
||||
replayEventsBatch: (params: Record<string, any>) => api.post<{ replayed: number; failed: number }>(`${BASE}/reading-events/replay-batch`, params),
|
||||
replayEventsBatch: (params: QueryParams) => api.post<{ replayed: number; failed: number }>(`${BASE}/reading-events/replay-batch`, params),
|
||||
exportData: (type: string) => api.get(`${BASE}/export?type=${encodeURIComponent(type)}`),
|
||||
getSessionsRaw: (qs: string) => api.get(`${BASE}/sessions?${qs}`),
|
||||
getAnalysis: (qs: string) => api.get(`${BASE}/analysis?${qs}`),
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
|
||||
export interface UpdateMembershipDto { userId: string; planId: string; status?: string; expiresAt?: string }
|
||||
|
||||
export const membersAPI = {
|
||||
list: () => api.get('/admin-api/users'),
|
||||
deletionRequests: () => api.get('/admin-api/users/deletion-requests'),
|
||||
processDeletion: (id: string, action: string) => api.post(`/admin-api/users/deletion-requests/${id}/${action}`),
|
||||
memberships: () => api.get('/admin-api/users/memberships'),
|
||||
updateMembership: (data: any) => api.post('/admin-api/users/memberships', data),
|
||||
updateMembership: (data: UpdateMembershipDto) => api.post('/admin-api/users/memberships', data),
|
||||
}
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { NotificationTemplate } from '@/types'
|
||||
|
||||
export type CreateTemplateDto = Omit<NotificationTemplate, 'id' | 'createdAt' | 'updatedAt'>
|
||||
export type UpdateTemplateDto = Partial<CreateTemplateDto>
|
||||
export interface SendNotificationDto { type: string; title: string; content?: string }
|
||||
|
||||
export const notificationsAPI = {
|
||||
templates: () => api.get('/admin-api/notifications/templates'),
|
||||
createTemplate: (data: any) => api.post('/admin-api/notifications/templates', data),
|
||||
updateTemplate: (id: string, data: any) => api.patch(`/admin-api/notifications/templates/${id}`, data),
|
||||
createTemplate: (data: CreateTemplateDto) => api.post('/admin-api/notifications/templates', data),
|
||||
updateTemplate: (id: string, data: UpdateTemplateDto) => api.patch(`/admin-api/notifications/templates/${id}`, data),
|
||||
deleteTemplate: (id: string) => api.delete(`/admin-api/notifications/templates/${id}`),
|
||||
sendLog: () => api.get('/admin-api/notifications/send-log?limit=50'),
|
||||
adminNotifications: () => api.get('/admin-api/admin-notifications'),
|
||||
send: (data: any) => api.post('/admin-api/admin-notifications/send', data),
|
||||
send: (data: SendNotificationDto) => api.post('/admin-api/admin-notifications/send', data),
|
||||
markRead: (id: string) => api.post(`/admin-api/admin-notifications/${id}/read`),
|
||||
}
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { MembershipPlan } from '@/types'
|
||||
|
||||
export type CreatePlanDto = Omit<MembershipPlan, 'id' | 'createdAt'>
|
||||
|
||||
export const quotaAPI = {
|
||||
plans: () => api.get('/admin-api/quota/plans'),
|
||||
createPlan: (data: any) => api.post('/admin-api/quota/plans', data),
|
||||
createPlan: (data: CreatePlanDto) => api.post('/admin-api/quota/plans', data),
|
||||
memberships: () => api.get('/admin-api/quota/memberships'),
|
||||
costs: () => api.get('/admin-api/quota/costs'),
|
||||
}
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { Changelog, ReleaseDecision, ReleaseChecklistItem } from '@/types'
|
||||
|
||||
export type CreateChangelogDto = Omit<Changelog, 'id' | 'createdAt'>
|
||||
export type UpdateChangelogDto = Partial<CreateChangelogDto>
|
||||
export type CreateDecisionDto = Omit<ReleaseDecision, 'id' | 'createdAt'>
|
||||
export type UpdateDecisionDto = Partial<CreateDecisionDto>
|
||||
|
||||
export const releaseAPI = {
|
||||
changelogs: () => api.get('/admin-api/release/changelogs'),
|
||||
decisions: () => api.get('/admin-api/release/decisions'),
|
||||
checklists: (ver: string) => api.get(`/admin-api/release/checklists/${ver}`),
|
||||
create: (type: string, data: any) => api.post(`/admin-api/release/${type}`, data),
|
||||
update: (type: string, id: string, data: any) => api.patch(`/admin-api/release/${type}/${id}`, data),
|
||||
checklists: (ver: string) => api.get<ReleaseChecklistItem[]>(`/admin-api/release/checklists/${ver}`),
|
||||
create: (type: string, data: CreateChangelogDto | CreateDecisionDto) => api.post(`/admin-api/release/${type}`, data),
|
||||
update: (type: string, id: string, data: UpdateChangelogDto | UpdateDecisionDto) => api.patch(`/admin-api/release/${type}/${id}`, data),
|
||||
delete: (type: string, id: string) => api.delete(`/admin-api/release/${type}/${id}`),
|
||||
toggleChecklist: (id: string, checked: boolean) => api.patch(`/admin-api/release/checklists/${id}`, { checked }),
|
||||
}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
|
||||
export interface CreateSecretDto { name: string; provider: string; value: string; expiresAt?: string }
|
||||
export interface CreateVendorBillDto { provider: string; billMonth: string; amount: number; notes?: string }
|
||||
|
||||
export const secretsAPI = {
|
||||
list() { return api.get('/admin-api/secrets') },
|
||||
logs() { return api.get('/admin-api/secrets/logs') },
|
||||
create(data: any) { return api.post('/admin-api/secrets', data) },
|
||||
create(data: CreateSecretDto) { return api.post('/admin-api/secrets', data) },
|
||||
delete(id: string) { return api.delete(`/admin-api/secrets/${id}`) },
|
||||
vendorBills() { return api.get('/admin-api/vendor/bills') },
|
||||
createVendorBill(data: any) { return api.post('/admin-api/vendor/bills', data) },
|
||||
createVendorBill(data: CreateVendorBillDto) { return api.post('/admin-api/vendor/bills', data) },
|
||||
deleteVendorBill(id: string) { return api.delete(`/admin-api/vendor/bills/${id}`) },
|
||||
rotateVendorSecret(id: string) { return api.post(`/admin-api/vendor/secrets/${id}/rotate`) },
|
||||
revokeVendorSecret(id: string) { return api.post(`/admin-api/vendor/secrets/${id}/revoke`) },
|
||||
|
||||
@ -1,18 +1,7 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { ServerInfo, ServerHealth, WorkerInfo } from '@/types'
|
||||
|
||||
export interface ServerInfo {
|
||||
hostname: string
|
||||
cpu: { usagePercent: number; cores: number; model?: string }
|
||||
memory: { percent: number; used: string; total: string }
|
||||
network: { domains: string[]; privateIp?: string; publicIp?: string }
|
||||
disks: { mount: string; size: string; used: string; percent: number }[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface ProcessInfo { pid: number; name: string; cpu: number; memory: number; desc?: string; command?: string; [key: string]: any }
|
||||
export interface HealthService { serviceName: string; status: string; message?: string }
|
||||
export interface ServerHealth { name: string; status: string; uptime?: string; pid?: number; services?: HealthService[]; queues?: string[]; [key: string]: any }
|
||||
export interface WorkerInfo { name: string; status: string; queues?: string[]; [key: string]: any }
|
||||
export type { ServerInfo, ProcessInfo, HealthService, ServerHealth, WorkerInfo, ServerMetrics } from '@/types'
|
||||
|
||||
export const serversAPI = {
|
||||
metrics() { return api.get<{ servers: ServerInfo[] }>('/admin-api/servers') },
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { VectorDebugQuery } from '@/types'
|
||||
|
||||
export const vectorAPI = {
|
||||
count: () => api.get('/admin-api/vector/count'),
|
||||
collection: () => api.get('/admin-api/vector/collection'),
|
||||
reindex: () => api.post('/admin-api/vector/reindex'),
|
||||
debugSearch: (data: any) => api.post('/admin-api/vector/debug-search', data),
|
||||
debugSearch: (data: VectorDebugQuery) => api.post('/admin-api/vector/debug-search', data),
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import { ProLayout } from '@ant-design/pro-components'
|
||||
import { Dropdown, Avatar, Tag, Space, message } from 'antd'
|
||||
import { LogoutOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import { useAuth } from '@/contexts/AuthContext'
|
||||
import { useUIStore } from '@/stores'
|
||||
import { filterMenuByRole, adminMenuItems } from '@/config/menu'
|
||||
import { ADMIN_ROLE_LABELS, ADMIN_ROLE_COLORS } from '@/constants/roles'
|
||||
import type { AdminRole } from '@/types/admin'
|
||||
@ -60,6 +61,9 @@ export default function AdminLayout() {
|
||||
const location = useLocation()
|
||||
const { adminUser, logout, hasPermission } = useAuth()
|
||||
|
||||
const sidebarCollapsed = useUIStore((s) => s.sidebarCollapsed)
|
||||
const setSidebarCollapsed = useUIStore((s) => s.setSidebarCollapsed)
|
||||
|
||||
const currentRole = adminUser?.role as AdminRole | undefined
|
||||
|
||||
const handleLogout = async () => {
|
||||
@ -135,6 +139,8 @@ export default function AdminLayout() {
|
||||
</Space>
|
||||
) : null
|
||||
}
|
||||
collapsed={sidebarCollapsed}
|
||||
onCollapse={setSidebarCollapsed}
|
||||
siderWidth={220}
|
||||
token={{
|
||||
header: { heightLayoutHeader: 48 },
|
||||
|
||||
@ -15,25 +15,25 @@ export default function AiGatewayPage() {
|
||||
|
||||
const { data: status } = useQuery({
|
||||
queryKey: ['ai-gateway', 'status'],
|
||||
queryFn: (): Promise<any> => aiGatewayAPI.status(),
|
||||
queryFn: () =>aiGatewayAPI.status(),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const { data: routes, refetch: refetchRoutes } = useQuery({
|
||||
queryKey: ['ai-gateway', 'routes'],
|
||||
queryFn: (): Promise<any> => aiGatewayAPI.routes(),
|
||||
queryFn: () =>aiGatewayAPI.routes(),
|
||||
enabled: activeTab === 'routes',
|
||||
})
|
||||
|
||||
const { data: providers, refetch: refetchProviders } = useQuery({
|
||||
queryKey: ['ai-gateway', 'providers'],
|
||||
queryFn: (): Promise<any> => aiGatewayAPI.providers(),
|
||||
queryFn: () =>aiGatewayAPI.providers(),
|
||||
enabled: activeTab === 'providers',
|
||||
})
|
||||
|
||||
const { data: fallbackEvents } = useQuery({
|
||||
queryKey: ['ai-gateway', 'fallback-events'],
|
||||
queryFn: (): Promise<any> => aiGatewayAPI.fallbackEvents(),
|
||||
queryFn: () =>aiGatewayAPI.fallbackEvents(),
|
||||
enabled: activeTab === 'fallback',
|
||||
refetchInterval: 30_000,
|
||||
})
|
||||
|
||||
@ -25,8 +25,8 @@ function BillingContent() {
|
||||
|
||||
const { data: billing } = useQuery({ queryKey: ['billing'], queryFn: billingAPI.get, staleTime: 60_000 })
|
||||
const { data: costs } = useQuery({ queryKey: ['costs', 'summary'], queryFn: costsAPI.summary, staleTime: 30_000 })
|
||||
const { data: aiReport } = useQuery({ queryKey: ['costs', 'report'], queryFn: (): Promise<any> => costsAPI.report(), staleTime: 30_000 })
|
||||
const { data: topUsers } = useQuery({ queryKey: ['costs', 'top-users'], queryFn: (): Promise<any> => costsAPI.topUsers(), staleTime: 30_000 })
|
||||
const { data: aiReport } = useQuery({ queryKey: ['costs', 'report'], queryFn: () => costsAPI.report(), staleTime: 30_000 })
|
||||
const { data: topUsers } = useQuery({ queryKey: ['costs', 'top-users'], queryFn: () => costsAPI.topUsers(), staleTime: 30_000 })
|
||||
|
||||
const refresh = async () => { setRefreshing(true); await qc.invalidateQueries({ queryKey: ['billing'] }); await qc.invalidateQueries({ queryKey: ['costs'] }); setTimeout(() => setRefreshing(false), 800) }
|
||||
|
||||
|
||||
@ -14,13 +14,13 @@ export default function ChatLogsPage() {
|
||||
|
||||
const { data: sessions, isLoading } = useQuery({
|
||||
queryKey: ['chat-logs', 'sessions'],
|
||||
queryFn: (): Promise<any> => chatLogsAPI.sessions(),
|
||||
queryFn: () => chatLogsAPI.sessions(),
|
||||
staleTime: 10_000,
|
||||
})
|
||||
|
||||
const { data: messages } = useQuery({
|
||||
queryKey: ['chat-logs', 'messages', selectedSession?.id],
|
||||
queryFn: (): Promise<any> => chatLogsAPI.sessionMessages(selectedSession?.id || ''),
|
||||
queryFn: () => chatLogsAPI.sessionMessages(selectedSession?.id || ''),
|
||||
enabled: !!selectedSession?.id,
|
||||
})
|
||||
|
||||
|
||||
@ -15,10 +15,10 @@ function CSPage() {
|
||||
const [category, setCategory] = useState('general')
|
||||
const [risk, setRisk] = useState('medium')
|
||||
|
||||
const { data: words } = useQuery({ queryKey: ['safety', 'words'], queryFn: (): Promise<any> => contentSafetyAPI.words() })
|
||||
const { data: checks } = useQuery({ queryKey: ['safety', 'checks'], queryFn: (): Promise<any> => contentSafetyAPI.checks() })
|
||||
const { data: reports } = useQuery({ queryKey: ['safety', 'reports'], queryFn: (): Promise<any> => contentSafetyAPI.reports() })
|
||||
const { data: violations } = useQuery({ queryKey: ['safety', 'violations'], queryFn: (): Promise<any> => contentSafetyAPI.violations() })
|
||||
const { data: words } = useQuery({ queryKey: ['safety', 'words'], queryFn: () => contentSafetyAPI.words() })
|
||||
const { data: checks } = useQuery({ queryKey: ['safety', 'checks'], queryFn: () => contentSafetyAPI.checks() })
|
||||
const { data: reports } = useQuery({ queryKey: ['safety', 'reports'], queryFn: () => contentSafetyAPI.reports() })
|
||||
const { data: violations } = useQuery({ queryKey: ['safety', 'violations'], queryFn: () => contentSafetyAPI.violations() })
|
||||
|
||||
const addWord = async () => {
|
||||
await contentSafetyAPI.addWord({ word: newWord, category, riskLevel: risk })
|
||||
|
||||
@ -14,26 +14,26 @@ function EventsPage() {
|
||||
|
||||
const { data: overview } = useQuery({
|
||||
queryKey: ['events', 'overview'],
|
||||
queryFn: (): Promise<any> => eventsQueueAPI.overview(),
|
||||
queryFn: () =>eventsQueueAPI.overview(),
|
||||
staleTime: 10_000,
|
||||
})
|
||||
|
||||
const { data: stats } = useQuery({
|
||||
queryKey: ['events', 'stats'],
|
||||
queryFn: (): Promise<any> => eventsQueueAPI.stats(),
|
||||
queryFn: () =>eventsQueueAPI.stats(),
|
||||
enabled: activeTab === 'stats',
|
||||
})
|
||||
|
||||
const { data: workers } = useQuery({
|
||||
queryKey: ['events', 'workers'],
|
||||
queryFn: (): Promise<any> => eventsQueueAPI.workers(),
|
||||
queryFn: () =>eventsQueueAPI.workers(),
|
||||
enabled: activeTab === 'stats',
|
||||
refetchInterval: 15_000,
|
||||
})
|
||||
|
||||
const { data: failed } = useQuery({
|
||||
queryKey: ['events', 'failed', selectedQueue],
|
||||
queryFn: (): Promise<any> => selectedQueue ? eventsQueueAPI.failedJobs(selectedQueue) : Promise.resolve(null),
|
||||
queryFn: () =>selectedQueue ? eventsQueueAPI.failedJobs(selectedQueue) : Promise.resolve(null),
|
||||
enabled: !!selectedQueue,
|
||||
})
|
||||
|
||||
|
||||
@ -4,12 +4,14 @@ import { Table, Tag, Typography, Button, App, Drawer, Space, Select, Popconfirm,
|
||||
import { ReloadOutlined, RetweetOutlined, ImportOutlined, EyeOutlined } from '@ant-design/icons'
|
||||
import { importsAdminAPI, knowledgeAPI } from '@/api'
|
||||
import { importStatusLabels, importStepLabels } from '@/constants/labels'
|
||||
import type { TablePaginationConfig } from 'antd'
|
||||
import type { FilterValue, SorterResult } from 'antd/es/table/interface'
|
||||
import type { KnowledgeBase, ImportJob, ImportJobDetail, ImportStepLog } from '@/types'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
// 格式化知识库选项:用户 - 名称 (id前8位)
|
||||
function formatKbLabel(kb: any) {
|
||||
function formatKbLabel(kb: KnowledgeBase) {
|
||||
const user = kb.user?.email || kb.user?.nickname || kb.userId || '?'
|
||||
const name = kb.title || '未命名'
|
||||
const shortId = kb.id?.slice(0, 8) || kb.id
|
||||
@ -20,7 +22,7 @@ export default function ImportMonitorPage() {
|
||||
const { message } = App.useApp()
|
||||
const qc = useQueryClient()
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
const [selectedJob, setSelectedJob] = useState<any>(null)
|
||||
const [selectedJob, setSelectedJob] = useState<ImportJobDetail | null>(null)
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
|
||||
const [kbFilter, setKbFilter] = useState<string | undefined>(undefined)
|
||||
const [retryAllOpen, setRetryAllOpen] = useState(false)
|
||||
@ -28,7 +30,6 @@ export default function ImportMonitorPage() {
|
||||
const [sortBy, setSortBy] = useState<string>('createdAt')
|
||||
const [sortOrder, setSortOrder] = useState<string>('descend')
|
||||
|
||||
// 知识库远程搜索(防抖)
|
||||
const [kbSearch, setKbSearch] = useState('')
|
||||
const kbSearchTimer = useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||
|
||||
@ -37,10 +38,9 @@ export default function ImportMonitorPage() {
|
||||
kbSearchTimer.current = setTimeout(() => setKbSearch(val), 300)
|
||||
}, [])
|
||||
|
||||
// 知识库列表
|
||||
const { data: kbData, isLoading: kbLoading } = useQuery({
|
||||
queryKey: ['admin-knowledge-bases', kbSearch],
|
||||
queryFn: (): Promise<any> => {
|
||||
queryFn: () => {
|
||||
const params = new URLSearchParams({ limit: '50' })
|
||||
if (kbSearch) params.set('search', kbSearch)
|
||||
return knowledgeAPI.search(params.toString())
|
||||
@ -56,17 +56,16 @@ export default function ImportMonitorPage() {
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['admin-imports', 'list', kbFilter, sortBy, sortOrder],
|
||||
queryFn: (): Promise<any> => importsAdminAPI.list(queryParams.toString()),
|
||||
queryFn: () => importsAdminAPI.list(queryParams.toString()),
|
||||
})
|
||||
|
||||
// 默认过滤已删除 source 的记录
|
||||
const tableData = showDeleted
|
||||
? (data?.items || [])
|
||||
: (data?.items || []).filter((r: any) => !r.sourceDeleted)
|
||||
: (data?.items || []).filter((r: ImportJob) => !r.sourceDeleted)
|
||||
|
||||
const { data: detail } = useQuery({
|
||||
queryKey: ['admin-import-detail', selectedJob?.id],
|
||||
queryFn: (): Promise<any> => importsAdminAPI.detail(selectedJob?.id || ''),
|
||||
queryFn: () => importsAdminAPI.detail(selectedJob?.id || ''),
|
||||
enabled: !!selectedJob?.id,
|
||||
})
|
||||
|
||||
@ -83,17 +82,17 @@ export default function ImportMonitorPage() {
|
||||
await importsAdminAPI.retry(id)
|
||||
message.success('已重新入队,等待 Worker 处理')
|
||||
refreshList()
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.message || err?.message || '重试失败'
|
||||
message.error(`重试失败: ${msg}`)
|
||||
} catch (err: unknown) {
|
||||
const e = err as { response?: { data?: { message?: string } }; message?: string }
|
||||
message.error(`重试失败: ${e?.response?.data?.message || e?.message || '未知错误'}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchRetry = async () => {
|
||||
try {
|
||||
const ids = selectedRowKeys as string[]
|
||||
const res = await importsAdminAPI.batchRetry(ids as string[])
|
||||
const { updated, skipped, skippedCount } = res
|
||||
const res = await importsAdminAPI.batchRetry(ids)
|
||||
const { updated, skipped, skippedCount } = res as { updated: number; skipped: string[]; skippedCount: number }
|
||||
let msg = `已重新入队 ${updated} 个任务`
|
||||
if (skippedCount > 0) {
|
||||
msg += `,${skippedCount} 个跳过(源已删除: ${skipped.slice(0, 3).join(', ')}${skipped.length > 3 ? '...' : ''})`
|
||||
@ -101,9 +100,9 @@ export default function ImportMonitorPage() {
|
||||
message.success(msg)
|
||||
setSelectedRowKeys([])
|
||||
refreshList()
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.message || err?.message || '批量重试失败'
|
||||
message.error(msg)
|
||||
} catch (err: unknown) {
|
||||
const e = err as { response?: { data?: { message?: string } }; message?: string }
|
||||
message.error(e?.response?.data?.message || e?.message || '批量重试失败')
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,14 +110,14 @@ export default function ImportMonitorPage() {
|
||||
try {
|
||||
const allItems = data?.items || []
|
||||
const retryableIds = allItems
|
||||
.filter((r: any) => r.status?.startsWith('FAILED') || r.status === 'CLAIMED' || r.status === 'QUEUED')
|
||||
.map((r: any) => r.id)
|
||||
.filter((r: ImportJob) => r.status?.startsWith('FAILED') || r.status === 'CLAIMED' || r.status === 'QUEUED')
|
||||
.map((r: ImportJob) => r.id)
|
||||
if (retryableIds.length === 0) {
|
||||
message.warning('没有可重新解析的任务')
|
||||
return
|
||||
}
|
||||
const res = await importsAdminAPI.batchRetry(retryableIds)
|
||||
const { updated, skipped, skippedCount } = res
|
||||
const { updated, skipped, skippedCount } = res as { updated: number; skipped: string[]; skippedCount: number }
|
||||
let msg = `已重新入队 ${updated} 个任务`
|
||||
if (skippedCount > 0) {
|
||||
msg += `,${skippedCount} 个跳过(源已删除: ${skipped.slice(0, 3).join(', ')}${skipped.length > 3 ? '...' : ''})`
|
||||
@ -127,58 +126,40 @@ export default function ImportMonitorPage() {
|
||||
setSelectedRowKeys([])
|
||||
setRetryAllOpen(false)
|
||||
refreshList()
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.message || err?.message || '全部重试失败'
|
||||
message.error(msg)
|
||||
setRetryAllOpen(false)
|
||||
} catch (err: unknown) {
|
||||
const e = err as { response?: { data?: { message?: string } }; message?: string }
|
||||
message.error(e?.response?.data?.message || e?.message || '全部重试失败')
|
||||
}
|
||||
}
|
||||
|
||||
const statusColor: Record<string, string> = {
|
||||
QUEUED: 'blue',
|
||||
CLAIMED: 'cyan',
|
||||
DOWNLOADING: 'processing',
|
||||
PARSING: 'processing',
|
||||
OCR_PROCESSING: 'processing',
|
||||
VISION_PROCESSING: 'processing',
|
||||
CLEANING: 'processing',
|
||||
CHUNKING: 'processing',
|
||||
EMBEDDING: 'processing',
|
||||
INDEXING: 'processing',
|
||||
GENERATING_CANDIDATES: 'processing',
|
||||
COMPLETED: 'green',
|
||||
FAILED: 'red',
|
||||
FAILED_RETRYABLE: 'orange',
|
||||
FAILED_FINAL: 'red',
|
||||
COMPLETED: 'green', QUEUED: 'default', CLAIMED: 'blue',
|
||||
DOWNLOADING: 'processing', PARSING: 'processing', CHUNKING: 'processing',
|
||||
EMBEDDING: 'processing', INDEXING: 'processing', GENERATING_CANDIDATES: 'processing',
|
||||
FAILED: 'red', FAILED_RETRYABLE: 'orange', CANCELLED: 'default',
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ title: '名称', dataIndex: 'sourceName', width: 180, ellipsis: true, sorter: true, sortOrder: sortBy === 'sourceName' ? (sortOrder as any) : undefined,
|
||||
render: (v: string, r: any) => (
|
||||
<Space size={4}>
|
||||
<Text ellipsis style={{ maxWidth: 140 }}>{v}</Text>
|
||||
{r.sourceDeleted && <Tag color="error" style={{ fontSize: 10 }}>源已删</Tag>}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ title: '类型', dataIndex: 'sourceType', width: 80, sorter: true, sortOrder: sortBy === 'sourceType' ? (sortOrder as any) : undefined, render: (s: string) => (s === 'file' ? '文件' : s === 'url' ? '链接' : s === 'text' ? '文本' : s) },
|
||||
{ title: '状态', dataIndex: 'status', width: 100, sorter: true, sortOrder: sortBy === 'status' ? (sortOrder as any) : undefined, render: (s: string) => <Tag color={statusColor[s] || 'default'}>{importStatusLabels[s] || s}</Tag> },
|
||||
{ title: '进度', dataIndex: 'progress', width: 70, sorter: true, sortOrder: sortBy === 'progress' ? (sortOrder as any) : undefined, render: (v: number) => `${v || 0}%` },
|
||||
{ title: '重试', dataIndex: 'retryCount', width: 60, align: 'center' as const, sorter: true, sortOrder: sortBy === 'retryCount' ? (sortOrder as any) : undefined },
|
||||
{ title: '名称', dataIndex: 'sourceName', width: 180, ellipsis: true, sorter: true, sortOrder: sortBy === 'sourceName' ? (sortOrder as 'ascend' | 'descend') : undefined,
|
||||
render: (v: string, r: ImportJob) => (
|
||||
<a onClick={() => { setSelectedJob(r); setDrawerOpen(true) }} style={{ fontSize: 12 }}>{v || '—'}</a>
|
||||
) },
|
||||
{ title: '类型', dataIndex: 'sourceType', width: 80, sorter: true, sortOrder: sortBy === 'sourceType' ? (sortOrder as 'ascend' | 'descend') : undefined, render: (s: string) => (s === 'file' ? '文件' : s === 'url' ? '链接' : s === 'text' ? '文本' : s) },
|
||||
{ title: '状态', dataIndex: 'status', width: 100, sorter: true, sortOrder: sortBy === 'status' ? (sortOrder as 'ascend' | 'descend') : undefined, render: (s: string) => <Tag color={statusColor[s] || 'default'}>{importStatusLabels[s] || s}</Tag> },
|
||||
{ title: '进度', dataIndex: 'progress', width: 70, sorter: true, sortOrder: sortBy === 'progress' ? (sortOrder as 'ascend' | 'descend') : undefined, render: (v: number) => `${v || 0}%` },
|
||||
{ title: '重试', dataIndex: 'retryCount', width: 60, align: 'center' as const, sorter: true, sortOrder: sortBy === 'retryCount' ? (sortOrder as 'ascend' | 'descend') : undefined },
|
||||
{ title: '错误', dataIndex: 'errorMessage', width: 180, ellipsis: true, render: (v: string) => v ? <Text type="danger" style={{ fontSize: 12 }}>{v}</Text> : '-' },
|
||||
{ title: '时间', dataIndex: 'createdAt', width: 130, sorter: true, sortOrder: sortBy === 'createdAt' ? (sortOrder as any) : undefined, render: (d: string) => dayjs(d).format('MM-DD HH:mm') },
|
||||
{
|
||||
title: '操作', width: 100,
|
||||
render: (_: any, r: any) => (
|
||||
<Space size="small">
|
||||
{ title: '时间', dataIndex: 'createdAt', width: 130, sorter: true, sortOrder: sortBy === 'createdAt' ? (sortOrder as 'ascend' | 'descend') : undefined, render: (d: string) => dayjs(d).format('MM-DD HH:mm') },
|
||||
{ title: '操作', width: 160,
|
||||
render: (_: unknown, r: ImportJob) => (
|
||||
<Space size={0}>
|
||||
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => { setSelectedJob(r); setDrawerOpen(true) }}>详情</Button>
|
||||
{(r.status?.startsWith('FAILED') || r.status === 'CLAIMED' || r.status === 'QUEUED') && <Button type="link" size="small" icon={<RetweetOutlined />} onClick={() => handleRetry(r.id, r.sourceDeleted)}>重新解析</Button>}
|
||||
<Button type="link" size="small" icon={<RetweetOutlined />} disabled={r.sourceDeleted} onClick={() => handleRetry(r.id, r.sourceDeleted)}>重试</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
) },
|
||||
]
|
||||
|
||||
const stepCols = [
|
||||
const stepColumns = [
|
||||
{ title: '步骤', dataIndex: 'step', width: 120, render: (s: string) => importStepLabels[s] || importStatusLabels[s] || s },
|
||||
{ title: '状态', dataIndex: 'status', width: 80, render: (s: string) => <Tag color={s === 'COMPLETED' || s === 'completed' ? 'green' : s?.includes('FAILED') || s === 'failed' ? 'red' : 'blue'}>{importStepLabels[s] || importStatusLabels[s] || s}</Tag> },
|
||||
{ title: '详情', dataIndex: 'detail', ellipsis: true },
|
||||
@ -186,99 +167,56 @@ export default function ImportMonitorPage() {
|
||||
{ title: '完成', dataIndex: 'completedAt', width: 130, render: (d: string) => d ? dayjs(d).format('HH:mm:ss') : '-' },
|
||||
]
|
||||
|
||||
const kbOptions = (kbData?.items || []).map((kb: any) => ({
|
||||
const kbOptions = (kbData?.items || []).map((kb: KnowledgeBase) => ({
|
||||
value: kb.id,
|
||||
label: formatKbLabel(kb),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<Title level={5} style={{ margin: 0 }}><ImportOutlined /> 文档导入</Title>
|
||||
<div style={{ padding: '0 0 24px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Title level={5} style={{ margin: 0 }}><ImportOutlined /> 文档导入监控</Title>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['admin-imports', 'list'] })}>刷新</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={refreshList} size="small">刷新</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row style={{ marginBottom: 12 }} gutter={12}>
|
||||
<Row gutter={[8, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={false}
|
||||
placeholder="搜索知识库(名称/ID/用户)"
|
||||
style={{ width: 320 }}
|
||||
value={kbFilter}
|
||||
loading={kbLoading}
|
||||
onChange={(val) => { setKbFilter(val); setSelectedRowKeys([]) }}
|
||||
onSearch={handleKbSearch}
|
||||
options={kbOptions}
|
||||
notFoundContent={kbLoading ? '搜索中...' : '无匹配知识库'}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<Checkbox checked={showDeleted} onChange={(e) => setShowDeleted(e.target.checked)}>
|
||||
显示已删除源
|
||||
</Checkbox>
|
||||
</Col>
|
||||
<Col>
|
||||
<Button
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
icon={<RetweetOutlined />}
|
||||
onClick={handleBatchRetry}
|
||||
>
|
||||
批量重新解析 ({selectedRowKeys.length})
|
||||
</Button>
|
||||
</Col>
|
||||
<Col>
|
||||
<Popconfirm
|
||||
title="确定重新解析当前筛选的所有任务?"
|
||||
description="将把所有 FAILED / QUEUED / CLAIMED 任务重新入队"
|
||||
open={retryAllOpen}
|
||||
onOpenChange={setRetryAllOpen}
|
||||
onConfirm={handleRetryAll}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button icon={<RetweetOutlined />}>全部重新解析</Button>
|
||||
</Popconfirm>
|
||||
<Select allowClear showSearch placeholder="知识库" value={kbFilter} onChange={setKbFilter} onSearch={handleKbSearch} filterOption={false} options={kbOptions} loading={kbLoading} style={{ width: 280 }} notFoundContent={kbSearch ? '未找到' : '输入关键词搜索'} />
|
||||
</Col>
|
||||
<Col><Checkbox checked={showDeleted} onChange={e => setShowDeleted(e.target.checked)}>显示已删除源</Checkbox></Col>
|
||||
<Col><Popconfirm title="确认重试所有可重试任务?" open={retryAllOpen} onOpenChange={setRetryAllOpen} onConfirm={handleRetryAll}><Button size="small" icon={<RetweetOutlined />}>全部重试</Button></Popconfirm></Col>
|
||||
{selectedRowKeys.length > 0 && <Col><Button size="small" type="primary" onClick={handleBatchRetry}>批量重试 ({selectedRowKeys.length})</Button></Col>}
|
||||
</Row>
|
||||
|
||||
<Table
|
||||
dataSource={tableData}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
loading={isLoading}
|
||||
pagination={{ pageSize: 30 }}
|
||||
rowSelection={{ selectedRowKeys, onChange: keys => setSelectedRowKeys(keys) }}
|
||||
pagination={false}
|
||||
size="small"
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys),
|
||||
}}
|
||||
onChange={(_pagination, _filters, sorter: any) => {
|
||||
if (sorter.order) {
|
||||
setSortBy(sorter.field || 'createdAt')
|
||||
setSortOrder(sorter.order)
|
||||
} else {
|
||||
setSortBy('createdAt')
|
||||
setSortOrder('descend')
|
||||
}
|
||||
onChange={(_pagination: TablePaginationConfig, _filters: Record<string, FilterValue>, sorter: SorterResult<ImportJob> | SorterResult<ImportJob>[]) => {
|
||||
const s = Array.isArray(sorter) ? sorter[0] : sorter
|
||||
if (s.columnKey) setSortBy(String(s.columnKey))
|
||||
setSortOrder(s.order || 'descend')
|
||||
}}
|
||||
/>
|
||||
|
||||
<Drawer title={selectedJob?.sourceName || '导入详情'} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={700}>
|
||||
{detail && (
|
||||
<Drawer title={selectedJob?.sourceName || '导入详情'} open={drawerOpen} onClose={() => { setDrawerOpen(false); setSelectedJob(null) }} width={700}>
|
||||
{detail ? (
|
||||
<>
|
||||
<Title level={5} style={{ fontSize: 14 }}>Job: {detail.job?.id}</Title>
|
||||
<Text>状态: <Tag color={statusColor[detail.job?.status]}>{importStatusLabels[detail.job?.status] || detail.job?.status}</Tag></Text>
|
||||
<Text style={{ marginLeft: 16 }}>进度: {detail.job?.progress || 0}%</Text>
|
||||
<Text style={{ marginLeft: 16 }}>重试: {detail.job?.retryCount}/{detail.job?.maxRetries}</Text>
|
||||
{detail.job?.errorMessage && <div style={{ marginTop: 8 }}><Text type="danger">{detail.job?.errorMessage}</Text></div>}
|
||||
<Title level={5} style={{ fontSize: 14, marginTop: 16 }}>步骤日志 ({detail.steps?.length || 0})</Title>
|
||||
<Table dataSource={detail.steps || []} columns={stepCols} rowKey="id" pagination={false} size="small" />
|
||||
<Space wrap style={{ marginBottom: 16 }}>
|
||||
<Tag color={statusColor[detail.status] || 'default'}>{importStatusLabels[detail.status] || detail.status}</Tag>
|
||||
<Text type="secondary">进度: {detail.progress || 0}%</Text>
|
||||
<Text type="secondary">重试: {detail.retryCount}</Text>
|
||||
<Text type="secondary">知识库: {detail.kbName || detail.kbId?.slice(0, 8)}</Text>
|
||||
<Text type="secondary">创建: {dayjs(detail.createdAt).format('YYYY-MM-DD HH:mm:ss')}</Text>
|
||||
</Space>
|
||||
{detail.errorMessage && <Text type="danger" style={{ display: 'block', marginBottom: 12 }}>错误: {detail.errorMessage}</Text>}
|
||||
<Title level={5}>步骤日志</Title>
|
||||
<Table rowKey="id" columns={stepColumns} dataSource={(detail.steps || []) as ImportStepLog[]} pagination={false} size="small" />
|
||||
</>
|
||||
)}
|
||||
) : <Text type="secondary">加载中...</Text>}
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -23,14 +23,14 @@ function KBPage() {
|
||||
|
||||
const { data: sources } = useQuery({
|
||||
queryKey: ['kb-sources', selectedKb?.id],
|
||||
queryFn: (): Promise<any> => knowledgeAPI.sources(selectedKb?.id || ''),
|
||||
queryFn: () => knowledgeAPI.sources(selectedKb?.id || ''),
|
||||
enabled: !!selectedKb?.id,
|
||||
})
|
||||
|
||||
const [refSourceId, setRefSourceId] = useState<string | null>(null)
|
||||
const { data: references } = useQuery({
|
||||
queryKey: ['source-refs', refSourceId],
|
||||
queryFn: (): Promise<any> => knowledgeAPI.sourceReferences(refSourceId || ''),
|
||||
queryFn: () => knowledgeAPI.sourceReferences(refSourceId || ''),
|
||||
enabled: !!refSourceId,
|
||||
})
|
||||
|
||||
|
||||
@ -16,12 +16,12 @@ export default function KnowledgeOpsPage() {
|
||||
|
||||
const { data: candidates } = useQuery({
|
||||
queryKey: ['ops', 'candidates'],
|
||||
queryFn: (): Promise<any> => knowledgeAPI.candidates(),
|
||||
queryFn: () => knowledgeAPI.candidates(),
|
||||
})
|
||||
|
||||
const { data: chunks } = useQuery({
|
||||
queryKey: ['ops', 'chunks', sourceId],
|
||||
queryFn: (): Promise<any> => knowledgeAPI.chunks(sourceId),
|
||||
queryFn: () => knowledgeAPI.chunks(sourceId),
|
||||
enabled: !!sourceId,
|
||||
})
|
||||
|
||||
|
||||
@ -13,9 +13,9 @@ export default function MemberManagement() {
|
||||
const [addOpen, setAddOpen] = useState(false)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const { data: usersData } = useQuery({ queryKey: ['users', 'list'], queryFn: (): Promise<any> => membersAPI.list() })
|
||||
const { data: memberships } = useQuery({ queryKey: ['users', 'memberships'], queryFn: (): Promise<any> => membersAPI.memberships() })
|
||||
const { data: deletions } = useQuery({ queryKey: ['users', 'deletions'], queryFn: (): Promise<any> => membersAPI.deletionRequests() })
|
||||
const { data: usersData } = useQuery({ queryKey: ['users', 'list'], queryFn: () => membersAPI.list() })
|
||||
const { data: memberships } = useQuery({ queryKey: ['users', 'memberships'], queryFn: () => membersAPI.memberships() })
|
||||
const { data: deletions } = useQuery({ queryKey: ['users', 'deletions'], queryFn: () => membersAPI.deletionRequests() })
|
||||
|
||||
const assignMembership = async (v: any) => {
|
||||
await membersAPI.updateMembership({ userId: v.userId, planId: v.planId, expiresAt: v.expiresAt?.toISOString() })
|
||||
|
||||
@ -13,9 +13,9 @@ function MembershipPage() {
|
||||
const [addOpen, setAddOpen] = useState(false)
|
||||
const [newPlan, setNewPlan] = useState({ name: '', code: '', priceMonthly: 0, monthlyChatCount: 10 })
|
||||
|
||||
const { data: plans } = useQuery({ queryKey: ['quota', 'plans'], queryFn: (): Promise<any> => quotaAPI.plans() })
|
||||
const { data: memberships } = useQuery({ queryKey: ['quota', 'memberships'], queryFn: (): Promise<any> => quotaAPI.memberships() })
|
||||
const { data: costs } = useQuery({ queryKey: ['quota', 'costs'], queryFn: (): Promise<any> => quotaAPI.costs() })
|
||||
const { data: plans } = useQuery({ queryKey: ['quota', 'plans'], queryFn: () => quotaAPI.plans() })
|
||||
const { data: memberships } = useQuery({ queryKey: ['quota', 'memberships'], queryFn: () => quotaAPI.memberships() })
|
||||
const { data: costs } = useQuery({ queryKey: ['quota', 'costs'], queryFn: () => quotaAPI.costs() })
|
||||
|
||||
const addPlan = async () => {
|
||||
await quotaAPI.createPlan(newPlan)
|
||||
|
||||
@ -9,11 +9,11 @@ const { Title } = Typography
|
||||
function MetricsPage() {
|
||||
const qc = useQueryClient()
|
||||
|
||||
const { data: overview } = useQuery({ queryKey: ['metrics', 'overview'], queryFn: (): Promise<any> => metricsAPI.overview(), staleTime: 10_000 })
|
||||
const { data: top } = useQuery({ queryKey: ['metrics', 'top'], queryFn: (): Promise<any> => metricsAPI.top(), staleTime: 10_000 })
|
||||
const { data: recent } = useQuery({ queryKey: ['metrics', 'recent'], queryFn: (): Promise<any> => metricsAPI.recent(), staleTime: 5_000 })
|
||||
const { data: ai } = useQuery({ queryKey: ['metrics', 'ai'], queryFn: (): Promise<any> => metricsAPI.ai(), staleTime: 10_000 })
|
||||
const { data: worker } = useQuery({ queryKey: ['metrics', 'worker'], queryFn: (): Promise<any> => metricsAPI.worker(), staleTime: 10_000 })
|
||||
const { data: overview } = useQuery({ queryKey: ['metrics', 'overview'], queryFn: () => metricsAPI.overview(), staleTime: 10_000 })
|
||||
const { data: top } = useQuery({ queryKey: ['metrics', 'top'], queryFn: () => metricsAPI.top(), staleTime: 10_000 })
|
||||
const { data: recent } = useQuery({ queryKey: ['metrics', 'recent'], queryFn: () => metricsAPI.recent(), staleTime: 5_000 })
|
||||
const { data: ai } = useQuery({ queryKey: ['metrics', 'ai'], queryFn: () => metricsAPI.ai(), staleTime: 10_000 })
|
||||
const { data: worker } = useQuery({ queryKey: ['metrics', 'worker'], queryFn: () => metricsAPI.worker(), staleTime: 10_000 })
|
||||
|
||||
const topCols = [
|
||||
{ title: '接口', dataIndex: 'path', width: 300, ellipsis: true },
|
||||
|
||||
@ -16,9 +16,9 @@ function SecretsPage() {
|
||||
const [form, setForm] = useState({ name: '', provider: 'deepseek', value: '', expiresAt: '' })
|
||||
const [billForm, setBillForm] = useState({ provider: 'deepseek', billMonth: dayjs().format('YYYY-MM'), amount: 0, notes: '' })
|
||||
|
||||
const { data: secrets } = useQuery({ queryKey: ['secrets'], queryFn: (): Promise<any> => secretsAPI.list() })
|
||||
const { data: logs } = useQuery({ queryKey: ['secrets', 'logs'], queryFn: (): Promise<any> => secretsAPI.logs() })
|
||||
const { data: bills } = useQuery({ queryKey: ['vendor', 'bills'], queryFn: (): Promise<any> => secretsAPI.vendorBills() })
|
||||
const { data: secrets } = useQuery({ queryKey: ['secrets'], queryFn: () => secretsAPI.list() })
|
||||
const { data: logs } = useQuery({ queryKey: ['secrets', 'logs'], queryFn: () => secretsAPI.logs() })
|
||||
const { data: bills } = useQuery({ queryKey: ['vendor', 'bills'], queryFn: () => secretsAPI.vendorBills() })
|
||||
|
||||
const addSecret = async () => {
|
||||
await secretsAPI.create(form)
|
||||
|
||||
@ -11,13 +11,13 @@ export default function VectorAdminPage() {
|
||||
|
||||
const { data: coll, isLoading: collLoading } = useQuery({
|
||||
queryKey: ['vector', 'collection'],
|
||||
queryFn: (): Promise<any> => vectorAPI.collection(),
|
||||
queryFn: () => vectorAPI.collection(),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
const { data: count } = useQuery({
|
||||
queryKey: ['vector', 'count'],
|
||||
queryFn: (): Promise<any> => vectorAPI.count(),
|
||||
queryFn: () => vectorAPI.count(),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
|
||||
@ -82,7 +82,7 @@ export default function SessionPage() {
|
||||
<Descriptions.Item label="结束时间" span={2}>{detail.endedAt ? dayjs(detail.endedAt).format('YYYY-MM-DD HH:mm:ss') : '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="最后事件时间" span={2}>{detail.lastEventAt ? dayjs(detail.lastEventAt).format('YYYY-MM-DD HH:mm:ss') : '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="关联事件数">{detail.eventCount ?? '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="时长组成">{detail.durationBreakdown || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="时长组成">{String(detail.durationBreakdown ?? '-')}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
{detail?.clientSessionId && (
|
||||
|
||||
1
src/stores/index.ts
Normal file
1
src/stores/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { useUIStore } from './ui-store'
|
||||
22
src/stores/ui-store.ts
Normal file
22
src/stores/ui-store.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
interface UIStore {
|
||||
sidebarCollapsed: boolean
|
||||
toggleSidebar: () => void
|
||||
setSidebarCollapsed: (collapsed: boolean) => void
|
||||
}
|
||||
|
||||
export const useUIStore = create<UIStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
sidebarCollapsed: false,
|
||||
toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })),
|
||||
setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),
|
||||
}),
|
||||
{
|
||||
name: 'admin-ui-prefs',
|
||||
partialize: (state) => ({ sidebarCollapsed: state.sidebarCollapsed }),
|
||||
},
|
||||
),
|
||||
)
|
||||
51
src/types/ai.ts
Normal file
51
src/types/ai.ts
Normal file
@ -0,0 +1,51 @@
|
||||
// ── AI Gateway ──
|
||||
|
||||
export interface AiRoute {
|
||||
id: string
|
||||
name?: string
|
||||
fromProvider: string
|
||||
fromModel: string
|
||||
toProvider: string
|
||||
toModel: string
|
||||
preferredProvider?: string
|
||||
preferredModel?: string
|
||||
fallbackProvider?: string
|
||||
fallbackModel?: string
|
||||
enabled: boolean
|
||||
priority?: number
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface AiProvider {
|
||||
name: string
|
||||
enabled: boolean
|
||||
models?: string[]
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface FallbackEvent {
|
||||
id: string
|
||||
routeId?: string
|
||||
fromProvider: string
|
||||
fromModel: string
|
||||
toProvider: string
|
||||
toModel: string
|
||||
reason?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AiGatewayStatus {
|
||||
providers: AiProvider[]
|
||||
routes: AiRoute[]
|
||||
}
|
||||
|
||||
export interface AiCostRecord {
|
||||
id: string
|
||||
provider: string
|
||||
model: string
|
||||
feature?: string
|
||||
promptTokens?: number
|
||||
completionTokens?: number
|
||||
cost?: number
|
||||
createdAt: string
|
||||
}
|
||||
40
src/types/billing.ts
Normal file
40
src/types/billing.ts
Normal file
@ -0,0 +1,40 @@
|
||||
// ── 成本 / 账单 ──
|
||||
|
||||
export interface CostItem {
|
||||
id: string
|
||||
name: string
|
||||
currency: string
|
||||
amount: number
|
||||
purchaseDate?: string
|
||||
expiryDate?: string
|
||||
billingCycle?: string
|
||||
category?: string
|
||||
provider?: string
|
||||
note?: string
|
||||
}
|
||||
|
||||
export interface CostSummary {
|
||||
totalMonthly: number
|
||||
totalYearly: number
|
||||
totalOneTime: number
|
||||
items: CostItem[]
|
||||
byMonth: CostByMonth[]
|
||||
expiringSoon: CostItem[]
|
||||
}
|
||||
|
||||
export interface CostByMonth {
|
||||
month: string
|
||||
total: number
|
||||
}
|
||||
|
||||
// ── 供应商账单 ──
|
||||
|
||||
export interface BillingInfo {
|
||||
name: string
|
||||
model: string
|
||||
balance: string
|
||||
currency: string
|
||||
status: 'ok' | 'unknown'
|
||||
consoleUrl: string
|
||||
note: string
|
||||
}
|
||||
20
src/types/chat.ts
Normal file
20
src/types/chat.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// ── 对话 / 聊天 ──
|
||||
|
||||
export interface Conversation {
|
||||
id: string
|
||||
title: string
|
||||
userId?: string
|
||||
knowledgeBaseId?: string
|
||||
messageCount?: number
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface MessageRecord {
|
||||
id: string
|
||||
content: string
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
conversationId?: string
|
||||
userId?: string
|
||||
createdAt?: string
|
||||
}
|
||||
49
src/types/compliance.ts
Normal file
49
src/types/compliance.ts
Normal file
@ -0,0 +1,49 @@
|
||||
// ── 合规 / 安全 ──
|
||||
|
||||
export interface PrivacyPolicy {
|
||||
id: string
|
||||
version: string
|
||||
title: string
|
||||
content: string
|
||||
effectiveAt: string
|
||||
published: boolean
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface UserAgreement {
|
||||
id: string
|
||||
version: string
|
||||
title: string
|
||||
content: string
|
||||
effectiveAt: string
|
||||
published: boolean
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface Filing {
|
||||
id: string
|
||||
type: string
|
||||
title: string
|
||||
description?: string
|
||||
status: string
|
||||
filedAt?: string
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface DeletionRequest {
|
||||
id: string
|
||||
userId: string
|
||||
userName?: string
|
||||
reason?: string
|
||||
status: 'pending' | 'approved' | 'rejected'
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface SecurityEvent {
|
||||
id: string
|
||||
type: string
|
||||
userId?: string
|
||||
detail?: string
|
||||
severity?: string
|
||||
createdAt: string
|
||||
}
|
||||
26
src/types/config.ts
Normal file
26
src/types/config.ts
Normal file
@ -0,0 +1,26 @@
|
||||
// ── 系统配置 ──
|
||||
|
||||
export interface AppConfig {
|
||||
key: string
|
||||
value: string
|
||||
description?: string
|
||||
category?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface FeatureFlag {
|
||||
name: string
|
||||
enabled: boolean
|
||||
description?: string
|
||||
whitelist?: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface ConfigChangeLog {
|
||||
id: string
|
||||
key: string
|
||||
oldValue?: string
|
||||
newValue: string
|
||||
changedBy?: string
|
||||
changedAt: string
|
||||
}
|
||||
31
src/types/events.ts
Normal file
31
src/types/events.ts
Normal file
@ -0,0 +1,31 @@
|
||||
// ── 事件队列 ──
|
||||
|
||||
export interface QueueInfo {
|
||||
name: string
|
||||
waiting: number
|
||||
active: number
|
||||
failed: number
|
||||
completed?: number
|
||||
delayed?: number
|
||||
}
|
||||
|
||||
export interface FailedJob {
|
||||
id: string
|
||||
name: string
|
||||
failedReason: string
|
||||
queue?: string
|
||||
failedAt?: string
|
||||
}
|
||||
|
||||
export interface JobDetail {
|
||||
id: string
|
||||
data: Record<string, unknown>
|
||||
queue?: string
|
||||
status?: string
|
||||
attempts?: number
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface QueueOverview {
|
||||
queues: QueueInfo[]
|
||||
}
|
||||
64
src/types/index.ts
Normal file
64
src/types/index.ts
Normal file
@ -0,0 +1,64 @@
|
||||
// ── 领域模型 ──
|
||||
export type { AdminRole, AdminUser, TrendPoint, DashboardStats, AuditLog } from './admin'
|
||||
export type {
|
||||
ReadingEvent,
|
||||
LearningSession,
|
||||
MaterialReadingProgress,
|
||||
DailyLearningActivity,
|
||||
LearningRecord,
|
||||
AdminDashboard,
|
||||
AnomalyData,
|
||||
UserTimeline,
|
||||
UserDiagnose,
|
||||
MaterialDiagnose,
|
||||
ExportResult,
|
||||
} from './learning'
|
||||
export type {
|
||||
KnowledgeBase,
|
||||
KnowledgeSource,
|
||||
SourceReference,
|
||||
ImportJob,
|
||||
ImportJobStatus,
|
||||
ImportStepLog,
|
||||
ImportJobDetail,
|
||||
KnowledgeChunk,
|
||||
KnowledgeCandidate,
|
||||
} from './knowledge'
|
||||
export type {
|
||||
ServerInfo,
|
||||
ProcessInfo,
|
||||
HealthService,
|
||||
ServerHealth,
|
||||
WorkerInfo,
|
||||
ServerMetrics,
|
||||
} from './server'
|
||||
export type { AiRoute, AiProvider, FallbackEvent, AiGatewayStatus, AiCostRecord } from './ai'
|
||||
export type { CostItem, CostSummary, CostByMonth, BillingInfo } from './billing'
|
||||
export type { QueueInfo, FailedJob, JobDetail, QueueOverview } from './events'
|
||||
export type { Conversation, MessageRecord } from './chat'
|
||||
export type { MemberUser, MembershipPlan, Membership } from './user'
|
||||
export type {
|
||||
PrivacyPolicy,
|
||||
UserAgreement,
|
||||
Filing,
|
||||
DeletionRequest,
|
||||
SecurityEvent,
|
||||
} from './compliance'
|
||||
export type { AppConfig, FeatureFlag, ConfigChangeLog } from './config'
|
||||
export type { SensitiveWord, SafetyCheck, SafetyReport, Violation } from './safety'
|
||||
export type { ApiKey, VendorBill } from './secret'
|
||||
export type { ReviewCard, Changelog, ReleaseDecision, ReleaseChecklistItem } from './review'
|
||||
export type { VectorSearchResult, VectorDebugQuery, VectorDebugResponse } from './vector'
|
||||
|
||||
// ── 通用 / API ──
|
||||
export type {
|
||||
PaginatedResult,
|
||||
PaginationParams,
|
||||
CacheStats,
|
||||
NotificationTemplate,
|
||||
NotificationLog,
|
||||
ReviewCardItem,
|
||||
} from './api'
|
||||
|
||||
// ── UI ──
|
||||
export type { TypedColumn, SortState, FilterState, ModalControl, DrawerControl } from './ui'
|
||||
110
src/types/knowledge.ts
Normal file
110
src/types/knowledge.ts
Normal file
@ -0,0 +1,110 @@
|
||||
// ── 知识库 ──
|
||||
|
||||
export interface KnowledgeBase {
|
||||
id: string
|
||||
name: string
|
||||
title?: string
|
||||
description?: string
|
||||
userId: string
|
||||
user?: { nickname?: string; email?: string }
|
||||
sourceCount?: number
|
||||
chunkCount?: number
|
||||
candidateCount?: number
|
||||
createdAt: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
// ── 知识源 ──
|
||||
|
||||
export interface KnowledgeSource {
|
||||
id: string
|
||||
knowledgeBaseId: string
|
||||
kbName?: string
|
||||
sourceName: string
|
||||
sourceType: 'file' | 'url' | 'text'
|
||||
sourceUrl?: string
|
||||
status: string
|
||||
fileType?: string
|
||||
fileSize?: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface SourceReference {
|
||||
id: string
|
||||
sourceId: string
|
||||
title?: string
|
||||
url?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
// ── 文档导入任务 ──
|
||||
|
||||
export interface ImportJob {
|
||||
id: string
|
||||
kbId: string
|
||||
kbName?: string
|
||||
sourceId?: string
|
||||
sourceName: string
|
||||
sourceType: 'file' | 'url' | 'text'
|
||||
status: ImportJobStatus
|
||||
progress: number
|
||||
retryCount: number
|
||||
errorMessage?: string
|
||||
sourceDeleted?: boolean
|
||||
createdAt: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export type ImportJobStatus =
|
||||
| 'QUEUED'
|
||||
| 'CLAIMED'
|
||||
| 'DOWNLOADING'
|
||||
| 'PARSING'
|
||||
| 'CHUNKING'
|
||||
| 'EMBEDDING'
|
||||
| 'INDEXING'
|
||||
| 'GENERATING_CANDIDATES'
|
||||
| 'COMPLETED'
|
||||
| 'FAILED'
|
||||
| 'FAILED_RETRYABLE'
|
||||
| 'CANCELLED'
|
||||
|
||||
// ── 导入步骤日志 ──
|
||||
|
||||
export interface ImportStepLog {
|
||||
id: string
|
||||
importId: string
|
||||
step: string
|
||||
status: string
|
||||
detail?: string
|
||||
startedAt?: string
|
||||
completedAt?: string
|
||||
}
|
||||
|
||||
export interface ImportJobDetail extends ImportJob {
|
||||
steps?: ImportStepLog[]
|
||||
}
|
||||
|
||||
// ── 知识块 ──
|
||||
|
||||
export interface KnowledgeChunk {
|
||||
id: string
|
||||
sourceId: string
|
||||
content: string
|
||||
chunkIndex?: number
|
||||
tokenCount?: number
|
||||
confidence?: number
|
||||
}
|
||||
|
||||
// ── 候选知识点 ──
|
||||
|
||||
export interface KnowledgeCandidate {
|
||||
id: string
|
||||
knowledgeBaseId: string
|
||||
sourceId: string
|
||||
importId?: string
|
||||
title: string
|
||||
content?: string
|
||||
confidence?: number
|
||||
status?: string
|
||||
}
|
||||
@ -17,6 +17,8 @@ export interface ReadingEvent {
|
||||
appVersion?: string;
|
||||
status: string;
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
retryCount?: number;
|
||||
warningCodes?: unknown;
|
||||
serverReceivedAt: string;
|
||||
processedAt?: string;
|
||||
@ -38,8 +40,17 @@ export interface LearningSession {
|
||||
totalActiveSeconds: number;
|
||||
clientSessionId?: string;
|
||||
materialId?: string;
|
||||
materialName?: string;
|
||||
readingTargetType?: string;
|
||||
targetType?: string;
|
||||
lastPosition?: unknown;
|
||||
userName?: string;
|
||||
kbName?: string;
|
||||
interrupted?: boolean;
|
||||
missingMaterialClosed?: boolean;
|
||||
abnormalDelta?: number;
|
||||
eventCount?: number;
|
||||
durationBreakdown?: unknown;
|
||||
lastEventAt?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@ -98,10 +109,11 @@ export interface LearningRecord {
|
||||
|
||||
// ── Dashboard ──
|
||||
export interface AdminDashboard {
|
||||
overview: { totalEvents: number; todayEvents: number; failedEvents: number; duplicateEvents: number };
|
||||
overview: { totalEvents: number; todayEvents: number; failedEvents: number; duplicateEvents: number; warningEvents?: number; totalActiveSeconds?: number };
|
||||
sessions: { active: number; interrupted: number; completed: number; total: number };
|
||||
users: { activeToday: number; totalWithEvents: number };
|
||||
materials: { totalRead: number; totalMarkedRead: number };
|
||||
recentAnomalies?: Array<{ eventId: string; eventType: string; errorCode?: string; createdAt: string }>;
|
||||
}
|
||||
|
||||
// ── Anomalies ──
|
||||
|
||||
49
src/types/review.ts
Normal file
49
src/types/review.ts
Normal file
@ -0,0 +1,49 @@
|
||||
// ── 复习 / 发布 ──
|
||||
|
||||
import type { ReviewCardItem } from './api'
|
||||
|
||||
export type { ReviewCardItem }
|
||||
|
||||
export interface ReviewCard {
|
||||
id: string
|
||||
userId: string
|
||||
userName?: string
|
||||
frontText: string
|
||||
backText?: string
|
||||
difficulty: string | null
|
||||
status: string
|
||||
scheduleState: string | null
|
||||
intervalDays: number
|
||||
repetitionCount: number
|
||||
lapseCount: number
|
||||
nextReviewAt: string | null
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface Changelog {
|
||||
id: string
|
||||
version: string
|
||||
title: string
|
||||
content: string
|
||||
type: 'feature' | 'fix' | 'improvement' | 'breaking'
|
||||
published: boolean
|
||||
publishedAt?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface ReleaseDecision {
|
||||
id: string
|
||||
title: string
|
||||
description?: string
|
||||
status: string
|
||||
priority?: string
|
||||
assignee?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface ReleaseChecklistItem {
|
||||
id: string
|
||||
label: string
|
||||
checked: boolean
|
||||
category?: string
|
||||
}
|
||||
42
src/types/safety.ts
Normal file
42
src/types/safety.ts
Normal file
@ -0,0 +1,42 @@
|
||||
// ── 内容安全 ──
|
||||
|
||||
export interface SensitiveWord {
|
||||
id: string
|
||||
word: string
|
||||
category?: string
|
||||
risk: 'low' | 'medium' | 'high'
|
||||
enabled: boolean
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface SafetyCheck {
|
||||
id: string
|
||||
userId: string
|
||||
userName?: string
|
||||
content: string
|
||||
type: string
|
||||
result: string
|
||||
matchedWords?: string[]
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface SafetyReport {
|
||||
id: string
|
||||
userId: string
|
||||
userName?: string
|
||||
targetType: string
|
||||
targetId: string
|
||||
reason: string
|
||||
status: 'pending' | 'handled' | 'dismissed'
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface Violation {
|
||||
id: string
|
||||
userId: string
|
||||
userName?: string
|
||||
type: string
|
||||
detail?: string
|
||||
penalty: 'none' | 'warning' | 'suspend' | 'ban'
|
||||
createdAt: string
|
||||
}
|
||||
22
src/types/secret.ts
Normal file
22
src/types/secret.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// ── 密钥 / API Key ──
|
||||
|
||||
export interface ApiKey {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
key: string
|
||||
status: string
|
||||
expiresAt?: string
|
||||
lastUsedAt?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface VendorBill {
|
||||
id: string
|
||||
vendor: string
|
||||
amount: number
|
||||
currency: string
|
||||
period?: string
|
||||
status?: string
|
||||
createdAt?: string
|
||||
}
|
||||
53
src/types/server.ts
Normal file
53
src/types/server.ts
Normal file
@ -0,0 +1,53 @@
|
||||
// ── 服务器信息 ──
|
||||
|
||||
export interface ServerInfo {
|
||||
name: string
|
||||
hostname?: string
|
||||
role?: string
|
||||
cpu: { usagePercent: number; cores: number; model?: string }
|
||||
memory: { percent: number; used: string; total: string }
|
||||
network: { domains: string[]; privateIp?: string; publicIp?: string }
|
||||
disks: { mount: string; size: string; used: string; percent: number }[]
|
||||
uptime?: string
|
||||
processes?: ProcessInfo[]
|
||||
}
|
||||
|
||||
export interface ProcessInfo {
|
||||
pid: number
|
||||
name: string
|
||||
cpu: number
|
||||
memory: number
|
||||
desc?: string
|
||||
command?: string
|
||||
}
|
||||
|
||||
export interface HealthService {
|
||||
serviceName: string
|
||||
status: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface ServerHealth {
|
||||
name: string
|
||||
serverName?: string
|
||||
status: string
|
||||
uptime?: string
|
||||
pid?: number
|
||||
services?: HealthService[]
|
||||
queues?: string[]
|
||||
}
|
||||
|
||||
export interface WorkerInfo {
|
||||
name: string
|
||||
status: string
|
||||
instanceId?: string
|
||||
hostname?: string
|
||||
appVersion?: string
|
||||
queues?: string[]
|
||||
startedAt?: string
|
||||
lastHeartbeatAgo?: number
|
||||
}
|
||||
|
||||
export interface ServerMetrics {
|
||||
servers: ServerInfo[]
|
||||
}
|
||||
40
src/types/ui.ts
Normal file
40
src/types/ui.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
// ── 通用表格列类型(替代 (_: any, r: any))──
|
||||
|
||||
export interface TypedColumn<T> {
|
||||
title: string
|
||||
dataIndex?: keyof T & string
|
||||
key?: string
|
||||
width?: number | string
|
||||
ellipsis?: boolean
|
||||
align?: 'left' | 'center' | 'right'
|
||||
sorter?: boolean
|
||||
sortOrder?: 'ascend' | 'descend' | null
|
||||
render?: (value: unknown, record: T, index: number) => ReactNode
|
||||
}
|
||||
|
||||
// ── 排序 / 筛选 ──
|
||||
|
||||
export interface SortState {
|
||||
sortBy: string
|
||||
sortOrder: 'ascend' | 'descend'
|
||||
}
|
||||
|
||||
export interface FilterState {
|
||||
page: number
|
||||
pageSize?: number
|
||||
search?: string
|
||||
}
|
||||
|
||||
// ── 通用 UI 状态 ──
|
||||
|
||||
export interface ModalControl<T = unknown> {
|
||||
open: boolean
|
||||
editing?: T
|
||||
}
|
||||
|
||||
export interface DrawerControl<T = unknown> {
|
||||
open: boolean
|
||||
selected?: T
|
||||
}
|
||||
37
src/types/user.ts
Normal file
37
src/types/user.ts
Normal file
@ -0,0 +1,37 @@
|
||||
// ── 用户 / 会员 ──
|
||||
|
||||
export interface MemberUser {
|
||||
id: string
|
||||
userId: string
|
||||
userName?: string
|
||||
email?: string
|
||||
nickname?: string
|
||||
role?: string
|
||||
status?: string
|
||||
plan?: MembershipPlan
|
||||
quotaUsed?: number
|
||||
quotaLimit?: number
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface MembershipPlan {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
priceMonthly: number
|
||||
monthlyChatCount?: number
|
||||
monthlyStorageBytes?: number
|
||||
maxKnowledgeBases?: number
|
||||
enabled?: boolean
|
||||
createdAt?: string
|
||||
}
|
||||
|
||||
export interface Membership {
|
||||
id: string
|
||||
userId: string
|
||||
planId: string
|
||||
plan?: MembershipPlan
|
||||
status: 'active' | 'cancelled' | 'expired' | 'pending'
|
||||
startedAt: string
|
||||
expiresAt?: string
|
||||
}
|
||||
22
src/types/vector.ts
Normal file
22
src/types/vector.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// ── 向量检索 ──
|
||||
|
||||
export interface VectorSearchResult {
|
||||
id: string
|
||||
score: number
|
||||
content?: string
|
||||
sourceId?: string
|
||||
sourceName?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface VectorDebugQuery {
|
||||
query: string
|
||||
limit?: number
|
||||
collection?: string
|
||||
}
|
||||
|
||||
export interface VectorDebugResponse {
|
||||
results: VectorSearchResult[]
|
||||
latency?: number
|
||||
collection?: string
|
||||
}
|
||||
10
src/vite-env.d.ts
vendored
Normal file
10
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
/** API 后端地址(仅本地开发使用,生产通过 Nginx 代理) */
|
||||
readonly VITE_API_BASE_URL?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
@ -15,6 +15,11 @@
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Strict — 渐进式收紧 */
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"strictNullChecks": false,
|
||||
|
||||
/* Linting */
|
||||
"baseUrl": ".",
|
||||
"paths": { "@/*": ["src/*"] },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user