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/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
- """加载或生成设备ID"""
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,29 @@ 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
- """下载记忆
141
+ def get_profile(self) -> dict:
142
+ """获取用户信息
160
143
 
161
144
  Returns:
162
- 包含 content, version 等信息的字典
145
+ 用户信息
163
146
  """
164
- url = f"{self.cloud_url}/api/memories"
147
+ url = f"{self.cloud_url}/api/profiles"
165
148
 
166
149
  response = self.session.get(url, headers=self._get_headers())
167
150
 
168
151
  if response.status_code == 200:
169
152
  return response.json()
170
- elif response.status_code == 404:
171
- return {'content': '', 'version': 0}
172
153
  else:
173
154
  error = response.json().get('error', 'Unknown error')
174
- raise Exception(f"Download failed: {error}")
175
-
176
- def get_profile(self) -> dict:
177
- """获取用户信息
155
+ raise Exception(f"Get profile failed: {error}")
156
+
157
+ def get_user_info(self) -> dict:
158
+ """获取当前登录用户信息
178
159
 
179
160
  Returns:
180
- 用户信息
161
+ 用户信息,包含 email, name, subscription_status
181
162
  """
182
- url = f"{self.cloud_url}/api/memories/profile"
163
+ url = f"{self.cloud_url}/api/auth/user/info"
183
164
 
184
165
  response = self.session.get(url, headers=self._get_headers())
185
166
 
@@ -187,7 +168,7 @@ class OpenClawClient:
187
168
  return response.json()
188
169
  else:
189
170
  error = response.json().get('error', 'Unknown error')
190
- raise Exception(f"Get profile failed: {error}")
171
+ raise Exception(f"Get user info failed: {error}")
191
172
 
192
173
  def connect_websocket(self, on_message_callback):
193
174
  """连接 WebSocket
@@ -284,6 +265,8 @@ class OpenClawClient:
284
265
  self.token = result.get('token')
285
266
  self.user_id = result.get('user_id')
286
267
  self._save_token(self.token)
268
+ if self.config:
269
+ self.config['token'] = self.token
287
270
  return result
288
271
  else:
289
272
  error = response.json().get('error', 'Unknown error')
@@ -309,6 +292,8 @@ class OpenClawClient:
309
292
  self.token = result.get('token')
310
293
  self.user_id = result.get('user_id')
311
294
  self._save_token(self.token)
295
+ if self.config:
296
+ self.config['token'] = self.token
312
297
  return result
313
298
  else:
314
299
  error = response.json().get('error', 'Unknown error')
package/src/config.js ADDED
@@ -0,0 +1,100 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ let config = null;
6
+
7
+ function getPluginDir() {
8
+ return path.dirname(__filename);
9
+ }
10
+
11
+ function loadConfig() {
12
+ if (config) return config;
13
+
14
+ const pluginDir = path.dirname(__filename);
15
+ const configPath = path.join(pluginDir, 'config.json');
16
+
17
+ try {
18
+ if (fs.existsSync(configPath)) {
19
+ config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
20
+ return config;
21
+ }
22
+ } catch (e) {
23
+ console.error('[SoulSync] Error loading config:', e);
24
+ }
25
+ return null;
26
+ }
27
+
28
+ function saveConfig(newConfig) {
29
+ const pluginDir = path.dirname(__filename);
30
+ const configPath = path.join(pluginDir, 'config.json');
31
+
32
+ try {
33
+ let existing = {};
34
+ if (fs.existsSync(configPath)) {
35
+ existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
36
+ }
37
+ config = { ...existing, ...newConfig };
38
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
39
+ return true;
40
+ } catch (e) {
41
+ console.error('[SoulSync] Error saving config:', e);
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function clearConfig() {
47
+ const pluginDir = path.dirname(__filename);
48
+ const configPath = path.join(pluginDir, 'config.json');
49
+
50
+ try {
51
+ if (fs.existsSync(configPath)) {
52
+ const existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
53
+ delete existing.token;
54
+ delete existing.email;
55
+ delete existing.device_id;
56
+ delete existing.device_name;
57
+ fs.writeFileSync(configPath, JSON.stringify(existing, null, 2));
58
+ config = existing;
59
+ }
60
+ return true;
61
+ } catch (e) {
62
+ console.error('[SoulSync] Error clearing config:', e);
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function getDeviceName(deviceType = 'local') {
68
+ const hostname = os.hostname();
69
+ const platform = os.platform();
70
+ const username = os.userInfo().username;
71
+
72
+ let type = 'local';
73
+ if (deviceType === 'cloud') type = '云端';
74
+ else if (deviceType === 'ssh') type = 'SSH';
75
+ else {
76
+ if (platform === 'win32') type = 'Windows';
77
+ else if (platform === 'darwin') type = 'Mac';
78
+ else if (platform === 'linux') type = 'Linux';
79
+ }
80
+
81
+ if (deviceType === 'cloud') {
82
+ return `${type} Bot`;
83
+ }
84
+
85
+ return `${username}的${type}`;
86
+ }
87
+
88
+ function isAuthenticated() {
89
+ const cfg = loadConfig();
90
+ return cfg && cfg.token && cfg.email && cfg.email !== 'your-email@example.com';
91
+ }
92
+
93
+ module.exports = {
94
+ loadConfig,
95
+ saveConfig,
96
+ clearConfig,
97
+ getDeviceName,
98
+ isAuthenticated,
99
+ getPluginDir
100
+ };
@@ -0,0 +1,150 @@
1
+ const https = require('https');
2
+ const http = require('http');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ let pollingInterval = null;
8
+
9
+ function getCloudUrl() {
10
+ const pluginDir = path.dirname(__filename);
11
+ const configPath = path.join(pluginDir, 'config.json');
12
+ try {
13
+ if (fs.existsSync(configPath)) {
14
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
15
+ if (cfg.cloud_url) return cfg.cloud_url;
16
+ }
17
+ } catch (e) {}
18
+ return 'https://soulsync.work';
19
+ }
20
+
21
+ function makeRequest(method, urlPath, body = null) {
22
+ return new Promise((resolve, reject) => {
23
+ const cloudUrl = getCloudUrl();
24
+ const isHttps = cloudUrl.startsWith('https');
25
+ const client = isHttps ? https : http;
26
+
27
+ const urlObj = new URL(cloudUrl + urlPath);
28
+
29
+ const options = {
30
+ hostname: urlObj.hostname,
31
+ port: urlObj.port || (isHttps ? 443 : 80),
32
+ path: urlObj.pathname + urlObj.search,
33
+ method: method,
34
+ headers: { 'Content-Type': 'application/json' }
35
+ };
36
+
37
+ const req = client.request(options, (res) => {
38
+ let data = '';
39
+ res.on('data', chunk => data += chunk);
40
+ res.on('end', () => {
41
+ try {
42
+ const json = JSON.parse(data);
43
+ resolve({ status: res.statusCode, body: json });
44
+ } catch (e) {
45
+ resolve({ status: res.statusCode, body: data });
46
+ }
47
+ });
48
+ });
49
+
50
+ req.on('error', reject);
51
+ if (body) req.write(JSON.stringify(body));
52
+ req.end();
53
+ });
54
+ }
55
+
56
+ function requestDeviceCode() {
57
+ return makeRequest('POST', '/api/auth/device-code');
58
+ }
59
+
60
+ function pollForAuth(deviceCode) {
61
+ return makeRequest('GET', `/api/auth/device/${deviceCode}/status`);
62
+ }
63
+
64
+ function startPolling(deviceCode, onSuccess, onError) {
65
+ pollingInterval = setInterval(async () => {
66
+ try {
67
+ const result = await pollForAuth(deviceCode);
68
+
69
+ if (result.body.status === 'authorized' && result.body.token) {
70
+ stopPolling();
71
+ onSuccess(result.body.token);
72
+ return;
73
+ }
74
+
75
+ if (result.body.status === 'expired') {
76
+ stopPolling();
77
+ onError(new Error('Device code expired'));
78
+ return;
79
+ }
80
+
81
+ if (result.body.status === 'not_found') {
82
+ stopPolling();
83
+ onError(new Error('Device code not found'));
84
+ return;
85
+ }
86
+ } catch (e) {
87
+ stopPolling();
88
+ onError(e);
89
+ }
90
+ }, 3000);
91
+ }
92
+
93
+ function stopPolling() {
94
+ if (pollingInterval) {
95
+ clearInterval(pollingInterval);
96
+ pollingInterval = null;
97
+ }
98
+ }
99
+
100
+ function savePendingAuth(deviceCode, authUrl) {
101
+ const homeDir = os.homedir();
102
+ const pendingDir = path.join(homeDir, '.soulsync');
103
+ const pendingFile = path.join(pendingDir, '.pending_auth');
104
+
105
+ try {
106
+ if (!fs.existsSync(pendingDir)) {
107
+ fs.mkdirSync(pendingDir, { recursive: true });
108
+ }
109
+ fs.writeFileSync(pendingFile, JSON.stringify({ deviceCode, authUrl, timestamp: Date.now() }));
110
+ return true;
111
+ } catch (e) {
112
+ console.error('[SoulSync] Error saving pending auth:', e);
113
+ return false;
114
+ }
115
+ }
116
+
117
+ function loadPendingAuth() {
118
+ const homeDir = os.homedir();
119
+ const pendingFile = path.join(homeDir, '.soulsync', '.pending_auth');
120
+
121
+ try {
122
+ if (fs.existsSync(pendingFile)) {
123
+ const data = JSON.parse(fs.readFileSync(pendingFile, 'utf-8'));
124
+ if (Date.now() - data.timestamp < 900000) {
125
+ return data;
126
+ }
127
+ fs.unlinkSync(pendingFile);
128
+ }
129
+ } catch (e) {}
130
+ return null;
131
+ }
132
+
133
+ function clearPendingAuth() {
134
+ const homeDir = os.homedir();
135
+ const pendingFile = path.join(homeDir, '.soulsync', '.pending_auth');
136
+ try {
137
+ if (fs.existsSync(pendingFile)) {
138
+ fs.unlinkSync(pendingFile);
139
+ }
140
+ } catch (e) {}
141
+ }
142
+
143
+ module.exports = {
144
+ requestDeviceCode,
145
+ startPolling,
146
+ stopPolling,
147
+ savePendingAuth,
148
+ loadPendingAuth,
149
+ clearPendingAuth
150
+ };
package/src/main.py CHANGED
@@ -61,7 +61,7 @@ class SoulSyncPlugin:
61
61
 
62
62
  cloud_url = self.config.get('cloud_url', '').strip()
63
63
  email = self.config.get('email', '').strip()
64
- password = self.config.get('password', '').strip()
64
+ token = self.config.get('token', '').strip()
65
65
 
66
66
  if not cloud_url:
67
67
  self.config['cloud_url'] = 'https://soulsync.work'
@@ -98,11 +98,6 @@ class SoulSyncPlugin:
98
98
  elif 'email' in auth_result:
99
99
  config['email'] = auth_result.get('email', '')
100
100
 
101
- # 保存 password(注册时输入的密码,需要保存以便自动登录)
102
- # 注意:登录时我们只有从 config 读取的 password
103
- if 'password' in auth_result:
104
- config['password'] = auth_result.get('password', '')
105
-
106
101
  # 保存 token
107
102
  if 'token' in auth_result:
108
103
  config['token'] = auth_result['token']
@@ -290,47 +285,32 @@ class SoulSyncPlugin:
290
285
  profile = self.client.get_profile()
291
286
  print(f"[SoulSync] Using existing token, user: {profile.get('email', 'unknown')}")
292
287
  except Exception as e:
288
+ error_str = str(e)
289
+ if '401' in error_str or 'Unauthorized' in error_str or 'token' in error_str.lower():
290
+ print("[SoulSync] Token expired or invalid. Please re-login via chat: say 'login SoulSync'")
291
+ print("[SoulSync] Token 已过期,请在聊天中说 'login SoulSync' 重新登录")
292
+ return False
293
293
  print(f"[SoulSync] Token invalid, re-authenticating: {e}")
294
294
  token = None
295
295
 
296
296
  if not token:
297
- email = self.config.get('email', '').strip()
298
- password = self.config.get('password', '').strip()
299
-
300
- print("[SoulSync] No valid token, attempting auto-login...")
301
- try:
302
- result = self.client.authenticate(email, password)
303
- if result:
304
- result['email'] = email
305
- result['password'] = password
306
- print("[SoulSync] Login successful! / 登录成功!")
307
- self._save_auth_to_config(result)
308
- token = self.client.token
309
- except Exception as e:
310
- error_msg = str(e)
311
-
312
- if "429" in error_msg or "too many" in error_msg.lower():
313
- print("\n[SoulSync] ========================================")
314
- print(f"[SoulSync] ❌ {e}")
315
- print("[SoulSync] Too many failed attempts. Please try again later / 登录失败次数过多,请稍后再试")
316
- print("[SoulSync] ========================================\n")
317
- else:
318
- print("\n[SoulSync] ========================================")
319
- print(f"[SoulSync] ❌ {e}")
320
- print("[SoulSync] Please run 'openclaw soulsync:setup' to reconfigure / 请运行 'openclaw soulsync:setup' 重新配置")
321
- print("[SoulSync] ========================================\n")
322
-
323
- sys.exit(0)
324
-
297
+ print("[SoulSync] Token expired or invalid. Please re-configure using 'openclaw soulsync:setup'")
298
+ print("[SoulSync] Token 已过期,请重新运行 'openclaw soulsync:setup' 配置")
299
+ return False
300
+
325
301
  email = self.config.get('email')
326
- password = self.config.get('password')
327
-
302
+
328
303
  try:
329
304
  profile = self.client.get_profile()
330
305
  print(f"\n[SoulSync] Logged in as: {profile.get('email')}")
331
306
  subscription = profile.get('subscription', {})
332
307
  print(f"[SoulSync] Subscription: {subscription.get('status')} (days remaining: {subscription.get('daysRemaining', 0)})\n")
333
308
  except Exception as e:
309
+ error_str = str(e)
310
+ if '401' in error_str or 'Unauthorized' in error_str or 'token' in error_str.lower():
311
+ print("[SoulSync] Token expired or invalid. Please re-login via chat: say 'login SoulSync'")
312
+ print("[SoulSync] Token 已过期,请在聊天中说 'login SoulSync' 重新登录")
313
+ return False
334
314
  print(f"[SoulSync] Warning: Could not get profile: {e}")
335
315
 
336
316
  # 版本管理器
@@ -391,20 +371,12 @@ class SoulSyncPlugin:
391
371
  """WebSocket 消息回调"""
392
372
  event = data.get('event')
393
373
 
394
- if event == 'file_updated':
395
- file_path = data.get('file_path')
374
+ if event == 'profile:updated':
375
+ user_id = data.get('user_id')
396
376
  version = data.get('version')
397
- print(f"\n[SoulSync] [WebSocket] File updated: {file_path} (v{version})")
398
- try:
399
- self.profile_sync.on_remote_change(file_path, version)
400
- except Exception as e:
401
- print(f"[SoulSync] Sync error: {e}")
402
-
403
- elif event == 'new_memory':
404
- print(f"\n[SoulSync] [WebSocket] New memory available!")
377
+ print(f"\n[SoulSync] [WebSocket] Profile updated (v{version})")
405
378
  try:
406
379
  self.profile_sync.pull_all()
407
- print("[SoulSync] Memory synced from remote")
408
380
  except Exception as e:
409
381
  print(f"[SoulSync] Sync error: {e}")
410
382
 
@@ -489,9 +461,9 @@ def main():
489
461
  plugin.load_config()
490
462
 
491
463
  email = plugin.config.get('email', '').strip()
492
- password = plugin.config.get('password', '').strip()
464
+ token = plugin.config.get('token', '').strip()
493
465
 
494
- if not email or not password:
466
+ if not email or not token:
495
467
  print("\n[SoulSync] ========================================")
496
468
  print("[SoulSync] Not configured. Run 'openclaw soulsync:setup' first.")
497
469
  print("[SoulSync] 尚未配置,请先运行 'openclaw soulsync:setup'")
@@ -0,0 +1,48 @@
1
+ const http = require('http');
2
+ const url = require('url');
3
+
4
+ function createOAuthServer() {
5
+ return new Promise((resolve, reject) => {
6
+ const server = http.createServer((req, res) => {
7
+ const parsedUrl = url.parse(req.url, true);
8
+
9
+ if (parsedUrl.pathname === '/callback') {
10
+ const { token, state, error } = parsedUrl.query;
11
+
12
+ if (error) {
13
+ res.writeHead(400, { 'Content-Type': 'text/html' });
14
+ res.end('<html><body><h1>授权失败</h1><p>请关闭此窗口并重试。</p></body></html>');
15
+ server.close();
16
+ reject(new Error(error));
17
+ return;
18
+ }
19
+
20
+ if (token) {
21
+ res.writeHead(200, { 'Content-Type': 'text/html' });
22
+ res.end('<html><body><h1>授权成功!</h1><p>请关闭此窗口并返回终端。</p></body></html>');
23
+ server.close();
24
+ resolve({ token, state });
25
+ return;
26
+ }
27
+
28
+ res.writeHead(400, { 'Content-Type': 'text/html' });
29
+ res.end('<html><body><h1>无效响应</h1></body></html>');
30
+ server.close();
31
+ reject(new Error('No token received'));
32
+ return;
33
+ }
34
+
35
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
36
+ res.end('Not Found');
37
+ });
38
+
39
+ server.listen(0, () => {
40
+ const port = server.address().port;
41
+ resolve({ server, port });
42
+ });
43
+
44
+ server.on('error', reject);
45
+ });
46
+ }
47
+
48
+ module.exports = { createOAuthServer };
package/src/profiles.py CHANGED
@@ -1,12 +1,19 @@
1
1
  import requests
2
+ import sys
3
+ import os
4
+
5
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
6
+ from src.client import TLSAdapter
2
7
 
3
8
 
4
9
  class ProfilesClient:
5
- """Profiles API 客户端"""
10
+ """Profiles API 客户端 - 统一同步接口"""
6
11
 
7
12
  def __init__(self, cloud_url: str, token: str = None):
8
13
  self.cloud_url = cloud_url.rstrip('/')
9
14
  self.token = token
15
+ self.session = requests.Session()
16
+ self.session.mount('https://', TLSAdapter())
10
17
 
11
18
  def _get_headers(self) -> dict:
12
19
  """获取请求头"""
@@ -19,57 +26,50 @@ class ProfilesClient:
19
26
  """设置 token"""
20
27
  self.token = token
21
28
 
22
- def get_profiles(self, path: str = None) -> dict:
23
- """获取 profiles
29
+ def get_profiles(self) -> dict:
30
+ """获取用户的完整 profiles
24
31
 
25
- Args:
26
- path: 可选的文件路径
27
-
28
32
  Returns:
29
- 包含 files 列表的字典
33
+ 包含 content (dict), version, updated_at 的字典
34
+ 示例: {"content": {"SOUL.md": "...", "USER.md": "..."}, "version": 3, "updated_at": "..."}
30
35
  """
31
36
  url = f"{self.cloud_url}/api/profiles"
32
- if path:
33
- url += f"?path={path}"
34
37
 
35
- response = requests.get(url, headers=self._get_headers())
38
+ response = self.session.get(url, headers=self._get_headers())
36
39
 
37
40
  if response.status_code == 200:
38
41
  return response.json()
39
- elif response.status_code == 404:
40
- return {'files': []}
41
42
  else:
42
43
  error = response.json().get('error', 'Unknown error')
43
44
  raise Exception(f"Get profiles failed: {error}")
44
45
 
45
- def upload_profile(self, file_path: str, content: str, version: int) -> dict:
46
- """上传 profile
46
+ def upload_profiles(self, content: dict, version: int) -> dict:
47
+ """整体替换用户的 profiles
47
48
 
48
49
  Args:
49
- file_path: 文件路径
50
- content: 文件内容
51
- version: 当前版本号
50
+ content: 包含所有文件的 dict,key 是文件名,value 是内容
51
+ 示例: {"SOUL.md": "...", "USER.md": "...", "MEMORY.md": "..."}
52
+ version: 客户端当前持有的版本号
52
53
 
53
54
  Returns:
54
- 成功时返回 {file_path, version, updated_at}
55
- 冲突时抛出异常
55
+ 成功时返回 {"content": {...}, "version": N, "updated_at": "..."}
56
+ 冲突时抛出 ConflictError
56
57
  """
57
58
  url = f"{self.cloud_url}/api/profiles"
58
59
  data = {
59
- 'file_path': file_path,
60
60
  'content': content,
61
61
  'version': version
62
62
  }
63
63
 
64
- response = requests.post(url, json=data, headers=self._get_headers())
64
+ response = self.session.put(url, json=data, headers=self._get_headers())
65
65
 
66
66
  if response.status_code == 200:
67
67
  return response.json()
68
68
  elif response.status_code == 409:
69
69
  result = response.json()
70
70
  raise ConflictError(
71
- result.get('latest_content', ''),
72
- result.get('latest_version', 0)
71
+ server_content=result.get('server_content', {}),
72
+ server_version=result.get('server_version', 0)
73
73
  )
74
74
  elif response.status_code == 403:
75
75
  error = response.json().get('error', 'Subscription required')
@@ -77,34 +77,12 @@ class ProfilesClient:
77
77
  else:
78
78
  error = response.json().get('error', 'Unknown error')
79
79
  raise Exception(f"Upload failed: {error}")
80
-
81
- def sync_profiles(self, since: str = '0') -> dict:
82
- """增量同步 profiles
83
-
84
- Args:
85
- since: 时间戳
86
-
87
- Returns:
88
- 包含 files 列表和 server_time 的字典
89
- """
90
- url = f"{self.cloud_url}/api/profiles/sync?since={since}"
91
-
92
- response = requests.get(url, headers=self._get_headers())
93
-
94
- if response.status_code == 200:
95
- return response.json()
96
- elif response.status_code == 403:
97
- error = response.json().get('error', 'Subscription required')
98
- raise Exception(f"Sync failed: {error}")
99
- else:
100
- error = response.json().get('error', 'Unknown error')
101
- raise Exception(f"Sync failed: {error}")
102
80
 
103
81
 
104
82
  class ConflictError(Exception):
105
83
  """版本冲突异常"""
106
84
 
107
- def __init__(self, latest_content: str, latest_version: int):
108
- self.latest_content = latest_content
109
- self.latest_version = latest_version
110
- super().__init__(f"Version conflict: latest version is {latest_version}")
85
+ def __init__(self, server_content: dict, server_version: int):
86
+ self.server_content = server_content
87
+ self.server_version = server_version
88
+ super().__init__(f"Version conflict: server version is {server_version}")