@wenyan-md/core 3.0.2 → 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
@@ -29,8 +29,8 @@ function stringToMap(str) {
29
29
  return map;
30
30
  }
31
31
  function replaceCSSVariables(css) {
32
- const variablePattern = /--([a-zA-Z0-9\-]+):\s*([^;()]*\((?:[^()]*|\([^()]*\))*\)[^;()]*|[^;]+);/g;
33
- const varPattern = /var\(--([a-zA-Z0-9\-]+)\)/g;
32
+ const variablePattern = /--([a-zA-Z0-9-]+):\s*([^;()]*\((?:[^()]*|\([^()]*\))*\)[^;()]*|[^;]+);/g;
33
+ const varPattern = /var\(--([a-zA-Z0-9-]+)\)/g;
34
34
  const cssVariables = {};
35
35
  let match;
36
36
  while ((match = variablePattern.exec(css)) !== null) {
@@ -48,7 +48,7 @@ function replaceCSSVariables(css) {
48
48
  if (resolved.has(value)) return value;
49
49
  resolved.add(value);
50
50
  let resolvedValue = value;
51
- const innerVarPattern = /var\(--([a-zA-Z0-9\-]+)\)/g;
51
+ const innerVarPattern = /var\(--([a-zA-Z0-9-]+)\)/g;
52
52
  resolvedValue = resolvedValue.replace(innerVarPattern, (match2, varName) => {
53
53
  if (variables[varName]) {
54
54
  return resolveVariable(variables[varName], variables, resolved);
@@ -61,7 +61,7 @@ function replaceCSSVariables(css) {
61
61
  const resolvedValue = resolveVariable(cssVariables[key], cssVariables);
62
62
  cssVariables[key] = resolvedValue;
63
63
  }
64
- let modifiedCSS = css.replace(varPattern, (match2, varName) => {
64
+ const modifiedCSS = css.replace(varPattern, (match2, varName) => {
65
65
  if (cssVariables[varName]) {
66
66
  return cssVariables[varName];
67
67
  }
@@ -285,7 +285,7 @@ function createCssApplier(css) {
285
285
  targets.forEach((el) => {
286
286
  declarations.forEach((decl) => {
287
287
  if (decl.type !== "Declaration") return;
288
- let value = csstree.generate(decl.value);
288
+ const value = csstree.generate(decl.value);
289
289
  const property = decl.property;
290
290
  const priority = decl.important ? "important" : "";
291
291
  el.style.setProperty(property, value, priority);
@@ -347,8 +347,8 @@ function buildPseudoElement(originalResults, document) {
347
347
  for (const [k, v] of beforeResults) {
348
348
  if (v.includes("url(")) {
349
349
  const svgMatch = v.match(/data:image\/svg\+xml;utf8,(.*<\/svg>)/);
350
- const base64SvgMatch = v.match(/data:image\/svg\+xml;base64,([^"'\)]*)["']?\)/);
351
- const httpMatch = v.match(/(?:"|')?(https?[^"'\)]*)(?:"|')?\)/);
350
+ const base64SvgMatch = v.match(/data:image\/svg\+xml;base64,([^"')]*)["']?\)/);
351
+ const httpMatch = v.match(/(?:"|')?(https?[^"')]*)(?:"|')?\)/);
352
352
  if (svgMatch) {
353
353
  const svgCode = decodeURIComponent(svgMatch[1]);
354
354
  section.innerHTML = svgCode;
@@ -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
@@ -62,6 +62,7 @@ class UploadCacheStore {
62
62
  cache = {};
63
63
  adapter;
64
64
  initPromise;
65
+ _saveQueue = Promise.resolve();
65
66
  constructor(adapter) {
66
67
  this.adapter = adapter;
67
68
  this.initPromise = this.load();
@@ -73,12 +74,13 @@ class UploadCacheStore {
73
74
  throw new Error(`无法加载上传缓存: ${error instanceof Error ? error.message : String(error)}`);
74
75
  }
75
76
  }
76
- async save() {
77
- try {
77
+ save() {
78
+ this._saveQueue = this._saveQueue.then(async () => {
78
79
  await this.adapter.saveCache(this.cache);
79
- } catch (error) {
80
- throw new Error(`无法保存上传缓存: ${error instanceof Error ? error.message : String(error)}`);
81
- }
80
+ }).catch((err) => {
81
+ console.error(err);
82
+ });
83
+ return this._saveQueue;
82
84
  }
83
85
  async waitForInit() {
84
86
  await this.initPromise;
@@ -105,6 +107,57 @@ class UploadCacheStore {
105
107
  return this.adapter.calcHash(buffer);
106
108
  }
107
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
+ }
108
161
  class WechatPublisher {
109
162
  tokenStore;
110
163
  uploadCacheStore;
@@ -154,10 +207,20 @@ class WechatPublisher {
154
207
  async publishToDraft(accessToken, options) {
155
208
  return await this.publishArticle(accessToken, options);
156
209
  }
210
+ async clearCache() {
211
+ if (this.tokenStore) {
212
+ await this.tokenStore.clear();
213
+ }
214
+ if (this.uploadCacheStore) {
215
+ await this.uploadCacheStore.clear();
216
+ }
217
+ }
157
218
  }
158
219
  export {
220
+ CredentialStore,
159
221
  TokenStore,
160
222
  UploadCacheStore,
161
223
  WechatPublisher,
224
+ defaultCredential,
162
225
  defaultTokenCache
163
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,5 +1,8 @@
1
1
  import { WechatPublishResponse } from "../wechat.js";
2
- import { ArticleOptions } from "../publish.js";
2
+ import { ArticleOptions, WechatPublisher } from "../publish.js";
3
+ import { CredentialStore } from "../credentialStore.js";
4
+ export declare const wechatPublisher: WechatPublisher;
5
+ export declare const credentialStore: CredentialStore;
3
6
  interface PublishOptions {
4
7
  appId?: string;
5
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>;
@@ -19,6 +19,8 @@ export declare class WechatPublisher {
19
19
  getAccessTokenWithCache(appId: string, appSecret: string): Promise<string>;
20
20
  uploadImage(file: Blob, filename: string, accessToken: string): Promise<WechatUploadResponse>;
21
21
  publishToDraft(accessToken: string, options: WechatPublishOptions): Promise<WechatPublishResponse>;
22
+ clearCache(): Promise<void>;
22
23
  }
23
24
  export * from "./tokenStore.js";
24
25
  export * from "./uploadCacheStore.js";
26
+ export * from "./credentialStore.js";
@@ -16,6 +16,7 @@ export declare class UploadCacheStore {
16
16
  private cache;
17
17
  private adapter;
18
18
  private initPromise;
19
+ private _saveQueue;
19
20
  constructor(adapter: UploadCacheStorageAdapter);
20
21
  private load;
21
22
  private save;
package/dist/wrapper.js CHANGED
@@ -1,14 +1,25 @@
1
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";
2
+ import http from "node:http";
3
+ import https from "node:https";
5
4
  import { JSDOM } from "jsdom";
6
5
  import fs, { stat } from "node:fs/promises";
7
6
  import crypto from "node:crypto";
8
- import { fileFromPath } from "formdata-node/file-from-path";
9
7
  import os from "node:os";
10
- import { defaultTokenCache, WechatPublisher } from "./publish.js";
8
+ import { fileFromPath } from "formdata-node/file-from-path";
9
+ import { FormDataEncoder } from "form-data-encoder";
10
+ import { FormData } from "formdata-node";
11
+ import { Readable } from "node:stream";
12
+ import { defaultTokenCache, defaultCredential, WechatPublisher, CredentialStore } from "./publish.js";
11
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
+ })();
12
23
  async function readFileContent(filePath) {
13
24
  return await fs.readFile(filePath, "utf-8");
14
25
  }
@@ -107,6 +118,72 @@ const RuntimeEnv = {
107
118
  return normalizedInput;
108
119
  }
109
120
  };
121
+ async function chunkedUpload(serverUrl, headers, fileBuffer, filename, mimeType) {
122
+ const url = new URL(`${serverUrl}/upload`);
123
+ const boundary = "----FormBoundary" + Math.random().toString(36).substring(2);
124
+ const headerPart = `--${boundary}\r
125
+ Content-Disposition: form-data; name="file"; filename="${filename}"\r
126
+ Content-Type: ${mimeType}\r
127
+ \r
128
+ `;
129
+ const footerPart = `\r
130
+ --${boundary}--\r
131
+ `;
132
+ const headerBuf = Buffer.from(headerPart, "utf-8");
133
+ const footerBuf = Buffer.from(footerPart, "utf-8");
134
+ return new Promise((resolve, reject) => {
135
+ const requestModule = url.protocol === "https:" ? https : http;
136
+ const defaultPort = url.protocol === "https:" ? 443 : 80;
137
+ const req = requestModule.request(
138
+ {
139
+ hostname: url.hostname,
140
+ port: url.port || defaultPort,
141
+ path: url.pathname,
142
+ method: "POST",
143
+ headers: {
144
+ "Content-Type": `multipart/form-data; boundary=${boundary}`,
145
+ "Transfer-Encoding": "chunked",
146
+ ...headers
147
+ }
148
+ },
149
+ (res) => {
150
+ let body = "";
151
+ res.on("data", (chunk) => body += chunk);
152
+ res.on("end", () => {
153
+ if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
154
+ reject(new Error(`Server returned status ${res.statusCode}: ${body}`));
155
+ return;
156
+ }
157
+ try {
158
+ resolve(JSON.parse(body));
159
+ } catch (_e) {
160
+ reject(new Error(`Invalid server response: ${body}`));
161
+ }
162
+ });
163
+ }
164
+ );
165
+ req.on("error", reject);
166
+ req.write(headerBuf);
167
+ const CHUNK = 65536;
168
+ let offset = 0;
169
+ function writeNext() {
170
+ if (offset >= fileBuffer.length) {
171
+ req.write(footerBuf);
172
+ req.end();
173
+ return;
174
+ }
175
+ const end = Math.min(offset + CHUNK, fileBuffer.length);
176
+ if (req.write(fileBuffer.subarray(offset, end))) {
177
+ offset = end;
178
+ writeNext();
179
+ } else {
180
+ offset = end;
181
+ req.once("drain", writeNext);
182
+ }
183
+ }
184
+ writeNext();
185
+ });
186
+ }
110
187
  function getServerUrl(options) {
111
188
  let serverUrl = options.server || "http://localhost:3000";
112
189
  serverUrl = serverUrl.replace(/\/$/, "");
@@ -156,27 +233,16 @@ async function verifyAuth(serverUrl, headers) {
156
233
  }
157
234
  async function uploadStyledContent(gzhContent, serverUrl, headers) {
158
235
  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}`);
236
+ const fileBuffer = Buffer.from(JSON.stringify(gzhContent), "utf-8");
237
+ const mdUploadData = await chunkedUpload(serverUrl, headers, fileBuffer, mdFilename, "application/json");
238
+ if (!mdUploadData.success) {
239
+ throw new Error(`Upload Document Failed: ${mdUploadData.error || mdUploadData.desc}`);
174
240
  }
175
241
  const mdFileId = mdUploadData.data.fileId;
176
242
  return mdFileId;
177
243
  }
178
244
  async function requestServerPublish(mdFileId, serverUrl, headers, options) {
179
- const { theme, customTheme, highlight, macStyle, footnote } = options;
245
+ const { theme, customTheme, highlight, macStyle, footnote, appId } = options;
180
246
  const publishRes = await fetch(`${serverUrl}/publish`, {
181
247
  method: "POST",
182
248
  headers: {
@@ -189,7 +255,8 @@ async function requestServerPublish(mdFileId, serverUrl, headers, options) {
189
255
  highlight,
190
256
  customTheme,
191
257
  macStyle,
192
- footnote
258
+ footnote,
259
+ appId
193
260
  })
194
261
  });
195
262
  const publishData = await publishRes.json();
@@ -224,17 +291,8 @@ async function uploadLocalImage(originalUrl, serverUrl, headers, relativePath) {
224
291
  ".svg": "image/svg+xml"
225
292
  };
226
293
  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) {
294
+ const uploadData = await chunkedUpload(serverUrl, headers, fileBuffer, filename, type);
295
+ if (uploadData.success) {
238
296
  return `asset://${uploadData.data.fileId}`;
239
297
  } else {
240
298
  console.error(`[Client] Warning: Failed to upload ${filename}: ${uploadData.error || uploadData.desc}`);
@@ -282,80 +340,6 @@ const nodeHttpAdapter = {
282
340
  };
283
341
  }
284
342
  };
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
343
  const tokenPath = path.join(configDir, "token.json");
360
344
  class NodeTokenStorageAdapter {
361
345
  async loadToken() {
@@ -399,8 +383,27 @@ class NodeUploadCacheAdapter {
399
383
  return crypto.createHash("md5").update(Buffer.from(buffer)).digest("hex");
400
384
  }
401
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
+ }
402
400
  const mediaIdMapping = /* @__PURE__ */ new Map();
403
- 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());
404
407
  async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
405
408
  let fileData;
406
409
  let finalName;
@@ -462,14 +465,10 @@ async function uploadImages(content, accessToken, relativePath) {
462
465
  async function publishToWechatDraft(articleOptions, publishOptions = {}) {
463
466
  const { title, content, cover, author, source_url } = articleOptions;
464
467
  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
- }
468
+ const { appId: appIdFinal, appSecret: appSecretFinal } = await getAppIdAndSecret(appId, appSecret);
470
469
  const accessToken = await wechatPublisher.getAccessTokenWithCache(appIdFinal, appSecretFinal);
471
470
  const { html, firstImageId } = await uploadImages(content, accessToken, relativePath);
472
- let thumbMediaId = "";
471
+ let thumbMediaId;
473
472
  if (cover) {
474
473
  const cachedThumbMediaId = mediaIdMapping.get(cover);
475
474
  if (cachedThumbMediaId) {
@@ -509,6 +508,97 @@ async function publishToWechatDraft(articleOptions, publishOptions = {}) {
509
508
  async function publishToDraft(title, content, cover = "", options = {}) {
510
509
  return publishToWechatDraft({ title, content, cover }, options);
511
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();
512
602
  const wenyanCoreInstance = await createWenyanCore();
513
603
  async function renderWithTheme(markdownContent, options) {
514
604
  if (!markdownContent) {
@@ -635,6 +725,7 @@ async function renderAndPublish(inputContent, options, getInputContent) {
635
725
  source_url: gzhContent.source_url
636
726
  },
637
727
  {
728
+ appId: options.appId,
638
729
  relativePath: absoluteDirPath
639
730
  }
640
731
  );
@@ -662,6 +753,7 @@ export {
662
753
  configDir,
663
754
  configPath,
664
755
  configStore,
756
+ credentialStore,
665
757
  ensureDir,
666
758
  getGzhContent,
667
759
  getNormalizeFilePath,
@@ -681,5 +773,6 @@ export {
681
773
  renderStyledContent,
682
774
  renderWithTheme,
683
775
  safeReadJson,
684
- safeWriteJson
776
+ safeWriteJson,
777
+ wechatPublisher
685
778
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenyan-md/core",
3
- "version": "3.0.2",
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",
@@ -48,15 +48,19 @@
48
48
  }
49
49
  },
50
50
  "devDependencies": {
51
+ "@eslint/js": "^10.0.1",
51
52
  "@types/css-tree": "^2.3.11",
52
53
  "@types/jsdom": "^27.0.0",
53
54
  "@types/node": "^24.3.0",
55
+ "eslint": "^10.1.0",
56
+ "globals": "^17.4.0",
54
57
  "typescript": "^5.9.2",
58
+ "typescript-eslint": "^8.58.0",
55
59
  "vite": "^7.1.4",
56
60
  "vitest": "^3.2.4"
57
61
  },
58
62
  "dependencies": {
59
- "css-tree": "^3.1.0",
63
+ "css-tree": "^3.2.1",
60
64
  "front-matter": "^4.0.2",
61
65
  "highlight.js": "11.10.0",
62
66
  "marked": "^15.0.12",
@@ -79,13 +83,14 @@
79
83
  "optional": true
80
84
  }
81
85
  },
86
+ "publishConfig": {
87
+ "registry": "https://registry.npmjs.org/"
88
+ },
82
89
  "scripts": {
83
- "check": "tsc --noEmit",
90
+ "typecheck": "tsc --noEmit",
84
91
  "build": "vite build && tsc",
85
- "test": "pnpm build && vitest",
86
- "test:wrapper": "vitest run tests/wrapper.test.ts",
87
- "test:publish": "vitest run tests/publish.test.ts",
88
- "test:realPublish": "vitest run tests/realPublish.test.ts",
89
- "test:runtimeEnv": "vitest run tests/runtimeEnv.test.ts"
92
+ "lint": "eslint . --fix",
93
+ "test": "vitest run",
94
+ "test:watch": "vitest"
90
95
  }
91
96
  }