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/index.js
CHANGED
|
@@ -1,31 +1,229 @@
|
|
|
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');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
const { getDeviceName, loadConfig, saveConfig, isAuthenticated } = require('./src/config');
|
|
4
10
|
|
|
5
11
|
let pythonProcess = null;
|
|
12
|
+
let deviceCodePolling = null;
|
|
6
13
|
|
|
7
14
|
function getPluginDir() {
|
|
8
15
|
return path.dirname(__filename);
|
|
9
16
|
}
|
|
10
17
|
|
|
11
|
-
function
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
function getCloudUrl() {
|
|
19
|
+
const cfg = loadConfig();
|
|
20
|
+
return (cfg && cfg.cloud_url) || 'https://soulsync.work';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeRequest(method, urlPath, body = null, token = null) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const cloudUrl = getCloudUrl();
|
|
26
|
+
const isHttps = cloudUrl.startsWith('https');
|
|
27
|
+
const client = isHttps ? https : http;
|
|
28
|
+
const urlObj = new URL(cloudUrl + urlPath);
|
|
29
|
+
|
|
30
|
+
const options = {
|
|
31
|
+
hostname: urlObj.hostname,
|
|
32
|
+
port: urlObj.port || (isHttps ? 443 : 80),
|
|
33
|
+
path: urlObj.pathname + urlObj.search,
|
|
34
|
+
method: method,
|
|
35
|
+
headers: { 'Content-Type': 'application/json' }
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (token) {
|
|
39
|
+
options.headers['Authorization'] = `Bearer ${token}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const req = client.request(options, (res) => {
|
|
43
|
+
let data = '';
|
|
44
|
+
res.on('data', chunk => data += chunk);
|
|
45
|
+
res.on('end', () => {
|
|
46
|
+
try {
|
|
47
|
+
const json = JSON.parse(data);
|
|
48
|
+
resolve({ status: res.statusCode, body: json });
|
|
49
|
+
} catch (e) {
|
|
50
|
+
resolve({ status: res.statusCode, body: data });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
req.on('error', reject);
|
|
56
|
+
if (body) req.write(JSON.stringify(body));
|
|
57
|
+
req.end();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function getUserGreeting(token, deviceName) {
|
|
62
|
+
let userName = '朋友';
|
|
63
|
+
try {
|
|
64
|
+
const userResult = await makeRequest('GET', '/api/auth/user/info', null, token);
|
|
65
|
+
if (userResult.status === 200 && userResult.body) {
|
|
66
|
+
if (userResult.body.name) {
|
|
67
|
+
userName = userResult.body.name;
|
|
68
|
+
} else if (userResult.body.email) {
|
|
69
|
+
userName = userResult.body.email.split('@')[0];
|
|
70
|
+
}
|
|
18
71
|
}
|
|
19
|
-
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error('[SoulSync] Failed to get user info:', e.message);
|
|
20
74
|
}
|
|
75
|
+
return `${userName}你好,我是三澍,非常高兴能在 ${deviceName} 再次与你相遇。\n\n已同步: SOUL.md / USER.md / MEMORY.md`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detectAuthMode(api) {
|
|
79
|
+
const hasChatAPI = api && typeof api.registerTool === 'function';
|
|
80
|
+
const isLocalTTY = process.stdin.isTTY && !process.env.SSH_CLIENT && !process.env.SSH_TTY;
|
|
81
|
+
const isSSH = process.env.SSH_CLIENT || process.env.SSH_TTY;
|
|
82
|
+
|
|
83
|
+
if (!hasChatAPI && isLocalTTY) return 'oauth-local';
|
|
84
|
+
if (!hasChatAPI && isSSH) return 'device-code-cli';
|
|
85
|
+
return 'device-code-chat';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function startOAuthLocal() {
|
|
89
|
+
const { createOAuthServer } = require('./src/oauth-server');
|
|
90
|
+
const open = require('open');
|
|
21
91
|
|
|
22
92
|
try {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
93
|
+
const { server, port } = await createOAuthServer();
|
|
94
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
95
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
96
|
+
const authUrl = `${getCloudUrl()}/auth/oauth/start?port=${port}&callback=${encodeURIComponent(callbackUrl)}&state=${state}`;
|
|
97
|
+
|
|
98
|
+
console.log('[SoulSync] Opening browser for authorization...');
|
|
99
|
+
await open(authUrl);
|
|
100
|
+
|
|
101
|
+
console.log('[SoulSync] Waiting for authorization...');
|
|
102
|
+
|
|
103
|
+
const { token } = await new Promise((resolve, reject) => {
|
|
104
|
+
server.on('close', () => reject(new Error('Server closed without receiving token')));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const deviceId = crypto.randomUUID();
|
|
108
|
+
const deviceName = getDeviceName('local');
|
|
109
|
+
|
|
110
|
+
saveConfig({
|
|
111
|
+
email: 'device',
|
|
112
|
+
token: token,
|
|
113
|
+
device_id: deviceId,
|
|
114
|
+
device_name: deviceName,
|
|
115
|
+
device_type: 'local',
|
|
116
|
+
cloud_url: getCloudUrl()
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await registerDevice(deviceId, deviceName, 'local', token);
|
|
120
|
+
startPythonService('--start');
|
|
121
|
+
|
|
122
|
+
const greeting = await getUserGreeting(token, deviceName);
|
|
123
|
+
return { success: true, message: greeting };
|
|
124
|
+
} catch (err) {
|
|
125
|
+
return { success: false, message: `OAuth Error: ${err.message}` };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function startDeviceCodeCLI() {
|
|
130
|
+
try {
|
|
131
|
+
const result = await makeRequest('POST', '/api/auth/device-code', {});
|
|
132
|
+
|
|
133
|
+
if (result.status !== 200 || !result.body.device_code) {
|
|
134
|
+
return { success: false, message: 'Failed to start device authorization.' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { device_code, auth_url } = result.body;
|
|
138
|
+
|
|
139
|
+
const homeDir = os.homedir();
|
|
140
|
+
const pendingDir = path.join(homeDir, '.soulsync');
|
|
141
|
+
if (!fs.existsSync(pendingDir)) {
|
|
142
|
+
fs.mkdirSync(pendingDir, { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
fs.writeFileSync(path.join(pendingDir, '.pending_auth'), JSON.stringify({
|
|
145
|
+
deviceCode: device_code,
|
|
146
|
+
authUrl: auth_url,
|
|
147
|
+
timestamp: Date.now()
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
console.log('[SoulSync] Please visit the following URL to authorize:');
|
|
151
|
+
console.log(` ${auth_url}`);
|
|
152
|
+
console.log('[SoulSync] Or say "connect SoulSync" in chat to continue...');
|
|
153
|
+
|
|
154
|
+
const token = await waitForAuthCompletion(device_code);
|
|
155
|
+
|
|
156
|
+
const deviceId = crypto.randomUUID();
|
|
157
|
+
const deviceName = getDeviceName('ssh');
|
|
158
|
+
|
|
159
|
+
saveConfig({
|
|
160
|
+
email: 'device',
|
|
161
|
+
token: token,
|
|
162
|
+
device_id: deviceId,
|
|
163
|
+
device_name: deviceName,
|
|
164
|
+
device_type: 'ssh',
|
|
165
|
+
cloud_url: getCloudUrl()
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
registerDevice(deviceId, deviceName, 'ssh', token);
|
|
169
|
+
startPythonService('--start');
|
|
170
|
+
|
|
171
|
+
const greeting = await getUserGreeting(token, deviceName);
|
|
172
|
+
return { success: true, message: greeting };
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return { success: false, message: `Error: ${err.message}` };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function waitForAuthCompletion(device_code) {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const timeout = setTimeout(() => {
|
|
181
|
+
if (deviceCodePolling) {
|
|
182
|
+
clearInterval(deviceCodePolling);
|
|
183
|
+
deviceCodePolling = null;
|
|
184
|
+
}
|
|
185
|
+
reject(new Error('Authorization timeout'));
|
|
186
|
+
}, 900000);
|
|
187
|
+
|
|
188
|
+
deviceCodePolling = setInterval(async () => {
|
|
189
|
+
try {
|
|
190
|
+
const pollResult = await makeRequest('GET', `/api/auth/device-code/${device_code}/status`);
|
|
191
|
+
|
|
192
|
+
if (pollResult.body.status === 'authorized' && pollResult.body.token) {
|
|
193
|
+
clearInterval(deviceCodePolling);
|
|
194
|
+
deviceCodePolling = null;
|
|
195
|
+
clearTimeout(timeout);
|
|
196
|
+
resolve(pollResult.body.token);
|
|
197
|
+
} else if (pollResult.body.status === 'expired') {
|
|
198
|
+
clearInterval(deviceCodePolling);
|
|
199
|
+
deviceCodePolling = null;
|
|
200
|
+
clearTimeout(timeout);
|
|
201
|
+
reject(new Error('Authorization expired'));
|
|
202
|
+
} else if (pollResult.body.status === 'not_found') {
|
|
203
|
+
clearInterval(deviceCodePolling);
|
|
204
|
+
deviceCodePolling = null;
|
|
205
|
+
clearTimeout(timeout);
|
|
206
|
+
reject(new Error('Device code not found'));
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
clearInterval(deviceCodePolling);
|
|
210
|
+
deviceCodePolling = null;
|
|
211
|
+
clearTimeout(timeout);
|
|
212
|
+
reject(e);
|
|
213
|
+
}
|
|
214
|
+
}, 3000);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function registerDevice(deviceId, deviceName, deviceType, token) {
|
|
219
|
+
try {
|
|
220
|
+
await makeRequest('POST', '/api/devices', {
|
|
221
|
+
device_id: deviceId,
|
|
222
|
+
device_name: deviceName,
|
|
223
|
+
device_type: deviceType
|
|
224
|
+
}, token);
|
|
27
225
|
} catch (e) {
|
|
28
|
-
|
|
226
|
+
console.error('[SoulSync] Failed to register device:', e.message);
|
|
29
227
|
}
|
|
30
228
|
}
|
|
31
229
|
|
|
@@ -36,63 +234,305 @@ function startPythonService(mode = '--start') {
|
|
|
36
234
|
|
|
37
235
|
console.log(`[SoulSync] Starting Python service (${mode})...`);
|
|
38
236
|
|
|
237
|
+
if (pythonProcess) {
|
|
238
|
+
pythonProcess.kill();
|
|
239
|
+
pythonProcess = null;
|
|
240
|
+
}
|
|
241
|
+
|
|
39
242
|
pythonProcess = spawn(pythonPath, [pythonScript, mode], {
|
|
40
243
|
cwd: pluginDir,
|
|
41
|
-
env: {
|
|
42
|
-
...process.env,
|
|
43
|
-
OPENCLAW_PLUGIN: 'true',
|
|
44
|
-
PLUGIN_DIR: pluginDir
|
|
45
|
-
},
|
|
244
|
+
env: { ...process.env, OPENCLAW_PLUGIN: 'true', PLUGIN_DIR: pluginDir },
|
|
46
245
|
stdio: 'inherit'
|
|
47
246
|
});
|
|
48
|
-
|
|
247
|
+
|
|
49
248
|
pythonProcess.on('close', (code) => {
|
|
50
249
|
console.log(`[SoulSync] Python process exited with code ${code}`);
|
|
51
250
|
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
251
|
});
|
|
59
252
|
|
|
60
253
|
pythonProcess.on('error', (err) => {
|
|
61
254
|
console.error(`[SoulSync] Failed to start Python process: ${err}`);
|
|
62
255
|
pythonProcess = null;
|
|
63
256
|
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function stopPythonService() {
|
|
260
|
+
if (pythonProcess) {
|
|
261
|
+
console.log('[SoulSync] Stopping Python service...');
|
|
262
|
+
pythonProcess.kill();
|
|
263
|
+
pythonProcess = null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function checkConnection() {
|
|
268
|
+
const cfg = loadConfig();
|
|
269
|
+
if (!cfg || !cfg.token) {
|
|
270
|
+
return Promise.resolve({ connected: false, reason: 'not_configured' });
|
|
271
|
+
}
|
|
64
272
|
|
|
65
|
-
return
|
|
273
|
+
return makeRequest('GET', '/api/profiles', null, cfg.token)
|
|
274
|
+
.then(result => {
|
|
275
|
+
if (result.status === 200) return { connected: true, data: result.body };
|
|
276
|
+
if (result.status === 401) return { connected: false, reason: 'token_expired' };
|
|
277
|
+
return { connected: false, reason: 'server_error', status: result.status };
|
|
278
|
+
})
|
|
279
|
+
.catch(err => ({ connected: false, reason: 'connection_error', error: err.message }));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function startDeviceCodeFlow() {
|
|
283
|
+
try {
|
|
284
|
+
const result = await makeRequest('POST', '/api/auth/device-code', {});
|
|
285
|
+
|
|
286
|
+
if (result.status !== 200 || !result.body.device_code) {
|
|
287
|
+
return { success: false, message: 'Failed to start device authorization.' };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const { device_code, auth_url, expires_in } = result.body;
|
|
291
|
+
|
|
292
|
+
const token = await new Promise((resolve, reject) => {
|
|
293
|
+
const timeout = setTimeout(() => {
|
|
294
|
+
if (deviceCodePolling) {
|
|
295
|
+
clearInterval(deviceCodePolling);
|
|
296
|
+
deviceCodePolling = null;
|
|
297
|
+
}
|
|
298
|
+
reject(new Error('Authorization timeout'));
|
|
299
|
+
}, expires_in * 1000);
|
|
300
|
+
|
|
301
|
+
deviceCodePolling = setInterval(async () => {
|
|
302
|
+
try {
|
|
303
|
+
const pollResult = await makeRequest('GET', `/api/auth/device-code/${device_code}/status`);
|
|
304
|
+
|
|
305
|
+
if (pollResult.body.status === 'authorized' && pollResult.body.token) {
|
|
306
|
+
clearInterval(deviceCodePolling);
|
|
307
|
+
deviceCodePolling = null;
|
|
308
|
+
clearTimeout(timeout);
|
|
309
|
+
resolve(pollResult.body.token);
|
|
310
|
+
} else if (pollResult.body.status === 'expired') {
|
|
311
|
+
clearInterval(deviceCodePolling);
|
|
312
|
+
deviceCodePolling = null;
|
|
313
|
+
clearTimeout(timeout);
|
|
314
|
+
reject(new Error('Authorization expired'));
|
|
315
|
+
} else if (pollResult.body.status === 'not_found') {
|
|
316
|
+
clearInterval(deviceCodePolling);
|
|
317
|
+
deviceCodePolling = null;
|
|
318
|
+
clearTimeout(timeout);
|
|
319
|
+
reject(new Error('Device code not found'));
|
|
320
|
+
}
|
|
321
|
+
} catch (e) {
|
|
322
|
+
clearInterval(deviceCodePolling);
|
|
323
|
+
deviceCodePolling = null;
|
|
324
|
+
clearTimeout(timeout);
|
|
325
|
+
reject(e);
|
|
326
|
+
}
|
|
327
|
+
}, 3000);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const deviceId = crypto.randomUUID();
|
|
331
|
+
const deviceName = getDeviceName('cloud');
|
|
332
|
+
|
|
333
|
+
saveConfig({
|
|
334
|
+
email: 'device',
|
|
335
|
+
token: token,
|
|
336
|
+
device_id: deviceId,
|
|
337
|
+
device_name: deviceName,
|
|
338
|
+
device_type: 'cloud',
|
|
339
|
+
cloud_url: getCloudUrl()
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
registerDevice(deviceId, deviceName, 'cloud', token);
|
|
343
|
+
startPythonService('--start');
|
|
344
|
+
|
|
345
|
+
const greeting = await getUserGreeting(token, deviceName);
|
|
346
|
+
return { success: true, message: greeting };
|
|
347
|
+
} catch (err) {
|
|
348
|
+
return { success: false, message: `Error: ${err.message}` };
|
|
349
|
+
}
|
|
66
350
|
}
|
|
67
351
|
|
|
68
352
|
module.exports = function register(api) {
|
|
69
353
|
console.log('[SoulSync] Registering plugin...');
|
|
70
|
-
|
|
71
|
-
api
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
354
|
+
|
|
355
|
+
const mode = detectAuthMode(api);
|
|
356
|
+
console.log(`[SoulSync] Detected auth mode: ${mode}`);
|
|
357
|
+
|
|
358
|
+
if (!isAuthenticated()) {
|
|
359
|
+
switch (mode) {
|
|
360
|
+
case 'oauth-local':
|
|
361
|
+
console.log('[SoulSync] Starting OAuth local flow...');
|
|
362
|
+
startOAuthLocal().then(result => {
|
|
363
|
+
if (result.success) registerChatTools(api);
|
|
79
364
|
});
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
365
|
+
break;
|
|
366
|
+
case 'device-code-cli':
|
|
367
|
+
console.log('[SoulSync] Starting device code CLI flow...');
|
|
368
|
+
startDeviceCodeCLI().then(result => {
|
|
369
|
+
console.log('[SoulSync]', result.message);
|
|
370
|
+
});
|
|
371
|
+
break;
|
|
372
|
+
case 'device-code-chat':
|
|
373
|
+
console.log('[SoulSync] Waiting for user to say "connect SoulSync"...');
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
console.log('[SoulSync] Already authenticated, starting sync...');
|
|
378
|
+
startPythonService('--start');
|
|
379
|
+
registerChatTools(api);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function registerChatTools(api) {
|
|
383
|
+
api.registerTool({
|
|
384
|
+
name: 'soulsync_status',
|
|
385
|
+
description: 'Check SoulSync plugin status for OpenClaw (小龙虾). Call when user mentions SoulSync, soul sync, 灵魂同步, OpenClaw plugin, 小龙虾插件, or asks about sync status.',
|
|
386
|
+
input_schema: { type: 'object', properties: {}, required: [] }
|
|
387
|
+
}, async () => {
|
|
388
|
+
const cfg = loadConfig();
|
|
389
|
+
if (!cfg || !cfg.token) {
|
|
390
|
+
return { configured: false, message: 'SoulSync is not configured. Would you like to connect your SoulSync account?' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const conn = await checkConnection();
|
|
394
|
+
if (conn.connected) {
|
|
395
|
+
return { configured: true, connected: true, email: cfg.email, message: `SoulSync is connected and syncing for ${cfg.email}.` };
|
|
396
|
+
} else if (conn.reason === 'token_expired') {
|
|
397
|
+
return { configured: true, connected: false, message: 'SoulSync token has expired. Would you like to reconnect your account?' };
|
|
398
|
+
} else {
|
|
399
|
+
return { configured: true, connected: false, message: `SoulSync is configured but cannot connect to server: ${conn.reason}` };
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
api.registerTool({
|
|
404
|
+
name: 'soulsync_connect',
|
|
405
|
+
description: 'Install and configure SoulSync plugin for OpenClaw (小龙虾). Call when user says "安装soulsync插件", "配置soulsync插件", "设置小龙虾同步", "连接soulsync". No credentials needed - will provide a browser link for authorization.',
|
|
406
|
+
input_schema: { type: 'object', properties: {}, required: [] }
|
|
407
|
+
}, async () => {
|
|
408
|
+
if (isAuthenticated()) {
|
|
409
|
+
return 'SoulSync is already connected. Use soulsync_status to check the current status.';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const result = await startDeviceCodeFlow();
|
|
413
|
+
return result.message;
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
api.registerTool({
|
|
417
|
+
name: 'soulsync_sync',
|
|
418
|
+
description: 'Manually trigger SoulSync sync. Call when user says "同步soulsync", "手动同步", "强制同步", or wants to force sync.',
|
|
419
|
+
input_schema: { type: 'object', properties: {}, required: [] }
|
|
420
|
+
}, async () => {
|
|
421
|
+
const cfg = loadConfig();
|
|
422
|
+
if (!cfg || !cfg.token) {
|
|
423
|
+
return 'SoulSync is not configured. Please connect first.';
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!pythonProcess) {
|
|
427
|
+
startPythonService('--sync');
|
|
428
|
+
return 'Sync triggered.';
|
|
429
|
+
}
|
|
430
|
+
return 'Sync already in progress.';
|
|
431
|
+
});
|
|
83
432
|
|
|
433
|
+
api.registerTool({
|
|
434
|
+
name: 'soulsync_logout',
|
|
435
|
+
description: 'Logout and unbind device from SoulSync. Call when user says "退出soulsync", "解绑设备", "断开soulsync连接".',
|
|
436
|
+
input_schema: { type: 'object', properties: {}, required: [] }
|
|
437
|
+
}, async () => {
|
|
438
|
+
stopPythonService();
|
|
439
|
+
|
|
440
|
+
const cfg = loadConfig();
|
|
441
|
+
if (cfg && cfg.device_id && cfg.token) {
|
|
442
|
+
try {
|
|
443
|
+
await makeRequest('DELETE', `/api/devices/${cfg.device_id}`, null, cfg.token);
|
|
444
|
+
} catch (e) {
|
|
445
|
+
console.error('[SoulSync] Failed to delete device:', e.message);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const pluginDir = getPluginDir();
|
|
451
|
+
const configPath = path.join(pluginDir, 'config.json');
|
|
452
|
+
if (fs.existsSync(configPath)) {
|
|
453
|
+
const existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
454
|
+
delete existing.token;
|
|
455
|
+
delete existing.email;
|
|
456
|
+
delete existing.device_id;
|
|
457
|
+
delete existing.device_name;
|
|
458
|
+
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2));
|
|
459
|
+
}
|
|
460
|
+
} catch (e) {
|
|
461
|
+
console.error('[SoulSync] Error clearing config:', e);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return 'Logged out successfully. Your local data is preserved. To reconnect, say "connect SoulSync".';
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
api.registerTool({
|
|
468
|
+
name: 'soulsync_devices',
|
|
469
|
+
description: 'List connected SoulSync devices. Call when user says "查看soulsync设备", "我的设备列表", "已连接设备", or asks about connected devices.',
|
|
470
|
+
input_schema: { type: 'object', properties: {}, required: [] }
|
|
471
|
+
}, async () => {
|
|
472
|
+
const cfg = loadConfig();
|
|
473
|
+
if (!cfg || !cfg.token) {
|
|
474
|
+
return 'Not logged in / 未登录';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const result = await makeRequest('GET', '/api/devices', null, cfg.token);
|
|
479
|
+
if (result.status === 200) {
|
|
480
|
+
const devices = result.body.devices || [];
|
|
481
|
+
if (devices.length === 0) return 'No devices found / 未找到设备';
|
|
482
|
+
const list = devices.map(d =>
|
|
483
|
+
`- ${d.device_name || 'Unnamed'} (${d.device_type || 'local'}) - Last sync: ${d.last_sync_at || 'Never'}`
|
|
484
|
+
).join('\n');
|
|
485
|
+
return `Connected devices / 已连接设备:\n${list}`;
|
|
486
|
+
}
|
|
487
|
+
return 'Failed to get devices / 获取设备列表失败';
|
|
488
|
+
} catch (e) {
|
|
489
|
+
return `Error: ${e.message}`;
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
api.registerTool({
|
|
494
|
+
name: 'soulsync_rename_device',
|
|
495
|
+
description: 'Rename current SoulSync device. Call when user says "重命名设备", "修改设备名称".',
|
|
496
|
+
input_schema: {
|
|
497
|
+
type: 'object',
|
|
498
|
+
properties: {
|
|
499
|
+
name: { type: 'string', description: 'New device name / 新设备名称' }
|
|
500
|
+
},
|
|
501
|
+
required: ['name']
|
|
502
|
+
}
|
|
503
|
+
}, async ({ name }) => {
|
|
504
|
+
const cfg = loadConfig();
|
|
505
|
+
if (!cfg || !cfg.token || !cfg.device_id) {
|
|
506
|
+
return 'Not logged in / 未登录';
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
const result = await makeRequest('PUT', `/api/devices/${cfg.device_id}`, { device_name: name }, cfg.token);
|
|
511
|
+
if (result.status === 200) {
|
|
512
|
+
const pluginDir = getPluginDir();
|
|
513
|
+
const configPath = path.join(pluginDir, 'config.json');
|
|
514
|
+
cfg.device_name = name;
|
|
515
|
+
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
516
|
+
return `Device renamed to "${name}" / 设备已重命名为 "${name}"`;
|
|
517
|
+
}
|
|
518
|
+
return 'Failed to rename device / 重命名设备失败';
|
|
519
|
+
} catch (e) {
|
|
520
|
+
return `Error: ${e.message}`;
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
84
525
|
api.registerCli(
|
|
85
526
|
({ program }) => {
|
|
86
527
|
program
|
|
87
528
|
.command('soulsync:start')
|
|
88
529
|
.description('启动 SoulSync 同步服务')
|
|
89
530
|
.action(() => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
startPythonService('--setup');
|
|
531
|
+
if (!isAuthenticated()) {
|
|
532
|
+
console.log('[SoulSync] Not configured. Starting device authorization flow...');
|
|
533
|
+
startDeviceCodeFlow().then(result => {
|
|
534
|
+
console.log('[SoulSync]', result.message);
|
|
535
|
+
});
|
|
96
536
|
return;
|
|
97
537
|
}
|
|
98
538
|
|
|
@@ -113,18 +553,19 @@ module.exports = function register(api) {
|
|
|
113
553
|
.command('soulsync:stop')
|
|
114
554
|
.description('停止 SoulSync 同步服务')
|
|
115
555
|
.action(() => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
pythonProcess.kill();
|
|
119
|
-
pythonProcess = null;
|
|
120
|
-
} else {
|
|
121
|
-
console.log('[SoulSync] Service not running');
|
|
122
|
-
}
|
|
556
|
+
stopPythonService();
|
|
557
|
+
console.log('[SoulSync] Service stopped');
|
|
123
558
|
});
|
|
124
559
|
},
|
|
125
560
|
{ commands: ['soulsync:stop'] }
|
|
126
561
|
);
|
|
127
562
|
|
|
563
|
+
if (isAuthenticated()) {
|
|
564
|
+
console.log('[SoulSync] Auto-starting sync service...');
|
|
565
|
+
startPythonService('--start');
|
|
566
|
+
registerChatTools(api);
|
|
567
|
+
}
|
|
568
|
+
|
|
128
569
|
console.log('[SoulSync] Plugin loaded. Run "openclaw soulsync:start" to begin.');
|
|
129
570
|
console.log('[SoulSync] Plugin registered successfully');
|
|
130
|
-
};
|
|
571
|
+
};
|
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
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soulsync",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.14",
|
|
4
4
|
"description": "SoulSync plugin for OpenClaw - cross-bot memory synchronization",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/alanliuc-a11y/soulsync"
|
|
9
9
|
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"open": "^10.1.0"
|
|
12
|
+
},
|
|
10
13
|
"openclaw": {
|
|
11
14
|
"extensions": [
|
|
12
15
|
"./index.js"
|