@@ -36,6 +36,19 @@ import {
3636import { registry } from '../../../backend/registry.js' ;
3737import { sendRestartSignal , sendStopSignal , isUnderSupervisor , getVncInfo } from '../../../utils/ipc.js' ;
3838import { getTodayStats , getStatsRange , clearStatsRange } from '../../../utils/stats.js' ;
39+ import {
40+ getList as getHistoryList ,
41+ getDetail as getHistoryDetail ,
42+ deleteRecords as deleteHistoryRecords ,
43+ deleteByDateRange as deleteHistoryByDateRange ,
44+ retryMediaDownload ,
45+ getStats as getHistoryStats ,
46+ getModelList as getHistoryModelList ,
47+ getMediaDir
48+ } from '../../../utils/history.js' ;
49+ import path from 'path' ;
50+ import fs from 'fs/promises' ;
51+ import { useContextDownload } from '../../../backend/utils/download.js' ;
3952
4053/**
4154 * 读取请求体
@@ -458,6 +471,148 @@ export function createAdminRouter(context) {
458471 return ;
459472 }
460473
474+ // ==================== 请求历史 ====================
475+
476+ // GET /admin/history - 历史记录列表
477+ if ( method === 'GET' && pathname === '/history' ) {
478+ const url = new URL ( req . url , `http://${ req . headers . host } ` ) ;
479+ const page = parseInt ( url . searchParams . get ( 'page' ) || '1' , 10 ) ;
480+ const pageSize = parseInt ( url . searchParams . get ( 'pageSize' ) || '20' , 10 ) ;
481+ const filters = {
482+ status : url . searchParams . get ( 'status' ) || null ,
483+ modelId : url . searchParams . get ( 'model' ) || null ,
484+ search : url . searchParams . get ( 'search' ) || null ,
485+ startDate : url . searchParams . get ( 'startDate' ) || null ,
486+ endDate : url . searchParams . get ( 'endDate' ) || null
487+ } ;
488+
489+ const result = getHistoryList ( filters , page , pageSize ) ;
490+ sendJson ( res , 200 , result ) ;
491+ return ;
492+ }
493+
494+ // GET /admin/history/stats - 历史统计摘要
495+ if ( method === 'GET' && pathname === '/history/stats' ) {
496+ const url = new URL ( req . url , `http://${ req . headers . host } ` ) ;
497+ const filters = {
498+ startDate : url . searchParams . get ( 'startDate' ) || null ,
499+ endDate : url . searchParams . get ( 'endDate' ) || null
500+ } ;
501+
502+ const stats = getHistoryStats ( filters ) ;
503+ sendJson ( res , 200 , stats ) ;
504+ return ;
505+ }
506+
507+ // GET /admin/history/models - 获取历史中使用过的模型列表
508+ if ( method === 'GET' && pathname === '/history/models' ) {
509+ const models = getHistoryModelList ( ) ;
510+ sendJson ( res , 200 , models ) ;
511+ return ;
512+ }
513+
514+ // GET /admin/history/media/:filename - 静态媒体文件服务
515+ if ( method === 'GET' && pathname . startsWith ( '/history/media/' ) ) {
516+ const filename = pathname . replace ( '/history/media/' , '' ) ;
517+ if ( ! filename || filename . includes ( '..' ) || filename . includes ( '/' ) ) {
518+ sendApiError ( res , { code : ERROR_CODES . INVALID_REQUEST_BODY , message : '无效的文件名' } ) ;
519+ return ;
520+ }
521+
522+ const mediaDir = getMediaDir ( ) ;
523+ const filePath = path . join ( mediaDir , filename ) ;
524+
525+ try {
526+ const data = await fs . readFile ( filePath ) ;
527+ const ext = path . extname ( filename ) . toLowerCase ( ) ;
528+ const mimeTypes = {
529+ '.png' : 'image/png' ,
530+ '.jpg' : 'image/jpeg' ,
531+ '.jpeg' : 'image/jpeg' ,
532+ '.gif' : 'image/gif' ,
533+ '.webp' : 'image/webp' ,
534+ '.mp4' : 'video/mp4' ,
535+ '.webm' : 'video/webm'
536+ } ;
537+ res . writeHead ( 200 , {
538+ 'Content-Type' : mimeTypes [ ext ] || 'application/octet-stream' ,
539+ 'Content-Length' : data . length ,
540+ 'Cache-Control' : 'public, max-age=31536000'
541+ } ) ;
542+ res . end ( data ) ;
543+ } catch ( e ) {
544+ sendApiError ( res , { code : ERROR_CODES . NOT_FOUND , message : '文件不存在' , status : 404 } ) ;
545+ }
546+ return ;
547+ }
548+
549+ // GET /admin/history/:id - 单条记录详情
550+ const historyDetailMatch = pathname . match ( / ^ \/ h i s t o r y \/ ( [ ^ / ] + ) $ / ) ;
551+ if ( method === 'GET' && historyDetailMatch && ! pathname . includes ( '/retry-media' ) ) {
552+ const id = historyDetailMatch [ 1 ] ;
553+ const record = getHistoryDetail ( id ) ;
554+ if ( record ) {
555+ sendJson ( res , 200 , record ) ;
556+ } else {
557+ sendApiError ( res , { code : ERROR_CODES . NOT_FOUND , message : '记录不存在' , status : 404 } ) ;
558+ }
559+ return ;
560+ }
561+
562+ // POST /admin/history/:id/retry-media - 重试下载媒体
563+ const retryMediaMatch = pathname . match ( / ^ \/ h i s t o r y \/ ( [ ^ / ] + ) \/ r e t r y - m e d i a $ / ) ;
564+ if ( method === 'POST' && retryMediaMatch ) {
565+ const id = retryMediaMatch [ 1 ] ;
566+ const body = await readBody ( req ) ;
567+ const mediaIndex = body . mediaIndex ?? 0 ;
568+
569+ // 使用 Pool 的浏览器下载(如果可用)
570+ let downloadFn = null ;
571+ try {
572+ const poolContext = queueManager ?. getPoolContext ?. ( ) ;
573+ const page = poolContext ?. getFirstPage ?. ( ) ;
574+ if ( page ) {
575+ const imgDlCfg = config ?. backend ?. pool ?. failover || { } ;
576+ downloadFn = ( url ) => useContextDownload ( url , page , {
577+ retries : imgDlCfg . imgDlRetry ? ( imgDlCfg . imgDlRetryMaxRetries || 3 ) : 1
578+ } ) ;
579+ }
580+ } catch { /* Pool 未初始化,使用后备方案 */ }
581+
582+ const result = await retryMediaDownload ( id , mediaIndex , downloadFn ) ;
583+ if ( result . success ) {
584+ sendJson ( res , 200 , result ) ;
585+ } else {
586+ sendApiError ( res , { code : ERROR_CODES . INTERNAL_ERROR , message : result . message } ) ;
587+ }
588+ return ;
589+ }
590+
591+ // DELETE /admin/history - 批量删除记录
592+ if ( method === 'DELETE' && pathname === '/history' ) {
593+ const url = new URL ( req . url , `http://${ req . headers . host } ` ) ;
594+ const startDate = url . searchParams . get ( 'startDate' ) ;
595+ const endDate = url . searchParams . get ( 'endDate' ) ;
596+
597+ // 支持按��期范围删除
598+ if ( startDate && endDate ) {
599+ const deleted = await deleteHistoryByDateRange ( startDate , endDate ) ;
600+ sendJson ( res , 200 , { success : true , deleted } ) ;
601+ return ;
602+ }
603+
604+ // 支持按 ID 列表删除
605+ const body = await readBody ( req ) ;
606+ if ( body . ids && Array . isArray ( body . ids ) ) {
607+ const deleted = await deleteHistoryRecords ( body . ids ) ;
608+ sendJson ( res , 200 , { success : true , deleted } ) ;
609+ return ;
610+ }
611+
612+ sendApiError ( res , { code : ERROR_CODES . INVALID_REQUEST_BODY , message : '缺少 ids 数组或日期范围参数' } ) ;
613+ return ;
614+ }
615+
461616 // 404
462617 res . writeHead ( 404 ) ;
463618 res . end ( JSON . stringify ( { error : 'Not Found' } ) ) ;
0 commit comments