- 파이프라인 42→51노드 확장 (calendar/mail/note 핸들러 추가) - 네이티브 서비스 6개: heic_converter(:8090), chat_bridge(:8091), caldav_bridge(:8092), devonthink_bridge(:8093), inbox_processor, news_digest - 분류기 v2→v3: calendar, reminder, mail, note intent 추가 - Mail Processing Pipeline (7노드, IMAP 폴링) - LaunchAgent plist 6개 + manage_services.sh - migrate-v3.sql: news_digest_log + calendar_events 확장 - 개발 문서 현행화 (CLAUDE.md, QUICK_REFERENCE.md, docs/architecture.md) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
126 lines
3.9 KiB
Python
126 lines
3.9 KiB
Python
"""DEVONthink Bridge — AppleScript REST API 래퍼 (port 8093)"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
|
|
from dotenv import load_dotenv
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
load_dotenv()
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
logger = logging.getLogger("devonthink_bridge")
|
|
|
|
DT_APP = os.getenv("DEVONTHINK_APP_NAME", "DEVONthink")
|
|
|
|
app = FastAPI()
|
|
|
|
|
|
def _run_applescript(script: str, timeout: int = 15) -> str:
|
|
"""AppleScript 실행."""
|
|
result = subprocess.run(
|
|
["osascript", "-e", script],
|
|
capture_output=True, text=True, timeout=timeout,
|
|
)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"AppleScript error: {result.stderr.strip()}")
|
|
return result.stdout.strip()
|
|
|
|
|
|
def _escape_as(s: str) -> str:
|
|
"""AppleScript 문자열 이스케이프."""
|
|
return s.replace("\\", "\\\\").replace('"', '\\"')
|
|
|
|
|
|
@app.post("/save")
|
|
async def save_record(request: Request):
|
|
body = await request.json()
|
|
title = body.get("title", "Untitled")
|
|
content = body.get("content", "")
|
|
record_type = body.get("type", "markdown")
|
|
database = body.get("database")
|
|
group = body.get("group")
|
|
tags = body.get("tags", [])
|
|
|
|
# Map type to DEVONthink record type
|
|
type_map = {"markdown": "markdown", "text": "txt", "html": "html"}
|
|
dt_type = type_map.get(record_type, "markdown")
|
|
|
|
# Build AppleScript
|
|
esc_title = _escape_as(title)
|
|
esc_content = _escape_as(content)
|
|
tags_str = ", ".join(f'"{_escape_as(t)}"' for t in tags) if tags else ""
|
|
|
|
if database:
|
|
db_line = f'set theDB to open database "{_escape_as(database)}"'
|
|
else:
|
|
db_line = "set theDB to first database"
|
|
|
|
if group:
|
|
group_line = f'set theGroup to get record at "/{_escape_as(group)}" in theDB'
|
|
else:
|
|
group_line = "set theGroup to incoming group of theDB"
|
|
|
|
script = f'''tell application "{DT_APP}"
|
|
{db_line}
|
|
{group_line}
|
|
set theRecord to create record with {{name:"{esc_title}", type:{dt_type}, plain text:"{esc_content}"}} in theGroup
|
|
{f'set tags of theRecord to {{{tags_str}}}' if tags_str else ''}
|
|
set theUUID to uuid of theRecord
|
|
set theName to name of theRecord
|
|
return theUUID & "|" & theName
|
|
end tell'''
|
|
|
|
try:
|
|
result = _run_applescript(script)
|
|
parts = result.split("|", 1)
|
|
uuid_val = parts[0] if parts else result
|
|
name_val = parts[1] if len(parts) > 1 else title
|
|
logger.info(f"Record saved: {uuid_val} '{name_val}'")
|
|
return JSONResponse({"success": True, "uuid": uuid_val, "name": name_val})
|
|
except Exception as e:
|
|
logger.error(f"Save failed: {e}")
|
|
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
|
|
|
|
|
|
@app.get("/databases")
|
|
async def list_databases():
|
|
script = f'''tell application "{DT_APP}"
|
|
set dbList to {{}}
|
|
repeat with theDB in databases
|
|
set end of dbList to (name of theDB) & "|" & (uuid of theDB)
|
|
end repeat
|
|
set AppleScript's text item delimiters to "\\n"
|
|
return dbList as text
|
|
end tell'''
|
|
|
|
try:
|
|
result = _run_applescript(script)
|
|
databases = []
|
|
for line in result.strip().split("\n"):
|
|
if "|" in line:
|
|
parts = line.split("|", 1)
|
|
databases.append({"name": parts[0], "uuid": parts[1]})
|
|
return JSONResponse({"databases": databases})
|
|
except Exception as e:
|
|
logger.error(f"List databases failed: {e}")
|
|
return JSONResponse({"databases": [], "error": str(e)}, status_code=500)
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
devonthink_running = False
|
|
try:
|
|
result = _run_applescript(
|
|
f'tell application "System Events" to return (name of processes) contains "{DT_APP}"',
|
|
timeout=5,
|
|
)
|
|
devonthink_running = result.lower() == "true"
|
|
except Exception:
|
|
pass
|
|
|
|
return {"status": "ok", "devonthink_running": devonthink_running}
|