a
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
小红书热点趋势自动化包
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "nanobot"
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
AI内容生成模块
|
||||
负责基于话题创作原创小红书内容
|
||||
"""
|
||||
|
||||
import os
|
||||
import requests
|
||||
import re
|
||||
from typing import Dict, Optional
|
||||
|
||||
class ContentGenerator:
|
||||
"""AI内容生成器"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.environ.get('MOONSHOT_API_KEY')
|
||||
self.api_base = os.environ.get('MOONSHOT_API_BASE', 'https://api.moonshot.cn/v1')
|
||||
self.model = "kimi-k2.5"
|
||||
|
||||
def generate(self, topic: str, style: str = "tech") -> Dict[str, str]:
|
||||
"""
|
||||
基于话题生成原创内容
|
||||
|
||||
Args:
|
||||
topic: 话题关键词
|
||||
style: 内容风格 (tech/warm/business)
|
||||
|
||||
Returns:
|
||||
包含title, body, tags的字典
|
||||
"""
|
||||
# 根据风格选择系统提示
|
||||
style_prompts = {
|
||||
"tech": "你是科技内容创作专家,擅长分析技术趋势。",
|
||||
"warm": "你是生活方式博主,擅长温暖有趣的科技分享。",
|
||||
"business": "你是商业分析师,擅长解读科技商业逻辑。"
|
||||
}
|
||||
|
||||
system_prompt = style_prompts.get(style, style_prompts["tech"])
|
||||
|
||||
# 构建创作提示
|
||||
user_prompt = f"""基于"{topic}"这个话题,创作一篇全新的小红书科技笔记。
|
||||
|
||||
创作要求:
|
||||
1. 标题吸引人,20-30字,不用任何markdown符号
|
||||
2. 正文600-800字,结构清晰
|
||||
3. 使用-作为列表符号,不要用**加粗
|
||||
4. 自然融入emoji
|
||||
5. 包含独到的技术趋势洞察
|
||||
6. 纯原创,不要复制任何现有文章
|
||||
7. 结尾带5个话题标签,格式为#标签
|
||||
|
||||
输出格式:
|
||||
标题:[你的标题]
|
||||
|
||||
正文:[你的正文]
|
||||
|
||||
标签:[标签1] [标签2] [标签3] [标签4] [标签5]"""
|
||||
|
||||
# 调用API
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.api_base}/chat/completions",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json={
|
||||
"model": self.model,
|
||||
"messages": [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_prompt}
|
||||
],
|
||||
"temperature": 1,
|
||||
"max_tokens": 1500
|
||||
},
|
||||
timeout=60
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
|
||||
# 检查API响应是否包含错误
|
||||
if 'error' in result:
|
||||
print(f"API错误: {result['error']}")
|
||||
return self._fallback_content(topic)
|
||||
|
||||
# 检查choices是否存在
|
||||
if 'choices' not in result or not result['choices']:
|
||||
print(f"API响应异常: {result}")
|
||||
return self._fallback_content(topic)
|
||||
|
||||
content = result['choices'][0]['message']['content']
|
||||
|
||||
# 解析内容
|
||||
return self._parse_content(content)
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
print("API请求超时")
|
||||
return self._fallback_content(topic)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"API请求失败: {e}")
|
||||
return self._fallback_content(topic)
|
||||
except (KeyError, IndexError) as e:
|
||||
print(f"API响应解析失败: {e}")
|
||||
return self._fallback_content(topic)
|
||||
except Exception as e:
|
||||
print(f"内容生成失败: {e}")
|
||||
return self._fallback_content(topic)
|
||||
|
||||
def _parse_content(self, content: str) -> Dict[str, str]:
|
||||
"""解析AI生成的内容"""
|
||||
# 提取标题
|
||||
title_match = re.search(r'标题:[\s]*(.+?)(?:\n|$)', content)
|
||||
title = title_match.group(1).strip() if title_match else "科技趋势分析"
|
||||
|
||||
# 提取正文(标题和标签之间的内容)
|
||||
body_match = re.search(r'正文:[\s]*(.+?)(?:标签:|$)', content, re.DOTALL)
|
||||
body = body_match.group(1).strip() if body_match else content
|
||||
|
||||
# 清理正文中的markdown
|
||||
body = re.sub(r'\*\*(.+?)\*\*', r'\1', body) # 去除加粗
|
||||
body = re.sub(r'##?\s*', '', body) # 去除标题标记
|
||||
|
||||
# 提取标签
|
||||
tags_match = re.search(r'标签:[\s]*(.+?)(?:\n|$)', content)
|
||||
tags_str = tags_match.group(1) if tags_match else "#科技趋势 #人工智能"
|
||||
tags = [t.strip().replace('#', '') for t in tags_str.split() if t.strip()]
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"tags": tags[:5], # 最多5个标签
|
||||
"raw": content
|
||||
}
|
||||
|
||||
def _fallback_content(self, topic: str) -> Dict[str, str]:
|
||||
"""备用内容(API失败时使用)"""
|
||||
return {
|
||||
"title": f"深度解析:{topic}的前沿趋势",
|
||||
"body": f"今天来聊聊{topic}的最新发展...",
|
||||
"tags": ["科技趋势", "人工智能", "AI产业"],
|
||||
"raw": ""
|
||||
}
|
||||
|
||||
def humanize(self, content: Dict[str, str]) -> Dict[str, str]:
|
||||
"""
|
||||
对内容进行去AI化处理
|
||||
|
||||
Args:
|
||||
content: 原始内容
|
||||
|
||||
Returns:
|
||||
去AI化后的内容
|
||||
"""
|
||||
try:
|
||||
from shared_utils import get_anti_ai_humanizer
|
||||
humanizer = get_anti_ai_humanizer()
|
||||
|
||||
if humanizer:
|
||||
content['body'] = humanizer(content['body'], mode='xiaohongshu')
|
||||
|
||||
except Exception as e:
|
||||
print(f"去AI化处理失败: {e}")
|
||||
|
||||
return content
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试
|
||||
generator = ContentGenerator()
|
||||
result = generator.generate("AI芯片竞争", style="tech")
|
||||
|
||||
print("生成结果:")
|
||||
print(f"标题: {result['title']}")
|
||||
print(f"\n正文预览: {result['body'][:150]}...")
|
||||
print(f"\n标签: {result['tags']}")
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
发布适配器模块
|
||||
封装IT资讯项目的共享发布功能
|
||||
"""
|
||||
|
||||
from shared_utils import get_xiaohongshu_publisher
|
||||
from typing import List, Optional
|
||||
|
||||
class XHSPublisherAdapter:
|
||||
"""小红书发布适配器"""
|
||||
|
||||
def __init__(self):
|
||||
self.PublisherClass = get_xiaohongshu_publisher()
|
||||
self.publisher = None
|
||||
|
||||
def publish(self,
|
||||
title: str,
|
||||
content: str,
|
||||
tags: List[str],
|
||||
style: str = "tech") -> bool:
|
||||
"""
|
||||
发布笔记到小红书
|
||||
|
||||
Args:
|
||||
title: 笔记标题
|
||||
content: 笔记正文
|
||||
tags: 话题标签列表
|
||||
style: 图片风格 (tech/warm/business)
|
||||
|
||||
Returns:
|
||||
是否发布成功
|
||||
"""
|
||||
try:
|
||||
# 初始化发布器
|
||||
if not self.publisher:
|
||||
self.publisher = self.PublisherClass()
|
||||
|
||||
# 调用共享发布模块
|
||||
success = self.publisher.publish_to_xiaohongshu(
|
||||
title=title,
|
||||
content=content,
|
||||
tags=tags,
|
||||
style=style
|
||||
)
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
print(f"发布失败: {e}")
|
||||
return False
|
||||
|
||||
def get_last_note_id(self) -> Optional[str]:
|
||||
"""获取最新发布的笔记ID"""
|
||||
try:
|
||||
# 从日志或返回信息中获取
|
||||
# 这里简化处理
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试
|
||||
adapter = XHSPublisherAdapter()
|
||||
print("发布适配器初始化成功")
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# 小红书自动化调度器 - IT资讯数据源版
|
||||
#
|
||||
# 与IT资讯自动化流程整合:
|
||||
# - IT资讯自动化:每天16:00抓取 → 翻译 → 发邮件
|
||||
# - 小红书自动化:每天10:00从IT数据库读取 → 发布小红书
|
||||
#
|
||||
# 两个流程密不可分,共享同一数据源
|
||||
|
||||
# 设置PATH环境变量(确保能找到xhs-cli等工具)
|
||||
export PATH="/home/ubuntu/.local/bin:$PATH"
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORK_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
LOG_FILE="$WORK_DIR/logs/scheduler_$(date +%Y%m%d).log"
|
||||
|
||||
mkdir -p "$WORK_DIR/logs"
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] =======================================" >> "$LOG_FILE"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 启动小红书工作流 (IT资讯数据源)" >> "$LOG_FILE"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] =======================================" >> "$LOG_FILE"
|
||||
|
||||
# 运行工作流 - 从IT资讯数据库获取文章并发布
|
||||
cd "$WORK_DIR"
|
||||
python3 scripts/workflow_from_it_db.py >> "$LOG_FILE" 2>&1
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✅ 今日任务完成" >> "$LOG_FILE"
|
||||
else
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ❌ 任务失败" >> "$LOG_FILE"
|
||||
fi
|
||||
|
||||
echo "" >> "$LOG_FILE"
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
共享工具模块
|
||||
负责导入IT资讯自动化项目的公共模块
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 将IT资讯项目路径加入Python路径
|
||||
IT_AUTO_PATH = "/home/ubuntu/nanocode/it_automation_enhanced/scripts"
|
||||
if IT_AUTO_PATH not in sys.path:
|
||||
sys.path.insert(0, IT_AUTO_PATH)
|
||||
|
||||
def get_xiaohongshu_publisher():
|
||||
"""
|
||||
获取小红书发布器(共享模块)
|
||||
"""
|
||||
try:
|
||||
from xiaohongshu_multi_image import XiaohongshuMultiImagePublisher
|
||||
return XiaohongshuMultiImagePublisher
|
||||
except ImportError as e:
|
||||
raise ImportError(f"无法加载共享发布模块: {e}")
|
||||
|
||||
def get_anti_ai_humanizer():
|
||||
"""
|
||||
获取去AI化处理器(共享模块)
|
||||
"""
|
||||
try:
|
||||
sys.path.insert(0, "/home/ubuntu/.nanobot/workspace/skills/anti-ai-detector")
|
||||
from humanize import humanize_text
|
||||
return humanize_text
|
||||
except ImportError:
|
||||
return None
|
||||
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
技术博客文章抓取器
|
||||
从指定技术博客源抓取最近3天内的短文章
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Optional
|
||||
from urllib.parse import urljoin, urlparse
|
||||
import feedparser
|
||||
|
||||
# 可用技术博客源(2026-03-30 检测)
|
||||
TECH_SOURCES = [
|
||||
{"name": "Coding Horror", "url": "https://blog.codinghorror.com/", "type": "rss"},
|
||||
{"name": "Joel on Software", "url": "https://www.joelonsoftware.com/", "type": "rss"},
|
||||
{"name": "TechCrunch", "url": "https://techcrunch.com/", "type": "rss"},
|
||||
{"name": "Stratechery", "url": "https://stratechery.com/", "type": "rss"},
|
||||
{"name": "Wired", "url": "https://wired.com/", "type": "rss"},
|
||||
{"name": "Dan Luu", "url": "https://danluu.com/", "type": "rss"},
|
||||
{"name": "Simon Willison", "url": "https://simonwillison.net/", "type": "rss"},
|
||||
{"name": "Martin Fowler", "url": "https://martinfowler.com/", "type": "rss"},
|
||||
{"name": "WorryDream", "url": "https://worrydream.com/", "type": "html"},
|
||||
{"name": "Microsoft DevBlogs", "url": "https://devblogs.microsoft.com/", "type": "rss"},
|
||||
{"name": "Flavio Copes", "url": "https://flaviocopes.com/", "type": "rss"},
|
||||
{"name": "Gwern", "url": "https://gwern.net/", "type": "rss"},
|
||||
{"name": "GitHub Blog", "url": "https://github.blog/", "type": "rss"},
|
||||
{"name": "InfoQ中文", "url": "https://www.infoq.cn/", "type": "api"},
|
||||
{"name": "NVIDIA Blog", "url": "https://developer.nvidia.com/blog/", "type": "rss"},
|
||||
]
|
||||
|
||||
|
||||
class TechBlogFetcher:
|
||||
"""技术博客文章抓取器"""
|
||||
|
||||
def __init__(self):
|
||||
self.headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
||||
}
|
||||
self.max_days = 7 # 放宽到7天内
|
||||
self.max_chars = 5000 # 放宽字数限制到5000
|
||||
self.min_chars = 300 # 最小字数
|
||||
|
||||
def get_random_sources(self, count: int = 3) -> List[Dict]:
|
||||
"""随机选择n个数据源"""
|
||||
return random.sample(TECH_SOURCES, k=min(count, len(TECH_SOURCES)))
|
||||
|
||||
def fetch_from_rss(self, source: Dict) -> List[Dict]:
|
||||
"""从RSS源抓取文章"""
|
||||
articles = []
|
||||
try:
|
||||
# 已知可用的RSS feed(2026-03-30验证)
|
||||
known_feeds = {
|
||||
"Coding Horror": "https://blog.codinghorror.com/rss/",
|
||||
"Joel on Software": "https://www.joelonsoftware.com/feed/",
|
||||
"TechCrunch": "https://techcrunch.com/feed/",
|
||||
"Wired": "https://www.wired.com/feed/",
|
||||
"Dan Luu": "https://danluu.com/atom.xml",
|
||||
"Simon Willison": "https://simonwillison.net/atom/everything/",
|
||||
"Martin Fowler": "https://martinfowler.com/feed.atom",
|
||||
"Gwern": "https://gwern.net/atom.xml",
|
||||
"Flavio Copes": "https://flaviocopes.com/rss.xml",
|
||||
"GitHub Blog": "https://github.blog/feed/",
|
||||
"Microsoft DevBlogs": "https://devblogs.microsoft.com/feed",
|
||||
"NVIDIA Blog": "https://developer.nvidia.com/blog/feed/",
|
||||
}
|
||||
|
||||
# 获取RSS URL
|
||||
rss_url = known_feeds.get(source["name"])
|
||||
if not rss_url:
|
||||
# 尝试通用路径
|
||||
rss_url = urljoin(source["url"], "/feed")
|
||||
|
||||
print(f" 尝试RSS: {rss_url}")
|
||||
feed = feedparser.parse(rss_url)
|
||||
|
||||
if not feed.entries:
|
||||
print(f" ⚠️ RSS无条目")
|
||||
return []
|
||||
|
||||
print(f" ✅ 获取到 {len(feed.entries)} 篇RSS条目")
|
||||
|
||||
cutoff_date = datetime.now() - timedelta(days=self.max_days)
|
||||
|
||||
for entry in feed.entries[:10]: # 最近10篇
|
||||
# 解析日期
|
||||
pub_date = None
|
||||
for date_field in ['published_parsed', 'updated_parsed', 'created_parsed']:
|
||||
if hasattr(entry, date_field) and getattr(entry, date_field):
|
||||
pub_date = datetime(*getattr(entry, date_field)[:6])
|
||||
break
|
||||
|
||||
if not pub_date or pub_date < cutoff_date:
|
||||
continue
|
||||
|
||||
article = {
|
||||
"title": entry.get("title", ""),
|
||||
"url": entry.get("link", ""),
|
||||
"source": source["name"],
|
||||
"published": pub_date.isoformat(),
|
||||
"summary": entry.get("summary", entry.get("description", ""))[:500],
|
||||
}
|
||||
articles.append(article)
|
||||
|
||||
except Exception as e:
|
||||
print(f"RSS抓取失败 {source['name']}: {e}")
|
||||
|
||||
return articles
|
||||
|
||||
def fetch_from_infoq_cn(self) -> List[Dict]:
|
||||
"""从InfoQ中文抓取"""
|
||||
articles = []
|
||||
try:
|
||||
url = "https://www.infoq.cn/public/v1/my/recommends"
|
||||
headers = {
|
||||
**self.headers,
|
||||
"Accept": "application/json",
|
||||
"Referer": "https://www.infoq.cn/",
|
||||
}
|
||||
resp = requests.post(url, headers=headers, json={"size": 20}, timeout=15)
|
||||
data = resp.json()
|
||||
|
||||
if data.get("code") == 0:
|
||||
cutoff_date = datetime.now() - timedelta(days=self.max_days)
|
||||
|
||||
for item in data.get("data", []):
|
||||
pub_time = item.get("publish_time", "")
|
||||
if pub_time:
|
||||
try:
|
||||
pub_date = datetime.fromtimestamp(pub_time / 1000)
|
||||
if pub_date < cutoff_date:
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
|
||||
articles.append({
|
||||
"title": item.get("article_title", ""),
|
||||
"url": f"https://www.infoq.cn/article/{item.get('uuid', '')}",
|
||||
"source": "InfoQ中文",
|
||||
"published": pub_time,
|
||||
"summary": item.get("article_summary", "")[:500],
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"InfoQ抓取失败: {e}")
|
||||
|
||||
return articles
|
||||
|
||||
def fetch_full_content(self, url: str) -> str:
|
||||
"""抓取文章完整内容"""
|
||||
try:
|
||||
resp = requests.get(url, headers=self.headers, timeout=15)
|
||||
|
||||
# 使用readability或简单提取
|
||||
from bs4 import BeautifulSoup
|
||||
soup = BeautifulSoup(resp.text, 'html.parser')
|
||||
|
||||
# 移除脚本和样式
|
||||
for tag in soup(["script", "style", "nav", "header", "footer"]):
|
||||
tag.decompose()
|
||||
|
||||
# 提取正文
|
||||
article = soup.find('article') or soup.find('main') or soup.find('div', class_=re.compile('content|post|entry'))
|
||||
if article:
|
||||
text = article.get_text(separator='\n', strip=True)
|
||||
else:
|
||||
text = soup.get_text(separator='\n', strip=True)
|
||||
|
||||
# 清理
|
||||
lines = [line.strip() for line in text.split('\n') if line.strip()]
|
||||
text = '\n'.join(lines)
|
||||
|
||||
return text
|
||||
except Exception as e:
|
||||
print(f"抓取全文失败 {url}: {e}")
|
||||
return ""
|
||||
|
||||
def filter_short_articles(self, articles: List[Dict]) -> List[Dict]:
|
||||
"""筛选短文章(< 5000字符)并获取完整内容"""
|
||||
result = []
|
||||
|
||||
for article in articles:
|
||||
# 优先使用RSS摘要
|
||||
content = article.get("summary", "")
|
||||
|
||||
# 如果摘要太短,尝试抓取全文
|
||||
if len(content) < self.min_chars:
|
||||
content = self.fetch_full_content(article["url"])
|
||||
|
||||
if content and len(content) < self.max_chars and len(content) > self.min_chars:
|
||||
article["content"] = content
|
||||
result.append(article)
|
||||
if len(result) >= 3: # 最多3篇
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def fetch_daily_articles(self) -> Optional[Dict]:
|
||||
"""
|
||||
每日抓取主流程
|
||||
返回一篇适合发布的文章
|
||||
"""
|
||||
print("🔍 开始抓取技术博客...")
|
||||
|
||||
# 随机选择3个源
|
||||
sources = self.get_random_sources(3)
|
||||
all_articles = []
|
||||
|
||||
for source in sources:
|
||||
print(f" 📡 检查 {source['name']}...")
|
||||
|
||||
if source["name"] == "InfoQ中文":
|
||||
articles = self.fetch_from_infoq_cn()
|
||||
elif source["type"] == "rss":
|
||||
articles = self.fetch_from_rss(source)
|
||||
else:
|
||||
continue
|
||||
|
||||
all_articles.extend(articles)
|
||||
print(f" 找到 {len(articles)} 篇候选")
|
||||
|
||||
if not all_articles:
|
||||
print("❌ 没有找到候选文章")
|
||||
return None
|
||||
|
||||
print(f"\n📄 共 {len(all_articles)} 篇候选,筛选短文章...")
|
||||
|
||||
# 筛选短文章
|
||||
short_articles = self.filter_short_articles(all_articles)
|
||||
|
||||
if not short_articles:
|
||||
print("❌ 没有符合字数要求的文章")
|
||||
return None
|
||||
|
||||
# 随机选择一篇
|
||||
selected = random.choice(short_articles)
|
||||
print(f"\n✅ 选中文章: {selected['title'][:50]}...")
|
||||
print(f" 来源: {selected['source']}")
|
||||
print(f" 字数: {len(selected['content'])}")
|
||||
|
||||
return selected
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fetcher = TechBlogFetcher()
|
||||
article = fetcher.fetch_daily_articles()
|
||||
|
||||
if article:
|
||||
print("\n" + "="*50)
|
||||
print(f"标题: {article['title']}")
|
||||
print(f"URL: {article['url']}")
|
||||
print(f"内容预览:\n{article['content'][:300]}...")
|
||||
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
技术文章改写器
|
||||
将英文技术文章改写成小红书风格的中文内容
|
||||
"""
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class TechContentRewriter:
|
||||
"""技术内容改写器"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.environ.get('MOONSHOT_API_KEY')
|
||||
self.api_base = os.environ.get('MOONSHOT_API_BASE', 'https://api.moonshot.cn/v1')
|
||||
self.model = "kimi-k2.5"
|
||||
|
||||
def rewrite(self, article: Dict) -> Dict[str, str]:
|
||||
"""
|
||||
改写文章为小红书风格
|
||||
|
||||
Args:
|
||||
article: 包含title, url, content的文章字典
|
||||
|
||||
Returns:
|
||||
改写后的内容字典
|
||||
"""
|
||||
if not self.api_key:
|
||||
print("❌ 未设置MOONSHOT_API_KEY")
|
||||
return self._fallback(article)
|
||||
|
||||
# 构建提示
|
||||
prompt = self._build_prompt(article)
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.api_base}/chat/completions",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
json={
|
||||
"model": self.model,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "你是科技内容创作专家,擅长将技术文章改写成适合小红书平台的中文内容。"
|
||||
},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": 1,
|
||||
"max_tokens": 2000
|
||||
},
|
||||
timeout=120
|
||||
)
|
||||
|
||||
result = response.json()
|
||||
|
||||
if 'error' in result:
|
||||
print(f"API错误: {result['error']}")
|
||||
return self._fallback(article)
|
||||
|
||||
content = result['choices'][0]['message']['content']
|
||||
return self._parse_result(content, article)
|
||||
|
||||
except Exception as e:
|
||||
print(f"改写失败: {e}")
|
||||
return self._fallback(article)
|
||||
|
||||
def _build_prompt(self, article: Dict) -> str:
|
||||
"""构建改写提示"""
|
||||
return f"""请将以下技术文章改写成适合小红书平台的中文笔记。
|
||||
|
||||
原文信息:
|
||||
- 标题: {article['title']}
|
||||
- 来源: {article['source']}
|
||||
- 原文链接: {article['url']}
|
||||
- 原文内容:
|
||||
{article['content'][:1500]} # 限制长度避免超出token
|
||||
|
||||
改写要求:
|
||||
1. 标题:20-30字,吸引眼球,不用markdown
|
||||
2. 正文:600-800字,用中文表达,保持技术准确性
|
||||
3. 结构:使用-作为列表符号,分3-4个要点阐述
|
||||
4. 风格:口语化、有个人观点,像朋友分享
|
||||
5. 格式:不要加粗(**),标题用自然语言
|
||||
6. Emoji:适当使用,增加亲和力
|
||||
7. 原创性:基于原文观点重新组织语言,不要直接翻译
|
||||
8. 结尾:5个相关话题标签,格式为#标签
|
||||
|
||||
输出格式:
|
||||
标题:[中文标题]
|
||||
|
||||
正文:[中文正文]
|
||||
|
||||
标签:#标签1 #标签2 #标签3 #标签4 #标签5"""
|
||||
|
||||
def _parse_result(self, content: str, article: Dict) -> Dict[str, str]:
|
||||
"""解析AI返回的内容"""
|
||||
# 提取标题
|
||||
title_match = re.search(r'标题:\s*(.+?)(?:\n|$)', content)
|
||||
title = title_match.group(1).strip() if title_match else article['title']
|
||||
|
||||
# 提取正文
|
||||
body_match = re.search(r'正文:\s*(.+?)(?:标签:|$)', content, re.DOTALL)
|
||||
body = body_match.group(1).strip() if body_match else content
|
||||
|
||||
# 清理markdown
|
||||
body = re.sub(r'\*\*(.+?)\*\*', r'\1', body)
|
||||
body = re.sub(r'##?\s*', '', body)
|
||||
|
||||
# 提取标签
|
||||
tags_match = re.search(r'标签:\s*(.+?)(?:\n|$)', content, re.DOTALL)
|
||||
tags_str = tags_match.group(1) if tags_match else "#科技趋势 #人工智能"
|
||||
tags = [t.strip().replace('#', '') for t in tags_str.split() if t.strip() and '#' in t]
|
||||
|
||||
return {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"tags": tags[:5],
|
||||
"source_title": article['title'],
|
||||
"source_url": article['url'],
|
||||
"source_name": article['source'],
|
||||
"raw": content
|
||||
}
|
||||
|
||||
def _fallback(self, article: Dict) -> Dict[str, str]:
|
||||
"""备用内容"""
|
||||
return {
|
||||
"title": f"【技术分享】{article['title'][:30]}",
|
||||
"body": f"今天读到一篇来自{article['source']}的好文章,分享给大家\n\n{article['content'][:500]}...",
|
||||
"tags": ["技术分享", "编程", "开发者"],
|
||||
"source_title": article['title'],
|
||||
"source_url": article['url'],
|
||||
"source_name": article['source'],
|
||||
"raw": ""
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试
|
||||
test_article = {
|
||||
"title": "The Future of AI Agents",
|
||||
"source": "Test Blog",
|
||||
"url": "https://example.com/article",
|
||||
"content": "AI agents are becoming more capable. They can now perform complex tasks autonomously. This represents a major shift in how we interact with technology."
|
||||
}
|
||||
|
||||
rewriter = TechContentRewriter()
|
||||
result = rewriter.rewrite(test_article)
|
||||
|
||||
print(f"标题: {result['title']}")
|
||||
print(f"\n正文:\n{result['body']}")
|
||||
print(f"\n标签: {result['tags']}")
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
热点发现模块
|
||||
负责从小红书搜索热门科技话题
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
class TopicDiscovery:
|
||||
"""热点话题发现器"""
|
||||
|
||||
def __init__(self):
|
||||
self.keyword = "AI科技"
|
||||
|
||||
def search_notes(self, keyword: str = None, limit: int = 5) -> List[Dict]:
|
||||
"""
|
||||
搜索小红书笔记
|
||||
|
||||
Args:
|
||||
keyword: 搜索关键词
|
||||
limit: 返回数量限制
|
||||
|
||||
Returns:
|
||||
笔记列表,每个包含id, title, likes, desc等信息
|
||||
"""
|
||||
keyword = keyword or self.keyword
|
||||
|
||||
try:
|
||||
# 执行xhs搜索命令
|
||||
result = subprocess.run(
|
||||
['xhs', 'search', keyword],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"搜索失败: {result.stderr}")
|
||||
return []
|
||||
|
||||
# 解析YAML输出
|
||||
return self._parse_search_result(result.stdout, limit)
|
||||
|
||||
except Exception as e:
|
||||
print(f"搜索异常: {e}")
|
||||
return []
|
||||
|
||||
def _parse_search_result(self, output: str, limit: int) -> List[Dict]:
|
||||
"""解析搜索结果"""
|
||||
notes = []
|
||||
|
||||
try:
|
||||
# 简单解析:提取id和title
|
||||
# 实际应该解析YAML,这里简化处理
|
||||
import yaml
|
||||
data = yaml.safe_load(output)
|
||||
|
||||
if data and 'data' in data and 'items' in data['data']:
|
||||
for item in data['data']['items'][:limit]:
|
||||
if 'note_card' in item:
|
||||
card = item['note_card']
|
||||
note = {
|
||||
'id': item.get('id', ''),
|
||||
'title': card.get('display_title', ''),
|
||||
'desc': card.get('desc', '')[:200],
|
||||
'likes': card.get('interact_info', {}).get('liked_count', '0'),
|
||||
'author': card.get('user', {}).get('nickname', ''),
|
||||
'images': len(card.get('image_list', []))
|
||||
}
|
||||
notes.append(note)
|
||||
|
||||
except Exception as e:
|
||||
print(f"解析结果异常: {e}")
|
||||
|
||||
return notes
|
||||
|
||||
def read_note_detail(self, note_id: str) -> Optional[Dict]:
|
||||
"""
|
||||
读取笔记详情
|
||||
|
||||
Args:
|
||||
note_id: 笔记ID
|
||||
|
||||
Returns:
|
||||
笔记详细信息
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['xhs', 'read', note_id],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
|
||||
# 解析详情
|
||||
import yaml
|
||||
data = yaml.safe_load(result.stdout)
|
||||
|
||||
if data and 'data' in data and 'items' in data['data']:
|
||||
item = data['data']['items'][0]
|
||||
card = item.get('note_card', {})
|
||||
return {
|
||||
'id': note_id,
|
||||
'title': card.get('display_title', ''),
|
||||
'desc': card.get('desc', ''),
|
||||
'likes': card.get('interact_info', {}).get('liked_count', '0'),
|
||||
'comments': card.get('interact_info', {}).get('comment_count', '0'),
|
||||
'tags': [t.get('name', '') for t in card.get('tag_list', [])]
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"读取详情异常: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def extract_topic(self, note: Dict) -> str:
|
||||
"""
|
||||
从笔记中提取话题关键词
|
||||
|
||||
Args:
|
||||
note: 笔记信息
|
||||
|
||||
Returns:
|
||||
话题关键词
|
||||
"""
|
||||
title = note.get('title', '')
|
||||
desc = note.get('desc', '')
|
||||
|
||||
# 简单的关键词提取逻辑
|
||||
# 实际可以使用NLP或关键词提取算法
|
||||
keywords = []
|
||||
|
||||
# 从标题提取
|
||||
if 'AI' in title or '人工智能' in title:
|
||||
keywords.append("AI产业趋势")
|
||||
if '芯片' in title:
|
||||
keywords.append("芯片技术")
|
||||
if '机器人' in title:
|
||||
keywords.append("机器人技术")
|
||||
|
||||
# 如果没有匹配到,使用默认
|
||||
if not keywords:
|
||||
keywords.append("科技趋势分析")
|
||||
|
||||
return keywords[0] if keywords else "AI科技"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试
|
||||
discovery = TopicDiscovery()
|
||||
notes = discovery.search_notes("AI科技", limit=3)
|
||||
|
||||
print(f"找到 {len(notes)} 篇热门笔记:")
|
||||
for note in notes:
|
||||
print(f"\n📱 {note['title'][:40]}...")
|
||||
print(f" 👍 {note['likes']} 📷 {note['images']}张")
|
||||
topic = discovery.extract_topic(note)
|
||||
print(f" 🏷️ 话题: {topic}")
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# 查看运行日志
|
||||
|
||||
WORK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}" && pwd)/.." && pwd)"
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
# 默认查看今天的日志
|
||||
LOG_FILE="$WORK_DIR/logs/scheduler_$(date +%Y%m%d).log"
|
||||
else
|
||||
# 查看指定日期的日志
|
||||
LOG_FILE="$WORK_DIR/logs/scheduler_$1.log"
|
||||
fi
|
||||
|
||||
if [ -f "$LOG_FILE" ]; then
|
||||
echo "📋 日志内容 ($LOG_FILE):"
|
||||
echo "==========================="
|
||||
tail -50 "$LOG_FILE"
|
||||
else
|
||||
echo "❌ 日志文件不存在: $LOG_FILE"
|
||||
echo "可用日志文件:"
|
||||
ls -la "$WORK_DIR/logs/" 2>/dev/null || echo "logs目录为空"
|
||||
fi
|
||||
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
小红书热点趋势工作流主模块
|
||||
整合热点发现、AI创作、发布全流程
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
# 导入子模块
|
||||
from topic_discovery import TopicDiscovery
|
||||
from content_generator import ContentGenerator
|
||||
from publisher_adapter import XHSPublisherAdapter
|
||||
|
||||
class XHSTrendingWorkflow:
|
||||
"""
|
||||
小红书热点趋势工作流
|
||||
|
||||
完整流程:
|
||||
1. 搜索热门话题
|
||||
2. AI创作原创内容
|
||||
3. 去AI化处理
|
||||
4. 发布到小红书
|
||||
"""
|
||||
|
||||
def __init__(self, config_path: str = None):
|
||||
self.config = self._load_config(config_path)
|
||||
|
||||
# 初始化各模块
|
||||
self.discovery = TopicDiscovery()
|
||||
self.generator = ContentGenerator()
|
||||
self.publisher = XHSPublisherAdapter()
|
||||
|
||||
# 日志
|
||||
self.log_dir = self.config.get('paths', {}).get('log_dir', './logs')
|
||||
os.makedirs(self.log_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': {
|
||||
'search_keyword': 'AI科技',
|
||||
'search_limit': 5,
|
||||
'content_style': 'tech'
|
||||
}
|
||||
}
|
||||
|
||||
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"workflow_{datetime.now().strftime('%Y%m%d')}.log"
|
||||
)
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(log_message + '\n')
|
||||
|
||||
def run(self,
|
||||
keyword: str = None,
|
||||
topic: str = None,
|
||||
style: str = None,
|
||||
dry_run: bool = False) -> bool:
|
||||
"""
|
||||
执行完整工作流
|
||||
|
||||
Args:
|
||||
keyword: 搜索关键词(自动发现模式)
|
||||
topic: 直接指定话题(直接创作模式)
|
||||
style: 内容风格
|
||||
dry_run: 仅生成不发布
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
self._log("🚀 工作流启动")
|
||||
|
||||
# 模式1:自动发现热点
|
||||
if keyword and not topic:
|
||||
self._log(f"🔍 搜索热门话题: {keyword}")
|
||||
notes = self.discovery.search_notes(keyword, limit=5)
|
||||
|
||||
if not notes:
|
||||
self._log("❌ 未找到热门笔记")
|
||||
return False
|
||||
|
||||
# 选择第一篇
|
||||
selected_note = notes[0]
|
||||
self._log(f"✅ 选中笔记: {selected_note['title'][:40]}...")
|
||||
|
||||
# 提取话题
|
||||
topic = self.discovery.extract_topic(selected_note)
|
||||
self._log(f"🏷️ 提取话题: {topic}")
|
||||
|
||||
# 模式2:直接创作
|
||||
if not topic:
|
||||
topic = self.config.get('workflow', {}).get('search_keyword', 'AI科技')
|
||||
self._log(f"📝 使用默认话题: {topic}")
|
||||
|
||||
# AI创作内容
|
||||
self._log("🤖 AI创作原创内容...")
|
||||
style = style or self.config.get('workflow', {}).get('content_style', 'tech')
|
||||
content = self.generator.generate(topic, style=style)
|
||||
|
||||
self._log(f"✅ 内容生成完成")
|
||||
self._log(f" 标题: {content['title']}")
|
||||
self._log(f" 正文长度: {len(content['body'])} 字符")
|
||||
|
||||
# 去AI化处理
|
||||
self._log("🎭 去AI化处理...")
|
||||
content = self.generator.humanize(content)
|
||||
|
||||
# 保存到文件
|
||||
output_file = os.path.join(
|
||||
self.config.get('paths', {}).get('output_dir', './output'),
|
||||
f"content_{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"💾 内容已保存: {output_file}")
|
||||
|
||||
# 发布
|
||||
if dry_run:
|
||||
self._log("🧪 试运行模式,跳过发布")
|
||||
return True
|
||||
|
||||
self._log("📱 发布到小红书...")
|
||||
success = self.publisher.publish(
|
||||
title=content['title'],
|
||||
content=content['body'],
|
||||
tags=content['tags'],
|
||||
style=style
|
||||
)
|
||||
|
||||
if success:
|
||||
self._log("🎉 发布成功!")
|
||||
else:
|
||||
self._log("❌ 发布失败")
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def main():
|
||||
"""命令行入口"""
|
||||
parser = argparse.ArgumentParser(description='小红书热点趋势工作流')
|
||||
parser.add_argument('--keyword', '-k', help='搜索关键词(自动发现模式)')
|
||||
parser.add_argument('--topic', '-t', help='直接指定话题(直接创作模式)')
|
||||
parser.add_argument('--style', '-s', default='tech',
|
||||
choices=['tech', 'warm', 'business'],
|
||||
help='内容风格')
|
||||
parser.add_argument('--dry-run', '-d', action='store_true',
|
||||
help='仅生成不发布')
|
||||
parser.add_argument('--config', '-c', help='配置文件路径')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 创建工作流
|
||||
workflow = XHSTrendingWorkflow(args.config)
|
||||
|
||||
# 执行
|
||||
success = workflow.run(
|
||||
keyword=args.keyword,
|
||||
topic=args.topic,
|
||||
style=args.style,
|
||||
dry_run=args.dry_run
|
||||
)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,312 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
小红书技术博客工作流
|
||||
从国际技术博客抓取文章,改写后发布
|
||||
|
||||
数据来源:/home/ubuntu/.nanobot/workspace/skills/tech-blog-sources/SKILL.md
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict
|
||||
|
||||
# 导入模块
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
from tech_blog_fetcher import TechBlogFetcher
|
||||
from tech_content_rewrite import TechContentRewriter
|
||||
from publisher_adapter import XHSPublisherAdapter
|
||||
|
||||
|
||||
class TechBlogWorkflow:
|
||||
"""
|
||||
技术博客工作流
|
||||
|
||||
完整流程:
|
||||
1. 从15个可用技术博客源随机选择
|
||||
2. 抓取最近3天内、字数<2000的文章
|
||||
3. 使用大模型改写成小红书风格
|
||||
4. 发布到小红书
|
||||
"""
|
||||
|
||||
def __init__(self, config_path: str = None):
|
||||
self.config = self._load_config(config_path)
|
||||
|
||||
# 初始化各模块
|
||||
self.fetcher = TechBlogFetcher()
|
||||
self.rewriter = TechContentRewriter()
|
||||
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"tech_workflow_{datetime.now().strftime('%Y%m%d')}.log"
|
||||
)
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(log_message + '\n')
|
||||
|
||||
def run(self, dry_run: bool = False) -> bool:
|
||||
"""
|
||||
执行技术博客工作流
|
||||
|
||||
Args:
|
||||
dry_run: 仅生成不发布
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
self._log("="*50)
|
||||
self._log("🚀 技术博客工作流启动")
|
||||
self._log("="*50)
|
||||
|
||||
# 步骤1: 抓取文章
|
||||
self._log("\n📡 步骤1: 从技术博客源抓取文章...")
|
||||
article = self.fetcher.fetch_daily_articles()
|
||||
|
||||
if not article:
|
||||
self._log("❌ 未能获取到合适的文章")
|
||||
return False
|
||||
|
||||
self._log(f"✅ 获取文章: {article['title'][:50]}...")
|
||||
self._log(f" 来源: {article['source']}")
|
||||
self._log(f" 原文链接: {article['url']}")
|
||||
self._log(f" 字数: {len(article['content'])}")
|
||||
|
||||
# 步骤2: 改写内容
|
||||
self._log("\n🤖 步骤2: 使用大模型改写成小红书风格...")
|
||||
content = self.rewriter.rewrite(article)
|
||||
|
||||
self._log(f"✅ 改写完成")
|
||||
self._log(f" 新标题: {content['title']}")
|
||||
self._log(f" 正文字数: {len(content['body'])}")
|
||||
self._log(f" 标签: {', '.join(content['tags'])}")
|
||||
|
||||
# 步骤3: 保存内容
|
||||
output_file = os.path.join(
|
||||
self.output_dir,
|
||||
f"tech_content_{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}")
|
||||
|
||||
# 步骤4: 发布
|
||||
if dry_run:
|
||||
self._log("\n🧪 试运行模式,跳过发布")
|
||||
self._log("="*50)
|
||||
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("🎉 发布成功!")
|
||||
else:
|
||||
self._log("❌ 发布失败")
|
||||
|
||||
self._log("="*50)
|
||||
return success
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='小红书技术博客工作流')
|
||||
parser.add_argument('--dry-run', action='store_true', help='试运行不发布')
|
||||
parser.add_argument('--config', help='配置文件路径')
|
||||
args = parser.parse_args()
|
||||
|
||||
workflow = TechBlogWorkflow(config_path=args.config)
|
||||
success = workflow.run(dry_run=args.dry_run)
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user