1实现原理 · 为什么它能做到
三段式管线:Markdown 文件先被本地 Python 脚本解析成结构化数据(JSON + 供粘贴的 HTML),再由宿主提供的 Playwright MCP 浏览器工具把内容灌进 X Articles 编辑器,最后只落成草稿;脚本层与浏览器层职责分离,脚本完全不碰网络。
Markdown File ↓ Python parsing Structured Data (title, images with block_index, HTML) ↓ Playwright MCP X Articles Editor (browser automation) ↓ Draft Saved (never auto-publishes)
块级切分是全部定位能力的地基:split_into_blocks 逐行扫描,把代码块(用 ___CODE_BLOCK_START/END 包裹,含未闭合兜底)、'---' 分割线、'#'/'>' 起始的标题与引用、独占一行的图片各自切成独立 block,其余连续行合并成段落块。
def split_into_blocks(markdown: str) -> list[str]: """Split markdown into logical blocks (paragraphs, headers, quotes, code blocks, etc.)."""
图片与分割线在解析阶段就被从正文里剥离:extract_images_and_dividers 逐个 block 判断 ___DIVIDER___ 或图片正则,记录 block_index = 已收集 clean block 的数量,并取前一块最后一行前 80 字符作 after_text;剥离后的正文才去做 HTML 转换。
block_index = len(clean_blocks) after_text = "" if clean_blocks: prev_block = clean_blocks[-1].strip() lines = [l for l in prev_block.split('\n') if l.strip()] after_text = lines[-1][:80] if lines else ""
标题抽取有明确的优先级与去重规则:extract_title 依次尝试首个 H1、H2、首个非图片非空行;若标题来自 H1,则把那一行从 Markdown 中删除,避免正文里再出现一次标题。
# Remove H1 title line from markdown to avoid duplication
富文本转换不是用 markdown 库,而是一串顺序敏感的正则替换:代码块先被转成 <blockquote>(因为 X 不支持 <pre><code>),随后 H2/H3→h2/h3、**→strong、*→em、[text](url)→a、>→blockquote、- 与 1.→li 并包进 ul,最后按空行切段、段内换行转 <br> 并用 <p> 包裹。
# Convert to blockquote format since X Articles doesn't support <pre><code>
进剪贴板走操作系统原生剪贴板 API 而非第三方库:macOS 分支用 pyobjc 的 NSPasteboard,先 clearContents 再 setData_forType_ 写 HTML 类型,并同时 setString_forType_ 写一份纯文本副本,所以 Cmd+V 到 X 编辑器能保留 H2/粗体/链接等富文本。
pasteboard.setData_forType_(ns_data, NSPasteboardTypeHTML) # Also set plain text version pasteboard.setString_forType_(html, NSPasteboardTypeString)
图片入剪贴板前可选压缩并用平台格式封装:compress_image 用 Pillow 打开图、必要时转 RGB、thumbnail 限制到 (2000,2000) 后按 JPEG quality 写入 BytesIO;Windows 分支还要存成 BMP 并裁掉 14 字节 BITMAPFILEHEADER,再把剩下的 DIB 数据塞进 CF_DIB。
data = output.getvalue()[14:] # Skip BITMAPFILEHEADER
表格转图完全自绘:table_to_image.py 自己解析 Markdown 表格(识别 :--- 对齐标记),用 Pillow ImageDraw 以 textbbox 量列宽、画圆角外框/表头底色/行分隔线,并按 macOS(sfns/pingfang/stheiti)→Linux(dejavu)→Windows(arial/msyh) 的顺序逐个尝试 truetype 字体,全失败才回退位图字体。
for path in font_paths: try: return ImageFont.truetype(path, size) except (OSError, IOError): continue
2核心能力
3外部依赖
| 类型 | 依赖 |
|---|---|
| cli | python3(解释器,脚本 shebang 与文档命令均为 python3/python) |
| cli | mmdc(@mermaid-js/mermaid-cli,Mermaid→PNG,需 npm 全局安装) |
| cli | pkill(排障步骤里杀 Playwright MCP 相关进程) |
| package | Pillow(image 压缩、BMP/DIB 封装、表格自绘) |
| package | pyobjc-framework-Cocoa(macOS AppKit/Foundation,NSPasteboard) |
| package | pywin32(win32clipboard/CF_DIB)与 clip-util(Windows HTML 剪贴板) |
| network | Playwright MCP(宿主浏览器自动化工具集 browser_navigate/browser_click/browser_press_key 等;skill 不自带浏览器驱动) |
| network | X Articles 编辑器(被自动化访问的站点,读取用户已登录会话) |
| api | X (Twitter) Premium Plus 订阅(Articles 功能的准入前提,非 API 调用) |
4风险提醒 风险提醒:橙色 · 评估后使用
- SKILL.md 内部策略自相矛盾,可能直接导致图片插错位置 — Step 6 明写『使用 after_text 文字搜索定位,比 block_index 更直观可靠』,Critical Rules 第 5 条却要求用 block_index,README 又称文本匹配已被替换;agent 若按 Step 6 执行就退回到作者自己承认不稳定的方法。
- 官方描述夸大输入能力(URL) — frontmatter 承诺 'publish a Markdown file/URL',实现只有本地文件路径;若 agent 自行补 URL 抓取,会把不受控网页内容带进剪贴板与 X 编辑器。
- 剪贴板被清空覆写,且图片可能来自用户个人目录 — copy_to_clipboard.py 调用 clearContents()/EmptyClipboard() 覆盖用户剪贴板;SEARCH_DIRS 会在 ~/Downloads、~/Desktop、~/Pictures 按同名回退找图并上传到 X,存在误传私人图片的隐私风险。
- 『绝不发布』只是提示词约束,非技术强制 — 仓库中没有禁用发布按钮或校验状态的代码,全部依赖 SKILL.md 文本;在用户已登录的 X 会话里,agent 的操作权限并未被代码层收窄。
- 依赖未锁版本、平台声明不一致、安装路径硬编码 — 无 requirements.txt/package.json,pip 与 `npm install -g @mermaid-js/mermaid-cli` 均无版本固定;plugin.json platforms 仅 macos 而 README 说 Windows 可用、Linux 未完成;SKILL.md 硬编码 ~/.claude/skills/x-article-publisher/scripts/ 路径。
- 文档指示读取用户配置文件与杀进程 — docs/GUIDE.md 给出 `cat ~/.claude/settings.json | grep playwright`(该文件可能含 API key)与 `pkill -f "mcp-server-playwright"`,属于对用户环境有实际副作用的排障动作。
5第二遍独立确认
- [ok] 全部 evidence.quote 逐字存在性(Python 子串校验,覆盖 SKILL.md/两个 README/GUIDE/三个脚本/plugin.json/LICENSE) — 所有引用均为原文精确子串,未发现 paraphrase 冒充 quote;含反斜杠的正则引用按源码原样保留。
- [ok] 网络调用反查(requests|urllib|httpx|socket|aiohttp|curl|wget|api.x.com|api.twitter) — 仅 parse_markdown.py 的 `import urllib.parse` 与 `urllib.parse.unquote(img_path)` 命中,用途是 URL 解码文件名;无任何 HTTP 客户端。
- [discrepancy] 凭证/密钥反查(token|api_key|environ|getenv|.env|cookie|oauth|Bearer) — 脚本层零命中,确认无凭证读取;但文档层 docs/GUIDE.md 指示 `cat ~/.claude/settings.json | grep playwright`,会读取可能含 API key 的 Claude Code 配置文件——第一遍未计入,已在 security.credential_reads 补记。
- [ok] 隐藏文件与额外可执行体反查(find 全清单 + .gitignore) — 仓库仅 13 个非 .git 条目:3 个 py 脚本、SKILL.md、GUIDE.md、两个 README、plugin.json、LICENSE、.gitignore、skills/、docs/、.claude-plugin/;无隐藏脚本、无二进制、无混淆内容。
- [ok] 外部依赖调用点复核(Pillow / pyobjc / pywin32 / clip-util / mmdc / pkill / Playwright MCP) — Pillow 在 copy_to_clipboard.py 与 table_to_image.py 的 import 行可定位;mmdc 与 pkill 只出现在 SKILL.md 文档指令中,仓库无脚本调用它们;Playwright MCP 只在 plugin.json 与 SKILL.md 工具名中出现,skill 不自带驱动。
- [discrepancy] 功能声明 vs 实际能力:frontmatter 宣称 'publish a Markdown file/URL to X Articles' — 夸大:parse_markdown.py 只有 `with open(filepath, 'r', encoding='utf-8') as f:` 与 `os.path.exists(args.file)`,仅接受本地文件,全仓库无 URL 抓取分支。若 agent 照字面接受 URL 输入,会自行引入不受控的网页内容。
- [discrepancy] 定位策略一致性:SKILL.md Step 6 推荐 after_text 文本搜索 vs Critical Rules 第 5 条与 README 声称的 block_index — SKILL.md 自身矛盾:Step 6 标题写 '(Text Search Positioning)' 且正文称『推荐方法: 使用 after_text 文字搜索定位,比 block_index 更直观可靠』,而同一文件 Critical Rules 写『5. Block index positioning - Use block_index for precise image/divider placement』,README 又称 v1.1 已用 block_index 替换文本匹配。agent 执行时选错策略会导致图片错位。
- [discrepancy] 版本一致性:plugin.json version 与 README/changelog 版本号、日期 — plugin.json 为 version 1.1.0,README 顶部却写 v1.2.0;changelog 条目为 v1.2.0 (2025-01)、v1.1.0 (2025-12)、v1.0.0 (2025-12),v1.2.0 的月份早于 v1.1.0,与 pin commit 日期 2026-01-25 也对不上,疑为文档笔误。
6结论
61755757ff05c8f7…eb3197d1d8