soulsync 1.0.22 → 1.2.2

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/main.py DELETED
@@ -1,479 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- SoulSync OpenClaw 插件主类
4
- """
5
-
6
- import json
7
- import os
8
- import sys
9
- import time
10
- import getpass
11
- import argparse
12
-
13
- # 获取插件根目录
14
- PLUGIN_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
15
- SRC_DIR = os.path.join(PLUGIN_DIR, 'src')
16
-
17
- # 添加 src 到路径
18
- if SRC_DIR not in sys.path:
19
- sys.path.insert(0, SRC_DIR)
20
-
21
- from client import OpenClawClient
22
- from watcher import OpenClawMultiWatcher
23
- from version_manager import VersionManager
24
- from profiles import ProfilesClient
25
- from sync import ProfileSync
26
-
27
-
28
- class SoulSyncPlugin:
29
- """SoulSync OpenClaw 插件主类"""
30
-
31
- def __init__(self):
32
- self.config = None
33
- self.client = None
34
- self.profiles_client = None
35
- self.watcher = None
36
- self.version_manager = None
37
- self.profile_sync = None
38
- self.running = False
39
-
40
- def load_config(self):
41
- """加载配置文件"""
42
- config_path = os.path.normpath(os.path.join(PLUGIN_DIR, 'config.json'))
43
- config_example_path = os.path.normpath(os.path.join(PLUGIN_DIR, 'config.json.example'))
44
-
45
- print(f"[SoulSync] Looking for config at: {config_path}")
46
-
47
- if not os.path.exists(config_path):
48
- if os.path.exists(config_example_path):
49
- print("[SoulSync] Config file not found, copying from config.json.example...")
50
- import shutil
51
- shutil.copy(config_example_path, config_path)
52
- print("[SoulSync] Created config.json from template")
53
- else:
54
- raise FileNotFoundError(f"Config file not found: {config_path}")
55
-
56
- try:
57
- with open(config_path, 'r', encoding='utf-8') as f:
58
- self.config = json.load(f)
59
- except json.JSONDecodeError as e:
60
- raise ValueError(f"Invalid JSON in config.json: {e}")
61
-
62
- cloud_url = self.config.get('cloud_url', '').strip()
63
- email = self.config.get('email', '').strip()
64
- token = self.config.get('token', '').strip()
65
-
66
- if not cloud_url:
67
- self.config['cloud_url'] = 'https://soulsync.work'
68
- print("[SoulSync] Cloud URL not set, using default: https://soulsync.work")
69
-
70
- workspace = self.config.get('workspace', './workspace')
71
- if workspace.startswith('./'):
72
- workspace = workspace[2:]
73
- workspace = os.path.normpath(os.path.join(PLUGIN_DIR, workspace))
74
-
75
- watch_files = self.config.get('watch_files', [])
76
-
77
- self.config['workspace'] = workspace
78
- self.config['watch_files'] = watch_files
79
-
80
- print(f"[SoulSync] Config loaded:")
81
- print(f" Cloud URL: {self.config.get('cloud_url')}")
82
- print(f" Workspace: {workspace}")
83
- print(f" Watch files: {watch_files}")
84
-
85
- def _save_auth_to_config(self, auth_result):
86
- """保存认证结果到 config.json"""
87
- config_path = os.path.normpath(os.path.join(PLUGIN_DIR, 'config.json'))
88
-
89
- try:
90
- with open(config_path, 'r', encoding='utf-8') as f:
91
- config = json.load(f)
92
- except:
93
- config = {}
94
-
95
- # 保存 email(支持两种返回格式:authenticate 有 user 对象,register 只有一个字段)
96
- if 'user' in auth_result and isinstance(auth_result.get('user'), dict):
97
- config['email'] = auth_result['user'].get('email', '')
98
- elif 'email' in auth_result:
99
- config['email'] = auth_result.get('email', '')
100
-
101
- # 保存 token
102
- if 'token' in auth_result:
103
- config['token'] = auth_result['token']
104
-
105
- with open(config_path, 'w', encoding='utf-8') as f:
106
- json.dump(config, f, indent=2, ensure_ascii=False)
107
-
108
- print("[SoulSync] Auth info saved to config.json")
109
-
110
- def run_setup(self):
111
- """交互式设置:注册或登录(带循环)"""
112
- while True:
113
- print("\n[SoulSync] ========================================")
114
- print("[SoulSync] Welcome to SoulSync! / 欢迎使用 SoulSync!")
115
- print("[SoulSync] ========================================")
116
- print("[SoulSync] 1. Register / 注册")
117
- print("[SoulSync] 2. Login / 登录")
118
- print("[SoulSync] 0. Exit / 退出")
119
- print("[SoulSync] ========================================")
120
-
121
- try:
122
- choice = input("[SoulSync] Choose / 选择 (0/1/2): ").strip()
123
- except KeyboardInterrupt:
124
- print("\n[SoulSync] Cancelled by user. Goodbye! / 已取消,再见!")
125
- sys.exit(0)
126
-
127
- if choice == '1':
128
- result = self._interactive_register()
129
- if result:
130
- return result
131
- elif choice == '2':
132
- result = self._interactive_login()
133
- if result:
134
- return result
135
- elif choice == '0':
136
- print("[SoulSync] Goodbye! / 再见!")
137
- sys.exit(2)
138
- else:
139
- print("[SoulSync] Invalid choice / 无效选择")
140
-
141
- def _interactive_login(self):
142
- """交互式登录(失败返回 None,由主循环处理)"""
143
- from client import OpenClawClient
144
-
145
- print("\n--- Login / 登录 ---")
146
-
147
- try:
148
- email = input("[SoulSync] Email / 邮箱: ").strip()
149
- if not email:
150
- print("[SoulSync] Email cannot be empty / 邮箱不能为空")
151
- return None
152
- except KeyboardInterrupt:
153
- print("\n[SoulSync] Cancelled. Returning to menu... / 已取消,返回菜单...")
154
- return None
155
-
156
- try:
157
- password = getpass.getpass("[SoulSync] Password / 密码: ")
158
- if not password:
159
- print("[SoulSync] Password cannot be empty / 密码不能为空")
160
- return None
161
- except KeyboardInterrupt:
162
- print("\n[SoulSync] Cancelled. Returning to menu... / 已取消,返回菜单...")
163
- return None
164
-
165
- try:
166
- temp_client = OpenClawClient(self.config)
167
- result = temp_client.authenticate(email, password)
168
- if result:
169
- result['email'] = email
170
- result['password'] = password
171
- print("\n[SoulSync] ✓ Login successful! / 登录成功!")
172
- self._save_auth_to_config(result)
173
- return True
174
- except Exception as e:
175
- error_msg = str(e)
176
-
177
- if "429" in error_msg or "too many" in error_msg.lower():
178
- print(f"\n[SoulSync] ❌ {e}")
179
- print("\n[SoulSync] Too many failed attempts. Please try again later / 登录失败次数过多,请稍后再试")
180
- print("[SoulSync] Exiting... / 退出...")
181
- sys.exit(0)
182
-
183
- print(f"\n[SoulSync] ❌ {e}")
184
- print("[SoulSync] Returning to menu... / 返回菜单...")
185
- return None
186
-
187
- print("[SoulSync] ❌ Login failed / 登录失败")
188
- print("[SoulSync] Returning to menu... / 返回菜单...")
189
- return None
190
-
191
- def _interactive_register(self):
192
- """交互式注册(失败返回 None)"""
193
- from client import OpenClawClient
194
-
195
- print("\n--- Register / 注册 ---")
196
-
197
- try:
198
- email = input("[SoulSync] Email / 邮箱: ").strip()
199
- if not email or '@' not in email:
200
- print("[SoulSync] Invalid email / 无效邮箱")
201
- return None
202
- except KeyboardInterrupt:
203
- print("\n[SoulSync] Cancelled. Returning to menu... / 已取消,返回菜单...")
204
- return None
205
-
206
- password = None
207
- while password is None:
208
- try:
209
- password = getpass.getpass("[SoulSync] Password / 密码: ")
210
- if len(password) < 6:
211
- print("[SoulSync] Password must be at least 6 characters / 密码至少6位")
212
- password = None
213
- continue
214
- except KeyboardInterrupt:
215
- print("\n[SoulSync] Cancelled. Returning to menu... / 已取消,返回菜单...")
216
- return None
217
-
218
- try:
219
- password2 = getpass.getpass("[SoulSync] Confirm password / 确认密码: ")
220
- except KeyboardInterrupt:
221
- print("\n[SoulSync] Cancelled. Returning to menu... / 已取消,返回菜单...")
222
- return None
223
-
224
- if password != password2:
225
- print("[SoulSync] Passwords do not match / 两次密码不一致")
226
- password = None
227
-
228
- print(f"\n[SoulSync] Sending verification code to {email}...")
229
- try:
230
- temp_client = OpenClawClient(self.config)
231
- temp_client.send_verification_code(email)
232
- print("[SoulSync] ✓ Verification code sent! / 验证码已发送!")
233
- except Exception as e:
234
- print(f"[SoulSync] ❌ Failed to send code: {e}")
235
- print("[SoulSync] Returning to menu... / 返回菜单...")
236
- return None
237
-
238
- code_success = False
239
- code_retry = 0
240
- max_code_retries = 3
241
-
242
- while code_retry < max_code_retries and not code_success:
243
- try:
244
- code = input(f"[SoulSync] Enter code / 输入验证码 ({max_code_retries - code_retry} left): ").strip()
245
- except KeyboardInterrupt:
246
- print("\n[SoulSync] Cancelled. Returning to menu... / 已取消,返回菜单...")
247
- return None
248
-
249
- if len(code) != 6 or not code.isdigit():
250
- code_retry += 1
251
- print("[SoulSync] Invalid code format / 验证码格式错误")
252
- continue
253
-
254
- try:
255
- result = temp_client.register(email, password, code)
256
- print("\n[SoulSync] ✓ Registration successful! / 注册成功!")
257
- self._save_auth_to_config(result)
258
- return True
259
- except Exception as e:
260
- code_retry += 1
261
- if "invalid" in str(e).lower() or "expired" in str(e).lower():
262
- if code_retry < max_code_retries:
263
- print(f"[SoulSync] ❌ Invalid or expired code: {e}")
264
- print(f"[SoulSync] Remaining attempts / 剩余尝试: {max_code_retries - code_retry}")
265
- else:
266
- print("[SoulSync] ❌ Too many code attempts / 验证码错误次数过多")
267
- else:
268
- print(f"[SoulSync] ❌ Registration failed: {e}")
269
-
270
- print("\n[SoulSync] Too many code verification failures.")
271
- print("[SoulSync] Returning to menu... / 返回菜单...")
272
- return None
273
-
274
- def initialize(self):
275
- """初始化组件"""
276
- print("\n[SoulSync] ========================================")
277
- print("[SoulSync] Initializing SoulSync Plugin")
278
- print("[SoulSync] ========================================\n")
279
-
280
- self.client = OpenClawClient(self.config)
281
-
282
- token = self.client._load_token()
283
- if token:
284
- try:
285
- profile = self.client.get_profile()
286
- print(f"[SoulSync] Using existing token, user: {profile.get('email', 'unknown')}")
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
293
- print(f"[SoulSync] Token invalid, re-authenticating: {e}")
294
- token = None
295
-
296
- if not token:
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
-
301
- email = self.config.get('email')
302
-
303
- try:
304
- profile = self.client.get_profile()
305
- print(f"\n[SoulSync] Logged in as: {profile.get('email')}")
306
- subscription = profile.get('subscription', {})
307
- print(f"[SoulSync] Subscription: {subscription.get('status')} (days remaining: {subscription.get('daysRemaining', 0)})\n")
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
314
- print(f"[SoulSync] Warning: Could not get profile: {e}")
315
-
316
- # 版本管理器
317
- versions_file = os.path.normpath(os.path.join(PLUGIN_DIR, 'versions.json'))
318
- self.version_manager = VersionManager(versions_file)
319
-
320
- self.profiles_client = ProfilesClient(
321
- self.config.get('cloud_url'),
322
- self.client.token
323
- )
324
-
325
- self.profile_sync = ProfileSync(
326
- self.profiles_client,
327
- self.version_manager,
328
- self.config.get('workspace')
329
- )
330
-
331
- print("[SoulSync] Pulling all profiles from cloud...")
332
- try:
333
- self.profile_sync.pull_all()
334
- except Exception as e:
335
- print(f"[SoulSync] Warning: Could not pull profiles: {e}")
336
-
337
- print("\n[SoulSync] Starting file watcher...")
338
- watch_files = self.config.get('watch_files', [])
339
- self.watcher = OpenClawMultiWatcher(
340
- self.config.get('workspace'),
341
- watch_files,
342
- self.on_file_change
343
- )
344
- self.watcher.start()
345
-
346
- print("\n[SoulSync] Connecting to WebSocket...")
347
- try:
348
- self.client.connect_websocket(self.on_websocket_message)
349
- except Exception as e:
350
- print(f"[SoulSync] Warning: Could not connect WebSocket: {e}")
351
-
352
- self.running = True
353
-
354
- def on_file_change(self, event_type: str, relative_path: str, absolute_path: str = None):
355
- """文件变化回调"""
356
- print(f"\n[SoulSync] [File {event_type}] {relative_path}")
357
-
358
- if event_type in ['modified', 'created']:
359
- time.sleep(0.5)
360
-
361
- try:
362
- self.profile_sync.push_file(relative_path)
363
- print(f"[SoulSync] Upload completed: {relative_path}")
364
- except Exception as e:
365
- print(f"[SoulSync] Upload error: {e}")
366
-
367
- elif event_type == 'deleted':
368
- print(f"[SoulSync] File deleted (not synced to cloud): {relative_path}")
369
-
370
- def on_websocket_message(self, data: dict):
371
- """WebSocket 消息回调"""
372
- event = data.get('event')
373
-
374
- if event == 'profile:updated':
375
- user_id = data.get('user_id')
376
- version = data.get('version')
377
- print(f"\n[SoulSync] [WebSocket] Profile updated (v{version})")
378
- try:
379
- self.profile_sync.pull_all()
380
- except Exception as e:
381
- print(f"[SoulSync] Sync error: {e}")
382
-
383
- elif data.get('type') == 'authenticated':
384
- print(f"[SoulSync] [WebSocket] Authenticated, socket_id: {data.get('socket_id')}")
385
- elif data.get('type') == 'error':
386
- print(f"[SoulSync] [WebSocket] Error: {data.get('message')}")
387
-
388
- def run(self):
389
- """运行插件"""
390
- print("\n" + "=" * 50)
391
- print("SoulSync OpenClaw Plugin (Multi-File Sync)")
392
- print("=" * 50 + "\n")
393
-
394
- try:
395
- self.load_config()
396
- self.initialize()
397
-
398
- print("\n[SoulSync] === Plugin Running ===")
399
- print("[SoulSync] Press Ctrl+C to stop\n")
400
-
401
- while self.running:
402
- time.sleep(1)
403
-
404
- except KeyboardInterrupt:
405
- print("\n[SoulSync] Shutting down...")
406
- self.shutdown()
407
- except Exception as e:
408
- print(f"\n[SoulSync] Error: {e}")
409
- import traceback
410
- traceback.print_exc()
411
- self.shutdown()
412
- raise
413
-
414
- def shutdown(self):
415
- """关闭插件"""
416
- print("[SoulSync] Shutting down SoulSync plugin...")
417
- self.running = False
418
-
419
- if self.watcher:
420
- try:
421
- self.watcher.stop()
422
- print("[SoulSync] File watcher stopped")
423
- except Exception as e:
424
- print(f"[SoulSync] Error stopping watcher: {e}")
425
-
426
- if self.client:
427
- try:
428
- self.client.close()
429
- print("[SoulSync] Client connection closed")
430
- except Exception as e:
431
- print(f"[SoulSync] Error closing client: {e}")
432
-
433
- print("[SoulSync] Plugin shutdown complete")
434
- def main():
435
- """主函数"""
436
- try:
437
- parser = argparse.ArgumentParser(description='SoulSync Plugin')
438
- parser.add_argument('--setup', action='store_true', help='Run interactive setup (register/login)')
439
- parser.add_argument('--start', action='store_true', help='Start sync service (auto-login from config)')
440
-
441
- args = parser.parse_args()
442
-
443
- plugin = SoulSyncPlugin()
444
-
445
- if args.setup:
446
- try:
447
- plugin.load_config()
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
457
- except KeyboardInterrupt:
458
- print("\n[SoulSync] Cancelled by user. Goodbye! / 已取消,再见!")
459
- sys.exit(2)
460
-
461
- plugin.load_config()
462
-
463
- email = plugin.config.get('email', '').strip()
464
- token = plugin.config.get('token', '').strip()
465
-
466
- if not email or not token:
467
- print("\n[SoulSync] ========================================")
468
- print("[SoulSync] Not configured. Run 'openclaw soulsync:setup' first.")
469
- print("[SoulSync] 尚未配置,请先运行 'openclaw soulsync:setup'")
470
- print("[SoulSync] ========================================\n")
471
- sys.exit(0)
472
-
473
- plugin.initialize()
474
- plugin.run()
475
- except KeyboardInterrupt:
476
- print("\n[SoulSync] Cancelled by user. Goodbye! / 已取消,再见!")
477
- sys.exit(0)
478
- if __name__ == '__main__':
479
- main()
package/src/profiles.py DELETED
@@ -1,88 +0,0 @@
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
7
-
8
-
9
- class ProfilesClient:
10
- """Profiles API 客户端 - 统一同步接口"""
11
-
12
- def __init__(self, cloud_url: str, token: str = None):
13
- self.cloud_url = cloud_url.rstrip('/')
14
- self.token = token
15
- self.session = requests.Session()
16
- self.session.mount('https://', TLSAdapter())
17
-
18
- def _get_headers(self) -> dict:
19
- """获取请求头"""
20
- headers = {'Content-Type': 'application/json'}
21
- if self.token:
22
- headers['Authorization'] = f'Bearer {self.token}'
23
- return headers
24
-
25
- def set_token(self, token: str):
26
- """设置 token"""
27
- self.token = token
28
-
29
- def get_profiles(self) -> dict:
30
- """获取用户的完整 profiles
31
-
32
- Returns:
33
- 包含 content (dict), version, updated_at 的字典
34
- 示例: {"content": {"SOUL.md": "...", "USER.md": "..."}, "version": 3, "updated_at": "..."}
35
- """
36
- url = f"{self.cloud_url}/api/profiles"
37
-
38
- response = self.session.get(url, headers=self._get_headers())
39
-
40
- if response.status_code == 200:
41
- return response.json()
42
- else:
43
- error = response.json().get('error', 'Unknown error')
44
- raise Exception(f"Get profiles failed: {error}")
45
-
46
- def upload_profiles(self, content: dict, version: int) -> dict:
47
- """整体替换用户的 profiles
48
-
49
- Args:
50
- content: 包含所有文件的 dict,key 是文件名,value 是内容
51
- 示例: {"SOUL.md": "...", "USER.md": "...", "MEMORY.md": "..."}
52
- version: 客户端当前持有的版本号
53
-
54
- Returns:
55
- 成功时返回 {"content": {...}, "version": N, "updated_at": "..."}
56
- 冲突时抛出 ConflictError
57
- """
58
- url = f"{self.cloud_url}/api/profiles"
59
- data = {
60
- 'content': content,
61
- 'version': version
62
- }
63
-
64
- response = self.session.put(url, json=data, headers=self._get_headers())
65
-
66
- if response.status_code == 200:
67
- return response.json()
68
- elif response.status_code == 409:
69
- result = response.json()
70
- raise ConflictError(
71
- server_content=result.get('server_content', {}),
72
- server_version=result.get('server_version', 0)
73
- )
74
- elif response.status_code == 403:
75
- error = response.json().get('error', 'Subscription required')
76
- raise Exception(f"Upload failed: {error}")
77
- else:
78
- error = response.json().get('error', 'Unknown error')
79
- raise Exception(f"Upload failed: {error}")
80
-
81
-
82
- class ConflictError(Exception):
83
- """版本冲突异常"""
84
-
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}")