feat(server): meter AI point usage across tenant AI features

Adds an ai_point_usage_logs ledger and a reserve/refund/complete pipeline
that charges AI points for article selection optimize, template analyze
/title/outline, and KOL prompt generate/optimize. Pending reservations
are reconciled when the kol-assist worker and template-assist tasks
finish or fail, so points refund automatically on errors. Workspace now
exposes the AI quota status and a paginated usage ledger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 11:33:47 +08:00
parent 794a2d89db
commit e307a048d1
18 changed files with 1270 additions and 27 deletions
@@ -47,26 +47,42 @@ func (s *KolAssistService) Run(
req.Description = s.enrichGenerateDescription(ctx, req.Description)
}
reservation, err := s.reserveStreamingAIPoints(ctx, actor, req)
if err != nil {
return nil, "", err
}
generateReq, err := BuildKolAssistGenerateRequest(req)
if err != nil {
s.refundReservedAIPoints(context.Background(), actor, reservation, err.Error())
return nil, "", response.ErrInternal(50087, "kol_assist_prompt_build_failed", err.Error())
}
streamAccumulator := newKolAssistContentStream(onDelta)
generated, err := s.llm.Generate(ctx, generateReq, streamAccumulator.ConsumeRawDelta)
if err != nil {
s.refundReservedAIPoints(context.Background(), actor, reservation, err.Error())
return nil, "", err
}
result, err := ParseKolAssistResponse(generated.Content)
if err != nil {
s.refundReservedAIPoints(context.Background(), actor, reservation, err.Error())
return nil, "", response.ErrInternal(50086, "kol_assist_result_invalid", err.Error())
}
result, err = FinalizeKolAssistResult(req, result)
if err != nil {
s.refundReservedAIPoints(context.Background(), actor, reservation, err.Error())
return nil, "", response.ErrInternal(50086, "kol_assist_result_invalid", err.Error())
}
completeCtx, cancel := newGenerationCleanupContext()
if err := CompleteAIPoints(completeCtx, s.pool, actor.TenantID, *reservation, generated.Model); err != nil {
cancel()
return nil, "", response.ErrInternal(50086, "kol_assist_result_invalid", "failed to confirm ai points usage")
}
cancel()
return result, generated.Model, nil
}