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:
184
note_bridge.py
Normal file
184
note_bridge.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""Note Bridge — Synology Note Station REST API 래퍼 (port 8096)"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
import httpx
|
||||
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("note_bridge")
|
||||
|
||||
# Note Station 앱 포털 포트 (9350)
|
||||
NOTE_STATION_URL = os.getenv("NOTE_STATION_URL", "http://192.168.1.227:9350")
|
||||
NOTE_ACCOUNT = os.getenv("NOTE_ACCOUNT", "chatbot-api")
|
||||
NOTE_PASSWORD = os.getenv("NOTE_PASSWORD", "U1Ob2L3a")
|
||||
|
||||
# 메모장 ID
|
||||
NOTEBOOK_MEMO = os.getenv("NOTEBOOK_MEMO", "1030_3CLANHDEQH4T9DRONETDNOG2U4") # chatbot (메모용)
|
||||
|
||||
# 세션
|
||||
_sid: str = ""
|
||||
|
||||
|
||||
async def _login() -> str:
|
||||
"""Note Station 앱 포털에 로그인하여 SID를 반환."""
|
||||
global _sid
|
||||
async with httpx.AsyncClient(verify=False, timeout=15) as c:
|
||||
resp = await c.post(f"{NOTE_STATION_URL}/webapi/entry.cgi", data={
|
||||
"api": "SYNO.API.Auth",
|
||||
"version": "7",
|
||||
"method": "login",
|
||||
"account": NOTE_ACCOUNT,
|
||||
"passwd": NOTE_PASSWORD,
|
||||
})
|
||||
data = resp.json()
|
||||
if data.get("success"):
|
||||
_sid = data["data"]["sid"]
|
||||
logger.info("Note Station login successful")
|
||||
return _sid
|
||||
else:
|
||||
logger.error(f"Note Station login failed: {data}")
|
||||
raise Exception(f"Login failed: {data}")
|
||||
|
||||
|
||||
async def _ensure_sid() -> str:
|
||||
"""SID가 유효한지 확인하고, 필요하면 재로그인."""
|
||||
global _sid
|
||||
if not _sid:
|
||||
return await _login()
|
||||
# SID 유효성 간단 체크
|
||||
async with httpx.AsyncClient(verify=False, timeout=10) as c:
|
||||
resp = await c.get(f"{NOTE_STATION_URL}/webapi/entry.cgi", params={
|
||||
"api": "SYNO.NoteStation.Info",
|
||||
"version": "1",
|
||||
"method": "get",
|
||||
"_sid": _sid,
|
||||
})
|
||||
data = resp.json()
|
||||
if data.get("success"):
|
||||
return _sid
|
||||
return await _login()
|
||||
|
||||
|
||||
async def _api_call(api: str, method: str, version: int = 3, **kwargs) -> dict:
|
||||
"""Note Station API 호출 래퍼."""
|
||||
sid = await _ensure_sid()
|
||||
params = {
|
||||
"api": api,
|
||||
"version": str(version),
|
||||
"method": method,
|
||||
"_sid": sid,
|
||||
**kwargs,
|
||||
}
|
||||
async with httpx.AsyncClient(verify=False, timeout=30) as c:
|
||||
resp = await c.post(f"{NOTE_STATION_URL}/webapi/entry.cgi", data=params)
|
||||
data = resp.json()
|
||||
if not data.get("success"):
|
||||
# SID 만료일 수 있으므로 재로그인 후 재시도
|
||||
sid = await _login()
|
||||
params["_sid"] = sid
|
||||
resp = await c.post(f"{NOTE_STATION_URL}/webapi/entry.cgi", data=params)
|
||||
data = resp.json()
|
||||
return data
|
||||
|
||||
|
||||
# ==================== FastAPI App ====================
|
||||
|
||||
app = FastAPI(title="Note Bridge")
|
||||
|
||||
|
||||
@app.post("/note/create")
|
||||
async def create_note(request: Request):
|
||||
"""노트 생성. body: {title, content}"""
|
||||
body = await request.json()
|
||||
title = body.get("title", "제목 없음")
|
||||
content = body.get("content", "")
|
||||
|
||||
# HTML 래핑 (이미 HTML이 아닌 경우)
|
||||
if content and not content.strip().startswith("<"):
|
||||
content = f"<p>{content}</p>"
|
||||
|
||||
result = await _api_call(
|
||||
"SYNO.NoteStation.Note",
|
||||
"create",
|
||||
parent_id=NOTEBOOK_MEMO,
|
||||
title=title,
|
||||
commit_msg='{"device":"api","listable":false}',
|
||||
encrypt="false",
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
note_id = result["data"].get("object_id", "")
|
||||
# 내용 업데이트 (create는 빈 노트만 생성)
|
||||
if content:
|
||||
await _api_call(
|
||||
"SYNO.NoteStation.Note",
|
||||
"update",
|
||||
object_id=note_id,
|
||||
content=content,
|
||||
commit_msg='{"device":"api","listable":false}',
|
||||
)
|
||||
logger.info(f"Note created: {note_id} '{title}'")
|
||||
return {"success": True, "note_id": note_id, "title": title,
|
||||
"message": f"메모 저장 완료: {title}"}
|
||||
else:
|
||||
logger.error(f"Note creation failed: {result}")
|
||||
return JSONResponse(status_code=500, content={
|
||||
"success": False, "error": str(result),
|
||||
"message": f"메모 저장 실패: {title}"})
|
||||
|
||||
|
||||
@app.post("/note/append")
|
||||
async def append_to_note(request: Request):
|
||||
"""기존 노트에 내용 추가. body: {note_id, content}"""
|
||||
body = await request.json()
|
||||
|
||||
note_id = body.get("note_id")
|
||||
content = body.get("content", "")
|
||||
|
||||
if not content:
|
||||
return JSONResponse(status_code=400, content={"success": False, "error": "content required"})
|
||||
if not note_id:
|
||||
return JSONResponse(status_code=400, content={"success": False, "error": "note_id required"})
|
||||
|
||||
# 기존 내용 가져오기
|
||||
existing = await _api_call("SYNO.NoteStation.Note", "get", object_id=note_id)
|
||||
if not existing.get("success"):
|
||||
return JSONResponse(status_code=404, content={"success": False, "error": "Note not found"})
|
||||
|
||||
old_content = existing["data"].get("content", "")
|
||||
|
||||
# HTML 래핑
|
||||
if not content.strip().startswith("<"):
|
||||
content = f"<p>{content}</p>"
|
||||
|
||||
updated_content = old_content + content
|
||||
|
||||
result = await _api_call(
|
||||
"SYNO.NoteStation.Note",
|
||||
"update",
|
||||
object_id=note_id,
|
||||
content=updated_content,
|
||||
commit_msg='{"device":"api","listable":false}',
|
||||
)
|
||||
|
||||
if result.get("success"):
|
||||
logger.info(f"Note appended: {note_id}")
|
||||
return {"success": True, "note_id": note_id, "message": f"메모 추가 완료"}
|
||||
else:
|
||||
return JSONResponse(status_code=500, content={"success": False, "error": str(result)})
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
"""헬스체크."""
|
||||
try:
|
||||
sid = await _ensure_sid()
|
||||
return {"status": "ok", "sid_active": bool(sid)}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
Reference in New Issue
Block a user