feat: add long article publish mode to post-to-xhs skill

Add "写长文" workflow with template selection support:
- cdp_publish.py: new commands (long-article, select-template, click-next-step)
- publish_pipeline.py: add --mode parameter (image-text / long-article)
- SKILL.md: document long article flow (B.1-B.5 steps)
- publish-workflow.md: add long article selectors, CLI usage, detailed steps
This commit is contained in:
Angiin
2026-02-27 15:52:44 +08:00
parent b50f0aa633
commit 8572c8c5e0
8 changed files with 562 additions and 24 deletions

View File

@@ -1,25 +1,30 @@
--- ---
name: post-to-xhs name: post-to-xhs
description: > description: >
小红书内容发布技能。支持两种输入方式:(1) 用户提供完整内容和图片/图片URL直接发布 小红书内容发布技能。支持两种发布模式:(1) 上传图文模式 - 图片+短文;(2) 写长文模式 - 长篇文章+排版模板。
(2) 用户提供网页URL自动提取内容和图片适当总结后发布。如果从URL提取不到图片 支持两种输入方式:用户提供完整内容和图片/图片URL直接发布或提供网页URL自动提取内容和图片。
提示用户手动下载并提供。适用于任何类型的内容发布 用户说"发长文"时使用长文模式,否则默认图文模式
--- ---
# 小红书内容发布 # 小红书内容发布
根据用户输入自动判断发布方式,简化发布流程。 根据用户输入自动判断发布方式和发布模式,简化发布流程。
## 发布模式
- **上传图文**(默认):图片 + 短文,适合日常分享
- **写长文**:长篇文章 + 排版模板选择,适合深度内容。用户明确说"发长文"时使用
## 工作流程 ## 工作流程
``` ```
用户输入 用户输入
├─ 完整内容 + 图片/图片URL → 直接进入发布流程 ├─ 完整内容 + 图片/图片URL → 判断模式 → 发布流程
└─ 网页 URL → WebFetch 提取内容和图片 └─ 网页 URL → WebFetch 提取内容和图片
├─ 有图片 → 适当总结内容 → 发布流程 ├─ 有图片 → 适当总结内容 → 判断模式 → 发布流程
└─ 无图片 → 提示用户手动下载图片 └─ 无图片 → 提示用户手动下载图片
@@ -116,7 +121,9 @@ AskUserQuestion 示例:
将标题和正文写入临时 UTF-8 文本文件。不要在 `python -c` 中内联中文文本。 将标题和正文写入临时 UTF-8 文本文件。不要在 `python -c` 中内联中文文本。
### 4.4 运行 Pipeline ### 4.4 运行发布(根据模式分流)
#### A. 上传图文模式(默认)
根据用户选择的模式执行发布脚本: 根据用户选择的模式执行发布脚本:
@@ -144,11 +151,44 @@ python ... --images "C:\path\to\image.jpg"
- `READY_TO_PUBLISH` (exit code 0) → 根据模式进入下一步 - `READY_TO_PUBLISH` (exit code 0) → 根据模式进入下一步
- Exit code 2 → 报告错误 - Exit code 2 → 报告错误
### 4.5 用户预览确认(仅有窗口模式 #### B. 写长文模式
**仅当用户选择有窗口模式时**,使用 `AskUserQuestion` 请用户在浏览器中检查预览,确认后再发布。 **Step B.1 — 填写长文内容 + 一键排版:**
无头模式跳过此步骤,直接进入 4.6。 ```bash
python "C:\Users\admin\AI\.claude\skills\post-to-xhs\scripts\cdp_publish.py" long-article --title-file title.txt --content-file content.txt
```
可选 `--images img1.jpg img2.jpg` 插入图片到编辑器中。
输出中包含 `TEMPLATES: [...]` JSON 数组,为可用的排版模板名称列表。
**Step B.2 — 让用户选择模板:**
使用 `AskUserQuestion` 将模板名称作为选项展示给用户选择(从 TEMPLATES 输出中解析)。
**Step B.3 — 选择模板:**
```bash
python "C:\Users\admin\AI\.claude\skills\post-to-xhs\scripts\cdp_publish.py" select-template --name "用户选择的模板名"
```
**Step B.4 — 点击下一步并填写发布页正文描述:**
```bash
python "C:\Users\admin\AI\.claude\skills\post-to-xhs\scripts\cdp_publish.py" click-next-step --content-file content.txt
```
注意:发布页有独立的正文描述编辑器,必须通过 `--content``--content-file` 传入内容填写。
如果正文超过 1000 字,应压缩到 800 字左右再填入,保持语义不变。
**Step B.5 — 用户预览确认并发布:** 进入下方 4.5 步骤。
### 4.5 用户预览确认(仅有窗口模式 / 长文模式)
**仅当用户选择有窗口模式或使用长文模式时**,使用 `AskUserQuestion` 请用户在浏览器中检查预览,确认后再发布。
无头模式的图文发布跳过此步骤,直接进入 4.6。
### 4.6 点击发布 ### 4.6 点击发布
@@ -164,8 +204,10 @@ python "C:\Users\admin\AI\.claude\skills\post-to-xhs\scripts\cdp_publish.py" cli
## 重要提示 ## 重要提示
- **绝不自动发布** - 必须在 Step 4.4 获得用户确认 - **绝不自动发布** - 必须获得用户确认
- **图片必须有** - 小红书发布必须有图片,没有图片不能发布 - **图片要求** - 上传图文模式必须有图片;写长文模式图片可选
- **长文模式** - 必须让用户选择模板,不要自动选择
- **正文描述** - 长文模式的发布页有独立正文描述框,超过 1000 字需压缩到 800 字左右
- **无头模式**:使用 `--headless` 参数自动化发布。如需登录,脚本自动切换到有窗口模式 - **无头模式**:使用 `--headless` 参数自动化发布。如需登录,脚本自动切换到有窗口模式
- 如果页面结构变化导致选择器失效,参考 `references/publish-workflow.md` 更新 - 如果页面结构变化导致选择器失效,参考 `references/publish-workflow.md` 更新

View File

@@ -10,10 +10,16 @@
## 流程概览 ## 流程概览
**上传图文模式**:
``` ```
生成文案 → 用户确认 → 启动 Chrome → 检查登录 → 导航发布页 → 上传图片 → 填写标题 → 填写正文 → 用户确认发布 生成文案 → 用户确认 → 启动 Chrome → 检查登录 → 导航发布页 → 上传图片 → 填写标题 → 填写正文 → 用户确认发布
``` ```
**写长文模式**:
```
生成文案 → 用户确认 → 启动 Chrome → 检查登录 → 导航发布页 → 点击"写长文"tab → 点击"新的创作" → 填写标题 → 填写正文 → 一键排版 → 用户选择模板 → 下一步 → 填写发布页正文描述 → 用户确认发布
```
## 详细步骤 ## 详细步骤
### 1. 启动 / 连接 Chrome ### 1. 启动 / 连接 Chrome
@@ -78,6 +84,64 @@
- 用户确认后,脚本点击发布按钮 - 用户确认后,脚本点击发布按钮
- 或用户选择手动点击发布按钮 - 或用户选择手动点击发布按钮
## 写长文模式详细步骤
### 1-2. 启动 Chrome 和检查登录
同上传图文模式。
### 3. 导航到发布页并点击"写长文"tab
脚本: `scripts/cdp_publish.py``_click_long_article_tab()`
- 导航到 `https://creator.xiaohongshu.com/publish/publish`
-`div.creator-tab` 中查找文本为"写长文"的 tab 并点击
### 4. 点击"新的创作"
脚本: `scripts/cdp_publish.py``_click_new_creation()`
- 在页面中查找包含"新的创作"文本的元素并点击
- 等待长文编辑器页面加载
### 5. 填写长文标题
脚本: `scripts/cdp_publish.py``_fill_long_title()`
- 定位 `textarea.d-text[placeholder="输入标题"]` 元素
- 使用 `HTMLTextAreaElement.prototype.value` 的 native setter 设置值
- 触发 `input``change` 事件
### 6. 填写长文正文
同上传图文模式的正文填写TipTap/ProseMirror 编辑器)。
### 7. 一键排版
脚本: `scripts/cdp_publish.py``_click_auto_format()`
- 查找并点击"一键排版"按钮
- 等待模板列表加载
### 8. 模板选择
脚本: `scripts/cdp_publish.py``get_template_names()` + `select_template(name)`
- `get_template_names()``.template-card .template-title` 获取所有模板名称
- `select_template(name)` 点击指定名称的模板卡片
- 已选中的模板卡片 class 为 `template-card selected`
### 9. 下一步并填写发布页描述
脚本: `scripts/cdp_publish.py``click_next_and_prepare_publish(content)`
- 点击"下一步"按钮进入发布预览页
- 发布页有独立的正文描述编辑器(`div.tiptap.ProseMirror`),需要单独填入内容
### 10. 用户确认并发布
同上传图文模式。
## DOM 选择器参考 ## DOM 选择器参考
> **注意**: 小红书前端可能随时更新,以下选择器基于编写时的页面结构。如果自动化失败,需要在浏览器 DevTools 中重新抓取选择器,并更新 `cdp_publish.py` 中的 `SELECTORS` 字典。 > **注意**: 小红书前端可能随时更新,以下选择器基于编写时的页面结构。如果自动化失败,需要在浏览器 DevTools 中重新抓取选择器,并更新 `cdp_publish.py` 中的 `SELECTORS` 字典。
@@ -85,9 +149,16 @@
| 元素 | 主选择器 | 备选选择器 | 说明 | | 元素 | 主选择器 | 备选选择器 | 说明 |
|---|---|---|---| |---|---|---|---|
| 图片上传 | `input.upload-input` | `input[type="file"]` | 隐藏的文件输入,通过 CDP 直接操作 | | 图片上传 | `input.upload-input` | `input[type="file"]` | 隐藏的文件输入,通过 CDP 直接操作 |
| 标题输入 | `input[placeholder*="填写标题"]` | `input.d-text` | 标题输入框 | | 标题输入(图文) | `input[placeholder*="填写标题"]` | `input.d-text` | 图文模式标题输入框 |
| 标题输入(长文) | `textarea.d-text[placeholder="输入标题"]` | - | 长文模式 textarea 标题 |
| 正文编辑 | `div.tiptap.ProseMirror` | `div.ProseMirror[contenteditable="true"]` | TipTap/ProseMirror 富文本编辑器 | | 正文编辑 | `div.tiptap.ProseMirror` | `div.ProseMirror[contenteditable="true"]` | TipTap/ProseMirror 富文本编辑器 |
| 发布按钮 | 文本匹配"发布"`button` + `.d-button-content .d-text` | - | 通过遍历按钮文本定位 | | 发布按钮 | 文本匹配"发布" | - | 通过遍历按钮文本定位 |
| 写长文 tab | 文本匹配"写长文"`div.creator-tab` | - | 长文模式入口 |
| 新的创作按钮 | 文本匹配"新的创作" | - | 长文编辑器入口 |
| 一键排版按钮 | 文本匹配"一键排版" | - | 触发模板选择 |
| 模板卡片 | `.template-card` | `.template-card.selected`(已选) | 排版模板列表 |
| 模板名称 | `.template-card .template-title` | - | 模板卡片内的名称 span |
| 下一步按钮 | 文本匹配"下一步" | - | 模板选择后进入发布页 |
| 登录检测 | URL 包含 "login" | `.user-info, .creator-header` | 重定向检测 + DOM 元素检测 | | 登录检测 | URL 包含 "login" | `.user-info, .creator-header` | 重定向检测 + DOM 元素检测 |
## 选择器维护指南 ## 选择器维护指南
@@ -140,7 +211,7 @@ python publish_pipeline.py --headless --title "标题" --content "正文" --imag
- 退出码 1 + `NOT_LOGGED_IN` = 未登录,需扫码(无头模式下会自动切换到有窗口模式) - 退出码 1 + `NOT_LOGGED_IN` = 未登录,需扫码(无头模式下会自动切换到有窗口模式)
- 退出码 2 = 其他错误 - 退出码 2 = 其他错误
### 方式 B: 分步调用 ### 方式 B: 分步调用(图文模式)
```bash ```bash
# 1. 启动 Chrome可选 --headless # 1. 启动 Chrome可选 --headless
@@ -162,6 +233,36 @@ python cdp_publish.py click-publish
python cdp_publish.py --headless publish --title "标题" --content-file body.txt --images img1.jpg python cdp_publish.py --headless publish --title "标题" --content-file body.txt --images img1.jpg
``` ```
### 方式 C: 分步调用(长文模式)
```bash
# 1. 启动 Chrome
python chrome_launcher.py
# 2. 检查登录
python cdp_publish.py check-login
# 3. 填写长文 + 一键排版(输出包含 TEMPLATES JSON
python cdp_publish.py long-article --title-file title.txt --content-file content.txt
# 4. 选择模板
python cdp_publish.py select-template --name "模板名称"
# 5. 下一步 + 填写发布页正文描述
python cdp_publish.py click-next-step --content-file content.txt
# 6. 用户确认后点击发布
python cdp_publish.py click-publish
```
### 方式 D: Pipeline 长文模式
```bash
# 长文模式(图片可选)
python publish_pipeline.py --mode long-article --title-file title.txt --content-file content.txt
python publish_pipeline.py --mode long-article --title "标题" --content "正文" --images img1.jpg
```
### 账号管理 ### 账号管理
```bash ```bash

View File

@@ -5,12 +5,16 @@ Connects to a Chrome instance via Chrome DevTools Protocol to automate
publishing articles on Xiaohongshu (RED) creator center. publishing articles on Xiaohongshu (RED) creator center.
CLI usage: CLI usage:
# Basic commands # Basic commands (image-text mode)
python cdp_publish.py check-login [--headless] [--account NAME] python cdp_publish.py check-login [--headless] [--account NAME]
python cdp_publish.py fill --title "标题" --content "正文" --images img1.jpg [--headless] [--account NAME] python cdp_publish.py fill --title "标题" --content "正文" --images img1.jpg [--headless] [--account NAME]
python cdp_publish.py publish --title "标题" --content "正文" --images img1.jpg [--headless] [--account NAME] python cdp_publish.py publish --title "标题" --content "正文" --images img1.jpg [--headless] [--account NAME]
python cdp_publish.py click-publish [--headless] [--account NAME] python cdp_publish.py click-publish [--headless] [--account NAME]
# Long article mode
python cdp_publish.py long-article --title "标题" --content "正文" [--images img1.jpg] [--account NAME]
python cdp_publish.py click-next-step [--account NAME]
# Account management # Account management
python cdp_publish.py login [--account NAME] # open browser for QR login python cdp_publish.py login [--account NAME] # open browser for QR login
python cdp_publish.py re-login [--account NAME] # clear cookies and re-login same account python cdp_publish.py re-login [--account NAME] # clear cookies and re-login same account
@@ -81,6 +85,13 @@ SELECTORS = {
"publish_button_text": "发布", "publish_button_text": "发布",
# Login indicator - URL-based check (redirect to /login if not logged in) # Login indicator - URL-based check (redirect to /login if not logged in)
"login_indicator": '.user-info, .creator-header, [class*="user"]', "login_indicator": '.user-info, .creator-header, [class*="user"]',
# Long article mode
"long_article_tab_text": "写长文",
"new_creation_btn_text": "新的创作",
"long_title_input": 'textarea.d-text[placeholder="输入标题"]',
"auto_format_btn_text": "一键排版",
"next_step_btn_text": "下一步",
"template_card": ".template-card",
} }
# Timing # Timing
@@ -88,6 +99,8 @@ PAGE_LOAD_WAIT = 3 # seconds to wait after navigation
TAB_CLICK_WAIT = 2 # seconds to wait after clicking tab TAB_CLICK_WAIT = 2 # seconds to wait after clicking tab
UPLOAD_WAIT = 6 # seconds to wait after image upload for editor to appear UPLOAD_WAIT = 6 # seconds to wait after image upload for editor to appear
ACTION_INTERVAL = 1 # seconds between actions ACTION_INTERVAL = 1 # seconds between actions
AUTO_FORMAT_WAIT = 5 # seconds to wait after clicking auto-format
TEMPLATE_WAIT = 10 # seconds max to wait for template cards to appear
class CDPError(Exception): class CDPError(Exception):
@@ -450,7 +463,305 @@ class XiaohongshuPublisher:
) )
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# Main publish workflow # Long article actions
# ------------------------------------------------------------------
def _click_long_article_tab(self):
"""Click the '写长文' tab to switch to long article mode."""
print("[cdp_publish] Clicking '写长文' tab...")
tab_text = SELECTORS["long_article_tab_text"]
selector = SELECTORS["image_text_tab"] # same container: div.creator-tab
clicked = self._evaluate(f"""
(function() {{
var tabs = document.querySelectorAll('{selector}');
for (var i = 0; i < tabs.length; i++) {{
if (tabs[i].textContent.trim() === '{tab_text}') {{
tabs[i].click();
return true;
}}
}}
return false;
}})();
""")
if not clicked:
raise CDPError(
f"Could not find '{tab_text}' tab. "
"The page structure may have changed."
)
print("[cdp_publish] '写长文' tab clicked.")
time.sleep(TAB_CLICK_WAIT)
def _click_new_creation(self):
"""Click the '新的创作' button to start a new long article."""
print("[cdp_publish] Clicking '新的创作' button...")
btn_text = SELECTORS["new_creation_btn_text"]
clicked = self._evaluate(f"""
(function() {{
// Search all elements for text match
var candidates = document.querySelectorAll(
'.center span, .center div, .center button, .center a, '
+ 'button, [role="button"], [class*="btn"], [class*="creation"]'
);
for (var i = 0; i < candidates.length; i++) {{
if (candidates[i].textContent.trim() === '{btn_text}') {{
candidates[i].click();
return true;
}}
}}
return false;
}})();
""")
if not clicked:
raise CDPError(
f"Could not find '{btn_text}' button. "
"The page structure may have changed."
)
print("[cdp_publish] '新的创作' button clicked.")
time.sleep(PAGE_LOAD_WAIT)
def _fill_long_title(self, title: str):
"""Fill in the long article title (textarea element)."""
print(f"[cdp_publish] Setting long article title: {title[:40]}...")
time.sleep(ACTION_INTERVAL)
selector = SELECTORS["long_title_input"]
found = self._evaluate(f"!!document.querySelector('{selector}')")
if not found:
raise CDPError(
f"Could not find long title textarea ('{selector}'). "
"The page structure may have changed."
)
escaped_title = json.dumps(title)
self._evaluate(f"""
(function() {{
var el = document.querySelector('{selector}');
var nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype, 'value'
).set;
el.focus();
nativeSetter.call(el, {escaped_title});
el.dispatchEvent(new Event('input', {{ bubbles: true }}));
el.dispatchEvent(new Event('change', {{ bubbles: true }}));
}})();
""")
print("[cdp_publish] Long article title set.")
def _click_auto_format(self):
"""Click the '一键排版' button."""
print("[cdp_publish] Clicking '一键排版' button...")
btn_text = SELECTORS["auto_format_btn_text"]
clicked = self._evaluate(f"""
(function() {{
var elems = document.querySelectorAll(
'button, [role="button"], span, div, a, [class*="btn"]'
);
for (var i = 0; i < elems.length; i++) {{
if (elems[i].textContent.trim() === '{btn_text}') {{
elems[i].click();
return true;
}}
}}
return false;
}})();
""")
if not clicked:
raise CDPError(
f"Could not find '{btn_text}' button. "
"The page structure may have changed."
)
print("[cdp_publish] '一键排版' button clicked. Waiting for templates...")
time.sleep(AUTO_FORMAT_WAIT)
def _wait_for_templates(self) -> bool:
"""Wait for template cards to appear after clicking auto-format."""
print("[cdp_publish] Waiting for template cards to load...")
selector = SELECTORS["template_card"]
for attempt in range(TEMPLATE_WAIT):
found = self._evaluate(
f"document.querySelectorAll('{selector}').length"
)
if found and found > 0:
print(f"[cdp_publish] Found {found} template card(s).")
return True
time.sleep(1)
print("[cdp_publish] Warning: No template cards found within timeout.")
return False
def get_template_names(self) -> list[str]:
"""Get the list of available template names from the page."""
selector = SELECTORS["template_card"]
names = self._evaluate(f"""
(function() {{
var cards = document.querySelectorAll('{selector}');
var names = [];
for (var i = 0; i < cards.length; i++) {{
var title = cards[i].querySelector('.template-title');
names.push(title ? title.textContent.trim() : 'Template ' + i);
}}
return names;
}})();
""")
return names or []
def select_template(self, name: str) -> bool:
"""Select a template by clicking the card with the matching name."""
print(f"[cdp_publish] Selecting template: {name}...")
selector = SELECTORS["template_card"]
clicked = self._evaluate(f"""
(function() {{
var cards = document.querySelectorAll('{selector}');
for (var i = 0; i < cards.length; i++) {{
var title = cards[i].querySelector('.template-title');
if (title && title.textContent.trim() === {json.dumps(name)}) {{
cards[i].click();
return true;
}}
}}
return false;
}})();
""")
if clicked:
print(f"[cdp_publish] Template '{name}' selected.")
time.sleep(ACTION_INTERVAL)
else:
print(f"[cdp_publish] Warning: Template '{name}' not found.")
return bool(clicked)
def _click_next_step(self):
"""Click the '下一步' button."""
print("[cdp_publish] Clicking '下一步' button...")
btn_text = SELECTORS["next_step_btn_text"]
clicked = self._evaluate(f"""
(function() {{
var elems = document.querySelectorAll(
'button, [role="button"], span, div, a, [class*="btn"]'
);
for (var i = 0; i < elems.length; i++) {{
if (elems[i].textContent.trim() === '{btn_text}') {{
elems[i].click();
return true;
}}
}}
return false;
}})();
""")
if not clicked:
raise CDPError(
f"Could not find '{btn_text}' button. "
"The page structure may have changed."
)
print("[cdp_publish] '下一步' button clicked.")
time.sleep(PAGE_LOAD_WAIT)
def publish_long_article(
self,
title: str,
content: str,
image_paths: list[str] | None = None,
) -> list[str]:
"""
Execute the full long article publish workflow:
1. Navigate to creator publish page
2. Click '写长文' tab
3. Click '新的创作' button
4. Fill title (textarea)
5. Fill content (TipTap editor)
6. (Optional) Insert images into editor
7. Click '一键排版'
8. Wait for templates
Returns list of available template names for the caller to
present to the user for selection.
Args:
title: Article title
content: Article body text (paragraphs separated by newlines)
image_paths: Optional list of local file paths to images
"""
if not self.ws:
raise CDPError("Not connected. Call connect() first.")
# Step 1: Navigate to publish page
self._navigate(XHS_CREATOR_URL)
time.sleep(2)
# Step 2: Click '写长文' tab
self._click_long_article_tab()
# Step 3: Click '新的创作'
self._click_new_creation()
# Step 4: Fill title
self._fill_long_title(title)
# Step 5: Fill content
self._fill_content(content)
# Step 6: Upload images into editor (if provided)
if image_paths:
print(f"[cdp_publish] Inserting {len(image_paths)} image(s) into editor...")
for img_path in image_paths:
normalized = img_path.replace("\\", "/")
self._evaluate(f"""
(function() {{
var editor = document.querySelector('{SELECTORS["content_editor"]}');
if (!editor) return false;
var img = document.createElement('img');
img.src = 'file:///{normalized}';
editor.appendChild(img);
editor.dispatchEvent(new Event('input', {{ bubbles: true }}));
return true;
}})();
""")
time.sleep(ACTION_INTERVAL)
# Step 7: Click '一键排版'
self._click_auto_format()
# Step 8: Wait for templates and return names
self._wait_for_templates()
template_names = self.get_template_names()
print(
"\n[cdp_publish] Templates loaded.\n"
" Available templates: " + ", ".join(template_names) + "\n"
)
return template_names
def click_next_and_prepare_publish(self, content: str = ""):
"""After user selects a template, click '下一步' and fill the publish page description."""
self._click_next_step()
# The publish page has a separate content editor for the post description
if content:
time.sleep(ACTION_INTERVAL)
self._fill_content(content)
print(
"\n[cdp_publish] Ready to publish.\n"
" Please review in the browser before confirming publish.\n"
)
# ------------------------------------------------------------------
# Main publish workflow (image-text mode)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def publish( def publish(
@@ -532,6 +843,23 @@ def main():
p_pub.add_argument("--content-file", default=None, help="Read content from file") p_pub.add_argument("--content-file", default=None, help="Read content from file")
p_pub.add_argument("--images", nargs="+", required=True) p_pub.add_argument("--images", nargs="+", required=True)
# long-article - long article mode
p_long = sub.add_parser("long-article", help="Fill long article content with auto-format and template selection")
p_long.add_argument("--title", default=None)
p_long.add_argument("--title-file", default=None, help="Read title from file")
p_long.add_argument("--content", default=None)
p_long.add_argument("--content-file", default=None, help="Read content from file")
p_long.add_argument("--images", nargs="+", default=None, help="Optional image file paths")
# select-template - select a template by name
p_tpl = sub.add_parser("select-template", help="Select a long article template by name")
p_tpl.add_argument("--name", required=True, help="Template name to select")
# click-next-step - click next step button (for long article after template selection)
p_next = sub.add_parser("click-next-step", help="Click '下一步' button after template selection")
p_next.add_argument("--content", default=None, help="Post description text")
p_next.add_argument("--content-file", default=None, help="Read post description from file")
# click-publish - just click the publish button on current page # click-publish - just click the publish button on current page
sub.add_parser("click-publish", help="Click publish button on already-filled page") sub.add_parser("click-publish", help="Click publish button on already-filled page")
@@ -648,6 +976,48 @@ def main():
publisher._click_publish() publisher._click_publish()
print("PUBLISH_STATUS: PUBLISHED") print("PUBLISH_STATUS: PUBLISHED")
elif args.command == "long-article":
title = args.title
if args.title_file:
with open(args.title_file, encoding="utf-8") as f:
title = f.read().strip()
if not title:
print("Error: --title or --title-file required.", file=sys.stderr)
sys.exit(1)
content = args.content
if args.content_file:
with open(args.content_file, encoding="utf-8") as f:
content = f.read().strip()
if not content:
print("Error: --content or --content-file required.", file=sys.stderr)
sys.exit(1)
publisher.connect()
template_names = publisher.publish_long_article(
title=title, content=content, image_paths=args.images,
)
# Print template names as JSON for programmatic consumption
print("TEMPLATES: " + json.dumps(template_names, ensure_ascii=False))
print("LONG_ARTICLE_STATUS: TEMPLATE_SELECTION")
elif args.command == "select-template":
publisher.connect(target_url_prefix="https://creator.xiaohongshu.com/publish")
if publisher.select_template(args.name):
print(f"TEMPLATE_SELECTED: {args.name}")
else:
print(f"Error: Template '{args.name}' not found.", file=sys.stderr)
sys.exit(1)
elif args.command == "click-next-step":
content = getattr(args, 'content', None)
if getattr(args, 'content_file', None):
with open(args.content_file, encoding="utf-8") as f:
content = f.read().strip()
publisher.connect(target_url_prefix="https://creator.xiaohongshu.com/publish")
publisher.click_next_and_prepare_publish(content=content or "")
print("LONG_ARTICLE_STATUS: READY_TO_PUBLISH")
elif args.command == "click-publish": elif args.command == "click-publish":
publisher.connect(target_url_prefix="https://creator.xiaohongshu.com/publish") publisher.connect(target_url_prefix="https://creator.xiaohongshu.com/publish")
publisher._click_publish() publisher._click_publish()

View File

@@ -21,6 +21,10 @@ Usage:
# Use local image files instead of URLs # Use local image files instead of URLs
python publish_pipeline.py --title "标题" --content "正文" --images img1.jpg img2.jpg python publish_pipeline.py --title "标题" --content "正文" --images img1.jpg img2.jpg
# Long article mode (images optional)
python publish_pipeline.py --mode long-article --title "标题" --content "正文"
python publish_pipeline.py --mode long-article --title "标题" --content "正文" --images img1.jpg
Exit codes: Exit codes:
0 = success (READY_TO_PUBLISH or PUBLISHED) 0 = success (READY_TO_PUBLISH or PUBLISHED)
1 = not logged in (NOT_LOGGED_IN) - headless auto-fallback will restart headed 1 = not logged in (NOT_LOGGED_IN) - headless auto-fallback will restart headed
@@ -65,8 +69,16 @@ def main():
content_group.add_argument("--content", help="Article body text") content_group.add_argument("--content", help="Article body text")
content_group.add_argument("--content-file", help="Read content from UTF-8 file") content_group.add_argument("--content-file", help="Read content from UTF-8 file")
# Images # Mode
img_group = parser.add_mutually_exclusive_group(required=True) parser.add_argument(
"--mode",
choices=["image-text", "long-article"],
default="image-text",
help="Publish mode: 'image-text' (default) or 'long-article'",
)
# Images (required for image-text, optional for long-article)
img_group = parser.add_mutually_exclusive_group(required=False)
img_group.add_argument( img_group.add_argument(
"--image-urls", nargs="+", help="Image URLs to download" "--image-urls", nargs="+", help="Image URLs to download"
) )
@@ -169,7 +181,7 @@ def main():
if not image_paths: if not image_paths:
print("Error: All image downloads failed.", file=sys.stderr) print("Error: All image downloads failed.", file=sys.stderr)
sys.exit(2) sys.exit(2)
else: elif args.images:
image_paths = args.images image_paths = args.images
# Verify local files exist # Verify local files exist
for p in image_paths: for p in image_paths:
@@ -177,20 +189,33 @@ def main():
print(f"Error: Image file not found: {p}", file=sys.stderr) print(f"Error: Image file not found: {p}", file=sys.stderr)
sys.exit(2) sys.exit(2)
print(f"[pipeline] Step 3: Using {len(image_paths)} local image(s).") print(f"[pipeline] Step 3: Using {len(image_paths)} local image(s).")
elif args.mode == "image-text":
print("Error: Images are required for image-text mode. Use --image-urls or --images.", file=sys.stderr)
sys.exit(2)
else:
print("[pipeline] Step 3: No images (optional for long-article mode).")
# --- Step 4: Fill form --- # --- Step 4: Fill form ---
print("[pipeline] Step 4: Filling form...") print("[pipeline] Step 4: Filling form...")
try: try:
publisher.publish(title=title, content=content, image_paths=image_paths) if args.mode == "long-article":
print("FILL_STATUS: READY_TO_PUBLISH") publisher.publish_long_article(
title=title,
content=content,
image_paths=image_paths or None,
)
print("LONG_ARTICLE_STATUS: TEMPLATE_SELECTION")
else:
publisher.publish(title=title, content=content, image_paths=image_paths)
print("FILL_STATUS: READY_TO_PUBLISH")
except CDPError as e: except CDPError as e:
print(f"Error during form fill: {e}", file=sys.stderr) print(f"Error during form fill: {e}", file=sys.stderr)
if downloader: if downloader:
downloader.cleanup() downloader.cleanup()
sys.exit(2) sys.exit(2)
# --- Step 5: Publish (optional) --- # --- Step 5: Publish (optional, image-text mode only) ---
if args.auto_publish: if args.auto_publish and args.mode == "image-text":
print("[pipeline] Step 5: Clicking publish button...") print("[pipeline] Step 5: Clicking publish button...")
try: try:
publisher._click_publish() publisher._click_publish()