如何用Python对接claude.ai,claude.ai的第三方API包装

第三方API包装源自此,感谢两位作者贡献:
https://github.com/KoushikNavuluri/Claude-API/pull/102

以下是第三方API包装,的代码:

import json
import os
import uuid
from enum import Enum

from typing import Dict, List, Optional, Union

import requests as req
import tzlocal
from curl_cffi import requests


class ContentType(Enum):
    PDF = 'application/pdf'
    TXT = 'text/plain'
    CSV = 'text/csv'
    OCTET_STREAM = 'application/octet-stream'

class Client:
    BASE_URL = "https://claude.ai/api"
    USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36'

    def __init__(self, cookie: str):
        self.cookie = cookie
        self.organization_id = self._get_organization_id()

    def _get_organization_id(self) -> str:
        url = f"{self.BASE_URL}/organizations"
        headers = self._get_headers()
        response = requests.get(url, headers=headers, impersonate="chrome110")
        response.raise_for_status()
        return response.json()[0]["uuid"]

    def _get_headers(self, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
        headers = {
            'User-Agent': self.USER_AGENT,
            'Accept-Language': 'en-US,en;q=0.5',
            'Referer': 'https://claude.ai/chats',
            'Content-Type': 'application/json',
            'Connection': 'keep-alive',
            'Cookie': self.cookie,
        }
        if extra_headers:
            headers.update(extra_headers)
        return headers

    @staticmethod
    def _get_content_type(file_path: str) -> str:
        extension = os.path.splitext(file_path)[-1].lower()
        return (
            ContentType[extension[1:].upper()].value
            if extension[1:].upper() in ContentType.__members__
            else ContentType.OCTET_STREAM.value
        )

    def list_all_conversations(self) -> List[Dict]:
        url = f"{self.BASE_URL}/organizations/{self.organization_id}/chat_conversations"
        response = requests.get(url, headers=self._get_headers(), impersonate="chrome110")
        response.raise_for_status()
        return response.json()

    def send_message(
        self,
        prompt: str,
        conversation_id: str,
        attachment: Optional[str] = None,
        timeout: int = 500,
        print_stream: bool = False,
    ) -> str:
        url = f"{self.BASE_URL}/organizations/{self.organization_id}/chat_conversations/{conversation_id}/completion"
        attachments = [self.upload_attachment(attachment)] if attachment else []
        
        payload = json.dumps(
            {"prompt": prompt, "timezone": tzlocal.get_localzone_name(), "attachments": attachments}
        )

        headers = self._get_headers({
            'Accept': 'text/event-stream, text/event-stream',
            'Origin': 'https://claude.ai',
            'DNT': '1',
            'TE': 'trailers',
        })

        response = requests.post(
            url,
            headers=headers,
            data=payload,
            impersonate="chrome110",
            timeout=timeout,
            stream=True,
        )
        return self._process_stream_response(response, print_stream)

    @staticmethod
    def _process_stream_response(response, print_stream: bool) -> str:
        gpt_response = []
        for line in response.iter_lines():
            if not line:
                continue
            decoded_line = line.decode('utf-8').strip()
            if not decoded_line.startswith('data: '):
                continue
            try:
                data = json.loads(decoded_line[6:])
                if data['type'] == 'completion':
                    completion = data['completion']
                    gpt_response.append(completion)
                    if print_stream:
                        print(completion, end="", flush=True)
            except json.JSONDecodeError:
                continue
        return "".join(gpt_response)

    def delete_conversation(self, conversation_id: str) -> bool:
        url = f"{self.BASE_URL}/organizations/{self.organization_id}/chat_conversations/{conversation_id}"
        response = requests.delete(
            url,
            headers=self._get_headers(),
            data=json.dumps(conversation_id),
            impersonate="chrome110",
        )
        return response.status_code == 204

    def chat_conversation_history(self, conversation_id: str) -> Dict:
        url = f"{self.BASE_URL}/organizations/{self.organization_id}/chat_conversations/{conversation_id}"
        response = requests.get(url, headers=self._get_headers(), impersonate="chrome110")
        response.raise_for_status()
        return response.json()

    @staticmethod
    def generate_uuid() -> str:
        return str(uuid.uuid4())

    def create_new_chat(self) -> Dict:
        url = f"{self.BASE_URL}/organizations/{self.organization_id}/chat_conversations"
        payload = json.dumps({"uuid": self.generate_uuid(), "name": ""})
        response = requests.post(
            url, headers=self._get_headers(), data=payload, impersonate="chrome110"
        )
        response.raise_for_status()
        return response.json()

    def reset_all(self) -> bool:
        conversations = self.list_all_conversations()
        for conversation in conversations:
            self.delete_conversation(conversation["uuid"])
        return True

    def upload_attachment(self, file_path: str) -> Union[Dict, bool]:
        if file_path.endswith('.txt'):
            return self._process_text_file(file_path)
        url = f'{self.BASE_URL}/convert_document'
        file_name = os.path.basename(file_path)
        content_type = self._get_content_type(file_path)

        files = {
            'file': (file_name, open(file_path, 'rb'), content_type),
            'orgUuid': (None, self.organization_id),
        }

        response = req.post(url, headers=self._get_headers(), files=files)
        response.raise_for_status()
        return response.json()

    @staticmethod
    def _process_text_file(file_path: str) -> Dict:
        file_name = os.path.basename(file_path)
        file_size = os.path.getsize(file_path)
        with open(file_path, 'r', encoding='utf-8') as file:
            file_content = file.read()
        return {
            "file_name": file_name,
            "file_type": ContentType.TXT.value,
            "file_size": file_size,
            "extracted_content": file_content,
        }

    def rename_chat(self, title: str, conversation_id: str) -> bool:
        url = f"{self.BASE_URL}/rename_chat"
        payload = json.dumps({
            "organization_uuid": self.organization_id,
            "conversation_uuid": conversation_id,
            "title": title,
        })
        response = requests.post(
            url, headers=self._get_headers(), data=payload, impersonate="chrome110"
        )
        return response.status_code == 200

以下是我基于他们,实现的http API

import os
from flask import Flask, request, jsonify
from claude_api import Client
import base64

app = Flask(__name__)

# 使用环境变量获取Claude AI cookie
cookie = os.environ.get('CLAUDE_COOKIE')
claude_client = Client(cookie)

@app.route('/send_message', methods=['POST'])
def send_message():
    data = request.json
    prompt = data.get('prompt')
    conversation_id = data.get('conversation_id')
    attachment = data.get('attachment')

    if not prompt:
        return jsonify({"error": "No prompt provided"}), 400

    if not conversation_id:
        conversation_id = claude_client.create_new_chat()['uuid']

    attachment_path = None
    if attachment:
        # 解码Base64并保存为临时文件
        file_content = base64.b64decode(attachment['content'])
        attachment_path = f"temp_{attachment['filename']}"
        with open(attachment_path, 'wb') as f:
            f.write(file_content)

    try:
        response = claude_client.send_message(prompt, conversation_id, attachment=attachment_path)
        return jsonify({"response": response, "conversation_id": conversation_id})
    finally:
        # 删除临时文件
        if attachment_path and os.path.exists(attachment_path):
            os.remove(attachment_path)

@app.route('/send_message_text', methods=['POST'])
def send_message_text():
    data = request.json
    prompt = data.get('prompt')
    conversation_id = data.get('conversation_id')

    if not prompt:
        return "Error: No prompt provided", 400

    if not conversation_id:
        conversation_id = claude_client.create_new_chat()['uuid']

    response = claude_client.send_message(prompt, conversation_id)
    return response

@app.route('/list_conversations', methods=['GET'])
def list_conversations():
    conversations = claude_client.list_all_conversations()
    return jsonify(conversations)

@app.route('/create_conversation', methods=['POST'])
def create_conversation():
    new_chat = claude_client.create_new_chat()
    return jsonify(new_chat)

@app.route('/rename_conversation', methods=['POST'])
def rename_conversation():
    data = request.json
    conversation_id = data.get('conversation_id')
    new_title = data.get('title')

    if not conversation_id or not new_title:
        return jsonify({"error": "Missing conversation_id or title"}), 400

    success = claude_client.rename_chat(new_title, conversation_id)
    return jsonify({"success": success})

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001)

如何使用

首先,你需要修改import,以引入Client类
如果遇到关于时区的报错

[2024-08-29 23:22:57,694] ERROR in app: Exception on /send_message_text [POST]
Traceback (most recent call last):
  File "/home/likewendy/Desktop/claude.ai/venv/lib/python3.12/site-packages/flask/app.py", line 1473, in wsgi_app
    response = self.full_dispatch_request()
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/likewendy/Desktop/claude.ai/venv/lib/python3.12/site-packages/flask/app.py", line 882, in full_dispatch_request
    rv = self.handle_user_exception(e)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/likewendy/Desktop/claude.ai/venv/lib/python3.12/site-packages/flask/app.py", line 880, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/likewendy/Desktop/claude.ai/venv/lib/python3.12/site-packages/flask/app.py", line 865, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/likewendy/Desktop/claude.ai/main.py", line 53, in send_message_text
    response = claude_client.send_message(prompt, conversation_id)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/likewendy/Desktop/claude.ai/venv/lib/python3.12/site-packages/claude_api.py", line 74, in send_message
    {"prompt": prompt, "timezone": tzlocal.get_localzone_name(), "attachments": attachments}
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/likewendy/Desktop/claude.ai/venv/lib/python3.12/site-packages/tzlocal/unix.py", line 209, in get_localzone_name
    _cache_tz_name = _get_localzone_name()
                     ^^^^^^^^^^^^^^^^^^^^^
  File "/home/likewendy/Desktop/claude.ai/venv/lib/python3.12/site-packages/tzlocal/unix.py", line 159, in _get_localzone_name
    raise zoneinfo.ZoneInfoNotFoundError(message)
zoneinfo._common.ZoneInfoNotFoundError: 'Multiple conflicting time zone configurations found:\n/etc/timezone: Asia/Beijing\n/etc/localtime is a symlink to: Asia/Shanghai\nFix the configuration, or set the time zone in a TZ environment variable.\n'
127.0.0.1 - - [29/Aug/2024 23:22:57] "POST /send_message_text HTTP/1.1" 500 -
^[[A^C(venv) likewendy@likewendy-PC:~/Desktop/claude.ai

则尝试

export TZ=Asia/Shangha

然后你需要

export CLAUDE_COOKIE="你的cookie“

下面是http API文档

Claude API HTTP服务文档

本文档概述了与Claude AI服务交互的API端点。

基础URL

所有端点都相对于:http://你的服务器地址:5001

认证

该服务使用环境变量CLAUDE_COOKIE进行认证。确保在启动服务器之前设置此变量。

端点

1. 发送消息

向Claude发送消息并获取回复。

  • URL: /send_message
  • 方法: POST
  • 内容类型: application/json
请求体
{
  "prompt": "你发送给Claude的消息",
  "conversation_id": "可选的对话ID",
  "attachment": {
    "filename": "可选的文件名.ext",
    "content": "可选的Base64编码文件内容"
  }
}
  • prompt(必需):发送给Claude的消息。
  • conversation_id(可选):现有对话的ID。如果不提供,将创建一个新的对话。
  • attachment(可选):如果要发送附件,包含文件信息的对象。
响应
{
  "response": "Claude的回复",
  "conversation_id": "对话ID"
}

2. 发送消息(纯文本响应)

向Claude发送消息并获取纯文本回复。

  • URL: /send_message_text
  • 方法: POST
  • 内容类型: application/json
请求体
{
  "prompt": "你发送给Claude的消息",
  "conversation_id": "可选的对话ID"
}
响应

Claude的纯文本回复。

3. 列出对话

获取所有对话的列表。

  • URL: /list_conversations
  • 方法: GET
响应
[
  {
    "uuid": "对话ID",
    "name": "对话名称",
    "created_at": "创建时间戳",
    "updated_at": "更新时间戳"
  },
  // ... 更多对话
]

4. 创建对话

创建一个新的对话。

  • URL: /create_conversation
  • 方法: POST
响应
{
  "uuid": "新对话ID",
  "name": "新对话名称",
  "created_at": "创建时间戳",
  "updated_at": "更新时间戳"
}

5. 重命名对话

重命名现有对话。

  • URL: /rename_conversation
  • 方法: POST
  • 内容类型: application/json
请求体
{
  "conversation_id": "现有对话ID",
  "title": "新对话标题"
}
响应
{
  "success": true
}

错误处理

所有端点都会返回适当的HTTP状态码:

  • 200:操作成功
  • 400:错误请求(例如,缺少必需参数)
  • 500:服务器错误

错误响应将包含一个带有"error"键的JSON对象,描述问题。

注意事项

  • 使用带附件的/send_message端点时,文件内容应该是Base64编码的。
  • 服务器默认运行在5001端口。确保此端口可用且可访问。
  • 除了/send_message_text端点返回纯文本外,所有响应均为JSON格式。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Deng_Xian_Shemg

捐助1元钱

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值