openclaw-weixin 3.1.6 → 3.2.0

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.
@@ -0,0 +1,8 @@
1
+ export function resolveWeixinBlockStreamingEnabled(cfg, accountId, fallbackAccountId) {
2
+ const section = cfg.channels?.["openclaw-weixin"];
3
+ return (section?.accounts?.[accountId]?.blockStreaming ??
4
+ (fallbackAccountId ? section?.accounts?.[fallbackAccountId]?.blockStreaming : undefined) ??
5
+ section?.blockStreaming ??
6
+ true);
7
+ }
8
+ //# sourceMappingURL=block-streaming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-streaming.js","sourceRoot":"","sources":["../../../src/config/block-streaming.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,kCAAkC,CAChD,GAAmB,EACnB,SAAiB,EACjB,iBAA0B;IAE1B,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC,iBAAiB,CAA2C,CAAC;IAC5F,OAAO,CACL,OAAO,EAAE,QAAQ,EAAE,CAAC,SAAS,CAAC,EAAE,cAAc;QAC9C,CAAC,iBAAiB,CAAC,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,CAAC,iBAAiB,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC;QACxF,OAAO,EAAE,cAAc;QACvB,IAAI,CACL,CAAC;AACJ,CAAC"}
@@ -9,6 +9,10 @@ const weixinAccountProperties = {
9
9
  baseUrl: { type: "string", default: DEFAULT_BASE_URL },
10
10
  cdnBaseUrl: { type: "string", default: CDN_BASE_URL },
11
11
  routeTag: { type: "number" },
12
+ blockStreaming: {
13
+ type: "boolean",
14
+ description: "Send completed text blocks before the final reply.",
15
+ },
12
16
  };
13
17
  const weixinAccountSchema = {
14
18
  type: "object",
@@ -24,6 +28,11 @@ const replyProgressMessagesSchema = {
24
28
  default: true,
25
29
  description: "Send structured tool-call progress messages.",
26
30
  };
31
+ const blockStreamingSchema = {
32
+ type: "boolean",
33
+ default: true,
34
+ description: "Send completed text blocks before the final reply.",
35
+ };
27
36
  /** Top-level weixin config schema (token is stored in credentials file, not config). */
28
37
  const weixinChannelConfigJsonSchema = {
29
38
  type: "object",
@@ -37,6 +46,7 @@ const weixinChannelConfigJsonSchema = {
37
46
  additionalProperties: weixinAccountSchema,
38
47
  },
39
48
  botAgent: botAgentSchema,
49
+ blockStreaming: blockStreamingSchema,
40
50
  replyProgressMessages: replyProgressMessagesSchema,
41
51
  /** ISO 8601; bumped on each successful login to refresh gateway config from disk. */
42
52
  channelConfigUpdatedAt: { type: "string" },
@@ -1 +1 @@
1
- {"version":3,"file":"config-schema.js","sourceRoot":"","sources":["../../../src/config/config-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,MAAM,2CAA2C,CAAC;AAEzF,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAErE,MAAM,cAAc,GAAG;IACrB,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;CACvC,CAAC;AAEX,MAAM,uBAAuB,GAAG;IAC9B,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IACxB,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IAC5B,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,gBAAgB,EAAE;IACtD,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE;IACrD,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;CACpB,CAAC;AAEX,MAAM,mBAAmB,GAAG;IAC1B,IAAI,EAAE,QAAQ;IACd,oBAAoB,EAAE,IAAI;IAC1B,UAAU,EAAE,uBAAuB;CAC3B,CAAC;AAEX,MAAM,cAAc,GAAG;IACrB,IAAI,EAAE,QAAQ;IACd,WAAW,EAAE,+DAA+D;CACpE,CAAC;AAEX,MAAM,2BAA2B,GAAG;IAClC,IAAI,EAAE,SAAS;IACf,OAAO,EAAE,IAAI;IACb,WAAW,EAAE,8CAA8C;CACnD,CAAC;AAEX,wFAAwF;AACxF,MAAM,6BAA6B,GAAG;IACpC,IAAI,EAAE,QAAQ;IACd,oBAAoB,EAAE,IAAI;IAC1B,UAAU,EAAE;QACV,GAAG,uBAAuB;QAC1B,iFAAiF;QACjF,QAAQ,EAAE,cAAc;QACxB,QAAQ,EAAE;YACR,IAAI,EAAE,QAAQ;YACd,oBAAoB,EAAE,mBAAmB;SAC1C;QACD,QAAQ,EAAE,cAAc;QACxB,qBAAqB,EAAE,2BAA2B;QAClD,qFAAqF;QACrF,sBAAsB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;KAC3C;CACO,CAAC;AAEX,MAAM,CAAC,MAAM,yBAAyB,GAAG,4BAA4B,CAAC,6BAA6B,CAAC,CAAC"}
1
+ {"version":3,"file":"config-schema.js","sourceRoot":"","sources":["../../../src/config/config-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,4BAA4B,EAAE,MAAM,2CAA2C,CAAC;AAEzF,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAErE,MAAM,cAAc,GAAG;IACrB,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;CACvC,CAAC;AAEX,MAAM,uBAAuB,GAAG;IAC9B,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IACxB,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;IAC5B,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,gBAAgB,EAAE;IACtD,UAAU,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE;IACrD,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC5B,cAAc,EAAE;QACd,IAAI,EAAE,SAAS;QACf,WAAW,EAAE,oDAAoD;KAClE;CACO,CAAC;AAEX,MAAM,mBAAmB,GAAG;IAC1B,IAAI,EAAE,QAAQ;IACd,oBAAoB,EAAE,IAAI;IAC1B,UAAU,EAAE,uBAAuB;CAC3B,CAAC;AAEX,MAAM,cAAc,GAAG;IACrB,IAAI,EAAE,QAAQ;IACd,WAAW,EAAE,+DAA+D;CACpE,CAAC;AAEX,MAAM,2BAA2B,GAAG;IAClC,IAAI,EAAE,SAAS;IACf,OAAO,EAAE,IAAI;IACb,WAAW,EAAE,8CAA8C;CACnD,CAAC;AAEX,MAAM,oBAAoB,GAAG;IAC3B,IAAI,EAAE,SAAS;IACf,OAAO,EAAE,IAAI;IACb,WAAW,EAAE,oDAAoD;CACzD,CAAC;AAEX,wFAAwF;AACxF,MAAM,6BAA6B,GAAG;IACpC,IAAI,EAAE,QAAQ;IACd,oBAAoB,EAAE,IAAI;IAC1B,UAAU,EAAE;QACV,GAAG,uBAAuB;QAC1B,iFAAiF;QACjF,QAAQ,EAAE,cAAc;QACxB,QAAQ,EAAE;YACR,IAAI,EAAE,QAAQ;YACd,oBAAoB,EAAE,mBAAmB;SAC1C;QACD,QAAQ,EAAE,cAAc;QACxB,cAAc,EAAE,oBAAoB;QACpC,qBAAqB,EAAE,2BAA2B;QAClD,qFAAqF;QACrF,sBAAsB,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;KAC3C;CACO,CAAC;AAEX,MAAM,CAAC,MAAM,yBAAyB,GAAG,4BAA4B,CAAC,6BAA6B,CAAC,CAAC"}
@@ -0,0 +1,102 @@
1
+ import { sendWeixinWithHooks } from "./outbound-hooks.js";
2
+ /** Only the two public inbound contracts are supported; a failed dispatch is never replayed. */
3
+ export async function dispatchWeixinInboundTurn(params) {
4
+ const { channelRuntime, route, onReplyAdmitted, onReplyDeferred, onDeferredComplete, ...turn } = params;
5
+ const inbound = channelRuntime.inbound;
6
+ if (!inbound) {
7
+ throw new Error("weixin: host inbound runtime is missing");
8
+ }
9
+ let admitted = false;
10
+ const admit = () => {
11
+ if (admitted)
12
+ return;
13
+ admitted = true;
14
+ onReplyAdmitted?.();
15
+ };
16
+ const defer = () => {
17
+ onReplyDeferred?.();
18
+ admit();
19
+ };
20
+ const onAgentRunStart = turn.replyOptions?.onAgentRunStart;
21
+ const replyOptions = {
22
+ ...turn.replyOptions,
23
+ onAgentRunStart: (...args) => {
24
+ admit();
25
+ return onAgentRunStart?.(...args);
26
+ },
27
+ };
28
+ if ("dispatch" in inbound) {
29
+ if (typeof inbound.dispatch !== "function") {
30
+ throw new Error("weixin: host inbound.dispatch is not callable");
31
+ }
32
+ const delivery = { ...turn.delivery, observeMessageSent: true };
33
+ const routedReplyOptions = {
34
+ ...replyOptions,
35
+ turnAdoptionLifecycle: {
36
+ onAdopted: admit,
37
+ onDeferred: defer,
38
+ onSettled: onDeferredComplete,
39
+ },
40
+ };
41
+ return await inbound.dispatch({
42
+ ...turn,
43
+ route,
44
+ delivery,
45
+ replyOptions: routedReplyOptions,
46
+ });
47
+ }
48
+ if (typeof inbound.dispatchReply !== "function" ||
49
+ typeof channelRuntime.session?.resolveStorePath !== "function" ||
50
+ typeof channelRuntime.session?.recordInboundSession !== "function" ||
51
+ typeof channelRuntime.reply?.dispatchReplyWithBufferedBlockDispatcher !== "function") {
52
+ throw new Error("weixin: host legacy inbound contract is incomplete");
53
+ }
54
+ const to = turn.ctxPayload.To;
55
+ if (typeof to !== "string") {
56
+ throw new Error("weixin: inbound reply target is missing");
57
+ }
58
+ const hookContext = {
59
+ to,
60
+ accountId: turn.accountId,
61
+ runId: turn.replyOptions?.runId,
62
+ sessionKey: route.sessionKey,
63
+ };
64
+ const { preparePayload, ...rawDelivery } = turn.delivery;
65
+ const delivery = {
66
+ ...rawDelivery,
67
+ deliver: async (payload, info) => {
68
+ // Old helpers do not preserve SDK payload metadata across a preparePayload clone.
69
+ // Prepare only inside provider delivery so the host keeps its original payload.
70
+ const prepared = preparePayload ? await preparePayload(payload, info) : payload;
71
+ if (prepared === null)
72
+ return { visibleReplySent: false };
73
+ return sendWeixinWithHooks({
74
+ ...hookContext,
75
+ text: prepared.text ?? "",
76
+ mediaUrl: prepared.mediaUrl ?? prepared.mediaUrls?.[0],
77
+ }, (text) => rawDelivery.deliver({ ...prepared, text }, info));
78
+ },
79
+ };
80
+ const legacyReplyOptions = {
81
+ ...replyOptions,
82
+ queuedFollowupLifecycle: { onEnqueued: defer, onComplete: onDeferredComplete },
83
+ onTurnAdopted: admit,
84
+ };
85
+ return await inbound.dispatchReply({
86
+ ...turn,
87
+ agentId: route.agentId,
88
+ routeSessionKey: route.sessionKey,
89
+ storePath: channelRuntime.session.resolveStorePath(turn.cfg.session?.store, { agentId: route.agentId }),
90
+ recordInboundSession: channelRuntime.session.recordInboundSession,
91
+ dispatchReplyWithBufferedBlockDispatcher: channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher,
92
+ dispatcherOptions: {
93
+ ...turn.dispatcherOptions,
94
+ // Replace the legacy SDK's text-only modifier; our provider wrapper owns
95
+ // message_sending after preparation, including captionless media.
96
+ beforeDeliver: turn.dispatcherOptions?.beforeDeliver ?? ((payload) => payload),
97
+ },
98
+ delivery,
99
+ replyOptions: legacyReplyOptions,
100
+ });
101
+ }
102
+ //# sourceMappingURL=inbound-turn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbound-turn.js","sourceRoot":"","sources":["../../../src/messaging/inbound-turn.ts"],"names":[],"mappings":"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"}
@@ -3,8 +3,34 @@ import { getGlobalHookRunner } from "openclaw/plugin-sdk/plugin-runtime";
3
3
  import { logger } from "../util/logger.js";
4
4
  import { redactError } from "../util/redact.js";
5
5
  const CHANNEL_ID = "openclaw-weixin";
6
+ /** Local sends share cancellation and settlement; host-managed sends bypass this wrapper. */
7
+ export async function sendWeixinWithHooks(params, send) {
8
+ const sending = await applyWeixinMessageSendingHook(params);
9
+ if (sending.cancelled) {
10
+ logger.info("outbound: cancelled by message_sending hook");
11
+ return { visibleReplySent: false };
12
+ }
13
+ let result;
14
+ try {
15
+ result = await send(sending.text);
16
+ }
17
+ catch (error) {
18
+ emitWeixinMessageSent({ ...params, content: sending.text, success: false, error: redactError(error) });
19
+ throw error;
20
+ }
21
+ if (result?.visibleReplySent !== false) {
22
+ emitWeixinMessageSent({
23
+ ...params,
24
+ content: sending.text,
25
+ success: true,
26
+ messageId: result?.messageIds?.find((id) => id.trim()),
27
+ });
28
+ }
29
+ return result;
30
+ }
6
31
  /**
7
- * Run message_sending hook before sending.
32
+ * Local hook boundary for legacy inbound replies and independent debug sends.
33
+ * Host-managed outbound adapters and routed inbound delivery must not call it.
8
34
  * Returns the (possibly modified) text content plus a cancelled flag.
9
35
  * Hook errors are caught and logged — sending proceeds regardless.
10
36
  */
@@ -23,7 +49,11 @@ export async function applyWeixinMessageSendingHook(params) {
23
49
  runId: params.runId,
24
50
  ...(params.mediaUrl ? { mediaUrls: [params.mediaUrl] } : {}),
25
51
  },
26
- }, { channelId: CHANNEL_ID, accountId: params.accountId });
52
+ }, {
53
+ channelId: CHANNEL_ID,
54
+ accountId: params.accountId,
55
+ ...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
56
+ });
27
57
  if (hookResult?.cancel) {
28
58
  return { cancelled: true, text: params.text };
29
59
  }
@@ -53,6 +83,8 @@ export function emitWeixinMessageSent(params) {
53
83
  accountId: params.accountId,
54
84
  conversationId: params.to,
55
85
  runId: params.runId,
86
+ sessionKey: params.sessionKey,
87
+ messageId: params.messageId,
56
88
  });
57
89
  fireAndForgetHook(Promise.resolve(hookRunner.runMessageSent(toPluginMessageSentEvent(canonical), toPluginMessageContext(canonical))), "weixin: message_sent plugin hook failed");
58
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-hooks.js","sourceRoot":"","sources":["../../../src/messaging/outbound-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oCAAoC,EACpC,iBAAiB,EACjB,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,oCAAoC,CAAC;AAEzE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,UAAU,GAAG,iBAAiB,CAAC;AAErC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CAAC,MAMnD;IACC,MAAM,UAAU,GAAG,mBAAmB,EAAE,CAAC;IACzC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;IACjD,CAAC;IACD,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,UAAU,CAAC,iBAAiB,CACnD;YACE,EAAE,EAAE,MAAM,CAAC,EAAE;YACb,OAAO,EAAE,MAAM,CAAC,IAAI;YACpB,QAAQ,EAAE;gBACR,OAAO,EAAE,UAAU;gBACnB,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC7D;SACF,EACD,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CACvD,CAAC;QACF,IAAI,UAAU,EAAE,MAAM,EAAE,CAAC;YACvB,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;QAChD,CAAC;QACD,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,IAAI,EAAE,UAAU,EAAE,OAAO,IAAI,MAAM,CAAC,IAAI;SACzC,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,qDAAqD,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACrF,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;IACjD,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAOrC;IACC,MAAM,UAAU,GAAG,mBAAmB,EAAE,CAAC;IACzC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,cAAc,CAAC;QAAE,OAAO;IAClD,MAAM,SAAS,GAAG,oCAAoC,CAAC;QACrD,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,SAAS,EAAE,UAAU;QACrB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,cAAc,EAAE,MAAM,CAAC,EAAE;QACzB,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC,CAAC;IACH,iBAAiB,CACf,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,wBAAwB,CAAC,SAAS,CAAC,EAAE,sBAAsB,CAAC,SAAS,CAAC,CAAC,CAAC,EAClH,yCAAyC,CAC1C,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"outbound-hooks.js","sourceRoot":"","sources":["../../../src/messaging/outbound-hooks.ts"],"names":[],"mappings":"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"}
@@ -9,6 +9,7 @@ import { MessageItemType, TypingStatus } from "../api/types.js";
9
9
  import { loadWeixinAccount } from "../auth/accounts.js";
10
10
  import { readFrameworkAllowFromList } from "../auth/pairing.js";
11
11
  import { downloadRemoteImageToTemp } from "../cdn/upload.js";
12
+ import { resolveWeixinBlockStreamingEnabled } from "../config/block-streaming.js";
12
13
  import { resolveReplyProgressMessagesEnabled } from "../config/reply-progress.js";
13
14
  import { downloadMediaFromItem } from "../media/media-download.js";
14
15
  import { logger } from "../util/logger.js";
@@ -16,8 +17,9 @@ import { redactError, redactToken } from "../util/redact.js";
16
17
  import { isDebugMode } from "./debug-mode.js";
17
18
  import { sendWeixinErrorNotice } from "./error-notice.js";
18
19
  import { getContextTokenFromMsgContext, isMediaItem, weixinMessageToMsgContext } from "./inbound.js";
20
+ import { dispatchWeixinInboundTurn } from "./inbound-turn.js";
19
21
  import { StreamingMarkdownFilter } from "./markdown-filter.js";
20
- import { applyWeixinMessageSendingHook, emitWeixinMessageSent } from "./outbound-hooks.js";
22
+ import { sendWeixinWithHooks } from "./outbound-hooks.js";
21
23
  import { WeixinReplyProgressSender } from "./reply-progress-sender.js";
22
24
  import { sendMessageWeixin } from "./send.js";
23
25
  import { sendWeixinMediaFile } from "./send-media.js";
@@ -140,7 +142,6 @@ export async function processOneMessage(full, deps) {
140
142
  const ctx = weixinMessageToMsgContext(full, deps.accountId, mediaOpts);
141
143
  // --- Framework command authorization ---
142
144
  const rawBody = textBody.trim() || (ctx.Body?.trim() ?? "");
143
- ctx.CommandBody = rawBody;
144
145
  const { senderAllowedForCommands, commandAuthorized } = await resolveSenderCommandAuthorizationWithRuntime({
145
146
  cfg: deps.config,
146
147
  rawBody,
@@ -169,108 +170,76 @@ export async function processOneMessage(full, deps) {
169
170
  logger.info(`authorization: dropping message from=${redactToken(senderId)} outcome=${directDmOutcome}`);
170
171
  return;
171
172
  }
172
- ctx.CommandAuthorized = commandAuthorized;
173
173
  logger.debug(`authorization: senderId=${redactToken(senderId, 6)} commandAuthorized=${String(commandAuthorized)} senderAllowed=${String(senderAllowedForCommands)}`);
174
174
  if (debug) {
175
175
  debugTrace.push("── 鉴权 ──", `│ auth: cmdAuthorized=${String(commandAuthorized)} senderAllowed=${String(senderAllowedForCommands)}`);
176
176
  debugTs.preDispatch = Date.now();
177
177
  }
178
- // Note: `route` was resolved earlier so downloadMedia could pick the
179
- // per-agent subdirectory. Session/storePath/dispatch below reuse the same
180
- // resolved object; no second routing call is needed.
181
- // Propagate the resolved session key into ctx so dispatchReplyFromConfig uses
182
- // the correct session (matching the dmScope from config) instead of falling back
183
- // to agent:main:main.
184
- ctx.SessionKey = route.sessionKey;
185
- const storePath = deps.channelRuntime.session.resolveStorePath(deps.config.session?.store, {
178
+ if (typeof deps.channelRuntime.inbound?.buildContext !== "function") {
179
+ throw new Error("weixin: host inbound.buildContext is missing");
180
+ }
181
+ const contextRoute = {
186
182
  agentId: route.agentId,
183
+ routeSessionKey: route.sessionKey,
184
+ ...("dmScope" in route ? { dmScope: route.dmScope } : {}),
185
+ };
186
+ const finalized = deps.channelRuntime.inbound.buildContext({
187
+ channel: "openclaw-weixin",
188
+ accountId: deps.accountId,
189
+ from: ctx.From,
190
+ sender: { id: senderId },
191
+ conversation: { kind: "direct", id: ctx.To },
192
+ route: contextRoute,
193
+ reply: { to: ctx.To },
194
+ message: { body: ctx.Body, rawBody: ctx.Body, bodyForAgent: ctx.Body, commandBody: rawBody },
195
+ messageId: ctx.MessageSid,
196
+ timestamp: ctx.Timestamp,
197
+ access: { commands: { authorized: commandAuthorized === true } },
198
+ media: ctx.MediaPath || ctx.MediaUrl
199
+ ? [{ path: ctx.MediaPath, url: ctx.MediaUrl, contentType: ctx.MediaType }]
200
+ : undefined,
187
201
  });
188
- const finalized = deps.channelRuntime.reply.finalizeInboundContext({
189
- ...ctx,
190
- BodyForAgent: ctx.Body,
191
- });
192
- logger.info(`inbound: from=${redactToken(finalized.From)} to=${redactToken(finalized.To)} bodyLen=${(finalized.Body ?? "").length} hasMedia=${Boolean(finalized.MediaPath ?? finalized.MediaUrl)}`);
193
- await deps.channelRuntime.session.recordInboundSession({
194
- storePath,
195
- sessionKey: route.sessionKey,
196
- ctx: finalized,
197
- updateLastRoute: {
198
- sessionKey: route.mainSessionKey,
199
- channel: "openclaw-weixin",
200
- to: ctx.To,
201
- accountId: deps.accountId,
202
- },
203
- onRecordError: (err) => deps.errLog(`recordInboundSession: ${redactError(err)}`),
204
- });
205
- logger.debug(`recordInboundSession: done sessionKey=${redactToken(route.sessionKey, 6)}`);
202
+ logger.info(`inbound: from=${redactToken(finalized.From)} to=${redactToken(finalized.To)} bodyLen=${(finalized.Body ?? "").length} hasMedia=${Boolean(ctx.MediaPath ?? ctx.MediaUrl)}`);
206
203
  const contextToken = getContextTokenFromMsgContext(ctx);
207
204
  const runId = randomUUID();
208
- const replyProgressSender = resolveReplyProgressMessagesEnabled(deps.config)
209
- ? new WeixinReplyProgressSender({
210
- runId,
211
- to: ctx.To,
212
- accountId: deps.accountId,
213
- opts: {
214
- baseUrl: deps.baseUrl,
215
- token: deps.token,
216
- contextToken,
217
- },
218
- })
219
- : undefined;
205
+ const sendOptions = { baseUrl: deps.baseUrl, token: deps.token, contextToken, runId };
220
206
  const humanDelay = deps.channelRuntime.reply.resolveHumanDelayConfig(deps.config, route.agentId);
221
207
  const typingTicket = deps.typingTicket;
222
- const typingCallbacks = createTypingCallbacks({
223
- start: typingTicket
224
- ? () => sendTyping({
208
+ const updateTyping = async (status) => {
209
+ if (typingTicket) {
210
+ await sendTyping({
225
211
  baseUrl: deps.baseUrl,
226
212
  token: deps.token,
227
- body: {
228
- ilink_user_id: ctx.To,
229
- typing_ticket: typingTicket,
230
- status: TypingStatus.TYPING,
231
- },
232
- })
233
- : async () => { },
234
- stop: typingTicket
235
- ? () => sendTyping({
236
- baseUrl: deps.baseUrl,
237
- token: deps.token,
238
- body: {
239
- ilink_user_id: ctx.To,
240
- typing_ticket: typingTicket,
241
- status: TypingStatus.CANCEL,
242
- },
243
- })
244
- : async () => { },
213
+ body: { ilink_user_id: ctx.To, typing_ticket: typingTicket, status },
214
+ });
215
+ }
216
+ };
217
+ const typingCallbacks = createTypingCallbacks({
218
+ start: () => updateTyping(TypingStatus.TYPING),
219
+ stop: () => updateTyping(TypingStatus.CANCEL),
245
220
  onStartError: (err) => deps.log(`[weixin] typing send error: ${redactError(err)}`),
246
221
  onStopError: (err) => deps.log(`[weixin] typing cancel error: ${redactError(err)}`),
247
222
  keepaliveIntervalMs: 5000,
248
223
  });
224
+ const replyProgressSender = resolveReplyProgressMessagesEnabled(deps.config)
225
+ ? new WeixinReplyProgressSender({
226
+ runId,
227
+ to: ctx.To,
228
+ accountId: deps.accountId,
229
+ opts: sendOptions,
230
+ })
231
+ : undefined;
249
232
  /** Delivery records populated synchronously at deliver() entry, safe to read in finally. */
250
233
  const debugDeliveries = [];
251
- const { dispatcher, replyOptions, markDispatchIdle } = deps.channelRuntime.reply.createReplyDispatcherWithTyping({
252
- humanDelay,
253
- typingCallbacks,
234
+ const delivery = {
235
+ preparePayload: (payload) => {
236
+ const filter = new StreamingMarkdownFilter();
237
+ return { ...payload, text: filter.feed(payload.text ?? "") + filter.flush() };
238
+ },
254
239
  deliver: async (payload) => {
255
- const rawText = payload.text ?? "";
256
- let text = (() => {
257
- const f = new StreamingMarkdownFilter();
258
- return f.feed(rawText) + f.flush();
259
- })();
240
+ const text = payload.text ?? "";
260
241
  const mediaUrl = payload.mediaUrl ?? payload.mediaUrls?.[0];
261
242
  logger.info(`outbound: to=${redactToken(ctx.To)} contextToken=${redactToken(contextToken)} textLen=${text.length} mediaUrl=${mediaUrl ? "present" : "none"}`);
262
- const sendingResult = await applyWeixinMessageSendingHook({
263
- to: ctx.To,
264
- text,
265
- accountId: deps.accountId,
266
- mediaUrl,
267
- runId,
268
- });
269
- if (sendingResult.cancelled) {
270
- logger.info(`outbound: cancelled by message_sending hook to=${redactToken(ctx.To)}`);
271
- return;
272
- }
273
- text = sendingResult.text;
274
243
  if (debug) {
275
244
  debugDeliveries.push({
276
245
  textLen: text.length,
@@ -279,76 +248,39 @@ export async function processOneMessage(full, deps) {
279
248
  });
280
249
  }
281
250
  try {
282
- if (mediaUrl) {
283
- let filePath;
284
- if (!mediaUrl.includes("://") || mediaUrl.startsWith("file://")) {
285
- if (mediaUrl.startsWith("file://")) {
286
- filePath = new URL(mediaUrl).pathname;
287
- }
288
- else if (!path.isAbsolute(mediaUrl)) {
289
- filePath = path.resolve(mediaUrl);
290
- }
291
- else {
292
- filePath = mediaUrl;
293
- }
294
- logger.debug("outbound: local media path resolved");
295
- }
296
- else if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
251
+ const remote = mediaUrl?.startsWith("http://") || mediaUrl?.startsWith("https://");
252
+ let result;
253
+ if (mediaUrl && (!mediaUrl.includes("://") || mediaUrl.startsWith("file://") || remote)) {
254
+ let filePath = mediaUrl;
255
+ if (remote) {
297
256
  logger.debug("outbound: downloading remote media");
298
257
  filePath = await downloadRemoteImageToTemp(mediaUrl, MEDIA_OUTBOUND_TEMP_DIR);
299
258
  logger.debug("outbound: remote media downloaded");
300
259
  }
301
- else {
302
- logger.warn("outbound: unrecognized media URL scheme, sending text only");
303
- await sendMessageWeixin({
304
- to: ctx.To,
305
- text,
306
- opts: {
307
- baseUrl: deps.baseUrl,
308
- token: deps.token,
309
- contextToken,
310
- runId,
311
- },
312
- });
313
- emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId, runId });
314
- logger.info(`outbound: text sent to=${redactToken(ctx.To)}`);
315
- return;
260
+ else if (mediaUrl.startsWith("file://")) {
261
+ filePath = new URL(mediaUrl).pathname;
262
+ }
263
+ else if (!path.isAbsolute(mediaUrl)) {
264
+ filePath = path.resolve(mediaUrl);
316
265
  }
317
- await sendWeixinMediaFile({
266
+ result = await sendWeixinMediaFile({
318
267
  filePath,
319
268
  to: ctx.To,
320
269
  text,
321
- opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken, runId },
270
+ opts: sendOptions,
322
271
  cdnBaseUrl: deps.cdnBaseUrl,
323
272
  });
324
- emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId, runId });
325
273
  logger.info(`outbound: media sent OK to=${redactToken(ctx.To)}`);
326
274
  }
327
275
  else {
328
- logger.debug(`outbound: sending text message to=${redactToken(ctx.To, 6)}`);
329
- await sendMessageWeixin({
330
- to: ctx.To,
331
- text,
332
- opts: {
333
- baseUrl: deps.baseUrl,
334
- token: deps.token,
335
- contextToken,
336
- runId,
337
- },
338
- });
339
- emitWeixinMessageSent({ to: ctx.To, content: text, success: true, accountId: deps.accountId, runId });
276
+ if (mediaUrl)
277
+ logger.warn("outbound: unrecognized media URL scheme, sending text only");
278
+ result = await sendMessageWeixin({ to: ctx.To, text, opts: sendOptions });
340
279
  logger.info(`outbound: text sent OK to=${redactToken(ctx.To)}`);
341
280
  }
281
+ return { messageIds: [result.messageId], content: text };
342
282
  }
343
283
  catch (err) {
344
- emitWeixinMessageSent({
345
- to: ctx.To,
346
- content: text,
347
- success: false,
348
- error: redactError(err),
349
- accountId: deps.accountId,
350
- runId,
351
- });
352
284
  logger.error(`outbound: FAILED to=${redactToken(ctx.To)} hasMedia=${Boolean(mediaUrl)} err=${redactError(err)}`);
353
285
  throw err;
354
286
  }
@@ -376,42 +308,46 @@ export async function processOneMessage(full, deps) {
376
308
  errLog: deps.errLog,
377
309
  });
378
310
  },
379
- });
311
+ };
380
312
  let queuedFollowup = false;
381
- const dispatchReplyOptions = {
382
- ...replyOptions,
383
- ...(replyProgressSender?.replyOptions ?? {}),
384
- // Newer hosts use this marker for active-run admission; older hosts ignore it.
385
- queuedFollowupLifecycle: {
386
- onEnqueued: () => {
313
+ try {
314
+ logger.debug(`inbound.dispatch: starting agentId=${redactToken(route.agentId, 6)}`);
315
+ await dispatchWeixinInboundTurn({
316
+ channelRuntime: deps.channelRuntime,
317
+ channel: "openclaw-weixin",
318
+ accountId: deps.accountId,
319
+ cfg: deps.config,
320
+ route,
321
+ ctxPayload: finalized,
322
+ record: {
323
+ updateLastRoute: {
324
+ sessionKey: route.mainSessionKey,
325
+ channel: "openclaw-weixin",
326
+ to: ctx.To,
327
+ accountId: deps.accountId,
328
+ },
329
+ onRecordError: (err) => deps.errLog(`recordInboundSession: ${redactError(err)}`),
330
+ },
331
+ delivery,
332
+ dispatcherOptions: { humanDelay, typingCallbacks },
333
+ replyOptions: {
334
+ ...(replyProgressSender?.replyOptions ?? {}),
335
+ runId,
336
+ disableBlockStreaming: !resolveWeixinBlockStreamingEnabled(deps.config, routeAccountId, deps.accountId),
337
+ },
338
+ onReplyAdmitted: deps.onReplyAdmitted,
339
+ onReplyDeferred: () => {
387
340
  queuedFollowup = true;
388
- deps.onReplyAdmitted?.();
389
341
  },
390
- onComplete: () => void replyProgressSender?.finalize(),
391
- },
392
- onAgentRunStart: () => deps.onReplyAdmitted?.(),
393
- onTurnAdopted: deps.onReplyAdmitted,
394
- disableBlockStreaming: true,
395
- };
396
- logger.debug(`dispatchReplyFromConfig: starting agentId=${redactToken(route.agentId, 6)}`);
397
- try {
398
- await deps.channelRuntime.reply.withReplyDispatcher({
399
- dispatcher,
400
- run: () => deps.channelRuntime.reply.dispatchReplyFromConfig({
401
- ctx: finalized,
402
- cfg: deps.config,
403
- dispatcher,
404
- replyOptions: dispatchReplyOptions,
405
- }),
342
+ onDeferredComplete: () => void replyProgressSender?.finalize(),
406
343
  });
407
- logger.debug(`dispatchReplyFromConfig: done agentId=${redactToken(route.agentId, 6)}`);
344
+ logger.debug(`inbound.dispatch: done agentId=${redactToken(route.agentId, 6)}`);
408
345
  }
409
346
  catch (err) {
410
- logger.error(`dispatchReplyFromConfig: error agentId=${redactToken(route.agentId)} err=${redactError(err)}`);
347
+ logger.error(`inbound.dispatch: error agentId=${redactToken(route.agentId)} err=${redactError(err)}`);
411
348
  throw err;
412
349
  }
413
350
  finally {
414
- markDispatchIdle();
415
351
  if (!queuedFollowup)
416
352
  await replyProgressSender?.finalize();
417
353
  logger.info(`debug-check: account=${redactToken(deps.accountId)} debug=${String(debug)} hasContextToken=${Boolean(contextToken)}`);
@@ -436,45 +372,14 @@ export async function processOneMessage(full, deps) {
436
372
  debugTrace.push("── 耗时 ──", `├ 平台→插件: ${platformDelay}`, `├ 入站处理(auth+route+media): ${inboundProcessMs}ms (mediaDownload: ${mediaDownloadMs}ms)`, `├ AI生成+回复: ${aiMs}ms`, `├ 总耗时: ${totalTime}`, `└ eventTime: ${eventTs > 0 ? new Date(eventTs).toISOString() : "N/A"}`);
437
373
  const timingText = `⏱ Debug 全链路\n${debugTrace.join("\n")}`;
438
374
  logger.info(`debug-timing: sending to=${redactToken(ctx.To)}`);
439
- let debugSentContent;
440
375
  try {
441
- const debugSendingResult = await applyWeixinMessageSendingHook({
442
- to: ctx.To,
443
- text: timingText,
444
- accountId: deps.accountId,
445
- runId,
376
+ const result = await sendWeixinWithHooks({ to: ctx.To, text: timingText, accountId: deps.accountId, runId }, async (text) => {
377
+ await sendMessageWeixin({ to: ctx.To, text, opts: sendOptions });
446
378
  });
447
- if (debugSendingResult.cancelled) {
448
- logger.info(`debug-timing: cancelled by message_sending hook to=${redactToken(ctx.To)}`);
449
- }
450
- else {
451
- debugSentContent = debugSendingResult.text;
452
- await sendMessageWeixin({
453
- to: ctx.To,
454
- text: debugSentContent,
455
- opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken, runId },
456
- });
457
- emitWeixinMessageSent({
458
- to: ctx.To,
459
- content: debugSentContent,
460
- success: true,
461
- accountId: deps.accountId,
462
- runId,
463
- });
379
+ if (result?.visibleReplySent !== false)
464
380
  logger.info(`debug-timing: sent OK`);
465
- }
466
381
  }
467
382
  catch (debugErr) {
468
- if (debugSentContent !== undefined) {
469
- emitWeixinMessageSent({
470
- to: ctx.To,
471
- content: debugSentContent,
472
- success: false,
473
- error: redactError(debugErr),
474
- accountId: deps.accountId,
475
- runId,
476
- });
477
- }
478
383
  logger.error(`debug-timing: send FAILED err=${redactError(debugErr)}`);
479
384
  }
480
385
  }