fix(media): 统一图片结果 URL 化并限制同步 Base64

将上游 URL 直接持久化,内联媒体经对象存储后仅保留 URL 与内部定位元数据;异步轮询、任务详情和幂等重放统一使用零对象读取的 URL 投影,并增加 64KiB 响应门禁。

OpenAI 图片接口接受 url 与 b64_json,同步 Base64 限制为 20MiB 和每 Pod 2 并发;新增历史结果迁移清零门禁、结果指标和 API GOMEMLIMIT。

验证:API go test ./...、go vet、聚焦 race、pnpm openapi、pnpm lint/test/build、迁移安全检查与 docker compose config 均通过。
This commit is contained in:
2026-08-05 18:15:06 +08:00
parent f9b945e4aa
commit b13392ef50
22 changed files with 1367 additions and 176 deletions
@@ -109,6 +109,11 @@ type Metrics struct {
storageChannelFailovers atomic.Uint64
storageAllChannelsFailed atomic.Uint64
storageObjectifiedBytes atomic.Uint64
resultSourceUpstreamURL atomic.Uint64
resultSourceUploaded atomic.Uint64
resultPollResponses atomic.Uint64
resultPollResponseBytes atomic.Uint64
resultPollOversized atomic.Uint64
taskAdmissionWaitBuckets [11]atomic.Uint64
taskAdmissionWaitMicros atomic.Uint64
}
@@ -345,6 +350,27 @@ func (m *Metrics) ObserveObjectStorage(event string, provider string, bytes int6
}
}
func (m *Metrics) ObserveResultStorage(source string) {
switch strings.ToLower(strings.TrimSpace(source)) {
case "upstream_url":
m.resultSourceUpstreamURL.Add(1)
case "uploaded":
m.resultSourceUploaded.Add(1)
}
}
func (m *Metrics) ObserveResultDelivery(event string, bytes int64) {
switch strings.ToLower(strings.TrimSpace(event)) {
case "poll_success":
m.resultPollResponses.Add(1)
if bytes > 0 {
m.resultPollResponseBytes.Add(uint64(bytes))
}
case "poll_oversized":
m.resultPollOversized.Add(1)
}
}
func (m *Metrics) ObserveTaskAdmissionWait(wait time.Duration) {
if wait < 0 {
wait = 0
@@ -561,6 +587,13 @@ func (m *Metrics) Handler(provider MetricsSnapshotProvider, issuer, audience str
plainCounter(w, "easyai_gateway_storage_channel_failovers_total", "Switches to the next configured storage channel.", m.storageChannelFailovers.Load())
plainCounter(w, "easyai_gateway_storage_all_channels_failed_total", "Storage writes where every eligible channel failed.", m.storageAllChannelsFailed.Load())
plainCounter(w, "easyai_gateway_storage_objectified_bytes_total", "Binary bytes successfully written through storage channels.", m.storageObjectifiedBytes.Load())
outcomeCounters(w, "easyai_gateway_result_storage_total", "Generated media results by canonical storage source.", []outcomeValue{
{"upstream_url", m.resultSourceUpstreamURL.Load()},
{"uploaded", m.resultSourceUploaded.Load()},
})
plainCounter(w, "easyai_gateway_result_poll_responses_total", "Successful URL-only task result responses.", m.resultPollResponses.Load())
plainCounter(w, "easyai_gateway_result_poll_response_bytes_total", "Bytes written by successful URL-only task result responses.", m.resultPollResponseBytes.Load())
plainCounter(w, "easyai_gateway_result_poll_oversized_total", "Task result responses rejected by the URL-only size gate.", m.resultPollOversized.Load())
publicErrorCounters(w, publicerror.MetricSnapshot())
platformModelRateLimitUtilizationGauges(w, modelRateLimits)
plainGauge(w, "easyai_gateway_postgres_pool_max_connections", "Maximum PostgreSQL connections in this process pool.", int64(postgresPool.MaxConnections))
@@ -57,6 +57,10 @@ func TestMetricsExposeBoundedOutcomesAndState(t *testing.T) {
metrics.ObserveObjectStorage("retry", "s3", 0, 0)
metrics.ObserveObjectStorage("failover", "s3", 0, 0)
metrics.ObserveObjectStorage("all_failed", "", 0, 0)
metrics.ObserveResultStorage("upstream_url")
metrics.ObserveResultStorage("uploaded")
metrics.ObserveResultDelivery("poll_success", 512)
metrics.ObserveResultDelivery("poll_oversized", 70<<10)
metrics.ObserveAsyncWorkerResize("success")
metrics.ObserveConcurrencyLeaseRenewal("success")
metrics.ObserveConcurrencyLeaseRenewal("lost")
@@ -117,6 +121,11 @@ func TestMetricsExposeBoundedOutcomesAndState(t *testing.T) {
`easyai_gateway_storage_channel_failovers_total 1`,
`easyai_gateway_storage_all_channels_failed_total 1`,
`easyai_gateway_storage_objectified_bytes_total 128`,
`easyai_gateway_result_storage_total{outcome="upstream_url"} 1`,
`easyai_gateway_result_storage_total{outcome="uploaded"} 1`,
`easyai_gateway_result_poll_responses_total 1`,
`easyai_gateway_result_poll_response_bytes_total 512`,
`easyai_gateway_result_poll_oversized_total 1`,
`easyai_gateway_platform_model_rate_limit_utilization{platform_model_id="platform-model-1",metric="concurrent"} 0.750000`,
`easyai_gateway_async_worker_resizes_total{outcome="success"} 1`,
`easyai_gateway_concurrency_lease_renewals_total{outcome="success"} 1`,