170 lines
6.1 KiB
Python
170 lines
6.1 KiB
Python
"""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()
|