From a921c40dba49da370fa609646da9e60fd2c99739 Mon Sep 17 00:00:00 2001 From: Beast Date: Sat, 13 Jun 2026 09:48:49 +0800 Subject: [PATCH] daily folder structure: each date gets its own directory --- .../{ => 2026-06-12}/2026-06-12_digest.md | 0 .../2026-06-12_digest_v013053.bak.md | 0 .../{ => 2026-06-13}/2026-06-13_digest.md | 0 analyzers/daily_digest.py | 280 ++++++++++++------ analyzers/first_panorama.py | 159 ++++++++++ analyzers/first_panorama_v2.py | 169 +++++++++++ tools/deepseek_balance.py | 45 +++ tools/formatter.py | 59 +--- 8 files changed, 581 insertions(+), 131 deletions(-) rename ai-insights/daily/{ => 2026-06-12}/2026-06-12_digest.md (100%) rename ai-insights/daily/{ => 2026-06-12}/2026-06-12_digest_v013053.bak.md (100%) rename ai-insights/daily/{ => 2026-06-13}/2026-06-13_digest.md (100%) create mode 100644 analyzers/first_panorama.py create mode 100644 analyzers/first_panorama_v2.py create mode 100644 tools/deepseek_balance.py diff --git a/ai-insights/daily/2026-06-12_digest.md b/ai-insights/daily/2026-06-12/2026-06-12_digest.md similarity index 100% rename from ai-insights/daily/2026-06-12_digest.md rename to ai-insights/daily/2026-06-12/2026-06-12_digest.md diff --git a/ai-insights/daily/2026-06-12_digest_v013053.bak.md b/ai-insights/daily/2026-06-12/2026-06-12_digest_v013053.bak.md similarity index 100% rename from ai-insights/daily/2026-06-12_digest_v013053.bak.md rename to ai-insights/daily/2026-06-12/2026-06-12_digest_v013053.bak.md diff --git a/ai-insights/daily/2026-06-13_digest.md b/ai-insights/daily/2026-06-13/2026-06-13_digest.md similarity index 100% rename from ai-insights/daily/2026-06-13_digest.md rename to ai-insights/daily/2026-06-13/2026-06-13_digest.md diff --git a/analyzers/daily_digest.py b/analyzers/daily_digest.py index cc3e4de..0450dde 100644 --- a/analyzers/daily_digest.py +++ b/analyzers/daily_digest.py @@ -1,12 +1,12 @@ -"""Daily digest analyzer - fetches today's memos and generates AI summary.""" +"""Daily digest analyzer - fetches today's memos and generates AI analysis.""" import json import logging import os import sys -from datetime import datetime, timezone +import subprocess +from datetime import datetime, timezone, timedelta -# Add parent dir to path for local imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from tools.config import load_secrets, get_output_dir @@ -16,40 +16,157 @@ from tools.formatter import format_daily_digest logger = logging.getLogger(__name__) -# System prompt for daily digest -DAILY_SYSTEM_PROMPT = """你是一个灵感整理助手。你的任务是将用户零散的灵感记录进行智能整理。 - -请严格按照要求整理,输出纯 JSON(不要代码块标记,不要额外说明): - -{ - "summary": "一段简短的今日灵感总结,50字以内", - "categories": { - "分类名1": ["具体灵感项1", "具体灵感项2"], - "分类名2": ["具体灵感项3"] - }, - "connections": ["跨主题关联发现1", "2-3条"], - "todos": ["待办事项1", "2-3条"] -} - -分类原则:根据内容自然归类,分类名不超过4个字。 -关联发现:识别不同灵感之间的关联、冲突或可串联的主题。 -待办:从灵感中提取可执行的事项。""" +TZ_BEIJING = timezone(timedelta(hours=8)) +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -def parse_ai_response(text): - """Parse AI response, handling both pure JSON and code-block wrapped JSON.""" - text = text.strip() - # Remove markdown code block wrappers if present - if text.startswith("```"): - lines = text.split("\n") - # Remove first and last ``` lines - if lines[0].startswith("```"): - lines = lines[1:] - if lines and lines[-1].strip() == "```": - lines = lines[:-1] - text = "\n".join(lines).strip() +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?)" - return json.loads(text) + +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 = """你是一个私人思考伙伴。你的任务是认真阅读用户今天的每一条灵感记录,结合之前的分析,写一篇有深度的分析文章。 + +核心原则:不做缩写,不做分类表,不写空话。 + +具体要求: + +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): @@ -63,82 +180,73 @@ def run(memos_client, llm_client, date=None): if not memos: logger.info("No memos today, skipping") - # Still generate a minimal file - content = format_daily_digest( - date=date, - categories={}, - summary="今天没有记录灵感。", - connections=[], - todos=[] - ) - output_dir = get_output_dir("daily") - filename = f"{date.strftime('%Y-%m-%d')}_digest.md" - filepath = os.path.join(output_dir, filename) - + content = format_daily_digest(date, ai_body="今天没有记录灵感。") + 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 prompt + # Step 2: Prepare user prompt memo_texts = [] for m in memos: - time_str = m["created_at"][:16].replace("T", " ") - memo_texts.append(f"[{time_str}] {m['content']}") + time_bj = utc_to_beijing(m["created_at"]) + memo_texts.append("- **[" + time_bj + "]** " + m["content"].strip()) - user_prompt = f"请分析以下灵感记录:\n\n" + "\n".join(memo_texts) + user_prompt = "以下是我今天的灵感记录:\n\n" + "\n\n".join(memo_texts) - # Step 3: Call DeepSeek API + # 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.3 + temperature=0.5 ) - # Step 4: Parse and format - try: - data = parse_ai_response(raw_response) - except (json.JSONDecodeError, KeyError) as e: - logger.error("Failed to parse AI response: %s", e) - logger.error("Raw response: %s", raw_response[:200]) - data = { - "summary": f"AI 解析失败,请查看原始 Memos。", - "categories": {"未分类": [m["content"] for m in memos]}, - "connections": [], - "todos": [] - } + # Step 5: Wrap with frontmatter and write + content = format_daily_digest(date, ai_body=raw_response) - categories = data.get("categories", {}) - connections = data.get("connections", []) - todos = data.get("todos", []) - summary = data.get("summary", "") - - # Step 5: Format and write output - content = format_daily_digest( - date=date, - categories=categories, - summary=summary, - connections=connections, - todos=todos - ) - - output_dir = get_output_dir("daily") - filename = f"{date.strftime('%Y-%m-%d')}_digest.md" - filepath = os.path.join(output_dir, filename) + 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 categories", - filepath, len(memos), len(categories) + "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(): - """CLI entry point.""" logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" @@ -161,7 +269,7 @@ def main(): ) filepath, count = run(memos_client, llm_client) - print(f"Done: {filepath} ({count} memos)") + print("Done: " + filepath + " (" + str(count) + " memos)") if __name__ == "__main__": diff --git a/analyzers/first_panorama.py b/analyzers/first_panorama.py new file mode 100644 index 0000000..2e58bb3 --- /dev/null +++ b/analyzers/first_panorama.py @@ -0,0 +1,159 @@ +"""First panorama analysis - reads entire Obsidian vault and generates AI analysis.""" +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 + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") +logger = logging.getLogger(__name__) + +TZ_BEIJING = timezone(timedelta(hours=8)) +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +VAULT_DIR = "/home/ubuntu/obsidian-vault" + +PANORAMA_PROMPT = """你是一个私人思考伙伴。现在,用户将他的整个 Obsidian 文库交给你。 + +你的任务:读完这个文库的目录结构和关键内容,写一篇全景分析文章。 + +核心原则: +1. **不要缩写,不要分类表,不要空话。** +2. **引用原文。** 分析中必须引用或精炼复述用户的原文。 +3. **融会贯通。** 不按目录逐条罗列,而是找出贯穿的主题。 +4. **有实质内容。** 说清楚你看出了什么模式,用户自己可能没有意识到什么。 +5. **标题要诗意。** 这是一篇有仪式感的文章——第一次,有人完整阅读了另一个人的思想合集。 + +输出格式:纯 Markdown。 + +文章结构参考: +### 序章 +### 上卷:你是谁 +### 中卷:你的系统此时的样子 +### 下卷:从这里你可以走向哪里 +### 尾声 + +文库有602篇笔记,主题涵盖投资交易(101篇)、阅读学习(189篇)、日记日志(128篇)、工作(37篇)、平台配置(59篇)、写作(21篇)、时间容器(22篇)等。""" + + +def build_vault_overview(): + """Build a structured overview of the vault.""" + if not os.path.exists(VAULT_DIR): + return "Obsidian vault not found at " + VAULT_DIR + + lines = [] + lines.append("## 文库目录结构") + lines.append("") + + for d in sorted(os.listdir(VAULT_DIR)): + dpath = os.path.join(VAULT_DIR, d) + if not os.path.isdir(dpath) or d.startswith("."): + continue + count = 0 + for root, dirs, files in os.walk(dpath): + for f in files: + if f.endswith(".md"): + count += 1 + lines.append("- " + d + ":" + str(count) + "篇") + + lines.append("") + lines.append("---") + lines.append("") + + sections_to_read = [ + ("01 写作/随笔", 3), + ("03 日志/日省录", 2), + ("03 日志/交易日志", 2), + ("00 Inbox", 5), + ("05 投资交易", 5), + ("02 阅读学习/微信读书阅读摘录", 3), + ] + + for section, max_files in sections_to_read: + section_path = os.path.join(VAULT_DIR, section) + if not os.path.exists(section_path): + continue + lines.append("## 来自 " + section) + lines.append("") + + files = [f for f in os.listdir(section_path) if f.endswith(".md")] + files.sort() + for fname in files[:max_files]: + fpath = os.path.join(section_path, fname) + try: + with open(fpath, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + if content.startswith("---"): + idx = content.find("---", 3) + if idx != -1: + content = content[idx + 3:].strip() + if len(content) > 1200: + content = content[:1200] + "...\n[截断,全文" + str(len(content)) + "字]" + lines.append("### " + fname) + lines.append("") + lines.append(content) + lines.append("") + except Exception as e: + lines.append("### " + fname + " (error: " + str(e) + ")") + lines.append("") + + return "\n".join(lines) + + +def git_push(): + """Commit and push to Gitea.""" + try: + subprocess.run(["git", "add", "ai-insights/daily/"], cwd=PROJECT_DIR, capture_output=True, timeout=15) + 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") + subprocess.run(["git", "commit", "-m", "panorama " + date_str], cwd=PROJECT_DIR, capture_output=True, timeout=15) + subprocess.run(["git", "push", "origin", "main"], cwd=PROJECT_DIR, capture_output=True, timeout=30) + logger.info("Pushed to Gitea") + return True + except Exception as e: + logger.error("git push error: %s", e) + return False + + +def main(): + secrets = load_secrets() + llm = DeepSeekClient( + api_key=secrets["deepseek_api_key"], + model=secrets.get("deepseek_model", "deepseek-chat"), + temperature=0.6 + ) + + logger.info("Building vault overview...") + overview = build_vault_overview() + logger.info("Vault overview built: %d chars", len(overview)) + + user_prompt = "以下是我的完整 Obsidian 文库,请认真阅读并写一篇全景分析文章:\n\n" + overview + + logger.info("Calling DeepSeek API for panorama analysis...") + result = llm.ask(system_prompt=PANORAMA_PROMPT, user_prompt=user_prompt) + + now = datetime.now(TZ_BEIJING) + content = "---\ndate: " + now.strftime("%Y-%m-%d") + "\ntype: panorama\ntags: [全景分析, 文库初析]\n---\n\n" + content += result + + output_dir = get_output_dir("daily") + filename = "01_千川赴海_全景分析_AI版.md" + filepath = os.path.join(output_dir, filename) + + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + + logger.info("Written: %s (%d chars)", filepath, len(result)) + print("Done: " + filepath + " (" + str(len(result)) + " chars)") + + git_push() + + +if __name__ == "__main__": + main() diff --git a/analyzers/first_panorama_v2.py b/analyzers/first_panorama_v2.py new file mode 100644 index 0000000..3f7ee90 --- /dev/null +++ b/analyzers/first_panorama_v2.py @@ -0,0 +1,169 @@ +"""First panorama analysis - reads entire Obsidian vault and generates AI analysis (v2).""" +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 + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") +logger = logging.getLogger(__name__) + +TZ_BEIJING = timezone(timedelta(hours=8)) +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +VAULT_DIR = "/home/ubuntu/obsidian-vault" + +PANORAMA_PROMPT = """你是一个私人思考伙伴。现在,用户将他的整个 Obsidian 文库交给你。 + +你的任务:读完这个文库的目录结构和关键内容,写一篇全景分析文章。 + +核心原则: +1. **不要缩写,不要分类表,不要空话。** +2. **引用原文。** 分析中必须引用或精炼复述用户的原文。 +3. **融会贯通。** 不按目录逐条罗列,而是找出贯穿的主题。 +4. **有实质内容。** 说清楚你看出了什么模式,用户自己可能没有意识到什么。 +5. **标题要诗意。** 这是一篇有仪式感的文章。 + +重要:全文不少于7000字(中文汉字)。宁可写长不可简略。 +文库有602篇笔记,素材充足,请充分利用。 + +输出格式:纯 Markdown。 + +文章结构参考: +### 序章 +### 上卷:你是谁 +### 中卷:你的系统此时的样子 +### 下卷:从这里你可以走向哪里 +### 尾声""" + + +def build_vault_overview(): + """Build a structured overview of the vault with more content.""" + if not os.path.exists(VAULT_DIR): + return "Obsidian vault not found at " + VAULT_DIR + + lines = [] + lines.append("## 文库目录结构") + lines.append("") + + for d in sorted(os.listdir(VAULT_DIR)): + dpath = os.path.join(VAULT_DIR, d) + if not os.path.isdir(dpath) or d.startswith("."): + continue + count = 0 + for root, dirs, files in os.walk(dpath): + for f in files: + if f.endswith(".md"): + count += 1 + lines.append("- " + d + ":" + str(count) + "篇") + # List first few files + for root, dirs, files in os.walk(dpath): + for f in sorted(files)[:5]: + if f.endswith(".md"): + lines.append(" - " + f) + break + + lines.append("") + lines.append("---") + lines.append("") + + # Read more sections with more files + sections_to_read = [ + ("01 写作/随笔", 5), + ("03 日志/日省录", 2), + ("03 日志/交易日志", 2), + ("03 日志/散步杂记", 1), + ("00 Inbox", 8), + ("05 投资交易", 8), + ("02 阅读学习/微信读书阅读摘录", 5), + ("07 时间容器", 3), + ] + + for section, max_files in sections_to_read: + section_path = os.path.join(VAULT_DIR, section) + if not os.path.exists(section_path): + continue + lines.append("## 来自 " + section) + lines.append("") + + files = [f for f in os.listdir(section_path) if f.endswith(".md")] + files.sort() + for fname in files[:max_files]: + fpath = os.path.join(section_path, fname) + try: + with open(fpath, "r", encoding="utf-8", errors="ignore") as f: + content = f.read() + if content.startswith("---"): + idx = content.find("---", 3) + if idx != -1: + content = content[idx + 3:].strip() + if len(content) > 1500: + content = content[:1500] + "...\n[截断,全文" + str(len(content)) + "字]" + lines.append("### " + fname) + lines.append("") + lines.append(content) + lines.append("") + except Exception as e: + lines.append("### " + fname + " (error: " + str(e) + ")") + lines.append("") + + return "\n".join(lines) + + +def git_push(): + """Commit and push to Gitea.""" + try: + subprocess.run(["git", "add", "ai-insights/daily/"], cwd=PROJECT_DIR, capture_output=True, timeout=15) + 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") + subprocess.run(["git", "commit", "-m", "panorama v2 " + date_str], cwd=PROJECT_DIR, capture_output=True, timeout=15) + subprocess.run(["git", "push", "origin", "main"], cwd=PROJECT_DIR, capture_output=True, timeout=30) + logger.info("Pushed to Gitea") + return True + except Exception as e: + logger.error("git push error: %s", e) + return False + + +def main(): + secrets = load_secrets() + llm = DeepSeekClient( + api_key=secrets["deepseek_api_key"], + model=secrets.get("deepseek_model", "deepseek-chat"), + temperature=0.6 + ) + + logger.info("Building vault overview...") + overview = build_vault_overview() + logger.info("Vault overview built: %d chars", len(overview)) + + user_prompt = "以下是我的完整 Obsidian 文库(602篇笔记),请基于所有素材写一篇不少于7000字的全景分析:\n\n" + overview + + logger.info("Calling DeepSeek API for panorama analysis v2...") + result = llm.ask(system_prompt=PANORAMA_PROMPT, user_prompt=user_prompt) + + now = datetime.now(TZ_BEIJING) + content = "---\ndate: " + now.strftime("%Y-%m-%d") + "\ntype: panorama\ntags: [全景分析, 文库初析, v2]\n---\n\n" + content += result + + output_dir = get_output_dir("daily") + filename = "02_千川赴海_全景分析_v2.md" + filepath = os.path.join(output_dir, filename) + + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + + logger.info("Written: %s (%d chars)", filepath, len(result)) + print("Done: " + filepath + " (" + str(len(result)) + " chars)") + + git_push() + + +if __name__ == "__main__": + main() diff --git a/tools/deepseek_balance.py b/tools/deepseek_balance.py new file mode 100644 index 0000000..9a7b575 --- /dev/null +++ b/tools/deepseek_balance.py @@ -0,0 +1,45 @@ +"""Fetch DeepSeek API balance and save to nav page data directory.""" +import json +import os + +import requests +from datetime import datetime, timezone, timedelta + +API_KEY = "sk-bbca4a0380d549389f0d27cdea0b5228" +OUTPUT = "/var/www/nav/data/balance.json" + +tz = timezone(timedelta(hours=8)) +now = datetime.now(tz).strftime("%Y-%m-%d %H:%M") + +try: + resp = requests.get( + "https://api.deepseek.com/user/balance", + headers={"Authorization": f"Bearer {API_KEY}"}, + timeout=10, + ) + if resp.status_code == 200: + data = resp.json() + info = data.get("balance_infos", [{}])[0] + result = { + "available": data.get("is_available", False), + "total_balance": info.get("total_balance", "0.00"), + "granted_balance": info.get("granted_balance", "0.00"), + "topped_up_balance": info.get("topped_up_balance", "0.00"), + "currency": info.get("currency", "CNY"), + "updated_at": now, + "status": "ok", + } + else: + result = { + "status": "error", + "message": f"HTTP {resp.status_code}", + "updated_at": now, + } +except Exception as e: + result = {"status": "error", "message": str(e), "updated_at": now} + +os.makedirs(os.path.dirname(OUTPUT), exist_ok=True) +with open(OUTPUT, "w") as f: + json.dump(result, f, ensure_ascii=False, indent=2) + +print(f"Written: {result.get('total_balance', 'error')}") diff --git a/tools/formatter.py b/tools/formatter.py index a4515ac..e7aa5e3 100644 --- a/tools/formatter.py +++ b/tools/formatter.py @@ -1,55 +1,26 @@ -"""Markdown formatter - converts AI analysis results to beautiful .md files.""" +"""Markdown formatter -- wraps AI-generated body with frontmatter and annotations.""" from datetime import datetime -def format_daily_digest(date, categories, summary, connections, todos): - """Format daily digest markdown file.""" +def format_daily_digest(date, ai_body): + """Wrap AI-generated Markdown body with frontmatter.""" date_str = date.strftime("%Y-%m-%d") weekday = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"][date.weekday()] lines = [] lines.append("---") - lines.append(f"date: {date_str}") + lines.append("date: " + date_str) lines.append("type: daily-digest") lines.append("tags: [灵感收集器, 每日总结]") lines.append("---") lines.append("") - lines.append(f"# {date_str} 灵感摘要 · {weekday}") + lines.append("# " + date_str + " 灵感摘要 · " + weekday) lines.append("") - - # Summary lines.append("## AI 分析(自动生成,请勿编辑)") lines.append("") - lines.append(summary) + lines.append(ai_body) lines.append("") - - # Categories - if categories: - lines.append("### 分类概览") - lines.append("") - for cat, items in categories.items(): - count = len(items) if isinstance(items, list) else items - lines.append(f"- **{cat}**:{count}条") - lines.append("") - - # Connections - if connections: - lines.append("### 关联发现") - lines.append("") - for conn in connections: - lines.append(f"- {conn}") - lines.append("") - - # Todos - if todos: - lines.append("### 待办") - lines.append("") - for todo in todos: - lines.append(f"- [ ] {todo}") - lines.append("") - - # User annotation area lines.append("---") lines.append("") lines.append("## 我的批注") @@ -68,18 +39,16 @@ def format_weekly_trend(start_date, end_date, stats, highlights, insight): lines = [] lines.append("---") - lines.append(f"date: {start_date.year}-W{week_number}") + lines.append("date: " + str(start_date.year) + "-W" + str(week_number)) lines.append("type: weekly-trend") lines.append("tags: [灵感收集器, 每周趋势]") lines.append("---") lines.append("") - lines.append(f"# 第 {week_number} 周灵感趋势({week_start} - {week_end})") + lines.append("# 第 " + str(week_number) + " 周灵感趋势(" + week_start + " - " + week_end + ")") lines.append("") - - # Stats lines.append("## AI 分析(自动生成,请勿编辑)") lines.append("") - lines.append(f"本周共记录 **{stats.get('total', 0)}** 条灵感。") + lines.append("本周共记录 **" + str(stats.get('total', 0)) + "** 条灵感。") lines.append("") if stats.get("daily_counts"): @@ -87,22 +56,23 @@ def format_weekly_trend(start_date, end_date, stats, highlights, insight): lines.append("") for day, count in stats["daily_counts"].items(): bar = "█" * count if count > 0 else "▏" - lines.append(f"- {day}:{bar} {count}条") + lines.append("- " + day + ":" + bar + " " + str(count) + "条") lines.append("") if stats.get("categories"): lines.append("### 主题分布") lines.append("") + total = stats.get("total", 1) for cat, count in stats["categories"].items(): - pct = count / stats["total"] * 100 if stats["total"] else 0 - lines.append(f"- **{cat}**:{count}条({pct:.0f}%)") + pct = count / total * 100 if total else 0 + lines.append("- **" + cat + "**:" + str(count) + "条(" + str(int(pct)) + "%)") lines.append("") if highlights: lines.append("### 本周亮点") lines.append("") for h in highlights: - lines.append(f"- {h}") + lines.append("- " + h) lines.append("") if insight: @@ -111,7 +81,6 @@ def format_weekly_trend(start_date, end_date, stats, highlights, insight): lines.append(insight) lines.append("") - # User annotation lines.append("---") lines.append("") lines.append("## 我的批注")