soulsync 1.0.22 → 1.2.0
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/index.js +41 -43
- package/package.json +5 -3
- package/src/api-client.js +84 -0
- package/src/daemon.js +94 -0
- package/src/sync-engine.js +745 -0
- package/debug.py +0 -221
- package/requirements.txt +0 -3
- package/setup.sh +0 -91
- package/src/__init__.py +0 -1
- package/src/client.py +0 -300
- package/src/main.py +0 -479
- package/src/profiles.py +0 -88
- package/src/sync.py +0 -210
- package/src/version_manager.py +0 -61
- package/src/watcher.py +0 -133
- package/test_ssl_fix.py +0 -58
package/index.js
CHANGED
|
@@ -7,15 +7,11 @@ const crypto = require('crypto');
|
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const { createRequire } = require('module');
|
|
9
9
|
|
|
10
|
-
const { getDeviceName, loadConfig, saveConfig, isAuthenticated } = require('./src/config');
|
|
10
|
+
const { getDeviceName, loadConfig, saveConfig, isAuthenticated, getPluginDir } = require('./src/config');
|
|
11
11
|
|
|
12
|
-
let
|
|
12
|
+
let nodeProcess = null;
|
|
13
13
|
let deviceCodePolling = null;
|
|
14
14
|
|
|
15
|
-
function getPluginDir() {
|
|
16
|
-
return path.dirname(__filename);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
15
|
function getCloudUrl() {
|
|
20
16
|
const cfg = loadConfig();
|
|
21
17
|
return (cfg && cfg.cloud_url) || 'https://soulsync.work';
|
|
@@ -120,7 +116,7 @@ async function startOAuthLocal() {
|
|
|
120
116
|
});
|
|
121
117
|
|
|
122
118
|
await registerDevice(deviceId, deviceName, 'local', token);
|
|
123
|
-
|
|
119
|
+
startNodeService('--start');
|
|
124
120
|
|
|
125
121
|
const greeting = await getUserGreeting(token, deviceName);
|
|
126
122
|
return { success: true, message: greeting };
|
|
@@ -169,7 +165,7 @@ async function startDeviceCodeCLI() {
|
|
|
169
165
|
});
|
|
170
166
|
|
|
171
167
|
registerDevice(deviceId, deviceName, 'ssh', token);
|
|
172
|
-
|
|
168
|
+
startNodeService('--start');
|
|
173
169
|
|
|
174
170
|
const greeting = await getUserGreeting(token, deviceName);
|
|
175
171
|
return { success: true, message: greeting };
|
|
@@ -230,40 +226,42 @@ async function registerDevice(deviceId, deviceName, deviceType, token) {
|
|
|
230
226
|
}
|
|
231
227
|
}
|
|
232
228
|
|
|
233
|
-
function
|
|
229
|
+
function startNodeService(mode = '--start') {
|
|
234
230
|
const pluginDir = getPluginDir();
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
pythonProcess = null;
|
|
231
|
+
const daemonScript = path.join(pluginDir, 'src', 'daemon.js');
|
|
232
|
+
|
|
233
|
+
console.log(`[SoulSync] Starting Node.js sync service (${mode})...`);
|
|
234
|
+
|
|
235
|
+
if (nodeProcess) {
|
|
236
|
+
nodeProcess.kill();
|
|
237
|
+
nodeProcess = null;
|
|
243
238
|
}
|
|
244
|
-
|
|
245
|
-
|
|
239
|
+
|
|
240
|
+
nodeProcess = spawn(process.execPath, [daemonScript, mode], {
|
|
246
241
|
cwd: pluginDir,
|
|
247
242
|
env: { ...process.env, OPENCLAW_PLUGIN: 'true', PLUGIN_DIR: pluginDir },
|
|
248
|
-
stdio: '
|
|
243
|
+
stdio: 'ignore',
|
|
244
|
+
detached: true
|
|
249
245
|
});
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
console.log(`[SoulSync]
|
|
253
|
-
|
|
246
|
+
|
|
247
|
+
nodeProcess.on('close', (code) => {
|
|
248
|
+
console.log(`[SoulSync] Node.js process exited with code ${code}`);
|
|
249
|
+
nodeProcess = null;
|
|
254
250
|
});
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
console.error(`[SoulSync] Failed to start
|
|
258
|
-
|
|
251
|
+
|
|
252
|
+
nodeProcess.on('error', (err) => {
|
|
253
|
+
console.error(`[SoulSync] Failed to start Node.js process: ${err}`);
|
|
254
|
+
nodeProcess = null;
|
|
259
255
|
});
|
|
256
|
+
|
|
257
|
+
nodeProcess.unref();
|
|
260
258
|
}
|
|
261
259
|
|
|
262
|
-
function
|
|
263
|
-
if (
|
|
264
|
-
console.log('[SoulSync] Stopping
|
|
265
|
-
|
|
266
|
-
|
|
260
|
+
function stopNodeService() {
|
|
261
|
+
if (nodeProcess) {
|
|
262
|
+
console.log('[SoulSync] Stopping Node.js service...');
|
|
263
|
+
nodeProcess.kill();
|
|
264
|
+
nodeProcess = null;
|
|
267
265
|
}
|
|
268
266
|
}
|
|
269
267
|
|
|
@@ -343,7 +341,7 @@ async function startDeviceCodeFlow() {
|
|
|
343
341
|
});
|
|
344
342
|
|
|
345
343
|
registerDevice(deviceId, deviceName, 'cloud', token);
|
|
346
|
-
|
|
344
|
+
startNodeService('--start');
|
|
347
345
|
|
|
348
346
|
const greeting = await getUserGreeting(token, deviceName);
|
|
349
347
|
return { success: true, message: greeting };
|
|
@@ -385,12 +383,12 @@ module.exports = function register(api) {
|
|
|
385
383
|
return;
|
|
386
384
|
}
|
|
387
385
|
|
|
388
|
-
if (
|
|
386
|
+
if (nodeProcess) {
|
|
389
387
|
console.log('[SoulSync] Service already running');
|
|
390
388
|
return;
|
|
391
389
|
}
|
|
392
390
|
|
|
393
|
-
|
|
391
|
+
startNodeService('--start');
|
|
394
392
|
});
|
|
395
393
|
},
|
|
396
394
|
{ commands: ['soulsync:start'] }
|
|
@@ -440,8 +438,8 @@ module.exports = function register(api) {
|
|
|
440
438
|
return 'SoulSync is not configured. Please connect first.';
|
|
441
439
|
}
|
|
442
440
|
|
|
443
|
-
if (!
|
|
444
|
-
|
|
441
|
+
if (!nodeProcess) {
|
|
442
|
+
startNodeService('--sync');
|
|
445
443
|
return 'Sync triggered.';
|
|
446
444
|
}
|
|
447
445
|
return 'Sync already in progress.';
|
|
@@ -452,7 +450,7 @@ module.exports = function register(api) {
|
|
|
452
450
|
description: 'Logout and unbind device from SoulSync. Call when user says "退出soulsync", "解绑设备", "断开soulsync连接".',
|
|
453
451
|
input_schema: { type: 'object', properties: {}, required: [] }
|
|
454
452
|
}, async () => {
|
|
455
|
-
|
|
453
|
+
stopNodeService();
|
|
456
454
|
|
|
457
455
|
const cfg = loadConfig();
|
|
458
456
|
if (cfg && cfg.device_id && cfg.token) {
|
|
@@ -567,12 +565,12 @@ module.exports = function register(api) {
|
|
|
567
565
|
return;
|
|
568
566
|
}
|
|
569
567
|
|
|
570
|
-
if (
|
|
568
|
+
if (nodeProcess) {
|
|
571
569
|
console.log('[SoulSync] Service already running');
|
|
572
570
|
return;
|
|
573
571
|
}
|
|
574
572
|
|
|
575
|
-
|
|
573
|
+
startNodeService('--start');
|
|
576
574
|
});
|
|
577
575
|
},
|
|
578
576
|
{ commands: ['soulsync:start'] }
|
|
@@ -584,7 +582,7 @@ module.exports = function register(api) {
|
|
|
584
582
|
.command('soulsync:stop')
|
|
585
583
|
.description('停止 SoulSync 同步服务')
|
|
586
584
|
.action(() => {
|
|
587
|
-
|
|
585
|
+
stopNodeService();
|
|
588
586
|
console.log('[SoulSync] Service stopped');
|
|
589
587
|
});
|
|
590
588
|
},
|
|
@@ -593,7 +591,7 @@ module.exports = function register(api) {
|
|
|
593
591
|
|
|
594
592
|
if (isAuthenticated()) {
|
|
595
593
|
console.log('[SoulSync] Auto-starting sync service...');
|
|
596
|
-
|
|
594
|
+
startNodeService('--start');
|
|
597
595
|
}
|
|
598
596
|
|
|
599
597
|
console.log('[SoulSync] Plugin loaded. Run "openclaw soulsync:start" to begin.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soulsync",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "SoulSync plugin for OpenClaw - cross-bot memory synchronization",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -8,11 +8,13 @@
|
|
|
8
8
|
"url": "https://github.com/alanliuc-a11y/soulsync"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"
|
|
11
|
+
"chokidar": "^3.5.3",
|
|
12
|
+
"open": "^10.1.0",
|
|
13
|
+
"ws": "^8.14.2"
|
|
12
14
|
},
|
|
13
15
|
"openclaw": {
|
|
14
16
|
"extensions": [
|
|
15
17
|
"./index.js"
|
|
16
18
|
]
|
|
17
19
|
}
|
|
18
|
-
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const { URL } = require('url');
|
|
4
|
+
|
|
5
|
+
class APIClient {
|
|
6
|
+
constructor(baseUrl, token) {
|
|
7
|
+
this.baseUrl = baseUrl || 'https://soulsync.work';
|
|
8
|
+
this.token = token;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async request(method, path, body = null) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const urlStr = this.baseUrl + path;
|
|
14
|
+
const url = new URL(urlStr);
|
|
15
|
+
const isHttps = url.protocol === 'https:';
|
|
16
|
+
const lib = isHttps ? https : http;
|
|
17
|
+
|
|
18
|
+
const options = {
|
|
19
|
+
hostname: url.hostname,
|
|
20
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
21
|
+
path: url.pathname + url.search,
|
|
22
|
+
method: method,
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
'Authorization': `Bearer ${this.token}`
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const req = lib.request(options, (res) => {
|
|
30
|
+
let data = '';
|
|
31
|
+
res.on('data', chunk => data += chunk);
|
|
32
|
+
res.on('end', () => {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = data ? JSON.parse(data) : {};
|
|
35
|
+
resolve({ status: res.statusCode, body: parsed });
|
|
36
|
+
} catch (e) {
|
|
37
|
+
resolve({ status: res.statusCode, body: data });
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
req.on('error', reject);
|
|
43
|
+
|
|
44
|
+
if (body) {
|
|
45
|
+
req.write(JSON.stringify(body));
|
|
46
|
+
}
|
|
47
|
+
req.end();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async getProfiles() {
|
|
52
|
+
try {
|
|
53
|
+
console.log('[SoulSync] API getProfiles called');
|
|
54
|
+
const result = await this.request('GET', '/api/profiles');
|
|
55
|
+
console.log(`[SoulSync] API raw response:`, JSON.stringify(result).substring(0, 300));
|
|
56
|
+
return result;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.error('[SoulSync] API getProfiles error:', e.message);
|
|
59
|
+
return { status: 500, body: null };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async updateProfiles(content, version = 0) {
|
|
64
|
+
return this.request('PUT', '/api/profiles', { content, version });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getUserInfo() {
|
|
68
|
+
return this.request('GET', '/api/auth/user/info');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async registerDevice(deviceId, deviceName, deviceType) {
|
|
72
|
+
return this.request('POST', '/api/devices', {
|
|
73
|
+
device_id: deviceId,
|
|
74
|
+
device_name: deviceName,
|
|
75
|
+
device_type: deviceType
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async deleteDevice(deviceId) {
|
|
80
|
+
return this.request('DELETE', `/api/devices/${deviceId}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { APIClient };
|
package/src/daemon.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { SyncEngine } = require('./sync-engine');
|
|
5
|
+
const { loadConfig, getPluginDir } = require('./config');
|
|
6
|
+
|
|
7
|
+
function getLockFile() {
|
|
8
|
+
return path.join(os.tmpdir(), 'soulsync.lock');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function checkAlreadyRunning() {
|
|
12
|
+
const lockFile = getLockFile();
|
|
13
|
+
if (fs.existsSync(lockFile)) {
|
|
14
|
+
try {
|
|
15
|
+
const pid = parseInt(fs.readFileSync(lockFile, 'utf-8'));
|
|
16
|
+
try {
|
|
17
|
+
process.kill(pid, 0);
|
|
18
|
+
console.log('[SoulSync] Sync service already running (PID:', pid, ')');
|
|
19
|
+
return true;
|
|
20
|
+
} catch (e) {
|
|
21
|
+
fs.unlinkSync(lockFile);
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
fs.unlinkSync(lockFile);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeLockFile() {
|
|
31
|
+
const lockFile = getLockFile();
|
|
32
|
+
fs.writeFileSync(lockFile, process.pid.toString(), 'utf-8');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function removeLockFile() {
|
|
36
|
+
const lockFile = getLockFile();
|
|
37
|
+
if (fs.existsSync(lockFile)) {
|
|
38
|
+
fs.unlinkSync(lockFile);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
const config = loadConfig();
|
|
44
|
+
if (!config || !config.token) {
|
|
45
|
+
console.error('[SoulSync] Not configured. Run "openclaw soulsync:start" first.');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (checkAlreadyRunning()) {
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
writeLockFile();
|
|
54
|
+
|
|
55
|
+
const engine = new SyncEngine(config);
|
|
56
|
+
|
|
57
|
+
process.on('SIGTERM', () => {
|
|
58
|
+
console.log('[SoulSync] Received SIGTERM, shutting down...');
|
|
59
|
+
engine.disconnect();
|
|
60
|
+
removeLockFile();
|
|
61
|
+
process.exit(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
process.on('SIGINT', () => {
|
|
65
|
+
console.log('[SoulSync] Received SIGINT, shutting down...');
|
|
66
|
+
engine.disconnect();
|
|
67
|
+
removeLockFile();
|
|
68
|
+
process.exit(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
process.on('uncaughtException', (e) => {
|
|
72
|
+
console.error('[SoulSync] Uncaught exception:', e);
|
|
73
|
+
engine.disconnect();
|
|
74
|
+
removeLockFile();
|
|
75
|
+
process.exit(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await engine.initialize();
|
|
80
|
+
engine.startWatching();
|
|
81
|
+
console.log('[SoulSync] Sync service started');
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error('[SoulSync] Failed to start sync service:', e.message);
|
|
84
|
+
engine.disconnect();
|
|
85
|
+
removeLockFile();
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (require.main === module) {
|
|
91
|
+
main();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { SyncEngine, checkAlreadyRunning };
|