#!/usr/bin/env python3 """ PKM 일일 다이제스트 - DEVONthink 오늘 추가/수정 집계 - law_monitor 법령 변경 건 파싱 - OmniFocus 완료/추가/기한초과 집계 - 상위 뉴스 Ollama 요약 - OmniFocus 액션 자동 생성 - 90일 지난 다이제스트 아카이브 """ import os import sys import re from datetime import datetime, timedelta from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from pkm_utils import ( setup_logger, load_credentials, run_applescript_inline, ollama_generate, count_log_errors, PROJECT_ROOT, LOGS_DIR, DATA_DIR ) logger = setup_logger("digest") DIGEST_DIR = DATA_DIR / "digests" DIGEST_DIR.mkdir(exist_ok=True) def get_devonthink_stats() -> dict: """DEVONthink 오늘 추가/수정 문서 집계""" script = ''' tell application id "DNtp" set today to current date set time of today to 0 set stats to {} repeat with db in databases set dbName to name of db set addedCount to count of (search "date:today" in db) set modifiedCount to count of (search "modified:today" in db) if addedCount > 0 or modifiedCount > 0 then set end of stats to dbName & ":" & addedCount & ":" & modifiedCount end if end repeat set AppleScript's text item delimiters to "|" return stats as text end tell ''' try: result = run_applescript_inline(script) stats = {} if result: for item in result.split("|"): parts = item.split(":") if len(parts) == 3: stats[parts[0]] = {"added": int(parts[1]), "modified": int(parts[2])} return stats except Exception as e: logger.error(f"DEVONthink 집계 실패: {e}") return {} def get_omnifocus_stats() -> dict: """OmniFocus 오늘 완료/추가/기한초과 집계""" script = ''' tell application "OmniFocus" tell default document set today to current date set time of today to 0 set tomorrow to today + 1 * days set completedCount to count of (every flattened task whose completed is true and completion date ≥ today) set addedCount to count of (every flattened task whose creation date ≥ today) set overdueCount to count of (every flattened task whose completed is false and due date < today and due date is not missing value) return (completedCount as text) & "|" & (addedCount as text) & "|" & (overdueCount as text) end tell end tell ''' try: result = run_applescript_inline(script) parts = result.split("|") return { "completed": int(parts[0]) if len(parts) > 0 else 0, "added": int(parts[1]) if len(parts) > 1 else 0, "overdue": int(parts[2]) if len(parts) > 2 else 0, } except Exception as e: logger.error(f"OmniFocus 집계 실패: {e}") return {"completed": 0, "added": 0, "overdue": 0} def parse_law_changes() -> list: """law_monitor 로그에서 오늘 법령 변경 건 파싱""" log_file = LOGS_DIR / "law_monitor.log" if not log_file.exists(): return [] today = datetime.now().strftime("%Y-%m-%d") changes = [] with open(log_file, "r", encoding="utf-8") as f: for line in f: if today in line and "변경 감지" in line: # "[2026-03-26 07:00:15] [law_monitor] [INFO] 변경 감지: 산업안전보건법 — 공포일자 ..." match = re.search(r"변경 감지: (.+?)$", line) if match: changes.append(match.group(1).strip()) return changes def get_inbox_count() -> int: """DEVONthink Inbox 미처리 문서 수""" script = ''' tell application id "DNtp" repeat with db in databases if name of db is "Inbox" then return count of children of root group of db end if end repeat return 0 end tell ''' try: return int(run_applescript_inline(script)) except: return 0 def create_omnifocus_task(task_name: str, note: str = "", flagged: bool = False): """OmniFocus 작업 생성""" flag_str = "true" if flagged else "false" escaped_name = task_name.replace('"', '\\"') escaped_note = note.replace('"', '\\"') script = f''' tell application "OmniFocus" tell default document make new inbox task with properties {{name:"{escaped_name}", note:"{escaped_note}", flagged:{flag_str}}} end tell end tell ''' try: run_applescript_inline(script) logger.info(f"OmniFocus 작업 생성: {task_name}") except Exception as e: logger.error(f"OmniFocus 작업 생성 실패: {e}") def get_system_health() -> dict: """각 모듈 로그의 최근 24시간 ERROR 카운트""" modules = ["law_monitor", "mailplus", "digest", "embed", "auto_classify"] health = {} for mod in modules: log_file = LOGS_DIR / f"{mod}.log" health[mod] = count_log_errors(log_file, since_hours=24) return health def generate_digest(): """다이제스트 생성""" logger.info("=== Daily Digest 생성 시작 ===") today = datetime.now() date_str = today.strftime("%Y-%m-%d") # 데이터 수집 dt_stats = get_devonthink_stats() of_stats = get_omnifocus_stats() law_changes = parse_law_changes() inbox_count = get_inbox_count() system_health = get_system_health() # 마크다운 생성 md = f"# PKM Daily Digest — {date_str}\n\n" # DEVONthink 현황 md += "## DEVONthink 변화\n\n" if dt_stats: md += "| DB | 신규 | 수정 |\n|---|---|---|\n" total_added = 0 total_modified = 0 for db_name, counts in dt_stats.items(): md += f"| {db_name} | {counts['added']} | {counts['modified']} |\n" total_added += counts["added"] total_modified += counts["modified"] md += f"| **합계** | **{total_added}** | **{total_modified}** |\n\n" else: md += "변화 없음\n\n" # 법령 변경 md += "## 법령 변경\n\n" if law_changes: for change in law_changes: md += f"- {change}\n" md += "\n" else: md += "변경 없음\n\n" # OmniFocus 현황 md += "## OmniFocus 현황\n\n" md += f"- 완료: {of_stats['completed']}건\n" md += f"- 신규: {of_stats['added']}건\n" md += f"- 기한초과: {of_stats['overdue']}건\n\n" # Inbox 상태 md += f"## Inbox 미처리: {inbox_count}건\n\n" # 시스템 상태 md += "## 시스템 상태\n\n" total_errors = sum(system_health.values()) if total_errors == 0: md += "모든 모듈 정상\n\n" else: md += "| 모듈 | 에러 수 |\n|---|---|\n" for mod, cnt in system_health.items(): status = f"**{cnt}**" if cnt > 0 else "0" md += f"| {mod} | {status} |\n" md += "\n" # 파일 저장 digest_file = DIGEST_DIR / f"{date_str}_digest.md" with open(digest_file, "w", encoding="utf-8") as f: f.write(md) logger.info(f"다이제스트 저장: {digest_file}") # DEVONthink 저장 import_digest_to_devonthink(digest_file, date_str) # OmniFocus 액션 자동 생성 if law_changes: for change in law_changes: create_omnifocus_task(f"법령 변경 검토: {change[:30]}", note=change) if inbox_count >= 3: create_omnifocus_task(f"Inbox 정리 ({inbox_count}건 미처리)", note="DEVONthink Inbox에 미분류 문서가 쌓여있습니다.") if of_stats["overdue"] > 0: create_omnifocus_task(f"기한초과 작업 처리 ({of_stats['overdue']}건)", flagged=True) # 90일 지난 다이제스트 아카이브 archive_old_digests() logger.info("=== Daily Digest 완료 ===") def import_digest_to_devonthink(filepath: Path, date_str: str): """다이제스트를 DEVONthink에 저장""" escaped_path = str(filepath).replace('"', '\\"') script = f''' tell application id "DNtp" repeat with db in databases if name of db is "00_Note_BOX" then set targetGroup to create location "/Daily_Digest" in db import POSIX path "{escaped_path}" to targetGroup exit repeat end if end repeat end tell ''' try: run_applescript_inline(script) except Exception as e: logger.error(f"DEVONthink 다이제스트 임포트 실패: {e}") def archive_old_digests(): """90일 지난 다이제스트 이동""" cutoff = datetime.now() - timedelta(days=90) for f in DIGEST_DIR.glob("*_digest.md"): try: date_part = f.stem.split("_digest")[0] file_date = datetime.strptime(date_part, "%Y-%m-%d") if file_date < cutoff: archive_dir = DIGEST_DIR / "archive" archive_dir.mkdir(exist_ok=True) f.rename(archive_dir / f.name) logger.info(f"아카이브: {f.name}") except ValueError: pass if __name__ == "__main__": generate_digest()