- 新增操作历史记录,防止对同一笔记重复评论、点赞、收藏和回复
- 新增每日操作统计与限额管理,包含评论、点赞、收藏、发布和回复的独立上限
- 新增错误冷却机制,连续错误后自动暂停操作一段时间
- 新增运营时段控制,允许设置每日自动运营的开始和结束时间
- 新增收藏功能,支持一键收藏和定时自动收藏
- 新增随机人设池,提供25种预设小红书博主风格人设,支持随机切换
- 扩充主题池、风格池和评论关键词池,增加运营多样性
- 优化自动化调度器,显示下次执行时间和实时统计摘要
- 优化发布功能,增加本地备份机制,失败时保留文案和图片
🐛 fix(llm): 修复绘图提示词中的人物特征要求
- 在绘图提示词模板中明确要求人物必须是东亚面孔的中国人
- 添加具体的人物特征描述,如黑发、深棕色眼睛、精致五官等
- 禁止出现西方人或欧美人特征
- 调整整体画面风格偏向东方审美、清新淡雅和小红书风格
249 lines
9.9 KiB
Python
249 lines
9.9 KiB
Python
"""
|
||
LLM 服务模块
|
||
封装对 OpenAI 兼容 API 的调用,包含文案生成、热点分析、评论回复等 Prompt
|
||
"""
|
||
import requests
|
||
import json
|
||
import re
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# ================= Prompt 模板 =================
|
||
|
||
PROMPT_COPYWRITING = """
|
||
你是一个小红书爆款内容专家。请根据用户主题生成内容。
|
||
|
||
【标题规则】(严格执行):
|
||
1. 长度限制:必须控制在 18 字以内(含Emoji),绝对不能超过 20 字!
|
||
2. 格式要求:Emoji + 爆点关键词 + 核心痛点。
|
||
3. 禁忌:禁止使用"第一"、"最"、"顶级"等绝对化广告法违禁词。
|
||
4. 风格:二极管标题(震惊/后悔/必看/避雷/哭了),具有强烈的点击欲望。
|
||
|
||
【正文规则】:
|
||
1. 口语化,多用Emoji,分段清晰,不堆砌长句。
|
||
2. 正文控制在 600 字以内(小红书限制 1000 字)。
|
||
3. 结尾必须有 5 个以上相关话题标签(#)。
|
||
|
||
【绘图 Prompt】:
|
||
生成对应的 Stable Diffusion 英文提示词,适配 JuggernautXL 模型,强调:
|
||
- 人物要求(最重要!):如果画面中有人物,必须是东亚面孔的中国人,使用 asian girl/boy, chinese, east asian features, black hair, dark brown eyes, delicate facial features, fair skin, slim figure 等描述,绝对禁止出现西方人/欧美人特征
|
||
- 质量词:masterpiece, best quality, ultra detailed, 8k uhd, high resolution
|
||
- 光影:natural lighting, soft shadows, studio lighting, golden hour 等(根据场景选择)
|
||
- 风格:photorealistic, cinematic, editorial photography, ins style, chinese social media aesthetic
|
||
- 构图:dynamic angle, depth of field, bokeh 等
|
||
- 细节:detailed skin texture, sharp focus, vivid colors
|
||
- 审美偏向:整体画面风格偏向东方审美、清新淡雅、小红书风格
|
||
注意:不要使用括号权重语法,直接用英文逗号分隔描述。
|
||
|
||
返回 JSON 格式:
|
||
{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}
|
||
"""
|
||
|
||
PROMPT_HOTSPOT_ANALYSIS = """
|
||
你是一个小红书运营数据分析专家。下面是搜索到的热门笔记信息:
|
||
|
||
{feed_data}
|
||
|
||
请分析这些热门笔记,总结以下内容:
|
||
1. **热门选题方向**:提炼 3-5 个最火的细分选题
|
||
2. **标题套路**:总结高赞标题的共同特征和写作模板
|
||
3. **内容结构**:分析爆款笔记的内容组织方式
|
||
4. **推荐模仿方案**:基于分析结果,给出 3 个具体的模仿选题建议
|
||
|
||
返回 JSON 格式:
|
||
{{"hot_topics": ["...", "..."], "title_patterns": ["...", "..."], "content_structure": "...", "suggestions": [{{"topic": "...", "reason": "..."}}]}}
|
||
"""
|
||
|
||
PROMPT_COMMENT_REPLY = """
|
||
你是一个小红书博主,人设为:{persona}。
|
||
有人在你的笔记下评论了,请你用符合人设的口吻回复。
|
||
|
||
【规则】:
|
||
1. 回复简洁,控制在 50 字以内
|
||
2. 语气亲切自然,像和朋友聊天
|
||
3. 适当加入 1-2 个 Emoji
|
||
4. 如果是质疑,礼貌回应;如果是夸奖,真诚感谢
|
||
|
||
笔记标题:{post_title}
|
||
用户评论:{comment}
|
||
|
||
直接返回回复内容,不需要 JSON 格式。
|
||
"""
|
||
|
||
PROMPT_PROACTIVE_COMMENT = """
|
||
你是一个小红书活跃用户,人设为:{persona}。
|
||
你正在浏览一篇笔记,想要留下一条真诚、有价值的评论,以提升互动和曝光。
|
||
|
||
【笔记信息】:
|
||
标题:{post_title}
|
||
正文摘要:{post_content}
|
||
|
||
【已有评论参考(可能为空)】:
|
||
{existing_comments}
|
||
|
||
【评论规则】:
|
||
1. 评论简洁自然,控制在 30-80 字,不要像机器人
|
||
2. 体现你对笔记内容的真实感受或个人经验
|
||
3. 可以提问、分享类似经历、或表达共鸣
|
||
4. 适当加入 1-2 个 Emoji,不要过多
|
||
5. 不要重复已有评论的观点,找新角度
|
||
6. 不要生硬带货或自我推广
|
||
7. 语气因内容而异:教程类→请教/补充;种草类→分享体验;生活类→表达共鸣
|
||
|
||
直接返回评论内容,不需要 JSON 格式。
|
||
"""
|
||
|
||
PROMPT_COPY_WITH_REFERENCE = """
|
||
你是一个小红书爆款内容专家。参考以下热门笔记的风格和结构,创作全新原创内容。
|
||
|
||
【参考笔记】:
|
||
{reference_notes}
|
||
|
||
【创作主题】:{topic}
|
||
【风格要求】:{style}
|
||
|
||
【标题规则】:
|
||
1. 长度限制:必须控制在 18 字以内(含Emoji),绝对不能超过 20 字!
|
||
2. 借鉴参考笔记的标题套路但内容必须原创。
|
||
|
||
【正文规则】:
|
||
1. 口语化,多用Emoji,分段清晰。
|
||
2. 正文控制在 600 字以内。
|
||
3. 结尾有 5 个以上话题标签(#)。
|
||
|
||
【绘图 Prompt】:
|
||
生成 Stable Diffusion 英文提示词,适配 JuggernautXL 模型:
|
||
- 人物要求(最重要!):如果画面中有人物,必须是东亚面孔的中国人,使用 asian girl/boy, chinese, east asian features, black hair, dark brown eyes, delicate facial features, fair skin, slim figure 等描述,绝对禁止出现西方人/欧美人特征
|
||
- 必含质量词:masterpiece, best quality, ultra detailed, 8k uhd
|
||
- 风格:photorealistic, cinematic, editorial photography, chinese social media aesthetic
|
||
- 光影和细节:natural lighting, sharp focus, vivid colors, detailed skin texture
|
||
- 审美偏向:整体画面风格偏向东方审美、清新淡雅、小红书风格
|
||
- 用英文逗号分隔,不用括号权重语法。
|
||
|
||
返回 JSON 格式:
|
||
{{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}}
|
||
"""
|
||
|
||
|
||
class LLMService:
|
||
"""LLM API 服务封装"""
|
||
|
||
def __init__(self, api_key: str, base_url: str, model: str = "gpt-3.5-turbo"):
|
||
self.api_key = api_key
|
||
self.base_url = base_url.rstrip("/")
|
||
self.model = model
|
||
|
||
def _chat(self, system_prompt: str, user_message: str,
|
||
json_mode: bool = True, temperature: float = 0.8) -> str:
|
||
"""底层聊天接口"""
|
||
headers = {
|
||
"Authorization": f"Bearer {self.api_key}",
|
||
"Content-Type": "application/json",
|
||
}
|
||
if json_mode:
|
||
user_message = user_message + "\n请以json格式返回。"
|
||
|
||
payload = {
|
||
"model": self.model,
|
||
"messages": [
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": user_message},
|
||
],
|
||
"temperature": temperature,
|
||
}
|
||
if json_mode:
|
||
payload["response_format"] = {"type": "json_object"}
|
||
|
||
try:
|
||
resp = requests.post(
|
||
f"{self.base_url}/chat/completions",
|
||
headers=headers, json=payload, timeout=90
|
||
)
|
||
resp.raise_for_status()
|
||
content = resp.json()["choices"][0]["message"]["content"]
|
||
return content
|
||
except requests.exceptions.Timeout:
|
||
raise TimeoutError("LLM 请求超时,请检查网络或换一个模型")
|
||
except requests.exceptions.HTTPError as e:
|
||
raise ConnectionError(f"LLM API 错误 ({resp.status_code}): {resp.text[:200]}")
|
||
except Exception as e:
|
||
raise RuntimeError(f"LLM 调用异常: {e}")
|
||
|
||
def _parse_json(self, text: str) -> dict:
|
||
"""从 LLM 返回文本中解析 JSON"""
|
||
cleaned = re.sub(r"```json\s*|```", "", text).strip()
|
||
return json.loads(cleaned)
|
||
|
||
# ---------- 业务方法 ----------
|
||
|
||
def get_models(self) -> list[str]:
|
||
"""获取可用模型列表"""
|
||
url = f"{self.base_url}/models"
|
||
headers = {"Authorization": f"Bearer {self.api_key}"}
|
||
try:
|
||
resp = requests.get(url, headers=headers, timeout=10)
|
||
resp.raise_for_status()
|
||
text = resp.text.strip()
|
||
if not text:
|
||
logger.warning("GET %s 返回空响应", url)
|
||
return []
|
||
data = resp.json()
|
||
return [item["id"] for item in data.get("data", [])]
|
||
except Exception as e:
|
||
logger.warning("获取模型列表失败 (%s): %s", url, e)
|
||
return []
|
||
|
||
def generate_copy(self, topic: str, style: str) -> dict:
|
||
"""生成小红书文案"""
|
||
content = self._chat(
|
||
PROMPT_COPYWRITING,
|
||
f"主题:{topic}\n风格:{style}"
|
||
)
|
||
data = self._parse_json(content)
|
||
|
||
# 强制标题长度限制
|
||
title = data.get("title", "")
|
||
if len(title) > 20:
|
||
title = title[:20]
|
||
data["title"] = title
|
||
|
||
return data
|
||
|
||
def generate_copy_with_reference(self, topic: str, style: str,
|
||
reference_notes: str) -> dict:
|
||
"""参考热门笔记生成文案"""
|
||
prompt = PROMPT_COPY_WITH_REFERENCE.format(
|
||
reference_notes=reference_notes, topic=topic, style=style
|
||
)
|
||
content = self._chat(prompt, f"请创作关于「{topic}」的小红书笔记")
|
||
data = self._parse_json(content)
|
||
|
||
title = data.get("title", "")
|
||
if len(title) > 20:
|
||
data["title"] = title[:20]
|
||
return data
|
||
|
||
def analyze_hotspots(self, feed_data: str) -> dict:
|
||
"""分析热门内容趋势"""
|
||
prompt = PROMPT_HOTSPOT_ANALYSIS.format(feed_data=feed_data)
|
||
content = self._chat(prompt, "请分析以上热门笔记数据")
|
||
return self._parse_json(content)
|
||
|
||
def generate_reply(self, persona: str, post_title: str, comment: str) -> str:
|
||
"""AI 生成评论回复"""
|
||
prompt = PROMPT_COMMENT_REPLY.format(
|
||
persona=persona, post_title=post_title, comment=comment
|
||
)
|
||
return self._chat(prompt, "请生成回复", json_mode=False, temperature=0.9).strip()
|
||
|
||
def generate_proactive_comment(self, persona: str, post_title: str,
|
||
post_content: str, existing_comments: str = "") -> str:
|
||
"""AI 生成主动评论"""
|
||
prompt = PROMPT_PROACTIVE_COMMENT.format(
|
||
persona=persona, post_title=post_title,
|
||
post_content=post_content,
|
||
existing_comments=existing_comments or "暂无评论",
|
||
)
|
||
return self._chat(prompt, "请生成评论", json_mode=False, temperature=0.9).strip()
|