daily folder structure: each date gets its own directory
This commit is contained in:
@ -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__":
|
||||
|
||||
159
analyzers/first_panorama.py
Normal file
159
analyzers/first_panorama.py
Normal file
@ -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()
|
||||
169
analyzers/first_panorama_v2.py
Normal file
169
analyzers/first_panorama_v2.py
Normal file
@ -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()
|
||||
45
tools/deepseek_balance.py
Normal file
45
tools/deepseek_balance.py
Normal file
@ -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')}")
|
||||
@ -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("## 我的批注")
|
||||
|
||||
Reference in New Issue
Block a user