License and Commercial Authorization
This Skill is permitted only for personal learning, research, and non-commercial personal workflows. Client deliverables, paid products or services,
internal corporate deployments, market packaging, course packaging, and other commercial uses require the author's explicit prior written authorization.
To apply for commercial authorization, please add WeChat yichen365ai, and note in the verification message 商业授权; commercial use is not permitted before explicit written
authorization is received. See the full terms in the same directory LICENSE. This restriction applies only to this Skill,
and does not change the AGPL-3.0-or-later license of Ailu core; third-party materials continue to be governed by their respective licenses.
Core Principles
Only create drafts. Unless the user explicitly confirms that it can be published publicly, do not click X's final 发布 button.
The cover image must be prepared according to the cover ratio 5:2 currently prompted by the X Article interface; do not continue using the old incorrect ratio.
X Article currently handles at most 25 media items in the body based on actual testing; the cover is uploaded separately and does not count toward these 25 body slots;1 张封面 + 25 张正文图 is the valid boundary. If dry-run shows expected_body_images > 25, do not open X and do not force-upload the 26th body image; first generate a temporary Markdown used only for uploading, merge consecutive screenshots into a long image, or split the article into multiple posts. Do not directly damage the original Markdown just to upload a compressed version.
X Article currently supports native table blocks, but tables cannot be automatically generated by pasting rich HTML <table> . The script replaces Markdown pipe tables with temporary X_TABLE_MARKER_* placeholders; after pasting the body, insert X native table blocks at the placeholder positions, then open the table block's Markdown editor to write the original table. Final verification must refresh the same draft after autosave: the number of tables must be exactly equal, each table's visible rows and columns and normalized cell text matrix must match the local version and must not be an empty table, and the Markdown read back by the pencil editor must also match; only reading back the popup source code does not count as success.
X's current table size selector has a measured maximum of 10 x 10. If dry-run shows table_too_large, do not open X; first split the table, reduce the number of columns, or convert the large table into an image/body paragraphs before uploading.
Use an independent Playwright browser session; do not seize the Chrome window the user is currently using. The Chrome login state may be reused, but only through a private Playwright cookie JSON.
By default, only create a new draft. Only when the user explicitly confirms in the current turn that a specific exact X draft URL should be overwritten may you use
--draft-url, and at the same time pass in --confirm-existing-draft-write; this mode by default clears the draft's existing
title, body, tables, body media, and cover. Do not pass any arbitrary URL to this parameter, and do not use it to handle other people's drafts.
Quick Start
- When using Ailu, prepare cookies via the settings page "Import from Chrome / Paste JSON / Select JSON".
When running the Skill independently, if
~/.ailu/secrets/x/cookies.jsondoes not exist or the login state has expired, then explicitly export:
python3 ~/.agents/skills/x-article-draft-uploader/scripts/export_x_cookies_from_chrome.py \
--output ~/.ailu/secrets/x/cookies.json
- Before formally touching X, first dry-run parse the article to confirm the optional cover, number of body images, number of tables, existence of image files, body ending acceptance text,
content_checkpoints、expected_compact_length、expected_compact_sha256and anchors. The recommended cover image ratio is 5:2 according to the current X interface prompt:
python3 ~/.agents/skills/x-article-draft-uploader/scripts/upload_markdown_to_x_article.py \
"/absolute/path/to/article.md" \
--cookies-json ~/.ailu/secrets/x/cookies.json \
--dry-run
If dry-run's preflight.errors is non-empty, you must stop work immediately. Do not open X, do not create a draft, do not automatically rerun. Handle it according to the error type:
missing_cover_fileormissing_body_image: first fix the Markdown image path or the missing file.weak_image_anchor: first add a unique, explicit caption line before the corresponding image.image_count_mismatch: first check whether the Markdown image syntax is abnormal.table_too_large: first split the table, convert it to an image, or rewrite it as body paragraphs; do not directly create an X draft.unsupported_raw_html: convert raw HTML outside code fences to Markdown; URI/email autolinks are not subject to this restriction.unsupported_remote_image: first download thehttp://、https://or//remote image locally, then switch to the local path.unsupported_reference_image: change![Alt][label]、![Alt][]or![Alt]to inline image syntax。
If dry-run only has missing_leading_cover warnings like this, do not block the upload. Continue creating the draft, and leave the cover area blank; after the upload is complete, remind the user: you forgot to add a cover image for this article. Do you want me to add a 5:2 cover image for you?
If dry-run shows body_media_limit_exceeded, the result will directly display the total number of body images and the excess count. You must handle the overage first: prioritize merging consecutive screenshots into a long image, generate a temporary upload version of the Markdown, then run dry-run again on the temporary version; do not directly rerun the formal upload. The cover is counted separately; do not incorrectly reduce the body to 24 images just because there is already 1 cover.
- Create a clean new X Article draft and upload it:
python3 ~/.agents/skills/x-article-draft-uploader/scripts/upload_markdown_to_x_article.py \
"/absolute/path/to/article.md" \
--cookies-json ~/.ailu/secrets/x/cookies.json
The script will output these result files:
- Draft URL:
~/.ailu/runs/x-article-draft-uploader/draft-url.txt - Validation result JSON:
~/.ailu/runs/x-article-draft-uploader/result.json - Final screenshot:
~/.ailu/runs/x-article-draft-uploader/final.png
Workflow
- Use the
scripts/parse_markdown.pyincluded with this Skill to parse Markdown. - Run preflight. Body image files, image counts, and image anchor issues must be resolved before opening X; a missing pinned cover is only a warning and does not block the upload.
- Count body media:
expected_body_imagesmust be less than or equal to 25; the cover is uploaded separately and does not occupy a body slot. When there are more than 25 body images, first create a temporary upload version, merge consecutive screenshots or split the article, and rerun dry-run until it is within 25. - Count tables:
expected_tablescan be greater than 0, but each table must be within10 x 10. Table uploads must use X native table blocks; do not just paste<table>HTML, and do not accept tables being flattened into plain text. - If the first image at the very beginning of the article exists, treat it as the 5:2 cover image; if the article does not start with an image, do not automatically pick a body image as the cover, but treat all images as body images.
- Read the line before each body image from the original Markdown as the image insertion anchor. Images must be located by the full canonical path after parsing the original Markdown destination and by occurrence, not matched only by basename; images with the same name in different directories must keep their respective anchors. Anchors are compared by the semantic text the user ultimately sees: on the source side, normalize bold, italics, strikethrough, code, Markdown links, autolinks, escapes, HTML entities, NFC, zero-width characters, and heading/list/quote structure prefixes; on the DOM side, only normalize the visible text that has already been rendered, and do not again turn literal
*, backticks, or internal|When Markdown is deleted. For an inline image at the end of a paragraph, use the complete and untruncated nearest visible semantic paragraph before the image; consecutive pure image lines immediately following it automatically skip the previous image marker and inherit that semantic paragraph, so users do not need to rewrite Markdown in order to upload. Only accept exact semantic full-text equality, or when X merges consecutive non-empty lines, actual having the complete expected as a suffix; unrestricted bidirectional substring or truncated paragraphs are prohibited. Semantically identical non-adjacent anchors must be blocked in preflight; consecutive images may share an anchor, but must be distinguished by occurrence and final DOM order. Anchors must be text that actually exists in the body, relatively unique, and sufficiently long; do not use code fences, blank lines, hyphens, list numbers, or overly short text. The only exception is the body image ofblock_index=0after parsing: such images must form the leading consecutivecomposer-startprefix of the body media sequence, all using empty anchors, located via the real editor's first block and occurrence/DOM order; once a normalafter-anchorimage appears, nocomposer-startmay appear after it. The final readback must also confirm that there is no body text before these images, and the deleted H1 cannot be used as an anchor. - Start a brand-new Playwright Chromium context and add X cookies.
- Open
https://x.com/compose/articles, clickcreate, record the newly generated/compose/articles/edit/...URL。 - If there is a cover image, upload the cover through the file input in the cover area, then click X's
应用. If there is no cover image, skip cover upload directly and leave the cover area blank. - Fill in the title, and paste the rich HTML body into
[data-testid="composer"]. At this point, the Markdown table positions in the body will be temporaryX_TABLE_MARKER_*。 - Process each table in body order: locate
X_TABLE_MARKER_*, delete the placeholder, click插入 -> 表格, select the corresponding rows and columns, open the pencil in the upper-right corner of the table block, write the original Markdown table into the text box, click更新。 - Insert body images from back to front. Process each image in this order:
- Find the highest-priority anchor in the current editor.
- Click at the end of that paragraph.
- Press
End, then pressEnter。 - Paste the image file via a clipboard paste event.
- Wait for the media count detected on the page to increase before continuing to the next image.
-
After polling X autosave, refresh the same
/compose/articles/edit/...draft and read it again; the final reload of the same URL is the authoritative hard gate. The autosave UI may only observe the real save-state node (#detail-header、role=status、aria-liveor an explicit save indicator), and must be bound to the current round's mutation epoch; the event cursor must use a monotonically increasing sequence, and must not use the length of an event array that may be trimmed. The event stream only records changes to the corresponding channel's own token ornodeInstance, and the save evidence must still be bound to the currently saved channel; preferentially accept this round'ssaving -> savedor the same save node token changing after mutation. When X has not changed the "just saved" text, only accept evidence that the node itself was remounted afterobservedAt > lastMutationAtor returned to "just saved" from another state of the same channel; do not replay an old Saved because of changes to unrelated status nodes. Do not scan the entire page text, and do not treat an oldSavedfrom before entering the page as evidence. If the autosave UI event/saveTextIf not captured, do not terminate immediately; only when, after the final reload of the same draft, the title, body hash, ordered checkpoints, all tables, cover image, and body image identity/count/order/semantic positions all strictly pass may it be recorded asreload_persistence_fallbacksuccessful and a warning logged. Transient DOM/decoding observations right after cover upload, right after body paste, right after a table update, or right after an image paste may also be deferred as warnings; each image paste must still find the target position and make the total body media count increase by exactly 1, but when transient identity, anchor, runtime key, old node sequence, or binding observations are unstable, do not abort early; leave it to the final reload to independently recompute. After the final reload, the title and body exact code point lengths, SHA-256, ordered checkpoints, each table's visible rows and columns/non-empty normalized matrix/Markdown read-back, cover source image, and all body images' source image identity, count, occurrence, DOM order, and semantic anchors must all pass; at this point, a wrong image, missing image, extra image, lost old node, displacement, or order corruption must all fail closed. Diagnostic screenshots, autosave copy, and runtime DOM keys are not core persistence evidence; their absence only logs a warning.Body images are counted exactly by media blocks in the composer, and the local source image's
visual-dhash-v1fingerprint, 8 x 8 RGB/luminance samples, occurrence sequence number of the same source image, semantic anchor, and final DOM order are combined into a binding key. A 256-bit dHash can be directly matched within a 64-bit Hamming radius; if X re-encoding puts the distance between 65 and 80 bits, dHash adaptive matching may be used only when the expected source image is the unique nearest group among all distinct source images, differs from the second-nearest source image by at least 16 bits, and the natural aspect ratio drift does not exceed 3%. When dHash exceeds 80 or is unstable for smooth gradients, multi-signal consensus may be used only when the 8 x 8 RGB average error does not exceed 12%, the luminance error does not exceed 10%, the luminance structure correlation is at least 0.88 (except for solid-color low-variance images), and the samples still uniquely point to the expected source image. Repeated occurrences of the same source image are still distinguished by occurrence, anchor, and DOM order; fingerprints before and after refresh are allowed to be not exactly equal due to X re-encoding. The body SHA may strip only nodes that have passed source image evidence and are bound to a binding key on the real media root; it must not guess the section based only onaria-labeland count. The cover uses the same strict/multi-signal visual contract, but evidence must also be obtained from the bounded ancestor region of the cover file input, and relative to the verified zero-cover state, only one image may be added; if an X-hosted cover triggers a Canvas cross-origin restriction, it is only allowed to read back image bytes from an X-controlled HTTPS host through the same Playwright session and then compute the same evidence. If the independent request channel fails, it may fall back to a temporary read-only page in the same browser session to fetch the image, but the final HTTPS URL, X-allowed host, image type, and 32 MiB upper limit must be re-verified, and the temporary page must be closed after evidence collection; it must not degrade to only looking at count or aspect ratio. The result JSON must contain the fixedverification_contract=x-article-persistence-v1、persistence_verified=trueand the correspondingpersistence_evidence; if any final persistence evidence is missing or inconsistent, it can only be counted as partial/failure, and it must not outputRESULT_OK True, nor tell the user the upload succeeded. -
If there is no cover image this time, after the upload is complete clearly remind the user: You forgot to add a cover image for this article—would you like me to help you add a 5:2 cover image?
-
If the run fails but has already generated
/compose/articles/edit/..., do not rerun immediately. First check~/.ailu/runs/x-article-draft-uploader/underdraft-url.txt、result.jsonandfinal.png, to confirm whether it is only a validation false alarm or a half-finished draft. If duplicate drafts need to be deleted, you must first ask the user.
Common blockers
- If X redirects to
/login, indicating the login session has expired. Re-export the cookies. - If the article does not start with an image, do not stop, and do not automatically pick a body image as the cover. Directly upload the title, body, tables, and body images, and leave the cover blank; after the upload is complete, remind the user to add a 5:2 cover image.
- If dry-run finds missing images, immediately fix the Markdown or move the images; do not wait until the script blows up inside X.
- If the body media exceeds 25, prioritize making a temporary upload version: merge consecutive screenshots, preserve the body order, and re-run dry-run until it is within 25. Upload the cover separately, and it does not count against the body quota; do not permanently change the original into a compressed version unless the user explicitly requests it.
- If dry-run finds
expected_tables > 0, confirm that each table'srows/columns; when a table is uploaded successfully, the result JSON will containinserted_tables、tableCountandtableDetails。 - After a table is uploaded successfully, the result JSON must contain both
visible_matrix_matches=trueandreadback_matches=true; if only the source code matches, and the visible table is empty or misaligned, it still counts as a failure. - If in the final screenshot the table only looks like it is laid out with spaces and has no grid lines, that means it is not an X native table. Do not judge it as successful; you must make
tableCountgreater than or equal toexpected_table_count。 - If dry-run finds
weak_image_anchor, add a line before the image that clearly explains it, for exampleWord 文档验收截图:. Do not let the anchor fall on```、---, a list number, or overly short text. - If the first body image immediately follows an opening H1 that will be extracted as the title, do not ask the user to modify the original for the sake of uploading; the script should recognize that image as
composer-start, insert it before the first real text block in the body, and verify after refreshing that it is still before all body text. - If dry-run finds
reused_anchor, you must first make the descriptions before non-adjacent images unique from one another; do not create an X draft under ambiguous positional relationships. - If a half-finished draft appears in the drafts after a failed run, first record the failed draft URL. Do not blindly rerun and create more duplicate drafts; if cleanup is needed, first ask the user to confirm.
- If the final validation fails, but the media count, title, and screenshots all look correct, first check the specific fields of
~/.ailu/runs/x-article-draft-uploader/result.json, and do not directly re-upload. - If the editor is blocked by an overlay after uploading the cover, find the
应用button and click it. - Do not directly use the hidden file input for body images. It may point to the cover uploader and cause the cover to be replaced.
- Do not only look at the media count. For images after lists, especially check whether the anchor is correct. The script will record
anchor_usedandexpected_anchor。 - If a run is only partially successful but the image positions are wrong, prioritize creating a clean new draft and rerunning, and do not force fixes on the old draft.
Script description
upload_markdown_to_x_article.py --dry-runis a safety check and will not open X.- The Markdown body will render common inline syntax losslessly:
**/__bold,*/_italic,~~strikethrough, inline code, all ASCII punctuation escapes, balanced-parenthesis inline links, full/collapsed reference links, URI/email autolinks, and HTML entities; code fences preserve their literal content. Ordinary unclosed<taglesswill also be escaped as text and will not be swallowed by the HTML parser along with the following content. - When the filename title has the same visible semantics as the first H1 (even if the H1 has bold or other formatting), the first H1 is simply deleted as a duplicate title; when the two have different semantics, the first H1 is a genuine body title, retained and demoted to an H2 of X.
- Using
--draft-urlfor a full replacement, the script clears before writing the new cover, title, and body, and attempts to observe whether the title, body, native tables, body media, and old cover have been reset to zero.--resume-images-onlyKeeps the existing title, body, native tables, and cover, and attempts to confirm before writing that body media is 0 and that the cover count and source images match this Markdown. If these pre-write DOM baselines are observed to be unstable, only a warning is recorded; the same draft's final reload will still independently and strictly verify all content from the local source file. Do not use this option to insert a full image set when you know old body images still exist, otherwise the final count or order acceptance will fail. - In a formal run, on any persistence verification exception it will write out
status=partialthe result JSON ofphase、error_type、error、draft_urlandmutation_epoch, to help distinguish the case where content has been persisted but post-verification failed; it will still exit as failed and will not call partial a success. - If only autosave UI/
saveText, transient pre-reload DOM, runtime key, final diagnostic screenshot, or browser cleanup observation fails, while all core content of the same-URL final reload passes strictly, the result remains successful and the reason is written toverification_warnings; these warnings cannot replace or lower the final acceptance of body, tables, cover, and body images. - dry-run outputs 3 to 5 deterministic body checkpoints that do not cross complete temporary table placeholders
content_checkpoints, the normalized body'sexpected_compact_lengthandexpected_compact_sha256;compact_length_unitandcheckpoint_position_unitfixed atunicode_code_points, ensuring emoji and extended Han characters use the same length and position conventions in Python and the browser. The initial paste requires exact length and SHA-256 to match, and the final read-back also requires checkpoints to be hit in order and native table DOM to be reliably stripped. - backtick/tilde fenced code (code fences) remain tokens untouched by ordinary Markdown rules throughout the full pipeline of cleaning, chunking, image/table extraction, and rich HTML generation, and are restored only at the end; the closing fence must use the same character, be no shorter than the opening, and be followed only by whitespace, and the info string of a backtick opening must not contain another backtick. Inside code,
**、*、[x](url)、, pipe table, angle brackets, and internal blank lines remain as-is and will not be extracted as body images or tables;<img>and other tags are displayed only as text and will not become real remote media. Converting body to plain text uses an HTML parser and will no longer use broad regexes to mistakenly swallow ordinary text such asx < y > z. - The Skill's own preflight ignores code fences and legal frontmatter, but blocks raw HTML outside fences, remote inline images, and any unescaped, non-inline reference-style image; the scan does not depend on where the definition is located, so blockquotes, lists, or full, collapsed, and shortcut forms whose definition has not yet been resolved will all fail closed, and it supports escaped, nested, and multi-line alt/label. Only
(or[the![[...]]that is not immediately adjacent after a complete outer alt is ignored as an Obsidian wiki embed;![[alt]](path)is still an inline image,![[alt]][ref]is still a reference image. Even if run directly--dry-runwithout going through the Obsidian plugin, it will not open X. - Markdown supports UTF-8 BOM; the beginning of YAML frontmatter must be a standalone line
---, the ending must be on its own line---or..., these substrings within field values will not be mistaken for end markers. - If the cover is missing, dry-run only outputs a warning; the actual upload will skip the cover upload and remind the user to add a 5:2 cover image after completion.
- Markdown pipe tables will output in dry-run
expected_tablesand the rows and columns of each table; the actual upload will write through X native table blocks, not relying on HTML table pasting, and will read back the X table block Markdown source for consistency verification. - The current script will automatically output when
expected_body_images > 25atbody_media_limit_exceededand block before opening X; 25 body images are allowed to continue even if there is another 1 cover image. - The upload script requires Python Playwright and a valid X cookie JSON.
- The Markdown parser is already built into the current Skill, not relying on the old
x-article-publisherSkill。 - The cookie export script only reads local Chrome cookies and atomically writes the specified output as
0600; the canonical path~/.ailu/secrets/x/cookies.jsondirectories at all levels will reject symbolic links and tighten to0700. The Skill source code does not contain any Cookies.