@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/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
- }
@@ -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
- }