heiyuheiyu commited on
Commit
2b0c433
·
verified ·
1 Parent(s): 0b0a0f7

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +731 -0
  2. dashboard.html +336 -0
app.py ADDED
@@ -0,0 +1,731 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import chromadb
22
+ from sentence_transformers import SentenceTransformer
23
+ import shutil
24
+
25
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # ============ AccountManager ============
29
+ class AccountStatus(str, Enum):
30
+ ACTIVE = "Active"
31
+ LIMITED = "Limited"
32
+ ERROR = "Error"
33
+ DISABLE = "Disable"
34
+
35
+ class Account:
36
+ def __init__(self, id: str, provider: str, credential_type: str, credential_value: str,
37
+ phone: str = "", email: str = "", note: str = ""):
38
+ self.id = id
39
+ self.provider = provider
40
+ self.account_type = "网页版"
41
+ self.credential_type = credential_type
42
+ self.credential_value = credential_value
43
+ self.phone = phone
44
+ self.email = email
45
+ self.note = note
46
+ self.status = AccountStatus.ACTIVE
47
+ self.daily_calls = 0
48
+ self.total_calls = 0
49
+ self.last_call_time = None
50
+ self.created_at = datetime.now()
51
+ self.error_403_count = 0
52
+
53
+ def to_dict(self):
54
+ return {
55
+ "id": self.id,
56
+ "provider": self.provider,
57
+ "account_type": self.account_type,
58
+ "credential_type": self.credential_type,
59
+ "credential_value_masked": self.credential_value[-10:] if len(self.credential_value) > 10 else "***",
60
+ "credential_value": self.credential_value,
61
+ "phone": self.phone,
62
+ "email": self.email,
63
+ "note": self.note,
64
+ "status": self.status,
65
+ "daily_calls": self.daily_calls,
66
+ "total_calls": self.total_calls,
67
+ "error_count": self.error_403_count,
68
+ "last_call_time": self.last_call_time.isoformat() if self.last_call_time else None,
69
+ "created_at": self.created_at.isoformat(),
70
+ "created_days": (datetime.now() - self.created_at).days
71
+ }
72
+
73
+ class AccountManager:
74
+ def __init__(self):
75
+ self.accounts: Dict[str, Account] = {}
76
+ self.current_index = 0
77
+
78
+ def add_account(self, account: Account):
79
+ self.accounts[account.id] = account
80
+
81
+ def remove_account(self, account_id: str):
82
+ if account_id in self.accounts:
83
+ del self.accounts[account_id]
84
+
85
+ def get_next_active_account(self, provider: str = None) -> Optional[Account]:
86
+ active = [a for a in self.accounts.values() if a.status == AccountStatus.ACTIVE]
87
+ if provider:
88
+ active = [a for a in active if a.provider == provider]
89
+ if not active:
90
+ return None
91
+ # 加权LRU: 优先选择调用次数少的,次数相同时选最久未使用的
92
+ return min(active, key=lambda a: (a.daily_calls, a.last_call_time or datetime.min))
93
+
94
+ def update_account_status(self, account_id: str, status: AccountStatus):
95
+ if account_id in self.accounts:
96
+ self.accounts[account_id].status = status
97
+ self.accounts[account_id].error_403_count = 0
98
+
99
+ def record_call(self, account_id: str, success: bool, error_msg: str = ""):
100
+ if account_id not in self.accounts:
101
+ return
102
+ acc = self.accounts[account_id]
103
+ acc.total_calls += 1
104
+ acc.daily_calls += 1
105
+ acc.last_call_time = datetime.now()
106
+ if not success:
107
+ if "403" in error_msg:
108
+ acc.error_403_count += 1
109
+ if acc.error_403_count >= 3:
110
+ acc.status = AccountStatus.ERROR
111
+ elif "limit" in error_msg.lower():
112
+ acc.status = AccountStatus.LIMITED
113
+ else:
114
+ acc.error_403_count = 0
115
+ acc.status = AccountStatus.ERROR
116
+
117
+ def reset_daily_calls(self):
118
+ for acc in self.accounts.values():
119
+ acc.daily_calls = 0
120
+
121
+ def to_dict(self):
122
+ return {aid: acc.to_dict() for aid, acc in self.accounts.items()}
123
+
124
+ def from_dict(self, data: dict):
125
+ for aid, acc_data in data.items():
126
+ acc = Account(
127
+ id=acc_data["id"],
128
+ provider=acc_data["provider"],
129
+ credential_type=acc_data["credential_type"],
130
+ credential_value=acc_data["credential_value"],
131
+ phone=acc_data.get("phone", ""),
132
+ email=acc_data.get("email", ""),
133
+ note=acc_data.get("note", "")
134
+ )
135
+ acc.status = AccountStatus(acc_data["status"])
136
+ acc.daily_calls = acc_data.get("daily_calls", 0)
137
+ acc.total_calls = acc_data.get("total_calls", 0)
138
+ if acc_data.get("last_call_time"):
139
+ acc.last_call_time = datetime.fromisoformat(acc_data["last_call_time"])
140
+ acc.created_at = datetime.fromisoformat(acc_data["created_at"])
141
+ self.accounts[aid] = acc
142
+
143
+ # ============ DatasetBackup ============
144
+ class DatasetBackup:
145
+ def __init__(self):
146
+ self.dataset_name = os.getenv("HF_BACKUP_REPO", "multi-ai-proxy-backup")
147
+ self.token = os.getenv("HF_TOKEN")
148
+ self.api = HfApi(token=self.token) if self.token else None
149
+ self.local_dir = Path("/tmp/backup")
150
+ self.local_dir.mkdir(exist_ok=True)
151
+
152
+ def backup_accounts(self, accounts_data: dict):
153
+ file_path = self.local_dir / "accounts.json"
154
+ with open(file_path, 'w') as f:
155
+ json.dump(accounts_data, f, indent=2)
156
+ if self.api:
157
+ try:
158
+ upload_file(path_or_fileobj=str(file_path), path_in_repo="accounts.json",
159
+ repo_id=self.dataset_name, repo_type="dataset", token=self.token)
160
+ logger.info("Accounts backed up")
161
+ except Exception as e:
162
+ logger.error(f"Backup accounts failed: {e}")
163
+
164
+ def restore_accounts(self) -> dict:
165
+ try:
166
+ if self.api:
167
+ file_path = hf_hub_download(repo_id=self.dataset_name, filename="accounts.json",
168
+ repo_type="dataset", token=self.token)
169
+ with open(file_path, 'r') as f:
170
+ return json.load(f)
171
+ except Exception as e:
172
+ logger.warning(f"Restore accounts failed: {e}")
173
+ return {}
174
+
175
+ def backup_conversations(self, conv_data: dict):
176
+ file_path = self.local_dir / "conversations.json"
177
+ with open(file_path, 'w') as f:
178
+ json.dump(conv_data, f, indent=2)
179
+ if self.api:
180
+ try:
181
+ upload_file(path_or_fileobj=str(file_path), path_in_repo="conversations.json",
182
+ repo_id=self.dataset_name, repo_type="dataset", token=self.token)
183
+ logger.info("Conversations backed up")
184
+ except Exception as e:
185
+ logger.error(f"Backup conversations failed: {e}")
186
+
187
+ def restore_conversations(self) -> dict:
188
+ try:
189
+ if self.api:
190
+ file_path = hf_hub_download(repo_id=self.dataset_name, filename="conversations.json",
191
+ repo_type="dataset", token=self.token)
192
+ with open(file_path, 'r') as f:
193
+ return json.load(f)
194
+ except Exception as e:
195
+ logger.warning(f"Restore conversations failed: {e}")
196
+ return {}
197
+
198
+ def backup_vectors(self, chroma_dir: Path):
199
+ if not chroma_dir.exists():
200
+ return
201
+ zip_path = self.local_dir / "chroma_db"
202
+ try:
203
+ shutil.make_archive(str(zip_path), 'zip', chroma_dir)
204
+ if self.api:
205
+ upload_file(path_or_fileobj=f"{zip_path}.zip", path_in_repo="chroma_db.zip",
206
+ repo_id=self.dataset_name, repo_type="dataset", token=self.token)
207
+ logger.info("Vectors backed up")
208
+ except Exception as e:
209
+ logger.error(f"Backup vectors failed: {e}")
210
+
211
+ def restore_vectors(self, chroma_dir: Path):
212
+ try:
213
+ if self.api:
214
+ zip_path = hf_hub_download(repo_id=self.dataset_name, filename="chroma_db.zip",
215
+ repo_type="dataset", token=self.token)
216
+ shutil.unpack_archive(zip_path, chroma_dir)
217
+ logger.info("Vectors restored")
218
+ except Exception as e:
219
+ logger.warning(f"Restore vectors failed: {e}")
220
+
221
+ # ============ ConversationCache ============
222
+ class ConversationCache:
223
+ def __init__(self):
224
+ self.short_term: Dict[str, List] = {}
225
+ self.long_term: Dict[str, Dict] = {}
226
+ self.chroma_dir = Path("/tmp/chroma_db")
227
+ self.chroma_dir.mkdir(exist_ok=True)
228
+ self.chroma_client = chromadb.PersistentClient(path=str(self.chroma_dir))
229
+ self.collection = self.chroma_client.get_or_create_collection("conversations")
230
+ self.embedder = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
231
+ logger.info("ConversationCache initialized with vector search")
232
+
233
+ def add_message(self, conv_id: str, role: str, content: str):
234
+ if conv_id not in self.short_term:
235
+ self.short_term[conv_id] = []
236
+ self.short_term[conv_id].append({"role": role, "content": content})
237
+ if len(self.short_term[conv_id]) > 10:
238
+ self.short_term[conv_id] = self.short_term[conv_id][-10:]
239
+
240
+ if role == "user" and len(content) > 10:
241
+ try:
242
+ embedding = self.embedder.encode(content).tolist()
243
+ self.collection.add(
244
+ embeddings=[embedding],
245
+ documents=[content],
246
+ metadatas=[{"conv_id": conv_id, "role": role, "timestamp": datetime.now().isoformat()}],
247
+ ids=[f"{conv_id}_{uuid.uuid4().hex[:8]}"]
248
+ )
249
+ except Exception as e:
250
+ logger.warning(f"Vector add failed: {e}")
251
+
252
+ def get_context(self, conv_id: str, query: str) -> List[Dict]:
253
+ context = []
254
+ short = self.short_term.get(conv_id, [])[-5:]
255
+ context.extend(short)
256
+
257
+ if len(query) > 10:
258
+ try:
259
+ query_embedding = self.embedder.encode(query).tolist()
260
+ results = self.collection.query(query_embeddings=[query_embedding], n_results=3)
261
+ if results['documents'] and results['documents'][0]:
262
+ for doc in results['documents'][0]:
263
+ context.append({"role": "user", "content": doc})
264
+ except Exception as e:
265
+ logger.warning(f"Vector search failed: {e}")
266
+
267
+ if conv_id in self.long_term:
268
+ context.insert(0, {"role": "system", "content": self.long_term[conv_id].get("summary", "")})
269
+
270
+ return context
271
+
272
+ def _extract_long_term(self, conv_id: str):
273
+ if conv_id not in self.short_term or len(self.short_term[conv_id]) < 5:
274
+ return
275
+ messages = self.short_term[conv_id]
276
+ summary = f"Previous context: {len(messages)} messages"
277
+ self.long_term[conv_id] = {"summary": summary, "updated": datetime.now().isoformat()}
278
+
279
+ def to_dict(self):
280
+ return {
281
+ "short_term": self.short_term,
282
+ "long_term": self.long_term
283
+ }
284
+
285
+ def from_dict(self, data: dict):
286
+ self.short_term = data.get("short_term", {})
287
+ self.long_term = data.get("long_term", {})
288
+
289
+ # ============ FingerprintSimulator ============
290
+ class FingerprintSimulator:
291
+ def __init__(self):
292
+ self.ua = UserAgent()
293
+
294
+ def get_headers(self):
295
+ return {
296
+ 'User-Agent': self.ua.random,
297
+ 'Accept': 'application/json',
298
+ 'Accept-Language': random.choice(['en-US,en;q=0.9', 'zh-CN,zh;q=0.9']),
299
+ 'Referer': 'https://claude.ai/chats',
300
+ 'Origin': 'https://claude.ai',
301
+ 'Sec-Fetch-Dest': 'empty',
302
+ 'Sec-Fetch-Mode': 'cors',
303
+ 'Sec-Fetch-Site': 'same-origin'
304
+ }
305
+
306
+ # ============ Global Instances ============
307
+ account_manager = AccountManager()
308
+ dataset_backup = DatasetBackup()
309
+ conv_cache = ConversationCache()
310
+ fingerprint = FingerprintSimulator()
311
+ last_request_time = datetime.now()
312
+
313
+ async def check_limited_accounts():
314
+ while True:
315
+ await asyncio.sleep(7200)
316
+ for acc in account_manager.accounts.values():
317
+ if acc.status == AccountStatus.LIMITED:
318
+ try:
319
+ headers = fingerprint.get_headers()
320
+ headers['Cookie'] = f'sessionKey={acc.credential_value}'
321
+ async with httpx.AsyncClient() as client:
322
+ r = await client.get('https://claude.ai/api/organizations', headers=headers, timeout=10)
323
+ if r.status_code == 200:
324
+ acc.status = AccountStatus.ACTIVE
325
+ except:
326
+ pass
327
+
328
+ async def daily_reset():
329
+ while True:
330
+ await asyncio.sleep(86400)
331
+ account_manager.reset_daily_calls()
332
+
333
+ async def auto_backup():
334
+ while True:
335
+ await asyncio.sleep(3600)
336
+ backup_mode = os.getenv("BACKUP_MODE", "all")
337
+ if backup_mode == "none":
338
+ continue
339
+ if backup_mode in ["all", "accounts"]:
340
+ dataset_backup.backup_accounts(account_manager.to_dict())
341
+ if backup_mode in ["all", "cache"]:
342
+ dataset_backup.backup_conversations(conv_cache.to_dict())
343
+ if backup_mode in ["all", "vector"]:
344
+ dataset_backup.backup_vectors(conv_cache.chroma_dir)
345
+
346
+ @asynccontextmanager
347
+ async def lifespan(app: FastAPI):
348
+ restore_mode = os.getenv("RESTORE_MODE", "all")
349
+
350
+ if restore_mode in ["all", "vector"]:
351
+ dataset_backup.restore_vectors(conv_cache.chroma_dir)
352
+
353
+ if restore_mode in ["all", "accounts"]:
354
+ data = dataset_backup.restore_accounts()
355
+ if data:
356
+ account_manager.from_dict(data)
357
+ logger.info(f"Restored {len(data)} accounts")
358
+
359
+ if restore_mode in ["all", "cache"]:
360
+ data = dataset_backup.restore_conversations()
361
+ if data:
362
+ conv_cache.from_dict(data)
363
+ logger.info("Restored conversations")
364
+
365
+ asyncio.create_task(check_limited_accounts())
366
+ asyncio.create_task(daily_reset())
367
+ asyncio.create_task(auto_backup())
368
+ logger.info("Application started")
369
+ yield
370
+ dataset_backup.backup_accounts(account_manager.to_dict())
371
+ dataset_backup.backup_conversations(conv_cache.to_dict())
372
+ dataset_backup.backup_vectors(conv_cache.chroma_dir)
373
+
374
+ app = FastAPI(title="Claude Proxy Enhanced", lifespan=lifespan)
375
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
376
+
377
+ @app.get("/")
378
+ async def root():
379
+ return {"name": "Claude Proxy Enhanced", "version": "3.0"}
380
+
381
+ @app.get("/health")
382
+ async def health():
383
+ return {"status": "ok", "accounts": len(account_manager.accounts)}
384
+
385
+ @app.get("/api/info")
386
+ async def api_info(request: Request):
387
+ base_url = str(request.base_url).rstrip('/')
388
+ return {
389
+ "claude": {
390
+ "baseUrl": base_url,
391
+ "api": "anthropic-messages",
392
+ "endpoint": f"{base_url}/v1/messages"
393
+ },
394
+ "chatgpt": {
395
+ "baseUrl": base_url,
396
+ "api": "openai-completions",
397
+ "endpoint": f"{base_url}/v1/chat/completions"
398
+ },
399
+ "gemini": {
400
+ "baseUrl": base_url,
401
+ "api": "openai-completions",
402
+ "endpoint": f"{base_url}/v1beta/chat/completions"
403
+ }
404
+ }
405
+
406
+ @app.get("/dashboard")
407
+ async def dashboard():
408
+ with open("dashboard.html", encoding='utf-8') as f:
409
+ return HTMLResponse(f.read())
410
+
411
+ def check_dashboard_auth(request: Request):
412
+ password = os.getenv("DASHBOARD_PASSWORD")
413
+ if not password:
414
+ return True
415
+ auth = request.headers.get("X-Dashboard-Password")
416
+ return auth == password
417
+
418
+ @app.get("/dashboard/accounts")
419
+ async def get_accounts(request: Request):
420
+ if not check_dashboard_auth(request):
421
+ return JSONResponse({"error": "Unauthorized"}, 401)
422
+ return account_manager.to_dict()
423
+
424
+ @app.post("/dashboard/accounts")
425
+ async def add_account(request: Request):
426
+ if not check_dashboard_auth(request):
427
+ return JSONResponse({"error": "Unauthorized"}, 401)
428
+ data = await request.json()
429
+ acc = Account(
430
+ id=str(uuid.uuid4()),
431
+ provider=data["provider"],
432
+ credential_type=data["credential_type"],
433
+ credential_value=data["credential_value"],
434
+ phone=data.get("phone", ""),
435
+ email=data.get("email", ""),
436
+ note=data.get("note", "")
437
+ )
438
+ account_manager.add_account(acc)
439
+ dataset_backup.backup_accounts(account_manager.to_dict())
440
+ return {"status": "ok"}
441
+
442
+ @app.put("/dashboard/accounts/{account_id}")
443
+ async def update_account(account_id: str, request: Request):
444
+ if not check_dashboard_auth(request):
445
+ return JSONResponse({"error": "Unauthorized"}, 401)
446
+ data = await request.json()
447
+ if account_id in account_manager.accounts:
448
+ acc = account_manager.accounts[account_id]
449
+ acc.provider = data.get("provider", acc.provider)
450
+ acc.credential_type = data.get("credential_type", acc.credential_type)
451
+ acc.credential_value = data.get("credential_value", acc.credential_value)
452
+ acc.phone = data.get("phone", acc.phone)
453
+ acc.email = data.get("email", acc.email)
454
+ acc.note = data.get("note", acc.note)
455
+ dataset_backup.backup_accounts(account_manager.to_dict())
456
+ return {"status": "ok"}
457
+
458
+ @app.delete("/dashboard/accounts/{account_id}")
459
+ async def delete_account(account_id: str, request: Request):
460
+ if not check_dashboard_auth(request):
461
+ return JSONResponse({"error": "Unauthorized"}, 401)
462
+ account_manager.remove_account(account_id)
463
+ dataset_backup.backup_accounts(account_manager.to_dict())
464
+ return {"status": "ok"}
465
+
466
+ @app.put("/dashboard/accounts/{account_id}/status")
467
+ async def update_account_status(account_id: str, request: Request):
468
+ if not check_dashboard_auth(request):
469
+ return JSONResponse({"error": "Unauthorized"}, 401)
470
+ data = await request.json()
471
+ account_manager.update_account_status(account_id, AccountStatus(data["status"]))
472
+ return {"status": "ok"}
473
+
474
+ @app.post("/dashboard/backup")
475
+ async def manual_backup(request: Request):
476
+ if not check_dashboard_auth(request):
477
+ return JSONResponse({"error": "Unauthorized"}, 401)
478
+ dataset_backup.backup_accounts(account_manager.to_dict())
479
+ dataset_backup.backup_conversations(conv_cache.to_dict())
480
+ dataset_backup.backup_vectors(conv_cache.chroma_dir)
481
+ return {"message": "备份成功(账号+聊天+向量库)"}
482
+
483
+ @app.post("/dashboard/restore")
484
+ async def manual_restore(request: Request):
485
+ if not check_dashboard_auth(request):
486
+ return JSONResponse({"error": "Unauthorized"}, 401)
487
+ acc_data = dataset_backup.restore_accounts()
488
+ conv_data = dataset_backup.restore_conversations()
489
+ dataset_backup.restore_vectors(conv_cache.chroma_dir)
490
+ if acc_data:
491
+ account_manager.from_dict(acc_data)
492
+ if conv_data:
493
+ conv_cache.from_dict(conv_data)
494
+ return {"message": f"恢复成功:{len(acc_data)} 个账号 + 聊天记录 + 向量库"}
495
+
496
+ @app.post("/dashboard/backup/accounts")
497
+ async def backup_accounts_only(request: Request):
498
+ if not check_dashboard_auth(request):
499
+ return JSONResponse({"error": "Unauthorized"}, 401)
500
+ dataset_backup.backup_accounts(account_manager.to_dict())
501
+ return {"message": "账号备份成功"}
502
+
503
+ @app.post("/dashboard/backup/cache")
504
+ async def backup_cache_only(request: Request):
505
+ if not check_dashboard_auth(request):
506
+ return JSONResponse({"error": "Unauthorized"}, 401)
507
+ dataset_backup.backup_conversations(conv_cache.to_dict())
508
+ return {"message": "聊天记录备份成功"}
509
+
510
+ @app.post("/dashboard/backup/vector")
511
+ async def backup_vector_only(request: Request):
512
+ if not check_dashboard_auth(request):
513
+ return JSONResponse({"error": "Unauthorized"}, 401)
514
+ dataset_backup.backup_vectors(conv_cache.chroma_dir)
515
+ return {"message": "向量库备份成功"}
516
+
517
+ @app.post("/dashboard/restore/accounts")
518
+ async def restore_accounts_only(request: Request):
519
+ if not check_dashboard_auth(request):
520
+ return JSONResponse({"error": "Unauthorized"}, 401)
521
+ data = dataset_backup.restore_accounts()
522
+ if data:
523
+ account_manager.from_dict(data)
524
+ return {"message": f"账号恢复成功:{len(data)} 个"}
525
+ return {"message": "无账号备份数据"}
526
+
527
+ @app.post("/dashboard/restore/cache")
528
+ async def restore_cache_only(request: Request):
529
+ if not check_dashboard_auth(request):
530
+ return JSONResponse({"error": "Unauthorized"}, 401)
531
+ data = dataset_backup.restore_conversations()
532
+ if data:
533
+ conv_cache.from_dict(data)
534
+ return {"message": "聊天记录恢复成功"}
535
+ return {"message": "无聊天记录备份"}
536
+
537
+ @app.post("/dashboard/restore/vector")
538
+ async def restore_vector_only(request: Request):
539
+ if not check_dashboard_auth(request):
540
+ return JSONResponse({"error": "Unauthorized"}, 401)
541
+ dataset_backup.restore_vectors(conv_cache.chroma_dir)
542
+ return {"message": "向量库恢复成功"}
543
+
544
+ async def get_org_id(key: str) -> Optional[str]:
545
+ headers = fingerprint.get_headers()
546
+ headers['Cookie'] = f'sessionKey={key}'
547
+ async with httpx.AsyncClient() as client:
548
+ r = await client.get('https://claude.ai/api/organizations', headers=headers, timeout=30)
549
+ if r.status_code == 200:
550
+ data = r.json()
551
+ return data[0]['uuid'] if data else None
552
+ raise Exception(f"Status {r.status_code}")
553
+
554
+ async def create_conv(key: str, org_id: str) -> Optional[str]:
555
+ conv_id = str(uuid.uuid4())
556
+ headers = fingerprint.get_headers()
557
+ headers['Cookie'] = f'sessionKey={key}'
558
+ headers['Content-Type'] = 'application/json'
559
+ async with httpx.AsyncClient() as client:
560
+ r = await client.post(f'https://claude.ai/api/organizations/{org_id}/chat_conversations',
561
+ headers=headers, json={
562
+ 'uuid': conv_id,
563
+ 'name': '',
564
+ 'include_conversation_preferences': True,
565
+ 'is_temporary': False,
566
+ 'enabled_imagine': False
567
+ }, timeout=30)
568
+ return conv_id if r.status_code in [200, 201] else None
569
+
570
+ @app.post("/v1/chat/completions")
571
+ async def chatgpt_completions(request: Request):
572
+ return await handle_chat_request(request, "chatgpt", "openai")
573
+
574
+ @app.post("/v1/messages")
575
+ async def claude_messages(request: Request):
576
+ return await handle_chat_request(request, "claude", "anthropic")
577
+
578
+ @app.post("/v1beta/chat/completions")
579
+ async def gemini_completions(request: Request):
580
+ return await handle_chat_request(request, "gemini", "openai")
581
+
582
+ async def handle_chat_request(request: Request, provider: str, api_format: str):
583
+ global last_request_time
584
+ last_request_time = datetime.now()
585
+ try:
586
+ body = await request.json()
587
+ account = account_manager.get_next_active_account(provider)
588
+ if not account:
589
+ return JSONResponse({"error": f"No active {provider} accounts"}, 503)
590
+
591
+ key = account.credential_value
592
+ messages = body.get('messages', [])
593
+ model = body.get('model', 'claude-sonnet-4-6')
594
+ stream = body.get('stream', True)
595
+
596
+ try:
597
+ org_id = await get_org_id(key)
598
+ if not org_id:
599
+ account_manager.record_call(account.id, False, "Invalid key")
600
+ return JSONResponse({"error": "Auth failed"}, 401)
601
+ except Exception as e:
602
+ error_msg = str(e)
603
+ logger.error(f"Account {account.id} get_org_id failed: {error_msg}")
604
+ account_manager.record_call(account.id, False, error_msg)
605
+ return JSONResponse({"error": error_msg}, 500)
606
+
607
+ try:
608
+ conv_id = await create_conv(key, org_id)
609
+ if not conv_id:
610
+ account_manager.record_call(account.id, False, "Conv failed")
611
+ return JSONResponse({"error": "Conv failed"}, 500)
612
+ except Exception as e:
613
+ error_msg = str(e)
614
+ logger.error(f"Account {account.id} create_conv failed: {error_msg}")
615
+ account_manager.record_call(account.id, False, error_msg)
616
+ return JSONResponse({"error": error_msg}, 500)
617
+
618
+ prompt = '\n\n'.join([f"{m['role']}: {m['content']}" for m in messages])
619
+ context = conv_cache.get_context(conv_id, prompt)
620
+ if context:
621
+ prompt = '\n\n'.join([f"{c['role']}: {c['content']}" for c in context]) + '\n\n' + prompt
622
+
623
+ conv_cache.add_message(conv_id, "user", messages[-1]['content'])
624
+
625
+ headers = fingerprint.get_headers()
626
+ headers['Cookie'] = f'sessionKey={key}'
627
+ headers['Content-Type'] = 'application/json'
628
+ payload = {
629
+ 'prompt': prompt,
630
+ 'timezone': 'Asia/Shanghai',
631
+ 'locale': 'en-US',
632
+ 'model': model,
633
+ 'rendering_mode': 'messages',
634
+ 'attachments': [],
635
+ 'files': [],
636
+ 'tools': [
637
+ {"type": "web_search_v0", "name": "web_search"},
638
+ {"type": "artifacts_v0", "name": "artifacts"},
639
+ {"type": "repl_v0", "name": "repl"}
640
+ ]
641
+ }
642
+
643
+ if not stream:
644
+ full_text = ''
645
+ async with httpx.AsyncClient() as client:
646
+ async with client.stream('POST',
647
+ f'https://claude.ai/api/organizations/{org_id}/chat_conversations/{conv_id}/completion',
648
+ headers=headers, json=payload, timeout=120) as resp:
649
+ async for line in resp.aiter_lines():
650
+ if line.startswith('data: '):
651
+ try:
652
+ data = json.loads(line[6:])
653
+ if data.get('type') == 'content_block_delta':
654
+ full_text += data.get('delta', {}).get('text', '')
655
+ except:
656
+ pass
657
+ conv_cache.add_message(conv_id, "assistant", full_text)
658
+ account_manager.record_call(account.id, True)
659
+
660
+ if api_format == "anthropic":
661
+ return JSONResponse({
662
+ "id": f"msg_{uuid.uuid4().hex[:24]}",
663
+ "type": "message",
664
+ "role": "assistant",
665
+ "content": [{"type": "text", "text": full_text}],
666
+ "model": model,
667
+ "stop_reason": "end_turn",
668
+ "stop_sequence": None,
669
+ "usage": {"input_tokens": 0, "output_tokens": 0}
670
+ })
671
+ else:
672
+ return JSONResponse({
673
+ "id": f"chatcmpl-{int(datetime.now().timestamp())}",
674
+ "object": "chat.completion",
675
+ "model": model,
676
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": full_text}, "finish_reason": "stop"}]
677
+ })
678
+
679
+ async def generate():
680
+ full_text = ''
681
+ msg_id = f"msg_{uuid.uuid4().hex[:24]}"
682
+ try:
683
+ if api_format == "anthropic":
684
+ yield f"event: message_start\ndata: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'usage': {'input_tokens': 0, 'output_tokens': 0}}})}\n\n"
685
+ yield f"event: content_block_start\ndata: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
686
+
687
+ async with httpx.AsyncClient() as client:
688
+ async with client.stream('POST',
689
+ f'https://claude.ai/api/organizations/{org_id}/chat_conversations/{conv_id}/completion',
690
+ headers=headers, json=payload, timeout=120) as resp:
691
+ async for line in resp.aiter_lines():
692
+ if line.startswith('data: '):
693
+ try:
694
+ data = json.loads(line[6:])
695
+ if data.get('type') == 'content_block_delta':
696
+ text = data.get('delta', {}).get('text', '')
697
+ full_text += text
698
+ if api_format == "anthropic":
699
+ yield f"event: content_block_delta\ndata: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': text}})}\n\n"
700
+ else:
701
+ chunk = {
702
+ "id": f"chatcmpl-{int(datetime.now().timestamp())}",
703
+ "object": "chat.completion.chunk",
704
+ "model": model,
705
+ "choices": [{"index": 0, "delta": {"content": text}, "finish_reason": None}]
706
+ }
707
+ yield f"data: {json.dumps(chunk)}\n\n"
708
+ except:
709
+ pass
710
+
711
+ if api_format == "anthropic":
712
+ yield f"event: content_block_stop\ndata: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
713
+ yield f"event: message_delta\ndata: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': 'end_turn', 'stop_sequence': None}, 'usage': {'output_tokens': 0}})}\n\n"
714
+ yield f"event: message_stop\ndata: {json.dumps({'type': 'message_stop'})}\n\n"
715
+ else:
716
+ yield "data: [DONE]\n\n"
717
+
718
+ conv_cache.add_message(conv_id, "assistant", full_text)
719
+ account_manager.record_call(account.id, True)
720
+ except Exception as e:
721
+ account_manager.record_call(account.id, False, str(e))
722
+ yield f"data: {json.dumps({'error': str(e)})}\n\n"
723
+
724
+ return StreamingResponse(generate(), media_type="text/event-stream")
725
+ except Exception as e:
726
+ logger.error(f"Error: {e}")
727
+ return JSONResponse({"error": str(e)}, 500)
728
+
729
+ if __name__ == "__main__":
730
+ import uvicorn
731
+ uvicorn.run(app, host="0.0.0.0", port=7860)
dashboard.html ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>AI Proxy Dashboard</title>
6
+ <style>
7
+ body{font-family:Arial;margin:20px;background:#f5f5f5}
8
+ .container{max-width:1400px;margin:0 auto;background:#fff;padding:20px;border-radius:8px}
9
+ .login-box{max-width:400px;margin:100px auto;background:#fff;padding:30px;border-radius:8px}
10
+ h1{color:#333}
11
+ .btn{padding:8px 16px;margin:5px;border:none;border-radius:4px;cursor:pointer}
12
+ .btn-primary{background:#007bff;color:#fff}
13
+ .btn-success{background:#28a745;color:#fff}
14
+ .btn-danger{background:#dc3545;color:#fff}
15
+ .btn-warning{background:#ffc107;color:#000}
16
+ table{width:100%;border-collapse:collapse;margin:20px 0;font-size:12px}
17
+ th,td{padding:8px;text-align:left;border-bottom:1px solid #ddd}
18
+ th{background:#f8f9fa}
19
+ .status-active{color:#28a745}
20
+ .status-limited{color:#ffc107}
21
+ .status-error{color:#dc3545}
22
+ .status-disable{color:#6c757d}
23
+ .form-group{margin:10px 0}
24
+ label{display:block;margin:5px 0}
25
+ input,select,textarea{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box}
26
+ .modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1000}
27
+ .modal-content{background:#fff;margin:50px auto;padding:20px;width:80%;max-width:600px;border-radius:8px;max-height:80vh;overflow-y:auto}
28
+ .hidden{display:none}
29
+ </style>
30
+ </head>
31
+ <body>
32
+ <div id="loginBox" class="login-box">
33
+ <h1>Dashboard 登录</h1>
34
+ <div class="form-group">
35
+ <label>密码</label>
36
+ <input id="password" type="password" placeholder="输入密码">
37
+ </div>
38
+ <button class="btn btn-primary" onclick="login()">登录</button>
39
+ </div>
40
+
41
+ <div id="mainContent" class="container hidden">
42
+ <h1>AI Proxy Dashboard</h1>
43
+ <div style="background:#f8f9fa;padding:15px;border-radius:4px;margin-bottom:20px">
44
+ <h3>API 地址</h3>
45
+ <div id="apiUrls"></div>
46
+ </div>
47
+ <button class="btn btn-primary" onclick="showAddModal()">添加账号</button>
48
+ <button class="btn btn-success" onclick="backup()">一键备份</button>
49
+ <button class="btn btn-success" onclick="restore()">一键恢复</button>
50
+ <br>
51
+ <button class="btn btn-success" onclick="backupAccounts()">备份账号</button>
52
+ <button class="btn btn-success" onclick="backupCache()">备份聊天</button>
53
+ <button class="btn btn-success" onclick="backupVector()">备份向量库</button>
54
+ <button class="btn btn-warning" onclick="restoreAccounts()">恢复账号</button>
55
+ <button class="btn btn-warning" onclick="restoreCache()">恢复聊天</button>
56
+ <button class="btn btn-warning" onclick="restoreVector()">恢复向量库</button>
57
+ <table id="accountTable">
58
+ <thead><tr>
59
+ <th>AI厂商</th><th>账号类型</th><th>凭证类型</th><th>凭证值</th><th>状态</th><th>今日调用</th><th>总调用</th><th>错误次数</th><th>最近调用</th><th>创建日期</th><th>创建天数</th><th>手机号</th><th>邮箱</th><th>操作</th>
60
+ </tr></thead>
61
+ <tbody id="accountList"></tbody>
62
+ </table>
63
+ </div>
64
+
65
+ <div id="addModal" class="modal">
66
+ <div class="modal-content">
67
+ <h2>添加账号</h2>
68
+ <div class="form-group">
69
+ <label>AI厂商</label>
70
+ <select id="addProvider">
71
+ <option value="claude">Claude</option>
72
+ <option value="chatgpt">ChatGPT</option>
73
+ <option value="gemini">Gemini</option>
74
+ </select>
75
+ </div>
76
+ <div class="form-group">
77
+ <label>凭证类型</label>
78
+ <select id="addCredType">
79
+ <option value="sessionKey">sessionKey</option>
80
+ <option value="apiKey">apiKey</option>
81
+ </select>
82
+ </div>
83
+ <div class="form-group">
84
+ <label>凭证值</label>
85
+ <input id="addCredValue" type="text">
86
+ </div>
87
+ <div class="form-group">
88
+ <label>手机号</label>
89
+ <input id="addPhone" type="text">
90
+ </div>
91
+ <div class="form-group">
92
+ <label>邮箱</label>
93
+ <input id="addEmail" type="text">
94
+ </div>
95
+ <div class="form-group">
96
+ <label>备注</label>
97
+ <textarea id="addNote"></textarea>
98
+ </div>
99
+ <button class="btn btn-primary" onclick="submitAdd()">提交</button>
100
+ <button class="btn" onclick="closeModal('addModal')">取消</button>
101
+ </div>
102
+ </div>
103
+
104
+ <div id="editModal" class="modal">
105
+ <div class="modal-content">
106
+ <h2>编辑账号</h2>
107
+ <input id="editId" type="hidden">
108
+ <div class="form-group">
109
+ <label>AI厂商</label>
110
+ <select id="editProvider">
111
+ <option value="claude">Claude</option>
112
+ <option value="chatgpt">ChatGPT</option>
113
+ <option value="gemini">Gemini</option>
114
+ </select>
115
+ </div>
116
+ <div class="form-group">
117
+ <label>凭证类型</label>
118
+ <select id="editCredType">
119
+ <option value="sessionKey">sessionKey</option>
120
+ <option value="apiKey">apiKey</option>
121
+ </select>
122
+ </div>
123
+ <div class="form-group">
124
+ <label>凭证值</label>
125
+ <input id="editCredValue" type="text">
126
+ </div>
127
+ <div class="form-group">
128
+ <label>手机号</label>
129
+ <input id="editPhone" type="text">
130
+ </div>
131
+ <div class="form-group">
132
+ <label>邮箱</label>
133
+ <input id="editEmail" type="text">
134
+ </div>
135
+ <div class="form-group">
136
+ <label>备注</label>
137
+ <textarea id="editNote"></textarea>
138
+ </div>
139
+ <div class="form-group">
140
+ <label>状态</label>
141
+ <select id="editStatus">
142
+ <option value="Active">Active</option>
143
+ <option value="Limited">Limited</option>
144
+ <option value="Error">Error</option>
145
+ <option value="Disable">Disable</option>
146
+ </select>
147
+ </div>
148
+ <button class="btn btn-primary" onclick="submitEdit()">保存</button>
149
+ <button class="btn" onclick="closeModal('editModal')">取消</button>
150
+ </div>
151
+ </div>
152
+
153
+ <script>
154
+ let password='';
155
+ window.onload=()=>{
156
+ const saved=localStorage.getItem('dashboardPassword');
157
+ if(saved){
158
+ password=saved;
159
+ fetch('/dashboard/accounts',{headers:{'X-Dashboard-Password':password}})
160
+ .then(r=>{
161
+ if(r.ok){
162
+ document.getElementById('loginBox').classList.add('hidden');
163
+ document.getElementById('mainContent').classList.remove('hidden');
164
+ loadApiUrls();
165
+ loadAccounts();
166
+ }else{
167
+ localStorage.removeItem('dashboardPassword');
168
+ }
169
+ });
170
+ }
171
+ };
172
+ async function login(){
173
+ password=document.getElementById('password').value;
174
+ const r=await fetch('/dashboard/accounts',{headers:{'X-Dashboard-Password':password}});
175
+ if(r.ok){
176
+ localStorage.setItem('dashboardPassword',password);
177
+ document.getElementById('loginBox').classList.add('hidden');
178
+ document.getElementById('mainContent').classList.remove('hidden');
179
+ loadApiUrls();
180
+ loadAccounts();
181
+ }else{
182
+ alert('密码错误');
183
+ }
184
+ }
185
+ async function loadApiUrls(){
186
+ const r=await fetch('/api/info');
187
+ const data=await r.json();
188
+ document.getElementById('apiUrls').innerHTML=`
189
+ <div><strong>Claude:</strong> baseUrl: ${data.claude.baseUrl} | api: ${data.claude.api}</div>
190
+ <div><strong>ChatGPT:</strong> baseUrl: ${data.chatgpt.baseUrl} | api: ${data.chatgpt.api}</div>
191
+ <div><strong>Gemini:</strong> baseUrl: ${data.gemini.baseUrl} | api: ${data.gemini.api}</div>
192
+ `;
193
+ }
194
+ async function loadAccounts(){
195
+ const r=await fetch('/dashboard/accounts',{headers:{'X-Dashboard-Password':password}});
196
+ const data=await r.json();
197
+ const tbody=document.getElementById('accountList');
198
+ tbody.innerHTML='';
199
+ for(const id in data){
200
+ const a=data[id];
201
+ const tr=document.createElement('tr');
202
+ tr.innerHTML=`
203
+ <td>${a.provider}</td>
204
+ <td>${a.account_type}</td>
205
+ <td>${a.credential_type}</td>
206
+ <td>${a.credential_value_masked}</td>
207
+ <td>
208
+ <select class="status-${a.status.toLowerCase()}" onchange="updateStatus('${id}',this.value)" style="padding:4px;border:1px solid #ddd;border-radius:4px">
209
+ <option value="Active" ${a.status==='Active'?'selected':''}>Active</option>
210
+ <option value="Limited" ${a.status==='Limited'?'selected':''}>Limited</option>
211
+ <option value="Error" ${a.status==='Error'?'selected':''}>Error</option>
212
+ <option value="Disable" ${a.status==='Disable'?'selected':''}>Disable</option>
213
+ </select>
214
+ </td>
215
+ <td>${a.daily_calls}</td>
216
+ <td>${a.total_calls}</td>
217
+ <td>${a.error_count}</td>
218
+ <td>${a.last_call_time?new Date(a.last_call_time).toLocaleString():'未调用'}</td>
219
+ <td>${new Date(a.created_at).toLocaleDateString()}</td>
220
+ <td>${a.created_days}</td>
221
+ <td>${a.phone}</td>
222
+ <td>${a.email}</td>
223
+ <td>
224
+ <button class="btn btn-warning" onclick="showEditModal('${id}')">编辑</button>
225
+ <button class="btn btn-danger" onclick="deleteAccount('${id}')">删除</button>
226
+ </td>
227
+ `;
228
+ tbody.appendChild(tr);
229
+ }
230
+ }
231
+ function showAddModal(){
232
+ document.getElementById('addModal').style.display='block';
233
+ }
234
+ function showEditModal(id){
235
+ fetch('/dashboard/accounts',{headers:{'X-Dashboard-Password':password}})
236
+ .then(r=>r.json())
237
+ .then(data=>{
238
+ const a=data[id];
239
+ document.getElementById('editId').value=id;
240
+ document.getElementById('editProvider').value=a.provider;
241
+ document.getElementById('editCredType').value=a.credential_type;
242
+ document.getElementById('editCredValue').value=a.credential_value;
243
+ document.getElementById('editPhone').value=a.phone;
244
+ document.getElementById('editEmail').value=a.email;
245
+ document.getElementById('editNote').value=a.note;
246
+ document.getElementById('editStatus').value=a.status;
247
+ document.getElementById('editModal').style.display='block';
248
+ });
249
+ }
250
+ function closeModal(id){
251
+ document.getElementById(id).style.display='none';
252
+ }
253
+ async function submitAdd(){
254
+ const data={
255
+ provider:document.getElementById('addProvider').value,
256
+ credential_type:document.getElementById('addCredType').value,
257
+ credential_value:document.getElementById('addCredValue').value,
258
+ phone:document.getElementById('addPhone').value,
259
+ email:document.getElementById('addEmail').value,
260
+ note:document.getElementById('addNote').value
261
+ };
262
+ await fetch('/dashboard/accounts',{method:'POST',headers:{'Content-Type':'application/json','X-Dashboard-Password':password},body:JSON.stringify(data)});
263
+ closeModal('addModal');
264
+ loadAccounts();
265
+ }
266
+ async function submitEdit(){
267
+ const id=document.getElementById('editId').value;
268
+ const data={
269
+ provider:document.getElementById('editProvider').value,
270
+ credential_type:document.getElementById('editCredType').value,
271
+ credential_value:document.getElementById('editCredValue').value,
272
+ phone:document.getElementById('editPhone').value,
273
+ email:document.getElementById('editEmail').value,
274
+ note:document.getElementById('editNote').value
275
+ };
276
+ await fetch(`/dashboard/accounts/${id}`,{method:'PUT',headers:{'Content-Type':'application/json','X-Dashboard-Password':password},body:JSON.stringify(data)});
277
+ const status=document.getElementById('editStatus').value;
278
+ await fetch(`/dashboard/accounts/${id}/status`,{method:'PUT',headers:{'Content-Type':'application/json','X-Dashboard-Password':password},body:JSON.stringify({status})});
279
+ closeModal('editModal');
280
+ loadAccounts();
281
+ }
282
+ async function updateStatus(id,status){
283
+ await fetch(`/dashboard/accounts/${id}/status`,{method:'PUT',headers:{'Content-Type':'application/json','X-Dashboard-Password':password},body:JSON.stringify({status})});
284
+ loadAccounts();
285
+ }
286
+ async function deleteAccount(id){
287
+ if(confirm('确认删除?')){
288
+ await fetch(`/dashboard/accounts/${id}`,{method:'DELETE',headers:{'X-Dashboard-Password':password}});
289
+ loadAccounts();
290
+ }
291
+ }
292
+ async function backup(){
293
+ const r=await fetch('/dashboard/backup',{method:'POST',headers:{'X-Dashboard-Password':password}});
294
+ const data=await r.json();
295
+ alert(data.message);
296
+ }
297
+ async function restore(){
298
+ const r=await fetch('/dashboard/restore',{method:'POST',headers:{'X-Dashboard-Password':password}});
299
+ const data=await r.json();
300
+ alert(data.message);
301
+ loadAccounts();
302
+ }
303
+ async function backupAccounts(){
304
+ const r=await fetch('/dashboard/backup/accounts',{method:'POST',headers:{'X-Dashboard-Password':password}});
305
+ const data=await r.json();
306
+ alert(data.message);
307
+ }
308
+ async function backupCache(){
309
+ const r=await fetch('/dashboard/backup/cache',{method:'POST',headers:{'X-Dashboard-Password':password}});
310
+ const data=await r.json();
311
+ alert(data.message);
312
+ }
313
+ async function backupVector(){
314
+ const r=await fetch('/dashboard/backup/vector',{method:'POST',headers:{'X-Dashboard-Password':password}});
315
+ const data=await r.json();
316
+ alert(data.message);
317
+ }
318
+ async function restoreAccounts(){
319
+ const r=await fetch('/dashboard/restore/accounts',{method:'POST',headers:{'X-Dashboard-Password':password}});
320
+ const data=await r.json();
321
+ alert(data.message);
322
+ loadAccounts();
323
+ }
324
+ async function restoreCache(){
325
+ const r=await fetch('/dashboard/restore/cache',{method:'POST',headers:{'X-Dashboard-Password':password}});
326
+ const data=await r.json();
327
+ alert(data.message);
328
+ }
329
+ async function restoreVector(){
330
+ const r=await fetch('/dashboard/restore/vector',{method:'POST',headers:{'X-Dashboard-Password':password}});
331
+ const data=await r.json();
332
+ alert(data.message);
333
+ }
334
+ </script>
335
+ </body>
336
+ </html>