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:
SlyAimer 2025-04-02 16:36:17 +08:00
parent c49139607e
commit e70b262402
10 changed files with 347 additions and 41 deletions

3
.gitignore vendored
View file

@ -26,4 +26,5 @@ bili.cookie
/src/resources/image/yuc_wiki/*
src/clover_lightnovel/wenku8.cookie
src/clover_lightnovel/output1.html
*.pyc
*.pyc
/src/resources/image/jm/*

View file

@ -29,6 +29,7 @@ paramiko
commonX
jmcomic
natsort
aiosmtplib
requests
pillow

View 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

View file

@ -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

View file

@ -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)

View file

@ -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("你拽什么啊?"))

View file

@ -0,0 +1,3 @@
dir_rule:
base_dir: src\resources\image\jm
rule: Bd_Pname

View file

@ -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"

View file

@ -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