@wenyan-md/core 2.0.3 → 2.0.5
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/configStore-lZ5bhrcC.js +111 -0
- package/dist/core.js +7 -1
- package/dist/http.js +1 -0
- package/dist/publish.js +208 -30
- package/dist/types/core/parser/frontMatterParser.d.ts +3 -1
- package/dist/types/{wechat/http.d.ts → http.d.ts} +0 -4
- package/dist/types/node/configStore.d.ts +1 -2
- package/dist/types/node/publish.d.ts +14 -0
- package/dist/types/node/tokenStore.d.ts +18 -0
- package/dist/types/node/uploadCacheStore.d.ts +15 -0
- package/dist/types/node/utils.d.ts +6 -0
- package/dist/types/node/wrapper.d.ts +2 -1
- package/dist/types/wechat.d.ts +29 -0
- package/dist/wechat.js +16 -19
- package/dist/wrapper.js +7 -93
- package/package.json +8 -5
- package/dist/runtimeEnv-pU2mTDLR.js +0 -56
- package/dist/types/wechat/core.d.ts +0 -9
- package/dist/types/wechat/publish.d.ts +0 -6
- /package/dist/types/{wechat/adapters/browser.d.ts → browser/browserHttpAdapter.d.ts} +0 -0
- /package/dist/types/{wechat/adapters/node.d.ts → node/nodeHttpAdapter.d.ts} +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
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/core.js
CHANGED
|
@@ -196,7 +196,7 @@ async function handleFrontMatter(markdown) {
|
|
|
196
196
|
const { attributes, body } = fm(markdown);
|
|
197
197
|
const result = { body: body || "" };
|
|
198
198
|
let head = "";
|
|
199
|
-
const { title, description, cover } = attributes;
|
|
199
|
+
const { title, description, cover, author, source_url } = attributes;
|
|
200
200
|
if (title) {
|
|
201
201
|
result.title = title;
|
|
202
202
|
}
|
|
@@ -210,6 +210,12 @@ async function handleFrontMatter(markdown) {
|
|
|
210
210
|
if (head) {
|
|
211
211
|
result.body = head + result.body;
|
|
212
212
|
}
|
|
213
|
+
if (author) {
|
|
214
|
+
result.author = author;
|
|
215
|
+
}
|
|
216
|
+
if (source_url) {
|
|
217
|
+
result.source_url = source_url;
|
|
218
|
+
}
|
|
213
219
|
return result;
|
|
214
220
|
}
|
|
215
221
|
const parseOptions = {
|
package/dist/http.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/dist/publish.js
CHANGED
|
@@ -2,11 +2,64 @@ import { JSDOM } from "jsdom";
|
|
|
2
2
|
import { fileFromPath } from "formdata-node/file-from-path";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { stat } from "node:fs/promises";
|
|
5
|
-
import { R as RuntimeEnv } from "./runtimeEnv-pU2mTDLR.js";
|
|
6
5
|
import { createWechatClient } from "./wechat.js";
|
|
7
6
|
import { FormDataEncoder } from "form-data-encoder";
|
|
8
7
|
import { FormData } from "formdata-node";
|
|
9
8
|
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";
|
|
11
|
+
function normalizePath(p) {
|
|
12
|
+
return p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
13
|
+
}
|
|
14
|
+
function isAbsolutePath(path2) {
|
|
15
|
+
if (!path2) return false;
|
|
16
|
+
const winAbsPattern = /^[a-zA-Z]:\//;
|
|
17
|
+
const linuxAbsPattern = /^\//;
|
|
18
|
+
return winAbsPattern.test(path2) || linuxAbsPattern.test(path2);
|
|
19
|
+
}
|
|
20
|
+
const RuntimeEnv = {
|
|
21
|
+
isContainer: !!process.env.CONTAINERIZED,
|
|
22
|
+
hostFilePath: normalizePath(process.env.HOST_FILE_PATH || ""),
|
|
23
|
+
containerFilePath: normalizePath(process.env.CONTAINER_FILE_PATH || "/mnt/host-downloads"),
|
|
24
|
+
resolveLocalPath(inputPath, relativeBase) {
|
|
25
|
+
if (!this.isContainer) {
|
|
26
|
+
if (relativeBase) {
|
|
27
|
+
return path.resolve(relativeBase, inputPath);
|
|
28
|
+
} else {
|
|
29
|
+
if (!path.isAbsolute(inputPath)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Invalid input: '${inputPath}'. When relativeBase is not provided, inputPath must be an absolute path.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return path.normalize(inputPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
let normalizedInput = normalizePath(inputPath);
|
|
38
|
+
relativeBase = normalizePath(relativeBase || "");
|
|
39
|
+
if (relativeBase) {
|
|
40
|
+
if (!isAbsolutePath(normalizedInput)) {
|
|
41
|
+
normalizedInput = relativeBase + (normalizedInput.startsWith("/") ? "" : "/") + normalizedInput;
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
if (!isAbsolutePath(normalizedInput)) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Invalid input: '${inputPath}'. When relativeBase is not provided, inputPath must be an absolute path.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (normalizedInput.startsWith(this.hostFilePath)) {
|
|
51
|
+
let relativePart = normalizedInput.slice(this.hostFilePath.length);
|
|
52
|
+
if (relativePart && !relativePart.startsWith("/")) {
|
|
53
|
+
return normalizedInput;
|
|
54
|
+
}
|
|
55
|
+
if (!relativePart.startsWith("/")) {
|
|
56
|
+
relativePart = "/" + relativePart;
|
|
57
|
+
}
|
|
58
|
+
return this.containerFilePath + relativePart;
|
|
59
|
+
}
|
|
60
|
+
return normalizedInput;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
10
63
|
const nodeHttpAdapter = {
|
|
11
64
|
fetch,
|
|
12
65
|
createMultipart(field, file, filename) {
|
|
@@ -20,22 +73,114 @@ const nodeHttpAdapter = {
|
|
|
20
73
|
};
|
|
21
74
|
}
|
|
22
75
|
};
|
|
76
|
+
const tokenPath = path.join(configDir, "token.json");
|
|
77
|
+
const defaultCache = {
|
|
78
|
+
appid: "",
|
|
79
|
+
accessToken: "",
|
|
80
|
+
expireAt: 0
|
|
81
|
+
};
|
|
82
|
+
class TokenStore {
|
|
83
|
+
cache = defaultCache;
|
|
84
|
+
constructor() {
|
|
85
|
+
this.load();
|
|
86
|
+
}
|
|
87
|
+
load() {
|
|
88
|
+
ensureDir(configDir);
|
|
89
|
+
if (fs.existsSync(tokenPath)) {
|
|
90
|
+
this.cache = safeReadJson(tokenPath, defaultCache);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
save() {
|
|
94
|
+
try {
|
|
95
|
+
ensureDir(configDir);
|
|
96
|
+
safeWriteJson(tokenPath, this.cache);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("❌ 无法保存 token:", error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
isValid(appid) {
|
|
102
|
+
if (!this.cache) return false;
|
|
103
|
+
return this.cache.appid === appid && this.cache.expireAt > Date.now() / 1e3 + 600;
|
|
104
|
+
}
|
|
105
|
+
getToken(appid) {
|
|
106
|
+
return this.isValid(appid) ? this.cache.accessToken : null;
|
|
107
|
+
}
|
|
108
|
+
setToken(appid, accessToken, expiresIn) {
|
|
109
|
+
this.cache = {
|
|
110
|
+
appid,
|
|
111
|
+
accessToken,
|
|
112
|
+
expireAt: Math.floor(Date.now() / 1e3) + expiresIn
|
|
113
|
+
};
|
|
114
|
+
this.save();
|
|
115
|
+
}
|
|
116
|
+
clear() {
|
|
117
|
+
this.cache = defaultCache;
|
|
118
|
+
try {
|
|
119
|
+
if (fs.existsSync(tokenPath)) {
|
|
120
|
+
fs.unlinkSync(tokenPath);
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const tokenStore = new TokenStore();
|
|
127
|
+
const cachePath = path.join(configDir, "upload-cache.json");
|
|
128
|
+
class UploadCacheStore {
|
|
129
|
+
cache = {};
|
|
130
|
+
constructor() {
|
|
131
|
+
this.load();
|
|
132
|
+
}
|
|
133
|
+
load() {
|
|
134
|
+
ensureDir(configDir);
|
|
135
|
+
if (fs.existsSync(cachePath)) {
|
|
136
|
+
this.cache = safeReadJson(cachePath, {});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
save() {
|
|
140
|
+
try {
|
|
141
|
+
ensureDir(configDir);
|
|
142
|
+
safeWriteJson(cachePath, this.cache);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error("❌ 无法保存上传缓存:", error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
get(md5) {
|
|
148
|
+
return this.cache[md5];
|
|
149
|
+
}
|
|
150
|
+
set(md5, mediaId, url) {
|
|
151
|
+
this.cache[md5] = { media_id: mediaId, url, updated_at: Date.now() };
|
|
152
|
+
this.save();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const uploadCacheStore = new UploadCacheStore();
|
|
23
156
|
const { uploadMaterial, publishArticle, fetchAccessToken } = createWechatClient(nodeHttpAdapter);
|
|
157
|
+
const mediaIdMapping = /* @__PURE__ */ new Map();
|
|
24
158
|
async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
|
|
25
159
|
let fileData;
|
|
26
160
|
let finalName;
|
|
161
|
+
let md5;
|
|
27
162
|
if (imageUrl.startsWith("http")) {
|
|
28
163
|
const response = await fetch(imageUrl);
|
|
29
164
|
if (!response.ok || !response.body) {
|
|
30
165
|
throw new Error(`Failed to download image from URL: ${imageUrl}`);
|
|
31
166
|
}
|
|
167
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
168
|
+
if (arrayBuffer.byteLength === 0) {
|
|
169
|
+
throw new Error(`远程图片大小为0,无法上传: ${imageUrl}`);
|
|
170
|
+
}
|
|
171
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
172
|
+
md5 = md5FromBuffer(buffer);
|
|
173
|
+
const cached = uploadCacheStore.get(md5);
|
|
174
|
+
if (cached) {
|
|
175
|
+
mediaIdMapping.set(cached.url, cached.media_id);
|
|
176
|
+
return {
|
|
177
|
+
media_id: cached.media_id,
|
|
178
|
+
url: cached.url
|
|
179
|
+
};
|
|
180
|
+
}
|
|
32
181
|
const fileNameFromUrl = path.basename(imageUrl.split("?")[0]);
|
|
33
182
|
const ext = path.extname(fileNameFromUrl);
|
|
34
183
|
finalName = fileName ?? (ext === "" ? `${fileNameFromUrl}.jpg` : fileNameFromUrl);
|
|
35
|
-
const buffer = await response.arrayBuffer();
|
|
36
|
-
if (buffer.byteLength === 0) {
|
|
37
|
-
throw new Error(`远程图片大小为0,无法上传: ${imageUrl}`);
|
|
38
|
-
}
|
|
39
184
|
const contentType = response.headers.get("content-type") || "image/jpeg";
|
|
40
185
|
fileData = new Blob([buffer], { type: contentType });
|
|
41
186
|
} else {
|
|
@@ -44,6 +189,15 @@ async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
|
|
|
44
189
|
if (stats.size === 0) {
|
|
45
190
|
throw new Error(`本地图片大小为0,无法上传: ${resolvedPath}`);
|
|
46
191
|
}
|
|
192
|
+
md5 = md5FromFile(resolvedPath);
|
|
193
|
+
const cached = uploadCacheStore.get(md5);
|
|
194
|
+
if (cached) {
|
|
195
|
+
mediaIdMapping.set(cached.url, cached.media_id);
|
|
196
|
+
return {
|
|
197
|
+
media_id: cached.media_id,
|
|
198
|
+
url: cached.url
|
|
199
|
+
};
|
|
200
|
+
}
|
|
47
201
|
const fileNameFromLocal = path.basename(resolvedPath);
|
|
48
202
|
const ext = path.extname(fileNameFromLocal);
|
|
49
203
|
finalName = fileName ?? (ext === "" ? `${fileNameFromLocal}.jpg` : fileNameFromLocal);
|
|
@@ -51,9 +205,8 @@ async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
|
|
|
51
205
|
fileData = new Blob([await fileFromPathResult.arrayBuffer()], { type: fileFromPathResult.type });
|
|
52
206
|
}
|
|
53
207
|
const data = await uploadMaterial("image", fileData, finalName, accessToken);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
208
|
+
uploadCacheStore.set(md5, data.media_id, data.url);
|
|
209
|
+
mediaIdMapping.set(data.url, data.media_id);
|
|
57
210
|
return data;
|
|
58
211
|
}
|
|
59
212
|
async function uploadImages(content, accessToken, relativePath) {
|
|
@@ -81,27 +234,36 @@ async function uploadImages(content, accessToken, relativePath) {
|
|
|
81
234
|
const updatedHtml = dom.serialize();
|
|
82
235
|
return { html: updatedHtml, firstImageId };
|
|
83
236
|
}
|
|
84
|
-
async function
|
|
85
|
-
const {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
if (!
|
|
90
|
-
|
|
91
|
-
throw new Error(`获取 Access Token 失败,错误码:${accessToken.errcode},${accessToken.errmsg}`);
|
|
92
|
-
} else {
|
|
93
|
-
throw new Error(`获取 Access Token 失败: ${accessToken}`);
|
|
94
|
-
}
|
|
237
|
+
async function publishToWechatDraft(articleOptions, publishOptions = {}) {
|
|
238
|
+
const { title, content, cover, author, source_url } = articleOptions;
|
|
239
|
+
const { appId, appSecret, relativePath } = publishOptions;
|
|
240
|
+
const appIdFinal = appId ?? process.env.WECHAT_APP_ID;
|
|
241
|
+
const appSecretFinal = appSecret ?? process.env.WECHAT_APP_SECRET;
|
|
242
|
+
if (!appIdFinal || !appSecretFinal) {
|
|
243
|
+
throw new Error("请通过参数或环境变量 WECHAT_APP_ID / WECHAT_APP_SECRET 提供公众号凭据");
|
|
95
244
|
}
|
|
96
|
-
const
|
|
245
|
+
const accessToken = await getAccessTokenWithCache(appIdFinal, appSecretFinal);
|
|
246
|
+
const { html, firstImageId } = await uploadImages(content, accessToken, relativePath);
|
|
97
247
|
let thumbMediaId = "";
|
|
98
248
|
if (cover) {
|
|
99
|
-
const
|
|
100
|
-
|
|
249
|
+
const cachedThumbMediaId = mediaIdMapping.get(cover);
|
|
250
|
+
if (cachedThumbMediaId) {
|
|
251
|
+
thumbMediaId = cachedThumbMediaId;
|
|
252
|
+
} else {
|
|
253
|
+
const resp = await uploadImage(cover, accessToken, "cover.jpg", relativePath);
|
|
254
|
+
thumbMediaId = resp.media_id;
|
|
255
|
+
mediaIdMapping.set(resp.url, resp.media_id);
|
|
256
|
+
}
|
|
101
257
|
} else {
|
|
102
258
|
if (firstImageId.startsWith("https://mmbiz.qpic.cn")) {
|
|
103
|
-
const
|
|
104
|
-
|
|
259
|
+
const cachedThumbMediaId = mediaIdMapping.get(firstImageId);
|
|
260
|
+
if (cachedThumbMediaId) {
|
|
261
|
+
thumbMediaId = cachedThumbMediaId;
|
|
262
|
+
} else {
|
|
263
|
+
const resp = await uploadImage(firstImageId, accessToken, "cover.jpg", relativePath);
|
|
264
|
+
thumbMediaId = resp.media_id;
|
|
265
|
+
mediaIdMapping.set(resp.url, resp.media_id);
|
|
266
|
+
}
|
|
105
267
|
} else {
|
|
106
268
|
thumbMediaId = firstImageId;
|
|
107
269
|
}
|
|
@@ -109,15 +271,31 @@ async function publishToDraft(title, content, cover = "", options = {}) {
|
|
|
109
271
|
if (!thumbMediaId) {
|
|
110
272
|
throw new Error("你必须指定一张封面图或者在正文中至少出现一张图片。");
|
|
111
273
|
}
|
|
112
|
-
const data = await publishArticle(
|
|
274
|
+
const data = await publishArticle(accessToken, {
|
|
275
|
+
title,
|
|
276
|
+
content: html,
|
|
277
|
+
thumb_media_id: thumbMediaId,
|
|
278
|
+
author,
|
|
279
|
+
content_source_url: source_url
|
|
280
|
+
});
|
|
113
281
|
if (data.media_id) {
|
|
114
282
|
return data;
|
|
115
|
-
} else if (data.errcode) {
|
|
116
|
-
throw new Error(`上传到公众号草稿失败,错误码:${data.errcode},${data.errmsg}`);
|
|
117
|
-
} else {
|
|
118
|
-
throw new Error(`上传到公众号草稿失败: ${data}`);
|
|
119
283
|
}
|
|
284
|
+
throw new Error(`上传到公众号草稿失败: ${JSON.stringify(data)}`);
|
|
285
|
+
}
|
|
286
|
+
async function publishToDraft(title, content, cover = "", options = {}) {
|
|
287
|
+
return publishToWechatDraft({ title, content, cover }, options);
|
|
288
|
+
}
|
|
289
|
+
async function getAccessTokenWithCache(appId, appSecret) {
|
|
290
|
+
const cached = tokenStore.getToken(appId);
|
|
291
|
+
if (cached) {
|
|
292
|
+
return cached;
|
|
293
|
+
}
|
|
294
|
+
const result = await fetchAccessToken(appId, appSecret);
|
|
295
|
+
tokenStore.setToken(appId, result.access_token, result.expires_in);
|
|
296
|
+
return result.access_token;
|
|
120
297
|
}
|
|
121
298
|
export {
|
|
122
|
-
publishToDraft
|
|
299
|
+
publishToDraft,
|
|
300
|
+
publishToWechatDraft
|
|
123
301
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export interface FrontMatterResult {
|
|
2
2
|
body: string;
|
|
3
3
|
title?: string;
|
|
4
|
-
description?: string;
|
|
5
4
|
cover?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
author?: string;
|
|
7
|
+
source_url?: string;
|
|
6
8
|
}
|
|
7
9
|
export declare function handleFrontMatter(markdown: string): Promise<FrontMatterResult>;
|
|
@@ -14,11 +14,10 @@ declare class ConfigStore {
|
|
|
14
14
|
constructor();
|
|
15
15
|
private load;
|
|
16
16
|
private save;
|
|
17
|
-
private mkdirIfNotExists;
|
|
18
17
|
getConfig(): WenyanConfig;
|
|
19
|
-
addThemeToConfig(name: string, content: string): void;
|
|
20
18
|
getThemes(): ThemeConfigOptions[];
|
|
21
19
|
getThemeById(themeId: string): string | undefined;
|
|
20
|
+
addThemeToConfig(name: string, content: string): void;
|
|
22
21
|
addThemeFile(themeId: string, themeContent: string): string;
|
|
23
22
|
deleteThemeFromConfig(themeId: string): void;
|
|
24
23
|
deleteThemeFile(filePath: string): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface PublishOptions {
|
|
2
|
+
appId?: string;
|
|
3
|
+
appSecret?: string;
|
|
4
|
+
relativePath?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ArticleOptions {
|
|
7
|
+
title: string;
|
|
8
|
+
content: string;
|
|
9
|
+
cover?: string;
|
|
10
|
+
author?: string;
|
|
11
|
+
source_url?: string;
|
|
12
|
+
}
|
|
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>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare const tokenPath: string;
|
|
2
|
+
export interface TokenCache {
|
|
3
|
+
appid: string;
|
|
4
|
+
accessToken: string;
|
|
5
|
+
expireAt: number;
|
|
6
|
+
}
|
|
7
|
+
declare class TokenStore {
|
|
8
|
+
private cache;
|
|
9
|
+
constructor();
|
|
10
|
+
private load;
|
|
11
|
+
private save;
|
|
12
|
+
isValid(appid: string): boolean;
|
|
13
|
+
getToken(appid: string): string | null;
|
|
14
|
+
setToken(appid: string, accessToken: string, expiresIn: number): void;
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare const tokenStore: TokenStore;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface MediaInfo {
|
|
2
|
+
media_id: string;
|
|
3
|
+
url: string;
|
|
4
|
+
updated_at?: number;
|
|
5
|
+
}
|
|
6
|
+
declare class UploadCacheStore {
|
|
7
|
+
private cache;
|
|
8
|
+
constructor();
|
|
9
|
+
private load;
|
|
10
|
+
private save;
|
|
11
|
+
get(md5: string): MediaInfo;
|
|
12
|
+
set(md5: string, mediaId: string, url: string): void;
|
|
13
|
+
}
|
|
14
|
+
export declare const uploadCacheStore: UploadCacheStore;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,6 @@
|
|
|
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;
|
|
5
|
+
export declare function md5FromBuffer(buf: crypto.BinaryLike): string;
|
|
6
|
+
export declare function md5FromFile(filePath: string): string;
|
|
@@ -4,8 +4,9 @@ export interface StyledContent {
|
|
|
4
4
|
title?: string;
|
|
5
5
|
cover?: string;
|
|
6
6
|
description?: string;
|
|
7
|
+
author?: string;
|
|
8
|
+
source_url?: string;
|
|
7
9
|
}
|
|
8
10
|
export declare function renderStyledContent(content: string, options?: ApplyStylesOptions): Promise<StyledContent>;
|
|
9
11
|
export declare function getGzhContent(content: string, themeId: string, hlThemeId: string, isMacStyle?: boolean, isAddFootnote?: boolean): Promise<StyledContent>;
|
|
10
12
|
export * from "./configStore.js";
|
|
11
|
-
export * from "./runtimeEnv.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HttpAdapter } from "./http.js";
|
|
2
|
+
export interface WechatPublishOptions {
|
|
3
|
+
title: string;
|
|
4
|
+
author?: string;
|
|
5
|
+
content: string;
|
|
6
|
+
thumb_media_id: string;
|
|
7
|
+
content_source_url?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface WechatErrorResponse {
|
|
10
|
+
errcode: number;
|
|
11
|
+
errmsg: string;
|
|
12
|
+
}
|
|
13
|
+
export interface WechatUploadResponse {
|
|
14
|
+
media_id: string;
|
|
15
|
+
url: string;
|
|
16
|
+
}
|
|
17
|
+
export interface WechatTokenResponse {
|
|
18
|
+
access_token: string;
|
|
19
|
+
expires_in: number;
|
|
20
|
+
}
|
|
21
|
+
export interface WechatPublishResponse {
|
|
22
|
+
media_id: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function createWechatClient(adapter: HttpAdapter): {
|
|
25
|
+
fetchAccessToken(appId: string, appSecret: string): Promise<WechatTokenResponse>;
|
|
26
|
+
uploadMaterial(type: string, file: Blob, filename: string, accessToken: string): Promise<WechatUploadResponse>;
|
|
27
|
+
publishArticle(accessToken: string, options: WechatPublishOptions): Promise<WechatPublishResponse>;
|
|
28
|
+
};
|
|
29
|
+
export type WechatClient = ReturnType<typeof createWechatClient>;
|
package/dist/wechat.js
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
const browserHttpAdapter = {
|
|
2
|
-
fetch: window.fetch.bind(window),
|
|
3
|
-
createMultipart(field, file, filename) {
|
|
4
|
-
const form = new FormData();
|
|
5
|
-
form.append(field, file, filename);
|
|
6
|
-
return {
|
|
7
|
-
body: form
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
1
|
const tokenUrl = "https://api.weixin.qq.com/cgi-bin/token";
|
|
12
2
|
const publishUrl = "https://api.weixin.qq.com/cgi-bin/draft/add";
|
|
13
3
|
const uploadUrl = "https://api.weixin.qq.com/cgi-bin/material/add_material";
|
|
@@ -18,7 +8,9 @@ function createWechatClient(adapter) {
|
|
|
18
8
|
`${tokenUrl}?grant_type=client_credential&appid=${appId}&secret=${appSecret}`
|
|
19
9
|
);
|
|
20
10
|
if (!res.ok) throw new Error(await res.text());
|
|
21
|
-
|
|
11
|
+
const data = await res.json();
|
|
12
|
+
assertWechatSuccess(data);
|
|
13
|
+
return data;
|
|
22
14
|
},
|
|
23
15
|
async uploadMaterial(type, file, filename, accessToken) {
|
|
24
16
|
const multipart = adapter.createMultipart("media", file, filename);
|
|
@@ -26,28 +18,33 @@ function createWechatClient(adapter) {
|
|
|
26
18
|
...multipart,
|
|
27
19
|
method: "POST"
|
|
28
20
|
});
|
|
21
|
+
if (!res.ok) throw new Error(await res.text());
|
|
29
22
|
const data = await res.json();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
if (data.url?.startsWith("http://")) {
|
|
23
|
+
assertWechatSuccess(data);
|
|
24
|
+
if (data.url.startsWith("http://")) {
|
|
34
25
|
data.url = data.url.replace(/^http:\/\//i, "https://");
|
|
35
26
|
}
|
|
36
27
|
return data;
|
|
37
28
|
},
|
|
38
|
-
async publishArticle(
|
|
29
|
+
async publishArticle(accessToken, options) {
|
|
39
30
|
const res = await adapter.fetch(`${publishUrl}?access_token=${accessToken}`, {
|
|
40
31
|
method: "POST",
|
|
41
32
|
body: JSON.stringify({
|
|
42
|
-
articles: [
|
|
33
|
+
articles: [options]
|
|
43
34
|
})
|
|
44
35
|
});
|
|
45
36
|
if (!res.ok) throw new Error(await res.text());
|
|
46
|
-
|
|
37
|
+
const data = await res.json();
|
|
38
|
+
assertWechatSuccess(data);
|
|
39
|
+
return data;
|
|
47
40
|
}
|
|
48
41
|
};
|
|
49
42
|
}
|
|
43
|
+
function assertWechatSuccess(data) {
|
|
44
|
+
if ("errcode" in data) {
|
|
45
|
+
throw new Error(`${data.errcode}: ${data.errmsg}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
50
48
|
export {
|
|
51
|
-
browserHttpAdapter,
|
|
52
49
|
createWechatClient
|
|
53
50
|
};
|
package/dist/wrapper.js
CHANGED
|
@@ -1,93 +1,6 @@
|
|
|
1
1
|
import { JSDOM } from "jsdom";
|
|
2
2
|
import { createWenyanCore } from "./core.js";
|
|
3
|
-
import
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import fs from "node:fs";
|
|
6
|
-
import { R } from "./runtimeEnv-pU2mTDLR.js";
|
|
7
|
-
const defaultConfig = {};
|
|
8
|
-
const configDir = process.env.APPDATA ? path.join(process.env.APPDATA, "wenyan-md") : path.join(os.homedir(), ".config", "wenyan-md");
|
|
9
|
-
const configPath = path.join(configDir, "config.json");
|
|
10
|
-
class ConfigStore {
|
|
11
|
-
config = { ...defaultConfig };
|
|
12
|
-
constructor() {
|
|
13
|
-
this.load();
|
|
14
|
-
}
|
|
15
|
-
load() {
|
|
16
|
-
if (fs.existsSync(configPath)) {
|
|
17
|
-
try {
|
|
18
|
-
const fileContent = fs.readFileSync(configPath, "utf-8");
|
|
19
|
-
this.config = { ...defaultConfig, ...JSON.parse(fileContent) };
|
|
20
|
-
} catch (error) {
|
|
21
|
-
console.warn("⚠️ 配置文件解析失败,将使用默认配置");
|
|
22
|
-
this.config = { ...defaultConfig };
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
save() {
|
|
27
|
-
this.mkdirIfNotExists();
|
|
28
|
-
try {
|
|
29
|
-
fs.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
30
|
-
} catch (error) {
|
|
31
|
-
console.error("❌ 无法保存配置文件:", error);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
mkdirIfNotExists(dir = configDir) {
|
|
35
|
-
try {
|
|
36
|
-
if (!fs.existsSync(dir)) {
|
|
37
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
38
|
-
}
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.error("❌ 无法创建配置目录:", error);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
getConfig() {
|
|
44
|
-
return this.config;
|
|
45
|
-
}
|
|
46
|
-
addThemeToConfig(name, content) {
|
|
47
|
-
const savedPath = this.addThemeFile(name, content);
|
|
48
|
-
this.config.themes = this.config.themes || {};
|
|
49
|
-
this.config.themes[name] = {
|
|
50
|
-
id: name,
|
|
51
|
-
name,
|
|
52
|
-
path: savedPath
|
|
53
|
-
};
|
|
54
|
-
this.save();
|
|
55
|
-
}
|
|
56
|
-
getThemes() {
|
|
57
|
-
return this.config.themes ? Object.values(this.config.themes) : [];
|
|
58
|
-
}
|
|
59
|
-
getThemeById(themeId) {
|
|
60
|
-
const themeOption = this.config.themes ? this.config.themes[themeId] : void 0;
|
|
61
|
-
if (themeOption) {
|
|
62
|
-
const absoluteFilePath = path.join(configDir, themeOption.path);
|
|
63
|
-
if (fs.existsSync(absoluteFilePath)) {
|
|
64
|
-
return fs.readFileSync(absoluteFilePath, "utf-8");
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return void 0;
|
|
68
|
-
}
|
|
69
|
-
addThemeFile(themeId, themeContent) {
|
|
70
|
-
const filePath = `themes/${themeId}.css`;
|
|
71
|
-
const absoluteFilePath = path.join(configDir, filePath);
|
|
72
|
-
this.mkdirIfNotExists(path.dirname(absoluteFilePath));
|
|
73
|
-
fs.writeFileSync(absoluteFilePath, themeContent, "utf-8");
|
|
74
|
-
return filePath;
|
|
75
|
-
}
|
|
76
|
-
deleteThemeFromConfig(themeId) {
|
|
77
|
-
if (this.config.themes && this.config.themes[themeId]) {
|
|
78
|
-
this.deleteThemeFile(this.config.themes[themeId].path);
|
|
79
|
-
delete this.config.themes[themeId];
|
|
80
|
-
this.save();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
deleteThemeFile(filePath) {
|
|
84
|
-
const absoluteFilePath = path.join(configDir, filePath);
|
|
85
|
-
if (fs.existsSync(absoluteFilePath)) {
|
|
86
|
-
fs.unlinkSync(absoluteFilePath);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
const configStore = new ConfigStore();
|
|
3
|
+
import { c, a, b } from "./configStore-lZ5bhrcC.js";
|
|
91
4
|
const wenyanCoreInstance = await createWenyanCore();
|
|
92
5
|
async function renderStyledContent(content, options = {}) {
|
|
93
6
|
const preHandlerContent = await wenyanCoreInstance.handleFrontMatter(content);
|
|
@@ -100,7 +13,9 @@ async function renderStyledContent(content, options = {}) {
|
|
|
100
13
|
content: result,
|
|
101
14
|
title: preHandlerContent.title,
|
|
102
15
|
cover: preHandlerContent.cover,
|
|
103
|
-
description: preHandlerContent.description
|
|
16
|
+
description: preHandlerContent.description,
|
|
17
|
+
author: preHandlerContent.author,
|
|
18
|
+
source_url: preHandlerContent.source_url
|
|
104
19
|
};
|
|
105
20
|
}
|
|
106
21
|
async function getGzhContent(content, themeId, hlThemeId, isMacStyle = true, isAddFootnote = true) {
|
|
@@ -112,10 +27,9 @@ async function getGzhContent(content, themeId, hlThemeId, isMacStyle = true, isA
|
|
|
112
27
|
});
|
|
113
28
|
}
|
|
114
29
|
export {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
configStore,
|
|
30
|
+
c as configDir,
|
|
31
|
+
a as configPath,
|
|
32
|
+
b as configStore,
|
|
119
33
|
getGzhContent,
|
|
120
34
|
renderStyledContent
|
|
121
35
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenyan-md/core",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
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",
|
|
@@ -28,12 +28,11 @@
|
|
|
28
28
|
"exports": {
|
|
29
29
|
".": {
|
|
30
30
|
"import": "./dist/core.js",
|
|
31
|
-
"types": "./dist/types/core/index.d.ts"
|
|
32
|
-
"browser": "./dist/browser/wenyan-core.js"
|
|
31
|
+
"types": "./dist/types/core/index.d.ts"
|
|
33
32
|
},
|
|
34
33
|
"./publish": {
|
|
35
34
|
"import": "./dist/publish.js",
|
|
36
|
-
"types": "./dist/types/
|
|
35
|
+
"types": "./dist/types/node/publish.d.ts"
|
|
37
36
|
},
|
|
38
37
|
"./wrapper": {
|
|
39
38
|
"import": "./dist/wrapper.js",
|
|
@@ -41,7 +40,11 @@
|
|
|
41
40
|
},
|
|
42
41
|
"./wechat": {
|
|
43
42
|
"import": "./dist/wechat.js",
|
|
44
|
-
"types": "./dist/types/wechat
|
|
43
|
+
"types": "./dist/types/wechat.d.ts"
|
|
44
|
+
},
|
|
45
|
+
"./http": {
|
|
46
|
+
"import": "./dist/http.js",
|
|
47
|
+
"types": "./dist/types/http.d.ts"
|
|
45
48
|
}
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
function normalizePath(p) {
|
|
3
|
-
return p.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4
|
-
}
|
|
5
|
-
function isAbsolutePath(path2) {
|
|
6
|
-
if (!path2) return false;
|
|
7
|
-
const winAbsPattern = /^[a-zA-Z]:\//;
|
|
8
|
-
const linuxAbsPattern = /^\//;
|
|
9
|
-
return winAbsPattern.test(path2) || linuxAbsPattern.test(path2);
|
|
10
|
-
}
|
|
11
|
-
const RuntimeEnv = {
|
|
12
|
-
isContainer: !!process.env.CONTAINERIZED,
|
|
13
|
-
hostFilePath: normalizePath(process.env.HOST_FILE_PATH || ""),
|
|
14
|
-
containerFilePath: normalizePath(process.env.CONTAINER_FILE_PATH || "/mnt/host-downloads"),
|
|
15
|
-
resolveLocalPath(inputPath, relativeBase) {
|
|
16
|
-
if (!this.isContainer) {
|
|
17
|
-
if (relativeBase) {
|
|
18
|
-
return path.resolve(relativeBase, inputPath);
|
|
19
|
-
} else {
|
|
20
|
-
if (!path.isAbsolute(inputPath)) {
|
|
21
|
-
throw new Error(
|
|
22
|
-
`Invalid input: '${inputPath}'. When relativeBase is not provided, inputPath must be an absolute path.`
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
return path.normalize(inputPath);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
let normalizedInput = normalizePath(inputPath);
|
|
29
|
-
relativeBase = normalizePath(relativeBase || "");
|
|
30
|
-
if (relativeBase) {
|
|
31
|
-
if (!isAbsolutePath(normalizedInput)) {
|
|
32
|
-
normalizedInput = relativeBase + (normalizedInput.startsWith("/") ? "" : "/") + normalizedInput;
|
|
33
|
-
}
|
|
34
|
-
} else {
|
|
35
|
-
if (!isAbsolutePath(normalizedInput)) {
|
|
36
|
-
throw new Error(
|
|
37
|
-
`Invalid input: '${inputPath}'. When relativeBase is not provided, inputPath must be an absolute path.`
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (normalizedInput.startsWith(this.hostFilePath)) {
|
|
42
|
-
let relativePart = normalizedInput.slice(this.hostFilePath.length);
|
|
43
|
-
if (relativePart && !relativePart.startsWith("/")) {
|
|
44
|
-
return normalizedInput;
|
|
45
|
-
}
|
|
46
|
-
if (!relativePart.startsWith("/")) {
|
|
47
|
-
relativePart = "/" + relativePart;
|
|
48
|
-
}
|
|
49
|
-
return this.containerFilePath + relativePart;
|
|
50
|
-
}
|
|
51
|
-
return normalizedInput;
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
export {
|
|
55
|
-
RuntimeEnv as R
|
|
56
|
-
};
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { HttpAdapter } from "./http.js";
|
|
2
|
-
export declare function createWechatClient(adapter: HttpAdapter): {
|
|
3
|
-
fetchAccessToken(appId: string, appSecret: string): Promise<any>;
|
|
4
|
-
uploadMaterial(type: string, file: Blob, filename: string, accessToken: string): Promise<any>;
|
|
5
|
-
publishArticle(title: string, content: string, thumbMediaId: string, accessToken: string): Promise<any>;
|
|
6
|
-
};
|
|
7
|
-
export type WechatClient = ReturnType<typeof createWechatClient>;
|
|
8
|
-
export * from "./http.js";
|
|
9
|
-
export * from "./adapters/browser.js";
|
|
File without changes
|
|
File without changes
|