@pawastation/wechat-kf 0.1.1

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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +291 -0
  3. package/README.zh-CN.md +401 -0
  4. package/dist/index.d.ts +27 -0
  5. package/dist/index.js +24 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/accounts.d.ts +37 -0
  8. package/dist/src/accounts.js +205 -0
  9. package/dist/src/accounts.js.map +1 -0
  10. package/dist/src/api.d.ts +29 -0
  11. package/dist/src/api.js +172 -0
  12. package/dist/src/api.js.map +1 -0
  13. package/dist/src/bot.d.ts +35 -0
  14. package/dist/src/bot.js +379 -0
  15. package/dist/src/bot.js.map +1 -0
  16. package/dist/src/channel.d.ts +113 -0
  17. package/dist/src/channel.js +183 -0
  18. package/dist/src/channel.js.map +1 -0
  19. package/dist/src/chunk-utils.d.ts +18 -0
  20. package/dist/src/chunk-utils.js +58 -0
  21. package/dist/src/chunk-utils.js.map +1 -0
  22. package/dist/src/config-schema.d.ts +56 -0
  23. package/dist/src/config-schema.js +38 -0
  24. package/dist/src/config-schema.js.map +1 -0
  25. package/dist/src/constants.d.ts +19 -0
  26. package/dist/src/constants.js +20 -0
  27. package/dist/src/constants.js.map +1 -0
  28. package/dist/src/crypto.d.ts +18 -0
  29. package/dist/src/crypto.js +80 -0
  30. package/dist/src/crypto.js.map +1 -0
  31. package/dist/src/fs-utils.d.ts +7 -0
  32. package/dist/src/fs-utils.js +13 -0
  33. package/dist/src/fs-utils.js.map +1 -0
  34. package/dist/src/monitor.d.ts +18 -0
  35. package/dist/src/monitor.js +131 -0
  36. package/dist/src/monitor.js.map +1 -0
  37. package/dist/src/outbound.d.ts +66 -0
  38. package/dist/src/outbound.js +234 -0
  39. package/dist/src/outbound.js.map +1 -0
  40. package/dist/src/reply-dispatcher.d.ts +40 -0
  41. package/dist/src/reply-dispatcher.js +120 -0
  42. package/dist/src/reply-dispatcher.js.map +1 -0
  43. package/dist/src/runtime.d.ts +130 -0
  44. package/dist/src/runtime.js +22 -0
  45. package/dist/src/runtime.js.map +1 -0
  46. package/dist/src/send-utils.d.ts +30 -0
  47. package/dist/src/send-utils.js +89 -0
  48. package/dist/src/send-utils.js.map +1 -0
  49. package/dist/src/send.d.ts +7 -0
  50. package/dist/src/send.js +13 -0
  51. package/dist/src/send.js.map +1 -0
  52. package/dist/src/token.d.ts +8 -0
  53. package/dist/src/token.js +57 -0
  54. package/dist/src/token.js.map +1 -0
  55. package/dist/src/types.d.ts +173 -0
  56. package/dist/src/types.js +3 -0
  57. package/dist/src/types.js.map +1 -0
  58. package/dist/src/unicode-format.d.ts +26 -0
  59. package/dist/src/unicode-format.js +157 -0
  60. package/dist/src/unicode-format.js.map +1 -0
  61. package/dist/src/webhook.d.ts +22 -0
  62. package/dist/src/webhook.js +138 -0
  63. package/dist/src/webhook.js.map +1 -0
  64. package/dist/src/wechat-kf-directives.d.ts +34 -0
  65. package/dist/src/wechat-kf-directives.js +65 -0
  66. package/dist/src/wechat-kf-directives.js.map +1 -0
  67. package/index.ts +32 -0
  68. package/openclaw.plugin.json +31 -0
  69. package/package.json +91 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Monitor — starts webhook server and manages lifecycle
3
+ *
4
+ * Single webhook server for the enterprise. KfIds are discovered dynamically
5
+ * from webhook callbacks. Polling fallback iterates all known kfids.
6
+ */
7
+ import { getChannelConfig, getKnownKfIds, loadKfIds } from "./accounts.js";
8
+ import { handleWebhookEvent } from "./bot.js";
9
+ import { getAccessToken } from "./token.js";
10
+ import { createWebhookServer } from "./webhook.js";
11
+ export async function startMonitor(ctx) {
12
+ const { cfg, runtime, abortSignal, stateDir, log } = ctx;
13
+ // Guard: if signal is already aborted, skip all resource creation
14
+ if (abortSignal?.aborted) {
15
+ log?.info("[wechat-kf] abort signal already triggered, skipping monitor start");
16
+ // Return a dummy server that is not listening
17
+ const dummyServer = createWebhookServer({
18
+ port: 0,
19
+ path: "/",
20
+ callbackToken: "",
21
+ encodingAESKey: "",
22
+ corpId: "",
23
+ onEvent: async () => { },
24
+ });
25
+ return dummyServer;
26
+ }
27
+ const config = getChannelConfig(cfg);
28
+ const { corpId, appSecret, token, encodingAESKey } = config;
29
+ const webhookPort = config.webhookPort ?? 9999;
30
+ const webhookPath = config.webhookPath ?? "/wechat-kf";
31
+ if (!corpId || !appSecret || !token || !encodingAESKey) {
32
+ throw new Error("[wechat-kf] missing required config fields (corpId, appSecret, token, encodingAESKey)");
33
+ }
34
+ // Load previously discovered kfids
35
+ await loadKfIds(stateDir);
36
+ // Validate access token on startup
37
+ try {
38
+ await getAccessToken(corpId, appSecret);
39
+ log?.info(`[wechat-kf] access_token validated`);
40
+ }
41
+ catch (err) {
42
+ log?.warn?.(`[wechat-kf] access_token validation failed (will retry on first message): ${err}`);
43
+ }
44
+ const botCtx = { cfg, runtime, stateDir, log };
45
+ const server = createWebhookServer({
46
+ port: webhookPort,
47
+ path: webhookPath,
48
+ callbackToken: token,
49
+ encodingAESKey,
50
+ corpId,
51
+ onEvent: async (kfId, syncToken) => {
52
+ if (!kfId) {
53
+ log?.error("[wechat-kf] webhook callback missing OpenKfId, ignoring");
54
+ return;
55
+ }
56
+ try {
57
+ await handleWebhookEvent(botCtx, kfId, syncToken);
58
+ }
59
+ catch (err) {
60
+ log?.error(`[wechat-kf:${kfId}] event processing error: ${err}`);
61
+ }
62
+ },
63
+ });
64
+ await new Promise((resolve, reject) => {
65
+ const timeout = setTimeout(() => reject(new Error(`[wechat-kf] server.listen(:${webhookPort}) timed out`)), 10_000);
66
+ const onError = (err) => {
67
+ clearTimeout(timeout);
68
+ reject(err);
69
+ };
70
+ server.once("error", onError);
71
+ server.listen(webhookPort, () => {
72
+ clearTimeout(timeout);
73
+ server.removeListener("error", onError);
74
+ log?.info(`[wechat-kf] webhook listening on :${webhookPort}${webhookPath}`);
75
+ resolve();
76
+ });
77
+ });
78
+ // ── Polling fallback ──
79
+ // Poll sync_msg for each known kfid as fallback
80
+ const POLL_INTERVAL_MS = 30000;
81
+ let pollTimer = null;
82
+ let polling = false;
83
+ pollTimer = setInterval(async () => {
84
+ if (abortSignal?.aborted)
85
+ return;
86
+ if (polling)
87
+ return;
88
+ polling = true;
89
+ try {
90
+ const kfIds = getKnownKfIds();
91
+ if (kfIds.length === 0) {
92
+ // No kfids discovered yet, nothing to poll
93
+ return;
94
+ }
95
+ for (const kfId of kfIds) {
96
+ if (abortSignal?.aborted)
97
+ break;
98
+ try {
99
+ log?.info(`[wechat-kf:${kfId}] polling sync_msg...`);
100
+ await handleWebhookEvent(botCtx, kfId, "");
101
+ }
102
+ catch (err) {
103
+ log?.error(`[wechat-kf:${kfId}] poll error: ${err instanceof Error ? err.stack || err.message : err}`);
104
+ }
105
+ }
106
+ }
107
+ finally {
108
+ polling = false;
109
+ }
110
+ }, POLL_INTERVAL_MS);
111
+ log?.info(`[wechat-kf] polling fallback enabled (every ${POLL_INTERVAL_MS}ms)`);
112
+ if (abortSignal) {
113
+ const cleanup = () => {
114
+ if (pollTimer) {
115
+ clearInterval(pollTimer);
116
+ pollTimer = null;
117
+ }
118
+ server.close();
119
+ log?.info("[wechat-kf] webhook server + polling stopped");
120
+ };
121
+ // Double-check: signal may have been aborted between the top guard and here
122
+ if (abortSignal.aborted) {
123
+ cleanup();
124
+ }
125
+ else {
126
+ abortSignal.addEventListener("abort", cleanup, { once: true });
127
+ }
128
+ }
129
+ return server;
130
+ }
131
+ //# sourceMappingURL=monitor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor.js","sourceRoot":"","sources":["../../src/monitor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Outbound message adapter for WeChat KF (framework-driven direct delivery)
3
+ *
4
+ * Responsibility:
5
+ * This module implements the OpenClaw `ChannelPlugin.outbound` interface and
6
+ * is called by the framework when the agent produces a final reply.
7
+ *
8
+ * Text chunking is handled by the framework itself via the declared `chunker`
9
+ * function (P0-01), so `sendText` always receives a single pre-chunked piece.
10
+ *
11
+ * For media, the file is read from disk (or downloaded from HTTP URL),
12
+ * classified, uploaded to WeChat, and sent using the shared
13
+ * `uploadAndSendMedia` helper from `send-utils.ts`.
14
+ *
15
+ * WeChat KF session limits:
16
+ * The API enforces a 48-hour / 5-message limit per session window.
17
+ * Once a customer sends a message, the agent may reply with up to 5 messages
18
+ * within 48 hours. After that, sending returns errcode 95026.
19
+ * This module detects that error and logs a clear warning rather than
20
+ * propagating a generic failure.
21
+ *
22
+ * Counterpart:
23
+ * `reply-dispatcher.ts` handles the *other* outbound path: typing-aware
24
+ * streaming replies dispatched internally by `bot.ts`.
25
+ *
26
+ * accountId = openKfId (dynamically discovered)
27
+ */
28
+ import type { OpenClawConfig } from "./types.js";
29
+ export type SendTextParams = {
30
+ cfg: OpenClawConfig;
31
+ to: string;
32
+ text: string;
33
+ accountId: string;
34
+ };
35
+ export type SendMediaParams = {
36
+ cfg: OpenClawConfig;
37
+ to: string;
38
+ text?: string;
39
+ mediaUrl?: string;
40
+ accountId: string;
41
+ };
42
+ export type SendResult = {
43
+ channel: string;
44
+ messageId: string;
45
+ chatId: string;
46
+ };
47
+ export type SendPayloadParams = {
48
+ cfg: OpenClawConfig;
49
+ to: string;
50
+ text?: string;
51
+ accountId: string;
52
+ payload: {
53
+ text?: string;
54
+ channelData?: Record<string, unknown>;
55
+ [key: string]: unknown;
56
+ };
57
+ };
58
+ export declare const wechatKfOutbound: {
59
+ deliveryMode: "direct";
60
+ chunker: (text: string, limit: number) => string[];
61
+ chunkerMode: "text";
62
+ textChunkLimit: number;
63
+ sendText: ({ cfg, to, text, accountId }: SendTextParams) => Promise<SendResult>;
64
+ sendMedia: ({ cfg, to, text, mediaUrl, accountId }: SendMediaParams) => Promise<SendResult>;
65
+ sendPayload: ({ cfg, to, text, accountId, payload }: SendPayloadParams) => Promise<SendResult>;
66
+ };
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Outbound message adapter for WeChat KF (framework-driven direct delivery)
3
+ *
4
+ * Responsibility:
5
+ * This module implements the OpenClaw `ChannelPlugin.outbound` interface and
6
+ * is called by the framework when the agent produces a final reply.
7
+ *
8
+ * Text chunking is handled by the framework itself via the declared `chunker`
9
+ * function (P0-01), so `sendText` always receives a single pre-chunked piece.
10
+ *
11
+ * For media, the file is read from disk (or downloaded from HTTP URL),
12
+ * classified, uploaded to WeChat, and sent using the shared
13
+ * `uploadAndSendMedia` helper from `send-utils.ts`.
14
+ *
15
+ * WeChat KF session limits:
16
+ * The API enforces a 48-hour / 5-message limit per session window.
17
+ * Once a customer sends a message, the agent may reply with up to 5 messages
18
+ * within 48 hours. After that, sending returns errcode 95026.
19
+ * This module detects that error and logs a clear warning rather than
20
+ * propagating a generic failure.
21
+ *
22
+ * Counterpart:
23
+ * `reply-dispatcher.ts` handles the *other* outbound path: typing-aware
24
+ * streaming replies dispatched internally by `bot.ts`.
25
+ *
26
+ * accountId = openKfId (dynamically discovered)
27
+ */
28
+ import { readFile } from "node:fs/promises";
29
+ import { extname } from "node:path";
30
+ import { resolveAccount } from "./accounts.js";
31
+ import { sendLinkMessage, sendTextMessage, uploadMedia } from "./api.js";
32
+ import { chunkText } from "./chunk-utils.js";
33
+ import { WECHAT_MSG_LIMIT_ERRCODE, WECHAT_TEXT_CHUNK_LIMIT } from "./constants.js";
34
+ import { detectMediaType, downloadMediaFromUrl, formatText, uploadAndSendMedia } from "./send-utils.js";
35
+ import { parseWechatLinkDirective } from "./wechat-kf-directives.js";
36
+ /**
37
+ * Check whether an error indicates the WeChat 48h/5-message session limit.
38
+ * When detected, logs a warning and returns true so the caller can handle
39
+ * gracefully instead of throwing a generic error.
40
+ */
41
+ function isSessionLimitError(err) {
42
+ if (err instanceof Error) {
43
+ return err.message.includes(String(WECHAT_MSG_LIMIT_ERRCODE));
44
+ }
45
+ return false;
46
+ }
47
+ export const wechatKfOutbound = {
48
+ deliveryMode: "direct",
49
+ chunker: (text, limit) => chunkText(text, limit),
50
+ chunkerMode: "text",
51
+ textChunkLimit: WECHAT_TEXT_CHUNK_LIMIT,
52
+ sendText: async ({ cfg, to, text, accountId }) => {
53
+ const account = resolveAccount(cfg, accountId);
54
+ const openKfId = account.openKfId ?? accountId;
55
+ if (!account.corpId || !account.appSecret || !openKfId) {
56
+ throw new Error("[wechat-kf] missing corpId/appSecret/openKfId");
57
+ }
58
+ const externalUserId = String(to).replace(/^user:/, "");
59
+ // ── Intercept [[wechat_link:...]] directives BEFORE formatText ──
60
+ // Parse on raw text so title/desc/url stay clean (formatText would
61
+ // convert markdown inside the directive to unicode characters).
62
+ const directive = parseWechatLinkDirective(text);
63
+ if (directive.link) {
64
+ try {
65
+ let thumbMediaId;
66
+ if (directive.link.thumbUrl) {
67
+ const downloaded = await downloadMediaFromUrl(directive.link.thumbUrl);
68
+ const uploaded = await uploadMedia(account.corpId, account.appSecret, "image", downloaded.buffer, downloaded.filename);
69
+ thumbMediaId = uploaded.media_id;
70
+ }
71
+ // WeChat requires thumb_media_id for link cards — fall back to plain text if missing
72
+ if (!thumbMediaId) {
73
+ const fallbackText = directive.text
74
+ ? `${formatText(directive.text)}\n${directive.link.title}: ${directive.link.url}`
75
+ : `${directive.link.title}: ${directive.link.url}`;
76
+ const result = await sendTextMessage(account.corpId, account.appSecret, externalUserId, openKfId, fallbackText);
77
+ return { channel: "wechat-kf", messageId: result.msgid, chatId: to };
78
+ }
79
+ const linkResult = await sendLinkMessage(account.corpId, account.appSecret, externalUserId, openKfId, {
80
+ title: directive.link.title,
81
+ desc: directive.link.desc,
82
+ url: directive.link.url,
83
+ thumb_media_id: thumbMediaId,
84
+ });
85
+ // Send remaining text if non-empty (apply formatText to surrounding text only)
86
+ if (directive.text) {
87
+ await sendTextMessage(account.corpId, account.appSecret, externalUserId, openKfId, formatText(directive.text));
88
+ }
89
+ return { channel: "wechat-kf", messageId: linkResult.msgid, chatId: to };
90
+ }
91
+ catch (err) {
92
+ if (isSessionLimitError(err)) {
93
+ console.error(`[wechat-kf] session limit exceeded (48h/5-msg) for user=${externalUserId} kf=${openKfId}. ` +
94
+ `The customer must send a new message before more replies can be delivered.`);
95
+ }
96
+ throw err;
97
+ }
98
+ }
99
+ const formatted = formatText(text);
100
+ try {
101
+ const result = await sendTextMessage(account.corpId, account.appSecret, externalUserId, openKfId, formatted);
102
+ return { channel: "wechat-kf", messageId: result.msgid, chatId: to };
103
+ }
104
+ catch (err) {
105
+ if (isSessionLimitError(err)) {
106
+ console.error(`[wechat-kf] session limit exceeded (48h/5-msg) for user=${externalUserId} kf=${openKfId}. ` +
107
+ `The customer must send a new message before more replies can be delivered.`);
108
+ }
109
+ throw err;
110
+ }
111
+ },
112
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
113
+ const account = resolveAccount(cfg, accountId);
114
+ const openKfId = account.openKfId ?? accountId;
115
+ if (!account.corpId || !account.appSecret || !openKfId) {
116
+ throw new Error("[wechat-kf] missing corpId/appSecret/openKfId");
117
+ }
118
+ const externalUserId = String(to).replace(/^user:/, "");
119
+ // ── HTTP/HTTPS URL: download then upload ──
120
+ if (mediaUrl?.startsWith("http")) {
121
+ const downloaded = await downloadMediaFromUrl(mediaUrl);
122
+ const ext = downloaded.ext.toLowerCase();
123
+ const mediaType = detectMediaType(ext);
124
+ try {
125
+ const result = await uploadAndSendMedia(account.corpId, account.appSecret, externalUserId, openKfId, downloaded.buffer, downloaded.filename, mediaType);
126
+ if (text?.trim()) {
127
+ await sendTextMessage(account.corpId, account.appSecret, externalUserId, openKfId, formatText(text));
128
+ }
129
+ return { channel: "wechat-kf", messageId: result.msgid, chatId: to };
130
+ }
131
+ catch (err) {
132
+ if (isSessionLimitError(err)) {
133
+ console.error(`[wechat-kf] session limit exceeded (48h/5-msg) for user=${externalUserId} kf=${openKfId}. ` +
134
+ `The customer must send a new message before more replies can be delivered.`);
135
+ }
136
+ throw err;
137
+ }
138
+ }
139
+ // ── Local file path: read then upload ──
140
+ if (mediaUrl) {
141
+ const buffer = await readFile(mediaUrl);
142
+ const ext = extname(mediaUrl).toLowerCase();
143
+ const mediaType = detectMediaType(ext);
144
+ const filename = mediaUrl.split("/").pop() || "file";
145
+ try {
146
+ const result = await uploadAndSendMedia(account.corpId, account.appSecret, externalUserId, openKfId, buffer, filename, mediaType);
147
+ if (text?.trim()) {
148
+ await sendTextMessage(account.corpId, account.appSecret, externalUserId, openKfId, formatText(text));
149
+ }
150
+ return { channel: "wechat-kf", messageId: result.msgid, chatId: to };
151
+ }
152
+ catch (err) {
153
+ if (isSessionLimitError(err)) {
154
+ console.error(`[wechat-kf] session limit exceeded (48h/5-msg) for user=${externalUserId} kf=${openKfId}. ` +
155
+ `The customer must send a new message before more replies can be delivered.`);
156
+ }
157
+ throw err;
158
+ }
159
+ }
160
+ // ── No resolvable media: send as text ──
161
+ const content = text?.trim() ? `${text}\n${mediaUrl || ""}` : mediaUrl || text || "";
162
+ try {
163
+ const result = await sendTextMessage(account.corpId, account.appSecret, externalUserId, openKfId, content);
164
+ return { channel: "wechat-kf", messageId: result.msgid, chatId: to };
165
+ }
166
+ catch (err) {
167
+ if (isSessionLimitError(err)) {
168
+ console.error(`[wechat-kf] session limit exceeded (48h/5-msg) for user=${externalUserId} kf=${openKfId}. ` +
169
+ `The customer must send a new message before more replies can be delivered.`);
170
+ }
171
+ throw err;
172
+ }
173
+ },
174
+ sendPayload: async ({ cfg, to, text, accountId, payload }) => {
175
+ const account = resolveAccount(cfg, accountId);
176
+ const openKfId = account.openKfId ?? accountId;
177
+ if (!account.corpId || !account.appSecret || !openKfId) {
178
+ throw new Error("[wechat-kf] missing corpId/appSecret/openKfId");
179
+ }
180
+ const externalUserId = String(to).replace(/^user:/, "");
181
+ const wechatKf = payload.channelData?.wechatKf;
182
+ const link = wechatKf?.link;
183
+ if (link) {
184
+ let thumbMediaId = link.thumb_media_id;
185
+ // Download and upload thumbnail if only URL is provided
186
+ if (!thumbMediaId && link.thumbUrl) {
187
+ const downloaded = await downloadMediaFromUrl(link.thumbUrl);
188
+ const uploaded = await uploadMedia(account.corpId, account.appSecret, "image", downloaded.buffer, downloaded.filename);
189
+ thumbMediaId = uploaded.media_id;
190
+ }
191
+ if (!thumbMediaId) {
192
+ throw new Error("[wechat-kf] sendPayload link requires thumb_media_id or thumbUrl");
193
+ }
194
+ try {
195
+ const result = await sendLinkMessage(account.corpId, account.appSecret, externalUserId, openKfId, {
196
+ title: link.title,
197
+ desc: link.desc,
198
+ url: link.url,
199
+ thumb_media_id: thumbMediaId,
200
+ });
201
+ // Send accompanying text if present
202
+ if ((text ?? payload.text)?.trim()) {
203
+ const textContent = (text ?? payload.text);
204
+ await sendTextMessage(account.corpId, account.appSecret, externalUserId, openKfId, formatText(textContent));
205
+ }
206
+ return { channel: "wechat-kf", messageId: result.msgid, chatId: to };
207
+ }
208
+ catch (err) {
209
+ if (isSessionLimitError(err)) {
210
+ console.error(`[wechat-kf] session limit exceeded (48h/5-msg) for user=${externalUserId} kf=${openKfId}. ` +
211
+ `The customer must send a new message before more replies can be delivered.`);
212
+ }
213
+ throw err;
214
+ }
215
+ }
216
+ // No link data — fall back to sending text
217
+ const textContent = text ?? payload.text ?? "";
218
+ if (textContent.trim()) {
219
+ try {
220
+ const result = await sendTextMessage(account.corpId, account.appSecret, externalUserId, openKfId, formatText(textContent));
221
+ return { channel: "wechat-kf", messageId: result.msgid, chatId: to };
222
+ }
223
+ catch (err) {
224
+ if (isSessionLimitError(err)) {
225
+ console.error(`[wechat-kf] session limit exceeded (48h/5-msg) for user=${externalUserId} kf=${openKfId}. ` +
226
+ `The customer must send a new message before more replies can be delivered.`);
227
+ }
228
+ throw err;
229
+ }
230
+ }
231
+ return { channel: "wechat-kf", messageId: "", chatId: to };
232
+ },
233
+ };
234
+ //# sourceMappingURL=outbound.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbound.js","sourceRoot":"","sources":["../../src/outbound.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Reply dispatcher for WeChat KF (typing-aware streaming replies)
3
+ *
4
+ * Responsibility:
5
+ * This module is used internally by `bot.ts` when the agent streams tokens
6
+ * back to the user. It wraps OpenClaw's `createReplyDispatcherWithTyping` to
7
+ * batch tokens into natural-looking messages with simulated human typing
8
+ * delay, then delivers them through the WeChat KF API.
9
+ *
10
+ * Text chunking is performed at delivery time via the runtime's
11
+ * `chunkTextWithMode` helper (NOT the framework auto-chunker), because
12
+ * streaming replies accumulate text incrementally.
13
+ *
14
+ * Counterpart:
15
+ * `outbound.ts` handles the *other* outbound path: framework-driven direct
16
+ * delivery where the framework itself pre-chunks text via the declared
17
+ * `chunker` function.
18
+ *
19
+ * accountId = openKfId (dynamically discovered)
20
+ */
21
+ import type { OpenClawConfig } from "./types.js";
22
+ /** Minimal runtime shape used only for error logging in the reply dispatcher. */
23
+ type RuntimeErrorLogger = {
24
+ error?: (...args: unknown[]) => void;
25
+ [key: string]: unknown;
26
+ };
27
+ export type CreateReplyDispatcherParams = {
28
+ cfg: OpenClawConfig;
29
+ agentId: string;
30
+ runtime: RuntimeErrorLogger;
31
+ externalUserId: string;
32
+ openKfId: string;
33
+ accountId: string;
34
+ };
35
+ export declare function createReplyDispatcher(params: CreateReplyDispatcherParams): {
36
+ dispatcher: unknown;
37
+ replyOptions: unknown;
38
+ markDispatchIdle: (() => void) | undefined;
39
+ };
40
+ export {};
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Reply dispatcher for WeChat KF (typing-aware streaming replies)
3
+ *
4
+ * Responsibility:
5
+ * This module is used internally by `bot.ts` when the agent streams tokens
6
+ * back to the user. It wraps OpenClaw's `createReplyDispatcherWithTyping` to
7
+ * batch tokens into natural-looking messages with simulated human typing
8
+ * delay, then delivers them through the WeChat KF API.
9
+ *
10
+ * Text chunking is performed at delivery time via the runtime's
11
+ * `chunkTextWithMode` helper (NOT the framework auto-chunker), because
12
+ * streaming replies accumulate text incrementally.
13
+ *
14
+ * Counterpart:
15
+ * `outbound.ts` handles the *other* outbound path: framework-driven direct
16
+ * delivery where the framework itself pre-chunks text via the declared
17
+ * `chunker` function.
18
+ *
19
+ * accountId = openKfId (dynamically discovered)
20
+ */
21
+ import { readFile } from "node:fs/promises";
22
+ import { basename, extname } from "node:path";
23
+ import { resolveAccount } from "./accounts.js";
24
+ import { sendLinkMessage, sendTextMessage, uploadMedia } from "./api.js";
25
+ import { WECHAT_TEXT_CHUNK_LIMIT } from "./constants.js";
26
+ import { getRuntime } from "./runtime.js";
27
+ import { detectMediaType, downloadMediaFromUrl, formatText, uploadAndSendMedia } from "./send-utils.js";
28
+ import { parseWechatLinkDirective } from "./wechat-kf-directives.js";
29
+ export function createReplyDispatcher(params) {
30
+ const core = getRuntime();
31
+ const { cfg, agentId, externalUserId, openKfId, accountId } = params;
32
+ const account = resolveAccount(cfg, accountId);
33
+ const kfId = openKfId; // accountId IS the kfid
34
+ const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "wechat-kf", accountId, {
35
+ fallbackLimit: WECHAT_TEXT_CHUNK_LIMIT,
36
+ });
37
+ const chunkMode = core.channel.text.resolveChunkMode(cfg, "wechat-kf");
38
+ const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
39
+ humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
40
+ deliver: async (payload) => {
41
+ const text = payload.text ?? "";
42
+ const attachments = payload.attachments ?? [];
43
+ const { corpId, appSecret } = account;
44
+ if (!corpId || !appSecret) {
45
+ throw new Error("[wechat-kf] missing corpId/appSecret for send");
46
+ }
47
+ // Handle media attachments (image, voice, video, file)
48
+ for (const attachment of attachments) {
49
+ if (attachment.path) {
50
+ try {
51
+ const ext = extname(attachment.path).toLowerCase();
52
+ const mediaType = detectMediaType(ext);
53
+ const filename = basename(attachment.path);
54
+ const buffer = await readFile(attachment.path);
55
+ await uploadAndSendMedia(corpId, appSecret, externalUserId, kfId, buffer, filename, mediaType);
56
+ }
57
+ catch (err) {
58
+ params.runtime?.error?.(`[wechat-kf] failed to send ${attachment.type ?? "media"} attachment: ${String(err)}`);
59
+ }
60
+ }
61
+ }
62
+ // ── Intercept [[wechat_link:...]] directives BEFORE formatText ──
63
+ // Parse on raw text so title/desc/url stay clean (formatText would
64
+ // convert markdown inside the directive to unicode characters).
65
+ if (text.trim()) {
66
+ const directive = parseWechatLinkDirective(text);
67
+ if (directive.link) {
68
+ let linkSent = false;
69
+ if (directive.link.thumbUrl) {
70
+ try {
71
+ const downloaded = await downloadMediaFromUrl(directive.link.thumbUrl);
72
+ const uploaded = await uploadMedia(corpId, appSecret, "image", downloaded.buffer, downloaded.filename);
73
+ await sendLinkMessage(corpId, appSecret, externalUserId, kfId, {
74
+ title: directive.link.title,
75
+ desc: directive.link.desc,
76
+ url: directive.link.url,
77
+ thumb_media_id: uploaded.media_id,
78
+ });
79
+ linkSent = true;
80
+ }
81
+ catch (err) {
82
+ params.runtime?.error?.(`[wechat-kf] failed to send link card: ${String(err)}`);
83
+ }
84
+ }
85
+ // Send remaining text (or fallback with title:url if link card failed / no thumbUrl)
86
+ const rawRemaining = linkSent
87
+ ? directive.text
88
+ : directive.text
89
+ ? `${directive.text}\n${directive.link.title}: ${directive.link.url}`
90
+ : `${directive.link.title}: ${directive.link.url}`;
91
+ if (rawRemaining?.trim()) {
92
+ const formatted = formatText(rawRemaining);
93
+ const chunks = core.channel.text.chunkTextWithMode(formatted, textChunkLimit, chunkMode);
94
+ for (const chunk of chunks) {
95
+ await sendTextMessage(corpId, appSecret, externalUserId, kfId, chunk);
96
+ }
97
+ }
98
+ }
99
+ else {
100
+ // No directive — normal path: formatText then chunk and send
101
+ const formatted = formatText(text);
102
+ if (formatted.trim()) {
103
+ const chunks = core.channel.text.chunkTextWithMode(formatted, textChunkLimit, chunkMode);
104
+ for (const chunk of chunks) {
105
+ await sendTextMessage(corpId, appSecret, externalUserId, kfId, chunk);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ if (!text.trim() && attachments.length === 0) {
111
+ return;
112
+ }
113
+ },
114
+ onError: (err, info) => {
115
+ params.runtime?.error?.(`[wechat-kf] ${info?.kind ?? "unknown"} reply failed: ${String(err)}`);
116
+ },
117
+ });
118
+ return { dispatcher, replyOptions, markDispatchIdle };
119
+ }
120
+ //# sourceMappingURL=reply-dispatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reply-dispatcher.js","sourceRoot":"","sources":["../../src/reply-dispatcher.ts"],"names":[],"mappings":"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"}