soulsync 1.0.11 → 1.0.13
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/config.json.example +2 -8
- package/index.js +421 -25
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/client.py +13 -46
- package/src/main.py +61 -73
- package/src/profiles.py +27 -49
- package/src/sync.py +67 -60
- package/src/interactive_auth.py +0 -283
- package/src/main_fixed.py +0 -434
- package/src/register.py +0 -131
package/config.json.example
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"cloud_url": "https://soulsync.work",
|
|
3
3
|
"email": "",
|
|
4
|
-
"
|
|
4
|
+
"token": "",
|
|
5
5
|
"workspace": "./workspace",
|
|
6
|
-
"memory_file": "MEMORY.md",
|
|
7
6
|
"watch_files": [
|
|
8
7
|
"SOUL.md",
|
|
9
|
-
"IDENTITY.md",
|
|
10
8
|
"USER.md",
|
|
11
|
-
"AGENTS.md",
|
|
12
|
-
"TOOLS.md",
|
|
13
|
-
"skills.json",
|
|
14
|
-
"memory/",
|
|
15
9
|
"MEMORY.md"
|
|
16
10
|
],
|
|
17
|
-
"_comment": "
|
|
11
|
+
"_comment": "Token is obtained after registration/login. Do not share your token."
|
|
18
12
|
}
|
package/index.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
const { spawn } = require('child_process');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const crypto = require('crypto');
|
|
4
7
|
|
|
5
8
|
let pythonProcess = null;
|
|
9
|
+
let config = null;
|
|
10
|
+
let deviceCodePolling = null;
|
|
6
11
|
|
|
7
12
|
function getPluginDir() {
|
|
8
13
|
return path.dirname(__filename);
|
|
9
14
|
}
|
|
10
15
|
|
|
16
|
+
function loadConfig(pluginDir) {
|
|
17
|
+
const configPath = path.join(pluginDir, 'config.json');
|
|
18
|
+
try {
|
|
19
|
+
if (fs.existsSync(configPath)) {
|
|
20
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.error('[SoulSync] Error loading config:', e);
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
function checkConfigExists(pluginDir) {
|
|
12
29
|
const configPath = path.join(pluginDir, 'config.json');
|
|
13
30
|
if (!fs.existsSync(configPath)) {
|
|
@@ -19,16 +36,80 @@ function checkConfigExists(pluginDir) {
|
|
|
19
36
|
return false;
|
|
20
37
|
}
|
|
21
38
|
|
|
39
|
+
config = loadConfig(pluginDir);
|
|
40
|
+
if (!config) return false;
|
|
41
|
+
|
|
42
|
+
const email = (config.email || '').trim();
|
|
43
|
+
const token = config.token;
|
|
44
|
+
return email && token && email !== 'your-email@example.com';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function saveConfig(pluginDir, newConfig) {
|
|
48
|
+
const configPath = path.join(pluginDir, 'config.json');
|
|
22
49
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
50
|
+
let existing = {};
|
|
51
|
+
if (fs.existsSync(configPath)) {
|
|
52
|
+
existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
53
|
+
}
|
|
54
|
+
const merged = { ...existing, ...newConfig };
|
|
55
|
+
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2));
|
|
56
|
+
config = merged;
|
|
57
|
+
return true;
|
|
27
58
|
} catch (e) {
|
|
59
|
+
console.error('[SoulSync] Error saving config:', e);
|
|
28
60
|
return false;
|
|
29
61
|
}
|
|
30
62
|
}
|
|
31
63
|
|
|
64
|
+
function getCloudUrl(pluginDir) {
|
|
65
|
+
const cfg = loadConfig(pluginDir);
|
|
66
|
+
return (cfg && cfg.cloud_url) || 'https://soulsync.work';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makeRequest(method, urlPath, body = null, token = null) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const cloudUrl = getCloudUrl(getPluginDir());
|
|
72
|
+
const isHttps = cloudUrl.startsWith('https');
|
|
73
|
+
const client = isHttps ? https : http;
|
|
74
|
+
|
|
75
|
+
const urlObj = new URL(cloudUrl + urlPath);
|
|
76
|
+
|
|
77
|
+
const options = {
|
|
78
|
+
hostname: urlObj.hostname,
|
|
79
|
+
port: urlObj.port || (isHttps ? 443 : 80),
|
|
80
|
+
path: urlObj.pathname + urlObj.search,
|
|
81
|
+
method: method,
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json'
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (token) {
|
|
88
|
+
options.headers['Authorization'] = `Bearer ${token}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const req = client.request(options, (res) => {
|
|
92
|
+
let data = '';
|
|
93
|
+
res.on('data', chunk => data += chunk);
|
|
94
|
+
res.on('end', () => {
|
|
95
|
+
try {
|
|
96
|
+
const json = JSON.parse(data);
|
|
97
|
+
resolve({ status: res.statusCode, body: json });
|
|
98
|
+
} catch (e) {
|
|
99
|
+
resolve({ status: res.statusCode, body: data });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
req.on('error', reject);
|
|
105
|
+
|
|
106
|
+
if (body) {
|
|
107
|
+
req.write(JSON.stringify(body));
|
|
108
|
+
}
|
|
109
|
+
req.end();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
32
113
|
function startPythonService(mode = '--start') {
|
|
33
114
|
const pluginDir = getPluginDir();
|
|
34
115
|
const pythonScript = path.join(pluginDir, 'src', 'main.py');
|
|
@@ -59,21 +140,334 @@ function startPythonService(mode = '--start') {
|
|
|
59
140
|
return pythonProcess;
|
|
60
141
|
}
|
|
61
142
|
|
|
143
|
+
function stopPythonService() {
|
|
144
|
+
if (pythonProcess) {
|
|
145
|
+
console.log('[SoulSync] Stopping Python service...');
|
|
146
|
+
pythonProcess.kill();
|
|
147
|
+
pythonProcess = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function checkConnection() {
|
|
152
|
+
const pluginDir = getPluginDir();
|
|
153
|
+
const cfg = loadConfig(pluginDir);
|
|
154
|
+
|
|
155
|
+
if (!cfg || !cfg.token) {
|
|
156
|
+
return { connected: false, reason: 'not_configured' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return makeRequest('GET', '/api/profiles', null, cfg.token)
|
|
160
|
+
.then(result => {
|
|
161
|
+
if (result.status === 200) {
|
|
162
|
+
return { connected: true, data: result.body };
|
|
163
|
+
} else if (result.status === 401) {
|
|
164
|
+
return { connected: false, reason: 'token_expired' };
|
|
165
|
+
} else {
|
|
166
|
+
return { connected: false, reason: 'server_error', status: result.status };
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
.catch(err => {
|
|
170
|
+
return { connected: false, reason: 'connection_error', error: err.message };
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function startDeviceCodeFlow() {
|
|
175
|
+
try {
|
|
176
|
+
const result = await makeRequest('POST', '/api/auth/device-code', {});
|
|
177
|
+
|
|
178
|
+
if (result.status !== 200 || !result.body.device_code) {
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
message: 'Failed to start device authorization. Please try again.'
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { device_code, auth_url, expires_in } = result.body;
|
|
186
|
+
|
|
187
|
+
deviceCodePolling = setInterval(async () => {
|
|
188
|
+
try {
|
|
189
|
+
const pollResult = await makeRequest('GET', `/api/auth/device-code/${device_code}/status`);
|
|
190
|
+
|
|
191
|
+
if (pollResult.body.status === 'authorized' && pollResult.body.token) {
|
|
192
|
+
clearInterval(deviceCodePolling);
|
|
193
|
+
deviceCodePolling = null;
|
|
194
|
+
|
|
195
|
+
const deviceId = crypto.randomUUID();
|
|
196
|
+
const pluginDir = getPluginDir();
|
|
197
|
+
saveConfig(pluginDir, {
|
|
198
|
+
email: 'device',
|
|
199
|
+
token: pollResult.body.token,
|
|
200
|
+
device_id: deviceId,
|
|
201
|
+
device_name: 'Device',
|
|
202
|
+
cloud_url: getCloudUrl(pluginDir)
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
makeRequest('POST', '/api/devices', {
|
|
206
|
+
device_id: deviceId,
|
|
207
|
+
device_name: 'Device',
|
|
208
|
+
device_type: 'local'
|
|
209
|
+
}, pollResult.body.token).catch(err => {
|
|
210
|
+
console.error('[SoulSync] Failed to register device:', err.message);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
startPythonService('--start');
|
|
214
|
+
|
|
215
|
+
console.log('[SoulSync] Authorization successful! Syncing started.');
|
|
216
|
+
} else if (pollResult.body.status === 'expired') {
|
|
217
|
+
clearInterval(deviceCodePolling);
|
|
218
|
+
deviceCodePolling = null;
|
|
219
|
+
console.log('[SoulSync] Authorization expired. Please try again.');
|
|
220
|
+
} else if (pollResult.body.status === 'not_found') {
|
|
221
|
+
clearInterval(deviceCodePolling);
|
|
222
|
+
deviceCodePolling = null;
|
|
223
|
+
console.log('[SoulSync] Device code not found. Please try again.');
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error('[SoulSync] Polling error:', e);
|
|
227
|
+
}
|
|
228
|
+
}, 3000);
|
|
229
|
+
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
if (deviceCodePolling) {
|
|
232
|
+
clearInterval(deviceCodePolling);
|
|
233
|
+
deviceCodePolling = null;
|
|
234
|
+
}
|
|
235
|
+
}, expires_in * 1000);
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
success: true,
|
|
239
|
+
message: `Please open ${auth_url} in your browser to complete authorization. This page will expire in ${Math.floor(expires_in / 60)} minutes.`
|
|
240
|
+
};
|
|
241
|
+
} catch (err) {
|
|
242
|
+
return {
|
|
243
|
+
success: false,
|
|
244
|
+
message: `Error: ${err.message}`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
62
249
|
module.exports = function register(api) {
|
|
63
250
|
console.log('[SoulSync] Registering plugin...');
|
|
64
251
|
|
|
65
|
-
api.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
252
|
+
api.registerTool({
|
|
253
|
+
name: 'soulsync_status',
|
|
254
|
+
description: 'Check SoulSync plugin status for OpenClaw (小龙虾). Call when user mentions SoulSync, soul sync, 灵魂同步, OpenClaw plugin, 小龙虾插件, or asks about sync status.',
|
|
255
|
+
input_schema: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
properties: {},
|
|
258
|
+
required: []
|
|
259
|
+
}
|
|
260
|
+
}, async () => {
|
|
261
|
+
const pluginDir = getPluginDir();
|
|
262
|
+
const cfg = loadConfig(pluginDir);
|
|
263
|
+
|
|
264
|
+
if (!cfg || !cfg.token) {
|
|
265
|
+
return {
|
|
266
|
+
configured: false,
|
|
267
|
+
message: 'SoulSync is not configured. Would you like to connect your SoulSync account?'
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const conn = await checkConnection();
|
|
272
|
+
|
|
273
|
+
if (conn.connected) {
|
|
274
|
+
return {
|
|
275
|
+
configured: true,
|
|
276
|
+
connected: true,
|
|
277
|
+
email: cfg.email,
|
|
278
|
+
message: `SoulSync is connected and syncing for ${cfg.email}.`
|
|
279
|
+
};
|
|
280
|
+
} else if (conn.reason === 'token_expired') {
|
|
281
|
+
return {
|
|
282
|
+
configured: true,
|
|
283
|
+
connected: false,
|
|
284
|
+
message: 'SoulSync token has expired. Would you like to reconnect your account?'
|
|
285
|
+
};
|
|
286
|
+
} else {
|
|
287
|
+
return {
|
|
288
|
+
configured: true,
|
|
289
|
+
connected: false,
|
|
290
|
+
message: `SoulSync is configured but cannot connect to server: ${conn.reason}`
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
api.registerTool({
|
|
296
|
+
name: 'soulsync_sync',
|
|
297
|
+
description: 'Manually trigger SoulSync OpenClaw plugin (小龙虾插件) to sync soul files (SOUL.md, USER.md, MEMORY.md) between local and cloud.',
|
|
298
|
+
input_schema: {
|
|
299
|
+
type: 'object',
|
|
300
|
+
properties: {},
|
|
301
|
+
required: []
|
|
302
|
+
}
|
|
303
|
+
}, async () => {
|
|
304
|
+
const pluginDir = getPluginDir();
|
|
305
|
+
const cfg = loadConfig(pluginDir);
|
|
306
|
+
|
|
307
|
+
if (!cfg || !cfg.token) {
|
|
308
|
+
return {
|
|
309
|
+
success: false,
|
|
310
|
+
message: 'SoulSync is not configured. Would you like to connect your SoulSync account?'
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const conn = await checkConnection();
|
|
315
|
+
|
|
316
|
+
if (!conn.connected) {
|
|
317
|
+
return {
|
|
318
|
+
success: false,
|
|
319
|
+
message: `Cannot sync: ${conn.reason}. Please check your connection.`
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (pythonProcess) {
|
|
324
|
+
return {
|
|
325
|
+
success: true,
|
|
326
|
+
message: 'Sync is already running in the background.'
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
startPythonService('--start');
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
success: true,
|
|
334
|
+
message: 'Sync started. Your soul files are being synchronized.'
|
|
335
|
+
};
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
api.registerTool({
|
|
339
|
+
name: 'soulsync_connect',
|
|
340
|
+
description: 'Connect SoulSync account using device authorization flow. User will be given a URL to open in browser to complete login/registration. Call when user wants to register, login, connect, or setup SoulSync.',
|
|
341
|
+
input_schema: {
|
|
342
|
+
type: 'object',
|
|
343
|
+
properties: {},
|
|
344
|
+
required: []
|
|
345
|
+
}
|
|
346
|
+
}, async () => {
|
|
347
|
+
if (deviceCodePolling) {
|
|
348
|
+
return {
|
|
349
|
+
success: false,
|
|
350
|
+
message: 'Authorization in progress. Please check your browser and complete the authorization.'
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return await startDeviceCodeFlow();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
api.registerTool({
|
|
358
|
+
name: 'soulsync_logout',
|
|
359
|
+
description: 'Disconnect SoulSync account and stop sync service. This will remove local authentication token.',
|
|
360
|
+
input_schema: {
|
|
361
|
+
type: 'object',
|
|
362
|
+
properties: {},
|
|
363
|
+
required: []
|
|
364
|
+
}
|
|
365
|
+
}, async () => {
|
|
366
|
+
stopPythonService();
|
|
367
|
+
|
|
368
|
+
const pluginDir = getPluginDir();
|
|
369
|
+
const configPath = path.join(pluginDir, 'config.json');
|
|
370
|
+
const cfg = loadConfig(pluginDir);
|
|
371
|
+
|
|
372
|
+
if (cfg && cfg.device_id && cfg.token) {
|
|
373
|
+
try {
|
|
374
|
+
await makeRequest('DELETE', `/api/devices/${cfg.device_id}`, null, cfg.token);
|
|
375
|
+
} catch (e) {
|
|
376
|
+
console.error('[SoulSync] Failed to delete device:', e.message);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
if (fs.existsSync(configPath)) {
|
|
382
|
+
const existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
383
|
+
delete existing.token;
|
|
384
|
+
delete existing.email;
|
|
385
|
+
delete existing.device_id;
|
|
386
|
+
delete existing.device_name;
|
|
387
|
+
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2));
|
|
388
|
+
}
|
|
389
|
+
} catch (e) {
|
|
390
|
+
console.error('[SoulSync] Error clearing config:', e);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (deviceCodePolling) {
|
|
394
|
+
clearInterval(deviceCodePolling);
|
|
395
|
+
deviceCodePolling = null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
success: true,
|
|
400
|
+
message: 'SoulSync disconnected. Your local data is preserved. To reconnect, say "connect SoulSync".'
|
|
401
|
+
};
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
api.registerTool({
|
|
405
|
+
name: 'soulsync_devices',
|
|
406
|
+
description: 'List all devices connected to your SoulSync account / 查看已连接设备列表',
|
|
407
|
+
schema: {
|
|
408
|
+
type: 'object',
|
|
409
|
+
properties: {},
|
|
410
|
+
required: []
|
|
74
411
|
},
|
|
75
|
-
|
|
76
|
-
|
|
412
|
+
async handler() {
|
|
413
|
+
const pluginDir = getPluginDir();
|
|
414
|
+
const cfg = loadConfig(pluginDir);
|
|
415
|
+
|
|
416
|
+
if (!cfg || !cfg.token) {
|
|
417
|
+
return 'Not logged in / 未登录';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
const result = await makeRequest('GET', '/api/devices', null, cfg.token);
|
|
422
|
+
if (result.status === 200) {
|
|
423
|
+
const devices = result.body.devices || [];
|
|
424
|
+
if (devices.length === 0) {
|
|
425
|
+
return 'No devices found / 未找到设备';
|
|
426
|
+
}
|
|
427
|
+
const list = devices.map(d =>
|
|
428
|
+
`- ${d.device_name || 'Unnamed'} (${d.device_type || 'local'}) - Last sync: ${d.last_sync_at || 'Never'}`
|
|
429
|
+
).join('\n');
|
|
430
|
+
return `Connected devices / 已连接设备:\n${list}`;
|
|
431
|
+
}
|
|
432
|
+
return 'Failed to get devices / 获取设备列表失败';
|
|
433
|
+
} catch (e) {
|
|
434
|
+
return `Error: ${e.message}`;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
api.registerTool({
|
|
440
|
+
name: 'soulsync_rename_device',
|
|
441
|
+
description: 'Rename the current device / 重命名当前设备',
|
|
442
|
+
schema: {
|
|
443
|
+
type: 'object',
|
|
444
|
+
properties: {
|
|
445
|
+
name: { type: 'string', description: 'New device name / 新设备名称' }
|
|
446
|
+
},
|
|
447
|
+
required: ['name']
|
|
448
|
+
},
|
|
449
|
+
async handler({ name }) {
|
|
450
|
+
const pluginDir = getPluginDir();
|
|
451
|
+
const cfg = loadConfig(pluginDir);
|
|
452
|
+
|
|
453
|
+
if (!cfg || !cfg.token || !cfg.device_id) {
|
|
454
|
+
return 'Not logged in / 未登录';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const result = await makeRequest('PUT', `/api/devices/${cfg.device_id}`, { device_name: name }, cfg.token);
|
|
459
|
+
if (result.status === 200) {
|
|
460
|
+
const configPath = path.join(pluginDir, 'config.json');
|
|
461
|
+
cfg.device_name = name;
|
|
462
|
+
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
463
|
+
return `Device renamed to "${name}" / 设备已重命名为 "${name}"`;
|
|
464
|
+
}
|
|
465
|
+
return 'Failed to rename device / 重命名设备失败';
|
|
466
|
+
} catch (e) {
|
|
467
|
+
return `Error: ${e.message}`;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
77
471
|
|
|
78
472
|
api.registerCli(
|
|
79
473
|
({ program }) => {
|
|
@@ -82,11 +476,12 @@ module.exports = function register(api) {
|
|
|
82
476
|
.description('启动 SoulSync 同步服务')
|
|
83
477
|
.action(() => {
|
|
84
478
|
const pluginDir = getPluginDir();
|
|
85
|
-
const configPath = path.join(pluginDir, 'config.json');
|
|
86
479
|
|
|
87
480
|
if (!checkConfigExists(pluginDir)) {
|
|
88
|
-
console.log('[SoulSync] Not configured. Starting
|
|
89
|
-
|
|
481
|
+
console.log('[SoulSync] Not configured. Starting device authorization flow...');
|
|
482
|
+
startDeviceCodeFlow().then(result => {
|
|
483
|
+
console.log('[SoulSync]', result.message);
|
|
484
|
+
});
|
|
90
485
|
return;
|
|
91
486
|
}
|
|
92
487
|
|
|
@@ -107,18 +502,19 @@ module.exports = function register(api) {
|
|
|
107
502
|
.command('soulsync:stop')
|
|
108
503
|
.description('停止 SoulSync 同步服务')
|
|
109
504
|
.action(() => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
pythonProcess.kill();
|
|
113
|
-
pythonProcess = null;
|
|
114
|
-
} else {
|
|
115
|
-
console.log('[SoulSync] Service not running');
|
|
116
|
-
}
|
|
505
|
+
stopPythonService();
|
|
506
|
+
console.log('[SoulSync] Service stopped');
|
|
117
507
|
});
|
|
118
508
|
},
|
|
119
509
|
{ commands: ['soulsync:stop'] }
|
|
120
510
|
);
|
|
121
511
|
|
|
512
|
+
const pluginDir = getPluginDir();
|
|
513
|
+
if (checkConfigExists(pluginDir)) {
|
|
514
|
+
console.log('[SoulSync] Auto-starting sync service...');
|
|
515
|
+
startPythonService('--start');
|
|
516
|
+
}
|
|
517
|
+
|
|
122
518
|
console.log('[SoulSync] Plugin loaded. Run "openclaw soulsync:start" to begin.');
|
|
123
519
|
console.log('[SoulSync] Plugin registered successfully');
|
|
124
520
|
};
|
package/openclaw.plugin.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"properties": {
|
|
8
8
|
"cloud_url": { "type": "string" },
|
|
9
9
|
"email": { "type": "string" },
|
|
10
|
-
"
|
|
10
|
+
"token": { "type": "string" }
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
13
|
"uiHints": {
|
|
14
|
-
"cloud_url": { "label": "服务器地址", "placeholder": "
|
|
15
|
-
"email": { "label": "邮箱" },
|
|
16
|
-
"
|
|
14
|
+
"cloud_url": { "label": "服务器地址 / Server URL", "placeholder": "https://soulsync.work" },
|
|
15
|
+
"email": { "label": "邮箱 / Email" },
|
|
16
|
+
"token": { "label": "Token", "sensitive": true }
|
|
17
17
|
}
|
|
18
18
|
}
|
package/package.json
CHANGED
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
|
|
|
@@ -48,8 +50,6 @@ class OpenClawClient:
|
|
|
48
50
|
new_device_id = str(uuid.uuid4())
|
|
49
51
|
with open(device_id_file, 'w') as f:
|
|
50
52
|
f.write(new_device_id)
|
|
51
|
-
|
|
52
|
-
print(f"Generated new device_id: {new_device_id}")
|
|
53
53
|
return new_device_id
|
|
54
54
|
|
|
55
55
|
def _save_token(self, token: str):
|
|
@@ -69,13 +69,17 @@ class OpenClawClient:
|
|
|
69
69
|
with open(token_file, 'r') as f:
|
|
70
70
|
return f.read().strip()
|
|
71
71
|
|
|
72
|
+
if self.config and self.config.get('token'):
|
|
73
|
+
return self.config.get('token')
|
|
74
|
+
|
|
72
75
|
return None
|
|
73
76
|
|
|
74
77
|
def _get_headers(self) -> dict:
|
|
75
|
-
"""获取请求头"""
|
|
76
78
|
headers = {'Content-Type': 'application/json'}
|
|
77
79
|
if self.token:
|
|
78
80
|
headers['Authorization'] = f'Bearer {self.token}'
|
|
81
|
+
if self.device_id:
|
|
82
|
+
headers['X-Device-ID'] = self.device_id
|
|
79
83
|
return headers
|
|
80
84
|
|
|
81
85
|
def authenticate(self, email: str = None, password: str = None) -> dict:
|
|
@@ -134,54 +138,13 @@ class OpenClawClient:
|
|
|
134
138
|
error = response.json().get('error', 'Unknown error')
|
|
135
139
|
raise Exception(f"Authentication failed: {error}")
|
|
136
140
|
|
|
137
|
-
def upload_memory(self, content: str) -> dict:
|
|
138
|
-
"""上传记忆
|
|
139
|
-
|
|
140
|
-
Args:
|
|
141
|
-
content: 记忆内容
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
上传结果
|
|
145
|
-
"""
|
|
146
|
-
url = f"{self.cloud_url}/api/memories"
|
|
147
|
-
data = {'content': content}
|
|
148
|
-
|
|
149
|
-
response = self.session.post(url, json=data, headers=self._get_headers())
|
|
150
|
-
|
|
151
|
-
if response.status_code == 200:
|
|
152
|
-
return response.json()
|
|
153
|
-
elif response.status_code == 403:
|
|
154
|
-
error = response.json().get('error', 'Subscription required')
|
|
155
|
-
raise Exception(f"Upload failed: {error}")
|
|
156
|
-
else:
|
|
157
|
-
error = response.json().get('error', 'Unknown error')
|
|
158
|
-
raise Exception(f"Upload failed: {error}")
|
|
159
|
-
|
|
160
|
-
def download_memory(self) -> dict:
|
|
161
|
-
"""下载记忆
|
|
162
|
-
|
|
163
|
-
Returns:
|
|
164
|
-
包含 content, version 等信息的字典
|
|
165
|
-
"""
|
|
166
|
-
url = f"{self.cloud_url}/api/memories"
|
|
167
|
-
|
|
168
|
-
response = self.session.get(url, headers=self._get_headers())
|
|
169
|
-
|
|
170
|
-
if response.status_code == 200:
|
|
171
|
-
return response.json()
|
|
172
|
-
elif response.status_code == 404:
|
|
173
|
-
return {'content': '', 'version': 0}
|
|
174
|
-
else:
|
|
175
|
-
error = response.json().get('error', 'Unknown error')
|
|
176
|
-
raise Exception(f"Download failed: {error}")
|
|
177
|
-
|
|
178
141
|
def get_profile(self) -> dict:
|
|
179
142
|
"""获取用户信息
|
|
180
143
|
|
|
181
144
|
Returns:
|
|
182
145
|
用户信息
|
|
183
146
|
"""
|
|
184
|
-
url = f"{self.cloud_url}/api/
|
|
147
|
+
url = f"{self.cloud_url}/api/profiles"
|
|
185
148
|
|
|
186
149
|
response = self.session.get(url, headers=self._get_headers())
|
|
187
150
|
|
|
@@ -286,6 +249,8 @@ class OpenClawClient:
|
|
|
286
249
|
self.token = result.get('token')
|
|
287
250
|
self.user_id = result.get('user_id')
|
|
288
251
|
self._save_token(self.token)
|
|
252
|
+
if self.config:
|
|
253
|
+
self.config['token'] = self.token
|
|
289
254
|
return result
|
|
290
255
|
else:
|
|
291
256
|
error = response.json().get('error', 'Unknown error')
|
|
@@ -311,6 +276,8 @@ class OpenClawClient:
|
|
|
311
276
|
self.token = result.get('token')
|
|
312
277
|
self.user_id = result.get('user_id')
|
|
313
278
|
self._save_token(self.token)
|
|
279
|
+
if self.config:
|
|
280
|
+
self.config['token'] = self.token
|
|
314
281
|
return result
|
|
315
282
|
else:
|
|
316
283
|
error = response.json().get('error', 'Unknown error')
|