
Claude Code 的 Computer Use 搬到本地浏览器后Playwright 管执行模型管规划TaoToken 在这套分工里只给两样东西一个 Key一个 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 入口。原文那套 Python 脚本结构其实挺干净XueDingMaoClient用requests往 OpenAI 兼容的chat/completions发消息模型返回goto、click、fill三种动作run_browser_actions()拿到数组后依次丢给 Playwright 执行浏览器就真的动起来了。麻烦的地方不在动作映射而在中间那一段通道。XDM_API_KEY和XDM_BASE_URL写在.env里看着很优雅可这两个值一旦绑死在一个平台上换模型、换通道、临时切一下备用线路你改的就不是一行model而是地址、密钥、甚至客户端里的路径拼接逻辑。更隐蔽的是拼接有的代码写f{base}/chat/completions有的写f{base}/v1/chat/completions两边混着改404 就是这么来的。这篇顺着原脚本的调用链往下走只动通道那一层Key 换成自己在控制台创建的Base URL 指向https://taotoken.net/apimodel先照原文写claude-sonnet-4-6正式可用 ID 以模型广场当时列表为准动作规划、JSON 解析、Playwright 执行循环全部保留原样。改完直接跑脚本重点看两件事plan_actions_with_llm()能不能吐回合法的 JSON 动作数组run_browser_actions()能不能按goto → fill → click的顺序把页面走完。1. 从 .env 里写死的 XDM_BASE_URL 说起1.1 密钥和地址成对绑定换一个就得翻代码原文的调用链很短读.env实例化XueDingMaoClient把「当前任务 页面状态」拼成 user message 发给模型模型返回动作数组Playwright 执行。问题在于XDM_API_KEY和XDM_BASE_URL是成对的——Key 属于哪个平台地址就得跟着是哪个平台。想临时把模型从claude-sonnet-4-6换成别的或者某条通道今天不稳想切走你要动的不只是 model 字段还要确认新平台的鉴权头是不是同一个写法、路径是不是同一层、返回结构choices[0].message.content有没有变。这种「地址 密钥 路径」三件套绑在一处的写法在小脚本里无所谓一旦你想做多模型协作——比如便宜模型负责第一步导航、强模型负责复杂表单判断——每次换模型都要重新配一遍通道代码里就会堆出一串if platform ...的分支。这跟 Computer Use 本身没关系纯粹是接入层没解耦。1.2 Computer Use 那套思路落到浏览器上是三个动作原脚本借的是 Computer Use 的分工思路不是它的截图识别能力。模型不直接操作鼠标而是先输出结构化动作数组[ {action: goto, url: https://example.com/login}, {action: fill, selector: #username, value: demo_user}, {action: fill, selector: #password, value: demo_pass}, {action: click, selector: button[typesubmit]}, {action: wait, ms: 1500} ]scripts里定义好的run_browser_actions()负责翻译成 Playwright 调用浏览器再去点、去填。所以模型侧真正的义务只有一条稳定输出可被json.loads()解析的动作数组。至于这个模型是走哪条通道、用哪把 Key跟动作规划的逻辑一点关系都没有——这正是接入层可以独立拆出去的原因。2. TaoToken 只补两个变量动作规划逻辑一行不动2.1 该换的是 .env不该动的是执行层改之前先把边界划清楚。要动的东西只有三行环境变量XDM_API_KEY、XDM_BASE_URL、XDM_MODEL。不要动的是plan_actions_with_llm()里的 prompt 结构、run_browser_actions()里goto/click/fill的映射关系、以及 Playwright 的启动参数。很多人在换通道时手一抖把请求体也重写了结果模型返回的字段名变了解析直接崩最后误以为是通道问题。判断标准很简单换完之后模型的返回内容格式一模一样Playwright 的输入一模一样那么这次改动就是纯接入层改动。如果run_browser_actions()也要跟着改说明你改多了。2.2 model 先照原文写正式 ID 去模型广场对一遍原文用的claude-sonnet-4-6可以先原样保留在.env里跑通链路优先。但正式接入时model 字段以模型广场当时列表为准不要凭记忆加日期后缀也不要把别处看到的 ID 直接粘过来。模型广场里能看到当前可调用的模型名复制粘贴最省事。如果列表里没有你想要的说明当前通道没上这个模型换一个能用的先跑通流程比死磕一个不存在的 ID 划算。3. 在 TaoToken 控制台建一把 Key顺便对一下模型名3.1 注册、创建 Key、复制到 .env打开 TaoToken 注册登录进控制台找到 API Keys 那一页创建一个新 Key。创建时建议按用途命名比如playwright-browser-agent以后在看用量时能一眼分辨是哪套脚本在调。复制出来的值先别急着塞进代码放.env里别写死在.py文件——脚本改一版传一次 GitKey 就跟着泄一次。这一步拿到的东西就两样一把 Key一个要填进工具的 Base URL。第一样是YOUR_API_KEY第二样是https://taotoken.net/api。注意后者是接口地址末尾不带/v1也不要往上加任何查询参数它跟浏览器里打开的官网落地页不是同一个东西。3.2 模型广场和用量页顺手看一眼同一个入口里模型广场列出当前可调用的模型 ID复制你打算用的那个填到XDM_MODEL。用量页则建议在第一次跑通脚本之后就去看一眼确认这次chat/completions调用有没有被记上——有记录说明地址和 Key 都对了没记录但脚本又不报错通常说明你看的是另一个账号或另一把 Key。把这两个页面加进收藏夹比每次翻聊天记录找地址要省事得多。4. 改 XueDingMaoClientbase_url 指向 https://taotoken.net/api4.1 .env 的最终形态三个变量一行一个不要带引号不要留行尾空格XDM_API_KEYYOUR_API_KEY XDM_BASE_URLhttps://taotoken.net/api XDM_MODELclaude-sonnet-4-6YOUR_API_KEY从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 的控制台创建后复制过来XDM_BASE_URL就写https://taotoken.net/api末尾不要带斜杠也不要带/v1。原来那版如果写的是https://xxx.com/v1这次把/v1整段删掉只留到/api这一层。4.2 拼接 chat/completions 时别再多加一层 /v1原文客户端里那行路径拼接是 404 的高发区。统一成一种写法并且只保留一处拼接点import os import json import requests from dotenv import load_dotenv load_dotenv() class XueDingMaoClient: 负责把「任务 页面状态」发给模型拿回动作数组。 def __init__(self) - None: self.api_key os.environ[XDM_API_KEY] # 末尾不带 /v1也不带斜杠 self.base_url os.environ.get(XDM_BASE_URL, https://taotoken.net/api).rstrip(/) self.model os.environ.get(XDM_MODEL, claude-sonnet-4-6) def chat_endpoint(self) - str: # 只在这里拼一次路径是 base /chat/completions return f{self.base_url}/chat/completions def _headers(self) - dict: return { Authorization: fBearer {self.api_key}, Content-Type: application/json, }如果旧代码里写的是f{base}/v1/chat/completions把它改成f{base}/chat/completions。Base URL 那一层已经交代清楚了路径里再叠一个/v1请求就会打到/api/v1/chat/completions这种不存在的地址上报错通常是干干净净的 404看不出任何线索。4.3 plan_actions_with_llm() 和它的 JSON 约束请求体保持 OpenAI 兼容的写法system prompt 里把「只输出 JSON 数组」讲死SYSTEM_PROMPT 你是一个浏览器动作规划器。 只输出 JSON 数组不要任何解释不要 Markdown 代码块围栏。 数组元素只能是下面三种之一 {action: goto, url: ...} {action: fill, selector: ..., value: ...} {action: click, selector: ...} def plan_actions_with_llm(self, task: str, page_state: str) - list: payload { model: self.model, messages: [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: f任务{task}\n当前页面{page_state}}, ], temperature: 0, } resp requests.post( self.chat_endpoint(), headersself._headers(), jsonpayload, timeout60, ) resp.raise_for_status() raw resp.json()[choices][0][message][content] # 有些模型仍会习惯性加上围栏这里兜一层 text raw.strip().removeprefix(json).removeprefix().removesuffix().strip() return json.loads(text)注意temperature压到 0浏览器动作规划不需要创意需要的是同一页面状态下尽量给出同一套动作。返回结构仍走choices[0].message.content这部分不用因为换了通道而重写。5. run_browser_actions() 照旧goto、fill、click 按序执行5.1 动作数组是模型和执行层之间的唯一契约接入层换掉之后模型和执行层之间的接口必须保持不变也就是那个 JSON 数组。它是整套脚本的契约模型只管写数组Playwright 只管读数组。契约不变换通道就是纯粹的配置改动。数组里的selector建议用稳定的id或语义化的 CSS不要用一层层div div:nth-child(3)。模型看不见你的 DOM只能靠你在 user message 里贴进去的页面片段来猜选择器选择器越脆弱模型猜错的概率越高最后表现成「通道好像有问题返回的动作点不动」其实是选择器写飘了。5.2 执行循环保持原样只补一个未知动作兜底from playwright.sync_api import sync_playwright def run_browser_actions(actions: list, headless: bool False) - None: with sync_playwright() as p: browser p.chromium.launch(headlessheadless) page browser.new_page() try: for step in actions: action step.get(action) if action goto: page.goto(step[url], wait_untildomcontentloaded) elif action fill: page.fill(step[selector], step[value]) elif action click: page.click(step[selector]) elif action wait: page.wait_for_timeout(int(step.get(ms, 500))) else: raise ValueError(f未知动作: {step}) finally: browser.close()这个循环跟模型走哪条通道毫无关系。goto先打开页面fill按顺序填两个输入框click提交中间夹一个wait等页面响应——顺序错了不是模型的问题多半是你在 user message 里贴的页面状态过期了。先让模型重新看一眼当前页面再让它出下一步动作比一次性要求它规划五步要稳。6. 跑一遍动作数组回来了页面也跟着动了6.1 第一次运行盯三个点第一步只打印不执行把plan_actions_with_llm()的返回值print出来确认它是一个 list元素是 dictaction字段在goto/fill/click三者之内。第二步接上run_browser_actions()打开headlessFalse肉眼看着浏览器把动作走完。第三步回 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 的用量页确认这次调用被记上账。三步都过说明接入层已经通了Key 有效、地址正确、路径没多带/v1、返回结构没变。6.2 顺手把 Claude Code 指到同一条通道如果你平时也用 Claude Code 帮着改这套脚本可以让它走同一把 Key、同一个地址省得在两套配置之间来回切。环境变量写法export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_AUTH_TOKENYOUR_API_KEY export ANTHROPIC_MODELclaude-sonnet-4-6或者写进~/.claude/settings.json的env字段键名同样是ANTHROPIC_BASE_URL、ANTHROPIC_AUTH_TOKEN、ANTHROPIC_MODEL。这里地址不带任何查询参数就是纯的https://taotoken.net/api模型名以模型广场为准。Claude Code 在这里的角色是帮你读报错、改脚本浏览器里的点击和填写仍然由本地 Playwright 完成。7. 报错对照401、404、JSON 解析失败7.1 认证和路径类现象常见原因处理方式401 UnauthorizedKey 复制时带了空格或换行或请求头没写成Bearer YOUR_API_KEY重新从控制台复制 Key检查Authorization头404 Not Found路径里出现/api/v1/chat/completions路径拼接时多叠了一层/v1改成base /chat/completionsBase URL 保持https://taotoken.net/api403 或权限类提示Key 属于另一个账号或该 Key 被限制可用模型换一把新 Key并核对模型广场里的模型名这三类都属于配置问题不是 Computer Use 逻辑问题。判断方法把同样的地址、Key、模型名塞进一次最简单的curl或对话测试里能通就是脚本写错了不通就是配置写错了。7.2 模型和输出类model not found一类的报错基本就是XDM_MODEL的值不在模型广场列表里。改成列表里能看到的 ID跑通之后再考虑替换。json.decoder.JSONDecodeError则是另一个方向的问题模型确实返回了内容但内容不是纯 JSON。可能是它加了解释文字也可能包了一层代码块围栏。先看removeprefix那一层兜底有没有生效如果还不行往 system prompt 里再强调一次「只输出 JSON 数组」并且把temperature保持为 0。这类错误跟通道无关换哪家都会遇到属于提示词层面的活。8. 换模型只改 model 字段通道不用重配8.1 多模型协作的切换成本降到一行接入层拆开之后「便宜模型做初筛、强模型做复杂表单」这种协作模式就好实现了两个XueDingMaoClient实例共用同一份XDM_BASE_URL和XDM_API_KEY只是初始化时传入不同的model。换通道、换模型都不用碰run_browser_actions()也不需要改路径拼接。这是这次改造最实在的收益把「通道」和「模型」拆成两个独立变量。以前它俩焊在一起每换一次都要重新验证一遍路径。8.2 跑通之后这几个页面值得打开脚本跑出第一组动作数组之后别急着写更长的流程。先用 TaoToken 模型对话 拿同一把 Key 发一条测试消息确认模型 ID 和地址是一对儿如果打算把这类浏览器 Agent 长期挂着跑去 Coding Plan 看一眼套餐是否够用需要另建一把专用 Key 就去 控制台 API Keys 创建想把这套流程和 Claude Code 的用法对应起来参数对照在 Claude Code 接入文档 里。回到脚本本身接下来要做的事只有一件把 user message 里的页面状态贴得更准一点。模型的动作规划质量八成取决于你喂给它的页面信息而不是取决于它走哪条通道。