Today's focus was on automating article publishing to CSDN and WeChat MP (微信公众号) using Playwright, after CSDN deprecated its public Open API. Key achievements include: injecting Markdown content into CSDN's dynamic editor, handling title input quirks, implementing QR code login for WeChat MP, updating the Dev.to API publisher, and consolidating platform configs into a single YAML file. We also fixed session log capture after a Claude Code update changed the log file path.
Background: In early 2026, CSDN silently shut down its public Open API. All endpoints returned 404/403. We needed a fallback to keep publishing to China's largest developer platform.
Solution: Use Playwright to simulate a real user login and article creation. The approach:
csdn_cookies.json
.Code snippet:
import asyncio
from playwright.async_api import async_playwright
async def publish_to_csdn(title: str, content_md: str):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(storage_state="csdn_cookies.json" if exists else None)
page = await context.new_page()
await page.goto("https://mp.csdn.net/mp_blog/creation/editor")
await page.evaluate(f'''() => {{
const editor = document.querySelector('.editor-content');
if (editor) {{
editor.innerHTML = `{escaped_content}`;
editor.dispatchEvent(new Event('input', {{ bubbles: true }}));
}}
}}''')
await page.fill('#title-input', title)
await page.click('button:has-text("发布")')
await page.wait_for_url("**/mp_blog/manage/article*")
if not exists:
await context.storage_state(path="csdn_cookies.json")
await browser.close()
Result: First run requires manual QR scan; subsequent runs are fully automated. The browser approach is 3–5 seconds slower than an API call, but it works.
Problem: CSDN's Markdown editor is not a simple <textarea>
. It's a nested rich-text component with shadow DOM and dynamic elements. page.fill()
and page.type()
failed to inject content correctly.
Root Cause: The editor uses contenteditable
but its state is managed by a frontend framework (Vue/React). Direct fill doesn't trigger the internal state update.
Solution: Use page.evaluate()
to set innerHTML
and manually dispatch an input
event. For the title input, first focus, then simulate typing with page.keyboard.type()
with a delay.
await page.click('#title-input')
await page.wait_for_timeout(300)
await page.keyboard.type(title, delay=50)
Result: Content and title injection now works reliably over 10 consecutive tests.
Background: After upgrading to Claude Code 2.1.143, our session capture hook found no data in ~/.claude/history.jsonl
.
Root Cause: Version 2.1.143 moved per-project logs to ~/.claude/projects/<project-name>/logs/
.
Solution: Update the hook to check the new path first, with a fallback to the old path. Also detect version to decide.
import pathlib
import subprocess
def get_history_path():
version = subprocess.run(["claude", "--version"], capture_output=True, text=True).stdout
if parse_version(version) >= (2, 1, 143):
return pathlib.Path.home() / ".claude" / "projects" / get_current_project() / "logs"
else:
return pathlib.Path.home() / ".claude" / "history.jsonl"
Result: Session capture works again without data loss.
Chosen: Playwright for browser automation.
Alternatives: Selenium WebDriver + ChromeDriver.
Why:
time.sleep()
.Trade-off: Larger package size (≈100MB), less team familiarity. But stability wins.
Chosen: Store all platform settings (publisher class, cookie file, selectors, endpoints) in platforms.yaml
.
Alternatives: Hardcode configs or use environment variables.
Why:
platforms:
csdn:
publisher_class: publishers.csdn.CSDNPublisher
login_url: "https://passport.csdn.net/login"
editor_url: "https://mp.csdn.net/mp_blog/creation/editor"
cookie_file: "csdn_cookies.json"
wechat_mp:
publisher_class: publishers.wechat_mp.WeChatMPPublisher
login_qrcode_selector: "#login-qrcode"
cookie_file: "wechat_cookies.json"
Trade-off: Requires validation and error handling, but long-term maintenance is easier.
Chosen: Use Playwright to automate WeChat MP login via QR code scanning, then cache cookies.
Alternatives: Unofficial APIs (risky, may be banned).
Why:
Trade-off: Requires human intervention on first run. But can be mitigated by notification to ops team.
Today's work proves that multi-platform publishing is feasible even without open APIs. The key is building flexible and resilient automation that can adapt to real-world changes.