feat(knowledge): add retry endpoint and 20-char text name limit
Add POST /api/tenant/knowledge/items/retry/:id to re-queue failed parse tasks, plus a 20-rune cap on text item names with matching frontend validation. Wires the retry button into the knowledge table with a colored status tag for clearer state feedback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -32,6 +33,7 @@ const (
|
||||
defaultKnowledgeCleanupPoll = 15 * time.Second
|
||||
defaultKnowledgeChildGroupName = "默认目录"
|
||||
maxKnowledgeSnippetsPerItem = 2
|
||||
maxKnowledgeTextNameRunes = 20
|
||||
)
|
||||
|
||||
type KnowledgeService struct {
|
||||
@@ -598,9 +600,14 @@ func (s *KnowledgeService) CreateTextItem(ctx context.Context, req KnowledgeText
|
||||
return nil, response.ErrBadRequest(40051, "knowledge_content_required", "knowledge content is required")
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(req.Name)
|
||||
if utf8.RuneCountInString(name) > maxKnowledgeTextNameRunes {
|
||||
return nil, response.ErrBadRequest(40055, "knowledge_text_name_too_long", "knowledge text name must be at most 20 characters")
|
||||
}
|
||||
|
||||
return s.queueKnowledgeItem(ctx, actor.TenantID, knowledgeIngestInput{
|
||||
GroupID: req.GroupID,
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Name: name,
|
||||
SourceType: "text",
|
||||
ContentText: &content,
|
||||
SizeBytes: int64(len([]byte(content))),
|
||||
@@ -715,6 +722,100 @@ func (s *KnowledgeService) DeleteItem(ctx context.Context, itemID int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) RetryItem(ctx context.Context, itemID int64) (*KnowledgeItemResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
|
||||
var sourceType string
|
||||
var status string
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT source_type, status
|
||||
FROM knowledge_items
|
||||
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
||||
`, itemID, actor.TenantID).Scan(&sourceType, &status)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, response.ErrNotFound(40450, "knowledge_item_not_found", "knowledge item not found")
|
||||
}
|
||||
return nil, response.ErrInternal(50051, "knowledge_items_query_failed", "failed to load knowledge item")
|
||||
}
|
||||
if status != "failed" {
|
||||
return nil, response.ErrConflict(40952, "knowledge_item_retry_unavailable", "only failed knowledge items can be retried")
|
||||
}
|
||||
if err := s.validateIngestionDependencies(sourceType); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50055, "knowledge_item_retry_failed", "failed to begin retry transaction")
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
var parseTaskID int64
|
||||
err = tx.QueryRow(ctx, `
|
||||
UPDATE knowledge_parse_tasks
|
||||
SET status = 'pending',
|
||||
error_message = NULL,
|
||||
started_at = NULL,
|
||||
completed_at = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE id = (
|
||||
SELECT id
|
||||
FROM knowledge_parse_tasks
|
||||
WHERE tenant_id = $1
|
||||
AND knowledge_item_id = $2
|
||||
ORDER BY created_at DESC, id DESC
|
||||
LIMIT 1
|
||||
)
|
||||
RETURNING id
|
||||
`, actor.TenantID, itemID).Scan(&parseTaskID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, response.ErrConflict(40952, "knowledge_item_retry_unavailable", "knowledge parse task is unavailable")
|
||||
}
|
||||
return nil, response.ErrInternal(50055, "knowledge_item_retry_failed", "failed to reset knowledge parse task")
|
||||
}
|
||||
|
||||
tag, err := tx.Exec(ctx, `
|
||||
UPDATE knowledge_items
|
||||
SET status = 'pending',
|
||||
error_message = NULL,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND deleted_at IS NULL
|
||||
AND status = 'failed'
|
||||
`, itemID, actor.TenantID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50055, "knowledge_item_retry_failed", "failed to reset knowledge item")
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return nil, response.ErrConflict(40952, "knowledge_item_retry_unavailable", "only failed knowledge items can be retried")
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, response.ErrInternal(50055, "knowledge_item_retry_failed", "failed to commit retry transaction")
|
||||
}
|
||||
|
||||
if err := s.enqueueParseJob(knowledgeParseJob{
|
||||
TenantID: actor.TenantID,
|
||||
ItemID: itemID,
|
||||
ParseTaskID: parseTaskID,
|
||||
}); err != nil {
|
||||
_ = s.markKnowledgeIngestionFailed(context.Background(), actor.TenantID, itemID, parseTaskID, err)
|
||||
return nil, response.ErrServiceUnavailable(50356, "knowledge_queue_unavailable", "knowledge parse queue is busy, please retry")
|
||||
}
|
||||
|
||||
items, err := s.listKnowledgeItemsByIDs(ctx, actor.TenantID, []int64{itemID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, response.ErrNotFound(40450, "knowledge_item_not_found", "knowledge item not found")
|
||||
}
|
||||
return &items[0], nil
|
||||
}
|
||||
|
||||
func (s *KnowledgeService) ResolveContext(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
|
||||
@@ -190,6 +190,21 @@ func (h *KnowledgeHandler) CreateFileItem(c *gin.Context) {
|
||||
response.SuccessWithStatus(c, http.StatusCreated, data)
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) RetryItem(c *gin.Context) {
|
||||
itemID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "knowledge item id must be a number"))
|
||||
return
|
||||
}
|
||||
|
||||
data, err := h.svc.RetryItem(c.Request.Context(), itemID)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *KnowledgeHandler) DeleteItem(c *gin.Context) {
|
||||
itemID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
|
||||
@@ -199,6 +199,7 @@ func RegisterRoutes(app *bootstrap.App) {
|
||||
knowledge.POST("/items/text", knowledgeHandler.CreateTextItem)
|
||||
knowledge.POST("/items/url", knowledgeHandler.CreateURLItem)
|
||||
knowledge.POST("/items/file", knowledgeHandler.CreateFileItem)
|
||||
knowledge.POST("/items/retry/:id", knowledgeHandler.RetryItem)
|
||||
knowledge.DELETE("/items/:id", knowledgeHandler.DeleteItem)
|
||||
|
||||
// Prompt Rules
|
||||
|
||||
Reference in New Issue
Block a user