@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 +9 -2
- package/dist/src/channel.js +145 -65
- package/dist/src/client-pool.js +82 -0
- package/index.ts +10 -2
- package/package.json +3 -3
- package/src/channel.ts +184 -65
- package/src/client-pool.ts +103 -0
- package/test-integration.ts +1 -1
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;
|
package/dist/src/channel.js
CHANGED
|
@@ -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
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
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
|
-
*
|
|
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,
|
|
90
|
-
nickName: section?.nickName || "WeChat User",
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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:
|
|
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:
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
218
|
-
supportsMarkdown: false,
|
|
276
|
+
maxAttachmentSize: 25 * 1024 * 1024,
|
|
277
|
+
supportsMarkdown: false,
|
|
219
278
|
supportsHtml: false,
|
|
220
279
|
supportsEmoji: true,
|
|
221
|
-
supportsReactions: false,
|
|
222
|
-
supportsThreads: true,
|
|
223
|
-
supportsEditing: false,
|
|
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:
|
|
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.
|
|
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": "
|
|
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
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
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 {
|
|
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;
|
|
91
|
-
wechatId: string;
|
|
92
|
-
nickName: string;
|
|
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
|
-
*
|
|
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,
|
|
126
|
-
nickName: section?.nickName || "WeChat User",
|
|
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
|
|
272
|
+
const accountId = params.accountId;
|
|
166
273
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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:
|
|
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:
|
|
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
|
|
310
|
+
const accountId = params.accountId;
|
|
311
|
+
|
|
312
|
+
// Resolve account to get credentials
|
|
313
|
+
const account = resolveAccount(cfg, accountId);
|
|
201
314
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
265
|
-
supportsMarkdown: false,
|
|
360
|
+
maxAttachmentSize: 25 * 1024 * 1024,
|
|
361
|
+
supportsMarkdown: false,
|
|
266
362
|
supportsHtml: false,
|
|
267
363
|
supportsEmoji: true,
|
|
268
|
-
supportsReactions: false,
|
|
269
|
-
supportsThreads: true,
|
|
270
|
-
supportsEditing: false,
|
|
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:
|
|
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
|
+
}
|
package/test-integration.ts
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
createCacheManager,
|
|
12
12
|
WeChatAccount,
|
|
13
13
|
WeChatMessage,
|
|
14
|
-
} from '
|
|
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';
|