refactor: TypeScript 类型体系重构 + 状态管理 + 环境配置
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:
wangdl 2026-07-04 11:02:52 +08:00
parent f291f39049
commit 91698a2f3b
58 changed files with 1003 additions and 257 deletions

13
.env.example Normal file
View File

@ -0,0 +1,13 @@
# ============================================
# 知习 Admin 前端 — 环境变量
# ============================================
# 复制此文件为 .env 用于本地开发
# cp .env.example .env
#
# 生产环境不需要配置Nginx 自动代理 API 请求
# ============================================
# API 后端地址(仅本地开发使用)
# - 本地开发http://127.0.0.1:3000
# - 连接远程 APIhttps://api.longde.cloud
VITE_API_BASE_URL=http://127.0.0.1:3000

9
.env.production Normal file
View 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
View File

@ -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",

View File

@ -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",

View File

@ -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 }) },
}

View File

@ -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') },

View File

@ -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'),

View File

@ -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') },
}

View File

@ -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) },
}

View File

@ -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') },

View File

@ -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') },

View File

@ -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') },

View File

@ -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 }),
}

View File

@ -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'

View File

@ -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}`) },
}

View File

@ -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}`),

View File

@ -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),
}

View File

@ -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`),
}

View File

@ -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'),
}

View File

@ -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 }),
}

View File

@ -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`) },

View File

@ -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') },

View File

@ -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),
}

View File

@ -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 },

View File

@ -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,
})

View File

@ -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) }

View File

@ -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,
})

View File

@ -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 })

View File

@ -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,
})

View File

@ -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>
)

View File

@ -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,
})

View File

@ -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,
})

View File

@ -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() })

View File

@ -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)

View File

@ -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 },

View File

@ -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)

View File

@ -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,
})

View File

@ -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
View File

@ -0,0 +1 @@
export { useUIStore } from './ui-store'

22
src/stores/ui-store.ts Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
/** API 后端地址(仅本地开发使用,生产通过 Nginx 代理) */
readonly VITE_API_BASE_URL?: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@ -15,6 +15,11 @@
"noEmit": true,
"jsx": "react-jsx",
/* Strict */
"strict": true,
"noImplicitAny": false,
"strictNullChecks": false,
/* Linting */
"baseUrl": ".",
"paths": { "@/*": ["src/*"] },