Import sanitized project structure and GitHub docs

This commit is contained in:
Rixuan Shao
2026-05-17 18:28:25 +08:00
parent d7e6bb1628
commit de4f3aeb74
87 changed files with 16696 additions and 1 deletions
+260
View File
@@ -0,0 +1,260 @@
import json
import logging
import os
import secrets
import sys
from copy import deepcopy
from enum import Enum
from pathlib import Path
from utils.logger import setup_logger
logger = setup_logger(level=logging.DEBUG)
DEBUG = False
CONFIGFILE = "config.json"
USERDATAFILE = "usersData.json"
APPSETTINGSFILE = "webui_settings.json"
DEFAULT_CONFIG = {
"multiTask": True,
"taskCount": 5,
"proxyAddress": "",
"messageTemplate": "【AI续火花】",
"saveDebugArtifacts": False,
"useProtocolSender": True,
"protocolDryRun": False,
"browserSenderAccounts": [],
"sendStrategy": {
"shuffleTargets": True,
"accountStartDelaySecondsMin": 0,
"accountStartDelaySecondsMax": 20,
"messageIntervalSecondsMin": 18,
"messageIntervalSecondsMax": 45,
"messageVariants": [],
},
"dailySendWindow": {
"enabled": False,
"startHour": 10,
"endHour": 18,
"scheduleIntervalMinutes": 10,
},
"hitokotoTypes": ["文学", "影视", "诗词", "哲学"],
"happyNewYear": {
"enabled": False,
"messageTemplate": "【[data]|[data_lunar]】\n[API]",
},
}
DEFAULT_APP_SETTINGS = {
"admin_username": "admin",
"admin_password_hash": "",
"session_secret": "",
"session_max_age_seconds": 8 * 60 * 60,
"compose_root": "",
"ui_host": "0.0.0.0",
"ui_port": 8787,
"login_poll_interval_seconds": 1,
"ops_log_file": "/var/log/douyin-sparkflow.log",
"proxy_refresh_script": "/opt/douyin-sparkflow/refresh_proxy.sh",
"local_login_helper_url": "http://127.0.0.1:18765",
"login_desktop_api_url": "http://127.0.0.1:18090",
"server_host": "",
"server_username": "",
"server_password": "",
}
config = None
userData = None
appSettings = None
class Environment(Enum):
GITHUBACTION = "GITHUB_ACTION"
LOCAL = "LOCAL"
PACKED = "PACKED"
def __str__(self):
return self.value
def get_environment():
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
return Environment.PACKED
if os.getenv("GITHUB_ACTIONS") == "true":
return Environment.GITHUBACTION
return Environment.LOCAL
def repo_root():
return Path(__file__).resolve().parents[1]
def _runtime_root():
env = get_environment()
if env == Environment.PACKED:
return Path(sys.executable).resolve().parent
return repo_root()
def config_path():
return _runtime_root() / CONFIGFILE
def users_data_path():
return _runtime_root() / USERDATAFILE
def app_settings_path():
return _runtime_root() / APPSETTINGSFILE
def default_compose_root():
root = repo_root()
parent = root.parent
if (parent / "docker-compose.yml").exists():
return str(parent)
return str(root)
def _merge_defaults(data, defaults):
merged = deepcopy(defaults)
for key, value in data.items():
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
merged[key].update(value)
else:
merged[key] = value
return merged
def _load_json_file(path, defaults=None):
if not path.exists():
if defaults is None:
raise FileNotFoundError(path)
path.write_text(json.dumps(defaults, ensure_ascii=False, indent=2), encoding="utf-8")
return deepcopy(defaults)
text = path.read_text(encoding="utf-8")
if not text.strip():
return deepcopy(defaults) if defaults is not None else None
data = json.loads(text)
if defaults is None:
return data
# _merge_defaults only works with dicts; for list-shaped data
# (e.g. usersData.json) just return the parsed data as-is.
if not isinstance(data, dict) or not isinstance(defaults, dict):
return data
return _merge_defaults(data, defaults)
def _save_json_file(path, data):
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def get_config(force_reload=False):
global config
if config is None or force_reload:
config = _load_json_file(config_path(), DEFAULT_CONFIG)
return deepcopy(config)
def save_config(new_config):
global config
config = _merge_defaults(new_config, DEFAULT_CONFIG)
_save_json_file(config_path(), config)
return deepcopy(config)
def get_userData(force_reload=False):
global userData
if userData is not None and not force_reload:
return deepcopy(userData)
env = get_environment()
if env == Environment.GITHUBACTION:
raw = os.getenv("USER_DATA", "")
if not raw:
logger.error("Environment variable USER_DATA is not set")
raise RuntimeError("USER_DATA is required in GITHUB_ACTIONS mode")
userData = json.loads(raw)
else:
userData = _load_json_file(users_data_path(), [])
return deepcopy(userData)
def save_userData(accounts):
global userData
normalized = list(accounts)
userData = normalized
_save_json_file(users_data_path(), normalized)
return deepcopy(userData)
def normalize_unique_id(unique_id):
if not unique_id:
return ""
digits = "".join(ch for ch in str(unique_id) if ch.isdigit())
return digits or str(unique_id).strip()
def upsert_user_account(unique_id, username, cookies, targets, extra=None):
unique_id = normalize_unique_id(unique_id)
accounts = get_userData(force_reload=True)
payload = {
"unique_id": unique_id,
"username": username,
"cookies": cookies,
"targets": list(targets),
}
if extra:
payload.update(extra)
for account in accounts:
if normalize_unique_id(account.get("unique_id")) == unique_id:
if "enabled" not in payload:
payload["enabled"] = account.get("enabled", True)
account.update(payload)
save_userData(accounts)
return payload
if "enabled" not in payload:
payload["enabled"] = True
accounts.append(payload)
save_userData(accounts)
return payload
def delete_user_account(unique_id):
normalized_id = normalize_unique_id(unique_id)
accounts = get_userData(force_reload=True)
remaining = [item for item in accounts if normalize_unique_id(item.get("unique_id")) != normalized_id]
removed = len(accounts) != len(remaining)
if removed:
save_userData(remaining)
return removed
def get_app_settings(force_reload=False):
global appSettings
if appSettings is None or force_reload:
appSettings = _load_json_file(app_settings_path(), DEFAULT_APP_SETTINGS)
if not appSettings.get("session_secret"):
appSettings["session_secret"] = secrets.token_urlsafe(32)
if not appSettings.get("compose_root"):
appSettings["compose_root"] = default_compose_root()
_save_json_file(app_settings_path(), appSettings)
return deepcopy(appSettings)
def save_app_settings(new_settings):
global appSettings
appSettings = _merge_defaults(new_settings, DEFAULT_APP_SETTINGS)
if not appSettings.get("session_secret"):
appSettings["session_secret"] = secrets.token_urlsafe(32)
if not appSettings.get("compose_root"):
appSettings["compose_root"] = default_compose_root()
_save_json_file(app_settings_path(), appSettings)
return deepcopy(appSettings)