完善生图5.0的功能

This commit is contained in:
andy committed 2026-08-25 14:06:04 +08:00
1 parent 88a4268d40
commit 678d213f40
31 files changed
+3281 -107

No files matched your search

+129 -5
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"strconv"
"strings"
"time"
@@ -23,8 +24,9 @@ type FencedSettlementStateWriter interface {
WriteBillingFenced(context.Context, string, json.RawMessage, jobs.Status, string) error
}
// SettlementProcessor adds Seedance actual-usage settlement to a jobs
// processor. Other providers and non-successful advances pass through.
// SettlementProcessor adds actual-usage settlement for Seedance videos and
// Seedream layer decomposition. Other providers and non-successful advances
// pass through.
type SettlementProcessor struct {
inner jobs.Processor
ledger SettlementLedger
@@ -44,9 +46,15 @@ func (p *SettlementProcessor) Advance(ctx context.Context, job jobs.Job) (jobs.J
return jobs.Job{}, errors.New("advance generation job")
}
advanced, err := p.inner.Advance(ctx, job)
if err != nil || advanced.Provider != "seedance" || advanced.Status != jobs.StatusSucceeded || len(advanced.Billing) == 0 {
if err != nil || advanced.Status != jobs.StatusSucceeded || len(advanced.Billing) == 0 {
return advanced, err
}
if advanced.Provider == "seedream" && seedreamLayerRequest(advanced.RequestPayload) {
return p.settleSeedreamLayers(ctx, advanced)
}
if advanced.Provider != "seedance" {
return advanced, nil
}
var charge billingState
if json.Unmarshal(advanced.Billing, &charge) != nil {
@@ -131,13 +139,84 @@ func (p *SettlementProcessor) Advance(ctx context.Context, job jobs.Job) (jobs.J
return p.write(ctx, advanced, charge.raw)
}
func (p *SettlementProcessor) settleSeedreamLayers(ctx context.Context, job jobs.Job) (jobs.Job, error) {
var charge billingState
if json.Unmarshal(job.Billing, &charge) != nil {
return jobs.Job{}, errors.New("settle seedream layer charge")
}
status, _ := charge.raw["settlementStatus"].(string)
if status == "settled" || status == "estimated" {
return job, nil
}
var use usageContext
if len(job.UsageContext) != 0 && json.Unmarshal(job.UsageContext, &use) != nil {
return jobs.Job{}, errors.New("settle seedream layer charge")
}
quotaExempt := charge.QuotaExempt || use.Source == "platform" && use.Role == "super_admin"
chargeReady := charge.Status == "charged" || quotaExempt && charge.Status == "not_charged"
if !chargeReady || !quotaExempt && use.OrganizationID == "" {
return job, nil
}
images := seedreamLayerImages(job.ResponsePayload)
if len(images) == 0 {
charge.raw["settlementStatus"] = "estimated"
charge.raw["settlementReason"] = "provider_layer_metadata_unavailable"
charge.raw["settledAt"] = p.now().UTC().Format(time.RFC3339Nano)
return p.write(ctx, job, charge.raw)
}
actualAmount, err := billing.CalculateSeedreamLayerAmountFen(images, numberOrZero(charge.raw["markupMultiplier"]))
if err != nil {
return jobs.Job{}, errors.New("settle seedream layer charge")
}
delta := actualAmount - charge.AmountFen
settledAt := p.now().UTC()
var settlementLedgerID string
if delta != 0 && !quotaExempt {
if p.ledger == nil {
return jobs.Job{}, errors.New("settle seedream layer charge")
}
description := capabilityLabel(job.Capability) + "图层拆分实际费用退回"
if delta > 0 {
description = capabilityLabel(job.Capability) + "图层拆分实际费用补扣"
}
posting, postErr := p.ledger.Settle(ctx, billing.SettlementRequest{
OrganizationID: use.OrganizationID, AccountID: use.AccountID, JobID: job.ID,
DeltaFen: delta, Description: description,
Metadata: map[string]any{
"operation": "seedream_layer_actual_settlement", "reservedAmountFen": charge.AmountFen,
"actualAmountFen": actualAmount, "outputCount": len(images), "model": job.ReqKey,
},
})
if postErr != nil {
return jobs.Job{}, errors.New("settle seedream layer charge")
}
settlementLedgerID = posting.LedgerID
if !posting.CreatedAt.IsZero() {
settledAt = posting.CreatedAt.UTC()
}
}
charge.raw["amountFen"] = actualAmount
charge.raw["settlementStatus"] = "settled"
charge.raw["settledAt"] = settledAt.Format(time.RFC3339Nano)
if settlementLedgerID != "" {
charge.raw["settlementLedgerEntryId"] = settlementLedgerID
} else {
delete(charge.raw, "settlementLedgerEntryId")
}
charge.raw["providerUsage"] = map[string]any{
"model": job.ReqKey, "layerDecomposition": true, "generatedImages": len(images),
}
return p.write(ctx, job, charge.raw)
}
func (p *SettlementProcessor) write(ctx context.Context, job jobs.Job, snapshot map[string]any) (jobs.Job, error) {
if p.state == nil {
return jobs.Job{}, errors.New("persist seedance generation settlement")
return jobs.Job{}, errors.New("persist generation settlement")
}
encoded, err := json.Marshal(snapshot)
if err != nil {
return jobs.Job{}, errors.New("persist seedance generation settlement")
return jobs.Job{}, errors.New("persist generation settlement")
}
var writeErr error
if fenced, ok := p.state.(FencedSettlementStateWriter); ok && job.LockedBy != "" {
@@ -163,6 +242,51 @@ func seedanceCompletionTokens(payload json.RawMessage) int64 {
return value
}
func seedreamLayerRequest(payload json.RawMessage) bool {
var request struct {
Settings map[string]any `json:"settings"`
}
if json.Unmarshal(payload, &request) != nil {
return false
}
value, _ := request.Settings["layerDecomposition"].(bool)
creationMode, _ := request.Settings["creationMode"].(string)
return value || strings.EqualFold(strings.TrimSpace(creationMode), "layers")
}
func seedreamLayerImages(payload json.RawMessage) []billing.SeedreamLayerImage {
var response struct {
Raw json.RawMessage `json:"raw"`
}
if json.Unmarshal(payload, &response) != nil || len(response.Raw) == 0 {
return nil
}
var providerResponse struct {
Data []struct {
Size string `json:"size"`
} `json:"data"`
}
if json.Unmarshal(response.Raw, &providerResponse) != nil || len(providerResponse.Data) == 0 || len(providerResponse.Data) > 17 {
return nil
}
images := make([]billing.SeedreamLayerImage, 0, len(providerResponse.Data))
for _, output := range providerResponse.Data {
parts := strings.FieldsFunc(strings.TrimSpace(output.Size), func(character rune) bool {
return character == 'x' || character == 'X' || character == '×' || character == '*'
})
if len(parts) != 2 {
return nil
}
width, widthErr := strconv.Atoi(strings.TrimSpace(parts[0]))
height, heightErr := strconv.Atoi(strings.TrimSpace(parts[1]))
if widthErr != nil || heightErr != nil || width <= 0 || height <= 0 {
return nil
}
images = append(images, billing.SeedreamLayerImage{Width: width, Height: height})
}
return images
}
func seedanceResolution(snapshot map[string]any) string {
parameters, _ := snapshot["parameters"].(map[string]any)
resolution, _ := parameters["resolution"].(string)