@gloablehive/celphone-wechat-plugin 1.0.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.
Files changed (45) hide show
  1. package/INSTALL.md +231 -0
  2. package/README.md +259 -0
  3. package/dist/index-simple.js +9 -0
  4. package/dist/index.d.ts +16 -0
  5. package/dist/index.js +77 -0
  6. package/dist/mock-server.d.ts +6 -0
  7. package/dist/mock-server.js +203 -0
  8. package/dist/openclaw.plugin.json +96 -0
  9. package/dist/setup-entry.d.ts +9 -0
  10. package/dist/setup-entry.js +8 -0
  11. package/dist/src/cache/compactor.d.ts +36 -0
  12. package/dist/src/cache/compactor.js +154 -0
  13. package/dist/src/cache/extractor.d.ts +48 -0
  14. package/dist/src/cache/extractor.js +120 -0
  15. package/dist/src/cache/index.d.ts +15 -0
  16. package/dist/src/cache/index.js +16 -0
  17. package/dist/src/cache/indexer.d.ts +41 -0
  18. package/dist/src/cache/indexer.js +262 -0
  19. package/dist/src/cache/manager.d.ts +113 -0
  20. package/dist/src/cache/manager.js +271 -0
  21. package/dist/src/cache/message-queue.d.ts +59 -0
  22. package/dist/src/cache/message-queue.js +147 -0
  23. package/dist/src/cache/saas-connector.d.ts +94 -0
  24. package/dist/src/cache/saas-connector.js +289 -0
  25. package/dist/src/cache/syncer.d.ts +60 -0
  26. package/dist/src/cache/syncer.js +177 -0
  27. package/dist/src/cache/types.d.ts +198 -0
  28. package/dist/src/cache/types.js +43 -0
  29. package/dist/src/cache/writer.d.ts +81 -0
  30. package/dist/src/cache/writer.js +461 -0
  31. package/dist/src/channel.d.ts +65 -0
  32. package/dist/src/channel.js +334 -0
  33. package/dist/src/client.d.ts +280 -0
  34. package/dist/src/client.js +248 -0
  35. package/index-simple.ts +11 -0
  36. package/index.ts +89 -0
  37. package/mock-server.ts +237 -0
  38. package/openclaw.plugin.json +98 -0
  39. package/package.json +37 -0
  40. package/setup-entry.ts +10 -0
  41. package/src/channel.ts +398 -0
  42. package/src/client.ts +412 -0
  43. package/test-cache.ts +260 -0
  44. package/test-integration.ts +319 -0
  45. package/tsconfig.json +22 -0
@@ -0,0 +1,289 @@
1
+ /**
2
+ * SAAS Connector - SAAS communication + offline fallback
3
+ *
4
+ * Features:
5
+ * - Health check for SAAS connectivity
6
+ * - Online/offline status tracking
7
+ * - Fallback to local cache when offline
8
+ * - Offline timeout alerts
9
+ * - Message queue for offline storage
10
+ */
11
+ import { DEFAULT_SAAS_CONFIG, } from './types.js';
12
+ /**
13
+ * SAAS Connector class
14
+ */
15
+ export class SAASConnector {
16
+ config;
17
+ status;
18
+ statusCallbacks;
19
+ offlineTimeoutCallbacks;
20
+ healthCheckInterval;
21
+ constructor(config = {}) {
22
+ this.config = { ...DEFAULT_SAAS_CONFIG, ...config };
23
+ this.status = {
24
+ isOnline: false,
25
+ lastConnected: 0,
26
+ consecutiveFailures: 0,
27
+ offlineDuration: 0,
28
+ };
29
+ this.statusCallbacks = [];
30
+ this.offlineTimeoutCallbacks = [];
31
+ }
32
+ /**
33
+ * Start health check loop
34
+ */
35
+ startHealthCheck(intervalMs = 30000) {
36
+ if (this.healthCheckInterval) {
37
+ clearInterval(this.healthCheckInterval);
38
+ }
39
+ // Initial check
40
+ this.checkHealth();
41
+ // Periodic check
42
+ this.healthCheckInterval = setInterval(() => {
43
+ this.checkHealth();
44
+ }, intervalMs);
45
+ }
46
+ /**
47
+ * Stop health check
48
+ */
49
+ stopHealthCheck() {
50
+ if (this.healthCheckInterval) {
51
+ clearInterval(this.healthCheckInterval);
52
+ this.healthCheckInterval = null;
53
+ }
54
+ }
55
+ /**
56
+ * Check SAAS health status
57
+ */
58
+ async checkHealth() {
59
+ try {
60
+ const response = await fetch(`${this.config.apiBaseUrl}/health`, {
61
+ method: 'GET',
62
+ headers: {
63
+ 'Authorization': `Bearer ${this.config.apiKey}`,
64
+ 'Content-Type': 'application/json',
65
+ },
66
+ signal: AbortSignal.timeout(this.config.timeout),
67
+ });
68
+ const wasOnline = this.status.isOnline;
69
+ this.status.isOnline = response.ok;
70
+ this.status.lastConnected = response.ok ? Date.now() : this.status.lastConnected;
71
+ this.status.consecutiveFailures = response.ok ? 0 : this.status.consecutiveFailures + 1;
72
+ // Calculate offline duration
73
+ if (!this.status.isOnline && this.status.lastConnected > 0) {
74
+ this.status.offlineDuration = Date.now() - this.status.lastConnected;
75
+ }
76
+ else if (this.status.isOnline) {
77
+ this.status.offlineDuration = 0;
78
+ }
79
+ // Notify status change
80
+ if (wasOnline !== this.status.isOnline) {
81
+ this.notifyStatusChange();
82
+ }
83
+ // Check offline timeout
84
+ if (this.status.offlineDuration > this.config.offlineThreshold) {
85
+ this.notifyOfflineTimeout();
86
+ }
87
+ return response.ok;
88
+ }
89
+ catch (error) {
90
+ const wasOnline = this.status.isOnline;
91
+ this.status.isOnline = false;
92
+ this.status.consecutiveFailures++;
93
+ this.status.offlineDuration = this.status.lastConnected > 0
94
+ ? Date.now() - this.status.lastConnected
95
+ : this.config.offlineThreshold + 1;
96
+ if (wasOnline) {
97
+ this.notifyStatusChange();
98
+ }
99
+ if (this.status.offlineDuration > this.config.offlineThreshold) {
100
+ this.notifyOfflineTimeout();
101
+ }
102
+ return false;
103
+ }
104
+ }
105
+ /**
106
+ * Get current connection status
107
+ */
108
+ getConnectionStatus() {
109
+ return { ...this.status };
110
+ }
111
+ /**
112
+ * Check if online
113
+ */
114
+ isOnline() {
115
+ return this.status.isOnline;
116
+ }
117
+ // ========== Online: SAAS API calls ==========
118
+ /**
119
+ * Query user profile from SAAS
120
+ */
121
+ async queryUserProfile(userId) {
122
+ if (!this.status.isOnline) {
123
+ throw new Error('SAAS is offline');
124
+ }
125
+ try {
126
+ const response = await fetch(`${this.config.apiBaseUrl}/users/${userId}`, {
127
+ method: 'GET',
128
+ headers: {
129
+ 'Authorization': `Bearer ${this.config.apiKey}`,
130
+ 'Content-Type': 'application/json',
131
+ },
132
+ signal: AbortSignal.timeout(this.config.timeout),
133
+ });
134
+ if (!response.ok) {
135
+ throw new Error(`SAAS API error: ${response.status}`);
136
+ }
137
+ return await response.json();
138
+ }
139
+ catch (error) {
140
+ this.status.consecutiveFailures++;
141
+ throw error;
142
+ }
143
+ }
144
+ /**
145
+ * Query order from SAAS
146
+ */
147
+ async queryOrder(orderId) {
148
+ if (!this.status.isOnline) {
149
+ throw new Error('SAAS is offline');
150
+ }
151
+ try {
152
+ const response = await fetch(`${this.config.apiBaseUrl}/orders/${orderId}`, {
153
+ method: 'GET',
154
+ headers: {
155
+ 'Authorization': `Bearer ${this.config.apiKey}`,
156
+ 'Content-Type': 'application/json',
157
+ },
158
+ signal: AbortSignal.timeout(this.config.timeout),
159
+ });
160
+ if (!response.ok) {
161
+ throw new Error(`SAAS API error: ${response.status}`);
162
+ }
163
+ return await response.json();
164
+ }
165
+ catch (error) {
166
+ this.status.consecutiveFailures++;
167
+ throw error;
168
+ }
169
+ }
170
+ /**
171
+ * Sync message to SAAS
172
+ */
173
+ async syncMessage(msg) {
174
+ if (!this.status.isOnline) {
175
+ throw new Error('SAAS is offline');
176
+ }
177
+ try {
178
+ const response = await fetch(`${this.config.apiBaseUrl}/messages`, {
179
+ method: 'POST',
180
+ headers: {
181
+ 'Authorization': `Bearer ${this.config.apiKey}`,
182
+ 'Content-Type': 'application/json',
183
+ },
184
+ body: JSON.stringify(msg),
185
+ signal: AbortSignal.timeout(this.config.timeout),
186
+ });
187
+ if (!response.ok) {
188
+ throw new Error(`SAAS API error: ${response.status}`);
189
+ }
190
+ }
191
+ catch (error) {
192
+ this.status.consecutiveFailures++;
193
+ throw error;
194
+ }
195
+ }
196
+ // ========== Offline: Fallback to local cache ==========
197
+ /**
198
+ * Query user profile from local cache (fallback)
199
+ */
200
+ async queryUserProfileOffline(userId, localCacheGetter) {
201
+ if (this.status.isOnline) {
202
+ // Online: try SAAS first, fallback to local
203
+ try {
204
+ return await this.queryUserProfile(userId);
205
+ }
206
+ catch {
207
+ // Fallback to local
208
+ return await localCacheGetter(userId);
209
+ }
210
+ }
211
+ // Offline: use local cache
212
+ return await localCacheGetter(userId);
213
+ }
214
+ /**
215
+ * Query order from local cache (fallback)
216
+ */
217
+ async queryOrderOffline(orderId, localCacheGetter) {
218
+ if (this.status.isOnline) {
219
+ try {
220
+ return await this.queryOrder(orderId);
221
+ }
222
+ catch {
223
+ return await localCacheGetter(orderId);
224
+ }
225
+ }
226
+ return await localCacheGetter(orderId);
227
+ }
228
+ /**
229
+ * Get offline message for user
230
+ */
231
+ getOfflineMessage() {
232
+ if (this.status.offlineDuration > this.config.offlineThreshold) {
233
+ return '抱歉,系统当前无法连接到后台服务,部分功能可能不可用。请稍后再试。';
234
+ }
235
+ return '您好,当前服务响应较慢,请稍等片刻。';
236
+ }
237
+ // ========== Event Callbacks ==========
238
+ /**
239
+ * Register status change callback
240
+ */
241
+ onStatusChange(callback) {
242
+ this.statusCallbacks.push(callback);
243
+ }
244
+ /**
245
+ * Register offline timeout callback
246
+ */
247
+ onOfflineTimeout(callback) {
248
+ this.offlineTimeoutCallbacks.push(callback);
249
+ }
250
+ /**
251
+ * Notify status change
252
+ */
253
+ notifyStatusChange() {
254
+ for (const callback of this.statusCallbacks) {
255
+ try {
256
+ callback(this.status);
257
+ }
258
+ catch (error) {
259
+ console.error('[SAASConnector] Status callback error:', error);
260
+ }
261
+ }
262
+ }
263
+ /**
264
+ * Notify offline timeout
265
+ */
266
+ notifyOfflineTimeout() {
267
+ for (const callback of this.offlineTimeoutCallbacks) {
268
+ try {
269
+ callback();
270
+ }
271
+ catch (error) {
272
+ console.error('[SAASConnector] Offline timeout callback error:', error);
273
+ }
274
+ }
275
+ }
276
+ /**
277
+ * Update config
278
+ */
279
+ updateConfig(config) {
280
+ this.config = { ...this.config, ...config };
281
+ }
282
+ }
283
+ /**
284
+ * Create SAAS connector instance
285
+ */
286
+ export function createSAASConnector(config) {
287
+ return new SAASConnector(config);
288
+ }
289
+ export { DEFAULT_SAAS_CONFIG };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Cloud Syncer - Sync local cache to cloud database
3
+ *
4
+ * Local-first + periodic sync strategy:
5
+ * - Incremental sync based on lastSyncTimestamp
6
+ * - Full sync for initialization/recovery
7
+ */
8
+ import { SyncConfig, SyncResult, SyncLog, WeChatMessage, UserProfile } from './types.js';
9
+ /**
10
+ * Cloud Syncer class
11
+ */
12
+ export declare class CloudSyncer {
13
+ private config;
14
+ private syncLogPath;
15
+ constructor(config: SyncConfig, syncLogPath: string);
16
+ /**
17
+ * Check if sync should run
18
+ */
19
+ shouldSync(): boolean;
20
+ /**
21
+ * Incremental sync - only sync new messages
22
+ */
23
+ syncIncremental(): Promise<SyncResult>;
24
+ /**
25
+ * Full sync - sync all data
26
+ */
27
+ syncFull(): Promise<SyncResult>;
28
+ /**
29
+ * Sync single message
30
+ */
31
+ syncMessage(message: WeChatMessage): Promise<void>;
32
+ /**
33
+ * Sync user profile
34
+ */
35
+ syncProfile(profile: UserProfile): Promise<void>;
36
+ /**
37
+ * Get unsynced messages from local cache
38
+ */
39
+ private getUnsyncedMessages;
40
+ /**
41
+ * Sync single record to database
42
+ */
43
+ private syncRecord;
44
+ /**
45
+ * Write sync log
46
+ */
47
+ private writeSyncLog;
48
+ /**
49
+ * Get sync logs
50
+ */
51
+ getSyncLogs(limit?: number): Promise<SyncLog[]>;
52
+ /**
53
+ * Test database connection
54
+ */
55
+ testConnection(): Promise<boolean>;
56
+ }
57
+ /**
58
+ * Create syncer instance
59
+ */
60
+ export declare function createSyncer(config: SyncConfig, syncLogPath: string): CloudSyncer;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Cloud Syncer - Sync local cache to cloud database
3
+ *
4
+ * Local-first + periodic sync strategy:
5
+ * - Incremental sync based on lastSyncTimestamp
6
+ * - Full sync for initialization/recovery
7
+ */
8
+ const ENCODING = 'utf-8';
9
+ /**
10
+ * Cloud Syncer class
11
+ */
12
+ export class CloudSyncer {
13
+ config;
14
+ syncLogPath;
15
+ constructor(config, syncLogPath) {
16
+ this.config = config;
17
+ this.syncLogPath = syncLogPath;
18
+ }
19
+ /**
20
+ * Check if sync should run
21
+ */
22
+ shouldSync() {
23
+ if (this.config.syncMode === 'realtime')
24
+ return true;
25
+ if (this.config.syncMode === 'manual')
26
+ return false;
27
+ const now = Date.now();
28
+ return now - this.config.lastSyncTimestamp > this.config.syncIntervalMs;
29
+ }
30
+ /**
31
+ * Incremental sync - only sync new messages
32
+ */
33
+ async syncIncremental() {
34
+ const startTime = Date.now();
35
+ const log = {
36
+ id: `sync-${startTime}`,
37
+ syncType: 'incremental',
38
+ startedAt: startTime,
39
+ recordsCount: 0,
40
+ status: 'pending',
41
+ };
42
+ try {
43
+ // In real implementation, this would:
44
+ // 1. Read local messages since lastSyncTimestamp
45
+ // 2. Batch insert/update to database
46
+ // 3. Update lastSyncTimestamp
47
+ console.log('[Syncer] Running incremental sync since', new Date(this.config.lastSyncTimestamp));
48
+ // Placeholder: would query database
49
+ const records = await this.getUnsyncedMessages();
50
+ for (const record of records) {
51
+ await this.syncRecord(record);
52
+ log.recordsCount++;
53
+ }
54
+ // Update last sync timestamp
55
+ this.config.lastSyncTimestamp = startTime;
56
+ log.status = 'completed';
57
+ log.completedAt = Date.now();
58
+ await this.writeSyncLog(log);
59
+ return {
60
+ recordsSynced: log.recordsCount,
61
+ duration: log.completedAt - log.startedAt,
62
+ errors: [],
63
+ };
64
+ }
65
+ catch (error) {
66
+ log.status = 'failed';
67
+ log.error = error.message;
68
+ log.completedAt = Date.now();
69
+ await this.writeSyncLog(log);
70
+ return {
71
+ recordsSynced: log.recordsCount,
72
+ duration: log.completedAt - log.startedAt,
73
+ errors: [error.message],
74
+ };
75
+ }
76
+ }
77
+ /**
78
+ * Full sync - sync all data
79
+ */
80
+ async syncFull() {
81
+ const startTime = Date.now();
82
+ const log = {
83
+ id: `sync-full-${startTime}`,
84
+ syncType: 'full',
85
+ startedAt: startTime,
86
+ recordsCount: 0,
87
+ status: 'pending',
88
+ };
89
+ try {
90
+ // Would sync all messages, profiles, etc.
91
+ console.log('[Syncer] Running full sync');
92
+ // Placeholder implementation
93
+ log.status = 'completed';
94
+ log.completedAt = Date.now();
95
+ await this.writeSyncLog(log);
96
+ return {
97
+ recordsSynced: log.recordsCount,
98
+ duration: log.completedAt - log.startedAt,
99
+ errors: [],
100
+ };
101
+ }
102
+ catch (error) {
103
+ log.status = 'failed';
104
+ log.error = error.message;
105
+ log.completedAt = Date.now();
106
+ await this.writeSyncLog(log);
107
+ return {
108
+ recordsSynced: log.recordsCount,
109
+ duration: log.completedAt - log.startedAt,
110
+ errors: [error.message],
111
+ };
112
+ }
113
+ }
114
+ /**
115
+ * Sync single message
116
+ */
117
+ async syncMessage(message) {
118
+ // In real implementation, would insert into database
119
+ console.log('[Syncer] Syncing message:', message.messageId);
120
+ }
121
+ /**
122
+ * Sync user profile
123
+ */
124
+ async syncProfile(profile) {
125
+ // In real implementation, would upsert into database
126
+ console.log('[Syncer] Syncing profile:', profile.wechatId);
127
+ }
128
+ /**
129
+ * Get unsynced messages from local cache
130
+ */
131
+ async getUnsyncedMessages() {
132
+ // Placeholder: would read from local queue
133
+ return [];
134
+ }
135
+ /**
136
+ * Sync single record to database
137
+ */
138
+ async syncRecord(record) {
139
+ // Placeholder: would call database API
140
+ }
141
+ /**
142
+ * Write sync log
143
+ */
144
+ async writeSyncLog(log) {
145
+ // Placeholder: would write to sync-logs directory
146
+ console.log('[Syncer] Sync log:', log.id, log.status, log.recordsCount);
147
+ }
148
+ /**
149
+ * Get sync logs
150
+ */
151
+ async getSyncLogs(limit = 10) {
152
+ // Placeholder: would read from sync-logs directory
153
+ return [];
154
+ }
155
+ /**
156
+ * Test database connection
157
+ */
158
+ async testConnection() {
159
+ // Placeholder: would test database connectivity
160
+ try {
161
+ // In real implementation:
162
+ // const client = new Client({ connectionString: this.config.databaseUrl });
163
+ // await client.connect();
164
+ // await client.end();
165
+ return true;
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ }
172
+ /**
173
+ * Create syncer instance
174
+ */
175
+ export function createSyncer(config, syncLogPath) {
176
+ return new CloudSyncer(config, syncLogPath);
177
+ }