diff --git a/analyzers/weekly_digest.py b/analyzers/weekly_digest.py new file mode 100644 index 0000000..dce5672 --- /dev/null +++ b/analyzers/weekly_digest.py @@ -0,0 +1,367 @@ +"""Weekly digest synthesizer - reads all daily digests and generates a week-level analysis.""" + +import logging +import os +import subprocess +import sys +from datetime import datetime, timezone, timedelta + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from tools.config import load_secrets, get_output_dir +from tools.llm import DeepSeekClient + +logger = logging.getLogger(__name__) + +TZ_BEIJING = timezone(timedelta(hours=8)) +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +WEEKLY_SYSTEM_PROMPT = """你是一个私人思考伙伴。你的任务是认真阅读用户过去一周的7份日报,写一篇有深度的周度分析报告。 + +## 核心原则 + +你读的是日报——不是原始灵感记录,而是已经经过每日分析提炼的内容。你的工作是: + +1. **提炼跨日主线**:7份日报各自都有"今日概览"——把它们串起来。这周的主旋律是什么?从周一到现在,思考的焦点如何演变? + +2. **追踪延续与断裂**:日报中反复出现的标签(如 #浙江培训 #交易 #灵感收集器)代表持续性的主题。哪些主题从头贯穿到尾?哪些在中间消失了?为什么? + +3. **识别本周的关键跃迁**:在7天的思考中,哪一天出现了认知上的质变?哪一个洞察是本周最核心的收获? + +4. **不逐日罗列**:不要"周一...周二...周三..."。要融会贯通,把7天当作一个整体来写。按主题组织,不是按时间组织。 + +5. **深度优先**:每周分析应该比每日分析更深一层。如果日报是在追问"今天发生了什么",周报应该回答"这一周意味着什么"。 + +## 输出格式 + +纯 Markdown,不要代码块包裹。 + +结构参考: + +### 1. 本周概览 +一两句话点出这周的底色,并指出最核心的1-2条暗线(可能不是最显眼的,但是对用户最具启发价值的)。 + +### 2. 深入分析 +按主题组织,融会贯通地写。可以包含但不限于: +- 持续性主题的演变轨迹 +- 认知跃迁的关键时刻 +- 看似无关事件之间的深层关联 +- 本周形成的新规则、新认知、新能力 +- 与用户长期目标/身份认同的关系 + +引用日报内容要自然嵌入行文,让用户知道你在引用哪一天的观察。 + +### 3. 待办事项 +基于本周分析,提出需要延续到下周的具体行动。要写到"什么时机做什么做到什么程度"的颗粒度。 + +--- + +注意:即便7份日报中有些比较简短,也要从整体中挖掘深度。篇幅不设上限。""" + + +def read_week_digests(week_start, week_end): + """Read all daily digest files from week_start to week_end. + + Returns list of (date_str, digest_text, tags) tuples. + """ + daily_root = get_output_dir("daily") + digests = [] + + current = week_start + while current <= week_end: + date_str = current.strftime("%Y-%m-%d") + daily_dir = os.path.join(daily_root, date_str) + + if os.path.isdir(daily_dir): + # Find the latest digest file for this day + files = sorted([ + f for f in os.listdir(daily_dir) + if f.endswith(".md") and "digest" in f and not f.endswith(".bak.md") + ], reverse=True) + + if files: + filepath = os.path.join(daily_dir, files[0]) + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + + # Extract AI analysis section + ai_start = content.find("## AI 分析") + annotation_start = content.find("## 我的批注", ai_start) if ai_start != -1 else -1 + + if ai_start != -1 and annotation_start != -1: + ai_body = content[ai_start:annotation_start].strip() + elif ai_start != -1: + ai_body = content[ai_start:].strip() + else: + ai_body = content + + # Extract tags from frontmatter + tags = [] + if "tags:" in content[:500]: + tag_line = content.split("tags:")[1].split("\n")[0] + import re + tags = re.findall(r"'([^']+)'", tag_line) + + digests.append((date_str, ai_body, tags)) + logger.info( + "Read digest %s: %d chars, tags=%s", + date_str, len(ai_body), ", ".join(tags[:5]) + ) + else: + logger.info("No digest file found for %s (skipped)", date_str) + else: + logger.info("No daily dir for %s (skipped)", date_str) + + current += timedelta(days=1) + + return digests + + +def build_weekly_prompt(digests): + """Build the user prompt containing all 7 daily digest bodies.""" + + if not digests: + return None + + # Extract week range + first_date = digests[0][0] + last_date = digests[-1][0] + + prompt_parts = [ + f"以下是我从 {first_date} 到 {last_date} 共7天的日报。", + f"共 {len(digests)} 份日报。", + "", + "请基于这些日报内容,写一篇周度分析报告。" + ] + + # Collect all unique tags across the week + all_tags = set() + for _, _, tags in digests: + all_tags.update(tags) + if all_tags: + tag_str = "、".join(f"#{t}" for t in sorted(all_tags)) + prompt_parts.append(f"\n本周出现的标签:{tag_str}") + + prompt_parts.append("\n---\n") + + for date_str, body, tags in digests: + # Show a weekday indicator + dt = datetime.strptime(date_str, "%Y-%m-%d") + weekday = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"][dt.weekday()] + tag_hint = "" + if tags: + important_tags = [t for t in tags if t not in ("灵感收集器", "每日总结", "AI分析")] + if important_tags: + tag_hint = " | 标签:" + ", ".join(f"#{t}" for t in important_tags[:5]) + + prompt_parts.append(f"\n## {date_str}({weekday}){tag_hint}\n") + prompt_parts.append(body) + prompt_parts.append("\n") + + prompt_parts.append("\n---\n") + prompt_parts.append("请基于以上所有日报内容,写一篇有深度的周度分析报告。") + + return "\n".join(prompt_parts) + + +def format_weekly_digest(week_start, week_end, ai_body, tags=None): + """Format weekly digest markdown with frontmatter, matching daily digest format.""" + week_start_str = week_start.strftime("%Y-%m-%d") + week_end_str = week_end.strftime("%m/%d") + week_number = week_start.isocalendar()[1] + start_month = week_start.strftime("%m/%d") + + char_count = len(ai_body.replace(" ", "").replace("\n", "")) + read_min = max(1, round(char_count / 300)) + + if tags is None: + tags = ["灵感收集器", "每周总结"] + tag_str = ", ".join(["'" + t + "'" for t in tags]) + + lines = [] + lines.append("---") + lines.append(f"date: {week_start.strftime('%Y')}-W{week_number}") + lines.append("type: weekly-digest") + lines.append(f"tags: [{tag_str}]") + lines.append("---") + lines.append("") + lines.append(f"# 第{week_number}周 · 周度总结({start_month} - {week_end_str})") + lines.append("") + lines.append(f"> 本文共 **{char_count}** 字 · 预计阅读 **{read_min}** 分钟") + lines.append("") + lines.append("> 基于本周7份日报综合生成。") + lines.append("") + lines.append("## AI 分析(自动生成,请勿编辑)") + lines.append("") + lines.append(ai_body) + lines.append("") + lines.append("---") + lines.append("") + lines.append("## 我的批注") + lines.append("") + lines.append("> *在这里写下你的想法、质疑、补充*") + lines.append("") + + return "\n".join(lines) + + +def git_push_weekly(filepath): + """Commit and push the weekly digest to Gitea.""" + try: + result = subprocess.run( + ["git", "add", filepath], + cwd=PROJECT_DIR, + capture_output=True, text=True, timeout=15 + ) + if result.returncode != 0: + logger.warning("git add failed: %s", result.stderr.strip()) + return False + + result = subprocess.run( + ["git", "status", "--porcelain", "ai-insights/"], + cwd=PROJECT_DIR, + capture_output=True, text=True, timeout=10 + ) + if not result.stdout.strip(): + logger.info("No changes to commit") + return True + + week_num = datetime.now(TZ_BEIJING).isocalendar()[1] + result = subprocess.run( + ["git", "commit", "-m", f"weekly digest W{week_num}"], + cwd=PROJECT_DIR, + capture_output=True, text=True, timeout=15 + ) + if result.returncode != 0 and "nothing to commit" not in result.stdout: + logger.warning("git commit failed: %s", result.stderr.strip()) + return False + + result = subprocess.run( + ["git", "push", "origin", "main"], + cwd=PROJECT_DIR, + capture_output=True, text=True, timeout=30 + ) + if result.returncode != 0: + logger.warning("git push failed: %s", result.stderr.strip()) + return False + + for line in result.stdout.split("\n"): + if "->" in line or "remote:" in line: + logger.info("Gitea push: %s", line.strip()) + logger.info("Pushed weekly digest to Gitea") + return True + + except subprocess.TimeoutExpired: + logger.error("git push timed out") + return False + except Exception as e: + logger.error("git push error: %s", e) + return False + + +def run(llm_client, date=None): + """Run weekly digest synthesis. + + Reads all daily digests from the past 7 days (Mon-Sun), synthesizes + a weekly report via DeepSeek API, and saves to ai-insights/weekly/. + """ + date = date or datetime.now(TZ_BEIJING) + + # Calculate week range (Monday to Sunday) + weekday = date.weekday() # 0=Monday, 6=Sunday + week_start = date - timedelta(days=weekday) # This Monday + week_end = week_start + timedelta(days=6) # This Sunday + + logger.info( + "Weekly digest started for W%d (%s - %s)", + week_start.isocalendar()[1], + week_start.strftime("%Y-%m-%d"), + week_end.strftime("%Y-%m-%d") + ) + + # Read all daily digests + digests = read_week_digests(week_start, week_end) + + if not digests: + logger.info("No daily digests found for this week, skipping") + return None, 0 + + logger.info("Found %d daily digests to synthesize", len(digests)) + + # Build prompt + user_prompt = build_weekly_prompt(digests) + if user_prompt is None: + logger.error("Failed to build weekly prompt") + return None, 0 + + # Call DeepSeek API + raw_response = llm_client.ask( + system_prompt=WEEKLY_SYSTEM_PROMPT, + user_prompt=user_prompt, + temperature=0.5 + ) + + if not raw_response: + logger.error("Empty response from DeepSeek API") + return None, 0 + + # Collect all unique tags from the week + all_tags = set() + for _, _, tags in digests: + all_tags.update(tags) + # Add default weekly tags + default_tags = ["灵感收集器", "每周总结"] + weekly_tags = default_tags + sorted(all_tags) + + # Format output + content = format_weekly_digest(week_start, week_end, raw_response, weekly_tags) + + # Save to weekly directory + output_dir = get_output_dir("weekly") + week_start_str = week_start.strftime("%Y-%m-%d") + filename = f"W{week_start_str}_digest.md" + filepath = os.path.join(output_dir, filename) + + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + + logger.info( + "Weekly digest written to %s | %d digests | %d chars analysis | %d tags", + filepath, len(digests), len(raw_response), len(weekly_tags) + ) + + # Auto-push to Gitea + git_push_weekly(filepath) + + return filepath, len(digests) + + +def main(): + """CLI entry point.""" + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" + ) + + try: + secrets = load_secrets() + except FileNotFoundError as e: + print(e) + sys.exit(1) + + llm_client = DeepSeekClient( + api_key=secrets["deepseek_api_key"], + model=secrets.get("deepseek_model", "deepseek-chat") + ) + + filepath, count = run(llm_client) + if filepath: + print(f"Done: {filepath} ({count} digests synthesized)") + else: + print("No digests found, skipped") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_weekly_digest.sh b/scripts/run_weekly_digest.sh new file mode 100755 index 0000000..137afbe --- /dev/null +++ b/scripts/run_weekly_digest.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Weekly digest cron wrapper +# Run every Sunday at 08:00 UTC (16:00 Beijing) +# Synthesizes 7 daily digests into a week-level analysis report +# +# Scheduled earlier than the daily digest (22:00) to ensure +# the most recent daily digest has already been generated. + +set -e + +PROJECT_DIR="$HOME/inspiration-collector" +LOG_FILE="$PROJECT_DIR/ai-insights/logs/weekly_digest.log" + +mkdir -p "$(dirname "$LOG_FILE")" + +echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting weekly digest synthesis..." >> "$LOG_FILE" + +cd "$PROJECT_DIR" +/usr/bin/python3 analyzers/weekly_digest.py >> "$LOG_FILE" 2>&1 + +# Git push handled inside weekly_digest.py (git_push_weekly) +# Duplicate push here as safety net in case the script's push failed +cd "$PROJECT_DIR" +if [[ -n $(git status --porcelain ai-insights/) ]]; then + git add ai-insights/ + git commit -m "weekly digest W$(date '+%V') - safety push" + git push origin main 2>&1 | tail -3 >> "$LOG_FILE" + echo "Safety push done" >> "$LOG_FILE" +fi + +echo "[$(date '+%Y-%m-%d %H:%M:%S')] Weekly digest done." >> "$LOG_FILE"