Monitor: parole chiave di esclusione dagli avvisi

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Fable 5.1 committed 2026-09-23 21:27:45 +02:00
1 parent 43c935f395
commit fdb954b92e
5 files changed
+71 -3

No files matched your search

+2
View File
@@ -40,6 +40,8 @@ Trascina un file nella zona "File upload" (o clicca per sceglierlo) e poi fai la
Attivabile in "Motore e Voce" › Monitor. Ogni minuto (intervallo regolabile) l'app raccoglie i messaggi nuovi da Mail (posta in arrivo), WhatsApp (ponte in tempo reale), Messaggi (SMS e iMessage) e Telegram (account collegato), li passa a un modello veloce con le regole che scrivi tu, e per quelli che meritano attenzione crea un avviso: riga rossa nel log, notifica di macOS e, se vuoi, annuncio a voce. Le regole predefinite segnalano richieste di aiuto o assistenza, domande che aspettano risposta, problemi, urgenze, scadenze, pagamenti e appuntamenti da confermare, e ignorano newsletter, promozioni e notifiche automatiche. Attivabile in "Motore e Voce" › Monitor. Ogni minuto (intervallo regolabile) l'app raccoglie i messaggi nuovi da Mail (posta in arrivo), WhatsApp (ponte in tempo reale), Messaggi (SMS e iMessage) e Telegram (account collegato), li passa a un modello veloce con le regole che scrivi tu, e per quelli che meritano attenzione crea un avviso: riga rossa nel log, notifica di macOS e, se vuoi, annuncio a voce. Le regole predefinite segnalano richieste di aiuto o assistenza, domande che aspettano risposta, problemi, urgenze, scadenze, pagamenti e appuntamenti da confermare, e ignorano newsletter, promozioni e notifiche automatiche.
Il campo "Escludi dagli avvisi" accetta parole o frasi separate da virgola (per esempio `newsletter, offerta, gruppo Calcetto`): i messaggi in cui compaiono, nel mittente, nel nome della chat o nel testo, vengono scartati prima della valutazione.
A voce: "ci sono avvisi?", "chiudi l'avviso di Marco", "controlla adesso". Il primo giro dopo l'attivazione prende solo la linea di base: vengono valutati i messaggi arrivati da quel momento in poi. Con il motore Claude Code la valutazione usa Haiku a basso sforzo; con Claude API usa Haiku 4.5; con il server locale usa il modello locale. A voce: "ci sono avvisi?", "chiudi l'avviso di Marco", "controlla adesso". Il primo giro dopo l'attivazione prende solo la linea di base: vengono valutati i messaggi arrivati da quel momento in poi. Con il motore Claude Code la valutazione usa Haiku a basso sforzo; con Claude API usa Haiku 4.5; con il server locale usa il modello locale.
## Plugin: comandare il Mac a voce ## Plugin: comandare il Mac a voce
+9
View File
@@ -208,6 +208,15 @@ class Monitor:
print(f"[monitor] {name}: {err}") print(f"[monitor] {name}: {err}")
seen = set(self.state.get("seen", [])) seen = set(self.state.get("seen", []))
items = [it for it in items if it["ref"] not in seen] items = [it for it in items if it["ref"] not in seen]
excluded = [w.strip().lower() for w in str(self.settings.get("monitor_escludi") or "").split(",") if w.strip()]
if excluded:
def _keep(it: dict) -> bool:
hay = f"{it.get('chi', '')} {it.get('testo', '')}".lower()
return not any(w in hay for w in excluded)
skipped = [it for it in items if not _keep(it)]
items = [it for it in items if _keep(it)]
for it in skipped:
print(f"[monitor] escluso per parola chiave: {it['fonte']} {it['chi']}")
self.state.setdefault("seen", []).extend(it["ref"] for it in items) self.state.setdefault("seen", []).extend(it["ref"] for it in items)
alerts = self._classify(items) if items else [] alerts = self._classify(items) if items else []
for a in alerts: for a in alerts:
+1
View File
@@ -35,6 +35,7 @@ DEFAULTS: dict[str, Any] = {
"monitor_annuncia": True, # annuncia a voce gli avvisi "monitor_annuncia": True, # annuncia a voce gli avvisi
"monitor_intervallo": 60, # secondi tra un controllo e l'altro "monitor_intervallo": 60, # secondi tra un controllo e l'altro
"monitor_regole": "", # cosa merita attenzione (vuoto = regole predefinite) "monitor_regole": "", # cosa merita attenzione (vuoto = regole predefinite)
"monitor_escludi": "", # parole/frasi (separate da virgola) che escludono un messaggio dagli avvisi
} }
SECRET_KEYS = ("anthropic_api_key", "local_api_key", "telegram_api_hash") SECRET_KEYS = ("anthropic_api_key", "local_api_key", "telegram_api_hash")
+3
View File
@@ -165,6 +165,8 @@ class SettingsDialog(QDialog):
from .monitor import DEFAULT_RULES from .monitor import DEFAULT_RULES
self.mon_rules = QLineEdit(s.get("monitor_regole") or ""); self.mon_rules.setPlaceholderText(DEFAULT_RULES[:110] + "…") self.mon_rules = QLineEdit(s.get("monitor_regole") or ""); self.mon_rules.setPlaceholderText(DEFAULT_RULES[:110] + "…")
form5.addRow("Cosa merita un avviso", self.mon_rules) form5.addRow("Cosa merita un avviso", self.mon_rules)
self.mon_excl = QLineEdit(s.get("monitor_escludi") or ""); self.mon_excl.setPlaceholderText("es. newsletter, offerta, gruppo Calcetto, Amazon — parole separate da virgola, cercate in mittente, chat e testo")
form5.addRow("Escludi dagli avvisi", self.mon_excl)
root.addLayout(form5) root.addLayout(form5)
btns = QHBoxLayout(); btns.addStretch() btns = QHBoxLayout(); btns.addStretch()
@@ -348,6 +350,7 @@ class SettingsDialog(QDialog):
"monitor_annuncia": self.mon_say.isChecked(), "monitor_annuncia": self.mon_say.isChecked(),
"monitor_intervallo": int(self.mon_int.currentData() or 60), "monitor_intervallo": int(self.mon_int.currentData() or 60),
"monitor_regole": self.mon_rules.text().strip(), "monitor_regole": self.mon_rules.text().strip(),
"monitor_escludi": self.mon_excl.text().strip(),
} }
if self.tg_hash.text().strip(): if self.tg_hash.text().strip():
values["telegram_api_hash"] = self.tg_hash.text().strip() values["telegram_api_hash"] = self.tg_hash.text().strip()
+56 -3
View File
@@ -53,6 +53,56 @@ def make_icon(dest_icns: Path) -> None:
shutil.rmtree(iconset, ignore_errors=True) shutil.rmtree(iconset, ignore_errors=True)
LAUNCHER_C = r'''
// Lanciatore nativo di LuZa: avvia Python come processo figlio, così i permessi
// di macOS (microfono, disco, contatti…) concessi all'app valgono anche per Python.
#include <spawn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/stat.h>
extern char **environ;
int main(void) {
const char *home = getenv("HOME"); if (!home) home = "/tmp";
char path[4096]; snprintf(path, sizeof path, "%s/.local/bin:/opt/homebrew/bin:/usr/local/bin:%s", home, getenv("PATH") ? getenv("PATH") : "/usr/bin:/bin");
setenv("PATH", path, 1);
unsetenv("CLAUDECODE"); unsetenv("CLAUDE_CONFIG_DIR"); unsetenv("ANTHROPIC_API_KEY");
setenv("PYTHONUNBUFFERED", "1", 1);
if (chdir(PROJECT_DIR) != 0) { perror("chdir"); return 1; }
char logdir[4096]; snprintf(logdir, sizeof logdir, "%s/Library/Logs", home); mkdir(logdir, 0755);
char logfile[4096]; snprintf(logfile, sizeof logfile, "%s/Library/Logs/" APP_NAME ".log", home);
int fd = open(logfile, O_WRONLY | O_CREAT | O_APPEND, 0644);
posix_spawn_file_actions_t fa; posix_spawn_file_actions_init(&fa);
if (fd >= 0) { posix_spawn_file_actions_adddup2(&fa, fd, 1); posix_spawn_file_actions_adddup2(&fa, fd, 2); }
char *argv[] = { PYTHON_BIN, "main.py", NULL };
pid_t pid; int rc = posix_spawn(&pid, PYTHON_BIN, &fa, NULL, argv, environ);
if (rc != 0) { fprintf(stderr, "posix_spawn: %s\n", strerror(rc)); return 1; }
int status = 0; waitpid(pid, &status, 0);
return WIFEXITED(status) ? WEXITSTATUS(status) : 1;
}
'''
def build_native_launcher(dest: Path, python: Path) -> bool:
"""Compila il lanciatore in C (serve clang, presente con Xcode o i Command Line Tools)."""
if not shutil.which("xcrun"):
return False
src = dest.parent / "launcher.c"
src.write_text(LAUNCHER_C.replace("PROJECT_DIR", f'"{PROJECT}"').replace("PYTHON_BIN", f'"{python}"').replace("APP_NAME", f'"{APP_NAME}"'))
# Compilatore di Xcode con ambiente pulito (le variabili di Homebrew possono rompere il link).
env = {k: v for k, v in os.environ.items() if k not in ("LDFLAGS", "CPPFLAGS", "CFLAGS", "LIBRARY_PATH", "CPATH", "SDKROOT")}
res = subprocess.run(["xcrun", "clang", "-O2", "-o", str(dest), str(src)], capture_output=True, text=True, env=env)
src.unlink(missing_ok=True)
if res.returncode != 0:
print(res.stderr[-400:])
return False
dest.chmod(0o755)
return True
def build(dest_dir: Path) -> Path: def build(dest_dir: Path) -> Path:
app = dest_dir / f"{APP_NAME}.app" app = dest_dir / f"{APP_NAME}.app"
shutil.rmtree(app, ignore_errors=True) shutil.rmtree(app, ignore_errors=True)
@@ -62,7 +112,10 @@ def build(dest_dir: Path) -> Path:
if not python.exists(): if not python.exists():
sys.exit("Ambiente .venv non trovato: esegui prima `uv sync` nella cartella del progetto.") sys.exit("Ambiente .venv non trovato: esegui prima `uv sync` nella cartella del progetto.")
launcher = app / "Contents" / "MacOS" / APP_NAME launcher = app / "Contents" / "MacOS" / APP_NAME
launcher.write_text(f'''#!/bin/bash if build_native_launcher(launcher, python):
print("Lanciatore nativo compilato.")
else:
launcher.write_text(f'''#!/bin/bash
# Avvio di {APP_NAME}: usa l'ambiente Python del progetto. # Avvio di {APP_NAME}: usa l'ambiente Python del progetto.
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" export PATH="$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"
unset CLAUDECODE CLAUDE_CONFIG_DIR ANTHROPIC_API_KEY unset CLAUDECODE CLAUDE_CONFIG_DIR ANTHROPIC_API_KEY
@@ -71,7 +124,7 @@ cd "{PROJECT}" || exit 1
mkdir -p "$HOME/Library/Logs" mkdir -p "$HOME/Library/Logs"
exec "{python}" main.py >> "$HOME/Library/Logs/{APP_NAME}.log" 2>&1 exec "{python}" main.py >> "$HOME/Library/Logs/{APP_NAME}.log" 2>&1
''') ''')
launcher.chmod(0o755) launcher.chmod(0o755)
make_icon(app / "Contents" / "Resources" / f"{APP_NAME}.icns") make_icon(app / "Contents" / "Resources" / f"{APP_NAME}.icns")
info = { info = {
"CFBundleName": APP_NAME, "CFBundleDisplayName": APP_NAME, "CFBundleIdentifier": BUNDLE_ID, "CFBundleName": APP_NAME, "CFBundleDisplayName": APP_NAME, "CFBundleIdentifier": BUNDLE_ID,
@@ -89,7 +142,7 @@ exec "{python}" main.py >> "$HOME/Library/Logs/{APP_NAME}.log" 2>&1
} }
with open(app / "Contents" / "Info.plist", "wb") as f: with open(app / "Contents" / "Info.plist", "wb") as f:
plistlib.dump(info, f) plistlib.dump(info, f)
subprocess.run(["codesign", "--force", "--deep", "--sign", "-", str(app)], check=False, capture_output=True) subprocess.run(["codesign", "--force", "--deep", "--sign", "-", "--identifier", BUNDLE_ID, str(app)], check=False, capture_output=True)
return app return app