feat: session bundle import/export to skip Playwright auto-login
Adds a lightweight way to pre-seed a Lingma workDir with an existing logged-in session: - New module session_bundle.py packs/unpacks only the four cache files that make up a Lingma login (id, user, quota, config.json). Everything else (db, logs, index, diagnosis) stays local so bundles stay tiny and never leak session-specific artefacts. - Safety: path-traversal/symlink members are rejected; size is capped; refuses to export from a workDir that isn't actually logged in; sensitive cache/user is chmod'd 0600 on restore. - LingmaAccount gains optional session_bundle_b64 / session_bundle_file; LINGMA_SESSION_BUNDLE[_FILE] env provide the singleton fallback. Credentials become optional when a bundle is supplied. - LingmaPool.start() restores the bundle into each instance workDir only if it isn't already logged in, so persistent volumes aren't clobbered and a corrupt bundle falls back to Playwright gracefully. - POST /internal/session/export returns the bundle as base64; ?instance= selects a specific pool instance. Requires an authed, already-logged-in instance to prevent exporting empties. - README + .env.example document the end-to-end flow. Made-with: Cursor
This commit is contained in:
@@ -8,6 +8,12 @@ from .auto_login import AutoLoginManager
|
||||
from .config import LingmaAccount
|
||||
from .lingma_client import LingmaGatewayClient
|
||||
from .logging_config import get_logger
|
||||
from .session_bundle import (
|
||||
apply_bundle_to_workdir,
|
||||
decode_bundle,
|
||||
is_logged_in_workdir,
|
||||
resolve_bundle_b64,
|
||||
)
|
||||
|
||||
|
||||
logger = get_logger("lingma_gateway.pool")
|
||||
@@ -183,20 +189,67 @@ class LingmaPool:
|
||||
pool-mode we skip it anyway, but Lingma may still write there internally)
|
||||
and keeps docker logs readable. Failures are non-fatal; per-instance
|
||||
reconnect loops will take over.
|
||||
|
||||
Before spawning each Lingma process we optionally restore a pre-captured
|
||||
session bundle into the workDir, which lets us skip Playwright login
|
||||
entirely on a fresh volume.
|
||||
"""
|
||||
for inst in self._instances:
|
||||
self._maybe_apply_session_bundle(inst)
|
||||
logger.info(
|
||||
"pool starting %s (workDir=%s port=%d account=%s)",
|
||||
"pool starting %s (workDir=%s port=%d account=%s bundle=%s logged_in=%s)",
|
||||
inst.name,
|
||||
inst.cfg.work_dir,
|
||||
inst.cfg.socket_port,
|
||||
inst.cfg.account.username or "<empty>",
|
||||
bool(
|
||||
inst.cfg.account.session_bundle_b64
|
||||
or inst.cfg.account.session_bundle_file
|
||||
),
|
||||
is_logged_in_workdir(inst.cfg.work_dir),
|
||||
)
|
||||
try:
|
||||
await inst.client.start()
|
||||
except Exception as exc:
|
||||
logger.warning("pool start %s failed: %s", inst.name, exc)
|
||||
|
||||
@staticmethod
|
||||
def _maybe_apply_session_bundle(inst: "PoolInstance") -> None:
|
||||
"""Restore an exported Lingma session into inst.work_dir, if needed.
|
||||
|
||||
Skipped when:
|
||||
- the workDir already looks logged in (persistent volume case);
|
||||
- no bundle is configured.
|
||||
"""
|
||||
acc = inst.cfg.account
|
||||
if is_logged_in_workdir(inst.cfg.work_dir):
|
||||
return
|
||||
|
||||
b64 = resolve_bundle_b64(
|
||||
inline=acc.session_bundle_b64 or None,
|
||||
file_path=acc.session_bundle_file or None,
|
||||
)
|
||||
if not b64:
|
||||
return
|
||||
|
||||
try:
|
||||
raw = decode_bundle(b64)
|
||||
restored = apply_bundle_to_workdir(inst.cfg.work_dir, raw)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"pool %s: failed to apply session bundle, will fall back to auto-login: %s",
|
||||
inst.name,
|
||||
exc,
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"pool %s: applied session bundle (%d files: %s)",
|
||||
inst.name,
|
||||
len(restored),
|
||||
",".join(restored),
|
||||
)
|
||||
|
||||
async def close(self) -> None:
|
||||
tasks = [asyncio.create_task(inst.client.close()) for inst in self._instances]
|
||||
for t in tasks:
|
||||
|
||||
Reference in New Issue
Block a user