新增内容: - modules/wp_image_generator.py: AI 图片生成核心模块 - scripts/wp_generate_image.py: 命令行生图脚本 - feishu_bot.py: 新增#生图、#发布并生图 指令支持 - config.py: 添加 DashScope API Key 和生图配置 支持功能: - 独立 AI 生图:#生图 图片描述 - 发布+生图:#发布并生图(文章自动配图) - 可配置参数:模型、尺寸、数量、风格 - 异步任务轮询,支持 1-4 张图片并发 测试结果: - ✅ 生图任务提交成功 - ✅ 图片生成并保存到本地(1024x1024 PNG) - ✅ 文件大小约 1.3MB
967 lines
33 KiB
Python
967 lines
33 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
飞书机器人消息接收与处理脚本
|
||
接收飞书消息,解析内容,调用 WordPress 发布脚本
|
||
支持 AI 图片生成
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import time
|
||
import logging
|
||
from datetime import datetime
|
||
|
||
# 添加项目根目录到 Python 路径
|
||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||
sys.path.insert(0, BASE_DIR)
|
||
|
||
# 导入飞书 API 客户端
|
||
from modules.feishu_api import create_feishu_client
|
||
|
||
# 分类映射(slug -> ID)
|
||
CATEGORY_MAP = {
|
||
'ai-kepu': 12,
|
||
'ai-zixun': 11,
|
||
'geo': 16,
|
||
'ai': 9,
|
||
'jishu': 5,
|
||
'fenxiang': 10,
|
||
'wenzhang': 4,
|
||
'zaji': 8,
|
||
'suibi': 7,
|
||
'guanyu': 1
|
||
}
|
||
|
||
# 配置日志
|
||
LOG_DIR = os.path.join(BASE_DIR, 'logs')
|
||
os.makedirs(LOG_DIR, exist_ok=True)
|
||
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||
handlers=[
|
||
logging.FileHandler(os.path.join(LOG_DIR, 'feishu_bot.log'), encoding='utf-8'),
|
||
logging.StreamHandler()
|
||
]
|
||
)
|
||
logger = logging.getLogger('feishu_bot')
|
||
|
||
|
||
class FeishuBot:
|
||
"""飞书机器人"""
|
||
|
||
def __init__(self):
|
||
"""初始化飞书机器人"""
|
||
self.config = self._load_config()
|
||
self.wp_script = os.path.join(BASE_DIR, 'scripts', 'wp_publish_text.py')
|
||
self.word_script = os.path.join(BASE_DIR, 'scripts', 'wp_publish.py')
|
||
self.image_gen_script = os.path.join(BASE_DIR, 'scripts', 'wp_generate_image.py')
|
||
|
||
# 初始化飞书 API 客户端
|
||
self.feishu_client = create_feishu_client(
|
||
app_id=self.config.get('app_id'),
|
||
app_secret=self.config.get('app_secret')
|
||
)
|
||
|
||
logger.info("🤖 飞书机器人初始化完成")
|
||
logger.info(f" WordPress 发布脚本:{self.wp_script}")
|
||
logger.info(f" Word 发布脚本:{self.word_script}")
|
||
logger.info(f" AI 生图脚本:{self.image_gen_script}")
|
||
logger.info(f" 飞书 API 客户端:已初始化")
|
||
|
||
def _load_config(self):
|
||
"""加载配置"""
|
||
try:
|
||
from feishu_config import (
|
||
FEISHU_APP_ID, FEISHU_APP_SECRET,
|
||
SERVER_HOST, SERVER_PORT, ALLOWED_USERS
|
||
)
|
||
return {
|
||
'app_id': FEISHU_APP_ID,
|
||
'app_secret': FEISHU_APP_SECRET,
|
||
'host': SERVER_HOST,
|
||
'port': SERVER_PORT,
|
||
'allowed_users': ALLOWED_USERS
|
||
}
|
||
except ImportError:
|
||
logger.warning("未找到 feishu_config.py,使用默认配置")
|
||
return {
|
||
'app_id': '',
|
||
'app_secret': '',
|
||
'host': '0.0.0.0',
|
||
'port': 8080,
|
||
'allowed_users': []
|
||
}
|
||
|
||
def process_message(self, message):
|
||
"""
|
||
处理接收到的消息
|
||
|
||
Args:
|
||
message: 飞书消息对象
|
||
|
||
Returns:
|
||
str: 回复消息
|
||
"""
|
||
try:
|
||
# 解析消息内容
|
||
msg_type = message.get('msg_type', '')
|
||
content = message.get('content', '')
|
||
sender_id = message.get('sender', {}).get('sender_id', '')
|
||
message_id = message.get('message_id', '')
|
||
chat_id = message.get('chat_id', '')
|
||
|
||
logger.info(f"📨 收到消息 - 类型:{msg_type}, 发送者:{sender_id}, 消息 ID: {message_id}")
|
||
|
||
# 检查权限
|
||
if self.config['allowed_users'] and sender_id not in self.config['allowed_users']:
|
||
return "⚠️ 您没有权限使用此机器人"
|
||
|
||
# 处理不同类型的消息
|
||
if msg_type == 'text':
|
||
return self._handle_text_message(content, sender_id, message_id, chat_id)
|
||
elif msg_type == 'image':
|
||
return self._handle_image_message(content, sender_id, message_id, chat_id)
|
||
elif msg_type == 'file':
|
||
return self._handle_file_message(content, sender_id, message_id, chat_id)
|
||
elif msg_type == 'interactive':
|
||
return self._handle_interactive_message(content, sender_id, message_id, chat_id)
|
||
else:
|
||
return f"📝 暂不支持的消息类型:{msg_type}"
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 处理消息失败:{str(e)}", exc_info=True)
|
||
return f"❌ 处理消息失败:{str(e)}"
|
||
|
||
def _handle_text_message(self, content, sender_id, message_id=None, chat_id=None):
|
||
"""
|
||
处理文字消息
|
||
|
||
Args:
|
||
content: 消息内容
|
||
sender_id: 发送者 ID
|
||
message_id: 消息 ID(用于获取图片)
|
||
chat_id: 聊天 ID
|
||
|
||
Returns:
|
||
str: 回复消息
|
||
"""
|
||
logger.info(f"📝 处理文字消息")
|
||
|
||
# 解析指令
|
||
instruction = self._parse_instruction(content)
|
||
|
||
# 获取消息中的图片(如果有)
|
||
images = []
|
||
if message_id:
|
||
images = self._get_message_images(message_id)
|
||
|
||
# 检查是否为生图指令
|
||
if instruction.get('action') == 'generate_image':
|
||
return self._generate_image(
|
||
prompt=instruction.get('prompt', ''),
|
||
count=instruction.get('image_count', 1),
|
||
model=instruction.get('image_model', None),
|
||
size=instruction.get('image_size', None),
|
||
style=instruction.get('image_style', None)
|
||
)
|
||
# 检查是否为发布+生图指令
|
||
elif instruction.get('action') == 'publish_with_image':
|
||
return self._publish_with_generated_image(
|
||
text=instruction.get('text', ''),
|
||
title=instruction.get('title', ''),
|
||
category=instruction.get('category', ''),
|
||
tags=instruction.get('tags', ''),
|
||
status=instruction.get('status', 'publish'),
|
||
image_count=instruction.get('image_count', 1),
|
||
model=instruction.get('image_model', None),
|
||
size=instruction.get('image_size', None),
|
||
style=instruction.get('image_style', None)
|
||
)
|
||
# 检查是否为发布指令
|
||
elif instruction.get('action') == 'publish':
|
||
return self._publish_article(
|
||
text=instruction.get('text', ''),
|
||
title=instruction.get('title', ''),
|
||
category=instruction.get('category', ''),
|
||
tags=instruction.get('tags', ''),
|
||
status=instruction.get('status', 'publish'),
|
||
images=images if images else None
|
||
)
|
||
elif instruction.get('action') == 'update':
|
||
return self._update_article(
|
||
target=instruction.get('target', ''),
|
||
text=instruction.get('text', ''),
|
||
title=instruction.get('title', ''),
|
||
status=instruction.get('status', 'publish')
|
||
)
|
||
elif instruction.get('action') == 'help':
|
||
return self._get_help_message()
|
||
elif instruction.get('action') == 'status':
|
||
return self._get_status_message()
|
||
else:
|
||
# 默认发布
|
||
return self._publish_article(text=content, images=images if images else None)
|
||
|
||
def _get_message_images(self, message_id):
|
||
"""
|
||
获取消息中的图片列表
|
||
|
||
Args:
|
||
message_id: 消息 ID
|
||
|
||
Returns:
|
||
list: 图片本地路径列表
|
||
"""
|
||
if not message_id:
|
||
return []
|
||
|
||
logger.info(f"🔍 获取消息图片 - Message ID: {message_id}")
|
||
|
||
try:
|
||
# 使用飞书 API 客户端获取图片
|
||
images = self.feishu_client.get_message_images(message_id)
|
||
|
||
if not images:
|
||
logger.info("消息中没有图片")
|
||
return []
|
||
|
||
# 下载所有图片
|
||
downloaded_images = []
|
||
for img in images:
|
||
image_key = img.get('image_key', '')
|
||
if image_key:
|
||
image_path = self._download_image(image_key)
|
||
if image_path:
|
||
downloaded_images.append(image_path)
|
||
logger.info(f"✅ 图片下载成功:{image_path}")
|
||
else:
|
||
logger.error(f"❌ 图片下载失败:{image_key}")
|
||
|
||
logger.info(f"📊 共下载 {len(downloaded_images)} 张图片")
|
||
return downloaded_images
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取消息图片失败:{str(e)}")
|
||
return []
|
||
|
||
def _handle_image_message(self, content, sender_id, message_id=None, chat_id=None):
|
||
"""
|
||
处理图片消息
|
||
|
||
Args:
|
||
content: 消息内容
|
||
sender_id: 发送者 ID
|
||
message_id: 消息 ID(用于获取图片)
|
||
chat_id: 聊天 ID
|
||
|
||
Returns:
|
||
str: 回复消息
|
||
"""
|
||
logger.info(f"🖼️ 处理图片消息")
|
||
|
||
try:
|
||
image_key = json.loads(content).get('image_key', '')
|
||
|
||
# 下载图片
|
||
image_path = self._download_image(image_key)
|
||
|
||
if image_path:
|
||
# 发布带图片的文章(保留原始文字内容)
|
||
return self._publish_article(
|
||
text="图片文章", # 默认文字,如果有文字消息会保留
|
||
images=[image_path],
|
||
message_id=message_id,
|
||
chat_id=chat_id
|
||
)
|
||
else:
|
||
return "❌ 图片下载失败"
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理图片消息失败:{str(e)}")
|
||
return f"❌ 处理图片消息失败:{str(e)}"
|
||
|
||
def _handle_file_message(self, content, sender_id, message_id=None, chat_id=None):
|
||
"""
|
||
处理文件消息
|
||
|
||
Args:
|
||
content: 消息内容
|
||
sender_id: 发送者 ID
|
||
message_id: 消息 ID
|
||
chat_id: 聊天 ID
|
||
|
||
Returns:
|
||
str: 回复消息
|
||
"""
|
||
logger.info(f"📁 处理文件消息")
|
||
|
||
try:
|
||
file_info = json.loads(content)
|
||
file_key = file_info.get('file_key', '')
|
||
file_name = file_info.get('file_name', '')
|
||
|
||
# 检查是否为 Word 文档
|
||
if not file_name.endswith('.docx'):
|
||
return "⚠️ 仅支持 .docx 格式的 Word 文档"
|
||
|
||
# 下载文件
|
||
file_path = self._download_file(file_key, file_name)
|
||
|
||
if file_path:
|
||
# 发布 Word 文档
|
||
return self._publish_word_document(file_path)
|
||
else:
|
||
return "❌ 文件下载失败"
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理文件消息失败:{str(e)}")
|
||
return f"❌ 处理文件消息失败:{str(e)}"
|
||
|
||
def _handle_interactive_message(self, content, sender_id, message_id=None, chat_id=None):
|
||
"""
|
||
处理交互式消息
|
||
|
||
Args:
|
||
content: 消息内容
|
||
sender_id: 发送者 ID
|
||
message_id: 消息 ID
|
||
chat_id: 聊天 ID
|
||
|
||
Returns:
|
||
str: 回复消息
|
||
"""
|
||
logger.info(f"🔄 处理交互式消息")
|
||
|
||
try:
|
||
data = json.loads(content)
|
||
action = data.get('action', '')
|
||
|
||
if action == 'publish':
|
||
return self._publish_article(
|
||
text=data.get('text', ''),
|
||
title=data.get('title', ''),
|
||
category=data.get('category', '')
|
||
)
|
||
else:
|
||
return f"⚠️ 未知的交互动作:{action}"
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理交互式消息失败:{str(e)}")
|
||
return f"❌ 处理交互式消息失败:{str(e)}"
|
||
|
||
def _parse_instruction(self, content):
|
||
"""
|
||
解析消息指令
|
||
|
||
Args:
|
||
content: 消息内容
|
||
|
||
Returns:
|
||
dict: 解析结果
|
||
"""
|
||
instruction = {
|
||
'action': 'publish', # 默认动作:发布
|
||
'text': '',
|
||
'title': '',
|
||
'category': '',
|
||
'tags': '',
|
||
'status': 'publish',
|
||
'prompt': '',
|
||
'image_count': 1,
|
||
'image_model': None,
|
||
'image_size': None,
|
||
'image_style': None
|
||
}
|
||
|
||
lines = content.strip().split('\n')
|
||
text_lines = []
|
||
|
||
for line in lines:
|
||
line = line.strip()
|
||
|
||
# 解析指令
|
||
if line.startswith('#标题'):
|
||
instruction['title'] = line.replace('#标题', '').strip()
|
||
elif line.startswith('#分类') or line.startswith('#category'):
|
||
instruction['category'] = line.replace('#分类', '').replace('#category', '').strip()
|
||
elif line.startswith('#标签') or line.startswith('#tag'):
|
||
instruction['tags'] = line.replace('#标签', '').replace('#tag', '').strip()
|
||
elif line.startswith('#状态') or line.startswith('#status'):
|
||
status = line.replace('#状态', '').replace('#status', '').strip().lower()
|
||
if status in ['publish', 'draft', 'pending', 'private']:
|
||
instruction['status'] = status
|
||
elif line.startswith('#发布'):
|
||
instruction['action'] = 'publish'
|
||
elif line.startswith('#草稿'):
|
||
instruction['status'] = 'draft'
|
||
instruction['action'] = 'publish'
|
||
elif line.startswith('#帮助') or line.startswith('#help'):
|
||
instruction['action'] = 'help'
|
||
elif line.startswith('#状态') or line.startswith('#status'):
|
||
instruction['action'] = 'status'
|
||
elif line.startswith('#更新') or line.startswith('#update'):
|
||
instruction['action'] = 'update'
|
||
instruction['target'] = line.replace('#更新', '').replace('#update', '').strip()
|
||
# AI 生图指令
|
||
elif line.startswith('#生图') or line.startswith('#生成图片'):
|
||
instruction['action'] = 'generate_image'
|
||
prompt_text = line.replace('#生图', '').replace('#生成图片', '').strip()
|
||
instruction['prompt'] = prompt_text
|
||
elif line.startswith('#生图数量') or line.startswith('#图片数量'):
|
||
try:
|
||
count = int(line.replace('#生图数量', '').replace('#图片数量', '').strip())
|
||
instruction['image_count'] = min(max(count, 1), 4)
|
||
except ValueError:
|
||
pass
|
||
elif line.startswith('#生图模型') or line.startswith('#图片模型'):
|
||
model = line.replace('#生图模型', '').replace('#图片模型', '').strip()
|
||
instruction['image_model'] = model
|
||
elif line.startswith('#生图尺寸') or line.startswith('#图片尺寸'):
|
||
size = line.replace('#生图尺寸', '').replace('#图片尺寸', '').strip()
|
||
instruction['image_size'] = size
|
||
elif line.startswith('#生图风格') or line.startswith('#图片风格'):
|
||
style = line.replace('#生图风格', '').replace('#图片风格', '').strip()
|
||
instruction['image_style'] = style
|
||
# 发布+生图指令
|
||
elif line.startswith('#发布并生图') or line.startswith('#发布+生图'):
|
||
instruction['action'] = 'publish_with_image'
|
||
else:
|
||
text_lines.append(line)
|
||
|
||
instruction['text'] = '\n'.join(text_lines)
|
||
|
||
return instruction
|
||
|
||
def _resolve_category(self, category_slug):
|
||
"""
|
||
将分类 slug 转换为 ID
|
||
|
||
Args:
|
||
category_slug: 分类 slug
|
||
|
||
Returns:
|
||
int: 分类 ID
|
||
"""
|
||
if not category_slug:
|
||
return None
|
||
|
||
# 先尝试直接匹配
|
||
slug = category_slug.lower().strip()
|
||
if slug in CATEGORY_MAP:
|
||
return CATEGORY_MAP[slug]
|
||
|
||
# 尝试模糊匹配
|
||
for key, value in CATEGORY_MAP.items():
|
||
if slug in key or key in slug:
|
||
return value
|
||
|
||
# 如果输入的是数字,直接返回
|
||
try:
|
||
return int(slug)
|
||
except ValueError:
|
||
pass
|
||
|
||
# 默认返回随笔分类
|
||
logger.warning(f"未找到分类:{category_slug},使用默认分类")
|
||
return CATEGORY_MAP.get('suibi', 7)
|
||
|
||
def _publish_article(self, text='', title='', category='', tags='', images=None, status='publish', message_id=None, chat_id=None):
|
||
"""
|
||
发布文章(直接调用 Python 函数,避免 shell 引号问题)
|
||
|
||
Args:
|
||
text: 文章正文
|
||
title: 文章标题
|
||
category: 分类
|
||
tags: 标签
|
||
images: 图片列表
|
||
status: 发布状态
|
||
message_id: 消息 ID
|
||
chat_id: 聊天 ID
|
||
|
||
Returns:
|
||
str: 回复消息
|
||
"""
|
||
if not text:
|
||
return "⚠️ 文章内容不能为空"
|
||
|
||
logger.info(f"📝 准备发布文章 - 标题:{title}, 分类:{category}")
|
||
|
||
try:
|
||
# 直接导入并调用 Python 函数(避免 subprocess 的 shell 转义问题)
|
||
from scripts.wp_publish_text import publish_text_with_images
|
||
|
||
# 解析分类(如果是 slug 需要转换为 ID)
|
||
category_id = self._resolve_category(category) if category else None
|
||
|
||
# 调用发布函数
|
||
publish_result = publish_text_with_images(
|
||
text=text,
|
||
images=images,
|
||
instruction=f"#分类 {category}" if category else None,
|
||
status=status,
|
||
category_id=category_id,
|
||
title=title if title else None
|
||
)
|
||
|
||
# 处理结果
|
||
if publish_result.get('success'):
|
||
post_url = publish_result.get('post_url', '')
|
||
post_id = publish_result.get('post_id', '')
|
||
|
||
reply = "✅ 文章发布成功!\n"
|
||
reply += f"📝 标题:{publish_result.get('title', title or '自动提取')}\n"
|
||
reply += f"🔗 链接:{post_url}\n"
|
||
reply += f"📊 文章 ID:{post_id}"
|
||
|
||
if publish_result.get('images_uploaded', 0) > 0:
|
||
reply += f"\n🖼️ 已上传图片:{publish_result['images_uploaded']} 张"
|
||
|
||
return reply
|
||
else:
|
||
error_msg = publish_result.get('error', '未知错误')
|
||
return f"❌ 发布失败:{error_msg}"
|
||
|
||
except Exception as e:
|
||
logger.error(f"发布文章失败:{str(e)}", exc_info=True)
|
||
return f"❌ 发布失败:{str(e)}"
|
||
|
||
def _publish_with_generated_image(self, text='', title='', category='', tags='', status='publish',
|
||
image_count=1, model=None, size=None, style=None):
|
||
"""
|
||
发布文章并自动生成配图
|
||
|
||
Args:
|
||
text: 文章正文
|
||
title: 文章标题
|
||
category: 分类
|
||
tags: 标签
|
||
status: 发布状态
|
||
image_count: 生成图片数量
|
||
model: 生图模型
|
||
size: 图片尺寸
|
||
style: 图片风格
|
||
|
||
Returns:
|
||
str: 回复消息
|
||
"""
|
||
if not text and not title:
|
||
return "⚠️ 请提供文章标题或内容"
|
||
|
||
logger.info(f"📝 准备发布文章(含 AI 配图)- 标题:{title}")
|
||
|
||
try:
|
||
# 加载配置获取 API Key
|
||
config = self._load_image_gen_config()
|
||
|
||
if not config.get('dashscope_api_key'):
|
||
return "⚠️ 未配置 DashScope API Key,无法生成图片\n\n请在 config.py 中配置 dashscope_api_key"
|
||
|
||
# 生成图片
|
||
from scripts.wp_generate_image import generate_images_for_article
|
||
|
||
pl = logger
|
||
pl.info(f"🎨 开始生成 AI 配图...")
|
||
|
||
image_result = generate_images_for_article(
|
||
title=title or '自动提取',
|
||
content=text,
|
||
count=image_count,
|
||
api_key=config.get('dashscope_api_key')
|
||
)
|
||
|
||
if not image_result.get('success'):
|
||
return f"⚠️ AI 配图生成失败:{image_result.get('error', '未知错误')}\n\n文章仍可发布,但不含配图。"
|
||
|
||
image_paths = image_result.get('paths', [])
|
||
pl.info(f"✅ 已生成 {len(image_paths)} 张配图")
|
||
|
||
# 发布文章(带图片)
|
||
from scripts.wp_publish_text import publish_text_with_images
|
||
|
||
category_id = self._resolve_category(category) if category else None
|
||
|
||
publish_result = publish_text_with_images(
|
||
text=text,
|
||
images=image_paths,
|
||
instruction=f"#分类 {category}" if category else None,
|
||
status=status,
|
||
category_id=category_id,
|
||
title=title if title else None
|
||
)
|
||
|
||
# 处理结果
|
||
if publish_result.get('success'):
|
||
post_url = publish_result.get('post_url', '')
|
||
post_id = publish_result.get('post_id', '')
|
||
|
||
reply = "✅ 文章发布成功(含 AI 配图)!\n"
|
||
reply += f"📝 标题:{publish_result.get('title', title or '自动提取')}\n"
|
||
reply += f"🔗 链接:{post_url}\n"
|
||
reply += f"📊 文章 ID:{post_id}\n"
|
||
reply += f"🎨 AI 配图:{len(image_paths)} 张"
|
||
|
||
return reply
|
||
else:
|
||
error_msg = publish_result.get('error', '未知错误')
|
||
return f"❌ 发布失败:{error_msg}"
|
||
|
||
except Exception as e:
|
||
logger.error(f"发布文章(含 AI 配图)失败:{str(e)}", exc_info=True)
|
||
return f"❌ 发布失败:{str(e)}"
|
||
|
||
def _generate_image(self, prompt='', count=1, model=None, size=None, style=None):
|
||
"""
|
||
生成 AI 图片
|
||
|
||
Args:
|
||
prompt: 图片描述
|
||
count: 生成数量
|
||
model: 模型名称
|
||
size: 图片尺寸
|
||
style: 图片风格
|
||
|
||
Returns:
|
||
str: 回复消息
|
||
"""
|
||
if not prompt:
|
||
return "⚠️ 请提供图片描述\n\n示例:`#生图 一只可爱的猫咪在草地上晒太阳`"
|
||
|
||
logger.info(f"🎨 开始 AI 生图 - 描述:{prompt}")
|
||
|
||
try:
|
||
# 加载配置获取 API Key
|
||
config = self._load_image_gen_config()
|
||
|
||
if not config.get('dashscope_api_key'):
|
||
return "⚠️ 未配置 DashScope API Key,无法生成图片\n\n请在 config.py 中配置 dashscope_api_key"
|
||
|
||
# 调用生图脚本
|
||
from scripts.wp_generate_image import generate_image
|
||
|
||
result = generate_image(
|
||
prompt=prompt,
|
||
api_key=config.get('dashscope_api_key'),
|
||
model=model,
|
||
size=size,
|
||
count=count,
|
||
style=style
|
||
)
|
||
|
||
if result.get('success'):
|
||
paths = result.get('paths', [])
|
||
reply = f"✅ AI 生图完成!成功生成 {len(paths)} 张图片\n"
|
||
reply += f"📊 模型:{result.get('model', 'wanx-v1')}\n"
|
||
reply += f"📐 尺寸:{result.get('size', '1024*1024')}\n\n"
|
||
reply += "📁 本地路径:\n"
|
||
for i, path in enumerate(paths):
|
||
reply += f" {i+1}. {path}\n"
|
||
|
||
return reply
|
||
else:
|
||
error_msg = result.get('error', '未知错误')
|
||
return f"❌ AI 生图失败:{error_msg}"
|
||
|
||
except Exception as e:
|
||
logger.error(f"AI 生图失败:{str(e)}", exc_info=True)
|
||
return f"❌ AI 生图失败:{str(e)}"
|
||
|
||
def _load_image_gen_config(self):
|
||
"""加载图片生成配置"""
|
||
config = {
|
||
'dashscope_api_key': '',
|
||
'image_model': 'wanx-v1',
|
||
'image_size': '1024*1024',
|
||
'image_count': 1
|
||
}
|
||
|
||
config_file = os.path.join(BASE_DIR, 'config.py')
|
||
if os.path.exists(config_file):
|
||
try:
|
||
with open(config_file, 'r', encoding='utf-8') as f:
|
||
exec(f.read(), config)
|
||
except Exception as e:
|
||
logger.warning(f"加载配置文件失败:{str(e)}")
|
||
|
||
return config
|
||
|
||
def _update_article(self, target='', text='', title='', status='publish'):
|
||
"""
|
||
更新已有文章(追加内容模式)
|
||
|
||
Args:
|
||
target: 文章 ID 或标题关键词
|
||
text: 新增内容
|
||
title: 新标题(可选)
|
||
status: 发布状态
|
||
|
||
Returns:
|
||
str: 回复消息
|
||
"""
|
||
if not target:
|
||
return "⚠️ 请指定文章 ID 或标题,例如:\n`#更新 12345`\n`#更新 文章标题`"
|
||
if not text:
|
||
return "⚠️ 更新内容不能为空"
|
||
|
||
logger.info(f"🔄 准备更新文章 - 目标:{target}")
|
||
|
||
try:
|
||
from scripts.wp_publish_text import update_post_with_text
|
||
|
||
result = update_post_with_text(
|
||
target=target,
|
||
new_text=text,
|
||
new_title=title if title else None,
|
||
status=status
|
||
)
|
||
|
||
if result.get('success'):
|
||
reply = "✅ 文章更新成功!\n"
|
||
reply += f"📝 标题:{result.get('title')}\n"
|
||
reply += f"🔗 链接:{result.get('post_url')}\n"
|
||
reply += f"📊 文章 ID:{result.get('post_id')}"
|
||
return reply
|
||
else:
|
||
return f"❌ 更新失败:{result.get('error')}"
|
||
|
||
except Exception as e:
|
||
logger.error(f"更新文章失败:{str(e)}", exc_info=True)
|
||
return f"❌ 更新失败:{str(e)}"
|
||
|
||
def _publish_word_document(self, file_path):
|
||
"""
|
||
发布 Word 文档(直接调用 Python 函数)
|
||
|
||
Args:
|
||
file_path: Word 文档路径
|
||
|
||
Returns:
|
||
str: 回复消息
|
||
"""
|
||
logger.info(f"📄 准备发布 Word 文档:{file_path}")
|
||
|
||
try:
|
||
# 直接导入并调用 Python 函数
|
||
from scripts.wp_publish import publish_word_document
|
||
|
||
# 调用发布函数
|
||
publish_result = publish_word_document(word_file_path=file_path)
|
||
|
||
# 处理结果
|
||
if publish_result.get('success'):
|
||
post_url = publish_result.get('post_url', '')
|
||
post_id = publish_result.get('post_id', '')
|
||
title = publish_result.get('title', '自动提取')
|
||
|
||
reply = "✅ Word 文档发布成功!\n"
|
||
reply += f"📝 标题:{title}\n"
|
||
reply += f"🔗 链接:{post_url}\n"
|
||
reply += f"📊 文章 ID:{post_id}"
|
||
|
||
if publish_result.get('images_uploaded', 0) > 0:
|
||
reply += f"\n🖼️ 已上传图片:{publish_result['images_uploaded']} 张"
|
||
|
||
return reply
|
||
else:
|
||
error_msg = publish_result.get('error', '未知错误')
|
||
return f"❌ 发布失败:{error_msg}"
|
||
|
||
except Exception as e:
|
||
logger.error(f"发布 Word 文档失败:{str(e)}", exc_info=True)
|
||
return f"❌ 发布失败:{str(e)}"
|
||
|
||
def _get_help_message(self):
|
||
"""
|
||
获取帮助信息
|
||
|
||
Returns:
|
||
str: 帮助消息
|
||
"""
|
||
help_text = """🤖 **WordPress 发布助手 - 使用说明**
|
||
|
||
📝 **发布文字文章**
|
||
直接发送文字内容即可发布
|
||
|
||
📄 **发布 Word 文档**
|
||
发送 .docx 格式的 Word 文档
|
||
|
||
🖼️ **发布图片文章**
|
||
发送图片即可发布
|
||
|
||
🎨 **AI 图片生成**(新增)
|
||
使用 AI 生成文章配图或独立图片
|
||
|
||
---
|
||
|
||
**基础指令**:
|
||
|
||
`#标题 文章标题` - 指定文章标题
|
||
`#分类 分类名` - 指定分类(如:#分类 ai)
|
||
`#标签 标签名` - 指定标签
|
||
`#草稿` - 保存为草稿
|
||
`#发布` - 立即发布(默认)
|
||
`#更新 ID或标题` - 更新已有文章(追加内容)
|
||
|
||
---
|
||
|
||
**AI 生图指令**(新增):
|
||
|
||
`#生图 图片描述` - 生成指定描述的图片
|
||
`#生图数量 2` - 设置生成数量(1-4)
|
||
`#生图模型 wanx-v1` - 选择模型
|
||
`#生图尺寸 1024*1024` - 设置尺寸
|
||
`#生图风格 写实` - 设置风格
|
||
|
||
**发布+生图指令**(新增):
|
||
|
||
`#发布并生图` - 发布文章并自动生成配图
|
||
(与 #标题、#分类 等指令配合使用)
|
||
|
||
---
|
||
|
||
**示例**:
|
||
|
||
```
|
||
#标题 AI 发展趋势
|
||
#分类 ai
|
||
人工智能正在改变世界...
|
||
```
|
||
|
||
**AI 生图示例**:
|
||
|
||
```
|
||
#生图 一只可爱的猫咪在草地上晒太阳,高质量插画
|
||
#生图数量 2
|
||
#生图尺寸 1024*1024
|
||
```
|
||
|
||
**发布+生图示例**:
|
||
|
||
```
|
||
#发布并生图
|
||
#标题 AI 未来展望
|
||
#分类 ai
|
||
人工智能正在改变世界...
|
||
```
|
||
|
||
**可用分类**:
|
||
- ai - 人工智能
|
||
- ai-kepu - Ai 科普
|
||
- ai-zixun - Ai 资讯
|
||
- geo - GEO
|
||
- jishu - 技术资料
|
||
- fenxiang - 好物分享
|
||
- wenzhang - 文章分享
|
||
- zaji - 杂记
|
||
- suibi - 随笔(默认)
|
||
|
||
发送 `#帮助` 查看此消息"""
|
||
|
||
return help_text
|
||
|
||
def _get_status_message(self):
|
||
"""
|
||
获取系统状态
|
||
|
||
Returns:
|
||
str: 状态消息
|
||
"""
|
||
status_text = f"""📊 **系统状态**
|
||
|
||
- **时间**:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||
- **状态**:✅ 运行中
|
||
- **WordPress**:https://www.nanlou.net
|
||
- **发布脚本**:已就绪
|
||
- **AI 生图**:✅ 支持
|
||
|
||
发送 `#帮助` 查看使用说明"""
|
||
|
||
return status_text
|
||
|
||
def _download_image(self, image_key):
|
||
"""
|
||
下载飞书图片到本地
|
||
|
||
Args:
|
||
image_key: 图片 key
|
||
|
||
Returns:
|
||
str: 图片本地路径
|
||
"""
|
||
if not image_key:
|
||
logger.warning("图片 key 为空")
|
||
return None
|
||
|
||
# 使用飞书 API 客户端下载图片
|
||
save_dir = os.path.join(BASE_DIR, 'temp')
|
||
image_path = self.feishu_client.download_image(image_key, save_dir)
|
||
|
||
if image_path:
|
||
logger.info(f"✅ 图片下载成功:{image_path}")
|
||
return image_path
|
||
else:
|
||
logger.error("❌ 图片下载失败")
|
||
return None
|
||
|
||
def _download_file(self, file_key, file_name):
|
||
"""
|
||
下载飞书文件到本地
|
||
|
||
Args:
|
||
file_key: 文件 key
|
||
file_name: 文件名
|
||
|
||
Returns:
|
||
str: 文件本地路径
|
||
"""
|
||
if not file_key:
|
||
logger.warning("文件 key 为空")
|
||
return None
|
||
|
||
# 使用飞书 API 客户端下载文件
|
||
save_dir = os.path.join(BASE_DIR, 'temp')
|
||
file_path = self.feishu_client.download_file(file_key, file_name, save_dir)
|
||
|
||
if file_path:
|
||
logger.info(f"✅ 文件下载成功:{file_path}")
|
||
return file_path
|
||
else:
|
||
logger.error("❌ 文件下载失败")
|
||
return None
|
||
|
||
|
||
# 创建机器人实例
|
||
bot = FeishuBot()
|
||
|
||
|
||
def handle_message(message):
|
||
"""
|
||
处理消息入口函数
|
||
|
||
Args:
|
||
message: 飞书消息字典
|
||
|
||
Returns:
|
||
str: 回复消息
|
||
"""
|
||
return bot.process_message(message)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
# 测试模式
|
||
print("🤖 飞书机器人测试模式")
|
||
print("=" * 50)
|
||
|
||
# 测试文字消息
|
||
test_message = {
|
||
'msg_type': 'text',
|
||
'content': '#标题 测试文章\n#分类 ai\n这是测试内容',
|
||
'sender': {'sender_id': 'test_user'}
|
||
}
|
||
|
||
reply = bot.process_message(test_message)
|
||
print(f"\n📨 回复:\n{reply}")
|