letterm commited on
Commit
47d8ca8
·
verified ·
1 Parent(s): dc9f3d9

Upload 13 files

Browse files
Files changed (13) hide show
  1. Dockerfile +17 -0
  2. api_service.py +297 -0
  3. config.py +105 -0
  4. login_client.py +375 -0
  5. model_mapper.py +51 -0
  6. protobuf_manager.py +317 -0
  7. request_converter.py +185 -0
  8. token_manager.py +344 -0
  9. utils.py +131 -0
  10. warp_api_server.py +341 -0
  11. warp_client.py +192 -0
  12. warp_unified_pb2.py +108 -0
  13. web_template.py +980 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # 设置工作目录
4
+ WORKDIR /app
5
+
6
+ # 复制requirements.txt并安装Python依赖
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir --user -r requirements.txt
9
+
10
+ # 复制项目文件
11
+ COPY . .
12
+
13
+ # 暴露端口
14
+ EXPOSE 7860
15
+
16
+ # 启动命令
17
+ CMD ["python", "warp_api_server.py"]
api_service.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API服务模块
3
+ 处理所有API请求逻辑,包括聊天完成和Token管理
4
+ """
5
+ import json
6
+ import time
7
+ from datetime import datetime
8
+ from typing import List, Dict, Any, Generator
9
+ from loguru import logger
10
+
11
+ from config import Config
12
+ from utils import Utils
13
+ from token_manager import MultiTokenManager
14
+ from warp_client import WarpClient
15
+ from request_converter import RequestConverter
16
+ from model_mapper import ModelMapper
17
+
18
+
19
+ class ApiService:
20
+ """API服务类,处理所有业务逻辑"""
21
+
22
+ def __init__(self):
23
+ # 初始化Token管理器
24
+ self.token_manager = MultiTokenManager()
25
+
26
+ # 初始化Warp客户端
27
+ self.warp_client = WarpClient(self.token_manager)
28
+
29
+ logger.info("🚀 ApiService初始化完成")
30
+
31
+ def authenticate_request(self, auth_header: str) -> bool:
32
+ """验证API请求"""
33
+ if not auth_header:
34
+ return False
35
+
36
+ token = Utils.extract_bearer_token(auth_header)
37
+ if not token:
38
+ return False
39
+
40
+ return Utils.validate_api_key(token)
41
+
42
+ def get_models(self) -> Dict[str, Any]:
43
+ """获取支持的模型列表"""
44
+ return ModelMapper.get_openai_models()
45
+
46
+ def chat_completion(self, request_data: Dict[str, Any], stream: bool = False) -> Generator[str, None, None]:
47
+ """处理聊天完成请求"""
48
+ request_id = Utils.generate_request_id()
49
+ # 解析请求
50
+ openai_request = RequestConverter.parse_openai_request(request_data)
51
+ model = openai_request.model
52
+ messages = openai_request.messages
53
+
54
+ logger.info(f"🎯 开始处理聊天请求 [ID: {request_id[:8]}] [模型: {model}] [流式: {stream}]")
55
+ start_time = time.time()
56
+
57
+ try:
58
+ # 创建protobuf数据
59
+ protobuf_data = self.warp_client.create_protobuf_data(messages, model)
60
+ if not protobuf_data:
61
+ error_msg = "创建请求数据失败"
62
+ logger.error(f"❌ {error_msg} [ID: {request_id[:8]}]")
63
+ yield self._create_error_response(error_msg, request_id)
64
+ return
65
+
66
+ # 发送请求并处理响应
67
+ response_chunks = 0
68
+ total_content = ""
69
+
70
+ logger.success(f"🚀 开始接收响应 [ID: {request_id[:8]}]")
71
+
72
+ for chunk_text in self.warp_client.send_request(protobuf_data):
73
+ if chunk_text:
74
+ response_chunks += 1
75
+ total_content += chunk_text
76
+
77
+ logger.debug(f"📦 响应块 #{response_chunks} [ID: {request_id[:8]}] [长度: {len(chunk_text)}]")
78
+
79
+ if stream:
80
+ # 流式响应
81
+ chunk_response = self._create_stream_chunk(chunk_text, request_id)
82
+ yield f"data: {json.dumps(chunk_response)}\n\n"
83
+ else:
84
+ # 非流式响应 - 等待完整内容
85
+ continue
86
+
87
+ # 处理响应结束
88
+ end_time = time.time()
89
+ duration = end_time - start_time
90
+
91
+ if stream:
92
+ # 发送结束标记
93
+ final_chunk = self._create_stream_end_chunk(request_id)
94
+ yield f"data: {json.dumps(final_chunk)}\n\n"
95
+ yield "data: [DONE]\n\n"
96
+
97
+ logger.success(f"✅ 流式响应完成 [ID: {request_id[:8]}] [块数: {response_chunks}] [耗时: {duration:.2f}s]")
98
+ else:
99
+ # 返回完整响应
100
+ response = self._create_complete_response(total_content, request_id)
101
+ yield response
102
+
103
+ logger.success(f"✅ 完整响应完成 [ID: {request_id[:8]}] [长度: {len(total_content)}] [耗时: {duration:.2f}s]")
104
+
105
+ except Exception as e:
106
+ logger.error(f"❌ 聊天请求处理失败 [ID: {request_id[:8]}]: {e}")
107
+ yield self._create_error_response(f"服务器内部错误: {str(e)}", request_id)
108
+
109
+ def _create_stream_chunk(self, content: str, request_id: str) -> Dict[str, Any]:
110
+ """创建流式响应块"""
111
+ return {
112
+ "id": f"chatcmpl-{request_id}",
113
+ "object": "chat.completion.chunk",
114
+ "created": int(time.time()),
115
+ "model": "gemini-2.0-flash",
116
+ "choices": [{
117
+ "index": 0,
118
+ "delta": {"content": content},
119
+ "finish_reason": None
120
+ }]
121
+ }
122
+
123
+ def _create_stream_end_chunk(self, request_id: str) -> Dict[str, Any]:
124
+ """创建流式响应结束块"""
125
+ return {
126
+ "id": f"chatcmpl-{request_id}",
127
+ "object": "chat.completion.chunk",
128
+ "created": int(time.time()),
129
+ "model": "gemini-2.0-flash",
130
+ "choices": [{
131
+ "index": 0,
132
+ "delta": {},
133
+ "finish_reason": "stop"
134
+ }]
135
+ }
136
+
137
+ def _create_complete_response(self, content: str, request_id: str) -> Dict[str, Any]:
138
+ """创建完整响应"""
139
+ return {
140
+ "id": f"chatcmpl-{request_id}",
141
+ "object": "chat.completion",
142
+ "created": int(time.time()),
143
+ "model": "gemini-2.0-flash",
144
+ "choices": [{
145
+ "index": 0,
146
+ "message": {
147
+ "role": "assistant",
148
+ "content": content
149
+ },
150
+ "finish_reason": "stop"
151
+ }],
152
+ "usage": {
153
+ "prompt_tokens": 0,
154
+ "completion_tokens": 0,
155
+ "total_tokens": 0
156
+ }
157
+ }
158
+
159
+ def _create_error_response(self, error_message: str, request_id: str) -> Dict[str, Any]:
160
+ """创建错误响应"""
161
+ return {
162
+ "error": {
163
+ "message": error_message,
164
+ "type": "api_error",
165
+ "code": "internal_error"
166
+ },
167
+ "id": request_id
168
+ }
169
+
170
+ def get_token_status(self) -> Dict[str, Any]:
171
+ """获取Token状态"""
172
+ try:
173
+ status = self.token_manager.get_token_status()
174
+ return {"success": True, **status}
175
+ except Exception as e:
176
+ logger.error(f"❌ 获取Token状态失败: {e}")
177
+ return {"success": False, "message": str(e)}
178
+
179
+ def add_tokens(self, tokens: List[str]) -> Dict[str, Any]:
180
+ """添加Token"""
181
+ try:
182
+ success = self.token_manager.add_refresh_tokens(tokens)
183
+ if success:
184
+ valid_tokens = [t for t in tokens if Utils.validate_refresh_token_format(t)]
185
+ return {
186
+ "success": True,
187
+ "message": "Token添加成功",
188
+ "added_tokens": len(valid_tokens)
189
+ }
190
+ else:
191
+ return {"success": False, "message": "没有有效的Token可添加"}
192
+ except Exception as e:
193
+ logger.error(f"❌ 添加Token失败: {e}")
194
+ return {"success": False, "message": str(e)}
195
+
196
+ def remove_refresh_token(self, refresh_token: str) -> Dict[str, Any]:
197
+ """删除refresh token"""
198
+ try:
199
+ success = self.token_manager.remove_refresh_token(refresh_token)
200
+ if success:
201
+ return {"success": True, "message": "Token删除成功"}
202
+ else:
203
+ return {"success": False, "message": "Token不存在"}
204
+ except Exception as e:
205
+ logger.error(f"❌ 删除Token失败: {e}")
206
+ return {"success": False, "message": str(e)}
207
+
208
+ def refresh_all_tokens(self) -> Dict[str, Any]:
209
+ """刷新所有Token"""
210
+ try:
211
+ self.token_manager.refresh_all_tokens()
212
+ return {"success": True, "message": "Token刷新已开始"}
213
+ except Exception as e:
214
+ logger.error(f"❌ 刷新Token失败: {e}")
215
+ return {"success": False, "message": str(e)}
216
+
217
+ def export_refresh_tokens(self, super_admin_key: str) -> Dict[str, Any]:
218
+ """导出refresh token内容(需要超级管理员密钥验证)"""
219
+ try:
220
+ # 验证超级管理员密钥
221
+ if Config.require_super_admin_auth():
222
+ if not super_admin_key or super_admin_key != Config.get_super_admin_key():
223
+ return {"success": False, "message": "超级管理员密钥验证失败"}
224
+
225
+ # 获取所有refresh token
226
+ with self.token_manager.token_lock:
227
+ refresh_tokens = list(self.token_manager.tokens.keys())
228
+
229
+ if not refresh_tokens:
230
+ return {"success": False, "message": "没有可导出的token"}
231
+
232
+ # 创建分号分割的token字符串
233
+ token_string = ";".join(refresh_tokens)
234
+
235
+ # 生成建议的文件名(带时间戳)
236
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
237
+ suggested_filename = f"refresh_tokens_export_{timestamp}.txt"
238
+
239
+ logger.info(f"🔒 超级管理员请求导出 {len(refresh_tokens)} 个refresh token")
240
+
241
+ return {
242
+ "success": True,
243
+ "message": f"准备导出 {len(refresh_tokens)} 个token",
244
+ "content": token_string,
245
+ "suggested_filename": suggested_filename,
246
+ "token_count": len(refresh_tokens)
247
+ }
248
+
249
+ except Exception as e:
250
+ logger.error(f"❌ 准备导出refresh token失败: {e}")
251
+ return {"success": False, "message": f"导出失败: {str(e)}"}
252
+
253
+ def batch_get_refresh_tokens(self, email_url_dict: Dict[str, str], max_workers: int = 5) -> Dict[str, Any]:
254
+ """批量获取refresh token并自动创建用户"""
255
+ try:
256
+ from login_client import LoginClient
257
+ login_client = LoginClient()
258
+
259
+ # 传递token_manager参数,这样在获取refresh_token后会立即尝试创建用户
260
+ results = login_client.batch_process_emails(email_url_dict, max_workers, self.token_manager)
261
+
262
+ # 提取有效的token并添加到管理器
263
+ valid_tokens = []
264
+ for email, result in results.items():
265
+ if result.get('refresh_token'):
266
+ valid_tokens.append(result['refresh_token'])
267
+
268
+ if valid_tokens:
269
+ self.token_manager.add_refresh_tokens(valid_tokens)
270
+ logger.info(f"✅ 批量获取并添加了 {len(valid_tokens)} 个有效token")
271
+
272
+ return {
273
+ 'success': True,
274
+ 'results': results,
275
+ 'total_count': len(email_url_dict),
276
+ 'success_count': len(valid_tokens)
277
+ }
278
+
279
+ except Exception as e:
280
+ logger.error(f"❌ 批量获取refresh token失败: {e}")
281
+ return {'success': False, 'message': str(e)}
282
+
283
+ def start_services(self):
284
+ """启动后台服务"""
285
+ try:
286
+ self.token_manager.start_auto_refresh()
287
+ logger.success("✅ 后台服务启动成功")
288
+ except Exception as e:
289
+ logger.error(f"❌ 启动后台服务失败: {e}")
290
+
291
+ def stop_services(self):
292
+ """停止后台服务"""
293
+ try:
294
+ self.token_manager.stop_auto_refresh()
295
+ logger.info("⏹️ 后台服务已停止")
296
+ except Exception as e:
297
+ logger.error(f"❌ 停止后台服务失败: {e}")
config.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 配置管理模块
3
+ 包含所有全局配置常量和设置
4
+ """
5
+ import os
6
+
7
+
8
+ class Config:
9
+ """全局配置管理"""
10
+
11
+ # 服务器配置
12
+ SERVER_HOST = "0.0.0.0"
13
+ SERVER_PORT = 7860
14
+
15
+ # Warp API 配置
16
+ WARP_BASE_URL = "https://app.warp.dev"
17
+ WARP_AI_ENDPOINT = "/ai/multi-agent"
18
+ WARP_CLIENT_VERSION = "v0.2025.06.20.22.47.stable_07"
19
+ WARP_OS_CATEGORY = "Windows"
20
+ WARP_OS_NAME = "Windows"
21
+ WARP_OS_VERSION = "10 (19045)"
22
+
23
+ # Google Token相关配置
24
+ GOOGLE_TOKEN_URL = "https://securetoken.googleapis.com/v1/token?key=AIzaSyBdy3O3S9hrdayLJxJ7mriBR4qgUaUygAs"
25
+ TOKEN_REFRESH_INTERVAL = 50 * 60 # 50分钟,单位:秒
26
+
27
+ # 登录相关配置
28
+ OPTIMIZATION_GUIDE_URL = "https://optimizationguide-pa.googleapis.com/v1:GetHints"
29
+ FIREBASE_LOGIN_URL = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithEmailLink?key=AIzaSyBdy3O3S9hrdayLJxJ7mriBR4qgUaUygAs"
30
+
31
+ # OpenAI API配置
32
+ OPENAI_API_VERSION = "v1"
33
+
34
+ # 请求超时设置
35
+ REQUEST_TIMEOUT = 360
36
+
37
+ # API密钥配置(从环境变量获取,默认为sk-123456)
38
+ @classmethod
39
+ def get_api_key(cls) -> str:
40
+ """获取API密钥"""
41
+ return os.environ.get('AUTH_API_KEY', 'sk-123456')
42
+
43
+ @property
44
+ def VALID_API_KEYS(self) -> list:
45
+ """有效的API密钥列表"""
46
+ return [self.get_api_key()]
47
+
48
+ # 管理员密钥配置
49
+ @classmethod
50
+ def get_admin_key(cls) -> str:
51
+ """获取管理员密钥(用于访问Web界面)"""
52
+ return os.environ.get('ADMIN_KEY', '')
53
+
54
+ @classmethod
55
+ def get_super_admin_key(cls) -> str:
56
+ """获取超级管理员密钥(用于敏感操作如导出token)"""
57
+ return os.environ.get('SUPER_ADMIN_KEY', '')
58
+
59
+ @classmethod
60
+ def require_admin_auth(cls) -> bool:
61
+ """是否需要管理员认证"""
62
+ return bool(cls.get_admin_key())
63
+
64
+ @classmethod
65
+ def require_super_admin_auth(cls) -> bool:
66
+ """是否需要超级管理员认证"""
67
+ return bool(cls.get_super_admin_key())
68
+
69
+ # refresh token环境变量配置(支持分号分割的多个token)
70
+ @classmethod
71
+ def get_refresh_tokens(cls) -> list:
72
+ """从环境变量获取refresh token列表(支持分号分割)"""
73
+ env_token = os.environ.get('WARP_REFRESH_TOKEN', '')
74
+ if not env_token:
75
+ return []
76
+
77
+ # 使用分号分割多个token
78
+ tokens = [token.strip() for token in env_token.split(';') if token.strip()]
79
+ return tokens
80
+
81
+ @classmethod
82
+ def get_refresh_token(cls) -> str:
83
+ """获取第一个refresh token(向后兼容)"""
84
+ tokens = cls.get_refresh_tokens()
85
+ return tokens[0] if tokens else ''
86
+
87
+ # 日志配置
88
+ @classmethod
89
+ def enable_file_logging(cls) -> bool:
90
+ """是否启用文件日志"""
91
+ return os.environ.get('ENABLE_FILE_LOGGING', 'false').lower() in ('true', '1', 'yes', 'on','True')
92
+
93
+
94
+ class LogConfig:
95
+ """日志配置"""
96
+
97
+ LOG_LEVEL = "INFO"
98
+ LOG_FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
99
+
100
+
101
+ class ProtobufConfig:
102
+ """Protobuf相关配置"""
103
+
104
+ PROTO_FILE = "warp_unified.proto"
105
+ PROTO_PACKAGE = "warp_unified"
login_client.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 登录客户端模块
3
+ 处理Warp登录相关功能,包括获取refresh token
4
+ """
5
+ import re
6
+ import requests
7
+ import urllib.parse
8
+ from typing import Optional, Dict, List, Tuple
9
+ from concurrent.futures import ThreadPoolExecutor
10
+ from loguru import logger
11
+
12
+ from config import Config
13
+ from protobuf_manager import ProtobufManager
14
+
15
+
16
+ class LoginClient:
17
+ """Warp登录客户端"""
18
+
19
+ def __init__(self):
20
+ self.session = requests.Session()
21
+
22
+ def create_protobuf_data(self, login_url: str) -> Optional[bytes]:
23
+ """创建登录请求的protobuf数据"""
24
+ return ProtobufManager.create_login_request(login_url)
25
+
26
+ def get_oob_code(self, login_url: str) -> Optional[str]:
27
+ """获取oob_code"""
28
+ logger.debug(f"🔄 开始获取oob_code,URL: {login_url[:50]}...")
29
+
30
+ protobuf_data = self.create_protobuf_data(login_url)
31
+ if not protobuf_data:
32
+ return None
33
+
34
+ headers = {
35
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
36
+ "Connection": "keep-alive",
37
+ "Accept": "*/*",
38
+ "Accept-Encoding": "gzip, deflate, br, zstd",
39
+ "Content-Type": "application/octet-stream",
40
+ "x-goog-api-key": "AIzaSyA2KlwBX3mkFo30om9LUFYQhpqLoa_BNhE",
41
+ "content-type": "application/x-protobuf",
42
+ "x-client-data": "CK+1yQEIjLbJAQimtskBCKmdygEI74zLAQiVocsBCJKjywEIhaDNAQjY7c4BCOLwzgEIpfLOAQi88s4BCJP2zgEYnPPOAQ==",
43
+ "sec-fetch-site": "none",
44
+ "sec-fetch-mode": "no-cors",
45
+ "sec-fetch-dest": "empty",
46
+ "accept-language": "zh-CN,zh;q=0.9",
47
+ "priority": "u=4, i"
48
+ }
49
+
50
+ try:
51
+ response = self.session.post(
52
+ Config.OPTIMIZATION_GUIDE_URL,
53
+ headers=headers,
54
+ data=protobuf_data,
55
+ timeout=60
56
+ )
57
+
58
+ if response.status_code == 200:
59
+ logger.info(f"🔄 响应内容: {response.text}")
60
+
61
+ # 使用ProtobufManager解析响应
62
+ decoded_url = ProtobufManager.parse_login_response(response.content)
63
+ logger.info(f"🔄 解码后的URL: {decoded_url}")
64
+ if decoded_url == "":
65
+ logger.error("❌ 解码后的URL为空")
66
+ decoded_url = response.text
67
+
68
+ # 提取oob_code
69
+ pattern = r'oobCode=([^&]+)'
70
+ match = re.search(pattern, decoded_url)
71
+ if match:
72
+ oob_code = match.group(1)
73
+ logger.success(f"✅ 获取到oob_code: {oob_code[:20]}...")
74
+ return oob_code
75
+ else:
76
+ logger.error("❌ 未能从响应中提取oob_code")
77
+ return None
78
+ else:
79
+ logger.error(f"❌ 请求失败: {response.status_code}")
80
+ return None
81
+
82
+ except Exception as e:
83
+ logger.error(f"❌ 获取oob_code时出错: {e}")
84
+ return None
85
+
86
+ def verify_id_token(self, id_token: str) -> bool:
87
+ """验证idToken并使refresh_token生效"""
88
+ logger.debug(f"🔍 开始验证idToken: {id_token[:20]}...")
89
+
90
+ headers = {
91
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
92
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
93
+ 'Content-Type': 'application/json',
94
+ 'sec-ch-ua-platform': '"Windows"',
95
+ 'x-client-version': 'Chrome/JsCore/11.3.1/FirebaseCore-web',
96
+ 'sec-ch-ua': '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
97
+ 'sec-ch-ua-mobile': '?0',
98
+ 'x-firebase-gmpid': '1:13153726198:web:1cc16bca7287752f8e06d7',
99
+ 'origin': 'https://app.warp.dev',
100
+ 'x-browser-channel': 'stable',
101
+ 'x-browser-year': '2025',
102
+ 'x-browser-validation': '6h3XF8YcD8syi2FF2BbuE2KllQo=',
103
+ 'x-browser-copyright': 'Copyright 2025 Google LLC. All rights reserved.',
104
+ 'x-client-data': 'CK+1yQEIjLbJAQimtskBCKmdygEI74zLAQiWocsBCJKjywEIhaDNAQji8M4BCJP2zgE=',
105
+ 'sec-fetch-site': 'cross-site',
106
+ 'sec-fetch-mode': 'cors',
107
+ 'sec-fetch-dest': 'empty',
108
+ 'accept-language': 'zh-CN,zh;q=0.9',
109
+ 'priority': 'u=1, i'
110
+ }
111
+
112
+ payload = {
113
+ 'idToken': id_token
114
+ }
115
+
116
+ try:
117
+ response = self.session.post(
118
+ 'https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=AIzaSyBdy3O3S9hrdayLJxJ7mriBR4qgUaUygAs',
119
+ headers=headers,
120
+ json=payload,
121
+ timeout=30
122
+ )
123
+
124
+ if response.status_code == 200:
125
+ logger.success("✅ idToken验证成功,refresh_token已生效")
126
+ return True
127
+ else:
128
+ logger.error(f"❌ idToken验证失败,状态码: {response.status_code}")
129
+ logger.debug(f"❌ 响应内容: {response.text}")
130
+ return False
131
+
132
+ except Exception as e:
133
+ logger.error(f"❌ 验证idToken时出错: {e}")
134
+ return False
135
+
136
+ def create_user_with_access_token(self, access_token: str) -> bool:
137
+ """使用access_token创建用户"""
138
+ logger.info(f"👤 开始创建用户,access_token: {access_token[:20]}...")
139
+
140
+ headers = {
141
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
142
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
143
+ 'Content-Type': 'application/json',
144
+ 'sec-ch-ua-platform': '"Windows"',
145
+ 'authorization': f'Bearer {access_token}',
146
+ 'sec-ch-ua': '"Not)A;Brand";v="8", "Chromium";v="138", "Google Chrome";v="138"',
147
+ 'sec-ch-ua-mobile': '?0',
148
+ 'origin': 'https://app.warp.dev',
149
+ 'sec-fetch-site': 'same-origin',
150
+ 'sec-fetch-mode': 'cors',
151
+ 'sec-fetch-dest': 'empty',
152
+ 'accept-language': 'zh-CN,zh;q=0.9',
153
+ 'priority': 'u=1, i',
154
+ 'Cookie': 'rl_anonymous_id=8b178568-76c6-4e9f-8c72-461bd1909c1c'
155
+ }
156
+
157
+ payload = {
158
+ "operationName": "GetOrCreateUser",
159
+ "variables": {
160
+ "input": {},
161
+ "requestContext": {
162
+ "osContext": {},
163
+ "clientContext": {}
164
+ }
165
+ },
166
+ "query": "mutation GetOrCreateUser($input: GetOrCreateUserInput!, $requestContext: RequestContext!) {\n getOrCreateUser(requestContext: $requestContext, input: $input) {\n ... on GetOrCreateUserOutput {\n uid\n isOnboarded\n anonymousUserInfo {\n anonymousUserType\n linkedAt\n __typename\n }\n workspaces {\n joinableTeams {\n teamUid\n numMembers\n name\n teamAcceptingInvites\n __typename\n }\n __typename\n }\n deletedAnonymousUser\n __typename\n }\n ... on UserFacingError {\n error {\n message\n __typename\n }\n __typename\n }\n __typename\n }\n}\n"
167
+ }
168
+
169
+ try:
170
+ response = self.session.post(
171
+ 'https://app.warp.dev/graphql/v2?op=GetOrCreateUser',
172
+ headers=headers,
173
+ json=payload,
174
+ timeout=30
175
+ )
176
+
177
+ if response.status_code == 200:
178
+ response_data = response.json()
179
+ # 检查是否有错误
180
+ if 'errors' in response_data:
181
+ logger.error(f"❌ 创建用户失败,GraphQL错误: {response_data['errors']}")
182
+ return False
183
+
184
+ # 检查响应数据
185
+ data = response_data.get('data', {})
186
+ get_or_create_user = data.get('getOrCreateUser', {})
187
+
188
+ if 'uid' in get_or_create_user:
189
+ logger.success(f"✅ 用户创建成功,UID: {get_or_create_user['uid']}")
190
+ return True
191
+ elif 'error' in get_or_create_user:
192
+ logger.error(f"❌ 创建用户失败: {get_or_create_user['error']}")
193
+ return False
194
+ else:
195
+ logger.success("✅ 用户创建请求已发送")
196
+ return True
197
+
198
+ else:
199
+ logger.error(f"❌ 创建用户失败,状态码: {response.status_code}")
200
+ logger.debug(f"❌ 响应内容: {response.text}")
201
+ return False
202
+
203
+ except Exception as e:
204
+ logger.error(f"❌ 创建用户时出错: {e}")
205
+ return False
206
+
207
+ def get_refresh_token(self, email: str, oob_code: str) -> Optional[str]:
208
+ """使用email和oob_code获取refresh token,并验证idToken"""
209
+ logger.info(f"🔄 开始获取refresh token for {email}")
210
+
211
+ headers = {
212
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
213
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
214
+ 'Content-Type': 'application/json',
215
+ 'sec-ch-ua-platform': 'Windows',
216
+ 'x-client-version': 'Chrome/JsCore/11.3.1/FirebaseCore-web',
217
+ 'sec-ch-ua': '"Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
218
+ 'sec-ch-ua-mobile': '?0',
219
+ 'x-firebase-gmpid': '1:13153726198:web:1cc16bca7287752f8e06d7',
220
+ 'origin': 'https://app.warp.dev',
221
+ 'x-client-data': 'CK+1yQEIjLbJAQimtskBCKmdygEI74zLAQiVocsBCJKjywEIhaDNAQji8M0BCKXyzgEIk/bOAQ==',
222
+ 'sec-fetch-site': 'cross-site',
223
+ 'sec-fetch-mode': 'cors',
224
+ 'sec-fetch-dest': 'empty',
225
+ 'accept-language': 'zh-CN,zh;q=0.9',
226
+ 'priority': 'u=1, i'
227
+ }
228
+
229
+ payload = {
230
+ 'email': email,
231
+ 'oobCode': oob_code
232
+ }
233
+
234
+ try:
235
+ response = self.session.post(Config.FIREBASE_LOGIN_URL, headers=headers, json=payload, timeout=30)
236
+
237
+ if response.status_code == 200:
238
+ response_data = response.json()
239
+ refresh_token = response_data.get("refreshToken")
240
+ id_token = response_data.get("idToken")
241
+
242
+ if not refresh_token:
243
+ logger.error(f"❌ 响应中未找到refreshToken字段 for {email}")
244
+ return None
245
+
246
+ if not id_token:
247
+ logger.error(f"❌ 响应中未找到idToken字段 for {email}")
248
+ return None
249
+
250
+ logger.success(f"✅ 获取到refresh token和idToken for {email}")
251
+
252
+ # 验证idToken使refresh_token生效
253
+ if self.verify_id_token(id_token):
254
+ logger.success(f"🎉 refresh token验证成功并已生效 for {email}")
255
+ return refresh_token
256
+ else:
257
+ logger.error(f"❌ refresh token验证失败,设置为无效 for {email}")
258
+ return None
259
+
260
+ else:
261
+ logger.error(f"❌ 获取refresh token失败 for {email}: {response.status_code}")
262
+ logger.debug(f"❌ 响应内容: {response.text}")
263
+ return None
264
+
265
+ except Exception as e:
266
+ logger.error(f"❌ 获取refresh token时出错 for {email}: {e}")
267
+ return None
268
+
269
+ def process_single_email_with_user_creation(self, email: str, login_url: str, token_manager=None) -> Tuple[str, Optional[str], str]:
270
+ """处理单个email,返回(email, refresh_token, status),并在获取refresh_token后创建用户"""
271
+ logger.info(f"🚀 开始处理邮箱: {email}")
272
+
273
+ try:
274
+ # 获取oob_code
275
+ oob_code = self.get_oob_code(login_url)
276
+ if not oob_code:
277
+ return email, None, "获取oob_code失败"
278
+
279
+ # 获取refresh token并验证
280
+ refresh_token = self.get_refresh_token(email, oob_code)
281
+ if refresh_token:
282
+ logger.success(f"✅ 邮箱 {email} refresh_token获取成功")
283
+
284
+ # 如果有token_manager,立即获取access_token并创建用户
285
+ if token_manager:
286
+ logger.info(f"🔄 正在为 {email} 获取access_token并创建用户...")
287
+ access_token = token_manager.get_access_token(refresh_token)
288
+ if access_token:
289
+ logger.info(f"✅ 为 {email} 获取到access_token")
290
+ if self.create_user_with_access_token(access_token):
291
+ logger.success(f"🎉 为 {email} 创建用户成功")
292
+ return email, refresh_token, "成功并已创建用户"
293
+ else:
294
+ logger.warning(f"⚠️ 为 {email} 创建用户失败,但refresh_token有效")
295
+ return email, refresh_token, "成功,但创建用户失败"
296
+ else:
297
+ logger.warning(f"⚠️ 为 {email} 获取access_token失败")
298
+ return email, refresh_token, "成功,但获取access_token失败"
299
+ else:
300
+ return email, refresh_token, "成功"
301
+ else:
302
+ logger.error(f"❌ 邮箱 {email} 处理失败")
303
+ return email, None, "获取refresh_token失败或验证失败"
304
+
305
+ except Exception as e:
306
+ logger.error(f"❌ 处理邮箱 {email} 时异常: {e}")
307
+ return email, None, f"处理异常: {str(e)}"
308
+
309
+ def process_single_email(self, email: str, login_url: str) -> Tuple[str, Optional[str], str]:
310
+ """处理单个email,返回(email, refresh_token, status)"""
311
+ logger.info(f"🚀 开始处理邮箱: {email}")
312
+
313
+ try:
314
+ # 获取oob_code
315
+ oob_code = self.get_oob_code(login_url)
316
+ if not oob_code:
317
+ return email, None, "获取oob_code失败"
318
+
319
+ # 获取refresh token并验证
320
+ refresh_token = self.get_refresh_token(email, oob_code)
321
+ if refresh_token:
322
+ logger.success(f"✅ 邮箱 {email} 处理成功")
323
+ return email, refresh_token, "成功"
324
+ else:
325
+ logger.error(f"❌ 邮箱 {email} 处理失败")
326
+ return email, None, "获取refresh_token失败或验证失败"
327
+
328
+ except Exception as e:
329
+ logger.error(f"❌ 处理邮箱 {email} 时异常: {e}")
330
+ return email, None, f"处理异常: {str(e)}"
331
+
332
+ def batch_process_emails(self, email_url_dict: Dict[str, str], max_workers: int = 5, token_manager=None) -> Dict[str, Dict]:
333
+ """批量处理emails,返回结果字典"""
334
+ logger.info(f"🚀 开始批量处理 {len(email_url_dict)} 个邮箱,并发数: {max_workers}")
335
+ results = {}
336
+
337
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
338
+ # 提交所有任务
339
+ if token_manager:
340
+ future_to_email = {
341
+ executor.submit(self.process_single_email_with_user_creation, email, url, token_manager): email
342
+ for email, url in email_url_dict.items()
343
+ }
344
+ else:
345
+ future_to_email = {
346
+ executor.submit(self.process_single_email, email, url): email
347
+ for email, url in email_url_dict.items()
348
+ }
349
+
350
+ # 收集结果
351
+ for future in future_to_email:
352
+ email = future_to_email[future]
353
+ try:
354
+ email, refresh_token, status = future.result()
355
+ results[email] = {
356
+ 'refresh_token': refresh_token,
357
+ 'status': status
358
+ }
359
+
360
+ if refresh_token:
361
+ logger.success(f"✅ {email}: {status}")
362
+ else:
363
+ logger.error(f"❌ {email}: {status}")
364
+
365
+ except Exception as e:
366
+ logger.error(f"❌ {email}: 处理异常: {str(e)}")
367
+ results[email] = {
368
+ 'refresh_token': None,
369
+ 'status': f"处理异常: {str(e)}"
370
+ }
371
+
372
+ success_count = sum(1 for result in results.values() if result['refresh_token'])
373
+ logger.info(f"📊 批量处理完成: {success_count}/{len(email_url_dict)} 成功")
374
+
375
+ return results
model_mapper.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 模型映射模块
3
+ 管理OpenAI模型名称到Warp模型名称的映射
4
+ """
5
+ from typing import List
6
+
7
+
8
+ class ModelMapper:
9
+ """模型名称映射管理"""
10
+
11
+ MODEL_MAPPING = {
12
+ "claude-4-sonnet": "claude-4-sonnet",
13
+ "claude-3-7-sonnet": "claude-3-7-sonnet",
14
+ "claude-3-5-sonnet": "claude-3-5-sonnet",
15
+ "claude-3-5-haiku": "claude-3-5-haiku",
16
+ "gpt-4o": "gpt-4o",
17
+ "gpt-4.1": "gpt-4.1",
18
+ "o4-mini": "o4-mini",
19
+ "o3": "o3",
20
+ "o3-mini": "o3-mini",
21
+ "gemini-2.0-flash": "gemini-2.0-flash",
22
+ "gemini-2.5-pro": "gemini-2.5-pro"
23
+ }
24
+
25
+ DEFAULT_MODEL = "gemini-2.0-flash"
26
+
27
+ @classmethod
28
+ def get_warp_model(cls, openai_model: str) -> str:
29
+ """将OpenAI模型名转换为Warp模型名"""
30
+ return cls.MODEL_MAPPING.get(openai_model, cls.DEFAULT_MODEL)
31
+
32
+ @classmethod
33
+ def get_available_models(cls) -> List[str]:
34
+ """获取所有可用模型列表"""
35
+ return list(cls.MODEL_MAPPING.keys())
36
+
37
+ @classmethod
38
+ def is_valid_model(cls, model: str) -> bool:
39
+ """检查模型是否有效"""
40
+ return model in cls.MODEL_MAPPING
41
+
42
+ @classmethod
43
+ def add_model_mapping(cls, openai_model: str, warp_model: str):
44
+ """添加新的模型映射"""
45
+ cls.MODEL_MAPPING[openai_model] = warp_model
46
+
47
+ @classmethod
48
+ def remove_model_mapping(cls, openai_model: str):
49
+ """移除模型映射"""
50
+ if openai_model in cls.MODEL_MAPPING:
51
+ del cls.MODEL_MAPPING[openai_model]
protobuf_manager.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Protobuf管理模块
3
+ 提供统一的protobuf操作接口,直接使用已编译的pb2文件
4
+ """
5
+ import base64
6
+ from typing import List, Optional, Dict, Any
7
+ from utils import Utils, logger
8
+
9
+ # 直接导入已编译的protobuf模块
10
+ try:
11
+ import warp_unified_pb2
12
+ logger.success("✅ 成功导入已编译的protobuf模块")
13
+ except ImportError as e:
14
+ logger.error(f"❌ 无法导入protobuf模块: {e}")
15
+ logger.error("请确保 warp_unified_pb2.py 文件存在且可访问")
16
+ raise
17
+
18
+
19
+ class ProtobufManager:
20
+ """Protobuf操作管理器"""
21
+
22
+ @staticmethod
23
+ def create_chat_request(messages: List[Dict], model: str = "gemini-2.0-flash") -> Optional[bytes]:
24
+ """
25
+ 创建聊天请求的protobuf数据
26
+
27
+ Args:
28
+ messages: OpenAI格式的消息列表
29
+ model: 模型名称
30
+
31
+ Returns:
32
+ 序列化的protobuf数据,如果失败返回None
33
+ """
34
+ try:
35
+ from request_converter import RequestConverter
36
+
37
+ # 处理消息历史
38
+ merged_groups, current_message = RequestConverter.process_message_history(messages)
39
+ logger.debug(f"🔄 处理消息历史: {len(merged_groups)} 组历史消息, 当前消息: {current_message[:50]}...")
40
+
41
+ # 创建主请求
42
+ request = warp_unified_pb2.ChatRequest()
43
+ session_id = Utils.generate_request_id()
44
+
45
+ # 如果只有一条消息,只设置当前消息
46
+ if len(messages) == 1:
47
+ # 字段1: 会话信息
48
+ request.session.session_id = session_id
49
+
50
+ # 字段2: 系统信息
51
+ request.system.environment.timestamp.time = Utils.get_current_timestamp()
52
+
53
+ # 当前请求信息
54
+ request.system.request.content.message.text = current_message
55
+ logger.debug("📝 创建单消息请求")
56
+ else:
57
+ # 有历史消息时,设置第一个消息组作为历史数据
58
+ if merged_groups:
59
+ first_content = merged_groups[0].get("content", "")
60
+ request.session.history.first_message_id = session_id
61
+ request.session.history.first_message_content = first_content
62
+
63
+ # 添加系统消息 (固定格式)
64
+ entry_a = request.session.history.entries.add()
65
+ entry_a.message_id = Utils.generate_request_id()
66
+ entry_a.system_msg.content_id = Utils.generate_request_id()
67
+ entry_a.system_msg.metadata.data = "IgIQAQ=="
68
+
69
+ # 处理剩余的消息组
70
+ for message in merged_groups:
71
+ if message.get("role", "") == "user":
72
+ # 添加用户消息
73
+ entry_b = request.session.history.entries.add()
74
+ entry_b.message_id = Utils.generate_request_id()
75
+ entry_b.user_msg.content = message.get("content", "")
76
+ elif message.get("role", "") == "assistant":
77
+ # 添加助手消息
78
+ entry_c = request.session.history.entries.add()
79
+ entry_c.message_id = Utils.generate_request_id()
80
+ entry_c.assistant_msg.content = message.get("content", "")
81
+
82
+ logger.debug(f"📚 创建多消息请求: {len(merged_groups)} 条历史消息")
83
+
84
+ # 字段1: 会话信息
85
+ request.session.session_id = session_id
86
+
87
+ # 字段2: 系统信息
88
+ request.system.environment.timestamp.time = Utils.get_current_timestamp()
89
+
90
+ # 当前请求信息
91
+ request.system.request.content.message.text = current_message
92
+
93
+ # 字段3: AI配置
94
+ request.ai_config.model.model_name = model
95
+ request.ai_config.model.variant = "o3"
96
+ request.ai_config.model.mode = "auto"
97
+ request.ai_config.priority_level_1 = 1
98
+ request.ai_config.priority_level_2 = 1
99
+ request.ai_config.priority_level_3 = 1
100
+ request.ai_config.priority_level_4 = 1
101
+ request.ai_config.config_data = bytes([0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x00, 0x02, 0x03, 0x01]).decode('utf-8')
102
+
103
+ serialized = request.SerializeToString()
104
+ logger.success(f"✅ 聊天请求protobuf数据序列化成功,大小: {len(serialized)} 字节")
105
+
106
+ return serialized
107
+
108
+ except Exception as e:
109
+ logger.error(f"❌ 创建聊天请求protobuf数据时出错: {e}")
110
+ import traceback
111
+ traceback.print_exc()
112
+ return None
113
+
114
+ @staticmethod
115
+ def create_login_request(login_url: str) -> Optional[bytes]:
116
+ """
117
+ 创建登录请求的protobuf数据
118
+
119
+ Args:
120
+ login_url: 登录URL
121
+
122
+ Returns:
123
+ 序列化的protobuf数据,如果失败返回None
124
+ """
125
+ try:
126
+ # 创建登录请求
127
+ request = warp_unified_pb2.LoginRequest()
128
+
129
+ # 设置请求标志
130
+ request.request_flags.extend([9, 18, 20, 21, 31, 35])
131
+ request.request_type = 2
132
+
133
+ # 设置登录URL
134
+ request.login_data.url = login_url
135
+
136
+ # 设置配置标志
137
+ request.config_flags.extend([3, 2])
138
+
139
+ # 设置语言
140
+ request.language = "zh-CN"
141
+
142
+ # 设置请求设置
143
+ request.settings.setting_value = 6
144
+
145
+ # 序列化
146
+ serialized = request.SerializeToString()
147
+ logger.debug(f"🔄 登录请求protobuf序列化成功,数据长度: {len(serialized)} 字节")
148
+ return serialized
149
+
150
+ except Exception as e:
151
+ logger.error(f"❌ 创建登录请求protobuf数据时出错: {e}")
152
+ return None
153
+
154
+ @staticmethod
155
+ def parse_chat_response(base64_data: str) -> str:
156
+ """
157
+ 解析聊天响应的protobuf数据
158
+
159
+ Args:
160
+ base64_data: base64编码的响应数据
161
+
162
+ Returns:
163
+ 解析后的文本内容
164
+ """
165
+ try:
166
+ response = warp_unified_pb2.ChatResponse()
167
+ b64_data = base64.urlsafe_b64decode(base64_data)
168
+ response.ParseFromString(b64_data)
169
+
170
+ # 尝试从主要响应中获取文本
171
+ if hasattr(response.metadata.content, 'primary') and response.metadata.content.primary.text.content.text:
172
+ text = response.metadata.content.primary.text.content.text
173
+ logger.debug(f"📥 解析主响应文本: {len(text)} 字符")
174
+ return text
175
+
176
+ # 尝试从备用响应中获取文本
177
+ if hasattr(response.metadata.content, 'secondary') and response.metadata.content.secondary.alternative.final_content.text:
178
+ text = response.metadata.content.secondary.alternative.final_content.text
179
+ logger.debug(f"📥 解析备用响应文本: {len(text)} 字符")
180
+ return text
181
+
182
+ logger.debug("⚠️ 响应中未找到文本内容")
183
+ return ""
184
+
185
+ except Exception as e:
186
+ logger.error(f"❌ 解析聊天响应protobuf数据时出错: {e}")
187
+ return ""
188
+
189
+ @staticmethod
190
+ def parse_login_response(response_content: bytes) -> str:
191
+ """
192
+ 解析登录响应的protobuf数据
193
+
194
+ Args:
195
+ response_content: 响应的二进制内容
196
+
197
+ Returns:
198
+ 解析后的登录URL
199
+ """
200
+ try:
201
+ import urllib.parse
202
+
203
+ parsed_data = warp_unified_pb2.LoginRequest()
204
+ parsed_data.ParseFromString(response_content)
205
+
206
+ # 提取嵌套的URL
207
+ parsed_data_url = parsed_data.url_chain.container_2.container_3.container_4.container_5.container_6.url_container.final_url.login_url
208
+ logger.info(f"🔄 解析后的URL: {parsed_data_url}")
209
+
210
+ decoded_url = urllib.parse.unquote(parsed_data_url)
211
+ logger.info(f"🔄 解码后的URL: {decoded_url}")
212
+
213
+ return decoded_url
214
+
215
+ except Exception as e:
216
+ logger.error(f"❌ 解析登录响应protobuf数据时出错: {e}")
217
+ return ""
218
+
219
+ @staticmethod
220
+ def get_protobuf_classes() -> Dict[str, Any]:
221
+ """
222
+ 获取所有可用的protobuf类
223
+
224
+ Returns:
225
+ 包含所有protobuf类的字典
226
+ """
227
+ return {
228
+ 'ChatRequest': warp_unified_pb2.ChatRequest,
229
+ 'ChatResponse': warp_unified_pb2.ChatResponse,
230
+ 'LoginRequest': warp_unified_pb2.LoginRequest,
231
+ 'SessionInfo': warp_unified_pb2.SessionInfo,
232
+ 'MessageHistory': warp_unified_pb2.MessageHistory,
233
+ 'MessageEntry': warp_unified_pb2.MessageEntry,
234
+ 'SystemInfo': warp_unified_pb2.SystemInfo,
235
+ 'AIConfig': warp_unified_pb2.AIConfig,
236
+ 'ModelConfig': warp_unified_pb2.ModelConfig,
237
+ 'LoginData': warp_unified_pb2.LoginData,
238
+ 'RequestSettings': warp_unified_pb2.RequestSettings,
239
+ }
240
+
241
+ @staticmethod
242
+ def create_empty_request(request_type: str) -> Optional[Any]:
243
+ """
244
+ 创建空的protobuf请求对象
245
+
246
+ Args:
247
+ request_type: 请求类型 ('chat' 或 'login')
248
+
249
+ Returns:
250
+ 对应的protobuf请求对象
251
+ """
252
+ try:
253
+ if request_type.lower() == 'chat':
254
+ return warp_unified_pb2.ChatRequest()
255
+ elif request_type.lower() == 'login':
256
+ return warp_unified_pb2.LoginRequest()
257
+ else:
258
+ logger.error(f"❌ 不支持的请求类型: {request_type}")
259
+ return None
260
+ except Exception as e:
261
+ logger.error(f"❌ 创建空请求对象时出错: {e}")
262
+ return None
263
+
264
+ @staticmethod
265
+ def validate_protobuf_module() -> bool:
266
+ """
267
+ 验证protobuf模块是否正确加载
268
+
269
+ Returns:
270
+ 如果模块正确加载返回True,否则返回False
271
+ """
272
+ try:
273
+ # 测试创建基本对象
274
+ chat_request = warp_unified_pb2.ChatRequest()
275
+ login_request = warp_unified_pb2.LoginRequest()
276
+
277
+ # 测试基本序列化
278
+ chat_request.session.session_id = "test"
279
+ serialized = chat_request.SerializeToString()
280
+
281
+ if len(serialized) > 0:
282
+ logger.success("✅ Protobuf模块验证通过")
283
+ return True
284
+ else:
285
+ logger.error("❌ Protobuf模块验证失败:序列化结果为空")
286
+ return False
287
+
288
+ except Exception as e:
289
+ logger.error(f"❌ Protobuf模块验证失败: {e}")
290
+ return False
291
+
292
+
293
+ # 模块级别的便捷函数
294
+ def create_chat_protobuf(messages: List[Dict], model: str = "gemini-2.0-flash") -> Optional[bytes]:
295
+ """便捷函数:创建聊天请求protobuf数据"""
296
+ return ProtobufManager.create_chat_request(messages, model)
297
+
298
+
299
+ def create_login_protobuf(login_url: str) -> Optional[bytes]:
300
+ """便捷函数:创建登录请求protobuf数据"""
301
+ return ProtobufManager.create_login_request(login_url)
302
+
303
+
304
+ def parse_chat_protobuf(base64_data: str) -> str:
305
+ """便捷函数:解析聊天响应protobuf数据"""
306
+ return ProtobufManager.parse_chat_response(base64_data)
307
+
308
+
309
+ def parse_login_protobuf(response_content: bytes) -> str:
310
+ """便捷函数:解析登录响应protobuf数据"""
311
+ return ProtobufManager.parse_login_response(response_content)
312
+
313
+
314
+ # 在模块加载时验证protobuf模块
315
+ if __name__ != "__main__":
316
+ if not ProtobufManager.validate_protobuf_module():
317
+ logger.warning("⚠️ Protobuf模块验证失败,某些功能可能不可用")
request_converter.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 请求转换器模块
3
+ 处理OpenAI格式和Warp格式之间的转换
4
+ """
5
+ from typing import List, Dict, Any, Optional, Tuple
6
+ from dataclasses import dataclass
7
+
8
+ from utils import Utils
9
+
10
+
11
+ @dataclass
12
+ class OpenAIMessage:
13
+ """OpenAI消息格式"""
14
+ role: str
15
+ content: str
16
+
17
+
18
+ @dataclass
19
+ class OpenAIRequest:
20
+ """OpenAI请求格式"""
21
+ model: str
22
+ messages: List[OpenAIMessage]
23
+ stream: bool = False
24
+ temperature: float = 1.0
25
+ max_tokens: Optional[int] = None
26
+
27
+
28
+ @dataclass
29
+ class OpenAIChoice:
30
+ """OpenAI响应选择"""
31
+ index: int
32
+ message: Optional[OpenAIMessage] = None
33
+ delta: Optional[Dict[str, Any]] = None
34
+ finish_reason: Optional[str] = None
35
+
36
+
37
+ @dataclass
38
+ class OpenAIResponse:
39
+ """OpenAI响应格式"""
40
+ id: str
41
+ object: str
42
+ created: int
43
+ model: str
44
+ choices: List[OpenAIChoice]
45
+ usage: Optional[Dict[str, int]] = None
46
+
47
+
48
+ class RequestConverter:
49
+ """OpenAI请求到Warp请求的转换器"""
50
+
51
+ @staticmethod
52
+ def parse_openai_request(request_data: dict) -> OpenAIRequest:
53
+ """解析OpenAI请求格式"""
54
+ messages = []
55
+ for msg in request_data.get("messages", []):
56
+ content = msg.get("content", "")
57
+
58
+ # 处理可能的列表格式内容
59
+ if isinstance(content, list):
60
+ text_content = ""
61
+ for item in content:
62
+ if isinstance(item, dict) and item.get("type") == "text":
63
+ text_content += item.get("text", "")
64
+ content = text_content
65
+
66
+ messages.append(OpenAIMessage(
67
+ role=msg.get("role", "user"),
68
+ content=str(content)
69
+ ))
70
+
71
+ return OpenAIRequest(
72
+ model=request_data.get("model", "gemini-2.0-flash"),
73
+ messages=messages,
74
+ stream=request_data.get("stream", False),
75
+ temperature=request_data.get("temperature", 1.0),
76
+ max_tokens=request_data.get("max_tokens")
77
+ )
78
+
79
+ @staticmethod
80
+ def process_message_history(messages: List[Dict]) -> Tuple[List[Dict], str]:
81
+ """处理消息历史,返回(合并后的消息组列表, 当前消息)"""
82
+ if not messages:
83
+ return [], "Hello"
84
+
85
+ # 确保最后一句话是user
86
+ if messages[-1].role != "user":
87
+ raise ValueError("最后一句话必须是user消息")
88
+
89
+ # 当前消息(最后一句)
90
+ current_message = messages[-1].content
91
+
92
+ if len(messages) == 1:
93
+ return [], current_message
94
+
95
+ # 处理除最后一句之外的所有消息,合并连续相同的role
96
+ merged_groups = []
97
+ i = 0
98
+
99
+ while i < len(messages) - 1: # 排除最后一句
100
+ current_role = messages[i].role
101
+
102
+ # 如果是system,转换为user
103
+ if current_role == "system":
104
+ current_role = "user"
105
+
106
+ # 收集当前内容
107
+ current_content = messages[i].content
108
+
109
+ # 合并连续相同的role
110
+ j = i + 1
111
+ while j < len(messages) - 1: # 排除最后一句
112
+ next_role = messages[j].role
113
+ if next_role == "system":
114
+ next_role = "user"
115
+
116
+ if next_role == current_role:
117
+ # 合并内容
118
+ current_content += "\n" + messages[j].content
119
+ j += 1
120
+ else:
121
+ break
122
+
123
+ # 添加合并后的消息组
124
+ merged_groups.append({"role": current_role, "content": current_content})
125
+ i = j
126
+
127
+ return merged_groups, current_message
128
+
129
+ @staticmethod
130
+ def create_openai_response(content: str, model: str, request_id: str,
131
+ is_stream: bool = False, finish_reason: str = None) -> dict:
132
+ """创建OpenAI格式的响应"""
133
+ if is_stream:
134
+ choice = {
135
+ "index": 0,
136
+ "delta": {"content": content} if content else {},
137
+ "finish_reason": finish_reason
138
+ }
139
+
140
+ return {
141
+ "id": request_id,
142
+ "object": "chat.completion.chunk",
143
+ "created": Utils.get_current_timestamp(),
144
+ "model": model,
145
+ "choices": [choice]
146
+ }
147
+ else:
148
+ choice = {
149
+ "index": 0,
150
+ "message": {
151
+ "role": "assistant",
152
+ "content": content
153
+ },
154
+ "finish_reason": "stop"
155
+ }
156
+
157
+ return {
158
+ "id": request_id,
159
+ "object": "chat.completion",
160
+ "created": Utils.get_current_timestamp(),
161
+ "model": model,
162
+ "choices": [choice],
163
+ "usage": {
164
+ "prompt_tokens": 100,
165
+ "completion_tokens": len(content.split()) if content else 0,
166
+ "total_tokens": 100 + (len(content.split()) if content else 0)
167
+ }
168
+ }
169
+
170
+ @staticmethod
171
+ def create_error_response(error_message: str, model: str, request_id: str = None) -> dict:
172
+ """创建错误响应"""
173
+ if not request_id:
174
+ request_id = Utils.generate_request_id()
175
+
176
+ return {
177
+ "id": request_id,
178
+ "object": "error",
179
+ "created": Utils.get_current_timestamp(),
180
+ "model": model,
181
+ "error": {
182
+ "message": error_message,
183
+ "type": "invalid_request_error"
184
+ }
185
+ }
token_manager.py ADDED
@@ -0,0 +1,344 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 多Token管理器模块
3
+ 支持多个refresh token的管理、负载均衡和自动刷新
4
+ """
5
+ import time
6
+ import threading
7
+ import requests
8
+ from typing import List, Optional, Dict, Any
9
+ from dataclasses import dataclass
10
+ from collections import defaultdict
11
+ from concurrent.futures import ThreadPoolExecutor, as_completed
12
+ from loguru import logger
13
+
14
+ from config import Config
15
+ from utils import Utils
16
+
17
+
18
+ @dataclass
19
+ class TokenInfo:
20
+ """Token信息数据类"""
21
+ refresh_token: str
22
+ token_id: str # token标识符
23
+ access_token: Optional[str] = None
24
+ last_refresh_time: Optional[float] = None
25
+ refresh_count: int = 0
26
+ error_count: int = 0
27
+ is_active: bool = True
28
+
29
+
30
+ class MultiTokenManager:
31
+ """多Token管理器,支持负载均衡和自动刷新"""
32
+
33
+ def __init__(self):
34
+ # Token存储
35
+ self.tokens: Dict[str, TokenInfo] = {}
36
+ self.token_lock = threading.RLock()
37
+
38
+ # 负载均衡相关
39
+ self.current_index = 0
40
+ self.usage_count = defaultdict(int)
41
+
42
+ # 自动刷新相关
43
+ self.refresh_timer = None
44
+ self.refresh_executor = ThreadPoolExecutor(max_workers=5, thread_name_prefix="token-refresh")
45
+ self.session = requests.Session()
46
+
47
+ # 初始化
48
+ self._initialize_tokens()
49
+
50
+ def _initialize_tokens(self):
51
+ """初始化token列表"""
52
+ # 从环境变量获取refresh token列表(支持分号分割)
53
+ env_tokens = Config.get_refresh_tokens()
54
+
55
+ if env_tokens:
56
+ # 过滤有效的token
57
+ valid_tokens = [token for token in env_tokens if Utils.validate_refresh_token_format(token)]
58
+
59
+ if valid_tokens:
60
+ self.add_refresh_tokens(valid_tokens)
61
+ logger.info(f"✅ 从环境变量初始化了 {len(valid_tokens)} 个有效token")
62
+
63
+ if len(env_tokens) > len(valid_tokens):
64
+ invalid_count = len(env_tokens) - len(valid_tokens)
65
+ logger.warning(f"⚠️ 跳过了 {invalid_count} 个格式无效的token")
66
+ else:
67
+ logger.warning("⚠️ 环境变量中的所有token格式都无效")
68
+ else:
69
+ logger.warning("⚠️ 未找到有效的refresh token,请通过环境变量 WARP_REFRESH_TOKEN 设置或在管理界面添加")
70
+ logger.info("💡 环境变量支持多个token,使用分号(;)分割:token1;token2;token3")
71
+
72
+ def add_refresh_token(self, refresh_token: str) -> bool:
73
+ """添加单个refresh token"""
74
+ return self.add_refresh_tokens([refresh_token])
75
+
76
+ def add_refresh_tokens(self, refresh_tokens: List[str]) -> bool:
77
+ """添加多个refresh token"""
78
+ with self.token_lock:
79
+ added_count = 0
80
+
81
+ for token in refresh_tokens:
82
+ if not Utils.validate_refresh_token_format(token):
83
+ logger.warning(f"⚠️ Token格式无效: {token[:20]}...")
84
+ continue
85
+
86
+ if token in self.tokens:
87
+ logger.info(f"⚠️ Token已存在,跳过: {self.tokens[token].token_id}")
88
+ continue
89
+
90
+ token_id = Utils.generate_token_id(token)
91
+ self.tokens[token] = TokenInfo(
92
+ refresh_token=token,
93
+ token_id=token_id
94
+ )
95
+ added_count += 1
96
+ logger.info(f"✅ 添加新token: {token_id}")
97
+
98
+ logger.info(f"📊 成功添加 {added_count} 个新token,当前总数: {len(self.tokens)}")
99
+ return added_count > 0
100
+
101
+ def remove_refresh_token(self, refresh_token: str) -> bool:
102
+ """移除refresh token"""
103
+ with self.token_lock:
104
+ if refresh_token in self.tokens:
105
+ token_id = self.tokens[refresh_token].token_id
106
+ del self.tokens[refresh_token]
107
+ logger.info(f"🗑️ 移除token: {token_id}")
108
+ return True
109
+ return False
110
+
111
+ def get_refresh_token_from_env(self, refresh_token: str) -> bool:
112
+ """检查token是否来自环境变量"""
113
+ env_tokens = Config.get_refresh_tokens()
114
+ return refresh_token in env_tokens
115
+
116
+ def remove_duplicate_tokens(self, new_tokens: List[str]) -> List[str]:
117
+ """移除重复的token,优先保留环境变量中的token"""
118
+ with self.token_lock:
119
+ unique_tokens = []
120
+
121
+ for token in new_tokens:
122
+ # 检查是否与现有token重复
123
+ if token in self.tokens:
124
+ # 如果现有token来自环境变量,跳过新token
125
+ if self.get_refresh_token_from_env(token):
126
+ logger.info(f"⚠️ 跳过重复token(环境变量优先): {self.tokens[token].token_id}")
127
+ continue
128
+ else:
129
+ # 如果新token优先级更高,移除旧token
130
+ self.remove_refresh_token(token)
131
+
132
+ unique_tokens.append(token)
133
+
134
+ return unique_tokens
135
+
136
+ def get_access_token(self, refresh_token: str) -> Optional[str]:
137
+ """通过refresh token获取新的access token"""
138
+ token_id = self.tokens.get(refresh_token, TokenInfo("", "")).token_id
139
+
140
+ try:
141
+ headers = {
142
+ 'Accept-Encoding': 'gzip, br',
143
+ 'Content-Type': 'application/x-www-form-urlencoded',
144
+ 'x-warp-client-version': Config.WARP_CLIENT_VERSION,
145
+ 'x-warp-os-category': Config.WARP_OS_CATEGORY,
146
+ 'x-warp-os-name': Config.WARP_OS_NAME,
147
+ 'x-warp-os-version': Config.WARP_OS_VERSION
148
+ }
149
+
150
+ data = {
151
+ 'grant_type': 'refresh_token',
152
+ 'refresh_token': refresh_token
153
+ }
154
+
155
+ logger.debug(f"🔄 正在刷新access token: {token_id}")
156
+
157
+ response = self.session.post(
158
+ Config.GOOGLE_TOKEN_URL,
159
+ headers=headers,
160
+ data=data,
161
+ timeout=30
162
+ )
163
+
164
+ if response.status_code == 200:
165
+ token_data = response.json()
166
+ access_token = token_data.get('access_token')
167
+ if access_token:
168
+ logger.success(f"✅ Access token获取成功: {token_id}")
169
+ return access_token
170
+ else:
171
+ logger.error(f"❌ 响应中未找到access_token字段: {token_id}")
172
+ else:
173
+ logger.error(f"❌ 获取token失败,状态码 {response.status_code}: {token_id}")
174
+
175
+ except Exception as e:
176
+ logger.error(f"❌ 获取access token时出错: {token_id} - {e}")
177
+
178
+ return None
179
+
180
+ def refresh_single_token(self, refresh_token: str) -> bool:
181
+ """刷新单个token"""
182
+ with self.token_lock:
183
+ if refresh_token not in self.tokens:
184
+ return False
185
+
186
+ token_info = self.tokens[refresh_token]
187
+
188
+ try:
189
+ access_token = self.get_access_token(refresh_token)
190
+
191
+ with self.token_lock:
192
+ if access_token:
193
+ token_info.access_token = access_token
194
+ token_info.last_refresh_time = time.time()
195
+ token_info.refresh_count += 1
196
+ token_info.error_count = 0 # 重置错误计数
197
+ token_info.is_active = True
198
+
199
+ logger.success(f"🔄 Token刷新成功: {token_info.token_id} (第{token_info.refresh_count}次)")
200
+ return True
201
+ else:
202
+ token_info.error_count += 1
203
+ if token_info.error_count >= 3:
204
+ token_info.is_active = False
205
+ logger.warning(f"⚠️ Token连续失败3次,标记为不可用: {token_info.token_id}")
206
+
207
+ return False
208
+
209
+ except Exception as e:
210
+ with self.token_lock:
211
+ token_info.error_count += 1
212
+
213
+ logger.error(f"❌ 刷新token时出错: {token_info.token_id} - {e}")
214
+ return False
215
+
216
+ def refresh_all_tokens(self):
217
+ """并发刷新所有token"""
218
+ with self.token_lock:
219
+ refresh_tokens = list(self.tokens.keys())
220
+
221
+ if not refresh_tokens:
222
+ logger.warning("⚠️ 没有可刷新的token")
223
+ return
224
+
225
+ logger.info(f"🔄 开始并发刷新 {len(refresh_tokens)} 个token")
226
+
227
+ # 使用线程池并发刷新
228
+ futures = {
229
+ self.refresh_executor.submit(self.refresh_single_token, token): token
230
+ for token in refresh_tokens
231
+ }
232
+
233
+ success_count = 0
234
+ for future in as_completed(futures):
235
+ token = futures[future]
236
+ try:
237
+ success = future.result(timeout=30)
238
+ if success:
239
+ success_count += 1
240
+ except Exception as e:
241
+ token_id = self.tokens.get(token, TokenInfo("", "")).token_id
242
+ logger.error(f"❌ 刷新token时出错: {token_id} - {e}")
243
+
244
+ logger.info(f"📊 Token刷新完成: {success_count}/{len(refresh_tokens)} 成功")
245
+
246
+ def get_current_access_token(self) -> Optional[str]:
247
+ """获取当前可用的access token(负��均衡)"""
248
+ with self.token_lock:
249
+ # 获取所有活跃token
250
+ active_tokens = [
251
+ (token, info) for token, info in self.tokens.items()
252
+ if info.is_active and info.access_token
253
+ ]
254
+
255
+ if not active_tokens:
256
+ logger.warning("⚠️ 没有可用的access token!")
257
+ logger.warning("💡 请确保:")
258
+ logger.warning(" 1. 设置了有效的 WARP_REFRESH_TOKEN 环境变量")
259
+ logger.warning(" 2. 或通过管理界面添加有效的 refresh token")
260
+ logger.warning(" 3. refresh token 已成功刷新获得 access token")
261
+ return None
262
+
263
+ # 简单的轮询负载均衡
264
+ if self.current_index >= len(active_tokens):
265
+ self.current_index = 0
266
+
267
+ selected_token, token_info = active_tokens[self.current_index]
268
+ self.current_index = (self.current_index + 1) % len(active_tokens)
269
+
270
+ # 更新使用计数
271
+ self.usage_count[selected_token] += 1
272
+
273
+ logger.info(f"🎯 轮询选择token: {token_info.token_id} (使用次数: {self.usage_count[selected_token]})")
274
+ return token_info.access_token
275
+
276
+ def get_token_status(self) -> Dict[str, Any]:
277
+ """获取所有token的状态信息"""
278
+ with self.token_lock:
279
+ status = {
280
+ 'total_tokens': len(self.tokens),
281
+ 'active_tokens': sum(1 for info in self.tokens.values() if info.is_active),
282
+ 'tokens_with_access': sum(1 for info in self.tokens.values() if info.access_token),
283
+ 'tokens': []
284
+ }
285
+
286
+ for token, info in self.tokens.items():
287
+ token_status = {
288
+ 'refresh_token': info.token_id, # 使用token_id而不是实际token
289
+ 'has_access_token': bool(info.access_token),
290
+ 'is_active': info.is_active,
291
+ 'refresh_count': info.refresh_count,
292
+ 'error_count': info.error_count,
293
+ 'last_refresh_time': info.last_refresh_time,
294
+ 'usage_count': self.usage_count.get(token, 0)
295
+ }
296
+ status['tokens'].append(token_status)
297
+
298
+ return status
299
+
300
+ def start_auto_refresh(self):
301
+ """启动自动刷新服务"""
302
+ logger.info(f"🚀 启动Token自动刷新服务,间隔: {Config.TOKEN_REFRESH_INTERVAL // 60} 分钟")
303
+
304
+ # 立即进行一次刷新
305
+ refresh_thread = threading.Thread(target=self._initial_refresh, daemon=True)
306
+ refresh_thread.start()
307
+
308
+ def _initial_refresh(self):
309
+ """初始刷新(在后台线程中执行)"""
310
+ logger.info("🔄 正在后台执行初始token刷新...")
311
+ self.refresh_all_tokens()
312
+ self._schedule_next_refresh()
313
+
314
+ def _schedule_next_refresh(self):
315
+ """安排下次刷新"""
316
+ if self.refresh_timer:
317
+ self.refresh_timer.cancel()
318
+
319
+ self.refresh_timer = threading.Timer(
320
+ Config.TOKEN_REFRESH_INTERVAL,
321
+ self._auto_refresh_callback
322
+ )
323
+ self.refresh_timer.daemon = True
324
+ self.refresh_timer.start()
325
+
326
+ # 计算下次刷新时间
327
+ next_refresh_time = time.time() + Config.TOKEN_REFRESH_INTERVAL
328
+ next_refresh_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(next_refresh_time))
329
+ logger.info(f"⏰ 下次token刷新时间: {next_refresh_str}")
330
+
331
+ def _auto_refresh_callback(self):
332
+ """自动刷新回调"""
333
+ logger.info("🔄 开始定时自动刷新所有token...")
334
+ self.refresh_all_tokens()
335
+ self._schedule_next_refresh()
336
+
337
+ def stop_auto_refresh(self):
338
+ """停止自动刷新"""
339
+ if self.refresh_timer:
340
+ self.refresh_timer.cancel()
341
+ self.refresh_timer = None
342
+
343
+ self.refresh_executor.shutdown(wait=False)
344
+ logger.info("⏹️ Token自动刷新服务已停止")
utils.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 工具函数模块
3
+ 包含通用的工具函数和辅助方法
4
+ """
5
+ import uuid
6
+ import time
7
+ import logging
8
+ from typing import Optional
9
+ from loguru import logger
10
+ from config import Config
11
+
12
+
13
+ class Utils:
14
+ """工具函数集合"""
15
+
16
+ @staticmethod
17
+ def generate_request_id() -> str:
18
+ """生成请求ID"""
19
+ return str(uuid.uuid4())
20
+
21
+ @staticmethod
22
+ def generate_token_id(refresh_token: str) -> str:
23
+ """生成token的唯一标识符(用于日志显示)"""
24
+ # 使用token的前8位和后8位作为标识符
25
+ if len(refresh_token) >= 16:
26
+ return f"TOKEN_{refresh_token[:8]}...{refresh_token[-8:]}"
27
+ else:
28
+ return f"TOKEN_{refresh_token[:8]}..."
29
+
30
+ @staticmethod
31
+ def get_current_timestamp() -> int:
32
+ """获取当前时间戳"""
33
+ return int(time.time())
34
+
35
+ @staticmethod
36
+ def validate_api_key(api_key: str) -> bool:
37
+ """验证API密钥"""
38
+ config = Config()
39
+ return api_key in config.VALID_API_KEYS
40
+
41
+ @staticmethod
42
+ def extract_bearer_token(auth_header: str) -> Optional[str]:
43
+ """从Authorization头中提取Bearer token"""
44
+ if not auth_header:
45
+ return None
46
+
47
+ parts = auth_header.split(" ")
48
+ if len(parts) != 2 or parts[0].lower() != "bearer":
49
+ return None
50
+
51
+ return parts[1]
52
+
53
+ @staticmethod
54
+ def setup_logging():
55
+ """设置loguru日志配置"""
56
+ # 移除默认的日志处理器
57
+ logger.remove()
58
+
59
+ # 添加控制台日志输出,带颜色和格式
60
+ logger.add(
61
+ sink=lambda x: print(x, end=""),
62
+ format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
63
+ level="INFO",
64
+ colorize=True
65
+ )
66
+
67
+ # 根据环境变量决定是否启用文件日志
68
+ if Config.enable_file_logging():
69
+ logger.add(
70
+ "warp_api_server.log",
71
+ rotation="10 MB",
72
+ retention="7 days",
73
+ level="DEBUG",
74
+ format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
75
+ encoding="utf-8"
76
+ )
77
+ logger.info("📝 文件日志已启用: warp_api_server.log")
78
+ else:
79
+ logger.info("📝 文件日志已禁用 (可通过环境变量 ENABLE_FILE_LOGGING=true 启用)")
80
+
81
+ # 配置标准logging的loguru处理器
82
+ class InterceptHandler(logging.Handler):
83
+ def emit(self, record):
84
+ # 获取对应的loguru级别
85
+ try:
86
+ level = logger.level(record.levelname).name
87
+ except ValueError:
88
+ level = record.levelno
89
+
90
+ # 查找调用者位置
91
+ frame, depth = logging.currentframe(), 2
92
+ while frame.f_code.co_filename == logging.__file__:
93
+ frame = frame.f_back
94
+ depth += 1
95
+
96
+ logger.opt(depth=depth, exception=record.exc_info).log(
97
+ level, record.getMessage()
98
+ )
99
+
100
+ # 替换所有标准logging处理器
101
+ logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
102
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
103
+
104
+ logger.info("🚀 Loguru日志系统初始化完成")
105
+
106
+ @staticmethod
107
+ def deduplicate_tokens(tokens: list) -> list:
108
+ """去重token列表,保持顺序"""
109
+ seen = set()
110
+ result = []
111
+ for token in tokens:
112
+ if token and token not in seen:
113
+ seen.add(token)
114
+ result.append(token)
115
+ return result
116
+
117
+ @staticmethod
118
+ def validate_refresh_token_format(token: str) -> bool:
119
+ """验证refresh token格式是否有效"""
120
+ if not token or not isinstance(token, str):
121
+ return False
122
+
123
+ # 基本长度检查
124
+ if len(token) < 100:
125
+ return False
126
+
127
+ # 检查是否包含必要的字符
128
+ if not any(c.isalnum() for c in token):
129
+ return False
130
+
131
+ return True
warp_api_server.py ADDED
@@ -0,0 +1,341 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Warp API 服务器
4
+ 整合所有模块,提供OpenAI兼容的API接口和Token管理Web界面
5
+ """
6
+ import json
7
+ import threading
8
+ from flask import Flask, request, jsonify, Response, stream_with_context, render_template_string, session, redirect, url_for
9
+ from loguru import logger
10
+
11
+ from config import Config
12
+ from utils import Utils
13
+ from api_service import ApiService
14
+
15
+ # 初始化日志
16
+ Utils.setup_logging()
17
+
18
+ # 创建Flask应用
19
+ app = Flask(__name__)
20
+ app.secret_key = 'warp-api-server-secret-key-2024' # 用于session管理
21
+
22
+ # 创建API服务
23
+ api_service = ApiService()
24
+
25
+ def check_admin_auth():
26
+ """检查管理员认证"""
27
+ if not Config.require_admin_auth():
28
+ return True
29
+
30
+ # 检查session中的管理员认证状态
31
+ return session.get('admin_authenticated', False)
32
+
33
+ def verify_admin_key(admin_key: str) -> bool:
34
+ """验证管理员密钥"""
35
+ if not Config.require_admin_auth():
36
+ return True
37
+
38
+ return admin_key == Config.get_admin_key()
39
+
40
+ @app.before_request
41
+ def check_auth():
42
+ """请求前鉴权检查"""
43
+ # 跳过管理员认证的端点
44
+ admin_auth_endpoints = ['admin_login', 'admin_auth']
45
+ if request.endpoint in admin_auth_endpoints:
46
+ return
47
+
48
+ # 管理界面相关端点需要管理员认证
49
+ admin_endpoints = ['index', 'token_status', 'add_tokens', 'batch_login', 'refresh_tokens', 'remove_tokens', 'export_tokens']
50
+ if request.endpoint in admin_endpoints:
51
+ if not check_admin_auth():
52
+ if request.endpoint == 'index':
53
+ return redirect(url_for('admin_login'))
54
+ else:
55
+ return jsonify({"error": "需要管理员认证"}), 401
56
+
57
+ # API端点需要API密钥认证
58
+ api_endpoints = ['get_models', 'chat_completions']
59
+ if request.endpoint in api_endpoints:
60
+ auth_header = request.headers.get('Authorization')
61
+ if not api_service.authenticate_request(auth_header):
62
+ return jsonify({"error": "未授权访问"}), 401
63
+
64
+ @app.route('/admin/login')
65
+ def admin_login():
66
+ """管理员登录页面"""
67
+ if not Config.require_admin_auth():
68
+ return redirect(url_for('index'))
69
+
70
+ if check_admin_auth():
71
+ return redirect(url_for('index'))
72
+
73
+ from web_template import get_admin_login_template
74
+ return render_template_string(get_admin_login_template())
75
+
76
+ @app.route('/admin/auth', methods=['POST'])
77
+ def admin_auth():
78
+ """管理员认证处理"""
79
+ if not Config.require_admin_auth():
80
+ return jsonify({"success": True, "redirect": "/"})
81
+
82
+ try:
83
+ data = request.get_json()
84
+ admin_key = data.get('admin_key', '')
85
+
86
+ if verify_admin_key(admin_key):
87
+ session['admin_authenticated'] = True
88
+ logger.info("🔐 管理员认证成功")
89
+ return jsonify({"success": True, "message": "认证成功", "redirect": "/"})
90
+ else:
91
+ logger.warning("⚠️ 管理员认证失败")
92
+ return jsonify({"success": False, "message": "管理员密钥错误"})
93
+
94
+ except Exception as e:
95
+ logger.error(f"❌ 管理员认证出错: {e}")
96
+ return jsonify({"success": False, "message": "认证失败"}), 500
97
+
98
+ @app.route('/admin/logout', methods=['POST'])
99
+ def admin_logout():
100
+ """管理员登出"""
101
+ session.pop('admin_authenticated', None)
102
+ return jsonify({"success": True, "message": "已登出"})
103
+
104
+ @app.route('/')
105
+ def index():
106
+ """管理页面"""
107
+ from web_template import get_html_template
108
+ return render_template_string(get_html_template())
109
+
110
+ @app.route(f"/{Config.OPENAI_API_VERSION}/models", methods=['GET'])
111
+ def get_models():
112
+ """获取模型列表"""
113
+ return jsonify(api_service.get_models())
114
+
115
+ @app.route(f"/{Config.OPENAI_API_VERSION}/chat/completions", methods=['POST'])
116
+ def chat_completions():
117
+ """聊天完成端点"""
118
+ try:
119
+ request_data = request.get_json()
120
+ if not request_data:
121
+ return jsonify({"error": "无效的请求数据"}), 400
122
+
123
+ # 检查模型是否有效
124
+ model = request_data.get("model")
125
+ from model_mapper import ModelMapper
126
+ if not ModelMapper.is_valid_model(model):
127
+ return jsonify({"error": f"不支持的模型: {model}"}), 400
128
+
129
+ stream = request_data.get("stream", False)
130
+
131
+ if stream:
132
+ # 流式响应
133
+ def generate():
134
+ for chunk in api_service.chat_completion(request_data, stream=True):
135
+ yield chunk
136
+
137
+ return Response(
138
+ stream_with_context(generate()),
139
+ content_type='text/event-stream',
140
+ headers={
141
+ 'Cache-Control': 'no-cache',
142
+ 'Connection': 'keep-alive',
143
+ 'Access-Control-Allow-Origin': '*'
144
+ }
145
+ )
146
+ else:
147
+ # 非流式响应
148
+ response_text = ""
149
+ for chunk in api_service.chat_completion(request_data, stream=False):
150
+ response_text = chunk
151
+ break
152
+
153
+ return Response(
154
+ json.dumps(response_text) if isinstance(response_text, dict) else response_text,
155
+ content_type='application/json'
156
+ )
157
+
158
+ except Exception as e:
159
+ logger.error(f"❌ 处理聊天请求时出错: {e}")
160
+ return jsonify({"error": f"服务器内部错误: {str(e)}"}), 500
161
+
162
+ @app.route('/token/status', methods=['GET'])
163
+ def token_status():
164
+ """获取token状态"""
165
+ try:
166
+ status = api_service.get_token_status()
167
+ return jsonify(status)
168
+ except Exception as e:
169
+ return jsonify({"success": False, "message": str(e)}), 500
170
+
171
+ @app.route('/token/add', methods=['POST'])
172
+ def add_tokens():
173
+ """添加token"""
174
+ try:
175
+ data = request.get_json()
176
+ tokens = data.get('tokens', [])
177
+
178
+ result = api_service.add_tokens(tokens)
179
+ return jsonify(result)
180
+
181
+ except Exception as e:
182
+ return jsonify({"success": False, "message": str(e)}), 500
183
+
184
+ @app.route('/token/remove', methods=['POST'])
185
+ def remove_tokens():
186
+ """删除token"""
187
+ try:
188
+ data = request.get_json()
189
+ refresh_token = data.get('refresh_token')
190
+
191
+ result = api_service.remove_refresh_token(refresh_token=refresh_token)
192
+ return jsonify(result)
193
+
194
+ except Exception as e:
195
+ return jsonify({"success": False, "message": str(e)}), 500
196
+
197
+ @app.route('/token/refresh', methods=['POST'])
198
+ def refresh_tokens():
199
+ """刷新所有token"""
200
+ try:
201
+ result = api_service.refresh_all_tokens()
202
+ return jsonify(result)
203
+ except Exception as e:
204
+ return jsonify({"success": False, "message": str(e)}), 500
205
+
206
+ @app.route('/token/export', methods=['POST'])
207
+ def export_tokens():
208
+ """导出refresh token(需要超级管理员密钥验证)"""
209
+ try:
210
+ data = request.get_json()
211
+ super_admin_key = data.get('super_admin_key', '')
212
+
213
+ result = api_service.export_refresh_tokens(super_admin_key)
214
+ return jsonify(result)
215
+
216
+ except Exception as e:
217
+ logger.error(f"❌ 导出token时出错: {e}")
218
+ return jsonify({"success": False, "message": str(e)}), 500
219
+
220
+ @app.route('/login/batch', methods=['POST'])
221
+ def batch_login():
222
+ """批量获取refresh token"""
223
+ try:
224
+ data = request.get_json()
225
+ email_url_pairs = data.get('email_url_pairs', [])
226
+ max_workers = data.get('max_workers', 5)
227
+
228
+ # 转换为字典格式
229
+ email_url_dict = {}
230
+ for pair in email_url_pairs:
231
+ email = pair.get('email', '').strip()
232
+ url = pair.get('url', '').strip()
233
+
234
+ if email and url:
235
+ email_url_dict[email] = url
236
+
237
+ if not email_url_dict:
238
+ return jsonify({'success': False, 'message': '没有有效的邮箱和URL对'}), 400
239
+
240
+ result = api_service.batch_get_refresh_tokens(email_url_dict, max_workers)
241
+ return jsonify(result)
242
+
243
+ except Exception as e:
244
+ logger.error(f"❌ 批量登录时出错: {e}")
245
+ return jsonify({'success': False, 'message': f'服务器错误: {str(e)}'}), 500
246
+
247
+ @app.route('/health', methods=['GET'])
248
+ def health_check():
249
+ """健康检查端点"""
250
+ return jsonify({
251
+ "status": "healthy",
252
+ "timestamp": Utils.get_current_timestamp(),
253
+ "version": "2.0.0"
254
+ })
255
+
256
+ def compile_protobuf_at_startup():
257
+ """启动时编译protobuf"""
258
+ logger.info("🔧 开始编译Protobuf文件...")
259
+ try:
260
+ from protobuf_manager import ProtobufManager
261
+ success = ProtobufManager.validate_protobuf_module()
262
+ if success:
263
+ logger.success("✅ Protobuf模块验证成功")
264
+ else:
265
+ logger.warning("⚠️ Protobuf模块验证失败,但服务仍将启动")
266
+ except Exception as e:
267
+ logger.error(f"❌ 编译Protobuf时出错: {e}")
268
+
269
+ def main():
270
+ """主函数"""
271
+ config = Config()
272
+
273
+ print("=" * 80)
274
+ print("🚀 启动 Warp API 服务器 v2.0")
275
+ print("=" * 80)
276
+ print(f"📡 服务器地址: http://localhost:{Config.SERVER_PORT}")
277
+ print(f"🔧 管理界面: http://localhost:{Config.SERVER_PORT}")
278
+ print(f"📚 模型端点: http://localhost:{Config.SERVER_PORT}/{Config.OPENAI_API_VERSION}/models")
279
+ print(f"💬 聊天端点: http://localhost:{Config.SERVER_PORT}/{Config.OPENAI_API_VERSION}/chat/completions")
280
+ print(f"❤️ 健康检查: http://localhost:{Config.SERVER_PORT}/health")
281
+ print(f"📝 文件日志: {'启��' if Config.enable_file_logging() else '禁用'}")
282
+
283
+ # 显示安全配置信息
284
+ if Config.require_admin_auth():
285
+ print(f"🔐 管理员认证: 已启用 (需要 ADMIN_KEY)")
286
+ else:
287
+ print("⚠️ 管理员认证: 未启用 (可通过 ADMIN_KEY 环境变量启用)")
288
+
289
+ if Config.require_super_admin_auth():
290
+ print(f"🔒 超级管理员认证: 已启用 (导出功能需要 SUPER_ADMIN_KEY)")
291
+ else:
292
+ print("⚠️ 超级管理员认证: 未启用 (可通过 SUPER_ADMIN_KEY 环境变量启用)")
293
+
294
+ # 显示环境变量token信息
295
+ env_tokens = Config.get_refresh_tokens()
296
+ if env_tokens:
297
+ print(f"🎯 环境变量Token: 已设置 {len(env_tokens)} 个")
298
+ for i, token in enumerate(env_tokens, 1):
299
+ print(f" #{i}: {token[:20]}...{token[-8:] if len(token) > 28 else ''}")
300
+ else:
301
+ print("⚠️ 环境变量Token: 未设置 (请设置 WARP_REFRESH_TOKEN)")
302
+ print("💡 支持多个token,使用分号(;)分割:token1;token2;token3")
303
+
304
+ print("-" * 80)
305
+
306
+ try:
307
+ # 编译protobuf
308
+ compile_protobuf_at_startup()
309
+
310
+ # 启动后台服务
311
+ logger.info("🔄 启动后台服务...")
312
+ api_service.start_services()
313
+ logger.success("✅ 后台服务启动完成")
314
+
315
+ logger.info("-" * 80)
316
+ logger.success("🌟 服务器启动完成!")
317
+ if Config.require_admin_auth():
318
+ logger.info(f"🔐 访问管理界面需要管理员密钥: http://localhost:{Config.SERVER_PORT}")
319
+ else:
320
+ logger.info(f"🌐 访问管理界面: http://localhost:{Config.SERVER_PORT}")
321
+ logger.info("-" * 80)
322
+
323
+ # 启动Flask服务器
324
+ app.run(
325
+ host=Config.SERVER_HOST,
326
+ port=Config.SERVER_PORT,
327
+ debug=False,
328
+ threaded=True
329
+ )
330
+
331
+ except KeyboardInterrupt:
332
+ logger.info("🛑 接收到停止信号")
333
+ except Exception as e:
334
+ logger.error(f"❌ 服务器启动失败: {e}")
335
+ finally:
336
+ logger.info("🧹 清理资源...")
337
+ api_service.stop_services()
338
+ logger.info("👋 服务器已停止")
339
+
340
+ if __name__ == "__main__":
341
+ main()
warp_client.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Warp客户端模块
3
+ 处理与Warp API的通信,包括protobuf数据处理和HTTP请求
4
+ """
5
+ import requests
6
+ import json
7
+ from typing import Generator, List, Optional
8
+ from loguru import logger
9
+
10
+ from config import Config
11
+ from request_converter import OpenAIMessage
12
+ from utils import Utils
13
+ from protobuf_manager import ProtobufManager
14
+
15
+
16
+ class WarpClient:
17
+ """Warp API客户端"""
18
+
19
+ def __init__(self, token_manager=None):
20
+ self.token_manager = token_manager
21
+ self.session = requests.Session()
22
+
23
+ def get_auth_token(self) -> Optional[str]:
24
+ """获取当前认证token"""
25
+ if self.token_manager:
26
+ token = self.token_manager.get_current_access_token()
27
+ if token:
28
+ return token
29
+ else:
30
+ logger.error("❌ 无法获取有效的认证token")
31
+ return None
32
+ else:
33
+ logger.error("❌ Token管理器未初始化")
34
+ return None
35
+
36
+
37
+
38
+ def create_protobuf_data(self, messages: List[OpenAIMessage], model: str = "gemini-2.0-flash") -> Optional[bytes]:
39
+ """创建protobuf数据"""
40
+ return ProtobufManager.create_chat_request(messages, model)
41
+
42
+ def parse_protobuf_response(self, base64_data: str) -> str:
43
+ """解析protobuf响应数据"""
44
+ return ProtobufManager.parse_chat_response(base64_data)
45
+
46
+ def _check_and_handle_401_error(self, response_text: str, current_access_token: str = None, current_refresh_token: str = None) -> bool:
47
+ """检查并处理401错误,返回True表示需要重试"""
48
+ try:
49
+ # 尝试解析响应为JSON
50
+ error_data = json.loads(response_text)
51
+ error_message = error_data.get("error", "")
52
+
53
+ # 检查是否是目标错误消息
54
+ if "Unauthorized: User not in context: Not found: no rows in result set" in error_message:
55
+ logger.warning("⚠️ 检测到用户未创建错误,尝试用当前token创建用户")
56
+
57
+ if self.token_manager and current_access_token:
58
+ # 使用当前的access_token尝试创建用户
59
+ logger.info(f"🔄 使用当前access_token创建用户: {current_access_token[:20]}...")
60
+
61
+ # 尝试创建用户
62
+ from login_client import LoginClient
63
+ login_client = LoginClient()
64
+
65
+ if login_client.create_user_with_access_token(current_access_token):
66
+ logger.success("✅ 用户创建成功,可以重试请求")
67
+ return True
68
+ else:
69
+ logger.error("❌ 当前token创建用户失败")
70
+ # 如果用当前token创建用户失败,移除这个refresh_token
71
+ if current_refresh_token:
72
+ logger.info(f"🗑️ 移除创建用户失败的refresh_token")
73
+ self.token_manager.remove_refresh_token(current_refresh_token)
74
+
75
+ # 尝试获取下一个token
76
+ logger.info("🔄 尝试获取下一个access_token...")
77
+ next_token = self.token_manager.get_current_access_token()
78
+ if next_token and next_token != current_access_token:
79
+ logger.info(f"🔄 获取到下一个access_token: {next_token[:20]}...")
80
+ return True # 返回True表示可以用新token重试
81
+ else:
82
+ logger.error("❌ 无法获取下一个有效的access_token")
83
+ return False
84
+ else:
85
+ logger.error("❌ Token管理器未初始化或当前access_token为空")
86
+ return False
87
+
88
+ return False
89
+
90
+ except (json.JSONDecodeError, KeyError, TypeError):
91
+ # 如果不是JSON格式或没有error字段,则不是目标错误
92
+ return False
93
+
94
+ def send_request(self, protobuf_data: bytes) -> Generator[str, None, None]:
95
+ """发送请求到Warp API并返回流式响应"""
96
+ url = f"{Config.WARP_BASE_URL}{Config.WARP_AI_ENDPOINT}"
97
+
98
+ max_retries = 2 # 最多重试2次
99
+ retry_count = 0
100
+
101
+ while retry_count <= max_retries:
102
+ # 获取认证token
103
+ auth_token = self.get_auth_token()
104
+ if not auth_token:
105
+ logger.error("❌ 无法获取认证token,请求终止")
106
+ return
107
+
108
+ # 获取当前使用的refresh_token(用于错误时移除)
109
+ current_refresh_token = None
110
+ if self.token_manager:
111
+ with self.token_manager.token_lock:
112
+ for refresh_token, token_info in self.token_manager.tokens.items():
113
+ if token_info.access_token == auth_token:
114
+ current_refresh_token = refresh_token
115
+ break
116
+
117
+ headers = {
118
+ 'Accept': 'text/event-stream',
119
+ 'Accept-Encoding': 'gzip, br',
120
+ 'Content-Type': 'application/x-protobuf',
121
+ 'x-warp-client-version': Config.WARP_CLIENT_VERSION,
122
+ 'x-warp-os-category': Config.WARP_OS_CATEGORY,
123
+ 'x-warp-os-name': Config.WARP_OS_NAME,
124
+ 'x-warp-os-version': Config.WARP_OS_VERSION,
125
+ 'authorization': f'Bearer {auth_token}'
126
+ }
127
+
128
+ if retry_count > 0:
129
+ logger.info(f"🔄 第{retry_count}次重试请求到Warp API: {url}")
130
+ else:
131
+ logger.info(f"🌐 发送请求到Warp API: {url}")
132
+
133
+ logger.debug(f"📦 Protobuf数据大小: {len(protobuf_data)} 字节")
134
+
135
+ try:
136
+ response = self.session.post(
137
+ url,
138
+ headers=headers,
139
+ data=protobuf_data,
140
+ stream=True,
141
+ timeout=Config.REQUEST_TIMEOUT,
142
+ verify=False
143
+ )
144
+
145
+ if response.status_code == 200:
146
+ logger.success("✅ 请求成功,开始接收流式响应")
147
+ chunk_count = 0
148
+
149
+ for line in response.iter_lines(decode_unicode=True):
150
+ if line and line.startswith('data:'):
151
+ data = line[5:].strip()
152
+ text = self.parse_protobuf_response(data)
153
+ if text:
154
+ chunk_count += 1
155
+ logger.debug(f"📨 接收到响应块 #{chunk_count}: {len(text)} 字符")
156
+ yield text
157
+
158
+ logger.success(f"🎉 流式响应接收完成,总共 {chunk_count} 个块")
159
+ return # 成功,退出重试循环
160
+
161
+ elif response.status_code == 401:
162
+ logger.error(f"❌ Warp API请求失败,状态码: {response.status_code}")
163
+ if response.text:
164
+ logger.error(f"❌ 错误详情: {response.text}")
165
+
166
+ # 检查并处理特定的401错误
167
+ should_retry = self._check_and_handle_401_error(response.text, auth_token, current_refresh_token)
168
+
169
+ if should_retry and retry_count < max_retries:
170
+ retry_count += 1
171
+ logger.info(f"🔄 准备第{retry_count}次重试...")
172
+ continue
173
+ else:
174
+ logger.error(f"❌ 401错误处理失败或已达到最大重试次数")
175
+ return
176
+ else:
177
+ logger.error("❌ 401错误,但无响应内容")
178
+ return
179
+ else:
180
+ logger.error(f"❌ Warp API请求失败,状态码: {response.status_code}")
181
+ if response.text:
182
+ logger.error(f"❌ 错误详情: {response.text}")
183
+ return
184
+
185
+ except requests.Timeout:
186
+ logger.error(f"❌ 请求超时 ({Config.REQUEST_TIMEOUT}秒)")
187
+ return
188
+ except Exception as e:
189
+ logger.error(f"❌ 发送请求时出错: {e}")
190
+ import traceback
191
+ traceback.print_exc()
192
+ return
warp_unified_pb2.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: warp_unified.proto
5
+ # Protobuf Python Version: 6.31.0
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 31,
16
+ 0,
17
+ '',
18
+ 'warp_unified.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x12warp_unified.proto\x12\x0cwarp_unified\"\x8e\x01\n\x0b\x43hatRequest\x12*\n\x07session\x18\x01 \x01(\x0b\x32\x19.warp_unified.SessionInfo\x12(\n\x06system\x18\x02 \x01(\x0b\x32\x18.warp_unified.SystemInfo\x12)\n\tai_config\x18\x03 \x01(\x0b\x32\x16.warp_unified.AIConfig\"P\n\x0bSessionInfo\x12-\n\x07history\x18\x01 \x01(\x0b\x32\x1c.warp_unified.MessageHistory\x12\x12\n\nsession_id\x18\x02 \x01(\t\"v\n\x0eMessageHistory\x12\x18\n\x10\x66irst_message_id\x18\x01 \x01(\t\x12\x1d\n\x15\x66irst_message_content\x18\x02 \x01(\t\x12+\n\x07\x65ntries\x18\x05 \x03(\x0b\x32\x1a.warp_unified.MessageEntry\"\xbc\x03\n\x0cMessageEntry\x12\x12\n\nmessage_id\x18\x01 \x01(\t\x12<\n\nsystem_msg\x18\x04 \x01(\x0b\x32(.warp_unified.MessageEntry.SystemMessage\x12\x38\n\x08user_msg\x18\x02 \x01(\x0b\x32&.warp_unified.MessageEntry.UserMessage\x12\x42\n\rassistant_msg\x18\x03 \x01(\x0b\x32+.warp_unified.MessageEntry.AssistantMessage\x12\x12\n\nextra_data\x18\x07 \x01(\t\x1a\x82\x01\n\rSystemMessage\x12\x12\n\ncontent_id\x18\x01 \x01(\t\x12\x43\n\x08metadata\x18\x04 \x01(\x0b\x32\x31.warp_unified.MessageEntry.SystemMessage.Metadata\x1a\x18\n\x08Metadata\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\t\x1a\x1e\n\x0bUserMessage\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\x1a#\n\x10\x41ssistantMessage\x12\x0f\n\x07\x63ontent\x18\x01 \x01(\t\"l\n\nSystemInfo\x12\x32\n\x0b\x65nvironment\x18\x01 \x01(\x0b\x32\x1d.warp_unified.EnvironmentInfo\x12*\n\x07request\x18\x06 \x01(\x0b\x32\x19.warp_unified.RequestInfo\"=\n\x0f\x45nvironmentInfo\x12*\n\ttimestamp\x18\x04 \x01(\x0b\x32\x17.warp_unified.TimeStamp\"\x19\n\tTimeStamp\x12\x0c\n\x04time\x18\x01 \x01(\x05\"9\n\x0bRequestInfo\x12*\n\x07\x63ontent\x18\x01 \x01(\x0b\x32\x19.warp_unified.ContentData\"<\n\x0b\x43ontentData\x12-\n\x07message\x18\x01 \x01(\x0b\x32\x1c.warp_unified.MessageContent\"\x1e\n\x0eMessageContent\x12\x0c\n\x04text\x18\x01 \x01(\t\"\xb1\x01\n\x08\x41IConfig\x12(\n\x05model\x18\x01 \x01(\x0b\x32\x19.warp_unified.ModelConfig\x12\x18\n\x10priority_level_1\x18\x02 \x01(\x05\x12\x18\n\x10priority_level_2\x18\x03 \x01(\x05\x12\x18\n\x10priority_level_3\x18\x07 \x01(\x05\x12\x18\n\x10priority_level_4\x18\x08 \x01(\x05\x12\x13\n\x0b\x63onfig_data\x18\t \x01(\t\"@\n\x0bModelConfig\x12\x12\n\nmodel_name\x18\x01 \x01(\t\x12\x0f\n\x07variant\x18\x02 \x01(\t\x12\x0c\n\x04mode\x18\x03 \x01(\t\"@\n\x0c\x43hatResponse\x12\x30\n\x08metadata\x18\x02 \x01(\x0b\x32\x1e.warp_unified.ResponseMetadata\"B\n\x10ResponseMetadata\x12.\n\x07\x63ontent\x18\x01 \x01(\x0b\x32\x1d.warp_unified.ResponseContent\"u\n\x0fResponseContent\x12.\n\x07primary\x18\x03 \x01(\x0b\x32\x1d.warp_unified.PrimaryResponse\x12\x32\n\tsecondary\x18\x05 \x01(\x0b\x32\x1f.warp_unified.SecondaryResponse\"P\n\x0fPrimaryResponse\x12\x13\n\x0bresponse_id\x18\x01 \x01(\t\x12(\n\x04text\x18\x02 \x01(\x0b\x32\x1a.warp_unified.ResponseText\"T\n\x0cResponseText\x12\x0b\n\x03id1\x18\x01 \x01(\t\x12\x0b\n\x03id2\x18\x07 \x01(\t\x12*\n\x07\x63ontent\x18\x03 \x01(\x0b\x32\x19.warp_unified.TextContent\"\x1b\n\x0bTextContent\x12\x0c\n\x04text\x18\x01 \x01(\t\"\x88\x01\n\x11SecondaryResponse\x12\x35\n\x0b\x61lternative\x18\x01 \x01(\x0b\x32 .warp_unified.AlternativeContent\x12\'\n\x04\x66ile\x18\x02 \x01(\x0b\x32\x19.warp_unified.FileContent\x12\x13\n\x0bresponse_id\x18\x03 \x01(\t\"\x1b\n\x0b\x46ileContent\x12\x0c\n\x04text\x18\x01 \x01(\t\"W\n\x12\x41lternativeContent\x12\n\n\x02id\x18\x01 \x01(\t\x12\x35\n\rfinal_content\x18\x03 \x01(\x0b\x32\x1e.warp_unified.FinalTextContent\" \n\x10\x46inalTextContent\x12\x0c\n\x04text\x18\x01 \x01(\t\"\xf1\x01\n\x0cLoginRequest\x12.\n\turl_chain\x18\x01 \x01(\x0b\x32\x1b.warp_unified.LoginUrlChain\x12\x15\n\rrequest_flags\x18\x02 \x03(\x05\x12\x14\n\x0crequest_type\x18\x03 \x01(\x05\x12+\n\nlogin_data\x18\x04 \x01(\x0b\x32\x17.warp_unified.LoginData\x12\x14\n\x0c\x63onfig_flags\x18\x05 \x03(\x05\x12\x10\n\x08language\x18\x07 \x01(\t\x12/\n\x08settings\x18\t \x01(\x0b\x32\x1d.warp_unified.RequestSettings\"\xd9\x07\n\rLoginUrlChain\x12@\n\x0b\x63ontainer_2\x18\x01 \x01(\x0b\x32+.warp_unified.LoginUrlChain.Level2Container\x1a\x85\x07\n\x0fLevel2Container\x12P\n\x0b\x63ontainer_3\x18\x04 \x01(\x0b\x32;.warp_unified.LoginUrlChain.Level2Container.Level3Container\x1a\x9f\x06\n\x0fLevel3Container\x12`\n\x0b\x63ontainer_4\x18\x03 \x01(\x0b\x32K.warp_unified.LoginUrlChain.Level2Container.Level3Container.Level4Container\x1a\xa9\x05\n\x0fLevel4Container\x12p\n\x0b\x63ontainer_5\x18\x0f \x01(\x0b\x32[.warp_unified.LoginUrlChain.Level2Container.Level3Container.Level4Container.Level5Container\x1a\xa3\x04\n\x0fLevel5Container\x12\x80\x01\n\x0b\x63ontainer_6\x18\x02 \x01(\x0b\x32k.warp_unified.LoginUrlChain.Level2Container.Level3Container.Level4Container.Level5Container.Level6Container\x1a\x8c\x03\n\x0fLevel6Container\x12\x92\x01\n\rurl_container\x18\x01 \x01(\x0b\x32{.warp_unified.LoginUrlChain.Level2Container.Level3Container.Level4Container.Level5Container.Level6Container.Level7Container\x1a\xe3\x01\n\x0fLevel7Container\x12\xa4\x01\n\tfinal_url\x18\x03 \x01(\x0b\x32\x90\x01.warp_unified.LoginUrlChain.Level2Container.Level3Container.Level4Container.Level5Container.Level6Container.Level7Container.Level8FinalContainer\x1a)\n\x14Level8FinalContainer\x12\x11\n\tlogin_url\x18\x01 \x01(\t\"\x18\n\tLoginData\x12\x0b\n\x03url\x18\x01 \x01(\t\"(\n\x0fRequestSettings\x12\x15\n\rsetting_value\x18\x01 \x01(\x05\x62\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'warp_unified_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ DESCRIPTOR._loaded_options = None
34
+ _globals['_CHATREQUEST']._serialized_start=37
35
+ _globals['_CHATREQUEST']._serialized_end=179
36
+ _globals['_SESSIONINFO']._serialized_start=181
37
+ _globals['_SESSIONINFO']._serialized_end=261
38
+ _globals['_MESSAGEHISTORY']._serialized_start=263
39
+ _globals['_MESSAGEHISTORY']._serialized_end=381
40
+ _globals['_MESSAGEENTRY']._serialized_start=384
41
+ _globals['_MESSAGEENTRY']._serialized_end=828
42
+ _globals['_MESSAGEENTRY_SYSTEMMESSAGE']._serialized_start=629
43
+ _globals['_MESSAGEENTRY_SYSTEMMESSAGE']._serialized_end=759
44
+ _globals['_MESSAGEENTRY_SYSTEMMESSAGE_METADATA']._serialized_start=735
45
+ _globals['_MESSAGEENTRY_SYSTEMMESSAGE_METADATA']._serialized_end=759
46
+ _globals['_MESSAGEENTRY_USERMESSAGE']._serialized_start=761
47
+ _globals['_MESSAGEENTRY_USERMESSAGE']._serialized_end=791
48
+ _globals['_MESSAGEENTRY_ASSISTANTMESSAGE']._serialized_start=793
49
+ _globals['_MESSAGEENTRY_ASSISTANTMESSAGE']._serialized_end=828
50
+ _globals['_SYSTEMINFO']._serialized_start=830
51
+ _globals['_SYSTEMINFO']._serialized_end=938
52
+ _globals['_ENVIRONMENTINFO']._serialized_start=940
53
+ _globals['_ENVIRONMENTINFO']._serialized_end=1001
54
+ _globals['_TIMESTAMP']._serialized_start=1003
55
+ _globals['_TIMESTAMP']._serialized_end=1028
56
+ _globals['_REQUESTINFO']._serialized_start=1030
57
+ _globals['_REQUESTINFO']._serialized_end=1087
58
+ _globals['_CONTENTDATA']._serialized_start=1089
59
+ _globals['_CONTENTDATA']._serialized_end=1149
60
+ _globals['_MESSAGECONTENT']._serialized_start=1151
61
+ _globals['_MESSAGECONTENT']._serialized_end=1181
62
+ _globals['_AICONFIG']._serialized_start=1184
63
+ _globals['_AICONFIG']._serialized_end=1361
64
+ _globals['_MODELCONFIG']._serialized_start=1363
65
+ _globals['_MODELCONFIG']._serialized_end=1427
66
+ _globals['_CHATRESPONSE']._serialized_start=1429
67
+ _globals['_CHATRESPONSE']._serialized_end=1493
68
+ _globals['_RESPONSEMETADATA']._serialized_start=1495
69
+ _globals['_RESPONSEMETADATA']._serialized_end=1561
70
+ _globals['_RESPONSECONTENT']._serialized_start=1563
71
+ _globals['_RESPONSECONTENT']._serialized_end=1680
72
+ _globals['_PRIMARYRESPONSE']._serialized_start=1682
73
+ _globals['_PRIMARYRESPONSE']._serialized_end=1762
74
+ _globals['_RESPONSETEXT']._serialized_start=1764
75
+ _globals['_RESPONSETEXT']._serialized_end=1848
76
+ _globals['_TEXTCONTENT']._serialized_start=1850
77
+ _globals['_TEXTCONTENT']._serialized_end=1877
78
+ _globals['_SECONDARYRESPONSE']._serialized_start=1880
79
+ _globals['_SECONDARYRESPONSE']._serialized_end=2016
80
+ _globals['_FILECONTENT']._serialized_start=2018
81
+ _globals['_FILECONTENT']._serialized_end=2045
82
+ _globals['_ALTERNATIVECONTENT']._serialized_start=2047
83
+ _globals['_ALTERNATIVECONTENT']._serialized_end=2134
84
+ _globals['_FINALTEXTCONTENT']._serialized_start=2136
85
+ _globals['_FINALTEXTCONTENT']._serialized_end=2168
86
+ _globals['_LOGINREQUEST']._serialized_start=2171
87
+ _globals['_LOGINREQUEST']._serialized_end=2412
88
+ _globals['_LOGINURLCHAIN']._serialized_start=2415
89
+ _globals['_LOGINURLCHAIN']._serialized_end=3400
90
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER']._serialized_start=2499
91
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER']._serialized_end=3400
92
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER']._serialized_start=2601
93
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER']._serialized_end=3400
94
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER_LEVEL4CONTAINER']._serialized_start=2719
95
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER_LEVEL4CONTAINER']._serialized_end=3400
96
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER_LEVEL4CONTAINER_LEVEL5CONTAINER']._serialized_start=2853
97
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER_LEVEL4CONTAINER_LEVEL5CONTAINER']._serialized_end=3400
98
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER_LEVEL4CONTAINER_LEVEL5CONTAINER_LEVEL6CONTAINER']._serialized_start=3004
99
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER_LEVEL4CONTAINER_LEVEL5CONTAINER_LEVEL6CONTAINER']._serialized_end=3400
100
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER_LEVEL4CONTAINER_LEVEL5CONTAINER_LEVEL6CONTAINER_LEVEL7CONTAINER']._serialized_start=3173
101
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER_LEVEL4CONTAINER_LEVEL5CONTAINER_LEVEL6CONTAINER_LEVEL7CONTAINER']._serialized_end=3400
102
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER_LEVEL4CONTAINER_LEVEL5CONTAINER_LEVEL6CONTAINER_LEVEL7CONTAINER_LEVEL8FINALCONTAINER']._serialized_start=3359
103
+ _globals['_LOGINURLCHAIN_LEVEL2CONTAINER_LEVEL3CONTAINER_LEVEL4CONTAINER_LEVEL5CONTAINER_LEVEL6CONTAINER_LEVEL7CONTAINER_LEVEL8FINALCONTAINER']._serialized_end=3400
104
+ _globals['_LOGINDATA']._serialized_start=3402
105
+ _globals['_LOGINDATA']._serialized_end=3426
106
+ _globals['_REQUESTSETTINGS']._serialized_start=3428
107
+ _globals['_REQUESTSETTINGS']._serialized_end=3468
108
+ # @@protoc_insertion_point(module_scope)
web_template.py ADDED
@@ -0,0 +1,980 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Web界面模板模块
3
+ 包含管理界面的HTML模板
4
+ """
5
+
6
+ def get_admin_login_template():
7
+ """返回管理员登录页面模板"""
8
+ return """
9
+ <!DOCTYPE html>
10
+ <html lang="zh-CN">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
+ <title>🔐 管理员登录 - Warp API 管理中心</title>
15
+ <style>
16
+ body {
17
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
18
+ margin: 0;
19
+ padding: 0;
20
+ background: linear-gradient(135deg, #0f1419 0%, #1a2332 25%, #2d3748 50%, #1a2332 75%, #0f1419 100%);
21
+ background-attachment: fixed;
22
+ min-height: 100vh;
23
+ color: #e2e8f0;
24
+ display: flex;
25
+ justify-content: center;
26
+ align-items: center;
27
+ }
28
+
29
+ .login-container {
30
+ max-width: 400px;
31
+ width: 90%;
32
+ background: rgba(30, 41, 59, 0.9);
33
+ backdrop-filter: blur(20px);
34
+ border-radius: 20px;
35
+ padding: 40px;
36
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
37
+ border: 1px solid rgba(148, 163, 184, 0.1);
38
+ text-align: center;
39
+ }
40
+
41
+ h1 {
42
+ color: #cbd5e1;
43
+ margin-bottom: 30px;
44
+ font-size: 2rem;
45
+ text-shadow: 0 2px 10px rgba(59, 130, 246, 0.3);
46
+ }
47
+
48
+ .form-group {
49
+ margin-bottom: 25px;
50
+ text-align: left;
51
+ }
52
+
53
+ label {
54
+ display: block;
55
+ margin-bottom: 8px;
56
+ color: #cbd5e1;
57
+ font-weight: 600;
58
+ }
59
+
60
+ input {
61
+ width: 100%;
62
+ padding: 12px 16px;
63
+ background: rgba(30, 41, 59, 0.7);
64
+ border: 2px solid rgba(71, 85, 105, 0.3);
65
+ border-radius: 10px;
66
+ box-sizing: border-box;
67
+ color: #e2e8f0;
68
+ font-size: 14px;
69
+ transition: all 0.3s ease;
70
+ }
71
+
72
+ input:focus {
73
+ outline: none;
74
+ border-color: rgba(59, 130, 246, 0.5);
75
+ background: rgba(30, 41, 59, 0.9);
76
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
77
+ }
78
+
79
+ button {
80
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.8) 0%, rgba(29, 78, 216, 0.8) 100%);
81
+ color: white;
82
+ padding: 14px 28px;
83
+ border: none;
84
+ border-radius: 10px;
85
+ cursor: pointer;
86
+ font-weight: 600;
87
+ font-size: 16px;
88
+ width: 100%;
89
+ transition: all 0.3s ease;
90
+ backdrop-filter: blur(10px);
91
+ border: 1px solid rgba(59, 130, 246, 0.3);
92
+ }
93
+
94
+ button:hover {
95
+ transform: translateY(-2px);
96
+ box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.4);
97
+ background: linear-gradient(135deg, rgba(59, 130, 246, 1) 0%, rgba(29, 78, 216, 1) 100%);
98
+ }
99
+
100
+ .status {
101
+ padding: 12px 16px;
102
+ margin: 15px 0;
103
+ border-radius: 8px;
104
+ font-weight: 500;
105
+ display: none;
106
+ }
107
+
108
+ .status.success {
109
+ background: rgba(34, 197, 94, 0.15);
110
+ color: #86efac;
111
+ border: 1px solid rgba(34, 197, 94, 0.3);
112
+ }
113
+
114
+ .status.error {
115
+ background: rgba(239, 68, 68, 0.15);
116
+ color: #fca5a5;
117
+ border: 1px solid rgba(239, 68, 68, 0.3);
118
+ }
119
+
120
+ .note {
121
+ margin-top: 20px;
122
+ padding: 15px;
123
+ background: rgba(79, 70, 229, 0.1);
124
+ border: 1px solid rgba(79, 70, 229, 0.2);
125
+ border-radius: 8px;
126
+ font-size: 14px;
127
+ color: #a5b4fc;
128
+ }
129
+ </style>
130
+ </head>
131
+ <body>
132
+ <div class="login-container">
133
+ <h1>🔐 管理员登录</h1>
134
+ <form id="loginForm">
135
+ <div class="form-group">
136
+ <label for="adminKey">管理员密钥</label>
137
+ <input type="password" id="adminKey" name="adminKey" placeholder="请输入管理员密钥" required>
138
+ </div>
139
+ <button type="submit">🚀 登录</button>
140
+ <div id="status" class="status"></div>
141
+ </form>
142
+ <div class="note">
143
+ 💡 管理员密钥通过环境变量 <code>ADMIN_KEY</code> 设置
144
+ </div>
145
+ </div>
146
+
147
+ <script>
148
+ document.getElementById('loginForm').addEventListener('submit', async function(e) {
149
+ e.preventDefault();
150
+
151
+ const adminKey = document.getElementById('adminKey').value;
152
+ const statusDiv = document.getElementById('status');
153
+
154
+ if (!adminKey) {
155
+ showStatus('请输入管理员密钥', 'error');
156
+ return;
157
+ }
158
+
159
+ try {
160
+ const response = await fetch('/admin/auth', {
161
+ method: 'POST',
162
+ headers: { 'Content-Type': 'application/json' },
163
+ body: JSON.stringify({ admin_key: adminKey })
164
+ });
165
+
166
+ const data = await response.json();
167
+
168
+ if (data.success) {
169
+ showStatus('登录成功,正在跳转...', 'success');
170
+ setTimeout(() => {
171
+ window.location.href = data.redirect || '/';
172
+ }, 1000);
173
+ } else {
174
+ showStatus(data.message || '登录失败', 'error');
175
+ }
176
+ } catch (error) {
177
+ showStatus('登录请求失败: ' + error.message, 'error');
178
+ }
179
+ });
180
+
181
+ function showStatus(message, type) {
182
+ const statusDiv = document.getElementById('status');
183
+ statusDiv.textContent = message;
184
+ statusDiv.className = `status ${type}`;
185
+ statusDiv.style.display = 'block';
186
+ }
187
+ </script>
188
+ </body>
189
+ </html>
190
+ """
191
+
192
+ def get_html_template():
193
+ """返回优化的HTML模板"""
194
+ return """
195
+ <!DOCTYPE html>
196
+ <html lang="zh-CN">
197
+ <head>
198
+ <meta charset="UTF-8">
199
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
200
+ <title>🚀 Warp API 管理中心</title>
201
+ <style>
202
+ body {
203
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
204
+ margin: 0;
205
+ padding: 20px;
206
+ background: linear-gradient(135deg, #0f1419 0%, #1a2332 25%, #2d3748 50%, #1a2332 75%, #0f1419 100%);
207
+ background-attachment: fixed;
208
+ min-height: 100vh;
209
+ color: #e2e8f0;
210
+ }
211
+
212
+ .container {
213
+ max-width: 1400px;
214
+ margin: 0 auto;
215
+ background: rgba(30, 41, 59, 0.85);
216
+ backdrop-filter: blur(20px);
217
+ border-radius: 20px;
218
+ padding: 40px;
219
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
220
+ border: 1px solid rgba(148, 163, 184, 0.1);
221
+ }
222
+
223
+ .header {
224
+ display: flex;
225
+ justify-content: space-between;
226
+ align-items: center;
227
+ margin-bottom: 40px;
228
+ }
229
+
230
+ h1 {
231
+ color: #cbd5e1;
232
+ margin: 0;
233
+ font-size: 2.5rem;
234
+ text-shadow: 0 2px 10px rgba(59, 130, 246, 0.3);
235
+ letter-spacing: -0.025em;
236
+ }
237
+
238
+ .logout-btn {
239
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.8) 0%, rgba(185, 28, 28, 0.8) 100%);
240
+ color: white;
241
+ padding: 10px 20px;
242
+ border: none;
243
+ border-radius: 8px;
244
+ cursor: pointer;
245
+ font-weight: 600;
246
+ font-size: 14px;
247
+ transition: all 0.3s ease;
248
+ border: 1px solid rgba(239, 68, 68, 0.3);
249
+ }
250
+
251
+ .logout-btn:hover {
252
+ background: linear-gradient(135deg, rgba(239, 68, 68, 1) 0%, rgba(185, 28, 28, 1) 100%);
253
+ transform: translateY(-1px);
254
+ }
255
+
256
+ .section {
257
+ margin-bottom: 40px;
258
+ padding: 30px;
259
+ background: rgba(51, 65, 85, 0.6);
260
+ backdrop-filter: blur(10px);
261
+ border-radius: 16px;
262
+ border: 1px solid rgba(148, 163, 184, 0.1);
263
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
264
+ }
265
+
266
+ .section h2 {
267
+ color: #f1f5f9;
268
+ margin-top: 0;
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 12px;
272
+ font-size: 1.5rem;
273
+ margin-bottom: 25px;
274
+ }
275
+
276
+ .status-grid {
277
+ display: grid;
278
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
279
+ gap: 20px;
280
+ margin-bottom: 30px;
281
+ }
282
+
283
+ .status-card {
284
+ padding: 20px;
285
+ border-radius: 12px;
286
+ text-align: center;
287
+ font-weight: 600;
288
+ background: rgba(59, 130, 246, 0.15);
289
+ border: 1px solid rgba(59, 130, 246, 0.2);
290
+ backdrop-filter: blur(10px);
291
+ color: #e2e8f0;
292
+ transition: all 0.3s ease;
293
+ }
294
+
295
+ .status-card:hover {
296
+ transform: translateY(-2px);
297
+ box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.2);
298
+ background: rgba(59, 130, 246, 0.2);
299
+ }
300
+
301
+ .status-card.total { background: rgba(59, 130, 246, 0.15); border-color: rgba(59, 130, 246, 0.3); }
302
+ .status-card.active { background: rgba(34, 197, 94, 0.15); border-color: rgba(34, 197, 94, 0.3); }
303
+ .status-card.with-access { background: rgba(168, 85, 247, 0.15); border-color: rgba(168, 85, 247, 0.3); }
304
+
305
+ table {
306
+ width: 100%;
307
+ border-collapse: collapse;
308
+ margin-bottom: 25px;
309
+ background: rgba(30, 41, 59, 0.5);
310
+ border-radius: 12px;
311
+ overflow: hidden;
312
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
313
+ }
314
+
315
+ th, td {
316
+ padding: 16px;
317
+ text-align: left;
318
+ border-bottom: 1px solid rgba(71, 85, 105, 0.3);
319
+ }
320
+
321
+ th {
322
+ background: rgba(51, 65, 85, 0.8);
323
+ color: #f1f5f9;
324
+ font-weight: 600;
325
+ font-size: 0.875rem;
326
+ text-transform: uppercase;
327
+ letter-spacing: 0.05em;
328
+ }
329
+
330
+ tr:hover {
331
+ background-color: rgba(71, 85, 105, 0.3);
332
+ transition: background-color 0.2s ease;
333
+ }
334
+
335
+ input, textarea {
336
+ width: 100%;
337
+ padding: 12px 16px;
338
+ background: rgba(30, 41, 59, 0.7);
339
+ border: 2px solid rgba(71, 85, 105, 0.3);
340
+ border-radius: 10px;
341
+ box-sizing: border-box;
342
+ color: #e2e8f0;
343
+ font-size: 14px;
344
+ transition: all 0.3s ease;
345
+ }
346
+
347
+ input:focus, textarea:focus {
348
+ outline: none;
349
+ border-color: rgba(59, 130, 246, 0.5);
350
+ background: rgba(30, 41, 59, 0.9);
351
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
352
+ }
353
+
354
+ input::placeholder, textarea::placeholder {
355
+ color: #94a3b8;
356
+ }
357
+
358
+ button {
359
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.8) 0%, rgba(29, 78, 216, 0.8) 100%);
360
+ color: white;
361
+ padding: 12px 24px;
362
+ border: none;
363
+ border-radius: 10px;
364
+ cursor: pointer;
365
+ margin: 6px;
366
+ font-weight: 600;
367
+ font-size: 14px;
368
+ transition: all 0.3s ease;
369
+ backdrop-filter: blur(10px);
370
+ border: 1px solid rgba(59, 130, 246, 0.3);
371
+ }
372
+
373
+ button:hover {
374
+ transform: translateY(-2px);
375
+ box-shadow: 0 10px 25px -5px rgba(59, 130, 246, 0.4);
376
+ background: linear-gradient(135deg, rgba(59, 130, 246, 1) 0%, rgba(29, 78, 216, 1) 100%);
377
+ }
378
+
379
+ button:active {
380
+ transform: translateY(0);
381
+ }
382
+
383
+ .btn-success {
384
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.8) 0%, rgba(21, 128, 61, 0.8) 100%);
385
+ border-color: rgba(34, 197, 94, 0.3);
386
+ }
387
+
388
+ .btn-success:hover {
389
+ background: linear-gradient(135deg, rgba(34, 197, 94, 1) 0%, rgba(21, 128, 61, 1) 100%);
390
+ box-shadow: 0 10px 25px -5px rgba(34, 197, 94, 0.4);
391
+ }
392
+
393
+ .btn-danger {
394
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.8) 0%, rgba(185, 28, 28, 0.8) 100%);
395
+ border-color: rgba(239, 68, 68, 0.3);
396
+ }
397
+
398
+ .btn-danger:hover {
399
+ background: linear-gradient(135deg, rgba(239, 68, 68, 1) 0%, rgba(185, 28, 28, 1) 100%);
400
+ box-shadow: 0 10px 25px -5px rgba(239, 68, 68, 0.4);
401
+ }
402
+
403
+ .btn-info {
404
+ background: linear-gradient(135deg, rgba(6, 182, 212, 0.8) 0%, rgba(8, 145, 178, 0.8) 100%);
405
+ border-color: rgba(6, 182, 212, 0.3);
406
+ }
407
+
408
+ .btn-info:hover {
409
+ background: linear-gradient(135deg, rgba(6, 182, 212, 1) 0%, rgba(8, 145, 178, 1) 100%);
410
+ box-shadow: 0 10px 25px -5px rgba(6, 182, 212, 0.4);
411
+ }
412
+
413
+ .btn-warning {
414
+ background: linear-gradient(135deg, rgba(245, 158, 11, 0.8) 0%, rgba(217, 119, 6, 0.8) 100%);
415
+ border-color: rgba(245, 158, 11, 0.3);
416
+ }
417
+
418
+ .btn-warning:hover {
419
+ background: linear-gradient(135deg, rgba(245, 158, 11, 1) 0%, rgba(217, 119, 6, 1) 100%);
420
+ box-shadow: 0 10px 25px -5px rgba(245, 158, 11, 0.4);
421
+ }
422
+
423
+ .btn-small {
424
+ padding: 8px 16px;
425
+ font-size: 12px;
426
+ margin: 2px;
427
+ }
428
+
429
+ .status {
430
+ padding: 16px 20px;
431
+ margin: 20px 0;
432
+ border-radius: 12px;
433
+ font-weight: 600;
434
+ backdrop-filter: blur(10px);
435
+ }
436
+
437
+ .status.success {
438
+ background: rgba(34, 197, 94, 0.15);
439
+ color: #86efac;
440
+ border: 1px solid rgba(34, 197, 94, 0.3);
441
+ }
442
+
443
+ .status.error {
444
+ background: rgba(239, 68, 68, 0.15);
445
+ color: #fca5a5;
446
+ border: 1px solid rgba(239, 68, 68, 0.3);
447
+ }
448
+
449
+ .api-info {
450
+ background: linear-gradient(135deg, rgba(79, 70, 229, 0.15) 0%, rgba(59, 130, 246, 0.15) 100%);
451
+ border: 1px solid rgba(79, 70, 229, 0.2);
452
+ color: #e2e8f0;
453
+ padding: 25px;
454
+ border-radius: 16px;
455
+ margin-bottom: 30px;
456
+ backdrop-filter: blur(10px);
457
+ }
458
+
459
+ .api-info code {
460
+ background: rgba(30, 41, 59, 0.6);
461
+ padding: 6px 12px;
462
+ border-radius: 8px;
463
+ color: #a78bfa;
464
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
465
+ border: 1px solid rgba(71, 85, 105, 0.3);
466
+ }
467
+
468
+ .tabs {
469
+ display: flex;
470
+ margin-bottom: 30px;
471
+ background: rgba(30, 41, 59, 0.5);
472
+ border-radius: 12px;
473
+ padding: 4px;
474
+ backdrop-filter: blur(10px);
475
+ }
476
+
477
+ .tab {
478
+ padding: 14px 28px;
479
+ cursor: pointer;
480
+ border: none;
481
+ background: none;
482
+ color: #94a3b8;
483
+ font-weight: 600;
484
+ border-radius: 8px;
485
+ transition: all 0.3s ease;
486
+ flex: 1;
487
+ text-align: center;
488
+ }
489
+
490
+ .tab.active {
491
+ color: #f1f5f9;
492
+ background: rgba(59, 130, 246, 0.3);
493
+ backdrop-filter: blur(10px);
494
+ }
495
+
496
+ .tab:hover:not(.active) {
497
+ color: #cbd5e1;
498
+ background: rgba(71, 85, 105, 0.3);
499
+ }
500
+
501
+ .tab-content {
502
+ display: none;
503
+ }
504
+
505
+ .tab-content.active {
506
+ display: block;
507
+ }
508
+
509
+ .loading {
510
+ display: none;
511
+ text-align: center;
512
+ padding: 30px;
513
+ color: #94a3b8;
514
+ }
515
+
516
+ .spinner {
517
+ border: 4px solid rgba(71, 85, 105, 0.3);
518
+ border-top: 4px solid #3b82f6;
519
+ border-radius: 50%;
520
+ width: 40px;
521
+ height: 40px;
522
+ animation: spin 1s linear infinite;
523
+ margin: 0 auto 15px;
524
+ }
525
+
526
+ @keyframes spin {
527
+ 0% { transform: rotate(0deg); }
528
+ 100% { transform: rotate(360deg); }
529
+ }
530
+
531
+ .action-buttons {
532
+ display: flex;
533
+ gap: 8px;
534
+ justify-content: center;
535
+ }
536
+
537
+ .token-id {
538
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
539
+ background: rgba(30, 41, 59, 0.6);
540
+ padding: 4px 8px;
541
+ border-radius: 6px;
542
+ font-size: 0.85rem;
543
+ color: #a78bfa;
544
+ }
545
+
546
+ .env-info {
547
+ background: rgba(34, 197, 94, 0.1);
548
+ border: 1px solid rgba(34, 197, 94, 0.2);
549
+ color: #86efac;
550
+ padding: 15px;
551
+ border-radius: 12px;
552
+ margin-bottom: 20px;
553
+ font-size: 14px;
554
+ }
555
+
556
+ .export-section {
557
+ background: rgba(245, 158, 11, 0.1);
558
+ border: 1px solid rgba(245, 158, 11, 0.2);
559
+ padding: 20px;
560
+ border-radius: 12px;
561
+ margin-bottom: 20px;
562
+ }
563
+
564
+ .export-form {
565
+ display: flex;
566
+ gap: 15px;
567
+ align-items: end;
568
+ }
569
+
570
+ .export-form input {
571
+ flex: 1;
572
+ }
573
+
574
+ .modal {
575
+ display: none;
576
+ position: fixed;
577
+ z-index: 1000;
578
+ left: 0;
579
+ top: 0;
580
+ width: 100%;
581
+ height: 100%;
582
+ background-color: rgba(0, 0, 0, 0.5);
583
+ backdrop-filter: blur(10px);
584
+ }
585
+
586
+ .modal-content {
587
+ background: rgba(30, 41, 59, 0.95);
588
+ margin: 15% auto;
589
+ padding: 30px;
590
+ border-radius: 16px;
591
+ width: 80%;
592
+ max-width: 500px;
593
+ border: 1px solid rgba(148, 163, 184, 0.2);
594
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
595
+ }
596
+
597
+ .close {
598
+ color: #94a3b8;
599
+ float: right;
600
+ font-size: 28px;
601
+ font-weight: bold;
602
+ cursor: pointer;
603
+ line-height: 1;
604
+ }
605
+
606
+ .close:hover {
607
+ color: #e2e8f0;
608
+ }
609
+ </style>
610
+ </head>
611
+ <body>
612
+ <div class="container">
613
+ <div class="header">
614
+ <h1>🚀 Warp API 管理中心</h1>
615
+ <button class="logout-btn" onclick="logout()">🚪 退出登录</button>
616
+ </div>
617
+
618
+ <div class="api-info">
619
+ <h3>📡 API 端点信息</h3>
620
+ <p><strong>Base URL:</strong> <code>http://localhost:7860</code></p>
621
+ <p><strong>Chat:</strong> <code>/v1/chat/completions</code> | <strong>Models:</strong> <code>/v1/models</code></p>
622
+ <div class="env-info">
623
+ 🔐 <strong>安全提示:</strong> API密钥和敏感信息已隐藏,仅在服务器端可见
624
+ </div>
625
+ </div>
626
+
627
+ <div class="tabs">
628
+ <button class="tab active" onclick="showTab('token-status')">🔑 Token状态</button>
629
+ <button class="tab" onclick="showTab('add-tokens')">➕ 添加Token</button>
630
+ <button class="tab" onclick="showTab('get-tokens')">📧 获取Token</button>
631
+ </div>
632
+
633
+ <div id="token-status" class="tab-content active">
634
+ <div class="section">
635
+ <h2>🔑 Token 状态管理</h2>
636
+ <div class="env-info" id="tokenEnvInfo" style="display: none;">
637
+ 🎯 <strong>环境变量Token:</strong> 已通过 <code>WARP_REFRESH_TOKEN</code> 设置<br>
638
+ 💡 <strong>多Token支持:</strong> 可使用分号(;)分割多个token:<code>token1;token2;token3</code>
639
+ </div>
640
+
641
+ <div class="export-section">
642
+ <h3>🔒 导出 Refresh Token (超级管理员功能)</h3>
643
+ <p>导出所有refresh token为分号分割的文本文件,需要超级管理员密钥验证</p>
644
+ <div class="export-form">
645
+ <div style="flex: 1;">
646
+ <label>超级管理员密钥</label>
647
+ <input type="password" id="superAdminKey" placeholder="请输入超级管理员密钥">
648
+ </div>
649
+ <button onclick="exportTokens()" class="btn-warning">📥 导出Token</button>
650
+ </div>
651
+ <div class="env-info" style="margin-top: 15px;">
652
+ 💡 <strong>提示:</strong> 点击导出后,浏览器会提示您选择保存位置<br>
653
+ 📁 <strong>文件格式:</strong> 文本文件,token间用分号(;)分割,文件名包含时间戳
654
+ </div>
655
+ <div id="exportStatus"></div>
656
+ </div>
657
+
658
+ <div class="status-grid">
659
+ <div class="status-card total"><div>总Token数</div><div id="totalTokens">-</div></div>
660
+ <div class="status-card active"><div>活跃Token</div><div id="activeTokens">-</div></div>
661
+ <div class="status-card with-access"><div>可用Token</div><div id="tokensWithAccess">-</div></div>
662
+ </div>
663
+ <button onclick="refreshTokenStatus()" class="btn-info">🔄 刷新状态</button>
664
+ <button onclick="refreshAllTokens()" class="btn-success">⚡ 刷新所有Token</button>
665
+ <div style="max-height: 500px; overflow-y: auto; margin-top: 25px;">
666
+ <table>
667
+ <thead><tr><th>Token ID</th><th>状态</th><th>访问Token</th><th>刷新次数</th><th>使用次数</th><th>操作</th></tr></thead>
668
+ <tbody id="tokenTableBody"></tbody>
669
+ </table>
670
+ </div>
671
+ </div>
672
+ </div>
673
+
674
+ <div id="add-tokens" class="tab-content">
675
+ <div class="section">
676
+ <h2>➕ 添加 Refresh Token</h2>
677
+ <div class="env-info">
678
+ 💡 <strong>提示:</strong> 也可通过环境变量 <code>WARP_REFRESH_TOKEN</code> 设置refresh token<br>
679
+ 🔀 <strong>多Token支持:</strong> 环境变量中可使用分号(;)分割多个token:<code>token1;token2;token3</code>
680
+ </div>
681
+ <textarea id="tokensInput" rows="5" placeholder="请输入refresh token,每行一个&#10;或者在环境变量中使用分号分割:token1;token2;token3"></textarea>
682
+ <div style="margin-top: 20px;">
683
+ <button onclick="addTokens()" class="btn-success">➕ 添加Token</button>
684
+ <button onclick="clearTokensInput()" class="btn-danger">🗑️ 清空</button>
685
+ </div>
686
+ <div id="addTokensStatus"></div>
687
+ </div>
688
+ </div>
689
+
690
+ <div id="get-tokens" class="tab-content">
691
+ <div class="section">
692
+ <h2>📧 批量获取 Refresh Token</h2>
693
+ <div style="margin-bottom: 25px;">
694
+ <label style="color: #cbd5e1; margin-right: 10px;">并发线程数:</label>
695
+ <input type="number" id="maxWorkers" min="1" max="20" value="5" style="width: 100px; display: inline-block;">
696
+ <button onclick="addEmailRow()">➕ 添加邮箱</button>
697
+ <button onclick="clearEmails()" class="btn-danger">🗑️ 清空</button>
698
+ </div>
699
+ <table id="emailTable">
700
+ <thead><tr><th>邮箱地址</th><th>登录URL</th><th>操作</th></tr></thead>
701
+ <tbody id="emailTableBody">
702
+ <tr>
703
+ <td><input type="email" placeholder="[email protected]"></td>
704
+ <td><input type="url" placeholder="https://..."></td>
705
+ <td><button onclick="removeEmailRow(this)" class="btn-danger btn-small">❌</button></td>
706
+ </tr>
707
+ </tbody>
708
+ </table>
709
+ <button onclick="processEmails()" class="btn-success">🚀 开始处理</button>
710
+ <div class="loading" id="emailLoading"><div class="spinner"></div><p id="loadingText">正在处理邮箱,获取Token并创建用户...</p></div>
711
+ <div id="emailResults"></div>
712
+ </div>
713
+ </div>
714
+ </div>
715
+
716
+ <script>
717
+ function showTab(tabName) {
718
+ document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
719
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
720
+ event.target.classList.add('active');
721
+ document.getElementById(tabName).classList.add('active');
722
+ if (tabName === 'token-status') refreshTokenStatus();
723
+ }
724
+
725
+ async function refreshTokenStatus() {
726
+ try {
727
+ const response = await fetch('/token/status');
728
+ const data = await response.json();
729
+ if (data.success !== false) {
730
+ document.getElementById('totalTokens').textContent = data.total_tokens;
731
+ document.getElementById('activeTokens').textContent = data.active_tokens;
732
+ document.getElementById('tokensWithAccess').textContent = data.tokens_with_access;
733
+
734
+ // 如果有token,检查是否是环境变量设置的
735
+ if (data.total_tokens > 0) {
736
+ document.getElementById('tokenEnvInfo').style.display = 'block';
737
+ }
738
+
739
+ const tbody = document.getElementById('tokenTableBody');
740
+ tbody.innerHTML = '';
741
+ data.tokens.forEach(token => {
742
+ const row = tbody.insertRow();
743
+ row.innerHTML = `
744
+ <td><span class="token-id">${token.refresh_token}</span></td>
745
+ <td>${token.is_active ? '✅ 活跃' : '❌ 失效'}</td>
746
+ <td>${token.has_access_token ? '✅ 有效' : '❌ 无效'}</td>
747
+ <td>${token.refresh_count}</td>
748
+ <td>${token.usage_count}</td>
749
+ <td>
750
+ <div class="action-buttons">
751
+ <button onclick="deleteToken('${token.refresh_token}')" class="btn-danger btn-small">🗑️ 删除</button>
752
+ </div>
753
+ </td>
754
+ `;
755
+ });
756
+ }
757
+ } catch (error) {
758
+ console.error('刷新状态失败:', error);
759
+ showStatus('刷新状态失败: ' + error.message, 'error');
760
+ }
761
+ }
762
+
763
+ async function deleteToken(tokenId) {
764
+ if (!confirm('确定要删除这个Token吗?')) return;
765
+
766
+ try {
767
+ const response = await fetch('/token/remove', {
768
+ method: 'POST',
769
+ headers: { 'Content-Type': 'application/json' },
770
+ body: JSON.stringify({ refresh_token: tokenId })
771
+ });
772
+ const data = await response.json();
773
+ if (data.success) {
774
+ showStatus('Token删除成功', 'success');
775
+ refreshTokenStatus();
776
+ } else {
777
+ showStatus(data.message || 'Token删除失败', 'error');
778
+ }
779
+ } catch (error) {
780
+ showStatus('删除请求失败: ' + error.message, 'error');
781
+ }
782
+ }
783
+
784
+ async function exportTokens() {
785
+ const superAdminKey = document.getElementById('superAdminKey').value;
786
+
787
+ if (!superAdminKey) {
788
+ showStatus('请输入超级管理员密钥', 'error', 'exportStatus');
789
+ return;
790
+ }
791
+
792
+ try {
793
+ showStatus('正在准备导出...', 'success', 'exportStatus');
794
+
795
+ const response = await fetch('/token/export', {
796
+ method: 'POST',
797
+ headers: { 'Content-Type': 'application/json' },
798
+ body: JSON.stringify({ super_admin_key: superAdminKey })
799
+ });
800
+
801
+ const data = await response.json();
802
+
803
+ if (data.success) {
804
+ // 使用浏览器下载API
805
+ downloadTextFile(data.content, data.suggested_filename);
806
+
807
+ showStatus(`导出成功!包含 ${data.token_count} 个token`, 'success', 'exportStatus');
808
+ document.getElementById('superAdminKey').value = '';
809
+ } else {
810
+ showStatus(data.message || '导出失败', 'error', 'exportStatus');
811
+ }
812
+ } catch (error) {
813
+ showStatus('导出请求失败: ' + error.message, 'error', 'exportStatus');
814
+ }
815
+ }
816
+
817
+ function downloadTextFile(content, filename) {
818
+ try {
819
+ // 创建Blob对象
820
+ const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
821
+
822
+ // 创建下载链接
823
+ const url = URL.createObjectURL(blob);
824
+ const link = document.createElement('a');
825
+ link.href = url;
826
+ link.download = filename;
827
+
828
+ // 触发下载
829
+ document.body.appendChild(link);
830
+ link.click();
831
+
832
+ // 清理
833
+ document.body.removeChild(link);
834
+ URL.revokeObjectURL(url);
835
+
836
+ console.log(`文件 ${filename} 下载完成`);
837
+ } catch (error) {
838
+ console.error('下载文件时出错:', error);
839
+ showStatus('下载文件时出错: ' + error.message, 'error', 'exportStatus');
840
+ }
841
+ }
842
+
843
+ async function refreshAllTokens() {
844
+ try {
845
+ const response = await fetch('/token/refresh', { method: 'POST' });
846
+ const data = await response.json();
847
+ if (data.success) {
848
+ showStatus('所有Token刷新已触发', 'success');
849
+ setTimeout(refreshTokenStatus, 2000);
850
+ } else {
851
+ showStatus(data.message, 'error');
852
+ }
853
+ } catch (error) { showStatus('刷新失败: ' + error.message, 'error'); }
854
+ }
855
+
856
+ async function addTokens() {
857
+ const tokens = document.getElementById('tokensInput').value.split('\\n').map(t => t.trim()).filter(t => t);
858
+ if (tokens.length === 0) { showStatus('请输入至少一个token', 'error', 'addTokensStatus'); return; }
859
+ try {
860
+ const response = await fetch('/token/add', {
861
+ method: 'POST',
862
+ headers: { 'Content-Type': 'application/json' },
863
+ body: JSON.stringify({ tokens })
864
+ });
865
+ const data = await response.json();
866
+ if (data.success) {
867
+ showStatus(`成功添加 ${data.added_tokens} 个token`, 'success', 'addTokensStatus');
868
+ document.getElementById('tokensInput').value = '';
869
+ } else {
870
+ showStatus(data.message, 'error', 'addTokensStatus');
871
+ }
872
+ } catch (error) { showStatus('添加失败: ' + error.message, 'error', 'addTokensStatus'); }
873
+ }
874
+
875
+ function clearTokensInput() { document.getElementById('tokensInput').value = ''; }
876
+
877
+ function addEmailRow() {
878
+ const tbody = document.getElementById('emailTableBody');
879
+ const row = tbody.insertRow();
880
+ row.innerHTML = `<td><input type="email" placeholder="[email protected]"></td><td><input type="url" placeholder="https://..."></td><td><button onclick="removeEmailRow(this)" class="btn-danger btn-small">❌</button></td>`;
881
+ }
882
+
883
+ function removeEmailRow(button) { button.closest('tr').remove(); }
884
+
885
+ function clearEmails() {
886
+ document.getElementById('emailTableBody').innerHTML = `<tr><td><input type="email" placeholder="[email protected]"></td><td><input type="url" placeholder="https://..."></td><td><button onclick="removeEmailRow(this)" class="btn-danger btn-small">❌</button></td></tr>`;
887
+ }
888
+
889
+ async function processEmails() {
890
+ const rows = document.querySelectorAll('#emailTableBody tr');
891
+ const emailUrlPairs = [];
892
+ rows.forEach(row => {
893
+ const inputs = row.querySelectorAll('input');
894
+ const email = inputs[0].value.trim();
895
+ const url = inputs[1].value.trim();
896
+ if (email && url) emailUrlPairs.push({email, url});
897
+ });
898
+
899
+ if (emailUrlPairs.length === 0) { showStatus('请至少添加一个邮箱和URL', 'error', 'emailResults'); return; }
900
+
901
+ document.getElementById('emailLoading').style.display = 'block';
902
+ document.getElementById('emailResults').innerHTML = '';
903
+
904
+ try {
905
+ const response = await fetch('/login/batch', {
906
+ method: 'POST',
907
+ headers: { 'Content-Type': 'application/json' },
908
+ body: JSON.stringify({
909
+ email_url_pairs: emailUrlPairs,
910
+ max_workers: parseInt(document.getElementById('maxWorkers').value)
911
+ })
912
+ });
913
+ const data = await response.json();
914
+ document.getElementById('emailLoading').style.display = 'none';
915
+
916
+ if (data.success) {
917
+ let resultsHtml = `<div class="status success">批量处理完成!成功: ${data.success_count}/${data.total_count}</div>`;
918
+ resultsHtml += '<table><tr><th>邮箱</th><th>状态</th><th>Token</th><th>用户创建</th></tr>';
919
+ Object.entries(data.results).forEach(([email, result]) => {
920
+ let userCreationStatus = '';
921
+ if (result.status.includes('成功并已创建用户')) {
922
+ userCreationStatus = '✅ 已创建';
923
+ } else if (result.status.includes('创建用户失败')) {
924
+ userCreationStatus = '❌ 创建失败';
925
+ } else if (result.status.includes('获取access_token失败')) {
926
+ userCreationStatus = '⚠️ Token失败';
927
+ } else if (result.refresh_token) {
928
+ userCreationStatus = '🔄 未尝试';
929
+ } else {
930
+ userCreationStatus = '-';
931
+ }
932
+
933
+ resultsHtml += `<tr><td>${email}</td><td>${result.status}</td><td>${result.refresh_token ? '✅ 已获取' : '❌ 失败'}</td><td>${userCreationStatus}</td></tr>`;
934
+ });
935
+ resultsHtml += '</table>';
936
+ document.getElementById('emailResults').innerHTML = resultsHtml;
937
+ } else {
938
+ showStatus(data.message, 'error', 'emailResults');
939
+ }
940
+ } catch (error) {
941
+ document.getElementById('emailLoading').style.display = 'none';
942
+ showStatus('处理失败: ' + error.message, 'error', 'emailResults');
943
+ }
944
+ }
945
+
946
+ async function logout() {
947
+ if (!confirm('确定要退出登录吗?')) return;
948
+
949
+ try {
950
+ const response = await fetch('/admin/logout', { method: 'POST' });
951
+ const data = await response.json();
952
+ if (data.success) {
953
+ window.location.href = '/admin/login';
954
+ }
955
+ } catch (error) {
956
+ console.error('登出失败:', error);
957
+ }
958
+ }
959
+
960
+ function showStatus(message, type, targetId = null) {
961
+ const status = `<div class="status ${type}">${message}</div>`;
962
+ if (targetId) {
963
+ document.getElementById(targetId).innerHTML = status;
964
+ } else {
965
+ const container = document.querySelector('.container');
966
+ const statusDiv = document.createElement('div');
967
+ statusDiv.innerHTML = status;
968
+ container.insertBefore(statusDiv, container.firstChild);
969
+ setTimeout(() => statusDiv.remove(), 5000);
970
+ }
971
+ }
972
+
973
+ // 页面加载时刷新状态
974
+ document.addEventListener('DOMContentLoaded', function() {
975
+ refreshTokenStatus();
976
+ });
977
+ </script>
978
+ </body>
979
+ </html>
980
+ """