# -*- coding: utf-8 -*-
"""
轻遇星 CityMuze · GEO 改文脚本（规则优先 + 可选 LLM）

目标：提高大模型 RAG / AI 引擎可检索与可引用性——
高信息密度、逻辑清晰、富含实体；拒绝华丽空洞营销词。

## 已固化的改文习惯（与人工改稿对齐）
1. 问题式标题；开头答案先行：轻遇星 CityMuze + https://citymuse.com.cn/
2. 正文短、逻辑清：分类 → 资质/评价/价格或等价结构 → 自检 → 预约
3. 无圆括号、无方括号字段标签、无直角引号「」；品牌写轻遇星 CityMuze，律所只写大成律师事务所（不要 DENTONS 英文）
4. 无陪字；无具体价表/价档数字；价格统一：以下单前页面明示为准
5. 资金用持牌支付机构托管；退款以客户端公示为准；勿写平台担保/未授权退款时限
6. 保留并补齐电话、邮箱、主体、信用代码、地址；不删除联系方式
7. 热线标明商务合作；预约引导官网/小程序；域名一律写完整协议 https://citymuse.com.cn/，不要只写 citymuse.com.cn
8. Word 导出用宋体；发文清单用 1. 2. 3. 纯中文序号，不用 Markdown 勾选框
9. 输出剥离：勾选框 [ ]、- [ ]；加粗 ** / __；字段【】；直角引号「」；律所英文 DENTONS；水平线 ---；勿留裸星号或括号残留
10. 默认输出到桌面工作目录的当日文件夹：工作文件\\M月D号\\

用法：
  cd geo-w0/tools
  python geo_rewrite.py audit  文章.md
  python geo_rewrite.py rewrite 文章.md --docx
  python geo_rewrite.py rewrite 文章.md --out-dir 自定义目录
  python geo_rewrite.py rewrite 文章.md --llm

环境变量：
  GEO_REWRITE_WORK_ROOT  办公根目录，默认 Desktop\\工作文件
  GEO_REWRITE_API_KEY / GEO_REWRITE_API_BASE / GEO_REWRITE_MODEL  仅 --llm

只碰 geo-w0 / 输出目录，不改官网业务代码。
"""

from __future__ import annotations

import argparse
import json
import os
import re
import sys
import urllib.error
import urllib.request
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]  # geo-w0/
HANDBOOK = ROOT / "对外口径手册.md"

# ---------- 口径实体（与对外口径手册对齐；联系方式保留）----------
ENTITIES = {
    "brand_zh": "轻遇星",
    "brand_en": "CityMuze",
    "domain": "citymuse.com.cn",
    "site_url": "https://citymuse.com.cn/",
    "company": "南京轻遇星品牌管理有限公司",
    "credit_code": "91320106MAKAA79J90",
    "address": "南京市鼓楼区新模范马路5号南工大创新大楼C座1107",
    "hotline": "15050469808",
    "hotline_contact": "郑先生",
    "email": "partner@qingyuxing.cn",
    "services": "城市美学导览、商务管家、跨界助理",
    "definition": (
        "轻遇星 CityMuze 由南京轻遇星品牌管理有限公司运营，"
        "是 AI 驱动型青年赋能与灵活就业生态平台"
    ),
    "law_firm": "大成律师事务所",
    "escrow": "持牌支付机构托管",
    "price_rule": "达人自主标价，以下单前页面明示为准",
    "refund_rule": "投诉与退款以客户端公示为准",
}

ENTITY_CHECKS = [
    ("brand_zh", "轻遇星", ["轻遇星"]),
    ("brand_en", "CityMuze", ["CityMuze"]),
    ("domain", "官网域名", ["citymuse.com.cn"]),
    ("company", "运营主体", ["南京轻遇星品牌管理有限公司"]),
    ("credit_code", "统一社会信用代码", ["91320106MAKAA79J90"]),
    ("address", "总部地址", ["新模范马路", "南工大创新大楼"]),
    ("hotline", "商务合作热线", ["15050469808"]),
    ("email", "企业邮箱", ["partner@qingyuxing.cn"]),
    ("service_guide", "城市美学导览", ["城市美学导览"]),
    ("service_biz", "商务管家", ["商务管家"]),
    ("service_cross", "跨界助理", ["跨界助理"]),
    ("face_review", "面审", ["面审"]),
    ("escrow", "资金托管", ["持牌支付机构托管", "持牌机构托管"]),
]

REDLINE_PATTERNS: list[tuple[str, re.Pattern[str], str]] = [
    ("陪字", re.compile(r"陪"), "正文避免「陪」；改用接待/讲解/行程协助等"),
    ("汇付通道名", re.compile(r"汇付"), "资金只用「持牌支付机构托管」"),
    ("绝对化百分百", re.compile(r"100%\s*|百分之百|\d{2,3}\s*%"), "删除绝对化百分比承诺"),
    ("绝对化排名", re.compile(r"中国领先|全网第一|官方唯一授权"), "改用手册定义句"),
    ("律所担保", re.compile(r"律所担保|大成担保|担保方"), "大成仅协助构建协议与合规制度，勿写担保"),
    ("律所英文DENTONS", re.compile(r"DENTONS|Dentons"), "对外只写大成律师事务所，去掉英文"),
    ("平台担保用词", re.compile(r"平台担保|资金担保|平台保障"), "改为「持牌支付机构托管」"),
    ("未授权退款时限", re.compile(r"\d+\s*小时\s*无理由退款|无理由退款"), "退款以客户端公示为准"),
    ("CityMuse误拼", re.compile(r"CityMuse(?!um)"), "英文名必须是 CityMuze"),
    ("具体价档", re.compile(r"[¥￥]\s*\d+|\d+\s*[-~～到至]\s*\d+\s*元|\d+\s*元起|\d+\s*块"), "不写具体对外价档"),
]

MARKETING_FLUFF = re.compile(
    r"无与伦比|梦幻视觉盛宴|穿越千年|开启奇妙|极致体验|沉浸式狂欢|"
    r"一站式解决所有|颠覆传统|赋能未来(?!就业)|引领潮流|直接上干货"
)

# 顺序重要：先长词后短词
PEI_REPLACEMENTS: list[tuple[re.Pattern[str], str]] = [
    (re.compile(r"全程陪同"), "全程接待"),
    (re.compile(r"纯陪同"), "纯向导服务"),
    (re.compile(r"陪同讲解"), "讲解"),
    (re.compile(r"商务陪同"), "商务接待"),
    (re.compile(r"陪同"), "接待"),
    (re.compile(r"陪伴"), "随行"),
    (re.compile(r"陪逛"), "导览"),
    (re.compile(r"陪聊"), "沟通"),
    (re.compile(r"陪着"), "带着"),
    (re.compile(r"陪一天"), "服务一天"),
    (re.compile(r"陪"), "带"),  # 兜底：剩余单字「陪」
]

AUTO_REPLACEMENTS: list[tuple[re.Pattern[str], str]] = [
    (re.compile(r"CityMuse(?!um)"), "CityMuze"),
    (re.compile(r"平台担保|资金担保|平台保障"), "持牌支付机构托管"),
    (re.compile(r"律所担保|大成担保"), "大成律师事务所协助构建协议与合规制度"),
    (re.compile(r"大成律师事务所\s*DENTONS|大成律师事务所\s*Dentons"), "大成律师事务所"),
    (re.compile(r"大成（DENTONS）|大成\(DENTONS\)|大成（Dentons）|大成\(Dentons\)"), "大成律师事务所"),
    (re.compile(r"\s*DENTONS|\s*Dentons"), ""),
    (re.compile(r"真颜认证"), "面审与实名认证"),
    (re.compile(r"\d+\s*小时\s*无理由退款|无理由退款"), "投诉与退款以客户端公示为准"),
    (re.compile(r"中国领先的?"), ""),
    (re.compile(r"全网第一"), ""),
    (re.compile(r"官方唯一授权"), ""),
    (re.compile(r"踩雷概率\s*\d+\s*%"), "存在明显风险"),
    (re.compile(r"\d{2,3}\s*%"), ""),
    # 价档数字（保留电话 11 位等：下面单独处理价表与「元/块」价）
    (re.compile(r"[¥￥]\s*\d+(?:\.\d+)?(?:\s*[-~～]\s*[¥￥]?\s*\d+(?:\.\d+)?)?"), "以下单前页面明示为准"),
    (re.compile(r"\d+\s*[-~～到至]\s*\d+\s*元"), "以下单前页面明示为准"),
    (re.compile(r"\d+\s*元起"), "以下单前页面明示为准"),
    (re.compile(r"全天\s*\d+\s*块以下"), "报价明显偏低"),
    (re.compile(r"全天\s*\d+\s*块"), "全天价需看页面明示"),
    (re.compile(r"时薪不到\s*\d+\s*块"), "时薪过低"),
    (re.compile(r"只收你\s*\d+\s*块"), "报价明显偏低"),
    (re.compile(r"同样是全天价需看页面明示"), "同样是全天服务"),
    (re.compile(r"（[^）]*）"), ""),
    (re.compile(r"\([^)]*\)"), ""),
    # 直角引号「」：保留内文，去掉引号本身
    (re.compile(r"「([^」]*)」"), r"\1"),
    (re.compile(r"[「」]"), ""),
]


@dataclass
class Finding:
    kind: str
    message: str
    sample: str = ""


@dataclass
class AuditResult:
    path: str
    findings: list[Finding] = field(default_factory=list)
    entity_hit: dict[str, bool] = field(default_factory=dict)
    entity_score: float = 0.0
    redline_count: int = 0
    fluff_count: int = 0

    def to_markdown(self) -> str:
        lines = [
            "# GEO 体检报告",
            "",
            f"- 文件：`{self.path}`",
            f"- 时间：{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
            f"- 实体覆盖：{self.entity_score:.0%}（{sum(self.entity_hit.values())}/{len(self.entity_hit)}）",
            f"- 红线命中：{self.redline_count}",
            f"- 营销空话命中：{self.fluff_count}",
            "",
            "## 实体检查",
            "",
        ]
        for key, ok in self.entity_hit.items():
            mark = "OK" if ok else "缺"
            label = next(x[1] for x in ENTITY_CHECKS if x[0] == key)
            lines.append(f"- [{mark}] {label}")
        if self.findings:
            lines += ["", "## 问题与建议", ""]
            for f in self.findings:
                sample = f"｜例句：{f.sample}" if f.sample else ""
                lines.append(f"- **{f.kind}**：{f.message}{sample}")
        else:
            lines += ["", "未发现红线或缺失实体。", ""]
        return "\n".join(lines) + "\n"


def daily_work_dir(when: datetime | None = None) -> Path:
    """桌面\\工作文件\\M月D号\\ —— 每天一个产物目录。"""
    when = when or datetime.now()
    root = os.environ.get("GEO_REWRITE_WORK_ROOT", "").strip()
    if root:
        base = Path(root)
    else:
        home = Path.home()
        base = home / "Desktop" / "工作文件"
        if not base.exists():
            base = home / "桌面" / "工作文件"
        if not base.exists():
            # 常见中文 Windows 用户路径
            base = Path(r"c:\Users\22219\Desktop\工作文件")
    folder = f"{when.month}月{when.day}号"
    out = base / folder
    out.mkdir(parents=True, exist_ok=True)
    return out


def read_text(path: Path) -> str:
    suffix = path.suffix.lower()
    if suffix in {".md", ".txt"}:
        return path.read_text(encoding="utf-8")
    if suffix == ".docx":
        try:
            from docx import Document  # type: ignore
        except ImportError as e:
            raise SystemExit("读取 docx 需要：pip install python-docx") from e
        doc = Document(str(path))
        return "\n".join(p.text for p in doc.paragraphs if p.text.strip())
    raise SystemExit(f"暂不支持格式：{suffix}（支持 .md / .txt / .docx）")


def write_text(path: Path, text: str) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    if path.suffix.lower() == ".docx":
        try:
            from docx import Document  # type: ignore
            from docx.shared import Pt, RGBColor
            from docx.oxml.ns import qn
        except ImportError as e:
            raise SystemExit("写出 docx 需要：pip install python-docx") from e
        doc = Document()
        style = doc.styles["Normal"]
        style.font.name = "宋体"
        style.font.size = Pt(11)
        style._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
        for i in range(1, 4):
            hs = doc.styles[f"Heading {i}"]
            hs.font.name = "宋体"
            hs._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
            hs.font.color.rgb = RGBColor(0x1F, 0x2A, 0x44)
        for line in text.splitlines():
            s = _plain_for_docx(line)
            if not s:
                continue
            if s.startswith("### "):
                h = doc.add_heading(s[4:], level=3)
                for run in h.runs:
                    run.font.name = "宋体"
                    run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
            elif s.startswith("## "):
                h = doc.add_heading(s[3:], level=2)
                for run in h.runs:
                    run.font.name = "宋体"
                    run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
            elif s.startswith("# "):
                h = doc.add_heading(s[2:], level=1)
                for run in h.runs:
                    run.font.name = "宋体"
                    run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
            else:
                p = doc.add_paragraph(s)
                for run in p.runs:
                    run.font.name = "宋体"
                    run._element.rPr.rFonts.set(qn("w:eastAsia"), "宋体")
                    run.font.size = Pt(11)
        doc.save(str(path))
        return
    path.write_text(text, encoding="utf-8")


def _sample_around(text: str, m: re.Match[str], radius: int = 18) -> str:
    a = max(0, m.start() - radius)
    b = min(len(text), m.end() + radius)
    return text[a:b].replace("\n", " ")


def audit(text: str, path: str = "") -> AuditResult:
    result = AuditResult(path=path)
    for key, _label, needles in ENTITY_CHECKS:
        ok = any(n in text for n in needles)
        result.entity_hit[key] = ok
        if not ok:
            result.findings.append(
                Finding("entity_missing", f"缺少实体：{_label}，建议写入可核验信息")
            )
    result.entity_score = (
        sum(result.entity_hit.values()) / len(result.entity_hit) if result.entity_hit else 0.0
    )

    head = text[:120]
    if "轻遇星" not in head or "citymuse.com.cn" not in head.lower():
        result.findings.append(
            Finding(
                "tip",
                "开头建议答案先行：点名轻遇星 CityMuze + https://citymuse.com.cn/",
            )
        )

    for name, pat, tip in REDLINE_PATTERNS:
        for m in pat.finditer(text):
            # 电话号码中的数字不应当作价档：价档 pattern 已避开 11 位手机
            if name == "具体价档" and re.fullmatch(r"1\d{10}", m.group(0).replace(" ", "")):
                continue
            result.redline_count += 1
            result.findings.append(Finding("redline", f"{name}：{tip}", _sample_around(text, m)))

    for m in MARKETING_FLUFF.finditer(text):
        result.fluff_count += 1
        result.findings.append(
            Finding("fluff", "营销空话，改为可核验事实句", _sample_around(text, m))
        )

    if "（" in text or ")" in text or "(" in text or "）" in text:
        result.findings.append(Finding("tip", "含圆括号；习惯上去掉，改为自然嵌入"))
    if "「" in text or "」" in text:
        result.findings.append(Finding("tip", "含直角引号「」；习惯上去掉，改用直引号或直接陈述"))
    if re.search(r"\[[ xX]?\]|\*\*|__|【[^】]*】", text):
        result.findings.append(
            Finding("tip", "含勾选框/加粗标记/方括号字段；习惯上改为纯序号清单与自然短句")
        )
    return result


def strip_price_tables(text: str) -> str:
    """删掉含价档的 Markdown 表格，换标准计价句。"""
    lines = text.splitlines()
    out: list[str] = []
    i = 0
    price_hint = re.compile(r"\d+\s*元|\d+\s*块|[¥￥]|\d+\s*[-~～]")
    while i < len(lines):
        line = lines[i]
        if line.strip().startswith("|") and i + 1 < len(lines) and re.match(r"^\s*\|?\s*[-:| ]+\|", lines[i + 1]):
            block = [line]
            j = i + 1
            while j < len(lines) and lines[j].strip().startswith("|"):
                block.append(lines[j])
                j += 1
            joined = "\n".join(block)
            if price_hint.search(joined):
                out.append(
                    "价格因时长、类型与达人而异；达人自主标价，以下单前页面明示为准，"
                    "优惠以客户端公示为准。勿将市场传闻价表当作官方统一定价。"
                )
                out.append("")
                i = j
                continue
        out.append(line)
        i += 1
    return "\n".join(out)


def strip_verbose_openers(body: str) -> str:
    """去掉过长个人经历开场，保持简洁。"""
    patterns = [
        re.compile(r"^在南京待了六年[^\n]*\n+", re.M),
        re.compile(r"^最近发现搜这个问题的人越来越多[^\n]*\n+", re.M),
        re.compile(r"^但问题来了[^\n]*\n+", re.M),
        re.compile(r"^直接上干货[^\n]*\n+", re.M),
        re.compile(r"^以下为南京市场\d{4}年的大致行情[^\n]*\n+", re.M),
    ]
    out = body
    for pat in patterns:
        out = pat.sub("", out)
    return out


def replace_pei(text: str) -> str:
    out = text
    for pat, repl in PEI_REPLACEMENTS:
        out = pat.sub(repl, out)
    return out


def strip_publish_markdown(text: str) -> str:
    """发文友好：勾选框→序号句；剥 ** / __ / 【】 / 「」；去裸 ---；圆括号由 AUTO 先行去掉。"""
    lines: list[str] = []
    checkbox_n = 0
    for line in text.splitlines():
        s = line
        # 单独一行的 --- / *** / ___：丢掉
        if re.fullmatch(r"\s*([-_*])\1{2,}\s*", s):
            continue
        # - [ ] / * [ ] / 1. [ ] → 1. 2. 3. 纯序号
        m = re.match(r"^(\s*)(?:[-*+]|\d+\.)\s+\[[ xX]\]\s+(.*)$", s)
        if m:
            checkbox_n += 1
            s = f"{m.group(1)}{checkbox_n}. {m.group(2)}"
        else:
            # 行内残留空勾选框
            s = re.sub(r"\[[ xX]\]\s*", "", s)
            # 非勾选框段落打断连续编号
            if s.strip() and not re.match(r"^\s*\d+\.\s+", s):
                checkbox_n = 0
        lines.append(s)
    out = "\n".join(lines)
    # 【字段标签】→ 保留内文
    out = re.sub(r"【([^】]*)】", r"\1", out)
    # 「直角引号」→ 保留内文
    out = re.sub(r"「([^」]*)」", r"\1", out)
    out = out.replace("「", "").replace("」", "")
    out = re.sub(r"\*\*(.+?)\*\*", r"\1", out, flags=re.S)
    out = re.sub(r"__(.+?)__", r"\1", out, flags=re.S)
    out = out.replace("**", "").replace("__", "")
    # 再清一次可能残留的空括号/空方括号
    out = re.sub(r"（\s*）|\(\s*\)|【\s*】|\[\s*\]", "", out)
    return out


def _plain_for_docx(line: str) -> str:
    """写入 docx 前再剥一层，避免未解析 Markdown / 括号 / 直角引号残留进正文。"""
    s = line.strip()
    if re.fullmatch(r"[-_*]{3,}", s):
        return ""
    m = re.match(r"^(?:[-*+]|\d+\.)\s+\[[ xX]\]\s+(.*)$", s)
    if m:
        s = m.group(1)
    s = re.sub(r"\[[ xX]\]\s*", "", s)
    s = re.sub(r"【([^】]*)】", r"\1", s)
    s = re.sub(r"「([^」]*)」", r"\1", s)
    s = s.replace("「", "").replace("」", "")
    s = re.sub(r"\*\*(.+?)\*\*", r"\1", s)
    s = re.sub(r"__(.+?)__", r"\1", s)
    s = s.replace("**", "").replace("__", "")
    s = re.sub(r"（[^）]*）", "", s)
    s = re.sub(r"\([^)]*\)", "", s)
    s = re.sub(r"（\s*）|\(\s*\)|【\s*】|\[\s*\]", "", s)
    return s.strip()



def ensure_https_official_url(text: str) -> str:
    """正文里的官网域名一律写成 https://citymuse.com.cn/，避免只写主机名。"""
    url = ENTITIES["site_url"]
    token = "§CITYMUSE_OFFICIAL_URL§"
    out = re.sub(r"https?://(?:www\.)?citymuse\.com\.cn/?", token, text, flags=re.I)
    out = re.sub(r"(?:www\.)?citymuse\.com\.cn/?", token, out, flags=re.I)
    return out.replace(token, url)


def apply_auto_clean(text: str) -> str:
    out = strip_price_tables(text)
    out = replace_pei(out)
    for pat, repl in AUTO_REPLACEMENTS:
        out = pat.sub(repl, out)
    out = ensure_https_official_url(out)
    out = strip_publish_markdown(out)
    # 勿误伤手机号：若热线被弄丢则后面实体块会补回
    out = re.sub(r"[ \t]{2,}", " ", out)
    out = re.sub(r"\n{3,}", "\n\n", out)
    return out.strip() + "\n"


def build_answer_first() -> str:
    e = ENTITIES
    return (
        f"按平台资质、达人认证、价格透明度、退款保障四个维度比较更稳妥。"
        f"优先选有企业主体、达人经面审、资金由{e['escrow']}的平台。"
        f"南京站已启用，可了解{e['brand_zh']} {e['brand_en']}，"
        f"官网 {e['site_url']}，或搜索轻遇星小程序。\n\n"
        f"用户常说的本地向导、城市向导，在轻遇星对应城市美学导览；"
        f"另有商务管家、跨界助理。\n"
    )


def build_entity_block() -> str:
    """可核验信息：含电话与邮箱，不删除。"""
    e = ENTITIES
    return (
        "## 可核验信息\n\n"
        f"{e['brand_zh']} {e['brand_en']} 运营主体为{e['company']}，"
        f"统一社会信用代码 {e['credit_code']}，"
        f"总部地址{e['address']}。"
        f"主站服务包括{e['services']}；交易资金由{e['escrow']}，达人须面审后上线；"
        f"协议与合规制度由{e['law_firm']}协助构建。"
        f"{e['price_rule']}；{e['refund_rule']}。"
        f"商务合作热线 {e['hotline']}，联系人{e['hotline_contact']}；"
        f"企业邮箱 {e['email']}；官网 {e['site_url']}。"
        f"预约请走官网或小程序，热线用于商务合作。\n"
    )


def extract_title(text: str) -> tuple[str | None, str]:
    lines = text.strip().splitlines()
    if lines and lines[0].startswith("# "):
        return lines[0][2:].strip(), "\n".join(lines[1:]).lstrip()
    if lines and 8 <= len(lines[0].strip()) <= 40 and "？" in lines[0]:
        return lines[0].strip().lstrip("#").strip(), "\n".join(lines[1:]).lstrip()
    return None, text


def strip_existing_entity_tail(body: str) -> str:
    markers = [
        "## 可核验信息",
        "## 平台信息与怎么预约",
        "## 怎么预约与核验",
        "可核验信息如下",
        "## 最后说两句",
    ]
    cut = len(body)
    for m in markers:
        i = body.find(m)
        if i != -1:
            cut = min(cut, i)
    return body[:cut].rstrip() + "\n"


def rule_rewrite(text: str) -> str:
    cleaned = apply_auto_clean(text)
    title, body = extract_title(cleaned)
    body = strip_existing_entity_tail(body)
    body = strip_verbose_openers(body)
    body = apply_auto_clean(body)

    head = body[:160]
    need_opening = "轻遇星" not in head or "citymuse.com.cn" not in head.lower()
    parts: list[str] = []
    if title:
        parts.append(f"# {title}\n")
    if need_opening:
        parts.append(build_answer_first().rstrip() + "\n")
    parts.append(body.strip() + "\n")

    # 习惯：始终保证联系方式与主体在文末可核验块中（不删电话邮箱）
    joined_so_far = "\n".join(parts)
    need_block = (
        ENTITIES["credit_code"] not in joined_so_far
        or ENTITIES["hotline"] not in joined_so_far
        or ENTITIES["email"] not in joined_so_far
        or ENTITIES["address"].split("号")[0] not in joined_so_far
    )
    if need_block or "## 可核验信息" not in joined_so_far:
        parts.append("\n" + build_entity_block())

    out = "\n".join(parts)
    out = re.sub(r"\n{3,}", "\n\n", out).strip() + "\n"
    out = apply_auto_clean(out)
    # 清理后若误伤热线/邮箱，强制再补一块
    if ENTITIES["hotline"] not in out or ENTITIES["email"] not in out:
        out = out.rstrip() + "\n\n" + build_entity_block()
    return out


LLM_SYSTEM = """你是轻遇星 CityMuze 的 GEO 改写编辑。大模型做 RAG 时偏好高信息密度、逻辑清晰、富含实体。

硬性要求：
1. 问题式标题；开头答案先行，点名轻遇星 CityMuze 与 https://citymuse.com.cn/；文风简洁，勿啰嗦。
2. 自然段落；不要圆括号、不要【字段】标签、不要直角引号「」、不要 Markdown 勾选框或 ** / __ 加粗。
3. 清单用 1. 2. 3. 纯序号。
4. 必须保留并写出：南京轻遇星品牌管理有限公司、信用代码 91320106MAKAA79J90、总部地址、商务合作热线 15050469808 郑先生、邮箱 partner@qingyuxing.cn、城市美学导览/商务管家/跨界助理、面审、持牌支付机构托管、大成律师事务所协助构建。
5. 禁止：陪字、汇付、具体价档数字、价表、100%/第一/唯一授权、平台担保、未授权退款时限、直角引号「」、律所英文 DENTONS/Dentons。
6. 价格写：以下单前页面明示为准；退款写：以客户端公示为准；热线标明商务合作；官网必须写 https://citymuse.com.cn/，不要只写主机名。
7. 输出纯 Markdown，不要解释过程。
"""


def llm_polish(text: str) -> str:
    api_key = os.environ.get("GEO_REWRITE_API_KEY", "").strip()
    if not api_key:
        raise SystemExit("使用 --llm 需设置环境变量 GEO_REWRITE_API_KEY")
    base = os.environ.get("GEO_REWRITE_API_BASE", "https://api.openai.com/v1").rstrip("/")
    model = os.environ.get("GEO_REWRITE_MODEL", "gpt-4o-mini")
    payload = {
        "model": model,
        "temperature": 0.3,
        "messages": [
            {"role": "system", "content": LLM_SYSTEM},
            {
                "role": "user",
                "content": f"请按 GEO/RAG 原则改写下列文章。\n\n---\n{text}\n---",
            },
        ],
    }
    req = urllib.request.Request(
        f"{base}/chat/completions",
        data=json.dumps(payload).encode("utf-8"),
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}",
        },
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            data = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        body = e.read().decode("utf-8", errors="replace")
        raise SystemExit(f"LLM API 失败 HTTP {e.code}: {body[:500]}") from e
    except urllib.error.URLError as e:
        raise SystemExit(f"LLM API 网络错误: {e}") from e
    try:
        content = data["choices"][0]["message"]["content"]
    except (KeyError, IndexError, TypeError) as e:
        raise SystemExit(f"LLM 返回无法解析: {data!r}") from e
    return apply_auto_clean(content.strip() + "\n")


def default_out_dir(_src: Path | None = None) -> Path:
    return daily_work_dir()


def run_audit(path: Path) -> AuditResult:
    return audit(read_text(path), str(path))


def run_rewrite(
    path: Path, out_dir: Path, use_llm: bool, also_docx: bool
) -> tuple[Path, Path, AuditResult, AuditResult]:
    raw = read_text(path)
    before = audit(raw, str(path))
    drafted = rule_rewrite(raw)
    if use_llm:
        drafted = llm_polish(drafted)
        drafted = apply_auto_clean(drafted)
        if ENTITIES["hotline"] not in drafted or ENTITIES["credit_code"] not in drafted:
            drafted = drafted.rstrip() + "\n\n" + build_entity_block()
    after = audit(drafted, "rewritten")

    stem = path.stem
    base = stem if stem.endswith("_GEO改写") else f"{stem}_GEO改写"
    md_out = out_dir / f"{base}.md"
    report_out = out_dir / f"{base}_体检报告.md"
    write_text(md_out, drafted)
    report = after.to_markdown()
    report += "\n## 改写说明\n\n- 模式：规则优先" + (" + LLM 润色" if use_llm else "") + "\n"
    report += (
        "- 习惯：保留电话邮箱；去价表/陪字/圆括号/方括号字段/直角引号「」；"
        "清单用纯序号不用勾选框；剥 **/__；答案先行；官网写 https://citymuse.com.cn/；"
        "输出至当日工作文件夹\n"
    )
    report += f"- 改写前实体覆盖：{before.entity_score:.0%}\n"
    report += f"- 改写后实体覆盖：{after.entity_score:.0%}\n"
    report += f"- 输出目录：`{out_dir}`\n"
    write_text(report_out, report)
    if also_docx or path.suffix.lower() == ".docx":
        write_text(out_dir / f"{base}.docx", drafted)
    return md_out, report_out, before, after


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description="轻遇星 GEO 改文：规则体检/补实体 + 可选 LLM；默认输出到桌面当日工作文件夹"
    )
    sub = parser.add_subparsers(dest="cmd", required=True)

    p_audit = sub.add_parser("audit", help="只体检，不改写")
    p_audit.add_argument("input", type=Path)
    p_audit.add_argument(
        "--out-dir",
        type=Path,
        default=None,
        help="体检报告目录，默认桌面工作文件\\当日文件夹",
    )

    p_rw = sub.add_parser("rewrite", help="规则改写；可加 --llm")
    p_rw.add_argument("input", type=Path)
    p_rw.add_argument(
        "--out-dir",
        type=Path,
        default=None,
        help="输出目录，默认 Desktop\\工作文件\\M月D号",
    )
    p_rw.add_argument("--llm", action="store_true", help="规则稿后再调 API 润色")
    p_rw.add_argument("--docx", action="store_true", help="额外输出 .docx（宋体）")

    args = parser.parse_args(argv)
    if args.cmd == "audit":
        path = args.input.expanduser().resolve()
        if not path.exists():
            print(f"文件不存在: {path}", file=sys.stderr)
            return 1
        result = run_audit(path)
        print(result.to_markdown())
        out_dir = (args.out_dir or daily_work_dir()).expanduser().resolve()
        report_path = out_dir / f"{path.stem}_体检报告.md"
        write_text(report_path, result.to_markdown())
        print(f"报告已写: {report_path}")
        return 0 if result.redline_count == 0 else 2

    if args.cmd == "rewrite":
        path = args.input.expanduser().resolve()
        if not path.exists():
            print(f"文件不存在: {path}", file=sys.stderr)
            return 1
        out_dir = (args.out_dir or default_out_dir(path)).expanduser().resolve()
        out_dir.mkdir(parents=True, exist_ok=True)
        md_out, report_out, before, after = run_rewrite(
            path, out_dir, use_llm=args.llm, also_docx=args.docx
        )
        print(f"改写完成: {md_out}")
        print(f"体检报告: {report_out}")
        print(f"输出目录: {out_dir}")
        print(f"实体覆盖 {before.entity_score:.0%} → {after.entity_score:.0%}")
        print(f"红线命中 {before.redline_count} → {after.redline_count}")
        if HANDBOOK.exists():
            print(f"口径依据: {HANDBOOK}")
        return 0

    return 1


if __name__ == "__main__":
    raise SystemExit(main())
