refactor md2Redbook skill with themes and paging

This commit is contained in:
ZhangJia
2026-01-29 15:52:15 +08:00
parent 04a17ed78a
commit 13add64b8b
47 changed files with 3209 additions and 615 deletions

273
README.md
View File

@@ -1,165 +1,151 @@
# 📕 Auto-Redbook-Skills ## 📕 Auto-Redbook-Skills(已重构版)
> 一个自动撰写笔记、生成图片、自动发布小红书的 Skills > 自动撰写小红书笔记、生成多主题卡片、可选自动发布的 Skills
> 当前版本对渲染脚本和样式系统做了**一次完整重构**,感谢 Cursor 的辅助开发 🙌
[![Python](https://img.shields.io/badge/Python-3.8+-blue.svg)](https://www.python.org/)
[![Node.js](https://img.shields.io/badge/Node.js-16+-green.svg)](https://nodejs.org/)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
--- ---
## 🆕 v2.0 版本更新 ## ✨ 本次重构亮点
### ✨ 新增功能 - **🎨 8 套主题皮肤**:默认简约灰 + Playful Geometric / Neo-Brutalism / Botanical / Professional / Retro / Terminal / Sketch
- **📐 4 种分页模式**
- **🚀 智能分页渲染** - 自动检测内容高度,超出时自动拆分到多张卡片,彻底解决文字溢出问题 - `separator`:按 `---` 分隔手动分页
- **🎨 7种可选样式** - 新增多种主题风格,一键切换不同视觉效果 - `auto-fit`:固定尺寸,自动整体缩放内容,避免溢出/大面积留白
- **⚡ V2 渲染脚本** - 全新 `render_xhs_v2.py` / `render_xhs_v2.js`,推荐升级使用 - `auto-split`:根据渲染后高度自动拆分为多张卡片
- `dynamic`:根据内容动态调整图片高度
### 📋 可用样式列表 - **🧱 统一卡片结构**:外层浅灰背景(`card-container`+ 内层主题背景(`card-inner`+ 纯排版层(`card-content`
- **🧠 封面与正文一体化**:封面背景、标题渐变和正文卡片背景都按主题自动匹配
| 样式 | 名称 | 预览 |
|------|------|------|
| `purple` | 紫韵(默认)| 蓝紫色渐变 |
| `xiaohongshu` | 小红书红 | 品牌红色系 |
| `mint` | 清新薄荷 | 绿色自然调 |
| `sunset` | 日落橙 | 粉橙浪漫调 |
| `ocean` | 深海蓝 | 海洋蓝色调 |
| `elegant` | 优雅白 | 灰白简约调 |
| `dark` | 暗黑模式 | 深色高对比 |
### 🎯 使用 V2 脚本
```bash
# Python 版本
python scripts/render_xhs_v2.py note.md --style sunset
# Node.js 版本
node scripts/render_xhs_v2.js note.md --style ocean
# 查看所有样式
python scripts/render_xhs_v2.py --list-styles
```
### 📁 v2.0 新增文件
```
scripts/
├── render_xhs_v2.py # 新增Python 智能分页版(推荐)
├── render_xhs_v2.js # 新增Node.js 智能分页版(推荐)
├── render_xhs.py # 旧版:保留兼容
└── render_xhs.js # 旧版:保留兼容
STYLES.md # 新增:样式选择指南
```
**注V1 旧版脚本保留兼容V2 版本完全向下兼容 Markdown 格式。**
--- ---
## ✨ 功能特性 ## 🖼 主题效果示例(来自 `demos`
- 📝 **撰写笔记** - 根据既定主题,撰写小红书笔记(提示词自己调整,在 `SKILL.md`里) > 所有示例均为 1080×1440px小红书推荐 3:4 比例
- 🎨 **生成卡片** - 根据内容自动渲染生成图片,包含 cover 和内容详情,支持 Markdown 渲染
- 🐍 **双语言脚本** - 提供 Python 和 Node.js 两种渲染方案
- 📤 **一键发布** - 支持直接发布到小红书(需配置 Cookie
### Playful Geometric活泼几何
## 🚀 快速开始 ![Playful Geometric](demos/playful-geometric/card_1.png)
### Clone 项目 ### Retro复古怀旧
Clone 项目到本地 ![Retro](demos/retro/card_1.png)
### Sketch手绘素描
> 注意:该目录在 demos 中是大写 `Sketch`
![Sketch](demos/Sketch/card_1.png)
### Terminal终端风格
![Terminal](demos/terminal/card_1.png)
### Auto-fit 模式示例(自动缩放)
![Auto Fit](demos/auto-fit/card_1.png)
---
## 🚀 使用方式总览
### 1. 克隆项目
```bash ```bash
git clone https://github.com/comeonzhj/Auto-Redbook-Skills.git git clone https://github.com/comeonzhj/Auto-Redbook-Skills.git
cd Auto-Redbook-Skills
``` ```
移动到支持 Skills 的客户端对应文件夹里 可以将本项目放到支持 Skills 的客户端目录,例如
- For Claude `~/.claude/skills/` - Claude`~/.claude/skills/`
- For Alma `~/.config/Alma/skills/` - Alma`~/.config/Alma/skills/`
- For TRAE `/your-path/.trae/skills/` - TRAE`/your-path/.trae/skills/`
### 安装依赖 ### 2. 安装依赖
**Python 版本** **Python**
```bash ```bash
pip install markdown pyyaml playwright python-dotenv xhs pip install -r requirements.txt
playwright install chromium playwright install chromium
``` ```
**Node.js 版本** **Node.js**
```bash ```bash
cd Auto-Redbook-Skills
npm install npm install
npx playwright install chromium npx playwright install chromium
``` ```
## 🎨 渲染图片 ---
### V2 渲染(推荐 ## 🎨 渲染图片Python
核心脚本:`scripts/render_xhs.py`
```bash ```bash
# 使用默认样式 # 最简单用法(默认主题 + 手动分页)
python scripts/render_xhs_v2.py note.md python scripts/render_xhs.py demos/content.md
# 指定样式主题 # 使用自动分页(推荐:内容长短难控)
python scripts/render_xhs_v2.py note.md --style sunset python scripts/render_xhs.py demos/content.md -m auto-split
# 指定输出目录 # 使用固定尺寸自动缩放auto-fit
python scripts/render_xhs_v2.py note.md -o ./output --style xiaohongshu python scripts/render_xhs.py demos/content_auto_fit.md -m auto-fit
# 查看所有样式 # 切换主题(例如 Playful Geometric
python scripts/render_xhs_v2.py --list-styles python scripts/render_xhs.py demos/content.md -t playful-geometric -m auto-split
# 自定义尺寸和像素比
python scripts/render_xhs.py demos/content.md -t retro -m dynamic --width 1080 --height 1440 --max-height 2160 --dpr 2
``` ```
**V2 特性** **主要参数**
- 智能分页:自动检测内容高度,自动拆分卡片
- 固定尺寸:所有卡片固定 1080×1440px
- 多种样式7种预设主题风格
### V1 渲染(旧版) | 参数 | 简写 | 说明 |
|------|------|------|
| `--theme` | `-t` | 主题:`default``playful-geometric``neo-brutalism``botanical``professional``retro``terminal``sketch` |
| `--mode` | `-m` | 分页模式:`separator` / `auto-fit` / `auto-split` / `dynamic` |
| `--width` | `-w` | 图片宽度(默认 1080 |
| `--height` | | 图片高度(默认 1440`dynamic` 为最小高度) |
| `--max-height` | | `dynamic` 模式最大高度(默认 2160 |
| `--dpr` | | 设备像素比,控制清晰度(默认 2 |
> 生成结果会包含:封面 `cover.png` + 正文卡片 `card_1.png`、`card_2.png`...
---
## 🎨 渲染图片Node.js
脚本:`scripts/render_xhs.js`,参数与 Python 基本一致:
```bash ```bash
# Python 版本 # 默认主题 + 手动分页
python scripts/render_xhs.py note.md node scripts/render_xhs.js demos/content.md
# Node.js 版本 # 指定主题 + 自动分页
node scripts/render_xhs.js note.md node scripts/render_xhs.js demos/content.md -t terminal -m auto-split
``` ```
**注意:** V1 版本当内容过长时可能出现溢出,建议手动使用 `---` 分隔内容。 ---
## 📤 发布到小红书 ## 📤 发布到小红书
### 1. 配置 Cookie ### 1. 配置 Cookie
复制 `env.example.txt``.env`,填入小红书 Cookie
```bash ```bash
cp env.example.txt .env cp env.example.txt .env
``` ```
编辑 `.env` 文件 编辑 `.env`
``` ```env
XHS_COOKIE=your_cookie_string_here XHS_COOKIE=your_cookie_string_here
``` ```
**获取 Cookie 方法:** > 获取方式:浏览器登录小红书 → F12 → Network → 任意请求的 Cookie 头,复制整串。
1. 在浏览器中登录 [小红书](https://www.xiaohongshu.com) ### 2. 手动发布(可选)
2. 打开开发者工具F12
3. 在 Network 标签中查看任意请求的 Cookie 头
4. 复制完整的 cookie 字符串
### 2. 发布笔记
Skills 会自动发布,也可以手动执行:
```bash ```bash
python scripts/publish_xhs.py \ python scripts/publish_xhs.py \
@@ -174,78 +160,67 @@ python scripts/publish_xhs.py \
|------|------| |------|------|
| `--private` | 设为私密笔记 | | `--private` | 设为私密笔记 |
| `--post-time "2024-01-01 12:00:00"` | 定时发布 | | `--post-time "2024-01-01 12:00:00"` | 定时发布 |
| `--api-mode` | 通过 xhs-api 服务发布 |
| `--dry-run` | 仅验证,不实际发布 | | `--dry-run` | 仅验证,不实际发布 |
## 🎨 自定义样式 ---
### 修改背景渐变V1 ## 📁 项目结构(重构后
编辑 `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` |
### 更多样式选择V2
V2 版本提供 7 种内置样式,通过 `--style` 参数快速切换:
```bash ```bash
python scripts/render_xhs_v2.py note.md --style dark # 暗黑模式 Auto-Redbook-Skills/
python scripts/render_xhs_v2.py note.md --style mint # 清新薄荷 ├── SKILL.md # 技能描述Agent 使用说明)
``` ├── README.md # 项目文档(你现在看到的)
详见 [STYLES.md](./STYLES.md)
## 📁 项目结构
```
md2Redbook/
├── SKILL.md # 技能描述AI Agent 使用)
├── README.md # 项目文档
├── STYLES.md # 样式选择指南
├── requirements.txt # Python 依赖 ├── requirements.txt # Python 依赖
├── package.json # Node.js 依赖 ├── package.json # Node.js 依赖
├── env.example.txt # Cookie 配置示例 ├── env.example.txt # Cookie 配置示例
├── assets/ ├── assets/
│ ├── cover.html # 封面 HTML 模板 │ ├── cover.html # 封面 HTML 模板
│ ├── card.html # 正文卡片 HTML 模板 │ ├── card.html # 正文卡片 HTML 模板
│ ├── styles.css # 共用样式表 │ ├── styles.css # 共用容器样式cover-inner / card-inner 等)
│ └── example.md # 示例 Markdown │ └── example.md # 示例 Markdown
├── assets/themes/ # 主题样式(只控制排版 & 内层背景)
│ ├── default.css
│ ├── playful-geometric.css
│ ├── neo-brutalism.css
│ ├── botanical.css
│ ├── professional.css
│ ├── retro.css
│ ├── terminal.css
│ └── sketch.css
├── demos/ # 各主题示例渲染结果
│ ├── content.md
│ ├── content_auto_fit.md
│ ├── auto-fit/
│ ├── playful-geometric/
│ ├── retro/
│ ├── Sketch/
│ └── terminal/
└── scripts/ └── scripts/
├── render_xhs_v2.py # Python 渲染脚本 V2推荐 ├── render_xhs.py # Python 渲染脚本(支持主题 + 分页模式)
├── render_xhs_v2.js # Node.js 渲染脚本 V2推荐 ├── render_xhs.js # Node.js 渲染脚本
├── render_xhs.py # Python 渲染脚本 V1
├── render_xhs.js # Node.js 渲染脚本 V1
└── publish_xhs.py # 小红书发布脚本 └── publish_xhs.py # 小红书发布脚本
``` ```
---
## ⚠️ 注意事项 ## ⚠️ 注意事项
1. **Cookie 安全** - Cookie 包含登录凭证,请勿泄露或提交到版本控制 1. **Cookie 安全**:不要把 `.env` 提交到 Git 或共享出去。
2. **Cookie 有效期** - 小红书 Cookie 会过期,需定期更新 2. **Cookie 有效期**:过期后发布失败是正常现象,重新抓一次 Cookie 即可。
3. **发布频率** - 避免频繁发布,以免触发平台限制 3. **发布频率**:避免短时间内高频发布,以免触发平台风控。
4. **图片尺寸** - 渲染的图片为 1080×1440px符合小红书推荐比例 4. **图片尺寸**:默认 1080×1440px符合小红书推荐比例
---
## 🙏 致谢 ## 🙏 致谢
- [Playwright](https://playwright.dev/) - 浏览器自动化渲染 - [Playwright](https://playwright.dev/) - 浏览器自动化渲染
- [Marked](https://marked.js.org/) - Markdown 解析 - [Marked](https://marked.js.org/) - Markdown 解析
- [Madopic](https://github.com/xiaolinbaba/Madopic) - Markdown 渲染
- [xhs](https://github.com/ReaJason/xhs) - 小红书 API 客户端 - [xhs](https://github.com/ReaJason/xhs) - 小红书 API 客户端
- **Cursor** - 本次重构过程中提供了极大帮助 ❤️
---
## 📄 License ## 📄 License

169
SKILL.md
View File

@@ -1,18 +1,17 @@
--- ---
name: Auto-Redbook name: xhs-note-creator
description: 小红书笔记素材创作技能。当用户需要创建小红书笔记素材时使用这个技能。技能包含:根据用户的需求和提供的资料,撰写小红书笔记内容(标题+正文),生成图片卡片(封面+正文卡片,支持多种样式主题) description: 小红书笔记素材创作技能。当用户需要创建小红书笔记素材时使用这个技能。技能包含:根据用户的需求和提供的资料,撰写小红书笔记内容(标题+正文),生成图片卡片(封面+正文卡片),以及发布小红书笔记
--- ---
# 小红书笔记创作技能 # 小红书笔记创作技能
这个技能用于创建专业的小红书笔记素材,包括内容撰写、图片卡片生成支持7种样式主题和智能分页渲染 这个技能用于创建专业的小红书笔记素材,包括内容撰写、图片卡片生成和笔记发布
## 使用场景 ## 使用场景
- 用户需要创建小红书笔记时 - 用户需要创建小红书笔记时
- 用户提供资料需要转化为小红书风格内容时 - 用户提供资料需要转化为小红书风格内容时
- 用户需要生成精美的图片卡片用于发布时 - 用户需要生成精美的图片卡片用于发布时
- 用户需要多种风格样式选择时
## 工作流程 ## 工作流程
@@ -48,11 +47,11 @@ subtitle: "副标题文案" # 封面副标题不超过15字
``` ```
2. 用于渲染卡片的 Markdown 文本内容: 2. 用于渲染卡片的 Markdown 文本内容:
- 使用 `---` 分割线将正文分隔为多个卡片段落 - 当待渲染内容必须严格切分为独立的数张图片时,可使用 `---` 分割线主动将正文分隔为多个卡片段落(每个段落文本控制在 200 字左右),输出图片时使用参数`-m separator`
- 每个分段的文字控制在 200 字左右 - 当待渲染内容无需严格分割,生成正常 Markdown 文本即可,跟下方分页模式参数规则按需选择
- 脚本会自动检测内容高度并智能分页
完整 Markdown 文档内容示例:
完整示例:
```markdown ```markdown
--- ---
emoji: "💡" emoji: "💡"
@@ -60,7 +59,7 @@ title: "5个效率神器让工作效率翻倍"
subtitle: "对着抄作业就好了,一起变高效" subtitle: "对着抄作业就好了,一起变高效"
--- ---
# 神器一Notion 📝 # 📝 神器一Notion
> 全能型笔记工具,支持数据库、看板、日历等多种视图... > 全能型笔记工具,支持数据库、看板、日历等多种视图...
@@ -69,11 +68,11 @@ subtitle: "对着抄作业就好了,一起变高效"
- 特色一 - 特色一
- 特色二 - 特色二
--- # ⚡ 神器二Raycast
# 神器二Raycast ⚡
\`\`\`
可使用代码块来增加渲染后图片的视觉丰富度 可使用代码块来增加渲染后图片的视觉丰富度
\`\`\`
## 推荐原因 ## 推荐原因
@@ -81,82 +80,82 @@ subtitle: "对着抄作业就好了,一起变高效"
- 原因二 - 原因二
- …… - ……
---
# 神器三Arc 🌈 # 🌈 神器三Arc
全新理念的浏览器,侧边栏标签管理... 全新理念的浏览器,侧边栏标签管理...
... ...
#效率工具 #生产力 #Mac软件
``` ```
### 第三步:渲染图片卡片 ### 第三步:渲染图片卡片
将 Markdown 文档渲染为图片卡片。**推荐使用 V2 版本脚本**,支持智能分页和多种样式。 将 Markdown 文档渲染为图片卡片。使用以下脚本渲染:
#### V2 渲染脚本(推荐)
V2 版本新增特性:
-**智能分页**:自动检测内容高度,超出时自动拆分到多张卡片
-**多种样式**:支持 7 种预设样式主题
-**字数预估**:基于字数预分配内容,减少渲染次数
**Python 版本:**
```bash ```bash
# 基本用法 python scripts/render_xhs.py <markdown_file> [options]
python scripts/render_xhs_v2.py <markdown_file>
# 指定输出目录
python scripts/render_xhs_v2.py <markdown_file> -o <output_directory>
# 指定样式主题
python scripts/render_xhs_v2.py <markdown_file> --style xiaohongshu
# 查看所有可用样式
python scripts/render_xhs_v2.py --list-styles
``` ```
**Node.js 版本:** - 默认输出目录为当前工作目录
- 生成的图片包括封面cover.png和正文卡片card_1.png, card_2.png, ...
#### 渲染参数Python
| 参数 | 简写 | 说明 | 默认值 |
|---|---|---|---|
| `--output-dir` | `-o` | 输出目录 | 当前工作目录 |
| `--theme` | `-t` | 排版主题 | `default` |
| `--mode` | `-m` | 分页模式 | `separator` |
| `--width` | `-w` | 图片宽度 | `1080` |
| `--height` | | 图片高度(`dynamic` 下为最小高度) | `1440` |
| `--max-height` | | `dynamic` 最大高度 | `4320` |
| `--dpr` | | 设备像素比(清晰度) | `2` |
#### 排版主题(`--theme`
- `default`:默认简约浅灰渐变背景(`#f3f3f3 -> #f9f9f9`
- `playful-geometric`活泼几何Memphis
- `neo-brutalism`:新粗野主义
- `botanical`:植物园自然
- `professional`:专业商务
- `retro`:复古怀旧
- `terminal`:终端命令行
- `sketch`:手绘素描
#### 分页模式(`--mode`
- `separator`:按 `---` 分隔符分页(适合内容已手动控量)
- `auto-fit`:固定尺寸下自动缩放文字,避免溢出/留白(适合封面+单张图片但尺寸固定的情况)
- `auto-split`:按渲染后高度自动切分分页(适合切分不影响阅读的长文内容)
- `dynamic`:根据内容动态调整图片高度(注意:图片最高 4320字数超过 550 的不建使用此模式)
#### 常用示例
```bash ```bash
# 基本用法 # 1) 默认主题 + 手动分隔分页
node scripts/render_xhs_v2.js <markdown_file> python scripts/render_xhs.py content.md -m separator
# 指定输出目录和样式 # 2) 固定 1080x1440自动缩放文字尽量填满画面
node scripts/render_xhs_v2.js <markdown_file> -o ./output --style mint python scripts/render_xhs.py content.md -m auto-fit
# 查看所有可用样式 # 3) 自动切分分页(推荐:内容长短不稳定)
node scripts/render_xhs_v2.js --list-styles python scripts/render_xhs.py content.md -m auto-split
# 4) 动态高度(允许不同高度卡片)
python scripts/render_xhs.py content.md -m dynamic --max-height 4320
# 5) 切换主题
python scripts/render_xhs.py content.md -t playful-geometric -m auto-split
``` ```
#### 可用样式主题 #### Node.js 渲染(可选)
| 样式键 | 名称 | 描述 |
|--------|------|------|
| `purple` | 紫韵 | 默认样式,紫蓝色渐变 |
| `xiaohongshu` | 小红书红 | 小红书品牌色系 |
| `mint` | 清新薄荷 | 绿色/自然调 |
| `sunset` | 日落橙 | 粉色/日落渐变 |
| `ocean` | 深海蓝 | 蓝绿色海洋调 |
| `elegant` | 优雅白 | 简约灰白调 |
| `dark` | 暗黑模式 | 深色背景,高对比度 |
#### 旧版渲染脚本(保留)
如需使用旧版(不支持自动分页):
```bash ```bash
# Python 版本 node scripts/render_xhs.js content.md -t default -m separator
python scripts/render_xhs.py <markdown_file> [--output-dir <output_directory>]
# Node.js 版本
node scripts/render_xhs.js <markdown_file> [--output-dir <output_directory>]
``` ```
**旧版已知问题**:单张卡片内容过多时可能出现文字溢出,需手动用 `---` 分隔 Node.js 参数与 Python 基本一致:`--output-dir/-o``--theme/-t``--mode/-m``--width/-w``--height``--max-height``--dpr`
### 第四步:发布小红书笔记(可选) ### 第四步:发布小红书笔记(可选)
@@ -168,7 +167,7 @@ python scripts/publish_xhs.py --title "笔记标题" --desc "笔记描述" --ima
**前置条件** **前置条件**
1. 在同目录下创建 `.env` 文件,配置小红书 Cookie 1. 配置小红书 Cookie
``` ```
XHS_COOKIE=your_cookie_string_here XHS_COOKIE=your_cookie_string_here
``` ```
@@ -184,46 +183,30 @@ XHS_COOKIE=your_cookie_string_here
- 尺寸比例3:4小红书推荐比例 - 尺寸比例3:4小红书推荐比例
- 基准尺寸1080×1440px - 基准尺寸1080×1440px
- 包含Emoji 装饰、大标题、副标题 - 包含Emoji 装饰、大标题、副标题
- 样式:渐变背景 + 圆角内容区(根据所选主题变化) - 样式:渐变背景 + 圆角内容区
### 正文卡片 ### 正文卡片
- 尺寸比例3:4 - 尺寸比例3:4
- 基准尺寸1080×1440px - 基准尺寸1080×1440px
- 支持:标题、段落、列表、引用、代码块、图片 - 支持:标题、段落、列表、引用、代码块、图片
- 样式:白色卡片 + 渐变背景边框(根据所选主题变化) - 样式:白色卡片 + 渐变背景边框
- V2 版本:自动分页,单张卡片内容不会溢出
## 技能资源 ## 技能资源
### 脚本文件 ### 脚本文件
- `scripts/render_xhs.py` - Python V1 渲染脚本(旧版) - `scripts/render_xhs.py` - Python 渲染脚本
- `scripts/render_xhs.js` - Node.js V1 渲染脚本(旧版) - `scripts/render_xhs.js` - Node.js 渲染脚本
- `scripts/render_xhs_v2.py` - Python V2 渲染脚本(推荐 ✅)
- `scripts/render_xhs_v2.js` - Node.js V2 渲染脚本(推荐 ✅)
- `scripts/publish_xhs.py` - 小红书发布脚本 - `scripts/publish_xhs.py` - 小红书发布脚本
### 资源文件 ### 资源文件
- `assets/cover.html` - 封面 HTML 模板(旧版) - `assets/cover.html` - 封面 HTML 模板
- `assets/card.html` - 正文卡片 HTML 模板(旧版) - `assets/card.html` - 正文卡片 HTML 模板
- `assets/styles.css` - 共用样式表(旧版) - `assets/styles.css` - 共用样式表
- `assets/example.md` - 示例 Markdown 文件
## 注意事项 ## 注意事项
1. **V2 版本推荐**V2 版本支持智能分页,可自动处理内容溢出问题 1. Markdown 文件应保存在工作目录,渲染后的图片也保存在工作目录
2. **样式选择**:根据内容风格选择合适的样式主题 2. 技能目录 (`md2Redbook/`) 仅存放脚本和模板,不存放用户数据
3. **Markdown 位置**Markdown 文件应保存在工作目录,渲染后的图片也保存在工作目录 3. 图片尺寸会根据内容自动调整,但保持 3:4 比例
4. **内容长度**:建议每个 `---` 分隔的内容块控制在 200 字以内 4. Cookie 有有效期限制,过期后需要重新获取
5. **Cookie 有效期**:发布功能的 Cookie 有过期限制,过期后需要重新获取 5. 发布功能依赖 xhs 库,需要安装:`pip install xhs`
6. **发布依赖**:发布功能依赖 xhs 库,需要安装:`pip install xhs`
## 智能分页说明
V2 版本的智能分页机制:
1. **预估阶段**:基于字数、元素类型预估内容高度
2. **预渲染阶段**:使用 Playwright 预渲染并测量实际高度
3. **拆分阶段**:如果内容超出,按段落/行智能拆分内容
4. **固定输出**:每张卡片固定为 1080×1440px确保一致性
这种机制确保无论内容多长,都不会出现文字溢出问题。

View File

@@ -23,7 +23,7 @@
.cover-container { .cover-container {
width: 1080px; width: 1080px;
height: 1440px; height: 1440px;
background: linear-gradient(180deg, #3450E4 0%, #D266DA 100%); background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@@ -51,7 +51,7 @@
font-weight: 900; font-weight: 900;
font-size: 130px; font-size: 130px;
line-height: 1.4; line-height: 1.4;
background: linear-gradient(180deg, #2E67B1 0%, #4C4C4C 100%); background: linear-gradient(180deg, #111827 0%, #4B5563 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;

View File

@@ -1,84 +1,63 @@
--- ---
emoji: "🚀" emoji: "🎨"
title: "5个效率神器" title: "8种超美排版风格"
subtitle: "让工作效率翻倍" subtitle: "小红书笔记创作神器"
--- ---
# 神器一Notion 📝 # 活泼几何风格 💜
全能型笔记工具,支持数据库、看板、日历等多种视图 这是 **Playful Geometric** 风格的示例,灵感来自 Memphis 设计
> 一个工具替代十个 App笔记、任务、项目管理全搞定 > 糖果般的色彩搭配,硬边框与圆角的对比,让内容更有活力
**核心功能:** ## 特点
- 📊 灵活的数据库视图
- 🔗 双向链接 - 紫色、粉色、黄色的俏皮配色
- 🎨 丰富的模板库 - 硬阴影贴纸效果
- 👥 团队协作 - 非对称圆角设计
--- ---
# 神器二Raycast # 新粗野主义风格
Mac 上的效率启动器,比 Spotlight 强大 100 倍! **Neo-Brutalism** 风格RAW. LOUD. UNAPOLOGETIC.
> 厚重的黑色边框,高饱和度色块,这就是反设计美学!
## 视觉特点
- 5px 以上的粗边框
- 8-10px 的硬阴影
- 荧光黄与电光青的撞色
---
# 植物园风格 🌿
**Botanical** 风格带来自然清新的感觉。
> 森林绿与米白的搭配,仿佛置身于植物园中
## 设计理念
- 柔和的绿色系配色
- 自然有机的圆角
- 温暖的米色背景
---
# 更多风格选择
还有更多精美风格等你探索:
1. **Professional** - 专业商务风格
2. **Retro** - 复古怀旧风格
3. **Terminal** - 终端命令行风格
4. **Sketch** - 手绘素描风格
使用命令:
```bash ```bash
# 快捷命令示例 python render_xhs.py example.md -t playful-geometric
raycast://extensions/raycast/clipboard/clipboard-history
``` ```
**必装插件推荐:** #小红书模板 #排版设计 #内容创作
- 剪贴板历史
- 窗口管理
- 快捷短语
- API 调试工具
---
# 神器三Arc 浏览器 🌈
全新理念的浏览器体验:
- 侧边栏标签管理
- 空间分组功能
- 内置笔记和画板
- 极简无干扰模式
---
# 神器四Warp 终端 🖥️
基于 Rust 的现代化终端:
```python
# 支持 AI 智能补全
def example():
print("Hello Warp!")
```
- ⚡ 极速性能
- 🤖 AI 智能提示
- 📋 自动补全
- 🎯 分组工作区
---
# 神器五Fig 自动补全 🔮
终端命令自动补全神器:
- 数百种 CLI 工具支持
- 可视化参数提示
- 团队协作分享
---
# 总结 🎯
效率提升不在于工具多少,而在于是否**真正用起来**。
选择 2-3 个适合自己的工具,持续使用,形成习惯,你就能:
✅ 节省 50% 的时间
✅ 减少 80% 的焦虑
✅ 提升 100% 的专注力
#效率工具 #生产力 #Mac软件 #工作技巧 #神器推荐 #Notion #Raycast #Arc浏览器 #Warp #Fig

View File

@@ -37,7 +37,7 @@ body {
.cover-container { .cover-container {
width: 1080px; width: 1080px;
height: 1440px; height: 1440px;
background: linear-gradient(180deg, #3450E4 0%, #D266DA 100%); background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@@ -65,7 +65,7 @@ body {
font-weight: 900; font-weight: 900;
font-size: 130px; font-size: 130px;
line-height: 1.35; line-height: 1.35;
background: linear-gradient(180deg, #2E67B1 0%, #4C4C4C 100%); background: linear-gradient(180deg, #111827 0%, #4B5563 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
@@ -89,7 +89,7 @@ body {
.card-container { .card-container {
width: 1080px; width: 1080px;
min-height: 1440px; min-height: 1440px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
position: relative; position: relative;
padding: 50px; padding: 50px;
overflow: hidden; overflow: hidden;
@@ -290,7 +290,7 @@ body {
} }
.bg-gradient-2 { .bg-gradient-2 {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
} }
.bg-gradient-3 { .bg-gradient-3 {

181
assets/themes/botanical.css Normal file
View File

@@ -0,0 +1,181 @@
/* ============================================
Botanical - 植物园风格
自然柔和,清新淡雅
适配小红书卡片渲染
============================================ */
/* 配色定义
背景色: #F9FAF6 (淡绿白)
前景色: #2D3B36 (深绿灰)
主绿色: #4A7C59 (森林绿)
浅绿: #8FBC8F (淡绿)
棕色: #8B7355 (木质棕)
米色: #E8E4DC (暖米白)
*/
.card-inner {
background-color: #f9faf6;
}
.card-content {
color: #2d3b36;
font-size: 42px;
line-height: 1.8;
}
/* 标题样式 */
.card-content h1 {
font-size: 72px;
font-weight: 700;
color: #4a7c59;
margin-bottom: 40px;
line-height: 1.3;
padding-bottom: 20px;
border-bottom: 4px solid #8fbc8f;
}
.card-content h2 {
font-size: 56px;
font-weight: 600;
color: #3d5a48;
margin: 50px 0 25px 0;
line-height: 1.4;
padding-left: 20px;
border-left: 6px solid #4a7c59;
}
.card-content h3 {
font-size: 48px;
font-weight: 600;
color: #4a7c59;
margin: 40px 0 20px 0;
}
/* 段落 */
.card-content p {
margin-bottom: 35px;
}
/* 粗体 */
.card-content strong,
.card-content b {
font-weight: 700;
color: #2d3b36;
background-color: rgba(143, 188, 143, 0.3);
padding: 0.1em 0.3em;
border-radius: 4px;
}
/* 斜体 */
.card-content em,
.card-content i {
font-style: italic;
color: #4a7c59;
}
/* 链接 */
.card-content a {
color: #4a7c59;
text-decoration: none;
border-bottom: 2px solid #8fbc8f;
transition: all 0.2s;
}
/* 列表 */
.card-content ul,
.card-content ol {
margin: 30px 0;
padding-left: 60px;
}
.card-content li {
margin-bottom: 20px;
line-height: 1.6;
}
.card-content li::marker {
color: #4a7c59;
}
/* 引用块 */
.card-content blockquote {
margin: 35px 0;
padding: 30px 40px;
background-color: #e8e4dc;
color: #3d5a48;
border-left: 6px solid #4a7c59;
border-radius: 0 12px 12px 0;
}
.card-content blockquote p {
margin: 0;
font-style: italic;
}
/* 行内代码 */
.card-content code {
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 38px;
background-color: #e8e4dc;
color: #8b7355;
padding: 6px 16px;
border-radius: 6px;
}
/* 代码块 */
.card-content pre {
margin: 35px 0;
padding: 40px;
background-color: #2d3b36;
color: #e8e4dc;
border-radius: 12px;
overflow-x: visible;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
.card-content pre code {
background-color: transparent;
color: inherit;
padding: 0;
font-size: 36px;
line-height: 1.5;
}
/* 分割线 */
.card-content hr {
margin: 50px 0;
border: none;
height: 3px;
background: linear-gradient(90deg, transparent, #8fbc8f, transparent);
}
/* 图片 */
.card-content img {
display: block;
max-width: 100%;
height: auto;
border-radius: 12px;
margin: 35px auto;
box-shadow: 0 4px 20px rgba(45, 59, 54, 0.1);
}
/* Tags 标签样式 */
.tags-container {
margin-top: 50px;
padding-top: 30px;
border-top: 2px solid #e8e4dc;
}
.tag {
display: inline-block;
background-color: #4a7c59;
color: white;
padding: 12px 28px;
border-radius: 30px;
font-size: 34px;
margin: 10px 15px 10px 0;
font-weight: 500;
}

170
assets/themes/default.css Normal file
View File

@@ -0,0 +1,170 @@
/* ============================================
Default - 默认风格
小红书原生风格,渐变紫色背景
适配小红书卡片渲染
============================================ */
/* 配色定义
背景色: #FFFFFF (白色卡片)
前景色: #475569 (中性灰)
主色: #6366f1 (靛蓝紫)
浅紫: #8b5cf6 (紫罗兰)
灰色: #64748b (石板灰)
边框: #e2e8f0 (浅灰边框)
*/
.card-content {
color: #475569;
background-color: #ffffff;
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,
.card-content b {
font-weight: 700;
color: #1e293b;
}
/* 斜体 */
.card-content em,
.card-content i {
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;
}

View File

@@ -0,0 +1,217 @@
/* ============================================
Neo-Brutalism - 新粗野主义风格
RAW. LOUD. UNAPOLOGETIC.
厚重黑色边框 + 硬阴影 + 高饱和色块
适配小红书卡片渲染
============================================ */
/* 配色定义
背景色: #FFFDF5 (奶油白)
主黑色: #000000 (纯黑)
强调红: #FF4757 (热辣红)
强调黄: #FECA57 (荧光黄)
强调青: #00D2D3 (电光青)
强调紫: #A29BFE (柔和紫)
*/
.card-inner {
background-color: #fffdf5;
/* 纸张纹理感 */
background-image: repeating-linear-gradient(
0deg,
transparent,
transparent 1px,
rgba(0, 0, 0, 0.008) 1px,
rgba(0, 0, 0, 0.008) 2px
);
}
.card-content {
color: #000000;
font-size: 42px;
line-height: 1.7;
}
/* 标题样式 */
.card-content h1 {
font-size: 72px;
font-weight: 900;
padding: 0.4em 0.6em;
background-color: #feca57;
color: #000000;
border: 5px solid #000000;
box-shadow: 8px 8px 0 #000000;
text-transform: uppercase;
margin-bottom: 40px;
line-height: 1.15;
}
.card-content h2 {
font-size: 56px;
font-weight: 900;
padding: 0.35em 0.6em;
background-color: #ffffff;
color: #000000;
border: 4px solid #000000;
border-left: 10px solid #ff4757;
box-shadow: 6px 6px 0 #000000;
margin: 50px 0 25px 0;
line-height: 1.15;
}
.card-content h3 {
font-size: 48px;
font-weight: 900;
padding: 0.25em 0.5em;
color: #000000;
background-color: #00d2d3;
border: 4px solid #000000;
box-shadow: 4px 4px 0 #000000;
display: inline-block;
margin: 40px 0 20px 0;
line-height: 1.15;
}
/* 段落 */
.card-content p {
margin-bottom: 35px;
}
/* 加粗 - 黄色高亮块 */
.card-content strong,
.card-content b {
font-weight: 900;
color: #000000;
background-color: #feca57;
padding: 0.08em 0.25em;
border: 2px solid #000000;
}
/* 斜体 - 红色下划线 */
.card-content em,
.card-content i {
font-style: italic;
color: #000000;
border-bottom: 3px solid #ff4757;
}
/* 链接 - 青色底纹+粗下划线 */
.card-content a {
color: #000000;
text-decoration: none;
background-color: #00d2d3;
padding: 0.08em 0.2em;
border-bottom: 4px solid #000000;
font-weight: 600;
}
/* 列表 */
.card-content ul,
.card-content ol {
margin: 30px 0;
padding-left: 60px;
}
.card-content ul {
list-style-type: square;
}
.card-content li {
margin-bottom: 20px;
line-height: 1.6;
}
.card-content li::marker {
color: #ff4757;
font-weight: 900;
}
/* 引用块 - 紫色色块+硬阴影 */
.card-content blockquote {
margin: 35px 0;
padding: 0.85em 1.1em;
background-color: #a29bfe;
color: #000000;
border: 5px solid #000000;
border-left: 12px solid #ff4757;
box-shadow: 8px 8px 0 #000000;
}
.card-content blockquote p {
margin: 0;
font-style: normal;
}
/* 行内代码 - 黄色背景 */
.card-content code {
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 38px;
background-color: #feca57;
color: #000000;
padding: 0.18em 0.45em;
border: 3px solid #000000;
font-weight: 600;
}
/* 代码块 - 黑色背景+红色阴影 */
.card-content pre {
margin: 35px 0;
padding: 40px;
background-color: #000000;
border: 5px solid #000000;
box-shadow: 10px 10px 0 #ff4757;
overflow-x: visible;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
.card-content pre code {
background-color: transparent;
color: #ffffff;
padding: 0;
border: none;
font-size: 36px;
line-height: 1.55;
font-weight: 400;
}
/* 分割线 - 粗犷几何 */
.card-content hr {
margin: 50px 0;
border: none;
height: 8px;
background-color: #000000;
box-shadow: 5px 5px 0 #ff4757;
}
/* 图片 - 厚边框框架 */
.card-content img {
display: block;
max-width: 100%;
height: auto;
border: 5px solid #000000;
box-shadow: 8px 8px 0 #000000;
margin: 35px auto;
}
/* Tags 标签样式 */
.tags-container {
margin-top: 50px;
padding-top: 30px;
border-top: 5px solid #000000;
}
.tag {
display: inline-block;
background-color: #ff4757;
color: white;
padding: 12px 28px;
font-size: 34px;
margin: 10px 15px 10px 0;
font-weight: 800;
border: 3px solid #000000;
box-shadow: 4px 4px 0 #000000;
text-transform: uppercase;
}

View File

@@ -0,0 +1,224 @@
/* ============================================
Playful Geometric - 活泼几何风格
Memphis 设计与现代波普艺术的融合
适配小红书卡片渲染
============================================ */
/* 配色定义
背景色: #FFFDF5 (温暖奶油白 - 纸质感)
前景色: #1E293B (石板色 800)
主紫色: #8B5CF6 (Vivid Violet - 品牌主色)
粉色: #F472B6 (Hot Pink - 俏皮活力)
黄色: #FBBF24 (Amber/Yellow - 乐观积极)
薄荷绿: #34D399 (Emerald/Mint - 清新感)
*/
.card-inner {
background-color: #fffdf5;
/* 几何点阵背景装饰 */
background-image: radial-gradient(circle, #e2e8f0 1.5px, transparent 1.5px);
background-size: 24px 24px;
}
.card-content {
color: #1e293b;
font-size: 42px;
line-height: 1.7;
}
/* 标题样式 */
.card-content h1 {
font-size: 72px;
font-weight: 800;
padding: 0.4em 0.65em;
background-color: #8b5cf6;
color: #ffffff;
border: 3px solid #1e293b;
border-radius: 0 28px 0 28px;
box-shadow: 6px 6px 0 #1e293b;
margin-bottom: 40px;
line-height: 1.25;
}
.card-content h2 {
font-size: 56px;
font-weight: 800;
padding: 0.3em 0.6em;
background-color: #fffdf5;
color: #7c3aed;
border: 3px solid #1e293b;
border-left: 10px solid #8b5cf6;
border-radius: 0 20px 20px 0;
box-shadow: 5px 5px 0 #f472b6;
margin: 50px 0 25px 0;
line-height: 1.25;
}
.card-content h3 {
font-size: 48px;
font-weight: 800;
padding: 0.25em 0.65em;
color: #1e293b;
background-color: #fbbf24;
border: 2px solid #1e293b;
border-radius: 9999px;
display: inline-block;
box-shadow: 4px 4px 0 #1e293b;
margin: 40px 0 20px 0;
line-height: 1.25;
}
/* 段落 */
.card-content p {
margin-bottom: 35px;
}
/* 粗体 - 黄色高亮贴纸效果 */
.card-content strong,
.card-content b {
font-weight: 700;
color: #1e293b;
background-color: #fbbf24;
padding: 0.1em 0.3em;
border-radius: 6px;
box-shadow: 2px 2px 0 rgba(30, 41, 59, 0.2);
}
/* 斜体 - 紫色强调 */
.card-content em,
.card-content i {
font-style: italic;
color: #8b5cf6;
font-weight: 500;
}
/* 链接 - 活泼几何风格 */
.card-content a {
color: #8b5cf6;
text-decoration: none;
font-weight: 500;
background-color: rgba(139, 92, 246, 0.1);
padding: 0.08em 0.25em;
border-radius: 5px;
border-bottom: 2px solid #8b5cf6;
}
/* 列表 */
.card-content ul,
.card-content ol {
margin: 30px 0;
padding-left: 60px;
}
.card-content li {
margin-bottom: 20px;
line-height: 1.6;
}
.card-content li::marker {
color: #8b5cf6;
font-weight: 700;
}
/* 引用块 - 气泡贴纸风格 */
.card-content blockquote {
margin: 35px 0;
padding: 0.8em 1.1em 0.8em 1.6em;
background-color: #f472b6;
color: #1e293b;
border: 3px solid #1e293b;
border-left: 10px solid #fbbf24;
border-radius: 0 28px 28px 0;
box-shadow: 6px 6px 0 #1e293b;
}
.card-content blockquote p {
margin: 0;
font-style: normal;
}
/* 行内代码 - 黄色贴纸 */
.card-content code {
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 38px;
background-color: #fbbf24;
color: #1e293b;
padding: 0.15em 0.5em;
border: 2px solid #1e293b;
border-radius: 8px;
}
/* 代码块 - 深色背景配紫色硬阴影 */
.card-content pre {
margin: 35px 0;
padding: 40px;
background-color: #1e293b;
border: 3px solid #1e293b;
border-radius: 20px;
box-shadow: 8px 8px 0 #8b5cf6;
overflow-x: visible;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
.card-content pre code {
background-color: transparent;
color: #ffffff;
padding: 0;
border: none;
border-radius: 0;
font-size: 36px;
line-height: 1.55;
}
/* 分割线 - 彩虹几何装饰条 */
.card-content hr {
margin: 50px 0;
border: none;
height: 10px;
background: repeating-linear-gradient(
90deg,
#8b5cf6 0px,
#8b5cf6 24px,
#f472b6 24px,
#f472b6 48px,
#fbbf24 48px,
#fbbf24 72px,
#34d399 72px,
#34d399 96px
);
border-radius: 9999px;
}
/* 图片 - 贴纸框架风格 */
.card-content img {
display: block;
max-width: 100%;
height: auto;
border: 4px solid #1e293b;
border-radius: 20px;
box-shadow: 8px 8px 0 #1e293b;
margin: 35px auto;
}
/* Tags 标签样式 */
.tags-container {
margin-top: 50px;
padding-top: 30px;
border-top: 5px solid #fbbf24;
}
.tag {
display: inline-block;
background-color: #8b5cf6;
color: white;
padding: 12px 28px;
border-radius: 9999px;
font-size: 34px;
margin: 10px 15px 10px 0;
font-weight: 600;
border: 2px solid #1e293b;
box-shadow: 3px 3px 0 #1e293b;
}

View File

@@ -0,0 +1,176 @@
/* ============================================
Professional - 专业商务风格
简洁、稳重、可读性强
适配小红书卡片渲染
============================================ */
/* 配色定义
背景色: #FFFFFF (纯白)
前景色: #1A202C (深灰黑)
主蓝色: #2563EB (专业蓝)
浅蓝: #DBEAFE (淡蓝背景)
灰色: #64748B (中性灰)
边框: #E2E8F0 (浅灰边框)
*/
.card-inner {
background-color: #ffffff;
}
.card-content {
color: #1a202c;
font-size: 42px;
line-height: 1.75;
}
/* 标题样式 */
.card-content h1 {
font-size: 72px;
font-weight: 700;
color: #1a202c;
margin-bottom: 40px;
line-height: 1.3;
padding-bottom: 20px;
border-bottom: 3px solid #2563eb;
}
.card-content h2 {
font-size: 56px;
font-weight: 600;
color: #1e40af;
margin: 50px 0 25px 0;
line-height: 1.4;
}
.card-content h3 {
font-size: 48px;
font-weight: 600;
color: #374151;
margin: 40px 0 20px 0;
}
/* 段落 */
.card-content p {
margin-bottom: 35px;
color: #374151;
}
/* 粗体 */
.card-content strong,
.card-content b {
font-weight: 700;
color: #1a202c;
}
/* 斜体 */
.card-content em,
.card-content i {
font-style: italic;
color: #2563eb;
}
/* 链接 */
.card-content a {
color: #2563eb;
text-decoration: none;
border-bottom: 2px solid #93c5fd;
}
/* 列表 */
.card-content ul,
.card-content ol {
margin: 30px 0;
padding-left: 60px;
}
.card-content li {
margin-bottom: 20px;
line-height: 1.6;
color: #374151;
}
.card-content li::marker {
color: #2563eb;
}
/* 引用块 */
.card-content blockquote {
margin: 35px 0;
padding: 30px 40px;
background-color: #dbeafe;
color: #1e40af;
border-left: 5px solid #2563eb;
border-radius: 0 8px 8px 0;
}
.card-content blockquote p {
margin: 0;
}
/* 行内代码 */
.card-content code {
font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 38px;
background-color: #f1f5f9;
color: #0f172a;
padding: 6px 16px;
border-radius: 6px;
}
/* 代码块 */
.card-content pre {
margin: 35px 0;
padding: 40px;
background-color: #0f172a;
color: #e2e8f0;
border-radius: 12px;
overflow-x: visible;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
.card-content pre code {
background-color: transparent;
color: inherit;
padding: 0;
font-size: 36px;
line-height: 1.5;
}
/* 分割线 */
.card-content hr {
margin: 50px 0;
border: none;
height: 2px;
background-color: #e2e8f0;
}
/* 图片 */
.card-content img {
display: block;
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 35px auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
/* Tags 标签样式 */
.tags-container {
margin-top: 50px;
padding-top: 30px;
border-top: 2px solid #e2e8f0;
}
.tag {
display: inline-block;
background-color: #2563eb;
color: white;
padding: 12px 28px;
border-radius: 6px;
font-size: 34px;
margin: 10px 15px 10px 0;
font-weight: 500;
}

183
assets/themes/retro.css Normal file
View File

@@ -0,0 +1,183 @@
/* ============================================
Retro - 复古怀旧风格
温暖的米色调和复古排版
适配小红书卡片渲染
============================================ */
/* 配色定义
背景色: #FDF6E3 (复古米黄)
前景色: #5C4033 (棕褐色)
主色: #D35400 (复古橙)
浅橙: #F39C12 (暖黄)
深棕: #8B4513 (马鞍棕)
米色: #F5DEB3 (小麦色)
*/
.card-inner {
background-color: #fdf6e3;
}
.card-content {
color: #5c4033;
font-size: 42px;
line-height: 1.8;
}
/* 标题样式 */
.card-content h1 {
font-size: 72px;
font-weight: 700;
color: #d35400;
margin-bottom: 40px;
line-height: 1.3;
border-bottom: 4px double #d35400;
padding-bottom: 15px;
}
.card-content h2 {
font-size: 56px;
font-weight: 600;
color: #8b4513;
margin: 50px 0 25px 0;
line-height: 1.4;
}
.card-content h3 {
font-size: 48px;
font-weight: 600;
color: #a0522d;
margin: 40px 0 20px 0;
font-style: italic;
}
/* 段落 */
.card-content p {
margin-bottom: 35px;
}
/* 粗体 */
.card-content strong,
.card-content b {
font-weight: 700;
color: #8b4513;
}
/* 斜体 */
.card-content em,
.card-content i {
font-style: italic;
color: #d35400;
}
/* 链接 */
.card-content a {
color: #d35400;
text-decoration: underline;
text-decoration-style: wavy;
text-underline-offset: 4px;
}
/* 列表 */
.card-content ul,
.card-content ol {
margin: 30px 0;
padding-left: 60px;
}
.card-content li {
margin-bottom: 20px;
line-height: 1.6;
}
.card-content li::marker {
color: #d35400;
}
/* 引用块 */
.card-content blockquote {
margin: 35px 0;
padding: 30px 40px;
background-color: #f5deb3;
color: #5c4033;
border-left: 6px solid #d35400;
font-style: italic;
}
.card-content blockquote p {
margin: 0;
}
/* 行内代码 */
.card-content code {
font-family: 'Courier New', Courier, monospace;
font-size: 38px;
background-color: #f5deb3;
color: #8b4513;
padding: 6px 16px;
border-radius: 4px;
}
/* 代码块 */
.card-content pre {
margin: 35px 0;
padding: 40px;
background-color: #5c4033;
color: #fdf6e3;
border: 3px solid #8b4513;
overflow-x: visible;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
.card-content pre code {
background-color: transparent;
color: inherit;
padding: 0;
font-size: 36px;
line-height: 1.5;
}
/* 分割线 */
.card-content hr {
margin: 50px 0;
border: none;
height: 3px;
background: repeating-linear-gradient(
90deg,
#d35400,
#d35400 10px,
transparent 10px,
transparent 20px
);
}
/* 图片 */
.card-content img {
display: block;
max-width: 100%;
height: auto;
margin: 35px auto;
border: 4px solid #8b4513;
box-shadow: 4px 4px 0 #d35400;
}
/* Tags 标签样式 */
.tags-container {
margin-top: 50px;
padding-top: 30px;
border-top: 3px double #d35400;
}
.tag {
display: inline-block;
background-color: #d35400;
color: #fdf6e3;
padding: 12px 28px;
border-radius: 4px;
font-size: 34px;
margin: 10px 15px 10px 0;
font-weight: 600;
border: 2px solid #8b4513;
}

198
assets/themes/sketch.css Normal file
View File

@@ -0,0 +1,198 @@
/* ============================================
Sketch - 手绘素描风格
纸张质感、手写字体效果
适配小红书卡片渲染
============================================ */
/* 配色定义
背景色: #FFFEF9 (米白纸张)
前景色: #333333 (炭笔黑)
主色: #555555 (铅笔灰)
强调: #E74C3C (红色标记笔)
蓝色: #3498DB (蓝色圆珠笔)
黄色: #F1C40F (荧光笔)
*/
.card-inner {
background-color: #fffef9;
/* 纸张网格背景 */
background-image:
linear-gradient(#e0e0e0 1px, transparent 1px),
linear-gradient(90deg, #e0e0e0 1px, transparent 1px);
background-size: 30px 30px;
}
.card-content {
color: #333333;
font-size: 42px;
line-height: 1.8;
}
/* 标题样式 */
.card-content h1 {
font-size: 72px;
font-weight: 700;
color: #333333;
margin-bottom: 40px;
line-height: 1.3;
text-decoration: underline;
text-decoration-style: wavy;
text-decoration-color: #e74c3c;
text-underline-offset: 10px;
}
.card-content h2 {
font-size: 56px;
font-weight: 600;
color: #555555;
margin: 50px 0 25px 0;
line-height: 1.4;
border-bottom: 3px dashed #999999;
padding-bottom: 10px;
}
.card-content h3 {
font-size: 48px;
font-weight: 600;
color: #666666;
margin: 40px 0 20px 0;
}
/* 段落 */
.card-content p {
margin-bottom: 35px;
}
/* 粗体 - 加粗圈标 */
.card-content strong,
.card-content b {
font-weight: 700;
color: #333333;
background-color: #f1c40f;
padding: 0.1em 0.3em;
border-radius: 4px;
}
/* 斜体 */
.card-content em,
.card-content i {
font-style: italic;
color: #3498db;
}
/* 链接 */
.card-content a {
color: #3498db;
text-decoration: underline;
text-decoration-style: dashed;
}
/* 列表 */
.card-content ul,
.card-content ol {
margin: 30px 0;
padding-left: 60px;
}
.card-content li {
margin-bottom: 20px;
line-height: 1.6;
}
.card-content li::marker {
color: #e74c3c;
}
/* 引用块 - 便签纸风格 */
.card-content blockquote {
margin: 35px 0;
padding: 30px 40px;
background-color: #fff9c4;
color: #333333;
border: none;
box-shadow: 3px 3px 0 #ddd;
transform: rotate(-0.5deg);
}
.card-content blockquote p {
margin: 0;
font-style: italic;
}
/* 行内代码 */
.card-content code {
font-family: 'Courier New', Courier, monospace;
font-size: 38px;
background-color: #f0f0f0;
color: #555555;
padding: 6px 16px;
border: 2px dashed #999999;
border-radius: 4px;
}
/* 代码块 */
.card-content pre {
margin: 35px 0;
padding: 40px;
background-color: #f5f5f5;
color: #333333;
border: 2px solid #333333;
overflow-x: visible;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
.card-content pre code {
background-color: transparent;
color: inherit;
padding: 0;
border: none;
font-size: 36px;
line-height: 1.5;
}
/* 分割线 */
.card-content hr {
margin: 50px 0;
border: none;
height: 3px;
background: repeating-linear-gradient(
90deg,
#333333,
#333333 5px,
transparent 5px,
transparent 10px
);
}
/* 图片 */
.card-content img {
display: block;
max-width: 100%;
height: auto;
margin: 35px auto;
border: 3px solid #333333;
box-shadow: 5px 5px 0 #ddd;
transform: rotate(0.5deg);
}
/* Tags 标签样式 */
.tags-container {
margin-top: 50px;
padding-top: 30px;
border-top: 2px dashed #999999;
}
.tag {
display: inline-block;
background-color: #fff;
color: #333333;
padding: 12px 28px;
border: 2px solid #333333;
font-size: 34px;
margin: 10px 15px 10px 0;
font-weight: 600;
transform: rotate(-1deg);
}

194
assets/themes/terminal.css Normal file
View File

@@ -0,0 +1,194 @@
/* ============================================
Terminal - 终端/命令行风格
黑客美学,极简科技感
适配小红书卡片渲染
============================================ */
/* 配色定义
背景色: #0D1117 (深黑)
前景色: #C9D1D9 (淡灰白)
主绿: #39D353 (终端绿)
黄色: #F0E68C (警告黄)
青色: #58A6FF (链接蓝)
紫色: #A371F7 (高亮紫)
*/
.card-inner {
background-color: #0d1117;
}
.card-content {
color: #c9d1d9;
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Liberation Mono', monospace;
font-size: 40px;
line-height: 1.6;
}
/* 标题样式 */
.card-content h1 {
font-size: 68px;
font-weight: 700;
color: #39d353;
margin-bottom: 40px;
line-height: 1.3;
}
.card-content h1::before {
content: '# ';
color: #58a6ff;
}
.card-content h2 {
font-size: 54px;
font-weight: 600;
color: #58a6ff;
margin: 50px 0 25px 0;
line-height: 1.4;
}
.card-content h2::before {
content: '## ';
color: #a371f7;
}
.card-content h3 {
font-size: 46px;
font-weight: 600;
color: #a371f7;
margin: 40px 0 20px 0;
}
.card-content h3::before {
content: '### ';
color: #39d353;
}
/* 段落 */
.card-content p {
margin-bottom: 35px;
}
/* 粗体 */
.card-content strong,
.card-content b {
font-weight: 700;
color: #39d353;
}
/* 斜体 */
.card-content em,
.card-content i {
font-style: italic;
color: #f0e68c;
}
/* 链接 */
.card-content a {
color: #58a6ff;
text-decoration: underline;
text-underline-offset: 4px;
}
/* 列表 */
.card-content ul,
.card-content ol {
margin: 30px 0;
padding-left: 60px;
}
.card-content li {
margin-bottom: 20px;
line-height: 1.6;
}
.card-content li::marker {
color: #39d353;
}
/* 引用块 */
.card-content blockquote {
margin: 35px 0;
padding: 30px 40px;
background-color: #161b22;
color: #8b949e;
border-left: 4px solid #39d353;
}
.card-content blockquote::before {
content: '> ';
color: #39d353;
}
.card-content blockquote p {
margin: 0;
}
/* 行内代码 */
.card-content code {
font-family: inherit;
font-size: 38px;
background-color: #21262d;
color: #f0e68c;
padding: 6px 16px;
border-radius: 6px;
}
/* 代码块 */
.card-content pre {
margin: 35px 0;
padding: 40px;
background-color: #161b22;
color: #c9d1d9;
border: 1px solid #30363d;
border-radius: 8px;
overflow-x: visible;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
white-space: pre-wrap;
}
.card-content pre code {
background-color: transparent;
color: inherit;
padding: 0;
font-size: 36px;
line-height: 1.5;
}
/* 分割线 */
.card-content hr {
margin: 50px 0;
border: none;
height: 2px;
background-color: #30363d;
}
/* 图片 */
.card-content img {
display: block;
max-width: 100%;
height: auto;
margin: 35px auto;
border: 1px solid #30363d;
border-radius: 8px;
}
/* Tags 标签样式 */
.tags-container {
margin-top: 50px;
padding-top: 30px;
border-top: 1px solid #30363d;
}
.tag {
display: inline-block;
background-color: #21262d;
color: #39d353;
padding: 12px 28px;
border-radius: 30px;
font-size: 32px;
margin: 10px 15px 10px 0;
font-weight: 500;
border: 1px solid #39d353;
}

BIN
demos/.DS_Store vendored Normal file

Binary file not shown.

BIN
demos/Sketch/card_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

BIN
demos/Sketch/card_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

BIN
demos/Sketch/card_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

BIN
demos/Sketch/card_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

BIN
demos/Sketch/card_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

BIN
demos/Sketch/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

BIN
demos/auto-fit/card_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

BIN
demos/auto-fit/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

70
demos/content.md Normal file
View File

@@ -0,0 +1,70 @@
---
emoji: "🚀"
title: "5个效率神器"
subtitle: "让工作效率翻倍"
---
# 神器一Notion 📝
全能型笔记工具,支持数据库、看板、日历等多种视图。
> 一个工具替代十个 App笔记、任务、项目管理全搞定
**核心功能:**
- 📊 灵活的数据库视图
- 🔗 双向链接
- 🎨 丰富的模板库
- 👥 团队协作
---
# 神器二Raycast ⚡
Mac 上的效率启动器,比 Spotlight 强大 100 倍!
```bash
# 快捷命令示例
raycast://extensions/raycast/clipboard/clipboard-history
```
**必装插件推荐:**
- 剪贴板历史
- 窗口管理
- 快捷短语
- API 调试工具
---
# 神器三Arc 浏览器 🌈
全新理念的浏览器体验:
- 侧边栏标签管理
- 空间分组功能
- 内置笔记和画板
- 极简无干扰模式
---
# 神器四Warp 终端 🖥️
基于 Rust 的现代化终端:
```python
# 支持 AI 智能补全
def example():
print("Hello Warp!")
```
- ⚡ 极速性能
- 🤖 AI 智能提示
- 📋 自动补全
- 🎯 分组工作区
---
# 神器五Fig 自动补全 🔮
终端命令自动补全神器:
- 数百种 CLI 工具支持
- 可视化参数提示
- 团队协作分享

42
demos/content_auto_fit.md Normal file
View File

@@ -0,0 +1,42 @@
---
emoji: "🚀"
title: "5个效率神器"
subtitle: "让工作效率翻倍"
---
# 神器一Notion 📝
全能型笔记工具,支持数据库、看板、日历等多种视图。
> 一个工具替代十个 App笔记、任务、项目管理全搞定
**核心功能:**
- 📊 灵活的数据库视图
- 🔗 双向链接
- 🎨 丰富的模板库
- 👥 团队协作
# 神器二Raycast ⚡
Mac 上的效率启动器,比 Spotlight 强大 100 倍!
```bash
# 快捷命令示例
raycast://extensions/raycast/clipboard/clipboard-history
```
**必装插件推荐:**
- 剪贴板历史
- 窗口管理
- 快捷短语
- API 调试工具
# 总结 🎯
效率提升不在于工具多少,而在于是否**真正用起来**。
选择 2-3 个适合自己的工具,持续使用,形成习惯,你就能:
✅ 节省 50% 的时间
✅ 减少 80% 的焦虑
✅ 提升 100% 的专注力

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
demos/retro/card_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

BIN
demos/retro/card_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

BIN
demos/retro/card_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

BIN
demos/retro/card_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

BIN
demos/retro/card_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

BIN
demos/retro/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

BIN
demos/terminal/card_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

BIN
demos/terminal/card_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

BIN
demos/terminal/card_3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

BIN
demos/terminal/card_4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

BIN
demos/terminal/card_5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

BIN
demos/terminal/cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -1,23 +1,23 @@
{ {
"name": "md2redbook", "name": "md2redbook",
"version": "1.0.0", "version": "2.0.0",
"description": "小红书笔记卡片渲染工具 - Node.js 版本", "description": "小红书笔记素材创作工具 - 支持多种排版样式和智能分页",
"main": "scripts/render_xhs.js", "main": "scripts/render_xhs.js",
"scripts": { "scripts": {
"render": "node scripts/render_xhs.js" "render": "node scripts/render_xhs.js",
}, "install-browsers": "npx playwright install chromium"
"dependencies": {
"marked": "^11.0.0",
"js-yaml": "^4.1.0",
"playwright": "^1.40.0"
}, },
"keywords": [ "keywords": [
"xiaohongshu", "xiaohongshu",
"markdown", "markdown",
"image", "image-generation",
"card", "social-media"
"generator"
], ],
"author": "", "author": "",
"license": "MIT" "license": "MIT",
"dependencies": {
"marked": "^11.0.0",
"yaml": "^2.3.0",
"playwright": "^1.40.0"
}
} }

View File

@@ -1,16 +1,17 @@
# 小红书笔记创作技能 - Python 依赖 # 小红书笔记创作技能依赖
# Markdown 解析 # Markdown 处理
markdown>=3.4.0 markdown>=3.4.0
# YAML 解析
PyYAML>=6.0 PyYAML>=6.0
# 图片渲染 (使用 Playwright) # 浏览器自动化(渲染图片)
playwright>=1.40.0 playwright>=1.40.0
# 小红书 API 客户端 # 小红书发布
xhs xhs>=0.4.0
# 环境变量加载 # 环境变量管理
python-dotenv>=1.0.0 python-dotenv>=1.0.0
# HTTP 请求API 模式)
requests>=2.28.0

View File

@@ -1,72 +1,107 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
小红书笔记发布脚本 小红书笔记发布脚本 - 增强版
将生成的图片卡片发布到小红书 支持直接发布(本地签名)和通过 API 服务发布两种方式
使用方法: 使用方法:
python publish_xhs.py --title "标题" --desc "描述" --images cover.png card_1.png card_2.png # 直接发布(使用本地签名)
python publish_xhs.py --title "标题" --desc "描述" --images cover.png card_1.png
# 通过 API 服务发布
python publish_xhs.py --title "标题" --desc "描述" --images cover.png card_1.png --api-mode
环境变量: 环境变量:
在同目录下创建 .env 文件,配置 XHS_COOKIE 在同目录或项目根目录下创建 .env 文件,配置:
# 必需:小红书 Cookie
XHS_COOKIE=your_cookie_string_here XHS_COOKIE=your_cookie_string_here
# 可选API 服务地址(使用 --api-mode 时需要)
XHS_API_URL=http://localhost:5005
依赖安装: 依赖安装:
pip install xhs python-dotenv pip install xhs python-dotenv requests
""" """
import argparse import argparse
import os import os
import sys import sys
import json
import re
from pathlib import Path from pathlib import Path
from typing import List, Optional, Dict, Any
try: try:
from dotenv import load_dotenv from dotenv import load_dotenv
from xhs import XhsClient import requests
except ImportError as e: except ImportError as e:
print(f"缺少依赖: {e}") print(f"缺少依赖: {e}")
print("请运行: pip install xhs python-dotenv") print("请运行: pip install python-dotenv requests")
sys.exit(1) sys.exit(1)
def load_cookie(): def load_cookie() -> str:
"""从 .env 文件加载 Cookie""" """从 .env 文件加载 Cookie"""
# 尝试从当前目录加载 .env # 尝试从多个位置加载 .env
env_path = Path.cwd() / '.env' env_paths = [
Path.cwd() / '.env',
Path(__file__).parent.parent / '.env',
Path(__file__).parent.parent.parent / '.env',
]
for env_path in env_paths:
if env_path.exists(): if env_path.exists():
load_dotenv(env_path) load_dotenv(env_path)
break
# 也尝试从脚本目录加载
script_env = Path(__file__).parent.parent / '.env'
if script_env.exists():
load_dotenv(script_env)
cookie = os.getenv('XHS_COOKIE') cookie = os.getenv('XHS_COOKIE')
if not cookie: if not cookie:
print("❌ 错误: 未找到 XHS_COOKIE 环境变量") print("❌ 错误: 未找到 XHS_COOKIE 环境变量")
print("在当前目录创建 .env 文件,添加以下内容:") print("请创建 .env 文件,添加以下内容:")
print("XHS_COOKIE=your_cookie_string_here") print("XHS_COOKIE=your_cookie_string_here")
print("\nCookie 获取方式:")
print("1. 在浏览器中登录小红书https://www.xiaohongshu.com")
print("2. 打开开发者工具F12")
print("3. 在 Network 标签中查看任意请求的 Cookie 头")
print("4. 复制完整的 cookie 字符串")
sys.exit(1) sys.exit(1)
return cookie return cookie
def create_client(cookie: str) -> XhsClient: def parse_cookie(cookie_string: str) -> Dict[str, str]:
"""创建小红书客户端""" """解析 Cookie 字符串为字典"""
try: cookies = {}
# 使用本地签名 for item in cookie_string.split(';'):
from xhs.help import sign as local_sign item = item.strip()
if '=' in item:
def sign_func(uri, data=None, a1="", web_session=""): key, value = item.split('=', 1)
return local_sign(uri, data, a1=a1) cookies[key.strip()] = value.strip()
return cookies
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: def validate_cookie(cookie_string: str) -> bool:
"""验证 Cookie 是否包含必要的字段"""
cookies = parse_cookie(cookie_string)
# 检查必需的 cookie 字段
required_fields = ['a1', 'web_session']
missing = [f for f in required_fields if f not in cookies]
if missing:
print(f"⚠️ Cookie 可能不完整,缺少字段: {', '.join(missing)}")
print("这可能导致签名失败,请确保 Cookie 包含 a1 和 web_session 字段")
return False
return True
def get_api_url() -> str:
"""获取 API 服务地址"""
return os.getenv('XHS_API_URL', 'http://localhost:5005')
def validate_images(image_paths: List[str]) -> List[str]:
"""验证图片文件是否存在""" """验证图片文件是否存在"""
valid_images = [] valid_images = []
for path in image_paths: for path in image_paths:
@@ -82,16 +117,53 @@ def validate_images(image_paths: list) -> list:
return valid_images return valid_images
def publish_note(client: XhsClient, title: str, desc: str, images: list, class LocalPublisher:
is_private: bool = False, post_time: str = None): """本地发布模式:直接使用 xhs 库"""
"""发布图文笔记"""
def __init__(self, cookie: str):
self.cookie = cookie
self.client = None
def init_client(self):
"""初始化 xhs 客户端"""
try: try:
print(f"\n🚀 准备发布笔记...") from xhs import XhsClient
from xhs.help import sign as local_sign
except ImportError:
print("❌ 错误: 缺少 xhs 库")
print("请运行: pip install xhs")
sys.exit(1)
# 解析 a1 值
cookies = parse_cookie(self.cookie)
a1 = cookies.get('a1', '')
def sign_func(uri, data=None, a1_param="", web_session=""):
# 使用 cookie 中的 a1 值
return local_sign(uri, data, a1=a1 or a1_param)
self.client = XhsClient(cookie=self.cookie, sign=sign_func)
def get_user_info(self) -> Optional[Dict[str, Any]]:
"""获取当前登录用户信息"""
try:
info = self.client.get_self_info()
print(f"👤 当前用户: {info.get('nickname', '未知')}")
return info
except Exception as e:
print(f"⚠️ 无法获取用户信息: {e}")
return None
def publish(self, title: str, desc: str, images: List[str],
is_private: bool = False, post_time: str = None) -> Dict[str, Any]:
"""发布图文笔记"""
print(f"\n🚀 准备发布笔记(本地模式)...")
print(f" 📌 标题: {title}") print(f" 📌 标题: {title}")
print(f" 📝 描述: {desc[:50]}..." if len(desc) > 50 else f" 📝 描述: {desc}") print(f" 📝 描述: {desc[:50]}..." if len(desc) > 50 else f" 📝 描述: {desc}")
print(f" 🖼️ 图片数量: {len(images)}") print(f" 🖼️ 图片数量: {len(images)}")
result = client.create_image_note( try:
result = self.client.create_image_note(
title=title, title=title,
desc=desc, desc=desc,
files=images, files=images,
@@ -109,24 +181,154 @@ def publish_note(client: XhsClient, title: str, desc: str, images: list,
return result return result
except Exception as e: except Exception as e:
print(f"\n❌ 发布失败: {e}") error_msg = str(e)
print(f"\n❌ 发布失败: {error_msg}")
# 提供具体的错误排查建议
if 'sign' in error_msg.lower() or 'signature' in error_msg.lower():
print("\n💡 签名错误排查建议:")
print("1. 确保 Cookie 包含有效的 a1 和 web_session 字段")
print("2. Cookie 可能已过期,请重新获取")
print("3. 尝试使用 --api-mode 通过 API 服务发布")
elif 'cookie' in error_msg.lower():
print("\n💡 Cookie 错误排查建议:")
print("1. 确保 Cookie 格式正确")
print("2. Cookie 可能已过期,请重新获取")
print("3. 确保 Cookie 来自已登录的小红书网页版")
raise
class ApiPublisher:
"""API 发布模式:通过 xhs-api 服务发布"""
def __init__(self, cookie: str, api_url: str = None):
self.cookie = cookie
self.api_url = api_url or get_api_url()
self.session_id = 'md2redbook_session'
def init_client(self):
"""初始化 API 客户端"""
print(f"📡 连接 API 服务: {self.api_url}")
# 健康检查
try:
resp = requests.get(f"{self.api_url}/health", timeout=5)
if resp.status_code != 200:
raise Exception("API 服务不可用")
except requests.exceptions.RequestException as e:
print(f"❌ 无法连接到 API 服务: {e}")
print(f"\n💡 请确保 xhs-api 服务已启动:")
print(f" cd xhs-api && python app_full.py")
sys.exit(1) sys.exit(1)
# 初始化 session
try:
resp = requests.post(
f"{self.api_url}/init",
json={
"session_id": self.session_id,
"cookie": self.cookie
},
timeout=30
)
result = resp.json()
def get_user_info(client: XhsClient): if resp.status_code == 200 and result.get('status') == 'success':
print(f"✅ API 初始化成功")
user_info = result.get('user_info', {})
if user_info:
print(f"👤 当前用户: {user_info.get('nickname', '未知')}")
elif result.get('status') == 'warning':
print(f"⚠️ {result.get('message')}")
else:
raise Exception(result.get('error', '初始化失败'))
except Exception as e:
print(f"❌ API 初始化失败: {e}")
sys.exit(1)
def get_user_info(self) -> Optional[Dict[str, Any]]:
"""获取当前登录用户信息""" """获取当前登录用户信息"""
try: try:
info = client.get_self_info() resp = requests.post(
print(f"\n👤 当前用户: {info.get('nickname', '未知')}") f"{self.api_url}/user/info",
json={"session_id": self.session_id},
timeout=10
)
if resp.status_code == 200:
result = resp.json()
if result.get('status') == 'success':
info = result.get('user_info', {})
print(f"👤 当前用户: {info.get('nickname', '未知')}")
return info return info
return None
except Exception as e: except Exception as e:
print(f"⚠️ 无法获取用户信息: {e}") print(f"⚠️ 无法获取用户信息: {e}")
return None return None
def publish(self, title: str, desc: str, images: List[str],
is_private: bool = False, post_time: str = None) -> Dict[str, Any]:
"""发布图文笔记"""
print(f"\n🚀 准备发布笔记API 模式)...")
print(f" 📌 标题: {title}")
print(f" 📝 描述: {desc[:50]}..." if len(desc) > 50 else f" 📝 描述: {desc}")
print(f" 🖼️ 图片数量: {len(images)}")
try:
payload = {
"session_id": self.session_id,
"title": title,
"desc": desc,
"files": images,
"is_private": is_private
}
if post_time:
payload["post_time"] = post_time
resp = requests.post(
f"{self.api_url}/publish/image",
json=payload,
timeout=120
)
result = resp.json()
if resp.status_code == 200 and result.get('status') == 'success':
print("\n✨ 笔记发布成功!")
publish_result = result.get('result', {})
if isinstance(publish_result, dict):
note_id = publish_result.get('note_id') or publish_result.get('id')
if note_id:
print(f" 📎 笔记ID: {note_id}")
print(f" 🔗 链接: https://www.xiaohongshu.com/explore/{note_id}")
return publish_result
else:
raise Exception(result.get('error', '发布失败'))
except Exception as e:
error_msg = str(e)
print(f"\n❌ 发布失败: {error_msg}")
raise
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='将图片发布为小红书笔记' description='将图片发布为小红书笔记',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
# 基本用法
python publish_xhs.py -t "我的标题" -d "正文内容" -i cover.png card_1.png card_2.png
# 使用 API 模式
python publish_xhs.py -t "我的标题" -d "正文内容" -i *.png --api-mode
# 设为私密笔记
python publish_xhs.py -t "我的标题" -d "正文内容" -i *.png --private
# 定时发布
python publish_xhs.py -t "我的标题" -d "正文内容" -i *.png --post-time "2024-12-01 10:00:00"
'''
) )
parser.add_argument( parser.add_argument(
'--title', '-t', '--title', '-t',
@@ -154,6 +356,16 @@ def main():
default=None, default=None,
help='定时发布时间格式2024-01-01 12:00:00' help='定时发布时间格式2024-01-01 12:00:00'
) )
parser.add_argument(
'--api-mode',
action='store_true',
help='使用 API 模式发布(需要 xhs-api 服务运行)'
)
parser.add_argument(
'--api-url',
default=None,
help='API 服务地址(默认: http://localhost:5005'
)
parser.add_argument( parser.add_argument(
'--dry-run', '--dry-run',
action='store_true', action='store_true',
@@ -170,32 +382,43 @@ def main():
# 加载 Cookie # 加载 Cookie
cookie = load_cookie() cookie = load_cookie()
# 验证 Cookie 格式
validate_cookie(cookie)
# 验证图片 # 验证图片
valid_images = validate_images(args.images) valid_images = validate_images(args.images)
# 创建客户端
client = create_client(cookie)
# 获取用户信息(验证 Cookie 有效性)
get_user_info(client)
if args.dry_run: if args.dry_run:
print("\n🔍 验证模式 - 不会实际发布") print("\n🔍 验证模式 - 不会实际发布")
print(f" 📌 标题: {args.title}") print(f" 📌 标题: {args.title}")
print(f" 📝 描述: {args.desc}") print(f" 📝 描述: {args.desc}")
print(f" 🖼️ 图片: {valid_images}") print(f" 🖼️ 图片: {valid_images}")
print(f" 🔒 私密: {args.private}")
print(f" ⏰ 定时: {args.post_time or '立即发布'}")
print(f" 📡 模式: {'API' if args.api_mode else '本地'}")
print("\n✅ 验证通过,可以发布") print("\n✅ 验证通过,可以发布")
return return
# 选择发布方式
if args.api_mode:
publisher = ApiPublisher(cookie, args.api_url)
else:
publisher = LocalPublisher(cookie)
# 初始化客户端
publisher.init_client()
# 发布笔记 # 发布笔记
publish_note( try:
client=client, publisher.publish(
title=args.title, title=args.title,
desc=args.desc, desc=args.desc,
images=valid_images, images=valid_images,
is_private=args.private, is_private=args.private,
post_time=args.post_time post_time=args.post_time
) )
except Exception as e:
sys.exit(1)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,188 +1,511 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* 小红书卡片渲染脚本 - Node.js 版 * 小红书卡片渲染脚本 - Node.js 增强
* 将 Markdown 文件渲染为小红书风格的图片卡片 * 支持多种排版样式和智能分页策略
* *
* 使用方法: * 使用方法:
* node render_xhs.js <markdown_file> [--output-dir <output_directory>] * node render_xhs.js <markdown_file> [options]
*
* 选项:
* --output-dir, -o 输出目录(默认为当前工作目录)
* --theme, -t 排版主题default, playful-geometric, neo-brutalism, 等
* --mode, -m 分页模式separator, auto-fit, auto-split, dynamic
* --width, -w 图片宽度(默认 1080
* --height, -h 图片高度(默认 1440
* --dpr 设备像素比(默认 2
* *
* 依赖安装: * 依赖安装:
* npm install marked js-yaml playwright * npm install marked yaml playwright
* npx playwright install chromium * npx playwright install chromium
*/ */
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { chromium } = require('playwright');
const { marked } = require('marked'); const { marked } = require('marked');
const yaml = require('js-yaml'); const yaml = require('yaml');
const { chromium } = require('playwright');
// 获取脚本所在目录 // 获取脚本所在目录
const SCRIPT_DIR = path.dirname(__dirname); const SCRIPT_DIR = path.dirname(__dirname);
const ASSETS_DIR = path.join(SCRIPT_DIR, 'assets'); const ASSETS_DIR = path.join(SCRIPT_DIR, 'assets');
const THEMES_DIR = path.join(ASSETS_DIR, 'themes');
// 卡片尺寸配置 (3:4 比例) // 默认卡片尺寸配置 (3:4 比例)
const CARD_WIDTH = 1080; const DEFAULT_WIDTH = 1080;
const CARD_HEIGHT = 1440; const DEFAULT_HEIGHT = 1440;
const MAX_HEIGHT = 2160;
// 可用主题列表
const AVAILABLE_THEMES = [
'default',
'playful-geometric',
'neo-brutalism',
'botanical',
'professional',
'retro',
'terminal',
'sketch'
];
// 分页模式
const PAGING_MODES = ['separator', 'auto-fit', 'auto-split', 'dynamic'];
// 主题背景色
const THEME_BACKGROUNDS = {
'default': 'linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%)',
'playful-geometric': 'linear-gradient(135deg, #8B5CF6 0%, #F472B6 100%)',
'neo-brutalism': 'linear-gradient(135deg, #FF4757 0%, #FECA57 100%)',
'botanical': 'linear-gradient(135deg, #4A7C59 0%, #8FBC8F 100%)',
'professional': 'linear-gradient(135deg, #2563EB 0%, #3B82F6 100%)',
'retro': 'linear-gradient(135deg, #D35400 0%, #F39C12 100%)',
'terminal': 'linear-gradient(135deg, #0D1117 0%, #161B22 100%)',
'sketch': 'linear-gradient(135deg, #555555 0%, #888888 100%)'
};
// 封面标题文字渐变(随主题变化)
const THEME_TITLE_GRADIENTS = {
'default': 'linear-gradient(180deg, #111827 0%, #4B5563 100%)',
'playful-geometric': 'linear-gradient(180deg, #7C3AED 0%, #F472B6 100%)',
'neo-brutalism': 'linear-gradient(180deg, #000000 0%, #FF4757 100%)',
'botanical': 'linear-gradient(180deg, #1F2937 0%, #4A7C59 100%)',
'professional': 'linear-gradient(180deg, #1E3A8A 0%, #2563EB 100%)',
'retro': 'linear-gradient(180deg, #8B4513 0%, #D35400 100%)',
'terminal': 'linear-gradient(180deg, #39D353 0%, #58A6FF 100%)',
'sketch': 'linear-gradient(180deg, #111827 0%, #6B7280 100%)',
};
/** /**
* 解析 Markdown 文件,提取 YAML 头部和正文内容 * 解析命令行参数
*/
function parseArgs() {
const args = process.argv.slice(2);
const options = {
markdownFile: null,
outputDir: process.cwd(),
theme: 'default',
mode: 'separator',
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
maxHeight: MAX_HEIGHT,
dpr: 2
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const nextArg = args[i + 1];
switch (arg) {
case '--output-dir':
case '-o':
options.outputDir = nextArg;
i++;
break;
case '--theme':
case '-t':
options.theme = nextArg;
i++;
break;
case '--mode':
case '-m':
options.mode = nextArg;
i++;
break;
case '--width':
case '-w':
options.width = parseInt(nextArg);
i++;
break;
case '--height':
options.height = parseInt(nextArg);
i++;
break;
case '--max-height':
options.maxHeight = parseInt(nextArg);
i++;
break;
case '--dpr':
options.dpr = parseInt(nextArg);
i++;
break;
case '--help':
printHelp();
process.exit(0);
default:
if (!arg.startsWith('-')) {
options.markdownFile = arg;
}
}
}
return options;
}
/**
* 打印帮助信息
*/
function printHelp() {
console.log(`
小红书卡片渲染脚本 - Node.js 版本
使用方法:
node render_xhs.js <markdown_file> [options]
选项:
--output-dir, -o 输出目录(默认为当前工作目录)
--theme, -t 排版主题
--mode, -m 分页模式
--width, -w 图片宽度(默认 1080
--height 图片高度(默认 1440
--max-height 最大高度(默认 2160
--dpr 设备像素比(默认 2
可用主题: ${AVAILABLE_THEMES.join(', ')}
分页模式: ${PAGING_MODES.join(', ')}
`);
}
/**
* 解析 Markdown 文件
*/ */
function parseMarkdownFile(filePath) { function parseMarkdownFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8'); const content = fs.readFileSync(filePath, 'utf-8');
// 解析 YAML 头部 // 解析 YAML 头部
const yamlPattern = /^---\s*\n([\s\S]*?)\n---\s*\n/; const yamlMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
const yamlMatch = content.match(yamlPattern);
let metadata = {}; let metadata = {};
let body = content; let body = content;
if (yamlMatch) { if (yamlMatch) {
try { try {
metadata = yaml.load(yamlMatch[1]) || {}; metadata = yaml.parse(yamlMatch[1]) || {};
} catch (e) { } catch (e) {
metadata = {}; metadata = {};
} }
body = content.slice(yamlMatch[0].length); body = content.slice(yamlMatch[0].length);
} }
return { return { metadata, body: body.trim() };
metadata,
body: body.trim()
};
} }
/** /**
* 按照 --- 分隔符拆分正文为多张卡片内容 * 按分隔符拆分内容
*/ */
function splitContentBySeparator(body) { function splitContentBySeparator(body) {
const parts = body.split(/\n---+\n/); const parts = body.split(/\n---+\n/);
return parts.filter(part => part.trim()).map(part => part.trim()); return parts.map(p => p.trim()).filter(p => p);
} }
/** /**
* 将 Markdown 转换为 HTML * 加载主题 CSS
*/ */
function convertMarkdownToHtml(mdContent) { function loadThemeCss(theme) {
// 处理 tags以 # 开头的标签) const themeFile = path.join(THEMES_DIR, `${theme}.css`);
const tagsPattern = /((?:#[\w\u4e00-\u9fa5]+\s*)+)$/m; if (fs.existsSync(themeFile)) {
const tagsMatch = mdContent.match(tagsPattern); return fs.readFileSync(themeFile, 'utf-8');
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>'; const defaultFile = path.join(THEMES_DIR, 'default.css');
if (fs.existsSync(defaultFile)) {
return fs.readFileSync(defaultFile, 'utf-8');
} }
} return '';
// 转换 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 * 生成封面 HTML
*/ */
function generateCoverHtml(metadata) { function generateCoverHtml(metadata, theme, width, height) {
let template = loadTemplate('cover.html'); const emoji = metadata.emoji || '📝';
let emoji = metadata.emoji || '📝';
let title = metadata.title || '标题'; let title = metadata.title || '标题';
let subtitle = metadata.subtitle || ''; let subtitle = metadata.subtitle || '';
// 限制标题和副标题长度 if (title.length > 15) title = title.slice(0, 15);
if (title.length > 15) { if (subtitle.length > 15) subtitle = subtitle.slice(0, 15);
title = title.slice(0, 15);
} const bg = THEME_BACKGROUNDS[theme] || THEME_BACKGROUNDS['default'];
if (subtitle.length > 15) { const titleBg = THEME_TITLE_GRADIENTS[theme] || THEME_TITLE_GRADIENTS['default'];
subtitle = subtitle.slice(0, 15);
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=${width}, height=${height}">
<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: ${width}px;
height: ${height}px;
overflow: hidden;
} }
template = template.replace('{{EMOJI}}', emoji); .cover-container {
template = template.replace('{{TITLE}}', title); width: ${width}px;
template = template.replace('{{SUBTITLE}}', subtitle); height: ${height}px;
background: ${bg};
position: relative;
overflow: hidden;
}
return template; .cover-inner {
position: absolute;
width: ${Math.floor(width * 0.88)}px;
height: ${Math.floor(height * 0.91)}px;
left: ${Math.floor(width * 0.06)}px;
top: ${Math.floor(height * 0.045)}px;
background: #F3F3F3;
border-radius: 25px;
display: flex;
flex-direction: column;
padding: ${Math.floor(width * 0.074)}px ${Math.floor(width * 0.079)}px;
}
.cover-emoji {
font-size: ${Math.floor(width * 0.167)}px;
line-height: 1.2;
margin-bottom: ${Math.floor(height * 0.035)}px;
}
.cover-title {
font-weight: 900;
font-size: ${Math.floor(width * 0.12)}px;
line-height: 1.4;
background: ${titleBg};
-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: ${Math.floor(width * 0.067)}px;
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>`;
} }
/** /**
* 生成正文卡片 HTML * 生成正文卡片 HTML
*/ */
function generateCardHtml(content, pageNumber = 1, totalPages = 1) { function generateCardHtml(content, theme, pageNumber, totalPages, width, height, mode) {
let template = loadTemplate('card.html'); const htmlContent = marked.parse(content);
const themeCss = loadThemeCss(theme);
const htmlContent = convertMarkdownToHtml(content);
const pageText = totalPages > 1 ? `${pageNumber}/${totalPages}` : ''; const pageText = totalPages > 1 ? `${pageNumber}/${totalPages}` : '';
const bg = THEME_BACKGROUNDS[theme] || THEME_BACKGROUNDS['default'];
template = template.replace('{{CONTENT}}', htmlContent); let containerStyle, innerStyle, contentStyle;
template = template.replace('{{PAGE_NUMBER}}', pageText);
return template; if (mode === 'auto-fit') {
containerStyle = `
width: ${width}px;
height: ${height}px;
background: ${bg};
position: relative;
padding: 50px;
overflow: hidden;
`;
innerStyle = `
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 60px;
height: calc(${height}px - 100px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
overflow: hidden;
display: flex;
flex-direction: column;
`;
contentStyle = 'flex: 1; overflow: hidden;';
} else if (mode === 'dynamic') {
containerStyle = `
width: ${width}px;
min-height: ${height}px;
background: ${bg};
position: relative;
padding: 50px;
`;
innerStyle = `
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 60px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
`;
contentStyle = '';
} else {
containerStyle = `
width: ${width}px;
min-height: ${height}px;
background: ${bg};
position: relative;
padding: 50px;
overflow: hidden;
`;
innerStyle = `
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 60px;
min-height: calc(${height}px - 100px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
`;
contentStyle = '';
}
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=${width}">
<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: ${width}px;
overflow: hidden;
background: transparent;
}
.card-container { ${containerStyle} }
.card-inner { ${innerStyle} }
.card-content { line-height: 1.7; ${contentStyle} }
/* auto-fit 用:对整个内容块做 transform 缩放 */
.card-content-scale { transform-origin: top left; will-change: transform; }
${themeCss}
.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">
<div class="card-content-scale">
${htmlContent}
</div>
</div>
</div>
<div class="page-number">${pageText}</div>
</div>
</body>
</html>`;
} }
/** /**
* 使用 Playwright 将 HTML 渲染为图片 * 渲染 HTML 为图片
*/ */
async function renderHtmlToImage(htmlContent, outputPath, width = CARD_WIDTH, height = CARD_HEIGHT) { async function renderHtmlToImage(htmlContent, outputPath, width, height, mode, maxHeight, dpr) {
const browser = await chromium.launch(); const browser = await chromium.launch();
const viewportHeight = mode !== 'dynamic' ? height : maxHeight;
const page = await browser.newPage({ const page = await browser.newPage({
viewport: { width, height } viewport: { width, height: viewportHeight },
deviceScaleFactor: dpr
}); });
// 设置 HTML 内容 await page.setContent(htmlContent);
await page.setContent(htmlContent, { await page.waitForLoadState('networkidle');
waitUntil: 'networkidle'
});
// 等待字体加载
await page.waitForTimeout(500); await page.waitForTimeout(500);
// 获取实际内容高度 let actualHeight;
if (mode === 'auto-fit') {
await page.evaluate(() => {
const viewportContent = document.querySelector('.card-content');
const scaleEl = document.querySelector('.card-content-scale');
if (!viewportContent || !scaleEl) return;
// reset
scaleEl.style.transform = 'none';
scaleEl.style.width = '';
scaleEl.style.height = '';
const availableWidth = viewportContent.clientWidth;
const availableHeight = viewportContent.clientHeight;
const rect = scaleEl.getBoundingClientRect();
const contentWidth = Math.max(scaleEl.scrollWidth, rect.width);
const contentHeight = Math.max(scaleEl.scrollHeight, rect.height);
if (!contentWidth || !contentHeight || !availableWidth || !availableHeight) return;
const scale = Math.min(1, availableWidth / contentWidth, availableHeight / contentHeight);
// expand layout box to avoid clip
scaleEl.style.width = (availableWidth / scale) + 'px';
scaleEl.style.transformOrigin = 'top left';
scaleEl.style.transform = `translate(0px, 0px) scale(${scale})`;
});
await page.waitForTimeout(100);
actualHeight = height;
} else if (mode === 'dynamic') {
const contentHeight = await page.evaluate(() => { const contentHeight = await page.evaluate(() => {
const container = document.querySelector('.card-container') || document.querySelector('.cover-container'); const container = document.querySelector('.card-container');
return container ? container.scrollHeight : document.body.scrollHeight; return container ? container.scrollHeight : document.body.scrollHeight;
}); });
actualHeight = Math.max(height, Math.min(contentHeight, maxHeight));
} else {
const contentHeight = await page.evaluate(() => {
const container = document.querySelector('.card-container');
return container ? container.scrollHeight : document.body.scrollHeight;
});
actualHeight = Math.max(height, contentHeight);
}
// 确保高度至少为 1440px3:4 比例)
const actualHeight = Math.max(height, contentHeight);
// 截图
await page.screenshot({ await page.screenshot({
path: outputPath, path: outputPath,
clip: { x: 0, y: 0, width, height: actualHeight }, clip: { x: 0, y: 0, width, height: actualHeight },
type: 'png' type: 'png'
}); });
console.log(` ✅ 已生成: ${outputPath}`);
await browser.close(); await browser.close();
console.log(` ✅ 已生成: ${outputPath} (${width}x${actualHeight})`);
return actualHeight;
} }
/** /**
* 主渲染函数:将 Markdown 文件渲染为多张卡片图片 * 主渲染函数
*/ */
async function renderMarkdownToCards(mdFile, outputDir) { async function renderMarkdownToCards(options) {
console.log(`\n🎨 开始渲染: ${mdFile}`); const { markdownFile, outputDir, theme, mode, width, height, maxHeight, dpr } = options;
console.log(`\n🎨 开始渲染: ${markdownFile}`);
console.log(` 📐 主题: ${theme}`);
console.log(` 📏 模式: ${mode}`);
console.log(` 📐 尺寸: ${width}x${height}`);
// 确保输出目录存在 // 确保输出目录存在
if (!fs.existsSync(outputDir)) { if (!fs.existsSync(outputDir)) {
@@ -190,10 +513,9 @@ async function renderMarkdownToCards(mdFile, outputDir) {
} }
// 解析 Markdown 文件 // 解析 Markdown 文件
const data = parseMarkdownFile(mdFile); const { metadata, body } = parseMarkdownFile(markdownFile);
const { metadata, body } = data;
// 分割正文内容 // 分割内容
const cardContents = splitContentBySeparator(body); const cardContents = splitContentBySeparator(body);
const totalCards = cardContents.length; const totalCards = cardContents.length;
@@ -202,67 +524,53 @@ async function renderMarkdownToCards(mdFile, outputDir) {
// 生成封面 // 生成封面
if (metadata.emoji || metadata.title) { if (metadata.emoji || metadata.title) {
console.log(' 📷 生成封面...'); console.log(' 📷 生成封面...');
const coverHtml = generateCoverHtml(metadata); const coverHtml = generateCoverHtml(metadata, theme, width, height);
const coverPath = path.join(outputDir, 'cover.png'); const coverPath = path.join(outputDir, 'cover.png');
await renderHtmlToImage(coverHtml, coverPath); await renderHtmlToImage(coverHtml, coverPath, width, height, 'separator', maxHeight, dpr);
} }
// 生成正文卡片 // 生成正文卡片
for (let i = 0; i < cardContents.length; i++) { for (let i = 0; i < cardContents.length; i++) {
const pageNum = i + 1; const content = cardContents[i];
console.log(` 📷 生成卡片 ${pageNum}/${totalCards}...`); console.log(` 📷 生成卡片 ${i + 1}/${totalCards}...`);
const cardHtml = generateCardHtml(cardContents[i], pageNum, totalCards); const cardHtml = generateCardHtml(content, theme, i + 1, totalCards, width, height, mode);
const cardPath = path.join(outputDir, `card_${pageNum}.png`); const cardPath = path.join(outputDir, `card_${i + 1}.png`);
await renderHtmlToImage(cardHtml, cardPath); await renderHtmlToImage(cardHtml, cardPath, width, height, mode, maxHeight, dpr);
} }
console.log(`\n✨ 渲染完成!图片已保存到: ${outputDir}`); 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() { async function main() {
const { markdownFile, outputDir } = parseArgs(); const options = parseArgs();
await renderMarkdownToCards(markdownFile, outputDir);
if (!options.markdownFile) {
console.error('❌ 错误: 请提供 Markdown 文件路径');
printHelp();
process.exit(1);
} }
main().catch(error => { if (!fs.existsSync(options.markdownFile)) {
console.error('❌ 渲染失败:', error.message); console.error(`❌ 错误: 文件不存在 - ${options.markdownFile}`);
process.exit(1); process.exit(1);
}); }
if (!AVAILABLE_THEMES.includes(options.theme)) {
console.error(`❌ 错误: 不支持的主题 - ${options.theme}`);
console.error(`可用主题: ${AVAILABLE_THEMES.join(', ')}`);
process.exit(1);
}
if (!PAGING_MODES.includes(options.mode)) {
console.error(`❌ 错误: 不支持的分页模式 - ${options.mode}`);
console.error(`可用模式: ${PAGING_MODES.join(', ')}`);
process.exit(1);
}
await renderMarkdownToCards(options);
}
main().catch(console.error);

View File

@@ -1,13 +1,27 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
小红书卡片渲染脚本 - Python 版本 小红书卡片渲染脚本 - 增强版
将 Markdown 文件渲染为小红书风格的图片卡片 支持多种排版样式和智能分页策略
使用方法: 使用方法:
python render_xhs.py <markdown_file> [--output-dir <output_directory>] python render_xhs.py <markdown_file> [options]
选项:
--output-dir, -o 输出目录(默认为当前工作目录)
--theme, -t 排版主题default, playful-geometric, neo-brutalism,
botanical, professional, retro, terminal, sketch
--mode, -m 分页模式:
- separator : 按 --- 分隔符手动分页(默认)
- auto-fit : 自动缩放文字以填满固定尺寸
- auto-split : 根据内容高度自动切分
- dynamic : 根据内容动态调整图片高度
--width, -w 图片宽度(默认 1080
--height, -h 图片高度(默认 1440dynamic 模式下为最小高度)
--max-height dynamic 模式下的最大高度(默认 4320
--dpr 设备像素比(默认 2
依赖安装: 依赖安装:
pip install markdown pyyaml pillow playwright pip install markdown pyyaml playwright
playwright install chromium playwright install chromium
""" """
@@ -18,6 +32,7 @@ import re
import sys import sys
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import List, Dict, Any, Optional
try: try:
import markdown import markdown
@@ -32,10 +47,27 @@ except ImportError as e:
# 获取脚本所在目录 # 获取脚本所在目录
SCRIPT_DIR = Path(__file__).parent.parent SCRIPT_DIR = Path(__file__).parent.parent
ASSETS_DIR = SCRIPT_DIR / "assets" ASSETS_DIR = SCRIPT_DIR / "assets"
THEMES_DIR = ASSETS_DIR / "themes"
# 卡片尺寸配置 (3:4 比例) # 默认卡片尺寸配置 (3:4 比例)
CARD_WIDTH = 1080 DEFAULT_WIDTH = 1080
CARD_HEIGHT = 1440 DEFAULT_HEIGHT = 1440
MAX_HEIGHT = 4320 # dynamic 模式最大高度
# 可用主题列表
AVAILABLE_THEMES = [
'default',
'playful-geometric',
'neo-brutalism',
'botanical',
'professional',
'retro',
'terminal',
'sketch'
]
# 分页模式
PAGING_MODES = ['separator', 'auto-fit', 'auto-split', 'dynamic']
def parse_markdown_file(file_path: str) -> dict: def parse_markdown_file(file_path: str) -> dict:
@@ -63,9 +95,8 @@ def parse_markdown_file(file_path: str) -> dict:
} }
def split_content_by_separator(body: str) -> list: def split_content_by_separator(body: str) -> List[str]:
"""按照 --- 分隔符拆分正文为多张卡片内容""" """按照 --- 分隔符拆分正文为多张卡片内容"""
# 使用 --- 作为分隔符,但要排除 YAML 头部的 ---
parts = re.split(r'\n---+\n', body) parts = re.split(r'\n---+\n', body)
return [part.strip() for part in parts if part.strip()] return [part.strip() for part in parts if part.strip()]
@@ -96,17 +127,23 @@ def convert_markdown_to_html(md_content: str) -> str:
return html + tags_html return html + tags_html
def load_template(template_name: str) -> str: def load_theme_css(theme: str) -> str:
"""加载 HTML 模板""" """加载主题 CSS 样式"""
template_path = ASSETS_DIR / template_name theme_file = THEMES_DIR / f"{theme}.css"
with open(template_path, 'r', encoding='utf-8') as f: if theme_file.exists():
with open(theme_file, 'r', encoding='utf-8') as f:
return f.read() return f.read()
else:
# 如果主题不存在,使用默认主题
default_file = THEMES_DIR / "default.css"
if default_file.exists():
with open(default_file, 'r', encoding='utf-8') as f:
return f.read()
return ""
def generate_cover_html(metadata: dict) -> str: def generate_cover_html(metadata: dict, theme: str, width: int, height: int) -> str:
"""生成封面 HTML""" """生成封面 HTML"""
template = load_template('cover.html')
emoji = metadata.get('emoji', '📝') emoji = metadata.get('emoji', '📝')
title = metadata.get('title', '标题') title = metadata.get('title', '标题')
subtitle = metadata.get('subtitle', '') subtitle = metadata.get('subtitle', '')
@@ -117,32 +154,283 @@ def generate_cover_html(metadata: dict) -> str:
if len(subtitle) > 15: if len(subtitle) > 15:
subtitle = subtitle[:15] subtitle = subtitle[:15]
html = template.replace('{{EMOJI}}', emoji) # 获取主题背景色
html = html.replace('{{TITLE}}', title) theme_backgrounds = {
html = html.replace('{{SUBTITLE}}', subtitle) 'default': 'linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%)',
'playful-geometric': 'linear-gradient(180deg, #8B5CF6 0%, #F472B6 100%)',
'neo-brutalism': 'linear-gradient(180deg, #FF4757 0%, #FECA57 100%)',
'botanical': 'linear-gradient(180deg, #4A7C59 0%, #8FBC8F 100%)',
'professional': 'linear-gradient(180deg, #2563EB 0%, #3B82F6 100%)',
'retro': 'linear-gradient(180deg, #D35400 0%, #F39C12 100%)',
'terminal': 'linear-gradient(180deg, #0D1117 0%, #21262D 100%)',
'sketch': 'linear-gradient(180deg, #555555 0%, #999999 100%)'
}
bg = theme_backgrounds.get(theme, theme_backgrounds['default'])
# 封面标题文字渐变随主题变化
title_gradients = {
'default': 'linear-gradient(180deg, #111827 0%, #4B5563 100%)',
'playful-geometric': 'linear-gradient(180deg, #7C3AED 0%, #F472B6 100%)',
'neo-brutalism': 'linear-gradient(180deg, #000000 0%, #FF4757 100%)',
'botanical': 'linear-gradient(180deg, #1F2937 0%, #4A7C59 100%)',
'professional': 'linear-gradient(180deg, #1E3A8A 0%, #2563EB 100%)',
'retro': 'linear-gradient(180deg, #8B4513 0%, #D35400 100%)',
'terminal': 'linear-gradient(180deg, #39D353 0%, #58A6FF 100%)',
'sketch': 'linear-gradient(180deg, #111827 0%, #6B7280 100%)',
}
title_bg = title_gradients.get(theme, title_gradients['default'])
html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width={width}, height={height}">
<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: {width}px;
height: {height}px;
overflow: hidden;
}}
.cover-container {{
width: {width}px;
height: {height}px;
background: {bg};
position: relative;
overflow: hidden;
}}
.cover-inner {{
position: absolute;
width: {int(width * 0.88)}px;
height: {int(height * 0.91)}px;
left: {int(width * 0.06)}px;
top: {int(height * 0.045)}px;
background: #F3F3F3;
border-radius: 25px;
display: flex;
flex-direction: column;
padding: {int(width * 0.074)}px {int(width * 0.079)}px;
}}
.cover-emoji {{
font-size: {int(width * 0.167)}px;
line-height: 1.2;
margin-bottom: {int(height * 0.035)}px;
}}
.cover-title {{
font-weight: 900;
font-size: {int(width * 0.12)}px;
line-height: 1.4;
background: {title_bg};
-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: {int(width * 0.067)}px;
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>'''
return html return html
def generate_card_html(content: str, page_number: int = 1, total_pages: int = 1) -> str: def generate_card_html(content: str, theme: str, page_number: int = 1,
total_pages: int = 1, width: int = DEFAULT_WIDTH,
height: int = DEFAULT_HEIGHT, mode: str = 'separator') -> str:
"""生成正文卡片 HTML""" """生成正文卡片 HTML"""
template = load_template('card.html')
html_content = convert_markdown_to_html(content) html_content = convert_markdown_to_html(content)
theme_css = load_theme_css(theme)
page_text = f"{page_number}/{total_pages}" if total_pages > 1 else "" 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) theme_backgrounds = {
'default': 'linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%)',
'playful-geometric': 'linear-gradient(135deg, #8B5CF6 0%, #F472B6 100%)',
'neo-brutalism': 'linear-gradient(135deg, #FF4757 0%, #FECA57 100%)',
'botanical': 'linear-gradient(135deg, #4A7C59 0%, #8FBC8F 100%)',
'professional': 'linear-gradient(135deg, #2563EB 0%, #3B82F6 100%)',
'retro': 'linear-gradient(135deg, #D35400 0%, #F39C12 100%)',
'terminal': 'linear-gradient(135deg, #0D1117 0%, #161B22 100%)',
'sketch': 'linear-gradient(135deg, #555555 0%, #888888 100%)'
}
bg = theme_backgrounds.get(theme, theme_backgrounds['default'])
# 根据模式设置不同的容器样式
if mode == 'auto-fit':
container_style = f'''
width: {width}px;
height: {height}px;
background: {bg};
position: relative;
padding: 50px;
overflow: hidden;
'''
inner_style = f'''
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 60px;
height: calc({height}px - 100px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
overflow: hidden;
display: flex;
flex-direction: column;
'''
content_style = '''
flex: 1;
overflow: hidden;
'''
elif mode == 'dynamic':
container_style = f'''
width: {width}px;
min-height: {height}px;
background: {bg};
position: relative;
padding: 50px;
'''
inner_style = '''
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 60px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
'''
content_style = ''
else: # separator 和 auto-split
container_style = f'''
width: {width}px;
min-height: {height}px;
background: {bg};
position: relative;
padding: 50px;
overflow: hidden;
'''
inner_style = f'''
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 60px;
min-height: calc({height}px - 100px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
'''
content_style = ''
html = f'''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width={width}">
<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: {width}px;
overflow: hidden;
background: transparent;
}}
.card-container {{
{container_style}
}}
.card-inner {{
{inner_style}
}}
.card-content {{
line-height: 1.7;
{content_style}
}}
/* auto-fit 用:对整个内容块做 transform 缩放 */
.card-content-scale {{
transform-origin: top left;
will-change: transform;
}}
{theme_css}
.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">
<div class="card-content-scale">{html_content}</div>
</div>
</div>
<div class="page-number">{page_text}</div>
</div>
</body>
</html>'''
return html return html
async def render_html_to_image(html_content: str, output_path: str, width: int = CARD_WIDTH, height: int = CARD_HEIGHT): async def render_html_to_image(html_content: str, output_path: str,
width: int = DEFAULT_WIDTH,
height: int = DEFAULT_HEIGHT,
mode: str = 'separator',
max_height: int = MAX_HEIGHT,
dpr: int = 2):
"""使用 Playwright 将 HTML 渲染为图片""" """使用 Playwright 将 HTML 渲染为图片"""
async with async_playwright() as p: async with async_playwright() as p:
browser = await p.chromium.launch() browser = await p.chromium.launch()
page = await browser.new_page(viewport={'width': width, 'height': height})
# 设置视口大小
viewport_height = height if mode != 'dynamic' else max_height
page = await browser.new_page(
viewport={'width': width, 'height': viewport_height},
device_scale_factor=dpr
)
# 创建临时 HTML 文件 # 创建临时 HTML 文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f: with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
@@ -156,13 +444,58 @@ async def render_html_to_image(html_content: str, output_path: str, width: int =
# 等待字体加载 # 等待字体加载
await page.wait_for_timeout(500) await page.wait_for_timeout(500)
# 获取实际内容高度 if mode == 'auto-fit':
# 自动缩放模式:对整个内容块做 transform 缩放(标题/代码块等固定 px 也会一起缩放)
await page.evaluate('''() => {
const viewportContent = document.querySelector('.card-content');
const scaleEl = document.querySelector('.card-content-scale');
if (!viewportContent || !scaleEl) return;
// 先重置,测量原始尺寸
scaleEl.style.transform = 'none';
scaleEl.style.width = '';
scaleEl.style.height = '';
const availableWidth = viewportContent.clientWidth;
const availableHeight = viewportContent.clientHeight;
// scrollWidth/scrollHeight 反映内容的自然尺寸
const contentWidth = Math.max(scaleEl.scrollWidth, scaleEl.getBoundingClientRect().width);
const contentHeight = Math.max(scaleEl.scrollHeight, scaleEl.getBoundingClientRect().height);
if (!contentWidth || !contentHeight || !availableWidth || !availableHeight) return;
// 只缩小不放大,避免“撑太大”
const scale = Math.min(1, availableWidth / contentWidth, availableHeight / contentHeight);
// 为避免 transform 后布局尺寸不匹配导致裁切,扩大布局盒子
scaleEl.style.width = (availableWidth / scale) + 'px';
// 顶部对齐更稳;如需居中可计算 offset
const offsetX = 0;
const offsetY = 0;
scaleEl.style.transformOrigin = 'top left';
scaleEl.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
}''')
await page.wait_for_timeout(100)
actual_height = height
elif mode == 'dynamic':
# 动态高度模式:根据内容调整图片高度
content_height = await page.evaluate('''() => { content_height = await page.evaluate('''() => {
const container = document.querySelector('.card-container') || document.querySelector('.cover-container'); const container = document.querySelector('.card-container');
return container ? container.scrollHeight : document.body.scrollHeight; return container ? container.scrollHeight : document.body.scrollHeight;
}''') }''')
# 确保高度在合理范围内
actual_height = max(height, min(content_height, max_height))
# 确保高度至少为 1440px3:4 比例) else: # separator 和 auto-split
# 获取实际内容高度
content_height = await page.evaluate('''() => {
const container = document.querySelector('.card-container');
return container ? container.scrollHeight : document.body.scrollHeight;
}''')
actual_height = max(height, content_height) actual_height = max(height, content_height)
# 截图 # 截图
@@ -172,16 +505,86 @@ async def render_html_to_image(html_content: str, output_path: str, width: int =
type='png' type='png'
) )
print(f" ✅ 已生成: {output_path}") print(f" ✅ 已生成: {output_path} ({width}x{actual_height})")
return actual_height
finally: finally:
os.unlink(temp_html_path) os.unlink(temp_html_path)
await browser.close() await browser.close()
async def render_markdown_to_cards(md_file: str, output_dir: str): async def auto_split_content(body: str, theme: str, width: int, height: int,
dpr: int = 2) -> List[str]:
"""自动切分内容:根据渲染后的高度自动分页"""
# 将内容按段落分割
paragraphs = re.split(r'\n\n+', body)
cards = []
current_content = []
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page(
viewport={'width': width, 'height': height * 2},
device_scale_factor=dpr
)
try:
for para in paragraphs:
# 尝试将当前段落加入
test_content = current_content + [para]
test_md = '\n\n'.join(test_content)
html = generate_card_html(test_md, theme, 1, 1, width, height, 'auto-split')
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
f.write(html)
temp_path = f.name
await page.goto(f'file://{temp_path}')
await page.wait_for_load_state('networkidle')
await page.wait_for_timeout(200)
content_height = await page.evaluate('''() => {
const content = document.querySelector('.card-content');
return content ? content.scrollHeight : 0;
}''')
os.unlink(temp_path)
# 内容区域的可用高度(去除 padding 等)
available_height = height - 220 # 50*2 padding + 60*2 inner padding
if content_height > available_height and current_content:
# 当前卡片已满,保存并开始新卡片
cards.append('\n\n'.join(current_content))
current_content = [para]
else:
current_content = test_content
# 保存最后一张卡片
if current_content:
cards.append('\n\n'.join(current_content))
finally:
await browser.close()
return cards
async def render_markdown_to_cards(md_file: str, output_dir: str,
theme: str = 'default',
mode: str = 'separator',
width: int = DEFAULT_WIDTH,
height: int = DEFAULT_HEIGHT,
max_height: int = MAX_HEIGHT,
dpr: int = 2):
"""主渲染函数:将 Markdown 文件渲染为多张卡片图片""" """主渲染函数:将 Markdown 文件渲染为多张卡片图片"""
print(f"\n🎨 开始渲染: {md_file}") print(f"\n🎨 开始渲染: {md_file}")
print(f" 📐 主题: {theme}")
print(f" 📏 模式: {mode}")
print(f" 📐 尺寸: {width}x{height}")
# 确保输出目录存在 # 确保输出目录存在
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
@@ -191,25 +594,29 @@ async def render_markdown_to_cards(md_file: str, output_dir: str):
metadata = data['metadata'] metadata = data['metadata']
body = data['body'] body = data['body']
# 分割正文内容 # 根据模式处理内容分割
if mode == 'auto-split':
print(" ⏳ 自动分析内容并切分...")
card_contents = await auto_split_content(body, theme, width, height, dpr)
else:
card_contents = split_content_by_separator(body) card_contents = split_content_by_separator(body)
total_cards = len(card_contents)
total_cards = len(card_contents)
print(f" 📄 检测到 {total_cards} 张正文卡片") print(f" 📄 检测到 {total_cards} 张正文卡片")
# 生成封面 # 生成封面
if metadata.get('emoji') or metadata.get('title'): if metadata.get('emoji') or metadata.get('title'):
print(" 📷 生成封面...") print(" 📷 生成封面...")
cover_html = generate_cover_html(metadata) cover_html = generate_cover_html(metadata, theme, width, height)
cover_path = os.path.join(output_dir, 'cover.png') cover_path = os.path.join(output_dir, 'cover.png')
await render_html_to_image(cover_html, cover_path) await render_html_to_image(cover_html, cover_path, width, height, 'separator', max_height, dpr)
# 生成正文卡片 # 生成正文卡片
for i, content in enumerate(card_contents, 1): for i, content in enumerate(card_contents, 1):
print(f" 📷 生成卡片 {i}/{total_cards}...") print(f" 📷 生成卡片 {i}/{total_cards}...")
card_html = generate_card_html(content, i, total_cards) card_html = generate_card_html(content, theme, i, total_cards, width, height, mode)
card_path = os.path.join(output_dir, f'card_{i}.png') card_path = os.path.join(output_dir, f'card_{i}.png')
await render_html_to_image(card_html, card_path) await render_html_to_image(card_html, card_path, width, height, mode, max_height, dpr)
print(f"\n✨ 渲染完成!图片已保存到: {output_dir}") print(f"\n✨ 渲染完成!图片已保存到: {output_dir}")
return total_cards return total_cards
@@ -217,7 +624,25 @@ async def render_markdown_to_cards(md_file: str, output_dir: str):
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='将 Markdown 文件渲染为小红书风格的图片卡片' description='将 Markdown 文件渲染为小红书风格的图片卡片(支持多种样式和分页模式)',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
可用主题:
default - 默认紫色渐变风格
playful-geometric - 活泼几何风格Memphis 设计)
neo-brutalism - 新粗野主义风格
botanical - 植物园自然风格
professional - 专业商务风格
retro - 复古怀旧风格
terminal - 终端/命令行风格
sketch - 手绘素描风格
分页模式:
separator - 按 --- 分隔符手动分页(默认)
auto-fit - 自动缩放文字以填满固定尺寸
auto-split - 根据内容高度自动切分
dynamic - 根据内容动态调整图片高度
'''
) )
parser.add_argument( parser.add_argument(
'markdown_file', 'markdown_file',
@@ -228,6 +653,42 @@ def main():
default=os.getcwd(), default=os.getcwd(),
help='输出目录(默认为当前工作目录)' help='输出目录(默认为当前工作目录)'
) )
parser.add_argument(
'--theme', '-t',
choices=AVAILABLE_THEMES,
default='default',
help='排版主题(默认: default'
)
parser.add_argument(
'--mode', '-m',
choices=PAGING_MODES,
default='separator',
help='分页模式(默认: separator'
)
parser.add_argument(
'--width', '-w',
type=int,
default=DEFAULT_WIDTH,
help=f'图片宽度(默认: {DEFAULT_WIDTH}'
)
parser.add_argument(
'--height',
type=int,
default=DEFAULT_HEIGHT,
help=f'图片高度(默认: {DEFAULT_HEIGHT}'
)
parser.add_argument(
'--max-height',
type=int,
default=MAX_HEIGHT,
help=f'dynamic 模式下的最大高度(默认: {MAX_HEIGHT}'
)
parser.add_argument(
'--dpr',
type=int,
default=2,
help='设备像素比(默认: 2'
)
args = parser.parse_args() args = parser.parse_args()
@@ -235,7 +696,16 @@ def main():
print(f"❌ 错误: 文件不存在 - {args.markdown_file}") print(f"❌ 错误: 文件不存在 - {args.markdown_file}")
sys.exit(1) sys.exit(1)
asyncio.run(render_markdown_to_cards(args.markdown_file, args.output_dir)) asyncio.run(render_markdown_to_cards(
args.markdown_file,
args.output_dir,
theme=args.theme,
mode=args.mode,
width=args.width,
height=args.height,
max_height=args.max_height,
dpr=args.dpr
))
if __name__ == '__main__': if __name__ == '__main__':