@tencent-weixin/openclaw-weixin 2.1.8 → 2.1.9
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/CHANGELOG.md +16 -0
- package/CHANGELOG.zh_CN.md +16 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/channel.ts +66 -24
- package/src/messaging/outbound-hooks.ts +84 -0
- package/src/messaging/process-message.ts +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
This project follows the [Keep a Changelog](https://keepachangelog.com/) format.
|
|
6
6
|
|
|
7
|
+
## [2.1.9] - 2026-04-20
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Outbound hook support:** Add `message_sending` (pre-send interception/modification) and `message_sent` (post-send notification) hook integration for all outbound paths — `sendText`, `sendMedia`, and the inbound-reply `deliver` in `process-message`. Hook logic is extracted into a shared `src/messaging/outbound-hooks.ts` module.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **Cleanup:** Remove unused `mediaUrl` parameter from `sendWeixinOutbound` signature.
|
|
16
|
+
|
|
17
|
+
## [2.1.8] - 2026-04-07
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Markdown filter:** `StreamingMarkdownFilter` now preserves more Markdown constructs in outbound text.
|
|
22
|
+
|
|
7
23
|
## [2.1.7] - 2026-04-07
|
|
8
24
|
|
|
9
25
|
### Fixed
|
package/CHANGELOG.zh_CN.md
CHANGED
|
@@ -4,6 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
本项目遵循 [Keep a Changelog](https://keepachangelog.com/) 格式。
|
|
6
6
|
|
|
7
|
+
## [2.1.9] - 2026-04-20
|
|
8
|
+
|
|
9
|
+
### 新增
|
|
10
|
+
|
|
11
|
+
- **外发 hook 支持:** 为所有外发路径(`sendText`、`sendMedia`、`process-message` 中的入站回复 `deliver`)接入 `message_sending`(发送前拦截/修改)和 `message_sent`(发送后通知)hook。hook 逻辑抽取至共享模块 `src/messaging/outbound-hooks.ts`。
|
|
12
|
+
|
|
13
|
+
### 变更
|
|
14
|
+
|
|
15
|
+
- **清理:** 移除 `sendWeixinOutbound` 签名中未使用的 `mediaUrl` 参数。
|
|
16
|
+
|
|
17
|
+
## [2.1.8] - 2026-04-07
|
|
18
|
+
|
|
19
|
+
### 变更
|
|
20
|
+
|
|
21
|
+
- **Markdown 过滤器:** `StreamingMarkdownFilter` 放开了更多 Markdown 格式的保留。
|
|
22
|
+
|
|
7
23
|
## [2.1.7] - 2026-04-07
|
|
8
24
|
|
|
9
25
|
### 修复
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -27,6 +27,7 @@ import type { WeixinQrStartResult, WeixinQrWaitResult } from "./auth/login-qr.js
|
|
|
27
27
|
// Lazy-imported inside startAccount to avoid pulling in the monitor -> process-message ->
|
|
28
28
|
// command-auth chain during plugin registration, which can re-enter plugin/provider registry
|
|
29
29
|
// resolution before the account actually starts.
|
|
30
|
+
import { applyWeixinMessageSendingHook, emitWeixinMessageSent } from "./messaging/outbound-hooks.js";
|
|
30
31
|
import { sendWeixinMediaFile } from "./messaging/send-media.js";
|
|
31
32
|
import { sendMessageWeixin, StreamingMarkdownFilter } from "./messaging/send.js";
|
|
32
33
|
import { downloadRemoteImageToTemp } from "./cdn/upload.js";
|
|
@@ -109,7 +110,6 @@ async function sendWeixinOutbound(params: {
|
|
|
109
110
|
text: string;
|
|
110
111
|
accountId?: string | null;
|
|
111
112
|
contextToken?: string;
|
|
112
|
-
mediaUrl?: string;
|
|
113
113
|
}): Promise<{ channel: string; messageId: string }> {
|
|
114
114
|
const account = resolveWeixinAccount(params.cfg, params.accountId);
|
|
115
115
|
const aLog = logger.withAccount(account.accountId);
|
|
@@ -123,13 +123,31 @@ async function sendWeixinOutbound(params: {
|
|
|
123
123
|
}
|
|
124
124
|
const f = new StreamingMarkdownFilter();
|
|
125
125
|
const rawText = params.text ?? "";
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
126
|
+
let filteredText = f.feed(rawText) + f.flush();
|
|
127
|
+
|
|
128
|
+
const sendingResult = await applyWeixinMessageSendingHook({
|
|
129
|
+
to: params.to,
|
|
130
|
+
text: filteredText,
|
|
131
|
+
accountId: account.accountId,
|
|
132
|
+
});
|
|
133
|
+
if (sendingResult.cancelled) {
|
|
134
|
+
aLog.info(`sendWeixinOutbound: cancelled by message_sending hook to=${params.to}`);
|
|
135
|
+
return { channel: "openclaw-weixin", messageId: "" };
|
|
136
|
+
}
|
|
137
|
+
filteredText = sendingResult.text;
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const result = await sendMessageWeixin({ to: params.to, text: filteredText, opts: {
|
|
141
|
+
baseUrl: account.baseUrl,
|
|
142
|
+
token: account.token,
|
|
143
|
+
contextToken: params.contextToken,
|
|
144
|
+
}});
|
|
145
|
+
emitWeixinMessageSent({ to: params.to, content: filteredText, success: true, accountId: account.accountId });
|
|
146
|
+
return { channel: "openclaw-weixin", messageId: result.messageId };
|
|
147
|
+
} catch (err) {
|
|
148
|
+
emitWeixinMessageSent({ to: params.to, content: filteredText, success: false, error: String(err), accountId: account.accountId });
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
133
151
|
}
|
|
134
152
|
|
|
135
153
|
export const weixinPlugin: ChannelPlugin<ResolvedWeixinAccount> = {
|
|
@@ -215,6 +233,19 @@ export const weixinPlugin: ChannelPlugin<ResolvedWeixinAccount> = {
|
|
|
215
233
|
}
|
|
216
234
|
|
|
217
235
|
const mediaUrl = ctx.mediaUrl;
|
|
236
|
+
let text = ctx.text ?? "";
|
|
237
|
+
|
|
238
|
+
const sendingResult = await applyWeixinMessageSendingHook({
|
|
239
|
+
to: ctx.to,
|
|
240
|
+
text,
|
|
241
|
+
accountId: account.accountId,
|
|
242
|
+
mediaUrl,
|
|
243
|
+
});
|
|
244
|
+
if (sendingResult.cancelled) {
|
|
245
|
+
aLog.info(`sendMedia: cancelled by message_sending hook to=${ctx.to}`);
|
|
246
|
+
return { channel: "openclaw-weixin", messageId: "" };
|
|
247
|
+
}
|
|
248
|
+
text = sendingResult.text;
|
|
218
249
|
|
|
219
250
|
if (mediaUrl && (isLocalFilePath(mediaUrl) || isRemoteUrl(mediaUrl))) {
|
|
220
251
|
let filePath: string;
|
|
@@ -227,24 +258,35 @@ export const weixinPlugin: ChannelPlugin<ResolvedWeixinAccount> = {
|
|
|
227
258
|
aLog.debug(`sendMedia: remote image downloaded to ${filePath}`);
|
|
228
259
|
}
|
|
229
260
|
const contextToken = getContextToken(account.accountId, ctx.to);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
261
|
+
try {
|
|
262
|
+
const result = await sendWeixinMediaFile({
|
|
263
|
+
filePath,
|
|
264
|
+
to: ctx.to,
|
|
265
|
+
text,
|
|
266
|
+
opts: { baseUrl: account.baseUrl, token: account.token, contextToken },
|
|
267
|
+
cdnBaseUrl: account.cdnBaseUrl,
|
|
268
|
+
});
|
|
269
|
+
emitWeixinMessageSent({ to: ctx.to, content: text, success: true, accountId: account.accountId });
|
|
270
|
+
return { channel: "openclaw-weixin", messageId: result.messageId };
|
|
271
|
+
} catch (err) {
|
|
272
|
+
emitWeixinMessageSent({ to: ctx.to, content: text, success: false, error: String(err), accountId: account.accountId });
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
238
275
|
}
|
|
239
276
|
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
to: ctx.to,
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
277
|
+
const contextToken = getContextToken(account.accountId, ctx.to);
|
|
278
|
+
try {
|
|
279
|
+
const result = await sendMessageWeixin({ to: ctx.to, text, opts: {
|
|
280
|
+
baseUrl: account.baseUrl,
|
|
281
|
+
token: account.token,
|
|
282
|
+
contextToken,
|
|
283
|
+
}});
|
|
284
|
+
emitWeixinMessageSent({ to: ctx.to, content: text, success: true, accountId: account.accountId });
|
|
285
|
+
return { channel: "openclaw-weixin", messageId: result.messageId };
|
|
286
|
+
} catch (err) {
|
|
287
|
+
emitWeixinMessageSent({ to: ctx.to, content: text, success: false, error: String(err), accountId: account.accountId });
|
|
288
|
+
throw err;
|
|
289
|
+
}
|
|
248
290
|
},
|
|
249
291
|
},
|
|
250
292
|
status: {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fireAndForgetHook,
|
|
3
|
+
buildCanonicalSentMessageHookContext,
|
|
4
|
+
toPluginMessageContext,
|
|
5
|
+
toPluginMessageSentEvent,
|
|
6
|
+
} from "openclaw/plugin-sdk/hook-runtime";
|
|
7
|
+
import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
|
|
8
|
+
|
|
9
|
+
import { logger } from "../util/logger.js";
|
|
10
|
+
|
|
11
|
+
const CHANNEL_ID = "openclaw-weixin";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Run message_sending hook before sending.
|
|
15
|
+
* Returns the (possibly modified) text content plus a cancelled flag.
|
|
16
|
+
* Hook errors are caught and logged — sending proceeds regardless.
|
|
17
|
+
*/
|
|
18
|
+
export async function applyWeixinMessageSendingHook(params: {
|
|
19
|
+
to: string;
|
|
20
|
+
text: string;
|
|
21
|
+
accountId?: string;
|
|
22
|
+
mediaUrl?: string;
|
|
23
|
+
}): Promise<{ cancelled: boolean; text: string }> {
|
|
24
|
+
const hookRunner = getGlobalHookRunner();
|
|
25
|
+
if (!hookRunner?.hasHooks("message_sending")) {
|
|
26
|
+
return { cancelled: false, text: params.text };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const hookResult = await hookRunner.runMessageSending(
|
|
30
|
+
{
|
|
31
|
+
to: params.to,
|
|
32
|
+
content: params.text,
|
|
33
|
+
metadata: {
|
|
34
|
+
channel: CHANNEL_ID,
|
|
35
|
+
accountId: params.accountId,
|
|
36
|
+
...(params.mediaUrl ? { mediaUrls: [params.mediaUrl] } : {}),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{ channelId: CHANNEL_ID, accountId: params.accountId },
|
|
40
|
+
);
|
|
41
|
+
if (hookResult?.cancel) {
|
|
42
|
+
return { cancelled: true, text: params.text };
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
cancelled: false,
|
|
46
|
+
text: hookResult?.content ?? params.text,
|
|
47
|
+
};
|
|
48
|
+
} catch (err) {
|
|
49
|
+
logger.warn(`message_sending hook error, proceeding with send: ${String(err)}`);
|
|
50
|
+
return { cancelled: false, text: params.text };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fire message_sent hook (fire-and-forget) after a send attempt.
|
|
56
|
+
*/
|
|
57
|
+
export function emitWeixinMessageSent(params: {
|
|
58
|
+
to: string;
|
|
59
|
+
content: string;
|
|
60
|
+
success: boolean;
|
|
61
|
+
error?: string;
|
|
62
|
+
accountId?: string;
|
|
63
|
+
}): void {
|
|
64
|
+
const hookRunner = getGlobalHookRunner();
|
|
65
|
+
if (!hookRunner?.hasHooks("message_sent")) return;
|
|
66
|
+
const canonical = buildCanonicalSentMessageHookContext({
|
|
67
|
+
to: params.to,
|
|
68
|
+
content: params.content,
|
|
69
|
+
success: params.success,
|
|
70
|
+
error: params.error,
|
|
71
|
+
channelId: CHANNEL_ID,
|
|
72
|
+
accountId: params.accountId,
|
|
73
|
+
conversationId: params.to,
|
|
74
|
+
});
|
|
75
|
+
fireAndForgetHook(
|
|
76
|
+
Promise.resolve(
|
|
77
|
+
hookRunner!.runMessageSent(
|
|
78
|
+
toPluginMessageSentEvent(canonical),
|
|
79
|
+
toPluginMessageContext(canonical),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
"weixin: message_sent plugin hook failed",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -20,6 +20,7 @@ import { redactBody, redactToken } from "../util/redact.js";
|
|
|
20
20
|
|
|
21
21
|
import { isDebugMode } from "./debug-mode.js";
|
|
22
22
|
import { sendWeixinErrorNotice } from "./error-notice.js";
|
|
23
|
+
import { applyWeixinMessageSendingHook, emitWeixinMessageSent } from "./outbound-hooks.js";
|
|
23
24
|
import {
|
|
24
25
|
setContextToken,
|
|
25
26
|
weixinMessageToMsgContext,
|
|
@@ -311,7 +312,7 @@ export async function processOneMessage(
|
|
|
311
312
|
typingCallbacks,
|
|
312
313
|
deliver: async (payload) => {
|
|
313
314
|
const rawText = payload.text ?? "";
|
|
314
|
-
|
|
315
|
+
let text = (() => {
|
|
315
316
|
const f = new StreamingMarkdownFilter();
|
|
316
317
|
return f.feed(rawText) + f.flush();
|
|
317
318
|
})();
|
|
@@ -330,11 +331,22 @@ export async function processOneMessage(
|
|
|
330
331
|
});
|
|
331
332
|
}
|
|
332
333
|
|
|
334
|
+
const sendingResult = await applyWeixinMessageSendingHook({
|
|
335
|
+
to: ctx.To,
|
|
336
|
+
text,
|
|
337
|
+
accountId: deps.accountId,
|
|
338
|
+
mediaUrl,
|
|
339
|
+
});
|
|
340
|
+
if (sendingResult.cancelled) {
|
|
341
|
+
logger.info(`outbound: cancelled by message_sending hook to=${ctx.To}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
text = sendingResult.text;
|
|
345
|
+
|
|
333
346
|
try {
|
|
334
347
|
if (mediaUrl) {
|
|
335
348
|
let filePath: string;
|
|
336
349
|
if (!mediaUrl.includes("://") || mediaUrl.startsWith("file://")) {
|
|
337
|
-
// Local path: absolute, relative, or file:// URL
|
|
338
350
|
if (mediaUrl.startsWith("file://")) {
|
|
339
351
|
filePath = new URL(mediaUrl).pathname;
|
|
340
352
|
} else if (!path.isAbsolute(mediaUrl)) {
|
|
@@ -357,6 +369,7 @@ export async function processOneMessage(
|
|
|
357
369
|
token: deps.token,
|
|
358
370
|
contextToken,
|
|
359
371
|
}});
|
|
372
|
+
emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId });
|
|
360
373
|
logger.info(`outbound: text sent to=${ctx.To}`);
|
|
361
374
|
return;
|
|
362
375
|
}
|
|
@@ -367,6 +380,7 @@ export async function processOneMessage(
|
|
|
367
380
|
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
|
|
368
381
|
cdnBaseUrl: deps.cdnBaseUrl,
|
|
369
382
|
});
|
|
383
|
+
emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId });
|
|
370
384
|
logger.info(`outbound: media sent OK to=${ctx.To}`);
|
|
371
385
|
} else {
|
|
372
386
|
logger.debug(`outbound: sending text message to=${ctx.To}`);
|
|
@@ -375,9 +389,11 @@ export async function processOneMessage(
|
|
|
375
389
|
token: deps.token,
|
|
376
390
|
contextToken,
|
|
377
391
|
}});
|
|
392
|
+
emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId });
|
|
378
393
|
logger.info(`outbound: text sent OK to=${ctx.To}`);
|
|
379
394
|
}
|
|
380
395
|
} catch (err) {
|
|
396
|
+
emitWeixinMessageSent({ to: ctx.To, content: text, success: false, error: String(err), accountId: deps.accountId });
|
|
381
397
|
logger.error(
|
|
382
398
|
`outbound: FAILED to=${ctx.To} mediaUrl=${mediaUrl ?? "none"} err=${String(err)} stack=${(err as Error).stack ?? ""}`,
|
|
383
399
|
);
|