diff --git a/TOOLS.md b/TOOLS.md index 2fd3ed5..6f52c70 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -89,6 +89,8 @@ agent-browser close ./email-processor.sh review accept # 接受所有建议 ./email-processor.sh review 1 delete # 处理第1封(删除) ./email-processor.sh stats # 查看统计 +./email-processor.sh digest # 今日处理摘要 +./email-processor.sh digest --recent 3 # 最近3天摘要 ``` **置信度机制**: diff --git a/scripts/email_processor/README.md b/scripts/email_processor/README.md index 11b0c56..fbf6446 100644 --- a/scripts/email_processor/README.md +++ b/scripts/email_processor/README.md @@ -60,6 +60,7 @@ The system separates **classification** (what the LLM does) from **confidence** - **Repeat senders** with consistent tag signatures reach 85%+ confidence and get auto-acted during `scan`. They never touch the pending queue. - **New or ambiguous senders** start at 50% and get queued. - **You occasionally run `review list`** to handle stragglers — each decision further builds history. +- **`digest` gives a quick glance** at what was processed recently — subject lines grouped by action, with `[auto]`/`[user]` markers. - **`stats` shows your automation rate** climbing over time. ### Confidence Computation @@ -96,6 +97,10 @@ chmod +x email-processor.sh ./email-processor.sh review all delete # delete all pending ./email-processor.sh review accept # accept all suggestions +# --- Digest --- +./email-processor.sh digest # today's processed emails +./email-processor.sh digest --recent 3 # last 3 days + # --- Other --- ./email-processor.sh stats # show decision history ``` @@ -244,13 +249,16 @@ ollama list # should show kamekichi128/qwen3-4b-instruct-2507:latest # 7. Check that the decision was recorded ./email-processor.sh stats + +# 8. Quick glance at what was processed today +./email-processor.sh digest ``` ## File Structure ``` email_processor/ - main.py # Entry point — scan/review/stats subcommands + main.py # Entry point — scan/review/stats/digest subcommands classifier.py # LLM prompt builder + response parser, tag taxonomy decision_store.py # Decision history, confidence computation, few-shot retrieval config.json # Ollama + automation settings diff --git a/scripts/email_processor/decision_store.py b/scripts/email_processor/decision_store.py index 2542373..7e08446 100644 --- a/scripts/email_processor/decision_store.py +++ b/scripts/email_processor/decision_store.py @@ -14,7 +14,7 @@ auto decision. import json import re -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path from collections import Counter @@ -200,6 +200,33 @@ def get_known_labels(): return labels +def get_recent_decisions(days=1): + """Return recent decisions grouped by action. + + Args: + days: number of days to look back (default 1 = today). + + Returns: + dict of action -> list of entries, e.g. {"delete": [...], "archive": [...]}. + Returns empty dict if no decisions found in the period. + """ + history = _load_history() + if not history: + return {} + + cutoff = datetime.now() - timedelta(days=days) + grouped = {} + for entry in history: + try: + ts = datetime.fromisoformat(entry["timestamp"]) + except (KeyError, ValueError): + continue + if ts >= cutoff: + action = entry.get("action", "unknown") + grouped.setdefault(action, []).append(entry) + return grouped + + def get_all_stats(): """Compute aggregate statistics across the full decision history. diff --git a/scripts/email_processor/email-processor.sh b/scripts/email_processor/email-processor.sh index a2d850d..d83f6ef 100755 --- a/scripts/email_processor/email-processor.sh +++ b/scripts/email_processor/email-processor.sh @@ -11,6 +11,8 @@ # ./email-processor.sh review all delete # act on all pending # ./email-processor.sh review accept # accept all suggestions # ./email-processor.sh stats # show history stats +# ./email-processor.sh digest # today's processed emails +# ./email-processor.sh digest --recent 3 # last 3 days # # Requires: uv, himalaya, Ollama running with model. diff --git a/scripts/email_processor/main.py b/scripts/email_processor/main.py index 2497a25..34c1379 100644 --- a/scripts/email_processor/main.py +++ b/scripts/email_processor/main.py @@ -19,6 +19,8 @@ Subcommands: python main.py review all # act on all pending python main.py review accept # accept all suggestions python main.py stats # show decision history + python main.py digest # today's processed emails + python main.py digest --recent 3 # last 3 days Action mapping (what each classification does to the email): delete -> himalaya message delete (moves to Trash) @@ -633,6 +635,56 @@ def cmd_stats(): print(f"\nKnown labels: {', '.join(sorted(labels))}") +# --------------------------------------------------------------------------- +# Subcommand: digest +# --------------------------------------------------------------------------- + +def cmd_digest(days=1): + """Print a compact summary of recently processed emails, grouped by action. + + Shows both auto and user decisions with a marker to distinguish them. + """ + grouped = decision_store.get_recent_decisions(days) + + if not grouped: + period = "today" if days == 1 else f"last {days} days" + print(f"No processed emails in this period ({period}).") + return + + period_label = "today" if days == 1 else f"last {days} days" + print(f"Email digest ({period_label})") + print("=" * 40) + + # Map action names to display labels + action_labels = { + "delete": "Deleted", + "archive": "Archived", + "keep": "Kept", + "mark_read": "Marked read", + } + + total = 0 + auto_count = 0 + user_count = 0 + + for action, entries in sorted(grouped.items()): + label = action_labels.get(action, action.replace("label:", "Labeled ").title()) + print(f"\n{label} ({len(entries)}):") + for entry in entries: + source = entry.get("source", "?") + sender = entry.get("sender", "unknown") + subject = entry.get("subject", "(no subject)") + print(f" [{source}] {sender}") + print(f" {subject}") + total += 1 + if source == "auto": + auto_count += 1 + else: + user_count += 1 + + print(f"\nTotal: {total} emails processed ({auto_count} auto, {user_count} user)") + + # --------------------------------------------------------------------------- # Entry point & argument parsing # @@ -695,7 +747,10 @@ if __name__ == "__main__": elif subcommand == "stats": cmd_stats() + elif subcommand == "digest": + cmd_digest(days=recent if recent else 1) + else: print(f"Unknown subcommand: {subcommand}") - print("Usage: python main.py [scan|review|stats] [--recent N] [--dry-run]") + print("Usage: python main.py [scan|review|stats|digest] [--recent N] [--dry-run]") sys.exit(1)