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-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.15.1",
|
"react-router-dom": "^7.15.1",
|
||||||
"react-syntax-highlighter": "^16.1.1",
|
"react-syntax-highlighter": "^16.1.1",
|
||||||
"remark-gfm": "^4.0.1"
|
"remark-gfm": "^4.0.1",
|
||||||
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@ -7075,6 +7076,35 @@
|
|||||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
"license": "0BSD"
|
"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": {
|
"node_modules/zwitch": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz",
|
"resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz",
|
||||||
|
|||||||
@ -23,7 +23,8 @@
|
|||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-router-dom": "^7.15.1",
|
"react-router-dom": "^7.15.1",
|
||||||
"react-syntax-highlighter": "^16.1.1",
|
"react-syntax-highlighter": "^16.1.1",
|
||||||
"remark-gfm": "^4.0.1"
|
"remark-gfm": "^4.0.1",
|
||||||
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import { api } from '@/lib/api-client'
|
import { api } from '@/lib/api-client'
|
||||||
|
import type { AiRoute } from '@/types'
|
||||||
|
|
||||||
|
export type CreateRouteDto = Omit<AiRoute, 'id' | 'createdAt'>
|
||||||
|
|
||||||
export const aiGatewayAPI = {
|
export const aiGatewayAPI = {
|
||||||
status() { return api.get('/admin-api/ai-gateway/status') },
|
status() { return api.get('/admin-api/ai-gateway/status') },
|
||||||
routes() { return api.get('/admin-api/ai-gateway/routes') },
|
routes() { return api.get('/admin-api/ai-gateway/routes') },
|
||||||
providers() { return api.get('/admin-api/ai-gateway/providers') },
|
providers() { return api.get('/admin-api/ai-gateway/providers') },
|
||||||
fallbackEvents() { return api.get('/admin-api/ai-gateway/fallback-events') },
|
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}`) },
|
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 }) },
|
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 { api } from '@/lib/api-client'
|
||||||
|
import type { BillingInfo } from '@/types'
|
||||||
|
|
||||||
export interface BillingInfo {
|
export type { BillingInfo } from '@/types'
|
||||||
name: string; model: string; balance: string; currency: string
|
|
||||||
status: 'ok' | 'unknown'; consoleUrl: string; note: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const billingAPI = {
|
export const billingAPI = {
|
||||||
get() { return api.get<{ providers: BillingInfo[] }>('/admin-api/billing') },
|
get() { return api.get<{ providers: BillingInfo[] }>('/admin-api/billing') },
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
import { api } from '@/lib/api-client'
|
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 = {
|
export const complianceAPI = {
|
||||||
privacyPolicies: () => api.get('/admin-api/compliance/privacy-policies'),
|
privacyPolicies: () => api.get('/admin-api/compliance/privacy-policies'),
|
||||||
createPrivacy: (data: any) => api.post('/admin-api/compliance/privacy-policies', data),
|
createPrivacy: (data: PrivacyDto) => api.post('/admin-api/compliance/privacy-policies', data),
|
||||||
updatePrivacy: (id: string, data: any) => api.patch(`/admin-api/compliance/privacy-policies/${id}`, data),
|
updatePrivacy: (id: string, data: Partial<PrivacyDto>) => api.patch(`/admin-api/compliance/privacy-policies/${id}`, data),
|
||||||
userAgreements: () => api.get('/admin-api/compliance/user-agreements'),
|
userAgreements: () => api.get('/admin-api/compliance/user-agreements'),
|
||||||
createAgreement: (data: any) => api.post('/admin-api/compliance/user-agreements', data),
|
createAgreement: (data: AgreementDto) => api.post('/admin-api/compliance/user-agreements', data),
|
||||||
updateAgreement: (id: string, data: any) => api.patch(`/admin-api/compliance/user-agreements/${id}`, data),
|
updateAgreement: (id: string, data: Partial<AgreementDto>) => api.patch(`/admin-api/compliance/user-agreements/${id}`, data),
|
||||||
filings: () => api.get('/admin-api/compliance/filings'),
|
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'),
|
deletionRequests: () => api.get('/admin-api/compliance/deletion-requests'),
|
||||||
approveDeletion: (id: string) => api.post(`/admin-api/compliance/deletion-requests/${id}/approve`),
|
approveDeletion: (id: string) => api.post(`/admin-api/compliance/deletion-requests/${id}/approve`),
|
||||||
exportRequests: () => api.get('/admin-api/compliance/export-requests'),
|
exportRequests: () => api.get('/admin-api/compliance/export-requests'),
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { api } from '@/lib/api-client'
|
import { api } from '@/lib/api-client'
|
||||||
|
import type { AppConfig, FeatureFlag, ConfigChangeLog } from '@/types'
|
||||||
|
|
||||||
export const configAPI = {
|
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 }) },
|
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 }) },
|
update(key: string, value: string) { return api.patch(`/admin-api/config/${key}`, { value }) },
|
||||||
remove(key: string) { return api.delete(`/admin-api/config/${key}`) },
|
remove(key: string) { return api.delete(`/admin-api/config/${key}`) },
|
||||||
toggleFlag(name: string, enabled: boolean) { return api.post(`/admin-api/config/flags/${name}`, { enabled }) },
|
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 }) },
|
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'
|
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 = {
|
export const contentSafetyAPI = {
|
||||||
words() { return api.get('/admin-api/content-safety/words') },
|
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}`) },
|
deleteWord(id: string) { return api.delete(`/admin-api/content-safety/words/${id}`) },
|
||||||
checks(params?: any) { return api.get('/admin-api/content-safety/checks', params) },
|
checks(params?: SafetyQueryParams) { return api.get('/admin-api/content-safety/checks', params) },
|
||||||
reports(params?: any) { return api.get('/admin-api/content-safety/reports', params) },
|
reports(params?: SafetyQueryParams) { return api.get('/admin-api/content-safety/reports', params) },
|
||||||
violations(params?: any) { return api.get('/admin-api/content-safety/violations', params) },
|
violations(params?: SafetyQueryParams) { 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) },
|
handleReport(id: string, data: HandleReportDto) { 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) },
|
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 { api } from '@/lib/api-client'
|
||||||
|
import type { Conversation, MessageRecord } from '@/types'
|
||||||
|
|
||||||
export interface Conversation { id: string; title: string; [key: string]: any }
|
export type { Conversation, MessageRecord } from '@/types'
|
||||||
export interface MessageRecord { id: string; content: string; role: string; [key: string]: any }
|
|
||||||
|
|
||||||
export const conversationsAPI = {
|
export const conversationsAPI = {
|
||||||
list() { return api.get<Conversation[]>('/admin-api/conversations') },
|
list() { return api.get<Conversation[]>('/admin-api/conversations') },
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { api } from '@/lib/api-client'
|
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 type { CostItem, CostSummary, CostByMonth } from '@/types'
|
||||||
export interface CostSummary { totalMonthly: number; totalYearly: number; totalOneTime: number; items: CostItem[]; byMonth: any[]; expiringSoon: any[] }
|
|
||||||
|
|
||||||
export const costsAPI = {
|
export const costsAPI = {
|
||||||
summary() { return api.get<CostSummary>('/admin-api/costs/summary') },
|
summary() { return api.get<CostSummary>('/admin-api/costs/summary') },
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { api } from '@/lib/api-client'
|
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 type { QueueInfo, FailedJob, JobDetail, QueueOverview } from '@/types'
|
||||||
export interface FailedJob { id: string; name: string; failedReason: string; [key: string]: any }
|
|
||||||
export interface JobDetail { id: string; data: any; [key: string]: any }
|
|
||||||
|
|
||||||
export const eventsQueueAPI = {
|
export const eventsQueueAPI = {
|
||||||
overview() { return api.get<{ queues: QueueInfo[] }>('/admin-api/events') },
|
overview() { return api.get<{ queues: QueueInfo[] }>('/admin-api/events') },
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { api } from '@/lib/api-client'
|
import { api } from '@/lib/api-client'
|
||||||
|
import type { ImportJob, ImportJobDetail } from '@/types'
|
||||||
|
|
||||||
export const importsAdminAPI = {
|
export const importsAdminAPI = {
|
||||||
list: (params: string) => api.get(`/admin-api/imports?${params}`),
|
list: (params: string) => api.get<{ items: ImportJob[] }>(`/admin-api/imports?${params}`),
|
||||||
detail: (id: string) => api.get(`/admin-api/imports/${id}`),
|
detail: (id: string) => api.get<ImportJobDetail>(`/admin-api/imports/${id}`),
|
||||||
retry: (id: string) => api.post(`/admin-api/imports/${id}/retry`),
|
retry: (id: string) => api.post(`/admin-api/imports/${id}/retry`),
|
||||||
batchRetry: (ids: string[]) => api.post('/admin-api/imports/batch-retry', { ids }),
|
batchRetry: (ids: string[]) => api.post('/admin-api/imports/batch-retry', { ids }),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,16 +2,22 @@ export { authAPI } from './auth'
|
|||||||
export { usersAPI } from './users'
|
export { usersAPI } from './users'
|
||||||
export { dashboardAPI } from './dashboard'
|
export { dashboardAPI } from './dashboard'
|
||||||
export { auditAPI } from './audit'
|
export { auditAPI } from './audit'
|
||||||
export { billingAPI, type BillingInfo } from './billing'
|
export { billingAPI } from './billing'
|
||||||
export { costsAPI, type CostItem, type CostSummary } from './costs'
|
export { costsAPI } from './costs'
|
||||||
export { configAPI } from './config'
|
export { configAPI } from './config'
|
||||||
export { conversationsAPI, type Conversation } from './conversations'
|
export { conversationsAPI } from './conversations'
|
||||||
export { eventsQueueAPI } from './events-queue'
|
export { eventsQueueAPI } from './events-queue'
|
||||||
export { knowledgeAPI } from './knowledge'
|
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 { learningAPI } from './learning'
|
||||||
export { aiAPI } from './ai'
|
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 { aiGatewayAPI } from './ai-gateway'
|
||||||
export { cacheAPI } from './cache'
|
export { cacheAPI } from './cache'
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
import { api } from '@/lib/api-client'
|
import { api } from '@/lib/api-client'
|
||||||
|
import type { KnowledgeBase, KnowledgeSource, PaginatedResult, SourceReference, KnowledgeCandidate, KnowledgeChunk } from '@/types'
|
||||||
|
|
||||||
export const knowledgeAPI = {
|
export const knowledgeAPI = {
|
||||||
list(page = 1, limit = 20) {
|
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}`) },
|
remove(id: string) { return api.delete<void>(`/admin-api/knowledge-bases/${id}`) },
|
||||||
candidates() { return api.get('/admin-api/knowledge-bases/candidates?limit=50') },
|
candidates() { return api.get<KnowledgeCandidate[]>('/admin-api/knowledge-bases/candidates?limit=50') },
|
||||||
chunks(sourceId: string) { return api.get(`/admin-api/knowledge-bases/chunks?sourceId=${sourceId}&limit=50`) },
|
chunks(sourceId: string) { return api.get<KnowledgeChunk[]>(`/admin-api/knowledge-bases/chunks?sourceId=${sourceId}&limit=50`) },
|
||||||
sources(kbId: string) { return api.get(`/admin-api/knowledge-bases/${kbId}/sources`) },
|
sources(kbId: string) { return api.get<KnowledgeSource[]>(`/admin-api/knowledge-bases/${kbId}/sources`) },
|
||||||
sourceReferences(sourceId: string) { return api.get(`/admin-api/knowledge-bases/sources/${sourceId}/references`) },
|
sourceReferences(sourceId: string) { return api.get<SourceReference[]>(`/admin-api/knowledge-bases/sources/${sourceId}/references`) },
|
||||||
allSources(params: string) { return api.get(`/admin-api/knowledge-bases/all-sources?${params}`) },
|
allSources(params: string) { return api.get<PaginatedResult<KnowledgeSource>>(`/admin-api/knowledge-bases/all-sources?${params}`) },
|
||||||
search(params: string) { return api.get(`/admin-api/knowledge-bases?${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 { 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'
|
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 ''
|
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}` : ''
|
return p ? `?${p}` : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginatedResponse<T> { items: T[]; total: number }
|
|
||||||
|
|
||||||
export const learningAPI = {
|
export const learningAPI = {
|
||||||
// dashboard
|
// 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`),
|
getKnowledgeBases: () => api.get<Array<{ id: string; name: string }>>(`${BASE}/knowledge-bases`),
|
||||||
|
|
||||||
// reading events
|
// reading events
|
||||||
getReadingEvents: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/reading-events${qs(params)}`),
|
getReadingEvents: (params?: QueryParams) => api.get<PaginatedResult<ReadingEvent>>(`${BASE}/reading-events${qs(params)}`),
|
||||||
getReadingEventDetail: (id: string) => api.get<any>(`${BASE}/reading-events/${id}`),
|
getReadingEventDetail: (id: string) => api.get<ReadingEvent>(`${BASE}/reading-events/${id}`),
|
||||||
getFailedEvents: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/reading-events/failed${qs(params)}`),
|
getFailedEvents: (params?: QueryParams) => api.get<PaginatedResult<ReadingEvent>>(`${BASE}/reading-events/failed${qs(params)}`),
|
||||||
|
|
||||||
// sessions
|
// sessions
|
||||||
getSessions: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/sessions${qs(params)}`),
|
getSessions: (params?: QueryParams) => api.get<PaginatedResult<LearningSession>>(`${BASE}/sessions${qs(params)}`),
|
||||||
getSessionDetail: (id: string) => api.get<any>(`${BASE}/sessions/${id}`),
|
getSessionDetail: (id: string) => api.get<LearningSession>(`${BASE}/sessions/${id}`),
|
||||||
deleteSessions: (ids: string[]) => api.post<{ deleted: number; message: string }>(`${BASE}/sessions/batch-delete`, { ids }),
|
deleteSessions: (ids: string[]) => api.post<{ deleted: number; message: string }>(`${BASE}/sessions/batch-delete`, { ids }),
|
||||||
|
|
||||||
// progress & activities
|
// progress & activities
|
||||||
getProgress: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/progress${qs(params)}`),
|
getProgress: (params?: QueryParams) => api.get<PaginatedResult<MaterialReadingProgress>>(`${BASE}/progress${qs(params)}`),
|
||||||
getDailyActivities: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/daily-activities${qs(params)}`),
|
getDailyActivities: (params?: QueryParams) => api.get<PaginatedResult<DailyLearningActivity>>(`${BASE}/daily-activities${qs(params)}`),
|
||||||
getRecords: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/records${qs(params)}`),
|
getRecords: (params?: QueryParams) => api.get<PaginatedResult<LearningRecord>>(`${BASE}/records${qs(params)}`),
|
||||||
|
|
||||||
// user/material
|
// user/material
|
||||||
getUserTimeline: (userId: string) => api.get(`${BASE}/user-timeline?userId=${encodeURIComponent(userId)}`),
|
getUserTimeline: (userId: string) => api.get<UserTimeline>(`${BASE}/user-timeline?userId=${encodeURIComponent(userId)}`),
|
||||||
getUserDiagnose: (userId: string) => api.get(`${BASE}/user-diagnose?userId=${encodeURIComponent(userId)}`),
|
getUserDiagnose: (userId: string) => api.get<UserDiagnose>(`${BASE}/user-diagnose?userId=${encodeURIComponent(userId)}`),
|
||||||
getMaterialDiagnose: (materialId: string) => api.get(`${BASE}/material-diagnose?materialId=${encodeURIComponent(materialId)}`),
|
getMaterialDiagnose: (materialId: string) => api.get<MaterialDiagnose>(`${BASE}/material-diagnose?materialId=${encodeURIComponent(materialId)}`),
|
||||||
|
|
||||||
// misc
|
// misc
|
||||||
getAnomalies: () => api.get<any>(`${BASE}/anomalies`),
|
getAnomalies: () => api.get<AnomalyData>(`${BASE}/anomalies`),
|
||||||
getTemporaryMaterials: (params?: Record<string, any>) => api.get<PaginatedResponse<unknown>>(`${BASE}/temporary-materials${qs(params)}`),
|
getTemporaryMaterials: (params?: QueryParams) => api.get<PaginatedResult<unknown>>(`${BASE}/temporary-materials${qs(params)}`),
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
recalculate: () => api.post(`${BASE}/recalculate`),
|
recalculate: () => api.post(`${BASE}/recalculate`),
|
||||||
replayEvent: (eventId: string) => api.post<{ success: boolean; message: string }>(`${BASE}/reading-events/${eventId}/replay`),
|
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)}`),
|
exportData: (type: string) => api.get(`${BASE}/export?type=${encodeURIComponent(type)}`),
|
||||||
getSessionsRaw: (qs: string) => api.get(`${BASE}/sessions?${qs}`),
|
getSessionsRaw: (qs: string) => api.get(`${BASE}/sessions?${qs}`),
|
||||||
getAnalysis: (qs: string) => api.get(`${BASE}/analysis?${qs}`),
|
getAnalysis: (qs: string) => api.get(`${BASE}/analysis?${qs}`),
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { api } from '@/lib/api-client'
|
import { api } from '@/lib/api-client'
|
||||||
|
|
||||||
|
export interface UpdateMembershipDto { userId: string; planId: string; status?: string; expiresAt?: string }
|
||||||
|
|
||||||
export const membersAPI = {
|
export const membersAPI = {
|
||||||
list: () => api.get('/admin-api/users'),
|
list: () => api.get('/admin-api/users'),
|
||||||
deletionRequests: () => api.get('/admin-api/users/deletion-requests'),
|
deletionRequests: () => api.get('/admin-api/users/deletion-requests'),
|
||||||
processDeletion: (id: string, action: string) => api.post(`/admin-api/users/deletion-requests/${id}/${action}`),
|
processDeletion: (id: string, action: string) => api.post(`/admin-api/users/deletion-requests/${id}/${action}`),
|
||||||
memberships: () => api.get('/admin-api/users/memberships'),
|
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 { 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 = {
|
export const notificationsAPI = {
|
||||||
templates: () => api.get('/admin-api/notifications/templates'),
|
templates: () => api.get('/admin-api/notifications/templates'),
|
||||||
createTemplate: (data: any) => api.post('/admin-api/notifications/templates', data),
|
createTemplate: (data: CreateTemplateDto) => api.post('/admin-api/notifications/templates', data),
|
||||||
updateTemplate: (id: string, data: any) => api.patch(`/admin-api/notifications/templates/${id}`, data),
|
updateTemplate: (id: string, data: UpdateTemplateDto) => api.patch(`/admin-api/notifications/templates/${id}`, data),
|
||||||
deleteTemplate: (id: string) => api.delete(`/admin-api/notifications/templates/${id}`),
|
deleteTemplate: (id: string) => api.delete(`/admin-api/notifications/templates/${id}`),
|
||||||
sendLog: () => api.get('/admin-api/notifications/send-log?limit=50'),
|
sendLog: () => api.get('/admin-api/notifications/send-log?limit=50'),
|
||||||
adminNotifications: () => api.get('/admin-api/admin-notifications'),
|
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`),
|
markRead: (id: string) => api.post(`/admin-api/admin-notifications/${id}/read`),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { api } from '@/lib/api-client'
|
import { api } from '@/lib/api-client'
|
||||||
|
import type { MembershipPlan } from '@/types'
|
||||||
|
|
||||||
|
export type CreatePlanDto = Omit<MembershipPlan, 'id' | 'createdAt'>
|
||||||
|
|
||||||
export const quotaAPI = {
|
export const quotaAPI = {
|
||||||
plans: () => api.get('/admin-api/quota/plans'),
|
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'),
|
memberships: () => api.get('/admin-api/quota/memberships'),
|
||||||
costs: () => api.get('/admin-api/quota/costs'),
|
costs: () => api.get('/admin-api/quota/costs'),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
import { api } from '@/lib/api-client'
|
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 = {
|
export const releaseAPI = {
|
||||||
changelogs: () => api.get('/admin-api/release/changelogs'),
|
changelogs: () => api.get('/admin-api/release/changelogs'),
|
||||||
decisions: () => api.get('/admin-api/release/decisions'),
|
decisions: () => api.get('/admin-api/release/decisions'),
|
||||||
checklists: (ver: string) => api.get(`/admin-api/release/checklists/${ver}`),
|
checklists: (ver: string) => api.get<ReleaseChecklistItem[]>(`/admin-api/release/checklists/${ver}`),
|
||||||
create: (type: string, data: any) => api.post(`/admin-api/release/${type}`, data),
|
create: (type: string, data: CreateChangelogDto | CreateDecisionDto) => api.post(`/admin-api/release/${type}`, data),
|
||||||
update: (type: string, id: string, data: any) => api.patch(`/admin-api/release/${type}/${id}`, 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}`),
|
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 }),
|
toggleChecklist: (id: string, checked: boolean) => api.patch(`/admin-api/release/checklists/${id}`, { checked }),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import { api } from '@/lib/api-client'
|
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 = {
|
export const secretsAPI = {
|
||||||
list() { return api.get('/admin-api/secrets') },
|
list() { return api.get('/admin-api/secrets') },
|
||||||
logs() { return api.get('/admin-api/secrets/logs') },
|
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}`) },
|
delete(id: string) { return api.delete(`/admin-api/secrets/${id}`) },
|
||||||
vendorBills() { return api.get('/admin-api/vendor/bills') },
|
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}`) },
|
deleteVendorBill(id: string) { return api.delete(`/admin-api/vendor/bills/${id}`) },
|
||||||
rotateVendorSecret(id: string) { return api.post(`/admin-api/vendor/secrets/${id}/rotate`) },
|
rotateVendorSecret(id: string) { return api.post(`/admin-api/vendor/secrets/${id}/rotate`) },
|
||||||
revokeVendorSecret(id: string) { return api.post(`/admin-api/vendor/secrets/${id}/revoke`) },
|
revokeVendorSecret(id: string) { return api.post(`/admin-api/vendor/secrets/${id}/revoke`) },
|
||||||
|
|||||||
@ -1,18 +1,7 @@
|
|||||||
import { api } from '@/lib/api-client'
|
import { api } from '@/lib/api-client'
|
||||||
|
import type { ServerInfo, ServerHealth, WorkerInfo } from '@/types'
|
||||||
|
|
||||||
export interface ServerInfo {
|
export type { ServerInfo, ProcessInfo, HealthService, ServerHealth, WorkerInfo, ServerMetrics } from '@/types'
|
||||||
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 const serversAPI = {
|
export const serversAPI = {
|
||||||
metrics() { return api.get<{ servers: ServerInfo[] }>('/admin-api/servers') },
|
metrics() { return api.get<{ servers: ServerInfo[] }>('/admin-api/servers') },
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { api } from '@/lib/api-client'
|
import { api } from '@/lib/api-client'
|
||||||
|
import type { VectorDebugQuery } from '@/types'
|
||||||
|
|
||||||
export const vectorAPI = {
|
export const vectorAPI = {
|
||||||
count: () => api.get('/admin-api/vector/count'),
|
count: () => api.get('/admin-api/vector/count'),
|
||||||
collection: () => api.get('/admin-api/vector/collection'),
|
collection: () => api.get('/admin-api/vector/collection'),
|
||||||
reindex: () => api.post('/admin-api/vector/reindex'),
|
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 { Dropdown, Avatar, Tag, Space, message } from 'antd'
|
||||||
import { LogoutOutlined, UserOutlined } from '@ant-design/icons'
|
import { LogoutOutlined, UserOutlined } from '@ant-design/icons'
|
||||||
import { useAuth } from '@/contexts/AuthContext'
|
import { useAuth } from '@/contexts/AuthContext'
|
||||||
|
import { useUIStore } from '@/stores'
|
||||||
import { filterMenuByRole, adminMenuItems } from '@/config/menu'
|
import { filterMenuByRole, adminMenuItems } from '@/config/menu'
|
||||||
import { ADMIN_ROLE_LABELS, ADMIN_ROLE_COLORS } from '@/constants/roles'
|
import { ADMIN_ROLE_LABELS, ADMIN_ROLE_COLORS } from '@/constants/roles'
|
||||||
import type { AdminRole } from '@/types/admin'
|
import type { AdminRole } from '@/types/admin'
|
||||||
@ -60,6 +61,9 @@ export default function AdminLayout() {
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { adminUser, logout, hasPermission } = useAuth()
|
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 currentRole = adminUser?.role as AdminRole | undefined
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
@ -135,6 +139,8 @@ export default function AdminLayout() {
|
|||||||
</Space>
|
</Space>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
collapsed={sidebarCollapsed}
|
||||||
|
onCollapse={setSidebarCollapsed}
|
||||||
siderWidth={220}
|
siderWidth={220}
|
||||||
token={{
|
token={{
|
||||||
header: { heightLayoutHeader: 48 },
|
header: { heightLayoutHeader: 48 },
|
||||||
|
|||||||
@ -15,25 +15,25 @@ export default function AiGatewayPage() {
|
|||||||
|
|
||||||
const { data: status } = useQuery({
|
const { data: status } = useQuery({
|
||||||
queryKey: ['ai-gateway', 'status'],
|
queryKey: ['ai-gateway', 'status'],
|
||||||
queryFn: (): Promise<any> => aiGatewayAPI.status(),
|
queryFn: () =>aiGatewayAPI.status(),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: routes, refetch: refetchRoutes } = useQuery({
|
const { data: routes, refetch: refetchRoutes } = useQuery({
|
||||||
queryKey: ['ai-gateway', 'routes'],
|
queryKey: ['ai-gateway', 'routes'],
|
||||||
queryFn: (): Promise<any> => aiGatewayAPI.routes(),
|
queryFn: () =>aiGatewayAPI.routes(),
|
||||||
enabled: activeTab === 'routes',
|
enabled: activeTab === 'routes',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: providers, refetch: refetchProviders } = useQuery({
|
const { data: providers, refetch: refetchProviders } = useQuery({
|
||||||
queryKey: ['ai-gateway', 'providers'],
|
queryKey: ['ai-gateway', 'providers'],
|
||||||
queryFn: (): Promise<any> => aiGatewayAPI.providers(),
|
queryFn: () =>aiGatewayAPI.providers(),
|
||||||
enabled: activeTab === 'providers',
|
enabled: activeTab === 'providers',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: fallbackEvents } = useQuery({
|
const { data: fallbackEvents } = useQuery({
|
||||||
queryKey: ['ai-gateway', 'fallback-events'],
|
queryKey: ['ai-gateway', 'fallback-events'],
|
||||||
queryFn: (): Promise<any> => aiGatewayAPI.fallbackEvents(),
|
queryFn: () =>aiGatewayAPI.fallbackEvents(),
|
||||||
enabled: activeTab === 'fallback',
|
enabled: activeTab === 'fallback',
|
||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -25,8 +25,8 @@ function BillingContent() {
|
|||||||
|
|
||||||
const { data: billing } = useQuery({ queryKey: ['billing'], queryFn: billingAPI.get, staleTime: 60_000 })
|
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: 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: aiReport } = useQuery({ queryKey: ['costs', 'report'], queryFn: () => costsAPI.report(), staleTime: 30_000 })
|
||||||
const { data: topUsers } = useQuery({ queryKey: ['costs', 'top-users'], queryFn: (): Promise<any> => costsAPI.topUsers(), 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) }
|
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({
|
const { data: sessions, isLoading } = useQuery({
|
||||||
queryKey: ['chat-logs', 'sessions'],
|
queryKey: ['chat-logs', 'sessions'],
|
||||||
queryFn: (): Promise<any> => chatLogsAPI.sessions(),
|
queryFn: () => chatLogsAPI.sessions(),
|
||||||
staleTime: 10_000,
|
staleTime: 10_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: messages } = useQuery({
|
const { data: messages } = useQuery({
|
||||||
queryKey: ['chat-logs', 'messages', selectedSession?.id],
|
queryKey: ['chat-logs', 'messages', selectedSession?.id],
|
||||||
queryFn: (): Promise<any> => chatLogsAPI.sessionMessages(selectedSession?.id || ''),
|
queryFn: () => chatLogsAPI.sessionMessages(selectedSession?.id || ''),
|
||||||
enabled: !!selectedSession?.id,
|
enabled: !!selectedSession?.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -15,10 +15,10 @@ function CSPage() {
|
|||||||
const [category, setCategory] = useState('general')
|
const [category, setCategory] = useState('general')
|
||||||
const [risk, setRisk] = useState('medium')
|
const [risk, setRisk] = useState('medium')
|
||||||
|
|
||||||
const { data: words } = useQuery({ queryKey: ['safety', 'words'], queryFn: (): Promise<any> => contentSafetyAPI.words() })
|
const { data: words } = useQuery({ queryKey: ['safety', 'words'], queryFn: () => contentSafetyAPI.words() })
|
||||||
const { data: checks } = useQuery({ queryKey: ['safety', 'checks'], queryFn: (): Promise<any> => contentSafetyAPI.checks() })
|
const { data: checks } = useQuery({ queryKey: ['safety', 'checks'], queryFn: () => contentSafetyAPI.checks() })
|
||||||
const { data: reports } = useQuery({ queryKey: ['safety', 'reports'], queryFn: (): Promise<any> => contentSafetyAPI.reports() })
|
const { data: reports } = useQuery({ queryKey: ['safety', 'reports'], queryFn: () => contentSafetyAPI.reports() })
|
||||||
const { data: violations } = useQuery({ queryKey: ['safety', 'violations'], queryFn: (): Promise<any> => contentSafetyAPI.violations() })
|
const { data: violations } = useQuery({ queryKey: ['safety', 'violations'], queryFn: () => contentSafetyAPI.violations() })
|
||||||
|
|
||||||
const addWord = async () => {
|
const addWord = async () => {
|
||||||
await contentSafetyAPI.addWord({ word: newWord, category, riskLevel: risk })
|
await contentSafetyAPI.addWord({ word: newWord, category, riskLevel: risk })
|
||||||
|
|||||||
@ -14,26 +14,26 @@ function EventsPage() {
|
|||||||
|
|
||||||
const { data: overview } = useQuery({
|
const { data: overview } = useQuery({
|
||||||
queryKey: ['events', 'overview'],
|
queryKey: ['events', 'overview'],
|
||||||
queryFn: (): Promise<any> => eventsQueueAPI.overview(),
|
queryFn: () =>eventsQueueAPI.overview(),
|
||||||
staleTime: 10_000,
|
staleTime: 10_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: stats } = useQuery({
|
const { data: stats } = useQuery({
|
||||||
queryKey: ['events', 'stats'],
|
queryKey: ['events', 'stats'],
|
||||||
queryFn: (): Promise<any> => eventsQueueAPI.stats(),
|
queryFn: () =>eventsQueueAPI.stats(),
|
||||||
enabled: activeTab === 'stats',
|
enabled: activeTab === 'stats',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: workers } = useQuery({
|
const { data: workers } = useQuery({
|
||||||
queryKey: ['events', 'workers'],
|
queryKey: ['events', 'workers'],
|
||||||
queryFn: (): Promise<any> => eventsQueueAPI.workers(),
|
queryFn: () =>eventsQueueAPI.workers(),
|
||||||
enabled: activeTab === 'stats',
|
enabled: activeTab === 'stats',
|
||||||
refetchInterval: 15_000,
|
refetchInterval: 15_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: failed } = useQuery({
|
const { data: failed } = useQuery({
|
||||||
queryKey: ['events', 'failed', selectedQueue],
|
queryKey: ['events', 'failed', selectedQueue],
|
||||||
queryFn: (): Promise<any> => selectedQueue ? eventsQueueAPI.failedJobs(selectedQueue) : Promise.resolve(null),
|
queryFn: () =>selectedQueue ? eventsQueueAPI.failedJobs(selectedQueue) : Promise.resolve(null),
|
||||||
enabled: !!selectedQueue,
|
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 { ReloadOutlined, RetweetOutlined, ImportOutlined, EyeOutlined } from '@ant-design/icons'
|
||||||
import { importsAdminAPI, knowledgeAPI } from '@/api'
|
import { importsAdminAPI, knowledgeAPI } from '@/api'
|
||||||
import { importStatusLabels, importStepLabels } from '@/constants/labels'
|
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'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
const { Title, Text } = Typography
|
const { Title, Text } = Typography
|
||||||
|
|
||||||
// 格式化知识库选项:用户 - 名称 (id前8位)
|
function formatKbLabel(kb: KnowledgeBase) {
|
||||||
function formatKbLabel(kb: any) {
|
|
||||||
const user = kb.user?.email || kb.user?.nickname || kb.userId || '?'
|
const user = kb.user?.email || kb.user?.nickname || kb.userId || '?'
|
||||||
const name = kb.title || '未命名'
|
const name = kb.title || '未命名'
|
||||||
const shortId = kb.id?.slice(0, 8) || kb.id
|
const shortId = kb.id?.slice(0, 8) || kb.id
|
||||||
@ -20,7 +22,7 @@ export default function ImportMonitorPage() {
|
|||||||
const { message } = App.useApp()
|
const { message } = App.useApp()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
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 [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
|
||||||
const [kbFilter, setKbFilter] = useState<string | undefined>(undefined)
|
const [kbFilter, setKbFilter] = useState<string | undefined>(undefined)
|
||||||
const [retryAllOpen, setRetryAllOpen] = useState(false)
|
const [retryAllOpen, setRetryAllOpen] = useState(false)
|
||||||
@ -28,7 +30,6 @@ export default function ImportMonitorPage() {
|
|||||||
const [sortBy, setSortBy] = useState<string>('createdAt')
|
const [sortBy, setSortBy] = useState<string>('createdAt')
|
||||||
const [sortOrder, setSortOrder] = useState<string>('descend')
|
const [sortOrder, setSortOrder] = useState<string>('descend')
|
||||||
|
|
||||||
// 知识库远程搜索(防抖)
|
|
||||||
const [kbSearch, setKbSearch] = useState('')
|
const [kbSearch, setKbSearch] = useState('')
|
||||||
const kbSearchTimer = useRef<ReturnType<typeof setTimeout>>(undefined)
|
const kbSearchTimer = useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||||
|
|
||||||
@ -37,10 +38,9 @@ export default function ImportMonitorPage() {
|
|||||||
kbSearchTimer.current = setTimeout(() => setKbSearch(val), 300)
|
kbSearchTimer.current = setTimeout(() => setKbSearch(val), 300)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 知识库列表
|
|
||||||
const { data: kbData, isLoading: kbLoading } = useQuery({
|
const { data: kbData, isLoading: kbLoading } = useQuery({
|
||||||
queryKey: ['admin-knowledge-bases', kbSearch],
|
queryKey: ['admin-knowledge-bases', kbSearch],
|
||||||
queryFn: (): Promise<any> => {
|
queryFn: () => {
|
||||||
const params = new URLSearchParams({ limit: '50' })
|
const params = new URLSearchParams({ limit: '50' })
|
||||||
if (kbSearch) params.set('search', kbSearch)
|
if (kbSearch) params.set('search', kbSearch)
|
||||||
return knowledgeAPI.search(params.toString())
|
return knowledgeAPI.search(params.toString())
|
||||||
@ -56,17 +56,16 @@ export default function ImportMonitorPage() {
|
|||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['admin-imports', 'list', kbFilter, sortBy, sortOrder],
|
queryKey: ['admin-imports', 'list', kbFilter, sortBy, sortOrder],
|
||||||
queryFn: (): Promise<any> => importsAdminAPI.list(queryParams.toString()),
|
queryFn: () => importsAdminAPI.list(queryParams.toString()),
|
||||||
})
|
})
|
||||||
|
|
||||||
// 默认过滤已删除 source 的记录
|
|
||||||
const tableData = showDeleted
|
const tableData = showDeleted
|
||||||
? (data?.items || [])
|
? (data?.items || [])
|
||||||
: (data?.items || []).filter((r: any) => !r.sourceDeleted)
|
: (data?.items || []).filter((r: ImportJob) => !r.sourceDeleted)
|
||||||
|
|
||||||
const { data: detail } = useQuery({
|
const { data: detail } = useQuery({
|
||||||
queryKey: ['admin-import-detail', selectedJob?.id],
|
queryKey: ['admin-import-detail', selectedJob?.id],
|
||||||
queryFn: (): Promise<any> => importsAdminAPI.detail(selectedJob?.id || ''),
|
queryFn: () => importsAdminAPI.detail(selectedJob?.id || ''),
|
||||||
enabled: !!selectedJob?.id,
|
enabled: !!selectedJob?.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -83,17 +82,17 @@ export default function ImportMonitorPage() {
|
|||||||
await importsAdminAPI.retry(id)
|
await importsAdminAPI.retry(id)
|
||||||
message.success('已重新入队,等待 Worker 处理')
|
message.success('已重新入队,等待 Worker 处理')
|
||||||
refreshList()
|
refreshList()
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
const msg = err?.response?.data?.message || err?.message || '重试失败'
|
const e = err as { response?: { data?: { message?: string } }; message?: string }
|
||||||
message.error(`重试失败: ${msg}`)
|
message.error(`重试失败: ${e?.response?.data?.message || e?.message || '未知错误'}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBatchRetry = async () => {
|
const handleBatchRetry = async () => {
|
||||||
try {
|
try {
|
||||||
const ids = selectedRowKeys as string[]
|
const ids = selectedRowKeys as string[]
|
||||||
const res = await importsAdminAPI.batchRetry(ids as string[])
|
const res = await importsAdminAPI.batchRetry(ids)
|
||||||
const { updated, skipped, skippedCount } = res
|
const { updated, skipped, skippedCount } = res as { updated: number; skipped: string[]; skippedCount: number }
|
||||||
let msg = `已重新入队 ${updated} 个任务`
|
let msg = `已重新入队 ${updated} 个任务`
|
||||||
if (skippedCount > 0) {
|
if (skippedCount > 0) {
|
||||||
msg += `,${skippedCount} 个跳过(源已删除: ${skipped.slice(0, 3).join(', ')}${skipped.length > 3 ? '...' : ''})`
|
msg += `,${skippedCount} 个跳过(源已删除: ${skipped.slice(0, 3).join(', ')}${skipped.length > 3 ? '...' : ''})`
|
||||||
@ -101,9 +100,9 @@ export default function ImportMonitorPage() {
|
|||||||
message.success(msg)
|
message.success(msg)
|
||||||
setSelectedRowKeys([])
|
setSelectedRowKeys([])
|
||||||
refreshList()
|
refreshList()
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
const msg = err?.response?.data?.message || err?.message || '批量重试失败'
|
const e = err as { response?: { data?: { message?: string } }; message?: string }
|
||||||
message.error(msg)
|
message.error(e?.response?.data?.message || e?.message || '批量重试失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,14 +110,14 @@ export default function ImportMonitorPage() {
|
|||||||
try {
|
try {
|
||||||
const allItems = data?.items || []
|
const allItems = data?.items || []
|
||||||
const retryableIds = allItems
|
const retryableIds = allItems
|
||||||
.filter((r: any) => r.status?.startsWith('FAILED') || r.status === 'CLAIMED' || r.status === 'QUEUED')
|
.filter((r: ImportJob) => r.status?.startsWith('FAILED') || r.status === 'CLAIMED' || r.status === 'QUEUED')
|
||||||
.map((r: any) => r.id)
|
.map((r: ImportJob) => r.id)
|
||||||
if (retryableIds.length === 0) {
|
if (retryableIds.length === 0) {
|
||||||
message.warning('没有可重新解析的任务')
|
message.warning('没有可重新解析的任务')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const res = await importsAdminAPI.batchRetry(retryableIds)
|
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} 个任务`
|
let msg = `已重新入队 ${updated} 个任务`
|
||||||
if (skippedCount > 0) {
|
if (skippedCount > 0) {
|
||||||
msg += `,${skippedCount} 个跳过(源已删除: ${skipped.slice(0, 3).join(', ')}${skipped.length > 3 ? '...' : ''})`
|
msg += `,${skippedCount} 个跳过(源已删除: ${skipped.slice(0, 3).join(', ')}${skipped.length > 3 ? '...' : ''})`
|
||||||
@ -127,58 +126,40 @@ export default function ImportMonitorPage() {
|
|||||||
setSelectedRowKeys([])
|
setSelectedRowKeys([])
|
||||||
setRetryAllOpen(false)
|
setRetryAllOpen(false)
|
||||||
refreshList()
|
refreshList()
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
const msg = err?.response?.data?.message || err?.message || '全部重试失败'
|
const e = err as { response?: { data?: { message?: string } }; message?: string }
|
||||||
message.error(msg)
|
message.error(e?.response?.data?.message || e?.message || '全部重试失败')
|
||||||
setRetryAllOpen(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColor: Record<string, string> = {
|
const statusColor: Record<string, string> = {
|
||||||
QUEUED: 'blue',
|
COMPLETED: 'green', QUEUED: 'default', CLAIMED: 'blue',
|
||||||
CLAIMED: 'cyan',
|
DOWNLOADING: 'processing', PARSING: 'processing', CHUNKING: 'processing',
|
||||||
DOWNLOADING: 'processing',
|
EMBEDDING: 'processing', INDEXING: 'processing', GENERATING_CANDIDATES: 'processing',
|
||||||
PARSING: 'processing',
|
FAILED: 'red', FAILED_RETRYABLE: 'orange', CANCELLED: 'default',
|
||||||
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',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '名称', dataIndex: 'sourceName', width: 180, ellipsis: true, sorter: true, sortOrder: sortBy === 'sourceName' ? (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: any) => (
|
render: (v: string, r: ImportJob) => (
|
||||||
<Space size={4}>
|
<a onClick={() => { setSelectedJob(r); setDrawerOpen(true) }} style={{ fontSize: 12 }}>{v || '—'}</a>
|
||||||
<Text ellipsis style={{ maxWidth: 140 }}>{v}</Text>
|
) },
|
||||||
{r.sourceDeleted && <Tag color="error" style={{ fontSize: 10 }}>源已删</Tag>}
|
{ 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) },
|
||||||
</Space>
|
{ 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: '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: 'errorMessage', width: 180, ellipsis: true, render: (v: string) => v ? <Text type="danger" style={{ fontSize: 12 }}>{v}</Text> : '-' },
|
{ 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: '时间', 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,
|
||||||
title: '操作', width: 100,
|
render: (_: unknown, r: ImportJob) => (
|
||||||
render: (_: any, r: any) => (
|
<Space size={0}>
|
||||||
<Space size="small">
|
|
||||||
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => { setSelectedJob(r); setDrawerOpen(true) }}>详情</Button>
|
<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>
|
</Space>
|
||||||
),
|
) },
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const stepCols = [
|
const stepColumns = [
|
||||||
{ title: '步骤', dataIndex: 'step', width: 120, render: (s: string) => importStepLabels[s] || importStatusLabels[s] || s },
|
{ 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: '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 },
|
{ 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') : '-' },
|
{ 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,
|
value: kb.id,
|
||||||
label: formatKbLabel(kb),
|
label: formatKbLabel(kb),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ padding: '0 0 24px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||||
<Title level={5} style={{ margin: 0 }}><ImportOutlined /> 文档导入</Title>
|
<Title level={5} style={{ margin: 0 }}><ImportOutlined /> 文档导入监控</Title>
|
||||||
<Space>
|
<Space>
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['admin-imports', 'list'] })}>刷新</Button>
|
<Button icon={<ReloadOutlined />} onClick={refreshList} size="small">刷新</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
<Row gutter={[8, 8]} style={{ marginBottom: 16 }}>
|
||||||
<Row style={{ marginBottom: 12 }} gutter={12}>
|
|
||||||
<Col>
|
<Col>
|
||||||
<Select
|
<Select allowClear showSearch placeholder="知识库" value={kbFilter} onChange={setKbFilter} onSearch={handleKbSearch} filterOption={false} options={kbOptions} loading={kbLoading} style={{ width: 280 }} notFoundContent={kbSearch ? '未找到' : '输入关键词搜索'} />
|
||||||
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>
|
|
||||||
</Col>
|
</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>
|
</Row>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
dataSource={tableData}
|
|
||||||
columns={columns}
|
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
columns={columns}
|
||||||
|
dataSource={tableData}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
pagination={{ pageSize: 30 }}
|
rowSelection={{ selectedRowKeys, onChange: keys => setSelectedRowKeys(keys) }}
|
||||||
|
pagination={false}
|
||||||
size="small"
|
size="small"
|
||||||
rowSelection={{
|
onChange={(_pagination: TablePaginationConfig, _filters: Record<string, FilterValue>, sorter: SorterResult<ImportJob> | SorterResult<ImportJob>[]) => {
|
||||||
selectedRowKeys,
|
const s = Array.isArray(sorter) ? sorter[0] : sorter
|
||||||
onChange: (keys) => setSelectedRowKeys(keys),
|
if (s.columnKey) setSortBy(String(s.columnKey))
|
||||||
}}
|
setSortOrder(s.order || 'descend')
|
||||||
onChange={(_pagination, _filters, sorter: any) => {
|
|
||||||
if (sorter.order) {
|
|
||||||
setSortBy(sorter.field || 'createdAt')
|
|
||||||
setSortOrder(sorter.order)
|
|
||||||
} else {
|
|
||||||
setSortBy('createdAt')
|
|
||||||
setSortOrder('descend')
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Drawer title={selectedJob?.sourceName || '导入详情'} open={drawerOpen} onClose={() => { setDrawerOpen(false); setSelectedJob(null) }} width={700}>
|
||||||
<Drawer title={selectedJob?.sourceName || '导入详情'} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={700}>
|
{detail ? (
|
||||||
{detail && (
|
|
||||||
<>
|
<>
|
||||||
<Title level={5} style={{ fontSize: 14 }}>Job: {detail.job?.id}</Title>
|
<Space wrap style={{ marginBottom: 16 }}>
|
||||||
<Text>状态: <Tag color={statusColor[detail.job?.status]}>{importStatusLabels[detail.job?.status] || detail.job?.status}</Tag></Text>
|
<Tag color={statusColor[detail.status] || 'default'}>{importStatusLabels[detail.status] || detail.status}</Tag>
|
||||||
<Text style={{ marginLeft: 16 }}>进度: {detail.job?.progress || 0}%</Text>
|
<Text type="secondary">进度: {detail.progress || 0}%</Text>
|
||||||
<Text style={{ marginLeft: 16 }}>重试: {detail.job?.retryCount}/{detail.job?.maxRetries}</Text>
|
<Text type="secondary">重试: {detail.retryCount}</Text>
|
||||||
{detail.job?.errorMessage && <div style={{ marginTop: 8 }}><Text type="danger">{detail.job?.errorMessage}</Text></div>}
|
<Text type="secondary">知识库: {detail.kbName || detail.kbId?.slice(0, 8)}</Text>
|
||||||
<Title level={5} style={{ fontSize: 14, marginTop: 16 }}>步骤日志 ({detail.steps?.length || 0})</Title>
|
<Text type="secondary">创建: {dayjs(detail.createdAt).format('YYYY-MM-DD HH:mm:ss')}</Text>
|
||||||
<Table dataSource={detail.steps || []} columns={stepCols} rowKey="id" pagination={false} size="small" />
|
</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>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -23,14 +23,14 @@ function KBPage() {
|
|||||||
|
|
||||||
const { data: sources } = useQuery({
|
const { data: sources } = useQuery({
|
||||||
queryKey: ['kb-sources', selectedKb?.id],
|
queryKey: ['kb-sources', selectedKb?.id],
|
||||||
queryFn: (): Promise<any> => knowledgeAPI.sources(selectedKb?.id || ''),
|
queryFn: () => knowledgeAPI.sources(selectedKb?.id || ''),
|
||||||
enabled: !!selectedKb?.id,
|
enabled: !!selectedKb?.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
const [refSourceId, setRefSourceId] = useState<string | null>(null)
|
const [refSourceId, setRefSourceId] = useState<string | null>(null)
|
||||||
const { data: references } = useQuery({
|
const { data: references } = useQuery({
|
||||||
queryKey: ['source-refs', refSourceId],
|
queryKey: ['source-refs', refSourceId],
|
||||||
queryFn: (): Promise<any> => knowledgeAPI.sourceReferences(refSourceId || ''),
|
queryFn: () => knowledgeAPI.sourceReferences(refSourceId || ''),
|
||||||
enabled: !!refSourceId,
|
enabled: !!refSourceId,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -16,12 +16,12 @@ export default function KnowledgeOpsPage() {
|
|||||||
|
|
||||||
const { data: candidates } = useQuery({
|
const { data: candidates } = useQuery({
|
||||||
queryKey: ['ops', 'candidates'],
|
queryKey: ['ops', 'candidates'],
|
||||||
queryFn: (): Promise<any> => knowledgeAPI.candidates(),
|
queryFn: () => knowledgeAPI.candidates(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: chunks } = useQuery({
|
const { data: chunks } = useQuery({
|
||||||
queryKey: ['ops', 'chunks', sourceId],
|
queryKey: ['ops', 'chunks', sourceId],
|
||||||
queryFn: (): Promise<any> => knowledgeAPI.chunks(sourceId),
|
queryFn: () => knowledgeAPI.chunks(sourceId),
|
||||||
enabled: !!sourceId,
|
enabled: !!sourceId,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -13,9 +13,9 @@ export default function MemberManagement() {
|
|||||||
const [addOpen, setAddOpen] = useState(false)
|
const [addOpen, setAddOpen] = useState(false)
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
const { data: usersData } = useQuery({ queryKey: ['users', 'list'], queryFn: (): Promise<any> => membersAPI.list() })
|
const { data: usersData } = useQuery({ queryKey: ['users', 'list'], queryFn: () => membersAPI.list() })
|
||||||
const { data: memberships } = useQuery({ queryKey: ['users', 'memberships'], queryFn: (): Promise<any> => membersAPI.memberships() })
|
const { data: memberships } = useQuery({ queryKey: ['users', 'memberships'], queryFn: () => membersAPI.memberships() })
|
||||||
const { data: deletions } = useQuery({ queryKey: ['users', 'deletions'], queryFn: (): Promise<any> => membersAPI.deletionRequests() })
|
const { data: deletions } = useQuery({ queryKey: ['users', 'deletions'], queryFn: () => membersAPI.deletionRequests() })
|
||||||
|
|
||||||
const assignMembership = async (v: any) => {
|
const assignMembership = async (v: any) => {
|
||||||
await membersAPI.updateMembership({ userId: v.userId, planId: v.planId, expiresAt: v.expiresAt?.toISOString() })
|
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 [addOpen, setAddOpen] = useState(false)
|
||||||
const [newPlan, setNewPlan] = useState({ name: '', code: '', priceMonthly: 0, monthlyChatCount: 10 })
|
const [newPlan, setNewPlan] = useState({ name: '', code: '', priceMonthly: 0, monthlyChatCount: 10 })
|
||||||
|
|
||||||
const { data: plans } = useQuery({ queryKey: ['quota', 'plans'], queryFn: (): Promise<any> => quotaAPI.plans() })
|
const { data: plans } = useQuery({ queryKey: ['quota', 'plans'], queryFn: () => quotaAPI.plans() })
|
||||||
const { data: memberships } = useQuery({ queryKey: ['quota', 'memberships'], queryFn: (): Promise<any> => quotaAPI.memberships() })
|
const { data: memberships } = useQuery({ queryKey: ['quota', 'memberships'], queryFn: () => quotaAPI.memberships() })
|
||||||
const { data: costs } = useQuery({ queryKey: ['quota', 'costs'], queryFn: (): Promise<any> => quotaAPI.costs() })
|
const { data: costs } = useQuery({ queryKey: ['quota', 'costs'], queryFn: () => quotaAPI.costs() })
|
||||||
|
|
||||||
const addPlan = async () => {
|
const addPlan = async () => {
|
||||||
await quotaAPI.createPlan(newPlan)
|
await quotaAPI.createPlan(newPlan)
|
||||||
|
|||||||
@ -9,11 +9,11 @@ const { Title } = Typography
|
|||||||
function MetricsPage() {
|
function MetricsPage() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
const { data: overview } = useQuery({ queryKey: ['metrics', 'overview'], queryFn: (): Promise<any> => metricsAPI.overview(), staleTime: 10_000 })
|
const { data: overview } = useQuery({ queryKey: ['metrics', 'overview'], queryFn: () => metricsAPI.overview(), staleTime: 10_000 })
|
||||||
const { data: top } = useQuery({ queryKey: ['metrics', 'top'], queryFn: (): Promise<any> => metricsAPI.top(), staleTime: 10_000 })
|
const { data: top } = useQuery({ queryKey: ['metrics', 'top'], queryFn: () => metricsAPI.top(), staleTime: 10_000 })
|
||||||
const { data: recent } = useQuery({ queryKey: ['metrics', 'recent'], queryFn: (): Promise<any> => metricsAPI.recent(), staleTime: 5_000 })
|
const { data: recent } = useQuery({ queryKey: ['metrics', 'recent'], queryFn: () => metricsAPI.recent(), staleTime: 5_000 })
|
||||||
const { data: ai } = useQuery({ queryKey: ['metrics', 'ai'], queryFn: (): Promise<any> => metricsAPI.ai(), staleTime: 10_000 })
|
const { data: ai } = useQuery({ queryKey: ['metrics', 'ai'], queryFn: () => metricsAPI.ai(), staleTime: 10_000 })
|
||||||
const { data: worker } = useQuery({ queryKey: ['metrics', 'worker'], queryFn: (): Promise<any> => metricsAPI.worker(), staleTime: 10_000 })
|
const { data: worker } = useQuery({ queryKey: ['metrics', 'worker'], queryFn: () => metricsAPI.worker(), staleTime: 10_000 })
|
||||||
|
|
||||||
const topCols = [
|
const topCols = [
|
||||||
{ title: '接口', dataIndex: 'path', width: 300, ellipsis: true },
|
{ title: '接口', dataIndex: 'path', width: 300, ellipsis: true },
|
||||||
|
|||||||
@ -16,9 +16,9 @@ function SecretsPage() {
|
|||||||
const [form, setForm] = useState({ name: '', provider: 'deepseek', value: '', expiresAt: '' })
|
const [form, setForm] = useState({ name: '', provider: 'deepseek', value: '', expiresAt: '' })
|
||||||
const [billForm, setBillForm] = useState({ provider: 'deepseek', billMonth: dayjs().format('YYYY-MM'), amount: 0, notes: '' })
|
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: secrets } = useQuery({ queryKey: ['secrets'], queryFn: () => secretsAPI.list() })
|
||||||
const { data: logs } = useQuery({ queryKey: ['secrets', 'logs'], queryFn: (): Promise<any> => secretsAPI.logs() })
|
const { data: logs } = useQuery({ queryKey: ['secrets', 'logs'], queryFn: () => secretsAPI.logs() })
|
||||||
const { data: bills } = useQuery({ queryKey: ['vendor', 'bills'], queryFn: (): Promise<any> => secretsAPI.vendorBills() })
|
const { data: bills } = useQuery({ queryKey: ['vendor', 'bills'], queryFn: () => secretsAPI.vendorBills() })
|
||||||
|
|
||||||
const addSecret = async () => {
|
const addSecret = async () => {
|
||||||
await secretsAPI.create(form)
|
await secretsAPI.create(form)
|
||||||
|
|||||||
@ -11,13 +11,13 @@ export default function VectorAdminPage() {
|
|||||||
|
|
||||||
const { data: coll, isLoading: collLoading } = useQuery({
|
const { data: coll, isLoading: collLoading } = useQuery({
|
||||||
queryKey: ['vector', 'collection'],
|
queryKey: ['vector', 'collection'],
|
||||||
queryFn: (): Promise<any> => vectorAPI.collection(),
|
queryFn: () => vectorAPI.collection(),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: count } = useQuery({
|
const { data: count } = useQuery({
|
||||||
queryKey: ['vector', 'count'],
|
queryKey: ['vector', 'count'],
|
||||||
queryFn: (): Promise<any> => vectorAPI.count(),
|
queryFn: () => vectorAPI.count(),
|
||||||
staleTime: 30_000,
|
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.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="最后事件时间" 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.eventCount ?? '-'}</Descriptions.Item>
|
||||||
<Descriptions.Item label="时长组成">{detail.durationBreakdown || '-'}</Descriptions.Item>
|
<Descriptions.Item label="时长组成">{String(detail.durationBreakdown ?? '-')}</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
)}
|
)}
|
||||||
{detail?.clientSessionId && (
|
{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;
|
appVersion?: string;
|
||||||
status: string;
|
status: string;
|
||||||
errorCode?: string;
|
errorCode?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
retryCount?: number;
|
||||||
warningCodes?: unknown;
|
warningCodes?: unknown;
|
||||||
serverReceivedAt: string;
|
serverReceivedAt: string;
|
||||||
processedAt?: string;
|
processedAt?: string;
|
||||||
@ -38,8 +40,17 @@ export interface LearningSession {
|
|||||||
totalActiveSeconds: number;
|
totalActiveSeconds: number;
|
||||||
clientSessionId?: string;
|
clientSessionId?: string;
|
||||||
materialId?: string;
|
materialId?: string;
|
||||||
|
materialName?: string;
|
||||||
readingTargetType?: string;
|
readingTargetType?: string;
|
||||||
|
targetType?: string;
|
||||||
lastPosition?: unknown;
|
lastPosition?: unknown;
|
||||||
|
userName?: string;
|
||||||
|
kbName?: string;
|
||||||
|
interrupted?: boolean;
|
||||||
|
missingMaterialClosed?: boolean;
|
||||||
|
abnormalDelta?: number;
|
||||||
|
eventCount?: number;
|
||||||
|
durationBreakdown?: unknown;
|
||||||
lastEventAt?: string;
|
lastEventAt?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
@ -98,10 +109,11 @@ export interface LearningRecord {
|
|||||||
|
|
||||||
// ── Dashboard ──
|
// ── Dashboard ──
|
||||||
export interface AdminDashboard {
|
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 };
|
sessions: { active: number; interrupted: number; completed: number; total: number };
|
||||||
users: { activeToday: number; totalWithEvents: number };
|
users: { activeToday: number; totalWithEvents: number };
|
||||||
materials: { totalRead: number; totalMarkedRead: number };
|
materials: { totalRead: number; totalMarkedRead: number };
|
||||||
|
recentAnomalies?: Array<{ eventId: string; eventType: string; errorCode?: string; createdAt: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Anomalies ──
|
// ── 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,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Strict — 渐进式收紧 */
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": { "@/*": ["src/*"] },
|
"paths": { "@/*": ["src/*"] },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user