@yaoqi10012/wechat-kf 0.3.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.
- package/LICENSE +21 -0
- package/dist/accounts.d.ts +38 -0
- package/dist/accounts.js +247 -0
- package/dist/accounts.js.map +1 -0
- package/dist/api.d.ts +58 -0
- package/dist/api.js +200 -0
- package/dist/api.js.map +1 -0
- package/dist/bot.d.ts +37 -0
- package/dist/bot.js +672 -0
- package/dist/bot.js.map +1 -0
- package/dist/channel.d.ts +14 -0
- package/dist/channel.js +320 -0
- package/dist/channel.js.map +1 -0
- package/dist/config-schema.d.ts +56 -0
- package/dist/config-schema.js +39 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/constants.d.ts +41 -0
- package/dist/constants.js +51 -0
- package/dist/constants.js.map +1 -0
- package/dist/crypto.d.ts +18 -0
- package/dist/crypto.js +81 -0
- package/dist/crypto.js.map +1 -0
- package/dist/fs-utils.d.ts +7 -0
- package/dist/fs-utils.js +13 -0
- package/dist/fs-utils.js.map +1 -0
- package/dist/monitor.d.ts +31 -0
- package/dist/monitor.js +80 -0
- package/dist/monitor.js.map +1 -0
- package/dist/outbound.d.ts +32 -0
- package/dist/outbound.js +411 -0
- package/dist/outbound.js.map +1 -0
- package/dist/reply-dispatcher.d.ts +36 -0
- package/dist/reply-dispatcher.js +216 -0
- package/dist/reply-dispatcher.js.map +1 -0
- package/dist/runtime.d.ts +12 -0
- package/dist/runtime.js +23 -0
- package/dist/runtime.js.map +1 -0
- package/dist/send-utils.d.ts +52 -0
- package/dist/send-utils.js +217 -0
- package/dist/send-utils.js.map +1 -0
- package/dist/token.d.ts +8 -0
- package/dist/token.js +61 -0
- package/dist/token.js.map +1 -0
- package/dist/types.d.ts +236 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/unicode-format.d.ts +26 -0
- package/dist/unicode-format.js +157 -0
- package/dist/unicode-format.js.map +1 -0
- package/dist/webhook.d.ts +22 -0
- package/dist/webhook.js +148 -0
- package/dist/webhook.js.map +1 -0
- package/dist/wechat-kf-directives.d.ts +157 -0
- package/dist/wechat-kf-directives.js +576 -0
- package/dist/wechat-kf-directives.js.map +1 -0
- package/openclaw.plugin.json +31 -0
- package/package.json +92 -0
|
@@ -0,0 +1,216 @@
|
|
|
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 `chunkTextByUtf8Bytes`
|
|
11
|
+
* (UTF-8 byte-aware, NOT the framework auto-chunker), because streaming
|
|
12
|
+
* 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 { resolveAccount } from "./accounts.js";
|
|
22
|
+
import { sendBusinessCardMessage, sendCaLinkMessage, sendLinkMessage, sendLocationMessage, sendMiniprogramMessage, sendMsgMenuMessage, sendRawMessage, sendTextMessage, } from "./api.js";
|
|
23
|
+
import { logTag, WECHAT_TEXT_CHUNK_BYTE_SAFETY_MARGIN, WECHAT_TEXT_CHUNK_LIMIT } from "./constants.js";
|
|
24
|
+
import { getRuntime } from "./runtime.js";
|
|
25
|
+
import { chunkTextByUtf8Bytes, formatText, mediaKindToWechatType, resolveThumbMediaId, uploadAndSendMedia, } from "./send-utils.js";
|
|
26
|
+
import { buildMsgMenuPayload, parseWechatDirective } from "./wechat-kf-directives.js";
|
|
27
|
+
export function createReplyDispatcher(params) {
|
|
28
|
+
const core = getRuntime();
|
|
29
|
+
const { cfg, agentId, externalUserId, openKfId, accountId } = params;
|
|
30
|
+
const account = resolveAccount(cfg, accountId);
|
|
31
|
+
const kfId = openKfId; // accountId IS the kfid
|
|
32
|
+
const byteLimit = WECHAT_TEXT_CHUNK_LIMIT - WECHAT_TEXT_CHUNK_BYTE_SAFETY_MARGIN;
|
|
33
|
+
const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
34
|
+
humanDelay: core.channel.reply.resolveHumanDelayConfig(cfg, agentId),
|
|
35
|
+
deliver: async (payload) => {
|
|
36
|
+
const text = payload.text ?? "";
|
|
37
|
+
const mediaUrls = payload.mediaUrls ?? (payload.mediaUrl ? [payload.mediaUrl] : []);
|
|
38
|
+
const { corpId, appSecret } = account;
|
|
39
|
+
if (!corpId || !appSecret) {
|
|
40
|
+
throw new Error(`${logTag()} missing corpId/appSecret for send`);
|
|
41
|
+
}
|
|
42
|
+
// Handle media (image, voice, video, file) via framework loadWebMedia
|
|
43
|
+
for (const url of mediaUrls) {
|
|
44
|
+
try {
|
|
45
|
+
const loaded = await core.media.loadWebMedia(url, { optimizeImages: false });
|
|
46
|
+
const mediaType = mediaKindToWechatType(loaded.kind);
|
|
47
|
+
await uploadAndSendMedia(corpId, appSecret, externalUserId, kfId, loaded.buffer, loaded.fileName ?? "file", mediaType);
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
params.runtime?.error?.(`${logTag()} failed to send media: ${String(err)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ── Intercept [[wechat_*:...]] directives BEFORE formatText ──
|
|
54
|
+
// Parse on raw text so fields stay clean (formatText would
|
|
55
|
+
// convert markdown inside the directive to unicode characters).
|
|
56
|
+
if (text.trim()) {
|
|
57
|
+
const directive = parseWechatDirective(text);
|
|
58
|
+
if (directive.link) {
|
|
59
|
+
let linkSent = false;
|
|
60
|
+
if (directive.link.thumbUrl) {
|
|
61
|
+
try {
|
|
62
|
+
const thumbMediaId = await resolveThumbMediaId(directive.link.thumbUrl, corpId, appSecret);
|
|
63
|
+
await sendLinkMessage(corpId, appSecret, externalUserId, kfId, {
|
|
64
|
+
title: directive.link.title,
|
|
65
|
+
desc: directive.link.desc,
|
|
66
|
+
url: directive.link.url,
|
|
67
|
+
thumb_media_id: thumbMediaId,
|
|
68
|
+
});
|
|
69
|
+
linkSent = true;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
params.runtime?.error?.(`${logTag()} failed to send link card: ${String(err)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Send remaining text (or fallback with title:url if link card failed / no thumbUrl)
|
|
76
|
+
const rawRemaining = linkSent
|
|
77
|
+
? directive.text
|
|
78
|
+
: directive.text
|
|
79
|
+
? `${directive.text}\n${directive.link.title}: ${directive.link.url}`
|
|
80
|
+
: `${directive.link.title}: ${directive.link.url}`;
|
|
81
|
+
if (rawRemaining?.trim()) {
|
|
82
|
+
const formatted = formatText(rawRemaining);
|
|
83
|
+
const chunks = chunkTextByUtf8Bytes(formatted, byteLimit);
|
|
84
|
+
for (const chunk of chunks) {
|
|
85
|
+
await sendTextMessage(corpId, appSecret, externalUserId, kfId, chunk);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
else if (directive.location) {
|
|
90
|
+
try {
|
|
91
|
+
await sendLocationMessage(corpId, appSecret, externalUserId, kfId, directive.location);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
params.runtime?.error?.(`${logTag()} failed to send location: ${String(err)}`);
|
|
95
|
+
}
|
|
96
|
+
if (directive.text?.trim()) {
|
|
97
|
+
const formatted = formatText(directive.text);
|
|
98
|
+
const chunks = chunkTextByUtf8Bytes(formatted, byteLimit);
|
|
99
|
+
for (const chunk of chunks) {
|
|
100
|
+
await sendTextMessage(corpId, appSecret, externalUserId, kfId, chunk);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else if (directive.miniprogram) {
|
|
105
|
+
let mpSent = false;
|
|
106
|
+
if (directive.miniprogram.thumbUrl) {
|
|
107
|
+
try {
|
|
108
|
+
const thumbMediaId = await resolveThumbMediaId(directive.miniprogram.thumbUrl, corpId, appSecret);
|
|
109
|
+
await sendMiniprogramMessage(corpId, appSecret, externalUserId, kfId, {
|
|
110
|
+
appid: directive.miniprogram.appid,
|
|
111
|
+
title: directive.miniprogram.title,
|
|
112
|
+
pagepath: directive.miniprogram.pagepath,
|
|
113
|
+
thumb_media_id: thumbMediaId,
|
|
114
|
+
});
|
|
115
|
+
mpSent = true;
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
params.runtime?.error?.(`${logTag()} failed to send miniprogram: ${String(err)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const rawRemaining = mpSent
|
|
122
|
+
? directive.text
|
|
123
|
+
: directive.text
|
|
124
|
+
? `${directive.text}\n[小程序] ${directive.miniprogram.title}`
|
|
125
|
+
: `[小程序] ${directive.miniprogram.title}`;
|
|
126
|
+
if (rawRemaining?.trim()) {
|
|
127
|
+
const formatted = formatText(rawRemaining);
|
|
128
|
+
const chunks = chunkTextByUtf8Bytes(formatted, byteLimit);
|
|
129
|
+
for (const chunk of chunks) {
|
|
130
|
+
await sendTextMessage(corpId, appSecret, externalUserId, kfId, chunk);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (directive.menu) {
|
|
135
|
+
try {
|
|
136
|
+
const menuPayload = buildMsgMenuPayload(directive.menu);
|
|
137
|
+
await sendMsgMenuMessage(corpId, appSecret, externalUserId, kfId, menuPayload);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
params.runtime?.error?.(`${logTag()} failed to send menu: ${String(err)}`);
|
|
141
|
+
}
|
|
142
|
+
if (directive.text?.trim()) {
|
|
143
|
+
const formatted = formatText(directive.text);
|
|
144
|
+
const chunks = chunkTextByUtf8Bytes(formatted, byteLimit);
|
|
145
|
+
for (const chunk of chunks) {
|
|
146
|
+
await sendTextMessage(corpId, appSecret, externalUserId, kfId, chunk);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else if (directive.businessCard) {
|
|
151
|
+
try {
|
|
152
|
+
await sendBusinessCardMessage(corpId, appSecret, externalUserId, kfId, directive.businessCard);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
params.runtime?.error?.(`${logTag()} failed to send business card: ${String(err)}`);
|
|
156
|
+
}
|
|
157
|
+
if (directive.text?.trim()) {
|
|
158
|
+
const formatted = formatText(directive.text);
|
|
159
|
+
const chunks = chunkTextByUtf8Bytes(formatted, byteLimit);
|
|
160
|
+
for (const chunk of chunks) {
|
|
161
|
+
await sendTextMessage(corpId, appSecret, externalUserId, kfId, chunk);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else if (directive.caLink) {
|
|
166
|
+
try {
|
|
167
|
+
await sendCaLinkMessage(corpId, appSecret, externalUserId, kfId, directive.caLink);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
params.runtime?.error?.(`${logTag()} failed to send ca_link: ${String(err)}`);
|
|
171
|
+
}
|
|
172
|
+
if (directive.text?.trim()) {
|
|
173
|
+
const formatted = formatText(directive.text);
|
|
174
|
+
const chunks = chunkTextByUtf8Bytes(formatted, byteLimit);
|
|
175
|
+
for (const chunk of chunks) {
|
|
176
|
+
await sendTextMessage(corpId, appSecret, externalUserId, kfId, chunk);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (directive.raw) {
|
|
181
|
+
try {
|
|
182
|
+
await sendRawMessage(corpId, appSecret, externalUserId, kfId, directive.raw.msgtype, directive.raw.payload);
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
params.runtime?.error?.(`${logTag()} failed to send raw message: ${String(err)}`);
|
|
186
|
+
}
|
|
187
|
+
if (directive.text?.trim()) {
|
|
188
|
+
const formatted = formatText(directive.text);
|
|
189
|
+
const chunks = chunkTextByUtf8Bytes(formatted, byteLimit);
|
|
190
|
+
for (const chunk of chunks) {
|
|
191
|
+
await sendTextMessage(corpId, appSecret, externalUserId, kfId, chunk);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// No directive — normal path: formatText then chunk and send
|
|
197
|
+
const formatted = formatText(text);
|
|
198
|
+
if (formatted.trim()) {
|
|
199
|
+
const chunks = chunkTextByUtf8Bytes(formatted, byteLimit);
|
|
200
|
+
for (const chunk of chunks) {
|
|
201
|
+
await sendTextMessage(corpId, appSecret, externalUserId, kfId, chunk);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (!text.trim() && mediaUrls.length === 0) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
onError: (err, info) => {
|
|
211
|
+
params.runtime?.error?.(`${logTag()} ${info?.kind ?? "unknown"} reply failed: ${String(err)}`);
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
return { dispatcher, replyOptions, markDispatchIdle };
|
|
215
|
+
}
|
|
216
|
+
//# 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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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin runtime reference
|
|
3
|
+
* Stores the PluginRuntime provided by OpenClaw gateway at startup.
|
|
4
|
+
*/
|
|
5
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
6
|
+
export declare function setRuntime(next: PluginRuntime): void;
|
|
7
|
+
export declare function getRuntime(): PluginRuntime;
|
|
8
|
+
/**
|
|
9
|
+
* Reset the module-level runtime reference to null.
|
|
10
|
+
* @internal Exposed for testing only — allows test isolation between runs.
|
|
11
|
+
*/
|
|
12
|
+
export declare function _reset(): void;
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin runtime reference
|
|
3
|
+
* Stores the PluginRuntime provided by OpenClaw gateway at startup.
|
|
4
|
+
*/
|
|
5
|
+
import { logTag } from "./constants.js";
|
|
6
|
+
let runtime = null;
|
|
7
|
+
export function setRuntime(next) {
|
|
8
|
+
runtime = next;
|
|
9
|
+
}
|
|
10
|
+
export function getRuntime() {
|
|
11
|
+
if (!runtime) {
|
|
12
|
+
throw new Error(`${logTag()} runtime not initialized — plugin not started via gateway?`);
|
|
13
|
+
}
|
|
14
|
+
return runtime;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Reset the module-level runtime reference to null.
|
|
18
|
+
* @internal Exposed for testing only — allows test isolation between runs.
|
|
19
|
+
*/
|
|
20
|
+
export function _reset() {
|
|
21
|
+
runtime = null;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAExC,IAAI,OAAO,GAAyB,IAAI,CAAC;AAEzC,MAAM,UAAU,UAAU,CAAC,IAAmB;IAC5C,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,EAAE,4DAA4D,CAAC,CAAC;IAC3F,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM;IACpB,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared outbound utilities for WeChat KF
|
|
3
|
+
*
|
|
4
|
+
* Extracted helpers used by both outbound paths:
|
|
5
|
+
* - outbound.ts (framework-driven direct delivery)
|
|
6
|
+
* - reply-dispatcher.ts (typing-aware streaming replies)
|
|
7
|
+
*
|
|
8
|
+
* Centralises Markdown formatting, media-type detection, and the
|
|
9
|
+
* upload-then-send media workflow so changes only need to happen once.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Split text into chunks that each fit within `byteLimit` UTF-8 bytes.
|
|
13
|
+
*
|
|
14
|
+
* Iterates by code point (safe for surrogate pairs / emoji) and prefers
|
|
15
|
+
* breaking at newline or space boundaries. When no natural break point
|
|
16
|
+
* exists the chunk is split at the byte-limit boundary (still on a code
|
|
17
|
+
* point edge, never mid-surrogate).
|
|
18
|
+
*/
|
|
19
|
+
export declare function chunkTextByUtf8Bytes(text: string, byteLimit: number): string[];
|
|
20
|
+
/** Markdown to Unicode text formatting (shared by both outbound paths) */
|
|
21
|
+
export declare function formatText(text: string): string;
|
|
22
|
+
export declare function contentTypeToExt(contentType: string): string;
|
|
23
|
+
/** Detect image MIME type from magic bytes (file header) */
|
|
24
|
+
export declare function detectImageMime(buffer: Buffer): string | null;
|
|
25
|
+
/** Map framework MediaKind to WeChat media type */
|
|
26
|
+
export declare function mediaKindToWechatType(kind: string): "image" | "voice" | "video" | "file";
|
|
27
|
+
/** Map file extension to WeChat media type */
|
|
28
|
+
export declare function detectMediaType(ext: string): "image" | "voice" | "video" | "file";
|
|
29
|
+
/** Upload media to WeChat and send via the appropriate message type */
|
|
30
|
+
export declare function uploadAndSendMedia(corpId: string, appSecret: string, toUser: string, openKfId: string, buffer: Buffer, filename: string, mediaType: "image" | "voice" | "video" | "file"): Promise<{
|
|
31
|
+
msgid: string;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Download media from an HTTP/HTTPS URL and return the buffer + filename.
|
|
35
|
+
*
|
|
36
|
+
* WeChat does not accept external URLs directly — media must be uploaded to
|
|
37
|
+
* the temporary media store first. This helper fetches the remote resource
|
|
38
|
+
* so the caller can then pass the buffer through `uploadAndSendMedia`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function downloadMediaFromUrl(url: string): Promise<{
|
|
41
|
+
buffer: Buffer;
|
|
42
|
+
filename: string;
|
|
43
|
+
ext: string;
|
|
44
|
+
}>;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a thumbnail reference to a WeChat thumb_media_id.
|
|
47
|
+
*
|
|
48
|
+
* Accepts three kinds of input:
|
|
49
|
+
* - URL (http://, https://) or local path (/, ~, file://, data:) → loadWebMedia + uploadMedia
|
|
50
|
+
* - media_id string (anything else) → used directly
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveThumbMediaId(thumbRef: string, corpId: string, appSecret: string): Promise<string>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared outbound utilities for WeChat KF
|
|
3
|
+
*
|
|
4
|
+
* Extracted helpers used by both outbound paths:
|
|
5
|
+
* - outbound.ts (framework-driven direct delivery)
|
|
6
|
+
* - reply-dispatcher.ts (typing-aware streaming replies)
|
|
7
|
+
*
|
|
8
|
+
* Centralises Markdown formatting, media-type detection, and the
|
|
9
|
+
* upload-then-send media workflow so changes only need to happen once.
|
|
10
|
+
*/
|
|
11
|
+
import { basename, extname } from "node:path";
|
|
12
|
+
import { sendFileMessage, sendImageMessage, sendVideoMessage, sendVoiceMessage, uploadMedia } from "./api.js";
|
|
13
|
+
import { logTag, MEDIA_DOWNLOAD_TIMEOUT_MS } from "./constants.js";
|
|
14
|
+
import { getRuntime } from "./runtime.js";
|
|
15
|
+
import { markdownToUnicode } from "./unicode-format.js";
|
|
16
|
+
/** Calculate the UTF-8 byte length of a single code point. */
|
|
17
|
+
function utf8ByteLengthOfCodePoint(cp) {
|
|
18
|
+
if (cp <= 0x7f)
|
|
19
|
+
return 1;
|
|
20
|
+
if (cp <= 0x7ff)
|
|
21
|
+
return 2;
|
|
22
|
+
if (cp <= 0xffff)
|
|
23
|
+
return 3;
|
|
24
|
+
return 4;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Split text into chunks that each fit within `byteLimit` UTF-8 bytes.
|
|
28
|
+
*
|
|
29
|
+
* Iterates by code point (safe for surrogate pairs / emoji) and prefers
|
|
30
|
+
* breaking at newline or space boundaries. When no natural break point
|
|
31
|
+
* exists the chunk is split at the byte-limit boundary (still on a code
|
|
32
|
+
* point edge, never mid-surrogate).
|
|
33
|
+
*/
|
|
34
|
+
export function chunkTextByUtf8Bytes(text, byteLimit) {
|
|
35
|
+
if (byteLimit <= 0)
|
|
36
|
+
return [];
|
|
37
|
+
const trimmed = text.trim();
|
|
38
|
+
if (trimmed.length === 0)
|
|
39
|
+
return [];
|
|
40
|
+
const chunks = [];
|
|
41
|
+
let chunkStart = 0; // index into `trimmed` (UTF-16 offset)
|
|
42
|
+
let byteCount = 0;
|
|
43
|
+
let lastBreak = -1; // UTF-16 index of last space/newline *start*
|
|
44
|
+
let i = 0;
|
|
45
|
+
for (const char of trimmed) {
|
|
46
|
+
const cp = char.codePointAt(0);
|
|
47
|
+
const cpBytes = utf8ByteLengthOfCodePoint(cp);
|
|
48
|
+
if (byteCount + cpBytes > byteLimit) {
|
|
49
|
+
// Need to flush a chunk
|
|
50
|
+
if (lastBreak > chunkStart) {
|
|
51
|
+
// Break at the last whitespace
|
|
52
|
+
const chunk = trimmed.slice(chunkStart, lastBreak).trimEnd();
|
|
53
|
+
if (chunk)
|
|
54
|
+
chunks.push(chunk);
|
|
55
|
+
chunkStart = lastBreak;
|
|
56
|
+
// Skip leading whitespace after break
|
|
57
|
+
while (chunkStart < trimmed.length && (trimmed[chunkStart] === " " || trimmed[chunkStart] === "\n")) {
|
|
58
|
+
chunkStart++;
|
|
59
|
+
}
|
|
60
|
+
// Recalculate byteCount from chunkStart to i
|
|
61
|
+
byteCount = 0;
|
|
62
|
+
for (const c of trimmed.slice(chunkStart, i)) {
|
|
63
|
+
byteCount += utf8ByteLengthOfCodePoint(c.codePointAt(0));
|
|
64
|
+
}
|
|
65
|
+
lastBreak = -1;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// No break point — hard cut at current position
|
|
69
|
+
const chunk = trimmed.slice(chunkStart, i).trimEnd();
|
|
70
|
+
if (chunk)
|
|
71
|
+
chunks.push(chunk);
|
|
72
|
+
chunkStart = i;
|
|
73
|
+
byteCount = 0;
|
|
74
|
+
lastBreak = -1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Track break points
|
|
78
|
+
if (char === "\n" || char === " ") {
|
|
79
|
+
lastBreak = i;
|
|
80
|
+
}
|
|
81
|
+
byteCount += cpBytes;
|
|
82
|
+
i += char.length; // 1 for BMP, 2 for supplementary
|
|
83
|
+
}
|
|
84
|
+
// Flush remaining
|
|
85
|
+
if (chunkStart < trimmed.length) {
|
|
86
|
+
const chunk = trimmed.slice(chunkStart).trim();
|
|
87
|
+
if (chunk)
|
|
88
|
+
chunks.push(chunk);
|
|
89
|
+
}
|
|
90
|
+
return chunks;
|
|
91
|
+
}
|
|
92
|
+
/** Markdown to Unicode text formatting (shared by both outbound paths) */
|
|
93
|
+
export function formatText(text) {
|
|
94
|
+
return markdownToUnicode(text);
|
|
95
|
+
}
|
|
96
|
+
const CONTENT_TYPE_EXT_MAP = {
|
|
97
|
+
"image/jpeg": ".jpg",
|
|
98
|
+
"image/png": ".png",
|
|
99
|
+
"image/gif": ".gif",
|
|
100
|
+
"image/webp": ".webp",
|
|
101
|
+
"image/bmp": ".bmp",
|
|
102
|
+
"audio/amr": ".amr",
|
|
103
|
+
"audio/mpeg": ".mp3",
|
|
104
|
+
"audio/ogg": ".ogg",
|
|
105
|
+
"audio/wav": ".wav",
|
|
106
|
+
"audio/x-wav": ".wav",
|
|
107
|
+
"video/mp4": ".mp4",
|
|
108
|
+
"application/pdf": ".pdf",
|
|
109
|
+
};
|
|
110
|
+
export function contentTypeToExt(contentType) {
|
|
111
|
+
return CONTENT_TYPE_EXT_MAP[contentType] ?? "";
|
|
112
|
+
}
|
|
113
|
+
/** Detect image MIME type from magic bytes (file header) */
|
|
114
|
+
export function detectImageMime(buffer) {
|
|
115
|
+
if (buffer.length < 4)
|
|
116
|
+
return null;
|
|
117
|
+
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46)
|
|
118
|
+
return "image/gif";
|
|
119
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47)
|
|
120
|
+
return "image/png";
|
|
121
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8)
|
|
122
|
+
return "image/jpeg";
|
|
123
|
+
if (buffer[0] === 0x42 && buffer[1] === 0x4d)
|
|
124
|
+
return "image/bmp";
|
|
125
|
+
if (buffer.length >= 12 &&
|
|
126
|
+
buffer.subarray(0, 4).toString() === "RIFF" &&
|
|
127
|
+
buffer.subarray(8, 12).toString() === "WEBP")
|
|
128
|
+
return "image/webp";
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/** Map framework MediaKind to WeChat media type */
|
|
132
|
+
export function mediaKindToWechatType(kind) {
|
|
133
|
+
switch (kind) {
|
|
134
|
+
case "image":
|
|
135
|
+
return "image";
|
|
136
|
+
case "audio":
|
|
137
|
+
return "voice";
|
|
138
|
+
case "video":
|
|
139
|
+
return "video";
|
|
140
|
+
default:
|
|
141
|
+
return "file"; // "document" | "unknown"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Map file extension to WeChat media type */
|
|
145
|
+
export function detectMediaType(ext) {
|
|
146
|
+
ext = ext.toLowerCase();
|
|
147
|
+
if ([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"].includes(ext))
|
|
148
|
+
return "image";
|
|
149
|
+
if (ext === ".amr")
|
|
150
|
+
return "voice";
|
|
151
|
+
if ([".mp4", ".avi", ".mov", ".mkv", ".wmv"].includes(ext))
|
|
152
|
+
return "video";
|
|
153
|
+
return "file";
|
|
154
|
+
}
|
|
155
|
+
/** Upload media to WeChat and send via the appropriate message type */
|
|
156
|
+
export async function uploadAndSendMedia(corpId, appSecret, toUser, openKfId, buffer, filename, mediaType) {
|
|
157
|
+
const uploaded = await uploadMedia(corpId, appSecret, mediaType, buffer, filename);
|
|
158
|
+
const mid = uploaded.media_id;
|
|
159
|
+
switch (mediaType) {
|
|
160
|
+
case "image":
|
|
161
|
+
return sendImageMessage(corpId, appSecret, toUser, openKfId, mid);
|
|
162
|
+
case "voice":
|
|
163
|
+
return sendVoiceMessage(corpId, appSecret, toUser, openKfId, mid);
|
|
164
|
+
case "video":
|
|
165
|
+
return sendVideoMessage(corpId, appSecret, toUser, openKfId, mid);
|
|
166
|
+
default:
|
|
167
|
+
return sendFileMessage(corpId, appSecret, toUser, openKfId, mid);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Download media from an HTTP/HTTPS URL and return the buffer + filename.
|
|
172
|
+
*
|
|
173
|
+
* WeChat does not accept external URLs directly — media must be uploaded to
|
|
174
|
+
* the temporary media store first. This helper fetches the remote resource
|
|
175
|
+
* so the caller can then pass the buffer through `uploadAndSendMedia`.
|
|
176
|
+
*/
|
|
177
|
+
export async function downloadMediaFromUrl(url) {
|
|
178
|
+
const resp = await fetch(url, {
|
|
179
|
+
signal: AbortSignal.timeout(MEDIA_DOWNLOAD_TIMEOUT_MS),
|
|
180
|
+
});
|
|
181
|
+
if (!resp.ok) {
|
|
182
|
+
throw new Error(`${logTag()} failed to download media: HTTP ${resp.status} from ${url}`);
|
|
183
|
+
}
|
|
184
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
185
|
+
const urlPath = new URL(resp.url ?? url).pathname;
|
|
186
|
+
let filename = basename(urlPath) || "download";
|
|
187
|
+
let ext = extname(filename);
|
|
188
|
+
// Fall back to Content-Type when URL has no extension
|
|
189
|
+
if (!ext) {
|
|
190
|
+
const ct = resp.headers.get("content-type")?.split(";")[0]?.trim() ?? "";
|
|
191
|
+
ext = contentTypeToExt(ct);
|
|
192
|
+
if (ext)
|
|
193
|
+
filename = `${filename}${ext}`;
|
|
194
|
+
}
|
|
195
|
+
return { buffer, filename, ext };
|
|
196
|
+
}
|
|
197
|
+
function isMediaSource(value) {
|
|
198
|
+
return /^(https?:\/\/|file:\/\/|data:|[~/])/.test(value);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Resolve a thumbnail reference to a WeChat thumb_media_id.
|
|
202
|
+
*
|
|
203
|
+
* Accepts three kinds of input:
|
|
204
|
+
* - URL (http://, https://) or local path (/, ~, file://, data:) → loadWebMedia + uploadMedia
|
|
205
|
+
* - media_id string (anything else) → used directly
|
|
206
|
+
*/
|
|
207
|
+
export async function resolveThumbMediaId(thumbRef, corpId, appSecret) {
|
|
208
|
+
if (isMediaSource(thumbRef)) {
|
|
209
|
+
const core = getRuntime();
|
|
210
|
+
const loaded = await core.media.loadWebMedia(thumbRef, { optimizeImages: false });
|
|
211
|
+
const uploaded = await uploadMedia(corpId, appSecret, "image", loaded.buffer, loaded.fileName ?? "thumb.jpg");
|
|
212
|
+
return uploaded.media_id;
|
|
213
|
+
}
|
|
214
|
+
// Treat as raw media_id
|
|
215
|
+
return thumbRef;
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=send-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send-utils.js","sourceRoot":"","sources":["../src/send-utils.ts"],"names":[],"mappings":"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"}
|
package/dist/token.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* access_token retrieval and caching
|
|
3
|
+
*/
|
|
4
|
+
/** Hash the cache key so appSecret is never stored as a plain-text Map key. @internal */
|
|
5
|
+
export declare function makeCacheKey(corpId: string, appSecret: string): string;
|
|
6
|
+
export declare function getAccessToken(corpId: string, appSecret: string): Promise<string>;
|
|
7
|
+
/** Clear cached token (e.g. on auth error) */
|
|
8
|
+
export declare function clearAccessToken(corpId: string, appSecret: string): void;
|
package/dist/token.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* access_token retrieval and caching
|
|
3
|
+
*/
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { logTag, TOKEN_FETCH_TIMEOUT_MS } from "./constants.js";
|
|
6
|
+
import { getSharedContext } from "./monitor.js";
|
|
7
|
+
/** Hash the cache key so appSecret is never stored as a plain-text Map key. @internal */
|
|
8
|
+
export function makeCacheKey(corpId, appSecret) {
|
|
9
|
+
const hash = createHash("sha256").update(appSecret).digest("hex").slice(0, 16);
|
|
10
|
+
return `${corpId}:${hash}`;
|
|
11
|
+
}
|
|
12
|
+
const cache = new Map();
|
|
13
|
+
const pending = new Map();
|
|
14
|
+
const REFRESH_MARGIN_MS = 5 * 60 * 1000; // refresh 5 minutes before expiry
|
|
15
|
+
export async function getAccessToken(corpId, appSecret) {
|
|
16
|
+
const cacheKey = makeCacheKey(corpId, appSecret);
|
|
17
|
+
const cached = cache.get(cacheKey);
|
|
18
|
+
if (cached && Date.now() < cached.expiresAt - REFRESH_MARGIN_MS) {
|
|
19
|
+
return cached.token;
|
|
20
|
+
}
|
|
21
|
+
// Deduplicate concurrent requests for the same credentials
|
|
22
|
+
const inflight = pending.get(cacheKey);
|
|
23
|
+
if (inflight)
|
|
24
|
+
return inflight;
|
|
25
|
+
getSharedContext()?.botCtx.log?.debug?.(`${logTag()} fetching new access_token`);
|
|
26
|
+
const promise = fetchAccessToken(corpId, appSecret, cacheKey);
|
|
27
|
+
pending.set(cacheKey, promise);
|
|
28
|
+
try {
|
|
29
|
+
return await promise;
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
pending.delete(cacheKey);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function fetchAccessToken(corpId, appSecret, cacheKey) {
|
|
36
|
+
// WeChat API requires credentials in URL query parameters
|
|
37
|
+
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${encodeURIComponent(corpId)}&corpsecret=${encodeURIComponent(appSecret)}`;
|
|
38
|
+
const resp = await fetch(url, {
|
|
39
|
+
signal: AbortSignal.timeout(TOKEN_FETCH_TIMEOUT_MS),
|
|
40
|
+
});
|
|
41
|
+
if (!resp.ok) {
|
|
42
|
+
const text = await resp.text().catch(() => "");
|
|
43
|
+
throw new Error(`${logTag()} gettoken HTTP ${resp.status}: ${text.slice(0, 200)}`);
|
|
44
|
+
}
|
|
45
|
+
const data = (await resp.json());
|
|
46
|
+
if (data.errcode !== 0) {
|
|
47
|
+
throw new Error(`${logTag()} gettoken failed: ${data.errcode} ${data.errmsg}`);
|
|
48
|
+
}
|
|
49
|
+
cache.set(cacheKey, {
|
|
50
|
+
token: data.access_token,
|
|
51
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
52
|
+
});
|
|
53
|
+
getSharedContext()?.botCtx.log?.info(`${logTag()} access_token refreshed (expires_in=${data.expires_in}s)`);
|
|
54
|
+
return data.access_token;
|
|
55
|
+
}
|
|
56
|
+
/** Clear cached token (e.g. on auth error) */
|
|
57
|
+
export function clearAccessToken(corpId, appSecret) {
|
|
58
|
+
cache.delete(makeCacheKey(corpId, appSecret));
|
|
59
|
+
getSharedContext()?.botCtx.log?.debug?.(`${logTag()} access_token cache cleared`);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../src/token.ts"],"names":[],"mappings":"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"}
|