xhs_factory/llm_service.py
zhoujie 88dfc09e2a feat(config): 新增多 LLM 提供商支持与账号数据看板
- 新增多 LLM 提供商管理功能,支持添加、删除和切换不同 API 提供商
- 新增账号数据看板,支持可视化展示用户核心指标和笔记点赞排行
- 新增自动获取并保存 xsec_token 功能,提升登录体验
- 新增退出登录功能,支持重新扫码登录
- 新增用户 ID 验证和保存功能,确保账号信息准确性

♻️ refactor(config): 重构配置管理和 LLM 服务调用

- 重构配置管理器,支持多 LLM 提供商配置和兼容旧配置自动迁移
- 重构 LLM 服务调用逻辑,统一从配置管理器获取激活的提供商信息
- 重构 MCP 客户端,增加单例模式和自动重试机制,提升连接稳定性
- 重构数据看板页面,优化用户数据获取和可视化展示逻辑

🐛 fix(mcp): 修复 MCP 连接和登录状态检查问题

- 修复 MCP 客户端初始化问题,避免重复握手
- 修复登录状态检查逻辑,自动获取并保存 xsec_token
- 修复获取我的笔记列表功能,支持通过用户 ID 准确获取
- 修复 JSON-RPC 通知格式问题,确保与 MCP 服务兼容

📝 docs(config): 更新配置文件和代码注释

- 更新配置文件结构,新增多 LLM 提供商配置字段
- 更新代码注释,明确各功能模块的作用和调用方式
- 更新用户界面提示信息,提供更清晰的操作指引
2026-02-08 21:52:29 +08:00

235 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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()