@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,157 @@
|
|
|
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
|
+
// Unicode Mathematical Alphanumeric offsets for A-Z, a-z
|
|
12
|
+
// Bold: U+1D400 (A) / U+1D41A (a)
|
|
13
|
+
// Italic: U+1D434 (A) / U+1D44E (a)
|
|
14
|
+
// Bold Italic: U+1D468 (A) / U+1D482 (a)
|
|
15
|
+
// Bold digits: U+1D7CE (0)
|
|
16
|
+
const BOLD_UPPER_START = 0x1d400;
|
|
17
|
+
const BOLD_LOWER_START = 0x1d41a;
|
|
18
|
+
const BOLD_DIGIT_START = 0x1d7ce;
|
|
19
|
+
const ITALIC_UPPER_START = 0x1d434;
|
|
20
|
+
const ITALIC_LOWER_START = 0x1d44e;
|
|
21
|
+
// Italic has no digit variant — use normal digits
|
|
22
|
+
const BOLD_ITALIC_UPPER_START = 0x1d468;
|
|
23
|
+
const BOLD_ITALIC_LOWER_START = 0x1d482;
|
|
24
|
+
function mapChar(ch, upperStart, lowerStart, digitStart) {
|
|
25
|
+
const code = ch.charCodeAt(0);
|
|
26
|
+
if (code >= 65 && code <= 90) {
|
|
27
|
+
// A-Z
|
|
28
|
+
return String.fromCodePoint(upperStart + (code - 65));
|
|
29
|
+
}
|
|
30
|
+
if (code >= 97 && code <= 122) {
|
|
31
|
+
// a-z
|
|
32
|
+
return String.fromCodePoint(lowerStart + (code - 97));
|
|
33
|
+
}
|
|
34
|
+
if (digitStart !== undefined && code >= 48 && code <= 57) {
|
|
35
|
+
// 0-9
|
|
36
|
+
return String.fromCodePoint(digitStart + (code - 48));
|
|
37
|
+
}
|
|
38
|
+
return ch;
|
|
39
|
+
}
|
|
40
|
+
function toBold(text) {
|
|
41
|
+
return [...text].map((ch) => mapChar(ch, BOLD_UPPER_START, BOLD_LOWER_START, BOLD_DIGIT_START)).join("");
|
|
42
|
+
}
|
|
43
|
+
function toItalic(text) {
|
|
44
|
+
return [...text]
|
|
45
|
+
.map((ch) => {
|
|
46
|
+
// Special case: italic lowercase 'h' is U+210E (ℎ) not in the block
|
|
47
|
+
if (ch === "h")
|
|
48
|
+
return "\u210E";
|
|
49
|
+
return mapChar(ch, ITALIC_UPPER_START, ITALIC_LOWER_START);
|
|
50
|
+
})
|
|
51
|
+
.join("");
|
|
52
|
+
}
|
|
53
|
+
function toBoldItalic(text) {
|
|
54
|
+
return [...text].map((ch) => mapChar(ch, BOLD_ITALIC_UPPER_START, BOLD_ITALIC_LOWER_START)).join("");
|
|
55
|
+
}
|
|
56
|
+
/** Regex matching any placeholder inserted by steps 1-4. */
|
|
57
|
+
const PLACEHOLDER_RE = /\x00(?:CB|IC|ES)\d+\x00/g;
|
|
58
|
+
/**
|
|
59
|
+
* Apply a Unicode transform while preserving placeholder sequences.
|
|
60
|
+
* Splits text on placeholder boundaries, transforms only non-placeholder
|
|
61
|
+
* segments, and reassembles.
|
|
62
|
+
*/
|
|
63
|
+
function applyTransform(text, transform) {
|
|
64
|
+
let result = "";
|
|
65
|
+
let lastIndex = 0;
|
|
66
|
+
for (const match of text.matchAll(PLACEHOLDER_RE)) {
|
|
67
|
+
const idx = match.index;
|
|
68
|
+
if (idx > lastIndex) {
|
|
69
|
+
result += transform(text.slice(lastIndex, idx));
|
|
70
|
+
}
|
|
71
|
+
result += match[0];
|
|
72
|
+
lastIndex = idx + match[0].length;
|
|
73
|
+
}
|
|
74
|
+
if (lastIndex < text.length) {
|
|
75
|
+
result += transform(text.slice(lastIndex));
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Convert markdown formatting to Unicode styled text.
|
|
81
|
+
*
|
|
82
|
+
* Handles:
|
|
83
|
+
* - `***text***` or `___text___` → bold italic
|
|
84
|
+
* - `**text**` or `__text__` → bold
|
|
85
|
+
* - `*text*` or `_text_` → italic
|
|
86
|
+
* - `` `code` `` → left as-is (backtick preserved)
|
|
87
|
+
* - ``` code blocks ``` → left as-is
|
|
88
|
+
* - `# headings` → 𝗛𝗲𝗮𝗱𝗶𝗻𝗴 (bold, # stripped)
|
|
89
|
+
* - `- list items` / `* list items` → • item
|
|
90
|
+
* - `1. numbered` → 1. (kept)
|
|
91
|
+
* - `[text](url)` → text (url)
|
|
92
|
+
* - `~~strikethrough~~` → stripped markers (no unicode strikethrough that's reliable)
|
|
93
|
+
*/
|
|
94
|
+
export function markdownToUnicode(text) {
|
|
95
|
+
if (!text)
|
|
96
|
+
return text;
|
|
97
|
+
// 1. Preserve code blocks
|
|
98
|
+
const codeBlocks = [];
|
|
99
|
+
let result = text.replace(/```[\s\S]*?```/g, (match) => {
|
|
100
|
+
codeBlocks.push(match.replace(/^```\w*\n?/, "").replace(/\n?```$/, ""));
|
|
101
|
+
return `\x00CB${codeBlocks.length - 1}\x00`;
|
|
102
|
+
});
|
|
103
|
+
// 2. Preserve inline code
|
|
104
|
+
const inlineCodes = [];
|
|
105
|
+
result = result.replace(/`([^`\n]+)`/g, (_match, code) => {
|
|
106
|
+
inlineCodes.push(code);
|
|
107
|
+
return `\x00IC${inlineCodes.length - 1}\x00`;
|
|
108
|
+
});
|
|
109
|
+
// 3. Images:  → [alt](url) (must come before link handling)
|
|
110
|
+
result = result.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "[$1]($2)");
|
|
111
|
+
// 4. Escape characters: protect \* \_ \~ \` \# \[ \] \( \) \\ \- \!
|
|
112
|
+
const escapes = [];
|
|
113
|
+
result = result.replace(/\\([*_~`#[\]()\\!-])/g, (_m, ch) => {
|
|
114
|
+
escapes.push(ch);
|
|
115
|
+
return `\x00ES${escapes.length - 1}\x00`;
|
|
116
|
+
});
|
|
117
|
+
// 5. Links: [text](url) → text (url)
|
|
118
|
+
result = result.replace(/\[([^\]]*)\]\(([^)]+)\)/g, "$1 ($2)");
|
|
119
|
+
// 6. Headings: # text → bold text (before bold/italic so inner markers are also bolded)
|
|
120
|
+
result = result.replace(/^(#{1,6})\s+(.+)$/gm, (_m, _hashes, content) => {
|
|
121
|
+
// Strip any inline bold/italic markers before applying bold
|
|
122
|
+
const stripped = content.replace(/\*{1,3}([^*]+)\*{1,3}/g, "$1").replace(/_{1,3}([^_]+)_{1,3}/g, "$1");
|
|
123
|
+
return applyTransform(stripped, toBold);
|
|
124
|
+
});
|
|
125
|
+
// 7. Bold italic: ***text*** or ___text___ (multiline with [\s\S])
|
|
126
|
+
result = result.replace(/\*{3}([\s\S]+?)\*{3}/g, (_m, inner) => applyTransform(inner, toBoldItalic));
|
|
127
|
+
result = result.replace(/_{3}([\s\S]+?)_{3}/g, (_m, inner) => applyTransform(inner, toBoldItalic));
|
|
128
|
+
// 8. Bold: **text** or __text__ (multiline with [\s\S])
|
|
129
|
+
result = result.replace(/\*{2}([\s\S]+?)\*{2}/g, (_m, inner) => applyTransform(inner, toBold));
|
|
130
|
+
result = result.replace(/_{2}([\s\S]+?)_{2}/g, (_m, inner) => applyTransform(inner, toBold));
|
|
131
|
+
// 9. Italic: *text* or _text_ (but not inside words for _)
|
|
132
|
+
// Use [^*\n] to avoid matching list markers like "* item *"
|
|
133
|
+
result = result.replace(/(?<!\w)\*([^*\n]+?)\*(?!\w)/g, (_m, inner) => applyTransform(inner, toItalic));
|
|
134
|
+
result = result.replace(/(?<!\w)_([^_\n]+?)_(?!\w)/g, (_m, inner) => applyTransform(inner, toItalic));
|
|
135
|
+
// 10. Strikethrough: ~~text~~ → just remove markers
|
|
136
|
+
result = result.replace(/~~([\s\S]+?)~~/g, "$1");
|
|
137
|
+
// 11. Task lists (must come before unordered list handling)
|
|
138
|
+
result = result.replace(/^([ \t]*)-\s+\[x\]\s+/gm, "$1\u2611 ");
|
|
139
|
+
result = result.replace(/^([ \t]*)-\s+\[ \]\s+/gm, "$1\u2610 ");
|
|
140
|
+
// 12. Unordered list: - item or * item → • item
|
|
141
|
+
result = result.replace(/^[ \t]*[-*]\s+/gm, "• ");
|
|
142
|
+
// 13. Blockquotes: > text → ┃ text
|
|
143
|
+
result = result.replace(/^>\s?/gm, "┃ ");
|
|
144
|
+
// 14. Horizontal rules
|
|
145
|
+
result = result.replace(/^[-*_]{3,}$/gm, "─────────");
|
|
146
|
+
// 15. Restore code blocks with visual markers
|
|
147
|
+
result = result.replace(/\x00CB(\d+)\x00/g, (_m, idx) => {
|
|
148
|
+
const code = codeBlocks[Number(idx)] ?? "";
|
|
149
|
+
return `\n━━━ code ━━━\n${code}\n━━━━━━━━━━━━\n`;
|
|
150
|
+
});
|
|
151
|
+
// 16. Restore inline code with backtick markers
|
|
152
|
+
result = result.replace(/\x00IC(\d+)\x00/g, (_m, idx) => `\`${inlineCodes[Number(idx)] ?? ""}\``);
|
|
153
|
+
// 17. Restore escape characters
|
|
154
|
+
result = result.replace(/\x00ES(\d+)\x00/g, (_m, idx) => escapes[Number(idx)] ?? "");
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=unicode-format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unicode-format.js","sourceRoot":"","sources":["../../src/unicode-format.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP webhook server for WeChat KF callbacks
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - GET: URL verification (echostr decrypt)
|
|
6
|
+
* - POST: Event notification (decrypt XML → trigger sync_msg)
|
|
7
|
+
*/
|
|
8
|
+
import { type Server } from "node:http";
|
|
9
|
+
export type WebhookHandler = (openKfId: string, token: string) => void | Promise<void>;
|
|
10
|
+
export type WebhookOptions = {
|
|
11
|
+
port: number;
|
|
12
|
+
path: string;
|
|
13
|
+
callbackToken: string;
|
|
14
|
+
encodingAESKey: string;
|
|
15
|
+
corpId: string;
|
|
16
|
+
onEvent: WebhookHandler;
|
|
17
|
+
log?: {
|
|
18
|
+
info: (...a: unknown[]) => void;
|
|
19
|
+
error: (...a: unknown[]) => void;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare function createWebhookServer(opts: WebhookOptions): Server;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP webhook server for WeChat KF callbacks
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - GET: URL verification (echostr decrypt)
|
|
6
|
+
* - POST: Event notification (decrypt XML → trigger sync_msg)
|
|
7
|
+
*/
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
import { decrypt, verifySignature } from "./crypto.js";
|
|
10
|
+
function parseQuery(url) {
|
|
11
|
+
const idx = url.indexOf("?");
|
|
12
|
+
if (idx < 0)
|
|
13
|
+
return {};
|
|
14
|
+
const params = {};
|
|
15
|
+
for (const pair of url.slice(idx + 1).split("&")) {
|
|
16
|
+
const eqIdx = pair.indexOf("=");
|
|
17
|
+
if (eqIdx < 0)
|
|
18
|
+
continue;
|
|
19
|
+
const k = pair.slice(0, eqIdx);
|
|
20
|
+
const v = pair.slice(eqIdx + 1);
|
|
21
|
+
if (k)
|
|
22
|
+
params[decodeURIComponent(k)] = decodeURIComponent(v);
|
|
23
|
+
}
|
|
24
|
+
return params;
|
|
25
|
+
}
|
|
26
|
+
function readBody(req, maxSize = 64 * 1024) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const chunks = [];
|
|
29
|
+
let size = 0;
|
|
30
|
+
let rejected = false;
|
|
31
|
+
req.on("data", (c) => {
|
|
32
|
+
if (rejected)
|
|
33
|
+
return;
|
|
34
|
+
size += c.length;
|
|
35
|
+
if (size > maxSize) {
|
|
36
|
+
rejected = true;
|
|
37
|
+
// Stop buffering but keep the connection alive so the server
|
|
38
|
+
// can still write a response (413). Resume drains remaining data.
|
|
39
|
+
req.removeAllListeners("data");
|
|
40
|
+
req.resume();
|
|
41
|
+
reject(new Error("[wechat-kf] request body too large"));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
chunks.push(c);
|
|
45
|
+
});
|
|
46
|
+
req.on("end", () => {
|
|
47
|
+
if (!rejected)
|
|
48
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
49
|
+
});
|
|
50
|
+
req.on("error", (err) => {
|
|
51
|
+
if (!rejected)
|
|
52
|
+
reject(err);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Extract a tag value from XML string */
|
|
57
|
+
function xmlTag(xml, tag) {
|
|
58
|
+
const re = new RegExp(`<${tag}><!\\[CDATA\\[(.+?)\\]\\]></${tag}>|<${tag}>(.+?)</${tag}>`);
|
|
59
|
+
const m = xml.match(re);
|
|
60
|
+
return m?.[1] ?? m?.[2];
|
|
61
|
+
}
|
|
62
|
+
export function createWebhookServer(opts) {
|
|
63
|
+
const { path, callbackToken, encodingAESKey, onEvent, log } = opts;
|
|
64
|
+
const server = createServer(async (req, res) => {
|
|
65
|
+
const url = req.url ?? "/";
|
|
66
|
+
const pathname = url.split("?")[0];
|
|
67
|
+
if (pathname !== path) {
|
|
68
|
+
res.writeHead(404);
|
|
69
|
+
res.end("not found");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const query = parseQuery(url);
|
|
73
|
+
try {
|
|
74
|
+
if (req.method === "GET") {
|
|
75
|
+
// URL verification
|
|
76
|
+
const { msg_signature, timestamp, nonce, echostr } = query;
|
|
77
|
+
if (!msg_signature || !timestamp || !nonce || !echostr) {
|
|
78
|
+
res.writeHead(400);
|
|
79
|
+
res.end("missing params");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!verifySignature(callbackToken, timestamp, nonce, echostr, msg_signature)) {
|
|
83
|
+
res.writeHead(403);
|
|
84
|
+
res.end("signature mismatch");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const { message } = decrypt(encodingAESKey, echostr);
|
|
88
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
89
|
+
res.end(message);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (req.method === "POST") {
|
|
93
|
+
const { msg_signature, timestamp, nonce } = query;
|
|
94
|
+
const body = await readBody(req);
|
|
95
|
+
// Extract Encrypt from XML
|
|
96
|
+
const encryptedMsg = xmlTag(body, "Encrypt");
|
|
97
|
+
if (!encryptedMsg || !msg_signature || !timestamp || !nonce) {
|
|
98
|
+
res.writeHead(400);
|
|
99
|
+
res.end("bad request");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (!verifySignature(callbackToken, timestamp, nonce, encryptedMsg, msg_signature)) {
|
|
103
|
+
res.writeHead(403);
|
|
104
|
+
res.end("signature mismatch");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const { message } = decrypt(encodingAESKey, encryptedMsg);
|
|
108
|
+
// Parse decrypted XML for Token and OpenKfId
|
|
109
|
+
const eventToken = xmlTag(message, "Token") ?? "";
|
|
110
|
+
const openKfId = xmlTag(message, "OpenKfId") ?? "";
|
|
111
|
+
// Respond immediately, process async
|
|
112
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
113
|
+
res.end("success");
|
|
114
|
+
// Trigger message sync
|
|
115
|
+
Promise.resolve(onEvent(openKfId, eventToken)).catch((err) => {
|
|
116
|
+
(log?.error ?? console.error)("[wechat-kf] onEvent error:", err);
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
res.writeHead(405);
|
|
121
|
+
res.end("method not allowed");
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (err instanceof Error && err.message.includes("body too large")) {
|
|
125
|
+
res.writeHead(413);
|
|
126
|
+
res.end("payload too large");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
(log?.error ?? console.error)("[wechat-kf] webhook error:", err);
|
|
130
|
+
if (!res.headersSent) {
|
|
131
|
+
res.writeHead(500);
|
|
132
|
+
res.end("internal error");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
return server;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=webhook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../src/webhook.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeChat KF directive parser
|
|
3
|
+
*
|
|
4
|
+
* Parses [[wechat_link: title | desc | url | thumbUrl]] directives
|
|
5
|
+
* embedded in agent text replies. The framework doesn't recognize these
|
|
6
|
+
* directives, so the text arrives intact for plugin-level interception.
|
|
7
|
+
*
|
|
8
|
+
* Syntax (pipe-separated fields):
|
|
9
|
+
* [[wechat_link: title | url]] → 2 fields
|
|
10
|
+
* [[wechat_link: title | desc | url]] → 3 fields
|
|
11
|
+
* [[wechat_link: title | desc | url | thumbUrl]] → 4 fields
|
|
12
|
+
*/
|
|
13
|
+
export type WechatLinkDirective = {
|
|
14
|
+
title: string;
|
|
15
|
+
desc?: string;
|
|
16
|
+
url: string;
|
|
17
|
+
thumbUrl?: string;
|
|
18
|
+
};
|
|
19
|
+
export type WechatDirectiveResult = {
|
|
20
|
+
text: string;
|
|
21
|
+
link?: WechatLinkDirective;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Quick check whether text contains a `[[wechat_link:...]]` directive.
|
|
25
|
+
*/
|
|
26
|
+
export declare function hasWechatLinkDirective(text: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Extract the first `[[wechat_link:...]]` directive from text.
|
|
29
|
+
*
|
|
30
|
+
* Returns the remaining text (with directive stripped and trimmed)
|
|
31
|
+
* plus the parsed link fields. If parsing fails (e.g. invalid URL),
|
|
32
|
+
* returns the original text unchanged with no link.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseWechatLinkDirective(text: string): WechatDirectiveResult;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeChat KF directive parser
|
|
3
|
+
*
|
|
4
|
+
* Parses [[wechat_link: title | desc | url | thumbUrl]] directives
|
|
5
|
+
* embedded in agent text replies. The framework doesn't recognize these
|
|
6
|
+
* directives, so the text arrives intact for plugin-level interception.
|
|
7
|
+
*
|
|
8
|
+
* Syntax (pipe-separated fields):
|
|
9
|
+
* [[wechat_link: title | url]] → 2 fields
|
|
10
|
+
* [[wechat_link: title | desc | url]] → 3 fields
|
|
11
|
+
* [[wechat_link: title | desc | url | thumbUrl]] → 4 fields
|
|
12
|
+
*/
|
|
13
|
+
const DIRECTIVE_RE = /\[\[wechat_link:\s*([^\]]+)\]\]/i;
|
|
14
|
+
/**
|
|
15
|
+
* Quick check whether text contains a `[[wechat_link:...]]` directive.
|
|
16
|
+
*/
|
|
17
|
+
export function hasWechatLinkDirective(text) {
|
|
18
|
+
return DIRECTIVE_RE.test(text);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extract the first `[[wechat_link:...]]` directive from text.
|
|
22
|
+
*
|
|
23
|
+
* Returns the remaining text (with directive stripped and trimmed)
|
|
24
|
+
* plus the parsed link fields. If parsing fails (e.g. invalid URL),
|
|
25
|
+
* returns the original text unchanged with no link.
|
|
26
|
+
*/
|
|
27
|
+
export function parseWechatLinkDirective(text) {
|
|
28
|
+
const match = DIRECTIVE_RE.exec(text);
|
|
29
|
+
if (!match) {
|
|
30
|
+
return { text };
|
|
31
|
+
}
|
|
32
|
+
const parts = match[1].split("|").map((s) => s.trim());
|
|
33
|
+
let link;
|
|
34
|
+
if (parts.length === 2) {
|
|
35
|
+
const [title, url] = parts;
|
|
36
|
+
if (isValidUrl(url)) {
|
|
37
|
+
link = { title, url };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (parts.length === 3) {
|
|
41
|
+
const [title, desc, url] = parts;
|
|
42
|
+
if (isValidUrl(url)) {
|
|
43
|
+
link = { title, desc, url };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (parts.length >= 4) {
|
|
47
|
+
const [title, desc, url, thumbUrl] = parts;
|
|
48
|
+
if (isValidUrl(url)) {
|
|
49
|
+
link = { title, desc, url, thumbUrl: thumbUrl || undefined };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!link) {
|
|
53
|
+
return { text };
|
|
54
|
+
}
|
|
55
|
+
// Strip the directive from text and clean up whitespace
|
|
56
|
+
const stripped = text
|
|
57
|
+
.replace(match[0], "")
|
|
58
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
59
|
+
.trim();
|
|
60
|
+
return { text: stripped, link };
|
|
61
|
+
}
|
|
62
|
+
function isValidUrl(url) {
|
|
63
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=wechat-kf-directives.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wechat-kf-directives.js","sourceRoot":"","sources":["../../src/wechat-kf-directives.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAcH,MAAM,YAAY,GAAG,kCAAkC,CAAC;AAExD;;GAEG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAAY;IACnD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,EAAE,IAAI,EAAE,CAAC;IAClB,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACvD,IAAI,IAAqC,CAAC;IAE1C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC;QAC3B,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,IAAI,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;SAAM,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC;QACjC,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,IAAI,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC;QAC9B,CAAC;IACH,CAAC;SAAM,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,KAAK,CAAC;QAC3C,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,IAAI,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,QAAQ,IAAI,SAAS,EAAE,CAAC;QAC/D,CAAC;IACH,CAAC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,IAAI,EAAE,CAAC;IAClB,CAAC;IAED,wDAAwD;IACxD,MAAM,QAAQ,GAAG,IAAI;SAClB,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;SACrB,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC;SAC1B,IAAI,EAAE,CAAC;IACV,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAClC,CAAC;AAED,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAO,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC;AACjE,CAAC"}
|
package/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeChat KF (微信客服) OpenClaw Channel Plugin
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { wechatKfPlugin } from "./src/channel.js";
|
|
6
|
+
import { type PluginRuntime, setRuntime } from "./src/runtime.js";
|
|
7
|
+
|
|
8
|
+
export { sendTextMessage, syncMessages } from "./src/api.js";
|
|
9
|
+
export { wechatKfPlugin } from "./src/channel.js";
|
|
10
|
+
export { computeSignature, decrypt, encrypt, verifySignature } from "./src/crypto.js";
|
|
11
|
+
export { getAccessToken } from "./src/token.js";
|
|
12
|
+
|
|
13
|
+
type OpenClawPluginApi = {
|
|
14
|
+
runtime: PluginRuntime;
|
|
15
|
+
registerChannel: (opts: { plugin: typeof wechatKfPlugin }) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const plugin = {
|
|
19
|
+
id: "wechat-kf",
|
|
20
|
+
name: "WeChat KF",
|
|
21
|
+
description: "WeChat Customer Service (企业微信客服) channel plugin",
|
|
22
|
+
// Plugin-level config schema (not channel-level).
|
|
23
|
+
// Channel config is handled via openclaw.plugin.json configSchema
|
|
24
|
+
// and the runtime schema in channel.ts → configSchema.
|
|
25
|
+
configSchema: { type: "object", additionalProperties: false, properties: {} },
|
|
26
|
+
register(api: OpenClawPluginApi) {
|
|
27
|
+
setRuntime(api.runtime);
|
|
28
|
+
api.registerChannel({ plugin: wechatKfPlugin });
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default plugin;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "wechat-kf",
|
|
3
|
+
"name": "WeChat Customer Service",
|
|
4
|
+
"description": "OpenClaw channel plugin for WeChat Customer Service (企业微信客服) via WeCom KF API",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"channels": ["wechat-kf"],
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"enabled": { "type": "boolean" },
|
|
11
|
+
"corpId": { "type": "string", "description": "WeCom Corp ID (企业ID)" },
|
|
12
|
+
"appSecret": { "type": "string", "description": "Self-built app secret (应用密钥)" },
|
|
13
|
+
"token": { "type": "string", "description": "Webhook callback token" },
|
|
14
|
+
"encodingAESKey": { "type": "string", "description": "43-character base64 AES key", "minLength": 43, "maxLength": 43 },
|
|
15
|
+
"webhookPort": { "type": "integer", "minimum": 1, "maximum": 65535, "default": 9999 },
|
|
16
|
+
"webhookPath": { "type": "string", "default": "/wechat-kf" },
|
|
17
|
+
"dmPolicy": { "type": "string", "enum": ["open", "pairing", "allowlist", "disabled"], "default": "open" },
|
|
18
|
+
"allowFrom": { "type": "array", "items": { "type": "string" } }
|
|
19
|
+
},
|
|
20
|
+
"uiHints": {
|
|
21
|
+
"corpId": { "label": "Corp ID", "placeholder": "ww0123456789abcdef" },
|
|
22
|
+
"appSecret": { "label": "App Secret", "placeholder": "Your WeCom app secret", "sensitive": true },
|
|
23
|
+
"token": { "label": "Callback Token", "placeholder": "Webhook verification token", "sensitive": true },
|
|
24
|
+
"encodingAESKey": { "label": "Encoding AES Key", "placeholder": "43-character base64 key", "sensitive": true },
|
|
25
|
+
"webhookPort": { "label": "Webhook Port", "placeholder": "9999" },
|
|
26
|
+
"webhookPath": { "label": "Webhook Path", "placeholder": "/wechat-kf" },
|
|
27
|
+
"dmPolicy": { "label": "DM Policy", "placeholder": "open" },
|
|
28
|
+
"allowFrom": { "label": "Allowlist", "placeholder": "List of allowed external user IDs" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pawastation/wechat-kf",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "OpenClaw channel plugin for WeChat Customer Service (企业微信客服) — enables AI-powered customer service bots via WeCom KF API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "pawaca",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/pawastation/wechat-kf.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/pawastation/wechat-kf#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/pawastation/wechat-kf/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"openclaw",
|
|
18
|
+
"wechat",
|
|
19
|
+
"wecom",
|
|
20
|
+
"customer-service",
|
|
21
|
+
"chatbot",
|
|
22
|
+
"ai-assistant"
|
|
23
|
+
],
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./crypto": {
|
|
32
|
+
"types": "./dist/src/crypto.d.ts",
|
|
33
|
+
"import": "./dist/src/crypto.js"
|
|
34
|
+
},
|
|
35
|
+
"./api": {
|
|
36
|
+
"types": "./dist/src/api.d.ts",
|
|
37
|
+
"import": "./dist/src/api.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist/",
|
|
42
|
+
"index.ts",
|
|
43
|
+
"openclaw.plugin.json",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@biomejs/biome": "^2.4.2",
|
|
55
|
+
"@types/node": "^25.2.3",
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"vitest": "^3.0.0"
|
|
58
|
+
},
|
|
59
|
+
"openclaw": {
|
|
60
|
+
"extensions": [
|
|
61
|
+
"./index.ts"
|
|
62
|
+
],
|
|
63
|
+
"channel": {
|
|
64
|
+
"id": "wechat-kf",
|
|
65
|
+
"label": "WeChat KF",
|
|
66
|
+
"selectionLabel": "WeChat Customer Service (微信客服)",
|
|
67
|
+
"docsPath": "/channels/wechat-kf",
|
|
68
|
+
"docsLabel": "wechat-kf",
|
|
69
|
+
"blurb": "企业微信客服 API channel for WeChat users.",
|
|
70
|
+
"aliases": [
|
|
71
|
+
"wxkf"
|
|
72
|
+
],
|
|
73
|
+
"order": 80
|
|
74
|
+
},
|
|
75
|
+
"install": {
|
|
76
|
+
"npmSpec": "@pawastation/wechat-kf",
|
|
77
|
+
"localPath": "extensions/wechat-kf",
|
|
78
|
+
"defaultChoice": "local"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"scripts": {
|
|
82
|
+
"build": "tsc",
|
|
83
|
+
"typecheck": "tsc --noEmit",
|
|
84
|
+
"test": "vitest run",
|
|
85
|
+
"test:watch": "vitest",
|
|
86
|
+
"lint": "biome check src/ index.ts",
|
|
87
|
+
"lint:fix": "biome check --write src/ index.ts",
|
|
88
|
+
"format": "biome format --write src/ index.ts",
|
|
89
|
+
"check": "biome check src/ index.ts"
|
|
90
|
+
}
|
|
91
|
+
}
|