Files
obsidian-wiki/scripts/extract_email_project_signals.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

170 lines
4.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
import collections
import json
import re
import sys
from pathlib import Path
OEM_TERMS = [
"零跑",
"Leapmotor",
"BYD",
"比亚迪",
"Toyota",
"SVW",
"上汽大众",
"MZD",
"Mazda",
"VW",
"STLA",
"Stellantis",
"Ford",
"Honda",
"Hyundai",
"Kia",
"CATARC",
"Yazaki",
"Marelli",
"Valeo",
"Visteon",
]
APP_TERMS = [
"吸顶屏",
"显示",
"显示屏",
"CID",
"FID",
"Cluster",
"仪表",
"Full Digital Cluster",
"氛围灯",
"Ambient Lighting",
"储能",
"车灯",
"lighting",
"霍尔小板",
"排线",
"FPC",
"FFC",
"Fakra",
"FAKRA",
"Socket Header",
"Pin Header",
"SMB",
"BTB",
"WTB",
"Wafer",
]
PRODUCT_RE = re.compile(
r"\b(?:\d{6}-\d{4,6}(?:-HF)?|\d{3,}-\d{3,}(?:-HF)?|FH\d+[A-Z0-9-]*|C-\d{7})\b",
re.I,
)
PROJECT_RE = re.compile(
r"(?:项目名称[::]\s*([^\n\r;;,,]{2,60})|项目车型[::]\s*([^\n\r;;,,]{2,60})|"
r"\b(?:eP|EP)\s*\d{4,6}\b|CU\d+|BR\d+|MZD[-\w]+|Scout|RUV|PICKUP)",
re.I,
)
def domain_of(person):
if not isinstance(person, dict):
return None
email = (person.get("email") or "").lower()
return email.split("@")[-1] if "@" in email else None
def context_snippet(text, term, limit=130):
idx = text.lower().find(term.lower())
if idx < 0:
return ""
start = max(0, idx - 45)
end = min(len(text), idx + len(term) + 85)
s = re.sub(r"\s+", " ", text[start:end]).strip()
return s[:limit]
def main():
if len(sys.argv) != 2:
print("usage: extract_email_project_signals.py /path/to/llm_input.jsonl", file=sys.stderr)
return 2
path = Path(sys.argv[1])
pairs = collections.Counter()
oems = collections.Counter()
apps = collections.Counter()
products = collections.Counter()
domains = collections.Counter()
rows = []
with path.open(encoding="utf-8", errors="replace") as fh:
for line in fh:
obj = json.loads(line)
messages = obj.get("messages", [])
text = " ".join(
[obj.get("suggested_title_from_subject", "")]
+ [(m.get("subject") or "") + " " + (m.get("body_text") or "") for m in messages]
)
found_oems = [x for x in OEM_TERMS if x.lower() in text.lower()]
found_apps = [x for x in APP_TERMS if x.lower() in text.lower()]
found_products = [x.upper() for x in PRODUCT_RE.findall(text)]
project_hits = []
for m in PROJECT_RE.finditer(text):
project_hits.append(next((g for g in m.groups() if g), m.group(0)))
if not (found_oems or found_apps or project_hits):
continue
thread_domains = collections.Counter()
for msg in messages:
for person in [msg.get("from", {})] + msg.get("to", []) + msg.get("cc", []):
d = domain_of(person)
if d:
thread_domains[d] += 1
for d, n in thread_domains.items():
domains[d] += n
for x in found_oems:
oems[x] += 1
for x in found_apps:
apps[x] += 1
for x in found_products[:25]:
products[x] += 1
for oem in found_oems:
for app in found_apps:
pairs[(oem, app)] += 1
score = len(found_oems) * 4 + len(found_apps) * 2 + len(project_hits) * 3 + len(found_products)
if score >= 8:
anchor = (found_oems + found_apps + project_hits + found_products)[:1]
rows.append(
{
"thread_id": obj.get("thread_id"),
"subject": obj.get("suggested_title_from_subject", "")[:160],
"date_end": obj.get("date_end"),
"customer_domains": [d for d, _ in thread_domains.most_common(5)],
"oems": found_oems[:8],
"applications": found_apps[:10],
"projects": project_hits[:8],
"products": found_products[:12],
"snippet": context_snippet(text, anchor[0]) if anchor else "",
}
)
out = {
"oems_top": oems.most_common(50),
"applications_top": apps.most_common(50),
"products_top": products.most_common(80),
"oem_application_pairs_top": [
{"oem": k[0], "application": k[1], "threads": v}
for k, v in pairs.most_common(80)
],
"customer_domains_top": domains.most_common(50),
"project_signal_threads": sorted(rows, key=lambda x: x["date_end"] or "", reverse=True)[:120],
}
json.dump(out, sys.stdout, ensure_ascii=False, indent=2)
print()
return 0
if __name__ == "__main__":
raise SystemExit(main())