0cbba0ceeb
DEVONagent/DEVONthink 가 발견한 웹페이지를 NAS Web/ drop → file_watcher
ingest → extract 4-tier fallback (trafilatura/sibling-md/readability/bs4)
→ embed + chunk 까지. classify/preview/markdown SKIP.
- source_channel='devonagent' (migration 001 dormant 활성화)
- file_watcher: SCAN_TARGETS 통합 + Web/ rglob + canonical_url dedup +
sidecar 누락 정책 (skip 안 함, web_meta.sidecar_missing=true flag)
- extract_worker: HTML+devonagent 분기 + md_extraction_engine 4-tier 구분
(trafilatura → sibling .md ≥200char → readability+markdownify → bs4_text)
- queue_consumer: enqueue_next_stage 의 extract stage 만 source_channel-
aware override (devonagent → [embed, chunk])
- classify_worker: devonagent safety skip (law_monitor 패턴 mirror,
ai_domain='Web', ai_tags=['Web/{host}'])
- requirements: trafilatura/readability-lxml/markdownify 추가
- docs: devonthink-web-bridge.md 설치 가이드 + first-wins 정책 명시
Phase 1 closure 기준 = 재료 품질 (검색 가능 + 노이즈율 + dedup + 엔진 분포).
활용처(ai_tldr/digest/PKM 회고)는 1-2주 OR 30-50건 관찰 후 별 PR 에서 결정.
Plan: ~/.claude/plans/db-snuggly-petal.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 KiB
11 KiB
DEVONthink → Document Server Web Bridge (devonagent 트랙)
DEVONagent / DEVONthink 가 발견·저장한 웹페이지를 Document Server 의 검색 가능한 재료로
보내기 위한 수동 설치 가이드. Plan: ~/.claude/plans/db-snuggly-petal.md.
흐름
DEVONagent (smart agent — 사용자 운영)
↓
DEVONthink Inbox / tagged group (web/ingest)
↓ Smart Rule (AppleScript)
NAS /volume4/Document_Server/Web/{domain}/{YYYY-MM-DD}/{slug}.{html,md,json}
↓ NFS → GPU file_watcher (5분 간격)
documents row (source_channel='devonagent') + extract → embed → chunk
↓
/api/search + bge-reranker-v2-m3 검색 가능 상태
정책 (Phase 1)
- 첫 ingest 만 유지 (first-wins): 같은
canonical_url은 한 번만 documents row 생성. DEVONthink 에서 같은 글을 다시 저장해도 내용이 갱신되지 않는다. UTM 파라미터 변형 (?utm_source=foo) 과 fragment (#section) 도 정규화되어 한 row 로 수렴. 업데이트 버전 관리는 추후 별 PR (PR-Web-Update-Policy) 에서 다룬다. - AI 가공 미적용: 이 단계는 "검색 가능한 재료" 까지만. ai_tldr / ai_bullets / 카테고리 자동 태깅은 별 PR (Mac mini derived-worker) 에서 결정.
- Sidecar (.json) 누락 시: skip 안 하고 ingest.
extract_meta.web_meta.sidecar_missing=true로 표시. URL 정보가 없어 검색 evidence 가치는 줄지만 침묵 누락보다 낫다.
NAS 경로 규칙
/volume4/Document_Server/Web/
├── example.com/
│ ├── 2026-05-15/
│ │ ├── sample-post.html # 본문 HTML
│ │ ├── sample-post.md # DEVONthink rendered markdown (fallback 용)
│ │ └── sample-post.json # 메타 sidecar
│ └── 2026-05-14/
│ └── another-post.html
└── ...
- 도메인:
urlparse(url).hostname의 lowercase - 날짜:
creation date의YYYY-MM-DD(KST 또는 UTC, 일관 유지) - slug: 파일명 안전한 형태로 변환 (영숫자/하이픈/언더스코어만)
Sidecar JSON 스키마
{
"title": "Sample Blog Post Title",
"url": "https://example.com/sample-post?utm_source=newsletter#main",
"author": "Author Name",
"pub_date": "2026-05-15T09:00:00Z",
"devonthink_uuid": "DEADBEEF-1234-5678-90AB-CDEF12345678",
"source_agent": "web-ingest"
}
title,url필수 (둘 다 없으면 sidecar_missing 처리)pub_date는 ISO 8601 UTC 권장 (한국 시간이면 명시적 +09:00)source_agent는 어떤 smart agent 가 수집했는지 (분석용 메타, 옵션)
DEVONthink Smart Rule 설치
1. Smart Rule 생성
DEVONthink 3 메뉴 → Tools → Smart Rules → + (새 규칙).
- Name:
Web → NAS for GPU ingest - Trigger:
On Adding Item to(Inbox) — Inbox 자동 처리- 또는
On Tagging Item—web/ingest태그 붙으면 발동 (수동 큐레이션 선호 시)
- Conditions (옵션):
KindisWebArchiveorHTMLorMarkdownURLis not empty
2. Action: Execute Script
다음 AppleScript 본문을 Action Scripts 영역에 붙여넣는다. NAS 경로
/Volumes/Document_Server 는 macOS 가 마운트한 SMB/AFP volume 이라고 가정한다.
(다른 mount point 면 kBaseDir 만 수정.)
-- DEVONthink Smart Rule: Web → NAS for GPU ingest
-- Plan: ~/.claude/plans/db-snuggly-petal.md
property kBaseDir : "/Volumes/Document_Server/Web"
on slugify(theText)
set theResult to ""
repeat with c in theText
set ch to c as string
set asciiVal to (id of ch)
if (asciiVal ≥ 48 and asciiVal ≤ 57) or ¬
(asciiVal ≥ 65 and asciiVal ≤ 90) or ¬
(asciiVal ≥ 97 and asciiVal ≤ 122) or ¬
ch is "-" or ch is "_" then
set theResult to theResult & ch
else if ch is " " or ch is "." or ch is "/" then
set theResult to theResult & "-"
end if
end repeat
if theResult is "" then set theResult to "untitled"
if (length of theResult) > 80 then ¬
set theResult to text 1 thru 80 of theResult
return theResult
end slugify
on hostnameFromURL(theURL)
try
set delim to "://"
set AppleScript's text item delimiters to delim
set tail to text item 2 of theURL
set AppleScript's text item delimiters to "/"
set host to text item 1 of tail
set AppleScript's text item delimiters to ""
-- strip port + 소문자
set AppleScript's text item delimiters to ":"
set host to text item 1 of host
set AppleScript's text item delimiters to ""
return do shell script "echo " & quoted form of host & " | tr 'A-Z' 'a-z'"
on error
return "unknown"
end try
end hostnameFromURL
on isoDate(theDate)
set y to year of theDate as string
set m to month of theDate as integer
set d to day of theDate as integer
if m < 10 then set m to "0" & m
if d < 10 then set d to "0" & d
return y & "-" & m & "-" & d
end isoDate
on performSmartRule(theRecords)
tell application id "DNtp"
repeat with theRecord in theRecords
try
set theURL to URL of theRecord
if theURL is missing value or theURL is "" then
log message "Web→NAS: URL 없음, skip — " & (name of theRecord)
-- continue
else
set theName to name of theRecord
set theUUID to uuid of theRecord
set theAuthor to ""
try
set theAuthor to (meta data of theRecord)'s |author|
end try
set theDate to (creation date of theRecord)
set dateStr to my isoDate(theDate)
set host to my hostnameFromURL(theURL)
set slug to my slugify(theName)
set targetDir to kBaseDir & "/" & host & "/" & dateStr
do shell script "mkdir -p " & quoted form of targetDir
set htmlPath to targetDir & "/" & slug & ".html"
set mdPath to targetDir & "/" & slug & ".md"
set jsonPath to targetDir & "/" & slug & ".json"
-- 1) HTML export
try
export record theRecord to htmlPath as HTML
on error errMsg
log message "Web→NAS HTML export 실패 (" & theName & "): " & errMsg
end try
-- 2) Markdown export (DEVONthink rendered, trafilatura fallback)
try
export record theRecord to mdPath as markdown
end try
-- 3) JSON sidecar
set pubISO to do shell script ¬
"date -u +%Y-%m-%dT%H:%M:%SZ -r " & ¬
(do shell script "stat -f %m " & quoted form of htmlPath)
set jsonText to "{" & ¬
"\"title\":" & my jsonEsc(theName) & "," & ¬
"\"url\":" & my jsonEsc(theURL) & "," & ¬
"\"author\":" & my jsonEsc(theAuthor) & "," & ¬
"\"pub_date\":\"" & pubISO & "\"," & ¬
"\"devonthink_uuid\":\"" & theUUID & "\"," & ¬
"\"source_agent\":\"smart-rule:web-ingest\"" & ¬
"}"
do shell script "cat > " & quoted form of jsonPath & ¬
" <<'EOF'" & linefeed & jsonText & linefeed & "EOF"
log message "Web→NAS: " & theName & " → " & host & "/" & dateStr
end if
on error errMsg
log message "Web→NAS 처리 실패: " & errMsg
end try
end repeat
end tell
end performSmartRule
on jsonEsc(theText)
if theText is missing value then return "\"\""
set s to theText as string
-- 최소 escape: backslash 와 따옴표
set AppleScript's text item delimiters to "\\"
set parts to text items of s
set AppleScript's text item delimiters to "\\\\"
set s to parts as string
set AppleScript's text item delimiters to "\""
set parts to text items of s
set AppleScript's text item delimiters to "\\\""
set s to parts as string
set AppleScript's text item delimiters to ""
return "\"" & s & "\""
end jsonEsc
참고: 위 스크립트는 시작점이다. 실제 사용 시 다음을 점검하라.
kBaseDir경로가 실제 NAS mount 와 일치하는지creation date가 글의 실제 발행일이 아닐 수 있음 (DEVONthink 가 저장한 시점) — 필요하면meta data → date사용- JSON escape 가 한국어/특수문자에서 깨지는지 →
do shell script "python3 -c ..."로 대체하는 게 안전
3. 동작 확인
- DEVONthink 에서 웹페이지를 Inbox 에 저장 (단축키
^⌥⌘)또는 Clip to DEVONthink) - Smart Rule 이 자동 발동 (혹은 우클릭 →
Apply Rule) /Volumes/Document_Server/Web/{host}/{date}/{slug}.{html,md,json}3종 생성 확인- 최대 5분 내 GPU file_watcher 가 ingest. SQL 확인:
SELECT id, title, edit_url, md_extraction_engine, md_status FROM documents WHERE source_channel='devonagent' ORDER BY created_at DESC LIMIT 5;
file_watcher 동작 요약
nas_mount_path / "Web"하위를 5분 간격 rglob 으로.html만 수집- 각
.html마다 sibling.json읽어 canonical URL 산출 file_hash = sha256(canonical_url)→ URL identity dedup- documents row 생성 +
processing_queue.stage='extract'등록 - extract_worker 의 4-tier fallback 으로 md_content 채움
source_channel='devonagent'인 doc 은classify/preview/markdownSKIP →embed+chunk만 enqueue
검증 (운영 후)
-- 도메인 분포 (어느 사이트가 많이 들어오는지)
SELECT split_part(edit_url, '/', 3) host, count(*) cnt
FROM documents WHERE source_channel='devonagent' AND edit_url IS NOT NULL
GROUP BY host ORDER BY cnt DESC;
-- 추출 엔진 분포 (bs4_text 비율 모니터링)
SELECT md_extraction_engine, count(*) cnt,
ROUND(100.0 * count(*) / sum(count(*)) OVER (), 1) pct
FROM documents WHERE source_channel='devonagent'
GROUP BY md_extraction_engine ORDER BY cnt DESC;
-- Sidecar 누락 분 (조용한 누락 가시화)
SELECT id, title, file_path
FROM documents
WHERE source_channel='devonagent'
AND extract_meta->'web_meta'->>'sidecar_missing' = 'true';
알려진 한계 (Phase 1)
- JS-rendered 페이지: SPA / React / Vue 로 본문이 client-side 렌더되는 사이트는 HTML 안에 본문 텍스트가 없어 trafilatura 가 빈 결과를 낸다. DEVONthink WebArchive export 가 렌더 결과를 잡아주면 OK, 아니면 bs4_text fallback 도 빈약하다. Playwright 컨테이너는 별 PR.
- 로그인/페이월 콘텐츠: DEVONthink 가 로그인 세션으로 capture 한 경우만 본문 보유.
- canonical_url 정책: 같은 글의 reprint (Medium → 본인 블로그) 는 다른 row 로 ingest 됨. URL identity 만 dedup 기준이다.
- 첫 ingest 만 유지: 글이 후속 편집되어도 갱신 안 됨. 별 PR 에서 정책 결정.