Commit ·
35d960d
1
Parent(s): 4bb28f5
fix: 文件缓存
Browse filesfeat: 自动更新st接口
- requirements.txt +1 -1
- src/api/admin.py +134 -0
- src/core/database.py +74 -1
- src/core/models.py +8 -0
- src/services/file_cache.py +118 -16
- static/manage.html +34 -1
requirements.txt
CHANGED
|
@@ -7,4 +7,4 @@ tomli==2.2.1
|
|
| 7 |
bcrypt==4.2.1
|
| 8 |
python-multipart==0.0.20
|
| 9 |
python-dateutil==2.8.2
|
| 10 |
-
playwright==1.
|
|
|
|
| 7 |
bcrypt==4.2.1
|
| 8 |
python-multipart==0.0.20
|
| 9 |
python-dateutil==2.8.2
|
| 10 |
+
playwright==1.53.0
|
src/api/admin.py
CHANGED
|
@@ -878,3 +878,137 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)):
|
|
| 878 |
"browser_proxy_enabled": captcha_config.browser_proxy_enabled,
|
| 879 |
"browser_proxy_url": captcha_config.browser_proxy_url or ""
|
| 880 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 878 |
"browser_proxy_enabled": captcha_config.browser_proxy_enabled,
|
| 879 |
"browser_proxy_url": captcha_config.browser_proxy_url or ""
|
| 880 |
}
|
| 881 |
+
|
| 882 |
+
|
| 883 |
+
# ========== Plugin Configuration Endpoints ==========
|
| 884 |
+
|
| 885 |
+
@router.get("/api/plugin/config")
|
| 886 |
+
async def get_plugin_config(token: str = Depends(verify_admin_token)):
|
| 887 |
+
"""Get plugin configuration"""
|
| 888 |
+
plugin_config = await db.get_plugin_config()
|
| 889 |
+
|
| 890 |
+
# Get server host and port from config
|
| 891 |
+
from ..core.config import config
|
| 892 |
+
server_host = config.server_host
|
| 893 |
+
server_port = config.server_port
|
| 894 |
+
|
| 895 |
+
# Generate connection URL
|
| 896 |
+
if server_host == "0.0.0.0":
|
| 897 |
+
connection_url = f"http://127.0.0.1:{server_port}/api/plugin/update-token"
|
| 898 |
+
else:
|
| 899 |
+
connection_url = f"http://{server_host}:{server_port}/api/plugin/update-token"
|
| 900 |
+
|
| 901 |
+
return {
|
| 902 |
+
"success": True,
|
| 903 |
+
"config": {
|
| 904 |
+
"connection_token": plugin_config.connection_token,
|
| 905 |
+
"connection_url": connection_url
|
| 906 |
+
}
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
|
| 910 |
+
@router.post("/api/plugin/config")
|
| 911 |
+
async def update_plugin_config(
|
| 912 |
+
request: dict,
|
| 913 |
+
token: str = Depends(verify_admin_token)
|
| 914 |
+
):
|
| 915 |
+
"""Update plugin configuration"""
|
| 916 |
+
connection_token = request.get("connection_token", "")
|
| 917 |
+
|
| 918 |
+
# Generate random token if empty
|
| 919 |
+
if not connection_token:
|
| 920 |
+
connection_token = secrets.token_urlsafe(32)
|
| 921 |
+
|
| 922 |
+
await db.update_plugin_config(connection_token=connection_token)
|
| 923 |
+
|
| 924 |
+
return {
|
| 925 |
+
"success": True,
|
| 926 |
+
"message": "插件配置更新成功",
|
| 927 |
+
"connection_token": connection_token
|
| 928 |
+
}
|
| 929 |
+
|
| 930 |
+
|
| 931 |
+
@router.post("/api/plugin/update-token")
|
| 932 |
+
async def plugin_update_token(request: dict, authorization: Optional[str] = Header(None)):
|
| 933 |
+
"""Receive token update from Chrome extension (no admin auth required, uses connection_token)"""
|
| 934 |
+
# Verify connection token
|
| 935 |
+
plugin_config = await db.get_plugin_config()
|
| 936 |
+
|
| 937 |
+
# Extract token from Authorization header
|
| 938 |
+
provided_token = None
|
| 939 |
+
if authorization:
|
| 940 |
+
if authorization.startswith("Bearer "):
|
| 941 |
+
provided_token = authorization[7:]
|
| 942 |
+
else:
|
| 943 |
+
provided_token = authorization
|
| 944 |
+
|
| 945 |
+
# Check if token matches
|
| 946 |
+
if not plugin_config.connection_token or provided_token != plugin_config.connection_token:
|
| 947 |
+
raise HTTPException(status_code=401, detail="Invalid connection token")
|
| 948 |
+
|
| 949 |
+
# Extract session token from request
|
| 950 |
+
session_token = request.get("session_token")
|
| 951 |
+
|
| 952 |
+
if not session_token:
|
| 953 |
+
raise HTTPException(status_code=400, detail="Missing session_token")
|
| 954 |
+
|
| 955 |
+
# Step 1: Convert ST to AT to get user info (including email)
|
| 956 |
+
try:
|
| 957 |
+
result = await token_manager.flow_client.st_to_at(session_token)
|
| 958 |
+
at = result["access_token"]
|
| 959 |
+
expires = result.get("expires")
|
| 960 |
+
user_info = result.get("user", {})
|
| 961 |
+
email = user_info.get("email", "")
|
| 962 |
+
|
| 963 |
+
if not email:
|
| 964 |
+
raise HTTPException(status_code=400, detail="Failed to get email from session token")
|
| 965 |
+
|
| 966 |
+
# Parse expiration time
|
| 967 |
+
from datetime import datetime
|
| 968 |
+
at_expires = None
|
| 969 |
+
if expires:
|
| 970 |
+
try:
|
| 971 |
+
at_expires = datetime.fromisoformat(expires.replace('Z', '+00:00'))
|
| 972 |
+
except:
|
| 973 |
+
pass
|
| 974 |
+
|
| 975 |
+
except Exception as e:
|
| 976 |
+
raise HTTPException(status_code=400, detail=f"Invalid session token: {str(e)}")
|
| 977 |
+
|
| 978 |
+
# Step 2: Check if token with this email exists
|
| 979 |
+
existing_token = await db.get_token_by_email(email)
|
| 980 |
+
|
| 981 |
+
if existing_token:
|
| 982 |
+
# Update existing token
|
| 983 |
+
try:
|
| 984 |
+
# Update token
|
| 985 |
+
await token_manager.update_token(
|
| 986 |
+
token_id=existing_token.id,
|
| 987 |
+
st=session_token,
|
| 988 |
+
at=at,
|
| 989 |
+
at_expires=at_expires
|
| 990 |
+
)
|
| 991 |
+
|
| 992 |
+
return {
|
| 993 |
+
"success": True,
|
| 994 |
+
"message": f"Token updated for {email}",
|
| 995 |
+
"action": "updated"
|
| 996 |
+
}
|
| 997 |
+
except Exception as e:
|
| 998 |
+
raise HTTPException(status_code=500, detail=f"Failed to update token: {str(e)}")
|
| 999 |
+
else:
|
| 1000 |
+
# Add new token
|
| 1001 |
+
try:
|
| 1002 |
+
new_token = await token_manager.add_token(
|
| 1003 |
+
st=session_token,
|
| 1004 |
+
remark="Added by Chrome Extension"
|
| 1005 |
+
)
|
| 1006 |
+
|
| 1007 |
+
return {
|
| 1008 |
+
"success": True,
|
| 1009 |
+
"message": f"Token added for {new_token.email}",
|
| 1010 |
+
"action": "added",
|
| 1011 |
+
"token_id": new_token.id
|
| 1012 |
+
}
|
| 1013 |
+
except Exception as e:
|
| 1014 |
+
raise HTTPException(status_code=500, detail=f"Failed to add token: {str(e)}")
|
src/core/database.py
CHANGED
|
@@ -4,7 +4,7 @@ import json
|
|
| 4 |
from datetime import datetime
|
| 5 |
from typing import Optional, List
|
| 6 |
from pathlib import Path
|
| 7 |
-
from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, GenerationConfig, CacheConfig, Project, CaptchaConfig
|
| 8 |
|
| 9 |
|
| 10 |
class Database:
|
|
@@ -167,6 +167,15 @@ class Database:
|
|
| 167 |
VALUES (1, ?, ?, ?)
|
| 168 |
""", (captcha_method, yescaptcha_api_key, yescaptcha_base_url))
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
async def check_and_migrate_db(self, config_dict: dict = None):
|
| 171 |
"""Check database integrity and perform migrations if needed
|
| 172 |
|
|
@@ -216,6 +225,18 @@ class Database:
|
|
| 216 |
)
|
| 217 |
""")
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
# ========== Step 2: Add missing columns to existing tables ==========
|
| 220 |
# Check and add missing columns to tokens table
|
| 221 |
if await self._table_exists(db, "tokens"):
|
|
@@ -463,6 +484,16 @@ class Database:
|
|
| 463 |
)
|
| 464 |
""")
|
| 465 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
# Create indexes
|
| 467 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
|
| 468 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_token_st ON tokens(st)")
|
|
@@ -572,6 +603,16 @@ class Database:
|
|
| 572 |
return Token(**dict(row))
|
| 573 |
return None
|
| 574 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
async def get_all_tokens(self) -> List[Token]:
|
| 576 |
"""Get all tokens"""
|
| 577 |
async with aiosqlite.connect(self.db_path) as db:
|
|
@@ -1174,3 +1215,35 @@ class Database:
|
|
| 1174 |
""", (new_method, new_api_key, new_base_url, new_proxy_enabled, new_proxy_url))
|
| 1175 |
|
| 1176 |
await db.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
from datetime import datetime
|
| 5 |
from typing import Optional, List
|
| 6 |
from pathlib import Path
|
| 7 |
+
from .models import Token, TokenStats, Task, RequestLog, AdminConfig, ProxyConfig, GenerationConfig, CacheConfig, Project, CaptchaConfig, PluginConfig
|
| 8 |
|
| 9 |
|
| 10 |
class Database:
|
|
|
|
| 167 |
VALUES (1, ?, ?, ?)
|
| 168 |
""", (captcha_method, yescaptcha_api_key, yescaptcha_base_url))
|
| 169 |
|
| 170 |
+
# Ensure plugin_config has a row
|
| 171 |
+
cursor = await db.execute("SELECT COUNT(*) FROM plugin_config")
|
| 172 |
+
count = await cursor.fetchone()
|
| 173 |
+
if count[0] == 0:
|
| 174 |
+
await db.execute("""
|
| 175 |
+
INSERT INTO plugin_config (id, connection_token)
|
| 176 |
+
VALUES (1, '')
|
| 177 |
+
""")
|
| 178 |
+
|
| 179 |
async def check_and_migrate_db(self, config_dict: dict = None):
|
| 180 |
"""Check database integrity and perform migrations if needed
|
| 181 |
|
|
|
|
| 225 |
)
|
| 226 |
""")
|
| 227 |
|
| 228 |
+
# Check and create plugin_config table if missing
|
| 229 |
+
if not await self._table_exists(db, "plugin_config"):
|
| 230 |
+
print(" ✓ Creating missing table: plugin_config")
|
| 231 |
+
await db.execute("""
|
| 232 |
+
CREATE TABLE plugin_config (
|
| 233 |
+
id INTEGER PRIMARY KEY DEFAULT 1,
|
| 234 |
+
connection_token TEXT DEFAULT '',
|
| 235 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 236 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 237 |
+
)
|
| 238 |
+
""")
|
| 239 |
+
|
| 240 |
# ========== Step 2: Add missing columns to existing tables ==========
|
| 241 |
# Check and add missing columns to tokens table
|
| 242 |
if await self._table_exists(db, "tokens"):
|
|
|
|
| 484 |
)
|
| 485 |
""")
|
| 486 |
|
| 487 |
+
# Plugin config table
|
| 488 |
+
await db.execute("""
|
| 489 |
+
CREATE TABLE IF NOT EXISTS plugin_config (
|
| 490 |
+
id INTEGER PRIMARY KEY DEFAULT 1,
|
| 491 |
+
connection_token TEXT DEFAULT '',
|
| 492 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 493 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 494 |
+
)
|
| 495 |
+
""")
|
| 496 |
+
|
| 497 |
# Create indexes
|
| 498 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_task_id ON tasks(task_id)")
|
| 499 |
await db.execute("CREATE INDEX IF NOT EXISTS idx_token_st ON tokens(st)")
|
|
|
|
| 603 |
return Token(**dict(row))
|
| 604 |
return None
|
| 605 |
|
| 606 |
+
async def get_token_by_email(self, email: str) -> Optional[Token]:
|
| 607 |
+
"""Get token by email"""
|
| 608 |
+
async with aiosqlite.connect(self.db_path) as db:
|
| 609 |
+
db.row_factory = aiosqlite.Row
|
| 610 |
+
cursor = await db.execute("SELECT * FROM tokens WHERE email = ?", (email,))
|
| 611 |
+
row = await cursor.fetchone()
|
| 612 |
+
if row:
|
| 613 |
+
return Token(**dict(row))
|
| 614 |
+
return None
|
| 615 |
+
|
| 616 |
async def get_all_tokens(self) -> List[Token]:
|
| 617 |
"""Get all tokens"""
|
| 618 |
async with aiosqlite.connect(self.db_path) as db:
|
|
|
|
| 1215 |
""", (new_method, new_api_key, new_base_url, new_proxy_enabled, new_proxy_url))
|
| 1216 |
|
| 1217 |
await db.commit()
|
| 1218 |
+
|
| 1219 |
+
# Plugin config operations
|
| 1220 |
+
async def get_plugin_config(self) -> PluginConfig:
|
| 1221 |
+
"""Get plugin configuration"""
|
| 1222 |
+
async with aiosqlite.connect(self.db_path) as db:
|
| 1223 |
+
db.row_factory = aiosqlite.Row
|
| 1224 |
+
cursor = await db.execute("SELECT * FROM plugin_config WHERE id = 1")
|
| 1225 |
+
row = await cursor.fetchone()
|
| 1226 |
+
if row:
|
| 1227 |
+
return PluginConfig(**dict(row))
|
| 1228 |
+
return PluginConfig()
|
| 1229 |
+
|
| 1230 |
+
async def update_plugin_config(self, connection_token: str):
|
| 1231 |
+
"""Update plugin configuration"""
|
| 1232 |
+
async with aiosqlite.connect(self.db_path) as db:
|
| 1233 |
+
db.row_factory = aiosqlite.Row
|
| 1234 |
+
cursor = await db.execute("SELECT * FROM plugin_config WHERE id = 1")
|
| 1235 |
+
row = await cursor.fetchone()
|
| 1236 |
+
|
| 1237 |
+
if row:
|
| 1238 |
+
await db.execute("""
|
| 1239 |
+
UPDATE plugin_config
|
| 1240 |
+
SET connection_token = ?, updated_at = CURRENT_TIMESTAMP
|
| 1241 |
+
WHERE id = 1
|
| 1242 |
+
""", (connection_token,))
|
| 1243 |
+
else:
|
| 1244 |
+
await db.execute("""
|
| 1245 |
+
INSERT INTO plugin_config (id, connection_token)
|
| 1246 |
+
VALUES (1, ?)
|
| 1247 |
+
""", (connection_token,))
|
| 1248 |
+
|
| 1249 |
+
await db.commit()
|
src/core/models.py
CHANGED
|
@@ -158,6 +158,14 @@ class CaptchaConfig(BaseModel):
|
|
| 158 |
updated_at: Optional[datetime] = None
|
| 159 |
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
# OpenAI Compatible Request Models
|
| 162 |
class ChatMessage(BaseModel):
|
| 163 |
"""Chat message"""
|
|
|
|
| 158 |
updated_at: Optional[datetime] = None
|
| 159 |
|
| 160 |
|
| 161 |
+
class PluginConfig(BaseModel):
|
| 162 |
+
"""Plugin connection configuration"""
|
| 163 |
+
id: int = 1
|
| 164 |
+
connection_token: str = "" # 插件连接token
|
| 165 |
+
created_at: Optional[datetime] = None
|
| 166 |
+
updated_at: Optional[datetime] = None
|
| 167 |
+
|
| 168 |
+
|
| 169 |
# OpenAI Compatible Request Models
|
| 170 |
class ChatMessage(BaseModel):
|
| 171 |
"""Chat message"""
|
src/services/file_cache.py
CHANGED
|
@@ -131,28 +131,130 @@ class FileCache:
|
|
| 131 |
# Download file
|
| 132 |
debug_logger.log_info(f"Downloading file from: {url}")
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
try:
|
| 135 |
-
# Get proxy if available
|
| 136 |
-
proxy_url = None
|
| 137 |
-
if self.proxy_manager:
|
| 138 |
-
proxy_config = await self.proxy_manager.get_proxy_config()
|
| 139 |
-
if proxy_config and proxy_config.enabled and proxy_config.proxy_url:
|
| 140 |
-
proxy_url = proxy_config.proxy_url
|
| 141 |
-
|
| 142 |
-
# Download with proxy support
|
| 143 |
async with AsyncSession() as session:
|
| 144 |
proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
-
|
| 148 |
-
|
| 149 |
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
except Exception as e:
|
| 158 |
debug_logger.log_error(
|
|
|
|
| 131 |
# Download file
|
| 132 |
debug_logger.log_info(f"Downloading file from: {url}")
|
| 133 |
|
| 134 |
+
# Get proxy if available
|
| 135 |
+
proxy_url = None
|
| 136 |
+
if self.proxy_manager:
|
| 137 |
+
proxy_config = await self.proxy_manager.get_proxy_config()
|
| 138 |
+
if proxy_config and proxy_config.enabled and proxy_config.proxy_url:
|
| 139 |
+
proxy_url = proxy_config.proxy_url
|
| 140 |
+
|
| 141 |
+
# Try method 1: curl_cffi with browser impersonation
|
| 142 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
async with AsyncSession() as session:
|
| 144 |
proxies = {"http": proxy_url, "https": proxy_url} if proxy_url else None
|
| 145 |
+
headers = {
|
| 146 |
+
"Accept": "*/*",
|
| 147 |
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
| 148 |
+
"Accept-Encoding": "gzip, deflate, br",
|
| 149 |
+
"Connection": "keep-alive",
|
| 150 |
+
"Sec-Fetch-Dest": "document",
|
| 151 |
+
"Sec-Fetch-Mode": "navigate",
|
| 152 |
+
"Sec-Fetch-Site": "none",
|
| 153 |
+
"Upgrade-Insecure-Requests": "1"
|
| 154 |
+
}
|
| 155 |
+
response = await session.get(
|
| 156 |
+
url,
|
| 157 |
+
timeout=60,
|
| 158 |
+
proxies=proxies,
|
| 159 |
+
headers=headers,
|
| 160 |
+
impersonate="chrome120",
|
| 161 |
+
verify=False
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
if response.status_code == 200:
|
| 165 |
+
with open(file_path, 'wb') as f:
|
| 166 |
+
f.write(response.content)
|
| 167 |
+
debug_logger.log_info(f"File cached (curl_cffi): {filename} ({len(response.content)} bytes)")
|
| 168 |
+
return filename
|
| 169 |
+
else:
|
| 170 |
+
debug_logger.log_warning(f"curl_cffi failed with HTTP {response.status_code}, trying wget...")
|
| 171 |
|
| 172 |
+
except Exception as e:
|
| 173 |
+
debug_logger.log_warning(f"curl_cffi failed: {str(e)}, trying wget...")
|
| 174 |
|
| 175 |
+
# Try method 2: wget command
|
| 176 |
+
try:
|
| 177 |
+
import subprocess
|
| 178 |
|
| 179 |
+
wget_cmd = [
|
| 180 |
+
"wget",
|
| 181 |
+
"-q", # Quiet mode
|
| 182 |
+
"-O", str(file_path), # Output file
|
| 183 |
+
"--timeout=60",
|
| 184 |
+
"--tries=3",
|
| 185 |
+
"--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
| 186 |
+
"--header=Accept: */*",
|
| 187 |
+
"--header=Accept-Language: zh-CN,zh;q=0.9,en;q=0.8",
|
| 188 |
+
"--header=Connection: keep-alive"
|
| 189 |
+
]
|
| 190 |
+
|
| 191 |
+
# Add proxy if configured
|
| 192 |
+
if proxy_url:
|
| 193 |
+
# wget uses environment variables for proxy
|
| 194 |
+
env = os.environ.copy()
|
| 195 |
+
env['http_proxy'] = proxy_url
|
| 196 |
+
env['https_proxy'] = proxy_url
|
| 197 |
+
else:
|
| 198 |
+
env = None
|
| 199 |
+
|
| 200 |
+
# Add URL
|
| 201 |
+
wget_cmd.append(url)
|
| 202 |
+
|
| 203 |
+
# Execute wget
|
| 204 |
+
result = subprocess.run(wget_cmd, capture_output=True, timeout=90, env=env)
|
| 205 |
+
|
| 206 |
+
if result.returncode == 0 and file_path.exists():
|
| 207 |
+
file_size = file_path.stat().st_size
|
| 208 |
+
if file_size > 0:
|
| 209 |
+
debug_logger.log_info(f"File cached (wget): {filename} ({file_size} bytes)")
|
| 210 |
+
return filename
|
| 211 |
+
else:
|
| 212 |
+
raise Exception("Downloaded file is empty")
|
| 213 |
+
else:
|
| 214 |
+
error_msg = result.stderr.decode('utf-8', errors='ignore') if result.stderr else "Unknown error"
|
| 215 |
+
debug_logger.log_warning(f"wget failed: {error_msg}, trying curl...")
|
| 216 |
+
|
| 217 |
+
except FileNotFoundError:
|
| 218 |
+
debug_logger.log_warning("wget not found, trying curl...")
|
| 219 |
+
except Exception as e:
|
| 220 |
+
debug_logger.log_warning(f"wget failed: {str(e)}, trying curl...")
|
| 221 |
+
|
| 222 |
+
# Try method 3: system curl command
|
| 223 |
+
try:
|
| 224 |
+
import subprocess
|
| 225 |
+
|
| 226 |
+
curl_cmd = [
|
| 227 |
+
"curl",
|
| 228 |
+
"-L", # Follow redirects
|
| 229 |
+
"-s", # Silent mode
|
| 230 |
+
"-o", str(file_path), # Output file
|
| 231 |
+
"--max-time", "60",
|
| 232 |
+
"-H", "Accept: */*",
|
| 233 |
+
"-H", "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8",
|
| 234 |
+
"-H", "Connection: keep-alive",
|
| 235 |
+
"-A", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
| 236 |
+
]
|
| 237 |
+
|
| 238 |
+
# Add proxy if configured
|
| 239 |
+
if proxy_url:
|
| 240 |
+
curl_cmd.extend(["-x", proxy_url])
|
| 241 |
+
|
| 242 |
+
# Add URL
|
| 243 |
+
curl_cmd.append(url)
|
| 244 |
+
|
| 245 |
+
# Execute curl
|
| 246 |
+
result = subprocess.run(curl_cmd, capture_output=True, timeout=90)
|
| 247 |
+
|
| 248 |
+
if result.returncode == 0 and file_path.exists():
|
| 249 |
+
file_size = file_path.stat().st_size
|
| 250 |
+
if file_size > 0:
|
| 251 |
+
debug_logger.log_info(f"File cached (curl): {filename} ({file_size} bytes)")
|
| 252 |
+
return filename
|
| 253 |
+
else:
|
| 254 |
+
raise Exception("Downloaded file is empty")
|
| 255 |
+
else:
|
| 256 |
+
error_msg = result.stderr.decode('utf-8', errors='ignore') if result.stderr else "Unknown error"
|
| 257 |
+
raise Exception(f"curl command failed: {error_msg}")
|
| 258 |
|
| 259 |
except Exception as e:
|
| 260 |
debug_logger.log_error(
|
static/manage.html
CHANGED
|
@@ -319,6 +319,35 @@
|
|
| 319 |
</div>
|
| 320 |
</div>
|
| 321 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
<!-- 生成超时配置 -->
|
| 323 |
<div class="rounded-lg border border-border bg-background p-6">
|
| 324 |
<h3 class="text-lg font-semibold mb-4">生成超时配置</h3>
|
|
@@ -637,13 +666,17 @@
|
|
| 637 |
toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
|
| 638 |
loadCaptchaConfig=async()=>{try{console.log('开始加载验证码配置...');const r=await apiRequest('/api/captcha/config');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('验证码配置数据:',d);$('cfgCaptchaMethod').value=d.captcha_method||'yescaptcha';$('cfgYescaptchaApiKey').value=d.yescaptcha_api_key||'';$('cfgYescaptchaBaseUrl').value=d.yescaptcha_base_url||'https://api.yescaptcha.com';$('cfgBrowserProxyEnabled').checked=d.browser_proxy_enabled||false;$('cfgBrowserProxyUrl').value=d.browser_proxy_url||'';toggleCaptchaOptions();toggleBrowserProxyInput();console.log('验证码配置加载成功')}catch(e){console.error('加载验证码配置失败:',e);showToast('加载验证码配置失败: '+e.message,'error')}},
|
| 639 |
saveCaptchaConfig=async()=>{const method=$('cfgCaptchaMethod').value,apiKey=$('cfgYescaptchaApiKey').value.trim(),baseUrl=$('cfgYescaptchaBaseUrl').value.trim(),browserProxyEnabled=$('cfgBrowserProxyEnabled').checked,browserProxyUrl=$('cfgBrowserProxyUrl').value.trim();console.log('保存验证码配置:',{method,apiKey,baseUrl,browserProxyEnabled,browserProxyUrl});try{const r=await apiRequest('/api/captcha/config',{method:'POST',body:JSON.stringify({captcha_method:method,yescaptcha_api_key:apiKey,yescaptcha_base_url:baseUrl,browser_proxy_enabled:browserProxyEnabled,browser_proxy_url:browserProxyUrl})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('验证码配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadCaptchaConfig()}else{console.error('保存失败:',d);showToast(d.message||'保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
|
| 641 |
loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
|
| 642 |
loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},
|
| 643 |
refreshLogs=async()=>{await loadLogs()},
|
| 644 |
showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)},
|
| 645 |
logout=()=>{if(!confirm('确定要退出登录吗?'))return;localStorage.removeItem('adminToken');location.href='/login'},
|
| 646 |
-
switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings','logs'].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();loadCacheConfig();loadGenerationTimeout();loadCaptchaConfig();loadATAutoRefreshConfig()}else if(t==='logs'){loadLogs()}};
|
| 647 |
window.addEventListener('DOMContentLoaded',()=>{checkAuth();refreshTokens();loadATAutoRefreshConfig()});
|
| 648 |
</script>
|
| 649 |
</body>
|
|
|
|
| 319 |
</div>
|
| 320 |
</div>
|
| 321 |
|
| 322 |
+
<!-- 插件连接配置 -->
|
| 323 |
+
<div class="rounded-lg border border-border bg-background p-6">
|
| 324 |
+
<h3 class="text-lg font-semibold mb-4">插件连接配置</h3>
|
| 325 |
+
<div class="space-y-4">
|
| 326 |
+
<div>
|
| 327 |
+
<label class="text-sm font-semibold mb-2 block">连接接口</label>
|
| 328 |
+
<div class="flex gap-2">
|
| 329 |
+
<input id="cfgPluginConnectionUrl" type="text" readonly class="flex h-9 flex-1 rounded-md border border-input bg-muted px-3 py-2 text-sm" placeholder="加载中...">
|
| 330 |
+
<button onclick="copyConnectionUrl()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
|
| 331 |
+
</div>
|
| 332 |
+
<p class="text-xs text-muted-foreground mt-1">Chrome扩展插件需要配置此接口地址</p>
|
| 333 |
+
</div>
|
| 334 |
+
<div>
|
| 335 |
+
<label class="text-sm font-semibold mb-2 block">连接Token</label>
|
| 336 |
+
<div class="flex gap-2">
|
| 337 |
+
<input id="cfgPluginConnectionToken" type="text" class="flex h-9 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm" placeholder="留空自动生成">
|
| 338 |
+
<button onclick="copyConnectionToken()" class="inline-flex items-center justify-center rounded-md bg-secondary text-secondary-foreground hover:bg-secondary/80 h-9 px-4">复制</button>
|
| 339 |
+
</div>
|
| 340 |
+
<p class="text-xs text-muted-foreground mt-1">用于验证Chrome扩展插件的身份,留空将自动生成随机token</p>
|
| 341 |
+
</div>
|
| 342 |
+
<div class="rounded-md bg-blue-50 dark:bg-blue-900/20 p-3 border border-blue-200 dark:border-blue-800">
|
| 343 |
+
<p class="text-xs text-blue-800 dark:text-blue-200">
|
| 344 |
+
ℹ️ <strong>使用说明:</strong>安装Chrome扩展后,将连接接口和Token配置到插件中,插件会自动提取Google Labs的cookie并更新到系统
|
| 345 |
+
</p>
|
| 346 |
+
</div>
|
| 347 |
+
<button onclick="savePluginConfig()" 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>
|
| 348 |
+
</div>
|
| 349 |
+
</div>
|
| 350 |
+
|
| 351 |
<!-- 生成超时配置 -->
|
| 352 |
<div class="rounded-lg border border-border bg-background p-6">
|
| 353 |
<h3 class="text-lg font-semibold mb-4">生成超时配置</h3>
|
|
|
|
| 666 |
toggleBrowserProxyInput=()=>{const enabled=$('cfgBrowserProxyEnabled').checked;$('browserProxyUrlInput').classList.toggle('hidden',!enabled)},
|
| 667 |
loadCaptchaConfig=async()=>{try{console.log('开始加载验证码配置...');const r=await apiRequest('/api/captcha/config');if(!r){console.error('API请求失败');return}const d=await r.json();console.log('验证码配置数据:',d);$('cfgCaptchaMethod').value=d.captcha_method||'yescaptcha';$('cfgYescaptchaApiKey').value=d.yescaptcha_api_key||'';$('cfgYescaptchaBaseUrl').value=d.yescaptcha_base_url||'https://api.yescaptcha.com';$('cfgBrowserProxyEnabled').checked=d.browser_proxy_enabled||false;$('cfgBrowserProxyUrl').value=d.browser_proxy_url||'';toggleCaptchaOptions();toggleBrowserProxyInput();console.log('验证码配置加载成功')}catch(e){console.error('加载验证码配置失败:',e);showToast('加载验证码配置失败: '+e.message,'error')}},
|
| 668 |
saveCaptchaConfig=async()=>{const method=$('cfgCaptchaMethod').value,apiKey=$('cfgYescaptchaApiKey').value.trim(),baseUrl=$('cfgYescaptchaBaseUrl').value.trim(),browserProxyEnabled=$('cfgBrowserProxyEnabled').checked,browserProxyUrl=$('cfgBrowserProxyUrl').value.trim();console.log('保存验证码配置:',{method,apiKey,baseUrl,browserProxyEnabled,browserProxyUrl});try{const r=await apiRequest('/api/captcha/config',{method:'POST',body:JSON.stringify({captcha_method:method,yescaptcha_api_key:apiKey,yescaptcha_base_url:baseUrl,browser_proxy_enabled:browserProxyEnabled,browser_proxy_url:browserProxyUrl})});if(!r){console.error('保存请求失败');return}const d=await r.json();console.log('保存结果:',d);if(d.success){showToast('验证码配置保存成功','success');await new Promise(r=>setTimeout(r,200));await loadCaptchaConfig()}else{console.error('保存失败:',d);showToast(d.message||'保存失败','error')}}catch(e){console.error('保存失败:',e);showToast('保存失败: '+e.message,'error')}},
|
| 669 |
+
loadPluginConfig=async()=>{try{const r=await apiRequest('/api/plugin/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('cfgPluginConnectionUrl').value=d.config.connection_url||'';$('cfgPluginConnectionToken').value=d.config.connection_token||''}}catch(e){console.error('加载插件配置失败:',e);showToast('加载插件配置失败: '+e.message,'error')}},
|
| 670 |
+
savePluginConfig=async()=>{const token=$('cfgPluginConnectionToken').value.trim();try{const r=await apiRequest('/api/plugin/config',{method:'POST',body:JSON.stringify({connection_token:token})});if(!r)return;const d=await r.json();if(d.success){showToast('插件配置保存成功','success');await loadPluginConfig()}else{showToast(d.message||'保存失败','error')}}catch(e){showToast('保存失败: '+e.message,'error')}},
|
| 671 |
+
copyConnectionUrl=()=>{const url=$('cfgPluginConnectionUrl').value;if(!url){showToast('连接接口为空','error');return}navigator.clipboard.writeText(url).then(()=>showToast('连接接口已复制','success')).catch(()=>showToast('复制失败','error'))},
|
| 672 |
+
copyConnectionToken=()=>{const token=$('cfgPluginConnectionToken').value;if(!token){showToast('连接Token为空','error');return}navigator.clipboard.writeText(token).then(()=>showToast('连接Token已复制','success')).catch(()=>showToast('复制失败','error'))},
|
| 673 |
toggleATAutoRefresh=async()=>{try{const enabled=$('atAutoRefreshToggle').checked;const r=await apiRequest('/api/token-refresh/enabled',{method:'POST',body:JSON.stringify({enabled:enabled})});if(!r){$('atAutoRefreshToggle').checked=!enabled;return}const d=await r.json();if(d.success){showToast(enabled?'AT自动刷新已启用':'AT自动刷新已禁用','success')}else{showToast('操作失败: '+(d.detail||'未知错误'),'error');$('atAutoRefreshToggle').checked=!enabled}}catch(e){showToast('操作失败: '+e.message,'error');$('atAutoRefreshToggle').checked=!enabled}},
|
| 674 |
loadATAutoRefreshConfig=async()=>{try{const r=await apiRequest('/api/token-refresh/config');if(!r)return;const d=await r.json();if(d.success&&d.config){$('atAutoRefreshToggle').checked=d.config.at_auto_refresh_enabled||false}else{console.error('AT自动刷新配置数据格式错误:',d)}}catch(e){console.error('加载AT自动刷新配置失败:',e)}},
|
| 675 |
loadLogs=async()=>{try{const r=await apiRequest('/api/logs?limit=100');if(!r)return;const logs=await r.json();const tb=$('logsTableBody');tb.innerHTML=logs.map(l=>`<tr><td class="py-2.5 px-3">${l.operation}</td><td class="py-2.5 px-3"><span class="text-xs ${l.token_email?'text-blue-600':'text-muted-foreground'}">${l.token_email||'未知'}</span></td><td class="py-2.5 px-3"><span class="inline-flex items-center rounded px-2 py-0.5 text-xs ${l.status_code===200?'bg-green-50 text-green-700':'bg-red-50 text-red-700'}">${l.status_code}</span></td><td class="py-2.5 px-3">${l.duration.toFixed(2)}</td><td class="py-2.5 px-3 text-xs text-muted-foreground">${l.created_at?new Date(l.created_at).toLocaleString('zh-CN'):'-'}</td></tr>`).join('')}catch(e){console.error('加载日志失败:',e)}},
|
| 676 |
refreshLogs=async()=>{await loadLogs()},
|
| 677 |
showToast=(m,t='info')=>{const d=document.createElement('div'),bc={success:'bg-green-600',error:'bg-destructive',info:'bg-primary'};d.className=`fixed bottom-4 right-4 ${bc[t]||bc.info} text-white px-4 py-2.5 rounded-lg shadow-lg text-sm font-medium z-50 animate-slide-up`;d.textContent=m;document.body.appendChild(d);setTimeout(()=>{d.style.opacity='0';d.style.transition='opacity .3s';setTimeout(()=>d.parentNode&&document.body.removeChild(d),300)},2000)},
|
| 678 |
logout=()=>{if(!confirm('确定要退出登录吗?'))return;localStorage.removeItem('adminToken');location.href='/login'},
|
| 679 |
+
switchTab=t=>{const cap=n=>n.charAt(0).toUpperCase()+n.slice(1);['tokens','settings','logs'].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();loadCacheConfig();loadGenerationTimeout();loadCaptchaConfig();loadPluginConfig();loadATAutoRefreshConfig()}else if(t==='logs'){loadLogs()}};
|
| 680 |
window.addEventListener('DOMContentLoaded',()=>{checkAuth();refreshTokens();loadATAutoRefreshConfig()});
|
| 681 |
</script>
|
| 682 |
</body>
|