soulsync 1.0.11 → 1.0.13
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/config.json.example +2 -8
- package/index.js +421 -25
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/client.py +13 -46
- package/src/main.py +61 -73
- package/src/profiles.py +27 -49
- package/src/sync.py +67 -60
- package/src/interactive_auth.py +0 -283
- package/src/main_fixed.py +0 -434
- package/src/register.py +0 -131
package/src/main.py
CHANGED
|
@@ -61,7 +61,7 @@ class SoulSyncPlugin:
|
|
|
61
61
|
|
|
62
62
|
cloud_url = self.config.get('cloud_url', '').strip()
|
|
63
63
|
email = self.config.get('email', '').strip()
|
|
64
|
-
|
|
64
|
+
token = self.config.get('token', '').strip()
|
|
65
65
|
|
|
66
66
|
if not cloud_url:
|
|
67
67
|
self.config['cloud_url'] = 'https://soulsync.work'
|
|
@@ -92,9 +92,11 @@ class SoulSyncPlugin:
|
|
|
92
92
|
except:
|
|
93
93
|
config = {}
|
|
94
94
|
|
|
95
|
-
# 保存 email
|
|
96
|
-
if 'user' in auth_result:
|
|
95
|
+
# 保存 email(支持两种返回格式:authenticate 有 user 对象,register 只有一个字段)
|
|
96
|
+
if 'user' in auth_result and isinstance(auth_result.get('user'), dict):
|
|
97
97
|
config['email'] = auth_result['user'].get('email', '')
|
|
98
|
+
elif 'email' in auth_result:
|
|
99
|
+
config['email'] = auth_result.get('email', '')
|
|
98
100
|
|
|
99
101
|
# 保存 token
|
|
100
102
|
if 'token' in auth_result:
|
|
@@ -132,7 +134,7 @@ class SoulSyncPlugin:
|
|
|
132
134
|
return result
|
|
133
135
|
elif choice == '0':
|
|
134
136
|
print("[SoulSync] Goodbye! / 再见!")
|
|
135
|
-
sys.exit(
|
|
137
|
+
sys.exit(2)
|
|
136
138
|
else:
|
|
137
139
|
print("[SoulSync] Invalid choice / 无效选择")
|
|
138
140
|
|
|
@@ -164,6 +166,8 @@ class SoulSyncPlugin:
|
|
|
164
166
|
temp_client = OpenClawClient(self.config)
|
|
165
167
|
result = temp_client.authenticate(email, password)
|
|
166
168
|
if result:
|
|
169
|
+
result['email'] = email
|
|
170
|
+
result['password'] = password
|
|
167
171
|
print("\n[SoulSync] ✓ Login successful! / 登录成功!")
|
|
168
172
|
self._save_auth_to_config(result)
|
|
169
173
|
return True
|
|
@@ -176,7 +180,7 @@ class SoulSyncPlugin:
|
|
|
176
180
|
print("[SoulSync] Exiting... / 退出...")
|
|
177
181
|
sys.exit(0)
|
|
178
182
|
|
|
179
|
-
print(f"\n[SoulSync] ❌
|
|
183
|
+
print(f"\n[SoulSync] ❌ {e}")
|
|
180
184
|
print("[SoulSync] Returning to menu... / 返回菜单...")
|
|
181
185
|
return None
|
|
182
186
|
|
|
@@ -281,46 +285,32 @@ class SoulSyncPlugin:
|
|
|
281
285
|
profile = self.client.get_profile()
|
|
282
286
|
print(f"[SoulSync] Using existing token, user: {profile.get('email', 'unknown')}")
|
|
283
287
|
except Exception as e:
|
|
288
|
+
error_str = str(e)
|
|
289
|
+
if '401' in error_str or 'Unauthorized' in error_str or 'token' in error_str.lower():
|
|
290
|
+
print("[SoulSync] Token expired or invalid. Please re-login via chat: say 'login SoulSync'")
|
|
291
|
+
print("[SoulSync] Token 已过期,请在聊天中说 'login SoulSync' 重新登录")
|
|
292
|
+
return False
|
|
284
293
|
print(f"[SoulSync] Token invalid, re-authenticating: {e}")
|
|
285
294
|
token = None
|
|
286
295
|
|
|
287
296
|
if not token:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
try:
|
|
293
|
-
result = self.client.authenticate(email, password)
|
|
294
|
-
if result:
|
|
295
|
-
print("[SoulSync] Login successful! / 登录成功!")
|
|
296
|
-
self._save_auth_to_config(result)
|
|
297
|
-
token = self.client.token
|
|
298
|
-
except Exception as e:
|
|
299
|
-
error_msg = str(e)
|
|
300
|
-
print(f"[SoulSync] Login failed: {e}")
|
|
301
|
-
|
|
302
|
-
if "429" in error_msg or "too many" in error_msg.lower():
|
|
303
|
-
print("\n[SoulSync] ========================================")
|
|
304
|
-
print("[SoulSync] Too many failed attempts. Please try again later / 登录失败次数过多,请稍后再试")
|
|
305
|
-
print("[SoulSync] ========================================\n")
|
|
306
|
-
else:
|
|
307
|
-
print("\n[SoulSync] ========================================")
|
|
308
|
-
print("[SoulSync] Login failed: invalid email or password / 登录失败:邮箱或密码错误")
|
|
309
|
-
print("[SoulSync] Please check your config file / 请检查配置文件:")
|
|
310
|
-
print(f" {os.path.normpath(os.path.join(PLUGIN_DIR, 'config.json'))}")
|
|
311
|
-
print("[SoulSync] ========================================\n")
|
|
312
|
-
|
|
313
|
-
sys.exit(0)
|
|
314
|
-
|
|
297
|
+
print("[SoulSync] Token expired or invalid. Please re-configure using 'openclaw soulsync:setup'")
|
|
298
|
+
print("[SoulSync] Token 已过期,请重新运行 'openclaw soulsync:setup' 配置")
|
|
299
|
+
return False
|
|
300
|
+
|
|
315
301
|
email = self.config.get('email')
|
|
316
|
-
|
|
317
|
-
|
|
302
|
+
|
|
318
303
|
try:
|
|
319
304
|
profile = self.client.get_profile()
|
|
320
305
|
print(f"\n[SoulSync] Logged in as: {profile.get('email')}")
|
|
321
306
|
subscription = profile.get('subscription', {})
|
|
322
307
|
print(f"[SoulSync] Subscription: {subscription.get('status')} (days remaining: {subscription.get('daysRemaining', 0)})\n")
|
|
323
308
|
except Exception as e:
|
|
309
|
+
error_str = str(e)
|
|
310
|
+
if '401' in error_str or 'Unauthorized' in error_str or 'token' in error_str.lower():
|
|
311
|
+
print("[SoulSync] Token expired or invalid. Please re-login via chat: say 'login SoulSync'")
|
|
312
|
+
print("[SoulSync] Token 已过期,请在聊天中说 'login SoulSync' 重新登录")
|
|
313
|
+
return False
|
|
324
314
|
print(f"[SoulSync] Warning: Could not get profile: {e}")
|
|
325
315
|
|
|
326
316
|
# 版本管理器
|
|
@@ -338,13 +328,13 @@ class SoulSyncPlugin:
|
|
|
338
328
|
self.config.get('workspace')
|
|
339
329
|
)
|
|
340
330
|
|
|
341
|
-
print("Pulling all profiles from cloud...")
|
|
331
|
+
print("[SoulSync] Pulling all profiles from cloud...")
|
|
342
332
|
try:
|
|
343
333
|
self.profile_sync.pull_all()
|
|
344
334
|
except Exception as e:
|
|
345
|
-
print(f"Warning: Could not pull profiles: {e}")
|
|
335
|
+
print(f"[SoulSync] Warning: Could not pull profiles: {e}")
|
|
346
336
|
|
|
347
|
-
print("\
|
|
337
|
+
print("\n[SoulSync] Starting file watcher...")
|
|
348
338
|
watch_files = self.config.get('watch_files', [])
|
|
349
339
|
self.watcher = OpenClawMultiWatcher(
|
|
350
340
|
self.config.get('workspace'),
|
|
@@ -353,55 +343,47 @@ class SoulSyncPlugin:
|
|
|
353
343
|
)
|
|
354
344
|
self.watcher.start()
|
|
355
345
|
|
|
356
|
-
print("\
|
|
346
|
+
print("\n[SoulSync] Connecting to WebSocket...")
|
|
357
347
|
try:
|
|
358
348
|
self.client.connect_websocket(self.on_websocket_message)
|
|
359
349
|
except Exception as e:
|
|
360
|
-
print(f"Warning: Could not connect WebSocket: {e}")
|
|
350
|
+
print(f"[SoulSync] Warning: Could not connect WebSocket: {e}")
|
|
361
351
|
|
|
362
352
|
self.running = True
|
|
363
353
|
|
|
364
354
|
def on_file_change(self, event_type: str, relative_path: str, absolute_path: str = None):
|
|
365
355
|
"""文件变化回调"""
|
|
366
|
-
print(f"\n[File {event_type}] {relative_path}")
|
|
356
|
+
print(f"\n[SoulSync] [File {event_type}] {relative_path}")
|
|
367
357
|
|
|
368
358
|
if event_type in ['modified', 'created']:
|
|
369
359
|
time.sleep(0.5)
|
|
370
360
|
|
|
371
361
|
try:
|
|
372
362
|
self.profile_sync.push_file(relative_path)
|
|
373
|
-
print(f"Upload completed: {relative_path}")
|
|
363
|
+
print(f"[SoulSync] Upload completed: {relative_path}")
|
|
374
364
|
except Exception as e:
|
|
375
|
-
print(f"Upload error: {e}")
|
|
365
|
+
print(f"[SoulSync] Upload error: {e}")
|
|
376
366
|
|
|
377
367
|
elif event_type == 'deleted':
|
|
378
|
-
print(f"File deleted (not synced to cloud): {relative_path}")
|
|
368
|
+
print(f"[SoulSync] File deleted (not synced to cloud): {relative_path}")
|
|
379
369
|
|
|
380
370
|
def on_websocket_message(self, data: dict):
|
|
381
371
|
"""WebSocket 消息回调"""
|
|
382
372
|
event = data.get('event')
|
|
383
373
|
|
|
384
|
-
if event == '
|
|
385
|
-
|
|
374
|
+
if event == 'profile:updated':
|
|
375
|
+
user_id = data.get('user_id')
|
|
386
376
|
version = data.get('version')
|
|
387
|
-
print(f"\n[WebSocket]
|
|
388
|
-
try:
|
|
389
|
-
self.profile_sync.on_remote_change(file_path, version)
|
|
390
|
-
except Exception as e:
|
|
391
|
-
print(f"Sync error: {e}")
|
|
392
|
-
|
|
393
|
-
elif event == 'new_memory':
|
|
394
|
-
print(f"\n[WebSocket] New memory available!")
|
|
377
|
+
print(f"\n[SoulSync] [WebSocket] Profile updated (v{version})")
|
|
395
378
|
try:
|
|
396
379
|
self.profile_sync.pull_all()
|
|
397
|
-
print("Memory synced from remote")
|
|
398
380
|
except Exception as e:
|
|
399
|
-
print(f"Sync error: {e}")
|
|
381
|
+
print(f"[SoulSync] Sync error: {e}")
|
|
400
382
|
|
|
401
383
|
elif data.get('type') == 'authenticated':
|
|
402
|
-
print(f"[WebSocket] Authenticated, socket_id: {data.get('socket_id')}")
|
|
384
|
+
print(f"[SoulSync] [WebSocket] Authenticated, socket_id: {data.get('socket_id')}")
|
|
403
385
|
elif data.get('type') == 'error':
|
|
404
|
-
print(f"[WebSocket] Error: {data.get('message')}")
|
|
386
|
+
print(f"[SoulSync] [WebSocket] Error: {data.get('message')}")
|
|
405
387
|
|
|
406
388
|
def run(self):
|
|
407
389
|
"""运行插件"""
|
|
@@ -413,17 +395,17 @@ class SoulSyncPlugin:
|
|
|
413
395
|
self.load_config()
|
|
414
396
|
self.initialize()
|
|
415
397
|
|
|
416
|
-
print("\n=== Plugin Running ===")
|
|
417
|
-
print("Press Ctrl+C to stop\n")
|
|
398
|
+
print("\n[SoulSync] === Plugin Running ===")
|
|
399
|
+
print("[SoulSync] Press Ctrl+C to stop\n")
|
|
418
400
|
|
|
419
401
|
while self.running:
|
|
420
402
|
time.sleep(1)
|
|
421
403
|
|
|
422
404
|
except KeyboardInterrupt:
|
|
423
|
-
print("\n
|
|
405
|
+
print("\n[SoulSync] Shutting down...")
|
|
424
406
|
self.shutdown()
|
|
425
407
|
except Exception as e:
|
|
426
|
-
print(f"\
|
|
408
|
+
print(f"\n[SoulSync] Error: {e}")
|
|
427
409
|
import traceback
|
|
428
410
|
traceback.print_exc()
|
|
429
411
|
self.shutdown()
|
|
@@ -431,24 +413,24 @@ class SoulSyncPlugin:
|
|
|
431
413
|
|
|
432
414
|
def shutdown(self):
|
|
433
415
|
"""关闭插件"""
|
|
434
|
-
print("Shutting down SoulSync plugin...")
|
|
416
|
+
print("[SoulSync] Shutting down SoulSync plugin...")
|
|
435
417
|
self.running = False
|
|
436
418
|
|
|
437
419
|
if self.watcher:
|
|
438
420
|
try:
|
|
439
421
|
self.watcher.stop()
|
|
440
|
-
print("File watcher stopped")
|
|
422
|
+
print("[SoulSync] File watcher stopped")
|
|
441
423
|
except Exception as e:
|
|
442
|
-
print(f"Error stopping watcher: {e}")
|
|
424
|
+
print(f"[SoulSync] Error stopping watcher: {e}")
|
|
443
425
|
|
|
444
426
|
if self.client:
|
|
445
427
|
try:
|
|
446
428
|
self.client.close()
|
|
447
|
-
print("Client connection closed")
|
|
429
|
+
print("[SoulSync] Client connection closed")
|
|
448
430
|
except Exception as e:
|
|
449
|
-
print(f"Error closing client: {e}")
|
|
431
|
+
print(f"[SoulSync] Error closing client: {e}")
|
|
450
432
|
|
|
451
|
-
print("Plugin shutdown complete")
|
|
433
|
+
print("[SoulSync] Plugin shutdown complete")
|
|
452
434
|
def main():
|
|
453
435
|
"""主函数"""
|
|
454
436
|
try:
|
|
@@ -463,19 +445,25 @@ def main():
|
|
|
463
445
|
if args.setup:
|
|
464
446
|
try:
|
|
465
447
|
plugin.load_config()
|
|
466
|
-
plugin.run_setup()
|
|
467
|
-
|
|
468
|
-
|
|
448
|
+
result = plugin.run_setup()
|
|
449
|
+
if result:
|
|
450
|
+
print("\n[SoulSync] ✓ Setup complete! Starting sync... / 设置完成!正在启动同步...")
|
|
451
|
+
print("\n[SoulSync] ========================================")
|
|
452
|
+
print("[SoulSync] Starting SoulSync sync service...")
|
|
453
|
+
print("[SoulSync] ========================================\n")
|
|
454
|
+
plugin.initialize()
|
|
455
|
+
plugin.run()
|
|
456
|
+
return
|
|
469
457
|
except KeyboardInterrupt:
|
|
470
458
|
print("\n[SoulSync] Cancelled by user. Goodbye! / 已取消,再见!")
|
|
471
|
-
sys.exit(
|
|
459
|
+
sys.exit(2)
|
|
472
460
|
|
|
473
461
|
plugin.load_config()
|
|
474
462
|
|
|
475
463
|
email = plugin.config.get('email', '').strip()
|
|
476
|
-
|
|
464
|
+
token = plugin.config.get('token', '').strip()
|
|
477
465
|
|
|
478
|
-
if not email or not
|
|
466
|
+
if not email or not token:
|
|
479
467
|
print("\n[SoulSync] ========================================")
|
|
480
468
|
print("[SoulSync] Not configured. Run 'openclaw soulsync:setup' first.")
|
|
481
469
|
print("[SoulSync] 尚未配置,请先运行 'openclaw soulsync:setup'")
|
package/src/profiles.py
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import requests
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
6
|
+
from src.client import TLSAdapter
|
|
2
7
|
|
|
3
8
|
|
|
4
9
|
class ProfilesClient:
|
|
5
|
-
"""Profiles API 客户端"""
|
|
10
|
+
"""Profiles API 客户端 - 统一同步接口"""
|
|
6
11
|
|
|
7
12
|
def __init__(self, cloud_url: str, token: str = None):
|
|
8
13
|
self.cloud_url = cloud_url.rstrip('/')
|
|
9
14
|
self.token = token
|
|
15
|
+
self.session = requests.Session()
|
|
16
|
+
self.session.mount('https://', TLSAdapter())
|
|
10
17
|
|
|
11
18
|
def _get_headers(self) -> dict:
|
|
12
19
|
"""获取请求头"""
|
|
@@ -19,57 +26,50 @@ class ProfilesClient:
|
|
|
19
26
|
"""设置 token"""
|
|
20
27
|
self.token = token
|
|
21
28
|
|
|
22
|
-
def get_profiles(self
|
|
23
|
-
"""
|
|
29
|
+
def get_profiles(self) -> dict:
|
|
30
|
+
"""获取用户的完整 profiles
|
|
24
31
|
|
|
25
|
-
Args:
|
|
26
|
-
path: 可选的文件路径
|
|
27
|
-
|
|
28
32
|
Returns:
|
|
29
|
-
包含
|
|
33
|
+
包含 content (dict), version, updated_at 的字典
|
|
34
|
+
示例: {"content": {"SOUL.md": "...", "USER.md": "..."}, "version": 3, "updated_at": "..."}
|
|
30
35
|
"""
|
|
31
36
|
url = f"{self.cloud_url}/api/profiles"
|
|
32
|
-
if path:
|
|
33
|
-
url += f"?path={path}"
|
|
34
37
|
|
|
35
|
-
response =
|
|
38
|
+
response = self.session.get(url, headers=self._get_headers())
|
|
36
39
|
|
|
37
40
|
if response.status_code == 200:
|
|
38
41
|
return response.json()
|
|
39
|
-
elif response.status_code == 404:
|
|
40
|
-
return {'files': []}
|
|
41
42
|
else:
|
|
42
43
|
error = response.json().get('error', 'Unknown error')
|
|
43
44
|
raise Exception(f"Get profiles failed: {error}")
|
|
44
45
|
|
|
45
|
-
def
|
|
46
|
-
"""
|
|
46
|
+
def upload_profiles(self, content: dict, version: int) -> dict:
|
|
47
|
+
"""整体替换用户的 profiles
|
|
47
48
|
|
|
48
49
|
Args:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
version:
|
|
50
|
+
content: 包含所有文件的 dict,key 是文件名,value 是内容
|
|
51
|
+
示例: {"SOUL.md": "...", "USER.md": "...", "MEMORY.md": "..."}
|
|
52
|
+
version: 客户端当前持有的版本号
|
|
52
53
|
|
|
53
54
|
Returns:
|
|
54
|
-
成功时返回 {
|
|
55
|
-
|
|
55
|
+
成功时返回 {"content": {...}, "version": N, "updated_at": "..."}
|
|
56
|
+
冲突时抛出 ConflictError
|
|
56
57
|
"""
|
|
57
58
|
url = f"{self.cloud_url}/api/profiles"
|
|
58
59
|
data = {
|
|
59
|
-
'file_path': file_path,
|
|
60
60
|
'content': content,
|
|
61
61
|
'version': version
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
response =
|
|
64
|
+
response = self.session.put(url, json=data, headers=self._get_headers())
|
|
65
65
|
|
|
66
66
|
if response.status_code == 200:
|
|
67
67
|
return response.json()
|
|
68
68
|
elif response.status_code == 409:
|
|
69
69
|
result = response.json()
|
|
70
70
|
raise ConflictError(
|
|
71
|
-
result.get('
|
|
72
|
-
result.get('
|
|
71
|
+
server_content=result.get('server_content', {}),
|
|
72
|
+
server_version=result.get('server_version', 0)
|
|
73
73
|
)
|
|
74
74
|
elif response.status_code == 403:
|
|
75
75
|
error = response.json().get('error', 'Subscription required')
|
|
@@ -77,34 +77,12 @@ class ProfilesClient:
|
|
|
77
77
|
else:
|
|
78
78
|
error = response.json().get('error', 'Unknown error')
|
|
79
79
|
raise Exception(f"Upload failed: {error}")
|
|
80
|
-
|
|
81
|
-
def sync_profiles(self, since: str = '0') -> dict:
|
|
82
|
-
"""增量同步 profiles
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
since: 时间戳
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
包含 files 列表和 server_time 的字典
|
|
89
|
-
"""
|
|
90
|
-
url = f"{self.cloud_url}/api/profiles/sync?since={since}"
|
|
91
|
-
|
|
92
|
-
response = requests.get(url, headers=self._get_headers())
|
|
93
|
-
|
|
94
|
-
if response.status_code == 200:
|
|
95
|
-
return response.json()
|
|
96
|
-
elif response.status_code == 403:
|
|
97
|
-
error = response.json().get('error', 'Subscription required')
|
|
98
|
-
raise Exception(f"Sync failed: {error}")
|
|
99
|
-
else:
|
|
100
|
-
error = response.json().get('error', 'Unknown error')
|
|
101
|
-
raise Exception(f"Sync failed: {error}")
|
|
102
80
|
|
|
103
81
|
|
|
104
82
|
class ConflictError(Exception):
|
|
105
83
|
"""版本冲突异常"""
|
|
106
84
|
|
|
107
|
-
def __init__(self,
|
|
108
|
-
self.
|
|
109
|
-
self.
|
|
110
|
-
super().__init__(f"Version conflict:
|
|
85
|
+
def __init__(self, server_content: dict, server_version: int):
|
|
86
|
+
self.server_content = server_content
|
|
87
|
+
self.server_version = server_version
|
|
88
|
+
super().__init__(f"Version conflict: server version is {server_version}")
|
package/src/sync.py
CHANGED
|
@@ -42,7 +42,7 @@ class ProfileSync:
|
|
|
42
42
|
with open(absolute_path, 'r', encoding='utf-8') as f:
|
|
43
43
|
return f.read()
|
|
44
44
|
except Exception as e:
|
|
45
|
-
print(f"[
|
|
45
|
+
print(f"[SoulSync] Error reading local file {file_path}: {e}")
|
|
46
46
|
return None
|
|
47
47
|
return None
|
|
48
48
|
|
|
@@ -57,7 +57,7 @@ class ProfileSync:
|
|
|
57
57
|
with open(absolute_path, 'w', encoding='utf-8') as f:
|
|
58
58
|
f.write(content)
|
|
59
59
|
except Exception as e:
|
|
60
|
-
print(f"[
|
|
60
|
+
print(f"[SoulSync] Error writing local file {file_path}: {e}")
|
|
61
61
|
|
|
62
62
|
def _create_conflict_backup(self, file_path: str, local_content: str, server_content: str):
|
|
63
63
|
"""创建冲突备份文件"""
|
|
@@ -71,64 +71,51 @@ class ProfileSync:
|
|
|
71
71
|
f.write(local_content or "(empty)")
|
|
72
72
|
f.write("\n\n========== SERVER VERSION ==========\n")
|
|
73
73
|
f.write(server_content or "(empty)")
|
|
74
|
-
print(f"[
|
|
74
|
+
print(f"[SoulSync] Conflict backup created: {conflict_path}")
|
|
75
75
|
except Exception as e:
|
|
76
|
-
print(f"[
|
|
76
|
+
print(f"[SoulSync] Error creating conflict backup: {e}")
|
|
77
77
|
|
|
78
78
|
def pull_all(self):
|
|
79
79
|
"""Pull all profiles from cloud"""
|
|
80
|
-
print("[
|
|
80
|
+
print("[SoulSync] Pulling all profiles from cloud...")
|
|
81
81
|
|
|
82
82
|
try:
|
|
83
83
|
result = self.client.get_profiles()
|
|
84
|
-
|
|
84
|
+
cloud_content = result.get('content', {})
|
|
85
|
+
cloud_version = result.get('version', 0)
|
|
85
86
|
except Exception as e:
|
|
86
|
-
print(f"[
|
|
87
|
+
print(f"[SoulSync] Error fetching cloud profiles: {e}")
|
|
87
88
|
return
|
|
88
89
|
|
|
89
|
-
if not
|
|
90
|
-
print("[
|
|
90
|
+
if not cloud_content:
|
|
91
|
+
print("[SoulSync] No profiles on cloud")
|
|
91
92
|
return
|
|
92
93
|
|
|
94
|
+
local_files = ['SOUL.md', 'USER.md', 'MEMORY.md']
|
|
93
95
|
pulled_count = 0
|
|
94
|
-
pushed_count = 0
|
|
95
96
|
skipped_count = 0
|
|
96
97
|
|
|
97
|
-
for
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if not file_path:
|
|
103
|
-
continue
|
|
104
|
-
|
|
105
|
-
local_content = self._read_local_file(file_path)
|
|
106
|
-
local_version = self.version_manager.get_version(file_path)
|
|
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)
|
|
107
102
|
|
|
108
103
|
if cloud_version > local_version:
|
|
109
|
-
self._mark_syncing(
|
|
104
|
+
self._mark_syncing(file_name)
|
|
110
105
|
try:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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})")
|
|
115
111
|
finally:
|
|
116
|
-
self._unmark_syncing(
|
|
117
|
-
elif local_version > cloud_version and local_content is not None:
|
|
118
|
-
try:
|
|
119
|
-
result = self.client.upload_profile(file_path, local_content, local_version)
|
|
120
|
-
self.version_manager.set_version(file_path, result.get('version', local_version))
|
|
121
|
-
pushed_count += 1
|
|
122
|
-
print(f"[Sync] Pushed: {file_path} (v{local_version})")
|
|
123
|
-
except ConflictError as e:
|
|
124
|
-
self._handle_conflict(file_path, local_content, e.server_content, e.server_version)
|
|
125
|
-
pushed_count += 1
|
|
126
|
-
except Exception as e:
|
|
127
|
-
print(f"[Sync] Error pushing {file_path}: {e}")
|
|
112
|
+
self._unmark_syncing(file_name)
|
|
128
113
|
else:
|
|
129
114
|
skipped_count += 1
|
|
130
115
|
|
|
131
|
-
|
|
116
|
+
self.version_manager.set_version('__profiles__', cloud_version)
|
|
117
|
+
|
|
118
|
+
print(f"[SoulSync] Sync complete: {pulled_count} pulled, {skipped_count} skipped")
|
|
132
119
|
|
|
133
120
|
def push_file(self, file_path: str):
|
|
134
121
|
"""Push a file to cloud"""
|
|
@@ -140,64 +127,84 @@ class ProfileSync:
|
|
|
140
127
|
try:
|
|
141
128
|
local_content = self._read_local_file(file_path)
|
|
142
129
|
if local_content is None:
|
|
143
|
-
print(f"[
|
|
130
|
+
print(f"[SoulSync] File not found locally: {file_path}")
|
|
144
131
|
return
|
|
145
132
|
|
|
146
133
|
local_version = self.version_manager.get_version(file_path)
|
|
147
134
|
|
|
135
|
+
profiles_version = self.version_manager.get_version('__profiles__')
|
|
136
|
+
|
|
137
|
+
current_profiles = {}
|
|
148
138
|
try:
|
|
149
|
-
result = self.client.
|
|
150
|
-
|
|
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)
|
|
151
150
|
self.version_manager.set_version(file_path, new_version)
|
|
152
|
-
print(f"[
|
|
151
|
+
print(f"[SoulSync] Pushed: {file_path} (v{new_version})")
|
|
153
152
|
except ConflictError as e:
|
|
154
153
|
self._handle_conflict(file_path, local_content, e.server_content, e.server_version)
|
|
155
154
|
except Exception as e:
|
|
156
|
-
print(f"[
|
|
155
|
+
print(f"[SoulSync] Error pushing {file_path}: {e}")
|
|
157
156
|
finally:
|
|
158
157
|
self._unmark_syncing(file_path)
|
|
159
158
|
|
|
160
159
|
def on_remote_change(self, file_path: str, version: int):
|
|
161
160
|
"""Handle remote file change"""
|
|
162
|
-
local_version = self.version_manager.get_version(
|
|
161
|
+
local_version = self.version_manager.get_version('__profiles__')
|
|
163
162
|
|
|
164
163
|
if version <= local_version:
|
|
165
|
-
print(f"[
|
|
164
|
+
print(f"[SoulSync] Remote version not newer, skipping: {file_path} (local: v{local_version}, remote: v{version})")
|
|
166
165
|
return
|
|
167
166
|
|
|
168
167
|
self._mark_syncing(file_path)
|
|
169
168
|
|
|
170
169
|
try:
|
|
171
|
-
result = self.client.get_profiles(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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}")
|
|
175
176
|
return
|
|
176
177
|
|
|
177
|
-
|
|
178
|
-
cloud_content = cloud_file.get('content', '')
|
|
178
|
+
cloud_file_content = cloud_content.get(file_path, '')
|
|
179
179
|
|
|
180
180
|
local_content = self._read_local_file(file_path)
|
|
181
181
|
|
|
182
|
-
if local_content is not None and local_content !=
|
|
183
|
-
self._create_conflict_backup(file_path, local_content,
|
|
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
184
|
|
|
185
|
-
self._write_local_file(file_path,
|
|
185
|
+
self._write_local_file(file_path, cloud_file_content)
|
|
186
|
+
self.version_manager.set_version('__profiles__', cloud_version)
|
|
186
187
|
self.version_manager.set_version(file_path, version)
|
|
187
|
-
print(f"[
|
|
188
|
+
print(f"[SoulSync] Pulled remote change: {file_path} (v{version})")
|
|
188
189
|
except Exception as e:
|
|
189
|
-
print(f"[
|
|
190
|
+
print(f"[SoulSync] Error handling remote change for {file_path}: {e}")
|
|
190
191
|
finally:
|
|
191
192
|
self._unmark_syncing(file_path)
|
|
192
193
|
|
|
193
194
|
def _handle_conflict(self, file_path: str, local_content: str, server_content: str, server_version: int):
|
|
194
195
|
"""Handle conflict when pushing file"""
|
|
195
|
-
print(f"[
|
|
196
|
+
print(f"[SoulSync] CONFLICT detected for {file_path}")
|
|
196
197
|
|
|
197
198
|
self._create_conflict_backup(file_path, local_content, server_content)
|
|
198
199
|
|
|
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)
|
|
200
207
|
self.version_manager.set_version(file_path, server_version)
|
|
201
208
|
|
|
202
|
-
print(f"[
|
|
203
|
-
print(f"[
|
|
209
|
+
print(f"[SoulSync] Conflict resolved: server version used for {file_path}")
|
|
210
|
+
print(f"[SoulSync] Please manually merge the conflict backup file")
|