soulsync 1.0.12 → 1.0.14
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/SSL_FIX_ANALYSIS.md +84 -0
- package/SSL_FIX_REPORT.md +90 -0
- package/config.json.example +2 -8
- package/index.js +493 -52
- package/openclaw.plugin.json +4 -4
- package/package.json +4 -1
- package/src/client.py +23 -38
- package/src/config.js +100 -0
- package/src/device-code.js +150 -0
- package/src/main.py +21 -49
- package/src/oauth-server.js +48 -0
- package/src/profiles.py +27 -49
- package/src/sync.py +67 -60
- package/test_ssl_fix.py +58 -0
- package/src/interactive_auth.py +0 -283
- package/src/main_fixed.py +0 -434
- package/src/register.py +0 -131
package/src/client.py
CHANGED
|
@@ -37,7 +37,9 @@ class OpenClawClient:
|
|
|
37
37
|
self.session.mount('https://', TLSAdapter())
|
|
38
38
|
|
|
39
39
|
def _load_or_generate_device_id(self) -> str:
|
|
40
|
-
|
|
40
|
+
if self.config.get('device_id'):
|
|
41
|
+
return self.config['device_id']
|
|
42
|
+
|
|
41
43
|
plugin_dir = os.path.dirname(os.path.dirname(__file__))
|
|
42
44
|
device_id_file = os.path.join(plugin_dir, 'device_id')
|
|
43
45
|
|
|
@@ -67,13 +69,17 @@ class OpenClawClient:
|
|
|
67
69
|
with open(token_file, 'r') as f:
|
|
68
70
|
return f.read().strip()
|
|
69
71
|
|
|
72
|
+
if self.config and self.config.get('token'):
|
|
73
|
+
return self.config.get('token')
|
|
74
|
+
|
|
70
75
|
return None
|
|
71
76
|
|
|
72
77
|
def _get_headers(self) -> dict:
|
|
73
|
-
"""获取请求头"""
|
|
74
78
|
headers = {'Content-Type': 'application/json'}
|
|
75
79
|
if self.token:
|
|
76
80
|
headers['Authorization'] = f'Bearer {self.token}'
|
|
81
|
+
if self.device_id:
|
|
82
|
+
headers['X-Device-ID'] = self.device_id
|
|
77
83
|
return headers
|
|
78
84
|
|
|
79
85
|
def authenticate(self, email: str = None, password: str = None) -> dict:
|
|
@@ -132,54 +138,29 @@ class OpenClawClient:
|
|
|
132
138
|
error = response.json().get('error', 'Unknown error')
|
|
133
139
|
raise Exception(f"Authentication failed: {error}")
|
|
134
140
|
|
|
135
|
-
def
|
|
136
|
-
"""
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
content: 记忆内容
|
|
140
|
-
|
|
141
|
-
Returns:
|
|
142
|
-
上传结果
|
|
143
|
-
"""
|
|
144
|
-
url = f"{self.cloud_url}/api/memories"
|
|
145
|
-
data = {'content': content}
|
|
146
|
-
|
|
147
|
-
response = self.session.post(url, json=data, headers=self._get_headers())
|
|
148
|
-
|
|
149
|
-
if response.status_code == 200:
|
|
150
|
-
return response.json()
|
|
151
|
-
elif response.status_code == 403:
|
|
152
|
-
error = response.json().get('error', 'Subscription required')
|
|
153
|
-
raise Exception(f"Upload failed: {error}")
|
|
154
|
-
else:
|
|
155
|
-
error = response.json().get('error', 'Unknown error')
|
|
156
|
-
raise Exception(f"Upload failed: {error}")
|
|
157
|
-
|
|
158
|
-
def download_memory(self) -> dict:
|
|
159
|
-
"""下载记忆
|
|
141
|
+
def get_profile(self) -> dict:
|
|
142
|
+
"""获取用户信息
|
|
160
143
|
|
|
161
144
|
Returns:
|
|
162
|
-
|
|
145
|
+
用户信息
|
|
163
146
|
"""
|
|
164
|
-
url = f"{self.cloud_url}/api/
|
|
147
|
+
url = f"{self.cloud_url}/api/profiles"
|
|
165
148
|
|
|
166
149
|
response = self.session.get(url, headers=self._get_headers())
|
|
167
150
|
|
|
168
151
|
if response.status_code == 200:
|
|
169
152
|
return response.json()
|
|
170
|
-
elif response.status_code == 404:
|
|
171
|
-
return {'content': '', 'version': 0}
|
|
172
153
|
else:
|
|
173
154
|
error = response.json().get('error', 'Unknown error')
|
|
174
|
-
raise Exception(f"
|
|
175
|
-
|
|
176
|
-
def
|
|
177
|
-
"""
|
|
155
|
+
raise Exception(f"Get profile failed: {error}")
|
|
156
|
+
|
|
157
|
+
def get_user_info(self) -> dict:
|
|
158
|
+
"""获取当前登录用户信息
|
|
178
159
|
|
|
179
160
|
Returns:
|
|
180
|
-
|
|
161
|
+
用户信息,包含 email, name, subscription_status
|
|
181
162
|
"""
|
|
182
|
-
url = f"{self.cloud_url}/api/
|
|
163
|
+
url = f"{self.cloud_url}/api/auth/user/info"
|
|
183
164
|
|
|
184
165
|
response = self.session.get(url, headers=self._get_headers())
|
|
185
166
|
|
|
@@ -187,7 +168,7 @@ class OpenClawClient:
|
|
|
187
168
|
return response.json()
|
|
188
169
|
else:
|
|
189
170
|
error = response.json().get('error', 'Unknown error')
|
|
190
|
-
raise Exception(f"Get
|
|
171
|
+
raise Exception(f"Get user info failed: {error}")
|
|
191
172
|
|
|
192
173
|
def connect_websocket(self, on_message_callback):
|
|
193
174
|
"""连接 WebSocket
|
|
@@ -284,6 +265,8 @@ class OpenClawClient:
|
|
|
284
265
|
self.token = result.get('token')
|
|
285
266
|
self.user_id = result.get('user_id')
|
|
286
267
|
self._save_token(self.token)
|
|
268
|
+
if self.config:
|
|
269
|
+
self.config['token'] = self.token
|
|
287
270
|
return result
|
|
288
271
|
else:
|
|
289
272
|
error = response.json().get('error', 'Unknown error')
|
|
@@ -309,6 +292,8 @@ class OpenClawClient:
|
|
|
309
292
|
self.token = result.get('token')
|
|
310
293
|
self.user_id = result.get('user_id')
|
|
311
294
|
self._save_token(self.token)
|
|
295
|
+
if self.config:
|
|
296
|
+
self.config['token'] = self.token
|
|
312
297
|
return result
|
|
313
298
|
else:
|
|
314
299
|
error = response.json().get('error', 'Unknown error')
|
package/src/config.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
let config = null;
|
|
6
|
+
|
|
7
|
+
function getPluginDir() {
|
|
8
|
+
return path.dirname(__filename);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function loadConfig() {
|
|
12
|
+
if (config) return config;
|
|
13
|
+
|
|
14
|
+
const pluginDir = path.dirname(__filename);
|
|
15
|
+
const configPath = path.join(pluginDir, 'config.json');
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(configPath)) {
|
|
19
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
20
|
+
return config;
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.error('[SoulSync] Error loading config:', e);
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function saveConfig(newConfig) {
|
|
29
|
+
const pluginDir = path.dirname(__filename);
|
|
30
|
+
const configPath = path.join(pluginDir, 'config.json');
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
let existing = {};
|
|
34
|
+
if (fs.existsSync(configPath)) {
|
|
35
|
+
existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
36
|
+
}
|
|
37
|
+
config = { ...existing, ...newConfig };
|
|
38
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
39
|
+
return true;
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('[SoulSync] Error saving config:', e);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function clearConfig() {
|
|
47
|
+
const pluginDir = path.dirname(__filename);
|
|
48
|
+
const configPath = path.join(pluginDir, 'config.json');
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(configPath)) {
|
|
52
|
+
const existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
53
|
+
delete existing.token;
|
|
54
|
+
delete existing.email;
|
|
55
|
+
delete existing.device_id;
|
|
56
|
+
delete existing.device_name;
|
|
57
|
+
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2));
|
|
58
|
+
config = existing;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error('[SoulSync] Error clearing config:', e);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getDeviceName(deviceType = 'local') {
|
|
68
|
+
const hostname = os.hostname();
|
|
69
|
+
const platform = os.platform();
|
|
70
|
+
const username = os.userInfo().username;
|
|
71
|
+
|
|
72
|
+
let type = 'local';
|
|
73
|
+
if (deviceType === 'cloud') type = '云端';
|
|
74
|
+
else if (deviceType === 'ssh') type = 'SSH';
|
|
75
|
+
else {
|
|
76
|
+
if (platform === 'win32') type = 'Windows';
|
|
77
|
+
else if (platform === 'darwin') type = 'Mac';
|
|
78
|
+
else if (platform === 'linux') type = 'Linux';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (deviceType === 'cloud') {
|
|
82
|
+
return `${type} Bot`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return `${username}的${type}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isAuthenticated() {
|
|
89
|
+
const cfg = loadConfig();
|
|
90
|
+
return cfg && cfg.token && cfg.email && cfg.email !== 'your-email@example.com';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
loadConfig,
|
|
95
|
+
saveConfig,
|
|
96
|
+
clearConfig,
|
|
97
|
+
getDeviceName,
|
|
98
|
+
isAuthenticated,
|
|
99
|
+
getPluginDir
|
|
100
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
let pollingInterval = null;
|
|
8
|
+
|
|
9
|
+
function getCloudUrl() {
|
|
10
|
+
const pluginDir = path.dirname(__filename);
|
|
11
|
+
const configPath = path.join(pluginDir, 'config.json');
|
|
12
|
+
try {
|
|
13
|
+
if (fs.existsSync(configPath)) {
|
|
14
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
15
|
+
if (cfg.cloud_url) return cfg.cloud_url;
|
|
16
|
+
}
|
|
17
|
+
} catch (e) {}
|
|
18
|
+
return 'https://soulsync.work';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeRequest(method, urlPath, body = null) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const cloudUrl = getCloudUrl();
|
|
24
|
+
const isHttps = cloudUrl.startsWith('https');
|
|
25
|
+
const client = isHttps ? https : http;
|
|
26
|
+
|
|
27
|
+
const urlObj = new URL(cloudUrl + urlPath);
|
|
28
|
+
|
|
29
|
+
const options = {
|
|
30
|
+
hostname: urlObj.hostname,
|
|
31
|
+
port: urlObj.port || (isHttps ? 443 : 80),
|
|
32
|
+
path: urlObj.pathname + urlObj.search,
|
|
33
|
+
method: method,
|
|
34
|
+
headers: { 'Content-Type': 'application/json' }
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const req = client.request(options, (res) => {
|
|
38
|
+
let data = '';
|
|
39
|
+
res.on('data', chunk => data += chunk);
|
|
40
|
+
res.on('end', () => {
|
|
41
|
+
try {
|
|
42
|
+
const json = JSON.parse(data);
|
|
43
|
+
resolve({ status: res.statusCode, body: json });
|
|
44
|
+
} catch (e) {
|
|
45
|
+
resolve({ status: res.statusCode, body: data });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
req.on('error', reject);
|
|
51
|
+
if (body) req.write(JSON.stringify(body));
|
|
52
|
+
req.end();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function requestDeviceCode() {
|
|
57
|
+
return makeRequest('POST', '/api/auth/device-code');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function pollForAuth(deviceCode) {
|
|
61
|
+
return makeRequest('GET', `/api/auth/device/${deviceCode}/status`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function startPolling(deviceCode, onSuccess, onError) {
|
|
65
|
+
pollingInterval = setInterval(async () => {
|
|
66
|
+
try {
|
|
67
|
+
const result = await pollForAuth(deviceCode);
|
|
68
|
+
|
|
69
|
+
if (result.body.status === 'authorized' && result.body.token) {
|
|
70
|
+
stopPolling();
|
|
71
|
+
onSuccess(result.body.token);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (result.body.status === 'expired') {
|
|
76
|
+
stopPolling();
|
|
77
|
+
onError(new Error('Device code expired'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (result.body.status === 'not_found') {
|
|
82
|
+
stopPolling();
|
|
83
|
+
onError(new Error('Device code not found'));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
stopPolling();
|
|
88
|
+
onError(e);
|
|
89
|
+
}
|
|
90
|
+
}, 3000);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stopPolling() {
|
|
94
|
+
if (pollingInterval) {
|
|
95
|
+
clearInterval(pollingInterval);
|
|
96
|
+
pollingInterval = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function savePendingAuth(deviceCode, authUrl) {
|
|
101
|
+
const homeDir = os.homedir();
|
|
102
|
+
const pendingDir = path.join(homeDir, '.soulsync');
|
|
103
|
+
const pendingFile = path.join(pendingDir, '.pending_auth');
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
if (!fs.existsSync(pendingDir)) {
|
|
107
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
fs.writeFileSync(pendingFile, JSON.stringify({ deviceCode, authUrl, timestamp: Date.now() }));
|
|
110
|
+
return true;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.error('[SoulSync] Error saving pending auth:', e);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function loadPendingAuth() {
|
|
118
|
+
const homeDir = os.homedir();
|
|
119
|
+
const pendingFile = path.join(homeDir, '.soulsync', '.pending_auth');
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (fs.existsSync(pendingFile)) {
|
|
123
|
+
const data = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
|
|
124
|
+
if (Date.now() - data.timestamp < 900000) {
|
|
125
|
+
return data;
|
|
126
|
+
}
|
|
127
|
+
fs.unlinkSync(pendingFile);
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function clearPendingAuth() {
|
|
134
|
+
const homeDir = os.homedir();
|
|
135
|
+
const pendingFile = path.join(homeDir, '.soulsync', '.pending_auth');
|
|
136
|
+
try {
|
|
137
|
+
if (fs.existsSync(pendingFile)) {
|
|
138
|
+
fs.unlinkSync(pendingFile);
|
|
139
|
+
}
|
|
140
|
+
} catch (e) {}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
requestDeviceCode,
|
|
145
|
+
startPolling,
|
|
146
|
+
stopPolling,
|
|
147
|
+
savePendingAuth,
|
|
148
|
+
loadPendingAuth,
|
|
149
|
+
clearPendingAuth
|
|
150
|
+
};
|
package/src/main.py
CHANGED
|
@@ -61,7 +61,7 @@ class SoulSyncPlugin:
|
|
|
61
61
|
|
|
62
62
|
cloud_url = self.config.get('cloud_url', '').strip()
|
|
63
63
|
email = self.config.get('email', '').strip()
|
|
64
|
-
|
|
64
|
+
token = self.config.get('token', '').strip()
|
|
65
65
|
|
|
66
66
|
if not cloud_url:
|
|
67
67
|
self.config['cloud_url'] = 'https://soulsync.work'
|
|
@@ -98,11 +98,6 @@ class SoulSyncPlugin:
|
|
|
98
98
|
elif 'email' in auth_result:
|
|
99
99
|
config['email'] = auth_result.get('email', '')
|
|
100
100
|
|
|
101
|
-
# 保存 password(注册时输入的密码,需要保存以便自动登录)
|
|
102
|
-
# 注意:登录时我们只有从 config 读取的 password
|
|
103
|
-
if 'password' in auth_result:
|
|
104
|
-
config['password'] = auth_result.get('password', '')
|
|
105
|
-
|
|
106
101
|
# 保存 token
|
|
107
102
|
if 'token' in auth_result:
|
|
108
103
|
config['token'] = auth_result['token']
|
|
@@ -290,47 +285,32 @@ class SoulSyncPlugin:
|
|
|
290
285
|
profile = self.client.get_profile()
|
|
291
286
|
print(f"[SoulSync] Using existing token, user: {profile.get('email', 'unknown')}")
|
|
292
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
293
|
print(f"[SoulSync] Token invalid, re-authenticating: {e}")
|
|
294
294
|
token = None
|
|
295
295
|
|
|
296
296
|
if not token:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
try:
|
|
302
|
-
result = self.client.authenticate(email, password)
|
|
303
|
-
if result:
|
|
304
|
-
result['email'] = email
|
|
305
|
-
result['password'] = password
|
|
306
|
-
print("[SoulSync] Login successful! / 登录成功!")
|
|
307
|
-
self._save_auth_to_config(result)
|
|
308
|
-
token = self.client.token
|
|
309
|
-
except Exception as e:
|
|
310
|
-
error_msg = str(e)
|
|
311
|
-
|
|
312
|
-
if "429" in error_msg or "too many" in error_msg.lower():
|
|
313
|
-
print("\n[SoulSync] ========================================")
|
|
314
|
-
print(f"[SoulSync] ❌ {e}")
|
|
315
|
-
print("[SoulSync] Too many failed attempts. Please try again later / 登录失败次数过多,请稍后再试")
|
|
316
|
-
print("[SoulSync] ========================================\n")
|
|
317
|
-
else:
|
|
318
|
-
print("\n[SoulSync] ========================================")
|
|
319
|
-
print(f"[SoulSync] ❌ {e}")
|
|
320
|
-
print("[SoulSync] Please run 'openclaw soulsync:setup' to reconfigure / 请运行 'openclaw soulsync:setup' 重新配置")
|
|
321
|
-
print("[SoulSync] ========================================\n")
|
|
322
|
-
|
|
323
|
-
sys.exit(0)
|
|
324
|
-
|
|
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
|
+
|
|
325
301
|
email = self.config.get('email')
|
|
326
|
-
|
|
327
|
-
|
|
302
|
+
|
|
328
303
|
try:
|
|
329
304
|
profile = self.client.get_profile()
|
|
330
305
|
print(f"\n[SoulSync] Logged in as: {profile.get('email')}")
|
|
331
306
|
subscription = profile.get('subscription', {})
|
|
332
307
|
print(f"[SoulSync] Subscription: {subscription.get('status')} (days remaining: {subscription.get('daysRemaining', 0)})\n")
|
|
333
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
|
|
334
314
|
print(f"[SoulSync] Warning: Could not get profile: {e}")
|
|
335
315
|
|
|
336
316
|
# 版本管理器
|
|
@@ -391,20 +371,12 @@ class SoulSyncPlugin:
|
|
|
391
371
|
"""WebSocket 消息回调"""
|
|
392
372
|
event = data.get('event')
|
|
393
373
|
|
|
394
|
-
if event == '
|
|
395
|
-
|
|
374
|
+
if event == 'profile:updated':
|
|
375
|
+
user_id = data.get('user_id')
|
|
396
376
|
version = data.get('version')
|
|
397
|
-
print(f"\n[SoulSync] [WebSocket]
|
|
398
|
-
try:
|
|
399
|
-
self.profile_sync.on_remote_change(file_path, version)
|
|
400
|
-
except Exception as e:
|
|
401
|
-
print(f"[SoulSync] Sync error: {e}")
|
|
402
|
-
|
|
403
|
-
elif event == 'new_memory':
|
|
404
|
-
print(f"\n[SoulSync] [WebSocket] New memory available!")
|
|
377
|
+
print(f"\n[SoulSync] [WebSocket] Profile updated (v{version})")
|
|
405
378
|
try:
|
|
406
379
|
self.profile_sync.pull_all()
|
|
407
|
-
print("[SoulSync] Memory synced from remote")
|
|
408
380
|
except Exception as e:
|
|
409
381
|
print(f"[SoulSync] Sync error: {e}")
|
|
410
382
|
|
|
@@ -489,9 +461,9 @@ def main():
|
|
|
489
461
|
plugin.load_config()
|
|
490
462
|
|
|
491
463
|
email = plugin.config.get('email', '').strip()
|
|
492
|
-
|
|
464
|
+
token = plugin.config.get('token', '').strip()
|
|
493
465
|
|
|
494
|
-
if not email or not
|
|
466
|
+
if not email or not token:
|
|
495
467
|
print("\n[SoulSync] ========================================")
|
|
496
468
|
print("[SoulSync] Not configured. Run 'openclaw soulsync:setup' first.")
|
|
497
469
|
print("[SoulSync] 尚未配置,请先运行 'openclaw soulsync:setup'")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const url = require('url');
|
|
3
|
+
|
|
4
|
+
function createOAuthServer() {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const server = http.createServer((req, res) => {
|
|
7
|
+
const parsedUrl = url.parse(req.url, true);
|
|
8
|
+
|
|
9
|
+
if (parsedUrl.pathname === '/callback') {
|
|
10
|
+
const { token, state, error } = parsedUrl.query;
|
|
11
|
+
|
|
12
|
+
if (error) {
|
|
13
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
14
|
+
res.end('<html><body><h1>授权失败</h1><p>请关闭此窗口并重试。</p></body></html>');
|
|
15
|
+
server.close();
|
|
16
|
+
reject(new Error(error));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (token) {
|
|
21
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
22
|
+
res.end('<html><body><h1>授权成功!</h1><p>请关闭此窗口并返回终端。</p></body></html>');
|
|
23
|
+
server.close();
|
|
24
|
+
resolve({ token, state });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
29
|
+
res.end('<html><body><h1>无效响应</h1></body></html>');
|
|
30
|
+
server.close();
|
|
31
|
+
reject(new Error('No token received'));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
36
|
+
res.end('Not Found');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
server.listen(0, () => {
|
|
40
|
+
const port = server.address().port;
|
|
41
|
+
resolve({ server, port });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
server.on('error', reject);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { createOAuthServer };
|
package/src/profiles.py
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
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
|
|
2
7
|
|
|
3
8
|
|
|
4
9
|
class ProfilesClient:
|
|
5
|
-
"""Profiles API 客户端"""
|
|
10
|
+
"""Profiles API 客户端 - 统一同步接口"""
|
|
6
11
|
|
|
7
12
|
def __init__(self, cloud_url: str, token: str = None):
|
|
8
13
|
self.cloud_url = cloud_url.rstrip('/')
|
|
9
14
|
self.token = token
|
|
15
|
+
self.session = requests.Session()
|
|
16
|
+
self.session.mount('https://', TLSAdapter())
|
|
10
17
|
|
|
11
18
|
def _get_headers(self) -> dict:
|
|
12
19
|
"""获取请求头"""
|
|
@@ -19,57 +26,50 @@ class ProfilesClient:
|
|
|
19
26
|
"""设置 token"""
|
|
20
27
|
self.token = token
|
|
21
28
|
|
|
22
|
-
def get_profiles(self
|
|
23
|
-
"""
|
|
29
|
+
def get_profiles(self) -> dict:
|
|
30
|
+
"""获取用户的完整 profiles
|
|
24
31
|
|
|
25
|
-
Args:
|
|
26
|
-
path: 可选的文件路径
|
|
27
|
-
|
|
28
32
|
Returns:
|
|
29
|
-
包含
|
|
33
|
+
包含 content (dict), version, updated_at 的字典
|
|
34
|
+
示例: {"content": {"SOUL.md": "...", "USER.md": "..."}, "version": 3, "updated_at": "..."}
|
|
30
35
|
"""
|
|
31
36
|
url = f"{self.cloud_url}/api/profiles"
|
|
32
|
-
if path:
|
|
33
|
-
url += f"?path={path}"
|
|
34
37
|
|
|
35
|
-
response =
|
|
38
|
+
response = self.session.get(url, headers=self._get_headers())
|
|
36
39
|
|
|
37
40
|
if response.status_code == 200:
|
|
38
41
|
return response.json()
|
|
39
|
-
elif response.status_code == 404:
|
|
40
|
-
return {'files': []}
|
|
41
42
|
else:
|
|
42
43
|
error = response.json().get('error', 'Unknown error')
|
|
43
44
|
raise Exception(f"Get profiles failed: {error}")
|
|
44
45
|
|
|
45
|
-
def
|
|
46
|
-
"""
|
|
46
|
+
def upload_profiles(self, content: dict, version: int) -> dict:
|
|
47
|
+
"""整体替换用户的 profiles
|
|
47
48
|
|
|
48
49
|
Args:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
version:
|
|
50
|
+
content: 包含所有文件的 dict,key 是文件名,value 是内容
|
|
51
|
+
示例: {"SOUL.md": "...", "USER.md": "...", "MEMORY.md": "..."}
|
|
52
|
+
version: 客户端当前持有的版本号
|
|
52
53
|
|
|
53
54
|
Returns:
|
|
54
|
-
成功时返回 {
|
|
55
|
-
|
|
55
|
+
成功时返回 {"content": {...}, "version": N, "updated_at": "..."}
|
|
56
|
+
冲突时抛出 ConflictError
|
|
56
57
|
"""
|
|
57
58
|
url = f"{self.cloud_url}/api/profiles"
|
|
58
59
|
data = {
|
|
59
|
-
'file_path': file_path,
|
|
60
60
|
'content': content,
|
|
61
61
|
'version': version
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
response =
|
|
64
|
+
response = self.session.put(url, json=data, headers=self._get_headers())
|
|
65
65
|
|
|
66
66
|
if response.status_code == 200:
|
|
67
67
|
return response.json()
|
|
68
68
|
elif response.status_code == 409:
|
|
69
69
|
result = response.json()
|
|
70
70
|
raise ConflictError(
|
|
71
|
-
result.get('
|
|
72
|
-
result.get('
|
|
71
|
+
server_content=result.get('server_content', {}),
|
|
72
|
+
server_version=result.get('server_version', 0)
|
|
73
73
|
)
|
|
74
74
|
elif response.status_code == 403:
|
|
75
75
|
error = response.json().get('error', 'Subscription required')
|
|
@@ -77,34 +77,12 @@ class ProfilesClient:
|
|
|
77
77
|
else:
|
|
78
78
|
error = response.json().get('error', 'Unknown error')
|
|
79
79
|
raise Exception(f"Upload failed: {error}")
|
|
80
|
-
|
|
81
|
-
def sync_profiles(self, since: str = '0') -> dict:
|
|
82
|
-
"""增量同步 profiles
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
since: 时间戳
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
包含 files 列表和 server_time 的字典
|
|
89
|
-
"""
|
|
90
|
-
url = f"{self.cloud_url}/api/profiles/sync?since={since}"
|
|
91
|
-
|
|
92
|
-
response = requests.get(url, headers=self._get_headers())
|
|
93
|
-
|
|
94
|
-
if response.status_code == 200:
|
|
95
|
-
return response.json()
|
|
96
|
-
elif response.status_code == 403:
|
|
97
|
-
error = response.json().get('error', 'Subscription required')
|
|
98
|
-
raise Exception(f"Sync failed: {error}")
|
|
99
|
-
else:
|
|
100
|
-
error = response.json().get('error', 'Unknown error')
|
|
101
|
-
raise Exception(f"Sync failed: {error}")
|
|
102
80
|
|
|
103
81
|
|
|
104
82
|
class ConflictError(Exception):
|
|
105
83
|
"""版本冲突异常"""
|
|
106
84
|
|
|
107
|
-
def __init__(self,
|
|
108
|
-
self.
|
|
109
|
-
self.
|
|
110
|
-
super().__init__(f"Version conflict:
|
|
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}")
|