zhaoxiaozhao07 commited on
Commit
68ad966
·
1 Parent(s): 10e6665

feat(core): 更新配置和请求处理逻辑

Browse files

- 移除 docker-compose.yml 中的版本声明
- 在 config.py 中添加强制禁用签名验证的配置
- 优化动态请求头生成,更新 User-Agent 和相关字段
- 在 openai.py 中增强错误处理逻辑,支持认证和速率限制错误的重试
- 改进 SSE 流处理,优化数据解析和完整性检查
- 在 zai_transformer.py 中添加请求签名生成和查询参数构建功能

app/core/config.py CHANGED
@@ -32,6 +32,11 @@ class Settings(BaseSettings):
32
  SCAN_LIMIT: int = int(os.getenv("SCAN_LIMIT", "200000"))
33
  SKIP_AUTH_TOKEN: bool = os.getenv("SKIP_AUTH_TOKEN", "false").lower() == "true"
34
 
 
 
 
 
 
35
  # Token Pool Configuration
36
  TOKEN_FILE_PATH: str = os.getenv("TOKEN_FILE_PATH", "./tokens.txt")
37
  TOKEN_MAX_FAILURES: int = int(os.getenv("TOKEN_MAX_FAILURES", "3"))
@@ -46,17 +51,22 @@ class Settings(BaseSettings):
46
  HTTP_PROXY: Optional[str] = os.getenv("HTTP_PROXY")
47
  HTTPS_PROXY: Optional[str] = os.getenv("HTTPS_PROXY")
48
 
49
- # Browser Headers
50
  CLIENT_HEADERS: Dict[str, str] = {
51
- "Content-Type": "application/json",
52
- "Accept": "application/json, text/event-stream",
53
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",
54
  "Accept-Language": "zh-CN",
55
- "sec-ch-ua": '"Not;A=Brand";v="99", "Microsoft Edge";v="139", "Chromium";v="139"',
56
- "sec-ch-ua-mobile": "?0",
57
- "sec-ch-ua-platform": '"Windows"',
58
- "X-FE-Version": "prod-fe-1.0.70",
 
 
 
 
 
59
  "Origin": "https://chat.z.ai",
 
60
  }
61
 
62
  class Config:
 
32
  SCAN_LIMIT: int = int(os.getenv("SCAN_LIMIT", "200000"))
33
  SKIP_AUTH_TOKEN: bool = os.getenv("SKIP_AUTH_TOKEN", "false").lower() == "true"
34
 
35
+ # Signature Configuration - 强制禁用,忽略所有环境变量
36
+ ENABLE_SIGNATURE: bool = False # 强制禁用签名验证
37
+ SIGNATURE_SECRET_KEY: str = "disabled" # 已禁用
38
+ SIGNATURE_ALGORITHM: str = "disabled" # 已禁用
39
+
40
  # Token Pool Configuration
41
  TOKEN_FILE_PATH: str = os.getenv("TOKEN_FILE_PATH", "./tokens.txt")
42
  TOKEN_MAX_FAILURES: int = int(os.getenv("TOKEN_MAX_FAILURES", "3"))
 
51
  HTTP_PROXY: Optional[str] = os.getenv("HTTP_PROXY")
52
  HTTPS_PROXY: Optional[str] = os.getenv("HTTPS_PROXY")
53
 
54
+ # Browser Headers - 匹配真实F12调试信息
55
  CLIENT_HEADERS: Dict[str, str] = {
56
+ "Accept": "*/*",
57
+ "Accept-Encoding": "gzip, deflate, br, zstd",
 
58
  "Accept-Language": "zh-CN",
59
+ "Content-Type": "application/json",
60
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
61
+ "Sec-Ch-Ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"',
62
+ "Sec-Ch-Ua-Mobile": "?0",
63
+ "Sec-Ch-Ua-Platform": '"Windows"',
64
+ "Sec-Fetch-Dest": "empty",
65
+ "Sec-Fetch-Mode": "cors",
66
+ "Sec-Fetch-Site": "same-origin",
67
+ "X-Fe-Version": "prod-fe-1.0.83", # 匹配F12信息中的版本
68
  "Origin": "https://chat.z.ai",
69
+ "Connection": "keep-alive",
70
  }
71
 
72
  class Config:
app/core/openai.py CHANGED
@@ -90,7 +90,7 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
90
 
91
  async with httpx.AsyncClient(timeout=60.0) as client:
92
  # 发送请求到上游
93
- debug_log(f"🎯 发送请求到 Z.AI: {transformed['config']['url']}")
94
  async with client.stream(
95
  "POST",
96
  transformed["config"]["url"],
@@ -125,18 +125,70 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
125
  yield "data: [DONE]\n\n"
126
  return
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  elif response.status_code != 200:
129
- # 其他错误,直接返回
130
- debug_log(f"❌ 上游返回错误: {response.status_code}")
131
  error_text = await response.aread()
132
  error_msg = error_text.decode('utf-8', errors='ignore')
133
- debug_log(f"❌ 错误详情: {error_msg}")
134
-
 
 
 
 
 
 
 
 
 
135
  error_response = {
136
  "error": {
137
  "message": f"Upstream error: {response.status_code}",
138
  "type": "upstream_error",
139
- "code": response.status_code
 
140
  }
141
  }
142
  yield f"data: {json.dumps(error_response)}\n\n"
@@ -173,131 +225,96 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
173
  # 处理状态
174
  has_thinking = False
175
  thinking_signature = None
 
176
 
177
- # 处理SSE流
178
- buffer = ""
 
179
  line_count = 0
 
 
180
  debug_log("📡 开始接收 SSE 流数据...")
181
 
182
- async for line in response.aiter_lines():
183
- line_count += 1
184
- if not line:
 
 
185
  continue
186
 
187
- # 累积到buffer处理完整的数据行
188
- buffer += line + "\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
- # 检查是否有完整的data
191
- while "\n" in buffer:
192
- current_line, buffer = buffer.split("\n", 1)
193
- if not current_line.strip():
194
- continue
 
 
195
 
196
- if current_line.startswith("data:"):
197
- chunk_str = current_line[5:].strip()
198
- if not chunk_str or chunk_str == "[DONE]":
199
- if chunk_str == "[DONE]":
200
- yield "data: [DONE]\n\n"
201
- continue
202
 
203
- debug_log(f"📦 解析数据块: {chunk_str[:200]}..." if len(chunk_str) > 200 else f"📦 ��析数据块: {chunk_str}")
204
-
205
- try:
206
- chunk = json.loads(chunk_str)
207
-
208
- if chunk.get("type") == "chat:completion":
209
- data = chunk.get("data", {})
210
- phase = data.get("phase")
211
-
212
- # 记录每个阶段(只在阶段变化时记录)
213
- if phase and phase != getattr(stream_response, '_last_phase', None):
214
- debug_log(f"📈 SSE 阶段: {phase}")
215
- stream_response._last_phase = phase
216
-
217
- # 处理工具调用
218
- if phase == "tool_call" and tool_handler:
219
- for output in tool_handler.process_tool_call_phase(data, True):
220
- yield output
221
-
222
- # 处理其他阶段(工具结束)
223
- elif phase == "other" and tool_handler:
224
- for output in tool_handler.process_other_phase(data, True):
225
- yield output
226
-
227
- # 处理思考内容
228
- elif phase == "thinking":
229
- if not has_thinking:
230
- has_thinking = True
231
- # 发送初始角色
232
- role_chunk = {
233
- "choices": [
234
- {
235
- "delta": {"role": "assistant"},
236
- "finish_reason": None,
237
- "index": 0,
238
- "logprobs": None,
239
- }
240
- ],
241
- "created": int(time.time()),
242
- "id": transformed["body"]["chat_id"],
243
- "model": request.model,
244
- "object": "chat.completion.chunk",
245
- "system_fingerprint": "fp_zai_001",
246
- }
247
- yield f"data: {json.dumps(role_chunk)}\n\n"
248
-
249
- delta_content = data.get("delta_content", "")
250
- if delta_content:
251
- # 处理思考内容格式
252
- if delta_content.startswith("<details"):
253
- content = (
254
- delta_content.split("</summary>\n>")[-1].strip()
255
- if "</summary>\n>" in delta_content
256
- else delta_content
257
- )
258
- else:
259
- content = delta_content
260
-
261
- thinking_chunk = {
262
- "choices": [
263
- {
264
- "delta": {
265
- "role": "assistant",
266
- "thinking": {"content": content},
267
- },
268
- "finish_reason": None,
269
- "index": 0,
270
- "logprobs": None,
271
- }
272
- ],
273
- "created": int(time.time()),
274
- "id": transformed["body"]["chat_id"],
275
- "model": request.model,
276
- "object": "chat.completion.chunk",
277
- "system_fingerprint": "fp_zai_001",
278
- }
279
- yield f"data: {json.dumps(thinking_chunk)}\n\n"
280
-
281
- # 处理答案内容
282
- elif phase == "answer":
283
- edit_content = data.get("edit_content", "")
284
- delta_content = data.get("delta_content", "")
285
-
286
- # 处理思考结束和答案开始
287
- if edit_content and "</details>\n" in edit_content:
288
- if has_thinking:
289
- # 发送思考签名
290
- thinking_signature = str(int(time.time() * 1000))
291
- sig_chunk = {
292
  "choices": [
293
  {
294
- "delta": {
295
- "role": "assistant",
296
- "thinking": {
297
- "content": "",
298
- "signature": thinking_signature,
299
- },
300
- },
301
  "finish_reason": None,
302
  "index": 0,
303
  "logprobs": None,
@@ -309,17 +326,33 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
309
  "object": "chat.completion.chunk",
310
  "system_fingerprint": "fp_zai_001",
311
  }
312
- yield f"data: {json.dumps(sig_chunk)}\n\n"
313
 
314
- # 提取答案内容
315
- content_after = edit_content.split("</details>\n")[-1]
316
- if content_after:
317
- content_chunk = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  "choices": [
319
  {
320
  "delta": {
321
  "role": "assistant",
322
- "content": content_after,
323
  },
324
  "finish_reason": None,
325
  "index": 0,
@@ -332,12 +365,16 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
332
  "object": "chat.completion.chunk",
333
  "system_fingerprint": "fp_zai_001",
334
  }
335
- yield f"data: {json.dumps(content_chunk)}\n\n"
336
 
337
- # 处理增量内容
338
- elif delta_content:
339
- # 如果还没有发送角色
 
 
 
340
  if not has_thinking:
 
341
  role_chunk = {
342
  "choices": [
343
  {
@@ -353,31 +390,102 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
353
  "object": "chat.completion.chunk",
354
  "system_fingerprint": "fp_zai_001",
355
  }
 
356
  yield f"data: {json.dumps(role_chunk)}\n\n"
357
 
358
- content_chunk = {
359
- "choices": [
360
- {
361
- "delta": {
362
- "role": "assistant",
363
- "content": delta_content,
364
- },
365
- "finish_reason": None,
366
- "index": 0,
367
- "logprobs": None,
 
 
 
 
 
 
 
 
 
 
 
 
368
  }
369
- ],
370
- "created": int(time.time()),
371
- "id": transformed["body"]["chat_id"],
372
- "model": request.model,
373
- "object": "chat.completion.chunk",
374
- "system_fingerprint": "fp_zai_001",
375
- }
376
- output_data = f"data: {json.dumps(content_chunk)}\n\n"
377
- debug_log(f"➡️ 输出内容块到客户端: {output_data[:200]}...")
378
- yield output_data
379
-
380
- # 处理完成
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  if data.get("usage"):
382
  debug_log(f"📦 完成响应 - 使用统计: {json.dumps(data['usage'])}")
383
 
@@ -386,7 +494,7 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
386
  finish_chunk = {
387
  "choices": [
388
  {
389
- "delta": {"role": "assistant", "content": ""},
390
  "finish_reason": "stop",
391
  "index": 0,
392
  "logprobs": None,
@@ -400,22 +508,82 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
400
  "system_fingerprint": "fp_zai_001",
401
  }
402
  finish_output = f"data: {json.dumps(finish_chunk)}\n\n"
403
- debug_log(f"➡️ 发送完成信号: {finish_output[:200]}...")
404
  yield finish_output
405
  debug_log("➡️ 发送 [DONE]")
406
  yield "data: [DONE]\n\n"
407
 
408
- except json.JSONDecodeError as e:
409
- debug_log(f"❌ JSON解析错误: {e}, 内容: {chunk_str[:200]}")
410
- except Exception as e:
411
- debug_log(f"❌ 处理chunk错误: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
413
  # 确保发送结束信号
414
  if not tool_handler or not tool_handler.has_tool_call:
415
  debug_log("📤 发送最终 [DONE] 信号")
416
  yield "data: [DONE]\n\n"
417
 
418
- debug_log(f"✅ SSE 流处理完成,共处理 {line_count} 行数据")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  # 成功处理完成,退出重试循环
420
  return
421
 
@@ -455,7 +623,7 @@ async def chat_completions(request: OpenAIRequest, authorization: str = Header(.
455
  debug_log("📤 开始向客户端流式传输数据...")
456
  async for chunk in stream_response():
457
  chunk_count += 1
458
- debug_log(f"📤 发送块[{chunk_count}]: {chunk[:200]}..." if len(chunk) > 200 else f" 📤 发送块[{chunk_count}]: {chunk}")
459
  yield chunk
460
  debug_log(f"✅ 流式传输完成,共发送 {chunk_count} 个数据块")
461
  except Exception as e:
 
90
 
91
  async with httpx.AsyncClient(timeout=60.0) as client:
92
  # 发送请求到上游
93
+ # debug_log(f"🎯 发送请求到 Z.AI: {transformed['config']['url']}")
94
  async with client.stream(
95
  "POST",
96
  transformed["config"]["url"],
 
125
  yield "data: [DONE]\n\n"
126
  return
127
 
128
+ elif response.status_code == 401:
129
+ # 认证错误,可能需要重新获取token
130
+ debug_log(f"❌ 认证失败 (401),标记token失效")
131
+ if current_token:
132
+ transformer.mark_token_failure(current_token, Exception("401 Unauthorized"))
133
+
134
+ retry_count += 1
135
+ last_error = "401 Unauthorized - Token may be invalid"
136
+
137
+ if retry_count <= settings.MAX_RETRIES:
138
+ continue
139
+ else:
140
+ error_response = {
141
+ "error": {
142
+ "message": "Authentication failed after retries",
143
+ "type": "auth_error",
144
+ "code": 401
145
+ }
146
+ }
147
+ yield f"data: {json.dumps(error_response)}\n\n"
148
+ yield "data: [DONE]\n\n"
149
+ return
150
+
151
+ elif response.status_code == 429:
152
+ # 速率限制,延长等待时间重试
153
+ debug_log(f"❌ 速率限制 (429),将延长等待时间重试")
154
+ retry_count += 1
155
+ last_error = "429 Rate Limited"
156
+
157
+ if retry_count <= settings.MAX_RETRIES:
158
+ continue
159
+ else:
160
+ error_response = {
161
+ "error": {
162
+ "message": "Rate limit exceeded",
163
+ "type": "rate_limit_error",
164
+ "code": 429
165
+ }
166
+ }
167
+ yield f"data: {json.dumps(error_response)}\n\n"
168
+ yield "data: [DONE]\n\n"
169
+ return
170
+
171
  elif response.status_code != 200:
172
+ # 其他错误,检查是否需要重试
 
173
  error_text = await response.aread()
174
  error_msg = error_text.decode('utf-8', errors='ignore')
175
+ debug_log(f"❌ 上游返回错误: {response.status_code}, 详情: {error_msg}")
176
+
177
+ # 某些错误可以重试
178
+ retryable_codes = [502, 503, 504]
179
+ if response.status_code in retryable_codes and retry_count < settings.MAX_RETRIES:
180
+ retry_count += 1
181
+ last_error = f"{response.status_code}: {error_msg}"
182
+ debug_log(f"⚠️ 服务器错误 {response.status_code},准备重试")
183
+ continue
184
+
185
+ # 不可重试的错误或已达到重试上限
186
  error_response = {
187
  "error": {
188
  "message": f"Upstream error: {response.status_code}",
189
  "type": "upstream_error",
190
+ "code": response.status_code,
191
+ "details": error_msg[:500] # 限制错误详情长度
192
  }
193
  }
194
  yield f"data: {json.dumps(error_response)}\n\n"
 
225
  # 处理状态
226
  has_thinking = False
227
  thinking_signature = None
228
+ first_thinking_chunk = True
229
 
230
+ # 处理SSE流 - 优化的buffer处理
231
+ buffer = bytearray()
232
+ incomplete_line = ""
233
  line_count = 0
234
+ chunk_count = 0
235
+ last_activity = time.time()
236
  debug_log("📡 开始接收 SSE 流数据...")
237
 
238
+ async for chunk in response.aiter_bytes():
239
+ chunk_count += 1
240
+ last_activity = time.time()
241
+
242
+ if not chunk:
243
  continue
244
 
245
+ # 将新数据添加到buffer
246
+ buffer.extend(chunk)
247
+
248
+ # 尝试解码并处理完整的行
249
+ try:
250
+ # 解码为字符串并处理
251
+ text_data = buffer.decode('utf-8')
252
+
253
+ # 分割为行
254
+ lines = text_data.split('\n')
255
+
256
+ # 最后一行可能不完整,保存到incomplete_line
257
+ if not text_data.endswith('\n'):
258
+ incomplete_line = lines[-1]
259
+ lines = lines[:-1]
260
+ else:
261
+ # 如果有未完成的行,将其与第一行合并
262
+ if incomplete_line:
263
+ lines[0] = incomplete_line + lines[0]
264
+ incomplete_line = ""
265
+
266
+ # 清空buffer,开始处理新的数据
267
+ buffer = bytearray()
268
+ if incomplete_line:
269
+ buffer.extend(incomplete_line.encode('utf-8'))
270
+
271
+ # 处理完整的行
272
+ for current_line in lines:
273
+ line_count += 1
274
+ if not current_line.strip():
275
+ continue
276
 
277
+ if current_line.startswith("data:"):
278
+ chunk_str = current_line[5:].strip()
279
+ if not chunk_str or chunk_str == "[DONE]":
280
+ if chunk_str == "[DONE]":
281
+ debug_log("📡 收到 [DONE] 信号")
282
+ yield "data: [DONE]\n\n"
283
+ continue
284
 
285
+ # debug_log(f"📦 解析数据块: {chunk_str[:200]}..." if len(chunk_str) > 200 else f"📦 解析数据块: {chunk_str}")
 
 
 
 
 
286
 
287
+ try:
288
+ chunk = json.loads(chunk_str)
289
+
290
+ if chunk.get("type") == "chat:completion":
291
+ data = chunk.get("data", {})
292
+ phase = data.get("phase")
293
+
294
+ # 记录每个阶段(只在阶段变化时记录)
295
+ if phase and phase != getattr(stream_response, '_last_phase', None):
296
+ debug_log(f"📈 SSE 阶段: {phase}")
297
+ stream_response._last_phase = phase
298
+
299
+ # 处理工具调用
300
+ if phase == "tool_call" and tool_handler:
301
+ for output in tool_handler.process_tool_call_phase(data, True):
302
+ yield output
303
+
304
+ # 处理其他阶段(工具结束)
305
+ elif phase == "other" and tool_handler:
306
+ for output in tool_handler.process_other_phase(data, True):
307
+ yield output
308
+
309
+ # 处理思考内容
310
+ elif phase == "thinking":
311
+ if not has_thinking:
312
+ has_thinking = True
313
+ # 发送初始角色
314
+ role_chunk = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
  "choices": [
316
  {
317
+ "delta": {"role": "assistant"},
 
 
 
 
 
 
318
  "finish_reason": None,
319
  "index": 0,
320
  "logprobs": None,
 
326
  "object": "chat.completion.chunk",
327
  "system_fingerprint": "fp_zai_001",
328
  }
329
+ yield f"data: {json.dumps(role_chunk)}\n\n"
330
 
331
+ delta_content = data.get("delta_content", "")
332
+ if delta_content:
333
+ # 处理思考内容格式
334
+ if delta_content.startswith("<details"):
335
+ content = (
336
+ delta_content.split("</summary>\n>")[-1].strip()
337
+ if "</summary>\n>" in delta_content
338
+ else delta_content
339
+ )
340
+ else:
341
+ content = delta_content
342
+
343
+ # 第一个思考块添加<think>开始标签,其他块保持纯内容
344
+ if first_thinking_chunk:
345
+ formatted_content = f"<think>{content}"
346
+ first_thinking_chunk = False
347
+ else:
348
+ formatted_content = content
349
+
350
+ thinking_chunk = {
351
  "choices": [
352
  {
353
  "delta": {
354
  "role": "assistant",
355
+ "content": formatted_content,
356
  },
357
  "finish_reason": None,
358
  "index": 0,
 
365
  "object": "chat.completion.chunk",
366
  "system_fingerprint": "fp_zai_001",
367
  }
368
+ yield f"data: {json.dumps(thinking_chunk)}\n\n"
369
 
370
+ # 处理答案内容
371
+ elif phase == "answer":
372
+ edit_content = data.get("edit_content", "")
373
+ delta_content = data.get("delta_content", "")
374
+
375
+ # 如果还没有发送角色,先发送角色chunk
376
  if not has_thinking:
377
+ has_thinking = True # 设置标志避免重复发送
378
  role_chunk = {
379
  "choices": [
380
  {
 
390
  "object": "chat.completion.chunk",
391
  "system_fingerprint": "fp_zai_001",
392
  }
393
+ debug_log("➡️ 发送初始角色chunk")
394
  yield f"data: {json.dumps(role_chunk)}\n\n"
395
 
396
+ # 处理思考结束和答案开始
397
+ if edit_content and "</details>\n" in edit_content:
398
+ if has_thinking and not first_thinking_chunk:
399
+ # 发送思考结束标记</think>
400
+ thinking_signature = str(int(time.time() * 1000))
401
+ sig_chunk = {
402
+ "choices": [
403
+ {
404
+ "delta": {
405
+ "role": "assistant",
406
+ "content": "</think>",
407
+ },
408
+ "finish_reason": None,
409
+ "index": 0,
410
+ "logprobs": None,
411
+ }
412
+ ],
413
+ "created": int(time.time()),
414
+ "id": transformed["body"]["chat_id"],
415
+ "model": request.model,
416
+ "object": "chat.completion.chunk",
417
+ "system_fingerprint": "fp_zai_001",
418
  }
419
+ yield f"data: {json.dumps(sig_chunk)}\n\n"
420
+
421
+ # 提取答案内容
422
+ content_after = edit_content.split("</details>\n")[-1]
423
+ if content_after:
424
+ content_chunk = {
425
+ "choices": [
426
+ {
427
+ "delta": {
428
+ "role": "assistant",
429
+ "content": content_after,
430
+ },
431
+ "finish_reason": None,
432
+ "index": 0,
433
+ "logprobs": None,
434
+ }
435
+ ],
436
+ "created": int(time.time()),
437
+ "id": transformed["body"]["chat_id"],
438
+ "model": request.model,
439
+ "object": "chat.completion.chunk",
440
+ "system_fingerprint": "fp_zai_001",
441
+ }
442
+ yield f"data: {json.dumps(content_chunk)}\n\n"
443
+
444
+ # 处理增量内容
445
+ elif delta_content:
446
+ # 如果还没有发送角色
447
+ if not has_thinking:
448
+ has_thinking = True # 避免重复发送
449
+ role_chunk = {
450
+ "choices": [
451
+ {
452
+ "delta": {"role": "assistant"},
453
+ "finish_reason": None,
454
+ "index": 0,
455
+ "logprobs": None,
456
+ }
457
+ ],
458
+ "created": int(time.time()),
459
+ "id": transformed["body"]["chat_id"],
460
+ "model": request.model,
461
+ "object": "chat.completion.chunk",
462
+ "system_fingerprint": "fp_zai_001",
463
+ }
464
+ debug_log("➡️ 发送初始角色chunk")
465
+ yield f"data: {json.dumps(role_chunk)}\n\n"
466
+
467
+ content_chunk = {
468
+ "choices": [
469
+ {
470
+ "delta": {
471
+ "content": delta_content,
472
+ },
473
+ "finish_reason": None,
474
+ "index": 0,
475
+ "logprobs": None,
476
+ }
477
+ ],
478
+ "created": int(time.time()),
479
+ "id": transformed["body"]["chat_id"],
480
+ "model": request.model,
481
+ "object": "chat.completion.chunk",
482
+ "system_fingerprint": "fp_zai_001",
483
+ }
484
+ output_data = f"data: {json.dumps(content_chunk)}\n\n"
485
+ # debug_log(f"➡️ 输出内容块到客户端: {delta_content[:50]}...")
486
+ yield output_data
487
+
488
+ # 处理完成 - 当收到usage信息时
489
  if data.get("usage"):
490
  debug_log(f"📦 完成响应 - 使用统计: {json.dumps(data['usage'])}")
491
 
 
494
  finish_chunk = {
495
  "choices": [
496
  {
497
+ "delta": {}, # 空的delta表示结束
498
  "finish_reason": "stop",
499
  "index": 0,
500
  "logprobs": None,
 
508
  "system_fingerprint": "fp_zai_001",
509
  }
510
  finish_output = f"data: {json.dumps(finish_chunk)}\n\n"
511
+ debug_log("➡️ 发送完成信号")
512
  yield finish_output
513
  debug_log("➡️ 发送 [DONE]")
514
  yield "data: [DONE]\n\n"
515
 
516
+ except json.JSONDecodeError as e:
517
+ debug_log(f"❌ JSON解析错误: {e}, 内容: {chunk_str[:200]}")
518
+ except Exception as e:
519
+ debug_log(f"❌ 处理chunk错误: {e}")
520
+
521
+ except UnicodeDecodeError:
522
+ # 如果解码失败,可能是数据不完整,继续接收
523
+ debug_log(f"⚠️ 数据解码失败,缓冲区大小: {len(buffer)}")
524
+ if len(buffer) > 1024 * 1024: # 1MB限制
525
+ debug_log("❌ 缓冲区过大,清空重试")
526
+ buffer = bytearray()
527
+ incomplete_line = ""
528
+ except Exception as e:
529
+ debug_log(f"❌ Buffer处理异常: {e}")
530
+ # 清空buffer继续处理
531
+ buffer = bytearray()
532
+ incomplete_line = ""
533
+
534
+ # 检查是否长时间没有活动(超时检查)
535
+ if time.time() - last_activity > 30: # 30秒超时
536
+ debug_log("⚠️ 检测到长时间无活动,可能连接中断")
537
+ break
538
 
539
  # 确保发送结束信号
540
  if not tool_handler or not tool_handler.has_tool_call:
541
  debug_log("📤 发送最终 [DONE] 信号")
542
  yield "data: [DONE]\n\n"
543
 
544
+ debug_log(f"✅ SSE 流处理完成,共处理 {line_count} 行数据,{chunk_count} 个数据块")
545
+
546
+ # 检查处理完整性
547
+ is_complete = True
548
+ completion_issues = []
549
+
550
+ if line_count == 0:
551
+ is_complete = False
552
+ completion_issues.append("没有处理任何数据行")
553
+ elif chunk_count == 0:
554
+ is_complete = False
555
+ completion_issues.append("没有收到任何数据块")
556
+ elif chunk_count > 0:
557
+ debug_log(f"📊 平均每个数据块包含 {line_count/chunk_count:.1f} 行")
558
+
559
+ # 检查工具调用完整性
560
+ if tool_handler and tool_handler.has_tool_call:
561
+ if not tool_handler.completed_tools:
562
+ completion_issues.append("工具调用未正常完成")
563
+ else:
564
+ debug_log(f"✅ 工具调用完成: {len(tool_handler.completed_tools)} 个工具")
565
+
566
+ # 检查思考内容完整性(只有真正的thinking模式才需要签名)
567
+ # 注意:普通的answer阶段不需要thinking签名,只有thinking阶段才需要
568
+ # if has_thinking and not thinking_signature:
569
+ # completion_issues.append("思考内容缺少签名")
570
+
571
+ # 报告完整性状态
572
+ if is_complete and not completion_issues:
573
+ debug_log("✅ 响应完整性检查通过")
574
+ else:
575
+ debug_log(f"⚠️ 响应完整性问题: {', '.join(completion_issues)}")
576
+
577
+ # 如果问题严重且还有重试机会,考虑重试
578
+ critical_issues = ["没有处理任何数据行", "没有收到任何数据块"]
579
+ has_critical_issue = any(issue in completion_issues for issue in critical_issues)
580
+
581
+ if has_critical_issue and retry_count < settings.MAX_RETRIES:
582
+ debug_log("🔄 检测到严重完整性问题,准备重试")
583
+ retry_count += 1
584
+ last_error = f"Incomplete response: {', '.join(completion_issues)}"
585
+ continue
586
+
587
  # 成功处理完成,退出重试循环
588
  return
589
 
 
623
  debug_log("📤 开始向客户端流式传输数据...")
624
  async for chunk in stream_response():
625
  chunk_count += 1
626
+ # debug_log(f"📤 发送块[{chunk_count}]: {chunk[:200]}..." if len(chunk) > 200 else f" 📤 发送块[{chunk_count}]: {chunk}")
627
  yield chunk
628
  debug_log(f"✅ 流式传输完成,共发送 {chunk_count} 个数据块")
629
  except Exception as e:
app/core/response_handlers.py CHANGED
@@ -1,376 +1,397 @@
1
- """
2
- Response handlers for streaming and non-streaming responses
3
- """
4
-
5
- import json
6
- import time
7
- from typing import Generator, Optional
8
- import requests
9
- from fastapi import HTTPException
10
- from fastapi.responses import JSONResponse, StreamingResponse
11
-
12
- from app.core.config import settings
13
- from app.models.schemas import (
14
- Message, Delta, Choice, Usage, OpenAIResponse,
15
- UpstreamRequest, UpstreamData, UpstreamError, ModelItem
16
- )
17
- from app.utils.helpers import debug_log, call_upstream_api, transform_thinking_content
18
- from app.core.token_manager import token_manager
19
- from app.utils.sse_parser import SSEParser
20
  from app.utils.tools import extract_tool_invocations, remove_tool_json_content
21
- from app.utils.sse_tool_handler import SSEToolHandler
22
-
23
-
24
- def create_openai_response_chunk(
25
- model: str,
26
- delta: Optional[Delta] = None,
27
- finish_reason: Optional[str] = None
28
- ) -> OpenAIResponse:
29
- """Create OpenAI response chunk for streaming"""
30
- return OpenAIResponse(
31
- id=f"chatcmpl-{int(time.time())}",
32
- object="chat.completion.chunk",
33
- created=int(time.time()),
34
- model=model,
35
- choices=[Choice(
36
- index=0,
37
- delta=delta or Delta(),
38
- finish_reason=finish_reason
39
- )]
40
- )
41
-
42
-
43
- def handle_upstream_error(error: UpstreamError) -> Generator[str, None, None]:
44
- """Handle upstream error response"""
45
- debug_log(f"上游错误: code={error.code}, detail={error.detail}")
46
-
47
- # Send end chunk
48
- end_chunk = create_openai_response_chunk(
49
- model=settings.PRIMARY_MODEL,
50
- finish_reason="stop"
51
- )
52
- yield f"data: {end_chunk.model_dump_json()}\n\n"
53
- yield "data: [DONE]\n\n"
54
-
55
-
56
- class ResponseHandler:
57
- """Base class for response handling"""
58
-
59
- def __init__(self, upstream_req: UpstreamRequest, chat_id: str, auth_token: str):
60
- self.upstream_req = upstream_req
61
- self.chat_id = chat_id
62
- self.auth_token = auth_token
63
-
64
- def _call_upstream(self) -> requests.Response:
65
- """Call upstream API with error handling"""
66
- max_retries = settings.MAX_RETRIES
67
- retry_count = 0
68
-
69
- while retry_count < max_retries:
70
- try:
71
- debug_log(f"尝试调用上游API (第 {retry_count + 1}/{max_retries} 次)")
72
- response = call_upstream_api(self.upstream_req, self.chat_id, self.auth_token)
73
-
74
- # Check if response is successful
75
- if response.status_code == 200:
76
- # Mark token as successful
77
- token_manager.mark_token_success(self.auth_token)
78
- debug_log("上游API调用成功")
79
- return response
80
- elif response.status_code in [401, 403]:
81
- # Authentication/authorization error - mark token as failed
82
- debug_log(f"Token认证失败 (状态码: {response.status_code}): {self.auth_token[:20]}...")
83
- token_manager.mark_token_failed(self.auth_token)
84
-
85
- # Try to get a new token
86
- new_token = token_manager.get_next_token()
87
- if new_token and new_token != self.auth_token:
88
- debug_log(f"尝试使用新token: {new_token[:20]}...")
89
- self.auth_token = new_token
90
- retry_count += 1
91
- continue
92
- else:
93
- debug_log("没有更多可用token")
94
- return response
95
- elif response.status_code in [429]:
96
- # Rate limit - don't mark token as failed, just retry
97
- debug_log(f"遇到速率限制 (状态码: {response.status_code}),等待后重试")
98
- if retry_count < max_retries - 1:
99
- import time
100
- time.sleep(2 ** retry_count) # 指数退避
101
- retry_count += 1
102
- continue
103
- else:
104
- return response
105
- elif response.status_code >= 500:
106
- # Server error - retry without marking token as failed
107
- debug_log(f"服务器错误 (状态码: {response.status_code}),稍后重试")
108
- if retry_count < max_retries - 1:
109
- import time
110
- time.sleep(1)
111
- retry_count += 1
112
- continue
113
- else:
114
- return response
115
- else:
116
- # Other client errors, return response as-is
117
- debug_log(f"客户端错误 (状态码: {response.status_code})")
118
- return response
119
-
120
- except Exception as e:
121
- error_msg = str(e)
122
- debug_log(f"调用上游失败 (尝试 {retry_count + 1}/{max_retries}): {error_msg}")
123
-
124
- # 判断是否是连接问题还是token问题
125
- is_connection_error = any(keyword in error_msg.lower() for keyword in [
126
- 'connection', 'timeout', 'network', 'dns', 'socket', 'ssl'
127
- ])
128
-
129
- if is_connection_error:
130
- debug_log("检测到网络连接问题,不标记token失败")
131
- # 网络问题不标记token失败,直接重试
132
- if retry_count < max_retries - 1:
133
- import time
134
- time.sleep(2) # 等待2秒后重试
135
- retry_count += 1
136
- continue
137
- else:
138
- raise Exception(f"网络连接问题,重试{max_retries}次后仍失败: {error_msg}")
139
- else:
140
- # 其他错误可能是token问题,标记失败并尝试新token
141
- debug_log("检测到可能的token问题,标记token失败")
142
- token_manager.mark_token_failed(self.auth_token)
143
-
144
- # Try to get a new token
145
- new_token = token_manager.get_next_token()
146
- if new_token and new_token != self.auth_token and retry_count < max_retries - 1:
147
- debug_log(f"尝试使用新token: {new_token[:20]}...")
148
- self.auth_token = new_token
149
- retry_count += 1
150
- continue
151
- else:
152
- raise
153
-
154
- # If we get here, all retries failed
155
- raise Exception("所有重试尝试均失败")
156
-
157
- def _handle_upstream_error(self, response: requests.Response) -> None:
158
- """Handle upstream error response"""
159
- debug_log(f"上游返回错误状态: {response.status_code}")
160
- if settings.DEBUG_LOGGING:
161
- debug_log(f"上游错误响应: {response.text}")
162
-
163
-
164
- class StreamResponseHandler(ResponseHandler):
165
- """Handler for streaming responses"""
166
-
167
- def __init__(self, upstream_req: UpstreamRequest, chat_id: str, auth_token: str, has_tools: bool = False):
168
- super().__init__(upstream_req, chat_id, auth_token)
169
- self.has_tools = has_tools
170
- self.buffered_content = ""
171
  self.tool_calls = None
172
  # Initialize SSE tool handler for improved tool processing
173
- self.tool_handler = SSEToolHandler(chat_id, settings.PRIMARY_MODEL) if has_tools else None
174
-
175
- def handle(self) -> Generator[str, None, None]:
176
- """Handle streaming response"""
177
- debug_log(f"开始处理流式响应 (chat_id={self.chat_id})")
178
-
179
- try:
180
- response = self._call_upstream()
181
- except Exception:
182
- yield "data: {\"error\": \"Failed to call upstream\"}\n\n"
183
- return
184
-
185
- if response.status_code != 200:
186
- self._handle_upstream_error(response)
187
- yield "data: {\"error\": \"Upstream error\"}\n\n"
188
- return
189
-
190
- # Send initial role chunk
191
- first_chunk = create_openai_response_chunk(
192
- model=settings.PRIMARY_MODEL,
193
- delta=Delta(role="assistant")
194
- )
195
- yield f"data: {first_chunk.model_dump_json()}\n\n"
196
-
197
- # Process stream
198
- debug_log("开始读取上游SSE流")
199
- sent_initial_answer = False
200
- stream_ended_normally = False
201
-
202
- try:
203
- with SSEParser(response, debug_mode=settings.DEBUG_LOGGING) as parser:
204
- for event in parser.iter_json_data(UpstreamData):
205
- upstream_data = event['data']
206
-
207
- # Check for errors
208
- if self._has_error(upstream_data):
209
- error = self._get_error(upstream_data)
210
- yield from handle_upstream_error(error)
211
- stream_ended_normally = True
212
- break
213
-
214
- debug_log(f"解析成功 - 类型: {upstream_data.type}, 阶段: {upstream_data.data.phase}, "
215
- f"内容长度: {len(upstream_data.data.delta_content or '')}, 完成: {upstream_data.data.done}")
216
-
217
- # Process content
218
- yield from self._process_content_with_tools(upstream_data, sent_initial_answer)
219
-
220
- # Update sent_initial_answer flag if we sent content
221
- if not sent_initial_answer and (upstream_data.data.delta_content or upstream_data.data.edit_content):
222
- sent_initial_answer = True
223
-
224
- # Check if done
225
- if upstream_data.data.done or upstream_data.data.phase == "done":
226
- debug_log("检测到流结束信号")
227
- yield from self._send_end_chunk()
228
- stream_ended_normally = True
229
- break
230
-
231
- except Exception as e:
232
- debug_log(f"SSE流处理异常: {e}")
233
- # 流异常结束,发送错误响应
234
- if not stream_ended_normally:
235
- error_chunk = create_openai_response_chunk(
236
- model=settings.PRIMARY_MODEL,
237
- delta=Delta(content=f"\n\n[系统提示: 连接中断,响应可能不完整]")
238
- )
239
- yield f"data: {error_chunk.model_dump_json()}\n\n"
240
-
241
- # 确保流正常结束
242
- if not stream_ended_normally:
243
- debug_log("流未正常结束,发送结束信号")
244
- yield from self._send_end_chunk(force_stop=True)
245
-
246
- def _has_error(self, upstream_data: UpstreamData) -> bool:
247
- """Check if upstream data contains error"""
248
- return bool(
249
- upstream_data.error or
250
- upstream_data.data.error or
251
- (upstream_data.data.inner and upstream_data.data.inner.error)
252
- )
253
-
254
- def _get_error(self, upstream_data: UpstreamData) -> UpstreamError:
255
- """Get error from upstream data"""
256
- return (
257
- upstream_data.error or
258
- upstream_data.data.error or
259
- (upstream_data.data.inner.error if upstream_data.data.inner else None)
260
- )
261
-
262
- def _process_content(
263
- self,
264
- upstream_data: UpstreamData,
265
- sent_initial_answer: bool
266
- ) -> Generator[str, None, None]:
267
- """Process content from upstream data"""
268
- content = upstream_data.data.delta_content or upstream_data.data.edit_content
269
-
270
- if not content:
271
- return
272
-
273
- # Transform thinking content
274
- if upstream_data.data.phase == "thinking":
275
- content = transform_thinking_content(content)
276
-
277
- # Buffer content if tools are enabled
278
- if self.has_tools:
279
- self.buffered_content += content
280
- else:
281
- # Handle initial answer content
282
- if (not sent_initial_answer and
283
- upstream_data.data.edit_content and
284
- upstream_data.data.phase == "answer"):
285
-
286
- content = self._extract_edit_content(upstream_data.data.edit_content)
287
- if content:
288
- debug_log(f"发送普通内容: {content}")
289
- chunk = create_openai_response_chunk(
290
- model=settings.PRIMARY_MODEL,
291
- delta=Delta(content=content)
292
- )
293
- yield f"data: {chunk.model_dump_json()}\n\n"
294
- sent_initial_answer = True
295
-
296
- # Handle delta content
297
- if upstream_data.data.delta_content:
298
- if content:
299
- if upstream_data.data.phase == "thinking":
300
- debug_log(f"发送思考内容: {content}")
301
- chunk = create_openai_response_chunk(
302
- model=settings.PRIMARY_MODEL,
303
- delta=Delta(reasoning_content=content)
304
- )
305
- else:
306
- debug_log(f"发送普通内容: {content}")
307
- chunk = create_openai_response_chunk(
308
- model=settings.PRIMARY_MODEL,
309
- delta=Delta(content=content)
310
- )
311
- yield f"data: {chunk.model_dump_json()}\n\n"
312
-
313
- def _extract_edit_content(self, edit_content: str) -> str:
314
- """Extract content from edit_content field"""
315
- parts = edit_content.split("</details>")
316
- return parts[1] if len(parts) > 1 else ""
317
-
318
- def _send_end_chunk(self, force_stop: bool = False) -> Generator[str, None, None]:
319
- """Send end chunk and DONE signal"""
320
- finish_reason = "stop"
321
-
322
- if self.has_tools and not force_stop:
323
- # Try to extract tool calls from buffered content
324
- self.tool_calls = extract_tool_invocations(self.buffered_content)
325
-
326
- if self.tool_calls:
327
- debug_log(f"检测到工具调用: {len(self.tool_calls)} 个")
328
- # Send tool calls with proper format
329
- for i, tc in enumerate(self.tool_calls):
330
- tool_call_delta = {
331
- "index": i,
332
- "id": tc.get("id"),
333
- "type": tc.get("type", "function"),
334
- "function": tc.get("function", {}),
335
- }
336
-
337
- out_chunk = create_openai_response_chunk(
338
- model=settings.PRIMARY_MODEL,
339
- delta=Delta(tool_calls=[tool_call_delta])
340
- )
341
- yield f"data: {out_chunk.model_dump_json()}\n\n"
342
-
343
- finish_reason = "tool_calls"
344
- else:
345
- # Send regular content
346
- trimmed_content = remove_tool_json_content(self.buffered_content)
347
- if trimmed_content:
348
- debug_log(f"发送常规内容: {len(trimmed_content)} 字符")
349
- content_chunk = create_openai_response_chunk(
350
- model=settings.PRIMARY_MODEL,
351
- delta=Delta(content=trimmed_content)
352
- )
353
- yield f"data: {content_chunk.model_dump_json()}\n\n"
354
- elif force_stop:
355
- # 强制结束时,发送缓冲的内容(如果有)
356
- if self.buffered_content:
357
- debug_log(f"强制结束,发送缓冲内容: {len(self.buffered_content)} 字符")
358
- content_chunk = create_openai_response_chunk(
359
- model=settings.PRIMARY_MODEL,
360
- delta=Delta(content=self.buffered_content)
361
- )
362
- yield f"data: {content_chunk.model_dump_json()}\n\n"
363
-
364
- # Send final chunk
365
- end_chunk = create_openai_response_chunk(
366
- model=settings.PRIMARY_MODEL,
367
- finish_reason=finish_reason
368
- )
369
- yield f"data: {end_chunk.model_dump_json()}\n\n"
370
- yield "data: [DONE]\n\n"
371
- debug_log(f"流式响应完成 (finish_reason: {finish_reason})")
372
-
373
-
374
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  def _process_content_with_tools(
376
  self,
377
  upstream_data: UpstreamData,
@@ -399,101 +420,127 @@ class StreamResponseHandler(ResponseHandler):
399
  yield from self._process_content(upstream_data, sent_initial_answer)
400
 
401
 
402
- class NonStreamResponseHandler(ResponseHandler):
403
- """Handler for non-streaming responses"""
404
-
405
- def __init__(self, upstream_req: UpstreamRequest, chat_id: str, auth_token: str, has_tools: bool = False):
406
- super().__init__(upstream_req, chat_id, auth_token)
407
- self.has_tools = has_tools
408
-
409
- def handle(self) -> JSONResponse:
410
- """Handle non-streaming response"""
411
- debug_log(f"开始处理非流式响应 (chat_id={self.chat_id})")
412
-
413
- try:
414
- response = self._call_upstream()
415
- except Exception as e:
416
- debug_log(f"调用上游失败: {e}")
417
- raise HTTPException(status_code=502, detail="Failed to call upstream")
418
-
419
- if response.status_code != 200:
420
- self._handle_upstream_error(response)
421
- raise HTTPException(status_code=502, detail="Upstream error")
422
-
423
- # Collect full response
424
- full_content = []
425
- debug_log("开始收集完整响应内容")
426
- response_completed = False
427
-
428
- try:
429
- with SSEParser(response, debug_mode=settings.DEBUG_LOGGING) as parser:
430
- for event in parser.iter_json_data(UpstreamData):
431
- upstream_data = event['data']
432
-
433
- if upstream_data.data.delta_content:
434
- content = upstream_data.data.delta_content
435
-
436
- if upstream_data.data.phase == "thinking":
437
- content = transform_thinking_content(content)
438
-
439
- if content:
440
- full_content.append(content)
441
-
442
- if upstream_data.data.done or upstream_data.data.phase == "done":
443
- debug_log("检测到完成信号,停止收集")
444
- response_completed = True
445
- break
446
-
447
- except Exception as e:
448
- debug_log(f"非流式响应收集异常: {e}")
449
- if not full_content:
450
- # 如果没有收集到任何内容,抛出异常
451
- raise HTTPException(status_code=502, detail=f"Response collection failed: {str(e)}")
452
- else:
453
- debug_log(f"部分内容收集成功,继续处理 ({len(full_content)} 个片段)")
454
-
455
- if not response_completed and not full_content:
456
- debug_log("响应未完成且无内容,可能是连接问题")
457
- raise HTTPException(status_code=502, detail="Incomplete response from upstream")
458
-
459
- final_content = "".join(full_content)
460
- debug_log(f"内容收集完成,最终长度: {len(final_content)}")
461
-
462
- # Handle tool calls for non-streaming
463
- tool_calls = None
464
- finish_reason = "stop"
465
- message_content = final_content
466
-
467
- if self.has_tools:
468
- tool_calls = extract_tool_invocations(final_content)
469
- if tool_calls:
470
- # Content must be null when tool_calls are present (OpenAI spec)
471
- message_content = None
472
- finish_reason = "tool_calls"
473
- debug_log(f"提取到工具调用: {json.dumps(tool_calls, ensure_ascii=False)}")
474
- else:
475
- # Remove tool JSON from content
476
- message_content = remove_tool_json_content(final_content)
477
- if not message_content:
478
- message_content = final_content # 保留原内容如果清理后为空
479
-
480
- # Build response
481
- response_data = OpenAIResponse(
482
- id=f"chatcmpl-{int(time.time())}",
483
- object="chat.completion",
484
- created=int(time.time()),
485
- model=settings.PRIMARY_MODEL,
486
- choices=[Choice(
487
- index=0,
488
- message=Message(
489
- role="assistant",
490
- content=message_content,
491
- tool_calls=tool_calls
492
- ),
493
- finish_reason=finish_reason
494
- )],
495
- usage=Usage()
496
- )
497
-
498
- debug_log("非流式响应发送完成")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  return JSONResponse(content=response_data.model_dump(exclude_none=True))
 
1
+ """
2
+ Response handlers for streaming and non-streaming responses
3
+ """
4
+
5
+ import json
6
+ import time
7
+ from typing import Generator, Optional
8
+ import requests
9
+ from fastapi import HTTPException
10
+ from fastapi.responses import JSONResponse, StreamingResponse
11
+
12
+ from app.core.config import settings
13
+ from app.models.schemas import (
14
+ Message, Delta, Choice, Usage, OpenAIResponse,
15
+ UpstreamRequest, UpstreamData, UpstreamError, ModelItem
16
+ )
17
+ from app.utils.helpers import debug_log, call_upstream_api, transform_thinking_content
18
+ from app.core.token_manager import token_manager
19
+ from app.utils.sse_parser import SSEParser
20
  from app.utils.tools import extract_tool_invocations, remove_tool_json_content
21
+ from app.utils.sse_tool_handler import SSEToolHandler
22
+
23
+
24
+ def create_openai_response_chunk(
25
+ model: str,
26
+ delta: Optional[Delta] = None,
27
+ finish_reason: Optional[str] = None
28
+ ) -> OpenAIResponse:
29
+ """Create OpenAI response chunk for streaming"""
30
+ return OpenAIResponse(
31
+ id=f"chatcmpl-{int(time.time())}",
32
+ object="chat.completion.chunk",
33
+ created=int(time.time()),
34
+ model=model,
35
+ choices=[Choice(
36
+ index=0,
37
+ delta=delta or Delta(),
38
+ finish_reason=finish_reason
39
+ )]
40
+ )
41
+
42
+
43
+ def handle_upstream_error(error: UpstreamError) -> Generator[str, None, None]:
44
+ """Handle upstream error response"""
45
+ debug_log(f"上游错误: code={error.code}, detail={error.detail}")
46
+
47
+ # Send end chunk
48
+ end_chunk = create_openai_response_chunk(
49
+ model=settings.PRIMARY_MODEL,
50
+ finish_reason="stop"
51
+ )
52
+ yield f"data: {end_chunk.model_dump_json()}\n\n"
53
+ yield "data: [DONE]\n\n"
54
+
55
+
56
+ class ResponseHandler:
57
+ """Base class for response handling"""
58
+
59
+ def __init__(self, upstream_req: UpstreamRequest, chat_id: str, auth_token: str):
60
+ self.upstream_req = upstream_req
61
+ self.chat_id = chat_id
62
+ self.auth_token = auth_token
63
+
64
+ def _call_upstream(self) -> requests.Response:
65
+ """Call upstream API with error handling"""
66
+ max_retries = settings.MAX_RETRIES
67
+ retry_count = 0
68
+
69
+ while retry_count < max_retries:
70
+ try:
71
+ debug_log(f"尝试调用上游API (第 {retry_count + 1}/{max_retries} 次)")
72
+ response = call_upstream_api(self.upstream_req, self.chat_id, self.auth_token)
73
+
74
+ # Check if response is successful
75
+ if response.status_code == 200:
76
+ # Mark token as successful
77
+ token_manager.mark_token_success(self.auth_token)
78
+ debug_log("上游API调用成功")
79
+ return response
80
+ elif response.status_code in [401, 403]:
81
+ # Authentication/authorization error - mark token as failed
82
+ debug_log(f"Token认证失败 (状态码: {response.status_code}): {self.auth_token[:20]}...")
83
+ token_manager.mark_token_failed(self.auth_token)
84
+
85
+ # Try to get a new token
86
+ new_token = token_manager.get_next_token()
87
+ if new_token and new_token != self.auth_token:
88
+ debug_log(f"尝试使用新token: {new_token[:20]}...")
89
+ self.auth_token = new_token
90
+ retry_count += 1
91
+ continue
92
+ else:
93
+ debug_log("没有更多可用token")
94
+ return response
95
+ elif response.status_code in [429]:
96
+ # Rate limit - don't mark token as failed, just retry
97
+ debug_log(f"遇到速率限制 (状态码: {response.status_code}),等待后重试")
98
+ if retry_count < max_retries - 1:
99
+ import time
100
+ time.sleep(2 ** retry_count) # 指数退避
101
+ retry_count += 1
102
+ continue
103
+ else:
104
+ return response
105
+ elif response.status_code >= 500:
106
+ # Server error - retry without marking token as failed
107
+ debug_log(f"服务器错误 (状态码: {response.status_code}),稍后重试")
108
+ if retry_count < max_retries - 1:
109
+ import time
110
+ time.sleep(1)
111
+ retry_count += 1
112
+ continue
113
+ else:
114
+ return response
115
+ else:
116
+ # Other client errors, return response as-is
117
+ debug_log(f"客户端错误 (状态码: {response.status_code})")
118
+ return response
119
+
120
+ except Exception as e:
121
+ error_msg = str(e)
122
+ debug_log(f"调用上游失败 (尝试 {retry_count + 1}/{max_retries}): {error_msg}")
123
+
124
+ # 判断是否是连接问题还是token问题
125
+ is_connection_error = any(keyword in error_msg.lower() for keyword in [
126
+ 'connection', 'timeout', 'network', 'dns', 'socket', 'ssl'
127
+ ])
128
+
129
+ if is_connection_error:
130
+ debug_log("检测到网络连接问题,不标记token失败")
131
+ # 网络问题不标记token失败,直接重试
132
+ if retry_count < max_retries - 1:
133
+ import time
134
+ time.sleep(2) # 等待2秒后重试
135
+ retry_count += 1
136
+ continue
137
+ else:
138
+ raise Exception(f"网络连接问题,重试{max_retries}次后仍失败: {error_msg}")
139
+ else:
140
+ # 其他错误可能是token问题,标记失败并尝试新token
141
+ debug_log("检测到可能的token问题,标记token失败")
142
+ token_manager.mark_token_failed(self.auth_token)
143
+
144
+ # Try to get a new token
145
+ new_token = token_manager.get_next_token()
146
+ if new_token and new_token != self.auth_token and retry_count < max_retries - 1:
147
+ debug_log(f"尝试使用新token: {new_token[:20]}...")
148
+ self.auth_token = new_token
149
+ retry_count += 1
150
+ continue
151
+ else:
152
+ raise
153
+
154
+ # If we get here, all retries failed
155
+ raise Exception("所有重试尝试均失败")
156
+
157
+ def _handle_upstream_error(self, response: requests.Response) -> None:
158
+ """Handle upstream error response"""
159
+ debug_log(f"上游返回错误状态: {response.status_code}")
160
+ if settings.DEBUG_LOGGING:
161
+ debug_log(f"上游错误响应: {response.text}")
162
+
163
+
164
+ class StreamResponseHandler(ResponseHandler):
165
+ """Handler for streaming responses"""
166
+
167
+ def __init__(self, upstream_req: UpstreamRequest, chat_id: str, auth_token: str, has_tools: bool = False):
168
+ super().__init__(upstream_req, chat_id, auth_token)
169
+ self.has_tools = has_tools
170
+ self.buffered_content = ""
171
  self.tool_calls = None
172
  # Initialize SSE tool handler for improved tool processing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
+ self.tool_handler = SSEToolHandler(chat_id, settings.PRIMARY_MODEL) if has_tools else None
175
+ # 思考状态跟踪
176
+ self.first_thinking_chunk = True
177
+
178
+ def handle(self) -> Generator[str, None, None]:
179
+ """Handle streaming response"""
180
+ debug_log(f"开始处理流式响应 (chat_id={self.chat_id})")
181
+
182
+ try:
183
+ response = self._call_upstream()
184
+ except Exception:
185
+ yield "data: {\"error\": \"Failed to call upstream\"}\n\n"
186
+ return
187
+
188
+ if response.status_code != 200:
189
+ self._handle_upstream_error(response)
190
+ yield "data: {\"error\": \"Upstream error\"}\n\n"
191
+ return
192
+
193
+ # Send initial role chunk
194
+ first_chunk = create_openai_response_chunk(
195
+ model=settings.PRIMARY_MODEL,
196
+ delta=Delta(role="assistant")
197
+ )
198
+ yield f"data: {first_chunk.model_dump_json()}\n\n"
199
+
200
+ # Process stream
201
+ debug_log("开始读取上游SSE流")
202
+ sent_initial_answer = False
203
+ stream_ended_normally = False
204
+
205
+ try:
206
+ with SSEParser(response, debug_mode=settings.DEBUG_LOGGING) as parser:
207
+ for event in parser.iter_json_data(UpstreamData):
208
+ upstream_data = event['data']
209
+
210
+ # Check for errors
211
+ if self._has_error(upstream_data):
212
+ error = self._get_error(upstream_data)
213
+ yield from handle_upstream_error(error)
214
+ stream_ended_normally = True
215
+ break
216
+
217
+ debug_log(f"解析成功 - 类型: {upstream_data.type}, 阶段: {upstream_data.data.phase}, "
218
+ f"内容长度: {len(upstream_data.data.delta_content or '')}, 完成: {upstream_data.data.done}")
219
+
220
+ # Process content
221
+ yield from self._process_content_with_tools(upstream_data, sent_initial_answer)
222
+
223
+ # Update sent_initial_answer flag if we sent content
224
+ if not sent_initial_answer and (upstream_data.data.delta_content or upstream_data.data.edit_content):
225
+ sent_initial_answer = True
226
+
227
+ # Check if done
228
+ if upstream_data.data.done or upstream_data.data.phase == "done":
229
+ debug_log("检测到流结束信号")
230
+ yield from self._send_end_chunk()
231
+ stream_ended_normally = True
232
+ break
233
+
234
+ except Exception as e:
235
+ debug_log(f"SSE流处理异常: {e}")
236
+ # 流异常结束,发送错误响应
237
+ if not stream_ended_normally:
238
+ error_chunk = create_openai_response_chunk(
239
+ model=settings.PRIMARY_MODEL,
240
+ delta=Delta(content=f"\n\n[系统提示: 连接中断,响应可能不完整]")
241
+ )
242
+ yield f"data: {error_chunk.model_dump_json()}\n\n"
243
+
244
+ # 确保流正常结束
245
+ if not stream_ended_normally:
246
+ debug_log("流未正常结束,发送结束信号")
247
+ yield from self._send_end_chunk(force_stop=True)
248
+
249
+ def _has_error(self, upstream_data: UpstreamData) -> bool:
250
+ """Check if upstream data contains error"""
251
+ return bool(
252
+ upstream_data.error or
253
+ upstream_data.data.error or
254
+ (upstream_data.data.inner and upstream_data.data.inner.error)
255
+ )
256
+
257
+ def _get_error(self, upstream_data: UpstreamData) -> UpstreamError:
258
+ """Get error from upstream data"""
259
+ return (
260
+ upstream_data.error or
261
+ upstream_data.data.error or
262
+ (upstream_data.data.inner.error if upstream_data.data.inner else None)
263
+ )
264
+
265
+ def _process_content(
266
+ self,
267
+ upstream_data: UpstreamData,
268
+ sent_initial_answer: bool
269
+ ) -> Generator[str, None, None]:
270
+ """Process content from upstream data"""
271
+ content = upstream_data.data.delta_content or upstream_data.data.edit_content
272
+
273
+ if not content:
274
+ return
275
+
276
+ # Transform thinking content
277
+ if upstream_data.data.phase == "thinking":
278
+ content = transform_thinking_content(content)
279
+
280
+ # Buffer content if tools are enabled
281
+ if self.has_tools:
282
+ self.buffered_content += content
283
+ else:
284
+ # Handle initial answer content
285
+ if (not sent_initial_answer and
286
+ upstream_data.data.edit_content and
287
+ upstream_data.data.phase == "answer"):
288
+
289
+ content = self._extract_edit_content(upstream_data.data.edit_content)
290
+ if content:
291
+ debug_log(f"发送普通内容: {content}")
292
+ chunk = create_openai_response_chunk(
293
+ model=settings.PRIMARY_MODEL,
294
+ delta=Delta(content=content)
295
+ )
296
+ yield f"data: {chunk.model_dump_json()}\n\n"
297
+ sent_initial_answer = True
298
+
299
+ # Handle delta content
300
+ if upstream_data.data.delta_content:
301
+ if content:
302
+ if upstream_data.data.phase == "thinking":
303
+ # 第一个思考块添加<think>开始标签,其他块保持纯内容
304
+ if self.first_thinking_chunk:
305
+ formatted_content = f"<think>{content}"
306
+ self.first_thinking_chunk = False
307
+ else:
308
+ formatted_content = content
309
+
310
+ debug_log(f"发送思考内容: {content}")
311
+ chunk = create_openai_response_chunk(
312
+ model=settings.PRIMARY_MODEL,
313
+ delta=Delta(content=formatted_content)
314
+ )
315
+ else:
316
+ # 如果从thinking阶段转到其他阶段,需要结束thinking标签
317
+ if not self.first_thinking_chunk and upstream_data.data.phase == "answer":
318
+ # 先发送思考结束标签
319
+ thinking_end_chunk = create_openai_response_chunk(
320
+ model=settings.PRIMARY_MODEL,
321
+ delta=Delta(content="</think>")
322
+ )
323
+ yield f"data: {thinking_end_chunk.model_dump_json()}\n\n"
324
+ # 重置状态
325
+ self.first_thinking_chunk = True
326
+
327
+ debug_log(f"发送普通内容: {content}")
328
+ chunk = create_openai_response_chunk(
329
+ model=settings.PRIMARY_MODEL,
330
+ delta=Delta(content=content)
331
+ )
332
+ yield f"data: {chunk.model_dump_json()}\n\n"
333
+
334
+ def _extract_edit_content(self, edit_content: str) -> str:
335
+ """Extract content from edit_content field"""
336
+ parts = edit_content.split("</details>")
337
+ return parts[1] if len(parts) > 1 else ""
338
+
339
+ def _send_end_chunk(self, force_stop: bool = False) -> Generator[str, None, None]:
340
+ """Send end chunk and DONE signal"""
341
+ finish_reason = "stop"
342
+
343
+ if self.has_tools and not force_stop:
344
+ # Try to extract tool calls from buffered content
345
+ self.tool_calls = extract_tool_invocations(self.buffered_content)
346
+
347
+ if self.tool_calls:
348
+ debug_log(f"检测到工具调用: {len(self.tool_calls)} 个")
349
+ # Send tool calls with proper format
350
+ for i, tc in enumerate(self.tool_calls):
351
+ tool_call_delta = {
352
+ "index": i,
353
+ "id": tc.get("id"),
354
+ "type": tc.get("type", "function"),
355
+ "function": tc.get("function", {}),
356
+ }
357
+
358
+ out_chunk = create_openai_response_chunk(
359
+ model=settings.PRIMARY_MODEL,
360
+ delta=Delta(tool_calls=[tool_call_delta])
361
+ )
362
+ yield f"data: {out_chunk.model_dump_json()}\n\n"
363
+
364
+ finish_reason = "tool_calls"
365
+ else:
366
+ # Send regular content
367
+ trimmed_content = remove_tool_json_content(self.buffered_content)
368
+ if trimmed_content:
369
+ debug_log(f"发送常规内容: {len(trimmed_content)} 字符")
370
+ content_chunk = create_openai_response_chunk(
371
+ model=settings.PRIMARY_MODEL,
372
+ delta=Delta(content=trimmed_content)
373
+ )
374
+ yield f"data: {content_chunk.model_dump_json()}\n\n"
375
+ elif force_stop:
376
+ # 强制结束时,发送缓冲的内容(如果有)
377
+ if self.buffered_content:
378
+ debug_log(f"强制结束,发送缓冲内容: {len(self.buffered_content)} 字符")
379
+ content_chunk = create_openai_response_chunk(
380
+ model=settings.PRIMARY_MODEL,
381
+ delta=Delta(content=self.buffered_content)
382
+ )
383
+ yield f"data: {content_chunk.model_dump_json()}\n\n"
384
+
385
+ # Send final chunk
386
+ end_chunk = create_openai_response_chunk(
387
+ model=settings.PRIMARY_MODEL,
388
+ finish_reason=finish_reason
389
+ )
390
+ yield f"data: {end_chunk.model_dump_json()}\n\n"
391
+ yield "data: [DONE]\n\n"
392
+ debug_log(f"流式响应完成 (finish_reason: {finish_reason})")
393
+
394
+
395
+
396
  def _process_content_with_tools(
397
  self,
398
  upstream_data: UpstreamData,
 
420
  yield from self._process_content(upstream_data, sent_initial_answer)
421
 
422
 
423
+ class NonStreamResponseHandler(ResponseHandler):
424
+ """Handler for non-streaming responses"""
425
+
426
+ def __init__(self, upstream_req: UpstreamRequest, chat_id: str, auth_token: str, has_tools: bool = False):
427
+ super().__init__(upstream_req, chat_id, auth_token)
428
+ self.has_tools = has_tools
429
+ # 思考状态跟踪
430
+ self.first_thinking_chunk = True
431
+ self.in_thinking_phase = False
432
+
433
+ def handle(self) -> JSONResponse:
434
+ """Handle non-streaming response"""
435
+ debug_log(f"开始处理非流式响应 (chat_id={self.chat_id})")
436
+
437
+ try:
438
+ response = self._call_upstream()
439
+ except Exception as e:
440
+ debug_log(f"调用上游失败: {e}")
441
+ raise HTTPException(status_code=502, detail="Failed to call upstream")
442
+
443
+ if response.status_code != 200:
444
+ self._handle_upstream_error(response)
445
+ raise HTTPException(status_code=502, detail="Upstream error")
446
+
447
+ # Collect full response
448
+ full_content = []
449
+ debug_log("开始收集完整响应内容")
450
+ response_completed = False
451
+
452
+ try:
453
+ with SSEParser(response, debug_mode=settings.DEBUG_LOGGING) as parser:
454
+ for event in parser.iter_json_data(UpstreamData):
455
+ upstream_data = event['data']
456
+
457
+ if upstream_data.data.delta_content:
458
+ content = upstream_data.data.delta_content
459
+
460
+ if upstream_data.data.phase == "thinking":
461
+ content = transform_thinking_content(content)
462
+
463
+ # 处理思考内容的分块格式
464
+ if not self.in_thinking_phase:
465
+ # 进入思考阶段,添加开始标签
466
+ self.in_thinking_phase = True
467
+ if self.first_thinking_chunk:
468
+ content = f"<think>{content}"
469
+ self.first_thinking_chunk = False
470
+ else:
471
+ content = f"<think>{content}"
472
+ # 如果已经在思考阶段,保持纯内容
473
+ else:
474
+ # 如果从thinking阶段转到其他阶段
475
+ if self.in_thinking_phase:
476
+ # 添加结束标签到前一个内容
477
+ if full_content and not self.first_thinking_chunk:
478
+ full_content.append("</think>")
479
+ self.in_thinking_phase = False
480
+ self.first_thinking_chunk = True
481
+
482
+ if content:
483
+ full_content.append(content)
484
+
485
+ if upstream_data.data.done or upstream_data.data.phase == "done":
486
+ debug_log("检测到完成信号,停止收集")
487
+ response_completed = True
488
+ break
489
+
490
+ except Exception as e:
491
+ debug_log(f"非流式响应收集异常: {e}")
492
+ if not full_content:
493
+ # 如果没有收集到任何内容,抛出异常
494
+ raise HTTPException(status_code=502, detail=f"Response collection failed: {str(e)}")
495
+ else:
496
+ debug_log(f"部分内容收集成功,继续处理 ({len(full_content)} 个片段)")
497
+
498
+ if not response_completed and not full_content:
499
+ debug_log("响应未完成且无内容,可能是连接问题")
500
+ raise HTTPException(status_code=502, detail="Incomplete response from upstream")
501
+
502
+ # 如果响应结束时还在思考阶段,需要添加结束标签
503
+ if self.in_thinking_phase and not self.first_thinking_chunk:
504
+ full_content.append("</think>")
505
+
506
+ final_content = "".join(full_content)
507
+ debug_log(f"内容收集完成,最终长度: {len(final_content)}")
508
+
509
+ # Handle tool calls for non-streaming
510
+ tool_calls = None
511
+ finish_reason = "stop"
512
+ message_content = final_content
513
+
514
+ if self.has_tools:
515
+ tool_calls = extract_tool_invocations(final_content)
516
+ if tool_calls:
517
+ # Content must be null when tool_calls are present (OpenAI spec)
518
+ message_content = None
519
+ finish_reason = "tool_calls"
520
+ debug_log(f"提取到工具调用: {json.dumps(tool_calls, ensure_ascii=False)}")
521
+ else:
522
+ # Remove tool JSON from content
523
+ message_content = remove_tool_json_content(final_content)
524
+ if not message_content:
525
+ message_content = final_content # 保留原内容如果清理后为空
526
+
527
+ # Build response
528
+ response_data = OpenAIResponse(
529
+ id=f"chatcmpl-{int(time.time())}",
530
+ object="chat.completion",
531
+ created=int(time.time()),
532
+ model=settings.PRIMARY_MODEL,
533
+ choices=[Choice(
534
+ index=0,
535
+ message=Message(
536
+ role="assistant",
537
+ content=message_content,
538
+ tool_calls=tool_calls
539
+ ),
540
+ finish_reason=finish_reason
541
+ )],
542
+ usage=Usage()
543
+ )
544
+
545
+ debug_log("非流式响应发送完成")
546
  return JSONResponse(content=response_data.model_dump(exclude_none=True))
app/core/zai_transformer.py CHANGED
@@ -5,6 +5,9 @@ import json
5
  import time
6
  import uuid
7
  import random
 
 
 
8
  from datetime import datetime
9
  from typing import Dict, List, Any, Optional, Generator, AsyncGenerator
10
  import httpx
@@ -27,31 +30,31 @@ def get_user_agent_instance() -> UserAgent:
27
  return _user_agent_instance
28
 
29
 
30
- def get_dynamic_headers(chat_id: str = "") -> Dict[str, str]:
31
  """生成动态浏览器headers,包含随机User-Agent"""
32
- ua = get_user_agent_instance()
33
-
34
- # 随机选择浏览器类型,偏向Chrome和Edge
35
- browser_choices = ["chrome", "chrome", "chrome", "edge", "edge", "firefox", "safari"]
36
- browser_type = random.choice(browser_choices)
37
-
38
- try:
39
- if browser_type == "chrome":
40
- user_agent = ua.chrome
41
- elif browser_type == "edge":
42
- user_agent = ua.edge
43
- elif browser_type == "firefox":
44
- user_agent = ua.firefox
45
- elif browser_type == "safari":
46
- user_agent = ua.safari
47
- else:
 
 
48
  user_agent = ua.random
49
- except:
50
- user_agent = ua.random
51
 
52
  # 提取版本信息
53
- chrome_version = "139"
54
- edge_version = "139"
55
 
56
  if "Chrome/" in user_agent:
57
  try:
@@ -62,27 +65,32 @@ def get_dynamic_headers(chat_id: str = "") -> Dict[str, str]:
62
  if "Edg/" in user_agent:
63
  try:
64
  edge_version = user_agent.split("Edg/")[1].split(".")[0]
65
- sec_ch_ua = f'"Microsoft Edge";v="{edge_version}", "Chromium";v="{chrome_version}", "Not_A Brand";v="24"'
66
  except:
67
- sec_ch_ua = f'"Not_A Brand";v="8", "Chromium";v="{chrome_version}", "Google Chrome";v="{chrome_version}"'
68
  elif "Firefox/" in user_agent:
69
  sec_ch_ua = None # Firefox不使用sec-ch-ua
70
  else:
71
- sec_ch_ua = f'"Not_A Brand";v="8", "Chromium";v="{chrome_version}", "Google Chrome";v="{chrome_version}"'
72
 
73
  headers = {
 
 
 
74
  "Content-Type": "application/json",
75
- "Accept": "application/json, text/event-stream",
76
  "User-Agent": user_agent,
77
- "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
78
- "X-FE-Version": "prod-fe-1.0.79",
79
  "Origin": "https://chat.z.ai",
 
 
 
 
80
  }
81
 
82
  if sec_ch_ua:
83
- headers["sec-ch-ua"] = sec_ch_ua
84
- headers["sec-ch-ua-mobile"] = "?0"
85
- headers["sec-ch-ua-platform"] = '"Windows"'
86
 
87
  if chat_id:
88
  headers["Referer"] = f"https://chat.z.ai/c/{chat_id}"
@@ -97,6 +105,114 @@ def generate_uuid() -> str:
97
  return str(uuid.uuid4())
98
 
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  def get_auth_token_sync() -> str:
101
  """同步获取认证令牌(用于非异步场景)"""
102
  if settings.ANONYMOUS_MODE:
@@ -303,21 +419,37 @@ class ZAITransformer:
303
  else:
304
  body["tools"] = None
305
 
 
 
 
 
306
  # 构建请求配置
307
- dynamic_headers = get_dynamic_headers(chat_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
 
309
  config = {
310
- "url": self.api_url, # 使用原始URL
311
- "headers": {
312
- **dynamic_headers, # 使用动态生成的headers
313
- "Authorization": f"Bearer {token}",
314
- "Cache-Control": "no-cache",
315
- "Connection": "keep-alive",
316
- "Pragma": "no-cache",
317
- "Sec-Fetch-Dest": "empty",
318
- "Sec-Fetch-Mode": "cors",
319
- "Sec-Fetch-Site": "same-origin",
320
- },
321
  }
322
 
323
  debug_log("✅ 请求转换完成")
 
5
  import time
6
  import uuid
7
  import random
8
+ import hashlib
9
+ import hmac
10
+ import urllib.parse
11
  from datetime import datetime
12
  from typing import Dict, List, Any, Optional, Generator, AsyncGenerator
13
  import httpx
 
30
  return _user_agent_instance
31
 
32
 
33
+ def get_dynamic_headers(chat_id: str = "", user_agent: str = "") -> Dict[str, str]:
34
  """生成动态浏览器headers,包含随机User-Agent"""
35
+ if not user_agent:
36
+ ua = get_user_agent_instance()
37
+ # 随机选择浏览器类型,偏向Chrome和Edge
38
+ browser_choices = ["chrome", "chrome", "chrome", "edge", "edge", "firefox", "safari"]
39
+ browser_type = random.choice(browser_choices)
40
+
41
+ try:
42
+ if browser_type == "chrome":
43
+ user_agent = ua.chrome
44
+ elif browser_type == "edge":
45
+ user_agent = ua.edge
46
+ elif browser_type == "firefox":
47
+ user_agent = ua.firefox
48
+ elif browser_type == "safari":
49
+ user_agent = ua.safari
50
+ else:
51
+ user_agent = ua.random
52
+ except:
53
  user_agent = ua.random
 
 
54
 
55
  # 提取版本信息
56
+ chrome_version = "140" # 更新版本号匹配F12信息
57
+ edge_version = "140"
58
 
59
  if "Chrome/" in user_agent:
60
  try:
 
65
  if "Edg/" in user_agent:
66
  try:
67
  edge_version = user_agent.split("Edg/")[1].split(".")[0]
68
+ sec_ch_ua = f'"Microsoft Edge";v="{edge_version}", "Chromium";v="{chrome_version}", "Not=A?Brand";v="24"'
69
  except:
70
+ sec_ch_ua = f'"Chromium";v="{chrome_version}", "Not=A?Brand";v="24", "Microsoft Edge";v="{edge_version}"'
71
  elif "Firefox/" in user_agent:
72
  sec_ch_ua = None # Firefox不使用sec-ch-ua
73
  else:
74
+ sec_ch_ua = f'"Chromium";v="{chrome_version}", "Not=A?Brand";v="24", "Google Chrome";v="{chrome_version}"'
75
 
76
  headers = {
77
+ "Accept": "*/*",
78
+ "Accept-Encoding": "gzip, deflate, br, zstd",
79
+ "Accept-Language": "zh-CN",
80
  "Content-Type": "application/json",
 
81
  "User-Agent": user_agent,
82
+ "X-Fe-Version": "prod-fe-1.0.83", # 匹配F12信息中的版本
 
83
  "Origin": "https://chat.z.ai",
84
+ "Connection": "keep-alive",
85
+ "Sec-Fetch-Dest": "empty",
86
+ "Sec-Fetch-Mode": "cors",
87
+ "Sec-Fetch-Site": "same-origin",
88
  }
89
 
90
  if sec_ch_ua:
91
+ headers["Sec-Ch-Ua"] = sec_ch_ua
92
+ headers["Sec-Ch-Ua-Mobile"] = "?0"
93
+ headers["Sec-Ch-Ua-Platform"] = '"Windows"'
94
 
95
  if chat_id:
96
  headers["Referer"] = f"https://chat.z.ai/c/{chat_id}"
 
105
  return str(uuid.uuid4())
106
 
107
 
108
+ def generate_signature(data: str, timestamp: str, secret_key: str = "") -> str:
109
+ """生成请求签名
110
+
111
+ Args:
112
+ data: 请求数据
113
+ timestamp: 时间戳
114
+ secret_key: 密钥(使用配置中的值)
115
+
116
+ Returns:
117
+ 签名字符串
118
+ """
119
+ if not settings.ENABLE_SIGNATURE:
120
+ return "" # 如果禁用签名,返回空字符串
121
+
122
+ if not secret_key:
123
+ secret_key = settings.SIGNATURE_SECRET_KEY
124
+
125
+ # 构建签名字符串
126
+ sign_string = f"{data}{timestamp}{secret_key}"
127
+
128
+ # 根据配置选择签名算法
129
+ if settings.SIGNATURE_ALGORITHM.lower() == "md5":
130
+ signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
131
+ elif settings.SIGNATURE_ALGORITHM.lower() == "sha1":
132
+ signature = hashlib.sha1(sign_string.encode('utf-8')).hexdigest()
133
+ else: # 默认使用sha256
134
+ signature = hashlib.sha256(sign_string.encode('utf-8')).hexdigest()
135
+
136
+ return signature
137
+
138
+
139
+ def build_query_params(
140
+ timestamp: int,
141
+ request_id: str,
142
+ token: str,
143
+ user_agent: str,
144
+ chat_id: str = ""
145
+ ) -> Dict[str, str]:
146
+ """构建查询参数,模拟真实的浏览器请求
147
+
148
+ Args:
149
+ timestamp: 时间戳(毫秒)
150
+ request_id: 请求ID
151
+ token: 用户token
152
+ user_agent: 用户代理字符串
153
+ chat_id: 聊天ID
154
+
155
+ Returns:
156
+ 查询参数字典
157
+ """
158
+ # 生成用户ID(从token中提取或生成假的)
159
+ user_id = "guest-user-" + str(abs(hash(token)) % 1000000)
160
+
161
+ # 编码用户代理
162
+ encoded_user_agent = urllib.parse.quote_plus(user_agent)
163
+
164
+ # 当前时间相关
165
+ current_time = datetime.now()
166
+ local_time = current_time.isoformat() + "Z"
167
+ utc_time = current_time.strftime("%a, %d %b %Y %H:%M:%S GMT")
168
+
169
+ # 构建当前URL
170
+ current_url = f"https://chat.z.ai/c/{chat_id}" if chat_id else "https://chat.z.ai/"
171
+ pathname = f"/c/{chat_id}" if chat_id else "/"
172
+
173
+ query_params = {
174
+ "timestamp": str(timestamp),
175
+ "requestId": request_id,
176
+ "version": "0.0.1",
177
+ "platform": "web",
178
+ "user_id": user_id,
179
+ "token": token,
180
+ "user_agent": encoded_user_agent,
181
+ "language": "zh-CN",
182
+ "languages": "zh-CN,en,en-GB,en-US",
183
+ "timezone": "Asia/Shanghai",
184
+ "cookie_enabled": "true",
185
+ "screen_width": "1536",
186
+ "screen_height": "864",
187
+ "screen_resolution": "1536x864",
188
+ "viewport_height": "331",
189
+ "viewport_width": "1528",
190
+ "viewport_size": "1528x331",
191
+ "color_depth": "24",
192
+ "pixel_ratio": "1.25",
193
+ "current_url": urllib.parse.quote_plus(current_url),
194
+ "pathname": pathname,
195
+ "search": "",
196
+ "hash": "",
197
+ "host": "chat.z.ai",
198
+ "hostname": "chat.z.ai",
199
+ "protocol": "https:",
200
+ "referrer": "",
201
+ "title": "Chat with Z.ai - Free AI Chatbot powered by GLM-4.5",
202
+ "timezone_offset": "-480",
203
+ "local_time": local_time,
204
+ "utc_time": utc_time,
205
+ "is_mobile": "false",
206
+ "is_touch": "false",
207
+ "max_touch_points": "10",
208
+ "browser_name": "Chrome",
209
+ "os_name": "Windows",
210
+ # "signature_timestamp": str(timestamp), # 已移除签名相关参数
211
+ }
212
+
213
+ return query_params
214
+
215
+
216
  def get_auth_token_sync() -> str:
217
  """同步获取认证令牌(用于非异步场景)"""
218
  if settings.ANONYMOUS_MODE:
 
419
  else:
420
  body["tools"] = None
421
 
422
+ # 生成时间戳和请求ID
423
+ timestamp = int(time.time() * 1000) # 毫秒时间戳
424
+ request_id = generate_uuid()
425
+
426
  # 构建请求配置
427
+ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0"
428
+ dynamic_headers = get_dynamic_headers(chat_id, user_agent)
429
+
430
+ # 构建查询参数
431
+ query_params = build_query_params(timestamp, request_id, token, user_agent, chat_id)
432
+
433
+ # 签名已强制禁用 - 不生成任何签名
434
+ # request_body_str = json.dumps(body, ensure_ascii=False, separators=(',', ':'))
435
+ # signature = generate_signature(request_body_str, str(timestamp))
436
+
437
+ # 构建完整的URL(包含查询参数)
438
+ url_with_params = f"{self.api_url}?" + "&".join([f"{k}={v}" for k, v in query_params.items()])
439
+
440
+ headers = {
441
+ **dynamic_headers, # 使用动态生成的headers
442
+ "Authorization": f"Bearer {token}",
443
+ "Cache-Control": "no-cache",
444
+ "Pragma": "no-cache",
445
+ }
446
+
447
+ # 签名功能已禁用
448
+ debug_log(" 🔓 签名验证已禁用")
449
 
450
  config = {
451
+ "url": url_with_params,
452
+ "headers": headers,
 
 
 
 
 
 
 
 
 
453
  }
454
 
455
  debug_log("✅ 请求转换完成")
docker-compose.yml CHANGED
@@ -1,5 +1,3 @@
1
- version: '3.8'
2
-
3
  services:
4
  z-ai2api:
5
  image: julienol/z-ai2api-python:latest
 
 
 
1
  services:
2
  z-ai2api:
3
  image: julienol/z-ai2api-python:latest