小红书笔记创作技能 根据用户提供的资料或需求,创作小红书笔记内容、生成精美图片卡片,并可选择发布到小红书。 详细参数文档见 --- 工作流程 第一步:撰写小红书笔记内容 根据用户需求和资料,创作符合小红书风格的内容: 标题 :不超过 20 字,吸引眼球,可用数字/疑问句/感叹号增强吸引力。 正文 :段落清晰,点缀少量 Emoji(每段 1-2 个),短句短段,结尾附 5-10 个 SEO 标签。 --- 第二步:生成渲染用 Markdown 文档 注意:此 Markdown 专为图片渲染设计,禁止直接使用上一步的笔记正文。 文档结构: 分页策略选择: - 内容需精确切分 → 用 手动分隔,配合 - 内容长短不稳定 → 生成普通 Markdown,使用 --- 第三步:渲染图片卡片 默认主题 : (手绘素描风格) 默认分页 : (按 分隔) 常用示例: 生成结果: (封面)+ 、 ...(正文卡片) 可用主题 ( ): 、 、 、 、 、 、 、 分页模式 ( ): 、 、 、 完整参数说明见 --- 第四步:发布小红书笔记(可选) 前置条件 :安装 Python 与 Node.js 依赖,并配置 中的 。Cookie 必须来自已登录的 (详见 )。 默认以「仅自己可见」发布 ,加 参数才会公开。 发布链路已内置在当前技能中,不依赖外部 API 服务或其他项目目录。 --- 技能资…

\n tags_match = re.search(tags_pattern, md_content, re.MULTILINE)\n tags_html = \"\"\n \n if tags_match:\n tags_str = tags_match.group(1)\n md_content = md_content[:tags_match.start()].strip()\n tags = re.findall(r'#([\\w\\u4e00-\\u9fa5]+)', tags_str)\n if tags:\n accent = style.get('accent_color', '#6366f1')\n tags_html = f'\u003cdiv class=\"tags-container\">'\n for tag in tags:\n tags_html += f'\u003cspan class=\"tag\" style=\"background: {accent};\">#{tag}\u003c/span>'\n tags_html += '\u003c/div>'\n \n # 转换 Markdown 为 HTML\n html = markdown.markdown(\n md_content,\n extensions=['extra', 'codehilite', 'tables', 'nl2br']\n )\n \n return html + tags_html\n\n\ndef generate_cover_html(metadata: dict, style_key: str = \"purple\") -> str:\n \"\"\"生成封面 HTML\"\"\"\n style = STYLES.get(style_key, STYLES[\"purple\"])\n \n emoji = metadata.get('emoji', '📝')\n title = metadata.get('title', '标题')\n subtitle = metadata.get('subtitle', '')\n \n \n # 动态调整标题字体大小\n title_len = len(title)\n if title_len \u003c= 6:\n title_size = 150 # 极大 (width * 0.14)\n elif title_len \u003c= 10:\n title_size = 130 # 大 (width * 0.12)\n elif title_len \u003c= 18:\n title_size = 100 # 中 (width * 0.09)\n elif title_len \u003c= 30:\n title_size = 80 # 小 (width * 0.07)\n else:\n title_size = 60 # 极小 (width * 0.055)\n\n # 暗黑模式特殊处理\n is_dark = style_key == \"dark\"\n text_color = \"#ffffff\" if is_dark else \"#000000\"\n title_gradient = \"linear-gradient(180deg, #ffffff 0%, #cccccc 100%)\" if is_dark else \"linear-gradient(180deg, #2E67B1 0%, #4C4C4C 100%)\"\n inner_bg = \"#1a1a2e\" if is_dark else \"#F3F3F3\"\n \n return f'''\u003c!DOCTYPE html>\n\u003chtml lang=\"zh-CN\">\n\u003chead>\n \u003cmeta charset=\"UTF-8\">\n \u003cmeta name=\"viewport\" content=\"width=1080, height=1440\">\n \u003ctitle>小红书封面\u003c/title>\n \u003cstyle>\n @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');\n * {{ margin: 0; padding: 0; box-sizing: border-box; }}\n body {{\n font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;\n width: 1080px; height: 1440px; overflow: hidden;\n }}\n .cover-container {{\n width: 1080px; height: 1440px;\n background: {style['cover_bg']};\n position: relative; overflow: hidden;\n }}\n .cover-inner {{\n position: absolute; width: 950px; height: 1310px;\n left: 65px; top: 65px;\n background: {inner_bg};\n border-radius: 25px;\n display: flex; flex-direction: column;\n padding: 80px 85px;\n }}\n .cover-emoji {{ font-size: 180px; line-height: 1.2; margin-bottom: 50px; }}\n .cover-title {{\n font-weight: 900; font-size: {title_size}px; line-height: 1.4;\n background: {title_gradient};\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n flex: 1;\n display: flex; align-items: flex-start;\n word-break: normal;\n overflow-wrap: break-word;\n }}\n .cover-subtitle {{\n font-weight: 350; font-size: 72px; line-height: 1.4;\n color: {text_color};\n margin-top: auto;\n }}\n \u003c/style>\n\u003c/head>\n\u003cbody>\n \u003cdiv class=\"cover-container\">\n \u003cdiv class=\"cover-inner\">\n \u003cdiv class=\"cover-emoji\">{emoji}\u003c/div>\n \u003cdiv class=\"cover-title\">{title}\u003c/div>\n \u003cdiv class=\"cover-subtitle\">{subtitle}\u003c/div>\n \u003c/div>\n \u003c/div>\n\u003c/body>\n\u003c/html>'''\n\n\ndef generate_card_html(content: str, page_number: int = 1, total_pages: int = 1, \n style_key: str = \"purple\") -> str:\n \"\"\"生成正文卡片 HTML\"\"\"\n style = STYLES.get(style_key, STYLES[\"purple\"])\n html_content = convert_markdown_to_html(content, style)\n page_text = f\"{page_number}/{total_pages}\" if total_pages > 1 else \"\"\n \n # 暗黑模式特殊处理\n is_dark = style_key == \"dark\"\n card_bg = \"rgba(30, 30, 46, 0.95)\" if is_dark else \"rgba(255, 255, 255, 0.95)\"\n text_color = \"#e0e0e0\" if is_dark else \"#475569\"\n heading_color = \"#ffffff\" if is_dark else \"#1e293b\"\n h2_color = \"#e0e0e0\" if is_dark else \"#334155\"\n h3_color = \"#c0c0c0\" if is_dark else \"#475569\"\n code_bg = \"#0f0f23\" if is_dark else \"#1e293b\"\n pre_bg = \"#0f0f23\" if is_dark else \"#1e293b\"\n blockquote_bg = \"#252540\" if is_dark else \"#f1f5f9\"\n blockquote_border = style['accent_color']\n blockquote_color = \"#a0a0a0\" if is_dark else \"#64748b\"\n \n return f'''\u003c!DOCTYPE html>\n\u003chtml lang=\"zh-CN\">\n\u003chead>\n \u003cmeta charset=\"UTF-8\">\n \u003cmeta name=\"viewport\" content=\"width=1080\">\n \u003ctitle>小红书卡片\u003c/title>\n \u003cstyle>\n @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');\n * {{ margin: 0; padding: 0; box-sizing: border-box; }}\n body {{\n font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;\n width: 1080px; min-height: 1440px; overflow: hidden; background: transparent;\n }}\n .card-container {{\n width: 1080px; min-height: 1440px;\n background: {style['card_bg']};\n position: relative; padding: 50px; overflow: hidden;\n }}\n .card-inner {{\n background: {card_bg};\n border-radius: 20px;\n padding: 60px;\n min-height: calc(1440px - 100px);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n }}\n .card-content {{\n color: {text_color};\n font-size: 42px;\n line-height: 1.7;\n }}\n .card-content h1 {{\n font-size: 72px; font-weight: 700; color: {heading_color};\n margin-bottom: 40px; line-height: 1.3;\n }}\n .card-content h2 {{\n font-size: 56px; font-weight: 600; color: {h2_color};\n margin: 50px 0 25px 0; line-height: 1.4;\n }}\n .card-content h3 {{\n font-size: 48px; font-weight: 600; color: {h3_color};\n margin: 40px 0 20px 0;\n }}\n .card-content p {{ margin-bottom: 35px; }}\n .card-content strong {{ font-weight: 700; color: {heading_color}; }}\n .card-content em {{ font-style: italic; color: {style['accent_color']}; }}\n .card-content a {{\n color: {style['accent_color']}; text-decoration: none;\n border-bottom: 2px solid {style['accent_color']};\n }}\n .card-content ul, .card-content ol {{\n margin: 30px 0; padding-left: 60px;\n }}\n .card-content li {{ margin-bottom: 20px; line-height: 1.6; }}\n .card-content blockquote {{\n border-left: 8px solid {blockquote_border};\n padding-left: 40px;\n background: {blockquote_bg};\n padding-top: 25px; padding-bottom: 25px; padding-right: 30px;\n margin: 35px 0;\n color: {blockquote_color};\n font-style: italic;\n border-radius: 0 12px 12px 0;\n }}\n .card-content blockquote p {{ margin: 0; }}\n .card-content code {{\n background: {'#252540' if is_dark else '#f1f5f9'};\n padding: 6px 16px; border-radius: 8px;\n font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;\n font-size: 38px;\n color: {style['accent_color']};\n }}\n .card-content pre {{\n background: {pre_bg};\n color: {'#e0e0e0' if is_dark else '#e2e8f0'};\n padding: 40px; border-radius: 16px;\n margin: 35px 0;\n overflow-x: visible;\n overflow-wrap: break-word;\n word-wrap: break-word;\n word-break: normal;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n font-size: 36px; line-height: 1.5;\n }}\n .card-content pre code {{\n background: transparent; color: inherit; padding: 0; font-size: inherit;\n }}\n .card-content img {{\n max-width: 100%; height: auto; border-radius: 16px;\n margin: 35px auto; display: block;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);\n }}\n .card-content hr {{\n border: none; height: 2px;\n background: {'#333355' if is_dark else '#e2e8f0'};\n margin: 50px 0;\n }}\n .tags-container {{\n margin-top: 50px; padding-top: 30px;\n border-top: 2px solid {'#333355' if is_dark else '#e2e8f0'};\n }}\n .tag {{\n display: inline-block;\n background: {style['accent_color']};\n color: white;\n padding: 12px 28px; border-radius: 30px;\n font-size: 34px;\n margin: 10px 15px 10px 0;\n font-weight: 500;\n }}\n .page-number {{\n position: absolute;\n bottom: 80px; right: 80px;\n font-size: 36px;\n color: rgba(255, 255, 255, 0.8);\n font-weight: 500;\n }}\n \u003c/style>\n\u003c/head>\n\u003cbody>\n \u003cdiv class=\"card-container\">\n \u003cdiv class=\"card-inner\">\n \u003cdiv class=\"card-content\">\n {html_content}\n \u003c/div>\n \u003c/div>\n \u003cdiv class=\"page-number\">{page_text}\u003c/div>\n \u003c/div>\n\u003c/body>\n\u003c/html>'''\n\n\nasync def measure_content_height(page: Page, html_content: str) -> int:\n \"\"\"使用 Playwright 测量实际内容高度\"\"\"\n await page.set_content(html_content, wait_until='networkidle')\n await page.wait_for_timeout(300) # 等待字体渲染\n \n height = await page.evaluate('''() => {\n const inner = document.querySelector('.card-inner');\n if (inner) {\n return inner.scrollHeight;\n }\n const container = document.querySelector('.card-container');\n return container ? container.scrollHeight : document.body.scrollHeight;\n }''')\n \n return height\n\n\nasync def render_html_to_image(html_content: str, output_path: str, \n width: int = CARD_WIDTH, height: int = CARD_HEIGHT):\n \"\"\"使用 Playwright 将 HTML 渲染为图片\"\"\"\n async with async_playwright() as p:\n browser = await p.chromium.launch()\n page = await browser.new_page(viewport={'width': width, 'height': height})\n \n try:\n await page.set_content(html_content, wait_until='networkidle')\n await page.wait_for_timeout(300)\n \n # 截图固定尺寸\n await page.screenshot(\n path=output_path,\n clip={'x': 0, 'y': 0, 'width': width, 'height': height},\n type='png'\n )\n \n print(f\" ✅ 已生成: {output_path}\")\n \n finally:\n await browser.close()\n\n\nasync def process_and_render_cards(card_contents: List[str], output_dir: str, \n style_key: str) -> List[str]:\n \"\"\"\n 处理卡片内容,检测高度并自动分页,然后渲染\n 返回最终生成的所有卡片文件路径\n \"\"\"\n async with async_playwright() as p:\n browser = await p.chromium.launch()\n page = await browser.new_page(viewport={'width': CARD_WIDTH, 'height': CARD_HEIGHT})\n \n all_cards = []\n \n try:\n for content in card_contents:\n # 预估内容高度\n estimated_height = estimate_content_height(content)\n \n # 如果预估高度超过安全高度,尝试拆分\n if estimated_height > SAFE_HEIGHT:\n split_contents = smart_split_content(content, SAFE_HEIGHT)\n else:\n split_contents = [content]\n \n # 验证每个拆分后的内容\n for split_content in split_contents:\n # 生成临时 HTML 测量\n temp_html = generate_card_html(split_content, 1, 1, style_key)\n actual_height = await measure_content_height(page, temp_html)\n \n # 如果仍然超出,进一步按行拆分\n if actual_height > CARD_HEIGHT - 100:\n lines = split_content.split('\\n')\n sub_contents = []\n sub_lines = []\n sub_height = 0\n \n for line in lines:\n test_lines = sub_lines + [line]\n test_html = generate_card_html('\\n'.join(test_lines), 1, 1, style_key)\n test_height = await measure_content_height(page, test_html)\n \n if test_height > CARD_HEIGHT - 100 and sub_lines:\n sub_contents.append('\\n'.join(sub_lines))\n sub_lines = [line]\n else:\n sub_lines = test_lines\n \n if sub_lines:\n sub_contents.append('\\n'.join(sub_lines))\n \n all_cards.extend(sub_contents)\n else:\n all_cards.append(split_content)\n \n finally:\n await browser.close()\n \n return all_cards\n\n\nasync def render_markdown_to_cards(md_file: str, output_dir: str, style_key: str = \"purple\"):\n \"\"\"主渲染函数:将 Markdown 文件渲染为多张卡片图片\"\"\"\n print(f\"\\n🎨 开始渲染: {md_file}\")\n print(f\"🎨 使用样式: {STYLES[style_key]['name']}\")\n \n # 确保输出目录存在\n os.makedirs(output_dir, exist_ok=True)\n \n # 解析 Markdown 文件\n data = parse_markdown_file(md_file)\n metadata = data['metadata']\n body = data['body']\n \n # 分割正文内容(基于用户手动分隔符)\n card_contents = split_content_by_separator(body)\n print(f\" 📄 检测到 {len(card_contents)} 个内容块\")\n \n # 处理内容,智能分页\n print(\" 🔍 分析内容高度并智能分页...\")\n processed_cards = await process_and_render_cards(card_contents, output_dir, style_key)\n total_cards = len(processed_cards)\n print(f\" 📄 将生成 {total_cards} 张卡片\")\n \n # 生成封面\n if metadata.get('emoji') or metadata.get('title'):\n print(\" 📷 生成封面...\")\n cover_html = generate_cover_html(metadata, style_key)\n cover_path = os.path.join(output_dir, 'cover.png')\n await render_html_to_image(cover_html, cover_path)\n \n # 生成正文卡片\n async with async_playwright() as p:\n browser = await p.chromium.launch()\n page = await browser.new_page(viewport={'width': CARD_WIDTH, 'height': CARD_HEIGHT})\n \n try:\n for i, content in enumerate(processed_cards, 1):\n print(f\" 📷 生成卡片 {i}/{total_cards}...\")\n card_html = generate_card_html(content, i, total_cards, style_key)\n card_path = os.path.join(output_dir, f'card_{i}.png')\n \n await page.set_content(card_html, wait_until='networkidle')\n await page.wait_for_timeout(300)\n \n await page.screenshot(\n path=card_path,\n clip={'x': 0, 'y': 0, 'width': CARD_WIDTH, 'height': CARD_HEIGHT},\n type='png'\n )\n print(f\" ✅ 已生成: {card_path}\")\n \n finally:\n await browser.close()\n \n print(f\"\\n✨ 渲染完成!共生成 {total_cards} 张卡片,保存到: {output_dir}\")\n return total_cards\n\n\ndef list_styles():\n \"\"\"列出所有可用样式\"\"\"\n print(\"\\n📋 可用样式列表:\")\n print(\"-\" * 40)\n for key, style in STYLES.items():\n print(f\" {key:12} - {style['name']}\")\n print(\"-\" * 40)\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description='将 Markdown 文件渲染为小红书风格的图片卡片(智能分页版)',\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog='''\n示例:\n python render_xhs_v2.py note.md\n python render_xhs_v2.py note.md -o ./output --style xiaohongshu\n python render_xhs_v2.py --list-styles\n '''\n )\n parser.add_argument(\n 'markdown_file',\n nargs='?',\n help='Markdown 文件路径'\n )\n parser.add_argument(\n '--output-dir', '-o',\n default=os.getcwd(),\n help='输出目录(默认为当前工作目录)'\n )\n parser.add_argument(\n '--style', '-s',\n default='purple',\n choices=list(STYLES.keys()),\n help='样式主题(默认: purple)'\n )\n parser.add_argument(\n '--list-styles',\n action='store_true',\n help='列出所有可用样式'\n )\n \n args = parser.parse_args()\n \n if args.list_styles:\n list_styles()\n return\n \n if not args.markdown_file:\n parser.print_help()\n sys.exit(1)\n \n if not os.path.exists(args.markdown_file):\n print(f\"❌ 错误: 文件不存在 - {args.markdown_file}\")\n sys.exit(1)\n \n asyncio.run(render_markdown_to_cards(args.markdown_file, args.output_dir, args.style))\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":26155,"content_sha256":"55d053cfc70e4c0ec51c6727739945483f37ad1cb21df25fa1c75fe33c4ef28b"},{"filename":"scripts/render_xhs.js","content":"#!/usr/bin/env node\n/**\n * 小红书卡片渲染脚本 - Node.js 增强版\n * 支持多种排版样式和智能分页策略\n * \n * 使用方法:\n * node render_xhs.js \u003cmarkdown_file> [options]\n * \n * 选项:\n * --output-dir, -o 输出目录(默认为当前工作目录)\n * --theme, -t 排版主题:default, playful-geometric, neo-brutalism, 等\n * --mode, -m 分页模式:separator, auto-fit, auto-split, dynamic\n * --width, -w 图片宽度(默认 1080)\n * --height, -h 图片高度(默认 1440)\n * --dpr 设备像素比(默认 2)\n * \n * 依赖安装:\n * npm install marked yaml playwright\n * npx playwright install chromium\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { marked } = require('marked');\nconst yaml = require('yaml');\nconst { chromium } = require('playwright');\n\n// 获取脚本所在目录\nconst SCRIPT_DIR = path.dirname(__dirname);\nconst ASSETS_DIR = path.join(SCRIPT_DIR, 'assets');\nconst THEMES_DIR = path.join(ASSETS_DIR, 'themes');\n\n// 默认卡片尺寸配置 (3:4 比例)\nconst DEFAULT_WIDTH = 1080;\nconst DEFAULT_HEIGHT = 1440;\nconst MAX_HEIGHT = 2160;\n\n// 可用主题列表\nconst AVAILABLE_THEMES = [\n 'default',\n 'playful-geometric',\n 'neo-brutalism',\n 'botanical',\n 'professional',\n 'retro',\n 'terminal',\n 'sketch'\n];\n\n// 分页模式\nconst PAGING_MODES = ['separator', 'auto-fit', 'auto-split', 'dynamic'];\n\n// 主题背景色\nconst THEME_BACKGROUNDS = {\n 'default': 'linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%)',\n 'playful-geometric': 'linear-gradient(135deg, #8B5CF6 0%, #F472B6 100%)',\n 'neo-brutalism': 'linear-gradient(135deg, #FF4757 0%, #FECA57 100%)',\n 'botanical': 'linear-gradient(135deg, #4A7C59 0%, #8FBC8F 100%)',\n 'professional': 'linear-gradient(135deg, #2563EB 0%, #3B82F6 100%)',\n 'retro': 'linear-gradient(135deg, #D35400 0%, #F39C12 100%)',\n 'terminal': 'linear-gradient(135deg, #0D1117 0%, #161B22 100%)',\n 'sketch': 'linear-gradient(135deg, #555555 0%, #888888 100%)'\n};\n\n// 封面标题文字渐变(随主题变化)\nconst THEME_TITLE_GRADIENTS = {\n 'default': 'linear-gradient(180deg, #111827 0%, #4B5563 100%)',\n 'playful-geometric': 'linear-gradient(180deg, #7C3AED 0%, #F472B6 100%)',\n 'neo-brutalism': 'linear-gradient(180deg, #000000 0%, #FF4757 100%)',\n 'botanical': 'linear-gradient(180deg, #1F2937 0%, #4A7C59 100%)',\n 'professional': 'linear-gradient(180deg, #1E3A8A 0%, #2563EB 100%)',\n 'retro': 'linear-gradient(180deg, #8B4513 0%, #D35400 100%)',\n 'terminal': 'linear-gradient(180deg, #39D353 0%, #58A6FF 100%)',\n 'sketch': 'linear-gradient(180deg, #111827 0%, #6B7280 100%)',\n};\n\n/**\n * 解析命令行参数\n */\nfunction parseArgs() {\n const args = process.argv.slice(2);\n const options = {\n markdownFile: null,\n outputDir: process.cwd(),\n theme: 'default',\n mode: 'separator',\n width: DEFAULT_WIDTH,\n height: DEFAULT_HEIGHT,\n maxHeight: MAX_HEIGHT,\n dpr: 2\n };\n\n for (let i = 0; i \u003c args.length; i++) {\n const arg = args[i];\n const nextArg = args[i + 1];\n\n switch (arg) {\n case '--output-dir':\n case '-o':\n options.outputDir = nextArg;\n i++;\n break;\n case '--theme':\n case '-t':\n options.theme = nextArg;\n i++;\n break;\n case '--mode':\n case '-m':\n options.mode = nextArg;\n i++;\n break;\n case '--width':\n case '-w':\n options.width = parseInt(nextArg);\n i++;\n break;\n case '--height':\n options.height = parseInt(nextArg);\n i++;\n break;\n case '--max-height':\n options.maxHeight = parseInt(nextArg);\n i++;\n break;\n case '--dpr':\n options.dpr = parseInt(nextArg);\n i++;\n break;\n case '--help':\n printHelp();\n process.exit(0);\n default:\n if (!arg.startsWith('-')) {\n options.markdownFile = arg;\n }\n }\n }\n\n return options;\n}\n\n/**\n * 打印帮助信息\n */\nfunction printHelp() {\n console.log(`\n小红书卡片渲染脚本 - Node.js 版本\n\n使用方法:\n node render_xhs.js \u003cmarkdown_file> [options]\n\n选项:\n --output-dir, -o 输出目录(默认为当前工作目录)\n --theme, -t 排版主题\n --mode, -m 分页模式\n --width, -w 图片宽度(默认 1080)\n --height 图片高度(默认 1440)\n --max-height 最大高度(默认 2160)\n --dpr 设备像素比(默认 2)\n\n可用主题: ${AVAILABLE_THEMES.join(', ')}\n分页模式: ${PAGING_MODES.join(', ')}\n`);\n}\n\n/**\n * 解析 Markdown 文件\n */\nfunction parseMarkdownFile(filePath) {\n const content = fs.readFileSync(filePath, 'utf-8');\n \n // 解析 YAML 头部\n const yamlMatch = content.match(/^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/);\n \n let metadata = {};\n let body = content;\n \n if (yamlMatch) {\n try {\n metadata = yaml.parse(yamlMatch[1]) || {};\n } catch (e) {\n metadata = {};\n }\n body = content.slice(yamlMatch[0].length);\n }\n \n return { metadata, body: body.trim() };\n}\n\n/**\n * 按分隔符拆分内容\n */\nfunction splitContentBySeparator(body) {\n const parts = body.split(/\\n---+\\n/);\n return parts.map(p => p.trim()).filter(p => p);\n}\n\n/**\n * 加载主题 CSS\n */\nfunction loadThemeCss(theme) {\n const themeFile = path.join(THEMES_DIR, `${theme}.css`);\n if (fs.existsSync(themeFile)) {\n return fs.readFileSync(themeFile, 'utf-8');\n }\n const defaultFile = path.join(THEMES_DIR, 'default.css');\n if (fs.existsSync(defaultFile)) {\n return fs.readFileSync(defaultFile, 'utf-8');\n }\n return '';\n}\n\n/**\n * 生成封面 HTML\n */\nfunction generateCoverHtml(metadata, theme, width, height) {\n const emoji = metadata.emoji || '📝';\n let title = metadata.title || '标题';\n let subtitle = metadata.subtitle || '';\n \n if (title.length > 15) title = title.slice(0, 15);\n if (subtitle.length > 15) subtitle = subtitle.slice(0, 15);\n \n const bg = THEME_BACKGROUNDS[theme] || THEME_BACKGROUNDS['default'];\n const titleBg = THEME_TITLE_GRADIENTS[theme] || THEME_TITLE_GRADIENTS['default'];\n \n return `\u003c!DOCTYPE html>\n\u003chtml lang=\"zh-CN\">\n\u003chead>\n \u003cmeta charset=\"UTF-8\">\n \u003cmeta name=\"viewport\" content=\"width=${width}, height=${height}\">\n \u003ctitle>小红书封面\u003c/title>\n \u003cstyle>\n @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');\n \n * { margin: 0; padding: 0; box-sizing: border-box; }\n \n body {\n font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;\n width: ${width}px;\n height: ${height}px;\n overflow: hidden;\n }\n \n .cover-container {\n width: ${width}px;\n height: ${height}px;\n background: ${bg};\n position: relative;\n overflow: hidden;\n }\n \n .cover-inner {\n position: absolute;\n width: ${Math.floor(width * 0.88)}px;\n height: ${Math.floor(height * 0.91)}px;\n left: ${Math.floor(width * 0.06)}px;\n top: ${Math.floor(height * 0.045)}px;\n background: #F3F3F3;\n border-radius: 25px;\n display: flex;\n flex-direction: column;\n padding: ${Math.floor(width * 0.074)}px ${Math.floor(width * 0.079)}px;\n }\n \n .cover-emoji {\n font-size: ${Math.floor(width * 0.167)}px;\n line-height: 1.2;\n margin-bottom: ${Math.floor(height * 0.035)}px;\n }\n \n .cover-title {\n font-weight: 900;\n font-size: ${Math.floor(width * 0.12)}px;\n line-height: 1.4;\n background: ${titleBg};\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n flex: 1;\n display: flex;\n align-items: flex-start;\n word-break: break-all;\n }\n \n .cover-subtitle {\n font-weight: 350;\n font-size: ${Math.floor(width * 0.067)}px;\n line-height: 1.4;\n color: #000000;\n margin-top: auto;\n }\n \u003c/style>\n\u003c/head>\n\u003cbody>\n \u003cdiv class=\"cover-container\">\n \u003cdiv class=\"cover-inner\">\n \u003cdiv class=\"cover-emoji\">${emoji}\u003c/div>\n \u003cdiv class=\"cover-title\">${title}\u003c/div>\n \u003cdiv class=\"cover-subtitle\">${subtitle}\u003c/div>\n \u003c/div>\n \u003c/div>\n\u003c/body>\n\u003c/html>`;\n}\n\n/**\n * 生成正文卡片 HTML\n */\nfunction generateCardHtml(content, theme, pageNumber, totalPages, width, height, mode) {\n const htmlContent = marked.parse(content);\n const themeCss = loadThemeCss(theme);\n const pageText = totalPages > 1 ? `${pageNumber}/${totalPages}` : '';\n const bg = THEME_BACKGROUNDS[theme] || THEME_BACKGROUNDS['default'];\n \n let containerStyle, innerStyle, contentStyle;\n \n if (mode === 'auto-fit') {\n containerStyle = `\n width: ${width}px;\n height: ${height}px;\n background: ${bg};\n position: relative;\n padding: 50px;\n overflow: hidden;\n `;\n innerStyle = `\n background: rgba(255, 255, 255, 0.95);\n border-radius: 20px;\n padding: 60px;\n height: calc(${height}px - 100px);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n `;\n contentStyle = 'flex: 1; overflow: hidden;';\n } else if (mode === 'dynamic') {\n containerStyle = `\n width: ${width}px;\n min-height: ${height}px;\n background: ${bg};\n position: relative;\n padding: 50px;\n `;\n innerStyle = `\n background: rgba(255, 255, 255, 0.95);\n border-radius: 20px;\n padding: 60px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n `;\n contentStyle = '';\n } else {\n containerStyle = `\n width: ${width}px;\n min-height: ${height}px;\n background: ${bg};\n position: relative;\n padding: 50px;\n overflow: hidden;\n `;\n innerStyle = `\n background: rgba(255, 255, 255, 0.95);\n border-radius: 20px;\n padding: 60px;\n min-height: calc(${height}px - 100px);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n `;\n contentStyle = '';\n }\n \n return `\u003c!DOCTYPE html>\n\u003chtml lang=\"zh-CN\">\n\u003chead>\n \u003cmeta charset=\"UTF-8\">\n \u003cmeta name=\"viewport\" content=\"width=${width}\">\n \u003ctitle>小红书卡片\u003c/title>\n \u003cstyle>\n @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');\n \n * { margin: 0; padding: 0; box-sizing: border-box; }\n \n body {\n font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;\n width: ${width}px;\n overflow: hidden;\n background: transparent;\n }\n \n .card-container { ${containerStyle} }\n .card-inner { ${innerStyle} }\n .card-content { line-height: 1.7; ${contentStyle} }\n /* auto-fit 用:对整个内容块做 transform 缩放 */\n .card-content-scale { transform-origin: top left; will-change: transform; }\n \n ${themeCss}\n \n .page-number {\n position: absolute;\n bottom: 80px;\n right: 80px;\n font-size: 36px;\n color: rgba(255, 255, 255, 0.8);\n font-weight: 500;\n }\n \u003c/style>\n\u003c/head>\n\u003cbody>\n \u003cdiv class=\"card-container\">\n \u003cdiv class=\"card-inner\">\n \u003cdiv class=\"card-content\">\n \u003cdiv class=\"card-content-scale\">\n ${htmlContent}\n \u003c/div>\n \u003c/div>\n \u003c/div>\n \u003cdiv class=\"page-number\">${pageText}\u003c/div>\n \u003c/div>\n\u003c/body>\n\u003c/html>`;\n}\n\n/**\n * 渲染 HTML 为图片\n */\nasync function renderHtmlToImage(htmlContent, outputPath, width, height, mode, maxHeight, dpr) {\n const browser = await chromium.launch();\n const viewportHeight = mode !== 'dynamic' ? height : maxHeight;\n const page = await browser.newPage({\n viewport: { width, height: viewportHeight },\n deviceScaleFactor: dpr\n });\n \n await page.setContent(htmlContent);\n await page.waitForLoadState('networkidle');\n await page.waitForTimeout(500);\n \n let actualHeight;\n \n if (mode === 'auto-fit') {\n await page.evaluate(() => {\n const viewportContent = document.querySelector('.card-content');\n const scaleEl = document.querySelector('.card-content-scale');\n if (!viewportContent || !scaleEl) return;\n\n // reset\n scaleEl.style.transform = 'none';\n scaleEl.style.width = '';\n scaleEl.style.height = '';\n\n const availableWidth = viewportContent.clientWidth;\n const availableHeight = viewportContent.clientHeight;\n\n const rect = scaleEl.getBoundingClientRect();\n const contentWidth = Math.max(scaleEl.scrollWidth, rect.width);\n const contentHeight = Math.max(scaleEl.scrollHeight, rect.height);\n\n if (!contentWidth || !contentHeight || !availableWidth || !availableHeight) return;\n\n const scale = Math.min(1, availableWidth / contentWidth, availableHeight / contentHeight);\n\n // expand layout box to avoid clip\n scaleEl.style.width = (availableWidth / scale) + 'px';\n\n scaleEl.style.transformOrigin = 'top left';\n scaleEl.style.transform = `translate(0px, 0px) scale(${scale})`;\n });\n await page.waitForTimeout(100);\n actualHeight = height;\n } else if (mode === 'dynamic') {\n const contentHeight = await page.evaluate(() => {\n const container = document.querySelector('.card-container');\n return container ? container.scrollHeight : document.body.scrollHeight;\n });\n actualHeight = Math.max(height, Math.min(contentHeight, maxHeight));\n } else {\n const contentHeight = await page.evaluate(() => {\n const container = document.querySelector('.card-container');\n return container ? container.scrollHeight : document.body.scrollHeight;\n });\n actualHeight = Math.max(height, contentHeight);\n }\n \n await page.screenshot({\n path: outputPath,\n clip: { x: 0, y: 0, width, height: actualHeight },\n type: 'png'\n });\n \n await browser.close();\n console.log(` ✅ 已生成: ${outputPath} (${width}x${actualHeight})`);\n return actualHeight;\n}\n\n/**\n * 主渲染函数\n */\nasync function renderMarkdownToCards(options) {\n const { markdownFile, outputDir, theme, mode, width, height, maxHeight, dpr } = options;\n \n console.log(`\\n🎨 开始渲染: ${markdownFile}`);\n console.log(` 📐 主题: ${theme}`);\n console.log(` 📏 模式: ${mode}`);\n console.log(` 📐 尺寸: ${width}x${height}`);\n \n // 确保输出目录存在\n if (!fs.existsSync(outputDir)) {\n fs.mkdirSync(outputDir, { recursive: true });\n }\n \n // 解析 Markdown 文件\n const { metadata, body } = parseMarkdownFile(markdownFile);\n \n // 分割内容\n const cardContents = splitContentBySeparator(body);\n const totalCards = cardContents.length;\n \n console.log(` 📄 检测到 ${totalCards} 张正文卡片`);\n \n // 生成封面\n if (metadata.emoji || metadata.title) {\n console.log(' 📷 生成封面...');\n const coverHtml = generateCoverHtml(metadata, theme, width, height);\n const coverPath = path.join(outputDir, 'cover.png');\n await renderHtmlToImage(coverHtml, coverPath, width, height, 'separator', maxHeight, dpr);\n }\n \n // 生成正文卡片\n for (let i = 0; i \u003c cardContents.length; i++) {\n const content = cardContents[i];\n console.log(` 📷 生成卡片 ${i + 1}/${totalCards}...`);\n const cardHtml = generateCardHtml(content, theme, i + 1, totalCards, width, height, mode);\n const cardPath = path.join(outputDir, `card_${i + 1}.png`);\n await renderHtmlToImage(cardHtml, cardPath, width, height, mode, maxHeight, dpr);\n }\n \n console.log(`\\n✨ 渲染完成!图片已保存到: ${outputDir}`);\n}\n\n/**\n * 主函数\n */\nasync function main() {\n const options = parseArgs();\n \n if (!options.markdownFile) {\n console.error('❌ 错误: 请提供 Markdown 文件路径');\n printHelp();\n process.exit(1);\n }\n \n if (!fs.existsSync(options.markdownFile)) {\n console.error(`❌ 错误: 文件不存在 - ${options.markdownFile}`);\n process.exit(1);\n }\n \n if (!AVAILABLE_THEMES.includes(options.theme)) {\n console.error(`❌ 错误: 不支持的主题 - ${options.theme}`);\n console.error(`可用主题: ${AVAILABLE_THEMES.join(', ')}`);\n process.exit(1);\n }\n \n if (!PAGING_MODES.includes(options.mode)) {\n console.error(`❌ 错误: 不支持的分页模式 - ${options.mode}`);\n console.error(`可用模式: ${PAGING_MODES.join(', ')}`);\n process.exit(1);\n }\n \n await renderMarkdownToCards(options);\n}\n\nmain().catch(console.error);\n","content_type":"text/javascript; charset=utf-8","language":"javascript","size":18291,"content_sha256":"250297dade6020daee32a14a23253a25819d57b5258b76ce37f3915d92de5a6e"},{"filename":"scripts/render_xhs.py","content":"#!/usr/bin/env python3\n\"\"\"\n小红书卡片渲染脚本 - 增强版\n支持多种排版样式和智能分页策略\n\n使用方法:\n python render_xhs.py \u003cmarkdown_file> [options]\n\n选项:\n --output-dir, -o 输出目录(默认为当前工作目录)\n --theme, -t 排版主题:default, playful-geometric, neo-brutalism, \n botanical, professional, retro, terminal, sketch\n --mode, -m 分页模式:\n - separator : 按 --- 分隔符手动分页(默认)\n - auto-fit : 自动缩放文字以填满固定尺寸\n - auto-split : 根据内容高度自动切分\n - dynamic : 根据内容动态调整图片高度\n --width, -w 图片宽度(默认 1080)\n --height, -h 图片高度(默认 1440,dynamic 模式下为最小高度)\n --max-height dynamic 模式下的最大高度(默认 4320\n --dpr 设备像素比(默认 2)\n\n依赖安装:\n pip install markdown pyyaml playwright\n playwright install chromium\n\"\"\"\n\nimport argparse\nimport asyncio\nimport os\nimport re\nimport sys\nimport tempfile\nfrom pathlib import Path\nfrom typing import List, Dict, Any, Optional\n\ntry:\n import markdown\n import yaml\n from playwright.async_api import async_playwright\nexcept ImportError as e:\n print(f\"缺少依赖: {e}\")\n print(\"请运行: pip install markdown pyyaml playwright && playwright install chromium\")\n sys.exit(1)\n\n\n# 获取脚本所在目录\nSCRIPT_DIR = Path(__file__).parent.parent\nASSETS_DIR = SCRIPT_DIR / \"assets\"\nTHEMES_DIR = ASSETS_DIR / \"themes\"\n\n# 默认卡片尺寸配置 (3:4 比例)\nDEFAULT_WIDTH = 1080\nDEFAULT_HEIGHT = 1440\nMAX_HEIGHT = 4320 # dynamic 模式最大高度\n\n# 可用主题列表\nAVAILABLE_THEMES = [\n 'default',\n 'playful-geometric',\n 'neo-brutalism',\n 'botanical',\n 'professional',\n 'retro',\n 'terminal',\n 'sketch'\n]\n\n# 分页模式\nPAGING_MODES = ['separator', 'auto-fit', 'auto-split', 'dynamic']\n\n\ndef parse_markdown_file(file_path: str) -> dict:\n \"\"\"解析 Markdown 文件,提取 YAML 头部和正文内容\"\"\"\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n \n # 解析 YAML 头部\n yaml_pattern = r'^---\\s*\\n(.*?)\\n---\\s*\\n'\n yaml_match = re.match(yaml_pattern, content, re.DOTALL)\n \n metadata = {}\n body = content\n \n if yaml_match:\n try:\n metadata = yaml.safe_load(yaml_match.group(1)) or {}\n except yaml.YAMLError:\n metadata = {}\n body = content[yaml_match.end():]\n \n return {\n 'metadata': metadata,\n 'body': body.strip()\n }\n\n\ndef split_content_by_separator(body: str) -> List[str]:\n \"\"\"按照 --- 分隔符拆分正文为多张卡片内容\"\"\"\n parts = re.split(r'\\n---+\\n', body)\n return [part.strip() for part in parts if part.strip()]\n\n\ndef convert_markdown_to_html(md_content: str) -> str:\n \"\"\"将 Markdown 转换为 HTML\"\"\"\n # 处理 tags(以 # 开头的标签)\n tags_pattern = r'((?:#[\\w\\u4e00-\\u9fa5]+\\s*)+)

小红书笔记创作技能 根据用户提供的资料或需求,创作小红书笔记内容、生成精美图片卡片,并可选择发布到小红书。 详细参数文档见 --- 工作流程 第一步:撰写小红书笔记内容 根据用户需求和资料,创作符合小红书风格的内容: 标题 :不超过 20 字,吸引眼球,可用数字/疑问句/感叹号增强吸引力。 正文 :段落清晰,点缀少量 Emoji(每段 1-2 个),短句短段,结尾附 5-10 个 SEO 标签。 --- 第二步:生成渲染用 Markdown 文档 注意:此 Markdown 专为图片渲染设计,禁止直接使用上一步的笔记正文。 文档结构: 分页策略选择: - 内容需精确切分 → 用 手动分隔,配合 - 内容长短不稳定 → 生成普通 Markdown,使用 --- 第三步:渲染图片卡片 默认主题 : (手绘素描风格) 默认分页 : (按 分隔) 常用示例: 生成结果: (封面)+ 、 ...(正文卡片) 可用主题 ( ): 、 、 、 、 、 、 、 分页模式 ( ): 、 、 、 完整参数说明见 --- 第四步:发布小红书笔记(可选) 前置条件 :安装 Python 与 Node.js 依赖,并配置 中的 。Cookie 必须来自已登录的 (详见 )。 默认以「仅自己可见」发布 ,加 参数才会公开。 发布链路已内置在当前技能中,不依赖外部 API 服务或其他项目目录。 --- 技能资…

\n tags_match = re.search(tags_pattern, md_content, re.MULTILINE)\n tags_html = \"\"\n \n if tags_match:\n tags_str = tags_match.group(1)\n md_content = md_content[:tags_match.start()].strip()\n tags = re.findall(r'#([\\w\\u4e00-\\u9fa5]+)', tags_str)\n if tags:\n tags_html = '\u003cdiv class=\"tags-container\">'\n for tag in tags:\n tags_html += f'\u003cspan class=\"tag\">#{tag}\u003c/span>'\n tags_html += '\u003c/div>'\n \n # 转换 Markdown 为 HTML\n html = markdown.markdown(\n md_content,\n extensions=['extra', 'codehilite', 'tables', 'nl2br']\n )\n \n return html + tags_html\n\n\ndef load_theme_css(theme: str) -> str:\n \"\"\"加载主题 CSS 样式\"\"\"\n theme_file = THEMES_DIR / f\"{theme}.css\"\n if theme_file.exists():\n with open(theme_file, 'r', encoding='utf-8') as f:\n return f.read()\n else:\n # 如果主题不存在,使用默认主题\n default_file = THEMES_DIR / \"default.css\"\n if default_file.exists():\n with open(default_file, 'r', encoding='utf-8') as f:\n return f.read()\n return \"\"\n\n\ndef generate_cover_html(metadata: dict, theme: str, width: int, height: int) -> str:\n \"\"\"生成封面 HTML\"\"\"\n emoji = metadata.get('emoji', '📝')\n title = metadata.get('title', '标题')\n subtitle = metadata.get('subtitle', '')\n \n \n # 动态调整标题字体大小\n title_len = len(title)\n if title_len \u003c= 6:\n title_size = int(width * 0.14) # 极大\n elif title_len \u003c= 10:\n title_size = int(width * 0.12) # 大\n elif title_len \u003c= 18:\n title_size = int(width * 0.09) # 中\n elif title_len \u003c= 30:\n title_size = int(width * 0.07) # 小\n else:\n title_size = int(width * 0.055) # 极小\n\n # 获取主题背景色\n theme_backgrounds = {\n 'default': 'linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%)',\n 'playful-geometric': 'linear-gradient(180deg, #8B5CF6 0%, #F472B6 100%)',\n 'neo-brutalism': 'linear-gradient(180deg, #FF4757 0%, #FECA57 100%)',\n 'botanical': 'linear-gradient(180deg, #4A7C59 0%, #8FBC8F 100%)',\n 'professional': 'linear-gradient(180deg, #2563EB 0%, #3B82F6 100%)',\n 'retro': 'linear-gradient(180deg, #D35400 0%, #F39C12 100%)',\n 'terminal': 'linear-gradient(180deg, #0D1117 0%, #21262D 100%)',\n 'sketch': 'linear-gradient(180deg, #555555 0%, #999999 100%)'\n }\n bg = theme_backgrounds.get(theme, theme_backgrounds['default'])\n\n # 封面标题文字渐变随主题变化\n title_gradients = {\n 'default': 'linear-gradient(180deg, #111827 0%, #4B5563 100%)',\n 'playful-geometric': 'linear-gradient(180deg, #7C3AED 0%, #F472B6 100%)',\n 'neo-brutalism': 'linear-gradient(180deg, #000000 0%, #FF4757 100%)',\n 'botanical': 'linear-gradient(180deg, #1F2937 0%, #4A7C59 100%)',\n 'professional': 'linear-gradient(180deg, #1E3A8A 0%, #2563EB 100%)',\n 'retro': 'linear-gradient(180deg, #8B4513 0%, #D35400 100%)',\n 'terminal': 'linear-gradient(180deg, #39D353 0%, #58A6FF 100%)',\n 'sketch': 'linear-gradient(180deg, #111827 0%, #6B7280 100%)',\n }\n title_bg = title_gradients.get(theme, title_gradients['default'])\n \n html = f'''\u003c!DOCTYPE html>\n\u003chtml lang=\"zh-CN\">\n\u003chead>\n \u003cmeta charset=\"UTF-8\">\n \u003cmeta name=\"viewport\" content=\"width={width}, height={height}\">\n \u003ctitle>小红书封面\u003c/title>\n \u003cstyle>\n @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');\n \n * {{\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }}\n \n body {{\n font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;\n width: {width}px;\n height: {height}px;\n overflow: hidden;\n }}\n \n .cover-container {{\n width: {width}px;\n height: {height}px;\n background: {bg};\n position: relative;\n overflow: hidden;\n }}\n \n .cover-inner {{\n position: absolute;\n width: {int(width * 0.88)}px;\n height: {int(height * 0.91)}px;\n left: {int(width * 0.06)}px;\n top: {int(height * 0.045)}px;\n background: #F3F3F3;\n border-radius: 25px;\n display: flex;\n flex-direction: column;\n padding: {int(width * 0.074)}px {int(width * 0.079)}px;\n }}\n \n .cover-emoji {{\n font-size: {int(width * 0.167)}px;\n line-height: 1.2;\n margin-bottom: {int(height * 0.035)}px;\n }}\n \n .cover-title {{\n font-weight: 900;\n font-size: {title_size}px;\n line-height: 1.4;\n background: {title_bg};\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n background-clip: text;\n flex: 1;\n display: flex;\n align-items: flex-start;\n word-break: break-all;\n }}\n \n .cover-subtitle {{\n font-weight: 350;\n font-size: {int(width * 0.067)}px;\n line-height: 1.4;\n color: #000000;\n margin-top: auto;\n }}\n \u003c/style>\n\u003c/head>\n\u003cbody>\n \u003cdiv class=\"cover-container\">\n \u003cdiv class=\"cover-inner\">\n \u003cdiv class=\"cover-emoji\">{emoji}\u003c/div>\n \u003cdiv class=\"cover-title\">{title}\u003c/div>\n \u003cdiv class=\"cover-subtitle\">{subtitle}\u003c/div>\n \u003c/div>\n \u003c/div>\n\u003c/body>\n\u003c/html>'''\n return html\n\n\ndef generate_card_html(content: str, theme: str, page_number: int = 1, \n total_pages: int = 1, width: int = DEFAULT_WIDTH, \n height: int = DEFAULT_HEIGHT, mode: str = 'separator') -> str:\n \"\"\"生成正文卡片 HTML\"\"\"\n \n html_content = convert_markdown_to_html(content)\n theme_css = load_theme_css(theme)\n \n page_text = f\"{page_number}/{total_pages}\" if total_pages > 1 else \"\"\n \n # 获取主题背景色\n theme_backgrounds = {\n 'default': 'linear-gradient(180deg, #f3f3f3 0%, #f9f9f9 100%)',\n 'playful-geometric': 'linear-gradient(135deg, #8B5CF6 0%, #F472B6 100%)',\n 'neo-brutalism': 'linear-gradient(135deg, #FF4757 0%, #FECA57 100%)',\n 'botanical': 'linear-gradient(135deg, #4A7C59 0%, #8FBC8F 100%)',\n 'professional': 'linear-gradient(135deg, #2563EB 0%, #3B82F6 100%)',\n 'retro': 'linear-gradient(135deg, #D35400 0%, #F39C12 100%)',\n 'terminal': 'linear-gradient(135deg, #0D1117 0%, #161B22 100%)',\n 'sketch': 'linear-gradient(135deg, #555555 0%, #888888 100%)'\n }\n bg = theme_backgrounds.get(theme, theme_backgrounds['default'])\n \n # 根据模式设置不同的容器样式\n if mode == 'auto-fit':\n container_style = f'''\n width: {width}px;\n height: {height}px;\n background: {bg};\n position: relative;\n padding: 50px;\n overflow: hidden;\n '''\n inner_style = f'''\n background: rgba(255, 255, 255, 0.95);\n border-radius: 20px;\n padding: 60px;\n height: calc({height}px - 100px);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n overflow: hidden;\n display: flex;\n flex-direction: column;\n '''\n content_style = '''\n flex: 1;\n overflow: hidden;\n '''\n elif mode == 'dynamic':\n container_style = f'''\n width: {width}px;\n min-height: {height}px;\n background: {bg};\n position: relative;\n padding: 50px;\n '''\n inner_style = '''\n background: rgba(255, 255, 255, 0.95);\n border-radius: 20px;\n padding: 60px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n '''\n content_style = ''\n else: # separator 和 auto-split\n container_style = f'''\n width: {width}px;\n min-height: {height}px;\n background: {bg};\n position: relative;\n padding: 50px;\n overflow: hidden;\n '''\n inner_style = f'''\n background: rgba(255, 255, 255, 0.95);\n border-radius: 20px;\n padding: 60px;\n min-height: calc({height}px - 100px);\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(10px);\n '''\n content_style = ''\n \n html = f'''\u003c!DOCTYPE html>\n\u003chtml lang=\"zh-CN\">\n\u003chead>\n \u003cmeta charset=\"UTF-8\">\n \u003cmeta name=\"viewport\" content=\"width={width}\">\n \u003ctitle>小红书卡片\u003c/title>\n \u003cstyle>\n @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');\n \n * {{\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }}\n \n body {{\n font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;\n width: {width}px;\n overflow: hidden;\n background: transparent;\n }}\n \n .card-container {{\n {container_style}\n }}\n \n .card-inner {{\n {inner_style}\n }}\n \n .card-content {{\n line-height: 1.7;\n {content_style}\n }}\n\n /* auto-fit 用:对整个内容块做 transform 缩放 */\n .card-content-scale {{\n transform-origin: top left;\n will-change: transform;\n }}\n \n {theme_css}\n\n .card-content :not(pre) > code {{\n overflow-wrap: anywhere;\n word-break: break-word;\n }}\n\n .page-number {{\n position: absolute;\n bottom: 80px;\n right: 80px;\n font-size: 36px;\n color: rgba(255, 255, 255, 0.8);\n font-weight: 500;\n }}\n \u003c/style>\n\u003c/head>\n\u003cbody>\n \u003cdiv class=\"card-container\">\n \u003cdiv class=\"card-inner\">\n \u003cdiv class=\"card-content\">\n \u003cdiv class=\"card-content-scale\">{html_content}\u003c/div>\n \u003c/div>\n \u003c/div>\n \u003cdiv class=\"page-number\">{page_text}\u003c/div>\n \u003c/div>\n\u003c/body>\n\u003c/html>'''\n return html\n\n\nasync def render_html_to_image(html_content: str, output_path: str, \n width: int = DEFAULT_WIDTH, \n height: int = DEFAULT_HEIGHT,\n mode: str = 'separator',\n max_height: int = MAX_HEIGHT,\n dpr: int = 2):\n \"\"\"使用 Playwright 将 HTML 渲染为图片\"\"\"\n async with async_playwright() as p:\n browser = await p.chromium.launch()\n \n # 设置视口大小\n viewport_height = height if mode != 'dynamic' else max_height\n page = await browser.new_page(\n viewport={'width': width, 'height': viewport_height},\n device_scale_factor=dpr\n )\n \n # 创建临时 HTML 文件\n with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:\n f.write(html_content)\n temp_html_path = f.name\n \n try:\n await page.goto(f'file://{temp_html_path}')\n await page.wait_for_load_state('networkidle')\n \n # 等待字体加载\n await page.wait_for_timeout(500)\n \n if mode == 'auto-fit':\n # 自动缩放模式:对整个内容块做 transform 缩放(标题/代码块等固定 px 也会一起缩放)\n await page.evaluate('''() => {\n const viewportContent = document.querySelector('.card-content');\n const scaleEl = document.querySelector('.card-content-scale');\n if (!viewportContent || !scaleEl) return;\n\n // 先重置,测量原始尺寸\n scaleEl.style.transform = 'none';\n scaleEl.style.width = '';\n scaleEl.style.height = '';\n\n const availableWidth = viewportContent.clientWidth;\n const availableHeight = viewportContent.clientHeight;\n\n // scrollWidth/scrollHeight 反映内容的自然尺寸\n const contentWidth = Math.max(scaleEl.scrollWidth, scaleEl.getBoundingClientRect().width);\n const contentHeight = Math.max(scaleEl.scrollHeight, scaleEl.getBoundingClientRect().height);\n\n if (!contentWidth || !contentHeight || !availableWidth || !availableHeight) return;\n\n // 只缩小不放大,避免“撑太大”\n const scale = Math.min(1, availableWidth / contentWidth, availableHeight / contentHeight);\n\n // 为避免 transform 后布局尺寸不匹配导致裁切,扩大布局盒子\n scaleEl.style.width = (availableWidth / scale) + 'px';\n\n // 顶部对齐更稳;如需居中可计算 offset\n const offsetX = 0;\n const offsetY = 0;\n\n scaleEl.style.transformOrigin = 'top left';\n scaleEl.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;\n }''')\n await page.wait_for_timeout(100)\n actual_height = height\n \n elif mode == 'dynamic':\n # 动态高度模式:根据内容调整图片高度\n content_height = await page.evaluate('''() => {\n const container = document.querySelector('.card-container');\n return container ? container.scrollHeight : document.body.scrollHeight;\n }''')\n # 确保高度在合理范围内\n actual_height = max(height, min(content_height, max_height))\n \n else: # separator 和 auto-split\n # 获取实际内容高度\n content_height = await page.evaluate('''() => {\n const container = document.querySelector('.card-container');\n return container ? container.scrollHeight : document.body.scrollHeight;\n }''')\n actual_height = max(height, content_height)\n \n # 截图\n await page.screenshot(\n path=output_path,\n clip={'x': 0, 'y': 0, 'width': width, 'height': actual_height},\n type='png'\n )\n \n print(f\" ✅ 已生成: {output_path} ({width}x{actual_height})\")\n return actual_height\n \n finally:\n os.unlink(temp_html_path)\n await browser.close()\n\n\nasync def auto_split_content(body: str, theme: str, width: int, height: int, \n dpr: int = 2) -> List[str]:\n \"\"\"自动切分内容:根据渲染后的高度自动分页\"\"\"\n \n # 将内容按段落分割\n paragraphs = re.split(r'\\n\\n+', body)\n \n cards = []\n current_content = []\n \n async with async_playwright() as p:\n browser = await p.chromium.launch()\n page = await browser.new_page(\n viewport={'width': width, 'height': height * 2},\n device_scale_factor=dpr\n )\n \n try:\n for para in paragraphs:\n # 尝试将当前段落加入\n test_content = current_content + [para]\n test_md = '\\n\\n'.join(test_content)\n \n html = generate_card_html(test_md, theme, 1, 1, width, height, 'auto-split')\n \n with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:\n f.write(html)\n temp_path = f.name\n \n await page.goto(f'file://{temp_path}')\n await page.wait_for_load_state('networkidle')\n await page.wait_for_timeout(200)\n \n content_height = await page.evaluate('''() => {\n const content = document.querySelector('.card-content');\n return content ? content.scrollHeight : 0;\n }''')\n \n os.unlink(temp_path)\n \n # 内容区域的可用高度(去除 padding 等)\n available_height = height - 220 # 50*2 padding + 60*2 inner padding\n\n if content_height > available_height and current_content:\n # 当前卡片已满,保存并开始新卡片\n cards.append('\\n\\n'.join(current_content))\n current_content = [para]\n else:\n current_content = test_content\n \n # 保存最后一张卡片\n if current_content:\n cards.append('\\n\\n'.join(current_content))\n \n finally:\n await browser.close()\n \n return cards\n\n\nasync def render_markdown_to_cards(md_file: str, output_dir: str, \n theme: str = 'default',\n mode: str = 'separator',\n width: int = DEFAULT_WIDTH,\n height: int = DEFAULT_HEIGHT,\n max_height: int = MAX_HEIGHT,\n dpr: int = 2):\n \"\"\"主渲染函数:将 Markdown 文件渲染为多张卡片图片\"\"\"\n print(f\"\\n🎨 开始渲染: {md_file}\")\n print(f\" 📐 主题: {theme}\")\n print(f\" 📏 模式: {mode}\")\n print(f\" 📐 尺寸: {width}x{height}\")\n \n # 确保输出目录存在\n os.makedirs(output_dir, exist_ok=True)\n \n # 解析 Markdown 文件\n data = parse_markdown_file(md_file)\n metadata = data['metadata']\n body = data['body']\n \n # 根据模式处理内容分割\n if mode == 'auto-split':\n print(\" ⏳ 自动分析内容并切分...\")\n card_contents = await auto_split_content(body, theme, width, height, dpr)\n else:\n card_contents = split_content_by_separator(body)\n \n total_cards = len(card_contents)\n print(f\" 📄 检测到 {total_cards} 张正文卡片\")\n \n # 生成封面\n if metadata.get('emoji') or metadata.get('title'):\n print(\" 📷 生成封面...\")\n cover_html = generate_cover_html(metadata, theme, width, height)\n cover_path = os.path.join(output_dir, 'cover.png')\n await render_html_to_image(cover_html, cover_path, width, height, 'separator', max_height, dpr)\n \n # 生成正文卡片\n for i, content in enumerate(card_contents, 1):\n print(f\" 📷 生成卡片 {i}/{total_cards}...\")\n card_html = generate_card_html(content, theme, i, total_cards, width, height, mode)\n card_path = os.path.join(output_dir, f'card_{i}.png')\n await render_html_to_image(card_html, card_path, width, height, mode, max_height, dpr)\n \n print(f\"\\n✨ 渲染完成!图片已保存到: {output_dir}\")\n return total_cards\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description='将 Markdown 文件渲染为小红书风格的图片卡片(支持多种样式和分页模式)',\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog='''\n可用主题:\n default - 默认紫色渐变风格\n playful-geometric - 活泼几何风格(Memphis 设计)\n neo-brutalism - 新粗野主义风格\n botanical - 植物园自然风格\n professional - 专业商务风格\n retro - 复古怀旧风格\n terminal - 终端/命令行风格\n sketch - 手绘素描风格\n\n分页模式:\n separator - 按 --- 分隔符手动分页(默认)\n auto-fit - 自动缩放文字以填满固定尺寸\n auto-split - 根据内容高度自动切分\n dynamic - 根据内容动态调整图片高度\n'''\n )\n parser.add_argument(\n 'markdown_file',\n help='Markdown 文件路径'\n )\n parser.add_argument(\n '--output-dir', '-o',\n default=os.getcwd(),\n help='输出目录(默认为当前工作目录)'\n )\n parser.add_argument(\n '--theme', '-t',\n choices=AVAILABLE_THEMES,\n default='sketch',\n help='排版主题(默认: sketch)'\n )\n parser.add_argument(\n '--mode', '-m',\n choices=PAGING_MODES,\n default='separator',\n help='分页模式(默认: separator)'\n )\n parser.add_argument(\n '--width', '-w',\n type=int,\n default=DEFAULT_WIDTH,\n help=f'图片宽度(默认: {DEFAULT_WIDTH})'\n )\n parser.add_argument(\n '--height',\n type=int,\n default=DEFAULT_HEIGHT,\n help=f'图片高度(默认: {DEFAULT_HEIGHT})'\n )\n parser.add_argument(\n '--max-height',\n type=int,\n default=MAX_HEIGHT,\n help=f'dynamic 模式下的最大高度(默认: {MAX_HEIGHT})'\n )\n parser.add_argument(\n '--dpr',\n type=int,\n default=2,\n help='设备像素比(默认: 2)'\n )\n \n args = parser.parse_args()\n \n if not os.path.exists(args.markdown_file):\n print(f\"❌ 错误: 文件不存在 - {args.markdown_file}\")\n sys.exit(1)\n \n asyncio.run(render_markdown_to_cards(\n args.markdown_file,\n args.output_dir,\n theme=args.theme,\n mode=args.mode,\n width=args.width,\n height=args.height,\n max_height=args.max_height,\n dpr=args.dpr\n ))\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":25277,"content_sha256":"f613680e2a6430b3bdbe18fbd5c5d36a1e7825a4122fcdae594b0bdf78ca72b6"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"小红书笔记创作技能","type":"text"}]},{"type":"paragraph","content":[{"text":"根据用户提供的资料或需求,创作小红书笔记内容、生成精美图片卡片,并可选择发布到小红书。","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"详细参数文档见 ","type":"text"},{"text":"references/params.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"工作流程","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"第一步:撰写小红书笔记内容","type":"text"}]},{"type":"paragraph","content":[{"text":"根据用户需求和资料,创作符合小红书风格的内容:","type":"text"}]},{"type":"paragraph","content":[{"text":"标题","type":"text","marks":[{"type":"strong"}]},{"text":":不超过 20 字,吸引眼球,可用数字/疑问句/感叹号增强吸引力。","type":"text"}]},{"type":"paragraph","content":[{"text":"正文","type":"text","marks":[{"type":"strong"}]},{"text":":段落清晰,点缀少量 Emoji(每段 1-2 个),短句短段,结尾附 5-10 个 SEO 标签。","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"第二步:生成渲染用 Markdown 文档","type":"text"}]},{"type":"paragraph","content":[{"text":"注意:此 Markdown 专为图片渲染设计,禁止直接使用上一步的笔记正文。","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"文档结构:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"markdown"},"content":[{"text":"---\nemoji: \"🚀\"\ntitle: \"封面大标题(≤15字)\"\nsubtitle: \"封面副标题(≤15字)\"\n---\n\n# 正文内容...\n\n---\n\n# 第二张卡片内容...(使用 --- 手动分隔时)","type":"text"}]},{"type":"paragraph","content":[{"text":"分页策略选择:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"内容需精确切分 → 用 ","type":"text"},{"text":"---","type":"text","marks":[{"type":"code_inline"}]},{"text":" 手动分隔,配合 ","type":"text"},{"text":"-m separator","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"内容长短不稳定 → 生成普通 Markdown,使用 ","type":"text"},{"text":"-m auto-split","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"第三步:渲染图片卡片","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python scripts/render_xhs.py \u003cmarkdown_file> [options]","type":"text"}]},{"type":"paragraph","content":[{"text":"默认主题","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"},{"text":"sketch","type":"text","marks":[{"type":"code_inline"}]},{"text":"(手绘素描风格)","type":"text"},{"type":"br"},{"text":"默认分页","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"},{"text":"separator","type":"text","marks":[{"type":"code_inline"}]},{"text":"(按 ","type":"text"},{"text":"---","type":"text","marks":[{"type":"code_inline"}]},{"text":" 分隔)","type":"text"}]},{"type":"paragraph","content":[{"text":"常用示例:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 默认(sketch 主题 + 手动分页)\npython scripts/render_xhs.py content.md\n\n# 自动分页(推荐内容长短不定时)\npython scripts/render_xhs.py content.md -m auto-split\n\n# 切换主题\npython scripts/render_xhs.py content.md -t playful-geometric -m auto-split\n\n# 固定尺寸自动缩放\npython scripts/render_xhs.py content.md -m auto-fit","type":"text"}]},{"type":"paragraph","content":[{"text":"生成结果:","type":"text"},{"text":"cover.png","type":"text","marks":[{"type":"code_inline"}]},{"text":"(封面)+ ","type":"text"},{"text":"card_1.png","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"card_2.png","type":"text","marks":[{"type":"code_inline"}]},{"text":"...(正文卡片)","type":"text"}]},{"type":"paragraph","content":[{"text":"可用主题","type":"text","marks":[{"type":"strong"}]},{"text":"(","type":"text"},{"text":"-t","type":"text","marks":[{"type":"code_inline"}]},{"text":"):","type":"text"},{"text":"sketch","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"default","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"playful-geometric","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"neo-brutalism","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"botanical","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"professional","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"retro","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"terminal","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"分页模式","type":"text","marks":[{"type":"strong"}]},{"text":"(","type":"text"},{"text":"-m","type":"text","marks":[{"type":"code_inline"}]},{"text":"):","type":"text"},{"text":"separator","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"auto-fit","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"auto-split","type":"text","marks":[{"type":"code_inline"}]},{"text":"、","type":"text"},{"text":"dynamic","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"完整参数说明见 ","type":"text"},{"text":"references/params.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"第四步:发布小红书笔记(可选)","type":"text"}]},{"type":"paragraph","content":[{"text":"前置条件","type":"text","marks":[{"type":"strong"}]},{"text":":安装 Python 与 Node.js 依赖,并配置 ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" 中的 ","type":"text"},{"text":"XHS_CREATOR_COOKIE","type":"text","marks":[{"type":"code_inline"}]},{"text":"。Cookie 必须来自已登录的 ","type":"text"},{"text":"https://creator.xiaohongshu.com","type":"text","marks":[{"type":"code_inline"}]},{"text":"(详见 ","type":"text"},{"text":"references/params.md","type":"text","marks":[{"type":"code_inline"}]},{"text":")。","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 默认仅自己可见(推荐先预览确认)\npython scripts/publish_xhs.py --title \"笔记标题\" --desc \"笔记描述\" \\\n --images cover.png card_1.png card_2.png\n\n# 确认无误后公开发布\npython scripts/publish_xhs.py --title \"笔记标题\" --desc \"笔记描述\" \\\n --images cover.png card_1.png card_2.png --public","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"默认以「仅自己可见」发布","type":"text","marks":[{"type":"strong"}]},{"text":",加 ","type":"text"},{"text":"--public","type":"text","marks":[{"type":"code_inline"}]},{"text":" 参数才会公开。 发布链路已内置在当前技能中,不依赖外部 API 服务或其他项目目录。","type":"text"}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"技能资源","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"脚本","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/render_xhs.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" — 渲染脚本(主推,8 主题 + 4 分页模式)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/render_xhs_v2.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" — 渲染脚本 V2(备用,7 种渐变色彩风格)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"scripts/publish_xhs.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" — 发布脚本","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"模板与样式","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/cover.html","type":"text","marks":[{"type":"code_inline"}]},{"text":" — 封面 HTML 模板","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/card.html","type":"text","marks":[{"type":"code_inline"}]},{"text":" — 正文卡片 HTML 模板","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/styles.css","type":"text","marks":[{"type":"code_inline"}]},{"text":" — 公共容器样式","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"assets/themes/","type":"text","marks":[{"type":"code_inline"}]},{"text":" — 各主题 CSS 文件","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"参考文档","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/params.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" — 完整参数参考(主题/模式/发布参数)","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"xhs-note-creator","author":"@skillopedia","source":{"stars":2436,"repo_name":"Auto-Redbook-Skills","origin_url":"https://github.com/comeonzhj/Auto-Redbook-Skills/blob/HEAD/SKILL.md","repo_owner":"comeonzhj","body_sha256":"432cbc51cc4ef843d3fa0558f360723bc7cf5def057b1eb37f442ba82d5a32f6","cluster_key":"3e4c4b1a2d2e180f1b597116f96ab4e1533d049fb97f0b90d56676fe3b527d45","clean_bundle":{"format":"clean-skill-bundle-v1","source":"comeonzhj/Auto-Redbook-Skills/SKILL.md","text_bytes":150960,"source_commit":"da1c73a6624d1df1330e161663c3614753d4220b","skills_sh_rank":10914,"attachment_count":18,"text_attachments":18,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"media-content","category_label":"Media"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"media-content","import_tag":"skills-sh-top20k-2026-11","description":"小红书笔记素材创作技能。当用户需要创建小红书笔记素材时使用这个技能。技能包含:根据用户的需求和提供的资料,撰写小红书笔记内容(标题+正文),生成图片卡片(封面+正文卡片),以及发布小红书笔记。支持 8 种精美排版主题和 4 种智能分页模式。"}},"renderedAt":1793666123851}

小红书笔记创作技能 根据用户提供的资料或需求,创作小红书笔记内容、生成精美图片卡片,并可选择发布到小红书。 详细参数文档见 --- 工作流程 第一步:撰写小红书笔记内容 根据用户需求和资料,创作符合小红书风格的内容: 标题 :不超过 20 字,吸引眼球,可用数字/疑问句/感叹号增强吸引力。 正文 :段落清晰,点缀少量 Emoji(每段 1-2 个),短句短段,结尾附 5-10 个 SEO 标签。 --- 第二步:生成渲染用 Markdown 文档 注意:此 Markdown 专为图片渲染设计,禁止直接使用上一步的笔记正文。 文档结构: 分页策略选择: - 内容需精确切分 → 用 手动分隔,配合 - 内容长短不稳定 → 生成普通 Markdown,使用 --- 第三步:渲染图片卡片 默认主题 : (手绘素描风格) 默认分页 : (按 分隔) 常用示例: 生成结果: (封面)+ 、 ...(正文卡片) 可用主题 ( ): 、 、 、 、 、 、 、 分页模式 ( ): 、 、 、 完整参数说明见 --- 第四步:发布小红书笔记(可选) 前置条件 :安装 Python 与 Node.js 依赖,并配置 中的 。Cookie 必须来自已登录的 (详见 )。 默认以「仅自己可见」发布 ,加 参数才会公开。 发布链路已内置在当前技能中,不依赖外部 API 服务或其他项目目录。 --- 技能资…