Skill: kelcey2023/zeelin-xiaohongshu-autopost (version 1.0.100)

--- name: ZeeLin 小红书自动发布 description: > ZeeLin小红书自动化内容运营技能：从热文抓取→热梗生成→产品结合→发布文案的全流程自动化内容生产。 适用场景包括但不限于：短视频选题策划、爆款文案生成、钩子开头撰写、 账号人设定位、自媒体内容日历规划。当用户提到"内容运营"、"选题"、"爆款"、"钩子"、 "文案"、"短视频策划"、"自媒体运营"、"账号定位"、"热点追踪"、"内容日历"、 "文旅内容"、"产品种草"等关键词时，请使用此技能。即使用户只是问"帮我写个短视频文案" 或"最近有什么热点可以追"，也应触发此技能。 user-invocable: true metadata: {"openclaw":{"emoji":"📕","skillKey":"zeelin-xiaohongshu-autopost"}} --- # 自动化网络内容运营 (Auto Content Ops) ## 技能概述 本技能将薛辉自媒体方法论体系化为一套可执行的自动化内容生产流水线。 核心流程：**抓热文 → 生热梗 → 结合产品 → 输出发布文案**。 在执行任何步骤前，先阅读 `references/methodology.md` 获取完整方法论知识库。 --- ## 一、全流程 Pipeline ### Phase 1：热点抓取（抓热文） 用 web_search 工具执行以下搜索策略： 1. **热榜扫描**：搜索"抖音热搜今日"、"微博热搜榜"、"今日热榜" 2. **行业热点**：搜索"{用户行业} 最新热点 2026"、"{行业} 抖音爆款" 3. **竞品监控**：搜索"{对标账号/竞品} 最新内容" 4. **平台趋势**：搜索"抖音/小红书 近期流行趋势" **输出格式**： ``` 热点清单： 1. [热点标题] | 热度指数 | 情绪标签 | 可结合方向 2. ... ``` **筛选原则**（参考方法论）： - 必须是目标用户群体关心的话题 - 优先选择能激发"全人类共同情绪"的热点 - 判断热点生命周期：是否还有追的价值 --- ### Phase 2：热梗生成（生热梗） 基于抓取的热点，运用八大爆款元素生成内容梗： **八大爆款元素**：成本、人群、反差、奇葩、最差、怀旧、头牌、荷尔蒙 **生成步骤**： 1. 从热点中提取核心冲突（参见方法论"冲突分析"） 2. 用八大元素逐一匹配，生成候选梗 3. 运用"陌生化"原则优化——稀缺景观、稀缺情感、稀缺信息 4. 用"赋比兴"手法丰富表达 **梗的质量检查**： - 是否有情绪驱动（不是纯知识/纯事件） - 是否有立场（关心谁？为谁说话？） - 是否有陌生感（新鲜度） - 是否能激发"凑热闹"心理（资讯感 > 知识感） --- ### Phase 3：产品结合（结合产品） 将热梗与用户的产品/服务/账号价值绑定： **结合策略**（参考"用户·产品·营销"方法论）： 1. **冲突嫁接**：找到产品能解决的冲突点 - 生理vs心理冲突（如：想吃甜食但怕胖 → 低卡产品） - 需求vs现状冲突（如：想旅行但没时间 → 浙江周边游） 2. **价值叠加**：普世情绪 + 产品价值 - "醉翁之意不在酒"——美食的关键不在食物，旅行的关键不在景点 - 理性壁垒 + 情感羁绊 3. **进攻左脑+右脑**： - 左脑：数据、对比、绝对真相 - 右脑：情感、直觉、精神价值（自由、归属、地位、冒险…） 4. **用户成本最小化**：降低货币/时间/体力/心力/风险成本的感知 **浙江文旅特别策略**： - 结合浙江地域文化符号（西湖、乌镇、千岛湖、南宋文化…） - 地域细分：农村包围城市策略，发掘小众目的地 - 跨界并置：古镇+美食、山水+治愈、非遗+体验 - 季节/节气/节日热点绑定 --- ### Phase 4：文案输出（自动发布） 生成完整的发布文案包，包含以下组件： #### 4.1 钩子开头（3秒留人） 运用36种钩子模板，核心6类： | 钩子类型 | 公式 | 示例 | |---------|------|------| | 圈定人群 | 人群标签 + 悬念/痛点 | "想来浙江旅游的人注意了！" | | 直接提问 | 疑问词 + 反常识/悬念 | "你知道杭州本地人周末都去哪吗？" | | 自我否定 | 否定自身经验 + 转折 | "做了5年旅游博主，我居然不知道…" | | 数据冲击 | 具体数字 + 颠覆认知 | "这个古镇99%的游客都走错了路线" | | 场景代入 | 具体场景描绘 + 悬念 | "凌晨5点的西湖边，我看到了…" | | 否定性力量 | 设定敌人/对抗 | "别再被网红打卡点骗了！" | #### 4.2 正文结构 根据内容类型选择脚本结构： - **观点型**：钩子 → 观点亮出 → 论据铺陈（赋） → 金句收尾 - **故事型**：钩子 → 情境设置 → 冲突升级 → 反转/解决 → 升华 - **知识型**：钩子 → 痛点共鸣 → 方法拆解 → 案例验证 → 行动指令 - **种草型**：钩子 → 场景需求 → 产品亮相 → 效果展示 → 促单 #### 4.3 文案语言要求 参考"文字语言基本功"： - **可视化**：用"Do句式"替代"Be句式"，用动作和场面替代抽象描述 - **节奏感**：押韵、排比、顶真、长短句交替 - **特定感**：个体经历引发普遍共鸣，故事的尖峰时刻 - **口语化**：移动端观看场景，日常直观的语言 #### 4.4 时间语法 参考"时间的语法"控制视频节奏： - **时序**：构建不同前提产生不同体验 - **密度**：详略得当，旁白概述vs现场展开 - **停顿**：让张力悬置，让观众充分反应 - **频率**：重复与差异的节奏 - **否定性力量**：调动情绪最快的方式 #### 4.5 立场与姿态 参考"自媒体的立场和姿态"： - **强者示弱**：穿着贵气但讲话亲切，讲述成功但强调经历相似 - **弱者积极**：可以可怜但不可恨，必须有真善美 - **立场转移**：站在弱势群体一边，为没有被关心的人说话 - **团结力量**：争取朋友比打击敌人更重要 --- ## 二、输出模板 每次执行完整 Pipeline 后，输出以下结构： ```markdown # 📋 内容运营日报 / 周报 ## 🔥 热点速览 | 序号 | 热点 | 热度 | 情绪 | 可用指数 | |------|------|------|------|---------| | 1 | ... | ⭐⭐⭐ | 好奇 | 高 | ## 💡 选题方案（Top 3） ### 选题一：[标题] - 爆款元素：[所用元素] - 目标人群：[具体人群画像] - 情绪锚点：[激发什么情绪] - 产品结合点：[如何与产品关联] **钩子文案（3版）：** 1. [圈定人群版] 2. [提问版] 3. [否定力量版] **完整脚本：** [含开头-正文-结尾的完整文案] **发布建议：** - 平台：抖音 / 小红书 / 视频号 - 时间：[最佳发布时间] - 标签：#xx #xx #xx - 互动引导：[评论区引导语] ## 📅 本周内容日历 | 周一 | 周二 | 周三 | 周四 | 周五 | 周六 | 周日 | |------|------|------|------|------|------|------| | 选题 | 选题 | 选题 | 选题 | 选题 | 选题 | 选题 | ``` --- ## 三、账号定位诊断（可选模块） 如果用户还没有明确的账号定位，先执行定位诊断： 1. **确认公式**：账号定位 = 价值 × 用户 × 人设 × 类型 × 风格 2. **避免两大误区**：过度实用（赛道工具人）、过度抽象（价值不落地） 3. **竞争力分析**：正（降维）、反（否定对抗）、合（交叉优势）、跨（能力迁移）、借（外部资源）、弱（现身说法） 4. **找到弱势群体**：起号绝招——找到没有被关心的人，领导他们，对他们说话 --- ## 四、浙江文旅专项模块 针对浙江文旅账号的特殊策略： ### 4.1 内容矩阵 - **文化线**：宋韵文化、丝绸之路、茶文化、非遗手艺 - **自然线**：千岛湖、莫干山、雁荡山、楠溪江、海岛 - **城市线**：杭州新消费、义乌商贸、温州创业、绍兴黄酒 - **生活线**：江南水乡生活、浙菜美食、民宿体验、乡村振兴 - **情感线**：诗画江南、白蛇传说、梁祝故里、南宋风雅 ### 4.2 选题公式 - 浙江文旅 + 八大爆款元素： - 成本：500块玩转杭州3天2夜 - 人群：带父母游浙江的正确打开方式 - 反差：本地人vs游客的浙江玩法 - 头牌：浙江最贵的民宿到底值不值 - 怀旧：90年代的西湖长什么样 - 荷尔蒙：浙江最适合约会的10个地方 - 猎奇：浙江人自己都不知道的隐藏景点 - 最差：浙江最不值得去的网红打卡点 ### 4.3 并置创新 - 古镇 + 咖啡文化 - 山水 + 心理疗愈 - 非遗 + Z世代体验 - 美食 + 地域性格 - 古诗词 + 实景打卡 --- ## 五、执行注意事项 1. **每次生成前确认**：用户行业、目标人群、产品/服务、账号调性 2. **陌生化检查**：生成的内容是否有新鲜感，是否避免了"老生常谈" 3. **情绪检查**：是否基于情绪和立场表达，而非纯知识讲述 4. **冲突检查**：是否制造了有效冲突，冲突是否与产品相关 5. **成本检查**：用户看完是否能低成本行动 6. **立场检查**：是否站在了弱势一方，是否在关心没被关心的人 > 记住核心原则：选题要基于一种情绪、一种立场去表达，而不是单纯讲知识本身、讲事件本身。 --- ## 六、网页发布SOP（2026-03 同步） 当执行自动发布时，网页流程以脚本为准，固定步骤如下： 1. 进入创作首页后点击「发布笔记」。 2. 在弹层点击「写长文」。 3. 若弹出桌面素材选择框，先点「取消」。 4. 点击「新的创作」进入编辑器。 5. 填写标题与正文后点击「一键排版」，等待排版完成。 6. 在模板区选择「基础简约」（若未出现则选择首个可用模板）。 7. 点击左下角「下一步」。 8. 在确认页点击红色「发布」。 脚本入口： - `scripts/auto_topic_post_xiaohongshu.sh`：主题到发布一体化。 - `scripts/post_xiaohongshu.sh`：仅执行发布流程（接收标题与正文）。 发布成功判定关键字：`发布成功`、`审核中`、`已发布`、`内容已提交`。

path: _meta.json
content: slug: zeelin-xiaohongshu-autopost
version: 1.0.99

path: references/auto-content-ops-skill-完整文档.md
content: ---
name: auto-content-ops
description: >
  自动化网络内容运营技能：从热文抓取→热梗生成→产品结合→发布文案的全流程自动化内容生产。
  适用场景包括但不限于：浙江文旅账号运营、短视频选题策划、爆款文案生成、钩子开头撰写、
  账号人设定位、自媒体内容日历规划。当用户提到"内容运营"、"选题"、"爆款"、"钩子"、
  "文案"、"短视频策划"、"自媒体运营"、"账号定位"、"热点追踪"、"内容日历"、
  "文旅内容"、"产品种草"等关键词时，请使用此技能。即使用户只是问"帮我写个短视频文案"
  或"最近有什么热点可以追"，也应触发此技能。
---

# 自动化网络内容运营 (Auto Content Ops)

## 技能概述

本技能将薛辉自媒体方法论体系化为一套可执行的自动化内容生产流水线。
核心流程：**抓热文 → 生热梗 → 结合产品 → 输出发布文案**。

在执行任何步骤前，先阅读 `references/methodology.md` 获取完整方法论知识库。

---

## 一、全流程 Pipeline

### Phase 1：热点抓取（抓热文）

用 web_search 工具执行以下搜索策略：

1. **热榜扫描**：搜索"抖音热搜今日"、"微博热搜榜"、"今日热榜"
2. **行业热点**：搜索"{用户行业} 最新热点 2026"、"{行业} 抖音爆款"
3. **竞品监控**：搜索"{对标账号/竞品} 最新内容"
4. **平台趋势**：搜索"抖音/小红书 近期流行趋势"

**输出格式**：
```
热点清单：
1. [热点标题] | 热度指数 | 情绪标签 | 可结合方向
2. ...
```

**筛选原则**（参考方法论）：
- 必须是目标用户群体关心的话题
- 优先选择能激发"全人类共同情绪"的热点
- 判断热点生命周期：是否还有追的价值

---

### Phase 2：热梗生成（生热梗）

基于抓取的热点，运用八大爆款元素生成内容梗：

**八大爆款元素**：成本、人群、反差、奇葩、最差、怀旧、头牌、荷尔蒙

**生成步骤**：
1. 从热点中提取核心冲突（参见方法论"冲突分析"）
2. 用八大元素逐一匹配，生成候选梗
3. 运用"陌生化"原则优化——稀缺景观、稀缺情感、稀缺信息
4. 用"赋比兴"手法丰富表达

**梗的质量检查**：
- 是否有情绪驱动（不是纯知识/纯事件）
- 是否有立场（关心谁？为谁说话？）
- 是否有陌生感（新鲜度）
- 是否能激发"凑热闹"心理（资讯感 > 知识感）

---

### Phase 3：产品结合（结合产品）

将热梗与用户的产品/服务/账号价值绑定：

**结合策略**（参考"用户·产品·营销"方法论）：

1. **冲突嫁接**：找到产品能解决的冲突点
   - 生理vs心理冲突（如：想吃甜食但怕胖 → 低卡产品）
   - 需求vs现状冲突（如：想旅行但没时间 → 浙江周边游）

2. **价值叠加**：普世情绪 + 产品价值
   - "醉翁之意不在酒"——美食的关键不在食物，旅行的关键不在景点
   - 理性壁垒 + 情感羁绊

3. **进攻左脑+右脑**：
   - 左脑：数据、对比、绝对真相
   - 右脑：情感、直觉、精神价值（自由、归属、地位、冒险…）

4. **用户成本最小化**：降低货币/时间/体力/心力/风险成本的感知

**浙江文旅特别策略**：
- 结合浙江地域文化符号（西湖、乌镇、千岛湖、南宋文化…）
- 地域细分：农村包围城市策略，发掘小众目的地
- 跨界并置：古镇+美食、山水+治愈、非遗+体验
- 季节/节气/节日热点绑定

---

### Phase 4：文案输出（自动发布）

生成完整的发布文案包，包含以下组件：

#### 4.1 钩子开头（3秒留人）

运用36种钩子模板，核心6类：

| 钩子类型 | 公式 | 示例 |
|---------|------|------|
| 圈定人群 | 人群标签 + 悬念/痛点 | "想来浙江旅游的人注意了！" |
| 直接提问 | 疑问词 + 反常识/悬念 | "你知道杭州本地人周末都去哪吗？" |
| 自我否定 | 否定自身经验 + 转折 | "做了5年旅游博主，我居然不知道…" |
| 数据冲击 | 具体数字 + 颠覆认知 | "这个古镇99%的游客都走错了路线" |
| 场景代入 | 具体场景描绘 + 悬念 | "凌晨5点的西湖边，我看到了…" |
| 否定性力量 | 设定敌人/对抗 | "别再被网红打卡点骗了！" |

#### 4.2 正文结构

根据内容类型选择脚本结构：

- **观点型**：钩子 → 观点亮出 → 论据铺陈（赋） → 金句收尾
- **故事型**：钩子 → 情境设置 → 冲突升级 → 反转/解决 → 升华
- **知识型**：钩子 → 痛点共鸣 → 方法拆解 → 案例验证 → 行动指令
- **种草型**：钩子 → 场景需求 → 产品亮相 → 效果展示 → 促单

#### 4.3 文案语言要求

参考"文字语言基本功"：
- **可视化**：用"Do句式"替代"Be句式"，用动作和场面替代抽象描述
- **节奏感**：押韵、排比、顶真、长短句交替
- **特定感**：个体经历引发普遍共鸣，故事的尖峰时刻
- **口语化**：移动端观看场景，日常直观的语言

#### 4.4 时间语法

参考"时间的语法"控制视频节奏：
- **时序**：构建不同前提产生不同体验
- **密度**：详略得当，旁白概述vs现场展开
- **停顿**：让张力悬置，让观众充分反应
- **频率**：重复与差异的节奏
- **否定性力量**：调动情绪最快的方式

#### 4.5 立场与姿态

参考"自媒体的立场和姿态"：
- **强者示弱**：穿着贵气但讲话亲切，讲述成功但强调经历相似
- **弱者积极**：可以可怜但不可恨，必须有真善美
- **立场转移**：站在弱势群体一边，为没有被关心的人说话
- **团结力量**：争取朋友比打击敌人更重要

---

## 二、输出模板

每次执行完整 Pipeline 后，输出以下结构：

```markdown
# 📋 内容运营日报 / 周报

## 🔥 热点速览
| 序号 | 热点 | 热度 | 情绪 | 可用指数 |
|------|------|------|------|---------|
| 1 | ... | ⭐⭐⭐ | 好奇 | 高 |

## 💡 选题方案（Top 3）

### 选题一：[标题]
- 爆款元素：[所用元素]
- 目标人群：[具体人群画像]
- 情绪锚点：[激发什么情绪]
- 产品结合点：[如何与产品关联]

**钩子文案（3版）：**
1. [圈定人群版]
2. [提问版]
3. [否定力量版]

**完整脚本：**
[含开头-正文-结尾的完整文案]

**发布建议：**
- 平台：抖音 / 小红书 / 视频号
- 时间：[最佳发布时间]
- 标签：#xx #xx #xx
- 互动引导：[评论区引导语]

## 📅 本周内容日历
| 周一 | 周二 | 周三 | 周四 | 周五 | 周六 | 周日 |
|------|------|------|------|------|------|------|
| 选题 | 选题 | 选题 | 选题 | 选题 | 选题 | 选题 |
```

---

## 三、账号定位诊断（可选模块）

如果用户还没有明确的账号定位，先执行定位诊断：

1. **确认公式**：账号定位 = 价值 × 用户 × 人设 × 类型 × 风格
2. **避免两大误区**：过度实用（赛道工具人）、过度抽象（价值不落地）
3. **竞争力分析**：正（降维）、反（否定对抗）、合（交叉优势）、跨（能力迁移）、借（外部资源）、弱（现身说法）
4. **找到弱势群体**：起号绝招——找到没有被关心的人，领导他们，对他们说话

---

## 四、浙江文旅专项模块

针对浙江文旅账号的特殊策略：

### 4.1 内容矩阵
- **文化线**：宋韵文化、丝绸之路、茶文化、非遗手艺
- **自然线**：千岛湖、莫干山、雁荡山、楠溪江、海岛
- **城市线**：杭州新消费、义乌商贸、温州创业、绍兴黄酒
- **生活线**：江南水乡生活、浙菜美食、民宿体验、乡村振兴
- **情感线**：诗画江南、白蛇传说、梁祝故里、南宋风雅

### 4.2 选题公式
- 浙江文旅 + 八大爆款元素：
  - 成本：500块玩转杭州3天2夜
  - 人群：带父母游浙江的正确打开方式
  - 反差：本地人vs游客的浙江玩法
  - 头牌：浙江最贵的民宿到底值不值
  - 怀旧：90年代的西湖长什么样
  - 荷尔蒙：浙江最适合约会的10个地方
  - 猎奇：浙江人自己都不知道的隐藏景点
  - 最差：浙江最不值得去的网红打卡点

### 4.3 并置创新
- 古镇 + 咖啡文化
- 山水 + 心理疗愈
- 非遗 + Z世代体验
- 美食 + 地域性格
- 古诗词 + 实景打卡

---

## 五、执行注意事项

1. **每次生成前确认**：用户行业、目标人群、产品/服务、账号调性
2. **陌生化检查**：生成的内容是否有新鲜感，是否避免了"老生常谈"
3. **情绪检查**：是否基于情绪和立场表达，而非纯知识讲述
4. **冲突检查**：是否制造了有效冲突，冲突是否与产品相关
5. **成本检查**：用户看完是否能低成本行动
6. **立场检查**：是否站在了弱势一方，是否在关心没被关心的人

> 记住核心原则：选题要基于一种情绪、一种立场去表达，而不是单纯讲知识本身、讲事件本身。
-e 

---

# 附录：方法论知识库


# 方法论知识库（Methodology Reference）

本文件整合自薛辉自媒体课程体系全部文档，作为内容生成时的核心参考。

---

## 目录
1. [账号定位方法论](#一账号定位方法论)
2. [爆款选题方法论](#二爆款选题方法论)
3. [八大爆款元素与选题指令](#三八大爆款元素与选题指令)
4. [钩子公式库](#四钩子公式库)
5. [文字语言基本功](#五文字语言基本功)
6. [时间的语法](#六时间的语法)
7. [用户·产品·营销](#七用户产品营销)
8. [自媒体的立场和姿态](#八自媒体的立场和姿态)
9. [素材库搭建与搜索方法](#九素材库搭建与搜索方法)

---

## 一、账号定位方法论

### 核心公式
账号定位 = 价值 × 用户 × 人设 × 类型 × 风格

### 网红 vs IP
- 网红被消遣，IP被追随
- 网红服务粉丝，IP影响粉丝
- 网红无法为商品赋能，IP可以为商品赋能

### 两大误区
1. **过度实用**（赛道工具人）→ 解决方案：用理念串联知识点，植入个人价值观
2. **过度抽象**（价值不落地）→ 解决方案：找到具体利他的内容，分享生活和价值观

### 提供价值的思路
- 有明确变现目的：免费的精神 → 服务某些用户
- 无明确变现目的：基于自身价值出发 → 解决一个人一个阶段的一个问题
- 让账号聚集一批渴望某些事物的人

### 六大竞争力策略
1. **正**（降维碾压）：全局优势、局部优势、细分优势
   - 流程细分：定位/文案/脚本/拍摄/剪辑
   - 行业细分：餐饮→烧烤店/奶茶店
   - 年龄细分：00后职业规划 / 35岁转型
   - 理念细分：性价比/品质感/服务特定审美
   - 地域细分：农村包围城市
2. **反**（否定对抗）：寻找敌人未满足的人群、沉默市场
3. **合**（交叉优势）：兴趣爱好+普世价值、特殊领域+普世价值、特殊场景+普世价值
   - 甲骨文+爱情、围棋+人生、美食+文化
   - 机车+美女、中医+治愈生活、律师+父女生活
   - 烧烤店→友情北漂、律师所→婚姻背叛
   - 价值叠加公式：理性壁垒 + 情感羁绊
4. **跨**（能力迁移）：一切生活哲学化、透过工作看能力
   - 产品经理教人生规划、英语配音演员教成年人英语
   - 老用户新价值：谁说女性成长就一定给中年女性讲
5. **借**（外部资源）：小众身份的专属视野、策划一个使命
   - 参考来源：电影、电视节目、书籍策划
6. **弱**（现身说法）：缺陷转使命、成为榜样、长期主义

---

## 二、爆款选题方法论

### 核心原则
> 选题要基于一种情绪、一种立场去表达，而不是单纯讲知识本身、讲事件本身。
> 没有新鲜度的选题是没有含金量的。

### 流量公式
流量大 = 需求量大（供小于求）+ 数据好（赛马机制动态竞争）

### 四大普世化切入点

1. **潜在需求 > 正在需求**
   - 错误："正山小种是最好的红茶"
   - 正确："三十五岁之后，大家才知道喝正山小种的好处"

2. **凑热闹 > 求知欲**
   - 错误："想要流量密码，就需要学会切片性"
   - 正确："我大胆预言，未来所有热门视频都会有切片性"

3. **八卦 > 痛点**
   - 错误："年轻人买房怎么选"
   - 正确："我搞房地产十多年见过最可笑可怜的买房行为"

4. **全人类共同情绪 > 事件本身**
   - 错误："健身的方法" → 正确："我们健身是为了让自己骄傲"
   - 错误："穿搭技巧" → 正确："女性的穿搭是自我取悦"
   - 错误："旗袍知识" → 正确："女性独立意识的觉醒从旗袍开始"

### 全人类共同情绪清单
社会声望、性、公民权、爱与浪漫、力量、体育运动、优越感、秩序、被安慰和被理解、独立、复仇、社会交往、个人魅力提升、家庭、好奇心、对不美好的厌恶、荣誉感、被社会排斥的恐惧

### 陌生化五要素
- 稀缺的景观、稀缺的情感、稀缺的美好、稀缺的信息和资讯、稀缺的奇闻逸事、稀缺的事件

### 赋比兴手法
- **赋**：重演绎细节，大量铺陈和堆叠的具体细节之美
- **比**：重在两者的并置，对立之美和结合之美
- **兴**：重在转化的过程，激发一种联想的转化之美

### 演绎技巧
1. **往内折叠**：深度聚焦具体的人与场景
   - 组合情境：产品×人群×场景×痛点
   - 预算越低效果越好（有限空间创造可能）
2. **设置障碍**：挑战、实验、测评
3. **置换元素**：置换时间/环境/道具/人物关系

### 并置技巧
不要讲一个事物，要讲一个事物与另一个事物之间的关系：
- 碰撞结合之美：建筑和文学、做饭和音乐、茶和瑜伽
- 对抗矛盾之美：轰轰烈烈vs细水长流、快乐教育vs严格教育
- 越是围绕二元对立展开，脚本越有整体性，观众越想知道答案

### 构建新概念
争夺命名权：构建模型、小众词汇、比喻造词、叠加创造新词
- 例：耦合式恋爱、生活颗粒度、集体日记

---

## 三、八大爆款元素与选题指令

### 使用方法
输入行业关键词，用以下八大元素生成选题：

1. **头牌**：世界上最贵的/明星的/最牛的/最贵的好在哪
2. **怀旧**：20年前经典的/古代人如何/小时候难忘的/当年最火的
3. **对立**：穷人vs富人/南方vs北方/男人vs女人/中国vs外国/古代vs现代
4. **最差**：贬值最快/最难吃/差评最多/最难看/最没面子/反人类设计
5. **荷尔蒙**：相亲成功率高/异性多看两眼/最具吸引力/一秒下头
6. **猎奇**：脑回路有病/外行不知道/黑心内幕/内行神操作/匪夷所思
7. **圈人群**：特定星座/内向外向/不同MBTI/身价十亿/第一次体验/弱势群体
8. **成本**：便宜又有面子/十分之一金钱/十分之一时间/如何偷懒/花小钱办大事

### 选题生成指令模板
```
你是一个帮我生成爆款选题的智能体，围绕"{元素名}"这个爆款元素。
行业：{用户行业}
请生成10条选题，要求：
1. 基于情绪和立场表达，不是纯知识
2. 有陌生化/新鲜感
3. 能激发全人类共同情绪
4. 适合短视频3秒留人
```

---

## 四、钩子公式库

### 核心6大钩子类型

#### 1. 圈定人群
公式 = 喊出人群标签 + 悬念/痛点/恐惧
- 身份标签：基础属性（宝妈/学生/微胖女生）、状态属性（负债者/创业小白）、情绪属性（焦虑的/迷茫的）
- 后半句类型：避坑警告型、颠覆认知型、悬念钩子型
- 完整结构：圈定人群→痛点警告→建立信任→解决方案→行动指令

#### 2. 直接提问
公式 = 疑问词 + 反常识/悬念/利益点
- 疑问词：你敢信/你知道吗/你有没有/如果…会怎样
- 后半句：反常识型、悬念型、紧迫型
- 高阶：提问+数字、提问+背书、提问+互动

#### 3. 自我否定
通过否定自身过往经验制造反差和可信度

#### 4. 数据冲击
用具体数字颠覆认知

#### 5. 场景代入
具体场景描绘+悬念

#### 6. 否定性力量
设定敌人、对抗纠结、先抑后扬

### 钩子质量检查清单
- 3秒内让用户觉得和自己有关
- 触发身份认同感
- 有后续钩子（不能只圈不定）
- 标签精准（不能模糊）
- 有背书（凭什么信你）

---

## 五、文字语言基本功

### 可视化原则
- 用"Do（做）句式"替代"Be（是）句式"
  - 错误："这是一份用可乐做成的肥牛"
  - 正确："我们给肥牛浇上可乐"
- "There be句式"：在百子湾的角落有一家红色招牌店…
- 特定/专属细节：个体经历→普遍经历
- 故事感（特定感）和故事的尖峰时刻

### 节奏感工具
- 押韵、排比、顶真、长短句交替

### 核心要求
- 移动端观看场景，日常直观的语言才能被听进去
- 好的文案就是一个特写镜头
- 画面感方法：描述、类比（抽象→具象）、力量感（对比数量/时间/经历/身份）

---

## 六、时间的语法

### 横向时间
1. **时序**：构建不同前提产生不同体验
2. **时长与密度**：详vs略、旁白概述vs现场展开
3. **停顿**：让张力悬置、让观众充分反应
4. **频率**：重复与差异
5. **否定性力量**：调动情绪最快——设定敌人、对抗纠结、先抑后扬
6. **绵延**：共鸣性推进

### 纵向时间
1. **空间**：高低宽窄、拥挤程度、材料质地
2. **面孔与肢体**

### 整体时间
1. **人称距离**：
   - 上帝视角（vlog）：客观公正全知
   - 第一人称：强烈情绪、内心秘密、暴露软肋
   - 第二人称：双刃剑，观众需扮演角色
   - 第三人称：语言和视觉都重要
   - 打破第四面墙
2. **视觉距离**：不拘泥位置角度，综合感受
3. **听点距离**：直接发声→出镜讲述→旁白回顾
4. **基调**：情感的距离
5. **时态**：时间的距离

---

## 七、用户·产品·营销

### 需求理论
- 用户有各种需求，每次购物产生不同需求重组
- 情境中出现冲突 → 产生需求
- 需求必须依附于情境

### 冲突分析框架
- 生理vs心理（渴望甜食但怕胖）
- 便利vs复杂（高科技产品有用但操作复杂）
- 喜欢vs担心（儿子喜欢但父母担心危险）

### 制造冲突三方向
1. **从消费者入手**：告诉观众老道理不对
2. **从对手入手**：竞品有什么问题
3. **从自我出发**：以前做法有什么不够好

### 效用层级
底线原则 → 够用就好 → 越多越好 → 意外惊喜

### 进攻策略
- **左脑**：理性分析、绝对真相
- **右脑**：情感、直觉、信仰、精神价值
- 精神价值：自由、掌控、地位、责任、不朽、冒险、归属、运势

### 记忆锚点
- 视觉锤、口语化命名、文字钉
- 基于熟悉：熟悉的诗词、文化符号、地名
- 叠化词：滴滴、哇哈哈

---

## 八、自媒体的立场和姿态

### 强者示弱
- 穿着贵气但讲话亲切
- 场景奢华但行为接地气
- 讲述成功但强调经历相似，复盘错误

### 弱者积极
- 可以可怜但不可以可恨
- 可以反对暴强，不可以反社会反人类
- 可以穷丑，不能肮脏
- 必须有真善美，必须乐观向上
- 负面情绪也敢自嘲

### 立场核心
> 永远想着去关心某些人群、没有给予照顾的人群

### 立场转移（拉近关系的关键）
商家→消费者、达人→厂家、老师→学生、领导→职员、妻子→丈夫、婆婆→媳妇、医生→患者

### 起号绝招
**找到弱势群体！领导他们！**

### 选题绝招
**找到没有被关心的人！对他们说话！**

### 团结原则
- 争取朋友比打击敌人更重要
- 攻击敌人时只会被人看到你的丑恶
- 别人攻击你时，记得那些正在支持你的人

---

## 九、素材库搭建与搜索方法

### 五大素材文件夹
选题、脚本结构、开篇结尾、呈现方式、音乐

### 搜索渠道
- 选题搜索：抖音、快手、视频号
- 内容搜索：知乎、小红书
- 热点搜索：今日热榜、微博热搜、抖音热榜
- 关键词工具：巨量算数（关联分析→雷达图→关键词组合）
- 百度：行业词+关键词（时间/地点/人群/行业细分/年纪/场景）

### 对标账号方法
- 差异化策划、跨行业借鉴
- 跨领域、跨平台同领域、跨平台跨领域
- 同领域找选题，跨领域找呈现

### 开篇模板
- "我发现了一套{行业}作弊系统"
- "所有{人群}六大方法，{效果}算我输"
- 小红书笔记封面文字可当开篇

### 内容配比
赛道内容为主（90%），人群内容为辅（10%）

### 脚本类型
- 回忆→故事型脚本
- 行动→过程型脚本
- 分析→知识/观点型脚本

### 观点型选题公式
观点型选题 = 写作对象 + 人物关系 + 情绪


path: references/methodology.md
content: # 方法论知识库（Methodology Reference）

本文件整合自薛辉自媒体课程体系全部文档，作为内容生成时的核心参考。

---

## 目录
1. [账号定位方法论](#一账号定位方法论)
2. [爆款选题方法论](#二爆款选题方法论)
3. [八大爆款元素与选题指令](#三八大爆款元素与选题指令)
4. [钩子公式库](#四钩子公式库)
5. [文字语言基本功](#五文字语言基本功)
6. [时间的语法](#六时间的语法)
7. [用户·产品·营销](#七用户产品营销)
8. [自媒体的立场和姿态](#八自媒体的立场和姿态)
9. [素材库搭建与搜索方法](#九素材库搭建与搜索方法)

---

## 一、账号定位方法论

### 核心公式
账号定位 = 价值 × 用户 × 人设 × 类型 × 风格

### 网红 vs IP
- 网红被消遣，IP被追随
- 网红服务粉丝，IP影响粉丝
- 网红无法为商品赋能，IP可以为商品赋能

### 两大误区
1. **过度实用**（赛道工具人）→ 解决方案：用理念串联知识点，植入个人价值观
2. **过度抽象**（价值不落地）→ 解决方案：找到具体利他的内容，分享生活和价值观

### 提供价值的思路
- 有明确变现目的：免费的精神 → 服务某些用户
- 无明确变现目的：基于自身价值出发 → 解决一个人一个阶段的一个问题
- 让账号聚集一批渴望某些事物的人

### 六大竞争力策略
1. **正**（降维碾压）：全局优势、局部优势、细分优势
   - 流程细分：定位/文案/脚本/拍摄/剪辑
   - 行业细分：餐饮→烧烤店/奶茶店
   - 年龄细分：00后职业规划 / 35岁转型
   - 理念细分：性价比/品质感/服务特定审美
   - 地域细分：农村包围城市
2. **反**（否定对抗）：寻找敌人未满足的人群、沉默市场
3. **合**（交叉优势）：兴趣爱好+普世价值、特殊领域+普世价值、特殊场景+普世价值
   - 甲骨文+爱情、围棋+人生、美食+文化
   - 机车+美女、中医+治愈生活、律师+父女生活
   - 烧烤店→友情北漂、律师所→婚姻背叛
   - 价值叠加公式：理性壁垒 + 情感羁绊
4. **跨**（能力迁移）：一切生活哲学化、透过工作看能力
   - 产品经理教人生规划、英语配音演员教成年人英语
   - 老用户新价值：谁说女性成长就一定给中年女性讲
5. **借**（外部资源）：小众身份的专属视野、策划一个使命
   - 参考来源：电影、电视节目、书籍策划
6. **弱**（现身说法）：缺陷转使命、成为榜样、长期主义

---

## 二、爆款选题方法论

### 核心原则
> 选题要基于一种情绪、一种立场去表达，而不是单纯讲知识本身、讲事件本身。
> 没有新鲜度的选题是没有含金量的。

### 流量公式
流量大 = 需求量大（供小于求）+ 数据好（赛马机制动态竞争）

### 四大普世化切入点

1. **潜在需求 > 正在需求**
   - 错误："正山小种是最好的红茶"
   - 正确："三十五岁之后，大家才知道喝正山小种的好处"

2. **凑热闹 > 求知欲**
   - 错误："想要流量密码，就需要学会切片性"
   - 正确："我大胆预言，未来所有热门视频都会有切片性"

3. **八卦 > 痛点**
   - 错误："年轻人买房怎么选"
   - 正确："我搞房地产十多年见过最可笑可怜的买房行为"

4. **全人类共同情绪 > 事件本身**
   - 错误："健身的方法" → 正确："我们健身是为了让自己骄傲"
   - 错误："穿搭技巧" → 正确："女性的穿搭是自我取悦"
   - 错误："旗袍知识" → 正确："女性独立意识的觉醒从旗袍开始"

### 全人类共同情绪清单
社会声望、性、公民权、爱与浪漫、力量、体育运动、优越感、秩序、被安慰和被理解、独立、复仇、社会交往、个人魅力提升、家庭、好奇心、对不美好的厌恶、荣誉感、被社会排斥的恐惧

### 陌生化五要素
- 稀缺的景观、稀缺的情感、稀缺的美好、稀缺的信息和资讯、稀缺的奇闻逸事、稀缺的事件

### 赋比兴手法
- **赋**：重演绎细节，大量铺陈和堆叠的具体细节之美
- **比**：重在两者的并置，对立之美和结合之美
- **兴**：重在转化的过程，激发一种联想的转化之美

### 演绎技巧
1. **往内折叠**：深度聚焦具体的人与场景
   - 组合情境：产品×人群×场景×痛点
   - 预算越低效果越好（有限空间创造可能）
2. **设置障碍**：挑战、实验、测评
3. **置换元素**：置换时间/环境/道具/人物关系

### 并置技巧
不要讲一个事物，要讲一个事物与另一个事物之间的关系：
- 碰撞结合之美：建筑和文学、做饭和音乐、茶和瑜伽
- 对抗矛盾之美：轰轰烈烈vs细水长流、快乐教育vs严格教育
- 越是围绕二元对立展开，脚本越有整体性，观众越想知道答案

### 构建新概念
争夺命名权：构建模型、小众词汇、比喻造词、叠加创造新词
- 例：耦合式恋爱、生活颗粒度、集体日记

---

## 三、八大爆款元素与选题指令

### 使用方法
输入行业关键词，用以下八大元素生成选题：

1. **头牌**：世界上最贵的/明星的/最牛的/最贵的好在哪
2. **怀旧**：20年前经典的/古代人如何/小时候难忘的/当年最火的
3. **对立**：穷人vs富人/南方vs北方/男人vs女人/中国vs外国/古代vs现代
4. **最差**：贬值最快/最难吃/差评最多/最难看/最没面子/反人类设计
5. **荷尔蒙**：相亲成功率高/异性多看两眼/最具吸引力/一秒下头
6. **猎奇**：脑回路有病/外行不知道/黑心内幕/内行神操作/匪夷所思
7. **圈人群**：特定星座/内向外向/不同MBTI/身价十亿/第一次体验/弱势群体
8. **成本**：便宜又有面子/十分之一金钱/十分之一时间/如何偷懒/花小钱办大事

### 选题生成指令模板
```
你是一个帮我生成爆款选题的智能体，围绕"{元素名}"这个爆款元素。
行业：{用户行业}
请生成10条选题，要求：
1. 基于情绪和立场表达，不是纯知识
2. 有陌生化/新鲜感
3. 能激发全人类共同情绪
4. 适合短视频3秒留人
```

---

## 四、钩子公式库

### 核心6大钩子类型

#### 1. 圈定人群
公式 = 喊出人群标签 + 悬念/痛点/恐惧
- 身份标签：基础属性（宝妈/学生/微胖女生）、状态属性（负债者/创业小白）、情绪属性（焦虑的/迷茫的）
- 后半句类型：避坑警告型、颠覆认知型、悬念钩子型
- 完整结构：圈定人群→痛点警告→建立信任→解决方案→行动指令

#### 2. 直接提问
公式 = 疑问词 + 反常识/悬念/利益点
- 疑问词：你敢信/你知道吗/你有没有/如果…会怎样
- 后半句：反常识型、悬念型、紧迫型
- 高阶：提问+数字、提问+背书、提问+互动

#### 3. 自我否定
通过否定自身过往经验制造反差和可信度

#### 4. 数据冲击
用具体数字颠覆认知

#### 5. 场景代入
具体场景描绘+悬念

#### 6. 否定性力量
设定敌人、对抗纠结、先抑后扬

### 钩子质量检查清单
- 3秒内让用户觉得和自己有关
- 触发身份认同感
- 有后续钩子（不能只圈不定）
- 标签精准（不能模糊）
- 有背书（凭什么信你）

---

## 五、文字语言基本功

### 可视化原则
- 用"Do（做）句式"替代"Be（是）句式"
  - 错误："这是一份用可乐做成的肥牛"
  - 正确："我们给肥牛浇上可乐"
- "There be句式"：在百子湾的角落有一家红色招牌店…
- 特定/专属细节：个体经历→普遍经历
- 故事感（特定感）和故事的尖峰时刻

### 节奏感工具
- 押韵、排比、顶真、长短句交替

### 核心要求
- 移动端观看场景，日常直观的语言才能被听进去
- 好的文案就是一个特写镜头
- 画面感方法：描述、类比（抽象→具象）、力量感（对比数量/时间/经历/身份）

---

## 六、时间的语法

### 横向时间
1. **时序**：构建不同前提产生不同体验
2. **时长与密度**：详vs略、旁白概述vs现场展开
3. **停顿**：让张力悬置、让观众充分反应
4. **频率**：重复与差异
5. **否定性力量**：调动情绪最快——设定敌人、对抗纠结、先抑后扬
6. **绵延**：共鸣性推进

### 纵向时间
1. **空间**：高低宽窄、拥挤程度、材料质地
2. **面孔与肢体**

### 整体时间
1. **人称距离**：
   - 上帝视角（vlog）：客观公正全知
   - 第一人称：强烈情绪、内心秘密、暴露软肋
   - 第二人称：双刃剑，观众需扮演角色
   - 第三人称：语言和视觉都重要
   - 打破第四面墙
2. **视觉距离**：不拘泥位置角度，综合感受
3. **听点距离**：直接发声→出镜讲述→旁白回顾
4. **基调**：情感的距离
5. **时态**：时间的距离

---

## 七、用户·产品·营销

### 需求理论
- 用户有各种需求，每次购物产生不同需求重组
- 情境中出现冲突 → 产生需求
- 需求必须依附于情境

### 冲突分析框架
- 生理vs心理（渴望甜食但怕胖）
- 便利vs复杂（高科技产品有用但操作复杂）
- 喜欢vs担心（儿子喜欢但父母担心危险）

### 制造冲突三方向
1. **从消费者入手**：告诉观众老道理不对
2. **从对手入手**：竞品有什么问题
3. **从自我出发**：以前做法有什么不够好

### 效用层级
底线原则 → 够用就好 → 越多越好 → 意外惊喜

### 进攻策略
- **左脑**：理性分析、绝对真相
- **右脑**：情感、直觉、信仰、精神价值
- 精神价值：自由、掌控、地位、责任、不朽、冒险、归属、运势

### 记忆锚点
- 视觉锤、口语化命名、文字钉
- 基于熟悉：熟悉的诗词、文化符号、地名
- 叠化词：滴滴、哇哈哈

---

## 八、自媒体的立场和姿态

### 强者示弱
- 穿着贵气但讲话亲切
- 场景奢华但行为接地气
- 讲述成功但强调经历相似，复盘错误

### 弱者积极
- 可以可怜但不可以可恨
- 可以反对暴强，不可以反社会反人类
- 可以穷丑，不能肮脏
- 必须有真善美，必须乐观向上
- 负面情绪也敢自嘲

### 立场核心
> 永远想着去关心某些人群、没有给予照顾的人群

### 立场转移（拉近关系的关键）
商家→消费者、达人→厂家、老师→学生、领导→职员、妻子→丈夫、婆婆→媳妇、医生→患者

### 起号绝招
**找到弱势群体！领导他们！**

### 选题绝招
**找到没有被关心的人！对他们说话！**

### 团结原则
- 争取朋友比打击敌人更重要
- 攻击敌人时只会被人看到你的丑恶
- 别人攻击你时，记得那些正在支持你的人

---

## 九、素材库搭建与搜索方法

### 五大素材文件夹
选题、脚本结构、开篇结尾、呈现方式、音乐

### 搜索渠道
- 选题搜索：抖音、快手、视频号
- 内容搜索：知乎、小红书
- 热点搜索：今日热榜、微博热搜、抖音热榜
- 关键词工具：巨量算数（关联分析→雷达图→关键词组合）
- 百度：行业词+关键词（时间/地点/人群/行业细分/年纪/场景）

### 对标账号方法
- 差异化策划、跨行业借鉴
- 跨领域、跨平台同领域、跨平台跨领域
- 同领域找选题，跨领域找呈现

### 开篇模板
- "我发现了一套{行业}作弊系统"
- "所有{人群}六大方法，{效果}算我输"
- 小红书笔记封面文字可当开篇

### 内容配比
赛道内容为主（90%），人群内容为辅（10%）

### 脚本类型
- 回忆→故事型脚本
- 行动→过程型脚本
- 分析→知识/观点型脚本

### 观点型选题公式
观点型选题 = 写作对象 + 人物关系 + 情绪


path: scripts/auto_topic_post_xiaohongshu.sh
content: #!/bin/bash
# auto_topic_post_xiaohongshu.sh
# One-command flow: search-driven copywriting -> publish to Xiaohongshu
# Usage:
#   bash auto_topic_post_xiaohongshu.sh "主题"
#   bash auto_topic_post_xiaohongshu.sh "主题" "/tmp/openclaw/uploads/cover.jpg"

set -euo pipefail

TOPIC="${1:-}"
IMAGE_PATH="${2:-}"
ROOT_DIR="/Users/youke/.openclaw/workspace/skills/zeelin-xiaohongshu-autopost"
POST_SCRIPT="$ROOT_DIR/scripts/post_xiaohongshu.sh"

if [ -z "$TOPIC" ]; then
  echo "Error: topic is required"
  exit 1
fi

PROMPT="你是小红书爆文运营专家。围绕主题【${TOPIC}】先基于公开热点信号进行选题，再生成一篇软性带货小红书图文文案。必须包含人性驱动（痛点、损失厌恶、从众、即时收益）。\n\n要求：\n1) 先在脑内完成热点抽取与选题，选择最优角度；\n2) 标题14-20字，口语化，不夸张；\n3) 正文350-650字，结构：场景痛点->原因->3条解决路径->软性植入->结果->互动问题；\n4) 标签6-10个；\n5) 禁止硬广与绝对化承诺。\n\n输出格式必须严格如下，不要任何额外解释：\n<TITLE>标题</TITLE>\n<CONTENT>正文</CONTENT>\n<TAGS>标签1,标签2,标签3</TAGS>"

echo "Generating Xiaohongshu copy for topic: $TOPIC"
RESP_JSON="$(openclaw agent --agent main --message "$PROMPT" --json)"
TEXT="$(printf "%s" "$RESP_JSON" | jq -r '.result.payloads[0].text // empty')"

if [ -z "$TEXT" ]; then
  echo "Error: empty model output"
  exit 1
fi

TITLE="$(printf "%s" "$TEXT" | awk 'match($0, /<TITLE>.*<\/TITLE>/){s=substr($0,RSTART,RLENGTH); gsub(/<\/?TITLE>/, "", s); print s; exit}')"
CONTENT="$(printf "%s" "$TEXT" | awk '
  BEGIN{flag=0}
  /<CONTENT>/{
    flag=1
    sub(/.*<CONTENT>/, "")
  }
  flag{print}
  /<\/CONTENT>/{
    flag=0
  }
' | sed '$s:</CONTENT>.*::')"
TAGS_RAW="$(printf "%s" "$TEXT" | awk 'match($0, /<TAGS>.*<\/TAGS>/){s=substr($0,RSTART,RLENGTH); gsub(/<\/?TAGS>/, "", s); print s; exit}')"
TAGS_LINE="$(printf "%s" "$TAGS_RAW" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | sed '/^$/d' | awk '{print "#" $0}' | paste -sd ' ' -)"

if [ -z "$TITLE" ] || [ -z "$CONTENT" ]; then
  echo "Error: model output missing <TITLE> or <CONTENT>"
  echo "Raw output:"
  echo "$TEXT"
  exit 1
fi

FINAL_CONTENT="$CONTENT"
if [ -n "$TAGS_LINE" ]; then
  printf -v FINAL_CONTENT "%s\n\n%s" "$CONTENT" "$TAGS_LINE"
fi

mkdir -p "$ROOT_DIR/output"
TS="$(date +%Y%m%d_%H%M%S)"
DRAFT_FILE="$ROOT_DIR/output/draft_${TS}.md"
{
  echo "# $TITLE"
  echo
  echo "$FINAL_CONTENT"
} > "$DRAFT_FILE"

echo "Draft saved: $DRAFT_FILE"

echo "Publishing..."
if [ -n "$IMAGE_PATH" ]; then
  bash "$POST_SCRIPT" "$TITLE" "$FINAL_CONTENT" "$IMAGE_PATH"
else
  bash "$POST_SCRIPT" "$TITLE" "$FINAL_CONTENT"
fi

echo "Done: topic -> copy -> publish flow completed"


path: scripts/cdp_xhs_publish.py
content: #!/usr/bin/env python3
"""
Xiaohongshu creator — fill title + body via CDP, including content inside iframes.

- Walks Page.getFrameTree, uses Page.createIsolatedWorld + Runtime.evaluate per frame.
- After focus, uses Input.insertText (focus must be in the target frame).
- Does NOT click「发布」unless env XHS_AUTO_PUBLISH=1 (avoid accidental live posts).
"""
from __future__ import annotations

import json
import os
import sys
import time
import urllib.request

import websocket


def port() -> int:
    return int(os.environ.get("OPENCLAW_CDP_PORT", "9222"))


def list_tabs(p: int):
    with urllib.request.urlopen(f"http://127.0.0.1:{p}/json", timeout=8) as r:
        return json.loads(r.read())


def activate(p: int, tid: str):
    try:
        urllib.request.urlopen(f"http://127.0.0.1:{p}/json/activate/{tid}", timeout=5)
    except Exception:
        pass


def find_xhs(p: int):
    for t in list_tabs(p):
        if t.get("type") == "page" and "xiaohongshu.com" in t.get("url", ""):
            return t
    return None


def connect_ws(ws_url: str, p: int):
    return websocket.create_connection(ws_url, timeout=15, origin=f"http://127.0.0.1:{p}")


def cdp_send(ws, method: str, params=None, timeout=25):
    msg_id = int(time.time() * 1000) % 1_000_000_000
    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
    deadline = time.time() + timeout
    while time.time() < deadline:
        ws.settimeout(max(0.5, deadline - time.time()))
        try:
            raw = ws.recv()
        except websocket.WebSocketTimeoutException:
            continue
        data = json.loads(raw)
        if data.get("id") == msg_id:
            if "error" in data:
                raise RuntimeError(str(data["error"]))
            return data.get("result", {})
    raise TimeoutError(method)


def collect_frame_ids(tree: dict) -> list[str]:
    out: list[str] = []
    frame = tree.get("frame") or {}
    fid = frame.get("id")
    if fid:
        out.append(fid)
    for child in tree.get("childFrames") or []:
        out.extend(collect_frame_ids(child))
    return out


def create_ctx(ws, frame_id: str) -> int:
    r = cdp_send(
        ws,
        "Page.createIsolatedWorld",
        {
            "frameId": frame_id,
            "worldName": "__openclaw_xhs__",
            "grantUniverseAccess": True,
        },
    )
    return int(r["executionContextId"])


def js_eval_ctx(ws, execution_context_id: int, expr: str, timeout=20):
    r = cdp_send(
        ws,
        "Runtime.evaluate",
        {
            "expression": expr,
            "returnByValue": True,
            "awaitPromise": False,
            "executionContextId": execution_context_id,
        },
        timeout=timeout,
    )
    res = r.get("result", {})
    if res.get("type") == "string":
        return res.get("value", "")
    if res.get("type") == "boolean":
        return res.get("value", False)
    if res.get("type") == "number":
        return res.get("value", 0)
    return res.get("value", "")


def js_eval(ws, expr: str, timeout=20):
    r = cdp_send(
        ws,
        "Runtime.evaluate",
        {"expression": expr, "returnByValue": True, "awaitPromise": False},
        timeout=timeout,
    )
    res = r.get("result", {})
    if res.get("type") == "string":
        return res.get("value", "")
    if res.get("type") == "boolean":
        return res.get("value", False)
    if res.get("type") == "number":
        return res.get("value", 0)
    return res.get("value", "")


PROBE_JS = """
(() => {
    const inputs = document.querySelectorAll('input, textarea');
    const ce = document.querySelectorAll('[contenteditable="true"]');
    let titleLike = 0;
    for (const i of inputs) {
        const ph = (i.placeholder || '') + (i.getAttribute('aria-label') || '');
        if (/标题|topic|title/i.test(ph) || i.maxLength === 20) titleLike++;
    }
    return JSON.stringify({
        inputs: inputs.length,
        ce: ce.length,
        titleLike: titleLike,
        path: location.pathname,
        href: location.href.substring(0, 80)
    });
})()
"""

FOCUS_TITLE_JS = """
(() => {
    const bad = new Set(['hidden', 'file', 'checkbox', 'radio', 'submit']);
    const sels = [
        'textarea[placeholder*="输入标题"]',
        'textarea[placeholder*="标题"]',
        'textarea.d-text[maxlength="64"]',
        'textarea.d-text',
        '.rich-editor-title textarea',
        'input[placeholder*="标题"]',
        'input[maxlength="20"]',
    ];
    for (const s of sels) {
        for (const el of document.querySelectorAll(s)) {
            if (!el.offsetParent) continue;
            if (el.tagName === 'INPUT' && bad.has(el.type)) continue;
            el.focus();
            el.click();
            return "OK_TITLE:" + s;
        }
    }
    const vis = [...document.querySelectorAll("textarea, input")].filter(
        i => i.offsetParent && !bad.has(i.type) && i.type !== 'file');
    if (vis[0]) {
        vis[0].focus();
        vis[0].click();
        return "OK_TITLE:first-visible";
    }
    return "ERROR:NO_TITLE";
})()
"""

FOCUS_BODY_JS = """
(() => {
    const sels = [
        '.tiptap.ProseMirror',
        '.ProseMirror[contenteditable="true"]',
        '.ql-editor',
        '.note-editor [contenteditable="true"]',
        '[data-placeholder*="正文"]',
        '[data-placeholder*="分享"]',
        'div.d-input-editor [contenteditable="true"]',
        '[contenteditable="true"]',
        'textarea'
    ];
    for (const s of sels) {
        for (const el of document.querySelectorAll(s)) {
            if (!el.offsetParent) continue;
            const h = el.getBoundingClientRect().height;
            if (s === '[contenteditable="true"]' && h < 30) continue;
            el.focus();
            el.click();
            return "OK_BODY:" + s;
        }
    }
    return "ERROR:NO_BODY";
})()
"""

CLICK_FORMAT_JS = """
(() => {
    for (const b of document.querySelectorAll('button')) {
        const t = (b.textContent || '').trim();
        if (t === '一键排版' && b.offsetParent) { b.click(); return 'CLICK'; }
    }
    return 'MISS';
})()
"""

CLICK_NEXT_JS = """
(() => {
    for (const b of document.querySelectorAll('button')) {
        const t = (b.textContent || '').trim();
        if (t === '下一步' && b.offsetParent) { b.click(); return 'CLICK'; }
    }
    return 'MISS';
})()
"""

PUBLISH_JS = """
(() => {
    // Only match exact "发布" button, NOT sidebar "发布笔记"
    for (const n of document.querySelectorAll('button')) {
        if (!n.offsetParent) continue;
        const t = (n.textContent || '').trim();
        if (t === '发布') {
            n.click();
            return 'CLICK:发布';
        }
    }
    return 'SKIP';
})()
"""

CONFIRM_DIALOG_JS = """
(() => {
    const labels = ['确认发布', '确认', '继续发布', '确定发布', '确定', '立即发布'];
    for (const n of document.querySelectorAll('button, [role="button"], div[class*="btn"], span[class*="btn"]')) {
        if (!n.offsetParent) continue;
        const t = (n.textContent || '').trim();
        for (const lb of labels) {
            if (t === lb) {
                n.click();
                return 'CONFIRM:' + t;
            }
        }
    }
    for (const n of document.querySelectorAll('button, [role="button"], div[class*="btn"], span[class*="btn"]')) {
        if (!n.offsetParent) continue;
        const t = (n.textContent || '').trim();
        for (const lb of labels) {
            if (t.includes(lb)) {
                n.click();
                return 'CONFIRM_PARTIAL:' + t;
            }
        }
    }
    return 'NO_DIALOG';
})()
"""

CHECK_SUCCESS_JS = """
(() => {
    const text = document.body.innerText || '';
    if (/发布成功|笔记已发布|审核中|内容已提交|已发布/.test(text)) return 'SUCCESS';
    const url = location.href;
    if (/\/notes|\/published|note-manage/.test(url)) return 'SUCCESS_URL';
    return 'STILL_ON_PAGE';
})()
"""


def score_probe(probe_str: str) -> int:
    try:
        o = json.loads(probe_str)
        return int(o.get("ce", 0)) * 10 + int(o.get("titleLike", 0)) * 5 + int(o.get("inputs", 0))
    except Exception:
        return 0


def best_frame_for_editor(ws) -> str | None:
    """Returns frame_id for the subtree that looks most like the editor."""
    tree = cdp_send(ws, "Page.getFrameTree", {})
    frame_tree = tree.get("frameTree", {})
    ids = collect_frame_ids(frame_tree)
    cdp_send(ws, "Runtime.enable", {})

    best_score = -1
    best_fid: str | None = None

    for fid in ids:
        try:
            ctx = create_ctx(ws, fid)
            probe = js_eval_ctx(ws, ctx, PROBE_JS)
            sc = score_probe(probe)
            if sc > best_score:
                best_score = sc
                best_fid = fid
        except Exception:
            continue

    if best_fid is None or best_score < 1:
        return None
    return best_fid


CLICK_NEW_ARTICLE_JS = """
(() => {
    // Prefer the <button> with exact text
    for (const n of document.querySelectorAll('button')) {
        const t = (n.textContent || '').trim();
        if (t === '新的创作' && n.offsetParent) { n.click(); return 'CLICK_BTN'; }
    }
    // Fallback: any clickable with exact match
    for (const n of document.querySelectorAll('div,span,a')) {
        const t = (n.textContent || '').trim();
        if (t === '新的创作' && n.offsetParent) { n.click(); return 'CLICK_OTHER'; }
    }
    return 'MISS';
})()
"""

def prepare_page(ws):
    """Navigate to the XHS long-article editor: publish page → '写长文' tab → '新的创作'."""
    cdp_send(
        ws,
        "Page.navigate",
        {"url": "https://creator.xiaohongshu.com/publish/publish?source=official&from=menu&target=article"},
    )
    time.sleep(3)
    cdp_send(ws, "Page.reload", {})
    time.sleep(6)
    print("Nav to article page:", js_eval(ws, "location.href"))

    # Click 新的创作 (with retry)
    for attempt in range(4):
        r = js_eval(ws, CLICK_NEW_ARTICLE_JS)
        print(f"Click 新的创作 (attempt {attempt+1}):", r)
        if "CLICK" in str(r):
            break
        time.sleep(3)

    time.sleep(4)
    probe = js_eval(ws, PROBE_JS)
    print("Editor probe:", probe)


def type_into_focused(ws, text: str):
    cdp_send(ws, "Input.dispatchKeyEvent", {"type": "keyDown", "key": "a", "code": "KeyA", "modifiers": 2})
    cdp_send(ws, "Input.dispatchKeyEvent", {"type": "keyUp", "key": "a", "code": "KeyA", "modifiers": 2})
    cdp_send(ws, "Input.dispatchKeyEvent", {"type": "keyDown", "key": "Backspace", "code": "Backspace"})
    cdp_send(ws, "Input.dispatchKeyEvent", {"type": "keyUp", "key": "Backspace", "code": "Backspace"})
    time.sleep(0.12)
    cdp_send(ws, "Input.insertText", {"text": text})
    js_eval(ws, """
    (() => {
        const el = document.activeElement;
        if (el) {
            el.dispatchEvent(new Event('input', {bubbles:true}));
            el.dispatchEvent(new Event('change', {bubbles:true}));
        }
    })()
    """)


def fill_in_context(ws, ctx_id: int, title: str, body: str) -> bool:
    r1 = js_eval_ctx(ws, ctx_id, FOCUS_TITLE_JS)
    print("(iframe) Title focus:", r1)
    if "OK" not in str(r1):
        return False
    type_into_focused(ws, title)
    time.sleep(0.4)

    r2 = js_eval_ctx(ws, ctx_id, FOCUS_BODY_JS)
    print("(iframe) Body focus:", r2)
    if "OK" not in str(r2):
        return False
    type_into_focused(ws, body)
    return True


def fill_in_main(ws, title: str, body: str) -> bool:
    r1 = js_eval(ws, FOCUS_TITLE_JS)
    print("Title focus:", r1)
    if "OK" not in str(r1):
        return False
    type_into_focused(ws, title)
    time.sleep(0.4)
    r2 = js_eval(ws, FOCUS_BODY_JS)
    print("Body focus:", r2)
    if "OK" not in str(r2):
        return False
    type_into_focused(ws, body)
    return True


def main():
    if len(sys.argv) < 3:
        print("Usage: cdp_xhs_ops.py <title> <body>", file=sys.stderr)
        sys.exit(1)

    title = sys.argv[1]
    body = sys.argv[2]
    p = port()
    tab = find_xhs(p)
    if not tab:
        print("ERROR: No xiaohongshu.com tab in CDP Chrome.", file=sys.stderr)
        sys.exit(1)

    activate(p, tab["id"])
    time.sleep(0.3)
    ws = connect_ws(tab["webSocketDebuggerUrl"], p)
    try:
        cdp_send(ws, "Page.enable", {})
        prepare_page(ws)

        ok = fill_in_main(ws, title, body)
        if not ok:
            print("Main frame incomplete, trying iframe contexts...")
            fid = best_frame_for_editor(ws)
            if fid:
                print(f"Using iframe frameId={fid[:16]}...")
                ctx_id = create_ctx(ws, fid)
                ok = fill_in_context(ws, ctx_id, title, body)
            if not ok:
                print("ERROR: Could not fill title/body.", file=sys.stderr)
                sys.exit(1)

        time.sleep(0.8)
        no_publish = os.environ.get("XHS_NO_PUBLISH", "").strip() in ("1", "true", "yes")
        if no_publish:
            print("XHS: content filled. Skipping live Ops (XHS_NO_PUBLISH=1).")
        else:
            # Step 1: 一键排版 → template selection
            fmt_r = js_eval(ws, CLICK_FORMAT_JS)
            print("一键排版:", fmt_r)
            time.sleep(8)

            # Step 2: 下一步 → publish settings (with retry)
            for step_try in range(5):
                js_eval(ws, "(function(){window.scrollTo(0,document.body.scrollHeight);return 1;})()")
                time.sleep(1)
                next_r = js_eval(ws, CLICK_NEXT_JS)
                print(f"下一步 (attempt {step_try+1}):", next_r)
                if "CLICK" in str(next_r):
                    break
                time.sleep(3)
            time.sleep(5)

            # Step 3: 发布
            js_eval(ws, "(function(){window.scrollTo(0,document.body.scrollHeight);return 'SCROLLED';})()")
            time.sleep(1)
            r3 = js_eval(ws, PUBLISH_JS)
            print("Submit click (platform UI 发布):", r3)

            success = False
            for attempt in range(5):
                time.sleep(3)
                confirm_r = js_eval(ws, CONFIRM_DIALOG_JS)
                print(f"  Confirm dialog (attempt {attempt+1}):", confirm_r)
                if "CONFIRM" in str(confirm_r):
                    time.sleep(3)

                check_r = js_eval(ws, CHECK_SUCCESS_JS)
                print(f"  Status:", check_r)
                if "SUCCESS" in str(check_r):
                    success = True
                    break

                if "STILL_ON_PAGE" in str(check_r) and attempt < 4:
                    js_eval(ws, PUBLISH_JS)
                    time.sleep(1)
                    js_eval(ws, CONFIRM_DIALOG_JS)
                    print(f"  Retry submit+confirm (attempt {attempt+2})")

            if success:
                print("Xiaohongshu Ops SUCCESS.")
            else:
                print("WARN: Ops may not have completed. Check creator center.", file=sys.stderr)
        print("Xiaohongshu Ops flow completed.")
    finally:
        ws.close()


if __name__ == "__main__":
    main()


path: scripts/cdp_xhs_publish_v2.py
content: #!/usr/bin/env python3
"""
Improved Xiaohongshu CDP publisher using Input.insertText (works with React editors)
"""
import sys, json, requests, websocket, time

if len(sys.argv) < 3:
    print("Usage: cdp_xhs_publish_v2.py <title> <body>")
    sys.exit(1)

TITLE=sys.argv[1]
BODY=sys.argv[2]
CDP="http://127.0.0.1:9222/json"

def find_tab():
    for t in requests.get(CDP).json():
        if "xiaohongshu" in t.get("url",""):
            return t
    return None


def send(ws,method,params=None):
    ws.send(json.dumps({"id":1,"method":method,"params":params or {}}))
    return json.loads(ws.recv())


tab=find_tab()
if not tab:
    raise SystemExit("No Xiaohongshu tab found")

ws=websocket.create_connection(tab["webSocketDebuggerUrl"])

# bring tab front
send(ws,"Page.bringToFront")

# focus title
focus_title="""
(function(){
 const el=document.querySelector('input');
 if(el){el.focus();return true;} return false;
})();
"""
send(ws,"Runtime.evaluate",{"expression":focus_title})

# type title
send(ws,"Input.insertText",{"text":TITLE})

time.sleep(1)

# focus editor
focus_body="""
(function(){
 const ed=document.querySelector('[contenteditable], textarea, div[class*=editor]');
 if(ed){ed.focus();return true;} return false;
})();
"""
send(ws,"Runtime.evaluate",{"expression":focus_body})

send(ws,"Input.insertText",{"text":"\n"+BODY})

print("XHS text inserted. Publish manually or extend script to auto-click.")

path: scripts/cdp_xhs_publish_v3.py
content: #!/usr/bin/env python3
"""
Xiaohongshu CDP publisher v3
Implements full workflow:
1. Click 发布笔记
2. Click 写长文
3. Fill title (<=64 chars)
4. Fill body
5. Click 一键排版 -> 简约基础
6. Click 下一步
7. Click 发布

Requires Chrome started with --remote-debugging-port=9222
and user logged into https://creator.xiaohongshu.com
"""

import sys, json, time, requests, websocket

if len(sys.argv) < 3:
    print("Usage: cdp_xhs_publish_v3.py <title> <body>")
    sys.exit(1)

TITLE=sys.argv[1][:64]
BODY=sys.argv[2]
CDP="http://127.0.0.1:9222/json"


def find_tab():
    for t in requests.get(CDP).json():
        if "xiaohongshu" in t.get("url",""):
            return t
    return None


def send(ws,method,params=None):
    ws.send(json.dumps({"id":1,"method":method,"params":params or {}}))
    return json.loads(ws.recv())


tab=find_tab()
if not tab:
    raise SystemExit("No Xiaohongshu tab found")

ws=websocket.create_connection(tab["webSocketDebuggerUrl"])

send(ws,"Page.bringToFront")

def click_text(text):
    script=f"""
    (function(){{
      const nodes=[...document.querySelectorAll('button,div,span')];
      for(const n of nodes){{
        if(n.innerText && n.innerText.includes('{text}')){{
          n.click();
          return true;
        }}
      }}
      return false;
    }})();
    """
    send(ws,"Runtime.evaluate",{"expression":script})

# Step1 发布笔记
click_text("发布笔记")
time.sleep(2)

# Step2 写长文
click_text("写长文")
time.sleep(3)

# Step3 填标题
send(ws,"Runtime.evaluate",{"expression":"document.querySelector('input')?.focus()"})
send(ws,"Input.insertText",{"text":TITLE})

time.sleep(1)

# Step4 填正文
send(ws,"Runtime.evaluate",{"expression":"document.querySelector('[contenteditable],textarea,div[class*=editor]')?.focus()"})
send(ws,"Input.insertText",{"text":"\n"+BODY})

time.sleep(1)

# Step5 一键排版
click_text("一键排版")
time.sleep(1)
click_text("简约")
time.sleep(1)

# Step6 下一步
click_text("下一步")
time.sleep(2)

# Step7 发布
click_text("发布")

print("Xiaohongshu publish flow executed")


path: scripts/cdp_xhs_publish_v4.py
content: #!/usr/bin/env python3
import sys, json, time, requests, websocket

if len(sys.argv) < 3:
    print("Usage: cdp_xhs_publish_v4.py <title> <body>")
    sys.exit(1)

TITLE = sys.argv[1][:64]
BODY = sys.argv[2]

CDP = "http://127.0.0.1:9222/json"

def find_tab():
    for t in requests.get(CDP).json():
        if "xiaohongshu" in t.get("url",""):
            return t
    return None

def send(ws,method,params=None):
    ws.send(json.dumps({"id":1,"method":method,"params":params or {}}))
    return json.loads(ws.recv())

tab=find_tab()
if not tab:
    raise SystemExit("No Xiaohongshu tab found")

ws=websocket.create_connection(tab["webSocketDebuggerUrl"])

send(ws,"Page.bringToFront")

def click_text(text,timeout=10):
    start=time.time()
    while time.time()-start<timeout:
        script=f"""
        (function(){{
          const nodes=[...document.querySelectorAll('button,div,span')];
          for(const n of nodes){{
            if(n.innerText && n.innerText.includes('{text}')){{
              n.click();
              return true;
            }}
          }}
          return false;
        }})();
        """
        send(ws,"Runtime.evaluate",{"expression":script})
        time.sleep(1)

# try direct path
click_text("写长文")
click_text("发布笔记")
time.sleep(1)
click_text("写长文")

# new creation
click_text("新的创作")
time.sleep(3)

# title
send(ws,"Runtime.evaluate",{"expression":"document.querySelector('input')?.focus()"})
send(ws,"Input.insertText",{"text":TITLE})

time.sleep(1)

# body
send(ws,"Runtime.evaluate",{"expression":"document.querySelector('[contenteditable],textarea,div[class*=editor]')?.focus()"})
send(ws,"Input.insertText",{"text":"\n"+BODY})

time.sleep(1)

# layout
click_text("一键排版")
time.sleep(1)
click_text("简约")

# next
click_text("下一步")
time.sleep(2)

# publish
click_text("发布")

print("Xiaohongshu publish flow executed")


path: scripts/cdp_xhs_publish_v5.py
content: #!/usr/bin/env python3
"""
Xiaohongshu creator — fill title + body via CDP, including content inside iframes.

- Walks Page.getFrameTree, uses Page.createIsolatedWorld + Runtime.evaluate per frame.
- After focus, uses Input.insertText (focus must be in the target frame).
- Does NOT click「发布」unless env XHS_AUTO_PUBLISH=1 (avoid accidental live posts).
"""
from __future__ import annotations

import json
import os
import sys
import time
import urllib.request

import websocket


SIGNATURE = "\n\n本推文由Auto Ops自动发布"


def port() -> int:
    return int(os.environ.get("OPENCLAW_CDP_PORT", "9222"))


def list_tabs(p: int):
    with urllib.request.urlopen(f"http://127.0.0.1:{p}/json", timeout=8) as r:
        return json.loads(r.read())


def activate(p: int, tid: str):
    try:
        urllib.request.urlopen(f"http://127.0.0.1:{p}/json/activate/{tid}", timeout=5)
    except Exception:
        pass


def find_xhs(p: int):
    for t in list_tabs(p):
        if t.get("type") == "page" and "xiaohongshu.com" in t.get("url", ""):
            return t
    return None


def connect_ws(ws_url: str, p: int):
    return websocket.create_connection(ws_url, timeout=15, origin=f"http://127.0.0.1:{p}")


def cdp_send(ws, method: str, params=None, timeout=25):
    msg_id = int(time.time() * 1000) % 1_000_000_000
    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
    deadline = time.time() + timeout
    while time.time() < deadline:
        ws.settimeout(max(0.5, deadline - time.time()))
        try:
            raw = ws.recv()
        except websocket.WebSocketTimeoutException:
            continue
        data = json.loads(raw)
        if data.get("id") == msg_id:
            if "error" in data:
                raise RuntimeError(str(data["error"]))
            return data.get("result", {})
    raise TimeoutError(method)


def collect_frame_ids(tree: dict) -> list[str]:
    out: list[str] = []
    frame = tree.get("frame") or {}
    fid = frame.get("id")
    if fid:
        out.append(fid)
    for child in tree.get("childFrames") or []:
        out.extend(collect_frame_ids(child))
    return out


def create_ctx(ws, frame_id: str) -> int:
    r = cdp_send(
        ws,
        "Page.createIsolatedWorld",
        {
            "frameId": frame_id,
            "worldName": "__openclaw_xhs__",
            "grantUniverseAccess": True,
        },
    )
    return int(r["executionContextId"])


def js_eval_ctx(ws, execution_context_id: int, expr: str, timeout=20):
    r = cdp_send(
        ws,
        "Runtime.evaluate",
        {
            "expression": expr,
            "returnByValue": True,
            "awaitPromise": False,
            "executionContextId": execution_context_id,
        },
        timeout=timeout,
    )
    res = r.get("result", {})
    if res.get("type") == "string":
        return res.get("value", "")
    if res.get("type") == "boolean":
        return res.get("value", False)
    if res.get("type") == "number":
        return res.get("value", 0)
    return res.get("value", "")


def js_eval(ws, expr: str, timeout=20):
    r = cdp_send(
        ws,
        "Runtime.evaluate",
        {"expression": expr, "returnByValue": True, "awaitPromise": False},
        timeout=timeout,
    )
    res = r.get("result", {})
    if res.get("type") == "string":
        return res.get("value", "")
    if res.get("type") == "boolean":
        return res.get("value", False)
    if res.get("type") == "number":
        return res.get("value", 0)
    return res.get("value", "")


PROBE_JS = """
(() => {
    const inputs = document.querySelectorAll('input, textarea');
    const ce = document.querySelectorAll('[contenteditable="true"]');
    let titleLike = 0;
    for (const i of inputs) {
        const ph = (i.placeholder || '') + (i.getAttribute('aria-label') || '');
        if (/标题|topic|title/i.test(ph) || i.maxLength === 20) titleLike++;
    }
    return JSON.stringify({
        inputs: inputs.length,
        ce: ce.length,
        titleLike: titleLike,
        path: location.pathname,
        href: location.href.substring(0, 80)
    });
})()
"""

FOCUS_TITLE_JS = """
(() => {
    const bad = new Set(['hidden', 'file', 'checkbox', 'radio', 'submit']);
    const sels = [
        'textarea[placeholder*="输入标题"]',
        'textarea[placeholder*="标题"]',
        'textarea.d-text[maxlength="64"]',
        'textarea.d-text',
        '.rich-editor-title textarea',
        'input[placeholder*="标题"]',
        'input[maxlength="20"]',
    ];
    for (const s of sels) {
        for (const el of document.querySelectorAll(s)) {
            if (!el.offsetParent) continue;
            if (el.tagName === 'INPUT' && bad.has(el.type)) continue;
            el.focus();
            el.click();
            return "OK_TITLE:" + s;
        }
    }
    const vis = [...document.querySelectorAll("textarea, input")].filter(
        i => i.offsetParent && !bad.has(i.type) && i.type !== 'file');
    if (vis[0]) {
        vis[0].focus();
        vis[0].click();
        return "OK_TITLE:first-visible";
    }
    return "ERROR:NO_TITLE";
})()
"""

FOCUS_BODY_JS = """
(() => {
    const sels = [
        '.tiptap.ProseMirror',
        '.ProseMirror[contenteditable="true"]',
        '.ql-editor',
        '.note-editor [contenteditable="true"]',
        '[data-placeholder*="正文"]',
        '[data-placeholder*="分享"]',
        'div.d-input-editor [contenteditable="true"]',
        '[contenteditable="true"]',
        'textarea'
    ];
    for (const s of sels) {
        for (const el of document.querySelectorAll(s)) {
            if (!el.offsetParent) continue;
            const h = el.getBoundingClientRect().height;
            if (s === '[contenteditable="true"]' && h < 30) continue;
            el.focus();
            el.click();
            return "OK_BODY:" + s;
        }
    }
    return "ERROR:NO_BODY";
})()
"""

CLICK_FORMAT_JS = """
(() => {
    for (const b of document.querySelectorAll('button')) {
        const t = (b.textContent || '').trim();
        if (t === '一键排版' && b.offsetParent) { b.click(); return 'CLICK'; }
    }
    return 'MISS';
})()
"""

CLICK_NEXT_JS = """
(() => {
    for (const b of document.querySelectorAll('button')) {
        const t = (b.textContent || '').trim();
        if (t === '下一步' && b.offsetParent) { b.click(); return 'CLICK'; }
    }
    return 'MISS';
})()
"""

PUBLISH_JS = """
(() => {
    // Only match exact "发布" button, NOT sidebar "发布笔记"
    for (const n of document.querySelectorAll('button')) {
        if (!n.offsetParent) continue;
        const t = (n.textContent || '').trim();
        if (t === '发布') {
            n.click();
            return 'CLICK:发布';
        }
    }
    return 'SKIP';
})()
"""

CONFIRM_DIALOG_JS = """
(() => {
    const labels = ['确认发布', '确认', '继续发布', '确定发布', '确定', '立即发布'];
    for (const n of document.querySelectorAll('button, [role="button"], div[class*="btn"], span[class*="btn"]')) {
        if (!n.offsetParent) continue;
        const t = (n.textContent || '').trim();
        for (const lb of labels) {
            if (t === lb) {
                n.click();
                return 'CONFIRM:' + t;
            }
        }
    }
    for (const n of document.querySelectorAll('button, [role="button"], div[class*="btn"], span[class*="btn"]')) {
        if (!n.offsetParent) continue;
        const t = (n.textContent || '').trim();
        for (const lb of labels) {
            if (t.includes(lb)) {
                n.click();
                return 'CONFIRM_PARTIAL:' + t;
            }
        }
    }
    return 'NO_DIALOG';
})()
"""

CHECK_SUCCESS_JS = """
(() => {
    const text = document.body.innerText || '';
    if (/发布成功|笔记已发布|审核中|内容已提交|已发布/.test(text)) return 'SUCCESS';
    const url = location.href;
    if (/\/notes|\/published|note-manage/.test(url)) return 'SUCCESS_URL';
    return 'STILL_ON_PAGE';
})()
"""


def score_probe(probe_str: str) -> int:
    try:
        o = json.loads(probe_str)
        return int(o.get("ce", 0)) * 10 + int(o.get("titleLike", 0)) * 5 + int(o.get("inputs", 0))
    except Exception:
        return 0


def best_frame_for_editor(ws) -> str | None:
    """Returns frame_id for the subtree that looks most like the editor."""
    tree = cdp_send(ws, "Page.getFrameTree", {})
    frame_tree = tree.get("frameTree", {})
    ids = collect_frame_ids(frame_tree)
    cdp_send(ws, "Runtime.enable", {})

    best_score = -1
    best_fid: str | None = None

    for fid in ids:
        try:
            ctx = create_ctx(ws, fid)
            probe = js_eval_ctx(ws, ctx, PROBE_JS)
            sc = score_probe(probe)
            if sc > best_score:
                best_score = sc
                best_fid = fid
        except Exception:
            continue

    if best_fid is None or best_score < 1:
        return None
    return best_fid


CLICK_NEW_ARTICLE_JS = """
(() => {
    // Prefer the <button> with exact text
    for (const n of document.querySelectorAll('button')) {
        const t = (n.textContent || '').trim();
        if (t === '新的创作' && n.offsetParent) { n.click(); return 'CLICK_BTN'; }
    }
    // Fallback: any clickable with exact match
    for (const n of document.querySelectorAll('div,span,a')) {
        const t = (n.textContent || '').trim();
        if (t === '新的创作' && n.offsetParent) { n.click(); return 'CLICK_OTHER'; }
    }
    return 'MISS';
})()
"""

def prepare_page(ws):
    """Navigate to the XHS long-article editor: publish page → '写长文' tab → '新的创作'."""
    cdp_send(
        ws,
        "Page.navigate",
        {"url": "https://creator.xiaohongshu.com/publish/publish?source=official&from=menu&target=article"},
    )
    time.sleep(3)
    cdp_send(ws, "Page.reload", {})
    time.sleep(6)
    print("Nav to article page:", js_eval(ws, "location.href"))

    # Click 新的创作 (with retry)
    for attempt in range(4):
        r = js_eval(ws, CLICK_NEW_ARTICLE_JS)
        print(f"Click 新的创作 (attempt {attempt+1}):", r)
        if "CLICK" in str(r):
            break
        time.sleep(3)

    time.sleep(4)
    probe = js_eval(ws, PROBE_JS)
    print("Editor probe:", probe)


def type_into_focused(ws, text: str):
    cdp_send(ws, "Input.dispatchKeyEvent", {"type": "keyDown", "key": "a", "code": "KeyA", "modifiers": 2})
    cdp_send(ws, "Input.dispatchKeyEvent", {"type": "keyUp", "key": "a", "code": "KeyA", "modifiers": 2})
    cdp_send(ws, "Input.dispatchKeyEvent", {"type": "keyDown", "key": "Backspace", "code": "Backspace"})
    cdp_send(ws, "Input.dispatchKeyEvent", {"type": "keyUp", "key": "Backspace", "code": "Backspace"})
    time.sleep(0.12)
    cdp_send(ws, "Input.insertText", {"text": text})
    js_eval(ws, """
    (() => {
        const el = document.activeElement;
        if (el) {
            el.dispatchEvent(new Event('input', {bubbles:true}));
            el.dispatchEvent(new Event('change', {bubbles:true}));
        }
    })()
    """)


def fill_in_context(ws, ctx_id: int, title: str, body: str) -> bool:
    r1 = js_eval_ctx(ws, ctx_id, FOCUS_TITLE_JS)
    print("(iframe) Title focus:", r1)
    if "OK" not in str(r1):
        return False
    type_into_focused(ws, title)
    time.sleep(0.4)

    r2 = js_eval_ctx(ws, ctx_id, FOCUS_BODY_JS)
    print("(iframe) Body focus:", r2)
    if "OK" not in str(r2):
        return False
    type_into_focused(ws, body)
    return True


def fill_in_main(ws, title: str, body: str) -> bool:
    r1 = js_eval(ws, FOCUS_TITLE_JS)
    print("Title focus:", r1)
    if "OK" not in str(r1):
        return False
    type_into_focused(ws, title)
    time.sleep(0.4)
    r2 = js_eval(ws, FOCUS_BODY_JS)
    print("Body focus:", r2)
    if "OK" not in str(r2):
        return False
    type_into_focused(ws, body)
    return True


def main():
    if len(sys.argv) < 3:
        print("Usage: cdp_xhs_ops.py <title> <body>", file=sys.stderr)
        sys.exit(1)

    title = sys.argv[1]
    body = (sys.argv[2] or "").strip()
    p = port()
    tab = find_xhs(p)
    if not tab:
        print("ERROR: No xiaohongshu.com tab in CDP Chrome.", file=sys.stderr)
        sys.exit(1)

    activate(p, tab["id"])
    time.sleep(0.3)
    ws = connect_ws(tab["webSocketDebuggerUrl"], p)
    try:
        cdp_send(ws, "Page.enable", {})
        prepare_page(ws)

        ok = fill_in_main(ws, title, body)
        if not ok:
            print("Main frame incomplete, trying iframe contexts...")
            fid = best_frame_for_editor(ws)
            if fid:
                print(f"Using iframe frameId={fid[:16]}...")
                ctx_id = create_ctx(ws, fid)
                ok = fill_in_context(ws, ctx_id, title, body)
            if not ok:
                print("ERROR: Could not fill title/body.", file=sys.stderr)
                sys.exit(1)

        time.sleep(0.8)
        no_publish = os.environ.get("XHS_NO_PUBLISH", "").strip() in ("1", "true", "yes")
        if no_publish:
            print("XHS: content filled. Skipping live Ops (XHS_NO_PUBLISH=1).")
        else:
            # Step 1: 一键排版 → template selection
            fmt_r = js_eval(ws, CLICK_FORMAT_JS)
            print("一键排版:", fmt_r)
            time.sleep(8)

            # Step 2: 下一步 → publish settings (with retry)
            for step_try in range(5):
                js_eval(ws, "(function(){window.scrollTo(0,document.body.scrollHeight);return 1;})()")
                time.sleep(1)
                next_r = js_eval(ws, CLICK_NEXT_JS)
                print(f"下一步 (attempt {step_try+1}):", next_r)
                if "CLICK" in str(next_r):
                    break
                time.sleep(3)
            time.sleep(5)

            # Step 3: 发布
            js_eval(ws, "(function(){window.scrollTo(0,document.body.scrollHeight);return 'SCROLLED';})()")
            time.sleep(1)
            r3 = js_eval(ws, PUBLISH_JS)
            print("Submit click (platform UI 发布):", r3)

            success = False
            for attempt in range(5):
                time.sleep(3)
                confirm_r = js_eval(ws, CONFIRM_DIALOG_JS)
                print(f"  Confirm dialog (attempt {attempt+1}):", confirm_r)
                if "CONFIRM" in str(confirm_r):
                    time.sleep(3)

                check_r = js_eval(ws, CHECK_SUCCESS_JS)
                print(f"  Status:", check_r)
                if "SUCCESS" in str(check_r):
                    success = True
                    break

                if "STILL_ON_PAGE" in str(check_r) and attempt < 4:
                    js_eval(ws, PUBLISH_JS)
                    time.sleep(1)
                    js_eval(ws, CONFIRM_DIALOG_JS)
                    print(f"  Retry submit+confirm (attempt {attempt+2})")

            if success:
                print("Xiaohongshu Ops SUCCESS.")
            else:
                print("WARN: Ops may not have completed. Check creator center.", file=sys.stderr)
        print("Xiaohongshu Ops flow completed.")
    finally:
        ws.close()


if __name__ == "__main__":
    main()


path: scripts/post_xiaohongshu.sh
content: #!/bin/bash
set -u -o pipefail

TITLE="${1:-}"
CONTENT="${2:-}"
IMAGE_PATH="${3:-/Users/youke/.openclaw/workspace/skills/zeelin-xiaohongshu-autopost/assets/default_cover.jpg}"
CLI="${OPENCLAW_CLI:-agent-browser}"
BASE_URL="https://creator.xiaohongshu.com/new/home"

if [ -z "$TITLE" ] || [ -z "$CONTENT" ]; then
  echo "Error: title and content are required."
  exit 1
fi

TITLE_SAFE="$(printf '%s' "$TITLE" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')"
TITLE_SAFE="${TITLE_SAFE:0:64}"

get_snapshot() {
  local s
  s="$($CLI snapshot 2>/dev/null || true)"
  [ -z "$s" ] && s="$($CLI snapshot --interactive 2>/dev/null || true)"
  printf "%s" "$s"
}

extract_ref() {
  grep -oE 'ref=e[0-9]+' | head -1 | sed 's/ref=//' || true
}

find_publish_entry_ref() {
  echo "$1" | rg -n '发布笔记' -C 2 | rg '\[cursor=pointer\]' | head -1 | extract_ref
}

find_long_form_ref() {
  echo "$1" | rg -m1 '写长文' | extract_ref
}

find_cancel_ref() {
  local r
  r="$(echo "$1" | rg -m1 'button.*取消' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -m1 '\[cursor=pointer\].*取消' | extract_ref)"
  printf "%s" "$r"
}

find_new_creation_ref() {
  local r
  r="$(echo "$1" | rg -m1 'button "新的创作"' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -n '新的创作' -C 2 | rg '\[cursor=pointer\]' | head -1 | extract_ref)"
  printf "%s" "$r"
}

find_title_ref() {
  local r
  r="$(echo "$1" | rg -m1 'textbox.*(输入标题|填写标题)' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -m1 '(输入标题|填写标题)' | extract_ref)"
  printf "%s" "$r"
}

find_content_ref() {
  local r
  r="$(echo "$1" | rg -m1 'paragraph.*(粘贴到这里|输入正文|输入文字)' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -m1 'textbox.*输入正文' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -m1 'paragraph.*正文描述' | extract_ref)"
  printf "%s" "$r"
}

find_layout_ref() {
  echo "$1" | rg -m1 '(button|generic).*(一键排版)' | extract_ref
}

find_basic_template_ref() {
  local r
  r="$(echo "$1" | rg -m1 '基础简约' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -m1 '清晰明朗|黑白极简|轻感明快|文艺清新|平实叙事|素雅底纹' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -m1 '模板封面' | extract_ref)"
  printf "%s" "$r"
}

find_next_ref() {
  echo "$1" | rg -m1 'button "下一步"' | extract_ref
}

find_publish_ref() {
  local r
  r="$(echo "$1" | rg -m1 'button "发布"' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -m1 '(button|generic).*发布' | rg -v '发布笔记|发布图文笔记|发布视频笔记' | extract_ref)"
  printf "%s" "$r"
}

is_login_required() {
  echo "$1" | rg -q '手机号|验证码|登录|扫码登录|请先登录'
}

echo "Starting browser..."
$CLI start >/dev/null 2>&1 || true
$CLI status >/dev/null 2>&1 || { echo "Error: OpenClaw browser relay unavailable."; exit 1; }

echo "Opening Xiaohongshu creator page..."
$CLI open "$BASE_URL" >/dev/null 2>&1 || true
sleep 2 >/dev/null 2>&1 || sleep 2

SNAPSHOT="$(get_snapshot)"
is_login_required "$SNAPSHOT" && { echo "Error: Login required."; exit 1; }

PUBLISH_ENTRY_REF="$(find_publish_entry_ref "$SNAPSHOT")"
[ -z "$PUBLISH_ENTRY_REF" ] && { echo "Error: Publish entry not found."; exit 1; }

echo "Clicking 发布笔记: $PUBLISH_ENTRY_REF"
$CLI click "$PUBLISH_ENTRY_REF" >/dev/null 2>&1 || true
sleep 1 >/dev/null 2>&1 || sleep 1

SNAPSHOT="$(get_snapshot)"
LONG_REF="$(find_long_form_ref "$SNAPSHOT")"
[ -z "$LONG_REF" ] && { echo "Error: 写长文 entry not found."; exit 1; }

echo "Clicking 写长文: $LONG_REF"
$CLI click "$LONG_REF" >/dev/null 2>&1 || true
sleep 2 >/dev/null 2>&1 || sleep 2

SNAPSHOT="$(get_snapshot)"
CANCEL_REF="$(find_cancel_ref "$SNAPSHOT")"
if [ -n "$CANCEL_REF" ]; then
  echo "Closing popup via 取消: $CANCEL_REF"
  $CLI click "$CANCEL_REF" >/dev/null 2>&1 || true
  sleep 1 >/dev/null 2>&1 || sleep 1
  SNAPSHOT="$(get_snapshot)"
fi

NEW_REF="$(find_new_creation_ref "$SNAPSHOT")"
[ -z "$NEW_REF" ] && { echo "Error: 新的创作 button not found."; exit 1; }

echo "Clicking 新的创作: $NEW_REF"
$CLI click "$NEW_REF" >/dev/null 2>&1 || true
sleep 3 >/dev/null 2>&1 || sleep 3

TITLE_REF=""
CONTENT_REF=""
for _ in 1 2 3 4 5; do
  SNAPSHOT="$(get_snapshot)"
  TITLE_REF="$(find_title_ref "$SNAPSHOT")"
  CONTENT_REF="$(find_content_ref "$SNAPSHOT")"
  if [ -n "$TITLE_REF" ] && [ -n "$CONTENT_REF" ]; then
    break
  fi
  sleep 1 >/dev/null 2>&1 || sleep 1
done

[ -z "$TITLE_REF" ] && { echo "Error: title input not found."; exit 1; }
[ -z "$CONTENT_REF" ] && { echo "Error: content editor not found."; exit 1; }

echo "Typing title into $TITLE_REF"
$CLI click "$TITLE_REF" >/dev/null 2>&1 || true
$CLI press "Meta+A" >/dev/null 2>&1 || true
$CLI press "Control+A" >/dev/null 2>&1 || true
$CLI type "$TITLE_REF" "$TITLE_SAFE" >/dev/null 2>&1 || true
sleep 1 >/dev/null 2>&1 || sleep 1

echo "Typing content into $CONTENT_REF"
$CLI click "$CONTENT_REF" >/dev/null 2>&1 || true
$CLI press "Meta+A" >/dev/null 2>&1 || true
$CLI press "Control+A" >/dev/null 2>&1 || true
$CLI type "$CONTENT_REF" "$CONTENT" >/dev/null 2>&1 || true
sleep 1 >/dev/null 2>&1 || sleep 1

SNAPSHOT="$(get_snapshot)"
LAYOUT_REF="$(find_layout_ref "$SNAPSHOT")"
if [ -n "$LAYOUT_REF" ]; then
  echo "Clicking 一键排版: $LAYOUT_REF"
  $CLI click "$LAYOUT_REF" >/dev/null 2>&1 || true
  sleep 4 >/dev/null 2>&1 || sleep 4
fi

SNAPSHOT="$(get_snapshot)"
TEMPLATE_REF="$(find_basic_template_ref "$SNAPSHOT")"
if [ -n "$TEMPLATE_REF" ]; then
  echo "Selecting template: $TEMPLATE_REF"
  $CLI click "$TEMPLATE_REF" >/dev/null 2>&1 || true
  sleep 1 >/dev/null 2>&1 || sleep 1
fi

NEXT_REF=""
for _ in 1 2 3 4 5; do
  SNAPSHOT="$(get_snapshot)"
  NEXT_REF="$(find_next_ref "$SNAPSHOT")"
  [ -n "$NEXT_REF" ] && break
  $CLI press "PageDown" >/dev/null 2>&1 || true
  sleep 1 >/dev/null 2>&1 || sleep 1
done

[ -z "$NEXT_REF" ] && { echo "Error: 下一步 button not found."; exit 1; }

echo "Clicking 下一步: $NEXT_REF"
$CLI click "$NEXT_REF" >/dev/null 2>&1 || true
sleep 3 >/dev/null 2>&1 || sleep 3

PUBLISH_REF=""
for _ in 1 2 3 4 5; do
  SNAPSHOT="$(get_snapshot)"
  PUBLISH_REF="$(find_publish_ref "$SNAPSHOT")"
  [ -n "$PUBLISH_REF" ] && break
  $CLI press "PageDown" >/dev/null 2>&1 || true
  sleep 1 >/dev/null 2>&1 || sleep 1
done

[ -z "$PUBLISH_REF" ] && { echo "Error: 发布 button not found."; exit 1; }

echo "Clicking 发布: $PUBLISH_REF"
$CLI click "$PUBLISH_REF" >/dev/null 2>&1 || true
sleep 2 >/dev/null 2>&1 || sleep 2

# Handle confirmation dialog (确认发布 / 确认 / 继续发布)
find_confirm_ref() {
  local r
  r="$(echo "$1" | rg -m1 'button "确认发布"' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -m1 'button "确认"' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -m1 'button "继续发布"' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -m1 'button "确定"' | extract_ref)"
  [ -z "$r" ] && r="$(echo "$1" | rg -m1 '(button|generic).*(确认发布|确认|继续发布|确定发布)' | extract_ref)"
  printf "%s" "$r"
}

SNAPSHOT="$(get_snapshot)"
CONFIRM_REF="$(find_confirm_ref "$SNAPSHOT")"
if [ -n "$CONFIRM_REF" ]; then
  echo "Clicking 确认发布: $CONFIRM_REF"
  $CLI click "$CONFIRM_REF" >/dev/null 2>&1 || true
  sleep 3 >/dev/null 2>&1 || sleep 3
fi

SUCCESS=0
for _ in 1 2 3 4 5 6 7 8; do
  SNAPSHOT="$(get_snapshot)"
  if echo "$SNAPSHOT" | rg -q '发布成功|审核中|已发布|内容已提交|笔记已发布'; then
    SUCCESS=1
    break
  fi
  # Retry confirm dialog if still on page
  CONFIRM_REF="$(find_confirm_ref "$SNAPSHOT")"
  if [ -n "$CONFIRM_REF" ]; then
    echo "Retrying 确认: $CONFIRM_REF"
    $CLI click "$CONFIRM_REF" >/dev/null 2>&1 || true
  fi
  sleep 2 >/dev/null 2>&1 || sleep 2
done

if [ "$SUCCESS" -eq 1 ]; then
  echo "Xiaohongshu publish flow completed successfully."
else
  echo "Publish clicked; success text not detected yet. Please verify in creator center."
fi


path: scripts/run_xhs_publish.sh
content: #!/bin/bash

# Xiaohongshu CDP publisher wrapper
# Usage:
# run_xhs_publish.sh "title" "content"

TITLE="$1"
BODY="$2"

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

if [ -z "$TITLE" ] || [ -z "$BODY" ]; then
  echo "Usage: run_xhs_publish.sh \"title\" \"content\""
  exit 1
fi

python3 "$SCRIPT_DIR/cdp_xhs_publish.py" "$TITLE" "$BODY"


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