1仕組みと動作原理
3段階のパイプライン:ローカルのPythonスクリプトがMarkdownファイルを構造化データ(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で囲まれ、未閉じのフォールバックを含む)、'---' 区切り線、'#'/'>' で始まる見出しと引用、独立行の画像をそれぞれ独立ブロックとして切り出し、その他の連続行を段落ブロックとしてマージします。
def split_into_blocks(markdown: str) -> list[str]: """Split markdown into logical blocks (paragraphs, headers, quotes, code blocks, etc.)."""
画像と区切り線は解析段階で本文から分離:extract_images_and_dividersが各ブロックで___DIVIDER___または画像正規表現を判定し、block_indexとして収集済みクリーンブロック数を記録、前ブロックの最終行の先頭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>
クリップボードへの格納はサードパーティ製ライブラリではなくOSネイティブAPIを使用:macOS環境ではpyobjcのNSPasteboardを使用し、clearContentsの後にsetData_forType_でHTML型を書き込み、同時にsetString_forType_でプレーンテキストのコピーも保持するため、XエディタへのCmd+Vで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外部依存関係
| Kind | Dependency / Endpoint |
|---|---|
| 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推奨統制措置と判定根拠
- scripts/: ローカルスクリプトまたはコマンドの実行: python3 skills/x-article-publisher/scripts/parse_markdown.py <markdown_file
- 网络端点: 外部ネットワークアクセスの宣言: スクリプト層:なし——3つのスクリプトすべてでrequests/urllib.request/httpx/socket呼び出しは皆無
- 本地工作目录: ファイルの読み書きおよび成果物の出力: /tmp/article_html.html(SKILL.mdにてAgentにシェルリダイレクト経由での書き込みを指示)
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…