fix: harden residual flow boundaries and pending recovery
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
"""Account result persistence and pending recovery helpers."""
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from filelock import FileLock
|
||||
|
||||
|
||||
def append_account_line(path, email, password, sso):
|
||||
with open(path, "a", encoding="utf-8") as handle:
|
||||
handle.write(f"{email}----{password}----{sso}\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
|
||||
|
||||
def save_mail_credential(base_dir, email, credential):
|
||||
path = os.path.join(base_dir, "mail_credentials.txt")
|
||||
with open(path, "a", encoding="utf-8") as handle:
|
||||
handle.write(f"{email}\t{credential}\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
return True
|
||||
|
||||
|
||||
def queue_unsaved_account(path, payload, error):
|
||||
pending_path = path + ".pending.jsonl"
|
||||
record = dict(payload)
|
||||
record["save_error"] = str(error)
|
||||
record["queued_at"] = datetime.now(timezone.utc).isoformat()
|
||||
with open(pending_path, "a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
try:
|
||||
os.chmod(pending_path, 0o600)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def _existing_account_keys(target_path):
|
||||
keys = set()
|
||||
if not os.path.isfile(target_path):
|
||||
return keys
|
||||
with open(target_path, "r", encoding="utf-8", errors="replace") as handle:
|
||||
for raw_line in handle:
|
||||
parts = raw_line.rstrip("\n").split("----", 2)
|
||||
if len(parts) == 3:
|
||||
keys.add((parts[0].strip(), parts[2].strip()))
|
||||
return keys
|
||||
|
||||
|
||||
def retry_pending_file(pending_path, output_path=None, log_callback=None):
|
||||
logger = log_callback or (lambda message: None)
|
||||
pending_path = os.path.realpath(os.path.abspath(os.path.expanduser(str(pending_path))))
|
||||
if not os.path.isfile(pending_path):
|
||||
raise FileNotFoundError(f"pending 文件不存在: {pending_path}")
|
||||
suffix = ".pending.jsonl"
|
||||
if output_path:
|
||||
target_path = os.path.realpath(os.path.abspath(os.path.expanduser(str(output_path))))
|
||||
elif pending_path.endswith(suffix):
|
||||
target_path = os.path.realpath(pending_path[:-len(suffix)])
|
||||
else:
|
||||
target_path = os.path.realpath(pending_path + ".recovered.txt")
|
||||
if os.path.normcase(pending_path) == os.path.normcase(target_path):
|
||||
raise ValueError("pending 输入文件与输出文件不能是同一个文件")
|
||||
|
||||
lock_path = pending_path + ".lock"
|
||||
with FileLock(lock_path, timeout=30):
|
||||
if not os.path.isfile(pending_path):
|
||||
return {"restored": 0, "remaining": 0, "output_path": target_path}
|
||||
with open(pending_path, "r", encoding="utf-8") as handle:
|
||||
lines = handle.readlines()
|
||||
existing = _existing_account_keys(target_path)
|
||||
unresolved = []
|
||||
restored = 0
|
||||
for line_number, raw_line in enumerate(lines, 1):
|
||||
stripped = raw_line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
try:
|
||||
record = json.loads(stripped)
|
||||
if not isinstance(record, dict):
|
||||
raise ValueError("record must be a JSON object")
|
||||
email = str(record.get("email") or "").strip()
|
||||
password = str(record.get("password") or "")
|
||||
sso = str(record.get("sso") or "").strip()
|
||||
if not email or not sso:
|
||||
raise ValueError("record missing email or sso")
|
||||
key = (email, sso)
|
||||
if key not in existing:
|
||||
append_account_line(target_path, email, password, sso)
|
||||
existing.add(key)
|
||||
restored += 1
|
||||
logger(f"[+] 已恢复 pending 账号: {email}")
|
||||
except Exception as exc:
|
||||
unresolved.append(raw_line if raw_line.endswith("\n") else raw_line + "\n")
|
||||
logger(f"[!] pending 第 {line_number} 行恢复失败: {exc}")
|
||||
|
||||
directory = os.path.dirname(pending_path) or "."
|
||||
fd, temp_path = tempfile.mkstemp(prefix=".pending-retry-", suffix=".jsonl.tmp", dir=directory)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
handle.writelines(unresolved)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
if unresolved:
|
||||
os.replace(temp_path, pending_path)
|
||||
temp_path = None
|
||||
try:
|
||||
os.chmod(pending_path, 0o600)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
os.unlink(temp_path)
|
||||
temp_path = None
|
||||
try:
|
||||
os.unlink(pending_path)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
finally:
|
||||
if temp_path and os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
return {"restored": restored, "remaining": len(unresolved), "output_path": target_path}
|
||||
Reference in New Issue
Block a user