feat: 이드 기능 완성 — note_bridge, intent_service, CalDAV Todo
- note_bridge.py: port 8096, log-conversation 제거, message 필드 추가 - intent_service.py: 의도 분류(Ollama→Claude fallback) + 한국어 날짜 파싱 + API 사용량 추적 - caldav_bridge.py: VTODO 생성 (/calendar/create-todo) + 응답 message 필드 - LaunchAgent plist: note-bridge (8096), intent-service (8097) - .env.example: API_MONTHLY_LIMIT, NOTE_BRIDGE_URL, INTENT_SERVICE_URL Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import httpx
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from icalendar import Calendar, Event, vText
|
||||
from icalendar import Calendar, Event, Todo, vText
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -42,6 +42,19 @@ def _make_ical(title: str, start: str, end: str | None, location: str | None,
|
||||
cal = Calendar()
|
||||
cal.add("prodid", "-//SynChatBot//CalDAV Bridge//KO")
|
||||
cal.add("version", "2.0")
|
||||
cal.add("calscale", "GREGORIAN")
|
||||
|
||||
# VTIMEZONE for Asia/Seoul (required by Synology Calendar)
|
||||
from icalendar import Timezone, TimezoneStandard
|
||||
tz = Timezone()
|
||||
tz.add("tzid", "Asia/Seoul")
|
||||
tz_std = TimezoneStandard()
|
||||
tz_std.add("tzname", "KST")
|
||||
tz_std.add("tzoffsetfrom", timedelta(hours=9))
|
||||
tz_std.add("tzoffsetto", timedelta(hours=9))
|
||||
tz_std.add("dtstart", datetime(1970, 1, 1))
|
||||
tz.add_component(tz_std)
|
||||
cal.add_component(tz)
|
||||
|
||||
evt = Event()
|
||||
evt.add("uid", uid)
|
||||
@@ -93,6 +106,44 @@ def _parse_events(ical_data: bytes) -> list[dict]:
|
||||
return events
|
||||
|
||||
|
||||
def _make_vtodo(title: str, due: str | None, description: str | None, uid: str) -> bytes:
|
||||
"""iCalendar VTODO 생성."""
|
||||
cal = Calendar()
|
||||
cal.add("prodid", "-//SynChatBot//CalDAV Bridge//KO")
|
||||
cal.add("version", "2.0")
|
||||
cal.add("calscale", "GREGORIAN")
|
||||
|
||||
from icalendar import Timezone, TimezoneStandard
|
||||
tz = Timezone()
|
||||
tz.add("tzid", "Asia/Seoul")
|
||||
tz_std = TimezoneStandard()
|
||||
tz_std.add("tzname", "KST")
|
||||
tz_std.add("tzoffsetfrom", timedelta(hours=9))
|
||||
tz_std.add("tzoffsetto", timedelta(hours=9))
|
||||
tz_std.add("dtstart", datetime(1970, 1, 1))
|
||||
tz.add_component(tz_std)
|
||||
cal.add_component(tz)
|
||||
|
||||
todo = Todo()
|
||||
todo.add("uid", uid)
|
||||
todo.add("dtstamp", datetime.now(KST))
|
||||
todo.add("summary", title)
|
||||
todo.add("created", datetime.now(KST))
|
||||
todo.add("status", "NEEDS-ACTION")
|
||||
|
||||
if due:
|
||||
dt_due = datetime.fromisoformat(due)
|
||||
if dt_due.tzinfo is None:
|
||||
dt_due = dt_due.replace(tzinfo=KST)
|
||||
todo.add("due", dt_due)
|
||||
|
||||
if description:
|
||||
todo["description"] = vText(description)
|
||||
|
||||
cal.add_component(todo)
|
||||
return cal.to_ical()
|
||||
|
||||
|
||||
@app.post("/calendar/create")
|
||||
async def create_event(request: Request):
|
||||
body = await request.json()
|
||||
@@ -108,6 +159,10 @@ async def create_event(request: Request):
|
||||
uid = f"{uuid.uuid4()}@syn-chat-bot"
|
||||
ical = _make_ical(title, start, end, location, description, uid)
|
||||
|
||||
# 표시용 시간 포맷
|
||||
dt = datetime.fromisoformat(start)
|
||||
display_time = dt.strftime("%-m/%d %H:%M")
|
||||
|
||||
async with _client() as client:
|
||||
resp = await client.put(
|
||||
f"{CALENDAR_URL}{uid}.ics",
|
||||
@@ -116,9 +171,46 @@ async def create_event(request: Request):
|
||||
)
|
||||
if resp.status_code in (200, 201, 204):
|
||||
logger.info(f"Event created: {uid} '{title}'")
|
||||
return JSONResponse({"success": True, "uid": uid})
|
||||
return JSONResponse({"success": True, "uid": uid,
|
||||
"message": f"일정 등록 완료: {display_time} {title}"})
|
||||
logger.error(f"CalDAV PUT failed: {resp.status_code} {resp.text[:200]}")
|
||||
return JSONResponse({"success": False, "error": f"CalDAV PUT {resp.status_code}"}, status_code=502)
|
||||
return JSONResponse({"success": False, "error": f"CalDAV PUT {resp.status_code}",
|
||||
"message": f"일정 등록 실패: {title}"}, status_code=502)
|
||||
|
||||
|
||||
@app.post("/calendar/create-todo")
|
||||
async def create_todo(request: Request):
|
||||
"""VTODO 생성. body: {title, due?, description?}"""
|
||||
body = await request.json()
|
||||
title = body.get("title", "")
|
||||
due = body.get("due")
|
||||
description = body.get("description")
|
||||
|
||||
if not title:
|
||||
return JSONResponse({"success": False, "error": "title required"}, status_code=400)
|
||||
|
||||
uid = f"{uuid.uuid4()}@syn-chat-bot"
|
||||
ical = _make_vtodo(title, due, description, uid)
|
||||
|
||||
# 표시용 기한
|
||||
display_due = ""
|
||||
if due:
|
||||
dt = datetime.fromisoformat(due)
|
||||
display_due = f"~{dt.strftime('%-m/%d')} "
|
||||
|
||||
async with _client() as client:
|
||||
resp = await client.put(
|
||||
f"{CALENDAR_URL}{uid}.ics",
|
||||
content=ical,
|
||||
headers={"Content-Type": "text/calendar; charset=utf-8"},
|
||||
)
|
||||
if resp.status_code in (200, 201, 204):
|
||||
logger.info(f"Todo created: {uid} '{title}'")
|
||||
return JSONResponse({"success": True, "uid": uid,
|
||||
"message": f"작업 등록 완료: {display_due}{title}"})
|
||||
logger.error(f"CalDAV PUT (todo) failed: {resp.status_code} {resp.text[:200]}")
|
||||
return JSONResponse({"success": False, "error": f"CalDAV PUT {resp.status_code}",
|
||||
"message": f"작업 등록 실패: {title}"}, status_code=502)
|
||||
|
||||
|
||||
@app.post("/calendar/query")
|
||||
|
||||
Reference in New Issue
Block a user