Files
obsidian-wiki/scripts/analyze_email_jsonl.py
nac 777607dc29 chore(repo): establish project baseline
Track the source-of-truth code, skills, bootstrap rules, docs, and packaging files before adding new features.

Exclude local vault config, generated skill mirrors, temporary exports, virtual environments, and business data files from version control.

Validation: inspected staged file list and git ignored status before commit.
2026-07-05 11:03:40 +00:00

172 lines
5.0 KiB
Python

#!/usr/bin/env python3
import collections
import json
import re
import sys
from pathlib import Path
PART_RE = re.compile(
r"\b(?:\d{6}-\d{4,6}(?:-HF)?|\d{3,}-\d{3,}(?:-HF)?|FH\d+[A-Z0-9-]*|C-\d{7}|\d{6,})\b",
re.I,
)
KEYWORDS = [
"报价",
"询价",
"样品",
"打样",
"图纸",
"承认书",
"规格书",
"替代",
"替换",
"竞品",
"Hirose",
"Molex",
"TE",
"泰科",
"JAE",
"Kyocera",
"IRISO",
"订单",
"交期",
"交货",
"品质",
"客诉",
"投诉",
"不良",
"8D",
"退货",
"索赔",
"对账",
"付款",
"发票",
"合同",
"送样",
"开模",
"开发",
"零跑",
"领跑",
"维胜",
"恒润",
"涵润",
"安悦",
"先锋",
]
def safe_person(person):
if not isinstance(person, dict):
return None, None
email = (person.get("email") or "").lower()
name = (person.get("name") or "").strip()
domain = email.split("@")[-1] if "@" in email else None
return domain, name if name and len(name) <= 30 else None
def main() -> int:
if len(sys.argv) != 2:
print("usage: analyze_email_jsonl.py /path/to/llm_input.jsonl", file=sys.stderr)
return 2
path = Path(sys.argv[1])
threads = msgs = attach = trunc = 0
kw = collections.Counter()
parts = collections.Counter()
domains = collections.Counter()
names = collections.Counter()
years = collections.Counter()
months = collections.Counter()
attach_ext = collections.Counter()
interesting = []
with path.open(encoding="utf-8", errors="replace") as fh:
for line in fh:
obj = json.loads(line)
threads += 1
trunc += bool(obj.get("body_truncated"))
messages = obj.get("messages", [])
subject = obj.get("suggested_title_from_subject", "")
text = " ".join(
[subject]
+ [
(m.get("subject") or "") + " " + (m.get("body_text") or "")
for m in messages
]
)
lowered = text.lower()
for key in KEYWORDS:
if key.lower() in lowered:
kw[key] += 1
found_parts = [x.upper() for x in PART_RE.findall(text)]
for part in found_parts:
if len(part) >= 6:
parts[part] += 1
for msg in messages:
msgs += 1
month = (msg.get("date") or "")[:7]
if len(month) == 7:
months[month] += 1
years[month[:4]] += 1
people = [msg.get("from", {})] + msg.get("to", []) + msg.get("cc", [])
for person in people:
domain, name = safe_person(person)
if domain:
domains[domain] += 1
if name:
names[name] += 1
for item in msg.get("attachments", []) or []:
attach += 1
if isinstance(item, dict):
filename = item.get("filename") or item.get("name") or ""
else:
filename = str(item)
ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "[no_ext]"
attach_ext[ext] += 1
score = (
sum(lowered.count(key.lower()) for key in KEYWORDS)
+ len(found_parts) * 2
+ len(messages) // 3
+ len(obj.get("attachments", [])) * 2
)
if score > 8:
interesting.append(
{
"score": score,
"date_end": obj.get("date_end", ""),
"thread_id": obj.get("thread_id", ""),
"subject": subject[:120],
"message_count": len(messages),
"attachment_count": len(obj.get("attachments", [])),
"parts": found_parts[:12],
"keywords": [
key for key in KEYWORDS if key.lower() in lowered
][:12],
}
)
result = {
"source": str(path),
"threads": threads,
"messages": msgs,
"attachments": attach,
"truncated_threads": trunc,
"years": years.most_common(),
"months_top": months.most_common(18),
"keywords": kw.most_common(50),
"parts_top": parts.most_common(80),
"domains_top": domains.most_common(60),
"names_top": names.most_common(60),
"attachment_extensions": attach_ext.most_common(40),
"interesting_threads": sorted(
interesting, key=lambda x: (x["score"], x["date_end"]), reverse=True
)[:120],
}
json.dump(result, sys.stdout, ensure_ascii=False, indent=2)
print()
return 0
if __name__ == "__main__":
raise SystemExit(main())