BOHE commited on
Commit
3e3f9fc
·
1 Parent(s): 8f37986

添加对上下文的改动(测试)

Browse files
Files changed (3) hide show
  1. api/main.go +325 -46
  2. go.mod +1 -0
  3. go.sum +4 -0
api/main.go CHANGED
@@ -2,17 +2,37 @@ package handler
2
 
3
  import (
4
  "bufio"
 
5
  "encoding/json"
6
  "fmt"
 
 
7
  "net/http"
 
 
8
  "strings"
9
  "time"
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  )
11
 
 
12
  type YouChatResponse struct {
13
  YouChatToken string `json:"youChatToken"`
14
  }
15
 
 
16
  type OpenAIStreamResponse struct {
17
  ID string `json:"id"`
18
  Object string `json:"object"`
@@ -21,27 +41,32 @@ type OpenAIStreamResponse struct {
21
  Choices []Choice `json:"choices"`
22
  }
23
 
 
24
  type Choice struct {
25
  Delta Delta `json:"delta"`
26
  Index int `json:"index"`
27
  FinishReason string `json:"finish_reason"`
28
  }
29
 
 
30
  type Delta struct {
31
  Content string `json:"content"`
32
  }
33
 
 
34
  type OpenAIRequest struct {
35
  Messages []Message `json:"messages"`
36
  Stream bool `json:"stream"`
37
  Model string `json:"model"`
38
  }
39
 
 
40
  type Message struct {
41
  Role string `json:"role"`
42
  Content string `json:"content"`
43
  }
44
 
 
45
  type OpenAIResponse struct {
46
  ID string `json:"id"`
47
  Object string `json:"object"`
@@ -50,17 +75,20 @@ type OpenAIResponse struct {
50
  Choices []OpenAIChoice `json:"choices"`
51
  }
52
 
 
53
  type OpenAIChoice struct {
54
  Message Message `json:"message"`
55
  Index int `json:"index"`
56
  FinishReason string `json:"finish_reason"`
57
  }
58
 
 
59
  type ModelResponse struct {
60
  Object string `json:"object"`
61
  Data []ModelDetail `json:"data"`
62
  }
63
 
 
64
  type ModelDetail struct {
65
  ID string `json:"id"`
66
  Object string `json:"object"`
@@ -68,6 +96,7 @@ type ModelDetail struct {
68
  OwnedBy string `json:"owned_by"`
69
  }
70
 
 
71
  var modelMap = map[string]string{
72
  "deepseek-reasoner": "deepseek_r1",
73
  "deepseek-chat": "deepseek_v3",
@@ -94,6 +123,7 @@ var modelMap = map[string]string{
94
  "command-r-plus": "command_r_plus",
95
  }
96
 
 
97
  func getReverseModelMap() map[string]string {
98
  reverse := make(map[string]string, len(modelMap))
99
  for k, v := range modelMap {
@@ -102,24 +132,43 @@ func getReverseModelMap() map[string]string {
102
  return reverse
103
  }
104
 
 
105
  func mapModelName(openAIModel string) string {
106
  if mappedModel, exists := modelMap[openAIModel]; exists {
107
  return mappedModel
108
  }
109
- return "deepseek_v3"
110
  }
111
 
 
112
  func reverseMapModelName(youModel string) string {
113
  reverseMap := getReverseModelMap()
114
  if mappedModel, exists := reverseMap[youModel]; exists {
115
  return mappedModel
116
  }
117
- return "deepseek-chat"
118
  }
119
 
 
120
  var originalModel string
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  func Handler(w http.ResponseWriter, r *http.Request) {
 
123
  if r.URL.Path == "/v1/models" || r.URL.Path == "/api/v1/models" {
124
  w.Header().Set("Content-Type", "application/json")
125
  w.Header().Set("Access-Control-Allow-Origin", "*")
@@ -151,7 +200,8 @@ func Handler(w http.ResponseWriter, r *http.Request) {
151
  return
152
  }
153
 
154
- if r.URL.Path != "/v1/chat/completions" && r.URL.Path != "/api/v1/chat/completions" {
 
155
  w.Header().Set("Content-Type", "application/json")
156
  json.NewEncoder(w).Encode(map[string]string{
157
  "status": "You2Api Service Running...",
@@ -160,6 +210,7 @@ func Handler(w http.ResponseWriter, r *http.Request) {
160
  return
161
  }
162
 
 
163
  w.Header().Set("Access-Control-Allow-Origin", "*")
164
  w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
165
  w.Header().Set("Access-Control-Allow-Headers", "*")
@@ -169,13 +220,15 @@ func Handler(w http.ResponseWriter, r *http.Request) {
169
  return
170
  }
171
 
 
172
  authHeader := r.Header.Get("Authorization")
173
  if !strings.HasPrefix(authHeader, "Bearer ") {
174
  http.Error(w, "Missing or invalid authorization header", http.StatusUnauthorized)
175
  return
176
  }
177
- dsToken := strings.TrimPrefix(authHeader, "Bearer ")
178
 
 
179
  var openAIReq OpenAIRequest
180
  if err := json.NewDecoder(r.Body).Decode(&openAIReq); err != nil {
181
  http.Error(w, "Invalid request body", http.StatusBadRequest)
@@ -183,13 +236,25 @@ func Handler(w http.ResponseWriter, r *http.Request) {
183
  }
184
 
185
  originalModel = openAIReq.Model
186
- lastMessage := openAIReq.Messages[len(openAIReq.Messages)-1].Content
 
 
 
 
 
 
 
 
 
 
 
187
  var chatHistory []map[string]interface{}
188
  for _, msg := range openAIReq.Messages {
189
  chatMsg := map[string]interface{}{
190
  "question": msg.Content,
191
  "answer": "",
192
  }
 
193
  if msg.Role == "assistant" {
194
  chatMsg["question"] = ""
195
  chatMsg["answer"] = msg.Content
@@ -197,27 +262,114 @@ func Handler(w http.ResponseWriter, r *http.Request) {
197
  chatHistory = append(chatHistory, chatMsg)
198
  }
199
 
200
- chatHistoryJSON, _ := json.Marshal(chatHistory)
201
 
 
202
  youReq, _ := http.NewRequest("GET", "https://you.com/api/streamingSearch", nil)
203
 
204
- q := youReq.URL.Query()
205
- q.Add("q", lastMessage)
206
- q.Add("page", "1")
207
- q.Add("count", "10")
208
- q.Add("safeSearch", "Moderate")
209
- q.Add("mkt", "zh-HK")
210
- q.Add("enable_worklow_generation_ux", "true")
211
- q.Add("domain", "youchat")
212
- q.Add("use_personalization_extraction", "true")
213
- q.Add("pastChatLength", fmt.Sprintf("%d", len(chatHistory)-1))
214
- q.Add("selectedChatMode", "custom")
215
- q.Add("selectedAiModel", mapModelName(openAIReq.Model))
216
- q.Add("enable_agent_clarification_questions", "true")
217
- q.Add("use_nested_youchat_updates", "true")
218
- q.Add("chat", string(chatHistoryJSON))
219
- youReq.URL.RawQuery = q.Encode()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
 
221
  youReq.Header = http.Header{
222
  "sec-ch-ua-platform": {"Windows"},
223
  "Cache-Control": {"no-cache"},
@@ -227,7 +379,7 @@ func Handler(w http.ResponseWriter, r *http.Request) {
227
  "sec-ch-ua-mobile": {"?0"},
228
  "sec-ch-ua-arch": {"x86"},
229
  "sec-ch-ua-full-version": {"133.0.3065.39"},
230
- "Accept": {"text/event-stream"},
231
  "User-Agent": {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
232
  "sec-ch-ua-platform-version": {"19.0.0"},
233
  "Sec-Fetch-Site": {"same-origin"},
@@ -236,6 +388,7 @@ func Handler(w http.ResponseWriter, r *http.Request) {
236
  "Host": {"you.com"},
237
  }
238
 
 
239
  cookies := getCookies(dsToken)
240
  var cookieStrings []string
241
  for name, value := range cookies {
@@ -243,29 +396,32 @@ func Handler(w http.ResponseWriter, r *http.Request) {
243
  }
244
  youReq.Header.Add("Cookie", strings.Join(cookieStrings, ";"))
245
 
 
246
  if !openAIReq.Stream {
247
- handleNonStreamingResponse(w, youReq)
248
  return
249
  }
250
 
251
- handleStreamingResponse(w, youReq)
252
  }
253
 
 
254
  func getCookies(dsToken string) map[string]string {
255
  return map[string]string{
256
  "guest_has_seen_legal_disclaimer": "true",
257
  "youchat_personalization": "true",
258
- "DS": dsToken,
259
- "you_subscription": "youpro_standard_year",
260
  "youpro_subscription": "true",
261
- "ai_model": "deepseek_r1",
262
  "youchat_smart_learn": "true",
263
  }
264
  }
265
 
 
266
  func handleNonStreamingResponse(w http.ResponseWriter, youReq *http.Request) {
267
  client := &http.Client{
268
- Timeout: 60 * time.Second,
269
  }
270
  resp, err := client.Do(youReq)
271
  if err != nil {
@@ -277,22 +433,24 @@ func handleNonStreamingResponse(w http.ResponseWriter, youReq *http.Request) {
277
  var fullResponse strings.Builder
278
  scanner := bufio.NewScanner(resp.Body)
279
 
 
280
  buf := make([]byte, 0, 64*1024)
281
  scanner.Buffer(buf, 1024*1024)
282
 
 
283
  for scanner.Scan() {
284
  line := scanner.Text()
285
  if strings.HasPrefix(line, "event: youChatToken") {
286
- scanner.Scan()
287
  data := scanner.Text()
288
  if !strings.HasPrefix(data, "data: ") {
289
- continue
290
  }
291
  var token YouChatResponse
292
  if err := json.Unmarshal([]byte(strings.TrimPrefix(data, "data: ")), &token); err != nil {
293
- continue
294
  }
295
- fullResponse.WriteString(token.YouChatToken)
296
  }
297
  }
298
 
@@ -301,19 +459,20 @@ func handleNonStreamingResponse(w http.ResponseWriter, youReq *http.Request) {
301
  return
302
  }
303
 
 
304
  openAIResp := OpenAIResponse{
305
  ID: "chatcmpl-" + fmt.Sprintf("%d", time.Now().Unix()),
306
  Object: "chat.completion",
307
  Created: time.Now().Unix(),
308
- Model: reverseMapModelName(mapModelName(originalModel)),
309
  Choices: []OpenAIChoice{
310
  {
311
  Message: Message{
312
  Role: "assistant",
313
- Content: fullResponse.String(),
314
  },
315
  Index: 0,
316
- FinishReason: "stop",
317
  },
318
  },
319
  }
@@ -325,8 +484,9 @@ func handleNonStreamingResponse(w http.ResponseWriter, youReq *http.Request) {
325
  }
326
  }
327
 
 
328
  func handleStreamingResponse(w http.ResponseWriter, youReq *http.Request) {
329
- client := &http.Client{}
330
  resp, err := client.Do(youReq)
331
  if err != nil {
332
  http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -334,40 +494,159 @@ func handleStreamingResponse(w http.ResponseWriter, youReq *http.Request) {
334
  }
335
  defer resp.Body.Close()
336
 
 
337
  w.Header().Set("Content-Type", "text/event-stream")
338
  w.Header().Set("Cache-Control", "no-cache")
339
  w.Header().Set("Connection", "keep-alive")
340
 
341
  scanner := bufio.NewScanner(resp.Body)
 
342
  for scanner.Scan() {
343
  line := scanner.Text()
344
 
345
  if strings.HasPrefix(line, "event: youChatToken") {
346
- scanner.Scan()
347
- data := scanner.Text()
348
 
349
  var token YouChatResponse
350
- json.Unmarshal([]byte(strings.TrimPrefix(data, "data: ")), &token)
351
 
 
352
  openAIResp := OpenAIStreamResponse{
353
  ID: "chatcmpl-" + fmt.Sprintf("%d", time.Now().Unix()),
354
  Object: "chat.completion.chunk",
355
  Created: time.Now().Unix(),
356
- Model: reverseMapModelName(mapModelName(originalModel)),
357
  Choices: []Choice{
358
  {
359
  Delta: Delta{
360
- Content: token.YouChatToken,
361
  },
362
  Index: 0,
363
- FinishReason: "",
364
  },
365
  },
366
  }
367
 
368
- respBytes, _ := json.Marshal(openAIResp)
369
- fmt.Fprintf(w, "data: %s\n\n", string(respBytes))
370
- w.(http.Flusher).Flush()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  }
372
  }
 
 
 
 
 
 
 
 
 
 
373
  }
 
2
 
3
  import (
4
  "bufio"
5
+ "bytes"
6
  "encoding/json"
7
  "fmt"
8
+ "io"
9
+ "mime/multipart"
10
  "net/http"
11
+ "os"
12
+ "path/filepath"
13
  "strings"
14
  "time"
15
+
16
+ "github.com/google/uuid"
17
+ )
18
+
19
+ // TokenCount 定义了 token 计数的结构
20
+ type TokenCount struct {
21
+ PromptTokens int `json:"prompt_tokens"`
22
+ CompletionTokens int `json:"completion_tokens"`
23
+ TotalTokens int `json:"total_tokens"`
24
+ }
25
+
26
+ const (
27
+ MaxContextTokens = 4000 // 最大上下文 token 数
28
  )
29
 
30
+ // YouChatResponse 定义了从 You.com API 接收的单个 token 的结构。
31
  type YouChatResponse struct {
32
  YouChatToken string `json:"youChatToken"`
33
  }
34
 
35
+ // OpenAIStreamResponse 定义了 OpenAI API 流式响应的结构。
36
  type OpenAIStreamResponse struct {
37
  ID string `json:"id"`
38
  Object string `json:"object"`
 
41
  Choices []Choice `json:"choices"`
42
  }
43
 
44
+ // Choice 定义了 OpenAI 流式响应中 choices 数组的单个元素的结构。
45
  type Choice struct {
46
  Delta Delta `json:"delta"`
47
  Index int `json:"index"`
48
  FinishReason string `json:"finish_reason"`
49
  }
50
 
51
+ // Delta 定义了流式响应中表示增量内容的结构。
52
  type Delta struct {
53
  Content string `json:"content"`
54
  }
55
 
56
+ // OpenAIRequest 定义了 OpenAI API 请求体的结构。
57
  type OpenAIRequest struct {
58
  Messages []Message `json:"messages"`
59
  Stream bool `json:"stream"`
60
  Model string `json:"model"`
61
  }
62
 
63
+ // Message 定义了 OpenAI 聊天消息的结构。
64
  type Message struct {
65
  Role string `json:"role"`
66
  Content string `json:"content"`
67
  }
68
 
69
+ // OpenAIResponse 定义了 OpenAI API 非流式响应的结构。
70
  type OpenAIResponse struct {
71
  ID string `json:"id"`
72
  Object string `json:"object"`
 
75
  Choices []OpenAIChoice `json:"choices"`
76
  }
77
 
78
+ // OpenAIChoice 定义了 OpenAI 非流式响应中 choices 数组的单个元素的结构。
79
  type OpenAIChoice struct {
80
  Message Message `json:"message"`
81
  Index int `json:"index"`
82
  FinishReason string `json:"finish_reason"`
83
  }
84
 
85
+ // ModelResponse 定义了 /v1/models 响应的结构。
86
  type ModelResponse struct {
87
  Object string `json:"object"`
88
  Data []ModelDetail `json:"data"`
89
  }
90
 
91
+ // ModelDetail 定义了模型列表中单个模型的详细信息。
92
  type ModelDetail struct {
93
  ID string `json:"id"`
94
  Object string `json:"object"`
 
96
  OwnedBy string `json:"owned_by"`
97
  }
98
 
99
+ // modelMap 存储 OpenAI 模型名称到 You.com 模型名称的映射。
100
  var modelMap = map[string]string{
101
  "deepseek-reasoner": "deepseek_r1",
102
  "deepseek-chat": "deepseek_v3",
 
123
  "command-r-plus": "command_r_plus",
124
  }
125
 
126
+ // getReverseModelMap 创建并返回 modelMap 的反向映射(You.com 模型名称 -> OpenAI 模型名称)。
127
  func getReverseModelMap() map[string]string {
128
  reverse := make(map[string]string, len(modelMap))
129
  for k, v := range modelMap {
 
132
  return reverse
133
  }
134
 
135
+ // mapModelName 将 OpenAI 模型名称映射到 You.com 模型名称。
136
  func mapModelName(openAIModel string) string {
137
  if mappedModel, exists := modelMap[openAIModel]; exists {
138
  return mappedModel
139
  }
140
+ return "deepseek_v3" // 默认模型
141
  }
142
 
143
+ // reverseMapModelName 将 You.com 模型名称映射回 OpenAI 模型名称。
144
  func reverseMapModelName(youModel string) string {
145
  reverseMap := getReverseModelMap()
146
  if mappedModel, exists := reverseMap[youModel]; exists {
147
  return mappedModel
148
  }
149
+ return "deepseek-chat" // 默认模型
150
  }
151
 
152
+ // originalModel 存储原始的 OpenAI 模型名称。
153
  var originalModel string
154
 
155
+ // NonceResponse 定义了获取 nonce 的响应结构
156
+ type NonceResponse struct {
157
+ Uuid string `json:"uuid"`
158
+ }
159
+
160
+ // UploadResponse 定义了文件上传的响应结构
161
+ type UploadResponse struct {
162
+ Filename string `json:"filename"`
163
+ UserFilename string `json:"user_filename"`
164
+ }
165
+
166
+ // 定义最大查询长度
167
+ const MaxQueryLength = 2000
168
+
169
+ // Handler 是处理所有传入 HTTP 请求的主处理函数。
170
  func Handler(w http.ResponseWriter, r *http.Request) {
171
+ // 处理 /v1/models 请求(列出可用模型)
172
  if r.URL.Path == "/v1/models" || r.URL.Path == "/api/v1/models" {
173
  w.Header().Set("Content-Type", "application/json")
174
  w.Header().Set("Access-Control-Allow-Origin", "*")
 
200
  return
201
  }
202
 
203
+ // 处理非 /v1/chat/completions 请求(服务状态检查)
204
+ if r.URL.Path != "/v1/chat/completions" && r.URL.Path != "/none/v1/chat/completions" && r.URL.Path != "/such/chat/completions" {
205
  w.Header().Set("Content-Type", "application/json")
206
  json.NewEncoder(w).Encode(map[string]string{
207
  "status": "You2Api Service Running...",
 
210
  return
211
  }
212
 
213
+ // 设置 CORS 头部
214
  w.Header().Set("Access-Control-Allow-Origin", "*")
215
  w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
216
  w.Header().Set("Access-Control-Allow-Headers", "*")
 
220
  return
221
  }
222
 
223
+ // 验证 Authorization 头部
224
  authHeader := r.Header.Get("Authorization")
225
  if !strings.HasPrefix(authHeader, "Bearer ") {
226
  http.Error(w, "Missing or invalid authorization header", http.StatusUnauthorized)
227
  return
228
  }
229
+ dsToken := strings.TrimPrefix(authHeader, "Bearer ") // 提取 DS token
230
 
231
+ // 解析 OpenAI 请求体
232
  var openAIReq OpenAIRequest
233
  if err := json.NewDecoder(r.Body).Decode(&openAIReq); err != nil {
234
  http.Error(w, "Invalid request body", http.StatusBadRequest)
 
236
  }
237
 
238
  originalModel = openAIReq.Model
239
+
240
+ // 转换 system 消息为 user 消息
241
+ openAIReq.Messages = convertSystemToUser(openAIReq.Messages)
242
+
243
+ // 计算 token 数(使用字符估算方法)
244
+ totalTokens, err := countTokens(openAIReq.Messages)
245
+ if err != nil {
246
+ http.Error(w, "Failed to count tokens", http.StatusInternalServerError)
247
+ return
248
+ }
249
+
250
+ // 构建 You.com 聊天历史
251
  var chatHistory []map[string]interface{}
252
  for _, msg := range openAIReq.Messages {
253
  chatMsg := map[string]interface{}{
254
  "question": msg.Content,
255
  "answer": "",
256
  }
257
+ // 如果是 assistant 的消息, 则交换 question 和 answer
258
  if msg.Role == "assistant" {
259
  chatMsg["question"] = ""
260
  chatMsg["answer"] = msg.Content
 
262
  chatHistory = append(chatHistory, chatMsg)
263
  }
264
 
265
+ chatHistoryJSON, _ := json.Marshal(chatHistory) // 将聊天历史序列化为 JSON
266
 
267
+ // 创建 You.com API 请求
268
  youReq, _ := http.NewRequest("GET", "https://you.com/api/streamingSearch", nil)
269
 
270
+ // 生成必要的 ID
271
+ chatId := uuid.New().String()
272
+ conversationTurnId := uuid.New().String()
273
+ traceId := fmt.Sprintf("%s|%s|%s", chatId, conversationTurnId, time.Now().Format(time.RFC3339))
274
+
275
+ // 如果超过限制,使用文件上传
276
+ if totalTokens > MaxContextTokens {
277
+ // 1. 获取 nonce
278
+ nonceResp, err := getNonce(dsToken)
279
+ if err != nil {
280
+ http.Error(w, "Failed to get nonce", http.StatusInternalServerError)
281
+ return
282
+ }
283
+
284
+ // 2. 创建临时文件,包含所有消息内容
285
+ var fileContent strings.Builder
286
+ for i, msg := range openAIReq.Messages {
287
+ if i > 0 {
288
+ fileContent.WriteString("\n\n")
289
+ }
290
+ fileContent.WriteString(fmt.Sprintf("%s: %s", msg.Role, msg.Content))
291
+ }
292
+
293
+ tempFile := fmt.Sprintf("temp_%s.txt", nonceResp.Uuid)
294
+ if err := os.WriteFile(tempFile, []byte(fileContent.String()), 0644); err != nil {
295
+ http.Error(w, "Failed to create temp file", http.StatusInternalServerError)
296
+ return
297
+ }
298
+ defer os.Remove(tempFile)
299
+
300
+ // 3. 上传文件
301
+ uploadResp, err := uploadFile(dsToken, tempFile)
302
+ if err != nil {
303
+ http.Error(w, "Failed to upload file", http.StatusInternalServerError)
304
+ return
305
+ }
306
+
307
+ // 4. 修改消息列表,只保留文件引用
308
+ openAIReq.Messages = []Message{
309
+ {
310
+ Role: "user",
311
+ Content: fmt.Sprintf("Please review the attached file: %s", uploadResp.UserFilename),
312
+ },
313
+ }
314
+
315
+ // 5. 添加文件源信息
316
+ sources := []map[string]interface{}{
317
+ {
318
+ "source_type": "user_file",
319
+ "filename": uploadResp.Filename,
320
+ "user_filename": uploadResp.UserFilename,
321
+ "size_bytes": len(fileContent.String()),
322
+ },
323
+ }
324
+ sourcesJSON, _ := json.Marshal(sources)
325
+
326
+ // 更新查询参数
327
+ q := youReq.URL.Query()
328
+ q.Add("sources", string(sourcesJSON))
329
+ q.Add("chatId", chatId)
330
+ q.Add("queryTraceId", chatId)
331
+ q.Add("conversationTurnId", conversationTurnId)
332
+ q.Add("traceId", traceId)
333
+ q.Add("q", openAIReq.Messages[0].Content)
334
+ q.Add("page", "1")
335
+ q.Add("count", "10")
336
+ q.Add("safeSearch", "Moderate")
337
+ q.Add("mkt", "zh-HK")
338
+ q.Add("enable_worklow_generation_ux", "true")
339
+ q.Add("domain", "youchat")
340
+ q.Add("use_personalization_extraction", "true")
341
+ q.Add("pastChatLength", fmt.Sprintf("%d", len(chatHistory)-1))
342
+ q.Add("selectedChatMode", "custom")
343
+ q.Add("selectedAiModel", mapModelName(openAIReq.Model))
344
+ q.Add("enable_agent_clarification_questions", "true")
345
+ q.Add("use_nested_youchat_updates", "true")
346
+ q.Add("chat", string(chatHistoryJSON))
347
+ youReq.URL.RawQuery = q.Encode()
348
+ } else {
349
+ // 构建常规查询参数
350
+ q := youReq.URL.Query()
351
+ q.Add("q", openAIReq.Messages[len(openAIReq.Messages)-1].Content)
352
+ q.Add("chatId", chatId)
353
+ q.Add("queryTraceId", chatId)
354
+ q.Add("conversationTurnId", conversationTurnId)
355
+ q.Add("traceId", traceId)
356
+ q.Add("page", "1")
357
+ q.Add("count", "10")
358
+ q.Add("safeSearch", "Moderate")
359
+ q.Add("mkt", "zh-HK")
360
+ q.Add("enable_worklow_generation_ux", "true")
361
+ q.Add("domain", "youchat")
362
+ q.Add("use_personalization_extraction", "true")
363
+ q.Add("pastChatLength", fmt.Sprintf("%d", len(chatHistory)-1))
364
+ q.Add("selectedChatMode", "custom")
365
+ q.Add("selectedAiModel", mapModelName(openAIReq.Model))
366
+ q.Add("enable_agent_clarification_questions", "true")
367
+ q.Add("use_nested_youchat_updates", "true")
368
+ q.Add("chat", string(chatHistoryJSON))
369
+ youReq.URL.RawQuery = q.Encode()
370
+ }
371
 
372
+ // 设置 You.com API 请求头
373
  youReq.Header = http.Header{
374
  "sec-ch-ua-platform": {"Windows"},
375
  "Cache-Control": {"no-cache"},
 
379
  "sec-ch-ua-mobile": {"?0"},
380
  "sec-ch-ua-arch": {"x86"},
381
  "sec-ch-ua-full-version": {"133.0.3065.39"},
382
+ "Accept": {"text/event-stream"}, // 重要:接受 SSE 流
383
  "User-Agent": {"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0"},
384
  "sec-ch-ua-platform-version": {"19.0.0"},
385
  "Sec-Fetch-Site": {"same-origin"},
 
388
  "Host": {"you.com"},
389
  }
390
 
391
+ // 设置 You.com API 请求的 Cookie
392
  cookies := getCookies(dsToken)
393
  var cookieStrings []string
394
  for name, value := range cookies {
 
396
  }
397
  youReq.Header.Add("Cookie", strings.Join(cookieStrings, ";"))
398
 
399
+ // 根据 OpenAI 请求的 stream 参数选择处理函数
400
  if !openAIReq.Stream {
401
+ handleNonStreamingResponse(w, youReq) // 处理非流式响应
402
  return
403
  }
404
 
405
+ handleStreamingResponse(w, youReq) // 处理流式响应
406
  }
407
 
408
+ // getCookies 根据提供的 DS token 生成所需的 Cookie。
409
  func getCookies(dsToken string) map[string]string {
410
  return map[string]string{
411
  "guest_has_seen_legal_disclaimer": "true",
412
  "youchat_personalization": "true",
413
+ "DS": dsToken, // 关键的 DS token
414
+ "you_subscription": "youpro_standard_year", // 示例订阅信息
415
  "youpro_subscription": "true",
416
+ "ai_model": "deepseek_r1", // 示例 AI 模型
417
  "youchat_smart_learn": "true",
418
  }
419
  }
420
 
421
+ // handleNonStreamingResponse 处理非流式请求。
422
  func handleNonStreamingResponse(w http.ResponseWriter, youReq *http.Request) {
423
  client := &http.Client{
424
+ Timeout: 60 * time.Second, // 设置超时时间
425
  }
426
  resp, err := client.Do(youReq)
427
  if err != nil {
 
433
  var fullResponse strings.Builder
434
  scanner := bufio.NewScanner(resp.Body)
435
 
436
+ // 设置 scanner 的缓冲区大小(可选,但对于大型响应很重要)
437
  buf := make([]byte, 0, 64*1024)
438
  scanner.Buffer(buf, 1024*1024)
439
 
440
+ // 逐行扫描响应,寻找 youChatToken 事件
441
  for scanner.Scan() {
442
  line := scanner.Text()
443
  if strings.HasPrefix(line, "event: youChatToken") {
444
+ scanner.Scan() // 读取下一行 (data 行)
445
  data := scanner.Text()
446
  if !strings.HasPrefix(data, "data: ") {
447
+ continue // 如果不是 data 行,则跳过
448
  }
449
  var token YouChatResponse
450
  if err := json.Unmarshal([]byte(strings.TrimPrefix(data, "data: ")), &token); err != nil {
451
+ continue // 如果解析失败,则跳过
452
  }
453
+ fullResponse.WriteString(token.YouChatToken) // 将 token 添加到完整响应中
454
  }
455
  }
456
 
 
459
  return
460
  }
461
 
462
+ // 构建 OpenAI 格式的非流式响应
463
  openAIResp := OpenAIResponse{
464
  ID: "chatcmpl-" + fmt.Sprintf("%d", time.Now().Unix()),
465
  Object: "chat.completion",
466
  Created: time.Now().Unix(),
467
+ Model: reverseMapModelName(mapModelName(originalModel)), // 映射回 OpenAI 模型名称
468
  Choices: []OpenAIChoice{
469
  {
470
  Message: Message{
471
  Role: "assistant",
472
+ Content: fullResponse.String(), // 完整的响应内容
473
  },
474
  Index: 0,
475
+ FinishReason: "stop", // 停止原因
476
  },
477
  },
478
  }
 
484
  }
485
  }
486
 
487
+ // handleStreamingResponse 处理流式请求。
488
  func handleStreamingResponse(w http.ResponseWriter, youReq *http.Request) {
489
+ client := &http.Client{} // 流式请求不需要设置超时,因为它会持续接收数据
490
  resp, err := client.Do(youReq)
491
  if err != nil {
492
  http.Error(w, err.Error(), http.StatusInternalServerError)
 
494
  }
495
  defer resp.Body.Close()
496
 
497
+ // 设置流式响应的头部
498
  w.Header().Set("Content-Type", "text/event-stream")
499
  w.Header().Set("Cache-Control", "no-cache")
500
  w.Header().Set("Connection", "keep-alive")
501
 
502
  scanner := bufio.NewScanner(resp.Body)
503
+ // 逐行扫描响应,寻找 youChatToken 事件
504
  for scanner.Scan() {
505
  line := scanner.Text()
506
 
507
  if strings.HasPrefix(line, "event: youChatToken") {
508
+ scanner.Scan() // 读取下一行 (data 行)
509
+ data := scanner.Text() // 获取数据行
510
 
511
  var token YouChatResponse
512
+ json.Unmarshal([]byte(strings.TrimPrefix(data, "data: ")), &token) // 解析 JSON
513
 
514
+ // 构建 OpenAI 格式的流式响应块
515
  openAIResp := OpenAIStreamResponse{
516
  ID: "chatcmpl-" + fmt.Sprintf("%d", time.Now().Unix()),
517
  Object: "chat.completion.chunk",
518
  Created: time.Now().Unix(),
519
+ Model: reverseMapModelName(mapModelName(originalModel)), // 映射回 OpenAI 模型名称
520
  Choices: []Choice{
521
  {
522
  Delta: Delta{
523
+ Content: token.YouChatToken, // 增量内容
524
  },
525
  Index: 0,
526
+ FinishReason: "", // 流式响应中通常为空
527
  },
528
  },
529
  }
530
 
531
+ respBytes, _ := json.Marshal(openAIResp) // 将响应块序列化为 JSON
532
+ fmt.Fprintf(w, "data: %s\n\n", string(respBytes)) // 写入响应数据
533
+ w.(http.Flusher).Flush() // 立即刷新输出
534
+ }
535
+ }
536
+
537
+ }
538
+
539
+ // 获取上传文件所需的 nonce
540
+ func getNonce(dsToken string) (*NonceResponse, error) {
541
+ req, _ := http.NewRequest("GET", "https://you.com/api/get_nonce", nil)
542
+ req.Header.Set("Cookie", fmt.Sprintf("ds_token=%s", dsToken))
543
+
544
+ resp, err := http.DefaultClient.Do(req)
545
+ if err != nil {
546
+ return nil, err
547
+ }
548
+ defer resp.Body.Close()
549
+
550
+ var nonceResp NonceResponse
551
+ if err := json.NewDecoder(resp.Body).Decode(&nonceResp); err != nil {
552
+ return nil, err
553
+ }
554
+ return &nonceResp, nil
555
+ }
556
+
557
+ // 上传文件
558
+ func uploadFile(dsToken, filePath string) (*UploadResponse, error) {
559
+ file, err := os.Open(filePath)
560
+ if err != nil {
561
+ return nil, err
562
+ }
563
+ defer file.Close()
564
+
565
+ body := &bytes.Buffer{}
566
+ writer := multipart.NewWriter(body)
567
+ part, err := writer.CreateFormFile("file", filepath.Base(filePath))
568
+ if err != nil {
569
+ return nil, err
570
+ }
571
+
572
+ if _, err := io.Copy(part, file); err != nil {
573
+ return nil, err
574
+ }
575
+ writer.Close()
576
+
577
+ req, _ := http.NewRequest("POST", "https://you.com/api/upload", body)
578
+ req.Header.Set("Content-Type", writer.FormDataContentType())
579
+ req.Header.Set("Cookie", fmt.Sprintf("ds_token=%s", dsToken))
580
+
581
+ resp, err := http.DefaultClient.Do(req)
582
+ if err != nil {
583
+ return nil, err
584
+ }
585
+ defer resp.Body.Close()
586
+
587
+ var uploadResp UploadResponse
588
+ if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil {
589
+ return nil, err
590
+ }
591
+ return &uploadResp, nil
592
+ }
593
+
594
+ // 计算消息的 token 数(使用字符估算方法)
595
+ func countTokens(messages []Message) (int, error) {
596
+ totalTokens := 0
597
+ for _, msg := range messages {
598
+ content := msg.Content
599
+ englishCount := 0
600
+ chineseCount := 0
601
+
602
+ // 遍历每个字符
603
+ for _, r := range content {
604
+ if r <= 127 { // ASCII 字符(英文和符号)
605
+ englishCount++
606
+ } else { // 非 ASCII 字符(中文等)
607
+ chineseCount++
608
+ }
609
+ }
610
+
611
+ // 计算 tokens:英文字符 * 0.3 + 中文字符 * 0.6
612
+ tokens := int(float64(englishCount)*0.3 + float64(chineseCount)*0.6)
613
+
614
+ // 加上角色名的 token(约 2 个)
615
+ totalTokens += tokens + 2
616
+ }
617
+ return totalTokens, nil
618
+ }
619
+
620
+ // 将 system 消息转换为第一条 user 消息
621
+ func convertSystemToUser(messages []Message) []Message {
622
+ if len(messages) == 0 {
623
+ return messages
624
+ }
625
+
626
+ var systemContent strings.Builder
627
+ var newMessages []Message
628
+ var systemFound bool
629
+
630
+ // 收��所有 system 消息
631
+ for _, msg := range messages {
632
+ if msg.Role == "system" {
633
+ if systemContent.Len() > 0 {
634
+ systemContent.WriteString("\n")
635
+ }
636
+ systemContent.WriteString(msg.Content)
637
+ systemFound = true
638
+ } else {
639
+ newMessages = append(newMessages, msg)
640
  }
641
  }
642
+
643
+ // 如果有 system 消息,将其作为第一条 user 消息
644
+ if systemFound {
645
+ newMessages = append([]Message{{
646
+ Role: "user",
647
+ Content: systemContent.String(),
648
+ }}, newMessages...)
649
+ }
650
+
651
+ return newMessages
652
  }
go.mod CHANGED
@@ -3,6 +3,7 @@ module you2api
3
  go 1.22.2
4
 
5
  require (
 
6
  github.com/prometheus/client_golang v1.18.0
7
  go.uber.org/zap v1.26.0
8
  )
 
3
  go 1.22.2
4
 
5
  require (
6
+ github.com/google/uuid v1.6.0
7
  github.com/prometheus/client_golang v1.18.0
8
  go.uber.org/zap v1.26.0
9
  )
go.sum CHANGED
@@ -8,6 +8,8 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
8
  github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
9
  github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
10
  github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 
 
11
  github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
12
  github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
13
  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -20,6 +22,8 @@ github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lne
20
  github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
21
  github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
22
  github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 
 
23
  github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
24
  github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
25
  go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
 
8
  github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
9
  github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
10
  github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
11
+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
12
+ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
13
  github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
14
  github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
15
  github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 
22
  github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
23
  github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
24
  github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
25
+ github.com/sashabaranov/go-openai v1.20.2 h1:nilzF2EKzaHyK4Rk2Dbu/aJEZbtIvskDIXvfS4yx+6M=
26
+ github.com/sashabaranov/go-openai v1.20.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
27
  github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
28
  github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
29
  go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=