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
+26 -16
View File
@@ -1,9 +1,9 @@
# Email Assistant — локальный архив и ассистент почты
**Дата:** 2026-09-11
**Фаза:** 1.5–1.7 + Портфель веб-UI (планирование)
**Дата:** 2026-09-13
**Фаза:** 1.5–1.7 + Портфель веб-UI (планирование) + Классификация/обработчики (в работе)
**Стек:** Himalaya CLI → Python → SQLite → Ollama (Qwen3:8b) → Yandex Disk
**Стек:** Himalaya CLI → Python → SQLite → Ollama (Qwen3:8b) → Radicale (CalDAV) → Telegram
---
@@ -116,23 +116,33 @@ Hermes cron:
### Задача 2: Локальный календарь + трекер задач 🔵 (в работе)
- [x] Решение пользователя: **Radicale (CalDAV) + Vikunja (трекер)**, всё в Docker-контейнерах
- [x] Change `local-calendar-tasks` создан и валиден (опenspec-lab) — proposal/specs/design/tasks
- [x] **РЕШЕНИЕ 2026-09-13: Vikunja — ЛИШНЯЯ СУЩНОСТЬ, задачи через Radicale VTODO** (change `remove-vikunja-use-radicale-tasks`). Radicale из коробки умеет VTODO (календарь «Задачи»), jtx board читает их по CalDAV. Vikunja выводится из эксплуатации.
- [x] Change `local-calendar-tasks` создан и валиден — proposal/specs/design/tasks (Radicale-часть актуальна, Vikunja-часть — SUPERSEDED)<br>
- [x] **Radicale развёрнут и РАБОТАЕТ (2026-09-13)**: контейнер на :5232, PROPFIND 207 с паролем / 401 без. Коллекции Личный/Рабочий/Задачи на ФС.
- [x] **Vikunja v2.6.0 развёрнут (2026-09-13)**: postgres + app, миграции прошли, HTTP слушает :3456. **Блокер:** снаружи connection reset (docker-proxy/NAT на bigbox) — контейнер отвечает, маунт файлов исправлен (./files → /app/vikunja/files).
- [x] **CardDAV-синк контактов (2026-09-13, change `contacts-caldav-server`)**: `scripts/contacts_caldav_sync.py` — двусторонний sync. PUSH: 81 контакт → vCard в Radicale. PULL: правки/создание/удаление карточек с телефона → contacts.json. Идемпотентно (162 unchanged, 0 PUT на повторе). Конфликты (412) — приоритет телефону, локальная версия в `caldav-sync.log`. Подробнее: `scripts/contacts_caldav_sync.py --help`, лог `/opt/hermes/email/contacts/caldav-sync.log`.
- [ ] **Vikunja: создать админа + API-токен, проверить tasks.nixg.ru**
- [ ] **Caddy reverse proxy (cal.nixg.ru → 5232, tasks.nixg.ru → 3456)** — СДЕЛАТЬ СЛЕДУЮЩЕЙ СЕССИЕЙ (см. ниже «DAVx⁵»)
- [ ] **Android-синхронизация (DAVx5)** — СДЕЛАТЬ СЛЕДУЮЩЕЙ СЕССИЕЙ (см. ниже)
- [x] **Vikunja ВЫВЕДЕНА ИЗ ЭКСПЛУАТАЦИИ (2026-09-13)**: контейнеры vikunja + vikunja-db удалены (`docker compose down -v`), каталог `/opt/hermes/email-assistant/vikunja/` удалён, порт 3456 свободен. Tasks.nixg.ru закомментирован в Caddy (строки 114-120), Caddy перезагружен (бэкап Caddyfile.bak-vikunja-removed). |
- [x] **Caddy reverse proxy (cal.nixg.ru → 5232)** — РАБОТАЕТ (2026-09-13): PROPFIND 207 снаружи. tasks.nixg.ru закомментирован.
- [x] **«Обход в Глории»** — повторяющееся событие (Рабочий, VTIMEZONE Europe/Moscow, RRULE WEEKLY BYDAY=TU 11:00), подтверждено на телефоне (GMT+3 ✓)
### Задача 3: Нативная синхронизация с Android 🔵 (следующая сессия)
- [ ] **Первая задача след. сессии: настроить синхронизацию с телефоном** (DAVx5 → Radicale; Vikunja → jtx board)
- [ ] Настроить Caddy reverse proxy для Radicale (cal.nixg.ru) — см. ниже
- [ ] Проверка двусторонней синхронизации (событие/контакт с телефона → bigbox → база)
### Задача 3: Нативная синхронизация с Android 🔵 (в работе)
- [x] **Контакты синхронизированы** (DAVx5 → Radicale «Контакты»; CardDAV-sync двусторонний, change `contacts-caldav-server`)
- [x] **Caddy reverse proxy (cal.nixg.ru → 5232)** — работает, PROPFIND 207 снаружи
- [x] **«Обход в Глории»** — VEVENT подтверждён на телефоне (GMT+3 ✓)
- [ ] **Проверить появление событий/задач в приложении** (тестовый VEVENT obhod-v-glorii-2026.ics в «Рабочий», тестовый VTODO test-vikunja-removal-2026 в «Задачи») — контакты синхронизируются, события/задачи на телефоне пока не проверены
- [ ] Двусторонняя синхронизация: событие/задача с телефона → bigbox → база
### Задача 8: Классификация писем и обработчики 🔵 (в работе, change `email-classification-handlers`)
- [x] **Вложения**: фикс бага `himalaya --dir` → `--downloads-dir`; вложения в `<msg_dir>/attachments/`; идемпотентно (2026-09-13 вечер, проверено на живом письме)
- [x] **Классификатор**: `scripts/email_classifier.py` — Qwen3:8b (Ollama localhost:11434) → теги info/urgent/task/meeting + `classification`/`classification_reason` в frontmatter; идемпотентно; прогон прошёл (письмо 422 → task,meeting)
- [x] **Обработчики**: `scripts/email_handlers.py` — urgent→Telegram (Bot API+SOCKS5), task→Radicale VTODO («Задачи»), meeting→Radicale VEVENT («Рабочий»), info→ничего; идемпотентно через `handled_*`; dry-run чист (VTODO 204/VEVENT 201)
- [ ] Живой прогон обработчиков без --dry-run + проверка доставки urgent→Telegram
- [ ] Cron: классификация/обработчики после `mail-archive`
- [ ] Telegram-секреты (chat_id) в `.env` (токен — `VESTI_BOT_TOKEN` из /opt/vesti/.env, канал-дефолт `@dedinit_vesti`)
### Задача 1: Веб-интерфейс ассистента ⬜
- [ ] FastAPI + SQLite FTS5: список писем (дата/адресант/тэги/папка)
- [ ] Перемещение в папку; тэги
- [ ] Кнопка «Создать задачу» → в Vikunja API (Задача 6)
- [ ] Кнопка «Создать задачу» → Radicale VTODO (Задача 6, вместо Vikunja API)
- [ ] Страница авторизации (Задача 7)
---
@@ -278,7 +288,7 @@ dav.example.com {
**В DAVx⁵ указывается базовый URL** (например, `https://dav.example.com` или `https://dav.example.com/radicale`); пути к календарям/контактам приложение определяет автоматически.
**Наш случай (следующая сессия):** поддомен `cal.nixg.ru` (Caddy на vps02, `reverse_proxy 10.8.0.2:5232` к bigbox) — Radicale слушает 127.0.0.1:5232, user `estorozhenko`, пароль `RADICALE_PASS` в `radicale/.env`. Адресная книга: `Контакты` (кириллица в URL — DAVx5 умеет). Vikunja → jtx board через `tasks.nixg.ru`.
**Наш случай (следующая сессия):** поддомен `cal.nixg.ru` (Caddy на vps02, `reverse_proxy 10.8.0.2:5232` к bigbox) — Radicale слушает 127.0.0.1:5232, user `estorozhenko`, пароль `RADICALE_PASS` в `radicale/.env`. Адресная книга: `Контакты` (кириллица в URL — DAVx5 умеет). **Задачи: Radicale VTODO (календарь «Задачи») → jtx board** (Vikunja выведена, change `remove-vikunja-use-radicale-tasks`).
---
@@ -295,11 +305,11 @@ dav.example.com {
|--------|-----------|--------|
| **Caddy (reverse proxy)** | vps02 = 87.242.100.206, контейнер `caddy` | SSH: `ssh vps02` (alias в `~/.ssh/config`, User estorozhenko, ключ cloudruVPS). Caddyfile: `/opt/caddy/Caddyfile` (root; правка через `sudo`). Reload: `sudo docker exec caddy caddy reload --config /etc/caddy/Caddyfile` |
| **Radicale (CalDAV)** | bigbox, контейнер `radicale`, порт 5232 | WGET: `http://127.0.0.1:5232` (с bigbox), наружу: `https://cal.nixg.ru`. Конфиг: `/opt/hermes/email-assistant/radicale/` (compose.yml, .env — пароль `RADICALE_PASS`). Коллекции на ФС: `/opt/hermes/email-assistant/radicale/data/collections/collection-root/estorozhenko/{Личный,Рабочий,Задачи}` |
| **Vikunja (трекер задач)** | bigbox, compose в `/opt/hermes/email-assistant/vikunja/` | Порт 3456 (внутр.), наружу: `https://tasks.nixg.ru` (Caddy → 10.8.0.2:3456). Данные: volumes `vikunja-db` (postgres) + `vikunja-files` |
| **Vikunja (трекер задач)** | ~~bigbox, compose в `/opt/hermes/email-assistant/vikunja/`~~ | ⛔ **ВЫВЕДЕНА** (change `remove-vikunja-use-radicale-tasks`). Задачи → Radicale VTODO (календарь «Задачи»). |
| **WG (сеть хостов)** | 10.8.0.0/24 | bigbox = 10.8.0.2 (цель reverse-proxy с vps02), vps01 = .1, vps03 = .3, vps02 = .4 |
| **SSH-ключи** | `/home/estorozhenko/.ssh/` | vps01_key (vps01), cloudruVPS (vps02), hostkeyVPS (vps03/root) |
**Домены (публичный DNS):** cal.nixg.ru → 87.242.100.206 (vps02/Caddy → bigbox Radicale 5232), tasks.nixg.ru → 87.242.100.206 (Caddy → bigbox Vikunja 3456).
**Домены (публичный DNS):** cal.nixg.ru → 87.242.100.206 (vps02/Caddy → bigbox Radicale 5232). **tasks.nixg.ru — НЕ используется** (Vikunja выведена).
- **Himalaya:** `~/.config/himalaya/config.toml`
- **Аккаунт:** `vinogorod`, IMAP `mail.corpoffice.tech:143` (STARTTLS)