文案优化

This commit is contained in:
andy
2026-09-03 17:23:00 +08:00
parent 26caa7435f
commit 98c6127ef7
4 changed files with 67 additions and 6 deletions

View File

@@ -74,7 +74,7 @@ func (p ProviderProcessor) Advance(ctx context.Context, job Job) (Job, error) {
ErrorClass: "unknown_outcome",
})
failed := StatusFailed
failure := &JobError{Message: "provider submission outcome is unknown; refusing duplicate submission", Retryable: false}
failure := &JobError{Message: unknownProviderSubmissionMessage, Retryable: false}
if p.Store == nil {
job.Status, job.Error = failed, failure
return job, nil
@@ -103,7 +103,7 @@ func (p ProviderProcessor) Advance(ctx context.Context, job Job) (Job, error) {
}
if err != nil && job.ProviderTaskID == "" && p.Store != nil {
failed := StatusFailed
failure := &JobError{Message: "provider submission outcome is unknown; refusing duplicate submission", Retryable: false}
failure := providerSubmissionFailure(err)
return p.Store.UpdateJob(ctx, job.ID, workerPatch(job, Patch{Status: &failed, Error: failure}))
}
if err != nil {
@@ -152,6 +152,20 @@ func (p ProviderProcessor) Advance(ctx context.Context, job Job) (Job, error) {
return job, nil
}
const (
unknownProviderSubmissionMessage = "provider submission outcome is unknown; refusing duplicate submission"
outputImageSafetyMessage = "生成结果触发内容安全审核,请更换素材或调整内容后重试。"
)
func providerSubmissionFailure(err error) *JobError {
message := unknownProviderSubmissionMessage
var providerError *providers.ProviderError
if errors.As(err, &providerError) && (providerError.Code == "OutputImageSensitiveContentDetected" || strings.HasPrefix(providerError.Code, "OutputImageSensitiveContentDetected.")) {
message = outputImageSafetyMessage
}
return &JobError{Message: message, Retryable: false}
}
type providerFailureDiagnostic struct {
JobID string
Provider string

View File

@@ -415,7 +415,7 @@ func TestProviderProcessorLogsSafeJobCorrelationWhenSubmitFails(t *testing.T) {
processor := ProviderProcessor{Providers: ProviderRegistry{"volcengine-visual": adapter}, Store: store}
got, err := processor.Advance(context.Background(), job)
if err != nil || got.Status != StatusFailed {
if err != nil || got.Status != StatusFailed || got.Error == nil || got.Error.Message != unknownProviderSubmissionMessage {
t.Fatalf("got=%#v err=%v", got, err)
}
logged := output.String()
@@ -431,6 +431,52 @@ func TestProviderProcessorLogsSafeJobCorrelationWhenSubmitFails(t *testing.T) {
}
}
func TestProviderProcessorPersistsFriendlySeedreamOutputSafetyMessage(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
if request.URL.Path != "/api/v3/images/generations" {
t.Fatalf("request path = %q", request.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{
"error": {
"code": "OutputImageSensitiveContentDetected.PolicyViolation",
"type": "BadRequest",
"message": "private upstream moderation detail"
}
}`))
}))
defer server.Close()
store := newMemoryJobStore()
job := Job{
ID: "job-seedream-output-safety", OwnerID: "owner", Provider: "seedream", ReqKey: providers.Seedream50ProModel,
Capability: "image.generate", Status: StatusRunning, LockedBy: "worker",
RequestPayload: json.RawMessage(`{
"capability":"image.generate",
"model":"doubao-seedream-5-0-pro-260628",
"prompt":"拆分元素",
"inputUrls":["https://assets.test/source.jpg"],
"settings":{"layerDecomposition":true,"size":"2K","outputFormat":"png"}
}`),
}
store.jobs[job.ID] = job
adapter := providers.NewSeedream(providers.Config{
BaseURL: server.URL + "/api/v3", APIKey: "private-api-key", Model: providers.Seedream50ProModel,
}, server.Client())
processor := ProviderProcessor{Providers: ProviderRegistry{"seedream": adapter}, Store: store}
got, err := processor.Advance(context.Background(), job)
const want = "生成结果触发内容安全审核,请更换素材或调整内容后重试。"
if err != nil || got.Status != StatusFailed || got.Error == nil || got.Error.Message != want || got.Error.Retryable {
t.Fatalf("got=%#v err=%v", got, err)
}
persisted := store.jobs[job.ID]
if persisted.Error == nil || persisted.Error.Message != want {
t.Fatalf("persisted error = %#v", persisted.Error)
}
}
func TestProviderProcessorClearsTransientErrorAfterSuccessfulPoll(t *testing.T) {
store := newMemoryJobStore()
job := Job{ID: "job-recovered", OwnerID: "owner", Provider: "fixture", ReqKey: "model-a", Capability: "image.generate", Status: StatusQueued, LockedBy: "worker", ProviderTaskID: "provider-task", Error: &JobError{Message: "temporary timeout", Retryable: true}, RequestPayload: json.RawMessage(`{"capability":"image.generate","model":"model-a","prompt":"hello"}`)}

View File

@@ -111,6 +111,7 @@ type Config struct {
type ProviderError struct {
Operation string
Status int
Code string
Cause error
}
@@ -215,7 +216,7 @@ func (a *httpAdapter) call(ctx context.Context, method, path string, body []byte
ErrorClass: "service",
ElapsedMS: elapsedMilliseconds(startedAt),
})
return Result{}, &ProviderError{Operation: a.name + " " + operation, Status: resp.StatusCode}
return Result{}, &ProviderError{Operation: a.name + " " + operation, Status: resp.StatusCode, Code: code}
}
if !json.Valid(raw) {
logHTTPProviderFailure(httpProviderDiagnostic{

View File

@@ -169,7 +169,7 @@ func (v *Volcengine) call(ctx context.Context, action, version string, payload a
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
diagnostic.ErrorClass = "service"
logVolcengineFailure(diagnostic)
return Result{}, &ProviderError{Operation: "volcengine request", Status: resp.StatusCode}
return Result{}, &ProviderError{Operation: "volcengine request", Status: resp.StatusCode, Code: diagnostic.Code}
}
if !validJSON {
diagnostic.ErrorClass = "invalid_json"
@@ -179,7 +179,7 @@ func (v *Volcengine) call(ctx context.Context, action, version string, payload a
if response.code != nil && !volcengineRequestSucceeded(response.code) {
diagnostic.ErrorClass = "service"
logVolcengineFailure(diagnostic)
return Result{}, &ProviderError{Operation: "volcengine request"}
return Result{}, &ProviderError{Operation: "volcengine request", Code: diagnostic.Code}
}
d := object(first(response.business["data"], response.business["Data"]))
out := []string{}