feat: 전체 PKM 스크립트 일괄 작성 — 분류/법령/메일/다이제스트/임베딩
- scripts/pkm_utils.py: 공통 유틸 (로거, dotenv, osascript 래퍼) - scripts/prompts/classify_document.txt: Ollama 분류 프롬프트 - applescript/auto_classify.scpt: Inbox → AI 분류 → DB 이동 - applescript/omnifocus_sync.scpt: Projects → OmniFocus 작업 생성 - scripts/law_monitor.py: 법령 변경 모니터링 + DEVONthink 임포트 - scripts/mailplus_archive.py: MailPlus IMAP → Archive DB - scripts/pkm_daily_digest.py: 일일 다이제스트 + OmniFocus 액션 - scripts/embed_to_chroma.py: GPU 서버 벡터 임베딩 → ChromaDB - launchd/*.plist: 3개 스케줄 (07:00, 07:00+18:00, 20:00) - docs/deploy.md: Mac mini 배포 가이드 - docs/devonagent-setup.md: 검색 세트 9종 설정 가이드 - tests/test_classify.py: 5종 문서 분류 테스트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
284
scripts/pkm_daily_digest.py
Normal file
284
scripts/pkm_daily_digest.py
Normal file
@@ -0,0 +1,284 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user