@gloablehive/celphone-wechat-plugin 1.0.1 → 1.1.1

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;
@@ -9,21 +9,50 @@
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
18
  // Import cache modules from shared package
24
- import { createCacheManager, } from "@gloablehive/wechat-cache";
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
+ let cacheManagerReady = null;
25
+ // Account registry for routing
26
+ let accountRegistry = null;
27
+ /**
28
+ * Get or create account registry
29
+ */
30
+ function getAccountRegistry() {
31
+ if (!accountRegistry) {
32
+ accountRegistry = createAccountRegistry();
33
+ }
34
+ return accountRegistry;
35
+ }
36
+ /**
37
+ * Initialize account registry from config
38
+ */
39
+ function initializeRegistry(cfg) {
40
+ const registry = getAccountRegistry();
41
+ registry.clear();
42
+ const section = cfg.channels?.["celphone-wechat"];
43
+ const accounts = (section?.accounts || []);
44
+ // Register each account
45
+ for (const account of accounts) {
46
+ if (account.enabled !== false) {
47
+ const registeredAccount = {
48
+ ...account,
49
+ channelType: "celphone-wechat",
50
+ registeredAt: Date.now(),
51
+ };
52
+ registry.register(registeredAccount);
53
+ }
54
+ }
55
+ }
27
56
  /**
28
57
  * Get or create cache manager
29
58
  */
@@ -42,11 +71,23 @@ function getCacheManager(cfg) {
42
71
  enabled: true,
43
72
  });
44
73
  }
74
+ // Initialize registry with accounts
75
+ initializeRegistry(cfg);
45
76
  const basePath = globalThis?.process?.env?.OPENCLAW_CACHE_PATH
46
77
  || "~/.openclaw/channels/celphone-wechat";
78
+ // Build knowledge config if provided in section
79
+ let knowledgeConfig;
80
+ if (section?.knowledge) {
81
+ knowledgeConfig = {
82
+ ...section.knowledge,
83
+ nodeId: section.knowledge.nodeId || 'celphone-wechat-node',
84
+ agentId: section.knowledge.agentId || section.agentId || 'default-agent',
85
+ };
86
+ }
47
87
  cacheManager = createCacheManager({
48
88
  basePath,
49
89
  accounts,
90
+ knowledgeConfig,
50
91
  saasConfig: section?.saas ? {
51
92
  apiBaseUrl: section.saas.apiBaseUrl,
52
93
  apiKey: section.saas.apiKey,
@@ -58,20 +99,62 @@ function getCacheManager(cfg) {
58
99
  syncIntervalMs: section.sync.syncIntervalMs || 5 * 60 * 1000,
59
100
  } : undefined,
60
101
  });
61
- // Initialize cache manager
62
- cacheManager.init().catch(err => {
102
+ // Initialize cache manager - await for ready state
103
+ cacheManagerReady = cacheManager.init().catch(err => {
63
104
  console.error("[CelPhoneWeChat] Cache manager init failed:", err);
105
+ throw err; // Re-throw so caller knows init failed
64
106
  });
65
107
  return cacheManager;
66
108
  }
109
+ /**
110
+ * Get cache manager ready promise - await before processing messages
111
+ */
112
+ export function getCacheManagerReady() {
113
+ return cacheManagerReady;
114
+ }
115
+ /**
116
+ * List all account IDs from config
117
+ */
118
+ function listAccountIds(cfg) {
119
+ const section = cfg.channels?.["celphone-wechat"];
120
+ const accounts = (section?.accounts || []);
121
+ if (accounts.length > 0) {
122
+ return accounts.map(a => a.accountId).filter(Boolean);
123
+ }
124
+ // Backward compatibility: single account
125
+ if (section?.wechatAccountId) {
126
+ return [section.accountId || "default"];
127
+ }
128
+ return [];
129
+ }
67
130
  /**
68
131
  * Resolve account from OpenClaw config
69
132
  *
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.
133
+ * Supports both multi-account (accounts array) and legacy (single) formats.
72
134
  */
73
135
  function resolveAccount(cfg, accountId) {
74
136
  const section = cfg.channels?.["celphone-wechat"];
137
+ const accounts = (section?.accounts || []);
138
+ // Multi-account mode: look up from accounts array
139
+ if (accounts.length > 0) {
140
+ const targetAccountId = accountId || accounts[0]?.accountId;
141
+ const account = accounts.find(a => a.accountId === targetAccountId);
142
+ if (!account) {
143
+ throw new Error(`celphone-wechat: account not found - ${targetAccountId}`);
144
+ }
145
+ return {
146
+ accountId: account.accountId,
147
+ agentId: account.agentId || null,
148
+ apiKey: account.apiKey || section?.apiKey || "",
149
+ baseUrl: account.baseUrl || section?.baseUrl || "https://api.workphone.example.com",
150
+ wechatAccountId: account.wechatAccountId,
151
+ wechatId: account.wechatId || account.wechatAccountId,
152
+ nickName: account.nickName || "WeChat User",
153
+ allowFrom: account.allowFrom ?? [],
154
+ dmPolicy: account.dmPolicy,
155
+ };
156
+ }
157
+ // Legacy single-account mode
75
158
  const apiKey = section?.apiKey;
76
159
  const baseUrl = section?.baseUrl || "https://api.workphone.example.com";
77
160
  const wechatAccountId = section?.wechatAccountId;
@@ -83,11 +166,12 @@ function resolveAccount(cfg, accountId) {
83
166
  }
84
167
  return {
85
168
  accountId: accountId ?? null,
169
+ agentId: null,
86
170
  apiKey,
87
171
  baseUrl,
88
172
  wechatAccountId,
89
- wechatId: section?.wechatId || wechatAccountId, // Actual WeChat ID (wxid_xxx)
90
- nickName: section?.nickName || "WeChat User", // Display name
173
+ wechatId: section?.wechatId || wechatAccountId,
174
+ nickName: section?.nickName || "WeChat User",
91
175
  allowFrom: section?.allowFrom ?? [],
92
176
  dmPolicy: section?.dmSecurity,
93
177
  };
@@ -122,27 +206,25 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
122
206
  channel: "celphone-wechat",
123
207
  sendText: async (params) => {
124
208
  const cfg = params.cfg;
125
- const section = cfg.channels?.["celphone-wechat"];
126
- const client = createWorkPhoneClient({
127
- baseUrl: section?.baseUrl || "https://api.workphone.example.com",
128
- apiKey: section?.apiKey,
129
- accountId: section?.accountId || undefined,
130
- wechatAccountId: section?.wechatAccountId,
131
- });
209
+ const accountId = params.accountId;
210
+ // Resolve account to get credentials
211
+ const account = resolveAccount(cfg, accountId);
212
+ // Use client pool for connection reuse
213
+ const clientConfig = createClientConfig(account.baseUrl, account.apiKey, account.accountId || undefined, account.wechatAccountId);
214
+ const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
132
215
  const isChatroom = params.to?.includes("@chatroom");
133
216
  let result;
134
217
  if (isChatroom) {
135
218
  result = await client.sendChatroomMessage({
136
- wechatAccountId: section?.wechatAccountId,
219
+ wechatAccountId: account.wechatAccountId,
137
220
  chatroomId: params.to,
138
221
  content: params.text,
139
222
  type: "text",
140
223
  });
141
224
  }
142
225
  else {
143
- // Send to friend (DM)
144
226
  result = await client.sendFriendMessage({
145
- wechatAccountId: section?.wechatAccountId,
227
+ wechatAccountId: account.wechatAccountId,
146
228
  friendWechatId: params.to,
147
229
  content: params.text,
148
230
  type: "text",
@@ -150,41 +232,20 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
150
232
  }
151
233
  return { messageId: result.messageId };
152
234
  },
153
- // Send media - also support both DM and group
154
235
  sendMedia: async (params) => {
155
236
  const cfg = params.cfg;
156
- const section = cfg.channels?.["celphone-wechat"];
157
- const client = createWorkPhoneClient({
158
- baseUrl: section?.baseUrl || "https://api.workphone.example.com",
159
- apiKey: section?.apiKey,
160
- accountId: section?.accountId || undefined,
161
- wechatAccountId: section?.wechatAccountId,
162
- });
237
+ const accountId = params.accountId;
238
+ // Resolve account to get credentials
239
+ const account = resolveAccount(cfg, accountId);
240
+ // Use client pool
241
+ const clientConfig = createClientConfig(account.baseUrl, account.apiKey, account.accountId || undefined, account.wechatAccountId);
242
+ const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
163
243
  const isChatroom = params.to?.includes("@chatroom");
164
- // Get file path from mediaUrl or mediaReadFile
165
- let filePath = "";
166
- if (params.mediaUrl) {
167
- filePath = params.mediaUrl;
168
- }
169
- else if (params.mediaReadFile) {
170
- // Read file from local roots
171
- const roots = params.mediaLocalRoots || ["./"];
172
- for (const root of roots) {
173
- try {
174
- // Need to find the file path from the message
175
- // In practice, we'd need to look at the attachment info
176
- filePath = root;
177
- break;
178
- }
179
- catch {
180
- // Try next root
181
- }
182
- }
183
- }
244
+ let filePath = params.mediaUrl || "";
184
245
  let result;
185
246
  if (isChatroom) {
186
247
  result = await client.sendChatroomMessage({
187
- wechatAccountId: section?.wechatAccountId,
248
+ wechatAccountId: account.wechatAccountId,
188
249
  chatroomId: params.to,
189
250
  content: filePath,
190
251
  type: "file",
@@ -192,7 +253,7 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
192
253
  }
193
254
  else {
194
255
  result = await client.sendFriendMessage({
195
- wechatAccountId: section?.wechatAccountId,
256
+ wechatAccountId: account.wechatAccountId,
196
257
  friendWechatId: params.to,
197
258
  content: filePath,
198
259
  type: "file",
@@ -202,8 +263,6 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
202
263
  },
203
264
  },
204
265
  },
205
- // Additional capabilities
206
- // - Describe the channel's message types and features
207
266
  capabilities: {
208
267
  supportedMessageTypes: [
209
268
  "text",
@@ -214,13 +273,13 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
214
273
  "location",
215
274
  "contact",
216
275
  ],
217
- maxAttachmentSize: 25 * 1024 * 1024, // 25MB
218
- supportsMarkdown: false, // WeChat uses a limited markup
276
+ maxAttachmentSize: 25 * 1024 * 1024,
277
+ supportsMarkdown: false,
219
278
  supportsHtml: false,
220
279
  supportsEmoji: true,
221
- supportsReactions: false, // WeChat doesn't support reactions
222
- supportsThreads: true, // Can reply in chat
223
- supportsEditing: false, // Cannot edit sent messages
280
+ supportsReactions: false,
281
+ supportsThreads: true,
282
+ supportsEditing: false,
224
283
  supportsDeleting: false,
225
284
  },
226
285
  });
@@ -228,6 +287,10 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
228
287
  * Helper function to handle inbound webhook messages
229
288
  * This should be called from your HTTP route handler
230
289
  *
290
+ * Supports multi-account routing via AccountRegistry:
291
+ * - Looks up agentId from wechatAccountId
292
+ * - Dispatches to correct agent
293
+ *
231
294
  * Integrates with Cache Manager for:
232
295
  * - Local MD file caching
233
296
  * - User profile storage
@@ -243,6 +306,21 @@ export async function handleInboundMessage(api, payload, cfg) {
243
306
  const conversationId = isChatroom
244
307
  ? message.chatroomId
245
308
  : message.fromUser || message.toUser || "";
309
+ // Determine the account ID for this message
310
+ // Priority: payload.accountId > wechatAccountId lookup > "default"
311
+ const resolvedAccountId = accountId || wechatAccountId || "default";
312
+ // Look up agentId for routing
313
+ let agentId = null;
314
+ if (cfg) {
315
+ const registry = getAccountRegistry();
316
+ // Try by wechatAccountId first
317
+ agentId = registry.getAgentId(wechatAccountId || "");
318
+ // Fallback to accountId
319
+ if (!agentId && accountId) {
320
+ const regAccount = registry.getByAccountId(accountId);
321
+ agentId = regAccount?.agentId || null;
322
+ }
323
+ }
246
324
  // Convert to cache format and store locally
247
325
  if (cfg) {
248
326
  try {
@@ -250,7 +328,7 @@ export async function handleInboundMessage(api, payload, cfg) {
250
328
  const wechatMessage = {
251
329
  messageId: message.messageId,
252
330
  msgSvrId: message.msgSvrId,
253
- accountId: accountId || "default",
331
+ accountId: resolvedAccountId,
254
332
  conversationType: isChatroom ? "chatroom" : "friend",
255
333
  conversationId,
256
334
  senderId: message.fromUser || "",
@@ -285,6 +363,8 @@ export async function handleInboundMessage(api, payload, cfg) {
285
363
  },
286
364
  timestamp: new Date(message.timestamp || Date.now()),
287
365
  isSelf: message.isSelf || false,
366
+ agentId, // Include agentId for routing
367
+ accountId: resolvedAccountId,
288
368
  };
289
369
  // Dispatch to OpenClaw
290
370
  await api.inbound.dispatchMessage(openclawMessage);
@@ -296,7 +376,7 @@ export async function handleInboundMessage(api, payload, cfg) {
296
376
  platformId: friendRequest.fromUser,
297
377
  scene: friendRequest.scene,
298
378
  ticket: friendRequest.ticket,
299
- accountId,
379
+ accountId: accountId || wechatAccountId,
300
380
  wechatAccountId,
301
381
  });
302
382
  }
@@ -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,6 +47,9 @@ 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
55
  const payload = (req as any).body;
@@ -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");
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@gloablehive/celphone-wechat-plugin",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
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.1",
27
+ "@gloablehive/wechat-cache": "file:../wechat-cache",
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,7 +20,7 @@ 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 {
@@ -32,10 +28,54 @@ import {
32
28
  CacheManager,
33
29
  WeChatAccount,
34
30
  WeChatMessage,
31
+ AccountRegistry,
32
+ createAccountRegistry,
33
+ type RegisteredAccount,
34
+ type KnowledgeConfig,
35
35
  } from "@gloablehive/wechat-cache";
36
36
 
37
+ // Import client pool
38
+ import { getWorkPhoneClient, createClientConfig, getClientPool } from "./client-pool.js";
39
+
37
40
  // Cache manager instance (lazy initialized)
38
41
  let cacheManager: CacheManager | null = null;
42
+ let cacheManagerReady: Promise<void> | null = null;
43
+
44
+ // Account registry for routing
45
+ let accountRegistry: AccountRegistry | null = null;
46
+
47
+ /**
48
+ * Get or create account registry
49
+ */
50
+ function getAccountRegistry(): AccountRegistry {
51
+ if (!accountRegistry) {
52
+ accountRegistry = createAccountRegistry();
53
+ }
54
+ return accountRegistry;
55
+ }
56
+
57
+ /**
58
+ * Initialize account registry from config
59
+ */
60
+ function initializeRegistry(cfg: OpenClawConfig): void {
61
+ const registry = getAccountRegistry();
62
+ registry.clear();
63
+
64
+ const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
65
+ const accounts = (section?.accounts || []) as WeChatAccount[];
66
+
67
+ // Register each account
68
+ for (const account of accounts) {
69
+ if (account.enabled !== false) {
70
+ const registeredAccount: RegisteredAccount = {
71
+ ...account,
72
+ channelType: "celphone-wechat",
73
+ registeredAt: Date.now(),
74
+ };
75
+ registry.register(registeredAccount);
76
+ }
77
+ }
78
+ }
39
79
 
40
80
  /**
41
81
  * Get or create cache manager
@@ -57,12 +97,26 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
57
97
  });
58
98
  }
59
99
 
100
+ // Initialize registry with accounts
101
+ initializeRegistry(cfg);
102
+
60
103
  const basePath = (globalThis as any)?.process?.env?.OPENCLAW_CACHE_PATH
61
104
  || "~/.openclaw/channels/celphone-wechat";
62
105
 
106
+ // Build knowledge config if provided in section
107
+ let knowledgeConfig: (Partial<KnowledgeConfig> & { nodeId: string; agentId: string }) | undefined;
108
+ if (section?.knowledge) {
109
+ knowledgeConfig = {
110
+ ...section.knowledge,
111
+ nodeId: section.knowledge.nodeId || 'celphone-wechat-node',
112
+ agentId: section.knowledge.agentId || section.agentId || 'default-agent',
113
+ };
114
+ }
115
+
63
116
  cacheManager = createCacheManager({
64
117
  basePath,
65
118
  accounts,
119
+ knowledgeConfig,
66
120
  saasConfig: section?.saas ? {
67
121
  apiBaseUrl: section.saas.apiBaseUrl,
68
122
  apiKey: section.saas.apiKey,
@@ -75,36 +129,88 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
75
129
  } : undefined,
76
130
  });
77
131
 
78
- // Initialize cache manager
79
- cacheManager.init().catch(err => {
132
+ // Initialize cache manager - await for ready state
133
+ cacheManagerReady = cacheManager.init().catch(err => {
80
134
  console.error("[CelPhoneWeChat] Cache manager init failed:", err);
135
+ throw err; // Re-throw so caller knows init failed
81
136
  });
82
137
 
83
138
  return cacheManager;
84
139
  }
85
140
 
141
+ /**
142
+ * Get cache manager ready promise - await before processing messages
143
+ */
144
+ export function getCacheManagerReady(): Promise<void> | null {
145
+ return cacheManagerReady;
146
+ }
147
+
86
148
  export interface CelPhoneWeChatResolvedAccount {
87
149
  accountId: string | null;
150
+ agentId: string | null;
88
151
  apiKey: string;
89
152
  baseUrl: string;
90
- wechatAccountId: string; // Required - which WeChat account this is
91
- wechatId: string; // The actual WeChat ID (wxid_xxx)
92
- nickName: string; // WeChat nickname for display
153
+ wechatAccountId: string;
154
+ wechatId: string;
155
+ nickName: string;
93
156
  allowFrom: string[];
94
157
  dmPolicy: string | undefined;
95
158
  }
96
159
 
160
+ /**
161
+ * List all account IDs from config
162
+ */
163
+ function listAccountIds(cfg: OpenClawConfig): string[] {
164
+ const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
165
+ const accounts = (section?.accounts || []) as WeChatAccount[];
166
+
167
+ if (accounts.length > 0) {
168
+ return accounts.map(a => a.accountId).filter(Boolean);
169
+ }
170
+
171
+ // Backward compatibility: single account
172
+ if (section?.wechatAccountId) {
173
+ return [section.accountId || "default"];
174
+ }
175
+
176
+ return [];
177
+ }
178
+
97
179
  /**
98
180
  * Resolve account from OpenClaw config
99
181
  *
100
- * Note: This is a "human account" model - the WeChat account represents
101
- * a real person with all their friends and groups, not a bot.
182
+ * Supports both multi-account (accounts array) and legacy (single) formats.
102
183
  */
103
184
  function resolveAccount(
104
185
  cfg: OpenClawConfig,
105
186
  accountId?: string | null
106
187
  ): CelPhoneWeChatResolvedAccount {
107
188
  const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
189
+ const accounts = (section?.accounts || []) as WeChatAccount[];
190
+
191
+ // Multi-account mode: look up from accounts array
192
+ if (accounts.length > 0) {
193
+ const targetAccountId = accountId || accounts[0]?.accountId;
194
+ const account = accounts.find(a => a.accountId === targetAccountId);
195
+
196
+ if (!account) {
197
+ throw new Error(`celphone-wechat: account not found - ${targetAccountId}`);
198
+ }
199
+
200
+ return {
201
+ accountId: account.accountId,
202
+ agentId: account.agentId || null,
203
+ apiKey: account.apiKey || section?.apiKey || "",
204
+ baseUrl: account.baseUrl || section?.baseUrl || "https://api.workphone.example.com",
205
+ wechatAccountId: account.wechatAccountId,
206
+ wechatId: account.wechatId || account.wechatAccountId,
207
+ nickName: account.nickName || "WeChat User",
208
+ allowFrom: account.allowFrom ?? [],
209
+ dmPolicy: account.dmPolicy,
210
+ };
211
+ }
212
+
213
+ // Legacy single-account mode
108
214
  const apiKey = section?.apiKey;
109
215
  const baseUrl = section?.baseUrl || "https://api.workphone.example.com";
110
216
  const wechatAccountId = section?.wechatAccountId;
@@ -119,11 +225,12 @@ function resolveAccount(
119
225
 
120
226
  return {
121
227
  accountId: accountId ?? null,
228
+ agentId: null,
122
229
  apiKey,
123
230
  baseUrl,
124
231
  wechatAccountId,
125
- wechatId: section?.wechatId || wechatAccountId, // Actual WeChat ID (wxid_xxx)
126
- nickName: section?.nickName || "WeChat User", // Display name
232
+ wechatId: section?.wechatId || wechatAccountId,
233
+ nickName: section?.nickName || "WeChat User",
127
234
  allowFrom: section?.allowFrom ?? [],
128
235
  dmPolicy: section?.dmSecurity,
129
236
  };
@@ -162,29 +269,33 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
162
269
  channel: "celphone-wechat",
163
270
  sendText: async (params: any) => {
164
271
  const cfg = params.cfg;
165
- const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
272
+ const accountId = params.accountId;
166
273
 
167
- const client = createWorkPhoneClient({
168
- baseUrl: section?.baseUrl || "https://api.workphone.example.com",
169
- apiKey: section?.apiKey,
170
- accountId: section?.accountId || undefined,
171
- wechatAccountId: section?.wechatAccountId,
172
- });
274
+ // Resolve account to get credentials
275
+ const account = resolveAccount(cfg, accountId);
276
+
277
+ // Use client pool for connection reuse
278
+ const clientConfig = createClientConfig(
279
+ account.baseUrl,
280
+ account.apiKey,
281
+ account.accountId || undefined,
282
+ account.wechatAccountId
283
+ );
284
+ const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
173
285
 
174
286
  const isChatroom = params.to?.includes("@chatroom");
175
287
 
176
288
  let result;
177
289
  if (isChatroom) {
178
290
  result = await client.sendChatroomMessage({
179
- wechatAccountId: section?.wechatAccountId,
291
+ wechatAccountId: account.wechatAccountId,
180
292
  chatroomId: params.to,
181
293
  content: params.text,
182
294
  type: "text",
183
295
  });
184
296
  } else {
185
- // Send to friend (DM)
186
297
  result = await client.sendFriendMessage({
187
- wechatAccountId: section?.wechatAccountId,
298
+ wechatAccountId: account.wechatAccountId,
188
299
  friendWechatId: params.to,
189
300
  content: params.text,
190
301
  type: "text",
@@ -194,50 +305,37 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
194
305
  return { messageId: result.messageId };
195
306
  },
196
307
 
197
- // Send media - also support both DM and group
198
308
  sendMedia: async (params: any) => {
199
309
  const cfg = params.cfg;
200
- const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
310
+ const accountId = params.accountId;
311
+
312
+ // Resolve account to get credentials
313
+ const account = resolveAccount(cfg, accountId);
201
314
 
202
- const client = createWorkPhoneClient({
203
- baseUrl: section?.baseUrl || "https://api.workphone.example.com",
204
- apiKey: section?.apiKey,
205
- accountId: section?.accountId || undefined,
206
- wechatAccountId: section?.wechatAccountId,
207
- });
315
+ // Use client pool
316
+ const clientConfig = createClientConfig(
317
+ account.baseUrl,
318
+ account.apiKey,
319
+ account.accountId || undefined,
320
+ account.wechatAccountId
321
+ );
322
+ const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
208
323
 
209
324
  const isChatroom = params.to?.includes("@chatroom");
210
325
 
211
- // Get file path from mediaUrl or mediaReadFile
212
- let filePath = "";
213
- if (params.mediaUrl) {
214
- filePath = params.mediaUrl;
215
- } else if (params.mediaReadFile) {
216
- // Read file from local roots
217
- const roots = params.mediaLocalRoots || ["./"];
218
- for (const root of roots) {
219
- try {
220
- // Need to find the file path from the message
221
- // In practice, we'd need to look at the attachment info
222
- filePath = root;
223
- break;
224
- } catch {
225
- // Try next root
226
- }
227
- }
228
- }
326
+ let filePath = params.mediaUrl || "";
229
327
 
230
328
  let result;
231
329
  if (isChatroom) {
232
330
  result = await client.sendChatroomMessage({
233
- wechatAccountId: section?.wechatAccountId,
331
+ wechatAccountId: account.wechatAccountId,
234
332
  chatroomId: params.to,
235
333
  content: filePath,
236
334
  type: "file",
237
335
  });
238
336
  } else {
239
337
  result = await client.sendFriendMessage({
240
- wechatAccountId: section?.wechatAccountId,
338
+ wechatAccountId: account.wechatAccountId,
241
339
  friendWechatId: params.to,
242
340
  content: filePath,
243
341
  type: "file",
@@ -249,8 +347,6 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
249
347
  },
250
348
  } as any,
251
349
 
252
- // Additional capabilities
253
- // - Describe the channel's message types and features
254
350
  capabilities: {
255
351
  supportedMessageTypes: [
256
352
  "text",
@@ -261,13 +357,13 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
261
357
  "location",
262
358
  "contact",
263
359
  ],
264
- maxAttachmentSize: 25 * 1024 * 1024, // 25MB
265
- supportsMarkdown: false, // WeChat uses a limited markup
360
+ maxAttachmentSize: 25 * 1024 * 1024,
361
+ supportsMarkdown: false,
266
362
  supportsHtml: false,
267
363
  supportsEmoji: true,
268
- supportsReactions: false, // WeChat doesn't support reactions
269
- supportsThreads: true, // Can reply in chat
270
- supportsEditing: false, // Cannot edit sent messages
364
+ supportsReactions: false,
365
+ supportsThreads: true,
366
+ supportsEditing: false,
271
367
  supportsDeleting: false,
272
368
  },
273
369
  } as any);
@@ -276,6 +372,10 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
276
372
  * Helper function to handle inbound webhook messages
277
373
  * This should be called from your HTTP route handler
278
374
  *
375
+ * Supports multi-account routing via AccountRegistry:
376
+ * - Looks up agentId from wechatAccountId
377
+ * - Dispatches to correct agent
378
+ *
279
379
  * Integrates with Cache Manager for:
280
380
  * - Local MD file caching
281
381
  * - User profile storage
@@ -297,6 +397,23 @@ export async function handleInboundMessage(
297
397
  ? (message as any).chatroomId
298
398
  : message.fromUser || message.toUser || "";
299
399
 
400
+ // Determine the account ID for this message
401
+ // Priority: payload.accountId > wechatAccountId lookup > "default"
402
+ const resolvedAccountId = accountId || wechatAccountId || "default";
403
+
404
+ // Look up agentId for routing
405
+ let agentId: string | null = null;
406
+ if (cfg) {
407
+ const registry = getAccountRegistry();
408
+ // Try by wechatAccountId first
409
+ agentId = registry.getAgentId(wechatAccountId || "");
410
+ // Fallback to accountId
411
+ if (!agentId && accountId) {
412
+ const regAccount = registry.getByAccountId(accountId);
413
+ agentId = regAccount?.agentId || null;
414
+ }
415
+ }
416
+
300
417
  // Convert to cache format and store locally
301
418
  if (cfg) {
302
419
  try {
@@ -304,7 +421,7 @@ export async function handleInboundMessage(
304
421
  const wechatMessage: WeChatMessage = {
305
422
  messageId: message.messageId,
306
423
  msgSvrId: message.msgSvrId,
307
- accountId: accountId || "default",
424
+ accountId: resolvedAccountId,
308
425
  conversationType: isChatroom ? "chatroom" : "friend",
309
426
  conversationId,
310
427
  senderId: message.fromUser || "",
@@ -339,6 +456,8 @@ export async function handleInboundMessage(
339
456
  },
340
457
  timestamp: new Date(message.timestamp || Date.now()),
341
458
  isSelf: message.isSelf || false,
459
+ agentId, // Include agentId for routing
460
+ accountId: resolvedAccountId,
342
461
  };
343
462
 
344
463
  // Dispatch to OpenClaw
@@ -350,7 +469,7 @@ export async function handleInboundMessage(
350
469
  platformId: friendRequest.fromUser,
351
470
  scene: friendRequest.scene,
352
471
  ticket: friendRequest.ticket,
353
- accountId,
472
+ accountId: accountId || wechatAccountId,
354
473
  wechatAccountId,
355
474
  });
356
475
  }
@@ -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
+ }
@@ -11,7 +11,7 @@ import {
11
11
  createCacheManager,
12
12
  WeChatAccount,
13
13
  WeChatMessage,
14
- } from './src/cache/index.js';
14
+ } from '@gloablehive/wechat-cache';
15
15
  import { handleInboundMessage } from './src/channel.js';
16
16
 
17
17
  const TEST_CACHE_PATH = '/tmp/wechat-integration-test';