@wenyan-md/core 3.0.3 → 3.0.4

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/core.js CHANGED
@@ -1087,7 +1087,7 @@ function transformUl(ulElement) {
1087
1087
  function tableToAsciiArt(table) {
1088
1088
  const rowsElements = Array.from(table.querySelectorAll("tr"));
1089
1089
  const rows = rowsElements.map(
1090
- (tr) => Array.from(tr.querySelectorAll("th, td")).map((td) => td.innerText.trim())
1090
+ (tr) => Array.from(tr.querySelectorAll("th, td")).map((td) => (td?.textContent || "").trim())
1091
1091
  );
1092
1092
  if (rows.length === 0) return "";
1093
1093
  const columnWidths = rows[0].map(
package/dist/publish.js CHANGED
@@ -107,6 +107,57 @@ class UploadCacheStore {
107
107
  return this.adapter.calcHash(buffer);
108
108
  }
109
109
  }
110
+ const defaultCredential = {};
111
+ class CredentialStore {
112
+ credential = { ...defaultCredential };
113
+ adapter;
114
+ initPromise;
115
+ constructor(adapter) {
116
+ this.adapter = adapter;
117
+ this.initPromise = this.load();
118
+ }
119
+ async load() {
120
+ try {
121
+ const loadedData = await this.adapter.loadCredential();
122
+ if (loadedData) {
123
+ this.credential = loadedData;
124
+ }
125
+ } catch (error) {
126
+ throw new Error(`无法加载凭据: ${error instanceof Error ? error.message : String(error)}`);
127
+ }
128
+ }
129
+ async save() {
130
+ try {
131
+ await this.adapter.saveCredential(this.credential);
132
+ } catch (error) {
133
+ throw new Error(`无法保存凭据: ${error instanceof Error ? error.message : String(error)}`);
134
+ }
135
+ }
136
+ async _getWechatCredential() {
137
+ await this.initPromise;
138
+ return this.credential.wechat ?? {};
139
+ }
140
+ async getWechatCredential(appId) {
141
+ const wechat = await this._getWechatCredential();
142
+ if (!wechat) return null;
143
+ const appSecret = wechat[appId];
144
+ if (!appSecret) return null;
145
+ return { appId, appSecret };
146
+ }
147
+ async saveWechatCredential(appId, appSecret) {
148
+ await this.initPromise;
149
+ this.credential.wechat ??= {};
150
+ this.credential.wechat[appId] = appSecret;
151
+ await this.save();
152
+ }
153
+ async deleteWechatCredential(appId) {
154
+ await this.initPromise;
155
+ if (this.credential.wechat) {
156
+ delete this.credential.wechat[appId];
157
+ await this.save();
158
+ }
159
+ }
160
+ }
110
161
  class WechatPublisher {
111
162
  tokenStore;
112
163
  uploadCacheStore;
@@ -166,8 +217,10 @@ class WechatPublisher {
166
217
  }
167
218
  }
168
219
  export {
220
+ CredentialStore,
169
221
  TokenStore,
170
222
  UploadCacheStore,
171
223
  WechatPublisher,
224
+ defaultCredential,
172
225
  defaultTokenCache
173
226
  };
@@ -0,0 +1,24 @@
1
+ export interface WenyanCredential {
2
+ wechat?: Record<string, string>;
3
+ }
4
+ export declare const defaultCredential: WenyanCredential;
5
+ export interface CredentialStorageAdapter {
6
+ loadCredential(): Promise<WenyanCredential | null>;
7
+ saveCredential(credential: WenyanCredential): Promise<void>;
8
+ clearCredential(): Promise<void>;
9
+ }
10
+ export declare class CredentialStore {
11
+ private credential;
12
+ private adapter;
13
+ private initPromise;
14
+ constructor(adapter: CredentialStorageAdapter);
15
+ private load;
16
+ private save;
17
+ private _getWechatCredential;
18
+ getWechatCredential(appId: string): Promise<{
19
+ appId: string;
20
+ appSecret: string;
21
+ } | null>;
22
+ saveWechatCredential(appId: string, appSecret: string): Promise<void>;
23
+ deleteWechatCredential(appId: string): Promise<void>;
24
+ }
@@ -7,7 +7,6 @@ export interface ThemeConfigOptions {
7
7
  description?: string;
8
8
  path: string;
9
9
  }
10
- export declare const configDir: string;
11
10
  export declare const configPath: string;
12
11
  declare class ConfigStore {
13
12
  private config;
@@ -0,0 +1,7 @@
1
+ import { CredentialStorageAdapter, WenyanCredential } from "../credentialStore.js";
2
+ export declare const credentialPath: string;
3
+ export declare class NodeCredentialStorageAdapter implements CredentialStorageAdapter {
4
+ loadCredential(): Promise<WenyanCredential | null>;
5
+ saveCredential(credential: WenyanCredential): Promise<void>;
6
+ clearCredential(): Promise<void>;
7
+ }
@@ -1,6 +1,8 @@
1
1
  import { WechatPublishResponse } from "../wechat.js";
2
2
  import { ArticleOptions, WechatPublisher } from "../publish.js";
3
+ import { CredentialStore } from "../credentialStore.js";
3
4
  export declare const wechatPublisher: WechatPublisher;
5
+ export declare const credentialStore: CredentialStore;
4
6
  interface PublishOptions {
5
7
  appId?: string;
6
8
  appSecret?: string;
@@ -7,8 +7,9 @@ export interface RenderOptions {
7
7
  footnote: boolean;
8
8
  }
9
9
  export interface PublishOptions extends RenderOptions {
10
+ appId?: string;
10
11
  }
11
- export interface ClientPublishOptions extends RenderOptions {
12
+ export interface ClientPublishOptions extends PublishOptions {
12
13
  server?: string;
13
14
  apiKey?: string;
14
15
  clientVersion?: string;
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ export declare const configDir: string;
2
3
  export declare function readFileContent(filePath: string): Promise<string>;
3
4
  export declare function readBinaryFile(filePath: string): Promise<Buffer>;
4
5
  export declare function safeReadJson<T>(file: string, fallback: T): Promise<T>;
@@ -23,3 +23,4 @@ export declare class WechatPublisher {
23
23
  }
24
24
  export * from "./tokenStore.js";
25
25
  export * from "./uploadCacheStore.js";
26
+ export * from "./credentialStore.js";
package/dist/wrapper.js CHANGED
@@ -4,13 +4,22 @@ import https from "node:https";
4
4
  import { JSDOM } from "jsdom";
5
5
  import fs, { stat } from "node:fs/promises";
6
6
  import crypto from "node:crypto";
7
+ import os from "node:os";
7
8
  import { fileFromPath } from "formdata-node/file-from-path";
8
9
  import { FormDataEncoder } from "form-data-encoder";
9
10
  import { FormData } from "formdata-node";
10
11
  import { Readable } from "node:stream";
11
- import os from "node:os";
12
- import { defaultTokenCache, WechatPublisher } from "./publish.js";
12
+ import { defaultTokenCache, defaultCredential, WechatPublisher, CredentialStore } from "./publish.js";
13
13
  import { createWenyanCore, getAllGzhThemes } from "./core.js";
14
+ const configDir = (() => {
15
+ if (process.env.XDG_CONFIG_HOME) {
16
+ return path.join(process.env.XDG_CONFIG_HOME, "wenyan-md");
17
+ }
18
+ if (process.env.APPDATA) {
19
+ return path.join(process.env.APPDATA, "wenyan-md");
20
+ }
21
+ return path.join(os.homedir(), ".config", "wenyan-md");
22
+ })();
14
23
  async function readFileContent(filePath) {
15
24
  return await fs.readFile(filePath, "utf-8");
16
25
  }
@@ -233,7 +242,7 @@ async function uploadStyledContent(gzhContent, serverUrl, headers) {
233
242
  return mdFileId;
234
243
  }
235
244
  async function requestServerPublish(mdFileId, serverUrl, headers, options) {
236
- const { theme, customTheme, highlight, macStyle, footnote } = options;
245
+ const { theme, customTheme, highlight, macStyle, footnote, appId } = options;
237
246
  const publishRes = await fetch(`${serverUrl}/publish`, {
238
247
  method: "POST",
239
248
  headers: {
@@ -246,7 +255,8 @@ async function requestServerPublish(mdFileId, serverUrl, headers, options) {
246
255
  highlight,
247
256
  customTheme,
248
257
  macStyle,
249
- footnote
258
+ footnote,
259
+ appId
250
260
  })
251
261
  });
252
262
  const publishData = await publishRes.json();
@@ -330,80 +340,6 @@ const nodeHttpAdapter = {
330
340
  };
331
341
  }
332
342
  };
333
- const defaultConfig = {};
334
- const configDir = process.env.APPDATA ? path.join(process.env.APPDATA, "wenyan-md") : path.join(os.homedir(), ".config", "wenyan-md");
335
- const configPath = path.join(configDir, "config.json");
336
- class ConfigStore {
337
- config = { ...defaultConfig };
338
- initPromise;
339
- constructor() {
340
- this.initPromise = this.load();
341
- }
342
- async load() {
343
- await ensureDir(configDir);
344
- this.config = await safeReadJson(configPath, defaultConfig);
345
- }
346
- async save() {
347
- try {
348
- await ensureDir(configDir);
349
- await safeWriteJson(configPath, this.config);
350
- } catch (error) {
351
- throw new Error(`无法保存配置文件: ${error instanceof Error ? error.message : String(error)}`);
352
- }
353
- }
354
- async getConfig() {
355
- await this.initPromise;
356
- return this.config;
357
- }
358
- async getThemes() {
359
- await this.initPromise;
360
- return Object.values(this.config.themes ?? {});
361
- }
362
- async getThemeById(themeId) {
363
- await this.initPromise;
364
- const themeOption = this.config.themes?.[themeId];
365
- if (!themeOption) return void 0;
366
- const absoluteFilePath = path.join(configDir, themeOption.path);
367
- try {
368
- return await fs.readFile(absoluteFilePath, "utf-8");
369
- } catch {
370
- return void 0;
371
- }
372
- }
373
- async addThemeToConfig(name, content) {
374
- await this.initPromise;
375
- const savedPath = await this.addThemeFile(name, content);
376
- this.config.themes ??= {};
377
- this.config.themes[name] = {
378
- id: name,
379
- name,
380
- path: savedPath
381
- };
382
- await this.save();
383
- }
384
- async addThemeFile(themeId, themeContent) {
385
- const filePath = `themes/${themeId}.css`;
386
- const absoluteFilePath = path.join(configDir, filePath);
387
- await ensureDir(path.dirname(absoluteFilePath));
388
- await fs.writeFile(absoluteFilePath, themeContent, "utf-8");
389
- return filePath;
390
- }
391
- async deleteThemeFromConfig(themeId) {
392
- await this.initPromise;
393
- const theme = this.config.themes?.[themeId];
394
- if (!theme) return;
395
- await this.deleteThemeFile(theme.path);
396
- delete this.config.themes[themeId];
397
- await this.save();
398
- }
399
- async deleteThemeFile(filePath) {
400
- try {
401
- await fs.unlink(path.join(configDir, filePath));
402
- } catch {
403
- }
404
- }
405
- }
406
- const configStore = new ConfigStore();
407
343
  const tokenPath = path.join(configDir, "token.json");
408
344
  class NodeTokenStorageAdapter {
409
345
  async loadToken() {
@@ -447,8 +383,27 @@ class NodeUploadCacheAdapter {
447
383
  return crypto.createHash("md5").update(Buffer.from(buffer)).digest("hex");
448
384
  }
449
385
  }
386
+ const credentialPath = path.join(configDir, "credential.json");
387
+ class NodeCredentialStorageAdapter {
388
+ async loadCredential() {
389
+ await ensureDir(configDir);
390
+ return await safeReadJson(credentialPath, defaultCredential);
391
+ }
392
+ async saveCredential(credential) {
393
+ await ensureDir(configDir);
394
+ await safeWriteJson(credentialPath, credential);
395
+ }
396
+ async clearCredential() {
397
+ throw new Error("Method not implemented.");
398
+ }
399
+ }
450
400
  const mediaIdMapping = /* @__PURE__ */ new Map();
451
- const wechatPublisher = new WechatPublisher(nodeHttpAdapter, new NodeTokenStorageAdapter(), new NodeUploadCacheAdapter());
401
+ const wechatPublisher = new WechatPublisher(
402
+ nodeHttpAdapter,
403
+ new NodeTokenStorageAdapter(),
404
+ new NodeUploadCacheAdapter()
405
+ );
406
+ const credentialStore = new CredentialStore(new NodeCredentialStorageAdapter());
452
407
  async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
453
408
  let fileData;
454
409
  let finalName;
@@ -510,11 +465,7 @@ async function uploadImages(content, accessToken, relativePath) {
510
465
  async function publishToWechatDraft(articleOptions, publishOptions = {}) {
511
466
  const { title, content, cover, author, source_url } = articleOptions;
512
467
  const { appId, appSecret, relativePath } = publishOptions;
513
- const appIdFinal = appId ?? process.env.WECHAT_APP_ID;
514
- const appSecretFinal = appSecret ?? process.env.WECHAT_APP_SECRET;
515
- if (!appIdFinal || !appSecretFinal) {
516
- throw new Error("请通过参数或环境变量 WECHAT_APP_ID / WECHAT_APP_SECRET 提供公众号凭据");
517
- }
468
+ const { appId: appIdFinal, appSecret: appSecretFinal } = await getAppIdAndSecret(appId, appSecret);
518
469
  const accessToken = await wechatPublisher.getAccessTokenWithCache(appIdFinal, appSecretFinal);
519
470
  const { html, firstImageId } = await uploadImages(content, accessToken, relativePath);
520
471
  let thumbMediaId;
@@ -557,6 +508,97 @@ async function publishToWechatDraft(articleOptions, publishOptions = {}) {
557
508
  async function publishToDraft(title, content, cover = "", options = {}) {
558
509
  return publishToWechatDraft({ title, content, cover }, options);
559
510
  }
511
+ async function getAppIdAndSecret(appId, appSecret) {
512
+ if (appId && appSecret) {
513
+ return { appId, appSecret };
514
+ }
515
+ const envAppId = process.env.WECHAT_APP_ID;
516
+ const envAppSecret = process.env.WECHAT_APP_SECRET;
517
+ if (envAppId && envAppSecret && (envAppId === appId || !appId)) {
518
+ return { appId: envAppId, appSecret: envAppSecret };
519
+ }
520
+ if (!appId) {
521
+ throw new Error("未提供 AppID:请通过参数、环境变量或配置文件指定。");
522
+ }
523
+ const credential = await credentialStore.getWechatCredential(appId);
524
+ if (credential?.appId && credential?.appSecret) {
525
+ return { appId: credential.appId, appSecret: credential.appSecret };
526
+ }
527
+ throw new Error(`未能找到 AppID 为 "${appId}" 的公众号凭据,请检查配置文件。`);
528
+ }
529
+ const defaultConfig = {};
530
+ const configPath = path.join(configDir, "config.json");
531
+ class ConfigStore {
532
+ config = { ...defaultConfig };
533
+ initPromise;
534
+ constructor() {
535
+ this.initPromise = this.load();
536
+ }
537
+ async load() {
538
+ await ensureDir(configDir);
539
+ this.config = await safeReadJson(configPath, defaultConfig);
540
+ }
541
+ async save() {
542
+ try {
543
+ await ensureDir(configDir);
544
+ await safeWriteJson(configPath, this.config);
545
+ } catch (error) {
546
+ throw new Error(`无法保存配置文件: ${error instanceof Error ? error.message : String(error)}`);
547
+ }
548
+ }
549
+ async getConfig() {
550
+ await this.initPromise;
551
+ return this.config;
552
+ }
553
+ async getThemes() {
554
+ await this.initPromise;
555
+ return Object.values(this.config.themes ?? {});
556
+ }
557
+ async getThemeById(themeId) {
558
+ await this.initPromise;
559
+ const themeOption = this.config.themes?.[themeId];
560
+ if (!themeOption) return void 0;
561
+ const absoluteFilePath = path.join(configDir, themeOption.path);
562
+ try {
563
+ return await fs.readFile(absoluteFilePath, "utf-8");
564
+ } catch {
565
+ return void 0;
566
+ }
567
+ }
568
+ async addThemeToConfig(name, content) {
569
+ await this.initPromise;
570
+ const savedPath = await this.addThemeFile(name, content);
571
+ this.config.themes ??= {};
572
+ this.config.themes[name] = {
573
+ id: name,
574
+ name,
575
+ path: savedPath
576
+ };
577
+ await this.save();
578
+ }
579
+ async addThemeFile(themeId, themeContent) {
580
+ const filePath = `themes/${themeId}.css`;
581
+ const absoluteFilePath = path.join(configDir, filePath);
582
+ await ensureDir(path.dirname(absoluteFilePath));
583
+ await fs.writeFile(absoluteFilePath, themeContent, "utf-8");
584
+ return filePath;
585
+ }
586
+ async deleteThemeFromConfig(themeId) {
587
+ await this.initPromise;
588
+ const theme = this.config.themes?.[themeId];
589
+ if (!theme) return;
590
+ await this.deleteThemeFile(theme.path);
591
+ delete this.config.themes[themeId];
592
+ await this.save();
593
+ }
594
+ async deleteThemeFile(filePath) {
595
+ try {
596
+ await fs.unlink(path.join(configDir, filePath));
597
+ } catch {
598
+ }
599
+ }
600
+ }
601
+ const configStore = new ConfigStore();
560
602
  const wenyanCoreInstance = await createWenyanCore();
561
603
  async function renderWithTheme(markdownContent, options) {
562
604
  if (!markdownContent) {
@@ -683,6 +725,7 @@ async function renderAndPublish(inputContent, options, getInputContent) {
683
725
  source_url: gzhContent.source_url
684
726
  },
685
727
  {
728
+ appId: options.appId,
686
729
  relativePath: absoluteDirPath
687
730
  }
688
731
  );
@@ -710,6 +753,7 @@ export {
710
753
  configDir,
711
754
  configPath,
712
755
  configStore,
756
+ credentialStore,
713
757
  ensureDir,
714
758
  getGzhContent,
715
759
  getNormalizeFilePath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenyan-md/core",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
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",
@@ -60,7 +60,7 @@
60
60
  "vitest": "^3.2.4"
61
61
  },
62
62
  "dependencies": {
63
- "css-tree": "^3.1.0",
63
+ "css-tree": "^3.2.1",
64
64
  "front-matter": "^4.0.2",
65
65
  "highlight.js": "11.10.0",
66
66
  "marked": "^15.0.12",
@@ -83,6 +83,9 @@
83
83
  "optional": true
84
84
  }
85
85
  },
86
+ "publishConfig": {
87
+ "registry": "https://registry.npmjs.org/"
88
+ },
86
89
  "scripts": {
87
90
  "typecheck": "tsc --noEmit",
88
91
  "build": "vite build && tsc",