feat: nerd-Brain (呆脑子) - 初始提交
- anchor_writer.py: 记忆写入与情感打标 - anchor_search.py: 多维语义检索引擎 - anchor_decay.py: 遗忘曲线 + 归档 + 随机返场 - anchor_vault_sync.py: Vault 索引生成 - README.md: 项目说明文档
This commit is contained in:
146
anchor_vault_sync.py
Normal file
146
anchor_vault_sync.py
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Anchor Vault Sync & Search
|
||||
轻量级 Obsidian Vault 索引与检索脚本
|
||||
路径: ~/.hermes/anchor_vault_sync.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
VAULT_PATH = "/Users/fyah/Documents/如梦初醒/memory/test"
|
||||
INDEX_PATH = os.path.expanduser("~/.hermes/anchor_vault_index.json")
|
||||
SKIP_DIRS = {".obsidian", ".trash", ".smart-env", ".space", ".makemd", ".git"}
|
||||
PREVIEW_LEN = 300
|
||||
|
||||
def parse_yaml_frontmatter(content):
|
||||
"""极简 YAML 解析,只取 tags 和 title"""
|
||||
tags = []
|
||||
title = ""
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 2:
|
||||
yaml_block = parts[1]
|
||||
for line in yaml_block.splitlines():
|
||||
if line.startswith("tags:"):
|
||||
# 处理 tags: [a, b] 或 tags:\n - a\n - b
|
||||
rest = line[5:].strip()
|
||||
if rest.startswith("["):
|
||||
tags = [t.strip().strip("'\"") for t in rest[1:-1].split(",")]
|
||||
else:
|
||||
tags = []
|
||||
elif line.startswith(" - "):
|
||||
if "tags" in locals() and tags is not None: # 简单判断是否在 tags 块下
|
||||
tags.append(line.strip().strip("- ").strip("'\""))
|
||||
elif line.startswith("title:"):
|
||||
title = line[6:].strip().strip("'\"")
|
||||
return tags, title
|
||||
|
||||
def scan_vault():
|
||||
"""扫描 Vault 并生成索引"""
|
||||
print(f"[*] 开始扫描: {VAULT_PATH}")
|
||||
index = []
|
||||
count = 0
|
||||
|
||||
for root, dirs, files in os.walk(VAULT_PATH):
|
||||
# 过滤隐藏目录
|
||||
dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")]
|
||||
|
||||
for file in files:
|
||||
if not file.endswith(".md"):
|
||||
continue
|
||||
|
||||
filepath = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(filepath, VAULT_PATH)
|
||||
|
||||
try:
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
tags, title = parse_yaml_frontmatter(content)
|
||||
mtime = os.path.getmtime(filepath)
|
||||
mtime_str = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
# 提取预览(去掉 YAML 头和空行)
|
||||
body = content.split("---", 2)[-1].strip()
|
||||
preview = body[:PREVIEW_LEN].replace("\n", " ").strip()
|
||||
if len(body) > PREVIEW_LEN:
|
||||
preview += "..."
|
||||
|
||||
index.append({
|
||||
"path": rel_path,
|
||||
"title": title or Path(file).stem,
|
||||
"tags": tags,
|
||||
"mtime": mtime_str,
|
||||
"preview": preview
|
||||
})
|
||||
count += 1
|
||||
except Exception as e:
|
||||
print(f"[!] 读取失败 {rel_path}: {e}")
|
||||
|
||||
# 保存索引
|
||||
with open(INDEX_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(index, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"[+] 同步完成。共索引 {count} 个文件。")
|
||||
print(f"[+] 索引已保存至: {INDEX_PATH}")
|
||||
return index
|
||||
|
||||
def search_vault(query):
|
||||
"""基于关键词搜索索引"""
|
||||
if not os.path.exists(INDEX_PATH):
|
||||
print("[-] 索引不存在,请先运行 sync。")
|
||||
return []
|
||||
|
||||
with open(INDEX_PATH, "r", encoding="utf-8") as f:
|
||||
index = json.load(f)
|
||||
|
||||
results = []
|
||||
q_lower = query.lower()
|
||||
|
||||
for item in index:
|
||||
# 匹配标题、标签、预览
|
||||
score = 0
|
||||
if q_lower in item["title"].lower(): score += 3
|
||||
if any(q_lower in t.lower() for t in item["tags"]): score += 2
|
||||
if q_lower in item["preview"].lower(): score += 1
|
||||
|
||||
if score > 0:
|
||||
item["score"] = score
|
||||
results.append(item)
|
||||
|
||||
# 按分数排序
|
||||
results.sort(key=lambda x: x["score"], reverse=True)
|
||||
|
||||
if not results:
|
||||
print(f"[-] 未找到与 '{query}' 相关的内容。")
|
||||
return []
|
||||
|
||||
print(f"[+] 找到 {len(results)} 条相关记忆(Top 5):")
|
||||
for r in results[:5]:
|
||||
print(f"\n📄 {r['title']} ({r['path']})")
|
||||
print(f"🏷️ 标签: {', '.join(r['tags']) if r['tags'] else '无'}")
|
||||
print(f"🕒 更新: {r['mtime']}")
|
||||
print(f"👁️ 预览: {r['preview'][:100]}...")
|
||||
|
||||
return results
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python3 anchor_vault_sync.py [sync|search <关键词>]")
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[1]
|
||||
if cmd == "sync":
|
||||
scan_vault()
|
||||
elif cmd == "search":
|
||||
if len(sys.argv) < 3:
|
||||
print("[-] 搜索需要关键词。用法: search <关键词>")
|
||||
sys.exit(1)
|
||||
query = " ".join(sys.argv[2:])
|
||||
search_vault(query)
|
||||
else:
|
||||
print(f"[-] 未知命令: {cmd}")
|
||||
Reference in New Issue
Block a user