diff --git a/config.yaml b/config.yaml index b75ed9a..d967a1b 100644 --- a/config.yaml +++ b/config.yaml @@ -1,13 +1,18 @@ server: - api: "https://your-server.zeabur.app" - token: "your_api_token" - vault: "defaultVault" + api: "http://127.0.0.1:9000" + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsIm5pY2tuYW1lIjoiaHViaW4iLCJpcCI6IjE4MC4xNTIuNjUuOTkiLCJpc3MiOiJmYXN0LW5vdGUtc3luYy1zZXJ2aWNlIiwic3ViIjoidXNlci10b2tlbiIsImV4cCI6MTgwNzQxMzM0MCwibmJmIjoxNzc1ODc3MzQwLCJpYXQiOjE3NzU4NzczNDAsImp0aSI6IjEifQ.OgUOJXHrxxdNF9pv0tY0j5_qhBMsZsyaD_ByHqAgVtw" + vault: "my-vault" sync: - watch_path: "./vault" + watch_path: "/home/ubuntu/space/notes/my-vault" sync_notes: true sync_files: true sync_config: true + # 配置同步目录列表(以 . 开头的目录会被视为配置目录进行同步) + # Config directories to sync (dot-prefixed dirs treated as config) + config_sync_dirs: + - ".obsidian" + - ".agents" exclude_patterns: - ".git/**" - ".trash/**" @@ -21,4 +26,4 @@ client: logging: level: "INFO" - file: "" + file: "/home/ubuntu/space/notes/fns-cli.log" diff --git a/fns_cli/config.py b/fns_cli/config.py index 182f47b..331d606 100644 --- a/fns_cli/config.py +++ b/fns_cli/config.py @@ -25,6 +25,9 @@ class SyncConfig: default_factory=lambda: [".git/**", ".trash/**", "*.tmp"] ) file_chunk_size: int = 524288 + config_sync_dirs: list[str] = field( + default_factory=lambda: [".obsidian", ".agents"] + ) @dataclass @@ -91,6 +94,9 @@ def load_config(path: str) -> AppConfig: "exclude_patterns", [".git/**", ".trash/**", "*.tmp"] ), file_chunk_size=s.get("file_chunk_size", 524288), + config_sync_dirs=s.get( + "config_sync_dirs", [".obsidian", ".agents"] + ), ) if "client" in raw: diff --git a/fns_cli/file_sync.py b/fns_cli/file_sync.py index f152f04..bd333b5 100644 --- a/fns_cli/file_sync.py +++ b/fns_cli/file_sync.py @@ -501,9 +501,11 @@ def _collect_local_files(self) -> list[dict]: if self.engine.is_excluded(rel) or rel.endswith(".md"): continue first = rel.split("/")[0] - if first.startswith(".") and not self.config.sync.sync_config: + # Skip dot-prefixed directories that are handled by SettingSync + # (.obsidian, .agents, and other config dirs) + if first.startswith(".") and self.config.sync.sync_config: continue - if not first.startswith(".") and not self.config.sync.sync_files: + if not self.config.sync.sync_files: continue try: stat = fp.stat() diff --git a/fns_cli/folder_sync.py b/fns_cli/folder_sync.py index 838267a..5ea3cd5 100644 --- a/fns_cli/folder_sync.py +++ b/fns_cli/folder_sync.py @@ -45,6 +45,12 @@ async def _on_sync_modify(self, msg: WSMessage) -> None: if not rel_path: return + # Skip dot-prefixed directories — these are config folders managed by SettingSync + first = rel_path.split("/")[0] + if first.startswith("."): + log.debug("Ignoring FolderSyncModify for config dir: %s", rel_path) + return + full = self.vault_path / rel_path try: full.mkdir(parents=True, exist_ok=True) @@ -58,6 +64,12 @@ async def _on_sync_delete(self, msg: WSMessage) -> None: if not rel_path: return + # Skip dot-prefixed directories — these are config folders managed by SettingSync + first = rel_path.split("/")[0] + if first.startswith("."): + log.debug("Ignoring FolderSyncDelete for config dir: %s", rel_path) + return + full = self.vault_path / rel_path try: if full.exists(): @@ -73,6 +85,13 @@ async def _on_sync_rename(self, msg: WSMessage) -> None: if not old_path or not new_path: return + # Skip dot-prefixed directories — these are config folders managed by SettingSync + first_old = old_path.split("/")[0] + first_new = new_path.split("/")[0] + if first_old.startswith(".") or first_new.startswith("."): + log.debug("Ignoring FolderSyncRename for config dir: %s → %s", old_path, new_path) + return + old_full = self.vault_path / old_path new_full = self.vault_path / new_path try: diff --git a/fns_cli/setting_sync.py b/fns_cli/setting_sync.py index 2c06101..6dc26b8 100644 --- a/fns_cli/setting_sync.py +++ b/fns_cli/setting_sync.py @@ -39,15 +39,24 @@ def _extract_inner(msg_data: dict) -> dict: return msg_data if isinstance(msg_data, dict) else {} -def _is_config_path(rel: str) -> bool: +def _is_config_path(rel: str, config_sync_dirs: list[str] | None = None) -> bool: """Check whether a relative path belongs to config/settings scope. This matches the Obsidian plugin behaviour: anything inside a dot-prefixed directory (e.g. .obsidian, .agents) is treated as a setting file. Standard exclusions (.git, .trash) are handled by is_excluded() upstream. + + config_sync_dirs: configured dot-prefixed directories to treat as config + (from config.yaml, e.g. [".obsidian", ".agents"]) """ first = rel.split("/")[0] - return first.startswith(".") + if not first.startswith("."): + return False + # Check configured config sync directories + if config_sync_dirs and first in config_sync_dirs: + return True + # By default, treat all dot-prefixed dirs as config (backward compatible) + return True class SettingSync: diff --git a/fns_cli/sync_engine.py b/fns_cli/sync_engine.py index 137af03..c27a95a 100644 --- a/fns_cli/sync_engine.py +++ b/fns_cli/sync_engine.py @@ -53,7 +53,13 @@ def _is_note(self, rel_path: str) -> bool: def _is_config(self, rel_path: str) -> bool: first = rel_path.split("/")[0] - return first.startswith(".") + if not first.startswith("."): + return False + # Check if the directory is in the configured config_sync_dirs list + if first in self.config.sync.config_sync_dirs: + return True + # For other dot-prefixed dirs, check if sync_config is enabled + return self.config.sync.sync_config def _should_sync_file(self, rel_path: str) -> bool: if self._is_config(rel_path): @@ -230,7 +236,7 @@ async def _initial_sync(self) -> None: await self.note_sync.request_sync() await self._wait_note_sync(timeout=300) - if self.config.sync.sync_files or self.config.sync.sync_config: + if self.config.sync.sync_files: await self.file_sync.request_sync() await self._wait_file_sync(timeout=300) @@ -284,16 +290,16 @@ async def _wait_setting_sync(self, timeout: float = 60) -> None: await asyncio.sleep(0.5) async def _push_all_files(self) -> None: - """Upload every non-note, non-excluded file in the vault.""" + """Upload every non-note, non-excluded, non-config file in the vault.""" for fp in self.vault_path.rglob("*"): if fp.is_dir(): continue rel = fp.relative_to(self.vault_path).as_posix() if self.is_excluded(rel) or rel.endswith(".md"): continue - if not self._is_config(rel) and not self.config.sync.sync_files: + if self._is_config(rel): continue - if self._is_config(rel) and not self.config.sync.sync_config: + if not self.config.sync.sync_files: continue await self.file_sync.push_upload(rel) await asyncio.sleep(0.05)