Overview
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.
Problems and Solutions
1. CSDN Open API Deprecation → Browser Automation
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:
- Launch a headless Chromium browser.
- Navigate to CSDN's login page.
- Perform one-time manual login via QR code.
- Serialize cookies to
csdn_cookies.json. - On subsequent runs, load the cookies and skip login.
- Go to the editor, inject Markdown content via DOM manipulation, fill the title, and click publish.
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")
# Inject content
await page.evaluate(f'''() => {{
const editor = document.querySelector('.editor-content');
if (editor) {{
editor.innerHTML = `{escaped_content}`;
editor.dispatchEvent(new Event('input', {{ bubbles: true }}));
}}
}}''')
# Fill title
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.
2. Dynamic Editor Selector Debugging
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.
3. Claude Code Log Format Change
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.
Architectural Decisions
Decision 1: Playwright over Selenium
Chosen: Playwright for browser automation.
Alternatives: Selenium WebDriver + ChromeDriver.
Why:
- Native async support matches pipeline.
- Built-in auto-waiting reduces
time.sleep(). - Powerful selector engine handles dynamic DOM better.
- Community reports higher reliability for SPAs.
Trade-off: Larger package size (≈100MB), less team familiarity. But stability wins.
Decision 2: YAML Config for Platforms
Chosen: Store all platform settings (publisher class, cookie file, selectors, endpoints) in platforms.yaml.
Alternatives: Hardcode configs or use environment variables.
Why:
- Add new platforms without touching core code.
- Switch environments via different YAML files.
- Easy dry-run support through config.
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.
Decision 3: QR Login for WeChat MP
Chosen: Use Playwright to automate WeChat MP login via QR code scanning, then cache cookies.
Alternatives: Unofficial APIs (risky, may be banned).
Why:
- WeChat offers no public write API.
- QR login is the official method.
- Cookie caching allows long-lived sessions after first scan.
Trade-off: Requires human intervention on first run. But can be mitigated by notification to ops team.
Key Takeaways
- Browser automation is a last resort when APIs fail: It works but costs time in debugging dynamic DOM. Prioritize official APIs if available.
- Cookie caching is essential: Serialize login state to avoid repeated manual logins. Add health checks to detect expired cookies.
- Version pinning matters: External tool updates can break integrations. Use version detection, adapters, or Docker to ensure stability.
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.
Top comments (0)