316 lines
12 KiB
Python
316 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime
|
|
|
|
from app.core.config import settings as app_settings
|
|
from app.core.files import read_json, settings_backup_root, write_json
|
|
from app.core.redis_client import get_redis
|
|
from app.services.runtime_settings_service import get_runtime_settings, update_runtime_settings
|
|
|
|
|
|
REDIS_KEYS = {
|
|
"detect_options": "domain_tool:detect_options",
|
|
"proxy_config": "domain_tool:proxy_config",
|
|
"thread_count": "domain_tool:thread_count",
|
|
"node_thread_counts": "domain_tool:node_thread_counts",
|
|
"credentials": "domain_tool:credentials",
|
|
}
|
|
|
|
DETECT_OPTION_KEYS = {
|
|
"detect_register",
|
|
"detect_wayback",
|
|
"detect_chinaz",
|
|
"detect_aizhan",
|
|
"detect_baidu_site",
|
|
"detect_360_site",
|
|
"detect_jucha",
|
|
"detect_juziseo",
|
|
}
|
|
|
|
|
|
def _normalize_thread_count(value: object, *, field_name: str = "thread_count") -> int:
|
|
try:
|
|
thread_count = int(value)
|
|
except Exception as exc:
|
|
raise ValueError(f"{field_name} must be an integer") from exc
|
|
if thread_count < 1 or thread_count > 256:
|
|
raise ValueError(f"{field_name} out of range")
|
|
return thread_count
|
|
|
|
|
|
def _normalize_node_thread_counts(payload: object) -> dict[str, int]:
|
|
if payload in (None, ""):
|
|
return {}
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("node_thread_counts must be an object")
|
|
|
|
normalized: dict[str, int] = {}
|
|
for raw_node_code, raw_thread_count in payload.items():
|
|
node_code = str(raw_node_code or "").strip()
|
|
if not node_code:
|
|
raise ValueError("node_thread_counts contains empty node code")
|
|
normalized[node_code] = _normalize_thread_count(raw_thread_count, field_name=f"node_thread_counts.{node_code}")
|
|
return normalized
|
|
|
|
|
|
def _load_thread_count_config() -> tuple[int, dict[str, int]]:
|
|
thread_count_payload = read_json("thread_count.json", default={"thread_count": "2"})
|
|
node_thread_counts_payload = read_json("node_thread_counts.json", default={})
|
|
|
|
try:
|
|
default_thread_count = _normalize_thread_count(thread_count_payload.get("thread_count", 2))
|
|
except ValueError:
|
|
default_thread_count = 2
|
|
try:
|
|
node_thread_counts = _normalize_node_thread_counts(node_thread_counts_payload)
|
|
except ValueError:
|
|
node_thread_counts = {}
|
|
|
|
redis_client = get_redis()
|
|
try:
|
|
if redis_thread_count := redis_client.get(REDIS_KEYS["thread_count"]):
|
|
try:
|
|
default_thread_count = _normalize_thread_count(redis_thread_count)
|
|
except ValueError:
|
|
pass
|
|
if redis_node_thread_counts := redis_client.get(REDIS_KEYS["node_thread_counts"]):
|
|
try:
|
|
node_thread_counts = _normalize_node_thread_counts(json.loads(redis_node_thread_counts))
|
|
except ValueError:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
return default_thread_count, node_thread_counts
|
|
|
|
|
|
def resolve_thread_count(node_code: str | None = None, settings_payload: dict | None = None) -> dict:
|
|
payload = settings_payload or get_settings_payload()
|
|
default_thread_count = int(payload.get("thread_count", 2))
|
|
node_thread_counts = _normalize_node_thread_counts(payload.get("node_thread_counts", {}))
|
|
normalized_node_code = str(node_code or app_settings.node_code or "").strip()
|
|
|
|
override_thread_count = None
|
|
source = "default"
|
|
effective_thread_count = default_thread_count
|
|
if normalized_node_code and normalized_node_code in node_thread_counts:
|
|
override_thread_count = node_thread_counts[normalized_node_code]
|
|
effective_thread_count = override_thread_count
|
|
source = "node_override"
|
|
|
|
return {
|
|
"node_code": normalized_node_code,
|
|
"default_thread_count": default_thread_count,
|
|
"effective_thread_count": effective_thread_count,
|
|
"override_thread_count": override_thread_count,
|
|
"source": source,
|
|
"node_thread_counts": node_thread_counts,
|
|
}
|
|
|
|
|
|
def get_settings_payload() -> dict:
|
|
detect_options = read_json("detect_options.json", default={})
|
|
proxy_config = read_json("proxy_config.json", default={})
|
|
thread_count, node_thread_counts = _load_thread_count_config()
|
|
|
|
redis_client = get_redis()
|
|
try:
|
|
if redis_detect_options := redis_client.get(REDIS_KEYS["detect_options"]):
|
|
detect_options = json.loads(redis_detect_options)
|
|
if redis_proxy_config := redis_client.get(REDIS_KEYS["proxy_config"]):
|
|
proxy_config = json.loads(redis_proxy_config)
|
|
except Exception:
|
|
pass
|
|
|
|
return {
|
|
"detect_options": detect_options,
|
|
"proxy_config": proxy_config,
|
|
"thread_count": thread_count,
|
|
"node_thread_counts": node_thread_counts,
|
|
"current_node_code": app_settings.node_code,
|
|
"runtime_settings": get_runtime_settings(),
|
|
}
|
|
|
|
|
|
def get_credentials_payload() -> dict:
|
|
credentials = read_json(
|
|
"credentials.json",
|
|
default={
|
|
"juming": {"email": "", "password": ""},
|
|
"juziseo": {"email": "", "password": ""},
|
|
},
|
|
)
|
|
|
|
redis_client = get_redis()
|
|
try:
|
|
if redis_credentials := redis_client.get(REDIS_KEYS["credentials"]):
|
|
credentials = json.loads(redis_credentials)
|
|
except Exception:
|
|
pass
|
|
|
|
return {
|
|
"juming": {
|
|
"email": str(credentials.get("juming", {}).get("email", "")),
|
|
"password": str(credentials.get("juming", {}).get("password", "")),
|
|
},
|
|
"juziseo": {
|
|
"email": str(credentials.get("juziseo", {}).get("email", "")),
|
|
"password": str(credentials.get("juziseo", {}).get("password", "")),
|
|
},
|
|
}
|
|
|
|
|
|
def update_credentials_payload(payload: dict) -> dict:
|
|
current = get_credentials_payload()
|
|
credentials = {
|
|
"juming": {
|
|
"email": str(payload.get("juming", {}).get("email", current["juming"]["email"])),
|
|
"password": str(payload.get("juming", {}).get("password", current["juming"]["password"])),
|
|
},
|
|
"juziseo": {
|
|
"email": str(payload.get("juziseo", {}).get("email", current["juziseo"]["email"])),
|
|
"password": str(payload.get("juziseo", {}).get("password", current["juziseo"]["password"])),
|
|
},
|
|
}
|
|
|
|
write_json("credentials.json", credentials)
|
|
|
|
redis_client = get_redis()
|
|
try:
|
|
redis_client.set(REDIS_KEYS["credentials"], json.dumps(credentials, ensure_ascii=False))
|
|
redis_client.publish("domain_tool:credentials:update", json.dumps(credentials, ensure_ascii=False))
|
|
except Exception:
|
|
pass
|
|
|
|
return credentials
|
|
|
|
|
|
def update_settings_payload(payload: dict) -> dict:
|
|
current = get_settings_payload()
|
|
detect_options = payload.get("detect_options", current["detect_options"])
|
|
proxy_config = payload.get("proxy_config", current["proxy_config"])
|
|
thread_count = _normalize_thread_count(payload.get("thread_count", current["thread_count"]))
|
|
node_thread_counts = _normalize_node_thread_counts(payload.get("node_thread_counts", current.get("node_thread_counts", {})))
|
|
runtime_settings = update_runtime_settings(payload.get("runtime_settings", current["runtime_settings"]))
|
|
|
|
write_json("detect_options.json", detect_options)
|
|
write_json("proxy_config.json", proxy_config)
|
|
write_json("thread_count.json", {"thread_count": str(thread_count)})
|
|
write_json("node_thread_counts.json", node_thread_counts)
|
|
|
|
redis_client = get_redis()
|
|
try:
|
|
redis_client.set(REDIS_KEYS["detect_options"], json.dumps(detect_options, ensure_ascii=False))
|
|
redis_client.publish("domain_tool:detect_options:update", json.dumps(detect_options, ensure_ascii=False))
|
|
redis_client.set(REDIS_KEYS["proxy_config"], json.dumps(proxy_config, ensure_ascii=False))
|
|
redis_client.publish("domain_tool:proxy_config:update", json.dumps(proxy_config, ensure_ascii=False))
|
|
redis_client.set(REDIS_KEYS["thread_count"], thread_count)
|
|
redis_client.publish("domain_tool:thread_count:update", str(thread_count))
|
|
redis_client.set(REDIS_KEYS["node_thread_counts"], json.dumps(node_thread_counts, ensure_ascii=False))
|
|
redis_client.publish("domain_tool:node_thread_counts:update", json.dumps(node_thread_counts, ensure_ascii=False))
|
|
except Exception:
|
|
pass
|
|
|
|
return {
|
|
"detect_options": detect_options,
|
|
"proxy_config": proxy_config,
|
|
"thread_count": thread_count,
|
|
"node_thread_counts": node_thread_counts,
|
|
"current_node_code": app_settings.node_code,
|
|
"runtime_settings": runtime_settings,
|
|
}
|
|
|
|
|
|
def export_settings_snapshot() -> dict:
|
|
return {
|
|
"schema_version": "1.0",
|
|
"exported_at": datetime.now().isoformat(),
|
|
"source": "domain-api",
|
|
"settings": get_settings_payload(),
|
|
}
|
|
|
|
|
|
def import_settings_snapshot(payload: dict) -> dict:
|
|
settings_payload = payload.get("settings", payload)
|
|
validate_settings_payload(settings_payload)
|
|
backup_current_settings("import")
|
|
return update_settings_payload(settings_payload)
|
|
|
|
|
|
def validate_settings_payload(payload: dict) -> None:
|
|
if not isinstance(payload, dict):
|
|
raise ValueError("invalid settings payload")
|
|
|
|
if "thread_count" in payload:
|
|
_normalize_thread_count(payload["thread_count"])
|
|
|
|
if "node_thread_counts" in payload:
|
|
_normalize_node_thread_counts(payload["node_thread_counts"])
|
|
|
|
if "detect_options" in payload:
|
|
detect_options = payload["detect_options"]
|
|
if not isinstance(detect_options, dict):
|
|
raise ValueError("detect_options must be an object")
|
|
detect_order = detect_options.get("detect_order", [])
|
|
if detect_order and not isinstance(detect_order, list):
|
|
raise ValueError("detect_order must be an array")
|
|
if isinstance(detect_order, list):
|
|
unknown_keys = [item for item in detect_order if item not in DETECT_OPTION_KEYS]
|
|
if unknown_keys:
|
|
raise ValueError(f"unknown detect option keys: {', '.join(unknown_keys)}")
|
|
|
|
if "proxy_config" in payload:
|
|
proxy_config = payload["proxy_config"]
|
|
if not isinstance(proxy_config, dict):
|
|
raise ValueError("proxy_config must be an object")
|
|
proxy_urls = proxy_config.get("proxy_urls", [])
|
|
if proxy_urls and not isinstance(proxy_urls, list):
|
|
raise ValueError("proxy_urls must be an array")
|
|
|
|
if "runtime_settings" in payload:
|
|
runtime_settings = payload["runtime_settings"]
|
|
if not isinstance(runtime_settings, dict):
|
|
raise ValueError("runtime_settings must be an object")
|
|
worker_mode = runtime_settings.get("worker_mode")
|
|
if worker_mode and worker_mode not in {"windows-local", "linux-systemd"}:
|
|
raise ValueError("worker_mode must be windows-local or linux-systemd")
|
|
for key in ("worker_service_name", "api_service_name", "sync_agent_service_name"):
|
|
if key in runtime_settings and runtime_settings[key] is not None and not str(runtime_settings[key]).strip():
|
|
raise ValueError(f"{key} must not be empty")
|
|
|
|
|
|
def backup_current_settings(reason: str = "manual") -> dict:
|
|
snapshot = export_settings_snapshot()
|
|
snapshot["backup_reason"] = reason
|
|
filename = f"settings_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
path = settings_backup_root() / filename
|
|
path.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
return {
|
|
"filename": filename,
|
|
"path": str(path),
|
|
}
|
|
|
|
|
|
def list_settings_backups(limit: int = 20) -> list[dict]:
|
|
root = settings_backup_root()
|
|
files = sorted(root.glob("settings_backup_*.json"), key=lambda item: item.stat().st_mtime, reverse=True)
|
|
result: list[dict] = []
|
|
for item in files[:limit]:
|
|
backup_reason = ""
|
|
try:
|
|
payload = json.loads(item.read_text(encoding="utf-8"))
|
|
backup_reason = str(payload.get("backup_reason", ""))
|
|
except Exception:
|
|
backup_reason = ""
|
|
result.append(
|
|
{
|
|
"filename": item.name,
|
|
"path": str(item),
|
|
"size": item.stat().st_size,
|
|
"modified_at": datetime.fromtimestamp(item.stat().st_mtime).isoformat(),
|
|
"backup_reason": backup_reason,
|
|
}
|
|
)
|
|
return result
|