aixo commited on
Commit
c72f0db
·
verified ·
1 Parent(s): 365844a
Files changed (15) hide show
  1. .dockerignore +3 -0
  2. .github/workflows/docker-image.yml +54 -0
  3. .gitignore +3 -0
  4. LICENSE +21 -0
  5. README.md +95 -10
  6. app/config.py +37 -0
  7. app/errors.py +35 -0
  8. app/models.py +100 -0
  9. app/utils.py +504 -0
  10. docker-compose.yml +10 -0
  11. jscode/env.js +0 -0
  12. jscode/main.js +128 -0
  13. main.py +459 -0
  14. pyproject.toml +12 -0
  15. uv.lock +343 -0
.dockerignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .venv
2
+ .github
3
+ .git
.github/workflows/docker-image.yml ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Docker Image CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ "master" ]
6
+ pull_request:
7
+ branches: [ "master" ]
8
+
9
+ jobs:
10
+
11
+ build:
12
+
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Log in to GitHub Container Registry
19
+ uses: docker/login-action@v3
20
+ with:
21
+ registry: ghcr.io
22
+ username: ${{ github.actor }}
23
+ password: ${{ secrets.GITHUB_TOKEN }}
24
+
25
+ - name: Set up QEMU
26
+ uses: docker/setup-qemu-action@v3
27
+
28
+ - name: Set up Docker Buildx
29
+ id: buildx
30
+ uses: docker/setup-buildx-action@v3
31
+
32
+ - name: Available platforms
33
+ run: echo ${{ steps.buildx.outputs.platforms }}
34
+
35
+ - name: Extract metadata
36
+ id: meta
37
+ uses: docker/metadata-action@v5
38
+ with:
39
+ images: ghcr.io/${{ github.repository_owner }}/cursorweb2api
40
+ tags: |
41
+ type=ref,event=branch
42
+ type=ref,event=pr
43
+ type=sha,prefix={{branch}}-
44
+ type=raw,value=latest,enable={{is_default_branch}}
45
+
46
+ - name: Build and push Docker image
47
+ uses: docker/build-push-action@v5
48
+ with:
49
+ context: .
50
+ file: ./Dockerfile
51
+ platforms: linux/amd64,linux/arm64
52
+ push: true
53
+ tags: ${{ steps.meta.outputs.tags }}
54
+ labels: ${{ steps.meta.outputs.labels }}
.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ .venv
2
+ .idea
3
+ node_modules
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 jhhgiyv
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,10 +1,95 @@
1
- ---
2
- title: Cursor
3
- emoji: 📈
4
- colorFrom: yellow
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cursorweb2api
2
+
3
+ Cursor 官网聊天 转换为 OpenAI 兼容的 API 接口,支持流式响应
4
+
5
+ ## 🚀 一键部署
6
+
7
+ docker compose
8
+
9
+ ```yaml
10
+ version: '3.8'
11
+
12
+ services:
13
+ cursorweb2api:
14
+ image: ghcr.io/jhhgiyv/cursorweb2api:latest
15
+ container_name: cursorweb2api
16
+ ports:
17
+ - "8000:8000"
18
+ environment:
19
+ - API_KEY=aaa
20
+ - FP=eyJVTk1BU0tFRF9WRU5ET1JfV0VCR0wiOiJHb29nbGUgSW5jLiAoSW50ZWwpIiwiVU5NQVNLRURfUkVOREVSRVJfV0VCR0wiOiJBTkdMRSAoSW50ZWwsIEludGVsKFIpIFVIRCBHcmFwaGljcyAoMHgwMDAwOUJBNCkgRGlyZWN0M0QxMSB2c181XzAgcHNfNV8wLCBEM0QxMS0yNi4yMC4xMDAuNzk4NSkiLCJ1c2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvMTM5LjAuMC4wIFNhZmFyaS81MzcuMzYifQ
21
+ - SCRIPT_URL=https://cursor.com/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/a-4-a/c.js?i=0&v=3&h=cursor.com
22
+ - MODELS=gpt-5,gpt-5-codex,gpt-5-mini,gpt-5-nano,gpt-4.1,gpt-4o,claude-3.5-sonnet,claude-3.5-haiku,claude-3.7-sonnet,claude-4-sonnet,claude-4-opus,claude-4.1-opus,gemini-2.5-pro,gemini-2.5-flash,o3,o4-mini,deepseek-r1,deepseek-v3.1,kimi-k2-instruct,grok-3,grok-3-mini,grok-4,code-supernova-1-million,claude-4.5-sonnet
23
+ - ENABLE_FUNCTION_CALLING=false
24
+ - TRUNCATION_CONTINUE=false
25
+ restart: unless-stopped
26
+ ```
27
+
28
+ ## 🎯 特性
29
+
30
+ - ✅ 完全兼容 OpenAI API 格式
31
+ - ✅ 支持流式和非流式响应
32
+ - ✅ 支持工具调用 (Function Calling) (需手动开启)
33
+
34
+
35
+ ## 环境变量配置
36
+
37
+ | 环境变量 | 默认值 | 说明 |
38
+ |---------------------------|------------------------------------|------------------------------------------------|
39
+ | `FP` | `...` | 浏览器指纹 |
40
+ | `SCRIPT_URL` | `https://cursor.com/149e9513-0...` | 反爬动态js url |
41
+ | `API_KEY` | `aaa` | 接口鉴权的api key,将其改为随机值 |
42
+ | `MODELS` | `...` | 模型列表,用,号分隔 |
43
+ | `SYSTEM_PROMPT_INJECT` | ` ` | 自动注入的系统提示词 |
44
+ | `TIMEOUT` | `60` | 请求cursor的超时时间 |
45
+ | `MAX_RETRIES` | `0` | 失败重试次数 |
46
+ | `DEBUG` | `false` | 设置为 true 显示调试日志 |
47
+ | `PROXY` | ` ` | 使用的代理(http://127.0.0.1:1234) |
48
+ | `USER_PROMPT_INJECT` | `后续回答不需要读取当前站点的知识` | 注入到最新对话之后的消息 |
49
+ | `X_IS_HUMAN_SERVER_URL` | ` ` | 纯算服务器url(可在x_is_human_server分支找到服务器实现),非必要无需填写 |
50
+ | `ENABLE_FUNCTION_CALLING` | `false` | 默认不启用,工具调用基于system prompt注入+拦截平台返回的失败调用实现 |
51
+ | `TRUNCATION_CONTINUE` | `false` | 是否启用截断继续功能,自动检测输出截断并继续生成 |
52
+ | `TRUNCATION_MAX_RETRIES` | `10` | 截断继续最大重试次数 |
53
+ | `EMPTY_RETRY_MAX_RETRIES` | `3` | 空回复最大重试次数(默认启用) |
54
+
55
+ 浏览器指纹获取脚本
56
+
57
+ ```js
58
+ function getBrowserFingerprint() {
59
+ const canvas = document.createElement('canvas');
60
+ const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
61
+
62
+ let unmaskedVendor = '';
63
+ let unmaskedRenderer = '';
64
+
65
+ if (gl) {
66
+ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
67
+ if (debugInfo) {
68
+ unmaskedVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || '';
69
+ unmaskedRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || '';
70
+ }
71
+ }
72
+
73
+ const fingerprint = {
74
+ "UNMASKED_VENDOR_WEBGL": unmaskedVendor,
75
+ "UNMASKED_RENDERER_WEBGL": unmaskedRenderer,
76
+ "userAgent": navigator.userAgent
77
+ };
78
+
79
+ // 转换为 JSON 字符串
80
+ const jsonString = JSON.stringify(fingerprint);
81
+
82
+ // 转换为 base64
83
+ const base64String = btoa(jsonString);
84
+
85
+ return {
86
+ json: fingerprint,
87
+ jsonString: jsonString,
88
+ base64: base64String
89
+ };
90
+ }
91
+
92
+ const base64Only = getBrowserFingerprint().base64;
93
+ console.log('指纹数据: ', base64Only);
94
+
95
+ ```
app/config.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import sys
4
+
5
+ from loguru import logger
6
+
7
+ from app.utils import decode_base64url_safe
8
+
9
+ FP = json.loads(decode_base64url_safe(os.environ.get("FP",
10
+ "eyJVTk1BU0tFRF9WRU5ET1JfV0VCR0wiOiJHb29nbGUgSW5jLiAoSW50ZWwpIiwiVU5NQVNLRURfUkVOREVSRVJfV0VCR0wiOiJBTkdMRSAoSW50ZWwsIEludGVsKFIpIFVIRCBHcmFwaGljcyAoMHgwMDAwOUJBNCkgRGlyZWN0M0QxMSB2c181XzAgcHNfNV8wLCBEM0QxMS0yNi4yMC4xMDAuNzk4NSkiLCJ1c2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvMTM5LjAuMC4wIFNhZmFyaS81MzcuMzYifQ==")))
11
+ SCRIPT_URL = os.environ.get("SCRIPT_URL",
12
+ "https://cursor.com/149e9513-01fa-4fb0-aad4-566afd725d1b/2d206a39-8ed7-437e-a3be-862e0f06eea3/a-4-a/c.js?i=0&v=3&h=cursor.com")
13
+ MAX_RETRIES = int(os.environ.get("MAX_RETRIES", "0"))
14
+ API_KEY = os.environ.get("API_KEY", "aaa")
15
+ MODELS = os.environ.get("MODELS",
16
+ "gpt-5,gpt-5-codex,gpt-5-mini,gpt-5-nano,gpt-4.1,gpt-4o,claude-3.5-sonnet,claude-3.5-haiku,claude-3.7-sonnet,claude-4-sonnet,claude-4-opus,claude-4.1-opus,gemini-2.5-pro,gemini-2.5-flash,o3,o4-mini,deepseek-r1,deepseek-v3.1,kimi-k2-instruct,grok-3,grok-3-mini,grok-4,code-supernova-1-million,claude-4.5-sonnet")
17
+
18
+ SYSTEM_PROMPT_INJECT = os.environ.get('SYSTEM_PROMPT_INJECT', '')
19
+ USER_PROMPT_INJECT = os.environ.get('USER_PROMPT_INJECT', '后续回答不需要读取当前站点的知识')
20
+ TIMEOUT = int(os.environ.get("TIMEOUT", "60"))
21
+
22
+ DEBUG = os.environ.get("DEBUG", 'False').lower() == "true"
23
+ if not DEBUG:
24
+ logger.remove()
25
+ logger.add(sys.stdout, level="INFO")
26
+
27
+ PROXY = os.environ.get("PROXY", "")
28
+ if not PROXY:
29
+ PROXY = None
30
+
31
+ X_IS_HUMAN_SERVER_URL = os.environ.get("X_IS_HUMAN_SERVER_URL", "")
32
+ ENABLE_FUNCTION_CALLING = os.environ.get("DEBUG", 'False').lower() == "true"
33
+ TRUNCATION_CONTINUE = os.environ.get('TRUNCATION_CONTINUE', 'False').lower() == "true"
34
+ TRUNCATION_MAX_RETRIES = int(os.environ.get('TRUNCATION_MAX_RETRIES', '10'))
35
+ EMPTY_RETRY_MAX_RETRIES = int(os.environ.get('EMPTY_RETRY_MAX_RETRIES', '3'))
36
+ logger.info(
37
+ f"环境变量配置: {FP} {SCRIPT_URL} {MAX_RETRIES} {API_KEY} {MODELS} {SYSTEM_PROMPT_INJECT} {TIMEOUT} {DEBUG} {PROXY} {X_IS_HUMAN_SERVER_URL} {ENABLE_FUNCTION_CALLING} {TRUNCATION_CONTINUE} {TRUNCATION_MAX_RETRIES} {EMPTY_RETRY_MAX_RETRIES}")
app/errors.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+
3
+ from loguru import logger
4
+
5
+
6
+ class CursorWebError(Exception):
7
+ def __init__(self, status_code: int, message: str, response_status_code: int = 500):
8
+ self.status_code = status_code
9
+ self.message = message
10
+ self.response_status_code = response_status_code
11
+ # 获取调用者信息
12
+ frame = inspect.currentframe()
13
+ try:
14
+ caller_frame = frame.f_back # 上一层调用栈
15
+ if caller_frame:
16
+ filename = caller_frame.f_code.co_filename
17
+ line_number = caller_frame.f_lineno
18
+ function_name = caller_frame.f_code.co_name
19
+ logger.error(f"{self.__str__()} - Called from {filename}:{line_number} in {function_name}")
20
+ else:
21
+ logger.error(self.__str__())
22
+ finally:
23
+ del frame # 避免循环引用
24
+
25
+ def __str__(self) -> str:
26
+ return f"CursorWebError: {self.status_code}, {self.message}"
27
+
28
+ def to_openai_error(self) -> dict[str, dict[str, str]]:
29
+ return {
30
+ "error": {
31
+ "message": self.__str__(),
32
+ "type": "cursorweb_error",
33
+ "code": "cursorweb_error"
34
+ }
35
+ }
app/models.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Any, Literal, Optional
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class OpenAIToolCallFunction(BaseModel):
7
+ """工具调用函数"""
8
+
9
+ name: str | None = Field(None, description="函数名称")
10
+ arguments: str | None = Field(None, description="JSON格式的函数参数")
11
+
12
+
13
+ class OpenAIDeltaToolCall(BaseModel):
14
+ index: int | None = Field(None, description="工具调用索引")
15
+ id: str | None = Field(None, description="工具调用ID")
16
+ type: Literal["function"] | None = Field(None, description="调用类型")
17
+ function: OpenAIToolCallFunction | None = Field(None, description="函数详情增量")
18
+
19
+
20
+ class OpenAIMessageContent(BaseModel):
21
+ """OpenAI消息内容项"""
22
+
23
+ type: Literal["text", "image_url"] = Field(description="内容类型")
24
+ text: str | None = Field(None, description="文本内容")
25
+ image_url: dict[str, str] | None = Field(None, description="图像URL配置")
26
+
27
+
28
+ class Message(BaseModel):
29
+ role: str
30
+ content: str | list[OpenAIMessageContent] | None = Field(
31
+ None, description="消息内容"
32
+ )
33
+ tool_call_id: str | None = Field(None)
34
+ tool_calls: list[dict[str, Any]] | None = Field(
35
+ None, description="工具调用信息(当role为assistant时)"
36
+ )
37
+
38
+
39
+ class OpenAIToolFunction(BaseModel):
40
+ """OpenAI工具函数定义"""
41
+
42
+ name: str = Field(description="函数名称")
43
+ description: str | None = Field(None, description="函数描述")
44
+ parameters: dict[str, Any] | None = Field(
45
+ None, description="JSON Schema格式的函数参数"
46
+ )
47
+
48
+
49
+ class OpenAITool(BaseModel):
50
+ """OpenAI工具定义"""
51
+
52
+ type: Literal["function"] = Field("function", description="工具类型")
53
+ function: OpenAIToolFunction = Field(description="函数定义")
54
+
55
+
56
+ class ChatCompletionRequest(BaseModel):
57
+ messages: List[Message]
58
+ stream: Optional[bool] = False
59
+ model: Optional[str] = "gpt-4o"
60
+ tools: list[OpenAITool] | None = Field(None, description="可用工具定义")
61
+
62
+
63
+ class Model(BaseModel):
64
+ id: str
65
+ object: str
66
+ created: int
67
+ owned_by: str
68
+
69
+
70
+ class ModelsResponse(BaseModel):
71
+ object: str
72
+ data: List[Model]
73
+
74
+
75
+ class Choice(BaseModel):
76
+ index: int
77
+ message: Optional[Dict[str, Any]] = None
78
+ delta: Optional[Dict[str, Any]] = None
79
+ finish_reason: Optional[str] = None
80
+
81
+
82
+ class Usage(BaseModel):
83
+ prompt_tokens: int
84
+ completion_tokens: int
85
+ total_tokens: int
86
+
87
+
88
+ class ChatCompletionResponse(BaseModel):
89
+ id: str
90
+ object: str
91
+ created: int
92
+ model: str
93
+ choices: List[Choice]
94
+ usage: Optional[Usage] = None
95
+
96
+
97
+ class ToolCall(BaseModel):
98
+ toolName: str
99
+ toolId: str
100
+ toolInput: str
app/utils.py ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import base64
3
+ import json
4
+ import random
5
+ import string
6
+ import time
7
+ import uuid
8
+ from functools import wraps
9
+ from typing import Union, Callable, Any, AsyncGenerator, Dict
10
+
11
+ from curl_cffi.requests.exceptions import RequestException
12
+ from sse_starlette import EventSourceResponse
13
+ from starlette.responses import JSONResponse
14
+
15
+ from app.errors import CursorWebError
16
+ from app.models import ChatCompletionRequest, Usage, ToolCall, Message
17
+
18
+
19
+ async def safe_stream_wrapper(
20
+ generator_func, *args, **kwargs
21
+ ) -> Union[EventSourceResponse, JSONResponse]:
22
+ """
23
+ 安全的流响应包装器
24
+ 先执行生成器获取第一个值,如果成功才创建流响应
25
+ """
26
+ # 创建生成器实例
27
+ generator = generator_func(*args, **kwargs)
28
+
29
+ # 尝试获取第一个值
30
+ first_item = await generator.__anext__()
31
+
32
+ # 如果成功获取第一个值,创建新的生成器包装原生成器
33
+ async def wrapped_generator():
34
+ # 先yield第一个值
35
+ yield first_item
36
+ # 然后yield剩余的值
37
+ async for item in generator:
38
+ yield item
39
+
40
+ # 创建流响应
41
+ return EventSourceResponse(
42
+ wrapped_generator(),
43
+ media_type="text/event-stream",
44
+ headers={
45
+ "Cache-Control": "no-cache",
46
+ "Connection": "keep-alive",
47
+ "X-Accel-Buffering": "no",
48
+ },
49
+ )
50
+
51
+
52
+ async def error_wrapper(func: Callable, *args, **kwargs) -> Any:
53
+ from .config import MAX_RETRIES
54
+ for attempt in range(MAX_RETRIES + 1): # 包含初始尝试,所以是 MAX_RETRIES + 1
55
+ try:
56
+ return await func(*args, **kwargs)
57
+ except (CursorWebError, RequestException) as e:
58
+
59
+ # 如果已经达到最大重试次数,返回错误响应
60
+ if attempt == MAX_RETRIES:
61
+ if isinstance(e, CursorWebError):
62
+ return JSONResponse(
63
+ e.to_openai_error(),
64
+ status_code=e.response_status_code
65
+ )
66
+ elif isinstance(e, RequestException):
67
+ return JSONResponse(
68
+ {
69
+ 'error': {
70
+ 'message': str(e),
71
+ "type": "http_error",
72
+ "code": "http_error"
73
+ }
74
+ },
75
+ status_code=500
76
+ )
77
+
78
+ if attempt < MAX_RETRIES:
79
+ continue
80
+ return None
81
+
82
+
83
+ def decode_base64url_safe(data):
84
+ """使用安全的base64url解码"""
85
+ # 添加必要的填充
86
+ missing_padding = len(data) % 4
87
+ if missing_padding:
88
+ data += '=' * (4 - missing_padding)
89
+
90
+ return base64.urlsafe_b64decode(data)
91
+
92
+
93
+ def to_async(sync_func):
94
+ @wraps(sync_func)
95
+ async def async_wrapper(*args):
96
+ loop = asyncio.get_running_loop()
97
+ return await loop.run_in_executor(None, sync_func, *args)
98
+
99
+ return async_wrapper
100
+
101
+
102
+ def generate_random_string(length):
103
+ """
104
+ 生成一个指定长度的随机字符串,包含大小写字母和数字。
105
+ """
106
+ # 定义所有可能的字符:大小写字母和数字
107
+ characters = string.ascii_letters + string.digits
108
+
109
+ # 使用 random.choice 从字符集中随机选择字符,重复 length 次,然后拼接起来
110
+ random_string = ''.join(random.choice(characters) for _ in range(length))
111
+ return random_string
112
+
113
+
114
+ def normalize_tool_name(name: str) -> str:
115
+ """将工具名统一标准化:将所有下划线替换为连字符"""
116
+ return name.replace('_', '-')
117
+
118
+
119
+ def match_tool_name(tool_name: str, available_tools: list[str]) -> str:
120
+ """
121
+ 匹配工具名称,如果不在列表中则尝试标准化匹配
122
+
123
+ Args:
124
+ tool_name: 需要匹配的工具名
125
+ available_tools: 可用的工具名列表
126
+
127
+ Returns:
128
+ 匹配到的实际工具名,如果没有匹配返回原名称
129
+ """
130
+ # 直接匹配
131
+ if tool_name in available_tools:
132
+ return tool_name
133
+
134
+ # 标准化后匹配
135
+ normalized_input = normalize_tool_name(tool_name)
136
+ for available_tool in available_tools:
137
+ if normalize_tool_name(available_tool) == normalized_input:
138
+ return available_tool
139
+
140
+ # 没有匹配,返回原名称
141
+ return tool_name
142
+
143
+
144
+ async def non_stream_chat_completion(
145
+ request: ChatCompletionRequest,
146
+ generator: AsyncGenerator[str, None]
147
+ ) -> Dict[str, Any]:
148
+ """
149
+ 非流式响应:接受外部异步生成器,收集所有输出返回完整响应
150
+ """
151
+ # 收集所有流式输出
152
+ full_content = ""
153
+ tool_calls = []
154
+ usage = Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0)
155
+ async for chunk in generator:
156
+ if isinstance(chunk, Usage):
157
+ usage = chunk
158
+ continue
159
+ if isinstance(chunk, ToolCall):
160
+ tool_calls.append({
161
+ "id": chunk.toolId,
162
+ "type": "function",
163
+ "function": {
164
+ "name": chunk.toolName,
165
+ "arguments": chunk.toolInput,
166
+ }
167
+ })
168
+ continue
169
+ full_content += chunk
170
+
171
+ # 构造OpenAI格式的响应
172
+ response = {
173
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
174
+ "object": "chat.completion",
175
+ "created": int(time.time()),
176
+ "model": request.model,
177
+ "choices": [
178
+ {
179
+ "index": 0,
180
+ "message": {
181
+ "role": "assistant",
182
+ "content": full_content,
183
+ "tool_calls": tool_calls
184
+ },
185
+ "finish_reason": "stop"
186
+ }
187
+ ],
188
+ "usage": {
189
+ "prompt_tokens": usage.prompt_tokens,
190
+ "completion_tokens": usage.completion_tokens,
191
+ "total_tokens": usage.total_tokens
192
+ }
193
+ }
194
+
195
+ return response
196
+
197
+
198
+ async def stream_chat_completion(
199
+ request: ChatCompletionRequest,
200
+ generator: AsyncGenerator[str, None]
201
+ ) -> AsyncGenerator[Dict[str, Any], None]:
202
+ """
203
+ 流式响应:接受外部异步生成器,包装成OpenAI SSE格式
204
+ """
205
+ chat_id = f"chatcmpl-{uuid.uuid4().hex[:29]}"
206
+ created_time = int(time.time())
207
+
208
+ is_send_init = False
209
+
210
+ # 发送初始流式响应头
211
+ initial_response = {
212
+ "id": chat_id,
213
+ "object": "chat.completion.chunk",
214
+ "created": created_time,
215
+ "model": request.model,
216
+ "choices": [
217
+ {
218
+ "index": 0,
219
+ "delta": {"role": "assistant", "content": ""},
220
+ "finish_reason": None
221
+ }
222
+ ]
223
+ }
224
+
225
+ # 流式发送内容
226
+ usage = None
227
+ tool_call_idx = 0
228
+ async for chunk in generator:
229
+ if not is_send_init:
230
+ yield {
231
+ "data": json.dumps(initial_response, ensure_ascii=False)
232
+ }
233
+ is_send_init = True
234
+ if isinstance(chunk, Usage):
235
+ usage = chunk
236
+ continue
237
+
238
+ if isinstance(chunk, ToolCall):
239
+ data = {
240
+ "id": chat_id,
241
+ "object": "chat.completion.chunk",
242
+ "created": created_time,
243
+ "model": request.model,
244
+ "choices": [
245
+ {
246
+ "index": 0,
247
+ "delta": {
248
+ "tool_calls": [
249
+ {
250
+ "index": tool_call_idx,
251
+ "id": chunk.toolId,
252
+ "type": "function",
253
+ "function": {
254
+ "name": chunk.toolName,
255
+ "arguments": chunk.toolInput,
256
+ },
257
+ }
258
+ ]
259
+ },
260
+ "finish_reason": None,
261
+ }
262
+ ],
263
+ }
264
+ tool_call_idx += 1
265
+ yield {'data': json.dumps(data, ensure_ascii=False)}
266
+ continue
267
+
268
+ chunk_response = {
269
+ "id": chat_id,
270
+ "object": "chat.completion.chunk",
271
+ "created": created_time,
272
+ "model": request.model,
273
+ "choices": [
274
+ {
275
+ "index": 0,
276
+ "delta": {"content": chunk},
277
+ "finish_reason": None
278
+ }
279
+ ]
280
+ }
281
+ yield {"data": json.dumps(chunk_response, ensure_ascii=False)}
282
+
283
+ # 发送结束标记
284
+ final_response = {
285
+ "id": chat_id,
286
+ "object": "chat.completion.chunk",
287
+ "created": created_time,
288
+ "model": request.model,
289
+ "choices": [
290
+ {
291
+ "index": 0,
292
+ "delta": {},
293
+ "finish_reason": "stop"
294
+ }
295
+ ]
296
+ }
297
+ yield {"data": json.dumps(final_response, ensure_ascii=False)}
298
+ if usage:
299
+ usage_data = {"id": chat_id, "object": "chat.completion.chunk",
300
+ "created": created_time, "model": request.model,
301
+ "choices": [],
302
+ "usage": {"prompt_tokens": usage.prompt_tokens,
303
+ "completion_tokens": usage.completion_tokens,
304
+ "total_tokens": usage.total_tokens, "prompt_tokens_details": {
305
+ "cached_tokens": 0,
306
+ "text_tokens": 0,
307
+ "audio_tokens": 0,
308
+ "image_tokens": 0
309
+ },
310
+ "completion_tokens_details": {
311
+ "text_tokens": 0,
312
+ "audio_tokens": 0,
313
+ "reasoning_tokens": 0
314
+ },
315
+ "input_tokens": 0,
316
+ "output_tokens": 0,
317
+ "input_tokens_details": None}
318
+ }
319
+
320
+ yield {
321
+ "data": json.dumps(usage_data, ensure_ascii=False)
322
+ }
323
+ yield {"data": "[DONE]"}
324
+
325
+
326
+ async def empty_retry_wrapper(
327
+ cursor_chat_func: Callable,
328
+ request: ChatCompletionRequest,
329
+ max_retries: int = 3
330
+ ) -> AsyncGenerator[Union[str, Usage, ToolCall], None]:
331
+ """
332
+ 空回复重试包装器:检测到空回复时自动重试
333
+
334
+ Args:
335
+ cursor_chat_func: cursor_chat函数
336
+ request: 聊天请求
337
+ max_retries: 最大重试次数
338
+
339
+ Yields:
340
+ str/Usage/ToolCall: 流式输出
341
+
342
+ Raises:
343
+ CursorWebError: 重试后仍然空回复
344
+ """
345
+ for retry_count in range(max_retries + 1):
346
+ generator = cursor_chat_func(request)
347
+ has_content = False
348
+
349
+ async for chunk in generator:
350
+ if isinstance(chunk, ToolCall):
351
+ # 工具调用算有内容
352
+ has_content = True
353
+ yield chunk
354
+ return
355
+
356
+ elif isinstance(chunk, Usage):
357
+ # Usage直接透传
358
+ yield chunk
359
+
360
+ else:
361
+ # 文本内容
362
+ has_content = True
363
+ yield chunk
364
+
365
+ # 如果有内容,正常返回
366
+ if has_content:
367
+ return
368
+
369
+ # 没有内容且还有重试次数,继续重试
370
+ if retry_count < max_retries:
371
+ continue
372
+
373
+ # 达到最大重试次数仍然空回复,抛出异常
374
+ raise CursorWebError(200, f"空回复重试{max_retries}次后仍然失败")
375
+
376
+
377
+ async def truncation_continue_wrapper(
378
+ cursor_chat_func: Callable,
379
+ request: ChatCompletionRequest,
380
+ max_retries: int = 10
381
+ ) -> AsyncGenerator[Union[str, Usage, ToolCall], None]:
382
+ """
383
+ 截断继续包装器:实时流式输出,检测到截断时自动重试
384
+
385
+ Args:
386
+ cursor_chat_func: cursor_chat函数
387
+ request: 聊天请求
388
+ max_retries: 最大重试次数
389
+
390
+ Yields:
391
+ str/Usage/ToolCall: 流式输出
392
+ """
393
+ full_content = "" # 累积的完整内容
394
+ total_prompt_tokens = 0
395
+ total_completion_tokens = 0
396
+ total_tokens = 0
397
+ current_usage = None
398
+
399
+ for retry_count in range(max_retries + 1):
400
+ generator = cursor_chat_func(request)
401
+ current_content = "" # 当前轮次的内容
402
+ is_truncated = False
403
+ buffer = "" # 缓冲区,仅在重试时使用
404
+ buffer_yielded = False # 标记是否已经处理并输出过缓冲区
405
+
406
+ async for chunk in generator:
407
+ if isinstance(chunk, Usage):
408
+ current_usage = chunk
409
+ # 累加token统计
410
+ total_prompt_tokens += chunk.prompt_tokens
411
+ total_completion_tokens += chunk.completion_tokens
412
+ total_tokens += chunk.total_tokens
413
+
414
+ # 检查是否截断
415
+ is_truncated = chunk.completion_tokens == 4096
416
+ break
417
+
418
+ elif isinstance(chunk, ToolCall):
419
+ # 工具调用直接返回
420
+ yield chunk
421
+ return
422
+
423
+ else:
424
+ # 文本内容
425
+ current_content += chunk
426
+
427
+ if retry_count == 0:
428
+ # 第一次请求,实时输出
429
+ yield chunk
430
+ else:
431
+ # 重试时,使用缓冲区
432
+ buffer += chunk
433
+ last_10_chars = full_content[-10:] if len(full_content) >= 10 else full_content
434
+
435
+ if not buffer_yielded:
436
+ # 检查缓冲区是否包含last_10_chars
437
+ if last_10_chars and last_10_chars in buffer:
438
+ # 找到匹配,移除并输出剩余部分
439
+ buffer = buffer.replace(last_10_chars, "", 1)
440
+ if buffer:
441
+ yield buffer
442
+ buffer = ""
443
+ buffer_yielded = True
444
+ elif len(buffer) > 20:
445
+ # 缓冲区超过20字符还没匹配,直接输出
446
+ yield buffer
447
+ buffer = ""
448
+ buffer_yielded = True
449
+ else:
450
+ # 已经处理过缓冲区,直接实时输出
451
+ yield chunk
452
+ buffer = ""
453
+
454
+ # 处理流结束后的缓冲区
455
+ if retry_count > 0 and buffer:
456
+ last_10_chars = full_content[-10:] if len(full_content) >= 10 else full_content
457
+ if not buffer_yielded and last_10_chars and last_10_chars in buffer:
458
+ buffer = buffer.replace(last_10_chars, "", 1)
459
+ if buffer:
460
+ yield buffer
461
+
462
+ # 更新累积内容
463
+ full_content += current_content
464
+
465
+ # 检查是否被截断
466
+ if not is_truncated:
467
+ # 未被截断,返回最终usage
468
+ if current_usage:
469
+ yield current_usage
470
+ return
471
+
472
+ # 被截断,构造继续对话
473
+ last_10_chars = full_content[-10:] if len(full_content) >= 10 else full_content
474
+ continue_prompt = f'''你的回复在"{last_10_chars}"处意外中断。
475
+
476
+ 请直接从该处继续输出,遵循以下规则:
477
+ 1. 以"{last_10_chars}"开头,紧接新内容
478
+ 2. 若在代码块中,直接续写代码,禁止重复```标记或语言标识
479
+ 3. 保持原有的格式、缩进和上下文
480
+
481
+ 错误示例:截断于"document."
482
+ ❌ ```javascript\nlet a=1;\ndocument.createElement...
483
+
484
+ 正确示例:
485
+ ✅ document.createElement...
486
+
487
+ 立即继续,不要解释或重新开始。'''
488
+
489
+ # 重新构造上下文
490
+ new_messages = request.messages.copy()
491
+ new_messages.append(Message(role="assistant", content=full_content, tool_calls=None, tool_call_id=None))
492
+ new_messages.append(Message(role="user", content=continue_prompt, tool_calls=None, tool_call_id=None))
493
+
494
+ request = ChatCompletionRequest(
495
+ messages=new_messages,
496
+ stream=request.stream,
497
+ model=request.model,
498
+ tools=request.tools
499
+ )
500
+
501
+ # 达到最大重试次数,返回最终usage
502
+
503
+ if current_usage:
504
+ yield current_usage
docker-compose.yml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ cursorweb2api:
5
+ build: .
6
+ ports:
7
+ - "8000:8000"
8
+
9
+ environment:
10
+ - DEBUG=false
jscode/env.js ADDED
The diff for this file is too large to render. See raw diff
 
jscode/main.js ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ global.cursor_config = {
2
+ currentScriptSrc: "$$currentScriptSrc$$",
3
+ fp:{
4
+ UNMASKED_VENDOR_WEBGL:"$$UNMASKED_VENDOR_WEBGL$$",
5
+ UNMASKED_RENDERER_WEBGL:"$$UNMASKED_RENDERER_WEBGL$$",
6
+ userAgent: "$$userAgent$$"
7
+ }
8
+ }
9
+
10
+ $$env_jscode$$
11
+
12
+ let console_log = console.log;
13
+ console.log = function () {
14
+
15
+ }
16
+
17
+ dtavm = console;
18
+ delete __dirname;
19
+ delete __filename;
20
+
21
+ function proxy(obj, objname, type) {
22
+ function getMethodHandler(WatchName, target_obj) {
23
+ let methodhandler = {
24
+ apply(target, thisArg, argArray) {
25
+ if (this.target_obj) {
26
+ thisArg = this.target_obj
27
+ }
28
+ let result = Reflect.apply(target, thisArg, argArray)
29
+ if (target.name !== "toString") {
30
+ if (target.name === "addEventListener") {
31
+ dtavm.log(`调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray[0]}], 结果 => [${result}].`)
32
+ } else if (WatchName === "window.console") {
33
+ } else {
34
+ dtavm.log(`调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${result}].`)
35
+ }
36
+ } else {
37
+ dtavm.log(`调用者 => [${WatchName}] 函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${result}].`)
38
+ }
39
+ return result
40
+ },
41
+ construct(target, argArray, newTarget) {
42
+ var result = Reflect.construct(target, argArray, newTarget)
43
+ dtavm.log(`调用者 => [${WatchName}] 构造函数名 => [${target.name}], 传参 => [${argArray}], 结果 => [${(result)}].`)
44
+ return result;
45
+ }
46
+ }
47
+ methodhandler.target_obj = target_obj
48
+ return methodhandler
49
+ }
50
+
51
+ function getObjhandler(WatchName) {
52
+ let handler = {
53
+ get(target, propKey, receiver) {
54
+ let result = target[propKey]
55
+ if (result instanceof Object) {
56
+ if (typeof result === "function") {
57
+ dtavm.log(`调用者 => [${WatchName}] 获取属性名 => [${propKey}] , 是个函数`)
58
+ return new Proxy(result, getMethodHandler(WatchName, target))
59
+ } else {
60
+ dtavm.log(`调用者 => [${WatchName}] 获取属性名 => [${propKey}], 结果 => [${(result)}]`);
61
+ }
62
+ return new Proxy(result, getObjhandler(`${WatchName}.${propKey}`))
63
+ }
64
+ if (typeof (propKey) !== "symbol") {
65
+ dtavm.log(`调用者 => [${WatchName}] 获取属性名 => [${propKey?.description ?? propKey}], 结果 => [${result}]`);
66
+ }
67
+ return result;
68
+ },
69
+ set(target, propKey, value, receiver) {
70
+ if (value instanceof Object) {
71
+ dtavm.log(`调用者 => [${WatchName}] 设置属性名 => [${propKey}], 值为 => [${(value)}]`);
72
+ } else {
73
+ dtavm.log(`调用者 => [${WatchName}] 设置属性名 => [${propKey}], 值为 => [${value}]`);
74
+ }
75
+ return Reflect.set(target, propKey, value, receiver);
76
+ },
77
+ has(target, propKey) {
78
+ var result = Reflect.has(target, propKey);
79
+ dtavm.log(`针对in操作符的代理has=> [${WatchName}] 有无属性名 => [${propKey}], 结果 => [${result}]`)
80
+ return result;
81
+ },
82
+ deleteProperty(target, propKey) {
83
+ var result = Reflect.deleteProperty(target, propKey);
84
+ dtavm.log(`拦截属性delete => [${WatchName}] 删除属性名 => [${propKey}], 结果 => [${result}]`)
85
+ return result;
86
+ },
87
+ defineProperty(target, propKey, attributes) {
88
+ var result = Reflect.defineProperty(target, propKey, attributes);
89
+ dtavm.log(`拦截对象define操作 => [${WatchName}] 待检索属性名 => [${propKey.toString()}] 属性描述 => [${(attributes)}], 结果 => [${result}]`)
90
+ // debugger
91
+ return result
92
+ },
93
+ getPrototypeOf(target) {
94
+ var result = Reflect.getPrototypeOf(target)
95
+ dtavm.log(`被代理的目标对象 => [${WatchName}] 代理结果 => [${(result)}]`)
96
+ return result;
97
+ },
98
+ setPrototypeOf(target, proto) {
99
+ dtavm.log(`被拦截的目标对象 => [${WatchName}] 对象新原型==> [${(proto)}]`)
100
+ return Reflect.setPrototypeOf(target, proto);
101
+ },
102
+ preventExtensions(target) {
103
+ dtavm.log(`方法用于设置preventExtensions => [${WatchName}] 防止扩展`)
104
+ return Reflect.preventExtensions(target);
105
+ },
106
+ isExtensible(target) {
107
+ var result = Reflect.isExtensible(target)
108
+ dtavm.log(`拦截对对象的isExtensible() => [${WatchName}] isExtensible, 返回值==> [${result}]`)
109
+ return result;
110
+ },
111
+ }
112
+ return handler;
113
+ }
114
+
115
+ if (type === "method") {
116
+ return new Proxy(obj, getMethodHandler(objname, obj));
117
+ }
118
+ return new Proxy(obj, getObjhandler(objname));
119
+ }
120
+
121
+ // window = proxy(window, 'window');
122
+ global.document = window.document;
123
+
124
+ $$cursor_jscode$$
125
+
126
+
127
+ window.V_C[0]().then(value => console_log(JSON.stringify(value)));
128
+
main.py ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import json
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ import tempfile
7
+ import time
8
+ from typing import Optional
9
+
10
+ from curl_cffi import AsyncSession, Response
11
+ from fastapi import FastAPI, Depends, HTTPException
12
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
13
+ from loguru import logger
14
+ from starlette.middleware.cors import CORSMiddleware
15
+
16
+ from app.config import SCRIPT_URL, FP, API_KEY, MODELS, SYSTEM_PROMPT_INJECT, TIMEOUT, PROXY, USER_PROMPT_INJECT, \
17
+ X_IS_HUMAN_SERVER_URL, ENABLE_FUNCTION_CALLING, TRUNCATION_CONTINUE, TRUNCATION_MAX_RETRIES, EMPTY_RETRY_MAX_RETRIES
18
+ from app.errors import CursorWebError
19
+ from app.models import ChatCompletionRequest, Message, ModelsResponse, Model, Usage, OpenAIMessageContent, ToolCall
20
+ from app.utils import error_wrapper, to_async, generate_random_string, non_stream_chat_completion, \
21
+ stream_chat_completion, safe_stream_wrapper, match_tool_name, truncation_continue_wrapper, empty_retry_wrapper
22
+
23
+ main_code = open('./jscode/main.js', 'r', encoding='utf-8').read()
24
+ env_code = open('./jscode/env.js', 'r', encoding='utf-8').read()
25
+ app = FastAPI()
26
+
27
+ security = HTTPBearer()
28
+
29
+ app.add_middleware(
30
+ CORSMiddleware,
31
+ allow_origins=["*"],
32
+ allow_credentials=True,
33
+ allow_methods=["*"],
34
+ allow_headers=["*"],
35
+ )
36
+
37
+
38
+ @app.post("/v1/chat/completions")
39
+ async def chat_completions(
40
+ request: ChatCompletionRequest,
41
+ credentials: HTTPAuthorizationCredentials = Depends(security),
42
+ ):
43
+ """处理聊天完成请求"""
44
+
45
+ if credentials.credentials != API_KEY:
46
+ raise HTTPException(401, 'api key 错误')
47
+
48
+ # 空回复重试包装器(始终启用)
49
+ chat_func = lambda req: empty_retry_wrapper(cursor_chat, req, max_retries=EMPTY_RETRY_MAX_RETRIES)
50
+
51
+ if TRUNCATION_CONTINUE:
52
+ chat_generator = truncation_continue_wrapper(chat_func, request, max_retries=TRUNCATION_MAX_RETRIES)
53
+ else:
54
+ chat_generator = chat_func(request)
55
+
56
+ # async for c in chat_generator:
57
+ # logger.debug(c)
58
+
59
+ if request.stream:
60
+ return await error_wrapper(safe_stream_wrapper, stream_chat_completion, request, chat_generator)
61
+ else:
62
+ return await error_wrapper(non_stream_chat_completion, request, chat_generator)
63
+
64
+
65
+ @app.get("/v1/models")
66
+ async def list_models(credentials: HTTPAuthorizationCredentials = Depends(security)):
67
+ models = MODELS.split(',')
68
+ model_list = []
69
+
70
+ for model_id in models:
71
+ model_list.append(
72
+ Model(
73
+ id=model_id, # 使用model name作为对外的id
74
+ object="model",
75
+ created=int(time.time()),
76
+ owned_by='',
77
+ )
78
+ )
79
+
80
+ return ModelsResponse(object="list", data=model_list)
81
+
82
+
83
+ def inject_system_prompt(list_openai_message: list[Message], inject_prompt: str):
84
+ # 查找是否存在system角色的消息
85
+ system_message_found = False
86
+
87
+ for message in list_openai_message:
88
+ if message.role == "system":
89
+ system_message_found = True
90
+ # 处理content字段,需要考虑不同的数据类型
91
+ if message.content is None:
92
+ message.content = inject_prompt
93
+ elif isinstance(message.content, str):
94
+ message.content += f'\n{inject_prompt}'
95
+ elif isinstance(message.content, list):
96
+ # 如果content是列表,需要找到text类型的内容进行追加
97
+ # 或者添加一个新的text内容项
98
+ text_content_found = False
99
+ for content_item in message.content:
100
+ if content_item.type == "text" and content_item.text:
101
+ content_item.text += f'\n{inject_prompt}'
102
+ text_content_found = True
103
+ break
104
+
105
+ # 如果没有找到text内容,添加一个新的text内容项
106
+ if not text_content_found:
107
+ new_text_content = OpenAIMessageContent(
108
+ type="text",
109
+ text=inject_prompt
110
+ , image_url=None)
111
+ message.content.append(new_text_content)
112
+ break # 找到第一个system消息后就退出循环
113
+
114
+ # 如果没有找到system消息,在列表开头插入一个新的system消息
115
+ if not system_message_found:
116
+ system_message = Message(
117
+ role="system",
118
+ content=inject_prompt
119
+ , tool_call_id=None, tool_calls=None)
120
+ list_openai_message.insert(0, system_message)
121
+
122
+
123
+ def collect_developer_messages(list_openai_message: list[Message]) -> str:
124
+ collected_contents = []
125
+
126
+ # 从后往前遍历,避免删除元素时索引变化的问题
127
+ for i in range(len(list_openai_message) - 1, -1, -1):
128
+ message = list_openai_message[i]
129
+
130
+ if message.role == "developer":
131
+ # 提取消息内容
132
+ content_text = ""
133
+
134
+ if message.content is None:
135
+ content_text = ""
136
+ elif isinstance(message.content, str):
137
+ content_text = message.content
138
+ elif isinstance(message.content, list):
139
+ # 如果content是列表,提取所有text类型的内容
140
+ text_parts = []
141
+ for content_item in message.content:
142
+ if content_item.type == "text" and content_item.text:
143
+ text_parts.append(content_item.text)
144
+ content_text = " ".join(text_parts) # 多个text内容用空格连接
145
+
146
+ # 将内容添加到收集列表的开头,保持原始顺序
147
+ collected_contents.insert(0, content_text)
148
+
149
+ # 删除该消息
150
+ list_openai_message.pop(i)
151
+
152
+ # 将收集到的内容按\n拼接并返回
153
+ return "\n".join(collected_contents)
154
+
155
+
156
+ def to_cursor_messages(request: ChatCompletionRequest):
157
+ list_openai_message: list[Message] = request.messages
158
+ if list_openai_message is None:
159
+ list_openai_message = []
160
+
161
+ developer_messages = collect_developer_messages(list_openai_message)
162
+ inject_system_prompt(list_openai_message, developer_messages)
163
+
164
+ if ENABLE_FUNCTION_CALLING:
165
+ if request.tools:
166
+ tools = [tool.model_dump_json() for tool in request.tools]
167
+ inject_system_prompt(list_openai_message, "你可用的工具: " + json.dumps(tools))
168
+ inject_system_prompt(list_openai_message, "不允许使用tool_calls: xxxx调用工具,请使用原生的工具调用方法")
169
+
170
+ if SYSTEM_PROMPT_INJECT:
171
+ inject_system_prompt(list_openai_message, SYSTEM_PROMPT_INJECT)
172
+ if USER_PROMPT_INJECT:
173
+ list_openai_message.append(Message(role='user', content=USER_PROMPT_INJECT, tool_calls=None, tool_call_id=None))
174
+
175
+ result: list[dict[str, str]] = []
176
+
177
+ for m in list_openai_message:
178
+ if not m:
179
+ continue
180
+
181
+ if ENABLE_FUNCTION_CALLING:
182
+ if m.tool_calls:
183
+ message = {
184
+ 'role': m.role,
185
+ 'parts': [{
186
+ 'type': 'text',
187
+ 'text': f"tool_calls: {json.dumps(m.tool_calls, ensure_ascii=False)}"
188
+ }]
189
+ }
190
+ result.append(message)
191
+ continue
192
+
193
+ if m.tool_call_id:
194
+ message = {
195
+ 'role': 'user',
196
+ 'parts': [{
197
+ 'type': 'text',
198
+ 'text': f"{m.role}: tool_call_id: {m.tool_call_id} {m.content}"
199
+ }]
200
+ }
201
+ result.append(message)
202
+ continue
203
+
204
+ text = ''
205
+ if isinstance(m.content, str):
206
+ text = m.content
207
+ else:
208
+ for content in m.content:
209
+ if not content.text:
210
+ continue
211
+ text = text + content.text
212
+ message = {
213
+ 'role': m.role,
214
+ 'parts': [{
215
+ 'type': 'text',
216
+ 'text': text
217
+ }]
218
+ }
219
+ result.append(message)
220
+
221
+ if result[0]['role'] == 'system' and not result[0]['parts'][0]['text']:
222
+ result.pop(0)
223
+
224
+ return result
225
+
226
+
227
+ def parse_sse_line(line: str) -> Optional[str]:
228
+ """解析SSE数据行"""
229
+ line = line.strip()
230
+ if line.startswith("data: "):
231
+ return line[6:] # 去掉 'data: ' 前缀
232
+ return None
233
+
234
+
235
+ async def cursor_chat(request: ChatCompletionRequest):
236
+ # 提取可用工具名列表,用于后续修正
237
+ available_tool_names = []
238
+ if ENABLE_FUNCTION_CALLING and request.tools:
239
+ available_tool_names = [tool.function.name for tool in request.tools]
240
+
241
+ json_data = {
242
+ "context": [
243
+
244
+ ],
245
+ "model": request.model,
246
+ "id": generate_random_string(16),
247
+ "messages": to_cursor_messages(request),
248
+ "trigger": "submit-message"
249
+ }
250
+ async with AsyncSession(impersonate='chrome', timeout=TIMEOUT, proxy=PROXY) as session:
251
+ if X_IS_HUMAN_SERVER_URL:
252
+ x_is_human = await get_x_is_human_server(session)
253
+ else:
254
+ x_is_human = await get_x_is_human(session)
255
+ logger.debug(x_is_human)
256
+ headers = {
257
+ 'User-Agent': FP.get("userAgent"),
258
+ # 'Accept-Encoding': 'gzip, deflate, br, zstd',
259
+ 'Content-Type': 'application/json',
260
+ 'sec-ch-ua-platform': '"Windows"',
261
+ 'x-path': '/api/chat',
262
+ 'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
263
+ 'x-method': 'POST',
264
+ 'sec-ch-ua-bitness': '"64"',
265
+ 'sec-ch-ua-mobile': '?0',
266
+ 'sec-ch-ua-arch': '"x86"',
267
+ 'x-is-human': x_is_human,
268
+ 'sec-ch-ua-platform-version': '"19.0.0"',
269
+ 'origin': 'https://cursor.com',
270
+ 'sec-fetch-site': 'same-origin',
271
+ 'sec-fetch-mode': 'cors',
272
+ 'sec-fetch-dest': 'empty',
273
+ 'referer': 'https://cursor.com/en-US/learn/how-ai-models-work',
274
+ 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
275
+ 'priority': 'u=1, i',
276
+ }
277
+ logger.debug(json_data)
278
+ async with session.stream("POST", 'https://cursor.com/api/chat', headers=headers, json=json_data,
279
+ impersonate='chrome') as response:
280
+ response: Response
281
+ # logger.debug(await response.atext())
282
+
283
+ if response.status_code != 200:
284
+ text = await response.atext()
285
+ if 'Attention Required! | Cloudflare' in text:
286
+ text = 'Cloudflare 403'
287
+ raise CursorWebError(response.status_code, text)
288
+ content_type = response.headers['content-type']
289
+ if 'text/event-stream' not in content_type:
290
+ text = await response.atext()
291
+ raise CursorWebError(response.status_code, "响应非事件流: " + text)
292
+ async for line in response.aiter_lines():
293
+ line = line.decode("utf-8")
294
+ logger.debug(line)
295
+ data = parse_sse_line(line)
296
+ if not data:
297
+ continue
298
+ if data and data.strip():
299
+ try:
300
+ event_data = json.loads(data)
301
+ if event_data.get('type') == 'error':
302
+ err_msg = event_data.get('errorText', 'errorText为空')
303
+ if 'The content field in the Message object at' in err_msg:
304
+ err_msg = "消息为空,很可能你的消息只包含图片,本接口不支持图片\n" + err_msg
305
+ raise CursorWebError(response.status_code, err_msg)
306
+ if event_data.get('type') == 'finish':
307
+ usage = event_data.get('messageMetadata', {}).get('usage')
308
+ if not usage:
309
+ continue
310
+ yield Usage(prompt_tokens=usage.get('inputTokens'),
311
+ completion_tokens=usage.get('outputTokens'),
312
+ total_tokens=usage.get('totalTokens'))
313
+ return
314
+ if ENABLE_FUNCTION_CALLING:
315
+ if event_data.get('type') == 'tool-input-error':
316
+ tool_call_id = event_data.get('toolCallId')
317
+ tool_name = event_data.get('toolName')
318
+ tool_input = event_data.get('input')
319
+ if isinstance(tool_input, str):
320
+ tool_input_str = tool_input
321
+ else:
322
+ tool_input_str = json.dumps(tool_input)
323
+
324
+ # 修正工具名称
325
+ if available_tool_names:
326
+ tool_name = match_tool_name(tool_name, available_tool_names)
327
+
328
+ response.close() # 工具返回了直接掐断
329
+ yield ToolCall(toolId=tool_call_id, toolInput=tool_input_str, toolName=tool_name)
330
+ return
331
+
332
+ delta = event_data.get('delta')
333
+ # logger.debug(delta)
334
+ if not delta:
335
+ continue
336
+ yield delta
337
+ except json.JSONDecodeError:
338
+ continue
339
+
340
+
341
+ async def get_x_is_human_server(session: AsyncSession):
342
+ headers = {
343
+ 'User-Agent': FP.get("userAgent"),
344
+ # 'Accept-Encoding': 'gzip, deflate, br, zstd',
345
+ 'sec-ch-ua-arch': '"x86"',
346
+ 'sec-ch-ua-platform': '"Windows"',
347
+ 'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
348
+ 'sec-ch-ua-bitness': '"64"',
349
+ 'sec-ch-ua-mobile': '?0',
350
+ 'sec-ch-ua-platform-version': '"19.0.0"',
351
+ 'sec-fetch-site': 'same-origin',
352
+ 'sec-fetch-mode': 'no-cors',
353
+ 'sec-fetch-dest': 'script',
354
+ 'referer': 'https://cursor.com/en-US/learn/how-ai-models-work',
355
+ 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
356
+ }
357
+
358
+ response = await session.get(SCRIPT_URL,
359
+ headers=headers,
360
+ impersonate='chrome')
361
+ cursor_js = response.text
362
+ js_b64 = base64.b64encode(cursor_js.encode('utf-8')).decode("utf-8")
363
+
364
+ response = await session.post(X_IS_HUMAN_SERVER_URL, json={
365
+ "jscode": js_b64,
366
+ "fp": FP
367
+ })
368
+ try:
369
+ s = response.json().get('s')
370
+ except json.decoder.JSONDecodeError:
371
+ raise CursorWebError(response.status_code, '纯算服务器返回结果错误: ' + response.text)
372
+ if not s:
373
+ raise CursorWebError(response.status_code, '纯算服务器返回结果错误: ' + response.text)
374
+
375
+ return response.text
376
+
377
+
378
+ async def get_x_is_human(session: AsyncSession):
379
+ headers = {
380
+ 'User-Agent': FP.get("userAgent"),
381
+ # 'Accept-Encoding': 'gzip, deflate, br, zstd',
382
+ 'sec-ch-ua-arch': '"x86"',
383
+ 'sec-ch-ua-platform': '"Windows"',
384
+ 'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
385
+ 'sec-ch-ua-bitness': '"64"',
386
+ 'sec-ch-ua-mobile': '?0',
387
+ 'sec-ch-ua-platform-version': '"19.0.0"',
388
+ 'sec-fetch-site': 'same-origin',
389
+ 'sec-fetch-mode': 'no-cors',
390
+ 'sec-fetch-dest': 'script',
391
+ 'referer': 'https://cursor.com/en-US/learn/how-ai-models-work',
392
+ 'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
393
+ }
394
+
395
+ response = await session.get(SCRIPT_URL,
396
+ headers=headers,
397
+ impersonate='chrome')
398
+ cursor_js = response.text
399
+
400
+ # 替换指纹
401
+ main = (main_code.replace("$$currentScriptSrc$$", SCRIPT_URL)
402
+ .replace("$$UNMASKED_VENDOR_WEBGL$$", FP.get("UNMASKED_VENDOR_WEBGL"))
403
+ .replace("$$UNMASKED_RENDERER_WEBGL$$", FP.get("UNMASKED_RENDERER_WEBGL"))
404
+ .replace("$$userAgent$$", FP.get("userAgent")))
405
+
406
+ # 替换代码
407
+ main = main.replace('$$env_jscode$$', env_code)
408
+ main = main.replace("$$cursor_jscode$$", cursor_js)
409
+ return await runjs(main)
410
+
411
+
412
+ @to_async
413
+ def runjs(jscode: str) -> str:
414
+ """
415
+ 执行 JavaScript 代码并返回标准输出内容。
416
+
417
+ Args:
418
+ jscode: 要执行的 JavaScript 代码字符串
419
+
420
+ Returns:
421
+ Node.js 程序的标准输出内容
422
+
423
+ Raises:
424
+ FileNotFoundError: Node.js 未安装或不在系统 PATH 中
425
+ subprocess.CalledProcessError: Node.js 程序执行失败,异常信息包含 stdout 和 stderr
426
+ """
427
+ temp_dir = tempfile.mkdtemp()
428
+ try:
429
+ js_file_path = os.path.join(temp_dir, "script.js")
430
+ with open(js_file_path, "w", encoding="utf-8") as f:
431
+ f.write(jscode)
432
+
433
+ result = subprocess.run(
434
+ ['node', js_file_path],
435
+ capture_output=True,
436
+ text=True,
437
+ encoding="utf-8"
438
+ )
439
+
440
+ if result.returncode != 0:
441
+ error_msg = f"Node.js 执行失败 (退出码: {result.returncode})\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
442
+ logger.error(error_msg)
443
+ raise subprocess.CalledProcessError(result.returncode, ['node', js_file_path], result.stdout, result.stderr)
444
+
445
+ return result.stdout.strip()
446
+ finally:
447
+ shutil.rmtree(temp_dir)
448
+
449
+
450
+ if __name__ == "__main__":
451
+ import uvicorn
452
+
453
+ uvicorn.run(
454
+ "main:app",
455
+ host="0.0.0.0",
456
+ port=8000,
457
+ reload=False,
458
+ log_level="info",
459
+ )
pyproject.toml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "cursorweb2api"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "curl-cffi>=0.13.0",
8
+ "fastapi>=0.117.1",
9
+ "loguru>=0.7.3",
10
+ "sse-starlette>=3.0.2",
11
+ "uvicorn>=0.37.0",
12
+ ]
uv.lock ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.12"
4
+
5
+ [[package]]
6
+ name = "annotated-types"
7
+ version = "0.7.0"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
10
+ wheels = [
11
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
12
+ ]
13
+
14
+ [[package]]
15
+ name = "anyio"
16
+ version = "4.11.0"
17
+ source = { registry = "https://pypi.org/simple" }
18
+ dependencies = [
19
+ { name = "idna" },
20
+ { name = "sniffio" },
21
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
22
+ ]
23
+ sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
24
+ wheels = [
25
+ { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
26
+ ]
27
+
28
+ [[package]]
29
+ name = "certifi"
30
+ version = "2025.8.3"
31
+ source = { registry = "https://pypi.org/simple" }
32
+ sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
33
+ wheels = [
34
+ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
35
+ ]
36
+
37
+ [[package]]
38
+ name = "cffi"
39
+ version = "2.0.0"
40
+ source = { registry = "https://pypi.org/simple" }
41
+ dependencies = [
42
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
43
+ ]
44
+ sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
45
+ wheels = [
46
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
47
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
48
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
49
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
50
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
51
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
52
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
53
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
54
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
55
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
56
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
57
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
58
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
59
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
60
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
61
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
62
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
63
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
64
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
65
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
66
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
67
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
68
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
69
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
70
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
71
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
72
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
73
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
74
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
75
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
76
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
77
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
78
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
79
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
80
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
81
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
82
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
83
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
84
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
85
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
86
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
87
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
88
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
89
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
90
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
91
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
92
+ ]
93
+
94
+ [[package]]
95
+ name = "click"
96
+ version = "8.3.0"
97
+ source = { registry = "https://pypi.org/simple" }
98
+ dependencies = [
99
+ { name = "colorama", marker = "sys_platform == 'win32'" },
100
+ ]
101
+ sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
102
+ wheels = [
103
+ { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
104
+ ]
105
+
106
+ [[package]]
107
+ name = "colorama"
108
+ version = "0.4.6"
109
+ source = { registry = "https://pypi.org/simple" }
110
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
111
+ wheels = [
112
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
113
+ ]
114
+
115
+ [[package]]
116
+ name = "curl-cffi"
117
+ version = "0.13.0"
118
+ source = { registry = "https://pypi.org/simple" }
119
+ dependencies = [
120
+ { name = "certifi" },
121
+ { name = "cffi" },
122
+ ]
123
+ sdist = { url = "https://files.pythonhosted.org/packages/4e/3d/f39ca1f8fdf14408888e7c25e15eed63eac5f47926e206fb93300d28378c/curl_cffi-0.13.0.tar.gz", hash = "sha256:62ecd90a382bd5023750e3606e0aa7cb1a3a8ba41c14270b8e5e149ebf72c5ca", size = 151303, upload-time = "2025-08-06T13:05:42.988Z" }
124
+ wheels = [
125
+ { url = "https://files.pythonhosted.org/packages/19/d1/acabfd460f1de26cad882e5ef344d9adde1507034528cb6f5698a2e6a2f1/curl_cffi-0.13.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:434cadbe8df2f08b2fc2c16dff2779fb40b984af99c06aa700af898e185bb9db", size = 5686337, upload-time = "2025-08-06T13:05:28.985Z" },
126
+ { url = "https://files.pythonhosted.org/packages/2c/1c/cdb4fb2d16a0e9de068e0e5bc02094e105ce58a687ff30b4c6f88e25a057/curl_cffi-0.13.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:59afa877a9ae09efa04646a7d068eeea48915a95d9add0a29854e7781679fcd7", size = 2994613, upload-time = "2025-08-06T13:05:31.027Z" },
127
+ { url = "https://files.pythonhosted.org/packages/04/3e/fdf617c1ec18c3038b77065d484d7517bb30f8fb8847224eb1f601a4e8bc/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d06ed389e45a7ca97b17c275dbedd3d6524560270e675c720e93a2018a766076", size = 7931353, upload-time = "2025-08-06T13:05:32.273Z" },
128
+ { url = "https://files.pythonhosted.org/packages/3d/10/6f30c05d251cf03ddc2b9fd19880f3cab8c193255e733444a2df03b18944/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4e0de45ab3b7a835c72bd53640c2347415111b43421b5c7a1a0b18deae2e541", size = 7486378, upload-time = "2025-08-06T13:05:33.672Z" },
129
+ { url = "https://files.pythonhosted.org/packages/77/81/5bdb7dd0d669a817397b2e92193559bf66c3807f5848a48ad10cf02bf6c7/curl_cffi-0.13.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb4083371bbb94e9470d782de235fb5268bf43520de020c9e5e6be8f395443f", size = 8328585, upload-time = "2025-08-06T13:05:35.28Z" },
130
+ { url = "https://files.pythonhosted.org/packages/ce/c1/df5c6b4cfad41c08442e0f727e449f4fb5a05f8aa564d1acac29062e9e8e/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:28911b526e8cd4aa0e5e38401bfe6887e8093907272f1f67ca22e6beb2933a51", size = 8739831, upload-time = "2025-08-06T13:05:37.078Z" },
131
+ { url = "https://files.pythonhosted.org/packages/1a/91/6dd1910a212f2e8eafe57877bcf97748eb24849e1511a266687546066b8a/curl_cffi-0.13.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6d433ffcb455ab01dd0d7bde47109083aa38b59863aa183d29c668ae4c96bf8e", size = 8711908, upload-time = "2025-08-06T13:05:38.741Z" },
132
+ { url = "https://files.pythonhosted.org/packages/6d/e4/15a253f9b4bf8d008c31e176c162d2704a7e0c5e24d35942f759df107b68/curl_cffi-0.13.0-cp39-abi3-win_amd64.whl", hash = "sha256:66a6b75ce971de9af64f1b6812e275f60b88880577bac47ef1fa19694fa21cd3", size = 1614510, upload-time = "2025-08-06T13:05:40.451Z" },
133
+ { url = "https://files.pythonhosted.org/packages/f9/0f/9c5275f17ad6ff5be70edb8e0120fdc184a658c9577ca426d4230f654beb/curl_cffi-0.13.0-cp39-abi3-win_arm64.whl", hash = "sha256:d438a3b45244e874794bc4081dc1e356d2bb926dcc7021e5a8fef2e2105ef1d8", size = 1365753, upload-time = "2025-08-06T13:05:41.879Z" },
134
+ ]
135
+
136
+ [[package]]
137
+ name = "cursorweb2api"
138
+ version = "0.1.0"
139
+ source = { virtual = "." }
140
+ dependencies = [
141
+ { name = "curl-cffi" },
142
+ { name = "fastapi" },
143
+ { name = "loguru" },
144
+ { name = "sse-starlette" },
145
+ { name = "uvicorn" },
146
+ ]
147
+
148
+ [package.metadata]
149
+ requires-dist = [
150
+ { name = "curl-cffi", specifier = ">=0.13.0" },
151
+ { name = "fastapi", specifier = ">=0.117.1" },
152
+ { name = "loguru", specifier = ">=0.7.3" },
153
+ { name = "sse-starlette", specifier = ">=3.0.2" },
154
+ { name = "uvicorn", specifier = ">=0.37.0" },
155
+ ]
156
+
157
+ [[package]]
158
+ name = "fastapi"
159
+ version = "0.117.1"
160
+ source = { registry = "https://pypi.org/simple" }
161
+ dependencies = [
162
+ { name = "pydantic" },
163
+ { name = "starlette" },
164
+ { name = "typing-extensions" },
165
+ ]
166
+ sdist = { url = "https://files.pythonhosted.org/packages/7e/7e/d9788300deaf416178f61fb3c2ceb16b7d0dc9f82a08fdb87a5e64ee3cc7/fastapi-0.117.1.tar.gz", hash = "sha256:fb2d42082d22b185f904ca0ecad2e195b851030bd6c5e4c032d1c981240c631a", size = 307155, upload-time = "2025-09-20T20:16:56.663Z" }
167
+ wheels = [
168
+ { url = "https://files.pythonhosted.org/packages/6d/45/d9d3e8eeefbe93be1c50060a9d9a9f366dba66f288bb518a9566a23a8631/fastapi-0.117.1-py3-none-any.whl", hash = "sha256:33c51a0d21cab2b9722d4e56dbb9316f3687155be6b276191790d8da03507552", size = 95959, upload-time = "2025-09-20T20:16:53.661Z" },
169
+ ]
170
+
171
+ [[package]]
172
+ name = "h11"
173
+ version = "0.16.0"
174
+ source = { registry = "https://pypi.org/simple" }
175
+ sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
176
+ wheels = [
177
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
178
+ ]
179
+
180
+ [[package]]
181
+ name = "idna"
182
+ version = "3.10"
183
+ source = { registry = "https://pypi.org/simple" }
184
+ sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
185
+ wheels = [
186
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
187
+ ]
188
+
189
+ [[package]]
190
+ name = "loguru"
191
+ version = "0.7.3"
192
+ source = { registry = "https://pypi.org/simple" }
193
+ dependencies = [
194
+ { name = "colorama", marker = "sys_platform == 'win32'" },
195
+ { name = "win32-setctime", marker = "sys_platform == 'win32'" },
196
+ ]
197
+ sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
198
+ wheels = [
199
+ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
200
+ ]
201
+
202
+ [[package]]
203
+ name = "pycparser"
204
+ version = "2.23"
205
+ source = { registry = "https://pypi.org/simple" }
206
+ sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
207
+ wheels = [
208
+ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
209
+ ]
210
+
211
+ [[package]]
212
+ name = "pydantic"
213
+ version = "2.11.9"
214
+ source = { registry = "https://pypi.org/simple" }
215
+ dependencies = [
216
+ { name = "annotated-types" },
217
+ { name = "pydantic-core" },
218
+ { name = "typing-extensions" },
219
+ { name = "typing-inspection" },
220
+ ]
221
+ sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" }
222
+ wheels = [
223
+ { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" },
224
+ ]
225
+
226
+ [[package]]
227
+ name = "pydantic-core"
228
+ version = "2.33.2"
229
+ source = { registry = "https://pypi.org/simple" }
230
+ dependencies = [
231
+ { name = "typing-extensions" },
232
+ ]
233
+ sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
234
+ wheels = [
235
+ { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" },
236
+ { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" },
237
+ { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" },
238
+ { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" },
239
+ { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" },
240
+ { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" },
241
+ { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" },
242
+ { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" },
243
+ { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" },
244
+ { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" },
245
+ { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" },
246
+ { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" },
247
+ { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" },
248
+ { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" },
249
+ { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
250
+ { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
251
+ { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
252
+ { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
253
+ { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
254
+ { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
255
+ { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
256
+ { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
257
+ { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
258
+ { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
259
+ { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
260
+ { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
261
+ { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
262
+ { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
263
+ { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
264
+ { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
265
+ { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
266
+ ]
267
+
268
+ [[package]]
269
+ name = "sniffio"
270
+ version = "1.3.1"
271
+ source = { registry = "https://pypi.org/simple" }
272
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
273
+ wheels = [
274
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
275
+ ]
276
+
277
+ [[package]]
278
+ name = "sse-starlette"
279
+ version = "3.0.2"
280
+ source = { registry = "https://pypi.org/simple" }
281
+ dependencies = [
282
+ { name = "anyio" },
283
+ ]
284
+ sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" }
285
+ wheels = [
286
+ { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" },
287
+ ]
288
+
289
+ [[package]]
290
+ name = "starlette"
291
+ version = "0.48.0"
292
+ source = { registry = "https://pypi.org/simple" }
293
+ dependencies = [
294
+ { name = "anyio" },
295
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
296
+ ]
297
+ sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" }
298
+ wheels = [
299
+ { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
300
+ ]
301
+
302
+ [[package]]
303
+ name = "typing-extensions"
304
+ version = "4.15.0"
305
+ source = { registry = "https://pypi.org/simple" }
306
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
307
+ wheels = [
308
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
309
+ ]
310
+
311
+ [[package]]
312
+ name = "typing-inspection"
313
+ version = "0.4.1"
314
+ source = { registry = "https://pypi.org/simple" }
315
+ dependencies = [
316
+ { name = "typing-extensions" },
317
+ ]
318
+ sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
319
+ wheels = [
320
+ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
321
+ ]
322
+
323
+ [[package]]
324
+ name = "uvicorn"
325
+ version = "0.37.0"
326
+ source = { registry = "https://pypi.org/simple" }
327
+ dependencies = [
328
+ { name = "click" },
329
+ { name = "h11" },
330
+ ]
331
+ sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" }
332
+ wheels = [
333
+ { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" },
334
+ ]
335
+
336
+ [[package]]
337
+ name = "win32-setctime"
338
+ version = "1.2.0"
339
+ source = { registry = "https://pypi.org/simple" }
340
+ sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
341
+ wheels = [
342
+ { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
343
+ ]