@gloablehive/ipad-wechat-plugin 1.0.10 → 1.0.12
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 +181 -84
- package/dist/src/client-pool.js +83 -0
- package/index.ts +53 -4
- package/package.json +2 -2
- package/src/channel.ts +208 -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: {
|
|
@@ -130,13 +206,33 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
130
206
|
defaultPolicy: "allowlist",
|
|
131
207
|
},
|
|
132
208
|
},
|
|
209
|
+
// Messaging adapter - for target resolution
|
|
210
|
+
messaging: {
|
|
211
|
+
targetResolver: {
|
|
212
|
+
looksLikeId: (raw) => {
|
|
213
|
+
// WeChat IDs typically start with "wxid_" or are phone numbers
|
|
214
|
+
return raw.startsWith("wxid_") || /^\d{5,15}$/.test(raw) || raw.includes("@chatroom");
|
|
215
|
+
},
|
|
216
|
+
resolveTarget: async (params) => {
|
|
217
|
+
const target = params.input?.trim() || params.normalized?.trim();
|
|
218
|
+
if (!target)
|
|
219
|
+
return null;
|
|
220
|
+
// For WeChat, just pass through the ID - it's already valid
|
|
221
|
+
return {
|
|
222
|
+
to: target,
|
|
223
|
+
kind: target.includes("@chatroom") ? "group" : "user",
|
|
224
|
+
display: target,
|
|
225
|
+
source: "normalized"
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
133
230
|
threading: {
|
|
134
231
|
topLevelReplyToMode: "reply",
|
|
135
232
|
},
|
|
136
233
|
outbound: {
|
|
137
234
|
deliveryMode: "gateway",
|
|
138
235
|
channel: "ipad-wechat",
|
|
139
|
-
// Resolve target - for WeChat, just pass through the ID
|
|
140
236
|
resolveTarget: (params) => {
|
|
141
237
|
const target = params.to?.trim();
|
|
142
238
|
if (!target) {
|
|
@@ -146,15 +242,16 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
146
242
|
},
|
|
147
243
|
attachedResults: {
|
|
148
244
|
sendText: async (params) => {
|
|
149
|
-
// Get config from params.cfg
|
|
150
245
|
const cfg = params.cfg;
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
246
|
+
const accountId = params.accountId;
|
|
247
|
+
// Resolve account to get credentials
|
|
248
|
+
const account = resolveAccount(cfg, accountId);
|
|
249
|
+
// Use client pool for connection reuse
|
|
250
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
251
|
+
appKey: account.appKey,
|
|
252
|
+
appSecret: account.appSecret,
|
|
253
|
+
guid: account.guid,
|
|
156
254
|
});
|
|
157
|
-
// Check if DM or group
|
|
158
255
|
const isChatroom = params.to?.includes("@chatroom");
|
|
159
256
|
if (isChatroom) {
|
|
160
257
|
const result = await client.sendRoomMessage({
|
|
@@ -173,34 +270,17 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
173
270
|
},
|
|
174
271
|
sendMedia: async (params) => {
|
|
175
272
|
const cfg = params.cfg;
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
273
|
+
const accountId = params.accountId;
|
|
274
|
+
// Resolve account to get credentials
|
|
275
|
+
const account = resolveAccount(cfg, accountId);
|
|
276
|
+
// Use client pool
|
|
277
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
278
|
+
appKey: account.appKey,
|
|
279
|
+
appSecret: account.appSecret,
|
|
280
|
+
guid: account.guid,
|
|
181
281
|
});
|
|
182
282
|
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
|
-
}
|
|
283
|
+
let filePath = params.mediaUrl || "";
|
|
204
284
|
if (isChatroom) {
|
|
205
285
|
const result = await client.sendRoomMedia({
|
|
206
286
|
roomId: params.to,
|
|
@@ -218,22 +298,21 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
218
298
|
},
|
|
219
299
|
},
|
|
220
300
|
},
|
|
221
|
-
// Directory adapter - for contact resolution
|
|
222
301
|
directory: {
|
|
223
302
|
self: async (params) => {
|
|
224
|
-
const
|
|
303
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
225
304
|
return {
|
|
226
|
-
id:
|
|
227
|
-
name:
|
|
305
|
+
id: account.wechatId || "",
|
|
306
|
+
name: account.nickName || "WeChat User",
|
|
228
307
|
accountId: params.accountId || "default",
|
|
229
308
|
};
|
|
230
309
|
},
|
|
231
310
|
listPeers: async (params) => {
|
|
232
|
-
const
|
|
233
|
-
const client =
|
|
234
|
-
appKey:
|
|
235
|
-
appSecret:
|
|
236
|
-
guid:
|
|
311
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
312
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
313
|
+
appKey: account.appKey,
|
|
314
|
+
appSecret: account.appSecret,
|
|
315
|
+
guid: account.guid,
|
|
237
316
|
});
|
|
238
317
|
try {
|
|
239
318
|
const contacts = await client.syncContacts();
|
|
@@ -249,17 +328,15 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
249
328
|
}
|
|
250
329
|
},
|
|
251
330
|
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
331
|
console.log("[iPad WeChat] listGroups: API not available, returning empty");
|
|
255
332
|
return [];
|
|
256
333
|
},
|
|
257
334
|
listGroupMembers: async (params) => {
|
|
258
|
-
const
|
|
259
|
-
const client =
|
|
260
|
-
appKey:
|
|
261
|
-
appSecret:
|
|
262
|
-
guid:
|
|
335
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
336
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
337
|
+
appKey: account.appKey,
|
|
338
|
+
appSecret: account.appSecret,
|
|
339
|
+
guid: account.guid,
|
|
263
340
|
});
|
|
264
341
|
try {
|
|
265
342
|
const members = await client.getRoomMembers(params.groupId);
|
|
@@ -287,13 +364,32 @@ export const ipadWeChatPlugin = createChatChannelPlugin({
|
|
|
287
364
|
supportsDeleting: false,
|
|
288
365
|
},
|
|
289
366
|
});
|
|
290
|
-
|
|
367
|
+
/**
|
|
368
|
+
* Handle inbound webhook message
|
|
369
|
+
* Supports multi-account routing via AccountRegistry
|
|
370
|
+
*/
|
|
371
|
+
export async function handleInboundMessage(api, payload, cfg, overrideAccountId) {
|
|
291
372
|
const { event, message } = payload;
|
|
292
373
|
if (event === "message" && message) {
|
|
293
374
|
const isChatroom = !!message.roomId;
|
|
294
375
|
const conversationId = isChatroom
|
|
295
376
|
? message.roomId
|
|
296
377
|
: message.fromUser || message.toUser || "";
|
|
378
|
+
// Determine account ID - use override from webhook path or resolve from registry
|
|
379
|
+
let resolvedAccountId = overrideAccountId;
|
|
380
|
+
let agentId = null;
|
|
381
|
+
if (cfg && !resolvedAccountId) {
|
|
382
|
+
const registry = getAccountRegistry();
|
|
383
|
+
// Try by guid if available in message
|
|
384
|
+
const messageGuid = message.guid;
|
|
385
|
+
if (messageGuid) {
|
|
386
|
+
agentId = registry.getAgentIdByGuid(messageGuid);
|
|
387
|
+
const regAccount = registry.getByGuid(messageGuid);
|
|
388
|
+
resolvedAccountId = regAccount?.accountId || null;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Default account ID if not resolved
|
|
392
|
+
resolvedAccountId = resolvedAccountId || "default";
|
|
297
393
|
const openclawMessage = {
|
|
298
394
|
id: message.messageId,
|
|
299
395
|
conversation: {
|
|
@@ -309,15 +405,16 @@ export async function handleInboundMessage(api, payload, cfg) {
|
|
|
309
405
|
},
|
|
310
406
|
timestamp: new Date(message.timestamp || Date.now()),
|
|
311
407
|
isSelf: message.isSelf || false,
|
|
408
|
+
agentId,
|
|
409
|
+
accountId: resolvedAccountId,
|
|
312
410
|
};
|
|
313
411
|
// Write to cache
|
|
314
412
|
if (cfg) {
|
|
315
413
|
try {
|
|
316
414
|
const cache = getCacheManager(cfg);
|
|
317
|
-
const section = getConfigSection(cfg);
|
|
318
415
|
const wechatMessage = {
|
|
319
416
|
messageId: message.messageId,
|
|
320
|
-
accountId:
|
|
417
|
+
accountId: resolvedAccountId,
|
|
321
418
|
conversationType: isChatroom ? "chatroom" : "friend",
|
|
322
419
|
conversationId,
|
|
323
420
|
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.12",
|
|
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
|
|
|
@@ -170,6 +267,28 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
170
267
|
},
|
|
171
268
|
},
|
|
172
269
|
|
|
270
|
+
// Messaging adapter - for target resolution
|
|
271
|
+
messaging: {
|
|
272
|
+
targetResolver: {
|
|
273
|
+
looksLikeId: (raw: string) => {
|
|
274
|
+
// WeChat IDs typically start with "wxid_" or are phone numbers
|
|
275
|
+
return raw.startsWith("wxid_") || /^\d{5,15}$/.test(raw) || raw.includes("@chatroom");
|
|
276
|
+
},
|
|
277
|
+
resolveTarget: async (params: any) => {
|
|
278
|
+
const target = params.input?.trim() || params.normalized?.trim();
|
|
279
|
+
if (!target) return null;
|
|
280
|
+
|
|
281
|
+
// For WeChat, just pass through the ID - it's already valid
|
|
282
|
+
return {
|
|
283
|
+
to: target,
|
|
284
|
+
kind: target.includes("@chatroom") ? "group" : "user",
|
|
285
|
+
display: target,
|
|
286
|
+
source: "normalized"
|
|
287
|
+
};
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
|
|
173
292
|
threading: {
|
|
174
293
|
topLevelReplyToMode: "reply",
|
|
175
294
|
},
|
|
@@ -177,7 +296,6 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
177
296
|
outbound: {
|
|
178
297
|
deliveryMode: "gateway",
|
|
179
298
|
channel: "ipad-wechat",
|
|
180
|
-
// Resolve target - for WeChat, just pass through the ID
|
|
181
299
|
resolveTarget: (params: any) => {
|
|
182
300
|
const target = params.to?.trim();
|
|
183
301
|
if (!target) {
|
|
@@ -187,17 +305,19 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
187
305
|
},
|
|
188
306
|
attachedResults: {
|
|
189
307
|
sendText: async (params: any) => {
|
|
190
|
-
// Get config from params.cfg
|
|
191
308
|
const cfg = params.cfg;
|
|
192
|
-
const
|
|
309
|
+
const accountId = params.accountId;
|
|
193
310
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
311
|
+
// Resolve account to get credentials
|
|
312
|
+
const account = resolveAccount(cfg, accountId);
|
|
313
|
+
|
|
314
|
+
// Use client pool for connection reuse
|
|
315
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
316
|
+
appKey: account.appKey,
|
|
317
|
+
appSecret: account.appSecret,
|
|
318
|
+
guid: account.guid,
|
|
198
319
|
});
|
|
199
320
|
|
|
200
|
-
// Check if DM or group
|
|
201
321
|
const isChatroom = params.to?.includes("@chatroom");
|
|
202
322
|
|
|
203
323
|
if (isChatroom) {
|
|
@@ -217,35 +337,21 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
217
337
|
|
|
218
338
|
sendMedia: async (params: any) => {
|
|
219
339
|
const cfg = params.cfg;
|
|
220
|
-
const
|
|
340
|
+
const accountId = params.accountId;
|
|
221
341
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
342
|
+
// Resolve account to get credentials
|
|
343
|
+
const account = resolveAccount(cfg, accountId);
|
|
344
|
+
|
|
345
|
+
// Use client pool
|
|
346
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
347
|
+
appKey: account.appKey,
|
|
348
|
+
appSecret: account.appSecret,
|
|
349
|
+
guid: account.guid,
|
|
226
350
|
});
|
|
227
351
|
|
|
228
352
|
const isChatroom = params.to?.includes("@chatroom");
|
|
229
353
|
|
|
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
|
-
}
|
|
354
|
+
let filePath = params.mediaUrl || "";
|
|
249
355
|
|
|
250
356
|
if (isChatroom) {
|
|
251
357
|
const result = await client.sendRoomMedia({
|
|
@@ -264,22 +370,21 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
264
370
|
},
|
|
265
371
|
},
|
|
266
372
|
|
|
267
|
-
// Directory adapter - for contact resolution
|
|
268
373
|
directory: {
|
|
269
374
|
self: async (params: any) => {
|
|
270
|
-
const
|
|
375
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
271
376
|
return {
|
|
272
|
-
id:
|
|
273
|
-
name:
|
|
377
|
+
id: account.wechatId || "",
|
|
378
|
+
name: account.nickName || "WeChat User",
|
|
274
379
|
accountId: params.accountId || "default",
|
|
275
380
|
};
|
|
276
381
|
},
|
|
277
382
|
listPeers: async (params: any) => {
|
|
278
|
-
const
|
|
279
|
-
const client =
|
|
280
|
-
appKey:
|
|
281
|
-
appSecret:
|
|
282
|
-
guid:
|
|
383
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
384
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
385
|
+
appKey: account.appKey,
|
|
386
|
+
appSecret: account.appSecret,
|
|
387
|
+
guid: account.guid,
|
|
283
388
|
});
|
|
284
389
|
try {
|
|
285
390
|
const contacts = await client.syncContacts();
|
|
@@ -294,17 +399,15 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
294
399
|
}
|
|
295
400
|
},
|
|
296
401
|
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
402
|
console.log("[iPad WeChat] listGroups: API not available, returning empty");
|
|
300
403
|
return [];
|
|
301
404
|
},
|
|
302
405
|
listGroupMembers: async (params: any) => {
|
|
303
|
-
const
|
|
304
|
-
const client =
|
|
305
|
-
appKey:
|
|
306
|
-
appSecret:
|
|
307
|
-
guid:
|
|
406
|
+
const account = resolveAccount(params.cfg, params.accountId);
|
|
407
|
+
const client = getIPadClient(account.accountId || "default", {
|
|
408
|
+
appKey: account.appKey,
|
|
409
|
+
appSecret: account.appSecret,
|
|
410
|
+
guid: account.guid,
|
|
308
411
|
});
|
|
309
412
|
try {
|
|
310
413
|
const members = await client.getRoomMembers(params.groupId);
|
|
@@ -333,10 +436,15 @@ export const ipadWeChatPlugin = createChatChannelPlugin<IPadWeChatResolvedAccoun
|
|
|
333
436
|
},
|
|
334
437
|
} as any);
|
|
335
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Handle inbound webhook message
|
|
441
|
+
* Supports multi-account routing via AccountRegistry
|
|
442
|
+
*/
|
|
336
443
|
export async function handleInboundMessage(
|
|
337
444
|
api: any,
|
|
338
445
|
payload: WebhookPayload,
|
|
339
|
-
cfg?: OpenClawConfig
|
|
446
|
+
cfg?: OpenClawConfig,
|
|
447
|
+
overrideAccountId?: string
|
|
340
448
|
): Promise<void> {
|
|
341
449
|
const { event, message } = payload;
|
|
342
450
|
|
|
@@ -346,6 +454,24 @@ export async function handleInboundMessage(
|
|
|
346
454
|
? (message as any).roomId
|
|
347
455
|
: message.fromUser || message.toUser || "";
|
|
348
456
|
|
|
457
|
+
// Determine account ID - use override from webhook path or resolve from registry
|
|
458
|
+
let resolvedAccountId = overrideAccountId;
|
|
459
|
+
let agentId: string | null = null;
|
|
460
|
+
|
|
461
|
+
if (cfg && !resolvedAccountId) {
|
|
462
|
+
const registry = getAccountRegistry();
|
|
463
|
+
// Try by guid if available in message
|
|
464
|
+
const messageGuid = (message as any).guid;
|
|
465
|
+
if (messageGuid) {
|
|
466
|
+
agentId = registry.getAgentIdByGuid(messageGuid);
|
|
467
|
+
const regAccount = registry.getByGuid(messageGuid);
|
|
468
|
+
resolvedAccountId = regAccount?.accountId || null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Default account ID if not resolved
|
|
473
|
+
resolvedAccountId = resolvedAccountId || "default";
|
|
474
|
+
|
|
349
475
|
const openclawMessage = {
|
|
350
476
|
id: message.messageId,
|
|
351
477
|
conversation: {
|
|
@@ -361,16 +487,17 @@ export async function handleInboundMessage(
|
|
|
361
487
|
},
|
|
362
488
|
timestamp: new Date(message.timestamp || Date.now()),
|
|
363
489
|
isSelf: message.isSelf || false,
|
|
490
|
+
agentId,
|
|
491
|
+
accountId: resolvedAccountId,
|
|
364
492
|
};
|
|
365
493
|
|
|
366
494
|
// Write to cache
|
|
367
495
|
if (cfg) {
|
|
368
496
|
try {
|
|
369
497
|
const cache = getCacheManager(cfg);
|
|
370
|
-
const section = getConfigSection(cfg);
|
|
371
498
|
const wechatMessage: WeChatMessage = {
|
|
372
499
|
messageId: message.messageId,
|
|
373
|
-
accountId:
|
|
500
|
+
accountId: resolvedAccountId,
|
|
374
501
|
conversationType: isChatroom ? "chatroom" : "friend",
|
|
375
502
|
conversationId,
|
|
376
503
|
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
|
+
}
|