heiyuheiyu commited on
Commit
f4f950f
·
verified ·
1 Parent(s): 37839db

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +490 -0
app.py ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Claude Web API Proxy - HuggingFace Spaces Enhanced
3
+ """
4
+ from fastapi import FastAPI, Request
5
+ from fastapi.responses import StreamingResponse, JSONResponse, HTMLResponse
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+ from contextlib import asynccontextmanager
8
+ import httpx
9
+ import json
10
+ import asyncio
11
+ from datetime import datetime
12
+ from typing import Optional, Dict, List
13
+ import os
14
+ import uuid
15
+ import logging
16
+ from enum import Enum
17
+ from pathlib import Path
18
+ from huggingface_hub import HfApi, hf_hub_download, upload_file
19
+ from fake_useragent import UserAgent
20
+ import random
21
+
22
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # ============ AccountManager ============
26
+ class AccountStatus(str, Enum):
27
+ ACTIVE = "Active"
28
+ LIMITED = "Limited"
29
+ ERROR = "Error"
30
+ DISABLE = "Disable"
31
+
32
+ class Account:
33
+ def __init__(self, id: str, provider: str, credential_type: str, credential_value: str,
34
+ phone: str = "", email: str = "", note: str = ""):
35
+ self.id = id
36
+ self.provider = provider
37
+ self.account_type = "网页版"
38
+ self.credential_type = credential_type
39
+ self.credential_value = credential_value
40
+ self.phone = phone
41
+ self.email = email
42
+ self.note = note
43
+ self.status = AccountStatus.ACTIVE
44
+ self.daily_calls = 0
45
+ self.total_calls = 0
46
+ self.last_call_time = None
47
+ self.created_at = datetime.now()
48
+
49
+ def to_dict(self):
50
+ return {
51
+ "id": self.id,
52
+ "provider": self.provider,
53
+ "account_type": self.account_type,
54
+ "credential_type": self.credential_type,
55
+ "credential_value_masked": self.credential_value[-10:] if len(self.credential_value) > 10 else "***",
56
+ "credential_value": self.credential_value,
57
+ "phone": self.phone,
58
+ "email": self.email,
59
+ "note": self.note,
60
+ "status": self.status,
61
+ "daily_calls": self.daily_calls,
62
+ "total_calls": self.total_calls,
63
+ "last_call_time": self.last_call_time.isoformat() if self.last_call_time else None,
64
+ "created_at": self.created_at.isoformat(),
65
+ "created_days": (datetime.now() - self.created_at).days
66
+ }
67
+
68
+ class AccountManager:
69
+ def __init__(self):
70
+ self.accounts: Dict[str, Account] = {}
71
+ self.current_index = 0
72
+
73
+ def add_account(self, account: Account):
74
+ self.accounts[account.id] = account
75
+
76
+ def remove_account(self, account_id: str):
77
+ if account_id in self.accounts:
78
+ del self.accounts[account_id]
79
+
80
+ def get_next_active_account(self, provider: str = None) -> Optional[Account]:
81
+ active = [a for a in self.accounts.values() if a.status == AccountStatus.ACTIVE]
82
+ if provider:
83
+ active = [a for a in active if a.provider == provider]
84
+ if not active:
85
+ return None
86
+ self.current_index = (self.current_index + 1) % len(active)
87
+ return active[self.current_index]
88
+
89
+ def update_account_status(self, account_id: str, status: AccountStatus):
90
+ if account_id in self.accounts:
91
+ self.accounts[account_id].status = status
92
+
93
+ def record_call(self, account_id: str, success: bool, error_msg: str = ""):
94
+ if account_id not in self.accounts:
95
+ return
96
+ acc = self.accounts[account_id]
97
+ acc.total_calls += 1
98
+ acc.daily_calls += 1
99
+ acc.last_call_time = datetime.now()
100
+ if not success:
101
+ if "limit" in error_msg.lower():
102
+ acc.status = AccountStatus.LIMITED
103
+ else:
104
+ acc.status = AccountStatus.ERROR
105
+
106
+ def reset_daily_calls(self):
107
+ for acc in self.accounts.values():
108
+ acc.daily_calls = 0
109
+
110
+ def to_dict(self):
111
+ return {aid: acc.to_dict() for aid, acc in self.accounts.items()}
112
+
113
+ def from_dict(self, data: dict):
114
+ for aid, acc_data in data.items():
115
+ acc = Account(
116
+ id=acc_data["id"],
117
+ provider=acc_data["provider"],
118
+ credential_type=acc_data["credential_type"],
119
+ credential_value=acc_data["credential_value"],
120
+ phone=acc_data.get("phone", ""),
121
+ email=acc_data.get("email", ""),
122
+ note=acc_data.get("note", "")
123
+ )
124
+ acc.status = AccountStatus(acc_data["status"])
125
+ acc.daily_calls = acc_data.get("daily_calls", 0)
126
+ acc.total_calls = acc_data.get("total_calls", 0)
127
+ if acc_data.get("last_call_time"):
128
+ acc.last_call_time = datetime.fromisoformat(acc_data["last_call_time"])
129
+ acc.created_at = datetime.fromisoformat(acc_data["created_at"])
130
+ self.accounts[aid] = acc
131
+
132
+ # ============ DatasetBackup ============
133
+ class DatasetBackup:
134
+ def __init__(self):
135
+ self.dataset_name = os.getenv("HF_BACKUP_REPO", "multi-ai-proxy-backup")
136
+ self.token = os.getenv("HF_TOKEN")
137
+ self.api = HfApi(token=self.token) if self.token else None
138
+ self.local_dir = Path("/tmp/backup")
139
+ self.local_dir.mkdir(exist_ok=True)
140
+
141
+ def backup_accounts(self, accounts_data: dict):
142
+ file_path = self.local_dir / "accounts.json"
143
+ with open(file_path, 'w') as f:
144
+ json.dump(accounts_data, f, indent=2)
145
+ if self.api:
146
+ try:
147
+ upload_file(path_or_fileobj=str(file_path), path_in_repo="accounts.json",
148
+ repo_id=self.dataset_name, repo_type="dataset", token=self.token)
149
+ logger.info("Accounts backed up")
150
+ except Exception as e:
151
+ logger.error(f"Backup failed: {e}")
152
+
153
+ def restore_accounts(self) -> dict:
154
+ try:
155
+ if self.api:
156
+ file_path = hf_hub_download(repo_id=self.dataset_name, filename="accounts.json",
157
+ repo_type="dataset", token=self.token)
158
+ with open(file_path, 'r') as f:
159
+ return json.load(f)
160
+ except Exception as e:
161
+ logger.warning(f"Restore failed: {e}")
162
+ return {}
163
+
164
+ # ============ ConversationCache ============
165
+ class ConversationCache:
166
+ def __init__(self):
167
+ self.short_term: Dict[str, List] = {}
168
+
169
+ def add_message(self, conv_id: str, role: str, content: str):
170
+ if conv_id not in self.short_term:
171
+ self.short_term[conv_id] = []
172
+ self.short_term[conv_id].append({"role": role, "content": content})
173
+ if len(self.short_term[conv_id]) > 10:
174
+ self.short_term[conv_id] = self.short_term[conv_id][-10:]
175
+
176
+ def get_context(self, conv_id: str, query: str) -> List[Dict]:
177
+ return self.short_term.get(conv_id, [])[-5:]
178
+
179
+ # ============ FingerprintSimulator ============
180
+ class FingerprintSimulator:
181
+ def __init__(self):
182
+ self.ua = UserAgent()
183
+
184
+ def get_headers(self):
185
+ return {
186
+ 'User-Agent': self.ua.random,
187
+ 'Accept': 'application/json',
188
+ 'Accept-Language': random.choice(['en-US,en;q=0.9', 'zh-CN,zh;q=0.9']),
189
+ 'Referer': 'https://claude.ai/chats',
190
+ 'Origin': 'https://claude.ai',
191
+ 'Sec-Fetch-Dest': 'empty',
192
+ 'Sec-Fetch-Mode': 'cors',
193
+ 'Sec-Fetch-Site': 'same-origin'
194
+ }
195
+
196
+ # ============ Global Instances ============
197
+ account_manager = AccountManager()
198
+ dataset_backup = DatasetBackup()
199
+ conv_cache = ConversationCache()
200
+ fingerprint = FingerprintSimulator()
201
+ last_request_time = datetime.now()
202
+
203
+ async def check_limited_accounts():
204
+ while True:
205
+ await asyncio.sleep(7200)
206
+ for acc in account_manager.accounts.values():
207
+ if acc.status == AccountStatus.LIMITED:
208
+ try:
209
+ headers = fingerprint.get_headers()
210
+ headers['Cookie'] = f'sessionKey={acc.credential_value}'
211
+ async with httpx.AsyncClient() as client:
212
+ r = await client.get('https://claude.ai/api/organizations', headers=headers, timeout=10)
213
+ if r.status_code == 200:
214
+ acc.status = AccountStatus.ACTIVE
215
+ except:
216
+ pass
217
+
218
+ async def daily_reset():
219
+ while True:
220
+ await asyncio.sleep(86400)
221
+ account_manager.reset_daily_calls()
222
+
223
+ async def auto_backup():
224
+ while True:
225
+ await asyncio.sleep(3600)
226
+ dataset_backup.backup_accounts(account_manager.to_dict())
227
+
228
+ @asynccontextmanager
229
+ async def lifespan(app: FastAPI):
230
+ restore_mode = os.getenv("RESTORE_MODE", "all")
231
+ if restore_mode in ["all", "accounts"]:
232
+ data = dataset_backup.restore_accounts()
233
+ if data:
234
+ account_manager.from_dict(data)
235
+ logger.info(f"Restored {len(data)} accounts")
236
+ asyncio.create_task(check_limited_accounts())
237
+ asyncio.create_task(daily_reset())
238
+ asyncio.create_task(auto_backup())
239
+ logger.info("Application started")
240
+ yield
241
+ dataset_backup.backup_accounts(account_manager.to_dict())
242
+
243
+ app = FastAPI(title="Claude Proxy Enhanced", lifespan=lifespan)
244
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
245
+
246
+ @app.get("/")
247
+ async def root():
248
+ return {"name": "Claude Proxy Enhanced", "version": "3.0"}
249
+
250
+ @app.get("/health")
251
+ async def health():
252
+ return {"status": "ok", "accounts": len(account_manager.accounts)}
253
+
254
+ @app.get("/api/info")
255
+ async def api_info(request: Request):
256
+ base_url = str(request.base_url).rstrip('/')
257
+ return {
258
+ "claude": f"{base_url}/v1/messages",
259
+ "chatgpt": f"{base_url}/v1/chat/completions",
260
+ "gemini": f"{base_url}/v1beta/chat/completions"
261
+ }
262
+
263
+ @app.get("/dashboard")
264
+ async def dashboard():
265
+ with open("dashboard.html", encoding='utf-8') as f:
266
+ return HTMLResponse(f.read())
267
+
268
+ def check_dashboard_auth(request: Request):
269
+ password = os.getenv("DASHBOARD_PASSWORD")
270
+ if not password:
271
+ return True
272
+ auth = request.headers.get("X-Dashboard-Password")
273
+ return auth == password
274
+
275
+ @app.get("/dashboard/accounts")
276
+ async def get_accounts(request: Request):
277
+ if not check_dashboard_auth(request):
278
+ return JSONResponse({"error": "Unauthorized"}, 401)
279
+ return account_manager.to_dict()
280
+
281
+ @app.post("/dashboard/accounts")
282
+ async def add_account(request: Request):
283
+ if not check_dashboard_auth(request):
284
+ return JSONResponse({"error": "Unauthorized"}, 401)
285
+ data = await request.json()
286
+ acc = Account(
287
+ id=str(uuid.uuid4()),
288
+ provider=data["provider"],
289
+ credential_type=data["credential_type"],
290
+ credential_value=data["credential_value"],
291
+ phone=data.get("phone", ""),
292
+ email=data.get("email", ""),
293
+ note=data.get("note", "")
294
+ )
295
+ account_manager.add_account(acc)
296
+ dataset_backup.backup_accounts(account_manager.to_dict())
297
+ return {"status": "ok"}
298
+
299
+ @app.put("/dashboard/accounts/{account_id}")
300
+ async def update_account(account_id: str, request: Request):
301
+ if not check_dashboard_auth(request):
302
+ return JSONResponse({"error": "Unauthorized"}, 401)
303
+ data = await request.json()
304
+ if account_id in account_manager.accounts:
305
+ acc = account_manager.accounts[account_id]
306
+ acc.provider = data.get("provider", acc.provider)
307
+ acc.credential_type = data.get("credential_type", acc.credential_type)
308
+ acc.credential_value = data.get("credential_value", acc.credential_value)
309
+ acc.phone = data.get("phone", acc.phone)
310
+ acc.email = data.get("email", acc.email)
311
+ acc.note = data.get("note", acc.note)
312
+ dataset_backup.backup_accounts(account_manager.to_dict())
313
+ return {"status": "ok"}
314
+
315
+ @app.delete("/dashboard/accounts/{account_id}")
316
+ async def delete_account(account_id: str, request: Request):
317
+ if not check_dashboard_auth(request):
318
+ return JSONResponse({"error": "Unauthorized"}, 401)
319
+ account_manager.remove_account(account_id)
320
+ dataset_backup.backup_accounts(account_manager.to_dict())
321
+ return {"status": "ok"}
322
+
323
+ @app.put("/dashboard/accounts/{account_id}/status")
324
+ async def update_account_status(account_id: str, request: Request):
325
+ if not check_dashboard_auth(request):
326
+ return JSONResponse({"error": "Unauthorized"}, 401)
327
+ data = await request.json()
328
+ account_manager.update_account_status(account_id, AccountStatus(data["status"]))
329
+ return {"status": "ok"}
330
+
331
+ @app.post("/dashboard/backup")
332
+ async def manual_backup(request: Request):
333
+ if not check_dashboard_auth(request):
334
+ return JSONResponse({"error": "Unauthorized"}, 401)
335
+ dataset_backup.backup_accounts(account_manager.to_dict())
336
+ return {"message": "备份成功"}
337
+
338
+ @app.post("/dashboard/restore")
339
+ async def manual_restore(request: Request):
340
+ if not check_dashboard_auth(request):
341
+ return JSONResponse({"error": "Unauthorized"}, 401)
342
+ data = dataset_backup.restore_accounts()
343
+ if data:
344
+ account_manager.from_dict(data)
345
+ return {"message": f"恢复成功,共 {len(data)} 个账号"}
346
+ return {"message": "无备份数据"}
347
+
348
+ async def get_org_id(key: str) -> Optional[str]:
349
+ headers = fingerprint.get_headers()
350
+ headers['Cookie'] = f'sessionKey={key}'
351
+ async with httpx.AsyncClient() as client:
352
+ r = await client.get('https://claude.ai/api/organizations', headers=headers, timeout=30)
353
+ if r.status_code == 200:
354
+ data = r.json()
355
+ return data[0]['uuid'] if data else None
356
+ raise Exception(f"Status {r.status_code}")
357
+
358
+ async def create_conv(key: str, org_id: str) -> Optional[str]:
359
+ conv_id = str(uuid.uuid4())
360
+ headers = fingerprint.get_headers()
361
+ headers['Cookie'] = f'sessionKey={key}'
362
+ headers['Content-Type'] = 'application/json'
363
+ async with httpx.AsyncClient() as client:
364
+ r = await client.post(f'https://claude.ai/api/organizations/{org_id}/chat_conversations',
365
+ headers=headers, json={
366
+ 'uuid': conv_id,
367
+ 'name': '',
368
+ 'include_conversation_preferences': True,
369
+ 'is_temporary': False,
370
+ 'enabled_imagine': False
371
+ }, timeout=30)
372
+ return conv_id if r.status_code in [200, 201] else None
373
+
374
+ @app.post("/v1/chat/completions")
375
+ async def chatgpt_completions(request: Request):
376
+ return await handle_chat_request(request, "chatgpt")
377
+
378
+ @app.post("/v1/messages")
379
+ async def claude_messages(request: Request):
380
+ return await handle_chat_request(request, "claude")
381
+
382
+ @app.post("/v1beta/chat/completions")
383
+ async def gemini_completions(request: Request):
384
+ return await handle_chat_request(request, "gemini")
385
+
386
+ async def handle_chat_request(request: Request, provider: str):
387
+ global last_request_time
388
+ last_request_time = datetime.now()
389
+ try:
390
+ body = await request.json()
391
+ account = account_manager.get_next_active_account(provider)
392
+ if not account:
393
+ return JSONResponse({"error": f"No active {provider} accounts"}, 503)
394
+
395
+ key = account.credential_value
396
+ messages = body.get('messages', [])
397
+ model = body.get('model', 'claude-sonnet-4-6')
398
+ stream = body.get('stream', True)
399
+
400
+ org_id = await get_org_id(key)
401
+ if not org_id:
402
+ account_manager.record_call(account.id, False, "Invalid key")
403
+ return JSONResponse({"error": "Auth failed"}, 401)
404
+
405
+ conv_id = await create_conv(key, org_id)
406
+ if not conv_id:
407
+ account_manager.record_call(account.id, False, "Conv failed")
408
+ return JSONResponse({"error": "Conv failed"}, 500)
409
+
410
+ prompt = '\n\n'.join([f"{m['role']}: {m['content']}" for m in messages])
411
+ context = conv_cache.get_context(conv_id, prompt)
412
+ if context:
413
+ prompt = '\n\n'.join([f"{c['role']}: {c['content']}" for c in context]) + '\n\n' + prompt
414
+
415
+ conv_cache.add_message(conv_id, "user", messages[-1]['content'])
416
+
417
+ headers = fingerprint.get_headers()
418
+ headers['Cookie'] = f'sessionKey={key}'
419
+ headers['Content-Type'] = 'application/json'
420
+ payload = {
421
+ 'prompt': prompt,
422
+ 'timezone': 'Asia/Shanghai',
423
+ 'locale': 'en-US',
424
+ 'model': model,
425
+ 'rendering_mode': 'messages',
426
+ 'attachments': [],
427
+ 'files': []
428
+ }
429
+
430
+ if not stream:
431
+ full_text = ''
432
+ async with httpx.AsyncClient() as client:
433
+ async with client.stream('POST',
434
+ f'https://claude.ai/api/organizations/{org_id}/chat_conversations/{conv_id}/completion',
435
+ headers=headers, json=payload, timeout=120) as resp:
436
+ async for line in resp.aiter_lines():
437
+ if line.startswith('data: '):
438
+ try:
439
+ data = json.loads(line[6:])
440
+ if data.get('type') == 'content_block_delta':
441
+ full_text += data.get('delta', {}).get('text', '')
442
+ except:
443
+ pass
444
+ conv_cache.add_message(conv_id, "assistant", full_text)
445
+ account_manager.record_call(account.id, True)
446
+ return JSONResponse({
447
+ "id": f"chatcmpl-{int(datetime.now().timestamp())}",
448
+ "object": "chat.completion",
449
+ "model": model,
450
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": full_text}, "finish_reason": "stop"}]
451
+ })
452
+
453
+ async def generate():
454
+ full_text = ''
455
+ try:
456
+ async with httpx.AsyncClient() as client:
457
+ async with client.stream('POST',
458
+ f'https://claude.ai/api/organizations/{org_id}/chat_conversations/{conv_id}/completion',
459
+ headers=headers, json=payload, timeout=120) as resp:
460
+ async for line in resp.aiter_lines():
461
+ if line.startswith('data: '):
462
+ try:
463
+ data = json.loads(line[6:])
464
+ if data.get('type') == 'content_block_delta':
465
+ text = data.get('delta', {}).get('text', '')
466
+ full_text += text
467
+ chunk = {
468
+ "id": f"chatcmpl-{int(datetime.now().timestamp())}",
469
+ "object": "chat.completion.chunk",
470
+ "model": model,
471
+ "choices": [{"index": 0, "delta": {"content": text}, "finish_reason": None}]
472
+ }
473
+ yield f"data: {json.dumps(chunk)}\n\n"
474
+ except:
475
+ pass
476
+ yield "data: [DONE]\n\n"
477
+ conv_cache.add_message(conv_id, "assistant", full_text)
478
+ account_manager.record_call(account.id, True)
479
+ except Exception as e:
480
+ account_manager.record_call(account.id, False, str(e))
481
+ yield f"data: {json.dumps({'error': str(e)})}\n\n"
482
+
483
+ return StreamingResponse(generate(), media_type="text/event-stream")
484
+ except Exception as e:
485
+ logger.error(f"Error: {e}")
486
+ return JSONResponse({"error": str(e)}, 500)
487
+
488
+ if __name__ == "__main__":
489
+ import uvicorn
490
+ uvicorn.run(app, host="0.0.0.0", port=7860)