313 lines
11 KiB
Python
313 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
小红书工作流 - 从IT资讯自动化数据库获取数据源
|
|
|
|
与IT资讯自动化流程整合:
|
|
- IT资讯自动化:抓取 → 翻译/总结 → 保存到数据库 → 发邮件
|
|
- 小红书工作流:从同一数据库读取 → 挑选文章 → 发布小红书
|
|
"""
|
|
import os
|
|
import sys
|
|
import json
|
|
import sqlite3
|
|
import argparse
|
|
from datetime import datetime
|
|
from typing import Optional, Dict, List
|
|
|
|
# 导入发布模块
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
from publisher_adapter import XHSPublisherAdapter
|
|
|
|
|
|
class XHSFromITDBWorkflow:
|
|
"""
|
|
小红书工作流 - IT资讯数据库版
|
|
|
|
数据源: /home/ubuntu/nanocode/it_automation_enhanced/data/articles.db
|
|
"""
|
|
|
|
# IT资讯数据库路径
|
|
IT_DB_PATH = "/home/ubuntu/nanocode/it_automation_enhanced/data/articles.db"
|
|
IT_OUTPUT_DIR = "/home/ubuntu/nanocode/it_automation_enhanced/output"
|
|
|
|
def __init__(self, config_path: str = None):
|
|
self.config = self._load_config(config_path)
|
|
self.publisher = XHSPublisherAdapter()
|
|
|
|
# 路径
|
|
self.log_dir = self.config.get('paths', {}).get('log_dir', './logs')
|
|
self.output_dir = self.config.get('paths', {}).get('output_dir', './output')
|
|
os.makedirs(self.log_dir, exist_ok=True)
|
|
os.makedirs(self.output_dir, exist_ok=True)
|
|
|
|
def _load_config(self, config_path: str) -> dict:
|
|
"""加载配置"""
|
|
if not config_path:
|
|
config_path = os.path.join(
|
|
os.path.dirname(__file__), '..', 'config', 'config.json'
|
|
)
|
|
|
|
try:
|
|
with open(config_path, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
except Exception as e:
|
|
print(f"配置加载失败: {e},使用默认配置")
|
|
return {
|
|
'workflow': {'content_style': 'tech'},
|
|
'paths': {'log_dir': './logs', 'output_dir': './output'}
|
|
}
|
|
|
|
def _log(self, message: str):
|
|
"""记录日志"""
|
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
log_message = f"[{timestamp}] {message}"
|
|
print(log_message)
|
|
|
|
log_file = os.path.join(
|
|
self.log_dir,
|
|
f"xhs_from_itdb_{datetime.now().strftime('%Y%m%d')}.log"
|
|
)
|
|
with open(log_file, 'a', encoding='utf-8') as f:
|
|
f.write(log_message + '\n')
|
|
|
|
def get_processed_articles(self, limit: int = 10) -> List[Dict]:
|
|
"""从IT资讯数据库获取已处理但未发小红书的文章"""
|
|
articles = []
|
|
|
|
if not os.path.exists(self.IT_DB_PATH):
|
|
self._log(f"❌ IT资讯数据库不存在: {self.IT_DB_PATH}")
|
|
return articles
|
|
|
|
try:
|
|
conn = sqlite3.connect(self.IT_DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
cursor = conn.cursor()
|
|
|
|
# 查询已处理但未发小红书的文章
|
|
cursor.execute("""
|
|
SELECT id, title, url, source, category,
|
|
publish_date, word_count, created_at, xhs_status
|
|
FROM articles
|
|
WHERE processed = 1
|
|
AND (xhs_status = 0 OR xhs_status IS NULL)
|
|
ORDER BY created_at DESC
|
|
LIMIT ?
|
|
""", (limit,))
|
|
|
|
rows = cursor.fetchall()
|
|
for row in rows:
|
|
articles.append({
|
|
'id': row['id'],
|
|
'title': row['title'],
|
|
'url': row['url'],
|
|
'source': row['source'],
|
|
'category': row['category'],
|
|
'publish_date': row['publish_date'],
|
|
'word_count': row['word_count'],
|
|
'created_at': row['created_at'],
|
|
'xhs_status': row['xhs_status'] or 0
|
|
})
|
|
|
|
conn.close()
|
|
self._log(f"📊 找到 {len(articles)} 篇已处理但未发小红书的文章")
|
|
|
|
except Exception as e:
|
|
self._log(f"❌ 数据库查询失败: {e}")
|
|
|
|
return articles
|
|
|
|
def load_processed_content(self, article_id: int) -> Optional[Dict]:
|
|
"""加载IT资讯自动化的处理结果"""
|
|
processed_file = os.path.join(
|
|
self.IT_OUTPUT_DIR,
|
|
f"article_{article_id}_processed.json"
|
|
)
|
|
|
|
if not os.path.exists(processed_file):
|
|
self._log(f"⚠️ 处理结果文件不存在: {processed_file}")
|
|
return None
|
|
|
|
try:
|
|
with open(processed_file, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
# 构建小红书内容
|
|
content = {
|
|
'article_id': article_id,
|
|
'title': data.get('translated_title', data.get('original_title', '')),
|
|
'body': data.get('summary', data.get('translated_content', ''))[:900],
|
|
'tags': self._generate_tags(data),
|
|
'source': data.get('source', '技术'),
|
|
'url': data.get('url', ''),
|
|
'original_title': data.get('original_title', ''),
|
|
}
|
|
|
|
return content
|
|
|
|
except Exception as e:
|
|
self._log(f"❌ 加载处理结果失败: {e}")
|
|
return None
|
|
|
|
def _generate_tags(self, data: Dict) -> List[str]:
|
|
"""根据文章数据生成标签"""
|
|
source = data.get('source', '技术')
|
|
category = data.get('category', '科技')
|
|
|
|
# 基础标签
|
|
base_tags = ['科技资讯', '技术分享']
|
|
|
|
# 根据来源添加标签
|
|
source_tags = {
|
|
'InfoQ': 'InfoQ',
|
|
'Wired': 'Wired',
|
|
'TechCrunch': 'TechCrunch',
|
|
}
|
|
|
|
if source in source_tags:
|
|
base_tags.append(source_tags[source])
|
|
|
|
# 根据分类添加标签
|
|
if category and category not in base_tags:
|
|
base_tags.append(category)
|
|
|
|
# 确保5个标签
|
|
while len(base_tags) < 5:
|
|
base_tags.append('程序员')
|
|
|
|
return base_tags[:5]
|
|
|
|
def mark_as_published(self, article_id: int) -> bool:
|
|
"""标记文章为已发布到小红书"""
|
|
try:
|
|
conn = sqlite3.connect(self.IT_DB_PATH)
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute("""
|
|
UPDATE articles
|
|
SET xhs_status = 1, updated_at = datetime('now')
|
|
WHERE id = ?
|
|
""", (article_id,))
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
self._log(f"✅ 文章 {article_id} 已标记为已发布")
|
|
return True
|
|
|
|
except Exception as e:
|
|
self._log(f"❌ 标记发布状态失败: {e}")
|
|
return False
|
|
|
|
def run(self, article_id: int = None, dry_run: bool = False) -> bool:
|
|
"""
|
|
执行小红书发布工作流
|
|
|
|
Args:
|
|
article_id: 指定文章ID(可选,不指定则自动选择)
|
|
dry_run: 试运行模式
|
|
|
|
Returns:
|
|
是否成功
|
|
"""
|
|
self._log("="*60)
|
|
self._log("🚀 小红书工作流启动 (IT资讯数据源)")
|
|
self._log("="*60)
|
|
|
|
# 步骤1: 获取文章
|
|
if article_id:
|
|
# 指定ID
|
|
self._log(f"\n📄 步骤1: 使用指定文章 ID={article_id}")
|
|
articles = self.get_processed_articles(limit=50)
|
|
article = next((a for a in articles if a['id'] == article_id), None)
|
|
|
|
if not article:
|
|
# 尝试从数据库直接查询
|
|
self._log(f"⚠️ 文章 {article_id} 不在未发布列表中,尝试直接加载...")
|
|
content = self.load_processed_content(article_id)
|
|
if content:
|
|
content['article_id'] = article_id
|
|
else:
|
|
self._log(f"❌ 无法加载文章 {article_id}")
|
|
return False
|
|
else:
|
|
content = self.load_processed_content(article_id)
|
|
else:
|
|
# 自动选择
|
|
self._log("\n📄 步骤1: 从IT资讯数据库获取文章...")
|
|
articles = self.get_processed_articles(limit=5)
|
|
|
|
if not articles:
|
|
self._log("❌ 没有可发布的文章(IT资讯可能尚未运行或文章都已发布)")
|
|
self._log("💡 提示: IT资讯自动化每天16:00运行,请等待或手动运行")
|
|
return False
|
|
|
|
# 选择第一篇
|
|
article = articles[0]
|
|
self._log(f"✅ 选中文章: {article['title'][:50]}...")
|
|
self._log(f" 来源: {article['source']} | 字数: {article['word_count']}")
|
|
|
|
content = self.load_processed_content(article['id'])
|
|
|
|
if not content:
|
|
self._log("❌ 无法加载文章内容")
|
|
return False
|
|
|
|
# 步骤2: 显示内容预览
|
|
self._log("\n📝 步骤2: 内容预览")
|
|
self._log(f"标题: {content['title'][:60]}...")
|
|
self._log(f"正文: {len(content['body'])} 字符")
|
|
self._log(f"标签: {', '.join(content['tags'])}")
|
|
|
|
# 保存到本地
|
|
output_file = os.path.join(
|
|
self.output_dir,
|
|
f"xhs_content_{content['article_id']}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
)
|
|
with open(output_file, 'w', encoding='utf-8') as f:
|
|
json.dump(content, f, ensure_ascii=False, indent=2)
|
|
self._log(f"\n💾 内容已保存: {output_file}")
|
|
|
|
# 步骤3: 发布
|
|
if dry_run:
|
|
self._log("\n🧪 试运行模式,跳过发布")
|
|
self._log("="*60)
|
|
return True
|
|
|
|
self._log("\n📱 步骤3: 发布到小红书...")
|
|
success = self.publisher.publish(
|
|
title=content['title'],
|
|
content=content['body'],
|
|
tags=content['tags'],
|
|
style=self.config.get('workflow', {}).get('content_style', 'tech')
|
|
)
|
|
|
|
if success:
|
|
self._log("🎉 发布成功!")
|
|
# 标记为已发布
|
|
self.mark_as_published(content['article_id'])
|
|
else:
|
|
self._log("❌ 发布失败")
|
|
|
|
self._log("="*60)
|
|
return success
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='小红书工作流 - IT资讯数据源')
|
|
parser.add_argument('--article-id', type=int, help='指定文章ID')
|
|
parser.add_argument('--dry-run', action='store_true', help='试运行不发布')
|
|
parser.add_argument('--config', help='配置文件路径')
|
|
args = parser.parse_args()
|
|
|
|
workflow = XHSFromITDBWorkflow(config_path=args.config)
|
|
success = workflow.run(
|
|
article_id=args.article_id,
|
|
dry_run=args.dry_run
|
|
)
|
|
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|