md2wechat-mcp 0.2.0 → 0.2.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/README.md +304 -5
- package/dist/browser.js +2 -0
- package/dist/cache.js +4 -3
- package/dist/cli.js +10 -1
- package/dist/markdown.d.ts +2 -2
- package/dist/markdown.js +11 -7
- package/dist/server.js +73 -9
- package/dist/themes.d.ts +2 -0
- package/dist/themes.js +41 -15
- package/dist/tools.js +577 -15
- package/dist/wechat-api.d.ts +10 -0
- package/dist/wechat-api.js +51 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -3,7 +3,8 @@ import { saveHtmlCache } from "./cache.js";
|
|
|
3
3
|
import { openFileInBrowser } from "./browser.js";
|
|
4
4
|
import { THEME_NAMES, THEMES } from "./themes.js";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
|
-
import {
|
|
6
|
+
import { basename, dirname, extname, resolve } from "node:path";
|
|
7
|
+
import { getAccessToken, draftAdd, draftUpdate, draftDelete, draftBatchGet, uploadImage, addMaterial } from "./wechat-api.js";
|
|
7
8
|
export function listThemesPayload() {
|
|
8
9
|
return {
|
|
9
10
|
themes: [
|
|
@@ -21,6 +22,13 @@ function invalidThemeResult(theme) {
|
|
|
21
22
|
isError: true
|
|
22
23
|
};
|
|
23
24
|
}
|
|
25
|
+
const FONT_SIZE_PRESETS = ["small", "medium", "large"];
|
|
26
|
+
function invalidFontSizePresetResult(preset) {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: `Invalid font_size_preset: ${String(preset)}. Available: ${FONT_SIZE_PRESETS.join(", ")}` }],
|
|
29
|
+
isError: true
|
|
30
|
+
};
|
|
31
|
+
}
|
|
24
32
|
function validateMarkdown(markdown) {
|
|
25
33
|
return typeof markdown === "string" && markdown.trim().length > 0;
|
|
26
34
|
}
|
|
@@ -30,6 +38,404 @@ function validateMarkdownPath(path) {
|
|
|
30
38
|
function validateCachePath(path) {
|
|
31
39
|
return typeof path === "string" && path.trim().length > 0;
|
|
32
40
|
}
|
|
41
|
+
function getFileExtension(inputPath) {
|
|
42
|
+
const cleaned = inputPath.split(/[?#]/)[0] ?? inputPath;
|
|
43
|
+
return extname(cleaned).toLowerCase().replace(".", "");
|
|
44
|
+
}
|
|
45
|
+
function isRemoteUrl(url) {
|
|
46
|
+
return /^https?:\/\//i.test(url);
|
|
47
|
+
}
|
|
48
|
+
function isWhitespace(char) {
|
|
49
|
+
return /\s/.test(char);
|
|
50
|
+
}
|
|
51
|
+
function mergeRanges(ranges) {
|
|
52
|
+
if (ranges.length === 0)
|
|
53
|
+
return [];
|
|
54
|
+
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
55
|
+
const merged = [sorted[0]];
|
|
56
|
+
for (let i = 1; i < sorted.length; i += 1) {
|
|
57
|
+
const current = sorted[i];
|
|
58
|
+
const last = merged[merged.length - 1];
|
|
59
|
+
if (current.start <= last.end) {
|
|
60
|
+
last.end = Math.max(last.end, current.end);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
merged.push({ ...current });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
function computeExcludedRanges(markdown) {
|
|
69
|
+
const ranges = [];
|
|
70
|
+
const lines = markdown.split("\n");
|
|
71
|
+
let offset = 0;
|
|
72
|
+
let inFence = false;
|
|
73
|
+
let fenceChar = "";
|
|
74
|
+
let fenceLen = 0;
|
|
75
|
+
let fenceStart = -1;
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
const trimmedLeft = line.replace(/^\s{0,3}/, "");
|
|
78
|
+
const fenceMatch = trimmedLeft.match(/^(`{3,}|~{3,})/);
|
|
79
|
+
if (fenceMatch) {
|
|
80
|
+
const marker = fenceMatch[1];
|
|
81
|
+
const markerChar = marker[0];
|
|
82
|
+
const markerLen = marker.length;
|
|
83
|
+
if (!inFence) {
|
|
84
|
+
inFence = true;
|
|
85
|
+
fenceChar = markerChar;
|
|
86
|
+
fenceLen = markerLen;
|
|
87
|
+
fenceStart = offset;
|
|
88
|
+
}
|
|
89
|
+
else if (markerChar === fenceChar && markerLen >= fenceLen) {
|
|
90
|
+
ranges.push({ start: fenceStart, end: offset + line.length + 1 });
|
|
91
|
+
inFence = false;
|
|
92
|
+
fenceChar = "";
|
|
93
|
+
fenceLen = 0;
|
|
94
|
+
fenceStart = -1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
offset += line.length + 1;
|
|
98
|
+
}
|
|
99
|
+
if (inFence && fenceStart >= 0) {
|
|
100
|
+
ranges.push({ start: fenceStart, end: markdown.length });
|
|
101
|
+
}
|
|
102
|
+
// inline code spans outside fenced code
|
|
103
|
+
const fenceRanges = mergeRanges(ranges);
|
|
104
|
+
let i = 0;
|
|
105
|
+
let fenceIdx = 0;
|
|
106
|
+
while (i < markdown.length) {
|
|
107
|
+
const activeFence = fenceRanges[fenceIdx];
|
|
108
|
+
if (activeFence && i >= activeFence.start) {
|
|
109
|
+
i = Math.max(i, activeFence.end);
|
|
110
|
+
fenceIdx += 1;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (markdown[i] !== "`") {
|
|
114
|
+
i += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
let runLen = 1;
|
|
118
|
+
while (i + runLen < markdown.length && markdown[i + runLen] === "`")
|
|
119
|
+
runLen += 1;
|
|
120
|
+
const start = i;
|
|
121
|
+
i += runLen;
|
|
122
|
+
let close = -1;
|
|
123
|
+
while (i < markdown.length) {
|
|
124
|
+
if (markdown[i] !== "`") {
|
|
125
|
+
i += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let closeRun = 1;
|
|
129
|
+
while (i + closeRun < markdown.length && markdown[i + closeRun] === "`")
|
|
130
|
+
closeRun += 1;
|
|
131
|
+
if (closeRun === runLen) {
|
|
132
|
+
close = i + runLen;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
i += closeRun;
|
|
136
|
+
}
|
|
137
|
+
if (close > 0) {
|
|
138
|
+
ranges.push({ start, end: close });
|
|
139
|
+
i = close;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
i = start + runLen;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return mergeRanges(ranges);
|
|
146
|
+
}
|
|
147
|
+
function findRangeAt(ranges, index) {
|
|
148
|
+
let left = 0;
|
|
149
|
+
let right = ranges.length - 1;
|
|
150
|
+
while (left <= right) {
|
|
151
|
+
const mid = Math.floor((left + right) / 2);
|
|
152
|
+
const range = ranges[mid];
|
|
153
|
+
if (index < range.start)
|
|
154
|
+
right = mid - 1;
|
|
155
|
+
else if (index >= range.end)
|
|
156
|
+
left = mid + 1;
|
|
157
|
+
else
|
|
158
|
+
return range;
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
function readEscapedChar(input, cursor, escapable) {
|
|
163
|
+
if (input[cursor] !== "\\" || cursor + 1 >= input.length)
|
|
164
|
+
return undefined;
|
|
165
|
+
const nextChar = input[cursor + 1];
|
|
166
|
+
if (!escapable.has(nextChar))
|
|
167
|
+
return undefined;
|
|
168
|
+
return { value: nextChar, next: cursor + 2 };
|
|
169
|
+
}
|
|
170
|
+
function escapeForQuotedTitle(value, quote) {
|
|
171
|
+
const escapedSlash = value.replaceAll("\\", "\\\\");
|
|
172
|
+
return escapedSlash.replaceAll(quote, `\\${quote}`);
|
|
173
|
+
}
|
|
174
|
+
function parseMarkdownImageTokens(markdown) {
|
|
175
|
+
const tokens = [];
|
|
176
|
+
const excludedRanges = computeExcludedRanges(markdown);
|
|
177
|
+
let i = 0;
|
|
178
|
+
while (i < markdown.length) {
|
|
179
|
+
const blocked = findRangeAt(excludedRanges, i);
|
|
180
|
+
if (blocked) {
|
|
181
|
+
i = blocked.end;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (markdown[i] !== "!" || markdown[i + 1] !== "[") {
|
|
185
|
+
i += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const start = i;
|
|
189
|
+
let cursor = i + 2;
|
|
190
|
+
let alt = "";
|
|
191
|
+
let altClosed = false;
|
|
192
|
+
while (cursor < markdown.length) {
|
|
193
|
+
const c = markdown[cursor];
|
|
194
|
+
const escaped = readEscapedChar(markdown, cursor, new Set(["\\", "]", "[", "(", ")", "\"", "'"]));
|
|
195
|
+
if (escaped) {
|
|
196
|
+
alt += escaped.value;
|
|
197
|
+
cursor = escaped.next;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (c === "]") {
|
|
201
|
+
altClosed = true;
|
|
202
|
+
cursor += 1;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
alt += c;
|
|
206
|
+
cursor += 1;
|
|
207
|
+
}
|
|
208
|
+
if (!altClosed || markdown[cursor] !== "(") {
|
|
209
|
+
i += 1;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
cursor += 1;
|
|
213
|
+
while (cursor < markdown.length && isWhitespace(markdown[cursor]))
|
|
214
|
+
cursor += 1;
|
|
215
|
+
let src = "";
|
|
216
|
+
if (markdown[cursor] === "<") {
|
|
217
|
+
cursor += 1;
|
|
218
|
+
while (cursor < markdown.length) {
|
|
219
|
+
const c = markdown[cursor];
|
|
220
|
+
const escaped = readEscapedChar(markdown, cursor, new Set(["\\", ">", "<", "(", ")", "\"", "'", " "]));
|
|
221
|
+
if (escaped) {
|
|
222
|
+
src += escaped.value;
|
|
223
|
+
cursor = escaped.next;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (c === ">") {
|
|
227
|
+
cursor += 1;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
src += c;
|
|
231
|
+
cursor += 1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
let depth = 0;
|
|
236
|
+
while (cursor < markdown.length) {
|
|
237
|
+
const c = markdown[cursor];
|
|
238
|
+
const escaped = readEscapedChar(markdown, cursor, new Set(["\\", "(", ")", " "]));
|
|
239
|
+
if (escaped) {
|
|
240
|
+
src += escaped.value;
|
|
241
|
+
cursor = escaped.next;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (c === "(") {
|
|
245
|
+
depth += 1;
|
|
246
|
+
src += c;
|
|
247
|
+
cursor += 1;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (c === ")") {
|
|
251
|
+
if (depth === 0)
|
|
252
|
+
break;
|
|
253
|
+
depth -= 1;
|
|
254
|
+
src += c;
|
|
255
|
+
cursor += 1;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (isWhitespace(c) && depth === 0)
|
|
259
|
+
break;
|
|
260
|
+
src += c;
|
|
261
|
+
cursor += 1;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
while (cursor < markdown.length && isWhitespace(markdown[cursor]))
|
|
265
|
+
cursor += 1;
|
|
266
|
+
let title;
|
|
267
|
+
let titleQuote;
|
|
268
|
+
if (markdown[cursor] === "\"" || markdown[cursor] === "'") {
|
|
269
|
+
titleQuote = markdown[cursor];
|
|
270
|
+
cursor += 1;
|
|
271
|
+
let titleText = "";
|
|
272
|
+
while (cursor < markdown.length) {
|
|
273
|
+
const c = markdown[cursor];
|
|
274
|
+
const escaped = readEscapedChar(markdown, cursor, new Set(["\\", "\"", "'"]));
|
|
275
|
+
if (escaped) {
|
|
276
|
+
titleText += escaped.value;
|
|
277
|
+
cursor = escaped.next;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (c === titleQuote) {
|
|
281
|
+
cursor += 1;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
titleText += c;
|
|
285
|
+
cursor += 1;
|
|
286
|
+
}
|
|
287
|
+
title = titleText;
|
|
288
|
+
while (cursor < markdown.length && isWhitespace(markdown[cursor]))
|
|
289
|
+
cursor += 1;
|
|
290
|
+
}
|
|
291
|
+
if (markdown[cursor] !== ")" || !src) {
|
|
292
|
+
i += 1;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
tokens.push({ start, end: cursor + 1, alt, src, title, titleQuote });
|
|
296
|
+
i = cursor + 1;
|
|
297
|
+
}
|
|
298
|
+
return tokens;
|
|
299
|
+
}
|
|
300
|
+
function replaceMarkdownImageSources(markdown, srcMap) {
|
|
301
|
+
const tokens = parseMarkdownImageTokens(markdown);
|
|
302
|
+
if (tokens.length === 0)
|
|
303
|
+
return markdown;
|
|
304
|
+
let out = "";
|
|
305
|
+
let last = 0;
|
|
306
|
+
for (const token of tokens) {
|
|
307
|
+
out += markdown.slice(last, token.start);
|
|
308
|
+
const mapped = srcMap.get(token.src);
|
|
309
|
+
if (!mapped) {
|
|
310
|
+
out += markdown.slice(token.start, token.end);
|
|
311
|
+
last = token.end;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const quote = token.titleQuote ?? "\"";
|
|
315
|
+
const titlePart = token.title ? ` ${quote}${escapeForQuotedTitle(token.title, quote)}${quote}` : "";
|
|
316
|
+
out += ``;
|
|
317
|
+
last = token.end;
|
|
318
|
+
}
|
|
319
|
+
out += markdown.slice(last);
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
function removeMarkdownSlice(markdown, start, end) {
|
|
323
|
+
const removed = `${markdown.slice(0, start)}${markdown.slice(end)}`;
|
|
324
|
+
return removed.replace(/\n{3,}/g, "\n\n");
|
|
325
|
+
}
|
|
326
|
+
function removeLeadingAtxH1(markdown) {
|
|
327
|
+
const normalized = markdown.replace(/^\uFEFF/, "").replaceAll("\r\n", "\n");
|
|
328
|
+
const lines = normalized.split("\n");
|
|
329
|
+
let index = 0;
|
|
330
|
+
while (index < lines.length && !lines[index]?.trim()) {
|
|
331
|
+
index += 1;
|
|
332
|
+
}
|
|
333
|
+
if (lines[index]?.trim() === "---") {
|
|
334
|
+
index += 1;
|
|
335
|
+
while (index < lines.length && lines[index]?.trim() !== "---") {
|
|
336
|
+
index += 1;
|
|
337
|
+
}
|
|
338
|
+
if (index < lines.length) {
|
|
339
|
+
index += 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
while (index < lines.length && !lines[index]?.trim()) {
|
|
343
|
+
index += 1;
|
|
344
|
+
}
|
|
345
|
+
const line = lines[index];
|
|
346
|
+
if (!line || !/^\s{0,3}#\s+(.+?)\s*#*\s*$/u.test(line)) {
|
|
347
|
+
return markdown;
|
|
348
|
+
}
|
|
349
|
+
lines.splice(index, 1);
|
|
350
|
+
while (index < lines.length && !lines[index]?.trim()) {
|
|
351
|
+
lines.splice(index, 1);
|
|
352
|
+
}
|
|
353
|
+
return lines.join("\n");
|
|
354
|
+
}
|
|
355
|
+
function extractLeadingAtxH1(markdown) {
|
|
356
|
+
const normalized = markdown.replace(/^\uFEFF/, "").replaceAll("\r\n", "\n");
|
|
357
|
+
const lines = normalized.split("\n");
|
|
358
|
+
let index = 0;
|
|
359
|
+
while (index < lines.length && !lines[index]?.trim()) {
|
|
360
|
+
index += 1;
|
|
361
|
+
}
|
|
362
|
+
if (lines[index]?.trim() === "---") {
|
|
363
|
+
index += 1;
|
|
364
|
+
while (index < lines.length && lines[index]?.trim() !== "---") {
|
|
365
|
+
index += 1;
|
|
366
|
+
}
|
|
367
|
+
if (index < lines.length) {
|
|
368
|
+
index += 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
while (index < lines.length && !lines[index]?.trim()) {
|
|
372
|
+
index += 1;
|
|
373
|
+
}
|
|
374
|
+
const line = lines[index];
|
|
375
|
+
const match = line?.match(/^\s{0,3}#\s+(.+?)\s*#*\s*$/u);
|
|
376
|
+
return match?.[1]?.trim() || undefined;
|
|
377
|
+
}
|
|
378
|
+
function inferArticleTitle(rawArticleTitle, markdown, markdownPath) {
|
|
379
|
+
if (typeof rawArticleTitle === "string" && rawArticleTitle.trim()) {
|
|
380
|
+
return rawArticleTitle.trim();
|
|
381
|
+
}
|
|
382
|
+
const headingTitle = extractLeadingAtxH1(markdown);
|
|
383
|
+
if (headingTitle) {
|
|
384
|
+
return headingTitle;
|
|
385
|
+
}
|
|
386
|
+
if (typeof markdownPath === "string" && markdownPath.trim()) {
|
|
387
|
+
const base = basename(markdownPath.trim(), extname(markdownPath.trim())).trim();
|
|
388
|
+
if (base) {
|
|
389
|
+
return base;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
async function resolveMarkdownSource(args) {
|
|
395
|
+
const directMarkdown = args.markdown;
|
|
396
|
+
const markdownPath = args.markdown_path;
|
|
397
|
+
if (directMarkdown !== undefined) {
|
|
398
|
+
if (!validateMarkdown(directMarkdown)) {
|
|
399
|
+
return {
|
|
400
|
+
content: [{ type: "text", text: "markdown must be a non-empty string when provided." }],
|
|
401
|
+
isError: true
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return { markdown: directMarkdown, markdownPath: typeof markdownPath === "string" ? markdownPath : undefined };
|
|
405
|
+
}
|
|
406
|
+
if (validateMarkdownPath(markdownPath)) {
|
|
407
|
+
try {
|
|
408
|
+
const markdown = await readFile(markdownPath, "utf8");
|
|
409
|
+
if (!validateMarkdown(markdown)) {
|
|
410
|
+
return {
|
|
411
|
+
content: [{ type: "text", text: "markdown_path points to an empty markdown file." }],
|
|
412
|
+
isError: true
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
return { markdown, markdownPath };
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
return {
|
|
419
|
+
content: [{ type: "text", text: `Failed to read markdown_path: ${error.message}` }],
|
|
420
|
+
isError: true
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
content: [{ type: "text", text: "Provide one markdown source: markdown or markdown_path." }],
|
|
426
|
+
isError: true
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function isToolResult(value) {
|
|
430
|
+
return "content" in value;
|
|
431
|
+
}
|
|
432
|
+
const PERMANENT_MATERIAL_TYPES = ["image", "voice", "video", "thumb"];
|
|
433
|
+
const MATERIAL_EXTENSION_RULES = {
|
|
434
|
+
image: ["bmp", "png", "jpeg", "jpg", "gif"],
|
|
435
|
+
voice: ["mp3", "wma", "wav", "amr"],
|
|
436
|
+
video: ["mp4"],
|
|
437
|
+
thumb: ["jpg", "jpeg"]
|
|
438
|
+
};
|
|
33
439
|
export async function handleToolCall(name, args) {
|
|
34
440
|
if (name === "list_wechat_themes") {
|
|
35
441
|
return {
|
|
@@ -72,24 +478,29 @@ export async function handleToolCall(name, args) {
|
|
|
72
478
|
isError: true
|
|
73
479
|
};
|
|
74
480
|
}
|
|
481
|
+
markdown = removeLeadingAtxH1(markdown);
|
|
75
482
|
const theme = args.theme ?? "default";
|
|
76
483
|
if (typeof theme !== "string" || !(theme in THEMES)) {
|
|
77
484
|
return invalidThemeResult(theme);
|
|
78
485
|
}
|
|
486
|
+
const fontSizePresetArg = args.font_size_preset ?? "medium";
|
|
487
|
+
if (typeof fontSizePresetArg !== "string" || !FONT_SIZE_PRESETS.includes(fontSizePresetArg)) {
|
|
488
|
+
return invalidFontSizePresetResult(fontSizePresetArg);
|
|
489
|
+
}
|
|
490
|
+
const fontSizePreset = fontSizePresetArg;
|
|
79
491
|
const title = typeof args.title === "string" ? args.title : undefined;
|
|
80
|
-
// Upload local images if access_token is provided
|
|
81
492
|
const accessToken = typeof args.access_token === "string" ? args.access_token.trim() : undefined;
|
|
493
|
+
// Upload local images if access_token is provided
|
|
82
494
|
if (accessToken) {
|
|
83
|
-
const baseDir = typeof markdownPath === "string" ? markdownPath
|
|
84
|
-
const localImagePattern = /!\[([^\]]*)\]\((?!https?:\/\/)([^)]+)\)/g;
|
|
495
|
+
const baseDir = typeof markdownPath === "string" ? dirname(markdownPath) : process.cwd();
|
|
85
496
|
const uploadErrors = [];
|
|
86
497
|
const uploadCache = new Map();
|
|
87
|
-
const
|
|
88
|
-
for (const
|
|
89
|
-
const rawPath =
|
|
90
|
-
if (!rawPath || uploadCache.has(rawPath))
|
|
498
|
+
const imageTokens = parseMarkdownImageTokens(markdown);
|
|
499
|
+
for (const token of imageTokens) {
|
|
500
|
+
const rawPath = token.src;
|
|
501
|
+
if (!rawPath || uploadCache.has(rawPath) || isRemoteUrl(rawPath))
|
|
91
502
|
continue;
|
|
92
|
-
const absPath = rawPath.startsWith("/") ? rawPath :
|
|
503
|
+
const absPath = rawPath.startsWith("/") ? rawPath : resolve(baseDir, rawPath);
|
|
93
504
|
try {
|
|
94
505
|
const result = await uploadImage(accessToken, absPath);
|
|
95
506
|
uploadCache.set(rawPath, result.url);
|
|
@@ -98,9 +509,7 @@ export async function handleToolCall(name, args) {
|
|
|
98
509
|
uploadErrors.push(`${rawPath}: ${error.message}`);
|
|
99
510
|
}
|
|
100
511
|
}
|
|
101
|
-
|
|
102
|
-
markdown = markdown.replaceAll(`](${localPath})`, `](${cdnUrl})`);
|
|
103
|
-
}
|
|
512
|
+
markdown = replaceMarkdownImageSources(markdown, uploadCache);
|
|
104
513
|
if (uploadErrors.length > 0) {
|
|
105
514
|
return {
|
|
106
515
|
content: [{ type: "text", text: `Image upload failed:\n${uploadErrors.join("\n")}` }],
|
|
@@ -108,10 +517,14 @@ export async function handleToolCall(name, args) {
|
|
|
108
517
|
};
|
|
109
518
|
}
|
|
110
519
|
}
|
|
111
|
-
const html = parseMarkdown(markdown, theme, title);
|
|
520
|
+
const html = parseMarkdown(markdown, theme, title, fontSizePreset);
|
|
112
521
|
const savedPath = saveHtmlCache(html);
|
|
522
|
+
const visibleMetaLines = [`cacheHtmlPath=${savedPath}`];
|
|
113
523
|
return {
|
|
114
|
-
content: [
|
|
524
|
+
content: [
|
|
525
|
+
{ type: "text", text: html },
|
|
526
|
+
{ type: "text", text: visibleMetaLines.join("\n") }
|
|
527
|
+
],
|
|
115
528
|
meta: { cacheHtmlPath: savedPath }
|
|
116
529
|
};
|
|
117
530
|
}
|
|
@@ -178,6 +591,118 @@ export async function handleToolCall(name, args) {
|
|
|
178
591
|
return { content: [{ type: "text", text: error.message }], isError: true };
|
|
179
592
|
}
|
|
180
593
|
}
|
|
594
|
+
if (name === "wechat_markdown_to_draft") {
|
|
595
|
+
const accessToken = args.access_token;
|
|
596
|
+
const rawArticleTitle = args.article_title;
|
|
597
|
+
const author = typeof args.author === "string" ? args.author : undefined;
|
|
598
|
+
const digest = typeof args.digest === "string" ? args.digest : undefined;
|
|
599
|
+
const contentSourceUrl = typeof args.content_source_url === "string" ? args.content_source_url : undefined;
|
|
600
|
+
const thumbMediaIdInput = typeof args.thumb_media_id === "string" ? args.thumb_media_id : undefined;
|
|
601
|
+
const needOpenComment = args.need_open_comment;
|
|
602
|
+
const onlyFansCanComment = args.only_fans_can_comment;
|
|
603
|
+
if (typeof accessToken !== "string" || !accessToken.trim()) {
|
|
604
|
+
return { content: [{ type: "text", text: "access_token is required." }], isError: true };
|
|
605
|
+
}
|
|
606
|
+
if (needOpenComment !== undefined && needOpenComment !== 0 && needOpenComment !== 1) {
|
|
607
|
+
return { content: [{ type: "text", text: "need_open_comment must be 0 or 1." }], isError: true };
|
|
608
|
+
}
|
|
609
|
+
if (onlyFansCanComment !== undefined && onlyFansCanComment !== 0 && onlyFansCanComment !== 1) {
|
|
610
|
+
return { content: [{ type: "text", text: "only_fans_can_comment must be 0 or 1." }], isError: true };
|
|
611
|
+
}
|
|
612
|
+
const source = await resolveMarkdownSource(args);
|
|
613
|
+
if (isToolResult(source)) {
|
|
614
|
+
return source;
|
|
615
|
+
}
|
|
616
|
+
const articleTitle = inferArticleTitle(rawArticleTitle, source.markdown, source.markdownPath);
|
|
617
|
+
if (!articleTitle) {
|
|
618
|
+
return {
|
|
619
|
+
content: [{ type: "text", text: "article_title is required when markdown has no leading H1 and markdown_path is unavailable." }],
|
|
620
|
+
isError: true
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
let markdownForConvert = source.markdown;
|
|
624
|
+
const markdownPath = source.markdownPath;
|
|
625
|
+
const baseDir = markdownPath ? dirname(markdownPath) : process.cwd();
|
|
626
|
+
const imageTokens = parseMarkdownImageTokens(markdownForConvert);
|
|
627
|
+
let designatedCoverMediaId;
|
|
628
|
+
let firstImageCoverMediaId;
|
|
629
|
+
if (!thumbMediaIdInput) {
|
|
630
|
+
const designatedCoverToken = imageTokens.find((token) => token.title?.trim() === "封面");
|
|
631
|
+
if (designatedCoverToken && !isRemoteUrl(designatedCoverToken.src)) {
|
|
632
|
+
const coverExt = getFileExtension(designatedCoverToken.src);
|
|
633
|
+
if (coverExt === "jpg" || coverExt === "jpeg") {
|
|
634
|
+
const coverPath = designatedCoverToken.src.startsWith("/") ? designatedCoverToken.src : resolve(baseDir, designatedCoverToken.src);
|
|
635
|
+
try {
|
|
636
|
+
const coverResult = await addMaterial(accessToken, "thumb", coverPath);
|
|
637
|
+
designatedCoverMediaId = coverResult.media_id;
|
|
638
|
+
markdownForConvert = removeMarkdownSlice(markdownForConvert, designatedCoverToken.start, designatedCoverToken.end);
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
return { content: [{ type: "text", text: `Cover upload failed: ${error.message}` }], isError: true };
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (!designatedCoverMediaId) {
|
|
646
|
+
const firstLocalImage = imageTokens.find((token) => !isRemoteUrl(token.src));
|
|
647
|
+
if (firstLocalImage) {
|
|
648
|
+
const ext = getFileExtension(firstLocalImage.src);
|
|
649
|
+
if (ext === "jpg" || ext === "jpeg") {
|
|
650
|
+
const firstPath = firstLocalImage.src.startsWith("/") ? firstLocalImage.src : resolve(baseDir, firstLocalImage.src);
|
|
651
|
+
try {
|
|
652
|
+
const firstResult = await addMaterial(accessToken, "thumb", firstPath);
|
|
653
|
+
firstImageCoverMediaId = firstResult.media_id;
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
firstImageCoverMediaId = undefined;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
markdownForConvert = removeLeadingAtxH1(markdownForConvert);
|
|
663
|
+
const convertResult = await handleToolCall("convert_markdown_to_wechat_html", {
|
|
664
|
+
markdown: markdownForConvert,
|
|
665
|
+
markdown_path: markdownPath,
|
|
666
|
+
theme: args.theme,
|
|
667
|
+
font_size_preset: args.font_size_preset,
|
|
668
|
+
access_token: accessToken
|
|
669
|
+
});
|
|
670
|
+
if (convertResult.isError) {
|
|
671
|
+
return convertResult;
|
|
672
|
+
}
|
|
673
|
+
const html = convertResult.content[0]?.text;
|
|
674
|
+
if (!validateMarkdown(html)) {
|
|
675
|
+
return {
|
|
676
|
+
content: [{ type: "text", text: "Failed to generate HTML content from markdown." }],
|
|
677
|
+
isError: true
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
const finalThumbMediaId = thumbMediaIdInput || designatedCoverMediaId || firstImageCoverMediaId;
|
|
681
|
+
const article = {
|
|
682
|
+
title: articleTitle,
|
|
683
|
+
content: html,
|
|
684
|
+
...(author ? { author } : {}),
|
|
685
|
+
...(digest ? { digest } : {}),
|
|
686
|
+
...(contentSourceUrl ? { content_source_url: contentSourceUrl } : {}),
|
|
687
|
+
...(finalThumbMediaId ? { thumb_media_id: finalThumbMediaId } : {}),
|
|
688
|
+
...(needOpenComment === 0 || needOpenComment === 1 ? { need_open_comment: needOpenComment } : {}),
|
|
689
|
+
...(onlyFansCanComment === 0 || onlyFansCanComment === 1 ? { only_fans_can_comment: onlyFansCanComment } : {})
|
|
690
|
+
};
|
|
691
|
+
try {
|
|
692
|
+
const draftResult = await draftAdd(accessToken, [article]);
|
|
693
|
+
return {
|
|
694
|
+
content: [{ type: "text", text: JSON.stringify(draftResult, null, 2) }],
|
|
695
|
+
meta: {
|
|
696
|
+
mediaId: draftResult.media_id,
|
|
697
|
+
cacheHtmlPath: convertResult.meta?.cacheHtmlPath,
|
|
698
|
+
...(finalThumbMediaId ? { thumbMediaId: finalThumbMediaId } : {})
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
return { content: [{ type: "text", text: error.message }], isError: true };
|
|
704
|
+
}
|
|
705
|
+
}
|
|
181
706
|
if (name === "wechat_draft_update") {
|
|
182
707
|
const accessToken = args.access_token;
|
|
183
708
|
const mediaId = args.media_id;
|
|
@@ -212,7 +737,7 @@ export async function handleToolCall(name, args) {
|
|
|
212
737
|
if (typeof filePath !== "string" || !filePath.trim()) {
|
|
213
738
|
return { content: [{ type: "text", text: "file_path is required." }], isError: true };
|
|
214
739
|
}
|
|
215
|
-
const ext = filePath
|
|
740
|
+
const ext = getFileExtension(filePath);
|
|
216
741
|
if (ext !== "jpg" && ext !== "jpeg" && ext !== "png") {
|
|
217
742
|
return { content: [{ type: "text", text: "Only JPG and PNG files are supported." }], isError: true };
|
|
218
743
|
}
|
|
@@ -224,6 +749,43 @@ export async function handleToolCall(name, args) {
|
|
|
224
749
|
return { content: [{ type: "text", text: error.message }], isError: true };
|
|
225
750
|
}
|
|
226
751
|
}
|
|
752
|
+
if (name === "wechat_add_material") {
|
|
753
|
+
const accessToken = args.access_token;
|
|
754
|
+
const type = args.type;
|
|
755
|
+
const filePath = args.file_path;
|
|
756
|
+
const title = typeof args.title === "string" ? args.title.trim() : undefined;
|
|
757
|
+
const introduction = typeof args.introduction === "string" ? args.introduction.trim() : undefined;
|
|
758
|
+
if (typeof accessToken !== "string" || !accessToken.trim()) {
|
|
759
|
+
return { content: [{ type: "text", text: "access_token is required." }], isError: true };
|
|
760
|
+
}
|
|
761
|
+
if (typeof type !== "string" || !PERMANENT_MATERIAL_TYPES.includes(type)) {
|
|
762
|
+
return { content: [{ type: "text", text: "type must be one of: image, voice, video, thumb." }], isError: true };
|
|
763
|
+
}
|
|
764
|
+
if (typeof filePath !== "string" || !filePath.trim()) {
|
|
765
|
+
return { content: [{ type: "text", text: "file_path is required." }], isError: true };
|
|
766
|
+
}
|
|
767
|
+
const ext = getFileExtension(filePath);
|
|
768
|
+
const allowedExt = MATERIAL_EXTENSION_RULES[type];
|
|
769
|
+
if (!ext || !allowedExt.includes(ext)) {
|
|
770
|
+
return {
|
|
771
|
+
content: [{ type: "text", text: `Invalid file extension for type=${type}. Allowed: ${allowedExt.join(", ")}.` }],
|
|
772
|
+
isError: true
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
if (type === "video" && (!title || !introduction)) {
|
|
776
|
+
return {
|
|
777
|
+
content: [{ type: "text", text: "For video material, both title and introduction are required." }],
|
|
778
|
+
isError: true
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
try {
|
|
782
|
+
const result = await addMaterial(accessToken, type, filePath, title || introduction ? { title, introduction } : undefined);
|
|
783
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
return { content: [{ type: "text", text: error.message }], isError: true };
|
|
787
|
+
}
|
|
788
|
+
}
|
|
227
789
|
if (name === "wechat_draft_batchget") {
|
|
228
790
|
const accessToken = args.access_token;
|
|
229
791
|
const offset = args.offset ?? 0;
|
package/dist/wechat-api.d.ts
CHANGED
|
@@ -48,6 +48,16 @@ export declare function draftBatchGet(accessToken: string, offset: number, count
|
|
|
48
48
|
export declare function uploadImage(accessToken: string, filePath: string): Promise<{
|
|
49
49
|
url: string;
|
|
50
50
|
}>;
|
|
51
|
+
export type PermanentMaterialType = "image" | "voice" | "video" | "thumb";
|
|
52
|
+
export interface MaterialDescription {
|
|
53
|
+
title?: string;
|
|
54
|
+
introduction?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface AddMaterialResult {
|
|
57
|
+
media_id: string;
|
|
58
|
+
url?: string;
|
|
59
|
+
}
|
|
60
|
+
export declare function addMaterial(accessToken: string, type: PermanentMaterialType, filePath: string, description?: MaterialDescription): Promise<AddMaterialResult>;
|
|
51
61
|
export declare function draftDelete(accessToken: string, mediaId: string): Promise<{
|
|
52
62
|
errcode: number;
|
|
53
63
|
errmsg: string;
|