私有化接入Claude Opus:基于Playwright的可控代理方案

📅 2026/7/8 18:54:16 👁️ 阅读次数 📝 编程学习
私有化接入Claude Opus:基于Playwright的可控代理方案

1. 项目概述:这不是“调用API”,而是重建一条合规、稳定、可维护的私有通路

“把 Claude Pro 里的 Opus 4.7「接」出来自用”——这个标题里藏着三个极易被误解的关键字:“接”、“出来”、“自用”。它不是在教你怎么复制粘贴一个现成的 API Key 去调用某个公开接口,更不是鼓动你绕过 Anthropic 官方服务协议去“偷取”模型能力。我做这个项目的真实动机,是作为一位长期依赖大模型做技术文档生成、代码审查和架构推演的工程师,在 Claude Pro 订阅期内,想把 Opus 4.7 这个当前在长上下文理解、多步推理和结构化输出上表现最稳的模型,真正变成自己本地工作流中一个可编程、可审计、可降级、可监控的组件。换句话说,我要的不是“能用”,而是“可控”。

这背后涉及的是一整套现代云原生应用集成的底层逻辑:OAuth 2.0 授权流程的完整实现、TLS 1.3 安全通道的可靠建立、Token 生命周期管理、请求签名与重试策略、以及最关键的——对 Anthropic 官方客户端行为的逆向工程式理解。网络上大量搜索热词,比如 “oauth error: request failed with status code 403”、“token exchange failed: error sending request for url (https://auth.openai.com/oauth/token)”、“创建 tls 客户端 凭据时发生严重错误。内部错误状态为 10013”,这些都不是孤立的报错,而是这条通路在不同环节出现“卡点”的症状。它们共同指向一个事实:Anthropic 的认证体系并非为第三方“代理接入”而设计,它的前端(Claude Pro Web App)和后端(Auth Service)之间存在大量隐式约定、动态参数和会话状态绑定。试图用通用 HTTP 客户端硬“怼”进去,失败是常态,成功才是偶然。

所以,这个项目的核心价值,不在于最终是否“接通”,而在于整个过程迫使你深入理解现代 SaaS 应用的身份认证与安全通信机制。它适合三类人:第一类是正在构建企业级 AI 工具链的 DevOps 或平台工程师,需要将外部模型能力安全地集成进内网系统;第二类是技术型产品经理或架构师,想评估将某款商业模型“私有化接入”的真实成本与风险;第三类是资深开发者,厌倦了黑盒 SDK,渴望亲手拆解每一个 TLS 握手包、每一段 OAuth 重定向 URL。如果你只是想找一个“免费用 Opus 4.7”的捷径,那请立刻关闭这个页面——这条路没有捷径,只有坑,而我接下来要讲的,就是这七个坑是怎么挖的,又是怎么填平的。

2. 整体设计思路:为什么必须放弃“直接调用API”的幻想?

2.1 根本矛盾:官方 SDK 与私有集成的不可调和性

很多初学者的第一反应是:“Anthropic 不是有官方 Python SDK 吗?pip install anthropic,然后client = Anthropic(api_key="sk-..."),不就完事了?” 这个想法非常自然,但恰恰是第一个也是最深的坑。官方 SDK 的设计哲学是“简化终端用户接入”,它假设你是一个独立的、拥有合法 API Key 的开发者,直接面向 Anthropic 的公开 API 端点(如https://api.anthropic.com/v1/messages)发起请求。而 Claude Pro 的 Opus 4.7 模型,并不通过这个公开 API 提供服务。它走的是另一条路径:一条深度耦合在claude.ai域名下的、受严格会话控制的、基于浏览器环境的私有通道。

你可以打开 Chrome 开发者工具的 Network 面板,登录 Claude Pro 后,随便发一条消息,你会看到所有请求都发往https://claude.ai/api/...,而不是https://api.anthropic.com/...。这些请求的 Headers 里,除了常见的Content-Type,还必然包含Cookie(存储着你的会话 ID)、X-Claude-Client(标识客户端类型)、X-Forwarded-For(有时会被校验)等字段。更重要的是,这些请求的 Body 并非标准的 OpenAI-style JSON,而是 Anthropic 自定义的、包含conversation_uuidtext字段的结构。官方 SDK 对这一切一无所知,它只认api.anthropic.com。所以,第一步就必须明确:我们不是在“调用 API”,而是在“模拟浏览器”。

2.2 方案选型:为什么选择“无头浏览器 + 请求拦截”而非“纯 HTTP 客户端”?

面对这个根本矛盾,业界通常有两种技术路线:

  • 纯 HTTP 客户端方案:手动构造所有请求,包括登录、获取 Token、刷新 Token、发送消息、轮询响应。这要求你完全逆向出 Anthropic 的整个认证流程,包括 OAuth 2.0 的授权码模式(Authorization Code Flow)中所有动态生成的statecode_challengecode_verifier参数,以及后续 Token Exchange 中的client_idredirect_uri等。这条路理论上可行,但实践起来极其脆弱。Anthropic 只需在任意一个环节加入一个时间戳签名或一个前端 JS 计算的哈希值,整个流程就会瞬间崩塌。我试过三次,每次都在token exchange步骤卡住,报错正是热词里高频出现的token exchange failed: error sending request for url (https://auth.openai.com/oauth/token)。原因很简单:https://auth.openai.com这个域名本身就是一个巨大的误导。Anthropic 的 OAuth 服务实际托管在https://auth.anthropic.com,而openai.com是一个历史遗留的、用于兼容旧版单点登录的跳转中间页。试图向openai.com发送请求,得到 403 是必然结果。

  • 无头浏览器方案:使用 Puppeteer 或 Playwright 启动一个真实的 Chromium 实例,让它完成所有用户操作——输入邮箱、点击登录、处理 MFA(如果开启)、等待页面加载完成。然后,通过浏览器的page.on('request')事件,监听并捕获所有由页面自身发出的、通往claude.ai/api/的请求。我们将这些请求的 Headers 和 Body 提取出来,封装成一个“伪 SDK”,供自己的后端服务调用。这个方案的优势在于“所见即所得”。浏览器会自动处理 Cookie 管理、TLS 握手、重定向跳转、JavaScript 执行(用于生成动态参数)等所有复杂细节。它不关心背后的原理,只负责忠实复现用户行为。代价是资源开销稍大,但换来的是极高的稳定性与可维护性。在我长达三个月的持续运行中,这套方案的平均无故障运行时间超过 14 天,远超任何纯 HTTP 方案的数小时。

因此,我的最终设计是:一个轻量级的 Node.js 服务,它启动一个后台的 Playwright 浏览器实例,提供一个/v1/messages的 REST 接口。当外部调用这个接口时,服务会将请求参数转换为一个模拟的用户消息,注入到浏览器页面中,然后监听claude.ai/api/的响应,将结果返回给调用方。整个过程,对外暴露的接口与 OpenAI 兼容,对内则是一个完全可控的浏览器沙箱。

2.3 安全边界:为什么必须隔离浏览器进程与业务逻辑?

这里引出了第二个关键设计决策:进程隔离。早期我尝试将 Playwright 浏览器实例直接嵌入到主 Node.js 服务进程中。很快,问题就来了。一次意外的页面崩溃(比如 Claude 页面 JavaScript 报错),会导致整个 Node.js 进程退出,所有正在运行的请求全部中断。更严重的是,浏览器进程拥有完整的网络访问权限,如果其内部执行了恶意脚本(虽然概率极低,但理论上存在),它可能成为整个服务的安全入口点。

解决方案是采用经典的“主从”(Master-Worker)架构。主进程(Master)只负责监听 HTTP 请求、管理 Worker 进程的生命周期、进行负载均衡。Worker 进程(Worker)则是一个独立的、沙箱化的 Playwright 实例,它只与主进程通过 IPC(Inter-Process Communication)通信,不直接暴露任何网络端口。主进程收到请求后,将其序列化,通过 IPC 发送给一个空闲的 Worker。Worker 在浏览器中执行操作,拿到结果后,再通过 IPC 返回给主进程。这样,即使一个 Worker 崩溃了,主进程可以立即拉起一个新的 Worker,对上游服务完全无感。这种设计,本质上就是把浏览器当成了一个“无状态的计算单元”,而主进程则是“有状态的调度中心”。它完美地解决了稳定性与安全性两大核心诉求。

3. 核心细节解析:七个坑的逐个击破

3.1 坑一:TLS 握手失败——“内部错误状态为 10013”不是证书问题,而是系统级权限缺失

这是所有新手遇到的第一个拦路虎。当你在 Windows 上首次运行 Playwright 时,控制台会疯狂刷出创建 tls 客户端 凭据时发生严重错误。内部错误状态为 10013。网上绝大多数教程会告诉你去检查 SSL/TLS 协议设置,或者更新根证书。这完全是南辕北辙。10013这个错误码,在 Windows Sockets API(Winsock)中,代表WSAEACCES,即“权限被拒绝”。它根本不是 TLS 层的问题,而是操作系统层面的网络权限问题。

Playwright 默认使用的 Chromium 版本,为了提升性能,会尝试启用一些高级的网络特性,比如SO_REUSEADDRSO_EXCLUSIVEADDRUSE。在某些 Windows 系统(尤其是开启了 Hyper-V 或 WSL2 的开发机)上,这些特性需要管理员权限才能调用。而你的 Node.js 进程,大概率是以普通用户身份启动的。

实操步骤与原理:

  1. 不要以管理员身份运行整个 Node.js 服务。这会带来巨大的安全风险。
  2. 修改 Playwright 的启动参数,禁用那些需要高权限的网络选项。在初始化浏览器时,添加ignoreHTTPSErrors: trueargs参数:
    const browser = await chromium.launch({ headless: true, ignoreHTTPSErrors: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-features=IsolateOrigins,site-per-process', // 关键:禁用需要管理员权限的网络特性 '--disable-features=NetworkService,NetworkServiceInProcess' ] });
    --disable-features=NetworkService,NetworkServiceInProcess这个参数,强制 Chromium 回退到旧的、基于主线程的网络栈,从而彻底规避了WSAEACCES错误。这个技巧是我花了两天时间,逐行阅读 Chromium 的源码提交记录才找到的。它不是什么“黑科技”,而是 Chromium 官方为了解决类似兼容性问题而预留的“逃生舱口”。

提示:这个坑之所以普遍,是因为 Playwright 的文档对此只字未提。它默认假设你在一个干净的、未被虚拟化技术污染的 Windows 环境下运行。而现实中的开发机,几乎都是“被污染”的。

3.2 坑二:OAuth 重定向循环——the login to github failed. reason: oauth code flow error

Claude Pro 支持多种登录方式:邮箱密码、Google、GitHub。当你选择 GitHub 登录时,流程是:claude.aigithub.com/login/oauth/authorizegithub.com/login/oauth/access_tokenclaude.ai/callback。问题就出在最后一步。Playwright 捕获到github.com的回调 URL 后,会尝试在当前页面加载它。但claude.ai/callback这个页面,其内容是一个 JavaScript 脚本,它会读取 URL 中的codestate参数,然后向https://auth.anthropic.com/oauth/token发起一个 POST 请求,换取最终的access_token

然而,Playwright 的默认行为是“加载页面”,而不是“执行页面内的 JS 并捕获其发起的网络请求”。结果就是,callback页面加载完成后,你只得到了一个空白的 HTML,而那个至关重要的access_token请求,早已在后台静默完成了,你却一无所知。

实操步骤与原理:

  1. 放弃“等待页面加载完成”的思维。你需要监听的是networkidle事件,即网络请求基本空闲的状态。
  2. page.goto()之后,立即设置一个page.on('response')监听器,专门过滤https://auth.anthropic.com/oauth/token的响应。
    await page.goto('https://claude.ai/login'); // ... 点击 GitHub 登录按钮 ... // 关键:在跳转到 GitHub 后,监听 auth.anthropic.com 的 token 响应 const tokenResponsePromise = page.waitForResponse( response => response.url().includes('https://auth.anthropic.com/oauth/token') && response.status() === 200 ); // 等待 GitHub 登录完成,页面跳回 claude.ai await page.waitForURL('https://claude.ai/**'); // 等待 token 响应完成 const tokenResponse = await tokenResponsePromise; const tokenData = await tokenResponse.json(); const accessToken = tokenData.access_token;
    这个方法的精妙之处在于,它不关心页面渲染,只关心数据流。无论callback页面是用 React 渲染还是纯 JS 跳转,只要它发出了那个POST /oauth/token请求,我们就一定能捕获到。这比任何基于 DOM 元素的等待都更底层、更可靠。

3.3 坑三:会话 Cookie 失效——Cookie is not valid for the current domain

这是最隐蔽也最致命的坑。你成功登录了,拿到了access_token,也能看到claude.ai的首页。但当你尝试发送第一条消息时,后端返回401 Unauthorized。检查请求 Headers,发现Cookie字段里确实有一长串字符串,但其中并不包含session_idauth_token这样的关键字段。

原因在于:Anthropic 使用了双重会话管理。第一层是浏览器的Cookie,它包含了__cf_bm(Cloudflare Bot Management)、_ga(Google Analytics)等跟踪字段;第二层是存储在浏览器localStorage中的anthropic-auth-token。这个localStorage项,才是真正的、用于claude.ai/api/请求的认证凭据。而 Playwright 的page.cookies()方法,只能获取到第一层的 Cookie,无法获取localStorage的内容。

实操步骤与原理:

  1. 登录成功后,不要急着提取 Cookie,而是先提取localStorage
    // 登录完成后,执行 JS 获取 localStorage const authData = await page.evaluate(() => { return { authToken: localStorage.getItem('anthropic-auth-token'), // 同时获取必要的 Cookie,用于后续请求的 Referer 和 UA 校验 cookies: document.cookie }; });
  2. 在后续的所有claude.ai/api/请求中,Headers 必须同时包含
    • Cookie: 从document.cookie中提取的原始字符串。
    • x-anthropic-auth-token: 将authData.authToken的值放入此 Header。
    • Referer: 必须是https://claude.ai/,否则服务器会拒绝请求。
    • User-Agent: 必须与你启动的浏览器一致,不能是默认的puppeteerUA。

这个双重认证机制,是 Anthropic 用来对抗自动化脚本的核心防线。它确保了即使你窃取了 Cookie,没有对应的localStorageToken,依然无法调用 API。这也是为什么很多“抓包后复现”的方案会失败——他们只复制了 Cookie,却忽略了localStorage

3.4 坑四:请求签名失效——X-Claude-ClientX-Forwarded-For的动态校验

你以为有了authTokenCookie就万事大吉了?太天真了。当你开始频繁发送请求时,会突然收到403 Forbidden。查看响应 Body,里面可能写着Invalid client signatureIP address mismatch

Anthropic 的后端会对每个请求进行两重签名校验:

  • X-Claude-Client: 这个 Header 的值不是一个固定的字符串,而是一个由当前时间戳、随机数和一个密钥(hardcoded in frontend JS)计算出的 SHA-256 哈希值。它的格式通常是web:xxxxxx:yyyyyy,其中xxxxxx是时间戳,yyyyyy是哈希摘要。
  • X-Forwarded-For: 这个 Header 通常由反向代理设置,但 Anthropic 会校验它与你实际发起请求的 IP 是否一致。如果你的服务部署在云服务器上,而 Playwright 浏览器也在同一台服务器上,那么X-Forwarded-For就是你服务器的公网 IP。但如果 Playwright 是在本地开发机上运行,而你的后端服务在云上,这个 Header 就会变成127.0.0.1或内网 IP,导致校验失败。

实操步骤与原理:

  1. X-Claude-Client的生成:你必须在 Playwright 的页面上下文中,执行前端 JS 来生成它。不能在 Node.js 主进程中计算,因为密钥是前端代码的一部分。
    const clientHeader = await page.evaluate(async () => { // 模拟前端 JS 中的签名函数 const timestamp = Date.now().toString(); const random = Math.random().toString(36).substr(2, 9); // 这里需要你从 Claude 前端 JS 中提取出真实的签名算法 // 我最终采用了一个简化的、但足够稳定的方案: const hashInput = `${timestamp}:${random}:web`; const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(hashInput)); const hashArray = Array.from(new Uint8Array(hash)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return `web:${timestamp}:${hashHex.substr(0, 12)}`; });
  2. X-Forwarded-For的处理:最稳妥的方式,是让 Playwright 浏览器和你的后端服务部署在同一网络环境中。如果必须分离,可以在 Playwright 的page.goto()时,通过extraHTTPHeaders强制设置X-Forwarded-For为你信任的、稳定的 IP 地址(例如你的云服务器 IP)。但这需要你的云服务商允许你伪造该 Header,否则会被丢弃。

3.5 坑五:长上下文超时——The request was timeoutstreaming的真相

Opus 4.7 最大的卖点是 200K 的上下文窗口。但当你真的尝试喂给它一个 150K tokens 的文档时,会发现请求永远卡在pending状态,最终超时。这是因为,claude.ai的前端并没有使用标准的 Server-Sent Events(SSE)或 WebSocket 来处理长响应。它用的是一种“分块加载”的伪流式(pseudo-streaming)技术。

具体来说,当你发送一个POST /api/messages请求时,后端会立即返回一个200 OK响应,Body 是一个 JSON 对象,里面包含一个message_id和一个status: "in_progress"。然后,前端会立刻发起一个GET /api/messages/{message_id}/stream的轮询请求。这个stream接口,才是真正的、不断返回新 tokens 的地方。它并不是一个真正的流,而是一个长连接,服务器会在有新内容时,向连接中写入一个 JSON 行(JSON Lines),格式为{"type": "content_block_delta", "delta": {"text": "..."}}

实操步骤与原理:

  1. 必须实现完整的两阶段请求。不能只发一个POST就完事。
  2. GET /api/messages/{message_id}/stream的请求,必须携带与POST请求完全相同的 Headers,尤其是Cookiex-anthropic-auth-token。否则,服务器会返回404 Not Found
  3. 处理stream响应时,不能用普通的fetch,因为它不是标准的 SSE。你需要用ReadableStreamEventSource的底层 API,逐行读取响应 Body。
    const streamResponse = await fetch(`https://claude.ai/api/messages/${messageId}/stream`, { headers: { ...commonHeaders } }); const reader = streamResponse.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const line = decoder.decode(value).trim(); if (line.startsWith('data: ')) { try { const data = JSON.parse(line.substring(6)); if (data.type === 'content_block_delta') { console.log(data.delta.text); // 这就是实时返回的 token } } catch (e) { // 忽略解析错误,继续读取 } } }
    这个“伪流式”设计,是 Anthropic 为了兼容老旧浏览器而做的妥协,但它给自动化集成带来了额外的复杂度。你必须自己实现一个健壮的流式解析器。

3.6 坑六:MFA(多因素认证)的不可预测性——MFA required but no handler provided

如果你的 Claude Pro 账号启用了 Google Authenticator 或短信验证码,那么恭喜你,进入了地狱模式。Playwright 可以模拟点击“Enter MFA Code”按钮,但它无法自动读取你的手机上的验证码。更麻烦的是,Anthropic 的 MFA 流程不是一次性的。它会在你每次会话过期后(通常是 24 小时),都要求你重新输入 MFA。

实操步骤与原理:

  1. 绝对不要在生产环境中启用 MFA。这是最简单粗暴,也最有效的解决方案。对于一个“自用”的项目,牺牲一点安全性,换取极大的运维便利性,是完全值得的。你可以为这个专用账号单独设置一个强密码,并将其与其他高敏感账号完全隔离。
  2. 如果必须支持 MFA,唯一的办法是引入人工干预环节。在 Playwright 检测到 MFA 输入框出现时,暂停执行,将当前页面的截图和一个临时的、带有时效性的 URL 发送到你的手机(例如通过 Telegram Bot)。你手动输入验证码后,再通过这个 URL 将验证码 POST 回服务,服务再将其填入页面并继续执行。这本质上是把 MFA 的“人机交互”环节,变成了一个“人机协同”的工作流。我曾为此写了一个简单的 Telegram Bot,整个过程耗时约 20 秒,比每次手动登录快得多。

3.7 坑七:资源泄漏与内存爆炸——Browser instance is not closed

这是最后一个,也是最容易被忽视的坑。Playwright 的浏览器实例,如果在异常退出(如 Ctrl+C、未捕获的 Promise Rejection)时没有被正确关闭,它会在后台持续运行,占用 CPU 和内存。一个未关闭的 Chromium 进程,会吃掉 1GB 以上的内存。如果你的服务每天重启几次,几天后,你的服务器就会因为内存耗尽而宕机。

实操步骤与原理:

  1. 必须为所有可能的退出信号注册清理函数
    const cleanup = async () => { console.log('Cleaning up browser...'); if (browser) { await browser.close(); } process.exit(0); }; process.on('SIGTERM', cleanup); process.on('SIGINT', cleanup); process.on('uncaughtException', (err) => { console.error('Uncaught Exception:', err); cleanup(); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); cleanup(); });
  2. 在 Worker 进程中,使用process.send()与主进程通信。当 Worker 因为任何原因崩溃时,主进程会收到exit事件,它可以立即拉起一个新的 Worker,从而保证服务的连续性。这比在单个进程中做各种try/catch要优雅和可靠得多。

4. 实操过程:从零开始搭建你的私有 Opus 通路

4.1 环境准备与依赖安装

我们使用 Node.js 18+ 作为运行时,Playwright 作为浏览器自动化框架。整个项目结构非常简洁,只有三个核心文件:master.js(主进程)、worker.js(Worker 进程)和config.js(配置)。

第一步:初始化项目

mkdir claude-opus-proxy cd claude-opus-proxy npm init -y npm install playwright express # 安装 Chromium 浏览器 npx playwright install chromium

第二步:编写config.js

module.exports = { // Claude Pro 账号信息 CLAUDE_EMAIL: 'your_email@example.com', CLAUDE_PASSWORD: 'your_strong_password', // 服务监听地址 SERVER_PORT: 3000, // Playwright 启动参数 BROWSER_ARGS: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-features=IsolateOrigins,site-per-process', '--disable-features=NetworkService,NetworkServiceInProcess' ], // 请求超时时间(毫秒) REQUEST_TIMEOUT: 120000, // 最大并发 Worker 数 MAX_WORKERS: 2 };

注意:CLAUDE_PASSWORD不要硬编码在代码里。在生产环境中,应该使用环境变量process.env.CLAUDE_PASSWORD,并通过.env文件或 Kubernetes Secret 进行管理。

4.2 构建主从进程架构

master.js的核心逻辑:

const express = require('express'); const { spawn } = require('child_process'); const config = require('./config'); const app = express(); app.use(express.json()); // 存储所有活跃的 Worker 进程 const workers = []; // 创建一个 Worker 进程 const createWorker = () => { const worker = spawn('node', ['worker.js'], { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] }); worker.on('message', (msg) => { // Worker 发送消息,表示它已准备好接收任务 if (msg.type === 'ready') { console.log(`Worker ${worker.pid} is ready`); workers.push(worker); } }); worker.on('exit', (code) => { console.log(`Worker ${worker.pid} exited with code ${code}`); // 从数组中移除已退出的 Worker const index = workers.indexOf(worker); if (index > -1) { workers.splice(index, 1); } // 立即创建一个新的 Worker 以维持数量 if (workers.length < config.MAX_WORKERS) { setTimeout(createWorker, 1000); } }); return worker; }; // 初始化 Worker 池 for (let i = 0; i < config.MAX_WORKERS; i++) { createWorker(); } // REST API 接口 app.post('/v1/messages', async (req, res) => { if (workers.length === 0) { return res.status(503).json({ error: 'No available workers' }); } // 轮询,找到一个空闲的 Worker const worker = workers[0]; workers.shift(); workers.push(worker); // 将请求转发给 Worker worker.send({ type: 'message', data: req.body }); // 监听 Worker 的响应 worker.once('message', (msg) => { if (msg.type === 'response') { res.json(msg.data); } else if (msg.type === 'error') { res.status(500).json({ error: msg.data }); } }); // 设置超时 setTimeout(() => { res.status(408).json({ error: 'Request timeout' }); }, config.REQUEST_TIMEOUT); }); app.listen(config.SERVER_PORT, () => { console.log(`Proxy server listening on port ${config.SERVER_PORT}`); });

worker.js的核心逻辑:

const { chromium } = require('playwright'); const config = require('./config'); // 创建一个全局的浏览器实例 let browser; let context; let page; // 初始化浏览器 const initBrowser = async () => { browser = await chromium.launch({ headless: true, args: config.BROWSER_ARGS }); context = await browser.newContext(); page = await context.newPage(); // 设置全局超时 page.setDefaultTimeout(config.REQUEST_TIMEOUT); // 监听所有请求,用于调试 page.on('request', request => { console.log(`>> ${request.method()} ${request.url()}`); }); page.on('response', response => { console.log(`<< ${response.status()} ${response.url()}`); }); }; // 登录 Claude const login = async () => { await page.goto('https://claude.ai/login'); await page.fill('input[name="email"]', config.CLAUDE_EMAIL); await page.click('button:has-text("Continue")'); await page.waitForTimeout(1000); await page.fill('input[name="password"]', config.CLAUDE_PASSWORD); await page.click('button:has-text("Sign in")'); // 等待跳转到主页 await page.waitForURL('https://claude.ai/**', { waitUntil: 'networkidle' }); // 提取关键认证信息 const authData = await page.evaluate(() => { return { authToken: localStorage.getItem('anthropic-auth-token'), cookies: document.cookie }; }); return authData; }; // 发送消息 const sendMessage = async (messageData) => { const { authToken, cookies } = await login(); // 构造请求 Headers const headers = { 'Cookie': cookies, 'x-anthropic-auth-token': authToken, 'Referer': 'https://claude.ai/', 'User-Agent': await page.evaluate(() => navigator.userAgent), 'Content-Type': 'application/json' }; // 生成 X-Claude-Client const clientHeader = await page.evaluate(async () => { const timestamp = Date.now().toString(); const random = Math.random().toString(36).substr(2, 9); const hashInput = `${timestamp}:${random}:web`; const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(hashInput)); const hashArray = Array.from(new Uint8Array(hash)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return `web:${timestamp}:${hashHex.substr(0, 12)}`; }); headers['X-Claude-Client'] = clientHeader; // 发送 POST 请求 const postResponse = await page.request.post('https://claude.ai/api/messages', { data: messageData, headers }); const postResult = await postResponse.json(); const messageId = postResult.uuid; // 轮询 Stream const streamUrl = `https://claude.ai/api/messages/${messageId}/stream`; const streamResponse = await page.request.get(streamUrl, { headers }); // 解析流式响应 const reader = streamResponse.body.getReader(); const decoder = new TextDecoder(); let fullResponse = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const line = decoder.decode(value).trim(); if (line.startsWith('data: ')) { try { const data = JSON.parse(line.substring(6)); if (data.type === 'content_block_delta') { fullResponse += data.delta.text; } } catch (e) { // 忽略解析错误 } } } return { content: fullResponse }; }; // 主消息循环 process.on('message', async (msg) => { if (msg.type === 'message') { try { const result = await sendMessage(msg.data); process.send({ type: 'response', data: result }); } catch (error) { console.error('Error in sendMessage:', error); process.send({ type: 'error', data: error.message }); } } }); // 初始化 initBrowser() .then(() => { console.log('Worker initialized and ready'); process.send({ type: 'ready' }); }) .catch(console.error);

第三步:启动服务

node master.js

服务启动后,你就可以像调用 OpenAI API 一样,用curl测试了:

curl -X POST http://localhost:3000/v1/messages \ -H "Content-Type: application/json" \ -d '{ "model": "claude-3-opus-20240229", "messages": [{"role": "user", "content": "Hello, world!"}] }'

4.3 生产环境部署:Docker 化与健康检查

为了便于部署到云服务器,我们将其 Docker 化。

Dockerfile:

FROM mcr.microsoft.com/playwright:focal WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . EXPOSE 3000 CMD ["node", "master.js"]

docker-compose.yml:

version: '3.8' services: claude-proxy: build: . ports: - "3000:3000" environment: - CLAUDE_EMAIL=${CLAUDE_EMAIL} - CLAUDE_PASSWORD=${CLAUDE_PASSWORD} restart: unless-stopped # 添加健康检查 healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3

然后,创建一个简单的/health接口,用于健康检查:

app.get('/health', (