soulsync 1.0.12 → 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 -31
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/client.py +13 -44
- package/src/main.py +21 -49
- 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');
|
|
@@ -49,12 +130,6 @@ function startPythonService(mode = '--start') {
|
|
|
49
130
|
pythonProcess.on('close', (code) => {
|
|
50
131
|
console.log(`[SoulSync] Python process exited with code ${code}`);
|
|
51
132
|
pythonProcess = null;
|
|
52
|
-
|
|
53
|
-
// If setup succeeded (code 0), auto-start sync
|
|
54
|
-
if (mode === '--setup' && code === 0) {
|
|
55
|
-
console.log('[SoulSync] Setup completed, starting sync service...');
|
|
56
|
-
startPythonService('--start');
|
|
57
|
-
}
|
|
58
133
|
});
|
|
59
134
|
|
|
60
135
|
pythonProcess.on('error', (err) => {
|
|
@@ -65,21 +140,334 @@ function startPythonService(mode = '--start') {
|
|
|
65
140
|
return pythonProcess;
|
|
66
141
|
}
|
|
67
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
|
+
|
|
68
249
|
module.exports = function register(api) {
|
|
69
250
|
console.log('[SoulSync] Registering plugin...');
|
|
70
251
|
|
|
71
|
-
api.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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: []
|
|
80
411
|
},
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
});
|
|
83
471
|
|
|
84
472
|
api.registerCli(
|
|
85
473
|
({ program }) => {
|
|
@@ -88,11 +476,12 @@ module.exports = function register(api) {
|
|
|
88
476
|
.description('启动 SoulSync 同步服务')
|
|
89
477
|
.action(() => {
|
|
90
478
|
const pluginDir = getPluginDir();
|
|
91
|
-
const configPath = path.join(pluginDir, 'config.json');
|
|
92
479
|
|
|
93
480
|
if (!checkConfigExists(pluginDir)) {
|
|
94
|
-
console.log('[SoulSync] Not configured. Starting
|
|
95
|
-
|
|
481
|
+
console.log('[SoulSync] Not configured. Starting device authorization flow...');
|
|
482
|
+
startDeviceCodeFlow().then(result => {
|
|
483
|
+
console.log('[SoulSync]', result.message);
|
|
484
|
+
});
|
|
96
485
|
return;
|
|
97
486
|
}
|
|
98
487
|
|
|
@@ -113,18 +502,19 @@ module.exports = function register(api) {
|
|
|
113
502
|
.command('soulsync:stop')
|
|
114
503
|
.description('停止 SoulSync 同步服务')
|
|
115
504
|
.action(() => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
pythonProcess.kill();
|
|
119
|
-
pythonProcess = null;
|
|
120
|
-
} else {
|
|
121
|
-
console.log('[SoulSync] Service not running');
|
|
122
|
-
}
|
|
505
|
+
stopPythonService();
|
|
506
|
+
console.log('[SoulSync] Service stopped');
|
|
123
507
|
});
|
|
124
508
|
},
|
|
125
509
|
{ commands: ['soulsync:stop'] }
|
|
126
510
|
);
|
|
127
511
|
|
|
512
|
+
const pluginDir = getPluginDir();
|
|
513
|
+
if (checkConfigExists(pluginDir)) {
|
|
514
|
+
console.log('[SoulSync] Auto-starting sync service...');
|
|
515
|
+
startPythonService('--start');
|
|
516
|
+
}
|
|
517
|
+
|
|
128
518
|
console.log('[SoulSync] Plugin loaded. Run "openclaw soulsync:start" to begin.');
|
|
129
519
|
console.log('[SoulSync] Plugin registered successfully');
|
|
130
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
|
|
|
@@ -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,13 @@ class OpenClawClient:
|
|
|
132
138
|
error = response.json().get('error', 'Unknown error')
|
|
133
139
|
raise Exception(f"Authentication failed: {error}")
|
|
134
140
|
|
|
135
|
-
def upload_memory(self, content: str) -> dict:
|
|
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
|
-
"""下载记忆
|
|
160
|
-
|
|
161
|
-
Returns:
|
|
162
|
-
包含 content, version 等信息的字典
|
|
163
|
-
"""
|
|
164
|
-
url = f"{self.cloud_url}/api/memories"
|
|
165
|
-
|
|
166
|
-
response = self.session.get(url, headers=self._get_headers())
|
|
167
|
-
|
|
168
|
-
if response.status_code == 200:
|
|
169
|
-
return response.json()
|
|
170
|
-
elif response.status_code == 404:
|
|
171
|
-
return {'content': '', 'version': 0}
|
|
172
|
-
else:
|
|
173
|
-
error = response.json().get('error', 'Unknown error')
|
|
174
|
-
raise Exception(f"Download failed: {error}")
|
|
175
|
-
|
|
176
141
|
def get_profile(self) -> dict:
|
|
177
142
|
"""获取用户信息
|
|
178
143
|
|
|
179
144
|
Returns:
|
|
180
145
|
用户信息
|
|
181
146
|
"""
|
|
182
|
-
url = f"{self.cloud_url}/api/
|
|
147
|
+
url = f"{self.cloud_url}/api/profiles"
|
|
183
148
|
|
|
184
149
|
response = self.session.get(url, headers=self._get_headers())
|
|
185
150
|
|
|
@@ -284,6 +249,8 @@ class OpenClawClient:
|
|
|
284
249
|
self.token = result.get('token')
|
|
285
250
|
self.user_id = result.get('user_id')
|
|
286
251
|
self._save_token(self.token)
|
|
252
|
+
if self.config:
|
|
253
|
+
self.config['token'] = self.token
|
|
287
254
|
return result
|
|
288
255
|
else:
|
|
289
256
|
error = response.json().get('error', 'Unknown error')
|
|
@@ -309,6 +276,8 @@ class OpenClawClient:
|
|
|
309
276
|
self.token = result.get('token')
|
|
310
277
|
self.user_id = result.get('user_id')
|
|
311
278
|
self._save_token(self.token)
|
|
279
|
+
if self.config:
|
|
280
|
+
self.config['token'] = self.token
|
|
312
281
|
return result
|
|
313
282
|
else:
|
|
314
283
|
error = response.json().get('error', 'Unknown error')
|