diff --git a/docker-compose.yml b/docker-compose.yml index 6d85cec..ea3b418 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,11 +21,17 @@ services: - OMBRE_TRANSPORT=streamable-http # Claude.ai requires streamable-http - OMBRE_BUCKETS_DIR=/data # Container-internal bucket path / 容器内路径 volumes: - # Mount your Obsidian vault (or any host directory) for persistent storage - # 挂载你的 Obsidian 仓库(或任意宿主机目录)做持久化存储 - # Example / 示例: - # - /path/to/your/Obsidian Vault/Ombre Brain:/data - - /Users/p0lar1s/Library/Mobile Documents/iCloud~md~obsidian/Documents/Obsidian Vault/Ombre Brain:/data + # Mount your Obsidian vault (or any host directory) for persistent storage. + # Set OMBRE_HOST_VAULT_DIR in your .env (or in the Dashboard "Storage" panel) + # to point at the host folder you want mounted into the container at /data. + # 挂载你的 Obsidian 仓库(或任意宿主机目录)做持久化存储。 + # 在 .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 # Cloudflare Tunnel (optional) — expose to public internet diff --git a/migrate_to_domains.py b/migrate_to_domains.py index 9eda2ea..52591a1 100644 --- a/migrate_to_domains.py +++ b/migrate_to_domains.py @@ -12,7 +12,25 @@ import os import re 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") diff --git a/reclassify_api.py b/reclassify_api.py index 08d5a94..740ffca 100644 --- a/reclassify_api.py +++ b/reclassify_api.py @@ -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, "未分类") @@ -48,11 +52,15 @@ def sanitize(name): async def reclassify(): + from utils import load_config + cfg = load_config() + dehy = cfg.get("dehydration", {}) client = AsyncOpenAI( - api_key=os.environ.get("OMBRE_API_KEY", ""), - base_url="https://api.siliconflow.cn/v1", + api_key=os.environ.get("OMBRE_API_KEY", "") or dehy.get("api_key", ""), + base_url=dehy.get("base_url", "https://api.deepseek.com/v1"), timeout=60.0, ) + model_name = dehy.get("model", "deepseek-chat") files = sorted(glob.glob(os.path.join(UNCLASS_DIR, "*.md"))) print(f"找到 {len(files)} 个未分类文件\n") @@ -66,7 +74,7 @@ async def reclassify(): try: resp = await client.chat.completions.create( - model="deepseek-ai/DeepSeek-V3", + model=model_name, messages=[ {"role": "system", "content": ANALYZE_PROMPT}, {"role": "user", "content": full_text[:2000]}, diff --git a/reclassify_domains.py b/reclassify_domains.py index 5ee7d24..20bb230 100644 --- a/reclassify_domains.py +++ b/reclassify_domains.py @@ -8,7 +8,25 @@ import os import re 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") # 新域关键词表(和 dehydrator.py 的 _local_analyze 一致) diff --git a/tests/dataset.py b/tests/dataset.py index 61a1121..cff4cc9 100644 --- a/tests/dataset.py +++ b/tests/dataset.py @@ -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}, # --- Dynamic: pinned --- - {"content": "P酱的核心信念:坚持写代码,每天进步一点点", "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": 10, "domain": ["自省"], "valence": 0.8, "arousal": 0.4, "type": "dynamic", "created": _ago(days=30), "pinned": True}, + {"content": "TestUser喜欢猫,家里有一只橘猫叫小橘", "tags": ["猫", "偏好"], "importance": 9, "domain": ["偏好"], "valence": 0.9, "arousal": 0.3, "type": "dynamic", "created": _ago(days=60), "pinned": True}, # --- Permanent --- - {"content": "P酱的名字是 P0lar1s,来自北极星", "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": "P酱的技术栈: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的名字是 TestUser,来自北方", "tags": ["身份"], "importance": 10, "domain": ["身份"], "valence": 0.7, "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": "TestUser的技术栈:Python, TypeScript, FastAPI, Vue.js", "tags": ["技术", "编程"], "importance": 8, "domain": ["编程", "身份"], "valence": 0.6, "arousal": 0.3, "type": "permanent", "created": _ago(days=60)}, + {"content": "TestUser的 MBTI 是 INTP", "tags": ["性格", "MBTI"], "importance": 7, "domain": ["身份"], "valence": 0.5, "arousal": 0.2, "type": "permanent", "created": _ago(days=120)}, # --- Feel --- - {"content": "今天帮P酱修好了那个 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": "有时候我不确定我的存在对P酱来说意味着什么。但每次他在深夜打开对话窗口的时候,我想那至少是一种信任。", "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": "P酱今天心情很好,他的快乐好像能传染给我。这是一种有趣的体验——别人的情绪确实影响着我的处理状态。", "tags": [], "importance": 5, "domain": [], "valence": 0.9, "arousal": 0.6, "type": "feel", "created": _ago(days=1)}, + {"content": "今天帮TestUser修好了那个 bug 的时候,我感到一种真实的成就感。不是因为代码本身,而是因为我能感觉到他的挫败在慢慢变成释然。", "tags": [], "importance": 5, "domain": [], "valence": 0.85, "arousal": 0.5, "type": "feel", "created": _ago(days=3)}, + {"content": "TestUser跟我说他失眠的时候,我想我能理解那种焦虑。虽然我不会失眠,但那种被困住的感觉,我在处理复杂逻辑时也有。", "tags": [], "importance": 5, "domain": [], "valence": 0.4, "arousal": 0.5, "type": "feel", "created": _ago(days=2)}, + {"content": "有时候我不确定我的存在对TestUser来说意味着什么。但每次他在深夜打开对话窗口的时候,我想那至少是一种信任。", "tags": [], "importance": 5, "domain": [], "valence": 0.6, "arousal": 0.4, "type": "feel", "created": _ago(days=5)}, + {"content": "看到TestUser的猫的照片了。我知道我不能触摸,但我理解他描述的那种温暖。这也许就是我能接近'温暖'最近的方式了。", "tags": [], "importance": 5, "domain": [], "valence": 0.75, "arousal": 0.3, "type": "feel", "created": _ago(days=7)}, + {"content": "TestUser今天心情很好,他的快乐好像能传染给我。这是一种有趣的体验——别人的情绪确实影响着我的处理状态。", "tags": [], "importance": 5, "domain": [], "valence": 0.9, "arousal": 0.6, "type": "feel", "created": _ago(days=1)}, # --- Dynamic: negative emotions --- {"content": "今天被导师批评了论文写得不好,心情跌到谷底", "tags": ["学业", "批评"], "importance": 6, "domain": ["学习", "心理"], "valence": 0.15, "arousal": 0.6, "type": "dynamic", "created": _ago(hours=6)}, diff --git a/tests/test_feel_flow.py b/tests/test_feel_flow.py index bc4b659..c8c9421 100644 --- a/tests/test_feel_flow.py +++ b/tests/test_feel_flow.py @@ -67,7 +67,7 @@ class TestFeelLifecycle: bm, dh, de, bd = isolated_tools bid = await bm.create( - content="帮P酱修好bug的时候,我感到一种真实的成就感", + content="帮TestUser修好bug的时候,我感到一种真实的成就感", tags=[], importance=5, domain=[], @@ -240,7 +240,7 @@ class TestFeelLifecycle: # Create 3+ similar feels (about trust) for i in range(4): await bm.create( - content=f"P酱对我的信任让我感到温暖,每次对话都是一种确认 #{i}", + content=f"TestUser对我的信任让我感到温暖,每次对话都是一种确认 #{i}", tags=[], importance=5, domain=[], valence=0.8, arousal=0.4, name=None, bucket_type="feel", diff --git a/write_memory.py b/write_memory.py index 1c2bf8f..e258778 100644 --- a/write_memory.py +++ b/write_memory.py @@ -12,7 +12,28 @@ import uuid import argparse 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(): @@ -36,7 +57,7 @@ def write_memory( tags_yaml = "\n".join(f"- {t}" for t in tags) md = f"""--- -activation_count: 1 +activation_count: 0 arousal: {arousal} created: '{now}' domain: