diff --git a/plugins/NanoImage/images/nano_20251230_110449_f74f62aa.jpg b/plugins/NanoImage/images/nano_20251230_110449_f74f62aa.jpg new file mode 100644 index 0000000..dd29154 Binary files /dev/null and b/plugins/NanoImage/images/nano_20251230_110449_f74f62aa.jpg differ diff --git a/plugins/NanoImage/main.py b/plugins/NanoImage/main.py index 5a45c43..311e802 100644 --- a/plugins/NanoImage/main.py +++ b/plugins/NanoImage/main.py @@ -10,6 +10,7 @@ import tomllib import httpx import uuid import base64 +import re from pathlib import Path from datetime import datetime from typing import List, Optional @@ -93,58 +94,117 @@ class NanoImage(PluginBase): async with client.stream("POST", url, json=payload, headers=headers) as response: logger.debug(f"收到响应状态码: {response.status_code}") if response.status_code == 200: - # 处理流式响应 + content_type = (response.headers.get("content-type") or "").lower() + is_sse = "text/event-stream" in content_type or "event-stream" in content_type + + # 处理流式响应(SSE) image_url = None image_base64 = None full_content = "" - async for line in response.aiter_lines(): - if line.startswith("data: "): - data_str = line[6:] - if data_str == "[DONE]": - break - try: - import json - data = json.loads(data_str) - if "choices" in data and data["choices"]: - delta = data["choices"][0].get("delta", {}) - - # 方式1: 从 delta.images 中提取(新格式) - images = delta.get("images", []) - if images and len(images) > 0: - img_data = images[0].get("image_url", {}).get("url", "") - if img_data: - if img_data.startswith("data:image"): - # base64 格式 - image_base64 = img_data - logger.info(f"从 delta.images 提取到 base64 图片") - elif img_data.startswith("http"): - image_url = img_data - logger.info(f"从 delta.images 提取到图片URL: {image_url}") - - # 方式2: 从 content 中提取(旧格式) - content = delta.get("content", "") - if content: - full_content += content - if "http" in content: - import re - urls = re.findall(r'https?://[^\s\)\]"\']+', content) - if urls: - image_url = urls[0].rstrip("'\"") - logger.info(f"从 content 提取到图片URL: {image_url}") - except Exception as e: - logger.warning(f"解析响应数据失败: {e}") + if is_sse: + async for line in response.aiter_lines(): + if not line: continue + if line.startswith("data:"): + data_str = line[5:].lstrip() + if data_str == "[DONE]": + break + try: + import json + data = json.loads(data_str) + if "choices" in data and data["choices"]: + delta = data["choices"][0].get("delta", {}) + + # 方式1: 从 delta.images 中提取(新格式) + images = delta.get("images", []) + if images and len(images) > 0: + img_data = images[0].get("image_url", {}).get("url", "") + if img_data: + if img_data.startswith("data:image"): + # base64 格式 + image_base64 = img_data + logger.info("从 delta.images 提取到 base64 图片") + elif img_data.startswith("http"): + image_url = img_data + logger.info(f"从 delta.images 提取到图片URL: {image_url}") + + # 方式2: 从 content 中提取(旧格式) + content = delta.get("content", "") + if content: + full_content += content + if "http" in content: + urls = re.findall(r'https?://[^\s\)\]"\']+', content) + if urls: + image_url = urls[0].rstrip("'\"") + logger.info(f"从 content 提取到图片URL: {image_url}") + except Exception as e: + logger.warning(f"解析响应数据失败: {e}") + continue + else: + # 非流式(application/json):某些网关即使传了 stream=true 也会返回完整 JSON + raw = await response.aread() + try: + import json + data = json.loads(raw.decode("utf-8", errors="ignore")) + except Exception as e: + logger.error(f"解析 JSON 响应失败: {type(e).__name__}: {e}") + data = None + + if isinstance(data, dict): + # 1) 标准 images endpoint 兼容:{"data":[{"url":...}|{"b64_json":...}]} + items = data.get("data") + if isinstance(items, list) and items: + first = items[0] if isinstance(items[0], dict) else {} + if isinstance(first, dict): + b64_json = first.get("b64_json") + if b64_json: + image_base64 = b64_json + logger.info("从 data[0].b64_json 提取到 base64 图片") + else: + u = first.get("url") or "" + if isinstance(u, str) and u: + image_url = u + logger.info(f"从 data[0].url 提取到图片URL: {image_url}") + + # 2) chat.completion 兼容:choices[0].message.images[0].image_url.url + if not image_url and not image_base64: + try: + choices = data.get("choices") or [] + if choices: + msg = (choices[0].get("message") or {}) if isinstance(choices[0], dict) else {} + images = msg.get("images") or [] + if isinstance(images, list) and images: + img0 = images[0] if isinstance(images[0], dict) else {} + if isinstance(img0, dict): + img_data = ( + (img0.get("image_url") or {}).get("url") + if isinstance(img0.get("image_url"), dict) + else img0.get("url") + ) + if isinstance(img_data, str) and img_data: + if img_data.startswith("data:image"): + image_base64 = img_data + logger.info("从 message.images 提取到 base64 图片") + elif img_data.startswith("http"): + image_url = img_data + logger.info(f"从 message.images 提取到图片URL: {image_url}") + except Exception: + pass # 如果没有从流中提取到URL,尝试从完整内容中提取 if not image_url and not image_base64 and full_content: - import re urls = re.findall(r'https?://[^\s\)\]"\']+', full_content) if urls: image_url = urls[0].rstrip("'\"") logger.info(f"从完整内容提取到图片URL: {image_url}") if not image_url and not image_base64: - logger.error(f"未能提取到图片,完整响应: {full_content[:500]}") + # 避免把 base64 打到日志里:只输出裁剪后的概要 + if full_content: + logger.error(f"未能提取到图片,完整响应(截断): {full_content[:500]}") + else: + # 非SSE时 full_content 可能为空,补充输出 content-type 便于定位 + logger.error(f"未能提取到图片(content-type={content_type or 'unknown'})") # 处理 base64 图片 if image_base64: