sanbo110 commited on
Commit
fdc7f56
·
1 Parent(s): 165e309

update sth at 2025-10-23 17:38:54

Browse files
deploy/.dockerignore → .dockerignore RENAMED
File without changes
.env CHANGED
@@ -1,54 +1,16 @@
1
- # 代理服务配置文件示例
2
- # 复制此文件为 .env 并根据需要修改配置值
3
 
4
- # ========== API 基础配置 ==========
5
- # 客户端认证密钥(您自定义的 API 密钥,用于客户端访问本服务)
6
- AUTH_TOKEN=sk-your-api-key
7
-
8
-
9
- # 跳过客户端认证(仅开发环境使用)
10
- SKIP_AUTH_TOKEN=false
11
-
12
- # ========== Z.ai Token池配置 ==========
13
- # Token失败阈值(失败多少次后标记为不可用)
14
- TOKEN_FAILURE_THRESHOLD=3
15
-
16
- # Token恢复超时时间(秒,失败token在此时间后重新尝试)
17
- TOKEN_RECOVERY_TIMEOUT=1800
18
 
19
  # Z.AI 匿名用户模式
20
  # false: 使用认证 Token 令牌,失败时自动降级为匿名请求
21
  # true: 自动从 Z.ai 获取临时访问令牌,避免对话历史共享
22
  ANONYMOUS_MODE=true
23
 
24
- # ========== LongCat 配置 ==========
25
- # LongCat token(单个token)
26
- # LONGCAT_TOKEN=your_passport_token_here
27
-
28
- # ========== 服务器配置 ==========
29
  # 服务监听端口
30
  LISTEN_PORT=7860
31
 
32
- # 服务名称
33
- SERVICE_NAME=z-ai2api-server
34
-
35
  # 调试日志
36
- DEBUG_LOGGING=false
37
-
38
- # Nginx 反向代理路径前缀(可选,用于在子路径下部署)
39
- # 例如:ROOT_PATH=/ai2api 则服务部署在 http://domain.com/ai2api
40
- # 留空表示部署在根路径
41
- ROOT_PATH=/api
42
-
43
- # Function Call 功能开关
44
- TOOL_SUPPORT=true
45
-
46
- # 工具调用扫描限制(字符数)
47
- SCAN_LIMIT=200000
48
-
49
- # ========== 管理后台认证 ==========
50
- # 管理后台登录密码(建议修改为复杂密码)
51
- ADMIN_PASSWORD=admin123
52
-
53
- # Session 密钥(用于加密会话,建议生成随机字符串)
54
- SESSION_SECRET_KEY=your-secret-key-change-in-production
 
1
+ # 代理服务配置文件
2
+ # 匿名模式配置
3
 
4
+ # 跳过客户端认证(启用匿名访问)
5
+ SKIP_AUTH_TOKEN=true
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  # Z.AI 匿名用户模式
8
  # false: 使用认证 Token 令牌,失败时自动降级为匿名请求
9
  # true: 自动从 Z.ai 获取临时访问令牌,避免对话历史共享
10
  ANONYMOUS_MODE=true
11
 
 
 
 
 
 
12
  # 服务监听端口
13
  LISTEN_PORT=7860
14
 
 
 
 
15
  # 调试日志
16
+ DEBUG_LOGGING=true
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.env.example CHANGED
@@ -1,53 +1,35 @@
1
- # 代理服务配置文件示例
2
- # 复制此文件为 .env 并根据需要修改配置值
 
3
 
4
- # ========== API 基础配置 ==========
5
- # 客户端认证密钥(您自定义的 API 密钥,用于客户端访问本服务)
6
- AUTH_TOKEN=sk-your-api-key
7
-
8
- # 跳过客户端认证(仅开发环境使用)
9
- SKIP_AUTH_TOKEN=false
10
-
11
- # ========== Z.ai Token池配置 ==========
12
- # Token失败阈值(失败多少次后标记为不可用)
13
- TOKEN_FAILURE_THRESHOLD=3
14
-
15
- # Token恢复超时时间(秒,失败token在此时间后重新尝试)
16
- TOKEN_RECOVERY_TIMEOUT=1800
17
-
18
- # Z.AI 匿名用户模式
19
- # false: 使用认证 Token 令牌,失败时自动降级为匿名请求
20
- # true: 自动从 Z.ai 获取临时访问令牌,避免对话历史共享
21
- ANONYMOUS_MODE=true
22
-
23
- # ========== LongCat 配置 ==========
24
- # LongCat token(单个token)
25
- # LONGCAT_TOKEN=your_passport_token_here
26
 
27
- # ========== 服务器配置 ==========
28
- # 服务监听端口
29
- LISTEN_PORT=7860
30
 
31
- # 服务名称
32
- SERVICE_NAME=z-ai2api-server
33
 
34
- # 调试日志
35
- DEBUG_LOGGING=false
36
 
37
- # Nginx 反向代理路径前缀(可选,用于在子路径下部署)
38
- # 例如:ROOT_PATH=/ai2api 则服务部署在 http://domain.com/ai2api
39
- # 留空表示部署在根路径
40
- ROOT_PATH=
41
 
42
- # Function Call 功能开关
43
  TOOL_SUPPORT=true
44
 
45
- # 工具调用扫描限制(字符数)
46
  SCAN_LIMIT=200000
47
 
48
- # ========== 管理后台认证 ==========
49
- # 管理后台登录密码(建议修改为复杂密码)
50
- ADMIN_PASSWORD=admin123
51
 
52
- # Session 密钥(用于加密会话,建议生成随机字符串)
53
- SESSION_SECRET_KEY=your-secret-key-change-in-production
 
 
 
 
 
 
1
+ # ==============================================
2
+ # Z.AI API Server - Docker 环境变量配置示例
3
+ # ==============================================
4
 
5
+ # 管理后台密码
6
+ ADMIN_PASSWORD=admin123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
+ # API 认证密钥 (用于验证客户端请求)
9
+ AUTH_TOKEN=sk-your-api-key-here
 
10
 
11
+ # 是否跳过 API Key 验证 (开发环境可设为 true)
12
+ SKIP_AUTH_TOKEN=false
13
 
14
+ # 调试日志 (生产环境建议设为 false)
15
+ DEBUG_LOGGING=true
16
 
17
+ # 匿名模式 (允许无 token 访问,需要配合 SKIP_AUTH_TOKEN=true)
18
+ ANONYMOUS_MODE=false
 
 
19
 
20
+ # Function Call 功能开关 (是否支持工具调用)
21
  TOOL_SUPPORT=true
22
 
23
+ # 工具调用扫描限制 (字符数)
24
  SCAN_LIMIT=200000
25
 
26
+ # 数据库路径 (Docker 环境使用持久化卷)
27
+ DB_PATH=/app/data/tokens.db
 
28
 
29
+ # Token 池配置
30
+ TOKEN_FAILURE_THRESHOLD=3
31
+ TOKEN_RECOVERY_TIMEOUT=300
32
+
33
+ # 服务配置
34
+ SERVICE_NAME=Z.AI_API_Server
35
+ LISTEN_PORT=7860
Dockerfile CHANGED
@@ -10,20 +10,22 @@ RUN mkdir -p /app/data /app/logs && \
10
  # Install dependencies
11
  COPY requirements.txt .
12
  RUN pip install --no-cache-dir -r requirements.txt
13
- RUN pip install --upgrade pip
14
  # Copy application code
15
  COPY . .
16
 
17
  # Set environment variable for database path
18
  ENV DB_PATH=/app/data/tokens.db
19
- ENV AUTH_TOKEN=sk-your-key
20
- ENV ROOT_PATH=/api
21
  ENV ANONYMOUS_MODE=true
 
22
  ENV LISTEN_PORT=7860
23
- ENV TOOL_SUPPORT=true
 
24
 
25
  # Expose port
26
  EXPOSE 7860
27
 
 
28
  # Run the application
29
  CMD ["python", "main.py"]
 
10
  # Install dependencies
11
  COPY requirements.txt .
12
  RUN pip install --no-cache-dir -r requirements.txt
13
+
14
  # Copy application code
15
  COPY . .
16
 
17
  # Set environment variable for database path
18
  ENV DB_PATH=/app/data/tokens.db
19
+ ENV SKIP_AUTH_TOKEN=true
 
20
  ENV ANONYMOUS_MODE=true
21
+ # 服务监听端口
22
  ENV LISTEN_PORT=7860
23
+ # 调试日志
24
+ ENV DEBUG_LOGGING=true
25
 
26
  # Expose port
27
  EXPOSE 7860
28
 
29
+
30
  # Run the application
31
  CMD ["python", "main.py"]
deploy/NGINX_SETUP.md → NGINX_SETUP.md RENAMED
@@ -37,7 +37,7 @@ server {
37
 
38
  location /ai2api {
39
  # 代理到后端服务
40
- proxy_pass http://127.0.0.1:8080;
41
 
42
  # 传递原始请求信息
43
  proxy_set_header Host $host;
@@ -257,9 +257,9 @@ server {
257
 
258
  ```nginx
259
  upstream ai2api_backend {
260
- server 127.0.0.1:7860;
261
- server 127.0.0.1:7861;
262
- server 127.0.0.1:7862;
263
  }
264
 
265
  server {
 
37
 
38
  location /ai2api {
39
  # 代理到后端服务
40
+ proxy_pass http://127.0.0.1:7860;
41
 
42
  # 传递原始请求信息
43
  proxy_set_header Host $host;
 
257
 
258
  ```nginx
259
  upstream ai2api_backend {
260
+ server 127.0.0.1:7860;
261
+ server 127.0.0.1:8081;
262
+ server 127.0.0.1:8082;
263
  }
264
 
265
  server {
deploy/README_DOCKER.md → README_DOCKER.md RENAMED
@@ -225,7 +225,7 @@ docker compose config
225
  如端口 7860 被占用,修改 `docker-compose.yml`:
226
  ```yaml
227
  ports:
228
- - "7861:7860" # 映射到宿主机 7861 端口
229
  ```
230
 
231
  ### 健康检查失败
 
225
  如端口 7860 被占用,修改 `docker-compose.yml`:
226
  ```yaml
227
  ports:
228
+ - "8081:7860" # 映射到宿主机 8081 端口
229
  ```
230
 
231
  ### 健康检查失败
app/core/config.py CHANGED
@@ -70,23 +70,15 @@ class Settings(BaseSettings):
70
  # Provider Configuration
71
  DEFAULT_PROVIDER: str = os.getenv("DEFAULT_PROVIDER", "zai") # 默认提供商:zai/k2think/longcat
72
 
 
 
 
 
 
73
  # Admin Panel Authentication
74
  ADMIN_PASSWORD: str = os.getenv("ADMIN_PASSWORD", "admin123") # 管理后台密码
75
  SESSION_SECRET_KEY: str = os.getenv("SESSION_SECRET_KEY", "your-secret-key-change-in-production") # Session 密钥
76
 
77
- # Browser Headers
78
- CLIENT_HEADERS: Dict[str, str] = {
79
- "Content-Type": "application/json",
80
- "Accept": "application/json, text/event-stream",
81
- "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",
82
- "Accept-Language": "zh-CN",
83
- "sec-ch-ua": '"Not;A=Brand";v="99", "Microsoft Edge";v="139", "Chromium";v="139"',
84
- "sec-ch-ua-mobile": "?0",
85
- "sec-ch-ua-platform": '"Windows"',
86
- "X-FE-Version": "prod-fe-1.0.98",
87
- "Origin": "https://chat.z.ai",
88
- }
89
-
90
  class Config:
91
  env_file = ".env"
92
  extra = "ignore" # 忽略额外字段,防止环境变量中的未知字段导致验证错误
 
70
  # Provider Configuration
71
  DEFAULT_PROVIDER: str = os.getenv("DEFAULT_PROVIDER", "zai") # 默认提供商:zai/k2think/longcat
72
 
73
+ # Proxy Configuration
74
+ HTTP_PROXY: Optional[str] = os.getenv("HTTP_PROXY") # HTTP代理,格式: http://user:pass@host:port 或 http://host:port
75
+ HTTPS_PROXY: Optional[str] = os.getenv("HTTPS_PROXY") # HTTPS代理,格式同上
76
+ SOCKS5_PROXY: Optional[str] = os.getenv("SOCKS5_PROXY") # SOCKS5代理,格式: socks5://user:pass@host:port
77
+
78
  # Admin Panel Authentication
79
  ADMIN_PASSWORD: str = os.getenv("ADMIN_PASSWORD", "admin123") # 管理后台密码
80
  SESSION_SECRET_KEY: str = os.getenv("SESSION_SECRET_KEY", "your-secret-key-change-in-production") # Session 密钥
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  class Config:
83
  env_file = ".env"
84
  extra = "ignore" # 忽略额外字段,防止环境变量中的未知字段导致验证错误
app/core/openai.py CHANGED
@@ -93,6 +93,8 @@ async def handle_non_stream_response(stream_response, request: OpenAIRequest) ->
93
 
94
 
95
  @router.get("/v1/models")
 
 
96
  async def list_models():
97
  """List available models from all providers"""
98
  try:
@@ -115,6 +117,8 @@ async def list_models():
115
 
116
 
117
  @router.post("/v1/chat/completions")
 
 
118
  async def chat_completions(request: OpenAIRequest, authorization: str = Header(...)):
119
  """Handle chat completion requests with multi-provider architecture"""
120
  role = request.messages[0].role if request.messages else "unknown"
 
93
 
94
 
95
  @router.get("/v1/models")
96
+ @router.get("/hf/v1/models")
97
+ @router.get("/api/v1/models")
98
  async def list_models():
99
  """List available models from all providers"""
100
  try:
 
117
 
118
 
119
  @router.post("/v1/chat/completions")
120
+ @router.post("/hf/v1/chat/completions")
121
+ @router.post("/api/v1/chat/completions")
122
  async def chat_completions(request: OpenAIRequest, authorization: str = Header(...)):
123
  """Handle chat completion requests with multi-provider architecture"""
124
  role = request.messages[0].role if request.messages else "unknown"
app/providers/zai_provider.py CHANGED
@@ -12,6 +12,7 @@ import httpx
12
  import hmac
13
  import hashlib
14
  import base64
 
15
  from urllib.parse import urlencode
16
  import os
17
  import uuid
@@ -19,6 +20,7 @@ import random
19
  from datetime import datetime
20
  from typing import Dict, List, Any, Optional, AsyncGenerator, Union
21
  from app.utils.user_agent import get_random_user_agent
 
22
  from app.providers.base import BaseProvider, ProviderConfig
23
  from app.models.schemas import OpenAIRequest, Message
24
  from app.core.config import settings
@@ -68,7 +70,7 @@ def get_zai_dynamic_headers(chat_id: str = "") -> Dict[str, str]:
68
  "Cache-Control": "no-cache",
69
  "User-Agent": user_agent,
70
  "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
71
- "X-FE-Version": "prod-fe-1.0.98",
72
  "Origin": "https://chat.z.ai",
73
  }
74
 
@@ -116,26 +118,6 @@ def _extract_user_id_from_token(token: str) -> str:
116
  return "guest"
117
 
118
 
119
- def generate_signature(message_text: str, request_id: str, timestamp_ms: int, user_id: str, secret: str = "junjie") -> str:
120
- """Dual-layer HMAC-SHA256 signature.
121
-
122
- Layer1: derived key = HMAC(secret, window_index)
123
- Layer2: signature = HMAC(derived_key, canonical_string)
124
- canonical_string = "requestId,<id>,timestamp,<ts>,user_id,<uid>|<msg>|<ts>"
125
- """
126
- r = str(timestamp_ms)
127
- e = f"requestId,{request_id},timestamp,{timestamp_ms},user_id,{user_id}"
128
- t = message_text or ""
129
- # Add content_base64 processing for new signature algorithm
130
- content_base64 = base64.b64encode(t.encode('utf-8')).decode('ascii')
131
- i = f"{e}|{content_base64}|{r}"
132
-
133
- window_index = timestamp_ms // (5 * 60 * 1000)
134
- root_key = (secret or "junjie").encode("utf-8")
135
- derived_hex = hmac.new(root_key, str(window_index).encode("utf-8"), hashlib.sha256).hexdigest()
136
- signature = hmac.new(derived_hex.encode("utf-8"), i.encode("utf-8"), hashlib.sha256).hexdigest()
137
- return signature
138
-
139
 
140
  class ZAIProvider(BaseProvider):
141
  """Z.AI 提供商"""
@@ -179,30 +161,94 @@ class ZAIProvider(BaseProvider):
179
  settings.GLM46_SEARCH_MODEL,
180
  settings.GLM46_ADVANCED_SEARCH_MODEL,
181
  ]
182
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  async def get_token(self) -> str:
184
  """获取认证令牌"""
185
  # 如果启用匿名模式,只尝试获取访客令牌
186
  if settings.ANONYMOUS_MODE:
187
- try:
188
- headers = get_zai_dynamic_headers()
189
- async with httpx.AsyncClient() as client:
190
- response = await client.get(self.auth_url, headers=headers, timeout=10.0)
191
- if response.status_code == 200:
192
- data = response.json()
193
- token = data.get("token", "")
194
- if token:
195
- # 判断令牌类型(通过检查邮箱或user_id)
196
- email = data.get("email", "")
197
- is_guest = "@guest.com" in email or "Guest-" in email
198
- token_type = "匿名用户" if is_guest else "认证用户"
199
- self.logger.debug(f"获取令牌成功 ({token_type}): {token[:20]}...")
200
- return token
201
- except Exception as e:
202
- self.logger.warning(f"异步获取访客令牌失败: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
  # 匿名模式下,如果获取访客令牌失败,直接返回空
205
- self.logger.error("❌ 匿名模式下获取访客令牌失败")
206
  return ""
207
 
208
  # 非匿名模式:首先使用token池获取备份令牌
@@ -253,7 +299,7 @@ class ZAIProvider(BaseProvider):
253
 
254
  self.logger.debug(f"📤 上传图片: {filename}, 大小: {len(image_data)} bytes")
255
 
256
- # 构建上传请求 - 使用简化的请求头配置
257
  upload_url = f"{self.base_url}/api/v1/files/"
258
  headers = {
259
  "Accept": "*/*",
@@ -273,8 +319,11 @@ class ZAIProvider(BaseProvider):
273
  "Authorization": f"Bearer {token}",
274
  }
275
 
 
 
 
276
  # 使用 httpx 上传文件
277
- async with httpx.AsyncClient(timeout=30.0) as client:
278
  files = {
279
  "file": (filename, image_data, mime_type)
280
  }
@@ -501,12 +550,8 @@ class ZAIProvider(BaseProvider):
501
  if is_advanced_search:
502
  mcp_servers.append("advanced-search")
503
  self.logger.info("🔍 检测到高级搜索模型,添加 advanced-search MCP 服务器")
504
- elif is_search and "-4.5" in requested_model:
505
- mcp_servers.append("deep-web-search")
506
- self.logger.info("🔍 检测到搜索模型,添加 deep-web-search MCP 服务器")
507
-
508
- # 构建上游请求体(chat_id 已在前面生成)
509
 
 
510
  body = {
511
  "stream": True, # 总是使用流式
512
  "model": upstream_model_id,
@@ -587,34 +632,48 @@ class ZAIProvider(BaseProvider):
587
  if request.max_tokens is not None:
588
  body["params"]["max_tokens"] = request.max_tokens
589
 
590
- # 构建请求头
591
- headers = get_zai_dynamic_headers(chat_id)
592
- if token:
593
- headers["Authorization"] = f"Bearer {token}"
594
-
595
  # Dual-layer HMAC signing metadata and header
596
  user_id = _extract_user_id_from_token(token)
597
  timestamp_ms = int(time.time() * 1000)
598
  request_id = generate_uuid()
599
- secret = os.getenv("ZAI_SIGNING_SECRET", "junjie") or "junjie"
600
- signature = generate_signature(
601
- message_text=last_user_text,
602
- request_id=request_id,
603
- timestamp_ms=timestamp_ms,
604
- user_id=user_id,
605
- secret=secret,
606
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  query_params = {
608
- "timestamp": timestamp_ms,
609
  "requestId": request_id,
610
  "user_id": user_id,
611
- "token": token or "",
 
 
612
  "current_url": f"https://chat.z.ai/c/{chat_id}",
613
  "pathname": f"/c/{chat_id}",
614
- "signature_timestamp": timestamp_ms,
615
  }
616
  signed_url = f"{self.config.api_endpoint}?{urlencode(query_params)}"
617
- headers["X-Signature"] = signature
 
 
 
618
 
619
  # 存储当前token用于错误处理
620
  self._current_token = token
@@ -645,8 +704,11 @@ class ZAIProvider(BaseProvider):
645
  # 流式响应
646
  return self._create_stream_response(request, transformed)
647
  else:
 
 
 
648
  # 非流式响应
649
- async with httpx.AsyncClient(timeout=30.0) as client:
650
  response = await client.post(
651
  transformed["url"],
652
  headers=transformed["headers"],
@@ -673,9 +735,13 @@ class ZAIProvider(BaseProvider):
673
 
674
  current_token = transformed.get("token", "")
675
  try:
 
 
 
676
  async with httpx.AsyncClient(
677
  timeout=60.0,
678
  http2=True,
 
679
  ) as client:
680
  self.logger.info(f"🎯 发送请求到 Z.AI: {transformed['url']}")
681
  # self.logger.info(f"📦 请求体 model: {transformed['body']['model']}")
@@ -692,13 +758,25 @@ class ZAIProvider(BaseProvider):
692
  error_msg = error_text.decode('utf-8', errors='ignore')
693
  if error_msg:
694
  self.logger.error(f"❌ 错误详情: {error_msg}")
695
- error_response = {
696
- "error": {
697
- "message": f"Upstream error: {response.status_code}",
698
- "type": "upstream_error",
699
- "code": response.status_code
 
 
 
 
 
 
 
 
 
 
 
 
 
700
  }
701
- }
702
  yield f"data: {json.dumps(error_response)}\n\n"
703
  yield "data: [DONE]\n\n"
704
  return
@@ -859,10 +937,9 @@ class ZAIProvider(BaseProvider):
859
 
860
  # 尝试从缓冲区提取 tool_calls
861
  tool_calls = None
862
- cleaned_content = buffered_content
863
 
864
  if has_tools:
865
- tool_calls, cleaned_content = parse_and_extract_tool_calls(buffered_content)
866
 
867
  if tool_calls:
868
  # 发现工具调用
@@ -909,28 +986,8 @@ class ZAIProvider(BaseProvider):
909
  yield "data: [DONE]\n\n"
910
 
911
  else:
912
- # 没有工具调用,正常返回内容
913
- # 处理思考结束和答案开始
914
- if edit_content and "</details>\n" in edit_content:
915
- if has_thinking:
916
- # 发送思考签名
917
- thinking_signature = str(int(time.time() * 1000))
918
- sig_chunk = self.create_openai_chunk(
919
- chat_id,
920
- model,
921
- {
922
- "role": "assistant",
923
- "thinking": {
924
- "content": "",
925
- "signature": thinking_signature,
926
- }
927
- }
928
- )
929
- yield await self.format_sse_chunk(sig_chunk)
930
-
931
- # 提取答案内容
932
- cleaned_content = edit_content.split("</details>\n")[-1]
933
-
934
  if not has_sent_role and not has_thinking:
935
  role_chunk = self.create_openai_chunk(
936
  chat_id,
@@ -940,17 +997,6 @@ class ZAIProvider(BaseProvider):
940
  yield await self.format_sse_chunk(role_chunk)
941
  has_sent_role = True
942
 
943
- if cleaned_content:
944
- content_chunk = self.create_openai_chunk(
945
- chat_id,
946
- model,
947
- {
948
- "role": "assistant",
949
- "content": cleaned_content
950
- }
951
- )
952
- yield await self.format_sse_chunk(content_chunk)
953
-
954
  finish_chunk = self.create_openai_chunk(
955
  chat_id,
956
  model,
 
12
  import hmac
13
  import hashlib
14
  import base64
15
+ import asyncio
16
  from urllib.parse import urlencode
17
  import os
18
  import uuid
 
20
  from datetime import datetime
21
  from typing import Dict, List, Any, Optional, AsyncGenerator, Union
22
  from app.utils.user_agent import get_random_user_agent
23
+ from app.utils.signature import generate_signature
24
  from app.providers.base import BaseProvider, ProviderConfig
25
  from app.models.schemas import OpenAIRequest, Message
26
  from app.core.config import settings
 
70
  "Cache-Control": "no-cache",
71
  "User-Agent": user_agent,
72
  "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
73
+ "X-FE-Version": "prod-fe-1.0.106",
74
  "Origin": "https://chat.z.ai",
75
  }
76
 
 
118
  return "guest"
119
 
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  class ZAIProvider(BaseProvider):
123
  """Z.AI 提供商"""
 
161
  settings.GLM46_SEARCH_MODEL,
162
  settings.GLM46_ADVANCED_SEARCH_MODEL,
163
  ]
164
+
165
+ def _get_proxy_config(self) -> Optional[str]:
166
+ """Get proxy configuration from settings"""
167
+ # In httpx 0.28.1, proxy parameter expects a single URL string
168
+ # Support HTTP_PROXY, HTTPS_PROXY and SOCKS5_PROXY
169
+
170
+ if settings.HTTPS_PROXY:
171
+ self.logger.info(f"🔄 使用HTTPS代理: {settings.HTTPS_PROXY}")
172
+ return settings.HTTPS_PROXY
173
+
174
+ if settings.HTTP_PROXY:
175
+ self.logger.info(f"🔄 使用HTTP代理: {settings.HTTP_PROXY}")
176
+ return settings.HTTP_PROXY
177
+
178
+ if settings.SOCKS5_PROXY:
179
+ self.logger.info(f"🔄 使用SOCKS5代理: {settings.SOCKS5_PROXY}")
180
+ return settings.SOCKS5_PROXY
181
+
182
+ return None
183
+
184
  async def get_token(self) -> str:
185
  """获取认证令牌"""
186
  # 如果启用匿名模式,只尝试获取访客令牌
187
  if settings.ANONYMOUS_MODE:
188
+ max_retries = 3
189
+ retry_count = 0
190
+
191
+ while retry_count < max_retries:
192
+ try:
193
+ headers = get_zai_dynamic_headers()
194
+ self.logger.debug(f"尝试获取访客令牌 (第{retry_count + 1}次): {self.auth_url}")
195
+ self.logger.debug(f"请求头: {headers}")
196
+
197
+ # Get proxy configuration
198
+ proxies = self._get_proxy_config()
199
+
200
+ async with httpx.AsyncClient(timeout=30.0, follow_redirects=True, proxy=proxies) as client:
201
+ response = await client.get(self.auth_url, headers=headers)
202
+
203
+ self.logger.debug(f"响应状态码: {response.status_code}")
204
+ self.logger.debug(f"响应头: {dict(response.headers)}")
205
+
206
+ if response.status_code == 200:
207
+ data = response.json()
208
+ self.logger.debug(f"响应数据: {data}")
209
+
210
+ token = data.get("token", "")
211
+ if token:
212
+ # 判断令牌类型(通过检查邮箱或user_id)
213
+ email = data.get("email", "")
214
+ is_guest = "@guest.com" in email or "Guest-" in email
215
+ token_type = "匿名用户" if is_guest else "认证用户"
216
+ self.logger.info(f"✅ 获取令牌成功 ({token_type}): {token[:20]}...")
217
+ return token
218
+ else:
219
+ self.logger.warning(f"响应中未找到token字段: {data}")
220
+ elif response.status_code == 405:
221
+ # WAF拦截
222
+ self.logger.error(f"🚫 请求被WAF拦截 (状态码405),请求头可能被识别为异常,请稍后重试...")
223
+ break
224
+ else:
225
+ self.logger.warning(f"HTTP请求失败,状态码: {response.status_code}")
226
+ try:
227
+ error_data = response.json()
228
+ self.logger.warning(f"错误响应: {error_data}")
229
+ except:
230
+ self.logger.warning(f"错误响应文本: {response.text}")
231
+
232
+ except httpx.TimeoutException as e:
233
+ self.logger.warning(f"请求超时 (第{retry_count + 1}次): {e}")
234
+ except httpx.ConnectError as e:
235
+ self.logger.warning(f"连接错误 (第{retry_count + 1}次): {e}")
236
+ except httpx.HTTPStatusError as e:
237
+ self.logger.warning(f"HTTP状态错误 (第{retry_count + 1}次): {e}")
238
+ except json.JSONDecodeError as e:
239
+ self.logger.warning(f"JSON解析错误 (第{retry_count + 1}次): {e}")
240
+ except Exception as e:
241
+ self.logger.warning(f"异步获取访客令牌失败 (第{retry_count + 1}次): {e}")
242
+ import traceback
243
+ self.logger.debug(f"错误堆栈: {traceback.format_exc()}")
244
+
245
+ retry_count += 1
246
+ if retry_count < max_retries:
247
+ self.logger.info(f"等待2秒后重试...")
248
+ await asyncio.sleep(2)
249
 
250
  # 匿名模式下,如果获取访客令牌失败,直接返回空
251
+ self.logger.error("❌ 匿名模式下获取访客令牌失败,已重试3次")
252
  return ""
253
 
254
  # 非匿名模式:首先使用token池获取备份令牌
 
299
 
300
  self.logger.debug(f"📤 上传图片: {filename}, 大小: {len(image_data)} bytes")
301
 
302
+ # 构建上传请求
303
  upload_url = f"{self.base_url}/api/v1/files/"
304
  headers = {
305
  "Accept": "*/*",
 
319
  "Authorization": f"Bearer {token}",
320
  }
321
 
322
+ # Get proxy configuration
323
+ proxies = self._get_proxy_config()
324
+
325
  # 使用 httpx 上传文件
326
+ async with httpx.AsyncClient(timeout=30.0, proxy=proxies) as client:
327
  files = {
328
  "file": (filename, image_data, mime_type)
329
  }
 
550
  if is_advanced_search:
551
  mcp_servers.append("advanced-search")
552
  self.logger.info("🔍 检测到高级搜索模型,添加 advanced-search MCP 服务器")
 
 
 
 
 
553
 
554
+ # 构建上游请求体
555
  body = {
556
  "stream": True, # 总是使用流式
557
  "model": upstream_model_id,
 
632
  if request.max_tokens is not None:
633
  body["params"]["max_tokens"] = request.max_tokens
634
 
 
 
 
 
 
635
  # Dual-layer HMAC signing metadata and header
636
  user_id = _extract_user_id_from_token(token)
637
  timestamp_ms = int(time.time() * 1000)
638
  request_id = generate_uuid()
639
+ try:
640
+ signing_metadata = f"requestId,{request_id},timestamp,{timestamp_ms},user_id,{user_id}"
641
+ prompt_for_signature = last_user_text or ""
642
+ signature_result = generate_signature(
643
+ e=signing_metadata,
644
+ t=prompt_for_signature,
645
+ s=timestamp_ms,
646
+ )
647
+ signature = signature_result["signature"]
648
+ logger.debug(f"[Z.AI] 生成签名成功: {signature[:16]}... (user_id={user_id}, request_id={request_id})")
649
+ except Exception as e:
650
+ logger.error(f"[Z.AI] 签名生成失败: {e}")
651
+ signature = ""
652
+
653
+ # 构建请求头 (匹配 X-FE-Version 和 X-Signature)
654
+ headers = {
655
+ "Authorization": f"Bearer {token}",
656
+ "Content-Type": "application/json",
657
+ "X-FE-Version": "prod-fe-1.0.106",
658
+ "X-Signature": signature,
659
+ }
660
+
661
  query_params = {
662
+ "timestamp": str(timestamp_ms),
663
  "requestId": request_id,
664
  "user_id": user_id,
665
+ "token": token,
666
+ "version": "0.0.1",
667
+ "platform": "web",
668
  "current_url": f"https://chat.z.ai/c/{chat_id}",
669
  "pathname": f"/c/{chat_id}",
670
+ "signature_timestamp": str(timestamp_ms),
671
  }
672
  signed_url = f"{self.config.api_endpoint}?{urlencode(query_params)}"
673
+
674
+ # 记录请求详情用于调试
675
+ logger.debug(f"[Z.AI] 请求头: Authorization=Bearer *****, X-Signature={signature[:16] if signature else '(空)'}...")
676
+ logger.debug(f"[Z.AI] URL 参数: timestamp={timestamp_ms}, requestId={request_id}, user_id={user_id}")
677
 
678
  # 存储当前token用于错误处理
679
  self._current_token = token
 
704
  # 流式响应
705
  return self._create_stream_response(request, transformed)
706
  else:
707
+ # Get proxy configuration
708
+ proxies = self._get_proxy_config()
709
+
710
  # 非流式响应
711
+ async with httpx.AsyncClient(timeout=30.0, proxy=proxies) as client:
712
  response = await client.post(
713
  transformed["url"],
714
  headers=transformed["headers"],
 
735
 
736
  current_token = transformed.get("token", "")
737
  try:
738
+ # Get proxy configuration
739
+ proxies = self._get_proxy_config()
740
+
741
  async with httpx.AsyncClient(
742
  timeout=60.0,
743
  http2=True,
744
+ proxy=proxies,
745
  ) as client:
746
  self.logger.info(f"🎯 发送请求到 Z.AI: {transformed['url']}")
747
  # self.logger.info(f"📦 请求体 model: {transformed['body']['model']}")
 
758
  error_msg = error_text.decode('utf-8', errors='ignore')
759
  if error_msg:
760
  self.logger.error(f"❌ 错误详情: {error_msg}")
761
+
762
+ # 特殊处理 405 状态码(WAF拦截)
763
+ if response.status_code == 405:
764
+ self.logger.error(f"🚫 请求被上游WAF拦截,可能是请求头或签名异常,请稍后重试...")
765
+ error_response = {
766
+ "error": {
767
+ "message": "请求被上游WAF拦截(405 Method Not Allowed),可能是请求头或签名异常,请稍后重试...",
768
+ "type": "waf_blocked",
769
+ "code": 405
770
+ }
771
+ }
772
+ else:
773
+ error_response = {
774
+ "error": {
775
+ "message": f"Upstream error: {response.status_code}",
776
+ "type": "upstream_error",
777
+ "code": response.status_code
778
+ }
779
  }
 
780
  yield f"data: {json.dumps(error_response)}\n\n"
781
  yield "data: [DONE]\n\n"
782
  return
 
937
 
938
  # 尝试从缓冲区提取 tool_calls
939
  tool_calls = None
 
940
 
941
  if has_tools:
942
+ tool_calls, _ = parse_and_extract_tool_calls(buffered_content)
943
 
944
  if tool_calls:
945
  # 发现工具调用
 
986
  yield "data: [DONE]\n\n"
987
 
988
  else:
989
+ # 没有工具调用,流式内容已经在上面的增量输出中发送过了
990
+ # 这里只需要发送 finish 块即可,不要再次发送内容
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
991
  if not has_sent_role and not has_thinking:
992
  role_chunk = self.create_openai_chunk(
993
  chat_id,
 
997
  yield await self.format_sse_chunk(role_chunk)
998
  has_sent_role = True
999
 
 
 
 
 
 
 
 
 
 
 
 
1000
  finish_chunk = self.create_openai_chunk(
1001
  chat_id,
1002
  model,
app/utils/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
 
4
- from app.utils import sse_tool_handler, reload_config, logger
5
 
6
- __all__ = ["sse_tool_handler", "reload_config", "logger"]
 
1
  #!/usr/bin/env python
2
  # -*- coding: utf-8 -*-
3
 
4
+ from app.utils import reload_config, logger
5
 
6
+ __all__ = ["reload_config", "logger"]
app/utils/signature.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ Z.AI 签名工具模块
6
+ """
7
+
8
+ import hmac
9
+ import hashlib
10
+ import base64
11
+ from typing import Dict
12
+
13
+
14
+ def generate_signature(e: str, t: str, s: int) -> dict:
15
+ """Generate signature matching JavaScript zs function.
16
+
17
+ Args:
18
+ e: canonical metadata string, e.g. "requestId,<uuid>,timestamp,<ms>,user_id,<id>"
19
+ t: latest user message text that feeds into the signature prompt (may be empty)
20
+ s: timestamp in milliseconds
21
+
22
+ Returns:
23
+ Dictionary with signature and timestamp
24
+ """
25
+ # r = Number(s) - convert to number (already a number in Python)
26
+ r = s
27
+ # i = s - timestamp as string
28
+ i = str(s)
29
+
30
+ # n = new TextEncoder
31
+ # a = n.encode(t)
32
+ a = t.encode('utf-8')
33
+
34
+ # w = btoa(String.fromCharCode(...a))
35
+ # This is equivalent to base64 encoding the UTF-8 bytes
36
+ w = base64.b64encode(a).decode('ascii')
37
+
38
+ # c = `${e}|${w}|${i}`
39
+ c = f"{e}|{w}|{i}"
40
+
41
+ # E = Math.floor(r / (5 * 60 * 1e3))
42
+ E = r // (5 * 60 * 1000)
43
+
44
+ # A = CryptoJS.HmacSHA256(`${E}`, "key-@@@@)))()((9))-xxxx&&&%%%%%")
45
+ secret = "key-@@@@)))()((9))-xxxx&&&%%%%%"
46
+ A = hmac.new(secret.encode('utf-8'), str(E).encode('utf-8'), hashlib.sha256).hexdigest()
47
+
48
+ # k = CryptoJS.HmacSHA256(c, A).toString()
49
+ k = hmac.new(A.encode('utf-8'), c.encode('utf-8'), hashlib.sha256).hexdigest()
50
+
51
+ # return n.encode(c), { signature: k, timestamp: i }
52
+ # Note: n.encode(c) is not used in the return value, so we ignore it
53
+ return {
54
+ "signature": k,
55
+ "timestamp": i
56
+ }
app/utils/sse_tool_handler.py DELETED
@@ -1,612 +0,0 @@
1
- #!/usr/bin/env python
2
- # -*- coding: utf-8 -*-
3
-
4
- """
5
- SSE Tool Handler
6
-
7
- 处理 Z.AI SSE 流数据并转换为 OpenAI 兼容格式的工具调用处理器。
8
-
9
- 主要功能:
10
- - 解析 glm_block 格式的工具调用
11
- - 从 metadata.arguments 提取完整参数
12
- - 支持多阶段处理:thinking → tool_call → other → answer
13
- - 输出符合 OpenAI API 规范的流式响应
14
- """
15
-
16
- import json
17
- import time
18
- from typing import Dict, Any, Generator
19
- from enum import Enum
20
-
21
- from app.utils.logger import get_logger
22
-
23
- logger = get_logger()
24
-
25
-
26
- class SSEPhase(Enum):
27
- """SSE 处理阶段枚举"""
28
- THINKING = "thinking"
29
- TOOL_CALL = "tool_call"
30
- OTHER = "other"
31
- ANSWER = "answer"
32
- DONE = "done"
33
-
34
-
35
- class SSEToolHandler:
36
- """SSE 工具调用处理器"""
37
-
38
- def __init__(self, model: str, stream: bool = True):
39
- self.model = model
40
- self.stream = stream
41
-
42
- # 状态管理
43
- self.current_phase = None
44
- self.has_tool_call = False
45
-
46
- # 工具调用状态
47
- self.tool_id = ""
48
- self.tool_name = ""
49
- self.tool_args = ""
50
- self.tool_call_usage = {}
51
- self.content_index = 0 # 工具调用索引
52
-
53
- # 性能优化:内容缓冲
54
- self.content_buffer = ""
55
- self.buffer_size = 0
56
- self.last_flush_time = time.time()
57
- self.flush_interval = 0.05 # 50ms 刷新间隔
58
- self.max_buffer_size = 100 # 最大缓冲字符数
59
-
60
- logger.debug(f"🔧 初始化工具处理器: model={model}, stream={stream}")
61
-
62
- def process_sse_chunk(self, chunk_data: Dict[str, Any]) -> Generator[str, None, None]:
63
- """
64
- 处理 SSE 数据块,返回 OpenAI 格式的流式响应
65
-
66
- Args:
67
- chunk_data: Z.AI SSE 数据块
68
-
69
- Yields:
70
- str: OpenAI 格式的 SSE 响应行
71
- """
72
- try:
73
- phase = chunk_data.get("phase")
74
- edit_content = chunk_data.get("edit_content", "")
75
- delta_content = chunk_data.get("delta_content", "")
76
- edit_index = chunk_data.get("edit_index")
77
- usage = chunk_data.get("usage", {})
78
-
79
- # 数据验证
80
- if not phase:
81
- logger.warning("⚠️ 收到无效的 SSE 块:缺少 phase 字段")
82
- return
83
-
84
- # 阶段变化检测和日志
85
- if phase != self.current_phase:
86
- # 阶段变化时强制刷新缓冲区
87
- if hasattr(self, 'content_buffer') and self.content_buffer:
88
- yield from self._flush_content_buffer()
89
-
90
- logger.info(f"📈 SSE 阶段变化: {self.current_phase} → {phase}")
91
- content_preview = edit_content or delta_content
92
- if content_preview:
93
- logger.debug(f" 📝 内容预览: {content_preview[:1000]}{'...' if len(content_preview) > 1000 else ''}")
94
- if edit_index is not None:
95
- logger.debug(f" 📍 edit_index: {edit_index}")
96
- self.current_phase = phase
97
-
98
- # 根据阶段处理
99
- if phase == SSEPhase.THINKING.value:
100
- yield from self._process_thinking_phase(delta_content)
101
-
102
- elif phase == SSEPhase.TOOL_CALL.value:
103
- yield from self._process_tool_call_phase(edit_content)
104
-
105
- elif phase == SSEPhase.OTHER.value:
106
- yield from self._process_other_phase(usage, edit_content)
107
-
108
- elif phase == SSEPhase.ANSWER.value:
109
- yield from self._process_answer_phase(delta_content)
110
-
111
- elif phase == SSEPhase.DONE.value:
112
- yield from self._process_done_phase(chunk_data)
113
- else:
114
- logger.warning(f"⚠️ 未知的 SSE 阶段: {phase}")
115
-
116
- except Exception as e:
117
- logger.error(f"❌ 处理 SSE 块时发生错误: {e}")
118
- logger.debug(f" 📦 错误块数据: {chunk_data}")
119
- # 不中断流,继续处理后续块
120
-
121
- def _process_thinking_phase(self, delta_content: str) -> Generator[str, None, None]:
122
- """处理思考阶段"""
123
- if not delta_content:
124
- return
125
-
126
- logger.debug(f"🤔 思考内容: +{len(delta_content)} 字符")
127
-
128
- # 在流模式下输出思考内容
129
- if self.stream:
130
- chunk = self._create_content_chunk(delta_content)
131
- yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
132
-
133
- def _process_tool_call_phase(self, edit_content: str) -> Generator[str, None, None]:
134
- """处理工具调用阶段"""
135
- if not edit_content:
136
- return
137
-
138
- logger.debug(f"🔧 进入工具调用阶段,内容长度: {len(edit_content)}")
139
-
140
- # 检测 glm_block 标记
141
- if "<glm_block " in edit_content:
142
- yield from self._handle_glm_blocks(edit_content)
143
- else:
144
- # 没有 glm_block 标记,可能是参数补充
145
- if self.has_tool_call:
146
- # 只累积���数部分,找到第一个 ", "result"" 之前的内容
147
- result_pos = edit_content.find('", "result"')
148
- if result_pos > 0:
149
- param_fragment = edit_content[:result_pos]
150
- self.tool_args += param_fragment
151
- logger.debug(f"📦 累积参数片段: {param_fragment}")
152
- else:
153
- # 如果没有找到结束标记,累积整个内容(可能是中间片段)
154
- self.tool_args += edit_content
155
- logger.debug(f"📦 累积参数片段: {edit_content[:100]}...")
156
-
157
- def _handle_glm_blocks(self, edit_content: str) -> Generator[str, None, None]:
158
- """处理 glm_block 标记的内容"""
159
- blocks = edit_content.split('<glm_block ')
160
- logger.debug(f"📦 分割得到 {len(blocks)} 个块")
161
-
162
- for index, block in enumerate(blocks):
163
- if not block.strip():
164
- continue
165
-
166
- if index == 0:
167
- # 第一个块:提取参数片段
168
- if self.has_tool_call:
169
- logger.debug(f"📦 从第一个块提取参数片段")
170
- # 找到 "result" 的位置,提取之前的参数片段
171
- result_pos = edit_content.find('"result"')
172
- if result_pos > 0:
173
- # 往前退3个字符去掉 ", "
174
- param_fragment = edit_content[:result_pos - 3]
175
- self.tool_args += param_fragment
176
- logger.debug(f"📦 累积参数片段: {param_fragment}")
177
- else:
178
- # 没有活跃工具调用,跳过第一个块
179
- continue
180
- else:
181
- # 后续块:处理新工具调用
182
- if "</glm_block>" not in block:
183
- continue
184
-
185
- # 如果有活跃的工具调用,先完成它
186
- if self.has_tool_call:
187
- # 补全参数并完成工具调用
188
- self.tool_args += '"' # 补全最后的引号
189
- yield from self._finish_current_tool()
190
-
191
- # 处理新工具调用
192
- yield from self._process_metadata_block(block)
193
-
194
- def _process_metadata_block(self, block: str) -> Generator[str, None, None]:
195
- """处理包含工具元数据的块"""
196
- try:
197
- # 提取 JSON 内容
198
- start_pos = block.find('>')
199
- end_pos = block.rfind('</glm_block>')
200
-
201
- if start_pos == -1 or end_pos == -1:
202
- logger.warning(f"❌ 无法找到 JSON 内容边界: {block[:1000]}...")
203
- return
204
-
205
- json_content = block[start_pos + 1:end_pos]
206
- logger.debug(f"📦 提取的 JSON 内容: {json_content[:1000]}...")
207
-
208
- # 解析工具元数据
209
- metadata_obj = json.loads(json_content)
210
-
211
- if "data" in metadata_obj and "metadata" in metadata_obj["data"]:
212
- metadata = metadata_obj["data"]["metadata"]
213
-
214
- # 开始新的工具调用
215
- self.tool_id = metadata.get("id", f"call_{int(time.time() * 1000000)}")
216
- self.tool_name = metadata.get("name", "unknown")
217
- self.has_tool_call = True
218
-
219
- # 只有在这是第二个及以后的工具调用时才递增 index
220
- # 第一个工具调用应该使用 index 0
221
-
222
- # 从 metadata.arguments 获取参数起始部分
223
- if "arguments" in metadata:
224
- arguments_str = metadata["arguments"]
225
- # 去掉最后一个字符
226
- self.tool_args = arguments_str[:-1] if arguments_str.endswith('"') else arguments_str
227
- logger.debug(f"🎯 新工具调用: {self.tool_name}(id={self.tool_id}), 初始参数: {self.tool_args}")
228
- else:
229
- self.tool_args = "{}"
230
- logger.debug(f"🎯 新工具调用: {self.tool_name}(id={self.tool_id}), 空参数")
231
-
232
- except (json.JSONDecodeError, KeyError, AttributeError) as e:
233
- logger.error(f"❌ 解析工具元数据失败: {e}, 块内容: {block[:1000]}...")
234
-
235
- # 确保返回生成器(即使为空)
236
- if False: # 永远不会执行,但确保函数是生成器
237
- yield
238
-
239
- def _process_other_phase(self, usage: Dict[str, Any], edit_content: str = "") -> Generator[str, None, None]:
240
- """处理其他阶段"""
241
- # 保存使用统计信息
242
- if usage:
243
- self.tool_call_usage = usage
244
- logger.debug(f"📊 保存使用统计: {usage}")
245
-
246
- # 工具调用完成判断:检测到 "null," 开头的 edit_content
247
- if self.has_tool_call and edit_content and edit_content.startswith("null,"):
248
- logger.info(f"🏁 检测到工具调用结束标记")
249
-
250
- # 完成当前工具调用
251
- yield from self._finish_current_tool()
252
-
253
- # 发��流结束标记
254
- if self.stream:
255
- yield "data: [DONE]\n\n"
256
-
257
- # 重置状态
258
- self._reset_all_state()
259
-
260
- def _process_answer_phase(self, delta_content: str) -> Generator[str, None, None]:
261
- """处理回答阶段(优化版本)"""
262
- if not delta_content:
263
- return
264
-
265
- logger.info(f"📝 工具处理器收到答案内容: {delta_content[:50]}...")
266
-
267
- # 添加到缓冲区
268
- self.content_buffer += delta_content
269
- self.buffer_size += len(delta_content)
270
-
271
- current_time = time.time()
272
- time_since_last_flush = current_time - self.last_flush_time
273
-
274
- # 检查是否需要刷新缓冲区
275
- should_flush = (
276
- self.buffer_size >= self.max_buffer_size or # 缓冲区满了
277
- time_since_last_flush >= self.flush_interval or # 时间间隔到了
278
- '\n' in delta_content or # 包含换行符
279
- '。' in delta_content or '!' in delta_content or '?' in delta_content # 包含句子结束符
280
- )
281
-
282
- if should_flush and self.content_buffer:
283
- yield from self._flush_content_buffer()
284
-
285
- def _flush_content_buffer(self) -> Generator[str, None, None]:
286
- """刷新内容缓冲区"""
287
- if not self.content_buffer:
288
- return
289
-
290
- logger.info(f"💬 工具处理器刷新缓冲区: {self.buffer_size} 字符 - {self.content_buffer[:50]}...")
291
-
292
- if self.stream:
293
- chunk = self._create_content_chunk(self.content_buffer)
294
- output_data = f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
295
- logger.info(f"➡️ 工具处理器输出: {output_data[:100]}...")
296
- yield output_data
297
-
298
- # 清空缓冲区
299
- self.content_buffer = ""
300
- self.buffer_size = 0
301
- self.last_flush_time = time.time()
302
-
303
- def _process_done_phase(self, chunk_data: Dict[str, Any]) -> Generator[str, None, None]:
304
- """处理完成阶段"""
305
- logger.info("🏁 对话完成")
306
-
307
- # 先刷新任何剩余的缓冲内容
308
- if self.content_buffer:
309
- yield from self._flush_content_buffer()
310
-
311
- # 完成任何未完成的工具调用
312
- if self.has_tool_call:
313
- yield from self._finish_current_tool()
314
-
315
- # 发送流结束标记
316
- if self.stream:
317
- # 创建最终的完成块
318
- final_chunk = {
319
- "id": f"chatcmpl-{int(time.time())}",
320
- "object": "chat.completion.chunk",
321
- "created": int(time.time()),
322
- "model": self.model,
323
- "choices": [{
324
- "index": 0,
325
- "delta": {},
326
- "finish_reason": "stop"
327
- }]
328
- }
329
-
330
- # 如果有 usage 信息,添加到最终块中
331
- if "usage" in chunk_data:
332
- final_chunk["usage"] = chunk_data["usage"]
333
-
334
- yield f"data: {json.dumps(final_chunk, ensure_ascii=False)}\n\n"
335
- yield "data: [DONE]\n\n"
336
-
337
- # 重置所有状态
338
- self._reset_all_state()
339
-
340
- def _finish_current_tool(self) -> Generator[str, None, None]:
341
- """完成当前工具调用"""
342
- if not self.has_tool_call:
343
- return
344
-
345
- # 修复参数格式
346
- fixed_args = self._fix_tool_arguments(self.tool_args)
347
- logger.debug(f"✅ 完成工具调用: {self.tool_name}, 参数: {fixed_args}")
348
-
349
- # 输出工具调用(开始 + 参数 + 完成)
350
- if self.stream:
351
- # 发送工具开始块
352
- start_chunk = self._create_tool_start_chunk()
353
- yield f"data: {json.dumps(start_chunk, ensure_ascii=False)}\n\n"
354
-
355
- # 发送参数块
356
- args_chunk = self._create_tool_arguments_chunk(fixed_args)
357
- yield f"data: {json.dumps(args_chunk, ensure_ascii=False)}\n\n"
358
-
359
- # 发送完成块
360
- finish_chunk = self._create_tool_finish_chunk()
361
- yield f"data: {json.dumps(finish_chunk, ensure_ascii=False)}\n\n"
362
-
363
- # 重置工具状态
364
- self._reset_tool_state()
365
-
366
- def _fix_tool_arguments(self, raw_args: str) -> str:
367
- """使用 json-repair 库修复工具参数格式"""
368
- if not raw_args or raw_args == "{}":
369
- return "{}"
370
-
371
- logger.debug(f"🔧 开始修复参数: {raw_args[:1000]}{'...' if len(raw_args) > 1000 else ''}")
372
-
373
- # 统一的修复流程:预处理 -> json-repair -> 后处理
374
- try:
375
- # 1. 预处理:只处理 json-repair 无法处理的问题
376
- processed_args = self._preprocess_json_string(raw_args.strip())
377
-
378
- # 2. 使用 json-repair 进行主要修复
379
- from json_repair import repair_json
380
- repaired_json = repair_json(processed_args)
381
- logger.debug(f"🔧 json-repair 修复结果: {repaired_json}")
382
-
383
- # 3. 解析并后处理
384
- args_obj = json.loads(repaired_json)
385
- args_obj = self._post_process_args(args_obj)
386
-
387
- # 4. 生成最终结果
388
- fixed_result = json.dumps(args_obj, ensure_ascii=False)
389
-
390
- return fixed_result
391
-
392
- except Exception as e:
393
- logger.error(f"❌ JSON 修复失败: {e}, 原始参数: {raw_args[:1000]}..., 使用空参数")
394
- return "{}"
395
-
396
- def _post_process_args(self, args_obj: Dict[str, Any]) -> Dict[str, Any]:
397
- """统一的后处理方法"""
398
- # 修复路径中的过度转义
399
- args_obj = self._fix_path_escaping_in_args(args_obj)
400
-
401
- # 修复命令中的多余引号
402
- args_obj = self._fix_command_quotes(args_obj)
403
-
404
- return args_obj
405
-
406
- def _preprocess_json_string(self, text: str) -> str:
407
- """预处理 JSON 字符串,只处理 json-repair 无法处理的问题"""
408
- import re
409
-
410
- # 只保留 json-repair 无法处理的预处理步骤
411
-
412
- # 1. 修复缺少开始括号的情况(json-repair 无法处理)
413
- if not text.startswith('{') and text.endswith('}'):
414
- text = '{' + text
415
- logger.debug(f"🔧 补全开始括号")
416
-
417
- # 2. 修复末尾多余的反斜杠和引号(json-repair 可能处理不当)
418
- # 匹配模式:字符串值末尾的 \" 后面跟着 } 或 ,
419
- # 例如:{"url":"https://www.bilibili.com\"} -> {"url":"https://www.bilibili.com"}
420
- # 例如:{"url":"https://www.bilibili.com\",} -> {"url":"https://www.bilibili.com",}
421
- pattern = r'([^\\])\\"([}\s,])'
422
- if re.search(pattern, text):
423
- text = re.sub(pattern, r'\1"\2', text)
424
- logger.debug(f"🔧 修复末尾多余的反斜杠")
425
-
426
- return text
427
-
428
- def _fix_path_escaping_in_args(self, args_obj: Dict[str, Any]) -> Dict[str, Any]:
429
- """修复参数对象中路径的过度转义问题"""
430
- import re
431
-
432
- # 需要检查的路径字段
433
- path_fields = ['file_path', 'path', 'directory', 'folder']
434
-
435
- for field in path_fields:
436
- if field in args_obj and isinstance(args_obj[field], str):
437
- path_value = args_obj[field]
438
-
439
- # 检查是否是Windows路径且包含过度转义
440
- if path_value.startswith('C:') and '\\\\' in path_value:
441
- logger.debug(f"🔍 检查路径字段 {field}: {repr(path_value)}")
442
-
443
- # 分析路径结构:正常路径应该是 C:\Users\...
444
- # 但过度转义的路径可能是 C:\Users\\Documents(多了一个反斜杠)
445
- # 我们需要找到不正常的双反斜杠模式并修复
446
-
447
- # 先检查是否有不正常的双反斜杠(不在路径开头)
448
- # 正常:C:\Users\Documents
449
- # 异常:C:\Users\\Documents 或 C:\Users\\\\Documents
450
-
451
- # 使用更精确的模式:匹配路径分隔符后的额外反斜杠
452
- # 但要保留正常的路径分隔符
453
- fixed_path = path_value
454
-
455
- # 检查是否有连续的多个反斜杠(超过正常的路径分隔符)
456
- if '\\\\' in path_value:
457
- # 计算反斜杠的数量,如果超过正常数量就修复
458
- parts = path_value.split('\\')
459
- # 重新组装路径,去除空的部分(由多余的反斜杠造成)
460
- clean_parts = [part for part in parts if part]
461
- if len(clean_parts) > 1:
462
- fixed_path = '\\'.join(clean_parts)
463
-
464
- logger.debug(f"🔍 修复后路径: {repr(fixed_path)}")
465
-
466
- if fixed_path != path_value:
467
- args_obj[field] = fixed_path
468
- logger.debug(f"🔧 修复字段 {field} 的路径转义: {path_value} -> {fixed_path}")
469
- else:
470
- logger.debug(f"🔍 路径无需修复: {path_value}")
471
-
472
- return args_obj
473
-
474
- def _fix_command_quotes(self, args_obj: Dict[str, Any]) -> Dict[str, Any]:
475
- """修复命令中的多余引号问题"""
476
- import re
477
-
478
- # 检查命令字段
479
- if 'command' in args_obj and isinstance(args_obj['command'], str):
480
- command = args_obj['command']
481
-
482
- # 检查是否以双引号结尾(多余的引号)
483
- if command.endswith('""'):
484
- logger.debug(f"🔧 发现命令末尾多余引号: {command}")
485
- # 移除最后一个多余的引号
486
- fixed_command = command[:-1]
487
- args_obj['command'] = fixed_command
488
- logger.debug(f"🔧 修复命令引号: {command} -> {fixed_command}")
489
-
490
- # 检查其他可能的引号问题
491
- # 例如:路径末尾的 \"" 模式
492
- elif re.search(r'\\""+$', command):
493
- logger.debug(f"🔧 发现命令末尾引号模式问题: {command}")
494
- # 修复路径末尾的引号问题
495
- fixed_command = re.sub(r'\\""+$', '\\"', command)
496
- args_obj['command'] = fixed_command
497
- logger.debug(f"🔧 修复命令引号模式: {command} -> {fixed_command}")
498
-
499
- return args_obj
500
-
501
- def _create_content_chunk(self, content: str) -> Dict[str, Any]:
502
- """创建内容块"""
503
- return {
504
- "id": f"chatcmpl-{int(time.time())}",
505
- "object": "chat.completion.chunk",
506
- "created": int(time.time()),
507
- "model": self.model,
508
- "choices": [{
509
- "index": 0,
510
- "delta": {
511
- "role": "assistant",
512
- "content": content
513
- },
514
- "finish_reason": None
515
- }]
516
- }
517
-
518
- def _create_tool_start_chunk(self) -> Dict[str, Any]:
519
- """创建工具开始块"""
520
- return {
521
- "id": f"chatcmpl-{int(time.time())}",
522
- "object": "chat.completion.chunk",
523
- "created": int(time.time()),
524
- "model": self.model,
525
- "choices": [{
526
- "index": 0,
527
- "delta": {
528
- "role": "assistant",
529
- "tool_calls": [{
530
- "index": self.content_index,
531
- "id": self.tool_id,
532
- "type": "function",
533
- "function": {
534
- "name": self.tool_name,
535
- "arguments": ""
536
- }
537
- }]
538
- },
539
- "finish_reason": None
540
- }]
541
- }
542
-
543
- def _create_tool_arguments_chunk(self, arguments: str) -> Dict[str, Any]:
544
- """创建工具参数块"""
545
- return {
546
- "id": f"chatcmpl-{int(time.time())}",
547
- "object": "chat.completion.chunk",
548
- "created": int(time.time()),
549
- "model": self.model,
550
- "choices": [{
551
- "index": 0,
552
- "delta": {
553
- "tool_calls": [{
554
- "index": self.content_index,
555
- "id": self.tool_id,
556
- "function": {
557
- "arguments": arguments
558
- }
559
- }]
560
- },
561
- "finish_reason": None
562
- }]
563
- }
564
-
565
- def _create_tool_finish_chunk(self) -> Dict[str, Any]:
566
- """创建工具完成块"""
567
- chunk = {
568
- "id": f"chatcmpl-{int(time.time())}",
569
- "object": "chat.completion.chunk",
570
- "created": int(time.time()),
571
- "model": self.model,
572
- "choices": [{
573
- "index": 0,
574
- "delta": {
575
- "tool_calls": []
576
- },
577
- "finish_reason": "tool_calls"
578
- }]
579
- }
580
-
581
- # 添加使用统计(如果有)
582
- if self.tool_call_usage:
583
- chunk["usage"] = self.tool_call_usage
584
-
585
- return chunk
586
-
587
- def _reset_tool_state(self):
588
- """重置工具状态"""
589
- self.tool_id = ""
590
- self.tool_name = ""
591
- self.tool_args = ""
592
- self.has_tool_call = False
593
- # content_index 在单次对话中应该保持不变,只有在新的工具调用开始时才递增
594
-
595
- def _reset_all_state(self):
596
- """重置所有状态"""
597
- # 先刷新任何剩余的缓冲内容
598
- if hasattr(self, 'content_buffer') and self.content_buffer:
599
- list(self._flush_content_buffer()) # 消费生成器
600
-
601
- self._reset_tool_state()
602
- self.current_phase = None
603
- self.tool_call_usage = {}
604
-
605
- # 重置缓冲区
606
- self.content_buffer = ""
607
- self.buffer_size = 0
608
- self.last_flush_time = time.time()
609
-
610
- # content_index 重置为 0,为下一轮对话做准备
611
- self.content_index = 0
612
- logger.debug("🔄 重置所有处理器状态")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy/.env.example DELETED
@@ -1,35 +0,0 @@
1
- # ==============================================
2
- # Z.AI API Server - Docker 环境变量配置示例
3
- # ==============================================
4
-
5
- # 管理后台密码
6
- ADMIN_PASSWORD=admin123
7
-
8
- # API 认证密钥 (用于验证客户端请求)
9
- AUTH_TOKEN=sk-your-api-key-here
10
-
11
- # 是否跳过 API Key 验证 (开发环境可设为 true)
12
- SKIP_AUTH_TOKEN=false
13
-
14
- # 调试日志 (生产环境建议设为 false)
15
- DEBUG_LOGGING=true
16
-
17
- # 匿名模式 (允许无 token 访问,需要配合 SKIP_AUTH_TOKEN=true)
18
- ANONYMOUS_MODE=false
19
-
20
- # Function Call 功能开关 (是否支持工具调用)
21
- TOOL_SUPPORT=true
22
-
23
- # 工具调用扫描限制 (字符数)
24
- SCAN_LIMIT=200000
25
-
26
- # 数据库路径 (Docker 环境使用持久化卷)
27
- DB_PATH=/app/data/tokens.db
28
-
29
- # Token 池配置
30
- TOKEN_FAILURE_THRESHOLD=3
31
- TOKEN_RECOVERY_TIMEOUT=300
32
-
33
- # 服务配置
34
- SERVICE_NAME=Z.AI_API_Server
35
- LISTEN_PORT=7860
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy/Dockerfile DELETED
@@ -1,24 +0,0 @@
1
- FROM python:3.12-slim
2
-
3
- # Set working directory
4
- WORKDIR /app
5
-
6
- # Create data and logs directories with proper permissions
7
- RUN mkdir -p /app/data /app/logs && \
8
- chmod 755 /app/data /app/logs
9
-
10
- # Install dependencies
11
- COPY requirements.txt .
12
- RUN pip install --no-cache-dir -r requirements.txt
13
-
14
- # Copy application code
15
- COPY . .
16
-
17
- # Set environment variable for database path
18
- ENV DB_PATH=/app/data/tokens.db
19
-
20
- # Expose port
21
- EXPOSE 7860
22
-
23
- # Run the application
24
- CMD ["python", "main.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deploy/docker-compose.yml → docker-compose.yml RENAMED
File without changes
deploy/nginx.conf.example → nginx.conf.example RENAMED
File without changes
tests/test_signature.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import sys
3
+ import os
4
+
5
+ # 添加项目根目录到 Python 路径
6
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
7
+
8
+ from app.utils.signature import generate_signature
9
+
10
+
11
+ if __name__ == "__main__":
12
+ # 示例用法
13
+ e_value = "requestId,eef12d6c-6dc9-47a0-aae8-b9f3454f98c5,timestamp,1761038714733,user_id,21ea9ec3-e492-4dbb-b522-fc0eaf64f0f6"
14
+ t_value = "hi"
15
+ # r_value = int(time.time() * 1000)
16
+ r_value = 1761038714733
17
+ result = generate_signature(e_value, t_value, r_value)
18
+ print(f"生成的签名: {result['signature']}")
19
+ print(f"时间戳: {result['timestamp']}")
tests/test_simple_signature.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """
5
+ 简单测试签名工具
6
+ """
7
+
8
+ import sys
9
+ import os
10
+
11
+ # 添加项目根目录到 Python 路径
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13
+
14
+ # 直接导入签名模块,避免导入整个应用
15
+ import importlib.util
16
+ spec = importlib.util.spec_from_file_location("signature", os.path.join(os.path.dirname(os.path.dirname(__file__)), "app/utils/signature.py"))
17
+ signature_module = importlib.util.module_from_spec(spec)
18
+ spec.loader.exec_module(signature_module)
19
+
20
+ generate_signature = signature_module.generate_signature
21
+
22
+
23
+ if __name__ == "__main__":
24
+ # 示例用法
25
+ e_value = "requestId,eef12d6c-6dc9-47a0-aae8-b9f3454f98c5,timestamp,1761038714733,user_id,21ea9ec3-e492-4dbb-b522-fc0eaf64f0f6"
26
+ t_value = "hi"
27
+ r_value = 1761038714733
28
+ result = generate_signature(e_value, t_value, r_value)
29
+ print(f"生成的签名: {result['signature']}")
30
+ print(f"时间戳: {result['timestamp']}")
31
+
32
+ # 验证函数是否正常工作
33
+ assert "signature" in result
34
+ assert "timestamp" in result
35
+ assert result["timestamp"] == str(r_value)
36
+ print("签名函数测试通过!")