soulsync 1.0.0 → 1.0.5

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.
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SoulSync OpenClaw 插件主类 - 修复版
4
+ 解决跨平台路径问题和导入问题
5
+ 支持交互式认证
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import sys
11
+ import time
12
+ import re
13
+ import getpass
14
+
15
+ # 获取插件根目录
16
+ PLUGIN_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17
+ SRC_DIR = os.path.join(PLUGIN_DIR, 'src')
18
+
19
+ # 添加 src 到路径
20
+ if SRC_DIR not in sys.path:
21
+ sys.path.insert(0, SRC_DIR)
22
+
23
+ try:
24
+ from client import OpenClawClient
25
+ from watcher import OpenClawMultiWatcher
26
+ from version_manager import VersionManager
27
+ from profiles import ProfilesClient
28
+ from sync import ProfileSync
29
+ except ImportError as e:
30
+ print(f"导入错误: {e}")
31
+ print(f"当前路径: {sys.path}")
32
+ print(f"SRC_DIR: {SRC_DIR}")
33
+ raise
34
+
35
+
36
+ class SoulSyncPlugin:
37
+ """SoulSync OpenClaw 插件主类"""
38
+
39
+ def __init__(self):
40
+ self.config = None
41
+ self.client = None
42
+ self.profiles_client = None
43
+ self.watcher = None
44
+ self.version_manager = None
45
+ self.profile_sync = None
46
+ self.running = False
47
+
48
+ def get_input(self, prompt):
49
+ """获取用户输入"""
50
+ try:
51
+ return input(prompt)
52
+ except KeyboardInterrupt:
53
+ print("\n\n操作已取消")
54
+ sys.exit(0)
55
+
56
+ def get_password(self, prompt):
57
+ """获取密码(隐藏输入)"""
58
+ try:
59
+ return getpass.getpass(prompt)
60
+ except KeyboardInterrupt:
61
+ print("\n\n操作已取消")
62
+ sys.exit(0)
63
+
64
+ def is_valid_email(self, email):
65
+ """验证邮箱格式"""
66
+ pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
67
+ return re.match(pattern, email) is not None
68
+
69
+ def save_config(self):
70
+ """保存配置文件"""
71
+ config_path = os.path.normpath(os.path.join(PLUGIN_DIR, 'config.json'))
72
+ try:
73
+ with open(config_path, 'w', encoding='utf-8') as f:
74
+ json.dump(self.config, f, indent=2, ensure_ascii=False)
75
+ return True
76
+ except Exception as e:
77
+ print(f"保存配置失败: {e}")
78
+ return False
79
+
80
+ def interactive_auth(self):
81
+ """交互式认证流程"""
82
+ print("\n" + "=" * 50)
83
+ print("欢迎使用 SoulSync!")
84
+ print("=" * 50)
85
+ print()
86
+
87
+ # 设置默认服务器
88
+ if not self.config.get('cloud_url'):
89
+ self.config['cloud_url'] = 'http://47.96.170.74:3000'
90
+ print(f"使用默认服务器: {self.config['cloud_url']}")
91
+ print()
92
+
93
+ # 创建临时客户端
94
+ self.client = OpenClawClient(self.config)
95
+
96
+ # 询问登录或注册
97
+ print("请选择:")
98
+ print("1. 登录已有账号")
99
+ print("2. 注册新账号")
100
+ print()
101
+
102
+ while True:
103
+ choice = self.get_input("输入选项 (1/2): ").strip()
104
+ if choice in ['1', '2']:
105
+ break
106
+ print("无效选项,请重新输入")
107
+
108
+ if choice == '1':
109
+ return self.interactive_login()
110
+ else:
111
+ return self.interactive_register()
112
+
113
+ def interactive_login(self):
114
+ """交互式登录"""
115
+ print("\n--- 登录 ---")
116
+
117
+ # 输入邮箱
118
+ while True:
119
+ email = self.get_input("邮箱: ").strip()
120
+ if self.is_valid_email(email):
121
+ break
122
+ print("邮箱格式不正确,请重新输入")
123
+
124
+ # 输入密码
125
+ password = self.get_password("密码: ")
126
+
127
+ print("\n正在登录...")
128
+
129
+ try:
130
+ result = self.client.login(email, password)
131
+ print(f"✅ 登录成功!")
132
+
133
+ # 保存到配置
134
+ self.config['email'] = email
135
+ self.config['password'] = password
136
+ self.save_config()
137
+ return True
138
+ except Exception as e:
139
+ print(f"❌ 登录失败: {e}")
140
+ print("\n请重新选择:")
141
+ return self.interactive_auth()
142
+
143
+ def interactive_register(self):
144
+ """交互式注册"""
145
+ print("\n--- 注册新账号 ---")
146
+
147
+ # 输入邮箱
148
+ while True:
149
+ email = self.get_input("邮箱: ").strip()
150
+ if not self.is_valid_email(email):
151
+ print("邮箱格式不正确,请重新输入")
152
+ continue
153
+ break
154
+
155
+ # 发送验证码
156
+ print(f"\n正在发送验证码到 {email}...")
157
+ try:
158
+ self.client.send_verification_code(email)
159
+ print("✅ 验证码已发送!")
160
+ print("请查看服务器控制台获取验证码")
161
+ except Exception as e:
162
+ print(f"❌ 发送验证码失败: {e}")
163
+ return self.interactive_auth()
164
+
165
+ # 输入验证码
166
+ max_attempts = 3
167
+ for attempt in range(max_attempts):
168
+ code = self.get_input(f"请输入验证码 (剩余尝试 {max_attempts - attempt} 次): ").strip()
169
+
170
+ if len(code) == 4 and code.isdigit():
171
+ break
172
+ else:
173
+ print("❌ 验证码格式错误,应为4位数字")
174
+ if attempt == max_attempts - 1:
175
+ print("验证失败次数过多,请重新注册")
176
+ return self.interactive_auth()
177
+
178
+ # 输入密码
179
+ while True:
180
+ password = self.get_password("设置密码 (至少6位): ")
181
+ if len(password) >= 6:
182
+ break
183
+ print("密码太短,请至少输入6位")
184
+
185
+ # 确认密码
186
+ while True:
187
+ password2 = self.get_password("确认密码: ")
188
+ if password == password2:
189
+ break
190
+ print("两次密码不一致,请重新输入")
191
+
192
+ # 注册
193
+ print("\n正在创建账号...")
194
+ try:
195
+ result = self.client.register(email, password, code)
196
+ print("✅ 注册成功!")
197
+
198
+ # 保存配置
199
+ self.config['email'] = email
200
+ self.config['password'] = password
201
+ self.save_config()
202
+
203
+ return True
204
+ except Exception as e:
205
+ print(f"❌ 注册失败: {e}")
206
+ print("\n请重新选择:")
207
+ return self.interactive_auth()
208
+
209
+ def load_config(self):
210
+ """加载配置文件"""
211
+ config_path = os.path.normpath(os.path.join(PLUGIN_DIR, 'config.json'))
212
+
213
+ print(f"Looking for config at: {config_path}")
214
+
215
+ # 如果配置文件不存在,创建默认配置并进行交互式认证
216
+ if not os.path.exists(config_path):
217
+ print("Config file not found, starting interactive setup...")
218
+ self.config = {}
219
+
220
+ # 设置默认 workspace
221
+ workspace = os.path.normpath(os.path.join(PLUGIN_DIR, 'workspace'))
222
+ self.config['workspace'] = './workspace'
223
+ self.config['watch_files'] = [
224
+ "SOUL.md",
225
+ "IDENTITY.md",
226
+ "USER.md",
227
+ "AGENTS.md",
228
+ "TOOLS.md",
229
+ "skills.json",
230
+ "memory/",
231
+ "MEMORY.md"
232
+ ]
233
+
234
+ # 交互式认证
235
+ if not self.interactive_auth():
236
+ raise RuntimeError("Authentication failed")
237
+
238
+ return
239
+
240
+ try:
241
+ with open(config_path, 'r', encoding='utf-8') as f:
242
+ self.config = json.load(f)
243
+ except json.JSONDecodeError as e:
244
+ raise ValueError(f"Invalid JSON in config.json: {e}")
245
+
246
+ # 处理 workspace 路径
247
+ workspace = self.config.get('workspace', './workspace')
248
+ if workspace.startswith('./'):
249
+ workspace = workspace[2:]
250
+ workspace = os.path.normpath(os.path.join(PLUGIN_DIR, workspace))
251
+
252
+ watch_files = self.config.get('watch_files', ['MEMORY.md', 'memory/'])
253
+
254
+ self.config['workspace'] = workspace
255
+ self.config['watch_files'] = watch_files
256
+
257
+ print(f"Config loaded:")
258
+ print(f" Cloud URL: {self.config.get('cloud_url')}")
259
+ print(f" Workspace: {workspace}")
260
+ print(f" Watch files: {watch_files}")
261
+
262
+ # 检查是否需要认证
263
+ email = self.config.get('email', '').strip()
264
+ password = self.config.get('password', '').strip()
265
+
266
+ if not email or not password:
267
+ print("\n邮箱或密码未配置,需要进行认证...")
268
+ if not self.interactive_auth():
269
+ raise RuntimeError("Authentication failed")
270
+
271
+ def initialize(self):
272
+ """初始化组件"""
273
+ print("\n=== Initializing SoulSync Plugin ===\n")
274
+
275
+ self.client = OpenClawClient(self.config)
276
+
277
+ email = self.config.get('email')
278
+ password = self.config.get('password')
279
+
280
+ if not email or not password:
281
+ self.interactive_auth()
282
+ else:
283
+ try:
284
+ self.client.authenticate(email, password)
285
+ print(f"\n✅ 登录成功: {email}")
286
+ except Exception as e:
287
+ print(f"\n❌ 登录失败: {e}")
288
+ print("将进入交互式认证...")
289
+ self.interactive_auth()
290
+
291
+ try:
292
+ profile = self.client.get_profile()
293
+ print(f"\nLogged in as: {profile.get('email')}")
294
+ subscription = profile.get('subscription', {})
295
+ print(f"Subscription: {subscription.get('status')} (days remaining: {subscription.get('daysRemaining', 0)})\n")
296
+ except Exception as e:
297
+ print(f"Warning: Could not get profile: {e}")
298
+
299
+ # 版本管理器
300
+ versions_file = os.path.normpath(os.path.join(PLUGIN_DIR, 'versions.json'))
301
+ self.version_manager = VersionManager(versions_file)
302
+
303
+ self.profiles_client = ProfilesClient(
304
+ self.config.get('cloud_url'),
305
+ self.client.token
306
+ )
307
+
308
+ self.profile_sync = ProfileSync(
309
+ self.profiles_client,
310
+ self.version_manager,
311
+ self.config.get('workspace')
312
+ )
313
+
314
+ print("Pulling all profiles from cloud...")
315
+ try:
316
+ self.profile_sync.pull_all()
317
+ except Exception as e:
318
+ print(f"Warning: Could not pull profiles: {e}")
319
+
320
+ print("\nStarting file watcher...")
321
+ watch_files = self.config.get('watch_files', [])
322
+ self.watcher = OpenClawMultiWatcher(
323
+ self.config.get('workspace'),
324
+ watch_files,
325
+ self.on_file_change
326
+ )
327
+ self.watcher.start()
328
+
329
+ print("\nConnecting to WebSocket...")
330
+ try:
331
+ self.client.connect_websocket(self.on_websocket_message)
332
+ except Exception as e:
333
+ print(f"Warning: Could not connect WebSocket: {e}")
334
+
335
+ self.running = True
336
+
337
+ def on_file_change(self, event_type: str, relative_path: str, absolute_path: str = None):
338
+ """文件变化回调"""
339
+ print(f"\n[File {event_type}] {relative_path}")
340
+
341
+ if event_type in ['modified', 'created']:
342
+ time.sleep(0.5)
343
+
344
+ try:
345
+ self.profile_sync.push_file(relative_path)
346
+ print(f"Upload completed: {relative_path}")
347
+ except Exception as e:
348
+ print(f"Upload error: {e}")
349
+
350
+ elif event_type == 'deleted':
351
+ print(f"File deleted (not synced to cloud): {relative_path}")
352
+
353
+ def on_websocket_message(self, data: dict):
354
+ """WebSocket 消息回调"""
355
+ event = data.get('event')
356
+
357
+ if event == 'file_updated':
358
+ file_path = data.get('file_path')
359
+ version = data.get('version')
360
+ print(f"\n[WebSocket] File updated: {file_path} (v{version})")
361
+ try:
362
+ self.profile_sync.on_remote_change(file_path, version)
363
+ except Exception as e:
364
+ print(f"Sync error: {e}")
365
+
366
+ elif event == 'new_memory':
367
+ print(f"\n[WebSocket] New memory available!")
368
+ try:
369
+ self.profile_sync.pull_all()
370
+ print("Memory synced from remote")
371
+ except Exception as e:
372
+ print(f"Sync error: {e}")
373
+
374
+ elif data.get('type') == 'authenticated':
375
+ print(f"[WebSocket] Authenticated, socket_id: {data.get('socket_id')}")
376
+ elif data.get('type') == 'error':
377
+ print(f"[WebSocket] Error: {data.get('message')}")
378
+
379
+ def run(self):
380
+ """运行插件"""
381
+ print("\n" + "=" * 50)
382
+ print("SoulSync OpenClaw Plugin (Multi-File Sync)")
383
+ print("=" * 50 + "\n")
384
+
385
+ try:
386
+ self.load_config()
387
+ self.initialize()
388
+
389
+ print("\n=== Plugin Running ===")
390
+ print("Press Ctrl+C to stop\n")
391
+
392
+ while self.running:
393
+ time.sleep(1)
394
+
395
+ except KeyboardInterrupt:
396
+ print("\n\nShutting down...")
397
+ self.shutdown()
398
+ except Exception as e:
399
+ print(f"\nError: {e}")
400
+ import traceback
401
+ traceback.print_exc()
402
+ self.shutdown()
403
+ raise
404
+
405
+ def shutdown(self):
406
+ """关闭插件"""
407
+ print("Shutting down SoulSync plugin...")
408
+ self.running = False
409
+
410
+ if self.watcher:
411
+ try:
412
+ self.watcher.stop()
413
+ print("File watcher stopped")
414
+ except Exception as e:
415
+ print(f"Error stopping watcher: {e}")
416
+
417
+ if self.client:
418
+ try:
419
+ self.client.close()
420
+ print("Client connection closed")
421
+ except Exception as e:
422
+ print(f"Error closing client: {e}")
423
+
424
+ print("Plugin shutdown complete")
425
+
426
+
427
+ def main():
428
+ """主函数"""
429
+ plugin = SoulSyncPlugin()
430
+ plugin.run()
431
+
432
+
433
+ if __name__ == '__main__':
434
+ main()
@@ -0,0 +1,131 @@
1
+ import getpass
2
+ import sys
3
+
4
+
5
+ class Register:
6
+ def __init__(self, client):
7
+ self.client = client
8
+
9
+ def run(self):
10
+ print("\n" + "=" * 50)
11
+ print("SoulSync User Registration / SoulSync 用户注册")
12
+ print("=" * 50 + "\n")
13
+
14
+ email = self.get_email()
15
+ if not email:
16
+ return None
17
+
18
+ self.send_code(email)
19
+ code = self.get_code()
20
+ if not code:
21
+ return None
22
+
23
+ password = self.get_password()
24
+ if not password:
25
+ return None
26
+
27
+ result = self.register(email, password, code)
28
+ return result
29
+
30
+ def get_email(self):
31
+ while True:
32
+ email = input("Please enter your email / 请输入您的邮箱: ").strip()
33
+ if email:
34
+ if '@' in email and '.' in email:
35
+ return email
36
+ else:
37
+ print("Please enter a valid email address / 请输入有效的邮箱地址")
38
+ else:
39
+ print("Email cannot be empty / 邮箱不能为空")
40
+
41
+ def send_code(self, email):
42
+ print(f"\nSending verification code to {email} / 正在发送验证码到 {email}...")
43
+ try:
44
+ result = self.client.send_verification_code(email)
45
+ print("Verification code sent! / 验证码已发送!")
46
+ print("Please check server console for the code / 请查看服务器控制台获取验证码")
47
+ print(f"Code expires in / 验证码有效期: {result.get('expires_in', 300)} seconds\n")
48
+ except Exception as e:
49
+ print(f"Failed to send code: {e} / 发送验证码失败: {e}")
50
+ sys.exit(1)
51
+
52
+ def get_code(self):
53
+ max_retries = 3
54
+ retries = 0
55
+ while retries < max_retries:
56
+ code = input("Please enter verification code / 请输入验证码: ").strip()
57
+ if code and len(code) == 6 and code.isdigit():
58
+ return code
59
+ else:
60
+ retries += 1
61
+ remaining = max_retries - retries
62
+ if remaining > 0:
63
+ print(f"Invalid code, please enter 6-digit code / 请输入6位数字验证码, remaining attempts: {remaining}")
64
+ else:
65
+ print("Max retries exceeded, exiting / 超过最大重试次数,退出")
66
+ sys.exit(1)
67
+
68
+ def get_password(self):
69
+ while True:
70
+ password = getpass.getpass("Please enter password / 请输入密码: ")
71
+ if len(password) < 6:
72
+ print("Password must be at least 6 characters / 密码至少6位")
73
+ continue
74
+
75
+ confirm = getpass.getpass("Please confirm password / 请确认密码: ")
76
+ if password == confirm:
77
+ return password
78
+ else:
79
+ print("Passwords do not match, please try again / 两次密码不一致,请重新输入")
80
+
81
+ def register(self, email, password, code):
82
+ print("\nRegistering... / 正在注册...")
83
+ try:
84
+ result = self.client.register(email, password, code)
85
+ print("\nRegistration successful! / 注册成功!")
86
+ return result
87
+ except Exception as e:
88
+ print(f"\nRegistration failed: {e} / 注册失败: {e}")
89
+ sys.exit(1)
90
+
91
+
92
+ class Login:
93
+ def __init__(self, client):
94
+ self.client = client
95
+
96
+ def run(self):
97
+ print("\n" + "=" * 50)
98
+ print("SoulSync User Login / SoulSync 用户登录")
99
+ print("=" * 50 + "\n")
100
+
101
+ email = self.get_email()
102
+ password = self.get_password()
103
+
104
+ result = self.login(email, password)
105
+ return result
106
+
107
+ def get_email(self):
108
+ while True:
109
+ email = input("Please enter your email / 请输入您的邮箱: ").strip()
110
+ if email:
111
+ return email
112
+ else:
113
+ print("Email cannot be empty / 邮箱不能为空")
114
+
115
+ def get_password(self):
116
+ while True:
117
+ password = getpass.getpass("Please enter password / 请输入密码: ")
118
+ if password:
119
+ return password
120
+ else:
121
+ print("Password cannot be empty / 密码不能为空")
122
+
123
+ def login(self, email, password):
124
+ print("\nLogging in... / 正在登录...")
125
+ try:
126
+ result = self.client.login(email, password)
127
+ print("\nLogin successful! / 登录成功!")
128
+ return result
129
+ except Exception as e:
130
+ print(f"\nLogin failed: {e} / 登录失败: {e}")
131
+ sys.exit(1)