197 lines
5.4 KiB
Go
197 lines
5.4 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package transport
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
|
)
|
|
|
|
// ExtractServerDiagnostics parses server diagnostic fields from a JSON
|
|
// payload (typically from RPCError.Data). Returns an empty struct if
|
|
// the payload is empty or unparseable.
|
|
func ExtractServerDiagnostics(data json.RawMessage) apperrors.ServerDiagnostics {
|
|
if len(data) == 0 {
|
|
return apperrors.ServerDiagnostics{}
|
|
}
|
|
var content map[string]any
|
|
if json.Unmarshal(data, &content) != nil {
|
|
return apperrors.ServerDiagnostics{}
|
|
}
|
|
return ExtractServerDiagnosticsFromMap(content)
|
|
}
|
|
|
|
// ExtractServerDiagnosticsFromMap parses server diagnostic fields from a
|
|
// map[string]any (typically from ToolCallResult.Content for business errors).
|
|
func ExtractServerDiagnosticsFromMap(content map[string]any) apperrors.ServerDiagnostics {
|
|
if len(content) == 0 {
|
|
return apperrors.ServerDiagnostics{}
|
|
}
|
|
diag := apperrors.ServerDiagnostics{
|
|
TraceID: stringFromMapRecursive(content, 0, "trace_id", "traceId", "request_id", "requestId"),
|
|
ServerErrorCode: serverErrorCodeFromMap(content, 0),
|
|
TechnicalDetail: stringFromMapRecursive(content, 0, "technical_detail", "technicalDetail"),
|
|
FriendlyHint: stringFromMapRecursive(content, 0, "friendly_hint", "friendlyHint"),
|
|
ActionURL: stringFromMapRecursive(content, 0, "action_url", "actionUrl"),
|
|
}
|
|
if v, ok := boolFromMapRecursive(content, 0, "retryable", "server_retryable", "serverRetryable"); ok {
|
|
diag.ServerRetryable = &v
|
|
}
|
|
return diag
|
|
}
|
|
|
|
func boolFromMapRecursive(content map[string]any, depth int, keys ...string) (bool, bool) {
|
|
if content == nil || depth > 8 {
|
|
return false, false
|
|
}
|
|
for _, key := range keys {
|
|
if value, ok := content[key].(bool); ok {
|
|
return value, true
|
|
}
|
|
}
|
|
for _, key := range []string{"content", "result", "data"} {
|
|
switch child := content[key].(type) {
|
|
case map[string]any:
|
|
if value, ok := boolFromMapRecursive(child, depth+1, keys...); ok {
|
|
return value, true
|
|
}
|
|
case []any:
|
|
for _, item := range child {
|
|
childMap, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if value, ok := boolFromMapRecursive(childMap, depth+1, keys...); ok {
|
|
return value, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
func stringFromMapRecursive(content map[string]any, depth int, keys ...string) string {
|
|
if content == nil || depth > 8 {
|
|
return ""
|
|
}
|
|
if value := stringFromMap(content, keys...); value != "" {
|
|
return value
|
|
}
|
|
for _, key := range []string{"content", "result", "data"} {
|
|
switch child := content[key].(type) {
|
|
case map[string]any:
|
|
if value := stringFromMapRecursive(child, depth+1, keys...); value != "" {
|
|
return value
|
|
}
|
|
case []any:
|
|
for _, item := range child {
|
|
childMap, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if value := stringFromMapRecursive(childMap, depth+1, keys...); value != "" {
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ExtractTraceIDFromHeaders reads a trace ID from standard HTTP response
|
|
// headers. Returns empty string if none found.
|
|
func ExtractTraceIDFromHeaders(headers http.Header) string {
|
|
for _, key := range []string{
|
|
"X-Trace-Id",
|
|
"X-Request-Id",
|
|
"x-dingtalk-trace-id",
|
|
} {
|
|
if v := headers.Get(key); v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// coalesceStr returns the first non-empty string.
|
|
func coalesceStr(values ...string) string {
|
|
for _, v := range values {
|
|
if v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// stringFromMap returns the first non-empty string value found for any of
|
|
// the given keys in the map.
|
|
func stringFromMap(m map[string]any, keys ...string) string {
|
|
for _, key := range keys {
|
|
if v, ok := m[key].(string); ok && v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func serverErrorCodeFromMap(content map[string]any, depth int) string {
|
|
if content == nil || depth > 8 {
|
|
return ""
|
|
}
|
|
direct := stringFromMap(content, "errorCode", "code", "server_error_code")
|
|
if isWrapperServerCode(direct) {
|
|
if nested := nestedServerErrorCode(content, depth); nested != "" {
|
|
return nested
|
|
}
|
|
}
|
|
if direct != "" {
|
|
return direct
|
|
}
|
|
return nestedServerErrorCode(content, depth)
|
|
}
|
|
|
|
func nestedServerErrorCode(content map[string]any, depth int) string {
|
|
for _, key := range []string{"content", "result", "data"} {
|
|
switch child := content[key].(type) {
|
|
case map[string]any:
|
|
if code := serverErrorCodeFromMap(child, depth+1); code != "" {
|
|
return code
|
|
}
|
|
case []any:
|
|
for _, item := range child {
|
|
childMap, ok := item.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if code := serverErrorCodeFromMap(childMap, depth+1); code != "" {
|
|
return code
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func isWrapperServerCode(code string) bool {
|
|
switch strings.ToUpper(strings.TrimSpace(code)) {
|
|
case "ERROR", "BUSINESS_ERROR", "-1":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|