@marshulll/openclaw-wecom 0.1.18 → 0.1.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marshulll/openclaw-wecom",
3
- "version": "0.1.18",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "description": "OpenClaw WeCom channel plugin (intelligent bot + internal app)",
6
6
  "author": "OpenClaw",
@@ -1,8 +1,10 @@
1
1
  import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { basename } from "node:path";
2
4
 
3
5
  import { getWecomRuntime } from "./runtime.js";
4
6
  import { listWecomAccountIds } from "./accounts.js";
5
- import { sendWecomText } from "./wecom-api.js";
7
+ import { sendWecomFile, sendWecomText, uploadWecomMedia } from "./wecom-api.js";
6
8
  import type { ResolvedWecomAccount } from "./types.js";
7
9
 
8
10
  export type CommandContext = {
@@ -21,6 +23,65 @@ async function sendAndRecord(ctx: CommandContext, text: string): Promise<void> {
21
23
  ctx.log?.(`[wecom] command reply sent to ${ctx.fromUser}`);
22
24
  }
23
25
 
26
+ function parseQuotedArgs(raw: string): string[] {
27
+ const args: string[] = [];
28
+ const normalized = raw.replace(/,/g, " ");
29
+ const regex = /"([^"]+)"|'([^']+)'|(\S+)/g;
30
+ let match: RegExpExecArray | null;
31
+ while ((match = regex.exec(normalized))) {
32
+ const value = match[1] || match[2] || match[3];
33
+ if (value) args.push(value.trim());
34
+ }
35
+ return args;
36
+ }
37
+
38
+ async function sendFiles(ctx: CommandContext, paths: string[]): Promise<{ sent: number; skipped: number }> {
39
+ let sent = 0;
40
+ let skipped = 0;
41
+ const maxBytes = ctx.account.config.media?.maxBytes;
42
+ for (const rawPath of paths) {
43
+ const path = rawPath.startsWith("file://") ? rawPath.replace(/^file:\/\//, "") : rawPath;
44
+ if (!path.startsWith("/")) {
45
+ skipped += 1;
46
+ await sendAndRecord(ctx, `⚠️ 路径需为绝对路径:${rawPath}`);
47
+ continue;
48
+ }
49
+ try {
50
+ const info = await stat(path);
51
+ if (!info.isFile()) {
52
+ skipped += 1;
53
+ await sendAndRecord(ctx, `⚠️ 不是文件:${path}`);
54
+ continue;
55
+ }
56
+ if (typeof maxBytes === "number" && maxBytes > 0 && info.size > maxBytes) {
57
+ skipped += 1;
58
+ await sendAndRecord(ctx, `⚠️ 文件过大(${info.size} bytes):${path}`);
59
+ continue;
60
+ }
61
+ const buffer = await readFile(path);
62
+ const filename = basename(path) || "file.bin";
63
+ const mediaId = await uploadWecomMedia({
64
+ account: ctx.account,
65
+ type: "file",
66
+ buffer,
67
+ filename,
68
+ });
69
+ await sendWecomFile({
70
+ account: ctx.account,
71
+ toUser: ctx.fromUser,
72
+ chatId: ctx.isGroup ? ctx.chatId : undefined,
73
+ mediaId,
74
+ });
75
+ sent += 1;
76
+ } catch (err) {
77
+ skipped += 1;
78
+ await sendAndRecord(ctx, `⚠️ 发送失败:${path} (${String(err)})`);
79
+ }
80
+ }
81
+ ctx.statusSink?.({ lastOutboundAt: Date.now() });
82
+ return { sent, skipped };
83
+ }
84
+
24
85
  async function handleHelp(ctx: CommandContext): Promise<void> {
25
86
  const helpText = `🤖 WeCom 助手使用帮助
26
87
 
@@ -28,6 +89,7 @@ async function handleHelp(ctx: CommandContext): Promise<void> {
28
89
  /help - 显示此帮助信息
29
90
  /clear - 清除会话历史,开始新对话
30
91
  /status - 查看系统状态
92
+ /sendfile <path...> - 发送服务器文件(支持多个路径,可用引号)
31
93
 
32
94
  直接发送消息即可与 AI 对话。`;
33
95
  await sendAndRecord(ctx, helpText);
@@ -79,6 +141,16 @@ async function handleClear(ctx: CommandContext): Promise<void> {
79
141
  await sendAndRecord(ctx, "✅ 会话已重置,请开始新的对话。");
80
142
  }
81
143
 
144
+ async function handleSendFile(cmd: string, ctx: CommandContext): Promise<void> {
145
+ const args = parseQuotedArgs(cmd.replace(/^\/sendfile(s)?\s*/i, ""));
146
+ if (args.length === 0) {
147
+ await sendAndRecord(ctx, "用法:/sendfile /absolute/path/to/file1 /absolute/path/to/file2\n支持引号:/sendfile \"/path/with space/a.txt\"");
148
+ return;
149
+ }
150
+ const { sent, skipped } = await sendFiles(ctx, args);
151
+ await sendAndRecord(ctx, `✅ 已发送 ${sent} 个文件${skipped ? `,跳过 ${skipped} 个` : ""}。`);
152
+ }
153
+
82
154
  const COMMANDS: Record<string, (ctx: CommandContext) => Promise<void>> = {
83
155
  "/help": handleHelp,
84
156
  "/status": handleStatus,
@@ -88,6 +160,11 @@ const COMMANDS: Record<string, (ctx: CommandContext) => Promise<void>> = {
88
160
  export async function handleCommand(cmd: string, ctx: CommandContext): Promise<boolean> {
89
161
  const key = cmd.trim().split(/\s+/)[0]?.toLowerCase();
90
162
  if (!key) return false;
163
+ if (key === "/sendfile" || key === "/sendfiles") {
164
+ ctx.log?.(`[wecom] handling command ${key}`);
165
+ await handleSendFile(cmd, ctx);
166
+ return true;
167
+ }
91
168
  const handler = COMMANDS[key];
92
169
  if (!handler) return false;
93
170
  ctx.log?.(`[wecom] handling command ${key}`);