@junjiezhang/openclaw-wecom-plugin 1.0.0 → 1.0.2

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 CHANGED
@@ -5,7 +5,7 @@ const plugin = {
5
5
  id: "wecom",
6
6
  name: "WeCom",
7
7
  description: "WeCom (企业微信) channel plugin for OpenClaw",
8
- version: "1.0.0",
8
+ version: "1.0.2",
9
9
  register(api) {
10
10
  setWeComRuntime(api.runtime);
11
11
  api.registerChannel({ plugin: wecomPlugin });
@@ -2,7 +2,7 @@
2
2
  "id": "wecom",
3
3
  "name": "WeCom",
4
4
  "description": "WeCom (企业微信) channel plugin for OpenClaw",
5
- "version": "1.0.0",
5
+ "version": "1.0.2",
6
6
  "channels": ["wecom"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@junjiezhang/openclaw-wecom-plugin",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenClaw WeCom (企业微信) channel plugin - Send and receive messages via WeCom",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "openclaw.plugin.json",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
8
14
  "keywords": [
9
15
  "openclaw",
10
16
  "plugin",
package/src/accounts.ts DELETED
@@ -1,81 +0,0 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
- import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
3
- import type { WeComConfig, ResolvedWeComAccount } from "./types.js";
4
-
5
- export function listWeComAccountIds(cfg: ClawdbotConfig): string[] {
6
- const wecomCfg = cfg.channels?.wecom as WeComConfig | undefined;
7
- if (!wecomCfg) return [];
8
- return [DEFAULT_ACCOUNT_ID];
9
- }
10
-
11
- export function resolveDefaultWeComAccountId(cfg: ClawdbotConfig): string {
12
- return DEFAULT_ACCOUNT_ID;
13
- }
14
-
15
- export function resolveWeComAccount({
16
- cfg,
17
- accountId,
18
- }: {
19
- cfg: ClawdbotConfig;
20
- accountId?: string;
21
- }): ResolvedWeComAccount {
22
- const id = accountId ?? DEFAULT_ACCOUNT_ID;
23
- const wecomCfg = cfg.channels?.wecom as WeComConfig | undefined;
24
-
25
- if (!wecomCfg) {
26
- return {
27
- accountId: id,
28
- enabled: false,
29
- configured: false,
30
- name: "default",
31
- };
32
- }
33
-
34
- const enabled = wecomCfg.enabled ?? false;
35
- const configured = !!(
36
- wecomCfg.corpId &&
37
- wecomCfg.agentId &&
38
- wecomCfg.secret &&
39
- wecomCfg.token &&
40
- wecomCfg.encodingAESKey
41
- );
42
-
43
- return {
44
- accountId: id,
45
- enabled,
46
- configured,
47
- name: "default",
48
- corpId: wecomCfg.corpId,
49
- agentId: wecomCfg.agentId,
50
- config: wecomCfg,
51
- };
52
- }
53
-
54
- export function resolveWeComCredentials({
55
- cfg,
56
- accountId,
57
- }: {
58
- cfg: ClawdbotConfig;
59
- accountId?: string;
60
- }): {
61
- corpId: string;
62
- agentId: string;
63
- secret: string;
64
- token?: string;
65
- encodingAESKey?: string;
66
- } {
67
- const account = resolveWeComAccount({ cfg, accountId });
68
- const wecomCfg = account.config;
69
-
70
- if (!wecomCfg?.corpId || !wecomCfg?.agentId || !wecomCfg?.secret) {
71
- throw new Error("WeCom credentials not configured");
72
- }
73
-
74
- return {
75
- corpId: wecomCfg.corpId,
76
- agentId: wecomCfg.agentId,
77
- secret: wecomCfg.secret,
78
- token: wecomCfg.token,
79
- encodingAESKey: wecomCfg.encodingAESKey,
80
- };
81
- }
package/src/bot.ts DELETED
@@ -1,410 +0,0 @@
1
- import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
2
- import {
3
- buildAgentMediaPayload,
4
- buildPendingHistoryContextFromMap,
5
- clearHistoryEntriesIfEnabled,
6
- createScopedPairingAccess,
7
- recordPendingHistoryEntryIfEnabled,
8
- } from "openclaw/plugin-sdk";
9
- import { resolveWeComAccount } from "./accounts.js";
10
- import { downloadImageWeCom, saveInboundImage } from "./media.js";
11
- import { createWeComReplyDispatcher } from "./reply-dispatcher.js";
12
- import { getWeComRuntime } from "./runtime.js";
13
- import { sendMessageWeCom, sendGroupMessageWeCom } from "./send.js";
14
-
15
- export interface WeComMessageEvent {
16
- ToUserName: string; // 企业微信 CorpID
17
- FromUserName: string; // 发送者 UserID
18
- CreateTime: number; // 消息创建时间
19
- MsgType: string; // 消息类型:text, image, voice, video, file, location, link
20
- Content?: string; // 文本消息内容
21
- MsgId: string; // 消息 ID
22
- AgentID: string; // 企业应用 ID
23
- // Image message
24
- PicUrl?: string; // 图片链接
25
- MediaId?: string; // 媒体文件 ID
26
- // File message
27
- Title?: string; // 文件名
28
- Description?: string; // 文件描述
29
- FileKey?: string; // 文件 Key
30
- // Location message
31
- Location_X?: string; // 纬度
32
- Location_Y?: string; // 经度
33
- Scale?: string; // 地图缩放大小
34
- Label?: string; // 地理位置信息
35
- // Link message
36
- Url?: string; // 链接地址
37
- // Group chat
38
- ChatId?: string; // 群聊 ID (when in group)
39
- ChatType?: string; // 聊天类型: single (单聊) or group (群聊)
40
- }
41
-
42
- /**
43
- * Handle incoming WeCom message
44
- */
45
- export async function handleWeComMessage({
46
- cfg,
47
- event,
48
- runtime,
49
- chatHistories,
50
- accountId,
51
- }: {
52
- cfg: ClawdbotConfig;
53
- event: WeComMessageEvent;
54
- runtime?: RuntimeEnv;
55
- chatHistories: Map<string, HistoryEntry[]>;
56
- accountId?: string;
57
- }): Promise<void> {
58
- const log = runtime?.log ?? console.log;
59
- const error = runtime?.error ?? console.error;
60
-
61
- // Media list for images/files
62
- const mediaList: Array<{ path: string; contentType?: string | null }> = [];
63
-
64
- try {
65
- const userId = event.FromUserName;
66
- const messageId = event.MsgId;
67
- const msgType = event.MsgType;
68
- const chatId = event.ChatId;
69
- const chatType = event.ChatType ?? (chatId ? "group" : "single");
70
- const isGroupChat = chatType === "group" || !!chatId;
71
-
72
- // Resolve configured history limit (group vs DM)
73
- const wecomCfg = resolveWeComAccount({ cfg, accountId }).config;
74
- const DEFAULT_HISTORY_LIMIT = 20;
75
- const historyLimit = isGroupChat
76
- ? (wecomCfg?.historyLimit ?? DEFAULT_HISTORY_LIMIT)
77
- : (wecomCfg?.dmHistoryLimit ?? DEFAULT_HISTORY_LIMIT);
78
-
79
- // Check for duplicate messages
80
- try {
81
- const { tryRecordMessagePersistent } = await import("./dedup.js");
82
- const isNew = await tryRecordMessagePersistent(messageId, accountId ?? "default", log);
83
- if (!isNew) {
84
- log(`wecom[${accountId}]: duplicate message ${messageId}, skipping`);
85
- return;
86
- }
87
- } catch (dedupErr) {
88
- error(`wecom[${accountId}]: dedup check failed: ${String(dedupErr)}`);
89
- // Continue processing even if dedup fails
90
- }
91
-
92
- log(
93
- `wecom[${accountId}]: received ${msgType} message from ${userId} in ${isGroupChat ? "group" : "DM"}`,
94
- );
95
-
96
- // Build session key
97
- const sessionKey = isGroupChat
98
- ? `wecom:${accountId}:group:${chatId}`
99
- : `wecom:${accountId}:user:${userId}`;
100
-
101
- // Get or create history
102
- let history = chatHistories.get(sessionKey);
103
- if (!history) {
104
- history = [];
105
- chatHistories.set(sessionKey, history);
106
- }
107
-
108
- let messageText = "";
109
-
110
- // Handle different message types
111
- switch (msgType) {
112
- case "text":
113
- messageText = event.Content ?? "";
114
- break;
115
-
116
- case "image":
117
- // Download and save image
118
- if (event.MediaId) {
119
- try {
120
- log(`wecom[${accountId}]: downloading image (mediaId=${event.MediaId})...`);
121
- const { buffer, contentType, fileName } = await downloadImageWeCom({
122
- cfg,
123
- mediaId: event.MediaId,
124
- accountId,
125
- });
126
- const filePath = await saveInboundImage({
127
- buffer,
128
- fileName: fileName || "image.jpg",
129
- accountId: accountId || "default",
130
- });
131
-
132
- // Add to media list
133
- mediaList.push({ path: filePath, contentType: contentType || null });
134
- messageText = "[图片]";
135
- log(`wecom[${accountId}]: image downloaded: ${filePath}`);
136
- } catch (imgErr) {
137
- log(`wecom[${accountId}]: image download failed: ${String(imgErr)}`);
138
- messageText = "[图片]";
139
- }
140
- } else {
141
- messageText = "[图片]";
142
- }
143
- break;
144
-
145
- case "file":
146
- messageText = `[文件: ${event.Title ?? "未知"}]`;
147
- break;
148
-
149
- case "location":
150
- messageText = `[位置: ${event.Label ?? ""}] (${event.Location_X}, ${event.Location_Y})`;
151
- break;
152
-
153
- case "link":
154
- messageText = `[链接: ${event.Title ?? ""}] ${event.Url ?? ""}`;
155
- break;
156
-
157
- case "voice":
158
- case "video":
159
- messageText = `[${msgType === "voice" ? "语音" : "视频"}]`;
160
- log(`wecom[${accountId}]: ${msgType} message not fully supported yet`);
161
- break;
162
-
163
- default:
164
- log(`wecom[${accountId}]: unsupported message type: ${msgType}`);
165
- return;
166
- }
167
-
168
- // For group chats, check if bot should respond
169
- // Check group policy and allowlist
170
- if (isGroupChat) {
171
- const account = resolveWeComAccount({ cfg, accountId });
172
- const wecomCfg = account.config;
173
-
174
- // Import policy functions
175
- const { resolveWeComReplyPolicy, isWeComGroupAllowed } = await import("./policy.js");
176
- const { resolveDefaultGroupPolicy, resolveAllowlistProviderRuntimeGroupPolicy } =
177
- await import("openclaw/plugin-sdk");
178
-
179
- const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg);
180
- const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({
181
- providerConfigPresent: cfg.channels?.wecom !== undefined,
182
- groupPolicy: wecomCfg?.groupPolicy,
183
- defaultGroupPolicy,
184
- });
185
-
186
- // Check if group is allowed — use chatId, not userId (groupAllowFrom contains group IDs)
187
- if (
188
- !isWeComGroupAllowed({
189
- groupPolicy,
190
- allowFrom: wecomCfg?.groupAllowFrom ?? [],
191
- senderId: chatId ?? "",
192
- })
193
- ) {
194
- log(`wecom[${accountId}]: group ${chatId} not in allowlist, skipping`);
195
- return;
196
- }
197
-
198
- // Check mention policy
199
- const { requireMention } = resolveWeComReplyPolicy({
200
- isDirectMessage: false,
201
- globalConfig: wecomCfg,
202
- });
203
-
204
- if (requireMention) {
205
- // TODO: Implement proper @mention detection for WeCom
206
- // For now, skip if requireMention is true (WeCom doesn't provide mention info in basic events)
207
- log(`wecom[${accountId}]: group message without mention, skipping (requireMention=true)`);
208
- return;
209
- }
210
- }
211
-
212
- // For DMs, enforce dmPolicy (allowlist / pairing)
213
- if (!isGroupChat) {
214
- const account = resolveWeComAccount({ cfg, accountId });
215
- const wecomCfg = account.config;
216
- const dmPolicy = wecomCfg?.dmPolicy ?? "pairing";
217
-
218
- if (dmPolicy !== "open") {
219
- const { resolveWeComAllowlistMatch } = await import("./policy.js");
220
- const core = getWeComRuntime();
221
- const pairing = createScopedPairingAccess({
222
- core,
223
- channel: "wecom",
224
- accountId: account.accountId,
225
- });
226
-
227
- const configAllowFrom = (wecomCfg?.allowFrom ?? []).map(String);
228
- const storeAllowFrom =
229
- dmPolicy === "pairing" ? await pairing.readAllowFromStore().catch(() => []) : [];
230
- const effectiveDmAllowFrom = [...configAllowFrom, ...storeAllowFrom];
231
-
232
- const dmAllowed = resolveWeComAllowlistMatch({
233
- allowFrom: effectiveDmAllowFrom,
234
- senderId: userId,
235
- }).allowed;
236
-
237
- if (!dmAllowed) {
238
- if (dmPolicy === "pairing") {
239
- const { code, created } = await pairing.upsertPairingRequest({
240
- id: userId,
241
- meta: { name: userId },
242
- });
243
- if (created) {
244
- log(`wecom[${accountId}]: pairing request from ${userId}`);
245
- try {
246
- await sendMessageWeCom({
247
- cfg,
248
- to: userId,
249
- text: core.channel.pairing.buildPairingReply({
250
- channel: "wecom",
251
- idLine: `Your WeCom user ID: ${userId}`,
252
- code,
253
- }),
254
- accountId,
255
- });
256
- } catch (pairErr) {
257
- log(`wecom[${accountId}]: pairing reply failed for ${userId}: ${String(pairErr)}`);
258
- }
259
- }
260
- } else {
261
- log(
262
- `wecom[${accountId}]: blocked unauthorized DM from ${userId} (dmPolicy=${dmPolicy})`,
263
- );
264
- }
265
- return;
266
- }
267
- }
268
- }
269
-
270
- // Get sender name for group chats
271
- let senderName = userId;
272
- if (isGroupChat) {
273
- try {
274
- const { getUserInfoWeCom } = await import("./directory.js");
275
- const userInfo = await getUserInfoWeCom({ cfg, userId, accountId });
276
- senderName = userInfo.name || userId;
277
- } catch (err) {
278
- log(`wecom[${accountId}]: failed to get sender name: ${String(err)}`);
279
- }
280
- }
281
-
282
- // Prefix message with sender name in group chats
283
- const displayMessage = isGroupChat ? `${senderName}: ${messageText}` : messageText;
284
-
285
- // Record user message in history
286
- recordPendingHistoryEntryIfEnabled({
287
- historyMap: chatHistories,
288
- historyKey: sessionKey,
289
- entry: {
290
- sender: senderName,
291
- body: messageText,
292
- timestamp: event.CreateTime * 1000,
293
- messageId: messageId,
294
- },
295
- limit: historyLimit,
296
- });
297
-
298
- // Build context for agent
299
- const core = getWeComRuntime();
300
- const historyContext = buildPendingHistoryContextFromMap({
301
- historyMap: chatHistories,
302
- historyKey: sessionKey,
303
- limit: historyLimit,
304
- currentMessage: displayMessage,
305
- formatEntry: (entry) =>
306
- core.channel.reply.formatAgentEnvelope({
307
- channel: "WeCom",
308
- from: isGroupChat ? `group:${chatId}` : userId,
309
- timestamp: new Date(),
310
- envelope: core.channel.reply.resolveEnvelopeFormatOptions(cfg),
311
- body: `${entry.sender}: ${entry.body}`,
312
- }),
313
- });
314
-
315
- // Resolve agent route
316
- const account = resolveWeComAccount({ cfg, accountId });
317
- const route = core.channel.routing.resolveAgentRoute({
318
- cfg,
319
- channel: "wecom",
320
- accountId: account.accountId,
321
- peer: { kind: isGroupChat ? "group" : "direct", id: isGroupChat ? (chatId ?? "") : userId },
322
- });
323
-
324
- // Create reply dispatcher
325
- const { dispatcher, replyOptions, markDispatchIdle } = createWeComReplyDispatcher({
326
- cfg,
327
- agentId: route.agentId,
328
- runtime: runtime as RuntimeEnv,
329
- userId,
330
- chatId,
331
- isGroupChat,
332
- accountId: account.accountId,
333
- });
334
-
335
- log(`wecom[${accountId}]: dispatching to agent (session=${route.sessionKey})`);
336
-
337
- // Build media payload
338
- const mediaPayload = buildAgentMediaPayload(mediaList);
339
-
340
- // Build context payload
341
- const ctxPayload = core.channel.reply.finalizeInboundContext({
342
- CommandBody: historyContext,
343
- From: userId,
344
- To: account.corpId ?? "",
345
- SessionKey: route.sessionKey,
346
- AccountId: route.accountId,
347
- ChatType: isGroupChat ? "group" : "direct",
348
- GroupSubject: isGroupChat ? chatId : undefined,
349
- SenderName: senderName,
350
- SenderId: userId,
351
- Provider: "wecom" as const,
352
- Surface: "wecom" as const,
353
- MessageSid: messageId,
354
- Timestamp: Date.now(),
355
- CommandAuthorized: true,
356
- OriginatingChannel: "wecom" as const,
357
- OriginatingTo: account.corpId ?? "",
358
- ...mediaPayload,
359
- });
360
-
361
- // Dispatch to agent
362
- const { queuedFinal, counts } = await core.channel.reply.dispatchReplyFromConfig({
363
- ctx: ctxPayload,
364
- cfg,
365
- dispatcher,
366
- replyOptions,
367
- });
368
-
369
- markDispatchIdle();
370
-
371
- clearHistoryEntriesIfEnabled({
372
- historyMap: chatHistories,
373
- historyKey: sessionKey,
374
- limit: historyLimit,
375
- });
376
-
377
- log(
378
- `wecom[${accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`,
379
- );
380
- } catch (err) {
381
- error(`wecom[${accountId}]: error handling message: ${String(err)}`);
382
- if (err instanceof Error && err.stack) {
383
- error(`wecom[${accountId}]: stack trace: ${err.stack}`);
384
- }
385
-
386
- // Try to send error message
387
- try {
388
- const chatId = event.ChatId;
389
- const isGroupChat = event.ChatType === "group" || !!chatId;
390
-
391
- if (isGroupChat && chatId) {
392
- await sendGroupMessageWeCom({
393
- cfg,
394
- chatId,
395
- text: "抱歉,处理消息时出现错误。",
396
- accountId,
397
- });
398
- } else {
399
- await sendMessageWeCom({
400
- cfg,
401
- to: event.FromUserName,
402
- text: "抱歉,处理消息时出现错误。",
403
- accountId,
404
- });
405
- }
406
- } catch (sendErr) {
407
- error(`wecom[${accountId}]: failed to send error message: ${String(sendErr)}`);
408
- }
409
- }
410
- }