Files
agent/backend/internal/logic/dataset/handlers.go

431 lines
11 KiB
Go

package dataset
import (
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"senlinai-agent/backend/internal/httpx"
"senlinai-agent/backend/internal/logic/auth"
"senlinai-agent/backend/internal/models"
)
type Handler struct {
service *Service
}
type SourceDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
URL string `json:"url"`
IconURL string `json:"iconUrl"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
BuiltIn bool `json:"builtIn"`
LastSyncedAt *time.Time `json:"lastSyncedAt"`
ItemCount int64 `json:"itemCount"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type ItemDTO struct {
ID string `json:"id"`
SourceID string `json:"sourceId"`
Title string `json:"title"`
Summary string `json:"summary"`
Content string `json:"content"`
URL string `json:"url"`
Status string `json:"status"`
Starred bool `json:"starred"`
PublishedAt *time.Time `json:"publishedAt"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type ItemPageDTO struct {
Items []ItemDTO `json:"items"`
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
type CronDTO struct {
ID string `json:"id"`
SourceID string `json:"sourceId"`
Status string `json:"status"`
LastRunAt *time.Time `json:"lastRunAt"`
LastResult string `json:"lastResult"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type DepositDTO struct {
NoteID string `json:"noteId"`
ProjectID string `json:"projectId"`
}
type sourceRequest struct {
Name string `json:"name"`
Kind string `json:"kind"`
URL string `json:"url"`
IconURL string `json:"iconUrl"`
Description string `json:"description"`
Enabled *bool `json:"enabled"`
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
func (h *Handler) Register(router gin.IRouter) {
router.GET("/dataset-sources", h.listSources)
router.POST("/dataset-sources", h.createSource)
router.PATCH("/dataset-sources/:id", h.updateSource)
router.DELETE("/dataset-sources/:id", h.deleteSource)
router.GET("/dataset-items", h.listItems)
router.POST("/dataset-items", h.createItem)
router.PATCH("/dataset-items/:id", h.updateItem)
router.POST("/dataset-items/:id/deposit", h.depositItem)
router.GET("/dataset-crons", h.listCrons)
router.POST("/dataset-crons/sync", h.queueSync)
}
func (h *Handler) listSources(c *gin.Context) {
userID, ok := currentUser(c)
if !ok {
return
}
records, err := h.service.ListSources(userID)
if err != nil {
writeError(c, err)
return
}
items := make([]SourceDTO, 0, len(records))
for _, record := range records {
items = append(items, sourceDTO(record.Source, record.ItemCount))
}
c.JSON(http.StatusOK, items)
}
func (h *Handler) createSource(c *gin.Context) {
userID, ok := currentUser(c)
if !ok {
return
}
var input sourceRequest
if err := c.ShouldBindJSON(&input); err != nil {
writeInvalidRequest(c)
return
}
enabled := true
if input.Enabled != nil {
enabled = *input.Enabled
}
source, err := h.service.CreateSource(userID, SourceInput{
Name: input.Name, Kind: input.Kind, URL: input.URL, IconURL: input.IconURL,
Description: input.Description, Enabled: enabled,
})
if err != nil {
writeError(c, err)
return
}
c.JSON(http.StatusCreated, sourceDTO(*source, 0))
}
func (h *Handler) updateSource(c *gin.Context) {
userID, ok := currentUser(c)
if !ok {
return
}
identity, ok := httpx.IdentityParam(c, "id")
if !ok {
return
}
var input struct {
Name *string `json:"name"`
Kind *string `json:"kind"`
URL *string `json:"url"`
IconURL *string `json:"iconUrl"`
Description *string `json:"description"`
Enabled *bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&input); err != nil {
writeInvalidRequest(c)
return
}
source, err := h.service.PatchSource(userID, identity, SourcePatch{
Name: input.Name, Kind: input.Kind, URL: input.URL, IconURL: input.IconURL,
Description: input.Description, Enabled: input.Enabled,
})
if err != nil {
writeError(c, err)
return
}
itemCount, err := h.service.CountItems(userID, source.ID)
if err != nil {
writeError(c, err)
return
}
c.JSON(http.StatusOK, sourceDTO(*source, itemCount))
}
func (h *Handler) deleteSource(c *gin.Context) {
userID, ok := currentUser(c)
if !ok {
return
}
identity, ok := httpx.IdentityParam(c, "id")
if !ok {
return
}
if err := h.service.DeleteSource(userID, identity); err != nil {
writeError(c, err)
return
}
c.Status(http.StatusNoContent)
}
func (h *Handler) listItems(c *gin.Context) {
userID, ok := currentUser(c)
if !ok {
return
}
limit, err := optionalNonNegativeInt(c.Query("limit"))
if err != nil {
writeInvalidRequest(c)
return
}
offset, err := optionalNonNegativeInt(c.Query("offset"))
if err != nil {
writeInvalidRequest(c)
return
}
page, err := h.service.ListItemsPage(userID, c.Query("sourceId"), limit, offset)
if err != nil {
writeError(c, err)
return
}
result := make([]ItemDTO, 0, len(page.Items))
for _, item := range page.Items {
result = append(result, itemDTO(item))
}
_, hasLimit := c.GetQuery("limit")
_, hasOffset := c.GetQuery("offset")
if !hasLimit && !hasOffset {
c.JSON(http.StatusOK, result)
return
}
c.JSON(http.StatusOK, ItemPageDTO{
Items: result, Total: page.Total, Limit: page.Limit, Offset: page.Offset,
})
}
func (h *Handler) createItem(c *gin.Context) {
userID, ok := currentUser(c)
if !ok {
return
}
var input struct {
SourceID string `json:"sourceId"`
Title string `json:"title"`
Summary string `json:"summary"`
Content string `json:"content"`
URL string `json:"url"`
PublishedAt string `json:"publishedAt"`
}
if err := c.ShouldBindJSON(&input); err != nil {
writeInvalidRequest(c)
return
}
publishedAt, err := parseOptionalTime(input.PublishedAt)
if err != nil {
writeInvalidRequest(c)
return
}
item, err := h.service.CreateItem(userID, ItemInput{
SourceIdentity: input.SourceID, Title: input.Title, Summary: input.Summary,
Content: input.Content, URL: input.URL, PublishedAt: publishedAt,
})
if err != nil {
writeError(c, err)
return
}
c.JSON(http.StatusCreated, itemDTO(*item))
}
func (h *Handler) updateItem(c *gin.Context) {
userID, ok := currentUser(c)
if !ok {
return
}
identity, ok := httpx.IdentityParam(c, "id")
if !ok {
return
}
var input struct {
Status *string `json:"status"`
Starred *bool `json:"starred"`
}
if err := c.ShouldBindJSON(&input); err != nil {
writeInvalidRequest(c)
return
}
item, err := h.service.PatchItem(userID, identity, ItemPatch{Status: input.Status, Starred: input.Starred})
if err != nil {
writeError(c, err)
return
}
c.JSON(http.StatusOK, itemDTO(*item))
}
func (h *Handler) depositItem(c *gin.Context) {
userID, ok := currentUser(c)
if !ok {
return
}
identity, ok := httpx.IdentityParam(c, "id")
if !ok {
return
}
var input struct {
ProjectID string `json:"projectId"`
}
if err := c.ShouldBindJSON(&input); err != nil || strings.TrimSpace(input.ProjectID) == "" {
writeInvalidRequest(c)
return
}
result, err := h.service.DepositItem(userID, identity, input.ProjectID)
if err != nil {
writeError(c, err)
return
}
c.JSON(http.StatusCreated, DepositDTO{
NoteID: result.NoteIdentity, ProjectID: result.ProjectIdentity,
})
}
func (h *Handler) listCrons(c *gin.Context) {
userID, ok := currentUser(c)
if !ok {
return
}
crons, err := h.service.ListCrons(userID)
if err != nil {
writeError(c, err)
return
}
result := make([]CronDTO, 0, len(crons))
for _, cron := range crons {
result = append(result, cronDTO(cron))
}
c.JSON(http.StatusOK, result)
}
func (h *Handler) queueSync(c *gin.Context) {
userID, ok := currentUser(c)
if !ok {
return
}
crons, err := h.service.QueueSources(userID)
if err != nil {
writeError(c, err)
return
}
result := make([]CronDTO, 0, len(crons))
for _, cron := range crons {
result = append(result, cronDTO(cron))
}
c.JSON(http.StatusAccepted, result)
}
func currentUser(c *gin.Context) (uint, bool) {
userID, ok := auth.CurrentUserID(c)
if !ok {
httpx.Error(c, http.StatusUnauthorized, "unauthorized", "未登录或登录已失效")
return 0, false
}
return userID, true
}
func writeError(c *gin.Context, err error) {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
httpx.Error(c, http.StatusNotFound, "not_found", "数据源或数据条目不存在")
case errors.Is(err, ErrBuiltInSource):
httpx.Error(c, http.StatusConflict, "built_in_source", "内置数据源不能删除,可以将其停用")
case errors.Is(err, ErrSyncInProgress):
httpx.Error(c, http.StatusConflict, "sync_in_progress", "已有数据源采集任务正在运行")
case errors.Is(err, ErrNameRequired), errors.Is(err, ErrKindInvalid),
errors.Is(err, ErrURLInvalid), errors.Is(err, ErrTitleRequired), errors.Is(err, ErrStatusInvalid):
writeInvalidRequest(c)
default:
httpx.Error(c, http.StatusInternalServerError, "internal_error", "探索数据操作失败")
}
}
func writeInvalidRequest(c *gin.Context) {
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效")
}
func sourceDTO(source models.SaDatasetSource, itemCount int64) SourceDTO {
return SourceDTO{
ID: source.Identity, Name: source.Name, Kind: source.Kind, URL: source.URL,
IconURL: source.IconURL, Description: source.Description, Enabled: source.Enabled,
BuiltIn: source.SeedKey != nil, LastSyncedAt: utcTime(source.LastSyncedAt), ItemCount: itemCount,
CreatedAt: source.CreatedAt.UTC(), UpdatedAt: source.UpdatedAt.UTC(),
}
}
func itemDTO(item models.SaDatasetItem) ItemDTO {
return ItemDTO{
ID: item.Identity, SourceID: item.SourceIdentity, Title: item.Title,
Summary: item.Summary, Content: item.Content, URL: item.URL, Status: item.Status,
Starred: item.Starred, PublishedAt: utcTime(item.PublishedAt),
CreatedAt: item.CreatedAt.UTC(), UpdatedAt: item.UpdatedAt.UTC(),
}
}
func cronDTO(cron models.SaDatasetCron) CronDTO {
return CronDTO{
ID: cron.Identity, SourceID: cron.SourceIdentity, Status: cron.Status,
LastRunAt: utcTime(cron.LastRunAt), LastResult: cron.LastResult,
CreatedAt: cron.CreatedAt.UTC(), UpdatedAt: cron.UpdatedAt.UTC(),
}
}
func optionalNonNegativeInt(value string) (int, error) {
if strings.TrimSpace(value) == "" {
return 0, nil
}
parsed, err := strconv.Atoi(value)
if err != nil || parsed < 0 {
return 0, errors.New("invalid non-negative integer")
}
return parsed, nil
}
func parseOptionalTime(value string) (*time.Time, error) {
if strings.TrimSpace(value) == "" {
return nil, nil
}
parsed, err := time.Parse(time.RFC3339, value)
if err != nil {
return nil, err
}
parsed = parsed.UTC()
return &parsed, nil
}
func utcTime(value *time.Time) *time.Time {
if value == nil {
return nil
}
result := value.UTC()
return &result
}