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/CHANGES.md +120 -0
- package/DEPLOY_CHECKLIST.md +151 -0
- package/INSTALL.md +103 -0
- package/SPEC.md +170 -0
- package/TROUBLESHOOTING.md +199 -0
- package/{config.json → config.json.example} +3 -3
- package/debug.py +221 -0
- package/index.js +79 -0
- package/openclaw.plugin.json +18 -0
- package/package.json +7 -21
- package/setup.sh +91 -0
- package/src/__init__.py +1 -0
- package/src/client.py +104 -15
- package/src/interactive_auth.py +283 -0
- package/src/main.py +205 -81
- package/src/main_fixed.py +434 -0
- package/src/register.py +131 -0
- package/src/sync.py +172 -109
- package/workspace/MEMORY.md +0 -5
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
|
-
|
|
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
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
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.
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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()
|