@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 +8 -8
- package/dist/publish.js +68 -5
- package/dist/types/credentialStore.d.ts +24 -0
- package/dist/types/node/configStore.d.ts +0 -1
- package/dist/types/node/credentialStoreNodeAdapter.d.ts +7 -0
- package/dist/types/node/publish.d.ts +4 -1
- package/dist/types/node/types.d.ts +2 -1
- package/dist/types/node/utils.d.ts +1 -0
- package/dist/types/publish.d.ts +2 -0
- package/dist/types/uploadCacheStore.d.ts +1 -0
- package/dist/wrapper.js +208 -115
- package/package.json +13 -8
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
|
|
33
|
-
const varPattern = /var\(--([a-zA-Z0-9
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
77
|
-
|
|
77
|
+
save() {
|
|
78
|
+
this._saveQueue = this._saveQueue.then(async () => {
|
|
78
79
|
await this.adapter.saveCache(this.cache);
|
|
79
|
-
}
|
|
80
|
-
|
|
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
|
+
}
|
|
@@ -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
|
|
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>;
|
package/dist/types/publish.d.ts
CHANGED
|
@@ -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";
|
package/dist/wrapper.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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 {
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
new
|
|
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
|
|
228
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
"
|
|
90
|
+
"typecheck": "tsc --noEmit",
|
|
84
91
|
"build": "vite build && tsc",
|
|
85
|
-
"
|
|
86
|
-
"test
|
|
87
|
-
"test:
|
|
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
|
}
|