@wenyan-md/core 2.0.6 → 2.0.8

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <img alt="logo" src="https://raw.githubusercontent.com/caol64/wenyan/main/Data/256-mac.png" />
2
+ <img alt="logo" src="https://raw.githubusercontent.com/caol64/wenyan/main/Data/256-mac.png" width="128" />
3
3
  </div>
4
4
 
5
5
  # 文颜 CORE
@@ -11,7 +11,7 @@
11
11
 
12
12
  ## 简介
13
13
 
14
- **文颜(Wenyan)** 是一套多平台 Markdown 排版与发布工具链, **文颜 CORE** 是其核心库,专注于:
14
+ **[文颜(Wenyan)](https://wenyan.yuzhi.tech/)** 是一套多平台 Markdown 排版与发布工具链, **文颜 CORE** 是其核心库,专注于:
15
15
 
16
16
  - Markdown → HTML 渲染
17
17
  - 主题排版(公众号 / Web)
package/dist/publish.js CHANGED
@@ -1,13 +1,42 @@
1
1
  import { JSDOM } from "jsdom";
2
2
  import { fileFromPath } from "formdata-node/file-from-path";
3
3
  import path from "node:path";
4
- import { stat } from "node:fs/promises";
4
+ import fs, { stat } from "node:fs/promises";
5
+ import crypto from "node:crypto";
5
6
  import { createWechatClient } from "./wechat.js";
6
7
  import { FormDataEncoder } from "form-data-encoder";
7
8
  import { FormData } from "formdata-node";
8
9
  import { Readable } from "node:stream";
9
- import fs from "node:fs";
10
- import { c as configDir, e as ensureDir, s as safeReadJson, d as safeWriteJson, m as md5FromBuffer, f as md5FromFile } from "./configStore-lZ5bhrcC.js";
10
+ import os from "node:os";
11
+ async function readFileContent(filePath) {
12
+ return await fs.readFile(filePath, "utf-8");
13
+ }
14
+ async function readBinaryFile(filePath) {
15
+ return await fs.readFile(filePath);
16
+ }
17
+ async function safeReadJson(file, fallback) {
18
+ try {
19
+ const content = await fs.readFile(file, "utf-8");
20
+ return JSON.parse(content);
21
+ } catch {
22
+ return fallback;
23
+ }
24
+ }
25
+ async function safeWriteJson(file, data) {
26
+ const tmp = file + ".tmp";
27
+ await fs.writeFile(tmp, JSON.stringify(data ?? {}, null, 2), "utf-8");
28
+ await fs.rename(tmp, file);
29
+ }
30
+ async function ensureDir(dir) {
31
+ await fs.mkdir(dir, { recursive: true });
32
+ }
33
+ function md5FromBuffer(buf) {
34
+ return crypto.createHash("md5").update(buf).digest("hex");
35
+ }
36
+ async function md5FromFile(filePath) {
37
+ const buf = await fs.readFile(filePath);
38
+ return md5FromBuffer(buf);
39
+ }
11
40
  function normalizePath(p) {
12
41
  return p.replace(/\\/g, "/").replace(/\/+$/, "");
13
42
  }
@@ -17,6 +46,23 @@ function isAbsolutePath(path2) {
17
46
  const linuxAbsPattern = /^\//;
18
47
  return winAbsPattern.test(path2) || linuxAbsPattern.test(path2);
19
48
  }
49
+ function getNormalizeFilePath(inputPath) {
50
+ const isContainer = !!process.env.CONTAINERIZED;
51
+ const hostFilePath = normalizePath(process.env.HOST_FILE_PATH || "");
52
+ if (isContainer && hostFilePath) {
53
+ const containerFilePath = normalizePath(process.env.CONTAINER_FILE_PATH || "/mnt/host-downloads");
54
+ let relativePart = normalizePath(inputPath);
55
+ if (relativePart.startsWith(hostFilePath)) {
56
+ relativePart = relativePart.slice(hostFilePath.length);
57
+ }
58
+ if (!relativePart.startsWith("/")) {
59
+ relativePart = "/" + relativePart;
60
+ }
61
+ return containerFilePath + relativePart;
62
+ } else {
63
+ return path.resolve(inputPath);
64
+ }
65
+ }
20
66
  const RuntimeEnv = {
21
67
  isContainer: !!process.env.CONTAINERIZED,
22
68
  hostFilePath: normalizePath(process.env.HOST_FILE_PATH || ""),
@@ -28,7 +74,7 @@ const RuntimeEnv = {
28
74
  } else {
29
75
  if (!path.isAbsolute(inputPath)) {
30
76
  throw new Error(
31
- `Invalid input: '${inputPath}'. When relativeBase is not provided, inputPath must be an absolute path.`
77
+ `Invalid input: '${inputPath}'. InputPath must be an absolute path.`
32
78
  );
33
79
  }
34
80
  return path.normalize(inputPath);
@@ -43,11 +89,11 @@ const RuntimeEnv = {
43
89
  } else {
44
90
  if (!isAbsolutePath(normalizedInput)) {
45
91
  throw new Error(
46
- `Invalid input: '${inputPath}'. When relativeBase is not provided, inputPath must be an absolute path.`
92
+ `Invalid input: '${inputPath}'. InputPath must be an absolute path.`
47
93
  );
48
94
  }
49
95
  }
50
- if (normalizedInput.startsWith(this.hostFilePath)) {
96
+ if (this.hostFilePath && normalizedInput.startsWith(this.hostFilePath)) {
51
97
  let relativePart = normalizedInput.slice(this.hostFilePath.length);
52
98
  if (relativePart && !relativePart.startsWith("/")) {
53
99
  return normalizedInput;
@@ -73,6 +119,80 @@ const nodeHttpAdapter = {
73
119
  };
74
120
  }
75
121
  };
122
+ const defaultConfig = {};
123
+ const configDir = process.env.APPDATA ? path.join(process.env.APPDATA, "wenyan-md") : path.join(os.homedir(), ".config", "wenyan-md");
124
+ const configPath = path.join(configDir, "config.json");
125
+ class ConfigStore {
126
+ config = { ...defaultConfig };
127
+ initPromise;
128
+ constructor() {
129
+ this.initPromise = this.load();
130
+ }
131
+ async load() {
132
+ await ensureDir(configDir);
133
+ this.config = await safeReadJson(configPath, defaultConfig);
134
+ }
135
+ async save() {
136
+ try {
137
+ await ensureDir(configDir);
138
+ await safeWriteJson(configPath, this.config);
139
+ } catch (error) {
140
+ throw new Error(`无法保存配置文件: ${error instanceof Error ? error.message : String(error)}`);
141
+ }
142
+ }
143
+ async getConfig() {
144
+ await this.initPromise;
145
+ return this.config;
146
+ }
147
+ async getThemes() {
148
+ await this.initPromise;
149
+ return Object.values(this.config.themes ?? {});
150
+ }
151
+ async getThemeById(themeId) {
152
+ await this.initPromise;
153
+ const themeOption = this.config.themes?.[themeId];
154
+ if (!themeOption) return void 0;
155
+ const absoluteFilePath = path.join(configDir, themeOption.path);
156
+ try {
157
+ return await fs.readFile(absoluteFilePath, "utf-8");
158
+ } catch {
159
+ return void 0;
160
+ }
161
+ }
162
+ async addThemeToConfig(name, content) {
163
+ await this.initPromise;
164
+ const savedPath = await this.addThemeFile(name, content);
165
+ this.config.themes ??= {};
166
+ this.config.themes[name] = {
167
+ id: name,
168
+ name,
169
+ path: savedPath
170
+ };
171
+ await this.save();
172
+ }
173
+ async addThemeFile(themeId, themeContent) {
174
+ const filePath = `themes/${themeId}.css`;
175
+ const absoluteFilePath = path.join(configDir, filePath);
176
+ await ensureDir(path.dirname(absoluteFilePath));
177
+ await fs.writeFile(absoluteFilePath, themeContent, "utf-8");
178
+ return filePath;
179
+ }
180
+ async deleteThemeFromConfig(themeId) {
181
+ await this.initPromise;
182
+ const theme = this.config.themes?.[themeId];
183
+ if (!theme) return;
184
+ await this.deleteThemeFile(theme.path);
185
+ delete this.config.themes[themeId];
186
+ await this.save();
187
+ }
188
+ async deleteThemeFile(filePath) {
189
+ try {
190
+ await fs.unlink(path.join(configDir, filePath));
191
+ } catch {
192
+ }
193
+ }
194
+ }
195
+ const configStore = new ConfigStore();
76
196
  const tokenPath = path.join(configDir, "token.json");
77
197
  const defaultCache = {
78
198
  appid: "",
@@ -80,46 +200,49 @@ const defaultCache = {
80
200
  expireAt: 0
81
201
  };
82
202
  class TokenStore {
83
- cache = defaultCache;
203
+ cache = { ...defaultCache };
204
+ initPromise;
84
205
  constructor() {
85
- this.load();
206
+ this.initPromise = this.load();
86
207
  }
87
- load() {
88
- ensureDir(configDir);
89
- if (fs.existsSync(tokenPath)) {
90
- this.cache = safeReadJson(tokenPath, defaultCache);
91
- }
208
+ async load() {
209
+ await ensureDir(configDir);
210
+ this.cache = await safeReadJson(tokenPath, defaultCache);
92
211
  }
93
- save() {
212
+ async save() {
94
213
  try {
95
- ensureDir(configDir);
96
- safeWriteJson(tokenPath, this.cache);
214
+ await ensureDir(configDir);
215
+ await safeWriteJson(tokenPath, this.cache);
97
216
  } catch (error) {
98
- console.error("❌ 无法保存 token:", error);
217
+ throw new Error(`无法保存 token: ${error instanceof Error ? error.message : String(error)}`);
99
218
  }
100
219
  }
101
220
  isValid(appid) {
102
- if (!this.cache) return false;
103
- return this.cache.appid === appid && this.cache.expireAt > Date.now() / 1e3 + 600;
221
+ const currentTime = Math.floor(Date.now() / 1e3);
222
+ const bufferTime = 600;
223
+ const isAppidMatch = this.cache.appid === appid;
224
+ const isNotExpired = this.cache.expireAt > currentTime + bufferTime;
225
+ return isAppidMatch && isNotExpired;
104
226
  }
105
227
  getToken(appid) {
106
228
  return this.isValid(appid) ? this.cache.accessToken : null;
107
229
  }
108
- setToken(appid, accessToken, expiresIn) {
230
+ async setToken(appid, accessToken, expiresIn) {
231
+ await this.initPromise;
109
232
  this.cache = {
110
233
  appid,
111
234
  accessToken,
112
235
  expireAt: Math.floor(Date.now() / 1e3) + expiresIn
113
236
  };
114
- this.save();
237
+ await this.save();
115
238
  }
116
- clear() {
117
- this.cache = defaultCache;
239
+ async clear() {
240
+ await this.initPromise;
241
+ this.cache = { ...defaultCache };
118
242
  try {
119
- if (fs.existsSync(tokenPath)) {
120
- fs.unlinkSync(tokenPath);
121
- }
243
+ await fs.unlink(tokenPath);
122
244
  } catch {
245
+ await this.save();
123
246
  }
124
247
  }
125
248
  }
@@ -127,29 +250,30 @@ const tokenStore = new TokenStore();
127
250
  const cachePath = path.join(configDir, "upload-cache.json");
128
251
  class UploadCacheStore {
129
252
  cache = {};
253
+ initPromise;
130
254
  constructor() {
131
- this.load();
255
+ this.initPromise = this.load();
132
256
  }
133
- load() {
134
- ensureDir(configDir);
135
- if (fs.existsSync(cachePath)) {
136
- this.cache = safeReadJson(cachePath, {});
137
- }
257
+ async load() {
258
+ await ensureDir(configDir);
259
+ this.cache = await safeReadJson(cachePath, {});
138
260
  }
139
- save() {
261
+ async save() {
140
262
  try {
141
- ensureDir(configDir);
142
- safeWriteJson(cachePath, this.cache);
263
+ await ensureDir(configDir);
264
+ await safeWriteJson(cachePath, this.cache);
143
265
  } catch (error) {
144
- console.error("❌ 无法保存上传缓存:", error);
266
+ throw new Error(`无法保存上传缓存: ${error instanceof Error ? error.message : String(error)}`);
145
267
  }
146
268
  }
147
- get(md5) {
269
+ async get(md5) {
270
+ await this.initPromise;
148
271
  return this.cache[md5];
149
272
  }
150
- set(md5, mediaId, url) {
273
+ async set(md5, mediaId, url) {
274
+ await this.initPromise;
151
275
  this.cache[md5] = { media_id: mediaId, url, updated_at: Date.now() };
152
- this.save();
276
+ await this.save();
153
277
  }
154
278
  }
155
279
  const uploadCacheStore = new UploadCacheStore();
@@ -162,7 +286,7 @@ async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
162
286
  if (imageUrl.startsWith("http")) {
163
287
  const response = await fetch(imageUrl);
164
288
  if (!response.ok || !response.body) {
165
- throw new Error(`Failed to download image from URL: ${imageUrl}`);
289
+ throw new Error(`下载图片失败 URL: ${imageUrl}`);
166
290
  }
167
291
  const arrayBuffer = await response.arrayBuffer();
168
292
  if (arrayBuffer.byteLength === 0) {
@@ -170,7 +294,7 @@ async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
170
294
  }
171
295
  const buffer = Buffer.from(arrayBuffer);
172
296
  md5 = md5FromBuffer(buffer);
173
- const cached = uploadCacheStore.get(md5);
297
+ const cached = await uploadCacheStore.get(md5);
174
298
  if (cached) {
175
299
  mediaIdMapping.set(cached.url, cached.media_id);
176
300
  return {
@@ -189,8 +313,8 @@ async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
189
313
  if (stats.size === 0) {
190
314
  throw new Error(`本地图片大小为0,无法上传: ${resolvedPath}`);
191
315
  }
192
- md5 = md5FromFile(resolvedPath);
193
- const cached = uploadCacheStore.get(md5);
316
+ md5 = await md5FromFile(resolvedPath);
317
+ const cached = await uploadCacheStore.get(md5);
194
318
  if (cached) {
195
319
  mediaIdMapping.set(cached.url, cached.media_id);
196
320
  return {
@@ -205,7 +329,7 @@ async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
205
329
  fileData = new Blob([await fileFromPathResult.arrayBuffer()], { type: fileFromPathResult.type });
206
330
  }
207
331
  const data = await uploadMaterial("image", fileData, finalName, accessToken);
208
- uploadCacheStore.set(md5, data.media_id, data.url);
332
+ await uploadCacheStore.set(md5, data.media_id, data.url);
209
333
  mediaIdMapping.set(data.url, data.media_id);
210
334
  return data;
211
335
  }
@@ -252,7 +376,6 @@ async function publishToWechatDraft(articleOptions, publishOptions = {}) {
252
376
  } else {
253
377
  const resp = await uploadImage(cover, accessToken, "cover.jpg", relativePath);
254
378
  thumbMediaId = resp.media_id;
255
- mediaIdMapping.set(resp.url, resp.media_id);
256
379
  }
257
380
  } else {
258
381
  if (firstImageId.startsWith("https://mmbiz.qpic.cn")) {
@@ -262,7 +385,6 @@ async function publishToWechatDraft(articleOptions, publishOptions = {}) {
262
385
  } else {
263
386
  const resp = await uploadImage(firstImageId, accessToken, "cover.jpg", relativePath);
264
387
  thumbMediaId = resp.media_id;
265
- mediaIdMapping.set(resp.url, resp.media_id);
266
388
  }
267
389
  } else {
268
390
  thumbMediaId = firstImageId;
@@ -292,10 +414,27 @@ async function getAccessTokenWithCache(appId, appSecret) {
292
414
  return cached;
293
415
  }
294
416
  const result = await fetchAccessToken(appId, appSecret);
295
- tokenStore.setToken(appId, result.access_token, result.expires_in);
417
+ await tokenStore.setToken(appId, result.access_token, result.expires_in);
296
418
  return result.access_token;
297
419
  }
298
420
  export {
421
+ RuntimeEnv as R,
422
+ readFileContent as a,
423
+ configDir as b,
424
+ configStore as c,
425
+ configPath as d,
426
+ ensureDir as e,
427
+ md5FromFile as f,
428
+ getNormalizeFilePath as g,
429
+ safeWriteJson as h,
430
+ isAbsolutePath as i,
431
+ tokenStore as j,
432
+ md5FromBuffer as m,
433
+ normalizePath as n,
299
434
  publishToDraft,
300
- publishToWechatDraft
435
+ publishToWechatDraft,
436
+ readBinaryFile as r,
437
+ safeReadJson as s,
438
+ tokenPath as t,
439
+ uploadCacheStore as u
301
440
  };
@@ -0,0 +1,15 @@
1
+ import { ClientPublishOptions, StyledContent } from "./types.js";
2
+ export declare function getServerUrl(options: ClientPublishOptions): string;
3
+ export declare function getHeaders(options: ClientPublishOptions): Record<string, string>;
4
+ /**
5
+ * 物理连通性测试
6
+ */
7
+ export declare function healthCheck(serverUrl: string): Promise<string>;
8
+ /**
9
+ * 鉴权探针测试
10
+ */
11
+ export declare function verifyAuth(serverUrl: string, headers: Record<string, string>): Promise<void>;
12
+ export declare function uploadStyledContent(gzhContent: StyledContent, serverUrl: string, headers: Record<string, string>): Promise<string>;
13
+ export declare function requestServerPublish(mdFileId: string, serverUrl: string, headers: Record<string, string>, options: ClientPublishOptions): Promise<string>;
14
+ export declare function uploadLocalImages(content: string, serverUrl: string, headers: Record<string, string>, relativePath?: string): Promise<string>;
15
+ export declare function uploadCover(serverUrl: string, headers: Record<string, string>, cover?: string, relativePath?: string): Promise<string | undefined>;
@@ -11,16 +11,17 @@ export declare const configDir: string;
11
11
  export declare const configPath: string;
12
12
  declare class ConfigStore {
13
13
  private config;
14
+ private initPromise;
14
15
  constructor();
15
16
  private load;
16
17
  private save;
17
- getConfig(): WenyanConfig;
18
- getThemes(): ThemeConfigOptions[];
19
- getThemeById(themeId: string): string | undefined;
20
- addThemeToConfig(name: string, content: string): void;
21
- addThemeFile(themeId: string, themeContent: string): string;
22
- deleteThemeFromConfig(themeId: string): void;
23
- deleteThemeFile(filePath: string): void;
18
+ getConfig(): Promise<WenyanConfig>;
19
+ getThemes(): Promise<ThemeConfigOptions[]>;
20
+ getThemeById(themeId: string): Promise<string | undefined>;
21
+ addThemeToConfig(name: string, content: string): Promise<void>;
22
+ addThemeFile(themeId: string, themeContent: string): Promise<string>;
23
+ deleteThemeFromConfig(themeId: string): Promise<void>;
24
+ deleteThemeFile(filePath: string): Promise<void>;
24
25
  }
25
26
  export declare const configStore: ConfigStore;
26
27
  export {};
@@ -1,4 +1,5 @@
1
- export interface PublishOptions {
1
+ import { WechatPublishResponse } from "../wechat.js";
2
+ export interface WechatPublishOptions {
2
3
  appId?: string;
3
4
  appSecret?: string;
4
5
  relativePath?: string;
@@ -10,5 +11,5 @@ export interface ArticleOptions {
10
11
  author?: string;
11
12
  source_url?: string;
12
13
  }
13
- export declare function publishToWechatDraft(articleOptions: ArticleOptions, publishOptions?: PublishOptions): Promise<import("../wechat.js").WechatPublishResponse>;
14
- export declare function publishToDraft(title: string, content: string, cover?: string, options?: PublishOptions): Promise<import("../wechat.js").WechatPublishResponse>;
14
+ export declare function publishToWechatDraft(articleOptions: ArticleOptions, publishOptions?: WechatPublishOptions): Promise<WechatPublishResponse>;
15
+ export declare function publishToDraft(title: string, content: string, cover?: string, options?: WechatPublishOptions): Promise<WechatPublishResponse>;
@@ -0,0 +1,5 @@
1
+ import { ApplyStylesOptions } from "../core/index.js";
2
+ import { GetInputContentFn, RenderContext, RenderOptions, StyledContent } from "./types.js";
3
+ export declare function renderWithTheme(markdownContent: string, options: RenderOptions): Promise<StyledContent>;
4
+ export declare function renderStyledContent(content: string, options?: ApplyStylesOptions): Promise<StyledContent>;
5
+ export declare function prepareRenderContext(inputContent: string | undefined, options: RenderOptions, getInputContent: GetInputContentFn): Promise<RenderContext>;
@@ -0,0 +1,16 @@
1
+ export interface ThemeOptions {
2
+ list?: boolean;
3
+ add?: boolean;
4
+ name?: string;
5
+ path?: string;
6
+ rm?: string;
7
+ }
8
+ export interface ThemeInfo {
9
+ id: string;
10
+ name: string;
11
+ description?: string;
12
+ isBuiltin: boolean;
13
+ }
14
+ export declare function listThemes(): Promise<ThemeInfo[]>;
15
+ export declare function addTheme(name?: string, path?: string): Promise<void>;
16
+ export declare function removeTheme(name: string): Promise<void>;
@@ -6,13 +6,14 @@ export interface TokenCache {
6
6
  }
7
7
  declare class TokenStore {
8
8
  private cache;
9
+ private initPromise;
9
10
  constructor();
10
11
  private load;
11
12
  private save;
12
13
  isValid(appid: string): boolean;
13
14
  getToken(appid: string): string | null;
14
- setToken(appid: string, accessToken: string, expiresIn: number): void;
15
- clear(): void;
15
+ setToken(appid: string, accessToken: string, expiresIn: number): Promise<void>;
16
+ clear(): Promise<void>;
16
17
  }
17
18
  export declare const tokenStore: TokenStore;
18
19
  export {};
@@ -0,0 +1,31 @@
1
+ export interface RenderOptions {
2
+ file?: string;
3
+ theme?: string;
4
+ customTheme?: string;
5
+ highlight: string;
6
+ macStyle: boolean;
7
+ footnote: boolean;
8
+ }
9
+ export interface PublishOptions extends RenderOptions {
10
+ }
11
+ export interface ClientPublishOptions extends RenderOptions {
12
+ server?: string;
13
+ apiKey?: string;
14
+ clientVersion?: string;
15
+ }
16
+ export interface RenderContext {
17
+ gzhContent: StyledContent;
18
+ absoluteDirPath: string | undefined;
19
+ }
20
+ export interface StyledContent {
21
+ content: string;
22
+ title?: string;
23
+ cover?: string;
24
+ description?: string;
25
+ author?: string;
26
+ source_url?: string;
27
+ }
28
+ export type GetInputContentFn = (inputContent?: string, filePath?: string) => Promise<{
29
+ content: string;
30
+ absoluteDirPath?: string;
31
+ }>;
@@ -5,11 +5,12 @@ export interface MediaInfo {
5
5
  }
6
6
  declare class UploadCacheStore {
7
7
  private cache;
8
+ private initPromise;
8
9
  constructor();
9
10
  private load;
10
11
  private save;
11
- get(md5: string): MediaInfo;
12
- set(md5: string, mediaId: string, url: string): void;
12
+ get(md5: string): Promise<MediaInfo | undefined>;
13
+ set(md5: string, mediaId: string, url: string): Promise<void>;
13
14
  }
14
15
  export declare const uploadCacheStore: UploadCacheStore;
15
16
  export {};
@@ -1,6 +1,16 @@
1
1
  import crypto from "node:crypto";
2
- export declare function safeReadJson<T>(file: string, fallback: T): T;
3
- export declare function safeWriteJson(file: string, data: unknown): void;
4
- export declare function ensureDir(dir: string): void;
2
+ export declare function readFileContent(filePath: string): Promise<string>;
3
+ export declare function readBinaryFile(filePath: string): Promise<Buffer>;
4
+ export declare function safeReadJson<T>(file: string, fallback: T): Promise<T>;
5
+ export declare function safeWriteJson(file: string, data: unknown): Promise<void>;
6
+ export declare function ensureDir(dir: string): Promise<void>;
5
7
  export declare function md5FromBuffer(buf: crypto.BinaryLike): string;
6
- export declare function md5FromFile(filePath: string): string;
8
+ export declare function md5FromFile(filePath: string): Promise<string>;
9
+ /**
10
+ * 路径标准化工具函数
11
+ * 将 Windows 的反斜杠 \ 转换为正斜杠 /,并去除末尾斜杠
12
+ * 目的:在 Linux 容器内也能正确处理 Windows 路径字符串
13
+ */
14
+ export declare function normalizePath(p: string): string;
15
+ export declare function isAbsolutePath(path: string): boolean;
16
+ export declare function getNormalizeFilePath(inputPath: string): string;
@@ -1,12 +1,11 @@
1
- import { ApplyStylesOptions } from "../core/index.js";
2
- export interface StyledContent {
3
- content: string;
4
- title?: string;
5
- cover?: string;
6
- description?: string;
7
- author?: string;
8
- source_url?: string;
9
- }
10
- export declare function renderStyledContent(content: string, options?: ApplyStylesOptions): Promise<StyledContent>;
1
+ import { ClientPublishOptions, GetInputContentFn, PublishOptions, StyledContent } from "./types.js";
11
2
  export declare function getGzhContent(content: string, themeId: string, hlThemeId: string, isMacStyle?: boolean, isAddFootnote?: boolean): Promise<StyledContent>;
3
+ export declare function renderAndPublish(inputContent: string | undefined, options: PublishOptions, getInputContent: GetInputContentFn): Promise<string>;
4
+ export declare function renderAndPublishToServer(inputContent: string | undefined, options: ClientPublishOptions, getInputContent: GetInputContentFn): Promise<string>;
12
5
  export * from "./configStore.js";
6
+ export * from "./uploadCacheStore.js";
7
+ export * from "./tokenStore.js";
8
+ export * from "./render.js";
9
+ export * from "./theme.js";
10
+ export * from "./types.js";
11
+ export * from "./utils.js";
package/dist/wrapper.js CHANGED
@@ -1,7 +1,198 @@
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 { R as RuntimeEnv, r as readBinaryFile, g as getNormalizeFilePath, a as readFileContent, c as configStore, publishToWechatDraft } from "./publish.js";
7
+ import { b, d, e, i, m, f, n, s, h, t, j, u } from "./publish.js";
8
+ import { createWenyanCore, getAllGzhThemes } from "./core.js";
9
+ function getServerUrl(options) {
10
+ let serverUrl = options.server || "http://localhost:3000";
11
+ serverUrl = serverUrl.replace(/\/$/, "");
12
+ return serverUrl;
13
+ }
14
+ function getHeaders(options) {
15
+ const headers = {};
16
+ if (options.clientVersion) {
17
+ headers["x-client-version"] = options.clientVersion;
18
+ }
19
+ if (options.apiKey) {
20
+ headers["x-api-key"] = options.apiKey;
21
+ }
22
+ return headers;
23
+ }
24
+ async function healthCheck(serverUrl) {
25
+ try {
26
+ const healthRes = await fetch(`${serverUrl}/health`, { method: "GET" });
27
+ if (!healthRes.ok) {
28
+ throw new Error(`HTTP Error: ${healthRes.status} ${healthRes.statusText}`);
29
+ }
30
+ const healthData = await healthRes.json();
31
+ if (healthData.status !== "ok" || healthData.service !== "wenyan-cli") {
32
+ throw new Error(`Invalid server response. Make sure the server URL is correct.`);
33
+ }
34
+ return healthData.version;
35
+ } catch (error) {
36
+ throw new Error(
37
+ `Failed to connect to server (${serverUrl}).
38
+ Please check if the server is running and the network is accessible.
39
+ Details: ${error.message}`
40
+ );
41
+ }
42
+ }
43
+ async function verifyAuth(serverUrl, headers) {
44
+ const verifyRes = await fetch(`${serverUrl}/verify`, {
45
+ method: "GET",
46
+ headers
47
+ // 携带 x-api-key 和 x-client-version
48
+ });
49
+ if (verifyRes.status === 401) {
50
+ throw new Error("鉴权失败 (401):Server 拒绝访问,请检查传入的 --api-key 是否正确。");
51
+ }
52
+ if (!verifyRes.ok) {
53
+ throw new Error(`Verify Error: ${verifyRes.status} ${verifyRes.statusText}`);
54
+ }
55
+ }
56
+ async function uploadStyledContent(gzhContent, serverUrl, headers) {
57
+ const mdFilename = "publish_target.json";
58
+ const mdForm = new FormData();
59
+ mdForm.append(
60
+ "file",
61
+ new File([Buffer.from(JSON.stringify(gzhContent), "utf-8")], mdFilename, { type: "application/json" })
62
+ );
63
+ const mdEncoder = new FormDataEncoder(mdForm);
64
+ const mdUploadRes = await fetch(`${serverUrl}/upload`, {
65
+ method: "POST",
66
+ headers: { ...headers, ...mdEncoder.headers },
67
+ body: Readable.from(mdEncoder),
68
+ duplex: "half"
69
+ });
70
+ const mdUploadData = await mdUploadRes.json();
71
+ if (!mdUploadRes.ok || !mdUploadData.success) {
72
+ throw new Error(`Upload Document Failed: ${mdUploadData.error || mdUploadData.desc || mdUploadRes.statusText}`);
73
+ }
74
+ const mdFileId = mdUploadData.data.fileId;
75
+ return mdFileId;
76
+ }
77
+ async function requestServerPublish(mdFileId, serverUrl, headers, options) {
78
+ const { theme, customTheme, highlight, macStyle, footnote } = options;
79
+ const publishRes = await fetch(`${serverUrl}/publish`, {
80
+ method: "POST",
81
+ headers: {
82
+ ...headers,
83
+ "Content-Type": "application/json"
84
+ },
85
+ body: JSON.stringify({
86
+ fileId: mdFileId,
87
+ theme,
88
+ highlight,
89
+ customTheme,
90
+ macStyle,
91
+ footnote
92
+ })
93
+ });
94
+ const publishData = await publishRes.json();
95
+ if (!publishRes.ok || publishData.code === -1) {
96
+ throw new Error(`Remote Publish Failed: ${publishData.desc || publishRes.statusText}`);
97
+ }
98
+ return publishData.media_id;
99
+ }
100
+ function needUpload(url) {
101
+ return !/^(https?:\/\/|data:|asset:\/\/)/i.test(url);
102
+ }
103
+ async function uploadLocalImage(originalUrl, serverUrl, headers, relativePath) {
104
+ const imagePath = RuntimeEnv.resolveLocalPath(originalUrl, relativePath);
105
+ let fileBuffer;
106
+ try {
107
+ fileBuffer = await readBinaryFile(imagePath);
108
+ } catch (error) {
109
+ if (error.code === "ENOENT") {
110
+ console.error(`[Client] Warning: Local image not found: ${imagePath}`);
111
+ return null;
112
+ }
113
+ throw new Error(`Failed to read local image (${imagePath}): ${error.message}`);
114
+ }
115
+ const filename = path.basename(imagePath);
116
+ const ext = path.extname(filename).toLowerCase();
117
+ const mimeTypes = {
118
+ ".jpg": "image/jpeg",
119
+ ".jpeg": "image/jpeg",
120
+ ".png": "image/png",
121
+ ".gif": "image/gif",
122
+ ".webp": "image/webp",
123
+ ".svg": "image/svg+xml"
124
+ };
125
+ const type = mimeTypes[ext] || "application/octet-stream";
126
+ const form = new FormData();
127
+ form.append("file", new File([fileBuffer], filename, { type }));
128
+ const encoder = new FormDataEncoder(form);
129
+ const uploadRes = await fetch(`${serverUrl}/upload`, {
130
+ method: "POST",
131
+ headers: { ...headers, ...encoder.headers },
132
+ body: Readable.from(encoder),
133
+ duplex: "half"
134
+ });
135
+ const uploadData = await uploadRes.json();
136
+ if (uploadRes.ok && uploadData.success) {
137
+ return `asset://${uploadData.data.fileId}`;
138
+ } else {
139
+ console.error(`[Client] Warning: Failed to upload ${filename}: ${uploadData.error || uploadData.desc}`);
140
+ return null;
141
+ }
142
+ }
143
+ async function uploadLocalImages(content, serverUrl, headers, relativePath) {
144
+ if (content.includes("<img")) {
145
+ const dom = new JSDOM(content);
146
+ const document = dom.window.document;
147
+ const images = Array.from(document.querySelectorAll("img"));
148
+ const uploadPromises = images.map(async (element) => {
149
+ const dataSrc = element.getAttribute("src");
150
+ if (dataSrc && needUpload(dataSrc)) {
151
+ const newUrl = await uploadLocalImage(dataSrc, serverUrl, headers, relativePath);
152
+ if (newUrl) {
153
+ element.setAttribute("src", newUrl);
154
+ }
155
+ }
156
+ });
157
+ await Promise.all(uploadPromises);
158
+ return document.body.innerHTML;
159
+ }
160
+ return content;
161
+ }
162
+ async function uploadCover(serverUrl, headers, cover, relativePath) {
163
+ if (cover && needUpload(cover)) {
164
+ const newCoverUrl = await uploadLocalImage(cover, serverUrl, headers, relativePath);
165
+ if (newCoverUrl) {
166
+ return newCoverUrl;
167
+ }
168
+ }
169
+ return cover;
170
+ }
4
171
  const wenyanCoreInstance = await createWenyanCore();
172
+ async function renderWithTheme(markdownContent, options) {
173
+ if (!markdownContent) {
174
+ throw new Error("No content provided for rendering.");
175
+ }
176
+ const { theme, customTheme, highlight, macStyle, footnote } = options;
177
+ let handledCustomTheme = customTheme;
178
+ if (customTheme) {
179
+ const normalizePath = getNormalizeFilePath(customTheme);
180
+ handledCustomTheme = await readFileContent(normalizePath);
181
+ } else if (theme) {
182
+ handledCustomTheme = await configStore.getThemeById(theme);
183
+ }
184
+ if (!handledCustomTheme && !theme) {
185
+ throw new Error(`theme "${theme}" not found.`);
186
+ }
187
+ const gzhContent = await renderStyledContent(markdownContent, {
188
+ themeId: theme,
189
+ hlThemeId: highlight,
190
+ isMacStyle: macStyle,
191
+ isAddFootnote: footnote,
192
+ themeCss: handledCustomTheme
193
+ });
194
+ return gzhContent;
195
+ }
5
196
  async function renderStyledContent(content, options = {}) {
6
197
  const preHandlerContent = await wenyanCoreInstance.handleFrontMatter(content);
7
198
  const html = await wenyanCoreInstance.renderMarkdown(preHandlerContent.body);
@@ -18,6 +209,71 @@ async function renderStyledContent(content, options = {}) {
18
209
  source_url: preHandlerContent.source_url
19
210
  };
20
211
  }
212
+ async function prepareRenderContext(inputContent, options, getInputContent) {
213
+ const { content, absoluteDirPath } = await getInputContent(inputContent, options.file);
214
+ const gzhContent = await renderWithTheme(content, options);
215
+ return { gzhContent, absoluteDirPath };
216
+ }
217
+ async function listThemes() {
218
+ const themes = getAllGzhThemes();
219
+ const themeList = themes.map((theme) => {
220
+ return {
221
+ id: theme.meta.id,
222
+ name: theme.meta.name,
223
+ description: theme.meta.description,
224
+ isBuiltin: true
225
+ };
226
+ });
227
+ const customThemes = await configStore.getThemes();
228
+ if (customThemes.length > 0) {
229
+ customThemes.forEach((theme) => {
230
+ themeList.push({
231
+ id: theme.id,
232
+ name: theme.id,
233
+ description: theme.description,
234
+ isBuiltin: false
235
+ });
236
+ });
237
+ }
238
+ return themeList;
239
+ }
240
+ async function addTheme(name, path2) {
241
+ if (!name || !path2) {
242
+ throw new Error("添加主题时必须提供名称(name)和路径(path)");
243
+ }
244
+ if (checkThemeExists(name) || await checkCustomThemeExists(name)) {
245
+ throw new Error(`主题 "${name}" 已存在`);
246
+ }
247
+ if (path2.startsWith("http")) {
248
+ const response = await fetch(path2);
249
+ if (!response.ok) {
250
+ throw new Error(`无法从远程获取主题: ${response.statusText}`);
251
+ }
252
+ const content = await response.text();
253
+ await configStore.addThemeToConfig(name, content);
254
+ } else {
255
+ const normalizePath = getNormalizeFilePath(path2);
256
+ const content = await readFileContent(normalizePath);
257
+ await configStore.addThemeToConfig(name, content);
258
+ }
259
+ }
260
+ async function removeTheme(name) {
261
+ if (checkThemeExists(name)) {
262
+ throw new Error(`默认主题 "${name}" 不能删除`);
263
+ }
264
+ if (!await checkCustomThemeExists(name)) {
265
+ throw new Error(`自定义主题 "${name}" 不存在`);
266
+ }
267
+ await configStore.deleteThemeFromConfig(name);
268
+ }
269
+ function checkThemeExists(themeId) {
270
+ const themes = getAllGzhThemes();
271
+ return themes.some((theme) => theme.meta.id === themeId);
272
+ }
273
+ async function checkCustomThemeExists(themeId) {
274
+ const customThemes = await configStore.getThemes();
275
+ return customThemes.some((theme) => theme.id === themeId);
276
+ }
21
277
  async function getGzhContent(content, themeId, hlThemeId, isMacStyle = true, isAddFootnote = true) {
22
278
  return await renderStyledContent(content, {
23
279
  themeId,
@@ -26,10 +282,64 @@ async function getGzhContent(content, themeId, hlThemeId, isMacStyle = true, isA
26
282
  isAddFootnote
27
283
  });
28
284
  }
285
+ async function renderAndPublish(inputContent, options, getInputContent) {
286
+ const { gzhContent, absoluteDirPath } = await prepareRenderContext(inputContent, options, getInputContent);
287
+ if (!gzhContent.title) throw new Error("未能找到文章标题");
288
+ const data = await publishToWechatDraft(
289
+ {
290
+ title: gzhContent.title,
291
+ content: gzhContent.content,
292
+ cover: gzhContent.cover,
293
+ author: gzhContent.author,
294
+ source_url: gzhContent.source_url
295
+ },
296
+ {
297
+ relativePath: absoluteDirPath
298
+ }
299
+ );
300
+ if (data.media_id) {
301
+ return data.media_id;
302
+ } else {
303
+ throw new Error(`发布到微信公众号失败,
304
+ ${data}`);
305
+ }
306
+ }
307
+ async function renderAndPublishToServer(inputContent, options, getInputContent) {
308
+ const serverUrl = getServerUrl(options);
309
+ const headers = getHeaders(options);
310
+ await healthCheck(serverUrl);
311
+ await verifyAuth(serverUrl, headers);
312
+ const { gzhContent, absoluteDirPath } = await prepareRenderContext(inputContent, options, getInputContent);
313
+ if (!gzhContent.title) throw new Error("未能找到文章标题");
314
+ gzhContent.content = await uploadLocalImages(gzhContent.content, serverUrl, headers, absoluteDirPath);
315
+ gzhContent.cover = await uploadCover(serverUrl, headers, gzhContent.cover, absoluteDirPath);
316
+ const mdFileId = await uploadStyledContent(gzhContent, serverUrl, headers);
317
+ return await requestServerPublish(mdFileId, serverUrl, headers, options);
318
+ }
29
319
  export {
30
- c as configDir,
31
- a as configPath,
32
- b as configStore,
320
+ addTheme,
321
+ b as configDir,
322
+ d as configPath,
323
+ configStore,
324
+ e as ensureDir,
33
325
  getGzhContent,
34
- renderStyledContent
326
+ getNormalizeFilePath,
327
+ i as isAbsolutePath,
328
+ listThemes,
329
+ m as md5FromBuffer,
330
+ f as md5FromFile,
331
+ n as normalizePath,
332
+ prepareRenderContext,
333
+ readBinaryFile,
334
+ readFileContent,
335
+ removeTheme,
336
+ renderAndPublish,
337
+ renderAndPublishToServer,
338
+ renderStyledContent,
339
+ renderWithTheme,
340
+ s as safeReadJson,
341
+ h as safeWriteJson,
342
+ t as tokenPath,
343
+ j as tokenStore,
344
+ u as uploadCacheStore
35
345
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenyan-md/core",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
4
4
  "description": "Core library for Wenyan markdown rendering & publishing",
5
5
  "author": "Lei <caol64@gmail.com> (https://github.com/caol64)",
6
6
  "license": "Apache-2.0",
@@ -43,7 +43,6 @@
43
43
  "types": "./dist/types/wechat.d.ts"
44
44
  },
45
45
  "./http": {
46
- "import": "./dist/http.js",
47
46
  "types": "./dist/types/http.d.ts"
48
47
  }
49
48
  },
@@ -80,17 +79,12 @@
80
79
  }
81
80
  },
82
81
  "scripts": {
83
- "dev": "vite build --watch",
84
82
  "check": "tsc --noEmit",
85
83
  "build": "vite build && tsc",
86
- "build:browser": "vite build --config vite.config.browser.ts",
87
- "build:styles": "vite build --config vite.config.styles.ts",
88
- "build:math": "vite build --config vite.config.math.ts",
89
- "build:all": "pnpm build && pnpm build:styles && pnpm build:math && pnpm build:browser",
90
84
  "test": "pnpm build && vitest",
91
- "test:wrapper": "vitest run test/wrapper.test.ts",
92
- "test:publish": "vitest run test/publish.test.ts",
93
- "test:realPublish": "vitest run test/realPublish.test.ts",
94
- "test:runtimeEnv": "vitest run test/runtimeEnv.test.ts"
85
+ "test:wrapper": "vitest run tests/wrapper.test.ts",
86
+ "test:publish": "vitest run tests/publish.test.ts",
87
+ "test:realPublish": "vitest run tests/realPublish.test.ts",
88
+ "test:runtimeEnv": "vitest run tests/runtimeEnv.test.ts"
95
89
  }
96
90
  }
@@ -1,111 +0,0 @@
1
- import path from "node:path";
2
- import os from "node:os";
3
- import fs from "node:fs";
4
- import crypto from "node:crypto";
5
- function safeReadJson(file, fallback) {
6
- try {
7
- return JSON.parse(fs.readFileSync(file, "utf-8"));
8
- } catch {
9
- return fallback;
10
- }
11
- }
12
- function safeWriteJson(file, data) {
13
- const tmp = file + ".tmp";
14
- fs.writeFileSync(tmp, JSON.stringify(data ?? {}, null, 2), "utf-8");
15
- fs.renameSync(tmp, file);
16
- }
17
- function ensureDir(dir) {
18
- if (!fs.existsSync(dir)) {
19
- fs.mkdirSync(dir, { recursive: true });
20
- }
21
- }
22
- function md5FromBuffer(buf) {
23
- return crypto.createHash("md5").update(buf).digest("hex");
24
- }
25
- function md5FromFile(filePath) {
26
- const buf = fs.readFileSync(filePath);
27
- return md5FromBuffer(buf);
28
- }
29
- const defaultConfig = {};
30
- const configDir = process.env.APPDATA ? path.join(process.env.APPDATA, "wenyan-md") : path.join(os.homedir(), ".config", "wenyan-md");
31
- const configPath = path.join(configDir, "config.json");
32
- class ConfigStore {
33
- config = { ...defaultConfig };
34
- constructor() {
35
- this.load();
36
- }
37
- load() {
38
- ensureDir(configDir);
39
- if (fs.existsSync(configPath)) {
40
- this.config = {
41
- ...defaultConfig,
42
- ...safeReadJson(configPath, defaultConfig)
43
- };
44
- }
45
- }
46
- save() {
47
- try {
48
- ensureDir(configDir);
49
- safeWriteJson(configPath, this.config);
50
- } catch (error) {
51
- console.error("❌ 无法保存配置文件:", error);
52
- }
53
- }
54
- getConfig() {
55
- return this.config;
56
- }
57
- getThemes() {
58
- return Object.values(this.config.themes ?? {});
59
- }
60
- getThemeById(themeId) {
61
- const themeOption = this.config.themes?.[themeId];
62
- if (!themeOption) return;
63
- const absoluteFilePath = path.join(configDir, themeOption.path);
64
- try {
65
- return fs.readFileSync(absoluteFilePath, "utf-8");
66
- } catch {
67
- return void 0;
68
- }
69
- }
70
- addThemeToConfig(name, content) {
71
- const savedPath = this.addThemeFile(name, content);
72
- this.config.themes ??= {};
73
- this.config.themes[name] = {
74
- id: name,
75
- name,
76
- path: savedPath
77
- };
78
- this.save();
79
- }
80
- addThemeFile(themeId, themeContent) {
81
- const filePath = `themes/${themeId}.css`;
82
- const absoluteFilePath = path.join(configDir, filePath);
83
- ensureDir(path.dirname(absoluteFilePath));
84
- fs.writeFileSync(absoluteFilePath, themeContent, "utf-8");
85
- return filePath;
86
- }
87
- deleteThemeFromConfig(themeId) {
88
- const theme = this.config.themes?.[themeId];
89
- if (!theme) return;
90
- this.deleteThemeFile(theme.path);
91
- delete this.config.themes[themeId];
92
- this.save();
93
- }
94
- deleteThemeFile(filePath) {
95
- try {
96
- fs.unlinkSync(path.join(configDir, filePath));
97
- } catch {
98
- }
99
- }
100
- }
101
- const configStore = new ConfigStore();
102
- export {
103
- configPath as a,
104
- configStore as b,
105
- configDir as c,
106
- safeWriteJson as d,
107
- ensureDir as e,
108
- md5FromFile as f,
109
- md5FromBuffer as m,
110
- safeReadJson as s
111
- };
package/dist/http.js DELETED
@@ -1 +0,0 @@
1
-