from __future__ import annotations import io from pathlib import Path import zipfile ERP_LOCAL_AGENT_VERSION = "1.0.0" ERP_LOCAL_AGENT_NAME = "ERP Local Agent" RUNTIME_ROOT = Path(__file__).resolve().parent / "local_agent_runtime" def build_agent_env(*, erp_base_url: str, node_code: str, node_secret: str, storage_root: str, tenant_id=None, branch_id=None, sync_interval_seconds: int = 30, request_timeout_seconds: int = 60, tunnel_enabled: bool = True, tunnel_reconnect_seconds: int = 10) -> str: erp_base_url = (erp_base_url or "").strip().rstrip("/") if erp_base_url.startswith("http://"): host = erp_base_url[7:].split("/", 1)[0].split(":", 1)[0].lower() if host not in {"localhost", "127.0.0.1", "::1"}: erp_base_url = "https://" + erp_base_url[7:] storage_root = (storage_root or r"D:\AuditFirmStorage").strip() return ( f"ERP_BASE_URL={erp_base_url}\n" f"NODE_CODE={(node_code or '').strip()}\n" f"NODE_SECRET={(node_secret or '').strip()}\n" f"STORAGE_ROOT={storage_root}\n" f"TENANT_ID={'' if tenant_id is None else tenant_id}\n" f"AUDIT_FIRM_ID={'' if tenant_id is None else tenant_id}\n" f"BRANCH_ID={'' if branch_id is None else branch_id}\n" f"SYNC_INTERVAL_SECONDS={int(sync_interval_seconds or 30)}\n" f"POLL_INTERVAL_SECONDS={int(sync_interval_seconds or 30)}\n" f"REQUEST_TIMEOUT_SECONDS={int(request_timeout_seconds or 60)}\n" f"TUNNEL_ENABLED={str(bool(tunnel_enabled)).lower()}\n" f"TUNNEL_RECONNECT_SECONDS={int(tunnel_reconnect_seconds or 10)}\n" "AUTO_UPDATE=true\n" "UPDATE_CHECK_INTERVAL_SECONDS=300\n" ) def _build_zip(*, env_text: str | None, include_env: bool, include_admin_readme: bool) -> bytes: if not RUNTIME_ROOT.exists(): raise RuntimeError(f"ERP Local Agent runtime is missing: {RUNTIME_ROOT}") buffer = io.BytesIO() with zipfile.ZipFile(buffer, "w", compression=zipfile.ZIP_DEFLATED) as dst: for path in sorted(RUNTIME_ROOT.rglob("*")): if not path.is_file() or "__pycache__" in path.parts: continue name = path.relative_to(RUNTIME_ROOT).as_posix() dst.writestr(name, path.read_bytes()) if include_env and env_text is not None: dst.writestr(".env", env_text) if include_admin_readme: dst.writestr("README_ERP_LOCAL_AGENT.txt", f"ERP Local Agent {ERP_LOCAL_AGENT_VERSION}\nStorage behaviour and D:\\AuditFirmStorage are preserved. Future compatible releases self-update securely from ERP.\n") return buffer.getvalue() def build_preconfigured_agent_zip(*, env_text: str, include_admin_readme: bool = False) -> bytes: return _build_zip(env_text=env_text, include_env=True, include_admin_readme=include_admin_readme) def build_agent_update_zip() -> bytes: # Update package deliberately excludes .env, .venv, data, logs and client files. return _build_zip(env_text=None, include_env=False, include_admin_readme=False)