- 新增多 LLM 提供商管理功能,支持添加、删除和切换不同 API 提供商 - 新增账号数据看板,支持可视化展示用户核心指标和笔记点赞排行 - 新增自动获取并保存 xsec_token 功能,提升登录体验 - 新增退出登录功能,支持重新扫码登录 - 新增用户 ID 验证和保存功能,确保账号信息准确性 ♻️ refactor(config): 重构配置管理和 LLM 服务调用 - 重构配置管理器,支持多 LLM 提供商配置和兼容旧配置自动迁移 - 重构 LLM 服务调用逻辑,统一从配置管理器获取激活的提供商信息 - 重构 MCP 客户端,增加单例模式和自动重试机制,提升连接稳定性 - 重构数据看板页面,优化用户数据获取和可视化展示逻辑 🐛 fix(mcp): 修复 MCP 连接和登录状态检查问题 - 修复 MCP 客户端初始化问题,避免重复握手 - 修复登录状态检查逻辑,自动获取并保存 xsec_token - 修复获取我的笔记列表功能,支持通过用户 ID 准确获取 - 修复 JSON-RPC 通知格式问题,确保与 MCP 服务兼容 📝 docs(config): 更新配置文件和代码注释 - 更新配置文件结构,新增多 LLM 提供商配置字段 - 更新代码注释,明确各功能模块的作用和调用方式 - 更新用户界面提示信息,提供更清晰的操作指引
235 lines
8.4 KiB
Python
235 lines
8.4 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 英文提示词,强调:masterpiece, best quality, 8k, soft lighting, ins style。
|
||
|
||
返回 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 英文提示词。
|
||
|
||
返回 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()
|