@gloablehive/ipad-wechat-plugin 1.0.9 → 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 +201 -68
- package/dist/src/client-pool.js +83 -0
- package/index.ts +53 -4
- package/package.json +2 -2
- package/src/channel.ts +226 -65
- 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,6 +277,60 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
218
277
|
},
|
|
219
278
|
},
|
|
220
279
|
},
|
|
280
|
+
directory: {
|
|
281
|
+
self: async (params) => {
|
|
282
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
283
|
+
return {
|
|
284
|
+
id: account.wechatId || "",
|
|
285
|
+
name: account.nickName || "WeChat User",
|
|
286
|
+
accountId: params.accountId || "default",
|
|
287
|
+
};
|
|
288
|
+
},
|
|
289
|
+
listPeers: async (params) => {
|
|
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,
|
|
295
|
+
});
|
|
296
|
+
try {
|
|
297
|
+
const contacts = await client.syncContacts();
|
|
298
|
+
return contacts.map((contact) => ({
|
|
299
|
+
id: contact.wechatId || contact.wxid,
|
|
300
|
+
name: contact.nickName || contact.remark || contact.wechatId,
|
|
301
|
+
accountId: params.accountId || "default",
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
console.error("[iPad WeChat] Failed to list peers:", error);
|
|
306
|
+
return [];
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
listGroups: async (params) => {
|
|
310
|
+
console.log("[iPad WeChat] listGroups: API not available, returning empty");
|
|
311
|
+
return [];
|
|
312
|
+
},
|
|
313
|
+
listGroupMembers: async (params) => {
|
|
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,
|
|
319
|
+
});
|
|
320
|
+
try {
|
|
321
|
+
const members = await client.getRoomMembers(params.groupId);
|
|
322
|
+
return members.map((member) => ({
|
|
323
|
+
id: member.userId || member.wxid,
|
|
324
|
+
name: member.nickName || member.displayName || member.userId,
|
|
325
|
+
accountId: params.accountId || "default",
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
console.error("[iPad WeChat] Failed to list group members:", error);
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
},
|
|
221
334
|
capabilities: {
|
|
222
335
|
supportedMessageTypes: ["text", "image", "video", "file", "link", "location", "contact"],
|
|
223
336
|
maxAttachmentSize: 25 * 1024 * 1024,
|
|
@@ -230,13 +343,32 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
230
343
|
supportsDeleting: false,
|
|
231
344
|
},
|
|
232
345
|
});
|
|
233
|
-
|
|
346
|
+
/**
|
|
347
|
+
* Handle inbound webhook message
|
|
348
|
+
* Supports multi-account routing via AccountRegistry
|
|
349
|
+
*/
|
|
350
|
+
export async function handleInboundMessage(api, payload, cfg, overrideAccountId) {
|
|
234
351
|
const { event, message } = payload;
|
|
235
352
|
if (event === "message" && message) {
|
|
236
353
|
const isChatroom = !!message.roomId;
|
|
237
354
|
const conversationId = isChatroom
|
|
238
355
|
? message.roomId
|
|
239
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";
|
|
240
372
|
const openclawMessage = {
|
|
241
373
|
id: message.messageId,
|
|
242
374
|
conversation: {
|
|
@@ -252,15 +384,16 @@ export async function handleInboundMessage(api, payload, cfg) {
|
|
|
252
384
|
},
|
|
253
385
|
timestamp: new Date(message.timestamp || Date.now()),
|
|
254
386
|
isSelf: message.isSelf || false,
|
|
387
|
+
agentId,
|
|
388
|
+
accountId: resolvedAccountId,
|
|
255
389
|
};
|
|
256
390
|
// Write to cache
|
|
257
391
|
if (cfg) {
|
|
258
392
|
try {
|
|
259
393
|
const cache = getCacheManager(cfg);
|
|
260
|
-
const section = getConfigSection(cfg);
|
|
261
394
|
const wechatMessage = {
|
|
262
395
|
messageId: message.messageId,
|
|
263
|
-
accountId:
|
|
396
|
+
accountId: resolvedAccountId,
|
|
264
397
|
conversationType: isChatroom ? "chatroom" : "friend",
|
|
265
398
|
conversationId,
|
|
266
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[];
|
|
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
|
+
}
|
|
90
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,6 +348,59 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
264
348
|
},
|
|
265
349
|
},
|
|
266
350
|
|
|
351
|
+
directory: {
|
|
352
|
+
self: async (params: any) => {
|
|
353
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
354
|
+
return {
|
|
355
|
+
id: account.wechatId || "",
|
|
356
|
+
name: account.nickName || "WeChat User",
|
|
357
|
+
accountId: params.accountId || "default",
|
|
358
|
+
};
|
|
359
|
+
},
|
|
360
|
+
listPeers: async (params: any) => {
|
|
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,
|
|
366
|
+
});
|
|
367
|
+
try {
|
|
368
|
+
const contacts = await client.syncContacts();
|
|
369
|
+
return contacts.map((contact: any) => ({
|
|
370
|
+
id: contact.wechatId || contact.wxid,
|
|
371
|
+
name: contact.nickName || contact.remark || contact.wechatId,
|
|
372
|
+
accountId: params.accountId || "default",
|
|
373
|
+
}));
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.error("[iPad WeChat] Failed to list peers:", error);
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
listGroups: async (params: any) => {
|
|
380
|
+
console.log("[iPad WeChat] listGroups: API not available, returning empty");
|
|
381
|
+
return [];
|
|
382
|
+
},
|
|
383
|
+
listGroupMembers: async (params: any) => {
|
|
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,
|
|
389
|
+
});
|
|
390
|
+
try {
|
|
391
|
+
const members = await client.getRoomMembers(params.groupId);
|
|
392
|
+
return members.map((member: any) => ({
|
|
393
|
+
id: member.userId || member.wxid,
|
|
394
|
+
name: member.nickName || member.displayName || member.userId,
|
|
395
|
+
accountId: params.accountId || "default",
|
|
396
|
+
}));
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error("[iPad WeChat] Failed to list group members:", error);
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
|
|
267
404
|
capabilities: {
|
|
268
405
|
supportedMessageTypes: ["text", "image", "video", "file", "link", "location", "contact"],
|
|
269
406
|
maxAttachmentSize: 25 * 1024 * 1024,
|
|
@@ -277,10 +414,15 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
277
414
|
},
|
|
278
415
|
} as any);
|
|
279
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Handle inbound webhook message
|
|
419
|
+
* Supports multi-account routing via AccountRegistry
|
|
420
|
+
*/
|
|
280
421
|
export async function handleInboundMessage(
|
|
281
422
|
api: any,
|
|
282
423
|
payload: WebhookPayload,
|
|
283
|
-
cfg?: OpenClawConfig
|
|
424
|
+
cfg?: OpenClawConfig,
|
|
425
|
+
overrideAccountId?: string
|
|
284
426
|
): Promise<void> {
|
|
285
427
|
const { event, message } = payload;
|
|
286
428
|
|
|
@@ -290,6 +432,24 @@ export async function handleInboundMessage(
|
|
|
290
432
|
? (message as any).roomId
|
|
291
433
|
: message.fromUser || message.toUser || "";
|
|
292
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
|
+
|
|
293
453
|
const openclawMessage = {
|
|
294
454
|
id: message.messageId,
|
|
295
455
|
conversation: {
|
|
@@ -305,16 +465,17 @@ export async function handleInboundMessage(
|
|
|
305
465
|
},
|
|
306
466
|
timestamp: new Date(message.timestamp || Date.now()),
|
|
307
467
|
isSelf: message.isSelf || false,
|
|
468
|
+
agentId,
|
|
469
|
+
accountId: resolvedAccountId,
|
|
308
470
|
};
|
|
309
471
|
|
|
310
472
|
// Write to cache
|
|
311
473
|
if (cfg) {
|
|
312
474
|
try {
|
|
313
475
|
const cache = getCacheManager(cfg);
|
|
314
|
-
const section = getConfigSection(cfg);
|
|
315
476
|
const wechatMessage: WeChatMessage = {
|
|
316
477
|
messageId: message.messageId,
|
|
317
|
-
accountId:
|
|
478
|
+
accountId: resolvedAccountId,
|
|
318
479
|
conversationType: isChatroom ? "chatroom" : "friend",
|
|
319
480
|
conversationId,
|
|
320
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
|
+
}
|