playingapi commited on
Commit
75beee1
·
verified ·
1 Parent(s): 5e68400

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +626 -0
main.py ADDED
@@ -0,0 +1,626 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import time
4
+ import uuid
5
+ import asyncio
6
+ import threading
7
+ from typing import Any, Dict, List, Optional, TypedDict, Union
8
+
9
+ import httpx
10
+ from fastapi import FastAPI, HTTPException, Depends, Query
11
+ from fastapi.responses import StreamingResponse
12
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ # Retool Account Management
17
+ class RetoolAccount(TypedDict):
18
+ domain_name: str
19
+ x_xsrf_token: str
20
+ accessToken: str
21
+ is_valid: bool
22
+ last_used: float
23
+ error_count: int
24
+ agents: List[Dict[str, Any]]
25
+
26
+
27
+ # Global variables
28
+ VALID_CLIENT_KEYS: set = set()
29
+ RETOOL_ACCOUNTS: List[RetoolAccount] = []
30
+ AVAILABLE_MODELS: List[Dict[str, Any]] = []
31
+ account_rotation_lock = threading.Lock()
32
+ MAX_ERROR_COUNT = 3
33
+ ERROR_COOLDOWN = 300 # 5 minutes cooldown for accounts with errors
34
+ DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true"
35
+
36
+
37
+ # Pydantic Models
38
+ class ChatMessage(BaseModel):
39
+ role: str
40
+ content: Union[str, List[Dict[str, Any]]]
41
+
42
+
43
+ class ChatCompletionRequest(BaseModel):
44
+ model: str
45
+ messages: List[ChatMessage]
46
+ stream: bool = True
47
+ temperature: Optional[float] = None
48
+ max_tokens: Optional[int] = None
49
+ top_p: Optional[float] = None
50
+
51
+
52
+ class ModelInfo(BaseModel):
53
+ id: str
54
+ object: str = "model"
55
+ created: int
56
+ owned_by: str
57
+ name: Optional[str] = None
58
+
59
+
60
+ class ModelList(BaseModel):
61
+ object: str = "list"
62
+ data: List[ModelInfo]
63
+
64
+
65
+ class ChatCompletionChoice(BaseModel):
66
+ message: ChatMessage
67
+ index: int = 0
68
+ finish_reason: str = "stop"
69
+
70
+
71
+ class ChatCompletionResponse(BaseModel):
72
+ id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
73
+ object: str = "chat.completion"
74
+ created: int = Field(default_factory=lambda: int(time.time()))
75
+ model: str
76
+ choices: List[ChatCompletionChoice]
77
+ usage: Dict[str, int] = Field(
78
+ default_factory=lambda: {
79
+ "prompt_tokens": 0,
80
+ "completion_tokens": 0,
81
+ "total_tokens": 0,
82
+ }
83
+ )
84
+
85
+
86
+ class StreamChoice(BaseModel):
87
+ delta: Dict[str, Any] = Field(default_factory=dict)
88
+ index: int = 0
89
+ finish_reason: Optional[str] = None
90
+
91
+
92
+ class StreamResponse(BaseModel):
93
+ id: str = Field(default_factory=lambda: f"chatcmpl-{uuid.uuid4().hex}")
94
+ object: str = "chat.completion.chunk"
95
+ created: int = Field(default_factory=lambda: int(time.time()))
96
+ model: str
97
+ choices: List[StreamChoice]
98
+
99
+
100
+ # FastAPI App
101
+ app = FastAPI(title="Retool OpenAI API Adapter")
102
+ security = HTTPBearer(auto_error=False)
103
+
104
+
105
+ def log_debug(message: str):
106
+ """Debug日志函数"""
107
+ if DEBUG_MODE:
108
+ print(f"[DEBUG] {message}")
109
+
110
+
111
+ def load_client_api_keys():
112
+ """从client_api_keys.json加载客户端API密钥"""
113
+ global VALID_CLIENT_KEYS
114
+ try:
115
+ with open("client_api_keys.json", "r", encoding="utf-8") as f:
116
+ keys = json.load(f)
117
+ VALID_CLIENT_KEYS = set(keys) if isinstance(keys, list) else set()
118
+ print(f"成功加载 {len(VALID_CLIENT_KEYS)} 个客户端API密钥")
119
+ except FileNotFoundError:
120
+ print("错误: client_api_keys.json未找到。客户端认证将失败。")
121
+ VALID_CLIENT_KEYS = set()
122
+ except Exception as e:
123
+ print(f"加载client_api_keys.json时出错: {e}")
124
+ VALID_CLIENT_KEYS = set()
125
+
126
+
127
+ def load_retool_accounts_from_file():
128
+ """从retool.json加载Retool账户"""
129
+ try:
130
+ with open("retool.json", "r", encoding="utf-8") as f:
131
+ accounts = json.load(f)
132
+ if not isinstance(accounts, list):
133
+ print("警告: retool.json应包含账户对象列表")
134
+ return []
135
+
136
+ result = []
137
+ for acc in accounts:
138
+ domain_name = acc.get("domain_name")
139
+ x_xsrf_token = acc.get("x_xsrf_token")
140
+ access_token = acc.get("accessToken")
141
+ if domain_name and x_xsrf_token and access_token:
142
+ result.append({
143
+ "domain_name": domain_name,
144
+ "x_xsrf_token": x_xsrf_token,
145
+ "accessToken": access_token,
146
+ "is_valid": True,
147
+ "last_used": 0,
148
+ "error_count": 0,
149
+ "agents": []
150
+ })
151
+ print(f"成功加载 {len(result)} 个Retool账户")
152
+ return result
153
+ except FileNotFoundError:
154
+ print("错误: retool.json未找到。API调用将失败。")
155
+ return []
156
+ except Exception as e:
157
+ print(f"加载retool.json时出错: {e}")
158
+ return []
159
+
160
+
161
+ async def retool_query_agents(client: httpx.AsyncClient, account: Dict[str, Any]):
162
+ """查询账户可用的Agents"""
163
+ url = f"https://{account['domain_name']}/api/agents"
164
+
165
+ headers = {
166
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0",
167
+ "Accept": "application/json",
168
+ "Accept-Encoding": "gzip, deflate, br, zstd",
169
+ "x-xsrf-token": account["x_xsrf_token"],
170
+ "Cookie": f"accessToken={account['accessToken']}",
171
+ }
172
+
173
+ try:
174
+ response = await client.get(url, headers=headers)
175
+ response.raise_for_status()
176
+ return response.json()["agents"]
177
+ except Exception as e:
178
+ log_debug(f"查询账户 {account['domain_name']} 的Agents时出错: {e}")
179
+ return []
180
+
181
+
182
+ async def retool_get_thread_id(client: httpx.AsyncClient, account: Dict[str, Any], agent_id: str):
183
+ """创建新的对话线程"""
184
+ url = f"https://{account['domain_name']}/api/agents/{agent_id}/threads"
185
+
186
+ payload = {"name": "", "timezone": ""}
187
+
188
+ headers = {
189
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0",
190
+ "Accept": "application/json",
191
+ "Accept-Encoding": "gzip, deflate, br, zstd",
192
+ "Content-Type": "application/json",
193
+ "x-xsrf-token": account["x_xsrf_token"],
194
+ "Cookie": f"accessToken={account['accessToken']}",
195
+ }
196
+
197
+ response = await client.post(url, json=payload, headers=headers)
198
+ response.raise_for_status()
199
+ return response.json()["id"]
200
+
201
+
202
+ async def retool_send_message(client: httpx.AsyncClient, account: Dict[str, Any], agent_id: str, thread_id: str, message: str):
203
+ """发送消息到线程"""
204
+ url = f"https://{account['domain_name']}/api/agents/{agent_id}/threads/{thread_id}/messages"
205
+
206
+ payload = {"type": "text", "text": message, "timezone": "Asia/Shanghai"}
207
+
208
+ headers = {
209
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0",
210
+ "Accept": "application/json",
211
+ "Accept-Encoding": "gzip, deflate, br, zstd",
212
+ "Content-Type": "application/json",
213
+ "x-xsrf-token": account["x_xsrf_token"],
214
+ "Cookie": f"accessToken={account['accessToken']}",
215
+ }
216
+
217
+ response = await client.post(url, json=payload, headers=headers)
218
+ response.raise_for_status()
219
+ return response.json()["content"]["runId"]
220
+
221
+
222
+ async def retool_get_message(client: httpx.AsyncClient, account: Dict[str, Any], agent_id: str, log_id: str):
223
+ """获取消息响应"""
224
+ url = f"https://{account['domain_name']}/api/agents/{agent_id}/logs/{log_id}"
225
+
226
+ headers = {
227
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Edg/136.0.0.0",
228
+ "Accept": "application/json",
229
+ "Accept-Encoding": "gzip, deflate, br, zstd",
230
+ "x-xsrf-token": account["x_xsrf_token"],
231
+ "Cookie": f"accessToken={account['accessToken']}",
232
+ }
233
+
234
+ for _ in range(300): # 最多等待300秒
235
+ try:
236
+ response = await client.get(url, headers=headers)
237
+ response.raise_for_status()
238
+ data = response.json()
239
+ if data["status"] == "COMPLETED":
240
+ trace = data["trace"]
241
+ message = trace[-1]["data"]["data"]["content"]
242
+ return message
243
+ else:
244
+ await asyncio.sleep(1)
245
+ except Exception as e:
246
+ log_debug(f"获取消息时出错: {e}")
247
+ return None
248
+
249
+ return None
250
+
251
+
252
+ def format_messages_for_retool(messages: List[ChatMessage]) -> str:
253
+ """将消息历史格式化为Retool可接受的格式"""
254
+ formatted = ""
255
+ for msg in messages:
256
+ role = "Human" if msg.role == "user" else "Assistant"
257
+ content = msg.content if isinstance(msg.content, str) else json.dumps(msg.content)
258
+ formatted += f"\n\n{role}: {content}"
259
+
260
+ # 如果最后一条消息是用户消息,不需要添加Assistant:前缀
261
+ # 如果最后一条是助手消息,需要添加Human:前缀以便模型继续
262
+ if messages and messages[-1].role == "assistant":
263
+ formatted += "\n\nHuman: "
264
+
265
+ return formatted
266
+
267
+
268
+ def get_best_retool_account(model_id: str) -> Optional[Dict[str, Any]]:
269
+ """获取最佳可用的Retool账户"""
270
+ with account_rotation_lock:
271
+ now = time.time()
272
+
273
+ # 查找支持此模型的所有agent IDs
274
+ supported_agent_ids = []
275
+ for model in AVAILABLE_MODELS:
276
+ if model["id"] == model_id:
277
+ supported_agent_ids = model["agents"]
278
+ break
279
+
280
+ if not supported_agent_ids:
281
+ return None
282
+
283
+ # 筛选出拥有支持此模型的agent且有效的账户
284
+ valid_accounts = []
285
+ for acc in RETOOL_ACCOUNTS:
286
+ has_agent = any(agent["id"] in supported_agent_ids for agent in acc["agents"])
287
+ is_valid = acc["is_valid"] and (
288
+ acc["error_count"] < MAX_ERROR_COUNT or
289
+ now - acc["last_used"] > ERROR_COOLDOWN
290
+ )
291
+ if has_agent and is_valid:
292
+ # 找出此账户中支持该模型的第一个agent ID
293
+ for agent in acc["agents"]:
294
+ if agent["id"] in supported_agent_ids:
295
+ acc["selected_agent_id"] = agent["id"] # 记录选中的agent ID
296
+ break
297
+ valid_accounts.append(acc)
298
+
299
+ if not valid_accounts:
300
+ return None
301
+
302
+ # 重置冷却期账户的错误计数
303
+ for acc in valid_accounts:
304
+ if acc["error_count"] >= MAX_ERROR_COUNT and now - acc["last_used"] > ERROR_COOLDOWN:
305
+ acc["error_count"] = 0
306
+
307
+ # 按最后使用时间(最旧优先)和错误计数(最少优先)排序
308
+ valid_accounts.sort(key=lambda x: (x["last_used"], x["error_count"]))
309
+ account = valid_accounts[0]
310
+ account["last_used"] = now
311
+ return account
312
+
313
+
314
+ async def initialize_retool_environment():
315
+ """初始化Retool环境,加载账户和模型"""
316
+ global RETOOL_ACCOUNTS, AVAILABLE_MODELS
317
+
318
+ # 加载账户
319
+ accounts = load_retool_accounts_from_file()
320
+ if not accounts:
321
+ print("警告: 未找到有效的Retool账户")
322
+ return
323
+
324
+ # 查询每个账户的可用Agents
325
+ async with httpx.AsyncClient(timeout=30.0) as client:
326
+ tasks = [retool_query_agents(client, account) for account in accounts]
327
+ results = await asyncio.gather(*tasks, return_exceptions=True)
328
+
329
+ for i, result in enumerate(results):
330
+ if isinstance(result, Exception):
331
+ print(f"查询账户 {accounts[i]['domain_name']} 的Agents时出错: {result}")
332
+ accounts[i]["agents"] = []
333
+ else:
334
+ accounts[i]["agents"] = result
335
+ log_debug(f"账户 {accounts[i]['domain_name']} 有 {len(result)} 个Agents")
336
+
337
+ # 更新全局账户列表
338
+ RETOOL_ACCOUNTS = accounts
339
+
340
+ # 聚合所有唯一模型 (使用model字段而非name)
341
+ model_map = {} # 用于映射模型ID到实际模型名称
342
+ all_models = {}
343
+ for account in RETOOL_ACCOUNTS:
344
+ for agent in account["agents"]:
345
+ # 获取实际模型名称,如claude-sonnet-4-20250514
346
+ model_name = agent.get("data", {}).get("model", "unknown")
347
+ # 提取模型系列名称,如claude-sonnet-4
348
+ model_series = model_name.split("-")[0:3]
349
+ model_series = "-".join(model_series)
350
+
351
+ # 创建模型映射,将agent ID映射到实际模型名称
352
+ model_map[agent["id"]] = model_name
353
+
354
+ if model_series not in all_models:
355
+ all_models[model_series] = {
356
+ "id": model_series, # 使用模型系列作为ID
357
+ "name": agent["name"], # 保留agent名称作为显示名称
358
+ "model_name": model_name, # 存储完整模型名称
359
+ "owned_by": "anthropic" if "claude" in model_name.lower() else "openai",
360
+ "agents": [agent["id"]] # 存储支持此模型的所有agent IDs
361
+ }
362
+ else:
363
+ # 添加到现有模型的agents列表
364
+ all_models[model_series]["agents"].append(agent["id"])
365
+
366
+ AVAILABLE_MODELS = list(all_models.values())
367
+ print(f"成功加载 {len(AVAILABLE_MODELS)} 个唯一模型系列")
368
+
369
+ # 打印模型映射关系
370
+ for model in AVAILABLE_MODELS:
371
+ print(f" - 模型系列: {model['id']}, 实际模型: {model['model_name']}, 支持的Agents: {len(model['agents'])}")
372
+
373
+
374
+ async def authenticate_client(
375
+ auth: Optional[HTTPAuthorizationCredentials] = Depends(security),
376
+ ):
377
+ """基于Authorization头中的API密钥验证客户端"""
378
+ if not VALID_CLIENT_KEYS:
379
+ raise HTTPException(
380
+ status_code=503,
381
+ detail="服务不可用: 服务器未配置客户端API密钥。",
382
+ )
383
+
384
+ if not auth or not auth.credentials:
385
+ raise HTTPException(
386
+ status_code=401,
387
+ detail="需要在Authorization头中提供API密钥。",
388
+ headers={"WWW-Authenticate": "Bearer"},
389
+ )
390
+
391
+ if auth.credentials not in VALID_CLIENT_KEYS:
392
+ raise HTTPException(status_code=403, detail="无效的客户端API密钥。")
393
+
394
+
395
+ @app.on_event("startup")
396
+ async def startup():
397
+ """应用启动时初始化配置"""
398
+ print("启动Retool OpenAI API适配器服务器...")
399
+ load_client_api_keys()
400
+ await initialize_retool_environment()
401
+ print("服务器初始化完成。")
402
+
403
+
404
+ def get_models_list_response() -> ModelList:
405
+ """构建模型列表响应"""
406
+ model_infos = [
407
+ ModelInfo(
408
+ id=model.get("id", "unknown"),
409
+ name=f"{model.get('name', 'Unknown')} ({model.get('model_name', 'unknown')})",
410
+ created=int(time.time()),
411
+ owned_by=model.get("owned_by", "anthropic")
412
+ )
413
+ for model in AVAILABLE_MODELS
414
+ ]
415
+ return ModelList(data=model_infos)
416
+
417
+
418
+ @app.get("/v1/models", response_model=ModelList)
419
+ async def list_v1_models(_: None = Depends(authenticate_client)):
420
+ """列出可用模型 - 需认证"""
421
+ return get_models_list_response()
422
+
423
+
424
+ @app.get("/models", response_model=ModelList)
425
+ async def list_models_no_auth():
426
+ """列出可用模型 - 无需认证(客户端兼容性)"""
427
+ return get_models_list_response()
428
+
429
+
430
+ @app.get("/debug")
431
+ async def toggle_debug(enable: bool = Query(None)):
432
+ """切换调试模式"""
433
+ global DEBUG_MODE
434
+ if enable is not None:
435
+ DEBUG_MODE = enable
436
+ return {"debug_mode": DEBUG_MODE}
437
+
438
+
439
+ async def retool_stream_generator(full_message: str, model_id: str):
440
+ """生成模拟的流式响应"""
441
+ stream_id = f"chatcmpl-{uuid.uuid4().hex}"
442
+ created_time = int(time.time())
443
+
444
+ # 发送初始角色增量
445
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={'role': 'assistant'})]).json()}\n\n"
446
+
447
+ # 将完整消息分成小块
448
+ chunk_size = 5 # 每个块的字符数
449
+ for i in range(0, len(full_message), chunk_size):
450
+ chunk = full_message[i:i+chunk_size]
451
+ delta = {"content": chunk}
452
+
453
+ response = StreamResponse(
454
+ id=stream_id,
455
+ created=created_time,
456
+ model=model_id,
457
+ choices=[StreamChoice(delta=delta)]
458
+ )
459
+
460
+ yield f"data: {response.json()}\n\n"
461
+ await asyncio.sleep(0.01) # 添加小延迟使流更自然
462
+
463
+ # 发送完成信号
464
+ yield f"data: {StreamResponse(id=stream_id, created=created_time, model=model_id, choices=[StreamChoice(delta={}, finish_reason='stop')]).json()}\n\n"
465
+ yield "data: [DONE]\n\n"
466
+
467
+
468
+ async def error_stream_generator(error_detail: str, status_code: int):
469
+ """生成错误流响应"""
470
+ yield f'data: {json.dumps({"error": {"message": error_detail, "type": "retool_api_error", "code": status_code}})}\n\n'
471
+ yield "data: [DONE]\n\n"
472
+
473
+
474
+ @app.post("/v1/chat/completions")
475
+ async def chat_completions(
476
+ request: ChatCompletionRequest, _: None = Depends(authenticate_client)
477
+ ):
478
+ """使用Retool后端创建聊天完成"""
479
+ # 检查模型是否存在
480
+ model_exists = any(model["id"] == request.model for model in AVAILABLE_MODELS)
481
+ if not model_exists:
482
+ raise HTTPException(status_code=404, detail=f"未找到模型 '{request.model}'。")
483
+
484
+ if not request.messages:
485
+ raise HTTPException(status_code=400, detail="请求中未提供消息。")
486
+
487
+ log_debug(f"处理模型请求: {request.model}")
488
+
489
+ # 格式化消息历史
490
+ formatted_message = format_messages_for_retool(request.messages)
491
+ log_debug(f"格式化后的消息: {formatted_message[:100]}...")
492
+
493
+ # 尝试所有可用账户
494
+ for attempt in range(len(RETOOL_ACCOUNTS)):
495
+ account = get_best_retool_account(request.model)
496
+ if not account:
497
+ raise HTTPException(
498
+ status_code=503,
499
+ detail=f"没有可用的账户支持模型 '{request.model}'。"
500
+ )
501
+
502
+ # 获取为此账户选择的agent ID
503
+ agent_id = account.get("selected_agent_id")
504
+ if not agent_id:
505
+ continue
506
+
507
+ try:
508
+ async with httpx.AsyncClient(timeout=120.0) as client:
509
+ # 创建线程
510
+ thread_id = await retool_get_thread_id(client, account, agent_id)
511
+ log_debug(f"创建线程: {thread_id}")
512
+
513
+ # 发送消息
514
+ log_id = await retool_send_message(client, account, agent_id, thread_id, formatted_message)
515
+ log_debug(f"发送消息, 日志ID: {log_id}")
516
+
517
+ # 获取响应
518
+ message = await retool_get_message(client, account, agent_id, log_id)
519
+ if not message:
520
+ raise Exception("获取消息响应超时")
521
+
522
+ log_debug(f"收到响应: {message[:100]}...")
523
+
524
+ # 根据请求类型返回流式或非流式响应
525
+ if request.stream:
526
+ return StreamingResponse(
527
+ retool_stream_generator(message, request.model),
528
+ media_type="text/event-stream",
529
+ headers={
530
+ "Cache-Control": "no-cache",
531
+ "Connection": "keep-alive",
532
+ "X-Accel-Buffering": "no",
533
+ },
534
+ )
535
+ else:
536
+ # 非流式响应
537
+ return ChatCompletionResponse(
538
+ model=request.model,
539
+ choices=[
540
+ ChatCompletionChoice(
541
+ message=ChatMessage(
542
+ role="assistant",
543
+ content=message
544
+ )
545
+ )
546
+ ],
547
+ )
548
+
549
+ except Exception as e:
550
+ log_debug(f"请求错误: {e}")
551
+
552
+ with account_rotation_lock:
553
+ account["error_count"] += 1
554
+ if isinstance(e, httpx.HTTPStatusError) and e.response.status_code in [401, 403]:
555
+ account["is_valid"] = False
556
+ print(f"账户 {account['domain_name']} 因认证错误被标记为无效。")
557
+
558
+ # 所有尝试都失败
559
+ if request.stream:
560
+ return StreamingResponse(
561
+ error_stream_generator("所有Retool API请求尝试均失败。", 503),
562
+ media_type="text/event-stream",
563
+ status_code=503,
564
+ )
565
+ else:
566
+ raise HTTPException(status_code=503, detail="所有Retool API请求尝试均失败。")
567
+
568
+
569
+ if __name__ == "__main__":
570
+ import uvicorn
571
+
572
+ # 设置环境变量以启用调试模式
573
+ if os.environ.get("DEBUG_MODE", "").lower() == "true":
574
+ DEBUG_MODE = True
575
+ print("通过环境变量启用调试模式")
576
+
577
+ # 检查必要文件
578
+ if not os.path.exists("retool.json"):
579
+ print("警告: retool.json未找到。创建示例文件。")
580
+ dummy_data = [
581
+ {
582
+ "domain_name": "your-domain.retool.com",
583
+ "x_xsrf_token": "your-xsrf-token",
584
+ "accessToken": "your-access-token"
585
+ }
586
+ ]
587
+ with open("retool.json", "w", encoding="utf-8") as f:
588
+ json.dump(dummy_data, f, indent=4)
589
+ print("创建示例retool.json。请替换为有效的Retool数据。")
590
+
591
+ if not os.path.exists("client_api_keys.json"):
592
+ print("警告: client_api_keys.json未找到。创建示例文件。")
593
+ dummy_key = f"sk-dummy-{uuid.uuid4().hex}"
594
+ with open("client_api_keys.json", "w", encoding="utf-8") as f:
595
+ json.dump([dummy_key], f, indent=2)
596
+ print(f"创建示例client_api_keys.json,密钥: {dummy_key}")
597
+
598
+ # 加载配置
599
+ load_client_api_keys()
600
+ asyncio.run(initialize_retool_environment())
601
+
602
+ print("\n--- Retool OpenAI API适配器 ---")
603
+ print(f"调试模式: {DEBUG_MODE}")
604
+ print("端点:")
605
+ print(" GET /v1/models (需客户端API密钥验证)")
606
+ print(" GET /models (无需验证)")
607
+ print(" POST /v1/chat/completions (需客户端API密钥验证)")
608
+ print(" GET /debug?enable=[true|false] (切换调试模式)")
609
+
610
+ print(f"\n客户端API密钥: {len(VALID_CLIENT_KEYS)}")
611
+ if RETOOL_ACCOUNTS:
612
+ print(f"Retool账户: {len(RETOOL_ACCOUNTS)}")
613
+ for account in RETOOL_ACCOUNTS:
614
+ print(f" - {account['domain_name']}: {len(account['agents'])} 个Agents")
615
+ else:
616
+ print("Retool账户: 未加载。检查retool.json。")
617
+
618
+ if AVAILABLE_MODELS:
619
+ models = sorted([m.get("id", "unknown") for m in AVAILABLE_MODELS])
620
+ print(f"可用模型系列: {len(AVAILABLE_MODELS)}")
621
+ print(f"模型系列列表: {', '.join(models)}")
622
+ else:
623
+ print("可用模型: 未加载。检查账户配置。")
624
+ print("------------------------------------")
625
+
626
+ uvicorn.run(app, host="0.0.0.0", port=8000)