@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,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Test for WeChat Channel Plugin
|
|
3
|
+
* Tests the full flow: webhook -> cache -> response
|
|
4
|
+
*
|
|
5
|
+
* Run: npx tsx test-integration.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs/promises';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import {
|
|
11
|
+
createCacheManager,
|
|
12
|
+
WeChatAccount,
|
|
13
|
+
WeChatMessage,
|
|
14
|
+
} from './src/cache/index.js';
|
|
15
|
+
import { handleInboundMessage } from './src/channel.js';
|
|
16
|
+
|
|
17
|
+
const TEST_CACHE_PATH = '/tmp/wechat-integration-test';
|
|
18
|
+
const ENCODING = 'utf-8';
|
|
19
|
+
|
|
20
|
+
async function cleanup() {
|
|
21
|
+
try {
|
|
22
|
+
await fs.rm(TEST_CACHE_PATH, { recursive: true, force: true });
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Test 1: Simulate full webhook -> cache flow
|
|
28
|
+
*/
|
|
29
|
+
async function testWebhookFlow() {
|
|
30
|
+
console.log('\nš Test 1: Webhook -> Cache Flow');
|
|
31
|
+
|
|
32
|
+
const accounts: WeChatAccount[] = [
|
|
33
|
+
{
|
|
34
|
+
accountId: 'wechat-service-001',
|
|
35
|
+
wechatAccountId: 'wp-wechat-001',
|
|
36
|
+
wechatId: 'wxid_cs001',
|
|
37
|
+
nickName: '客ęå°å¾®',
|
|
38
|
+
enabled: true,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const manager = createCacheManager({
|
|
43
|
+
basePath: TEST_CACHE_PATH,
|
|
44
|
+
accounts,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
await manager.init();
|
|
48
|
+
console.log('ā
Cache manager initialized');
|
|
49
|
+
|
|
50
|
+
// Simulate Webhook payload (like what WorkPhone would send)
|
|
51
|
+
const webhookPayload = {
|
|
52
|
+
event: 'message',
|
|
53
|
+
accountId: 'wechat-service-001',
|
|
54
|
+
wechatAccountId: 'wp-wechat-001',
|
|
55
|
+
message: {
|
|
56
|
+
messageId: 'webhook-msg-001',
|
|
57
|
+
msgSvrId: 'msg-svr-12345',
|
|
58
|
+
fromUser: 'wxid_customer001',
|
|
59
|
+
toUser: 'wxid_cs001',
|
|
60
|
+
content: 'ä½ å„½ļ¼ęę³åØčÆ¢ä½ ä»¬ēäŗ§å',
|
|
61
|
+
type: 1, // text
|
|
62
|
+
timestamp: Date.now(),
|
|
63
|
+
isSelf: false,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
console.log('š„ Simulated webhook payload:', JSON.stringify(webhookPayload, null, 2));
|
|
68
|
+
|
|
69
|
+
// Process through handleInboundMessage (simulated - no actual OpenClaw API)
|
|
70
|
+
// In real usage, this would be called from the HTTP route handler
|
|
71
|
+
const message: WeChatMessage = {
|
|
72
|
+
messageId: webhookPayload.message.messageId,
|
|
73
|
+
msgSvrId: webhookPayload.message.msgSvrId,
|
|
74
|
+
accountId: webhookPayload.accountId,
|
|
75
|
+
conversationType: 'friend',
|
|
76
|
+
conversationId: webhookPayload.message.fromUser,
|
|
77
|
+
senderId: webhookPayload.message.fromUser,
|
|
78
|
+
content: webhookPayload.message.content,
|
|
79
|
+
messageType: webhookPayload.message.type,
|
|
80
|
+
timestamp: webhookPayload.message.timestamp,
|
|
81
|
+
isSelf: webhookPayload.message.isSelf,
|
|
82
|
+
direction: 'inbound',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
await manager.onMessage(message);
|
|
86
|
+
console.log('ā
Message processed through cache');
|
|
87
|
+
|
|
88
|
+
// Verify files created
|
|
89
|
+
const convFile = path.join(
|
|
90
|
+
TEST_CACHE_PATH,
|
|
91
|
+
'accounts/wechat-service-001/friends/wxid_customer001/memory/2026-04.md'
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const content = await fs.readFile(convFile, ENCODING);
|
|
95
|
+
console.log('š Cached conversation file:');
|
|
96
|
+
console.log(content.slice(0, 600) + '...');
|
|
97
|
+
|
|
98
|
+
return manager;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Test 2: Customer profile from webhook
|
|
103
|
+
*/
|
|
104
|
+
async function testProfileFromWebhook(manager: any) {
|
|
105
|
+
console.log('\nš Test 2: Customer Profile from Webhook');
|
|
106
|
+
|
|
107
|
+
// Simulate receiving customer info from webhook or after message
|
|
108
|
+
await manager.updateProfile('wechat-service-001', 'wxid_customer001', {
|
|
109
|
+
remark: 'ęå
ē-ę½åØå®¢ę·',
|
|
110
|
+
tags: ['ę½åØå®¢ę·', 'åØčÆ¢äø'],
|
|
111
|
+
customFields: {
|
|
112
|
+
source: 'å®ē½åØčÆ¢',
|
|
113
|
+
interest: 'åŗē”å„é¤',
|
|
114
|
+
},
|
|
115
|
+
conversationSummary: 'åØčÆ¢äŗ§åäæ”ęÆ',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
console.log('ā
Profile updated from webhook context');
|
|
119
|
+
|
|
120
|
+
const profile = await manager.getProfile('wechat-service-001', 'wxid_customer001');
|
|
121
|
+
console.log('š Retrieved profile:');
|
|
122
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Test 3: Agent responds -> cache updated
|
|
127
|
+
*/
|
|
128
|
+
async function testAgentResponse(manager: any) {
|
|
129
|
+
console.log('\nš Test 3: Agent Response -> Cache Updated');
|
|
130
|
+
|
|
131
|
+
// Simulate agent sending a response
|
|
132
|
+
const responseMessage: WeChatMessage = {
|
|
133
|
+
messageId: 'webhook-msg-002',
|
|
134
|
+
msgSvrId: 'msg-svr-12346',
|
|
135
|
+
accountId: 'wechat-service-001',
|
|
136
|
+
conversationType: 'friend',
|
|
137
|
+
conversationId: 'wxid_customer001',
|
|
138
|
+
senderId: 'wxid_cs001',
|
|
139
|
+
content: 'ęØå„½ļ¼ę们ēåŗē”å„é¤ęÆ 999 å
/幓ļ¼å
å«ęęę øåæåč½ć请é®ęØę³äŗč§£åŖę¹é¢ē详ę
ļ¼',
|
|
140
|
+
messageType: 1,
|
|
141
|
+
timestamp: Date.now() + 5000,
|
|
142
|
+
isSelf: true,
|
|
143
|
+
direction: 'outbound',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
await manager.onMessage(responseMessage);
|
|
147
|
+
console.log('ā
Agent response cached');
|
|
148
|
+
|
|
149
|
+
// Verify both messages in conversation
|
|
150
|
+
const convFile = path.join(
|
|
151
|
+
TEST_CACHE_PATH,
|
|
152
|
+
'accounts/wechat-service-001/friends/wxid_customer001/memory/2026-04.md'
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const content = await fs.readFile(convFile, ENCODING);
|
|
156
|
+
const customerMsgCount = (content.match(/\*\*\[wxid_customer001\]/g) || []).length;
|
|
157
|
+
const agentMsgCount = (content.match(/\*\*\[ę\]/g) || []).length;
|
|
158
|
+
|
|
159
|
+
console.log(`š Message count - Customer: ${customerMsgCount}, Agent: ${agentMsgCount}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Test 4: MEMORY.md index
|
|
164
|
+
*/
|
|
165
|
+
async function testMemoryIndex() {
|
|
166
|
+
console.log('\nš Test 4: MEMORY.md Index');
|
|
167
|
+
|
|
168
|
+
const globalIndex = path.join(TEST_CACHE_PATH, 'MEMORY.md');
|
|
169
|
+
const globalContent = await fs.readFile(globalIndex, ENCODING);
|
|
170
|
+
console.log('š Global MEMORY.md:');
|
|
171
|
+
console.log(globalContent);
|
|
172
|
+
|
|
173
|
+
const accountIndex = path.join(TEST_CACHE_PATH, 'accounts/wechat-service-001/MEMORY.md');
|
|
174
|
+
const accountContent = await fs.readFile(accountIndex, ENCODING);
|
|
175
|
+
console.log('š Account MEMORY.md:');
|
|
176
|
+
console.log(accountContent.slice(0, 400) + '...');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Test 5: Query by agent (simulated)
|
|
181
|
+
*/
|
|
182
|
+
async function testAgentQuery(manager: any) {
|
|
183
|
+
console.log('\nš Test 5: Agent Query - Get Customer History');
|
|
184
|
+
|
|
185
|
+
// Agent wants to know conversation history with this customer
|
|
186
|
+
const profile = await manager.getProfile('wechat-service-001', 'wxid_customer001');
|
|
187
|
+
|
|
188
|
+
console.log('š Agent queries customer profile:');
|
|
189
|
+
console.log('- å¤ę³Ø:', profile?.remark);
|
|
190
|
+
console.log('- ę ē¾:', profile?.tags);
|
|
191
|
+
console.log('- ę„ęŗ:', profile?.customFields?.source);
|
|
192
|
+
console.log('- ęå:', profile?.customFields?.interest);
|
|
193
|
+
|
|
194
|
+
// Agent gets connection status
|
|
195
|
+
const status = manager.getConnectionStatus();
|
|
196
|
+
console.log('š SAAS connection status:', status?.isOnline ?? false);
|
|
197
|
+
|
|
198
|
+
console.log('ā
Agent can query customer info from cache');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Test 6: OpenClaw binding simulation
|
|
203
|
+
*/
|
|
204
|
+
async function testBindingSimulation() {
|
|
205
|
+
console.log('\nš Test 6: OpenClaw Binding Simulation');
|
|
206
|
+
|
|
207
|
+
// This simulates how OpenClaw would route messages
|
|
208
|
+
const bindingConfig = {
|
|
209
|
+
channels: {
|
|
210
|
+
'celphone-wechat': {
|
|
211
|
+
accounts: [
|
|
212
|
+
{
|
|
213
|
+
accountId: 'wechat-service-001',
|
|
214
|
+
wechatAccountId: 'wp-wechat-001',
|
|
215
|
+
wechatId: 'wxid_cs001',
|
|
216
|
+
nickName: '客ęå°å¾®',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
accountId: 'wechat-service-002',
|
|
220
|
+
wechatAccountId: 'wp-wechat-002',
|
|
221
|
+
wechatId: 'wxid_sales01',
|
|
222
|
+
nickName: 'éå®å°ē',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
bindings: [
|
|
228
|
+
{
|
|
229
|
+
agentId: 'service-frontdesk',
|
|
230
|
+
match: {
|
|
231
|
+
channel: 'celphone-wechat',
|
|
232
|
+
accountId: 'wechat-service-001',
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
agentId: 'service-sales',
|
|
237
|
+
match: {
|
|
238
|
+
channel: 'celphone-wechat',
|
|
239
|
+
accountId: 'wechat-service-002',
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
console.log('š Binding config:', JSON.stringify(bindingConfig, null, 2));
|
|
246
|
+
|
|
247
|
+
// Simulate routing decision
|
|
248
|
+
const testMessage = {
|
|
249
|
+
channel: 'celphone-wechat',
|
|
250
|
+
accountId: 'wechat-service-001',
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const matchedBinding = bindingConfig.bindings.find(
|
|
254
|
+
b => b.match.channel === testMessage.channel && b.match.accountId === testMessage.accountId
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
console.log('š Message from account:', testMessage.accountId);
|
|
258
|
+
console.log('š Routed to agent:', matchedBinding?.agentId);
|
|
259
|
+
|
|
260
|
+
console.log('ā
Binding simulation complete');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function runIntegrationTests() {
|
|
264
|
+
console.log('š¬ WeChat Channel Plugin Integration Tests');
|
|
265
|
+
console.log('='.repeat(60));
|
|
266
|
+
|
|
267
|
+
await cleanup();
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
// Test 1: Webhook flow
|
|
271
|
+
const manager = await testWebhookFlow();
|
|
272
|
+
|
|
273
|
+
// Test 2: Profile from webhook
|
|
274
|
+
await testProfileFromWebhook(manager);
|
|
275
|
+
|
|
276
|
+
// Test 3: Agent response
|
|
277
|
+
await testAgentResponse(manager);
|
|
278
|
+
|
|
279
|
+
// Test 4: Memory index
|
|
280
|
+
await testMemoryIndex();
|
|
281
|
+
|
|
282
|
+
// Test 5: Agent query
|
|
283
|
+
await testAgentQuery(manager);
|
|
284
|
+
|
|
285
|
+
// Test 6: Binding simulation
|
|
286
|
+
await testBindingSimulation();
|
|
287
|
+
|
|
288
|
+
console.log('\n' + '='.repeat(60));
|
|
289
|
+
console.log('š All integration tests passed!');
|
|
290
|
+
console.log('\nš Test cache location:', TEST_CACHE_PATH);
|
|
291
|
+
|
|
292
|
+
// List all created files
|
|
293
|
+
console.log('\nš Created files:');
|
|
294
|
+
const allFiles = await walkDir(TEST_CACHE_PATH);
|
|
295
|
+
allFiles.forEach(f => console.log(' ', f.replace(TEST_CACHE_PATH, '')));
|
|
296
|
+
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.error('\nā Integration test failed:', error);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function walkDir(dir: string): Promise<string[]> {
|
|
304
|
+
const files: string[] = [];
|
|
305
|
+
try {
|
|
306
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
307
|
+
for (const entry of entries) {
|
|
308
|
+
const fullPath = path.join(dir, entry.name);
|
|
309
|
+
if (entry.isDirectory()) {
|
|
310
|
+
files.push(...await walkDir(fullPath));
|
|
311
|
+
} else {
|
|
312
|
+
files.push(fullPath);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch {}
|
|
316
|
+
return files;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
runIntegrationTests();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": ".",
|
|
9
|
+
"declaration": false,
|
|
10
|
+
"strict": false,
|
|
11
|
+
"noImplicitAny": false,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"allowSyntheticDefaultImports": true,
|
|
17
|
+
"noEmit": false,
|
|
18
|
+
"noEmitOnError": false
|
|
19
|
+
},
|
|
20
|
+
"include": ["*.ts", "src/**/*.ts"],
|
|
21
|
+
"exclude": ["node_modules", "dist", "test*.ts"]
|
|
22
|
+
}
|