#!/usr/bin/env python3 """le-app-design 证据账本:所有对上游 Skill 的真实调用经此包裹,落入 calls.jsonl。 用法: 执行型调用(CLI 类上游): python3 log_call.py --run-dir -- python3 /scripts/search.py "…" --design-system 装载型调用(文档型上游 / playbook): python3 log_call.py --run-dir --load /SKILL.md dep ∈ {ui-ux-pro-max, frontend-design, impeccable, le-app-design(自检)} --run-dir 缺省时按序: $LE_APP_DESIGN_RUN_DIR > /le-app-design-runs/-run/ 行为: - exec: 运行命令,透传 stdout/stderr,透传退出码;成功失败都记账。 - load: 读文件记 sha256 与字节数(证明用的是原文而非转述),文件缺失记 FAIL 退出 1。 - 记录字段: ts / dep / kind / command / exit_code / duration_ms / stdout_tail / stderr_tail / bytes / sha256 / note。stdout/stderr 仅保留尾部 4000 字符防账本膨胀。 仅 python3 标准库。 """ from __future__ import annotations import argparse import hashlib import json import os import subprocess import sys import time from datetime import datetime, timezone from pathlib import Path TAIL = 4000 KNOWN_DEPS = {"ui-ux-pro-max", "frontend-design", "impeccable", "le-app-design"} def default_run_dir() -> Path: env = os.environ.get("LE_APP_DESIGN_RUN_DIR") if env: return Path(env).expanduser() stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%SZ") return Path.cwd() / "le-app-design-runs" / f"{stamp}-run" def append_record(run_dir: Path, record: dict) -> Path: run_dir.mkdir(parents=True, exist_ok=True) ledger = run_dir / "calls.jsonl" with ledger.open("a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n") return ledger def confirm(dep: str, kind: str, status: str, ledger: Path, extra: str = "") -> None: print( f"[log_call] {dep} {kind} {status} → {ledger}" + (f" ({extra})" if extra else ""), file=sys.stderr, ) def main() -> int: # 手工在第一个裸 "--" 处切分:argparse 的 REMAINDER 会把 -- 之前的选项误吞进命令 argv = sys.argv[1:] cmd: list[str] = [] if "--" in argv: split = argv.index("--") argv, cmd = argv[:split], argv[split + 1:] ap = argparse.ArgumentParser(description="le-app-design 调用账本") ap.add_argument("dep", help="上游标识: ui-ux-pro-max | frontend-design | impeccable | le-app-design") ap.add_argument("--run-dir", default=None, help="运行目录(缺省自动创建)") ap.add_argument("--note", default="", help="阶段/用途备注,如 S2-design-system") ap.add_argument("--load", metavar="PATH", default=None, help="装载型调用:记录读取的原文文件") args = ap.parse_args(argv) dep = args.dep if dep not in KNOWN_DEPS: print(f"[log_call] 未知依赖标识 {dep!r};允许: {sorted(KNOWN_DEPS)}", file=sys.stderr) return 2 run_dir = Path(args.run_dir).expanduser() if args.run_dir else default_run_dir() base = {"ts": datetime.now(timezone.utc).isoformat(timespec="seconds"), "dep": dep, "note": args.note} if args.load: path = Path(args.load).expanduser() rec = dict(base, kind="load", command=f"load:{path}") try: data = path.read_bytes() except OSError as e: rec.update(exit_code=1, stderr_tail=f"read failed: {e}"[:TAIL]) ledger = append_record(run_dir, rec) confirm(dep, "load", "FAIL", ledger, str(e)) return 1 rec.update( exit_code=0, bytes=len(data), sha256=hashlib.sha256(data).hexdigest(), ) ledger = append_record(run_dir, rec) confirm(dep, "load", "OK", ledger, f"{len(data)} bytes sha256:{rec['sha256'][:12]}…") return 0 if not cmd: print("[log_call] 需要 --load PATH 或 -- <命令…>", file=sys.stderr) return 2 rec = dict(base, kind="exec", command=json.dumps(cmd, ensure_ascii=False)) t0 = time.monotonic() try: proc = subprocess.run( cmd, capture_output=True, text=True, timeout=900, env=dict(os.environ, PYTHONDONTWRITEBYTECODE="1"), ) rc, out, err = proc.returncode, proc.stdout or "", proc.stderr or "" except subprocess.TimeoutExpired as e: rc, out, err = 124, (e.stdout or b"").decode(errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or ""), f"timeout after {e.timeout}s" except FileNotFoundError as e: rc, out, err = 127, "", f"command not found: {e}" rec.update( exit_code=rc, duration_ms=int((time.monotonic() - t0) * 1000), stdout_tail=out[-TAIL:], stderr_tail=err[-TAIL:], ) ledger = append_record(run_dir, rec) if out: sys.stdout.write(out) if not out.endswith("\n"): sys.stdout.write("\n") if err: sys.stderr.write(err) if not err.endswith("\n"): sys.stderr.write("\n") confirm(dep, "exec", f"exit={rc}", ledger, f"{rec['duration_ms']}ms") return rc if __name__ == "__main__": sys.exit(main())