mirror of
https://github.com/TheSmallHanCat/sora2api.git
synced 2026-02-04 02:04:42 +08:00
feat: 新增获取Sentinel Token及POW代理配置
This commit is contained in:
33
Dockerfile
33
Dockerfile
@@ -8,9 +8,42 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies for Playwright
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
wget \
|
||||||
|
gnupg \
|
||||||
|
ca-certificates \
|
||||||
|
fonts-liberation \
|
||||||
|
libatk1.0-0 \
|
||||||
|
libatk-bridge2.0-0 \
|
||||||
|
libcups2 \
|
||||||
|
libxcomposite1 \
|
||||||
|
libxdamage1 \
|
||||||
|
libxfixes3 \
|
||||||
|
libxrandr2 \
|
||||||
|
libgbm1 \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libcairo2 \
|
||||||
|
libasound2 \
|
||||||
|
libatspi2.0-0 \
|
||||||
|
libxshmfence1 \
|
||||||
|
libnss3 \
|
||||||
|
libnspr4 \
|
||||||
|
libdbus-1-3 \
|
||||||
|
libdrm2 \
|
||||||
|
libxkbcommon0 \
|
||||||
|
libx11-6 \
|
||||||
|
libxcb1 \
|
||||||
|
libxext6 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Install Playwright browsers
|
||||||
|
RUN playwright install chromium
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|||||||
@@ -13,3 +13,4 @@ toml
|
|||||||
faker==24.0.0
|
faker==24.0.0
|
||||||
python-dateutil==2.8.2
|
python-dateutil==2.8.2
|
||||||
APScheduler==3.10.4
|
APScheduler==3.10.4
|
||||||
|
playwright==1.48.0
|
||||||
@@ -167,6 +167,10 @@ class UpdateCallLogicConfigRequest(BaseModel):
|
|||||||
call_mode: Optional[str] = None # "default" or "polling"
|
call_mode: Optional[str] = None # "default" or "polling"
|
||||||
polling_mode_enabled: Optional[bool] = None # Legacy support
|
polling_mode_enabled: Optional[bool] = None # Legacy support
|
||||||
|
|
||||||
|
class UpdatePowProxyConfigRequest(BaseModel):
|
||||||
|
pow_proxy_enabled: bool
|
||||||
|
pow_proxy_url: Optional[str] = None
|
||||||
|
|
||||||
class BatchDisableRequest(BaseModel):
|
class BatchDisableRequest(BaseModel):
|
||||||
token_ids: List[int]
|
token_ids: List[int]
|
||||||
|
|
||||||
@@ -1369,6 +1373,36 @@ async def update_call_logic_config(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to update call logic configuration: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Failed to update call logic configuration: {str(e)}")
|
||||||
|
|
||||||
|
# POW proxy config endpoints
|
||||||
|
@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()
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"config": {
|
||||||
|
"pow_proxy_enabled": config_obj.pow_proxy_enabled,
|
||||||
|
"pow_proxy_url": config_obj.pow_proxy_url or ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.post("/api/pow-proxy/config")
|
||||||
|
async def update_pow_proxy_config(
|
||||||
|
request: UpdatePowProxyConfigRequest,
|
||||||
|
token: str = Depends(verify_admin_token)
|
||||||
|
):
|
||||||
|
"""Update POW proxy configuration"""
|
||||||
|
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 "")
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "POW proxy configuration updated"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to update POW proxy configuration: {str(e)}")
|
||||||
|
|
||||||
# Task management endpoints
|
# Task management endpoints
|
||||||
@router.post("/api/tasks/{task_id}/cancel")
|
@router.post("/api/tasks/{task_id}/cancel")
|
||||||
async def cancel_task(task_id: str, token: str = Depends(verify_admin_token)):
|
async def cancel_task(task_id: str, token: str = Depends(verify_admin_token)):
|
||||||
|
|||||||
@@ -236,5 +236,27 @@ class Config:
|
|||||||
self._config["call_logic"]["call_mode"] = normalized
|
self._config["call_logic"]["call_mode"] = normalized
|
||||||
self._config["call_logic"]["polling_mode_enabled"] = normalized == "polling"
|
self._config["call_logic"]["polling_mode_enabled"] = normalized == "polling"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pow_proxy_enabled(self) -> bool:
|
||||||
|
"""Get POW proxy enabled status"""
|
||||||
|
return self._config.get("pow_proxy", {}).get("pow_proxy_enabled", False)
|
||||||
|
|
||||||
|
def set_pow_proxy_enabled(self, enabled: bool):
|
||||||
|
"""Set POW proxy enabled/disabled"""
|
||||||
|
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"""
|
||||||
|
return self._config.get("pow_proxy", {}).get("pow_proxy_url", "")
|
||||||
|
|
||||||
|
def set_pow_proxy_url(self, url: str):
|
||||||
|
"""Set POW proxy URL"""
|
||||||
|
if "pow_proxy" not in self._config:
|
||||||
|
self._config["pow_proxy"] = {}
|
||||||
|
self._config["pow_proxy"]["pow_proxy_url"] = url
|
||||||
|
|
||||||
# Global config instance
|
# Global config instance
|
||||||
config = Config()
|
config = Config()
|
||||||
|
|||||||
@@ -204,6 +204,26 @@ class Database:
|
|||||||
VALUES (1, ?, ?)
|
VALUES (1, ?, ?)
|
||||||
""", (call_mode, polling_mode_enabled))
|
""", (call_mode, polling_mode_enabled))
|
||||||
|
|
||||||
|
# Ensure pow_proxy_config has a row
|
||||||
|
cursor = await db.execute("SELECT COUNT(*) FROM pow_proxy_config")
|
||||||
|
count = await cursor.fetchone()
|
||||||
|
if count[0] == 0:
|
||||||
|
# Get POW proxy config from config_dict if provided, otherwise use defaults
|
||||||
|
pow_proxy_enabled = False
|
||||||
|
pow_proxy_url = None
|
||||||
|
|
||||||
|
if config_dict:
|
||||||
|
pow_proxy_config = config_dict.get("pow_proxy", {})
|
||||||
|
pow_proxy_enabled = pow_proxy_config.get("pow_proxy_enabled", False)
|
||||||
|
pow_proxy_url = pow_proxy_config.get("pow_proxy_url", "")
|
||||||
|
# Convert empty string to None
|
||||||
|
pow_proxy_url = pow_proxy_url if pow_proxy_url else None
|
||||||
|
|
||||||
|
await db.execute("""
|
||||||
|
INSERT INTO pow_proxy_config (id, pow_proxy_enabled, pow_proxy_url)
|
||||||
|
VALUES (1, ?, ?)
|
||||||
|
""", (pow_proxy_enabled, pow_proxy_url))
|
||||||
|
|
||||||
|
|
||||||
async def check_and_migrate_db(self, config_dict: dict = None):
|
async def check_and_migrate_db(self, config_dict: dict = None):
|
||||||
"""Check database integrity and perform migrations if needed
|
"""Check database integrity and perform migrations if needed
|
||||||
@@ -486,6 +506,17 @@ class Database:
|
|||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# POW proxy config table
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS pow_proxy_config (
|
||||||
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
||||||
|
pow_proxy_enabled BOOLEAN DEFAULT 0,
|
||||||
|
pow_proxy_url TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
# Create indexes
|
# 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_id ON tasks(task_id)")
|
||||||
await db.execute("CREATE INDEX IF NOT EXISTS idx_task_status ON tasks(status)")
|
await db.execute("CREATE INDEX IF NOT EXISTS idx_task_status ON tasks(status)")
|
||||||
@@ -1233,3 +1264,25 @@ class Database:
|
|||||||
""", (normalized, polling_mode_enabled))
|
""", (normalized, polling_mode_enabled))
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
# POW proxy config operations
|
||||||
|
async def get_pow_proxy_config(self) -> "PowProxyConfig":
|
||||||
|
"""Get POW proxy configuration"""
|
||||||
|
from .models import PowProxyConfig
|
||||||
|
async with aiosqlite.connect(self.db_path) as db:
|
||||||
|
db.row_factory = aiosqlite.Row
|
||||||
|
cursor = await db.execute("SELECT * FROM pow_proxy_config WHERE id = 1")
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
if row:
|
||||||
|
return PowProxyConfig(**dict(row))
|
||||||
|
return PowProxyConfig(pow_proxy_enabled=False, pow_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:
|
||||||
|
# Use INSERT OR REPLACE to ensure the row exists
|
||||||
|
await db.execute("""
|
||||||
|
INSERT OR REPLACE INTO pow_proxy_config (id, pow_proxy_enabled, pow_proxy_url, updated_at)
|
||||||
|
VALUES (1, ?, ?, CURRENT_TIMESTAMP)
|
||||||
|
""", (pow_proxy_enabled, pow_proxy_url))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,14 @@ class CallLogicConfig(BaseModel):
|
|||||||
created_at: Optional[datetime] = None
|
created_at: Optional[datetime] = None
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class PowProxyConfig(BaseModel):
|
||||||
|
"""POW proxy configuration"""
|
||||||
|
id: int = 1
|
||||||
|
pow_proxy_enabled: bool = False # Whether to enable POW proxy
|
||||||
|
pow_proxy_url: Optional[str] = None # POW proxy URL (e.g., http://127.0.0.1:7890)
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
# API Request/Response models
|
# API Request/Response models
|
||||||
class ChatMessage(BaseModel):
|
class ChatMessage(BaseModel):
|
||||||
role: str
|
role: str
|
||||||
|
|||||||
@@ -19,14 +19,29 @@ from .proxy_manager import ProxyManager
|
|||||||
from ..core.config import config
|
from ..core.config import config
|
||||||
from ..core.logger import debug_logger
|
from ..core.logger import debug_logger
|
||||||
|
|
||||||
|
try:
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
PLAYWRIGHT_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
PLAYWRIGHT_AVAILABLE = False
|
||||||
|
|
||||||
# PoW related constants
|
# PoW related constants
|
||||||
POW_MAX_ITERATION = 500000
|
POW_MAX_ITERATION = 500000
|
||||||
POW_CORES = [8, 16, 24, 32]
|
POW_CORES = [4, 8, 12, 16, 24, 32]
|
||||||
|
|
||||||
|
POW_SCREEN_SIZES = [1266, 1536, 1920, 2560, 3000, 3072, 3120, 3840]
|
||||||
POW_SCRIPTS = [
|
POW_SCRIPTS = [
|
||||||
"https://cdn.oaistatic.com/_next/static/cXh69klOLzS0Gy2joLDRS/_ssgManifest.js?dpl=453ebaec0d44c2decab71692e1bfe39be35a24b3"
|
"https://sora-cdn.oaistatic.com/_next/static/chunks/polyfills-42372ed130431b0a.js",
|
||||||
|
"https://sora-cdn.oaistatic.com/_next/static/chunks/6974-eaafbe7db9c73c96.js",
|
||||||
|
"https://sora-cdn.oaistatic.com/_next/static/chunks/main-app-5f0c58611778fb36.js",
|
||||||
|
"https://chatgpt.com/backend-api/sentinel/sdk.js",
|
||||||
]
|
]
|
||||||
POW_DPL = ["prod-f501fe933b3edf57aea882da888e1a544df99840"]
|
|
||||||
POW_NAVIGATOR_KEYS = [
|
POW_NAVIGATOR_KEYS = [
|
||||||
|
"mimeTypes−[object MimeTypeArray]",
|
||||||
|
"userAgentData−[object NavigatorUAData]",
|
||||||
|
"scheduling−[object Scheduling]",
|
||||||
|
"keyboard−[object Keyboard]",
|
||||||
|
"webkitPersistentStorage−[object DeprecatedStorageQuota]",
|
||||||
"registerProtocolHandler−function registerProtocolHandler() { [native code] }",
|
"registerProtocolHandler−function registerProtocolHandler() { [native code] }",
|
||||||
"storage−[object StorageManager]",
|
"storage−[object StorageManager]",
|
||||||
"locks−[object LockManager]",
|
"locks−[object LockManager]",
|
||||||
@@ -41,12 +56,31 @@ POW_NAVIGATOR_KEYS = [
|
|||||||
"hardwareConcurrency−32",
|
"hardwareConcurrency−32",
|
||||||
"onLine−true",
|
"onLine−true",
|
||||||
]
|
]
|
||||||
POW_DOCUMENT_KEYS = ["_reactListeningo743lnnpvdg", "location"]
|
POW_DOCUMENT_KEYS = [
|
||||||
|
"__reactContainer$3k0e9yog4o3",
|
||||||
|
"__reactContainer$ft149nhgior",
|
||||||
|
"__reactResources$9nnifsagitb",
|
||||||
|
"_reactListeningou2wvttp2d9",
|
||||||
|
"_reactListeningu9qurgpwsme",
|
||||||
|
"_reactListeningo743lnnpvdg",
|
||||||
|
"location",
|
||||||
|
"body",
|
||||||
|
]
|
||||||
POW_WINDOW_KEYS = [
|
POW_WINDOW_KEYS = [
|
||||||
|
"getSelection",
|
||||||
|
"btoa",
|
||||||
|
"__next_s",
|
||||||
|
"crossOriginIsolated",
|
||||||
|
"print",
|
||||||
"0", "window", "self", "document", "name", "location",
|
"0", "window", "self", "document", "name", "location",
|
||||||
"navigator", "screen", "innerWidth", "innerHeight",
|
"navigator", "screen", "innerWidth", "innerHeight",
|
||||||
"localStorage", "sessionStorage", "crypto", "performance",
|
"localStorage", "sessionStorage", "crypto", "performance",
|
||||||
"fetch", "setTimeout", "setInterval", "console",
|
]
|
||||||
|
POW_LANGUAGES = [
|
||||||
|
("zh-CN", "zh-CN,zh"),
|
||||||
|
("en-US", "en-US,en"),
|
||||||
|
("ja-JP", "ja-JP,ja,en"),
|
||||||
|
("ko-KR", "ko-KR,ko,en"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# User-Agent pools
|
# User-Agent pools
|
||||||
@@ -74,7 +108,7 @@ class SoraClient:
|
|||||||
"""Sora API client with proxy support"""
|
"""Sora API client with proxy support"""
|
||||||
|
|
||||||
CHATGPT_BASE_URL = "https://chatgpt.com"
|
CHATGPT_BASE_URL = "https://chatgpt.com"
|
||||||
SENTINEL_FLOW = "sora_2_create_task"
|
SENTINEL_FLOW = "sora_2_create_task__auto"
|
||||||
|
|
||||||
def __init__(self, proxy_manager: ProxyManager):
|
def __init__(self, proxy_manager: ProxyManager):
|
||||||
self.proxy_manager = proxy_manager
|
self.proxy_manager = proxy_manager
|
||||||
@@ -83,32 +117,50 @@ class SoraClient:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_pow_parse_time() -> str:
|
def _get_pow_parse_time() -> str:
|
||||||
"""Generate time string for PoW (EST timezone)"""
|
"""Generate time string for PoW (local timezone)"""
|
||||||
now = datetime.now(timezone(timedelta(hours=-5)))
|
now = datetime.now()
|
||||||
return now.strftime("%a %b %d %Y %H:%M:%S") + " GMT-0500 (Eastern Standard Time)"
|
|
||||||
|
# Get local timezone offset (seconds)
|
||||||
|
if time.daylight and time.localtime().tm_isdst > 0:
|
||||||
|
utc_offset_seconds = -time.altzone
|
||||||
|
else:
|
||||||
|
utc_offset_seconds = -time.timezone
|
||||||
|
|
||||||
|
# Format as +0800 or -0500
|
||||||
|
offset_hours = utc_offset_seconds // 3600
|
||||||
|
offset_minutes = abs(utc_offset_seconds % 3600) // 60
|
||||||
|
offset_sign = '+' if offset_hours >= 0 else '-'
|
||||||
|
offset_str = f"{offset_sign}{abs(offset_hours):02d}{offset_minutes:02d}"
|
||||||
|
|
||||||
|
# Get timezone name
|
||||||
|
tz_name = time.tzname[1] if time.daylight and time.localtime().tm_isdst > 0 else time.tzname[0]
|
||||||
|
|
||||||
|
return now.strftime("%a %b %d %Y %H:%M:%S") + f" GMT{offset_str} ({tz_name})"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_pow_config(user_agent: str) -> list:
|
def _get_pow_config(user_agent: str) -> list:
|
||||||
"""Generate PoW config array with browser fingerprint"""
|
"""Generate PoW config array with browser fingerprint"""
|
||||||
|
lang = random.choice(POW_LANGUAGES)
|
||||||
|
perf_time = random.uniform(10000, 100000)
|
||||||
return [
|
return [
|
||||||
random.choice([1920 + 1080, 2560 + 1440, 1920 + 1200, 2560 + 1600]),
|
random.choice(POW_SCREEN_SIZES), # [0] screen size
|
||||||
SoraClient._get_pow_parse_time(),
|
SoraClient._get_pow_parse_time(), # [1] time string (local timezone)
|
||||||
4294705152,
|
random.choice([4294967296, 4294705152, 2147483648]), # [2] jsHeapSizeLimit
|
||||||
0, # [3] dynamic
|
0, # [3] iteration count (dynamic)
|
||||||
user_agent,
|
user_agent, # [4] UA
|
||||||
random.choice(POW_SCRIPTS) if POW_SCRIPTS else "",
|
random.choice(POW_SCRIPTS) if POW_SCRIPTS else "", # [5] sora cdn script
|
||||||
random.choice(POW_DPL) if POW_DPL else None,
|
None, # [6] must be null
|
||||||
"en-US",
|
lang[0], # [7] language
|
||||||
"en-US,es-US,en,es",
|
lang[1], # [8] languages
|
||||||
0, # [9] dynamic
|
random.randint(2, 10), # [9] random initial value for dynamic calc
|
||||||
random.choice(POW_NAVIGATOR_KEYS),
|
random.choice(POW_NAVIGATOR_KEYS), # [10] navigator key
|
||||||
random.choice(POW_DOCUMENT_KEYS),
|
random.choice(POW_DOCUMENT_KEYS), # [11] document key
|
||||||
random.choice(POW_WINDOW_KEYS),
|
random.choice(POW_WINDOW_KEYS), # [12] window key
|
||||||
time.perf_counter() * 1000,
|
perf_time, # [13] perf time (random)
|
||||||
str(uuid4()),
|
str(uuid4()), # [14] UUID
|
||||||
"",
|
"", # [15] empty
|
||||||
random.choice(POW_CORES),
|
random.choice(POW_CORES), # [16] cores
|
||||||
time.time() * 1000 - (time.perf_counter() * 1000),
|
time.time() * 1000 - perf_time, # [17] time origin
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -121,10 +173,12 @@ class SoraClient:
|
|||||||
static_part1 = (json.dumps(config_list[:3], separators=(',', ':'), ensure_ascii=False)[:-1] + ',').encode()
|
static_part1 = (json.dumps(config_list[:3], separators=(',', ':'), ensure_ascii=False)[:-1] + ',').encode()
|
||||||
static_part2 = (',' + json.dumps(config_list[4:9], separators=(',', ':'), ensure_ascii=False)[1:-1] + ',').encode()
|
static_part2 = (',' + json.dumps(config_list[4:9], separators=(',', ':'), ensure_ascii=False)[1:-1] + ',').encode()
|
||||||
static_part3 = (',' + json.dumps(config_list[10:], separators=(',', ':'), ensure_ascii=False)[1:]).encode()
|
static_part3 = (',' + json.dumps(config_list[10:], separators=(',', ':'), ensure_ascii=False)[1:]).encode()
|
||||||
|
initial_j = config_list[9]
|
||||||
|
|
||||||
for i in range(POW_MAX_ITERATION):
|
for i in range(POW_MAX_ITERATION):
|
||||||
dynamic_i = str(i).encode()
|
dynamic_i = str(i).encode()
|
||||||
dynamic_j = str(i >> 1).encode()
|
|
||||||
|
dynamic_j = str(initial_j + (i + 29) // 30).encode()
|
||||||
|
|
||||||
final_json = static_part1 + dynamic_i + static_part2 + dynamic_j + static_part3
|
final_json = static_part1 + dynamic_i + static_part2 + dynamic_j + static_part3
|
||||||
b64_encoded = base64.b64encode(final_json)
|
b64_encoded = base64.b64encode(final_json)
|
||||||
@@ -167,7 +221,7 @@ class SoraClient:
|
|||||||
solution, success = SoraClient._solve_pow(seed, difficulty, config_list)
|
solution, success = SoraClient._solve_pow(seed, difficulty, config_list)
|
||||||
final_pow_token = "gAAAAAB" + solution
|
final_pow_token = "gAAAAAB" + solution
|
||||||
if not success:
|
if not success:
|
||||||
debug_logger.log_warning("PoW calculation failed, using error token")
|
debug_logger.log_info("[Warning] PoW calculation failed, using error token")
|
||||||
|
|
||||||
if not final_pow_token.endswith("~S"):
|
if not final_pow_token.endswith("~S"):
|
||||||
final_pow_token = final_pow_token + "~S"
|
final_pow_token = final_pow_token + "~S"
|
||||||
@@ -203,18 +257,122 @@ class SoraClient:
|
|||||||
except URLError as exc:
|
except URLError as exc:
|
||||||
raise Exception(f"URL Error: {exc}") from exc
|
raise Exception(f"URL Error: {exc}") from exc
|
||||||
|
|
||||||
async def _nf_create_urllib(self, token: str, payload: dict, sentinel_token: str,
|
async def _get_sentinel_token_via_browser(self, proxy_url: Optional[str] = None) -> Optional[str]:
|
||||||
proxy_url: Optional[str], token_id: Optional[int] = None) -> Dict[str, Any]:
|
if not PLAYWRIGHT_AVAILABLE:
|
||||||
url = f"{self.base_url}/nf/create"
|
debug_logger.log_info("[Warning] Playwright not available, cannot use browser fallback")
|
||||||
user_agent = random.choice(MOBILE_USER_AGENTS)
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with async_playwright() as p:
|
||||||
|
launch_args = {
|
||||||
|
"headless": True,
|
||||||
|
"args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]
|
||||||
|
}
|
||||||
|
|
||||||
|
if proxy_url:
|
||||||
|
launch_args["proxy"] = {"server": proxy_url}
|
||||||
|
|
||||||
|
browser = await p.chromium.launch(**launch_args)
|
||||||
|
context = await browser.new_context(
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
page = await context.new_page()
|
||||||
|
|
||||||
|
debug_logger.log_info(f"[Browser] Navigating to sora.chatgpt.com...")
|
||||||
|
await page.goto("https://sora.chatgpt.com", wait_until="domcontentloaded", timeout=90000)
|
||||||
|
|
||||||
|
cookies = await context.cookies()
|
||||||
|
device_id = None
|
||||||
|
for cookie in cookies:
|
||||||
|
if cookie.get("name") == "oai-did":
|
||||||
|
device_id = cookie.get("value")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not device_id:
|
||||||
|
device_id = str(uuid4())
|
||||||
|
debug_logger.log_info(f"[Browser] No oai-did cookie, generated: {device_id}")
|
||||||
|
else:
|
||||||
|
debug_logger.log_info(f"[Browser] Got oai-did from cookie: {device_id}")
|
||||||
|
|
||||||
|
debug_logger.log_info(f"[Browser] Waiting for SentinelSDK...")
|
||||||
|
for _ in range(120):
|
||||||
|
try:
|
||||||
|
sdk_ready = await page.evaluate("() => typeof window.SentinelSDK !== 'undefined'")
|
||||||
|
if sdk_ready:
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
else:
|
||||||
|
debug_logger.log_info("[Browser] SentinelSDK load timeout")
|
||||||
|
await browser.close()
|
||||||
|
return None
|
||||||
|
|
||||||
|
debug_logger.log_info(f"[Browser] SentinelSDK ready, getting token...")
|
||||||
|
|
||||||
|
# 尝试获取 token,最多重试 3 次
|
||||||
|
for attempt in range(3):
|
||||||
|
debug_logger.log_info(f"[Browser] Getting token, attempt {attempt + 1}/3...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = await page.evaluate(
|
||||||
|
"(deviceId) => window.SentinelSDK.token('sora_2_create_task__auto', deviceId)",
|
||||||
|
device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if token:
|
||||||
|
debug_logger.log_info(f"[Browser] Token obtained successfully")
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
if isinstance(token, str):
|
||||||
|
token_data = json.loads(token)
|
||||||
|
else:
|
||||||
|
token_data = token
|
||||||
|
|
||||||
|
if "id" not in token_data or not token_data.get("id"):
|
||||||
|
token_data["id"] = device_id
|
||||||
|
|
||||||
|
return json.dumps(token_data, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
else:
|
||||||
|
debug_logger.log_info(f"[Browser] Token is empty")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
debug_logger.log_info(f"[Browser] Token exception: {str(e)}")
|
||||||
|
|
||||||
|
if attempt < 2:
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
debug_logger.log_error(
|
||||||
|
error_message=f"Browser sentinel token failed: {str(e)}",
|
||||||
|
status_code=0,
|
||||||
|
response_text=str(e),
|
||||||
|
source="Server"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _nf_create_urllib(self, token: str, payload: dict, sentinel_token: str,
|
||||||
|
proxy_url: Optional[str], token_id: Optional[int] = None,
|
||||||
|
user_agent: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
url = f"{self.base_url}/nf/create"
|
||||||
|
if not user_agent:
|
||||||
|
user_agent = random.choice(DESKTOP_USER_AGENTS)
|
||||||
|
|
||||||
|
import json as json_mod
|
||||||
|
sentinel_data = json_mod.loads(sentinel_token)
|
||||||
|
device_id = sentinel_data.get("id", str(uuid4()))
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {token}",
|
"Authorization": f"Bearer {token}",
|
||||||
"openai-sentinel-token": sentinel_token,
|
"OpenAI-Sentinel-Token": sentinel_token,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"User-Agent": user_agent,
|
"User-Agent": user_agent,
|
||||||
"Origin": "https://sora.chatgpt.com",
|
"OAI-Language": "en-US",
|
||||||
"Referer": "https://sora.chatgpt.com/",
|
"OAI-Device-Id": device_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -223,39 +381,108 @@ class SoraClient:
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
error_str = str(e)
|
||||||
debug_logger.log_error(
|
debug_logger.log_error(
|
||||||
error_message=f"nf/create request failed: {str(e)}",
|
error_message=f"nf/create request failed: {error_str}",
|
||||||
status_code=0,
|
status_code=0,
|
||||||
response_text=str(e),
|
response_text=error_str,
|
||||||
source="Server"
|
source="Server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if "400" in error_str or "sentinel" in error_str.lower() or "invalid" in error_str.lower():
|
||||||
|
debug_logger.log_info("Attempting browser fallback for sentinel token...")
|
||||||
|
|
||||||
|
browser_token = await self._get_sentinel_token_via_browser(proxy_url)
|
||||||
|
|
||||||
|
if browser_token:
|
||||||
|
debug_logger.log_info("Got sentinel token from browser, retrying nf/create...")
|
||||||
|
|
||||||
|
browser_data = json.loads(browser_token)
|
||||||
|
browser_device_id = browser_data.get("id", device_id)
|
||||||
|
|
||||||
|
headers["OpenAI-Sentinel-Token"] = browser_token
|
||||||
|
headers["OAI-Device-Id"] = browser_device_id
|
||||||
|
|
||||||
|
result = await asyncio.to_thread(
|
||||||
|
self._post_json_sync, url, headers, payload, 30, proxy_url
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def _generate_sentinel_token(self, token: Optional[str] = None) -> str:
|
@staticmethod
|
||||||
|
def _post_text_sync(url: str, headers: dict, body: str, timeout: int, proxy: Optional[str]) -> Dict[str, Any]:
|
||||||
|
data = body.encode("utf-8")
|
||||||
|
req = Request(url, data=data, headers=headers, method="POST")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if proxy:
|
||||||
|
opener = build_opener(ProxyHandler({"http": proxy, "https": proxy}))
|
||||||
|
resp = opener.open(req, timeout=timeout)
|
||||||
|
else:
|
||||||
|
resp = urlopen(req, timeout=timeout)
|
||||||
|
|
||||||
|
resp_text = resp.read().decode("utf-8")
|
||||||
|
if resp.status not in (200, 201):
|
||||||
|
raise Exception(f"Request failed: {resp.status} {resp_text}")
|
||||||
|
return json.loads(resp_text)
|
||||||
|
except HTTPError as exc:
|
||||||
|
body_text = exc.read().decode("utf-8", errors="ignore")
|
||||||
|
raise Exception(f"HTTP Error: {exc.code} {body_text}") from exc
|
||||||
|
except URLError as exc:
|
||||||
|
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"""
|
||||||
req_id = str(uuid4())
|
req_id = str(uuid4())
|
||||||
user_agent = random.choice(DESKTOP_USER_AGENTS)
|
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)
|
pow_token = self._get_pow_token(user_agent)
|
||||||
|
|
||||||
|
init_payload = {
|
||||||
|
"p": pow_token,
|
||||||
|
"id": req_id,
|
||||||
|
"flow": "sora_init"
|
||||||
|
}
|
||||||
|
ua_with_pow = f"{user_agent} {json.dumps(init_payload, separators=(',', ':'))}"
|
||||||
|
|
||||||
proxy_url = await self.proxy_manager.get_proxy_url()
|
proxy_url = await self.proxy_manager.get_proxy_url()
|
||||||
|
|
||||||
# Request sentinel/req endpoint
|
# Request sentinel/req endpoint
|
||||||
url = f"{self.CHATGPT_BASE_URL}/backend-api/sentinel/req"
|
url = f"{self.CHATGPT_BASE_URL}/backend-api/sentinel/req"
|
||||||
payload = {"p": pow_token, "flow": self.SENTINEL_FLOW, "id": req_id}
|
request_payload = {
|
||||||
headers = {
|
"p": pow_token,
|
||||||
"Accept": "application/json, text/plain, */*",
|
"id": req_id,
|
||||||
"Content-Type": "application/json",
|
"flow": "sora_init"
|
||||||
"Origin": "https://sora.chatgpt.com",
|
}
|
||||||
"Referer": "https://sora.chatgpt.com/",
|
request_body = json.dumps(request_payload, separators=(',', ':'))
|
||||||
"User-Agent": user_agent,
|
|
||||||
|
headers = {
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Content-Type": "text/plain;charset=UTF-8",
|
||||||
|
"Origin": "https://chatgpt.com",
|
||||||
|
"Referer": "https://chatgpt.com/backend-api/sentinel/frame.html",
|
||||||
|
"User-Agent": ua_with_pow,
|
||||||
|
"sec-ch-ua": '"Not(A:Brand";v="8", "Chromium";v="131", "Google Chrome";v="131"',
|
||||||
|
"sec-ch-ua-mobile": "?1",
|
||||||
|
"sec-ch-ua-platform": '"Android"',
|
||||||
}
|
}
|
||||||
if token:
|
|
||||||
headers["Authorization"] = f"Bearer {token}"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
resp = await asyncio.to_thread(
|
async with AsyncSession(impersonate="chrome131") as session:
|
||||||
self._post_json_sync, url, headers, payload, 10, proxy_url
|
response = await session.post(
|
||||||
)
|
url,
|
||||||
|
headers=headers,
|
||||||
|
data=request_body,
|
||||||
|
proxy=proxy_url,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
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:
|
except Exception as e:
|
||||||
debug_logger.log_error(
|
debug_logger.log_error(
|
||||||
error_message=f"Sentinel request failed: {str(e)}",
|
error_message=f"Sentinel request failed: {str(e)}",
|
||||||
@@ -269,7 +496,12 @@ class SoraClient:
|
|||||||
sentinel_token = self._build_sentinel_token(
|
sentinel_token = self._build_sentinel_token(
|
||||||
self.SENTINEL_FLOW, req_id, pow_token, resp, user_agent
|
self.SENTINEL_FLOW, req_id, pow_token, resp, user_agent
|
||||||
)
|
)
|
||||||
return sentinel_token
|
|
||||||
|
# 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
|
@staticmethod
|
||||||
def is_storyboard_prompt(prompt: str) -> bool:
|
def is_storyboard_prompt(prompt: str) -> bool:
|
||||||
@@ -358,7 +590,9 @@ class SoraClient:
|
|||||||
|
|
||||||
# 只在生成请求时添加 sentinel token
|
# 只在生成请求时添加 sentinel token
|
||||||
if add_sentinel_token:
|
if add_sentinel_token:
|
||||||
headers["openai-sentinel-token"] = await self._generate_sentinel_token(token)
|
sentinel_token, ua = await self._generate_sentinel_token(token)
|
||||||
|
headers["openai-sentinel-token"] = sentinel_token
|
||||||
|
headers["User-Agent"] = ua
|
||||||
|
|
||||||
if not multipart:
|
if not multipart:
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
@@ -557,10 +791,23 @@ class SoraClient:
|
|||||||
"style_id": style_id
|
"style_id": style_id
|
||||||
}
|
}
|
||||||
|
|
||||||
# 生成请求需要添加 sentinel token
|
|
||||||
proxy_url = await self.proxy_manager.get_proxy_url(token_id)
|
proxy_url = await self.proxy_manager.get_proxy_url(token_id)
|
||||||
sentinel_token = await self._generate_sentinel_token(token)
|
|
||||||
result = await self._nf_create_urllib(token, json_data, sentinel_token, proxy_url, token_id)
|
# Get POW proxy from configuration
|
||||||
|
pow_proxy_url = None
|
||||||
|
if config.pow_proxy_enabled:
|
||||||
|
pow_proxy_url = config.pow_proxy_url or None
|
||||||
|
|
||||||
|
sentinel_token = await self._get_sentinel_token_via_browser(pow_proxy_url)
|
||||||
|
|
||||||
|
if not sentinel_token:
|
||||||
|
# 如果浏览器方式失败,回退到手动 POW
|
||||||
|
debug_logger.log_info("[Warning] Browser sentinel token failed, falling back to manual POW")
|
||||||
|
sentinel_token, user_agent = await self._generate_sentinel_token(token)
|
||||||
|
else:
|
||||||
|
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"
|
||||||
|
|
||||||
|
result = await self._nf_create_urllib(token, json_data, sentinel_token, proxy_url, token_id, user_agent)
|
||||||
return result["id"]
|
return result["id"]
|
||||||
|
|
||||||
async def get_image_tasks(self, token: str, limit: int = 20, token_id: Optional[int] = None) -> Dict[str, Any]:
|
async def get_image_tasks(self, token: str, limit: int = 20, token_id: Optional[int] = None) -> Dict[str, Any]:
|
||||||
@@ -969,8 +1216,8 @@ class SoraClient:
|
|||||||
|
|
||||||
# Generate sentinel token and call /nf/create using urllib
|
# Generate sentinel token and call /nf/create using urllib
|
||||||
proxy_url = await self.proxy_manager.get_proxy_url()
|
proxy_url = await self.proxy_manager.get_proxy_url()
|
||||||
sentinel_token = await self._generate_sentinel_token(token)
|
sentinel_token, user_agent = await self._generate_sentinel_token(token)
|
||||||
result = await self._nf_create_urllib(token, json_data, sentinel_token, proxy_url)
|
result = await self._nf_create_urllib(token, json_data, sentinel_token, proxy_url, user_agent=user_agent)
|
||||||
return result.get("id")
|
return result.get("id")
|
||||||
|
|
||||||
async def generate_storyboard(self, prompt: str, token: str, orientation: str = "landscape",
|
async def generate_storyboard(self, prompt: str, token: str, orientation: str = "landscape",
|
||||||
|
|||||||
@@ -390,6 +390,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- POW代理配置 -->
|
||||||
|
<div class="rounded-lg border border-border bg-background p-6">
|
||||||
|
<h3 class="text-lg font-semibold mb-4">POW代理配置</h3>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="inline-flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" id="cfgPowProxyEnabled" 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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 错误处理配置 -->
|
<!-- 错误处理配置 -->
|
||||||
<div class="rounded-lg border border-border bg-background p-6">
|
<div class="rounded-lg border border-border bg-background p-6">
|
||||||
<h3 class="text-lg font-semibold mb-4">错误处理配置</h3>
|
<h3 class="text-lg font-semibold mb-4">错误处理配置</h3>
|
||||||
@@ -1098,7 +1118,9 @@
|
|||||||
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')}},
|
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)}},
|
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')}},
|
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')}},
|
||||||
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();loadWatermarkFreeConfig();loadCacheConfig();loadGenerationTimeout();loadATAutoRefreshConfig();loadCallLogicConfig()}else if(t==='logs'){loadLogs()}};
|
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()}};
|
||||||
// 自适应生成面板 iframe 高度
|
// 自适应生成面板 iframe 高度
|
||||||
window.addEventListener('message', (event) => {
|
window.addEventListener('message', (event) => {
|
||||||
const data = event.data || {};
|
const data = event.data || {};
|
||||||
|
|||||||
Reference in New Issue
Block a user