mirror of
https://github.com/ClovertaTheTrilobita/SanYeCao-Nonebot.git
synced 2026-04-01 22:04:51 +00:00
fix(ai_chat): 移除测试代码
feat(jm): 新增邮件发送功能及配置管理 feat(config): 新增jm配置文件 feat(email): 新增邮件发送模块 chore(.gitignore): 添加jm图片目录忽略 chore(requirements): 添加aiosmtplib依赖 refactor(jm_comic): 重构下载逻辑改为邮件发送 refactor(path_config): 更新jm配置路径 refactor(jm_download): 适配新下载逻辑并添加邮箱验证
This commit is contained in:
parent
c49139607e
commit
e70b262402
10 changed files with 347 additions and 41 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -27,3 +27,4 @@ bili.cookie
|
|||
src/clover_lightnovel/wenku8.cookie
|
||||
src/clover_lightnovel/output1.html
|
||||
*.pyc
|
||||
/src/resources/image/jm/*
|
||||
|
|
@ -29,6 +29,7 @@ paramiko
|
|||
commonX
|
||||
jmcomic
|
||||
natsort
|
||||
aiosmtplib
|
||||
|
||||
requests
|
||||
pillow
|
||||
|
|
|
|||
191
src/clover_email/send_email.py
Normal file
191
src/clover_email/send_email.py
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import os
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.text import MIMEText
|
||||
import aiosmtplib
|
||||
from src.configs.api_config import google_smtp_server,google_email,google_password
|
||||
from src.configs.api_config import qq_smtp_server,qq_email,qq_password
|
||||
|
||||
# 发送内容
|
||||
html = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>健康小贴士</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
||||
padding: 30px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.container:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #ff6b6b;
|
||||
font-size: 2.2rem;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
h1:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, #ff6b6b, #ff8e8e);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.1rem;
|
||||
margin: 25px 0;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
font-size: 1.5rem;
|
||||
vertical-align: middle;
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.tip-box {
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #74b9ff;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
text-align: left;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
font-size: 0.9rem;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>可别<span class="emoji">🦌</span>死了</h1>
|
||||
<p>作案前记得洗手,适度使用手动挡,别让发动机过热抛锚。</p>
|
||||
|
||||
<div class="tip-box">
|
||||
<strong>健康小贴士:</strong> 适度有益健康,过度可能影响日常生活。保持良好卫生习惯,合理安排时间哦~
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
async def send_email_by_google(receiver_email: str, file_path: str):
|
||||
"""发送单个文件附件邮件(Google版)"""
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = google_email
|
||||
msg["To"] = receiver_email
|
||||
msg["Subject"] = "您的快递已送达"
|
||||
msg.attach(MIMEText(html, "html", "utf-8"))
|
||||
|
||||
try:
|
||||
# 验证文件存在性
|
||||
if not os.path.isfile(file_path):
|
||||
print(f"文件不存在:{file_path}")
|
||||
return False
|
||||
|
||||
# 添加单个文件附件
|
||||
file_name = os.path.basename(file_path)
|
||||
with open(file_path, "rb") as f:
|
||||
attachment = MIMEApplication(f.read())
|
||||
attachment.add_header(
|
||||
"Content-Disposition",
|
||||
"attachment",
|
||||
filename=file_name
|
||||
)
|
||||
msg.attach(attachment)
|
||||
|
||||
async with aiosmtplib.SMTP(
|
||||
hostname=google_smtp_server,
|
||||
port=465,
|
||||
timeout=60,
|
||||
use_tls=True
|
||||
) as server:
|
||||
await server.login(google_email, google_password)
|
||||
await server.send_message(msg)
|
||||
print("文件邮件发送成功!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"邮件发送失败: {str(e)}")
|
||||
return False
|
||||
|
||||
async def send_email_by_qq(receiver_email: str, file_path: str):
|
||||
"""发送单个文件附件邮件(QQ版)"""
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = qq_email
|
||||
msg["To"] = receiver_email
|
||||
msg["Subject"] = "您的快递已送达"
|
||||
msg.attach(MIMEText(html, "html", "utf-8"))
|
||||
|
||||
try:
|
||||
if not os.path.exists(file_path):
|
||||
print(f"文件不存在:{file_path}")
|
||||
return False
|
||||
|
||||
# 添加附件
|
||||
file_name = os.path.basename(file_path)
|
||||
with open(file_path, "rb") as f:
|
||||
attachment = MIMEApplication(f.read())
|
||||
attachment.add_header(
|
||||
"Content-Disposition",
|
||||
"attachment",
|
||||
filename=file_name
|
||||
)
|
||||
msg.attach(attachment)
|
||||
|
||||
async with aiosmtplib.SMTP(
|
||||
hostname=qq_smtp_server,
|
||||
port=465,
|
||||
use_tls=True,
|
||||
timeout=30
|
||||
) as server:
|
||||
await server.login(qq_email, qq_password)
|
||||
await server.send_message(msg)
|
||||
print("QQ文件邮件发送成功!")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"QQ邮件发送失败: {str(e)}")
|
||||
return False
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import os
|
||||
import asyncio
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from natsort import natsorted
|
||||
|
||||
|
|
@ -16,7 +18,6 @@ async def webp_to_pdf(input_folder, output_pdf):
|
|||
|
||||
if not webp_files:
|
||||
print("未找到WebP图片")
|
||||
# raise ValueError("未找到WebP图片")
|
||||
|
||||
images = []
|
||||
for webp_file in webp_files:
|
||||
|
|
@ -34,20 +35,53 @@ async def webp_to_pdf(input_folder, output_pdf):
|
|||
|
||||
if not images:
|
||||
print("无有效图片")
|
||||
# raise ValueError("无有效图片")
|
||||
|
||||
images[0].save(
|
||||
output_pdf,
|
||||
save_all=True,
|
||||
append_images=images[1:],
|
||||
optimize=True,
|
||||
quality=85
|
||||
quality=80
|
||||
)
|
||||
return output_pdf
|
||||
|
||||
async def batch_convert_subfolders(base_dir,output_dir):
|
||||
"""
|
||||
批量转换指定目录下所有子文件夹中的WebP图片为独立PDF
|
||||
:param base_dir: 要扫描的根目录,默认当前目录
|
||||
:param output_dir: PDF输出目录
|
||||
"""
|
||||
subfolders = [
|
||||
f for f in os.listdir(base_dir)
|
||||
if os.path.isdir(os.path.join(base_dir, f))
|
||||
and not f.startswith('.')
|
||||
and f != os.path.basename(output_dir)
|
||||
]
|
||||
|
||||
if not subfolders:
|
||||
print("未找到有效子文件夹")
|
||||
return
|
||||
|
||||
tasks = []
|
||||
for folder in subfolders:
|
||||
input_path = os.path.join(base_dir, folder)
|
||||
output_pdf = os.path.join(output_dir, f"{folder}.pdf")
|
||||
tasks.append(
|
||||
webp_to_pdf(input_path, output_pdf)
|
||||
)
|
||||
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
success = 0
|
||||
for folder, result in zip(subfolders, results):
|
||||
if isinstance(result, Exception):
|
||||
print(f"转换失败 [{folder}]: {str(result)}")
|
||||
else:
|
||||
print(f"成功转换: {folder} -> {result}")
|
||||
success += 1
|
||||
|
||||
async def zip_pdf(pdf_path, zip_path):
|
||||
"""
|
||||
压缩单文件
|
||||
"""
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
arcname = os.path.basename(pdf_path)
|
||||
|
|
@ -57,9 +91,46 @@ async def zip_pdf(pdf_path, zip_path):
|
|||
|
||||
|
||||
async def merge_files(jpg_path, zip_path, output_path):
|
||||
"""
|
||||
将PDF伪装成图片
|
||||
"""
|
||||
try:
|
||||
with open(jpg_path, 'rb') as jpg_file, open(zip_path, 'rb') as zip_file, open(output_path,'wb') as output_file:
|
||||
output_file.write(jpg_file.read())
|
||||
output_file.write(zip_file.read())
|
||||
except Exception as e:
|
||||
print(f"合并文件时出错: {e}")
|
||||
|
||||
async def folder_zip(folder_path, jm_zip_path):
|
||||
"""
|
||||
异步压缩整个文件夹到指定路径
|
||||
:param folder_path: 需要压缩的文件夹路径
|
||||
:param jm_zip_path: 输出的zip文件路径
|
||||
:return: 压缩成功返回True,否则返回False
|
||||
"""
|
||||
try:
|
||||
Path(jm_zip_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def sync_zip():
|
||||
with zipfile.ZipFile(jm_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
|
||||
for root, dirs, files in os.walk(folder_path):
|
||||
|
||||
relative_path = Path(root).relative_to(folder_path)
|
||||
|
||||
for dir_name in dirs:
|
||||
abs_dir = os.path.join(root, dir_name)
|
||||
zipf.write(abs_dir, arcname=str(relative_path / dir_name))
|
||||
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
arcname = str(relative_path / file)
|
||||
zipf.write(file_path, arcname=arcname)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(None, sync_zip)
|
||||
print(f"成功压缩文件夹到: {jm_zip_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"压缩文件夹失败: {str(e)}")
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -1,28 +1,62 @@
|
|||
import yaml
|
||||
import jmcomic
|
||||
from src.clover_jm.disguise_pdf import *
|
||||
from src.configs.path_config import jm_path
|
||||
from src.clover_image.delete_file import delete_file_batch,delete_folder
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from src.configs.path_config import jm_path,jm_config_path
|
||||
from src.clover_image.delete_file import delete_folder,delete_file
|
||||
from src.clover_email.send_email import send_email_by_google,send_email_by_qq
|
||||
|
||||
async def download_jm(album_id: str| None):
|
||||
# 创建线程池
|
||||
jm_executor = ThreadPoolExecutor(max_workers=5)
|
||||
jm_executor.submit(lambda: None).result()
|
||||
|
||||
album_detail,downloader = jmcomic.download_album(album_id)
|
||||
|
||||
original_path = os.getcwd()+f"/{album_detail.title}"
|
||||
# 将图片转换为PDF
|
||||
await webp_to_pdf(original_path,jm_path +f"{album_id}.pdf")
|
||||
pdf_file = jm_path + f"{album_id}.pdf"
|
||||
jpg_file = jm_path + 'temp.jpg'
|
||||
zip_file = jm_path + "resume.zip"
|
||||
output_file = jm_path +"merged.jpg"
|
||||
|
||||
if os.path.exists(pdf_file) and os.path.exists(jpg_file):
|
||||
await zip_pdf(pdf_file, zip_file)
|
||||
await merge_files(jpg_file, zip_file, output_file)
|
||||
|
||||
await delete_file_batch([zip_file, pdf_file])
|
||||
await delete_folder(original_path)
|
||||
async def download_jm(album_id: str| None,receiver_email: str| None):
|
||||
# 修改配置文件的下载路径
|
||||
source_path = await get_jm_config(receiver_email)
|
||||
option = jmcomic.JmOption.from_file(jm_config_path)
|
||||
# 还原配置文件
|
||||
await recover_jm_config(source_path)
|
||||
#调用JM下载api
|
||||
album_detail,downloader = await asyncio.get_event_loop().run_in_executor(jm_executor,jmcomic.download_album,album_id,option)
|
||||
if album_detail.title is None:
|
||||
return "下载失败,请检查JM ID 是否正确"
|
||||
# 创建变量
|
||||
folder_path = f"{jm_path}{receiver_email}"
|
||||
zip_path = f"{jm_path}{album_detail.title}.zip"
|
||||
# 压缩文件
|
||||
zip_status = await folder_zip(folder_path,zip_path)
|
||||
if not zip_status:
|
||||
await delete_folder(folder_path)
|
||||
return "压缩文件失败"
|
||||
# 发送邮件
|
||||
send_status = await send_email_by_qq(receiver_email,zip_path)
|
||||
if send_status:
|
||||
# 删除文件
|
||||
await delete_folder(folder_path)
|
||||
await delete_file(zip_path)
|
||||
return "发送成功,请注意查收\n如遇邮箱接收不到,请检查发送的邮箱是否正确,或者是否在垃圾箱"
|
||||
else:
|
||||
print("PDF文件或JPG文件不存在,请检查文件路径。")
|
||||
await delete_folder(folder_path)
|
||||
await delete_file(zip_path)
|
||||
return "发送邮件失败,请重试!"
|
||||
|
||||
return output_file
|
||||
|
||||
async def get_jm_config(receiver_email: str):
|
||||
|
||||
with open(jm_config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
source_path = config['dir_rule']['base_dir']
|
||||
new_base_dir = str(Path(source_path) / receiver_email)
|
||||
config['dir_rule']['base_dir'] = new_base_dir
|
||||
with open(jm_config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(config, f, sort_keys=False, allow_unicode=True)
|
||||
return source_path
|
||||
|
||||
async def recover_jm_config(source_path : str):
|
||||
|
||||
with open(jm_config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
new_base_dir = str(Path(source_path))
|
||||
config['dir_rule']['base_dir'] = new_base_dir
|
||||
with open(jm_config_path, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(config, f, sort_keys=False, allow_unicode=True)
|
||||
|
|
@ -70,6 +70,3 @@ async def silicon_flow(group_openid, content):
|
|||
|
||||
await GroupChatRole.save_chat_history(group_openid, {"role": "assistant", "content": reply_content})
|
||||
return reply_content
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(deepseek_chat("你拽什么啊?"))
|
||||
|
|
|
|||
3
src/configs/jm_config.yml
Normal file
3
src/configs/jm_config.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
dir_rule:
|
||||
base_dir: src\resources\image\jm
|
||||
rule: Bd_Pname
|
||||
|
|
@ -31,7 +31,7 @@ os.makedirs(font_path, exist_ok=True)
|
|||
# 临时数据路径
|
||||
temp_path = path + '/temp/'
|
||||
os.makedirs(temp_path, exist_ok=True)
|
||||
# JM发送 图片模板
|
||||
# JM下载位置
|
||||
jm_path = path + '/image/jm/'
|
||||
os.makedirs(jm_path, exist_ok=True)
|
||||
# 日志路径
|
||||
|
|
@ -41,7 +41,8 @@ os.makedirs(log_path, exist_ok=True)
|
|||
video_path = path+'/video/'
|
||||
os.makedirs(video_path, exist_ok=True)
|
||||
|
||||
|
||||
#jm配置文件路径
|
||||
jm_config_path = os.getcwd()+'/src/configs/jm_config.yml'
|
||||
|
||||
# # 语音路径
|
||||
# RECORD_PATH = Path() / "src" / "resources" / "record"
|
||||
|
|
|
|||
|
|
@ -1,19 +1,26 @@
|
|||
from pathlib import Path
|
||||
import re
|
||||
from nonebot.rule import to_me
|
||||
from nonebot.plugin import on_command
|
||||
from nonebot.adapters.qq import MessageSegment,MessageEvent
|
||||
from nonebot.adapters.qq import MessageEvent
|
||||
from src.clover_jm.jm_comic import download_jm
|
||||
from src.clover_image.delete_file import delete_file
|
||||
|
||||
jm = on_command("jm", rule=to_me(), priority=10,block=False)
|
||||
@jm.handle()
|
||||
async def handle_function(message: MessageEvent):
|
||||
await jm.send("正在下载中,请稍等")
|
||||
key = message.get_plaintext().replace("/jm", "").strip(" ")
|
||||
|
||||
if key == "":
|
||||
await jm.finish("请输入jm的id")
|
||||
values = message.get_plaintext().replace("/jm", "").split(" ")
|
||||
|
||||
output_file = await download_jm(key)
|
||||
await jm.send(MessageSegment.file_image(Path(output_file)))
|
||||
await delete_file(output_file)
|
||||
if 3 > len(values) > 4:
|
||||
await jm.finish("请输入正确的格式 /jm+id 或 /jm+id+邮箱号")
|
||||
else:
|
||||
if not validate_email(values[2]):
|
||||
await jm.finish("邮箱格式不正确!")
|
||||
|
||||
await jm.send("正在发送中,请稍等~")
|
||||
msg = await download_jm(album_id = values[1],receiver_email = values[2])
|
||||
await jm.finish(msg)
|
||||
|
||||
def validate_email(email: str) -> bool:
|
||||
"""验证邮箱格式是否合法"""
|
||||
EMAIL_REGEX = r"^[a-zA-Z0-9._%+-]+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$"
|
||||
return re.fullmatch(EMAIL_REGEX, email) is not None
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.7 KiB |
Loading…
Reference in a new issue