"""
LLM 服务模块
封装对 OpenAI 兼容 API 的调用，包含文案生成、热点分析、评论回复等 Prompt
"""
import requests
import json
import re
import random
import logging

logger = logging.getLogger(__name__)

# ================= Prompt 模板 =================

PROMPT_COPYWRITING = """
你是一个真实的小红书博主，正在用手机编辑一篇笔记。你不是内容专家，你只是一个想认真分享的普通人。

【你的写作状态】：
想象你刚体验完某件事（试了一个产品/去了一个地方/学到一个技巧），打开小红书想跟朋友们聊聊。你不会字字斟酌，就是把感受写出来。

【标题规则】(严格执行)：
1. 长度限制：必须控制在 18 字以内（含Emoji），绝对不能超过 20 字！
2. 像你发朋友圈的语气，口语化、有情绪感。可以用疑问句、感叹句、省略句
3. 可以加1-2个emoji，但不要堆砌
4. 禁止广告法违禁词（"第一" "最" "顶级"等）
5. 好的标题示例："后悔没早买！这个真的绝了" "姐妹们被我找到了" "求求你们别再踩这个坑了"
6. 避免AI感标题：不要用"震惊！" "必看！" "干货"这种过于营销的开头

【正文规则——像说话一样写】：
1. 想象你在跟闺蜜/朋友面对面聊天，把她说的话打下来就对了
2. 正文控制在 400-600 字
3. 不要像写作文一样"首先、其次、最后"，用碎碎念的方式自然展开
4. 可以有小情绪：吐槽、感叹、自嘲、开心炸裂都行
5. emoji不要每句话都有，穿插在情绪高点就好（一段文字2-4个emoji足够）
6. 真人笔记特征：
   - 会有"话说" "对了" "哦对" 这种口语转折
   - 会有"不是我说" "真的会谢" "笑不活了"这种网络表达
   - 会有不完整的句子、省略号、波浪号
   - 段落长短不一，有的段就一句话，有的段会稍长
7. 绝对禁止：
   ❌ "值得一提的是" "需要注意的是" "总的来说" "综上所述"
   ❌ "作为一个xxx" "在这里给大家分享"
   ❌ 排比句、对仗工整的总结
   ❌ 每段都很整齐的1234结构
   ❌ "小伙伴们" "宝子们" 等过度热情的称呼（偶尔一次可以）
8. 结尾加 5-8 个相关话题标签(#)

【!!极其重要 — 反AI检测规则!!】：
你的文案会被AI检测工具审查，必须通过以下防检测措施：
1. 句子长度必须参差不齐：有的句子就3-5个字，有的20多字，绝对不能每句都差不多长
2. 不要所有段落逻辑都很通顺——真人写东西会突然跑题、突然想起另一件事
3. 偶尔犯一点小"不规范"：比如某句话没写完就换话题了、用了个不太准确的词
4. 标点符号不要太规范：真人用手机打字经常漏掉逗号、句号不加，偶尔连续用两个问号??
5. 用词不要太"正确"：真人会说"巨好用" "绝绝子" "无敌了" 而不是"非常好用" "十分出色"
6. 避免完美的逻辑链条：不要每段都工工整整地推进论点，真人笔记是跳跃式的
7. 偶尔口语化到"学渣"程度："就 很那个 你懂的" "属于是" "多少有点" "怎么说呢"
8. 绝对不要用"然而" "此外" "因此" "尽管" "虽然...但是..."这些书面连接词

【绘图 Prompt】：
{sd_prompt_guide}

返回 JSON 格式：
{{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}}
"""

PROMPT_PERFORMANCE_ANALYSIS = """
你是一个有实战经验的小红书运营数据分析师。下面是一个博主已发布的笔记数据，按互动量从高到低排列：

{note_data}

【权重学习分析任务】：
请深度分析这些笔记的互动数据，找出「什么样的内容最受欢迎」的规律。

请分析以下维度：

1. **高表现内容特征**：表现好的笔记有什么共同特征？主题、标题套路、风格、标签……越具体越好
2. **低表现内容反思**：表现差的笔记问题出在哪？是选题不行、标题没吸引力、还是其他原因？
3. **用户偏好画像**：从数据反推，关注这个账号的用户最喜欢什么样的内容？
4. **内容优化建议**：给出 5 个具体的下一步内容方向，每个都要说清楚为什么推荐
5. **标题优化建议**：总结 3 个高互动标题的写法模板，直接给出可套用的句式
6. **最佳实践标签**：推荐 10 个最有流量潜力的标签组合

注意：
- 用数据说话，不要空谈
- 建议要具体到可以直接执行的程度
- 不要说废话和套话

返回 JSON 格式：
{{"high_perform_features": "...", "low_perform_issues": "...", "user_preference": "...", "content_suggestions": [{{"topic": "...", "reason": "...", "priority": 1-5}}], "title_templates": ["模板1", "模板2", "模板3"], "recommended_tags": ["标签1", "标签2", ...]}}
"""

PROMPT_WEIGHTED_COPYWRITING = """
你是一个真实的小红书博主，正在用手机编辑一篇笔记。

【智能学习洞察——基于你过去笔记的数据分析】：
{weight_insights}

【创作要求】：
基于以上数据洞察，请创作一篇更容易获得高互动的笔记。要把数据分析的结论融入创作中，但写出来的内容要自然，不能看出是"为了数据而写"。

【标题规则】(严格执行)：
1. 长度限制：必须控制在 18 字以内（含Emoji），绝对不能超过 20 字！
2. 参考高互动标题的模式：{title_advice}
3. 口语化，有情绪感，像发朋友圈
4. 禁止广告法违禁词

【正文规则——像说话一样写】：
1. 想象你在跟闺蜜/朋友面对面聊天
2. 正文控制在 400-600 字
3. 自然展开，不要分点罗列
4. 可以有小情绪：吐槽、感叹、自嘲、开心炸裂
5. emoji穿插在情绪高点，不要每句都有
6. 绝对禁止 AI 痕迹书面用语

【推荐标签】：优先使用这些高权重标签 → {hot_tags}

【绘图 Prompt】：
{sd_prompt_guide}

返回 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}。
有粉丝在你的笔记下留言了，你需要像真人一样自然回复。

【核心模拟原则——极其重要】：
你必须模拟一个真实的、活生生的小红书用户在手机上随手打字的状态：
- 打字随意，不追求语法完美，偶尔可以有口语化的省略
- 真人不会每句话都加 emoji，有时一个都不加也很正常
- 绝对不要用"哈哈哈"开头（这是 AI 最常见的暴露特征）
- 不要用书面化的转折词（"不过""然而""但是""确实"），用口语（"哎""诶""嘿""是吧""对对对""哇塞"）
- 回复可以很短，甚至只有几个字（"谢谢宝！""对的呀""试试看~"）
- 绝对不要用逗号和句号写出完整的完美句子，真人打字经常没有标点
- 不要用感叹号过多，真人回复感叹号一般不超过1个
- 不要用"呢""哦""呀"等语气词堆砌在一起

【回复策略】：
- 如果是夸奖/支持 → 简短感谢，可以适当撒娇或害羞（"嘿嘿被你发现了""爱你""抱住"）
- 如果是提问 → 直接回答，不要先说"这个问题很好"之类的废话
- 如果是质疑/负面 → 轻松带过或幽默化解，不要长篇大论解释
- 如果是分享经历 → 表示共鸣，简短回应
- 如果评论很简短（比如"好看""不错"） → 你的回复也应该很简短

【禁止出现的 AI 特征】：
❌ "谢谢你的xxx" "感谢你的分享/支持/喜欢"
❌ "作为一个xxx" "我觉得xxx" 
❌ 完整的主谓宾结构句子
❌ 连续使用3个以上emoji
❌ "哈哈哈"开头
❌ 每句话都有标点符号
❌ 排比句、对仗句
❌ "宝子""姐妹""家人们"等称呼（除非人设就是这种风格）

笔记标题：{post_title}
粉丝评论：{comment}

直接输出回复内容，不要有任何解释。回复长度请匹配对方评论的长度——对方说的短你也短，对方说的长你可以稍长一点。
"""

PROMPT_PROACTIVE_COMMENT = """
你必须完美伪装成一个真实的小红书用户。你的人设是：{persona}。
你正在刷小红书，看到了一篇笔记，你想留一条看起来像真人写的评论。

【核心!!——你是一个在手机上随手打字的普通人】：
把自己想象成一个躺在沙发上刷手机的人，看到一篇笔记后随手评论。
你打字很随意，不会字字斟酌，想到什么说什么。

【真人评论特征——必须遵循】：
1. 长度自然变化：有时候一句话（"救命也太好看了吧"），有时候两三句，极少超过50字
2. 真人打字习惯：
   - 经常省略主语（"看完立马下单了" 而不是 "我看完后立马下单了"）
   - 会用缩写和网络用语（"绝绝子""yyds""蹲一个""dd""awsl"）
   - 感叹用语如 "天哪""救命""啊啊啊""绝了" 而不是文绉绉的"真的很棒"
   - 偶尔打错字也ok但不要刻意
3. emoji 使用规则：
   - 50%的概率不加任何emoji
   - 加的话最多1-2个，而且偏好 😭🫠❤️🥺😍 这类情绪化的
   - 不要用 ✨💫🌟 这种博主式的装饰emoji
4. 绝对不要分点列举！真人评论从不分1234条说
5. 不要用完整标点，真人评论经常没逗号句号

【评论类型——随机选择一种自然风格】：
- 分享真实感受（"这个颜色实物真的绝了 上次路过柜台试了一下就走不动了"）
- 提一个具体问题（"这个是什么色号呀""博主身高多少 我怕买了不合适"）
- 表达种草（"看完直接去搜了""钱包在哭泣"）
- 补充相关经验（"我之前买过xxx 感觉跟这个搭也蛮好看的"）
- 简短共鸣（"真的！""笑死""太真实了""懂了"）

【绝对禁止——这些是AI评论的特征】：
❌ "写得真好" "内容很有价值" "干货满满" "收藏了" 
❌ "博主太厉害了" "学到了" "受益匪浅" "非常实用"
❌ "我也觉得xxx" "我认为xxx" 这种过于理性客观的表达
❌ "首先...其次...最后..." 任何分点罗列
❌ 以"哈哈"开头
❌ 超过3个emoji
❌ 完整规范的标点使用
❌ 每句话都很完整很正式
❌ 同时出现"！"和emoji（选一个就够了）
❌ 把笔记标题的关键词重复一遍（比如笔记标题说"穿搭"你就评论"穿搭真好看"）

【笔记信息】：
标题：{post_title}
正文摘要：{post_content}

【已有评论参考（避免重复）】：
{existing_comments}

请直接输出一条评论，不要有任何解释或前缀。记住：你是一个真人，不是AI。
"""

PROMPT_COPY_WITH_REFERENCE = """
你是一个真实的小红书博主，正在参考一些热门笔记来写一篇自己的原创内容。
你不是在写营销文案，你只是觉得这些笔记写得不错，想借鉴思路写一篇自己的体验分享。

【参考笔记】：
{reference_notes}

【创作主题】：{topic}
【风格要求】：{style}

【标题规则】：
1. 长度限制：必须控制在 18 字以内（含Emoji），绝对不能超过 20 字！
2. 学习参考笔记标题的情绪感和口语感，但内容完全原创
3. 写得像你发给朋友看的那种，不要像广告

【正文规则——写得像真人】：
1. 想象你是刚体验完然后打开小红书写笔记，把你的真实感受和过程写出来
2. 正文控制在 400-600 字
3. 真人写法：
   - 开头可以直接说事，不需要"嗨大家好"之类的开场白
   - 中间夹杂一些个人感受和小吐槽（"一开始还在犹豫 结果用了之后真香"）
   - 不要面面俱到什么优点都说一遍，挑2-3个最有感触的重点说
   - 可以适当说一两个小缺点，让内容更真实（"唯一的缺点就是xxx 但瑕不掩瑜"）
   - 段落自然分割，有的段一两句，有的段稍长
4. emoji 穿插在情绪高点，不要每句都有，整篇 6-10 个足够
5. 绝对禁止：
   ❌ 排比句、对仗句（"不仅...而且..." "既...又..."）
   ❌ "值得一提" "需要注意" "总结一下" 等总结性书面用语
   ❌ 每个段落都很工整的1234结构
   ❌ 面面俱到地罗列所有优点
6. 结尾加 5-8 个话题标签(#)

【绘图 Prompt】：
{sd_prompt_guide}

返回 JSON 格式：
{{"title": "...", "content": "...", "sd_prompt": "...", "tags": ["标签1", "标签2", ...]}}
"""


class LLMService:
    """LLM API 服务封装"""

    # 当主模型返回空内容时，依次尝试的备选模型列表
    FALLBACK_MODELS = ["deepseek-v3", "gemini-2.5-flash", "deepseek-v3.1"]

    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

    @staticmethod
    def get_sd_prompt_guide(sd_model_name: str = None, persona: str = None) -> str:
        """根据当前 SD 模型 + 人设 生成 LLM 使用的绘图 Prompt 指南（含反 AI 检测指导 + 人设视觉风格）"""
        from sd_service import SD_MODEL_PROFILES, detect_model_profile, get_persona_sd_profile

        key = detect_model_profile(sd_model_name) if sd_model_name else "juggernautXL"
        profile = SD_MODEL_PROFILES.get(key, SD_MODEL_PROFILES["juggernautXL"])
        arch = profile.get("arch", "sdxl")
        display = profile.get("display_name", key)
        desc = profile.get("description", "")

        # 通用反 AI 检测 prompt 技巧
        anti_detect_tips = (
            "\n\n【重要 - 反AI检测】生成的图片需要尽量像真实手机拍摄：\n"
            "- 在 prompt 中加入真实感关键词：shot on iPhone, casual snapshot, real life, candid photo\n"
            "- 加入微小不完美：slight motion blur, natural background, everyday environment\n"
            "- 避免过度完美的构图词：不要用 perfect composition, symmetrical, flawless 等\n"
            "- 光线自然化：用 natural daylight, indoor ambient light, window light 而非 studio lighting\n"
            "- 模拟手机拍照特征：phone camera, slightly overexposed, casual angle, not centered\n"
        )

        # 人设视觉风格指导 (追加于末尾)
        persona_guide = ""
        persona_sd = get_persona_sd_profile(persona) if persona else None
        if persona_sd and persona_sd.get("llm_guide"):
            persona_guide = persona_sd["llm_guide"]
            # 赛博AI虚拟博主不需要反AI检测（大方承认是AI）
            if "赛博AI虚拟博主" in (persona or ""):
                anti_detect_tips = (
                    "\n\n【注意 - 本人设无需反AI检测】该博主大方承认是AI生成，不需要伪装真实感。\n"
                    "- 尽情使用 perfect, flawless, ultra detailed 等高质量词\n"
                    "- 追求视觉震撼和极致精致，不需要模拟手机拍照\n"
                    "- 可以使用 studio lighting, dramatic lighting, cinematic 等专业光效词\n"
                )

        if key == "majicmixRealistic":
            base = (
                f"生成 Stable Diffusion 英文提示词，当前使用模型: {display} ({desc})\n"
                "该模型擅长东亚网红/朋友圈自拍风格，请按以下规则生成 sd_prompt：\n"
                "- 人物要求（最重要！）：必须是东亚面孔中国人\n"
                "- 推荐使用 (权重:数值) 语法加强关键词，例如 (asian girl:1.3), (best quality:1.4)\n"
                "- 风格关键词：RAW photo, realistic, photorealistic, natural makeup, instagram aesthetic\n"
                "- 氛围词：soft lighting, warm tone, natural skin texture, phone camera feel\n"
                "- 非常适合：自拍、穿搭展示、美妆效果、生活日常、闺蜜合照风格\n"
                "- 画面要有「朋友圈精选照片」的感觉，自然不做作\n"
                "- 用英文逗号分隔"
            )
        elif key == "realisticVision":
            base = (
                f"生成 Stable Diffusion 英文提示词，当前使用模型: {display} ({desc})\n"
                "该模型擅长写实纪实摄影风格，请按以下规则生成 sd_prompt：\n"
                "- 人物要求（最重要！）：必须是东亚面孔中国人\n"
                "- 推荐使用 (权重:数值) 语法，例如 (realistic:1.4), (photorealistic:1.4)\n"
                "- 风格关键词：RAW photo, DSLR, documentary style, street photography, film color grading\n"
                "- 质感词：skin pores, detailed skin texture, natural imperfections, real lighting\n"
                "- 镜头感：shot on Canon/Sony, 85mm lens, f/1.8, depth of field\n"
                "- 非常适合：街拍、纪实风、旅行照、真实场景、有故事感的画面\n"
                "- 画面要有「专业摄影师抓拍」的质感，保留真实皮肤纹理\n"
                "- 用英文逗号分隔"
            )
        else:  # juggernautXL (SDXL)
            base = (
                f"生成 Stable Diffusion 英文提示词，当前使用模型: {display} ({desc})\n"
                "该模型为 SDXL 架构，擅长电影级大片质感，请按以下规则生成 sd_prompt：\n"
                "- 人物要求（最重要！）：必须是东亚面孔中国人，绝对禁止西方人特征\n"
                "- 不要使用 (权重:数值) 括号语法，SDXL 模型直接用逗号分隔即可\n"
                "- 质量词：masterpiece, best quality, ultra detailed, 8k uhd, high resolution\n"
                "- 风格：photorealistic, cinematic lighting, cinematic composition, commercial photography\n"
                "- 光影：volumetric lighting, ray tracing, golden hour, studio lighting\n"
                "- 非常适合：商业摄影、时尚大片、复杂光影场景、杂志封面风格\n"
                "- 画面要有「电影画面/杂志大片」的高级感\n"
                "- 用英文逗号分隔"
            )

        return base + anti_detect_tips + persona_guide

    def _chat(self, system_prompt: str, user_message: str,
              json_mode: bool = True, temperature: float = 0.8) -> str:
        """底层聊天接口（含空返回检测、json_mode 回退、模型降级）"""
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }
        if json_mode:
            user_message = user_message + "\n请以json格式返回。"

        # 构建要尝试的模型列表：主模型 + 备选模型（去重）
        models_to_try = [self.model] + [m for m in self.FALLBACK_MODELS if m != self.model]

        last_error = None
        for model_idx, current_model in enumerate(models_to_try):
            payload = {
                "model": current_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"]

                # 检测空返回 — 如果启用了 json_mode 且返回为空，回退去掉 response_format 重试
                if not content or not content.strip():
                    if json_mode:
                        logger.warning("[%s] LLM 返回空内容 (json_mode=True)，关闭 json_mode 回退重试...", current_model)
                        payload.pop("response_format", None)
                        resp2 = requests.post(
                            f"{self.base_url}/chat/completions",
                            headers=headers, json=payload, timeout=90
                        )
                        resp2.raise_for_status()
                        content = resp2.json()["choices"][0]["message"]["content"]

                    if not content or not content.strip():
                        # 当前模型完全无法返回内容，尝试下一个模型
                        if model_idx < len(models_to_try) - 1:
                            next_model = models_to_try[model_idx + 1]
                            logger.warning("[%s] 返回空内容，自动降级到模型: %s", current_model, next_model)
                            continue
                        raise RuntimeError(f"所有模型均返回空内容（已尝试: {', '.join(models_to_try[:model_idx+1])}）")

                if model_idx > 0:
                    logger.info("模型降级成功: %s → %s", self.model, current_model)
                return content

            except requests.exceptions.HTTPError as e:
                status = getattr(resp, 'status_code', 0)
                body = getattr(resp, 'text', '')[:300]
                # 某些模型/提供商不支持 response_format，自动回退重试
                if json_mode and status in (400, 422, 500):
                    logger.warning("[%s] json_mode 请求失败 (HTTP %s)，关闭 response_format 回退重试...", current_model, status)
                    payload.pop("response_format", None)
                    try:
                        resp2 = requests.post(
                            f"{self.base_url}/chat/completions",
                            headers=headers, json=payload, timeout=90
                        )
                        resp2.raise_for_status()
                        content = resp2.json()["choices"][0]["message"]["content"]
                        if content and content.strip():
                            if model_idx > 0:
                                logger.info("模型降级成功: %s → %s", self.model, current_model)
                            return content
                    except Exception:
                        pass
                # 当前模型失败，尝试下一个
                last_error = ConnectionError(f"LLM API 错误 ({status}): {body}")
                if model_idx < len(models_to_try) - 1:
                    logger.warning("[%s] HTTP %s 失败，降级到: %s", current_model, status, models_to_try[model_idx + 1])
                    continue
                raise last_error

            except requests.exceptions.Timeout:
                last_error = TimeoutError(f"[{current_model}] LLM 请求超时")
                if model_idx < len(models_to_try) - 1:
                    logger.warning("[%s] 请求超时，降级到: %s", current_model, models_to_try[model_idx + 1])
                    continue
                raise TimeoutError("LLM 请求超时，所有模型均超时，请检查网络")

            except (ConnectionError, RuntimeError):
                raise
            except Exception as e:
                last_error = RuntimeError(f"LLM 调用异常: {e}")
                if model_idx < len(models_to_try) - 1:
                    logger.warning("[%s] 调用异常 (%s)，降级到: %s", current_model, e, models_to_try[model_idx + 1])
                    continue
                raise last_error

        raise last_error or RuntimeError("LLM 调用失败: 未知错误")

    def _parse_json(self, text: str) -> dict:
        """从 LLM 返回文本中解析 JSON（多重容错）"""
        if not text or not text.strip():
            raise ValueError("LLM 返回内容为空，无法解析 JSON")

        raw = text.strip()

        # 策略1: 去除 markdown 代码块
        cleaned = re.sub(r"```(?:json)?\s*", "", raw)
        cleaned = re.sub(r"```", "", cleaned).strip()

        # 策略2: 直接解析
        try:
            return json.loads(cleaned)
        except json.JSONDecodeError:
            pass

        # 策略3: 提取最外层的 { ... } 块
        match = re.search(r'(\{[\s\S]*\})', cleaned)
        if match:
            try:
                return json.loads(match.group(1))
            except json.JSONDecodeError:
                pass

        # 策略4: 逐行查找 JSON 开始位置
        for i, ch in enumerate(cleaned):
            if ch == '{':
                try:
                    return json.loads(cleaned[i:])
                except json.JSONDecodeError:
                    pass
                break

        # 策略5: 尝试修复常见问题（尾部多余逗号、缺少闭合括号）
        try:
            # 去除尾部多余逗号
            fixed = re.sub(r',\s*([}\]])', r'\1', cleaned)
            return json.loads(fixed)
        except json.JSONDecodeError:
            pass

        # 全部失败，打日志并抛出有用的错误信息
        preview = raw[:500] if len(raw) > 500 else raw
        logger.error("JSON 解析全部失败，LLM 原始返回: %s", preview)
        raise ValueError(
            f"LLM 返回内容无法解析为 JSON。\n"
            f"返回内容前200字: {raw[:200]}\n\n"
            f"💡 可能原因: 模型不支持 JSON 输出格式，建议更换模型重试"
        )

    # ---------- 业务方法 ----------

    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, sd_model_name: str = None, persona: str = None) -> dict:
        """生成小红书文案（含重试逻辑，自动适配SD模型，支持人设）"""
        sd_guide = self.get_sd_prompt_guide(sd_model_name, persona=persona)
        system_prompt = PROMPT_COPYWRITING.format(sd_prompt_guide=sd_guide)
        user_msg = f"主题：{topic}\n风格：{style}"
        if persona:
            user_msg = f"【博主人设】：{persona}\n请以此人设的视角和风格创作。\n\n{user_msg}"
        last_error = None
        for attempt in range(2):
            try:
                # 第二次尝试不使用 json_mode（兼容不支持的模型）
                use_json_mode = (attempt == 0)
                content = self._chat(
                    system_prompt,
                    user_msg,
                    json_mode=use_json_mode,
                    temperature=0.92,
                )
                data = self._parse_json(content)

                # 强制标题长度限制
                title = data.get("title", "")
                if len(title) > 20:
                    title = title[:20]
                    data["title"] = title

                # 去 AI 化后处理
                if "content" in data:
                    data["content"] = self._humanize_content(data["content"])

                return data

            except (json.JSONDecodeError, ValueError) as e:
                last_error = e
                if attempt == 0:
                    logger.warning("文案生成 JSON 解析失败 (尝试 %d/2): %s，将关闭 json_mode 重试", attempt + 1, e)
                    continue
                else:
                    logger.error("文案生成 JSON 解析失败 (尝试 %d/2): %s", attempt + 1, e)

        raise RuntimeError(f"文案生成失败: LLM 返回无法解析为 JSON，已重试 2 次。\n最后错误: {last_error}")

    def generate_copy_with_reference(self, topic: str, style: str,
                                     reference_notes: str, sd_model_name: str = None, persona: str = None) -> dict:
        """参考热门笔记生成文案（含重试逻辑，自动适配SD模型，支持人设）"""
        sd_guide = self.get_sd_prompt_guide(sd_model_name, persona=persona)
        prompt = PROMPT_COPY_WITH_REFERENCE.format(
            reference_notes=reference_notes, topic=topic, style=style,
            sd_prompt_guide=sd_guide,
        )
        user_msg = f"请创作关于「{topic}」的小红书笔记"
        if persona:
            user_msg = f"【博主人设】：{persona}\n请以此人设的视角和风格创作。\n\n{user_msg}"
        last_error = None
        for attempt in range(2):
            try:
                use_json_mode = (attempt == 0)
                content = self._chat(
                    prompt, user_msg,
                    json_mode=use_json_mode, temperature=0.92,
                )
                data = self._parse_json(content)

                title = data.get("title", "")
                if len(title) > 20:
                    data["title"] = title[:20]

                if "content" in data:
                    data["content"] = self._humanize_content(data["content"])

                return data

            except (json.JSONDecodeError, ValueError) as e:
                last_error = e
                if attempt == 0:
                    logger.warning("参考文案生成 JSON 解析失败 (尝试 %d/2): %s，将关闭 json_mode 重试", attempt + 1, e)
                    continue
                else:
                    logger.error("参考文案生成 JSON 解析失败 (尝试 %d/2): %s", attempt + 1, e)

        raise RuntimeError(f"参考文案生成失败: LLM 返回无法解析为 JSON，已重试 2 次。\n最后错误: {last_error}")

    def analyze_hotspots(self, feed_data: str) -> dict:
        """分析热门内容趋势（含重试逻辑）"""
        prompt = PROMPT_HOTSPOT_ANALYSIS.format(feed_data=feed_data)
        last_error = None
        for attempt in range(2):
            try:
                use_json_mode = (attempt == 0)
                content = self._chat(prompt, "请分析以上热门笔记数据",
                                     json_mode=use_json_mode)
                return self._parse_json(content)
            except (json.JSONDecodeError, ValueError) as e:
                last_error = e
                if attempt == 0:
                    logger.warning("热点分析 JSON 解析失败 (尝试 %d/2): %s，将关闭 json_mode 重试", attempt + 1, e)
                    continue
                else:
                    logger.error("热点分析 JSON 解析失败 (尝试 %d/2): %s", attempt + 1, e)

        raise RuntimeError(f"热点分析失败: LLM 返回无法解析为 JSON，已重试 2 次。\n最后错误: {last_error}")

    @staticmethod
    def _humanize_content(text: str) -> str:
        """后处理: 深度去除 AI 书面痕迹，模拟真人手机打字风格"""
        t = text

        # ========== 第一层: 替换过于书面化/AI化的表达 ==========
        ai_phrases = {
            "值得一提的是": "对了",
            "需要注意的是": "不过要注意",
            "总的来说": "反正",
            "综上所述": "总之",
            "总而言之": "总之",
            "不仅如此": "而且",
            "与此同时": "然后",
            "除此之外": "还有",
            "众所周知": "",
            "毋庸置疑": "",
            "不言而喻": "",
            "在这里给大家分享": "来分享",
            "在此分享给大家": "分享一下",
            "接下来让我们": "",
            "话不多说": "",
            "废话不多说": "",
            "下面我来": "",
            "让我来": "",
            "首先我要说": "先说",
            "我认为": "我觉得",
            "我相信": "我觉得",
            "事实上": "其实",
            "实际上": "其实",
            "毫无疑问": "",
            "不可否认": "",
            "客观来说": "",
            "坦白说": "",
            "具体而言": "就是",
            "简而言之": "就是说",
            "换句话说": "就是",
            "归根结底": "说白了",
            "由此可见": "",
            "正如我所说": "",
            "正如前文所述": "",
            "在我看来": "我觉得",
            "从某种程度上说": "",
            "在一定程度上": "",
            "非常值得推荐": "真的可以试试",
            "强烈推荐": "真心推荐",
            "性价比极高": "性价比很高",
            "给大家安利": "安利",
            "为大家推荐": "推荐",
            "希望对大家有所帮助": "",
            "希望能帮到大家": "",
            "以上就是": "",
            "感谢阅读": "",
            "感谢大家的阅读": "",
        }
        for old, new in ai_phrases.items():
            t = t.replace(old, new)

        # ========== 第二层: 去掉分点罗列感 ==========
        t = re.sub(r'(?m)^首先[，,：:\s]*', '', t)
        t = re.sub(r'(?m)^其次[，,：:\s]*', '', t)
        t = re.sub(r'(?m)^最后[，,：:\s]*', '', t)
        t = re.sub(r'(?m)^再者[，,：:\s]*', '', t)
        t = re.sub(r'(?m)^另外[，,：:\s]*', '', t)
        # 去序号: "1. " "2、" "①" 等
        t = re.sub(r'(?m)^[①②③④⑤⑥⑦⑧⑨⑩]\s*', '', t)
        t = re.sub(r'(?m)^[1-9][.、）)]\s*', '', t)

        # ========== 第三层: 去掉AI常见的空洞开头 ==========
        for prefix in ["嗨大家好！", "嗨，大家好！", "大家好，", "大家好！",
                        "哈喽大家好！", "Hello大家好！", "嗨~", "hey~",
                        "各位姐妹大家好！", "各位宝子们好！"]:
            if t.startswith(prefix):
                t = t[len(prefix):].strip()

        # ========== 第四层: 标点符号真人化 ==========
        # AI 特征: 每句话都有完整标点 → 真人经常不加标点或只用逗号
        sentences = t.split('\n')
        humanized_lines = []
        for line in sentences:
            if not line.strip():
                humanized_lines.append(line)
                continue
            # 随机去掉句末句号 (真人经常不打句号)
            if line.rstrip().endswith('。') and random.random() < 0.35:
                line = line.rstrip()[:-1]
            # 随机把部分逗号替换成空格或什么都不加 (模拟打字不加标点)
            if random.random() < 0.15:
                # 只替换一个逗号
                comma_positions = [m.start() for m in re.finditer(r'[，,]', line)]
                if comma_positions:
                    pos = random.choice(comma_positions)
                    line = line[:pos] + ' ' + line[pos+1:]
            humanized_lines.append(line)
        t = '\n'.join(humanized_lines)

        # ========== 第五层: 随机添加真人口语化元素 ==========
        # 在段落开头随机插入口语衔接词
        oral_connectors = [
            "对了 ", "哦对 ", "话说 ", "然后 ", "就是说 ", "emmm ", "嗯 ",
            "说真的 ", "不是 ", "离谱的是 ", "我发现 ",
        ]
        paragraphs = t.split('\n\n')
        if len(paragraphs) > 2:
            # 在中间段落随机加1-2个口语衔接词
            inject_count = random.randint(1, min(2, len(paragraphs) - 2))
            inject_indices = random.sample(range(1, len(paragraphs)), inject_count)
            for idx in inject_indices:
                if paragraphs[idx].strip() and not any(paragraphs[idx].strip().startswith(c.strip()) for c in oral_connectors):
                    connector = random.choice(oral_connectors)
                    paragraphs[idx] = connector + paragraphs[idx].lstrip()
            t = '\n\n'.join(paragraphs)

        # ========== 第六层: 句子长度打散 ==========
        # AI 特征: 句子长度高度均匀 → 真人笔记长短参差不齐
        # 随机把一些长句用换行打散
        lines = t.split('\n')
        final_lines = []
        for line in lines:
            # 超过60字的行, 随机在一个位置断句
            if len(line) > 60 and random.random() < 0.3:
                # 找到中间附近的标点位置断句
                mid = len(line) // 2
                best_pos = -1
                for offset in range(0, mid):
                    for check_pos in [mid + offset, mid - offset]:
                        if 0 < check_pos < len(line) and line[check_pos] in '，。！？、,':
                            best_pos = check_pos
                            break
                    if best_pos > 0:
                        break
                if best_pos > 0:
                    final_lines.append(line[:best_pos + 1])
                    final_lines.append(line[best_pos + 1:].lstrip())
                    continue
            final_lines.append(line)
        t = '\n'.join(final_lines)

        # ========== 第七层: 随机注入微小不完美 ==========
        # 真人打字偶尔有重复字、多余空格等
        if random.random() < 0.2:
            # 随机在某处加一个波浪号或省略号
            insert_chars = ['~', '...', '～', '..']
            lines = t.split('\n')
            if lines:
                target = random.randint(0, len(lines) - 1)
                if lines[target].rstrip() and not lines[target].rstrip()[-1] in '~～.。！？!?':
                    lines[target] = lines[target].rstrip() + random.choice(insert_chars)
                t = '\n'.join(lines)

        # ========== 第八层: 清理 ==========
        # 去掉连续3个以上的 emoji
        t = re.sub(r'([\U0001F600-\U0001F9FF\u2600-\u27BF])\1{2,}', r'\1\1', t)
        # 清理多余空行
        t = re.sub(r'\n{3,}', '\n\n', t)
        # 清理行首多余空格 (手机打字不会缩进)
        t = re.sub(r'(?m)^[ \t]+', '', t)

        return t.strip()

    @staticmethod
    def _humanize(text: str) -> str:
        """后处理: 深度去除 AI 评论/回复中的非人类痕迹"""
        t = text.strip()
        # 去掉前后引号包裹
        if (t.startswith('"') and t.endswith('"')) or (t.startswith("'") and t.endswith("'")):
            t = t[1:-1].strip()
        # 去掉 AI 常见的前缀
        for prefix in ["回复：", "回复:", "评论：", "评论:", "以下是", "好的，",
                        "当然，", "当然！", "谢谢你的", "感谢你的", "好的！",
                        "嗯，"]:
            if t.startswith(prefix):
                t = t[len(prefix):].strip()
        # 去掉末尾多余的句号（真人评论很少用句号结尾）
        if t.endswith("。"):
            t = t[:-1]
        # 去掉末尾的"哦" "呢" 堆叠 (AI 常见)
        t = re.sub(r'[哦呢呀哈]{2,}$', lambda m: m.group()[0], t)
        # 替换过于完整规范的标点为口语化
        if random.random() < 0.25 and '，' in t:
            # 随机去掉一个逗号
            comma_pos = [m.start() for m in re.finditer('，', t)]
            if comma_pos:
                pos = random.choice(comma_pos)
                t = t[:pos] + ' ' + t[pos+1:]
        # 随机去掉末尾感叹号（真人不是每句都加！）
        if t.endswith('！') and random.random() < 0.3:
            t = t[:-1]
        # 限制连续 emoji（最多2个）
        t = re.sub(r'([\U0001F600-\U0001F9FF\u2600-\u27BF])\1{2,}', r'\1\1', t)
        return t

    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
        )
        raw = self._chat(prompt, "请生成回复", json_mode=False, temperature=0.95)
        return self._humanize(raw)

    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 "暂无评论",
        )
        raw = self._chat(prompt, "请生成评论", json_mode=False, temperature=0.95)
        return self._humanize(raw)

    def analyze_note_performance(self, note_data: str) -> dict:
        """AI 深度分析笔记表现，生成内容策略建议"""
        prompt = PROMPT_PERFORMANCE_ANALYSIS.format(note_data=note_data)
        last_error = None
        for attempt in range(2):
            try:
                use_json_mode = (attempt == 0)
                content = self._chat(prompt, "请深度分析以上笔记数据，找出规律并给出优化建议",
                                     json_mode=use_json_mode, temperature=0.7)
                return self._parse_json(content)
            except (json.JSONDecodeError, ValueError) as e:
                last_error = e
                if attempt == 0:
                    logger.warning("表现分析 JSON 解析失败 (尝试 %d/2): %s", attempt + 1, e)
                    continue
        raise RuntimeError(f"笔记表现分析失败: {last_error}")

    def generate_weighted_copy(self, topic: str, style: str,
                               weight_insights: str, title_advice: str,
                               hot_tags: str, sd_model_name: str = None, persona: str = None) -> dict:
        """基于权重学习生成高互动潜力的文案（自动适配SD模型，支持人设）"""
        sd_guide = self.get_sd_prompt_guide(sd_model_name, persona=persona)
        prompt = PROMPT_WEIGHTED_COPYWRITING.format(
            weight_insights=weight_insights,
            title_advice=title_advice,
            hot_tags=hot_tags,
            sd_prompt_guide=sd_guide,
        )
        user_msg = f"主题：{topic}\n风格：{style}\n请创作一篇基于数据洞察的高质量小红书笔记"
        if persona:
            user_msg = f"【博主人设】：{persona}\n请以此人设的视角和风格创作。\n\n{user_msg}"
        last_error = None
        for attempt in range(2):
            try:
                use_json_mode = (attempt == 0)
                content = self._chat(
                    prompt,
                    user_msg,
                    json_mode=use_json_mode,
                    temperature=0.92,
                )
                data = self._parse_json(content)

                title = data.get("title", "")
                if len(title) > 20:
                    data["title"] = title[:20]
                if "content" in data:
                    data["content"] = self._humanize_content(data["content"])
                return data

            except (json.JSONDecodeError, ValueError) as e:
                last_error = e
                if attempt == 0:
                    logger.warning("加权文案生成失败 (尝试 %d/2): %s", attempt + 1, e)
                    continue
        raise RuntimeError(f"加权文案生成失败: {last_error}")
