feat: add managed llm summaries and memory timeline
ci / verify (push) Canceled after 0s

This commit is contained in:
Codex Local
2026-08-30 10:51:32 +08:00
parent 47f8a164fa
commit 962dd1a063
22 changed files with 1417 additions and 86 deletions
+30
View File
@@ -24,6 +24,7 @@ var (
ErrPersonalizedMemoryConsentNeeded = errors.New("personalized memory consent is required")
ErrReviewStateConflict = errors.New("content review state does not allow this operation")
ErrFactReviewStateConflict = errors.New("fact candidate review state does not allow this operation")
ErrConversationSummaryNotReady = errors.New("conversation is not ready for summary generation")
)
type ConversationTurn struct {
@@ -216,6 +217,35 @@ func (s *Service) ListConversationContentSummaries(ctx context.Context, principa
return items, rows.Err()
}
func (s *Service) RegenerateConversationSummary(ctx context.Context, principal identity.Principal, conversationPublicID, requestID string) (int64, error) {
var generationVersion int64
err := database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
access, conversationID, err := s.resolveConversationContent(ctx, tx, principal, conversationPublicID, identity.CapabilityStoryReview, true)
if err != nil {
return err
}
var status string
var maxSequence int64
if err := tx.QueryRow(ctx, `SELECT c.status,(SELECT COALESCE(MAX(sequence_no),0) FROM conversation_turn ct WHERE ct.conversation_id=c.id AND ct.tenant_id=c.tenant_id) FROM conversation c WHERE c.id=$1::uuid`, conversationID).Scan(&status, &maxSequence); err != nil {
return err
}
if (status != "COMPLETED" && status != "FAILED") || maxSequence == 0 {
return ErrConversationSummaryNotReady
}
if err := tx.QueryRow(ctx, `SELECT GREATEST($2::bigint,COALESCE(MAX(generation_version),0))+1 FROM conversation_summary WHERE tenant_id=$1::uuid AND conversation_id=$3::uuid`, access.tenantID, maxSequence, conversationID).Scan(&generationVersion); err != nil {
return err
}
payload, _ := json.Marshal(map[string]any{"generation_version": generationVersion, "reason": "manual_regeneration"})
dedupe := fmt.Sprintf("content-regenerate:%s:%d", conversationID, generationVersion)
if _, err := outbox.NewStore(s.pool).Enqueue(ctx, tx, outbox.NewEvent{TenantID: &access.tenantID, AggregateType: "conversation", AggregateID: conversationID, EventType: "conversation.content_extraction_requested", DeduplicationKey: &dedupe, Payload: payload}); err != nil {
return err
}
resourceID := conversationPublicID
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "conversation.summary_regenerate", ResourceType: "conversation", ResourceID: &resourceID, RequestID: requestID, Purpose: "使用当前激活的大模型重新总结聊天", Result: "queued", Detail: map[string]any{"generation_version": generationVersion}})
})
return generationVersion, err
}
func (s *Service) ListSubjectStories(ctx context.Context, principal identity.Principal, subjectPublicID, status string) ([]Story, error) {
access, _, err := resolveSubject(ctx, s.pool, principal, subjectPublicID, identity.CapabilitySubjectRead, false)
if err != nil {