463 lines
17 KiB
Python
Executable File
463 lines
17 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
小红书自动发布器
|
||
集成到IT资讯自动化系统
|
||
"""
|
||
|
||
import json
|
||
import os
|
||
import sys
|
||
import subprocess
|
||
from datetime import datetime
|
||
import time
|
||
from pathlib import Path
|
||
|
||
class XiaohongshuPublisher:
|
||
"""小红书自动发布器"""
|
||
|
||
def __init__(self):
|
||
self.cookie_manager = None
|
||
self.content_converter = None
|
||
|
||
# 初始化组件
|
||
self._init_components()
|
||
|
||
def _init_components(self):
|
||
"""初始化组件"""
|
||
try:
|
||
# 导入Cookie管理器
|
||
sys.path.append(os.path.dirname(__file__))
|
||
from cookie_manager import XiaohongshuCookieManager
|
||
self.cookie_manager = XiaohongshuCookieManager()
|
||
|
||
# 导入内容转换器
|
||
from content_converter import ContentConverter, Article
|
||
self.content_converter = ContentConverter()
|
||
|
||
print("✅ 组件初始化完成")
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 组件初始化警告: {e}")
|
||
# 即使某些组件失败,仍可继续
|
||
|
||
def check_prerequisites(self):
|
||
"""检查前置条件"""
|
||
print("🔍 检查前置条件...")
|
||
|
||
# 检查xhs命令
|
||
try:
|
||
result = subprocess.run(['which', 'xhs'], capture_output=True, text=True)
|
||
if result.returncode != 0:
|
||
print("❌ xhs命令未找到")
|
||
return False
|
||
print("✅ xhs命令可用")
|
||
except:
|
||
print("❌ 无法检查xhs命令")
|
||
return False
|
||
|
||
# 检查Cookie
|
||
if self.cookie_manager:
|
||
if not self.cookie_manager.check_cookie_validity():
|
||
print("❌ Cookie无效")
|
||
return False
|
||
print("✅ Cookie有效")
|
||
|
||
print("✅ 所有前置条件满足")
|
||
return True
|
||
|
||
def create_tech_image(self, title, source, style="modern"):
|
||
"""创建技术资讯图片(使用ImageMagick)"""
|
||
print(f"🎨 创建技术资讯图片 (风格: {style})...")
|
||
|
||
try:
|
||
# 使用高级图片生成器
|
||
from advanced_image_generator import AdvancedImageGenerator
|
||
|
||
generator = AdvancedImageGenerator()
|
||
image_path = generator.create_tech_news_image(title, source, style=style)
|
||
|
||
if image_path:
|
||
print(f"✅ 图片创建成功: {image_path}")
|
||
return image_path
|
||
else:
|
||
print("❌ 高级图片生成失败,使用回退方法")
|
||
return self._create_fallback_image(title, source)
|
||
|
||
except Exception as e:
|
||
print(f"❌ 高级图片生成器异常: {e}")
|
||
return self._create_fallback_image(title, source)
|
||
|
||
def _create_fallback_image(self, title, source):
|
||
"""回退图片生成方法"""
|
||
try:
|
||
# 使用ImageMagick创建简单图片
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
image_dir = "/tmp/xiaohongshu_images"
|
||
os.makedirs(image_dir, exist_ok=True)
|
||
|
||
image_path = os.path.join(image_dir, f"fallback_{timestamp}.jpg")
|
||
|
||
# 创建渐变背景
|
||
cmd = [
|
||
'convert', '-size', '800x600',
|
||
'gradient:blue-red',
|
||
'-fill', 'white',
|
||
'-pointsize', '30',
|
||
'-gravity', 'Center',
|
||
'-draw', f'text 0,-50 "技术资讯"',
|
||
'-pointsize', '24',
|
||
'-draw', f'text 0,0 "{title[:40]}"',
|
||
'-pointsize', '18',
|
||
'-draw', f'text 0,100 "来源: {source}"',
|
||
'-pointsize', '16',
|
||
'-draw', f'text 0,150 "{datetime.now().strftime("%Y-%m-%d %H:%M")}"',
|
||
image_path
|
||
]
|
||
|
||
subprocess.run(cmd, check=True)
|
||
print(f"✅ 回退图片创建成功: {image_path}")
|
||
return image_path
|
||
|
||
except Exception as e:
|
||
print(f"❌ 回退图片也失败: {e}")
|
||
return None
|
||
|
||
def _wrap_text(self, text, font, max_width):
|
||
"""文本换行"""
|
||
lines = []
|
||
words = text.split()
|
||
current_line = []
|
||
|
||
for word in words:
|
||
test_line = ' '.join(current_line + [word])
|
||
# 简单估算宽度(字符数 * 20)
|
||
test_width = len(test_line) * 20
|
||
|
||
if test_width <= max_width:
|
||
current_line.append(word)
|
||
else:
|
||
if current_line:
|
||
lines.append(' '.join(current_line))
|
||
current_line = [word]
|
||
|
||
if current_line:
|
||
lines.append(' '.join(current_line))
|
||
|
||
return lines
|
||
|
||
def publish_article(self, article_data, image_path=None, image_style="auto"):
|
||
"""发布文章到小红书(支持A/B测试)"""
|
||
print(f"🚀 发布文章到小红书: {article_data.get('title', '')[:50]}...")
|
||
|
||
try:
|
||
# 准备内容
|
||
title = article_data.get('title', '技术资讯分享')
|
||
content = article_data.get('content', '')
|
||
source = article_data.get('source', 'IT资讯')
|
||
tags = article_data.get('tags', [])
|
||
|
||
# 构建小红书内容
|
||
xhs_content = self._format_xiaohongshu_content(title, content, source, tags)
|
||
|
||
# 选择图片风格
|
||
if image_style == "auto":
|
||
# 使用A/B测试选择最佳风格
|
||
try:
|
||
from image_ab_test import ImageABTest
|
||
ab_test = ImageABTest()
|
||
selected_style = ab_test.select_best_style(title, source)
|
||
print(f"🎯 A/B测试选择风格: {selected_style}")
|
||
image_style = selected_style
|
||
except Exception as e:
|
||
print(f"⚠️ A/B测试失败,使用默认风格: {e}")
|
||
image_style = "modern"
|
||
|
||
# 创建图片(如果没有提供)
|
||
if not image_path or not os.path.exists(image_path):
|
||
image_path = self.create_tech_image(title, source, style=image_style)
|
||
|
||
if not image_path:
|
||
print("❌ 无法创建图片,无法发布")
|
||
return False
|
||
|
||
# 执行发布命令
|
||
print(f"📤 执行发布...")
|
||
print(f" 标题: {title[:30]}...")
|
||
print(f" 内容长度: {len(xhs_content)} 字符")
|
||
print(f" 图片: {image_path}")
|
||
|
||
# 截断标题(小红书可能有长度限制)
|
||
short_title = title[:30]
|
||
|
||
cmd = [
|
||
'xhs', 'post',
|
||
'--title', short_title,
|
||
'--body', xhs_content,
|
||
'--images', image_path,
|
||
'--json'
|
||
]
|
||
|
||
result = subprocess.run(
|
||
cmd,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=60
|
||
)
|
||
|
||
print(f"返回码: {result.returncode}")
|
||
|
||
if result.returncode == 0:
|
||
try:
|
||
data = json.loads(result.stdout)
|
||
if data.get('ok'):
|
||
note_id = data.get('data', {}).get('id', '未知')
|
||
print(f"🎉 发布成功! 笔记ID: {note_id}")
|
||
|
||
# 保存发布记录
|
||
self._save_publish_record(article_data, note_id, image_path)
|
||
|
||
# 记录A/B测试
|
||
try:
|
||
from image_ab_test import ImageABTest
|
||
ab_test = ImageABTest()
|
||
test_id = ab_test.record_test(
|
||
title,
|
||
source,
|
||
image_style,
|
||
note_id
|
||
)
|
||
print(f"📝 A/B测试记录已创建 (ID: {test_id})")
|
||
except Exception as e:
|
||
print(f"⚠️ A/B测试记录失败: {e}")
|
||
|
||
return {
|
||
'success': True,
|
||
'note_id': note_id,
|
||
'data': data,
|
||
'image_style': image_style
|
||
}
|
||
else:
|
||
print(f"❌ 发布失败: {data}")
|
||
return {'success': False, 'error': 'API返回错误'}
|
||
except json.JSONDecodeError:
|
||
print(f"❌ JSON解析失败")
|
||
return {'success': False, 'error': 'JSON解析失败'}
|
||
else:
|
||
print(f"❌ 命令执行失败")
|
||
if result.stderr:
|
||
print(f"错误信息: {result.stderr[:200]}")
|
||
return {'success': False, 'error': '命令执行失败'}
|
||
|
||
except subprocess.TimeoutExpired:
|
||
print("⏰ 发布超时")
|
||
return {'success': False, 'error': '超时'}
|
||
except Exception as e:
|
||
print(f"❌ 发布异常: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return {'success': False, 'error': str(e)}
|
||
|
||
def _format_xiaohongshu_content(self, title, content, source, tags):
|
||
"""格式化为小红书内容(优化版)"""
|
||
from content_converter import ContentConverter, Article
|
||
|
||
# 创建文章对象
|
||
article = Article(
|
||
title=title,
|
||
content=content,
|
||
source=source,
|
||
url="",
|
||
images=[],
|
||
publish_date=datetime.now().strftime("%Y-%m-%d"),
|
||
author="IT资讯自动化",
|
||
category="技术"
|
||
)
|
||
|
||
# 使用内容转换器
|
||
converter = ContentConverter({
|
||
'max_title_length': 40,
|
||
'max_content_length': 1200,
|
||
'auto_generate_tags': True,
|
||
'add_source_info': True,
|
||
'add_emoji': True,
|
||
'max_tags': 8
|
||
})
|
||
|
||
# 生成小红书内容
|
||
xhs_content = converter.to_xiaohongshu(article)
|
||
|
||
# 添加自定义标签
|
||
if tags:
|
||
tag_text = " ".join([f"#{tag}" for tag in tags[:3]])
|
||
xhs_content += f"\n\n{tag_text}"
|
||
|
||
# 添加互动引导
|
||
xhs_content += "\n\n💬 你对这个话题有什么看法?欢迎在评论区讨论!"
|
||
xhs_content += "\n⭐ 如果觉得有用,记得点赞收藏哦~"
|
||
|
||
return xhs_content
|
||
|
||
def _save_publish_record(self, article_data, note_id, image_path):
|
||
"""保存发布记录"""
|
||
record_dir = "/home/ubuntu/.nanobot/workspace/xiaohongshu_publish_records"
|
||
os.makedirs(record_dir, exist_ok=True)
|
||
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
record_file = os.path.join(record_dir, f"publish_{timestamp}.json")
|
||
|
||
record = {
|
||
'timestamp': timestamp,
|
||
'note_id': note_id,
|
||
'article': {
|
||
'title': article_data.get('title'),
|
||
'source': article_data.get('source'),
|
||
'url': article_data.get('url')
|
||
},
|
||
'image_path': image_path,
|
||
'publish_time': datetime.now().isoformat()
|
||
}
|
||
|
||
with open(record_file, 'w', encoding='utf-8') as f:
|
||
json.dump(record, f, indent=2, ensure_ascii=False)
|
||
|
||
print(f"📝 发布记录已保存: {record_file}")
|
||
|
||
def publish_from_markdown(self, markdown_file):
|
||
"""从Markdown文件发布"""
|
||
print(f"📄 从Markdown文件发布: {markdown_file}")
|
||
|
||
try:
|
||
# 读取Markdown文件
|
||
with open(markdown_file, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
|
||
# 解析文章信息
|
||
article_data = self._parse_markdown_article(content)
|
||
|
||
if not article_data:
|
||
print("❌ 无法解析文章")
|
||
return False
|
||
|
||
# 发布文章
|
||
return self.publish_article(article_data)
|
||
|
||
except Exception as e:
|
||
print(f"❌ 发布失败: {e}")
|
||
return False
|
||
|
||
def _parse_markdown_article(self, markdown_content):
|
||
"""从Markdown内容解析文章信息"""
|
||
lines = markdown_content.split('\n')
|
||
|
||
article_data = {
|
||
'title': '',
|
||
'content': '',
|
||
'source': '',
|
||
'url': '',
|
||
'tags': ['技术资讯', '科技新闻', '技术']
|
||
}
|
||
|
||
# 提取标题
|
||
for line in lines:
|
||
if line.startswith('# ') and not line.startswith('# 📋'):
|
||
article_data['title'] = line[2:].strip()
|
||
break
|
||
|
||
# 提取文章信息
|
||
in_info_section = False
|
||
for line in lines:
|
||
if line.startswith('## 📋 文章信息'):
|
||
in_info_section = True
|
||
continue
|
||
elif line.startswith('---') and in_info_section:
|
||
break
|
||
elif in_info_section:
|
||
if line.startswith('- **来源**:'):
|
||
article_data['source'] = line.replace('- **来源**:', '').strip()
|
||
elif line.startswith('- **原文链接**:'):
|
||
article_data['url'] = line.replace('- **原文链接**:', '').strip()
|
||
|
||
# 提取中文内容
|
||
in_chinese_content = False
|
||
chinese_lines = []
|
||
for line in lines:
|
||
if '【中文翻译】' in line or '中文翻译' in line:
|
||
in_chinese_content = True
|
||
continue
|
||
elif '---' in line and in_chinese_content:
|
||
break
|
||
elif in_chinese_content and line.strip():
|
||
chinese_lines.append(line.strip())
|
||
|
||
article_data['content'] = '\n'.join(chinese_lines)
|
||
|
||
# 根据内容添加标签
|
||
content_lower = article_data['content'].lower()
|
||
if 'ai' in content_lower or '人工智能' in content_lower:
|
||
article_data['tags'].append('AI')
|
||
if '数据库' in content_lower:
|
||
article_data['tags'].append('数据库')
|
||
if '云计算' in content_lower:
|
||
article_data['tags'].append('云计算')
|
||
if '安全' in content_lower:
|
||
article_data['tags'].append('网络安全')
|
||
|
||
return article_data
|
||
|
||
def main():
|
||
"""主函数"""
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser(description='小红书自动发布器')
|
||
subparsers = parser.add_subparsers(dest='command', help='命令')
|
||
|
||
# publish命令
|
||
publish_parser = subparsers.add_parser('publish', help='发布文章')
|
||
publish_parser.add_argument('--markdown', help='Markdown文件路径')
|
||
publish_parser.add_argument('--title', help='文章标题')
|
||
publish_parser.add_argument('--content', help='文章内容')
|
||
publish_parser.add_argument('--source', default='IT资讯', help='文章来源')
|
||
|
||
# check命令
|
||
subparsers.add_parser('check', help='检查系统状态')
|
||
|
||
# test命令
|
||
subparsers.add_parser('test', help='测试发布')
|
||
|
||
args = parser.parse_args()
|
||
|
||
publisher = XiaohongshuPublisher()
|
||
|
||
if args.command == 'check':
|
||
publisher.check_prerequisites()
|
||
|
||
elif args.command == 'test':
|
||
print("🧪 测试发布...")
|
||
test_article = {
|
||
'title': '测试文章 - 小红书自动化发布',
|
||
'content': '这是一个测试内容,用于验证小红书自动化发布功能。技术资讯自动化系统已成功集成小红书发布能力。',
|
||
'source': '测试',
|
||
'tags': ['测试', '自动化', '技术']
|
||
}
|
||
result = publisher.publish_article(test_article)
|
||
print(f"测试结果: {result}")
|
||
|
||
elif args.command == 'publish':
|
||
if args.markdown:
|
||
publisher.publish_from_markdown(args.markdown)
|
||
elif args.title and args.content:
|
||
article_data = {
|
||
'title': args.title,
|
||
'content': args.content,
|
||
'source': args.source,
|
||
'tags': ['技术资讯']
|
||
}
|
||
publisher.publish_article(article_data)
|
||
else:
|
||
print("❌ 需要提供--markdown或--title和--content参数")
|
||
|
||
else:
|
||
parser.print_help()
|
||
|
||
if __name__ == "__main__":
|
||
main() |