@gloablehive/celphone-wechat-plugin 1.0.1 → 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;
@@ -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
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
+ // 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
  */
@@ -42,6 +70,8 @@ function getCacheManager(cfg) {
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
76
  || "~/.openclaw/channels/celphone-wechat";
47
77
  cacheManager = createCacheManager({
@@ -58,20 +88,55 @@ function getCacheManager(cfg) {
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
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
  };
@@ -122,27 +188,25 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
122
188
  channel: "celphone-wechat",
123
189
  sendText: async (params) => {
124
190
  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
- });
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);
132
197
  const isChatroom = params.to?.includes("@chatroom");
133
198
  let result;
134
199
  if (isChatroom) {
135
200
  result = await client.sendChatroomMessage({
136
- wechatAccountId: section?.wechatAccountId,
201
+ wechatAccountId: account.wechatAccountId,
137
202
  chatroomId: params.to,
138
203
  content: params.text,
139
204
  type: "text",
140
205
  });
141
206
  }
142
207
  else {
143
- // Send to friend (DM)
144
208
  result = await client.sendFriendMessage({
145
- wechatAccountId: section?.wechatAccountId,
209
+ wechatAccountId: account.wechatAccountId,
146
210
  friendWechatId: params.to,
147
211
  content: params.text,
148
212
  type: "text",
@@ -150,41 +214,20 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
150
214
  }
151
215
  return { messageId: result.messageId };
152
216
  },
153
- // Send media - also support both DM and group
154
217
  sendMedia: async (params) => {
155
218
  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
- });
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);
163
225
  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
- }
226
+ let filePath = params.mediaUrl || "";
184
227
  let result;
185
228
  if (isChatroom) {
186
229
  result = await client.sendChatroomMessage({
187
- wechatAccountId: section?.wechatAccountId,
230
+ wechatAccountId: account.wechatAccountId,
188
231
  chatroomId: params.to,
189
232
  content: filePath,
190
233
  type: "file",
@@ -192,7 +235,7 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
192
235
  }
193
236
  else {
194
237
  result = await client.sendFriendMessage({
195
- wechatAccountId: section?.wechatAccountId,
238
+ wechatAccountId: account.wechatAccountId,
196
239
  friendWechatId: params.to,
197
240
  content: filePath,
198
241
  type: "file",
@@ -202,8 +245,6 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
202
245
  },
203
246
  },
204
247
  },
205
- // Additional capabilities
206
- // - Describe the channel's message types and features
207
248
  capabilities: {
208
249
  supportedMessageTypes: [
209
250
  "text",
@@ -214,13 +255,13 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
214
255
  "location",
215
256
  "contact",
216
257
  ],
217
- maxAttachmentSize: 25 * 1024 * 1024, // 25MB
218
- supportsMarkdown: false, // WeChat uses a limited markup
258
+ maxAttachmentSize: 25 * 1024 * 1024,
259
+ supportsMarkdown: false,
219
260
  supportsHtml: false,
220
261
  supportsEmoji: true,
221
- supportsReactions: false, // WeChat doesn't support reactions
222
- supportsThreads: true, // Can reply in chat
223
- supportsEditing: false, // Cannot edit sent messages
262
+ supportsReactions: false,
263
+ supportsThreads: true,
264
+ supportsEditing: false,
224
265
  supportsDeleting: false,
225
266
  },
226
267
  });
@@ -228,6 +269,10 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
228
269
  * Helper function to handle inbound webhook messages
229
270
  * This should be called from your HTTP route handler
230
271
  *
272
+ * Supports multi-account routing via AccountRegistry:
273
+ * - Looks up agentId from wechatAccountId
274
+ * - Dispatches to correct agent
275
+ *
231
276
  * Integrates with Cache Manager for:
232
277
  * - Local MD file caching
233
278
  * - User profile storage
@@ -243,6 +288,21 @@ export async function handleInboundMessage(api, payload, cfg) {
243
288
  const conversationId = isChatroom
244
289
  ? message.chatroomId
245
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
+ }
246
306
  // Convert to cache format and store locally
247
307
  if (cfg) {
248
308
  try {
@@ -250,7 +310,7 @@ export async function handleInboundMessage(api, payload, cfg) {
250
310
  const wechatMessage = {
251
311
  messageId: message.messageId,
252
312
  msgSvrId: message.msgSvrId,
253
- accountId: accountId || "default",
313
+ accountId: resolvedAccountId,
254
314
  conversationType: isChatroom ? "chatroom" : "friend",
255
315
  conversationId,
256
316
  senderId: message.fromUser || "",
@@ -285,6 +345,8 @@ export async function handleInboundMessage(api, payload, cfg) {
285
345
  },
286
346
  timestamp: new Date(message.timestamp || Date.now()),
287
347
  isSelf: message.isSelf || false,
348
+ agentId, // Include agentId for routing
349
+ accountId: resolvedAccountId,
288
350
  };
289
351
  // Dispatch to OpenClaw
290
352
  await api.inbound.dispatchMessage(openclawMessage);
@@ -296,7 +358,7 @@ export async function handleInboundMessage(api, payload, cfg) {
296
358
  platformId: friendRequest.fromUser,
297
359
  scene: friendRequest.scene,
298
360
  ticket: friendRequest.ticket,
299
- accountId,
361
+ accountId: accountId || wechatAccountId,
300
362
  wechatAccountId,
301
363
  });
302
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,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.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.1",
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,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,11 +28,53 @@ import {
32
28
  CacheManager,
33
29
  WeChatAccount,
34
30
  WeChatMessage,
31
+ AccountRegistry,
32
+ createAccountRegistry,
33
+ type RegisteredAccount,
35
34
  } from "@gloablehive/wechat-cache";
36
35
 
36
+ // Import client pool
37
+ import { getWorkPhoneClient, createClientConfig, getClientPool } from "./client-pool.js";
38
+
37
39
  // Cache manager instance (lazy initialized)
38
40
  let cacheManager: CacheManager | null = null;
39
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
+
40
78
  /**
41
79
  * Get or create cache manager
42
80
  */
@@ -57,6 +95,9 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
57
95
  });
58
96
  }
59
97
 
98
+ // Initialize registry with accounts
99
+ initializeRegistry(cfg);
100
+
60
101
  const basePath = (globalThis as any)?.process?.env?.OPENCLAW_CACHE_PATH
61
102
  || "~/.openclaw/channels/celphone-wechat";
62
103
 
@@ -75,7 +116,7 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
75
116
  } : undefined,
76
117
  });
77
118
 
78
- // Initialize cache manager
119
+ // Initialize cache manager - must await for ready state
79
120
  cacheManager.init().catch(err => {
80
121
  console.error("[CelPhoneWeChat] Cache manager init failed:", err);
81
122
  });
@@ -85,26 +126,70 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
85
126
 
86
127
  export interface CelPhoneWeChatResolvedAccount {
87
128
  accountId: string | null;
129
+ agentId: string | null;
88
130
  apiKey: string;
89
131
  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
132
+ wechatAccountId: string;
133
+ wechatId: string;
134
+ nickName: string;
93
135
  allowFrom: string[];
94
136
  dmPolicy: string | undefined;
95
137
  }
96
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
+
97
158
  /**
98
159
  * Resolve account from OpenClaw config
99
160
  *
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.
161
+ * Supports both multi-account (accounts array) and legacy (single) formats.
102
162
  */
103
163
  function resolveAccount(
104
164
  cfg: OpenClawConfig,
105
165
  accountId?: string | null
106
166
  ): CelPhoneWeChatResolvedAccount {
107
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
108
193
  const apiKey = section?.apiKey;
109
194
  const baseUrl = section?.baseUrl || "https://api.workphone.example.com";
110
195
  const wechatAccountId = section?.wechatAccountId;
@@ -119,11 +204,12 @@ function resolveAccount(
119
204
 
120
205
  return {
121
206
  accountId: accountId ?? null,
207
+ agentId: null,
122
208
  apiKey,
123
209
  baseUrl,
124
210
  wechatAccountId,
125
- wechatId: section?.wechatId || wechatAccountId, // Actual WeChat ID (wxid_xxx)
126
- nickName: section?.nickName || "WeChat User", // Display name
211
+ wechatId: section?.wechatId || wechatAccountId,
212
+ nickName: section?.nickName || "WeChat User",
127
213
  allowFrom: section?.allowFrom ?? [],
128
214
  dmPolicy: section?.dmSecurity,
129
215
  };
@@ -162,29 +248,33 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
162
248
  channel: "celphone-wechat",
163
249
  sendText: async (params: any) => {
164
250
  const cfg = params.cfg;
165
- const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
251
+ const accountId = params.accountId;
252
+
253
+ // Resolve account to get credentials
254
+ const account = resolveAccount(cfg, accountId);
166
255
 
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
- });
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);
173
264
 
174
265
  const isChatroom = params.to?.includes("@chatroom");
175
266
 
176
267
  let result;
177
268
  if (isChatroom) {
178
269
  result = await client.sendChatroomMessage({
179
- wechatAccountId: section?.wechatAccountId,
270
+ wechatAccountId: account.wechatAccountId,
180
271
  chatroomId: params.to,
181
272
  content: params.text,
182
273
  type: "text",
183
274
  });
184
275
  } else {
185
- // Send to friend (DM)
186
276
  result = await client.sendFriendMessage({
187
- wechatAccountId: section?.wechatAccountId,
277
+ wechatAccountId: account.wechatAccountId,
188
278
  friendWechatId: params.to,
189
279
  content: params.text,
190
280
  type: "text",
@@ -194,50 +284,37 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
194
284
  return { messageId: result.messageId };
195
285
  },
196
286
 
197
- // Send media - also support both DM and group
198
287
  sendMedia: async (params: any) => {
199
288
  const cfg = params.cfg;
200
- const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
289
+ const accountId = params.accountId;
201
290
 
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
- });
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);
208
302
 
209
303
  const isChatroom = params.to?.includes("@chatroom");
210
304
 
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
- }
305
+ let filePath = params.mediaUrl || "";
229
306
 
230
307
  let result;
231
308
  if (isChatroom) {
232
309
  result = await client.sendChatroomMessage({
233
- wechatAccountId: section?.wechatAccountId,
310
+ wechatAccountId: account.wechatAccountId,
234
311
  chatroomId: params.to,
235
312
  content: filePath,
236
313
  type: "file",
237
314
  });
238
315
  } else {
239
316
  result = await client.sendFriendMessage({
240
- wechatAccountId: section?.wechatAccountId,
317
+ wechatAccountId: account.wechatAccountId,
241
318
  friendWechatId: params.to,
242
319
  content: filePath,
243
320
  type: "file",
@@ -249,8 +326,6 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
249
326
  },
250
327
  } as any,
251
328
 
252
- // Additional capabilities
253
- // - Describe the channel's message types and features
254
329
  capabilities: {
255
330
  supportedMessageTypes: [
256
331
  "text",
@@ -261,13 +336,13 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
261
336
  "location",
262
337
  "contact",
263
338
  ],
264
- maxAttachmentSize: 25 * 1024 * 1024, // 25MB
265
- supportsMarkdown: false, // WeChat uses a limited markup
339
+ maxAttachmentSize: 25 * 1024 * 1024,
340
+ supportsMarkdown: false,
266
341
  supportsHtml: false,
267
342
  supportsEmoji: true,
268
- supportsReactions: false, // WeChat doesn't support reactions
269
- supportsThreads: true, // Can reply in chat
270
- supportsEditing: false, // Cannot edit sent messages
343
+ supportsReactions: false,
344
+ supportsThreads: true,
345
+ supportsEditing: false,
271
346
  supportsDeleting: false,
272
347
  },
273
348
  } as any);
@@ -276,6 +351,10 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
276
351
  * Helper function to handle inbound webhook messages
277
352
  * This should be called from your HTTP route handler
278
353
  *
354
+ * Supports multi-account routing via AccountRegistry:
355
+ * - Looks up agentId from wechatAccountId
356
+ * - Dispatches to correct agent
357
+ *
279
358
  * Integrates with Cache Manager for:
280
359
  * - Local MD file caching
281
360
  * - User profile storage
@@ -297,6 +376,23 @@ export async function handleInboundMessage(
297
376
  ? (message as any).chatroomId
298
377
  : message.fromUser || message.toUser || "";
299
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
+ }
395
+
300
396
  // Convert to cache format and store locally
301
397
  if (cfg) {
302
398
  try {
@@ -304,7 +400,7 @@ export async function handleInboundMessage(
304
400
  const wechatMessage: WeChatMessage = {
305
401
  messageId: message.messageId,
306
402
  msgSvrId: message.msgSvrId,
307
- accountId: accountId || "default",
403
+ accountId: resolvedAccountId,
308
404
  conversationType: isChatroom ? "chatroom" : "friend",
309
405
  conversationId,
310
406
  senderId: message.fromUser || "",
@@ -339,6 +435,8 @@ export async function handleInboundMessage(
339
435
  },
340
436
  timestamp: new Date(message.timestamp || Date.now()),
341
437
  isSelf: message.isSelf || false,
438
+ agentId, // Include agentId for routing
439
+ accountId: resolvedAccountId,
342
440
  };
343
441
 
344
442
  // Dispatch to OpenClaw
@@ -350,7 +448,7 @@ export async function handleInboundMessage(
350
448
  platformId: friendRequest.fromUser,
351
449
  scene: friendRequest.scene,
352
450
  ticket: friendRequest.ticket,
353
- accountId,
451
+ accountId: accountId || wechatAccountId,
354
452
  wechatAccountId,
355
453
  });
356
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
+ }