This commit is contained in:
Ubuntu
2026-07-03 16:58:28 +08:00
commit 32b43f85bd
6240 changed files with 1987678 additions and 0 deletions
@@ -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("发布适配器初始化成功")
+35
View File
@@ -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}")
+22
View File
@@ -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
+189
View File
@@ -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()