KiroProxy User commited on
Commit
0e30efb
·
1 Parent(s): ac93713

安全性修复

Browse files
KiroProxy/kiro_proxy/core/admin_auth.py CHANGED
@@ -32,34 +32,16 @@ class AdminAuth:
32
 
33
  return secret_key
34
 
35
- def get_admin_password(self) -> str:
36
- """获取管理员密码"""
37
- # 1. 优先使用环境变量
 
 
 
38
  env_password = os.getenv("ADMIN_PASSWORD")
39
  if env_password:
40
- logger.info("使用环境变量中的管理员密码")
41
  return env_password
42
-
43
- # 2. 从配置文件获取
44
- admin_config = load_admin_config()
45
- stored_password = admin_config.get("password")
46
-
47
- if stored_password:
48
- logger.info("使用配置文件中的管理员密码")
49
- return stored_password
50
-
51
- # 3. 生成随机密码
52
- random_password = secrets.token_urlsafe(12)
53
- admin_config["password"] = random_password
54
- save_admin_config(admin_config)
55
-
56
- logger.warning("=" * 60)
57
- logger.warning("🔐 管理员密码已自动生成")
58
- logger.warning(f"密码: {random_password}")
59
- logger.warning("请妥善保存此密码,或设置环境变量 ADMIN_PASSWORD")
60
- logger.warning("=" * 60)
61
-
62
- return random_password
63
 
64
  def hash_password(self, password: str) -> str:
65
  """哈希密码"""
@@ -80,6 +62,8 @@ class AdminAuth:
80
  def authenticate(self, password: str) -> bool:
81
  """验证管理员密码"""
82
  admin_password = self.get_admin_password()
 
 
83
  return hmac.compare_digest(password, admin_password)
84
 
85
  def create_session(self, user_id: str = "admin") -> str:
@@ -186,6 +170,11 @@ def revoke_admin_session(session_id: str) -> bool:
186
  """撤销管理员会话"""
187
  return get_admin_auth().revoke_session(session_id)
188
 
189
- def get_admin_password() -> str:
190
  """获取管理员密码"""
191
- return get_admin_auth().get_admin_password()
 
 
 
 
 
 
32
 
33
  return secret_key
34
 
35
+ def is_auth_required(self) -> bool:
36
+ """检查是否需要认证(仅当设置了 ADMIN_PASSWORD 环境变量时才需要)"""
37
+ return bool(os.getenv("ADMIN_PASSWORD"))
38
+
39
+ def get_admin_password(self) -> Optional[str]:
40
+ """获取管理员密码,未设置环境变量时返回 None"""
41
  env_password = os.getenv("ADMIN_PASSWORD")
42
  if env_password:
 
43
  return env_password
44
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  def hash_password(self, password: str) -> str:
47
  """哈希密码"""
 
62
  def authenticate(self, password: str) -> bool:
63
  """验证管理员密码"""
64
  admin_password = self.get_admin_password()
65
+ if admin_password is None:
66
+ return True
67
  return hmac.compare_digest(password, admin_password)
68
 
69
  def create_session(self, user_id: str = "admin") -> str:
 
170
  """撤销管理员会话"""
171
  return get_admin_auth().revoke_session(session_id)
172
 
173
+ def get_admin_password() -> Optional[str]:
174
  """获取管理员密码"""
175
+ return get_admin_auth().get_admin_password()
176
+
177
+
178
+ def is_auth_required() -> bool:
179
+ """检查是否需要认证"""
180
+ return get_admin_auth().is_auth_required()
KiroProxy/kiro_proxy/core/auth_middleware.py CHANGED
@@ -1,12 +1,74 @@
1
  """管理员认证中间件和装饰器"""
 
 
2
  from functools import wraps
3
  from typing import Optional
4
  from fastapi import HTTPException, Request, Depends
5
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
6
- from .admin_auth import get_admin_auth, validate_admin_session
7
 
8
  security = HTTPBearer(auto_error=False)
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  class AdminAuthMiddleware:
11
  """管理员认证中间件"""
12
 
@@ -34,6 +96,9 @@ class AdminAuthMiddleware:
34
 
35
  async def authenticate_request(self, request: Request) -> bool:
36
  """验证请求是否已认证"""
 
 
 
37
  session_id = self.get_session_from_request(request)
38
  if not session_id:
39
  return False
@@ -56,8 +121,11 @@ def get_auth_middleware() -> AdminAuthMiddleware:
56
  async def require_admin_auth(
57
  request: Request,
58
  credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
59
- ) -> str:
60
  """要求管理员认证的依赖项"""
 
 
 
61
  middleware = get_auth_middleware()
62
 
63
  # 检查认证
 
1
  """管理员认证中间件和装饰器"""
2
+ import os
3
+ import hmac
4
  from functools import wraps
5
  from typing import Optional
6
  from fastapi import HTTPException, Request, Depends
7
  from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
8
+ from .admin_auth import get_admin_auth, validate_admin_session, is_auth_required
9
 
10
  security = HTTPBearer(auto_error=False)
11
 
12
+
13
+ def get_api_key() -> Optional[str]:
14
+ """获取 API 密钥环境变量"""
15
+ return os.getenv("API_KEY")
16
+
17
+
18
+ def is_api_auth_required() -> bool:
19
+ """检查是否需要 API 认证"""
20
+ return bool(get_api_key())
21
+
22
+
23
+ def verify_api_key(provided_key: str) -> bool:
24
+ """验证 API 密钥"""
25
+ expected_key = get_api_key()
26
+ if not expected_key:
27
+ return True
28
+ return hmac.compare_digest(provided_key, expected_key)
29
+
30
+
31
+ def extract_api_key_from_request(request: Request) -> Optional[str]:
32
+ """从请求中提取 API 密钥"""
33
+ auth_header = request.headers.get("Authorization")
34
+ if auth_header:
35
+ if auth_header.startswith("Bearer "):
36
+ return auth_header[7:]
37
+ return auth_header
38
+
39
+ api_key = request.headers.get("X-API-Key")
40
+ if api_key:
41
+ return api_key
42
+
43
+ api_key = request.query_params.get("api_key")
44
+ if api_key:
45
+ return api_key
46
+
47
+ return None
48
+
49
+
50
+ async def require_api_auth(request: Request) -> bool:
51
+ """要求 API 认证的依赖项"""
52
+ if not is_api_auth_required():
53
+ return True
54
+
55
+ api_key = extract_api_key_from_request(request)
56
+ if not api_key:
57
+ raise HTTPException(
58
+ status_code=401,
59
+ detail="Missing API key. Please provide via Authorization header (Bearer <key>), X-API-Key header, or api_key query parameter.",
60
+ headers={"WWW-Authenticate": "Bearer"}
61
+ )
62
+
63
+ if not verify_api_key(api_key):
64
+ raise HTTPException(
65
+ status_code=401,
66
+ detail="Invalid API key",
67
+ headers={"WWW-Authenticate": "Bearer"}
68
+ )
69
+
70
+ return True
71
+
72
  class AdminAuthMiddleware:
73
  """管理员认证中间件"""
74
 
 
96
 
97
  async def authenticate_request(self, request: Request) -> bool:
98
  """验证请求是否已认证"""
99
+ if not is_auth_required():
100
+ return True
101
+
102
  session_id = self.get_session_from_request(request)
103
  if not session_id:
104
  return False
 
121
  async def require_admin_auth(
122
  request: Request,
123
  credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
124
+ ) -> Optional[str]:
125
  """要求管理员认证的依赖项"""
126
+ if not is_auth_required():
127
+ return "no_auth_required"
128
+
129
  middleware = get_auth_middleware()
130
 
131
  # 检查认证
KiroProxy/kiro_proxy/core/database.py CHANGED
@@ -1,14 +1,17 @@
1
- """数据库抽象层 - 支持文件系统和远程SQL数据库"""
2
  import os
3
  import json
4
  import asyncio
 
5
  from abc import ABC, abstractmethod
6
- from typing import List, Dict, Any, Optional
7
  from pathlib import Path
8
  import logging
 
9
 
10
  logger = logging.getLogger(__name__)
11
 
 
12
  class DatabaseInterface(ABC):
13
  """数据库接口抽象类"""
14
 
@@ -47,6 +50,11 @@ class DatabaseInterface(ABC):
47
  """初始化数据库"""
48
  pass
49
 
 
 
 
 
 
50
 
51
  class FileSystemDatabase(DatabaseInterface):
52
  """文件系统数据库实现(原有逻辑)"""
@@ -127,6 +135,16 @@ class FileSystemDatabase(DatabaseInterface):
127
  logger.error(f"加载管理员配置失败: {e}")
128
  return {}
129
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  class SQLDatabase(DatabaseInterface):
132
  """SQL数据库实现"""
@@ -370,6 +388,48 @@ class SQLDatabase(DatabaseInterface):
370
  config = await self._get_config_value("kiro_admin", "main")
371
  return config or {}
372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
 
374
  # 数据库工厂
375
  def create_database() -> DatabaseInterface:
@@ -387,11 +447,307 @@ def create_database() -> DatabaseInterface:
387
 
388
  # 全局数据库实例
389
  _db_instance: Optional[DatabaseInterface] = None
 
390
 
391
  async def get_database() -> DatabaseInterface:
392
- """获取数据库实例(单例模式)"""
393
- global _db_instance
 
394
  if _db_instance is None:
395
  _db_instance = create_database()
396
  await _db_instance.initialize()
397
- return _db_instance
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """数据库抽象层 - 支持文件系统和远程SQL数据库,以及双向同步"""
2
  import os
3
  import json
4
  import asyncio
5
+ import hashlib
6
  from abc import ABC, abstractmethod
7
+ from typing import List, Dict, Any, Optional, Callable
8
  from pathlib import Path
9
  import logging
10
+ import time
11
 
12
  logger = logging.getLogger(__name__)
13
 
14
+
15
  class DatabaseInterface(ABC):
16
  """数据库接口抽象类"""
17
 
 
50
  """初始化数据库"""
51
  pass
52
 
53
+ @abstractmethod
54
+ async def get_config_hash(self) -> str:
55
+ """获取配置哈希值用于变更检测"""
56
+ pass
57
+
58
 
59
  class FileSystemDatabase(DatabaseInterface):
60
  """文件系统数据库实现(原有逻辑)"""
 
135
  logger.error(f"加载管理员配置失败: {e}")
136
  return {}
137
 
138
+ async def get_config_hash(self) -> str:
139
+ """获取配置文件的哈希值"""
140
+ try:
141
+ if self.config_file.exists():
142
+ content = self.config_file.read_bytes()
143
+ return hashlib.md5(content).hexdigest()
144
+ except Exception as e:
145
+ logger.error(f"获取配置哈希失败: {e}")
146
+ return ""
147
+
148
 
149
  class SQLDatabase(DatabaseInterface):
150
  """SQL数据库实现"""
 
388
  config = await self._get_config_value("kiro_admin", "main")
389
  return config or {}
390
 
391
+ async def get_config_hash(self) -> str:
392
+ """获取配置的哈希值"""
393
+ try:
394
+ config = await self.load_config()
395
+ content = json.dumps(config, sort_keys=True)
396
+ return hashlib.md5(content.encode()).hexdigest()
397
+ except Exception as e:
398
+ logger.error(f"获取配置哈希失败: {e}")
399
+ return ""
400
+
401
+
402
+ def _get_account_key(account: Dict[str, Any]) -> str:
403
+ """获取账号唯一标识用于去重"""
404
+ if account.get("email"):
405
+ return f"email:{account['email']}"
406
+ elif account.get("token"):
407
+ return f"token:{account['token'][:32]}"
408
+ elif account.get("id"):
409
+ return f"id:{account['id']}"
410
+ return f"hash:{hash(json.dumps(account, sort_keys=True))}"
411
+
412
+
413
+ def _merge_accounts(local_accounts: List[Dict[str, Any]], remote_accounts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
414
+ """合并本地和远程账号并去重,远程优先"""
415
+ seen_keys = set()
416
+ merged = []
417
+
418
+ for account in remote_accounts:
419
+ key = _get_account_key(account)
420
+ if key not in seen_keys:
421
+ seen_keys.add(key)
422
+ merged.append(account)
423
+
424
+ for account in local_accounts:
425
+ key = _get_account_key(account)
426
+ if key not in seen_keys:
427
+ seen_keys.add(key)
428
+ merged.append(account)
429
+ logger.info(f"从本地合并账号: {key}")
430
+
431
+ return merged
432
+
433
 
434
  # 数据库工厂
435
  def create_database() -> DatabaseInterface:
 
447
 
448
  # 全局数据库实例
449
  _db_instance: Optional[DatabaseInterface] = None
450
+ _merge_completed: bool = False
451
 
452
  async def get_database() -> DatabaseInterface:
453
+ """获取数据库实例(单例模式),首次启动时自动合并本地账号到远程"""
454
+ global _db_instance, _merge_completed
455
+
456
  if _db_instance is None:
457
  _db_instance = create_database()
458
  await _db_instance.initialize()
459
+
460
+ database_url = os.getenv("DATABASE_URL")
461
+ if database_url and not _merge_completed:
462
+ await _auto_merge_local_to_remote()
463
+ _merge_completed = True
464
+
465
+ return _db_instance
466
+
467
+
468
+ async def _auto_merge_local_to_remote():
469
+ """自动将本地账号合并到远程数据库"""
470
+ global _db_instance
471
+
472
+ try:
473
+ from ..config import DATA_DIR
474
+ local_db = FileSystemDatabase(DATA_DIR)
475
+ await local_db.initialize()
476
+
477
+ local_accounts = await local_db.load_accounts()
478
+ if not local_accounts:
479
+ logger.info("本地无账号,跳过合并")
480
+ return
481
+
482
+ remote_accounts = await _db_instance.load_accounts()
483
+
484
+ merged = _merge_accounts(local_accounts, remote_accounts)
485
+
486
+ if len(merged) > len(remote_accounts):
487
+ await _db_instance.save_accounts(merged)
488
+ new_count = len(merged) - len(remote_accounts)
489
+ logger.info(f"已将 {new_count} 个本地账号合并到远程数据库,共 {len(merged)} 个账号")
490
+ else:
491
+ logger.info(f"所有本地账号已存在于远程,无需合并")
492
+
493
+ except Exception as e:
494
+ logger.error(f"自动合并账号失败: {e}")
495
+
496
+
497
+ class SyncManager:
498
+ """���向数据同步管理器"""
499
+
500
+ def __init__(self, local_db: FileSystemDatabase, remote_db: SQLDatabase):
501
+ self.local_db = local_db
502
+ self.remote_db = remote_db
503
+ self._local_hash: str = ""
504
+ self._remote_hash: str = ""
505
+ self._sync_lock = asyncio.Lock()
506
+ self._running = False
507
+ self._sync_task: Optional[asyncio.Task] = None
508
+ self._sync_interval = int(os.getenv("SYNC_INTERVAL", "30"))
509
+ self._callbacks: List[Callable] = []
510
+
511
+ def register_callback(self, callback: Callable):
512
+ """注册同步完成回调"""
513
+ self._callbacks.append(callback)
514
+
515
+ async def _notify_callbacks(self):
516
+ """通知所有回调"""
517
+ for callback in self._callbacks:
518
+ try:
519
+ if asyncio.iscoroutinefunction(callback):
520
+ await callback()
521
+ else:
522
+ callback()
523
+ except Exception as e:
524
+ logger.error(f"同步回调执行失败: {e}")
525
+
526
+ async def start(self):
527
+ """启动同步"""
528
+ if self._running:
529
+ return
530
+
531
+ self._running = True
532
+ await self._initial_sync()
533
+
534
+ self._sync_task = asyncio.create_task(self._sync_loop())
535
+ logger.info(f"双向同步已启动,间隔 {self._sync_interval} 秒")
536
+
537
+ async def stop(self):
538
+ """停止同步"""
539
+ self._running = False
540
+ if self._sync_task:
541
+ self._sync_task.cancel()
542
+ try:
543
+ await self._sync_task
544
+ except asyncio.CancelledError:
545
+ pass
546
+ logger.info("双向同步已停止")
547
+
548
+ async def _initial_sync(self):
549
+ """初始同步:合并两端数据"""
550
+ async with self._sync_lock:
551
+ try:
552
+ local_accounts = await self.local_db.load_accounts()
553
+ remote_accounts = await self.remote_db.load_accounts()
554
+
555
+ merged = _merge_accounts_bidirectional(local_accounts, remote_accounts)
556
+
557
+ await self.local_db.save_accounts(merged)
558
+ await self.remote_db.save_accounts(merged)
559
+
560
+ self._local_hash = await self.local_db.get_config_hash()
561
+ self._remote_hash = await self.remote_db.get_config_hash()
562
+
563
+ logger.info(f"初始同步完成,共 {len(merged)} 个账号")
564
+ await self._notify_callbacks()
565
+
566
+ except Exception as e:
567
+ logger.error(f"初始同步失败: {e}")
568
+
569
+ async def _sync_loop(self):
570
+ """同步循环"""
571
+ while self._running:
572
+ try:
573
+ await asyncio.sleep(self._sync_interval)
574
+ await self._check_and_sync()
575
+ except asyncio.CancelledError:
576
+ break
577
+ except Exception as e:
578
+ logger.error(f"同步循环异常: {e}")
579
+
580
+ async def _check_and_sync(self):
581
+ """检查并同步变更"""
582
+ async with self._sync_lock:
583
+ try:
584
+ current_local_hash = await self.local_db.get_config_hash()
585
+ current_remote_hash = await self.remote_db.get_config_hash()
586
+
587
+ local_changed = current_local_hash != self._local_hash
588
+ remote_changed = current_remote_hash != self._remote_hash
589
+
590
+ if not local_changed and not remote_changed:
591
+ return
592
+
593
+ local_accounts = await self.local_db.load_accounts()
594
+ remote_accounts = await self.remote_db.load_accounts()
595
+
596
+ if local_changed and remote_changed:
597
+ logger.info("检测到本地和远程都有变更,执行合并")
598
+ merged = _merge_accounts_bidirectional(local_accounts, remote_accounts)
599
+ await self.local_db.save_accounts(merged)
600
+ await self.remote_db.save_accounts(merged)
601
+
602
+ elif local_changed:
603
+ logger.info("检测到本地变更,同步到远程")
604
+ merged = _merge_accounts_bidirectional(local_accounts, remote_accounts)
605
+ await self.remote_db.save_accounts(merged)
606
+ await self.local_db.save_accounts(merged)
607
+
608
+ elif remote_changed:
609
+ logger.info("检测到远程变更,同步到本地")
610
+ merged = _merge_accounts_bidirectional(local_accounts, remote_accounts)
611
+ await self.local_db.save_accounts(merged)
612
+ await self.remote_db.save_accounts(merged)
613
+
614
+ self._local_hash = await self.local_db.get_config_hash()
615
+ self._remote_hash = await self.remote_db.get_config_hash()
616
+
617
+ await self._notify_callbacks()
618
+
619
+ except Exception as e:
620
+ logger.error(f"同步检查失败: {e}")
621
+
622
+ async def force_sync(self, source: str = "merge"):
623
+ """强制同步
624
+
625
+ Args:
626
+ source: 同步源 - "local" (本地覆盖远程), "remote" (远程覆盖本地), "merge" (合并)
627
+ """
628
+ async with self._sync_lock:
629
+ try:
630
+ if source == "local":
631
+ accounts = await self.local_db.load_accounts()
632
+ await self.remote_db.save_accounts(accounts)
633
+ logger.info(f"强制同步:本地 -> 远程,{len(accounts)} 个账号")
634
+
635
+ elif source == "remote":
636
+ accounts = await self.remote_db.load_accounts()
637
+ await self.local_db.save_accounts(accounts)
638
+ logger.info(f"强制同步:远程 -> 本地,{len(accounts)} 个账号")
639
+
640
+ else:
641
+ local_accounts = await self.local_db.load_accounts()
642
+ remote_accounts = await self.remote_db.load_accounts()
643
+ merged = _merge_accounts_bidirectional(local_accounts, remote_accounts)
644
+ await self.local_db.save_accounts(merged)
645
+ await self.remote_db.save_accounts(merged)
646
+ logger.info(f"强制同步:双向合并,{len(merged)} 个账号")
647
+
648
+ self._local_hash = await self.local_db.get_config_hash()
649
+ self._remote_hash = await self.remote_db.get_config_hash()
650
+
651
+ await self._notify_callbacks()
652
+ return True
653
+
654
+ except Exception as e:
655
+ logger.error(f"强制同步失败: {e}")
656
+ return False
657
+
658
+ async def get_sync_status(self) -> Dict[str, Any]:
659
+ """获取同步状态"""
660
+ try:
661
+ local_accounts = await self.local_db.load_accounts()
662
+ remote_accounts = await self.remote_db.load_accounts()
663
+
664
+ current_local_hash = await self.local_db.get_config_hash()
665
+ current_remote_hash = await self.remote_db.get_config_hash()
666
+
667
+ return {
668
+ "enabled": True,
669
+ "running": self._running,
670
+ "sync_interval": self._sync_interval,
671
+ "local_accounts": len(local_accounts),
672
+ "remote_accounts": len(remote_accounts),
673
+ "local_changed": current_local_hash != self._local_hash,
674
+ "remote_changed": current_remote_hash != self._remote_hash,
675
+ "in_sync": current_local_hash == current_remote_hash,
676
+ }
677
+ except Exception as e:
678
+ return {
679
+ "enabled": True,
680
+ "running": self._running,
681
+ "error": str(e),
682
+ }
683
+
684
+
685
+ def _merge_accounts_bidirectional(
686
+ local_accounts: List[Dict[str, Any]],
687
+ remote_accounts: List[Dict[str, Any]]
688
+ ) -> List[Dict[str, Any]]:
689
+ """双向合并账号,基于更新时间或合并策略"""
690
+ accounts_map: Dict[str, Dict[str, Any]] = {}
691
+
692
+ for account in local_accounts:
693
+ key = _get_account_key(account)
694
+ account["_source"] = "local"
695
+ accounts_map[key] = account
696
+
697
+ for account in remote_accounts:
698
+ key = _get_account_key(account)
699
+ if key in accounts_map:
700
+ local_acc = accounts_map[key]
701
+ local_updated = local_acc.get("updated_at", 0)
702
+ remote_updated = account.get("updated_at", 0)
703
+
704
+ if remote_updated > local_updated:
705
+ account["_source"] = "remote"
706
+ accounts_map[key] = account
707
+ else:
708
+ account["_source"] = "remote"
709
+ accounts_map[key] = account
710
+
711
+ merged = []
712
+ for account in accounts_map.values():
713
+ clean_account = {k: v for k, v in account.items() if not k.startswith("_")}
714
+ merged.append(clean_account)
715
+
716
+ return merged
717
+
718
+
719
+ _sync_manager: Optional[SyncManager] = None
720
+
721
+
722
+ async def get_sync_manager() -> Optional[SyncManager]:
723
+ """获取同步管理器实例"""
724
+ return _sync_manager
725
+
726
+
727
+ async def start_sync_manager():
728
+ """启动同步管理器"""
729
+ global _sync_manager
730
+
731
+ database_url = os.getenv("DATABASE_URL")
732
+ if not database_url:
733
+ logger.info("未配置 DATABASE_URL,跳过同步管理器")
734
+ return
735
+
736
+ from ..config import DATA_DIR
737
+
738
+ local_db = FileSystemDatabase(DATA_DIR)
739
+ await local_db.initialize()
740
+
741
+ remote_db = SQLDatabase(database_url)
742
+ await remote_db.initialize()
743
+
744
+ _sync_manager = SyncManager(local_db, remote_db)
745
+ await _sync_manager.start()
746
+
747
+
748
+ async def stop_sync_manager():
749
+ """停止同步管理器"""
750
+ global _sync_manager
751
+ if _sync_manager:
752
+ await _sync_manager.stop()
753
+ _sync_manager = None
KiroProxy/kiro_proxy/main.py CHANGED
@@ -7,6 +7,7 @@ from fastapi.middleware.cors import CORSMiddleware
7
 
8
  from . import __version__
9
  from .core import get_quota_scheduler, get_refresh_manager, scheduler, state
 
10
  from .routers import admin, protocols, web
11
 
12
 
@@ -54,6 +55,8 @@ async def _background_initial_token_check(refresh_manager, accounts):
54
  @asynccontextmanager
55
  async def lifespan(app: FastAPI):
56
  await scheduler.start()
 
 
57
 
58
  refresh_manager = get_refresh_manager()
59
  refresh_manager.set_accounts_getter(lambda: state.accounts)
@@ -76,6 +79,7 @@ async def lifespan(app: FastAPI):
76
 
77
  await refresh_manager.stop_auto_refresh()
78
  await quota_scheduler.stop()
 
79
  await scheduler.stop()
80
 
81
 
 
7
 
8
  from . import __version__
9
  from .core import get_quota_scheduler, get_refresh_manager, scheduler, state
10
+ from .core.database import start_sync_manager, stop_sync_manager
11
  from .routers import admin, protocols, web
12
 
13
 
 
55
  @asynccontextmanager
56
  async def lifespan(app: FastAPI):
57
  await scheduler.start()
58
+
59
+ await start_sync_manager()
60
 
61
  refresh_manager = get_refresh_manager()
62
  refresh_manager.set_accounts_getter(lambda: state.accounts)
 
79
 
80
  await refresh_manager.stop_auto_refresh()
81
  await quota_scheduler.stop()
82
+ await stop_sync_manager()
83
  await scheduler.stop()
84
 
85
 
KiroProxy/kiro_proxy/routers/admin.py CHANGED
@@ -3,7 +3,7 @@ from fastapi.responses import JSONResponse
3
 
4
  from ..core import get_history_config, get_rate_limiter, update_history_config
5
  from ..core.auth_middleware import require_admin_auth, optional_admin_auth
6
- from ..core.admin_auth import get_admin_auth, authenticate_admin, create_admin_session, revoke_admin_session
7
  from ..handlers import admin as admin_handler
8
  from ..resources import get_resource_path
9
 
@@ -112,8 +112,10 @@ async def api_admin_sessions(session_id: str = Depends(require_admin_auth)):
112
  @router.post("/auth/check")
113
  async def api_admin_auth_check(session_id: str = Depends(optional_admin_auth)):
114
  """检查认证状态"""
 
115
  return {
116
- "authenticated": session_id is not None,
 
117
  "session_id": session_id
118
  }
119
 
@@ -169,12 +171,12 @@ async def api_refresh_all(session_id: str = Depends(require_admin_auth)):
169
 
170
 
171
  @router.get("/accounts/status")
172
- async def api_accounts_status_enhanced():
173
  return await admin_handler.get_accounts_status_enhanced()
174
 
175
 
176
  @router.get("/accounts/summary")
177
- async def api_accounts_summary():
178
  return await admin_handler.get_accounts_summary()
179
 
180
 
@@ -184,7 +186,7 @@ async def api_refresh_all_quotas(session_id: str = Depends(require_admin_auth)):
184
 
185
 
186
  @router.get("/refresh/progress")
187
- async def api_refresh_progress():
188
  return await admin_handler.get_refresh_progress()
189
 
190
 
@@ -194,7 +196,7 @@ async def api_refresh_all_with_progress(session_id: str = Depends(require_admin_
194
 
195
 
196
  @router.get("/refresh/config")
197
- async def api_get_refresh_config():
198
  return await admin_handler.get_refresh_config()
199
 
200
 
@@ -204,12 +206,12 @@ async def api_update_refresh_config(request: Request, session_id: str = Depends(
204
 
205
 
206
  @router.get("/refresh/status")
207
- async def api_refresh_status():
208
  return await admin_handler.get_refresh_manager_status()
209
 
210
 
211
  @router.get("/accounts")
212
- async def api_accounts():
213
  return await admin_handler.get_accounts()
214
 
215
 
@@ -234,167 +236,167 @@ async def api_toggle_account(account_id: str, session_id: str = Depends(require_
234
 
235
 
236
  @router.post("/speedtest")
237
- async def api_speedtest():
238
  return await admin_handler.speedtest()
239
 
240
 
241
  @router.get("/accounts/{account_id}/test")
242
- async def api_test_account_token(account_id: str):
243
  return await admin_handler.test_account_token(account_id)
244
 
245
 
246
  @router.get("/token/scan")
247
- async def api_scan_tokens():
248
  return await admin_handler.scan_tokens()
249
 
250
 
251
  @router.post("/token/add-from-scan")
252
- async def api_add_from_scan(request: Request):
253
  return await admin_handler.add_from_scan(request)
254
 
255
 
256
  @router.get("/config/export")
257
- async def api_export_config():
258
  return await admin_handler.export_config()
259
 
260
 
261
  @router.post("/config/import")
262
- async def api_import_config(request: Request):
263
  return await admin_handler.import_config(request)
264
 
265
 
266
  @router.post("/token/refresh-check")
267
- async def api_refresh_check():
268
  return await admin_handler.refresh_token_check()
269
 
270
 
271
  @router.post("/accounts/{account_id}/refresh")
272
- async def api_refresh_account(account_id: str):
273
  return await admin_handler.refresh_account_token_with_manager(account_id)
274
 
275
 
276
  @router.post("/accounts/{account_id}/restore")
277
- async def api_restore_account(account_id: str):
278
  return await admin_handler.restore_account(account_id)
279
 
280
 
281
  @router.get("/accounts/{account_id}/usage")
282
- async def api_account_usage(account_id: str):
283
  return await admin_handler.get_account_usage_info(account_id)
284
 
285
 
286
  @router.get("/accounts/{account_id}")
287
- async def api_account_detail(account_id: str):
288
  return await admin_handler.get_account_detail(account_id)
289
 
290
 
291
  @router.post("/accounts/{account_id}/refresh-quota")
292
- async def api_refresh_account_quota(account_id: str):
293
  return await admin_handler.refresh_account_quota_with_token(account_id)
294
 
295
 
296
  @router.get("/priority")
297
- async def api_get_priority_accounts():
298
  return await admin_handler.get_priority_accounts()
299
 
300
 
301
  @router.post("/priority/{account_id}")
302
- async def api_set_priority_account(account_id: str, request: Request):
303
  return await admin_handler.set_priority_account(account_id, request)
304
 
305
 
306
  @router.delete("/priority/{account_id}")
307
- async def api_remove_priority_account(account_id: str):
308
  return await admin_handler.remove_priority_account(account_id)
309
 
310
 
311
  @router.put("/priority/reorder")
312
- async def api_reorder_priority_accounts(request: Request):
313
  return await admin_handler.reorder_priority_accounts(request)
314
 
315
 
316
  @router.get("/quota")
317
- async def api_quota_status():
318
  return await admin_handler.get_quota_status()
319
 
320
 
321
  @router.get("/kiro/login-url")
322
- async def api_login_url():
323
  return await admin_handler.get_kiro_login_url()
324
 
325
 
326
  @router.get("/stats/detailed")
327
- async def api_detailed_stats():
328
  return await admin_handler.get_detailed_stats()
329
 
330
 
331
  @router.post("/health-check")
332
- async def api_health_check():
333
  return await admin_handler.run_health_check()
334
 
335
 
336
  @router.get("/browsers")
337
- async def api_browsers():
338
  return await admin_handler.get_browsers()
339
 
340
 
341
  @router.post("/kiro/login/start")
342
- async def api_kiro_login_start(request: Request):
343
  return await admin_handler.start_kiro_login(request)
344
 
345
 
346
  @router.get("/kiro/login/poll")
347
- async def api_kiro_login_poll():
348
  return await admin_handler.poll_kiro_login()
349
 
350
 
351
  @router.post("/kiro/login/cancel")
352
- async def api_kiro_login_cancel():
353
  return await admin_handler.cancel_kiro_login()
354
 
355
 
356
  @router.get("/kiro/login/status")
357
- async def api_kiro_login_status():
358
  return await admin_handler.get_kiro_login_status()
359
 
360
 
361
  @router.post("/kiro/social/start")
362
- async def api_social_login_start(request: Request):
363
  return await admin_handler.start_social_login(request)
364
 
365
 
366
  @router.post("/kiro/social/exchange")
367
- async def api_social_token_exchange(request: Request):
368
  return await admin_handler.exchange_social_token(request)
369
 
370
 
371
  @router.post("/kiro/social/cancel")
372
- async def api_social_login_cancel():
373
  return await admin_handler.cancel_social_login()
374
 
375
 
376
  @router.get("/kiro/social/status")
377
- async def api_social_login_status():
378
  return await admin_handler.get_social_login_status()
379
 
380
 
381
  @router.post("/protocol/register")
382
- async def api_register_protocol():
383
  return await admin_handler.register_kiro_protocol()
384
 
385
 
386
  @router.post("/protocol/unregister")
387
- async def api_unregister_protocol():
388
  return await admin_handler.unregister_kiro_protocol()
389
 
390
 
391
  @router.get("/protocol/status")
392
- async def api_protocol_status():
393
  return await admin_handler.get_protocol_status()
394
 
395
 
396
  @router.get("/protocol/callback")
397
- async def api_protocol_callback():
398
  return await admin_handler.get_callback_result()
399
 
400
 
@@ -409,6 +411,7 @@ async def api_flows(
409
  search: str = None,
410
  limit: int = 50,
411
  offset: int = 0,
 
412
  ):
413
  return await admin_handler.get_flows(
414
  protocol=protocol,
@@ -424,50 +427,50 @@ async def api_flows(
424
 
425
 
426
  @router.get("/flows/stats")
427
- async def api_flow_stats():
428
  return await admin_handler.get_flow_stats()
429
 
430
 
431
  @router.get("/flows/{flow_id}")
432
- async def api_flow_detail(flow_id: str):
433
  return await admin_handler.get_flow_detail(flow_id)
434
 
435
 
436
  @router.post("/flows/{flow_id}/bookmark")
437
- async def api_bookmark_flow(flow_id: str, request: Request):
438
  return await admin_handler.bookmark_flow(flow_id, request)
439
 
440
 
441
  @router.post("/flows/{flow_id}/note")
442
- async def api_add_flow_note(flow_id: str, request: Request):
443
  return await admin_handler.add_flow_note(flow_id, request)
444
 
445
 
446
  @router.post("/flows/{flow_id}/tag")
447
- async def api_add_flow_tag(flow_id: str, request: Request):
448
  return await admin_handler.add_flow_tag(flow_id, request)
449
 
450
 
451
  @router.post("/flows/export")
452
- async def api_export_flows(request: Request):
453
  return await admin_handler.export_flows(request)
454
 
455
 
456
  @router.get("/settings/history")
457
- async def api_get_history_config():
458
  config = get_history_config()
459
  return config.to_dict()
460
 
461
 
462
  @router.post("/settings/history")
463
- async def api_update_history_config(request: Request):
464
  data = await request.json()
465
  update_history_config(data)
466
  return {"ok": True, "config": get_history_config().to_dict()}
467
 
468
 
469
  @router.get("/settings/rate-limit")
470
- async def api_get_rate_limit_config():
471
  limiter = get_rate_limiter()
472
  return {
473
  "enabled": limiter.config.enabled,
@@ -479,7 +482,7 @@ async def api_get_rate_limit_config():
479
 
480
 
481
  @router.post("/settings/rate-limit")
482
- async def api_update_rate_limit_config(request: Request):
483
  data = await request.json()
484
  limiter = get_rate_limiter()
485
  limiter.update_config(**data)
@@ -524,3 +527,42 @@ async def api_docs_content(doc_id: str):
524
  content = doc_file.read_text(encoding="utf-8")
525
  title = DOC_TITLES.get(doc_id, doc_id)
526
  return {"id": doc_id, "title": title, "content": content}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  from ..core import get_history_config, get_rate_limiter, update_history_config
5
  from ..core.auth_middleware import require_admin_auth, optional_admin_auth
6
+ from ..core.admin_auth import get_admin_auth, authenticate_admin, create_admin_session, revoke_admin_session, is_auth_required
7
  from ..handlers import admin as admin_handler
8
  from ..resources import get_resource_path
9
 
 
112
  @router.post("/auth/check")
113
  async def api_admin_auth_check(session_id: str = Depends(optional_admin_auth)):
114
  """检查认证状态"""
115
+ auth_required = is_auth_required()
116
  return {
117
+ "auth_required": auth_required,
118
+ "authenticated": not auth_required or session_id is not None,
119
  "session_id": session_id
120
  }
121
 
 
171
 
172
 
173
  @router.get("/accounts/status")
174
+ async def api_accounts_status_enhanced(session_id: str = Depends(require_admin_auth)):
175
  return await admin_handler.get_accounts_status_enhanced()
176
 
177
 
178
  @router.get("/accounts/summary")
179
+ async def api_accounts_summary(session_id: str = Depends(require_admin_auth)):
180
  return await admin_handler.get_accounts_summary()
181
 
182
 
 
186
 
187
 
188
  @router.get("/refresh/progress")
189
+ async def api_refresh_progress(session_id: str = Depends(require_admin_auth)):
190
  return await admin_handler.get_refresh_progress()
191
 
192
 
 
196
 
197
 
198
  @router.get("/refresh/config")
199
+ async def api_get_refresh_config(session_id: str = Depends(require_admin_auth)):
200
  return await admin_handler.get_refresh_config()
201
 
202
 
 
206
 
207
 
208
  @router.get("/refresh/status")
209
+ async def api_refresh_status(session_id: str = Depends(require_admin_auth)):
210
  return await admin_handler.get_refresh_manager_status()
211
 
212
 
213
  @router.get("/accounts")
214
+ async def api_accounts(session_id: str = Depends(require_admin_auth)):
215
  return await admin_handler.get_accounts()
216
 
217
 
 
236
 
237
 
238
  @router.post("/speedtest")
239
+ async def api_speedtest(session_id: str = Depends(require_admin_auth)):
240
  return await admin_handler.speedtest()
241
 
242
 
243
  @router.get("/accounts/{account_id}/test")
244
+ async def api_test_account_token(account_id: str, session_id: str = Depends(require_admin_auth)):
245
  return await admin_handler.test_account_token(account_id)
246
 
247
 
248
  @router.get("/token/scan")
249
+ async def api_scan_tokens(session_id: str = Depends(require_admin_auth)):
250
  return await admin_handler.scan_tokens()
251
 
252
 
253
  @router.post("/token/add-from-scan")
254
+ async def api_add_from_scan(request: Request, session_id: str = Depends(require_admin_auth)):
255
  return await admin_handler.add_from_scan(request)
256
 
257
 
258
  @router.get("/config/export")
259
+ async def api_export_config(session_id: str = Depends(require_admin_auth)):
260
  return await admin_handler.export_config()
261
 
262
 
263
  @router.post("/config/import")
264
+ async def api_import_config(request: Request, session_id: str = Depends(require_admin_auth)):
265
  return await admin_handler.import_config(request)
266
 
267
 
268
  @router.post("/token/refresh-check")
269
+ async def api_refresh_check(session_id: str = Depends(require_admin_auth)):
270
  return await admin_handler.refresh_token_check()
271
 
272
 
273
  @router.post("/accounts/{account_id}/refresh")
274
+ async def api_refresh_account(account_id: str, session_id: str = Depends(require_admin_auth)):
275
  return await admin_handler.refresh_account_token_with_manager(account_id)
276
 
277
 
278
  @router.post("/accounts/{account_id}/restore")
279
+ async def api_restore_account(account_id: str, session_id: str = Depends(require_admin_auth)):
280
  return await admin_handler.restore_account(account_id)
281
 
282
 
283
  @router.get("/accounts/{account_id}/usage")
284
+ async def api_account_usage(account_id: str, session_id: str = Depends(require_admin_auth)):
285
  return await admin_handler.get_account_usage_info(account_id)
286
 
287
 
288
  @router.get("/accounts/{account_id}")
289
+ async def api_account_detail(account_id: str, session_id: str = Depends(require_admin_auth)):
290
  return await admin_handler.get_account_detail(account_id)
291
 
292
 
293
  @router.post("/accounts/{account_id}/refresh-quota")
294
+ async def api_refresh_account_quota(account_id: str, session_id: str = Depends(require_admin_auth)):
295
  return await admin_handler.refresh_account_quota_with_token(account_id)
296
 
297
 
298
  @router.get("/priority")
299
+ async def api_get_priority_accounts(session_id: str = Depends(require_admin_auth)):
300
  return await admin_handler.get_priority_accounts()
301
 
302
 
303
  @router.post("/priority/{account_id}")
304
+ async def api_set_priority_account(account_id: str, request: Request, session_id: str = Depends(require_admin_auth)):
305
  return await admin_handler.set_priority_account(account_id, request)
306
 
307
 
308
  @router.delete("/priority/{account_id}")
309
+ async def api_remove_priority_account(account_id: str, session_id: str = Depends(require_admin_auth)):
310
  return await admin_handler.remove_priority_account(account_id)
311
 
312
 
313
  @router.put("/priority/reorder")
314
+ async def api_reorder_priority_accounts(request: Request, session_id: str = Depends(require_admin_auth)):
315
  return await admin_handler.reorder_priority_accounts(request)
316
 
317
 
318
  @router.get("/quota")
319
+ async def api_quota_status(session_id: str = Depends(require_admin_auth)):
320
  return await admin_handler.get_quota_status()
321
 
322
 
323
  @router.get("/kiro/login-url")
324
+ async def api_login_url(session_id: str = Depends(require_admin_auth)):
325
  return await admin_handler.get_kiro_login_url()
326
 
327
 
328
  @router.get("/stats/detailed")
329
+ async def api_detailed_stats(session_id: str = Depends(require_admin_auth)):
330
  return await admin_handler.get_detailed_stats()
331
 
332
 
333
  @router.post("/health-check")
334
+ async def api_health_check(session_id: str = Depends(require_admin_auth)):
335
  return await admin_handler.run_health_check()
336
 
337
 
338
  @router.get("/browsers")
339
+ async def api_browsers(session_id: str = Depends(require_admin_auth)):
340
  return await admin_handler.get_browsers()
341
 
342
 
343
  @router.post("/kiro/login/start")
344
+ async def api_kiro_login_start(request: Request, session_id: str = Depends(require_admin_auth)):
345
  return await admin_handler.start_kiro_login(request)
346
 
347
 
348
  @router.get("/kiro/login/poll")
349
+ async def api_kiro_login_poll(session_id: str = Depends(require_admin_auth)):
350
  return await admin_handler.poll_kiro_login()
351
 
352
 
353
  @router.post("/kiro/login/cancel")
354
+ async def api_kiro_login_cancel(session_id: str = Depends(require_admin_auth)):
355
  return await admin_handler.cancel_kiro_login()
356
 
357
 
358
  @router.get("/kiro/login/status")
359
+ async def api_kiro_login_status(session_id: str = Depends(require_admin_auth)):
360
  return await admin_handler.get_kiro_login_status()
361
 
362
 
363
  @router.post("/kiro/social/start")
364
+ async def api_social_login_start(request: Request, session_id: str = Depends(require_admin_auth)):
365
  return await admin_handler.start_social_login(request)
366
 
367
 
368
  @router.post("/kiro/social/exchange")
369
+ async def api_social_token_exchange(request: Request, session_id: str = Depends(require_admin_auth)):
370
  return await admin_handler.exchange_social_token(request)
371
 
372
 
373
  @router.post("/kiro/social/cancel")
374
+ async def api_social_login_cancel(session_id: str = Depends(require_admin_auth)):
375
  return await admin_handler.cancel_social_login()
376
 
377
 
378
  @router.get("/kiro/social/status")
379
+ async def api_social_login_status(session_id: str = Depends(require_admin_auth)):
380
  return await admin_handler.get_social_login_status()
381
 
382
 
383
  @router.post("/protocol/register")
384
+ async def api_register_protocol(session_id: str = Depends(require_admin_auth)):
385
  return await admin_handler.register_kiro_protocol()
386
 
387
 
388
  @router.post("/protocol/unregister")
389
+ async def api_unregister_protocol(session_id: str = Depends(require_admin_auth)):
390
  return await admin_handler.unregister_kiro_protocol()
391
 
392
 
393
  @router.get("/protocol/status")
394
+ async def api_protocol_status(session_id: str = Depends(require_admin_auth)):
395
  return await admin_handler.get_protocol_status()
396
 
397
 
398
  @router.get("/protocol/callback")
399
+ async def api_protocol_callback(session_id: str = Depends(require_admin_auth)):
400
  return await admin_handler.get_callback_result()
401
 
402
 
 
411
  search: str = None,
412
  limit: int = 50,
413
  offset: int = 0,
414
+ session_id: str = Depends(require_admin_auth),
415
  ):
416
  return await admin_handler.get_flows(
417
  protocol=protocol,
 
427
 
428
 
429
  @router.get("/flows/stats")
430
+ async def api_flow_stats(session_id: str = Depends(require_admin_auth)):
431
  return await admin_handler.get_flow_stats()
432
 
433
 
434
  @router.get("/flows/{flow_id}")
435
+ async def api_flow_detail(flow_id: str, session_id: str = Depends(require_admin_auth)):
436
  return await admin_handler.get_flow_detail(flow_id)
437
 
438
 
439
  @router.post("/flows/{flow_id}/bookmark")
440
+ async def api_bookmark_flow(flow_id: str, request: Request, session_id: str = Depends(require_admin_auth)):
441
  return await admin_handler.bookmark_flow(flow_id, request)
442
 
443
 
444
  @router.post("/flows/{flow_id}/note")
445
+ async def api_add_flow_note(flow_id: str, request: Request, session_id: str = Depends(require_admin_auth)):
446
  return await admin_handler.add_flow_note(flow_id, request)
447
 
448
 
449
  @router.post("/flows/{flow_id}/tag")
450
+ async def api_add_flow_tag(flow_id: str, request: Request, session_id: str = Depends(require_admin_auth)):
451
  return await admin_handler.add_flow_tag(flow_id, request)
452
 
453
 
454
  @router.post("/flows/export")
455
+ async def api_export_flows(request: Request, session_id: str = Depends(require_admin_auth)):
456
  return await admin_handler.export_flows(request)
457
 
458
 
459
  @router.get("/settings/history")
460
+ async def api_get_history_config(session_id: str = Depends(require_admin_auth)):
461
  config = get_history_config()
462
  return config.to_dict()
463
 
464
 
465
  @router.post("/settings/history")
466
+ async def api_update_history_config(request: Request, session_id: str = Depends(require_admin_auth)):
467
  data = await request.json()
468
  update_history_config(data)
469
  return {"ok": True, "config": get_history_config().to_dict()}
470
 
471
 
472
  @router.get("/settings/rate-limit")
473
+ async def api_get_rate_limit_config(session_id: str = Depends(require_admin_auth)):
474
  limiter = get_rate_limiter()
475
  return {
476
  "enabled": limiter.config.enabled,
 
482
 
483
 
484
  @router.post("/settings/rate-limit")
485
+ async def api_update_rate_limit_config(request: Request, session_id: str = Depends(require_admin_auth)):
486
  data = await request.json()
487
  limiter = get_rate_limiter()
488
  limiter.update_config(**data)
 
527
  content = doc_file.read_text(encoding="utf-8")
528
  title = DOC_TITLES.get(doc_id, doc_id)
529
  return {"id": doc_id, "title": title, "content": content}
530
+
531
+
532
+ # ==================== 数据同步相关端点 ====================
533
+
534
+ @router.get("/sync/status")
535
+ async def api_sync_status(session_id: str = Depends(require_admin_auth)):
536
+ """获取同步状态"""
537
+ from ..core.database import get_sync_manager
538
+
539
+ sync_manager = await get_sync_manager()
540
+ if not sync_manager:
541
+ return {
542
+ "enabled": False,
543
+ "message": "未配置 DATABASE_URL,同步未启用"
544
+ }
545
+
546
+ return await sync_manager.get_sync_status()
547
+
548
+
549
+ @router.post("/sync/force")
550
+ async def api_force_sync(request: Request, session_id: str = Depends(require_admin_auth)):
551
+ """强制同步"""
552
+ from ..core.database import get_sync_manager
553
+
554
+ sync_manager = await get_sync_manager()
555
+ if not sync_manager:
556
+ raise HTTPException(status_code=400, detail="同步未启用")
557
+
558
+ data = await request.json()
559
+ source = data.get("source", "merge")
560
+
561
+ if source not in ["local", "remote", "merge"]:
562
+ raise HTTPException(status_code=400, detail="无效的同步源,可选: local, remote, merge")
563
+
564
+ success = await sync_manager.force_sync(source)
565
+ if success:
566
+ return {"ok": True, "message": f"同步完成 ({source})"}
567
+ else:
568
+ raise HTTPException(status_code=500, detail="同步失败")
KiroProxy/kiro_proxy/routers/protocols.py CHANGED
@@ -1,11 +1,12 @@
1
  import uuid
2
 
3
  import httpx
4
- from fastapi import APIRouter, Request
5
  from fastapi.responses import JSONResponse
6
 
7
  from ..config import MODELS_URL
8
  from ..core import state
 
9
  from ..credential import get_kiro_version
10
  from ..handlers import anthropic, gemini, openai
11
  from ..handlers import responses as responses_handler
@@ -14,7 +15,7 @@ router = APIRouter()
14
 
15
 
16
  @router.get("/v1/models")
17
- async def models():
18
  try:
19
  account = state.get_available_account()
20
  if not account:
@@ -71,18 +72,18 @@ async def models():
71
 
72
 
73
  @router.post("/v1/messages")
74
- async def anthropic_messages(request: Request):
75
  print(f"[Main] Received /v1/messages request from {request.client.host}")
76
  return await anthropic.handle_messages(request)
77
 
78
 
79
  @router.post("/v1/messages/count_tokens")
80
- async def anthropic_count_tokens(request: Request):
81
  return await anthropic.handle_count_tokens(request)
82
 
83
 
84
  @router.post("/v1/complete")
85
- async def anthropic_complete(request: Request):
86
  print(f"[Main] Received /v1/complete request from {request.client.host}")
87
  return JSONResponse(
88
  status_code=400,
@@ -96,16 +97,16 @@ async def anthropic_complete(request: Request):
96
 
97
 
98
  @router.post("/v1/chat/completions")
99
- async def openai_chat(request: Request):
100
  return await openai.handle_chat_completions(request)
101
 
102
 
103
  @router.post("/v1/responses")
104
- async def openai_responses(request: Request):
105
  return await responses_handler.handle_responses(request)
106
 
107
 
108
  @router.post("/v1beta/models/{model_name}:generateContent")
109
  @router.post("/v1/models/{model_name}:generateContent")
110
- async def gemini_generate(model_name: str, request: Request):
111
  return await gemini.handle_generate_content(model_name, request)
 
1
  import uuid
2
 
3
  import httpx
4
+ from fastapi import APIRouter, Request, Depends
5
  from fastapi.responses import JSONResponse
6
 
7
  from ..config import MODELS_URL
8
  from ..core import state
9
+ from ..core.auth_middleware import require_api_auth
10
  from ..credential import get_kiro_version
11
  from ..handlers import anthropic, gemini, openai
12
  from ..handlers import responses as responses_handler
 
15
 
16
 
17
  @router.get("/v1/models")
18
+ async def models(_: bool = Depends(require_api_auth)):
19
  try:
20
  account = state.get_available_account()
21
  if not account:
 
72
 
73
 
74
  @router.post("/v1/messages")
75
+ async def anthropic_messages(request: Request, _: bool = Depends(require_api_auth)):
76
  print(f"[Main] Received /v1/messages request from {request.client.host}")
77
  return await anthropic.handle_messages(request)
78
 
79
 
80
  @router.post("/v1/messages/count_tokens")
81
+ async def anthropic_count_tokens(request: Request, _: bool = Depends(require_api_auth)):
82
  return await anthropic.handle_count_tokens(request)
83
 
84
 
85
  @router.post("/v1/complete")
86
+ async def anthropic_complete(request: Request, _: bool = Depends(require_api_auth)):
87
  print(f"[Main] Received /v1/complete request from {request.client.host}")
88
  return JSONResponse(
89
  status_code=400,
 
97
 
98
 
99
  @router.post("/v1/chat/completions")
100
+ async def openai_chat(request: Request, _: bool = Depends(require_api_auth)):
101
  return await openai.handle_chat_completions(request)
102
 
103
 
104
  @router.post("/v1/responses")
105
+ async def openai_responses(request: Request, _: bool = Depends(require_api_auth)):
106
  return await responses_handler.handle_responses(request)
107
 
108
 
109
  @router.post("/v1beta/models/{model_name}:generateContent")
110
  @router.post("/v1/models/{model_name}:generateContent")
111
+ async def gemini_generate(model_name: str, request: Request, _: bool = Depends(require_api_auth)):
112
  return await gemini.handle_generate_content(model_name, request)
KiroProxy/kiro_proxy/web/html.py CHANGED
@@ -644,10 +644,62 @@ CSS_UI_COMPONENTS = '''
644
  .summary-actions { margin-top: 1rem; display: flex; gap: 0.5rem; }
645
  '''
646
 
647
- CSS_STYLES = CSS_BASE + CSS_LAYOUT + CSS_COMPONENTS + CSS_FORMS + CSS_ACCOUNTS + CSS_API + CSS_DOCS + CSS_UI_COMPONENTS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
648
 
649
 
650
  # ==================== HTML 模板 ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  # 全局进度条容器 - 显示在页面顶部
652
  HTML_GLOBAL_PROGRESS = '''
653
  <!-- 全局进度条 - 批量刷新操作进度显示 -->
@@ -1099,7 +1151,7 @@ HTML_SETTINGS = '''
1099
  <span style="background:var(--success);color:white;padding:0.125rem 0.5rem;border-radius:4px;font-size:0.75rem">自动</span>
1100
  </div>
1101
  <p style="font-size:0.875rem;color:var(--muted);margin:0">
1102
- 上下文超限时自动压缩,智能生成摘要保留关键信息
1103
  </p>
1104
  </div>
1105
  <div style="padding:1rem;background:linear-gradient(135deg,rgba(34,197,94,0.1),rgba(59,130,246,0.1));border-radius:8px">
@@ -1200,14 +1252,14 @@ HTML_SETTINGS = '''
1200
  <div id="rateLimitStats" style="padding:0.75rem;background:var(--bg);border-radius:6px;font-size:0.875rem"></div>
1201
  </div>
1202
 
1203
- <!-- 历史消息管理面板 - 已隐藏,自动化管理 -->
1204
- <div class="card" style="display:none">
1205
  <h3>历史消息管理
1206
  <button class="secondary small" onclick="loadHistoryConfig()">刷新</button>
1207
  <button class="secondary small" onclick="resetHistoryConfig()">还原默认</button>
1208
  </h3>
1209
  <p style="color:var(--muted);font-size:0.875rem;margin-bottom:1rem">
1210
- 自动处理 Kiro API 的输入长度限制,收到超限错误时智能压缩而非强硬截断
1211
  </p>
1212
 
1213
  <div style="padding:1rem;background:linear-gradient(135deg,rgba(34,197,94,0.1),rgba(59,130,246,0.1));border-radius:8px;margin-bottom:1rem">
@@ -1221,6 +1273,11 @@ HTML_SETTINGS = '''
1221
  </p>
1222
  </div>
1223
 
 
 
 
 
 
1224
  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;margin-bottom:1rem">
1225
  <div>
1226
  <label style="display:block;font-size:0.875rem;color:var(--muted);margin-bottom:0.25rem">最大重试次数</label>
@@ -1246,6 +1303,7 @@ HTML_SETTINGS = '''
1246
  2. 收到 CONTENT_LENGTH_EXCEEDS_THRESHOLD 错误时触发压缩<br>
1247
  3. 用 AI 生成早期对话摘要,保留最近 6-20 条消息<br>
1248
  4. 压缩目标: 20K-50K 字符,自动重试<br>
 
1249
  <br>
1250
  <span style="color:var(--success)">✓ 最大化利用上下文</span> &nbsp;
1251
  <span style="color:var(--success)">✓ 错误触发无需预估</span> &nbsp;
@@ -1256,10 +1314,98 @@ HTML_SETTINGS = '''
1256
  </div>
1257
  '''
1258
 
1259
- HTML_BODY = HTML_GLOBAL_PROGRESS + HTML_HEADER + HTML_HELP + HTML_MONITOR + HTML_ACCOUNTS + HTML_API + HTML_SETTINGS
1260
 
1261
 
1262
  # ==================== JavaScript ====================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1263
  JS_UTILS = '''
1264
  const $=s=>document.querySelector(s);
1265
  const $$=s=>document.querySelectorAll(s);
@@ -2251,12 +2397,14 @@ async function exportFlows(){
2251
 
2252
  JS_SETTINGS = '''
2253
  // 设置页面
2254
- // 历史消息管理(简化版,自动管理)
2255
 
2256
  async function loadHistoryConfig(){
2257
  try{
2258
  const r=await fetch('/api/settings/history');
2259
  const d=await r.json();
 
 
2260
  $('#maxRetries').value=d.max_retries||3;
2261
  $('#summaryCacheMaxAge').value=d.summary_cache_max_age_seconds||300;
2262
  $('#addWarningHeader').checked=d.add_warning_header!==false;
@@ -2264,8 +2412,10 @@ async function loadHistoryConfig(){
2264
  }
2265
 
2266
  async function updateHistoryConfig(){
 
 
2267
  const config={
2268
- strategies:['error_retry'], // 固定使用错误重试策略
2269
  max_retries:parseInt($('#maxRetries').value)||3,
2270
  summary_cache_enabled:true,
2271
  summary_cache_max_age_seconds:parseInt($('#summaryCacheMaxAge').value)||300,
@@ -3799,7 +3949,7 @@ async function pollRefreshProgress() {
3799
  }
3800
  '''
3801
 
3802
- JS_SCRIPTS = JS_UTILS + JS_TABS + JS_STATUS + JS_DOCS + JS_STATS + JS_LOGS + JS_ACCOUNTS + JS_LOGIN + JS_FLOWS + JS_SETTINGS + JS_UI_COMPONENTS
3803
 
3804
 
3805
  # ==================== 组装最终 HTML ====================
 
644
  .summary-actions { margin-top: 1rem; display: flex; gap: 0.5rem; }
645
  '''
646
 
647
+ CSS_AUTH = '''
648
+ /* 登录模态框 */
649
+ .auth-overlay {
650
+ position: fixed;
651
+ top: 0;
652
+ left: 0;
653
+ right: 0;
654
+ bottom: 0;
655
+ background: var(--bg);
656
+ z-index: 2000;
657
+ display: flex;
658
+ align-items: center;
659
+ justify-content: center;
660
+ }
661
+ .auth-modal {
662
+ background: var(--card);
663
+ border: 1px solid var(--border);
664
+ border-radius: 16px;
665
+ padding: 2rem;
666
+ max-width: 400px;
667
+ width: 90%;
668
+ text-align: center;
669
+ }
670
+ .auth-modal h2 {
671
+ margin-bottom: 1rem;
672
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
673
+ -webkit-background-clip: text;
674
+ -webkit-text-fill-color: transparent;
675
+ }
676
+ .auth-modal input {
677
+ margin: 1rem 0;
678
+ }
679
+ .auth-modal .auth-error {
680
+ color: var(--error);
681
+ font-size: 0.875rem;
682
+ margin-top: 0.5rem;
683
+ }
684
+ '''
685
+
686
+ CSS_STYLES = CSS_BASE + CSS_LAYOUT + CSS_COMPONENTS + CSS_FORMS + CSS_ACCOUNTS + CSS_API + CSS_DOCS + CSS_UI_COMPONENTS + CSS_AUTH
687
 
688
 
689
  # ==================== HTML 模板 ====================
690
+ # 登录模态框
691
+ HTML_AUTH_MODAL = '''
692
+ <div class="auth-overlay" id="authOverlay" style="display:none">
693
+ <div class="auth-modal">
694
+ <h2>🔐 管理员登录</h2>
695
+ <p style="color:var(--muted);margin-bottom:1rem">请输入管理员密码访问管理面板</p>
696
+ <input type="password" id="authPassword" placeholder="输入密码" onkeydown="if(event.key==='Enter')adminLogin()">
697
+ <button onclick="adminLogin()" style="width:100%">登录</button>
698
+ <div class="auth-error" id="authError"></div>
699
+ </div>
700
+ </div>
701
+ '''
702
+
703
  # 全局进度条容器 - 显示在页面顶部
704
  HTML_GLOBAL_PROGRESS = '''
705
  <!-- 全局进度条 - 批量刷新操作进度显示 -->
 
1151
  <span style="background:var(--success);color:white;padding:0.125rem 0.5rem;border-radius:4px;font-size:0.75rem">自动</span>
1152
  </div>
1153
  <p style="font-size:0.875rem;color:var(--muted);margin:0">
1154
+ 上下文超限时默认自动压缩并重试,也可在下方设置为超限直接报错
1155
  </p>
1156
  </div>
1157
  <div style="padding:1rem;background:linear-gradient(135deg,rgba(34,197,94,0.1),rgba(59,130,246,0.1));border-radius:8px">
 
1252
  <div id="rateLimitStats" style="padding:0.75rem;background:var(--bg);border-radius:6px;font-size:0.875rem"></div>
1253
  </div>
1254
 
1255
+ <!-- 历史消息管理面板 -->
1256
+ <div class="card">
1257
  <h3>历史消息管理
1258
  <button class="secondary small" onclick="loadHistoryConfig()">刷新</button>
1259
  <button class="secondary small" onclick="resetHistoryConfig()">还原默认</button>
1260
  </h3>
1261
  <p style="color:var(--muted);font-size:0.875rem;margin-bottom:1rem">
1262
+ 控制「上下文超限」时的行为:自动压缩重试,或直接报错
1263
  </p>
1264
 
1265
  <div style="padding:1rem;background:linear-gradient(135deg,rgba(34,197,94,0.1),rgba(59,130,246,0.1));border-radius:8px;margin-bottom:1rem">
 
1273
  </p>
1274
  </div>
1275
 
1276
+ <label style="display:flex;align-items:center;gap:0.5rem;margin-bottom:1rem;cursor:pointer">
1277
+ <input type="checkbox" id="historyErrorRetryEnabled" checked onchange="updateHistoryConfig()">
1278
+ <span><strong>超限自动压缩并重试</strong>(关闭则直接报错)</span>
1279
+ </label>
1280
+
1281
  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;margin-bottom:1rem">
1282
  <div>
1283
  <label style="display:block;font-size:0.875rem;color:var(--muted);margin-bottom:0.25rem">最大重试次数</label>
 
1303
  2. 收到 CONTENT_LENGTH_EXCEEDS_THRESHOLD 错误时触发压缩<br>
1304
  3. 用 AI 生成早期对话摘要,保留最近 6-20 条消息<br>
1305
  4. 压缩目标: 20K-50K 字符,自动重试<br>
1306
+ 5. 关闭“超限自动压缩并重试”后:超限直接报错,不进行压缩/重试<br>
1307
  <br>
1308
  <span style="color:var(--success)">✓ 最大化利用上下文</span> &nbsp;
1309
  <span style="color:var(--success)">✓ 错误触发无需预估</span> &nbsp;
 
1314
  </div>
1315
  '''
1316
 
1317
+ HTML_BODY = HTML_AUTH_MODAL + HTML_GLOBAL_PROGRESS + HTML_HEADER + HTML_HELP + HTML_MONITOR + HTML_ACCOUNTS + HTML_API + HTML_SETTINGS
1318
 
1319
 
1320
  # ==================== JavaScript ====================
1321
+ JS_AUTH = '''
1322
+ // ==================== 认证检查 ====================
1323
+ let authToken = localStorage.getItem('admin_session');
1324
+ let authRequired = false;
1325
+
1326
+ async function checkAuth() {
1327
+ try {
1328
+ const r = await fetch('/api/auth/check', {
1329
+ method: 'POST',
1330
+ headers: authToken ? {'Authorization': 'Bearer ' + authToken} : {}
1331
+ });
1332
+ const d = await r.json();
1333
+ authRequired = d.auth_required;
1334
+
1335
+ if (d.auth_required && !d.authenticated) {
1336
+ showAuthModal();
1337
+ return false;
1338
+ } else {
1339
+ hideAuthModal();
1340
+ return true;
1341
+ }
1342
+ } catch(e) {
1343
+ console.error('认证检查失败:', e);
1344
+ return true;
1345
+ }
1346
+ }
1347
+
1348
+ function showAuthModal() {
1349
+ const overlay = $('#authOverlay');
1350
+ if (overlay) {
1351
+ overlay.style.display = 'flex';
1352
+ setTimeout(() => $('#authPassword')?.focus(), 100);
1353
+ }
1354
+ }
1355
+
1356
+ function hideAuthModal() {
1357
+ const overlay = $('#authOverlay');
1358
+ if (overlay) {
1359
+ overlay.style.display = 'none';
1360
+ }
1361
+ }
1362
+
1363
+ async function adminLogin() {
1364
+ const password = $('#authPassword').value;
1365
+ const errorEl = $('#authError');
1366
+
1367
+ if (!password) {
1368
+ errorEl.textContent = '请输入密码';
1369
+ return;
1370
+ }
1371
+
1372
+ try {
1373
+ const r = await fetch('/api/auth/login', {
1374
+ method: 'POST',
1375
+ headers: {'Content-Type': 'application/json'},
1376
+ body: JSON.stringify({password})
1377
+ });
1378
+ const d = await r.json();
1379
+
1380
+ if (d.success && d.session_id) {
1381
+ authToken = d.session_id;
1382
+ localStorage.setItem('admin_session', authToken);
1383
+ hideAuthModal();
1384
+ errorEl.textContent = '';
1385
+ $('#authPassword').value = '';
1386
+ Toast.success('登录成功');
1387
+ initializeDefaultTab();
1388
+ } else {
1389
+ errorEl.textContent = d.detail || '密码错误';
1390
+ }
1391
+ } catch(e) {
1392
+ errorEl.textContent = '登录失败: ' + e.message;
1393
+ }
1394
+ }
1395
+
1396
+ function adminLogout() {
1397
+ localStorage.removeItem('admin_session');
1398
+ authToken = null;
1399
+ if (authRequired) {
1400
+ showAuthModal();
1401
+ }
1402
+ Toast.info('已登出');
1403
+ }
1404
+
1405
+ // 页面加载时检查认证
1406
+ checkAuth();
1407
+ '''
1408
+
1409
  JS_UTILS = '''
1410
  const $=s=>document.querySelector(s);
1411
  const $$=s=>document.querySelectorAll(s);
 
2397
 
2398
  JS_SETTINGS = '''
2399
  // 设置页面
2400
+ // 历史消息管理
2401
 
2402
  async function loadHistoryConfig(){
2403
  try{
2404
  const r=await fetch('/api/settings/history');
2405
  const d=await r.json();
2406
+ const strategies=Array.isArray(d.strategies)?d.strategies:[];
2407
+ $('#historyErrorRetryEnabled').checked=strategies.includes('error_retry');
2408
  $('#maxRetries').value=d.max_retries||3;
2409
  $('#summaryCacheMaxAge').value=d.summary_cache_max_age_seconds||300;
2410
  $('#addWarningHeader').checked=d.add_warning_header!==false;
 
2412
  }
2413
 
2414
  async function updateHistoryConfig(){
2415
+ const strategies=[];
2416
+ if($('#historyErrorRetryEnabled').checked) strategies.push('error_retry');
2417
  const config={
2418
+ strategies,
2419
  max_retries:parseInt($('#maxRetries').value)||3,
2420
  summary_cache_enabled:true,
2421
  summary_cache_max_age_seconds:parseInt($('#summaryCacheMaxAge').value)||300,
 
3949
  }
3950
  '''
3951
 
3952
+ JS_SCRIPTS = JS_UTILS + JS_TABS + JS_STATUS + JS_DOCS + JS_STATS + JS_LOGS + JS_ACCOUNTS + JS_LOGIN + JS_FLOWS + JS_SETTINGS + JS_UI_COMPONENTS + JS_AUTH
3953
 
3954
 
3955
  # ==================== 组装最终 HTML ====================
run.bat CHANGED
@@ -4,6 +4,14 @@ echo ========================================
4
  echo 启动 KiroProxy 服务...
5
  echo ========================================
6
 
 
 
 
 
 
 
 
 
7
  cd KiroProxy
8
 
9
  echo [1/3] 正在启动服务...
 
4
  echo 启动 KiroProxy 服务...
5
  echo ========================================
6
 
7
+ :: 设置数据库连接 (可选,留空则使用本地存储)
8
+ :: 格式: postgresql://user:password@host:port/dbname
9
+ set DATABASE_URL=
10
+
11
+ :: 设置管理员密码 (可选,留空则不需要登录)
12
+ :: 设置后访问管理面板需要输入密码
13
+ set ADMIN_PASSWORD=
14
+
15
  cd KiroProxy
16
 
17
  echo [1/3] 正在启动服务...