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:
60
app/main.py
60
app/main.py
@@ -24,6 +24,7 @@ from .openai_schema import (
|
||||
ModelsResponse,
|
||||
flatten_content,
|
||||
)
|
||||
from .session_bundle import encode_bundle, pack_workdir
|
||||
from .session_cache import SessionCache
|
||||
from .stats import StatsCollector, estimate_tokens
|
||||
|
||||
@@ -633,6 +634,65 @@ async def internal_auto_login_status():
|
||||
return {"ok": True, "instances": out}
|
||||
|
||||
|
||||
@app.post("/internal/session/export", dependencies=[Depends(auth_guard)])
|
||||
async def internal_session_export(instance: str | None = None):
|
||||
"""Export a logged-in Lingma session as a base64 tar.gz bundle.
|
||||
|
||||
The returned `bundle_b64` can be dropped into `LINGMA_SESSION_BUNDLE`
|
||||
(or the `session_bundle` field in `LINGMA_ACCOUNTS` JSON) on any other
|
||||
deployment to skip Playwright login entirely.
|
||||
|
||||
Safety:
|
||||
- Requires a valid API key.
|
||||
- Only works on instances that are currently authenticated (prevents
|
||||
exporting garbage from a half-initialised workDir).
|
||||
- Response is not streamed to logs; callers must store it themselves.
|
||||
"""
|
||||
p = _require_pool()
|
||||
target = None
|
||||
if instance:
|
||||
for inst in p.instances:
|
||||
if inst.name == instance:
|
||||
target = inst
|
||||
break
|
||||
if target is None:
|
||||
raise HTTPException(status_code=404, detail={"error": f"instance {instance} not found"})
|
||||
else:
|
||||
target = p.pick()
|
||||
|
||||
try:
|
||||
status = await target.client.auth_status()
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail={"error": f"instance {target.name} not ready: {exc}"},
|
||||
)
|
||||
if not (status and status.get("id")):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail={"error": f"instance {target.name} is not logged in"},
|
||||
)
|
||||
|
||||
try:
|
||||
raw = pack_workdir(target.cfg.work_dir)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail={"error": str(exc)})
|
||||
|
||||
bundle_b64 = encode_bundle(raw)
|
||||
logger.info(
|
||||
"session bundle exported from %s (%d bytes raw, %d bytes b64)",
|
||||
target.name,
|
||||
len(raw),
|
||||
len(bundle_b64),
|
||||
)
|
||||
return {
|
||||
"instance": target.name,
|
||||
"account": target.cfg.account.username or "",
|
||||
"raw_bytes": len(raw),
|
||||
"bundle_b64": bundle_b64,
|
||||
}
|
||||
|
||||
|
||||
@app.get("/internal/models/raw", dependencies=[Depends(auth_guard)])
|
||||
async def internal_models_raw(instance: str | None = None):
|
||||
"""Return the raw `config/queryModels` response from Lingma.
|
||||
|
||||
Reference in New Issue
Block a user