@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 +1 -1
- package/wecom/src/commands.ts +78 -1
package/package.json
CHANGED
package/wecom/src/commands.ts
CHANGED
|
@@ -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}`);
|