chore: update app generation workflow
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Frontend CI / Frontend (push) Successful in 2m58s
Backend CI / Backend (push) Successful in 16m11s
Desktop Client Build / Build Desktop Client (push) Successful in 24m1s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 28s

This commit is contained in:
2026-06-24 23:39:12 +08:00
parent 98e3dce017
commit 590c219a64
11 changed files with 232 additions and 9 deletions
@@ -10,6 +10,7 @@ import (
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
sharedcache "github.com/geo-platform/tenant-api/internal/shared/cache"
"github.com/geo-platform/tenant-api/internal/shared/llm"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
@@ -26,6 +27,12 @@ type ClaimedGenerationTask struct {
const generationCleanupTimeout = 30 * time.Second
var errGenerationArticleUnavailable = errors.New("generation article is unavailable")
type generationDBTX interface {
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
type promptRuleTaskContext struct {
PromptRuleID int64
BrandID int64
@@ -46,6 +53,124 @@ func rollbackGenerationTx(tx pgx.Tx) {
_ = tx.Rollback(ctx)
}
func generationStringPtr(value string) *string {
return &value
}
func EnsureGenerationArticleWritable(ctx context.Context, db generationDBTX, articleID, tenantID int64) error {
if articleID <= 0 || tenantID <= 0 {
return errGenerationArticleUnavailable
}
var lockedArticleID int64
if err := db.QueryRow(ctx, `
SELECT id
FROM articles
WHERE id = $1
AND tenant_id = $2
AND deleted_at IS NULL
FOR UPDATE
`, articleID, tenantID).Scan(&lockedArticleID); err != nil {
return err
}
return nil
}
func IsGenerationArticleUnavailable(err error) bool {
return errors.Is(err, errGenerationArticleUnavailable) || errors.Is(err, pgx.ErrNoRows)
}
func HandleGenerationTaskArticleUnavailable(
ctx context.Context,
pool *pgxpool.Pool,
cache sharedcache.Cache,
task ClaimedGenerationTask,
taskType string,
updateKolTaskRecord bool,
) error {
cleanupCtx, cancel := newGenerationCleanupContext()
defer cancel()
const errMsg = "文章已删除,生成结果已丢弃"
now := time.Now()
auditRepo := repository.NewAuditRepository(pool)
quotaRepo := repository.NewQuotaRepository(pool)
usageRepo := repository.NewKolUsageRepository(pool)
var cleanupErr error
if task.ID > 0 {
if err := auditRepo.UpdateGenerationTaskStatus(cleanupCtx, repository.GenerationTaskStatusInput{
ID: task.ID,
TenantID: task.TenantID,
Status: "cancelled",
ErrorMessage: generationStringPtr(errMsg),
CompletedAt: &now,
}); err != nil {
cleanupErr = errors.Join(cleanupErr, fmt.Errorf("cancel generation task: %w", err))
}
if updateKolTaskRecord {
_ = auditRepo.UpdateTaskRecordStatus(cleanupCtx, repository.TaskRecordInput{
TenantID: task.TenantID,
TaskType: kolGenerationTaskRecordType,
ResourceType: kolGenerationTaskRecordScope,
ResourceID: task.ID,
Status: "cancelled",
ErrorMessage: generationStringPtr(errMsg),
})
_ = usageRepo.MarkFailedByTask(cleanupCtx, task.TenantID, task.ID, errMsg)
}
}
if task.QuotaReservationID > 0 {
var balance int
balanceErr := error(nil)
if task.OperatorID > 0 {
balance, balanceErr = quotaRepo.GetCurrentBalance(cleanupCtx, task.TenantID, "article_generation")
}
refunded, err := quotaRepo.RefundPendingReservation(cleanupCtx, task.QuotaReservationID, task.TenantID)
if err != nil {
cleanupErr = errors.Join(cleanupErr, fmt.Errorf("refund generation reservation: %w", err))
} else if refunded {
if balanceErr != nil {
cleanupErr = errors.Join(cleanupErr, fmt.Errorf("load quota balance for refund: %w", balanceErr))
} else if task.OperatorID > 0 {
reason := "generation_cancel_refund"
referenceType := "generation_task"
taskReferenceID := task.ID
if _, err := quotaRepo.InsertQuotaLedger(cleanupCtx, repository.QuotaLedgerInput{
TenantID: task.TenantID,
OperatorID: task.OperatorID,
QuotaType: "article_generation",
Delta: 1,
BalanceAfter: balance + 1,
Reason: &reason,
ReferenceType: &referenceType,
ReferenceID: &taskReferenceID,
}); err != nil {
cleanupErr = errors.Join(cleanupErr, fmt.Errorf("insert generation cancel refund ledger: %w", err))
}
}
}
}
if task.ArticleID > 0 {
invalidateArticleCaches(cleanupCtx, cache, task.TenantID, &task.ArticleID)
}
RecordGenerationTaskEvent(GenerationTaskEventTransition, GenerationTaskLogContext{
TaskID: task.ID,
TenantID: task.TenantID,
OperatorID: task.OperatorID,
ArticleID: task.ArticleID,
ReservationID: task.QuotaReservationID,
TaskType: taskType,
Stage: "article_deleted",
StatusAfter: "cancelled",
Result: GenerationTaskResultSkipped,
})
return cleanupErr
}
func HandleClaimedGenerationTaskFailure(
ctx context.Context,
pool *pgxpool.Pool,