- .dockerignore +3 -0
- .github/workflows/docker-image.yml +54 -0
- .gitignore +3 -0
- LICENSE +21 -0
- README.md +95 -10
- app/config.py +37 -0
- app/errors.py +35 -0
- app/models.py +100 -0
- app/utils.py +504 -0
- docker-compose.yml +10 -0
- jscode/env.js +0 -0
- jscode/main.js +128 -0
- main.py +459 -0
- pyproject.toml +12 -0
- 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
]
|