@lyhue1991/wxgzh 0.1.0

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.
Files changed (50) hide show
  1. package/README.md +432 -0
  2. package/assets/backgrounds//345/217/214/347/214/253.jpg +0 -0
  3. package/assets/backgrounds//345/217/244/351/243/216.jpg +0 -0
  4. package/assets/backgrounds//345/217/244/351/243/2162.jpg +0 -0
  5. package/assets/backgrounds//345/260/221/345/245/263/346/230/237/347/251/272.jpg +0 -0
  6. package/assets/backgrounds//345/277/203/345/277/203/347/233/270/345/215/260.jpg +0 -0
  7. package/assets/backgrounds//346/230/237/347/251/272.jpg +0 -0
  8. package/assets/backgrounds//346/240/221/346/234/250.jpg +0 -0
  9. package/assets/backgrounds//346/260/264/345/275/251.jpg +0 -0
  10. package/assets/backgrounds//346/260/264/346/274/253.jpg +0 -0
  11. package/assets/backgrounds//346/265/267/346/230/237.jpg +0 -0
  12. package/assets/backgrounds//346/265/267/346/273/251.jpg +0 -0
  13. package/assets/backgrounds//346/265/267/350/261/232.jpg +0 -0
  14. package/assets/backgrounds//346/270/205/346/231/250.jpg +0 -0
  15. package/assets/backgrounds//347/203/255/346/260/224/347/220/203.jpg +0 -0
  16. package/assets/backgrounds//347/214/253/345/222/252/345/245/263/345/255/251.jpg +0 -0
  17. package/assets/backgrounds//347/223/246/345/212/233.jpg +0 -0
  18. package/assets/backgrounds//347/272/242/345/217/266/351/243/230/351/243/230.jpg +0 -0
  19. package/assets/backgrounds//350/212/261/346/234/265.jpg +0 -0
  20. package/assets/backgrounds//351/243/216/350/275/246.jpg +0 -0
  21. package/assets/backgrounds//351/273/204/346/230/217.jpg +0 -0
  22. package/bin/wxgzh.js +3 -0
  23. package/dist/cli/config.js +62 -0
  24. package/dist/cli/cover.js +39 -0
  25. package/dist/cli/fix.js +38 -0
  26. package/dist/cli/index.js +161 -0
  27. package/dist/cli/md2html.js +49 -0
  28. package/dist/cli/publish.js +56 -0
  29. package/dist/core/converter.js +656 -0
  30. package/dist/core/cover.js +164 -0
  31. package/dist/core/fixer.js +97 -0
  32. package/dist/core/parser.js +105 -0
  33. package/dist/core/themes.js +59 -0
  34. package/dist/core/wechat.js +162 -0
  35. package/dist/types.js +2 -0
  36. package/dist/utils/config.js +89 -0
  37. package/dist/utils/fs.js +20 -0
  38. package/dist/utils/logger.js +18 -0
  39. package/dist/utils/tls.js +46 -0
  40. package/package.json +47 -0
  41. package/spec.md +490 -0
  42. package/styles/black.css +600 -0
  43. package/styles/blue.css +616 -0
  44. package/styles/brown.css +620 -0
  45. package/styles/custom.css +8 -0
  46. package/styles/default.css +227 -0
  47. package/styles/green.css +617 -0
  48. package/styles/orange.css +617 -0
  49. package/styles/red.css +617 -0
  50. package/styles/yellow.css +202 -0
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listCoverPresets = listCoverPresets;
7
+ exports.createCover = createCover;
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const sharp_1 = __importDefault(require("sharp"));
11
+ const BUILTIN_BACKGROUND_DIR = node_path_1.default.resolve(__dirname, '../../assets/backgrounds');
12
+ const FALLBACK_GRADIENT = {
13
+ start: '#e8f3ef',
14
+ end: '#c9ded6'
15
+ };
16
+ function escapeXml(value) {
17
+ return value
18
+ .replace(/&/g, '&')
19
+ .replace(/</g, '&lt;')
20
+ .replace(/>/g, '&gt;')
21
+ .replace(/"/g, '&quot;')
22
+ .replace(/'/g, '&apos;');
23
+ }
24
+ function escapeAttribute(value) {
25
+ return escapeXml(value);
26
+ }
27
+ function readFiles(dirPath, pattern) {
28
+ try {
29
+ return (0, node_fs_1.readdirSync)(dirPath)
30
+ .filter((fileName) => pattern.test(fileName))
31
+ .map((fileName) => node_path_1.default.join(dirPath, fileName))
32
+ .sort((left, right) => left.localeCompare(right, 'zh-Hans-CN'));
33
+ }
34
+ catch {
35
+ return [];
36
+ }
37
+ }
38
+ function pickRandom(items) {
39
+ if (items.length === 0) {
40
+ return undefined;
41
+ }
42
+ return items[Math.floor(Math.random() * items.length)];
43
+ }
44
+ function getBuiltinBackgrounds() {
45
+ return readFiles(BUILTIN_BACKGROUND_DIR, /\.(jpe?g|png)$/i);
46
+ }
47
+ function buildTitleLines(text, maxLineLength) {
48
+ const trimmed = text.trim() || '未命名文章';
49
+ const quoted = trimmed.includes('『') || trimmed.includes('』') ? trimmed : `『${trimmed}』`;
50
+ if (quoted.includes('\n')) {
51
+ return quoted.split('\n').map((line) => line.trim()).filter(Boolean);
52
+ }
53
+ const chars = [...quoted];
54
+ const lines = [];
55
+ for (let index = 0; index < chars.length; index += maxLineLength) {
56
+ lines.push(chars.slice(index, index + maxLineLength).join(''));
57
+ }
58
+ return lines;
59
+ }
60
+ function estimateFontSize(width, lines) {
61
+ const longestChars = Math.max(...lines.map((line) => [...line].length), 1);
62
+ const title = Math.max(42, Math.floor(width / (longestChars + 6)));
63
+ return {
64
+ title,
65
+ author: Math.max(20, Math.floor(title / 2)),
66
+ longestChars
67
+ };
68
+ }
69
+ function buildGradientBackground(width, height) {
70
+ const svg = `
71
+ <svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
72
+ <defs>
73
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
74
+ <stop offset="0%" stop-color="${FALLBACK_GRADIENT.start}" />
75
+ <stop offset="100%" stop-color="${FALLBACK_GRADIENT.end}" />
76
+ </linearGradient>
77
+ </defs>
78
+ <rect width="${width}" height="${height}" fill="url(#bg)" />
79
+ </svg>`;
80
+ return Buffer.from(svg);
81
+ }
82
+ function buildTextSvg(params) {
83
+ const lines = buildTitleLines(params.title, 10);
84
+ const sizes = estimateFontSize(params.width, lines);
85
+ const lineHeight = Math.round(sizes.title * 1.15);
86
+ const titleBlockHeight = lineHeight * lines.length;
87
+ const titleTop = Math.round((params.height - titleBlockHeight) / 2);
88
+ const fontFamily = '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif';
89
+ const titleWidth = sizes.longestChars * sizes.title;
90
+ const titleRight = Math.round(params.width / 2 + titleWidth / 2);
91
+ const authorY = titleTop + titleBlockHeight + sizes.author + Math.round(sizes.author * 0.4);
92
+ const titleText = lines
93
+ .map((line, index) => {
94
+ const y = titleTop + sizes.title + index * lineHeight;
95
+ return `<text x="50%" y="${y}" text-anchor="middle" font-size="${sizes.title}" font-family="${escapeAttribute(fontFamily)}" fill="rgba(0,0,0,0.72)">${escapeXml(line)}</text>`;
96
+ })
97
+ .join('');
98
+ const authorText = params.author.trim()
99
+ ? `<text x="${titleRight}" y="${authorY}" text-anchor="end" font-size="${sizes.author}" font-family="${escapeAttribute(fontFamily)}" fill="rgba(0,0,0,0.72)">${escapeXml(params.author.trim())}</text>`
100
+ : '';
101
+ const svg = `
102
+ <svg width="${params.width}" height="${params.height}" viewBox="0 0 ${params.width} ${params.height}" xmlns="http://www.w3.org/2000/svg">
103
+ ${titleText}
104
+ ${authorText}
105
+ </svg>`;
106
+ return Buffer.from(svg);
107
+ }
108
+ function resolveBackgroundPath(backgroundPath, presetName) {
109
+ if (backgroundPath) {
110
+ return node_path_1.default.resolve(backgroundPath);
111
+ }
112
+ const backgrounds = getBuiltinBackgrounds();
113
+ if (backgrounds.length === 0) {
114
+ return undefined;
115
+ }
116
+ if (presetName) {
117
+ const matched = backgrounds.find((filePath) => node_path_1.default.basename(filePath, node_path_1.default.extname(filePath)) === presetName);
118
+ if (matched) {
119
+ return matched;
120
+ }
121
+ }
122
+ return pickRandom(backgrounds);
123
+ }
124
+ function listCoverPresets() {
125
+ const backgrounds = getBuiltinBackgrounds();
126
+ if (backgrounds.length === 0) {
127
+ return [];
128
+ }
129
+ return backgrounds.map((filePath) => node_path_1.default.basename(filePath, node_path_1.default.extname(filePath)));
130
+ }
131
+ async function createCover(options) {
132
+ const width = options.width ?? 1000;
133
+ const height = options.height ?? 700;
134
+ const author = options.author?.trim() || 'wxgzh';
135
+ const outputPath = node_path_1.default.resolve(options.outputPath);
136
+ const backgroundPath = resolveBackgroundPath(options.backgroundPath, options.presetName);
137
+ const background = backgroundPath
138
+ ? (0, sharp_1.default)(backgroundPath).resize(width, height, { fit: 'cover' })
139
+ : (0, sharp_1.default)(buildGradientBackground(width, height));
140
+ await background
141
+ .composite([
142
+ {
143
+ input: {
144
+ create: {
145
+ width,
146
+ height,
147
+ channels: 4,
148
+ background: { r: 255, g: 255, b: 255, alpha: 0.58 }
149
+ }
150
+ }
151
+ },
152
+ {
153
+ input: buildTextSvg({
154
+ title: options.title,
155
+ author,
156
+ width,
157
+ height
158
+ })
159
+ }
160
+ ])
161
+ .jpeg({ quality: 92 })
162
+ .toFile(outputPath);
163
+ return outputPath;
164
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.fixHtmlFile = fixHtmlFile;
40
+ const node_path_1 = __importDefault(require("node:path"));
41
+ const cheerio = __importStar(require("cheerio"));
42
+ const converter_1 = require("./converter");
43
+ const fs_1 = require("../utils/fs");
44
+ function isRemoteUrl(value) {
45
+ return /^https?:\/\//i.test(value);
46
+ }
47
+ function toCdnUrl(cdn, source) {
48
+ const cleanBase = cdn.replace(/\/+$/, '');
49
+ const fileName = node_path_1.default.basename(source);
50
+ return `${cleanBase}/${encodeURIComponent(fileName)}`;
51
+ }
52
+ function resolveImageSource(baseDir, source) {
53
+ if (isRemoteUrl(source) || node_path_1.default.isAbsolute(source)) {
54
+ return source;
55
+ }
56
+ const normalizedSource = (() => {
57
+ try {
58
+ return decodeURIComponent(source);
59
+ }
60
+ catch {
61
+ return source;
62
+ }
63
+ })();
64
+ return node_path_1.default.resolve(baseDir, normalizedSource);
65
+ }
66
+ async function fixHtmlFile(articlePath, options) {
67
+ const html = await (0, fs_1.readTextFile)(articlePath);
68
+ const metadata = (0, converter_1.readHtmlMetadata)(html);
69
+ const $ = cheerio.load(html);
70
+ const sourceBaseDir = metadata.sourceDir ? node_path_1.default.resolve(metadata.sourceDir) : node_path_1.default.dirname(articlePath);
71
+ $('script,iframe').remove();
72
+ const images = $('img').toArray();
73
+ for (const element of images) {
74
+ const image = $(element);
75
+ const source = image.attr('src');
76
+ if (!source) {
77
+ continue;
78
+ }
79
+ if (options.upload && options.wechat && !source.includes('mmbiz.qpic.cn')) {
80
+ const uploadedUrl = await options.wechat.uploadArticleImage(resolveImageSource(sourceBaseDir, source));
81
+ image.attr('src', uploadedUrl);
82
+ image.attr('data-original-src', source);
83
+ }
84
+ else if (options.cdn && !isRemoteUrl(source)) {
85
+ image.attr('src', toCdnUrl(options.cdn, source));
86
+ }
87
+ image.attr('style', [
88
+ 'display:block',
89
+ 'max-width:100%',
90
+ 'height:auto',
91
+ 'margin:0 auto',
92
+ 'border-radius:6px'
93
+ ].join(';'));
94
+ }
95
+ await (0, fs_1.writeTextFile)(articlePath, $.html());
96
+ return { imageCount: images.length };
97
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseMarkdown = parseMarkdown;
7
+ const gray_matter_1 = __importDefault(require("gray-matter"));
8
+ function extractFirstHeading(markdown) {
9
+ const lines = markdown.split(/\r?\n/);
10
+ let inCodeBlock = false;
11
+ for (let index = 0; index < lines.length; index += 1) {
12
+ const line = lines[index] ?? '';
13
+ if (line.trim().startsWith('```')) {
14
+ inCodeBlock = !inCodeBlock;
15
+ continue;
16
+ }
17
+ if (inCodeBlock) {
18
+ continue;
19
+ }
20
+ const match = line.match(/^#\s+(.+)$/);
21
+ if (!match) {
22
+ continue;
23
+ }
24
+ const nextLines = [...lines.slice(0, index), ...lines.slice(index + 1)];
25
+ if ((nextLines[index] ?? '').trim() === '') {
26
+ nextLines.splice(index, 1);
27
+ }
28
+ return {
29
+ title: match[1].trim(),
30
+ body: nextLines.join('\n')
31
+ };
32
+ }
33
+ return { body: markdown };
34
+ }
35
+ function extractSecondaryHeadings(markdown) {
36
+ const lines = markdown.split(/\r?\n/);
37
+ const headings = [];
38
+ let inCodeBlock = false;
39
+ for (const rawLine of lines) {
40
+ const line = rawLine ?? '';
41
+ if (line.trim().startsWith('```')) {
42
+ inCodeBlock = !inCodeBlock;
43
+ continue;
44
+ }
45
+ if (inCodeBlock) {
46
+ continue;
47
+ }
48
+ const match = line.match(/^##\s+(.+)$/);
49
+ if (!match) {
50
+ continue;
51
+ }
52
+ const heading = match[1].trim();
53
+ if (heading) {
54
+ headings.push(heading);
55
+ }
56
+ }
57
+ return headings;
58
+ }
59
+ function buildDigest(markdown, title) {
60
+ const headings = extractSecondaryHeadings(markdown);
61
+ if (headings.length > 0) {
62
+ return headings.join(';');
63
+ }
64
+ if (title?.trim()) {
65
+ return title.trim();
66
+ }
67
+ return '由 wxgzh 自动生成的公众号草稿';
68
+ }
69
+ function normalizeMetadata(data) {
70
+ const metadata = {};
71
+ if (typeof data.title === 'string' && data.title.trim()) {
72
+ metadata.title = data.title.trim();
73
+ }
74
+ if (typeof data.author === 'string' && data.author.trim()) {
75
+ metadata.author = data.author.trim();
76
+ }
77
+ if (typeof data.digest === 'string' && data.digest.trim()) {
78
+ metadata.digest = data.digest.trim();
79
+ }
80
+ if (typeof data.theme === 'string' && data.theme.trim()) {
81
+ metadata.theme = data.theme.trim();
82
+ }
83
+ if (typeof data.cover === 'string' && data.cover.trim()) {
84
+ metadata.cover = data.cover.trim();
85
+ }
86
+ if (typeof data.enableComment === 'boolean') {
87
+ metadata.enableComment = data.enableComment;
88
+ }
89
+ return metadata;
90
+ }
91
+ function parseMarkdown(rawMarkdown) {
92
+ const parsed = (0, gray_matter_1.default)(rawMarkdown);
93
+ const frontMatter = normalizeMetadata(parsed.data);
94
+ const extracted = extractFirstHeading(parsed.content.trim());
95
+ const resolvedTitle = frontMatter.title ?? extracted.title;
96
+ return {
97
+ metadata: {
98
+ ...frontMatter,
99
+ title: resolvedTitle,
100
+ digest: frontMatter.digest ?? buildDigest(extracted.body, resolvedTitle)
101
+ },
102
+ body: extracted.body,
103
+ originalBody: parsed.content
104
+ };
105
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listAvailableThemes = listAvailableThemes;
7
+ exports.isValidTheme = isValidTheme;
8
+ exports.assertThemeExists = assertThemeExists;
9
+ exports.getDefaultThemeName = getDefaultThemeName;
10
+ const node_fs_1 = require("node:fs");
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const DEFAULT_THEME = 'default';
13
+ function getStylesDir() {
14
+ return node_path_1.default.resolve(__dirname, '../../styles');
15
+ }
16
+ function normalizeThemeName(fileName) {
17
+ if (!fileName.endsWith('.css')) {
18
+ return undefined;
19
+ }
20
+ const themeName = node_path_1.default.basename(fileName, '.css');
21
+ if (!themeName || themeName === 'custom') {
22
+ return undefined;
23
+ }
24
+ return themeName;
25
+ }
26
+ function listAvailableThemes() {
27
+ try {
28
+ const themes = (0, node_fs_1.readdirSync)(getStylesDir())
29
+ .map(normalizeThemeName)
30
+ .filter((themeName) => Boolean(themeName))
31
+ .sort((left, right) => left.localeCompare(right, 'en'));
32
+ if (!themes.includes(DEFAULT_THEME)) {
33
+ return [DEFAULT_THEME, ...themes];
34
+ }
35
+ return themes;
36
+ }
37
+ catch {
38
+ return [DEFAULT_THEME];
39
+ }
40
+ }
41
+ function isValidTheme(theme) {
42
+ if (!theme) {
43
+ return false;
44
+ }
45
+ return listAvailableThemes().includes(theme.trim());
46
+ }
47
+ function assertThemeExists(theme) {
48
+ const normalized = theme?.trim();
49
+ if (!normalized) {
50
+ throw new Error('主题名不能为空');
51
+ }
52
+ if (!isValidTheme(normalized)) {
53
+ throw new Error(`不支持的主题: ${normalized}。可用主题:${listAvailableThemes().join(', ')}`);
54
+ }
55
+ return normalized;
56
+ }
57
+ function getDefaultThemeName() {
58
+ return DEFAULT_THEME;
59
+ }
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.WechatClient = void 0;
7
+ const node_fs_1 = require("node:fs");
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_os_1 = __importDefault(require("node:os"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const axios_1 = __importDefault(require("axios"));
12
+ const form_data_1 = __importDefault(require("form-data"));
13
+ const tls_1 = require("../utils/tls");
14
+ const CONFIG_DIR = node_path_1.default.join(node_os_1.default.homedir(), '.config', 'wxgzh');
15
+ const http = axios_1.default.create({
16
+ httpsAgent: (0, tls_1.getHttpsAgent)(),
17
+ timeout: 30_000
18
+ });
19
+ function tokenCachePath(appid) {
20
+ return node_path_1.default.join(CONFIG_DIR, `token.${appid}.json`);
21
+ }
22
+ async function pathExists(filePath) {
23
+ try {
24
+ await (0, promises_1.access)(filePath);
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function assertWechatResponse(data, fallbackMessage) {
32
+ if (typeof data.errcode === 'number' && data.errcode !== 0) {
33
+ throw new Error(`微信接口调用失败: ${data.errmsg ?? fallbackMessage} (${data.errcode})`);
34
+ }
35
+ }
36
+ function guessContentType(fileName) {
37
+ const ext = node_path_1.default.extname(fileName).toLowerCase();
38
+ if (ext === '.png') {
39
+ return 'image/png';
40
+ }
41
+ if (ext === '.gif') {
42
+ return 'image/gif';
43
+ }
44
+ if (ext === '.webp') {
45
+ return 'image/webp';
46
+ }
47
+ return 'image/jpeg';
48
+ }
49
+ class WechatClient {
50
+ credentials;
51
+ constructor(credentials) {
52
+ this.credentials = credentials;
53
+ }
54
+ async readTokenCache() {
55
+ const cachePath = tokenCachePath(this.credentials.appid);
56
+ if (!(await pathExists(cachePath))) {
57
+ return null;
58
+ }
59
+ const raw = await (0, promises_1.readFile)(cachePath, 'utf8');
60
+ return JSON.parse(raw);
61
+ }
62
+ async writeTokenCache(cache) {
63
+ await (0, promises_1.mkdir)(CONFIG_DIR, { recursive: true });
64
+ await (0, promises_1.writeFile)(tokenCachePath(this.credentials.appid), `${JSON.stringify(cache, null, 2)}\n`, 'utf8');
65
+ }
66
+ async getAccessToken(forceRefresh = false) {
67
+ if (!forceRefresh) {
68
+ const cached = await this.readTokenCache();
69
+ if (cached && cached.expires_at > Date.now() + 60_000) {
70
+ return cached.access_token;
71
+ }
72
+ }
73
+ const response = await http.get('https://api.weixin.qq.com/cgi-bin/token', {
74
+ params: {
75
+ grant_type: 'client_credential',
76
+ appid: this.credentials.appid,
77
+ secret: this.credentials.appsecret
78
+ }
79
+ });
80
+ const data = response.data;
81
+ assertWechatResponse(data, '获取 access_token 失败');
82
+ const accessToken = String(data.access_token);
83
+ const expiresIn = Number(data.expires_in ?? 7200);
84
+ await this.writeTokenCache({
85
+ access_token: accessToken,
86
+ expires_at: Date.now() + Math.max(expiresIn - 300, 60) * 1000
87
+ });
88
+ return accessToken;
89
+ }
90
+ async buildImageForm(source) {
91
+ const form = new form_data_1.default();
92
+ if (/^https?:\/\//i.test(source)) {
93
+ const response = await http.get(source, { responseType: 'arraybuffer' });
94
+ const fileName = node_path_1.default.basename(new URL(source).pathname || 'image.jpg') || 'image.jpg';
95
+ form.append('media', Buffer.from(response.data), {
96
+ filename: fileName,
97
+ contentType: response.headers['content-type'] ?? guessContentType(fileName)
98
+ });
99
+ return form;
100
+ }
101
+ const absolutePath = node_path_1.default.resolve(source);
102
+ form.append('media', (0, node_fs_1.createReadStream)(absolutePath), {
103
+ filename: node_path_1.default.basename(absolutePath),
104
+ contentType: guessContentType(absolutePath)
105
+ });
106
+ return form;
107
+ }
108
+ async uploadArticleImage(source) {
109
+ const accessToken = await this.getAccessToken();
110
+ const form = await this.buildImageForm(source);
111
+ const response = await http.post('https://api.weixin.qq.com/cgi-bin/media/uploadimg', form, {
112
+ params: { access_token: accessToken },
113
+ headers: form.getHeaders()
114
+ });
115
+ const data = response.data;
116
+ assertWechatResponse(data, '上传正文图片失败');
117
+ if (!data.url) {
118
+ throw new Error('微信未返回正文图片地址');
119
+ }
120
+ return String(data.url).split('?')[0] ?? String(data.url);
121
+ }
122
+ async uploadCoverImage(source) {
123
+ const accessToken = await this.getAccessToken();
124
+ const form = await this.buildImageForm(source);
125
+ const response = await http.post('https://api.weixin.qq.com/cgi-bin/material/add_material', form, {
126
+ params: { access_token: accessToken, type: 'thumb' },
127
+ headers: form.getHeaders()
128
+ });
129
+ const data = response.data;
130
+ assertWechatResponse(data, '上传封面失败');
131
+ if (!data.media_id) {
132
+ throw new Error('微信未返回封面 media_id');
133
+ }
134
+ return {
135
+ mediaId: String(data.media_id),
136
+ url: typeof data.url === 'string' ? String(data.url).split('?')[0] : undefined
137
+ };
138
+ }
139
+ async createDraft(payload) {
140
+ const accessToken = await this.getAccessToken();
141
+ const response = await http.post('https://api.weixin.qq.com/cgi-bin/draft/add', {
142
+ articles: [
143
+ {
144
+ title: payload.title,
145
+ author: payload.author,
146
+ digest: payload.digest,
147
+ content: payload.content,
148
+ thumb_media_id: payload.thumbMediaId,
149
+ need_open_comment: payload.enableComment ? 1 : 0,
150
+ only_fans_can_comment: 0
151
+ }
152
+ ]
153
+ }, {
154
+ params: { access_token: accessToken },
155
+ headers: { 'Content-Type': 'application/json; charset=utf-8' }
156
+ });
157
+ const data = response.data;
158
+ assertWechatResponse(data, '创建草稿失败');
159
+ return data;
160
+ }
161
+ }
162
+ exports.WechatClient = WechatClient;
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getUserConfigPath = getUserConfigPath;
7
+ exports.loadConfig = loadConfig;
8
+ exports.saveUserConfig = saveUserConfig;
9
+ exports.clearUserConfig = clearUserConfig;
10
+ exports.maskSecret = maskSecret;
11
+ const promises_1 = require("node:fs/promises");
12
+ const node_os_1 = __importDefault(require("node:os"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const themes_1 = require("../core/themes");
15
+ const CONFIG_DIR = node_path_1.default.join(node_os_1.default.homedir(), '.config', 'wxgzh');
16
+ const USER_CONFIG_PATH = node_path_1.default.join(CONFIG_DIR, 'wxgzh.json');
17
+ function getUserConfigPath() {
18
+ return USER_CONFIG_PATH;
19
+ }
20
+ async function pathExists(filePath) {
21
+ try {
22
+ await (0, promises_1.access)(filePath);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ async function readJson(filePath) {
30
+ if (!(await pathExists(filePath))) {
31
+ return {};
32
+ }
33
+ const raw = await (0, promises_1.readFile)(filePath, 'utf8');
34
+ return JSON.parse(raw);
35
+ }
36
+ function sanitizeConfig(input) {
37
+ const output = {};
38
+ if (typeof input.appid === 'string' && input.appid.trim()) {
39
+ output.appid = input.appid.trim();
40
+ }
41
+ if (typeof input.appsecret === 'string' && input.appsecret.trim()) {
42
+ output.appsecret = input.appsecret.trim();
43
+ }
44
+ if (typeof input.author === 'string' && input.author.trim()) {
45
+ output.author = input.author.trim();
46
+ }
47
+ if (typeof input.defaultTheme === 'string' && input.defaultTheme.trim()) {
48
+ output.defaultTheme = (0, themes_1.assertThemeExists)(input.defaultTheme.trim());
49
+ }
50
+ if (typeof input.enableComment === 'boolean') {
51
+ output.enableComment = input.enableComment;
52
+ }
53
+ return output;
54
+ }
55
+ function envConfig() {
56
+ return sanitizeConfig({
57
+ appid: process.env.WX_APPID,
58
+ appsecret: process.env.WX_APPSECRET
59
+ });
60
+ }
61
+ async function loadConfig() {
62
+ const env = envConfig();
63
+ const user = sanitizeConfig(await readJson(getUserConfigPath()));
64
+ return {
65
+ ...env,
66
+ ...user
67
+ };
68
+ }
69
+ async function saveUserConfig(patch) {
70
+ await (0, promises_1.mkdir)(CONFIG_DIR, { recursive: true });
71
+ const current = sanitizeConfig(await readJson(getUserConfigPath()));
72
+ const next = sanitizeConfig({ ...current, ...patch });
73
+ await (0, promises_1.writeFile)(getUserConfigPath(), `${JSON.stringify(next, null, 2)}\n`, 'utf8');
74
+ return next;
75
+ }
76
+ async function clearUserConfig() {
77
+ if (await pathExists(getUserConfigPath())) {
78
+ await (0, promises_1.rm)(getUserConfigPath(), { force: true });
79
+ }
80
+ }
81
+ function maskSecret(secret) {
82
+ if (!secret) {
83
+ return undefined;
84
+ }
85
+ if (secret.length <= 8) {
86
+ return '*'.repeat(secret.length);
87
+ }
88
+ return `${secret.slice(0, 4)}${'*'.repeat(secret.length - 8)}${secret.slice(-4)}`;
89
+ }