@lyhue1991/wxgzh 0.2.0 → 0.2.1

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 CHANGED
@@ -40,7 +40,7 @@ npm install -g @lyhue1991/wxgzh
40
40
  * step2: 一键配置 (提前准备好微信公众号AppID和 AppSecret , 并配置好IP白名单)
41
41
 
42
42
  ```bash
43
- wxgzh config --appid 你的AppID --appsecret 你的AppSecret
43
+ wxgzh config --account 公众号名称1 --appid 你的AppID --appsecret 你的AppSecret
44
44
 
45
45
  ```
46
46
 
@@ -67,24 +67,93 @@ wxgzh article.md --author 文章作者姓名
67
67
  前往微信公众号公众平台,使用已认证的公众号管理员账号登录:
68
68
 
69
69
  1. 打开公众号后台
70
- 2. 进入“设置与开发”
71
- 3. 找到“开发接口管理”
70
+ 2. 进入"设置与开发"
71
+ 3. 找到"开发接口管理"
72
72
  4. 查看并复制 `AppID` 和 `AppSecret`
73
73
 
74
- 拿到后,执行:
74
+ 拿到后,执行(**必须同时指定公众号账号名**):
75
75
 
76
76
  ```bash
77
- wxgzh config --appid 你的AppID --appsecret 你的AppSecret
77
+ wxgzh config --account 公众号名称1 --appid 你的AppID --appsecret 你的AppSecret
78
78
  ```
79
79
 
80
80
  也可以同时设置默认作者、默认主题:
81
81
 
82
82
  ```bash
83
- wxgzh config --appid 你的AppID --appsecret 你的AppSecret --author "wxgzh" --default-theme blue
83
+ wxgzh config --account 公众号名称1 --appid 你的AppID --appsecret 你的AppSecret --author "wxgzh" --default-theme blue
84
84
  ```
85
85
 
86
+ ### 2. 如何管理多个公众号账号
86
87
 
87
- ### 2. 如何把本机IP加入公众号 IP 白名单
88
+ `wxgzh` 支持同时管理多个公众号账号,适合有多个公众号或需要在不同账号间切换的场景。
89
+
90
+ **添加多个账号:**
91
+
92
+ ```bash
93
+ # 主账号
94
+ wxgzh config --account 公众号名称1 --appid 你的AppID --appsecret 你的AppSecret
95
+
96
+ # 备用账号
97
+ wxgzh config --account 公众号名称2 --appid 另一个AppID --appsecret 另一个AppSecret
98
+ ```
99
+
100
+ **设置默认账号:**
101
+
102
+ ```bash
103
+ wxgzh config --set-default-account 公众号名称1
104
+ ```
105
+
106
+ **查看已配置的账号:**
107
+
108
+ ```bash
109
+ wxgzh config --list-accounts
110
+ ```
111
+
112
+ **发布时指定账号:**
113
+
114
+ ```bash
115
+ # 使用公众号名称1发布
116
+ wxgzh article.md --account 公众号名称1
117
+
118
+ # 使用公众号名称2发布
119
+ wxgzh article.md --account 公众号名称2
120
+
121
+ # 不指定则使用默认账号
122
+ wxgzh article.md
123
+ ```
124
+
125
+ **账号选择优先级:**
126
+
127
+ 命令行 `--account` > front matter 中的 `account` 字段 > 环境变量 `WX_ACCOUNT` > 用户配置的默认账号
128
+
129
+ **使用 front matter 指定账号:**
130
+
131
+ ```yaml
132
+ ---
133
+ title: 我的文章
134
+ author: wxgzh
135
+ account: 公众号名称2
136
+ ---
137
+
138
+ # 文章正文
139
+ ```
140
+
141
+ **使用环境变量(适用于 CI/CD 场景):**
142
+
143
+ ```bash
144
+ export WX_ACCOUNT=公众号名称2
145
+ export WX_APPID=你的AppID
146
+ export WX_APPSECRET=你的AppSecret
147
+ wxgzh article.md
148
+ ```
149
+
150
+ **删除账号配置:**
151
+
152
+ ```bash
153
+ wxgzh config --remove-account 公众号名称2
154
+ ```
155
+
156
+ ### 3. 如何把本机IP加入公众号 IP 白名单
88
157
 
89
158
  > [!IMPORTANT]
90
159
  > 调用微信公众号接口前,除了配置 `appid` 和 `appsecret`,还要把当前机器的公网 IP 加到公众号后台的 IP 白名单。
@@ -100,7 +169,7 @@ wxgzh config --appid 你的AppID --appsecret 你的AppSecret --author "wxgzh" --
100
169
  查看本机公网 IP 的方法:浏览器访问 https://ip.sb/ 查看 Address
101
170
 
102
171
 
103
- ### 3. 如何查看当前配置
172
+ ### 4. 如何查看当前配置
104
173
 
105
174
  ```bash
106
175
  wxgzh config --list
@@ -391,6 +460,7 @@ enableComment: true
391
460
  - `author`:作者
392
461
  - `digest`:摘要
393
462
  - `theme`:主题名
463
+ - `account`:目标公众号账号名
394
464
  - `cover`:封面图路径
395
465
  - `enableComment`:是否开启评论
396
466
 
@@ -424,4 +494,3 @@ wxgzh fix .wxgzh/article.html --no-upload
424
494
  wxgzh cover --title "我的文章" --to .wxgzh/cover.jpg
425
495
  wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg
426
496
  ```
427
-
@@ -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
- ...config,
10
- appsecret: (0, config_1.maskSecret)(config.appsecret)
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('--appid <appid>', '设置微信公众号 appid')
22
- .option('--appsecret <appsecret>', '设置微信公众号 appsecret')
23
- .option('--author <author>', '设置默认作者名')
24
- .option('--default-theme <theme>', `设置默认主题,可用值:${(0, themes_1.listAvailableThemes)().join(', ')}`)
25
- .option('--enable-comment', '把默认评论开关设为开启')
26
- .option('--disable-comment', '把默认评论开关设为关闭')
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-themes\n $ wxgzh config --author "wxgzh" --default-theme blue\n $ wxgzh config --enable-comment\n')
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
- const patch = {
47
- ...(mergedOptions.appid ? { appid: mergedOptions.appid } : {}),
48
- ...(mergedOptions.appsecret ? { appsecret: mergedOptions.appsecret } : {}),
49
- ...(mergedOptions.author ? { author: mergedOptions.author } : {}),
50
- ...(mergedOptions.defaultTheme ? { defaultTheme: (0, themes_1.assertThemeExists)(mergedOptions.defaultTheme) } : {}),
51
- ...(mergedOptions.enableComment ? { enableComment: true } : {}),
52
- ...(mergedOptions.disableComment ? { enableComment: false } : {})
53
- };
54
- if (Object.keys(patch).length === 0) {
55
- listConfig(await (0, config_1.loadConfig)());
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
- console.log(JSON.stringify({ ...next, appsecret: (0, config_1.maskSecret)(next.appsecret) }, null, 2));
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 config = await (0, config_1.loadConfig)();
26
- if (!config.appid || !config.appsecret) {
27
- (0, logger_1.error)('未配置微信认证信息,请先运行: wxgzh config --appid xxx --appsecret yyy');
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)(node_path_1.default.resolve(articlePath), {
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
- if (!config.appid || !config.appsecret) {
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>', '覆盖文章摘要;未传时自动从正文提取')
@@ -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)
@@ -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({ appid: config.appid, appsecret: config.appsecret });
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,
@@ -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
  }
@@ -4,9 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getUserConfigPath = getUserConfigPath;
7
+ exports.loadUserConfig = loadUserConfig;
7
8
  exports.loadConfig = loadConfig;
8
- exports.saveUserConfig = saveUserConfig;
9
+ exports.saveAccountConfig = saveAccountConfig;
10
+ exports.setDefaultAccount = setDefaultAccount;
11
+ exports.removeAccountConfig = removeAccountConfig;
9
12
  exports.clearUserConfig = clearUserConfig;
13
+ exports.resolveWechatCredentials = resolveWechatCredentials;
10
14
  exports.maskSecret = maskSecret;
11
15
  const promises_1 = require("node:fs/promises");
12
16
  const node_os_1 = __importDefault(require("node:os"));
@@ -33,7 +37,14 @@ async function readJson(filePath) {
33
37
  const raw = await (0, promises_1.readFile)(filePath, 'utf8');
34
38
  return JSON.parse(raw);
35
39
  }
36
- function sanitizeConfig(input) {
40
+ function isRecord(value) {
41
+ return typeof value === 'object' && value !== null;
42
+ }
43
+ function sanitizeAccountName(value) {
44
+ const trimmed = value?.trim();
45
+ return trimmed ? trimmed : undefined;
46
+ }
47
+ function sanitizeAccountConfig(input) {
37
48
  const output = {};
38
49
  if (typeof input.appid === 'string' && input.appid.trim()) {
39
50
  output.appid = input.appid.trim();
@@ -52,32 +63,128 @@ function sanitizeConfig(input) {
52
63
  }
53
64
  return output;
54
65
  }
66
+ function sanitizeUserConfig(input) {
67
+ const accounts = {};
68
+ if (isRecord(input.accounts)) {
69
+ for (const [accountName, accountValue] of Object.entries(input.accounts)) {
70
+ const sanitizedName = sanitizeAccountName(accountName);
71
+ if (!sanitizedName || !isRecord(accountValue)) {
72
+ continue;
73
+ }
74
+ const sanitizedAccount = sanitizeAccountConfig(accountValue);
75
+ if (Object.keys(sanitizedAccount).length > 0) {
76
+ accounts[sanitizedName] = sanitizedAccount;
77
+ }
78
+ }
79
+ }
80
+ const legacyAccount = sanitizeAccountConfig(input);
81
+ if (Object.keys(accounts).length === 0 && Object.keys(legacyAccount).length > 0) {
82
+ accounts.default = legacyAccount;
83
+ }
84
+ const currentAccount = sanitizeAccountName(typeof input.currentAccount === 'string' ? input.currentAccount : undefined)
85
+ ?? (Object.keys(accounts).length === 1 ? Object.keys(accounts)[0] : undefined);
86
+ return {
87
+ ...(currentAccount ? { currentAccount } : {}),
88
+ accounts
89
+ };
90
+ }
55
91
  function envConfig() {
56
- return sanitizeConfig({
92
+ return sanitizeAccountConfig({
57
93
  appid: process.env.WX_APPID,
58
94
  appsecret: process.env.WX_APPSECRET
59
95
  });
60
96
  }
61
- async function loadConfig() {
97
+ async function writeUserConfig(config) {
98
+ await (0, promises_1.mkdir)(CONFIG_DIR, { recursive: true });
99
+ const next = sanitizeUserConfig(config);
100
+ await (0, promises_1.writeFile)(getUserConfigPath(), `${JSON.stringify(next, null, 2)}\n`, 'utf8');
101
+ return next;
102
+ }
103
+ async function loadUserConfig() {
104
+ return sanitizeUserConfig(await readJson(getUserConfigPath()));
105
+ }
106
+ async function loadConfig(options) {
62
107
  const env = envConfig();
63
- const user = sanitizeConfig(await readJson(getUserConfigPath()));
108
+ const user = await loadUserConfig();
109
+ const account = sanitizeAccountName(options?.account) ?? sanitizeAccountName(process.env.WX_ACCOUNT) ?? user.currentAccount;
110
+ const selectedAccountConfig = account ? user.accounts[account] ?? {} : {};
64
111
  return {
112
+ ...selectedAccountConfig,
65
113
  ...env,
66
- ...user
114
+ ...(account ? { account } : {}),
115
+ ...(user.currentAccount ? { currentAccount: user.currentAccount } : {}),
116
+ accounts: user.accounts
67
117
  };
68
118
  }
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;
119
+ async function saveAccountConfig(accountName, patch) {
120
+ const sanitizedAccountName = sanitizeAccountName(accountName);
121
+ if (!sanitizedAccountName) {
122
+ throw new Error('公众号账号名不能为空');
123
+ }
124
+ const current = await loadUserConfig();
125
+ const nextAccounts = {
126
+ ...current.accounts,
127
+ [sanitizedAccountName]: sanitizeAccountConfig({
128
+ ...(current.accounts[sanitizedAccountName] ?? {}),
129
+ ...patch
130
+ })
131
+ };
132
+ return writeUserConfig({
133
+ currentAccount: current.currentAccount ?? sanitizedAccountName,
134
+ accounts: nextAccounts
135
+ });
136
+ }
137
+ async function setDefaultAccount(accountName) {
138
+ const sanitizedAccountName = sanitizeAccountName(accountName);
139
+ if (!sanitizedAccountName) {
140
+ throw new Error('公众号账号名不能为空');
141
+ }
142
+ const current = await loadUserConfig();
143
+ if (!current.accounts[sanitizedAccountName]) {
144
+ throw new Error(`未找到公众号账号 ${sanitizedAccountName},请先通过 --account ${sanitizedAccountName} 写入配置`);
145
+ }
146
+ return writeUserConfig({
147
+ ...current,
148
+ currentAccount: sanitizedAccountName
149
+ });
150
+ }
151
+ async function removeAccountConfig(accountName) {
152
+ const sanitizedAccountName = sanitizeAccountName(accountName);
153
+ if (!sanitizedAccountName) {
154
+ throw new Error('公众号账号名不能为空');
155
+ }
156
+ const current = await loadUserConfig();
157
+ if (!current.accounts[sanitizedAccountName]) {
158
+ throw new Error(`未找到公众号账号 ${sanitizedAccountName}`);
159
+ }
160
+ const nextAccounts = { ...current.accounts };
161
+ delete nextAccounts[sanitizedAccountName];
162
+ const remainingAccountNames = Object.keys(nextAccounts);
163
+ const nextCurrentAccount = current.currentAccount === sanitizedAccountName
164
+ ? remainingAccountNames[0]
165
+ : current.currentAccount;
166
+ return writeUserConfig({
167
+ ...(nextCurrentAccount ? { currentAccount: nextCurrentAccount } : {}),
168
+ accounts: nextAccounts
169
+ });
75
170
  }
76
171
  async function clearUserConfig() {
77
172
  if (await pathExists(getUserConfigPath())) {
78
173
  await (0, promises_1.rm)(getUserConfigPath(), { force: true });
79
174
  }
80
175
  }
176
+ function resolveWechatCredentials(config) {
177
+ if (!config.appid || !config.appsecret) {
178
+ if (config.account) {
179
+ throw new Error(`未找到公众号账号 ${config.account} 的完整微信认证信息,请先运行: wxgzh config --account ${config.account} --appid xxx --appsecret yyy`);
180
+ }
181
+ throw new Error('未配置微信认证信息,请先运行: wxgzh config --account <name> --appid xxx --appsecret yyy,或通过 --account <name> 指定已配置账号');
182
+ }
183
+ return {
184
+ appid: config.appid,
185
+ appsecret: config.appsecret
186
+ };
187
+ }
81
188
  function maskSecret(secret) {
82
189
  if (!secret) {
83
190
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyhue1991/wxgzh",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Markdown to WeChat Official Account draft CLI",
5
5
  "license": "MIT",
6
6
  "main": "dist/cli/index.js",