Files
xk-ai-agent/internal/handler/kb_crawl_handler.go
2026-08-14 21:50:48 +08:00

238 lines
7.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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})
}