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

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -490
app.py DELETED
@@ -1,490 +0,0 @@
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)