soulsync 1.0.0 → 1.0.4
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 +54 -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 +81 -11
- package/src/interactive_auth.py +283 -0
- package/src/main.py +130 -83
- 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,102 @@ 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
|
+
|
|
44
|
+
print(f"Looking for config at: {config_path}")
|
|
45
|
+
|
|
31
46
|
if not os.path.exists(config_path):
|
|
32
47
|
raise FileNotFoundError(f"Config file not found: {config_path}")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
51
|
+
self.config = json.load(f)
|
|
52
|
+
except json.JSONDecodeError as e:
|
|
53
|
+
raise ValueError(f"Invalid JSON in config.json: {e}")
|
|
54
|
+
|
|
55
|
+
# 处理 workspace 路径
|
|
37
56
|
workspace = self.config.get('workspace', './workspace')
|
|
38
|
-
workspace
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
if workspace.startswith('./'):
|
|
58
|
+
workspace = workspace[2:]
|
|
59
|
+
workspace = os.path.normpath(os.path.join(PLUGIN_DIR, workspace))
|
|
60
|
+
|
|
61
|
+
watch_files = self.config.get('watch_files', [])
|
|
62
|
+
|
|
42
63
|
self.config['workspace'] = workspace
|
|
43
64
|
self.config['watch_files'] = watch_files
|
|
44
|
-
|
|
65
|
+
|
|
45
66
|
print(f"Config loaded:")
|
|
46
67
|
print(f" Cloud URL: {self.config.get('cloud_url')}")
|
|
47
68
|
print(f" Workspace: {workspace}")
|
|
48
69
|
print(f" Watch files: {watch_files}")
|
|
49
|
-
|
|
70
|
+
|
|
50
71
|
def initialize(self):
|
|
51
72
|
"""初始化组件"""
|
|
52
73
|
print("\n=== Initializing SoulSync Plugin ===\n")
|
|
53
|
-
|
|
74
|
+
|
|
54
75
|
self.client = OpenClawClient(self.config)
|
|
55
|
-
|
|
76
|
+
|
|
77
|
+
token = self.client._load_token()
|
|
78
|
+
if token:
|
|
79
|
+
try:
|
|
80
|
+
profile = self.client.get_profile()
|
|
81
|
+
print(f"Using existing token, user: {profile.get('email', 'unknown')}")
|
|
82
|
+
except Exception as e:
|
|
83
|
+
print(f"Token invalid, please login again")
|
|
84
|
+
token = None
|
|
85
|
+
|
|
86
|
+
if not token:
|
|
87
|
+
print("\n=== First run - Please login or register / 首次运行,请先登录或注册 ===\n")
|
|
88
|
+
print("1. Login / 登录(已有账号)")
|
|
89
|
+
print("2. Register / 注册(新用户)")
|
|
90
|
+
|
|
91
|
+
choice = input("Choose (1/2): ").strip()
|
|
92
|
+
|
|
93
|
+
if choice == '1':
|
|
94
|
+
login = Login(self.client)
|
|
95
|
+
result = login.run()
|
|
96
|
+
elif choice == '2':
|
|
97
|
+
register = Register(self.client)
|
|
98
|
+
result = register.run()
|
|
99
|
+
else:
|
|
100
|
+
print("Invalid choice / 无效选择")
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
56
103
|
email = self.config.get('email')
|
|
57
104
|
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
|
-
|
|
105
|
+
|
|
63
106
|
try:
|
|
64
|
-
self.client.
|
|
107
|
+
profile = self.client.get_profile()
|
|
108
|
+
print(f"\nLogged in as: {profile.get('email')}")
|
|
109
|
+
subscription = profile.get('subscription', {})
|
|
110
|
+
print(f"Subscription: {subscription.get('status')} (days remaining: {subscription.get('daysRemaining', 0)})\n")
|
|
65
111
|
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')
|
|
112
|
+
print(f"Warning: Could not get profile: {e}")
|
|
113
|
+
|
|
114
|
+
# 版本管理器
|
|
115
|
+
versions_file = os.path.normpath(os.path.join(PLUGIN_DIR, 'versions.json'))
|
|
76
116
|
self.version_manager = VersionManager(versions_file)
|
|
77
|
-
|
|
117
|
+
|
|
78
118
|
self.profiles_client = ProfilesClient(
|
|
79
119
|
self.config.get('cloud_url'),
|
|
80
120
|
self.client.token
|
|
81
121
|
)
|
|
82
|
-
|
|
122
|
+
|
|
83
123
|
self.profile_sync = ProfileSync(
|
|
84
124
|
self.profiles_client,
|
|
85
125
|
self.version_manager,
|
|
86
126
|
self.config.get('workspace')
|
|
87
127
|
)
|
|
88
|
-
|
|
128
|
+
|
|
89
129
|
print("Pulling all profiles from cloud...")
|
|
90
|
-
|
|
91
|
-
|
|
130
|
+
try:
|
|
131
|
+
self.profile_sync.pull_all()
|
|
132
|
+
except Exception as e:
|
|
133
|
+
print(f"Warning: Could not pull profiles: {e}")
|
|
134
|
+
|
|
92
135
|
print("\nStarting file watcher...")
|
|
93
136
|
watch_files = self.config.get('watch_files', [])
|
|
94
137
|
self.watcher = OpenClawMultiWatcher(
|
|
@@ -97,32 +140,35 @@ class SoulSyncPlugin:
|
|
|
97
140
|
self.on_file_change
|
|
98
141
|
)
|
|
99
142
|
self.watcher.start()
|
|
100
|
-
|
|
143
|
+
|
|
101
144
|
print("\nConnecting to WebSocket...")
|
|
102
|
-
|
|
103
|
-
|
|
145
|
+
try:
|
|
146
|
+
self.client.connect_websocket(self.on_websocket_message)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
print(f"Warning: Could not connect WebSocket: {e}")
|
|
149
|
+
|
|
104
150
|
self.running = True
|
|
105
|
-
|
|
151
|
+
|
|
106
152
|
def on_file_change(self, event_type: str, relative_path: str, absolute_path: str = None):
|
|
107
153
|
"""文件变化回调"""
|
|
108
154
|
print(f"\n[File {event_type}] {relative_path}")
|
|
109
|
-
|
|
155
|
+
|
|
110
156
|
if event_type in ['modified', 'created']:
|
|
111
157
|
time.sleep(0.5)
|
|
112
|
-
|
|
158
|
+
|
|
113
159
|
try:
|
|
114
160
|
self.profile_sync.push_file(relative_path)
|
|
115
161
|
print(f"Upload completed: {relative_path}")
|
|
116
162
|
except Exception as e:
|
|
117
163
|
print(f"Upload error: {e}")
|
|
118
|
-
|
|
164
|
+
|
|
119
165
|
elif event_type == 'deleted':
|
|
120
166
|
print(f"File deleted (not synced to cloud): {relative_path}")
|
|
121
|
-
|
|
167
|
+
|
|
122
168
|
def on_websocket_message(self, data: dict):
|
|
123
169
|
"""WebSocket 消息回调"""
|
|
124
170
|
event = data.get('event')
|
|
125
|
-
|
|
171
|
+
|
|
126
172
|
if event == 'file_updated':
|
|
127
173
|
file_path = data.get('file_path')
|
|
128
174
|
version = data.get('version')
|
|
@@ -131,7 +177,7 @@ class SoulSyncPlugin:
|
|
|
131
177
|
self.profile_sync.on_remote_change(file_path, version)
|
|
132
178
|
except Exception as e:
|
|
133
179
|
print(f"Sync error: {e}")
|
|
134
|
-
|
|
180
|
+
|
|
135
181
|
elif event == 'new_memory':
|
|
136
182
|
print(f"\n[WebSocket] New memory available!")
|
|
137
183
|
try:
|
|
@@ -139,60 +185,61 @@ class SoulSyncPlugin:
|
|
|
139
185
|
print("Memory synced from remote")
|
|
140
186
|
except Exception as e:
|
|
141
187
|
print(f"Sync error: {e}")
|
|
142
|
-
|
|
188
|
+
|
|
143
189
|
elif data.get('type') == 'authenticated':
|
|
144
190
|
print(f"[WebSocket] Authenticated, socket_id: {data.get('socket_id')}")
|
|
145
191
|
elif data.get('type') == 'error':
|
|
146
192
|
print(f"[WebSocket] Error: {data.get('message')}")
|
|
147
|
-
|
|
193
|
+
|
|
148
194
|
def run(self):
|
|
149
195
|
"""运行插件"""
|
|
150
196
|
print("\n" + "=" * 50)
|
|
151
197
|
print("SoulSync OpenClaw Plugin (Multi-File Sync)")
|
|
152
198
|
print("=" * 50 + "\n")
|
|
153
|
-
|
|
199
|
+
|
|
154
200
|
try:
|
|
155
201
|
self.load_config()
|
|
156
202
|
self.initialize()
|
|
157
|
-
|
|
203
|
+
|
|
158
204
|
print("\n=== Plugin Running ===")
|
|
159
205
|
print("Press Ctrl+C to stop\n")
|
|
160
|
-
|
|
206
|
+
|
|
161
207
|
while self.running:
|
|
162
208
|
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
|
-
|
|
209
|
+
|
|
172
210
|
except KeyboardInterrupt:
|
|
173
211
|
print("\n\nShutting down...")
|
|
212
|
+
self.shutdown()
|
|
174
213
|
except Exception as e:
|
|
175
|
-
print(f"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
214
|
+
print(f"\nError: {e}")
|
|
215
|
+
import traceback
|
|
216
|
+
traceback.print_exc()
|
|
217
|
+
self.shutdown()
|
|
218
|
+
raise
|
|
219
|
+
|
|
220
|
+
def shutdown(self):
|
|
221
|
+
"""关闭插件"""
|
|
222
|
+
print("Shutting down SoulSync plugin...")
|
|
181
223
|
self.running = False
|
|
182
|
-
|
|
224
|
+
|
|
183
225
|
if self.watcher:
|
|
184
|
-
|
|
185
|
-
|
|
226
|
+
try:
|
|
227
|
+
self.watcher.stop()
|
|
228
|
+
print("File watcher stopped")
|
|
229
|
+
except Exception as e:
|
|
230
|
+
print(f"Error stopping watcher: {e}")
|
|
231
|
+
|
|
186
232
|
if self.client:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
233
|
+
try:
|
|
234
|
+
self.client.close()
|
|
235
|
+
print("Client connection closed")
|
|
236
|
+
except Exception as e:
|
|
237
|
+
print(f"Error closing client: {e}")
|
|
238
|
+
|
|
239
|
+
print("Plugin shutdown complete")
|
|
192
240
|
def main():
|
|
241
|
+
"""主函数"""
|
|
193
242
|
plugin = SoulSyncPlugin()
|
|
194
243
|
plugin.run()
|
|
195
|
-
|
|
196
|
-
|
|
197
244
|
if __name__ == '__main__':
|
|
198
245
|
main()
|