md2wechat-mcp 0.2.0 → 0.2.2
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 +76 -11
- package/dist/themes.d.ts +2 -0
- package/dist/themes.js +61 -35
- package/dist/tools.js +645 -16
- 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,458 @@ 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
|
+
async function uploadLocalImageSourcesInHtml(html, accessToken, baseDir) {
|
|
323
|
+
const uploadErrors = [];
|
|
324
|
+
const uploadCache = new Map();
|
|
325
|
+
let uploadedCount = 0;
|
|
326
|
+
const imgTags = [...html.matchAll(/<img\b[^>]*\bsrc\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+)[^>]*>/giu)];
|
|
327
|
+
if (imgTags.length === 0) {
|
|
328
|
+
return { html, uploadedCount };
|
|
329
|
+
}
|
|
330
|
+
const replacements = new Map();
|
|
331
|
+
for (const match of imgTags) {
|
|
332
|
+
const imgTag = match[0];
|
|
333
|
+
if (replacements.has(imgTag)) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const srcMatch = imgTag.match(/src\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/iu);
|
|
337
|
+
if (!srcMatch) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const rawSrc = srcMatch?.[1] ?? srcMatch?.[2] ?? srcMatch?.[3];
|
|
341
|
+
if (!rawSrc) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const normalizedSrc = rawSrc.trim();
|
|
345
|
+
if (!normalizedSrc || isRemoteUrl(normalizedSrc) || /^data:/iu.test(normalizedSrc)) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const cached = uploadCache.get(normalizedSrc);
|
|
349
|
+
if (cached) {
|
|
350
|
+
replacements.set(imgTag, imgTag.replace(srcMatch[0], `src="${cached}"`));
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const absPath = normalizedSrc.startsWith("/") ? normalizedSrc : resolve(baseDir, normalizedSrc);
|
|
354
|
+
try {
|
|
355
|
+
const uploaded = await uploadImage(accessToken, absPath);
|
|
356
|
+
uploadCache.set(normalizedSrc, uploaded.url);
|
|
357
|
+
uploadedCount += 1;
|
|
358
|
+
replacements.set(imgTag, imgTag.replace(srcMatch[0], `src="${uploaded.url}"`));
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
uploadErrors.push(`${normalizedSrc}: ${error.message}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (uploadErrors.length > 0) {
|
|
365
|
+
throw new Error(`Image upload failed:\n${uploadErrors.join("\n")}`);
|
|
366
|
+
}
|
|
367
|
+
let rewrittenHtml = html;
|
|
368
|
+
for (const [before, after] of replacements) {
|
|
369
|
+
rewrittenHtml = rewrittenHtml.split(before).join(after);
|
|
370
|
+
}
|
|
371
|
+
return { html: rewrittenHtml, uploadedCount };
|
|
372
|
+
}
|
|
373
|
+
function removeLeadingHtmlH1(html) {
|
|
374
|
+
return html.replace(/^\s*<h1\b[^>]*>[\s\S]*?<\/h1>\s*/iu, "");
|
|
375
|
+
}
|
|
376
|
+
function removeMarkdownSlice(markdown, start, end) {
|
|
377
|
+
const removed = `${markdown.slice(0, start)}${markdown.slice(end)}`;
|
|
378
|
+
return removed.replace(/\n{3,}/g, "\n\n");
|
|
379
|
+
}
|
|
380
|
+
function removeLeadingAtxH1(markdown) {
|
|
381
|
+
const normalized = markdown.replace(/^\uFEFF/, "").replaceAll("\r\n", "\n");
|
|
382
|
+
const lines = normalized.split("\n");
|
|
383
|
+
let index = 0;
|
|
384
|
+
while (index < lines.length && !lines[index]?.trim()) {
|
|
385
|
+
index += 1;
|
|
386
|
+
}
|
|
387
|
+
if (lines[index]?.trim() === "---") {
|
|
388
|
+
index += 1;
|
|
389
|
+
while (index < lines.length && lines[index]?.trim() !== "---") {
|
|
390
|
+
index += 1;
|
|
391
|
+
}
|
|
392
|
+
if (index < lines.length) {
|
|
393
|
+
index += 1;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
while (index < lines.length && !lines[index]?.trim()) {
|
|
397
|
+
index += 1;
|
|
398
|
+
}
|
|
399
|
+
const line = lines[index];
|
|
400
|
+
if (!line || !/^\s{0,3}#\s+(.+?)\s*#*\s*$/u.test(line)) {
|
|
401
|
+
return markdown;
|
|
402
|
+
}
|
|
403
|
+
lines.splice(index, 1);
|
|
404
|
+
while (index < lines.length && !lines[index]?.trim()) {
|
|
405
|
+
lines.splice(index, 1);
|
|
406
|
+
}
|
|
407
|
+
return lines.join("\n");
|
|
408
|
+
}
|
|
409
|
+
function extractLeadingAtxH1(markdown) {
|
|
410
|
+
const normalized = markdown.replace(/^\uFEFF/, "").replaceAll("\r\n", "\n");
|
|
411
|
+
const lines = normalized.split("\n");
|
|
412
|
+
let index = 0;
|
|
413
|
+
while (index < lines.length && !lines[index]?.trim()) {
|
|
414
|
+
index += 1;
|
|
415
|
+
}
|
|
416
|
+
if (lines[index]?.trim() === "---") {
|
|
417
|
+
index += 1;
|
|
418
|
+
while (index < lines.length && lines[index]?.trim() !== "---") {
|
|
419
|
+
index += 1;
|
|
420
|
+
}
|
|
421
|
+
if (index < lines.length) {
|
|
422
|
+
index += 1;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
while (index < lines.length && !lines[index]?.trim()) {
|
|
426
|
+
index += 1;
|
|
427
|
+
}
|
|
428
|
+
const line = lines[index];
|
|
429
|
+
const match = line?.match(/^\s{0,3}#\s+(.+?)\s*#*\s*$/u);
|
|
430
|
+
return match?.[1]?.trim() || undefined;
|
|
431
|
+
}
|
|
432
|
+
function inferArticleTitle(rawArticleTitle, markdown, markdownPath) {
|
|
433
|
+
if (typeof rawArticleTitle === "string" && rawArticleTitle.trim()) {
|
|
434
|
+
return rawArticleTitle.trim();
|
|
435
|
+
}
|
|
436
|
+
const headingTitle = extractLeadingAtxH1(markdown);
|
|
437
|
+
if (headingTitle) {
|
|
438
|
+
return headingTitle;
|
|
439
|
+
}
|
|
440
|
+
if (typeof markdownPath === "string" && markdownPath.trim()) {
|
|
441
|
+
const base = basename(markdownPath.trim(), extname(markdownPath.trim())).trim();
|
|
442
|
+
if (base) {
|
|
443
|
+
return base;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
async function resolveMarkdownSource(args) {
|
|
449
|
+
const directMarkdown = args.markdown;
|
|
450
|
+
const markdownPath = args.markdown_path;
|
|
451
|
+
if (directMarkdown !== undefined) {
|
|
452
|
+
if (!validateMarkdown(directMarkdown)) {
|
|
453
|
+
return {
|
|
454
|
+
content: [{ type: "text", text: "markdown must be a non-empty string when provided." }],
|
|
455
|
+
isError: true
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
return { markdown: directMarkdown, markdownPath: typeof markdownPath === "string" ? markdownPath : undefined };
|
|
459
|
+
}
|
|
460
|
+
if (validateMarkdownPath(markdownPath)) {
|
|
461
|
+
try {
|
|
462
|
+
const markdown = await readFile(markdownPath, "utf8");
|
|
463
|
+
if (!validateMarkdown(markdown)) {
|
|
464
|
+
return {
|
|
465
|
+
content: [{ type: "text", text: "markdown_path points to an empty markdown file." }],
|
|
466
|
+
isError: true
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
return { markdown, markdownPath };
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
return {
|
|
473
|
+
content: [{ type: "text", text: `Failed to read markdown_path: ${error.message}` }],
|
|
474
|
+
isError: true
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
content: [{ type: "text", text: "Provide one markdown source: markdown or markdown_path." }],
|
|
480
|
+
isError: true
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function isToolResult(value) {
|
|
484
|
+
return "content" in value;
|
|
485
|
+
}
|
|
486
|
+
const PERMANENT_MATERIAL_TYPES = ["image", "voice", "video", "thumb"];
|
|
487
|
+
const MATERIAL_EXTENSION_RULES = {
|
|
488
|
+
image: ["bmp", "png", "jpeg", "jpg", "gif"],
|
|
489
|
+
voice: ["mp3", "wma", "wav", "amr"],
|
|
490
|
+
video: ["mp4"],
|
|
491
|
+
thumb: ["jpg", "jpeg"]
|
|
492
|
+
};
|
|
33
493
|
export async function handleToolCall(name, args) {
|
|
34
494
|
if (name === "list_wechat_themes") {
|
|
35
495
|
return {
|
|
@@ -72,24 +532,29 @@ export async function handleToolCall(name, args) {
|
|
|
72
532
|
isError: true
|
|
73
533
|
};
|
|
74
534
|
}
|
|
535
|
+
markdown = removeLeadingAtxH1(markdown);
|
|
75
536
|
const theme = args.theme ?? "default";
|
|
76
537
|
if (typeof theme !== "string" || !(theme in THEMES)) {
|
|
77
538
|
return invalidThemeResult(theme);
|
|
78
539
|
}
|
|
540
|
+
const fontSizePresetArg = args.font_size_preset ?? "medium";
|
|
541
|
+
if (typeof fontSizePresetArg !== "string" || !FONT_SIZE_PRESETS.includes(fontSizePresetArg)) {
|
|
542
|
+
return invalidFontSizePresetResult(fontSizePresetArg);
|
|
543
|
+
}
|
|
544
|
+
const fontSizePreset = fontSizePresetArg;
|
|
79
545
|
const title = typeof args.title === "string" ? args.title : undefined;
|
|
80
|
-
// Upload local images if access_token is provided
|
|
81
546
|
const accessToken = typeof args.access_token === "string" ? args.access_token.trim() : undefined;
|
|
547
|
+
// Upload local images if access_token is provided
|
|
82
548
|
if (accessToken) {
|
|
83
|
-
const baseDir = typeof markdownPath === "string" ? markdownPath
|
|
84
|
-
const localImagePattern = /!\[([^\]]*)\]\((?!https?:\/\/)([^)]+)\)/g;
|
|
549
|
+
const baseDir = typeof markdownPath === "string" ? dirname(markdownPath) : process.cwd();
|
|
85
550
|
const uploadErrors = [];
|
|
86
551
|
const uploadCache = new Map();
|
|
87
|
-
const
|
|
88
|
-
for (const
|
|
89
|
-
const rawPath =
|
|
90
|
-
if (!rawPath || uploadCache.has(rawPath))
|
|
552
|
+
const imageTokens = parseMarkdownImageTokens(markdown);
|
|
553
|
+
for (const token of imageTokens) {
|
|
554
|
+
const rawPath = token.src;
|
|
555
|
+
if (!rawPath || uploadCache.has(rawPath) || isRemoteUrl(rawPath))
|
|
91
556
|
continue;
|
|
92
|
-
const absPath = rawPath.startsWith("/") ? rawPath :
|
|
557
|
+
const absPath = rawPath.startsWith("/") ? rawPath : resolve(baseDir, rawPath);
|
|
93
558
|
try {
|
|
94
559
|
const result = await uploadImage(accessToken, absPath);
|
|
95
560
|
uploadCache.set(rawPath, result.url);
|
|
@@ -98,9 +563,7 @@ export async function handleToolCall(name, args) {
|
|
|
98
563
|
uploadErrors.push(`${rawPath}: ${error.message}`);
|
|
99
564
|
}
|
|
100
565
|
}
|
|
101
|
-
|
|
102
|
-
markdown = markdown.replaceAll(`](${localPath})`, `](${cdnUrl})`);
|
|
103
|
-
}
|
|
566
|
+
markdown = replaceMarkdownImageSources(markdown, uploadCache);
|
|
104
567
|
if (uploadErrors.length > 0) {
|
|
105
568
|
return {
|
|
106
569
|
content: [{ type: "text", text: `Image upload failed:\n${uploadErrors.join("\n")}` }],
|
|
@@ -108,10 +571,14 @@ export async function handleToolCall(name, args) {
|
|
|
108
571
|
};
|
|
109
572
|
}
|
|
110
573
|
}
|
|
111
|
-
const html = parseMarkdown(markdown, theme, title);
|
|
574
|
+
const html = parseMarkdown(markdown, theme, title, fontSizePreset);
|
|
112
575
|
const savedPath = saveHtmlCache(html);
|
|
576
|
+
const visibleMetaLines = [`cacheHtmlPath=${savedPath}`];
|
|
113
577
|
return {
|
|
114
|
-
content: [
|
|
578
|
+
content: [
|
|
579
|
+
{ type: "text", text: html },
|
|
580
|
+
{ type: "text", text: visibleMetaLines.join("\n") }
|
|
581
|
+
],
|
|
115
582
|
meta: { cacheHtmlPath: savedPath }
|
|
116
583
|
};
|
|
117
584
|
}
|
|
@@ -178,11 +645,124 @@ export async function handleToolCall(name, args) {
|
|
|
178
645
|
return { content: [{ type: "text", text: error.message }], isError: true };
|
|
179
646
|
}
|
|
180
647
|
}
|
|
648
|
+
if (name === "wechat_markdown_to_draft") {
|
|
649
|
+
const accessToken = args.access_token;
|
|
650
|
+
const rawArticleTitle = args.article_title;
|
|
651
|
+
const author = typeof args.author === "string" ? args.author : undefined;
|
|
652
|
+
const digest = typeof args.digest === "string" ? args.digest : undefined;
|
|
653
|
+
const contentSourceUrl = typeof args.content_source_url === "string" ? args.content_source_url : undefined;
|
|
654
|
+
const thumbMediaIdInput = typeof args.thumb_media_id === "string" ? args.thumb_media_id : undefined;
|
|
655
|
+
const needOpenComment = args.need_open_comment;
|
|
656
|
+
const onlyFansCanComment = args.only_fans_can_comment;
|
|
657
|
+
if (typeof accessToken !== "string" || !accessToken.trim()) {
|
|
658
|
+
return { content: [{ type: "text", text: "access_token is required." }], isError: true };
|
|
659
|
+
}
|
|
660
|
+
if (needOpenComment !== undefined && needOpenComment !== 0 && needOpenComment !== 1) {
|
|
661
|
+
return { content: [{ type: "text", text: "need_open_comment must be 0 or 1." }], isError: true };
|
|
662
|
+
}
|
|
663
|
+
if (onlyFansCanComment !== undefined && onlyFansCanComment !== 0 && onlyFansCanComment !== 1) {
|
|
664
|
+
return { content: [{ type: "text", text: "only_fans_can_comment must be 0 or 1." }], isError: true };
|
|
665
|
+
}
|
|
666
|
+
const source = await resolveMarkdownSource(args);
|
|
667
|
+
if (isToolResult(source)) {
|
|
668
|
+
return source;
|
|
669
|
+
}
|
|
670
|
+
const articleTitle = inferArticleTitle(rawArticleTitle, source.markdown, source.markdownPath);
|
|
671
|
+
if (!articleTitle) {
|
|
672
|
+
return {
|
|
673
|
+
content: [{ type: "text", text: "article_title is required when markdown has no leading H1 and markdown_path is unavailable." }],
|
|
674
|
+
isError: true
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
let markdownForConvert = source.markdown;
|
|
678
|
+
const markdownPath = source.markdownPath;
|
|
679
|
+
const baseDir = markdownPath ? dirname(markdownPath) : process.cwd();
|
|
680
|
+
const imageTokens = parseMarkdownImageTokens(markdownForConvert);
|
|
681
|
+
let designatedCoverMediaId;
|
|
682
|
+
let firstImageCoverMediaId;
|
|
683
|
+
if (!thumbMediaIdInput) {
|
|
684
|
+
const designatedCoverToken = imageTokens.find((token) => token.title?.trim() === "封面");
|
|
685
|
+
if (designatedCoverToken && !isRemoteUrl(designatedCoverToken.src)) {
|
|
686
|
+
const coverExt = getFileExtension(designatedCoverToken.src);
|
|
687
|
+
if (coverExt === "jpg" || coverExt === "jpeg") {
|
|
688
|
+
const coverPath = designatedCoverToken.src.startsWith("/") ? designatedCoverToken.src : resolve(baseDir, designatedCoverToken.src);
|
|
689
|
+
try {
|
|
690
|
+
const coverResult = await addMaterial(accessToken, "thumb", coverPath);
|
|
691
|
+
designatedCoverMediaId = coverResult.media_id;
|
|
692
|
+
markdownForConvert = removeMarkdownSlice(markdownForConvert, designatedCoverToken.start, designatedCoverToken.end);
|
|
693
|
+
}
|
|
694
|
+
catch (error) {
|
|
695
|
+
return { content: [{ type: "text", text: `Cover upload failed: ${error.message}` }], isError: true };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (!designatedCoverMediaId) {
|
|
700
|
+
const firstLocalImage = imageTokens.find((token) => !isRemoteUrl(token.src));
|
|
701
|
+
if (firstLocalImage) {
|
|
702
|
+
const ext = getFileExtension(firstLocalImage.src);
|
|
703
|
+
if (ext === "jpg" || ext === "jpeg") {
|
|
704
|
+
const firstPath = firstLocalImage.src.startsWith("/") ? firstLocalImage.src : resolve(baseDir, firstLocalImage.src);
|
|
705
|
+
try {
|
|
706
|
+
const firstResult = await addMaterial(accessToken, "thumb", firstPath);
|
|
707
|
+
firstImageCoverMediaId = firstResult.media_id;
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
firstImageCoverMediaId = undefined;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
markdownForConvert = removeLeadingAtxH1(markdownForConvert);
|
|
717
|
+
const convertResult = await handleToolCall("convert_markdown_to_wechat_html", {
|
|
718
|
+
markdown: markdownForConvert,
|
|
719
|
+
markdown_path: markdownPath,
|
|
720
|
+
theme: args.theme,
|
|
721
|
+
font_size_preset: args.font_size_preset,
|
|
722
|
+
access_token: accessToken
|
|
723
|
+
});
|
|
724
|
+
if (convertResult.isError) {
|
|
725
|
+
return convertResult;
|
|
726
|
+
}
|
|
727
|
+
const html = convertResult.content[0]?.text;
|
|
728
|
+
if (!validateMarkdown(html)) {
|
|
729
|
+
return {
|
|
730
|
+
content: [{ type: "text", text: "Failed to generate HTML content from markdown." }],
|
|
731
|
+
isError: true
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
const finalThumbMediaId = thumbMediaIdInput || designatedCoverMediaId || firstImageCoverMediaId;
|
|
735
|
+
const article = {
|
|
736
|
+
title: articleTitle,
|
|
737
|
+
content: html,
|
|
738
|
+
...(author ? { author } : {}),
|
|
739
|
+
...(digest ? { digest } : {}),
|
|
740
|
+
...(contentSourceUrl ? { content_source_url: contentSourceUrl } : {}),
|
|
741
|
+
...(finalThumbMediaId ? { thumb_media_id: finalThumbMediaId } : {}),
|
|
742
|
+
...(needOpenComment === 0 || needOpenComment === 1 ? { need_open_comment: needOpenComment } : {}),
|
|
743
|
+
...(onlyFansCanComment === 0 || onlyFansCanComment === 1 ? { only_fans_can_comment: onlyFansCanComment } : {})
|
|
744
|
+
};
|
|
745
|
+
try {
|
|
746
|
+
const draftResult = await draftAdd(accessToken, [article]);
|
|
747
|
+
return {
|
|
748
|
+
content: [{ type: "text", text: JSON.stringify(draftResult, null, 2) }],
|
|
749
|
+
meta: {
|
|
750
|
+
mediaId: draftResult.media_id,
|
|
751
|
+
cacheHtmlPath: convertResult.meta?.cacheHtmlPath,
|
|
752
|
+
...(finalThumbMediaId ? { thumbMediaId: finalThumbMediaId } : {})
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
return { content: [{ type: "text", text: error.message }], isError: true };
|
|
758
|
+
}
|
|
759
|
+
}
|
|
181
760
|
if (name === "wechat_draft_update") {
|
|
182
761
|
const accessToken = args.access_token;
|
|
183
762
|
const mediaId = args.media_id;
|
|
184
763
|
const index = args.index ?? 0;
|
|
185
764
|
const article = args.article;
|
|
765
|
+
const baseDirArg = args.base_dir;
|
|
186
766
|
if (typeof accessToken !== "string" || !accessToken.trim()) {
|
|
187
767
|
return { content: [{ type: "text", text: "access_token is required." }], isError: true };
|
|
188
768
|
}
|
|
@@ -195,8 +775,20 @@ export async function handleToolCall(name, args) {
|
|
|
195
775
|
if (typeof article !== "object" || article === null) {
|
|
196
776
|
return { content: [{ type: "text", text: "article is required." }], isError: true };
|
|
197
777
|
}
|
|
778
|
+
const baseDir = typeof baseDirArg === "string" && baseDirArg.trim() ? baseDirArg.trim() : process.cwd();
|
|
779
|
+
const articleToUpdate = { ...article };
|
|
780
|
+
if (typeof articleToUpdate.content === "string" && articleToUpdate.content.trim()) {
|
|
781
|
+
try {
|
|
782
|
+
const sanitizedHtml = removeLeadingHtmlH1(articleToUpdate.content);
|
|
783
|
+
const replaced = await uploadLocalImageSourcesInHtml(sanitizedHtml, accessToken, baseDir);
|
|
784
|
+
articleToUpdate.content = replaced.html;
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
return { content: [{ type: "text", text: error.message }], isError: true };
|
|
788
|
+
}
|
|
789
|
+
}
|
|
198
790
|
try {
|
|
199
|
-
const result = await draftUpdate(accessToken, mediaId, index,
|
|
791
|
+
const result = await draftUpdate(accessToken, mediaId, index, articleToUpdate);
|
|
200
792
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
201
793
|
}
|
|
202
794
|
catch (error) {
|
|
@@ -212,7 +804,7 @@ export async function handleToolCall(name, args) {
|
|
|
212
804
|
if (typeof filePath !== "string" || !filePath.trim()) {
|
|
213
805
|
return { content: [{ type: "text", text: "file_path is required." }], isError: true };
|
|
214
806
|
}
|
|
215
|
-
const ext = filePath
|
|
807
|
+
const ext = getFileExtension(filePath);
|
|
216
808
|
if (ext !== "jpg" && ext !== "jpeg" && ext !== "png") {
|
|
217
809
|
return { content: [{ type: "text", text: "Only JPG and PNG files are supported." }], isError: true };
|
|
218
810
|
}
|
|
@@ -224,6 +816,43 @@ export async function handleToolCall(name, args) {
|
|
|
224
816
|
return { content: [{ type: "text", text: error.message }], isError: true };
|
|
225
817
|
}
|
|
226
818
|
}
|
|
819
|
+
if (name === "wechat_add_material") {
|
|
820
|
+
const accessToken = args.access_token;
|
|
821
|
+
const type = args.type;
|
|
822
|
+
const filePath = args.file_path;
|
|
823
|
+
const title = typeof args.title === "string" ? args.title.trim() : undefined;
|
|
824
|
+
const introduction = typeof args.introduction === "string" ? args.introduction.trim() : undefined;
|
|
825
|
+
if (typeof accessToken !== "string" || !accessToken.trim()) {
|
|
826
|
+
return { content: [{ type: "text", text: "access_token is required." }], isError: true };
|
|
827
|
+
}
|
|
828
|
+
if (typeof type !== "string" || !PERMANENT_MATERIAL_TYPES.includes(type)) {
|
|
829
|
+
return { content: [{ type: "text", text: "type must be one of: image, voice, video, thumb." }], isError: true };
|
|
830
|
+
}
|
|
831
|
+
if (typeof filePath !== "string" || !filePath.trim()) {
|
|
832
|
+
return { content: [{ type: "text", text: "file_path is required." }], isError: true };
|
|
833
|
+
}
|
|
834
|
+
const ext = getFileExtension(filePath);
|
|
835
|
+
const allowedExt = MATERIAL_EXTENSION_RULES[type];
|
|
836
|
+
if (!ext || !allowedExt.includes(ext)) {
|
|
837
|
+
return {
|
|
838
|
+
content: [{ type: "text", text: `Invalid file extension for type=${type}. Allowed: ${allowedExt.join(", ")}.` }],
|
|
839
|
+
isError: true
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
if (type === "video" && (!title || !introduction)) {
|
|
843
|
+
return {
|
|
844
|
+
content: [{ type: "text", text: "For video material, both title and introduction are required." }],
|
|
845
|
+
isError: true
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
const result = await addMaterial(accessToken, type, filePath, title || introduction ? { title, introduction } : undefined);
|
|
850
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
851
|
+
}
|
|
852
|
+
catch (error) {
|
|
853
|
+
return { content: [{ type: "text", text: error.message }], isError: true };
|
|
854
|
+
}
|
|
855
|
+
}
|
|
227
856
|
if (name === "wechat_draft_batchget") {
|
|
228
857
|
const accessToken = args.access_token;
|
|
229
858
|
const offset = args.offset ?? 0;
|