openclaw-diag-cli 0.1.3 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +83 -71
  2. package/bin/ocdiag +0 -1
  3. package/bin/openclaw-diag.js +65 -176
  4. package/diag/01_sys_health.py +0 -2
  5. package/diag/02_environment.py +32 -6
  6. package/diag/03_configuration.py +4 -1
  7. package/diag/04_gateway.py +30 -8
  8. package/diag/05_recent_errors.py +24 -14
  9. package/diag/06_cron_jobs.py +4 -41
  10. package/diag/07_performance.py +114 -42
  11. package/diag/08_sessions.py +2 -54
  12. package/diag/09_plugin_diag.py +52 -25
  13. package/diag/10_shell_history.py +28 -10
  14. package/lib/__pycache__/bundle.cpython-310.pyc +0 -0
  15. package/lib/bundle.py +6 -13
  16. package/ocdiag/__init__.py +1 -1
  17. package/ocdiag/__pycache__/__init__.cpython-310.pyc +0 -0
  18. package/ocdiag/__pycache__/cli.cpython-310.pyc +0 -0
  19. package/ocdiag/__pycache__/dispatcher.cpython-310.pyc +0 -0
  20. package/ocdiag/__pycache__/doctor.cpython-310.pyc +0 -0
  21. package/ocdiag/__pycache__/jsonlog.cpython-310.pyc +0 -0
  22. package/ocdiag/__pycache__/output.cpython-310.pyc +0 -0
  23. package/ocdiag/__pycache__/paths.cpython-310.pyc +0 -0
  24. package/ocdiag/__pycache__/recent_logs.cpython-310.pyc +0 -0
  25. package/ocdiag/__pycache__/sensitive.cpython-310.pyc +0 -0
  26. package/ocdiag/__pycache__/sessions.cpython-310.pyc +0 -0
  27. package/ocdiag/__pycache__/timeutil.cpython-310.pyc +0 -0
  28. package/ocdiag/__pycache__/tokens.cpython-310.pyc +0 -0
  29. package/ocdiag/cli.py +16 -1
  30. package/ocdiag/dispatcher.py +140 -53
  31. package/ocdiag/doctor.py +162 -0
  32. package/ocdiag/jsonlog.py +0 -5
  33. package/ocdiag/paths.py +0 -17
  34. package/ocdiag/recent_logs.py +0 -3
  35. package/ocdiag/sensitive.py +95 -1
  36. package/ocdiag/sessions.py +161 -0
  37. package/ocdiag/timeutil.py +0 -11
  38. package/ocdiag/tokens.py +0 -4
  39. package/package.json +2 -2
  40. package/tools/oc_session_extract.py +190 -67
  41. package/tools/oc_session_trace.py +48 -46
@@ -21,7 +21,7 @@ from typing import Any, Dict, List, Optional, Tuple
21
21
 
22
22
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
23
23
 
24
- from ocdiag import paths
24
+ from ocdiag import paths, sessions
25
25
 
26
26
 
27
27
  DEFAULT_BASE_DIR = paths.SESSIONS_BASE
@@ -52,14 +52,6 @@ def fmt_duration(ms: float) -> str:
52
52
  return f"{m}m{s:.1f}s"
53
53
 
54
54
 
55
- def human_size(n: int) -> str:
56
- for unit in ("B", "KB", "MB", "GB"):
57
- if n < 1024:
58
- return f"{n:.1f} {unit}" if unit != "B" else f"{n} {unit}"
59
- n /= 1024
60
- return f"{n:.1f} TB"
61
-
62
-
63
55
  def extract_text(content: Any) -> str:
64
56
  if isinstance(content, str):
65
57
  return content
@@ -75,41 +67,25 @@ def extract_text(content: Any) -> str:
75
67
  return str(content)
76
68
 
77
69
 
78
- def find_session_file(
70
+ def resolve_session_file(
79
71
  session_id: str,
80
72
  base_dir: str = DEFAULT_BASE_DIR,
81
73
  agent: Optional[str] = None,
82
- ) -> Optional[str]:
83
- if agent:
84
- agent_dirs = [os.path.join(base_dir, agent)]
85
- else:
86
- agent_dirs = sorted(glob.glob(os.path.join(base_dir, "*")))
87
-
88
- candidates: List[Tuple[str, str]] = []
89
- for ad in agent_dirs:
90
- sd = os.path.join(ad, "sessions")
91
- if not os.path.isdir(sd):
92
- continue
93
- for entry in os.listdir(sd):
94
- if not entry.startswith(session_id):
95
- continue
96
- if ".trajectory" in entry or entry.endswith(".json"):
97
- continue
98
- full = os.path.join(sd, entry)
99
- if not os.path.isfile(full):
100
- continue
101
- if entry == f"{session_id}.jsonl":
102
- candidates.append((full, "active"))
103
- elif ".jsonl.deleted." in entry:
104
- candidates.append((full, "deleted"))
105
- elif ".jsonl.reset." in entry:
106
- candidates.append((full, "reset"))
107
- elif ".jsonl.bak-" in entry:
108
- candidates.append((full, "backup"))
109
-
110
- prio = {"active": 0, "deleted": 1, "reset": 2, "backup": 3}
111
- candidates.sort(key=lambda x: prio.get(x[1], 9))
112
- return candidates[0][0] if candidates else None
74
+ ) -> Tuple[Optional[str], List[str]]:
75
+ """Resolve UUID-or-prefix to a single session file path.
76
+
77
+ Returns ``(path, candidates)``. ``path`` is None on miss or ambiguity;
78
+ ``candidates`` is non-empty only when the prefix matched multiple
79
+ distinct session UUIDs.
80
+ """
81
+ files, candidates = sessions.resolve(
82
+ session_id, base_dir=base_dir, agent=agent, include_transient=False,
83
+ )
84
+ if candidates:
85
+ return None, candidates
86
+ if not files:
87
+ return None, []
88
+ return files[0][0], []
113
89
 
114
90
 
115
91
  def find_trajectory_file(session_file: str) -> Optional[str]:
@@ -634,6 +610,7 @@ def format_json(session_id, session_file, user_msg_index, user_msg_id, analysis,
634
610
 
635
611
  def main():
636
612
  parser = argparse.ArgumentParser(
613
+ prog=os.environ.get("OPENCLAW_DIAG_PROG") or None,
637
614
  description="Trace the processing timeline of a user message in an OpenClaw session.",
638
615
  formatter_class=argparse.ArgumentDefaultsHelpFormatter,
639
616
  )
@@ -650,12 +627,37 @@ def main():
650
627
  parser.add_argument("--json", action="store_true", help="Output as structured JSON")
651
628
  args = parser.parse_args()
652
629
 
653
- session_file = find_session_file(args.session_id, args.base_dir, args.agent)
630
+ ok, msg = sessions.is_valid_query(args.session_id)
631
+ if not ok:
632
+ print(f"Error: {msg}", file=sys.stderr)
633
+ sys.exit(2)
634
+ session_file, candidates = resolve_session_file(
635
+ args.session_id, args.base_dir, args.agent,
636
+ )
637
+ if candidates:
638
+ print(
639
+ f"Error: 前缀 '{args.session_id}' 匹配多个 session(请补长前缀):",
640
+ file=sys.stderr,
641
+ )
642
+ for sid in candidates:
643
+ print(f" {sid}", file=sys.stderr)
644
+ sys.exit(1)
654
645
  if not session_file:
655
- print(f"Error: no session file found for '{args.session_id}' under {args.base_dir}",
646
+ print(f"Error: 找不到 session '{args.session_id}'(在 {args.base_dir} 下)",
656
647
  file=sys.stderr)
648
+ suggestions = sessions.recent_session_ids(args.base_dir, limit=5)
649
+ if suggestions:
650
+ print(f" 最近的 5 个 session:", file=sys.stderr)
651
+ for sid in suggestions:
652
+ print(f" {sid}", file=sys.stderr)
653
+ print(f" 提示:UUID 完整 36 位,前缀也可(至少 8 位)。", file=sys.stderr)
657
654
  sys.exit(1)
658
655
 
656
+ # If the user passed a prefix, recover the full UUID from the resolved
657
+ # filename so log lookups and JSON output use the canonical id.
658
+ resolved_basename = os.path.basename(session_file)
659
+ full_session_id = resolved_basename.split(".jsonl", 1)[0]
660
+
659
661
  records = load_records(session_file)
660
662
  if not records:
661
663
  print(f"Error: session file is empty: {session_file}", file=sys.stderr)
@@ -686,13 +688,13 @@ def main():
686
688
  if not args.no_log:
687
689
  log_files = find_gateway_logs(args.log_dir)
688
690
  if log_files:
689
- gw_info = load_gateway_timing(log_files, args.session_id, analysis["base_epoch_ms"])
691
+ gw_info = load_gateway_timing(log_files, full_session_id, analysis["base_epoch_ms"])
690
692
 
691
693
  if args.json:
692
- out_str = format_json(args.session_id, session_file, user_msg_ordinal,
694
+ out_str = format_json(full_session_id, session_file, user_msg_ordinal,
693
695
  user_msg_id, analysis, traj_info, gw_info)
694
696
  else:
695
- out_str = format_text(args.session_id, user_msg_ordinal, user_msg_id,
697
+ out_str = format_text(full_session_id, user_msg_ordinal, user_msg_id,
696
698
  analysis, traj_info, gw_info)
697
699
 
698
700
  if args.output: