soulsync 1.0.21 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/sync.py DELETED
@@ -1,210 +0,0 @@
1
- import os
2
- import time
3
- import shutil
4
- import threading
5
- from profiles import ProfilesClient, ConflictError
6
-
7
-
8
- class ProfileSync:
9
- """多文件同步逻辑"""
10
-
11
- def __init__(self, client: ProfilesClient, version_manager, workspace: str):
12
- self.client = client
13
- self.version_manager = version_manager
14
- self.workspace = workspace
15
- self._syncing_files = set()
16
- self._sync_lock = threading.Lock()
17
-
18
- def _get_absolute_path(self, relative_path: str) -> str:
19
- """获取文件的绝对路径"""
20
- return os.path.normpath(os.path.join(self.workspace, relative_path))
21
-
22
- def _is_syncing(self, file_path: str) -> bool:
23
- """检查文件是否正在同步"""
24
- return file_path in self._syncing_files
25
-
26
- def _mark_syncing(self, file_path: str):
27
- """标记文件正在同步"""
28
- self._syncing_files.add(file_path)
29
-
30
- def _unmark_syncing(self, file_path: str):
31
- """延迟取消同步标记"""
32
- def remove():
33
- time.sleep(2)
34
- self._syncing_files.discard(file_path)
35
- threading.Thread(target=remove, daemon=True).start()
36
-
37
- def _read_local_file(self, file_path: str) -> str | None:
38
- """读取本地文件内容"""
39
- absolute_path = self._get_absolute_path(file_path)
40
- if os.path.exists(absolute_path):
41
- try:
42
- with open(absolute_path, 'r', encoding='utf-8') as f:
43
- return f.read()
44
- except Exception as e:
45
- print(f"[SoulSync] Error reading local file {file_path}: {e}")
46
- return None
47
- return None
48
-
49
- def _write_local_file(self, file_path: str, content: str):
50
- """写入本地文件"""
51
- absolute_path = self._get_absolute_path(file_path)
52
- try:
53
- directory = os.path.dirname(absolute_path)
54
- if directory and not os.path.exists(directory):
55
- os.makedirs(directory, exist_ok=True)
56
-
57
- with open(absolute_path, 'w', encoding='utf-8') as f:
58
- f.write(content)
59
- except Exception as e:
60
- print(f"[SoulSync] Error writing local file {file_path}: {e}")
61
-
62
- def _create_conflict_backup(self, file_path: str, local_content: str, server_content: str):
63
- """创建冲突备份文件"""
64
- absolute_path = self._get_absolute_path(file_path)
65
- conflict_path = absolute_path + '.conflict'
66
- try:
67
- with open(conflict_path, 'w', encoding='utf-8') as f:
68
- f.write(f"# Conflict: {file_path}\n")
69
- f.write(f"# Created at: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
70
- f.write("========== LOCAL VERSION ==========\n")
71
- f.write(local_content or "(empty)")
72
- f.write("\n\n========== SERVER VERSION ==========\n")
73
- f.write(server_content or "(empty)")
74
- print(f"[SoulSync] Conflict backup created: {conflict_path}")
75
- except Exception as e:
76
- print(f"[SoulSync] Error creating conflict backup: {e}")
77
-
78
- def pull_all(self):
79
- """Pull all profiles from cloud"""
80
- print("[SoulSync] Pulling all profiles from cloud...")
81
-
82
- try:
83
- result = self.client.get_profiles()
84
- cloud_content = result.get('content', {})
85
- cloud_version = result.get('version', 0)
86
- except Exception as e:
87
- print(f"[SoulSync] Error fetching cloud profiles: {e}")
88
- return
89
-
90
- if not cloud_content:
91
- print("[SoulSync] No profiles on cloud")
92
- return
93
-
94
- local_files = ['SOUL.md', 'USER.md', 'MEMORY.md']
95
- pulled_count = 0
96
- skipped_count = 0
97
-
98
- for file_name in local_files:
99
- cloud_file_content = cloud_content.get(file_name, '')
100
- local_content = self._read_local_file(file_name)
101
- local_version = self.version_manager.get_version(file_name)
102
-
103
- if cloud_version > local_version:
104
- self._mark_syncing(file_name)
105
- try:
106
- if cloud_file_content:
107
- self._write_local_file(file_name, cloud_file_content)
108
- self.version_manager.set_version(file_name, cloud_version)
109
- pulled_count += 1
110
- print(f"[SoulSync] Pulled: {file_name} (v{cloud_version})")
111
- finally:
112
- self._unmark_syncing(file_name)
113
- else:
114
- skipped_count += 1
115
-
116
- self.version_manager.set_version('__profiles__', cloud_version)
117
-
118
- print(f"[SoulSync] Sync complete: {pulled_count} pulled, {skipped_count} skipped")
119
-
120
- def push_file(self, file_path: str):
121
- """Push a file to cloud"""
122
- if self._is_syncing(file_path):
123
- return
124
-
125
- self._mark_syncing(file_path)
126
-
127
- try:
128
- local_content = self._read_local_file(file_path)
129
- if local_content is None:
130
- print(f"[SoulSync] File not found locally: {file_path}")
131
- return
132
-
133
- local_version = self.version_manager.get_version(file_path)
134
-
135
- profiles_version = self.version_manager.get_version('__profiles__')
136
-
137
- current_profiles = {}
138
- try:
139
- result = self.client.get_profiles()
140
- current_profiles = result.get('content', {})
141
- except Exception:
142
- pass
143
-
144
- current_profiles[file_path] = local_content
145
-
146
- try:
147
- result = self.client.upload_profiles(current_profiles, profiles_version)
148
- new_version = result.get('version', profiles_version + 1)
149
- self.version_manager.set_version('__profiles__', new_version)
150
- self.version_manager.set_version(file_path, new_version)
151
- print(f"[SoulSync] Pushed: {file_path} (v{new_version})")
152
- except ConflictError as e:
153
- self._handle_conflict(file_path, local_content, e.server_content, e.server_version)
154
- except Exception as e:
155
- print(f"[SoulSync] Error pushing {file_path}: {e}")
156
- finally:
157
- self._unmark_syncing(file_path)
158
-
159
- def on_remote_change(self, file_path: str, version: int):
160
- """Handle remote file change"""
161
- local_version = self.version_manager.get_version('__profiles__')
162
-
163
- if version <= local_version:
164
- print(f"[SoulSync] Remote version not newer, skipping: {file_path} (local: v{local_version}, remote: v{version})")
165
- return
166
-
167
- self._mark_syncing(file_path)
168
-
169
- try:
170
- result = self.client.get_profiles()
171
- cloud_content = result.get('content', {})
172
- cloud_version = result.get('version', 0)
173
-
174
- if file_path not in cloud_content:
175
- print(f"[SoulSync] File not found on cloud: {file_path}")
176
- return
177
-
178
- cloud_file_content = cloud_content.get(file_path, '')
179
-
180
- local_content = self._read_local_file(file_path)
181
-
182
- if local_content is not None and local_content != cloud_file_content:
183
- self._create_conflict_backup(file_path, local_content, cloud_file_content)
184
-
185
- self._write_local_file(file_path, cloud_file_content)
186
- self.version_manager.set_version('__profiles__', cloud_version)
187
- self.version_manager.set_version(file_path, version)
188
- print(f"[SoulSync] Pulled remote change: {file_path} (v{version})")
189
- except Exception as e:
190
- print(f"[SoulSync] Error handling remote change for {file_path}: {e}")
191
- finally:
192
- self._unmark_syncing(file_path)
193
-
194
- def _handle_conflict(self, file_path: str, local_content: str, server_content: str, server_version: int):
195
- """Handle conflict when pushing file"""
196
- print(f"[SoulSync] CONFLICT detected for {file_path}")
197
-
198
- self._create_conflict_backup(file_path, local_content, server_content)
199
-
200
- if isinstance(server_content, dict):
201
- server_file_content = server_content.get(file_path, '')
202
- else:
203
- server_file_content = server_content
204
-
205
- self._write_local_file(file_path, server_file_content)
206
- self.version_manager.set_version('__profiles__', server_version)
207
- self.version_manager.set_version(file_path, server_version)
208
-
209
- print(f"[SoulSync] Conflict resolved: server version used for {file_path}")
210
- print(f"[SoulSync] Please manually merge the conflict backup file")
@@ -1,61 +0,0 @@
1
- import json
2
- import os
3
-
4
-
5
- class VersionManager:
6
- """本地版本管理"""
7
-
8
- def __init__(self, versions_file: str):
9
- self.versions_file = versions_file
10
- self.versions = {}
11
- self.load()
12
-
13
- def load(self):
14
- """加载版本文件"""
15
- if os.path.exists(self.versions_file):
16
- try:
17
- with open(self.versions_file, 'r', encoding='utf-8') as f:
18
- self.versions = json.load(f)
19
- except Exception as e:
20
- print(f"Failed to load versions file: {e}")
21
- self.versions = {}
22
- else:
23
- self.versions = {}
24
-
25
- def save(self):
26
- """保存版本文件"""
27
- try:
28
- directory = os.path.dirname(self.versions_file)
29
- if directory and not os.path.exists(directory):
30
- os.makedirs(directory, exist_ok=True)
31
-
32
- with open(self.versions_file, 'w', encoding='utf-8') as f:
33
- json.dump(self.versions, f, indent=2, ensure_ascii=False)
34
- except Exception as e:
35
- print(f"Failed to save versions file: {e}")
36
-
37
- def get_version(self, file_path: str) -> int:
38
- """获取文件版本"""
39
- return self.versions.get(file_path, 0)
40
-
41
- def set_version(self, file_path: str, version: int):
42
- """设置文件版本"""
43
- self.versions[file_path] = version
44
- self.save()
45
-
46
- def increment_version(self, file_path: str) -> int:
47
- """递增版本"""
48
- current = self.get_version(file_path)
49
- new_version = current + 1
50
- self.set_version(file_path, new_version)
51
- return new_version
52
-
53
- def update_versions(self, updates: dict):
54
- """批量更新版本"""
55
- for file_path, version in updates.items():
56
- self.versions[file_path] = version
57
- self.save()
58
-
59
- def get_all_versions(self) -> dict:
60
- """获取所有版本"""
61
- return self.versions.copy()
package/src/watcher.py DELETED
@@ -1,133 +0,0 @@
1
- import os
2
- import time
3
- from watchdog.observers import Observer
4
- from watchdog.events import FileSystemEventHandler, FileSystemEvent
5
-
6
-
7
- class OpenClawMultiWatcher(FileSystemEventHandler):
8
- """OpenClaw 多文件/目录监听器"""
9
-
10
- def __init__(self, workspace: str, watch_paths: list, callback):
11
- self.workspace = os.path.abspath(workspace)
12
- self.watch_paths = watch_paths
13
- self.callback = callback
14
- self.observer = None
15
- self.last_events = {}
16
- self.debounce_seconds = 1
17
- self.ignore_patterns = ['.tmp', '.swp', '.bak', '~']
18
-
19
- def start(self):
20
- """开始监听"""
21
- if not os.path.exists(self.workspace):
22
- os.makedirs(self.workspace, exist_ok=True)
23
-
24
- self.observer = Observer()
25
-
26
- for watch_path in self.watch_paths:
27
- full_path = os.path.join(self.workspace, watch_path)
28
-
29
- if watch_path.endswith('/'):
30
- dir_name = watch_path.rstrip('/')
31
- if not os.path.exists(full_path):
32
- os.makedirs(full_path, exist_ok=True)
33
- print(f"Created directory: {full_path}")
34
-
35
- self.observer.schedule(self, full_path, recursive=True)
36
- print(f"Watching directory: {watch_path}")
37
- else:
38
- directory = os.path.dirname(full_path)
39
- if directory and not os.path.exists(directory):
40
- os.makedirs(directory, exist_ok=True)
41
-
42
- if not os.path.exists(full_path):
43
- with open(full_path, 'w', encoding='utf-8') as f:
44
- pass
45
- print(f"Created file: {watch_path}")
46
-
47
- self.observer.schedule(self, os.path.dirname(full_path), recursive=False)
48
- print(f"Watching file: {watch_path}")
49
-
50
- self.observer.start()
51
- print(f"Started watching {len(self.watch_paths)} paths in {self.workspace}")
52
-
53
- def stop(self):
54
- """停止监听"""
55
- if self.observer:
56
- self.observer.stop()
57
- self.observer.join()
58
- print("Stopped watching")
59
-
60
- def _should_ignore(self, path: str) -> bool:
61
- """检查是否应该忽略"""
62
- basename = os.path.basename(path)
63
- for pattern in self.ignore_patterns:
64
- if pattern in basename:
65
- return True
66
- return False
67
-
68
- def _get_relative_path(self, absolute_path: str) -> str:
69
- """获取相对路径"""
70
- if absolute_path.startswith(self.workspace):
71
- relative = absolute_path[len(self.workspace):].lstrip('/')
72
- return relative
73
- return absolute_path
74
-
75
- def _debounce_check(self, file_path: str) -> bool:
76
- """防抖检查"""
77
- current_time = time.time()
78
- last_time = self.last_events.get(file_path, 0)
79
-
80
- if current_time - last_time < self.debounce_seconds:
81
- return False
82
-
83
- self.last_events[file_path] = current_time
84
- return True
85
-
86
- def on_modified(self, event):
87
- """文件被修改"""
88
- if event.is_directory:
89
- return
90
-
91
- event_path = os.path.abspath(event.src_path)
92
-
93
- if self._should_ignore(event_path):
94
- return
95
-
96
- if self._debounce_check(event_path):
97
- relative_path = self._get_relative_path(event_path)
98
- print(f"[Watcher] File modified: {relative_path}")
99
- self._trigger_callback('modified', relative_path, event_path)
100
-
101
- def on_created(self, event):
102
- """文件被创建"""
103
- if event.is_directory:
104
- return
105
-
106
- event_path = os.path.abspath(event.src_path)
107
-
108
- if self._should_ignore(event_path):
109
- return
110
-
111
- if self._debounce_check(event_path):
112
- relative_path = self._get_relative_path(event_path)
113
- print(f"[Watcher] File created: {relative_path}")
114
- self._trigger_callback('created', relative_path, event_path)
115
-
116
- def on_deleted(self, event):
117
- """文件被删除"""
118
- if event.is_directory:
119
- return
120
-
121
- event_path = os.path.abspath(event.src_path)
122
-
123
- if self._should_ignore(event_path):
124
- return
125
-
126
- relative_path = self._get_relative_path(event_path)
127
- print(f"[Watcher] File deleted: {relative_path}")
128
- self._trigger_callback('deleted', relative_path, event_path)
129
-
130
- def _trigger_callback(self, event_type: str, relative_path: str, absolute_path: str = None):
131
- """触发回调函数"""
132
- if self.callback:
133
- self.callback(event_type, relative_path, absolute_path)
package/test_ssl_fix.py DELETED
@@ -1,58 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- 测试 SSL 修复是否有效
4
- """
5
- import ssl
6
- import sys
7
-
8
- print(f"Python 版本: {sys.version}")
9
- print(f"SSL 版本: {ssl.OPENSSL_VERSION}")
10
- print()
11
-
12
- # 测试 1: 使用 ssl.create_default_context()
13
- print("测试 1: 使用 ssl.create_default_context()")
14
- try:
15
- import urllib.request
16
- ctx = ssl.create_default_context()
17
- ctx.minimum_version = ssl.TLSVersion.TLSv1_2
18
- req = urllib.request.Request('https://soulsync.work/api/health')
19
- response = urllib.request.urlopen(req, context=ctx)
20
- print(f"✅ 成功: {response.read().decode()}")
21
- except Exception as e:
22
- print(f"❌ 失败: {e}")
23
-
24
- print()
25
-
26
- # 测试 2: 使用 requests + TLSAdapter
27
- print("测试 2: 使用 requests + 自定义 TLSAdapter")
28
- try:
29
- import requests
30
- from requests.adapters import HTTPAdapter
31
-
32
- class TLSAdapter(HTTPAdapter):
33
- def init_poolmanager(self, *args, **kwargs):
34
- ctx = ssl.create_default_context()
35
- ctx.minimum_version = ssl.TLSVersion.TLSv1_2
36
- kwargs['ssl_context'] = ctx
37
- return super().init_poolmanager(*args, **kwargs)
38
-
39
- session = requests.Session()
40
- session.mount('https://', TLSAdapter())
41
- response = session.get('https://soulsync.work/api/health')
42
- print(f"✅ 成功: {response.text}")
43
- except Exception as e:
44
- print(f"❌ 失败: {e}")
45
-
46
- print()
47
-
48
- # 测试 3: 直接 requests(无适配器)
49
- print("测试 3: 直接 requests(无适配器)")
50
- try:
51
- import requests
52
- response = requests.get('https://soulsync.work/api/health')
53
- print(f"✅ 成功: {response.text}")
54
- except Exception as e:
55
- print(f"❌ 失败: {e}")
56
-
57
- print()
58
- print("测试完成")