fix: 全站 ruff 清零 — 77 errors → 0

Fixes:
- F821: install.py site_url 未定义(NameError)、sync_v2.py os 未导入、script_jobs.py LOG f-string
- F401: 移除 22 个未使用导入(models/__init__.py、install.py、auth.py 等)
- B904: 20 个 except 块 raise 添加 from e/from None
- S110: 20 个有意的 try-except-pass 添加 noqa 注释(SSH清理/DDL幂等/WS断连)
- B007: 3 个未使用循环变量重命名(dirs→_dirs、server_id→_server_id)
- F541: 3 个无占位符 f-string 修正
- F841: auth.py 未使用 ip_address 变量移除
- S105: auth_service.py Redis key prefix 误报 noqa
- B023: script_execution_flush.py 循环变量绑定修复

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-05-30 20:07:45 +08:00
parent ebe276ea29
commit 32feb1b6db
21 changed files with 63 additions and 72 deletions
-1
View File
@@ -334,7 +334,6 @@ async def issue_webssh_token(
if not server.domain: if not server.domain:
raise HTTPException(status_code=400, detail="Server has no domain configured") raise HTTPException(status_code=400, detail="Server has no domain configured")
ip_address = request.client.host if request.client else ""
result = await service.create_webssh_token(admin, payload.server_id) result = await service.create_webssh_token(admin, payload.server_id)
return {**result, "server_name": server.name} return {**result, "server_name": server.name}
+10 -11
View File
@@ -17,7 +17,7 @@ from urllib.parse import quote_plus
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession from sqlalchemy.ext.asyncio import create_async_engine
from server.infrastructure.database.engine_compat import patch_aiomysql_do_ping from server.infrastructure.database.engine_compat import patch_aiomysql_do_ping
@@ -206,7 +206,7 @@ def _configure_guardian(install_dir: str, api_port: str) -> list[str]:
log_dir.mkdir(parents=True, exist_ok=True) log_dir.mkdir(parents=True, exist_ok=True)
results.append(f"✓ 日志目录已创建 {log_dir}") results.append(f"✓ 日志目录已创建 {log_dir}")
except OSError: except OSError:
results.append(f"✗ 日志目录创建失败(需 root 权限)") results.append("✗ 日志目录创建失败(需 root 权限)")
# 2. Supervisor config # 2. Supervisor config
if bt_panel: if bt_panel:
@@ -320,7 +320,6 @@ async def env_check():
"""Detect environment readiness — Python, MySQL client, Redis, write permissions.""" """Detect environment readiness — Python, MySQL client, Redis, write permissions."""
_reject_post_install_wizard() _reject_post_install_wizard()
import sys import sys
import importlib
checks = [] checks = []
@@ -406,12 +405,13 @@ async def init_db(req: InitDbRequest):
db_url = _make_db_url(req) db_url = _make_db_url(req)
redis_url = _build_redis_url(req) redis_url = _build_redis_url(req)
site_url = req.site_url.rstrip("/") if req.site_url else f"http://127.0.0.1:{req.api_port}"
try: try:
patch_aiomysql_do_ping() patch_aiomysql_do_ping()
engine = create_async_engine(db_url, pool_pre_ping=True, pool_recycle=300) engine = create_async_engine(db_url, pool_pre_ping=True, pool_recycle=300)
except Exception as e: except Exception as e:
raise HTTPException(400, f"数据库引擎创建失败: {e}") raise HTTPException(400, f"数据库引擎创建失败: {e}") from e
try: try:
async with engine.begin() as conn: async with engine.begin() as conn:
@@ -435,7 +435,7 @@ async def init_db(req: InitDbRequest):
for idx_sql in indexes: for idx_sql in indexes:
try: try:
await conn.execute(text(idx_sql)) await conn.execute(text(idx_sql))
except Exception: except Exception: # noqa: S110 — DDL idempotent, may already exist
pass # Index may already exist pass # Index may already exist
# Schema migrations for existing databases (safe — no-op if column exists) # Schema migrations for existing databases (safe — no-op if column exists)
@@ -445,7 +445,7 @@ async def init_db(req: InitDbRequest):
for mig_sql in migrations: for mig_sql in migrations:
try: try:
await conn.execute(text(mig_sql)) await conn.execute(text(mig_sql))
except Exception: except Exception: # noqa: S110 — DDL idempotent, may already exist
pass # Column may already exist pass # Column may already exist
# Calculate pool_size from max_connections # Calculate pool_size from max_connections
@@ -462,7 +462,7 @@ async def init_db(req: InitDbRequest):
secret_key = secrets.token_hex(32) secret_key = secrets.token_hex(32)
api_key = secrets.token_hex(16) api_key = secrets.token_hex(16)
# Generate Fernet-compatible encryption key (32 url-safe base64 bytes) # Generate Fernet-compatible encryption key (32 url-safe base64 bytes)
import base64, hashlib import base64
encryption_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode() encryption_key = base64.urlsafe_b64encode(secrets.token_bytes(32)).decode()
# Insert settings # Insert settings
@@ -491,12 +491,11 @@ async def init_db(req: InitDbRequest):
except Exception as e: except Exception as e:
await engine.dispose() await engine.dispose()
raise HTTPException(400, f"数据库初始化失败: {e}") raise HTTPException(400, f"数据库初始化失败: {e}") from e
await engine.dispose() await engine.dispose()
# Write .env # Write .env
site_url = req.site_url.rstrip("/") if req.site_url else f"http://127.0.0.1:{req.api_port}"
_write_env(req, secret_key, api_key, encryption_key, pool_size, max_overflow, redis_url, site_url) _write_env(req, secret_key, api_key, encryption_key, pool_size, max_overflow, redis_url, site_url)
# Write config.json # Write config.json
@@ -588,7 +587,7 @@ async def create_admin(req: CreateAdminRequest):
await engine.dispose() await engine.dispose()
except Exception as e: except Exception as e:
raise HTTPException(400, f"创建管理员失败: {e}") raise HTTPException(400, f"创建管理员失败: {e}") from e
# Update config.json with brand # Update config.json with brand
if CONFIG_JSON.exists(): if CONFIG_JSON.exists():
@@ -597,7 +596,7 @@ async def create_admin(req: CreateAdminRequest):
config = json.loads(CONFIG_JSON.read_text()) config = json.loads(CONFIG_JSON.read_text())
config["app_name"] = req.system_name config["app_name"] = req.system_name
CONFIG_JSON.write_text(json.dumps(config, indent=2, ensure_ascii=False)) CONFIG_JSON.write_text(json.dumps(config, indent=2, ensure_ascii=False))
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
# Update .env with brand # Update .env with brand
-1
View File
@@ -3,7 +3,6 @@ Presentation layer — receives HTTP requests, delegates to ScriptService.
All operations require JWT authentication. All operations require JWT authentication.
""" """
import json
import logging import logging
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi import APIRouter, Depends, HTTPException, Query, Request
+7 -7
View File
@@ -19,7 +19,7 @@ from server.infrastructure.database.push_schedule_repo import PushScheduleReposi
from server.infrastructure.database.password_preset_repo import PasswordPresetRepositoryImpl from server.infrastructure.database.password_preset_repo import PasswordPresetRepositoryImpl
from server.infrastructure.database.ssh_key_preset_repo import SshKeyPresetRepositoryImpl from server.infrastructure.database.ssh_key_preset_repo import SshKeyPresetRepositoryImpl
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
from server.domain.models import Setting, PushSchedule, PushRetryJob, PasswordPreset, SshKeyPreset, AuditLog, Admin from server.domain.models import PushSchedule, PushRetryJob, PasswordPreset, SshKeyPreset, AuditLog, Admin
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
@@ -327,7 +327,7 @@ async def reveal_preset(
try: try:
plaintext = decrypt_value(preset.encrypted_pw) plaintext = decrypt_value(preset.encrypted_pw)
except ValueError: except ValueError:
raise HTTPException(status_code=500, detail="解密失败") raise HTTPException(status_code=500, detail="解密失败") from None
audit_repo = AuditLogRepositoryImpl(db) audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog( await audit_repo.create(AuditLog(
@@ -472,7 +472,7 @@ async def reveal_ssh_key_preset(
try: try:
plaintext = decrypt_value(preset.encrypted_private_key) plaintext = decrypt_value(preset.encrypted_private_key)
except ValueError: except ValueError:
raise HTTPException(status_code=500, detail="解密失败") raise HTTPException(status_code=500, detail="解密失败") from None
audit_repo = AuditLogRepositoryImpl(db) audit_repo = AuditLogRepositoryImpl(db)
await audit_repo.create(AuditLog( await audit_repo.create(AuditLog(
@@ -689,7 +689,7 @@ async def telegram_get_chats(
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
raise HTTPException(status_code=502, detail=f"请求失败: {e}") raise HTTPException(status_code=502, detail=f"请求失败: {e}") from e
# Extract unique chats # Extract unique chats
seen: set[int] = set() seen: set[int] = set()
@@ -748,11 +748,11 @@ async def parse_subscription(
raise HTTPException(status_code=400, detail=f"订阅链接返回 HTTP {resp.status_code}") raise HTTPException(status_code=400, detail=f"订阅链接返回 HTTP {resp.status_code}")
raw = resp.text raw = resp.text
except httpx.TimeoutException: except httpx.TimeoutException:
raise HTTPException(status_code=400, detail="请求订阅链接超时(15s") raise HTTPException(status_code=400, detail="请求订阅链接超时(15s") from None
except HTTPException: except HTTPException:
raise raise
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f"获取订阅失败: {e}") raise HTTPException(status_code=400, detail=f"获取订阅失败: {e}") from e
hosts = parse_subscription_content(raw) hosts = parse_subscription_content(raw)
if not hosts: if not hosts:
@@ -914,7 +914,7 @@ async def get_ip_allowlist(
): ):
"""Return full allowlist state.""" """Return full allowlist state."""
from server.config import settings as _settings from server.config import settings as _settings
from server.background.ip_allowlist_refresh import get_last_refresh_time, get_subscription_count from server.background.ip_allowlist_refresh import get_last_refresh_time
allowlist_on = (_settings.LOGIN_ALLOWLIST_ENABLED or "false").lower() in ("true","1","yes","on") allowlist_on = (_settings.LOGIN_ALLOWLIST_ENABLED or "false").lower() in ("true","1","yes","on")
sub_ips_raw = _settings.LOGIN_SUBSCRIPTION_IPS or "" sub_ips_raw = _settings.LOGIN_SUBSCRIPTION_IPS or ""
+13 -13
View File
@@ -10,7 +10,6 @@ import logging
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
from server.api.schemas import SyncFiles, SyncBrowse, SyncBrowseLocal, SyncPreview, FileOperation, LocalFileOperation, LocalFilePreview, SyncVerify, SyncCancel, ValidateSourcePath, SyncDiagnose, FileSyncDiff from server.api.schemas import SyncFiles, SyncBrowse, SyncBrowseLocal, SyncPreview, FileOperation, LocalFileOperation, LocalFilePreview, SyncVerify, SyncCancel, ValidateSourcePath, SyncDiagnose, FileSyncDiff
from server.api.dependencies import get_server_service
from server.application.services.sync_engine_v2 import SyncEngineV2 from server.application.services.sync_engine_v2 import SyncEngineV2
from server.infrastructure.database.server_repo import ServerRepositoryImpl from server.infrastructure.database.server_repo import ServerRepositoryImpl
from server.infrastructure.database.sync_log_repo import SyncLogRepositoryImpl from server.infrastructure.database.sync_log_repo import SyncLogRepositoryImpl
@@ -227,7 +226,7 @@ async def browse_local_directory(
except OSError: except OSError:
continue continue
except PermissionError: except PermissionError:
raise HTTPException(status_code=403, detail="无权限访问该目录") raise HTTPException(status_code=403, detail="无权限访问该目录") from None
return {"path": real_path, "entries": entries} return {"path": real_path, "entries": entries}
@@ -261,7 +260,7 @@ async def local_file_operation(
else: else:
os.unlink(real_path) os.unlink(real_path)
except OSError as e: except OSError as e:
raise HTTPException(status_code=500, detail=f"删除失败: {e}") raise HTTPException(status_code=500, detail=f"删除失败: {e}") from e
await _audit_sync( await _audit_sync(
"local_file_delete", "sync", 0, "local_file_delete", "sync", 0,
@@ -293,7 +292,7 @@ async def local_file_operation(
try: try:
os.rename(real_path, new_path) os.rename(real_path, new_path)
except OSError as e: except OSError as e:
raise HTTPException(status_code=500, detail=f"重命名失败: {e}") raise HTTPException(status_code=500, detail=f"重命名失败: {e}") from e
await _audit_sync( await _audit_sync(
"local_file_rename", "sync", 0, "local_file_rename", "sync", 0,
@@ -398,7 +397,7 @@ async def cancel_sync(
redis = get_redis() redis = get_redis()
await redis.set(f"sync:cancel:{payload.batch_id}", "1", ex=3600) await redis.set(f"sync:cancel:{payload.batch_id}", "1", ex=3600)
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"取消操作失败: {e}") raise HTTPException(status_code=500, detail=f"取消操作失败: {e}") from e
await _audit_sync( await _audit_sync(
"sync_cancel", "sync", 0, "sync_cancel", "sync", 0,
@@ -745,7 +744,7 @@ async def upload_file(
try: try:
server_id = int(server_id_raw) server_id = int(server_id_raw)
except (ValueError, TypeError): except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="server_id 必须为数字") raise HTTPException(status_code=400, detail="server_id 必须为数字") from None
if not file or not hasattr(file, "filename") or not file.filename: if not file or not hasattr(file, "filename") or not file.filename:
raise HTTPException(status_code=400, detail="请选择要上传的文件") raise HTTPException(status_code=400, detail="请选择要上传的文件")
@@ -785,11 +784,11 @@ async def upload_file(
finally: finally:
await ssh_pool.release(server.id) await ssh_pool.release(server.id)
except asyncssh.PermissionDenied: except asyncssh.PermissionDenied:
raise HTTPException(status_code=403, detail=f"SSH 权限不足,无法写入 {full_remote_path}") raise HTTPException(status_code=403, detail=f"SSH 权限不足,无法写入 {full_remote_path}") from None
except asyncssh.SFTPError as e: except asyncssh.SFTPError as e:
raise HTTPException(status_code=400, detail=f"SFTP 上传失败: {e}") raise HTTPException(status_code=400, detail=f"SFTP 上传失败: {e}") from e
except Exception as e: except Exception as e:
raise HTTPException(status_code=502, detail=f"SSH 连接失败: {e}") raise HTTPException(status_code=502, detail=f"SSH 连接失败: {e}") from e
# Audit # Audit
await _audit_sync( await _audit_sync(
@@ -881,7 +880,7 @@ async def upload_zip(
# Build file listing (relative paths, capped at 500) # Build file listing (relative paths, capped at 500)
files = [] files = []
for root, dirs, fnames in os.walk(extract_dir): for root, _dirs, fnames in os.walk(extract_dir):
for fn in fnames: for fn in fnames:
rel = os.path.relpath(os.path.join(root, fn), extract_dir) rel = os.path.relpath(os.path.join(root, fn), extract_dir)
files.append(rel) files.append(rel)
@@ -921,14 +920,14 @@ async def upload_zip(
os.unlink(zip_path) os.unlink(zip_path)
except OSError: except OSError:
pass pass
raise HTTPException(status_code=400, detail="无效的 ZIP 文件") raise HTTPException(status_code=400, detail="无效的 ZIP 文件") from None
except Exception as e: except Exception as e:
shutil.rmtree(extract_dir, ignore_errors=True) shutil.rmtree(extract_dir, ignore_errors=True)
try: try:
os.unlink(zip_path) os.unlink(zip_path)
except OSError: except OSError:
pass pass
raise HTTPException(status_code=500, detail=f"解压失败: {e}") raise HTTPException(status_code=500, detail=f"解压失败: {e}") from e
# ── Post-Push Verification (sha256sum comparison) ── # ── Post-Push Verification (sha256sum comparison) ──
@@ -947,6 +946,7 @@ async def verify_sync(
""" """
import asyncio import asyncio
import hashlib import hashlib
import os
import shlex import shlex
from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command from server.infrastructure.ssh.asyncssh_pool import exec_ssh_command
from server.infrastructure.database.server_repo import ServerRepositoryImpl from server.infrastructure.database.server_repo import ServerRepositoryImpl
@@ -958,7 +958,7 @@ async def verify_sync(
# Build local file manifest: {relative_path: sha256hex} # Build local file manifest: {relative_path: sha256hex}
local_files = {} local_files = {}
for root, dirs, fnames in os.walk(payload.source_path): for root, _dirs, fnames in os.walk(payload.source_path):
for fn in fnames: for fn in fnames:
full = os.path.join(root, fn) full = os.path.join(root, fn)
rel = os.path.relpath(full, payload.source_path) rel = os.path.relpath(full, payload.source_path)
+2 -2
View File
@@ -15,7 +15,7 @@ import asyncio
import json import json
import logging import logging
import time import time
from typing import Dict, Set, Optional from typing import Dict, Optional
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
@@ -71,7 +71,7 @@ class ConnectionManager:
) )
except RuntimeError: except RuntimeError:
pass # No running event loop (e.g. during shutdown) pass # No running event loop (e.g. during shutdown)
except Exception: except Exception: # noqa: S110 — client disconnected, best-effort cleanup
pass pass
logger.info(f"WebSocket disconnected: {client_id}, total: {len(self._connections)}") logger.info(f"WebSocket disconnected: {client_id}, total: {len(self._connections)}")
+9 -10
View File
@@ -5,7 +5,6 @@ import asyncio
import json import json
import logging import logging
import uuid import uuid
from datetime import datetime
from typing import Optional from typing import Optional
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query, Path from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query, Path
@@ -166,11 +165,11 @@ async def terminal_ws(
logger.error(f"WebSSH connection failed: {e}") logger.error(f"WebSSH connection failed: {e}")
try: try:
await websocket.send_json({"type": MSG_ERROR, "message": f"SSH连接失败: {str(e)}"}) await websocket.send_json({"type": MSG_ERROR, "message": f"SSH连接失败: {str(e)}"})
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
try: try:
await websocket.close(code=4003, reason="SSH connection failed") await websocket.close(code=4003, reason="SSH connection failed")
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
await _close_ssh_session(session_id) await _close_ssh_session(session_id)
return return
@@ -199,11 +198,11 @@ async def terminal_ws(
logger.error(f"WebSSH shell creation failed on fresh connection (server={server.id}): {type(e2).__name__}: {e2!r}") logger.error(f"WebSSH shell creation failed on fresh connection (server={server.id}): {type(e2).__name__}: {e2!r}")
try: try:
await websocket.send_json({"type": MSG_ERROR, "message": f"Shell创建失败: {type(e2).__name__}"}) await websocket.send_json({"type": MSG_ERROR, "message": f"Shell创建失败: {type(e2).__name__}"})
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
try: try:
await websocket.close(code=4003, reason="Shell creation failed") await websocket.close(code=4003, reason="Shell creation failed")
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
ws_closed = True ws_closed = True
await _close_ssh_session(session_id) await _close_ssh_session(session_id)
@@ -240,7 +239,7 @@ async def terminal_ws(
"type": MSG_DATA, "type": MSG_DATA,
"data": data, "data": data,
}) })
except Exception: except Exception: # noqa: S110 — SSH read loop, connection teardown
pass pass
async def _read_websocket_input(): async def _read_websocket_input():
@@ -296,7 +295,7 @@ async def terminal_ws(
except WebSocketDisconnect: except WebSocketDisconnect:
pass pass
except Exception: except Exception: # noqa: S110 — WebSocket read loop, connection teardown
pass pass
# Run both directions concurrently # Run both directions concurrently
@@ -323,7 +322,7 @@ async def terminal_ws(
if not ws_closed: if not ws_closed:
try: try:
await websocket.send_json({"type": MSG_ERROR, "message": f"Shell创建失败: {type(e).__name__}"}) await websocket.send_json({"type": MSG_ERROR, "message": f"Shell创建失败: {type(e).__name__}"})
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
finally: finally:
# ── Cleanup ── # ── Cleanup ──
@@ -333,12 +332,12 @@ async def terminal_ws(
if not ws_closed: if not ws_closed:
try: try:
await websocket.send_json({"type": MSG_CLOSE, "session_id": session_id}) await websocket.send_json({"type": MSG_CLOSE, "session_id": session_id})
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
try: try:
await websocket.close() await websocket.close()
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
logger.info(f"WebSSH session closed: admin={admin.username}, server={server.name}, session={session_id}") logger.info(f"WebSSH session closed: admin={admin.username}, server={server.name}, session={session_id}")
+1 -1
View File
@@ -60,7 +60,7 @@ JWT_REFRESH_TOKEN_EXPIRE_DAYS = 30 # 30 days
JWT_WEBSSH_TOKEN_EXPIRE_MINUTES = 15 JWT_WEBSSH_TOKEN_EXPIRE_MINUTES = 15
# Redis key prefix for multi-device refresh token storage # Redis key prefix for multi-device refresh token storage
REFRESH_TOKEN_REDIS_PREFIX = "refresh_tokens:" REFRESH_TOKEN_REDIS_PREFIX = "refresh_tokens:" # noqa: S105
class AuthService: class AuthService:
+1 -1
View File
@@ -62,7 +62,7 @@ def build_long_task_command(
"mkdir -p /var/log/nexus-job && " "mkdir -p /var/log/nexus-job && "
'LOG=/var/log/nexus-job/job-$(date +%Y%m%d-%H%M%S).log && ' 'LOG=/var/log/nexus-job/job-$(date +%Y%m%d-%H%M%S).log && '
f"nohup bash -c '{inner}' >> \"$LOG\" 2>&1 & " f"nohup bash -c '{inner}' >> \"$LOG\" 2>&1 & "
f'echo $! > "${LOG}.pid" && ' 'echo $! > "${LOG}.pid" && '
f'echo "started job_id={job_id} pid=$! log=$LOG"' f'echo "started job_id={job_id} pid=$! log=$LOG"'
) )
@@ -9,7 +9,6 @@ import asyncio
import logging import logging
import os import os
import re import re
import shutil
import tempfile import tempfile
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
-1
View File
@@ -4,7 +4,6 @@ MySQL only stores historical snapshots; frontend reads live data from Redis.
""" """
import asyncio import asyncio
import json
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
+1 -1
View File
@@ -8,7 +8,7 @@ import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from server.infrastructure.database.session import AsyncSessionLocal from server.infrastructure.database.session import AsyncSessionLocal
from server.domain.models import PushSchedule, SyncLog, AuditLog from server.domain.models import PushSchedule
logger = logging.getLogger("nexus.schedule_runner") logger = logging.getLogger("nexus.schedule_runner")
+2 -2
View File
@@ -26,8 +26,8 @@ async def script_execution_flush_loop():
repo = ScriptExecutionRepositoryImpl(session) repo = ScriptExecutionRepositoryImpl(session)
audit_repo = AuditLogRepositoryImpl(session) audit_repo = AuditLogRepositoryImpl(session)
async def _audit(action, target_type, target_id, operator): async def _audit(action, target_type, target_id, operator, _repo=audit_repo): # noqa: B023
await audit_repo.create(AuditLog( await _repo.create(AuditLog(
admin_username=operator or "system", admin_username=operator or "system",
action=action, action=action,
target_type=target_type, target_type=target_type,
+2 -2
View File
@@ -68,7 +68,7 @@ async def self_monitor_loop():
# Redis recovery notification # Redis recovery notification
if redis_ok and not _prev_redis_ok: if redis_ok and not _prev_redis_ok:
try: try:
await send_telegram_system_alert(f"🟢 Redis连接已恢复正常", notify_key="NOTIFY_SYSTEM_REDIS") await send_telegram_system_alert("🟢 Redis连接已恢复正常", notify_key="NOTIFY_SYSTEM_REDIS")
_last_telegram_time.pop("NOTIFY_SYSTEM_REDIS", None) # clear cooldown _last_telegram_time.pop("NOTIFY_SYSTEM_REDIS", None) # clear cooldown
except Exception: except Exception:
logger.error("Failed to send Redis recovery via Telegram") logger.error("Failed to send Redis recovery via Telegram")
@@ -92,7 +92,7 @@ async def self_monitor_loop():
# MySQL recovery notification # MySQL recovery notification
if mysql_ok and not _prev_mysql_ok: if mysql_ok and not _prev_mysql_ok:
try: try:
await send_telegram_system_alert(f"🟢 MySQL连接已恢复正常", notify_key="NOTIFY_SYSTEM_MYSQL") await send_telegram_system_alert("🟢 MySQL连接已恢复正常", notify_key="NOTIFY_SYSTEM_MYSQL")
_last_telegram_time.pop("NOTIFY_SYSTEM_MYSQL", None) # clear cooldown _last_telegram_time.pop("NOTIFY_SYSTEM_MYSQL", None) # clear cooldown
except Exception: except Exception:
logger.error("Failed to send MySQL recovery via Telegram") logger.error("Failed to send MySQL recovery via Telegram")
+2 -4
View File
@@ -5,11 +5,9 @@ SQLAlchemy ORM models for all database tables.
import datetime import datetime
from datetime import timezone from datetime import timezone
import uuid import uuid
import base64
import hashlib
from sqlalchemy import ( from sqlalchemy import (
create_engine, Column, Integer, String, Boolean, Column, Integer, String, Boolean,
DateTime, Text, ForeignKey, Enum, Index, JSON DateTime, Text, ForeignKey, Index, JSON
) )
from sqlalchemy.orm import declarative_base, relationship from sqlalchemy.orm import declarative_base, relationship
+1 -1
View File
@@ -3,7 +3,7 @@ Protocol-based interfaces for dependency inversion.
Concrete implementations in infrastructure/database/. Concrete implementations in infrastructure/database/.
""" """
from typing import Protocol, Optional, List, Dict from typing import Protocol, Optional, List
from server.domain.models import ( from server.domain.models import (
Server, SyncLog, Script, ScriptExecution, DbCredential, Server, SyncLog, Script, ScriptExecution, DbCredential,
Admin, LoginAttempt, Setting, PasswordPreset, Admin, LoginAttempt, Setting, PasswordPreset,
+1 -1
View File
@@ -1,6 +1,6 @@
"""Nexus — Admin & LoginAttempt Repository (Async SQLAlchemy)""" """Nexus — Admin & LoginAttempt Repository (Async SQLAlchemy)"""
from typing import Optional, List from typing import Optional
from sqlalchemy import select, func from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
+1 -2
View File
@@ -8,9 +8,8 @@ Also handles safe schema migrations for existing databases (adding new columns).
import logging import logging
from sqlalchemy import select, update, text from sqlalchemy import select, update, text
from sqlalchemy.ext.asyncio import AsyncSession
from server.domain.models import Server, Node, Platform, Base from server.domain.models import Server, Node, Platform
from server.infrastructure.database.session import AsyncSessionLocal from server.infrastructure.database.session import AsyncSessionLocal
logger = logging.getLogger("nexus.migration") logger = logging.getLogger("nexus.migration")
+1 -1
View File
@@ -60,7 +60,7 @@ async def init_redis(app: FastAPI) -> None:
except Exception as e: except Exception as e:
logger.error(f"Redis startup validation failed: {e}") logger.error(f"Redis startup validation failed: {e}")
raise SystemExit(f"Redis unavailable — Nexus cannot start (ADR-009). Error: {e}") raise SystemExit(f"Redis unavailable — Nexus cannot start (ADR-009). Error: {e}") from e
async def close_redis() -> None: async def close_redis() -> None:
+7 -7
View File
@@ -74,10 +74,10 @@ class AsyncSSHPool:
pass pass
async with self._lock: async with self._lock:
for server_id, pooled in self._pool.items(): for _server_id, pooled in self._pool.items():
try: try:
pooled.conn.close() pooled.conn.close()
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
self._pool.clear() self._pool.clear()
logger.info("AsyncSSH pool stopped — all connections closed") logger.info("AsyncSSH pool stopped — all connections closed")
@@ -118,7 +118,7 @@ class AsyncSSHPool:
# Close our new connection, reuse the existing one # Close our new connection, reuse the existing one
try: try:
conn.close() conn.close()
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
pooled.ref_count += 1 pooled.ref_count += 1
pooled.last_used = time.monotonic() pooled.last_used = time.monotonic()
@@ -153,7 +153,7 @@ class AsyncSSHPool:
if pooled: if pooled:
try: try:
pooled.conn.close() pooled.conn.close()
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
logger.debug(f"Force-closed SSH connection: server={server_id}") logger.debug(f"Force-closed SSH connection: server={server_id}")
@@ -190,7 +190,7 @@ class AsyncSSHPool:
conn = await asyncssh.connect(**connect_kwargs) conn = await asyncssh.connect(**connect_kwargs)
return conn return conn
except asyncssh.Error as e: except asyncssh.Error as e:
raise ConnectionError(f"SSH connection failed to {server.domain}:{server.port}: {e}") raise ConnectionError(f"SSH connection failed to {server.domain}:{server.port}: {e}") from e
async def _evict_one(self) -> bool: async def _evict_one(self) -> bool:
"""Evict the oldest idle connection from the pool. """Evict the oldest idle connection from the pool.
@@ -209,7 +209,7 @@ class AsyncSSHPool:
pooled = self._pool.pop(oldest_idle) pooled = self._pool.pop(oldest_idle)
try: try:
pooled.conn.close() pooled.conn.close()
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
logger.debug(f"Evicted idle SSH connection: server={oldest_idle}") logger.debug(f"Evicted idle SSH connection: server={oldest_idle}")
return True return True
@@ -233,7 +233,7 @@ class AsyncSSHPool:
pooled = self._pool.pop(server_id) pooled = self._pool.pop(server_id)
try: try:
pooled.conn.close() pooled.conn.close()
except Exception: except Exception: # noqa: S110 — best-effort cleanup
pass pass
if to_remove: if to_remove:
+2 -2
View File
@@ -47,7 +47,7 @@ def _parse_ss(line: str) -> Optional[str]:
if "@" in decoded: if "@" in decoded:
host_part = decoded.split("@", 1)[1].split(":")[0] host_part = decoded.split("@", 1)[1].split(":")[0]
return host_part return host_part
except Exception: except Exception: # noqa: S110 — non-critical URL parse fallback
pass pass
return None return None
@@ -93,7 +93,7 @@ def parse_subscription_content(raw_content: str) -> list[str]:
decoded = _decode_b64(content) decoded = _decode_b64(content)
if any(decoded.startswith(p) for p in ("ss://", "vmess://", "vless://", "trojan://")): if any(decoded.startswith(p) for p in ("ss://", "vmess://", "vless://", "trojan://")):
content = decoded content = decoded
except Exception: except Exception: # noqa: S110 — non-critical parse, treat as non-encoded
pass pass
hosts: list[str] = [] hosts: list[str] = []