فهرست منبع

统一夭夭持久数据目录并恢复团队

LSamien 2 ماه پیش
والد
کامیت
7ca96b3597

+ 2 - 1
README.md

@@ -99,7 +99,8 @@ Content-Type: application/json
 
 插件数据会从旧的 `<profile-home>/plugins/yaoyao/data` 一次性迁移到
 `<profile-home>/plugin-data/yaoyao`。如果旧、新目录同时包含数据,安装接口会
-返回 `409 yaoyao_storage_conflict`,不会覆盖任一目录。旧版插件没有迁移检查
+返回 `409 yaoyao_storage_conflict`,不会覆盖任一目录;运行时仍以
+`plugin-data/yaoyao` 为唯一权威目录,不会回退读取旧数据。旧版插件没有迁移检查
 接口时,也会返回 `409 yaoyao_storage_migration_required`;此时先按上面的
 `dashboard/` 局部同步方式安装一次当前兼容版本并重启,之后才能使用一键升级。
 

+ 2 - 1
docs/agent-install.md

@@ -128,7 +128,8 @@ test "$(node -e \"console.log(require(process.argv[1]).version)\" "$PLUGIN_DIR/m
 `<profile-home>/plugins/yaoyao/data` 原子迁移到
 `<profile-home>/plugin-data/yaoyao`。后者独立于插件安装树,后续通过 Hermes
 插件接口替换 `plugins/yaoyao` 时不会丢失运行数据。若两个目录同时包含数据,
-插件不会自动合并;应人工核对后保留唯一权威目录。
+插件不会自动合并,运行时仍只读取 `plugin-data/yaoyao`;应人工核对后移出旧目录,
+最终只保留这一权威数据目录。
 
 完成这次兼容版本启动后,后续可在已登录的 8800 会话中调用:
 

+ 6 - 6
hermes-plugins/yaoyao/dashboard/data_paths.py

@@ -35,9 +35,10 @@ def _has_entries(path: Path) -> bool:
 def ensure_durable_data_root(profile_home: Path) -> DataPathStatus:
     """Move the legacy data directory once, without merging ambiguous trees.
 
-    When both locations contain data, continue serving the legacy tree and
-    report a conflict. The install coordinator refuses a full plugin replace
-    until an operator resolves that conflict, preventing silent data loss.
+    The durable tree is always the runtime authority once it exists. When both
+    locations contain data, keep both untouched and report a conflict so the
+    install coordinator can refuse a full plugin replace without making the
+    application silently fall back to stale legacy state.
     """
 
     home = Path(profile_home).expanduser().resolve()
@@ -49,14 +50,13 @@ def ensure_durable_data_root(profile_home: Path) -> DataPathStatus:
         return DataPathStatus(durable, True, False, False, False)
 
     if legacy.is_symlink() or durable.is_symlink():
-        return DataPathStatus(legacy, False, False, True, True)
+        return DataPathStatus(durable, False, False, True, True)
 
     if durable.exists():
         if _has_entries(legacy):
-            return DataPathStatus(legacy, False, False, True, True)
+            return DataPathStatus(durable, False, False, True, True)
         return DataPathStatus(durable, True, False, True, False)
 
     durable.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
     os.replace(legacy, durable)
     return DataPathStatus(durable, True, True, False, False)
-

+ 6 - 1
hermes-plugins/yaoyao/dashboard/group_node_registry.py

@@ -12,6 +12,11 @@ from typing import Mapping
 from urllib.parse import urlsplit, urlunsplit
 import uuid
 
+try:
+    from .data_paths import ensure_durable_data_root
+except ImportError:  # Loaded by the Dashboard plugin loader as a top-level module.
+    from data_paths import ensure_durable_data_root
+
 
 class NodeRegistryError(RuntimeError):
     pass
@@ -32,7 +37,7 @@ class PairedNodeRegistry:
     @classmethod
     def from_environment(cls) -> "PairedNodeRegistry":
         hermes_home = Path(os.environ.get("HERMES_HOME") or Path.home() / ".hermes")
-        return cls(hermes_home / "plugins" / "yaoyao" / "data")
+        return cls(ensure_durable_data_root(hermes_home).data_root)
 
     def register(self, command: Mapping[str, object]) -> dict[str, object]:
         allowed = {

+ 70 - 29
hermes-plugins/yaoyao/dashboard/tests/test_data_paths.py

@@ -1,43 +1,84 @@
+import os
 from pathlib import Path
+import tempfile
+import unittest
+from unittest.mock import patch
 
 from data_paths import ensure_durable_data_root
+from group_node_registry import PairedNodeRegistry
 
 
-def test_moves_legacy_tree_to_durable_plugin_data(tmp_path: Path) -> None:
-    legacy = tmp_path / "plugins" / "yaoyao" / "data"
-    legacy.mkdir(parents=True)
-    (legacy / "index.sqlite3").write_bytes(b"database")
+class DataPathTests(unittest.TestCase):
+    def setUp(self) -> None:
+        self.temporary = tempfile.TemporaryDirectory()
+        self.root = Path(self.temporary.name).resolve()
 
-    status = ensure_durable_data_root(tmp_path)
+    def tearDown(self) -> None:
+        self.temporary.cleanup()
 
-    assert status.ready is True
-    assert status.migrated is True
-    assert status.data_root == tmp_path / "plugin-data" / "yaoyao"
-    assert (status.data_root / "index.sqlite3").read_bytes() == b"database"
-    assert not legacy.exists()
+    def test_moves_legacy_tree_to_durable_plugin_data(self) -> None:
+        legacy = self.root / "plugins" / "yaoyao" / "data"
+        legacy.mkdir(parents=True)
+        (legacy / "index.sqlite3").write_bytes(b"database")
 
+        status = ensure_durable_data_root(self.root)
 
-def test_refuses_to_merge_two_nonempty_data_trees(tmp_path: Path) -> None:
-    legacy = tmp_path / "plugins" / "yaoyao" / "data"
-    durable = tmp_path / "plugin-data" / "yaoyao"
-    legacy.mkdir(parents=True)
-    durable.mkdir(parents=True)
-    (legacy / "legacy.db").write_bytes(b"legacy")
-    (durable / "current.db").write_bytes(b"current")
+        self.assertTrue(status.ready)
+        self.assertTrue(status.migrated)
+        self.assertEqual(status.data_root, self.root / "plugin-data" / "yaoyao")
+        self.assertEqual(
+            (status.data_root / "index.sqlite3").read_bytes(), b"database"
+        )
+        self.assertFalse(legacy.exists())
 
-    status = ensure_durable_data_root(tmp_path)
+    def test_refuses_to_merge_without_falling_back(self) -> None:
+        legacy = self.root / "plugins" / "yaoyao" / "data"
+        durable = self.root / "plugin-data" / "yaoyao"
+        legacy.mkdir(parents=True)
+        durable.mkdir(parents=True)
+        (legacy / "legacy.db").write_bytes(b"legacy")
+        (durable / "current.db").write_bytes(b"current")
 
-    assert status.ready is False
-    assert status.conflict is True
-    assert status.data_root == legacy
-    assert (legacy / "legacy.db").exists()
-    assert (durable / "current.db").exists()
+        status = ensure_durable_data_root(self.root)
 
+        self.assertFalse(status.ready)
+        self.assertTrue(status.conflict)
+        self.assertEqual(status.data_root, durable)
+        self.assertTrue((legacy / "legacy.db").exists())
+        self.assertTrue((durable / "current.db").exists())
 
-def test_fresh_profile_uses_durable_location_without_creating_it(tmp_path: Path) -> None:
-    status = ensure_durable_data_root(tmp_path)
+    def test_fresh_profile_uses_durable_location_without_creating_it(self) -> None:
+        status = ensure_durable_data_root(self.root)
 
-    assert status.ready is True
-    assert status.migrated is False
-    assert status.data_root == tmp_path / "plugin-data" / "yaoyao"
-    assert not status.data_root.exists()
+        self.assertTrue(status.ready)
+        self.assertFalse(status.migrated)
+        self.assertEqual(status.data_root, self.root / "plugin-data" / "yaoyao")
+        self.assertFalse(status.data_root.exists())
+
+    def test_node_registry_uses_durable_data_root(self) -> None:
+        with patch.dict(os.environ, {"HERMES_HOME": str(self.root)}):
+            registry = PairedNodeRegistry.from_environment()
+
+        self.assertEqual(registry.root, self.root / "plugin-data" / "yaoyao")
+        self.assertTrue(registry.key_path.exists())
+        self.assertFalse((self.root / "plugins" / "yaoyao" / "data").exists())
+
+    def test_node_registry_keeps_durable_authority_during_legacy_conflict(
+        self,
+    ) -> None:
+        durable = self.root / "plugin-data" / "yaoyao"
+        legacy = self.root / "plugins" / "yaoyao" / "data"
+        durable.mkdir(parents=True)
+        legacy.mkdir(parents=True)
+        (legacy / "stale.db").write_bytes(b"stale")
+
+        with patch.dict(os.environ, {"HERMES_HOME": str(self.root)}):
+            registry = PairedNodeRegistry.from_environment()
+
+        self.assertEqual(registry.root, durable)
+        self.assertTrue(registry.key_path.exists())
+        self.assertFalse((legacy / "paired-group-nodes.key").exists())
+
+
+if __name__ == "__main__":
+    unittest.main()

+ 6 - 1
hermes-plugins/yaoyao/dashboard/tests/test_group_merge_contract.py

@@ -272,7 +272,12 @@ class GroupMergeContractTests(unittest.TestCase):
         self.assertEqual(payload["lifecycle"], ["start", "ready", "shutdown"])
         self.assertEqual(
             payload["storePath"],
-            str(Path(hermes_home) / "plugins" / "yaoyao" / "data" / "group-chat.db"),
+            str(
+                Path(hermes_home).resolve()
+                / "plugin-data"
+                / "yaoyao"
+                / "group-chat.db"
+            ),
         )