@gloablehive/celphone-wechat-plugin 1.0.0 → 1.1.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/dist/index.js CHANGED
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * WorkPhone WeChat Plugin - enables OpenClaw to send/receive WeChat messages
5
5
  * through the WorkPhone API platform.
6
+ *
7
+ * Multi-account support:
8
+ * - Each account in accounts[] can bind to a different agent
9
+ * - Webhook routing uses AccountRegistry to find agentId
10
+ * - cfg is passed to handleInboundMessage for cache isolation
6
11
  */
7
12
  import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
8
13
  import { celPhoneWeChatPlugin, handleInboundMessage } from "./src/channel.js";
@@ -35,6 +40,8 @@ export default defineChannelPluginEntry({
35
40
  auth: "plugin", // Plugin-managed auth - verify signatures yourself
36
41
  handler: async (req, res) => {
37
42
  try {
43
+ // Get config from request (provided by OpenClaw framework)
44
+ const cfg = req.cfg;
38
45
  // Parse the webhook payload
39
46
  // The exact format depends on WorkPhone's webhook configuration
40
47
  const payload = req.body;
@@ -46,8 +53,8 @@ export default defineChannelPluginEntry({
46
53
  res.end("Missing signature");
47
54
  return true;
48
55
  }
49
- // Handle the inbound message
50
- await handleInboundMessage(api, payload);
56
+ // Handle the inbound message with cfg for multi-account routing
57
+ await handleInboundMessage(api, payload, cfg);
51
58
  res.statusCode = 200;
52
59
  res.end("ok");
53
60
  return true;
@@ -61,15 +68,17 @@ export default defineChannelPluginEntry({
61
68
  },
62
69
  });
63
70
  // Register gateway method for outbound media handling
64
- api.registerGatewayMethod({
65
- method: "POST",
71
+ api.registerHttpRoute({
66
72
  path: "/celphone-wechat/media",
73
+ auth: "plugin",
67
74
  handler: async (req, res) => {
68
- const { messageId, mediaUrl, mediaType } = req.body;
75
+ const payload = req.body;
76
+ const { messageId, mediaUrl, mediaType } = payload || {};
69
77
  // Handle media upload/send through WorkPhone
70
78
  // This is called when the bot needs to send media files
79
+ res.setHeader("Content-Type", "application/json");
71
80
  res.statusCode = 200;
72
- res.json({ success: true, messageId });
81
+ res.end(JSON.stringify({ success: true, messageId }));
73
82
  return true;
74
83
  },
75
84
  });
@@ -9,21 +9,49 @@
9
9
  * The agent communicates with ALL friends and groups under that WeChat account,
10
10
  * not just one DM context. This is "human mode" vs "bot mode".
11
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
12
+ * Multi-account support:
13
+ * - Config supports accounts array with per-account credentials
14
+ * - Each account can be bound to a different agent
15
+ * - Security policies are per-account isolated
20
16
  */
21
17
  import { createChatChannelPlugin, createChannelPluginBase, } from "openclaw/plugin-sdk/core";
22
- import { createWorkPhoneClient } from "./client.js";
23
- // Import cache modules
24
- import { createCacheManager, } from "./cache/index.js";
18
+ // Import cache modules from shared package
19
+ import { createCacheManager, createAccountRegistry, } from "@gloablehive/wechat-cache";
20
+ // Import client pool
21
+ import { getWorkPhoneClient, createClientConfig } from "./client-pool.js";
25
22
  // Cache manager instance (lazy initialized)
26
23
  let cacheManager = null;
24
+ // Account registry for routing
25
+ let accountRegistry = null;
26
+ /**
27
+ * Get or create account registry
28
+ */
29
+ function getAccountRegistry() {
30
+ if (!accountRegistry) {
31
+ accountRegistry = createAccountRegistry();
32
+ }
33
+ return accountRegistry;
34
+ }
35
+ /**
36
+ * Initialize account registry from config
37
+ */
38
+ function initializeRegistry(cfg) {
39
+ const registry = getAccountRegistry();
40
+ registry.clear();
41
+ const section = cfg.channels?.["celphone-wechat"];
42
+ const accounts = (section?.accounts || []);
43
+ // Register each account
44
+ for (const account of accounts) {
45
+ if (account.enabled !== false) {
46
+ const registeredAccount = {
47
+ ...account,
48
+ channelType: "celphone-wechat",
49
+ registeredAt: Date.now(),
50
+ };
51
+ registry.register(registeredAccount);
52
+ }
53
+ }
54
+ }
27
55
  /**
28
56
  * Get or create cache manager
29
57
  */
@@ -35,15 +63,17 @@ function getCacheManager(cfg) {
35
63
  // If no accounts configured, create default from main config
36
64
  if (accounts.length === 0 && section?.wechatAccountId) {
37
65
  accounts.push({
38
- accountId: section.accountId || 'default',
66
+ accountId: section.accountId || "default",
39
67
  wechatAccountId: section.wechatAccountId,
40
68
  wechatId: section.wechatId || section.wechatAccountId,
41
- nickName: section.nickName || 'WeChat User',
69
+ nickName: section.nickName || "WeChat User",
42
70
  enabled: true,
43
71
  });
44
72
  }
73
+ // Initialize registry with accounts
74
+ initializeRegistry(cfg);
45
75
  const basePath = globalThis?.process?.env?.OPENCLAW_CACHE_PATH
46
- || '~/.openclaw/channels/celphone-wechat';
76
+ || "~/.openclaw/channels/celphone-wechat";
47
77
  cacheManager = createCacheManager({
48
78
  basePath,
49
79
  accounts,
@@ -54,24 +84,59 @@ function getCacheManager(cfg) {
54
84
  } : undefined,
55
85
  syncConfig: section?.sync ? {
56
86
  databaseUrl: section.sync.databaseUrl,
57
- syncMode: section.sync.syncMode || 'interval',
87
+ syncMode: section.sync.syncMode || "interval",
58
88
  syncIntervalMs: section.sync.syncIntervalMs || 5 * 60 * 1000,
59
89
  } : undefined,
60
90
  });
61
- // Initialize cache manager
91
+ // Initialize cache manager - must await for ready state
62
92
  cacheManager.init().catch(err => {
63
- console.error('[CelPhoneWeChat] Cache manager init failed:', err);
93
+ console.error("[CelPhoneWeChat] Cache manager init failed:", err);
64
94
  });
65
95
  return cacheManager;
66
96
  }
97
+ /**
98
+ * List all account IDs from config
99
+ */
100
+ function listAccountIds(cfg) {
101
+ const section = cfg.channels?.["celphone-wechat"];
102
+ const accounts = (section?.accounts || []);
103
+ if (accounts.length > 0) {
104
+ return accounts.map(a => a.accountId).filter(Boolean);
105
+ }
106
+ // Backward compatibility: single account
107
+ if (section?.wechatAccountId) {
108
+ return [section.accountId || "default"];
109
+ }
110
+ return [];
111
+ }
67
112
  /**
68
113
  * Resolve account from OpenClaw config
69
114
  *
70
- * Note: This is a "human account" model - the WeChat account represents
71
- * a real person with all their friends and groups, not a bot.
115
+ * Supports both multi-account (accounts array) and legacy (single) formats.
72
116
  */
73
117
  function resolveAccount(cfg, accountId) {
74
118
  const section = cfg.channels?.["celphone-wechat"];
119
+ const accounts = (section?.accounts || []);
120
+ // Multi-account mode: look up from accounts array
121
+ if (accounts.length > 0) {
122
+ const targetAccountId = accountId || accounts[0]?.accountId;
123
+ const account = accounts.find(a => a.accountId === targetAccountId);
124
+ if (!account) {
125
+ throw new Error(`celphone-wechat: account not found - ${targetAccountId}`);
126
+ }
127
+ return {
128
+ accountId: account.accountId,
129
+ agentId: account.agentId || null,
130
+ apiKey: account.apiKey || section?.apiKey || "",
131
+ baseUrl: account.baseUrl || section?.baseUrl || "https://api.workphone.example.com",
132
+ wechatAccountId: account.wechatAccountId,
133
+ wechatId: account.wechatId || account.wechatAccountId,
134
+ nickName: account.nickName || "WeChat User",
135
+ allowFrom: account.allowFrom ?? [],
136
+ dmPolicy: account.dmPolicy,
137
+ };
138
+ }
139
+ // Legacy single-account mode
75
140
  const apiKey = section?.apiKey;
76
141
  const baseUrl = section?.baseUrl || "https://api.workphone.example.com";
77
142
  const wechatAccountId = section?.wechatAccountId;
@@ -83,11 +148,12 @@ function resolveAccount(cfg, accountId) {
83
148
  }
84
149
  return {
85
150
  accountId: accountId ?? null,
151
+ agentId: null,
86
152
  apiKey,
87
153
  baseUrl,
88
154
  wechatAccountId,
89
- wechatId: section?.wechatId || wechatAccountId, // Actual WeChat ID (wxid_xxx)
90
- nickName: section?.nickName || "WeChat User", // Display name
155
+ wechatId: section?.wechatId || wechatAccountId,
156
+ nickName: section?.nickName || "WeChat User",
91
157
  allowFrom: section?.allowFrom ?? [],
92
158
  dmPolicy: section?.dmSecurity,
93
159
  };
@@ -99,115 +165,86 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
99
165
  base: createChannelPluginBase({
100
166
  id: "celphone-wechat",
101
167
  setup: {
102
- resolveAccount,
103
- inspectAccount(cfg, accountId) {
104
- const section = cfg.channels?.["celphone-wechat"];
105
- const hasApiKey = Boolean(section?.apiKey);
106
- return {
107
- enabled: hasApiKey,
108
- configured: hasApiKey,
109
- tokenStatus: hasApiKey ? "available" : "missing",
110
- };
168
+ resolveAccountId: (params) => {
169
+ return resolveAccount(params.cfg, params.accountId)?.accountId || "";
111
170
  },
171
+ applyAccountConfig: (params) => params.cfg,
112
172
  },
113
173
  }),
114
- // DM security: who can message the bot
115
174
  security: {
116
175
  dm: {
117
176
  channelKey: "celphone-wechat",
118
177
  resolvePolicy: (account) => account.dmPolicy,
119
178
  resolveAllowFrom: (account) => account.allowFrom,
120
- defaultPolicy: "allowlist", // Default to allowlist for security
179
+ defaultPolicy: "allowlist",
121
180
  },
122
181
  },
123
- // Pairing: not currently supported for this channel
124
- // WorkPhone WeChat doesn't have a standard pairing flow
125
- // pairing: { ... },
126
- // Threading: how replies are delivered
127
- // For WeChat, replies go back to the same chat (friend or chatroom)
128
182
  threading: {
129
- topLevelReplyToMode: "reply", // Reply to the last message in the thread
183
+ topLevelReplyToMode: "reply",
130
184
  },
131
- // Outbound: send messages to WeChat via WorkPhone API
132
- //
133
- // HUMAN ACCOUNT MODEL IMPORTANT:
134
- // This channel connects to a real person's WeChat account.
135
- // The agent needs to communicate with ALL their friends and groups,
136
- // not just one conversation. We distinguish by conversation type:
137
- // - DM (direct): friendWechatId in params.to
138
- // - Group chat: chatroomId in params.to (detect by format or metadata)
139
185
  outbound: {
186
+ channel: "celphone-wechat",
140
187
  attachedResults: {
141
- // Send text - detect if DM or group based on conversation metadata
188
+ channel: "celphone-wechat",
142
189
  sendText: async (params) => {
143
- const client = createWorkPhoneClient({
144
- baseUrl: params.account.baseUrl,
145
- apiKey: params.account.apiKey,
146
- accountId: params.account.accountId || undefined,
147
- wechatAccountId: params.account.wechatAccountId,
148
- });
149
- // Check if this is a group message (chatroom)
150
- // Chatroom IDs typically start with certain prefix or have specific format
151
- const isChatroom = params.metadata?.conversationType === 'group' ||
152
- (params.to && params.to.includes('@chatroom'));
190
+ const cfg = params.cfg;
191
+ const accountId = params.accountId;
192
+ // Resolve account to get credentials
193
+ const account = resolveAccount(cfg, accountId);
194
+ // Use client pool for connection reuse
195
+ const clientConfig = createClientConfig(account.baseUrl, account.apiKey, account.accountId || undefined, account.wechatAccountId);
196
+ const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
197
+ const isChatroom = params.to?.includes("@chatroom");
153
198
  let result;
154
199
  if (isChatroom) {
155
- // Send to chatroom (group)
156
200
  result = await client.sendChatroomMessage({
157
- wechatAccountId: params.account.wechatAccountId,
201
+ wechatAccountId: account.wechatAccountId,
158
202
  chatroomId: params.to,
159
203
  content: params.text,
160
- type: 'text',
204
+ type: "text",
161
205
  });
162
206
  }
163
207
  else {
164
- // Send to friend (DM)
165
208
  result = await client.sendFriendMessage({
166
- wechatAccountId: params.account.wechatAccountId,
209
+ wechatAccountId: account.wechatAccountId,
167
210
  friendWechatId: params.to,
168
211
  content: params.text,
169
- type: 'text',
212
+ type: "text",
170
213
  });
171
214
  }
172
215
  return { messageId: result.messageId };
173
216
  },
174
- // Send media - also support both DM and group
175
217
  sendMedia: async (params) => {
176
- const client = createWorkPhoneClient({
177
- baseUrl: params.account.baseUrl,
178
- apiKey: params.account.apiKey,
179
- accountId: params.account.accountId || undefined,
180
- wechatAccountId: params.account.wechatAccountId,
181
- });
182
- const isChatroom = params.metadata?.conversationType === 'group' ||
183
- (params.to && params.to.includes('@chatroom'));
218
+ const cfg = params.cfg;
219
+ const accountId = params.accountId;
220
+ // Resolve account to get credentials
221
+ const account = resolveAccount(cfg, accountId);
222
+ // Use client pool
223
+ const clientConfig = createClientConfig(account.baseUrl, account.apiKey, account.accountId || undefined, account.wechatAccountId);
224
+ const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
225
+ const isChatroom = params.to?.includes("@chatroom");
226
+ let filePath = params.mediaUrl || "";
184
227
  let result;
185
228
  if (isChatroom) {
186
229
  result = await client.sendChatroomMessage({
187
- wechatAccountId: params.account.wechatAccountId,
230
+ wechatAccountId: account.wechatAccountId,
188
231
  chatroomId: params.to,
189
- content: params.filePath || '',
190
- type: 'file',
232
+ content: filePath,
233
+ type: "file",
191
234
  });
192
235
  }
193
236
  else {
194
237
  result = await client.sendFriendMessage({
195
- wechatAccountId: params.account.wechatAccountId,
238
+ wechatAccountId: account.wechatAccountId,
196
239
  friendWechatId: params.to,
197
- content: params.filePath || '',
198
- type: 'file',
240
+ content: filePath,
241
+ type: "file",
199
242
  });
200
243
  }
201
244
  return { messageId: result.messageId };
202
245
  },
203
246
  },
204
- // Additional outbound handlers can be added here for:
205
- // - sendLink: Send link cards
206
- // - sendLocation: Send location
207
- // - sendContact: Send contact card
208
247
  },
209
- // Additional capabilities
210
- // - Describe the channel's message types and features
211
248
  capabilities: {
212
249
  supportedMessageTypes: [
213
250
  "text",
@@ -218,20 +255,24 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
218
255
  "location",
219
256
  "contact",
220
257
  ],
221
- maxAttachmentSize: 25 * 1024 * 1024, // 25MB
222
- supportsMarkdown: false, // WeChat uses a limited markup
258
+ maxAttachmentSize: 25 * 1024 * 1024,
259
+ supportsMarkdown: false,
223
260
  supportsHtml: false,
224
261
  supportsEmoji: true,
225
- supportsReactions: false, // WeChat doesn't support reactions
226
- supportsThreads: true, // Can reply in chat
227
- supportsEditing: false, // Cannot edit sent messages
228
- supportsDeleting: false, // Cannot delete sent messages
262
+ supportsReactions: false,
263
+ supportsThreads: true,
264
+ supportsEditing: false,
265
+ supportsDeleting: false,
229
266
  },
230
267
  });
231
268
  /**
232
269
  * Helper function to handle inbound webhook messages
233
270
  * This should be called from your HTTP route handler
234
271
  *
272
+ * Supports multi-account routing via AccountRegistry:
273
+ * - Looks up agentId from wechatAccountId
274
+ * - Dispatches to correct agent
275
+ *
235
276
  * Integrates with Cache Manager for:
236
277
  * - Local MD file caching
237
278
  * - User profile storage
@@ -241,31 +282,48 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
241
282
  */
242
283
  export async function handleInboundMessage(api, payload, cfg) {
243
284
  const { event, accountId, wechatAccountId, message, friendRequest } = payload;
244
- if (event === 'message' && message) {
285
+ if (event === "message" && message) {
245
286
  // Determine conversation type
246
287
  const isChatroom = !!message.chatroomId;
247
288
  const conversationId = isChatroom
248
289
  ? message.chatroomId
249
- : message.fromUser || message.toUser || '';
290
+ : message.fromUser || message.toUser || "";
291
+ // Determine the account ID for this message
292
+ // Priority: payload.accountId > wechatAccountId lookup > "default"
293
+ const resolvedAccountId = accountId || wechatAccountId || "default";
294
+ // Look up agentId for routing
295
+ let agentId = null;
296
+ if (cfg) {
297
+ const registry = getAccountRegistry();
298
+ // Try by wechatAccountId first
299
+ agentId = registry.getAgentId(wechatAccountId || "");
300
+ // Fallback to accountId
301
+ if (!agentId && accountId) {
302
+ const regAccount = registry.getByAccountId(accountId);
303
+ agentId = regAccount?.agentId || null;
304
+ }
305
+ }
250
306
  // Convert to cache format and store locally
251
- if (cacheManager && accountId) {
307
+ if (cfg) {
252
308
  try {
253
- await cacheManager.onMessage({
309
+ const cache = getCacheManager(cfg);
310
+ const wechatMessage = {
254
311
  messageId: message.messageId,
255
312
  msgSvrId: message.msgSvrId,
256
- accountId: accountId,
257
- conversationType: isChatroom ? 'chatroom' : 'friend',
313
+ accountId: resolvedAccountId,
314
+ conversationType: isChatroom ? "chatroom" : "friend",
258
315
  conversationId,
259
- senderId: message.fromUser || '',
316
+ senderId: message.fromUser || "",
260
317
  content: message.content,
261
318
  messageType: message.type || 1,
262
319
  timestamp: message.timestamp || Date.now(),
263
320
  isSelf: message.isSelf || false,
264
- direction: message.isSelf ? 'outbound' : 'inbound',
265
- });
321
+ direction: message.isSelf ? "outbound" : "inbound",
322
+ };
323
+ await cache.onMessage(wechatMessage);
266
324
  }
267
325
  catch (err) {
268
- console.error('[CelPhoneWeChat] Cache write failed:', err);
326
+ console.error("[CelPhoneWeChat] Cache write failed:", err);
269
327
  }
270
328
  }
271
329
  // Convert WorkPhone message format to OpenClaw format
@@ -273,32 +331,34 @@ export async function handleInboundMessage(api, payload, cfg) {
273
331
  id: message.messageId,
274
332
  rawId: message.msgSvrId || message.messageId,
275
333
  conversation: {
276
- type: isChatroom ? 'group' : 'dm',
334
+ type: isChatroom ? "group" : "dm",
277
335
  id: conversationId,
278
336
  chatroomId: isChatroom ? conversationId : undefined,
279
337
  },
280
338
  sender: {
281
- id: message.fromUser || '',
339
+ id: message.fromUser || "",
282
340
  platformId: message.wechatId,
283
341
  },
284
342
  content: {
285
- type: message.type === 1 ? 'text' : 'media',
343
+ type: message.type === 1 ? "text" : "media",
286
344
  text: message.content,
287
345
  },
288
346
  timestamp: new Date(message.timestamp || Date.now()),
289
347
  isSelf: message.isSelf || false,
348
+ agentId, // Include agentId for routing
349
+ accountId: resolvedAccountId,
290
350
  };
291
351
  // Dispatch to OpenClaw
292
352
  await api.inbound.dispatchMessage(openclawMessage);
293
353
  }
294
- else if (event === 'friend_request' && friendRequest) {
354
+ else if (event === "friend_request" && friendRequest) {
295
355
  // Handle friend request
296
356
  await api.inbound.dispatchFriendRequest({
297
357
  id: friendRequest.v1,
298
358
  platformId: friendRequest.fromUser,
299
359
  scene: friendRequest.scene,
300
360
  ticket: friendRequest.ticket,
301
- accountId,
361
+ accountId: accountId || wechatAccountId,
302
362
  wechatAccountId,
303
363
  });
304
364
  }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Client Pool - Per-account client management
3
+ *
4
+ * Manages WorkPhone API clients per account to avoid creating
5
+ * new clients for each message and ensure credential isolation.
6
+ */
7
+ import { WorkPhoneWeChatClient } from "./client.js";
8
+ /**
9
+ * Client pool for WorkPhone WeChat clients
10
+ */
11
+ class ClientPool {
12
+ clients = new Map();
13
+ /**
14
+ * Get or create a client for the given account
15
+ */
16
+ getClient(accountId, config) {
17
+ if (this.clients.has(accountId)) {
18
+ return this.clients.get(accountId);
19
+ }
20
+ const client = new WorkPhoneWeChatClient(config);
21
+ this.clients.set(accountId, client);
22
+ return client;
23
+ }
24
+ /**
25
+ * Check if client exists
26
+ */
27
+ hasClient(accountId) {
28
+ return this.clients.has(accountId);
29
+ }
30
+ /**
31
+ * Remove a client by accountId
32
+ */
33
+ removeClient(accountId) {
34
+ return this.clients.delete(accountId);
35
+ }
36
+ /**
37
+ * Clear all clients
38
+ */
39
+ clear() {
40
+ this.clients.clear();
41
+ }
42
+ /**
43
+ * Get all account IDs with active clients
44
+ */
45
+ getActiveAccountIds() {
46
+ return Array.from(this.clients.keys());
47
+ }
48
+ /**
49
+ * Get pool size
50
+ */
51
+ get size() {
52
+ return this.clients.size;
53
+ }
54
+ }
55
+ // Singleton instance
56
+ let clientPool = null;
57
+ /**
58
+ * Get or create the client pool singleton
59
+ */
60
+ export function getClientPool() {
61
+ if (!clientPool) {
62
+ clientPool = new ClientPool();
63
+ }
64
+ return clientPool;
65
+ }
66
+ /**
67
+ * Get a client for a specific account
68
+ */
69
+ export function getWorkPhoneClient(accountId, config) {
70
+ return getClientPool().getClient(accountId, config);
71
+ }
72
+ /**
73
+ * Create a client config from resolved account
74
+ */
75
+ export function createClientConfig(baseUrl, apiKey, accountId, wechatAccountId) {
76
+ return {
77
+ baseUrl,
78
+ apiKey,
79
+ accountId,
80
+ wechatAccountId,
81
+ };
82
+ }
package/index.ts CHANGED
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * WorkPhone WeChat Plugin - enables OpenClaw to send/receive WeChat messages
5
5
  * through the WorkPhone API platform.
6
+ *
7
+ * Multi-account support:
8
+ * - Each account in accounts[] can bind to a different agent
9
+ * - Webhook routing uses AccountRegistry to find agentId
10
+ * - cfg is passed to handleInboundMessage for cache isolation
6
11
  */
7
12
 
8
13
  import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
@@ -42,9 +47,12 @@ export default defineChannelPluginEntry({
42
47
  auth: "plugin", // Plugin-managed auth - verify signatures yourself
43
48
  handler: async (req, res) => {
44
49
  try {
50
+ // Get config from request (provided by OpenClaw framework)
51
+ const cfg = (req as any).cfg;
52
+
45
53
  // Parse the webhook payload
46
54
  // The exact format depends on WorkPhone's webhook configuration
47
- const payload = req.body;
55
+ const payload = (req as any).body;
48
56
 
49
57
  // Verify the request is from WorkPhone
50
58
  // Add signature verification here if needed
@@ -55,8 +63,8 @@ export default defineChannelPluginEntry({
55
63
  return true;
56
64
  }
57
65
 
58
- // Handle the inbound message
59
- await handleInboundMessage(api, payload);
66
+ // Handle the inbound message with cfg for multi-account routing
67
+ await handleInboundMessage(api, payload, cfg);
60
68
 
61
69
  res.statusCode = 200;
62
70
  res.end("ok");
@@ -71,17 +79,19 @@ export default defineChannelPluginEntry({
71
79
  });
72
80
 
73
81
  // Register gateway method for outbound media handling
74
- api.registerGatewayMethod({
75
- method: "POST",
82
+ api.registerHttpRoute({
76
83
  path: "/celphone-wechat/media",
84
+ auth: "plugin",
77
85
  handler: async (req, res) => {
78
- const { messageId, mediaUrl, mediaType } = req.body;
86
+ const payload = (req as any).body;
87
+ const { messageId, mediaUrl, mediaType } = payload || {};
79
88
 
80
89
  // Handle media upload/send through WorkPhone
81
90
  // This is called when the bot needs to send media files
82
91
 
92
+ res.setHeader("Content-Type", "application/json");
83
93
  res.statusCode = 200;
84
- res.json({ success: true, messageId });
94
+ res.end(JSON.stringify({ success: true, messageId }));
85
95
  return true;
86
96
  },
87
97
  });
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@gloablehive/celphone-wechat-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
- "description": "OpenClaw channel plugin for workphone-wechat API - enables sending/receiving WeChat messages through workphone",
5
+ "description": "OpenClaw channel plugin for workphone-wechat API - enables sending/receiving WeChat messages through workphone. Supports multi-account with per-account agent binding.",
6
6
  "main": "index.ts",
7
7
  "scripts": {
8
8
  "test": "npx tsx test-cache.ts",
@@ -24,7 +24,7 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@gloablehive/wechat-cache": "^1.0.0",
27
+ "@gloablehive/wechat-cache": "^1.0.2",
28
28
  "openclaw": ">=1.0.0"
29
29
  },
30
30
  "devDependencies": {
package/src/channel.ts CHANGED
@@ -9,14 +9,10 @@
9
9
  * The agent communicates with ALL friends and groups under that WeChat account,
10
10
  * not just one DM context. This is "human mode" vs "bot mode".
11
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
12
+ * Multi-account support:
13
+ * - Config supports accounts array with per-account credentials
14
+ * - Each account can be bound to a different agent
15
+ * - Security policies are per-account isolated
20
16
  */
21
17
 
22
18
  import {
@@ -24,18 +20,61 @@ import {
24
20
  createChannelPluginBase,
25
21
  } from "openclaw/plugin-sdk/core";
26
22
  import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
27
- import { createWorkPhoneClient, type WebhookPayload } from "./client.js";
23
+ import type { WebhookPayload } from "./client.js";
28
24
 
29
25
  // Import cache modules from shared package
30
26
  import {
31
27
  createCacheManager,
32
28
  CacheManager,
33
29
  WeChatAccount,
30
+ WeChatMessage,
31
+ AccountRegistry,
32
+ createAccountRegistry,
33
+ type RegisteredAccount,
34
34
  } from "@gloablehive/wechat-cache";
35
35
 
36
+ // Import client pool
37
+ import { getWorkPhoneClient, createClientConfig, getClientPool } from "./client-pool.js";
38
+
36
39
  // Cache manager instance (lazy initialized)
37
40
  let cacheManager: CacheManager | null = null;
38
41
 
42
+ // Account registry for routing
43
+ let accountRegistry: AccountRegistry | null = null;
44
+
45
+ /**
46
+ * Get or create account registry
47
+ */
48
+ function getAccountRegistry(): AccountRegistry {
49
+ if (!accountRegistry) {
50
+ accountRegistry = createAccountRegistry();
51
+ }
52
+ return accountRegistry;
53
+ }
54
+
55
+ /**
56
+ * Initialize account registry from config
57
+ */
58
+ function initializeRegistry(cfg: OpenClawConfig): void {
59
+ const registry = getAccountRegistry();
60
+ registry.clear();
61
+
62
+ const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
63
+ const accounts = (section?.accounts || []) as WeChatAccount[];
64
+
65
+ // Register each account
66
+ for (const account of accounts) {
67
+ if (account.enabled !== false) {
68
+ const registeredAccount: RegisteredAccount = {
69
+ ...account,
70
+ channelType: "celphone-wechat",
71
+ registeredAt: Date.now(),
72
+ };
73
+ registry.register(registeredAccount);
74
+ }
75
+ }
76
+ }
77
+
39
78
  /**
40
79
  * Get or create cache manager
41
80
  */
@@ -48,16 +87,19 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
48
87
  // If no accounts configured, create default from main config
49
88
  if (accounts.length === 0 && section?.wechatAccountId) {
50
89
  accounts.push({
51
- accountId: section.accountId || 'default',
90
+ accountId: section.accountId || "default",
52
91
  wechatAccountId: section.wechatAccountId,
53
92
  wechatId: section.wechatId || section.wechatAccountId,
54
- nickName: section.nickName || 'WeChat User',
93
+ nickName: section.nickName || "WeChat User",
55
94
  enabled: true,
56
95
  });
57
96
  }
58
97
 
98
+ // Initialize registry with accounts
99
+ initializeRegistry(cfg);
100
+
59
101
  const basePath = (globalThis as any)?.process?.env?.OPENCLAW_CACHE_PATH
60
- || '~/.openclaw/channels/celphone-wechat';
102
+ || "~/.openclaw/channels/celphone-wechat";
61
103
 
62
104
  cacheManager = createCacheManager({
63
105
  basePath,
@@ -69,14 +111,14 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
69
111
  } : undefined,
70
112
  syncConfig: section?.sync ? {
71
113
  databaseUrl: section.sync.databaseUrl,
72
- syncMode: section.sync.syncMode || 'interval',
114
+ syncMode: section.sync.syncMode || "interval",
73
115
  syncIntervalMs: section.sync.syncIntervalMs || 5 * 60 * 1000,
74
116
  } : undefined,
75
117
  });
76
118
 
77
- // Initialize cache manager
119
+ // Initialize cache manager - must await for ready state
78
120
  cacheManager.init().catch(err => {
79
- console.error('[CelPhoneWeChat] Cache manager init failed:', err);
121
+ console.error("[CelPhoneWeChat] Cache manager init failed:", err);
80
122
  });
81
123
 
82
124
  return cacheManager;
@@ -84,26 +126,70 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
84
126
 
85
127
  export interface CelPhoneWeChatResolvedAccount {
86
128
  accountId: string | null;
129
+ agentId: string | null;
87
130
  apiKey: string;
88
131
  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
132
+ wechatAccountId: string;
133
+ wechatId: string;
134
+ nickName: string;
92
135
  allowFrom: string[];
93
136
  dmPolicy: string | undefined;
94
137
  }
95
138
 
139
+ /**
140
+ * List all account IDs from config
141
+ */
142
+ function listAccountIds(cfg: OpenClawConfig): string[] {
143
+ const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
144
+ const accounts = (section?.accounts || []) as WeChatAccount[];
145
+
146
+ if (accounts.length > 0) {
147
+ return accounts.map(a => a.accountId).filter(Boolean);
148
+ }
149
+
150
+ // Backward compatibility: single account
151
+ if (section?.wechatAccountId) {
152
+ return [section.accountId || "default"];
153
+ }
154
+
155
+ return [];
156
+ }
157
+
96
158
  /**
97
159
  * Resolve account from OpenClaw config
98
160
  *
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.
161
+ * Supports both multi-account (accounts array) and legacy (single) formats.
101
162
  */
102
163
  function resolveAccount(
103
164
  cfg: OpenClawConfig,
104
165
  accountId?: string | null
105
166
  ): CelPhoneWeChatResolvedAccount {
106
167
  const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
168
+ const accounts = (section?.accounts || []) as WeChatAccount[];
169
+
170
+ // Multi-account mode: look up from accounts array
171
+ if (accounts.length > 0) {
172
+ const targetAccountId = accountId || accounts[0]?.accountId;
173
+ const account = accounts.find(a => a.accountId === targetAccountId);
174
+
175
+ if (!account) {
176
+ throw new Error(`celphone-wechat: account not found - ${targetAccountId}`);
177
+ }
178
+
179
+ return {
180
+ accountId: account.accountId,
181
+ agentId: account.agentId || null,
182
+ apiKey: account.apiKey || section?.apiKey || "",
183
+ baseUrl: account.baseUrl || section?.baseUrl || "https://api.workphone.example.com",
184
+ wechatAccountId: account.wechatAccountId,
185
+ wechatId: account.wechatId || account.wechatAccountId,
186
+ nickName: account.nickName || "WeChat User",
187
+ allowFrom: account.allowFrom ?? [],
188
+ dmPolicy: account.dmPolicy,
189
+ };
190
+ }
191
+
192
+ // Legacy single-account mode
107
193
  const apiKey = section?.apiKey;
108
194
  const baseUrl = section?.baseUrl || "https://api.workphone.example.com";
109
195
  const wechatAccountId = section?.wechatAccountId;
@@ -118,11 +204,12 @@ function resolveAccount(
118
204
 
119
205
  return {
120
206
  accountId: accountId ?? null,
207
+ agentId: null,
121
208
  apiKey,
122
209
  baseUrl,
123
210
  wechatAccountId,
124
- wechatId: section?.wechatId || wechatAccountId, // Actual WeChat ID (wxid_xxx)
125
- nickName: section?.nickName || "WeChat User", // Display name
211
+ wechatId: section?.wechatId || wechatAccountId,
212
+ nickName: section?.nickName || "WeChat User",
126
213
  allowFrom: section?.allowFrom ?? [],
127
214
  dmPolicy: section?.dmSecurity,
128
215
  };
@@ -135,126 +222,110 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
135
222
  base: createChannelPluginBase({
136
223
  id: "celphone-wechat",
137
224
  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
- };
225
+ resolveAccountId: (params: any) => {
226
+ return resolveAccount(params.cfg, params.accountId)?.accountId || "";
147
227
  },
228
+ applyAccountConfig: (params) => params.cfg,
148
229
  },
149
- }),
230
+ }) as any,
150
231
 
151
- // DM security: who can message the bot
152
232
  security: {
153
233
  dm: {
154
234
  channelKey: "celphone-wechat",
155
235
  resolvePolicy: (account) => account.dmPolicy,
156
236
  resolveAllowFrom: (account) => account.allowFrom,
157
- defaultPolicy: "allowlist", // Default to allowlist for security
237
+ defaultPolicy: "allowlist",
158
238
  },
159
239
  },
160
240
 
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
241
  threading: {
168
- topLevelReplyToMode: "reply", // Reply to the last message in the thread
242
+ topLevelReplyToMode: "reply",
169
243
  },
170
244
 
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
245
  outbound: {
246
+ channel: "celphone-wechat",
180
247
  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'));
248
+ channel: "celphone-wechat",
249
+ sendText: async (params: any) => {
250
+ const cfg = params.cfg;
251
+ const accountId = params.accountId;
252
+
253
+ // Resolve account to get credentials
254
+ const account = resolveAccount(cfg, accountId);
255
+
256
+ // Use client pool for connection reuse
257
+ const clientConfig = createClientConfig(
258
+ account.baseUrl,
259
+ account.apiKey,
260
+ account.accountId || undefined,
261
+ account.wechatAccountId
262
+ );
263
+ const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
264
+
265
+ const isChatroom = params.to?.includes("@chatroom");
194
266
 
195
267
  let result;
196
268
  if (isChatroom) {
197
- // Send to chatroom (group)
198
269
  result = await client.sendChatroomMessage({
199
- wechatAccountId: params.account.wechatAccountId,
270
+ wechatAccountId: account.wechatAccountId,
200
271
  chatroomId: params.to,
201
272
  content: params.text,
202
- type: 'text',
273
+ type: "text",
203
274
  });
204
275
  } else {
205
- // Send to friend (DM)
206
276
  result = await client.sendFriendMessage({
207
- wechatAccountId: params.account.wechatAccountId,
277
+ wechatAccountId: account.wechatAccountId,
208
278
  friendWechatId: params.to,
209
279
  content: params.text,
210
- type: 'text',
280
+ type: "text",
211
281
  });
212
282
  }
213
283
 
214
284
  return { messageId: result.messageId };
215
285
  },
216
286
 
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
- });
287
+ sendMedia: async (params: any) => {
288
+ const cfg = params.cfg;
289
+ const accountId = params.accountId;
290
+
291
+ // Resolve account to get credentials
292
+ const account = resolveAccount(cfg, accountId);
293
+
294
+ // Use client pool
295
+ const clientConfig = createClientConfig(
296
+ account.baseUrl,
297
+ account.apiKey,
298
+ account.accountId || undefined,
299
+ account.wechatAccountId
300
+ );
301
+ const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
302
+
303
+ const isChatroom = params.to?.includes("@chatroom");
225
304
 
226
- const isChatroom = params.metadata?.conversationType === 'group' ||
227
- (params.to && params.to.includes('@chatroom'));
305
+ let filePath = params.mediaUrl || "";
228
306
 
229
307
  let result;
230
308
  if (isChatroom) {
231
309
  result = await client.sendChatroomMessage({
232
- wechatAccountId: params.account.wechatAccountId,
310
+ wechatAccountId: account.wechatAccountId,
233
311
  chatroomId: params.to,
234
- content: params.filePath || '',
235
- type: 'file',
312
+ content: filePath,
313
+ type: "file",
236
314
  });
237
315
  } else {
238
316
  result = await client.sendFriendMessage({
239
- wechatAccountId: params.account.wechatAccountId,
317
+ wechatAccountId: account.wechatAccountId,
240
318
  friendWechatId: params.to,
241
- content: params.filePath || '',
242
- type: 'file',
319
+ content: filePath,
320
+ type: "file",
243
321
  });
244
322
  }
245
323
 
246
324
  return { messageId: result.messageId };
247
325
  },
248
326
  },
327
+ } as any,
249
328
 
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
329
  capabilities: {
259
330
  supportedMessageTypes: [
260
331
  "text",
@@ -265,21 +336,25 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
265
336
  "location",
266
337
  "contact",
267
338
  ],
268
- maxAttachmentSize: 25 * 1024 * 1024, // 25MB
269
- supportsMarkdown: false, // WeChat uses a limited markup
339
+ maxAttachmentSize: 25 * 1024 * 1024,
340
+ supportsMarkdown: false,
270
341
  supportsHtml: false,
271
342
  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
343
+ supportsReactions: false,
344
+ supportsThreads: true,
345
+ supportsEditing: false,
346
+ supportsDeleting: false,
276
347
  },
277
- });
348
+ } as any);
278
349
 
279
350
  /**
280
351
  * Helper function to handle inbound webhook messages
281
352
  * This should be called from your HTTP route handler
282
353
  *
354
+ * Supports multi-account routing via AccountRegistry:
355
+ * - Looks up agentId from wechatAccountId
356
+ * - Dispatches to correct agent
357
+ *
283
358
  * Integrates with Cache Manager for:
284
359
  * - Local MD file caching
285
360
  * - User profile storage
@@ -294,31 +369,50 @@ export async function handleInboundMessage(
294
369
  ): Promise<void> {
295
370
  const { event, accountId, wechatAccountId, message, friendRequest } = payload;
296
371
 
297
- if (event === 'message' && message) {
372
+ if (event === "message" && message) {
298
373
  // Determine conversation type
299
374
  const isChatroom = !!(message as any).chatroomId;
300
375
  const conversationId = isChatroom
301
376
  ? (message as any).chatroomId
302
- : message.fromUser || message.toUser || '';
377
+ : message.fromUser || message.toUser || "";
378
+
379
+ // Determine the account ID for this message
380
+ // Priority: payload.accountId > wechatAccountId lookup > "default"
381
+ const resolvedAccountId = accountId || wechatAccountId || "default";
382
+
383
+ // Look up agentId for routing
384
+ let agentId: string | null = null;
385
+ if (cfg) {
386
+ const registry = getAccountRegistry();
387
+ // Try by wechatAccountId first
388
+ agentId = registry.getAgentId(wechatAccountId || "");
389
+ // Fallback to accountId
390
+ if (!agentId && accountId) {
391
+ const regAccount = registry.getByAccountId(accountId);
392
+ agentId = regAccount?.agentId || null;
393
+ }
394
+ }
303
395
 
304
396
  // Convert to cache format and store locally
305
- if (cacheManager && accountId) {
397
+ if (cfg) {
306
398
  try {
307
- await cacheManager.onMessage({
399
+ const cache = getCacheManager(cfg);
400
+ const wechatMessage: WeChatMessage = {
308
401
  messageId: message.messageId,
309
402
  msgSvrId: message.msgSvrId,
310
- accountId: accountId,
311
- conversationType: isChatroom ? 'chatroom' : 'friend',
403
+ accountId: resolvedAccountId,
404
+ conversationType: isChatroom ? "chatroom" : "friend",
312
405
  conversationId,
313
- senderId: message.fromUser || '',
406
+ senderId: message.fromUser || "",
314
407
  content: message.content,
315
408
  messageType: message.type || 1,
316
409
  timestamp: message.timestamp || Date.now(),
317
410
  isSelf: message.isSelf || false,
318
- direction: message.isSelf ? 'outbound' : 'inbound',
319
- });
411
+ direction: message.isSelf ? "outbound" : "inbound",
412
+ };
413
+ await cache.onMessage(wechatMessage);
320
414
  } catch (err) {
321
- console.error('[CelPhoneWeChat] Cache write failed:', err);
415
+ console.error("[CelPhoneWeChat] Cache write failed:", err);
322
416
  }
323
417
  }
324
418
 
@@ -327,32 +421,34 @@ export async function handleInboundMessage(
327
421
  id: message.messageId,
328
422
  rawId: message.msgSvrId || message.messageId,
329
423
  conversation: {
330
- type: isChatroom ? 'group' as const : 'dm' as const,
424
+ type: isChatroom ? "group" as const : "dm" as const,
331
425
  id: conversationId,
332
426
  chatroomId: isChatroom ? conversationId : undefined,
333
427
  },
334
428
  sender: {
335
- id: message.fromUser || '',
429
+ id: message.fromUser || "",
336
430
  platformId: message.wechatId,
337
431
  },
338
432
  content: {
339
- type: message.type === 1 ? 'text' : 'media',
433
+ type: message.type === 1 ? "text" as const : "media" as const,
340
434
  text: message.content,
341
435
  },
342
436
  timestamp: new Date(message.timestamp || Date.now()),
343
437
  isSelf: message.isSelf || false,
438
+ agentId, // Include agentId for routing
439
+ accountId: resolvedAccountId,
344
440
  };
345
441
 
346
442
  // Dispatch to OpenClaw
347
443
  await api.inbound.dispatchMessage(openclawMessage);
348
- } else if (event === 'friend_request' && friendRequest) {
444
+ } else if (event === "friend_request" && friendRequest) {
349
445
  // Handle friend request
350
446
  await api.inbound.dispatchFriendRequest({
351
447
  id: friendRequest.v1,
352
448
  platformId: friendRequest.fromUser,
353
449
  scene: friendRequest.scene,
354
450
  ticket: friendRequest.ticket,
355
- accountId,
451
+ accountId: accountId || wechatAccountId,
356
452
  wechatAccountId,
357
453
  });
358
454
  }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Client Pool - Per-account client management
3
+ *
4
+ * Manages WorkPhone API clients per account to avoid creating
5
+ * new clients for each message and ensure credential isolation.
6
+ */
7
+
8
+ import { WorkPhoneWeChatClient, type WorkPhoneConfig } from "./client.js";
9
+
10
+ /**
11
+ * Client pool for WorkPhone WeChat clients
12
+ */
13
+ class ClientPool {
14
+ private clients: Map<string, WorkPhoneWeChatClient> = new Map();
15
+
16
+ /**
17
+ * Get or create a client for the given account
18
+ */
19
+ getClient(accountId: string, config: WorkPhoneConfig): WorkPhoneWeChatClient {
20
+ if (this.clients.has(accountId)) {
21
+ return this.clients.get(accountId)!;
22
+ }
23
+
24
+ const client = new WorkPhoneWeChatClient(config);
25
+ this.clients.set(accountId, client);
26
+ return client;
27
+ }
28
+
29
+ /**
30
+ * Check if client exists
31
+ */
32
+ hasClient(accountId: string): boolean {
33
+ return this.clients.has(accountId);
34
+ }
35
+
36
+ /**
37
+ * Remove a client by accountId
38
+ */
39
+ removeClient(accountId: string): boolean {
40
+ return this.clients.delete(accountId);
41
+ }
42
+
43
+ /**
44
+ * Clear all clients
45
+ */
46
+ clear(): void {
47
+ this.clients.clear();
48
+ }
49
+
50
+ /**
51
+ * Get all account IDs with active clients
52
+ */
53
+ getActiveAccountIds(): string[] {
54
+ return Array.from(this.clients.keys());
55
+ }
56
+
57
+ /**
58
+ * Get pool size
59
+ */
60
+ get size(): number {
61
+ return this.clients.size;
62
+ }
63
+ }
64
+
65
+ // Singleton instance
66
+ let clientPool: ClientPool | null = null;
67
+
68
+ /**
69
+ * Get or create the client pool singleton
70
+ */
71
+ export function getClientPool(): ClientPool {
72
+ if (!clientPool) {
73
+ clientPool = new ClientPool();
74
+ }
75
+ return clientPool;
76
+ }
77
+
78
+ /**
79
+ * Get a client for a specific account
80
+ */
81
+ export function getWorkPhoneClient(
82
+ accountId: string,
83
+ config: WorkPhoneConfig
84
+ ): WorkPhoneWeChatClient {
85
+ return getClientPool().getClient(accountId, config);
86
+ }
87
+
88
+ /**
89
+ * Create a client config from resolved account
90
+ */
91
+ export function createClientConfig(
92
+ baseUrl: string,
93
+ apiKey: string,
94
+ accountId: string | undefined,
95
+ wechatAccountId: string | undefined
96
+ ): WorkPhoneConfig {
97
+ return {
98
+ baseUrl,
99
+ apiKey,
100
+ accountId,
101
+ wechatAccountId,
102
+ };
103
+ }
package/src/client.ts CHANGED
@@ -107,7 +107,7 @@ export class WorkPhoneWeChatClient {
107
107
  throw new Error(`WorkPhone API error: ${response.status} - ${error}`);
108
108
  }
109
109
 
110
- return response.json();
110
+ return response.json() as Promise<T>;
111
111
  }
112
112
 
113
113
  // ========== WeChat Account Operations ==========
package/test-cache.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  CacheManager,
11
11
  WeChatAccount,
12
12
  WeChatMessage,
13
- } from './src/cache/index.js';
13
+ } from "@gloablehive/wechat-cache";
14
14
 
15
15
  const TEST_CACHE_PATH = '/tmp/wechat-cache-test';
16
16
  const ENCODING = 'utf-8';