资讯详情 B端系统自动化MCP工具开发指南:用TaoToken统一Key打通Playwright与Node.js
📅 2026/10/11 13:01:30
1. B端后台重复录入的真实困境与MCP工具定位B端后台系统里最消耗人力的往往不是复杂决策而是那些每天重复几十遍的录入动作把邮件里的客户信息抄进CRM、把聊天记录里的订单填进ERP、把外部网站抓来的数据一条条粘进内部系统。这些系统大多没有开放API或者API申请流程长到让人放弃于是运营同学只能开着两个浏览器窗口来回切换复制、粘贴、提交一天下来眼睛发花还容易出错。我试过用纯Playwright脚本硬扛写的时候挺爽但页面一改版选择器就全废维护成本高得离谱。后来把思路换成MCP工具让模型负责理解“要填什么”让Playwright负责“怎么填”两者通过MCP协议解耦。这样即使表单结构变了也只需要调整提示词或选择器回退策略不用重写整个流程。MCP在这里扮演的是“能力插座”的角色。它把浏览器自动化封装成一组标准工具登录、填表、批量提交、关闭浏览器任何支持MCP的客户端都能按需调用。对B端场景来说这意味着你可以用自然语言描述任务模型自动拆解成工具调用序列而不是每次都手写脚本。适合谁跟做有Node.js基础、被B端重复操作折磨过的开发或运营想用MCP协议把内部系统能力暴露给AI助手的团队以及需要批量处理无API系统数据录入的自动化工程师。整条链路的核心检索词就是“MCP工具开发”和“Playwright浏览器自动化”下面从环境准备到端到端验证一步步走通。2. TaoToken统一Key接入与MCP服务端前置配置在动手写MCP服务端之前先把模型调用的通道理顺。B端自动化里模型要干两件事从非结构化文本里抽取结构化数据比如从邮件正文提取客户名、电话、需求以及根据页面描述生成或修复Playwright操作片段。这两件事都需要稳定的模型API通道而TaoToken的价值就在于用一个Key统一管理多家模型调用不用在代码里散落一堆不同厂商的Key和Base URL。TaoToken的API地址是 https://taotoken.net/api 官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。你需要在控制台创建一个API Key然后把它写进环境变量。控制台入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。拿到Key之后项目根目录建一个.env文件内容如下# .env TAOTOKEN_API_KEYsk-你的实际Key TAOTOKEN_BASE_URLhttps://taotoken.net/api TAOTOKEN_MODELgpt-4o-mini这里选gpt-4o-mini是因为数据抽取和脚本生成对推理深度要求不高但调用频次高用轻量模型成本更可控。如果你需要更强的代码生成能力可以在模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 先对比几个模型的实际输出质量再决定用哪个Model ID。Node.js项目里用openai这个npm包就能对接TaoToken的兼容接口因为TaoToken的API格式与OpenAI兼容。初始化客户端时把baseURL指向TaoToken的API地址即可// src/services/aiClient.js const OpenAI require(openai); require(dotenv).config(); const client new OpenAI({ apiKey: process.env.TAOTOKEN_API_KEY, baseURL: process.env.TAOTOKEN_BASE_URL, }); module.exports { client };这样整个项目里所有模型调用都走同一个客户端实例换模型只改.env里的TAOTOKEN_MODEL不用动业务代码。对于需要长期跑批量任务的场景可以考虑用Coding Plan来获得更稳定的调用配额入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。前置配置做完后你的项目结构应该是这样b-system-automation-mcp/ ├── src/ │ ├── server.js │ ├── services/ │ │ ├── aiClient.js │ │ ├── aiExtractor.js │ │ └── aiScriptGenerator.js │ └── browser/ │ └── browser.js ├── .env └── package.json依赖安装命令npm init -y npm install modelcontextprotocol/sdk openai playwright dotenv npx playwright install chromium注意MCP SDK的包名是modelcontextprotocol/sdk不是旧文档里的sdk-node。安装完后用node -e require(modelcontextprotocol/sdk)验证一下能否正常加载。3. 可复制的MCP服务端配置与Playwright脚本骨架这一节给出可以直接复制运行的配置片段和代码骨架。先看MCP客户端注册配置以Claude Desktop为例配置文件路径macOS:~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:%APPDATA%\Claude\claude_desktop_config.json配置内容{ mcpServers: { b-system-automation: { command: node, args: [/绝对路径/到/你的项目/src/server.js], env: { TAOTOKEN_API_KEY: sk-你的实际Key, TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_MODEL: gpt-4o-mini } } } }如果你用的是Cline或CC Switch这类支持MCP的工具配置结构类似关键是三件套要写全Base URL填https://taotoken.net/apiKey填你的TaoToken KeyModel ID填你在模型对话页选定的模型名。缺任何一个都会导致调用失败。接下来是MCP服务端主文件它注册四个工具登录、单条新增、批量新增、关闭浏览器。// src/server.js const { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const { CallToolRequestSchema, ListToolsRequestSchema, } require(modelcontextprotocol/sdk/types.js); const { BrowserAutomation } require(./browser/browser.js); class BSystemAutomationServer { constructor() { this.server new Server( { name: b-system-automation, version: 1.0.0 }, { capabilities: { tools: {} } } ); this.browser new BrowserAutomation(); this.isLoggedIn false; this.setupHandlers(); } setupHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () ({ tools: [ { name: login_to_system, description: 登录到B端管理系统, inputSchema: { type: object, properties: { systemUrl: { type: string, description: 系统登录地址 }, username: { type: string, description: 用户名 }, password: { type: string, description: 密码 }, usernameSelector: { type: string, default: input[typetext], input[name*user] }, passwordSelector: { type: string, default: input[typepassword] }, submitSelector: { type: string, default: button[typesubmit], input[typesubmit] }, }, required: [systemUrl, username, password], }, }, { name: add_data_entry, description: 在B端系统中新增数据条目, inputSchema: { type: object, properties: { targetUrl: { type: string, description: 新增页面URL }, formData: { type: object, additionalProperties: { type: string } }, submitButton: { type: string, default: button[typesubmit] }, successIndicator: { type: string, default: .success, .alert-success }, }, required: [targetUrl, formData], }, }, { name: batch_add_entries, description: 批量新增多个数据条目, inputSchema: { type: object, properties: { targetUrl: { type: string }, entries: { type: array, items: { type: object, additionalProperties: { type: string } } }, delayBetweenEntries: { type: number, default: 1000 }, }, required: [targetUrl, entries], }, }, { name: close_browser, description: 关闭浏览器实例释放资源, inputSchema: { type: object, properties: {} }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; try { switch (name) { case login_to_system: return await this.handleLogin(args); case add_data_entry: return await this.handleAddEntry(args); case batch_add_entries: return await this.handleBatchAdd(args); case close_browser: return await this.handleCloseBrowser(); default: throw new Error(未知工具: ${name}); } } catch (error) { return { content: [{ type: text, text: 操作失败: ${error.message} }], isError: true, }; } }); } async handleLogin(args) { const { systemUrl, username, password } args; await this.browser.launch(); const result await this.browser.login(systemUrl, username, password, args); this.isLoggedIn result.success; return { content: [{ type: text, text: result.message }] }; } async handleAddEntry(args) { if (!this.isLoggedIn) throw new Error(请先登录系统); const result await this.browser.fillForm(args.targetUrl, args.formData, args); return { content: [{ type: text, text: result.message }] }; } async handleBatchAdd(args) { if (!this.isLoggedIn) throw new Error(请先登录系统); const { targetUrl, entries, delayBetweenEntries 1000 } args; const results []; for (let i 0; i entries.length; i) { const result await this.browser.fillForm(targetUrl, entries[i]); results.push({ index: i 1, success: result.success, message: result.message }); if (i entries.length - 1) { await new Promise((r) setTimeout(r, delayBetweenEntries)); } } const ok results.filter((r) r.success).length; return { content: [ { type: text, text: 批量完成: 成功 ${ok}/${entries.length} }, { type: text, text: JSON.stringify(results, null, 2) }, ], }; } async handleCloseBrowser() { await this.browser.close(); this.isLoggedIn false; return { content: [{ type: text, text: 浏览器已关闭 }] }; } async connect() { const transport new StdioServerTransport(); await this.server.connect(transport); console.error(B端自动化MCP服务端已启动); } } const server new BSystemAutomationServer(); server.connect().catch(console.error);Playwright浏览器控制模块的骨架// src/browser/browser.js const { chromium } require(playwright); class BrowserAutomation { constructor() { this.browser null; this.page null; this.isLaunched false; } async launch() { if (this.isLaunched) return; this.browser await chromium.launch({ headless: false, slowMo: 100 }); const context await this.browser.newContext({ viewport: { width: 1280, height: 720 }, }); this.page await context.newPage(); this.isLaunched true; } async login(systemUrl, username, password, selectors {}) { const { usernameSelector input[typetext], input[name*user], passwordSelector input[typepassword], submitSelector button[typesubmit], input[typesubmit], } selectors; try { await this.page.goto(systemUrl, { waitUntil: networkidle }); await this.page.fill(usernameSelector, username); await this.page.fill(passwordSelector, password); await this.page.click(submitSelector); await this.page.waitForTimeout(3000); const url this.page.url(); if (url ! systemUrl) { return { success: true, message: 登录成功 }; } const err await this.page.$(.error, .alert-danger); if (err) { return { success: false, message: await err.textContent() }; } return { success: false, message: 登录状态未知 }; } catch (e) { return { success: false, message: e.message }; } } async fillForm(targetUrl, formData, selectors {}) { const { submitButton button[typesubmit], input[typesubmit], successIndicator .success, .alert-success, } selectors; try { await this.page.goto(targetUrl, { waitUntil: networkidle }); await this.page.waitForTimeout(1500); for (const [field, value] of Object.entries(formData)) { await this.intelligentFill(field, value); } await this.page.click(submitButton); await this.page.waitForTimeout(2500); const ok await this.page.$(successIndicator); if (ok) return { success: true, message: 新增成功 }; const err await this.page.$(.error, .alert-danger); if (err) return { success: false, message: await err.textContent() }; return { success: true, message: 操作完成状态未检测 }; } catch (e) { return { success: false, message: e.message }; } } async intelligentFill(fieldName, value) { const candidates [ input[name*${fieldName}], textarea[name*${fieldName}], [placeholder*${fieldName}], input[typetext], ]; for (const sel of candidates) { const el await this.page.$(sel); if (el) { await el.fill(value); return true; } } await this.page.keyboard.type(value); return false; } async close() { if (this.browser) { await this.browser.close(); this.isLaunched false; } } } module.exports { BrowserAutomation };这套骨架的关键设计是选择器回退intelligentFill会依次尝试name匹配、placeholder匹配、通用input[typetext]最后兜底用键盘输入。B端系统表单字段命名往往不规范这种多级回退能显著降低脚本失效率。4. 端到端验证请求与成功结果确认配置写完后需要验证整条链路是否跑通。验证分两步先确认MCP服务端能正常启动并列出工具再模拟一次完整的登录加新增操作。第一步在终端直接启动服务端node src/server.js如果看到B端自动化MCP服务端已启动输出且进程不退出说明Stdio传输层正常。此时服务端在等待MCP客户端通过标准输入发送请求。第二步用MCP Inspector做交互验证。安装并启动npx modelcontextprotocol/inspector node src/server.jsInspector会打开一个本地页面在Tools标签下应该能看到四个工具login_to_system、add_data_entry、batch_add_entries、close_browser。点击login_to_system填入测试参数{ systemUrl: https://你的测试系统/login, username: testuser, password: testpass, usernameSelector: input[nameusername], passwordSelector: input[namepassword], submitSelector: button.login-btn }点击执行后浏览器窗口会弹出并自动完成登录动作。如果返回登录成功说明Playwright控制层和MCP工具注册都正常。第三步验证数据新增。在Inspector里调用add_data_entry{ targetUrl: https://你的测试系统/data/add, formData: { name: 测试产品A, description: MCP自动化验证条目, price: 199.00 }, submitButton: button[typesubmit], successIndicator: .alert-success }预期返回新增成功。此时去B端系统列表页刷新应该能看到刚提交的条目。第四步验证模型调用链路。这一步确认TaoToken的Key配置正确。在项目里跑一个独立测试脚本// test-ai.js const { client } require(./src/services/aiClient); require(dotenv).config(); async function main() { const res await client.chat.completions.create({ model: process.env.TAOTOKEN_MODEL, messages: [ { role: system, content: 你是一个数据抽取助手只输出JSON。 }, { role: user, content: 从这句话提取客户信息张三电话13800138000需要采购办公用品。 }, ], temperature: 0.1, }); console.log(res.choices[0].message.content); } main().catch(console.error);运行node test-ai.js如果输出类似{name:张三,phone:13800138000,need:办公用品}的JSON说明TaoToken通道完全打通。这一步很关键因为后续的AI数据抽取和脚本生成都依赖这个客户端。端到端验证的完整链路是MCP客户端发起工具调用 → 服务端接收请求 → Playwright执行浏览器操作 → 返回结果文本。模型调用则穿插在数据抽取和脚本生成环节通过TaoToken统一通道完成。两条链路都验证通过后就可以接入真实的B端系统做批量任务了。5. 本篇常见报错排查与修复对照实际跑的时候大概率会遇到几个典型报错这里按真实错误信息给出排查路径。401 Unauthorized 或 invalid api key这个报错来自TaoToken通道。先检查.env里的TAOTOKEN_API_KEY是否以sk-开头且没有多余空格。然后确认TAOTOKEN_BASE_URL填的是https://taotoken.net/api注意末尾不要加/v1或斜杠。如果Key是在控制台刚创建的确认没有复制到前后空白字符。可以用curl快速验证curl https://taotoken.net/api/v1/models \ -H Authorization: Bearer sk-你的Key返回模型列表说明Key有效。如果返回401去API Keys页面重新生成一个Key。local proxy failed 或 ECONNREFUSED这个报错通常出现在Playwright启动浏览器时。检查是否执行过npx playwright install chromium如果没装浏览器驱动会报连接失败。另外确认系统没有设置全局代理环境变量Playwright默认会读取HTTP_PROXY如果代理不可用就会报local proxy failed。在启动脚本里显式清空this.browser await chromium.launch({ headless: false, slowMo: 100, proxy: undefined, });reading choices of undefined这个报错说明模型返回结构不符合预期。常见原因是TAOTOKEN_MODEL填了一个不存在的模型名或者请求被限流返回了错误对象。在aiExtractor.js里加一层防御const res await client.chat.completions.create({ ... }); if (!res.choices || !res.choices[0]) { throw new Error(模型返回异常: ${JSON.stringify(res)}); } const content res.choices[0].message.content;同时确认模型名与模型对话页里显示的一致不要自己拼写。OAuth 相关报错或 authentication failed如果你用的是Claude Code或Codex这类需要OAuth的工具报错可能来自客户端认证而非TaoToken。检查~/.claude/settings.json或~/.codex/auth.json里的配置。以Codex的auth.json为例需要确保{ api_key: sk-你的TaoToken Key, base_url: https://taotoken.net/api }Claude Code的配置在settings.json里关键字段是env.ANTHROPIC_BASE_URL和env.ANTHROPIC_API_KEY。如果出现OAuth循环跳转通常是Base URL没指向TaoToken的API地址或者Key权限不足。去接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 对照最新配置格式。MCP服务端启动后客户端看不到工具检查claude_desktop_config.json里的args路径是否是绝对路径相对路径在客户端启动时工作目录不同会找不到文件。另外确认command填的是node而不是npxnpx在MCP环境下可能因为交互提示卡住。改完后完全退出客户端再重启配置文件是启动时读取的。Playwright点击后页面无变化B端系统常用iframe嵌套表单直接page.click可能点到了外层。用page.frameLocator(iframe[namecontent]).locator(button)定位。另外有些系统用pointer-events: none遮挡按钮需要先page.evaluate移除遮挡层再点击。调试时把headless设为false并加slowMo: 200肉眼观察点击位置是否正确。6. 从单次验证到批量任务的落地建议跑通单次验证后下一步是把它变成能稳定处理批量任务的工具。这里给几个实战中踩过坑才总结出来的建议。批量任务一定要加延迟。B端系统大多有操作频率限制连续快速提交会触发风控。batch_add_entries里的delayBetweenEntries默认1000毫秒实际用的时候根据系统响应速度调到1500到3000毫秒更稳妥。如果系统有验证码批量模式基本走不通需要改成半自动模型抽取数据后生成待填列表人工确认后再逐条提交。会话复用比每次重新登录高效得多。BrowserAutomation类里保持this.page实例登录一次后连续执行多个新增操作不要每条数据都重新走登录流程。但要注意会话超时可以在每次操作前检查页面是否还在登录态如果被踢出就自动重新登录。错误截图是排查利器。在fillForm的catch块里加截图catch (e) { await this.page.screenshot({ path: screenshots/fail_${Date.now()}.png }); return { success: false, message: e.message }; }失败时把截图和当时的表单数据一起记录下来事后分析是选择器问题还是数据格式问题。截图目录记得加进.gitignore。模型抽取的提示词要带字段约束。不要只说“提取客户信息”而是明确列出字段名和格式从以下文本提取JSON字段包括 - name: 客户姓名字符串 - phone: 手机号11位数字字符串 - need: 需求描述字符串 只输出JSON不要解释。这样模型返回的结构稳定后续填表时字段映射不会错位。如果某个字段经常抽取失败可以在提示词里加一个示例。对于需要长期运行的自动化任务建议把MCP服务端和任务队列分开部署。MCP服务端只负责接收工具调用实际执行交给BullMQ或类似队列这样即使浏览器操作耗时较长也不会阻塞MCP响应。队列的Worker里再调用Playwright执行具体操作失败自动重试。最后所有涉及账号密码的地方都不要硬编码。用环境变量或密钥管理服务.env文件加进.gitignore。B端系统往往涉及企业内部数据自动化工具的访问权限要最小化只开放必要的页面和操作。如果系统支持给自动化账号单独建一个角色限制其只能访问数据录入相关页面。整套方案的核心思路是用MCP把浏览器自动化能力标准化用TaoToken把模型调用通道统一化两者结合后B端系统的重复录入工作就能从“人肉复制粘贴”变成“描述任务等结果”。先从一个小表单跑通再逐步扩展到更多页面和更复杂的操作序列。