feat: connect inbox confirmation workflow

This commit is contained in:
2026-07-21 18:24:15 +08:00
parent 8cc130244b
commit 85a25ca017
15 changed files with 1069 additions and 123 deletions

View File

@@ -1,17 +1,33 @@
package inbox
import (
"errors"
"net/http"
"strconv"
"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
}
// InboxItemDTO 隔离 Gorm 自增主键,只通过公开 identity 关联项目和条目。
type InboxItemDTO struct {
ID string `json:"id"`
ProjectID string `json:"projectId"`
SourceType string `json:"sourceType"`
Title string `json:"title"`
Body string `json:"body"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
@@ -23,68 +39,101 @@ func (h *Handler) Register(router gin.IRouter) {
}
func (h *Handler) capture(c *gin.Context) {
userID, ok := auth.CurrentUserID(c)
userID, ok := currentInboxUser(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return
}
projectID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project id"})
projectIdentity, ok := httpx.IdentityParam(c, "id")
if !ok {
return
}
var input struct {
SourceType string `json:"source_type"`
SourceType string `json:"sourceType"`
Title string `json:"title"`
Body string `json:"body"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效")
return
}
item, err := h.service.Capture(CaptureInput{ProjectID: uint(projectID), UserID: userID, SourceType: input.SourceType, Title: input.Title, Body: input.Body})
item, err := h.service.Capture(CaptureInput{
ProjectIdentity: projectIdentity, UserID: userID, SourceType: input.SourceType, Title: input.Title, Body: input.Body,
})
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
writeInboxError(c, err)
return
}
c.JSON(http.StatusCreated, item)
c.JSON(http.StatusCreated, inboxItemDTO(*item))
}
func (h *Handler) analyze(c *gin.Context) {
userID, ok := auth.CurrentUserID(c)
userID, ok := currentInboxUser(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return
}
itemID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
itemIdentity, ok := httpx.IdentityParam(c, "id")
if !ok {
return
}
suggestions, err := h.service.Analyze(uint(itemID), userID)
suggestions, err := h.service.Analyze(itemIdentity, userID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
writeInboxError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"suggestions": suggestions})
}
func (h *Handler) confirm(c *gin.Context) {
itemID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"})
userID, ok := currentInboxUser(c)
if !ok {
return
}
itemIdentity, ok := httpx.IdentityParam(c, "id")
if !ok {
return
}
var input struct {
Suggestions []Suggestion `json:"suggestions"`
SuggestionIDs []string `json:"suggestionIds"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数无效")
return
}
if err := h.service.Confirm(uint(itemID), input.Suggestions); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
result, err := h.service.Confirm(itemIdentity, userID, input.SuggestionIDs)
if err != nil {
writeInboxError(c, err)
return
}
c.Status(http.StatusNoContent)
c.JSON(http.StatusOK, result)
}
func currentInboxUser(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 inboxItemDTO(item models.SenlinAgentInboxItem) InboxItemDTO {
return InboxItemDTO{
ID: item.Identity, ProjectID: item.ProjectIdentity, SourceType: item.SourceType,
Title: item.Title, Body: item.Body, Status: item.Status,
CreatedAt: item.CreatedAt.UTC(), UpdatedAt: item.UpdatedAt.UTC(),
}
}
func writeInboxError(c *gin.Context, err error) {
switch {
case errors.Is(err, gorm.ErrRecordNotFound):
httpx.Error(c, http.StatusNotFound, "not_found", "Inbox 条目或项目不存在")
case errors.Is(err, ErrInboxAlreadyConfirmed):
httpx.Error(c, http.StatusConflict, "conflict", "该 Inbox 条目已经确认处理")
case errors.Is(err, ErrProjectAndUserRequired), errors.Is(err, ErrSourceTypeRequired),
errors.Is(err, ErrSuggestionSelectionRequired), errors.Is(err, ErrSuggestionNotFound):
httpx.Error(c, http.StatusBadRequest, "invalid_request", "请求参数或建议选择无效")
default:
httpx.Error(c, http.StatusInternalServerError, "internal_error", "Inbox 操作失败,请稍后重试")
}
}