mirror of
https://github.com/TheSmallHanCat/sora2api.git
synced 2026-02-14 01:54:41 +08:00
feat: 新增外部pow获取
This commit is contained in:
@@ -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)):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
136
src/services/pow_service_client.py
Normal file
136
src/services/pow_service_client.py
Normal file
@@ -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}]: <string, length={len(value)}>")
|
||||
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()
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -390,23 +390,45 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- POW代理配置 -->
|
||||
<!-- POW配置 -->
|
||||
<div class="rounded-lg border border-border bg-background p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">POW代理配置</h3>
|
||||
<h3 class="text-lg font-semibold mb-4">POW配置</h3>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium mb-2 block">计算模式</label>
|
||||
<select id="cfgPowMode" onchange="togglePowFields()" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm">
|
||||
<option value="local">本地计算</option>
|
||||
<option value="external">外部服务</option>
|
||||
</select>
|
||||
<p class="text-xs text-muted-foreground mt-1">选择 POW 计算方式</p>
|
||||
</div>
|
||||
<div id="powExternalFields" style="display: none;">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-sm font-medium mb-2 block">服务器地址</label>
|
||||
<input id="cfgPowServerUrl" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="http://127.0.0.1:8002">
|
||||
<p class="text-xs text-muted-foreground mt-1">外部 POW 服务的地址</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium mb-2 block">API 密钥</label>
|
||||
<input id="cfgPowApiKey" type="password" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="输入 API 密钥">
|
||||
<p class="text-xs text-muted-foreground mt-1">访问外部 POW 服务的密钥</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" id="cfgPowProxyEnabled" class="h-4 w-4 rounded border-input">
|
||||
<input type="checkbox" id="cfgPowProxyEnabled" onchange="togglePowProxyFields()" class="h-4 w-4 rounded border-input">
|
||||
<span class="text-sm font-medium">启用POW代理</span>
|
||||
</label>
|
||||
<p class="text-xs text-muted-foreground mt-1">获取 Sentinel Token 时使用的代理</p>
|
||||
</div>
|
||||
<div>
|
||||
<div id="powProxyUrlField" style="display: none;">
|
||||
<label class="text-sm font-medium mb-2 block">POW代理地址</label>
|
||||
<input id="cfgPowProxyUrl" class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="http://127.0.0.1:7890">
|
||||
<p class="text-xs text-muted-foreground mt-1">用于获取 POW Token 的代理地址</p>
|
||||
</div>
|
||||
<button onclick="savePowProxyConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full">保存配置</button>
|
||||
<button onclick="savePowConfig()" class="inline-flex items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 h-9 px-4 w-full">保存配置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 || {};
|
||||
|
||||
Reference in New Issue
Block a user