diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4d9f502 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..e42cbc1 --- /dev/null +++ b/.env.production @@ -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 +# ============================================ diff --git a/package-lock.json b/package-lock.json index 4413e25..79f44e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,8 @@ "react-markdown": "^10.1.0", "react-router-dom": "^7.15.1", "react-syntax-highlighter": "^16.1.1", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "zustand": "^5.0.14" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -7075,6 +7076,35 @@ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", "license": "0BSD" }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", diff --git a/package.json b/package.json index 44229e5..5875ada 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "react-markdown": "^10.1.0", "react-router-dom": "^7.15.1", "react-syntax-highlighter": "^16.1.1", - "remark-gfm": "^4.0.1" + "remark-gfm": "^4.0.1", + "zustand": "^5.0.14" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/src/api/ai-gateway.ts b/src/api/ai-gateway.ts index 0a72446..0efc367 100644 --- a/src/api/ai-gateway.ts +++ b/src/api/ai-gateway.ts @@ -1,11 +1,14 @@ import { api } from '@/lib/api-client' +import type { AiRoute } from '@/types' + +export type CreateRouteDto = Omit export const aiGatewayAPI = { status() { return api.get('/admin-api/ai-gateway/status') }, routes() { return api.get('/admin-api/ai-gateway/routes') }, providers() { return api.get('/admin-api/ai-gateway/providers') }, fallbackEvents() { return api.get('/admin-api/ai-gateway/fallback-events') }, - createRoute(data: any) { return api.post('/admin-api/ai-gateway/routes', data) }, + createRoute(data: CreateRouteDto) { return api.post('/admin-api/ai-gateway/routes', data) }, deleteRoute(id: string) { return api.delete(`/admin-api/ai-gateway/routes/${id}`) }, toggleProvider(name: string, enabled: boolean) { return api.put(`/admin-api/ai-gateway/providers/${name}`, { enabled }) }, } diff --git a/src/api/billing.ts b/src/api/billing.ts index de5b5c8..a87687f 100644 --- a/src/api/billing.ts +++ b/src/api/billing.ts @@ -1,9 +1,7 @@ import { api } from '@/lib/api-client' +import type { BillingInfo } from '@/types' -export interface BillingInfo { - name: string; model: string; balance: string; currency: string - status: 'ok' | 'unknown'; consoleUrl: string; note: string -} +export type { BillingInfo } from '@/types' export const billingAPI = { get() { return api.get<{ providers: BillingInfo[] }>('/admin-api/billing') }, diff --git a/src/api/compliance.ts b/src/api/compliance.ts index ef47ddc..2c4aba3 100644 --- a/src/api/compliance.ts +++ b/src/api/compliance.ts @@ -1,14 +1,18 @@ import { api } from '@/lib/api-client' +export interface PrivacyDto { version: string; title: string; content: string; effectiveAt: string; published?: boolean } +export interface AgreementDto { version: string; title: string; content: string; effectiveAt: string; published?: boolean } +export interface FilingDto { type: string; title: string; description?: string; status?: string } + export const complianceAPI = { privacyPolicies: () => api.get('/admin-api/compliance/privacy-policies'), - createPrivacy: (data: any) => api.post('/admin-api/compliance/privacy-policies', data), - updatePrivacy: (id: string, data: any) => api.patch(`/admin-api/compliance/privacy-policies/${id}`, data), + createPrivacy: (data: PrivacyDto) => api.post('/admin-api/compliance/privacy-policies', data), + updatePrivacy: (id: string, data: Partial) => api.patch(`/admin-api/compliance/privacy-policies/${id}`, data), userAgreements: () => api.get('/admin-api/compliance/user-agreements'), - createAgreement: (data: any) => api.post('/admin-api/compliance/user-agreements', data), - updateAgreement: (id: string, data: any) => api.patch(`/admin-api/compliance/user-agreements/${id}`, data), + createAgreement: (data: AgreementDto) => api.post('/admin-api/compliance/user-agreements', data), + updateAgreement: (id: string, data: Partial) => api.patch(`/admin-api/compliance/user-agreements/${id}`, data), filings: () => api.get('/admin-api/compliance/filings'), - createFiling: (data: any) => api.post('/admin-api/compliance/filings', data), + createFiling: (data: FilingDto) => api.post('/admin-api/compliance/filings', data), deletionRequests: () => api.get('/admin-api/compliance/deletion-requests'), approveDeletion: (id: string) => api.post(`/admin-api/compliance/deletion-requests/${id}/approve`), exportRequests: () => api.get('/admin-api/compliance/export-requests'), diff --git a/src/api/config.ts b/src/api/config.ts index 2ae217d..e14f9de 100644 --- a/src/api/config.ts +++ b/src/api/config.ts @@ -1,11 +1,12 @@ import { api } from '@/lib/api-client' +import type { AppConfig, FeatureFlag, ConfigChangeLog } from '@/types' export const configAPI = { - get() { return api.get<{ configs: any[]; flags: any[] }>('/admin-api/config') }, + get() { return api.get<{ configs: AppConfig[]; flags: FeatureFlag[] }>('/admin-api/config') }, set(key: string, value: string) { return api.post('/admin-api/config', { key, value }) }, update(key: string, value: string) { return api.patch(`/admin-api/config/${key}`, { value }) }, remove(key: string) { return api.delete(`/admin-api/config/${key}`) }, toggleFlag(name: string, enabled: boolean) { return api.post(`/admin-api/config/flags/${name}`, { enabled }) }, setFlagWhitelist(name: string, whitelist: string) { return api.post(`/admin-api/config/flags/${name}`, { whitelist }) }, - changeLog() { return api.get('/admin-api/config/changelog') }, + changeLog() { return api.get('/admin-api/config/changelog') }, } diff --git a/src/api/content-safety.ts b/src/api/content-safety.ts index f0f02cf..158fb4e 100644 --- a/src/api/content-safety.ts +++ b/src/api/content-safety.ts @@ -1,12 +1,17 @@ import { api } from '@/lib/api-client' +export interface AddWordDto { word: string; category?: string; riskLevel?: string } +export interface HandleReportDto { action?: string; note?: string } +export interface PenaltyDto { penalty: string } +export interface SafetyQueryParams { page?: number; limit?: number; status?: string; type?: string } + export const contentSafetyAPI = { words() { return api.get('/admin-api/content-safety/words') }, - addWord(data: any) { return api.post('/admin-api/content-safety/words', data) }, + addWord(data: AddWordDto) { return api.post('/admin-api/content-safety/words', data) }, deleteWord(id: string) { return api.delete(`/admin-api/content-safety/words/${id}`) }, - checks(params?: any) { return api.get('/admin-api/content-safety/checks', params) }, - reports(params?: any) { return api.get('/admin-api/content-safety/reports', params) }, - violations(params?: any) { return api.get('/admin-api/content-safety/violations', params) }, - handleReport(id: string, data: any) { return api.post(`/admin-api/content-safety/reports/${id}/handle`, data) }, - penaltyViolation(id: string, data: any) { return api.post(`/admin-api/content-safety/violations/${id}/penalty`, data) }, + checks(params?: SafetyQueryParams) { return api.get('/admin-api/content-safety/checks', params) }, + reports(params?: SafetyQueryParams) { return api.get('/admin-api/content-safety/reports', params) }, + violations(params?: SafetyQueryParams) { return api.get('/admin-api/content-safety/violations', params) }, + handleReport(id: string, data: HandleReportDto) { return api.post(`/admin-api/content-safety/reports/${id}/handle`, data) }, + penaltyViolation(id: string, data: PenaltyDto) { return api.post(`/admin-api/content-safety/violations/${id}/penalty`, data) }, } diff --git a/src/api/conversations.ts b/src/api/conversations.ts index 0c0747a..a752570 100644 --- a/src/api/conversations.ts +++ b/src/api/conversations.ts @@ -1,7 +1,7 @@ import { api } from '@/lib/api-client' +import type { Conversation, MessageRecord } from '@/types' -export interface Conversation { id: string; title: string; [key: string]: any } -export interface MessageRecord { id: string; content: string; role: string; [key: string]: any } +export type { Conversation, MessageRecord } from '@/types' export const conversationsAPI = { list() { return api.get('/admin-api/conversations') }, diff --git a/src/api/costs.ts b/src/api/costs.ts index 2626209..a990427 100644 --- a/src/api/costs.ts +++ b/src/api/costs.ts @@ -1,7 +1,7 @@ import { api } from '@/lib/api-client' +import type { CostItem, CostSummary } from '@/types' -export interface CostItem { id: string; name: string; currency: string; amount: number; purchaseDate?: string; expiryDate?: string; [key: string]: any } -export interface CostSummary { totalMonthly: number; totalYearly: number; totalOneTime: number; items: CostItem[]; byMonth: any[]; expiringSoon: any[] } +export type { CostItem, CostSummary, CostByMonth } from '@/types' export const costsAPI = { summary() { return api.get('/admin-api/costs/summary') }, diff --git a/src/api/events-queue.ts b/src/api/events-queue.ts index c565721..fe796e1 100644 --- a/src/api/events-queue.ts +++ b/src/api/events-queue.ts @@ -1,8 +1,7 @@ import { api } from '@/lib/api-client' +import type { QueueInfo, FailedJob, JobDetail } from '@/types' -export interface QueueInfo { name: string; waiting: number; active: number; failed: number; [key: string]: any } -export interface FailedJob { id: string; name: string; failedReason: string; [key: string]: any } -export interface JobDetail { id: string; data: any; [key: string]: any } +export type { QueueInfo, FailedJob, JobDetail, QueueOverview } from '@/types' export const eventsQueueAPI = { overview() { return api.get<{ queues: QueueInfo[] }>('/admin-api/events') }, diff --git a/src/api/imports.ts b/src/api/imports.ts index 59c823d..4d9a7ab 100644 --- a/src/api/imports.ts +++ b/src/api/imports.ts @@ -1,8 +1,9 @@ import { api } from '@/lib/api-client' +import type { ImportJob, ImportJobDetail } from '@/types' export const importsAdminAPI = { - list: (params: string) => api.get(`/admin-api/imports?${params}`), - detail: (id: string) => api.get(`/admin-api/imports/${id}`), + list: (params: string) => api.get<{ items: ImportJob[] }>(`/admin-api/imports?${params}`), + detail: (id: string) => api.get(`/admin-api/imports/${id}`), retry: (id: string) => api.post(`/admin-api/imports/${id}/retry`), batchRetry: (ids: string[]) => api.post('/admin-api/imports/batch-retry', { ids }), } diff --git a/src/api/index.ts b/src/api/index.ts index 680b1af..eeade16 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,16 +2,22 @@ export { authAPI } from './auth' export { usersAPI } from './users' export { dashboardAPI } from './dashboard' export { auditAPI } from './audit' -export { billingAPI, type BillingInfo } from './billing' -export { costsAPI, type CostItem, type CostSummary } from './costs' +export { billingAPI } from './billing' +export { costsAPI } from './costs' export { configAPI } from './config' -export { conversationsAPI, type Conversation } from './conversations' +export { conversationsAPI } from './conversations' export { eventsQueueAPI } from './events-queue' export { knowledgeAPI } from './knowledge' -export { serversAPI, type ServerInfo, type ProcessInfo, type ServerHealth, type HealthService } from './servers' +export { serversAPI } from './servers' export { learningAPI } from './learning' export { aiAPI } from './ai' +// 类型统一从 @/types 导出(源) +export type { BillingInfo } from '@/types' +export type { CostItem, CostSummary } from '@/types' +export type { Conversation, MessageRecord } from '@/types' +export type { ServerInfo, ProcessInfo, ServerHealth, HealthService } from '@/types' + // ── 新增 ── export { aiGatewayAPI } from './ai-gateway' export { cacheAPI } from './cache' diff --git a/src/api/knowledge.ts b/src/api/knowledge.ts index 07df741..8d03260 100644 --- a/src/api/knowledge.ts +++ b/src/api/knowledge.ts @@ -1,15 +1,16 @@ import { api } from '@/lib/api-client' +import type { KnowledgeBase, KnowledgeSource, PaginatedResult, SourceReference, KnowledgeCandidate, KnowledgeChunk } from '@/types' export const knowledgeAPI = { list(page = 1, limit = 20) { - return api.get<{ items: any[]; total: number; page: number; limit: number; totalPages: number }>(`/admin-api/knowledge-bases?page=${page}&limit=${limit}`) + return api.get>(`/admin-api/knowledge-bases?page=${page}&limit=${limit}`) }, - getById(id: string) { return api.get(`/admin-api/knowledge-bases/${id}`) }, + getById(id: string) { return api.get(`/admin-api/knowledge-bases/${id}`) }, remove(id: string) { return api.delete(`/admin-api/knowledge-bases/${id}`) }, - candidates() { return api.get('/admin-api/knowledge-bases/candidates?limit=50') }, - chunks(sourceId: string) { return api.get(`/admin-api/knowledge-bases/chunks?sourceId=${sourceId}&limit=50`) }, - sources(kbId: string) { return api.get(`/admin-api/knowledge-bases/${kbId}/sources`) }, - sourceReferences(sourceId: string) { return api.get(`/admin-api/knowledge-bases/sources/${sourceId}/references`) }, - allSources(params: string) { return api.get(`/admin-api/knowledge-bases/all-sources?${params}`) }, - search(params: string) { return api.get(`/admin-api/knowledge-bases?${params}`) }, + candidates() { return api.get('/admin-api/knowledge-bases/candidates?limit=50') }, + chunks(sourceId: string) { return api.get(`/admin-api/knowledge-bases/chunks?sourceId=${sourceId}&limit=50`) }, + sources(kbId: string) { return api.get(`/admin-api/knowledge-bases/${kbId}/sources`) }, + sourceReferences(sourceId: string) { return api.get(`/admin-api/knowledge-bases/sources/${sourceId}/references`) }, + allSources(params: string) { return api.get>(`/admin-api/knowledge-bases/all-sources?${params}`) }, + search(params: string) { return api.get>(`/admin-api/knowledge-bases?${params}`) }, } diff --git a/src/api/learning.ts b/src/api/learning.ts index 87be7bd..225c39e 100644 --- a/src/api/learning.ts +++ b/src/api/learning.ts @@ -1,48 +1,64 @@ import { api } from '@/lib/api-client' +import type { + PaginatedResult, + ReadingEvent, + LearningSession, + MaterialReadingProgress, + DailyLearningActivity, + LearningRecord, + AdminDashboard, + AnomalyData, + UserTimeline, + UserDiagnose, + MaterialDiagnose, +} from '@/types' const BASE = '/admin-api/learning' -function qs(params?: Record): string { +type QueryParams = Record + +function qs(params?: QueryParams): string { if (!params) return '' - const p = Object.entries(params).filter(([, v]) => v !== undefined && v !== '').map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&') + const p = Object.entries(params) + .filter(([, v]) => v !== undefined && v !== '') + .map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`) + .join('&') return p ? `?${p}` : '' } -export interface PaginatedResponse { items: T[]; total: number } - export const learningAPI = { // dashboard - getDashboard: (params?: Record) => api.get(`${BASE}/dashboard${qs(params)}`), + getDashboard: (params?: QueryParams) => api.get(`${BASE}/dashboard${qs(params)}`), getKnowledgeBases: () => api.get>(`${BASE}/knowledge-bases`), // reading events - getReadingEvents: (params?: Record) => api.get>(`${BASE}/reading-events${qs(params)}`), - getReadingEventDetail: (id: string) => api.get(`${BASE}/reading-events/${id}`), - getFailedEvents: (params?: Record) => api.get>(`${BASE}/reading-events/failed${qs(params)}`), + getReadingEvents: (params?: QueryParams) => api.get>(`${BASE}/reading-events${qs(params)}`), + getReadingEventDetail: (id: string) => api.get(`${BASE}/reading-events/${id}`), + getFailedEvents: (params?: QueryParams) => api.get>(`${BASE}/reading-events/failed${qs(params)}`), // sessions - getSessions: (params?: Record) => api.get>(`${BASE}/sessions${qs(params)}`), - getSessionDetail: (id: string) => api.get(`${BASE}/sessions/${id}`), + getSessions: (params?: QueryParams) => api.get>(`${BASE}/sessions${qs(params)}`), + getSessionDetail: (id: string) => api.get(`${BASE}/sessions/${id}`), deleteSessions: (ids: string[]) => api.post<{ deleted: number; message: string }>(`${BASE}/sessions/batch-delete`, { ids }), // progress & activities - getProgress: (params?: Record) => api.get>(`${BASE}/progress${qs(params)}`), - getDailyActivities: (params?: Record) => api.get>(`${BASE}/daily-activities${qs(params)}`), - getRecords: (params?: Record) => api.get>(`${BASE}/records${qs(params)}`), + getProgress: (params?: QueryParams) => api.get>(`${BASE}/progress${qs(params)}`), + getDailyActivities: (params?: QueryParams) => api.get>(`${BASE}/daily-activities${qs(params)}`), + getRecords: (params?: QueryParams) => api.get>(`${BASE}/records${qs(params)}`), // user/material - getUserTimeline: (userId: string) => api.get(`${BASE}/user-timeline?userId=${encodeURIComponent(userId)}`), - getUserDiagnose: (userId: string) => api.get(`${BASE}/user-diagnose?userId=${encodeURIComponent(userId)}`), - getMaterialDiagnose: (materialId: string) => api.get(`${BASE}/material-diagnose?materialId=${encodeURIComponent(materialId)}`), + getUserTimeline: (userId: string) => api.get(`${BASE}/user-timeline?userId=${encodeURIComponent(userId)}`), + getUserDiagnose: (userId: string) => api.get(`${BASE}/user-diagnose?userId=${encodeURIComponent(userId)}`), + getMaterialDiagnose: (materialId: string) => api.get(`${BASE}/material-diagnose?materialId=${encodeURIComponent(materialId)}`), // misc - getAnomalies: () => api.get(`${BASE}/anomalies`), - getTemporaryMaterials: (params?: Record) => api.get>(`${BASE}/temporary-materials${qs(params)}`), + getAnomalies: () => api.get(`${BASE}/anomalies`), + getTemporaryMaterials: (params?: QueryParams) => api.get>(`${BASE}/temporary-materials${qs(params)}`), // actions recalculate: () => api.post(`${BASE}/recalculate`), replayEvent: (eventId: string) => api.post<{ success: boolean; message: string }>(`${BASE}/reading-events/${eventId}/replay`), - replayEventsBatch: (params: Record) => api.post<{ replayed: number; failed: number }>(`${BASE}/reading-events/replay-batch`, params), + replayEventsBatch: (params: QueryParams) => api.post<{ replayed: number; failed: number }>(`${BASE}/reading-events/replay-batch`, params), exportData: (type: string) => api.get(`${BASE}/export?type=${encodeURIComponent(type)}`), getSessionsRaw: (qs: string) => api.get(`${BASE}/sessions?${qs}`), getAnalysis: (qs: string) => api.get(`${BASE}/analysis?${qs}`), diff --git a/src/api/members.ts b/src/api/members.ts index 82951e8..1567b70 100644 --- a/src/api/members.ts +++ b/src/api/members.ts @@ -1,9 +1,11 @@ import { api } from '@/lib/api-client' +export interface UpdateMembershipDto { userId: string; planId: string; status?: string; expiresAt?: string } + export const membersAPI = { list: () => api.get('/admin-api/users'), deletionRequests: () => api.get('/admin-api/users/deletion-requests'), processDeletion: (id: string, action: string) => api.post(`/admin-api/users/deletion-requests/${id}/${action}`), memberships: () => api.get('/admin-api/users/memberships'), - updateMembership: (data: any) => api.post('/admin-api/users/memberships', data), + updateMembership: (data: UpdateMembershipDto) => api.post('/admin-api/users/memberships', data), } diff --git a/src/api/notifications.ts b/src/api/notifications.ts index 5f0fec6..70fb2fb 100644 --- a/src/api/notifications.ts +++ b/src/api/notifications.ts @@ -1,12 +1,17 @@ import { api } from '@/lib/api-client' +import type { NotificationTemplate } from '@/types' + +export type CreateTemplateDto = Omit +export type UpdateTemplateDto = Partial +export interface SendNotificationDto { type: string; title: string; content?: string } export const notificationsAPI = { templates: () => api.get('/admin-api/notifications/templates'), - createTemplate: (data: any) => api.post('/admin-api/notifications/templates', data), - updateTemplate: (id: string, data: any) => api.patch(`/admin-api/notifications/templates/${id}`, data), + createTemplate: (data: CreateTemplateDto) => api.post('/admin-api/notifications/templates', data), + updateTemplate: (id: string, data: UpdateTemplateDto) => api.patch(`/admin-api/notifications/templates/${id}`, data), deleteTemplate: (id: string) => api.delete(`/admin-api/notifications/templates/${id}`), sendLog: () => api.get('/admin-api/notifications/send-log?limit=50'), adminNotifications: () => api.get('/admin-api/admin-notifications'), - send: (data: any) => api.post('/admin-api/admin-notifications/send', data), + send: (data: SendNotificationDto) => api.post('/admin-api/admin-notifications/send', data), markRead: (id: string) => api.post(`/admin-api/admin-notifications/${id}/read`), } diff --git a/src/api/quota.ts b/src/api/quota.ts index 9fd86cb..78f49c3 100644 --- a/src/api/quota.ts +++ b/src/api/quota.ts @@ -1,8 +1,11 @@ import { api } from '@/lib/api-client' +import type { MembershipPlan } from '@/types' + +export type CreatePlanDto = Omit export const quotaAPI = { plans: () => api.get('/admin-api/quota/plans'), - createPlan: (data: any) => api.post('/admin-api/quota/plans', data), + createPlan: (data: CreatePlanDto) => api.post('/admin-api/quota/plans', data), memberships: () => api.get('/admin-api/quota/memberships'), costs: () => api.get('/admin-api/quota/costs'), } diff --git a/src/api/release.ts b/src/api/release.ts index 528006e..bb97e60 100644 --- a/src/api/release.ts +++ b/src/api/release.ts @@ -1,11 +1,17 @@ import { api } from '@/lib/api-client' +import type { Changelog, ReleaseDecision, ReleaseChecklistItem } from '@/types' + +export type CreateChangelogDto = Omit +export type UpdateChangelogDto = Partial +export type CreateDecisionDto = Omit +export type UpdateDecisionDto = Partial export const releaseAPI = { changelogs: () => api.get('/admin-api/release/changelogs'), decisions: () => api.get('/admin-api/release/decisions'), - checklists: (ver: string) => api.get(`/admin-api/release/checklists/${ver}`), - create: (type: string, data: any) => api.post(`/admin-api/release/${type}`, data), - update: (type: string, id: string, data: any) => api.patch(`/admin-api/release/${type}/${id}`, data), + checklists: (ver: string) => api.get(`/admin-api/release/checklists/${ver}`), + create: (type: string, data: CreateChangelogDto | CreateDecisionDto) => api.post(`/admin-api/release/${type}`, data), + update: (type: string, id: string, data: UpdateChangelogDto | UpdateDecisionDto) => api.patch(`/admin-api/release/${type}/${id}`, data), delete: (type: string, id: string) => api.delete(`/admin-api/release/${type}/${id}`), toggleChecklist: (id: string, checked: boolean) => api.patch(`/admin-api/release/checklists/${id}`, { checked }), } diff --git a/src/api/secrets.ts b/src/api/secrets.ts index 1feac90..727270e 100644 --- a/src/api/secrets.ts +++ b/src/api/secrets.ts @@ -1,12 +1,15 @@ import { api } from '@/lib/api-client' +export interface CreateSecretDto { name: string; provider: string; value: string; expiresAt?: string } +export interface CreateVendorBillDto { provider: string; billMonth: string; amount: number; notes?: string } + export const secretsAPI = { list() { return api.get('/admin-api/secrets') }, logs() { return api.get('/admin-api/secrets/logs') }, - create(data: any) { return api.post('/admin-api/secrets', data) }, + create(data: CreateSecretDto) { return api.post('/admin-api/secrets', data) }, delete(id: string) { return api.delete(`/admin-api/secrets/${id}`) }, vendorBills() { return api.get('/admin-api/vendor/bills') }, - createVendorBill(data: any) { return api.post('/admin-api/vendor/bills', data) }, + createVendorBill(data: CreateVendorBillDto) { return api.post('/admin-api/vendor/bills', data) }, deleteVendorBill(id: string) { return api.delete(`/admin-api/vendor/bills/${id}`) }, rotateVendorSecret(id: string) { return api.post(`/admin-api/vendor/secrets/${id}/rotate`) }, revokeVendorSecret(id: string) { return api.post(`/admin-api/vendor/secrets/${id}/revoke`) }, diff --git a/src/api/servers.ts b/src/api/servers.ts index 3b1fd6e..b98b2da 100644 --- a/src/api/servers.ts +++ b/src/api/servers.ts @@ -1,18 +1,7 @@ import { api } from '@/lib/api-client' +import type { ServerInfo, ServerHealth, WorkerInfo } from '@/types' -export interface ServerInfo { - hostname: string - cpu: { usagePercent: number; cores: number; model?: string } - memory: { percent: number; used: string; total: string } - network: { domains: string[]; privateIp?: string; publicIp?: string } - disks: { mount: string; size: string; used: string; percent: number }[] - [key: string]: any -} - -export interface ProcessInfo { pid: number; name: string; cpu: number; memory: number; desc?: string; command?: string; [key: string]: any } -export interface HealthService { serviceName: string; status: string; message?: string } -export interface ServerHealth { name: string; status: string; uptime?: string; pid?: number; services?: HealthService[]; queues?: string[]; [key: string]: any } -export interface WorkerInfo { name: string; status: string; queues?: string[]; [key: string]: any } +export type { ServerInfo, ProcessInfo, HealthService, ServerHealth, WorkerInfo, ServerMetrics } from '@/types' export const serversAPI = { metrics() { return api.get<{ servers: ServerInfo[] }>('/admin-api/servers') }, diff --git a/src/api/vector.ts b/src/api/vector.ts index 112f5c7..eeccf80 100644 --- a/src/api/vector.ts +++ b/src/api/vector.ts @@ -1,8 +1,9 @@ import { api } from '@/lib/api-client' +import type { VectorDebugQuery } from '@/types' export const vectorAPI = { count: () => api.get('/admin-api/vector/count'), collection: () => api.get('/admin-api/vector/collection'), reindex: () => api.post('/admin-api/vector/reindex'), - debugSearch: (data: any) => api.post('/admin-api/vector/debug-search', data), + debugSearch: (data: VectorDebugQuery) => api.post('/admin-api/vector/debug-search', data), } diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index bb36c3d..b4410ff 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -3,6 +3,7 @@ import { ProLayout } from '@ant-design/pro-components' import { Dropdown, Avatar, Tag, Space, message } from 'antd' import { LogoutOutlined, UserOutlined } from '@ant-design/icons' import { useAuth } from '@/contexts/AuthContext' +import { useUIStore } from '@/stores' import { filterMenuByRole, adminMenuItems } from '@/config/menu' import { ADMIN_ROLE_LABELS, ADMIN_ROLE_COLORS } from '@/constants/roles' import type { AdminRole } from '@/types/admin' @@ -60,6 +61,9 @@ export default function AdminLayout() { const location = useLocation() const { adminUser, logout, hasPermission } = useAuth() + const sidebarCollapsed = useUIStore((s) => s.sidebarCollapsed) + const setSidebarCollapsed = useUIStore((s) => s.setSidebarCollapsed) + const currentRole = adminUser?.role as AdminRole | undefined const handleLogout = async () => { @@ -135,6 +139,8 @@ export default function AdminLayout() { ) : null } + collapsed={sidebarCollapsed} + onCollapse={setSidebarCollapsed} siderWidth={220} token={{ header: { heightLayoutHeader: 48 }, diff --git a/src/pages/AiGateway.tsx b/src/pages/AiGateway.tsx index 12df719..1dfc0d4 100644 --- a/src/pages/AiGateway.tsx +++ b/src/pages/AiGateway.tsx @@ -15,25 +15,25 @@ export default function AiGatewayPage() { const { data: status } = useQuery({ queryKey: ['ai-gateway', 'status'], - queryFn: (): Promise => aiGatewayAPI.status(), + queryFn: () =>aiGatewayAPI.status(), staleTime: 30_000, }) const { data: routes, refetch: refetchRoutes } = useQuery({ queryKey: ['ai-gateway', 'routes'], - queryFn: (): Promise => aiGatewayAPI.routes(), + queryFn: () =>aiGatewayAPI.routes(), enabled: activeTab === 'routes', }) const { data: providers, refetch: refetchProviders } = useQuery({ queryKey: ['ai-gateway', 'providers'], - queryFn: (): Promise => aiGatewayAPI.providers(), + queryFn: () =>aiGatewayAPI.providers(), enabled: activeTab === 'providers', }) const { data: fallbackEvents } = useQuery({ queryKey: ['ai-gateway', 'fallback-events'], - queryFn: (): Promise => aiGatewayAPI.fallbackEvents(), + queryFn: () =>aiGatewayAPI.fallbackEvents(), enabled: activeTab === 'fallback', refetchInterval: 30_000, }) diff --git a/src/pages/Billing.tsx b/src/pages/Billing.tsx index 238ea48..9fe28a9 100644 --- a/src/pages/Billing.tsx +++ b/src/pages/Billing.tsx @@ -25,8 +25,8 @@ function BillingContent() { const { data: billing } = useQuery({ queryKey: ['billing'], queryFn: billingAPI.get, staleTime: 60_000 }) const { data: costs } = useQuery({ queryKey: ['costs', 'summary'], queryFn: costsAPI.summary, staleTime: 30_000 }) - const { data: aiReport } = useQuery({ queryKey: ['costs', 'report'], queryFn: (): Promise => costsAPI.report(), staleTime: 30_000 }) - const { data: topUsers } = useQuery({ queryKey: ['costs', 'top-users'], queryFn: (): Promise => costsAPI.topUsers(), staleTime: 30_000 }) + const { data: aiReport } = useQuery({ queryKey: ['costs', 'report'], queryFn: () => costsAPI.report(), staleTime: 30_000 }) + const { data: topUsers } = useQuery({ queryKey: ['costs', 'top-users'], queryFn: () => costsAPI.topUsers(), staleTime: 30_000 }) const refresh = async () => { setRefreshing(true); await qc.invalidateQueries({ queryKey: ['billing'] }); await qc.invalidateQueries({ queryKey: ['costs'] }); setTimeout(() => setRefreshing(false), 800) } diff --git a/src/pages/ChatLogs.tsx b/src/pages/ChatLogs.tsx index 4b36680..a36109f 100644 --- a/src/pages/ChatLogs.tsx +++ b/src/pages/ChatLogs.tsx @@ -14,13 +14,13 @@ export default function ChatLogsPage() { const { data: sessions, isLoading } = useQuery({ queryKey: ['chat-logs', 'sessions'], - queryFn: (): Promise => chatLogsAPI.sessions(), + queryFn: () => chatLogsAPI.sessions(), staleTime: 10_000, }) const { data: messages } = useQuery({ queryKey: ['chat-logs', 'messages', selectedSession?.id], - queryFn: (): Promise => chatLogsAPI.sessionMessages(selectedSession?.id || ''), + queryFn: () => chatLogsAPI.sessionMessages(selectedSession?.id || ''), enabled: !!selectedSession?.id, }) diff --git a/src/pages/ContentSafety.tsx b/src/pages/ContentSafety.tsx index 1f50c83..270dbb3 100644 --- a/src/pages/ContentSafety.tsx +++ b/src/pages/ContentSafety.tsx @@ -15,10 +15,10 @@ function CSPage() { const [category, setCategory] = useState('general') const [risk, setRisk] = useState('medium') - const { data: words } = useQuery({ queryKey: ['safety', 'words'], queryFn: (): Promise => contentSafetyAPI.words() }) - const { data: checks } = useQuery({ queryKey: ['safety', 'checks'], queryFn: (): Promise => contentSafetyAPI.checks() }) - const { data: reports } = useQuery({ queryKey: ['safety', 'reports'], queryFn: (): Promise => contentSafetyAPI.reports() }) - const { data: violations } = useQuery({ queryKey: ['safety', 'violations'], queryFn: (): Promise => contentSafetyAPI.violations() }) + const { data: words } = useQuery({ queryKey: ['safety', 'words'], queryFn: () => contentSafetyAPI.words() }) + const { data: checks } = useQuery({ queryKey: ['safety', 'checks'], queryFn: () => contentSafetyAPI.checks() }) + const { data: reports } = useQuery({ queryKey: ['safety', 'reports'], queryFn: () => contentSafetyAPI.reports() }) + const { data: violations } = useQuery({ queryKey: ['safety', 'violations'], queryFn: () => contentSafetyAPI.violations() }) const addWord = async () => { await contentSafetyAPI.addWord({ word: newWord, category, riskLevel: risk }) diff --git a/src/pages/Events.tsx b/src/pages/Events.tsx index 68ac39a..19cd9a7 100644 --- a/src/pages/Events.tsx +++ b/src/pages/Events.tsx @@ -14,26 +14,26 @@ function EventsPage() { const { data: overview } = useQuery({ queryKey: ['events', 'overview'], - queryFn: (): Promise => eventsQueueAPI.overview(), + queryFn: () =>eventsQueueAPI.overview(), staleTime: 10_000, }) const { data: stats } = useQuery({ queryKey: ['events', 'stats'], - queryFn: (): Promise => eventsQueueAPI.stats(), + queryFn: () =>eventsQueueAPI.stats(), enabled: activeTab === 'stats', }) const { data: workers } = useQuery({ queryKey: ['events', 'workers'], - queryFn: (): Promise => eventsQueueAPI.workers(), + queryFn: () =>eventsQueueAPI.workers(), enabled: activeTab === 'stats', refetchInterval: 15_000, }) const { data: failed } = useQuery({ queryKey: ['events', 'failed', selectedQueue], - queryFn: (): Promise => selectedQueue ? eventsQueueAPI.failedJobs(selectedQueue) : Promise.resolve(null), + queryFn: () =>selectedQueue ? eventsQueueAPI.failedJobs(selectedQueue) : Promise.resolve(null), enabled: !!selectedQueue, }) diff --git a/src/pages/ImportMonitor.tsx b/src/pages/ImportMonitor.tsx index 14c1c3c..b26aff0 100644 --- a/src/pages/ImportMonitor.tsx +++ b/src/pages/ImportMonitor.tsx @@ -4,12 +4,14 @@ import { Table, Tag, Typography, Button, App, Drawer, Space, Select, Popconfirm, import { ReloadOutlined, RetweetOutlined, ImportOutlined, EyeOutlined } from '@ant-design/icons' import { importsAdminAPI, knowledgeAPI } from '@/api' import { importStatusLabels, importStepLabels } from '@/constants/labels' +import type { TablePaginationConfig } from 'antd' +import type { FilterValue, SorterResult } from 'antd/es/table/interface' +import type { KnowledgeBase, ImportJob, ImportJobDetail, ImportStepLog } from '@/types' import dayjs from 'dayjs' const { Title, Text } = Typography -// 格式化知识库选项:用户 - 名称 (id前8位) -function formatKbLabel(kb: any) { +function formatKbLabel(kb: KnowledgeBase) { const user = kb.user?.email || kb.user?.nickname || kb.userId || '?' const name = kb.title || '未命名' const shortId = kb.id?.slice(0, 8) || kb.id @@ -20,7 +22,7 @@ export default function ImportMonitorPage() { const { message } = App.useApp() const qc = useQueryClient() const [drawerOpen, setDrawerOpen] = useState(false) - const [selectedJob, setSelectedJob] = useState(null) + const [selectedJob, setSelectedJob] = useState(null) const [selectedRowKeys, setSelectedRowKeys] = useState([]) const [kbFilter, setKbFilter] = useState(undefined) const [retryAllOpen, setRetryAllOpen] = useState(false) @@ -28,7 +30,6 @@ export default function ImportMonitorPage() { const [sortBy, setSortBy] = useState('createdAt') const [sortOrder, setSortOrder] = useState('descend') - // 知识库远程搜索(防抖) const [kbSearch, setKbSearch] = useState('') const kbSearchTimer = useRef>(undefined) @@ -37,10 +38,9 @@ export default function ImportMonitorPage() { kbSearchTimer.current = setTimeout(() => setKbSearch(val), 300) }, []) - // 知识库列表 const { data: kbData, isLoading: kbLoading } = useQuery({ queryKey: ['admin-knowledge-bases', kbSearch], - queryFn: (): Promise => { + queryFn: () => { const params = new URLSearchParams({ limit: '50' }) if (kbSearch) params.set('search', kbSearch) return knowledgeAPI.search(params.toString()) @@ -56,17 +56,16 @@ export default function ImportMonitorPage() { const { data, isLoading } = useQuery({ queryKey: ['admin-imports', 'list', kbFilter, sortBy, sortOrder], - queryFn: (): Promise => importsAdminAPI.list(queryParams.toString()), + queryFn: () => importsAdminAPI.list(queryParams.toString()), }) - // 默认过滤已删除 source 的记录 const tableData = showDeleted ? (data?.items || []) - : (data?.items || []).filter((r: any) => !r.sourceDeleted) + : (data?.items || []).filter((r: ImportJob) => !r.sourceDeleted) const { data: detail } = useQuery({ queryKey: ['admin-import-detail', selectedJob?.id], - queryFn: (): Promise => importsAdminAPI.detail(selectedJob?.id || ''), + queryFn: () => importsAdminAPI.detail(selectedJob?.id || ''), enabled: !!selectedJob?.id, }) @@ -83,17 +82,17 @@ export default function ImportMonitorPage() { await importsAdminAPI.retry(id) message.success('已重新入队,等待 Worker 处理') refreshList() - } catch (err: any) { - const msg = err?.response?.data?.message || err?.message || '重试失败' - message.error(`重试失败: ${msg}`) + } catch (err: unknown) { + const e = err as { response?: { data?: { message?: string } }; message?: string } + message.error(`重试失败: ${e?.response?.data?.message || e?.message || '未知错误'}`) } } const handleBatchRetry = async () => { try { const ids = selectedRowKeys as string[] - const res = await importsAdminAPI.batchRetry(ids as string[]) - const { updated, skipped, skippedCount } = res + const res = await importsAdminAPI.batchRetry(ids) + const { updated, skipped, skippedCount } = res as { updated: number; skipped: string[]; skippedCount: number } let msg = `已重新入队 ${updated} 个任务` if (skippedCount > 0) { msg += `,${skippedCount} 个跳过(源已删除: ${skipped.slice(0, 3).join(', ')}${skipped.length > 3 ? '...' : ''})` @@ -101,9 +100,9 @@ export default function ImportMonitorPage() { message.success(msg) setSelectedRowKeys([]) refreshList() - } catch (err: any) { - const msg = err?.response?.data?.message || err?.message || '批量重试失败' - message.error(msg) + } catch (err: unknown) { + const e = err as { response?: { data?: { message?: string } }; message?: string } + message.error(e?.response?.data?.message || e?.message || '批量重试失败') } } @@ -111,14 +110,14 @@ export default function ImportMonitorPage() { try { const allItems = data?.items || [] const retryableIds = allItems - .filter((r: any) => r.status?.startsWith('FAILED') || r.status === 'CLAIMED' || r.status === 'QUEUED') - .map((r: any) => r.id) + .filter((r: ImportJob) => r.status?.startsWith('FAILED') || r.status === 'CLAIMED' || r.status === 'QUEUED') + .map((r: ImportJob) => r.id) if (retryableIds.length === 0) { message.warning('没有可重新解析的任务') return } const res = await importsAdminAPI.batchRetry(retryableIds) - const { updated, skipped, skippedCount } = res + const { updated, skipped, skippedCount } = res as { updated: number; skipped: string[]; skippedCount: number } let msg = `已重新入队 ${updated} 个任务` if (skippedCount > 0) { msg += `,${skippedCount} 个跳过(源已删除: ${skipped.slice(0, 3).join(', ')}${skipped.length > 3 ? '...' : ''})` @@ -127,58 +126,40 @@ export default function ImportMonitorPage() { setSelectedRowKeys([]) setRetryAllOpen(false) refreshList() - } catch (err: any) { - const msg = err?.response?.data?.message || err?.message || '全部重试失败' - message.error(msg) - setRetryAllOpen(false) + } catch (err: unknown) { + const e = err as { response?: { data?: { message?: string } }; message?: string } + message.error(e?.response?.data?.message || e?.message || '全部重试失败') } } const statusColor: Record = { - QUEUED: 'blue', - CLAIMED: 'cyan', - DOWNLOADING: 'processing', - PARSING: 'processing', - OCR_PROCESSING: 'processing', - VISION_PROCESSING: 'processing', - CLEANING: 'processing', - CHUNKING: 'processing', - EMBEDDING: 'processing', - INDEXING: 'processing', - GENERATING_CANDIDATES: 'processing', - COMPLETED: 'green', - FAILED: 'red', - FAILED_RETRYABLE: 'orange', - FAILED_FINAL: 'red', + COMPLETED: 'green', QUEUED: 'default', CLAIMED: 'blue', + DOWNLOADING: 'processing', PARSING: 'processing', CHUNKING: 'processing', + EMBEDDING: 'processing', INDEXING: 'processing', GENERATING_CANDIDATES: 'processing', + FAILED: 'red', FAILED_RETRYABLE: 'orange', CANCELLED: 'default', } const columns = [ - { title: '名称', dataIndex: 'sourceName', width: 180, ellipsis: true, sorter: true, sortOrder: sortBy === 'sourceName' ? (sortOrder as any) : undefined, - render: (v: string, r: any) => ( - - {v} - {r.sourceDeleted && 源已删} - - ), - }, - { 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) => {importStatusLabels[s] || s} }, - { title: '进度', dataIndex: 'progress', width: 70, sorter: true, sortOrder: sortBy === 'progress' ? (sortOrder as any) : undefined, render: (v: number) => `${v || 0}%` }, - { title: '重试', dataIndex: 'retryCount', width: 60, align: 'center' as const, sorter: true, sortOrder: sortBy === 'retryCount' ? (sortOrder as any) : undefined }, + { title: '名称', dataIndex: 'sourceName', width: 180, ellipsis: true, sorter: true, sortOrder: sortBy === 'sourceName' ? (sortOrder as 'ascend' | 'descend') : undefined, + render: (v: string, r: ImportJob) => ( + { setSelectedJob(r); setDrawerOpen(true) }} style={{ fontSize: 12 }}>{v || '—'} + ) }, + { title: '类型', dataIndex: 'sourceType', width: 80, sorter: true, sortOrder: sortBy === 'sourceType' ? (sortOrder as 'ascend' | 'descend') : undefined, render: (s: string) => (s === 'file' ? '文件' : s === 'url' ? '链接' : s === 'text' ? '文本' : s) }, + { title: '状态', dataIndex: 'status', width: 100, sorter: true, sortOrder: sortBy === 'status' ? (sortOrder as 'ascend' | 'descend') : undefined, render: (s: string) => {importStatusLabels[s] || s} }, + { title: '进度', dataIndex: 'progress', width: 70, sorter: true, sortOrder: sortBy === 'progress' ? (sortOrder as 'ascend' | 'descend') : undefined, render: (v: number) => `${v || 0}%` }, + { title: '重试', dataIndex: 'retryCount', width: 60, align: 'center' as const, sorter: true, sortOrder: sortBy === 'retryCount' ? (sortOrder as 'ascend' | 'descend') : undefined }, { title: '错误', dataIndex: 'errorMessage', width: 180, ellipsis: true, render: (v: string) => v ? {v} : '-' }, - { title: '时间', dataIndex: 'createdAt', width: 130, sorter: true, sortOrder: sortBy === 'createdAt' ? (sortOrder as any) : undefined, render: (d: string) => dayjs(d).format('MM-DD HH:mm') }, - { - title: '操作', width: 100, - render: (_: any, r: any) => ( - + { title: '时间', dataIndex: 'createdAt', width: 130, sorter: true, sortOrder: sortBy === 'createdAt' ? (sortOrder as 'ascend' | 'descend') : undefined, render: (d: string) => dayjs(d).format('MM-DD HH:mm') }, + { title: '操作', width: 160, + render: (_: unknown, r: ImportJob) => ( + - {(r.status?.startsWith('FAILED') || r.status === 'CLAIMED' || r.status === 'QUEUED') && } + - ), - }, + ) }, ] - const stepCols = [ + const stepColumns = [ { title: '步骤', dataIndex: 'step', width: 120, render: (s: string) => importStepLabels[s] || importStatusLabels[s] || s }, { title: '状态', dataIndex: 'status', width: 80, render: (s: string) => {importStepLabels[s] || importStatusLabels[s] || s} }, { title: '详情', dataIndex: 'detail', ellipsis: true }, @@ -186,99 +167,56 @@ export default function ImportMonitorPage() { { title: '完成', dataIndex: 'completedAt', width: 130, render: (d: string) => d ? dayjs(d).format('HH:mm:ss') : '-' }, ] - const kbOptions = (kbData?.items || []).map((kb: any) => ({ + const kbOptions = (kbData?.items || []).map((kb: KnowledgeBase) => ({ value: kb.id, label: formatKbLabel(kb), })) return ( -
-
- <ImportOutlined /> 文档导入 +
+
+ <ImportOutlined /> 文档导入监控 - +
- - + - + setShowDeleted(e.target.checked)}>显示已删除源 + + {selectedRowKeys.length > 0 && } - setSelectedRowKeys(keys) }} + pagination={false} size="small" - rowSelection={{ - selectedRowKeys, - onChange: (keys) => setSelectedRowKeys(keys), - }} - onChange={(_pagination, _filters, sorter: any) => { - if (sorter.order) { - setSortBy(sorter.field || 'createdAt') - setSortOrder(sorter.order) - } else { - setSortBy('createdAt') - setSortOrder('descend') - } + onChange={(_pagination: TablePaginationConfig, _filters: Record, sorter: SorterResult | SorterResult[]) => { + const s = Array.isArray(sorter) ? sorter[0] : sorter + if (s.columnKey) setSortBy(String(s.columnKey)) + setSortOrder(s.order || 'descend') }} /> - - setDrawerOpen(false)} width={700}> - {detail && ( + { setDrawerOpen(false); setSelectedJob(null) }} width={700}> + {detail ? ( <> - Job: {detail.job?.id} - 状态: {importStatusLabels[detail.job?.status] || detail.job?.status} - 进度: {detail.job?.progress || 0}% - 重试: {detail.job?.retryCount}/{detail.job?.maxRetries} - {detail.job?.errorMessage &&
{detail.job?.errorMessage}
} - 步骤日志 ({detail.steps?.length || 0}) -
+ + {importStatusLabels[detail.status] || detail.status} + 进度: {detail.progress || 0}% + 重试: {detail.retryCount} + 知识库: {detail.kbName || detail.kbId?.slice(0, 8)} + 创建: {dayjs(detail.createdAt).format('YYYY-MM-DD HH:mm:ss')} + + {detail.errorMessage && 错误: {detail.errorMessage}} + 步骤日志 +
- )} + ) : 加载中...} ) diff --git a/src/pages/KnowledgeBases.tsx b/src/pages/KnowledgeBases.tsx index c2e4501..7d6d592 100644 --- a/src/pages/KnowledgeBases.tsx +++ b/src/pages/KnowledgeBases.tsx @@ -23,14 +23,14 @@ function KBPage() { const { data: sources } = useQuery({ queryKey: ['kb-sources', selectedKb?.id], - queryFn: (): Promise => knowledgeAPI.sources(selectedKb?.id || ''), + queryFn: () => knowledgeAPI.sources(selectedKb?.id || ''), enabled: !!selectedKb?.id, }) const [refSourceId, setRefSourceId] = useState(null) const { data: references } = useQuery({ queryKey: ['source-refs', refSourceId], - queryFn: (): Promise => knowledgeAPI.sourceReferences(refSourceId || ''), + queryFn: () => knowledgeAPI.sourceReferences(refSourceId || ''), enabled: !!refSourceId, }) diff --git a/src/pages/KnowledgeOps.tsx b/src/pages/KnowledgeOps.tsx index 53e3a2a..b11809d 100644 --- a/src/pages/KnowledgeOps.tsx +++ b/src/pages/KnowledgeOps.tsx @@ -16,12 +16,12 @@ export default function KnowledgeOpsPage() { const { data: candidates } = useQuery({ queryKey: ['ops', 'candidates'], - queryFn: (): Promise => knowledgeAPI.candidates(), + queryFn: () => knowledgeAPI.candidates(), }) const { data: chunks } = useQuery({ queryKey: ['ops', 'chunks', sourceId], - queryFn: (): Promise => knowledgeAPI.chunks(sourceId), + queryFn: () => knowledgeAPI.chunks(sourceId), enabled: !!sourceId, }) diff --git a/src/pages/MemberManagement.tsx b/src/pages/MemberManagement.tsx index 45e2893..e3a31d2 100644 --- a/src/pages/MemberManagement.tsx +++ b/src/pages/MemberManagement.tsx @@ -13,9 +13,9 @@ export default function MemberManagement() { const [addOpen, setAddOpen] = useState(false) const [form] = Form.useForm() - const { data: usersData } = useQuery({ queryKey: ['users', 'list'], queryFn: (): Promise => membersAPI.list() }) - const { data: memberships } = useQuery({ queryKey: ['users', 'memberships'], queryFn: (): Promise => membersAPI.memberships() }) - const { data: deletions } = useQuery({ queryKey: ['users', 'deletions'], queryFn: (): Promise => membersAPI.deletionRequests() }) + const { data: usersData } = useQuery({ queryKey: ['users', 'list'], queryFn: () => membersAPI.list() }) + const { data: memberships } = useQuery({ queryKey: ['users', 'memberships'], queryFn: () => membersAPI.memberships() }) + const { data: deletions } = useQuery({ queryKey: ['users', 'deletions'], queryFn: () => membersAPI.deletionRequests() }) const assignMembership = async (v: any) => { await membersAPI.updateMembership({ userId: v.userId, planId: v.planId, expiresAt: v.expiresAt?.toISOString() }) diff --git a/src/pages/Membership.tsx b/src/pages/Membership.tsx index e19f19d..630eb40 100644 --- a/src/pages/Membership.tsx +++ b/src/pages/Membership.tsx @@ -13,9 +13,9 @@ function MembershipPage() { const [addOpen, setAddOpen] = useState(false) const [newPlan, setNewPlan] = useState({ name: '', code: '', priceMonthly: 0, monthlyChatCount: 10 }) - const { data: plans } = useQuery({ queryKey: ['quota', 'plans'], queryFn: (): Promise => quotaAPI.plans() }) - const { data: memberships } = useQuery({ queryKey: ['quota', 'memberships'], queryFn: (): Promise => quotaAPI.memberships() }) - const { data: costs } = useQuery({ queryKey: ['quota', 'costs'], queryFn: (): Promise => quotaAPI.costs() }) + const { data: plans } = useQuery({ queryKey: ['quota', 'plans'], queryFn: () => quotaAPI.plans() }) + const { data: memberships } = useQuery({ queryKey: ['quota', 'memberships'], queryFn: () => quotaAPI.memberships() }) + const { data: costs } = useQuery({ queryKey: ['quota', 'costs'], queryFn: () => quotaAPI.costs() }) const addPlan = async () => { await quotaAPI.createPlan(newPlan) diff --git a/src/pages/Metrics.tsx b/src/pages/Metrics.tsx index 0b6810b..f951a4f 100644 --- a/src/pages/Metrics.tsx +++ b/src/pages/Metrics.tsx @@ -9,11 +9,11 @@ const { Title } = Typography function MetricsPage() { const qc = useQueryClient() - const { data: overview } = useQuery({ queryKey: ['metrics', 'overview'], queryFn: (): Promise => metricsAPI.overview(), staleTime: 10_000 }) - const { data: top } = useQuery({ queryKey: ['metrics', 'top'], queryFn: (): Promise => metricsAPI.top(), staleTime: 10_000 }) - const { data: recent } = useQuery({ queryKey: ['metrics', 'recent'], queryFn: (): Promise => metricsAPI.recent(), staleTime: 5_000 }) - const { data: ai } = useQuery({ queryKey: ['metrics', 'ai'], queryFn: (): Promise => metricsAPI.ai(), staleTime: 10_000 }) - const { data: worker } = useQuery({ queryKey: ['metrics', 'worker'], queryFn: (): Promise => metricsAPI.worker(), staleTime: 10_000 }) + const { data: overview } = useQuery({ queryKey: ['metrics', 'overview'], queryFn: () => metricsAPI.overview(), staleTime: 10_000 }) + const { data: top } = useQuery({ queryKey: ['metrics', 'top'], queryFn: () => metricsAPI.top(), staleTime: 10_000 }) + const { data: recent } = useQuery({ queryKey: ['metrics', 'recent'], queryFn: () => metricsAPI.recent(), staleTime: 5_000 }) + const { data: ai } = useQuery({ queryKey: ['metrics', 'ai'], queryFn: () => metricsAPI.ai(), staleTime: 10_000 }) + const { data: worker } = useQuery({ queryKey: ['metrics', 'worker'], queryFn: () => metricsAPI.worker(), staleTime: 10_000 }) const topCols = [ { title: '接口', dataIndex: 'path', width: 300, ellipsis: true }, diff --git a/src/pages/Secrets.tsx b/src/pages/Secrets.tsx index e0325a7..39d1c2b 100644 --- a/src/pages/Secrets.tsx +++ b/src/pages/Secrets.tsx @@ -16,9 +16,9 @@ function SecretsPage() { const [form, setForm] = useState({ name: '', provider: 'deepseek', value: '', expiresAt: '' }) const [billForm, setBillForm] = useState({ provider: 'deepseek', billMonth: dayjs().format('YYYY-MM'), amount: 0, notes: '' }) - const { data: secrets } = useQuery({ queryKey: ['secrets'], queryFn: (): Promise => secretsAPI.list() }) - const { data: logs } = useQuery({ queryKey: ['secrets', 'logs'], queryFn: (): Promise => secretsAPI.logs() }) - const { data: bills } = useQuery({ queryKey: ['vendor', 'bills'], queryFn: (): Promise => secretsAPI.vendorBills() }) + const { data: secrets } = useQuery({ queryKey: ['secrets'], queryFn: () => secretsAPI.list() }) + const { data: logs } = useQuery({ queryKey: ['secrets', 'logs'], queryFn: () => secretsAPI.logs() }) + const { data: bills } = useQuery({ queryKey: ['vendor', 'bills'], queryFn: () => secretsAPI.vendorBills() }) const addSecret = async () => { await secretsAPI.create(form) diff --git a/src/pages/VectorAdmin.tsx b/src/pages/VectorAdmin.tsx index 1f66dc6..0c6c1f6 100644 --- a/src/pages/VectorAdmin.tsx +++ b/src/pages/VectorAdmin.tsx @@ -11,13 +11,13 @@ export default function VectorAdminPage() { const { data: coll, isLoading: collLoading } = useQuery({ queryKey: ['vector', 'collection'], - queryFn: (): Promise => vectorAPI.collection(), + queryFn: () => vectorAPI.collection(), staleTime: 30_000, }) const { data: count } = useQuery({ queryKey: ['vector', 'count'], - queryFn: (): Promise => vectorAPI.count(), + queryFn: () => vectorAPI.count(), staleTime: 30_000, }) diff --git a/src/pages/learning/SessionPage.tsx b/src/pages/learning/SessionPage.tsx index f5f8ba6..3499395 100644 --- a/src/pages/learning/SessionPage.tsx +++ b/src/pages/learning/SessionPage.tsx @@ -82,7 +82,7 @@ export default function SessionPage() { {detail.endedAt ? dayjs(detail.endedAt).format('YYYY-MM-DD HH:mm:ss') : '-'} {detail.lastEventAt ? dayjs(detail.lastEventAt).format('YYYY-MM-DD HH:mm:ss') : '-'} {detail.eventCount ?? '-'} - {detail.durationBreakdown || '-'} + {String(detail.durationBreakdown ?? '-')} )} {detail?.clientSessionId && ( diff --git a/src/stores/index.ts b/src/stores/index.ts new file mode 100644 index 0000000..777968d --- /dev/null +++ b/src/stores/index.ts @@ -0,0 +1 @@ +export { useUIStore } from './ui-store' diff --git a/src/stores/ui-store.ts b/src/stores/ui-store.ts new file mode 100644 index 0000000..638be16 --- /dev/null +++ b/src/stores/ui-store.ts @@ -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()( + persist( + (set) => ({ + sidebarCollapsed: false, + toggleSidebar: () => set((s) => ({ sidebarCollapsed: !s.sidebarCollapsed })), + setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }), + }), + { + name: 'admin-ui-prefs', + partialize: (state) => ({ sidebarCollapsed: state.sidebarCollapsed }), + }, + ), +) diff --git a/src/types/ai.ts b/src/types/ai.ts new file mode 100644 index 0000000..d917eaa --- /dev/null +++ b/src/types/ai.ts @@ -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 +} diff --git a/src/types/billing.ts b/src/types/billing.ts new file mode 100644 index 0000000..e873510 --- /dev/null +++ b/src/types/billing.ts @@ -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 +} diff --git a/src/types/chat.ts b/src/types/chat.ts new file mode 100644 index 0000000..4ba51f2 --- /dev/null +++ b/src/types/chat.ts @@ -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 +} diff --git a/src/types/compliance.ts b/src/types/compliance.ts new file mode 100644 index 0000000..3702f2d --- /dev/null +++ b/src/types/compliance.ts @@ -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 +} diff --git a/src/types/config.ts b/src/types/config.ts new file mode 100644 index 0000000..c8ef87e --- /dev/null +++ b/src/types/config.ts @@ -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 +} diff --git a/src/types/events.ts b/src/types/events.ts new file mode 100644 index 0000000..3c5fa5d --- /dev/null +++ b/src/types/events.ts @@ -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 + queue?: string + status?: string + attempts?: number + createdAt?: string +} + +export interface QueueOverview { + queues: QueueInfo[] +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..b562c07 --- /dev/null +++ b/src/types/index.ts @@ -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' diff --git a/src/types/knowledge.ts b/src/types/knowledge.ts new file mode 100644 index 0000000..773c6e0 --- /dev/null +++ b/src/types/knowledge.ts @@ -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 +} diff --git a/src/types/learning.ts b/src/types/learning.ts index cf5da19..a4d1bbc 100644 --- a/src/types/learning.ts +++ b/src/types/learning.ts @@ -17,6 +17,8 @@ export interface ReadingEvent { appVersion?: string; status: string; errorCode?: string; + errorMessage?: string; + retryCount?: number; warningCodes?: unknown; serverReceivedAt: string; processedAt?: string; @@ -38,8 +40,17 @@ export interface LearningSession { totalActiveSeconds: number; clientSessionId?: string; materialId?: string; + materialName?: string; readingTargetType?: string; + targetType?: string; lastPosition?: unknown; + userName?: string; + kbName?: string; + interrupted?: boolean; + missingMaterialClosed?: boolean; + abnormalDelta?: number; + eventCount?: number; + durationBreakdown?: unknown; lastEventAt?: string; createdAt: string; } @@ -98,10 +109,11 @@ export interface LearningRecord { // ── Dashboard ── export interface AdminDashboard { - overview: { totalEvents: number; todayEvents: number; failedEvents: number; duplicateEvents: number }; + overview: { totalEvents: number; todayEvents: number; failedEvents: number; duplicateEvents: number; warningEvents?: number; totalActiveSeconds?: number }; sessions: { active: number; interrupted: number; completed: number; total: number }; users: { activeToday: number; totalWithEvents: number }; materials: { totalRead: number; totalMarkedRead: number }; + recentAnomalies?: Array<{ eventId: string; eventType: string; errorCode?: string; createdAt: string }>; } // ── Anomalies ── diff --git a/src/types/review.ts b/src/types/review.ts new file mode 100644 index 0000000..cdc1d65 --- /dev/null +++ b/src/types/review.ts @@ -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 +} diff --git a/src/types/safety.ts b/src/types/safety.ts new file mode 100644 index 0000000..a80d7d9 --- /dev/null +++ b/src/types/safety.ts @@ -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 +} diff --git a/src/types/secret.ts b/src/types/secret.ts new file mode 100644 index 0000000..b262441 --- /dev/null +++ b/src/types/secret.ts @@ -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 +} diff --git a/src/types/server.ts b/src/types/server.ts new file mode 100644 index 0000000..5cf9584 --- /dev/null +++ b/src/types/server.ts @@ -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[] +} diff --git a/src/types/ui.ts b/src/types/ui.ts new file mode 100644 index 0000000..307f0b1 --- /dev/null +++ b/src/types/ui.ts @@ -0,0 +1,40 @@ +import type { ReactNode } from 'react' + +// ── 通用表格列类型(替代 (_: any, r: any))── + +export interface TypedColumn { + 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 { + open: boolean + editing?: T +} + +export interface DrawerControl { + open: boolean + selected?: T +} diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 0000000..48a789d --- /dev/null +++ b/src/types/user.ts @@ -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 +} diff --git a/src/types/vector.ts b/src/types/vector.ts new file mode 100644 index 0000000..de89f67 --- /dev/null +++ b/src/types/vector.ts @@ -0,0 +1,22 @@ +// ── 向量检索 ── + +export interface VectorSearchResult { + id: string + score: number + content?: string + sourceId?: string + sourceName?: string + metadata?: Record +} + +export interface VectorDebugQuery { + query: string + limit?: number + collection?: string +} + +export interface VectorDebugResponse { + results: VectorSearchResult[] + latency?: number + collection?: string +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..a07f26b --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + /** API 后端地址(仅本地开发使用,生产通过 Nginx 代理) */ + readonly VITE_API_BASE_URL?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/tsconfig.app.json b/tsconfig.app.json index a358e37..3f53ce4 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -15,6 +15,11 @@ "noEmit": true, "jsx": "react-jsx", + /* Strict — 渐进式收紧 */ + "strict": true, + "noImplicitAny": false, + "strictNullChecks": false, + /* Linting */ "baseUrl": ".", "paths": { "@/*": ["src/*"] },