perf(storage): 极简化任务历史并增加保留治理
停止持久化 provider 原始响应、兼容响应快照、attempt/event/outbox 重复 JSON,并由标准任务结果动态生成 Kling/Keling/Volces 兼容响应。 增加事件去重与预算、极简 callback 投递、7/30 天分批清理、安全删除条件、并发迁移索引及可实际恢复的任务域排除备份。历史清理默认关闭,待兼容协议和异步恢复在线验证后单独启用。 验证:Go 全量测试与 go vet、PostgreSQL 18 集成与实际备份恢复、迁移安全测试、bash -n、ShellCheck、Compose 配置和人工发布脚本测试均通过。
This commit is contained in:
@@ -2029,6 +2029,22 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
if err != nil {
|
||||
t.Fatalf("read migration %s: %v", filepath.Base(migrationPath), err)
|
||||
}
|
||||
migrationSQL := string(migration)
|
||||
const noTransactionMarker = "-- easyai:migration:no-transaction"
|
||||
if strings.HasPrefix(strings.TrimSpace(migrationSQL), noTransactionMarker) {
|
||||
migrationSQL = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(migrationSQL), noTransactionMarker))
|
||||
for _, statement := range strings.Split(migrationSQL, "-- easyai:migration:statement") {
|
||||
if statement = strings.TrimSpace(statement); statement != "" {
|
||||
if _, err := pool.Exec(ctx, statement); err != nil {
|
||||
t.Fatalf("apply non-transaction migration %s: %v", filepath.Base(migrationPath), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := pool.Exec(ctx, "INSERT INTO schema_migrations(version) VALUES($1)", version); err != nil {
|
||||
t.Fatalf("record non-transaction migration %s: %v", filepath.Base(migrationPath), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("begin migration %s: %v", filepath.Base(migrationPath), err)
|
||||
|
||||
@@ -202,15 +202,11 @@ func (s *Server) createKelingOmniVideo(w http.ResponseWriter, r *http.Request) {
|
||||
writeKelingCompatError(w, requestID, kelingCompatGatewayError(createErr))
|
||||
return
|
||||
}
|
||||
if kelingTaskUsesNativeProtocol(task) && len(task.CompatibilitySubmitBody) > 0 {
|
||||
writeJSON(w, task.CompatibilitySubmitHTTPStatus, task.CompatibilitySubmitBody)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, KelingCompatibleEnvelope{
|
||||
Code: 0,
|
||||
Message: "SUCCEED",
|
||||
RequestID: requestID,
|
||||
Data: kelingCompatTaskData(task),
|
||||
Data: kelingCompatSubmissionData(task),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -251,16 +247,6 @@ func (s *Server) getKelingOmniVideo(w http.ResponseWriter, r *http.Request) {
|
||||
writeKelingCompatError(w, requestID, newKelingCompatError(http.StatusNotFound, 1203, "task not found"))
|
||||
return
|
||||
}
|
||||
if kelingTaskUsesNativeProtocol(task) {
|
||||
if raw, ok := task.Result["raw"].(map[string]any); ok && len(raw) > 0 {
|
||||
writeJSON(w, http.StatusOK, raw)
|
||||
return
|
||||
}
|
||||
if len(task.RemoteTaskPayload) > 0 {
|
||||
writeJSON(w, http.StatusOK, task.RemoteTaskPayload)
|
||||
return
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, KelingCompatibleEnvelope{
|
||||
Code: 0,
|
||||
Message: "SUCCEED",
|
||||
@@ -705,15 +691,22 @@ func kelingCompatTaskData(task store.GatewayTask) map[string]any {
|
||||
return data
|
||||
}
|
||||
|
||||
func kelingCompatPublicID(task store.GatewayTask) string {
|
||||
if publicID := strings.TrimSpace(task.CompatibilityPublicID); publicID != "" {
|
||||
return publicID
|
||||
}
|
||||
return task.ID
|
||||
func kelingCompatSubmissionData(task store.GatewayTask) map[string]any {
|
||||
data := kelingCompatTaskData(task)
|
||||
data["task_status"] = "submitted"
|
||||
delete(data, "task_status_code")
|
||||
delete(data, "task_status_msg")
|
||||
delete(data, "task_result")
|
||||
return data
|
||||
}
|
||||
|
||||
func kelingTaskUsesNativeProtocol(task store.GatewayTask) bool {
|
||||
return task.CompatibilityProtocol == clients.ProtocolKlingV1Omni && task.CompatibilitySourceProtocol == clients.ProtocolKlingV1Omni
|
||||
func kelingCompatPublicID(task store.GatewayTask) string {
|
||||
if task.CompatibilityProtocol == task.CompatibilitySourceProtocol {
|
||||
if remoteTaskID := strings.TrimSpace(task.RemoteTaskID); remoteTaskID != "" {
|
||||
return remoteTaskID
|
||||
}
|
||||
}
|
||||
return task.ID
|
||||
}
|
||||
|
||||
func kelingCompatTaskStatus(status string) string {
|
||||
|
||||
@@ -209,6 +209,10 @@ func TestKelingCompatTaskDataAndOwnership(t *testing.T) {
|
||||
if video["id"] != "video-1" || video["watermark_url"] != "https://example.com/watermarked.mp4" || video["duration"] != "5" {
|
||||
t.Fatalf("unexpected compatible video: %+v", video)
|
||||
}
|
||||
submission := kelingCompatSubmissionData(task)
|
||||
if submission["task_status"] != "submitted" || submission["task_result"] != nil {
|
||||
t.Fatalf("submission response must be stable and minimal: %+v", submission)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireKelingAPIKeyWritesOfficialAuthEnvelope(t *testing.T) {
|
||||
|
||||
@@ -177,10 +177,10 @@ func (s *Server) createKlingCompatTask(w http.ResponseWriter, r *http.Request, v
|
||||
w.Header().Set("Idempotent-Replayed", "true")
|
||||
}
|
||||
if version == "v2" {
|
||||
writeJSON(w, http.StatusOK, klingV2Envelope(task))
|
||||
writeJSON(w, http.StatusOK, klingV2SubmissionEnvelope(task))
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, klingV1Envelope(task))
|
||||
writeJSON(w, http.StatusOK, klingV1SubmissionEnvelope(task))
|
||||
}
|
||||
|
||||
func klingCompatTaskBody(version string, model string, native map[string]any) (map[string]any, string, error) {
|
||||
@@ -612,20 +612,17 @@ func (s *Server) klingV2ListTasks(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func klingV1Envelope(task store.GatewayTask) map[string]any {
|
||||
if klingTaskUsesNativeV1Protocol(task) {
|
||||
if raw := klingMap(task.Result["raw"]); len(raw) > 0 {
|
||||
return raw
|
||||
}
|
||||
if len(task.RemoteTaskPayload) > 0 {
|
||||
return task.RemoteTaskPayload
|
||||
}
|
||||
if raw := klingMap(task.CompatibilitySubmitBody); len(raw) > 0 {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
return map[string]any{"code": 0, "message": "success", "request_id": firstNonEmpty(task.RequestID, task.ID), "data": klingV1TaskData(task)}
|
||||
}
|
||||
|
||||
func klingV1SubmissionEnvelope(task store.GatewayTask) map[string]any {
|
||||
data := klingV1TaskData(task)
|
||||
data["task_status"] = "submitted"
|
||||
delete(data, "task_status_msg")
|
||||
delete(data, "task_result")
|
||||
return map[string]any{"code": 0, "message": "success", "request_id": firstNonEmpty(task.RequestID, task.ID), "data": data}
|
||||
}
|
||||
|
||||
func klingV1TaskData(task store.GatewayTask) map[string]any {
|
||||
data := map[string]any{
|
||||
"task_id": klingCompatibilityPublicID(task), "task_status": klingV1Status(task.Status),
|
||||
@@ -646,20 +643,17 @@ func klingV1TaskData(task store.GatewayTask) map[string]any {
|
||||
}
|
||||
|
||||
func klingV2Envelope(task store.GatewayTask) map[string]any {
|
||||
if task.CompatibilityProtocol == clients.ProtocolKlingV2Omni && task.CompatibilitySourceProtocol == clients.ProtocolKlingV2Omni {
|
||||
if raw := klingMap(task.Result["raw"]); len(raw) > 0 {
|
||||
return raw
|
||||
}
|
||||
if len(task.RemoteTaskPayload) > 0 {
|
||||
return task.RemoteTaskPayload
|
||||
}
|
||||
if len(task.CompatibilitySubmitBody) > 0 {
|
||||
return task.CompatibilitySubmitBody
|
||||
}
|
||||
}
|
||||
return map[string]any{"code": 0, "message": "success", "request_id": firstNonEmpty(task.RequestID, task.ID), "data": klingV2TaskData(task)}
|
||||
}
|
||||
|
||||
func klingV2SubmissionEnvelope(task store.GatewayTask) map[string]any {
|
||||
data := klingV2TaskData(task)
|
||||
data["status"] = "submitted"
|
||||
delete(data, "message")
|
||||
delete(data, "outputs")
|
||||
return map[string]any{"code": 0, "message": "success", "request_id": firstNonEmpty(task.RequestID, task.ID), "data": data}
|
||||
}
|
||||
|
||||
func klingV2TaskData(task store.GatewayTask) map[string]any {
|
||||
data := map[string]any{
|
||||
"id": klingCompatibilityPublicID(task), "status": klingV2Status(task.Status),
|
||||
@@ -676,16 +670,14 @@ func klingV2TaskData(task store.GatewayTask) map[string]any {
|
||||
}
|
||||
|
||||
func klingCompatibilityPublicID(task store.GatewayTask) string {
|
||||
if publicID := strings.TrimSpace(task.CompatibilityPublicID); publicID != "" {
|
||||
return publicID
|
||||
if task.CompatibilityProtocol == task.CompatibilitySourceProtocol {
|
||||
if remoteTaskID := strings.TrimSpace(task.RemoteTaskID); remoteTaskID != "" {
|
||||
return remoteTaskID
|
||||
}
|
||||
}
|
||||
return task.ID
|
||||
}
|
||||
|
||||
func klingTaskUsesNativeV1Protocol(task store.GatewayTask) bool {
|
||||
return task.CompatibilityProtocol == clients.ProtocolKlingV1Omni && task.CompatibilitySourceProtocol == clients.ProtocolKlingV1Omni
|
||||
}
|
||||
|
||||
func klingMap(value any) map[string]any {
|
||||
result, _ := value.(map[string]any)
|
||||
return result
|
||||
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func TestKlingV1O1CompatibilityBody(t *testing.T) {
|
||||
@@ -86,6 +89,27 @@ func TestKlingV2CompatibilityBody(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestKlingSubmissionEnvelopesDoNotReplayCurrentResult(t *testing.T) {
|
||||
task := store.GatewayTask{
|
||||
ID: "gateway-task", Status: "succeeded", RemoteTaskID: "upstream-task",
|
||||
CompatibilityProtocol: "kling-v1-omni",
|
||||
CompatibilitySourceProtocol: "kling-v1-omni",
|
||||
Result: map[string]any{"data": []any{map[string]any{"url": "https://example.invalid/result.mp4"}}},
|
||||
CreatedAt: time.Unix(100, 0),
|
||||
UpdatedAt: time.Unix(101, 0),
|
||||
}
|
||||
v1 := klingV1SubmissionEnvelope(task)
|
||||
v1Data, _ := v1["data"].(map[string]any)
|
||||
if v1Data["task_status"] != "submitted" || v1Data["task_result"] != nil {
|
||||
t.Fatalf("V1 submission envelope=%#v", v1)
|
||||
}
|
||||
v2 := klingV2SubmissionEnvelope(task)
|
||||
v2Data, _ := v2["data"].(map[string]any)
|
||||
if v2Data["status"] != "submitted" || v2Data["outputs"] != nil {
|
||||
t.Fatalf("V2 submission envelope=%#v", v2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKlingCompatibilityValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -122,6 +122,7 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
|
||||
server.auth.LocalAPIKeyVerifier = db.VerifyLocalAPIKey
|
||||
server.runner.StartAsyncQueueWorker(ctx)
|
||||
server.runner.StartBillingSettlementWorker(ctx)
|
||||
server.runner.StartTaskHistoryWorkers(ctx)
|
||||
server.startLocalTempAssetCleanup(ctx)
|
||||
server.startOIDCSessionCleanup(ctx)
|
||||
|
||||
|
||||
@@ -223,7 +223,6 @@ func (s *Server) createVolcesCompatibleTask(r *http.Request, user *auth.User, bo
|
||||
return store.GatewayTask{}, err
|
||||
}
|
||||
task.CompatibilityProtocol = clients.ProtocolVolcesContents
|
||||
task.CompatibilityPublicID = task.ID
|
||||
if err := s.runner.EnqueueAsyncTask(r.Context(), task); err != nil {
|
||||
return store.GatewayTask{}, &clients.ClientError{Code: "enqueue_failed", Message: err.Error(), StatusCode: http.StatusServiceUnavailable, Retryable: true}
|
||||
}
|
||||
@@ -258,7 +257,6 @@ func (s *Server) waitForCompatibilitySubmission(r *http.Request, task store.Gate
|
||||
Message: firstNonEmpty(current.ErrorMessage, current.Error, current.Message),
|
||||
StatusCode: status,
|
||||
Retryable: false,
|
||||
Wire: compatibilitySubmissionWire(current),
|
||||
}
|
||||
}
|
||||
select {
|
||||
@@ -271,34 +269,6 @@ func (s *Server) waitForCompatibilitySubmission(r *http.Request, task store.Gate
|
||||
}
|
||||
}
|
||||
|
||||
func compatibilitySubmissionWire(task store.GatewayTask) *clients.WireResponse {
|
||||
if task.CompatibilitySubmitHTTPStatus == 0 || strings.TrimSpace(task.CompatibilitySourceProtocol) == "" {
|
||||
return nil
|
||||
}
|
||||
headers := map[string][]string{}
|
||||
for name, value := range task.CompatibilitySubmitHeaders {
|
||||
switch typed := value.(type) {
|
||||
case []any:
|
||||
for _, item := range typed {
|
||||
if text := strings.TrimSpace(volcesCompatString(item)); text != "" {
|
||||
headers[name] = append(headers[name], text)
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
headers[name] = append([]string(nil), typed...)
|
||||
case string:
|
||||
headers[name] = []string{typed}
|
||||
}
|
||||
}
|
||||
return &clients.WireResponse{
|
||||
Protocol: task.CompatibilitySourceProtocol,
|
||||
StatusCode: task.CompatibilitySubmitHTTPStatus,
|
||||
Headers: headers,
|
||||
Body: cloneVolcesCompatibleMap(task.CompatibilitySubmitBody),
|
||||
Converted: task.CompatibilitySourceProtocol != task.CompatibilityProtocol,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) volcesCompatibleTaskForUser(w http.ResponseWriter, r *http.Request) (store.GatewayTask, bool) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
if !ok || user == nil {
|
||||
@@ -327,15 +297,30 @@ func isVolcesCompatibleTask(task store.GatewayTask) bool {
|
||||
}
|
||||
|
||||
func volcesCompatibleTask(task store.GatewayTask) map[string]any {
|
||||
if volcesTaskUsesNativeProtocol(task) {
|
||||
if raw := cloneVolcesCompatibleMap(mapFromVolcesCompat(task.Result["raw"])); len(raw) > 0 {
|
||||
return raw
|
||||
response := map[string]any{}
|
||||
for _, key := range []string{"model", "seed", "resolution", "ratio", "duration", "frames", "framespersecond"} {
|
||||
if task.Result[key] != nil {
|
||||
response[key] = task.Result[key]
|
||||
}
|
||||
if raw := cloneVolcesCompatibleMap(task.RemoteTaskPayload); len(raw) > 0 {
|
||||
return raw
|
||||
}
|
||||
if content, ok := task.Result["content"].(map[string]any); ok && len(content) > 0 {
|
||||
// Transitional read support for historical canonical results. New
|
||||
// results only persist data[] and reconstruct this protocol field.
|
||||
response["content"] = content
|
||||
} else if data, ok := task.Result["data"].([]any); ok && len(data) > 0 {
|
||||
if item, ok := data[0].(map[string]any); ok {
|
||||
content := map[string]any{}
|
||||
if videoURL := firstNonEmpty(volcesCompatString(item["url"]), volcesCompatString(item["video_url"])); videoURL != "" {
|
||||
content["video_url"] = videoURL
|
||||
}
|
||||
if lastFrameURL := volcesCompatString(item["last_frame_url"]); lastFrameURL != "" {
|
||||
content["last_frame_url"] = lastFrameURL
|
||||
}
|
||||
if len(content) > 0 {
|
||||
response["content"] = content
|
||||
}
|
||||
}
|
||||
}
|
||||
response := map[string]any{}
|
||||
response["id"] = volcesCompatiblePublicID(task)
|
||||
response["model"] = firstNonEmpty(volcesCompatString(response["model"]), task.Model)
|
||||
response["status"] = volcesCompatibleTaskStatus(task.Status)
|
||||
@@ -346,8 +331,10 @@ func volcesCompatibleTask(task store.GatewayTask) map[string]any {
|
||||
response[key] = task.Request[key]
|
||||
}
|
||||
}
|
||||
if len(task.Usage) > 0 && response["usage"] == nil {
|
||||
response["usage"] = task.Usage
|
||||
if usage := volcesCompatibleUsage(task.Usage); len(usage) > 0 {
|
||||
response["usage"] = usage
|
||||
} else if legacyUsage, ok := task.Result["usage"].(map[string]any); ok && len(legacyUsage) > 0 {
|
||||
response["usage"] = legacyUsage
|
||||
}
|
||||
if task.Status == "failed" || task.Status == "cancelled" {
|
||||
response["error"] = map[string]any{"code": firstNonEmpty(task.ErrorCode, strings.ToUpper(task.Status)), "message": firstNonEmpty(task.ErrorMessage, task.Error, task.Message)}
|
||||
@@ -355,19 +342,28 @@ func volcesCompatibleTask(task store.GatewayTask) map[string]any {
|
||||
return response
|
||||
}
|
||||
|
||||
func volcesCompatibleCreateResponse(task store.GatewayTask) map[string]any {
|
||||
if volcesTaskUsesNativeProtocol(task) {
|
||||
if raw := cloneVolcesCompatibleMap(task.RemoteTaskPayload); len(raw) > 0 {
|
||||
return raw
|
||||
func volcesCompatibleUsage(usage map[string]any) map[string]any {
|
||||
out := map[string]any{}
|
||||
for outputKey, inputKeys := range map[string][]string{
|
||||
"prompt_tokens": {"inputTokens", "promptTokens"},
|
||||
"completion_tokens": {"outputTokens", "completionTokens"},
|
||||
"total_tokens": {"totalTokens"},
|
||||
} {
|
||||
for _, inputKey := range inputKeys {
|
||||
if value := usage[inputKey]; value != nil {
|
||||
out[outputKey] = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func volcesCompatibleCreateResponse(task store.GatewayTask) map[string]any {
|
||||
return map[string]any{"id": volcesCompatiblePublicID(task)}
|
||||
}
|
||||
|
||||
func volcesCompatiblePublicID(task store.GatewayTask) string {
|
||||
if strings.TrimSpace(task.CompatibilityPublicID) != "" {
|
||||
return strings.TrimSpace(task.CompatibilityPublicID)
|
||||
}
|
||||
if volcesTaskUsesNativeProtocol(task) && strings.TrimSpace(task.RemoteTaskID) != "" {
|
||||
return strings.TrimSpace(task.RemoteTaskID)
|
||||
}
|
||||
@@ -386,11 +382,6 @@ func volcesTaskUsesNativeProtocol(task store.GatewayTask) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func mapFromVolcesCompat(value any) map[string]any {
|
||||
result, _ := value.(map[string]any)
|
||||
return result
|
||||
}
|
||||
|
||||
func volcesCompatibleTaskStatus(status string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case "succeeded", "success", "completed":
|
||||
@@ -406,21 +397,6 @@ func volcesCompatibleTaskStatus(status string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func cloneVolcesCompatibleMap(source map[string]any) map[string]any {
|
||||
if len(source) == 0 {
|
||||
return nil
|
||||
}
|
||||
raw, err := json.Marshal(source)
|
||||
if err != nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(raw, &out); err != nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func writeVolcesCompatibleTaskError(w http.ResponseWriter, err error) {
|
||||
status := http.StatusInternalServerError
|
||||
var staged *gatewayTaskCreationError
|
||||
|
||||
@@ -7,23 +7,22 @@ import (
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func TestVolcesCompatibleTaskPreservesNativeOfficialFieldsWithoutGatewayExtensions(t *testing.T) {
|
||||
func TestVolcesCompatibleTaskBuildsProtocolResponseFromCanonicalResult(t *testing.T) {
|
||||
now := time.Date(2026, 7, 18, 8, 0, 0, 0, time.UTC)
|
||||
task := store.GatewayTask{
|
||||
ID: "gateway-task-1", Kind: "videos.generations", Status: "succeeded", Model: "doubao-seedance-2-0-mini-260615",
|
||||
RemoteTaskID: "cgt-upstream-1", CreatedAt: now, UpdatedAt: now.Add(time.Second),
|
||||
Result: map[string]any{
|
||||
"raw": map[string]any{
|
||||
"id": "cgt-upstream-1", "model": "doubao-seedance-2-0-mini-260615", "status": "succeeded",
|
||||
"content": map[string]any{"video_url": "https://example.com/out.mp4"}, "usage": map[string]any{"total_tokens": 9},
|
||||
"future_official_field": "preserved",
|
||||
},
|
||||
"id": "cgt-upstream-1", "model": "doubao-seedance-2-0-mini-260615", "status": "succeeded",
|
||||
"data": []any{map[string]any{"url": "https://example.com/out.mp4", "type": "video"}},
|
||||
"future_official_field": "not-whitelisted",
|
||||
},
|
||||
Usage: map[string]any{"totalTokens": 9},
|
||||
Billings: []any{map[string]any{"amount": 3}}, BillingSummary: map[string]any{"currency": "resource"}, FinalChargeAmount: 3,
|
||||
Attempts: []store.TaskAttempt{{Provider: "volces"}},
|
||||
}
|
||||
got := volcesCompatibleTask(task)
|
||||
if got["id"] != task.RemoteTaskID || got["status"] != "succeeded" || got["future_official_field"] != "preserved" {
|
||||
if got["id"] != task.RemoteTaskID || got["status"] != "succeeded" || got["future_official_field"] != nil {
|
||||
t.Fatalf("unexpected compatibility identity/status: %+v", got)
|
||||
}
|
||||
content, _ := got["content"].(map[string]any)
|
||||
|
||||
Reference in New Issue
Block a user