mirror of
https://github.com/comeonzhj/Auto-Redbook-Skills.git
synced 2026-03-27 04:29:28 +08:00
Add files via upload
This commit is contained in:
242
README.md
242
README.md
@@ -1,2 +1,240 @@
|
|||||||
# Auto-Redbook-Skills
|
# 📕 md2Redbook
|
||||||
一个自动撰写小红书笔记,自动生成图片,自动发布的 Skills
|
|
||||||
|
> 将 Markdown 文档一键转换为精美的小红书图片卡片
|
||||||
|
|
||||||
|
[](https://www.python.org/)
|
||||||
|
[](https://nodejs.org/)
|
||||||
|
[](LICENSE)
|
||||||
|
|
||||||
|
## ✨ 功能特性
|
||||||
|
|
||||||
|
- 🎨 **精美卡片** - 小红书风格的封面和正文卡片,3:4 比例,1080×1440px
|
||||||
|
- 📝 **Markdown 支持** - 完整支持标题、列表、引用、代码块、图片等元素
|
||||||
|
- 🔀 **自动分页** - 使用 `---` 分隔符自动拆分为多张卡片
|
||||||
|
- 🐍 **双语言脚本** - 提供 Python 和 Node.js 两种渲染方案
|
||||||
|
- 📤 **一键发布** - 支持直接发布到小红书(需配置 Cookie)
|
||||||
|
- 🎯 **命令行工具** - 终端直接运行,无需浏览器交互
|
||||||
|
|
||||||
|
## 📸 效果预览
|
||||||
|
|
||||||
|
| 封面卡片 | 正文卡片 |
|
||||||
|
|:---:|:---:|
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
**Python 版本:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install markdown pyyaml playwright python-dotenv xhs
|
||||||
|
playwright install chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
**Node.js 版本:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd md2Redbook
|
||||||
|
npm install
|
||||||
|
npx playwright install chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建 Markdown 文档
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
emoji: "🚀"
|
||||||
|
title: "5个效率神器"
|
||||||
|
subtitle: "让工作效率翻倍"
|
||||||
|
---
|
||||||
|
|
||||||
|
## 神器一:Notion 📝
|
||||||
|
|
||||||
|
全能型笔记工具,支持数据库、看板、日历等多种视图。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 神器二:Raycast ⚡
|
||||||
|
|
||||||
|
Mac 上的效率启动器,比 Spotlight 强大 100 倍!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#效率工具 #生产力 #神器推荐
|
||||||
|
```
|
||||||
|
|
||||||
|
### 渲染图片
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/render_xhs.py your_note.md --output-dir ./output
|
||||||
|
```
|
||||||
|
|
||||||
|
**Node.js:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/render_xhs.js your_note.md --output-dir ./output
|
||||||
|
```
|
||||||
|
|
||||||
|
### 输出结果
|
||||||
|
|
||||||
|
```
|
||||||
|
output/
|
||||||
|
├── cover.png # 封面图片
|
||||||
|
├── card_1.png # 第一张正文卡片
|
||||||
|
├── card_2.png # 第二张正文卡片
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 Markdown 格式说明
|
||||||
|
|
||||||
|
### YAML 头部(封面信息)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
emoji: "🎯" # 封面装饰 Emoji
|
||||||
|
title: "大标题文字" # 不超过 15 字
|
||||||
|
subtitle: "副标题文案" # 不超过 15 字
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### 正文分页
|
||||||
|
|
||||||
|
使用 `---` 分隔线拆分为多张卡片:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
第一张卡片内容...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
第二张卡片内容...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
第三张卡片内容...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 标签
|
||||||
|
|
||||||
|
在正文末尾添加 SEO 标签:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
#标签1 #标签2 #标签3 #标签4 #标签5
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📤 发布到小红书
|
||||||
|
|
||||||
|
### 1. 配置 Cookie
|
||||||
|
|
||||||
|
复制 `env.example.txt` 为 `.env`,填入小红书 Cookie:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp env.example.txt .env
|
||||||
|
```
|
||||||
|
|
||||||
|
编辑 `.env` 文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
XHS_COOKIE=your_cookie_string_here
|
||||||
|
```
|
||||||
|
|
||||||
|
**获取 Cookie 方法:**
|
||||||
|
|
||||||
|
1. 在浏览器中登录 [小红书](https://www.xiaohongshu.com)
|
||||||
|
2. 打开开发者工具(F12)
|
||||||
|
3. 在 Network 标签中查看任意请求的 Cookie 头
|
||||||
|
4. 复制完整的 cookie 字符串
|
||||||
|
|
||||||
|
### 2. 发布笔记
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/publish_xhs.py \
|
||||||
|
--title "笔记标题" \
|
||||||
|
--desc "笔记描述内容" \
|
||||||
|
--images cover.png card_1.png card_2.png
|
||||||
|
```
|
||||||
|
|
||||||
|
**可选参数:**
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `--private` | 设为私密笔记 |
|
||||||
|
| `--post-time "2024-01-01 12:00:00"` | 定时发布 |
|
||||||
|
| `--dry-run` | 仅验证,不实际发布 |
|
||||||
|
|
||||||
|
## 🎨 自定义样式
|
||||||
|
|
||||||
|
### 修改背景渐变
|
||||||
|
|
||||||
|
编辑 `assets/card.html` 中的 `.card-container`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
.card-container {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**预设渐变色:**
|
||||||
|
|
||||||
|
| 名称 | 渐变值 |
|
||||||
|
|------|--------|
|
||||||
|
| 紫蓝 | `#667eea → #764ba2` |
|
||||||
|
| 粉红 | `#f093fb → #f5576c` |
|
||||||
|
| 青蓝 | `#4facfe → #00f2fe` |
|
||||||
|
| 绿色 | `#43e97b → #38f9d7` |
|
||||||
|
| 橙黄 | `#fa709a → #fee140` |
|
||||||
|
|
||||||
|
### 修改封面样式
|
||||||
|
|
||||||
|
编辑 `assets/cover.html` 中的样式。
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
md2Redbook/
|
||||||
|
├── SKILL.md # 技能描述(AI Agent 使用)
|
||||||
|
├── README.md # 项目文档
|
||||||
|
├── requirements.txt # Python 依赖
|
||||||
|
├── package.json # Node.js 依赖
|
||||||
|
├── env.example.txt # Cookie 配置示例
|
||||||
|
├── assets/
|
||||||
|
│ ├── cover.html # 封面 HTML 模板
|
||||||
|
│ ├── card.html # 正文卡片 HTML 模板
|
||||||
|
│ ├── styles.css # 共用样式表
|
||||||
|
│ └── example.md # 示例 Markdown
|
||||||
|
└── scripts/
|
||||||
|
├── render_xhs.py # Python 渲染脚本
|
||||||
|
├── render_xhs.js # Node.js 渲染脚本
|
||||||
|
└── publish_xhs.py # 小红书发布脚本
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤖 作为 AI Skill 使用
|
||||||
|
|
||||||
|
本项目也是一个 AI 技能包,可以被 Claude 等 AI Agent 使用:
|
||||||
|
|
||||||
|
1. 将 `md2Redbook` 目录添加到 AI 的技能库
|
||||||
|
2. AI 会根据 `SKILL.md` 中的说明自动使用此技能
|
||||||
|
3. 当用户需要创建小红书笔记时,AI 会:
|
||||||
|
- 撰写符合小红书风格的内容
|
||||||
|
- 生成 Markdown 文档
|
||||||
|
- 调用脚本渲染图片
|
||||||
|
- (可选)发布到小红书
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **Cookie 安全** - Cookie 包含登录凭证,请勿泄露或提交到版本控制
|
||||||
|
2. **Cookie 有效期** - 小红书 Cookie 会过期,需定期更新
|
||||||
|
3. **发布频率** - 避免频繁发布,以免触发平台限制
|
||||||
|
4. **图片尺寸** - 渲染的图片为 1080×1440px,符合小红书推荐比例
|
||||||
|
|
||||||
|
## 🙏 致谢
|
||||||
|
|
||||||
|
- [Playwright](https://playwright.dev/) - 浏览器自动化渲染
|
||||||
|
- [Marked](https://marked.js.org/) - Markdown 解析
|
||||||
|
- [xhs](https://github.com/ReaJason/xhs) - 小红书 API 客户端
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT License © 2024
|
||||||
|
|||||||
168
SKILL.md
Normal file
168
SKILL.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
---
|
||||||
|
name: xhs-note-creator
|
||||||
|
description: 小红书笔记素材创作技能。当用户需要创建小红书笔记素材时使用这个技能。技能包含:根据用户的需求和提供的资料,撰写小红书笔记内容(标题+正文),生成图片卡片(封面+正文卡片),以及发布小红书笔记。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 小红书笔记创作技能
|
||||||
|
|
||||||
|
这个技能用于创建专业的小红书笔记素材,包括内容撰写、图片卡片生成和笔记发布。
|
||||||
|
|
||||||
|
## 使用场景
|
||||||
|
|
||||||
|
- 用户需要创建小红书笔记时
|
||||||
|
- 用户提供资料需要转化为小红书风格内容时
|
||||||
|
- 用户需要生成精美的图片卡片用于发布时
|
||||||
|
|
||||||
|
## 工作流程
|
||||||
|
|
||||||
|
### 第一步:撰写小红书笔记内容
|
||||||
|
|
||||||
|
根据用户需求和提供的资料,创作符合小红书风格的内容:
|
||||||
|
|
||||||
|
#### 标题要求
|
||||||
|
- 不超过 20 字
|
||||||
|
- 吸引眼球,制造好奇心
|
||||||
|
- 可使用数字、疑问句、感叹号增强吸引力
|
||||||
|
- 示例:「5个让效率翻倍的神器推荐!」「震惊!原来这样做才对」
|
||||||
|
|
||||||
|
#### 正文要求
|
||||||
|
- 使用良好的排版,段落清晰
|
||||||
|
- 点缀少量 Emoji 增加可读性(每段 1-2 个即可)
|
||||||
|
- 使用简短的句子和段落
|
||||||
|
- 结尾给出 SEO 友好的 Tags 标签(5-10 个相关标签)
|
||||||
|
|
||||||
|
### 第二步:生成 Markdown 文档
|
||||||
|
|
||||||
|
**注意:这里生成的 Markdown 文档是用于渲染卡片的,必须专门生成,禁止直接使用上一步的笔记正文内容。**
|
||||||
|
|
||||||
|
Markdown 文件,文件应包含:
|
||||||
|
|
||||||
|
1. YAML 头部元数据(封面信息):
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
emoji: "🚀" # 封面装饰 Emoji
|
||||||
|
title: "大标题" # 封面大标题(不超过15字)
|
||||||
|
subtitle: "副标题文案" # 封面副标题(不超过15字)
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 用于渲染卡片的 Markdown 文本内容:
|
||||||
|
- 使用 `---` 分割线将正文分隔为多个卡片段落
|
||||||
|
- 每个分段的文字控制在 200 字左右
|
||||||
|
- 后续会将每个卡片段落渲染为一张图片
|
||||||
|
|
||||||
|
完整示例:
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
emoji: "💡"
|
||||||
|
title: "5个效率神器让工作效率翻倍"
|
||||||
|
subtitle: "对着抄作业就好了,一起变高效"
|
||||||
|
---
|
||||||
|
|
||||||
|
# 神器一:Notion 📝
|
||||||
|
|
||||||
|
> 全能型笔记工具,支持数据库、看板、日历等多种视图...
|
||||||
|
|
||||||
|
## 特色功能
|
||||||
|
|
||||||
|
- 特色一
|
||||||
|
- 特色二
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 神器二:Raycast ⚡
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
可使用代码块来增加渲染后图片的视觉丰富度
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## 推荐原因
|
||||||
|
|
||||||
|
- 原因一
|
||||||
|
- 原因二
|
||||||
|
- ……
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 神器三:Arc 🌈
|
||||||
|
|
||||||
|
全新理念的浏览器,侧边栏标签管理...
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第三步:渲染图片卡片
|
||||||
|
|
||||||
|
将 Markdown 文档渲染为图片卡片。提供两种渲染脚本:
|
||||||
|
|
||||||
|
#### Python 渲染脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/render_xhs.py <markdown_file> [--output-dir <output_directory>]
|
||||||
|
```
|
||||||
|
|
||||||
|
- 默认输出目录为当前工作目录
|
||||||
|
- 生成的图片包括:封面(cover.png)和正文卡片(card_1.png, card_2.png, ...)
|
||||||
|
|
||||||
|
#### Node.js 渲染脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node scripts/render_xhs.js <markdown_file> [--output-dir <output_directory>]
|
||||||
|
```
|
||||||
|
|
||||||
|
功能与 Python 版本相同。
|
||||||
|
|
||||||
|
### 第四步:发布小红书笔记(可选)
|
||||||
|
|
||||||
|
使用发布脚本将生成的图片发布到小红书:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/publish_xhs.py --title "笔记标题" --desc "笔记描述" --images card_1.png card_2.png cover.png
|
||||||
|
```
|
||||||
|
|
||||||
|
**前置条件**:
|
||||||
|
|
||||||
|
1. 在同目录下创建 `.env` 文件,配置小红书 Cookie:
|
||||||
|
```
|
||||||
|
XHS_COOKIE=your_cookie_string_here
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Cookie 获取方式:
|
||||||
|
- 在浏览器中登录小红书(https://www.xiaohongshu.com)
|
||||||
|
- 打开开发者工具(F12)
|
||||||
|
- 在 Network 标签中查看请求头的 Cookie
|
||||||
|
|
||||||
|
## 图片规格说明
|
||||||
|
|
||||||
|
### 封面卡片
|
||||||
|
- 尺寸比例:3:4(小红书推荐比例)
|
||||||
|
- 基准尺寸:1080×1440px
|
||||||
|
- 包含:Emoji 装饰、大标题、副标题
|
||||||
|
- 样式:渐变背景 + 圆角内容区
|
||||||
|
|
||||||
|
### 正文卡片
|
||||||
|
- 尺寸比例:3:4
|
||||||
|
- 基准尺寸:1080×1440px
|
||||||
|
- 支持:标题、段落、列表、引用、代码块、图片
|
||||||
|
- 样式:白色卡片 + 渐变背景边框
|
||||||
|
|
||||||
|
## 技能资源
|
||||||
|
|
||||||
|
### 脚本文件
|
||||||
|
- `scripts/render_xhs.py` - Python 渲染脚本
|
||||||
|
- `scripts/render_xhs.js` - Node.js 渲染脚本
|
||||||
|
- `scripts/publish_xhs.py` - 小红书发布脚本
|
||||||
|
|
||||||
|
### 资源文件
|
||||||
|
- `assets/cover.html` - 封面 HTML 模板
|
||||||
|
- `assets/card.html` - 正文卡片 HTML 模板
|
||||||
|
- `assets/styles.css` - 共用样式表
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. Markdown 文件应保存在工作目录,渲染后的图片也保存在工作目录
|
||||||
|
2. 技能目录 (`md2Redbook/`) 仅存放脚本和模板,不存放用户数据
|
||||||
|
3. 图片尺寸会根据内容自动调整,但保持 3:4 比例
|
||||||
|
4. Cookie 有有效期限制,过期后需要重新获取
|
||||||
|
5. 发布功能依赖 xhs 库,需要安装:`pip install xhs`
|
||||||
216
assets/card.html
Normal file
216
assets/card.html
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1080">
|
||||||
|
<title>小红书卡片</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
width: 1080px;
|
||||||
|
min-height: 1440px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-container {
|
||||||
|
width: 1080px;
|
||||||
|
min-height: 1440px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
position: relative;
|
||||||
|
padding: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-inner {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 60px;
|
||||||
|
min-height: calc(1440px - 100px);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown 内容样式 */
|
||||||
|
.card-content {
|
||||||
|
color: #475569;
|
||||||
|
font-size: 42px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content h1 {
|
||||||
|
font-size: 72px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content h2 {
|
||||||
|
font-size: 56px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #334155;
|
||||||
|
margin: 50px 0 25px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content h3 {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #475569;
|
||||||
|
margin: 40px 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content p {
|
||||||
|
margin-bottom: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content em {
|
||||||
|
font-style: italic;
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content a {
|
||||||
|
color: #6366f1;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 2px solid #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content ul,
|
||||||
|
.card-content ol {
|
||||||
|
margin: 30px 0;
|
||||||
|
padding-left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content li {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content blockquote {
|
||||||
|
border-left: 8px solid #6366f1;
|
||||||
|
padding-left: 40px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding-top: 25px;
|
||||||
|
padding-bottom: 25px;
|
||||||
|
padding-right: 30px;
|
||||||
|
margin: 35px 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-style: italic;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content blockquote p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content code {
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||||
|
font-size: 38px;
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content pre {
|
||||||
|
background: #1e293b;
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin: 35px 0;
|
||||||
|
overflow-x: visible;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 36px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content pre code {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin: 35px auto;
|
||||||
|
display: block;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content hr {
|
||||||
|
border: none;
|
||||||
|
height: 2px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
margin: 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tags 标签样式 */
|
||||||
|
.tags-container {
|
||||||
|
margin-top: 50px;
|
||||||
|
padding-top: 30px;
|
||||||
|
border-top: 2px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border-radius: 30px;
|
||||||
|
font-size: 34px;
|
||||||
|
margin: 10px 15px 10px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息卡片样式 */
|
||||||
|
.info-card {
|
||||||
|
margin: 40px 0;
|
||||||
|
padding: 40px 50px;
|
||||||
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(139, 92, 246, 0.12) 100%);
|
||||||
|
border: 2px solid rgba(99, 102, 241, 0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页码样式 */
|
||||||
|
.page-number {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 80px;
|
||||||
|
right: 80px;
|
||||||
|
font-size: 36px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card-container">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-content">
|
||||||
|
{{CONTENT}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-number">{{PAGE_NUMBER}}</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
82
assets/cover.html
Normal file
82
assets/cover.html
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1080, height=1440">
|
||||||
|
<title>小红书封面</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
width: 1080px;
|
||||||
|
height: 1440px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-container {
|
||||||
|
width: 1080px;
|
||||||
|
height: 1440px;
|
||||||
|
background: linear-gradient(180deg, #3450E4 0%, #D266DA 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-inner {
|
||||||
|
position: absolute;
|
||||||
|
width: 950px;
|
||||||
|
height: 1310px;
|
||||||
|
left: 65px;
|
||||||
|
top: 65px;
|
||||||
|
background: #F3F3F3;
|
||||||
|
border-radius: 25px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 80px 85px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-emoji {
|
||||||
|
font-size: 180px;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-title {
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 130px;
|
||||||
|
line-height: 1.4;
|
||||||
|
background: linear-gradient(180deg, #2E67B1 0%, #4C4C4C 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-subtitle {
|
||||||
|
font-weight: 350;
|
||||||
|
font-size: 72px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #000000;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="cover-container">
|
||||||
|
<div class="cover-inner">
|
||||||
|
<div class="cover-emoji">{{EMOJI}}</div>
|
||||||
|
<div class="cover-title">{{TITLE}}</div>
|
||||||
|
<div class="cover-subtitle">{{SUBTITLE}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
57
assets/example.md
Normal file
57
assets/example.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
emoji: "🚀"
|
||||||
|
title: "5个效率神器"
|
||||||
|
subtitle: "让工作效率翻倍"
|
||||||
|
---
|
||||||
|
|
||||||
|
## 神器一:Notion 📝
|
||||||
|
|
||||||
|
全能型笔记工具,支持数据库、看板、日历等多种视图。
|
||||||
|
|
||||||
|
> 一个工具替代十个 App,笔记、任务、项目管理全搞定!
|
||||||
|
|
||||||
|
**核心功能:**
|
||||||
|
- 📊 灵活的数据库视图
|
||||||
|
- 🔗 双向链接
|
||||||
|
- 🎨 丰富的模板库
|
||||||
|
- 👥 团队协作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 神器二:Raycast ⚡
|
||||||
|
|
||||||
|
Mac 上的效率启动器,比 Spotlight 强大 100 倍!
|
||||||
|
|
||||||
|
**必装插件推荐:**
|
||||||
|
- 剪贴板历史
|
||||||
|
- 窗口管理
|
||||||
|
- 快捷短语
|
||||||
|
- API 调试工具
|
||||||
|
|
||||||
|
一键搜索、快速启动,让你的 Mac 飞起来 ✈️
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 神器三:Arc 浏览器 🌈
|
||||||
|
|
||||||
|
重新定义浏览器体验:
|
||||||
|
- 侧边栏标签管理
|
||||||
|
- 空间分组功能
|
||||||
|
- 内置笔记和画板
|
||||||
|
- 极简无干扰模式
|
||||||
|
|
||||||
|
告别标签栏焦虑,专注当下任务!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结 🎯
|
||||||
|
|
||||||
|
效率提升不在于工具多少,而在于是否**真正用起来**。
|
||||||
|
|
||||||
|
选择 2-3 个适合自己的工具,持续使用,形成习惯,你就能:
|
||||||
|
|
||||||
|
✅ 节省 50% 的时间
|
||||||
|
✅ 减少 80% 的焦虑
|
||||||
|
✅ 提升 100% 的专注力
|
||||||
|
|
||||||
|
#效率工具 #生产力 #Mac软件 #工作技巧 #神器推荐 #Notion #Raycast #Arc浏览器
|
||||||
318
assets/styles.css
Normal file
318
assets/styles.css
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
/* 小红书卡片样式 */
|
||||||
|
|
||||||
|
/* CSS 变量定义 */
|
||||||
|
:root {
|
||||||
|
--primary-color: #ff2442;
|
||||||
|
--primary-light: #ff6b81;
|
||||||
|
--text-primary: #1e293b;
|
||||||
|
--text-secondary: #334155;
|
||||||
|
--text-muted: #64748b;
|
||||||
|
--text-light: #475569;
|
||||||
|
--background-light: #f8fafc;
|
||||||
|
--background-gray: #f1f5f9;
|
||||||
|
--border-color: #e2e8f0;
|
||||||
|
--border-radius: 25px;
|
||||||
|
--card-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
--font-family: 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', -apple-system, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 全局样式重置 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================
|
||||||
|
封面卡片样式
|
||||||
|
======================== */
|
||||||
|
|
||||||
|
.cover-container {
|
||||||
|
width: 1080px;
|
||||||
|
height: 1440px;
|
||||||
|
background: linear-gradient(180deg, #3450E4 0%, #D266DA 100%);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-inner {
|
||||||
|
position: absolute;
|
||||||
|
width: 950px;
|
||||||
|
height: 1310px;
|
||||||
|
left: 65px;
|
||||||
|
top: 65px;
|
||||||
|
background: #F3F3F3;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 60px 85px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-emoji {
|
||||||
|
font-size: 180px;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-title {
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 130px;
|
||||||
|
line-height: 1.35;
|
||||||
|
background: linear-gradient(180deg, #2E67B1 0%, #4C4C4C 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: auto;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-subtitle {
|
||||||
|
font-weight: 350;
|
||||||
|
font-size: 72px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #000000;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================
|
||||||
|
正文卡片样式
|
||||||
|
======================== */
|
||||||
|
|
||||||
|
.card-container {
|
||||||
|
width: 1080px;
|
||||||
|
min-height: 1440px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
position: relative;
|
||||||
|
padding: 50px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-inner {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 60px;
|
||||||
|
min-height: calc(1440px - 100px);
|
||||||
|
box-shadow: var(--card-shadow);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Markdown 内容样式 */
|
||||||
|
.card-content {
|
||||||
|
color: var(--text-light);
|
||||||
|
font-size: 42px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content h1 {
|
||||||
|
font-size: 72px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 40px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content h2 {
|
||||||
|
font-size: 56px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 50px 0 25px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content h3 {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-light);
|
||||||
|
margin: 40px 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content p {
|
||||||
|
margin-bottom: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content strong {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content em {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 2px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content ul,
|
||||||
|
.card-content ol {
|
||||||
|
margin: 30px 0;
|
||||||
|
padding-left: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content li {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content blockquote {
|
||||||
|
border-left: 8px solid var(--primary-color);
|
||||||
|
padding-left: 40px;
|
||||||
|
background: var(--background-gray);
|
||||||
|
padding-top: 25px;
|
||||||
|
padding-bottom: 25px;
|
||||||
|
margin: 35px 0;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-style: italic;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content blockquote p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content code {
|
||||||
|
background: var(--background-gray);
|
||||||
|
padding: 6px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||||
|
font-size: 38px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content pre {
|
||||||
|
background: var(--text-primary);
|
||||||
|
color: #e2e8f0;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin: 35px 0;
|
||||||
|
overflow-x: visible;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 36px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content pre code {
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin: 35px auto;
|
||||||
|
display: block;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content hr {
|
||||||
|
border: none;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--border-color);
|
||||||
|
margin: 50px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tags 标签样式 */
|
||||||
|
.card-content .tags {
|
||||||
|
margin-top: 50px;
|
||||||
|
padding-top: 30px;
|
||||||
|
border-top: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content .tag {
|
||||||
|
display: inline-block;
|
||||||
|
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-light) 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border-radius: 30px;
|
||||||
|
font-size: 34px;
|
||||||
|
margin: 10px 15px 10px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 信息卡片样式 */
|
||||||
|
.info-card {
|
||||||
|
margin: 40px 0;
|
||||||
|
padding: 40px 50px;
|
||||||
|
background: linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(139, 92, 246, 0.12) 100%);
|
||||||
|
border: 2px solid rgba(99, 102, 241, 0.2);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.success {
|
||||||
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.12) 0%, rgba(5, 150, 105, 0.12) 100%);
|
||||||
|
border-color: rgba(16, 185, 129, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.warning {
|
||||||
|
background: linear-gradient(135deg, rgba(245, 158, 11, 0.12) 0%, rgba(217, 119, 6, 0.12) 100%);
|
||||||
|
border-color: rgba(245, 158, 11, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.error {
|
||||||
|
background: linear-gradient(135deg, rgba(239, 68, 68, 0.12) 0%, rgba(220, 38, 38, 0.12) 100%);
|
||||||
|
border-color: rgba(239, 68, 68, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页码样式 */
|
||||||
|
.page-number {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 80px;
|
||||||
|
right: 80px;
|
||||||
|
font-size: 36px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================
|
||||||
|
渐变背景预设
|
||||||
|
======================== */
|
||||||
|
|
||||||
|
.bg-gradient-1 {
|
||||||
|
background: linear-gradient(180deg, #3450E4 0%, #D266DA 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-2 {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-3 {
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-4 {
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-5 {
|
||||||
|
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-6 {
|
||||||
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-7 {
|
||||||
|
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient-8 {
|
||||||
|
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
|
||||||
|
}
|
||||||
14
env.example.txt
Normal file
14
env.example.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 小红书 Cookie 配置
|
||||||
|
# 将此文件复制为 .env 并填入真实的 Cookie
|
||||||
|
#
|
||||||
|
# 获取方式:
|
||||||
|
# 1. 在浏览器中登录小红书 (https://www.xiaohongshu.com)
|
||||||
|
# 2. 打开开发者工具 (F12)
|
||||||
|
# 3. 在 Network 标签中,访问任意页面
|
||||||
|
# 4. 查看请求头中的 Cookie,复制完整的 cookie 字符串
|
||||||
|
|
||||||
|
XHS_COOKIE=your_cookie_string_here
|
||||||
|
|
||||||
|
# 注意事项:
|
||||||
|
# - Cookie 有有效期,过期后需要重新获取
|
||||||
|
# - 不要分享你的 Cookie,它包含登录凭证
|
||||||
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "md2redbook",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "小红书笔记卡片渲染工具 - Node.js 版本",
|
||||||
|
"main": "scripts/render_xhs.js",
|
||||||
|
"scripts": {
|
||||||
|
"render": "node scripts/render_xhs.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"marked": "^11.0.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"playwright": "^1.40.0"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"xiaohongshu",
|
||||||
|
"markdown",
|
||||||
|
"image",
|
||||||
|
"card",
|
||||||
|
"generator"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
16
requirements.txt
Normal file
16
requirements.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 小红书笔记创作技能 - Python 依赖
|
||||||
|
|
||||||
|
# Markdown 解析
|
||||||
|
markdown>=3.4.0
|
||||||
|
|
||||||
|
# YAML 解析
|
||||||
|
PyYAML>=6.0
|
||||||
|
|
||||||
|
# 图片渲染 (使用 Playwright)
|
||||||
|
playwright>=1.40.0
|
||||||
|
|
||||||
|
# 小红书 API 客户端
|
||||||
|
xhs
|
||||||
|
|
||||||
|
# 环境变量加载
|
||||||
|
python-dotenv>=1.0.0
|
||||||
202
scripts/publish_xhs.py
Normal file
202
scripts/publish_xhs.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
小红书笔记发布脚本
|
||||||
|
将生成的图片卡片发布到小红书
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
python publish_xhs.py --title "标题" --desc "描述" --images cover.png card_1.png card_2.png
|
||||||
|
|
||||||
|
环境变量:
|
||||||
|
在同目录下创建 .env 文件,配置 XHS_COOKIE:
|
||||||
|
XHS_COOKIE=your_cookie_string_here
|
||||||
|
|
||||||
|
依赖安装:
|
||||||
|
pip install xhs python-dotenv
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from xhs import XhsClient
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"缺少依赖: {e}")
|
||||||
|
print("请运行: pip install xhs python-dotenv")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def load_cookie():
|
||||||
|
"""从 .env 文件加载 Cookie"""
|
||||||
|
# 尝试从当前目录加载 .env
|
||||||
|
env_path = Path.cwd() / '.env'
|
||||||
|
if env_path.exists():
|
||||||
|
load_dotenv(env_path)
|
||||||
|
|
||||||
|
# 也尝试从脚本目录加载
|
||||||
|
script_env = Path(__file__).parent.parent / '.env'
|
||||||
|
if script_env.exists():
|
||||||
|
load_dotenv(script_env)
|
||||||
|
|
||||||
|
cookie = os.getenv('XHS_COOKIE')
|
||||||
|
if not cookie:
|
||||||
|
print("❌ 错误: 未找到 XHS_COOKIE 环境变量")
|
||||||
|
print("请在当前目录创建 .env 文件,添加以下内容:")
|
||||||
|
print("XHS_COOKIE=your_cookie_string_here")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return cookie
|
||||||
|
|
||||||
|
|
||||||
|
def create_client(cookie: str) -> XhsClient:
|
||||||
|
"""创建小红书客户端"""
|
||||||
|
try:
|
||||||
|
# 使用本地签名
|
||||||
|
from xhs.help import sign as local_sign
|
||||||
|
|
||||||
|
def sign_func(uri, data=None, a1="", web_session=""):
|
||||||
|
return local_sign(uri, data, a1=a1)
|
||||||
|
|
||||||
|
client = XhsClient(cookie=cookie, sign=sign_func)
|
||||||
|
return client
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 创建客户端失败: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_images(image_paths: list) -> list:
|
||||||
|
"""验证图片文件是否存在"""
|
||||||
|
valid_images = []
|
||||||
|
for path in image_paths:
|
||||||
|
if os.path.exists(path):
|
||||||
|
valid_images.append(os.path.abspath(path))
|
||||||
|
else:
|
||||||
|
print(f"⚠️ 警告: 图片不存在 - {path}")
|
||||||
|
|
||||||
|
if not valid_images:
|
||||||
|
print("❌ 错误: 没有有效的图片文件")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return valid_images
|
||||||
|
|
||||||
|
|
||||||
|
def publish_note(client: XhsClient, title: str, desc: str, images: list,
|
||||||
|
is_private: bool = False, post_time: str = None):
|
||||||
|
"""发布图文笔记"""
|
||||||
|
try:
|
||||||
|
print(f"\n🚀 准备发布笔记...")
|
||||||
|
print(f" 📌 标题: {title}")
|
||||||
|
print(f" 📝 描述: {desc[:50]}..." if len(desc) > 50 else f" 📝 描述: {desc}")
|
||||||
|
print(f" 🖼️ 图片数量: {len(images)}")
|
||||||
|
|
||||||
|
result = client.create_image_note(
|
||||||
|
title=title,
|
||||||
|
desc=desc,
|
||||||
|
files=images,
|
||||||
|
is_private=is_private,
|
||||||
|
post_time=post_time
|
||||||
|
)
|
||||||
|
|
||||||
|
print("\n✨ 笔记发布成功!")
|
||||||
|
if isinstance(result, dict):
|
||||||
|
note_id = result.get('note_id') or result.get('id')
|
||||||
|
if note_id:
|
||||||
|
print(f" 📎 笔记ID: {note_id}")
|
||||||
|
print(f" 🔗 链接: https://www.xiaohongshu.com/explore/{note_id}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ 发布失败: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_info(client: XhsClient):
|
||||||
|
"""获取当前登录用户信息"""
|
||||||
|
try:
|
||||||
|
info = client.get_self_info()
|
||||||
|
print(f"\n👤 当前用户: {info.get('nickname', '未知')}")
|
||||||
|
return info
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 无法获取用户信息: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='将图片发布为小红书笔记'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--title', '-t',
|
||||||
|
required=True,
|
||||||
|
help='笔记标题(不超过20字)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--desc', '-d',
|
||||||
|
default='',
|
||||||
|
help='笔记描述/正文内容'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--images', '-i',
|
||||||
|
nargs='+',
|
||||||
|
required=True,
|
||||||
|
help='图片文件路径(可以多个)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--private',
|
||||||
|
action='store_true',
|
||||||
|
help='是否设为私密笔记'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--post-time',
|
||||||
|
default=None,
|
||||||
|
help='定时发布时间(格式:2024-01-01 12:00:00)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--dry-run',
|
||||||
|
action='store_true',
|
||||||
|
help='仅验证,不实际发布'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 验证标题长度
|
||||||
|
if len(args.title) > 20:
|
||||||
|
print(f"⚠️ 警告: 标题超过20字,将被截断")
|
||||||
|
args.title = args.title[:20]
|
||||||
|
|
||||||
|
# 加载 Cookie
|
||||||
|
cookie = load_cookie()
|
||||||
|
|
||||||
|
# 验证图片
|
||||||
|
valid_images = validate_images(args.images)
|
||||||
|
|
||||||
|
# 创建客户端
|
||||||
|
client = create_client(cookie)
|
||||||
|
|
||||||
|
# 获取用户信息(验证 Cookie 有效性)
|
||||||
|
get_user_info(client)
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print("\n🔍 验证模式 - 不会实际发布")
|
||||||
|
print(f" 📌 标题: {args.title}")
|
||||||
|
print(f" 📝 描述: {args.desc}")
|
||||||
|
print(f" 🖼️ 图片: {valid_images}")
|
||||||
|
print("\n✅ 验证通过,可以发布")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 发布笔记
|
||||||
|
publish_note(
|
||||||
|
client=client,
|
||||||
|
title=args.title,
|
||||||
|
desc=args.desc,
|
||||||
|
images=valid_images,
|
||||||
|
is_private=args.private,
|
||||||
|
post_time=args.post_time
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
268
scripts/render_xhs.js
Normal file
268
scripts/render_xhs.js
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* 小红书卡片渲染脚本 - Node.js 版本
|
||||||
|
* 将 Markdown 文件渲染为小红书风格的图片卡片
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* node render_xhs.js <markdown_file> [--output-dir <output_directory>]
|
||||||
|
*
|
||||||
|
* 依赖安装:
|
||||||
|
* npm install marked js-yaml playwright
|
||||||
|
* npx playwright install chromium
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { chromium } = require('playwright');
|
||||||
|
const { marked } = require('marked');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
|
||||||
|
// 获取脚本所在目录
|
||||||
|
const SCRIPT_DIR = path.dirname(__dirname);
|
||||||
|
const ASSETS_DIR = path.join(SCRIPT_DIR, 'assets');
|
||||||
|
|
||||||
|
// 卡片尺寸配置 (3:4 比例)
|
||||||
|
const CARD_WIDTH = 1080;
|
||||||
|
const CARD_HEIGHT = 1440;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Markdown 文件,提取 YAML 头部和正文内容
|
||||||
|
*/
|
||||||
|
function parseMarkdownFile(filePath) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
// 解析 YAML 头部
|
||||||
|
const yamlPattern = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
||||||
|
const yamlMatch = content.match(yamlPattern);
|
||||||
|
|
||||||
|
let metadata = {};
|
||||||
|
let body = content;
|
||||||
|
|
||||||
|
if (yamlMatch) {
|
||||||
|
try {
|
||||||
|
metadata = yaml.load(yamlMatch[1]) || {};
|
||||||
|
} catch (e) {
|
||||||
|
metadata = {};
|
||||||
|
}
|
||||||
|
body = content.slice(yamlMatch[0].length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
metadata,
|
||||||
|
body: body.trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按照 --- 分隔符拆分正文为多张卡片内容
|
||||||
|
*/
|
||||||
|
function splitContentBySeparator(body) {
|
||||||
|
const parts = body.split(/\n---+\n/);
|
||||||
|
return parts.filter(part => part.trim()).map(part => part.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Markdown 转换为 HTML
|
||||||
|
*/
|
||||||
|
function convertMarkdownToHtml(mdContent) {
|
||||||
|
// 处理 tags(以 # 开头的标签)
|
||||||
|
const tagsPattern = /((?:#[\w\u4e00-\u9fa5]+\s*)+)$/m;
|
||||||
|
const tagsMatch = mdContent.match(tagsPattern);
|
||||||
|
let tagsHtml = "";
|
||||||
|
|
||||||
|
if (tagsMatch) {
|
||||||
|
const tagsStr = tagsMatch[1];
|
||||||
|
mdContent = mdContent.slice(0, tagsMatch.index).trim();
|
||||||
|
const tags = tagsStr.match(/#([\w\u4e00-\u9fa5]+)/g);
|
||||||
|
if (tags) {
|
||||||
|
tagsHtml = '<div class="tags-container">';
|
||||||
|
for (const tag of tags) {
|
||||||
|
tagsHtml += `<span class="tag">${tag}</span>`;
|
||||||
|
}
|
||||||
|
tagsHtml += '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换 Markdown 为 HTML
|
||||||
|
const html = marked.parse(mdContent, {
|
||||||
|
breaks: true,
|
||||||
|
gfm: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return html + tagsHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载 HTML 模板
|
||||||
|
*/
|
||||||
|
function loadTemplate(templateName) {
|
||||||
|
const templatePath = path.join(ASSETS_DIR, templateName);
|
||||||
|
return fs.readFileSync(templatePath, 'utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成封面 HTML
|
||||||
|
*/
|
||||||
|
function generateCoverHtml(metadata) {
|
||||||
|
let template = loadTemplate('cover.html');
|
||||||
|
|
||||||
|
let emoji = metadata.emoji || '📝';
|
||||||
|
let title = metadata.title || '标题';
|
||||||
|
let subtitle = metadata.subtitle || '';
|
||||||
|
|
||||||
|
// 限制标题和副标题长度
|
||||||
|
if (title.length > 15) {
|
||||||
|
title = title.slice(0, 15);
|
||||||
|
}
|
||||||
|
if (subtitle.length > 15) {
|
||||||
|
subtitle = subtitle.slice(0, 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
template = template.replace('{{EMOJI}}', emoji);
|
||||||
|
template = template.replace('{{TITLE}}', title);
|
||||||
|
template = template.replace('{{SUBTITLE}}', subtitle);
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成正文卡片 HTML
|
||||||
|
*/
|
||||||
|
function generateCardHtml(content, pageNumber = 1, totalPages = 1) {
|
||||||
|
let template = loadTemplate('card.html');
|
||||||
|
|
||||||
|
const htmlContent = convertMarkdownToHtml(content);
|
||||||
|
const pageText = totalPages > 1 ? `${pageNumber}/${totalPages}` : '';
|
||||||
|
|
||||||
|
template = template.replace('{{CONTENT}}', htmlContent);
|
||||||
|
template = template.replace('{{PAGE_NUMBER}}', pageText);
|
||||||
|
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 Playwright 将 HTML 渲染为图片
|
||||||
|
*/
|
||||||
|
async function renderHtmlToImage(htmlContent, outputPath, width = CARD_WIDTH, height = CARD_HEIGHT) {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({
|
||||||
|
viewport: { width, height }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置 HTML 内容
|
||||||
|
await page.setContent(htmlContent, {
|
||||||
|
waitUntil: 'networkidle'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 等待字体加载
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
// 获取实际内容高度
|
||||||
|
const contentHeight = await page.evaluate(() => {
|
||||||
|
const container = document.querySelector('.card-container') || document.querySelector('.cover-container');
|
||||||
|
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确保高度至少为 1440px(3:4 比例)
|
||||||
|
const actualHeight = Math.max(height, contentHeight);
|
||||||
|
|
||||||
|
// 截图
|
||||||
|
await page.screenshot({
|
||||||
|
path: outputPath,
|
||||||
|
clip: { x: 0, y: 0, width, height: actualHeight },
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` ✅ 已生成: ${outputPath}`);
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主渲染函数:将 Markdown 文件渲染为多张卡片图片
|
||||||
|
*/
|
||||||
|
async function renderMarkdownToCards(mdFile, outputDir) {
|
||||||
|
console.log(`\n🎨 开始渲染: ${mdFile}`);
|
||||||
|
|
||||||
|
// 确保输出目录存在
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 Markdown 文件
|
||||||
|
const data = parseMarkdownFile(mdFile);
|
||||||
|
const { metadata, body } = data;
|
||||||
|
|
||||||
|
// 分割正文内容
|
||||||
|
const cardContents = splitContentBySeparator(body);
|
||||||
|
const totalCards = cardContents.length;
|
||||||
|
|
||||||
|
console.log(` 📄 检测到 ${totalCards} 张正文卡片`);
|
||||||
|
|
||||||
|
// 生成封面
|
||||||
|
if (metadata.emoji || metadata.title) {
|
||||||
|
console.log(' 📷 生成封面...');
|
||||||
|
const coverHtml = generateCoverHtml(metadata);
|
||||||
|
const coverPath = path.join(outputDir, 'cover.png');
|
||||||
|
await renderHtmlToImage(coverHtml, coverPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成正文卡片
|
||||||
|
for (let i = 0; i < cardContents.length; i++) {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
console.log(` 📷 生成卡片 ${pageNum}/${totalCards}...`);
|
||||||
|
const cardHtml = generateCardHtml(cardContents[i], pageNum, totalCards);
|
||||||
|
const cardPath = path.join(outputDir, `card_${pageNum}.png`);
|
||||||
|
await renderHtmlToImage(cardHtml, cardPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✨ 渲染完成!图片已保存到: ${outputDir}`);
|
||||||
|
return totalCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析命令行参数
|
||||||
|
*/
|
||||||
|
function parseArgs() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0) {
|
||||||
|
console.log('用法: node render_xhs.js <markdown_file> [--output-dir <output_directory>]');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let markdownFile = null;
|
||||||
|
let outputDir = process.cwd();
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] === '--output-dir' || args[i] === '-o') {
|
||||||
|
outputDir = args[i + 1];
|
||||||
|
i++;
|
||||||
|
} else if (!args[i].startsWith('-')) {
|
||||||
|
markdownFile = args[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!markdownFile) {
|
||||||
|
console.error('❌ 错误: 请指定 Markdown 文件');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(markdownFile)) {
|
||||||
|
console.error(`❌ 错误: 文件不存在 - ${markdownFile}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { markdownFile, outputDir };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
async function main() {
|
||||||
|
const { markdownFile, outputDir } = parseArgs();
|
||||||
|
await renderMarkdownToCards(markdownFile, outputDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('❌ 渲染失败:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
242
scripts/render_xhs.py
Normal file
242
scripts/render_xhs.py
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
小红书卡片渲染脚本 - Python 版本
|
||||||
|
将 Markdown 文件渲染为小红书风格的图片卡片
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
python render_xhs.py <markdown_file> [--output-dir <output_directory>]
|
||||||
|
|
||||||
|
依赖安装:
|
||||||
|
pip install markdown pyyaml pillow playwright
|
||||||
|
playwright install chromium
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import markdown
|
||||||
|
import yaml
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"缺少依赖: {e}")
|
||||||
|
print("请运行: pip install markdown pyyaml playwright && playwright install chromium")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# 获取脚本所在目录
|
||||||
|
SCRIPT_DIR = Path(__file__).parent.parent
|
||||||
|
ASSETS_DIR = SCRIPT_DIR / "assets"
|
||||||
|
|
||||||
|
# 卡片尺寸配置 (3:4 比例)
|
||||||
|
CARD_WIDTH = 1080
|
||||||
|
CARD_HEIGHT = 1440
|
||||||
|
|
||||||
|
|
||||||
|
def parse_markdown_file(file_path: str) -> dict:
|
||||||
|
"""解析 Markdown 文件,提取 YAML 头部和正文内容"""
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 解析 YAML 头部
|
||||||
|
yaml_pattern = r'^---\s*\n(.*?)\n---\s*\n'
|
||||||
|
yaml_match = re.match(yaml_pattern, content, re.DOTALL)
|
||||||
|
|
||||||
|
metadata = {}
|
||||||
|
body = content
|
||||||
|
|
||||||
|
if yaml_match:
|
||||||
|
try:
|
||||||
|
metadata = yaml.safe_load(yaml_match.group(1)) or {}
|
||||||
|
except yaml.YAMLError:
|
||||||
|
metadata = {}
|
||||||
|
body = content[yaml_match.end():]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'metadata': metadata,
|
||||||
|
'body': body.strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def split_content_by_separator(body: str) -> list:
|
||||||
|
"""按照 --- 分隔符拆分正文为多张卡片内容"""
|
||||||
|
# 使用 --- 作为分隔符,但要排除 YAML 头部的 ---
|
||||||
|
parts = re.split(r'\n---+\n', body)
|
||||||
|
return [part.strip() for part in parts if part.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def convert_markdown_to_html(md_content: str) -> str:
|
||||||
|
"""将 Markdown 转换为 HTML"""
|
||||||
|
# 处理 tags(以 # 开头的标签)
|
||||||
|
tags_pattern = r'((?:#[\w\u4e00-\u9fa5]+\s*)+)$'
|
||||||
|
tags_match = re.search(tags_pattern, md_content, re.MULTILINE)
|
||||||
|
tags_html = ""
|
||||||
|
|
||||||
|
if tags_match:
|
||||||
|
tags_str = tags_match.group(1)
|
||||||
|
md_content = md_content[:tags_match.start()].strip()
|
||||||
|
tags = re.findall(r'#([\w\u4e00-\u9fa5]+)', tags_str)
|
||||||
|
if tags:
|
||||||
|
tags_html = '<div class="tags-container">'
|
||||||
|
for tag in tags:
|
||||||
|
tags_html += f'<span class="tag">#{tag}</span>'
|
||||||
|
tags_html += '</div>'
|
||||||
|
|
||||||
|
# 转换 Markdown 为 HTML
|
||||||
|
html = markdown.markdown(
|
||||||
|
md_content,
|
||||||
|
extensions=['extra', 'codehilite', 'tables', 'nl2br']
|
||||||
|
)
|
||||||
|
|
||||||
|
return html + tags_html
|
||||||
|
|
||||||
|
|
||||||
|
def load_template(template_name: str) -> str:
|
||||||
|
"""加载 HTML 模板"""
|
||||||
|
template_path = ASSETS_DIR / template_name
|
||||||
|
with open(template_path, 'r', encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_cover_html(metadata: dict) -> str:
|
||||||
|
"""生成封面 HTML"""
|
||||||
|
template = load_template('cover.html')
|
||||||
|
|
||||||
|
emoji = metadata.get('emoji', '📝')
|
||||||
|
title = metadata.get('title', '标题')
|
||||||
|
subtitle = metadata.get('subtitle', '')
|
||||||
|
|
||||||
|
# 限制标题和副标题长度
|
||||||
|
if len(title) > 15:
|
||||||
|
title = title[:15]
|
||||||
|
if len(subtitle) > 15:
|
||||||
|
subtitle = subtitle[:15]
|
||||||
|
|
||||||
|
html = template.replace('{{EMOJI}}', emoji)
|
||||||
|
html = html.replace('{{TITLE}}', title)
|
||||||
|
html = html.replace('{{SUBTITLE}}', subtitle)
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
def generate_card_html(content: str, page_number: int = 1, total_pages: int = 1) -> str:
|
||||||
|
"""生成正文卡片 HTML"""
|
||||||
|
template = load_template('card.html')
|
||||||
|
|
||||||
|
html_content = convert_markdown_to_html(content)
|
||||||
|
|
||||||
|
page_text = f"{page_number}/{total_pages}" if total_pages > 1 else ""
|
||||||
|
|
||||||
|
html = template.replace('{{CONTENT}}', html_content)
|
||||||
|
html = html.replace('{{PAGE_NUMBER}}', page_text)
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
|
||||||
|
async def render_html_to_image(html_content: str, output_path: str, width: int = CARD_WIDTH, height: int = CARD_HEIGHT):
|
||||||
|
"""使用 Playwright 将 HTML 渲染为图片"""
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch()
|
||||||
|
page = await browser.new_page(viewport={'width': width, 'height': height})
|
||||||
|
|
||||||
|
# 创建临时 HTML 文件
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
|
||||||
|
f.write(html_content)
|
||||||
|
temp_html_path = f.name
|
||||||
|
|
||||||
|
try:
|
||||||
|
await page.goto(f'file://{temp_html_path}')
|
||||||
|
await page.wait_for_load_state('networkidle')
|
||||||
|
|
||||||
|
# 等待字体加载
|
||||||
|
await page.wait_for_timeout(500)
|
||||||
|
|
||||||
|
# 获取实际内容高度
|
||||||
|
content_height = await page.evaluate('''() => {
|
||||||
|
const container = document.querySelector('.card-container') || document.querySelector('.cover-container');
|
||||||
|
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||||
|
}''')
|
||||||
|
|
||||||
|
# 确保高度至少为 1440px(3:4 比例)
|
||||||
|
actual_height = max(height, content_height)
|
||||||
|
|
||||||
|
# 截图
|
||||||
|
await page.screenshot(
|
||||||
|
path=output_path,
|
||||||
|
clip={'x': 0, 'y': 0, 'width': width, 'height': actual_height},
|
||||||
|
type='png'
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" ✅ 已生成: {output_path}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
os.unlink(temp_html_path)
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def render_markdown_to_cards(md_file: str, output_dir: str):
|
||||||
|
"""主渲染函数:将 Markdown 文件渲染为多张卡片图片"""
|
||||||
|
print(f"\n🎨 开始渲染: {md_file}")
|
||||||
|
|
||||||
|
# 确保输出目录存在
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 解析 Markdown 文件
|
||||||
|
data = parse_markdown_file(md_file)
|
||||||
|
metadata = data['metadata']
|
||||||
|
body = data['body']
|
||||||
|
|
||||||
|
# 分割正文内容
|
||||||
|
card_contents = split_content_by_separator(body)
|
||||||
|
total_cards = len(card_contents)
|
||||||
|
|
||||||
|
print(f" 📄 检测到 {total_cards} 张正文卡片")
|
||||||
|
|
||||||
|
# 生成封面
|
||||||
|
if metadata.get('emoji') or metadata.get('title'):
|
||||||
|
print(" 📷 生成封面...")
|
||||||
|
cover_html = generate_cover_html(metadata)
|
||||||
|
cover_path = os.path.join(output_dir, 'cover.png')
|
||||||
|
await render_html_to_image(cover_html, cover_path)
|
||||||
|
|
||||||
|
# 生成正文卡片
|
||||||
|
for i, content in enumerate(card_contents, 1):
|
||||||
|
print(f" 📷 生成卡片 {i}/{total_cards}...")
|
||||||
|
card_html = generate_card_html(content, i, total_cards)
|
||||||
|
card_path = os.path.join(output_dir, f'card_{i}.png')
|
||||||
|
await render_html_to_image(card_html, card_path)
|
||||||
|
|
||||||
|
print(f"\n✨ 渲染完成!图片已保存到: {output_dir}")
|
||||||
|
return total_cards
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='将 Markdown 文件渲染为小红书风格的图片卡片'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'markdown_file',
|
||||||
|
help='Markdown 文件路径'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--output-dir', '-o',
|
||||||
|
default=os.getcwd(),
|
||||||
|
help='输出目录(默认为当前工作目录)'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not os.path.exists(args.markdown_file):
|
||||||
|
print(f"❌ 错误: 文件不存在 - {args.markdown_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
asyncio.run(render_markdown_to_cards(args.markdown_file, args.output_dir))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user