init: inspiration collector v1.0

This commit is contained in:
2026-06-12 23:30:27 +08:00
commit 5a876e5ecd
17 changed files with 881 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
# Python
__pycache__/
*.pyc
*.pyo
.env
venv/
.venv/
# Logs
*.log
# OS
.DS_Store
Thumbs.db

74
README.md Normal file
View File

@ -0,0 +1,74 @@
# 灵感收集器 (Inspiration Collector)
手机随手记灵感 → AI 自动整理 → Obsidian 自动同步
## 数据流向
```
手机 Moe Memos → 服务器 Memos 实例
Python 脚本 + DeepSeek API
ai-insights/daily/ 中的 .md 文件
git push → Gitea
Obsidian Git 插件 pull → 你看到
```
## 目录结构
```
inspiration-collector/
├── ai-insights/ # AI 生成的报告git 追踪)
│ ├── daily/ # 每日灵感摘要
│ └── weekly/ # 每周灵感趋势
├── tools/ # 核心工具模块
│ ├── llm.py # DeepSeek API 客户端
│ ├── memos_client.py # Memos 数据读取
│ ├── config.py # 配置加载
│ └── formatter.py # Markdown 输出格式化
├── analyzers/ # 分析逻辑
│ ├── daily_digest.py # 每日分析
│ └── weekly_trend.py # 每周分析
├── scripts/ # cron 入口脚本
│ ├── run_daily_digest.sh
│ └── run_weekly_trend.sh
├── secrets/ # 密钥(不入库)
│ ├── .gitignore
│ └── secrets.json.template
└── README.md
```
## 部署步骤
### 1. 服务器上
```bash
# 克隆仓库
cd ~
git clone http://localhost:3000/你的用户名/inspiration-collector.git
# 配置密钥
cp secrets/secrets.json.template secrets/secrets.json
# 编辑 secrets.json填入你的 DeepSeek API Key 和 Memos Token
chmod 600 secrets/secrets.json
# 测试运行
python3 analyzers/daily_digest.py
# 设置 cron
crontab -e
# 添加:
# 每日 22:00 北京时间
0 14 * * * /home/ubuntu/inspiration-collector/scripts/run_daily_digest.sh
# 每周日 16:00 北京时间
0 8 * * 0 /home/ubuntu/inspiration-collector/scripts/run_weekly_trend.sh
```
### 2. 笔记本 Obsidian
1. `git clone` 到本地
2. Obsidian 中打开文件夹
3. 安装 Obsidian Git 插件
4. 设置自动拉取

View File

View File

@ -0,0 +1,32 @@
---
date: 2026-06-12
type: daily-digest
tags: [灵感收集器, 每日总结]
---
# 2026-06-12 灵感摘要 · 周五
## AI 分析(自动生成,请勿编辑)
今日灵感围绕时间流逝与生活节奏,记录个人感受与部署服务的开始。
### 分类概览
- **生活感悟**4条
- **技术记录**2条
### 关联发现
- 时间倒计时的主题贯穿生活与工作,暗示对变化的敏感
- 部署服务与记录灵感,体现个人化数字生活的开始
### 待办
- [ ] 记录更多关于时间流逝的感悟
- [ ] 完善自己部署的服务功能
---
## 我的批注
> *在这里写下你的想法、质疑、补充*

View File

0
analyzers/__init__.py Normal file
View File

168
analyzers/daily_digest.py Normal file
View File

@ -0,0 +1,168 @@
"""Daily digest analyzer - fetches today's memos and generates AI summary."""
import json
import logging
import os
import sys
from datetime import datetime, timezone
# 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
from tools.llm import DeepSeekClient
from tools.memos_client import MemosClient
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个字。
关联发现:识别不同灵感之间的关联、冲突或可串联的主题。
待办:从灵感中提取可执行的事项。"""
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()
return json.loads(text)
def run(memos_client, llm_client, date=None):
"""Run daily digest analysis."""
date = date or datetime.now(timezone.utc)
logger.info("Daily digest started for %s", date.strftime("%Y-%m-%d"))
# Step 1: Fetch today's memos
memos = memos_client.list_memos(days=1)
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)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
logger.info("Empty digest written to %s", filepath)
return filepath, 0
# Step 2: Prepare prompt
memo_texts = []
for m in memos:
time_str = m["created_at"][:16].replace("T", " ")
memo_texts.append(f"[{time_str}] {m['content']}")
user_prompt = f"请分析以下灵感记录:\n\n" + "\n".join(memo_texts)
# Step 3: Call DeepSeek API
raw_response = llm_client.ask(
system_prompt=DAILY_SYSTEM_PROMPT,
user_prompt=user_prompt,
temperature=0.3
)
# 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": []
}
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)
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)
)
return filepath, len(memos)
def main():
"""CLI entry point."""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
try:
secrets = load_secrets()
except FileNotFoundError as e:
print(e)
sys.exit(1)
memos_client = MemosClient(
base_url=secrets.get("memos_url", "http://localhost:5230"),
access_token=secrets["memos_token"]
)
llm_client = DeepSeekClient(
api_key=secrets["deepseek_api_key"],
model=secrets.get("deepseek_model", "deepseek-chat")
)
filepath, count = run(memos_client, llm_client)
print(f"Done: {filepath} ({count} memos)")
if __name__ == "__main__":
main()

159
analyzers/weekly_trend.py Normal file
View File

@ -0,0 +1,159 @@
"""Weekly trend analyzer - identifies patterns across a week of memos."""
import json
import logging
import os
import sys
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
from tools.memos_client import MemosClient
from tools.formatter import format_weekly_trend
logger = logging.getLogger(__name__)
WEEKLY_SYSTEM_PROMPT = """你是一个灵感趋势分析师。你的任务是对用户一周的灵感记录进行趋势分析。
请输出纯 JSON不要代码块标记不要额外说明
{
"daily_counts": {
"周一": 3,
"周二": 1
},
"categories": {
"分类名1": 5,
"分类名2": 3
},
"highlights": ["本周最有价值的灵感1", "2-3条"],
"insight": "一段关于用户本周思维模式的洞察60字以内"
}
分类原则根据内容自然归类分类名不超过4个字。
亮点:选出本周最有启发性、行动性、或值得继续深挖的灵感。
洞察:识别用户本周的关注焦点变化、思维模式、或信息缺口。"""
def parse_ai_response(text):
"""Parse AI JSON response, handling code block wrappers."""
text = text.strip()
if text.startswith("```"):
lines = text.split("\n")
if lines[0].startswith("```"):
lines = lines[1:]
if lines and lines[-1].strip() == "```":
lines = lines[:-1]
text = "\n".join(lines).strip()
return json.loads(text)
def run(memos_client, llm_client, date=None):
"""Run weekly trend analysis."""
date = date or datetime.now(timezone.utc)
# Calculate week start (Monday) and end (Sunday)
weekday = date.weekday() # 0=Monday
week_start = date - timedelta(days=weekday + 7) # Previous Monday
week_end = week_start + timedelta(days=6) # Sunday
logger.info(
"Weekly trend started for W%s (%s - %s)",
week_start.isocalendar()[1],
week_start.strftime("%Y-%m-%d"),
week_end.strftime("%Y-%m-%d")
)
# Fetch week's memos
memos = memos_client.list_all_memos_from_range(week_start, week_end)
if not memos:
logger.info("No memos this week, skipping")
stats = {"total": 0, "daily_counts": {}, "categories": {}}
content = format_weekly_trend(week_start, week_end, stats, [], "")
output_dir = get_output_dir("weekly")
filename = f"{week_start.strftime('W%Y-%m-%d')}_trend.md"
filepath = os.path.join(output_dir, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
return filepath, 0
# Prepare prompt
memo_texts = []
for m in memos:
time_str = m["created_at"][:16].replace("T", " ")
memo_texts.append(f"[{time_str}] {m['content']}")
user_prompt = (
f"分析以下本周({week_start.strftime('%m/%d')} - {week_end.strftime('%m/%d')}"
f"{len(memos)} 条灵感记录的趋势:\n\n"
+ "\n".join(memo_texts)
)
# Call API
raw_response = llm_client.ask(
system_prompt=WEEKLY_SYSTEM_PROMPT,
user_prompt=user_prompt,
temperature=0.3
)
# Parse
try:
data = parse_ai_response(raw_response)
except (json.JSONDecodeError, KeyError) as e:
logger.error("Failed to parse AI response: %s", e)
data = {"daily_counts": {}, "categories": {}, "highlights": [], "insight": ""}
stats = {
"total": len(memos),
"daily_counts": data.get("daily_counts", {}),
"categories": data.get("categories", {}),
}
highlights = data.get("highlights", [])
insight = data.get("insight", "")
# Format output
content = format_weekly_trend(week_start, week_end, stats, highlights, insight)
output_dir = get_output_dir("weekly")
filename = f"{week_start.strftime('W%Y-%m-%d')}_trend.md"
filepath = os.path.join(output_dir, filename)
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
logger.info(
"Weekly trend written to %s | %d memos | %d categories",
filepath, len(memos), len(stats["categories"])
)
return filepath, len(memos)
def main():
"""CLI entry point."""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
try:
secrets = load_secrets()
except FileNotFoundError as e:
print(e)
sys.exit(1)
memos_client = MemosClient(
base_url=secrets.get("memos_url", "http://localhost:5230"),
access_token=secrets["memos_token"]
)
llm_client = DeepSeekClient(
api_key=secrets["deepseek_api_key"],
model=secrets.get("deepseek_model", "deepseek-chat")
)
filepath, count = run(memos_client, llm_client)
print(f"Done: {filepath} ({count} memos)")
if __name__ == "__main__":
main()

30
scripts/run_daily_digest.sh Executable file
View File

@ -0,0 +1,30 @@
#!/bin/bash
# Daily digest cron wrapper
# Run daily at 14:00 UTC (22:00 Beijing)
set -e
PROJECT_DIR="$HOME/inspiration-collector"
LOG_FILE="$PROJECT_DIR/ai-insights/logs/daily_digest.log"
mkdir -p "$(dirname "$LOG_FILE")"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting daily digest..." >> "$LOG_FILE"
cd "$PROJECT_DIR"
python3 analyzers/daily_digest.py >> "$LOG_FILE" 2>&1
# Git push to Gitea
cd "$PROJECT_DIR"
# Check if there are changes
if [[ -n $(git status --porcelain ai-insights/) ]]; then
git add ai-insights/
git commit -m "daily digest $(date '+%Y-%m-%d')"
git push origin main 2>&1 | tail -3 >> "$LOG_FILE"
echo "Pushed to Gitea" >> "$LOG_FILE"
else
echo "No changes to push" >> "$LOG_FILE"
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Daily digest done." >> "$LOG_FILE"

28
scripts/run_weekly_trend.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
# Weekly trend cron wrapper
# Run every Sunday at 08:00 UTC (16:00 Beijing)
set -e
PROJECT_DIR="$HOME/inspiration-collector"
LOG_FILE="$PROJECT_DIR/ai-insights/logs/weekly_trend.log"
mkdir -p "$(dirname "$LOG_FILE")"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting weekly trend..." >> "$LOG_FILE"
cd "$PROJECT_DIR"
python3 analyzers/weekly_trend.py >> "$LOG_FILE" 2>&1
# Git push to Gitea
cd "$PROJECT_DIR"
if [[ -n $(git status --porcelain ai-insights/) ]]; then
git add ai-insights/
git commit -m "weekly trend W$(date '+%V')"
git push origin main 2>&1 | tail -3 >> "$LOG_FILE"
echo "Pushed to Gitea" >> "$LOG_FILE"
else
echo "No changes to push" >> "$LOG_FILE"
fi
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Weekly trend done." >> "$LOG_FILE"

4
secrets/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Secrets directory - never checked into git
# Copy secrets.json.template to secrets.json and fill in your keys
secrets.json
*.json

View File

@ -0,0 +1,6 @@
{
"memos_url": "http://localhost:5230",
"memos_token": "YOUR_MEMOS_TOKEN_HERE",
"deepseek_api_key": "YOUR_DEEPSEEK_API_KEY_HERE",
"deepseek_model": "deepseek-chat"
}

0
tools/__init__.py Normal file
View File

35
tools/config.py Normal file
View File

@ -0,0 +1,35 @@
"""Configuration loader - loads secrets from secrets.json"""
import json
import os
from pathlib import Path
# Default location: ~/inspiration-collector/secrets/secrets.json on server
SECRETS_PATH = os.environ.get(
"IC_SECRETS_PATH",
str(Path.home() / "inspiration-collector" / "secrets" / "secrets.json")
)
DEFAULT_OUTPUT_DIR = os.environ.get(
"IC_OUTPUT_DIR",
str(Path.home() / "inspiration-collector" / "ai-insights")
)
def load_secrets(path=None):
"""Load secrets from JSON file. Returns dict with api_key, memos_token, memos_url."""
path = path or SECRETS_PATH
if not os.path.exists(path):
raise FileNotFoundError(
f"Secrets file not found: {path}\n"
f"Copy secrets/secrets.json.template to {path} and fill in your keys."
)
with open(path) as f:
return json.load(f)
def get_output_dir(subdir="daily"):
"""Get output directory for AI-generated insights."""
base = DEFAULT_OUTPUT_DIR
os.makedirs(os.path.join(base, subdir), exist_ok=True)
return os.path.join(base, subdir)

122
tools/formatter.py Normal file
View File

@ -0,0 +1,122 @@
"""Markdown formatter - converts AI analysis results to beautiful .md files."""
from datetime import datetime
def format_daily_digest(date, categories, summary, connections, todos):
"""Format daily digest markdown file."""
date_str = date.strftime("%Y-%m-%d")
weekday = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"][date.weekday()]
lines = []
lines.append("---")
lines.append(f"date: {date_str}")
lines.append("type: daily-digest")
lines.append("tags: [灵感收集器, 每日总结]")
lines.append("---")
lines.append("")
lines.append(f"# {date_str} 灵感摘要 · {weekday}")
lines.append("")
# Summary
lines.append("## AI 分析(自动生成,请勿编辑)")
lines.append("")
lines.append(summary)
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("## 我的批注")
lines.append("")
lines.append("> *在这里写下你的想法、质疑、补充*")
lines.append("")
return "\n".join(lines)
def format_weekly_trend(start_date, end_date, stats, highlights, insight):
"""Format weekly trend markdown file."""
week_start = start_date.strftime("%m/%d")
week_end = end_date.strftime("%m/%d")
week_number = start_date.isocalendar()[1]
lines = []
lines.append("---")
lines.append(f"date: {start_date.year}-W{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("")
# Stats
lines.append("## AI 分析(自动生成,请勿编辑)")
lines.append("")
lines.append(f"本周共记录 **{stats.get('total', 0)}** 条灵感。")
lines.append("")
if stats.get("daily_counts"):
lines.append("### 每日活跃度")
lines.append("")
for day, count in stats["daily_counts"].items():
bar = "" * count if count > 0 else ""
lines.append(f"- {day}{bar} {count}")
lines.append("")
if stats.get("categories"):
lines.append("### 主题分布")
lines.append("")
for cat, count in stats["categories"].items():
pct = count / stats["total"] * 100 if stats["total"] else 0
lines.append(f"- **{cat}**{count}条({pct:.0f}%")
lines.append("")
if highlights:
lines.append("### 本周亮点")
lines.append("")
for h in highlights:
lines.append(f"- {h}")
lines.append("")
if insight:
lines.append("### 值得关注的模式")
lines.append("")
lines.append(insight)
lines.append("")
# User annotation
lines.append("---")
lines.append("")
lines.append("## 我的批注")
lines.append("")
lines.append("> *有什么想补充的写在下面*")
lines.append("")
return "\n".join(lines)

71
tools/llm.py Normal file
View File

@ -0,0 +1,71 @@
"""DeepSeek API client - OpenAI-compatible, zero extra dependencies."""
import json
import logging
import time
import requests
logger = logging.getLogger(__name__)
class DeepSeekClient:
"""Client for DeepSeek API (OpenAI-compatible format)."""
BASE_URL = "https://api.deepseek.com/v1/chat/completions"
def __init__(self, api_key, model="deepseek-chat", temperature=0.7, max_retries=3):
self.api_key = api_key
self.model = model
self.temperature = temperature
self.max_retries = max_retries
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
})
def ask(self, system_prompt, user_prompt, temperature=None):
"""Send a chat completion request. Returns response text."""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
]
payload = {
"model": self.model,
"messages": messages,
"temperature": temperature or self.temperature
}
last_error = None
for attempt in range(self.max_retries):
try:
resp = self.session.post(self.BASE_URL, json=payload, timeout=60)
resp.raise_for_status()
data = resp.json()
result = data["choices"][0]["message"]["content"]
logger.info(
"DeepSeek API OK | model=%s | input=%d chars | output=%d chars",
self.model, len(user_prompt), len(result)
)
return result
except requests.exceptions.Timeout:
last_error = "Timeout"
logger.warning("Attempt %d/%d timed out", attempt + 1, self.max_retries)
except requests.exceptions.HTTPError as e:
last_error = str(e)
logger.warning("Attempt %d/%d HTTP error: %s", attempt + 1, self.max_retries, e)
if "insufficient_quota" in str(e).lower() or "invalid_api_key" in str(e).lower():
raise # don't retry these
except (json.JSONDecodeError, KeyError) as e:
last_error = f"Parse error: {e}"
logger.warning("Attempt %d/%d parse error: %s", attempt + 1, self.max_retries, e)
if attempt < self.max_retries - 1:
sleep_time = 2 ** attempt
logger.info("Retrying in %ds...", sleep_time)
time.sleep(sleep_time)
raise RuntimeError(
f"DeepSeek API failed after {self.max_retries} retries. Last error: {last_error}"
)

138
tools/memos_client.py Normal file
View File

@ -0,0 +1,138 @@
"""Memos API client - fetch memos using Connect RPC protocol."""
import logging
from datetime import datetime, timezone, timedelta
import requests
logger = logging.getLogger(__name__)
class MemosClient:
"""Client for self-hosted Memos API (Connect RPC protocol)."""
def __init__(self, base_url, access_token):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
})
self._user_id = None
def get_user(self):
"""Get current user info via Connect RPC."""
resp = self.session.post(
f"{self.base_url}/memos.api.v1.AuthService/GetCurrentUser",
json={},
timeout=10
)
resp.raise_for_status()
data = resp.json()
user = data.get("user", data)
self._user_id = user.get("name", "")
logger.info("Memos user: %s (%s)", user.get("username"), self._user_id)
return user
def get_user_id(self):
"""Get current user's resource name (e.g. 'users/FXY')."""
if not self._user_id:
self.get_user()
return self._user_id
def list_memos(self, days=1, page_size=100):
"""Fetch memos from the last N days via Connect RPC."""
user = self.get_user_id()
since = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
# Connect RPC uses POST with JSON body
payload = {
"pageSize": page_size,
"filter": f"creator == '{user}'",
}
resp = self.session.post(
f"{self.base_url}/memos.api.v1.MemoService/ListMemos",
json=payload,
timeout=15
)
resp.raise_for_status()
data = resp.json()
memos = data.get("memos", [])
logger.info("Memos API OK | fetched %d memos", len(memos))
# Filter by time client-side (Connect RPC filter might not work as expected)
results = []
since_dt = datetime.now(timezone.utc) - timedelta(days=days)
for m in memos:
created = m.get("createTime", "")
try:
created_dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
except (ValueError, AttributeError):
created_dt = datetime.now(timezone.utc)
if created_dt < since_dt:
continue
results.append({
"id": m.get("name", "").split("/")[-1],
"content": m.get("content", ""),
"created_at": created,
"tags": self._extract_tags(m.get("content", "")),
"visibility": m.get("visibility", ""),
})
logger.info(
"Filtered to %d memos since %s", len(results), since[:10]
)
return results
def list_all_memos_from_range(self, start_date, end_date, page_size=200):
"""Fetch memos within a date range via Connect RPC."""
user = self.get_user_id()
payload = {"pageSize": page_size}
resp = self.session.post(
f"{self.base_url}/memos.api.v1.MemoService/ListMemos",
json=payload,
timeout=15
)
resp.raise_for_status()
data = resp.json()
memos = data.get("memos", [])
logger.info("Memos API OK | fetched %d memos total", len(memos))
# Client-side date range filtering
results = []
for m in memos:
created = m.get("createTime", "")
try:
created_dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
except (ValueError, AttributeError):
continue
if created_dt < start_date or created_dt > end_date:
continue
results.append({
"id": m.get("name", "").split("/")[-1],
"content": m.get("content", ""),
"created_at": created,
"tags": self._extract_tags(m.get("content", "")),
"visibility": m.get("visibility", ""),
})
logger.info(
"Filtered to %d memos in range %s - %s",
len(results), start_date.strftime("%m/%d"), end_date.strftime("%m/%d")
)
return results
@staticmethod
def _extract_tags(content):
"""Extract #tags from memo content."""
import re
return re.findall(r"#(\w[\w\-]*)", content)