Spaces:
Sleeping
Sleeping
Upload 115 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- Dockerfile +12 -0
- LICENSE +21 -0
- README.md +216 -11
- __pycache__/config.cpython-312.pyc +0 -0
- config.py +52 -0
- docker-compose.yml +15 -0
- main.py +178 -0
- openai_compat.py +31 -0
- pool_maintenance.py +516 -0
- pool_service.py +546 -0
- proto/attachment.proto +57 -0
- proto/citations.proto +20 -0
- proto/debug.proto +12 -0
- proto/file_content.proto +18 -0
- proto/input_context.proto +64 -0
- proto/options.proto +12 -0
- proto/request.proto +173 -0
- proto/response.proto +159 -0
- proto/suggestions.proto +22 -0
- proto/task.proto +503 -0
- proto/todo.proto +23 -0
- protobuf2openai/__init__.py +3 -0
- protobuf2openai/__pycache__/__init__.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/__init__.cpython-38.pyc +0 -0
- protobuf2openai/__pycache__/app.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/app.cpython-38.pyc +0 -0
- protobuf2openai/__pycache__/bridge.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/bridge.cpython-38.pyc +0 -0
- protobuf2openai/__pycache__/config.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/config.cpython-38.pyc +0 -0
- protobuf2openai/__pycache__/helpers.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/helpers.cpython-38.pyc +0 -0
- protobuf2openai/__pycache__/logging.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/logging.cpython-38.pyc +0 -0
- protobuf2openai/__pycache__/models.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/models.cpython-38.pyc +0 -0
- protobuf2openai/__pycache__/packets.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/packets.cpython-38.pyc +0 -0
- protobuf2openai/__pycache__/reorder.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/reorder.cpython-38.pyc +0 -0
- protobuf2openai/__pycache__/router.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/router.cpython-38.pyc +0 -0
- protobuf2openai/__pycache__/sse_transform.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/sse_transform.cpython-38.pyc +0 -0
- protobuf2openai/__pycache__/state.cpython-312.pyc +0 -0
- protobuf2openai/__pycache__/state.cpython-38.pyc +0 -0
- protobuf2openai/app.py +59 -0
- protobuf2openai/bridge.py +210 -0
- protobuf2openai/config.py +14 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
warp_accounts.db filter=lfs diff=lfs merge=lfs -text
|
Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt ./
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
EXPOSE 7777 8019 8000
|
| 11 |
+
|
| 12 |
+
CMD ["python", "main.py", "all"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024
|
| 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,11 +1,216 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Warp AI 代理服务与账号池系统
|
| 2 |
+
|
| 3 |
+
这是一个功能完备的Warp AI API代理服务,它不仅提供了与OpenAI Chat Completions API的兼容性,还集成了一套全自动的账号注册、维护和分配系统。项目的设计目标是提供一个稳定、高效且易于管理的Warp AI接口。
|
| 4 |
+
|
| 5 |
+
该项目的设计思路和部分实现得益于以下优秀项目:
|
| 6 |
+
- **Protobuf协议逆向基础**: [libaxuan/Warp2Api](https://github.com/libaxuan/Warp2Api)
|
| 7 |
+
- **账号池与注册机思路**: [dundunduan/warp2api](https://github.com/dundunduan/warp2api)
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 🚀 核心特性
|
| 12 |
+
|
| 13 |
+
- **OpenAI API 兼容**: 完全兼容 OpenAI Chat Completions API 格式,可无缝对接现有生态。
|
| 14 |
+
- **全自动账号池**:
|
| 15 |
+
- **自动注册**: 通过Outlook API自动购买邮箱并注册Warp账号。
|
| 16 |
+
- **自动维护**: 定期检查账号状态,自动刷新即将过期的Token。
|
| 17 |
+
- **智能分配**: 通过独立的API服务,安全、高效地分配和回收账号。
|
| 18 |
+
- **统一启动与管理**: 使用`main.py`一键启动所有服务,也支持为调试目的单独启动某个服务。
|
| 19 |
+
- **中心化配置**: 所有配置项(端口、API密钥、数据库路径等)均在`config.py`中统一管理,清晰明了。
|
| 20 |
+
- **高性能架构**:
|
| 21 |
+
- **Protobuf 通信**: 底层与Warp服务通过高效的Protobuf协议进行通信。
|
| 22 |
+
- **多进程模型**: 每个核心服务(API、账号池、维护等)都运行在独立的进程中,互不干扰。
|
| 23 |
+
- **流式响应 (Streaming)**: 完全支持OpenAI的SSE流式响应格式。
|
| 24 |
+
- **WebSocket 监控**: 内置WebSocket端点,用于实时监控Protobuf通信数据包。
|
| 25 |
+
|
| 26 |
+
## 📁 项目结构
|
| 27 |
+
|
| 28 |
+
项目采用扁平化结构,核心服务均在主目录下,方便理解和修改。
|
| 29 |
+
|
| 30 |
+
```
|
| 31 |
+
/
|
| 32 |
+
├── main.py # 🚀 统一服务启动器
|
| 33 |
+
├── config.py # ⚙️ 全局配置文件
|
| 34 |
+
│
|
| 35 |
+
├── server.py # 🔌 Protobuf 核心服务 (端口: 8000)
|
| 36 |
+
├── openai_compat.py # 🤖 OpenAI 兼容API服务 (端口: 8010)
|
| 37 |
+
│
|
| 38 |
+
├── pool_service.py # 💧 账号池API服务 (端口: 8019)
|
| 39 |
+
├── pool_maintenance.py # 🛠️ 账号池维护与Token刷新服务
|
| 40 |
+
├── warp_register.py # 📧 Warp 账号自动注册服务
|
| 41 |
+
│
|
| 42 |
+
├── warp_accounts.db # 🗃️ 存储Warp账号的SQLite数据库
|
| 43 |
+
├── requirements.txt # 🐍 Python 依赖
|
| 44 |
+
└── README.md # 📄 项目文档
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
## 🛠️ 安装与配置
|
| 48 |
+
|
| 49 |
+
### 1. 克隆仓库
|
| 50 |
+
|
| 51 |
+
```bash
|
| 52 |
+
git clone <your-repository-url>
|
| 53 |
+
cd <repository-name>
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### 2. 安装依赖
|
| 57 |
+
|
| 58 |
+
推荐使用 `uv` 或 `pip` 安装 `requirements.txt` 中的依赖。
|
| 59 |
+
|
| 60 |
+
```bash
|
| 61 |
+
# 使用 uv (推荐)
|
| 62 |
+
uv pip install -r requirements.txt
|
| 63 |
+
|
| 64 |
+
# 或者使用 pip
|
| 65 |
+
pip install -r requirements.txt
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### 3. 配置 `config.py`
|
| 69 |
+
|
| 70 |
+
这是最关键的一步。打开 [`config.py`](config.py) 文件并填写必要的配置信息。
|
| 71 |
+
|
| 72 |
+
**必须配置的选项:**
|
| 73 |
+
|
| 74 |
+
- `OUTLOOK_BASE_URL`: 你的Outlook邮箱API购买地址的基础URL。
|
| 75 |
+
- `OUTLOOK_API_CONFIG`:
|
| 76 |
+
- `app_id`: 你的Outlook API App ID。
|
| 77 |
+
- `app_key`: 你的Outlook API App Key。
|
| 78 |
+
|
| 79 |
+
**可选配置(通常保持默认即可):**
|
| 80 |
+
|
| 81 |
+
- 各个服务的端口号(`SERVER_PORT`, `OPENAI_COMPAT_PORT`, `POOL_SERVICE_PORT`)。
|
| 82 |
+
- 代理地址 `PROXY_URL`。
|
| 83 |
+
- 账号池大小 `MIN_POOL_SIZE`, `MAX_POOL_SIZE`。
|
| 84 |
+
- 目标注册账号数 `TARGET_ACCOUNTS`。
|
| 85 |
+
|
| 86 |
+
## 🎯 使用方法
|
| 87 |
+
|
| 88 |
+
我们提供了统一的启动脚本 [`main.py`](main.py),极大简化了服务的管理和调试。
|
| 89 |
+
|
| 90 |
+
### 一键启动所有服务(推荐)
|
| 91 |
+
|
| 92 |
+
在终端中运行以下命令,即可启动全部五个核心服务:
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
python main.py all
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
脚本会为每个服务创建一个独立的进程,并打印出每个服务的启动信息和进程ID。你可以通过 `Ctrl+C` 来优雅地关闭所有服务。
|
| 99 |
+
|
| 100 |
+
### 单独启动服务(用于调试)
|
| 101 |
+
|
| 102 |
+
如果你想单独调试某个服务,可以使用 `main.py` 启动它。这对于问题排查非常有用。
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
# 仅启动 Protobuf 主服务
|
| 106 |
+
python main.py server
|
| 107 |
+
|
| 108 |
+
# 仅启动 OpenAI 兼容API
|
| 109 |
+
python main.py openai
|
| 110 |
+
|
| 111 |
+
# 仅启动账号池API服务
|
| 112 |
+
python main.py pool_service
|
| 113 |
+
|
| 114 |
+
# 仅启动账号池维护脚本
|
| 115 |
+
python main.py pool_maintenance
|
| 116 |
+
|
| 117 |
+
# 仅启动账号注册服务
|
| 118 |
+
python main.py register
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
## 📝 API 使用
|
| 122 |
+
|
| 123 |
+
服务启动后,你可以通过两个主要的API端点与系统交互。
|
| 124 |
+
|
| 125 |
+
### 1. OpenAI 兼容 API (`http://127.0.0.1:8010`)
|
| 126 |
+
|
| 127 |
+
你可以使用任何支持OpenAI API的客户端来访问此接口。
|
| 128 |
+
|
| 129 |
+
- **Base URL**: `http://127.0.0.1:8010/v1`
|
| 130 |
+
- **API Key**: **无���提供**。你可以填写任意字符串(例如 "dummy"),服务器不会进行验证。
|
| 131 |
+
|
| 132 |
+
#### Python 示例
|
| 133 |
+
|
| 134 |
+
```python
|
| 135 |
+
import openai
|
| 136 |
+
|
| 137 |
+
client = openai.OpenAI(
|
| 138 |
+
base_url="http://127.0.0.1:8010/v1",
|
| 139 |
+
api_key="not-needed"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
response = client.chat.completions.create(
|
| 143 |
+
model="gemini-2.5-pro", # 或者其他Warp支持的模型
|
| 144 |
+
messages=[
|
| 145 |
+
{"role": "user", "content": "你好,请介绍一下你自己"}
|
| 146 |
+
],
|
| 147 |
+
stream=True
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
for chunk in response:
|
| 151 |
+
if chunk.choices[0].delta.content:
|
| 152 |
+
print(chunk.choices[0].delta.content, end="")
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
#### cURL 示例
|
| 156 |
+
|
| 157 |
+
```bash
|
| 158 |
+
curl -X POST http://127.0.0.1:8010/v1/chat/completions \
|
| 159 |
+
-H "Content-Type: application/json" \
|
| 160 |
+
-d '{
|
| 161 |
+
"model": "claude-4-sonnet",
|
| 162 |
+
"messages": [
|
| 163 |
+
{"role": "user", "content": "解释量子计算的基本原理"}
|
| 164 |
+
],
|
| 165 |
+
"stream": true
|
| 166 |
+
}'
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
### 2. 账号池服务 API (`http://0.0.0.0:8019`)
|
| 170 |
+
|
| 171 |
+
你可以直接与账号池服务交互来监控其状态。
|
| 172 |
+
|
| 173 |
+
#### 查看账号池状态
|
| 174 |
+
|
| 175 |
+
```bash
|
| 176 |
+
curl http://localhost:8019/api/status | jq
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
这将返回一个JSON对象,包含总账号数、可用账号数、锁定账号数等信息。
|
| 180 |
+
|
| 181 |
+
#### 健康检查
|
| 182 |
+
|
| 183 |
+
```bash
|
| 184 |
+
curl http://localhost:8019/api/health
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
## 🏗️ 架构说明
|
| 188 |
+
|
| 189 |
+
系统由五个协同工作的独立服务进程组成:
|
| 190 |
+
|
| 191 |
+
1. **账号注册服务 (`warp_register.py`)**: 作为一个生产者,它不断地通过Outlook API获取新邮箱,并自动完成Warp账号的注册流程,然后将成功的账号存入`warp_accounts.db`数据库。
|
| 192 |
+
|
| 193 |
+
2. **账号池维护服务 (`pool_maintenance.py`)**: 这是一个后台守护进程,定期扫描数据库中的所有账号,检查其Token的有效性。当Token即将过期时,它会自动执行刷新操作,确保账号池中的账号始终保持可用状态。
|
| 194 |
+
|
| 195 |
+
3. **账号池API服务 (`pool_service.py`)**: 这是一个面向内部的API服务,负责管理对数据库中账号的访问。当其他服务需要一个Warp账号时,会向它请求。它会从池中分配一个当前未被使用的账号,并将其标记为“锁定”状态,以防止并发冲突。使用完毕后,账号会被释放回池中。
|
| 196 |
+
|
| 197 |
+
4. **Protobuf主服务 (`server.py`)**: 这是与Warp官方服务器直接通信的核心桥梁。它接收内部请求,使用Protobuf协议对数据进行编码,然后发送给Warp。同样,它也负责解码从Warp返回的Protobuf数据。
|
| 198 |
+
|
| 199 |
+
5. **OpenAI兼容API服务 (`openai_compat.py`)**: 这是暴露给最终用户的服务。它接收一个标准格式的OpenAI API请求,然后向**账号池API服务**申请一个可用的Warp账号。获取到账号凭证后,它将请求转发给**Protobuf主服务**进行处理,最终将Warp的响应转换成OpenAI格式返回给用户。
|
| 200 |
+
|
| 201 |
+
这个多进程、微服务化的架构确保了各个模块职责单一、高内聚、低耦合,提高了系统的健壮性和可维护性。
|
| 202 |
+
|
| 203 |
+
## 🐛 故障排查
|
| 204 |
+
|
| 205 |
+
- **服务无法启动**:
|
| 206 |
+
- 检查`config.py`中的端口是否被其他程序占用。
|
| 207 |
+
- 查看终端日志,了解详细的错误信息。
|
| 208 |
+
- **账号注册失败**:
|
| 209 |
+
- 确保`config.py`中的Outlook API信息 (`app_id`, `app_key`, `base_url`) 正确无误且账户有余额。
|
| 210 |
+
- 检查`PROXY_URL`是否可用,注册过程依赖代理。
|
| 211 |
+
- **账号池为空**:
|
| 212 |
+
- 首次启动时,请耐心等待`warp_register.py`服务完成第一批账号的注册。
|
| 213 |
+
- 查看`warp_register.py`进程的日志,确认注册流程是否正常。
|
| 214 |
+
- **API请求失败**:
|
| 215 |
+
- 确保`all`服务都已正常启动。
|
| 216 |
+
- 检查`openai_compat.py`和`server.py`的日志,定位请求失败的具体环节。
|
__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (1.06 kB). View file
|
|
|
config.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
统一配置管理
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
# ==================== 临时邮箱API配置 ====================
|
| 8 |
+
# 临时邮箱服务的基础URL
|
| 9 |
+
TEMP_MAIL_BASE_URL = "https://mail.chatgpt.org.uk/api"
|
| 10 |
+
|
| 11 |
+
# ==================== 代理配置 ====================
|
| 12 |
+
# HTTP代理,用于常规请求
|
| 13 |
+
# PROXY_URL = "http://127.0.0.1:7890"
|
| 14 |
+
PROXY_URL = ""
|
| 15 |
+
|
| 16 |
+
# ==================== 账号池维护 (pool_maintenance.py) ====================
|
| 17 |
+
MIN_POOL_SIZE = 5 # 最小账号池大小
|
| 18 |
+
MAX_POOL_SIZE = 50 # 最大账号池大小
|
| 19 |
+
TOKEN_REFRESH_HOURS = 1 # Token刷新间隔(小时)
|
| 20 |
+
MAINTENANCE_CHECK_INTERVAL = 60 # 维护检查间隔(秒)
|
| 21 |
+
|
| 22 |
+
# ==================== 数据库配置 ====================
|
| 23 |
+
DATABASE_PATH = "warp_accounts.db"
|
| 24 |
+
DB_TIMEOUT = 10.0 # 数据库操作超时时间(秒)
|
| 25 |
+
|
| 26 |
+
# ==================== Firebase API 配置 ====================
|
| 27 |
+
FIREBASE_API_KEY = "AIzaSyBdy3O3S9hrdayLJxJ7mriBR4qgUaUygAs"
|
| 28 |
+
FIREBASE_API_KEYS = [
|
| 29 |
+
FIREBASE_API_KEY
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
# ==================== 账号池服务 (pool_service.py) ====================
|
| 33 |
+
POOL_SERVICE_HOST = "0.0.0.0"
|
| 34 |
+
POOL_SERVICE_PORT = 8019
|
| 35 |
+
MAX_SESSION_DURATION = 30 * 60 # 会话最大持续时间(30分钟)
|
| 36 |
+
|
| 37 |
+
# ==================== 账号注册 (warp_register.py) ====================
|
| 38 |
+
TARGET_ACCOUNTS = 200 # 目标账号数
|
| 39 |
+
MAX_CONCURRENT_REGISTER = 2 # 最大并发注册数
|
| 40 |
+
MAX_PROXY_RETRIES = 5 # 代理重试次数
|
| 41 |
+
|
| 42 |
+
# ==================== OpenAI兼容服务 (openai_compat.py) ====================
|
| 43 |
+
OPENAI_COMPAT_HOST = "0.0.0.0"
|
| 44 |
+
OPENAI_COMPAT_PORT = 7777
|
| 45 |
+
|
| 46 |
+
# ==================== Protobuf主服务 (server.py) ====================
|
| 47 |
+
SERVER_HOST = "0.0.0.0"
|
| 48 |
+
SERVER_PORT = 8000
|
| 49 |
+
|
| 50 |
+
# ==================== 日志配置 ====================
|
| 51 |
+
LOG_LEVEL = "INFO"
|
| 52 |
+
LOG_FORMAT = '%(asctime)s - %(levelname)s - [%(processName)s] - %(message)s'
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
warp2api:
|
| 3 |
+
build: .
|
| 4 |
+
container_name: warp2api
|
| 5 |
+
restart: unless-stopped
|
| 6 |
+
environment:
|
| 7 |
+
- PYTHONUNBUFFERED=1
|
| 8 |
+
ports:
|
| 9 |
+
- "7777:7777"
|
| 10 |
+
- "8777:8019"
|
| 11 |
+
- "8778:8000"
|
| 12 |
+
volumes:
|
| 13 |
+
- ./config.py:/app/config.py
|
| 14 |
+
- ./warp_accounts.db:/app/warp_accounts.db
|
| 15 |
+
command: ["python", "main.py", "all"]
|
main.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Warp 服务统一启动器
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import multiprocessing
|
| 8 |
+
import time
|
| 9 |
+
import sys
|
| 10 |
+
import os
|
| 11 |
+
import importlib
|
| 12 |
+
import logging
|
| 13 |
+
import asyncio
|
| 14 |
+
|
| 15 |
+
# 在导入项目模块之前,确保项目根目录在sys.path中
|
| 16 |
+
# 这有助于解决在不同环境下模块导入失败的问题
|
| 17 |
+
project_root = os.path.dirname(os.path.abspath(__file__))
|
| 18 |
+
if project_root not in sys.path:
|
| 19 |
+
sys.path.insert(0, project_root)
|
| 20 |
+
|
| 21 |
+
import config
|
| 22 |
+
|
| 23 |
+
# 配置日志
|
| 24 |
+
logging.basicConfig(
|
| 25 |
+
level=config.LOG_LEVEL,
|
| 26 |
+
format=config.LOG_FORMAT
|
| 27 |
+
)
|
| 28 |
+
logger = logging.getLogger(__name__)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ==================== 服务启动函数 ====================
|
| 32 |
+
|
| 33 |
+
def run_server():
|
| 34 |
+
"""启动 Protobuf 主服务 (server.py)"""
|
| 35 |
+
logger.info("正在启动 Protobuf 主服务...")
|
| 36 |
+
try:
|
| 37 |
+
# 动态导入并执行main函数
|
| 38 |
+
module = importlib.import_module("server")
|
| 39 |
+
module.main()
|
| 40 |
+
except Exception as e:
|
| 41 |
+
logger.error(f"Protobuf 主服务启动失败: {e}", exc_info=True)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def run_openai_compat():
|
| 45 |
+
"""启动 OpenAI 兼容服务 (openai_compat.py)"""
|
| 46 |
+
logger.info("正在启动 OpenAI 兼容服务...")
|
| 47 |
+
try:
|
| 48 |
+
# openai_compat.py 使用 uvicorn.run 并且没有main函数
|
| 49 |
+
# 我们需要模拟它的 __main__ 执行块
|
| 50 |
+
module = importlib.import_module("openai_compat")
|
| 51 |
+
uvicorn = importlib.import_module("uvicorn")
|
| 52 |
+
|
| 53 |
+
# 刷新JWT
|
| 54 |
+
try:
|
| 55 |
+
from warp2protobuf.core.auth import refresh_jwt_if_needed as _refresh_jwt
|
| 56 |
+
asyncio.run(_refresh_jwt())
|
| 57 |
+
except Exception:
|
| 58 |
+
pass
|
| 59 |
+
|
| 60 |
+
uvicorn.run(
|
| 61 |
+
module.app,
|
| 62 |
+
host=config.OPENAI_COMPAT_HOST,
|
| 63 |
+
port=config.OPENAI_COMPAT_PORT,
|
| 64 |
+
log_level=config.LOG_LEVEL.lower(),
|
| 65 |
+
)
|
| 66 |
+
except Exception as e:
|
| 67 |
+
logger.error(f"OpenAI 兼容服务启动失败: {e}", exc_info=True)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def run_pool_service():
|
| 71 |
+
"""启动账号池HTTP服务 (pool_service.py)"""
|
| 72 |
+
logger.info("正在启动账号池HTTP服务...")
|
| 73 |
+
try:
|
| 74 |
+
module = importlib.import_module("pool_service")
|
| 75 |
+
asyncio.run(module.main())
|
| 76 |
+
except Exception as e:
|
| 77 |
+
logger.error(f"账号池HTTP服务启动失败: {e}", exc_info=True)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def run_pool_maintenance():
|
| 81 |
+
"""启动账号池维护脚本 (pool_maintenance.py)"""
|
| 82 |
+
logger.info("正在启动账号池维护脚本...")
|
| 83 |
+
try:
|
| 84 |
+
module = importlib.import_module("pool_maintenance")
|
| 85 |
+
# 默认以 'auto' 模式运行
|
| 86 |
+
sys.argv = [sys.argv[0], 'auto']
|
| 87 |
+
asyncio.run(module.main())
|
| 88 |
+
except Exception as e:
|
| 89 |
+
logger.error(f"账号池维护脚本启动失败: {e}", exc_info=True)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def run_warp_register():
|
| 93 |
+
"""启动Warp账号注册脚本 (warp_register.py)"""
|
| 94 |
+
logger.info("正在启动Warp账号注册脚本...")
|
| 95 |
+
try:
|
| 96 |
+
module = importlib.import_module("warp_register")
|
| 97 |
+
asyncio.run(module.main())
|
| 98 |
+
except Exception as e:
|
| 99 |
+
logger.error(f"Warp账号注册脚本启动失败: {e}", exc_info=True)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
# ==================== 进程管理 ====================
|
| 103 |
+
|
| 104 |
+
SERVICES = {
|
| 105 |
+
"server": run_server,
|
| 106 |
+
"openai": run_openai_compat,
|
| 107 |
+
"pool_service": run_pool_service,
|
| 108 |
+
"pool_maintenance": run_pool_maintenance,
|
| 109 |
+
"register": run_warp_register,
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def start_all_services():
|
| 114 |
+
"""启动所有服务"""
|
| 115 |
+
processes = []
|
| 116 |
+
for name, target_func in SERVICES.items():
|
| 117 |
+
process = multiprocessing.Process(target=target_func, name=f"Process-{name}")
|
| 118 |
+
processes.append(process)
|
| 119 |
+
process.start()
|
| 120 |
+
logger.info(f"服务 '{name}' 已在进程 {process.pid} 中启动。")
|
| 121 |
+
|
| 122 |
+
try:
|
| 123 |
+
while True:
|
| 124 |
+
time.sleep(1)
|
| 125 |
+
for process in processes:
|
| 126 |
+
if not process.is_alive():
|
| 127 |
+
logger.warning(f"进程 '{process.name}' (PID: {process.pid}) 已退出。")
|
| 128 |
+
# 可选择在这里添加重启逻辑
|
| 129 |
+
processes.remove(process)
|
| 130 |
+
|
| 131 |
+
if not processes:
|
| 132 |
+
logger.info("所有服务进程都已退出。")
|
| 133 |
+
break
|
| 134 |
+
|
| 135 |
+
except KeyboardInterrupt:
|
| 136 |
+
logger.info("接收到停止信号,正在关闭所有服务...")
|
| 137 |
+
for process in processes:
|
| 138 |
+
process.terminate()
|
| 139 |
+
process.join()
|
| 140 |
+
logger.info("所有服务已停止。")
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def print_usage():
|
| 144 |
+
"""打印使用说明"""
|
| 145 |
+
print("=" * 60)
|
| 146 |
+
print("Warp 服务统一启动器")
|
| 147 |
+
print("=" * 60)
|
| 148 |
+
print("用法:")
|
| 149 |
+
print(" python main.py [命令]")
|
| 150 |
+
print("\n可用命令:")
|
| 151 |
+
print(" all - 启动所有服务")
|
| 152 |
+
for name in SERVICES:
|
| 153 |
+
print(f" {name:<18} - 仅启动 {name} 服务 (用于调试)")
|
| 154 |
+
print("\n示例:")
|
| 155 |
+
print(" python main.py all")
|
| 156 |
+
print(" python main.py server")
|
| 157 |
+
print("=" * 60)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
# 设置多进程启动方式,这对于Windows和macOS是推荐的
|
| 162 |
+
multiprocessing.set_start_method("spawn", force=True)
|
| 163 |
+
|
| 164 |
+
if len(sys.argv) < 2:
|
| 165 |
+
print_usage()
|
| 166 |
+
sys.exit(1)
|
| 167 |
+
|
| 168 |
+
command = sys.argv[1].lower()
|
| 169 |
+
|
| 170 |
+
if command == "all":
|
| 171 |
+
start_all_services()
|
| 172 |
+
elif command in SERVICES:
|
| 173 |
+
logger.info(f"以调试模式启动单个服务: '{command}'")
|
| 174 |
+
SERVICES[command]()
|
| 175 |
+
else:
|
| 176 |
+
print(f"错误: 未知命令 '{command}'\n")
|
| 177 |
+
print_usage()
|
| 178 |
+
sys.exit(1)
|
openai_compat.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
OpenAI Chat Completions compatible server (system-prompt flavored)
|
| 5 |
+
|
| 6 |
+
Startup entrypoint that exposes the modular app implemented in protobuf2openai.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import asyncio
|
| 13 |
+
|
| 14 |
+
from protobuf2openai.app import app # FastAPI app
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
if __name__ == "__main__":
|
| 18 |
+
import uvicorn
|
| 19 |
+
import config
|
| 20 |
+
# Refresh JWT on startup before running the server
|
| 21 |
+
try:
|
| 22 |
+
from warp2protobuf.core.auth import refresh_jwt_if_needed as _refresh_jwt
|
| 23 |
+
asyncio.run(_refresh_jwt())
|
| 24 |
+
except Exception:
|
| 25 |
+
pass
|
| 26 |
+
uvicorn.run(
|
| 27 |
+
app,
|
| 28 |
+
host=os.getenv("HOST", config.OPENAI_COMPAT_HOST),
|
| 29 |
+
port=int(os.getenv("PORT", config.OPENAI_COMPAT_PORT)),
|
| 30 |
+
log_level=config.LOG_LEVEL.lower(),
|
| 31 |
+
)
|
pool_maintenance.py
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
Warp账号池维护脚本
|
| 5 |
+
管理已注册的账号,包括token刷新、状态检查等
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
import sqlite3
|
| 10 |
+
import json
|
| 11 |
+
import time
|
| 12 |
+
import base64
|
| 13 |
+
import traceback
|
| 14 |
+
|
| 15 |
+
import requests
|
| 16 |
+
import logging
|
| 17 |
+
from typing import Dict, List, Optional, Tuple, Any
|
| 18 |
+
from datetime import datetime, timedelta
|
| 19 |
+
from dataclasses import dataclass
|
| 20 |
+
|
| 21 |
+
# ==================== 配置部分 ====================
|
| 22 |
+
import config
|
| 23 |
+
|
| 24 |
+
# 日志配置
|
| 25 |
+
logging.basicConfig(
|
| 26 |
+
level=config.LOG_LEVEL,
|
| 27 |
+
format=config.LOG_FORMAT
|
| 28 |
+
)
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# ==================== 数据模型 ====================
|
| 33 |
+
@dataclass
|
| 34 |
+
class Account:
|
| 35 |
+
"""账号数据模型"""
|
| 36 |
+
id: Optional[int] = None
|
| 37 |
+
email: str = ""
|
| 38 |
+
email_password: Optional[str] = None
|
| 39 |
+
local_id: str = ""
|
| 40 |
+
id_token: str = ""
|
| 41 |
+
refresh_token: str = ""
|
| 42 |
+
status: str = "active"
|
| 43 |
+
created_at: Optional[datetime] = None
|
| 44 |
+
last_used: Optional[datetime] = None
|
| 45 |
+
last_refresh_time: Optional[datetime] = None
|
| 46 |
+
use_count: int = 0
|
| 47 |
+
proxy_info: Optional[str] = None
|
| 48 |
+
user_agent: Optional[str] = None
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
# ==================== 数据库管理 ====================
|
| 52 |
+
class DatabaseManager:
|
| 53 |
+
"""数据库管理器"""
|
| 54 |
+
|
| 55 |
+
def __init__(self, db_path=config.DATABASE_PATH):
|
| 56 |
+
self.db_path = db_path
|
| 57 |
+
|
| 58 |
+
def get_all_accounts(self, status: str = None) -> List[Account]:
|
| 59 |
+
"""获取所有账号"""
|
| 60 |
+
conn = sqlite3.connect(self.db_path)
|
| 61 |
+
conn.row_factory = sqlite3.Row
|
| 62 |
+
cursor = conn.cursor()
|
| 63 |
+
|
| 64 |
+
if status:
|
| 65 |
+
cursor.execute('SELECT * FROM accounts WHERE status = ?', (status,))
|
| 66 |
+
else:
|
| 67 |
+
cursor.execute('SELECT * FROM accounts')
|
| 68 |
+
|
| 69 |
+
rows = cursor.fetchall()
|
| 70 |
+
accounts = []
|
| 71 |
+
|
| 72 |
+
for row in rows:
|
| 73 |
+
account = Account(
|
| 74 |
+
id=row['id'],
|
| 75 |
+
email=row['email'],
|
| 76 |
+
email_password=row['email_password'],
|
| 77 |
+
local_id=row['local_id'],
|
| 78 |
+
id_token=row['id_token'],
|
| 79 |
+
refresh_token=row['refresh_token'],
|
| 80 |
+
status=row['status'],
|
| 81 |
+
created_at=datetime.fromisoformat(row['created_at']) if row['created_at'] else None,
|
| 82 |
+
last_used=datetime.fromisoformat(row['last_used']) if row['last_used'] else None,
|
| 83 |
+
last_refresh_time=datetime.fromisoformat(row['last_refresh_time']) if row[
|
| 84 |
+
'last_refresh_time'] else None,
|
| 85 |
+
use_count=row['use_count'] or 0,
|
| 86 |
+
proxy_info=row['proxy_info'],
|
| 87 |
+
user_agent=row['user_agent']
|
| 88 |
+
)
|
| 89 |
+
accounts.append(account)
|
| 90 |
+
|
| 91 |
+
conn.close()
|
| 92 |
+
return accounts
|
| 93 |
+
|
| 94 |
+
def update_account_token(self, email: str, id_token: str, refresh_token: str = None):
|
| 95 |
+
"""更新账号token"""
|
| 96 |
+
conn = sqlite3.connect(self.db_path)
|
| 97 |
+
cursor = conn.cursor()
|
| 98 |
+
|
| 99 |
+
if refresh_token:
|
| 100 |
+
cursor.execute('''
|
| 101 |
+
UPDATE accounts
|
| 102 |
+
SET id_token = ?,
|
| 103 |
+
refresh_token = ?,
|
| 104 |
+
last_refresh_time = ?
|
| 105 |
+
WHERE email = ?
|
| 106 |
+
''', (id_token, refresh_token, datetime.now(), email))
|
| 107 |
+
else:
|
| 108 |
+
cursor.execute('''
|
| 109 |
+
UPDATE accounts
|
| 110 |
+
SET id_token = ?,
|
| 111 |
+
last_refresh_time = ?
|
| 112 |
+
WHERE email = ?
|
| 113 |
+
''', (id_token, datetime.now(), email))
|
| 114 |
+
|
| 115 |
+
conn.commit()
|
| 116 |
+
conn.close()
|
| 117 |
+
logger.info(f"✅ 更新账号token: {email}")
|
| 118 |
+
|
| 119 |
+
def update_account_status(self, email: str, status: str):
|
| 120 |
+
"""更新账号状态"""
|
| 121 |
+
conn = sqlite3.connect(self.db_path)
|
| 122 |
+
cursor = conn.cursor()
|
| 123 |
+
|
| 124 |
+
cursor.execute('''
|
| 125 |
+
UPDATE accounts
|
| 126 |
+
SET status = ?
|
| 127 |
+
WHERE email = ?
|
| 128 |
+
''', (status, email))
|
| 129 |
+
|
| 130 |
+
conn.commit()
|
| 131 |
+
conn.close()
|
| 132 |
+
logger.info(f"📝 更新账号状态: {email} -> {status}")
|
| 133 |
+
|
| 134 |
+
def get_statistics(self) -> Dict[str, int]:
|
| 135 |
+
"""获取统计信息"""
|
| 136 |
+
conn = sqlite3.connect(self.db_path)
|
| 137 |
+
cursor = conn.cursor()
|
| 138 |
+
|
| 139 |
+
stats = {}
|
| 140 |
+
cursor.execute('SELECT status, COUNT(*) FROM accounts GROUP BY status')
|
| 141 |
+
for row in cursor.fetchall():
|
| 142 |
+
stats[row[0]] = row[1]
|
| 143 |
+
|
| 144 |
+
cursor.execute('SELECT COUNT(*) FROM accounts')
|
| 145 |
+
stats['total'] = cursor.fetchone()[0]
|
| 146 |
+
|
| 147 |
+
conn.close()
|
| 148 |
+
return stats
|
| 149 |
+
|
| 150 |
+
def cleanup_expired_accounts(self, days: int = 30):
|
| 151 |
+
"""清理过期账号"""
|
| 152 |
+
conn = sqlite3.connect(self.db_path)
|
| 153 |
+
cursor = conn.cursor()
|
| 154 |
+
|
| 155 |
+
# 删除30天未使用的账号
|
| 156 |
+
cutoff_date = datetime.now() - timedelta(days=days)
|
| 157 |
+
cursor.execute('''
|
| 158 |
+
DELETE
|
| 159 |
+
FROM accounts
|
| 160 |
+
WHERE status = 'expired'
|
| 161 |
+
OR (last_used IS NOT NULL AND last_used < ?)
|
| 162 |
+
''', (cutoff_date,))
|
| 163 |
+
|
| 164 |
+
deleted_count = cursor.rowcount
|
| 165 |
+
conn.commit()
|
| 166 |
+
conn.close()
|
| 167 |
+
|
| 168 |
+
if deleted_count > 0:
|
| 169 |
+
logger.info(f"🗑️ 清理了 {deleted_count} 个过期账号")
|
| 170 |
+
|
| 171 |
+
return deleted_count
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
# ==================== Token刷新服务 ====================
|
| 175 |
+
class TokenRefreshService:
|
| 176 |
+
"""Token刷新服务"""
|
| 177 |
+
|
| 178 |
+
def __init__(self, firebase_api_key: str = config.FIREBASE_API_KEY):
|
| 179 |
+
self.firebase_api_key = firebase_api_key
|
| 180 |
+
self.base_url = "https://securetoken.googleapis.com/v1/token"
|
| 181 |
+
|
| 182 |
+
def is_token_expired(self, id_token: str, buffer_minutes: int = 5) -> bool:
|
| 183 |
+
"""检查JWT token是否过期"""
|
| 184 |
+
try:
|
| 185 |
+
if not id_token:
|
| 186 |
+
return True
|
| 187 |
+
|
| 188 |
+
# 解码JWT token
|
| 189 |
+
parts = id_token.split('.')
|
| 190 |
+
if len(parts) != 3:
|
| 191 |
+
return True
|
| 192 |
+
|
| 193 |
+
# 解码payload
|
| 194 |
+
payload_part = parts[1]
|
| 195 |
+
payload_part += '=' * (4 - len(payload_part) % 4)
|
| 196 |
+
|
| 197 |
+
payload_bytes = base64.urlsafe_b64decode(payload_part)
|
| 198 |
+
payload = json.loads(payload_bytes.decode('utf-8'))
|
| 199 |
+
|
| 200 |
+
# 检查过期时间
|
| 201 |
+
exp_timestamp = payload.get('exp')
|
| 202 |
+
if not exp_timestamp:
|
| 203 |
+
return True
|
| 204 |
+
|
| 205 |
+
# 添加缓冲时间
|
| 206 |
+
current_time = time.time()
|
| 207 |
+
buffer_seconds = buffer_minutes * 60
|
| 208 |
+
|
| 209 |
+
return (exp_timestamp - current_time) <= buffer_seconds
|
| 210 |
+
|
| 211 |
+
except Exception as e:
|
| 212 |
+
logger.error(f"检查Token过期状态失败: {e}")
|
| 213 |
+
return True
|
| 214 |
+
|
| 215 |
+
def can_refresh_token(self, account: Account) -> Tuple[bool, Optional[str]]:
|
| 216 |
+
"""检查是否可以刷新token(遵守1小时限制)"""
|
| 217 |
+
if not account.last_refresh_time:
|
| 218 |
+
return True, None
|
| 219 |
+
|
| 220 |
+
# 检查时间间隔
|
| 221 |
+
time_elapsed = datetime.now() - account.last_refresh_time
|
| 222 |
+
min_interval = timedelta(hours=config.TOKEN_REFRESH_HOURS)
|
| 223 |
+
|
| 224 |
+
if time_elapsed >= min_interval:
|
| 225 |
+
return True, None
|
| 226 |
+
else:
|
| 227 |
+
remaining = min_interval - time_elapsed
|
| 228 |
+
minutes = int(remaining.total_seconds() // 60)
|
| 229 |
+
seconds = int(remaining.total_seconds() % 60)
|
| 230 |
+
return False, f"需要等待 {minutes}分{seconds}秒"
|
| 231 |
+
|
| 232 |
+
def refresh_firebase_token(self, refresh_token: str) -> Tuple[bool, Optional[str], Optional[str]]:
|
| 233 |
+
"""刷新Firebase Token"""
|
| 234 |
+
try:
|
| 235 |
+
payload = {
|
| 236 |
+
"grant_type": "refresh_token",
|
| 237 |
+
"refresh_token": refresh_token
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
url = f"{self.base_url}?key={self.firebase_api_key}"
|
| 241 |
+
|
| 242 |
+
response = requests.post(
|
| 243 |
+
url,
|
| 244 |
+
json=payload,
|
| 245 |
+
headers={"Content-Type": "application/json"},
|
| 246 |
+
timeout=30,
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
if response.ok:
|
| 250 |
+
data = response.json()
|
| 251 |
+
new_id_token = data.get('id_token')
|
| 252 |
+
if new_id_token:
|
| 253 |
+
logger.info("✅ Firebase Token刷新成功")
|
| 254 |
+
return True, new_id_token, None
|
| 255 |
+
|
| 256 |
+
return False, None, f"HTTP {response.status_code}"
|
| 257 |
+
|
| 258 |
+
except Exception as e:
|
| 259 |
+
return False, None, str(e)
|
| 260 |
+
|
| 261 |
+
async def refresh_account_if_needed(self, account: Account, db_manager: DatabaseManager) -> bool:
|
| 262 |
+
"""根据需要刷新账号token"""
|
| 263 |
+
# 检查是否过期
|
| 264 |
+
if not self.is_token_expired(account.id_token, buffer_minutes=10):
|
| 265 |
+
return True
|
| 266 |
+
|
| 267 |
+
# 检查是否可以刷新
|
| 268 |
+
can_refresh, error_msg = self.can_refresh_token(account)
|
| 269 |
+
if not can_refresh:
|
| 270 |
+
logger.warning(f"⏰ {account.email} - {error_msg}")
|
| 271 |
+
return False
|
| 272 |
+
|
| 273 |
+
# 执行刷新
|
| 274 |
+
success, new_token, error = self.refresh_firebase_token(account.refresh_token)
|
| 275 |
+
if success and new_token:
|
| 276 |
+
db_manager.update_account_token(account.email, new_token)
|
| 277 |
+
logger.info(f"✨ 刷新token成功: {account.email}")
|
| 278 |
+
return True
|
| 279 |
+
else:
|
| 280 |
+
logger.error(f"❌ 刷新token失败: {account.email} - {error}")
|
| 281 |
+
return False
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
# ==================== 账号池维护器 ====================
|
| 285 |
+
class PoolMaintainer:
|
| 286 |
+
"""账号池维护器"""
|
| 287 |
+
|
| 288 |
+
def __init__(self):
|
| 289 |
+
self.db_manager = DatabaseManager()
|
| 290 |
+
self.token_refresh_service = TokenRefreshService()
|
| 291 |
+
self.running = False
|
| 292 |
+
|
| 293 |
+
async def check_pool_health(self):
|
| 294 |
+
"""检查账号池健康状态"""
|
| 295 |
+
stats = self.db_manager.get_statistics()
|
| 296 |
+
total = stats.get('total', 0)
|
| 297 |
+
active = stats.get('active', 0)
|
| 298 |
+
expired = stats.get('expired', 0)
|
| 299 |
+
|
| 300 |
+
logger.info("=" * 50)
|
| 301 |
+
logger.info("📊 账号池状态")
|
| 302 |
+
logger.info(f"📦 总账号数: {total}")
|
| 303 |
+
logger.info(f"✅ 活跃账号: {active}")
|
| 304 |
+
logger.info(f"❌ 过期账号: {expired}")
|
| 305 |
+
|
| 306 |
+
# 健康评估
|
| 307 |
+
if active < config.MIN_POOL_SIZE:
|
| 308 |
+
logger.warning(f"⚠️ 活跃账号不足 (当前: {active}, 最小: {config.MIN_POOL_SIZE})")
|
| 309 |
+
elif active > config.MAX_POOL_SIZE:
|
| 310 |
+
logger.warning(f"⚠️ 活跃账号过多 (当前: {active}, 最大: {config.MAX_POOL_SIZE})")
|
| 311 |
+
else:
|
| 312 |
+
logger.info(f"💚 账号池健康")
|
| 313 |
+
|
| 314 |
+
logger.info("=" * 50)
|
| 315 |
+
|
| 316 |
+
return stats
|
| 317 |
+
|
| 318 |
+
async def refresh_tokens(self):
|
| 319 |
+
"""批量刷新token"""
|
| 320 |
+
logger.info("🔄 开始刷新token...")
|
| 321 |
+
|
| 322 |
+
accounts = self.db_manager.get_all_accounts(status='active')
|
| 323 |
+
refreshed = 0
|
| 324 |
+
failed = 0
|
| 325 |
+
skipped = 0
|
| 326 |
+
|
| 327 |
+
for account in accounts:
|
| 328 |
+
try:
|
| 329 |
+
if await self.token_refresh_service.refresh_account_if_needed(account, self.db_manager):
|
| 330 |
+
refreshed += 1
|
| 331 |
+
else:
|
| 332 |
+
skipped += 1
|
| 333 |
+
except Exception as e:
|
| 334 |
+
logger.error(f"刷新账号 {account.email} 失败: {e}")
|
| 335 |
+
failed += 1
|
| 336 |
+
|
| 337 |
+
logger.info(f"🔄 Token刷新完成 - 成功: {refreshed}, 跳过: {skipped}, 失败: {failed}")
|
| 338 |
+
|
| 339 |
+
async def verify_accounts(self):
|
| 340 |
+
"""验证账号可用性"""
|
| 341 |
+
logger.info("🔍 验证账号可用性...")
|
| 342 |
+
|
| 343 |
+
accounts = self.db_manager.get_all_accounts(status='active')
|
| 344 |
+
verified = 0
|
| 345 |
+
invalid = 0
|
| 346 |
+
|
| 347 |
+
for account in accounts:
|
| 348 |
+
try:
|
| 349 |
+
# 简单验证token格式
|
| 350 |
+
if account.id_token and len(account.id_token.split('.')) == 3:
|
| 351 |
+
verified += 1
|
| 352 |
+
else:
|
| 353 |
+
self.db_manager.update_account_status(account.email, 'expired')
|
| 354 |
+
invalid += 1
|
| 355 |
+
except Exception as e:
|
| 356 |
+
logger.error(f"验证账号 {account.email} 失败: {e}")
|
| 357 |
+
invalid += 1
|
| 358 |
+
|
| 359 |
+
logger.info(f"🔍 账号验证完成 - 有效: {verified}, 无效: {invalid}")
|
| 360 |
+
|
| 361 |
+
async def cleanup(self):
|
| 362 |
+
"""清理任务"""
|
| 363 |
+
logger.info("🗑️ 执行清理任务...")
|
| 364 |
+
|
| 365 |
+
# 清理过期账号
|
| 366 |
+
deleted = self.db_manager.cleanup_expired_accounts(days=30)
|
| 367 |
+
logger.info(f"🗑️ 清理完成,删除 {deleted} 个过期账号")
|
| 368 |
+
|
| 369 |
+
async def maintenance_loop(self):
|
| 370 |
+
"""维护循环"""
|
| 371 |
+
logger.info("🔧 账号池维护服务启动")
|
| 372 |
+
|
| 373 |
+
cycle = 0
|
| 374 |
+
while self.running:
|
| 375 |
+
cycle += 1
|
| 376 |
+
logger.info(f"\n🔄 第 {cycle} 个维护周期开始")
|
| 377 |
+
|
| 378 |
+
try:
|
| 379 |
+
# 1. 检查池健康状态
|
| 380 |
+
await self.check_pool_health()
|
| 381 |
+
|
| 382 |
+
# 2. 刷新即将过期的token
|
| 383 |
+
await self.refresh_tokens()
|
| 384 |
+
|
| 385 |
+
# 3. 验证账号可用性
|
| 386 |
+
await self.verify_accounts()
|
| 387 |
+
|
| 388 |
+
# 4. 每10个周期执行一次清理
|
| 389 |
+
if cycle % 10 == 0:
|
| 390 |
+
await self.cleanup()
|
| 391 |
+
|
| 392 |
+
logger.info(f"✅ 第 {cycle} 个维护周期完成")
|
| 393 |
+
|
| 394 |
+
except Exception as e:
|
| 395 |
+
logger.error(f"❌ 维护周期异常: {e}")
|
| 396 |
+
logging.error(f"详细错误: {traceback.format_exc()}")
|
| 397 |
+
|
| 398 |
+
# 等待下一个周期
|
| 399 |
+
logger.info(f"⏰ 等待 {config.MAINTENANCE_CHECK_INTERVAL} 秒后进行下一次检查...")
|
| 400 |
+
await asyncio.sleep(config.MAINTENANCE_CHECK_INTERVAL)
|
| 401 |
+
|
| 402 |
+
async def start(self):
|
| 403 |
+
"""启动维护服务"""
|
| 404 |
+
self.running = True
|
| 405 |
+
|
| 406 |
+
try:
|
| 407 |
+
await self.maintenance_loop()
|
| 408 |
+
except KeyboardInterrupt:
|
| 409 |
+
logger.info("⌨️ 收到停止信号")
|
| 410 |
+
finally:
|
| 411 |
+
self.running = False
|
| 412 |
+
logger.info("🛑 维护服务已停止")
|
| 413 |
+
|
| 414 |
+
async def manual_refresh(self, email: str = None, force: bool = False):
|
| 415 |
+
"""手动刷新指定账号或所有账号"""
|
| 416 |
+
if email:
|
| 417 |
+
accounts = [acc for acc in self.db_manager.get_all_accounts() if acc.email == email]
|
| 418 |
+
if not accounts:
|
| 419 |
+
logger.error(f"账号不存在: {email}")
|
| 420 |
+
return
|
| 421 |
+
else:
|
| 422 |
+
accounts = self.db_manager.get_all_accounts(status='active')
|
| 423 |
+
|
| 424 |
+
logger.info(f"📋 手动刷新 {len(accounts)} 个账号")
|
| 425 |
+
|
| 426 |
+
for account in accounts:
|
| 427 |
+
try:
|
| 428 |
+
if force:
|
| 429 |
+
# 强制刷新
|
| 430 |
+
success, new_token, error = self.token_refresh_service.refresh_firebase_token(account.refresh_token)
|
| 431 |
+
if success and new_token:
|
| 432 |
+
self.db_manager.update_account_token(account.email, new_token)
|
| 433 |
+
logger.info(f"✅ 强制刷新成功: {account.email}")
|
| 434 |
+
else:
|
| 435 |
+
logger.error(f"❌ 强制刷新失败: {account.email} - {error}")
|
| 436 |
+
else:
|
| 437 |
+
# 正常刷新
|
| 438 |
+
await self.token_refresh_service.refresh_account_if_needed(account, self.db_manager)
|
| 439 |
+
|
| 440 |
+
except Exception as e:
|
| 441 |
+
logger.error(f"刷新账号 {account.email} 时出错: {e}")
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
# ==================== 命令行接口 ====================
|
| 445 |
+
async def interactive_mode():
|
| 446 |
+
"""交互模式"""
|
| 447 |
+
maintainer = PoolMaintainer()
|
| 448 |
+
|
| 449 |
+
print("\n" + "=" * 60)
|
| 450 |
+
print("🎮 Warp账号池维护 - 交互模式")
|
| 451 |
+
print("=" * 60)
|
| 452 |
+
print("命令列表:")
|
| 453 |
+
print(" status - 查看账号池状态")
|
| 454 |
+
print(" refresh - 刷新所有账号token")
|
| 455 |
+
print(" verify - 验证账号可用性")
|
| 456 |
+
print(" clean - 清理过期账号")
|
| 457 |
+
print(" auto - 启动自动维护")
|
| 458 |
+
print(" exit - 退出程序")
|
| 459 |
+
print("=" * 60)
|
| 460 |
+
|
| 461 |
+
while True:
|
| 462 |
+
try:
|
| 463 |
+
cmd = input("\n> ").strip().lower()
|
| 464 |
+
|
| 465 |
+
if cmd == "status":
|
| 466 |
+
await maintainer.check_pool_health()
|
| 467 |
+
elif cmd == "refresh":
|
| 468 |
+
await maintainer.refresh_tokens()
|
| 469 |
+
elif cmd == "verify":
|
| 470 |
+
await maintainer.verify_accounts()
|
| 471 |
+
elif cmd == "clean":
|
| 472 |
+
await maintainer.cleanup()
|
| 473 |
+
elif cmd == "auto":
|
| 474 |
+
print("🔧 启动自动维护模式...")
|
| 475 |
+
await maintainer.start()
|
| 476 |
+
elif cmd == "exit":
|
| 477 |
+
print("👋 再见!")
|
| 478 |
+
break
|
| 479 |
+
else:
|
| 480 |
+
print(f"❓ 未知命令: {cmd}")
|
| 481 |
+
|
| 482 |
+
except KeyboardInterrupt:
|
| 483 |
+
print("\n👋 再见!")
|
| 484 |
+
break
|
| 485 |
+
except Exception as e:
|
| 486 |
+
print(f"❌ 错误: {e}")
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
# ==================== 主函数 ====================
|
| 490 |
+
async def main():
|
| 491 |
+
"""主函数"""
|
| 492 |
+
import sys
|
| 493 |
+
|
| 494 |
+
if len(sys.argv) > 1:
|
| 495 |
+
mode = sys.argv[1].lower()
|
| 496 |
+
|
| 497 |
+
if mode == "auto":
|
| 498 |
+
# 自动模式
|
| 499 |
+
logger.info("🔧 启动自动维护模式")
|
| 500 |
+
maintainer = PoolMaintainer()
|
| 501 |
+
await maintainer.start()
|
| 502 |
+
elif mode == "interactive":
|
| 503 |
+
# 交互模式
|
| 504 |
+
await interactive_mode()
|
| 505 |
+
else:
|
| 506 |
+
print(f"❓ 未知模式: {mode}")
|
| 507 |
+
print("用法: python pool_maintenance.py [auto|interactive]")
|
| 508 |
+
else:
|
| 509 |
+
# 默认自动模式
|
| 510 |
+
logger.info("🔧 启动自动维护模式(默认)")
|
| 511 |
+
maintainer = PoolMaintainer()
|
| 512 |
+
await maintainer.start()
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
if __name__ == "__main__":
|
| 516 |
+
asyncio.run(main())
|
pool_service.py
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# -*- coding: utf-8 -*-
|
| 3 |
+
"""
|
| 4 |
+
账号池HTTP服务
|
| 5 |
+
提供账号分配、释放、状态查询等API
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import asyncio
|
| 9 |
+
import logging
|
| 10 |
+
import time
|
| 11 |
+
import traceback
|
| 12 |
+
import uuid
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
from typing import Dict, List, Optional, Any
|
| 15 |
+
|
| 16 |
+
import aiosqlite
|
| 17 |
+
import uvicorn
|
| 18 |
+
from fastapi import FastAPI, HTTPException
|
| 19 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 20 |
+
from pydantic import BaseModel
|
| 21 |
+
|
| 22 |
+
# ==================== 配置 ====================
|
| 23 |
+
import config
|
| 24 |
+
|
| 25 |
+
# 日志配置
|
| 26 |
+
logging.basicConfig(
|
| 27 |
+
level=config.LOG_LEVEL,
|
| 28 |
+
format=config.LOG_FORMAT
|
| 29 |
+
)
|
| 30 |
+
logger = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ==================== 数据模型 ====================
|
| 34 |
+
class AllocateRequest(BaseModel):
|
| 35 |
+
count: int = 1
|
| 36 |
+
session_duration: Optional[int] = 1800 # 默认30分钟
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ReleaseRequest(BaseModel):
|
| 40 |
+
session_id: str
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class RefreshRequest(BaseModel):
|
| 44 |
+
session_id: str
|
| 45 |
+
account_email: str
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class BlockAccountRequest(BaseModel):
|
| 49 |
+
jwt_token: Optional[str] = None
|
| 50 |
+
email: Optional[str] = None
|
| 51 |
+
|
| 52 |
+
# ==================== 数据库优化器 ====================
|
| 53 |
+
class DatabaseOptimizer:
|
| 54 |
+
"""数据库性能优化器"""
|
| 55 |
+
|
| 56 |
+
@staticmethod
|
| 57 |
+
async def optimize_database(db_path: str):
|
| 58 |
+
"""优化数据库性能"""
|
| 59 |
+
try:
|
| 60 |
+
async with aiosqlite.connect(db_path) as db:
|
| 61 |
+
# 创建索引以提升查询速度
|
| 62 |
+
await db.execute("""
|
| 63 |
+
CREATE INDEX IF NOT EXISTS idx_accounts_status_email
|
| 64 |
+
ON accounts(status, email)
|
| 65 |
+
""")
|
| 66 |
+
|
| 67 |
+
await db.execute("""
|
| 68 |
+
CREATE INDEX IF NOT EXISTS idx_accounts_status_last_used
|
| 69 |
+
ON accounts(status, last_used)
|
| 70 |
+
""")
|
| 71 |
+
|
| 72 |
+
await db.execute("""
|
| 73 |
+
CREATE INDEX IF NOT EXISTS idx_accounts_email
|
| 74 |
+
ON accounts(email)
|
| 75 |
+
""")
|
| 76 |
+
|
| 77 |
+
# 优化数据库设置
|
| 78 |
+
await db.execute("PRAGMA journal_mode = WAL") # 使用WAL模式,提升并发性能
|
| 79 |
+
await db.execute("PRAGMA synchronous = NORMAL") # 平衡性能和安全性
|
| 80 |
+
await db.execute("PRAGMA cache_size = 10000") # 增加缓存大小
|
| 81 |
+
await db.execute("PRAGMA temp_store = MEMORY") # 使用内存存储临时数据
|
| 82 |
+
|
| 83 |
+
await db.commit()
|
| 84 |
+
logger.info("✅ 数据库优化完成")
|
| 85 |
+
except Exception as e:
|
| 86 |
+
logger.error(f"数据库优化失败: {e}")
|
| 87 |
+
|
| 88 |
+
# ==================== 账号池管理器 ====================
|
| 89 |
+
class AccountPoolManager:
|
| 90 |
+
"""账号池管理器"""
|
| 91 |
+
|
| 92 |
+
def __init__(self, db_path: str = config.DATABASE_PATH):
|
| 93 |
+
self.db_path = db_path
|
| 94 |
+
self.sessions: Dict[str, Dict] = {} # 会话管理
|
| 95 |
+
self.locked_accounts: Dict[str, str] = {} # email -> session_id
|
| 96 |
+
self.lock = asyncio.Lock()
|
| 97 |
+
self.account_cache: List[Dict] = [] # 账号缓存
|
| 98 |
+
self.cache_updated_at = 0
|
| 99 |
+
self.cache_ttl = 30 # 缓存有效期30秒
|
| 100 |
+
|
| 101 |
+
async def init_async(self):
|
| 102 |
+
"""异步初始化"""
|
| 103 |
+
# 优化数据库
|
| 104 |
+
await DatabaseOptimizer.optimize_database(self.db_path)
|
| 105 |
+
# 预加载账号缓存
|
| 106 |
+
await self.refresh_account_cache()
|
| 107 |
+
|
| 108 |
+
async def refresh_account_cache(self):
|
| 109 |
+
"""刷新账号缓存"""
|
| 110 |
+
try:
|
| 111 |
+
async with aiosqlite.connect(self.db_path, timeout=config.DB_TIMEOUT) as db:
|
| 112 |
+
db.row_factory = aiosqlite.Row
|
| 113 |
+
|
| 114 |
+
# 只缓存活跃账号的基本信息
|
| 115 |
+
cursor = await db.execute("""
|
| 116 |
+
SELECT email,
|
| 117 |
+
local_id,
|
| 118 |
+
id_token,
|
| 119 |
+
refresh_token,
|
| 120 |
+
client_id,
|
| 121 |
+
outlook_refresh_token,
|
| 122 |
+
proxy_info,
|
| 123 |
+
user_agent,
|
| 124 |
+
email_password,
|
| 125 |
+
last_used,
|
| 126 |
+
created_at
|
| 127 |
+
FROM accounts
|
| 128 |
+
WHERE status = 'active'
|
| 129 |
+
ORDER BY COALESCE(last_used, created_at) ASC
|
| 130 |
+
""")
|
| 131 |
+
|
| 132 |
+
rows = await cursor.fetchall()
|
| 133 |
+
self.account_cache = [dict(row) for row in rows]
|
| 134 |
+
self.cache_updated_at = time.time()
|
| 135 |
+
|
| 136 |
+
logger.info(f"账号缓存已更新: {len(self.account_cache)} 个账号")
|
| 137 |
+
except Exception as e:
|
| 138 |
+
logger.error(f"刷新账号缓存失败: {e}")
|
| 139 |
+
|
| 140 |
+
async def get_available_accounts_fast(self, count: int = 1) -> List[Dict[str, Any]]:
|
| 141 |
+
"""快速获取可用账号(使用缓存)"""
|
| 142 |
+
# 检查缓存是否需要更新
|
| 143 |
+
if time.time() - self.cache_updated_at > self.cache_ttl:
|
| 144 |
+
asyncio.create_task(self.refresh_account_cache()) # 异步更新,不阻塞当前请求
|
| 145 |
+
|
| 146 |
+
# 从缓存中找出未锁定的账号
|
| 147 |
+
available = []
|
| 148 |
+
for account in self.account_cache:
|
| 149 |
+
if account['email'] not in self.locked_accounts:
|
| 150 |
+
available.append(account)
|
| 151 |
+
if len(available) >= count:
|
| 152 |
+
break
|
| 153 |
+
|
| 154 |
+
return available
|
| 155 |
+
|
| 156 |
+
async def allocate_accounts(self, count: int = 1, session_duration: int = config.MAX_SESSION_DURATION) -> Dict[str, Any]:
|
| 157 |
+
"""分配账号(优化版)"""
|
| 158 |
+
start_time = time.time()
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
# 使用超时锁,避免无限等待
|
| 162 |
+
async with asyncio.timeout(3): # 3秒超时
|
| 163 |
+
async with self.lock:
|
| 164 |
+
logger.info(f"开始分配 {count} 个账号...")
|
| 165 |
+
|
| 166 |
+
# 快速获取可用账号
|
| 167 |
+
accounts = await self.get_available_accounts_fast(count)
|
| 168 |
+
|
| 169 |
+
if not accounts:
|
| 170 |
+
logger.warning("没有可用账号")
|
| 171 |
+
raise HTTPException(status_code=503, detail="No available accounts")
|
| 172 |
+
|
| 173 |
+
# 创建会话
|
| 174 |
+
session_id = str(uuid.uuid4())
|
| 175 |
+
session_info = {
|
| 176 |
+
'session_id': session_id,
|
| 177 |
+
'accounts': accounts,
|
| 178 |
+
'created_at': time.time(),
|
| 179 |
+
'expires_at': time.time() + session_duration,
|
| 180 |
+
'status': 'active'
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
# 锁定账号
|
| 184 |
+
for account in accounts:
|
| 185 |
+
self.locked_accounts[account['email']] = session_id
|
| 186 |
+
|
| 187 |
+
self.sessions[session_id] = session_info
|
| 188 |
+
|
| 189 |
+
# 异步更新数据库(不阻塞响应)
|
| 190 |
+
asyncio.create_task(self.update_last_used_async(accounts))
|
| 191 |
+
|
| 192 |
+
elapsed = time.time() - start_time
|
| 193 |
+
logger.info(f"✅ 分配了 {len(accounts)} 个账号,会话ID: {session_id},耗时: {elapsed:.2f}秒")
|
| 194 |
+
|
| 195 |
+
return {
|
| 196 |
+
'success': True,
|
| 197 |
+
'session_id': session_id,
|
| 198 |
+
'accounts': accounts,
|
| 199 |
+
'expires_at': session_info['expires_at']
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
except asyncio.TimeoutError:
|
| 203 |
+
logger.error("分配账号超时")
|
| 204 |
+
raise HTTPException(status_code=503, detail="Request timeout")
|
| 205 |
+
except Exception as e:
|
| 206 |
+
logger.error(f"分配账号失败: {e}")
|
| 207 |
+
raise
|
| 208 |
+
|
| 209 |
+
async def mark_account_blocked(self, jwt_token: Optional[str] = None, email: Optional[str] = None) -> Dict[str, Any]:
|
| 210 |
+
"""标记账号为已封禁"""
|
| 211 |
+
try:
|
| 212 |
+
async with aiosqlite.connect(self.db_path, timeout=config.DB_TIMEOUT) as db:
|
| 213 |
+
found_email = None
|
| 214 |
+
|
| 215 |
+
if email:
|
| 216 |
+
# 直接根据email标记
|
| 217 |
+
found_email = email
|
| 218 |
+
elif jwt_token:
|
| 219 |
+
# 根据token片段查找账号
|
| 220 |
+
# 注意:这是简化实现,实际可能需要更复杂的匹配逻辑
|
| 221 |
+
cursor = await db.execute(
|
| 222 |
+
'SELECT email, id_token FROM accounts WHERE status = "active"'
|
| 223 |
+
)
|
| 224 |
+
rows = await cursor.fetchall()
|
| 225 |
+
for row in rows:
|
| 226 |
+
# 粗略匹配token前缀(因为我们只传了前50个字符)
|
| 227 |
+
if row[1] and jwt_token in row[1][:50]:
|
| 228 |
+
found_email = row[0]
|
| 229 |
+
break
|
| 230 |
+
|
| 231 |
+
if found_email:
|
| 232 |
+
# 更新数据库状态为blocked
|
| 233 |
+
await db.execute(
|
| 234 |
+
'UPDATE accounts SET status = "blocked", last_used = ? WHERE email = ?',
|
| 235 |
+
(datetime.now().isoformat(), found_email)
|
| 236 |
+
)
|
| 237 |
+
await db.commit()
|
| 238 |
+
|
| 239 |
+
# 从缓存中移除
|
| 240 |
+
self.account_cache = [
|
| 241 |
+
acc for acc in self.account_cache
|
| 242 |
+
if acc.get('email') != found_email
|
| 243 |
+
]
|
| 244 |
+
|
| 245 |
+
# 从锁定列表中移除
|
| 246 |
+
if found_email in self.locked_accounts:
|
| 247 |
+
session_id = self.locked_accounts[found_email]
|
| 248 |
+
del self.locked_accounts[found_email]
|
| 249 |
+
|
| 250 |
+
# 更新会话信息
|
| 251 |
+
if session_id in self.sessions:
|
| 252 |
+
self.sessions[session_id]['accounts'] = [
|
| 253 |
+
acc for acc in self.sessions[session_id]['accounts']
|
| 254 |
+
if acc.get('email') != found_email
|
| 255 |
+
]
|
| 256 |
+
|
| 257 |
+
logger.warning(f"⛔ 账号已标记为封禁: {found_email}")
|
| 258 |
+
|
| 259 |
+
return {
|
| 260 |
+
'success': True,
|
| 261 |
+
'message': f'Account {found_email} marked as blocked',
|
| 262 |
+
'email': found_email
|
| 263 |
+
}
|
| 264 |
+
else:
|
| 265 |
+
return {
|
| 266 |
+
'success': False,
|
| 267 |
+
'message': 'Account not found'
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
except Exception as e:
|
| 271 |
+
logger.error(f"标记账号失败: {e}")
|
| 272 |
+
return {
|
| 273 |
+
'success': False,
|
| 274 |
+
'message': str(e)
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
async def update_last_used_async(self, accounts: List[Dict]):
|
| 278 |
+
"""异步更新账号最后使用时间(后台任务)"""
|
| 279 |
+
try:
|
| 280 |
+
async with aiosqlite.connect(self.db_path, timeout=config.DB_TIMEOUT) as db:
|
| 281 |
+
for account in accounts:
|
| 282 |
+
await db.execute(
|
| 283 |
+
'UPDATE accounts SET last_used = ?, use_count = use_count + 1 WHERE email = ?',
|
| 284 |
+
(datetime.now().isoformat(), account['email'])
|
| 285 |
+
)
|
| 286 |
+
await db.commit()
|
| 287 |
+
logger.info(f"已更新 {len(accounts)} 个账号的使用时间")
|
| 288 |
+
except Exception as e:
|
| 289 |
+
logger.error(f"更新账号使用时间失败: {e}")
|
| 290 |
+
|
| 291 |
+
async def release_session(self, session_id: str) -> Dict[str, Any]:
|
| 292 |
+
"""释放会话"""
|
| 293 |
+
try:
|
| 294 |
+
async with asyncio.timeout(2):
|
| 295 |
+
async with self.lock:
|
| 296 |
+
if session_id not in self.sessions:
|
| 297 |
+
return {
|
| 298 |
+
'success': False,
|
| 299 |
+
'message': 'Session not found'
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
session_info = self.sessions[session_id]
|
| 303 |
+
|
| 304 |
+
# 解锁账号
|
| 305 |
+
for account in session_info['accounts']:
|
| 306 |
+
if account['email'] in self.locked_accounts:
|
| 307 |
+
del self.locked_accounts[account['email']]
|
| 308 |
+
|
| 309 |
+
# 删除会话
|
| 310 |
+
del self.sessions[session_id]
|
| 311 |
+
|
| 312 |
+
logger.info(f"释放会话: {session_id}")
|
| 313 |
+
|
| 314 |
+
return {
|
| 315 |
+
'success': True,
|
| 316 |
+
'message': 'Session released'
|
| 317 |
+
}
|
| 318 |
+
except asyncio.TimeoutError:
|
| 319 |
+
return {
|
| 320 |
+
'success': False,
|
| 321 |
+
'message': 'Release timeout'
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
async def get_pool_status(self) -> Dict[str, Any]:
|
| 325 |
+
"""获取池状态(优化版)"""
|
| 326 |
+
try:
|
| 327 |
+
# 使用缓存的账号数量
|
| 328 |
+
total_active = len(self.account_cache)
|
| 329 |
+
locked_count = len(self.locked_accounts)
|
| 330 |
+
available_count = total_active - locked_count
|
| 331 |
+
|
| 332 |
+
# 异步获取过期账号数(不阻塞主查询)
|
| 333 |
+
total_expired = 0
|
| 334 |
+
try:
|
| 335 |
+
async with aiosqlite.connect(self.db_path, timeout=2) as db:
|
| 336 |
+
cursor = await db.execute('SELECT COUNT(*) FROM accounts WHERE status = "expired"')
|
| 337 |
+
total_expired = (await cursor.fetchone())[0]
|
| 338 |
+
except:
|
| 339 |
+
pass
|
| 340 |
+
|
| 341 |
+
return {
|
| 342 |
+
'total_active': total_active,
|
| 343 |
+
'total_expired': total_expired,
|
| 344 |
+
'locked': locked_count,
|
| 345 |
+
'available': available_count,
|
| 346 |
+
'active_sessions': len(self.sessions),
|
| 347 |
+
'cache_age_seconds': int(time.time() - self.cache_updated_at),
|
| 348 |
+
'sessions': [
|
| 349 |
+
{
|
| 350 |
+
'session_id': sid,
|
| 351 |
+
'account_count': len(info['accounts']),
|
| 352 |
+
'created_at': info['created_at'],
|
| 353 |
+
'expires_at': info['expires_at']
|
| 354 |
+
}
|
| 355 |
+
for sid, info in self.sessions.items()
|
| 356 |
+
]
|
| 357 |
+
}
|
| 358 |
+
except Exception as e:
|
| 359 |
+
logger.error(f"获取状态失败: {e}")
|
| 360 |
+
raise
|
| 361 |
+
|
| 362 |
+
async def cleanup_expired_sessions(self):
|
| 363 |
+
"""清理过期会话"""
|
| 364 |
+
current_time = time.time()
|
| 365 |
+
expired_sessions = []
|
| 366 |
+
|
| 367 |
+
try:
|
| 368 |
+
async with self.lock:
|
| 369 |
+
for session_id, session_info in self.sessions.items():
|
| 370 |
+
if current_time > session_info['expires_at']:
|
| 371 |
+
expired_sessions.append(session_id)
|
| 372 |
+
|
| 373 |
+
# 在锁外释放会话,避免长时间持锁
|
| 374 |
+
for session_id in expired_sessions:
|
| 375 |
+
await self.release_session(session_id)
|
| 376 |
+
logger.info(f"清理过期会话: {session_id}")
|
| 377 |
+
except Exception as e:
|
| 378 |
+
logger.error(f"清理会话失败: {e}")
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
# ==================== FastAPI应用 ====================
|
| 382 |
+
app = FastAPI(title="Warp账号池服务", version="2.0.0")
|
| 383 |
+
|
| 384 |
+
app.add_middleware(
|
| 385 |
+
CORSMiddleware,
|
| 386 |
+
allow_origins=["*"],
|
| 387 |
+
allow_credentials=True,
|
| 388 |
+
allow_methods=["*"],
|
| 389 |
+
allow_headers=["*"],
|
| 390 |
+
)
|
| 391 |
+
|
| 392 |
+
# 全局管理器实例
|
| 393 |
+
pool_manager = None
|
| 394 |
+
|
| 395 |
+
|
| 396 |
+
@app.on_event("startup")
|
| 397 |
+
async def startup_event():
|
| 398 |
+
"""启动事件"""
|
| 399 |
+
global pool_manager
|
| 400 |
+
|
| 401 |
+
logger.info("账号池服务启动中...")
|
| 402 |
+
|
| 403 |
+
# 初始化管理器
|
| 404 |
+
pool_manager = AccountPoolManager()
|
| 405 |
+
await pool_manager.init_async()
|
| 406 |
+
|
| 407 |
+
logger.info("账号池服务已启动")
|
| 408 |
+
|
| 409 |
+
# 启动定期任务
|
| 410 |
+
async def periodic_tasks():
|
| 411 |
+
while True:
|
| 412 |
+
await asyncio.sleep(60) # 每分钟执行一次
|
| 413 |
+
try:
|
| 414 |
+
# 清理过期会话
|
| 415 |
+
await pool_manager.cleanup_expired_sessions()
|
| 416 |
+
# 刷新缓存
|
| 417 |
+
await pool_manager.refresh_account_cache()
|
| 418 |
+
except Exception as e:
|
| 419 |
+
logger.error(f"定期任务执行失败: {e}")
|
| 420 |
+
|
| 421 |
+
asyncio.create_task(periodic_tasks())
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
@app.get("/")
|
| 425 |
+
async def root():
|
| 426 |
+
"""根路径"""
|
| 427 |
+
return {
|
| 428 |
+
"service": "Warp Account Pool",
|
| 429 |
+
"version": "2.0.0",
|
| 430 |
+
"status": "running",
|
| 431 |
+
"optimized": True
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
@app.post("/api/accounts/allocate")
|
| 436 |
+
async def allocate_accounts(request: AllocateRequest):
|
| 437 |
+
"""分配账号"""
|
| 438 |
+
try:
|
| 439 |
+
if not pool_manager:
|
| 440 |
+
raise HTTPException(status_code=503, detail="Service initializing")
|
| 441 |
+
|
| 442 |
+
result = await pool_manager.allocate_accounts(
|
| 443 |
+
count=request.count,
|
| 444 |
+
session_duration=request.session_duration
|
| 445 |
+
)
|
| 446 |
+
return result
|
| 447 |
+
except HTTPException:
|
| 448 |
+
raise
|
| 449 |
+
except Exception as e:
|
| 450 |
+
logger.error(f"分配账号失败: {e}\n{traceback.format_exc()}")
|
| 451 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
@app.post("/api/accounts/release")
|
| 455 |
+
async def release_accounts(request: ReleaseRequest):
|
| 456 |
+
"""释放账号"""
|
| 457 |
+
try:
|
| 458 |
+
if not pool_manager:
|
| 459 |
+
raise HTTPException(status_code=503, detail="Service initializing")
|
| 460 |
+
|
| 461 |
+
result = await pool_manager.release_session(request.session_id)
|
| 462 |
+
return result
|
| 463 |
+
except Exception as e:
|
| 464 |
+
logger.error(f"释放账号失败: {e}")
|
| 465 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
@app.post("/api/accounts/mark_blocked")
|
| 469 |
+
async def mark_account_blocked(request: BlockAccountRequest):
|
| 470 |
+
"""标记账号为已封禁"""
|
| 471 |
+
try:
|
| 472 |
+
if not pool_manager:
|
| 473 |
+
raise HTTPException(status_code=503, detail="Service initializing")
|
| 474 |
+
|
| 475 |
+
# 根据JWT token片段或email找到并标记账号
|
| 476 |
+
result = await pool_manager.mark_account_blocked(
|
| 477 |
+
jwt_token=request.jwt_token,
|
| 478 |
+
email=request.email
|
| 479 |
+
)
|
| 480 |
+
|
| 481 |
+
if not result['success']:
|
| 482 |
+
raise HTTPException(status_code=404, detail=result['message'])
|
| 483 |
+
|
| 484 |
+
return result
|
| 485 |
+
except HTTPException as e:
|
| 486 |
+
logger.error(f"标记账号失败: {e}")
|
| 487 |
+
raise
|
| 488 |
+
except Exception as e:
|
| 489 |
+
logger.error(f"标记账号失败: {e}")
|
| 490 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
@app.get("/api/status")
|
| 494 |
+
async def get_status():
|
| 495 |
+
"""获取池状态"""
|
| 496 |
+
try:
|
| 497 |
+
if not pool_manager:
|
| 498 |
+
raise HTTPException(status_code=503, detail="Service initializing")
|
| 499 |
+
|
| 500 |
+
status = await pool_manager.get_pool_status()
|
| 501 |
+
return status
|
| 502 |
+
except Exception as e:
|
| 503 |
+
logger.error(f"获取状态失败: {e}")
|
| 504 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
@app.get("/api/health")
|
| 508 |
+
async def health_check():
|
| 509 |
+
"""健康检查"""
|
| 510 |
+
return {
|
| 511 |
+
"status": "healthy",
|
| 512 |
+
"timestamp": datetime.now().isoformat(),
|
| 513 |
+
"cache_enabled": True,
|
| 514 |
+
"optimized": True
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
|
| 518 |
+
# ==================== 主函数 ====================
|
| 519 |
+
async def main():
|
| 520 |
+
"""主函数"""
|
| 521 |
+
logger.info("=" * 60)
|
| 522 |
+
logger.info("Warp账号池HTTP服务 v2.0 (优化版)")
|
| 523 |
+
logger.info(f"端口: {config.POOL_SERVICE_PORT}")
|
| 524 |
+
logger.info(f"数据库: {config.DATABASE_PATH}")
|
| 525 |
+
logger.info("=" * 60)
|
| 526 |
+
|
| 527 |
+
# 检查数据库
|
| 528 |
+
import os
|
| 529 |
+
if not os.path.exists(config.DATABASE_PATH):
|
| 530 |
+
logger.error(f"数据库文件不存在: {config.DATABASE_PATH}")
|
| 531 |
+
logger.error("请先运行注册脚本创建账号")
|
| 532 |
+
return
|
| 533 |
+
|
| 534 |
+
# 启动服务
|
| 535 |
+
uvicorn_config = uvicorn.Config(
|
| 536 |
+
app=app,
|
| 537 |
+
host=config.POOL_SERVICE_HOST,
|
| 538 |
+
port=config.POOL_SERVICE_PORT,
|
| 539 |
+
log_level=config.LOG_LEVEL.lower()
|
| 540 |
+
)
|
| 541 |
+
server = uvicorn.Server(uvicorn_config)
|
| 542 |
+
await server.serve()
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
if __name__ == "__main__":
|
| 546 |
+
asyncio.run(main())
|
proto/attachment.proto
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package warp.multi_agent.v1;
|
| 4 |
+
|
| 5 |
+
import "options.proto";
|
| 6 |
+
|
| 7 |
+
option go_package = "github.com/warp/warp-proto-apis/multi_agent/v1";
|
| 8 |
+
|
| 9 |
+
message Attachment {
|
| 10 |
+
oneof value {
|
| 11 |
+
string plain_text = 1;
|
| 12 |
+
ExecutedShellCommand executed_shell_command = 2;
|
| 13 |
+
RunningShellCommand running_shell_command = 3;
|
| 14 |
+
DriveObject drive_object = 4;
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
message ExecutedShellCommand {
|
| 19 |
+
string command = 1;
|
| 20 |
+
string output = 2;
|
| 21 |
+
int32 exit_code = 3;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
message RunningShellCommand {
|
| 25 |
+
string command = 1;
|
| 26 |
+
LongRunningShellCommandSnapshot snapshot = 2;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
message LongRunningShellCommandSnapshot {
|
| 30 |
+
string output = 1;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
message DriveObject {
|
| 34 |
+
string uid = 1;
|
| 35 |
+
|
| 36 |
+
oneof object_payload {
|
| 37 |
+
Workflow workflow = 2;
|
| 38 |
+
Notebook notebook = 3;
|
| 39 |
+
GenericStringObject generic_string_object = 4;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
message Workflow {
|
| 44 |
+
string name = 1;
|
| 45 |
+
string description = 2;
|
| 46 |
+
string command = 3;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
message Notebook {
|
| 50 |
+
string title = 1;
|
| 51 |
+
string content = 2;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
message GenericStringObject {
|
| 55 |
+
string payload = 1;
|
| 56 |
+
string object_type = 2;
|
| 57 |
+
}
|
proto/citations.proto
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package warp.multi_agent.v1;
|
| 4 |
+
|
| 5 |
+
option go_package = "github.com/warp/warp-proto-apis/multi_agent/v1";
|
| 6 |
+
|
| 7 |
+
message Citation {
|
| 8 |
+
string document_id = 1;
|
| 9 |
+
DocumentType document_type = 2;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
enum DocumentType {
|
| 13 |
+
WARP_DRIVE_WORKFLOW = 0;
|
| 14 |
+
WARP_DRIVE_NOTEBOOK = 1;
|
| 15 |
+
WARP_DRIVE_ENV_VAR = 2;
|
| 16 |
+
RULE = 3;
|
| 17 |
+
WARP_DOCUMENTATION = 4;
|
| 18 |
+
WEB_PAGE = 5;
|
| 19 |
+
UNKNOWN = 6;
|
| 20 |
+
}
|
proto/debug.proto
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package warp.multi_agent.v1;
|
| 4 |
+
|
| 5 |
+
import "task.proto";
|
| 6 |
+
|
| 7 |
+
option go_package = "github.com/warp/warp-proto-apis/multi_agent/v1";
|
| 8 |
+
|
| 9 |
+
message TaskList {
|
| 10 |
+
repeated Task tasks = 1;
|
| 11 |
+
repeated string ordered_message_ids = 2;
|
| 12 |
+
}
|
proto/file_content.proto
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package warp.multi_agent.v1;
|
| 4 |
+
|
| 5 |
+
import "options.proto";
|
| 6 |
+
|
| 7 |
+
option go_package = "github.com/warp/warp-proto-apis/multi_agent/v1";
|
| 8 |
+
|
| 9 |
+
message FileContentLineRange {
|
| 10 |
+
uint32 start = 1;
|
| 11 |
+
uint32 end = 2;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
message FileContent {
|
| 15 |
+
string file_path = 1;
|
| 16 |
+
string content = 2;
|
| 17 |
+
FileContentLineRange line_range = 3;
|
| 18 |
+
}
|
proto/input_context.proto
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package warp.multi_agent.v1;
|
| 4 |
+
|
| 5 |
+
import "google/protobuf/timestamp.proto";
|
| 6 |
+
import "file_content.proto";
|
| 7 |
+
import "attachment.proto";
|
| 8 |
+
import "options.proto";
|
| 9 |
+
|
| 10 |
+
option go_package = "github.com/warp/warp-proto-apis/multi_agent/v1";
|
| 11 |
+
|
| 12 |
+
message InputContext {
|
| 13 |
+
Directory directory = 1;
|
| 14 |
+
message Directory {
|
| 15 |
+
string pwd = 1;
|
| 16 |
+
string home = 2;
|
| 17 |
+
bool pwd_file_symbols_indexed = 3;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
OperatingSystem operating_system = 2;
|
| 21 |
+
message OperatingSystem {
|
| 22 |
+
string platform = 1;
|
| 23 |
+
string distribution = 2;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
Shell shell = 3;
|
| 27 |
+
message Shell {
|
| 28 |
+
string name = 1;
|
| 29 |
+
string version = 2;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
google.protobuf.Timestamp current_time = 4;
|
| 33 |
+
|
| 34 |
+
repeated Codebase codebases = 8;
|
| 35 |
+
message Codebase {
|
| 36 |
+
string name = 1;
|
| 37 |
+
string path = 2;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
repeated ProjectRules project_rules = 10;
|
| 41 |
+
message ProjectRules {
|
| 42 |
+
string root_path = 1;
|
| 43 |
+
repeated FileContent active_rule_files = 2;
|
| 44 |
+
repeated string additional_rule_file_paths = 3;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
repeated ExecutedShellCommand executed_shell_commands = 5 [deprecated = true];
|
| 48 |
+
|
| 49 |
+
repeated SelectedText selected_text = 6;
|
| 50 |
+
message SelectedText {
|
| 51 |
+
string text = 1;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
repeated Image images = 7;
|
| 55 |
+
message Image {
|
| 56 |
+
bytes data = 1;
|
| 57 |
+
string mime_type = 2;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
repeated File files = 9;
|
| 61 |
+
message File {
|
| 62 |
+
FileContent content = 1;
|
| 63 |
+
}
|
| 64 |
+
}
|
proto/options.proto
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package warp.multi_agent.v1;
|
| 4 |
+
|
| 5 |
+
import "google/protobuf/descriptor.proto";
|
| 6 |
+
|
| 7 |
+
option go_package = "github.com/warp/warp-proto-apis/multi_agent/v1";
|
| 8 |
+
|
| 9 |
+
extend google.protobuf.FieldOptions {
|
| 10 |
+
bool sensitive = 50000;
|
| 11 |
+
bool internal = 50001;
|
| 12 |
+
}
|
proto/request.proto
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package warp.multi_agent.v1;
|
| 4 |
+
|
| 5 |
+
import "google/protobuf/struct.proto";
|
| 6 |
+
import "input_context.proto";
|
| 7 |
+
import "attachment.proto";
|
| 8 |
+
import "options.proto";
|
| 9 |
+
import "suggestions.proto";
|
| 10 |
+
import "task.proto";
|
| 11 |
+
|
| 12 |
+
option go_package = "github.com/warp/warp-proto-apis/multi_agent/v1";
|
| 13 |
+
|
| 14 |
+
message Request {
|
| 15 |
+
TaskContext task_context = 1;
|
| 16 |
+
message TaskContext {
|
| 17 |
+
repeated Task tasks = 1;
|
| 18 |
+
string active_task_id = 2;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
Input input = 2;
|
| 22 |
+
message Input {
|
| 23 |
+
InputContext context = 1;
|
| 24 |
+
|
| 25 |
+
oneof type {
|
| 26 |
+
UserInputs user_inputs = 6;
|
| 27 |
+
QueryWithCannedResponse query_with_canned_response = 4;
|
| 28 |
+
AutoCodeDiffQuery auto_code_diff_query = 5;
|
| 29 |
+
ResumeConversation resume_conversation = 7;
|
| 30 |
+
InitProjectRules init_project_rules = 8;
|
| 31 |
+
UserQuery user_query = 2 [deprecated = true];
|
| 32 |
+
ToolCallResult tool_call_result = 3 [deprecated = true];
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
message UserQuery {
|
| 36 |
+
string query = 1;
|
| 37 |
+
map<string, Attachment> referenced_attachments = 2;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
message UserInputs {
|
| 41 |
+
repeated UserInput inputs = 1;
|
| 42 |
+
message UserInput {
|
| 43 |
+
oneof input {
|
| 44 |
+
UserQuery user_query = 1;
|
| 45 |
+
ToolCallResult tool_call_result = 2;
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
message ToolCallResult {
|
| 51 |
+
string tool_call_id = 1;
|
| 52 |
+
|
| 53 |
+
oneof result {
|
| 54 |
+
RunShellCommandResult run_shell_command = 2;
|
| 55 |
+
ReadFilesResult read_files = 3;
|
| 56 |
+
SearchCodebaseResult search_codebase = 4;
|
| 57 |
+
ApplyFileDiffsResult apply_file_diffs = 5;
|
| 58 |
+
SuggestPlanResult suggest_plan = 6;
|
| 59 |
+
SuggestCreatePlanResult suggest_create_plan = 7;
|
| 60 |
+
GrepResult grep = 8;
|
| 61 |
+
FileGlobResult file_glob = 9;
|
| 62 |
+
RefineResult refine = 10;
|
| 63 |
+
ReadMCPResourceResult read_mcp_resource = 11;
|
| 64 |
+
CallMCPToolResult call_mcp_tool = 12;
|
| 65 |
+
WriteToLongRunningShellCommandResult write_to_long_running_shell_command = 13;
|
| 66 |
+
SuggestNewConversationResult suggest_new_conversation = 14;
|
| 67 |
+
FileGlobV2Result file_glob_v2 = 15;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
message RefineResult {
|
| 71 |
+
UserQuery user_query = 1;
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
message QueryWithCannedResponse {
|
| 76 |
+
string query = 1;
|
| 77 |
+
|
| 78 |
+
oneof type {
|
| 79 |
+
Install install = 2;
|
| 80 |
+
Code code = 3;
|
| 81 |
+
Deploy deploy = 4;
|
| 82 |
+
SomethingElse something_else = 5;
|
| 83 |
+
CustomOnboardingRequest custom_onboarding_request = 6;
|
| 84 |
+
AgenticOnboardingKickoff agentic_onboarding_kickoff = 7;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
message Install {
|
| 88 |
+
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
message Code {
|
| 92 |
+
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
message Deploy {
|
| 96 |
+
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
message SomethingElse {
|
| 100 |
+
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
message CustomOnboardingRequest {
|
| 104 |
+
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
message AgenticOnboardingKickoff {
|
| 108 |
+
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
message AutoCodeDiffQuery {
|
| 113 |
+
string query = 1;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
message ResumeConversation {
|
| 117 |
+
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
message InitProjectRules {
|
| 121 |
+
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
Settings settings = 3;
|
| 126 |
+
message Settings {
|
| 127 |
+
ModelConfig model_config = 1;
|
| 128 |
+
message ModelConfig {
|
| 129 |
+
string base = 1;
|
| 130 |
+
string planning = 2;
|
| 131 |
+
string coding = 3;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
bool rules_enabled = 2;
|
| 135 |
+
bool web_context_retrieval_enabled = 3;
|
| 136 |
+
bool supports_parallel_tool_calls = 4;
|
| 137 |
+
bool use_anthropic_text_editor_tools = 5;
|
| 138 |
+
bool planning_enabled = 6;
|
| 139 |
+
bool warp_drive_context_enabled = 7;
|
| 140 |
+
bool supports_create_files = 8;
|
| 141 |
+
repeated ToolType supported_tools = 9;
|
| 142 |
+
bool supports_long_running_commands = 10;
|
| 143 |
+
bool should_preserve_file_content_in_history = 11;
|
| 144 |
+
bool supports_todos_ui = 12;
|
| 145 |
+
bool supports_linked_code_blocks = 13;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
Metadata metadata = 4;
|
| 149 |
+
message Metadata {
|
| 150 |
+
string conversation_id = 1;
|
| 151 |
+
map<string, google.protobuf.Value> logging = 2;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
Suggestions existing_suggestions = 5;
|
| 155 |
+
|
| 156 |
+
MCPContext mcp_context = 6;
|
| 157 |
+
message MCPContext {
|
| 158 |
+
repeated MCPResource resources = 1;
|
| 159 |
+
message MCPResource {
|
| 160 |
+
string uri = 1;
|
| 161 |
+
string name = 2;
|
| 162 |
+
string description = 3;
|
| 163 |
+
string mime_type = 4;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
repeated MCPTool tools = 2;
|
| 167 |
+
message MCPTool {
|
| 168 |
+
string name = 1;
|
| 169 |
+
string description = 2;
|
| 170 |
+
google.protobuf.Struct input_schema = 3;
|
| 171 |
+
}
|
| 172 |
+
}
|
| 173 |
+
}
|
proto/response.proto
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package warp.multi_agent.v1;
|
| 4 |
+
|
| 5 |
+
import "google/protobuf/field_mask.proto";
|
| 6 |
+
import "options.proto";
|
| 7 |
+
import "suggestions.proto";
|
| 8 |
+
import "task.proto";
|
| 9 |
+
|
| 10 |
+
option go_package = "github.com/warp/warp-proto-apis/multi_agent/v1";
|
| 11 |
+
|
| 12 |
+
message ResponseEvent {
|
| 13 |
+
oneof type {
|
| 14 |
+
StreamInit init = 1;
|
| 15 |
+
ClientActions client_actions = 2;
|
| 16 |
+
StreamFinished finished = 3;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
message StreamInit {
|
| 20 |
+
string conversation_id = 1;
|
| 21 |
+
string request_id = 2;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
message ClientActions {
|
| 25 |
+
repeated ClientAction actions = 1;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
message StreamFinished {
|
| 29 |
+
repeated TokenUsage token_usage = 8;
|
| 30 |
+
message TokenUsage {
|
| 31 |
+
string model_id = 1;
|
| 32 |
+
uint32 total_input = 2;
|
| 33 |
+
uint32 output = 3;
|
| 34 |
+
uint32 input_cache_read = 4;
|
| 35 |
+
uint32 input_cache_write = 5;
|
| 36 |
+
float cost_in_cents = 6;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
bool should_refresh_model_config = 9;
|
| 40 |
+
|
| 41 |
+
RequestCost request_cost = 10;
|
| 42 |
+
message RequestCost {
|
| 43 |
+
float exact = 1;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
ContextWindowInfo context_window_info = 11;
|
| 47 |
+
message ContextWindowInfo {
|
| 48 |
+
float context_window_usage = 1;
|
| 49 |
+
bool summarized = 2;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
oneof reason {
|
| 53 |
+
Other other = 1;
|
| 54 |
+
Done done = 2;
|
| 55 |
+
ReachedMaxTokenLimit max_token_limit = 3;
|
| 56 |
+
QuotaLimit quota_limit = 4;
|
| 57 |
+
ContextWindowExceeded context_window_exceeded = 5;
|
| 58 |
+
LLMUnavailable llm_unavailable = 6;
|
| 59 |
+
InternalError internal_error = 7;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
message Other {
|
| 63 |
+
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
message Done {
|
| 67 |
+
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
message ReachedMaxTokenLimit {
|
| 71 |
+
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
message QuotaLimit {
|
| 75 |
+
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
message ContextWindowExceeded {
|
| 79 |
+
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
message LLMUnavailable {
|
| 83 |
+
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
message InternalError {
|
| 87 |
+
string message = 1;
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
message ClientAction {
|
| 93 |
+
oneof action {
|
| 94 |
+
CreateTask create_task = 1;
|
| 95 |
+
UpdateTaskStatus update_task_status = 2;
|
| 96 |
+
AddMessagesToTask add_messages_to_task = 3;
|
| 97 |
+
UpdateTaskMessage update_task_message = 4;
|
| 98 |
+
AppendToMessageContent append_to_message_content = 5;
|
| 99 |
+
Suggestions show_suggestions = 6;
|
| 100 |
+
UpdateTaskSummary update_task_summary = 7;
|
| 101 |
+
UpdateTaskDescription update_task_description = 8;
|
| 102 |
+
BeginTransaction begin_transaction = 9;
|
| 103 |
+
CommitTransaction commit_transaction = 10;
|
| 104 |
+
RollbackTransaction rollback_transaction = 11;
|
| 105 |
+
StartNewConversation start_new_conversation = 12;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
message CreateTask {
|
| 109 |
+
Task task = 1;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
message UpdateTaskStatus {
|
| 113 |
+
string task_id = 1;
|
| 114 |
+
TaskStatus task_status = 2;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
message UpdateTaskDescription {
|
| 118 |
+
string task_id = 1;
|
| 119 |
+
string description = 2;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
message AddMessagesToTask {
|
| 123 |
+
string task_id = 1;
|
| 124 |
+
repeated Message messages = 2;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
message UpdateTaskMessage {
|
| 128 |
+
string task_id = 3;
|
| 129 |
+
Message message = 1;
|
| 130 |
+
google.protobuf.FieldMask mask = 2;
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
message AppendToMessageContent {
|
| 134 |
+
string task_id = 3;
|
| 135 |
+
Message message = 1;
|
| 136 |
+
google.protobuf.FieldMask mask = 2;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
message UpdateTaskSummary {
|
| 140 |
+
string task_id = 1;
|
| 141 |
+
string summary = 2;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
message BeginTransaction {
|
| 145 |
+
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
message CommitTransaction {
|
| 149 |
+
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
message RollbackTransaction {
|
| 153 |
+
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
message StartNewConversation {
|
| 157 |
+
string start_from_message_id = 1;
|
| 158 |
+
}
|
| 159 |
+
}
|
proto/suggestions.proto
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package warp.multi_agent.v1;
|
| 4 |
+
|
| 5 |
+
option go_package = "github.com/warp/warp-proto-apis/multi_agent/v1";
|
| 6 |
+
|
| 7 |
+
message Suggestions {
|
| 8 |
+
repeated SuggestedRule rules = 1;
|
| 9 |
+
repeated SuggestedAgentModeWorkflow workflows = 2;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
message SuggestedRule {
|
| 13 |
+
string name = 1;
|
| 14 |
+
string content = 2;
|
| 15 |
+
string logging_id = 3;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
message SuggestedAgentModeWorkflow {
|
| 19 |
+
string name = 1;
|
| 20 |
+
string prompt = 2;
|
| 21 |
+
string logging_id = 3;
|
| 22 |
+
}
|
proto/task.proto
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package warp.multi_agent.v1;
|
| 4 |
+
|
| 5 |
+
import "google/protobuf/empty.proto";
|
| 6 |
+
import "google/protobuf/descriptor.proto";
|
| 7 |
+
import "google/protobuf/struct.proto";
|
| 8 |
+
import "citations.proto";
|
| 9 |
+
import "input_context.proto";
|
| 10 |
+
import "attachment.proto";
|
| 11 |
+
import "file_content.proto";
|
| 12 |
+
import "options.proto";
|
| 13 |
+
import "todo.proto";
|
| 14 |
+
|
| 15 |
+
option go_package = "github.com/warp/warp-proto-apis/multi_agent/v1";
|
| 16 |
+
|
| 17 |
+
message Task {
|
| 18 |
+
string id = 1;
|
| 19 |
+
string description = 2;
|
| 20 |
+
|
| 21 |
+
Dependencies dependencies = 3;
|
| 22 |
+
message Dependencies {
|
| 23 |
+
string parent_task_id = 1;
|
| 24 |
+
repeated string sibling_dependencies = 2;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
TaskStatus status = 4;
|
| 28 |
+
repeated Message messages = 5;
|
| 29 |
+
string summary = 6;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
message TaskStatus {
|
| 33 |
+
oneof status {
|
| 34 |
+
Pending pending = 1;
|
| 35 |
+
InProgress in_progress = 2;
|
| 36 |
+
Blocked blocked = 3;
|
| 37 |
+
Succeeded succeeded = 4;
|
| 38 |
+
Failed failed = 5;
|
| 39 |
+
Aborted aborted = 6;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
message Pending {
|
| 43 |
+
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
message InProgress {
|
| 47 |
+
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
message Blocked {
|
| 51 |
+
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
message Succeeded {
|
| 55 |
+
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
message Failed {
|
| 59 |
+
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
message Aborted {
|
| 63 |
+
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
message Message {
|
| 68 |
+
string id = 1;
|
| 69 |
+
string task_id = 11;
|
| 70 |
+
string server_message_data = 7;
|
| 71 |
+
repeated Citation citations = 8;
|
| 72 |
+
|
| 73 |
+
oneof message {
|
| 74 |
+
UserQuery user_query = 2;
|
| 75 |
+
AgentOutput agent_output = 3;
|
| 76 |
+
ToolCall tool_call = 4;
|
| 77 |
+
ToolCallResult tool_call_result = 5;
|
| 78 |
+
ServerEvent server_event = 6;
|
| 79 |
+
SystemQuery system_query = 9;
|
| 80 |
+
UpdateTodos update_todos = 10;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
message UserQuery {
|
| 84 |
+
string query = 1;
|
| 85 |
+
InputContext context = 2;
|
| 86 |
+
map<string, Attachment> referenced_attachments = 3;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
message SystemQuery {
|
| 90 |
+
InputContext context = 2;
|
| 91 |
+
|
| 92 |
+
oneof type {
|
| 93 |
+
AutoCodeDiff auto_code_diff = 1;
|
| 94 |
+
ResumeConversation resume_conversation = 3;
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
message AutoCodeDiff {
|
| 99 |
+
string query = 1;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
message ResumeConversation {
|
| 103 |
+
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
message AgentOutput {
|
| 107 |
+
string text = 1;
|
| 108 |
+
string reasoning = 2;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
message ToolCall {
|
| 112 |
+
string tool_call_id = 1;
|
| 113 |
+
|
| 114 |
+
oneof tool {
|
| 115 |
+
RunShellCommand run_shell_command = 2;
|
| 116 |
+
SearchCodebase search_codebase = 3;
|
| 117 |
+
Server server = 4;
|
| 118 |
+
ReadFiles read_files = 5;
|
| 119 |
+
ApplyFileDiffs apply_file_diffs = 6;
|
| 120 |
+
SuggestPlan suggest_plan = 7;
|
| 121 |
+
SuggestCreatePlan suggest_create_plan = 8;
|
| 122 |
+
Grep grep = 9;
|
| 123 |
+
FileGlob file_glob = 10 [deprecated = true];
|
| 124 |
+
ReadMCPResource read_mcp_resource = 11;
|
| 125 |
+
CallMCPTool call_mcp_tool = 12;
|
| 126 |
+
WriteToLongRunningShellCommand write_to_long_running_shell_command = 13;
|
| 127 |
+
SuggestNewConversation suggest_new_conversation = 14;
|
| 128 |
+
FileGlobV2 file_glob_v2 = 15;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
message Server {
|
| 132 |
+
string payload = 1;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
message RunShellCommand {
|
| 136 |
+
string command = 1;
|
| 137 |
+
bool is_read_only = 2;
|
| 138 |
+
bool uses_pager = 3;
|
| 139 |
+
repeated Citation citations = 4;
|
| 140 |
+
bool is_risky = 5;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
message WriteToLongRunningShellCommand {
|
| 144 |
+
bytes input = 1;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
message SuggestNewConversation {
|
| 148 |
+
string message_id = 1;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
message ReadFiles {
|
| 152 |
+
repeated File files = 1;
|
| 153 |
+
message File {
|
| 154 |
+
string name = 1;
|
| 155 |
+
repeated FileContentLineRange line_ranges = 2;
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
message SearchCodebase {
|
| 160 |
+
string query = 1;
|
| 161 |
+
repeated string path_filters = 2;
|
| 162 |
+
string codebase_path = 3;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
message ApplyFileDiffs {
|
| 166 |
+
string summary = 1;
|
| 167 |
+
|
| 168 |
+
repeated FileDiff diffs = 2;
|
| 169 |
+
message FileDiff {
|
| 170 |
+
string file_path = 1;
|
| 171 |
+
string search = 2;
|
| 172 |
+
string replace = 3;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
repeated NewFile new_files = 3;
|
| 176 |
+
message NewFile {
|
| 177 |
+
string file_path = 1;
|
| 178 |
+
string content = 2;
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
message SuggestPlan {
|
| 183 |
+
string summary = 1;
|
| 184 |
+
repeated Task proposed_tasks = 2;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
message SuggestCreatePlan {
|
| 188 |
+
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
message Grep {
|
| 192 |
+
repeated string queries = 1;
|
| 193 |
+
string path = 2;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
message FileGlob {
|
| 197 |
+
repeated string patterns = 1;
|
| 198 |
+
string path = 2;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
message FileGlobV2 {
|
| 202 |
+
repeated string patterns = 1;
|
| 203 |
+
string search_dir = 2;
|
| 204 |
+
int32 max_matches = 3;
|
| 205 |
+
int32 max_depth = 4;
|
| 206 |
+
int32 min_depth = 5;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
message ReadMCPResource {
|
| 210 |
+
string uri = 1;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
message CallMCPTool {
|
| 214 |
+
string name = 1;
|
| 215 |
+
google.protobuf.Struct args = 2;
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
message ToolCallResult {
|
| 220 |
+
string tool_call_id = 1;
|
| 221 |
+
InputContext context = 11;
|
| 222 |
+
|
| 223 |
+
oneof result {
|
| 224 |
+
RunShellCommandResult run_shell_command = 2;
|
| 225 |
+
SearchCodebaseResult search_codebase = 3;
|
| 226 |
+
ServerResult server = 4;
|
| 227 |
+
ReadFilesResult read_files = 5;
|
| 228 |
+
ApplyFileDiffsResult apply_file_diffs = 6;
|
| 229 |
+
SuggestPlanResult suggest_plan = 7;
|
| 230 |
+
SuggestCreatePlanResult suggest_create_plan = 8;
|
| 231 |
+
GrepResult grep = 9;
|
| 232 |
+
FileGlobResult file_glob = 10 [deprecated = true];
|
| 233 |
+
RefineResult refine = 13;
|
| 234 |
+
google.protobuf.Empty cancel = 14;
|
| 235 |
+
ReadMCPResourceResult read_mcp_resource = 15;
|
| 236 |
+
CallMCPToolResult call_mcp_tool = 16;
|
| 237 |
+
WriteToLongRunningShellCommandResult write_to_long_running_shell_command = 17;
|
| 238 |
+
SuggestNewConversationResult suggest_new_conversation = 18;
|
| 239 |
+
FileGlobV2Result file_glob_v2 = 19;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
message ServerResult {
|
| 243 |
+
string serialized_result = 1;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
message RefineResult {
|
| 247 |
+
UserQuery user_query = 1;
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
message ServerEvent {
|
| 252 |
+
string payload = 1;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
message UpdateTodos {
|
| 256 |
+
oneof operation {
|
| 257 |
+
CreateTodoList create_todo_list = 1;
|
| 258 |
+
UpdatePendingTodos update_pending_todos = 2;
|
| 259 |
+
MarkTodosCompleted mark_todos_completed = 3;
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
message RunShellCommandResult {
|
| 265 |
+
string command = 3;
|
| 266 |
+
string output = 1 [deprecated = true];
|
| 267 |
+
int32 exit_code = 2 [deprecated = true];
|
| 268 |
+
|
| 269 |
+
oneof result {
|
| 270 |
+
LongRunningShellCommandSnapshot long_running_command_snapshot = 4;
|
| 271 |
+
ShellCommandFinished command_finished = 5;
|
| 272 |
+
}
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
message ReadFilesResult {
|
| 276 |
+
oneof result {
|
| 277 |
+
Success success = 1;
|
| 278 |
+
Error error = 2;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
message Success {
|
| 282 |
+
repeated FileContent files = 1;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
message Error {
|
| 286 |
+
string message = 1;
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
message SearchCodebaseResult {
|
| 291 |
+
oneof result {
|
| 292 |
+
Success success = 1;
|
| 293 |
+
Error error = 2;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
message Success {
|
| 297 |
+
repeated FileContent files = 1;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
message Error {
|
| 301 |
+
string message = 1;
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
message ApplyFileDiffsResult {
|
| 306 |
+
oneof result {
|
| 307 |
+
Success success = 1;
|
| 308 |
+
Error error = 2;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
message Success {
|
| 312 |
+
repeated FileContent updated_files = 1 [deprecated = true];
|
| 313 |
+
|
| 314 |
+
repeated UpdatedFileContent updated_files_v2 = 2;
|
| 315 |
+
message UpdatedFileContent {
|
| 316 |
+
FileContent file = 1;
|
| 317 |
+
bool was_edited_by_user = 2;
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
message Error {
|
| 322 |
+
string message = 1;
|
| 323 |
+
}
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
message SuggestCreatePlanResult {
|
| 327 |
+
bool accepted = 1;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
message SuggestPlanResult {
|
| 331 |
+
oneof result {
|
| 332 |
+
google.protobuf.Empty accepted = 1;
|
| 333 |
+
UserEditedPlan user_edited_plan = 2;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
message UserEditedPlan {
|
| 337 |
+
string plan_text = 1;
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
message GrepResult {
|
| 342 |
+
oneof result {
|
| 343 |
+
Success success = 1;
|
| 344 |
+
Error error = 2;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
message Success {
|
| 348 |
+
repeated GrepFileMatch matched_files = 1;
|
| 349 |
+
message GrepFileMatch {
|
| 350 |
+
string file_path = 1;
|
| 351 |
+
|
| 352 |
+
repeated GrepLineMatch matched_lines = 2;
|
| 353 |
+
message GrepLineMatch {
|
| 354 |
+
uint32 line_number = 1;
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
message Error {
|
| 360 |
+
string message = 1;
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
message FileGlobResult {
|
| 365 |
+
oneof result {
|
| 366 |
+
Success success = 1;
|
| 367 |
+
Error error = 2;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
message Success {
|
| 371 |
+
string matched_files = 1;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
message Error {
|
| 375 |
+
string message = 1;
|
| 376 |
+
}
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
message FileGlobV2Result {
|
| 380 |
+
oneof result {
|
| 381 |
+
Success success = 1;
|
| 382 |
+
Error error = 2;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
message Success {
|
| 386 |
+
repeated FileGlobMatch matched_files = 1;
|
| 387 |
+
message FileGlobMatch {
|
| 388 |
+
string file_path = 1;
|
| 389 |
+
}
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
message Error {
|
| 393 |
+
string message = 1;
|
| 394 |
+
}
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
message MCPResourceContent {
|
| 398 |
+
string uri = 1;
|
| 399 |
+
|
| 400 |
+
oneof content_type {
|
| 401 |
+
Text text = 2;
|
| 402 |
+
Binary binary = 3;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
message Text {
|
| 406 |
+
string content = 1;
|
| 407 |
+
string mime_type = 2;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
message Binary {
|
| 411 |
+
bytes data = 1;
|
| 412 |
+
string mime_type = 2;
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
message ReadMCPResourceResult {
|
| 417 |
+
oneof result {
|
| 418 |
+
Success success = 1;
|
| 419 |
+
Error error = 2;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
message Success {
|
| 423 |
+
repeated MCPResourceContent contents = 1;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
message Error {
|
| 427 |
+
string message = 1;
|
| 428 |
+
}
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
message WriteToLongRunningShellCommandResult {
|
| 432 |
+
oneof result {
|
| 433 |
+
LongRunningShellCommandSnapshot long_running_command_snapshot = 1;
|
| 434 |
+
ShellCommandFinished command_finished = 2;
|
| 435 |
+
}
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
message SuggestNewConversationResult {
|
| 439 |
+
oneof result {
|
| 440 |
+
Accepted accepted = 1;
|
| 441 |
+
Rejected rejected = 2;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
message Accepted {
|
| 445 |
+
string message_id = 1;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
message Rejected {
|
| 449 |
+
|
| 450 |
+
}
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
message ShellCommandFinished {
|
| 454 |
+
string output = 1;
|
| 455 |
+
int32 exit_code = 2;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
message CallMCPToolResult {
|
| 459 |
+
oneof result {
|
| 460 |
+
Success success = 1;
|
| 461 |
+
Error error = 2;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
message Success {
|
| 465 |
+
repeated Result results = 1;
|
| 466 |
+
message Result {
|
| 467 |
+
oneof result {
|
| 468 |
+
Text text = 1;
|
| 469 |
+
Image image = 2;
|
| 470 |
+
MCPResourceContent resource = 3;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
message Text {
|
| 474 |
+
string text = 1;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
message Image {
|
| 478 |
+
bytes data = 1;
|
| 479 |
+
string mime_type = 2;
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
message Error {
|
| 485 |
+
string message = 1;
|
| 486 |
+
}
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
enum ToolType {
|
| 490 |
+
RUN_SHELL_COMMAND = 0;
|
| 491 |
+
SEARCH_CODEBASE = 1;
|
| 492 |
+
READ_FILES = 2;
|
| 493 |
+
APPLY_FILE_DIFFS = 3;
|
| 494 |
+
SUGGEST_PLAN = 4;
|
| 495 |
+
SUGGEST_CREATE_PLAN = 5;
|
| 496 |
+
GREP = 6;
|
| 497 |
+
FILE_GLOB = 7;
|
| 498 |
+
READ_MCP_RESOURCE = 8;
|
| 499 |
+
CALL_MCP_TOOL = 9;
|
| 500 |
+
WRITE_TO_LONG_RUNNING_SHELL_COMMAND = 10;
|
| 501 |
+
SUGGEST_NEW_CONVERSATION = 11;
|
| 502 |
+
FILE_GLOB_V2 = 12;
|
| 503 |
+
}
|
proto/todo.proto
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
syntax = "proto3";
|
| 2 |
+
|
| 3 |
+
package warp.multi_agent.v1;
|
| 4 |
+
|
| 5 |
+
option go_package = "github.com/warp/warp-proto-apis/multi_agent/v1";
|
| 6 |
+
|
| 7 |
+
message TodoItem {
|
| 8 |
+
string id = 1;
|
| 9 |
+
string title = 2;
|
| 10 |
+
string description = 3;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
message CreateTodoList {
|
| 14 |
+
repeated TodoItem initial_todos = 1;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
message UpdatePendingTodos {
|
| 18 |
+
repeated TodoItem updated_pending_todos = 1;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
message MarkTodosCompleted {
|
| 22 |
+
repeated string todo_ids = 1;
|
| 23 |
+
}
|
protobuf2openai/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Package for converting between Warp protobuf JSON and OpenAI Chat Completions API
|
| 2 |
+
|
| 3 |
+
__all__ = []
|
protobuf2openai/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (172 Bytes). View file
|
|
|
protobuf2openai/__pycache__/__init__.cpython-38.pyc
ADDED
|
Binary file (159 Bytes). View file
|
|
|
protobuf2openai/__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (3.71 kB). View file
|
|
|
protobuf2openai/__pycache__/app.cpython-38.pyc
ADDED
|
Binary file (2.12 kB). View file
|
|
|
protobuf2openai/__pycache__/bridge.cpython-312.pyc
ADDED
|
Binary file (5.02 kB). View file
|
|
|
protobuf2openai/__pycache__/bridge.cpython-38.pyc
ADDED
|
Binary file (3.17 kB). View file
|
|
|
protobuf2openai/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (909 Bytes). View file
|
|
|
protobuf2openai/__pycache__/config.cpython-38.pyc
ADDED
|
Binary file (617 Bytes). View file
|
|
|
protobuf2openai/__pycache__/helpers.cpython-312.pyc
ADDED
|
Binary file (2.96 kB). View file
|
|
|
protobuf2openai/__pycache__/helpers.cpython-38.pyc
ADDED
|
Binary file (1.61 kB). View file
|
|
|
protobuf2openai/__pycache__/logging.cpython-312.pyc
ADDED
|
Binary file (1.56 kB). View file
|
|
|
protobuf2openai/__pycache__/logging.cpython-38.pyc
ADDED
|
Binary file (933 Bytes). View file
|
|
|
protobuf2openai/__pycache__/models.cpython-312.pyc
ADDED
|
Binary file (1.88 kB). View file
|
|
|
protobuf2openai/__pycache__/models.cpython-38.pyc
ADDED
|
Binary file (1.64 kB). View file
|
|
|
protobuf2openai/__pycache__/packets.cpython-312.pyc
ADDED
|
Binary file (6.32 kB). View file
|
|
|
protobuf2openai/__pycache__/packets.cpython-38.pyc
ADDED
|
Binary file (3.74 kB). View file
|
|
|
protobuf2openai/__pycache__/reorder.cpython-312.pyc
ADDED
|
Binary file (4.28 kB). View file
|
|
|
protobuf2openai/__pycache__/reorder.cpython-38.pyc
ADDED
|
Binary file (2.05 kB). View file
|
|
|
protobuf2openai/__pycache__/router.cpython-312.pyc
ADDED
|
Binary file (16.8 kB). View file
|
|
|
protobuf2openai/__pycache__/router.cpython-38.pyc
ADDED
|
Binary file (9.17 kB). View file
|
|
|
protobuf2openai/__pycache__/sse_transform.cpython-312.pyc
ADDED
|
Binary file (18.2 kB). View file
|
|
|
protobuf2openai/__pycache__/sse_transform.cpython-38.pyc
ADDED
|
Binary file (8.3 kB). View file
|
|
|
protobuf2openai/__pycache__/state.cpython-312.pyc
ADDED
|
Binary file (4.1 kB). View file
|
|
|
protobuf2openai/__pycache__/state.cpython-38.pyc
ADDED
|
Binary file (2.86 kB). View file
|
|
|
protobuf2openai/app.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
|
| 5 |
+
import httpx
|
| 6 |
+
from fastapi import FastAPI
|
| 7 |
+
|
| 8 |
+
from .bridge import initialize_once
|
| 9 |
+
from .config import BRIDGE_BASE_URL, WARMUP_INIT_RETRIES, WARMUP_INIT_DELAY_S
|
| 10 |
+
from .logging import logger
|
| 11 |
+
from .router import router
|
| 12 |
+
|
| 13 |
+
app = FastAPI(title="OpenAI Chat Completions - Streaming")
|
| 14 |
+
app.include_router(router)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@app.on_event("startup")
|
| 18 |
+
async def _on_startup():
|
| 19 |
+
try:
|
| 20 |
+
logger.info("[OpenAI Compat] Server starting. BRIDGE_BASE_URL=%s", BRIDGE_BASE_URL)
|
| 21 |
+
logger.info("[OpenAI Compat] Endpoints: GET /healthz, GET /v1/models, POST /v1/chat/completions")
|
| 22 |
+
except Exception:
|
| 23 |
+
pass
|
| 24 |
+
|
| 25 |
+
url = f"{BRIDGE_BASE_URL}/healthz"
|
| 26 |
+
retries = WARMUP_INIT_RETRIES
|
| 27 |
+
delay_s = WARMUP_INIT_DELAY_S
|
| 28 |
+
for attempt in range(1, retries + 1):
|
| 29 |
+
try:
|
| 30 |
+
async with httpx.AsyncClient(timeout=5.0, trust_env=True) as client:
|
| 31 |
+
resp = await client.get(url)
|
| 32 |
+
if resp.status_code == 200:
|
| 33 |
+
logger.info("[OpenAI Compat] Bridge server is ready at %s", url)
|
| 34 |
+
break
|
| 35 |
+
else:
|
| 36 |
+
logger.warning("[OpenAI Compat] Bridge health at %s -> HTTP %s", url, resp.status_code)
|
| 37 |
+
except Exception as e:
|
| 38 |
+
logger.warning("[OpenAI Compat] Bridge health attempt %s/%s failed: %s", attempt, retries, e)
|
| 39 |
+
await asyncio.sleep(delay_s)
|
| 40 |
+
else:
|
| 41 |
+
logger.error("[OpenAI Compat] Bridge server not ready at %s", url)
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
await initialize_once()
|
| 45 |
+
except Exception as e:
|
| 46 |
+
logger.warning(f"[OpenAI Compat] Warmup initialize_once on startup failed: {e}")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@app.on_event("shutdown")
|
| 50 |
+
async def _on_shutdown():
|
| 51 |
+
"""清理全局资源"""
|
| 52 |
+
try:
|
| 53 |
+
# 关闭全局 HTTP 客户端
|
| 54 |
+
from .bridge import _http_client
|
| 55 |
+
if _http_client is not None:
|
| 56 |
+
await _http_client.aclose()
|
| 57 |
+
logger.info("[OpenAI Compat] Global HTTP client closed")
|
| 58 |
+
except Exception as e:
|
| 59 |
+
logger.warning(f"[OpenAI Compat] Error during shutdown: {e}")
|
protobuf2openai/bridge.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import time
|
| 5 |
+
import uuid
|
| 6 |
+
import asyncio
|
| 7 |
+
from typing import Any, Dict, Optional
|
| 8 |
+
|
| 9 |
+
import httpx
|
| 10 |
+
from .logging import logger
|
| 11 |
+
|
| 12 |
+
from .config import (
|
| 13 |
+
BRIDGE_BASE_URL,
|
| 14 |
+
FALLBACK_BRIDGE_URLS,
|
| 15 |
+
WARMUP_INIT_RETRIES,
|
| 16 |
+
WARMUP_INIT_DELAY_S,
|
| 17 |
+
WARMUP_REQUEST_RETRIES,
|
| 18 |
+
WARMUP_REQUEST_DELAY_S,
|
| 19 |
+
)
|
| 20 |
+
from .packets import packet_template
|
| 21 |
+
from .state import GLOBAL_BASELINE, ensure_tool_ids, STATE
|
| 22 |
+
|
| 23 |
+
# 创建一个全局的、可复用的 httpx.AsyncClient 实例以提高性能
|
| 24 |
+
_http_client: Optional[httpx.AsyncClient] = None
|
| 25 |
+
_initialization_lock = asyncio.Lock()
|
| 26 |
+
_initialized = False
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def get_http_client() -> httpx.AsyncClient:
|
| 30 |
+
"""获取或创建全局 HTTP 客户端"""
|
| 31 |
+
global _http_client
|
| 32 |
+
if _http_client is None:
|
| 33 |
+
_http_client = httpx.AsyncClient(
|
| 34 |
+
timeout=httpx.Timeout(connect=5.0, read=180.0, write=10.0, pool=10.0),
|
| 35 |
+
limits=httpx.Limits(max_keepalive_connections=200, max_connections=400),
|
| 36 |
+
trust_env=True
|
| 37 |
+
)
|
| 38 |
+
return _http_client
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
async def bridge_send_stream(packet: Dict[str, Any]) -> Dict[str, Any]:
|
| 42 |
+
"""异步发送数据流到 bridge 服务"""
|
| 43 |
+
last_exc: Optional[Exception] = None
|
| 44 |
+
client = get_http_client()
|
| 45 |
+
|
| 46 |
+
for base in FALLBACK_BRIDGE_URLS:
|
| 47 |
+
url = f"{base}/api/warp/send_stream"
|
| 48 |
+
try:
|
| 49 |
+
wrapped_packet = {"json_data": packet, "message_type": "warp.multi_agent.v1.Request"}
|
| 50 |
+
|
| 51 |
+
# try:
|
| 52 |
+
# logger.info("[OpenAI Compat] Bridge request URL: %s", url)
|
| 53 |
+
# logger.info("[OpenAI Compat] Bridge request payload: %s",
|
| 54 |
+
# json.dumps(wrapped_packet, ensure_ascii=False))
|
| 55 |
+
# except Exception:
|
| 56 |
+
# logger.info("[OpenAI Compat] Bridge request payload serialization failed for URL %s", url)
|
| 57 |
+
|
| 58 |
+
# 使用全局的 httpx.AsyncClient 实例发送异步请求
|
| 59 |
+
r = await client.post(url, json=wrapped_packet)
|
| 60 |
+
|
| 61 |
+
if r.status_code == 200:
|
| 62 |
+
try:
|
| 63 |
+
logger.info("[OpenAI Compat] Bridge response (raw text): %s", r.text)
|
| 64 |
+
except Exception:
|
| 65 |
+
pass
|
| 66 |
+
return r.json()
|
| 67 |
+
else:
|
| 68 |
+
txt = r.text
|
| 69 |
+
last_exc = Exception(f"bridge_error: HTTP {r.status_code} {txt}")
|
| 70 |
+
|
| 71 |
+
except httpx.ReadTimeout:
|
| 72 |
+
# logger.warning(f"[OpenAI Compat] Request timeout for {url}, trying next fallback")
|
| 73 |
+
last_exc = Exception("Request timeout")
|
| 74 |
+
continue
|
| 75 |
+
|
| 76 |
+
except Exception as e:
|
| 77 |
+
last_exc = e
|
| 78 |
+
continue
|
| 79 |
+
|
| 80 |
+
if last_exc:
|
| 81 |
+
raise last_exc
|
| 82 |
+
raise Exception("bridge_unreachable")
|
| 83 |
+
#
|
| 84 |
+
#
|
| 85 |
+
# async def initialize_once() -> None:
|
| 86 |
+
# """异步地、线程安全地执行一次性初始化"""
|
| 87 |
+
# global _initialized
|
| 88 |
+
#
|
| 89 |
+
# # 快速检查,避免不必要的加锁开销
|
| 90 |
+
# if _initialized:
|
| 91 |
+
# return
|
| 92 |
+
#
|
| 93 |
+
# async with _initialization_lock:
|
| 94 |
+
# # 在锁内再次检查,防止并发进入
|
| 95 |
+
# if _initialized:
|
| 96 |
+
# return
|
| 97 |
+
#
|
| 98 |
+
# logger.info("[OpenAI Compat] Starting one-time initialization...")
|
| 99 |
+
#
|
| 100 |
+
# ensure_tool_ids()
|
| 101 |
+
#
|
| 102 |
+
# first_task_id = STATE.baseline_task_id or str(uuid.uuid4())
|
| 103 |
+
# STATE.baseline_task_id = first_task_id
|
| 104 |
+
#
|
| 105 |
+
# client = get_http_client()
|
| 106 |
+
# health_urls = [f"{base}/healthz" for base in FALLBACK_BRIDGE_URLS]
|
| 107 |
+
# last_err: Optional[str] = None
|
| 108 |
+
#
|
| 109 |
+
# for _ in range(WARMUP_INIT_RETRIES):
|
| 110 |
+
# try:
|
| 111 |
+
# ok = False
|
| 112 |
+
# last_err = None
|
| 113 |
+
# for h in health_urls:
|
| 114 |
+
# try:
|
| 115 |
+
# resp = await client.get(h, timeout=5.0)
|
| 116 |
+
# if resp.status_code == 200:
|
| 117 |
+
# ok = True
|
| 118 |
+
# break
|
| 119 |
+
# else:
|
| 120 |
+
# last_err = f"HTTP {resp.status_code} at {h}"
|
| 121 |
+
# except Exception as he:
|
| 122 |
+
# last_err = f"{type(he).__name__}: {he} at {h}"
|
| 123 |
+
# if ok:
|
| 124 |
+
# break
|
| 125 |
+
# except Exception as e:
|
| 126 |
+
# last_err = str(e)
|
| 127 |
+
# await asyncio.sleep(WARMUP_INIT_DELAY_S)
|
| 128 |
+
# else:
|
| 129 |
+
# # 注意:我们不再抛出异常,只是记录警告
|
| 130 |
+
# logger.warning(f"Bridge server not ready during init: {last_err}")
|
| 131 |
+
#
|
| 132 |
+
# pkt = packet_template()
|
| 133 |
+
# pkt["task_context"]["active_task_id"] = first_task_id
|
| 134 |
+
# pkt["input"]["user_inputs"]["inputs"].append({"user_query": {"query": "warmup"}})
|
| 135 |
+
#
|
| 136 |
+
# last_exc: Optional[Exception] = None
|
| 137 |
+
# for attempt in range(1, WARMUP_REQUEST_RETRIES + 1):
|
| 138 |
+
# try:
|
| 139 |
+
# resp = await bridge_send_stream(pkt)
|
| 140 |
+
# # ================ 关键修改 ================
|
| 141 |
+
# # 将结果存入真正的全局对象,而不是临时的上下文状态
|
| 142 |
+
# GLOBAL_BASELINE.conversation_id = resp.get("conversation_id") or GLOBAL_BASELINE.conversation_id
|
| 143 |
+
# ret_task_id = resp.get("task_id")
|
| 144 |
+
# if isinstance(ret_task_id, str) and ret_task_id:
|
| 145 |
+
# GLOBAL_BASELINE.baseline_task_id = ret_task_id
|
| 146 |
+
# # ==========================================
|
| 147 |
+
# break
|
| 148 |
+
# except Exception as e:
|
| 149 |
+
# last_exc = e
|
| 150 |
+
# logger.warning(f"[OpenAI Compat] Warmup attempt {attempt}/{WARMUP_REQUEST_RETRIES} failed: {e}")
|
| 151 |
+
# if attempt < WARMUP_REQUEST_RETRIES:
|
| 152 |
+
# await asyncio.sleep(WARMUP_REQUEST_DELAY_S)
|
| 153 |
+
#
|
| 154 |
+
# # 即使预热失败,我们也标记为已初始化,避免重复尝试
|
| 155 |
+
# _initialized = True
|
| 156 |
+
#
|
| 157 |
+
# if last_exc:
|
| 158 |
+
# logger.warning(f"[OpenAI Compat] Initialization completed with warnings: {last_exc}")
|
| 159 |
+
# else:
|
| 160 |
+
# logger.info("[OpenAI Compat] One-time initialization completed successfully.")
|
| 161 |
+
# logger.info(f"[OpenAI Compat] Global baseline set: conversation_id='{GLOBAL_BASELINE.conversation_id}', baseline_task_id='{GLOBAL_BASELINE.baseline_task_id}'")
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
async def initialize_once() -> None:
|
| 165 |
+
"""异步地、线程安全地执行一次性初始化"""
|
| 166 |
+
global _initialized
|
| 167 |
+
|
| 168 |
+
# 快速检查,避免不必要的加锁开销
|
| 169 |
+
if _initialized:
|
| 170 |
+
return
|
| 171 |
+
|
| 172 |
+
async with _initialization_lock:
|
| 173 |
+
# 在锁内再次检查,防止并发进入
|
| 174 |
+
if _initialized:
|
| 175 |
+
return
|
| 176 |
+
|
| 177 |
+
ensure_tool_ids()
|
| 178 |
+
|
| 179 |
+
first_task_id = STATE.baseline_task_id or str(uuid.uuid4())
|
| 180 |
+
STATE.baseline_task_id = first_task_id
|
| 181 |
+
|
| 182 |
+
client = get_http_client()
|
| 183 |
+
health_urls = [f"{base}/healthz" for base in FALLBACK_BRIDGE_URLS]
|
| 184 |
+
last_err: Optional[str] = None
|
| 185 |
+
|
| 186 |
+
for _ in range(WARMUP_INIT_RETRIES):
|
| 187 |
+
try:
|
| 188 |
+
ok = False
|
| 189 |
+
last_err = None
|
| 190 |
+
for h in health_urls:
|
| 191 |
+
try:
|
| 192 |
+
resp = await client.get(h, timeout=5.0)
|
| 193 |
+
if resp.status_code == 200:
|
| 194 |
+
ok = True
|
| 195 |
+
break
|
| 196 |
+
else:
|
| 197 |
+
last_err = f"HTTP {resp.status_code} at {h}"
|
| 198 |
+
except Exception as he:
|
| 199 |
+
last_err = f"{type(he).__name__}: {he} at {h}"
|
| 200 |
+
if ok:
|
| 201 |
+
break
|
| 202 |
+
except Exception as e:
|
| 203 |
+
last_err = str(e)
|
| 204 |
+
await asyncio.sleep(WARMUP_INIT_DELAY_S)
|
| 205 |
+
else:
|
| 206 |
+
# 注意:我们不再抛出异常,只是记录警告
|
| 207 |
+
logger.warning(f"Bridge server not ready during init: {last_err}")
|
| 208 |
+
|
| 209 |
+
# 即使预热失败,我们也标记为已初始化,避免重复尝试
|
| 210 |
+
_initialized = True
|
protobuf2openai/config.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
BRIDGE_BASE_URL = os.getenv("WARP_BRIDGE_URL", "http://127.0.0.1:8000")
|
| 6 |
+
FALLBACK_BRIDGE_URLS = [
|
| 7 |
+
BRIDGE_BASE_URL,
|
| 8 |
+
"http://127.0.0.1:8000",
|
| 9 |
+
]
|
| 10 |
+
|
| 11 |
+
WARMUP_INIT_RETRIES = int(os.getenv("WARP_COMPAT_INIT_RETRIES", "10"))
|
| 12 |
+
WARMUP_INIT_DELAY_S = float(os.getenv("WARP_COMPAT_INIT_DELAY", "0.5"))
|
| 13 |
+
WARMUP_REQUEST_RETRIES = int(os.getenv("WARP_COMPAT_WARMUP_RETRIES", "3"))
|
| 14 |
+
WARMUP_REQUEST_DELAY_S = float(os.getenv("WARP_COMPAT_WARMUP_DELAY", "1.5"))
|