@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.
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/README.zh-CN.md +401 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/src/accounts.d.ts +37 -0
- package/dist/src/accounts.js +205 -0
- package/dist/src/accounts.js.map +1 -0
- package/dist/src/api.d.ts +29 -0
- package/dist/src/api.js +172 -0
- package/dist/src/api.js.map +1 -0
- package/dist/src/bot.d.ts +35 -0
- package/dist/src/bot.js +379 -0
- package/dist/src/bot.js.map +1 -0
- package/dist/src/channel.d.ts +113 -0
- package/dist/src/channel.js +183 -0
- package/dist/src/channel.js.map +1 -0
- package/dist/src/chunk-utils.d.ts +18 -0
- package/dist/src/chunk-utils.js +58 -0
- package/dist/src/chunk-utils.js.map +1 -0
- package/dist/src/config-schema.d.ts +56 -0
- package/dist/src/config-schema.js +38 -0
- package/dist/src/config-schema.js.map +1 -0
- package/dist/src/constants.d.ts +19 -0
- package/dist/src/constants.js +20 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/crypto.d.ts +18 -0
- package/dist/src/crypto.js +80 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/fs-utils.d.ts +7 -0
- package/dist/src/fs-utils.js +13 -0
- package/dist/src/fs-utils.js.map +1 -0
- package/dist/src/monitor.d.ts +18 -0
- package/dist/src/monitor.js +131 -0
- package/dist/src/monitor.js.map +1 -0
- package/dist/src/outbound.d.ts +66 -0
- package/dist/src/outbound.js +234 -0
- package/dist/src/outbound.js.map +1 -0
- package/dist/src/reply-dispatcher.d.ts +40 -0
- package/dist/src/reply-dispatcher.js +120 -0
- package/dist/src/reply-dispatcher.js.map +1 -0
- package/dist/src/runtime.d.ts +130 -0
- package/dist/src/runtime.js +22 -0
- package/dist/src/runtime.js.map +1 -0
- package/dist/src/send-utils.d.ts +30 -0
- package/dist/src/send-utils.js +89 -0
- package/dist/src/send-utils.js.map +1 -0
- package/dist/src/send.d.ts +7 -0
- package/dist/src/send.js +13 -0
- package/dist/src/send.js.map +1 -0
- package/dist/src/token.d.ts +8 -0
- package/dist/src/token.js +57 -0
- package/dist/src/token.js.map +1 -0
- package/dist/src/types.d.ts +173 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/unicode-format.d.ts +26 -0
- package/dist/src/unicode-format.js +157 -0
- package/dist/src/unicode-format.js.map +1 -0
- package/dist/src/webhook.d.ts +22 -0
- package/dist/src/webhook.js +138 -0
- package/dist/src/webhook.js.map +1 -0
- package/dist/src/wechat-kf-directives.d.ts +34 -0
- package/dist/src/wechat-kf-directives.js +65 -0
- package/dist/src/wechat-kf-directives.js.map +1 -0
- package/index.ts +32 -0
- package/openclaw.plugin.json +31 -0
- package/package.json +91 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin runtime reference
|
|
3
|
+
* Stores the PluginRuntime provided by OpenClaw gateway at startup.
|
|
4
|
+
*/
|
|
5
|
+
import type { OpenClawConfig } from "./types.js";
|
|
6
|
+
export type SaveMediaResult = {
|
|
7
|
+
path: string;
|
|
8
|
+
};
|
|
9
|
+
export type AgentRoute = {
|
|
10
|
+
sessionKey: string;
|
|
11
|
+
agentId: string;
|
|
12
|
+
};
|
|
13
|
+
export type ResolveAgentRouteOpts = {
|
|
14
|
+
cfg: OpenClawConfig;
|
|
15
|
+
channel: string;
|
|
16
|
+
accountId: string;
|
|
17
|
+
peer: {
|
|
18
|
+
kind: string;
|
|
19
|
+
id: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export type EnvelopeFormatOptions = Record<string, unknown>;
|
|
23
|
+
export type FormatAgentEnvelopeOpts = {
|
|
24
|
+
channel: string;
|
|
25
|
+
from: string;
|
|
26
|
+
timestamp: Date;
|
|
27
|
+
envelope: EnvelopeFormatOptions;
|
|
28
|
+
body: string;
|
|
29
|
+
};
|
|
30
|
+
export type InboundContext = Record<string, unknown>;
|
|
31
|
+
export type FinalizeInboundContextOpts = {
|
|
32
|
+
Body: string;
|
|
33
|
+
RawBody: string;
|
|
34
|
+
CommandBody: string;
|
|
35
|
+
From: string;
|
|
36
|
+
To: string;
|
|
37
|
+
SessionKey: string;
|
|
38
|
+
AccountId: string;
|
|
39
|
+
ChatType: string;
|
|
40
|
+
SenderName: string;
|
|
41
|
+
SenderId: string;
|
|
42
|
+
Provider: string;
|
|
43
|
+
Surface: string;
|
|
44
|
+
MessageSid: string;
|
|
45
|
+
Timestamp: number;
|
|
46
|
+
WasMentioned: boolean;
|
|
47
|
+
CommandAuthorized: boolean;
|
|
48
|
+
OriginatingChannel: string;
|
|
49
|
+
OriginatingTo: string;
|
|
50
|
+
MediaPaths?: string[];
|
|
51
|
+
MediaTypes?: string[];
|
|
52
|
+
};
|
|
53
|
+
export type HumanDelayConfig = Record<string, unknown>;
|
|
54
|
+
export type ReplyPayload = {
|
|
55
|
+
text?: string;
|
|
56
|
+
attachments?: Array<{
|
|
57
|
+
path?: string;
|
|
58
|
+
type?: string;
|
|
59
|
+
url?: string;
|
|
60
|
+
}>;
|
|
61
|
+
};
|
|
62
|
+
export type ReplyErrorInfo = {
|
|
63
|
+
kind?: string;
|
|
64
|
+
};
|
|
65
|
+
export type ReplyDispatcherResult = {
|
|
66
|
+
dispatcher?: unknown;
|
|
67
|
+
replyOptions?: unknown;
|
|
68
|
+
markDispatchIdle?: () => void;
|
|
69
|
+
};
|
|
70
|
+
export type DispatchReplyResult = {
|
|
71
|
+
queuedFinal?: boolean;
|
|
72
|
+
counts?: {
|
|
73
|
+
final?: number;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
export type SystemEventOpts = {
|
|
77
|
+
sessionKey: string;
|
|
78
|
+
contextKey: string;
|
|
79
|
+
};
|
|
80
|
+
export type ChunkMode = "length" | "newline";
|
|
81
|
+
export interface PluginRuntime {
|
|
82
|
+
channel: {
|
|
83
|
+
media: {
|
|
84
|
+
saveMediaBuffer: (buffer: Buffer, mimeType: string, direction: string, opts: unknown | undefined, filename: string) => Promise<SaveMediaResult>;
|
|
85
|
+
};
|
|
86
|
+
routing: {
|
|
87
|
+
resolveAgentRoute: (opts: ResolveAgentRouteOpts) => AgentRoute;
|
|
88
|
+
};
|
|
89
|
+
reply: {
|
|
90
|
+
resolveEnvelopeFormatOptions: (cfg: OpenClawConfig) => EnvelopeFormatOptions;
|
|
91
|
+
formatAgentEnvelope: (opts: FormatAgentEnvelopeOpts) => string;
|
|
92
|
+
finalizeInboundContext: (opts: FinalizeInboundContextOpts) => InboundContext;
|
|
93
|
+
createReplyDispatcherWithTyping: (opts: {
|
|
94
|
+
humanDelay: HumanDelayConfig;
|
|
95
|
+
deliver: (payload: ReplyPayload) => Promise<void>;
|
|
96
|
+
onError: (err: unknown, info: ReplyErrorInfo) => void;
|
|
97
|
+
}) => ReplyDispatcherResult;
|
|
98
|
+
dispatchReplyFromConfig: (opts: {
|
|
99
|
+
ctx: InboundContext;
|
|
100
|
+
cfg: OpenClawConfig;
|
|
101
|
+
dispatcher: unknown;
|
|
102
|
+
replyOptions: unknown;
|
|
103
|
+
}) => Promise<DispatchReplyResult>;
|
|
104
|
+
resolveHumanDelayConfig: (cfg: OpenClawConfig, agentId: string) => HumanDelayConfig;
|
|
105
|
+
};
|
|
106
|
+
text: {
|
|
107
|
+
resolveTextChunkLimit: (cfg: OpenClawConfig, channel: string, accountId: string, opts: {
|
|
108
|
+
fallbackLimit: number;
|
|
109
|
+
}) => number;
|
|
110
|
+
resolveChunkMode: (cfg: OpenClawConfig, channel: string) => ChunkMode;
|
|
111
|
+
chunkTextWithMode: (text: string, limit: number, mode: ChunkMode) => string[];
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
system: {
|
|
115
|
+
enqueueSystemEvent: (message: string, opts: SystemEventOpts) => void;
|
|
116
|
+
};
|
|
117
|
+
state?: {
|
|
118
|
+
resolveStateDir?: () => string;
|
|
119
|
+
};
|
|
120
|
+
/** Optional error logger exposed by some runtime implementations. */
|
|
121
|
+
error?: (...args: unknown[]) => void;
|
|
122
|
+
[key: string]: unknown;
|
|
123
|
+
}
|
|
124
|
+
export declare function setRuntime(next: PluginRuntime): void;
|
|
125
|
+
export declare function getRuntime(): PluginRuntime;
|
|
126
|
+
/**
|
|
127
|
+
* Reset the module-level runtime reference to null.
|
|
128
|
+
* @internal Exposed for testing only — allows test isolation between runs.
|
|
129
|
+
*/
|
|
130
|
+
export declare function _reset(): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin runtime reference
|
|
3
|
+
* Stores the PluginRuntime provided by OpenClaw gateway at startup.
|
|
4
|
+
*/
|
|
5
|
+
let runtime = null;
|
|
6
|
+
export function setRuntime(next) {
|
|
7
|
+
runtime = next;
|
|
8
|
+
}
|
|
9
|
+
export function getRuntime() {
|
|
10
|
+
if (!runtime) {
|
|
11
|
+
throw new Error("[wechat-kf] runtime not initialized — plugin not started via gateway?");
|
|
12
|
+
}
|
|
13
|
+
return runtime;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reset the module-level runtime reference to null.
|
|
17
|
+
* @internal Exposed for testing only — allows test isolation between runs.
|
|
18
|
+
*/
|
|
19
|
+
export function _reset() {
|
|
20
|
+
runtime = null;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/runtime.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAsIH,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,uEAAuE,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,30 @@
|
|
|
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
|
+
/** Markdown to Unicode text formatting (shared by both outbound paths) */
|
|
12
|
+
export declare function formatText(text: string): string;
|
|
13
|
+
/** Map file extension to WeChat media type */
|
|
14
|
+
export declare function detectMediaType(ext: string): "image" | "voice" | "video" | "file";
|
|
15
|
+
/** Upload media to WeChat and send via the appropriate message type */
|
|
16
|
+
export declare function uploadAndSendMedia(corpId: string, appSecret: string, toUser: string, openKfId: string, buffer: Buffer, filename: string, mediaType: "image" | "voice" | "video" | "file"): Promise<{
|
|
17
|
+
msgid: string;
|
|
18
|
+
}>;
|
|
19
|
+
/**
|
|
20
|
+
* Download media from an HTTP/HTTPS URL and return the buffer + filename.
|
|
21
|
+
*
|
|
22
|
+
* WeChat does not accept external URLs directly — media must be uploaded to
|
|
23
|
+
* the temporary media store first. This helper fetches the remote resource
|
|
24
|
+
* so the caller can then pass the buffer through `uploadAndSendMedia`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function downloadMediaFromUrl(url: string): Promise<{
|
|
27
|
+
buffer: Buffer;
|
|
28
|
+
filename: string;
|
|
29
|
+
ext: string;
|
|
30
|
+
}>;
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { MEDIA_DOWNLOAD_TIMEOUT_MS } from "./constants.js";
|
|
14
|
+
import { markdownToUnicode } from "./unicode-format.js";
|
|
15
|
+
/** Markdown to Unicode text formatting (shared by both outbound paths) */
|
|
16
|
+
export function formatText(text) {
|
|
17
|
+
return markdownToUnicode(text);
|
|
18
|
+
}
|
|
19
|
+
const CONTENT_TYPE_EXT_MAP = {
|
|
20
|
+
"image/jpeg": ".jpg",
|
|
21
|
+
"image/png": ".png",
|
|
22
|
+
"image/gif": ".gif",
|
|
23
|
+
"image/webp": ".webp",
|
|
24
|
+
"image/bmp": ".bmp",
|
|
25
|
+
"audio/amr": ".amr",
|
|
26
|
+
"audio/mpeg": ".mp3",
|
|
27
|
+
"audio/ogg": ".ogg",
|
|
28
|
+
"audio/wav": ".wav",
|
|
29
|
+
"audio/x-wav": ".wav",
|
|
30
|
+
"video/mp4": ".mp4",
|
|
31
|
+
"application/pdf": ".pdf",
|
|
32
|
+
};
|
|
33
|
+
function contentTypeToExt(contentType) {
|
|
34
|
+
return CONTENT_TYPE_EXT_MAP[contentType] ?? "";
|
|
35
|
+
}
|
|
36
|
+
/** Map file extension to WeChat media type */
|
|
37
|
+
export function detectMediaType(ext) {
|
|
38
|
+
ext = ext.toLowerCase();
|
|
39
|
+
if ([".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"].includes(ext))
|
|
40
|
+
return "image";
|
|
41
|
+
if ([".amr", ".mp3", ".wav", ".ogg", ".silk", ".m4a", ".aac"].includes(ext))
|
|
42
|
+
return "voice";
|
|
43
|
+
if ([".mp4", ".avi", ".mov", ".mkv", ".wmv"].includes(ext))
|
|
44
|
+
return "video";
|
|
45
|
+
return "file";
|
|
46
|
+
}
|
|
47
|
+
/** Upload media to WeChat and send via the appropriate message type */
|
|
48
|
+
export async function uploadAndSendMedia(corpId, appSecret, toUser, openKfId, buffer, filename, mediaType) {
|
|
49
|
+
const uploaded = await uploadMedia(corpId, appSecret, mediaType, buffer, filename);
|
|
50
|
+
const mid = uploaded.media_id;
|
|
51
|
+
switch (mediaType) {
|
|
52
|
+
case "image":
|
|
53
|
+
return sendImageMessage(corpId, appSecret, toUser, openKfId, mid);
|
|
54
|
+
case "voice":
|
|
55
|
+
return sendVoiceMessage(corpId, appSecret, toUser, openKfId, mid);
|
|
56
|
+
case "video":
|
|
57
|
+
return sendVideoMessage(corpId, appSecret, toUser, openKfId, mid);
|
|
58
|
+
default:
|
|
59
|
+
return sendFileMessage(corpId, appSecret, toUser, openKfId, mid);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Download media from an HTTP/HTTPS URL and return the buffer + filename.
|
|
64
|
+
*
|
|
65
|
+
* WeChat does not accept external URLs directly — media must be uploaded to
|
|
66
|
+
* the temporary media store first. This helper fetches the remote resource
|
|
67
|
+
* so the caller can then pass the buffer through `uploadAndSendMedia`.
|
|
68
|
+
*/
|
|
69
|
+
export async function downloadMediaFromUrl(url) {
|
|
70
|
+
const resp = await fetch(url, {
|
|
71
|
+
signal: AbortSignal.timeout(MEDIA_DOWNLOAD_TIMEOUT_MS),
|
|
72
|
+
});
|
|
73
|
+
if (!resp.ok) {
|
|
74
|
+
throw new Error(`[wechat-kf] failed to download media: HTTP ${resp.status} from ${url}`);
|
|
75
|
+
}
|
|
76
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
77
|
+
const urlPath = new URL(resp.url ?? url).pathname;
|
|
78
|
+
let filename = basename(urlPath) || "download";
|
|
79
|
+
let ext = extname(filename);
|
|
80
|
+
// Fall back to Content-Type when URL has no extension
|
|
81
|
+
if (!ext) {
|
|
82
|
+
const ct = resp.headers.get("content-type")?.split(";")[0]?.trim() ?? "";
|
|
83
|
+
ext = contentTypeToExt(ct);
|
|
84
|
+
if (ext)
|
|
85
|
+
filename = `${filename}${ext}`;
|
|
86
|
+
}
|
|
87
|
+
return { buffer, filename, ext };
|
|
88
|
+
}
|
|
89
|
+
//# 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/src/send.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send message helpers
|
|
3
|
+
*/
|
|
4
|
+
import { sendTextMessage } from "./api.js";
|
|
5
|
+
export async function sendText(account, toUser, content) {
|
|
6
|
+
const openKfId = account.openKfId ?? account.accountId;
|
|
7
|
+
if (!account.corpId || !account.appSecret || !openKfId) {
|
|
8
|
+
throw new Error("[wechat-kf] missing corpId/appSecret/openKfId for sending");
|
|
9
|
+
}
|
|
10
|
+
const result = await sendTextMessage(account.corpId, account.appSecret, toUser, openKfId, content);
|
|
11
|
+
return { messageId: result.msgid };
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=send.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/send.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAG3C,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,OAAgC,EAChC,MAAc,EACd,OAAe;IAEf,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,SAAS,CAAC;IACvD,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,SAAS,IAAI,CAAC,QAAQ,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC/E,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACnG,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;AACrC,CAAC"}
|
|
@@ -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;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* access_token retrieval and caching
|
|
3
|
+
*/
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { TOKEN_FETCH_TIMEOUT_MS } from "./constants.js";
|
|
6
|
+
/** Hash the cache key so appSecret is never stored as a plain-text Map key. @internal */
|
|
7
|
+
export function makeCacheKey(corpId, appSecret) {
|
|
8
|
+
const hash = createHash("sha256").update(appSecret).digest("hex").slice(0, 16);
|
|
9
|
+
return `${corpId}:${hash}`;
|
|
10
|
+
}
|
|
11
|
+
const cache = new Map();
|
|
12
|
+
const pending = new Map();
|
|
13
|
+
const REFRESH_MARGIN_MS = 5 * 60 * 1000; // refresh 5 minutes before expiry
|
|
14
|
+
export async function getAccessToken(corpId, appSecret) {
|
|
15
|
+
const cacheKey = makeCacheKey(corpId, appSecret);
|
|
16
|
+
const cached = cache.get(cacheKey);
|
|
17
|
+
if (cached && Date.now() < cached.expiresAt - REFRESH_MARGIN_MS) {
|
|
18
|
+
return cached.token;
|
|
19
|
+
}
|
|
20
|
+
// Deduplicate concurrent requests for the same credentials
|
|
21
|
+
const inflight = pending.get(cacheKey);
|
|
22
|
+
if (inflight)
|
|
23
|
+
return inflight;
|
|
24
|
+
const promise = fetchAccessToken(corpId, appSecret, cacheKey);
|
|
25
|
+
pending.set(cacheKey, promise);
|
|
26
|
+
try {
|
|
27
|
+
return await promise;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
pending.delete(cacheKey);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function fetchAccessToken(corpId, appSecret, cacheKey) {
|
|
34
|
+
// WeChat API requires credentials in URL query parameters
|
|
35
|
+
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=${encodeURIComponent(corpId)}&corpsecret=${encodeURIComponent(appSecret)}`;
|
|
36
|
+
const resp = await fetch(url, {
|
|
37
|
+
signal: AbortSignal.timeout(TOKEN_FETCH_TIMEOUT_MS),
|
|
38
|
+
});
|
|
39
|
+
if (!resp.ok) {
|
|
40
|
+
const text = await resp.text().catch(() => "");
|
|
41
|
+
throw new Error(`[wechat-kf] gettoken HTTP ${resp.status}: ${text.slice(0, 200)}`);
|
|
42
|
+
}
|
|
43
|
+
const data = (await resp.json());
|
|
44
|
+
if (data.errcode !== 0) {
|
|
45
|
+
throw new Error(`[wechat-kf] gettoken failed: ${data.errcode} ${data.errmsg}`);
|
|
46
|
+
}
|
|
47
|
+
cache.set(cacheKey, {
|
|
48
|
+
token: data.access_token,
|
|
49
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
50
|
+
});
|
|
51
|
+
return data.access_token;
|
|
52
|
+
}
|
|
53
|
+
/** Clear cached token (e.g. on auth error) */
|
|
54
|
+
export function clearAccessToken(corpId, appSecret) {
|
|
55
|
+
cache.delete(makeCacheKey(corpId, appSecret));
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/token.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
export type WechatKfConfig = {
|
|
2
|
+
enabled?: boolean;
|
|
3
|
+
corpId?: string;
|
|
4
|
+
appSecret?: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
encodingAESKey?: string;
|
|
7
|
+
webhookPort?: number;
|
|
8
|
+
webhookPath?: string;
|
|
9
|
+
dmPolicy?: "open" | "pairing" | "allowlist" | "disabled";
|
|
10
|
+
allowFrom?: string[];
|
|
11
|
+
};
|
|
12
|
+
export type OpenClawConfig = {
|
|
13
|
+
channels?: Record<string, unknown>;
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
};
|
|
16
|
+
export type ResolvedWechatKfAccount = {
|
|
17
|
+
accountId: string;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
configured: boolean;
|
|
20
|
+
corpId?: string;
|
|
21
|
+
appSecret?: string;
|
|
22
|
+
token?: string;
|
|
23
|
+
encodingAESKey?: string;
|
|
24
|
+
openKfId?: string;
|
|
25
|
+
webhookPort: number;
|
|
26
|
+
webhookPath: string;
|
|
27
|
+
config: WechatKfConfig;
|
|
28
|
+
};
|
|
29
|
+
export type WechatAccessTokenResponse = {
|
|
30
|
+
errcode: number;
|
|
31
|
+
errmsg: string;
|
|
32
|
+
access_token: string;
|
|
33
|
+
expires_in: number;
|
|
34
|
+
};
|
|
35
|
+
export type WechatKfSyncMsgRequest = {
|
|
36
|
+
cursor?: string;
|
|
37
|
+
token?: string;
|
|
38
|
+
limit?: number;
|
|
39
|
+
voice_format?: number;
|
|
40
|
+
open_kfid?: string;
|
|
41
|
+
};
|
|
42
|
+
export type WechatKfMergedMsgItem = {
|
|
43
|
+
sender_name?: string;
|
|
44
|
+
msg_content?: string;
|
|
45
|
+
};
|
|
46
|
+
export type WechatKfMessage = {
|
|
47
|
+
msgid: string;
|
|
48
|
+
open_kfid: string;
|
|
49
|
+
external_userid: string;
|
|
50
|
+
send_time: number;
|
|
51
|
+
origin: number;
|
|
52
|
+
servicer_userid?: string;
|
|
53
|
+
msgtype: string;
|
|
54
|
+
text?: {
|
|
55
|
+
content: string;
|
|
56
|
+
};
|
|
57
|
+
image?: {
|
|
58
|
+
media_id: string;
|
|
59
|
+
};
|
|
60
|
+
voice?: {
|
|
61
|
+
media_id: string;
|
|
62
|
+
};
|
|
63
|
+
video?: {
|
|
64
|
+
media_id: string;
|
|
65
|
+
};
|
|
66
|
+
file?: {
|
|
67
|
+
media_id: string;
|
|
68
|
+
};
|
|
69
|
+
location?: {
|
|
70
|
+
latitude: number;
|
|
71
|
+
longitude: number;
|
|
72
|
+
name: string;
|
|
73
|
+
address: string;
|
|
74
|
+
};
|
|
75
|
+
link?: {
|
|
76
|
+
title: string;
|
|
77
|
+
desc: string;
|
|
78
|
+
url: string;
|
|
79
|
+
pic_url: string;
|
|
80
|
+
};
|
|
81
|
+
event?: {
|
|
82
|
+
event_type: string;
|
|
83
|
+
open_kfid?: string;
|
|
84
|
+
external_userid?: string;
|
|
85
|
+
scene?: string;
|
|
86
|
+
scene_param?: string;
|
|
87
|
+
welcome_code?: string;
|
|
88
|
+
fail_msgid?: string;
|
|
89
|
+
fail_type?: number;
|
|
90
|
+
servicer_userid?: string;
|
|
91
|
+
status?: number;
|
|
92
|
+
};
|
|
93
|
+
merged_msg?: {
|
|
94
|
+
title?: string;
|
|
95
|
+
item?: WechatKfMergedMsgItem[];
|
|
96
|
+
};
|
|
97
|
+
channels?: {
|
|
98
|
+
nickname?: string;
|
|
99
|
+
title?: string;
|
|
100
|
+
sub_type?: number;
|
|
101
|
+
};
|
|
102
|
+
miniprogram?: {
|
|
103
|
+
title?: string;
|
|
104
|
+
appid?: string;
|
|
105
|
+
pagepath?: string;
|
|
106
|
+
};
|
|
107
|
+
business_card?: {
|
|
108
|
+
userid?: string;
|
|
109
|
+
};
|
|
110
|
+
msgmenu?: {
|
|
111
|
+
head_content?: string;
|
|
112
|
+
list?: {
|
|
113
|
+
id: string;
|
|
114
|
+
content?: string;
|
|
115
|
+
}[];
|
|
116
|
+
tail_content?: string;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
export type WechatKfSyncMsgResponse = {
|
|
120
|
+
errcode: number;
|
|
121
|
+
errmsg: string;
|
|
122
|
+
next_cursor: string;
|
|
123
|
+
has_more: number;
|
|
124
|
+
msg_list: WechatKfMessage[];
|
|
125
|
+
};
|
|
126
|
+
export type WechatKfSendMsgRequest = {
|
|
127
|
+
touser: string;
|
|
128
|
+
open_kfid: string;
|
|
129
|
+
msgid?: string;
|
|
130
|
+
msgtype: string;
|
|
131
|
+
text?: {
|
|
132
|
+
content: string;
|
|
133
|
+
};
|
|
134
|
+
image?: {
|
|
135
|
+
media_id: string;
|
|
136
|
+
};
|
|
137
|
+
file?: {
|
|
138
|
+
media_id: string;
|
|
139
|
+
};
|
|
140
|
+
voice?: {
|
|
141
|
+
media_id: string;
|
|
142
|
+
};
|
|
143
|
+
video?: {
|
|
144
|
+
media_id: string;
|
|
145
|
+
};
|
|
146
|
+
link?: {
|
|
147
|
+
title: string;
|
|
148
|
+
desc?: string;
|
|
149
|
+
url: string;
|
|
150
|
+
thumb_media_id: string;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
export type WechatKfSendMsgResponse = {
|
|
154
|
+
errcode: number;
|
|
155
|
+
errmsg: string;
|
|
156
|
+
msgid: string;
|
|
157
|
+
};
|
|
158
|
+
export type WechatMediaUploadResponse = {
|
|
159
|
+
errcode: number;
|
|
160
|
+
errmsg: string;
|
|
161
|
+
type: string;
|
|
162
|
+
media_id: string;
|
|
163
|
+
created_at: number;
|
|
164
|
+
};
|
|
165
|
+
export type WechatCallbackXml = {
|
|
166
|
+
ToUserName: string;
|
|
167
|
+
CreateTime: string;
|
|
168
|
+
MsgType: string;
|
|
169
|
+
Event?: string;
|
|
170
|
+
Token?: string;
|
|
171
|
+
OpenKfId?: string;
|
|
172
|
+
Encrypt?: string;
|
|
173
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,+BAA+B"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown → Unicode text formatting
|
|
3
|
+
*
|
|
4
|
+
* Converts markdown bold/italic/bold-italic to Unicode Mathematical
|
|
5
|
+
* Alphanumeric Symbols. Only converts ASCII letters (a-z, A-Z) and
|
|
6
|
+
* digits (0-9) — other characters pass through unchanged.
|
|
7
|
+
*
|
|
8
|
+
* This is meant for plain-text surfaces like WeChat KF where
|
|
9
|
+
* rich text / HTML isn't supported.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Convert markdown formatting to Unicode styled text.
|
|
13
|
+
*
|
|
14
|
+
* Handles:
|
|
15
|
+
* - `***text***` or `___text___` → bold italic
|
|
16
|
+
* - `**text**` or `__text__` → bold
|
|
17
|
+
* - `*text*` or `_text_` → italic
|
|
18
|
+
* - `` `code` `` → left as-is (backtick preserved)
|
|
19
|
+
* - ``` code blocks ``` → left as-is
|
|
20
|
+
* - `# headings` → 𝗛𝗲𝗮𝗱𝗶𝗻𝗴 (bold, # stripped)
|
|
21
|
+
* - `- list items` / `* list items` → • item
|
|
22
|
+
* - `1. numbered` → 1. (kept)
|
|
23
|
+
* - `[text](url)` → text (url)
|
|
24
|
+
* - `~~strikethrough~~` → stripped markers (no unicode strikethrough that's reliable)
|
|
25
|
+
*/
|
|
26
|
+
export declare function markdownToUnicode(text: string): string;
|