x-article-publisher
npx skills add https://github.com/arcocodes/skill-x-article-publisher --skill x-article-publisher
Agent 安装分布
Skill 文档
X Article Publisher
Publish Markdown content to X (Twitter) Articles editor, preserving formatting with rich text conversion.
Prerequisites
- Playwright MCP for browser automation
- User logged into X with Premium Plus subscription
- Python 3.9+ with dependencies:
- macOS:
pip install Pillow pyobjc-framework-Cocoa - Windows:
pip install Pillow pywin32 clip-util
- macOS:
- For Mermaid diagrams:
npm install -g @mermaid-js/mermaid-cli
Scripts
Located in ~/.claude/skills/x-article-publisher/scripts/:
parse_markdown.py
Parse Markdown and extract structured data:
python parse_markdown.py <markdown_file> [--output json|html] [--html-only]
Returns JSON with: title, cover_image, content_images, dividers (with block_index for positioning), html, total_blocks
copy_to_clipboard.py
Copy image or HTML to system clipboard (cross-platform):
# Copy image (with optional compression)
python copy_to_clipboard.py image /path/to/image.jpg [--quality 80]
# Copy HTML for rich text paste
python copy_to_clipboard.py html --file /path/to/content.html
table_to_image.py
Convert Markdown table to PNG image:
python table_to_image.py <input.md> <output.png> [--scale 2]
Use when X Articles doesn’t support native table rendering or for consistent styling.
Pre-Processing (Optional)
Before publishing, scan the Markdown for elements that need conversion:
Tables â PNG
# Extract table to temp file, then convert
python ~/.claude/skills/x-article-publisher/scripts/table_to_image.py /tmp/table.md /tmp/table.png
# Replace table in markdown with: 
Mermaid Diagrams â PNG
# Extract mermaid block to .mmd file, then convert
mmdc -i /tmp/diagram.mmd -o /tmp/diagram.png -b white -s 2
# Replace mermaid block with: 
Dividers (—)
Dividers are automatically detected by parse_markdown.py and output in the dividers array. They must be inserted via X Articles’ Insert > Divider menu (HTML <hr> tags are ignored by X).
Workflow
Strategy: “å æåå¾ååå²çº¿” (Text First, Images Second, Dividers Last)
For articles with images and dividers, paste ALL text content first, then insert images and dividers at correct positions using block index.
- (Optional) Pre-process: Convert tables/mermaid to images
- Parse Markdown with Python script â get title, images, dividers with block_index, HTML
- Navigate to X Articles editor
- Upload cover image (first image)
- Fill title
- Copy HTML to clipboard (Python) â Paste with Cmd+V
- Insert content images at positions specified by block_index
- Insert dividers at positions specified by block_index (via Insert > Divider menu)
- Save as draft (NEVER auto-publish)
髿æ§è¡åå (Efficiency Guidelines)
ç®æ : æå°åæä½ä¹é´ççå¾ æ¶é´ï¼å®ç°æµç çèªå¨åä½éªã
1. é¿å ä¸å¿ è¦ç browser_snapshot
大夿°æµè§å¨æä½ï¼click, type, press_key çï¼é½ä¼å¨è¿åç»æä¸å
å«é¡µé¢ç¶æãä¸è¦å¨æ¯æ¬¡æä½ååç¬è°ç¨ browser_snapshotï¼ç´æ¥ä½¿ç¨æä½è¿åç页é¢ç¶æå³å¯ã
â éè¯¯åæ³ï¼
browser_click â browser_snapshot â åæ â browser_click â browser_snapshot â ...
â
æ£ç¡®åæ³ï¼
browser_click â ä»è¿åç»æä¸è·å页é¢ç¶æ â browser_click â ...
2. é¿å ä¸å¿ è¦ç browser_wait_for
åªå¨ä»¥ä¸æ
åµä½¿ç¨ browser_wait_forï¼
- çå¾
å¾çä¸ä¼ 宿ï¼
textGone="æ£å¨ä¸ä¼ åªä½"ï¼ - çå¾ é¡µé¢åå§å è½½ï¼æå°æ°æ åµï¼
ä¸è¦ä½¿ç¨ browser_wait_for æ¥çå¾
æé®æè¾å
¥æ¡åºç° – å®ä»¬å¨é¡µé¢å è½½å®æåç«å³å¯ç¨ã
3. å¹¶è¡æ§è¡ç¬ç«æä½
å½ä¸¤ä¸ªæä½æ²¡æä¾èµå ³ç³»æ¶ï¼å¯ä»¥å¨åä¸ä¸ªæ¶æ¯ä¸å¹¶è¡è°ç¨å¤ä¸ªå·¥å ·ï¼
â
å¯ä»¥å¹¶è¡ï¼
- å¡«åæ é¢ (browser_type) + å¤å¶HTMLå°åªè´´æ¿ (Bash)
- è§£æMarkdownçæJSON + çæHTMLæä»¶
â ä¸è½å¹¶è¡ï¼æä¾èµï¼ï¼
- å¿
é¡»å
ç¹å»createæè½ä¸ä¼ å°é¢å¾
- å¿
é¡»å
ç²è´´å
容æè½æå
¥å¾ç
4. è¿ç»æ§è¡æµè§å¨æä½
æ¯ä¸ªæµè§å¨æä½è¿åç页é¢ç¶æå 嫿æéè¦çå ç´ å¼ç¨ãç´æ¥ä½¿ç¨è¿äºå¼ç¨è¿è¡ä¸ä¸æ¥æä½ï¼
# çæ³æµç¨ï¼æ¯æ¥ç´æ¥æ§è¡ï¼ä¸é¢å¤çå¾
ï¼ï¼
browser_navigate â ä»è¿åç¶ææ¾createæé® â browser_click(create)
â ä»è¿åç¶ææ¾ä¸ä¼ æé® â browser_click(ä¸ä¼ ) â browser_file_upload
â ä»è¿åç¶ææ¾åºç¨æé® â browser_click(åºç¨)
â ä»è¿åç¶ææ¾æ 颿¡ â browser_type(æ é¢)
â ç¹å»ç¼è¾å¨ â browser_press_key(Meta+v)
â ...
5. åå¤å·¥ä½åç½®
å¨å¼å§æµè§å¨æä½ä¹åï¼å 宿ææåå¤å·¥ä½ï¼
- è§£æ Markdown è·å JSON æ°æ®
- çæ HTML æä»¶å° /tmp/
- è®°å½ titleãcover_imageãcontent_images çä¿¡æ¯
è¿æ ·æµè§å¨æä½é¶æ®µå¯ä»¥è¿ç»æ§è¡ï¼ä¸éè¦ä¸éå䏿¥å¤çæ°æ®ã
Step 1: Parse Markdown (Python)
Use parse_markdown.py to extract all structured data:
python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md
Output JSON:
{
"title": "Article Title",
"cover_image": "/path/to/first-image.jpg",
"cover_exists": true,
"content_images": [
{"path": "/path/to/img2.jpg", "original_path": "/md/dir/assets/img2.jpg", "exists": true, "block_index": 5, "after_text": "context..."},
{"path": "/path/to/img3.jpg", "original_path": "/md/dir/assets/img3.jpg", "exists": true, "block_index": 12, "after_text": "another..."}
],
"html": "<p>Content...</p><h2>Section</h2>...",
"total_blocks": 45,
"missing_images": 0
}
Key fields:
block_index: The image should be inserted AFTER block element at this index (0-indexed)total_blocks: Total number of block elements in the HTMLafter_text: Kept for reference/debugging only, NOT for positioningexists: Whether the image file was found (if false, upload will fail)original_path: The path resolved from Markdown (before auto-search)path: The actual path to use (may differ from original_path if auto-searched)missing_images: Count of images not found anywhere
Save HTML to temp file for clipboard:
python parse_markdown.py article.md --html-only > /tmp/article_html.html
Step 2: Open X Articles Editor
æµè§å¨é误å¤ç
妿éå° Error: Browser is already in use é误ï¼
# æ¹æ¡1ï¼å
å
³éæµè§å¨åéæ°æå¼
browser_close
browser_navigate: https://x.com/compose/articles
# æ¹æ¡2ï¼å¦æ browser_close æ æï¼éå®ï¼ï¼æç¤ºç¨æ·æå¨å
³é Chrome
# æ¹æ¡3ï¼ä½¿ç¨å·²ææ ç¾é¡µï¼ç´æ¥å¯¼èª
browser_tabs action=list # æ¥çç°ææ ç¾
browser_navigate: https://x.com/compose/articles # å¨å½åæ ç¾å¯¼èª
æä½³å®è·µï¼æ¯æ¬¡å¼å§åå
ç¨ browser_tabs action=list æ£æ¥ç¶æï¼é¿å
å建å¤ä½ç©ºç½æ ç¾ã
导èªå°ç¼è¾å¨
browser_navigate: https://x.com/compose/articles
éè¦: 页é¢å è½½å伿¾ç¤ºè稿å表ï¼ä¸æ¯ç¼è¾å¨ãéè¦ï¼
- çå¾
页é¢å è½½å®æ: 使ç¨
browser_snapshotæ£æ¥é¡µé¢ç¶æ - ç«å³ç¹å» “create” æé®: ä¸è¦çå¾ “æ·»å æ 颔 çç¼è¾å¨å ç´ ï¼å®ä»¬åªæç¹å» create åæåºç°
- çå¾ ç¼è¾å¨å è½½: ç¹å» create åï¼çå¾ ç¼è¾å¨å ç´ åºç°
# 1. 导èªå°é¡µé¢
browser_navigate: https://x.com/compose/articles
# 2. è·å页é¢å¿«ç
§ï¼æ¾å° create æé®
browser_snapshot
# 3. ç¹å» create æé®ï¼é常 ref 类似 "create" æå¸¦æ create æ ç¾ï¼
browser_click: element="create button", ref=<create_button_ref>
# 4. ç°å¨ç¼è¾å¨åºè¯¥æå¼äºï¼å¯ä»¥ç»§ç»ä¸ä¼ å°é¢å¾çæä½
注æ: ä¸è¦ä½¿ç¨ browser_wait_for text="æ·»å æ é¢" æ¥çå¾
页é¢å è½½ï¼å 为è¿ä¸ªææ¬åªæå¨ç¹å» create åæåºç°ï¼ä¼å¯¼è´è¶
æ¶ã
If login needed, prompt user to log in manually.
Step 3: Upload Cover Image
- Click “æ·»å ç §çæè§é¢” button
- Use browser_file_upload with the cover image path (from JSON output)
- Verify image uploaded
Step 4: Fill Title
- Find textbox with “æ·»å æ 颔 placeholder
- Use browser_type to input title (from JSON output)
Step 5: Paste Text Content (Python Clipboard)
Copy HTML to system clipboard using Python, then paste:
# Copy HTML to clipboard
python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py html --file /tmp/article_html.html
Then in browser:
browser_click on editor textbox
browser_press_key: Meta+v
This preserves all rich text formatting (H2, bold, links, lists).
Step 6: Insert Content Images (Text Search Positioning)
æ¨èæ¹æ³: ä½¿ç¨ after_text æåæç´¢å®ä½ï¼æ¯ block_index æ´ç´è§å¯é ã
å®ä½åç
æ¯å¼ å¾çç after_text åæ®µè®°å½äºå®åä¸ä¸ªæ®µè½çæ«å°¾æåï¼æå¤80å符ï¼ãå¨ç¼è¾å¨ä¸æç´¢å
å«è¯¥æåçæ®µè½ï¼ç¹å»åæå
¥å¾çã
æä½æ¥éª¤
For each content image (from content_images array), æ block_index ä»å¤§å°å°ç顺åºï¼
# 1. Copy image to clipboard (with compression)
python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py image /path/to/img.jpg --quality 85
# 2. å¨ browser_snapshot ä¸æç´¢å
å« after_text çæ®µè½
# æ¾å°è¯¥æ®µè½ç ref
# 3. Click the paragraph containing after_text
browser_click: element="paragraph with target text", ref=<paragraph_ref>
# 4. **å
³é®æ¥éª¤**: æ End é®ç§»å¨å
æ å°è¡å°¾
# è¿ä¸æ¥é常éè¦ï¼é¿å
ç¹å»å°æ®µè½ä¸ç龿¥å¯¼è´ä½ç½®åç§»
browser_press_key: End
# 5. Paste image
browser_press_key: Meta+v
# 6. Wait for upload (only use textGone, no time parameter)
browser_wait_for textGone="æ£å¨ä¸ä¼ åªä½"
为ä»ä¹éè¦æ End é®ï¼
é®é¢: 彿®µè½å
å«é¾æ¥æ¶ï¼å¦ [龿¥æå](url)ï¼ï¼ç¹å»æ®µè½å¯è½ä¼ï¼
- 触å龿¥ç¼è¾å¼¹çª
- å°å æ å®ä½å¨é¾æ¥å é¨èéæ®µè½æ«å°¾
è§£å³æ¹æ¡: ç¹å»æ®µè½åç«å³æ End é®ï¼
- ç¡®ä¿å æ ç§»å¨å°æ®µè½æ«å°¾
- é¿å 龿¥å¹²æ°
- å¾çå°æ£ç¡®æå ¥å¨è¯¥æ®µè½ä¹å
å®ä½çç¥
å¨ browser_snapshot è¿åçç»æä¸ï¼æç´¢ after_text çå
³é®è¯ï¼
textbox [ref=editor]:
generic [ref=p1]:
- StaticText: "å
æ¦åææå¨å®¶éç¿»ææºç¸å..." # 妿 after_text å
å«è¿æ®µæåï¼ç¹å» p1
heading [ref=h1]:
- StaticText: "æ¼ç¤º"
generic [ref=p2]:
- StaticText: "è¿ä¸è¥¿å°åºæå¤çäºå¿ï¼"
- link [ref=link1]: "Claude Code" # 注æï¼æ®µè½å¯è½å
å«é¾æ¥
...
ååæå ¥ç¤ºä¾
妿æ3å¼ å¾çï¼block_index åå«ä¸º 5, 12, 27ï¼
- å æå ¥ block_index=27 çå¾çï¼after_text æç´¢ + End + ç²è´´ï¼
- åæå ¥ block_index=12 çå¾ç
- æåæå ¥ block_index=5 çå¾ç
ä»å¤§å°å°æå ¥å¯ä»¥é¿å ä½ç½®åç§»é®é¢ã
Step 6.5: Insert Dividers (Via Menu)
éè¦: Markdown ä¸ç --- åå²çº¿ä¸è½éè¿ HTML <hr> æ ç¾ç²è´´ï¼X Articles ä¼å¿½ç¥å®ï¼ãå¿
é¡»éè¿ X Articles ç Insert èåæå
¥ã
æä½æ¥éª¤
For each divider (from dividers array), in reverse order of block_index:
# 1. Click the block element at block_index position
browser_click on the element at position block_index in the editor
# 2. Open Insert menu (Add Media button)
browser_click on "Insert" or "æ·»å åªä½" button
# 3. Click Divider menu item
browser_click on "Divider" or "åå²çº¿" menuitem
# Divider is inserted at cursor position
ä¸å¾ççæå ¥é¡ºåº
å»ºè®®å æå ¥ææå¾çï¼åæå ¥ææåå²çº¿ã两è 齿 block_index ä»å¤§å°å°ç顺åºï¼
- æå ¥ææå¾çï¼ä»æå¤§ block_index å¼å§ï¼
- æå ¥ææåå²çº¿ï¼ä»æå¤§ block_index å¼å§ï¼
Step 7: Save Draft
- Verify content pasted (check word count indicator)
- Draft auto-saves, or click Save button if needed
- Click “é¢è§” to verify formatting
- Report: “Draft saved. Review and publish manually.”
Critical Rules
- NEVER publish – Only save draft
- First image = cover – Upload first image as cover image
- Rich text conversion – Always convert Markdown to HTML before pasting
- Use clipboard API – Paste via clipboard for proper formatting
- Block index positioning – Use block_index for precise image/divider placement
- Reverse order insertion – Insert images and dividers from highest to lowest block_index
- H1 title handling – H1 is used as title only, not included in body
- Dividers via menu – Markdown
---must be inserted via Insert > Divider menu (HTML<hr>is ignored)
Supported Formatting
| Element | Support | Notes |
|---|---|---|
H2 (##) |
Native | Section headers |
Bold (**) |
Native | Strong emphasis |
Italic (*) |
Native | Emphasis |
Links ([](url)) |
Native | Hyperlinks |
| Ordered lists | Native | 1. 2. 3. |
| Unordered lists | Native | – bullets |
Blockquotes (>) |
Native | Quoted text |
| Code blocks | Converted | â Blockquotes |
| Tables | Converted | â PNG images (use table_to_image.py) |
| Mermaid | Converted | â PNG images (use mmdc) |
Dividers (---) |
Menu insert | â Insert > Divider |
Example Flow
User: “Publish /path/to/article.md to X”
# Step 1: Parse Markdown
python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md > /tmp/article.json
python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md --html-only > /tmp/article_html.html
- Navigate to https://x.com/compose/articles
- Upload cover image (browser_file_upload for cover only)
- Fill title (from JSON:
title) - Copy & paste HTML:
Then: browser_press_key Meta+vpython ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py html --file /tmp/article_html.html - For each content image, in reverse order of block_index:
python copy_to_clipboard.py image /path/to/img.jpg --quality 85- Click block element at
block_indexposition - browser_press_key Meta+v
- Wait until upload complete
- Click block element at
- Verify in preview
- “Draft saved. Please review and publish manually.”
Best Practices
为ä»ä¹ç¨ block_index èéæåå¹é ï¼
- 精确å®ä½: ä¸ä¾èµæåå 容ï¼å³ä½¿å¤å¤æåç¸ä¼¼ä¹è½æ£ç¡®å®ä½
- å¯é æ§: ç´¢å¼æ¯ç¡®å®æ§çï¼ä¸ä¼å 为æåç¸ä¼¼èæ··æ·
- è°è¯æ¹ä¾¿:
after_textä»ä¿çç¨äºäººå·¥æ ¸éª
为ä»ä¹ç¨ Python èéæµè§å¨å JavaScriptï¼
- æ¬å°å¤çæ´å¯é : Python ç´æ¥æä½ç³»ç»åªè´´æ¿ï¼ä¸åæµè§å¨æ²çéå¶
- å¾çå缩: ä¸ä¼ åå缩å¾ç (–quality 85)ï¼åå°ä¸ä¼ æ¶é´
- 代ç å¤ç¨: èæ¬åºå®ä¸åï¼æ éæ¯æ¬¡éæ°ç¼å转æ¢é»è¾
- è°è¯æ¹ä¾¿: èæ¬å¯åç¬æµè¯ï¼é®é¢æå®ä½
çå¾ çç¥
éè¦åç°: Playwright MCP ç browser_wait_for å®é
è¡ä¸ºæ¯ å
çå¾
time ç§ï¼åæ£æ¥æ¡ä»¶ï¼èé轮询ï¼
// å®é
æ§è¡ç代ç ï¼
await new Promise(f => setTimeout(f, time * 1000)); // å
åºå®çå¾
await page.getByText("xxx").waitFor({ state: 'hidden' }); // 忣æ¥
æ£ç¡®ç¨æ³:
- â
åªç¨
textGoneï¼ä¸è®¾timeï¼è®© Playwright èªå·±è½®è¯¢çå¾ - â
åªç¨
timeï¼åºå®çå¾ æå®ç§æ° - â åæ¶ç¨
textGone+timeï¼ä¼å ç time ç§åæ£æ¥ï¼æµªè´¹æ¶é´
# æ¨èï¼åªç¨ textGoneï¼è®©å®èªå¨çå¾
æ¡ä»¶æ»¡è¶³
browser_wait_for textGone="æ£å¨ä¸ä¼ åªä½"
# æè
ï¼ç¨ browser_snapshot è½®è¯¢æ£æ¥ç¶æ
# æ¯æ¬¡æä½åæ£æ¥è¿åç页é¢ç¶æï¼æ éé¢å¤çå¾
å¾çæå ¥æç
æ¯å¼ å¾ççæµè§å¨æä½ä»5æ¥åå°å°2æ¥ï¼
- æ§: ç¹å» â æ·»å åªä½ â åªä½ â æ·»å ç §ç â file_upload
- æ°: ç¹å»æ®µè½ â Meta+v
å°é¢å¾ vs å 容å¾
- å°é¢å¾: ä½¿ç¨ browser_file_uploadï¼å 为æä¸é¨çä¸ä¼ æé®ï¼
- å 容å¾: ä½¿ç¨ Python åªè´´æ¿ + ç²è´´ï¼æ´é«æï¼
æ éæé¤
MCP è¿æ¥é®é¢
妿 Playwright MCP å·¥å
·ä¸å¯ç¨ï¼æ¥é No such tool available æ Not connectedï¼ï¼
æ¹æ¡1ï¼éæ°è¿æ¥ MCPï¼æ¨èï¼
æ§è¡ /mcp å½ä»¤ï¼éæ© playwrightï¼éæ© Restart
æ¹æ¡2ï¼æ¸ çæ®çè¿ç¨åéè¿
# æææææ®çç playwright è¿ç¨
pkill -f "mcp-server-playwright"
pkill -f "@playwright/mcp"
# ç¶åæ§è¡ /mcp éæ°è¿æ¥
é
ç½®æä»¶ä½ç½®: ~/.claude/mcp_servers.json
æµè§å¨é误å¤ç
妿éå° Error: Browser is already in use é误ï¼
# æ¹æ¡1ï¼å
å
³éæµè§å¨åéæ°æå¼
browser_close
browser_navigate: https://x.com/compose/articles
# æ¹æ¡2ï¼ææ Chrome è¿ç¨
pkill -f "Chrome.*--remote-debugging"
# ç¶åéæ° navigate
å¾çä½ç½®åç§»
妿å¾çæå ¥ä½ç½®ä¸æ£ç¡®ï¼ç¹å«æ¯ç¹å»å«é¾æ¥çæ®µè½æ¶ï¼ï¼
åå : ç¹å»æ®µè½æ¶å¯è½è¯¯è§¦é¾æ¥ï¼å¯¼è´å æ ä½ç½®é误
è§£å³æ¹æ¡: ç¹å»åå¿ é¡»æ End é®ç§»å¨å æ å°è¡å°¾
# æ£ç¡®æµç¨
1. browser_click ç¹å»ç®æ 段è½
2. browser_press_key: End # å
³é®æ¥éª¤ï¼
3. browser_press_key: Meta+v # ç²è´´å¾ç
4. browser_wait_for textGone="æ£å¨ä¸ä¼ åªä½"
å¾çè·¯å¾æ¾ä¸å°
妿 Markdown ä¸çç¸å¯¹è·¯å¾å¾çæ¾ä¸å°ï¼å¦ ./assets/image.png å®é
å¨å
¶ä»ä½ç½®ï¼ï¼
èªå¨æç´¢: parse_markdown.py ä¼èªå¨å¨ä»¥ä¸ç®å½æç´¢ååæä»¶ï¼
~/Downloads~/Desktop~/Pictures
stderr è¾åºç¤ºä¾:
[parse_markdown] Image not found at '/path/to/assets/img.png', using '/Users/xxx/Downloads/img.png' instead
JSON åæ®µè¯´æ:
original_path: Markdown 䏿å®çè·¯å¾ï¼è§£æåçç»å¯¹è·¯å¾ï¼path: å®é 使ç¨çè·¯å¾ï¼å¦æèªå¨æç´¢æåï¼ä¼ä¸åäº original_pathï¼exists:true表示æ¾å°æä»¶ï¼falseè¡¨ç¤ºæªæ¾å°ï¼ä¸ä¼ ä¼å¤±è´¥ï¼
妿ä»ç¶æ¾ä¸å°:
- æ£æ¥ JSON è¾åºä¸ç
missing_imagesåæ®µ - æå¨å°å¾çå¤å¶å° Markdown æä»¶åç®å½ç
assets/åç®å½ - æä¿®æ¹ Markdown ä¸çå¾çè·¯å¾ä¸ºç»å¯¹è·¯å¾