@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,271 @@
1
+ /**
2
+ * Cache Manager - Main cache management
3
+ *
4
+ * Ties together all cache modules:
5
+ * - Writer (file operations)
6
+ * - Indexer (MEMORY.md)
7
+ * - Extractor (AI summary)
8
+ * - Compactor (4-layer compression)
9
+ * - Syncer (cloud sync)
10
+ * - SAAS Connector (offline fallback)
11
+ * - Message Queue (offline retry)
12
+ */
13
+ import * as fs from 'fs/promises';
14
+ import * as path from 'path';
15
+ import { DEFAULT_COMPACT_CONFIG } from './types.js';
16
+ import { DEFAULT_EXTRACTOR_CONFIG } from './extractor.js';
17
+ import { getAccountCachePath } from './types.js';
18
+ // Import all modules
19
+ import * as writer from './writer.js';
20
+ import * as indexer from './indexer.js';
21
+ import * as extractor from './extractor.js';
22
+ import * as compactor from './compactor.js';
23
+ import { CloudSyncer, createSyncer } from './syncer.js';
24
+ import { SAASConnector, createSAASConnector } from './saas-connector.js';
25
+ import { MessageQueue, createMessageQueue } from './message-queue.js';
26
+ /**
27
+ * Cache Manager class
28
+ */
29
+ export class CacheManager {
30
+ basePath;
31
+ accounts;
32
+ compactConfig;
33
+ extractorConfig;
34
+ syncer = null;
35
+ saasConnector = null;
36
+ messageQueue = null;
37
+ messageCounts = new Map();
38
+ lastExtractionCounts = new Map();
39
+ constructor(options) {
40
+ this.basePath = options.basePath;
41
+ this.accounts = options.accounts;
42
+ this.compactConfig = { ...DEFAULT_COMPACT_CONFIG, ...options.compactConfig };
43
+ this.extractorConfig = { ...DEFAULT_EXTRACTOR_CONFIG, ...options.extractorConfig };
44
+ }
45
+ /**
46
+ * Initialize cache manager
47
+ */
48
+ async init() {
49
+ // Ensure base directories exist
50
+ await fs.mkdir(this.basePath, { recursive: true });
51
+ // Initialize for each account
52
+ for (const account of this.accounts) {
53
+ const accountPath = getAccountCachePath(this.basePath, account.accountId);
54
+ await fs.mkdir(path.join(accountPath, 'friends'), { recursive: true });
55
+ await fs.mkdir(path.join(accountPath, 'chatrooms'), { recursive: true });
56
+ await fs.mkdir(path.join(accountPath, 'profiles'), { recursive: true });
57
+ await fs.mkdir(path.join(accountPath, 'logs'), { recursive: true });
58
+ }
59
+ // Initialize cloud syncer
60
+ if (this.accounts.length > 0) {
61
+ this.syncer = createSyncer({
62
+ databaseUrl: '',
63
+ syncMode: 'interval',
64
+ syncIntervalMs: 5 * 60 * 1000, // 5 minutes
65
+ lastSyncTimestamp: 0,
66
+ ...this.options?.syncConfig,
67
+ }, path.join(this.basePath, 'sync-logs'));
68
+ }
69
+ // Initialize SAAS connector
70
+ if (this.options?.saasConfig?.apiBaseUrl) {
71
+ this.saasConnector = createSAASConnector(this.options.saasConfig);
72
+ this.saasConnector.startHealthCheck();
73
+ }
74
+ // Initialize message queue
75
+ this.messageQueue = createMessageQueue(path.join(this.basePath, 'message-queue'));
76
+ await this.messageQueue.init();
77
+ // Rebuild indexes
78
+ await indexer.rebuildAllIndexes(this.basePath, this.accounts);
79
+ console.log('[CacheManager] Initialized for', this.accounts.length, 'accounts');
80
+ }
81
+ /**
82
+ * Handle incoming message
83
+ */
84
+ async onMessage(message) {
85
+ const key = `${message.accountId}:${message.conversationId}`;
86
+ // Update message count
87
+ const count = this.messageCounts.get(key) || 0;
88
+ this.messageCounts.set(key, count + 1);
89
+ // Write to conversation file
90
+ await writer.ensureConversationDir(this.basePath, message.accountId, message.conversationType, message.conversationId);
91
+ const conversationPath = writer.getConversationFilePath(this.basePath, message.accountId, message.conversationType, message.conversationId, new Date(message.timestamp));
92
+ const exists = await writer.conversationFileExists(conversationPath);
93
+ if (!exists) {
94
+ await writer.createConversationFile(conversationPath, message.conversationId, message.accountId, '', message.conversationType);
95
+ }
96
+ await writer.appendMessageToConversation(conversationPath, message);
97
+ // Check for AI extraction
98
+ await this.checkExtraction(message);
99
+ // Check for compaction
100
+ await this.checkCompaction(message);
101
+ // Queue for sync
102
+ if (this.messageQueue) {
103
+ await this.messageQueue.enqueue(message);
104
+ }
105
+ // Try to sync to SAAS if online
106
+ if (this.saasConnector?.isOnline()) {
107
+ try {
108
+ await this.saasConnector.syncMessage(message);
109
+ }
110
+ catch (error) {
111
+ console.error('[CacheManager] SAAS sync error:', error);
112
+ }
113
+ }
114
+ // Rebuild index for account
115
+ const account = this.accounts.find(a => a.accountId === message.accountId);
116
+ if (account) {
117
+ await indexer.rebuildAccountIndex(this.basePath, account);
118
+ }
119
+ }
120
+ /**
121
+ * Check if extraction should run
122
+ */
123
+ async checkExtraction(message) {
124
+ const key = `${message.accountId}:${message.conversationId}`;
125
+ const messageCount = this.messageCounts.get(key) || 0;
126
+ const lastExtraction = this.lastExtractionCounts.get(key) || 0;
127
+ if (extractor.shouldExtract(this.extractorConfig, messageCount - lastExtraction, 0)) {
128
+ // Would run extraction via LLM
129
+ console.log('[CacheManager] Should extract for', key);
130
+ // Update last extraction count
131
+ this.lastExtractionCounts.set(key, messageCount);
132
+ }
133
+ }
134
+ /**
135
+ * Check if compaction should run
136
+ */
137
+ async checkCompaction(message) {
138
+ const conversationPath = writer.getConversationFilePath(this.basePath, message.accountId, message.conversationType, message.conversationId, new Date(message.timestamp));
139
+ const tokenEstimate = await compactor.estimateConversationTokens(conversationPath);
140
+ await compactor.runCompaction(this.compactConfig, conversationPath, tokenEstimate);
141
+ }
142
+ // ========== Profile Operations ==========
143
+ /**
144
+ * Get user profile (with offline fallback)
145
+ */
146
+ async getProfile(accountId, wechatId) {
147
+ const profilePath = writer.getProfileFilePath(this.basePath, accountId, wechatId);
148
+ // Try local first
149
+ let profile = await writer.readProfile(profilePath);
150
+ // If online and no local profile, try SAAS
151
+ if (!profile && this.saasConnector?.isOnline()) {
152
+ try {
153
+ const saasProfile = await this.saasConnector.queryUserProfile(wechatId);
154
+ if (saasProfile) {
155
+ // Cache locally
156
+ await writer.writeProfile(profilePath, saasProfile);
157
+ return saasProfile;
158
+ }
159
+ }
160
+ catch {
161
+ // Fallback to local
162
+ }
163
+ }
164
+ return profile;
165
+ }
166
+ /**
167
+ * Update user profile
168
+ */
169
+ async updateProfile(accountId, wechatId, updates) {
170
+ const profilePath = writer.getProfileFilePath(this.basePath, accountId, wechatId);
171
+ await writer.updateProfileFields(profilePath, {
172
+ ...updates,
173
+ accountId,
174
+ wechatId,
175
+ });
176
+ // Rebuild index
177
+ const account = this.accounts.find(a => a.accountId === accountId);
178
+ if (account) {
179
+ await indexer.rebuildAccountIndex(this.basePath, account);
180
+ }
181
+ }
182
+ // ========== Session Memory ==========
183
+ /**
184
+ * Write session memory
185
+ */
186
+ async writeSessionMemory(accountId, conversationId, session) {
187
+ const sessionPath = writer.getSessionMemoryPath(this.basePath, accountId, conversationId);
188
+ await writer.writeSessionMemory(sessionPath, session);
189
+ }
190
+ /**
191
+ * Read session memory
192
+ */
193
+ async readSessionMemory(accountId, conversationId) {
194
+ const sessionPath = writer.getSessionMemoryPath(this.basePath, accountId, conversationId);
195
+ return await writer.readSessionMemory(sessionPath);
196
+ }
197
+ // ========== SAAS Operations ==========
198
+ /**
199
+ * Get SAAS connection status
200
+ */
201
+ getConnectionStatus() {
202
+ return this.saasConnector?.getConnectionStatus();
203
+ }
204
+ /**
205
+ * Check if SAAS is online
206
+ */
207
+ isSAASOnline() {
208
+ return this.saasConnector?.isOnline() ?? false;
209
+ }
210
+ /**
211
+ * Get offline message
212
+ */
213
+ getOfflineMessage() {
214
+ return this.saasConnector?.getOfflineMessage() ?? '服务暂时不可用';
215
+ }
216
+ // ========== Sync Operations ==========
217
+ /**
218
+ * Run sync
219
+ */
220
+ async runSync() {
221
+ if (!this.syncer || !this.syncer.shouldSync())
222
+ return;
223
+ await this.syncer.syncIncremental();
224
+ }
225
+ /**
226
+ * Process message queue
227
+ */
228
+ async processQueue() {
229
+ if (!this.messageQueue || !this.saasConnector?.isOnline())
230
+ return;
231
+ const pending = this.messageQueue.getPending();
232
+ for (const queued of pending) {
233
+ try {
234
+ await this.saasConnector.syncMessage(queued.message);
235
+ await this.messageQueue.markAsSynced(queued.id);
236
+ }
237
+ catch (error) {
238
+ await this.messageQueue.markAsFailed(queued.id, error.message);
239
+ }
240
+ }
241
+ }
242
+ // ========== Index Operations ==========
243
+ /**
244
+ * Rebuild all indexes
245
+ */
246
+ async rebuildIndexes() {
247
+ await indexer.rebuildAllIndexes(this.basePath, this.accounts);
248
+ }
249
+ // ========== Cleanup ==========
250
+ /**
251
+ * Shutdown cache manager
252
+ */
253
+ async shutdown() {
254
+ this.saasConnector?.stopHealthCheck();
255
+ // Process remaining queue
256
+ await this.processQueue();
257
+ console.log('[CacheManager] Shutdown complete');
258
+ }
259
+ // Private options storage
260
+ options;
261
+ }
262
+ /**
263
+ * Create cache manager instance
264
+ */
265
+ export function createCacheManager(options) {
266
+ const manager = new CacheManager(options);
267
+ manager['options'] = options;
268
+ return manager;
269
+ }
270
+ export { writer, indexer, extractor, compactor };
271
+ export { CloudSyncer, SAASConnector, MessageQueue };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Message Queue - Offline message retry
3
+ *
4
+ * Store messages locally when offline, retry when back online
5
+ */
6
+ import { QueuedMessage, WeChatMessage } from './types.js';
7
+ /**
8
+ * Message Queue class
9
+ */
10
+ export declare class MessageQueue {
11
+ private queuePath;
12
+ private queue;
13
+ constructor(queuePath: string);
14
+ /**
15
+ * Initialize queue (load from disk)
16
+ */
17
+ init(): Promise<void>;
18
+ /**
19
+ * Enqueue a message for sync
20
+ */
21
+ enqueue(message: WeChatMessage): Promise<string>;
22
+ /**
23
+ * Get pending messages
24
+ */
25
+ getPending(): QueuedMessage[];
26
+ /**
27
+ * Mark message as synced
28
+ */
29
+ markAsSynced(id: string): Promise<void>;
30
+ /**
31
+ * Mark message as failed
32
+ */
33
+ markAsFailed(id: string, error: string): Promise<void>;
34
+ /**
35
+ * Get queue statistics
36
+ */
37
+ getStats(): {
38
+ pending: number;
39
+ synced: number;
40
+ failed: number;
41
+ total: number;
42
+ };
43
+ /**
44
+ * Persist queue item to disk
45
+ */
46
+ private persist;
47
+ /**
48
+ * Delete queue item from disk
49
+ */
50
+ private delete;
51
+ /**
52
+ * Clean up old synced messages
53
+ */
54
+ cleanup(maxAge?: number): Promise<number>;
55
+ }
56
+ /**
57
+ * Create message queue instance
58
+ */
59
+ export declare function createMessageQueue(queuePath: string): MessageQueue;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Message Queue - Offline message retry
3
+ *
4
+ * Store messages locally when offline, retry when back online
5
+ */
6
+ import * as fs from 'fs/promises';
7
+ import * as path from 'path';
8
+ const ENCODING = 'utf-8';
9
+ /**
10
+ * Message Queue class
11
+ */
12
+ export class MessageQueue {
13
+ queuePath;
14
+ queue;
15
+ constructor(queuePath) {
16
+ this.queuePath = queuePath;
17
+ this.queue = new Map();
18
+ }
19
+ /**
20
+ * Initialize queue (load from disk)
21
+ */
22
+ async init() {
23
+ try {
24
+ const files = await fs.readdir(this.queuePath);
25
+ for (const file of files) {
26
+ if (!file.endsWith('.json'))
27
+ continue;
28
+ const content = await fs.readFile(path.join(this.queuePath, file), ENCODING);
29
+ const queued = JSON.parse(content);
30
+ this.queue.set(queued.id, queued);
31
+ }
32
+ }
33
+ catch (error) {
34
+ if (error.code !== 'ENOENT') {
35
+ console.error('[MessageQueue] Init error:', error);
36
+ }
37
+ }
38
+ }
39
+ /**
40
+ * Enqueue a message for sync
41
+ */
42
+ async enqueue(message) {
43
+ const id = `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
44
+ const queued = {
45
+ id,
46
+ message,
47
+ retryCount: 0,
48
+ createdAt: Date.now(),
49
+ status: 'pending',
50
+ };
51
+ this.queue.set(id, queued);
52
+ await this.persist(queued);
53
+ return id;
54
+ }
55
+ /**
56
+ * Get pending messages
57
+ */
58
+ getPending() {
59
+ return Array.from(this.queue.values())
60
+ .filter(m => m.status === 'pending' || m.status === 'failed')
61
+ .filter(m => m.retryCount < 5) // Max 5 retries
62
+ .sort((a, b) => a.createdAt - b.createdAt);
63
+ }
64
+ /**
65
+ * Mark message as synced
66
+ */
67
+ async markAsSynced(id) {
68
+ const queued = this.queue.get(id);
69
+ if (!queued)
70
+ return;
71
+ queued.status = 'synced';
72
+ this.queue.set(id, queued);
73
+ // Update file
74
+ await this.persist(queued);
75
+ // Delete file after some time (optional)
76
+ // await this.delete(id);
77
+ }
78
+ /**
79
+ * Mark message as failed
80
+ */
81
+ async markAsFailed(id, error) {
82
+ const queued = this.queue.get(id);
83
+ if (!queued)
84
+ return;
85
+ queued.status = 'failed';
86
+ queued.retryCount++;
87
+ queued.lastRetryAt = Date.now();
88
+ queued.error = error;
89
+ this.queue.set(id, queued);
90
+ await this.persist(queued);
91
+ }
92
+ /**
93
+ * Get queue statistics
94
+ */
95
+ getStats() {
96
+ const pending = Array.from(this.queue.values()).filter(m => m.status === 'pending').length;
97
+ const synced = Array.from(this.queue.values()).filter(m => m.status === 'synced').length;
98
+ const failed = Array.from(this.queue.values()).filter(m => m.status === 'failed').length;
99
+ return {
100
+ pending,
101
+ synced,
102
+ failed,
103
+ total: this.queue.size,
104
+ };
105
+ }
106
+ /**
107
+ * Persist queue item to disk
108
+ */
109
+ async persist(queued) {
110
+ await fs.mkdir(this.queuePath, { recursive: true });
111
+ await fs.writeFile(path.join(this.queuePath, `${queued.id}.json`), JSON.stringify(queued, null, 2), ENCODING);
112
+ }
113
+ /**
114
+ * Delete queue item from disk
115
+ */
116
+ async delete(id) {
117
+ try {
118
+ await fs.unlink(path.join(this.queuePath, `${id}.json`));
119
+ this.queue.delete(id);
120
+ }
121
+ catch (error) {
122
+ if (error.code !== 'ENOENT') {
123
+ console.error('[MessageQueue] Delete error:', error);
124
+ }
125
+ }
126
+ }
127
+ /**
128
+ * Clean up old synced messages
129
+ */
130
+ async cleanup(maxAge = 24 * 60 * 60 * 1000) {
131
+ const cutoff = Date.now() - maxAge;
132
+ let cleaned = 0;
133
+ for (const [id, queued] of this.queue.entries()) {
134
+ if (queued.status === 'synced' && queued.createdAt < cutoff) {
135
+ await this.delete(id);
136
+ cleaned++;
137
+ }
138
+ }
139
+ return cleaned;
140
+ }
141
+ }
142
+ /**
143
+ * Create message queue instance
144
+ */
145
+ export function createMessageQueue(queuePath) {
146
+ return new MessageQueue(queuePath);
147
+ }
@@ -0,0 +1,94 @@
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 type { SAASConfig, ConnectionStatus } from './types.js';
12
+ import { DEFAULT_SAAS_CONFIG, UserProfile, WeChatMessage } from './types.js';
13
+ type StatusChangeCallback = (status: ConnectionStatus) => void;
14
+ type OfflineTimeoutCallback = () => void;
15
+ /**
16
+ * SAAS Connector class
17
+ */
18
+ export declare class SAASConnector {
19
+ private config;
20
+ private status;
21
+ private statusCallbacks;
22
+ private offlineTimeoutCallbacks;
23
+ private healthCheckInterval;
24
+ constructor(config?: Partial<SAASConfig>);
25
+ /**
26
+ * Start health check loop
27
+ */
28
+ startHealthCheck(intervalMs?: number): void;
29
+ /**
30
+ * Stop health check
31
+ */
32
+ stopHealthCheck(): void;
33
+ /**
34
+ * Check SAAS health status
35
+ */
36
+ checkHealth(): Promise<boolean>;
37
+ /**
38
+ * Get current connection status
39
+ */
40
+ getConnectionStatus(): ConnectionStatus;
41
+ /**
42
+ * Check if online
43
+ */
44
+ isOnline(): boolean;
45
+ /**
46
+ * Query user profile from SAAS
47
+ */
48
+ queryUserProfile(userId: string): Promise<UserProfile | null>;
49
+ /**
50
+ * Query order from SAAS
51
+ */
52
+ queryOrder(orderId: string): Promise<any>;
53
+ /**
54
+ * Sync message to SAAS
55
+ */
56
+ syncMessage(msg: WeChatMessage): Promise<void>;
57
+ /**
58
+ * Query user profile from local cache (fallback)
59
+ */
60
+ queryUserProfileOffline(userId: string, localCacheGetter: (userId: string) => Promise<UserProfile | null>): Promise<UserProfile | null>;
61
+ /**
62
+ * Query order from local cache (fallback)
63
+ */
64
+ queryOrderOffline(orderId: string, localCacheGetter: (orderId: string) => Promise<any | null>): Promise<any | null>;
65
+ /**
66
+ * Get offline message for user
67
+ */
68
+ getOfflineMessage(): string;
69
+ /**
70
+ * Register status change callback
71
+ */
72
+ onStatusChange(callback: StatusChangeCallback): void;
73
+ /**
74
+ * Register offline timeout callback
75
+ */
76
+ onOfflineTimeout(callback: OfflineTimeoutCallback): void;
77
+ /**
78
+ * Notify status change
79
+ */
80
+ private notifyStatusChange;
81
+ /**
82
+ * Notify offline timeout
83
+ */
84
+ private notifyOfflineTimeout;
85
+ /**
86
+ * Update config
87
+ */
88
+ updateConfig(config: Partial<SAASConfig>): void;
89
+ }
90
+ /**
91
+ * Create SAAS connector instance
92
+ */
93
+ export declare function createSAASConnector(config?: Partial<SAASConfig>): SAASConnector;
94
+ export { SAASConfig, ConnectionStatus, DEFAULT_SAAS_CONFIG };