fix: 阅读日报时间窗口修复 - 日历日改为24小时滚动窗口
- parse_time_from_line: 改为捕获完整 datetime (YYY-MM-DD HH:MM) - load_today_data: 过滤条件从 date_str 字符串匹配 改为 since_dt 时间戳比较 - 晚间22:00~23:59的阅读标记不再漏入次日日历间隙 - 经验:任何每日定时任务如果按日历日过滤,滚动窗口(过去N小时)比日历日更鲁棒
This commit is contained in:
315
daily_report_ai.py
Executable file
315
daily_report_ai.py
Executable file
@ -0,0 +1,315 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
daily_report_ai.py — AI 驱动的微信读书每日阅读报告
|
||||||
|
|
||||||
|
调用 DeepSeek API 对当日划线+批注进行深度分析,
|
||||||
|
生成有洞察的阅读报告,推送到 Gitea。
|
||||||
|
|
||||||
|
定时:每天 22:00(配合 sync.py 同步)
|
||||||
|
|
||||||
|
依赖:pip install requests
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
REPO_DIR = os.path.expanduser("~/weread-notes")
|
||||||
|
NOTES_DIR = os.path.join(REPO_DIR, "notes")
|
||||||
|
DAILY_DIR = os.path.join(REPO_DIR, "daily")
|
||||||
|
SYNC_SCRIPT = os.path.expanduser("~/weread-sync/sync.py")
|
||||||
|
API_KEY_FILE = os.path.expanduser("~/.keys/deepseek_api_key")
|
||||||
|
|
||||||
|
DEEPSEEK_URL = "https://api.deepseek.com/chat/completions"
|
||||||
|
DEEPSEEK_MODEL = "deepseek-chat"
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 读取今日数据 ────────────────────────────────────
|
||||||
|
|
||||||
|
def parse_time_from_line(line):
|
||||||
|
m = re.search(r'🕐\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})', line)
|
||||||
|
if m:
|
||||||
|
return datetime.strptime(m.group(1), "%Y-%m-%d %H:%M")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_today_data(date_str=None):
|
||||||
|
"""读取过去24小时所有划线+批注(滚动窗口,避免漏掉晚间阅读)"""
|
||||||
|
if not date_str:
|
||||||
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# 24小时滚动窗口:避免晚间阅读漏入次日日历间隙
|
||||||
|
since_dt = datetime.now() - timedelta(hours=24)
|
||||||
|
|
||||||
|
today_entries = []
|
||||||
|
|
||||||
|
for fname in os.listdir(NOTES_DIR):
|
||||||
|
if not fname.endswith(".md"):
|
||||||
|
continue
|
||||||
|
filepath = os.path.join(NOTES_DIR, fname)
|
||||||
|
book_title = fname[:-3]
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# 提取书籍元信息
|
||||||
|
meta = {"author": "", "progress": "", "category": ""}
|
||||||
|
for line in lines[:35]:
|
||||||
|
m = re.match(r'\|\s*\*\*作者\*\*\s*\|\s*(.+?)\s*\|', line)
|
||||||
|
if m: meta["author"] = m.group(1).strip()
|
||||||
|
m = re.match(r'\|\s*\*\*阅读进度\*\*\s*\|\s*(\d+)%\s*\|', line)
|
||||||
|
if m: meta["progress"] = f"{m.group(1)}%"
|
||||||
|
m = re.match(r'\|\s*\*\*分类\*\*\s*\|\s*(.+?)\s*\|', line)
|
||||||
|
if m: meta["category"] = m.group(1).strip()
|
||||||
|
|
||||||
|
# 解析时间戳行
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
ts_dt = parse_time_from_line(line)
|
||||||
|
if ts_dt is None:
|
||||||
|
continue
|
||||||
|
if ts_dt < since_dt:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
"book": book_title,
|
||||||
|
"author": meta["author"],
|
||||||
|
"progress": meta["progress"],
|
||||||
|
"category": meta["category"],
|
||||||
|
"time": line.strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 上一行是原文
|
||||||
|
if i > 0:
|
||||||
|
prev = lines[i - 1].strip()
|
||||||
|
entry["text"] = re.sub(r'^>\s*', '', prev).strip()
|
||||||
|
|
||||||
|
# 后续行找批注
|
||||||
|
for j in range(i + 1, min(i + 5, len(lines))):
|
||||||
|
t = lines[j].strip()
|
||||||
|
if t.startswith("💬"):
|
||||||
|
entry["thought"] = re.sub(r'^💬\s*\*{0,2}(.*?)\*{0,2}$', r'\1', t).strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
today_entries.append(entry)
|
||||||
|
|
||||||
|
return today_entries
|
||||||
|
|
||||||
|
|
||||||
|
def format_entries_for_prompt(entries):
|
||||||
|
"""将今日条目格式化为 AI 可读的文本"""
|
||||||
|
if not entries:
|
||||||
|
return "今日暂无阅读记录。"
|
||||||
|
|
||||||
|
# 按书名分组
|
||||||
|
from collections import defaultdict
|
||||||
|
by_book = defaultdict(list)
|
||||||
|
for e in entries:
|
||||||
|
by_book[e["book"]].append(e)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
for book_name in sorted(by_book.keys()):
|
||||||
|
items = by_book[book_name]
|
||||||
|
info = []
|
||||||
|
if items[0]["author"]:
|
||||||
|
info.append(f"作者:{items[0]['author']}")
|
||||||
|
if items[0]["category"]:
|
||||||
|
info.append(f"分类:{items[0]['category']}")
|
||||||
|
if items[0]["progress"]:
|
||||||
|
info.append(f"进度:{items[0]['progress']}")
|
||||||
|
|
||||||
|
parts.append(f"## 《{book_name}》")
|
||||||
|
if info:
|
||||||
|
parts.append("(" + " | ".join(info) + ")")
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
|
for e in items:
|
||||||
|
parts.append(f"### 划线")
|
||||||
|
parts.append(f"{e['text']}")
|
||||||
|
parts.append(f"时间:{e['time']}")
|
||||||
|
if e.get("thought"):
|
||||||
|
parts.append(f"")
|
||||||
|
parts.append(f"我的批注:{e['thought']}")
|
||||||
|
parts.append("")
|
||||||
|
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── AI 分析 ──────────────────────────────────────────
|
||||||
|
|
||||||
|
def call_deepseek(prompt, system_prompt):
|
||||||
|
"""调用 DeepSeek API"""
|
||||||
|
api_key_file = os.path.expanduser(API_KEY_FILE)
|
||||||
|
if os.path.exists(api_key_file):
|
||||||
|
with open(api_key_file) as f:
|
||||||
|
api_key = f.read().strip()
|
||||||
|
else:
|
||||||
|
print("[ERROR] DeepSeek API Key not found")
|
||||||
|
print(f" Please save your key to: {API_KEY_FILE}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
body = json.dumps({
|
||||||
|
"model": DEEPSEEK_MODEL,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": prompt},
|
||||||
|
],
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tokens": 4096,
|
||||||
|
}).encode()
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
DEEPSEEK_URL,
|
||||||
|
data=body,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
resp = urllib.request.urlopen(req, timeout=120)
|
||||||
|
result = json.loads(resp.read())
|
||||||
|
return result["choices"][0]["message"]["content"]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] DeepSeek API call failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 报告生成 ────────────────────────────────────────
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = """你是一位深度阅读分析助手,不是摘要工具。
|
||||||
|
|
||||||
|
你的任务是分析用户今日在微信读书上的划线和批注,生成一份高质量的阅读报告。
|
||||||
|
|
||||||
|
## 写作要求
|
||||||
|
- 每一条划线都要展开分析:补充背景知识、指出其深层含义、联系用户的实际场景
|
||||||
|
- 如果有批注,要围绕批注展开讨论,追问关键问题
|
||||||
|
- 找出当日阅读内容之间的内在联系(主题、方法、思想上的呼应)
|
||||||
|
- 像一位私人思考伙伴一样,提供有启发的见解和追问
|
||||||
|
- 报告要有深度、有观点,不要写成流水账或摘要
|
||||||
|
- 使用平实、客观的语言,不要抒情、不要煽情
|
||||||
|
- 适当引用原文,但不要大段堆砌
|
||||||
|
|
||||||
|
## 报告结构
|
||||||
|
1. 今日阅读概览(总览性描述当天阅读的特点)
|
||||||
|
2. 分书深度分析(每本书独立小节,有洞察的分析)
|
||||||
|
3. 主题串联(找出跨书的联系和启发)
|
||||||
|
4. 明日阅读建议(基于当天阅读给出具体建议)"""
|
||||||
|
|
||||||
|
|
||||||
|
def generate_report_md(entries, ai_analysis, date_str):
|
||||||
|
"""组装最终报告 Markdown,含字数与阅读时间估算。"""
|
||||||
|
cn_chars = len(re.findall(r"[\u4e00-\u9fff]", ai_analysis))
|
||||||
|
en_words = len(re.findall(r"[a-zA-Z]+", ai_analysis))
|
||||||
|
total = cn_chars + en_words
|
||||||
|
read_min = max(1, round(total / 400))
|
||||||
|
header = f"> 本文共 **{total}** 字 · 预计阅读 **{read_min}** 分钟"
|
||||||
|
lines = []
|
||||||
|
lines.append(f"# 今日阅读报告 · {date_str}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(header)
|
||||||
|
lines.append("")
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(ai_analysis)
|
||||||
|
lines.append("")
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"*报告生成:{datetime.now().strftime('%Y-%m-%d %H:%M')} | AI 模型:DeepSeek*")
|
||||||
|
return "\n".join(lines)
|
||||||
|
lines.append(f"# 今日阅读报告 · {date_str}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(ai_analysis)
|
||||||
|
lines.append("")
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"*报告生成:{datetime.now().strftime('%Y-%m-%d %H:%M')} | AI 模型:DeepSeek*")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Git 操作 ────────────────────────────────────────
|
||||||
|
|
||||||
|
def save_and_push(content, date_str):
|
||||||
|
"""保存报告并推送到 Gitea"""
|
||||||
|
os.makedirs(DAILY_DIR, exist_ok=True)
|
||||||
|
filename = f"每日阅读_{date_str}.md"
|
||||||
|
filepath = os.path.join(DAILY_DIR, filename)
|
||||||
|
with open(filepath, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
print(f"[OUTPUT] {filepath}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(["git", "add", "daily/"], cwd=REPO_DIR,
|
||||||
|
capture_output=True, timeout=30)
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "commit", "-m", f"report: 每日阅读报告 {date_str} (AI分析)", "--allow-empty"],
|
||||||
|
cwd=REPO_DIR, capture_output=True, timeout=30
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
subprocess.run(["git", "push"], cwd=REPO_DIR,
|
||||||
|
capture_output=True, timeout=60)
|
||||||
|
print(f"[GIT] Pushed: 每日阅读报告 {date_str}")
|
||||||
|
else:
|
||||||
|
print(f"[GIT] No changes")
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"[GIT] Error: {e.stderr.decode()[:200]}")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 主流程 ───────────────────────────────────────────
|
||||||
|
|
||||||
|
def main():
|
||||||
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"每日阅读报告 (AI) | {datetime.now().strftime('%Y-%m-%d %H:%M')}")
|
||||||
|
print(f"{'='*50}")
|
||||||
|
|
||||||
|
# 1. 同步
|
||||||
|
print("\n[1/4] 同步微信读书笔记...")
|
||||||
|
if os.path.exists(SYNC_SCRIPT):
|
||||||
|
subprocess.run(["python3", SYNC_SCRIPT], capture_output=True, timeout=300)
|
||||||
|
print(" 同步完成")
|
||||||
|
else:
|
||||||
|
print(f" [SKIP] {SYNC_SCRIPT} not found")
|
||||||
|
|
||||||
|
# 2. 解析今日数据
|
||||||
|
print(f"\n[2/4] 提取今日数据 ({date_str})...")
|
||||||
|
entries = load_today_data(date_str)
|
||||||
|
print(f" 找到 {len(entries)} 条记录")
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
md = f"# 今日阅读报告 · {date_str}\n\n> 暂无阅读记录\n"
|
||||||
|
save_and_push(md, date_str)
|
||||||
|
print("\n今日无阅读数据,已生成空报告")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. AI 分析
|
||||||
|
print(f"\n[3/4] AI 深度分析 ({len(entries)} 条)...")
|
||||||
|
prompt = format_entries_for_prompt(entries)
|
||||||
|
full_prompt = f"""以下是用户今日在微信读书上的划线和批注数据:
|
||||||
|
|
||||||
|
{prompt}
|
||||||
|
|
||||||
|
请根据这些数据生成一份高质量的今日阅读报告。"""
|
||||||
|
|
||||||
|
ai_content = call_deepseek(full_prompt, SYSTEM_PROMPT)
|
||||||
|
if not ai_content:
|
||||||
|
print("[ERROR] AI 分析失败,使用备用模板")
|
||||||
|
ai_content = "> AI 分析暂时不可用,请稍后重试。"
|
||||||
|
|
||||||
|
# 4. 输出
|
||||||
|
print(f"\n[4/4] 生成报告并推送...")
|
||||||
|
md = generate_report_md(entries, ai_content, date_str)
|
||||||
|
save_and_push(md, date_str)
|
||||||
|
|
||||||
|
print(f"\n{'='*50}")
|
||||||
|
print(f"每日阅读报告完成 ✅")
|
||||||
|
print(f"{'='*50}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user