Skip to content

Commit fa9ca63

Browse files
ArcSolverclaude
andcommitted
fix(zotero): 독립 검증 반영 + 카탈로그/매니페스트 통합
머지 전 독립·적대적 검증(공식 Web API v3 + server_localAPI.js 대조) 후 교정: - 로컬 백엔드가 그룹 라이브러리(groups/<id>)도 지원하도록 _resolve 수정 (공식 로컬 소스가 /api/groups/:groupID 라우트 제공 — 기존 users/0 고정은 과도한 축소). - qmode가 '아이템 검색 전용'(titleCreatorYear/everything)임을 계약에 명시. 태그 qmode(contains/startsWith)는 MVP 범위 밖이며 list_tags는 qmode 미전달. - search docstring에 q=제목/저자 quick search·전문은 qmode=everything 명시. - UNVERIFIED 주석 정확화(library/links/meta 일부 서브키는 공식 명시 — dict 유지는 정당). 통합: providers zotero 상태 done, 카탈로그 5서비스·27도구 재생성, CHANGELOG 합본. 런타임 테스트에 로컬+그룹 prefix 케이스 추가. 검증: pytest 171 passed, ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 317b07f commit fa9ca63

6 files changed

Lines changed: 48 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@
2222
- **telegram**: sendMessage 기반 telegram_send_message 추가
2323
- **telegram**: 코어 도구 확장 — getMe(헬스체크)/sendPhoto/sendDocument/editMessageText/deleteMessage 추가
2424
- **telegram**: sendPhoto/sendDocument 로컬 파일 multipart 업로드 지원(사진≤10MB·파일≤50MB), editMessageText inline_message_id 경로 추가
25+
- **zotero**: Zotero 라이브러리 읽기 서비스 추가(Web API v3 + 로컬 데스크톱 API 단일 서비스·백엔드 전환) — 검색/아이템/자식/컬렉션/컬렉션 아이템/태그/전문/헬스 8개 GET 도구, 응답 헤더 기반 페이지네이션 안내
2526
<!-- END UNRELEASED -->

arcsolve/services/zotero/contract.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,10 @@ def tags_path(prefix: str) -> str:
138138
MAX_ITEMKEYS = 50
139139
MAX_BIB_ITEMS = 150
140140

141-
# qmode 허용값. titleCreatorYear(기본) / everything(전문 포함).
142-
# 출처: basics — "qmode" 파라미터 ("titleCreatorYear" 또는 "everything")
141+
# qmode 허용값 — **아이템 quick search 전용**. titleCreatorYear(기본) / everything(전문 포함).
142+
# (태그 엔드포인트는 별도 qmode contains/startsWith를 쓴다 — MVP 범위 밖이라 여기서 다루지 않으며,
143+
# list_tags는 qmode를 보내지 않는다.)
144+
# 출처: basics — items "qmode" ("titleCreatorYear" 또는 "everything")
143145
QMode = Literal["titleCreatorYear", "everything"]
144146
QMODES: tuple[str, ...] = ("titleCreatorYear", "everything")
145147

@@ -164,9 +166,10 @@ def build_search_params(
164166
limit: int = DEFAULT_LIMIT,
165167
start: int = 0,
166168
) -> dict[str, str | int]:
167-
"""검색/리스트 쿼리스트링을 만든다. None/빈값은 생략한다.
169+
"""**아이템 검색/리스트** 쿼리스트링을 만든다. None/빈값은 생략한다.
168170
169-
공식 파라미터명: q · qmode · itemType · tag · sort · limit · start.
171+
공식 파라미터명: q · qmode · itemType · tag · sort · limit · start. qmode는 아이템 전용
172+
(titleCreatorYear/everything). 컬렉션 아이템·태그 리스트는 qmode 없이 limit/start만 쓴다.
170173
출처: https://www.zotero.org/support/dev/web_api/v3/basics
171174
"""
172175
validate_limit(limit)
@@ -230,9 +233,10 @@ class ZoteroItem(BaseModel):
230233
공식 최상위 키: key · version · library · links · meta · data.
231234
출처: https://www.zotero.org/support/dev/web_api/v3/basics (Read Requests / format=json)
232235
233-
UNVERIFIED: library/links/meta 서브객체의 정확한 키는 공식 산문에서 확정 못 했다.
234-
→ dict로 느슨히 받는다(extra="ignore"). data는 아이템 타입별 가변 필드 → dict.
235-
# TODO(provenance): library/links/meta 서브객체 키를 실 응답으로 확정해 모델 정밀화.
236+
참고: 공식에 library(type/id/name) · links(rel별 {href,type}) · meta(numChildren/
237+
creatorSummary/parsedDate 등) 일부 서브키가 명시되나 전수 스키마는 미열거 → dict로 느슨히
238+
받는다(extra="ignore"). data는 아이템 타입별 가변 필드 → dict.
239+
# TODO(provenance): 필요 시 library/links/meta 핵심 서브키를 모델로 승격.
236240
"""
237241

238242
model_config = {"extra": "ignore"}

arcsolve/services/zotero/tools.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,10 @@ def _resolve(s: ZoteroSettings) -> tuple[str, str, dict[str, str], str]:
6767
"""
6868
source = s.resolved_source()
6969
if source == "local":
70-
# 로컬은 무인증·읽기전용·users/0 고정.
71-
return (
72-
s.local_base.rstrip("/"),
73-
z.user_prefix(z.LOCAL_USER_ID),
74-
z.base_headers(None),
75-
"local",
76-
)
70+
# 로컬은 무인증·읽기전용. 본인 사용자 데이터는 users/0이지만, 그룹 라이브러리는
71+
# groups/<id>도 지원한다(server_localAPI.js가 /api/groups/:groupID/... 라우트 제공).
72+
prefix = z.group_prefix(s.group_id) if s.group_id else z.user_prefix(z.LOCAL_USER_ID)
73+
return (s.local_base.rstrip("/"), prefix, z.base_headers(None), "local")
7774
if source == "web":
7875
if s.group_id:
7976
prefix = z.group_prefix(s.group_id)
@@ -178,7 +175,8 @@ async def zotero_search_items(
178175
"""Zotero 라이브러리에서 아이템을 검색/나열한다(GET /{prefix}/items).
179176
180177
Args:
181-
q: 검색어. 미지정 시 라이브러리의 상위 아이템을 나열한다.
178+
q: 검색어(기본은 제목·저자 quick search). 전문까지 포함하려면 qmode='everything'.
179+
미지정 시 라이브러리 아이템을 나열한다.
182180
item_type: itemType 필터(예: book, journalArticle). 부정은 '-' 접두(예: -attachment).
183181
tag: 태그 필터.
184182
limit: 페이지 크기. 기본 25, 최대 100.

docs/providers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
---
7373

7474
## zotero — Zotero 라이브러리 읽기 (Web API v3 + 로컬 데스크톱 API, 단일 서비스·백엔드 전환)
75-
- 상태: `planned`
75+
- 상태: `done`
7676
- 구조: **한 서비스 = 두 백엔드.** 로컬 API는 Web API v3를 미러하므로 계약(경로·쿼리·응답 모델)이 거의 동일.
7777
`ZOTERO_SOURCE=web|local`(미지정 시 API 키 있으면 web, 없으면 local 자동)로 **base URL·인증만** 분기.
7878
- 인증:

docs/services.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> ⚙️ 자동 생성 — 직접 수정하지 마세요. `arcsolve-mcp catalog`로 재생성됩니다.
44
5-
현재 **4개 서비스 · 총 19개 도구**.
5+
현재 **5개 서비스 · 총 27개 도구**.
66

77
## discord — Discord — Webhook으로 채널에 메시지 전송
88
공식 문서: https://discord.com/developers/docs/resources/webhook
@@ -47,3 +47,17 @@
4747
| `telegram_send_message` | Telegram 봇으로 텍스트 메시지를 전송한다(sendMessage). |
4848
| `telegram_send_photo` | Telegram 봇으로 사진을 전송한다(sendPhoto). |
4949

50+
## zotero — Zotero 라이브러리 읽기(Web API v3 + 로컬 데스크톱 API)
51+
공식 문서: https://www.zotero.org/support/dev/web_api/v3/basics
52+
53+
| 도구 | 설명 |
54+
|------|------|
55+
| `zotero_get_collection_items` | 컬렉션의 아이템을 나열한다(GET /{prefix}/collections/{collectionKey}/items). |
56+
| `zotero_get_fulltext` | 첨부 아이템의 전문(full-text)을 조회한다(GET /{prefix}/items/{itemKey}/fulltext). |
57+
| `zotero_get_item` | 단일 아이템을 조회한다(GET /{prefix}/items/{itemKey}). |
58+
| `zotero_get_item_children` | 아이템의 자식(노트/첨부)을 나열한다(GET /{prefix}/items/{itemKey}/children). |
59+
| `zotero_health` | 백엔드 연결/설정 상태를 점검한다. |
60+
| `zotero_list_collections` | 컬렉션을 나열한다(GET /{prefix}/collections, top=True면 /collections/top). |
61+
| `zotero_list_tags` | 라이브러리의 태그를 나열한다(GET /{prefix}/tags). |
62+
| `zotero_search_items` | Zotero 라이브러리에서 아이템을 검색/나열한다(GET /{prefix}/items). |
63+

tests/test_zotero_tools.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ async def test_local_backend_uses_users0_and_no_auth(monkeypatch, load_tools, re
6060
assert http.last["headers"] == {"Zotero-API-Version": "3"}
6161

6262

63+
async def test_local_backend_supports_group_prefix(monkeypatch, load_tools, recording_http):
64+
# 로컬도 그룹 라이브러리(groups/<id>)를 지원한다(server_localAPI.js 라우트).
65+
monkeypatch.setenv("ZOTERO_SOURCE", "local")
66+
monkeypatch.delenv("ZOTERO_API_KEY", raising=False)
67+
monkeypatch.setenv("ZOTERO_GROUP_ID", "999")
68+
tools = load_tools(register)
69+
http = recording_http(ret=([], {}))
70+
monkeypatch.setattr(f"{MOD}.get_with_headers", http)
71+
72+
await tools["zotero_search_items"]()
73+
assert http.last["url"].startswith("http://localhost:23119/api/groups/999/items")
74+
assert http.last["headers"] == {"Zotero-API-Version": "3"} # 로컬은 무인증
75+
76+
6377
async def test_get_item_parse(zt, monkeypatch, recording_http):
6478
item = {"key": "ABC", "version": 9, "data": {"itemType": "journalArticle", "title": "Paper"}}
6579
http = recording_http(ret=item)

0 commit comments

Comments
 (0)