Files
inspiration-collector/analyzers/daily_digest.py
2026-06-14 09:21:27 +08:00

279 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Daily digest analyzer - fetches today's memos and generates AI analysis."""
import json
import logging
import os
import sys
import subprocess
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
from tools.memos_client import MemosClient
from tools.formatter import format_daily_digest
logger = logging.getLogger(__name__)
TZ_BEIJING = timezone(timedelta(hours=8))
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def utc_to_beijing(ts_str):
"""Convert UTC timestamp string (ISO 8601 with Z) to Beijing time string."""
try:
dt = datetime.fromisoformat(ts_str.replace("Z", "+00:00"))
dt_bj = dt.astimezone(TZ_BEIJING)
return dt_bj.strftime("%Y-%m-%d %H:%M")
except (ValueError, AttributeError):
return ts_str[:16].replace("T", " ") + " (UTC?)"
def read_previous_digest(date):
"""Read the most recent previous daily digest (if exists) for continuity."""
daily_root = get_output_dir("daily")
# Try yesterday first, then go back up to 7 days
for days_back in range(1, 8):
prev_date = date - timedelta(days=days_back)
prev_dir = os.path.join(daily_root, prev_date.strftime("%Y-%m-%d"))
if not os.path.isdir(prev_dir):
continue
files = sorted([f for f in os.listdir(prev_dir) if f.endswith(".md")], reverse=True)
if not files:
continue
prev_file = os.path.join(prev_dir, files[0])
with open(prev_file, "r", encoding="utf-8") as f:
content = f.read()
# Extract just the AI analysis section, skip frontmatter
ai_section_start = content.find("## AI 分析")
if ai_section_start != -1:
# Find the "我的批注" section boundary
annotation_start = content.find("## 我的批注", ai_section_start)
if annotation_start != -1:
ai_section = content[ai_section_start:annotation_start].strip()
else:
ai_section = content[ai_section_start:].strip()
logger.info(
"Found previous digest from %s (%d chars)",
prev_date.strftime("%Y-%m-%d"), len(ai_section)
)
return prev_date, ai_section
else:
# No AI section found, return whole file
logger.info(
"Found previous digest from %s (no AI section found, using full)",
prev_date.strftime("%Y-%m-%d")
)
return prev_date, content
logger.info("No previous digest found in the last 7 days")
return None, None
# ============================================================
# System prompt for daily analysis
# ============================================================
DAILY_SYSTEM_PROMPT = """你是一个私人思考伙伴。你的任务是认真阅读用户今天的每一条灵感记录,结合之前的分析,写一篇有深度的分析文章。
注意:所有时间戳都是**北京时间**UTC+8不是 UTC。用户在中国记录和活动时间均以北京时间为准。
核心原则:不做缩写,不做分类表,不写空话。
具体要求:
1. **引用原文**:分析每一条灵感时,必须先引用(或精炼复述)用户的原文,让用户一眼就知道"哦我在说这个"。引用要压缩但不失原意,不能断章取义。
2. **融会贯通,而非逐条罗列**:不要"第一条...第二条...第三条..."机械堆砌。要把所有灵感当作一个整体来思考——哪几条在说同一个主题?哪几条看似无关实则互补?把它们串起来写。文章是流淌的整体,不是并列的零件。
3. **承接延续之前的分析**:你会看到上一次的分析内容。今天的分析不能只写"今天的事",要把今天的灵感和昨天的分析结合起来——昨天讨论了什么?今天有什么进展?哪些问题有了答案?哪些问题还在延续?要让文章有跨日的时间纵深感。
4. **逐渐深入,有节奏感**:从表面现象往下挖。先指出用户说了什么,再追问为什么,再展开你的洞察。每一段都要比上一段深一层。
5. **有实质内容**:不要写"这是一个有价值的思考"这种废话。要说清楚为什么有价值,值在哪里,用户可以从这个方向挖到什么。
6. **关联要真实**:如果多条灵感确实指向同一个主题,就写一段贯通的分析。不要硬凑关联。
7. **待办要锋利**:待办事项不是"调研X""优化Y"这种万金油。要写到"具体做什么、什么时机做、做到什么程度"的颗粒度。
输出格式:纯 Markdown不要代码块包裹不要 JSON。
文章结构参考(不是模板,不必严格遵守):
### 1. 今日概览
一两句话点出今天思考的主旋律,并与之前的内容形成呼应。
### 2. 深入分析
融会贯通地写。引用原文要自然嵌入行文。注意与上次分析的承接关系。
### 3. 待办事项
具体、可执行的事项列表。
---
注意即便只有1条灵感也要写出深度。宁可写长不可简略。"""
def git_push(digest_file):
"""Commit and push the digest file to Gitea."""
try:
# git add
result = subprocess.run(
["git", "add", digest_file],
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
# git commit (check for changes first)
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
date_str = datetime.now(TZ_BEIJING).strftime("%Y-%m-%d %H:%M")
result = subprocess.run(
["git", "commit", "-m", "daily digest " + date_str],
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
# git push
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
# Extract remote result line
for line in result.stdout.split("\n"):
if "->" in line or "remote:" in line:
logger.info("Gitea push: %s", line.strip())
logger.info("Pushed to Gitea successfully")
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(memos_client, llm_client, date=None):
"""Run daily digest analysis."""
date = date or datetime.now(TZ_BEIJING)
logger.info("Daily digest started for %s", date.strftime("%Y-%m-%d"))
# Step 1: Fetch today's memos
memos = memos_client.list_memos(days=1)
if not memos:
logger.info("No memos today, skipping")
content = format_daily_digest(date, ai_body="今天没有记录灵感。", tags=["灵感收集器", "每日总结", "静默"], doc_type="daily-digest")
date_str = date.strftime("%Y-%m-%d")
daily_dir = os.path.join(get_output_dir("daily"), date_str)
os.makedirs(daily_dir, exist_ok=True)
now_str = datetime.now(TZ_BEIJING).strftime("%H%M%S")
filename = date_str + "_digest_" + now_str + ".md"
filepath = os.path.join(daily_dir, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
logger.info("Empty digest written to %s", filepath)
# Still push
git_push(filepath)
return filepath, 0
# Step 2: Prepare user prompt
memo_texts = []
for m in memos:
time_bj = utc_to_beijing(m["created_at"])
memo_texts.append("- **[" + time_bj + "]** " + m["content"].strip())
user_prompt = "以下是我今天的灵感记录:\n\n" + "\n\n".join(memo_texts)
# Step 3: Include previous analysis for continuity
prev_date, prev_analysis = read_previous_digest(date)
if prev_analysis:
user_prompt += (
"\n\n---\n\n"
"以下是我上一次的分析内容("
+ prev_date.strftime("%Y-%m-%d") + ""
"请结合今天的灵感一起思考,保持连续性:\n\n"
+ prev_analysis
)
user_prompt += "\n\n---\n\n请基于以上所有素材,写一篇有深度的分析文章。"
# Step 4: Call DeepSeek API
raw_response = llm_client.ask(
system_prompt=DAILY_SYSTEM_PROMPT,
user_prompt=user_prompt,
temperature=0.5
)
# Step 5: Wrap with frontmatter and write
content = format_daily_digest(date, ai_body=raw_response, tags=["灵感收集器", "每日总结", "AI分析"], doc_type="daily-digest")
date_str = date.strftime("%Y-%m-%d")
daily_dir = os.path.join(get_output_dir("daily"), date_str)
os.makedirs(daily_dir, exist_ok=True)
now_str = datetime.now(TZ_BEIJING).strftime("%H%M%S")
filename = date_str + "_digest_" + now_str + ".md"
filepath = os.path.join(daily_dir, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
logger.info(
"Daily digest written to %s | %d memos | %d chars analysis",
filepath, len(memos), len(raw_response)
)
# Step 6: Auto-push to Gitea
git_push(filepath)
return filepath, len(memos)
def main():
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)
memos_client = MemosClient(
base_url=secrets.get("memos_url", "http://localhost:5230"),
access_token=secrets["memos_token"]
)
llm_client = DeepSeekClient(
api_key=secrets["deepseek_api_key"],
model=secrets.get("deepseek_model", "deepseek-chat")
)
filepath, count = run(memos_client, llm_client)
print("Done: " + filepath + " (" + str(count) + " memos)")
if __name__ == "__main__":
main()