@creatoraris/openclaw-wecom 0.2.2 → 0.3.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/index.js +123 -6
- package/openclaw.plugin.json +12 -3
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
3
5
|
import { URL } from 'node:url';
|
|
4
6
|
import { WeComCrypto } from './crypto.js';
|
|
5
7
|
|
|
@@ -9,6 +11,7 @@ const plugin = {
|
|
|
9
11
|
const token = cfg.token || process.env.WECOM_TOKEN;
|
|
10
12
|
const encodingAESKey = cfg.encodingAESKey || process.env.WECOM_ENCODING_AES_KEY;
|
|
11
13
|
const corpId = cfg.corpId || process.env.WECOM_CORP_ID || '';
|
|
14
|
+
const corpSecret = cfg.corpSecret || process.env.WECOM_CORPSECRET || '';
|
|
12
15
|
const port = Number(cfg.port || process.env.PORT || 8788);
|
|
13
16
|
|
|
14
17
|
if (!token || !encodingAESKey) {
|
|
@@ -73,17 +76,130 @@ const plugin = {
|
|
|
73
76
|
return JSON.stringify({ encrypt: encrypted, msgsignature, timestamp: Number(timestamp), nonce });
|
|
74
77
|
}
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
// ── Image handling ──
|
|
80
|
+
|
|
81
|
+
const IMAGE_CACHE_DIR = '/tmp/openclaw-wecom-images';
|
|
82
|
+
const IMAGE_MAX_AGE_MS = 60 * 60 * 1000;
|
|
83
|
+
|
|
84
|
+
let wecomAccessToken = null;
|
|
85
|
+
let tokenExpireTime = 0;
|
|
86
|
+
|
|
87
|
+
async function getWeComAccessToken() {
|
|
88
|
+
if (!corpId || !corpSecret) return null;
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
if (wecomAccessToken && now < tokenExpireTime) return wecomAccessToken;
|
|
91
|
+
try {
|
|
92
|
+
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${corpId}&corpsecret=${corpSecret}`;
|
|
93
|
+
const data = await (await fetch(url)).json();
|
|
94
|
+
if (data.errcode === 0 && data.access_token) {
|
|
95
|
+
wecomAccessToken = data.access_token;
|
|
96
|
+
tokenExpireTime = now + (data.expires_in - 300) * 1000;
|
|
97
|
+
log.info('[WeCom] access_token obtained');
|
|
98
|
+
return wecomAccessToken;
|
|
99
|
+
}
|
|
100
|
+
log.error(`[WeCom] access_token failed: ${data.errmsg}`);
|
|
101
|
+
return null;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
log.error(`[WeCom] access_token error: ${err.message}`);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function downloadWeComMedia(mediaId) {
|
|
109
|
+
try {
|
|
110
|
+
const accessToken = await getWeComAccessToken();
|
|
111
|
+
if (!accessToken) return null;
|
|
112
|
+
const url = `https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token=${accessToken}&media_id=${mediaId}`;
|
|
113
|
+
log.info(`[WeCom Media] downloading media_id=${mediaId}`);
|
|
114
|
+
const response = await fetch(url);
|
|
115
|
+
if (!response.ok) return null;
|
|
116
|
+
const contentType = response.headers.get('content-type') || '';
|
|
117
|
+
if (contentType.includes('application/json')) return null;
|
|
118
|
+
const buffer = await response.arrayBuffer();
|
|
119
|
+
if (buffer.byteLength > 10 * 1024 * 1024) return null;
|
|
120
|
+
await fs.mkdir(IMAGE_CACHE_DIR, { recursive: true });
|
|
121
|
+
const filename = `${Date.now()}-${crypto.randomBytes(4).toString('hex')}-media.jpg`;
|
|
122
|
+
const filepath = path.join(IMAGE_CACHE_DIR, filename);
|
|
123
|
+
await fs.writeFile(filepath, Buffer.from(buffer));
|
|
124
|
+
log.info(`[WeCom Media] saved ${filename} (${(buffer.byteLength / 1024 / 1024).toFixed(2)}MB)`);
|
|
125
|
+
return filepath;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
log.error(`[WeCom Media] download failed: ${err.message}`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function downloadImage(imageUrl) {
|
|
133
|
+
try {
|
|
134
|
+
log.info(`[Image] downloading ${imageUrl.slice(0, 100)}`);
|
|
135
|
+
const response = await fetch(imageUrl);
|
|
136
|
+
if (!response.ok) return null;
|
|
137
|
+
const buffer = await response.arrayBuffer();
|
|
138
|
+
if (buffer.byteLength > 10 * 1024 * 1024) return null;
|
|
139
|
+
await fs.mkdir(IMAGE_CACHE_DIR, { recursive: true });
|
|
140
|
+
const ext = path.extname(new URL(imageUrl).pathname) || '.jpg';
|
|
141
|
+
const filename = `${Date.now()}-${crypto.randomBytes(4).toString('hex')}${ext}`;
|
|
142
|
+
const filepath = path.join(IMAGE_CACHE_DIR, filename);
|
|
143
|
+
await fs.writeFile(filepath, Buffer.from(buffer));
|
|
144
|
+
log.info(`[Image] saved ${filename} (${(buffer.byteLength / 1024 / 1024).toFixed(2)}MB)`);
|
|
145
|
+
return filepath;
|
|
146
|
+
} catch (err) {
|
|
147
|
+
log.error(`[Image] download failed: ${err.message}`);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function resolveImage(imageObj) {
|
|
153
|
+
const mediaId = imageObj?.media_id;
|
|
154
|
+
const imageUrl = imageObj?.url || imageObj?.pic_url;
|
|
155
|
+
let localPath = null;
|
|
156
|
+
if (mediaId) localPath = await downloadWeComMedia(mediaId);
|
|
157
|
+
if (!localPath && imageUrl) localPath = await downloadImage(imageUrl);
|
|
158
|
+
if (localPath) return `[用户发送了一张图片]\n本地路径: ${localPath}\n请使用image工具分析这张图片并回复用户。`;
|
|
159
|
+
if (imageUrl) return `[用户发送了一张图片]\n图片URL: ${imageUrl}`;
|
|
160
|
+
return '[用户发送了一张图片,但无法获取]';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function cleanupImageCache() {
|
|
164
|
+
try {
|
|
165
|
+
const files = await fs.readdir(IMAGE_CACHE_DIR).catch(() => []);
|
|
166
|
+
const cutoff = Date.now() - IMAGE_MAX_AGE_MS;
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
const filepath = path.join(IMAGE_CACHE_DIR, file);
|
|
169
|
+
const stat = await fs.stat(filepath).catch(() => null);
|
|
170
|
+
if (stat && stat.mtimeMs < cutoff) await fs.unlink(filepath).catch(() => {});
|
|
171
|
+
}
|
|
172
|
+
} catch {}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function extractUserText(msg, isGroup = false) {
|
|
77
176
|
const cleanMention = (text) => isGroup ? text.replace(/@[^\s@]+\s*/g, '').trim() : text.trim();
|
|
78
177
|
|
|
79
178
|
if (msg.msgtype === 'text') return cleanMention(msg.text?.content || '');
|
|
80
179
|
if (msg.msgtype === 'voice') return msg.voice?.content?.trim() || '';
|
|
180
|
+
|
|
81
181
|
if (msg.msgtype === 'mixed') {
|
|
82
182
|
const parts = msg.mixed?.msg_item || [];
|
|
83
|
-
const
|
|
84
|
-
|
|
183
|
+
const textParts = [];
|
|
184
|
+
const imagePrompts = [];
|
|
185
|
+
for (const part of parts) {
|
|
186
|
+
if (part.msgtype === 'text') {
|
|
187
|
+
textParts.push(part.text?.content || '');
|
|
188
|
+
} else if (part.msgtype === 'image') {
|
|
189
|
+
imagePrompts.push(await resolveImage(part.image));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let result = cleanMention(textParts.join(' '));
|
|
193
|
+
if (imagePrompts.length > 0) {
|
|
194
|
+
result = result ? `${result}\n\n${imagePrompts.join('\n\n')}` : imagePrompts.join('\n\n');
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (msg.msgtype === 'image') {
|
|
200
|
+
return await resolveImage(msg.image);
|
|
85
201
|
}
|
|
86
|
-
|
|
202
|
+
|
|
87
203
|
return '';
|
|
88
204
|
}
|
|
89
205
|
|
|
@@ -280,7 +396,7 @@ const plugin = {
|
|
|
280
396
|
}
|
|
281
397
|
|
|
282
398
|
const isGroup = chattype === 'group';
|
|
283
|
-
const text = extractUserText(msg, isGroup);
|
|
399
|
+
const text = await extractUserText(msg, isGroup);
|
|
284
400
|
log.info(`[<- ${source}] (${msgtype}) ${text.slice(0, 100)}`);
|
|
285
401
|
|
|
286
402
|
if (!text) {
|
|
@@ -325,6 +441,7 @@ const plugin = {
|
|
|
325
441
|
});
|
|
326
442
|
|
|
327
443
|
cleanupStreams();
|
|
444
|
+
cleanupImageCache();
|
|
328
445
|
return;
|
|
329
446
|
}
|
|
330
447
|
|
|
@@ -354,7 +471,7 @@ const plugin = {
|
|
|
354
471
|
});
|
|
355
472
|
|
|
356
473
|
server.listen(port, '127.0.0.1', () => {
|
|
357
|
-
log.info(`wecom
|
|
474
|
+
log.info(`openclaw-wecom plugin listening on 127.0.0.1:${port}`);
|
|
358
475
|
log.info(` Callback: /callback`);
|
|
359
476
|
log.info(` OpenClaw: ${openclawApi}`);
|
|
360
477
|
resolve();
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-wecom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"name": "企业微信(WeCom)",
|
|
5
5
|
"description": "企业微信智能助手机器人桥接,支持流式回复、加密消息、单聊和群聊",
|
|
6
6
|
"author": "CreatorAris",
|
|
@@ -38,7 +38,11 @@
|
|
|
38
38
|
},
|
|
39
39
|
"corpId": {
|
|
40
40
|
"type": "string",
|
|
41
|
-
"description": "企业微信 CorpID
|
|
41
|
+
"description": "企业微信 CorpID(可选,用于图片下载)"
|
|
42
|
+
},
|
|
43
|
+
"corpSecret": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "企业微信 CorpSecret(可选,用于图片下载)"
|
|
42
46
|
},
|
|
43
47
|
"port": {
|
|
44
48
|
"type": "number",
|
|
@@ -60,7 +64,12 @@
|
|
|
60
64
|
},
|
|
61
65
|
"corpId": {
|
|
62
66
|
"label": "CorpID",
|
|
63
|
-
"description": "企业微信企业 ID
|
|
67
|
+
"description": "企业微信企业 ID,配合 CorpSecret 可下载图片"
|
|
68
|
+
},
|
|
69
|
+
"corpSecret": {
|
|
70
|
+
"label": "CorpSecret",
|
|
71
|
+
"sensitive": true,
|
|
72
|
+
"description": "企业微信应用密钥,配合 CorpID 可下载图片"
|
|
64
73
|
},
|
|
65
74
|
"port": {
|
|
66
75
|
"label": "端口",
|