x-article-publisher

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

X Article Publisher

X 文章发布器

Publish Markdown content to X (Twitter) Articles editor, preserving formatting with rich text conversion.
将Markdown内容发布到X(原Twitter)文章编辑器,通过富文本转换保留格式。

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
  • For Mermaid diagrams:
    npm install -g @mermaid-js/mermaid-cli
  • 用于浏览器自动化的Playwright MCP
  • 已登录X且拥有Premium Plus订阅的用户
  • 安装Python 3.9+及对应依赖:
    • macOS:
      pip install Pillow pyobjc-framework-Cocoa
    • Windows:
      pip install Pillow pywin32 clip-util
  • 若需处理Mermaid图表:
    npm install -g @mermaid-js/mermaid-cli

Scripts

脚本文件

Located in
~/.claude/skills/x-article-publisher/scripts/
:
位于
~/.claude/skills/x-article-publisher/scripts/

parse_markdown.py

parse_markdown.py

Parse Markdown and extract structured data:
bash
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
解析Markdown并提取结构化数据:
bash
python parse_markdown.py <markdown_file> [--output json|html] [--html-only]
返回包含以下字段的JSON:标题、cover_image、content_images、dividers(含定位用的block_index)、html、total_blocks

copy_to_clipboard.py

copy_to_clipboard.py

Copy image or HTML to system clipboard (cross-platform):
bash
undefined
将图片或HTML复制到系统剪贴板(跨平台):
bash
undefined

Copy image (with optional compression)

复制图片(可选压缩)

python copy_to_clipboard.py image /path/to/image.jpg [--quality 80]
python copy_to_clipboard.py image /path/to/image.jpg [--quality 80]

Copy HTML for rich text paste

复制HTML用于富文本粘贴

python copy_to_clipboard.py html --file /path/to/content.html
undefined
python copy_to_clipboard.py html --file /path/to/content.html
undefined

table_to_image.py

table_to_image.py

Convert Markdown table to PNG image:
bash
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.
将Markdown表格转换为PNG图片:
bash
python table_to_image.py <input.md> <output.png> [--scale 2]
当X文章不支持原生表格渲染或需要统一样式时使用。

Pre-Processing (Optional)

预处理(可选)

Before publishing, scan the Markdown for elements that need conversion:
发布前,扫描Markdown中需要转换的元素:

Tables → PNG

表格 → PNG

bash
undefined
bash
undefined

Extract table to temp file, then convert

提取表格到临时文件,然后转换

python ~/.claude/skills/x-article-publisher/scripts/table_to_image.py /tmp/table.md /tmp/table.png
python ~/.claude/skills/x-article-publisher/scripts/table_to_image.py /tmp/table.md /tmp/table.png

Replace table in markdown with: Table

将Markdown中的表格替换为:Table

undefined
undefined

Mermaid Diagrams → PNG

Mermaid图表 → PNG

bash
undefined
bash
undefined

Extract mermaid block to .mmd file, then convert

提取Mermaid块到.mmd文件,然后转换

mmdc -i /tmp/diagram.mmd -o /tmp/diagram.png -b white -s 2
mmdc -i /tmp/diagram.mmd -o /tmp/diagram.png -b white -s 2

Replace mermaid block with: Diagram

将Mermaid块替换为:Diagram

undefined
undefined

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).
分割线会被
parse_markdown.py
自动检测并输出到
dividers
数组中。必须通过X文章的插入 > 分割线菜单插入(X会忽略HTML
<hr>
标签)。

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.
  1. (Optional) Pre-process: Convert tables/mermaid to images
  2. Parse Markdown with Python script → get title, images, dividers with block_index, HTML
  3. Navigate to X Articles editor
  4. Upload cover image (first image)
  5. Fill title
  6. Copy HTML to clipboard (Python) → Paste with Cmd+V
  7. Insert content images at positions specified by block_index
  8. Insert dividers at positions specified by block_index (via Insert > Divider menu)
  9. Save as draft (NEVER auto-publish)
策略:"先文后图后分割线"
对于包含图片和分割线的文章,先粘贴所有文本内容,再根据block_index在正确位置插入图片和分割线。
  1. (可选)预处理:将表格/Mermaid转换为图片
  2. 使用Python脚本解析Markdown → 获取标题、图片、含block_index的分割线、HTML
  3. 导航到X文章编辑器
  4. 上传封面图(第一张图片)
  5. 填写标题
  6. 使用Python将HTML复制到剪贴板 → 按Cmd+V粘贴
  7. 根据block_index指定的位置插入内容图片
  8. 根据block_index指定的位置插入分割线(通过插入 > 分割线菜单)
  9. 保存为草稿(绝不自动发布)

高效执行原则 (Efficiency Guidelines)

高效执行原则

目标: 最小化操作之间的等待时间,实现流畅的自动化体验。
目标: 最小化操作间的等待时间,实现流畅的自动化体验。

1. 避免不必要的 browser_snapshot

1. 避免不必要的browser_snapshot

大多数浏览器操作(click, type, press_key 等)都会在返回结果中包含页面状态。不要在每次操作后单独调用
browser_snapshot
,直接使用操作返回的页面状态即可。
❌ 错误做法:
browser_click → browser_snapshot → 分析 → browser_click → browser_snapshot → ...

✅ 正确做法:
browser_click → 从返回结果中获取页面状态 → browser_click → ...
大多数浏览器操作(click、type、press_key等)会在返回结果中包含页面状态。不要在每次操作后单独调用
browser_snapshot
,直接使用操作返回的页面状态即可。
❌ 错误做法:
browser_click → browser_snapshot → 分析 → browser_click → browser_snapshot → ...

✅ 正确做法:
browser_click → 从返回状态中获取页面信息 → browser_click → ...

2. 避免不必要的 browser_wait_for

2. 避免不必要的browser_wait_for

只在以下情况使用
browser_wait_for
  • 等待图片上传完成(
    textGone="正在上传媒体"
  • 等待页面初始加载(极少数情况)
不要使用
browser_wait_for
来等待按钮或输入框出现 - 它们在页面加载完成后立即可用。
仅在以下情况使用
browser_wait_for
  • 等待图片上传完成(
    textGone="正在上传媒体"
  • 等待页面初始加载(极少数情况)
不要使用
browser_wait_for
等待按钮或输入框出现 - 页面加载完成后它们立即可用。

3. 并行执行独立操作

3. 并行执行独立操作

当两个操作没有依赖关系时,可以在同一个消息中并行调用多个工具:
✅ 可以并行:
- 填写标题 (browser_type) + 复制HTML到剪贴板 (Bash)
- 解析Markdown生成JSON + 生成HTML文件

❌ 不能并行(有依赖):
- 必须先点击create才能上传封面图
- 必须先粘贴内容才能插入图片
当两个操作无依赖关系时,可在同一消息中并行调用多个工具:
✅ 可以并行:
- 填写标题 (browser_type) + 将HTML复制到剪贴板 (Bash)
- 解析Markdown生成JSON + 生成HTML文件

❌ 不能并行(有依赖):
- 必须先点击创建按钮才能上传封面图
- 必须先粘贴内容才能插入图片

4. 连续执行浏览器操作

4. 连续执行浏览器操作

每个浏览器操作返回的页面状态包含所有需要的元素引用。直接使用这些引用进行下一步操作:
undefined
每个浏览器操作返回的页面状态包含所有需要的元素引用。直接使用这些引用进行下一步操作:
undefined

理想流程(每步直接执行,不额外等待):

理想流程(每步直接执行,无额外等待):

browser_navigate → 从返回状态找create按钮 → browser_click(create) → 从返回状态找上传按钮 → browser_click(上传) → browser_file_upload → 从返回状态找应用按钮 → browser_click(应用) → 从返回状态找标题框 → browser_type(标题) → 点击编辑器 → browser_press_key(Meta+v) → ...
undefined
browser_navigate → 从返回状态中找到创建按钮 → browser_click(create) → 从返回状态中找到上传按钮 → browser_click(上传) → browser_file_upload → 从返回状态中找到应用按钮 → browser_click(应用) → 从返回状态中找到标题框 → browser_type(标题) → 点击编辑器 → browser_press_key(Meta+v) → ...
undefined

5. 准备工作前置

5. 准备工作前置

在开始浏览器操作之前,先完成所有准备工作:
  1. 解析 Markdown 获取 JSON 数据
  2. 生成 HTML 文件到 /tmp/
  3. 记录 title、cover_image、content_images 等信息
这样浏览器操作阶段可以连续执行,不需要中途停下来处理数据。
在开始浏览器操作前,先完成所有准备工作:
  1. 解析Markdown获取JSON数据
  2. 生成HTML文件到/tmp/
  3. 记录title、cover_image、content_images等信息
这样浏览器操作阶段可连续执行,无需中途处理数据。

Step 1: Parse Markdown (Python)

步骤1:解析Markdown(Python)

Use
parse_markdown.py
to extract all structured data:
bash
python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md
Output JSON:
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 HTML
  • after_text
    : Kept for reference/debugging only, NOT for positioning
  • exists
    : 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:
bash
python parse_markdown.py article.md --html-only > /tmp/article_html.html
使用
parse_markdown.py
提取所有结构化数据:
bash
python ~/.claude/skills/x-article-publisher/scripts/parse_markdown.py /path/to/article.md
输出JSON:
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
}
关键字段:
  • block_index
    : 图片应插入到此索引对应的块元素之后(从0开始计数)
  • total_blocks
    : HTML中的块元素总数
  • after_text
    : 仅用于参考/调试,不用于定位
  • exists
    : 是否找到图片文件(若为false,上传会失败)
  • original_path
    : Markdown中解析出的路径(自动搜索前)
  • path
    : 实际使用的路径(若自动搜索成功,可能与original_path不同)
  • missing_images
    : 未找到的图片数量
将HTML保存到临时文件用于剪贴板:
bash
python parse_markdown.py article.md --html-only > /tmp/article_html.html

Step 2: Open X Articles Editor

步骤2:打开X文章编辑器

浏览器错误处理

浏览器错误处理

如果遇到
Error: Browser is already in use
错误:
undefined
若遇到
Error: Browser is already in use
错误:
undefined

方案1:先关闭浏览器再重新打开

方案1:先关闭浏览器再重新打开

browser_close browser_navigate: https://x.com/compose/articles
browser_close browser_navigate: https://x.com/compose/articles

方案2:如果 browser_close 无效(锁定),提示用户手动关闭 Chrome

方案2:若browser_close无效(锁定),提示用户手动关闭Chrome

方案3:使用已有标签页,直接导航

方案3:使用已有标签页,直接导航

browser_tabs action=list # 查看现有标签 browser_navigate: https://x.com/compose/articles # 在当前标签导航

**最佳实践**:每次开始前先用 `browser_tabs action=list` 检查状态,避免创建多余空白标签。
browser_tabs action=list # 查看现有标签 browser_navigate: https://x.com/compose/articles # 在当前标签导航

**最佳实践**:每次开始前先用`browser_tabs action=list`检查状态,避免创建多余空白标签。

导航到编辑器

导航到编辑器

browser_navigate: https://x.com/compose/articles
重要: 页面加载后会显示草稿列表,不是编辑器。需要:
  1. 等待页面加载完成: 使用
    browser_snapshot
    检查页面状态
  2. 立即点击 "create" 按钮: 不要等待 "添加标题" 等编辑器元素,它们只有点击 create 后才出现
  3. 等待编辑器加载: 点击 create 后,等待编辑器元素出现
undefined
browser_navigate: https://x.com/compose/articles
重要: 页面加载后会显示草稿列表,而非编辑器。需要:
  1. 等待页面加载完成: 使用
    browser_snapshot
    检查页面状态
  2. 立即点击"创建"按钮: 不要等待“添加标题”等编辑器元素,它们仅在点击创建后才会出现
  3. 等待编辑器加载: 点击创建后,等待编辑器元素出现
undefined

1. 导航到页面

1. 导航到页面

2. 获取页面快照,找到 create 按钮

2. 获取页面快照,找到创建按钮

browser_snapshot
browser_snapshot

3. 点击 create 按钮(通常 ref 类似 "create" 或带有 create 标签)

3. 点击创建按钮(通常ref类似"create"或带有create标签)

browser_click: element="create button", ref=<create_button_ref>
browser_click: element="create button", ref=<create_button_ref>

4. 现在编辑器应该打开了,可以继续上传封面图等操作

4. 现在编辑器应该已打开,可继续上传封面图等操作


**注意**: 不要使用 `browser_wait_for text="添加标题"` 来等待页面加载,因为这个文本只有在点击 create 后才出现,会导致超时。

If login needed, prompt user to log in manually.

**注意**: 不要使用`browser_wait_for text="添加标题"`等待页面加载,因为该文本仅在点击创建后才会出现,会导致超时。

若需要登录,提示用户手动登录。

Step 3: Upload Cover Image

步骤3:上传封面图

  1. Click "添加照片或视频" button
  2. Use browser_file_upload with the cover image path (from JSON output)
  3. Verify image uploaded
  1. 点击“添加照片或视频”按钮
  2. 使用browser_file_upload并传入JSON输出中的封面图路径
  3. 验证图片上传完成

Step 4: Fill Title

步骤4:填写标题

  • Find textbox with "添加标题" placeholder
  • Use browser_type to input title (from JSON output)
  • 找到占位符为“添加标题”的文本框
  • 使用browser_type输入JSON输出中的标题

Step 5: Paste Text Content (Python Clipboard)

步骤5:粘贴文本内容(Python剪贴板)

Copy HTML to system clipboard using Python, then paste:
bash
undefined
使用Python将HTML复制到系统剪贴板,然后粘贴:
bash
undefined

Copy HTML to clipboard

将HTML复制到剪贴板

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).
python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py html --file /tmp/article_html.html

然后在浏览器中:
browser_click on editor textbox browser_press_key: Meta+v

这将保留所有富文本格式(H2、粗体、链接、列表)。

Step 6: Insert Content Images (Text Search Positioning)

步骤6:插入内容图片(文本搜索定位)

推荐方法: 使用
after_text
文字搜索定位,比
block_index
更直观可靠。
推荐方法: 使用
after_text
文字搜索定位,比
block_index
更直观可靠。

定位原理

定位原理

每张图片的
after_text
字段记录了它前一个段落的末尾文字(最多80字符)。在编辑器中搜索包含该文字的段落,点击后插入图片。
每张图片的
after_text
字段记录了其前一段落的末尾文字(最多80字符)。在编辑器中搜索包含该文字的段落,点击后插入图片。

操作步骤

操作步骤

For each content image (from
content_images
array), 按 block_index 从大到小的顺序
bash
undefined
对于每个内容图片(来自
content_images
数组),按block_index从大到小的顺序
bash
undefined

1. Copy image to clipboard (with compression)

1. 将图片复制到剪贴板(带压缩)

python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py image /path/to/img.jpg --quality 85
undefined
python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py image /path/to/img.jpg --quality 85
undefined

2. 在 browser_snapshot 中搜索包含 after_text 的段落

2. 在browser_snapshot中搜索包含after_text的段落

找到该段落的 ref

找到该段落的ref

3. Click the paragraph containing after_text

3. 点击包含after_text的段落

browser_click: element="paragraph with target text", ref=<paragraph_ref>
browser_click: element="paragraph with target text", ref=<paragraph_ref>

4. 关键步骤: 按 End 键移动光标到行尾

4. 关键步骤: 按End键将光标移动到行尾

这一步非常重要!避免点击到段落中的链接导致位置偏移

这一步非常重要!避免点击到段落中的链接导致位置偏移

browser_press_key: End
browser_press_key: End

5. Paste image

5. 粘贴图片

browser_press_key: Meta+v
browser_press_key: Meta+v

6. Wait for upload (only use textGone, no time parameter)

6. 等待上传完成(仅使用textGone,不设置time参数)

browser_wait_for textGone="正在上传媒体"
undefined
browser_wait_for textGone="正在上传媒体"
undefined

为什么需要按 End 键?

为什么需要按End键?

问题: 当段落包含链接时(如
[链接文字](url)
),点击段落可能会:
  • 触发链接编辑弹窗
  • 将光标定位在链接内部而非段落末尾
解决方案: 点击段落后立即按
End
键:
  • 确保光标移动到段落末尾
  • 避免链接干扰
  • 图片将正确插入在该段落之后
问题: 当段落包含链接时(如
[链接文字](url)
),点击段落可能会:
  • 触发链接编辑弹窗
  • 将光标定位在链接内部而非段落末尾
解决方案: 点击段落后立即按
End
键:
  • 确保光标移动到段落末尾
  • 避免链接干扰
  • 图片将正确插入到该段落之后

定位策略

定位策略

在 browser_snapshot 返回的结构中,搜索
after_text
的关键词:
yaml
textbox [ref=editor]:
  generic [ref=p1]:
    - StaticText: "元旦假期我在家里翻手机相册..."  # 如果 after_text 包含这段文字,点击 p1
  heading [ref=h1]:
    - StaticText: "演示"
  generic [ref=p2]:
    - StaticText: "这东西到底有多省事儿?"
    - link [ref=link1]: "Claude Code"  # 注意:段落可能包含链接
  ...
在browser_snapshot返回的结构中,搜索
after_text
的关键词:
yaml
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:
  1. 先插入 block_index=27 的图片(after_text 搜索 + End + 粘贴)
  2. 再插入 block_index=12 的图片
  3. 最后插入 block_index=5 的图片
从大到小插入可以避免位置偏移问题。
若有3张图片,block_index分别为5、12、27:
  1. 先插入block_index=27的图片(after_text搜索 + End + 粘贴)
  2. 再插入block_index=12的图片
  3. 最后插入block_index=5的图片
从大到小插入可避免位置偏移问题。

Step 6.5: Insert Dividers (Via Menu)

步骤6.5:插入分割线(通过菜单)

重要: Markdown 中的
---
分割线不能通过 HTML
<hr>
标签粘贴(X Articles 会忽略它)。必须通过 X Articles 的 Insert 菜单插入。
重要: Markdown中的
---
分割线无法通过HTML
<hr>
标签粘贴(X文章会忽略它)。必须通过X文章的插入菜单插入。

操作步骤

操作步骤

For each divider (from
dividers
array), in reverse order of block_index:
undefined
对于每个分割线(来自
dividers
数组),按block_index从大到小的顺序
undefined

1. Click the block element at block_index position

1. 点击编辑器中block_index位置的块元素

browser_click on the element at position block_index in the editor
browser_click on the element at position block_index in the editor

2. Open Insert menu (Add Media button)

2. 打开插入菜单(添加媒体按钮)

browser_click on "Insert" or "添加媒体" button
browser_click on "Insert" or "添加媒体" button

3. Click Divider menu item

3. 点击分割菜单项

browser_click on "Divider" or "分割线" menuitem
browser_click on "Divider" or "分割线" menuitem

Divider is inserted at cursor position

分割线将插入到光标位置

undefined
undefined

与图片的插入顺序

与图片的插入顺序

建议先插入所有图片,再插入所有分割线。两者都按 block_index 从大到小的顺序:
  1. 插入所有图片(从最大 block_index 开始)
  2. 插入所有分割线(从最大 block_index 开始)
建议先插入所有图片,再插入所有分割线。两者都按block_index从大到小的顺序:
  1. 插入所有图片(从最大的block_index开始)
  2. 插入所有分割线(从最大的block_index开始)

Step 7: Save Draft

步骤7:保存草稿

  1. Verify content pasted (check word count indicator)
  2. Draft auto-saves, or click Save button if needed
  3. Click "预览" to verify formatting
  4. Report: "Draft saved. Review and publish manually."
  1. 验证内容已粘贴(检查字数统计指示器)
  2. 草稿会自动保存,或按需点击保存按钮
  3. 点击“预览”验证格式
  4. 反馈:“草稿已保存,请审核后手动发布。”

Critical Rules

关键规则

  1. NEVER publish - Only save draft
  2. First image = cover - Upload first image as cover image
  3. Rich text conversion - Always convert Markdown to HTML before pasting
  4. Use clipboard API - Paste via clipboard for proper formatting
  5. Block index positioning - Use block_index for precise image/divider placement
  6. Reverse order insertion - Insert images and dividers from highest to lowest block_index
  7. H1 title handling - H1 is used as title only, not included in body
  8. Dividers via menu - Markdown
    ---
    must be inserted via Insert > Divider menu (HTML
    <hr>
    is ignored)
  1. 绝不自动发布 - 仅保存草稿
  2. 第一张图片为封面 - 上传第一张图片作为封面图
  3. 富文本转换 - 发布前务必将Markdown转换为HTML
  4. 使用剪贴板API - 通过剪贴板粘贴以保证格式正确
  5. Block index定位 - 使用block_index精确定位图片/分割线位置
  6. 反向插入顺序 - 按block_index从高到低插入图片和分割线
  7. H1标题处理 - H1仅用作标题,不包含正文中
  8. 分割线通过菜单插入 - Markdown
    ---
    必须通过插入 > 分割线菜单插入(HTML
    <hr>
    会被忽略)

Supported Formatting

支持的格式

ElementSupportNotes
H2 (
##
)
NativeSection headers
Bold (
**
)
NativeStrong emphasis
Italic (
*
)
NativeEmphasis
Links (
[](url)
)
NativeHyperlinks
Ordered listsNative1. 2. 3.
Unordered listsNative- bullets
Blockquotes (
>
)
NativeQuoted text
Code blocksConverted→ Blockquotes
TablesConverted→ PNG images (use table_to_image.py)
MermaidConverted→ PNG images (use mmdc)
Dividers (
---
)
Menu insert→ Insert > Divider
元素支持情况说明
H2 (
##
)
原生支持章节标题
粗体 (
**
)
原生支持强调
斜体 (
*
)
原生支持斜体强调
链接 (
[](url)
)
原生支持超链接
有序列表原生支持1. 2. 3.
无序列表原生支持- 项目符号
块引用 (
>
)
原生支持引用文本
代码块转换支持→ 块引用
表格转换支持→ PNG图片(使用table_to_image.py)
Mermaid图表转换支持→ PNG图片(使用mmdc)
分割线 (
---
)
菜单插入→ 插入 > 分割线

Example Flow

示例流程

User: "Publish /path/to/article.md to X"
bash
undefined
用户:“将/path/to/article.md发布到X”
bash
undefined

Step 1: Parse Markdown

步骤1:解析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

2. Navigate to https://x.com/compose/articles
3. Upload cover image (browser_file_upload for cover only)
4. Fill title (from JSON: `title`)
5. Copy & paste HTML:
   ```bash
   python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py html --file /tmp/article_html.html
Then: browser_press_key Meta+v 6. For each content image, in reverse order of block_index:
bash
python copy_to_clipboard.py image /path/to/img.jpg --quality 85
  • Click block element at
    block_index
    position
  • browser_press_key Meta+v
  • Wait until upload complete
  1. Verify in preview
  2. "Draft saved. Please review and publish manually."
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

2. 导航到https://x.com/compose/articles
3. 上传封面图(仅对封面图使用browser_file_upload)
4. 填写标题(来自JSON的`title`字段)
5. 复制并粘贴HTML:
   ```bash
   python ~/.claude/skills/x-article-publisher/scripts/copy_to_clipboard.py html --file /tmp/article_html.html
然后:browser_press_key Meta+v 6. 对于每个内容图片,按block_index从大到小的顺序
bash
python copy_to_clipboard.py image /path/to/img.jpg --quality 85
  • 点击
    block_index
    位置的块元素
  • browser_press_key Meta+v
  • 等待上传完成
  1. 在预览中验证
  2. 反馈:“草稿已保存,请审核后手动发布。”

Best Practices

最佳实践

为什么用 block_index 而非文字匹配?

为什么用block_index而非文字匹配?

  1. 精确定位: 不依赖文字内容,即使多处文字相似也能正确定位
  2. 可靠性: 索引是确定性的,不会因为文字相似而混淆
  3. 调试方便:
    after_text
    仍保留用于人工核验
  1. 精确定位: 不依赖文字内容,即使多处文字相似也能正确定位
  2. 可靠性: 索引是确定性的,不会因文字相似而混淆
  3. 调试方便:
    after_text
    仍保留用于人工核验

为什么用 Python 而非浏览器内 JavaScript?

为什么用Python而非浏览器内JavaScript?

  1. 本地处理更可靠: Python 直接操作系统剪贴板,不受浏览器沙盒限制
  2. 图片压缩: 上传前压缩图片 (--quality 85),减少上传时间
  3. 代码复用: 脚本固定不变,无需每次重新编写转换逻辑
  4. 调试方便: 脚本可单独测试,问题易定位
  1. 本地处理更可靠: Python直接操作系统剪贴板,不受浏览器沙盒限制
  2. 图片压缩: 上传前压缩图片(--quality 85),减少上传时间
  3. 代码复用: 脚本固定不变,无需每次重新编写转换逻辑
  4. 调试方便: 脚本可单独测试,问题易定位

等待策略

等待策略

重要发现: Playwright MCP 的
browser_wait_for
实际行为是 先等待 time 秒,再检查条件,而非轮询!
javascript
// 实际执行的代码:
await new Promise(f => setTimeout(f, time * 1000));  // 先固定等待
await page.getByText("xxx").waitFor({ state: 'hidden' });  // 再检查
正确用法:
  • ✅ 只用
    textGone
    ,不设
    time
    :让 Playwright 自己轮询等待
  • ✅ 只用
    time
    :固定等待指定秒数
  • ❌ 同时用
    textGone
    +
    time
    :会先等 time 秒再检查,浪费时间
undefined
重要发现: Playwright MCP的
browser_wait_for
实际行为是先等待time秒,再检查条件,而非轮询!
javascript
// 实际执行的代码:
await new Promise(f => setTimeout(f, time * 1000));  // 先固定等待
await page.getByText("xxx").waitFor({ state: 'hidden' });  // 再检查
正确用法:
  • ✅ 仅使用
    textGone
    ,不设置
    time
    :让Playwright自动轮询等待
  • ✅ 仅使用
    time
    :固定等待指定秒数
  • ❌ 同时使用
    textGone
    +
    time
    :会先等待time秒再检查,浪费时间
undefined

推荐:只用 textGone,让它自动等待条件满足

推荐:仅使用textGone,让它自动等待条件满足

browser_wait_for textGone="正在上传媒体"
browser_wait_for textGone="正在上传媒体"

或者:用 browser_snapshot 轮询检查状态

或者:使用browser_snapshot轮询检查状态

每次操作后检查返回的页面状态,无需额外等待

每次操作后检查返回的页面状态,无需额外等待

undefined
undefined

图片插入效率

图片插入效率

每张图片的浏览器操作从5步减少到2步:
  • 旧: 点击 → 添加媒体 → 媒体 → 添加照片 → file_upload
  • 新: 点击段落 → Meta+v
每张图片的浏览器操作从5步减少到2步:
  • 旧流程: 点击 → 添加媒体 → 媒体 → 添加照片 → file_upload
  • 新流程: 点击段落 → Meta+v

封面图 vs 内容图

封面图 vs 内容图

  • 封面图: 使用 browser_file_upload(因为有专门的上传按钮)
  • 内容图: 使用 Python 剪贴板 + 粘贴(更高效)
  • 封面图: 使用browser_file_upload(因为有专门的上传按钮)
  • 内容图: 使用Python剪贴板 + 粘贴(更高效)

故障排除

故障排除

MCP 连接问题

MCP连接问题

如果 Playwright MCP 工具不可用(报错
No such tool available
Not connected
):
方案1:重新连接 MCP(推荐)
执行 /mcp 命令,选择 playwright,选择 Restart
方案2:清理残留进程后重连
bash
undefined
若Playwright MCP工具不可用(报错
No such tool available
Not connected
):
方案1:重新连接MCP(推荐)
执行/mcp命令,选择playwright,选择Restart
方案2:清理残留进程后重连
bash
undefined

杀掉所有残留的 playwright 进程

杀掉所有残留的playwright进程

pkill -f "mcp-server-playwright" pkill -f "@playwright/mcp"
pkill -f "mcp-server-playwright" pkill -f "@playwright/mcp"

然后执行 /mcp 重新连接

然后执行/mcp重新连接


**配置文件位置**: `~/.claude/mcp_servers.json`

**配置文件位置**: `~/.claude/mcp_servers.json`

浏览器错误处理

浏览器错误处理

如果遇到
Error: Browser is already in use
错误:
bash
undefined
若遇到
Error: Browser is already in use
错误:
bash
undefined

方案1:先关闭浏览器再重新打开

方案1:先关闭浏览器再重新打开

browser_close browser_navigate: https://x.com/compose/articles
browser_close browser_navigate: https://x.com/compose/articles

方案2:杀掉 Chrome 进程

方案2:杀掉Chrome进程

pkill -f "Chrome.*--remote-debugging"
pkill -f "Chrome.*--remote-debugging"

然后重新 navigate

然后重新导航

undefined
undefined

图片位置偏移

图片位置偏移

如果图片插入位置不正确(特别是点击含链接的段落时):
原因: 点击段落时可能误触链接,导致光标位置错误
解决方案: 点击后必须按 End 键移动光标到行尾
undefined
若图片插入位置不正确(特别是点击含链接的段落时):
原因: 点击段落时可能误触链接,导致光标位置错误
解决方案: 点击后必须按End键将光标移动到行尾
undefined

正确流程

正确流程

  1. browser_click 点击目标段落
  2. browser_press_key: End # 关键步骤!
  3. browser_press_key: Meta+v # 粘贴图片
  4. browser_wait_for textGone="正在上传媒体"
undefined
  1. browser_click点击目标段落
  2. browser_press_key: End # 关键步骤!
  3. browser_press_key: Meta+v # 粘贴图片
  4. browser_wait_for textGone="正在上传媒体"
undefined

图片路径找不到

图片路径找不到

如果 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
    表示未找到(上传会失败)
如果仍然找不到:
  1. 检查 JSON 输出中的
    missing_images
    字段
  2. 手动将图片复制到 Markdown 文件同目录的
    assets/
    子目录
  3. 或修改 Markdown 中的图片路径为绝对路径
若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
    表示未找到(上传会失败)
如果仍然找不到:
  1. 检查JSON输出中的
    missing_images
    字段
  2. 手动将图片复制到Markdown文件同目录的
    assets/
    子目录
  3. 或修改Markdown中的图片路径为绝对路径