baohuixiao commited on
Commit
8551878
·
verified ·
1 Parent(s): 8366419

Upload 115 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +12 -0
  3. LICENSE +21 -0
  4. README.md +216 -11
  5. __pycache__/config.cpython-312.pyc +0 -0
  6. config.py +52 -0
  7. docker-compose.yml +15 -0
  8. main.py +178 -0
  9. openai_compat.py +31 -0
  10. pool_maintenance.py +516 -0
  11. pool_service.py +546 -0
  12. proto/attachment.proto +57 -0
  13. proto/citations.proto +20 -0
  14. proto/debug.proto +12 -0
  15. proto/file_content.proto +18 -0
  16. proto/input_context.proto +64 -0
  17. proto/options.proto +12 -0
  18. proto/request.proto +173 -0
  19. proto/response.proto +159 -0
  20. proto/suggestions.proto +22 -0
  21. proto/task.proto +503 -0
  22. proto/todo.proto +23 -0
  23. protobuf2openai/__init__.py +3 -0
  24. protobuf2openai/__pycache__/__init__.cpython-312.pyc +0 -0
  25. protobuf2openai/__pycache__/__init__.cpython-38.pyc +0 -0
  26. protobuf2openai/__pycache__/app.cpython-312.pyc +0 -0
  27. protobuf2openai/__pycache__/app.cpython-38.pyc +0 -0
  28. protobuf2openai/__pycache__/bridge.cpython-312.pyc +0 -0
  29. protobuf2openai/__pycache__/bridge.cpython-38.pyc +0 -0
  30. protobuf2openai/__pycache__/config.cpython-312.pyc +0 -0
  31. protobuf2openai/__pycache__/config.cpython-38.pyc +0 -0
  32. protobuf2openai/__pycache__/helpers.cpython-312.pyc +0 -0
  33. protobuf2openai/__pycache__/helpers.cpython-38.pyc +0 -0
  34. protobuf2openai/__pycache__/logging.cpython-312.pyc +0 -0
  35. protobuf2openai/__pycache__/logging.cpython-38.pyc +0 -0
  36. protobuf2openai/__pycache__/models.cpython-312.pyc +0 -0
  37. protobuf2openai/__pycache__/models.cpython-38.pyc +0 -0
  38. protobuf2openai/__pycache__/packets.cpython-312.pyc +0 -0
  39. protobuf2openai/__pycache__/packets.cpython-38.pyc +0 -0
  40. protobuf2openai/__pycache__/reorder.cpython-312.pyc +0 -0
  41. protobuf2openai/__pycache__/reorder.cpython-38.pyc +0 -0
  42. protobuf2openai/__pycache__/router.cpython-312.pyc +0 -0
  43. protobuf2openai/__pycache__/router.cpython-38.pyc +0 -0
  44. protobuf2openai/__pycache__/sse_transform.cpython-312.pyc +0 -0
  45. protobuf2openai/__pycache__/sse_transform.cpython-38.pyc +0 -0
  46. protobuf2openai/__pycache__/state.cpython-312.pyc +0 -0
  47. protobuf2openai/__pycache__/state.cpython-38.pyc +0 -0
  48. protobuf2openai/app.py +59 -0
  49. protobuf2openai/bridge.py +210 -0
  50. 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
- title: Warp2api
3
- emoji: 🔥
4
- colorFrom: green
5
- colorTo: yellow
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"))