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

275
README.md
View File

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

169
SKILL.md
View File

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

View File

@@ -23,7 +23,7 @@
.cover-container {
width: 1080px;
height: 1440px;
background: linear-gradient(180deg, #3450E4 0%, #D266DA 100%);
background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
position: relative;
overflow: hidden;
}
@@ -51,7 +51,7 @@
font-weight: 900;
font-size: 130px;
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-text-fill-color: transparent;
background-clip: text;

View File

@@ -1,84 +1,63 @@
---
emoji: "🚀"
title: "5个效率神器"
subtitle: "让工作效率翻倍"
emoji: "🎨"
title: "8种超美排版风格"
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
# 快捷命令示例
raycast://extensions/raycast/clipboard/clipboard-history
python render_xhs.py example.md -t playful-geometric
```
**必装插件推荐:**
- 剪贴板历史
- 窗口管理
- 快捷短语
- 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 {
width: 1080px;
height: 1440px;
background: linear-gradient(180deg, #3450E4 0%, #D266DA 100%);
background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
position: relative;
overflow: hidden;
}
@@ -65,7 +65,7 @@ body {
font-weight: 900;
font-size: 130px;
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-text-fill-color: transparent;
background-clip: text;
@@ -89,7 +89,7 @@ body {
.card-container {
width: 1080px;
min-height: 1440px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
position: relative;
padding: 50px;
overflow: hidden;
@@ -290,7 +290,7 @@ body {
}
.bg-gradient-2 {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%);
}
.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",
"version": "1.0.0",
"description": "小红书笔记卡片渲染工具 - Node.js 版本",
"version": "2.0.0",
"description": "小红书笔记素材创作工具 - 支持多种排版样式和智能分页",
"main": "scripts/render_xhs.js",
"scripts": {
"render": "node scripts/render_xhs.js"
},
"dependencies": {
"marked": "^11.0.0",
"js-yaml": "^4.1.0",
"playwright": "^1.40.0"
"render": "node scripts/render_xhs.js",
"install-browsers": "npx playwright install chromium"
},
"keywords": [
"xiaohongshu",
"markdown",
"image",
"card",
"generator"
"image-generation",
"social-media"
],
"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
# YAML 解析
PyYAML>=6.0
# 图片渲染 (使用 Playwright)
# 浏览器自动化(渲染图片)
playwright>=1.40.0
# 小红书 API 客户端
xhs
# 小红书发布
xhs>=0.4.0
# 环境变量加载
# 环境变量管理
python-dotenv>=1.0.0
# HTTP 请求API 模式)
requests>=2.28.0

View File

@@ -1,72 +1,107 @@
#!/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
# 可选API 服务地址(使用 --api-mode 时需要)
XHS_API_URL=http://localhost:5005
依赖安装:
pip install xhs python-dotenv
pip install xhs python-dotenv requests
"""
import argparse
import os
import sys
import json
import re
from pathlib import Path
from typing import List, Optional, Dict, Any
try:
from dotenv import load_dotenv
from xhs import XhsClient
import requests
except ImportError as e:
print(f"缺少依赖: {e}")
print("请运行: pip install xhs python-dotenv")
print("请运行: pip install python-dotenv requests")
sys.exit(1)
def load_cookie():
def load_cookie() -> str:
"""从 .env 文件加载 Cookie"""
# 尝试从当前目录加载 .env
env_path = Path.cwd() / '.env'
if env_path.exists():
load_dotenv(env_path)
# 尝试从多个位置加载 .env
env_paths = [
Path.cwd() / '.env',
Path(__file__).parent.parent / '.env',
Path(__file__).parent.parent.parent / '.env',
]
# 也尝试从脚本目录加载
script_env = Path(__file__).parent.parent / '.env'
if script_env.exists():
load_dotenv(script_env)
for env_path in env_paths:
if env_path.exists():
load_dotenv(env_path)
break
cookie = os.getenv('XHS_COOKIE')
if not cookie:
print("❌ 错误: 未找到 XHS_COOKIE 环境变量")
print("在当前目录创建 .env 文件,添加以下内容:")
print("请创建 .env 文件,添加以下内容:")
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)
return cookie
def create_client(cookie: str) -> XhsClient:
"""创建小红书客户端"""
try:
# 使用本地签名
from xhs.help import sign as local_sign
def sign_func(uri, data=None, a1="", web_session=""):
return local_sign(uri, data, a1=a1)
client = XhsClient(cookie=cookie, sign=sign_func)
return client
except Exception as e:
print(f"❌ 创建客户端失败: {e}")
sys.exit(1)
def parse_cookie(cookie_string: str) -> Dict[str, str]:
"""解析 Cookie 字符串为字典"""
cookies = {}
for item in cookie_string.split(';'):
item = item.strip()
if '=' in item:
key, value = item.split('=', 1)
cookies[key.strip()] = value.strip()
return cookies
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 = []
for path in image_paths:
@@ -82,51 +117,218 @@ def validate_images(image_paths: list) -> list:
return valid_images
def publish_note(client: XhsClient, title: str, desc: str, images: list,
is_private: bool = False, post_time: str = None):
"""发布图文笔记"""
try:
print(f"\n🚀 准备发布笔记...")
class LocalPublisher:
"""本地发布模式:直接使用 xhs 库"""
def __init__(self, cookie: str):
self.cookie = cookie
self.client = None
def init_client(self):
"""初始化 xhs 客户端"""
try:
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" 📝 描述: {desc[:50]}..." if len(desc) > 50 else f" 📝 描述: {desc}")
print(f" 🖼️ 图片数量: {len(images)}")
result = client.create_image_note(
title=title,
desc=desc,
files=images,
is_private=is_private,
post_time=post_time
)
print("\n✨ 笔记发布成功!")
if isinstance(result, dict):
note_id = result.get('note_id') or result.get('id')
if note_id:
print(f" 📎 笔记ID: {note_id}")
print(f" 🔗 链接: https://www.xiaohongshu.com/explore/{note_id}")
return result
except Exception as e:
print(f"\n❌ 发布失败: {e}")
sys.exit(1)
try:
result = self.client.create_image_note(
title=title,
desc=desc,
files=images,
is_private=is_private,
post_time=post_time
)
print("\n✨ 笔记发布成功!")
if isinstance(result, dict):
note_id = result.get('note_id') or result.get('id')
if note_id:
print(f" 📎 笔记ID: {note_id}")
print(f" 🔗 链接: https://www.xiaohongshu.com/explore/{note_id}")
return result
except Exception as e:
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
def get_user_info(client: XhsClient):
"""获取当前登录用户信息"""
try:
info = client.get_self_info()
print(f"\n👤 当前用户: {info.get('nickname', '未知')}")
return info
except Exception as e:
print(f"⚠️ 无法获取用户信息: {e}")
return None
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)
# 初始化 session
try:
resp = requests.post(
f"{self.api_url}/init",
json={
"session_id": self.session_id,
"cookie": self.cookie
},
timeout=30
)
result = resp.json()
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:
resp = requests.post(
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 None
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🚀 准备发布笔记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():
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(
'--title', '-t',
@@ -154,6 +356,16 @@ def main():
default=None,
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(
'--dry-run',
action='store_true',
@@ -170,32 +382,43 @@ def main():
# 加载 Cookie
cookie = load_cookie()
# 验证 Cookie 格式
validate_cookie(cookie)
# 验证图片
valid_images = validate_images(args.images)
# 创建客户端
client = create_client(cookie)
# 获取用户信息(验证 Cookie 有效性)
get_user_info(client)
if args.dry_run:
print("\n🔍 验证模式 - 不会实际发布")
print(f" 📌 标题: {args.title}")
print(f" 📝 描述: {args.desc}")
print(f" 🖼️ 图片: {valid_images}")
print(f" 🔒 私密: {args.private}")
print(f" ⏰ 定时: {args.post_time or '立即发布'}")
print(f" 📡 模式: {'API' if args.api_mode else '本地'}")
print("\n✅ 验证通过,可以发布")
return
# 选择发布方式
if args.api_mode:
publisher = ApiPublisher(cookie, args.api_url)
else:
publisher = LocalPublisher(cookie)
# 初始化客户端
publisher.init_client()
# 发布笔记
publish_note(
client=client,
title=args.title,
desc=args.desc,
images=valid_images,
is_private=args.private,
post_time=args.post_time
)
try:
publisher.publish(
title=args.title,
desc=args.desc,
images=valid_images,
is_private=args.private,
post_time=args.post_time
)
except Exception as e:
sys.exit(1)
if __name__ == '__main__':

View File

@@ -1,188 +1,511 @@
#!/usr/bin/env node
/**
* 小红书卡片渲染脚本 - Node.js 版
* 将 Markdown 文件渲染为小红书风格的图片卡片
* 小红书卡片渲染脚本 - Node.js 增强
* 支持多种排版样式和智能分页策略
*
* 使用方法:
* 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
* npx playwright install chromium
* npm install marked yaml playwright
* npx playwright install chromium
*/
const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright');
const { marked } = require('marked');
const yaml = require('js-yaml');
const yaml = require('yaml');
const { chromium } = require('playwright');
// 获取脚本所在目录
const SCRIPT_DIR = path.dirname(__dirname);
const ASSETS_DIR = path.join(SCRIPT_DIR, 'assets');
const THEMES_DIR = path.join(ASSETS_DIR, 'themes');
// 卡片尺寸配置 (3:4 比例)
const CARD_WIDTH = 1080;
const CARD_HEIGHT = 1440;
// 默认卡片尺寸配置 (3:4 比例)
const DEFAULT_WIDTH = 1080;
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) {
const content = fs.readFileSync(filePath, 'utf-8');
// 解析 YAML 头部
const yamlPattern = /^---\s*\n([\s\S]*?)\n---\s*\n/;
const yamlMatch = content.match(yamlPattern);
const yamlMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
let metadata = {};
let body = content;
if (yamlMatch) {
try {
metadata = yaml.load(yamlMatch[1]) || {};
metadata = yaml.parse(yamlMatch[1]) || {};
} catch (e) {
metadata = {};
}
body = content.slice(yamlMatch[0].length);
}
return {
metadata,
body: body.trim()
};
return { metadata, body: body.trim() };
}
/**
* 按照 --- 分隔符拆分正文为多张卡片内容
* 按分隔符拆分内容
*/
function splitContentBySeparator(body) {
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) {
// 处理 tags以 # 开头的标签)
const tagsPattern = /((?:#[\w\u4e00-\u9fa5]+\s*)+)$/m;
const tagsMatch = mdContent.match(tagsPattern);
let tagsHtml = "";
if (tagsMatch) {
const tagsStr = tagsMatch[1];
mdContent = mdContent.slice(0, tagsMatch.index).trim();
const tags = tagsStr.match(/#([\w\u4e00-\u9fa5]+)/g);
if (tags) {
tagsHtml = '<div class="tags-container">';
for (const tag of tags) {
tagsHtml += `<span class="tag">${tag}</span>`;
}
tagsHtml += '</div>';
}
function loadThemeCss(theme) {
const themeFile = path.join(THEMES_DIR, `${theme}.css`);
if (fs.existsSync(themeFile)) {
return fs.readFileSync(themeFile, 'utf-8');
}
// 转换 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');
const defaultFile = path.join(THEMES_DIR, 'default.css');
if (fs.existsSync(defaultFile)) {
return fs.readFileSync(defaultFile, 'utf-8');
}
return '';
}
/**
* 生成封面 HTML
*/
function generateCoverHtml(metadata) {
let template = loadTemplate('cover.html');
let emoji = metadata.emoji || '📝';
function generateCoverHtml(metadata, theme, width, height) {
const emoji = metadata.emoji || '📝';
let title = metadata.title || '标题';
let subtitle = metadata.subtitle || '';
// 限制标题和副标题长度
if (title.length > 15) {
title = title.slice(0, 15);
}
if (subtitle.length > 15) {
subtitle = subtitle.slice(0, 15);
}
if (title.length > 15) title = title.slice(0, 15);
if (subtitle.length > 15) subtitle = subtitle.slice(0, 15);
template = template.replace('{{EMOJI}}', emoji);
template = template.replace('{{TITLE}}', title);
template = template.replace('{{SUBTITLE}}', subtitle);
const bg = THEME_BACKGROUNDS[theme] || THEME_BACKGROUNDS['default'];
const titleBg = THEME_TITLE_GRADIENTS[theme] || THEME_TITLE_GRADIENTS['default'];
return template;
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;
}
.cover-container {
width: ${width}px;
height: ${height}px;
background: ${bg};
position: relative;
overflow: hidden;
}
.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
*/
function generateCardHtml(content, pageNumber = 1, totalPages = 1) {
let template = loadTemplate('card.html');
const htmlContent = convertMarkdownToHtml(content);
function generateCardHtml(content, theme, pageNumber, totalPages, width, height, mode) {
const htmlContent = marked.parse(content);
const themeCss = loadThemeCss(theme);
const pageText = totalPages > 1 ? `${pageNumber}/${totalPages}` : '';
const bg = THEME_BACKGROUNDS[theme] || THEME_BACKGROUNDS['default'];
template = template.replace('{{CONTENT}}', htmlContent);
template = template.replace('{{PAGE_NUMBER}}', pageText);
let containerStyle, innerStyle, contentStyle;
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 viewportHeight = mode !== 'dynamic' ? height : maxHeight;
const page = await browser.newPage({
viewport: { width, height }
viewport: { width, height: viewportHeight },
deviceScaleFactor: dpr
});
// 设置 HTML 内容
await page.setContent(htmlContent, {
waitUntil: 'networkidle'
});
// 等待字体加载
await page.setContent(htmlContent);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
// 获取实际内容高度
const contentHeight = await page.evaluate(() => {
const container = document.querySelector('.card-container') || document.querySelector('.cover-container');
return container ? container.scrollHeight : document.body.scrollHeight;
});
let actualHeight;
// 确保高度至少为 1440px3:4 比例)
const actualHeight = Math.max(height, contentHeight);
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 container = document.querySelector('.card-container');
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);
}
// 截图
await page.screenshot({
path: outputPath,
clip: { x: 0, y: 0, width, height: actualHeight },
type: 'png'
});
console.log(` ✅ 已生成: ${outputPath}`);
await browser.close();
console.log(` ✅ 已生成: ${outputPath} (${width}x${actualHeight})`);
return actualHeight;
}
/**
* 主渲染函数:将 Markdown 文件渲染为多张卡片图片
* 主渲染函数
*/
async function renderMarkdownToCards(mdFile, outputDir) {
console.log(`\n🎨 开始渲染: ${mdFile}`);
async function renderMarkdownToCards(options) {
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)) {
@@ -190,10 +513,9 @@ async function renderMarkdownToCards(mdFile, outputDir) {
}
// 解析 Markdown 文件
const data = parseMarkdownFile(mdFile);
const { metadata, body } = data;
const { metadata, body } = parseMarkdownFile(markdownFile);
// 分割正文内容
// 分割内容
const cardContents = splitContentBySeparator(body);
const totalCards = cardContents.length;
@@ -202,67 +524,53 @@ async function renderMarkdownToCards(mdFile, outputDir) {
// 生成封面
if (metadata.emoji || metadata.title) {
console.log(' 📷 生成封面...');
const coverHtml = generateCoverHtml(metadata);
const coverHtml = generateCoverHtml(metadata, theme, width, height);
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++) {
const pageNum = i + 1;
console.log(` 📷 生成卡片 ${pageNum}/${totalCards}...`);
const cardHtml = generateCardHtml(cardContents[i], pageNum, totalCards);
const cardPath = path.join(outputDir, `card_${pageNum}.png`);
await renderHtmlToImage(cardHtml, cardPath);
const content = cardContents[i];
console.log(` 📷 生成卡片 ${i + 1}/${totalCards}...`);
const cardHtml = generateCardHtml(content, theme, i + 1, totalCards, width, height, mode);
const cardPath = path.join(outputDir, `card_${i + 1}.png`);
await renderHtmlToImage(cardHtml, cardPath, width, height, mode, maxHeight, dpr);
}
console.log(`\n✨ 渲染完成!图片已保存到: ${outputDir}`);
return totalCards;
}
/**
* 解析命令行参
* 主函
*/
function parseArgs() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('用法: node render_xhs.js <markdown_file> [--output-dir <output_directory>]');
process.exit(1);
}
let markdownFile = null;
let outputDir = process.cwd();
for (let i = 0; i < args.length; i++) {
if (args[i] === '--output-dir' || args[i] === '-o') {
outputDir = args[i + 1];
i++;
} else if (!args[i].startsWith('-')) {
markdownFile = args[i];
}
}
if (!markdownFile) {
console.error('❌ 错误: 请指定 Markdown 文件');
process.exit(1);
}
if (!fs.existsSync(markdownFile)) {
console.error(`❌ 错误: 文件不存在 - ${markdownFile}`);
process.exit(1);
}
return { markdownFile, outputDir };
}
// 主函数
async function main() {
const { markdownFile, outputDir } = parseArgs();
await renderMarkdownToCards(markdownFile, outputDir);
const options = parseArgs();
if (!options.markdownFile) {
console.error('❌ 错误: 请提供 Markdown 文件路径');
printHelp();
process.exit(1);
}
if (!fs.existsSync(options.markdownFile)) {
console.error(`❌ 错误: 文件不存在 - ${options.markdownFile}`);
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(error => {
console.error('❌ 渲染失败:', error.message);
process.exit(1);
});
main().catch(console.error);

View File

@@ -1,13 +1,27 @@
#!/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
"""
@@ -18,6 +32,7 @@ import re
import sys
import tempfile
from pathlib import Path
from typing import List, Dict, Any, Optional
try:
import markdown
@@ -32,10 +47,27 @@ except ImportError as e:
# 获取脚本所在目录
SCRIPT_DIR = Path(__file__).parent.parent
ASSETS_DIR = SCRIPT_DIR / "assets"
THEMES_DIR = ASSETS_DIR / "themes"
# 卡片尺寸配置 (3:4 比例)
CARD_WIDTH = 1080
CARD_HEIGHT = 1440
# 默认卡片尺寸配置 (3:4 比例)
DEFAULT_WIDTH = 1080
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:
@@ -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)
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
def load_template(template_name: str) -> str:
"""加载 HTML 模板"""
template_path = ASSETS_DIR / template_name
with open(template_path, 'r', encoding='utf-8') as f:
return f.read()
def load_theme_css(theme: str) -> str:
"""加载主题 CSS 样式"""
theme_file = THEMES_DIR / f"{theme}.css"
if theme_file.exists():
with open(theme_file, 'r', encoding='utf-8') as f:
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"""
template = load_template('cover.html')
emoji = metadata.get('emoji', '📝')
title = metadata.get('title', '标题')
subtitle = metadata.get('subtitle', '')
@@ -117,32 +154,283 @@ def generate_cover_html(metadata: dict) -> str:
if len(subtitle) > 15:
subtitle = subtitle[:15]
html = template.replace('{{EMOJI}}', emoji)
html = html.replace('{{TITLE}}', title)
html = html.replace('{{SUBTITLE}}', subtitle)
# 获取主题背景色
theme_backgrounds = {
'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
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"""
template = load_template('card.html')
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 ""
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
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 渲染为图片"""
async with async_playwright() as p:
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 文件
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
@@ -156,14 +444,59 @@ async def render_html_to_image(html_content: str, output_path: str, width: int =
# 等待字体加载
await page.wait_for_timeout(500)
# 获取实际内容高度
content_height = await page.evaluate('''() => {
const container = document.querySelector('.card-container') || document.querySelector('.cover-container');
return container ? container.scrollHeight : document.body.scrollHeight;
}''')
# 确保高度至少为 1440px3:4 比例)
actual_height = max(height, content_height)
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('''() => {
const container = document.querySelector('.card-container');
return container ? container.scrollHeight : document.body.scrollHeight;
}''')
# 确保高度在合理范围内
actual_height = max(height, min(content_height, max_height))
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)
# 截图
await page.screenshot(
@@ -172,16 +505,86 @@ async def render_html_to_image(html_content: str, output_path: str, width: int =
type='png'
)
print(f" ✅ 已生成: {output_path}")
print(f" ✅ 已生成: {output_path} ({width}x{actual_height})")
return actual_height
finally:
os.unlink(temp_html_path)
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 文件渲染为多张卡片图片"""
print(f"\n🎨 开始渲染: {md_file}")
print(f" 📐 主题: {theme}")
print(f" 📏 模式: {mode}")
print(f" 📐 尺寸: {width}x{height}")
# 确保输出目录存在
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']
body = data['body']
# 分割正文内容
card_contents = split_content_by_separator(body)
total_cards = len(card_contents)
# 根据模式处理内容分割
if mode == 'auto-split':
print(" ⏳ 自动分析内容并切分...")
card_contents = await auto_split_content(body, theme, width, height, dpr)
else:
card_contents = split_content_by_separator(body)
total_cards = len(card_contents)
print(f" 📄 检测到 {total_cards} 张正文卡片")
# 生成封面
if metadata.get('emoji') or metadata.get('title'):
print(" 📷 生成封面...")
cover_html = generate_cover_html(metadata)
cover_html = generate_cover_html(metadata, theme, width, height)
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):
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')
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}")
return total_cards
@@ -217,7 +624,25 @@ async def render_markdown_to_cards(md_file: str, output_dir: str):
def main():
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(
'markdown_file',
@@ -228,6 +653,42 @@ def main():
default=os.getcwd(),
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()
@@ -235,7 +696,16 @@ def main():
print(f"❌ 错误: 文件不存在 - {args.markdown_file}")
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__':