126 lines
3.9 KiB
Python
Executable File
126 lines
3.9 KiB
Python
Executable File
"""Extract todos from the latest inspiration digest + sync to todo API.
|
||
Supports multiple formats:
|
||
- [ ] checkbox style
|
||
- 1. **bold**: description (numbered list with bold prefix)
|
||
- - **bold**: description (dash list with bold prefix)
|
||
"""
|
||
import json
|
||
import os
|
||
import re
|
||
import requests
|
||
|
||
DIGEST_DIR = os.path.expanduser("~/inspiration-collector/ai-insights/daily")
|
||
TODO_API = "http://127.0.0.1:9001/api/todos"
|
||
|
||
|
||
def get_latest_digest():
|
||
"""Find the most recent digest file (checks date subdirectories)."""
|
||
if not os.path.isdir(DIGEST_DIR):
|
||
return None
|
||
files = []
|
||
for root, dirs, filenames in os.walk(DIGEST_DIR):
|
||
for f in filenames:
|
||
if "_digest" in f and f.endswith(".md"):
|
||
files.append(os.path.join(root, f))
|
||
if not files:
|
||
return None
|
||
files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
|
||
return files[0]
|
||
|
||
|
||
def extract_todos(filepath):
|
||
"""Extract todo items from the digest markdown.
|
||
Matches:
|
||
1. [ ] checkbox items
|
||
2. Numbered items like "1. **bold title**:description"
|
||
3. Dash items like "- **bold title**:description"
|
||
Only extracts from the "待办事项" section onwards.
|
||
"""
|
||
with open(filepath) as f:
|
||
content = f.read()
|
||
|
||
todos = []
|
||
in_todo_section = False
|
||
|
||
for line in content.split("\n"):
|
||
stripped = line.strip()
|
||
|
||
# Detect start of todo section
|
||
if re.search(r"待办事项|待办|TODO|Todo", stripped) and not in_todo_section:
|
||
in_todo_section = True
|
||
continue
|
||
|
||
# End of todo section: next major heading or empty line after content
|
||
if in_todo_section and stripped.startswith("## ") and "待办" not in stripped:
|
||
break
|
||
if in_todo_section and stripped == "" and todos:
|
||
# Allow one blank line, but stop at second
|
||
if todos and todos[-1] == "__blank__":
|
||
todos.pop() # remove previous blank marker
|
||
break
|
||
todos.append("__blank__")
|
||
continue
|
||
|
||
if not in_todo_section:
|
||
continue
|
||
|
||
# Pattern 1: [ ] checkbox
|
||
m = re.match(r"\s*[-*]\s*\[\s*[ ]\s*\]\s*(.+)", line)
|
||
if m:
|
||
text = re.sub(r"\*\*", "", m.group(1).strip()) # strip bold markers
|
||
if text:
|
||
todos.append(text)
|
||
continue
|
||
|
||
# Pattern 2: numbered list "1. **bold**:description" or "1. description"
|
||
m = re.match(r"\s*\d+\.\s+(.+)", line)
|
||
if m:
|
||
text = re.sub(r"\*\*", "", m.group(1).strip()) # strip bold markers
|
||
if text:
|
||
todos.append(text)
|
||
continue
|
||
|
||
# Pattern 3: dash list "- **bold**:description"
|
||
m = re.match(r"\s*[-*]\s+(.+)", line)
|
||
if m:
|
||
text = re.sub(r"\*\*", "", m.group(1).strip())
|
||
# Skip template headers like 【今日输入】
|
||
if text and len(text) > 8 and not re.match(r"^【.+】$", text):
|
||
todos.append(text)
|
||
continue
|
||
|
||
# Clean up blank markers
|
||
todos = [t for t in todos if t != "__blank__"]
|
||
return todos
|
||
|
||
|
||
def push_todos(todos):
|
||
"""Send extracted todos to the API (skip if already exists)."""
|
||
try:
|
||
resp = requests.get(TODO_API, timeout=5)
|
||
existing = [t["text"] for t in resp.json().get("todos", [])]
|
||
except:
|
||
existing = []
|
||
|
||
count = 0
|
||
for text in todos:
|
||
if text not in existing:
|
||
requests.post(TODO_API, json={"text": text, "source": "inspiration"}, timeout=5)
|
||
count += 1
|
||
return count
|
||
|
||
|
||
if __name__ == "__main__":
|
||
filepath = get_latest_digest()
|
||
if not filepath:
|
||
print("No digest found")
|
||
exit(0)
|
||
|
||
todos = extract_todos(filepath)
|
||
print(f"Found {len(todos)} todos in {os.path.basename(filepath)}")
|
||
if todos:
|
||
for t in todos:
|
||
print(f" - {t[:60]}")
|
||
pushed = push_todos(todos)
|
||
print(f"Pushed {pushed} new todos to API")
|