├── __init__.py ├── requirements.txt ├── __pycache__ ├── main.cpython-311.pyc ├── __init__.cpython-311.pyc ├── get_image.cpython-311.pyc └── forward_message.cpython-311.pyc ├── 配置说明.md ├── get_image.py ├── .gitignore ├── forward_message.py ├── config.py ├── message_parser.py ├── README.md ├── main.py ├── platform_sender.py └── nextcloud_client.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httpx>=0.24.0 2 | aiofiles>=23.0.0 3 | aiohttp>=3.8.0 -------------------------------------------------------------------------------- /__pycache__/main.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hanschase/GiveMeSetuPlugin/HEAD/__pycache__/main.cpython-311.pyc -------------------------------------------------------------------------------- /__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hanschase/GiveMeSetuPlugin/HEAD/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /__pycache__/get_image.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hanschase/GiveMeSetuPlugin/HEAD/__pycache__/get_image.cpython-311.pyc -------------------------------------------------------------------------------- /__pycache__/forward_message.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hanschase/GiveMeSetuPlugin/HEAD/__pycache__/forward_message.cpython-311.pyc -------------------------------------------------------------------------------- /配置说明.md: -------------------------------------------------------------------------------- 1 | # GiveMeSetuPlugin 配置说明 2 | 3 | ## 必要配置 4 | 5 | 在使用插件前,请确保修改 `config.py` 中的以下配置项: 6 | 7 | ### NextCloud 配置 8 | ```python 9 | # 将以下配置修改为您的实际NextCloud信息 10 | NEXTCLOUD_URL = "https://your-nextcloud-domain.com" # 您的NextCloud域名 11 | NEXTCLOUD_USERNAME = "your_username" # NextCloud用户名 12 | NEXTCLOUD_PASSWORD = "your_app_password" # 应用专用密码(推荐) 13 | NEXTCLOUD_FOLDER = "/Pictures/Setu" # 图片文件夹路径 14 | ``` 15 | 16 | ### Napcat 配置 17 | ```python 18 | # 根据您的Napcat部署情况调整 19 | NAPCAT_HOST = "127.0.0.1" # Napcat服务器地址 20 | NAPCAT_PORT = 3000 # Napcat服务器端口 21 | ``` 22 | 23 | ### QQ BOT 配置 24 | ```python 25 | # 用于QQ卡片消息显示 26 | BOT_QQ = "your_bot_qq_number" # BOT的QQ号 27 | FORWARD_USER_ID = "sender_qq_number" # 发送者QQ号 28 | BOT_NICKNAME = "BOT" # BOT昵称 29 | ``` 30 | 31 | ## NextCloud 设置指南 32 | 33 | ### 1. 创建应用专用密码 34 | 1. 登录NextCloud 35 | 2. 进入 设置 → 安全 36 | 3. 创建新的应用专用密码 37 | 4. 将生成的密码填入 `NEXTCLOUD_PASSWORD` 38 | 39 | ### 2. 准备图片文件夹 40 | 1. 在NextCloud中创建图片文件夹(如 `/Pictures/Setu`) 41 | 2. 上传一些图片文件 42 | 3. 确保支持的格式:jpg, jpeg, png, gif, webp, bmp 43 | 44 | ## 支持的命令格式 45 | 46 | ``` 47 | 色图|涩图 [-s [lolicon|cloud]] [tag,...] 48 | ``` 49 | 50 | ### 示例命令 51 | - `涩图` - 从默认源随机获取图片 52 | - `涩图 莉音` - 从Lolicon获取标签为"莉音"的图片 53 | - `涩图 -s lolicon 公主连接` - 明确指定从Lolicon获取 54 | - `涩图 -s cloud` - 从NextCloud随机获取图片 55 | - `r18涩图` - 获取R18图片(需要配置权限) 56 | 57 | ## 平台支持 58 | 59 | - **QQ平台**:自动使用卡片模式发送 60 | - **Discord等其他平台**:自动使用文本+图片模式发送 61 | 62 | ## 故障排除 63 | 64 | ### NextCloud连接失败 65 | 1. 检查域名和端口是否正确 66 | 2. 确认用户名和密码无误 67 | 3. 检查网络连接 68 | 4. 确认NextCloud服务正常运行 69 | 70 | ### 图片获取失败 71 | 1. 检查Lolicon API是否可访问 72 | 2. 确认NextCloud文件夹中有图片文件 73 | 3. 检查图片格式是否支持 74 | 4. 查看日志获取详细错误信息 75 | 76 | ### 消息发送失败 77 | 1. 检查Napcat服务是否运行 78 | 2. 确认BOT有发送权限 79 | 3. 检查网络连接 80 | 4. 验证QQ号配置是否正确 81 | -------------------------------------------------------------------------------- /get_image.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import sys 3 | import asyncio 4 | import json 5 | import re 6 | from .config import Config 7 | 8 | config = Config() 9 | 10 | async def get_json(keyword: [],r18_flag: int = 0): 11 | url = config.LOLICON_API_URL 12 | params = { 13 | "tag": keyword, 14 | "r18": r18_flag, 15 | **config.LOLICON_DEFAULT_PARAMS 16 | } 17 | timeout = httpx.Timeout(config.LOLICON_TIMEOUT) 18 | async with httpx.AsyncClient(timeout=timeout) as client: 19 | response = await client.get(url, params=params) 20 | return response 21 | 22 | async def download_image(url: str): 23 | timeout = httpx.Timeout(config.IMAGE_DOWNLOAD_TIMEOUT) 24 | async with httpx.AsyncClient(timeout=timeout) as client: 25 | response = await client.get(url) 26 | if response.status_code == 404: 27 | raise Exception(404) 28 | content = response.content 29 | import aiofiles 30 | async with aiofiles.open(config.TEMP_IMAGE_NAME, 'wb') as f: 31 | await f.write(content) 32 | 33 | async def get_image(keyword: [], r18_flag): 34 | img = await get_json(keyword, r18_flag) 35 | img = img.json() 36 | if img["data"] == []: 37 | raise Exception("输入了未知的tag") 38 | pid = img["data"][0]["pid"] 39 | title = img["data"][0]["title"] 40 | author = img["data"][0]["author"] 41 | img_url = img["data"][0]["urls"]["regular"] 42 | try: 43 | await download_image(img_url) 44 | return { 45 | 'source': 'lolicon', 46 | 'pid': pid, 47 | 'title': title, 48 | 'author': author, 49 | 'image_path': config.TEMP_IMAGE_NAME, 50 | 'tags': keyword 51 | } 52 | except Exception as e: 53 | raise e 54 | 55 | async def main(): 56 | msg = "setu 公主连接".strip() 57 | r18_flag = 0 58 | if re.search(r'setu|涩图|色图', msg, re.IGNORECASE): 59 | msg = msg.split(" ") 60 | keyword = msg[1] 61 | if 'r' in msg[0] or 'R' in msg[0]: 62 | r18_flag = 1 63 | try: 64 | img_info = await get_image(keyword, r18_flag) 65 | except Exception as e: 66 | print(e) 67 | return 68 | img_info = await get_image(keyword, r18_flag) 69 | print(msg) 70 | print(keyword, r18_flag) 71 | print(img_info) 72 | if __name__ == "__main__": 73 | asyncio.run(main()) 74 | pass 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 设计文档.md 132 | config.py.bak 133 | temp.jpg 134 | 135 | -------------------------------------------------------------------------------- /forward_message.py: -------------------------------------------------------------------------------- 1 | import json 2 | import aiohttp 3 | import os 4 | from .config import Config 5 | 6 | class forward_message(): 7 | def __init__(self, host:str = None, port: int = None): 8 | self.config = Config() 9 | self.host = host or self.config.NAPCAT_HOST 10 | self.port = port or self.config.NAPCAT_PORT 11 | self.url = f"http://{self.host}:{self.port}" 12 | 13 | async def send(self, group_id:str, img_info:list): 14 | image = self.get_media_path(self.config.TEMP_IMAGE_NAME) 15 | message_data = { 16 | "group_id": group_id, 17 | "user_id": self.config.FORWARD_USER_ID, 18 | "messages": [ 19 | { 20 | "type": "node", 21 | "data": { 22 | "user_id": self.config.BOT_QQ, 23 | "nickname": self.config.BOT_NICKNAME, 24 | "content": [ 25 | { 26 | "type": "text", 27 | "data": { 28 | "text": f"pid:{img_info[0]}\ntitle:{img_info[1]}\nauthor:{img_info[2]}" 29 | } 30 | }, 31 | { 32 | "type": "image", 33 | "data": { 34 | "file": image, 35 | } 36 | } 37 | ] 38 | } 39 | } 40 | ], 41 | "news": [ 42 | { 43 | "text": f"pid:{img_info[0]}" 44 | } 45 | ], 46 | "prompt": self.config.CARD_MESSAGE_CONFIG["prompt"], 47 | "summary": self.config.CARD_MESSAGE_CONFIG["summary"], 48 | "source": img_info[1] 49 | } 50 | headers = { 51 | 'Content-Type': 'application/json' 52 | } 53 | payload = json.dumps(message_data) 54 | async with aiohttp.ClientSession(self.url, headers=headers) as session: 55 | async with session.post("/send_forward_msg", data=payload) as response: 56 | await response.json() 57 | print(response) 58 | 59 | def get_media_path(self, media_path): 60 | """ 61 | 获取媒体的本地绝对路径或网络路径 62 | """ 63 | if media_path: 64 | if media_path.startswith('http'): 65 | return media_path 66 | elif os.path.isfile(media_path): 67 | abspath = os.path.abspath(os.path.join(os.getcwd(), media_path)).replace('\\', '\\\\') 68 | return f"file:///{abspath}" 69 | return '' -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | """ 2 | GiveMeSetuPlugin 配置管理模块 3 | 统一管理所有配置项,便于维护和扩展 4 | """ 5 | 6 | class Config: 7 | # ==================== Lolicon API 配置 ==================== 8 | # Lolicon API地址 9 | LOLICON_API_URL = "https://api.lolicon.app/setu/v2" 10 | 11 | # 默认请求参数 12 | LOLICON_DEFAULT_PARAMS = { 13 | "num": 1, 14 | "size": "regular", # regular, original, small 15 | } 16 | 17 | # API请求超时时间(秒) 18 | LOLICON_TIMEOUT = 30 19 | 20 | # ==================== NextCloud 配置 ==================== 21 | # NextCloud服务器地址 22 | NEXTCLOUD_URL = "https://your-nextcloud-domain.com" 23 | 24 | # NextCloud用户名 25 | NEXTCLOUD_USERNAME = "your_username" 26 | 27 | # NextCloud应用专用密码(推荐使用应用密码而非账户密码) 28 | NEXTCLOUD_PASSWORD = "your_app_password" 29 | 30 | # NextCloud图片文件夹路径 31 | NEXTCLOUD_FOLDER = "/Pictures/Setu" 32 | 33 | # NextCloud WebDAV路径 34 | NEXTCLOUD_WEBDAV_PATH = "/remote.php/dav/files/{username}" 35 | 36 | # 连接超时时间(秒) 37 | NEXTCLOUD_TIMEOUT = 30 38 | 39 | # ==================== 消息转发配置 ==================== 40 | # Napcat服务器地址 41 | NAPCAT_HOST = "127.0.0.1" 42 | 43 | # Napcat服务器端口 44 | NAPCAT_PORT = 3000 45 | 46 | # 发送消息的用户ID(用于卡片消息显示) 47 | FORWARD_USER_ID = "1900487324" 48 | 49 | # BOT昵称(用于卡片消息显示) 50 | BOT_NICKNAME = "BOT" 51 | 52 | # BOT的QQ号(用于卡片消息显示) 53 | BOT_QQ = "3870128501" 54 | 55 | # 卡片消息配置 56 | CARD_MESSAGE_CONFIG = { 57 | "prompt": "冲就完事儿了", 58 | "summary": "一天就知道冲冲冲" 59 | } 60 | 61 | # ==================== 图片处理配置 ==================== 62 | # 支持的图片格式 63 | SUPPORTED_FORMATS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'] 64 | 65 | # 临时图片文件名(使用绝对路径) 66 | import os 67 | TEMP_IMAGE_NAME = os.path.abspath("temp.jpg") 68 | 69 | # 图片下载超时时间(秒) 70 | IMAGE_DOWNLOAD_TIMEOUT = 60 71 | 72 | # 最大图片文件大小(MB) 73 | MAX_IMAGE_SIZE = 50 74 | 75 | # ==================== 应用行为配置 ==================== 76 | # 默认图片源 (auto, lolicon, cloud) 77 | DEFAULT_SOURCE = "auto" 78 | 79 | # 自动模式下的源选择策略 80 | AUTO_SOURCE_PRIORITY = ["lolicon", "cloud"] 81 | 82 | # 错误重试次数 83 | MAX_RETRY_COUNT = 3 84 | 85 | # R18内容开关 (0: 关闭, 1: 开启, 2: 随机) 86 | DEFAULT_R18_FLAG = 0 87 | 88 | # 启用详细日志 89 | ENABLE_DEBUG_LOG = True 90 | 91 | # NextCloud文件列表缓存时间(秒) 92 | NEXTCLOUD_CACHE_DURATION = 300 # 5分钟 93 | 94 | # 临时文件清理间隔(秒) 95 | TEMP_FILE_CLEANUP_INTERVAL = 3600 # 1小时 96 | 97 | # ==================== 平台检测配置 ==================== 98 | # 支持卡片消息的平台列表 99 | CARD_SUPPORTED_PLATFORMS = ["qq", "onebot"] 100 | 101 | # 平台类型映射 102 | PLATFORM_TYPE_MAPPING = { 103 | "onebot": "qq", 104 | "qq": "qq", 105 | "discord": "discord", 106 | "telegram": "telegram", 107 | "lark": "lark", 108 | "wecom": "wecom", 109 | "slack": "slack", 110 | "unknown": "default" 111 | } 112 | 113 | # 不同平台的消息格式 114 | PLATFORM_MESSAGE_FORMATS = { 115 | "qq": "card", # 卡片模式 116 | "discord": "text", # 纯文本模式 117 | "telegram": "text", 118 | "default": "text" 119 | } 120 | 121 | @classmethod 122 | def get_nextcloud_webdav_url(cls) -> str: 123 | """获取完整的NextCloud WebDAV URL""" 124 | webdav_path = cls.NEXTCLOUD_WEBDAV_PATH.format(username=cls.NEXTCLOUD_USERNAME) 125 | return f"{cls.NEXTCLOUD_URL.rstrip('/')}{webdav_path}" 126 | 127 | @classmethod 128 | def get_napcat_url(cls) -> str: 129 | """获取完整的Napcat API URL""" 130 | return f"http://{cls.NAPCAT_HOST}:{cls.NAPCAT_PORT}" 131 | 132 | @classmethod 133 | def is_platform_support_card(cls, platform: str) -> bool: 134 | """检查平台是否支持卡片消息""" 135 | return platform.lower() in cls.CARD_SUPPORTED_PLATFORMS 136 | 137 | @classmethod 138 | def get_platform_message_format(cls, platform: str) -> str: 139 | """获取平台的消息格式""" 140 | return cls.PLATFORM_MESSAGE_FORMATS.get(platform.lower(), cls.PLATFORM_MESSAGE_FORMATS["default"]) 141 | 142 | @classmethod 143 | def validate_config(cls) -> bool: 144 | """验证配置完整性""" 145 | required_configs = [ 146 | 'NEXTCLOUD_URL', 'NEXTCLOUD_USERNAME', 'NEXTCLOUD_PASSWORD', 147 | 'NAPCAT_HOST', 'NAPCAT_PORT' 148 | ] 149 | return all(hasattr(cls, config) and getattr(cls, config) for config in required_configs) 150 | -------------------------------------------------------------------------------- /message_parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | 消息解析模块 3 | 用于解析用户输入的命令并提取参数 4 | """ 5 | 6 | import re 7 | from typing import Dict, List, Any 8 | from .config import Config 9 | 10 | 11 | class MessageParser: 12 | def __init__(self): 13 | self.config = Config() 14 | # 支持的关键词 15 | self.keywords = ['色图', '涩图', 'setu'] 16 | # R18关键词 17 | self.r18_keywords = ['r18', 'R18'] 18 | 19 | def parse_command(self, message: str) -> Dict[str, Any]: 20 | """ 21 | 解析消息命令 22 | 返回: { 23 | 'is_valid': bool, 24 | 'source': 'lolicon'|'cloud'|'auto', 25 | 'tags': list, 26 | 'r18': bool 27 | } 28 | """ 29 | message = message.strip() 30 | 31 | # 检查是否以支持的关键词开头 32 | if not self._is_valid_keyword(message): 33 | return { 34 | 'is_valid': False, 35 | 'source': self.config.DEFAULT_SOURCE, 36 | 'tags': [], 37 | 'r18': False 38 | } 39 | 40 | # 分割消息 41 | parts = message.split() 42 | 43 | # 找到匹配的关键词和提取真正的关键词部分 44 | keyword = "" 45 | keyword_end_index = 0 46 | 47 | for kw in self.keywords: 48 | if message.lower().startswith(kw.lower()): 49 | keyword = kw 50 | keyword_end_index = len(kw) 51 | break 52 | elif message.lower().startswith('r18' + kw.lower()): 53 | keyword = 'r18' + kw 54 | keyword_end_index = len('r18' + kw) 55 | break 56 | elif message.lower().startswith('r' + kw.lower()): 57 | keyword = 'r' + kw 58 | keyword_end_index = len('r' + kw) 59 | break 60 | 61 | # 提取关键词后的剩余部分作为参数 62 | remaining_message = message[keyword_end_index:].strip() 63 | args = remaining_message.split() if remaining_message else [] 64 | 65 | # 检测R18 66 | r18_flag = self._detect_r18(keyword) 67 | if not r18_flag: 68 | r18_flag = self.config.DEFAULT_R18_FLAG 69 | 70 | # 解析源和标签 71 | source, tags = self._parse_source_and_tags(args) 72 | 73 | return { 74 | 'is_valid': True, 75 | 'source': source, 76 | 'tags': tags, 77 | 'r18': r18_flag 78 | } 79 | 80 | def _is_valid_keyword(self, message: str) -> bool: 81 | """检查消息是否以有效关键词开头""" 82 | message_lower = message.lower() 83 | for keyword in self.keywords: 84 | # 检查是否以关键词开头(可能包含r18前缀) 85 | patterns = [ 86 | keyword.lower(), 87 | 'r18' + keyword.lower(), 88 | 'r' + keyword.lower() 89 | ] 90 | 91 | for pattern in patterns: 92 | if message_lower.startswith(pattern): 93 | # 确保关键词后面是空格或字符串结束,避免"setu图"这种情况 94 | if len(message) == len(pattern) or message[len(pattern)].isspace(): 95 | return True 96 | return False 97 | 98 | def _detect_r18(self, keyword: str) -> bool: 99 | """检测R18标志""" 100 | keyword_lower = keyword.lower() 101 | return any(r18_kw in keyword_lower for r18_kw in self.r18_keywords) 102 | 103 | def _parse_source_and_tags(self, args: List[str]) -> tuple: 104 | """ 105 | 解析源和标签 106 | 返回: (source, tags) 107 | """ 108 | source = self.config.DEFAULT_SOURCE 109 | tags = [] 110 | 111 | i = 0 112 | while i < len(args): 113 | arg = args[i] 114 | 115 | # 检查是否是源参数 116 | if arg == '-s' and i + 1 < len(args): 117 | next_arg = args[i + 1].lower() 118 | if next_arg in ['lolicon', 'cloud']: 119 | source = next_arg 120 | i += 2 # 跳过 -s 和源名称 121 | continue 122 | else: 123 | # -s 后面跟的不是有效源,当作标签处理 124 | tags.append(arg) 125 | i += 1 126 | else: 127 | # 普通标签 128 | tags.append(arg) 129 | i += 1 130 | 131 | return source, tags 132 | 133 | def get_help_text(self) -> str: 134 | """获取帮助文本""" 135 | return """ 136 | 命令格式:色图|涩图 [-s [lolicon|cloud]] [tag,...] 137 | 138 | 参数说明: 139 | - -s lolicon:从Lolicon API获取图片(默认) 140 | - -s cloud:从NextCloud随机获取图片 141 | - tag:图片标签(仅对lolicon有效) 142 | 143 | 示例: 144 | - 涩图:随机获取图片 145 | - 涩图 莉音:获取标签为"莉音"的图片 146 | - 涩图 -s cloud:从NextCloud获取图片 147 | - r18涩图:获取R18图片(需要权限) 148 | """.strip() 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GiveMeSetuPlugin 2 | 3 | 基于 LangBot 的多源图片获取插件,支持从 Lolicon API 和 NextCloud 获取图片,具备多平台适配能力。 4 | 5 | ## 特性 6 | 7 | - 🌍 **多平台支持**:支持 QQ(卡片模式)、Discord(直接发送)等平台 8 | - � **多图片源**:支持 Lolicon API 和 NextCloud 两种图片源 9 | - � **智能解析**:精确匹配关键词,支持参数解析 10 | - 🔄 **自动降级**:API 失败时自动切换到备用源 11 | - ⚙️ **配置化**:所有参数都可以在配置文件中修改 12 | - 🛡️ **错误处理**:完善的重试机制和友好的错误提示 13 | 14 | ## 版本信息 15 | 16 | - **当前版本**:v0.2 by ydzat 17 | - **原始版本**:v0.1 by Hanschase 18 | 19 | ### 主要更新 20 | - 添加 NextCloud 支持 21 | - 多平台适配(QQ 卡片模式 + Discord 直接发送) 22 | - 改进消息解析逻辑 23 | - 模块化重构 24 | - 统一配置管理 25 | 26 | ## 安装 27 | 28 | ### 前置要求 29 | 30 | 1. 已安装并配置 [LangBot](https://github.com/RockChinQ/LangBot) 主程序 31 | 2. 如果使用 QQ 平台,需要配置 NapCat 适配器 32 | 3. 如果使用 NextCloud 源,需要有 NextCloud 服务器访问权限 33 | 34 | ### 插件安装 35 | 36 | 使用管理员账号向机器人发送命令: 37 | 38 | ``` 39 | !plugin get https://github.com/Hanschase/GiveMeSetuPlugin 40 | ``` 41 | 42 | ### 依赖安装 43 | 44 | 插件会自动安装以下依赖: 45 | ``` 46 | httpx>=0.24.0 47 | aiofiles>=23.0.0 48 | aiohttp>=3.8.0 49 | webdav4>=0.9.0 50 | ``` 51 | 52 | ## 配置 53 | 54 | ### 基础配置 55 | 56 | 编辑 `plugins/GiveMeSetuPlugin/config.py` 文件: 57 | 58 | #### NextCloud 配置(如果使用) 59 | ```python 60 | # NextCloud 服务器地址 61 | NEXTCLOUD_URL = "https://your-nextcloud-domain.com" 62 | 63 | # NextCloud 用户名 64 | NEXTCLOUD_USERNAME = "your_username" 65 | 66 | # NextCloud 应用专用密码(推荐使用应用密码) 67 | NEXTCLOUD_PASSWORD = "your_app_password" 68 | 69 | # NextCloud 图片文件夹路径 70 | NEXTCLOUD_FOLDER = "/Pictures/Setu" 71 | ``` 72 | 73 | #### QQ 平台配置(如果使用) 74 | ```python 75 | # NapCat 服务器配置 76 | NAPCAT_HOST = "127.0.0.1" 77 | NAPCAT_PORT = 3000 78 | 79 | # BOT 信息(用于卡片消息显示) 80 | BOT_QQ = "your_bot_qq_number" 81 | FORWARD_USER_ID = "sender_qq_number" 82 | BOT_NICKNAME = "BOT" 83 | ``` 84 | 85 | ### 高级配置 86 | 87 | ```python 88 | # 默认图片源 (auto, lolicon, cloud) 89 | DEFAULT_SOURCE = "auto" 90 | 91 | # 自动模式下的源选择策略 92 | AUTO_SOURCE_PRIORITY = ["lolicon", "cloud"] 93 | 94 | # 错误重试次数 95 | MAX_RETRY_COUNT = 3 96 | 97 | # R18 内容开关 (0: 关闭, 1: 开启, 2: 随机) 98 | DEFAULT_R18_FLAG = 0 99 | ``` 100 | 101 | ## 使用方法 102 | 103 | ### 命令格式 104 | 105 | ``` 106 | 色图|涩图 [-s [lolicon|cloud]] [tag,...] 107 | ``` 108 | 109 | ### 参数说明 110 | 111 | - **关键词**:`色图` 或 `涩图`(必须在消息开头) 112 | - **`-s lolicon`**:从 Lolicon API 获取图片(默认) 113 | - **`-s cloud`**:从 NextCloud 随机获取图片 114 | - **`tag`**:图片标签(仅对 lolicon 有效,cloud 模式下忽略 tag) 115 | 116 | ### 使用示例 117 | 118 | ``` 119 | 涩图 # 从默认源随机获取图片 120 | 涩图 莉音 # 从 Lolicon API 获取标签为"莉音"的图片 121 | 涩图 -s lolicon 莉音 # 明确指定从 Lolicon API 获取 122 | 涩图 -s cloud # 从 NextCloud 随机获取图片 123 | 涩图 -s cloud 莉音 # 从 NextCloud 获取(忽略 tag) 124 | ``` 125 | 126 | ## 平台特性 127 | 128 | ### QQ 平台 129 | - **发送方式**:卡片模式 130 | - **显示内容**:图片作者、标题、标签等详细信息 131 | - **特殊要求**:需要配置 NapCat HTTP 服务器 132 | 133 | ![QQ 卡片效果](https://github.com/user-attachments/assets/6952b2e1-c022-4ce0-9eaa-d1a1604cfe9c) 134 | 135 | ### Discord 平台 136 | - **发送方式**:直接发送图片文件 137 | - **显示内容**:简化的文本信息 + 图片附件 138 | - **特殊要求**:Bot 需要有发送消息和附件的权限 139 | 140 | ### 其他平台 141 | - **发送方式**:纯文本模式 142 | - **自动适配**:插件会自动检测平台类型并选择合适的发送方式 143 | 144 | ## 图片源说明 145 | 146 | ### Lolicon API 147 | - **特点**:支持标签搜索,图片质量高 148 | - **优势**:标签丰富,搜索精确 149 | - **限制**:依赖网络连接,可能有频率限制 150 | 151 | ### NextCloud 152 | - **特点**:从个人云盘随机获取 153 | - **优势**:完全可控,无网络依赖 154 | - **限制**:需要自建 NextCloud 服务器,不支持标签搜索 155 | 156 | ### 自动模式 157 | - **工作原理**:按优先级顺序尝试不同源 158 | - **默认策略**:先尝试 Lolicon,失败后尝试 NextCloud 159 | - **可配置**:可在配置文件中自定义优先级 160 | 161 | ## 故障排除 162 | 163 | ### 常见问题 164 | 165 | #### 1. 插件无响应 166 | - 检查关键词是否正确(必须是 `色图` 或 `涩图` 开头) 167 | - 查看 LangBot 日志获取详细错误信息 168 | - 确认插件已正确加载 169 | 170 | #### 2. QQ 平台图片无法发送 171 | - 确认 NapCat HTTP 服务器已启动 172 | - 检查 NAPCAT_HOST 和 NAPCAT_PORT 配置 173 | - 验证端口是否被占用 174 | 175 | ![NapCat 配置示例](https://github.com/user-attachments/assets/0a5e68b4-ec3e-416c-96e9-da5f2a94b007) 176 | 177 | #### 3. NextCloud 连接失败 178 | - 检查 NEXTCLOUD_URL 是否正确 179 | - 确认用户名和密码正确 180 | - 建议使用应用专用密码而非账户密码 181 | - 检查文件夹路径是否存在 182 | 183 | #### 4. Discord 平台无响应 184 | - 确认 Bot 具有发送消息和附件的权限 185 | - 检查频道权限设置 186 | - 查看控制台日志获取详细错误 187 | 188 | ### 调试模式 189 | 190 | 启用详细日志: 191 | ```python 192 | ENABLE_DEBUG_LOG = True 193 | ``` 194 | 195 | ## 安全建议 196 | 197 | ### NextCloud 安全 198 | - 使用应用专用密码而非账户密码 199 | - 定期轮换密码和访问令牌 200 | - 限制应用权限范围 201 | 202 | ### 配置安全 203 | - 敏感配置信息建议使用环境变量 204 | - 定期检查配置文件权限 205 | - 避免在公共代码仓库中提交敏感信息 206 | 207 | ## 开发信息 208 | 209 | ### 项目结构 210 | ``` 211 | GiveMeSetuPlugin/ 212 | ├── main.py # 主插件文件 213 | ├── message_parser.py # 消息解析模块 214 | ├── get_image.py # Lolicon API 图片获取 215 | ├── nextcloud_client.py # NextCloud 客户端 216 | ├── platform_sender.py # 平台适配发送模块 217 | ├── forward_message.py # QQ 卡片消息发送 218 | ├── config.py # 配置管理 219 | └── requirements.txt # 依赖包 220 | ``` 221 | 222 | ### 贡献 223 | 欢迎提交 Issue 和 Pull Request 来改进插件功能。 224 | 225 | ## 更新日志 226 | 227 | ### v0.2 228 | - ✅ 添加 NextCloud 支持 229 | - ✅ 多平台适配(QQ 卡片模式 + Discord 直接发送) 230 | - ✅ 改进消息解析逻辑 231 | - ✅ 模块化重构 232 | - ✅ 统一配置管理 233 | - ✅ 完善错误处理和重试机制 234 | - ✅ 支持自动源切换 235 | 236 | ### v0.1 237 | - 🎉 初始版本发布 by Hanschase 238 | - ✅ 基础 Lolicon API 支持 239 | - ✅ QQ 平台卡片消息发送 240 | 241 | ## 许可证 242 | 243 | 请参考项目 LICENSE 文件。 244 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | GiveMeSetuPlugin v0.2 3 | 多源图片获取插件,支持Lolicon API和NextCloud,适配多平台发送 4 | 5 | 原始版本: v0.1 by Hanschase 6 | 当前版本: v0.2 by ydzat 7 | 8 | 主要更新: 9 | - 添加NextCloud支持 10 | - 多平台适配(QQ卡片模式 + Discord直接发送) 11 | - 改进消息解析逻辑 12 | - 模块化重构 13 | - 统一配置管理 14 | """ 15 | 16 | from mirai import MessageChain 17 | from pkg.plugin.context import register, handler, llm_func, BasePlugin, APIHost, EventContext 18 | from pkg.plugin.events import * # 导入事件类 19 | from pkg.platform.types import * 20 | from .get_image import get_image 21 | from .nextcloud_client import NextCloudClient 22 | from .message_parser import MessageParser 23 | from .platform_sender import PlatformSender 24 | from .config import Config 25 | import re 26 | import asyncio 27 | 28 | # 注册插件 29 | @register(name="GiveMeSetuPlugin", description="多源图片获取插件,支持Lolicon API和NextCloud,适配多平台发送", version="0.2", author="ydzat (基于Hanschase v0.1)") 30 | class MyPlugin(BasePlugin): 31 | 32 | # 插件加载时触发 33 | def __init__(self, host: APIHost): 34 | super().__init__(host) # 调用父类初始化 35 | self.plugin_config = Config() # 使用不同的名称避免冲突 36 | self.message_parser = MessageParser() 37 | self.nextcloud_client = NextCloudClient() 38 | self.platform_sender = PlatformSender() 39 | 40 | # 异步初始化 41 | async def initialize(self): 42 | # 验证配置 43 | if not self.plugin_config.validate_config(): 44 | self.ap.logger.warning("配置验证失败,部分功能可能不可用") 45 | 46 | # 测试NextCloud连接 47 | try: 48 | if await self.nextcloud_client.test_connection(): 49 | self.ap.logger.info("NextCloud连接测试成功") 50 | else: 51 | self.ap.logger.warning("NextCloud连接测试失败,cloud模式不可用") 52 | except Exception as e: 53 | self.ap.logger.warning(f"NextCloud连接测试异常: {e}") 54 | 55 | @handler(PersonMessageReceived) 56 | @handler(GroupMessageReceived) 57 | async def message_received(self, ctx: EventContext): 58 | msg = str(ctx.event.message_chain).strip() 59 | 60 | # 解析命令 61 | parse_result = self.message_parser.parse_command(msg) 62 | 63 | if not parse_result['is_valid']: 64 | return # 不是有效的命令,忽略 65 | 66 | ctx.prevent_default() 67 | 68 | source = parse_result['source'] 69 | tags = parse_result['tags'] 70 | r18_flag = parse_result['r18'] 71 | 72 | self.ap.logger.info(f"接收到命令 - 源:{source}, 标签:{tags}, R18:{r18_flag}") 73 | 74 | # 获取图片 75 | image_info = None 76 | retry_count = 0 77 | max_retries = self.plugin_config.MAX_RETRY_COUNT 78 | 79 | while retry_count < max_retries and image_info is None: 80 | try: 81 | if source == 'auto': 82 | # 自动模式:按优先级尝试 83 | image_info = await self._get_image_auto(tags, r18_flag) 84 | elif source == 'lolicon': 85 | # Lolicon API 86 | image_info = await get_image(tags, r18_flag) 87 | elif source == 'cloud': 88 | # NextCloud 89 | if tags: 90 | self.ap.logger.info("NextCloud模式忽略标签参数") 91 | image_info = await self.nextcloud_client.get_random_image() 92 | else: 93 | await self.platform_sender.send_error_message(ctx, f"未知的图片源: {source}") 94 | return 95 | 96 | break # 成功获取,跳出重试循环 97 | 98 | except Exception as e: 99 | retry_count += 1 100 | error_msg = str(e) 101 | 102 | self.ap.logger.error(f"获取图片失败 (尝试 {retry_count}/{max_retries}): {error_msg}") 103 | 104 | if error_msg == "404" and retry_count < max_retries: 105 | # 404错误继续重试 106 | continue 107 | elif retry_count >= max_retries: 108 | # 达到最大重试次数 109 | await self.platform_sender.send_error_message(ctx, f"获取图片失败,已重试{max_retries}次: {error_msg}") 110 | return 111 | else: 112 | # 其他错误直接返回 113 | await self.platform_sender.send_error_message(ctx, f"获取图片失败: {error_msg}") 114 | return 115 | 116 | # 发送图片 117 | if image_info: 118 | self.ap.logger.info(f"正在发送图片: {image_info.get('source', 'unknown')}") 119 | try: 120 | await self.platform_sender.send_image(ctx, image_info) 121 | self.ap.logger.info("图片发送成功") 122 | except Exception as e: 123 | self.ap.logger.error(f"图片发送失败: {e}") 124 | await self.platform_sender.send_error_message(ctx, f"图片发送失败: {str(e)}") 125 | else: 126 | await self.platform_sender.send_error_message(ctx, "获取图片失败,请稍后重试") 127 | 128 | async def _get_image_auto(self, tags, r18_flag): 129 | """自动模式:按配置的优先级尝试不同的源""" 130 | for source in self.plugin_config.AUTO_SOURCE_PRIORITY: 131 | try: 132 | if source == 'lolicon': 133 | return await get_image(tags, r18_flag) 134 | elif source == 'cloud': 135 | if tags: 136 | self.ap.logger.info("自动模式切换到NextCloud,忽略标签") 137 | return await self.nextcloud_client.get_random_image() 138 | except Exception as e: 139 | self.ap.logger.warning(f"自动模式中{source}源失败: {e}") 140 | continue 141 | 142 | # 所有源都失败 143 | raise Exception("所有图片源都不可用") 144 | 145 | # 插件卸载时触发 146 | def __del__(self): 147 | pass 148 | -------------------------------------------------------------------------------- /platform_sender.py: -------------------------------------------------------------------------------- 1 | """ 2 | 平台适配发送模块 3 | 根据不同平台采用不同的发送方式 4 | """ 5 | 6 | from typing import Dict, Any 7 | from pkg.platform.types.message import MessageChain, Image, Plain 8 | from pkg.plugin.context import EventContext 9 | from .config import Config 10 | from .forward_message import forward_message 11 | 12 | 13 | class PlatformSender: 14 | def __init__(self, forward_message_host: str = None, forward_message_port: int = None): 15 | """初始化平台发送器""" 16 | self.config = Config() 17 | self.forward_message_client = forward_message( 18 | host=forward_message_host or self.config.NAPCAT_HOST, 19 | port=forward_message_port or self.config.NAPCAT_PORT 20 | ) 21 | 22 | async def send_image(self, ctx: EventContext, image_info: Dict[str, Any]): 23 | """根据平台类型发送图片""" 24 | platform = self.detect_platform(ctx) 25 | message_format = self.config.get_platform_message_format(platform) 26 | 27 | if message_format == "card": 28 | await self._send_card_message(ctx, image_info) 29 | else: 30 | await self._send_text_message(ctx, image_info) 31 | 32 | def detect_platform(self, ctx: EventContext) -> str: 33 | """检测消息平台类型""" 34 | # 通过适配器类名检测平台类型 35 | adapter_class_name = ctx.event.query.adapter.__class__.__name__ 36 | 37 | # 将适配器类名映射到平台类型 38 | adapter_name_mapping = { 39 | "AiocqhttpAdapter": "onebot", 40 | "NakuruAdapter": "onebot", 41 | "OfficialAdapter": "qq", 42 | "QQOfficialAdapter": "qq", 43 | "DiscordAdapter": "discord", 44 | "TelegramAdapter": "telegram", 45 | "LarkAdapter": "lark", 46 | "WecomAdapter": "wecom", 47 | "SlackAdapter": "slack" 48 | } 49 | 50 | adapter_type = adapter_name_mapping.get(adapter_class_name, "unknown") 51 | return self.config.PLATFORM_TYPE_MAPPING.get(adapter_type, "default") 52 | 53 | async def _send_card_message(self, ctx: EventContext, image_info: Dict[str, Any]): 54 | """发送卡片消息(QQ平台)""" 55 | try: 56 | if image_info['source'] == 'lolicon': 57 | # Lolicon来源,使用现有的卡片格式 58 | lolicon_info = [ 59 | image_info['pid'], 60 | image_info['title'], 61 | image_info['author'] 62 | ] 63 | await self.forward_message_client.send(str(ctx.event.launcher_id), lolicon_info) 64 | else: 65 | # NextCloud来源,使用简化的卡片格式 66 | await self._send_nextcloud_card(ctx, image_info) 67 | except Exception as e: 68 | # 卡片发送失败,降级为文本发送 69 | await self._send_text_message(ctx, image_info) 70 | 71 | async def _send_nextcloud_card(self, ctx: EventContext, image_info: Dict[str, Any]): 72 | """发送NextCloud图片的卡片消息""" 73 | # 使用forward_message的send方法,但传入特殊格式的数据 74 | nextcloud_info = [ 75 | "NextCloud", # 作为pid使用 76 | image_info['filename'], # 作为title使用 77 | "Local Storage" # 作为author使用 78 | ] 79 | await self.forward_message_client.send(str(ctx.event.launcher_id), nextcloud_info) 80 | 81 | async def _send_text_message(self, ctx: EventContext, image_info: Dict[str, Any]): 82 | """发送文本消息(Discord等平台)""" 83 | try: 84 | if image_info['source'] == 'lolicon': 85 | # Lolicon来源的文本格式 86 | text_content = f"""🎨 图片信息 87 | 📝 标题: {image_info['title']} 88 | 👤 作者: {image_info['author']} 89 | 🆔 PID: {image_info['pid']} 90 | 🏷️ 标签: {', '.join(image_info['tags']) if image_info['tags'] else '无'} 91 | 📦 来源: Lolicon API""" 92 | else: 93 | # NextCloud来源的文本格式 94 | text_content = f"""🎨 图片信息 95 | 📁 文件名: {image_info['filename']} 96 | 📦 来源: NextCloud""" 97 | 98 | # 清理和处理图片路径 99 | image_path = image_info['image_path'] 100 | # 确保路径不包含空字节 101 | if '\x00' in image_path: 102 | image_path = image_path.replace('\x00', '') 103 | 104 | # 转换为绝对路径 105 | import os 106 | image_path = os.path.abspath(image_path) 107 | 108 | # 检查文件是否存在 109 | if not os.path.exists(image_path): 110 | raise Exception(f"图片文件不存在: {image_path}") 111 | 112 | # 发送文本和图片 113 | message_chain = MessageChain([ 114 | Plain(text=text_content), 115 | Image(path=image_path) 116 | ]) 117 | 118 | await ctx.send_message( 119 | ctx.event.launcher_type, 120 | str(ctx.event.launcher_id), 121 | message_chain 122 | ) 123 | 124 | except Exception as e: 125 | # 发送失败,只发送错误信息 126 | await ctx.send_message( 127 | ctx.event.launcher_type, 128 | str(ctx.event.launcher_id), 129 | MessageChain([Plain(text=f"图片发送失败: {str(e)}")]) 130 | ) 131 | 132 | async def send_error_message(self, ctx: EventContext, error_msg: str): 133 | """发送错误消息""" 134 | try: 135 | await ctx.send_message( 136 | ctx.event.launcher_type, 137 | str(ctx.event.launcher_id), 138 | MessageChain([Plain(text=f"❌ {error_msg}")]) 139 | ) 140 | except Exception: 141 | # 如果连错误消息都发送失败,就不做任何处理 142 | pass 143 | 144 | async def send_help_message(self, ctx: EventContext, help_text: str): 145 | """发送帮助消息""" 146 | try: 147 | await ctx.send_message( 148 | ctx.event.launcher_type, 149 | str(ctx.event.launcher_id), 150 | MessageChain([Plain(text=help_text)]) 151 | ) 152 | except Exception: 153 | pass 154 | -------------------------------------------------------------------------------- /nextcloud_client.py: -------------------------------------------------------------------------------- 1 | """ 2 | NextCloud客户端模块 3 | 用于连接NextCloud服务器并获取图片 4 | """ 5 | 6 | import aiohttp 7 | import aiofiles 8 | import random 9 | import os 10 | from typing import List, Optional, Dict, Any 11 | from .config import Config 12 | import base64 13 | import xml.etree.ElementTree as ET 14 | 15 | 16 | class NextCloudClient: 17 | def __init__(self, base_url: str = None, username: str = None, 18 | password: str = None, folder_path: str = None): 19 | """初始化NextCloud客户端,优先使用传入参数,否则使用配置文件""" 20 | self.config = Config() 21 | self.base_url = base_url or self.config.NEXTCLOUD_URL 22 | self.username = username or self.config.NEXTCLOUD_USERNAME 23 | self.password = password or self.config.NEXTCLOUD_PASSWORD 24 | self.folder_path = folder_path or self.config.NEXTCLOUD_FOLDER 25 | self.timeout = self.config.NEXTCLOUD_TIMEOUT 26 | self.supported_formats = self.config.SUPPORTED_FORMATS 27 | 28 | # 缓存 29 | self._image_list_cache = None 30 | self._cache_timestamp = 0 31 | 32 | # WebDAV URL 33 | self.webdav_url = self._get_webdav_url() 34 | 35 | # 认证头 36 | self.auth_header = self._get_auth_header() 37 | 38 | def _get_webdav_url(self) -> str: 39 | """获取WebDAV URL""" 40 | webdav_path = self.config.NEXTCLOUD_WEBDAV_PATH.format(username=self.username) 41 | return f"{self.base_url.rstrip('/')}{webdav_path}{self.folder_path}" 42 | 43 | def _get_auth_header(self) -> str: 44 | """获取认证头""" 45 | credentials = f"{self.username}:{self.password}" 46 | encoded_credentials = base64.b64encode(credentials.encode()).decode() 47 | return f"Basic {encoded_credentials}" 48 | 49 | async def get_random_image(self) -> Dict[str, Any]: 50 | """ 51 | 从指定文件夹随机获取图片 52 | 返回: { 53 | 'source': 'cloud', 54 | 'filename': str, 55 | 'image_path': str 56 | } 57 | """ 58 | try: 59 | # 获取图片列表 60 | image_list = await self.list_images() 61 | 62 | if not image_list: 63 | raise Exception("NextCloud文件夹中没有找到图片") 64 | 65 | # 随机选择一张图片 66 | selected_image = random.choice(image_list) 67 | 68 | # 下载图片 69 | image_path = await self._download_image(selected_image['href']) 70 | 71 | return { 72 | 'source': 'cloud', 73 | 'filename': selected_image['name'], 74 | 'image_path': image_path 75 | } 76 | 77 | except Exception as e: 78 | raise Exception(f"NextCloud获取图片失败: {str(e)}") 79 | 80 | async def list_images(self) -> List[Dict[str, str]]: 81 | """ 82 | 列出文件夹中的所有图片 83 | 返回图片文件信息列表 84 | """ 85 | import time 86 | current_time = time.time() 87 | 88 | # 检查缓存是否有效 89 | if (self._image_list_cache and 90 | current_time - self._cache_timestamp < self.config.NEXTCLOUD_CACHE_DURATION): 91 | return self._image_list_cache 92 | 93 | try: 94 | # WebDAV PROPFIND请求 95 | headers = { 96 | 'Authorization': self.auth_header, 97 | 'Content-Type': 'application/xml', 98 | 'Depth': '1' 99 | } 100 | 101 | # PROPFIND请求体 102 | propfind_body = ''' 103 | 104 | 105 | 106 | 107 | 108 | 109 | ''' 110 | 111 | timeout = aiohttp.ClientTimeout(total=self.timeout) 112 | async with aiohttp.ClientSession(timeout=timeout) as session: 113 | async with session.request( 114 | 'PROPFIND', 115 | self.webdav_url, 116 | headers=headers, 117 | data=propfind_body 118 | ) as response: 119 | 120 | if response.status == 404: 121 | raise Exception(f"NextCloud文件夹不存在: {self.folder_path}") 122 | elif response.status == 401: 123 | raise Exception("NextCloud认证失败,请检查用户名和密码") 124 | elif response.status != 207: # WebDAV Multi-Status 125 | raise Exception(f"NextCloud请求失败: {response.status}") 126 | 127 | # 解析响应 128 | xml_content = await response.text() 129 | image_list = self._parse_webdav_response(xml_content) 130 | 131 | # 更新缓存 132 | self._image_list_cache = image_list 133 | self._cache_timestamp = current_time 134 | 135 | return image_list 136 | 137 | except aiohttp.ClientError as e: 138 | raise Exception(f"NextCloud连接失败: {str(e)}") 139 | 140 | def _parse_webdav_response(self, xml_content: str) -> List[Dict[str, str]]: 141 | """解析WebDAV响应,提取图片文件信息""" 142 | image_list = [] 143 | 144 | try: 145 | # 解析XML 146 | root = ET.fromstring(xml_content) 147 | 148 | # 查找所有response元素 149 | for response in root.findall('.//{DAV:}response'): 150 | # 获取文件路径 151 | href_elem = response.find('.//{DAV:}href') 152 | if href_elem is None: 153 | continue 154 | 155 | href = href_elem.text 156 | 157 | # 获取文件名 158 | displayname_elem = response.find('.//{DAV:}displayname') 159 | if displayname_elem is None: 160 | # 从href中提取文件名 161 | filename = href.split('/')[-1] 162 | else: 163 | filename = displayname_elem.text 164 | 165 | # 检查是否是文件夹 166 | resourcetype_elem = response.find('.//{DAV:}resourcetype') 167 | if resourcetype_elem is not None and resourcetype_elem.find('.//{DAV:}collection') is not None: 168 | continue # 跳过文件夹 169 | 170 | # 检查是否是图片文件 171 | if self._is_image_file(filename): 172 | image_list.append({ 173 | 'name': filename, 174 | 'href': href 175 | }) 176 | 177 | except ET.ParseError as e: 178 | raise Exception(f"解析NextCloud响应失败: {str(e)}") 179 | 180 | return image_list 181 | 182 | async def _download_image(self, file_href: str) -> str: 183 | """下载图片到本地临时文件""" 184 | try: 185 | # 构建完整的下载URL 186 | download_url = f"{self.base_url.rstrip('/')}{file_href}" 187 | 188 | headers = { 189 | 'Authorization': self.auth_header 190 | } 191 | 192 | timeout = aiohttp.ClientTimeout(total=self.config.IMAGE_DOWNLOAD_TIMEOUT) 193 | async with aiohttp.ClientSession(timeout=timeout) as session: 194 | async with session.get(download_url, headers=headers) as response: 195 | if response.status != 200: 196 | raise Exception(f"下载图片失败: {response.status}") 197 | 198 | # 检查文件大小 199 | content_length = response.headers.get('Content-Length') 200 | if content_length and int(content_length) > self.config.MAX_IMAGE_SIZE * 1024 * 1024: 201 | raise Exception(f"图片文件过大: {content_length} bytes") 202 | 203 | # 保存到临时文件 204 | temp_path = self.config.TEMP_IMAGE_NAME 205 | async with aiofiles.open(temp_path, 'wb') as f: 206 | async for chunk in response.content.iter_chunked(8192): 207 | await f.write(chunk) 208 | 209 | return temp_path 210 | 211 | except aiohttp.ClientError as e: 212 | raise Exception(f"下载图片失败: {str(e)}") 213 | 214 | def _is_image_file(self, filename: str) -> bool: 215 | """检查文件是否为支持的图片格式""" 216 | if not filename: 217 | return False 218 | return any(filename.lower().endswith(fmt) for fmt in self.supported_formats) 219 | 220 | async def test_connection(self) -> bool: 221 | """测试NextCloud连接""" 222 | try: 223 | await self.list_images() 224 | return True 225 | except Exception: 226 | return False 227 | --------------------------------------------------------------------------------