完善本地图片配图功能并优化图片上传机制
This commit is contained in:
@@ -50,7 +50,8 @@ class MaiZoneRefactoredPlugin(BasePlugin):
|
|||||||
"enable_ai_image": ConfigField(type=bool, default=False, description="是否启用AI生成配图"),
|
"enable_ai_image": ConfigField(type=bool, default=False, description="是否启用AI生成配图"),
|
||||||
"enable_reply": ConfigField(type=bool, default=True, description="完成后是否回复"),
|
"enable_reply": ConfigField(type=bool, default=True, description="完成后是否回复"),
|
||||||
"ai_image_number": ConfigField(type=int, default=1, description="AI生成图片数量"),
|
"ai_image_number": ConfigField(type=int, default=1, description="AI生成图片数量"),
|
||||||
"image_directory": ConfigField(type=str, default="./data/plugins/maizone_refactored/images", description="图片存储目录")
|
"image_number": ConfigField(type=int, default=1, description="本地配图数量(1-9张)"),
|
||||||
|
"image_directory": ConfigField(type=str, default="./data/images", description="图片存储目录")
|
||||||
},
|
},
|
||||||
"read": {
|
"read": {
|
||||||
"permission": ConfigField(type=list, default=[], description="阅读权限QQ号列表"),
|
"permission": ConfigField(type=list, default=[], description="阅读权限QQ号列表"),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
import base64
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Optional, Dict, Any, List, Tuple
|
from typing import Callable, Optional, Dict, Any, List, Tuple
|
||||||
|
|
||||||
@@ -217,17 +218,52 @@ class QZoneService:
|
|||||||
await api_client["like"](target_qq, fid)
|
await api_client["like"](target_qq, fid)
|
||||||
|
|
||||||
def _load_local_images(self, image_dir: str) -> List[bytes]:
|
def _load_local_images(self, image_dir: str) -> List[bytes]:
|
||||||
|
"""随机加载本地图片(不删除文件)"""
|
||||||
images = []
|
images = []
|
||||||
if not os.path.exists(image_dir):
|
if not os.path.exists(image_dir):
|
||||||
|
logger.warning(f"图片目录不存在: {image_dir}")
|
||||||
return images
|
return images
|
||||||
|
|
||||||
try:
|
try:
|
||||||
files = sorted([f for f in os.listdir(image_dir) if os.path.isfile(os.path.join(image_dir, f))])
|
# 获取所有图片文件
|
||||||
for filename in files:
|
all_files = [f for f in os.listdir(image_dir)
|
||||||
|
if os.path.isfile(os.path.join(image_dir, f))
|
||||||
|
and f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp'))]
|
||||||
|
|
||||||
|
if not all_files:
|
||||||
|
logger.warning(f"图片目录中没有找到图片文件: {image_dir}")
|
||||||
|
return images
|
||||||
|
|
||||||
|
# 检查是否启用配图
|
||||||
|
enable_image = bool(self.get_config("send.enable_image", False))
|
||||||
|
if not enable_image:
|
||||||
|
logger.info("说说配图功能已关闭")
|
||||||
|
return images
|
||||||
|
|
||||||
|
# 根据配置选择图片数量
|
||||||
|
config_image_number = self.get_config("send.image_number", 1)
|
||||||
|
try:
|
||||||
|
config_image_number = int(config_image_number)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
config_image_number = 1
|
||||||
|
logger.warning(f"配置项 image_number 值无效,使用默认值 1")
|
||||||
|
|
||||||
|
max_images = min(min(config_image_number, 9), len(all_files)) # 最多9张,最少1张
|
||||||
|
selected_count = max(1, max_images) # 确保至少选择1张
|
||||||
|
selected_files = random.sample(all_files, selected_count)
|
||||||
|
|
||||||
|
logger.info(f"从 {len(all_files)} 张图片中随机选择了 {selected_count} 张配图")
|
||||||
|
|
||||||
|
for filename in selected_files:
|
||||||
full_path = os.path.join(image_dir, filename)
|
full_path = os.path.join(image_dir, filename)
|
||||||
with open(full_path, "rb") as f:
|
try:
|
||||||
images.append(f.read())
|
with open(full_path, "rb") as f:
|
||||||
os.remove(full_path)
|
image_data = f.read()
|
||||||
|
images.append(image_data)
|
||||||
|
logger.info(f"加载图片: {filename} ({len(image_data)} bytes)")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"加载图片 {filename} 失败: {e}")
|
||||||
|
|
||||||
return images
|
return images
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"加载本地图片失败: {e}")
|
logger.error(f"加载本地图片失败: {e}")
|
||||||
@@ -353,31 +389,175 @@ class QZoneService:
|
|||||||
"format": "json",
|
"format": "json",
|
||||||
"qzreferrer": f"https://user.qzone.qq.com/{uin}",
|
"qzreferrer": f"https://user.qzone.qq.com/{uin}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 处理图片上传
|
||||||
if images:
|
if images:
|
||||||
pic_bos, richvals = [], [] # noqa: F841
|
logger.info(f"开始上传 {len(images)} 张图片...")
|
||||||
# The original logic for uploading images is complex and involves multiple steps.
|
pic_bos = []
|
||||||
# This simplified version captures the essence. A full implementation would require
|
richvals = []
|
||||||
# a separate, robust image upload function.
|
|
||||||
for _img_bytes in images:
|
for i, img_bytes in enumerate(images):
|
||||||
# This is a placeholder for the actual image upload logic which is quite complex.
|
try:
|
||||||
# In a real scenario, you would call a dedicated `_upload_image` method here.
|
# 上传图片到QQ空间
|
||||||
# For now, we assume the upload is successful and we get back dummy data.
|
upload_result = await _upload_image(img_bytes, i)
|
||||||
pass # Simplified for this example
|
if upload_result:
|
||||||
|
pic_bos.append(upload_result["pic_bo"])
|
||||||
# Dummy data for illustration
|
richvals.append(upload_result["richval"])
|
||||||
if images:
|
logger.info(f"图片 {i+1} 上传成功")
|
||||||
post_data["pic_bo"] = "dummy_pic_bo"
|
else:
|
||||||
post_data["richtype"] = "1"
|
logger.error(f"图片 {i+1} 上传失败")
|
||||||
post_data["richval"] = "dummy_rich_val"
|
except Exception as e:
|
||||||
|
logger.error(f"上传图片 {i+1} 时发生异常: {e}")
|
||||||
|
|
||||||
|
if pic_bos and richvals:
|
||||||
|
# 完全按照原版格式设置图片参数
|
||||||
|
post_data['pic_bo'] = ','.join(pic_bos)
|
||||||
|
post_data['richtype'] = '1'
|
||||||
|
post_data['richval'] = '\t'.join(richvals) # 原版使用制表符分隔
|
||||||
|
|
||||||
|
logger.info(f"准备发布带图说说: {len(pic_bos)} 张图片")
|
||||||
|
logger.info(f"pic_bo参数: {post_data['pic_bo']}")
|
||||||
|
logger.info(f"richval参数长度: {len(post_data['richval'])} 字符")
|
||||||
|
else:
|
||||||
|
logger.warning("所有图片上传失败,将发布纯文本说说")
|
||||||
|
|
||||||
res_text = await _request("POST", self.EMOTION_PUBLISH_URL, params={"g_tk": gtk}, data=post_data)
|
res_text = await _request("POST", self.EMOTION_PUBLISH_URL, params={"g_tk": gtk}, data=post_data)
|
||||||
result = json.loads(res_text)
|
result = json.loads(res_text)
|
||||||
tid = result.get("tid", "")
|
tid = result.get("tid", "")
|
||||||
|
|
||||||
|
if tid:
|
||||||
|
if images and pic_bos:
|
||||||
|
logger.info(f"成功发布带图说说,tid: {tid},包含 {len(pic_bos)} 张图片")
|
||||||
|
else:
|
||||||
|
logger.info(f"成功发布文本说说,tid: {tid}")
|
||||||
|
else:
|
||||||
|
logger.error(f"发布说说失败,API返回: {result}")
|
||||||
|
|
||||||
return bool(tid), tid
|
return bool(tid), tid
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"发布说说异常: {e}", exc_info=True)
|
logger.error(f"发布说说异常: {e}", exc_info=True)
|
||||||
return False, ""
|
return False, ""
|
||||||
|
|
||||||
|
def _image_to_base64(image_bytes: bytes) -> str:
|
||||||
|
"""将图片字节转换为base64字符串(仿照原版实现)"""
|
||||||
|
pic_base64 = base64.b64encode(image_bytes)
|
||||||
|
return str(pic_base64)[2:-1] # 去掉 b'...' 的前缀和后缀
|
||||||
|
|
||||||
|
def _get_picbo_and_richval(upload_result: dict) -> tuple:
|
||||||
|
"""从上传结果中提取图片的picbo和richval值(仿照原版实现)"""
|
||||||
|
json_data = upload_result
|
||||||
|
|
||||||
|
if 'ret' not in json_data:
|
||||||
|
raise Exception("获取图片picbo和richval失败")
|
||||||
|
|
||||||
|
if json_data['ret'] != 0:
|
||||||
|
raise Exception("上传图片失败")
|
||||||
|
|
||||||
|
# 从URL中提取bo参数
|
||||||
|
picbo_spt = json_data['data']['url'].split('&bo=')
|
||||||
|
if len(picbo_spt) < 2:
|
||||||
|
raise Exception("上传图片失败")
|
||||||
|
picbo = picbo_spt[1]
|
||||||
|
|
||||||
|
# 构造richval - 完全按照原版格式
|
||||||
|
richval = ",{},{},{},{},{},{},,{},{}".format(
|
||||||
|
json_data['data']['albumid'],
|
||||||
|
json_data['data']['lloc'],
|
||||||
|
json_data['data']['sloc'],
|
||||||
|
json_data['data']['type'],
|
||||||
|
json_data['data']['height'],
|
||||||
|
json_data['data']['width'],
|
||||||
|
json_data['data']['height'],
|
||||||
|
json_data['data']['width']
|
||||||
|
)
|
||||||
|
|
||||||
|
return picbo, richval
|
||||||
|
|
||||||
|
async def _upload_image(image_bytes: bytes, index: int) -> Optional[Dict[str, str]]:
|
||||||
|
"""上传图片到QQ空间(完全按照原版实现)"""
|
||||||
|
try:
|
||||||
|
upload_url = "https://up.qzone.qq.com/cgi-bin/upload/cgi_upload_image"
|
||||||
|
|
||||||
|
# 完全按照原版构建请求数据
|
||||||
|
post_data = {
|
||||||
|
"filename": "filename",
|
||||||
|
"zzpanelkey": "",
|
||||||
|
"uploadtype": "1",
|
||||||
|
"albumtype": "7",
|
||||||
|
"exttype": "0",
|
||||||
|
"skey": cookies.get("skey", ""),
|
||||||
|
"zzpaneluin": uin,
|
||||||
|
"p_uin": uin,
|
||||||
|
"uin": uin,
|
||||||
|
"p_skey": cookies.get('p_skey', ''),
|
||||||
|
"output_type": "json",
|
||||||
|
"qzonetoken": "",
|
||||||
|
"refer": "shuoshuo",
|
||||||
|
"charset": "utf-8",
|
||||||
|
"output_charset": "utf-8",
|
||||||
|
"upload_hd": "1",
|
||||||
|
"hd_width": "2048",
|
||||||
|
"hd_height": "10000",
|
||||||
|
"hd_quality": "96",
|
||||||
|
"backUrls": "http://upbak.photo.qzone.qq.com/cgi-bin/upload/cgi_upload_image,"
|
||||||
|
"http://119.147.64.75/cgi-bin/upload/cgi_upload_image",
|
||||||
|
"url": f"https://up.qzone.qq.com/cgi-bin/upload/cgi_upload_image?g_tk={gtk}",
|
||||||
|
"base64": "1",
|
||||||
|
"picfile": _image_to_base64(image_bytes),
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'referer': f'https://user.qzone.qq.com/{uin}',
|
||||||
|
'origin': 'https://user.qzone.qq.com'
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"开始上传图片 {index+1}...")
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession(cookies=cookies) as session:
|
||||||
|
timeout = aiohttp.ClientTimeout(total=60)
|
||||||
|
async with session.post(
|
||||||
|
upload_url,
|
||||||
|
data=post_data,
|
||||||
|
headers=headers,
|
||||||
|
timeout=timeout
|
||||||
|
) as response:
|
||||||
|
if response.status == 200:
|
||||||
|
resp_text = await response.text()
|
||||||
|
logger.info(f"图片上传响应状态码: {response.status}")
|
||||||
|
logger.info(f"图片上传响应内容前500字符: {resp_text[:500]}")
|
||||||
|
|
||||||
|
# 按照原版方式解析响应
|
||||||
|
start_idx = resp_text.find('{')
|
||||||
|
end_idx = resp_text.rfind('}') + 1
|
||||||
|
if start_idx != -1 and end_idx != -1:
|
||||||
|
json_str = resp_text[start_idx:end_idx]
|
||||||
|
upload_result = eval(json_str) # 与原版保持一致使用eval
|
||||||
|
|
||||||
|
logger.info(f"图片上传解析结果: {upload_result}")
|
||||||
|
|
||||||
|
if upload_result.get('ret') == 0:
|
||||||
|
# 使用原版的参数提取逻辑
|
||||||
|
picbo, richval = _get_picbo_and_richval(upload_result)
|
||||||
|
logger.info(f"图片 {index+1} 上传成功: picbo={picbo}")
|
||||||
|
return {
|
||||||
|
"pic_bo": picbo,
|
||||||
|
"richval": richval
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
logger.error(f"图片 {index+1} 上传失败: {upload_result}")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
logger.error("无法解析上传响应")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
error_text = await response.text()
|
||||||
|
logger.error(f"图片上传HTTP请求失败,状态码: {response.status}, 响应: {error_text[:200]}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"上传图片 {index+1} 异常: {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
async def _list_feeds(t_qq: str, num: int) -> List[Dict]:
|
async def _list_feeds(t_qq: str, num: int) -> List[Dict]:
|
||||||
"""获取指定用户说说列表"""
|
"""获取指定用户说说列表"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user