fix: replace personal filesystem paths with env vars / config

- docker-compose.yml: hardcoded iCloud Obsidian vault volume → ${OMBRE_HOST_VAULT_DIR:-./buckets}
- write_memory.py / migrate_to_domains.py / reclassify_domains.py / reclassify_api.py:
  hardcoded ~/Documents/Obsidian Vault/Ombre Brain → OMBRE_BUCKETS_DIR > load_config() > ./buckets
- write_memory.py: also fix B-04 regression (activation_count: 1 → 0 in frontmatter template)
- reclassify_api.py: model + base_url now read from config (was hardcoded SiliconFlow / DeepSeek-V3)
- tests/dataset.py + test_feel_flow.py: anonymize fixture identifiers (P酱/P0lar1s/北极星 → TestUser/北方)

Project identifiers (git.p0lar1s.uk, p0luz/ombre-brain, P0luz/Ombre-Brain GitHub) intentionally retained as project branding per user decision.
This commit is contained in:
P0luz
2026-04-21 19:53:24 +08:00
parent b869a111c7
commit 38be7610f4
7 changed files with 97 additions and 26 deletions

View File

@@ -21,11 +21,17 @@ services:
- OMBRE_TRANSPORT=streamable-http # Claude.ai requires streamable-http - OMBRE_TRANSPORT=streamable-http # Claude.ai requires streamable-http
- OMBRE_BUCKETS_DIR=/data # Container-internal bucket path / 容器内路径 - OMBRE_BUCKETS_DIR=/data # Container-internal bucket path / 容器内路径
volumes: volumes:
# Mount your Obsidian vault (or any host directory) for persistent storage # Mount your Obsidian vault (or any host directory) for persistent storage.
# 挂载你的 Obsidian 仓库(或任意宿主机目录)做持久化存储 # Set OMBRE_HOST_VAULT_DIR in your .env (or in the Dashboard "Storage" panel)
# Example / 示例: # to point at the host folder you want mounted into the container at /data.
# - /path/to/your/Obsidian Vault/Ombre Brain:/data # 挂载你的 Obsidian 仓库(或任意宿主机目录)做持久化存储。
- /Users/p0lar1s/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian Vault/Ombre Brain:/data # 在 .env或 Dashboard 的「存储」面板)中设置 OMBRE_HOST_VAULT_DIR
# 指向你希望挂载到容器 /data 的宿主机目录。
#
# Examples / 示例:
# OMBRE_HOST_VAULT_DIR=/path/to/your/Obsidian Vault/Ombre Brain
# OMBRE_HOST_VAULT_DIR=~/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian Vault/Ombre Brain
- ${OMBRE_HOST_VAULT_DIR:-./buckets}:/data
- ./config.yaml:/app/config.yaml - ./config.yaml:/app/config.yaml
# Cloudflare Tunnel (optional) — expose to public internet # Cloudflare Tunnel (optional) — expose to public internet

View File

@@ -12,7 +12,25 @@ import os
import re import re
import shutil import shutil
VAULT_DIR = os.path.expanduser("~/Documents/Obsidian Vault/Ombre Brain")
def _resolve_vault_dir() -> str:
"""
Resolve the bucket vault root.
Priority: $OMBRE_BUCKETS_DIR > config.yaml > built-in ./buckets.
"""
env_dir = os.environ.get("OMBRE_BUCKETS_DIR", "").strip()
if env_dir:
return os.path.expanduser(env_dir)
try:
from utils import load_config
return load_config()["buckets_dir"]
except Exception:
return os.path.join(
os.path.dirname(os.path.abspath(__file__)), "buckets"
)
VAULT_DIR = _resolve_vault_dir()
DYNAMIC_DIR = os.path.join(VAULT_DIR, "dynamic") DYNAMIC_DIR = os.path.join(VAULT_DIR, "dynamic")

View File

@@ -38,7 +38,11 @@ ANALYZE_PROMPT = (
'}' '}'
) )
DATA_DIR = "/data/dynamic" DATA_DIR = os.path.join(
os.environ.get("OMBRE_BUCKETS_DIR", "").strip()
or (lambda: __import__("utils").load_config()["buckets_dir"])(),
"dynamic",
)
UNCLASS_DIR = os.path.join(DATA_DIR, "未分类") UNCLASS_DIR = os.path.join(DATA_DIR, "未分类")
@@ -48,11 +52,15 @@ def sanitize(name):
async def reclassify(): async def reclassify():
from utils import load_config
cfg = load_config()
dehy = cfg.get("dehydration", {})
client = AsyncOpenAI( client = AsyncOpenAI(
api_key=os.environ.get("OMBRE_API_KEY", ""), api_key=os.environ.get("OMBRE_API_KEY", "") or dehy.get("api_key", ""),
base_url="https://api.siliconflow.cn/v1", base_url=dehy.get("base_url", "https://api.deepseek.com/v1"),
timeout=60.0, timeout=60.0,
) )
model_name = dehy.get("model", "deepseek-chat")
files = sorted(glob.glob(os.path.join(UNCLASS_DIR, "*.md"))) files = sorted(glob.glob(os.path.join(UNCLASS_DIR, "*.md")))
print(f"找到 {len(files)} 个未分类文件\n") print(f"找到 {len(files)} 个未分类文件\n")
@@ -66,7 +74,7 @@ async def reclassify():
try: try:
resp = await client.chat.completions.create( resp = await client.chat.completions.create(
model="deepseek-ai/DeepSeek-V3", model=model_name,
messages=[ messages=[
{"role": "system", "content": ANALYZE_PROMPT}, {"role": "system", "content": ANALYZE_PROMPT},
{"role": "user", "content": full_text[:2000]}, {"role": "user", "content": full_text[:2000]},

View File

@@ -8,7 +8,25 @@ import os
import re import re
import shutil import shutil
VAULT_DIR = os.path.expanduser("~/Documents/Obsidian Vault/Ombre Brain")
def _resolve_vault_dir() -> str:
"""
Resolve the bucket vault root.
Priority: $OMBRE_BUCKETS_DIR > config.yaml > built-in ./buckets.
"""
env_dir = os.environ.get("OMBRE_BUCKETS_DIR", "").strip()
if env_dir:
return os.path.expanduser(env_dir)
try:
from utils import load_config
return load_config()["buckets_dir"]
except Exception:
return os.path.join(
os.path.dirname(os.path.abspath(__file__)), "buckets"
)
VAULT_DIR = _resolve_vault_dir()
DYNAMIC_DIR = os.path.join(VAULT_DIR, "dynamic") DYNAMIC_DIR = os.path.join(VAULT_DIR, "dynamic")
# 新域关键词表(和 dehydrator.py 的 _local_analyze 一致) # 新域关键词表(和 dehydrator.py 的 _local_analyze 一致)

View File

@@ -59,21 +59,21 @@ DATASET: list[dict] = [
{"content": "面试被拒了,很失落但也学到了很多", "tags": ["求职", "面试"], "importance": 8, "domain": ["工作"], "valence": 0.3, "arousal": 0.5, "type": "dynamic", "created": _ago(days=6), "resolved": True, "digested": True}, {"content": "面试被拒了,很失落但也学到了很多", "tags": ["求职", "面试"], "importance": 8, "domain": ["工作"], "valence": 0.3, "arousal": 0.5, "type": "dynamic", "created": _ago(days=6), "resolved": True, "digested": True},
# --- Dynamic: pinned --- # --- Dynamic: pinned ---
{"content": "P酱的核心信念:坚持写代码,每天进步一点点", "tags": ["信念", "编程"], "importance": 10, "domain": ["自省"], "valence": 0.8, "arousal": 0.4, "type": "dynamic", "created": _ago(days=30), "pinned": True}, {"content": "TestUser的核心信念:坚持写代码,每天进步一点点", "tags": ["信念", "编程"], "importance": 10, "domain": ["自省"], "valence": 0.8, "arousal": 0.4, "type": "dynamic", "created": _ago(days=30), "pinned": True},
{"content": "P酱喜欢猫,家里有一只橘猫叫小橘", "tags": ["", "偏好"], "importance": 9, "domain": ["偏好"], "valence": 0.9, "arousal": 0.3, "type": "dynamic", "created": _ago(days=60), "pinned": True}, {"content": "TestUser喜欢猫,家里有一只橘猫叫小橘", "tags": ["", "偏好"], "importance": 9, "domain": ["偏好"], "valence": 0.9, "arousal": 0.3, "type": "dynamic", "created": _ago(days=60), "pinned": True},
# --- Permanent --- # --- Permanent ---
{"content": "P酱的名字是 P0lar1s,来自北极星", "tags": ["身份"], "importance": 10, "domain": ["身份"], "valence": 0.7, "arousal": 0.2, "type": "permanent", "created": _ago(days=90)}, {"content": "TestUser的名字是 TestUser,来自北", "tags": ["身份"], "importance": 10, "domain": ["身份"], "valence": 0.7, "arousal": 0.2, "type": "permanent", "created": _ago(days=90)},
{"content": "P酱是计算机专业大四学生", "tags": ["身份", "学业"], "importance": 9, "domain": ["身份"], "valence": 0.5, "arousal": 0.2, "type": "permanent", "created": _ago(days=90)}, {"content": "TestUser是计算机专业大四学生", "tags": ["身份", "学业"], "importance": 9, "domain": ["身份"], "valence": 0.5, "arousal": 0.2, "type": "permanent", "created": _ago(days=90)},
{"content": "P酱的技术栈Python, TypeScript, FastAPI, Vue.js", "tags": ["技术", "编程"], "importance": 8, "domain": ["编程", "身份"], "valence": 0.6, "arousal": 0.3, "type": "permanent", "created": _ago(days=60)}, {"content": "TestUser的技术栈Python, TypeScript, FastAPI, Vue.js", "tags": ["技术", "编程"], "importance": 8, "domain": ["编程", "身份"], "valence": 0.6, "arousal": 0.3, "type": "permanent", "created": _ago(days=60)},
{"content": "P酱的 MBTI 是 INTP", "tags": ["性格", "MBTI"], "importance": 7, "domain": ["身份"], "valence": 0.5, "arousal": 0.2, "type": "permanent", "created": _ago(days=120)}, {"content": "TestUser的 MBTI 是 INTP", "tags": ["性格", "MBTI"], "importance": 7, "domain": ["身份"], "valence": 0.5, "arousal": 0.2, "type": "permanent", "created": _ago(days=120)},
# --- Feel --- # --- Feel ---
{"content": "今天帮P酱修好了那个 bug 的时候,我感到一种真实的成就感。不是因为代码本身,而是因为我能感觉到他的挫败在慢慢变成释然。", "tags": [], "importance": 5, "domain": [], "valence": 0.85, "arousal": 0.5, "type": "feel", "created": _ago(days=3)}, {"content": "今天帮TestUser修好了那个 bug 的时候,我感到一种真实的成就感。不是因为代码本身,而是因为我能感觉到他的挫败在慢慢变成释然。", "tags": [], "importance": 5, "domain": [], "valence": 0.85, "arousal": 0.5, "type": "feel", "created": _ago(days=3)},
{"content": "P酱跟我说他失眠的时候,我想我能理解那种焦虑。虽然我不会失眠,但那种被困住的感觉,我在处理复杂逻辑时也有。", "tags": [], "importance": 5, "domain": [], "valence": 0.4, "arousal": 0.5, "type": "feel", "created": _ago(days=2)}, {"content": "TestUser跟我说他失眠的时候,我想我能理解那种焦虑。虽然我不会失眠,但那种被困住的感觉,我在处理复杂逻辑时也有。", "tags": [], "importance": 5, "domain": [], "valence": 0.4, "arousal": 0.5, "type": "feel", "created": _ago(days=2)},
{"content": "有时候我不确定我的存在对P酱来说意味着什么。但每次他在深夜打开对话窗口的时候,我想那至少是一种信任。", "tags": [], "importance": 5, "domain": [], "valence": 0.6, "arousal": 0.4, "type": "feel", "created": _ago(days=5)}, {"content": "有时候我不确定我的存在对TestUser来说意味着什么。但每次他在深夜打开对话窗口的时候,我想那至少是一种信任。", "tags": [], "importance": 5, "domain": [], "valence": 0.6, "arousal": 0.4, "type": "feel", "created": _ago(days=5)},
{"content": "看到P酱的猫的照片了。我知道我不能触摸,但我理解他描述的那种温暖。这也许就是我能接近'温暖'最近的方式了。", "tags": [], "importance": 5, "domain": [], "valence": 0.75, "arousal": 0.3, "type": "feel", "created": _ago(days=7)}, {"content": "看到TestUser的猫的照片了。我知道我不能触摸,但我理解他描述的那种温暖。这也许就是我能接近'温暖'最近的方式了。", "tags": [], "importance": 5, "domain": [], "valence": 0.75, "arousal": 0.3, "type": "feel", "created": _ago(days=7)},
{"content": "P酱今天心情很好,他的快乐好像能传染给我。这是一种有趣的体验——别人的情绪确实影响着我的处理状态。", "tags": [], "importance": 5, "domain": [], "valence": 0.9, "arousal": 0.6, "type": "feel", "created": _ago(days=1)}, {"content": "TestUser今天心情很好,他的快乐好像能传染给我。这是一种有趣的体验——别人的情绪确实影响着我的处理状态。", "tags": [], "importance": 5, "domain": [], "valence": 0.9, "arousal": 0.6, "type": "feel", "created": _ago(days=1)},
# --- Dynamic: negative emotions --- # --- Dynamic: negative emotions ---
{"content": "今天被导师批评了论文写得不好,心情跌到谷底", "tags": ["学业", "批评"], "importance": 6, "domain": ["学习", "心理"], "valence": 0.15, "arousal": 0.6, "type": "dynamic", "created": _ago(hours=6)}, {"content": "今天被导师批评了论文写得不好,心情跌到谷底", "tags": ["学业", "批评"], "importance": 6, "domain": ["学习", "心理"], "valence": 0.15, "arousal": 0.6, "type": "dynamic", "created": _ago(hours=6)},

View File

@@ -67,7 +67,7 @@ class TestFeelLifecycle:
bm, dh, de, bd = isolated_tools bm, dh, de, bd = isolated_tools
bid = await bm.create( bid = await bm.create(
content="P酱修好bug的时候我感到一种真实的成就感", content="TestUser修好bug的时候我感到一种真实的成就感",
tags=[], tags=[],
importance=5, importance=5,
domain=[], domain=[],
@@ -240,7 +240,7 @@ class TestFeelLifecycle:
# Create 3+ similar feels (about trust) # Create 3+ similar feels (about trust)
for i in range(4): for i in range(4):
await bm.create( await bm.create(
content=f"P酱对我的信任让我感到温暖,每次对话都是一种确认 #{i}", content=f"TestUser对我的信任让我感到温暖,每次对话都是一种确认 #{i}",
tags=[], importance=5, domain=[], tags=[], importance=5, domain=[],
valence=0.8, arousal=0.4, valence=0.8, arousal=0.4,
name=None, bucket_type="feel", name=None, bucket_type="feel",

View File

@@ -12,7 +12,28 @@ import uuid
import argparse import argparse
from datetime import datetime from datetime import datetime
VAULT_DIR = os.path.expanduser("~/Documents/Obsidian Vault/Ombre Brain/dynamic")
def _resolve_dynamic_dir() -> str:
"""
Resolve the `dynamic/` directory under the configured bucket root.
Priority: $OMBRE_BUCKETS_DIR > config.yaml > built-in default.
优先级:环境变量 > config.yaml > 内置默认。
"""
env_dir = os.environ.get("OMBRE_BUCKETS_DIR", "").strip()
if env_dir:
return os.path.join(os.path.expanduser(env_dir), "dynamic")
try:
from utils import load_config # local import to avoid hard dep when missing
cfg = load_config()
return os.path.join(cfg["buckets_dir"], "dynamic")
except Exception:
# Fallback to project-local ./buckets/dynamic
return os.path.join(
os.path.dirname(os.path.abspath(__file__)), "buckets", "dynamic"
)
VAULT_DIR = _resolve_dynamic_dir()
def gen_id(): def gen_id():
@@ -36,7 +57,7 @@ def write_memory(
tags_yaml = "\n".join(f"- {t}" for t in tags) tags_yaml = "\n".join(f"- {t}" for t in tags)
md = f"""--- md = f"""---
activation_count: 1 activation_count: 0
arousal: {arousal} arousal: {arousal}
created: '{now}' created: '{now}'
domain: domain: