soulsync 1.0.13 → 1.0.15
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/index.js +391 -326
- package/package.json +4 -1
- package/src/client.py +16 -0
- package/src/config.js +100 -0
- package/src/device-code.js +150 -0
- package/src/oauth-server.js +48 -0
- package/test_ssl_fix.py +58 -0
package/index.js
CHANGED
|
@@ -4,74 +4,27 @@ const fs = require('fs');
|
|
|
4
4
|
const https = require('https');
|
|
5
5
|
const http = require('http');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
|
|
9
|
+
const { getDeviceName, loadConfig, saveConfig, isAuthenticated } = require('./src/config');
|
|
7
10
|
|
|
8
11
|
let pythonProcess = null;
|
|
9
|
-
let config = null;
|
|
10
12
|
let deviceCodePolling = null;
|
|
11
13
|
|
|
12
14
|
function getPluginDir() {
|
|
13
15
|
return path.dirname(__filename);
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
function
|
|
17
|
-
const
|
|
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
|
-
|
|
28
|
-
function checkConfigExists(pluginDir) {
|
|
29
|
-
const configPath = path.join(pluginDir, 'config.json');
|
|
30
|
-
if (!fs.existsSync(configPath)) {
|
|
31
|
-
const examplePath = path.join(pluginDir, 'config.json.example');
|
|
32
|
-
if (fs.existsSync(examplePath)) {
|
|
33
|
-
fs.copyFileSync(examplePath, configPath);
|
|
34
|
-
console.log('[SoulSync] Created config.json from template');
|
|
35
|
-
}
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
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');
|
|
49
|
-
try {
|
|
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;
|
|
58
|
-
} catch (e) {
|
|
59
|
-
console.error('[SoulSync] Error saving config:', e);
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function getCloudUrl(pluginDir) {
|
|
65
|
-
const cfg = loadConfig(pluginDir);
|
|
18
|
+
function getCloudUrl() {
|
|
19
|
+
const cfg = loadConfig();
|
|
66
20
|
return (cfg && cfg.cloud_url) || 'https://soulsync.work';
|
|
67
21
|
}
|
|
68
22
|
|
|
69
23
|
function makeRequest(method, urlPath, body = null, token = null) {
|
|
70
24
|
return new Promise((resolve, reject) => {
|
|
71
|
-
const cloudUrl = getCloudUrl(
|
|
25
|
+
const cloudUrl = getCloudUrl();
|
|
72
26
|
const isHttps = cloudUrl.startsWith('https');
|
|
73
27
|
const client = isHttps ? https : http;
|
|
74
|
-
|
|
75
28
|
const urlObj = new URL(cloudUrl + urlPath);
|
|
76
29
|
|
|
77
30
|
const options = {
|
|
@@ -79,9 +32,7 @@ function makeRequest(method, urlPath, body = null, token = null) {
|
|
|
79
32
|
port: urlObj.port || (isHttps ? 443 : 80),
|
|
80
33
|
path: urlObj.pathname + urlObj.search,
|
|
81
34
|
method: method,
|
|
82
|
-
headers: {
|
|
83
|
-
'Content-Type': 'application/json'
|
|
84
|
-
}
|
|
35
|
+
headers: { 'Content-Type': 'application/json' }
|
|
85
36
|
};
|
|
86
37
|
|
|
87
38
|
if (token) {
|
|
@@ -102,14 +53,180 @@ function makeRequest(method, urlPath, body = null, token = null) {
|
|
|
102
53
|
});
|
|
103
54
|
|
|
104
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
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.error('[SoulSync] Failed to get user info:', e.message);
|
|
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');
|
|
91
|
+
|
|
92
|
+
try {
|
|
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', {});
|
|
105
132
|
|
|
106
|
-
if (body) {
|
|
107
|
-
|
|
133
|
+
if (result.status !== 200 || !result.body.device_code) {
|
|
134
|
+
return { success: false, message: 'Failed to start device authorization.' };
|
|
108
135
|
}
|
|
109
|
-
|
|
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);
|
|
110
215
|
});
|
|
111
216
|
}
|
|
112
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);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.error('[SoulSync] Failed to register device:', e.message);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
113
230
|
function startPythonService(mode = '--start') {
|
|
114
231
|
const pluginDir = getPluginDir();
|
|
115
232
|
const pythonScript = path.join(pluginDir, 'src', 'main.py');
|
|
@@ -117,16 +234,17 @@ function startPythonService(mode = '--start') {
|
|
|
117
234
|
|
|
118
235
|
console.log(`[SoulSync] Starting Python service (${mode})...`);
|
|
119
236
|
|
|
237
|
+
if (pythonProcess) {
|
|
238
|
+
pythonProcess.kill();
|
|
239
|
+
pythonProcess = null;
|
|
240
|
+
}
|
|
241
|
+
|
|
120
242
|
pythonProcess = spawn(pythonPath, [pythonScript, mode], {
|
|
121
243
|
cwd: pluginDir,
|
|
122
|
-
env: {
|
|
123
|
-
...process.env,
|
|
124
|
-
OPENCLAW_PLUGIN: 'true',
|
|
125
|
-
PLUGIN_DIR: pluginDir
|
|
126
|
-
},
|
|
244
|
+
env: { ...process.env, OPENCLAW_PLUGIN: 'true', PLUGIN_DIR: pluginDir },
|
|
127
245
|
stdio: 'inherit'
|
|
128
246
|
});
|
|
129
|
-
|
|
247
|
+
|
|
130
248
|
pythonProcess.on('close', (code) => {
|
|
131
249
|
console.log(`[SoulSync] Python process exited with code ${code}`);
|
|
132
250
|
pythonProcess = null;
|
|
@@ -136,8 +254,6 @@ function startPythonService(mode = '--start') {
|
|
|
136
254
|
console.error(`[SoulSync] Failed to start Python process: ${err}`);
|
|
137
255
|
pythonProcess = null;
|
|
138
256
|
});
|
|
139
|
-
|
|
140
|
-
return pythonProcess;
|
|
141
257
|
}
|
|
142
258
|
|
|
143
259
|
function stopPythonService() {
|
|
@@ -149,26 +265,18 @@ function stopPythonService() {
|
|
|
149
265
|
}
|
|
150
266
|
|
|
151
267
|
function checkConnection() {
|
|
152
|
-
const
|
|
153
|
-
const cfg = loadConfig(pluginDir);
|
|
154
|
-
|
|
268
|
+
const cfg = loadConfig();
|
|
155
269
|
if (!cfg || !cfg.token) {
|
|
156
|
-
return { connected: false, reason: 'not_configured' };
|
|
270
|
+
return Promise.resolve({ connected: false, reason: 'not_configured' });
|
|
157
271
|
}
|
|
158
272
|
|
|
159
273
|
return makeRequest('GET', '/api/profiles', null, cfg.token)
|
|
160
274
|
.then(result => {
|
|
161
|
-
if (result.status === 200) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return { connected: false, reason: 'token_expired' };
|
|
165
|
-
} else {
|
|
166
|
-
return { connected: false, reason: 'server_error', status: result.status };
|
|
167
|
-
}
|
|
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 };
|
|
168
278
|
})
|
|
169
|
-
.catch(err => {
|
|
170
|
-
return { connected: false, reason: 'connection_error', error: err.message };
|
|
171
|
-
});
|
|
279
|
+
.catch(err => ({ connected: false, reason: 'connection_error', error: err.message }));
|
|
172
280
|
}
|
|
173
281
|
|
|
174
282
|
async function startDeviceCodeFlow() {
|
|
@@ -176,243 +284,192 @@ async function startDeviceCodeFlow() {
|
|
|
176
284
|
const result = await makeRequest('POST', '/api/auth/device-code', {});
|
|
177
285
|
|
|
178
286
|
if (result.status !== 200 || !result.body.device_code) {
|
|
179
|
-
return {
|
|
180
|
-
success: false,
|
|
181
|
-
message: 'Failed to start device authorization. Please try again.'
|
|
182
|
-
};
|
|
287
|
+
return { success: false, message: 'Failed to start device authorization.' };
|
|
183
288
|
}
|
|
184
289
|
|
|
185
290
|
const { device_code, auth_url, expires_in } = result.body;
|
|
186
291
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (pollResult.body.status === 'authorized' && pollResult.body.token) {
|
|
292
|
+
const token = await new Promise((resolve, reject) => {
|
|
293
|
+
const timeout = setTimeout(() => {
|
|
294
|
+
if (deviceCodePolling) {
|
|
192
295
|
clearInterval(deviceCodePolling);
|
|
193
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`);
|
|
194
304
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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') {
|
|
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) {
|
|
221
322
|
clearInterval(deviceCodePolling);
|
|
222
323
|
deviceCodePolling = null;
|
|
223
|
-
|
|
324
|
+
clearTimeout(timeout);
|
|
325
|
+
reject(e);
|
|
224
326
|
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
}, 3000);
|
|
327
|
+
}, 3000);
|
|
328
|
+
});
|
|
229
329
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
clearInterval(deviceCodePolling);
|
|
233
|
-
deviceCodePolling = null;
|
|
234
|
-
}
|
|
235
|
-
}, expires_in * 1000);
|
|
330
|
+
const deviceId = crypto.randomUUID();
|
|
331
|
+
const deviceName = getDeviceName('cloud');
|
|
236
332
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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 };
|
|
241
347
|
} catch (err) {
|
|
242
|
-
return {
|
|
243
|
-
success: false,
|
|
244
|
-
message: `Error: ${err.message}`
|
|
245
|
-
};
|
|
348
|
+
return { success: false, message: `Error: ${err.message}` };
|
|
246
349
|
}
|
|
247
350
|
}
|
|
248
351
|
|
|
249
352
|
module.exports = function register(api) {
|
|
250
353
|
console.log('[SoulSync] Registering plugin...');
|
|
251
|
-
|
|
252
|
-
api
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
};
|
|
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);
|
|
364
|
+
});
|
|
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;
|
|
328
375
|
}
|
|
329
|
-
|
|
376
|
+
} else {
|
|
377
|
+
console.log('[SoulSync] Already authenticated, starting sync...');
|
|
330
378
|
startPythonService('--start');
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
});
|
|
337
402
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
});
|
|
403
|
+
api.registerTool({
|
|
404
|
+
name: 'soulsync_connect',
|
|
405
|
+
description: '连接 SoulSync 灵魂同步服务。当用户说"安装soulsync"、"配置soulsync"、"连接soulsync"、"设置同步"、"开启灵魂同步"时调用。无需账号密码,会提供浏览器授权链接。',
|
|
406
|
+
input_schema: { type: 'object', properties: {}, required: [] }
|
|
407
|
+
}, async () => {
|
|
408
|
+
if (isAuthenticated()) {
|
|
409
|
+
return 'SoulSync 已连接。使用 soulsync_status 查看状态。';
|
|
410
|
+
}
|
|
356
411
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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);
|
|
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.';
|
|
377
424
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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));
|
|
425
|
+
|
|
426
|
+
if (!pythonProcess) {
|
|
427
|
+
startPythonService('--sync');
|
|
428
|
+
return 'Sync triggered.';
|
|
388
429
|
}
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
});
|
|
430
|
+
return 'Sync already in progress.';
|
|
431
|
+
});
|
|
403
432
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
+
}
|
|
415
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();
|
|
416
473
|
if (!cfg || !cfg.token) {
|
|
417
474
|
return 'Not logged in / 未登录';
|
|
418
475
|
}
|
|
@@ -421,9 +478,7 @@ module.exports = function register(api) {
|
|
|
421
478
|
const result = await makeRequest('GET', '/api/devices', null, cfg.token);
|
|
422
479
|
if (result.status === 200) {
|
|
423
480
|
const devices = result.body.devices || [];
|
|
424
|
-
if (devices.length === 0)
|
|
425
|
-
return 'No devices found / 未找到设备';
|
|
426
|
-
}
|
|
481
|
+
if (devices.length === 0) return 'No devices found / 未找到设备';
|
|
427
482
|
const list = devices.map(d =>
|
|
428
483
|
`- ${d.device_name || 'Unnamed'} (${d.device_type || 'local'}) - Last sync: ${d.last_sync_at || 'Never'}`
|
|
429
484
|
).join('\n');
|
|
@@ -433,23 +488,20 @@ module.exports = function register(api) {
|
|
|
433
488
|
} catch (e) {
|
|
434
489
|
return `Error: ${e.message}`;
|
|
435
490
|
}
|
|
436
|
-
}
|
|
437
|
-
});
|
|
491
|
+
});
|
|
438
492
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
async
|
|
450
|
-
const
|
|
451
|
-
const cfg = loadConfig(pluginDir);
|
|
452
|
-
|
|
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();
|
|
453
505
|
if (!cfg || !cfg.token || !cfg.device_id) {
|
|
454
506
|
return 'Not logged in / 未登录';
|
|
455
507
|
}
|
|
@@ -457,6 +509,7 @@ module.exports = function register(api) {
|
|
|
457
509
|
try {
|
|
458
510
|
const result = await makeRequest('PUT', `/api/devices/${cfg.device_id}`, { device_name: name }, cfg.token);
|
|
459
511
|
if (result.status === 200) {
|
|
512
|
+
const pluginDir = getPluginDir();
|
|
460
513
|
const configPath = path.join(pluginDir, 'config.json');
|
|
461
514
|
cfg.device_name = name;
|
|
462
515
|
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
|
|
@@ -466,30 +519,42 @@ module.exports = function register(api) {
|
|
|
466
519
|
} catch (e) {
|
|
467
520
|
return `Error: ${e.message}`;
|
|
468
521
|
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
472
525
|
api.registerCli(
|
|
473
526
|
({ program }) => {
|
|
474
527
|
program
|
|
475
528
|
.command('soulsync:start')
|
|
476
529
|
.description('启动 SoulSync 同步服务')
|
|
477
|
-
.action(() => {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (!checkConfigExists(pluginDir)) {
|
|
530
|
+
.action(async () => {
|
|
531
|
+
if (!isAuthenticated()) {
|
|
481
532
|
console.log('[SoulSync] Not configured. Starting device authorization flow...');
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
533
|
+
|
|
534
|
+
const authMode = detectAuthMode(null);
|
|
535
|
+
let result;
|
|
536
|
+
|
|
537
|
+
if (authMode === 'oauth-local') {
|
|
538
|
+
result = await startOAuthLocal();
|
|
539
|
+
} else if (authMode === 'device-code-cli') {
|
|
540
|
+
result = await startDeviceCodeCLI();
|
|
541
|
+
} else {
|
|
542
|
+
result = await startDeviceCodeFlow();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (result.success) {
|
|
546
|
+
console.log(result.message);
|
|
547
|
+
} else {
|
|
548
|
+
console.error(`[SoulSync] ${result.message}`);
|
|
549
|
+
}
|
|
485
550
|
return;
|
|
486
551
|
}
|
|
487
|
-
|
|
552
|
+
|
|
488
553
|
if (pythonProcess) {
|
|
489
554
|
console.log('[SoulSync] Service already running');
|
|
490
555
|
return;
|
|
491
556
|
}
|
|
492
|
-
|
|
557
|
+
|
|
493
558
|
startPythonService('--start');
|
|
494
559
|
});
|
|
495
560
|
},
|
|
@@ -509,12 +574,12 @@ module.exports = function register(api) {
|
|
|
509
574
|
{ commands: ['soulsync:stop'] }
|
|
510
575
|
);
|
|
511
576
|
|
|
512
|
-
|
|
513
|
-
if (checkConfigExists(pluginDir)) {
|
|
577
|
+
if (isAuthenticated()) {
|
|
514
578
|
console.log('[SoulSync] Auto-starting sync service...');
|
|
515
579
|
startPythonService('--start');
|
|
580
|
+
registerChatTools(api);
|
|
516
581
|
}
|
|
517
582
|
|
|
518
583
|
console.log('[SoulSync] Plugin loaded. Run "openclaw soulsync:start" to begin.');
|
|
519
584
|
console.log('[SoulSync] Plugin registered successfully');
|
|
520
|
-
};
|
|
585
|
+
};
|