#!/usr/bin/env python3 """Append a generated manual-invoice Markdown report to a user-approved Lark document.""" from __future__ import annotations import argparse import datetime as dt import hashlib import json import os import shutil import subprocess from pathlib import Path from urllib.parse import urlparse, urlunparse ALLOWED_HOST_SUFFIXES = (".feishu.cn", ".larksuite.com") ALLOWED_DOCUMENT_PATHS = ("/docx/", "/docs/", "/wiki/") def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Publish a generated Markdown report to an existing Lark/Feishu document." ) parser.add_argument("--report", type=Path, required=True) parser.add_argument("--document-url", required=True) parser.add_argument("--metadata-output", type=Path, required=True) parser.add_argument( "--mode", choices=("append", "overwrite"), default="append", help="Append by default; use overwrite only after explicit user authorization.", ) parser.add_argument("--identity", choices=("user", "bot"), default="user") parser.add_argument("--timeout", type=int, default=120) parser.add_argument("--dry-run", action="store_true") return parser.parse_args() def validate_document_url(value: str) -> str: parsed = urlparse(value.strip()) hostname = (parsed.hostname or "").lower() allowed_host = hostname in {"feishu.cn", "larksuite.com"} or hostname.endswith( ALLOWED_HOST_SUFFIXES ) if parsed.scheme != "https" or not allowed_host: raise ValueError("Document link must be an HTTPS Feishu or Lark document URL.") if not any(segment in parsed.path for segment in ALLOWED_DOCUMENT_PATHS): raise ValueError("Document link must point to a docx, docs, or wiki document.") return urlunparse((parsed.scheme, parsed.netloc, parsed.path, "", "", "")) def sha256_file(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def safe_response_summary(stdout: str) -> dict[str, object]: try: value = json.loads(stdout) except json.JSONDecodeError: return {"json_response": False, "response_received": bool(stdout.strip())} if not isinstance(value, dict): return {"json_response": True, "response_received": True} summary: dict[str, object] = {"json_response": True, "response_received": True} for key in ("ok", "code", "msg", "message", "success"): if key in value and isinstance(value[key], (str, int, float, bool, type(None))): summary[key] = value[key] error = value.get("error") if isinstance(error, dict) and isinstance(error.get("type"), str): summary["error_type"] = error["type"] return summary def write_private_json(path: Path, value: dict[str, object]) -> None: path = path.expanduser().resolve() path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) temporary = path.with_name(f".{path.name}.tmp") temporary.write_text( json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" ) os.chmod(temporary, 0o600) os.replace(temporary, path) os.chmod(path, 0o600) def main() -> int: os.umask(0o077) args = parse_args() report = args.report.expanduser().resolve() if report.is_symlink() or not report.is_file(): raise ValueError(f"Report must be a regular file: {report}") document_url = validate_document_url(args.document_url) lark_cli = shutil.which("lark-cli") if not lark_cli: raise FileNotFoundError("lark-cli is not installed or is not available on PATH.") command = [ lark_cli, "docs", "+update", "--as", args.identity, "--doc", document_url, "--markdown", f"@./{report.name}", "--mode", args.mode, ] if args.dry_run: command.append("--dry-run") completed = subprocess.run( command, capture_output=True, text=True, cwd=report.parent, timeout=args.timeout, check=False, ) metadata: dict[str, object] = { "format": "manual-invoice-lark-publication/v1", "status": ( "dry_run" if args.dry_run and completed.returncode == 0 else "published" if completed.returncode == 0 else "error" ), "attempted_at_utc": dt.datetime.now(dt.timezone.utc).isoformat(), "document_url": document_url, "update_mode": args.mode, "identity": args.identity, "report_path": str(report), "report_size_bytes": report.stat().st_size, "report_sha256": sha256_file(report), "lark_cli_path": lark_cli, "dry_run": args.dry_run, "exit_code": completed.returncode, "response": safe_response_summary(completed.stdout), } if completed.returncode != 0: metadata["error"] = "lark-cli document update failed; inspect the interactive output." write_private_json(args.metadata_output, metadata) print(json.dumps(metadata, ensure_ascii=False)) return 0 if completed.returncode == 0 else 3 if __name__ == "__main__": raise SystemExit(main())