开发 VSCode 插件 Markdown Publisher 之简书篇:用 Puppeteer 打通发布链路 1. 为什么要在 VSCode 插件里接简书发布做 Markdown Publisher 这个 VSCode 插件时我一开始只接了 CSDN用起来挺顺但很快发现一个问题写技术文章的人往往不只发一个平台简书虽然这几年热度有起伏但它的编辑器对 Markdown 支持还算友好而且草稿箱机制适合先存后改。于是我就想能不能在插件里再加一条链路把本地 Markdown 一键推到简书草稿。核心难点其实不在 Markdown 解析而在“登录态”和“页面自动填充”。简书的登录不是简单的表单提交它有验证码、有跳转、有 cookie 过期。如果每次发布都让用户重新扫码或输密码体验就废了。所以我的方案是用 Puppeteer 启动一个带持久化用户目录的 Chromium第一次手动登录之后复用 cookie。这样插件里点一下“发布到简书”就能自动打开草稿页、填标题、灌正文、点保存。这篇文章就围绕这个模块把可复制的 Puppeteer 启动配置、简书页面选择器清单、本地调试验证步骤全部摊开。你如果是前端或者 Node.js 方向想给自己的工具加一个“多平台分发”能力这套思路可以直接搬。我试过在 Windows 和 macOS 上跑差异不大主要注意路径写法。2. TaoToken 前置给插件加一个模型辅助层虽然简书发布本身不依赖大模型但我在插件里加了一个小功能发布前用模型快速生成摘要或标签方便填到简书的“文章摘要”字段。这时候就需要一个稳定的 API 入口。我选的是 TaoToken它的模型对话接口兼容 OpenAI 格式接入成本低。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 根地址是 https://taotoken.net/api 。你需要在控制台创建一个 API Key然后就能在 Node 侧用 fetch 或 axios 调用。如果你只是做发布链路不接模型也完全没问题这一章可以跳过。但如果你想让插件更“智能”一点比如自动根据正文生成 100 字摘要那这一步值得做。我实测下来用模型对话接口生成摘要比正则截取前 200 字要自然得多。创建 Key 的入口在控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。拿到 Key 后建议放在 VSCode 的 SecretStorage 里不要硬编码在插件代码中。3. 可复制配置Puppeteer 启动与登录态保持3.1 安装依赖与目录结构在插件项目根目录执行npm install puppeteer-core注意我用的是puppeteer-core不是完整版puppeteer。因为 VSCode 插件打包时完整版会下载一个几百 MB 的 Chromium体积太大。puppeteer-core只提供 API浏览器路径由我们自己指定。用户本地一般都有 Chrome 或 Edge直接复用即可。目录建议这样组织src/ publisher/ jianshu/ index.ts // 入口函数 selectors.ts // 选择器常量 browser.ts // Puppeteer 启动与登录态 content.ts // Markdown 转 HTML 与填充3.2 启动配置持久化用户目录关键点是userDataDir。只要指定一个固定目录Puppeteer 就会把 cookie、localStorage 都存进去下次启动还是登录状态。import puppeteer, { Browser, Page } from puppeteer-core; import * as path from path; import * as os from os; const USER_DATA_DIR path.join(os.homedir(), .markdown-publisher, jianshu-profile); export async function launchBrowser(): PromiseBrowser { const executablePath process.platform win32 ? C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe : /Applications/Google Chrome.app/Contents/MacOS/Google Chrome; const browser await puppeteer.launch({ executablePath, headless: false, // 首次登录必须可见后续可改 true userDataDir: USER_DATA_DIR, defaultViewport: { width: 1280, height: 900 }, args: [ --no-sandbox, --disable-setuid-sandbox, --disable-blink-featuresAutomationControlled, ], }); return browser; }--disable-blink-featuresAutomationControlled这个参数能减少navigator.webdriver被检测到的概率。简书目前对自动化不算严格但加上更稳。3.3 登录态判断与首次登录启动后先打开简书首页检查是否已经登录。判断依据是页面上有没有“写文章”按钮或者用户头像。export async function ensureLogin(page: Page): Promisevoid { await page.goto(https://www.jianshu.com, { waitUntil: networkidle2 }); const loggedIn await page.$(a[href/writer]); if (loggedIn) { console.log(已检测到登录态跳过登录); return; } console.log(未登录请在打开的浏览器中手动完成登录); await page.waitForSelector(a[href/writer], { timeout: 120000 }); }这里给 120 秒超时足够用户扫码或输密码。登录完成后cookie 自动写入userDataDir下次就不用再登。4. 简书页面选择器清单与自动填充4.1 选择器清单简书的编辑器页面结构会变但核心几个元素相对稳定。下面是我当前版本实测可用的选择器你如果发现失效用 DevTools 重新取一下即可。用途选择器备注新建文章按钮a[href/writer]首页右上角标题输入框input[placeholder请输入标题]编辑器顶部正文编辑区div[contenteditabletrue]ProseMirror 容器保存按钮a[data-actionsave]或按钮文本“保存”可能随版本变摘要输入框textarea[placeholder请输入摘要]发布设置里正文区是contenteditable不能直接page.type因为 Markdown 转 HTML 后需要保留格式。我的做法是把 HTML 字符串通过page.evaluate注入。4.2 Markdown 转 HTML 并填充用marked把 Markdown 转成 HTMLnpm install markedimport { marked } from marked; export function mdToHtml(md: string): string { return marked.parse(md, { breaks: true, gfm: true }) as string; } export async function fillArticle(page: Page, title: string, md: string): Promisevoid { await page.waitForSelector(input[placeholder请输入标题]); await page.type(input[placeholder请输入标题], title, { delay: 30 }); const html mdToHtml(md); await page.evaluate((content) { const editor document.querySelector(div[contenteditabletrue]) as HTMLElement; if (editor) { editor.innerHTML content; editor.dispatchEvent(new Event(input, { bubbles: true })); } }, html); }注意dispatchEvent那行简书的编辑器依赖 input 事件来同步内部状态不触发的话保存时可能丢内容。4.3 保存并确认结果export async function saveArticle(page: Page): Promisestring { await page.waitForSelector(a[data-actionsave], { timeout: 10000 }); await page.click(a[data-actionsave]); await page.waitForFunction( () document.body.innerText.includes(已保存) || location.href.includes(/notes/), { timeout: 15000 } ); return page.url(); }返回的 URL 就是草稿地址插件里可以把它显示给用户或者写入日志。5. 验证请求与成功结果5.1 本地调试步骤在插件里加一个命令markdownPublisher.publishToJianshu然后在extension.ts里注册vscode.commands.registerCommand(markdownPublisher.publishToJianshu, async () { const editor vscode.window.activeTextEditor; if (!editor) return; const md editor.document.getText(); const title md.split(\n)[0].replace(/^#\s*/, ) || 未命名文章; const browser await launchBrowser(); const page await browser.newPage(); await ensureLogin(page); await page.goto(https://www.jianshu.com/writer, { waitUntil: networkidle2 }); await fillArticle(page, title, md); const url await saveArticle(page); vscode.window.showInformationMessage(简书草稿已保存${url}); await browser.close(); });按 F5 启动扩展开发宿主打开一个 Markdown 文件执行命令。第一次会弹出 Chrome手动登录简书。登录后再次执行应该直接跳到编辑器并自动填充。5.2 成功结果判断控制台会打印草稿 URL形如https://www.jianshu.com/notes/xxxxx。打开这个链接能看到标题和正文都在格式基本正确。如果正文里的代码块丢了高亮那是简书编辑器自身的限制不影响内容。6. 本篇常见错排查报错一Could not find browser executable说明executablePath不对。Windows 下常见路径是C:\Program Files\Google\Chrome\Application\chrome.exemacOS 是/Applications/Google Chrome.app/Contents/MacOS/Google Chrome。如果你用的是 Edge路径换成 Edge 的即可。报错二登录后第二次启动还是未登录检查userDataDir是否被清理。有些插件在卸载时会删目录或者你用了临时目录。确保路径固定且可写。另外如果 Chrome 已经在运行Puppeteer 可能无法复用同一个 profile先关掉所有 Chrome 窗口再试。报错三正文填充后保存为空大概率是没触发input事件。确认page.evaluate里执行了dispatchEvent。另外简书编辑器有时会延迟初始化可以在waitForSelector之后加await page.waitForTimeout(1000)再填充。报错四保存按钮点击无效选择器可能变了。用 DevTools 检查保存按钮的实际属性更新selectors.ts。如果按钮是动态渲染的用page.waitForSelector等它出现再点。报错五模型摘要接口返回 401检查 API Key 是否放在请求头Authorization: Bearer key里以及是否用了正确的根地址https://taotoken.net/api。如果 Key 是在控制台新建的确认没有多余空格。7. 接入文档与后续扩展发布链路跑通后你可以把简书模块和 CSDN 模块抽象成统一的Publisher接口插件里用配置决定启用哪些平台。模型摘要那块如果调用量不大用模型对话接口就够了如果要做长期批量发布可以考虑 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的请求示例和参数说明。API Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 建议给插件单独建一个 Key方便轮换。最后提醒一句Puppeteer 操作第三方平台时频率别太高模拟正常用户节奏。简书草稿保存本身不限制但短时间内大量创建可能触发风控。我一般建议用户手动确认后再发插件只负责填充和保存不自动点“发布”。这样既安全也符合平台规则。