package handler import ( "net/http" "strconv" "tcm-agent/internal/crawler" "tcm-agent/internal/dao" "tcm-agent/internal/service" "github.com/gin-gonic/gin" ) // ======================================================================== // KBCrawlHandler —— 药品抓取任务管理 API // ======================================================================== // 暴露 7 个端点(前缀 /api/v1/kb/admin/crawl,复用 KB 管理鉴权: // 口令头或面板 JWT 均可): // // GET /sources 可用抓取源列表(前端下拉) // GET /tasks 任务列表(含实时 running 标记) // POST /tasks 创建任务 // PUT /tasks/:id 更新任务(名称/目标库/调度/限量/启停) // DELETE /tasks/:id 软删除任务 // POST /tasks/:id/run 立即抓取(异步,立即返回) // GET /tasks/:id/logs 任务运行历史(最近 N 条) // ======================================================================== // KBCrawlHandler 抓取任务处理器(无状态,直接调 dao/service) type KBCrawlHandler struct{} // NewKBCrawlHandler 构造 func NewKBCrawlHandler() *KBCrawlHandler { return &KBCrawlHandler{} } // ListSources 可用抓取源 // GET /api/v1/kb/admin/crawl/sources func (h *KBCrawlHandler) ListSources(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 200, "data": crawler.ListSources()}) } // ListTasks 任务列表 // GET /api/v1/kb/admin/crawl/tasks // // 每行附加 running 字段(内存实时状态)——DB 的 last_status 有落库延迟, // 前端旋转图标要跟内存状态走 func (h *KBCrawlHandler) ListTasks(c *gin.Context) { rows, err := dao.KBCrawlTaskList() if err != nil { c.JSON(http.StatusOK, gin.H{"code": 500, "message": err.Error()}) return } out := make([]gin.H, 0, len(rows)) for _, t := range rows { out = append(out, gin.H{ "id": t.ID, "name": t.Name, "source": t.Source, "library_id": t.LibraryID, "schedule_type": t.ScheduleType, "interval_hours": t.IntervalHours, "run_at_hour": t.RunAtHour, "run_at_weekday": t.RunAtWeekday, "items_per_run": t.ItemsPerRun, "progress_offset": t.ProgressOffset, "status": t.Status, "last_run_at": t.LastRunAt, "last_status": t.LastStatus, "last_message": t.LastMessage, "created_at": t.CreatedAt, "running": service.CrawlTaskRunning(t.ID), }) } c.JSON(http.StatusOK, gin.H{"code": 200, "data": out}) } // crawlTaskRequest 创建/更新任务的入参 type crawlTaskRequest struct { Name string `json:"name"` Source string `json:"source"` LibraryID uint `json:"library_id"` ScheduleType string `json:"schedule_type"` IntervalHours int `json:"interval_hours"` RunAtHour int `json:"run_at_hour"` RunAtWeekday int `json:"run_at_weekday"` ItemsPerRun int `json:"items_per_run"` Status *int `json:"status"` // 指针区分"没传"和"传了 0(禁用)" } // normalize 归一化 + 校验入参(创建和更新共用) func (r *crawlTaskRequest) normalize() string { if r.Name == "" { return "任务名称不能为空" } if r.Source == "" { r.Source = "zhongyoo" } if _, ok := crawler.GetSource(r.Source); !ok { return "抓取源不存在: " + r.Source } if r.LibraryID == 0 { return "请选择目标知识库" } switch r.ScheduleType { case "", "manual": r.ScheduleType = "manual" case "interval": if r.IntervalHours < 1 { r.IntervalHours = 24 } case "daily", "weekly": if r.RunAtHour < 0 || r.RunAtHour > 23 { r.RunAtHour = 3 } if r.RunAtWeekday < 0 || r.RunAtWeekday > 6 { r.RunAtWeekday = 1 } default: return "调度类型不合法: " + r.ScheduleType } if r.ItemsPerRun < 1 || r.ItemsPerRun > 500 { r.ItemsPerRun = 50 } return "" } // CreateTask 创建任务 // POST /api/v1/kb/admin/crawl/tasks func (h *KBCrawlHandler) CreateTask(c *gin.Context) { var req crawlTaskRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"code": 400, "message": "参数格式错误: " + err.Error()}) return } if msg := req.normalize(); msg != "" { c.JSON(http.StatusOK, gin.H{"code": 400, "message": msg}) return } // 目标库必须真实存在(防手滑填错 ID,跑的时候才发现) if _, err := dao.KBGetLibrary(req.LibraryID); err != nil { c.JSON(http.StatusOK, gin.H{"code": 400, "message": "目标知识库不存在,请先在知识库管理里创建"}) return } row := &dao.KBCrawlTaskRow{ Name: req.Name, Source: req.Source, LibraryID: req.LibraryID, ScheduleType: req.ScheduleType, IntervalHours: req.IntervalHours, RunAtHour: req.RunAtHour, RunAtWeekday: req.RunAtWeekday, ItemsPerRun: req.ItemsPerRun, Status: 1, } if err := dao.KBCrawlTaskCreate(row); err != nil { c.JSON(http.StatusOK, gin.H{"code": 500, "message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 200, "data": row, "message": "任务已创建"}) } // UpdateTask 更新任务 // PUT /api/v1/kb/admin/crawl/tasks/:id func (h *KBCrawlHandler) UpdateTask(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { c.JSON(http.StatusOK, gin.H{"code": 400, "message": err.Error()}) return } var req crawlTaskRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"code": 400, "message": "参数格式错误: " + err.Error()}) return } if msg := req.normalize(); msg != "" { c.JSON(http.StatusOK, gin.H{"code": 400, "message": msg}) return } if _, err := dao.KBGetLibrary(req.LibraryID); err != nil { c.JSON(http.StatusOK, gin.H{"code": 400, "message": "目标知识库不存在"}) return } fields := map[string]any{ "name": req.Name, "source": req.Source, "library_id": req.LibraryID, "schedule_type": req.ScheduleType, "interval_hours": req.IntervalHours, "run_at_hour": req.RunAtHour, "run_at_weekday": req.RunAtWeekday, "items_per_run": req.ItemsPerRun, } if req.Status != nil { fields["status"] = *req.Status } if err := dao.KBCrawlTaskUpdate(id, fields); err != nil { c.JSON(http.StatusOK, gin.H{"code": 500, "message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 200, "message": "任务已更新"}) } // DeleteTask 软删除任务 // DELETE /api/v1/kb/admin/crawl/tasks/:id func (h *KBCrawlHandler) DeleteTask(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { c.JSON(http.StatusOK, gin.H{"code": 400, "message": err.Error()}) return } if service.CrawlTaskRunning(id) { c.JSON(http.StatusOK, gin.H{"code": 400, "message": "任务正在运行中,等本次跑完再删除"}) return } if err := dao.KBCrawlTaskDelete(id); err != nil { c.JSON(http.StatusOK, gin.H{"code": 500, "message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 200, "message": "任务已删除"}) } // RunTask 立即抓取(异步触发) // POST /api/v1/kb/admin/crawl/tasks/:id/run func (h *KBCrawlHandler) RunTask(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { c.JSON(http.StatusOK, gin.H{"code": 400, "message": err.Error()}) return } if err := service.TriggerCrawlTask(id, "manual"); err != nil { c.JSON(http.StatusOK, gin.H{"code": 400, "message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "code": 200, "message": "抓取已在后台启动,可在运行历史里查看进度(每批约需 1-3 分钟)", }) } // ListLogs 任务运行历史 // GET /api/v1/kb/admin/crawl/tasks/:id/logs?limit=20 func (h *KBCrawlHandler) ListLogs(c *gin.Context) { id, err := parseUintParam(c, "id") if err != nil { c.JSON(http.StatusOK, gin.H{"code": 400, "message": err.Error()}) return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) rows, err := dao.KBCrawlLogList(id, limit) if err != nil { c.JSON(http.StatusOK, gin.H{"code": 500, "message": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 200, "data": rows}) }