feat: kb_writer 마이크로서비스 + mail_bridge 추가

- kb_writer.py: DEVONthink AppleScript 브릿지 → 마크다운 파일 기반 전환 (포트 8095)
- knowledge-base/ 디렉토리 구조 (note, chat-memory, news)
- Handle Note: kb_writer 파일 저장 + Qdrant 임베딩 추가
- Embed & Save Memory: DEVONthink → kb_writer 교체
- mail_bridge.py: IMAP 날짜 기반 메일 조회 (포트 8094)
- mail-processing-pipeline: IMAP Trigger → Schedule + mail_bridge + dedup
- docker-compose, manage_services, LaunchAgent plist 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-03-18 14:46:57 +09:00
parent 66a8b63cad
commit 852f5cb648
16 changed files with 445 additions and 38 deletions

View File

@@ -4,25 +4,19 @@
"nodes": [
{
"parameters": {
"mailbox": "INBOX",
"postProcessAction": "read",
"options": {
"customEmailConfig": "{ \"host\": \"{{$env.IMAP_HOST || '192.168.1.227'}}\", \"port\": {{$env.IMAP_PORT || 993}}, \"secure\": true, \"auth\": { \"user\": \"{{$env.IMAP_USER}}\", \"pass\": \"{{$env.IMAP_PASSWORD}}\" } }"
},
"pollTimes": {
"item": [
"rule": {
"interval": [
{
"mode": "everyX",
"value": 15,
"unit": "minutes"
"field": "minutes",
"minutesInterval": 15
}
]
}
},
"id": "m1000001-0000-0000-0000-000000000001",
"name": "IMAP Trigger",
"type": "n8n-nodes-base.imapEmail",
"typeVersion": 2,
"id": "m1000001-0000-0000-0000-000000000010",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
0,
300
@@ -30,14 +24,52 @@
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst results = [];\nfor (const item of items) {\n const j = item.json;\n const from = j.from?.text || j.from || '';\n const subject = (j.subject || '').substring(0, 500);\n const body = (j.text || j.textPlain || j.html || '').substring(0, 5000)\n .replace(/<[^>]*>/g, '').replace(/\\s+/g, ' ').trim();\n const mailDate = j.date || new Date().toISOString();\n results.push({ json: { from, subject, body, mailDate, messageId: j.messageId || '' } });\n}\nreturn results;"
"method": "GET",
"url": "={{ $env.MAIL_BRIDGE_URL }}/recent?days=1",
"options": {
"timeout": 15000
}
},
"id": "m1000001-0000-0000-0000-000000000011",
"name": "Fetch Mails",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
220,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT message_id FROM mail_logs WHERE mail_date >= NOW() - INTERVAL '2 days'",
"options": {}
},
"id": "m1000001-0000-0000-0000-000000000012",
"name": "Get Existing IDs",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
440,
300
],
"credentials": {
"postgres": {
"id": "KaxU8iKtraFfsrTF",
"name": "bot-postgres"
}
}
},
{
"parameters": {
"jsCode": "const mailsResponse = $('Fetch Mails').first().json;\nconst mails = mailsResponse.mails || [];\nconst existingRows = $input.all();\nconst existingIds = new Set(existingRows.map(r => r.json.message_id).filter(Boolean));\n\nconst results = [];\nfor (const mail of mails) {\n const mid = mail.messageId || '';\n if (!mid || existingIds.has(mid)) continue;\n results.push({ json: {\n from: mail.from || '',\n subject: (mail.subject || '').substring(0, 500),\n body: (mail.text || '').substring(0, 5000).replace(/<[^>]*>/g, '').replace(/\\s+/g, ' ').trim(),\n mailDate: mail.date || new Date().toISOString(),\n messageId: mid\n }});\n}\nreturn results;"
},
"id": "m1000001-0000-0000-0000-000000000002",
"name": "Parse Mail",
"name": "Parse & Filter New",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [
220,
660,
300
]
},
@@ -50,14 +82,14 @@
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [
440,
880,
300
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "=INSERT INTO mail_logs (from_address,subject,summary,label,has_events,has_tasks,mail_date) VALUES ('{{ ($json.from||'').replace(/'/g,\"''\").substring(0,255) }}','{{ ($json.subject||'').replace(/'/g,\"''\").substring(0,500) }}','{{ ($json.summary||'').replace(/'/g,\"''\").substring(0,2000) }}','{{ $json.label }}',{{ $json.has_events }},{{ $json.has_tasks }},'{{ $json.mailDate }}')",
"query": "=INSERT INTO mail_logs (from_address,subject,summary,label,has_events,has_tasks,mail_date,message_id) VALUES ('{{ ($json.from||'').replace(/'/g,\"''\").substring(0,255) }}','{{ ($json.subject||'').replace(/'/g,\"''\").substring(0,500) }}','{{ ($json.summary||'').replace(/'/g,\"''\").substring(0,2000) }}','{{ $json.label }}',{{ $json.has_events }},{{ $json.has_tasks }},'{{ $json.mailDate }}','{{ ($json.messageId||'').replace(/'/g,\"''\").substring(0,500) }}') ON CONFLICT (message_id) DO NOTHING",
"options": {}
},
"id": "m1000001-0000-0000-0000-000000000004",
@@ -65,7 +97,7 @@
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
660,
1100,
300
],
"credentials": {
@@ -84,7 +116,7 @@
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [
880,
1320,
300
]
},
@@ -125,7 +157,7 @@
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1100,
1540,
300
]
},
@@ -145,24 +177,46 @@
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1320,
1760,
200
]
}
],
"connections": {
"IMAP Trigger": {
"Schedule Trigger": {
"main": [
[
{
"node": "Parse Mail",
"node": "Fetch Mails",
"type": "main",
"index": 0
}
]
]
},
"Parse Mail": {
"Fetch Mails": {
"main": [
[
{
"node": "Get Existing IDs",
"type": "main",
"index": 0
}
]
]
},
"Get Existing IDs": {
"main": [
[
{
"node": "Parse & Filter New",
"type": "main",
"index": 0
}
]
]
},
"Parse & Filter New": {
"main": [
[
{
@@ -222,4 +276,4 @@
"settings": {
"executionOrder": "v1"
}
}
}

File diff suppressed because one or more lines are too long