@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
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@gloablehive/celphone-wechat-plugin",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "OpenClaw channel plugin for workphone-wechat API - enables sending/receiving WeChat messages through workphone",
6
+ "main": "index.ts",
7
+ "scripts": {
8
+ "test": "npx tsx test-cache.ts",
9
+ "test:cache": "npx tsx test-cache.ts",
10
+ "test:integration": "npx tsx test-integration.ts",
11
+ "test:mock": "npx tsx mock-server.ts",
12
+ "build": "tsc",
13
+ "clean": "rm -rf /tmp/wechat-cache-test*"
14
+ },
15
+ "openclaw": {
16
+ "extensions": [
17
+ "./index.ts"
18
+ ],
19
+ "setupEntry": "./setup-entry.ts",
20
+ "channel": {
21
+ "id": "celphone-wechat",
22
+ "label": "WorkPhone WeChat",
23
+ "blurb": "Connect OpenClaw to WorkPhone WeChat API for sending and receiving WeChat messages"
24
+ }
25
+ },
26
+ "dependencies": {
27
+ "@gloablehive/wechat-cache": "^1.0.0",
28
+ "openclaw": ">=1.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/express": "^4.17.0",
32
+ "@types/node": "^20.0.0",
33
+ "express": "^4.18.0",
34
+ "tsx": "^4.21.0",
35
+ "typescript": "^5.0.0"
36
+ }
37
+ }
package/setup-entry.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Setup Entry Point
3
+ *
4
+ * Lightweight entry for loading during onboarding/config when channel is disabled
5
+ */
6
+
7
+ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
8
+ import { celPhoneWeChatPlugin } from "./src/channel.js";
9
+
10
+ export default defineSetupPluginEntry(celPhoneWeChatPlugin);
package/src/channel.ts ADDED
@@ -0,0 +1,398 @@
1
+ /**
2
+ * WorkPhone WeChat Channel Plugin for OpenClaw
3
+ *
4
+ * This channel plugin connects OpenClaw to WorkPhone's WeChat API,
5
+ * enabling sending and receiving WeChat messages through the WorkPhone platform.
6
+ *
7
+ * IMPORTANT - Human Account Model:
8
+ * Unlike Telegram/WhatsApp bots, this connects to a real human WeChat account.
9
+ * The agent communicates with ALL friends and groups under that WeChat account,
10
+ * not just one DM context. This is "human mode" vs "bot mode".
11
+ *
12
+ * Includes Local Cache:
13
+ * - Per-account, per-user/conversation MD files
14
+ * - YAML frontmatter (aligned with Claude Code)
15
+ * - MEMORY.md indexing
16
+ * - 4-layer compression
17
+ * - AI summary extraction
18
+ * - SAAS connectivity + offline fallback
19
+ * - Cloud sync
20
+ */
21
+
22
+ import {
23
+ createChatChannelPlugin,
24
+ createChannelPluginBase,
25
+ } from "openclaw/plugin-sdk/core";
26
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
27
+ import { createWorkPhoneClient, type WebhookPayload } from "./client.js";
28
+
29
+ // Import cache modules from shared package
30
+ import {
31
+ createCacheManager,
32
+ CacheManager,
33
+ WeChatAccount,
34
+ } from "@gloablehive/wechat-cache";
35
+
36
+ // Cache manager instance (lazy initialized)
37
+ let cacheManager: CacheManager | null = null;
38
+
39
+ /**
40
+ * Get or create cache manager
41
+ */
42
+ function getCacheManager(cfg: OpenClawConfig): CacheManager {
43
+ if (cacheManager) return cacheManager;
44
+
45
+ const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
46
+ const accounts = (section?.accounts || []) as WeChatAccount[];
47
+
48
+ // If no accounts configured, create default from main config
49
+ if (accounts.length === 0 && section?.wechatAccountId) {
50
+ accounts.push({
51
+ accountId: section.accountId || 'default',
52
+ wechatAccountId: section.wechatAccountId,
53
+ wechatId: section.wechatId || section.wechatAccountId,
54
+ nickName: section.nickName || 'WeChat User',
55
+ enabled: true,
56
+ });
57
+ }
58
+
59
+ const basePath = (globalThis as any)?.process?.env?.OPENCLAW_CACHE_PATH
60
+ || '~/.openclaw/channels/celphone-wechat';
61
+
62
+ cacheManager = createCacheManager({
63
+ basePath,
64
+ accounts,
65
+ saasConfig: section?.saas ? {
66
+ apiBaseUrl: section.saas.apiBaseUrl,
67
+ apiKey: section.saas.apiKey,
68
+ timeout: section.saas.timeout || 5000,
69
+ } : undefined,
70
+ syncConfig: section?.sync ? {
71
+ databaseUrl: section.sync.databaseUrl,
72
+ syncMode: section.sync.syncMode || 'interval',
73
+ syncIntervalMs: section.sync.syncIntervalMs || 5 * 60 * 1000,
74
+ } : undefined,
75
+ });
76
+
77
+ // Initialize cache manager
78
+ cacheManager.init().catch(err => {
79
+ console.error('[CelPhoneWeChat] Cache manager init failed:', err);
80
+ });
81
+
82
+ return cacheManager;
83
+ }
84
+
85
+ export interface CelPhoneWeChatResolvedAccount {
86
+ accountId: string | null;
87
+ apiKey: string;
88
+ baseUrl: string;
89
+ wechatAccountId: string; // Required - which WeChat account this is
90
+ wechatId: string; // The actual WeChat ID (wxid_xxx)
91
+ nickName: string; // WeChat nickname for display
92
+ allowFrom: string[];
93
+ dmPolicy: string | undefined;
94
+ }
95
+
96
+ /**
97
+ * Resolve account from OpenClaw config
98
+ *
99
+ * Note: This is a "human account" model - the WeChat account represents
100
+ * a real person with all their friends and groups, not a bot.
101
+ */
102
+ function resolveAccount(
103
+ cfg: OpenClawConfig,
104
+ accountId?: string | null
105
+ ): CelPhoneWeChatResolvedAccount {
106
+ const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
107
+ const apiKey = section?.apiKey;
108
+ const baseUrl = section?.baseUrl || "https://api.workphone.example.com";
109
+ const wechatAccountId = section?.wechatAccountId;
110
+
111
+ if (!apiKey) {
112
+ throw new Error("celphone-wechat: apiKey is required");
113
+ }
114
+
115
+ if (!wechatAccountId) {
116
+ throw new Error("celphone-wechat: wechatAccountId is required (the WeChat account to use)");
117
+ }
118
+
119
+ return {
120
+ accountId: accountId ?? null,
121
+ apiKey,
122
+ baseUrl,
123
+ wechatAccountId,
124
+ wechatId: section?.wechatId || wechatAccountId, // Actual WeChat ID (wxid_xxx)
125
+ nickName: section?.nickName || "WeChat User", // Display name
126
+ allowFrom: section?.allowFrom ?? [],
127
+ dmPolicy: section?.dmSecurity,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Create the channel plugin
133
+ */
134
+ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolvedAccount>({
135
+ base: createChannelPluginBase({
136
+ id: "celphone-wechat",
137
+ setup: {
138
+ resolveAccount,
139
+ inspectAccount(cfg, accountId) {
140
+ const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
141
+ const hasApiKey = Boolean(section?.apiKey);
142
+ return {
143
+ enabled: hasApiKey,
144
+ configured: hasApiKey,
145
+ tokenStatus: hasApiKey ? "available" : "missing",
146
+ };
147
+ },
148
+ },
149
+ }),
150
+
151
+ // DM security: who can message the bot
152
+ security: {
153
+ dm: {
154
+ channelKey: "celphone-wechat",
155
+ resolvePolicy: (account) => account.dmPolicy,
156
+ resolveAllowFrom: (account) => account.allowFrom,
157
+ defaultPolicy: "allowlist", // Default to allowlist for security
158
+ },
159
+ },
160
+
161
+ // Pairing: not currently supported for this channel
162
+ // WorkPhone WeChat doesn't have a standard pairing flow
163
+ // pairing: { ... },
164
+
165
+ // Threading: how replies are delivered
166
+ // For WeChat, replies go back to the same chat (friend or chatroom)
167
+ threading: {
168
+ topLevelReplyToMode: "reply", // Reply to the last message in the thread
169
+ },
170
+
171
+ // Outbound: send messages to WeChat via WorkPhone API
172
+ //
173
+ // HUMAN ACCOUNT MODEL IMPORTANT:
174
+ // This channel connects to a real person's WeChat account.
175
+ // The agent needs to communicate with ALL their friends and groups,
176
+ // not just one conversation. We distinguish by conversation type:
177
+ // - DM (direct): friendWechatId in params.to
178
+ // - Group chat: chatroomId in params.to (detect by format or metadata)
179
+ outbound: {
180
+ attachedResults: {
181
+ // Send text - detect if DM or group based on conversation metadata
182
+ sendText: async (params) => {
183
+ const client = createWorkPhoneClient({
184
+ baseUrl: params.account.baseUrl,
185
+ apiKey: params.account.apiKey,
186
+ accountId: params.account.accountId || undefined,
187
+ wechatAccountId: params.account.wechatAccountId,
188
+ });
189
+
190
+ // Check if this is a group message (chatroom)
191
+ // Chatroom IDs typically start with certain prefix or have specific format
192
+ const isChatroom = params.metadata?.conversationType === 'group' ||
193
+ (params.to && params.to.includes('@chatroom'));
194
+
195
+ let result;
196
+ if (isChatroom) {
197
+ // Send to chatroom (group)
198
+ result = await client.sendChatroomMessage({
199
+ wechatAccountId: params.account.wechatAccountId,
200
+ chatroomId: params.to,
201
+ content: params.text,
202
+ type: 'text',
203
+ });
204
+ } else {
205
+ // Send to friend (DM)
206
+ result = await client.sendFriendMessage({
207
+ wechatAccountId: params.account.wechatAccountId,
208
+ friendWechatId: params.to,
209
+ content: params.text,
210
+ type: 'text',
211
+ });
212
+ }
213
+
214
+ return { messageId: result.messageId };
215
+ },
216
+
217
+ // Send media - also support both DM and group
218
+ sendMedia: async (params) => {
219
+ const client = createWorkPhoneClient({
220
+ baseUrl: params.account.baseUrl,
221
+ apiKey: params.account.apiKey,
222
+ accountId: params.account.accountId || undefined,
223
+ wechatAccountId: params.account.wechatAccountId,
224
+ });
225
+
226
+ const isChatroom = params.metadata?.conversationType === 'group' ||
227
+ (params.to && params.to.includes('@chatroom'));
228
+
229
+ let result;
230
+ if (isChatroom) {
231
+ result = await client.sendChatroomMessage({
232
+ wechatAccountId: params.account.wechatAccountId,
233
+ chatroomId: params.to,
234
+ content: params.filePath || '',
235
+ type: 'file',
236
+ });
237
+ } else {
238
+ result = await client.sendFriendMessage({
239
+ wechatAccountId: params.account.wechatAccountId,
240
+ friendWechatId: params.to,
241
+ content: params.filePath || '',
242
+ type: 'file',
243
+ });
244
+ }
245
+
246
+ return { messageId: result.messageId };
247
+ },
248
+ },
249
+
250
+ // Additional outbound handlers can be added here for:
251
+ // - sendLink: Send link cards
252
+ // - sendLocation: Send location
253
+ // - sendContact: Send contact card
254
+ },
255
+
256
+ // Additional capabilities
257
+ // - Describe the channel's message types and features
258
+ capabilities: {
259
+ supportedMessageTypes: [
260
+ "text",
261
+ "image",
262
+ "video",
263
+ "file",
264
+ "link",
265
+ "location",
266
+ "contact",
267
+ ],
268
+ maxAttachmentSize: 25 * 1024 * 1024, // 25MB
269
+ supportsMarkdown: false, // WeChat uses a limited markup
270
+ supportsHtml: false,
271
+ supportsEmoji: true,
272
+ supportsReactions: false, // WeChat doesn't support reactions
273
+ supportsThreads: true, // Can reply in chat
274
+ supportsEditing: false, // Cannot edit sent messages
275
+ supportsDeleting: false, // Cannot delete sent messages
276
+ },
277
+ });
278
+
279
+ /**
280
+ * Helper function to handle inbound webhook messages
281
+ * This should be called from your HTTP route handler
282
+ *
283
+ * Integrates with Cache Manager for:
284
+ * - Local MD file caching
285
+ * - User profile storage
286
+ * - Session memory
287
+ * - Cloud sync
288
+ * - Offline fallback
289
+ */
290
+ export async function handleInboundMessage(
291
+ api: any,
292
+ payload: WebhookPayload,
293
+ cfg?: OpenClawConfig
294
+ ): Promise<void> {
295
+ const { event, accountId, wechatAccountId, message, friendRequest } = payload;
296
+
297
+ if (event === 'message' && message) {
298
+ // Determine conversation type
299
+ const isChatroom = !!(message as any).chatroomId;
300
+ const conversationId = isChatroom
301
+ ? (message as any).chatroomId
302
+ : message.fromUser || message.toUser || '';
303
+
304
+ // Convert to cache format and store locally
305
+ if (cacheManager && accountId) {
306
+ try {
307
+ await cacheManager.onMessage({
308
+ messageId: message.messageId,
309
+ msgSvrId: message.msgSvrId,
310
+ accountId: accountId,
311
+ conversationType: isChatroom ? 'chatroom' : 'friend',
312
+ conversationId,
313
+ senderId: message.fromUser || '',
314
+ content: message.content,
315
+ messageType: message.type || 1,
316
+ timestamp: message.timestamp || Date.now(),
317
+ isSelf: message.isSelf || false,
318
+ direction: message.isSelf ? 'outbound' : 'inbound',
319
+ });
320
+ } catch (err) {
321
+ console.error('[CelPhoneWeChat] Cache write failed:', err);
322
+ }
323
+ }
324
+
325
+ // Convert WorkPhone message format to OpenClaw format
326
+ const openclawMessage = {
327
+ id: message.messageId,
328
+ rawId: message.msgSvrId || message.messageId,
329
+ conversation: {
330
+ type: isChatroom ? 'group' as const : 'dm' as const,
331
+ id: conversationId,
332
+ chatroomId: isChatroom ? conversationId : undefined,
333
+ },
334
+ sender: {
335
+ id: message.fromUser || '',
336
+ platformId: message.wechatId,
337
+ },
338
+ content: {
339
+ type: message.type === 1 ? 'text' : 'media',
340
+ text: message.content,
341
+ },
342
+ timestamp: new Date(message.timestamp || Date.now()),
343
+ isSelf: message.isSelf || false,
344
+ };
345
+
346
+ // Dispatch to OpenClaw
347
+ await api.inbound.dispatchMessage(openclawMessage);
348
+ } else if (event === 'friend_request' && friendRequest) {
349
+ // Handle friend request
350
+ await api.inbound.dispatchFriendRequest({
351
+ id: friendRequest.v1,
352
+ platformId: friendRequest.fromUser,
353
+ scene: friendRequest.scene,
354
+ ticket: friendRequest.ticket,
355
+ accountId,
356
+ wechatAccountId,
357
+ });
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Get user profile from cache (for agent use)
363
+ */
364
+ export async function getUserProfile(
365
+ accountId: string,
366
+ wechatId: string
367
+ ): Promise<any> {
368
+ if (!cacheManager) return null;
369
+ return await cacheManager.getProfile(accountId, wechatId);
370
+ }
371
+
372
+ /**
373
+ * Update user profile in cache
374
+ */
375
+ export async function updateUserProfile(
376
+ accountId: string,
377
+ wechatId: string,
378
+ updates: any
379
+ ): Promise<void> {
380
+ if (!cacheManager) return;
381
+ await cacheManager.updateProfile(accountId, wechatId, updates);
382
+ }
383
+
384
+ /**
385
+ * Get SAAS connection status
386
+ */
387
+ export function getConnectionStatus() {
388
+ return cacheManager?.getConnectionStatus();
389
+ }
390
+
391
+ /**
392
+ * Check if system is online (SAAS connected)
393
+ */
394
+ export function isOnline(): boolean {
395
+ return cacheManager?.isSAASOnline() ?? false;
396
+ }
397
+
398
+ export default celPhoneWeChatPlugin;