@lyhue1991/wxgzh 0.2.0 → 0.2.2
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/README.md +84 -9
- package/dist/cli/config.js +74 -22
- package/dist/cli/fix.js +10 -7
- package/dist/cli/index.js +6 -5
- package/dist/cli/md2html.js +2 -1
- package/dist/cli/publish.js +6 -6
- package/dist/core/converter.js +52 -1
- package/dist/core/parser.js +3 -0
- package/dist/utils/config.js +119 -12
- package/package.json +1 -1
- package/spec.md +67 -29
- package/styles/black.css +82 -98
- package/styles/brown.css +69 -105
- package/styles/default.css +559 -203
- package/styles/green.css +1 -1
- package/styles/orange.css +2 -2
- package/styles/red.css +1 -1
- package/styles/yellow.css +496 -81
package/README.md
CHANGED
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
<mark>安装此SKILL</mark>
|
|
8
8
|
|
|
9
|
+
```bash
|
|
10
|
+
#for openclaw
|
|
11
|
+
npx clawhub@latest install wxgzh
|
|
9
12
|
```
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
#for opencode/claudecode/cursor/openclaw/...
|
|
10
16
|
npx skills add lyhue1991/wxgzh
|
|
11
17
|
```
|
|
12
18
|
|
|
@@ -40,7 +46,7 @@ npm install -g @lyhue1991/wxgzh
|
|
|
40
46
|
* step2: 一键配置 (提前准备好微信公众号AppID和 AppSecret , 并配置好IP白名单)
|
|
41
47
|
|
|
42
48
|
```bash
|
|
43
|
-
wxgzh config --appid 你的AppID --appsecret 你的AppSecret
|
|
49
|
+
wxgzh config --account 公众号名称1 --appid 你的AppID --appsecret 你的AppSecret
|
|
44
50
|
|
|
45
51
|
```
|
|
46
52
|
|
|
@@ -67,24 +73,93 @@ wxgzh article.md --author 文章作者姓名
|
|
|
67
73
|
前往微信公众号公众平台,使用已认证的公众号管理员账号登录:
|
|
68
74
|
|
|
69
75
|
1. 打开公众号后台
|
|
70
|
-
2.
|
|
71
|
-
3.
|
|
76
|
+
2. 进入"设置与开发"
|
|
77
|
+
3. 找到"开发接口管理"
|
|
72
78
|
4. 查看并复制 `AppID` 和 `AppSecret`
|
|
73
79
|
|
|
74
|
-
|
|
80
|
+
拿到后,执行(**必须同时指定公众号账号名**):
|
|
75
81
|
|
|
76
82
|
```bash
|
|
77
|
-
wxgzh config --appid 你的AppID --appsecret 你的AppSecret
|
|
83
|
+
wxgzh config --account 公众号名称1 --appid 你的AppID --appsecret 你的AppSecret
|
|
78
84
|
```
|
|
79
85
|
|
|
80
86
|
也可以同时设置默认作者、默认主题:
|
|
81
87
|
|
|
82
88
|
```bash
|
|
83
|
-
wxgzh config --appid 你的AppID --appsecret 你的AppSecret --author "wxgzh" --default-theme blue
|
|
89
|
+
wxgzh config --account 公众号名称1 --appid 你的AppID --appsecret 你的AppSecret --author "wxgzh" --default-theme blue
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 2. 如何管理多个公众号账号
|
|
93
|
+
|
|
94
|
+
`wxgzh` 支持同时管理多个公众号账号,适合有多个公众号或需要在不同账号间切换的场景。
|
|
95
|
+
|
|
96
|
+
**添加多个账号:**
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# 主账号
|
|
100
|
+
wxgzh config --account 公众号名称1 --appid 你的AppID --appsecret 你的AppSecret
|
|
101
|
+
|
|
102
|
+
# 备用账号
|
|
103
|
+
wxgzh config --account 公众号名称2 --appid 另一个AppID --appsecret 另一个AppSecret
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**设置默认账号:**
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
wxgzh config --set-default-account 公众号名称1
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**查看已配置的账号:**
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
wxgzh config --list-accounts
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**发布时指定账号:**
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# 使用公众号名称1发布
|
|
122
|
+
wxgzh article.md --account 公众号名称1
|
|
123
|
+
|
|
124
|
+
# 使用公众号名称2发布
|
|
125
|
+
wxgzh article.md --account 公众号名称2
|
|
126
|
+
|
|
127
|
+
# 不指定则使用默认账号
|
|
128
|
+
wxgzh article.md
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**账号选择优先级:**
|
|
132
|
+
|
|
133
|
+
命令行 `--account` > front matter 中的 `account` 字段 > 环境变量 `WX_ACCOUNT` > 用户配置的默认账号
|
|
134
|
+
|
|
135
|
+
**使用 front matter 指定账号:**
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
---
|
|
139
|
+
title: 我的文章
|
|
140
|
+
author: wxgzh
|
|
141
|
+
account: 公众号名称2
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
# 文章正文
|
|
84
145
|
```
|
|
85
146
|
|
|
147
|
+
**使用环境变量(适用于 CI/CD 场景):**
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
export WX_ACCOUNT=公众号名称2
|
|
151
|
+
export WX_APPID=你的AppID
|
|
152
|
+
export WX_APPSECRET=你的AppSecret
|
|
153
|
+
wxgzh article.md
|
|
154
|
+
```
|
|
86
155
|
|
|
87
|
-
|
|
156
|
+
**删除账号配置:**
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
wxgzh config --remove-account 公众号名称2
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 3. 如何把本机IP加入公众号 IP 白名单
|
|
88
163
|
|
|
89
164
|
> [!IMPORTANT]
|
|
90
165
|
> 调用微信公众号接口前,除了配置 `appid` 和 `appsecret`,还要把当前机器的公网 IP 加到公众号后台的 IP 白名单。
|
|
@@ -100,7 +175,7 @@ wxgzh config --appid 你的AppID --appsecret 你的AppSecret --author "wxgzh" --
|
|
|
100
175
|
查看本机公网 IP 的方法:浏览器访问 https://ip.sb/ 查看 Address
|
|
101
176
|
|
|
102
177
|
|
|
103
|
-
###
|
|
178
|
+
### 4. 如何查看当前配置
|
|
104
179
|
|
|
105
180
|
```bash
|
|
106
181
|
wxgzh config --list
|
|
@@ -391,6 +466,7 @@ enableComment: true
|
|
|
391
466
|
- `author`:作者
|
|
392
467
|
- `digest`:摘要
|
|
393
468
|
- `theme`:主题名
|
|
469
|
+
- `account`:目标公众号账号名
|
|
394
470
|
- `cover`:封面图路径
|
|
395
471
|
- `enableComment`:是否开启评论
|
|
396
472
|
|
|
@@ -424,4 +500,3 @@ wxgzh fix .wxgzh/article.html --no-upload
|
|
|
424
500
|
wxgzh cover --title "我的文章" --to .wxgzh/cover.jpg
|
|
425
501
|
wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg
|
|
426
502
|
```
|
|
427
|
-
|
package/dist/cli/config.js
CHANGED
|
@@ -6,57 +6,109 @@ const config_1 = require("../utils/config");
|
|
|
6
6
|
const logger_1 = require("../utils/logger");
|
|
7
7
|
function listConfig(config) {
|
|
8
8
|
const payload = {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
account: config.account,
|
|
10
|
+
currentAccount: config.currentAccount,
|
|
11
|
+
availableAccounts: Object.keys(config.accounts),
|
|
12
|
+
appid: config.appid,
|
|
13
|
+
appsecret: (0, config_1.maskSecret)(config.appsecret),
|
|
14
|
+
author: config.author,
|
|
15
|
+
defaultTheme: config.defaultTheme,
|
|
16
|
+
enableComment: config.enableComment
|
|
11
17
|
};
|
|
12
18
|
console.log(JSON.stringify(payload, null, 2));
|
|
13
19
|
(0, logger_1.info)(`用户级配置路径: ${(0, config_1.getUserConfigPath)()}`);
|
|
14
20
|
}
|
|
21
|
+
function listAccounts(config) {
|
|
22
|
+
const accountNames = Object.keys(config.accounts);
|
|
23
|
+
if (accountNames.length === 0) {
|
|
24
|
+
(0, logger_1.info)('当前还没有配置任何公众号账号');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const accountName of accountNames) {
|
|
28
|
+
console.log(accountName === config.currentAccount ? `${accountName} (default)` : accountName);
|
|
29
|
+
}
|
|
30
|
+
(0, logger_1.info)(`用户级配置路径: ${(0, config_1.getUserConfigPath)()}`);
|
|
31
|
+
}
|
|
15
32
|
function registerConfigCommand(program) {
|
|
16
33
|
program
|
|
17
34
|
.command('config')
|
|
18
35
|
.description('查看或更新 wxgzh 的用户级默认配置')
|
|
19
36
|
.usage('[options]')
|
|
20
37
|
.helpOption('-h, --help', '查看帮助')
|
|
21
|
-
.option('--
|
|
22
|
-
.option('--
|
|
23
|
-
.option('--
|
|
24
|
-
.option('--
|
|
25
|
-
.option('--
|
|
26
|
-
.option('--
|
|
38
|
+
.option('--account <name>', '指定要查看或更新的公众号账号名;设置 appid/appsecret 时必填')
|
|
39
|
+
.option('--appid <appid>', '为目标账号设置微信公众号 appid')
|
|
40
|
+
.option('--appsecret <appsecret>', '为目标账号设置微信公众号 appsecret')
|
|
41
|
+
.option('--author <author>', '为目标账号设置默认作者名')
|
|
42
|
+
.option('--default-theme <theme>', `为目标账号设置默认主题,可用值:${(0, themes_1.listAvailableThemes)().join(', ')}`)
|
|
43
|
+
.option('--enable-comment', '把目标账号的默认评论开关设为开启')
|
|
44
|
+
.option('--disable-comment', '把目标账号的默认评论开关设为关闭')
|
|
45
|
+
.option('--set-default-account <name>', '设置默认公众号账号')
|
|
46
|
+
.option('--remove-account <name>', '删除指定公众号账号配置')
|
|
27
47
|
.option('--list', '查看当前生效配置')
|
|
48
|
+
.option('--list-accounts', '查看全部已配置账号')
|
|
28
49
|
.option('--list-themes', '查看全部可用主题名')
|
|
29
50
|
.option('--clear', '清空用户级配置文件')
|
|
30
|
-
.addHelpText('after', '\n示例:\n $ wxgzh config --list\n $ wxgzh config --list-
|
|
51
|
+
.addHelpText('after', '\n示例:\n $ wxgzh config --list\n $ wxgzh config --list-accounts\n $ wxgzh config --account 公众号名称1 --appid wx123 --appsecret abc123\n $ wxgzh config --account 公众号名称1 --author "wxgzh" --default-theme blue\n $ wxgzh config --set-default-account 公众号名称1\n\n说明:\n 设置 appid 或 appsecret 时,必须同时通过 --account 指定公众号账号名\n')
|
|
31
52
|
.action(async (options, command) => {
|
|
32
53
|
const mergedOptions = command.optsWithGlobals();
|
|
54
|
+
const patch = {
|
|
55
|
+
...(mergedOptions.appid ? { appid: mergedOptions.appid } : {}),
|
|
56
|
+
...(mergedOptions.appsecret ? { appsecret: mergedOptions.appsecret } : {}),
|
|
57
|
+
...(mergedOptions.author ? { author: mergedOptions.author } : {}),
|
|
58
|
+
...(mergedOptions.defaultTheme ? { defaultTheme: (0, themes_1.assertThemeExists)(mergedOptions.defaultTheme) } : {}),
|
|
59
|
+
...(mergedOptions.enableComment ? { enableComment: true } : {}),
|
|
60
|
+
...(mergedOptions.disableComment ? { enableComment: false } : {})
|
|
61
|
+
};
|
|
33
62
|
if (mergedOptions.clear) {
|
|
34
63
|
await (0, config_1.clearUserConfig)();
|
|
35
64
|
(0, logger_1.success)('已清空用户级配置');
|
|
36
65
|
return;
|
|
37
66
|
}
|
|
38
67
|
if (mergedOptions.list) {
|
|
39
|
-
listConfig(await (0, config_1.loadConfig)());
|
|
68
|
+
listConfig(await (0, config_1.loadConfig)({ account: mergedOptions.account }));
|
|
40
69
|
return;
|
|
41
70
|
}
|
|
42
71
|
if (mergedOptions.listThemes) {
|
|
43
72
|
console.log((0, themes_1.listAvailableThemes)().join('\n'));
|
|
44
73
|
return;
|
|
45
74
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
75
|
+
if (mergedOptions.listAccounts) {
|
|
76
|
+
listAccounts(await (0, config_1.loadUserConfig)());
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (mergedOptions.removeAccount) {
|
|
80
|
+
if (Object.keys(patch).length > 0 || mergedOptions.setDefaultAccount) {
|
|
81
|
+
throw new Error('--remove-account 不能与其他写入类参数同时使用');
|
|
82
|
+
}
|
|
83
|
+
const next = await (0, config_1.removeAccountConfig)(mergedOptions.removeAccount);
|
|
84
|
+
(0, logger_1.success)(`已删除公众号账号 ${mergedOptions.removeAccount}`);
|
|
85
|
+
listAccounts(next);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let selectedAccount = mergedOptions.account;
|
|
89
|
+
const hasCredentialsToSave = mergedOptions.appid || mergedOptions.appsecret;
|
|
90
|
+
if (hasCredentialsToSave && !selectedAccount) {
|
|
91
|
+
throw new Error('配置 appid 或 appsecret 时,必须通过 --account <name> 指定公众号账号名');
|
|
92
|
+
}
|
|
93
|
+
if (Object.keys(patch).length > 0) {
|
|
94
|
+
if (!selectedAccount) {
|
|
95
|
+
const userConfig = await (0, config_1.loadUserConfig)();
|
|
96
|
+
selectedAccount = userConfig.currentAccount;
|
|
97
|
+
}
|
|
98
|
+
if (!selectedAccount) {
|
|
99
|
+
throw new Error('当前没有默认公众号账号,请通过 --account <name> 指定要写入的账号');
|
|
100
|
+
}
|
|
101
|
+
await (0, config_1.saveAccountConfig)(selectedAccount, patch);
|
|
102
|
+
}
|
|
103
|
+
if (mergedOptions.setDefaultAccount) {
|
|
104
|
+
await (0, config_1.setDefaultAccount)(mergedOptions.setDefaultAccount);
|
|
105
|
+
selectedAccount = mergedOptions.setDefaultAccount;
|
|
106
|
+
}
|
|
107
|
+
if (Object.keys(patch).length === 0 && !mergedOptions.setDefaultAccount) {
|
|
108
|
+
listConfig(await (0, config_1.loadConfig)({ account: mergedOptions.account }));
|
|
56
109
|
return;
|
|
57
110
|
}
|
|
58
|
-
const next = await (0, config_1.saveUserConfig)(patch);
|
|
59
111
|
(0, logger_1.success)('用户级配置已更新');
|
|
60
|
-
|
|
112
|
+
listConfig(await (0, config_1.loadConfig)({ account: selectedAccount }));
|
|
61
113
|
});
|
|
62
114
|
}
|
package/dist/cli/fix.js
CHANGED
|
@@ -5,9 +5,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.registerFixCommand = registerFixCommand;
|
|
7
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const converter_1 = require("../core/converter");
|
|
8
9
|
const fixer_1 = require("../core/fixer");
|
|
9
10
|
const wechat_1 = require("../core/wechat");
|
|
10
11
|
const config_1 = require("../utils/config");
|
|
12
|
+
const fs_1 = require("../utils/fs");
|
|
11
13
|
const logger_1 = require("../utils/logger");
|
|
12
14
|
function registerFixCommand(program) {
|
|
13
15
|
program
|
|
@@ -16,19 +18,20 @@ function registerFixCommand(program) {
|
|
|
16
18
|
.usage('<article.html> [options]')
|
|
17
19
|
.helpOption('-h, --help', '查看帮助')
|
|
18
20
|
.argument('<article.html>', '要修复的 HTML 文件路径')
|
|
21
|
+
.option('--account <name>', '指定上传正文图片时使用的公众号账号')
|
|
19
22
|
.option('--no-upload', '只修复 HTML 结构,不上传图片到微信')
|
|
20
23
|
.option('--cdn <url>', '把本地图片路径替换为指定 CDN 地址')
|
|
21
|
-
.addHelpText('after', '\n示例:\n $ wxgzh fix .wxgzh/article.html\n $ wxgzh fix .wxgzh/article.html --no-upload\n $ wxgzh fix .wxgzh/article.html --cdn https://cdn.example.com\n')
|
|
24
|
+
.addHelpText('after', '\n示例:\n $ wxgzh fix .wxgzh/article.html\n $ wxgzh fix .wxgzh/article.html --account 公众号名称1\n $ wxgzh fix .wxgzh/article.html --no-upload\n $ wxgzh fix .wxgzh/article.html --cdn https://cdn.example.com\n')
|
|
22
25
|
.action(async (articlePath, options) => {
|
|
26
|
+
const absoluteArticlePath = node_path_1.default.resolve(articlePath);
|
|
23
27
|
let wechat;
|
|
24
28
|
if (options.upload !== false) {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
wechat = new wechat_1.WechatClient({ appid: config.appid, appsecret: config.appsecret });
|
|
29
|
+
const html = await (0, fs_1.readTextFile)(absoluteArticlePath);
|
|
30
|
+
const metadata = (0, converter_1.readHtmlMetadata)(html);
|
|
31
|
+
const config = await (0, config_1.loadConfig)({ account: options.account ?? metadata.account });
|
|
32
|
+
wechat = new wechat_1.WechatClient((0, config_1.resolveWechatCredentials)(config));
|
|
30
33
|
}
|
|
31
|
-
const result = await (0, fixer_1.fixHtmlFile)(
|
|
34
|
+
const result = await (0, fixer_1.fixHtmlFile)(absoluteArticlePath, {
|
|
32
35
|
upload: options.upload !== false,
|
|
33
36
|
cdn: options.cdn,
|
|
34
37
|
wechat
|
package/dist/cli/index.js
CHANGED
|
@@ -56,6 +56,7 @@ dotenv_1.default.config();
|
|
|
56
56
|
const EXAMPLES_TEXT = [
|
|
57
57
|
'常用示例:',
|
|
58
58
|
' $ wxgzh article.md',
|
|
59
|
+
' $ wxgzh article.md --account 公众号名称1',
|
|
59
60
|
' $ wxgzh article.md --theme blue',
|
|
60
61
|
' $ wxgzh article.md --author "wxgzh" --digest "这是一篇摘要"',
|
|
61
62
|
' $ wxgzh article.md --cover ./cover.jpg',
|
|
@@ -83,25 +84,23 @@ async function runDefaultPipeline(articlePath, options) {
|
|
|
83
84
|
const articleBaseName = node_path_1.default.basename(absoluteArticle, node_path_1.default.extname(absoluteArticle));
|
|
84
85
|
const htmlPath = node_path_1.default.join(outputDir, `${articleBaseName}.html`);
|
|
85
86
|
const coverPath = node_path_1.default.join(outputDir, `${articleBaseName}.cover.jpg`);
|
|
86
|
-
const config = await (0, config_1.loadConfig)();
|
|
87
87
|
const rawMarkdown = await (0, promises_1.readFile)(absoluteArticle, 'utf8');
|
|
88
88
|
const parsed = (0, parser_1.parseMarkdown)(rawMarkdown);
|
|
89
|
+
const config = await (0, config_1.loadConfig)({ account: options.account ?? parsed.metadata.account });
|
|
89
90
|
const title = parsed.metadata.title ?? articleBaseName;
|
|
90
91
|
const author = options.author ?? parsed.metadata.author ?? config.author ?? 'wxgzh';
|
|
91
92
|
const digest = options.digest ?? parsed.metadata.digest ?? '由 wxgzh 自动生成的公众号草稿';
|
|
92
93
|
const autoCoverDisabled = options.cover === false;
|
|
93
94
|
const resolvedTheme = options.theme ?? parsed.metadata.theme ?? config.defaultTheme;
|
|
94
95
|
await (0, md2html_1.convertMarkdownFile)(absoluteArticle, htmlPath, {
|
|
96
|
+
account: config.account,
|
|
95
97
|
theme: resolvedTheme ? (0, themes_1.assertThemeExists)(resolvedTheme) : undefined,
|
|
96
98
|
author
|
|
97
99
|
});
|
|
98
100
|
(0, logger_1.info)(`已生成 HTML: ${htmlPath}`);
|
|
99
101
|
const { fixHtmlFile } = await Promise.resolve().then(() => __importStar(require('../core/fixer')));
|
|
100
102
|
const { WechatClient } = await Promise.resolve().then(() => __importStar(require('../core/wechat')));
|
|
101
|
-
|
|
102
|
-
throw new Error('未配置微信认证信息,请先运行: wxgzh config --appid xxx --appsecret yyy');
|
|
103
|
-
}
|
|
104
|
-
const wechat = new WechatClient({ appid: config.appid, appsecret: config.appsecret });
|
|
103
|
+
const wechat = new WechatClient((0, config_1.resolveWechatCredentials)(config));
|
|
105
104
|
await fixHtmlFile(htmlPath, { upload: true, wechat });
|
|
106
105
|
(0, logger_1.info)(`已修复并上传正文图片: ${htmlPath}`);
|
|
107
106
|
let finalCoverPath = resolveCoverValue(options.cover) ?? parsed.metadata.cover;
|
|
@@ -118,6 +117,7 @@ async function runDefaultPipeline(articlePath, options) {
|
|
|
118
117
|
throw new Error('当前未提供封面图。请通过 --cover 指定,或去掉 --no-cover 以自动生成简易封面。');
|
|
119
118
|
}
|
|
120
119
|
const result = await (0, publish_1.publishDraft)(htmlPath, finalCoverPath, {
|
|
120
|
+
account: config.account,
|
|
121
121
|
title,
|
|
122
122
|
author,
|
|
123
123
|
digest,
|
|
@@ -137,6 +137,7 @@ async function main() {
|
|
|
137
137
|
.argument('[article]', 'Markdown 文件路径;传入后会自动执行 md2html -> fix -> cover -> publish')
|
|
138
138
|
.option('--theme <theme>', `指定主题名,可用值:${(0, themes_1.listAvailableThemes)().join(', ')}`)
|
|
139
139
|
.option('--author <author>', '覆盖作者名;未传时优先使用 front matter 或 config 中的 author')
|
|
140
|
+
.option('--account <name>', '指定本次发布使用的公众号账号')
|
|
140
141
|
.option('--cover <cover.jpg>', '指定现成封面图;未传时默认自动生成')
|
|
141
142
|
.option('--no-cover', '禁用自动封面生成;适合 front matter 已配置 cover 的场景')
|
|
142
143
|
.option('--digest <digest>', '覆盖文章摘要;未传时自动从正文提取')
|
package/dist/cli/md2html.js
CHANGED
|
@@ -14,13 +14,14 @@ const config_1 = require("../utils/config");
|
|
|
14
14
|
const fs_1 = require("../utils/fs");
|
|
15
15
|
const logger_1 = require("../utils/logger");
|
|
16
16
|
async function convertMarkdownFile(inputPath, outputPath, options) {
|
|
17
|
-
const config = await (0, config_1.loadConfig)();
|
|
18
17
|
const absoluteInputPath = node_path_1.default.resolve(inputPath);
|
|
19
18
|
const raw = await (0, promises_1.readFile)(absoluteInputPath, 'utf8');
|
|
20
19
|
const parsed = (0, parser_1.parseMarkdown)(raw);
|
|
20
|
+
const config = await (0, config_1.loadConfig)({ account: options?.account ?? parsed.metadata.account });
|
|
21
21
|
const resolvedTheme = options?.theme ?? parsed.metadata.theme ?? config.defaultTheme ?? 'default';
|
|
22
22
|
const html = await (0, converter_1.renderMarkdownToHtml)(parsed.body, {
|
|
23
23
|
...parsed.metadata,
|
|
24
|
+
account: options?.account ?? parsed.metadata.account ?? config.account,
|
|
24
25
|
theme: (0, themes_1.assertThemeExists)(resolvedTheme),
|
|
25
26
|
author: options?.author ?? parsed.metadata.author ?? config.author,
|
|
26
27
|
sourceDir: node_path_1.default.dirname(absoluteInputPath)
|
package/dist/cli/publish.js
CHANGED
|
@@ -12,14 +12,12 @@ const config_1 = require("../utils/config");
|
|
|
12
12
|
const fs_1 = require("../utils/fs");
|
|
13
13
|
const logger_1 = require("../utils/logger");
|
|
14
14
|
async function publishDraft(articlePath, coverPath, options) {
|
|
15
|
-
const config = await (0, config_1.loadConfig)();
|
|
16
|
-
if (!config.appid || !config.appsecret) {
|
|
17
|
-
(0, logger_1.error)('未配置微信认证信息,请先运行: wxgzh config --appid xxx --appsecret yyy');
|
|
18
|
-
}
|
|
19
15
|
const html = await (0, fs_1.readTextFile)(node_path_1.default.resolve(articlePath));
|
|
20
16
|
const metadata = (0, converter_1.readHtmlMetadata)(html);
|
|
17
|
+
const config = await (0, config_1.loadConfig)({ account: options?.account ?? metadata.account });
|
|
18
|
+
const credentials = (0, config_1.resolveWechatCredentials)(config);
|
|
21
19
|
const publishableContent = (0, converter_1.extractPublishableContent)(html);
|
|
22
|
-
const client = new wechat_1.WechatClient(
|
|
20
|
+
const client = new wechat_1.WechatClient(credentials);
|
|
23
21
|
const cover = await client.uploadCoverImage(node_path_1.default.resolve(coverPath));
|
|
24
22
|
return client.createDraft({
|
|
25
23
|
title: options?.title ?? metadata.title ?? node_path_1.default.basename(articlePath, node_path_1.default.extname(articlePath)),
|
|
@@ -38,13 +36,15 @@ function registerPublishCommand(program) {
|
|
|
38
36
|
.helpOption('-h, --help', '查看帮助')
|
|
39
37
|
.requiredOption('--article <article.html>', '已处理好的 HTML 正文路径')
|
|
40
38
|
.requiredOption('--cover <cover.jpg>', '封面图路径')
|
|
39
|
+
.option('--account <name>', '指定本次发布使用的公众号账号')
|
|
41
40
|
.option('--title <title>', '覆盖文章标题')
|
|
42
41
|
.option('--author <author>', '覆盖作者名')
|
|
43
42
|
.option('--digest <digest>', '覆盖文章摘要')
|
|
44
43
|
.option('--enable-comment', '为这篇草稿开启评论')
|
|
45
|
-
.addHelpText('after', '\n示例:\n $ wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg\n $ wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg --title "新的标题"\n')
|
|
44
|
+
.addHelpText('after', '\n示例:\n $ wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg\n $ wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg --account 公众号名称1\n $ wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg --title "新的标题"\n')
|
|
46
45
|
.action(async (options) => {
|
|
47
46
|
const result = await publishDraft(options.article, options.cover, {
|
|
47
|
+
account: options.account,
|
|
48
48
|
title: options.title,
|
|
49
49
|
author: options.author,
|
|
50
50
|
digest: options.digest,
|
package/dist/core/converter.js
CHANGED
|
@@ -478,12 +478,30 @@ function normalizeListItems($) {
|
|
|
478
478
|
});
|
|
479
479
|
const firstChild = childElements[0];
|
|
480
480
|
if (!hasBlockChild && isTagNode(firstChild, 'span') && childElements.length === 1) {
|
|
481
|
+
$(firstChild).attr('data-wxgzh', 'li-inline');
|
|
481
482
|
return;
|
|
482
483
|
}
|
|
483
484
|
if (hasBlockChild) {
|
|
485
|
+
const inlineNodes = [];
|
|
486
|
+
for (const node of childElements) {
|
|
487
|
+
if (node.type === 'tag') {
|
|
488
|
+
const tagName = node.tagName.toLowerCase();
|
|
489
|
+
if (['p', 'ul', 'ol', 'section', 'blockquote', 'pre', 'table'].includes(tagName)) {
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
inlineNodes.push(node);
|
|
494
|
+
}
|
|
495
|
+
if (inlineNodes.length > 0) {
|
|
496
|
+
const inlineHtml = inlineNodes.map((node) => $.html(node)).join('');
|
|
497
|
+
for (const node of inlineNodes) {
|
|
498
|
+
$(node).remove();
|
|
499
|
+
}
|
|
500
|
+
$item.prepend(`<span data-wxgzh="li-inline">${inlineHtml}</span>`);
|
|
501
|
+
}
|
|
484
502
|
return;
|
|
485
503
|
}
|
|
486
|
-
$item.html(`<span>${$item.html() ?? ''}</span>`);
|
|
504
|
+
$item.html(`<span data-wxgzh="li-inline">${$item.html() ?? ''}</span>`);
|
|
487
505
|
});
|
|
488
506
|
}
|
|
489
507
|
function normalizeImages($) {
|
|
@@ -700,6 +718,38 @@ function normalizeInlinedListStyles($) {
|
|
|
700
718
|
$item.removeAttr('style');
|
|
701
719
|
});
|
|
702
720
|
}
|
|
721
|
+
function decorateUnorderedListMarkers($) {
|
|
722
|
+
$('ul').each((_, element) => {
|
|
723
|
+
const $list = $(element);
|
|
724
|
+
const depth = $list.parents('ul').length;
|
|
725
|
+
const paddingLeft = depth === 0 ? '0 !important' : '1.2em !important';
|
|
726
|
+
$list.attr('style', setInlineStyleProperty($list.attr('style'), 'list-style', 'none !important'));
|
|
727
|
+
$list.attr('style', setInlineStyleProperty($list.attr('style'), 'margin-left', '0 !important'));
|
|
728
|
+
$list.attr('style', setInlineStyleProperty($list.attr('style'), 'padding-left', paddingLeft));
|
|
729
|
+
});
|
|
730
|
+
$('ul > li').each((_, element) => {
|
|
731
|
+
const $item = $(element);
|
|
732
|
+
const hasCheckbox = $item.children('input[type="checkbox"]').length > 0;
|
|
733
|
+
if (hasCheckbox) {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
$item.children('span[data-wxgzh="ul-marker"]').remove();
|
|
737
|
+
$item.attr('style', setInlineStyleProperty($item.attr('style'), 'list-style', 'none !important'));
|
|
738
|
+
$item.attr('style', setInlineStyleProperty($item.attr('style'), 'margin-left', '0 !important'));
|
|
739
|
+
const $content = $item.children('span[data-wxgzh="li-inline"]').first();
|
|
740
|
+
if ($content.length === 0) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
$content.children('span[data-wxgzh="ul-marker"]').remove();
|
|
744
|
+
$content.prepend('<span data-wxgzh="ul-marker">● </span>');
|
|
745
|
+
const $marker = $content.children('span[data-wxgzh="ul-marker"]').first();
|
|
746
|
+
$marker.attr('style', setInlineStyleProperty($marker.attr('style'), 'display', 'inline !important'));
|
|
747
|
+
$marker.attr('style', setInlineStyleProperty($marker.attr('style'), 'line-height', 'inherit !important'));
|
|
748
|
+
$marker.attr('style', setInlineStyleProperty($marker.attr('style'), 'font-size', '1.1em !important'));
|
|
749
|
+
$marker.attr('style', setInlineStyleProperty($marker.attr('style'), 'font-weight', '700 !important'));
|
|
750
|
+
$marker.attr('style', setInlineStyleProperty($marker.attr('style'), 'color', '#000000 !important'));
|
|
751
|
+
});
|
|
752
|
+
}
|
|
703
753
|
function removeWechatUnsafeListWhitespace($) {
|
|
704
754
|
$('ol, ul').each((_, element) => {
|
|
705
755
|
$(element)
|
|
@@ -807,6 +857,7 @@ function cleanInlinedHtml(html) {
|
|
|
807
857
|
$element.html(renderWechatFriendlyCodeHtml($element.contents().toArray()));
|
|
808
858
|
});
|
|
809
859
|
normalizeInlinedListStyles($);
|
|
860
|
+
decorateUnorderedListMarkers($);
|
|
810
861
|
removeWechatUnsafeListWhitespace($);
|
|
811
862
|
normalizeWechatCodeBlocks($);
|
|
812
863
|
const $article = $('body > #write > article').first();
|
package/dist/core/parser.js
CHANGED
|
@@ -80,6 +80,9 @@ function normalizeMetadata(data) {
|
|
|
80
80
|
if (typeof data.theme === 'string' && data.theme.trim()) {
|
|
81
81
|
metadata.theme = data.theme.trim();
|
|
82
82
|
}
|
|
83
|
+
if (typeof data.account === 'string' && data.account.trim()) {
|
|
84
|
+
metadata.account = data.account.trim();
|
|
85
|
+
}
|
|
83
86
|
if (typeof data.cover === 'string' && data.cover.trim()) {
|
|
84
87
|
metadata.cover = data.cover.trim();
|
|
85
88
|
}
|