This commit is contained in:
2026-07-23 15:59:00 +08:00
parent 9ab7ba66f3
commit 3bf04be347
19 changed files with 1144 additions and 34 deletions

View File

@@ -5,14 +5,21 @@ import (
)
// generateCurlCommand 生成可直接复现的 curl 命令,便于联调排查。
func generateCurlCommand(method, url string, headers map[string]string, body string) string {
// proxyURL 非空时附加 -x便于确认线上是否应走 SOCKS。
func generateCurlCommand(method, requestURL string, headers map[string]string, body, proxyURL string) string {
var cmd strings.Builder
cmd.WriteString("curl -X ")
cmd.WriteString(method)
cmd.WriteString(" '")
cmd.WriteString(url)
cmd.WriteString(requestURL)
cmd.WriteString("'")
if p := strings.TrimSpace(proxyURL); p != "" {
cmd.WriteString(" \\\n -x '")
cmd.WriteString(escapeSingleQuotes(p))
cmd.WriteString("'")
}
for key, value := range headers {
cmd.WriteString(" \\\n -H '")
cmd.WriteString(key)

View File

@@ -10,6 +10,7 @@ import (
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
@@ -163,7 +164,7 @@ func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData m
"Content-Type": "application/json",
headerDmsToken: token,
}
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr)
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr, s.config.Proxy)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes))
if err != nil {
@@ -298,7 +299,7 @@ func (s *RongxingService) login(ctx context.Context, transactionID string) (stri
bodyStr := string(bodyBytes)
headers := map[string]string{"Content-Type": "application/json"}
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr)
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr, s.config.Proxy)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes))
if err != nil {
@@ -356,7 +357,7 @@ func (s *RongxingService) login(ctx context.Context, transactionID string) (stri
func (s *RongxingService) doHTTP(req *http.Request) ([]byte, int, error) {
resp, err := s.client.Do(req)
if err != nil {
return nil, 0, err
return nil, 0, s.wrapTransportError(req.URL.String(), err)
}
defer resp.Body.Close()
@@ -367,6 +368,48 @@ func (s *RongxingService) doHTTP(req *http.Request) ([]byte, int, error) {
return body, resp.StatusCode, nil
}
// wrapTransportError 把超时/拒连/代理失败等包装成可读诊断,便于区分「联不通」原因。
func (s *RongxingService) wrapTransportError(targetURL string, err error) error {
diag := classifyTransportError(err)
proxyMode := strings.TrimSpace(s.config.Proxy)
if proxyMode == "" {
proxyMode = "(直连,未配置 proxy)"
}
return fmt.Errorf(
"戎行 HTTP 失败 target=%s proxy=%s diagnosis=%s cause=%w",
targetURL, proxyMode, diag, err,
)
}
func classifyTransportError(err error) string {
if err == nil {
return "unknown"
}
msg := err.Error()
switch {
case os.IsTimeout(err) || errors.Is(err, context.DeadlineExceeded) ||
strings.Contains(msg, "Client.Timeout") || strings.Contains(msg, "deadline exceeded"):
return "请求超时(未在 timeout 内收到响应头;可能:目标 192.168.3.43:7007 不可达、VPN/SOCKS 未转发、或服务无响应)"
case strings.Contains(msg, "connection refused"):
return "连接被拒绝(端口未监听或代理/目标拒绝)"
case strings.Contains(msg, "no such host") || strings.Contains(msg, "lookup"):
return "DNS/主机名解析失败(检查 rongxing-vpn 服务名或目标域名)"
case strings.Contains(msg, "network is unreachable") || strings.Contains(msg, "no route to host"):
return "网络不可达(无路由;直连内网 IP 时常见于未走 VPN/代理)"
case strings.Contains(msg, "i/o timeout") || strings.Contains(msg, "TLS handshake timeout"):
return "传输层超时(链路通但握手/读写超时)"
case strings.Contains(msg, "proxy") || strings.Contains(msg, "socks"):
return "代理链路异常(检查 socks5://rongxing-vpn:1080 与 VPN 容器)"
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return "网络超时"
}
return "其他网络错误(详见 cause)"
}
func (s *RongxingService) validateConfig() error {
if s.config.BaseURL == "" {
return errors.New("戎行 url 未配置")
@@ -429,12 +472,35 @@ func (s *RongxingService) logErrorWithCurl(transactionID, apiKey string, err err
if s.logger == nil {
return
}
proxyMode := strings.TrimSpace(s.config.Proxy)
if proxyMode == "" {
proxyMode = "(直连,未配置 proxy)"
}
s.logger.LogErrorWithFields("rongxing API错误",
zap.String("transaction_id", transactionID),
zap.String("api_code", apiKey),
zap.String("base_url", s.config.BaseURL),
zap.String("proxy", proxyMode),
zap.String("diagnosis", extractDiagnosis(err)),
zap.Error(err),
zap.Any("params", payload),
zap.String("curl", curlCmd),
zap.String("response_body", respBody),
)
}
func extractDiagnosis(err error) string {
if err == nil {
return ""
}
const marker = "diagnosis="
msg := err.Error()
if i := strings.Index(msg, marker); i >= 0 {
rest := msg[i+len(marker):]
if j := strings.Index(rest, " cause="); j >= 0 {
return rest[:j]
}
return rest
}
return classifyTransportError(err)
}

View File

@@ -0,0 +1,43 @@
package rongxing
import (
"context"
"errors"
"strings"
"testing"
"time"
)
func TestClassifyTransportError_Timeout(t *testing.T) {
err := errors.New(`Post "http://192.168.3.43:7007/auth/login": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`)
got := classifyTransportError(err)
if !strings.Contains(got, "请求超时") {
t.Fatalf("got %q", got)
}
}
func TestClassifyTransportError_Deadline(t *testing.T) {
got := classifyTransportError(context.DeadlineExceeded)
if !strings.Contains(got, "请求超时") {
t.Fatalf("got %q", got)
}
}
func TestWrapTransportError_IncludesProxy(t *testing.T) {
svc, err := NewRongxingService(serviceConfig{
BaseURL: "http://192.168.3.43:7007",
Timeout: time.Second,
Proxy: "socks5://rongxing-vpn:1080",
}, nil)
if err != nil {
t.Fatal(err)
}
wrapped := svc.wrapTransportError("http://192.168.3.43:7007/auth/login", context.DeadlineExceeded)
msg := wrapped.Error()
if !strings.Contains(msg, "proxy=socks5://rongxing-vpn:1080") {
t.Fatalf("missing proxy in error: %s", msg)
}
if !strings.Contains(msg, "diagnosis=") {
t.Fatalf("missing diagnosis in error: %s", msg)
}
}