@wenyan-md/cli 1.0.8 → 1.0.9

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
@@ -9,69 +9,129 @@
9
9
  ![NPM Downloads](https://img.shields.io/npm/dm/%40wenyan-md%2Fcli)
10
10
  [![Stars](https://img.shields.io/github/stars/caol64/wenyan-cli?style=social)](https://github.com/caol64/wenyan-cli)
11
11
 
12
- 「文颜」是一款多平台排版美化工具,让你将 Markdown 一键发布至微信公众号、知乎、今日头条等主流写作平台。
12
+ ## 简介
13
13
 
14
- **文颜**现已推出多个版本:
14
+ **文颜(Wenyan)** 是一款多平台 Markdown 排版与发布工具,支持将 Markdown 一键转换并发布至:
15
15
 
16
- * [macOS App Store 版](https://github.com/caol64/wenyan) - MAC 桌面应用
17
- * [跨平台版本](https://github.com/caol64/wenyan-pc) - Windows/Linux 跨平台桌面应用
18
- * [CLI 版本](https://github.com/caol64/wenyan-cli) - CI/CD 或脚本自动化发布公众号文章
19
- * [MCP 版本](https://github.com/caol64/wenyan-mcp) - 让 AI 自动发布公众号文章
20
- * [嵌入版本](https://github.com/caol64/wenyan-core) - 将文颜的核心功能嵌入 Node 或者 Web 项目
16
+ - 微信公众号
17
+ - 知乎
18
+ - 今日头条
19
+ - 以及其它内容平台(持续扩展中)
21
20
 
22
- 本项目是 **文颜的 CLI 版本**。
21
+ 文颜的目标是:**让写作者专注内容,而不是排版和平台适配**。
23
22
 
24
- ## 功能
23
+ 本仓库是 **文颜的 CLI 版本**,适合以下场景:
25
24
 
26
- * 使用内置主题对 Markdown 内容排版
27
- * 支持图片自动上传
28
- * 支持数学公式渲染
29
- * 一键发布文章到微信公众号草稿箱
25
+ - 命令行使用
26
+ - CI / CD 自动化发布
27
+ - 脚本或工具链集成
28
+ - 与 AI / MCP 系统联动自动发文
30
29
 
31
- ## 主题效果
30
+ ## 文颜的不同版本
31
+
32
+ 文颜目前提供多种形态,覆盖不同使用场景:
33
+
34
+ - [macOS App Store 版](https://github.com/caol64/wenyan) - MAC 桌面应用
35
+ - [跨平台桌面版](https://github.com/caol64/wenyan-pc) - Windows/Linux
36
+ - 👉 [CLI 版本](https://github.com/caol64/wenyan-cli) - 本项目
37
+ - [MCP 版本](https://github.com/caol64/wenyan-mcp) - AI 自动发文
38
+ - [核心库](https://github.com/caol64/wenyan-core) - 嵌入 Node / Web 项目
39
+
40
+ ## 功能特性
41
+
42
+ - 使用内置主题对 Markdown 内容排版
43
+ - 自动处理并上传图片(本地 / 网络)
44
+ - 支持数学公式(MathJax)
45
+ - 一键发布文章到微信公众号草稿箱
46
+ - 支持 CI / 自动化流程调用
47
+
48
+ ## 主题效果预览
32
49
 
33
50
  👉 [内置主题预览](https://yuzhi.tech/docs/wenyan/theme)
34
51
 
35
- 文颜采用了多个开源的 Typora 主题,在此向各位作者表示感谢:
52
+ 文颜内置并适配了多个优秀的 Typora 主题,在此感谢原作者:
36
53
 
37
- - [Orange Heart](https://github.com/evgo2017/typora-theme-orange-heart)
38
- - [Rainbow](https://github.com/thezbm/typora-theme-rainbow)
39
- - [Lapis](https://github.com/YiNNx/typora-theme-lapis)
40
- - [Pie](https://github.com/kevinzhao2233/typora-theme-pie)
41
- - [Maize](https://github.com/BEATREE/typora-maize-theme)
42
- - [Purple](https://github.com/hliu202/typora-purple-theme)
43
- - [物理猫-薄荷](https://github.com/sumruler/typora-theme-phycat)
54
+ - [Orange Heart](https://github.com/evgo2017/typora-theme-orange-heart)
55
+ - [Rainbow](https://github.com/thezbm/typora-theme-rainbow)
56
+ - [Lapis](https://github.com/YiNNx/typora-theme-lapis)
57
+ - [Pie](https://github.com/kevinzhao2233/typora-theme-pie)
58
+ - [Maize](https://github.com/BEATREE/typora-maize-theme)
59
+ - [Purple](https://github.com/hliu202/typora-purple-theme)
60
+ - [物理猫-薄荷](https://github.com/sumruler/typora-theme-phycat)
44
61
 
45
- ## 安装
62
+ ## 安装方式
46
63
 
47
- ```
64
+ ### 方式一:npm(推荐)
65
+
66
+ ```bash
48
67
  npm install -g @wenyan-md/cli
49
68
  ```
50
69
 
70
+ 安装完成后即可使用:
71
+
72
+ ```bash
73
+ wenyan --help
74
+ ```
75
+
76
+ ### 方式二:Docker(无需 Node 环境)
77
+
78
+ 如果你不想在本地安装 Node.js,也可以直接使用 Docker。
79
+
80
+ **拉取镜像**
81
+
82
+ ```bash
83
+ docker pull caol64/wenyan-cli
84
+ ```
85
+
86
+ **查看帮助**
87
+
88
+ ```bash
89
+ docker run --rm caol64/wenyan-cli
90
+ ```
91
+
92
+ **发布文章示例**
93
+
94
+ ```bash
95
+ docker run --rm \
96
+ --env-file .env.test \
97
+ -e HOST_FILE_PATH=$(pwd) \
98
+ -v $(pwd):/mnt/host-downloads \
99
+ caol64/wenyan-cli \
100
+ publish -f ./test/publish.md -t phycat
101
+ ```
102
+
103
+ > 说明:
104
+ >
105
+ > - 使用 `-e` 传入环境变量
106
+ > - 使用 `-v` 挂载本地 Markdown 文件
107
+ > - 容器启动即执行 `wenyan` 命令
108
+
51
109
  ## 基本用法
52
110
 
53
- 主命令为:
111
+ CLI 主命令:
54
112
 
55
113
  ```bash
56
114
  wenyan <command> [options]
57
115
  ```
58
116
 
59
- ## 环境变量
117
+ 目前最常用的子命令是 `publish`。
118
+
119
+ ## 环境变量配置
60
120
 
61
- 某些功能(如发布到微信公众号)需要配置以下环境变量:
121
+ 部分功能(如发布微信公众号)需要配置以下环境变量:
62
122
 
63
- * `WECHAT_APP_ID`
64
- * `WECHAT_APP_SECRET`
123
+ - `WECHAT_APP_ID`
124
+ - `WECHAT_APP_SECRET`
65
125
 
66
126
  ### macOS / Linux
67
127
 
68
- 可在命令前临时设置:
128
+ 临时使用:
69
129
 
70
130
  ```bash
71
131
  WECHAT_APP_ID=xxx WECHAT_APP_SECRET=yyy wenyan publish "your markdown"
72
132
  ```
73
133
 
74
- 或在 `~/.bashrc` / `~/.zshrc` 中永久添加:
134
+ 永久配置(推荐):
75
135
 
76
136
  ```bash
77
137
  export WECHAT_APP_ID=xxx
@@ -80,10 +140,12 @@ export WECHAT_APP_SECRET=yyy
80
140
 
81
141
  ### Windows (PowerShell)
82
142
 
83
- 临时设置:
143
+ 临时使用:
84
144
 
85
145
  ```powershell
86
- $env:WECHAT_APP_ID="xxx"; $env:WECHAT_APP_SECRET="yyy"; wenyan publish "your markdown"
146
+ $env:WECHAT_APP_ID="xxx"
147
+ $env:WECHAT_APP_SECRET="yyy"
148
+ wenyan publish example.md
87
149
  ```
88
150
 
89
151
  永久设置(在环境变量里添加):
@@ -94,37 +156,28 @@ $env:WECHAT_APP_ID="xxx"; $env:WECHAT_APP_SECRET="yyy"; wenyan publish "your mar
94
156
 
95
157
  `publish`
96
158
 
97
- 将 Markdown 转换为适配微信公众号的富文本 HTML 并上传到公众号。
159
+ 将 Markdown 转换为适配微信公众号的富文本 HTML,并上传到草稿箱。
98
160
 
99
161
  ### 参数
100
162
 
101
- - `<input-content>`,要转换的 Markdown 内容。可直接作为参数传入,或通过管道/重定向从 `stdin` 读取
102
-
103
- ### 选项
104
-
105
- - `-t`,主题id,默认`default`
106
- - default
107
- - orangeheart
108
- - rainbow
109
- - lapis
110
- - pie
111
- - maize
112
- - purple
113
- - phycat
114
- - `-h`,代码高亮主题,默认`solarized-light`
115
- - atom-one-dark
116
- - atom-one-light
117
- - dracula
118
- - github-dark
119
- - github
120
- - monokai
121
- - solarized-dark
122
- - solarized-light
123
- - xcode
124
- - 代码块默认使用 Mac 风格,如要关闭:`--no-mac-style`
125
- - 链接默认转脚注,如要关闭:`--no-footnote`
126
-
127
- ## 示例
163
+ - `<input-content>`
164
+
165
+ Markdown 内容,可以:
166
+
167
+ - 直接作为参数传入
168
+ - 通过 stdin 管道输入
169
+
170
+ ### 常用选项
171
+
172
+ - `-t`:主题(默认 `default`)
173
+ - default / orangeheart / rainbow / lapis / pie / maize / purple / phycat
174
+ - `-h`:代码高亮主题(默认 `solarized-light`)
175
+ - atom-one-dark / atom-one-light / dracula / github-dark / github / monokai / solarized-dark / solarized-light / xcode
176
+ - `--no-mac-style`:关闭代码块 Mac 风格
177
+ - `--no-footnote`:关闭链接转脚注
178
+ - `-f`:指定本地 Markdown 文件路径
179
+
180
+ ## 使用示例
128
181
 
129
182
  直接传入内容:
130
183
 
@@ -132,40 +185,50 @@ $env:WECHAT_APP_ID="xxx"; $env:WECHAT_APP_SECRET="yyy"; wenyan publish "your mar
132
185
  wenyan publish "# Hello, Wenyan" -t lapis -h solarized-light
133
186
  ```
134
187
 
135
- 从文件读取:
188
+ 从管道读取:
136
189
 
137
190
  ```bash
138
191
  cat example.md | wenyan publish -t lapis -h solarized-light --no-mac-style
139
192
  ```
140
193
 
141
- ## 微信公众号 IP 白名单
194
+ 从文件读取:
142
195
 
143
- 请务必将服务器 IP 加入公众号平台的 IP 白名单,以确保上传接口调用成功。
144
- 详细配置说明请参考:[https://yuzhi.tech/docs/wenyan/upload](https://yuzhi.tech/docs/wenyan/upload)
196
+ ```bash
197
+ wenyan publish -f "./example.md" -t lapis -h solarized-light --no-mac-style
198
+ ```
145
199
 
146
- ## 配置说明(Frontmatter
200
+ ## Markdown Frontmatter 说明(必读)
147
201
 
148
- 为了可以正确上传文章,需要在每一篇 Markdown 文章的开头添加一段`frontmatter`,提供`title`、`cover`两个字段:
202
+ 为了正确上传文章,每篇 Markdown 顶部需要包含 frontmatter
149
203
 
150
204
  ```md
151
205
  ---
152
206
  title: 在本地跑一个大语言模型(2) - 给模型提供外部知识库
153
- cover: /Users/lei/Downloads/result_image.jpg
207
+ cover: /Users/xxx/image.jpg
154
208
  ---
155
209
  ```
156
210
 
157
- * `title` 是文章标题,必填。
158
- * `cover` 是文章封面,支持本地路径和网络图片:
211
+ 字段说明:
159
212
 
160
- * 如果正文有至少一张图片,可省略,此时将使用其中一张作为封面;
161
- * 如果正文无图片,则必须提供 cover。
213
+ - `title` 文章标题(必填)
214
+ - `cover` 文章封面
215
+ - 本地路径或网络图片
216
+ - 如果正文中已有图片,可省略
162
217
 
163
218
  ## 关于图片自动上传
164
219
 
165
- * 支持图片路径:
220
+ 支持以下图片来源:
166
221
 
167
- * 本地路径(如:`/Users/lei/Downloads/result_image.jpg`)
168
- * 网络路径(如:`https://example.com/image.jpg`)
222
+ - 本地路径(如:`/Users/xxx/image.jpg`)
223
+ - 网络路径(如:`https://example.com/image.jpg`)
224
+
225
+ ## 微信公众号 IP 白名单
226
+
227
+ > ⚠️ 重要
228
+ >
229
+ > 请确保运行文颜的机器 IP 已加入微信公众号后台的 IP 白名单,否则上传接口将调用失败。
230
+
231
+ 配置说明文档:[https://yuzhi.tech/docs/wenyan/upload](https://yuzhi.tech/docs/wenyan/upload)
169
232
 
170
233
  ## 示例文章格式
171
234
 
@@ -186,7 +249,9 @@ cover: /Users/lei/Downloads/result_image.jpg
186
249
 
187
250
  ## 赞助
188
251
 
189
- 如果您觉得不错,可以给我家猫咪买点罐头吃。[喂猫❤️](https://yuzhi.tech/sponsor)
252
+ 如果你觉得文颜对你有帮助,可以给我家猫咪买点罐头 ❤️
253
+
254
+ [https://yuzhi.tech/sponsor](https://yuzhi.tech/sponsor)
190
255
 
191
256
  ## License
192
257
 
@@ -1,14 +1,25 @@
1
1
  import { getGzhContent } from "@wenyan-md/core/wrapper";
2
2
  import { publishToDraft } from "@wenyan-md/core/publish";
3
- import { readStdin } from "../utils.js";
3
+ import { getNormalizeFilePath, readStdin } from "../utils.js";
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
4
6
  export async function publishCommand(inputContent, options) {
5
7
  try {
8
+ const { file } = options;
9
+ let absoluteDirPath = undefined;
6
10
  if (!inputContent) {
7
- if (process.stdin.isTTY) {
8
- console.error("Error: missing input-content (no argument and no stdin).");
9
- process.exit(1);
11
+ if (!process.stdin.isTTY) {
12
+ inputContent = await readStdin();
10
13
  }
11
- inputContent = await readStdin();
14
+ }
15
+ if (!inputContent && file) {
16
+ const normalizePath = getNormalizeFilePath(file);
17
+ inputContent = await fs.readFile(normalizePath, "utf-8");
18
+ absoluteDirPath = path.dirname(normalizePath);
19
+ }
20
+ if (!inputContent) {
21
+ console.error("Error: missing input-content (no argument, no stdin, and no file).");
22
+ process.exit(1);
12
23
  }
13
24
  const gzhContent = await getGzhContent(inputContent, options["theme"], options["highlight"], options["macStyle"], options["footnote"]);
14
25
  if (!gzhContent.title) {
@@ -19,7 +30,9 @@ export async function publishCommand(inputContent, options) {
19
30
  console.error("未能找到文章封面");
20
31
  process.exit(1);
21
32
  }
22
- const data = await publishToDraft(gzhContent.title, gzhContent.content, gzhContent.cover);
33
+ const data = await publishToDraft(gzhContent.title, gzhContent.content, gzhContent.cover, {
34
+ relativePath: absoluteDirPath,
35
+ });
23
36
  if (data.media_id) {
24
37
  console.log(`上传成功,media_id: ${data.media_id}`);
25
38
  }
@@ -1,13 +1,21 @@
1
1
  import { getGzhContent } from "@wenyan-md/core/wrapper";
2
- import { readStdin } from "../utils.js";
2
+ import { getNormalizeFilePath, readStdin } from "../utils.js";
3
+ import fs from "node:fs/promises";
3
4
  export async function renderCommand(inputContent, options) {
4
5
  try {
6
+ const { file } = options;
5
7
  if (!inputContent) {
6
- if (process.stdin.isTTY) {
7
- console.error("Error: missing input-content (no argument and no stdin).");
8
- process.exit(1);
8
+ if (!process.stdin.isTTY) {
9
+ inputContent = await readStdin();
9
10
  }
10
- inputContent = await readStdin();
11
+ }
12
+ if (!inputContent && file) {
13
+ const normalizePath = getNormalizeFilePath(file);
14
+ inputContent = await fs.readFile(normalizePath, "utf-8");
15
+ }
16
+ if (!inputContent) {
17
+ console.error("Error: missing input-content (no argument, no stdin, and no file).");
18
+ process.exit(1);
11
19
  }
12
20
  const gzhContent = await getGzhContent(inputContent, options["theme"], options["highlight"], options["macStyle"], options["footnote"]);
13
21
  console.log(gzhContent.content);
package/dist/index.js CHANGED
@@ -14,7 +14,8 @@ program
14
14
  program
15
15
  .command("publish")
16
16
  .description("Render a markdown file to styled HTML and publish to wechat GZH")
17
- .argument("[input-content]", "content of the input markdown file")
17
+ .argument("[input-content]", "markdown content (string input)")
18
+ .option("-f, --file <path>", "read markdown content from local file")
18
19
  .option("-t, --theme <theme-id>", "ID of the theme to use", "default")
19
20
  .option("-h, --highlight <highlight-theme-id>", "ID of the code highlight theme to use", "solarized-light")
20
21
  .option("--mac-style", "display codeblock with mac style", true)
@@ -25,7 +26,8 @@ program
25
26
  program
26
27
  .command("render")
27
28
  .description("Render a markdown file to styled HTML")
28
- .argument("[input-content]", "content of the input markdown file")
29
+ .argument("[input-content]", "markdown content (string input)")
30
+ .option("-f, --file <path>", "read markdown content from local file")
29
31
  .option("-t, --theme <theme-id>", "ID of the theme to use", "default")
30
32
  .option("-h, --highlight <highlight-theme-id>", "ID of the code highlight theme to use", "solarized-light")
31
33
  .option("--mac-style", "display codeblock with mac style", true)
@@ -1,8 +1,9 @@
1
1
  interface RenderOptions {
2
- output?: string;
2
+ file?: string;
3
3
  theme: string;
4
4
  highlight: string;
5
5
  macStyle: boolean;
6
+ footnote: boolean;
6
7
  }
7
- export declare function publishCommand(inputContent: string, options: RenderOptions): Promise<void>;
8
+ export declare function publishCommand(inputContent: string | undefined, options: RenderOptions): Promise<void>;
8
9
  export {};
@@ -1,8 +1,9 @@
1
1
  interface RenderOptions {
2
- output?: string;
2
+ file?: string;
3
3
  theme: string;
4
4
  highlight: string;
5
5
  macStyle: boolean;
6
+ footnote: boolean;
6
7
  }
7
8
  export declare function renderCommand(inputContent: string | undefined, options: RenderOptions): Promise<void>;
8
9
  export {};
@@ -1 +1,2 @@
1
1
  export declare function readStdin(): Promise<string>;
2
+ export declare function getNormalizeFilePath(inputPath: string): string;
package/dist/utils.js CHANGED
@@ -1,3 +1,4 @@
1
+ import path from "node:path";
1
2
  export async function readStdin() {
2
3
  return new Promise((resolve, reject) => {
3
4
  let data = "";
@@ -7,3 +8,29 @@ export async function readStdin() {
7
8
  process.stdin.on("error", reject);
8
9
  });
9
10
  }
11
+ /**
12
+ * 路径标准化工具函数
13
+ * 将 Windows 的反斜杠 \ 转换为正斜杠 /,并去除末尾斜杠
14
+ * 目的:在 Linux 容器内也能正确处理 Windows 路径字符串
15
+ */
16
+ function normalizePath(p) {
17
+ return p.replace(/\\/g, "/").replace(/\/+$/, "");
18
+ }
19
+ export function getNormalizeFilePath(inputPath) {
20
+ const isContainer = !!process.env.CONTAINERIZED;
21
+ if (isContainer) {
22
+ const hostFilePath = normalizePath(process.env.HOST_FILE_PATH || "");
23
+ const containerFilePath = normalizePath(process.env.CONTAINER_FILE_PATH || "/mnt/host-downloads");
24
+ let relativePart = normalizePath(inputPath);
25
+ if (relativePart.startsWith(hostFilePath)) {
26
+ relativePart = relativePart.slice(hostFilePath.length);
27
+ }
28
+ if (!relativePart.startsWith("/")) {
29
+ relativePart = "/" + relativePart;
30
+ }
31
+ return containerFilePath + relativePart;
32
+ }
33
+ else {
34
+ return path.resolve(inputPath);
35
+ }
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenyan-md/cli",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "A CLI tool for Wenyan markdown rendering & publishing",
5
5
  "author": "Lei <caol64@gmail.com> (https://github.com/caol64)",
6
6
  "license": "Apache-2.0",
@@ -32,7 +32,7 @@
32
32
  "url": "https://github.com/caol64/wenyan-cli/issues"
33
33
  },
34
34
  "dependencies": {
35
- "@wenyan-md/core": "^1.0.14",
35
+ "@wenyan-md/core": "^1.0.17",
36
36
  "commander": "^14.0.0"
37
37
  },
38
38
  "devDependencies": {
@@ -45,7 +45,7 @@
45
45
  "build": "tsc",
46
46
  "dev": "tsx src/index.ts",
47
47
  "upgrade:core": "pnpm update @wenyan-md/core",
48
- "test:render": "pnpm build && cat test/publish.md | node ./bin/cli.js render",
49
- "test:publish": "pnpm build && cat test/publish.md | pnpm dotenv -e .env.test -- node ./bin/cli.js publish -t lapis"
48
+ "test:render": "pnpm build && node ./bin/cli.js render -f test/publish.md",
49
+ "test:publish": "pnpm build && pnpm dotenv -e .env.test -- node ./bin/cli.js publish -f test/publish.md -t phycat"
50
50
  }
51
51
  }