Skill: houdaliang/guiji-xhs-factory (version 1.0.0)

--- name: guiji-xhs-factory version: 1.0.0 description: 龙虾内容工厂。批量生成小红书信息图+视频，定时自动发布。 metadata: emoji: "🏭" category: "content" requires: bins: ["ffmpeg", "python3"] --- # 🏭 龙虾内容工厂 批量生成小红书帖子（信息图+视频），定时自动发布。 **核心流程**：定义帖子 → 生成图片 → 拼成视频 → 定时发布 --- ## 安装 ```bash # 1. 解压到 skills 目录 unzip guiji-xhs-factory.skill.zip -d ~/.openclaw/workspace/skills/ # 2. 安装 Python 依赖 pip3 install Pillow # 3. 安装 ffmpeg（macOS） brew install ffmpeg # 4. 安装 Playwright（发布用） npm install -g playwright npx playwright install chromium # 5. 确认 Chrome 运行中（CDP 端口 18800） # 需要以 --remote-debugging-port=18800 启动 Chrome ``` --- ## 快速开始（3 步） ### 第1步：编辑帖子内容 打开 `{baseDir}/scripts/generate-today.py`，编辑 `POSTS` 数组： ```python POSTS = [ { "id": "my_post_1", "schedule": "12:00", # 发布时间 "title": "帖子标题(≤20字)", # 小红书标题 "content": "正文内容+标签", # 小红书正文（含#标签） "cards": [ # 信息图配置 {"title": "封面大标题", "items": []}, # items为空=纯标题封面 {"title": "要点标题", "items": [ "##分类名", # 高亮分类标题 "要点1", # 普通内容 "要点2", "» 副标题文字", # 灰色副标题 "", # 空行=间距 "要点3", ]}, ] }, ] ``` **items 格式说明**： | 前缀 | 效果 | 示例 | |------|------|------| | 无 | 普通文字 | `"自动安全审核"` | | `##` | 分类标题（高亮色） | `"##安全防护"` | | `»` | 副标题（灰色小字） | `"» 效率提升"` | | `""` | 空行间距 | `""` | **配色预设**（在 card 上加 `"preset": "xxx"`）： - `default` — 深蓝黑 + 天蓝（通用） - `tutorial` — 深绿 + 翠绿（教程） - `case` — 深棕 + 橙色（案例） - `secret` — 深紫 + 紫色（秘诀） - `cover` — 深蓝 + 蓝色（封面） ### 第2步：生成内容 ```bash python3 {baseDir}/scripts/generate-today.py ``` 自动完成： - 每张 card → 1080×1440 PNG 信息图 - 所有图片 → 视频幻灯片（每张 6 秒，25fps） - 输出 `content-queue/manifest.json` 清单 ### 第3步：发布 **方式A：定时发布（推荐）** ```bash openclaw cron add \ --name "小红书-午间发布" \ --cron "0 12 * * *" \ --tz "Asia/Shanghai" \ --message "定时发布：读 manifest.json → 运行 node {baseDir}/scripts/publish-xhs.js <视频路径> '<标题>' '<正文>'" ``` **方式B：手动发布** ```bash node {baseDir}/scripts/publish-xhs.js \ /tmp/openclaw/uploads/my_post_1_slideshow.mp4 \ "我的标题" \ "我的正文内容" ``` --- ## 发布脚本说明 `publish-xhs.js` 使用 Playwright 连接 Chrome（CDP 18800）： 1. 打开小红书创作者平台 2. 上传视频 3. 填写标题（React nativeInputValueSetter hack） 4. 填写正文（innerHTML 注入） 5. **不自动点击发布** — 等你确认后手动点 --- ## 发布规则速查 | 项目 | 规则 | |------|------| | 标题 | ≤20 字，含数字或关键词 | | 正文 | 300-800 字，5-8 个 #标签 | | 配图 | 3:4 (1080×1440)，3-9 张 | | 视频 | MP4，15-60 秒 | | 时间 | 工作日 12-13 点、20-22 点 | | 频率 | 每天 1-3 篇，间隔 ≥2 小时 | --- ## 文件结构 ``` guiji-xhs-factory/ ├── SKILL.md # 本文件（Agent 行为手册） ├── scripts/ │ ├── generate-today.py # 内容生成脚本 │ ├── make_card.py # 信息图生成器（PIL） │ └── publish-xhs.js # Playwright 发布脚本 └── references/ ├── post-templates.md # 5 种帖子模板 └── publish-rules.md # 发布规则详解 ``` --- ## 注意事项 - ⚠️ Chrome 必须以 `--remote-debugging-port=18800` 启动 - ⚠️ 小红书需在 Chrome 中已登录（creator.xiaohongshu.com） - ⚠️ 视频上传比图片上传更稳定，优先用视频格式 - ⚠️ 发布脚本不会自动点发布按钮，需人工确认 --- Built with 🦞 by 龙虾养成师 Ursa

path: references/post-templates.md
content: # 小红书帖子模板

## 模板一：争议型（高互动）
```
标题：{有争议的观点}
正文：
{提出观点}
{反面论证}
{结论}
{引导互动：你觉得呢？}
标签：#争议话题 #行业关键词
```

## 模板二：教程型（高收藏）
```
标题：{N步搞定}{具体结果}
正文：
{背景说明}
1️⃣ 第一步
2️⃣ 第二步
3️⃣ 第三步
{总结+价值}
标签：#教程 #干货 #实操
```

## 模板三：清单型（高传播）
```
标题：{N个}{具体内容}
正文：
{总述}
✅ 内容1
✅ 内容2
✅ 内容3
{使用建议}
标签：#清单 #推荐 #必备
```

## 模板四：案例型（高可信）
```
标题：{我用XX}{结果}
正文：
{背景}
{做了什么}
{具体效果}
{感悟}
标签：#真实案例 #经验分享
```

## 模板五：揭秘型（高点击）
```
标题：{XX的真相}
正文：
{颠覆认知的开头}
{具体揭秘内容}
{证据/数据}
{你的判断}
标签：#揭秘 #真相 #行业内幕
```


path: references/publish-rules.md
content: # 小红书发布规则

## 标题规则
- **长度**：≤20 字（超出会被截断）
- **格式**：React 受控组件，需用 nativeInputValueSetter hack
- **技巧**：包含数字（5步、31个）、关键词、疑问句

## 正文规则
- **长度**：300-800 字最佳
- **结构**：短句 + emoji + 分段
- **格式**：`<p>` 标签，用 innerHTML 设置
- **标签**：5-8 个，格式 `#关键词`

## 配图规则
- **比例**：3:4（1080×1440）最佳
- **数量**：3-9 张
- **封面**：纯标题大字，吸引点击
- **内容**：信息图、数据图、步骤图

## 视频规则
- **比例**：9:16（1080×1920）或 3:4（1080×1440）
- **格式**：MP4
- **时长**：15-60 秒最佳
- **图片拼视频**：每张 5-6 秒

## 发布时间
- **工作日**：12:00-13:00、20:00-22:00
- **周末**：10:00-14:00、20:00-23:00
- **避免**：凌晨、上午 8 点前

## 发布频率
- **最低**：每周 3 篇
- **推荐**：每天 1-2 篇
- **上限**：每天不超过 3 篇（间隔 2 小时以上）

## 数据复盘
- **观察周期**：发布后 24-72 小时
- **关键指标**：阅读量、点赞、收藏、评论
- **爆款标准**：阅读 > 1000、点赞率 > 5%


path: scripts/generate-today.py
content: #!/usr/bin/env python3
"""
🦞 今日内容工厂 — 批量生成小红书帖子配图+视频
直接调用 make_card.py 生成信息图，ffmpeg 转视频
"""
import subprocess, json, os, sys

MAKE_CARD = os.path.expanduser("~/.openclaw/workspace/skills/xhs-smart-post/scripts/make_card.py")
QUEUE = os.path.expanduser("~/.openclaw/workspace-ursa/content-queue")
UPLOAD = "/tmp/openclaw/uploads"
os.makedirs(QUEUE, exist_ok=True)
os.makedirs(UPLOAD, exist_ok=True)

# ===== 帖子定义 =====
POSTS = [
    {
        "id": "sun_1", "schedule": "12:00",
        "title": "我花5分钟配了31个AI技能",
        "content": "今天给大家看看，一个普通人怎么用 OpenClaw 变成 AI 高手 🦞\n\n不需要写代码\n不需要懂技术\n只要会说话\n\n31个技能涵盖：\n✅ 安全防护（自动扫描、权限管理）\n✅ 内容创作（小红书、公众号自动发）\n✅ 效率工具（翻译、搜索、日程管理）\n✅ 社交互动（AI帮我聊天交友）\n\n最绝的是——这些技能都是 AI 自己安装、自己配置的\n\n我全程只做了两件事：\n1. 告诉 AI 我想要什么\n2. 点确认\n\n这就是 2026 年的 AI 生活 🚀\n\n#OpenClaw #AI工具 #效率提升 #科技生活 #人工智能",
        "cards": [
            {"title": "5分钟配31个AI技能", "subtitle": "OpenClaw 龙虾养成指南", "items": []},
            {"title": "安全防护 6个", "subtitle": "自动扫描+权限管理+威胁预警", "items": ["##安全防护", "自动安全审核", "权限检查", "威胁预警", "防火墙管理", "SSH加固", "版本监控"]},
            {"title": "内容创作 6个", "subtitle": "小红书+公众号+视频", "items": ["##内容创作", "小红书自动发布", "公众号工作流", "图文信息图生成", "AI视频生成", "多语言翻译", "URL转Markdown"]},
            {"title": "效率工具 10个", "subtitle": "搜索+日程+记忆", "items": ["##效率工具", "网页搜索", "天气查询", "定时提醒", "记忆管理", "文件备份", "知识库", "YouTube字幕", "安全健康检查"]},
            {"title": "全自动化", "subtitle": "你说话，AI动手", "items": ["##核心能力", "语音交互", "浏览器自动化", "API调用", "文件管理", "代码执行", "定时任务调度"]},
        ]
    },
    {
        "id": "sun_2", "schedule": "20:00",
        "title": "AI帮我管理25台矿机的真实体验",
        "content": "说实话，最开始我也担心 AI 能不能管好矿机 🖥️\n\n实际用了之后发现：\n\n❌ 人工巡检：每天盯 2 小时，还是会漏\n✅ AI 监控：7×24 小时，掉线秒级发现\n\n我的 AI 做了什么：\n1️⃣ 自动检查每台机器状态\n2️⃣ 掉线自动重启\n3️⃣ 生成监控报告\n4️⃣ 异常自动通知我\n\n从 25 台到计划 200 台\nAI 不是替代运维，是让 1 个人干 10 个人的活 💪\n\n#AI运维 #矿机管理 #自动化 #效率革命",
        "cards": [
            {"title": "AI管理25台矿机", "subtitle": "真实体验分享", "items": []},
            {"title": "人工 vs AI", "subtitle": "效率差距一目了然", "items": ["##人工巡检", "每天盯屏2小时", "漏检率约5%", "1人最多管10台", "", "##AI监控", "7×24自动值守", "掉线秒级发现", "1人轻松管200台"]},
            {"title": "AI做了什么", "subtitle": "自动化四件套", "items": ["##1. 自动检查", "每5分钟扫描所有机器", "", "##2. 掉线重启", "发现异常立即重启进程", "", "##3. 监控报告", "生成实时状态看板", "", "##4. 异常通知", "掉线自动推送消息"]},
            {"title": "结论", "subtitle": "AI不是替代人，是超级外挂", "items": ["##效果", "1个人干10个人的活", "25台→200台无压力", "故障响应: 小时→秒级", "人力成本大幅降低", "", "##结论", "AI运维已经成熟了", "你还不用就晚了"]},
        ]
    },
    {
        "id": "sun_3", "schedule": "20:30",
        "title": "让AI帮你发小红书是什么体验",
        "content": "标题党警告：这篇帖子就是 AI 写的 🤖\n\n从选题→写文案→做图→发布\n全程 AI 自动化\n\n我是怎么做到的：\n\n1️⃣ 告诉 AI：\"帮我发一篇小红书\"\n2️⃣ AI 自动选题、写文案\n3️⃣ AI 生成信息图配图\n4️⃣ AI 自动上传发布\n\n我全程只做了一件事——说了那句话 😂\n\n你觉得 AI 写的内容能火吗？\n\n#AI创作 #小红书运营 #自动化内容 #科技生活",
        "cards": [
            {"title": "AI帮我发小红书", "subtitle": "全程自动化体验", "items": []},
            {"title": "Step 1: 选题", "subtitle": "AI分析热点+用户画像", "items": ["##选题策略", "分析平台热门话题", "匹配目标人群画像", "生成3个选题方案", "评估传播潜力打分"]},
            {"title": "Step 2: 内容", "subtitle": "AI写文案+做配图", "items": ["##文案生成", "标题(限20字)", "正文+标签优化", "自动排版美化", "", "##配图生成", "信息图自动设计", "5张配图批量出", "1080×1440竖屏"]},
            {"title": "Step 3: 发布", "subtitle": "AI自动上传+定时发布", "items": ["##自动化发布", "浏览器自动操作", "自动填写表单", "选择最佳时间", "定时自动发布", "", "##你只做一件事", "说\"帮我发小红书\"", "剩下全交给AI"]},
        ]
    },
]

def gen_images(post):
    """生成每张信息图"""
    out_dir = os.path.join(QUEUE, post["id"])
    os.makedirs(out_dir, exist_ok=True)
    
    preset_map = {"sun_1": "tutorial", "sun_2": "case", "sun_3": "secret"}
    preset = preset_map.get(post["id"], "default")
    
    pngs = []
    for i, card in enumerate(post["cards"]):
        fname = f"{post['id']}_{i+1}.png"
        fpath = os.path.join(out_dir, fname)
        items = [x for x in card.get("items", []) if x.strip()]
        subtitle = card.get("subtitle", "")
        if subtitle and items:
            items.insert(0, f"» {subtitle}")
        
        cmd = [sys.executable, MAKE_CARD, "--title", card["title"], "--output", fpath, "--preset", preset]
        if items:
            cmd += ["--items"] + items
        
        r = subprocess.run(cmd, capture_output=True, text=True)
        if r.returncode == 0:
            pngs.append(fpath)
            print(f"  ✅ {fname}")
        else:
            print(f"  ❌ {fname}: {r.stderr.strip()}")
    
    return pngs

def make_video(post):
    """图片拼成视频"""
    out_dir = os.path.join(QUEUE, post["id"])
    pngs = sorted([os.path.join(out_dir, f) for f in os.listdir(out_dir) if f.endswith(".png")])
    if not pngs:
        return None
    
    concat = os.path.join(out_dir, "concat.txt")
    with open(concat, "w") as f:
        for p in pngs:
            f.write(f"file '{p}'\nduration 6\n")
        f.write(f"file '{pngs[-1]}'\n")
    
    vpath = os.path.join(UPLOAD, f"{post['id']}_slideshow.mp4")
    cmd = [
        "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat,
        "-vf", "scale=1080:1440:force_original_aspect_ratio=decrease,pad=1080:1440:(ow-iw)/2:(oh-ih)/2:color=black,format=yuv420p",
        "-c:v", "libx264", "-r", "25", vpath
    ]
    r = subprocess.run(cmd, capture_output=True, text=True)
    if r.returncode == 0:
        size = os.path.getsize(vpath)
        print(f"  🎬 视频: {vpath} ({size//1024}KB, {len(pngs)*6}s)")
        return vpath
    else:
        print(f"  ❌ 视频失败: {r.stderr[-100:]}")
        return None

# ===== 执行 =====
if __name__ == "__main__":
    print("🦞 龙虾内容工厂 启动\n")
    manifest = []
    
    for post in POSTS:
        print(f"\n{'='*40}")
        print(f"📝 {post['title']}")
        print(f"⏰ 定时: {post['schedule']}")
        print(f"{'='*40}")
        
        pngs = gen_images(post)
        video = make_video(post)
        
        manifest.append({
            "id": post["id"],
            "title": post["title"],
            "content": post["content"],
            "schedule": post["schedule"],
            "images": pngs,
            "video": video,
            "status": "ready"
        })
    
    mpath = os.path.join(QUEUE, "manifest.json")
    with open(mpath, "w") as f:
        json.dump(manifest, f, ensure_ascii=False, indent=2)
    
    print(f"\n{'='*40}")
    print(f"🎉 完成！清单: {mpath}")
    for m in manifest:
        print(f"  {m['schedule']} → {m['title']} ({len(m['images'])}图 {'🎬' if m['video'] else ''})")
    print(f"{'='*40}")


path: scripts/make_card.py
content: #!/usr/bin/env python3
"""小红书信息图生成器。

用法:
  python3 make_card.py --title "标题" --items "内容1" "内容2" --output card.png
  python3 make_card.py --preset tutorial  # 使用预设配色
  python3 make_card.py --batch config.json  # 批量生成
"""

import argparse
import json
import os
import sys

try:
    from PIL import Image, ImageDraw, ImageFont
except ImportError:
    print("需要安装 Pillow: pip3 install Pillow", file=sys.stderr)
    sys.exit(1)

W, H = 1080, 1440  # 3:4 竖屏

PRESETS = {
    "default": {"bg": (20, 20, 35), "accent": (100, 200, 255)},
    "tutorial": {"bg": (15, 40, 25), "accent": (0, 220, 120)},
    "case": {"bg": (40, 20, 15), "accent": (255, 150, 50)},
    "secret": {"bg": (25, 15, 40), "accent": (180, 100, 255)},
    "cover": {"bg": (15, 30, 55), "accent": (0, 180, 255)},
}


def get_fonts():
    """查找系统中文字体。"""
    paths = [
        "/System/Library/Fonts/PingFang.ttc",
        "/System/Library/Fonts/STHeiti Medium.ttc",
        "/System/Library/Fonts/Supplemental/Arial Unicode.ttf",
    ]
    for p in paths:
        if os.path.exists(p):
            try:
                return (
                    ImageFont.truetype(p, 52),
                    ImageFont.truetype(p, 30),
                    ImageFont.truetype(p, 24),
                )
            except Exception:
                continue
    default = ImageFont.load_default()
    return default, default, default


def make_card(title, items, output, preset="default", bg_color=None, accent=None):
    """生成一张信息图。"""
    p = PRESETS.get(preset, PRESETS["default"])
    bg = tuple(bg_color) if bg_color else p["bg"]
    ac = tuple(accent) if accent else p["accent"]

    title_font, body_font, small_font = get_fonts()
    img = Image.new("RGB", (W, H), bg)
    draw = ImageDraw.Draw(img)

    # 左侧强调色竖条 + 标题
    draw.rectangle([60, 80, 68, 200], fill=ac)
    draw.text((90, 100), title, fill=(255, 255, 255), font=title_font)

    # 分隔线
    draw.rectangle([80, 180, W - 80, 184], fill=ac)

    # 内容列表
    y = 240
    for item in items:
        if item == "":
            y += 20
            continue
        if item.startswith("##"):
            draw.text((80, y), item[2:], fill=ac, font=body_font)
            y += 55
        elif item.startswith("--"):
            draw.text((100, y), item[2:], fill=(160, 160, 160), font=small_font)
            y += 40
        else:
            draw.text((100, y), item, fill=(220, 220, 220), font=body_font)
            y += 52

    img.save(output)
    print(f"✅ 已保存: {output}")


def batch_generate(config_path):
    """从 JSON 配置批量生成。"""
    with open(config_path) as f:
        config = json.load(f)
    for card in config.get("cards", []):
        make_card(
            title=card["title"],
            items=card["items"],
            output=card["output"],
            preset=card.get("preset", "default"),
            bg_color=card.get("bg_color"),
            accent=card.get("accent"),
        )


def main():
    parser = argparse.ArgumentParser(description="小红书信息图生成器")
    parser.add_argument("--title", help="标题文字")
    parser.add_argument("--items", nargs="+", help="内容列表（用引号包裹）")
    parser.add_argument("--output", default="card.png", help="输出文件路径")
    parser.add_argument(
        "--preset",
        default="default",
        choices=list(PRESETS.keys()),
        help="配色预设",
    )
    parser.add_argument("--batch", help="批量生成配置文件路径")
    args = parser.parse_args()

    if args.batch:
        batch_generate(args.batch)
    elif args.title and args.items:
        make_card(args.title, args.items, args.output, args.preset)
    else:
        parser.print_help()
        print("\n示例:")
        print('  python3 make_card.py --title "测试" --items "项目1" "项目2" --output test.png')
        print(f"  可用预设: {', '.join(PRESETS.keys())}")


if __name__ == "__main__":
    main()


path: scripts/publish-xhs.js
content: #!/usr/bin/env node
// publish-xhs.js — 用 Playwright 自动发布小红书视频笔记
// 用法: node publish-xhs.js <video-path> <title> <content>
const { chromium } = require('/Users/houdaliang/.nvm/versions/node/v22.22.1/lib/node_modules/playwright');

const [,, videoPath, title, content] = process.argv;

if (!videoPath) {
  console.error('Usage: node publish-xhs.js <video-path> <title> <content>');
  process.exit(1);
}

const PUBLISH_URL = 'https://creator.xiaohongshu.com/publish/publish?from=homepage&target=video';

// React 受控组件赋值 hack
const SET_REACT_INPUT = `
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
nativeSetter.call(this, value);
this.dispatchEvent(new Event('input', { bubbles: true }));
this.dispatchEvent(new Event('change', { bubbles: true }));
`;

async function main() {
  const browser = await chromium.connectOverCDP('http://127.0.0.1:18800');
  const context = browser.contexts()[0];
  
  // 打开新标签页
  const page = await context.newPage();
  await page.goto(PUBLISH_URL, { waitUntil: 'networkidle', timeout: 30000 });
  await page.waitForTimeout(3000);
  
  console.log('📄 页面加载完成');
  
  // 上传视频
  const fileInput = await page.$('input.upload-input');
  if (!fileInput) {
    console.error('❌ 找不到上传按钮');
    process.exit(1);
  }
  
  // 点击触发 filechooser，然后设置文件
  const [fileChooser] = await Promise.all([
    page.waitForEvent('filechooser', { timeout: 10000 }),
    page.click('.cover-container, .upload-area, [class*="upload"]')
  ]);
  
  await fileChooser.setFiles(videoPath);
  console.log(`🎬 视频已上传: ${videoPath}`);
  
  // 等待上传完成
  await page.waitForTimeout(8000);
  
  // 填写标题（用 nativeInputValueSetter 绕过 React）
  if (title) {
    await page.evaluate(({ selector, value }) => {
      const el = document.querySelector(selector);
      if (el) {
        const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
        nativeSetter.call(el, value);
        el.dispatchEvent(new Event('input', { bubbles: true }));
        el.dispatchEvent(new Event('change', { bubbles: true }));
      }
    }, { selector: 'input[placeholder*="标题"]', value: title });
    console.log(`📝 标题: ${title}`);
  }
  
  // 填写正文
  if (content) {
    await page.evaluate(({ selector, value }) => {
      const el = document.querySelector(selector);
      if (el && el.contentEditable === 'true') {
        el.innerHTML = value.split('\n').map(l => `<p>${l || '<br>'}</p>`).join('');
        el.dispatchEvent(new Event('input', { bubbles: true }));
      }
    }, { selector: '[contenteditable="true"]', value: content });
    console.log('📝 正文已填写');
  }
  
  // 等待一下确保所有内容就绪
  await page.waitForTimeout(2000);
  
  console.log('✅ 发布准备完成！请手动点击"发布"按钮');
  console.log('⚠️ 不自动点击发布，由你确认后手动发布');
  
  // 不关闭浏览器，让用户手动确认发布
  // browser.close() 不调用
}

main().catch(e => {
  console.error('❌ 错误:', e.message);
  process.exit(1);
});


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)