fix(bedrock): 代理改为反向代理模式,兼容不支持 CONNECT 的代理

- 取消 http.Transport.Proxy(要求代理支持 HTTP CONNECT 隧道)
- 改为直接向代理地址发 HTTP 请求(host:port),路径同 Bedrock 原生 API
- httpReq.Host 设为真实 Bedrock host,SigV4 仍针对真实 host 签名后复制头部
- 支持凭证内 bedrock_proxy_url 与全局 BEDROCK_PROXY 环境变量(优先凭证)
- config.Gaia 增加 BedrockProxy 字段,overrideGaiaFromEnv 从 BEDROCK_PROXY 读取
This commit is contained in:
npc0-hue
2026-04-24 10:58:58 +08:00
parent 618a355ec8
commit efc25217dc
3 changed files with 48 additions and 36 deletions
+1 -1
View File
@@ -6,5 +6,5 @@ type Gaia struct {
SuperAdminAccountId string `mapstructure:"SUPER_ADMIN_ACCOUNT_ID" json:"SUPER_ADMIN_ACCOUNT_ID" yaml:"SUPER_ADMIN_ACCOUNT_ID"` // 超级管理员账号
SuperAdminTenantId string `mapstructure:"SUPER_ADMIN_TENANT_ID" json:"SUPER_ADMIN_TENANT_ID" yaml:"SUPER_ADMIN_TENANT_ID"` // 系统默认工作区
StoragePath string `mapstructure:"storage-path" json:"storage-path" yaml:"storage-path"` // Dify storage 目录路径,用于读取私钥
BedrockProxy string `mapstructure:"bedrock_proxy" json:"bedrock_proxy" yaml:"bedrock_proxy"` // 全局 Bedrock HTTP 代理(host:port 或 http://host:port),凭证未配置 bedrock_proxy_url 时使用
BedrockProxy string `mapstructure:"bedrock_proxy" json:"bedrock_proxy" yaml:"bedrock_proxy"` // 全局 Bedrock 代理地址host:port 或 http://host:port),凭证未配置 bedrock_proxy_url 时使用
}
+8 -8
View File
@@ -121,16 +121,16 @@ func overrideRedisFromEnv() {
// overrideAllFromEnv 从环境变量覆盖所有配置
func overrideAllFromEnv() {
overrideDBFromEnv()
overrideRedisFromEnv()
overrideGaiaFromEnv()
overrideDBFromEnv()
overrideRedisFromEnv()
overrideGaiaFromEnv()
}
// overrideGaiaFromEnv 从环境变量覆盖 Gaia 配置
func overrideGaiaFromEnv() {
// BEDROCK_PROXY: 全局 Bedrock HTTP 代理,与 Dify Python 侧 BEDROCK_PROXY 含义一致
if proxy := os.Getenv("BEDROCK_PROXY"); proxy != "" {
global.GVA_CONFIG.Gaia.BedrockProxy = proxy
fmt.Printf("Bedrock proxy overridden from BEDROCK_PROXY environment variable: %s\n", proxy)
}
// BEDROCK_PROXY: 全局 Bedrock 反向代理地址,与 Dify Python 侧 BEDROCK_PROXY 含义一致
if proxy := os.Getenv("BEDROCK_PROXY"); proxy != "" {
global.GVA_CONFIG.Gaia.BedrockProxy = proxy
fmt.Printf("Bedrock proxy overridden from BEDROCK_PROXY environment variable: %s\n", proxy)
}
}
+39 -27
View File
@@ -7,7 +7,6 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
@@ -74,18 +73,40 @@ func (s *ModelProviderService) proxyBedrockRequest(
return fmt.Errorf("重写 Bedrock 请求 body 失败:%w", err)
}
// 3) 构建 Bedrock URL
host := fmt.Sprintf("bedrock-runtime.%s.amazonaws.com", region)
// 3) 确定代理地址(优先凭证内 bedrock_proxy_url,其次全局 BEDROCK_PROXY env
proxyAddr := creds.BedrockProxyURL
if proxyAddr == "" {
proxyAddr = global.GVA_CONFIG.Gaia.BedrockProxy
}
// 规范化:去除 scheme,仅保留 host:port
proxyHost := strings.TrimPrefix(strings.TrimPrefix(proxyAddr, "https://"), "http://")
proxyHost = strings.TrimRight(proxyHost, "/")
// 4) 构建请求 URL
// 真实 Bedrock hostSigV4 & Host 头使用)
bedrockHost := fmt.Sprintf("bedrock-runtime.%s.amazonaws.com", region)
op := "invoke"
if streaming {
op = "invoke-with-response-stream"
}
requestURL := fmt.Sprintf("https://%s/model/%s/%s", host, modelID, op)
var requestURL string
if proxyHost != "" {
// 反向代理模式:发普通 HTTP 到代理地址,不需要代理支持 CONNECT 隧道
requestURL = fmt.Sprintf("http://%s/model/%s/%s", proxyHost, modelID, op)
} else {
requestURL = fmt.Sprintf("https://%s/model/%s/%s", bedrockHost, modelID, op)
}
httpReq, err := http.NewRequest(method, requestURL, bytes.NewReader(rewritten))
if err != nil {
return fmt.Errorf("构建 Bedrock 请求失败:%w", err)
}
// 代理模式下显式设置 Host 为真实 Bedrock 地址,使 SigV4 校验通过
if proxyHost != "" {
httpReq.Host = bedrockHost
global.GVA_LOG.Info("Bedrock 经反向代理转发",
zap.String("proxy", proxyHost), zap.String("bedrockHost", bedrockHost), zap.String("model", modelID))
}
httpReq.Header.Set("Content-Type", "application/json")
if streaming {
httpReq.Header.Set("Accept", "application/vnd.amazon.eventstream")
@@ -94,30 +115,23 @@ func (s *ModelProviderService) proxyBedrockRequest(
httpReq.Header.Set("Accept", "application/json")
}
// 4) SigV4 签名(service=bedrock
// 5) SigV4 签名(始终针对真实 Bedrock HTTPS URL 签名,再把签名头复制到实际请求
awsCreds := credentials.NewStaticCredentials(creds.AWSAccessKeyID, creds.AWSSecretAccessKey, creds.AWSSessionToken)
signer := v4.NewSigner(awsCreds)
if _, err = signer.Sign(httpReq, bytes.NewReader(rewritten), "bedrock", region, time.Now()); err != nil {
signURL := fmt.Sprintf("https://%s/model/%s/%s", bedrockHost, modelID, op)
signReq, _ := http.NewRequest(method, signURL, bytes.NewReader(rewritten))
signReq.Header.Set("Content-Type", "application/json")
if _, err = signer.Sign(signReq, bytes.NewReader(rewritten), "bedrock", region, time.Now()); err != nil {
return fmt.Errorf("Bedrock SigV4 签名失败:%w", err)
}
// 把签名产生的 Authorization / X-Amz-* 头复制到实际发送的请求
for k, vals := range signReq.Header {
httpReq.Header[k] = vals
}
// 5) 发起请求(若配置了代理则经 HTTP 代理转发:优先用凭证内 bedrock_proxy_url,其次用全局 BEDROCK_PROXY
// 6) 发起请求
startTime := time.Now()
proxyAddr := creds.BedrockProxyURL
if proxyAddr == "" {
proxyAddr = global.GVA_CONFIG.Gaia.BedrockProxy
}
transport := http.DefaultTransport
if proxyAddr != "" {
if !strings.HasPrefix(proxyAddr, "http://") && !strings.HasPrefix(proxyAddr, "https://") {
proxyAddr = "http://" + proxyAddr
}
if proxyURL, parseErr := url.Parse(proxyAddr); parseErr == nil {
transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}
global.GVA_LOG.Info("Bedrock 请求经代理转发", zap.String("proxy", proxyAddr), zap.String("model", modelID))
}
}
client := &http.Client{Timeout: 5 * time.Minute, Transport: transport}
client := &http.Client{Timeout: 5 * time.Minute}
resp, err := client.Do(httpReq)
if err != nil {
s.logBedrock(userID, modelID, "error", err.Error(), startTime, 0, 0)
@@ -125,7 +139,7 @@ func (s *ModelProviderService) proxyBedrockRequest(
}
defer func() { _ = resp.Body.Close() }()
// 6) 写回响应头/状态码(流式改写 Content-Type 为 SSE
// 7) 写回响应头/状态码(流式改写 Content-Type 为 SSE
if w, ok := writer.(http.ResponseWriter); ok {
for k, v := range resp.Header {
lower := strings.ToLower(k)
@@ -152,7 +166,7 @@ func (s *ModelProviderService) proxyBedrockRequest(
return nil
}
// 7) 处理响应体
// 8) 处理响应体
var inputTokens, outputTokens int
if streaming {
inputTokens, outputTokens, err = s.streamBedrockEventStream(resp.Body, writer)
@@ -170,7 +184,7 @@ func (s *ModelProviderService) proxyBedrockRequest(
inputTokens, outputTokens = parseAnthropicUsage(buf.Bytes())
}
// 8) 记录日志 + 计费扣款
// 9) 记录日志 + 计费扣款
s.logBedrock(userID, modelID, "success", "", startTime, inputTokens, outputTokens)
if inputTokens > 0 || outputTokens > 0 {
pricing, _ := s.fetchModelPricingFromDify(modelID)
@@ -288,5 +302,3 @@ func (s *ModelProviderService) logBedrock(userID, modelID, status, errMsg string
}