☰
开发 VSCode 插件 Markdown Publisher 之简书篇:用 Puppeteer 打通发布链路
2026/9/27 13:07:16 网站建设 项目流程

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_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 根地址是 https://taotoken.net/api 。你需要在控制台创建一个 API Key,然后就能在 Node 侧用 fetch 或 axios 调用。

如果你只是做发布链路,不接模型也完全没问题,这一章可以跳过。但如果你想让插件更“智能”一点,比如自动根据正文生成 100 字摘要,那这一步值得做。我实测下来,用模型对话接口生成摘要,比正则截取前 200 字要自然得多。

创建 Key 的入口在控制台,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。拿到 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(): Promise<Browser> { 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-features=AutomationControlled', ], }); return browser; }

--disable-blink-features=AutomationControlled这个参数能减少navigator.webdriver被检测到的概率。简书目前对自动化不算严格,但加上更稳。

3.3 登录态判断与首次登录

启动后先打开简书首页,检查是否已经登录。判断依据是页面上有没有“写文章”按钮,或者用户头像。

export async function ensureLogin(page: Page): Promise<void> { 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[contenteditable="true"]ProseMirror 容器
保存按钮a[data-action="save"]或按钮文本“保存”可能随版本变
摘要输入框textarea[placeholder="请输入摘要"]发布设置里

正文区是contenteditable,不能直接page.type,因为 Markdown 转 HTML 后需要保留格式。我的做法是把 HTML 字符串通过page.evaluate注入。

4.2 Markdown 转 HTML 并填充

用marked把 Markdown 转成 HTML:

npm install marked
import { 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): Promise<void> { 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[contenteditable="true"]') 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): Promise<string> { await page.waitForSelector('a[data-action="save"]', { timeout: 10000 }); await page.click('a[data-action="save"]'); 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.exe,macOS 是/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_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。

接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有完整的请求示例和参数说明。API Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,建议给插件单独建一个 Key,方便轮换。

最后提醒一句:Puppeteer 操作第三方平台时,频率别太高,模拟正常用户节奏。简书草稿保存本身不限制,但短时间内大量创建可能触发风控。我一般建议用户手动确认后再发,插件只负责填充和保存,不自动点“发布”。这样既安全,也符合平台规则。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询