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