@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 +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -1
- package/src/accounts.ts +0 -81
- package/src/bot.ts +0 -410
- package/src/channel.ts +0 -278
- package/src/client.ts +0 -55
- package/src/config-schema.ts +0 -102
- package/src/dedup.ts +0 -60
- package/src/directory.ts +0 -150
- package/src/index.ts +0 -20
- package/src/media.ts +0 -105
- package/src/monitor.ts +0 -344
- package/src/outbound.ts +0 -26
- package/src/policy.ts +0 -108
- package/src/probe.ts +0 -13
- package/src/reply-dispatcher.ts +0 -78
- package/src/runtime.ts +0 -14
- package/src/send.ts +0 -91
- package/src/targets.ts +0 -21
- package/src/types.d.ts +0 -17
- package/src/types.ts +0 -3
- package/tsconfig.json +0 -32
- package/types.d.ts +0 -43
package/src/media.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import os from "os";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
5
|
-
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk";
|
|
6
|
-
import { resolveWeComAccount } from "./accounts.js";
|
|
7
|
-
import { getWeComAccessToken } from "./client.js";
|
|
8
|
-
|
|
9
|
-
export type DownloadImageResult = {
|
|
10
|
-
buffer: Buffer;
|
|
11
|
-
contentType?: string;
|
|
12
|
-
fileName?: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const WECOM_API_POLICY = { allowedHostnames: ["qyapi.weixin.qq.com"] };
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Download an image from WeCom using media_id.
|
|
19
|
-
* WeCom image API: GET https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
|
|
20
|
-
*/
|
|
21
|
-
export async function downloadImageWeCom(params: {
|
|
22
|
-
cfg: ClawdbotConfig;
|
|
23
|
-
mediaId: string;
|
|
24
|
-
accountId?: string;
|
|
25
|
-
}): Promise<DownloadImageResult> {
|
|
26
|
-
const { cfg, mediaId, accountId } = params;
|
|
27
|
-
const account = resolveWeComAccount({ cfg, accountId });
|
|
28
|
-
if (!account.configured) {
|
|
29
|
-
throw new Error(`WeCom account "${account.accountId}" not configured`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const accessToken = await getWeComAccessToken({ cfg, accountId });
|
|
33
|
-
const url = `https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token=${accessToken}&media_id=${mediaId}`;
|
|
34
|
-
|
|
35
|
-
const { response, release } = await fetchWithSsrFGuard({
|
|
36
|
-
url,
|
|
37
|
-
policy: WECOM_API_POLICY,
|
|
38
|
-
auditContext: "wecom-download-image",
|
|
39
|
-
});
|
|
40
|
-
try {
|
|
41
|
-
if (!response.ok) {
|
|
42
|
-
throw new Error(`WeCom image download failed: ${response.status} ${response.statusText}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Check if response is JSON error
|
|
46
|
-
const contentType = response.headers.get("content-type");
|
|
47
|
-
if (contentType?.includes("application/json")) {
|
|
48
|
-
const errorData = await response.json();
|
|
49
|
-
throw new Error(`WeCom image download error ${errorData.errcode}: ${errorData.errmsg}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const buffer = Buffer.from(await response.arrayBuffer());
|
|
53
|
-
|
|
54
|
-
// Try to determine file extension from content-type
|
|
55
|
-
let fileName = `image_${mediaId}`;
|
|
56
|
-
if (contentType) {
|
|
57
|
-
const ext = contentTypeToExtension(contentType);
|
|
58
|
-
if (ext) {
|
|
59
|
-
fileName = `image_${mediaId}${ext}`;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return { buffer, contentType: contentType ?? undefined, fileName };
|
|
64
|
-
} finally {
|
|
65
|
-
await release();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Map content-type to file extension
|
|
71
|
-
*/
|
|
72
|
-
function contentTypeToExtension(contentType: string): string | null {
|
|
73
|
-
const map: Record<string, string> = {
|
|
74
|
-
"image/jpeg": ".jpg",
|
|
75
|
-
"image/png": ".png",
|
|
76
|
-
"image/gif": ".gif",
|
|
77
|
-
"image/webp": ".webp",
|
|
78
|
-
"image/bmp": ".bmp",
|
|
79
|
-
"image/tiff": ".tiff",
|
|
80
|
-
};
|
|
81
|
-
return map[contentType] || null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Save image to inbound media directory
|
|
86
|
-
*/
|
|
87
|
-
export async function saveInboundImage(params: {
|
|
88
|
-
buffer: Buffer;
|
|
89
|
-
fileName: string;
|
|
90
|
-
accountId: string;
|
|
91
|
-
}): Promise<string> {
|
|
92
|
-
const { buffer, fileName, accountId } = params;
|
|
93
|
-
|
|
94
|
-
// Ensure media directory exists
|
|
95
|
-
const mediaDir = path.join(os.homedir(), ".openclaw", "media", "inbound");
|
|
96
|
-
await fs.promises.mkdir(mediaDir, { recursive: true });
|
|
97
|
-
|
|
98
|
-
// Generate unique filename
|
|
99
|
-
const uniqueName = `${accountId}_${Date.now()}_${fileName}`;
|
|
100
|
-
const filePath = path.join(mediaDir, uniqueName);
|
|
101
|
-
|
|
102
|
-
await fs.promises.writeFile(filePath, buffer);
|
|
103
|
-
|
|
104
|
-
return filePath;
|
|
105
|
-
}
|
package/src/monitor.ts
DELETED
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
import * as crypto from "crypto";
|
|
2
|
-
import * as http from "http";
|
|
3
|
-
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
|
|
4
|
-
import { installRequestBodyLimitGuard } from "openclaw/plugin-sdk";
|
|
5
|
-
import { resolveWeComAccount } from "./accounts.js";
|
|
6
|
-
import { handleWeComMessage, type WeComMessageEvent } from "./bot.js";
|
|
7
|
-
import { probeWeCom } from "./probe.js";
|
|
8
|
-
import type { ResolvedWeComAccount } from "./types.js";
|
|
9
|
-
|
|
10
|
-
export type MonitorWeComOpts = {
|
|
11
|
-
config?: ClawdbotConfig;
|
|
12
|
-
runtime?: RuntimeEnv;
|
|
13
|
-
abortSignal?: AbortSignal;
|
|
14
|
-
accountId?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const httpServers = new Map<string, http.Server>();
|
|
18
|
-
const chatHistoriesMap = new Map<string, Map<string, HistoryEntry[]>>();
|
|
19
|
-
const WECOM_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
20
|
-
const WECOM_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Verify WeCom webhook signature
|
|
24
|
-
*/
|
|
25
|
-
function verifyWeComSignature(
|
|
26
|
-
signature: string,
|
|
27
|
-
timestamp: string,
|
|
28
|
-
nonce: string,
|
|
29
|
-
body: string,
|
|
30
|
-
token: string,
|
|
31
|
-
): boolean {
|
|
32
|
-
const arr = [token, timestamp, nonce, body].sort();
|
|
33
|
-
const str = arr.join("");
|
|
34
|
-
const hash = crypto.createHash("sha1").update(str).digest("hex");
|
|
35
|
-
return hash === signature;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Decrypt WeCom message
|
|
40
|
-
*/
|
|
41
|
-
function decryptWeComMessage(
|
|
42
|
-
encrypt: string,
|
|
43
|
-
encodingAESKey: string,
|
|
44
|
-
): { message: string; corpId: string } {
|
|
45
|
-
const key = Buffer.from(encodingAESKey + "=", "base64");
|
|
46
|
-
const encryptBuffer = Buffer.from(encrypt, "base64");
|
|
47
|
-
|
|
48
|
-
// Decrypt using key as IV (WeCom uses the key itself as IV)
|
|
49
|
-
const decipher = crypto.createDecipheriv("aes-256-cbc", key, key.slice(0, 16));
|
|
50
|
-
decipher.setAutoPadding(false);
|
|
51
|
-
let decrypted = Buffer.concat([decipher.update(encryptBuffer), decipher.final()]);
|
|
52
|
-
|
|
53
|
-
// Remove PKCS7 padding
|
|
54
|
-
const pad = decrypted[decrypted.length - 1];
|
|
55
|
-
decrypted = decrypted.slice(0, decrypted.length - pad);
|
|
56
|
-
|
|
57
|
-
// Message structure: random(16) + msg_len(4) + msg(msg_len) + corpId
|
|
58
|
-
// Skip random 16 bytes, read msg_len as network byte order (big-endian)
|
|
59
|
-
const msgLen = decrypted.readUInt32BE(16);
|
|
60
|
-
const message = decrypted.slice(20, 20 + msgLen).toString("utf8");
|
|
61
|
-
const corpId = decrypted.slice(20 + msgLen).toString("utf8");
|
|
62
|
-
|
|
63
|
-
return { message, corpId };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Monitor WeCom webhook
|
|
68
|
-
*/
|
|
69
|
-
async function monitorWeComWebhook({
|
|
70
|
-
cfg,
|
|
71
|
-
account,
|
|
72
|
-
runtime,
|
|
73
|
-
abortSignal,
|
|
74
|
-
}: {
|
|
75
|
-
cfg: ClawdbotConfig;
|
|
76
|
-
account: ResolvedWeComAccount;
|
|
77
|
-
runtime?: RuntimeEnv;
|
|
78
|
-
abortSignal?: AbortSignal;
|
|
79
|
-
}): Promise<void> {
|
|
80
|
-
const { accountId } = account;
|
|
81
|
-
const log = runtime?.log ?? console.log;
|
|
82
|
-
const error = runtime?.error ?? console.error;
|
|
83
|
-
|
|
84
|
-
const port = account.config?.webhookPort ?? 3000;
|
|
85
|
-
const path = account.config?.webhookPath ?? "/wecom/events";
|
|
86
|
-
const host = account.config?.webhookHost ?? "127.0.0.1";
|
|
87
|
-
const token = account.config?.token;
|
|
88
|
-
const encodingAESKey = account.config?.encodingAESKey;
|
|
89
|
-
|
|
90
|
-
if (!token || !encodingAESKey) {
|
|
91
|
-
throw new Error(`WeCom account "${accountId}" requires token and encodingAESKey`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
log(`wecom[${accountId}]: starting Webhook server on ${host}:${port}, path ${path}...`);
|
|
95
|
-
|
|
96
|
-
// Get or create chatHistories for this account
|
|
97
|
-
let chatHistories = chatHistoriesMap.get(accountId);
|
|
98
|
-
if (!chatHistories) {
|
|
99
|
-
chatHistories = new Map<string, HistoryEntry[]>();
|
|
100
|
-
chatHistoriesMap.set(accountId, chatHistories);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const server = http.createServer();
|
|
104
|
-
|
|
105
|
-
server.on("request", async (req, res) => {
|
|
106
|
-
// Extract pathname from URL (ignore query parameters)
|
|
107
|
-
const urlPath = req.url?.split("?")[0];
|
|
108
|
-
if (urlPath !== path) {
|
|
109
|
-
res.statusCode = 404;
|
|
110
|
-
res.end("Not Found");
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Handle URL verification (GET request)
|
|
115
|
-
if (req.method === "GET") {
|
|
116
|
-
// Parse URL and get URL-decoded parameters
|
|
117
|
-
const url = new URL(req.url!, `http://${req.headers.host}`);
|
|
118
|
-
const msgSignature = url.searchParams.get("msg_signature");
|
|
119
|
-
const timestamp = url.searchParams.get("timestamp");
|
|
120
|
-
const nonce = url.searchParams.get("nonce");
|
|
121
|
-
const echostr = url.searchParams.get("echostr");
|
|
122
|
-
|
|
123
|
-
if (!msgSignature || !timestamp || !nonce || !echostr) {
|
|
124
|
-
res.statusCode = 400;
|
|
125
|
-
res.end("Bad Request");
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
// Verify signature using URL-decoded echostr
|
|
131
|
-
if (!verifyWeComSignature(msgSignature, timestamp, nonce, echostr, token)) {
|
|
132
|
-
error(`wecom[${accountId}]: signature verification failed`);
|
|
133
|
-
res.statusCode = 401;
|
|
134
|
-
res.end("Unauthorized");
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Decrypt echostr
|
|
139
|
-
const { message, corpId } = decryptWeComMessage(echostr, encodingAESKey);
|
|
140
|
-
|
|
141
|
-
// Verify corpId matches configuration
|
|
142
|
-
const expectedCorpId = account.corpId;
|
|
143
|
-
if (corpId !== expectedCorpId) {
|
|
144
|
-
error(`wecom[${accountId}]: corpId mismatch, expected=${expectedCorpId}, got=${corpId}`);
|
|
145
|
-
res.statusCode = 403;
|
|
146
|
-
res.end("Forbidden");
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Return plain text without quotes, BOM, or newlines
|
|
151
|
-
res.statusCode = 200;
|
|
152
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
153
|
-
res.end(message);
|
|
154
|
-
log(`wecom[${accountId}]: URL verification successful`);
|
|
155
|
-
} catch (err) {
|
|
156
|
-
error(`wecom[${accountId}]: URL verification error: ${String(err)}`);
|
|
157
|
-
res.statusCode = 500;
|
|
158
|
-
res.end("Internal Server Error");
|
|
159
|
-
}
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Handle message events (POST request)
|
|
164
|
-
if (req.method === "POST") {
|
|
165
|
-
const guard = installRequestBodyLimitGuard(req, res, {
|
|
166
|
-
maxBytes: WECOM_WEBHOOK_MAX_BODY_BYTES,
|
|
167
|
-
timeoutMs: WECOM_WEBHOOK_BODY_TIMEOUT_MS,
|
|
168
|
-
responseFormat: "text",
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
if (guard.isTripped()) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const chunks: Buffer[] = [];
|
|
176
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
177
|
-
req.on("end", async () => {
|
|
178
|
-
try {
|
|
179
|
-
const body = Buffer.concat(chunks).toString("utf8");
|
|
180
|
-
|
|
181
|
-
// Parse XML to extract Encrypt field
|
|
182
|
-
const encryptMatch = body.match(/<Encrypt><!\[CDATA\[(.*?)\]\]><\/Encrypt>/);
|
|
183
|
-
if (!encryptMatch) {
|
|
184
|
-
error(`wecom[${accountId}]: failed to extract Encrypt from XML body`);
|
|
185
|
-
res.statusCode = 400;
|
|
186
|
-
res.end("Bad Request");
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
const encrypt = encryptMatch[1];
|
|
190
|
-
|
|
191
|
-
const url = new URL(req.url!, `http://${req.headers.host}`);
|
|
192
|
-
const msgSignature = url.searchParams.get("msg_signature");
|
|
193
|
-
const timestamp = url.searchParams.get("timestamp");
|
|
194
|
-
const nonce = url.searchParams.get("nonce");
|
|
195
|
-
|
|
196
|
-
if (!msgSignature || !timestamp || !nonce) {
|
|
197
|
-
res.statusCode = 400;
|
|
198
|
-
res.end("Bad Request");
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Verify signature
|
|
203
|
-
if (!verifyWeComSignature(msgSignature, timestamp, nonce, encrypt, token)) {
|
|
204
|
-
error(`wecom[${accountId}]: signature verification failed`);
|
|
205
|
-
res.statusCode = 401;
|
|
206
|
-
res.end("Unauthorized");
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Decrypt message
|
|
211
|
-
const { message } = decryptWeComMessage(encrypt, encodingAESKey);
|
|
212
|
-
|
|
213
|
-
// Parse decrypted XML message
|
|
214
|
-
const event: WeComMessageEvent = {
|
|
215
|
-
ToUserName: message.match(/<ToUserName><!\[CDATA\[(.*?)\]\]><\/ToUserName>/)?.[1] || "",
|
|
216
|
-
FromUserName:
|
|
217
|
-
message.match(/<FromUserName><!\[CDATA\[(.*?)\]\]><\/FromUserName>/)?.[1] || "",
|
|
218
|
-
CreateTime: Number.parseInt(
|
|
219
|
-
message.match(/<CreateTime>(\d+)<\/CreateTime>/)?.[1] || "0",
|
|
220
|
-
),
|
|
221
|
-
MsgType: message.match(/<MsgType><!\[CDATA\[(.*?)\]\]><\/MsgType>/)?.[1] || "",
|
|
222
|
-
Content: message.match(/<Content><!\[CDATA\[(.*?)\]\]><\/Content>/)?.[1],
|
|
223
|
-
MsgId: message.match(/<MsgId>(\d+)<\/MsgId>/)?.[1] || "",
|
|
224
|
-
AgentID: message.match(/<AgentID>(\d+)<\/AgentID>/)?.[1] || "",
|
|
225
|
-
PicUrl: message.match(/<PicUrl><!\[CDATA\[(.*?)\]\]><\/PicUrl>/)?.[1],
|
|
226
|
-
MediaId: message.match(/<MediaId><!\[CDATA\[(.*?)\]\]><\/MediaId>/)?.[1],
|
|
227
|
-
Title: message.match(/<Title><!\[CDATA\[(.*?)\]\]><\/Title>/)?.[1],
|
|
228
|
-
Description: message.match(/<Description><!\[CDATA\[(.*?)\]\]><\/Description>/)?.[1],
|
|
229
|
-
FileKey: message.match(/<FileKey><!\[CDATA\[(.*?)\]\]><\/FileKey>/)?.[1],
|
|
230
|
-
Location_X: message.match(/<Location_X>(.*?)<\/Location_X>/)?.[1],
|
|
231
|
-
Location_Y: message.match(/<Location_Y>(.*?)<\/Location_Y>/)?.[1],
|
|
232
|
-
Scale: message.match(/<Scale>(\d+)<\/Scale>/)?.[1],
|
|
233
|
-
Label: message.match(/<Label><!\[CDATA\[(.*?)\]\]><\/Label>/)?.[1],
|
|
234
|
-
Url: message.match(/<Url><!\[CDATA\[(.*?)\]\]><\/Url>/)?.[1],
|
|
235
|
-
ChatId: message.match(/<ChatId><!\[CDATA\[(.*?)\]\]><\/ChatId>/)?.[1],
|
|
236
|
-
ChatType: message.match(/<ChatType><!\[CDATA\[(.*?)\]\]><\/ChatType>/)?.[1],
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
log(
|
|
240
|
-
`wecom[${accountId}]: received message from ${event.FromUserName}, type=${event.MsgType}`,
|
|
241
|
-
);
|
|
242
|
-
|
|
243
|
-
// Handle message (fire and forget to avoid blocking response).
|
|
244
|
-
// handleWeComMessage has its own try/catch and sends error replies internally.
|
|
245
|
-
handleWeComMessage({
|
|
246
|
-
cfg,
|
|
247
|
-
event,
|
|
248
|
-
runtime,
|
|
249
|
-
chatHistories,
|
|
250
|
-
accountId: accountId,
|
|
251
|
-
}).catch((err) => {
|
|
252
|
-
error(`wecom[${accountId}]: unexpected error handling message: ${String(err)}`);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
res.statusCode = 200;
|
|
256
|
-
res.end("success");
|
|
257
|
-
} catch (err) {
|
|
258
|
-
if (!guard.isTripped()) {
|
|
259
|
-
error(`wecom[${accountId}]: webhook handler error: ${String(err)}`);
|
|
260
|
-
res.statusCode = 500;
|
|
261
|
-
res.end("Internal Server Error");
|
|
262
|
-
}
|
|
263
|
-
} finally {
|
|
264
|
-
guard.dispose();
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
res.statusCode = 405;
|
|
271
|
-
res.end("Method Not Allowed");
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
httpServers.set(accountId, server);
|
|
275
|
-
|
|
276
|
-
return new Promise((resolve, reject) => {
|
|
277
|
-
const cleanup = (callback?: () => void) => {
|
|
278
|
-
server.close(() => {
|
|
279
|
-
httpServers.delete(accountId);
|
|
280
|
-
callback?.();
|
|
281
|
-
});
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const handleAbort = () => {
|
|
285
|
-
log(`wecom[${accountId}]: abort signal received, stopping`);
|
|
286
|
-
cleanup(() => resolve());
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
if (abortSignal?.aborted) {
|
|
290
|
-
cleanup(() => resolve());
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
abortSignal?.addEventListener("abort", handleAbort, { once: true });
|
|
295
|
-
|
|
296
|
-
// Attach error handler before listen() so async bind failures (e.g. EADDRINUSE) are caught.
|
|
297
|
-
server.on("error", (err) => {
|
|
298
|
-
error(`wecom[${accountId}]: server error: ${String(err)}`);
|
|
299
|
-
abortSignal?.removeEventListener("abort", handleAbort);
|
|
300
|
-
cleanup(() => reject(err));
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
server.listen(port, host, () => {
|
|
304
|
-
log(`wecom[${accountId}]: Webhook server listening on ${host}:${port}${path}`);
|
|
305
|
-
});
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Monitor WeCom provider
|
|
311
|
-
*/
|
|
312
|
-
export async function monitorWeComProvider(opts: MonitorWeComOpts): Promise<() => void> {
|
|
313
|
-
const { config, runtime, abortSignal, accountId } = opts;
|
|
314
|
-
|
|
315
|
-
if (!config) {
|
|
316
|
-
throw new Error("Config is required");
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const account = resolveWeComAccount({ cfg: config, accountId });
|
|
320
|
-
|
|
321
|
-
if (!account.configured) {
|
|
322
|
-
throw new Error(`WeCom account "${account.accountId}" is not configured`);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const log = runtime?.log ?? console.log;
|
|
326
|
-
log(`wecom[${account.accountId}]: starting monitor...`);
|
|
327
|
-
|
|
328
|
-
// Start webhook server and wait for it to be ready
|
|
329
|
-
await monitorWeComWebhook({
|
|
330
|
-
cfg: config,
|
|
331
|
-
account,
|
|
332
|
-
runtime,
|
|
333
|
-
abortSignal,
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
// Return cleanup function
|
|
337
|
-
return () => {
|
|
338
|
-
const server = httpServers.get(account.accountId);
|
|
339
|
-
if (server) {
|
|
340
|
-
server.close();
|
|
341
|
-
httpServers.delete(account.accountId);
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
}
|
package/src/outbound.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
|
|
2
|
-
import { sendMessageWeCom } from "./send.js";
|
|
3
|
-
|
|
4
|
-
export const wecomOutbound: ChannelOutboundAdapter = {
|
|
5
|
-
deliveryMode: "direct",
|
|
6
|
-
sendText: async ({ cfg, to, text, accountId }) => {
|
|
7
|
-
const result = await sendMessageWeCom({
|
|
8
|
-
cfg,
|
|
9
|
-
to,
|
|
10
|
-
text,
|
|
11
|
-
accountId: accountId ?? undefined,
|
|
12
|
-
});
|
|
13
|
-
return { channel: "wecom", ...result };
|
|
14
|
-
},
|
|
15
|
-
sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
|
|
16
|
-
// WeCom media upload not yet implemented; send text + URL fallback
|
|
17
|
-
const content = [text?.trim(), mediaUrl].filter(Boolean).join("\n");
|
|
18
|
-
const result = await sendMessageWeCom({
|
|
19
|
-
cfg,
|
|
20
|
-
to,
|
|
21
|
-
text: content || "(media)",
|
|
22
|
-
accountId: accountId ?? undefined,
|
|
23
|
-
});
|
|
24
|
-
return { channel: "wecom", ...result };
|
|
25
|
-
},
|
|
26
|
-
};
|
package/src/policy.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AllowlistMatch,
|
|
3
|
-
ChannelGroupContext,
|
|
4
|
-
GroupToolPolicyConfig,
|
|
5
|
-
} from "openclaw/plugin-sdk";
|
|
6
|
-
import { normalizeWeComTarget } from "./targets.js";
|
|
7
|
-
import type { WeComConfig, WeComGroupConfig } from "./types.js";
|
|
8
|
-
|
|
9
|
-
export type WeComAllowlistMatch = AllowlistMatch<"wildcard" | "id">;
|
|
10
|
-
|
|
11
|
-
function normalizeWeComAllowEntry(raw: string): string {
|
|
12
|
-
const trimmed = raw.trim();
|
|
13
|
-
if (!trimmed) {
|
|
14
|
-
return "";
|
|
15
|
-
}
|
|
16
|
-
if (trimmed === "*") {
|
|
17
|
-
return "*";
|
|
18
|
-
}
|
|
19
|
-
const withoutProviderPrefix = trimmed.replace(/^wecom:/i, "");
|
|
20
|
-
const normalized = normalizeWeComTarget(withoutProviderPrefix) ?? withoutProviderPrefix;
|
|
21
|
-
return normalized.trim().toLowerCase();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function resolveWeComAllowlistMatch(params: {
|
|
25
|
-
allowFrom: Array<string | number>;
|
|
26
|
-
senderId: string;
|
|
27
|
-
senderIds?: Array<string | null | undefined>;
|
|
28
|
-
senderName?: string | null;
|
|
29
|
-
}): WeComAllowlistMatch {
|
|
30
|
-
const allowFrom = params.allowFrom
|
|
31
|
-
.map((entry) => normalizeWeComAllowEntry(String(entry)))
|
|
32
|
-
.filter(Boolean);
|
|
33
|
-
if (allowFrom.length === 0) {
|
|
34
|
-
return { allowed: false };
|
|
35
|
-
}
|
|
36
|
-
if (allowFrom.includes("*")) {
|
|
37
|
-
return { allowed: true, matchKey: "*", matchSource: "wildcard" };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// WeCom allowlists are ID-based
|
|
41
|
-
const senderCandidates = [params.senderId, ...(params.senderIds ?? [])]
|
|
42
|
-
.map((entry) => normalizeWeComAllowEntry(String(entry ?? "")))
|
|
43
|
-
.filter(Boolean);
|
|
44
|
-
|
|
45
|
-
for (const senderId of senderCandidates) {
|
|
46
|
-
if (allowFrom.includes(senderId)) {
|
|
47
|
-
return { allowed: true, matchKey: senderId, matchSource: "id" };
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return { allowed: false };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function resolveWeComGroupConfig(params: {
|
|
55
|
-
cfg?: WeComConfig;
|
|
56
|
-
groupId?: string | null;
|
|
57
|
-
}): WeComGroupConfig | undefined {
|
|
58
|
-
// WeCom doesn't have per-group config yet, return undefined
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function resolveWeComGroupToolPolicy(
|
|
63
|
-
params: ChannelGroupContext,
|
|
64
|
-
): GroupToolPolicyConfig | undefined {
|
|
65
|
-
const cfg = params.cfg.channels?.wecom as WeComConfig | undefined;
|
|
66
|
-
if (!cfg) {
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const groupConfig = resolveWeComGroupConfig({
|
|
71
|
-
cfg,
|
|
72
|
-
groupId: params.groupId,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
return groupConfig?.tools;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function isWeComGroupAllowed(params: {
|
|
79
|
-
groupPolicy: "open" | "allowlist" | "disabled";
|
|
80
|
-
allowFrom: Array<string | number>;
|
|
81
|
-
senderId: string;
|
|
82
|
-
senderIds?: Array<string | null | undefined>;
|
|
83
|
-
senderName?: string | null;
|
|
84
|
-
}): boolean {
|
|
85
|
-
const { groupPolicy } = params;
|
|
86
|
-
if (groupPolicy === "disabled") {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
if (groupPolicy === "open") {
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
return resolveWeComAllowlistMatch(params).allowed;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function resolveWeComReplyPolicy(params: {
|
|
96
|
-
isDirectMessage: boolean;
|
|
97
|
-
globalConfig?: WeComConfig;
|
|
98
|
-
groupConfig?: WeComGroupConfig;
|
|
99
|
-
}): { requireMention: boolean } {
|
|
100
|
-
if (params.isDirectMessage) {
|
|
101
|
-
return { requireMention: false };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const requireMention =
|
|
105
|
-
params.groupConfig?.requireMention ?? params.globalConfig?.requireMention ?? false;
|
|
106
|
-
|
|
107
|
-
return { requireMention };
|
|
108
|
-
}
|
package/src/probe.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { ResolvedWeComAccount } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export async function probeWeCom(account: ResolvedWeComAccount): Promise<{
|
|
4
|
-
ok: boolean;
|
|
5
|
-
error?: string;
|
|
6
|
-
}> {
|
|
7
|
-
if (!account.configured) {
|
|
8
|
-
return { ok: false, error: "Not configured" };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// TODO: Implement actual probe (e.g., test API call)
|
|
12
|
-
return { ok: true };
|
|
13
|
-
}
|
package/src/reply-dispatcher.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createReplyPrefixContext,
|
|
3
|
-
type ClawdbotConfig,
|
|
4
|
-
type ReplyPayload,
|
|
5
|
-
type RuntimeEnv,
|
|
6
|
-
} from "openclaw/plugin-sdk";
|
|
7
|
-
import { resolveWeComAccount } from "./accounts.js";
|
|
8
|
-
import { getWeComRuntime } from "./runtime.js";
|
|
9
|
-
import { sendMessageWeCom, sendGroupMessageWeCom } from "./send.js";
|
|
10
|
-
|
|
11
|
-
export type CreateWeComReplyDispatcherParams = {
|
|
12
|
-
cfg: ClawdbotConfig;
|
|
13
|
-
agentId: string;
|
|
14
|
-
runtime: RuntimeEnv;
|
|
15
|
-
userId?: string;
|
|
16
|
-
chatId?: string;
|
|
17
|
-
isGroupChat: boolean;
|
|
18
|
-
accountId?: string;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export function createWeComReplyDispatcher(params: CreateWeComReplyDispatcherParams) {
|
|
22
|
-
const core = getWeComRuntime();
|
|
23
|
-
const { cfg, agentId, userId, chatId, isGroupChat, accountId } = params;
|
|
24
|
-
const account = resolveWeComAccount({ cfg, accountId });
|
|
25
|
-
const prefixContext = createReplyPrefixContext({ cfg, agentId });
|
|
26
|
-
|
|
27
|
-
const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "wecom", accountId, {
|
|
28
|
-
fallbackLimit: 2000,
|
|
29
|
-
});
|
|
30
|
-
const chunkMode = core.channel.text.resolveChunkMode(cfg, "wecom");
|
|
31
|
-
|
|
32
|
-
const { dispatcher, replyOptions, markDispatchIdle } =
|
|
33
|
-
core.channel.reply.createReplyDispatcherWithTyping({
|
|
34
|
-
responsePrefix: prefixContext.responsePrefix,
|
|
35
|
-
responsePrefixContextProvider: prefixContext.responsePrefixContextProvider,
|
|
36
|
-
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
|
37
|
-
deliver: async (payload: ReplyPayload) => {
|
|
38
|
-
const text = payload.text ?? "";
|
|
39
|
-
if (!text.trim()) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
for (const chunk of core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode)) {
|
|
44
|
-
if (isGroupChat && chatId) {
|
|
45
|
-
await sendGroupMessageWeCom({
|
|
46
|
-
cfg,
|
|
47
|
-
chatId,
|
|
48
|
-
text: chunk,
|
|
49
|
-
accountId,
|
|
50
|
-
});
|
|
51
|
-
} else if (userId) {
|
|
52
|
-
await sendMessageWeCom({
|
|
53
|
-
cfg,
|
|
54
|
-
to: userId,
|
|
55
|
-
text: chunk,
|
|
56
|
-
accountId,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
onError: async (error, info) => {
|
|
62
|
-
params.runtime.error?.(
|
|
63
|
-
`wecom[${account.accountId}] ${info.kind} reply failed: ${String(error)}`,
|
|
64
|
-
);
|
|
65
|
-
},
|
|
66
|
-
onIdle: async () => {},
|
|
67
|
-
onCleanup: () => {},
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
dispatcher,
|
|
72
|
-
replyOptions: {
|
|
73
|
-
...replyOptions,
|
|
74
|
-
onModelSelected: prefixContext.onModelSelected,
|
|
75
|
-
},
|
|
76
|
-
markDispatchIdle,
|
|
77
|
-
};
|
|
78
|
-
}
|
package/src/runtime.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
-
|
|
3
|
-
let runtime: PluginRuntime | null = null;
|
|
4
|
-
|
|
5
|
-
export function setWeComRuntime(next: PluginRuntime) {
|
|
6
|
-
runtime = next;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getWeComRuntime(): PluginRuntime {
|
|
10
|
-
if (!runtime) {
|
|
11
|
-
throw new Error("WeCom runtime not initialized");
|
|
12
|
-
}
|
|
13
|
-
return runtime;
|
|
14
|
-
}
|