mirror of
https://github.com/daimyomizukagebay61/SteamPanel.git
synced 2026-07-25 03:34:30 +00:00
v3.0.6
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# SteamPanel configuration
|
||||
# Copy this file to .env and edit as needed.
|
||||
# All settings are optional — defaults are shown below.
|
||||
|
||||
# Log level: DEBUG shows detailed internal logs (browser, DB, tasks)
|
||||
# INFO is the default (clean output)
|
||||
STEAM_PANEL_DEBUG=false
|
||||
|
||||
# Server bind address and port
|
||||
STEAM_PANEL_HOST=127.0.0.1
|
||||
STEAM_PANEL_PORT=8000
|
||||
|
||||
# Per-account HTTP timeout in milliseconds
|
||||
STEAM_PANEL_REQUEST_TIMEOUT=30000
|
||||
@@ -1,3 +1,6 @@
|
||||
# Environment / local config
|
||||
.env
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
+18
-7
@@ -3,9 +3,9 @@ import asyncio
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from loguru import logger
|
||||
|
||||
from app.database import get_db
|
||||
from app.config import read_validation_settings
|
||||
from app.core.task_manager import task_manager
|
||||
from app.database import get_db
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["browser"])
|
||||
|
||||
@@ -20,8 +20,7 @@ async def open_browser(account_id: int):
|
||||
|
||||
account = dict(row)
|
||||
|
||||
from app.services.steam_auth import check_cookies_alive, _resolve_proxy
|
||||
from app.config import read_validation_settings
|
||||
from app.services.steam_auth import _resolve_proxy, check_cookies_alive
|
||||
|
||||
# If account isn't validated or has no cookies — auto-revalidate if enabled
|
||||
if account["status"] != "valid" or not account.get("session_cookies"):
|
||||
@@ -32,8 +31,14 @@ async def open_browser(account_id: int):
|
||||
accounts=[account],
|
||||
params={},
|
||||
)
|
||||
logger.info(f"Account {account['login']} not valid/no cookies, auto-revalidating → task {task_id}")
|
||||
return {"status": "revalidating", "message": "Cookies expired. Re-validating...", "task_id": task_id}
|
||||
logger.info(
|
||||
f"Account {account['login']} not valid/no cookies, auto-revalidating → task {task_id}"
|
||||
)
|
||||
return {
|
||||
"status": "revalidating",
|
||||
"message": "Cookies expired. Re-validating...",
|
||||
"task_id": task_id,
|
||||
}
|
||||
if account["status"] != "valid":
|
||||
raise HTTPException(400, "Account is not validated")
|
||||
raise HTTPException(400, "No session cookies. Validate the account first.")
|
||||
@@ -48,8 +53,14 @@ async def open_browser(account_id: int):
|
||||
accounts=[account],
|
||||
params={},
|
||||
)
|
||||
logger.info(f"Cookies dead for {account['login']}, auto-revalidating → task {task_id}")
|
||||
return {"status": "revalidating", "message": "Cookies expired. Re-validating...", "task_id": task_id}
|
||||
logger.info(
|
||||
f"Cookies dead for {account['login']}, auto-revalidating → task {task_id}"
|
||||
)
|
||||
return {
|
||||
"status": "revalidating",
|
||||
"message": "Cookies expired. Re-validating...",
|
||||
"task_id": task_id,
|
||||
}
|
||||
raise HTTPException(400, "Session cookies expired. Re-validate the account.")
|
||||
|
||||
from app.services.browser_login import open_browser_with_cookies
|
||||
|
||||
+7
-2
@@ -17,10 +17,15 @@ class Settings(BaseSettings):
|
||||
proxies_path: Path = data_dir / "proxies.json"
|
||||
|
||||
max_concurrent_tasks: int = 5
|
||||
request_timeout: int = 30
|
||||
request_timeout: int = 30000 # milliseconds
|
||||
proxy_rotation_interval: int = 60
|
||||
|
||||
model_config = {"env_prefix": "STEAM_PANEL_"}
|
||||
model_config = {
|
||||
"env_prefix": "STEAM_PANEL_",
|
||||
"env_file": Path(__file__).resolve().parent.parent / ".env",
|
||||
"env_file_encoding": "utf-8",
|
||||
"extra": "ignore",
|
||||
}
|
||||
|
||||
|
||||
settings = Settings()
|
||||
|
||||
+18
-18
@@ -6,30 +6,29 @@ from loguru import logger
|
||||
|
||||
from app.config import settings
|
||||
|
||||
|
||||
LEVEL_ICONS = {
|
||||
"TRACE": "[~]",
|
||||
"DEBUG": "[.]",
|
||||
"INFO": "[i]",
|
||||
"TRACE": "[~]",
|
||||
"DEBUG": "[.]",
|
||||
"INFO": "[i]",
|
||||
"SUCCESS": "[+]",
|
||||
"WARNING": "[!]",
|
||||
"ERROR": "[e]",
|
||||
"CRITICAL":"[e]",
|
||||
"ERROR": "[e]",
|
||||
"CRITICAL": "[e]",
|
||||
}
|
||||
|
||||
LEVEL_COLORS = {
|
||||
"TRACE": "\033[35m",
|
||||
"DEBUG": "\033[37m",
|
||||
"INFO": "\033[34m",
|
||||
"TRACE": "\033[35m",
|
||||
"DEBUG": "\033[37m",
|
||||
"INFO": "\033[34m",
|
||||
"SUCCESS": "\033[32m",
|
||||
"WARNING": "\033[33m",
|
||||
"ERROR": "\033[31m",
|
||||
"CRITICAL":"\033[31m",
|
||||
"ERROR": "\033[31m",
|
||||
"CRITICAL": "\033[31m",
|
||||
}
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
def _loguru_fmt(record: dict) -> str:
|
||||
def _loguru_fmt(record) -> str: # type: ignore[override]
|
||||
level = record["level"].name
|
||||
icon = LEVEL_ICONS.get(level, "[i]")
|
||||
color = LEVEL_COLORS.get(level, "")
|
||||
@@ -52,7 +51,9 @@ class _UvicornInterceptHandler(logging.Handler):
|
||||
frame = frame.f_back
|
||||
depth += 1
|
||||
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
|
||||
logger.opt(depth=depth, exception=record.exc_info).log(
|
||||
level, record.getMessage()
|
||||
)
|
||||
|
||||
|
||||
_logging_configured = False
|
||||
@@ -75,11 +76,13 @@ def setup_logging() -> None:
|
||||
|
||||
settings.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _file_fmt(record: dict) -> str:
|
||||
def _file_fmt(record) -> str: # type: ignore[override]
|
||||
level = record["level"].name
|
||||
icon = LEVEL_ICONS.get(level, "[i]")
|
||||
ts = record["time"].strftime("%Y-%m-%d %H:%M:%S")
|
||||
msg = record["message"].replace("<", "\\<").replace("{", "{{").replace("}", "}}")
|
||||
msg = (
|
||||
record["message"].replace("<", "\\<").replace("{", "{{").replace("}", "}}")
|
||||
)
|
||||
return f"[{ts}] {icon} {msg}\n"
|
||||
|
||||
logger.add(
|
||||
@@ -133,6 +136,3 @@ def _install_intercept() -> None:
|
||||
aiosqlite_log.addFilter(_AioSQLiteFilter())
|
||||
|
||||
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
|
||||
|
||||
+211
-42
@@ -1,5 +1,8 @@
|
||||
import asyncio
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -13,6 +16,93 @@ STEAM_DOMAINS = [
|
||||
".login.steampowered.com",
|
||||
]
|
||||
|
||||
# Common Chrome/Chromium install locations on Windows
|
||||
_WINDOWS_CHROME_PATHS = [
|
||||
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
||||
r"C:\Program Files\Google\Chrome Beta\Application\chrome.exe",
|
||||
r"C:\Program Files\Google\Chrome Dev\Application\chrome.exe",
|
||||
r"C:\Program Files\Chromium\Application\chrome.exe",
|
||||
r"C:\Program Files (x86)\Chromium\Application\chrome.exe",
|
||||
]
|
||||
|
||||
|
||||
def _find_chrome() -> str | None:
|
||||
"""Try to locate a Chrome/Chromium binary. Returns path or None."""
|
||||
# 1. Check PATH
|
||||
for name in ("chrome", "chromium", "chromium-browser", "google-chrome"):
|
||||
found = shutil.which(name)
|
||||
if found:
|
||||
return found
|
||||
|
||||
# 2. Windows-specific fixed paths + Registry
|
||||
if sys.platform == "win32":
|
||||
import os
|
||||
|
||||
local = os.environ.get("LOCALAPPDATA", "")
|
||||
user_path = Path(local) / "Google" / "Chrome" / "Application" / "chrome.exe"
|
||||
candidates = _WINDOWS_CHROME_PATHS + ([str(user_path)] if local else [])
|
||||
for p in candidates:
|
||||
if Path(p).exists():
|
||||
return p
|
||||
|
||||
# Registry lookup (handles non-standard install paths)
|
||||
try:
|
||||
import winreg
|
||||
|
||||
reg_keys = [
|
||||
(
|
||||
winreg.HKEY_LOCAL_MACHINE,
|
||||
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe",
|
||||
),
|
||||
(
|
||||
winreg.HKEY_CURRENT_USER,
|
||||
r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe",
|
||||
),
|
||||
]
|
||||
for hive, key_path in reg_keys:
|
||||
try:
|
||||
with winreg.OpenKey(hive, key_path) as key:
|
||||
path, _ = winreg.QueryValueEx(key, "")
|
||||
if path and Path(path).exists():
|
||||
return path
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def _wait_connection_closed(browser) -> None:
|
||||
"""Fallback: wait until CDP connection closes (with grace period)."""
|
||||
await asyncio.sleep(3)
|
||||
consecutive_closed = 0
|
||||
tick = 0
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
tick += 1
|
||||
try:
|
||||
closed = not browser or not browser.connection or browser.connection.closed
|
||||
except Exception as e:
|
||||
logger.debug(f"[browser] connection check error at tick={tick}: {e}")
|
||||
closed = True
|
||||
|
||||
if tick % 5 == 0:
|
||||
logger.debug(
|
||||
f"[browser] fallback tick={tick} closed={closed} consecutive={consecutive_closed}"
|
||||
)
|
||||
|
||||
if closed:
|
||||
consecutive_closed += 1
|
||||
logger.debug(
|
||||
f"[browser] connection closed tick={tick}, consecutive={consecutive_closed}"
|
||||
)
|
||||
if consecutive_closed >= 3:
|
||||
break
|
||||
else:
|
||||
consecutive_closed = 0
|
||||
|
||||
|
||||
async def open_browser_with_cookies(account: dict) -> None:
|
||||
"""Open a Chrome browser with Steam session cookies using nodriver."""
|
||||
@@ -24,55 +114,134 @@ async def open_browser_with_cookies(account: dict) -> None:
|
||||
|
||||
cookies = json.loads(cookies_json)
|
||||
|
||||
browser = await uc.start()
|
||||
# Try to find Chrome ourselves so we can give a better error
|
||||
chrome_path = _find_chrome()
|
||||
if chrome_path:
|
||||
logger.debug(f"[browser] Using Chrome at: {chrome_path}")
|
||||
else:
|
||||
logger.warning(
|
||||
"[browser] Chrome not found in standard locations — "
|
||||
"nodriver will attempt its own detection"
|
||||
)
|
||||
|
||||
try:
|
||||
if chrome_path:
|
||||
browser = await uc.start(browser_executable_path=chrome_path)
|
||||
else:
|
||||
browser = await uc.start()
|
||||
except Exception as exc:
|
||||
exc_str = str(exc).lower()
|
||||
if (
|
||||
"chrome" in exc_str
|
||||
or "binary" in exc_str
|
||||
or "executable" in exc_str
|
||||
or "not found" in exc_str
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Chrome не найден. Установите Google Chrome с https://www.google.com/chrome/ "
|
||||
f"(искали в: {chrome_path or 'стандартные пути'})"
|
||||
) from exc
|
||||
raise RuntimeError(f"Не удалось запустить браузер: {exc}") from exc
|
||||
|
||||
_active_browsers.append(browser)
|
||||
|
||||
tab = await browser.get("about:blank")
|
||||
|
||||
for c in cookies:
|
||||
domain = c.get("domain") or ".steamcommunity.com"
|
||||
domains_to_set = [domain]
|
||||
if "steam" in domain:
|
||||
domains_to_set = list(set([domain] + STEAM_DOMAINS))
|
||||
|
||||
for d in domains_to_set:
|
||||
try:
|
||||
await tab.send(uc.cdp.network.set_cookie(
|
||||
name=c["name"],
|
||||
value=c["value"],
|
||||
domain=d,
|
||||
path=c.get("path") or "/",
|
||||
secure=c.get("secure", True),
|
||||
http_only=c.get("httpOnly", False),
|
||||
))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
steam_id = account.get("steam_id") or ""
|
||||
if steam_id:
|
||||
start_url = f"https://steamcommunity.com/profiles/{steam_id}/"
|
||||
else:
|
||||
start_url = "https://steamcommunity.com/"
|
||||
await browser.get(start_url)
|
||||
logger.info(f"Browser opened for {account['login']}")
|
||||
|
||||
# Wait until browser is closed by user, suppress connection errors
|
||||
try:
|
||||
while browser:
|
||||
await asyncio.sleep(1)
|
||||
tab = await browser.get("about:blank")
|
||||
|
||||
for c in cookies:
|
||||
domain = c.get("domain") or ".steamcommunity.com"
|
||||
domains_to_set = [domain]
|
||||
if "steam" in domain:
|
||||
domains_to_set = list(set([domain] + STEAM_DOMAINS))
|
||||
|
||||
for d in domains_to_set:
|
||||
try:
|
||||
await tab.send(
|
||||
uc.cdp.network.set_cookie(
|
||||
name=c["name"],
|
||||
value=c["value"],
|
||||
domain=d,
|
||||
path=c.get("path") or "/",
|
||||
secure=c.get("secure", True),
|
||||
http_only=c.get("httpOnly", False),
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
steam_id = account.get("steam_id") or ""
|
||||
start_url = (
|
||||
f"https://steamcommunity.com/profiles/{steam_id}/"
|
||||
if steam_id
|
||||
else "https://steamcommunity.com/"
|
||||
)
|
||||
await browser.get(start_url)
|
||||
logger.info(f"[browser] Opened for {account.get('login', account.get('id'))}")
|
||||
|
||||
# Wait until the Chrome process exits.
|
||||
# We check the process PID directly — this is immune to CDP
|
||||
# connection state fluctuations during page navigation.
|
||||
await asyncio.sleep(2) # let Chrome fully start
|
||||
|
||||
pid = getattr(browser, "_process_pid", None) or getattr(
|
||||
getattr(browser, "_process", None), "pid", None
|
||||
)
|
||||
logger.debug(f"[browser] monitoring Chrome: pid={pid}")
|
||||
|
||||
if pid:
|
||||
try:
|
||||
if not browser.connection or browser.connection.closed:
|
||||
break
|
||||
except Exception:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
import psutil # type: ignore
|
||||
|
||||
proc = psutil.Process(pid)
|
||||
logger.debug(
|
||||
f"[browser] Chrome pid={pid} status={proc.status()} — waiting for user to close"
|
||||
)
|
||||
tick = 0
|
||||
while True:
|
||||
await asyncio.sleep(1)
|
||||
tick += 1
|
||||
try:
|
||||
running = proc.is_running()
|
||||
status = proc.status()
|
||||
except psutil.NoSuchProcess:
|
||||
logger.debug(
|
||||
f"[browser] Chrome pid={pid} — process no longer exists (tick={tick})"
|
||||
)
|
||||
break
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
f"[browser] Chrome pid={pid} — psutil error (tick={tick}): {e}"
|
||||
)
|
||||
break
|
||||
if tick % 5 == 0:
|
||||
conn_closed = getattr(
|
||||
getattr(browser, "connection", None), "closed", "?"
|
||||
)
|
||||
logger.debug(
|
||||
f"[browser] Chrome pid={pid} alive tick={tick} status={status} conn.closed={conn_closed}"
|
||||
)
|
||||
if not running or status == psutil.STATUS_ZOMBIE:
|
||||
logger.debug(
|
||||
f"[browser] Chrome pid={pid} exited at tick={tick}, final status={status}"
|
||||
)
|
||||
break
|
||||
except ImportError:
|
||||
logger.debug(
|
||||
"[browser] psutil not installed — falling back to connection check"
|
||||
)
|
||||
await _wait_connection_closed(browser)
|
||||
except Exception as e:
|
||||
logger.debug(f"[browser] pid monitoring error: {e}")
|
||||
else:
|
||||
logger.debug(
|
||||
"[browser] could not get Chrome PID — falling back to connection check"
|
||||
)
|
||||
await _wait_connection_closed(browser)
|
||||
|
||||
finally:
|
||||
if browser in _active_browsers:
|
||||
_active_browsers.remove(browser)
|
||||
|
||||
# Temporarily suppress nodriver's internal task errors during cleanup
|
||||
# (Browser.update_targets() raises ConnectionRefusedError after close)
|
||||
loop = asyncio.get_running_loop()
|
||||
_original_handler = loop.get_exception_handler()
|
||||
|
||||
@@ -93,4 +262,4 @@ async def open_browser_with_cookies(account: dict) -> None:
|
||||
await asyncio.sleep(0.5)
|
||||
loop.set_exception_handler(_original_handler)
|
||||
|
||||
logger.debug(f"Browser closed for {account['login']}")
|
||||
logger.debug(f"[browser] Closed for {account.get('login', account.get('id'))}")
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SteamPanel</title>
|
||||
<script type="module" crossorigin src="/assets/index-_9agmVKx.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-ztmqkldd.css">
|
||||
<script type="module" crossorigin src="/assets/index-sYKK8_qa.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-gklBug3W.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -23,9 +23,9 @@ function Row({
|
||||
}) {
|
||||
return (
|
||||
<div className="flex text-[11px] font-mono leading-[1.7]">
|
||||
<span className="w-[110px] shrink-0 text-gray-500">{label}</span>
|
||||
<span className="w-27.5 shrink-0 text-gray-500">{label}</span>
|
||||
<span
|
||||
className={`min-w-0 break-words ${
|
||||
className={`min-w-0 wrap-break-word ${
|
||||
good ? "text-green-400" : bad ? "text-red-400" : "text-gray-200"
|
||||
}`}
|
||||
>
|
||||
@@ -116,7 +116,7 @@ export function TokenCheckModal({ account, onClose, onRecheck }: Props) {
|
||||
className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
|
||||
onClick={handleBackdrop}
|
||||
>
|
||||
<div className="bg-dark-800 border border-dark-600 rounded-xl w-full max-w-[640px] max-h-[85vh] flex flex-col shadow-2xl mx-4">
|
||||
<div className="bg-dark-800 border border-dark-600 rounded-xl w-full max-w-160 max-h-[85vh] flex flex-col shadow-2xl mx-4">
|
||||
{/* Header */}
|
||||
<div className="px-5 pt-4 pb-3 border-b border-dark-600 flex items-start justify-between shrink-0">
|
||||
<div className="flex items-start gap-3 min-w-0">
|
||||
@@ -274,22 +274,41 @@ export function TokenCheckModal({ account, onClose, onRecheck }: Props) {
|
||||
|
||||
{/* Inventory */}
|
||||
<Card title="Inventory">
|
||||
<Row
|
||||
label="CS2"
|
||||
value={`${data.inventory_cs2} (${data.inventory_cs2_marketable})`}
|
||||
/>
|
||||
<Row
|
||||
label="Dota 2"
|
||||
value={`${data.inventory_dota2} (${data.inventory_dota2_marketable})`}
|
||||
/>
|
||||
<Row
|
||||
label="TF2"
|
||||
value={`${data.inventory_tf2} (${data.inventory_tf2_marketable})`}
|
||||
/>
|
||||
<Row
|
||||
label="Rust"
|
||||
value={`${data.inventory_rust} (${data.inventory_rust_marketable})`}
|
||||
/>
|
||||
{[
|
||||
{
|
||||
label: "CS2",
|
||||
total: data.inventory_cs2,
|
||||
market: data.inventory_cs2_marketable,
|
||||
},
|
||||
{
|
||||
label: "Dota 2",
|
||||
total: data.inventory_dota2,
|
||||
market: data.inventory_dota2_marketable,
|
||||
},
|
||||
{
|
||||
label: "TF2",
|
||||
total: data.inventory_tf2,
|
||||
market: data.inventory_tf2_marketable,
|
||||
},
|
||||
{
|
||||
label: "Rust",
|
||||
total: data.inventory_rust,
|
||||
market: data.inventory_rust_marketable,
|
||||
},
|
||||
].map(({ label, total, market }) => (
|
||||
<div
|
||||
key={label}
|
||||
className="flex text-[11px] font-mono leading-[1.7]"
|
||||
>
|
||||
<span className="w-27.5 shrink-0 text-gray-500">
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-gray-200">
|
||||
{total}
|
||||
<span className="text-gray-600"> ({market})</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -53,6 +53,7 @@ function ThreadStepper({
|
||||
value: number;
|
||||
onChange: (v: number) => void;
|
||||
}) {
|
||||
const t = useT();
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -72,7 +73,7 @@ function ThreadStepper({
|
||||
className="text-sm text-gray-400 flex-1"
|
||||
title="Двойной клик на числе для ручного ввода"
|
||||
>
|
||||
Max threads
|
||||
{t("tools.maxThreads")}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
|
||||
@@ -26,15 +26,13 @@ from app.core.logging import setup_logging
|
||||
from app.core.proxy_manager import proxy_manager
|
||||
from app.database import close_db, get_db
|
||||
from app.services.registry import register_all_handlers
|
||||
from version import VERSION
|
||||
|
||||
mimetypes.add_type("application/javascript", ".js")
|
||||
mimetypes.add_type("text/css", ".css")
|
||||
mimetypes.add_type("image/svg+xml", ".svg")
|
||||
|
||||
|
||||
VERSION = "0.3.5"
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
setup_logging()
|
||||
|
||||
+2
-1
@@ -1,4 +1,4 @@
|
||||
# === SteamPanel v0.3.0 ===
|
||||
# === SteamPanel ===
|
||||
|
||||
# Web framework
|
||||
fastapi>=0.115.0
|
||||
@@ -26,6 +26,7 @@ loguru>=0.7.3
|
||||
|
||||
# Browser automation
|
||||
nodriver
|
||||
psutil>=6.0.0
|
||||
|
||||
# Steam auth (embedded locally from pysteamauth, no pip install needed)
|
||||
bitstring>=3.1.2
|
||||
|
||||
@@ -33,20 +33,22 @@ if errorlevel 1 (
|
||||
venv\Scripts\pip.exe install -r requirements.txt
|
||||
if errorlevel 1 goto :err_deps
|
||||
echo Dependencies installed.
|
||||
rem === Patch nodriver/cdp/network.py encoding bug (non-UTF-8 byte in comment) ===
|
||||
venv\Scripts\python.exe -c "import pathlib; p=pathlib.Path('venv/Lib/site-packages/nodriver/cdp/network.py'); d=p.read_bytes() if p.exists() else b''; p.write_bytes(d.replace(b'\xb1',b'+-')) if p.exists() and b'\xb1' in d else None"
|
||||
) else (
|
||||
echo Dependencies OK.
|
||||
)
|
||||
|
||||
rem === Ensure psutil is installed (required for browser process monitoring) ===
|
||||
venv\Scripts\pip.exe show psutil >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo Installing psutil...
|
||||
venv\Scripts\pip.exe install psutil>=6.0.0 -q
|
||||
)
|
||||
|
||||
:launch
|
||||
cls
|
||||
|
||||
rem === Patch nodriver/cdp/network.py encoding bug (non-UTF-8 byte \xb1 in comment) ===
|
||||
venv\Scripts\python.exe -c ^
|
||||
"import pathlib;^
|
||||
p=pathlib.Path('venv/Lib/site-packages/nodriver/cdp/network.py');^
|
||||
d=p.read_bytes() if p.exists() else b'';^
|
||||
p.write_bytes(d.replace(b'\xb1',b'+-')) if p.exists() and b'\xb1' in d else None"
|
||||
|
||||
rem === Check built frontend ===
|
||||
if not exist "app\static\assets\" (
|
||||
echo.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
VERSION = "0.3.6"
|
||||
Reference in New Issue
Block a user