DM-Agent / src /main.py
unknown
refactor: consolidate repo structure, remove upload_to_github duplication
3291500
Raw History Blame Contribute Delete
9.19 kB
"""
入口:
--web 网页界面(推荐):浏览器打开,点击即用
--chat 终端交互模式(默认仍会推钉钉,加 --no-ding 仅本机预览)
--run-once 单次分析最新一周并推送(加 --no-ding 仅打印,不调钉钉)
--dry-run / --preview 只生成报告输出到终端(不推送)
(无参数) 常驻定时任务
"""
import argparse
import logging
import sys
from pathlib import Path
_root = Path(__file__).resolve().parent.parent
if str(_root) not in sys.path:
sys.path.insert(0, str(_root))
from src.analyzer import run_analysis, run_monthly_analysis
from src.dingtalk import send_markdown
from src.formatter import (
format_report, format_title,
format_monthly_report, format_monthly_title,
)
from src.loader import load_config, load_weekly_data, get_available_weeks
from src.parser import parse_query, describe_query, HELP_TEXT
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler(_root / "logs" / "agent.log", encoding="utf-8"),
],
)
log = logging.getLogger("ad-agent")
def _configure_stdout_utf8() -> None:
"""避免 Windows 控制台默认 GBK 打印周报里的 emoji 时崩溃。"""
try:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
def _do_analysis(params: dict, config_path: Path) -> tuple[str, str]:
"""根据解析参数执行分析,返回 (title, markdown_body)。"""
mode = params["mode"]
if mode == "month":
result = run_monthly_analysis(
config_path=config_path,
target_year=params["year"],
target_month=params["month"],
)
if result.get("error"):
return "数据不足", result["error"]
return format_monthly_title(result), format_monthly_report(result)
year, week = None, None
if mode == "week":
year, week = params["year"], params["week"]
elif mode == "prev":
df = load_weekly_data(config_path)
weeks = get_available_weeks(df)
if len(weeks) >= 2:
year, week = weeks[-2]
result = run_analysis(config_path=config_path, target_year=year, target_week=week)
return format_title(result), format_report(result)
def run_once(
config_path: Path | None = None,
target_year: int | None = None,
target_week: int | None = None,
*,
send_ding: bool = True,
) -> bool:
config_path = config_path or _root / "config.yaml"
log.info("开始周度分析...")
try:
result = run_analysis(
config_path=config_path,
target_year=target_year,
target_week=target_week,
)
except Exception as e:
log.error(f"分析失败: {e}", exc_info=True)
return False
title = format_title(result)
text = format_report(result)
if not send_ding:
print("\n" + "=" * 64)
print(f"【终端预览 · 未推送钉钉】{title}")
print("=" * 64 + "\n")
print(text)
print("\n" + "=" * 64)
log.info("已输出到终端(预览模式,未调用钉钉)。")
return True
ok, err = send_markdown(title=title, text=text)
if not ok:
log.error(f"钉钉发送失败: {err}")
print(text)
else:
log.info(f"已发送: {title}")
return ok
def chat_loop(config_path: Path, *, send_ding: bool = True) -> None:
"""本地交互模式:读取用户输入 → 分析 → 可选推送到钉钉群。"""
print("=" * 50)
print(" DM 广告数据分析 Agent(交互模式)")
print("=" * 50)
print()
if send_ding:
print("输入分析指令,报告将通过钉钉 Webhook 推送到群。")
else:
print("【调试模式】报告只打印在本终端,不会推送到钉钉。")
print("支持的指令:")
print(" 分析最新一周 / 分析上周")
print(" 分析25年12月 / 分析2025年12月")
print(" 分析W9 / 分析第9周 / 分析2025 W40")
print(" exit / quit 退出")
print()
while True:
try:
user_input = input(">>> ").strip()
except (EOFError, KeyboardInterrupt):
print("\n再见!")
break
if not user_input:
continue
if user_input.lower() in ("exit", "quit", "q", "退出"):
print("再见!")
break
if user_input in ("帮助", "help", "?", "?"):
print(HELP_TEXT)
continue
params = parse_query(user_input)
if params is None:
print(f"无法识别「{user_input}」,请输入「帮助」查看支持的指令。")
continue
desc = describe_query(params)
print(f"正在分析: {desc} ...")
try:
title, body = _do_analysis(params, config_path)
except Exception as e:
print(f"分析出错: {e}")
continue
print(f"报告已生成: {title}({len(body)} 字符)")
if not send_ding:
print("\n--- 报告正文(未推送钉钉)---\n")
print(body)
print("\n--- 结束 ---\n")
continue
ok, err = send_markdown(title=title, text=body)
if ok:
print(f"已推送到钉钉群。")
else:
print(f"钉钉推送失败: {err}")
print("--- 报告内容 ---")
print(body)
print("---")
print()
def main() -> None:
_configure_stdout_utf8()
parser = argparse.ArgumentParser(description="广告数据周度分析 Agent")
parser.add_argument("--web", action="store_true", help="启动网页界面(推荐)")
parser.add_argument("--chat", action="store_true", help="终端交互模式")
parser.add_argument("--run-once", action="store_true", help="分析最新一周并推送钉钉(加 --no-ding 则只打印到终端)")
parser.add_argument("--no-ding", action="store_true", help="不调用钉钉,仅终端输出(配合 --run-once 或 --chat)")
parser.add_argument("--config", type=Path, default=None, help="config.yaml 路径")
parser.add_argument("--week", type=str, default=None, help="指定分析周 (YYYY-W)")
parser.add_argument("--month", type=str, default=None, help="指定分析月 (YYYY-M)")
parser.add_argument("--dry-run", action="store_true", help="只输出报告到终端,不推送钉钉")
parser.add_argument("--preview", action="store_true", help="同 --dry-run,便于记")
args = parser.parse_args()
config_path = args.config or _root / "config.yaml"
# ── 网页界面 ──
if args.web:
from src.web import start_web
start_web()
return
# ── 终端交互模式 ──
if args.chat:
chat_loop(config_path, send_ding=not args.no_ding)
return
# ── 解析时间参数 ──
target_year, target_week, target_month = None, None, None
if args.week:
parts = args.week.split("-")
target_year, target_week = int(parts[0]), int(parts[1])
if args.month:
parts = args.month.split("-")
target_year, target_month = int(parts[0]), int(parts[1])
# ── Dry-run / Preview ──
if args.dry_run or args.preview:
if target_month is not None:
result = run_monthly_analysis(
config_path=config_path,
target_year=target_year,
target_month=target_month,
)
title = format_monthly_title(result)
text = format_monthly_report(result)
else:
result = run_analysis(
config_path=config_path,
target_year=target_year,
target_week=target_week,
)
title = format_title(result)
text = format_report(result)
print(f"=== {title} ===")
print(text)
return
# ── Run-once ──
if args.run_once:
run_once(
config_path=config_path,
target_year=target_year,
target_week=target_week,
send_ding=not args.no_ding,
)
return
# ── 定时任务 ──
from apscheduler.schedulers.blocking import BlockingScheduler
from apscheduler.triggers.cron import CronTrigger
config = load_config(config_path)
cron_expr = (config.get("schedule", {}).get("weekly_cron") or "0 9 * * 1").strip()
parts = cron_expr.split()
if len(parts) != 5:
minute, hour, day, month, dow = "0", "9", "*", "*", "1"
else:
minute, hour, day, month, dow = parts
scheduler = BlockingScheduler()
scheduler.add_job(
run_once,
CronTrigger(minute=minute, hour=hour, day=day, month=month, day_of_week=dow),
args=[config_path],
id="weekly_report",
)
log.info(f"定时任务已启动,cron: {cron_expr}。按 Ctrl+C 退出。")
scheduler.start()
if __name__ == "__main__":
main()