@gloablehive/ipad-wechat-plugin 1.0.10 → 1.0.11
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 +47 -4
- package/dist/src/channel.js +160 -84
- package/dist/src/client-pool.js +83 -0
- package/index.ts +53 -4
- package/package.json +2 -2
- package/src/channel.ts +186 -81
- package/src/client-pool.ts +98 -0
package/dist/index.js
CHANGED
|
@@ -3,26 +3,69 @@
|
|
|
3
3
|
*
|
|
4
4
|
* iPad WeChat Plugin - enables OpenClaw to send/receive WeChat messages
|
|
5
5
|
* through the iPad protocol (different from WorkPhone/phone protocol)
|
|
6
|
+
*
|
|
7
|
+
* Multi-account support:
|
|
8
|
+
* - cfg is passed to handleInboundMessage for cache isolation
|
|
9
|
+
* - Each account's guid can have a dedicated webhook path
|
|
6
10
|
*/
|
|
7
11
|
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
8
|
-
import { ipadWeChatPlugin, handleInboundMessage } from "./src/channel.js";
|
|
12
|
+
import { ipadWeChatPlugin, handleInboundMessage, getCacheManagerReady } from "./src/channel.js";
|
|
9
13
|
export default defineChannelPluginEntry({
|
|
10
14
|
id: "ipad-wechat",
|
|
11
15
|
name: "iPad WeChat",
|
|
12
16
|
description: "Connect OpenClaw to iPad WeChat protocol for sending and receiving WeChat messages",
|
|
13
17
|
plugin: ipadWeChatPlugin,
|
|
14
18
|
registerFull(api) {
|
|
15
|
-
// Register webhook endpoint for receiving messages
|
|
19
|
+
// Register main webhook endpoint for receiving messages
|
|
16
20
|
api.registerHttpRoute({
|
|
17
21
|
path: "/ipad-wechat/webhook",
|
|
18
22
|
auth: "plugin",
|
|
19
23
|
handler: async (req, res) => {
|
|
20
24
|
try {
|
|
25
|
+
// Get config from request
|
|
26
|
+
const cfg = req.cfg;
|
|
27
|
+
// Wait for cache manager to be ready
|
|
28
|
+
const ready = getCacheManagerReady();
|
|
29
|
+
if (ready) {
|
|
30
|
+
await ready;
|
|
31
|
+
}
|
|
21
32
|
const payload = req.body;
|
|
22
|
-
// Handle inbound message
|
|
23
33
|
console.log("[iPad WeChat] Received webhook:", payload);
|
|
24
34
|
if (payload) {
|
|
25
|
-
await handleInboundMessage(api, payload);
|
|
35
|
+
await handleInboundMessage(api, payload, cfg);
|
|
36
|
+
}
|
|
37
|
+
res.statusCode = 200;
|
|
38
|
+
res.end("ok");
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error("[iPad WeChat] Webhook error:", error);
|
|
43
|
+
res.statusCode = 500;
|
|
44
|
+
res.end("Internal error");
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
// Register per-guid webhook endpoints for multi-account routing
|
|
50
|
+
// These paths allow distinguishing which account received the message
|
|
51
|
+
// Path format: /ipad-wechat/webhook/guid-12345
|
|
52
|
+
api.registerHttpRoute({
|
|
53
|
+
path: "/ipad-wechat/webhook/:guid",
|
|
54
|
+
auth: "plugin",
|
|
55
|
+
handler: async (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
const cfg = req.cfg;
|
|
58
|
+
const guid = req.params?.guid;
|
|
59
|
+
// Wait for cache manager to be ready
|
|
60
|
+
const ready = getCacheManagerReady();
|
|
61
|
+
if (ready) {
|
|
62
|
+
await ready;
|
|
63
|
+
}
|
|
64
|
+
const payload = req.body;
|
|
65
|
+
console.log(`[iPad WeChat] Received webhook for guid ${guid}:`, payload);
|
|
66
|
+
if (payload) {
|
|
67
|
+
// Pass the guid as overrideAccountId so handleInboundMessage knows which account
|
|
68
|
+
await handleInboundMessage(api, payload, cfg, guid);
|
|
26
69
|
}
|
|
27
70
|
res.statusCode = 200;
|
|
28
71
|
res.end("ok");
|
package/dist/src/channel.js
CHANGED
|
@@ -4,21 +4,21 @@
|
|
|
4
4
|
* Uses iPad protocol (different from phone/WorkPhone protocol)
|
|
5
5
|
* API documentation: wechat/api.md
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* - 4-layer compression
|
|
12
|
-
* - AI summary extraction
|
|
13
|
-
* - SAAS connectivity + offline fallback
|
|
14
|
-
* - Cloud sync
|
|
7
|
+
* Multi-account support:
|
|
8
|
+
* - Config supports accounts array with per-account credentials
|
|
9
|
+
* - Each account can be bound to a different agent
|
|
10
|
+
* - Security policies are per-account isolated
|
|
15
11
|
*/
|
|
16
12
|
import { createChatChannelPlugin, createChannelPluginBase, } from "openclaw/plugin-sdk/core";
|
|
17
|
-
import { createIPadClient } from "./client.js";
|
|
18
13
|
// Import cache modules from shared package
|
|
19
|
-
import { createCacheManager, } from "@gloablehive/wechat-cache";
|
|
14
|
+
import { createCacheManager, createAccountRegistry, } from "@gloablehive/wechat-cache";
|
|
15
|
+
// Import client pool
|
|
16
|
+
import { getIPadClient } from "./client-pool.js";
|
|
20
17
|
// Cache manager instance (lazy initialized)
|
|
21
18
|
let cacheManager = null;
|
|
19
|
+
let cacheManagerReady = null;
|
|
20
|
+
// Account registry for routing
|
|
21
|
+
let accountRegistry = null;
|
|
22
22
|
/**
|
|
23
23
|
* Get config section from either channels or plugins.entries
|
|
24
24
|
*/
|
|
@@ -26,6 +26,35 @@ function getConfigSection(cfg) {
|
|
|
26
26
|
return cfg?.channels?.["ipad-wechat"]
|
|
27
27
|
|| cfg?.plugins?.entries?.["ipad-wechat"]?.config;
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Get or create account registry
|
|
31
|
+
*/
|
|
32
|
+
function getAccountRegistry() {
|
|
33
|
+
if (!accountRegistry) {
|
|
34
|
+
accountRegistry = createAccountRegistry();
|
|
35
|
+
}
|
|
36
|
+
return accountRegistry;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Initialize account registry from config
|
|
40
|
+
*/
|
|
41
|
+
function initializeRegistry(cfg) {
|
|
42
|
+
const registry = getAccountRegistry();
|
|
43
|
+
registry.clear();
|
|
44
|
+
const section = getConfigSection(cfg);
|
|
45
|
+
const accounts = (section?.accounts || []);
|
|
46
|
+
// Register each account
|
|
47
|
+
for (const account of accounts) {
|
|
48
|
+
if (account.enabled !== false) {
|
|
49
|
+
const registeredAccount = {
|
|
50
|
+
...account,
|
|
51
|
+
channelType: "ipad-wechat",
|
|
52
|
+
registeredAt: Date.now(),
|
|
53
|
+
};
|
|
54
|
+
registry.register(registeredAccount);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
29
58
|
/**
|
|
30
59
|
* Get or create cache manager
|
|
31
60
|
*/
|
|
@@ -35,24 +64,82 @@ function getCacheManager(cfg) {
|
|
|
35
64
|
const section = getConfigSection(cfg);
|
|
36
65
|
const accounts = (section?.accounts || []);
|
|
37
66
|
// If no accounts configured, create default from main config
|
|
38
|
-
if (accounts.length === 0 && section?.
|
|
67
|
+
if (accounts.length === 0 && section?.guid) {
|
|
39
68
|
accounts.push({
|
|
40
69
|
accountId: section.accountId || "default",
|
|
41
|
-
wechatAccountId: section.wechatAccountId,
|
|
70
|
+
wechatAccountId: section.wechatAccountId || "",
|
|
42
71
|
wechatId: section.wechatId || "",
|
|
43
72
|
nickName: section.nickName || "WeChat User",
|
|
44
73
|
enabled: true,
|
|
74
|
+
appKey: section.appKey,
|
|
75
|
+
appSecret: section.appSecret,
|
|
76
|
+
guid: section.guid,
|
|
45
77
|
});
|
|
46
78
|
}
|
|
79
|
+
// Initialize registry with accounts
|
|
80
|
+
initializeRegistry(cfg);
|
|
47
81
|
const basePath = section?.cachePath || "./cache/wechat-ipad";
|
|
48
82
|
cacheManager = createCacheManager({
|
|
49
83
|
basePath,
|
|
50
84
|
accounts,
|
|
51
85
|
});
|
|
86
|
+
// Initialize cache manager - await for ready state
|
|
87
|
+
cacheManagerReady = cacheManager.init().catch(err => {
|
|
88
|
+
console.error("[iPad WeChat] Cache manager init failed:", err);
|
|
89
|
+
throw err;
|
|
90
|
+
});
|
|
52
91
|
return cacheManager;
|
|
53
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Get cache manager ready promise
|
|
95
|
+
*/
|
|
96
|
+
export function getCacheManagerReady() {
|
|
97
|
+
return cacheManagerReady;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* List all account IDs from config
|
|
101
|
+
*/
|
|
102
|
+
function listAccountIds(cfg) {
|
|
103
|
+
const section = getConfigSection(cfg);
|
|
104
|
+
const accounts = (section?.accounts || []);
|
|
105
|
+
if (accounts.length > 0) {
|
|
106
|
+
return accounts.map(a => a.accountId).filter(Boolean);
|
|
107
|
+
}
|
|
108
|
+
// Backward compatibility: single account
|
|
109
|
+
if (section?.guid) {
|
|
110
|
+
return [section.accountId || "default"];
|
|
111
|
+
}
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve account from OpenClaw config
|
|
116
|
+
*
|
|
117
|
+
* Supports both multi-account (accounts array) and legacy (single) formats.
|
|
118
|
+
*/
|
|
54
119
|
function resolveAccount(cfg, accountId) {
|
|
55
120
|
const section = getConfigSection(cfg);
|
|
121
|
+
const accounts = (section?.accounts || []);
|
|
122
|
+
// Multi-account mode: look up from accounts array
|
|
123
|
+
if (accounts.length > 0) {
|
|
124
|
+
const targetAccountId = accountId || accounts[0]?.accountId;
|
|
125
|
+
const account = accounts.find(a => a.accountId === targetAccountId);
|
|
126
|
+
if (!account) {
|
|
127
|
+
throw new Error(`ipad-wechat: account not found - ${targetAccountId}`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
accountId: account.accountId,
|
|
131
|
+
agentId: account.agentId || null,
|
|
132
|
+
appKey: account.appKey || section?.appKey || "",
|
|
133
|
+
appSecret: account.appSecret || section?.appSecret || "",
|
|
134
|
+
guid: account.guid || section?.guid || "",
|
|
135
|
+
wechatAccountId: account.wechatAccountId || "",
|
|
136
|
+
wechatId: account.wechatId || "",
|
|
137
|
+
nickName: account.nickName || "WeChat User",
|
|
138
|
+
allowFrom: account.allowFrom ?? [],
|
|
139
|
+
dmPolicy: account.dmPolicy,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Legacy single-account mode
|
|
56
143
|
const appKey = section?.appKey;
|
|
57
144
|
const appSecret = section?.appSecret;
|
|
58
145
|
const guid = section?.guid;
|
|
@@ -61,6 +148,7 @@ function resolveAccount(cfg, accountId) {
|
|
|
61
148
|
}
|
|
62
149
|
return {
|
|
63
150
|
accountId: accountId ?? null,
|
|
151
|
+
agentId: null,
|
|
64
152
|
appKey,
|
|
65
153
|
appSecret,
|
|
66
154
|
guid,
|
|
@@ -85,25 +173,17 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
85
173
|
supportsEditing: false,
|
|
86
174
|
supportsDeleting: false,
|
|
87
175
|
},
|
|
88
|
-
// Config adapter - for runtime account management
|
|
89
176
|
config: {
|
|
90
|
-
listAccountIds: (cfg) =>
|
|
91
|
-
|
|
92
|
-
if (section?.wechatAccountId) {
|
|
93
|
-
return [section.wechatAccountId];
|
|
94
|
-
}
|
|
95
|
-
return ["default"];
|
|
96
|
-
},
|
|
97
|
-
resolveAccount: (cfg, accountId) => {
|
|
98
|
-
return resolveAccount(cfg, accountId);
|
|
99
|
-
},
|
|
177
|
+
listAccountIds: (cfg) => listAccountIds(cfg),
|
|
178
|
+
resolveAccount: (cfg, accountId) => resolveAccount(cfg, accountId),
|
|
100
179
|
defaultAccountId: (cfg) => {
|
|
101
180
|
const section = getConfigSection(cfg);
|
|
102
|
-
|
|
181
|
+
const accounts = (section?.accounts || []);
|
|
182
|
+
return accounts[0]?.accountId || section?.accountId || "default";
|
|
103
183
|
},
|
|
104
184
|
inspectAccount: (cfg, accountId) => {
|
|
105
|
-
const
|
|
106
|
-
const hasCredentials = Boolean(
|
|
185
|
+
const account = resolveAccount(cfg, accountId);
|
|
186
|
+
const hasCredentials = Boolean(account.appKey && account.appSecret && account.guid);
|
|
107
187
|
return {
|
|
108
188
|
enabled: hasCredentials,
|
|
109
189
|
configured: hasCredentials,
|
|
@@ -111,15 +191,11 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
111
191
|
};
|
|
112
192
|
},
|
|
113
193
|
},
|
|
114
|
-
// Setup adapter - for initial configuration
|
|
115
194
|
setup: {
|
|
116
195
|
resolveAccountId: (params) => {
|
|
117
196
|
return resolveAccount(params.cfg, params.accountId)?.accountId || "";
|
|
118
197
|
},
|
|
119
|
-
applyAccountConfig: (params) =>
|
|
120
|
-
const { cfg, accountId, input } = params;
|
|
121
|
-
return cfg;
|
|
122
|
-
},
|
|
198
|
+
applyAccountConfig: (params) => params.cfg,
|
|
123
199
|
},
|
|
124
200
|
}),
|
|
125
201
|
security: {
|
|
@@ -136,7 +212,6 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
136
212
|
outbound: {
|
|
137
213
|
deliveryMode: "gateway",
|
|
138
214
|
channel: "ipad-wechat",
|
|
139
|
-
// Resolve target - for WeChat, just pass through the ID
|
|
140
215
|
resolveTarget: (params) => {
|
|
141
216
|
const target = params.to?.trim();
|
|
142
217
|
if (!target) {
|
|
@@ -146,15 +221,16 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
146
221
|
},
|
|
147
222
|
attachedResults: {
|
|
148
223
|
sendText: async (params) => {
|
|
149
|
-
// Get config from params.cfg
|
|
150
224
|
const cfg = params.cfg;
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
225
|
+
const accountId = params.accountId;
|
|
226
|
+
// Resolve account to get credentials
|
|
227
|
+
const account = resolveAccount(cfg, accountId);
|
|
228
|
+
// Use client pool for connection reuse
|
|
229
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
230
|
+
appKey: account.appKey,
|
|
231
|
+
appSecret: account.appSecret,
|
|
232
|
+
guid: account.guid,
|
|
156
233
|
});
|
|
157
|
-
// Check if DM or group
|
|
158
234
|
const isChatroom = params.to?.includes("@chatroom");
|
|
159
235
|
if (isChatroom) {
|
|
160
236
|
const result = await client.sendRoomMessage({
|
|
@@ -173,34 +249,17 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
173
249
|
},
|
|
174
250
|
sendMedia: async (params) => {
|
|
175
251
|
const cfg = params.cfg;
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
252
|
+
const accountId = params.accountId;
|
|
253
|
+
// Resolve account to get credentials
|
|
254
|
+
const account = resolveAccount(cfg, accountId);
|
|
255
|
+
// Use client pool
|
|
256
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
257
|
+
appKey: account.appKey,
|
|
258
|
+
appSecret: account.appSecret,
|
|
259
|
+
guid: account.guid,
|
|
181
260
|
});
|
|
182
261
|
const isChatroom = params.to?.includes("@chatroom");
|
|
183
|
-
|
|
184
|
-
let filePath = "";
|
|
185
|
-
if (params.mediaUrl) {
|
|
186
|
-
filePath = params.mediaUrl;
|
|
187
|
-
}
|
|
188
|
-
else if (params.mediaReadFile) {
|
|
189
|
-
// Read file from local roots
|
|
190
|
-
const roots = params.mediaLocalRoots || ["./"];
|
|
191
|
-
for (const root of roots) {
|
|
192
|
-
try {
|
|
193
|
-
const fullPath = `${root}/${filePath}`;
|
|
194
|
-
const buffer = await params.mediaReadFile(fullPath);
|
|
195
|
-
// In a real implementation, we'd upload the file first
|
|
196
|
-
filePath = fullPath;
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
// Try next root
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
262
|
+
let filePath = params.mediaUrl || "";
|
|
204
263
|
if (isChatroom) {
|
|
205
264
|
const result = await client.sendRoomMedia({
|
|
206
265
|
roomId: params.to,
|
|
@@ -218,22 +277,21 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
218
277
|
},
|
|
219
278
|
},
|
|
220
279
|
},
|
|
221
|
-
// Directory adapter - for contact resolution
|
|
222
280
|
directory: {
|
|
223
281
|
self: async (params) => {
|
|
224
|
-
const
|
|
282
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
225
283
|
return {
|
|
226
|
-
id:
|
|
227
|
-
name:
|
|
284
|
+
id: account.wechatId || "",
|
|
285
|
+
name: account.nickName || "WeChat User",
|
|
228
286
|
accountId: params.accountId || "default",
|
|
229
287
|
};
|
|
230
288
|
},
|
|
231
289
|
listPeers: async (params) => {
|
|
232
|
-
const
|
|
233
|
-
const client =
|
|
234
|
-
appKey:
|
|
235
|
-
appSecret:
|
|
236
|
-
guid:
|
|
290
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
291
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
292
|
+
appKey: account.appKey,
|
|
293
|
+
appSecret: account.appSecret,
|
|
294
|
+
guid: account.guid,
|
|
237
295
|
});
|
|
238
296
|
try {
|
|
239
297
|
const contacts = await client.syncContacts();
|
|
@@ -249,17 +307,15 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
249
307
|
}
|
|
250
308
|
},
|
|
251
309
|
listGroups: async (params) => {
|
|
252
|
-
// Note: JuHeBot API doesn't have a direct "list all chatrooms" endpoint
|
|
253
|
-
// For now, return empty array - users can still send to known chatroom IDs
|
|
254
310
|
console.log("[iPad WeChat] listGroups: API not available, returning empty");
|
|
255
311
|
return [];
|
|
256
312
|
},
|
|
257
313
|
listGroupMembers: async (params) => {
|
|
258
|
-
const
|
|
259
|
-
const client =
|
|
260
|
-
appKey:
|
|
261
|
-
appSecret:
|
|
262
|
-
guid:
|
|
314
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
315
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
316
|
+
appKey: account.appKey,
|
|
317
|
+
appSecret: account.appSecret,
|
|
318
|
+
guid: account.guid,
|
|
263
319
|
});
|
|
264
320
|
try {
|
|
265
321
|
const members = await client.getRoomMembers(params.groupId);
|
|
@@ -287,13 +343,32 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
287
343
|
supportsDeleting: false,
|
|
288
344
|
},
|
|
289
345
|
});
|
|
290
|
-
|
|
346
|
+
/**
|
|
347
|
+
* Handle inbound webhook message
|
|
348
|
+
* Supports multi-account routing via AccountRegistry
|
|
349
|
+
*/
|
|
350
|
+
export async function handleInboundMessage(api, payload, cfg, overrideAccountId) {
|
|
291
351
|
const { event, message } = payload;
|
|
292
352
|
if (event === "message" && message) {
|
|
293
353
|
const isChatroom = !!message.roomId;
|
|
294
354
|
const conversationId = isChatroom
|
|
295
355
|
? message.roomId
|
|
296
356
|
: message.fromUser || message.toUser || "";
|
|
357
|
+
// Determine account ID - use override from webhook path or resolve from registry
|
|
358
|
+
let resolvedAccountId = overrideAccountId;
|
|
359
|
+
let agentId = null;
|
|
360
|
+
if (cfg && !resolvedAccountId) {
|
|
361
|
+
const registry = getAccountRegistry();
|
|
362
|
+
// Try by guid if available in message
|
|
363
|
+
const messageGuid = message.guid;
|
|
364
|
+
if (messageGuid) {
|
|
365
|
+
agentId = registry.getAgentIdByGuid(messageGuid);
|
|
366
|
+
const regAccount = registry.getByGuid(messageGuid);
|
|
367
|
+
resolvedAccountId = regAccount?.accountId || null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Default account ID if not resolved
|
|
371
|
+
resolvedAccountId = resolvedAccountId || "default";
|
|
297
372
|
const openclawMessage = {
|
|
298
373
|
id: message.messageId,
|
|
299
374
|
conversation: {
|
|
@@ -309,15 +384,16 @@ export async function handleInboundMessage(api, payload, cfg) {
|
|
|
309
384
|
},
|
|
310
385
|
timestamp: new Date(message.timestamp || Date.now()),
|
|
311
386
|
isSelf: message.isSelf || false,
|
|
387
|
+
agentId,
|
|
388
|
+
accountId: resolvedAccountId,
|
|
312
389
|
};
|
|
313
390
|
// Write to cache
|
|
314
391
|
if (cfg) {
|
|
315
392
|
try {
|
|
316
393
|
const cache = getCacheManager(cfg);
|
|
317
|
-
const section = getConfigSection(cfg);
|
|
318
394
|
const wechatMessage = {
|
|
319
395
|
messageId: message.messageId,
|
|
320
|
-
accountId:
|
|
396
|
+
accountId: resolvedAccountId,
|
|
321
397
|
conversationType: isChatroom ? "chatroom" : "friend",
|
|
322
398
|
conversationId,
|
|
323
399
|
senderId: message.fromUser || "",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Pool - Per-account client management for iPad WeChat
|
|
3
|
+
*
|
|
4
|
+
* Manages iPad (JuHeBot) API clients per account to avoid creating
|
|
5
|
+
* new clients for each message and ensure credential isolation.
|
|
6
|
+
*/
|
|
7
|
+
import { createIPadClient } from "./client.js";
|
|
8
|
+
/**
|
|
9
|
+
* Client pool for iPad WeChat clients
|
|
10
|
+
*/
|
|
11
|
+
class IPadClientPool {
|
|
12
|
+
clients = new Map();
|
|
13
|
+
configs = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* Get or create a client for the given account
|
|
16
|
+
*/
|
|
17
|
+
getClient(accountId, config) {
|
|
18
|
+
// Check if we already have a client and configs match
|
|
19
|
+
const existingConfig = this.configs.get(accountId);
|
|
20
|
+
if (this.clients.has(accountId) && existingConfig) {
|
|
21
|
+
// Verify configs are the same
|
|
22
|
+
if (existingConfig.appKey === config.appKey &&
|
|
23
|
+
existingConfig.appSecret === config.appSecret &&
|
|
24
|
+
existingConfig.guid === config.guid) {
|
|
25
|
+
return this.clients.get(accountId);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Create new client
|
|
29
|
+
const client = createIPadClient(config);
|
|
30
|
+
this.clients.set(accountId, client);
|
|
31
|
+
this.configs.set(accountId, config);
|
|
32
|
+
return client;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if client exists
|
|
36
|
+
*/
|
|
37
|
+
hasClient(accountId) {
|
|
38
|
+
return this.clients.has(accountId);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Remove a client by accountId
|
|
42
|
+
*/
|
|
43
|
+
removeClient(accountId) {
|
|
44
|
+
this.configs.delete(accountId);
|
|
45
|
+
return this.clients.delete(accountId);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Clear all clients
|
|
49
|
+
*/
|
|
50
|
+
clear() {
|
|
51
|
+
this.clients.clear();
|
|
52
|
+
this.configs.clear();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get all account IDs with active clients
|
|
56
|
+
*/
|
|
57
|
+
getActiveAccountIds() {
|
|
58
|
+
return Array.from(this.clients.keys());
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get pool size
|
|
62
|
+
*/
|
|
63
|
+
get size() {
|
|
64
|
+
return this.clients.size;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Singleton instance
|
|
68
|
+
let clientPool = null;
|
|
69
|
+
/**
|
|
70
|
+
* Get or create the client pool singleton
|
|
71
|
+
*/
|
|
72
|
+
function getClientPool() {
|
|
73
|
+
if (!clientPool) {
|
|
74
|
+
clientPool = new IPadClientPool();
|
|
75
|
+
}
|
|
76
|
+
return clientPool;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get a client for a specific account
|
|
80
|
+
*/
|
|
81
|
+
export function getIPadClient(accountId, config) {
|
|
82
|
+
return getClientPool().getClient(accountId, config);
|
|
83
|
+
}
|
package/index.ts
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* iPad WeChat Plugin - enables OpenClaw to send/receive WeChat messages
|
|
5
5
|
* through the iPad protocol (different from WorkPhone/phone protocol)
|
|
6
|
+
*
|
|
7
|
+
* Multi-account support:
|
|
8
|
+
* - cfg is passed to handleInboundMessage for cache isolation
|
|
9
|
+
* - Each account's guid can have a dedicated webhook path
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
9
|
-
import { ipadWeChatPlugin, handleInboundMessage } from "./src/channel.js";
|
|
13
|
+
import { ipadWeChatPlugin, handleInboundMessage, getCacheManagerReady } from "./src/channel.js";
|
|
10
14
|
|
|
11
15
|
export default defineChannelPluginEntry({
|
|
12
16
|
id: "ipad-wechat",
|
|
@@ -15,18 +19,63 @@ export default defineChannelPluginEntry({
|
|
|
15
19
|
plugin: ipadWeChatPlugin,
|
|
16
20
|
|
|
17
21
|
registerFull(api) {
|
|
18
|
-
// Register webhook endpoint for receiving messages
|
|
22
|
+
// Register main webhook endpoint for receiving messages
|
|
19
23
|
api.registerHttpRoute({
|
|
20
24
|
path: "/ipad-wechat/webhook",
|
|
21
25
|
auth: "plugin",
|
|
22
26
|
handler: async (req, res) => {
|
|
23
27
|
try {
|
|
28
|
+
// Get config from request
|
|
29
|
+
const cfg = (req as any).cfg;
|
|
30
|
+
|
|
31
|
+
// Wait for cache manager to be ready
|
|
32
|
+
const ready = getCacheManagerReady();
|
|
33
|
+
if (ready) {
|
|
34
|
+
await ready;
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
const payload = (req as any).body;
|
|
25
|
-
// Handle inbound message
|
|
26
38
|
console.log("[iPad WeChat] Received webhook:", payload);
|
|
27
39
|
|
|
28
40
|
if (payload) {
|
|
29
|
-
await handleInboundMessage(api, payload);
|
|
41
|
+
await handleInboundMessage(api, payload, cfg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
res.statusCode = 200;
|
|
45
|
+
res.end("ok");
|
|
46
|
+
return true;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("[iPad WeChat] Webhook error:", error);
|
|
49
|
+
res.statusCode = 500;
|
|
50
|
+
res.end("Internal error");
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Register per-guid webhook endpoints for multi-account routing
|
|
57
|
+
// These paths allow distinguishing which account received the message
|
|
58
|
+
// Path format: /ipad-wechat/webhook/guid-12345
|
|
59
|
+
api.registerHttpRoute({
|
|
60
|
+
path: "/ipad-wechat/webhook/:guid",
|
|
61
|
+
auth: "plugin",
|
|
62
|
+
handler: async (req, res) => {
|
|
63
|
+
try {
|
|
64
|
+
const cfg = (req as any).cfg;
|
|
65
|
+
const guid = (req as any).params?.guid;
|
|
66
|
+
|
|
67
|
+
// Wait for cache manager to be ready
|
|
68
|
+
const ready = getCacheManagerReady();
|
|
69
|
+
if (ready) {
|
|
70
|
+
await ready;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const payload = (req as any).body;
|
|
74
|
+
console.log(`[iPad WeChat] Received webhook for guid ${guid}:`, payload);
|
|
75
|
+
|
|
76
|
+
if (payload) {
|
|
77
|
+
// Pass the guid as overrideAccountId so handleInboundMessage knows which account
|
|
78
|
+
await handleInboundMessage(api, payload, cfg, guid);
|
|
30
79
|
}
|
|
31
80
|
|
|
32
81
|
res.statusCode = 200;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gloablehive/ipad-wechat-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw channel plugin for iPad WeChat protocol - enables sending/receiving WeChat messages through iPad protocol",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@gloablehive/wechat-cache": "^1.0.
|
|
23
|
+
"@gloablehive/wechat-cache": "^1.0.2",
|
|
24
24
|
"openclaw": ">=1.0.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
package/src/channel.ts
CHANGED
|
@@ -4,14 +4,10 @@
|
|
|
4
4
|
* Uses iPad protocol (different from phone/WorkPhone protocol)
|
|
5
5
|
* API documentation: wechat/api.md
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
* - 4-layer compression
|
|
12
|
-
* - AI summary extraction
|
|
13
|
-
* - SAAS connectivity + offline fallback
|
|
14
|
-
* - Cloud sync
|
|
7
|
+
* Multi-account support:
|
|
8
|
+
* - Config supports accounts array with per-account credentials
|
|
9
|
+
* - Each account can be bound to a different agent
|
|
10
|
+
* - Security policies are per-account isolated
|
|
15
11
|
*/
|
|
16
12
|
|
|
17
13
|
import {
|
|
@@ -19,7 +15,7 @@ import {
|
|
|
19
15
|
createChannelPluginBase,
|
|
20
16
|
} from "openclaw/plugin-sdk/core";
|
|
21
17
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
22
|
-
import {
|
|
18
|
+
import type { WebhookPayload } from "./client.js";
|
|
23
19
|
|
|
24
20
|
// Import cache modules from shared package
|
|
25
21
|
import {
|
|
@@ -27,10 +23,20 @@ import {
|
|
|
27
23
|
CacheManager,
|
|
28
24
|
WeChatAccount,
|
|
29
25
|
WeChatMessage,
|
|
26
|
+
AccountRegistry,
|
|
27
|
+
createAccountRegistry,
|
|
28
|
+
RegisteredAccount,
|
|
30
29
|
} from "@gloablehive/wechat-cache";
|
|
31
30
|
|
|
31
|
+
// Import client pool
|
|
32
|
+
import { getIPadClient } from "./client-pool.js";
|
|
33
|
+
|
|
32
34
|
// Cache manager instance (lazy initialized)
|
|
33
35
|
let cacheManager: CacheManager | null = null;
|
|
36
|
+
let cacheManagerReady: Promise<void> | null = null;
|
|
37
|
+
|
|
38
|
+
// Account registry for routing
|
|
39
|
+
let accountRegistry: AccountRegistry | null = null;
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* Get config section from either channels or plugins.entries
|
|
@@ -40,6 +46,39 @@ function getConfigSection(cfg: any): any {
|
|
|
40
46
|
|| cfg?.plugins?.entries?.["ipad-wechat"]?.config;
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Get or create account registry
|
|
51
|
+
*/
|
|
52
|
+
function getAccountRegistry(): AccountRegistry {
|
|
53
|
+
if (!accountRegistry) {
|
|
54
|
+
accountRegistry = createAccountRegistry();
|
|
55
|
+
}
|
|
56
|
+
return accountRegistry;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initialize account registry from config
|
|
61
|
+
*/
|
|
62
|
+
function initializeRegistry(cfg: OpenClawConfig): void {
|
|
63
|
+
const registry = getAccountRegistry();
|
|
64
|
+
registry.clear();
|
|
65
|
+
|
|
66
|
+
const section = getConfigSection(cfg);
|
|
67
|
+
const accounts = (section?.accounts || []) as WeChatAccount[];
|
|
68
|
+
|
|
69
|
+
// Register each account
|
|
70
|
+
for (const account of accounts) {
|
|
71
|
+
if (account.enabled !== false) {
|
|
72
|
+
const registeredAccount: RegisteredAccount = {
|
|
73
|
+
...account,
|
|
74
|
+
channelType: "ipad-wechat",
|
|
75
|
+
registeredAt: Date.now(),
|
|
76
|
+
};
|
|
77
|
+
registry.register(registeredAccount);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
43
82
|
/**
|
|
44
83
|
* Get or create cache manager
|
|
45
84
|
*/
|
|
@@ -50,16 +89,22 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
|
|
|
50
89
|
const accounts = (section?.accounts || []) as WeChatAccount[];
|
|
51
90
|
|
|
52
91
|
// If no accounts configured, create default from main config
|
|
53
|
-
if (accounts.length === 0 && section?.
|
|
92
|
+
if (accounts.length === 0 && section?.guid) {
|
|
54
93
|
accounts.push({
|
|
55
94
|
accountId: section.accountId || "default",
|
|
56
|
-
wechatAccountId: section.wechatAccountId,
|
|
95
|
+
wechatAccountId: section.wechatAccountId || "",
|
|
57
96
|
wechatId: section.wechatId || "",
|
|
58
97
|
nickName: section.nickName || "WeChat User",
|
|
59
98
|
enabled: true,
|
|
99
|
+
appKey: section.appKey,
|
|
100
|
+
appSecret: section.appSecret,
|
|
101
|
+
guid: section.guid,
|
|
60
102
|
});
|
|
61
103
|
}
|
|
62
104
|
|
|
105
|
+
// Initialize registry with accounts
|
|
106
|
+
initializeRegistry(cfg);
|
|
107
|
+
|
|
63
108
|
const basePath = section?.cachePath || "./cache/wechat-ipad";
|
|
64
109
|
|
|
65
110
|
cacheManager = createCacheManager({
|
|
@@ -67,11 +112,25 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
|
|
|
67
112
|
accounts,
|
|
68
113
|
});
|
|
69
114
|
|
|
115
|
+
// Initialize cache manager - await for ready state
|
|
116
|
+
cacheManagerReady = cacheManager.init().catch(err => {
|
|
117
|
+
console.error("[iPad WeChat] Cache manager init failed:", err);
|
|
118
|
+
throw err;
|
|
119
|
+
});
|
|
120
|
+
|
|
70
121
|
return cacheManager;
|
|
71
122
|
}
|
|
72
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Get cache manager ready promise
|
|
126
|
+
*/
|
|
127
|
+
export function getCacheManagerReady(): Promise<void> | null {
|
|
128
|
+
return cacheManagerReady;
|
|
129
|
+
}
|
|
130
|
+
|
|
73
131
|
export interface IPadWeChatResolvedAccount {
|
|
74
132
|
accountId: string | null;
|
|
133
|
+
agentId: string | null;
|
|
75
134
|
appKey: string;
|
|
76
135
|
appSecret: string;
|
|
77
136
|
guid: string;
|
|
@@ -82,12 +141,61 @@ export interface IPadWeChatResolvedAccount {
|
|
|
82
141
|
dmPolicy: string | undefined;
|
|
83
142
|
}
|
|
84
143
|
|
|
144
|
+
/**
|
|
145
|
+
* List all account IDs from config
|
|
146
|
+
*/
|
|
147
|
+
function listAccountIds(cfg: OpenClawConfig): string[] {
|
|
148
|
+
const section = getConfigSection(cfg);
|
|
149
|
+
const accounts = (section?.accounts || []) as WeChatAccount[];
|
|
150
|
+
|
|
151
|
+
if (accounts.length > 0) {
|
|
152
|
+
return accounts.map(a => a.accountId).filter(Boolean);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Backward compatibility: single account
|
|
156
|
+
if (section?.guid) {
|
|
157
|
+
return [section.accountId || "default"];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve account from OpenClaw config
|
|
165
|
+
*
|
|
166
|
+
* Supports both multi-account (accounts array) and legacy (single) formats.
|
|
167
|
+
*/
|
|
85
168
|
function resolveAccount(
|
|
86
169
|
cfg: OpenClawConfig,
|
|
87
170
|
accountId?: string | null
|
|
88
171
|
): IPadWeChatResolvedAccount {
|
|
89
172
|
const section = getConfigSection(cfg);
|
|
173
|
+
const accounts = (section?.accounts || []) as WeChatAccount[];
|
|
90
174
|
|
|
175
|
+
// Multi-account mode: look up from accounts array
|
|
176
|
+
if (accounts.length > 0) {
|
|
177
|
+
const targetAccountId = accountId || accounts[0]?.accountId;
|
|
178
|
+
const account = accounts.find(a => a.accountId === targetAccountId);
|
|
179
|
+
|
|
180
|
+
if (!account) {
|
|
181
|
+
throw new Error(`ipad-wechat: account not found - ${targetAccountId}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
accountId: account.accountId,
|
|
186
|
+
agentId: account.agentId || null,
|
|
187
|
+
appKey: account.appKey || section?.appKey || "",
|
|
188
|
+
appSecret: account.appSecret || section?.appSecret || "",
|
|
189
|
+
guid: account.guid || section?.guid || "",
|
|
190
|
+
wechatAccountId: account.wechatAccountId || "",
|
|
191
|
+
wechatId: account.wechatId || "",
|
|
192
|
+
nickName: account.nickName || "WeChat User",
|
|
193
|
+
allowFrom: account.allowFrom ?? [],
|
|
194
|
+
dmPolicy: account.dmPolicy,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Legacy single-account mode
|
|
91
199
|
const appKey = section?.appKey;
|
|
92
200
|
const appSecret = section?.appSecret;
|
|
93
201
|
const guid = section?.guid;
|
|
@@ -98,6 +206,7 @@ function resolveAccount(
|
|
|
98
206
|
|
|
99
207
|
return {
|
|
100
208
|
accountId: accountId ?? null,
|
|
209
|
+
agentId: null,
|
|
101
210
|
appKey,
|
|
102
211
|
appSecret,
|
|
103
212
|
guid,
|
|
@@ -123,25 +232,17 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
123
232
|
supportsEditing: false,
|
|
124
233
|
supportsDeleting: false,
|
|
125
234
|
} as any,
|
|
126
|
-
// Config adapter - for runtime account management
|
|
127
235
|
config: {
|
|
128
|
-
listAccountIds: (cfg: any) =>
|
|
129
|
-
|
|
130
|
-
if (section?.wechatAccountId) {
|
|
131
|
-
return [section.wechatAccountId];
|
|
132
|
-
}
|
|
133
|
-
return ["default"];
|
|
134
|
-
},
|
|
135
|
-
resolveAccount: (cfg: any, accountId?: string | null) => {
|
|
136
|
-
return resolveAccount(cfg, accountId);
|
|
137
|
-
},
|
|
236
|
+
listAccountIds: (cfg: any) => listAccountIds(cfg),
|
|
237
|
+
resolveAccount: (cfg: any, accountId?: string | null) => resolveAccount(cfg, accountId),
|
|
138
238
|
defaultAccountId: (cfg: any) => {
|
|
139
239
|
const section = getConfigSection(cfg);
|
|
140
|
-
|
|
240
|
+
const accounts = (section?.accounts || []) as WeChatAccount[];
|
|
241
|
+
return accounts[0]?.accountId || section?.accountId || "default";
|
|
141
242
|
},
|
|
142
243
|
inspectAccount: (cfg: any, accountId?: string | null) => {
|
|
143
|
-
const
|
|
144
|
-
const hasCredentials = Boolean(
|
|
244
|
+
const account = resolveAccount(cfg, accountId);
|
|
245
|
+
const hasCredentials = Boolean(account.appKey && account.appSecret && account.guid);
|
|
145
246
|
return {
|
|
146
247
|
enabled: hasCredentials,
|
|
147
248
|
configured: hasCredentials,
|
|
@@ -149,15 +250,11 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
149
250
|
};
|
|
150
251
|
},
|
|
151
252
|
} as any,
|
|
152
|
-
// Setup adapter - for initial configuration
|
|
153
253
|
setup: {
|
|
154
254
|
resolveAccountId: (params: any) => {
|
|
155
255
|
return resolveAccount(params.cfg, params.accountId)?.accountId || "";
|
|
156
256
|
},
|
|
157
|
-
applyAccountConfig: (params: any) =>
|
|
158
|
-
const { cfg, accountId, input } = params;
|
|
159
|
-
return cfg;
|
|
160
|
-
},
|
|
257
|
+
applyAccountConfig: (params: any) => params.cfg,
|
|
161
258
|
},
|
|
162
259
|
}) as any,
|
|
163
260
|
|
|
@@ -177,7 +274,6 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
177
274
|
outbound: {
|
|
178
275
|
deliveryMode: "gateway",
|
|
179
276
|
channel: "ipad-wechat",
|
|
180
|
-
// Resolve target - for WeChat, just pass through the ID
|
|
181
277
|
resolveTarget: (params: any) => {
|
|
182
278
|
const target = params.to?.trim();
|
|
183
279
|
if (!target) {
|
|
@@ -187,17 +283,19 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
187
283
|
},
|
|
188
284
|
attachedResults: {
|
|
189
285
|
sendText: async (params: any) => {
|
|
190
|
-
// Get config from params.cfg
|
|
191
286
|
const cfg = params.cfg;
|
|
192
|
-
const
|
|
287
|
+
const accountId = params.accountId;
|
|
288
|
+
|
|
289
|
+
// Resolve account to get credentials
|
|
290
|
+
const account = resolveAccount(cfg, accountId);
|
|
193
291
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
292
|
+
// Use client pool for connection reuse
|
|
293
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
294
|
+
appKey: account.appKey,
|
|
295
|
+
appSecret: account.appSecret,
|
|
296
|
+
guid: account.guid,
|
|
198
297
|
});
|
|
199
298
|
|
|
200
|
-
// Check if DM or group
|
|
201
299
|
const isChatroom = params.to?.includes("@chatroom");
|
|
202
300
|
|
|
203
301
|
if (isChatroom) {
|
|
@@ -217,35 +315,21 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
217
315
|
|
|
218
316
|
sendMedia: async (params: any) => {
|
|
219
317
|
const cfg = params.cfg;
|
|
220
|
-
const
|
|
318
|
+
const accountId = params.accountId;
|
|
319
|
+
|
|
320
|
+
// Resolve account to get credentials
|
|
321
|
+
const account = resolveAccount(cfg, accountId);
|
|
221
322
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
323
|
+
// Use client pool
|
|
324
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
325
|
+
appKey: account.appKey,
|
|
326
|
+
appSecret: account.appSecret,
|
|
327
|
+
guid: account.guid,
|
|
226
328
|
});
|
|
227
329
|
|
|
228
330
|
const isChatroom = params.to?.includes("@chatroom");
|
|
229
331
|
|
|
230
|
-
|
|
231
|
-
let filePath = "";
|
|
232
|
-
if (params.mediaUrl) {
|
|
233
|
-
filePath = params.mediaUrl;
|
|
234
|
-
} else if (params.mediaReadFile) {
|
|
235
|
-
// Read file from local roots
|
|
236
|
-
const roots = params.mediaLocalRoots || ["./"];
|
|
237
|
-
for (const root of roots) {
|
|
238
|
-
try {
|
|
239
|
-
const fullPath = `${root}/${filePath}`;
|
|
240
|
-
const buffer = await params.mediaReadFile(fullPath);
|
|
241
|
-
// In a real implementation, we'd upload the file first
|
|
242
|
-
filePath = fullPath;
|
|
243
|
-
break;
|
|
244
|
-
} catch {
|
|
245
|
-
// Try next root
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
332
|
+
let filePath = params.mediaUrl || "";
|
|
249
333
|
|
|
250
334
|
if (isChatroom) {
|
|
251
335
|
const result = await client.sendRoomMedia({
|
|
@@ -264,22 +348,21 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
264
348
|
},
|
|
265
349
|
},
|
|
266
350
|
|
|
267
|
-
// Directory adapter - for contact resolution
|
|
268
351
|
directory: {
|
|
269
352
|
self: async (params: any) => {
|
|
270
|
-
const
|
|
353
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
271
354
|
return {
|
|
272
|
-
id:
|
|
273
|
-
name:
|
|
355
|
+
id: account.wechatId || "",
|
|
356
|
+
name: account.nickName || "WeChat User",
|
|
274
357
|
accountId: params.accountId || "default",
|
|
275
358
|
};
|
|
276
359
|
},
|
|
277
360
|
listPeers: async (params: any) => {
|
|
278
|
-
const
|
|
279
|
-
const client =
|
|
280
|
-
appKey:
|
|
281
|
-
appSecret:
|
|
282
|
-
guid:
|
|
361
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
362
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
363
|
+
appKey: account.appKey,
|
|
364
|
+
appSecret: account.appSecret,
|
|
365
|
+
guid: account.guid,
|
|
283
366
|
});
|
|
284
367
|
try {
|
|
285
368
|
const contacts = await client.syncContacts();
|
|
@@ -294,17 +377,15 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
294
377
|
}
|
|
295
378
|
},
|
|
296
379
|
listGroups: async (params: any) => {
|
|
297
|
-
// Note: JuHeBot API doesn't have a direct "list all chatrooms" endpoint
|
|
298
|
-
// For now, return empty array - users can still send to known chatroom IDs
|
|
299
380
|
console.log("[iPad WeChat] listGroups: API not available, returning empty");
|
|
300
381
|
return [];
|
|
301
382
|
},
|
|
302
383
|
listGroupMembers: async (params: any) => {
|
|
303
|
-
const
|
|
304
|
-
const client =
|
|
305
|
-
appKey:
|
|
306
|
-
appSecret:
|
|
307
|
-
guid:
|
|
384
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
385
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
386
|
+
appKey: account.appKey,
|
|
387
|
+
appSecret: account.appSecret,
|
|
388
|
+
guid: account.guid,
|
|
308
389
|
});
|
|
309
390
|
try {
|
|
310
391
|
const members = await client.getRoomMembers(params.groupId);
|
|
@@ -333,10 +414,15 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
333
414
|
},
|
|
334
415
|
} as any);
|
|
335
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Handle inbound webhook message
|
|
419
|
+
* Supports multi-account routing via AccountRegistry
|
|
420
|
+
*/
|
|
336
421
|
export async function handleInboundMessage(
|
|
337
422
|
api: any,
|
|
338
423
|
payload: WebhookPayload,
|
|
339
|
-
cfg?: OpenClawConfig
|
|
424
|
+
cfg?: OpenClawConfig,
|
|
425
|
+
overrideAccountId?: string
|
|
340
426
|
): Promise<void> {
|
|
341
427
|
const { event, message } = payload;
|
|
342
428
|
|
|
@@ -346,6 +432,24 @@ export async function handleInboundMessage(
|
|
|
346
432
|
? (message as any).roomId
|
|
347
433
|
: message.fromUser || message.toUser || "";
|
|
348
434
|
|
|
435
|
+
// Determine account ID - use override from webhook path or resolve from registry
|
|
436
|
+
let resolvedAccountId = overrideAccountId;
|
|
437
|
+
let agentId: string | null = null;
|
|
438
|
+
|
|
439
|
+
if (cfg && !resolvedAccountId) {
|
|
440
|
+
const registry = getAccountRegistry();
|
|
441
|
+
// Try by guid if available in message
|
|
442
|
+
const messageGuid = (message as any).guid;
|
|
443
|
+
if (messageGuid) {
|
|
444
|
+
agentId = registry.getAgentIdByGuid(messageGuid);
|
|
445
|
+
const regAccount = registry.getByGuid(messageGuid);
|
|
446
|
+
resolvedAccountId = regAccount?.accountId || null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Default account ID if not resolved
|
|
451
|
+
resolvedAccountId = resolvedAccountId || "default";
|
|
452
|
+
|
|
349
453
|
const openclawMessage = {
|
|
350
454
|
id: message.messageId,
|
|
351
455
|
conversation: {
|
|
@@ -361,16 +465,17 @@ export async function handleInboundMessage(
|
|
|
361
465
|
},
|
|
362
466
|
timestamp: new Date(message.timestamp || Date.now()),
|
|
363
467
|
isSelf: message.isSelf || false,
|
|
468
|
+
agentId,
|
|
469
|
+
accountId: resolvedAccountId,
|
|
364
470
|
};
|
|
365
471
|
|
|
366
472
|
// Write to cache
|
|
367
473
|
if (cfg) {
|
|
368
474
|
try {
|
|
369
475
|
const cache = getCacheManager(cfg);
|
|
370
|
-
const section = getConfigSection(cfg);
|
|
371
476
|
const wechatMessage: WeChatMessage = {
|
|
372
477
|
messageId: message.messageId,
|
|
373
|
-
accountId:
|
|
478
|
+
accountId: resolvedAccountId,
|
|
374
479
|
conversationType: isChatroom ? "chatroom" : "friend",
|
|
375
480
|
conversationId,
|
|
376
481
|
senderId: message.fromUser || "",
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Pool - Per-account client management for iPad WeChat
|
|
3
|
+
*
|
|
4
|
+
* Manages iPad (JuHeBot) API clients per account to avoid creating
|
|
5
|
+
* new clients for each message and ensure credential isolation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createIPadClient, type JuHeBotConfig } from "./client.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Client pool for iPad WeChat clients
|
|
12
|
+
*/
|
|
13
|
+
class IPadClientPool {
|
|
14
|
+
private clients: Map<string, ReturnType<typeof createIPadClient>> = new Map();
|
|
15
|
+
private configs: Map<string, JuHeBotConfig> = new Map();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get or create a client for the given account
|
|
19
|
+
*/
|
|
20
|
+
getClient(accountId: string, config: JuHeBotConfig): ReturnType<typeof createIPadClient> {
|
|
21
|
+
// Check if we already have a client and configs match
|
|
22
|
+
const existingConfig = this.configs.get(accountId);
|
|
23
|
+
if (this.clients.has(accountId) && existingConfig) {
|
|
24
|
+
// Verify configs are the same
|
|
25
|
+
if (existingConfig.appKey === config.appKey &&
|
|
26
|
+
existingConfig.appSecret === config.appSecret &&
|
|
27
|
+
existingConfig.guid === config.guid) {
|
|
28
|
+
return this.clients.get(accountId)!;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create new client
|
|
33
|
+
const client = createIPadClient(config);
|
|
34
|
+
this.clients.set(accountId, client);
|
|
35
|
+
this.configs.set(accountId, config);
|
|
36
|
+
return client;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if client exists
|
|
41
|
+
*/
|
|
42
|
+
hasClient(accountId: string): boolean {
|
|
43
|
+
return this.clients.has(accountId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Remove a client by accountId
|
|
48
|
+
*/
|
|
49
|
+
removeClient(accountId: string): boolean {
|
|
50
|
+
this.configs.delete(accountId);
|
|
51
|
+
return this.clients.delete(accountId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Clear all clients
|
|
56
|
+
*/
|
|
57
|
+
clear(): void {
|
|
58
|
+
this.clients.clear();
|
|
59
|
+
this.configs.clear();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get all account IDs with active clients
|
|
64
|
+
*/
|
|
65
|
+
getActiveAccountIds(): string[] {
|
|
66
|
+
return Array.from(this.clients.keys());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get pool size
|
|
71
|
+
*/
|
|
72
|
+
get size(): number {
|
|
73
|
+
return this.clients.size;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Singleton instance
|
|
78
|
+
let clientPool: IPadClientPool | null = null;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get or create the client pool singleton
|
|
82
|
+
*/
|
|
83
|
+
function getClientPool(): IPadClientPool {
|
|
84
|
+
if (!clientPool) {
|
|
85
|
+
clientPool = new IPadClientPool();
|
|
86
|
+
}
|
|
87
|
+
return clientPool;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get a client for a specific account
|
|
92
|
+
*/
|
|
93
|
+
export function getIPadClient(
|
|
94
|
+
accountId: string,
|
|
95
|
+
config: JuHeBotConfig
|
|
96
|
+
): ReturnType<typeof createIPadClient> {
|
|
97
|
+
return getClientPool().getClient(accountId, config);
|
|
98
|
+
}
|