From 5a0ccbe2de933a7ae34712e3fd13e457982d85a6 Mon Sep 17 00:00:00 2001 From: TheSmallHanCat Date: Mon, 2 Feb 2026 12:57:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=A4=96=E9=83=A8pow?= =?UTF-8?q?=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/admin.py | 79 +++++++++++++-- src/core/config.py | 79 ++++++++++++++- src/core/database.py | 77 +++++++++++++++ src/core/logger.py | 12 +++ src/core/models.py | 11 +++ src/main.py | 9 ++ src/services/pow_service_client.py | 136 ++++++++++++++++++++++++++ src/services/sora_client.py | 152 ++++++++++++++++++++++++----- static/manage.html | 42 ++++++-- 9 files changed, 554 insertions(+), 43 deletions(-) create mode 100644 src/services/pow_service_client.py diff --git a/src/api/admin.py b/src/api/admin.py index 4986397..65973af 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -171,6 +171,13 @@ class UpdatePowProxyConfigRequest(BaseModel): pow_proxy_enabled: bool pow_proxy_url: Optional[str] = None +class UpdatePowServiceConfigRequest(BaseModel): + mode: str # "local" or "external" + server_url: Optional[str] = None + api_key: Optional[str] = None + proxy_enabled: Optional[bool] = None + proxy_url: Optional[str] = None + class BatchDisableRequest(BaseModel): token_ids: List[int] @@ -1373,16 +1380,17 @@ async def update_call_logic_config( except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to update call logic configuration: {str(e)}") -# POW proxy config endpoints +# POW proxy config endpoints (redirected to pow_service config for unified management) @router.get("/api/pow-proxy/config") async def get_pow_proxy_config(token: str = Depends(verify_admin_token)) -> dict: - """Get POW proxy configuration""" - config_obj = await db.get_pow_proxy_config() + """Get POW proxy configuration (unified with pow_service config)""" + # Read from pow_service config for unified management + config_obj = await db.get_pow_service_config() return { "success": True, "config": { - "pow_proxy_enabled": config_obj.pow_proxy_enabled, - "pow_proxy_url": config_obj.pow_proxy_url or "" + "pow_proxy_enabled": config_obj.proxy_enabled, + "pow_proxy_url": config_obj.proxy_url or "" } } @@ -1391,11 +1399,20 @@ async def update_pow_proxy_config( request: UpdatePowProxyConfigRequest, token: str = Depends(verify_admin_token) ): - """Update POW proxy configuration""" + """Update POW proxy configuration (unified with pow_service config)""" try: - await db.update_pow_proxy_config(request.pow_proxy_enabled, request.pow_proxy_url) - config.set_pow_proxy_enabled(request.pow_proxy_enabled) - config.set_pow_proxy_url(request.pow_proxy_url or "") + # Update pow_service config instead for unified management + config_obj = await db.get_pow_service_config() + await db.update_pow_service_config( + mode=config_obj.mode, + server_url=config_obj.server_url, + api_key=config_obj.api_key, + proxy_enabled=request.pow_proxy_enabled, + proxy_url=request.pow_proxy_url + ) + # Update in-memory config + config.set_pow_service_proxy_enabled(request.pow_proxy_enabled) + config.set_pow_service_proxy_url(request.pow_proxy_url or "") return { "success": True, "message": "POW proxy configuration updated" @@ -1403,6 +1420,50 @@ async def update_pow_proxy_config( except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to update POW proxy configuration: {str(e)}") +# POW service config endpoints +@router.get("/api/pow/config") +async def get_pow_service_config(token: str = Depends(verify_admin_token)) -> dict: + """Get POW service configuration""" + config_obj = await db.get_pow_service_config() + return { + "success": True, + "config": { + "mode": config_obj.mode, + "server_url": config_obj.server_url or "", + "api_key": config_obj.api_key or "", + "proxy_enabled": config_obj.proxy_enabled, + "proxy_url": config_obj.proxy_url or "" + } + } + +@router.post("/api/pow/config") +async def update_pow_service_config( + request: UpdatePowServiceConfigRequest, + token: str = Depends(verify_admin_token) +): + """Update POW service configuration""" + try: + await db.update_pow_service_config( + mode=request.mode, + server_url=request.server_url, + api_key=request.api_key, + proxy_enabled=request.proxy_enabled, + proxy_url=request.proxy_url + ) + # Update runtime config + config.set_pow_service_mode(request.mode) + config.set_pow_service_server_url(request.server_url or "") + config.set_pow_service_api_key(request.api_key or "") + config.set_pow_service_proxy_enabled(request.proxy_enabled or False) + config.set_pow_service_proxy_url(request.proxy_url or "") + + return { + "success": True, + "message": "POW service configuration updated" + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to update POW service configuration: {str(e)}") + # Task management endpoints @router.post("/api/tasks/{task_id}/cancel") async def cancel_task(task_id: str, token: str = Depends(verify_admin_token)): diff --git a/src/core/config.py b/src/core/config.py index 15fbd45..b97c978 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -238,25 +238,96 @@ class Config: @property def pow_proxy_enabled(self) -> bool: - """Get POW proxy enabled status""" + """Get POW proxy enabled status + + DEPRECATED: This configuration is deprecated. Use pow_service_proxy_enabled instead. + All POW proxy settings are now unified under [pow_service] section. + """ return self._config.get("pow_proxy", {}).get("pow_proxy_enabled", False) def set_pow_proxy_enabled(self, enabled: bool): - """Set POW proxy enabled/disabled""" + """Set POW proxy enabled/disabled + + DEPRECATED: This configuration is deprecated. Use set_pow_service_proxy_enabled instead. + All POW proxy settings are now unified under [pow_service] section. + """ if "pow_proxy" not in self._config: self._config["pow_proxy"] = {} self._config["pow_proxy"]["pow_proxy_enabled"] = enabled @property def pow_proxy_url(self) -> str: - """Get POW proxy URL""" + """Get POW proxy URL + + DEPRECATED: This configuration is deprecated. Use pow_service_proxy_url instead. + All POW proxy settings are now unified under [pow_service] section. + """ return self._config.get("pow_proxy", {}).get("pow_proxy_url", "") def set_pow_proxy_url(self, url: str): - """Set POW proxy URL""" + """Set POW proxy URL + + DEPRECATED: This configuration is deprecated. Use set_pow_service_proxy_url instead. + All POW proxy settings are now unified under [pow_service] section. + """ if "pow_proxy" not in self._config: self._config["pow_proxy"] = {} self._config["pow_proxy"]["pow_proxy_url"] = url + @property + def pow_service_mode(self) -> str: + """Get POW service mode (local or external)""" + return self._config.get("pow_service", {}).get("mode", "local") + + def set_pow_service_mode(self, mode: str): + """Set POW service mode""" + if "pow_service" not in self._config: + self._config["pow_service"] = {} + self._config["pow_service"]["mode"] = mode + + @property + def pow_service_server_url(self) -> str: + """Get POW service server URL""" + return self._config.get("pow_service", {}).get("server_url", "") + + def set_pow_service_server_url(self, url: str): + """Set POW service server URL""" + if "pow_service" not in self._config: + self._config["pow_service"] = {} + self._config["pow_service"]["server_url"] = url + + @property + def pow_service_api_key(self) -> str: + """Get POW service API key""" + return self._config.get("pow_service", {}).get("api_key", "") + + def set_pow_service_api_key(self, api_key: str): + """Set POW service API key""" + if "pow_service" not in self._config: + self._config["pow_service"] = {} + self._config["pow_service"]["api_key"] = api_key + + @property + def pow_service_proxy_enabled(self) -> bool: + """Get POW service proxy enabled status""" + return self._config.get("pow_service", {}).get("proxy_enabled", False) + + def set_pow_service_proxy_enabled(self, enabled: bool): + """Set POW service proxy enabled status""" + if "pow_service" not in self._config: + self._config["pow_service"] = {} + self._config["pow_service"]["proxy_enabled"] = enabled + + @property + def pow_service_proxy_url(self) -> str: + """Get POW service proxy URL""" + return self._config.get("pow_service", {}).get("proxy_url", "") + + def set_pow_service_proxy_url(self, url: str): + """Set POW service proxy URL""" + if "pow_service" not in self._config: + self._config["pow_service"] = {} + self._config["pow_service"]["proxy_url"] = url + # Global config instance config = Config() diff --git a/src/core/database.py b/src/core/database.py index 5ab27a9..79752e4 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -224,6 +224,34 @@ class Database: VALUES (1, ?, ?) """, (pow_proxy_enabled, pow_proxy_url)) + # Ensure pow_service_config has a row + cursor = await db.execute("SELECT COUNT(*) FROM pow_service_config") + count = await cursor.fetchone() + if count[0] == 0: + # Get POW service config from config_dict if provided, otherwise use defaults + mode = "local" + server_url = None + api_key = None + proxy_enabled = False + proxy_url = None + + if config_dict: + pow_service_config = config_dict.get("pow_service", {}) + mode = pow_service_config.get("mode", "local") + server_url = pow_service_config.get("server_url", "") + api_key = pow_service_config.get("api_key", "") + proxy_enabled = pow_service_config.get("proxy_enabled", False) + proxy_url = pow_service_config.get("proxy_url", "") + # Convert empty strings to None + server_url = server_url if server_url else None + api_key = api_key if api_key else None + proxy_url = proxy_url if proxy_url else None + + await db.execute(""" + INSERT INTO pow_service_config (id, mode, server_url, api_key, proxy_enabled, proxy_url) + VALUES (1, ?, ?, ?, ?, ?) + """, (mode, server_url, api_key, proxy_enabled, proxy_url)) + async def check_and_migrate_db(self, config_dict: dict = None): """Check database integrity and perform migrations if needed @@ -517,6 +545,20 @@ class Database: ) """) + # Create pow_service_config table + await db.execute(""" + CREATE TABLE IF NOT EXISTS pow_service_config ( + id INTEGER PRIMARY KEY DEFAULT 1, + mode TEXT DEFAULT 'local', + server_url TEXT, + api_key TEXT, + proxy_enabled BOOLEAN DEFAULT 0, + proxy_url TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + # Create indexes await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)") await db.execute("CREATE INDEX IF NOT EXISTS idx_task_status ON tasks(status)") @@ -1276,6 +1318,23 @@ class Database: return PowProxyConfig(**dict(row)) return PowProxyConfig(pow_proxy_enabled=False, pow_proxy_url=None) + async def get_pow_service_config(self) -> "PowServiceConfig": + """Get POW service configuration""" + from .models import PowServiceConfig + async with aiosqlite.connect(self.db_path) as db: + db.row_factory = aiosqlite.Row + cursor = await db.execute("SELECT * FROM pow_service_config WHERE id = 1") + row = await cursor.fetchone() + if row: + return PowServiceConfig(**dict(row)) + return PowServiceConfig( + mode="local", + server_url=None, + api_key=None, + proxy_enabled=False, + proxy_url=None + ) + async def update_pow_proxy_config(self, pow_proxy_enabled: bool, pow_proxy_url: Optional[str] = None): """Update POW proxy configuration""" async with aiosqlite.connect(self.db_path) as db: @@ -1286,3 +1345,21 @@ class Database: """, (pow_proxy_enabled, pow_proxy_url)) await db.commit() + async def update_pow_service_config( + self, + mode: str, + server_url: Optional[str] = None, + api_key: Optional[str] = None, + proxy_enabled: Optional[bool] = None, + proxy_url: Optional[str] = None + ): + """Update POW service configuration""" + async with aiosqlite.connect(self.db_path) as db: + # Use INSERT OR REPLACE to ensure the row exists + await db.execute(""" + INSERT OR REPLACE INTO pow_service_config (id, mode, server_url, api_key, proxy_enabled, proxy_url, updated_at) + VALUES (1, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + """, (mode, server_url, api_key, proxy_enabled, proxy_url)) + await db.commit() + + diff --git a/src/core/logger.py b/src/core/logger.py index 8fdc663..5758d42 100644 --- a/src/core/logger.py +++ b/src/core/logger.py @@ -270,6 +270,18 @@ class DebugLogger: except Exception as e: self.logger.error(f"Error logging info: {e}") + def log_warning(self, message: str): + """Log warning message to log.txt""" + + # Check if debug mode is enabled + if not config.debug_enabled: + return + + try: + self.logger.warning(f"⚠️ [{self._format_timestamp()}] {message}") + except Exception as e: + self.logger.error(f"Error logging warning: {e}") + # Global debug logger instance debug_logger = DebugLogger() diff --git a/src/core/models.py b/src/core/models.py index 1ef25a5..c30b079 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -154,6 +154,17 @@ class PowProxyConfig(BaseModel): created_at: Optional[datetime] = None updated_at: Optional[datetime] = None +class PowServiceConfig(BaseModel): + """POW service configuration""" + id: int = 1 + mode: str = "local" # "local" or "external" + server_url: Optional[str] = None # External POW service URL + api_key: Optional[str] = None # External POW service API key + proxy_enabled: bool = False # Whether to enable proxy for POW service + proxy_url: Optional[str] = None # Proxy URL for POW service + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + # API Request/Response models class ChatMessage(BaseModel): role: str diff --git a/src/main.py b/src/main.py index 25cba37..8a268b0 100644 --- a/src/main.py +++ b/src/main.py @@ -144,6 +144,15 @@ async def startup_event(): config.set_call_logic_mode(call_logic_config.call_mode) print(f"✓ Call logic mode: {call_logic_config.call_mode}") + # Load POW service configuration from database + pow_service_config = await db.get_pow_service_config() + config.set_pow_service_mode(pow_service_config.mode) + config.set_pow_service_server_url(pow_service_config.server_url or "") + config.set_pow_service_api_key(pow_service_config.api_key or "") + config.set_pow_service_proxy_enabled(pow_service_config.proxy_enabled) + config.set_pow_service_proxy_url(pow_service_config.proxy_url or "") + print(f"✓ POW service mode: {pow_service_config.mode}") + # Initialize concurrency manager with all tokens all_tokens = await db.get_all_tokens() await concurrency_manager.initialize(all_tokens) diff --git a/src/services/pow_service_client.py b/src/services/pow_service_client.py new file mode 100644 index 0000000..c6b3542 --- /dev/null +++ b/src/services/pow_service_client.py @@ -0,0 +1,136 @@ +"""POW Service Client - External POW service integration""" +import json +from typing import Optional, Tuple +from curl_cffi.requests import AsyncSession + +from ..core.config import config +from ..core.logger import debug_logger + + +class POWServiceClient: + """Client for external POW service API""" + + async def get_sentinel_token(self) -> Optional[Tuple[str, str, str]]: + """Get sentinel token from external POW service + + Returns: + Tuple of (sentinel_token, device_id, user_agent) or None on failure + """ + # Read configuration dynamically on each call + server_url = config.pow_service_server_url + api_key = config.pow_service_api_key + proxy_enabled = config.pow_service_proxy_enabled + proxy_url = config.pow_service_proxy_url if proxy_enabled else None + + if not server_url or not api_key: + debug_logger.log_error( + error_message="POW service not configured: missing server_url or api_key", + status_code=0, + response_text="Configuration error", + source="POWServiceClient" + ) + return None + + # Construct API endpoint + api_url = f"{server_url.rstrip('/')}/api/pow/token" + + headers = { + "Authorization": f"Bearer {api_key}", + "Accept": "application/json" + } + + try: + debug_logger.log_info(f"[POW Service] Requesting token from {api_url}") + + async with AsyncSession(impersonate="chrome131") as session: + response = await session.get( + api_url, + headers=headers, + proxy=proxy_url, + timeout=30 + ) + + if response.status_code != 200: + error_msg = f"POW service request failed: {response.status_code}" + debug_logger.log_error( + error_message=error_msg, + status_code=response.status_code, + response_text=response.text, + source="POWServiceClient" + ) + return None + + data = response.json() + + if not data.get("success"): + debug_logger.log_error( + error_message="POW service returned success=false", + status_code=response.status_code, + response_text=response.text, + source="POWServiceClient" + ) + return None + + token = data.get("token") + device_id = data.get("device_id") + user_agent = data.get("user_agent") + cached = data.get("cached", False) + + if not token: + debug_logger.log_error( + error_message="POW service returned empty token", + status_code=response.status_code, + response_text=response.text, + source="POWServiceClient" + ) + return None + + # Parse token to extract device_id if not provided + token_data = None + if not device_id: + try: + token_data = json.loads(token) + device_id = token_data.get("id") + except: + pass + + # 记录详细的 token 信息 + cache_status = "cached" if cached else "fresh" + debug_logger.log_info("=" * 100) + debug_logger.log_info(f"[POW Service] Token obtained successfully ({cache_status})") + debug_logger.log_info(f"[POW Service] Token length: {len(token)}") + debug_logger.log_info(f"[POW Service] Device ID: {device_id}") + debug_logger.log_info(f"[POW Service] User Agent: {user_agent}") + + # 解析并显示 token 结构 + if not token_data: + try: + token_data = json.loads(token) + except: + debug_logger.log_info(f"[POW Service] Token is not valid JSON") + token_data = None + + if token_data: + debug_logger.log_info(f"[POW Service] Token structure keys: {list(token_data.keys())}") + for key, value in token_data.items(): + if isinstance(value, str) and len(value) > 100: + debug_logger.log_info(f"[POW Service] Token[{key}]: ") + else: + debug_logger.log_info(f"[POW Service] Token[{key}]: {value}") + + debug_logger.log_info("=" * 100) + + return token, device_id, user_agent + + except Exception as e: + debug_logger.log_error( + error_message=f"POW service request exception: {str(e)}", + status_code=0, + response_text=str(e), + source="POWServiceClient" + ) + return None + + +# Global instance +pow_service_client = POWServiceClient() diff --git a/src/services/sora_client.py b/src/services/sora_client.py index 1819d28..57e842b 100644 --- a/src/services/sora_client.py +++ b/src/services/sora_client.py @@ -16,6 +16,7 @@ from urllib.error import HTTPError, URLError from curl_cffi.requests import AsyncSession from curl_cffi import CurlMime from .proxy_manager import ProxyManager +from .pow_service_client import pow_service_client from ..core.config import config from ..core.logger import debug_logger @@ -232,32 +233,47 @@ async def _generate_sentinel_token_lightweight(proxy_url: str = None, device_id: async def _get_cached_sentinel_token(proxy_url: str = None, force_refresh: bool = False) -> str: """Get sentinel token with caching support - + Args: proxy_url: Optional proxy URL force_refresh: Force refresh token (e.g., after 400 error) - + Returns: Sentinel token string or None - + Raises: Exception: If 403/429 when fetching oai-did """ global _cached_sentinel_token - + + # Check if external POW service is configured + if config.pow_service_mode == "external": + debug_logger.log_info("[POW] Using external POW service (cached sentinel)") + from .pow_service_client import pow_service_client + result = await pow_service_client.get_sentinel_token() + + if result: + sentinel_token, device_id, service_user_agent = result + debug_logger.log_info("[POW] External service returned sentinel token successfully") + return sentinel_token + else: + # Fallback to local mode if external service fails + debug_logger.log_info("[POW] External service failed, falling back to local mode") + + # Local mode (original logic) # Return cached token if available and not forcing refresh if _cached_sentinel_token and not force_refresh: debug_logger.log_info("[Sentinel] Using cached token") return _cached_sentinel_token - + # Generate new token debug_logger.log_info("[Sentinel] Generating new token...") token = await _generate_sentinel_token_lightweight(proxy_url) - + if token: _cached_sentinel_token = token debug_logger.log_info("[Sentinel] Token cached successfully") - + return token @@ -602,10 +618,10 @@ class SoraClient: proxy_url: Optional[str], token_id: Optional[int] = None, user_agent: Optional[str] = None) -> Dict[str, Any]: """Make nf/create request - + Returns: Response dict on success - + Raises: Exception: With error info, including '400' in message for sentinel token errors """ @@ -616,20 +632,85 @@ class SoraClient: import json as json_mod sentinel_data = json_mod.loads(sentinel_token) device_id = sentinel_data.get("id", str(uuid4())) - + headers = { "Authorization": f"Bearer {token}", - "OpenAI-Sentinel-Token": sentinel_token, + "openai-sentinel-token": sentinel_token, # 使用小写,与成功的 curl 请求一致 "Content-Type": "application/json", "User-Agent": user_agent, - "OAI-Language": "en-US", - "OAI-Device-Id": device_id, + "oai-language": "en-US", # 使用小写 + "oai-device-id": device_id, # 使用小写 + "Accept": "*/*", + "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", + "Cache-Control": "no-cache", + "Origin": "https://sora.chatgpt.com", + "Referer": "https://sora.chatgpt.com/explore", + "Sec-Ch-Ua": '"Not(A:Brand";v="8", "Chromium";v="131", "Google Chrome";v="131"', + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": '"Windows"', + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "Pragma": "no-cache", + "Priority": "u=1, i", } + # 添加 Cookie 头(关键修复) + if token_id: + try: + from src.core.database import Database + db = Database() + token_obj = await db.get_token(token_id) + if token_obj and token_obj.st: + # 添加 session token cookie + headers["Cookie"] = f"__Secure-next-auth.session-token={token_obj.st}" + debug_logger.log_info(f"[nf/create] Added session token cookie (length: {len(token_obj.st)})") + else: + debug_logger.log_warning("[nf/create] No session token (st) found for this token") + except Exception as e: + debug_logger.log_warning(f"[nf/create] Failed to get session token: {e}") + + # 记录详细的 Sentinel Token 信息 + debug_logger.log_info(f"[nf/create] Preparing request to {url}") + debug_logger.log_info(f"[nf/create] Device ID: {device_id}") + + # Sentinel Token 前100字符和后50字符 + if len(sentinel_token) > 150: + debug_logger.log_info(f"[nf/create] Sentinel Token (first 100 chars): {sentinel_token[:100]}...") + debug_logger.log_info(f"[nf/create] Sentinel Token (last 50 chars): ...{sentinel_token[-50:]}") + else: + debug_logger.log_info(f"[nf/create] Sentinel Token: {sentinel_token}") + + debug_logger.log_info(f"[nf/create] Sentinel Token length: {len(sentinel_token)}") + + # Sentinel Token 结构信息 + debug_logger.log_info(f"[nf/create] Sentinel Token structure:") + if "p" in sentinel_data: + debug_logger.log_info(f" - p (POW) length: {len(sentinel_data['p'])}") + if "t" in sentinel_data: + debug_logger.log_info(f" - t (Turnstile) length: {len(sentinel_data['t'])}") + if "c" in sentinel_data: + debug_logger.log_info(f" - c (Challenge) length: {len(sentinel_data['c'])}") + if "id" in sentinel_data: + debug_logger.log_info(f" - id: {sentinel_data['id']}") + if "flow" in sentinel_data: + debug_logger.log_info(f" - flow: {sentinel_data['flow']}") + + # 使用 log_request 方法记录完整的请求详情 + debug_logger.log_request( + method="POST", + url=url, + headers=headers, + body=payload, + proxy=proxy_url, + source="Server" + ) + try: result = await asyncio.to_thread( self._post_json_sync, url, headers, payload, 30, proxy_url ) + debug_logger.log_info(f"[nf/create] Request succeeded, task_id: {result.get('id', 'N/A')}") return result except Exception as e: error_str = str(e) @@ -664,13 +745,40 @@ class SoraClient: raise Exception(f"URL Error: {exc}") from exc async def _generate_sentinel_token(self, token: Optional[str] = None, user_agent: Optional[str] = None) -> Tuple[str, str]: - """Generate openai-sentinel-token by calling /backend-api/sentinel/req and solving PoW""" + """Generate openai-sentinel-token by calling /backend-api/sentinel/req and solving PoW + + Supports two modes: + - external: Get complete sentinel token from external POW service + - local: Generate POW locally and call sentinel/req endpoint + """ + # Check if external POW service is configured + if config.pow_service_mode == "external": + debug_logger.log_info("[Sentinel] Using external POW service...") + result = await pow_service_client.get_sentinel_token() + + if result: + sentinel_token, device_id, service_user_agent = result + # Use service user agent if provided, otherwise use default + final_user_agent = service_user_agent if service_user_agent else ( + user_agent if user_agent else + "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36" + ) + + debug_logger.log_info(f"[Sentinel] Got token from external service") + debug_logger.log_info(f"[Sentinel] Token cached successfully (external)") + return sentinel_token, final_user_agent + else: + # Fallback to local mode if external service fails + debug_logger.log_info("[Sentinel] External service failed, falling back to local mode") + + # Local mode (original logic) + debug_logger.log_info("[POW] Using local POW generation") req_id = str(uuid4()) if not user_agent: user_agent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36" pow_token = self._get_pow_token(user_agent) - + init_payload = { "p": pow_token, "id": req_id, @@ -688,7 +796,7 @@ class SoraClient: "flow": "sora_init" } request_body = json.dumps(request_payload, separators=(',', ':')) - + headers = { "Accept": "*/*", "Content-Type": "text/plain;charset=UTF-8", @@ -712,7 +820,7 @@ class SoraClient: if response.status_code != 200: raise Exception(f"Sentinel request failed: {response.status_code} {response.text}") resp = response.json() - + debug_logger.log_info(f"Sentinel response: turnstile.dx={bool(resp.get('turnstile', {}).get('dx'))}, token={bool(resp.get('token'))}, pow_required={resp.get('proofofwork', {}).get('required')}") except Exception as e: debug_logger.log_error( @@ -727,11 +835,11 @@ class SoraClient: sentinel_token = self._build_sentinel_token( self.SENTINEL_FLOW, req_id, pow_token, resp, user_agent ) - + # Log final token for debugging parsed = json.loads(sentinel_token) debug_logger.log_info(f"Final sentinel: p_prefix={parsed['p'][:10]}, p_suffix={parsed['p'][-5:]}, t_len={len(parsed['t'])}, c_len={len(parsed['c'])}, flow={parsed['flow']}") - + return sentinel_token, user_agent @staticmethod @@ -1024,10 +1132,10 @@ class SoraClient: proxy_url = await self.proxy_manager.get_proxy_url(token_id) - # Get POW proxy from configuration + # Get POW proxy from configuration (unified with pow_service config) pow_proxy_url = None - if config.pow_proxy_enabled: - pow_proxy_url = config.pow_proxy_url or None + if config.pow_service_proxy_enabled: + pow_proxy_url = config.pow_service_proxy_url or None user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" diff --git a/static/manage.html b/static/manage.html index fa2a325..cc326b9 100644 --- a/static/manage.html +++ b/static/manage.html @@ -390,23 +390,45 @@ - +
-

POW代理配置

+

POW配置

+
+ + +

选择 POW 计算方式

+
+

获取 Sentinel Token 时使用的代理

-
+ - +
@@ -1118,9 +1140,13 @@ deleteCharacter=async(id)=>{if(!confirm('确定要删除这个角色卡吗?'))return;try{const r=await apiRequest(`/api/characters/${id}`,{method:'DELETE'});if(!r)return;const d=await r.json();if(d.success){showToast('删除成功','success');await loadCharacters()}else{showToast('删除失败','error')}}catch(e){showToast('删除失败: '+e.message,'error')}}, loadCallLogicConfig=async()=>{try{const r=await apiRequest('/api/call-logic/config');if(!r)return;const d=await r.json();if(d.success&&d.config){const mode=d.config.call_mode||((d.config.polling_mode_enabled||false)?'polling':'default');$('cfgCallLogicMode').value=mode}else{console.error('调用逻辑配置数据格式错误:',d)}}catch(e){console.error('加载调用逻辑配置失败:',e)}}, saveCallLogicConfig=async()=>{try{const mode=$('cfgCallLogicMode').value||'default';const r=await apiRequest('/api/call-logic/config',{method:'POST',body:JSON.stringify({call_mode:mode})});if(!r)return;const d=await r.json();if(d.success){showToast('调用逻辑配置保存成功','success')}else{showToast('保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}}, - loadPowProxyConfig=async()=>{try{const r=await apiRequest('/api/pow-proxy/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPowProxyEnabled').checked=d.config.pow_proxy_enabled||false;$('cfgPowProxyUrl').value=d.config.pow_proxy_url||''}else{console.error('POW代理配置数据格式错误:',d)}}catch(e){console.error('加载POW代理配置失败:',e)}}, - savePowProxyConfig=async()=>{try{const r=await apiRequest('/api/pow-proxy/config',{method:'POST',body:JSON.stringify({pow_proxy_enabled:$('cfgPowProxyEnabled').checked,pow_proxy_url:$('cfgPowProxyUrl').value.trim()})});if(!r)return;const d=await r.json();if(d.success){showToast('POW代理配置保存成功','success')}else{showToast('保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}}, - switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings','logs','generate'].forEach(n=>{const active=n===t;$(`panel${cap(n)}`).classList.toggle('hidden',!active);$(`tab${cap(n)}`).classList.toggle('border-primary',active);$(`tab${cap(n)}`).classList.toggle('text-primary',active);$(`tab${cap(n)}`).classList.toggle('border-transparent',!active);$(`tab${cap(n)}`).classList.toggle('text-muted-foreground',!active)});if(t==='settings'){loadAdminConfig();loadProxyConfig();loadPowProxyConfig();loadWatermarkFreeConfig();loadCacheConfig();loadGenerationTimeout();loadATAutoRefreshConfig();loadCallLogicConfig()}else if(t==='logs'){loadLogs()}}; + loadPowConfig=async()=>{try{const r=await apiRequest('/api/pow/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPowMode').value=d.config.mode||'local';$('cfgPowServerUrl').value=d.config.server_url||'';$('cfgPowApiKey').value=d.config.api_key||'';$('cfgPowProxyEnabled').checked=d.config.proxy_enabled||false;$('cfgPowProxyUrl').value=d.config.proxy_url||'';togglePowFields();togglePowProxyFields()}else{console.error('POW配置数据格式错误:',d)}}catch(e){console.error('加载POW配置失败:',e)}}, + savePowConfig=async()=>{try{const mode=$('cfgPowMode').value;const serverUrl=$('cfgPowServerUrl').value.trim();const apiKey=$('cfgPowApiKey').value.trim();const proxyEnabled=$('cfgPowProxyEnabled').checked;const proxyUrl=$('cfgPowProxyUrl').value.trim();if(mode==='external'){if(!serverUrl)return showToast('请输入服务器地址','error');if(!apiKey)return showToast('请输入API密钥','error')}const r=await apiRequest('/api/pow/config',{method:'POST',body:JSON.stringify({mode:mode,server_url:serverUrl||null,api_key:apiKey||null,proxy_enabled:proxyEnabled,proxy_url:proxyUrl||null})});if(!r)return;const d=await r.json();if(d.success){showToast('POW配置保存成功','success')}else{showToast('保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}}, + loadPowProxyConfig=loadPowConfig,savePowProxyConfig=savePowConfig,loadPowServiceConfig=loadPowConfig,savePowServiceConfig=savePowConfig, + togglePowFields=()=>{const mode=$('cfgPowMode').value;const externalFields=$('powExternalFields');if(externalFields){externalFields.style.display=mode==='external'?'block':'none'}}, + togglePowProxyFields=()=>{const enabled=$('cfgPowProxyEnabled').checked;const proxyUrlField=$('powProxyUrlField');if(proxyUrlField){proxyUrlField.style.display=enabled?'block':'none'}}, + togglePowServiceFields=()=>{togglePowFields()}, + switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings','logs','generate'].forEach(n=>{const active=n===t;$(`panel${cap(n)}`).classList.toggle('hidden',!active);$(`tab${cap(n)}`).classList.toggle('border-primary',active);$(`tab${cap(n)}`).classList.toggle('text-primary',active);$(`tab${cap(n)}`).classList.toggle('border-transparent',!active);$(`tab${cap(n)}`).classList.toggle('text-muted-foreground',!active)});if(t==='settings'){loadAdminConfig();loadProxyConfig();loadPowProxyConfig();loadPowServiceConfig();loadWatermarkFreeConfig();loadCacheConfig();loadGenerationTimeout();loadATAutoRefreshConfig();loadCallLogicConfig()}else if(t==='logs'){loadLogs()}}; // 自适应生成面板 iframe 高度 window.addEventListener('message', (event) => { const data = event.data || {};