mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
199 lines
6.9 KiB
Python
199 lines
6.9 KiB
Python
"""一键简报包:ZIP 内含完整 Markdown 报告、结构化 JSON、要点摘录。"""
|
||
from __future__ import annotations
|
||
|
||
import io
|
||
import json
|
||
import zipfile
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
|
||
def _pct(x: Any) -> str:
|
||
if x is None:
|
||
return "—"
|
||
try:
|
||
return f"{100 * float(x):.1f}%"
|
||
except (TypeError, ValueError):
|
||
return str(x)
|
||
|
||
|
||
def _num(x: Any) -> str:
|
||
if x is None:
|
||
return "—"
|
||
if isinstance(x, (int, float)):
|
||
if isinstance(x, float) and x != int(x):
|
||
return f"{x:.2f}"
|
||
return str(int(x)) if isinstance(x, float) and x == int(x) else str(x)
|
||
return str(x)
|
||
|
||
|
||
def markdown_summary_from_brief(brief: dict[str, Any]) -> str:
|
||
"""由 ``competitor-brief`` JSON 生成便于扫读的 Markdown(非 LLM)。"""
|
||
lines: list[str] = [
|
||
"# 竞品要点摘录(机器整理)",
|
||
"",
|
||
"> 与同批 **完整报告**、**结构化 JSON** 同源;规则汇总,定稿前请人工核对。",
|
||
"",
|
||
]
|
||
kw = brief.get("keyword") or "—"
|
||
batch = brief.get("batch_label") or "—"
|
||
lines.extend(
|
||
[
|
||
"## 基本信息",
|
||
"",
|
||
f"- **监测词**:{kw}",
|
||
f"- **批次**:{batch}",
|
||
"",
|
||
]
|
||
)
|
||
|
||
scope = brief.get("scope") or {}
|
||
if scope:
|
||
lines.extend(
|
||
[
|
||
"## 样本范围",
|
||
"",
|
||
f"- **深入 SKU 数**:{_num(scope.get('merged_sku_count'))}",
|
||
f"- **评价条数(扁平)**:{_num(scope.get('comment_flat_rows'))}",
|
||
f"- **结构分析用列表行数**:{_num(scope.get('structure_source_rows'))}",
|
||
f"- **是否含 PC 搜索全量导出**:{'是' if scope.get('uses_pc_search_list_export') else '否'}",
|
||
"",
|
||
]
|
||
)
|
||
|
||
raw = brief.get("pc_search_raw") or {}
|
||
if raw.get("result_count_consensus") is not None:
|
||
lines.extend(
|
||
[
|
||
"## 列表侧检索规模(接口申报)",
|
||
"",
|
||
f"- **resultCount 共识值**:{_num(raw.get('result_count_consensus'))}",
|
||
"",
|
||
]
|
||
)
|
||
|
||
conc = brief.get("concentration") or {}
|
||
shops = conc.get("shops_from_list") or {}
|
||
if shops.get("cr1") is not None or shops.get("top_label"):
|
||
lines.extend(
|
||
[
|
||
"## 店铺集中度(列表)",
|
||
"",
|
||
f"- **第一大店铺份额**:{_pct(shops.get('cr1'))}(第一店铺:{shops.get('top_label') or '—'})",
|
||
f"- **前三店铺合计份额**:{_pct(shops.get('cr3'))}",
|
||
"",
|
||
]
|
||
)
|
||
dbrand = conc.get("detail_brand_among_merged") or {}
|
||
if dbrand.get("cr1") is not None or dbrand.get("top_label"):
|
||
lines.extend(
|
||
[
|
||
"## 品牌(深入样本)",
|
||
"",
|
||
f"- **第一大品牌份额(深入样本)**:{_pct(dbrand.get('cr1'))}(头部:{dbrand.get('top_label') or '—'})",
|
||
f"- **前三品牌合计份额**:{_pct(dbrand.get('cr3'))}",
|
||
"",
|
||
]
|
||
)
|
||
|
||
pst = brief.get("price_stats") or {}
|
||
if pst.get("n"):
|
||
src = brief.get("price_stats_source") or "—"
|
||
lines.extend(
|
||
[
|
||
"## 价格(展示价统计)",
|
||
"",
|
||
f"- **样本量 n**:{_num(pst.get('n'))};**统计口径**:{src}",
|
||
f"- **区间**:{_num(pst.get('min'))} ~ {_num(pst.get('max'))};**中位数**:{_num(pst.get('median'))}",
|
||
"",
|
||
]
|
||
)
|
||
|
||
mix = brief.get("category_mix_top") or []
|
||
if mix:
|
||
lines.extend(["## 类目结构(Top)", ""])
|
||
for item in mix[:8]:
|
||
if isinstance(item, dict):
|
||
lines.append(
|
||
f"- {item.get('label') or '—'}:{_num(item.get('count'))}"
|
||
)
|
||
lines.append("")
|
||
|
||
ckw = brief.get("comment_focus_keywords") or []
|
||
if ckw:
|
||
lines.extend(["## 评价关注词(Top)", ""])
|
||
for item in ckw[:10]:
|
||
if isinstance(item, dict):
|
||
lines.append(
|
||
f"- **{item.get('word') or '—'}**:{_num(item.get('count'))} 次"
|
||
)
|
||
lines.append("")
|
||
|
||
usc = brief.get("usage_scenarios") or []
|
||
if usc:
|
||
lines.extend(["## 用途/场景(预设词组,Top)", ""])
|
||
for item in usc[:8]:
|
||
if isinstance(item, dict):
|
||
lines.append(
|
||
f"- **{item.get('scenario') or '—'}**:{_num(item.get('count'))} 条(约 {_pct(item.get('share_of_text_units'))} 文本单元)"
|
||
)
|
||
lines.append("")
|
||
|
||
hints = brief.get("strategy_hints") or []
|
||
if hints:
|
||
lines.extend(["## 策略提示(规则)", ""])
|
||
for h in hints:
|
||
lines.append(f"- {h}")
|
||
lines.append("")
|
||
|
||
lines.extend(
|
||
[
|
||
"---",
|
||
"",
|
||
"*更细的矩阵与消费者反馈见简报包内完整分析报告;结构化字段见同包内摘要数据文件。*",
|
||
"",
|
||
]
|
||
)
|
||
return "\n".join(lines)
|
||
|
||
|
||
README_TXT = """竞品「一键简报包」说明(Market-Assistant)
|
||
============================================
|
||
|
||
本 ZIP 由「报告查看」页一键导出,内含:
|
||
|
||
说明文件 — 本文件
|
||
完整分析报告 — 与任务批次中的主报告文稿一致
|
||
统计图 PNG — report_assets 目录(与报告内插图同源)
|
||
结构化摘要数据 — 与「结构化摘要」接口同源,可供其它工具读取
|
||
要点摘录 — 由摘要自动整理的速读稿,便于邮件/转发前浏览
|
||
|
||
使用建议:对外发送前请核对要点摘录与完整报告中的结论;数据边界见报告第一章。
|
||
"""
|
||
|
||
|
||
def build_brief_pack_zip_bytes(run_dir: Path, brief: dict[str, Any]) -> bytes:
|
||
"""
|
||
生成 ZIP 字节流。``run_dir`` 下须存在 ``competitor_analysis.md``。
|
||
"""
|
||
run_dir = Path(run_dir).resolve()
|
||
report_path = run_dir / "competitor_analysis.md"
|
||
if not report_path.is_file():
|
||
raise FileNotFoundError("缺少已生成的分析报告文件,请先在「报告生成」中生成报告")
|
||
|
||
buf = io.BytesIO()
|
||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
||
zf.writestr("01_竞品分析报告.md", report_path.read_text(encoding="utf-8"))
|
||
zf.writestr(
|
||
"02_结构化摘要.json",
|
||
json.dumps(brief, ensure_ascii=False, indent=2),
|
||
)
|
||
zf.writestr("03_要点摘录.md", markdown_summary_from_brief(brief))
|
||
zf.writestr("00_说明.txt", README_TXT)
|
||
assets = run_dir / "report_assets"
|
||
if assets.is_dir():
|
||
for fp in sorted(assets.iterdir()):
|
||
if fp.is_file():
|
||
zf.write(fp, f"report_assets/{fp.name}")
|
||
return buf.getvalue()
|