@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.
- package/INSTALL.md +231 -0
- package/README.md +259 -0
- package/dist/index-simple.js +9 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +77 -0
- package/dist/mock-server.d.ts +6 -0
- package/dist/mock-server.js +203 -0
- package/dist/openclaw.plugin.json +96 -0
- package/dist/setup-entry.d.ts +9 -0
- package/dist/setup-entry.js +8 -0
- package/dist/src/cache/compactor.d.ts +36 -0
- package/dist/src/cache/compactor.js +154 -0
- package/dist/src/cache/extractor.d.ts +48 -0
- package/dist/src/cache/extractor.js +120 -0
- package/dist/src/cache/index.d.ts +15 -0
- package/dist/src/cache/index.js +16 -0
- package/dist/src/cache/indexer.d.ts +41 -0
- package/dist/src/cache/indexer.js +262 -0
- package/dist/src/cache/manager.d.ts +113 -0
- package/dist/src/cache/manager.js +271 -0
- package/dist/src/cache/message-queue.d.ts +59 -0
- package/dist/src/cache/message-queue.js +147 -0
- package/dist/src/cache/saas-connector.d.ts +94 -0
- package/dist/src/cache/saas-connector.js +289 -0
- package/dist/src/cache/syncer.d.ts +60 -0
- package/dist/src/cache/syncer.js +177 -0
- package/dist/src/cache/types.d.ts +198 -0
- package/dist/src/cache/types.js +43 -0
- package/dist/src/cache/writer.d.ts +81 -0
- package/dist/src/cache/writer.js +461 -0
- package/dist/src/channel.d.ts +65 -0
- package/dist/src/channel.js +334 -0
- package/dist/src/client.d.ts +280 -0
- package/dist/src/client.js +248 -0
- package/index-simple.ts +11 -0
- package/index.ts +89 -0
- package/mock-server.ts +237 -0
- package/openclaw.plugin.json +98 -0
- package/package.json +37 -0
- package/setup-entry.ts +10 -0
- package/src/channel.ts +398 -0
- package/src/client.ts +412 -0
- package/test-cache.ts +260 -0
- package/test-integration.ts +319 -0
- 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 };
|