note-connector 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/py/pyproject.toml +1 -1
- package/py/src/note_mcp/api/images.py +77 -13
- package/py/src/note_mcp/server.py +6 -2
package/package.json
CHANGED
package/py/pyproject.toml
CHANGED
|
@@ -117,6 +117,43 @@ _MAGIC_BYTES: dict[str, tuple[bytes, int]] = {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
def _extract_image_url_from_response(response: dict[str, Any]) -> str | None:
|
|
121
|
+
"""Extract image URL from API response, trying multiple possible field names.
|
|
122
|
+
|
|
123
|
+
note.com API may return the URL under different field names.
|
|
124
|
+
This function tries known field names in order of preference.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
response: Full API response dictionary
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Image URL string if found, None otherwise
|
|
131
|
+
"""
|
|
132
|
+
data = response.get("data", {})
|
|
133
|
+
|
|
134
|
+
# Try known field names for image URL
|
|
135
|
+
candidate_keys = [
|
|
136
|
+
"url",
|
|
137
|
+
"image_url",
|
|
138
|
+
"src",
|
|
139
|
+
"download_url",
|
|
140
|
+
"note_image_url",
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
for key in candidate_keys:
|
|
144
|
+
value = data.get(key)
|
|
145
|
+
if value and isinstance(value, str) and value.strip():
|
|
146
|
+
return str(value)
|
|
147
|
+
|
|
148
|
+
# Check if the top-level response itself is a string URL
|
|
149
|
+
for key in candidate_keys:
|
|
150
|
+
value = response.get(key)
|
|
151
|
+
if value and isinstance(value, str) and value.strip():
|
|
152
|
+
return str(value)
|
|
153
|
+
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
120
157
|
def validate_image_file(file_path: str) -> None:
|
|
121
158
|
"""Validate image file before upload.
|
|
122
159
|
|
|
@@ -207,25 +244,36 @@ async def _upload_image_internal(
|
|
|
207
244
|
async with NoteAPIClient(session) as client:
|
|
208
245
|
response = await client.post(endpoint, files=files, data=data)
|
|
209
246
|
|
|
210
|
-
#
|
|
211
|
-
|
|
247
|
+
# Debug: log full API response for investigation
|
|
248
|
+
logger.debug(
|
|
249
|
+
"Image upload response for note_id=%s, endpoint=%s: %s",
|
|
250
|
+
numeric_note_id,
|
|
251
|
+
endpoint,
|
|
252
|
+
{k: v for k, v in response.items() if k != "data"},
|
|
253
|
+
)
|
|
254
|
+
if "data" in response:
|
|
255
|
+
logger.debug("Image upload response data keys: %s", list(response["data"].keys()))
|
|
256
|
+
logger.debug("Image upload response data: %s", response["data"])
|
|
212
257
|
|
|
213
|
-
#
|
|
214
|
-
|
|
215
|
-
|
|
258
|
+
# Extract image URL from response (tries multiple field names)
|
|
259
|
+
image_url = _extract_image_url_from_response(response)
|
|
260
|
+
|
|
261
|
+
image_data = response.get("data", {})
|
|
216
262
|
image_key = image_data.get("key")
|
|
217
263
|
|
|
218
|
-
|
|
264
|
+
# URL is optional - eyecatch API may not always return it
|
|
265
|
+
# The eyecatch is set server-side even without a URL in the response
|
|
219
266
|
if not image_url:
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
267
|
+
logger.warning(
|
|
268
|
+
"Image upload response missing image URL for note_id=%s. Response keys: top=%s, data=%s",
|
|
269
|
+
numeric_note_id,
|
|
270
|
+
list(response.keys()),
|
|
271
|
+
list(image_data.keys()) if image_data else "none",
|
|
224
272
|
)
|
|
225
273
|
|
|
226
274
|
return Image(
|
|
227
275
|
key=str(image_key) if image_key else None,
|
|
228
|
-
url=str(image_url),
|
|
276
|
+
url=str(image_url) if image_url else "",
|
|
229
277
|
original_path=file_path,
|
|
230
278
|
size_bytes=file_size,
|
|
231
279
|
uploaded_at=int(time.time()),
|
|
@@ -522,14 +570,30 @@ def _decode_base64_image(image_base64: str) -> bytes:
|
|
|
522
570
|
"""
|
|
523
571
|
clean = _strip_data_url_prefix(image_base64)
|
|
524
572
|
|
|
525
|
-
|
|
573
|
+
# Strip whitespace and newlines (common in ChatGPT output)
|
|
574
|
+
clean = re.sub(r"\s+", "", clean)
|
|
575
|
+
|
|
576
|
+
if not clean:
|
|
526
577
|
raise NoteAPIError(
|
|
527
578
|
code=ErrorCode.INVALID_BASE64,
|
|
528
579
|
message="image_base64 が空です。",
|
|
529
580
|
)
|
|
530
581
|
|
|
582
|
+
# Fix missing padding: base64 length must be a multiple of 4
|
|
583
|
+
# ChatGPT sometimes drops trailing '=' padding chars
|
|
584
|
+
remainder = len(clean) % 4
|
|
585
|
+
if remainder:
|
|
586
|
+
clean += "=" * (4 - remainder)
|
|
587
|
+
|
|
531
588
|
try:
|
|
532
|
-
|
|
589
|
+
# validate=False: tolerate non-strict base64 (extra chars, minor format issues)
|
|
590
|
+
result = base64.b64decode(clean, validate=False)
|
|
591
|
+
if not result:
|
|
592
|
+
raise NoteAPIError(
|
|
593
|
+
code=ErrorCode.INVALID_BASE64,
|
|
594
|
+
message="image_base64 をデコードしましたが、データが空でした。",
|
|
595
|
+
)
|
|
596
|
+
return result
|
|
533
597
|
except (binascii.Error, ValueError) as e:
|
|
534
598
|
raise NoteAPIError(
|
|
535
599
|
code=ErrorCode.INVALID_BASE64,
|
|
@@ -285,7 +285,9 @@ async def note_upload_eyecatch(
|
|
|
285
285
|
アップロード結果(画像URLを含む)
|
|
286
286
|
"""
|
|
287
287
|
image = await upload_eyecatch_image(session, file_path, note_id=note_id)
|
|
288
|
-
|
|
288
|
+
if image.url:
|
|
289
|
+
return f"アイキャッチ画像をアップロードしました。URL: {image.url}"
|
|
290
|
+
return "アイキャッチ画像をアップロードしました。"
|
|
289
291
|
|
|
290
292
|
|
|
291
293
|
@mcp.tool()
|
|
@@ -319,7 +321,9 @@ async def note_set_eyecatch_base64(
|
|
|
319
321
|
mime_type=mime_type,
|
|
320
322
|
image_base64=image_base64,
|
|
321
323
|
)
|
|
322
|
-
|
|
324
|
+
if image.url:
|
|
325
|
+
return f"アイキャッチ画像を設定しました。URL: {image.url}"
|
|
326
|
+
return "アイキャッチ画像を設定しました。"
|
|
323
327
|
|
|
324
328
|
|
|
325
329
|
@mcp.tool()
|