soulsync 1.0.22 → 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/index.js +41 -43
- package/package.json +5 -3
- package/src/api-client.js +84 -0
- package/src/daemon.js +94 -0
- package/src/sync-engine.js +745 -0
- package/debug.py +0 -221
- package/requirements.txt +0 -3
- package/setup.sh +0 -91
- package/src/__init__.py +0 -1
- package/src/client.py +0 -300
- package/src/main.py +0 -479
- package/src/profiles.py +0 -88
- package/src/sync.py +0 -210
- package/src/version_manager.py +0 -61
- package/src/watcher.py +0 -133
- package/test_ssl_fix.py +0 -58
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}")
|