Files
xiaohongshu-mcp/skills/post-to-xhs/scripts/publish_pipeline.py
Angiin 8572c8c5e0 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
2026-02-27 16:27:16 +08:00

239 lines
8.1 KiB
Python

"""
Unified publish pipeline for Xiaohongshu.
Single CLI entry point that orchestrates:
chrome_launcher → login check → image download → form fill → (optional) publish
Usage:
# Fill form only (default) - review in browser before publishing
python publish_pipeline.py --title "标题" --content "正文" --image-urls URL1 URL2
python publish_pipeline.py --title-file t.txt --content-file body.txt --image-urls URL1
# Headless mode (no GUI window) - faster for automated publishing
python publish_pipeline.py --headless --title-file t.txt --content-file body.txt --image-urls URL1
# Publish to a specific account
python publish_pipeline.py --account myaccount --title "标题" --content "正文" --image-urls URL1
# Fill and auto-publish in one step
python publish_pipeline.py --title "标题" --content "正文" --image-urls URL1 --auto-publish
# Use local image files instead of URLs
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:
0 = success (READY_TO_PUBLISH or PUBLISHED)
1 = not logged in (NOT_LOGGED_IN) - headless auto-fallback will restart headed
2 = error (see stderr)
"""
import argparse
import os
import sys
# Ensure UTF-8 output on Windows consoles
if sys.platform == "win32":
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
# Add scripts dir to path so sibling modules can be imported
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
from chrome_launcher import ensure_chrome, restart_chrome
from cdp_publish import XiaohongshuPublisher, CDPError
from image_downloader import ImageDownloader
def main():
parser = argparse.ArgumentParser(
description="Xiaohongshu publish pipeline - unified entry point"
)
# Title
title_group = parser.add_mutually_exclusive_group(required=True)
title_group.add_argument("--title", help="Article title text")
title_group.add_argument("--title-file", help="Read title from UTF-8 file")
# Content
content_group = parser.add_mutually_exclusive_group(required=True)
content_group.add_argument("--content", help="Article body text")
content_group.add_argument("--content-file", help="Read content from UTF-8 file")
# Mode
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(
"--image-urls", nargs="+", help="Image URLs to download"
)
img_group.add_argument(
"--images", nargs="+", help="Local image file paths"
)
# Publish mode
parser.add_argument(
"--auto-publish",
action="store_true",
default=False,
help="Click publish button after filling (default: fill only)",
)
# Headless mode
parser.add_argument(
"--headless",
action="store_true",
default=False,
help="Run Chrome in headless mode (no GUI). Auto-falls back to headed if login is needed.",
)
# Optional temp dir for downloaded images
parser.add_argument(
"--temp-dir",
default=None,
help="Directory for downloaded images (default: auto-created temp dir)",
)
# Account selection
parser.add_argument(
"--account",
default=None,
help="Account name to publish to (default: default account)",
)
args = parser.parse_args()
headless = args.headless
account = args.account
# --- Resolve title ---
if args.title_file:
with open(args.title_file, encoding="utf-8") as f:
title = f.read().strip()
else:
title = args.title
if not title:
print("Error: title is empty.", file=sys.stderr)
sys.exit(2)
# --- Resolve content ---
if args.content_file:
with open(args.content_file, encoding="utf-8") as f:
content = f.read().strip()
else:
content = args.content
if not content:
print("Error: content is empty.", file=sys.stderr)
sys.exit(2)
# --- Step 1: Ensure Chrome is running ---
mode_label = "headless" if headless else "headed"
account_label = account or "default"
print(f"[pipeline] Step 1: Ensuring Chrome is running ({mode_label}, account: {account_label})...")
if not ensure_chrome(headless=headless, account=account):
print("Error: Failed to start Chrome.", file=sys.stderr)
sys.exit(2)
# --- Step 2: Connect and check login ---
print("[pipeline] Step 2: Checking login status...")
publisher = XiaohongshuPublisher()
try:
publisher.connect()
logged_in = publisher.check_login()
if not logged_in:
publisher.disconnect()
if headless:
# Auto-fallback: restart Chrome in headed mode for QR login
print("[pipeline] Headless mode: not logged in. Switching to headed mode for login...")
restart_chrome(headless=False, account=account)
publisher.connect()
publisher.open_login_page()
print("NOT_LOGGED_IN")
sys.exit(1)
except CDPError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(2)
# --- Step 3: Prepare images ---
image_paths = []
downloader = None
if args.image_urls:
print(f"[pipeline] Step 3: Downloading {len(args.image_urls)} image(s)...")
downloader = ImageDownloader(temp_dir=args.temp_dir)
image_paths = downloader.download_all(args.image_urls)
if not image_paths:
print("Error: All image downloads failed.", file=sys.stderr)
sys.exit(2)
elif args.images:
image_paths = args.images
# Verify local files exist
for p in image_paths:
if not os.path.isfile(p):
print(f"Error: Image file not found: {p}", file=sys.stderr)
sys.exit(2)
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 ---
print("[pipeline] Step 4: Filling form...")
try:
if args.mode == "long-article":
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:
print(f"Error during form fill: {e}", file=sys.stderr)
if downloader:
downloader.cleanup()
sys.exit(2)
# --- Step 5: Publish (optional, image-text mode only) ---
if args.auto_publish and args.mode == "image-text":
print("[pipeline] Step 5: Clicking publish button...")
try:
publisher._click_publish()
print("PUBLISH_STATUS: PUBLISHED")
except CDPError as e:
print(f"Error clicking publish: {e}", file=sys.stderr)
if downloader:
downloader.cleanup()
sys.exit(2)
# --- Cleanup ---
publisher.disconnect()
if downloader:
downloader.cleanup()
print("[pipeline] Done.")
if __name__ == "__main__":
main()