feat(pow): 新增计算pow支持传入token

This commit is contained in:
TheSmallHanCat
2026-03-02 01:28:37 +08:00
parent 404cbd44f0
commit ad554d900a
9 changed files with 144 additions and 27 deletions

View File

@@ -64,6 +64,10 @@ timezone_offset = 8
# beta测试目前仍处于测试阶段
# POW 计算模式local本地计算或 external外部服务
mode = "external"
# 是否使用对应 token 进行 POW 计算(默认关闭)
# local 模式开启后会使用当前轮询 token 获取 POW
# external 模式开启后会向外部服务传递 accesstoken 字段
use_token_for_pow = false
# 外部 POW 服务地址(仅在 external 模式下使用)
server_url = "http://localhost:8002"
# 外部 POW 服务访问密钥(仅在 external 模式下使用)

View File

@@ -173,6 +173,7 @@ class UpdatePowProxyConfigRequest(BaseModel):
class UpdatePowServiceConfigRequest(BaseModel):
mode: str # "local" or "external"
use_token_for_pow: Optional[bool] = False
server_url: Optional[str] = None
api_key: Optional[str] = None
proxy_enabled: Optional[bool] = None
@@ -1408,6 +1409,7 @@ async def update_pow_proxy_config(
config_obj = await db.get_pow_service_config()
await db.update_pow_service_config(
mode=config_obj.mode,
use_token_for_pow=config_obj.use_token_for_pow,
server_url=config_obj.server_url,
api_key=config_obj.api_key,
proxy_enabled=request.pow_proxy_enabled,
@@ -1432,6 +1434,7 @@ async def get_pow_service_config(token: str = Depends(verify_admin_token)) -> di
"success": True,
"config": {
"mode": config_obj.mode,
"use_token_for_pow": config_obj.use_token_for_pow,
"server_url": config_obj.server_url or "",
"api_key": config_obj.api_key or "",
"proxy_enabled": config_obj.proxy_enabled,
@@ -1448,6 +1451,7 @@ async def update_pow_service_config(
try:
await db.update_pow_service_config(
mode=request.mode,
use_token_for_pow=request.use_token_for_pow or False,
server_url=request.server_url,
api_key=request.api_key,
proxy_enabled=request.proxy_enabled,
@@ -1455,6 +1459,7 @@ async def update_pow_service_config(
)
# Update runtime config
config.set_pow_service_mode(request.mode)
config.set_pow_service_use_token_for_pow(request.use_token_for_pow or False)
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)

View File

@@ -285,6 +285,17 @@ class Config:
self._config["pow_service"] = {}
self._config["pow_service"]["mode"] = mode
@property
def pow_service_use_token_for_pow(self) -> bool:
"""Whether to use current token for POW calculation"""
return self._config.get("pow_service", {}).get("use_token_for_pow", False)
def set_pow_service_use_token_for_pow(self, enabled: bool):
"""Set whether to use current token for POW calculation"""
if "pow_service" not in self._config:
self._config["pow_service"] = {}
self._config["pow_service"]["use_token_for_pow"] = enabled
@property
def pow_service_server_url(self) -> str:
"""Get POW service server URL"""

View File

@@ -230,6 +230,7 @@ class Database:
if count[0] == 0:
# Get POW service config from config_dict if provided, otherwise use defaults
mode = "local"
use_token_for_pow = False
server_url = None
api_key = None
proxy_enabled = False
@@ -238,6 +239,7 @@ class Database:
if config_dict:
pow_service_config = config_dict.get("pow_service", {})
mode = pow_service_config.get("mode", "local")
use_token_for_pow = pow_service_config.get("use_token_for_pow", False)
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)
@@ -248,9 +250,9 @@ class Database:
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))
INSERT INTO pow_service_config (id, mode, use_token_for_pow, server_url, api_key, proxy_enabled, proxy_url)
VALUES (1, ?, ?, ?, ?, ?, ?)
""", (mode, use_token_for_pow, server_url, api_key, proxy_enabled, proxy_url))
async def check_and_migrate_db(self, config_dict: dict = None):
@@ -319,6 +321,35 @@ class Database:
except Exception as e:
print(f" ✗ Failed to add column '{col_name}': {e}")
# Check and add missing columns to pow_service_config table
if await self._table_exists(db, "pow_service_config"):
added_use_token_for_pow_column = False
columns_to_add = [
("use_token_for_pow", "BOOLEAN DEFAULT 0"),
]
for col_name, col_type in columns_to_add:
if not await self._column_exists(db, "pow_service_config", col_name):
try:
await db.execute(f"ALTER TABLE pow_service_config ADD COLUMN {col_name} {col_type}")
print(f" ✓ Added column '{col_name}' to pow_service_config table")
if col_name == "use_token_for_pow":
added_use_token_for_pow_column = True
except Exception as e:
print(f" ✗ Failed to add column '{col_name}': {e}")
# On upgrade, initialize value from setting.toml only when this column is newly added
if config_dict and added_use_token_for_pow_column:
try:
use_token_for_pow = config_dict.get("pow_service", {}).get("use_token_for_pow", False)
await db.execute("""
UPDATE pow_service_config
SET use_token_for_pow = ?
WHERE id = 1
""", (use_token_for_pow,))
except Exception as e:
print(f" ✗ Failed to initialize use_token_for_pow from config: {e}")
# Check and add missing columns to watermark_free_config table
if await self._table_exists(db, "watermark_free_config"):
columns_to_add = [
@@ -551,6 +582,7 @@ class Database:
CREATE TABLE IF NOT EXISTS pow_service_config (
id INTEGER PRIMARY KEY DEFAULT 1,
mode TEXT DEFAULT 'local',
use_token_for_pow BOOLEAN DEFAULT 0,
server_url TEXT,
api_key TEXT,
proxy_enabled BOOLEAN DEFAULT 0,
@@ -1354,6 +1386,7 @@ class Database:
return PowServiceConfig(**dict(row))
return PowServiceConfig(
mode="local",
use_token_for_pow=False,
server_url=None,
api_key=None,
proxy_enabled=False,
@@ -1373,6 +1406,7 @@ class Database:
async def update_pow_service_config(
self,
mode: str,
use_token_for_pow: bool = False,
server_url: Optional[str] = None,
api_key: Optional[str] = None,
proxy_enabled: Optional[bool] = None,
@@ -1382,9 +1416,9 @@ class Database:
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))
INSERT OR REPLACE INTO pow_service_config (id, mode, use_token_for_pow, server_url, api_key, proxy_enabled, proxy_url, updated_at)
VALUES (1, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
""", (mode, use_token_for_pow, server_url, api_key, proxy_enabled, proxy_url))
await db.commit()

View File

@@ -160,6 +160,7 @@ class PowServiceConfig(BaseModel):
"""POW service configuration"""
id: int = 1
mode: str = "local" # "local" or "external"
use_token_for_pow: bool = False # Whether to use current token for POW calculation
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

View File

@@ -147,11 +147,12 @@ async def startup_event():
# 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_use_token_for_pow(pow_service_config.use_token_for_pow)
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}")
print(f"✓ POW service mode: {pow_service_config.mode}, use_token_for_pow: {pow_service_config.use_token_for_pow}")
# Initialize concurrency manager with all tokens
all_tokens = await db.get_all_tokens()

View File

@@ -39,24 +39,59 @@ class POWServiceClient:
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/json"
"Accept": "application/json",
"Content-Type": "application/json",
}
# Add access_token to headers if provided
if access_token:
headers["X-Access-Token"] = access_token
# Controlled by config switch: whether to pass current token to POW service
send_access_token = bool(config.pow_service_use_token_for_pow and access_token)
def _mask_token(token_value: Optional[str]) -> str:
if not token_value:
return "none"
if len(token_value) <= 10:
return "***"
return f"{token_value[:6]}...{token_value[-4:]}"
debug_logger.log_info(
f"[POW Service] use_token_for_pow={config.pow_service_use_token_for_pow}, access_token={_mask_token(access_token)}"
)
try:
debug_logger.log_info(f"[POW Service] Requesting token from {api_url}")
async with AsyncSession(impersonate="chrome131") as session:
response = await session.get(
# Preferred protocol: POST + JSON body
payload = {"flow": "sora_init"}
if send_access_token:
payload["accesstoken"] = access_token
response = await session.post(
api_url,
headers=headers,
json=payload,
proxy=proxy_url,
timeout=30
)
# Backward compatibility: older services may only support GET + X-Access-Token
if response.status_code in (404, 405, 415):
fallback_headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/json"
}
if send_access_token:
fallback_headers["X-Access-Token"] = access_token
debug_logger.log_info(
f"[POW Service] POST unsupported ({response.status_code}), fallback to GET compatibility mode"
)
response = await session.get(
api_url,
headers=fallback_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(

View File

@@ -32,7 +32,7 @@ _playwright = None
_current_proxy = None
# Sentinel token cache
_cached_sentinel_token = None
_cached_sentinel_token_map = {}
_cached_device_id = None
@@ -245,13 +245,19 @@ async def _get_cached_sentinel_token(proxy_url: str = None, force_refresh: bool
Raises:
Exception: If 403/429 when fetching oai-did
"""
global _cached_sentinel_token
global _cached_sentinel_token_map
# Whether current request should be token-aware for POW
use_token_for_pow = bool(config.pow_service_use_token_for_pow and access_token)
cache_key = access_token if use_token_for_pow else "__default__"
# 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(access_token=access_token)
result = await pow_service_client.get_sentinel_token(
access_token=access_token if use_token_for_pow else None
)
if result:
sentinel_token, device_id, service_user_agent = result
@@ -263,25 +269,36 @@ async def _get_cached_sentinel_token(proxy_url: str = None, force_refresh: bool
# 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
if not force_refresh and cache_key in _cached_sentinel_token_map:
if use_token_for_pow:
debug_logger.log_info("[Sentinel] Using token-scoped cached token")
else:
debug_logger.log_info("[Sentinel] Using shared cached token")
return _cached_sentinel_token_map[cache_key]
# 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
_cached_sentinel_token_map[cache_key] = token
debug_logger.log_info("[Sentinel] Token cached successfully")
return token
def _invalidate_sentinel_cache():
"""Invalidate cached sentinel token (call after 400 error)"""
global _cached_sentinel_token
_cached_sentinel_token = None
def _invalidate_sentinel_cache(access_token: Optional[str] = None):
"""Invalidate cached sentinel token (call after 400 error)
Args:
access_token: Optional current access token for token-scoped cache invalidation
"""
global _cached_sentinel_token_map
use_token_for_pow = bool(config.pow_service_use_token_for_pow and access_token)
cache_key = access_token if use_token_for_pow else "__default__"
if cache_key in _cached_sentinel_token_map:
del _cached_sentinel_token_map[cache_key]
debug_logger.log_info("[Sentinel] Cache invalidated")
@@ -755,7 +772,9 @@ class SoraClient:
# 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(access_token=token)
result = await pow_service_client.get_sentinel_token(
access_token=token if config.pow_service_use_token_for_pow else None
)
if result:
sentinel_token, device_id, service_user_agent = result
@@ -1173,7 +1192,7 @@ class SoraClient:
debug_logger.log_info("[Sentinel] Got 400 error, refreshing token and retrying...")
# Invalidate cache and get fresh token
_invalidate_sentinel_cache()
_invalidate_sentinel_cache(token)
try:
sentinel_token = await _get_cached_sentinel_token(pow_proxy_url, force_refresh=True, access_token=token)

View File

@@ -402,6 +402,13 @@
</select>
<p class="text-xs text-muted-foreground mt-1">选择 POW 计算方式</p>
</div>
<div>
<label class="inline-flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="cfgPowUseTokenForPow" class="h-4 w-4 rounded border-input">
<span class="text-sm font-medium">使用对应 Token 进行计算</span>
</label>
<p class="text-xs text-muted-foreground mt-1">默认关闭。local 模式下使用当前轮询 Token 计算external 模式下会传递 accesstoken 字段。</p>
</div>
<div id="powExternalFields" style="display: none;">
<div class="space-y-4">
<div>
@@ -1142,8 +1149,8 @@
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')}},
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')}},
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';$('cfgPowUseTokenForPow').checked=d.config.use_token_for_pow||false;$('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 useTokenForPow=$('cfgPowUseTokenForPow').checked;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,use_token_for_pow:useTokenForPow,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'}},