Files
xk-ai-agent/internal/handler/kb_crawl_handler.go

238 lines
7.8 KiB
Go
Raw Permalink Normal View History

2026-08-14 21:50:48 +08:00
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})
}