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.
package/src/main.py CHANGED
@@ -1,20 +1,33 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SoulSync OpenClaw 插件主类
4
+ """
5
+
1
6
  import json
2
7
  import os
3
8
  import sys
4
9
  import time
5
10
 
6
- sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'base'))
11
+ # 获取插件根目录
12
+ PLUGIN_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13
+ SRC_DIR = os.path.join(PLUGIN_DIR, 'src')
14
+
15
+ # 添加 src 到路径
16
+ if SRC_DIR not in sys.path:
17
+ sys.path.insert(0, SRC_DIR)
7
18
 
8
- from src.client import OpenClawClient
9
- from src.watcher import OpenClawMultiWatcher
10
- from src.version_manager import VersionManager
11
- from src.profiles import ProfilesClient
12
- from src.sync import ProfileSync
19
+ from client import OpenClawClient
20
+ from watcher import OpenClawMultiWatcher
21
+ from version_manager import VersionManager
22
+ from profiles import ProfilesClient
23
+ from sync import ProfileSync
24
+ from register import Register, Login
25
+ from interactive_auth import prompt_for_missing_config, interactive_setup, check_existing_config
13
26
 
14
27
 
15
28
  class SoulSyncPlugin:
16
29
  """SoulSync OpenClaw 插件主类"""
17
-
30
+
18
31
  def __init__(self):
19
32
  self.config = None
20
33
  self.client = None
@@ -23,72 +36,179 @@ class SoulSyncPlugin:
23
36
  self.version_manager = None
24
37
  self.profile_sync = None
25
38
  self.running = False
26
-
39
+
27
40
  def load_config(self):
28
41
  """加载配置文件"""
29
- config_path = os.path.join(os.path.dirname(__file__), '..', 'config.json')
30
-
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"Looking for config at: {config_path}")
46
+
31
47
  if not os.path.exists(config_path):
32
- raise FileNotFoundError(f"Config file not found: {config_path}")
33
-
34
- with open(config_path, 'r', encoding='utf-8') as f:
35
- self.config = json.load(f)
48
+ if os.path.exists(config_example_path):
49
+ print("Config file not found, copying from config.json.example...")
50
+ import shutil
51
+ shutil.copy(config_example_path, config_path)
52
+ print(f"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
+ # 检查必要配置
63
+ cloud_url = self.config.get('cloud_url', '').strip()
64
+ email = self.config.get('email', '').strip()
65
+ password = self.config.get('password', '').strip()
36
66
 
37
- workspace = self.config.get('workspace', './workspace')
38
- workspace = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', workspace))
67
+ # 如果 cloud_url 为空,设置为默认值
68
+ if not cloud_url:
69
+ self.config['cloud_url'] = 'https://soulsync.work'
70
+ print("Cloud URL not set, using default: https://soulsync.work")
39
71
 
40
- watch_files = self.config.get('watch_files', ['MEMORY.md', 'memory/'])
72
+ # 如果 email password 为空,需要交互式认证
73
+ if not email or not password:
74
+ print("\nEmail or password not configured, initiating interactive setup...")
75
+ self._interactive_setup()
76
+ # 重新加载配置
77
+ with open(config_path, 'r', encoding='utf-8') as f:
78
+ self.config = json.load(f)
41
79
 
80
+ # 处理 workspace 路径
81
+ workspace = self.config.get('workspace', './workspace')
82
+ if workspace.startswith('./'):
83
+ workspace = workspace[2:]
84
+ workspace = os.path.normpath(os.path.join(PLUGIN_DIR, workspace))
85
+
86
+ watch_files = self.config.get('watch_files', [])
87
+
42
88
  self.config['workspace'] = workspace
43
89
  self.config['watch_files'] = watch_files
44
-
90
+
45
91
  print(f"Config loaded:")
46
92
  print(f" Cloud URL: {self.config.get('cloud_url')}")
47
93
  print(f" Workspace: {workspace}")
48
94
  print(f" Watch files: {watch_files}")
95
+
96
+ def _interactive_setup(self):
97
+ """交互式设置:引导用户登录或注册"""
98
+ from register import Register, Login
99
+ from interactive_auth import interactive_setup
100
+
101
+ print("\n=== First run - Please login or register / 首次运行,请先登录或注册 ===\n")
102
+ print("1. Login / 登录(已有账号)")
103
+ print("2. Register / 注册(新用户)")
104
+
105
+ choice = input("Choose (1/2): ").strip()
106
+
107
+ if choice == '1':
108
+ # 先创建临时 client 用于登录
109
+ temp_client = OpenClawClient(self.config)
110
+ login = Login(temp_client)
111
+ result = login.run()
112
+ if result:
113
+ # 保存认证信息到 config
114
+ self._save_auth_to_config(result)
115
+ elif choice == '2':
116
+ temp_client = OpenClawClient(self.config)
117
+ register = Register(temp_client)
118
+ result = register.run()
119
+ if result:
120
+ self._save_auth_to_config(result)
121
+ else:
122
+ print("Invalid choice / 无效选择")
123
+ sys.exit(1)
49
124
 
125
+ def _save_auth_to_config(self, auth_result):
126
+ """保存认证结果到 config.json"""
127
+ config_path = os.path.normpath(os.path.join(PLUGIN_DIR, 'config.json'))
128
+
129
+ try:
130
+ with open(config_path, 'r', encoding='utf-8') as f:
131
+ config = json.load(f)
132
+ except:
133
+ config = {}
134
+
135
+ # 保存 email 和 password(如果 auth_result 包含)
136
+ if 'user' in auth_result:
137
+ config['email'] = auth_result['user'].get('email', '')
138
+
139
+ # 保存 token
140
+ if 'token' in auth_result:
141
+ config['token'] = auth_result['token']
142
+
143
+ with open(config_path, 'w', encoding='utf-8') as f:
144
+ json.dump(config, f, indent=2, ensure_ascii=False)
145
+
146
+ print("Auth info saved to config.json")
147
+
50
148
  def initialize(self):
51
149
  """初始化组件"""
52
150
  print("\n=== Initializing SoulSync Plugin ===\n")
53
-
151
+
54
152
  self.client = OpenClawClient(self.config)
55
-
153
+
154
+ token = self.client._load_token()
155
+ if token:
156
+ try:
157
+ profile = self.client.get_profile()
158
+ print(f"Using existing token, user: {profile.get('email', 'unknown')}")
159
+ except Exception as e:
160
+ print(f"Token invalid / 令牌无效, re-authenticating: {e}")
161
+ token = None
162
+
163
+ if not token:
164
+ print("\n=== Token invalid - Please login or register / 令牌无效,请先登录或注册 ===\n")
165
+ print("1. Login / 登录(已有账号)")
166
+ print("2. Register / 注册(新用户)")
167
+
168
+ choice = input("Choose (1/2): ").strip()
169
+
170
+ if choice == '1':
171
+ login = Login(self.client)
172
+ result = login.run()
173
+ elif choice == '2':
174
+ register = Register(self.client)
175
+ result = register.run()
176
+ else:
177
+ print("Invalid choice / 无效选择")
178
+ sys.exit(1)
179
+
56
180
  email = self.config.get('email')
57
181
  password = self.config.get('password')
58
-
59
- if not email or not password:
60
- print("WARNING: Email and password not configured!")
61
- print("Please edit config.json and add your email and password\n")
62
-
182
+
63
183
  try:
64
- self.client.authenticate(email, password)
184
+ profile = self.client.get_profile()
185
+ print(f"\nLogged in as: {profile.get('email')}")
186
+ subscription = profile.get('subscription', {})
187
+ print(f"Subscription: {subscription.get('status')} (days remaining: {subscription.get('daysRemaining', 0)})\n")
65
188
  except Exception as e:
66
- print(f"Authentication error: {e}")
67
- print("Please check your config.json and try again\n")
68
- raise
69
-
70
- profile = self.client.get_profile()
71
- print(f"\nLogged in as: {profile.get('email')}")
72
- subscription = profile.get('subscription', {})
73
- print(f"Subscription: {subscription.get('status')} (days remaining: {subscription.get('daysRemaining', 0)})\n")
74
-
75
- versions_file = os.path.join(os.path.dirname(__file__), '..', 'versions.json')
189
+ print(f"Warning: Could not get profile: {e}")
190
+
191
+ # 版本管理器
192
+ versions_file = os.path.normpath(os.path.join(PLUGIN_DIR, 'versions.json'))
76
193
  self.version_manager = VersionManager(versions_file)
77
-
194
+
78
195
  self.profiles_client = ProfilesClient(
79
196
  self.config.get('cloud_url'),
80
197
  self.client.token
81
198
  )
82
-
199
+
83
200
  self.profile_sync = ProfileSync(
84
201
  self.profiles_client,
85
202
  self.version_manager,
86
203
  self.config.get('workspace')
87
204
  )
88
-
205
+
89
206
  print("Pulling all profiles from cloud...")
90
- self.profile_sync.pull_all()
91
-
207
+ try:
208
+ self.profile_sync.pull_all()
209
+ except Exception as e:
210
+ print(f"Warning: Could not pull profiles: {e}")
211
+
92
212
  print("\nStarting file watcher...")
93
213
  watch_files = self.config.get('watch_files', [])
94
214
  self.watcher = OpenClawMultiWatcher(
@@ -97,32 +217,35 @@ class SoulSyncPlugin:
97
217
  self.on_file_change
98
218
  )
99
219
  self.watcher.start()
100
-
220
+
101
221
  print("\nConnecting to WebSocket...")
102
- self.client.connect_websocket(self.on_websocket_message)
103
-
222
+ try:
223
+ self.client.connect_websocket(self.on_websocket_message)
224
+ except Exception as e:
225
+ print(f"Warning: Could not connect WebSocket: {e}")
226
+
104
227
  self.running = True
105
-
228
+
106
229
  def on_file_change(self, event_type: str, relative_path: str, absolute_path: str = None):
107
230
  """文件变化回调"""
108
231
  print(f"\n[File {event_type}] {relative_path}")
109
-
232
+
110
233
  if event_type in ['modified', 'created']:
111
234
  time.sleep(0.5)
112
-
235
+
113
236
  try:
114
237
  self.profile_sync.push_file(relative_path)
115
238
  print(f"Upload completed: {relative_path}")
116
239
  except Exception as e:
117
240
  print(f"Upload error: {e}")
118
-
241
+
119
242
  elif event_type == 'deleted':
120
243
  print(f"File deleted (not synced to cloud): {relative_path}")
121
-
244
+
122
245
  def on_websocket_message(self, data: dict):
123
246
  """WebSocket 消息回调"""
124
247
  event = data.get('event')
125
-
248
+
126
249
  if event == 'file_updated':
127
250
  file_path = data.get('file_path')
128
251
  version = data.get('version')
@@ -131,7 +254,7 @@ class SoulSyncPlugin:
131
254
  self.profile_sync.on_remote_change(file_path, version)
132
255
  except Exception as e:
133
256
  print(f"Sync error: {e}")
134
-
257
+
135
258
  elif event == 'new_memory':
136
259
  print(f"\n[WebSocket] New memory available!")
137
260
  try:
@@ -139,60 +262,61 @@ class SoulSyncPlugin:
139
262
  print("Memory synced from remote")
140
263
  except Exception as e:
141
264
  print(f"Sync error: {e}")
142
-
265
+
143
266
  elif data.get('type') == 'authenticated':
144
267
  print(f"[WebSocket] Authenticated, socket_id: {data.get('socket_id')}")
145
268
  elif data.get('type') == 'error':
146
269
  print(f"[WebSocket] Error: {data.get('message')}")
147
-
270
+
148
271
  def run(self):
149
272
  """运行插件"""
150
273
  print("\n" + "=" * 50)
151
274
  print("SoulSync OpenClaw Plugin (Multi-File Sync)")
152
275
  print("=" * 50 + "\n")
153
-
276
+
154
277
  try:
155
278
  self.load_config()
156
279
  self.initialize()
157
-
280
+
158
281
  print("\n=== Plugin Running ===")
159
282
  print("Press Ctrl+C to stop\n")
160
-
283
+
161
284
  while self.running:
162
285
  time.sleep(1)
163
-
164
- if hasattr(self.client, 'ws') and self.client.ws:
165
- if not self.client.ws.sock or not self.client.ws.sock.connected:
166
- print("WebSocket disconnected, reconnecting...")
167
- try:
168
- self.client.connect_websocket(self.on_websocket_message)
169
- except Exception as e:
170
- print(f"Reconnect error: {e}")
171
-
286
+
172
287
  except KeyboardInterrupt:
173
288
  print("\n\nShutting down...")
289
+ self.shutdown()
174
290
  except Exception as e:
175
- print(f"Error: {e}")
176
- finally:
177
- self.stop()
178
-
179
- def stop(self):
180
- """停止插件"""
291
+ print(f"\nError: {e}")
292
+ import traceback
293
+ traceback.print_exc()
294
+ self.shutdown()
295
+ raise
296
+
297
+ def shutdown(self):
298
+ """关闭插件"""
299
+ print("Shutting down SoulSync plugin...")
181
300
  self.running = False
182
-
301
+
183
302
  if self.watcher:
184
- self.watcher.stop()
185
-
303
+ try:
304
+ self.watcher.stop()
305
+ print("File watcher stopped")
306
+ except Exception as e:
307
+ print(f"Error stopping watcher: {e}")
308
+
186
309
  if self.client:
187
- self.client.disconnect_websocket()
188
-
189
- print("Plugin stopped")
190
-
191
-
310
+ try:
311
+ self.client.close()
312
+ print("Client connection closed")
313
+ except Exception as e:
314
+ print(f"Error closing client: {e}")
315
+
316
+ print("Plugin shutdown complete")
192
317
  def main():
318
+ """主函数"""
193
319
  plugin = SoulSyncPlugin()
194
320
  plugin.run()
195
-
196
-
197
321
  if __name__ == '__main__':
198
322
  main()