@marshulll/openclaw-wecom 0.1.23 → 0.1.24

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/README.en.md CHANGED
@@ -93,6 +93,7 @@ Install guide: `docs/INSTALL.md`
93
93
  - `/sendfile`: send files from server (multiple absolute paths)
94
94
  - Directories are zipped automatically
95
95
  - Example: `/sendfile /tmp/openclaw-wecom /home/shu/Desktop/report.pdf`
96
+ - Natural language also works: "send me this file image-xxx.jpg" (default match in `media.tempDir`)
96
97
 
97
98
  ## Media auto recognition (optional)
98
99
  - **Voice send/receive does NOT require API**; only auto transcription needs an OpenAI-compatible API
package/README.md CHANGED
@@ -95,6 +95,7 @@ openclaw gateway restart
95
95
  - `/sendfile`:发送服务器文件(支持多个绝对路径)
96
96
  - 支持目录:自动打包为 zip 后发送
97
97
  - 示例:`/sendfile /tmp/openclaw-wecom /home/shu/Desktop/report.pdf`
98
+ - 也支持自然语言:`把这个文件发给我 image-xxx.jpg`(默认仅在 `media.tempDir` 内匹配)
98
99
 
99
100
  ## 多媒体自动识别(可选)
100
101
  - **语音收发不需要 API**,只有开启“语音自动转写”才需要 OpenAI 兼容接口
package/README.zh.md CHANGED
@@ -95,6 +95,7 @@ openclaw gateway restart
95
95
  - `/sendfile`:发送服务器文件(支持多个绝对路径)
96
96
  - 支持目录:自动打包为 zip 后发送
97
97
  - 示例:`/sendfile /tmp/openclaw-wecom /home/shu/Desktop/report.pdf`
98
+ - 也支持自然语言:`把这个文件发给我 image-xxx.jpg`(默认仅在 `media.tempDir` 内匹配)
98
99
 
99
100
  ## 多媒体自动识别(可选)
100
101
  - **语音收发不需要 API**,只有开启“语音自动转写”才需要 OpenAI 兼容接口
package/docs/INSTALL.md CHANGED
@@ -131,6 +131,7 @@ openclaw gateway restart
131
131
  ### /sendfile(文件与文件夹)
132
132
  - `/sendfile` 仅支持 **服务器绝对路径**
133
133
  - 目录会自动打包为 zip 再发送
134
+ - 自然语言也可触发:`把这个文件发给我 image-xxx.jpg`(默认仅在 `media.tempDir` 内匹配)
134
135
 
135
136
  示例:
136
137
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marshulll/openclaw-wecom",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "type": "module",
5
5
  "description": "OpenClaw WeCom channel plugin (intelligent bot + internal app)",
6
6
  "author": "OpenClaw",
@@ -130,6 +130,139 @@ function sleep(ms: number): Promise<void> {
130
130
  return new Promise((resolve) => setTimeout(resolve, ms));
131
131
  }
132
132
 
133
+ function resolveSendIntervalMs(target: WecomWebhookTarget): number {
134
+ const interval = target.account.config.sendQueue?.intervalMs;
135
+ return typeof interval === "number" && interval >= 0 ? interval : 400;
136
+ }
137
+
138
+ function extractFilenameCandidates(text: string): string[] {
139
+ const candidates = new Set<string>();
140
+ const normalized = text.replace(/[,,;;|]/g, " ");
141
+ const regex = /(?:\/|file:\/\/)?[A-Za-z0-9._-]+\.[A-Za-z0-9]{1,8}/g;
142
+ for (const match of normalized.matchAll(regex)) {
143
+ const value = match[0];
144
+ if (value) candidates.add(value.replace(/^file:\/\//, ""));
145
+ }
146
+ return Array.from(candidates);
147
+ }
148
+
149
+ async function tryHandleNaturalFileSend(params: {
150
+ target: WecomWebhookTarget;
151
+ text: string;
152
+ fromUser: string;
153
+ chatId?: string;
154
+ isGroup: boolean;
155
+ }): Promise<boolean> {
156
+ const { target, text, fromUser, chatId, isGroup } = params;
157
+ if (!text || text.trim().startsWith("/")) return false;
158
+ if (!/(发给我|发送给我|发我|给我)/.test(text)) return false;
159
+ const names = extractFilenameCandidates(text);
160
+ if (names.length === 0) return false;
161
+
162
+ const tempDir = resolveMediaTempDir(target);
163
+ let dirEntries: string[] = [];
164
+ try {
165
+ dirEntries = await readdir(tempDir);
166
+ } catch {
167
+ dirEntries = [];
168
+ }
169
+ const dirSet = new Set(dirEntries);
170
+
171
+ const resolved: string[] = [];
172
+ const missing: string[] = [];
173
+ for (const name of names) {
174
+ let fullPath = "";
175
+ if (name.startsWith("/")) {
176
+ fullPath = name;
177
+ } else if (dirSet.has(name)) {
178
+ fullPath = join(tempDir, name);
179
+ }
180
+ if (!fullPath) {
181
+ missing.push(name);
182
+ continue;
183
+ }
184
+ try {
185
+ const info = await stat(fullPath);
186
+ if (info.isFile()) {
187
+ resolved.push(fullPath);
188
+ } else {
189
+ missing.push(name);
190
+ }
191
+ } catch {
192
+ missing.push(name);
193
+ }
194
+ }
195
+
196
+ if (resolved.length === 0) {
197
+ const hint = dirEntries.length ? `可用文件示例:${dirEntries.slice(0, 5).join(", ")}` : "当前目录无可用文件";
198
+ await sendWecomText({
199
+ account: target.account,
200
+ toUser: fromUser,
201
+ chatId: isGroup ? chatId : undefined,
202
+ text: `未找到指定文件:${missing.join(", ")}。\n${hint}`,
203
+ });
204
+ return true;
205
+ }
206
+
207
+ const maxBytes = resolveMediaMaxBytes(target);
208
+ const intervalMs = resolveSendIntervalMs(target);
209
+ let sent = 0;
210
+ const failed: string[] = [];
211
+
212
+ for (const path of resolved) {
213
+ try {
214
+ const info = await stat(path);
215
+ if (maxBytes && info.size > maxBytes) {
216
+ failed.push(`${basename(path)}(过大)`);
217
+ continue;
218
+ }
219
+ const buffer = await readFile(path);
220
+ const filename = basename(path) || "file.bin";
221
+ const mediaId = await uploadWecomMedia({
222
+ account: target.account,
223
+ type: "file",
224
+ buffer,
225
+ filename,
226
+ });
227
+ await sendWecomFile({
228
+ account: target.account,
229
+ toUser: fromUser,
230
+ chatId: isGroup ? chatId : undefined,
231
+ mediaId,
232
+ });
233
+ sent += 1;
234
+ await appendOperationLog(target, {
235
+ action: "natural-sendfile",
236
+ accountId: target.account.accountId,
237
+ toUser: fromUser,
238
+ chatId,
239
+ path,
240
+ size: info.size,
241
+ });
242
+ if (intervalMs) await sleep(intervalMs);
243
+ } catch (err) {
244
+ failed.push(basename(path));
245
+ await appendOperationLog(target, {
246
+ action: "natural-sendfile",
247
+ accountId: target.account.accountId,
248
+ toUser: fromUser,
249
+ chatId,
250
+ path,
251
+ error: String(err),
252
+ });
253
+ }
254
+ }
255
+
256
+ const summary = `已发送 ${sent} 个文件${failed.length ? `,失败:${failed.join(", ")}` : ""}${missing.length ? `,未找到:${missing.join(", ")}` : ""}`;
257
+ await sendWecomText({
258
+ account: target.account,
259
+ toUser: fromUser,
260
+ chatId: isGroup ? chatId : undefined,
261
+ text: summary,
262
+ });
263
+ return true;
264
+ }
265
+
133
266
  async function appendOperationLog(target: WecomWebhookTarget, entry: Record<string, unknown>): Promise<void> {
134
267
  const logPath = target.account.config.operations?.logPath?.trim();
135
268
  if (!logPath) return;
@@ -861,6 +994,17 @@ async function processAppMessage(params: {
861
994
  if (handled) return;
862
995
  }
863
996
 
997
+ if (msgType === "text") {
998
+ const handled = await tryHandleNaturalFileSend({
999
+ target,
1000
+ text: messageText,
1001
+ fromUser,
1002
+ chatId,
1003
+ isGroup,
1004
+ });
1005
+ if (handled) return;
1006
+ }
1007
+
864
1008
  try {
865
1009
  await startAgentForApp({
866
1010
  target,