magpie-gemini-web 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +29 -5
- package/README.md +33 -5
- package/index.mjs +23 -6
- package/lib/backend.mjs +9 -5
- package/lib/task.mjs +87 -0
- package/lib/usage.mjs +118 -16
- package/native-task/LICENSE +21 -0
- package/native-task/README.md +87 -0
- package/native-task/bridge/gemini_native_bridge/__init__.py +3 -0
- package/native-task/bridge/gemini_native_bridge/__main__.py +235 -0
- package/native-task/bridge/gemini_native_bridge/auth.py +143 -0
- package/native-task/bridge/gemini_native_bridge/config.py +222 -0
- package/native-task/bridge/gemini_native_bridge/contract.py +283 -0
- package/native-task/bridge/gemini_native_bridge/native_auth_keychain.swift +111 -0
- package/native-task/bridge/gemini_native_bridge/oauth.py +154 -0
- package/native-task/bridge/gemini_native_bridge/protocol.py +363 -0
- package/native-task/bridge/gemini_native_bridge/server.py +441 -0
- package/native-task/bridge/gemini_native_bridge/state.py +806 -0
- package/native-task/bridge/gemini_native_bridge/transport.py +531 -0
- package/native-task/index.mjs +76 -0
- package/native-task/lib/runtime.mjs +189 -0
- package/native-task/pyproject.toml +18 -0
- package/native-task/requirements.txt +2 -0
- package/native-task/scripts/bridge.mjs +97 -0
- package/package.json +11 -3
- package/patches/argon.patch +231 -51
- package/scripts/backend-argon.mjs +1 -1
package/patches/argon.patch
CHANGED
|
@@ -699,6 +699,53 @@ index 0000000..e5f1b6f
|
|
|
699
699
|
+ }
|
|
700
700
|
+ t.Fatal("没有记录这次请求")
|
|
701
701
|
+}
|
|
702
|
+
diff --git a/internal/app/client.go b/internal/app/client.go
|
|
703
|
+
index 1750391..e613956 100644
|
|
704
|
+
--- a/internal/app/client.go
|
|
705
|
+
+++ b/internal/app/client.go
|
|
706
|
+
@@ -154,17 +154,32 @@ func makeSAPISIDHash(sapisid string) string {
|
|
707
|
+
return fmt.Sprintf("SAPISIDHASH %d_%s", ts, hex.EncodeToString(h[:]))
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
-// ChromeUA 是给 stdlib 走代理时用的 Chrome 146 真实 UA 模板。
|
|
711
|
+
+// ChromeUA 是 Chrome 146 的真实 UA 模板。
|
|
712
|
+
const ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
713
|
+
|
|
714
|
+
-// applyChromeHeaders 给 stdlib 请求填上模拟 Chrome 146 的应用层 header。
|
|
715
|
+
-// utls 那层做 TLS/HTTP2 指纹我们做不到(走代理),但 header 一定要齐。
|
|
716
|
+
+// chromeHeaders 是模拟 Chrome 146 的应用层 header,两条传输路径都要带齐:
|
|
717
|
+
+// 走代理时 utls 那层的 TLS/HTTP2 指纹我们做不到(stdlib),但 header 一定要齐;
|
|
718
|
+
+// 直连时 tls-client 只管指纹、不填 header,什么都不带的话 UA 是 Go-http-client/2.0,
|
|
719
|
+
+// 跟 Chrome 的握手对不上,同一个账号也会因为走没走代理而呈现两种客户端。
|
|
720
|
+
+var chromeHeaders = [][2]string{
|
|
721
|
+
+ {"User-Agent", ChromeUA},
|
|
722
|
+
+ {"Sec-CH-UA", `"Chromium";v="146", "Google Chrome";v="146", "Not?A_Brand";v="24"`},
|
|
723
|
+
+ {"Sec-CH-UA-Mobile", "?0"},
|
|
724
|
+
+ {"Sec-CH-UA-Platform", `"Windows"`},
|
|
725
|
+
+ {"Sec-Fetch-Dest", "empty"},
|
|
726
|
+
+ {"Sec-Fetch-Mode", "cors"},
|
|
727
|
+
+ {"Sec-Fetch-Site", "same-origin"},
|
|
728
|
+
+}
|
|
729
|
+
+
|
|
730
|
+
+// setChromeHeaders 把 chromeHeaders 填进 net/http 或 fhttp 的 Header(两者都有 Set)。
|
|
731
|
+
+// 调用方之后再写自己的 header,同名的以调用方为准。
|
|
732
|
+
+func setChromeHeaders(h interface{ Set(key, value string) }) {
|
|
733
|
+
+ for _, kv := range chromeHeaders {
|
|
734
|
+
+ h.Set(kv[0], kv[1])
|
|
735
|
+
+ }
|
|
736
|
+
+}
|
|
737
|
+
+
|
|
738
|
+
+// applyChromeHeaders 给 stdlib 请求填上 chromeHeaders。
|
|
739
|
+
func applyChromeHeaders(req *http.Request) {
|
|
740
|
+
- req.Header.Set("User-Agent", ChromeUA)
|
|
741
|
+
- req.Header.Set("Sec-CH-UA", `"Chromium";v="146", "Google Chrome";v="146", "Not?A_Brand";v="24"`)
|
|
742
|
+
- req.Header.Set("Sec-CH-UA-Mobile", "?0")
|
|
743
|
+
- req.Header.Set("Sec-CH-UA-Platform", `"Windows"`)
|
|
744
|
+
- req.Header.Set("Sec-Fetch-Dest", "empty")
|
|
745
|
+
- req.Header.Set("Sec-Fetch-Mode", "cors")
|
|
746
|
+
- req.Header.Set("Sec-Fetch-Site", "same-origin")
|
|
747
|
+
+ setChromeHeaders(req.Header)
|
|
748
|
+
}
|
|
702
749
|
diff --git a/internal/app/context_file.go b/internal/app/context_file.go
|
|
703
750
|
index bfc577b..cb60661 100644
|
|
704
751
|
--- a/internal/app/context_file.go
|
|
@@ -779,7 +826,7 @@ index bfc577b..cb60661 100644
|
|
|
779
826
|
+ return promptParts{body: parts}.join()
|
|
780
827
|
}
|
|
781
828
|
diff --git a/internal/app/conversation.go b/internal/app/conversation.go
|
|
782
|
-
index aef12eb..
|
|
829
|
+
index aef12eb..8ba963f 100644
|
|
783
830
|
--- a/internal/app/conversation.go
|
|
784
831
|
+++ b/internal/app/conversation.go
|
|
785
832
|
@@ -1,6 +1,7 @@
|
|
@@ -790,7 +837,23 @@ index aef12eb..cbf1371 100644
|
|
|
790
837
|
"crypto/sha256"
|
|
791
838
|
"encoding/hex"
|
|
792
839
|
"encoding/json"
|
|
793
|
-
@@ -
|
|
840
|
+
@@ -205,7 +206,7 @@ func getAnonSession(proxyURL string) (string, error) {
|
|
841
|
+
if err != nil {
|
|
842
|
+
return nil, nil, nil, err
|
|
843
|
+
}
|
|
844
|
+
- r.Header.Set("User-Agent", webUA)
|
|
845
|
+
+ setChromeHeaders(r.Header)
|
|
846
|
+
resp, err := getTLSClient().Do(r)
|
|
847
|
+
if err != nil {
|
|
848
|
+
return nil, nil, nil, err
|
|
849
|
+
@@ -237,15 +238,14 @@ func getAnonSession(proxyURL string) (string, error) {
|
|
850
|
+
return strings.Join(pairs, "; "), nil
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
-// webUA 是匿名 GET /app 的 User-Agent(tls-client 路径要手动带)。
|
|
854
|
+
-const webUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
|
855
|
+
- "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
856
|
+
-
|
|
794
857
|
// streamGenerateConv 发一路会话的一轮。turn==0 是首轮(建会话),>0 是续接(只发新消息)。
|
|
795
858
|
// 整条会话绑定 conv.cookie / conv.proxyID,中途不换号不换出口。成功后回填 conv 的
|
|
796
859
|
// cid/rid/rcid/tok26 和 turn+1,供下一轮续接。
|
|
@@ -803,7 +866,7 @@ index aef12eb..cbf1371 100644
|
|
|
803
866
|
p, slotOK, slotErr := acquireSlot(conv.proxyID)
|
|
804
867
|
if !slotOK {
|
|
805
868
|
return &StreamResult{}, slotErr
|
|
806
|
-
@@ -365,13 +
|
|
869
|
+
@@ -365,13 +365,19 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
|
|
807
870
|
t0 := time.Now()
|
|
808
871
|
var lastErr error
|
|
809
872
|
for attempt := 0; attempt < rtCfg().RetryAttempts; attempt++ {
|
|
@@ -824,7 +887,7 @@ index aef12eb..cbf1371 100644
|
|
|
824
887
|
if err == nil && statusCode == 200 {
|
|
825
888
|
lastErr = fmt.Errorf("续接无内容帧(会话可能已失效,raw %d 字节)", len(raw))
|
|
826
889
|
} else if err != nil {
|
|
827
|
-
@@ -386,7 +
|
|
890
|
+
@@ -386,7 +392,9 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
|
|
828
891
|
break
|
|
829
892
|
}
|
|
830
893
|
if attempt < rtCfg().RetryAttempts-1 {
|
|
@@ -835,7 +898,7 @@ index aef12eb..cbf1371 100644
|
|
|
835
898
|
}
|
|
836
899
|
continue
|
|
837
900
|
}
|
|
838
|
-
@@ -427,7 +
|
|
901
|
+
@@ -427,7 +435,7 @@ func streamGenerateConv(prompt string, mc ModelConfig, conv *convState,
|
|
839
902
|
// - 没命中 → 新建会话,首轮发全量拼接的历史(含 tools 指令,把当前上下文交给服务端)。
|
|
840
903
|
//
|
|
841
904
|
// 支持 tools(续接里也解析 ```tool_call``` 围栏);图片走原单轮路径(callGemini)。
|
|
@@ -844,7 +907,7 @@ index aef12eb..cbf1371 100644
|
|
|
844
907
|
tools []map[string]interface{}, toolChoice interface{},
|
|
845
908
|
onDelta, onReasoning func(string)) (string, []ToolCall, *StreamResult, error) {
|
|
846
909
|
parentKey := convParentKey(messages)
|
|
847
|
-
@@ -462,7 +
|
|
910
|
+
@@ -462,7 +470,7 @@ func callGeminiConv(messages []map[string]interface{}, mc ModelConfig,
|
|
848
911
|
logf("[conv] 续接命中 turn=%d,只发 %d 字节(历史留服务端)", conv.turn, len(prompt))
|
|
849
912
|
}
|
|
850
913
|
|
|
@@ -854,7 +917,7 @@ index aef12eb..cbf1371 100644
|
|
|
854
917
|
if !fresh {
|
|
855
918
|
// 续接失败:这路会话作废,客户端重试时会当新会话全量重发。
|
|
856
919
|
diff --git a/internal/app/gemini.go b/internal/app/gemini.go
|
|
857
|
-
index 723725f..
|
|
920
|
+
index 723725f..150b022 100644
|
|
858
921
|
--- a/internal/app/gemini.go
|
|
859
922
|
+++ b/internal/app/gemini.go
|
|
860
923
|
@@ -3,6 +3,7 @@ package app
|
|
@@ -1148,7 +1211,7 @@ index 723725f..a1e5221 100644
|
|
|
1148
1211
|
if err != nil {
|
|
1149
1212
|
return resp.StatusCode, nil, ttfb, resp.Header.Values("Set-Cookie"), err
|
|
1150
1213
|
}
|
|
1151
|
-
@@ -844,
|
|
1214
|
+
@@ -844,10 +944,11 @@ func doGeminiRequest(endpoint, body string, headers map[string]string, proxyURL
|
|
1152
1215
|
}
|
|
1153
1216
|
|
|
1154
1217
|
// 直连 → tls-client,保留 chrome146 TLS/HTTP2 真指纹
|
|
@@ -1157,7 +1220,11 @@ index 723725f..a1e5221 100644
|
|
|
1157
1220
|
if err != nil {
|
|
1158
1221
|
return 0, nil, 0, nil, err
|
|
1159
1222
|
}
|
|
1160
|
-
|
|
1223
|
+
+ setChromeHeaders(req.Header)
|
|
1224
|
+
for k, v := range headers {
|
|
1225
|
+
req.Header.Set(k, v)
|
|
1226
|
+
}
|
|
1227
|
+
@@ -857,7 +958,7 @@ func doGeminiRequest(endpoint, body string, headers map[string]string, proxyURL
|
|
1161
1228
|
return 0, nil, 0, nil, err
|
|
1162
1229
|
}
|
|
1163
1230
|
defer resp.Body.Close()
|
|
@@ -1166,6 +1233,14 @@ index 723725f..a1e5221 100644
|
|
|
1166
1233
|
if err != nil {
|
|
1167
1234
|
return resp.StatusCode, nil, ttfb, resp.Header.Values("Set-Cookie"), err
|
|
1168
1235
|
}
|
|
1236
|
+
@@ -1159,6 +1260,7 @@ func probeGemini(prompt, proxyURL string) ProbeResult {
|
|
1237
|
+
res.Diagnostic = "构建请求失败: " + e.Error()
|
|
1238
|
+
return res
|
|
1239
|
+
}
|
|
1240
|
+
+ setChromeHeaders(req.Header)
|
|
1241
|
+
for k, v := range headers {
|
|
1242
|
+
req.Header.Set(k, v)
|
|
1243
|
+
}
|
|
1169
1244
|
diff --git a/internal/app/gemini_test.go b/internal/app/gemini_test.go
|
|
1170
1245
|
index 00eb305..2b4c6ca 100644
|
|
1171
1246
|
--- a/internal/app/gemini_test.go
|
|
@@ -1178,12 +1253,114 @@ index 00eb305..2b4c6ca 100644
|
|
|
1178
1253
|
}
|
|
1179
1254
|
for _, c := range cases {
|
|
1180
1255
|
name, mc, err := resolveModel(c.name)
|
|
1256
|
+
diff --git a/internal/app/headers_test.go b/internal/app/headers_test.go
|
|
1257
|
+
new file mode 100644
|
|
1258
|
+
index 0000000..b80a9fa
|
|
1259
|
+
--- /dev/null
|
|
1260
|
+
+++ b/internal/app/headers_test.go
|
|
1261
|
+
@@ -0,0 +1,96 @@
|
|
1262
|
+
+package app
|
|
1263
|
+
+
|
|
1264
|
+
+import (
|
|
1265
|
+
+ "context"
|
|
1266
|
+
+ "net/http"
|
|
1267
|
+
+ "net/http/httptest"
|
|
1268
|
+
+ "testing"
|
|
1269
|
+
+
|
|
1270
|
+
+ tls_client "github.com/bogdanfinn/tls-client"
|
|
1271
|
+
+)
|
|
1272
|
+
+
|
|
1273
|
+
+// 同一个账号的请求不能因为走没走代理而呈现两种客户端:两条路径都要带齐 chromeHeaders,
|
|
1274
|
+
+// 调用方自己的 header 照样生效。直连那条以前一个都不带,UA 是 Go-http-client/2.0。
|
|
1275
|
+
+//
|
|
1276
|
+
+// 直连走 tls-client,这里换成跳过证书校验的同款客户端,打本机的 HTTPS 测试服务器;
|
|
1277
|
+
+// 代理那条把 http:// 目标以绝对 URI 发给代理,测试服务器本身就能当代理(同 cancel_test)。
|
|
1278
|
+
+func TestDirectAndProxyPathsSendSameBrowserHeaders(t *testing.T) {
|
|
1279
|
+
+ got := map[string]http.Header{}
|
|
1280
|
+
+ record := func(name string) http.Handler {
|
|
1281
|
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
1282
|
+
+ got[name] = r.Header.Clone()
|
|
1283
|
+
+ w.WriteHeader(200)
|
|
1284
|
+
+ })
|
|
1285
|
+
+ }
|
|
1286
|
+
+ direct := httptest.NewUnstartedServer(record("direct"))
|
|
1287
|
+
+ direct.EnableHTTP2 = true
|
|
1288
|
+
+ direct.StartTLS()
|
|
1289
|
+
+ defer direct.Close()
|
|
1290
|
+
+ proxy := httptest.NewServer(record("proxy"))
|
|
1291
|
+
+ defer proxy.Close()
|
|
1292
|
+
+
|
|
1293
|
+
+ key := rtCfg().Impersonate
|
|
1294
|
+
+ insecure, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(),
|
|
1295
|
+
+ tls_client.WithClientProfile(resolveProfile(key)),
|
|
1296
|
+
+ tls_client.WithInsecureSkipVerify(),
|
|
1297
|
+
+ tls_client.WithNotFollowRedirects())
|
|
1298
|
+
+ if err != nil {
|
|
1299
|
+
+ t.Fatal(err)
|
|
1300
|
+
+ }
|
|
1301
|
+
+ clientCacheMu.Lock()
|
|
1302
|
+
+ prev, had := clientCache[key]
|
|
1303
|
+
+ clientCache[key] = insecure
|
|
1304
|
+
+ clientCacheMu.Unlock()
|
|
1305
|
+
+ t.Cleanup(func() {
|
|
1306
|
+
+ clientCacheMu.Lock()
|
|
1307
|
+
+ defer clientCacheMu.Unlock()
|
|
1308
|
+
+ if had {
|
|
1309
|
+
+ clientCache[key] = prev
|
|
1310
|
+
+ } else {
|
|
1311
|
+
+ delete(clientCache, key)
|
|
1312
|
+
+ }
|
|
1313
|
+
+ })
|
|
1314
|
+
+
|
|
1315
|
+
+ own := map[string]string{"Accept-Language": "en-US,en;q=0.9", "X-Same-Domain": "1"}
|
|
1316
|
+
+ paths := []struct {
|
|
1317
|
+
+ name string
|
|
1318
|
+
+ send func(endpoint, proxyURL string)
|
|
1319
|
+
+ }{
|
|
1320
|
+
+ {"StreamGenerate", func(endpoint, proxyURL string) {
|
|
1321
|
+
+ doGeminiRequestStop(context.Background(), endpoint, "f.req=x", own, proxyURL, nil, nil)
|
|
1322
|
+
+ }},
|
|
1323
|
+
+ {"upload", func(endpoint, proxyURL string) {
|
|
1324
|
+
+ uploadPostCtx(context.Background(), endpoint, own, []byte("x"), proxyURL)
|
|
1325
|
+
+ }},
|
|
1326
|
+
+ {"media", func(endpoint, proxyURL string) {
|
|
1327
|
+
+ mediaGetOnce(endpoint, own, proxyURL)
|
|
1328
|
+
+ }},
|
|
1329
|
+
+ {"image", func(endpoint, proxyURL string) {
|
|
1330
|
+
+ fetchImage(endpoint, proxyURL, 0)
|
|
1331
|
+
+ }},
|
|
1332
|
+
+ }
|
|
1333
|
+
+ for _, p := range paths {
|
|
1334
|
+
+ clear(got)
|
|
1335
|
+
+ p.send(direct.URL+"/x", "")
|
|
1336
|
+
+ p.send("http://upstream.invalid/x", proxy.URL)
|
|
1337
|
+
+ d, pr := got["direct"], got["proxy"]
|
|
1338
|
+
+ if d == nil || pr == nil {
|
|
1339
|
+
+ t.Fatalf("%s: 没收到请求(direct=%v proxy=%v)", p.name, d != nil, pr != nil)
|
|
1340
|
+
+ }
|
|
1341
|
+
+ if ua := d.Get("User-Agent"); ua != ChromeUA {
|
|
1342
|
+
+ t.Errorf("%s: 直连 UA = %q", p.name, ua)
|
|
1343
|
+
+ }
|
|
1344
|
+
+ for _, kv := range chromeHeaders {
|
|
1345
|
+
+ if d.Get(kv[0]) != pr.Get(kv[0]) {
|
|
1346
|
+
+ t.Errorf("%s: %s 直连 %q,代理 %q", p.name, kv[0], d.Get(kv[0]), pr.Get(kv[0]))
|
|
1347
|
+
+ }
|
|
1348
|
+
+ }
|
|
1349
|
+
+ if p.name != "image" {
|
|
1350
|
+
+ for k, v := range own {
|
|
1351
|
+
+ if d.Get(k) != v || pr.Get(k) != v {
|
|
1352
|
+
+ t.Errorf("%s: 调用方的 %s 没生效(直连 %q,代理 %q)", p.name, k, d.Get(k), pr.Get(k))
|
|
1353
|
+
+ }
|
|
1354
|
+
+ }
|
|
1355
|
+
+ }
|
|
1356
|
+
+ }
|
|
1357
|
+
+}
|
|
1181
1358
|
diff --git a/internal/app/latency_test.go b/internal/app/latency_test.go
|
|
1182
1359
|
new file mode 100644
|
|
1183
|
-
index 0000000..
|
|
1360
|
+
index 0000000..b30336f
|
|
1184
1361
|
--- /dev/null
|
|
1185
1362
|
+++ b/internal/app/latency_test.go
|
|
1186
|
-
@@ -0,0 +1,
|
|
1363
|
+
@@ -0,0 +1,297 @@
|
|
1187
1364
|
+package app
|
|
1188
1365
|
+
|
|
1189
1366
|
+import (
|
|
@@ -1406,17 +1583,15 @@ index 0000000..9329309
|
|
|
1406
1583
|
+ }
|
|
1407
1584
|
+}
|
|
1408
1585
|
+
|
|
1409
|
-
+//
|
|
1410
|
-
+func
|
|
1586
|
+
+// 后台重取失败时必须作废缓存,否则失效的号会一直拿着旧 token 被当成可用。
|
|
1587
|
+
+func TestRefreshAppTokensFailureDropsCache(t *testing.T) {
|
|
1411
1588
|
+ cookie := "SAPISID=sap2; __Secure-1PSID=psid2"
|
|
1412
1589
|
+ xsrfMu.Lock()
|
|
1413
1590
|
+ xsrfCache[cookieKey(cookie)] = xsrfEntry{token: "stale", fetched: time.Now()}
|
|
1414
1591
|
+ xsrfMu.Unlock()
|
|
1415
1592
|
+ t.Cleanup(func() { invalidateXSRF(cookie) })
|
|
1416
1593
|
+
|
|
1417
|
-
+
|
|
1418
|
-
+ t.Fatal("不可达出口应报错")
|
|
1419
|
-
+ }
|
|
1594
|
+
+ refreshAppTokens(cookieKey(cookie), cookie, "http://127.0.0.1:1")
|
|
1420
1595
|
+ xsrfMu.Lock()
|
|
1421
1596
|
+ _, ok := xsrfCache[cookieKey(cookie)]
|
|
1422
1597
|
+ xsrfMu.Unlock()
|
|
@@ -1483,6 +1658,18 @@ index 0000000..9329309
|
|
|
1483
1658
|
+ t.Fatalf("非流式: stop=%v reads=%d tracker=%q", lh.stop.Load(), src.reads, lh.tracker.emitted)
|
|
1484
1659
|
+ }
|
|
1485
1660
|
+}
|
|
1661
|
+
diff --git a/internal/app/media.go b/internal/app/media.go
|
|
1662
|
+
index 9844778..03610f9 100644
|
|
1663
|
+
--- a/internal/app/media.go
|
|
1664
|
+
+++ b/internal/app/media.go
|
|
1665
|
+
@@ -505,6 +505,7 @@ func mediaGetOnce(rawURL string, headers map[string]string, proxyURL string) (in
|
|
1666
|
+
if err != nil {
|
|
1667
|
+
return 0, nil, nil, err
|
|
1668
|
+
}
|
|
1669
|
+
+ setChromeHeaders(req.Header)
|
|
1670
|
+
for k, v := range headers {
|
|
1671
|
+
req.Header.Set(k, v)
|
|
1672
|
+
}
|
|
1486
1673
|
diff --git a/internal/app/messages.go b/internal/app/messages.go
|
|
1487
1674
|
index 376171f..87b91fe 100644
|
|
1488
1675
|
--- a/internal/app/messages.go
|
|
@@ -1680,26 +1867,6 @@ index 376171f..87b91fe 100644
|
|
|
1680
1867
|
// parseToolChoice 解析 OpenAI 的 tool_choice。
|
|
1681
1868
|
// 返回 (mode, forcedName):mode ∈ {"auto","none","required"};
|
|
1682
1869
|
// forcedName 非空表示客户端点名了某个函数。
|
|
1683
|
-
diff --git a/internal/app/rotate.go b/internal/app/rotate.go
|
|
1684
|
-
index e082b15..908a052 100644
|
|
1685
|
-
--- a/internal/app/rotate.go
|
|
1686
|
-
+++ b/internal/app/rotate.go
|
|
1687
|
-
@@ -95,7 +95,14 @@ func rotateAccount(a CookieAccount) (time.Duration, []string, error) {
|
|
1688
|
-
if cookie != a.Cookie {
|
|
1689
|
-
old := a.Cookie
|
|
1690
|
-
updateAccountCookie(a.ID, cookie)
|
|
1691
|
-
- invalidateXSRF(old)
|
|
1692
|
-
+ // 轮转本身不在请求路径上,顺手用新 cookie 重取 token:既验证轮转结果,
|
|
1693
|
-
+ // 又让下一个请求直接命中缓存。失败时缓存已作废,下个请求同步重取。
|
|
1694
|
-
+ if err := refreshXSRF(cookie, proxyURL); err != nil {
|
|
1695
|
-
+ logf("[rotate] 账号 #%d 轮转后取 XSRF 失败: %v", a.ID, err)
|
|
1696
|
-
+ }
|
|
1697
|
-
+ if cookieKey(old) != cookieKey(cookie) {
|
|
1698
|
-
+ invalidateXSRF(old) // 身份项都变了(理论上不会):旧键别留着
|
|
1699
|
-
+ }
|
|
1700
|
-
}
|
|
1701
|
-
if len(names) > 0 {
|
|
1702
|
-
logf("[rotate] 账号 #%d 刷新了 %s", a.ID, strings.Join(names, ", "))
|
|
1703
1870
|
diff --git a/internal/app/server.go b/internal/app/server.go
|
|
1704
1871
|
index c3a7df1..eb02909 100644
|
|
1705
1872
|
--- a/internal/app/server.go
|
|
@@ -1808,7 +1975,7 @@ index c3a7df1..eb02909 100644
|
|
|
1808
1975
|
if stream {
|
|
1809
1976
|
writeEvent("response.failed", map[string]interface{}{
|
|
1810
1977
|
diff --git a/internal/app/upload.go b/internal/app/upload.go
|
|
1811
|
-
index 7747a72..
|
|
1978
|
+
index 7747a72..2753f93 100644
|
|
1812
1979
|
--- a/internal/app/upload.go
|
|
1813
1980
|
+++ b/internal/app/upload.go
|
|
1814
1981
|
@@ -2,6 +2,7 @@ package app
|
|
@@ -1865,7 +2032,7 @@ index 7747a72..09e8a04 100644
|
|
|
1865
2032
|
if err != nil {
|
|
1866
2033
|
return 0, nil, nil, err
|
|
1867
2034
|
}
|
|
1868
|
-
@@ -131,
|
|
2035
|
+
@@ -131,10 +142,11 @@ func uploadPost(url string, headers map[string]string, body []byte, proxyURL str
|
|
1869
2036
|
}
|
|
1870
2037
|
return resp.StatusCode, h, b, err
|
|
1871
2038
|
}
|
|
@@ -1874,6 +2041,10 @@ index 7747a72..09e8a04 100644
|
|
|
1874
2041
|
if err != nil {
|
|
1875
2042
|
return 0, nil, nil, err
|
|
1876
2043
|
}
|
|
2044
|
+
+ setChromeHeaders(req.Header)
|
|
2045
|
+
for k, v := range headers {
|
|
2046
|
+
req.Header.Set(k, v)
|
|
2047
|
+
}
|
|
1877
2048
|
diff --git a/internal/app/videos.go b/internal/app/videos.go
|
|
1878
2049
|
index 390b580..0365a78 100644
|
|
1879
2050
|
--- a/internal/app/videos.go
|
|
@@ -1895,8 +2066,20 @@ index 390b580..0365a78 100644
|
|
|
1895
2066
|
if err != nil {
|
|
1896
2067
|
recordRequest("videos", j.Model, j.Prompt, "", res, 502, err.Error(), false)
|
|
1897
2068
|
finishVideoJob(j, nil, "", err.Error())
|
|
2069
|
+
diff --git a/internal/app/vision.go b/internal/app/vision.go
|
|
2070
|
+
index b0c9724..b1ca7cb 100644
|
|
2071
|
+
--- a/internal/app/vision.go
|
|
2072
|
+
+++ b/internal/app/vision.go
|
|
2073
|
+
@@ -160,6 +160,7 @@ func fetchImage(src, proxyURL string, idx int) (pendingUpload, error) {
|
|
2074
|
+
if err != nil {
|
|
2075
|
+
return pendingUpload{}, err
|
|
2076
|
+
}
|
|
2077
|
+
+ setChromeHeaders(req.Header)
|
|
2078
|
+
resp, err := getTLSClient().Do(req)
|
|
2079
|
+
if err != nil {
|
|
2080
|
+
return pendingUpload{}, fmt.Errorf("image %d: fetch failed: %w", idx, err)
|
|
1898
2081
|
diff --git a/internal/app/xsrf.go b/internal/app/xsrf.go
|
|
1899
|
-
index c43b104..
|
|
2082
|
+
index c43b104..d3d7145 100644
|
|
1900
2083
|
--- a/internal/app/xsrf.go
|
|
1901
2084
|
+++ b/internal/app/xsrf.go
|
|
1902
2085
|
@@ -27,15 +27,23 @@ var (
|
|
@@ -1961,7 +2144,7 @@ index c43b104..55634b6 100644
|
|
|
1961
2144
|
|
|
1962
2145
|
e, err := fetchAppTokens(cookie, proxyURL)
|
|
1963
2146
|
if err != nil {
|
|
1964
|
-
@@ -84,16 +99,
|
|
2147
|
+
@@ -84,16 +99,48 @@ func getXSRF(cookie, proxyURL string) (string, error) {
|
|
1965
2148
|
return e.token, nil
|
|
1966
2149
|
}
|
|
1967
2150
|
|
|
@@ -1988,17 +2171,6 @@ index c43b104..55634b6 100644
|
|
|
1988
2171
|
+ }
|
|
1989
2172
|
+}
|
|
1990
2173
|
+
|
|
1991
|
-
+// refreshXSRF 立即重抓页面并写回缓存,失败则作废缓存。
|
|
1992
|
-
+//
|
|
1993
|
-
+// 给本来就不在请求关键路径上的调用方用(cookie 轮转):轮转后直接作废的话,
|
|
1994
|
-
+// 缓存按账号身份取键,下一个请求就得同步抓一次页面(实测约 1.2 秒)。
|
|
1995
|
-
+func refreshXSRF(cookie, proxyURL string) error {
|
|
1996
|
-
+ if cookie == "" {
|
|
1997
|
-
+ return nil
|
|
1998
|
-
+ }
|
|
1999
|
-
+ return storeAppTokens(cookieKey(cookie), cookie, proxyURL)
|
|
2000
|
-
+}
|
|
2001
|
-
+
|
|
2002
2174
|
+func storeAppTokens(key, cookie, proxyURL string) error {
|
|
2003
2175
|
+ e, err := fetchAppTokens(cookie, proxyURL)
|
|
2004
2176
|
+ xsrfMu.Lock()
|
|
@@ -2025,3 +2197,11 @@ index c43b104..55634b6 100644
|
|
|
2025
2197
|
|
|
2026
2198
|
e, err := fetchAppTokens(cookie, proxyURL)
|
|
2027
2199
|
if err != nil {
|
|
2200
|
+
@@ -146,6 +193,7 @@ func fetchAppPage(cookie, proxyURL string) ([]byte, error) {
|
|
2201
|
+
if err != nil {
|
|
2202
|
+
return nil, err
|
|
2203
|
+
}
|
|
2204
|
+
+ setChromeHeaders(req.Header)
|
|
2205
|
+
for k, v := range headers {
|
|
2206
|
+
req.Header.Set(k, v)
|
|
2207
|
+
}
|