@gloablehive/celphone-wechat-plugin 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +15 -6
- package/dist/src/channel.js +165 -105
- package/dist/src/client-pool.js +82 -0
- package/index.ts +17 -7
- package/package.json +3 -3
- package/src/channel.ts +210 -114
- package/src/client-pool.ts +103 -0
- package/src/client.ts +1 -1
- package/test-cache.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* WorkPhone WeChat Plugin - enables OpenClaw to send/receive WeChat messages
|
|
5
5
|
* through the WorkPhone API platform.
|
|
6
|
+
*
|
|
7
|
+
* Multi-account support:
|
|
8
|
+
* - Each account in accounts[] can bind to a different agent
|
|
9
|
+
* - Webhook routing uses AccountRegistry to find agentId
|
|
10
|
+
* - cfg is passed to handleInboundMessage for cache isolation
|
|
6
11
|
*/
|
|
7
12
|
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
8
13
|
import { celPhoneWeChatPlugin, handleInboundMessage } from "./src/channel.js";
|
|
@@ -35,6 +40,8 @@ export default defineChannelPluginEntry({
|
|
|
35
40
|
auth: "plugin", // Plugin-managed auth - verify signatures yourself
|
|
36
41
|
handler: async (req, res) => {
|
|
37
42
|
try {
|
|
43
|
+
// Get config from request (provided by OpenClaw framework)
|
|
44
|
+
const cfg = req.cfg;
|
|
38
45
|
// Parse the webhook payload
|
|
39
46
|
// The exact format depends on WorkPhone's webhook configuration
|
|
40
47
|
const payload = req.body;
|
|
@@ -46,8 +53,8 @@ export default defineChannelPluginEntry({
|
|
|
46
53
|
res.end("Missing signature");
|
|
47
54
|
return true;
|
|
48
55
|
}
|
|
49
|
-
// Handle the inbound message
|
|
50
|
-
await handleInboundMessage(api, payload);
|
|
56
|
+
// Handle the inbound message with cfg for multi-account routing
|
|
57
|
+
await handleInboundMessage(api, payload, cfg);
|
|
51
58
|
res.statusCode = 200;
|
|
52
59
|
res.end("ok");
|
|
53
60
|
return true;
|
|
@@ -61,15 +68,17 @@ export default defineChannelPluginEntry({
|
|
|
61
68
|
},
|
|
62
69
|
});
|
|
63
70
|
// Register gateway method for outbound media handling
|
|
64
|
-
api.
|
|
65
|
-
method: "POST",
|
|
71
|
+
api.registerHttpRoute({
|
|
66
72
|
path: "/celphone-wechat/media",
|
|
73
|
+
auth: "plugin",
|
|
67
74
|
handler: async (req, res) => {
|
|
68
|
-
const
|
|
75
|
+
const payload = req.body;
|
|
76
|
+
const { messageId, mediaUrl, mediaType } = payload || {};
|
|
69
77
|
// Handle media upload/send through WorkPhone
|
|
70
78
|
// This is called when the bot needs to send media files
|
|
79
|
+
res.setHeader("Content-Type", "application/json");
|
|
71
80
|
res.statusCode = 200;
|
|
72
|
-
res.
|
|
81
|
+
res.end(JSON.stringify({ success: true, messageId }));
|
|
73
82
|
return true;
|
|
74
83
|
},
|
|
75
84
|
});
|
package/dist/src/channel.js
CHANGED
|
@@ -9,21 +9,49 @@
|
|
|
9
9
|
* The agent communicates with ALL friends and groups under that WeChat account,
|
|
10
10
|
* not just one DM context. This is "human mode" vs "bot mode".
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* - 4-layer compression
|
|
17
|
-
* - AI summary extraction
|
|
18
|
-
* - SAAS connectivity + offline fallback
|
|
19
|
-
* - Cloud sync
|
|
12
|
+
* Multi-account support:
|
|
13
|
+
* - Config supports accounts array with per-account credentials
|
|
14
|
+
* - Each account can be bound to a different agent
|
|
15
|
+
* - Security policies are per-account isolated
|
|
20
16
|
*/
|
|
21
17
|
import { createChatChannelPlugin, createChannelPluginBase, } from "openclaw/plugin-sdk/core";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
// Import cache modules from shared package
|
|
19
|
+
import { createCacheManager, createAccountRegistry, } from "@gloablehive/wechat-cache";
|
|
20
|
+
// Import client pool
|
|
21
|
+
import { getWorkPhoneClient, createClientConfig } from "./client-pool.js";
|
|
25
22
|
// Cache manager instance (lazy initialized)
|
|
26
23
|
let cacheManager = null;
|
|
24
|
+
// Account registry for routing
|
|
25
|
+
let accountRegistry = null;
|
|
26
|
+
/**
|
|
27
|
+
* Get or create account registry
|
|
28
|
+
*/
|
|
29
|
+
function getAccountRegistry() {
|
|
30
|
+
if (!accountRegistry) {
|
|
31
|
+
accountRegistry = createAccountRegistry();
|
|
32
|
+
}
|
|
33
|
+
return accountRegistry;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Initialize account registry from config
|
|
37
|
+
*/
|
|
38
|
+
function initializeRegistry(cfg) {
|
|
39
|
+
const registry = getAccountRegistry();
|
|
40
|
+
registry.clear();
|
|
41
|
+
const section = cfg.channels?.["celphone-wechat"];
|
|
42
|
+
const accounts = (section?.accounts || []);
|
|
43
|
+
// Register each account
|
|
44
|
+
for (const account of accounts) {
|
|
45
|
+
if (account.enabled !== false) {
|
|
46
|
+
const registeredAccount = {
|
|
47
|
+
...account,
|
|
48
|
+
channelType: "celphone-wechat",
|
|
49
|
+
registeredAt: Date.now(),
|
|
50
|
+
};
|
|
51
|
+
registry.register(registeredAccount);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
27
55
|
/**
|
|
28
56
|
* Get or create cache manager
|
|
29
57
|
*/
|
|
@@ -35,15 +63,17 @@ function getCacheManager(cfg) {
|
|
|
35
63
|
// If no accounts configured, create default from main config
|
|
36
64
|
if (accounts.length === 0 && section?.wechatAccountId) {
|
|
37
65
|
accounts.push({
|
|
38
|
-
accountId: section.accountId ||
|
|
66
|
+
accountId: section.accountId || "default",
|
|
39
67
|
wechatAccountId: section.wechatAccountId,
|
|
40
68
|
wechatId: section.wechatId || section.wechatAccountId,
|
|
41
|
-
nickName: section.nickName ||
|
|
69
|
+
nickName: section.nickName || "WeChat User",
|
|
42
70
|
enabled: true,
|
|
43
71
|
});
|
|
44
72
|
}
|
|
73
|
+
// Initialize registry with accounts
|
|
74
|
+
initializeRegistry(cfg);
|
|
45
75
|
const basePath = globalThis?.process?.env?.OPENCLAW_CACHE_PATH
|
|
46
|
-
||
|
|
76
|
+
|| "~/.openclaw/channels/celphone-wechat";
|
|
47
77
|
cacheManager = createCacheManager({
|
|
48
78
|
basePath,
|
|
49
79
|
accounts,
|
|
@@ -54,24 +84,59 @@ function getCacheManager(cfg) {
|
|
|
54
84
|
} : undefined,
|
|
55
85
|
syncConfig: section?.sync ? {
|
|
56
86
|
databaseUrl: section.sync.databaseUrl,
|
|
57
|
-
syncMode: section.sync.syncMode ||
|
|
87
|
+
syncMode: section.sync.syncMode || "interval",
|
|
58
88
|
syncIntervalMs: section.sync.syncIntervalMs || 5 * 60 * 1000,
|
|
59
89
|
} : undefined,
|
|
60
90
|
});
|
|
61
|
-
// Initialize cache manager
|
|
91
|
+
// Initialize cache manager - must await for ready state
|
|
62
92
|
cacheManager.init().catch(err => {
|
|
63
|
-
console.error(
|
|
93
|
+
console.error("[CelPhoneWeChat] Cache manager init failed:", err);
|
|
64
94
|
});
|
|
65
95
|
return cacheManager;
|
|
66
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* List all account IDs from config
|
|
99
|
+
*/
|
|
100
|
+
function listAccountIds(cfg) {
|
|
101
|
+
const section = cfg.channels?.["celphone-wechat"];
|
|
102
|
+
const accounts = (section?.accounts || []);
|
|
103
|
+
if (accounts.length > 0) {
|
|
104
|
+
return accounts.map(a => a.accountId).filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
// Backward compatibility: single account
|
|
107
|
+
if (section?.wechatAccountId) {
|
|
108
|
+
return [section.accountId || "default"];
|
|
109
|
+
}
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
67
112
|
/**
|
|
68
113
|
* Resolve account from OpenClaw config
|
|
69
114
|
*
|
|
70
|
-
*
|
|
71
|
-
* a real person with all their friends and groups, not a bot.
|
|
115
|
+
* Supports both multi-account (accounts array) and legacy (single) formats.
|
|
72
116
|
*/
|
|
73
117
|
function resolveAccount(cfg, accountId) {
|
|
74
118
|
const section = cfg.channels?.["celphone-wechat"];
|
|
119
|
+
const accounts = (section?.accounts || []);
|
|
120
|
+
// Multi-account mode: look up from accounts array
|
|
121
|
+
if (accounts.length > 0) {
|
|
122
|
+
const targetAccountId = accountId || accounts[0]?.accountId;
|
|
123
|
+
const account = accounts.find(a => a.accountId === targetAccountId);
|
|
124
|
+
if (!account) {
|
|
125
|
+
throw new Error(`celphone-wechat: account not found - ${targetAccountId}`);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
accountId: account.accountId,
|
|
129
|
+
agentId: account.agentId || null,
|
|
130
|
+
apiKey: account.apiKey || section?.apiKey || "",
|
|
131
|
+
baseUrl: account.baseUrl || section?.baseUrl || "https://api.workphone.example.com",
|
|
132
|
+
wechatAccountId: account.wechatAccountId,
|
|
133
|
+
wechatId: account.wechatId || account.wechatAccountId,
|
|
134
|
+
nickName: account.nickName || "WeChat User",
|
|
135
|
+
allowFrom: account.allowFrom ?? [],
|
|
136
|
+
dmPolicy: account.dmPolicy,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Legacy single-account mode
|
|
75
140
|
const apiKey = section?.apiKey;
|
|
76
141
|
const baseUrl = section?.baseUrl || "https://api.workphone.example.com";
|
|
77
142
|
const wechatAccountId = section?.wechatAccountId;
|
|
@@ -83,11 +148,12 @@ function resolveAccount(cfg, accountId) {
|
|
|
83
148
|
}
|
|
84
149
|
return {
|
|
85
150
|
accountId: accountId ?? null,
|
|
151
|
+
agentId: null,
|
|
86
152
|
apiKey,
|
|
87
153
|
baseUrl,
|
|
88
154
|
wechatAccountId,
|
|
89
|
-
wechatId: section?.wechatId || wechatAccountId,
|
|
90
|
-
nickName: section?.nickName || "WeChat User",
|
|
155
|
+
wechatId: section?.wechatId || wechatAccountId,
|
|
156
|
+
nickName: section?.nickName || "WeChat User",
|
|
91
157
|
allowFrom: section?.allowFrom ?? [],
|
|
92
158
|
dmPolicy: section?.dmSecurity,
|
|
93
159
|
};
|
|
@@ -99,115 +165,86 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
|
|
|
99
165
|
base: createChannelPluginBase({
|
|
100
166
|
id: "celphone-wechat",
|
|
101
167
|
setup: {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const section = cfg.channels?.["celphone-wechat"];
|
|
105
|
-
const hasApiKey = Boolean(section?.apiKey);
|
|
106
|
-
return {
|
|
107
|
-
enabled: hasApiKey,
|
|
108
|
-
configured: hasApiKey,
|
|
109
|
-
tokenStatus: hasApiKey ? "available" : "missing",
|
|
110
|
-
};
|
|
168
|
+
resolveAccountId: (params) => {
|
|
169
|
+
return resolveAccount(params.cfg, params.accountId)?.accountId || "";
|
|
111
170
|
},
|
|
171
|
+
applyAccountConfig: (params) => params.cfg,
|
|
112
172
|
},
|
|
113
173
|
}),
|
|
114
|
-
// DM security: who can message the bot
|
|
115
174
|
security: {
|
|
116
175
|
dm: {
|
|
117
176
|
channelKey: "celphone-wechat",
|
|
118
177
|
resolvePolicy: (account) => account.dmPolicy,
|
|
119
178
|
resolveAllowFrom: (account) => account.allowFrom,
|
|
120
|
-
defaultPolicy: "allowlist",
|
|
179
|
+
defaultPolicy: "allowlist",
|
|
121
180
|
},
|
|
122
181
|
},
|
|
123
|
-
// Pairing: not currently supported for this channel
|
|
124
|
-
// WorkPhone WeChat doesn't have a standard pairing flow
|
|
125
|
-
// pairing: { ... },
|
|
126
|
-
// Threading: how replies are delivered
|
|
127
|
-
// For WeChat, replies go back to the same chat (friend or chatroom)
|
|
128
182
|
threading: {
|
|
129
|
-
topLevelReplyToMode: "reply",
|
|
183
|
+
topLevelReplyToMode: "reply",
|
|
130
184
|
},
|
|
131
|
-
// Outbound: send messages to WeChat via WorkPhone API
|
|
132
|
-
//
|
|
133
|
-
// HUMAN ACCOUNT MODEL IMPORTANT:
|
|
134
|
-
// This channel connects to a real person's WeChat account.
|
|
135
|
-
// The agent needs to communicate with ALL their friends and groups,
|
|
136
|
-
// not just one conversation. We distinguish by conversation type:
|
|
137
|
-
// - DM (direct): friendWechatId in params.to
|
|
138
|
-
// - Group chat: chatroomId in params.to (detect by format or metadata)
|
|
139
185
|
outbound: {
|
|
186
|
+
channel: "celphone-wechat",
|
|
140
187
|
attachedResults: {
|
|
141
|
-
|
|
188
|
+
channel: "celphone-wechat",
|
|
142
189
|
sendText: async (params) => {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const isChatroom = params.metadata?.conversationType === 'group' ||
|
|
152
|
-
(params.to && params.to.includes('@chatroom'));
|
|
190
|
+
const cfg = params.cfg;
|
|
191
|
+
const accountId = params.accountId;
|
|
192
|
+
// Resolve account to get credentials
|
|
193
|
+
const account = resolveAccount(cfg, accountId);
|
|
194
|
+
// Use client pool for connection reuse
|
|
195
|
+
const clientConfig = createClientConfig(account.baseUrl, account.apiKey, account.accountId || undefined, account.wechatAccountId);
|
|
196
|
+
const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
|
|
197
|
+
const isChatroom = params.to?.includes("@chatroom");
|
|
153
198
|
let result;
|
|
154
199
|
if (isChatroom) {
|
|
155
|
-
// Send to chatroom (group)
|
|
156
200
|
result = await client.sendChatroomMessage({
|
|
157
|
-
wechatAccountId:
|
|
201
|
+
wechatAccountId: account.wechatAccountId,
|
|
158
202
|
chatroomId: params.to,
|
|
159
203
|
content: params.text,
|
|
160
|
-
type:
|
|
204
|
+
type: "text",
|
|
161
205
|
});
|
|
162
206
|
}
|
|
163
207
|
else {
|
|
164
|
-
// Send to friend (DM)
|
|
165
208
|
result = await client.sendFriendMessage({
|
|
166
|
-
wechatAccountId:
|
|
209
|
+
wechatAccountId: account.wechatAccountId,
|
|
167
210
|
friendWechatId: params.to,
|
|
168
211
|
content: params.text,
|
|
169
|
-
type:
|
|
212
|
+
type: "text",
|
|
170
213
|
});
|
|
171
214
|
}
|
|
172
215
|
return { messageId: result.messageId };
|
|
173
216
|
},
|
|
174
|
-
// Send media - also support both DM and group
|
|
175
217
|
sendMedia: async (params) => {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
218
|
+
const cfg = params.cfg;
|
|
219
|
+
const accountId = params.accountId;
|
|
220
|
+
// Resolve account to get credentials
|
|
221
|
+
const account = resolveAccount(cfg, accountId);
|
|
222
|
+
// Use client pool
|
|
223
|
+
const clientConfig = createClientConfig(account.baseUrl, account.apiKey, account.accountId || undefined, account.wechatAccountId);
|
|
224
|
+
const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
|
|
225
|
+
const isChatroom = params.to?.includes("@chatroom");
|
|
226
|
+
let filePath = params.mediaUrl || "";
|
|
184
227
|
let result;
|
|
185
228
|
if (isChatroom) {
|
|
186
229
|
result = await client.sendChatroomMessage({
|
|
187
|
-
wechatAccountId:
|
|
230
|
+
wechatAccountId: account.wechatAccountId,
|
|
188
231
|
chatroomId: params.to,
|
|
189
|
-
content:
|
|
190
|
-
type:
|
|
232
|
+
content: filePath,
|
|
233
|
+
type: "file",
|
|
191
234
|
});
|
|
192
235
|
}
|
|
193
236
|
else {
|
|
194
237
|
result = await client.sendFriendMessage({
|
|
195
|
-
wechatAccountId:
|
|
238
|
+
wechatAccountId: account.wechatAccountId,
|
|
196
239
|
friendWechatId: params.to,
|
|
197
|
-
content:
|
|
198
|
-
type:
|
|
240
|
+
content: filePath,
|
|
241
|
+
type: "file",
|
|
199
242
|
});
|
|
200
243
|
}
|
|
201
244
|
return { messageId: result.messageId };
|
|
202
245
|
},
|
|
203
246
|
},
|
|
204
|
-
// Additional outbound handlers can be added here for:
|
|
205
|
-
// - sendLink: Send link cards
|
|
206
|
-
// - sendLocation: Send location
|
|
207
|
-
// - sendContact: Send contact card
|
|
208
247
|
},
|
|
209
|
-
// Additional capabilities
|
|
210
|
-
// - Describe the channel's message types and features
|
|
211
248
|
capabilities: {
|
|
212
249
|
supportedMessageTypes: [
|
|
213
250
|
"text",
|
|
@@ -218,20 +255,24 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
|
|
|
218
255
|
"location",
|
|
219
256
|
"contact",
|
|
220
257
|
],
|
|
221
|
-
maxAttachmentSize: 25 * 1024 * 1024,
|
|
222
|
-
supportsMarkdown: false,
|
|
258
|
+
maxAttachmentSize: 25 * 1024 * 1024,
|
|
259
|
+
supportsMarkdown: false,
|
|
223
260
|
supportsHtml: false,
|
|
224
261
|
supportsEmoji: true,
|
|
225
|
-
supportsReactions: false,
|
|
226
|
-
supportsThreads: true,
|
|
227
|
-
supportsEditing: false,
|
|
228
|
-
supportsDeleting: false,
|
|
262
|
+
supportsReactions: false,
|
|
263
|
+
supportsThreads: true,
|
|
264
|
+
supportsEditing: false,
|
|
265
|
+
supportsDeleting: false,
|
|
229
266
|
},
|
|
230
267
|
});
|
|
231
268
|
/**
|
|
232
269
|
* Helper function to handle inbound webhook messages
|
|
233
270
|
* This should be called from your HTTP route handler
|
|
234
271
|
*
|
|
272
|
+
* Supports multi-account routing via AccountRegistry:
|
|
273
|
+
* - Looks up agentId from wechatAccountId
|
|
274
|
+
* - Dispatches to correct agent
|
|
275
|
+
*
|
|
235
276
|
* Integrates with Cache Manager for:
|
|
236
277
|
* - Local MD file caching
|
|
237
278
|
* - User profile storage
|
|
@@ -241,31 +282,48 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin({
|
|
|
241
282
|
*/
|
|
242
283
|
export async function handleInboundMessage(api, payload, cfg) {
|
|
243
284
|
const { event, accountId, wechatAccountId, message, friendRequest } = payload;
|
|
244
|
-
if (event ===
|
|
285
|
+
if (event === "message" && message) {
|
|
245
286
|
// Determine conversation type
|
|
246
287
|
const isChatroom = !!message.chatroomId;
|
|
247
288
|
const conversationId = isChatroom
|
|
248
289
|
? message.chatroomId
|
|
249
|
-
: message.fromUser || message.toUser ||
|
|
290
|
+
: message.fromUser || message.toUser || "";
|
|
291
|
+
// Determine the account ID for this message
|
|
292
|
+
// Priority: payload.accountId > wechatAccountId lookup > "default"
|
|
293
|
+
const resolvedAccountId = accountId || wechatAccountId || "default";
|
|
294
|
+
// Look up agentId for routing
|
|
295
|
+
let agentId = null;
|
|
296
|
+
if (cfg) {
|
|
297
|
+
const registry = getAccountRegistry();
|
|
298
|
+
// Try by wechatAccountId first
|
|
299
|
+
agentId = registry.getAgentId(wechatAccountId || "");
|
|
300
|
+
// Fallback to accountId
|
|
301
|
+
if (!agentId && accountId) {
|
|
302
|
+
const regAccount = registry.getByAccountId(accountId);
|
|
303
|
+
agentId = regAccount?.agentId || null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
250
306
|
// Convert to cache format and store locally
|
|
251
|
-
if (
|
|
307
|
+
if (cfg) {
|
|
252
308
|
try {
|
|
253
|
-
|
|
309
|
+
const cache = getCacheManager(cfg);
|
|
310
|
+
const wechatMessage = {
|
|
254
311
|
messageId: message.messageId,
|
|
255
312
|
msgSvrId: message.msgSvrId,
|
|
256
|
-
accountId:
|
|
257
|
-
conversationType: isChatroom ?
|
|
313
|
+
accountId: resolvedAccountId,
|
|
314
|
+
conversationType: isChatroom ? "chatroom" : "friend",
|
|
258
315
|
conversationId,
|
|
259
|
-
senderId: message.fromUser ||
|
|
316
|
+
senderId: message.fromUser || "",
|
|
260
317
|
content: message.content,
|
|
261
318
|
messageType: message.type || 1,
|
|
262
319
|
timestamp: message.timestamp || Date.now(),
|
|
263
320
|
isSelf: message.isSelf || false,
|
|
264
|
-
direction: message.isSelf ?
|
|
265
|
-
}
|
|
321
|
+
direction: message.isSelf ? "outbound" : "inbound",
|
|
322
|
+
};
|
|
323
|
+
await cache.onMessage(wechatMessage);
|
|
266
324
|
}
|
|
267
325
|
catch (err) {
|
|
268
|
-
console.error(
|
|
326
|
+
console.error("[CelPhoneWeChat] Cache write failed:", err);
|
|
269
327
|
}
|
|
270
328
|
}
|
|
271
329
|
// Convert WorkPhone message format to OpenClaw format
|
|
@@ -273,32 +331,34 @@ export async function handleInboundMessage(api, payload, cfg) {
|
|
|
273
331
|
id: message.messageId,
|
|
274
332
|
rawId: message.msgSvrId || message.messageId,
|
|
275
333
|
conversation: {
|
|
276
|
-
type: isChatroom ?
|
|
334
|
+
type: isChatroom ? "group" : "dm",
|
|
277
335
|
id: conversationId,
|
|
278
336
|
chatroomId: isChatroom ? conversationId : undefined,
|
|
279
337
|
},
|
|
280
338
|
sender: {
|
|
281
|
-
id: message.fromUser ||
|
|
339
|
+
id: message.fromUser || "",
|
|
282
340
|
platformId: message.wechatId,
|
|
283
341
|
},
|
|
284
342
|
content: {
|
|
285
|
-
type: message.type === 1 ?
|
|
343
|
+
type: message.type === 1 ? "text" : "media",
|
|
286
344
|
text: message.content,
|
|
287
345
|
},
|
|
288
346
|
timestamp: new Date(message.timestamp || Date.now()),
|
|
289
347
|
isSelf: message.isSelf || false,
|
|
348
|
+
agentId, // Include agentId for routing
|
|
349
|
+
accountId: resolvedAccountId,
|
|
290
350
|
};
|
|
291
351
|
// Dispatch to OpenClaw
|
|
292
352
|
await api.inbound.dispatchMessage(openclawMessage);
|
|
293
353
|
}
|
|
294
|
-
else if (event ===
|
|
354
|
+
else if (event === "friend_request" && friendRequest) {
|
|
295
355
|
// Handle friend request
|
|
296
356
|
await api.inbound.dispatchFriendRequest({
|
|
297
357
|
id: friendRequest.v1,
|
|
298
358
|
platformId: friendRequest.fromUser,
|
|
299
359
|
scene: friendRequest.scene,
|
|
300
360
|
ticket: friendRequest.ticket,
|
|
301
|
-
accountId,
|
|
361
|
+
accountId: accountId || wechatAccountId,
|
|
302
362
|
wechatAccountId,
|
|
303
363
|
});
|
|
304
364
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Pool - Per-account client management
|
|
3
|
+
*
|
|
4
|
+
* Manages WorkPhone API clients per account to avoid creating
|
|
5
|
+
* new clients for each message and ensure credential isolation.
|
|
6
|
+
*/
|
|
7
|
+
import { WorkPhoneWeChatClient } from "./client.js";
|
|
8
|
+
/**
|
|
9
|
+
* Client pool for WorkPhone WeChat clients
|
|
10
|
+
*/
|
|
11
|
+
class ClientPool {
|
|
12
|
+
clients = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Get or create a client for the given account
|
|
15
|
+
*/
|
|
16
|
+
getClient(accountId, config) {
|
|
17
|
+
if (this.clients.has(accountId)) {
|
|
18
|
+
return this.clients.get(accountId);
|
|
19
|
+
}
|
|
20
|
+
const client = new WorkPhoneWeChatClient(config);
|
|
21
|
+
this.clients.set(accountId, client);
|
|
22
|
+
return client;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check if client exists
|
|
26
|
+
*/
|
|
27
|
+
hasClient(accountId) {
|
|
28
|
+
return this.clients.has(accountId);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Remove a client by accountId
|
|
32
|
+
*/
|
|
33
|
+
removeClient(accountId) {
|
|
34
|
+
return this.clients.delete(accountId);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Clear all clients
|
|
38
|
+
*/
|
|
39
|
+
clear() {
|
|
40
|
+
this.clients.clear();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get all account IDs with active clients
|
|
44
|
+
*/
|
|
45
|
+
getActiveAccountIds() {
|
|
46
|
+
return Array.from(this.clients.keys());
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get pool size
|
|
50
|
+
*/
|
|
51
|
+
get size() {
|
|
52
|
+
return this.clients.size;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Singleton instance
|
|
56
|
+
let clientPool = null;
|
|
57
|
+
/**
|
|
58
|
+
* Get or create the client pool singleton
|
|
59
|
+
*/
|
|
60
|
+
export function getClientPool() {
|
|
61
|
+
if (!clientPool) {
|
|
62
|
+
clientPool = new ClientPool();
|
|
63
|
+
}
|
|
64
|
+
return clientPool;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get a client for a specific account
|
|
68
|
+
*/
|
|
69
|
+
export function getWorkPhoneClient(accountId, config) {
|
|
70
|
+
return getClientPool().getClient(accountId, config);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create a client config from resolved account
|
|
74
|
+
*/
|
|
75
|
+
export function createClientConfig(baseUrl, apiKey, accountId, wechatAccountId) {
|
|
76
|
+
return {
|
|
77
|
+
baseUrl,
|
|
78
|
+
apiKey,
|
|
79
|
+
accountId,
|
|
80
|
+
wechatAccountId,
|
|
81
|
+
};
|
|
82
|
+
}
|
package/index.ts
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* WorkPhone WeChat Plugin - enables OpenClaw to send/receive WeChat messages
|
|
5
5
|
* through the WorkPhone API platform.
|
|
6
|
+
*
|
|
7
|
+
* Multi-account support:
|
|
8
|
+
* - Each account in accounts[] can bind to a different agent
|
|
9
|
+
* - Webhook routing uses AccountRegistry to find agentId
|
|
10
|
+
* - cfg is passed to handleInboundMessage for cache isolation
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
@@ -42,9 +47,12 @@ export default defineChannelPluginEntry({
|
|
|
42
47
|
auth: "plugin", // Plugin-managed auth - verify signatures yourself
|
|
43
48
|
handler: async (req, res) => {
|
|
44
49
|
try {
|
|
50
|
+
// Get config from request (provided by OpenClaw framework)
|
|
51
|
+
const cfg = (req as any).cfg;
|
|
52
|
+
|
|
45
53
|
// Parse the webhook payload
|
|
46
54
|
// The exact format depends on WorkPhone's webhook configuration
|
|
47
|
-
const payload = req.body;
|
|
55
|
+
const payload = (req as any).body;
|
|
48
56
|
|
|
49
57
|
// Verify the request is from WorkPhone
|
|
50
58
|
// Add signature verification here if needed
|
|
@@ -55,8 +63,8 @@ export default defineChannelPluginEntry({
|
|
|
55
63
|
return true;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
|
-
// Handle the inbound message
|
|
59
|
-
await handleInboundMessage(api, payload);
|
|
66
|
+
// Handle the inbound message with cfg for multi-account routing
|
|
67
|
+
await handleInboundMessage(api, payload, cfg);
|
|
60
68
|
|
|
61
69
|
res.statusCode = 200;
|
|
62
70
|
res.end("ok");
|
|
@@ -71,17 +79,19 @@ export default defineChannelPluginEntry({
|
|
|
71
79
|
});
|
|
72
80
|
|
|
73
81
|
// Register gateway method for outbound media handling
|
|
74
|
-
api.
|
|
75
|
-
method: "POST",
|
|
82
|
+
api.registerHttpRoute({
|
|
76
83
|
path: "/celphone-wechat/media",
|
|
84
|
+
auth: "plugin",
|
|
77
85
|
handler: async (req, res) => {
|
|
78
|
-
const
|
|
86
|
+
const payload = (req as any).body;
|
|
87
|
+
const { messageId, mediaUrl, mediaType } = payload || {};
|
|
79
88
|
|
|
80
89
|
// Handle media upload/send through WorkPhone
|
|
81
90
|
// This is called when the bot needs to send media files
|
|
82
91
|
|
|
92
|
+
res.setHeader("Content-Type", "application/json");
|
|
83
93
|
res.statusCode = 200;
|
|
84
|
-
res.
|
|
94
|
+
res.end(JSON.stringify({ success: true, messageId }));
|
|
85
95
|
return true;
|
|
86
96
|
},
|
|
87
97
|
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gloablehive/celphone-wechat-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OpenClaw channel plugin for workphone-wechat API - enables sending/receiving WeChat messages through workphone",
|
|
5
|
+
"description": "OpenClaw channel plugin for workphone-wechat API - enables sending/receiving WeChat messages through workphone. Supports multi-account with per-account agent binding.",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "npx tsx test-cache.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@gloablehive/wechat-cache": "^1.0.
|
|
27
|
+
"@gloablehive/wechat-cache": "^1.0.2",
|
|
28
28
|
"openclaw": ">=1.0.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
package/src/channel.ts
CHANGED
|
@@ -9,14 +9,10 @@
|
|
|
9
9
|
* The agent communicates with ALL friends and groups under that WeChat account,
|
|
10
10
|
* not just one DM context. This is "human mode" vs "bot mode".
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
16
|
-
* - 4-layer compression
|
|
17
|
-
* - AI summary extraction
|
|
18
|
-
* - SAAS connectivity + offline fallback
|
|
19
|
-
* - Cloud sync
|
|
12
|
+
* Multi-account support:
|
|
13
|
+
* - Config supports accounts array with per-account credentials
|
|
14
|
+
* - Each account can be bound to a different agent
|
|
15
|
+
* - Security policies are per-account isolated
|
|
20
16
|
*/
|
|
21
17
|
|
|
22
18
|
import {
|
|
@@ -24,18 +20,61 @@ import {
|
|
|
24
20
|
createChannelPluginBase,
|
|
25
21
|
} from "openclaw/plugin-sdk/core";
|
|
26
22
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
27
|
-
import {
|
|
23
|
+
import type { WebhookPayload } from "./client.js";
|
|
28
24
|
|
|
29
25
|
// Import cache modules from shared package
|
|
30
26
|
import {
|
|
31
27
|
createCacheManager,
|
|
32
28
|
CacheManager,
|
|
33
29
|
WeChatAccount,
|
|
30
|
+
WeChatMessage,
|
|
31
|
+
AccountRegistry,
|
|
32
|
+
createAccountRegistry,
|
|
33
|
+
type RegisteredAccount,
|
|
34
34
|
} from "@gloablehive/wechat-cache";
|
|
35
35
|
|
|
36
|
+
// Import client pool
|
|
37
|
+
import { getWorkPhoneClient, createClientConfig, getClientPool } from "./client-pool.js";
|
|
38
|
+
|
|
36
39
|
// Cache manager instance (lazy initialized)
|
|
37
40
|
let cacheManager: CacheManager | null = null;
|
|
38
41
|
|
|
42
|
+
// Account registry for routing
|
|
43
|
+
let accountRegistry: AccountRegistry | null = null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get or create account registry
|
|
47
|
+
*/
|
|
48
|
+
function getAccountRegistry(): AccountRegistry {
|
|
49
|
+
if (!accountRegistry) {
|
|
50
|
+
accountRegistry = createAccountRegistry();
|
|
51
|
+
}
|
|
52
|
+
return accountRegistry;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Initialize account registry from config
|
|
57
|
+
*/
|
|
58
|
+
function initializeRegistry(cfg: OpenClawConfig): void {
|
|
59
|
+
const registry = getAccountRegistry();
|
|
60
|
+
registry.clear();
|
|
61
|
+
|
|
62
|
+
const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
|
|
63
|
+
const accounts = (section?.accounts || []) as WeChatAccount[];
|
|
64
|
+
|
|
65
|
+
// Register each account
|
|
66
|
+
for (const account of accounts) {
|
|
67
|
+
if (account.enabled !== false) {
|
|
68
|
+
const registeredAccount: RegisteredAccount = {
|
|
69
|
+
...account,
|
|
70
|
+
channelType: "celphone-wechat",
|
|
71
|
+
registeredAt: Date.now(),
|
|
72
|
+
};
|
|
73
|
+
registry.register(registeredAccount);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
39
78
|
/**
|
|
40
79
|
* Get or create cache manager
|
|
41
80
|
*/
|
|
@@ -48,16 +87,19 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
|
|
|
48
87
|
// If no accounts configured, create default from main config
|
|
49
88
|
if (accounts.length === 0 && section?.wechatAccountId) {
|
|
50
89
|
accounts.push({
|
|
51
|
-
accountId: section.accountId ||
|
|
90
|
+
accountId: section.accountId || "default",
|
|
52
91
|
wechatAccountId: section.wechatAccountId,
|
|
53
92
|
wechatId: section.wechatId || section.wechatAccountId,
|
|
54
|
-
nickName: section.nickName ||
|
|
93
|
+
nickName: section.nickName || "WeChat User",
|
|
55
94
|
enabled: true,
|
|
56
95
|
});
|
|
57
96
|
}
|
|
58
97
|
|
|
98
|
+
// Initialize registry with accounts
|
|
99
|
+
initializeRegistry(cfg);
|
|
100
|
+
|
|
59
101
|
const basePath = (globalThis as any)?.process?.env?.OPENCLAW_CACHE_PATH
|
|
60
|
-
||
|
|
102
|
+
|| "~/.openclaw/channels/celphone-wechat";
|
|
61
103
|
|
|
62
104
|
cacheManager = createCacheManager({
|
|
63
105
|
basePath,
|
|
@@ -69,14 +111,14 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
|
|
|
69
111
|
} : undefined,
|
|
70
112
|
syncConfig: section?.sync ? {
|
|
71
113
|
databaseUrl: section.sync.databaseUrl,
|
|
72
|
-
syncMode: section.sync.syncMode ||
|
|
114
|
+
syncMode: section.sync.syncMode || "interval",
|
|
73
115
|
syncIntervalMs: section.sync.syncIntervalMs || 5 * 60 * 1000,
|
|
74
116
|
} : undefined,
|
|
75
117
|
});
|
|
76
118
|
|
|
77
|
-
// Initialize cache manager
|
|
119
|
+
// Initialize cache manager - must await for ready state
|
|
78
120
|
cacheManager.init().catch(err => {
|
|
79
|
-
console.error(
|
|
121
|
+
console.error("[CelPhoneWeChat] Cache manager init failed:", err);
|
|
80
122
|
});
|
|
81
123
|
|
|
82
124
|
return cacheManager;
|
|
@@ -84,26 +126,70 @@ function getCacheManager(cfg: OpenClawConfig): CacheManager {
|
|
|
84
126
|
|
|
85
127
|
export interface CelPhoneWeChatResolvedAccount {
|
|
86
128
|
accountId: string | null;
|
|
129
|
+
agentId: string | null;
|
|
87
130
|
apiKey: string;
|
|
88
131
|
baseUrl: string;
|
|
89
|
-
wechatAccountId: string;
|
|
90
|
-
wechatId: string;
|
|
91
|
-
nickName: string;
|
|
132
|
+
wechatAccountId: string;
|
|
133
|
+
wechatId: string;
|
|
134
|
+
nickName: string;
|
|
92
135
|
allowFrom: string[];
|
|
93
136
|
dmPolicy: string | undefined;
|
|
94
137
|
}
|
|
95
138
|
|
|
139
|
+
/**
|
|
140
|
+
* List all account IDs from config
|
|
141
|
+
*/
|
|
142
|
+
function listAccountIds(cfg: OpenClawConfig): string[] {
|
|
143
|
+
const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
|
|
144
|
+
const accounts = (section?.accounts || []) as WeChatAccount[];
|
|
145
|
+
|
|
146
|
+
if (accounts.length > 0) {
|
|
147
|
+
return accounts.map(a => a.accountId).filter(Boolean);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Backward compatibility: single account
|
|
151
|
+
if (section?.wechatAccountId) {
|
|
152
|
+
return [section.accountId || "default"];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
|
|
96
158
|
/**
|
|
97
159
|
* Resolve account from OpenClaw config
|
|
98
160
|
*
|
|
99
|
-
*
|
|
100
|
-
* a real person with all their friends and groups, not a bot.
|
|
161
|
+
* Supports both multi-account (accounts array) and legacy (single) formats.
|
|
101
162
|
*/
|
|
102
163
|
function resolveAccount(
|
|
103
164
|
cfg: OpenClawConfig,
|
|
104
165
|
accountId?: string | null
|
|
105
166
|
): CelPhoneWeChatResolvedAccount {
|
|
106
167
|
const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
|
|
168
|
+
const accounts = (section?.accounts || []) as WeChatAccount[];
|
|
169
|
+
|
|
170
|
+
// Multi-account mode: look up from accounts array
|
|
171
|
+
if (accounts.length > 0) {
|
|
172
|
+
const targetAccountId = accountId || accounts[0]?.accountId;
|
|
173
|
+
const account = accounts.find(a => a.accountId === targetAccountId);
|
|
174
|
+
|
|
175
|
+
if (!account) {
|
|
176
|
+
throw new Error(`celphone-wechat: account not found - ${targetAccountId}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
accountId: account.accountId,
|
|
181
|
+
agentId: account.agentId || null,
|
|
182
|
+
apiKey: account.apiKey || section?.apiKey || "",
|
|
183
|
+
baseUrl: account.baseUrl || section?.baseUrl || "https://api.workphone.example.com",
|
|
184
|
+
wechatAccountId: account.wechatAccountId,
|
|
185
|
+
wechatId: account.wechatId || account.wechatAccountId,
|
|
186
|
+
nickName: account.nickName || "WeChat User",
|
|
187
|
+
allowFrom: account.allowFrom ?? [],
|
|
188
|
+
dmPolicy: account.dmPolicy,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Legacy single-account mode
|
|
107
193
|
const apiKey = section?.apiKey;
|
|
108
194
|
const baseUrl = section?.baseUrl || "https://api.workphone.example.com";
|
|
109
195
|
const wechatAccountId = section?.wechatAccountId;
|
|
@@ -118,11 +204,12 @@ function resolveAccount(
|
|
|
118
204
|
|
|
119
205
|
return {
|
|
120
206
|
accountId: accountId ?? null,
|
|
207
|
+
agentId: null,
|
|
121
208
|
apiKey,
|
|
122
209
|
baseUrl,
|
|
123
210
|
wechatAccountId,
|
|
124
|
-
wechatId: section?.wechatId || wechatAccountId,
|
|
125
|
-
nickName: section?.nickName || "WeChat User",
|
|
211
|
+
wechatId: section?.wechatId || wechatAccountId,
|
|
212
|
+
nickName: section?.nickName || "WeChat User",
|
|
126
213
|
allowFrom: section?.allowFrom ?? [],
|
|
127
214
|
dmPolicy: section?.dmSecurity,
|
|
128
215
|
};
|
|
@@ -135,126 +222,110 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
|
|
|
135
222
|
base: createChannelPluginBase({
|
|
136
223
|
id: "celphone-wechat",
|
|
137
224
|
setup: {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const section = (cfg.channels as Record<string, any>)?.["celphone-wechat"];
|
|
141
|
-
const hasApiKey = Boolean(section?.apiKey);
|
|
142
|
-
return {
|
|
143
|
-
enabled: hasApiKey,
|
|
144
|
-
configured: hasApiKey,
|
|
145
|
-
tokenStatus: hasApiKey ? "available" : "missing",
|
|
146
|
-
};
|
|
225
|
+
resolveAccountId: (params: any) => {
|
|
226
|
+
return resolveAccount(params.cfg, params.accountId)?.accountId || "";
|
|
147
227
|
},
|
|
228
|
+
applyAccountConfig: (params) => params.cfg,
|
|
148
229
|
},
|
|
149
|
-
}),
|
|
230
|
+
}) as any,
|
|
150
231
|
|
|
151
|
-
// DM security: who can message the bot
|
|
152
232
|
security: {
|
|
153
233
|
dm: {
|
|
154
234
|
channelKey: "celphone-wechat",
|
|
155
235
|
resolvePolicy: (account) => account.dmPolicy,
|
|
156
236
|
resolveAllowFrom: (account) => account.allowFrom,
|
|
157
|
-
defaultPolicy: "allowlist",
|
|
237
|
+
defaultPolicy: "allowlist",
|
|
158
238
|
},
|
|
159
239
|
},
|
|
160
240
|
|
|
161
|
-
// Pairing: not currently supported for this channel
|
|
162
|
-
// WorkPhone WeChat doesn't have a standard pairing flow
|
|
163
|
-
// pairing: { ... },
|
|
164
|
-
|
|
165
|
-
// Threading: how replies are delivered
|
|
166
|
-
// For WeChat, replies go back to the same chat (friend or chatroom)
|
|
167
241
|
threading: {
|
|
168
|
-
topLevelReplyToMode: "reply",
|
|
242
|
+
topLevelReplyToMode: "reply",
|
|
169
243
|
},
|
|
170
244
|
|
|
171
|
-
// Outbound: send messages to WeChat via WorkPhone API
|
|
172
|
-
//
|
|
173
|
-
// HUMAN ACCOUNT MODEL IMPORTANT:
|
|
174
|
-
// This channel connects to a real person's WeChat account.
|
|
175
|
-
// The agent needs to communicate with ALL their friends and groups,
|
|
176
|
-
// not just one conversation. We distinguish by conversation type:
|
|
177
|
-
// - DM (direct): friendWechatId in params.to
|
|
178
|
-
// - Group chat: chatroomId in params.to (detect by format or metadata)
|
|
179
245
|
outbound: {
|
|
246
|
+
channel: "celphone-wechat",
|
|
180
247
|
attachedResults: {
|
|
181
|
-
|
|
182
|
-
sendText: async (params) => {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
248
|
+
channel: "celphone-wechat",
|
|
249
|
+
sendText: async (params: any) => {
|
|
250
|
+
const cfg = params.cfg;
|
|
251
|
+
const accountId = params.accountId;
|
|
252
|
+
|
|
253
|
+
// Resolve account to get credentials
|
|
254
|
+
const account = resolveAccount(cfg, accountId);
|
|
255
|
+
|
|
256
|
+
// Use client pool for connection reuse
|
|
257
|
+
const clientConfig = createClientConfig(
|
|
258
|
+
account.baseUrl,
|
|
259
|
+
account.apiKey,
|
|
260
|
+
account.accountId || undefined,
|
|
261
|
+
account.wechatAccountId
|
|
262
|
+
);
|
|
263
|
+
const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
|
|
264
|
+
|
|
265
|
+
const isChatroom = params.to?.includes("@chatroom");
|
|
194
266
|
|
|
195
267
|
let result;
|
|
196
268
|
if (isChatroom) {
|
|
197
|
-
// Send to chatroom (group)
|
|
198
269
|
result = await client.sendChatroomMessage({
|
|
199
|
-
wechatAccountId:
|
|
270
|
+
wechatAccountId: account.wechatAccountId,
|
|
200
271
|
chatroomId: params.to,
|
|
201
272
|
content: params.text,
|
|
202
|
-
type:
|
|
273
|
+
type: "text",
|
|
203
274
|
});
|
|
204
275
|
} else {
|
|
205
|
-
// Send to friend (DM)
|
|
206
276
|
result = await client.sendFriendMessage({
|
|
207
|
-
wechatAccountId:
|
|
277
|
+
wechatAccountId: account.wechatAccountId,
|
|
208
278
|
friendWechatId: params.to,
|
|
209
279
|
content: params.text,
|
|
210
|
-
type:
|
|
280
|
+
type: "text",
|
|
211
281
|
});
|
|
212
282
|
}
|
|
213
283
|
|
|
214
284
|
return { messageId: result.messageId };
|
|
215
285
|
},
|
|
216
286
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
287
|
+
sendMedia: async (params: any) => {
|
|
288
|
+
const cfg = params.cfg;
|
|
289
|
+
const accountId = params.accountId;
|
|
290
|
+
|
|
291
|
+
// Resolve account to get credentials
|
|
292
|
+
const account = resolveAccount(cfg, accountId);
|
|
293
|
+
|
|
294
|
+
// Use client pool
|
|
295
|
+
const clientConfig = createClientConfig(
|
|
296
|
+
account.baseUrl,
|
|
297
|
+
account.apiKey,
|
|
298
|
+
account.accountId || undefined,
|
|
299
|
+
account.wechatAccountId
|
|
300
|
+
);
|
|
301
|
+
const client = getWorkPhoneClient(account.accountId || "default", clientConfig);
|
|
302
|
+
|
|
303
|
+
const isChatroom = params.to?.includes("@chatroom");
|
|
225
304
|
|
|
226
|
-
|
|
227
|
-
(params.to && params.to.includes('@chatroom'));
|
|
305
|
+
let filePath = params.mediaUrl || "";
|
|
228
306
|
|
|
229
307
|
let result;
|
|
230
308
|
if (isChatroom) {
|
|
231
309
|
result = await client.sendChatroomMessage({
|
|
232
|
-
wechatAccountId:
|
|
310
|
+
wechatAccountId: account.wechatAccountId,
|
|
233
311
|
chatroomId: params.to,
|
|
234
|
-
content:
|
|
235
|
-
type:
|
|
312
|
+
content: filePath,
|
|
313
|
+
type: "file",
|
|
236
314
|
});
|
|
237
315
|
} else {
|
|
238
316
|
result = await client.sendFriendMessage({
|
|
239
|
-
wechatAccountId:
|
|
317
|
+
wechatAccountId: account.wechatAccountId,
|
|
240
318
|
friendWechatId: params.to,
|
|
241
|
-
content:
|
|
242
|
-
type:
|
|
319
|
+
content: filePath,
|
|
320
|
+
type: "file",
|
|
243
321
|
});
|
|
244
322
|
}
|
|
245
323
|
|
|
246
324
|
return { messageId: result.messageId };
|
|
247
325
|
},
|
|
248
326
|
},
|
|
327
|
+
} as any,
|
|
249
328
|
|
|
250
|
-
// Additional outbound handlers can be added here for:
|
|
251
|
-
// - sendLink: Send link cards
|
|
252
|
-
// - sendLocation: Send location
|
|
253
|
-
// - sendContact: Send contact card
|
|
254
|
-
},
|
|
255
|
-
|
|
256
|
-
// Additional capabilities
|
|
257
|
-
// - Describe the channel's message types and features
|
|
258
329
|
capabilities: {
|
|
259
330
|
supportedMessageTypes: [
|
|
260
331
|
"text",
|
|
@@ -265,21 +336,25 @@ export const celPhoneWeChatPlugin = createChatChannelPlugin<CelPhoneWeChatResolv
|
|
|
265
336
|
"location",
|
|
266
337
|
"contact",
|
|
267
338
|
],
|
|
268
|
-
maxAttachmentSize: 25 * 1024 * 1024,
|
|
269
|
-
supportsMarkdown: false,
|
|
339
|
+
maxAttachmentSize: 25 * 1024 * 1024,
|
|
340
|
+
supportsMarkdown: false,
|
|
270
341
|
supportsHtml: false,
|
|
271
342
|
supportsEmoji: true,
|
|
272
|
-
supportsReactions: false,
|
|
273
|
-
supportsThreads: true,
|
|
274
|
-
supportsEditing: false,
|
|
275
|
-
supportsDeleting: false,
|
|
343
|
+
supportsReactions: false,
|
|
344
|
+
supportsThreads: true,
|
|
345
|
+
supportsEditing: false,
|
|
346
|
+
supportsDeleting: false,
|
|
276
347
|
},
|
|
277
|
-
});
|
|
348
|
+
} as any);
|
|
278
349
|
|
|
279
350
|
/**
|
|
280
351
|
* Helper function to handle inbound webhook messages
|
|
281
352
|
* This should be called from your HTTP route handler
|
|
282
353
|
*
|
|
354
|
+
* Supports multi-account routing via AccountRegistry:
|
|
355
|
+
* - Looks up agentId from wechatAccountId
|
|
356
|
+
* - Dispatches to correct agent
|
|
357
|
+
*
|
|
283
358
|
* Integrates with Cache Manager for:
|
|
284
359
|
* - Local MD file caching
|
|
285
360
|
* - User profile storage
|
|
@@ -294,31 +369,50 @@ export async function handleInboundMessage(
|
|
|
294
369
|
): Promise<void> {
|
|
295
370
|
const { event, accountId, wechatAccountId, message, friendRequest } = payload;
|
|
296
371
|
|
|
297
|
-
if (event ===
|
|
372
|
+
if (event === "message" && message) {
|
|
298
373
|
// Determine conversation type
|
|
299
374
|
const isChatroom = !!(message as any).chatroomId;
|
|
300
375
|
const conversationId = isChatroom
|
|
301
376
|
? (message as any).chatroomId
|
|
302
|
-
: message.fromUser || message.toUser ||
|
|
377
|
+
: message.fromUser || message.toUser || "";
|
|
378
|
+
|
|
379
|
+
// Determine the account ID for this message
|
|
380
|
+
// Priority: payload.accountId > wechatAccountId lookup > "default"
|
|
381
|
+
const resolvedAccountId = accountId || wechatAccountId || "default";
|
|
382
|
+
|
|
383
|
+
// Look up agentId for routing
|
|
384
|
+
let agentId: string | null = null;
|
|
385
|
+
if (cfg) {
|
|
386
|
+
const registry = getAccountRegistry();
|
|
387
|
+
// Try by wechatAccountId first
|
|
388
|
+
agentId = registry.getAgentId(wechatAccountId || "");
|
|
389
|
+
// Fallback to accountId
|
|
390
|
+
if (!agentId && accountId) {
|
|
391
|
+
const regAccount = registry.getByAccountId(accountId);
|
|
392
|
+
agentId = regAccount?.agentId || null;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
303
395
|
|
|
304
396
|
// Convert to cache format and store locally
|
|
305
|
-
if (
|
|
397
|
+
if (cfg) {
|
|
306
398
|
try {
|
|
307
|
-
|
|
399
|
+
const cache = getCacheManager(cfg);
|
|
400
|
+
const wechatMessage: WeChatMessage = {
|
|
308
401
|
messageId: message.messageId,
|
|
309
402
|
msgSvrId: message.msgSvrId,
|
|
310
|
-
accountId:
|
|
311
|
-
conversationType: isChatroom ?
|
|
403
|
+
accountId: resolvedAccountId,
|
|
404
|
+
conversationType: isChatroom ? "chatroom" : "friend",
|
|
312
405
|
conversationId,
|
|
313
|
-
senderId: message.fromUser ||
|
|
406
|
+
senderId: message.fromUser || "",
|
|
314
407
|
content: message.content,
|
|
315
408
|
messageType: message.type || 1,
|
|
316
409
|
timestamp: message.timestamp || Date.now(),
|
|
317
410
|
isSelf: message.isSelf || false,
|
|
318
|
-
direction: message.isSelf ?
|
|
319
|
-
}
|
|
411
|
+
direction: message.isSelf ? "outbound" : "inbound",
|
|
412
|
+
};
|
|
413
|
+
await cache.onMessage(wechatMessage);
|
|
320
414
|
} catch (err) {
|
|
321
|
-
console.error(
|
|
415
|
+
console.error("[CelPhoneWeChat] Cache write failed:", err);
|
|
322
416
|
}
|
|
323
417
|
}
|
|
324
418
|
|
|
@@ -327,32 +421,34 @@ export async function handleInboundMessage(
|
|
|
327
421
|
id: message.messageId,
|
|
328
422
|
rawId: message.msgSvrId || message.messageId,
|
|
329
423
|
conversation: {
|
|
330
|
-
type: isChatroom ?
|
|
424
|
+
type: isChatroom ? "group" as const : "dm" as const,
|
|
331
425
|
id: conversationId,
|
|
332
426
|
chatroomId: isChatroom ? conversationId : undefined,
|
|
333
427
|
},
|
|
334
428
|
sender: {
|
|
335
|
-
id: message.fromUser ||
|
|
429
|
+
id: message.fromUser || "",
|
|
336
430
|
platformId: message.wechatId,
|
|
337
431
|
},
|
|
338
432
|
content: {
|
|
339
|
-
type: message.type === 1 ?
|
|
433
|
+
type: message.type === 1 ? "text" as const : "media" as const,
|
|
340
434
|
text: message.content,
|
|
341
435
|
},
|
|
342
436
|
timestamp: new Date(message.timestamp || Date.now()),
|
|
343
437
|
isSelf: message.isSelf || false,
|
|
438
|
+
agentId, // Include agentId for routing
|
|
439
|
+
accountId: resolvedAccountId,
|
|
344
440
|
};
|
|
345
441
|
|
|
346
442
|
// Dispatch to OpenClaw
|
|
347
443
|
await api.inbound.dispatchMessage(openclawMessage);
|
|
348
|
-
} else if (event ===
|
|
444
|
+
} else if (event === "friend_request" && friendRequest) {
|
|
349
445
|
// Handle friend request
|
|
350
446
|
await api.inbound.dispatchFriendRequest({
|
|
351
447
|
id: friendRequest.v1,
|
|
352
448
|
platformId: friendRequest.fromUser,
|
|
353
449
|
scene: friendRequest.scene,
|
|
354
450
|
ticket: friendRequest.ticket,
|
|
355
|
-
accountId,
|
|
451
|
+
accountId: accountId || wechatAccountId,
|
|
356
452
|
wechatAccountId,
|
|
357
453
|
});
|
|
358
454
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Pool - Per-account client management
|
|
3
|
+
*
|
|
4
|
+
* Manages WorkPhone API clients per account to avoid creating
|
|
5
|
+
* new clients for each message and ensure credential isolation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { WorkPhoneWeChatClient, type WorkPhoneConfig } from "./client.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Client pool for WorkPhone WeChat clients
|
|
12
|
+
*/
|
|
13
|
+
class ClientPool {
|
|
14
|
+
private clients: Map<string, WorkPhoneWeChatClient> = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get or create a client for the given account
|
|
18
|
+
*/
|
|
19
|
+
getClient(accountId: string, config: WorkPhoneConfig): WorkPhoneWeChatClient {
|
|
20
|
+
if (this.clients.has(accountId)) {
|
|
21
|
+
return this.clients.get(accountId)!;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const client = new WorkPhoneWeChatClient(config);
|
|
25
|
+
this.clients.set(accountId, client);
|
|
26
|
+
return client;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if client exists
|
|
31
|
+
*/
|
|
32
|
+
hasClient(accountId: string): boolean {
|
|
33
|
+
return this.clients.has(accountId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Remove a client by accountId
|
|
38
|
+
*/
|
|
39
|
+
removeClient(accountId: string): boolean {
|
|
40
|
+
return this.clients.delete(accountId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Clear all clients
|
|
45
|
+
*/
|
|
46
|
+
clear(): void {
|
|
47
|
+
this.clients.clear();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get all account IDs with active clients
|
|
52
|
+
*/
|
|
53
|
+
getActiveAccountIds(): string[] {
|
|
54
|
+
return Array.from(this.clients.keys());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get pool size
|
|
59
|
+
*/
|
|
60
|
+
get size(): number {
|
|
61
|
+
return this.clients.size;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Singleton instance
|
|
66
|
+
let clientPool: ClientPool | null = null;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get or create the client pool singleton
|
|
70
|
+
*/
|
|
71
|
+
export function getClientPool(): ClientPool {
|
|
72
|
+
if (!clientPool) {
|
|
73
|
+
clientPool = new ClientPool();
|
|
74
|
+
}
|
|
75
|
+
return clientPool;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get a client for a specific account
|
|
80
|
+
*/
|
|
81
|
+
export function getWorkPhoneClient(
|
|
82
|
+
accountId: string,
|
|
83
|
+
config: WorkPhoneConfig
|
|
84
|
+
): WorkPhoneWeChatClient {
|
|
85
|
+
return getClientPool().getClient(accountId, config);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create a client config from resolved account
|
|
90
|
+
*/
|
|
91
|
+
export function createClientConfig(
|
|
92
|
+
baseUrl: string,
|
|
93
|
+
apiKey: string,
|
|
94
|
+
accountId: string | undefined,
|
|
95
|
+
wechatAccountId: string | undefined
|
|
96
|
+
): WorkPhoneConfig {
|
|
97
|
+
return {
|
|
98
|
+
baseUrl,
|
|
99
|
+
apiKey,
|
|
100
|
+
accountId,
|
|
101
|
+
wechatAccountId,
|
|
102
|
+
};
|
|
103
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -107,7 +107,7 @@ export class WorkPhoneWeChatClient {
|
|
|
107
107
|
throw new Error(`WorkPhone API error: ${response.status} - ${error}`);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
return response.json()
|
|
110
|
+
return response.json() as Promise<T>;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
// ========== WeChat Account Operations ==========
|