feat: классификация писем Qwen3:8b + обработчики (urgent→TG, task→VTODO, meeting→VEVENT)

- mail_archive.py: фикс вложений himalaya --dir → --downloads-dir; attachments/ только при has_attachment; идемпотентно
- email_classifier.py: Qwen3:8b (Ollama) → теги info/urgent/task/meeting в frontmatter email.md
- email_handlers.py: urgent→Telegram (Bot API+SOCKS5), task→Radicale VTODO, meeting→Radicale VEVENT; handled_* идемпотентность; --dry-run
- .gitignore: игнор *.env.bak*
- openspec: чейндж email-classification-handlers (в работе)
- STATUS/TODO/WALKTHROUGH: прогресс сессии, подводные камни Radicale (http.client, Depth:1, формат дат)
This commit is contained in:
2026-09-13 20:35:58 +00:00
parent 8ea022f5c0
commit 7550aff102
22 changed files with 1728 additions and 76 deletions
+30 -8
View File
@@ -398,18 +398,34 @@ def make_email_md(meta, extra_headers, body, folder_name):
def get_attachments(uid, folder, dest_dir):
"""Скачать вложения письма в dest_dir."""
"""
Скачать вложения письма в dest_dir.
Спек email-attachments: правильный флаг — `--downloads-dir` (не `--dir`).
Идемпотентность: если в dest_dir уже есть файлы — не качаем повторно.
"""
try:
existing = list(dest_dir.iterdir()) if dest_dir.exists() else []
if existing:
print(f" вложения уже скачаны ({len(existing)} ф.) — пропускаю")
return
except OSError:
pass
try:
run_cmd(
HIMALAYA_CMD + [
"attachment", "download", str(uid),
"--folder", folder,
"--dir", str(dest_dir),
"--downloads-dir", str(dest_dir),
],
timeout=60,
)
except RuntimeError:
pass # нет вложений — норм
except RuntimeError as e:
# Нет вложений / письмо не имеет вложений — норм для has_attachment=false.
# Но если письмо помечено has_attachment=true, а скачать не вышло —
# оставляем пустую папку и пишем warning (письмо не теряется).
print(f" [WARN] вложения не скачаны: {e}", file=sys.stderr)
def archive_folder(folder, limit=100):
@@ -454,10 +470,9 @@ def archive_folder(folder, limit=100):
date_str = env.get("date") or env.get("internal_date") or ""
year, month = parse_date(date_str)
# Путь: /mnt/yandex-disk/hermes/email/<folder>/YYYY/MM/UID/
# Путь: /opt/hermes/email/<folder>/YYYY/MM/UID/
msg_dir = ARCHIVE_ROOT / folder / f"{year:04d}" / f"{month:02d}" / str(uid)
email_path = msg_dir / "email.md"
attachments_dir = msg_dir / "attachments"
# Проверка — уже сохранено
if email_path.exists():
@@ -467,7 +482,13 @@ def archive_folder(folder, limit=100):
continue
msg_dir.mkdir(parents=True, exist_ok=True)
attachments_dir.mkdir(parents=True, exist_ok=True)
has_attachment = bool(env.get("has_attachment", False))
# Папку attachments/ создаём ТОЛЬКО если у письма есть вложения
# (спек email-attachments: без вложений пустую папку не создаём).
attachments_dir = None
if has_attachment:
attachments_dir = msg_dir / "attachments"
attachments_dir.mkdir(parents=True, exist_ok=True)
# Получаем заголовки и тело
extra_headers, body = get_email_content(uid, folder)
@@ -477,7 +498,8 @@ def archive_folder(folder, limit=100):
email_path.write_text(content, encoding="utf-8")
# Вложения
get_attachments(uid, folder, attachments_dir)
if attachments_dir is not None:
get_attachments(uid, folder, attachments_dir)
subj = (env.get("subject") or "")[:60]
print(f" ✓ UID {uid} ({subj})")