@wenyan-md/core 2.0.7 → 3.0.0

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/dist/wrapper.js CHANGED
@@ -1,7 +1,539 @@
1
+ import path from "node:path";
2
+ import { Readable } from "node:stream";
3
+ import { FormData, File } from "formdata-node";
4
+ import { FormDataEncoder } from "form-data-encoder";
1
5
  import { JSDOM } from "jsdom";
2
- import { createWenyanCore } from "./core.js";
3
- import { c, a, b } from "./configStore-lZ5bhrcC.js";
6
+ import fs, { stat } from "node:fs/promises";
7
+ import crypto from "node:crypto";
8
+ import { fileFromPath } from "formdata-node/file-from-path";
9
+ import os from "node:os";
10
+ import { d as defaultTokenCache, WechatPublisher } from "./publish.js";
11
+ import { createWenyanCore, getAllGzhThemes } from "./core.js";
12
+ async function readFileContent(filePath) {
13
+ return await fs.readFile(filePath, "utf-8");
14
+ }
15
+ async function readBinaryFile(filePath) {
16
+ return await fs.readFile(filePath);
17
+ }
18
+ async function safeReadJson(file, fallback) {
19
+ try {
20
+ const content = await fs.readFile(file, "utf-8");
21
+ return JSON.parse(content);
22
+ } catch {
23
+ return fallback;
24
+ }
25
+ }
26
+ async function safeWriteJson(file, data) {
27
+ const tmp = file + ".tmp";
28
+ await fs.writeFile(tmp, JSON.stringify(data ?? {}, null, 2), "utf-8");
29
+ await fs.rename(tmp, file);
30
+ }
31
+ async function ensureDir(dir) {
32
+ await fs.mkdir(dir, { recursive: true });
33
+ }
34
+ function md5FromBuffer(buf) {
35
+ return crypto.createHash("md5").update(buf).digest("hex");
36
+ }
37
+ async function md5FromFile(filePath) {
38
+ const buf = await fs.readFile(filePath);
39
+ return md5FromBuffer(buf);
40
+ }
41
+ function normalizePath(p) {
42
+ return p.replace(/\\/g, "/").replace(/\/+$/, "");
43
+ }
44
+ function isAbsolutePath(path2) {
45
+ if (!path2) return false;
46
+ const winAbsPattern = /^[a-zA-Z]:\//;
47
+ const linuxAbsPattern = /^\//;
48
+ return winAbsPattern.test(path2) || linuxAbsPattern.test(path2);
49
+ }
50
+ function getNormalizeFilePath(inputPath) {
51
+ const isContainer = !!process.env.CONTAINERIZED;
52
+ const hostFilePath = normalizePath(process.env.HOST_FILE_PATH || "");
53
+ if (isContainer && hostFilePath) {
54
+ const containerFilePath = normalizePath(process.env.CONTAINER_FILE_PATH || "/mnt/host-downloads");
55
+ let relativePart = normalizePath(inputPath);
56
+ if (relativePart.startsWith(hostFilePath)) {
57
+ relativePart = relativePart.slice(hostFilePath.length);
58
+ }
59
+ if (!relativePart.startsWith("/")) {
60
+ relativePart = "/" + relativePart;
61
+ }
62
+ return containerFilePath + relativePart;
63
+ } else {
64
+ return path.resolve(inputPath);
65
+ }
66
+ }
67
+ const RuntimeEnv = {
68
+ isContainer: !!process.env.CONTAINERIZED,
69
+ hostFilePath: normalizePath(process.env.HOST_FILE_PATH || ""),
70
+ containerFilePath: normalizePath(process.env.CONTAINER_FILE_PATH || "/mnt/host-downloads"),
71
+ resolveLocalPath(inputPath, relativeBase) {
72
+ if (!this.isContainer) {
73
+ if (relativeBase) {
74
+ return path.resolve(relativeBase, inputPath);
75
+ } else {
76
+ if (!path.isAbsolute(inputPath)) {
77
+ throw new Error(
78
+ `Invalid input: '${inputPath}'. InputPath must be an absolute path.`
79
+ );
80
+ }
81
+ return path.normalize(inputPath);
82
+ }
83
+ }
84
+ let normalizedInput = normalizePath(inputPath);
85
+ relativeBase = normalizePath(relativeBase || "");
86
+ if (relativeBase) {
87
+ if (!isAbsolutePath(normalizedInput)) {
88
+ normalizedInput = relativeBase + (normalizedInput.startsWith("/") ? "" : "/") + normalizedInput;
89
+ }
90
+ } else {
91
+ if (!isAbsolutePath(normalizedInput)) {
92
+ throw new Error(
93
+ `Invalid input: '${inputPath}'. InputPath must be an absolute path.`
94
+ );
95
+ }
96
+ }
97
+ if (this.hostFilePath && normalizedInput.startsWith(this.hostFilePath)) {
98
+ let relativePart = normalizedInput.slice(this.hostFilePath.length);
99
+ if (relativePart && !relativePart.startsWith("/")) {
100
+ return normalizedInput;
101
+ }
102
+ if (!relativePart.startsWith("/")) {
103
+ relativePart = "/" + relativePart;
104
+ }
105
+ return this.containerFilePath + relativePart;
106
+ }
107
+ return normalizedInput;
108
+ }
109
+ };
110
+ function getServerUrl(options) {
111
+ let serverUrl = options.server || "http://localhost:3000";
112
+ serverUrl = serverUrl.replace(/\/$/, "");
113
+ return serverUrl;
114
+ }
115
+ function getHeaders(options) {
116
+ const headers = {};
117
+ if (options.clientVersion) {
118
+ headers["x-client-version"] = options.clientVersion;
119
+ }
120
+ if (options.apiKey) {
121
+ headers["x-api-key"] = options.apiKey;
122
+ }
123
+ return headers;
124
+ }
125
+ async function healthCheck(serverUrl) {
126
+ try {
127
+ const healthRes = await fetch(`${serverUrl}/health`, { method: "GET" });
128
+ if (!healthRes.ok) {
129
+ throw new Error(`HTTP Error: ${healthRes.status} ${healthRes.statusText}`);
130
+ }
131
+ const healthData = await healthRes.json();
132
+ if (healthData.status !== "ok" || healthData.service !== "wenyan-cli") {
133
+ throw new Error(`Invalid server response. Make sure the server URL is correct.`);
134
+ }
135
+ return healthData.version;
136
+ } catch (error) {
137
+ throw new Error(
138
+ `Failed to connect to server (${serverUrl}).
139
+ Please check if the server is running and the network is accessible.
140
+ Details: ${error.message}`
141
+ );
142
+ }
143
+ }
144
+ async function verifyAuth(serverUrl, headers) {
145
+ const verifyRes = await fetch(`${serverUrl}/verify`, {
146
+ method: "GET",
147
+ headers
148
+ // 携带 x-api-key 和 x-client-version
149
+ });
150
+ if (verifyRes.status === 401) {
151
+ throw new Error("鉴权失败 (401):Server 拒绝访问,请检查传入的 --api-key 是否正确。");
152
+ }
153
+ if (!verifyRes.ok) {
154
+ throw new Error(`Verify Error: ${verifyRes.status} ${verifyRes.statusText}`);
155
+ }
156
+ }
157
+ async function uploadStyledContent(gzhContent, serverUrl, headers) {
158
+ const mdFilename = "publish_target.json";
159
+ const mdForm = new FormData();
160
+ mdForm.append(
161
+ "file",
162
+ new File([Buffer.from(JSON.stringify(gzhContent), "utf-8")], mdFilename, { type: "application/json" })
163
+ );
164
+ const mdEncoder = new FormDataEncoder(mdForm);
165
+ const mdUploadRes = await fetch(`${serverUrl}/upload`, {
166
+ method: "POST",
167
+ headers: { ...headers, ...mdEncoder.headers },
168
+ body: Readable.from(mdEncoder),
169
+ duplex: "half"
170
+ });
171
+ const mdUploadData = await mdUploadRes.json();
172
+ if (!mdUploadRes.ok || !mdUploadData.success) {
173
+ throw new Error(`Upload Document Failed: ${mdUploadData.error || mdUploadData.desc || mdUploadRes.statusText}`);
174
+ }
175
+ const mdFileId = mdUploadData.data.fileId;
176
+ return mdFileId;
177
+ }
178
+ async function requestServerPublish(mdFileId, serverUrl, headers, options) {
179
+ const { theme, customTheme, highlight, macStyle, footnote } = options;
180
+ const publishRes = await fetch(`${serverUrl}/publish`, {
181
+ method: "POST",
182
+ headers: {
183
+ ...headers,
184
+ "Content-Type": "application/json"
185
+ },
186
+ body: JSON.stringify({
187
+ fileId: mdFileId,
188
+ theme,
189
+ highlight,
190
+ customTheme,
191
+ macStyle,
192
+ footnote
193
+ })
194
+ });
195
+ const publishData = await publishRes.json();
196
+ if (!publishRes.ok || publishData.code === -1) {
197
+ throw new Error(`Remote Publish Failed: ${publishData.desc || publishRes.statusText}`);
198
+ }
199
+ return publishData.media_id;
200
+ }
201
+ function needUpload(url) {
202
+ return !/^(https?:\/\/|data:|asset:\/\/)/i.test(url);
203
+ }
204
+ async function uploadLocalImage(originalUrl, serverUrl, headers, relativePath) {
205
+ const imagePath = RuntimeEnv.resolveLocalPath(originalUrl, relativePath);
206
+ let fileBuffer;
207
+ try {
208
+ fileBuffer = await readBinaryFile(imagePath);
209
+ } catch (error) {
210
+ if (error.code === "ENOENT") {
211
+ console.error(`[Client] Warning: Local image not found: ${imagePath}`);
212
+ return null;
213
+ }
214
+ throw new Error(`Failed to read local image (${imagePath}): ${error.message}`);
215
+ }
216
+ const filename = path.basename(imagePath);
217
+ const ext = path.extname(filename).toLowerCase();
218
+ const mimeTypes = {
219
+ ".jpg": "image/jpeg",
220
+ ".jpeg": "image/jpeg",
221
+ ".png": "image/png",
222
+ ".gif": "image/gif",
223
+ ".webp": "image/webp",
224
+ ".svg": "image/svg+xml"
225
+ };
226
+ const type = mimeTypes[ext] || "application/octet-stream";
227
+ const form = new FormData();
228
+ form.append("file", new File([fileBuffer], filename, { type }));
229
+ const encoder = new FormDataEncoder(form);
230
+ const uploadRes = await fetch(`${serverUrl}/upload`, {
231
+ method: "POST",
232
+ headers: { ...headers, ...encoder.headers },
233
+ body: Readable.from(encoder),
234
+ duplex: "half"
235
+ });
236
+ const uploadData = await uploadRes.json();
237
+ if (uploadRes.ok && uploadData.success) {
238
+ return `asset://${uploadData.data.fileId}`;
239
+ } else {
240
+ console.error(`[Client] Warning: Failed to upload ${filename}: ${uploadData.error || uploadData.desc}`);
241
+ return null;
242
+ }
243
+ }
244
+ async function uploadLocalImages(content, serverUrl, headers, relativePath) {
245
+ if (content.includes("<img")) {
246
+ const dom = new JSDOM(content);
247
+ const document = dom.window.document;
248
+ const images = Array.from(document.querySelectorAll("img"));
249
+ const uploadPromises = images.map(async (element) => {
250
+ const dataSrc = element.getAttribute("src");
251
+ if (dataSrc && needUpload(dataSrc)) {
252
+ const newUrl = await uploadLocalImage(dataSrc, serverUrl, headers, relativePath);
253
+ if (newUrl) {
254
+ element.setAttribute("src", newUrl);
255
+ }
256
+ }
257
+ });
258
+ await Promise.all(uploadPromises);
259
+ return document.body.innerHTML;
260
+ }
261
+ return content;
262
+ }
263
+ async function uploadCover(serverUrl, headers, cover, relativePath) {
264
+ if (cover && needUpload(cover)) {
265
+ const newCoverUrl = await uploadLocalImage(cover, serverUrl, headers, relativePath);
266
+ if (newCoverUrl) {
267
+ return newCoverUrl;
268
+ }
269
+ }
270
+ return cover;
271
+ }
272
+ const nodeHttpAdapter = {
273
+ fetch,
274
+ createMultipart(field, file, filename) {
275
+ const form = new FormData();
276
+ form.append(field, file, filename);
277
+ const encoder = new FormDataEncoder(form);
278
+ return {
279
+ body: Readable.from(encoder),
280
+ headers: encoder.headers,
281
+ duplex: "half"
282
+ };
283
+ }
284
+ };
285
+ const defaultConfig = {};
286
+ const configDir = process.env.APPDATA ? path.join(process.env.APPDATA, "wenyan-md") : path.join(os.homedir(), ".config", "wenyan-md");
287
+ const configPath = path.join(configDir, "config.json");
288
+ class ConfigStore {
289
+ config = { ...defaultConfig };
290
+ initPromise;
291
+ constructor() {
292
+ this.initPromise = this.load();
293
+ }
294
+ async load() {
295
+ await ensureDir(configDir);
296
+ this.config = await safeReadJson(configPath, defaultConfig);
297
+ }
298
+ async save() {
299
+ try {
300
+ await ensureDir(configDir);
301
+ await safeWriteJson(configPath, this.config);
302
+ } catch (error) {
303
+ throw new Error(`无法保存配置文件: ${error instanceof Error ? error.message : String(error)}`);
304
+ }
305
+ }
306
+ async getConfig() {
307
+ await this.initPromise;
308
+ return this.config;
309
+ }
310
+ async getThemes() {
311
+ await this.initPromise;
312
+ return Object.values(this.config.themes ?? {});
313
+ }
314
+ async getThemeById(themeId) {
315
+ await this.initPromise;
316
+ const themeOption = this.config.themes?.[themeId];
317
+ if (!themeOption) return void 0;
318
+ const absoluteFilePath = path.join(configDir, themeOption.path);
319
+ try {
320
+ return await fs.readFile(absoluteFilePath, "utf-8");
321
+ } catch {
322
+ return void 0;
323
+ }
324
+ }
325
+ async addThemeToConfig(name, content) {
326
+ await this.initPromise;
327
+ const savedPath = await this.addThemeFile(name, content);
328
+ this.config.themes ??= {};
329
+ this.config.themes[name] = {
330
+ id: name,
331
+ name,
332
+ path: savedPath
333
+ };
334
+ await this.save();
335
+ }
336
+ async addThemeFile(themeId, themeContent) {
337
+ const filePath = `themes/${themeId}.css`;
338
+ const absoluteFilePath = path.join(configDir, filePath);
339
+ await ensureDir(path.dirname(absoluteFilePath));
340
+ await fs.writeFile(absoluteFilePath, themeContent, "utf-8");
341
+ return filePath;
342
+ }
343
+ async deleteThemeFromConfig(themeId) {
344
+ await this.initPromise;
345
+ const theme = this.config.themes?.[themeId];
346
+ if (!theme) return;
347
+ await this.deleteThemeFile(theme.path);
348
+ delete this.config.themes[themeId];
349
+ await this.save();
350
+ }
351
+ async deleteThemeFile(filePath) {
352
+ try {
353
+ await fs.unlink(path.join(configDir, filePath));
354
+ } catch {
355
+ }
356
+ }
357
+ }
358
+ const configStore = new ConfigStore();
359
+ const tokenPath = path.join(configDir, "token.json");
360
+ class NodeTokenStorageAdapter {
361
+ async loadToken() {
362
+ await ensureDir(configDir);
363
+ return await safeReadJson(tokenPath, defaultTokenCache);
364
+ }
365
+ async saveToken(cache) {
366
+ await ensureDir(configDir);
367
+ await safeWriteJson(tokenPath, cache);
368
+ }
369
+ async clearToken() {
370
+ try {
371
+ await fs.unlink(tokenPath);
372
+ } catch (error) {
373
+ if (error.code !== "ENOENT") {
374
+ throw error;
375
+ }
376
+ }
377
+ }
378
+ }
379
+ const cachePath = path.join(configDir, "upload-cache.json");
380
+ class NodeUploadCacheAdapter {
381
+ async loadCache() {
382
+ await ensureDir(configDir);
383
+ return await safeReadJson(cachePath, {});
384
+ }
385
+ async saveCache(cache) {
386
+ await ensureDir(configDir);
387
+ await safeWriteJson(cachePath, cache);
388
+ }
389
+ async clearCache() {
390
+ try {
391
+ await fs.unlink(cachePath);
392
+ } catch (error) {
393
+ if (error.code !== "ENOENT") {
394
+ throw error;
395
+ }
396
+ }
397
+ }
398
+ async calcHash(buffer) {
399
+ return crypto.createHash("md5").update(Buffer.from(buffer)).digest("hex");
400
+ }
401
+ }
402
+ const mediaIdMapping = /* @__PURE__ */ new Map();
403
+ const wechatPublisher = new WechatPublisher(nodeHttpAdapter, new NodeTokenStorageAdapter(), new NodeUploadCacheAdapter());
404
+ async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
405
+ let fileData;
406
+ let finalName;
407
+ if (imageUrl.startsWith("http")) {
408
+ const response = await fetch(imageUrl);
409
+ if (!response.ok || !response.body) {
410
+ throw new Error(`下载图片失败 URL: ${imageUrl}`);
411
+ }
412
+ const arrayBuffer = await response.arrayBuffer();
413
+ if (arrayBuffer.byteLength === 0) {
414
+ throw new Error(`远程图片大小为0,无法上传: ${imageUrl}`);
415
+ }
416
+ const fileNameFromUrl = path.basename(imageUrl.split("?")[0]);
417
+ const ext = path.extname(fileNameFromUrl);
418
+ finalName = fileName ?? (ext === "" ? `${fileNameFromUrl}.jpg` : fileNameFromUrl);
419
+ const contentType = response.headers.get("content-type") || "image/jpeg";
420
+ fileData = new Blob([arrayBuffer], { type: contentType });
421
+ } else {
422
+ const resolvedPath = RuntimeEnv.resolveLocalPath(imageUrl, relativePath);
423
+ const stats = await stat(resolvedPath);
424
+ if (stats.size === 0) {
425
+ throw new Error(`本地图片大小为0,无法上传: ${resolvedPath}`);
426
+ }
427
+ const fileNameFromLocal = path.basename(resolvedPath);
428
+ const ext = path.extname(fileNameFromLocal);
429
+ finalName = fileName ?? (ext === "" ? `${fileNameFromLocal}.jpg` : fileNameFromLocal);
430
+ const fileFromPathResult = await fileFromPath(resolvedPath);
431
+ fileData = new Blob([await fileFromPathResult.arrayBuffer()], { type: fileFromPathResult.type });
432
+ }
433
+ const data = await wechatPublisher.uploadImage(fileData, finalName, accessToken);
434
+ mediaIdMapping.set(data.url, data.media_id);
435
+ return data;
436
+ }
437
+ async function uploadImages(content, accessToken, relativePath) {
438
+ if (!content.includes("<img")) {
439
+ return { html: content, firstImageId: "" };
440
+ }
441
+ const dom = new JSDOM(content);
442
+ const document = dom.window.document;
443
+ const images = Array.from(document.querySelectorAll("img"));
444
+ const uploadPromises = images.map(async (element) => {
445
+ const dataSrc = element.getAttribute("src");
446
+ if (dataSrc) {
447
+ if (!dataSrc.startsWith("https://mmbiz.qpic.cn")) {
448
+ const resp = await uploadImage(dataSrc, accessToken, void 0, relativePath);
449
+ element.setAttribute("src", resp.url);
450
+ return resp.media_id;
451
+ } else {
452
+ return dataSrc;
453
+ }
454
+ }
455
+ return null;
456
+ });
457
+ const mediaIds = (await Promise.all(uploadPromises)).filter(Boolean);
458
+ const firstImageId = mediaIds[0] || "";
459
+ const updatedHtml = dom.serialize();
460
+ return { html: updatedHtml, firstImageId };
461
+ }
462
+ async function publishToWechatDraft(articleOptions, publishOptions = {}) {
463
+ const { title, content, cover, author, source_url } = articleOptions;
464
+ const { appId, appSecret, relativePath } = publishOptions;
465
+ const appIdFinal = appId ?? process.env.WECHAT_APP_ID;
466
+ const appSecretFinal = appSecret ?? process.env.WECHAT_APP_SECRET;
467
+ if (!appIdFinal || !appSecretFinal) {
468
+ throw new Error("请通过参数或环境变量 WECHAT_APP_ID / WECHAT_APP_SECRET 提供公众号凭据");
469
+ }
470
+ const accessToken = await wechatPublisher.getAccessTokenWithCache(appIdFinal, appSecretFinal);
471
+ const { html, firstImageId } = await uploadImages(content, accessToken, relativePath);
472
+ let thumbMediaId = "";
473
+ if (cover) {
474
+ const cachedThumbMediaId = mediaIdMapping.get(cover);
475
+ if (cachedThumbMediaId) {
476
+ thumbMediaId = cachedThumbMediaId;
477
+ } else {
478
+ const resp = await uploadImage(cover, accessToken, "cover.jpg", relativePath);
479
+ thumbMediaId = resp.media_id;
480
+ }
481
+ } else {
482
+ if (firstImageId.startsWith("https://mmbiz.qpic.cn")) {
483
+ const cachedThumbMediaId = mediaIdMapping.get(firstImageId);
484
+ if (cachedThumbMediaId) {
485
+ thumbMediaId = cachedThumbMediaId;
486
+ } else {
487
+ const resp = await uploadImage(firstImageId, accessToken, "cover.jpg", relativePath);
488
+ thumbMediaId = resp.media_id;
489
+ }
490
+ } else {
491
+ thumbMediaId = firstImageId;
492
+ }
493
+ }
494
+ if (!thumbMediaId) {
495
+ throw new Error("你必须指定一张封面图或者在正文中至少出现一张图片。");
496
+ }
497
+ const data = await wechatPublisher.publishToDraft(accessToken, {
498
+ title,
499
+ content: html,
500
+ thumb_media_id: thumbMediaId,
501
+ author,
502
+ content_source_url: source_url
503
+ });
504
+ if (data.media_id) {
505
+ return data;
506
+ }
507
+ throw new Error(`上传到公众号草稿失败: ${JSON.stringify(data)}`);
508
+ }
509
+ async function publishToDraft(title, content, cover = "", options = {}) {
510
+ return publishToWechatDraft({ title, content, cover }, options);
511
+ }
4
512
  const wenyanCoreInstance = await createWenyanCore();
513
+ async function renderWithTheme(markdownContent, options) {
514
+ if (!markdownContent) {
515
+ throw new Error("No content provided for rendering.");
516
+ }
517
+ const { theme, customTheme, highlight, macStyle, footnote } = options;
518
+ let handledCustomTheme = customTheme;
519
+ if (customTheme) {
520
+ const normalizePath2 = getNormalizeFilePath(customTheme);
521
+ handledCustomTheme = await readFileContent(normalizePath2);
522
+ } else if (theme) {
523
+ handledCustomTheme = await configStore.getThemeById(theme);
524
+ }
525
+ if (!handledCustomTheme && !theme) {
526
+ throw new Error(`theme "${theme}" not found.`);
527
+ }
528
+ const gzhContent = await renderStyledContent(markdownContent, {
529
+ themeId: theme,
530
+ hlThemeId: highlight,
531
+ isMacStyle: macStyle,
532
+ isAddFootnote: footnote,
533
+ themeCss: handledCustomTheme
534
+ });
535
+ return gzhContent;
536
+ }
5
537
  async function renderStyledContent(content, options = {}) {
6
538
  const preHandlerContent = await wenyanCoreInstance.handleFrontMatter(content);
7
539
  const html = await wenyanCoreInstance.renderMarkdown(preHandlerContent.body);
@@ -18,6 +550,71 @@ async function renderStyledContent(content, options = {}) {
18
550
  source_url: preHandlerContent.source_url
19
551
  };
20
552
  }
553
+ async function prepareRenderContext(inputContent, options, getInputContent) {
554
+ const { content, absoluteDirPath } = await getInputContent(inputContent, options.file);
555
+ const gzhContent = await renderWithTheme(content, options);
556
+ return { gzhContent, absoluteDirPath };
557
+ }
558
+ async function listThemes() {
559
+ const themes = getAllGzhThemes();
560
+ const themeList = themes.map((theme) => {
561
+ return {
562
+ id: theme.meta.id,
563
+ name: theme.meta.name,
564
+ description: theme.meta.description,
565
+ isBuiltin: true
566
+ };
567
+ });
568
+ const customThemes = await configStore.getThemes();
569
+ if (customThemes.length > 0) {
570
+ customThemes.forEach((theme) => {
571
+ themeList.push({
572
+ id: theme.id,
573
+ name: theme.id,
574
+ description: theme.description,
575
+ isBuiltin: false
576
+ });
577
+ });
578
+ }
579
+ return themeList;
580
+ }
581
+ async function addTheme(name, path2) {
582
+ if (!name || !path2) {
583
+ throw new Error("添加主题时必须提供名称(name)和路径(path)");
584
+ }
585
+ if (checkThemeExists(name) || await checkCustomThemeExists(name)) {
586
+ throw new Error(`主题 "${name}" 已存在`);
587
+ }
588
+ if (path2.startsWith("http")) {
589
+ const response = await fetch(path2);
590
+ if (!response.ok) {
591
+ throw new Error(`无法从远程获取主题: ${response.statusText}`);
592
+ }
593
+ const content = await response.text();
594
+ await configStore.addThemeToConfig(name, content);
595
+ } else {
596
+ const normalizePath2 = getNormalizeFilePath(path2);
597
+ const content = await readFileContent(normalizePath2);
598
+ await configStore.addThemeToConfig(name, content);
599
+ }
600
+ }
601
+ async function removeTheme(name) {
602
+ if (checkThemeExists(name)) {
603
+ throw new Error(`默认主题 "${name}" 不能删除`);
604
+ }
605
+ if (!await checkCustomThemeExists(name)) {
606
+ throw new Error(`自定义主题 "${name}" 不存在`);
607
+ }
608
+ await configStore.deleteThemeFromConfig(name);
609
+ }
610
+ function checkThemeExists(themeId) {
611
+ const themes = getAllGzhThemes();
612
+ return themes.some((theme) => theme.meta.id === themeId);
613
+ }
614
+ async function checkCustomThemeExists(themeId) {
615
+ const customThemes = await configStore.getThemes();
616
+ return customThemes.some((theme) => theme.id === themeId);
617
+ }
21
618
  async function getGzhContent(content, themeId, hlThemeId, isMacStyle = true, isAddFootnote = true) {
22
619
  return await renderStyledContent(content, {
23
620
  themeId,
@@ -26,10 +623,63 @@ async function getGzhContent(content, themeId, hlThemeId, isMacStyle = true, isA
26
623
  isAddFootnote
27
624
  });
28
625
  }
626
+ async function renderAndPublish(inputContent, options, getInputContent) {
627
+ const { gzhContent, absoluteDirPath } = await prepareRenderContext(inputContent, options, getInputContent);
628
+ if (!gzhContent.title) throw new Error("未能找到文章标题");
629
+ const data = await publishToWechatDraft(
630
+ {
631
+ title: gzhContent.title,
632
+ content: gzhContent.content,
633
+ cover: gzhContent.cover,
634
+ author: gzhContent.author,
635
+ source_url: gzhContent.source_url
636
+ },
637
+ {
638
+ relativePath: absoluteDirPath
639
+ }
640
+ );
641
+ if (data.media_id) {
642
+ return data.media_id;
643
+ } else {
644
+ throw new Error(`发布到微信公众号失败,
645
+ ${data}`);
646
+ }
647
+ }
648
+ async function renderAndPublishToServer(inputContent, options, getInputContent) {
649
+ const serverUrl = getServerUrl(options);
650
+ const headers = getHeaders(options);
651
+ await healthCheck(serverUrl);
652
+ await verifyAuth(serverUrl, headers);
653
+ const { gzhContent, absoluteDirPath } = await prepareRenderContext(inputContent, options, getInputContent);
654
+ if (!gzhContent.title) throw new Error("未能找到文章标题");
655
+ gzhContent.content = await uploadLocalImages(gzhContent.content, serverUrl, headers, absoluteDirPath);
656
+ gzhContent.cover = await uploadCover(serverUrl, headers, gzhContent.cover, absoluteDirPath);
657
+ const mdFileId = await uploadStyledContent(gzhContent, serverUrl, headers);
658
+ return await requestServerPublish(mdFileId, serverUrl, headers, options);
659
+ }
29
660
  export {
30
- c as configDir,
31
- a as configPath,
32
- b as configStore,
661
+ addTheme,
662
+ configDir,
663
+ configPath,
664
+ configStore,
665
+ ensureDir,
33
666
  getGzhContent,
34
- renderStyledContent
667
+ getNormalizeFilePath,
668
+ isAbsolutePath,
669
+ listThemes,
670
+ md5FromBuffer,
671
+ md5FromFile,
672
+ normalizePath,
673
+ prepareRenderContext,
674
+ publishToDraft,
675
+ publishToWechatDraft,
676
+ readBinaryFile,
677
+ readFileContent,
678
+ removeTheme,
679
+ renderAndPublish,
680
+ renderAndPublishToServer,
681
+ renderStyledContent,
682
+ renderWithTheme,
683
+ safeReadJson,
684
+ safeWriteJson
35
685
  };