204848 commited on
Commit
c12b13c
·
1 Parent(s): c7ba3dd

feat: 更新Web端支持,强制使用HTTP/2并调整TLS拨号器以兼容IPv4

Browse files
.gitignore CHANGED
@@ -74,3 +74,4 @@ chat.deepseek.com.har.txt
74
  chat.deepseek.com2.har.txt
75
  chat.deepseek.com/
76
  chat.deepseek.com3.har
 
 
74
  chat.deepseek.com2.har.txt
75
  chat.deepseek.com/
76
  chat.deepseek.com3.har
77
+ chat.deepseek.com_2026_07_14_20_56_28.har
docs/web-endpoint-upgrade.md CHANGED
@@ -321,7 +321,8 @@ func (p *HIFPoller) GetHeaders() map[string]string // 获取当前HIF头部
321
 
322
  ```
323
  ClientHello: HelloSafari_Auto
324
- ALPN: 强制 http/1.1 (forceHTTP11ALPN)
 
325
  ```
326
 
327
  ### Web端
@@ -332,6 +333,11 @@ ALPN: 自然协商 (支持 h2 和 http/1.1)
332
  ForceAttemptHTTP2: true
333
  ```
334
 
 
 
 
 
 
335
  实现位于 `internal/deepseek/transport/transport.go`:
336
 
337
  ```go
@@ -339,7 +345,7 @@ func TLSDialerForPlatform(platform string) func(...) {
339
  if platform == "web" {
340
  return chromeTLSDialer(dialContext) // Chrome + HTTP/2
341
  }
342
- return safariTLSDialer(dialContext) // Safari + HTTP/1.1
343
  }
344
  ```
345
 
@@ -350,7 +356,7 @@ func TLSDialerForPlatform(platform string) func(...) {
350
  ### 6.1 设置面板切换
351
 
352
  访问 WebUI 设置页面,在"接口平台"选项中选择:
353
- - **Android端(默认)**:使用Safari TLS指纹,device_id为固定值
354
  - **Web端(Chrome浏览器)**:使用Chrome TLS指纹+HTTP/2,动态生成数美device_id,轮询HIF头部
355
 
356
  切换后**立即生效**,无需重启服务。
 
321
 
322
  ```
323
  ClientHello: HelloSafari_Auto
324
+ ALPN: h2 + http/1.1 (setSafariALPN,与真实 Safari 一致)
325
+ ForceAttemptHTTP2: true
326
  ```
327
 
328
  ### Web端
 
333
  ForceAttemptHTTP2: true
334
  ```
335
 
336
+ > chat.deepseek.com 现已要求 HTTP/2:HTTP/1.1 客户端会收到服务端返回的 h2
337
+ > SETTINGS 帧,被 Go 的 http.Transport 解析为 "malformed HTTP response"。
338
+ > 因此两个平台均通过 ALPN 协商 h2 并 `ForceAttemptHTTP2: true`。Android 端
339
+ > 早期曾强制 http/1.1,现已对齐真实 Safari 行为改用 h2。
340
+
341
  实现位于 `internal/deepseek/transport/transport.go`:
342
 
343
  ```go
 
345
  if platform == "web" {
346
  return chromeTLSDialer(dialContext) // Chrome + HTTP/2
347
  }
348
+ return safariTLSDialer(dialContext) // Safari + HTTP/2
349
  }
350
  ```
351
 
 
356
  ### 6.1 设置面板切换
357
 
358
  访问 WebUI 设置页面,在"接口平台"选项中选择:
359
+ - **Android端(默认)**:使用Safari TLS指纹 + HTTP/2,device_id为固定值
360
  - **Web端(Chrome浏览器)**:使用Chrome TLS指纹+HTTP/2,动态生成数美device_id,轮询HIF头部
361
 
362
  切换后**立即生效**,无需重启服务。
internal/deepseek/hif/hif.go CHANGED
@@ -37,11 +37,13 @@ type HIFPoller struct {
37
  }
38
 
39
  // NewHIFPoller creates a new HIFPoller using web-platform TLS fingerprinting.
 
 
40
  func NewHIFPoller() *HIFPoller {
41
  return &HIFPoller{
42
  stopCh: make(chan struct{}),
43
- client: trans.NewWithPlatform(15*time.Second, "web"),
44
- fallback: trans.NewFallbackClientWithPlatform(15*time.Second, nil, "web"),
45
  }
46
  }
47
 
 
37
  }
38
 
39
  // NewHIFPoller creates a new HIFPoller using web-platform TLS fingerprinting.
40
+ // 拨号器强制 IPv4:HuggingFace Spaces 等环境没有 IPv6 出口,
41
+ // hif-dliq/hif-leim 常解析到 IPv6,会导致 "network is unreachable" 反复重试。
42
  func NewHIFPoller() *HIFPoller {
43
  return &HIFPoller{
44
  stopCh: make(chan struct{}),
45
+ client: trans.NewWithDialContextAndPlatform(15*time.Second, trans.IPv4DialContext, "web"),
46
+ fallback: trans.NewFallbackClientWithPlatform(15*time.Second, trans.IPv4DialContext, "web"),
47
  }
48
  }
49
 
internal/deepseek/protocol/constants.go CHANGED
@@ -135,7 +135,7 @@ func buildBaseHeaders(client clientConstants, overrides map[string]string) map[s
135
  return out
136
  }
137
 
138
- const webUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
139
 
140
  func userAgentForPlatform(client clientConstants) string {
141
  if client.Platform == "web" {
@@ -215,7 +215,7 @@ func UserAgent() string {
215
  // config but need to be added dynamically for the web platform.
216
  func WebExtraHeaders() map[string]string {
217
  return map[string]string{
218
- "sec-ch-ua": `"Chromium";v="137", "Not/A)Brand";v="24"`,
219
  "sec-ch-ua-mobile": "?0",
220
  "sec-ch-ua-platform": `"Windows"`,
221
  "origin": "https://chat.deepseek.com",
 
135
  return out
136
  }
137
 
138
+ const webUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0"
139
 
140
  func userAgentForPlatform(client clientConstants) string {
141
  if client.Platform == "web" {
 
215
  // config but need to be added dynamically for the web platform.
216
  func WebExtraHeaders() map[string]string {
217
  return map[string]string{
218
+ "sec-ch-ua": `"Not;A=Brand";v="8", "Chromium";v="150", "Microsoft Edge";v="150"`,
219
  "sec-ch-ua-mobile": "?0",
220
  "sec-ch-ua-platform": `"Windows"`,
221
  "origin": "https://chat.deepseek.com",
internal/deepseek/protocol/constants_shared.json CHANGED
@@ -2,7 +2,7 @@
2
  "client": {
3
  "name": "DeepSeek",
4
  "platform": "android",
5
- "version": "2.1.5",
6
  "android_api_level": "32",
7
  "locale": "zh_CN"
8
  },
 
2
  "client": {
3
  "name": "DeepSeek",
4
  "platform": "android",
5
+ "version": "2.2.2",
6
  "android_api_level": "32",
7
  "locale": "zh_CN"
8
  },
internal/deepseek/protocol/constants_web.json CHANGED
@@ -2,7 +2,7 @@
2
  "client": {
3
  "name": "DeepSeek",
4
  "platform": "web",
5
- "version": "2.0.0",
6
  "locale": "zh_CN"
7
  },
8
  "base_headers": {
@@ -11,11 +11,12 @@
11
  "Content-Type": "application/json",
12
  "accept-encoding": "gzip, deflate, br, zstd",
13
  "accept-charset": "UTF-8",
14
- "x-app-version": "2.0.0",
 
15
  "x-client-locale": "zh_CN",
16
  "x-client-platform": "web",
17
  "x-client-timezone-offset": "28800",
18
- "x-client-version": "2.0.0"
19
  },
20
  "skip_contains_patterns": [
21
  "quasi_status",
 
2
  "client": {
3
  "name": "DeepSeek",
4
  "platform": "web",
5
+ "version": "2.2.0",
6
  "locale": "zh_CN"
7
  },
8
  "base_headers": {
 
11
  "Content-Type": "application/json",
12
  "accept-encoding": "gzip, deflate, br, zstd",
13
  "accept-charset": "UTF-8",
14
+ "x-app-version": "2.2.0",
15
+ "x-client-bundle-id": "com.deepseek.chat",
16
  "x-client-locale": "zh_CN",
17
  "x-client-platform": "web",
18
  "x-client-timezone-offset": "28800",
19
+ "x-client-version": "2.2.0"
20
  },
21
  "skip_contains_patterns": [
22
  "quasi_status",
internal/deepseek/transport/transport.go CHANGED
@@ -39,9 +39,10 @@ func NewWithDialContextAndPlatform(timeout time.Duration, dialContext DialContex
39
  dialContext = (&net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}).DialContext
40
  }
41
  tlsDialer := TLSDialerForPlatform(platform, dialContext)
42
- forceHTTP2 := platform == "web"
 
43
  base := &http.Transport{
44
- ForceAttemptHTTP2: forceHTTP2,
45
  MaxIdleConns: 200,
46
  MaxIdleConnsPerHost: 100,
47
  IdleConnTimeout: 90 * time.Second,
@@ -68,9 +69,10 @@ func NewFallbackClientWithPlatform(timeout time.Duration, dialContext DialContex
68
  if dialContext == nil {
69
  dialContext = (&net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}).DialContext
70
  }
71
- forceHTTP2 := platform == "web"
 
72
  base := &http.Transport{
73
- ForceAttemptHTTP2: forceHTTP2,
74
  MaxIdleConns: 200,
75
  MaxIdleConnsPerHost: 100,
76
  IdleConnTimeout: 90 * time.Second,
@@ -84,8 +86,9 @@ func NewFallbackClientWithPlatform(timeout time.Duration, dialContext DialContex
84
  }
85
 
86
  // TLSDialerForPlatform returns the appropriate TLS dialer based on the platform.
87
- // "web" uses Chrome TLS fingerprinting with HTTP/2 support;
88
- // all other values (including "android") use Safari TLS fingerprinting with HTTP/1.1 only.
 
89
  func TLSDialerForPlatform(platform string, dialContext DialContextFunc) func(ctx context.Context, network, addr string) (net.Conn, error) {
90
  if platform == "web" {
91
  return chromeTLSDialer(dialContext)
@@ -127,7 +130,9 @@ func safariTLSDialer(dialContext DialContextFunc) func(ctx context.Context, netw
127
  host, _, _ := net.SplitHostPort(addr)
128
  uCfg := &utls.Config{ServerName: host}
129
  uConn := utls.UClient(plainConn, uCfg, utls.HelloSafari_Auto)
130
- if err := forceHTTP11ALPN(uConn); err != nil {
 
 
131
  _ = plainConn.Close()
132
  return nil, err
133
  }
@@ -136,15 +141,12 @@ func safariTLSDialer(dialContext DialContextFunc) func(ctx context.Context, netw
136
  _ = plainConn.Close()
137
  return nil, err
138
  }
139
- if negotiated := uConn.ConnectionState().NegotiatedProtocol; negotiated != "" && negotiated != "http/1.1" {
140
- _ = uConn.Close()
141
- return nil, fmt.Errorf("unexpected ALPN protocol negotiated: %s", negotiated)
142
- }
143
  return uConn, nil
144
  }
145
  }
146
 
147
- func forceHTTP11ALPN(uConn *utls.UConn) error {
 
148
  if err := uConn.BuildHandshakeState(); err != nil {
149
  return err
150
  }
@@ -153,8 +155,46 @@ func forceHTTP11ALPN(uConn *utls.UConn) error {
153
  if !ok {
154
  continue
155
  }
156
- alpnExt.AlpnProtocols = []string{"http/1.1"}
157
  return nil
158
  }
159
  return nil
160
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  dialContext = (&net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}).DialContext
40
  }
41
  tlsDialer := TLSDialerForPlatform(platform, dialContext)
42
+ // chat.deepseek.com 现已要求 HTTP/2(HTTP/1.1 会被服务端返回的 h2 SETTINGS 帧
43
+ // 击穿为 "malformed HTTP response"),因此所有平台都强制尝试 h2。
44
  base := &http.Transport{
45
+ ForceAttemptHTTP2: true,
46
  MaxIdleConns: 200,
47
  MaxIdleConnsPerHost: 100,
48
  IdleConnTimeout: 90 * time.Second,
 
69
  if dialContext == nil {
70
  dialContext = (&net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}).DialContext
71
  }
72
+ // 同主 transport:所有平台强制尝试 h2,保证 utls 指纹请求失败回退到 std
73
+ // transport 时仍能与要求 HTTP/2 的 chat.deepseek.com 通信。
74
  base := &http.Transport{
75
+ ForceAttemptHTTP2: true,
76
  MaxIdleConns: 200,
77
  MaxIdleConnsPerHost: 100,
78
  IdleConnTimeout: 90 * time.Second,
 
86
  }
87
 
88
  // TLSDialerForPlatform returns the appropriate TLS dialer based on the platform.
89
+ // "web" uses Chrome TLS fingerprinting; all other values (including "android")
90
+ // use Safari TLS fingerprinting. Both negotiate HTTP/2 via ALPN, since
91
+ // chat.deepseek.com now requires HTTP/2.
92
  func TLSDialerForPlatform(platform string, dialContext DialContextFunc) func(ctx context.Context, network, addr string) (net.Conn, error) {
93
  if platform == "web" {
94
  return chromeTLSDialer(dialContext)
 
130
  host, _, _ := net.SplitHostPort(addr)
131
  uCfg := &utls.Config{ServerName: host}
132
  uConn := utls.UClient(plainConn, uCfg, utls.HelloSafari_Auto)
133
+ // 真实 Safari 协商 HTTP/2;显式声明 h2+http/1.1,避免被 HelloSafari_Auto
134
+ // 的默认 ALPN 漏掉 h2,导致服务端按 HTTP/1.1 应答而被 h2-only 端点拒绝。
135
+ if err := setSafariALPN(uConn); err != nil {
136
  _ = plainConn.Close()
137
  return nil, err
138
  }
 
141
  _ = plainConn.Close()
142
  return nil, err
143
  }
 
 
 
 
144
  return uConn, nil
145
  }
146
  }
147
 
148
+ // setSafariALPN 显式把 ALPN 设为 h2 + http/1.1,与真实 Safari 行为一致。
149
+ func setSafariALPN(uConn *utls.UConn) error {
150
  if err := uConn.BuildHandshakeState(); err != nil {
151
  return err
152
  }
 
155
  if !ok {
156
  continue
157
  }
158
+ alpnExt.AlpnProtocols = []string{"h2", "http/1.1"}
159
  return nil
160
  }
161
  return nil
162
  }
163
+
164
+ // IPv4DialContext 仅拨号解析到的 IPv4 地址。用于 HuggingFace Spaces 等
165
+ // 没有 IPv6 出口的部署:标准 Happy Eyeballs 在某些纯 IPv6 解析结果下会反复
166
+ // 拨 IPv6 拿到 "network is unreachable",这里显式跳过 IPv6。
167
+ func IPv4DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
168
+ host, port, err := net.SplitHostPort(addr)
169
+ if err != nil {
170
+ return nil, err
171
+ }
172
+ // 已经是 IP 直连时,IPv4 直接拨,IPv6 直接放弃。
173
+ if ip := net.ParseIP(host); ip != nil {
174
+ if ip.To4() == nil {
175
+ return nil, fmt.Errorf("ipv4-only dialer: %s is not IPv4", host)
176
+ }
177
+ d := &net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}
178
+ return d.DialContext(ctx, "tcp4", net.JoinHostPort(host, port))
179
+ }
180
+ ips, err := net.DefaultResolver.LookupIPAddr(ctx, host)
181
+ if err != nil {
182
+ return nil, err
183
+ }
184
+ var lastErr error
185
+ d := &net.Dialer{Timeout: 15 * time.Second, KeepAlive: 30 * time.Second}
186
+ for _, ip := range ips {
187
+ if ip.IP.To4() == nil {
188
+ continue
189
+ }
190
+ conn, derr := d.DialContext(ctx, "tcp4", net.JoinHostPort(ip.IP.String(), port))
191
+ if derr == nil {
192
+ return conn, nil
193
+ }
194
+ lastErr = derr
195
+ }
196
+ if lastErr != nil {
197
+ return nil, lastErr
198
+ }
199
+ return nil, fmt.Errorf("ipv4-only dialer: no IPv4 address resolved for %s", host)
200
+ }