yinming commited on
Commit ·
bbb1195
0
Parent(s):
feat: Antigravity API Proxy for HuggingFace Spaces
Browse files- Rust backend with Axum (multi-protocol API proxy)
- React frontend (account management UI)
- Docker multi-stage build
- Support OpenAI/Claude/Gemini protocols
- Basic Auth for admin UI (ADMIN_USER/ADMIN_PASS)
- API Key auth for proxy (API_KEYS env var)
This view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +57 -0
- README.md +136 -0
- index.html +59 -0
- package-lock.json +1886 -0
- package.json +38 -0
- postcss.config.cjs +6 -0
- server/Cargo.lock +2475 -0
- server/Cargo.toml +55 -0
- server/src/api/accounts.rs +205 -0
- server/src/api/config.rs +39 -0
- server/src/api/mod.rs +42 -0
- server/src/api/proxy.rs +90 -0
- server/src/error.rs +65 -0
- server/src/lib.rs +8 -0
- server/src/main.rs +126 -0
- server/src/models/account.rs +71 -0
- server/src/models/config.rs +31 -0
- server/src/models/mod.rs +9 -0
- server/src/models/quota.rs +46 -0
- server/src/models/token.rs +39 -0
- server/src/modules/account.rs +471 -0
- server/src/modules/config.rs +34 -0
- server/src/modules/logger.rs +57 -0
- server/src/modules/mod.rs +17 -0
- server/src/modules/oauth.rs +128 -0
- server/src/modules/quota.rs +214 -0
- server/src/proxy/common/error.rs +41 -0
- server/src/proxy/common/json_schema.rs +252 -0
- server/src/proxy/common/mod.rs +7 -0
- server/src/proxy/common/model_mapping.rs +167 -0
- server/src/proxy/common/rate_limiter.rs +51 -0
- server/src/proxy/common/utils.rs +20 -0
- server/src/proxy/config.rs +86 -0
- server/src/proxy/handlers/claude.rs +475 -0
- server/src/proxy/handlers/gemini.rs +259 -0
- server/src/proxy/handlers/mod.rs +6 -0
- server/src/proxy/handlers/openai.rs +520 -0
- server/src/proxy/mappers/claude/mod.rs +356 -0
- server/src/proxy/mappers/claude/models.rs +379 -0
- server/src/proxy/mappers/claude/request.rs +688 -0
- server/src/proxy/mappers/claude/response.rs +408 -0
- server/src/proxy/mappers/claude/streaming.rs +660 -0
- server/src/proxy/mappers/claude/utils.rs +43 -0
- server/src/proxy/mappers/common_utils.rs +301 -0
- server/src/proxy/mappers/gemini/mod.rs +8 -0
- server/src/proxy/mappers/gemini/models.rs +16 -0
- server/src/proxy/mappers/gemini/wrapper.rs +140 -0
- server/src/proxy/mappers/mod.rs +8 -0
- server/src/proxy/mappers/openai/mod.rs +12 -0
- server/src/proxy/mappers/openai/models.rs +105 -0
Dockerfile
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================
|
| 2 |
+
# Multi-stage Dockerfile for Antigravity API Proxy
|
| 3 |
+
# For deployment to HuggingFace Spaces
|
| 4 |
+
# ============================================
|
| 5 |
+
|
| 6 |
+
# Stage 1: Build Frontend
|
| 7 |
+
FROM node:20-alpine AS frontend-builder
|
| 8 |
+
WORKDIR /app
|
| 9 |
+
COPY package*.json ./
|
| 10 |
+
RUN npm ci --legacy-peer-deps
|
| 11 |
+
COPY . .
|
| 12 |
+
RUN npm run build
|
| 13 |
+
|
| 14 |
+
# Stage 2: Build Backend
|
| 15 |
+
FROM rust:1.75-slim AS backend-builder
|
| 16 |
+
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
|
| 17 |
+
WORKDIR /app
|
| 18 |
+
|
| 19 |
+
# Copy server source
|
| 20 |
+
COPY server/ ./
|
| 21 |
+
|
| 22 |
+
# Build release binary
|
| 23 |
+
RUN cargo build --release
|
| 24 |
+
|
| 25 |
+
# Stage 3: Runtime
|
| 26 |
+
FROM debian:bookworm-slim
|
| 27 |
+
|
| 28 |
+
# Install runtime dependencies
|
| 29 |
+
RUN apt-get update && apt-get install -y \
|
| 30 |
+
ca-certificates \
|
| 31 |
+
curl \
|
| 32 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 33 |
+
|
| 34 |
+
# Create non-root user (required by HuggingFace Spaces)
|
| 35 |
+
RUN useradd -m -u 1000 user
|
| 36 |
+
USER user
|
| 37 |
+
ENV HOME=/home/user
|
| 38 |
+
WORKDIR $HOME/app
|
| 39 |
+
|
| 40 |
+
# Copy backend binary
|
| 41 |
+
COPY --chown=user --from=backend-builder /app/target/release/antigravity-server ./
|
| 42 |
+
|
| 43 |
+
# Copy frontend static files
|
| 44 |
+
COPY --chown=user --from=frontend-builder /app/dist ./static
|
| 45 |
+
|
| 46 |
+
# Create data directory
|
| 47 |
+
RUN mkdir -p /home/user/app/data
|
| 48 |
+
|
| 49 |
+
# Expose port
|
| 50 |
+
EXPOSE 7860
|
| 51 |
+
|
| 52 |
+
# Health check
|
| 53 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
| 54 |
+
CMD curl -f http://localhost:7860/healthz || exit 1
|
| 55 |
+
|
| 56 |
+
# Run the server
|
| 57 |
+
CMD ["./antigravity-server"]
|
README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Antigravity API Proxy
|
| 3 |
+
emoji: "\U0001F680"
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: cc-by-nc-sa-4.0
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Antigravity API Proxy
|
| 13 |
+
|
| 14 |
+
A cloud-deployed API proxy service that converts Google AI Web sessions to standard API interfaces (OpenAI, Claude, Gemini formats).
|
| 15 |
+
|
| 16 |
+
## Features
|
| 17 |
+
|
| 18 |
+
- **Multi-Protocol Support**: OpenAI, Claude, and Gemini API formats
|
| 19 |
+
- **Token Management**: Automatic token refresh and rotation
|
| 20 |
+
- **Web UI**: Account and configuration management interface
|
| 21 |
+
- **Model Mapping**: Flexible model aliasing
|
| 22 |
+
- **API Key Authentication**: Secure access with multiple API keys
|
| 23 |
+
|
| 24 |
+
## Authentication
|
| 25 |
+
|
| 26 |
+
### Frontend Access (Web UI)
|
| 27 |
+
The web management interface is protected by HuggingFace Spaces password.
|
| 28 |
+
Set this in: **Settings → Access control → Password**
|
| 29 |
+
|
| 30 |
+
### API Proxy Authentication
|
| 31 |
+
Configure API keys in HuggingFace **Settings → Secrets**:
|
| 32 |
+
|
| 33 |
+
| Secret Name | Description |
|
| 34 |
+
|-------------|-------------|
|
| 35 |
+
| `API_KEYS` | Comma-separated list: `key1,key2,key3` |
|
| 36 |
+
| `API_KEY_1` | First API key |
|
| 37 |
+
| `API_KEY_2` | Second API key |
|
| 38 |
+
| `API_KEY` | Single API key (backward compatible) |
|
| 39 |
+
|
| 40 |
+
**Example:**
|
| 41 |
+
```
|
| 42 |
+
API_KEYS = sk-abc123,sk-def456,sk-ghi789
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
When calling the API, include your key in the request header:
|
| 46 |
+
```bash
|
| 47 |
+
# Option 1: Authorization header
|
| 48 |
+
curl -H "Authorization: Bearer sk-abc123" ...
|
| 49 |
+
|
| 50 |
+
# Option 2: X-API-Key header
|
| 51 |
+
curl -H "X-API-Key: sk-abc123" ...
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
## Endpoints
|
| 55 |
+
|
| 56 |
+
### OpenAI Compatible
|
| 57 |
+
- `POST /v1/chat/completions` - Chat completions
|
| 58 |
+
- `GET /v1/models` - List models
|
| 59 |
+
|
| 60 |
+
### Claude Compatible
|
| 61 |
+
- `POST /v1/messages` - Messages API
|
| 62 |
+
- `GET /v1/models/claude` - List Claude models
|
| 63 |
+
|
| 64 |
+
### Gemini Native
|
| 65 |
+
- `POST /v1beta/models/:model:generateContent` - Generate content
|
| 66 |
+
- `GET /v1beta/models` - List Gemini models
|
| 67 |
+
|
| 68 |
+
## Usage Examples
|
| 69 |
+
|
| 70 |
+
### OpenAI SDK (Python)
|
| 71 |
+
```python
|
| 72 |
+
from openai import OpenAI
|
| 73 |
+
|
| 74 |
+
client = OpenAI(
|
| 75 |
+
base_url="https://your-space.hf.space/v1",
|
| 76 |
+
api_key="sk-your-api-key"
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
response = client.chat.completions.create(
|
| 80 |
+
model="gpt-4",
|
| 81 |
+
messages=[{"role": "user", "content": "Hello!"}]
|
| 82 |
+
)
|
| 83 |
+
print(response.choices[0].message.content)
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### Claude SDK (Python)
|
| 87 |
+
```python
|
| 88 |
+
import anthropic
|
| 89 |
+
|
| 90 |
+
client = anthropic.Anthropic(
|
| 91 |
+
base_url="https://your-space.hf.space",
|
| 92 |
+
api_key="sk-your-api-key"
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
message = client.messages.create(
|
| 96 |
+
model="claude-3-5-sonnet-20241022",
|
| 97 |
+
max_tokens=1024,
|
| 98 |
+
messages=[{"role": "user", "content": "Hello!"}]
|
| 99 |
+
)
|
| 100 |
+
print(message.content)
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
### cURL
|
| 104 |
+
```bash
|
| 105 |
+
curl -X POST https://your-space.hf.space/v1/chat/completions \
|
| 106 |
+
-H "Authorization: Bearer sk-your-api-key" \
|
| 107 |
+
-H "Content-Type: application/json" \
|
| 108 |
+
-d '{
|
| 109 |
+
"model": "gpt-4",
|
| 110 |
+
"messages": [{"role": "user", "content": "Hello!"}]
|
| 111 |
+
}'
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
## Configuration
|
| 115 |
+
|
| 116 |
+
Access the web UI at the root URL to manage:
|
| 117 |
+
- **Accounts**: Add via Refresh Token
|
| 118 |
+
- **Model Mappings**: Map OpenAI/Claude models to Gemini
|
| 119 |
+
- **Proxy Settings**: Configure upstream proxy if needed
|
| 120 |
+
|
| 121 |
+
## Environment Variables
|
| 122 |
+
|
| 123 |
+
| Variable | Description | Required |
|
| 124 |
+
|----------|-------------|----------|
|
| 125 |
+
| `API_KEYS` | Comma-separated API keys | No* |
|
| 126 |
+
| `API_KEY_1`, `API_KEY_2`, ... | Individual API keys | No* |
|
| 127 |
+
| `PORT` | Server port (default: 7860) | No |
|
| 128 |
+
|
| 129 |
+
*If no API keys are configured, authentication is disabled.
|
| 130 |
+
|
| 131 |
+
## Security Notes
|
| 132 |
+
|
| 133 |
+
1. **Always set API keys** in production to prevent unauthorized access
|
| 134 |
+
2. **Enable Space password** to protect the management UI
|
| 135 |
+
3. Refresh tokens are stored in `/data` persistent storage
|
| 136 |
+
4. Consider the security implications of storing tokens in cloud environments
|
index.html
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en" style="background-color: #1a1f2e;">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8" />
|
| 6 |
+
<link rel="icon" type="image/png" href="/icon.png" />
|
| 7 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 8 |
+
<title>Antigravity Tools</title>
|
| 9 |
+
<script>
|
| 10 |
+
(function () {
|
| 11 |
+
try {
|
| 12 |
+
const savedTheme = localStorage.getItem('app-theme-preference');
|
| 13 |
+
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
| 14 |
+
|
| 15 |
+
// Determine if should be dark for later use, but ALWAYS start with splash color
|
| 16 |
+
const shouldBeDark = savedTheme === 'dark' || ((!savedTheme || savedTheme === 'system') && systemDark);
|
| 17 |
+
|
| 18 |
+
// Set background color IMMEDIATELY to SPLASH COLOR (seamless transition)
|
| 19 |
+
document.documentElement.style.backgroundColor = '#1a1f2e';
|
| 20 |
+
|
| 21 |
+
if (shouldBeDark) {
|
| 22 |
+
document.documentElement.classList.add('dark');
|
| 23 |
+
document.documentElement.setAttribute('data-theme', 'dark');
|
| 24 |
+
} else {
|
| 25 |
+
document.documentElement.classList.remove('dark');
|
| 26 |
+
document.documentElement.setAttribute('data-theme', 'light');
|
| 27 |
+
}
|
| 28 |
+
} catch (e) {
|
| 29 |
+
console.error('Failed to apply theme during boot:', e);
|
| 30 |
+
document.documentElement.style.backgroundColor = '#1a1f2e';
|
| 31 |
+
}
|
| 32 |
+
})();
|
| 33 |
+
</script>
|
| 34 |
+
<style>
|
| 35 |
+
/* Critical CSS: Force splash color initially */
|
| 36 |
+
html {
|
| 37 |
+
background-color: #1a1f2e !important;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* These specific overrides will apply when React loads and removes the splash screen logic,
|
| 41 |
+
but for now we want everything to look like the splash screen */
|
| 42 |
+
|
| 43 |
+
body {
|
| 44 |
+
margin: 0;
|
| 45 |
+
background-color: transparent;
|
| 46 |
+
}
|
| 47 |
+
</style>
|
| 48 |
+
</head>
|
| 49 |
+
|
| 50 |
+
<body style="margin: 0; background-color: #1a1f2e;">
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
<div id="root"></div>
|
| 56 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 57 |
+
</body>
|
| 58 |
+
|
| 59 |
+
</html>
|
package-lock.json
ADDED
|
@@ -0,0 +1,1886 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "antigravity-tools-cloud",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"lockfileVersion": 1,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"dependencies": {
|
| 7 |
+
"@alloc/quick-lru": {
|
| 8 |
+
"version": "5.2.0",
|
| 9 |
+
"resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
| 10 |
+
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
| 11 |
+
"dev": true
|
| 12 |
+
},
|
| 13 |
+
"@babel/code-frame": {
|
| 14 |
+
"version": "7.27.1",
|
| 15 |
+
"resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
| 16 |
+
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
| 17 |
+
"dev": true,
|
| 18 |
+
"requires": {
|
| 19 |
+
"@babel/helper-validator-identifier": "^7.27.1",
|
| 20 |
+
"js-tokens": "^4.0.0",
|
| 21 |
+
"picocolors": "^1.1.1"
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"@babel/compat-data": {
|
| 25 |
+
"version": "7.28.5",
|
| 26 |
+
"resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz",
|
| 27 |
+
"integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
|
| 28 |
+
"dev": true
|
| 29 |
+
},
|
| 30 |
+
"@babel/core": {
|
| 31 |
+
"version": "7.28.5",
|
| 32 |
+
"resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz",
|
| 33 |
+
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
| 34 |
+
"dev": true,
|
| 35 |
+
"requires": {
|
| 36 |
+
"@babel/code-frame": "^7.27.1",
|
| 37 |
+
"@babel/generator": "^7.28.5",
|
| 38 |
+
"@babel/helper-compilation-targets": "^7.27.2",
|
| 39 |
+
"@babel/helper-module-transforms": "^7.28.3",
|
| 40 |
+
"@babel/helpers": "^7.28.4",
|
| 41 |
+
"@babel/parser": "^7.28.5",
|
| 42 |
+
"@babel/template": "^7.27.2",
|
| 43 |
+
"@babel/traverse": "^7.28.5",
|
| 44 |
+
"@babel/types": "^7.28.5",
|
| 45 |
+
"@jridgewell/remapping": "^2.3.5",
|
| 46 |
+
"convert-source-map": "^2.0.0",
|
| 47 |
+
"debug": "^4.1.0",
|
| 48 |
+
"gensync": "^1.0.0-beta.2",
|
| 49 |
+
"json5": "^2.2.3",
|
| 50 |
+
"semver": "^6.3.1"
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"@babel/generator": {
|
| 54 |
+
"version": "7.28.5",
|
| 55 |
+
"resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.5.tgz",
|
| 56 |
+
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
|
| 57 |
+
"dev": true,
|
| 58 |
+
"requires": {
|
| 59 |
+
"@babel/parser": "^7.28.5",
|
| 60 |
+
"@babel/types": "^7.28.5",
|
| 61 |
+
"@jridgewell/gen-mapping": "^0.3.12",
|
| 62 |
+
"@jridgewell/trace-mapping": "^0.3.28",
|
| 63 |
+
"jsesc": "^3.0.2"
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
"@babel/helper-compilation-targets": {
|
| 67 |
+
"version": "7.27.2",
|
| 68 |
+
"resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
| 69 |
+
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
|
| 70 |
+
"dev": true,
|
| 71 |
+
"requires": {
|
| 72 |
+
"@babel/compat-data": "^7.27.2",
|
| 73 |
+
"@babel/helper-validator-option": "^7.27.1",
|
| 74 |
+
"browserslist": "^4.24.0",
|
| 75 |
+
"lru-cache": "^5.1.1",
|
| 76 |
+
"semver": "^6.3.1"
|
| 77 |
+
}
|
| 78 |
+
},
|
| 79 |
+
"@babel/helper-globals": {
|
| 80 |
+
"version": "7.28.0",
|
| 81 |
+
"resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
| 82 |
+
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
| 83 |
+
"dev": true
|
| 84 |
+
},
|
| 85 |
+
"@babel/helper-module-imports": {
|
| 86 |
+
"version": "7.27.1",
|
| 87 |
+
"resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
| 88 |
+
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
| 89 |
+
"dev": true,
|
| 90 |
+
"requires": {
|
| 91 |
+
"@babel/traverse": "^7.27.1",
|
| 92 |
+
"@babel/types": "^7.27.1"
|
| 93 |
+
}
|
| 94 |
+
},
|
| 95 |
+
"@babel/helper-module-transforms": {
|
| 96 |
+
"version": "7.28.3",
|
| 97 |
+
"resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
|
| 98 |
+
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
|
| 99 |
+
"dev": true,
|
| 100 |
+
"requires": {
|
| 101 |
+
"@babel/helper-module-imports": "^7.27.1",
|
| 102 |
+
"@babel/helper-validator-identifier": "^7.27.1",
|
| 103 |
+
"@babel/traverse": "^7.28.3"
|
| 104 |
+
}
|
| 105 |
+
},
|
| 106 |
+
"@babel/helper-plugin-utils": {
|
| 107 |
+
"version": "7.27.1",
|
| 108 |
+
"resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
| 109 |
+
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
| 110 |
+
"dev": true
|
| 111 |
+
},
|
| 112 |
+
"@babel/helper-string-parser": {
|
| 113 |
+
"version": "7.27.1",
|
| 114 |
+
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 115 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 116 |
+
"dev": true
|
| 117 |
+
},
|
| 118 |
+
"@babel/helper-validator-identifier": {
|
| 119 |
+
"version": "7.28.5",
|
| 120 |
+
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
| 121 |
+
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
| 122 |
+
"dev": true
|
| 123 |
+
},
|
| 124 |
+
"@babel/helper-validator-option": {
|
| 125 |
+
"version": "7.27.1",
|
| 126 |
+
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
| 127 |
+
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
| 128 |
+
"dev": true
|
| 129 |
+
},
|
| 130 |
+
"@babel/helpers": {
|
| 131 |
+
"version": "7.28.4",
|
| 132 |
+
"resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz",
|
| 133 |
+
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
|
| 134 |
+
"dev": true,
|
| 135 |
+
"requires": {
|
| 136 |
+
"@babel/template": "^7.27.2",
|
| 137 |
+
"@babel/types": "^7.28.4"
|
| 138 |
+
}
|
| 139 |
+
},
|
| 140 |
+
"@babel/parser": {
|
| 141 |
+
"version": "7.28.5",
|
| 142 |
+
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz",
|
| 143 |
+
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
| 144 |
+
"dev": true,
|
| 145 |
+
"requires": {
|
| 146 |
+
"@babel/types": "^7.28.5"
|
| 147 |
+
}
|
| 148 |
+
},
|
| 149 |
+
"@babel/plugin-transform-react-jsx-self": {
|
| 150 |
+
"version": "7.27.1",
|
| 151 |
+
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
|
| 152 |
+
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
| 153 |
+
"dev": true,
|
| 154 |
+
"requires": {
|
| 155 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 156 |
+
}
|
| 157 |
+
},
|
| 158 |
+
"@babel/plugin-transform-react-jsx-source": {
|
| 159 |
+
"version": "7.27.1",
|
| 160 |
+
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
|
| 161 |
+
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
| 162 |
+
"dev": true,
|
| 163 |
+
"requires": {
|
| 164 |
+
"@babel/helper-plugin-utils": "^7.27.1"
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
+
"@babel/runtime": {
|
| 168 |
+
"version": "7.28.4",
|
| 169 |
+
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz",
|
| 170 |
+
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="
|
| 171 |
+
},
|
| 172 |
+
"@babel/template": {
|
| 173 |
+
"version": "7.27.2",
|
| 174 |
+
"resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz",
|
| 175 |
+
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
| 176 |
+
"dev": true,
|
| 177 |
+
"requires": {
|
| 178 |
+
"@babel/code-frame": "^7.27.1",
|
| 179 |
+
"@babel/parser": "^7.27.2",
|
| 180 |
+
"@babel/types": "^7.27.1"
|
| 181 |
+
}
|
| 182 |
+
},
|
| 183 |
+
"@babel/traverse": {
|
| 184 |
+
"version": "7.28.5",
|
| 185 |
+
"resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.5.tgz",
|
| 186 |
+
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
|
| 187 |
+
"dev": true,
|
| 188 |
+
"requires": {
|
| 189 |
+
"@babel/code-frame": "^7.27.1",
|
| 190 |
+
"@babel/generator": "^7.28.5",
|
| 191 |
+
"@babel/helper-globals": "^7.28.0",
|
| 192 |
+
"@babel/parser": "^7.28.5",
|
| 193 |
+
"@babel/template": "^7.27.2",
|
| 194 |
+
"@babel/types": "^7.28.5",
|
| 195 |
+
"debug": "^4.3.1"
|
| 196 |
+
}
|
| 197 |
+
},
|
| 198 |
+
"@babel/types": {
|
| 199 |
+
"version": "7.28.5",
|
| 200 |
+
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz",
|
| 201 |
+
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
| 202 |
+
"dev": true,
|
| 203 |
+
"requires": {
|
| 204 |
+
"@babel/helper-string-parser": "^7.27.1",
|
| 205 |
+
"@babel/helper-validator-identifier": "^7.28.5"
|
| 206 |
+
}
|
| 207 |
+
},
|
| 208 |
+
"@esbuild/aix-ppc64": {
|
| 209 |
+
"version": "0.27.2",
|
| 210 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
|
| 211 |
+
"integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
|
| 212 |
+
"dev": true,
|
| 213 |
+
"optional": true
|
| 214 |
+
},
|
| 215 |
+
"@esbuild/android-arm": {
|
| 216 |
+
"version": "0.27.2",
|
| 217 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
|
| 218 |
+
"integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
|
| 219 |
+
"dev": true,
|
| 220 |
+
"optional": true
|
| 221 |
+
},
|
| 222 |
+
"@esbuild/android-arm64": {
|
| 223 |
+
"version": "0.27.2",
|
| 224 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
|
| 225 |
+
"integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
|
| 226 |
+
"dev": true,
|
| 227 |
+
"optional": true
|
| 228 |
+
},
|
| 229 |
+
"@esbuild/android-x64": {
|
| 230 |
+
"version": "0.27.2",
|
| 231 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
|
| 232 |
+
"integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
|
| 233 |
+
"dev": true,
|
| 234 |
+
"optional": true
|
| 235 |
+
},
|
| 236 |
+
"@esbuild/darwin-arm64": {
|
| 237 |
+
"version": "0.27.2",
|
| 238 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
|
| 239 |
+
"integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
|
| 240 |
+
"dev": true,
|
| 241 |
+
"optional": true
|
| 242 |
+
},
|
| 243 |
+
"@esbuild/darwin-x64": {
|
| 244 |
+
"version": "0.27.2",
|
| 245 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
|
| 246 |
+
"integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
|
| 247 |
+
"dev": true,
|
| 248 |
+
"optional": true
|
| 249 |
+
},
|
| 250 |
+
"@esbuild/freebsd-arm64": {
|
| 251 |
+
"version": "0.27.2",
|
| 252 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
|
| 253 |
+
"integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
|
| 254 |
+
"dev": true,
|
| 255 |
+
"optional": true
|
| 256 |
+
},
|
| 257 |
+
"@esbuild/freebsd-x64": {
|
| 258 |
+
"version": "0.27.2",
|
| 259 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
|
| 260 |
+
"integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
|
| 261 |
+
"dev": true,
|
| 262 |
+
"optional": true
|
| 263 |
+
},
|
| 264 |
+
"@esbuild/linux-arm": {
|
| 265 |
+
"version": "0.27.2",
|
| 266 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
|
| 267 |
+
"integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
|
| 268 |
+
"dev": true,
|
| 269 |
+
"optional": true
|
| 270 |
+
},
|
| 271 |
+
"@esbuild/linux-arm64": {
|
| 272 |
+
"version": "0.27.2",
|
| 273 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
|
| 274 |
+
"integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
|
| 275 |
+
"dev": true,
|
| 276 |
+
"optional": true
|
| 277 |
+
},
|
| 278 |
+
"@esbuild/linux-ia32": {
|
| 279 |
+
"version": "0.27.2",
|
| 280 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
|
| 281 |
+
"integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
|
| 282 |
+
"dev": true,
|
| 283 |
+
"optional": true
|
| 284 |
+
},
|
| 285 |
+
"@esbuild/linux-loong64": {
|
| 286 |
+
"version": "0.27.2",
|
| 287 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
|
| 288 |
+
"integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
|
| 289 |
+
"dev": true,
|
| 290 |
+
"optional": true
|
| 291 |
+
},
|
| 292 |
+
"@esbuild/linux-mips64el": {
|
| 293 |
+
"version": "0.27.2",
|
| 294 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
|
| 295 |
+
"integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
|
| 296 |
+
"dev": true,
|
| 297 |
+
"optional": true
|
| 298 |
+
},
|
| 299 |
+
"@esbuild/linux-ppc64": {
|
| 300 |
+
"version": "0.27.2",
|
| 301 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
|
| 302 |
+
"integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
|
| 303 |
+
"dev": true,
|
| 304 |
+
"optional": true
|
| 305 |
+
},
|
| 306 |
+
"@esbuild/linux-riscv64": {
|
| 307 |
+
"version": "0.27.2",
|
| 308 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
|
| 309 |
+
"integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
|
| 310 |
+
"dev": true,
|
| 311 |
+
"optional": true
|
| 312 |
+
},
|
| 313 |
+
"@esbuild/linux-s390x": {
|
| 314 |
+
"version": "0.27.2",
|
| 315 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
|
| 316 |
+
"integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
|
| 317 |
+
"dev": true,
|
| 318 |
+
"optional": true
|
| 319 |
+
},
|
| 320 |
+
"@esbuild/linux-x64": {
|
| 321 |
+
"version": "0.27.2",
|
| 322 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
|
| 323 |
+
"integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
|
| 324 |
+
"dev": true,
|
| 325 |
+
"optional": true
|
| 326 |
+
},
|
| 327 |
+
"@esbuild/netbsd-arm64": {
|
| 328 |
+
"version": "0.27.2",
|
| 329 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
|
| 330 |
+
"integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
|
| 331 |
+
"dev": true,
|
| 332 |
+
"optional": true
|
| 333 |
+
},
|
| 334 |
+
"@esbuild/netbsd-x64": {
|
| 335 |
+
"version": "0.27.2",
|
| 336 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
|
| 337 |
+
"integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
|
| 338 |
+
"dev": true,
|
| 339 |
+
"optional": true
|
| 340 |
+
},
|
| 341 |
+
"@esbuild/openbsd-arm64": {
|
| 342 |
+
"version": "0.27.2",
|
| 343 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
|
| 344 |
+
"integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
|
| 345 |
+
"dev": true,
|
| 346 |
+
"optional": true
|
| 347 |
+
},
|
| 348 |
+
"@esbuild/openbsd-x64": {
|
| 349 |
+
"version": "0.27.2",
|
| 350 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
|
| 351 |
+
"integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
|
| 352 |
+
"dev": true,
|
| 353 |
+
"optional": true
|
| 354 |
+
},
|
| 355 |
+
"@esbuild/openharmony-arm64": {
|
| 356 |
+
"version": "0.27.2",
|
| 357 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
|
| 358 |
+
"integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
|
| 359 |
+
"dev": true,
|
| 360 |
+
"optional": true
|
| 361 |
+
},
|
| 362 |
+
"@esbuild/sunos-x64": {
|
| 363 |
+
"version": "0.27.2",
|
| 364 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
|
| 365 |
+
"integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
|
| 366 |
+
"dev": true,
|
| 367 |
+
"optional": true
|
| 368 |
+
},
|
| 369 |
+
"@esbuild/win32-arm64": {
|
| 370 |
+
"version": "0.27.2",
|
| 371 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
|
| 372 |
+
"integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
|
| 373 |
+
"dev": true,
|
| 374 |
+
"optional": true
|
| 375 |
+
},
|
| 376 |
+
"@esbuild/win32-ia32": {
|
| 377 |
+
"version": "0.27.2",
|
| 378 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
|
| 379 |
+
"integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
|
| 380 |
+
"dev": true,
|
| 381 |
+
"optional": true
|
| 382 |
+
},
|
| 383 |
+
"@esbuild/win32-x64": {
|
| 384 |
+
"version": "0.27.2",
|
| 385 |
+
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
|
| 386 |
+
"integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
|
| 387 |
+
"dev": true,
|
| 388 |
+
"optional": true
|
| 389 |
+
},
|
| 390 |
+
"@jridgewell/gen-mapping": {
|
| 391 |
+
"version": "0.3.13",
|
| 392 |
+
"resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 393 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 394 |
+
"dev": true,
|
| 395 |
+
"requires": {
|
| 396 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 397 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 398 |
+
}
|
| 399 |
+
},
|
| 400 |
+
"@jridgewell/remapping": {
|
| 401 |
+
"version": "2.3.5",
|
| 402 |
+
"resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
| 403 |
+
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
| 404 |
+
"dev": true,
|
| 405 |
+
"requires": {
|
| 406 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 407 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 408 |
+
}
|
| 409 |
+
},
|
| 410 |
+
"@jridgewell/resolve-uri": {
|
| 411 |
+
"version": "3.1.2",
|
| 412 |
+
"resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 413 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 414 |
+
"dev": true
|
| 415 |
+
},
|
| 416 |
+
"@jridgewell/sourcemap-codec": {
|
| 417 |
+
"version": "1.5.5",
|
| 418 |
+
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 419 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 420 |
+
"dev": true
|
| 421 |
+
},
|
| 422 |
+
"@jridgewell/trace-mapping": {
|
| 423 |
+
"version": "0.3.31",
|
| 424 |
+
"resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 425 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 426 |
+
"dev": true,
|
| 427 |
+
"requires": {
|
| 428 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 429 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 430 |
+
}
|
| 431 |
+
},
|
| 432 |
+
"@nodelib/fs.scandir": {
|
| 433 |
+
"version": "2.1.5",
|
| 434 |
+
"resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
| 435 |
+
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
| 436 |
+
"dev": true,
|
| 437 |
+
"requires": {
|
| 438 |
+
"@nodelib/fs.stat": "2.0.5",
|
| 439 |
+
"run-parallel": "^1.1.9"
|
| 440 |
+
}
|
| 441 |
+
},
|
| 442 |
+
"@nodelib/fs.stat": {
|
| 443 |
+
"version": "2.0.5",
|
| 444 |
+
"resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
| 445 |
+
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
| 446 |
+
"dev": true
|
| 447 |
+
},
|
| 448 |
+
"@nodelib/fs.walk": {
|
| 449 |
+
"version": "1.2.8",
|
| 450 |
+
"resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
| 451 |
+
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
| 452 |
+
"dev": true,
|
| 453 |
+
"requires": {
|
| 454 |
+
"@nodelib/fs.scandir": "2.1.5",
|
| 455 |
+
"fastq": "^1.6.0"
|
| 456 |
+
}
|
| 457 |
+
},
|
| 458 |
+
"@reduxjs/toolkit": {
|
| 459 |
+
"version": "2.11.2",
|
| 460 |
+
"resolved": "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
| 461 |
+
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
| 462 |
+
"requires": {
|
| 463 |
+
"@standard-schema/spec": "^1.0.0",
|
| 464 |
+
"@standard-schema/utils": "^0.3.0",
|
| 465 |
+
"immer": "^11.0.0",
|
| 466 |
+
"redux": "^5.0.1",
|
| 467 |
+
"redux-thunk": "^3.1.0",
|
| 468 |
+
"reselect": "^5.1.0"
|
| 469 |
+
},
|
| 470 |
+
"dependencies": {
|
| 471 |
+
"immer": {
|
| 472 |
+
"version": "11.1.3",
|
| 473 |
+
"resolved": "https://registry.npmmirror.com/immer/-/immer-11.1.3.tgz",
|
| 474 |
+
"integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q=="
|
| 475 |
+
}
|
| 476 |
+
}
|
| 477 |
+
},
|
| 478 |
+
"@rolldown/pluginutils": {
|
| 479 |
+
"version": "1.0.0-beta.27",
|
| 480 |
+
"resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
| 481 |
+
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
| 482 |
+
"dev": true
|
| 483 |
+
},
|
| 484 |
+
"@rollup/rollup-android-arm-eabi": {
|
| 485 |
+
"version": "4.54.0",
|
| 486 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
|
| 487 |
+
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
|
| 488 |
+
"dev": true,
|
| 489 |
+
"optional": true
|
| 490 |
+
},
|
| 491 |
+
"@rollup/rollup-android-arm64": {
|
| 492 |
+
"version": "4.54.0",
|
| 493 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
|
| 494 |
+
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
|
| 495 |
+
"dev": true,
|
| 496 |
+
"optional": true
|
| 497 |
+
},
|
| 498 |
+
"@rollup/rollup-darwin-arm64": {
|
| 499 |
+
"version": "4.54.0",
|
| 500 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
|
| 501 |
+
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
|
| 502 |
+
"dev": true,
|
| 503 |
+
"optional": true
|
| 504 |
+
},
|
| 505 |
+
"@rollup/rollup-darwin-x64": {
|
| 506 |
+
"version": "4.54.0",
|
| 507 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
|
| 508 |
+
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
|
| 509 |
+
"dev": true,
|
| 510 |
+
"optional": true
|
| 511 |
+
},
|
| 512 |
+
"@rollup/rollup-freebsd-arm64": {
|
| 513 |
+
"version": "4.54.0",
|
| 514 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
|
| 515 |
+
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
|
| 516 |
+
"dev": true,
|
| 517 |
+
"optional": true
|
| 518 |
+
},
|
| 519 |
+
"@rollup/rollup-freebsd-x64": {
|
| 520 |
+
"version": "4.54.0",
|
| 521 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
|
| 522 |
+
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
|
| 523 |
+
"dev": true,
|
| 524 |
+
"optional": true
|
| 525 |
+
},
|
| 526 |
+
"@rollup/rollup-linux-arm-gnueabihf": {
|
| 527 |
+
"version": "4.54.0",
|
| 528 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
|
| 529 |
+
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
|
| 530 |
+
"dev": true,
|
| 531 |
+
"optional": true
|
| 532 |
+
},
|
| 533 |
+
"@rollup/rollup-linux-arm-musleabihf": {
|
| 534 |
+
"version": "4.54.0",
|
| 535 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
|
| 536 |
+
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
|
| 537 |
+
"dev": true,
|
| 538 |
+
"optional": true
|
| 539 |
+
},
|
| 540 |
+
"@rollup/rollup-linux-arm64-gnu": {
|
| 541 |
+
"version": "4.54.0",
|
| 542 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
|
| 543 |
+
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
|
| 544 |
+
"dev": true,
|
| 545 |
+
"optional": true
|
| 546 |
+
},
|
| 547 |
+
"@rollup/rollup-linux-arm64-musl": {
|
| 548 |
+
"version": "4.54.0",
|
| 549 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
|
| 550 |
+
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
|
| 551 |
+
"dev": true,
|
| 552 |
+
"optional": true
|
| 553 |
+
},
|
| 554 |
+
"@rollup/rollup-linux-loong64-gnu": {
|
| 555 |
+
"version": "4.54.0",
|
| 556 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
|
| 557 |
+
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
|
| 558 |
+
"dev": true,
|
| 559 |
+
"optional": true
|
| 560 |
+
},
|
| 561 |
+
"@rollup/rollup-linux-ppc64-gnu": {
|
| 562 |
+
"version": "4.54.0",
|
| 563 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
|
| 564 |
+
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
|
| 565 |
+
"dev": true,
|
| 566 |
+
"optional": true
|
| 567 |
+
},
|
| 568 |
+
"@rollup/rollup-linux-riscv64-gnu": {
|
| 569 |
+
"version": "4.54.0",
|
| 570 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
|
| 571 |
+
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
|
| 572 |
+
"dev": true,
|
| 573 |
+
"optional": true
|
| 574 |
+
},
|
| 575 |
+
"@rollup/rollup-linux-riscv64-musl": {
|
| 576 |
+
"version": "4.54.0",
|
| 577 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
|
| 578 |
+
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
|
| 579 |
+
"dev": true,
|
| 580 |
+
"optional": true
|
| 581 |
+
},
|
| 582 |
+
"@rollup/rollup-linux-s390x-gnu": {
|
| 583 |
+
"version": "4.54.0",
|
| 584 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
|
| 585 |
+
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
|
| 586 |
+
"dev": true,
|
| 587 |
+
"optional": true
|
| 588 |
+
},
|
| 589 |
+
"@rollup/rollup-linux-x64-gnu": {
|
| 590 |
+
"version": "4.54.0",
|
| 591 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
|
| 592 |
+
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
|
| 593 |
+
"dev": true,
|
| 594 |
+
"optional": true
|
| 595 |
+
},
|
| 596 |
+
"@rollup/rollup-linux-x64-musl": {
|
| 597 |
+
"version": "4.54.0",
|
| 598 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
|
| 599 |
+
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
|
| 600 |
+
"dev": true,
|
| 601 |
+
"optional": true
|
| 602 |
+
},
|
| 603 |
+
"@rollup/rollup-openharmony-arm64": {
|
| 604 |
+
"version": "4.54.0",
|
| 605 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
|
| 606 |
+
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
|
| 607 |
+
"dev": true,
|
| 608 |
+
"optional": true
|
| 609 |
+
},
|
| 610 |
+
"@rollup/rollup-win32-arm64-msvc": {
|
| 611 |
+
"version": "4.54.0",
|
| 612 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
|
| 613 |
+
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
|
| 614 |
+
"dev": true,
|
| 615 |
+
"optional": true
|
| 616 |
+
},
|
| 617 |
+
"@rollup/rollup-win32-ia32-msvc": {
|
| 618 |
+
"version": "4.54.0",
|
| 619 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
|
| 620 |
+
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
|
| 621 |
+
"dev": true,
|
| 622 |
+
"optional": true
|
| 623 |
+
},
|
| 624 |
+
"@rollup/rollup-win32-x64-gnu": {
|
| 625 |
+
"version": "4.54.0",
|
| 626 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
|
| 627 |
+
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
|
| 628 |
+
"dev": true,
|
| 629 |
+
"optional": true
|
| 630 |
+
},
|
| 631 |
+
"@rollup/rollup-win32-x64-msvc": {
|
| 632 |
+
"version": "4.54.0",
|
| 633 |
+
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
|
| 634 |
+
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
|
| 635 |
+
"dev": true,
|
| 636 |
+
"optional": true
|
| 637 |
+
},
|
| 638 |
+
"@standard-schema/spec": {
|
| 639 |
+
"version": "1.1.0",
|
| 640 |
+
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
|
| 641 |
+
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="
|
| 642 |
+
},
|
| 643 |
+
"@standard-schema/utils": {
|
| 644 |
+
"version": "0.3.0",
|
| 645 |
+
"resolved": "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz",
|
| 646 |
+
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
|
| 647 |
+
},
|
| 648 |
+
"@types/babel__core": {
|
| 649 |
+
"version": "7.20.5",
|
| 650 |
+
"resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz",
|
| 651 |
+
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
| 652 |
+
"dev": true,
|
| 653 |
+
"requires": {
|
| 654 |
+
"@babel/parser": "^7.20.7",
|
| 655 |
+
"@babel/types": "^7.20.7",
|
| 656 |
+
"@types/babel__generator": "*",
|
| 657 |
+
"@types/babel__template": "*",
|
| 658 |
+
"@types/babel__traverse": "*"
|
| 659 |
+
}
|
| 660 |
+
},
|
| 661 |
+
"@types/babel__generator": {
|
| 662 |
+
"version": "7.27.0",
|
| 663 |
+
"resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
| 664 |
+
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
| 665 |
+
"dev": true,
|
| 666 |
+
"requires": {
|
| 667 |
+
"@babel/types": "^7.0.0"
|
| 668 |
+
}
|
| 669 |
+
},
|
| 670 |
+
"@types/babel__template": {
|
| 671 |
+
"version": "7.4.4",
|
| 672 |
+
"resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz",
|
| 673 |
+
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
| 674 |
+
"dev": true,
|
| 675 |
+
"requires": {
|
| 676 |
+
"@babel/parser": "^7.1.0",
|
| 677 |
+
"@babel/types": "^7.0.0"
|
| 678 |
+
}
|
| 679 |
+
},
|
| 680 |
+
"@types/babel__traverse": {
|
| 681 |
+
"version": "7.28.0",
|
| 682 |
+
"resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
|
| 683 |
+
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
|
| 684 |
+
"dev": true,
|
| 685 |
+
"requires": {
|
| 686 |
+
"@babel/types": "^7.28.2"
|
| 687 |
+
}
|
| 688 |
+
},
|
| 689 |
+
"@types/d3-array": {
|
| 690 |
+
"version": "3.2.2",
|
| 691 |
+
"resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz",
|
| 692 |
+
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="
|
| 693 |
+
},
|
| 694 |
+
"@types/d3-color": {
|
| 695 |
+
"version": "3.1.3",
|
| 696 |
+
"resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz",
|
| 697 |
+
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
|
| 698 |
+
},
|
| 699 |
+
"@types/d3-ease": {
|
| 700 |
+
"version": "3.0.2",
|
| 701 |
+
"resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
| 702 |
+
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
|
| 703 |
+
},
|
| 704 |
+
"@types/d3-interpolate": {
|
| 705 |
+
"version": "3.0.4",
|
| 706 |
+
"resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
| 707 |
+
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
| 708 |
+
"requires": {
|
| 709 |
+
"@types/d3-color": "*"
|
| 710 |
+
}
|
| 711 |
+
},
|
| 712 |
+
"@types/d3-path": {
|
| 713 |
+
"version": "3.1.1",
|
| 714 |
+
"resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz",
|
| 715 |
+
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="
|
| 716 |
+
},
|
| 717 |
+
"@types/d3-scale": {
|
| 718 |
+
"version": "4.0.9",
|
| 719 |
+
"resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
| 720 |
+
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
| 721 |
+
"requires": {
|
| 722 |
+
"@types/d3-time": "*"
|
| 723 |
+
}
|
| 724 |
+
},
|
| 725 |
+
"@types/d3-shape": {
|
| 726 |
+
"version": "3.1.7",
|
| 727 |
+
"resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
| 728 |
+
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
| 729 |
+
"requires": {
|
| 730 |
+
"@types/d3-path": "*"
|
| 731 |
+
}
|
| 732 |
+
},
|
| 733 |
+
"@types/d3-time": {
|
| 734 |
+
"version": "3.0.4",
|
| 735 |
+
"resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz",
|
| 736 |
+
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
|
| 737 |
+
},
|
| 738 |
+
"@types/d3-timer": {
|
| 739 |
+
"version": "3.0.2",
|
| 740 |
+
"resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
| 741 |
+
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
|
| 742 |
+
},
|
| 743 |
+
"@types/estree": {
|
| 744 |
+
"version": "1.0.8",
|
| 745 |
+
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
|
| 746 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 747 |
+
"dev": true
|
| 748 |
+
},
|
| 749 |
+
"@types/react": {
|
| 750 |
+
"version": "19.2.7",
|
| 751 |
+
"resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.7.tgz",
|
| 752 |
+
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
| 753 |
+
"dev": true,
|
| 754 |
+
"requires": {
|
| 755 |
+
"csstype": "^3.2.2"
|
| 756 |
+
}
|
| 757 |
+
},
|
| 758 |
+
"@types/react-dom": {
|
| 759 |
+
"version": "19.2.3",
|
| 760 |
+
"resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz",
|
| 761 |
+
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
| 762 |
+
"dev": true
|
| 763 |
+
},
|
| 764 |
+
"@types/use-sync-external-store": {
|
| 765 |
+
"version": "0.0.6",
|
| 766 |
+
"resolved": "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
| 767 |
+
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
|
| 768 |
+
},
|
| 769 |
+
"@vitejs/plugin-react": {
|
| 770 |
+
"version": "4.7.0",
|
| 771 |
+
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
| 772 |
+
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
| 773 |
+
"dev": true,
|
| 774 |
+
"requires": {
|
| 775 |
+
"@babel/core": "^7.28.0",
|
| 776 |
+
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
| 777 |
+
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
| 778 |
+
"@rolldown/pluginutils": "1.0.0-beta.27",
|
| 779 |
+
"@types/babel__core": "^7.20.5",
|
| 780 |
+
"react-refresh": "^0.17.0"
|
| 781 |
+
}
|
| 782 |
+
},
|
| 783 |
+
"any-promise": {
|
| 784 |
+
"version": "1.3.0",
|
| 785 |
+
"resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz",
|
| 786 |
+
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
| 787 |
+
"dev": true
|
| 788 |
+
},
|
| 789 |
+
"anymatch": {
|
| 790 |
+
"version": "3.1.3",
|
| 791 |
+
"resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz",
|
| 792 |
+
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
| 793 |
+
"dev": true,
|
| 794 |
+
"requires": {
|
| 795 |
+
"normalize-path": "^3.0.0",
|
| 796 |
+
"picomatch": "^2.0.4"
|
| 797 |
+
}
|
| 798 |
+
},
|
| 799 |
+
"arg": {
|
| 800 |
+
"version": "5.0.2",
|
| 801 |
+
"resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz",
|
| 802 |
+
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
| 803 |
+
"dev": true
|
| 804 |
+
},
|
| 805 |
+
"autoprefixer": {
|
| 806 |
+
"version": "10.4.23",
|
| 807 |
+
"resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
| 808 |
+
"integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==",
|
| 809 |
+
"dev": true,
|
| 810 |
+
"requires": {
|
| 811 |
+
"browserslist": "^4.28.1",
|
| 812 |
+
"caniuse-lite": "^1.0.30001760",
|
| 813 |
+
"fraction.js": "^5.3.4",
|
| 814 |
+
"picocolors": "^1.1.1",
|
| 815 |
+
"postcss-value-parser": "^4.2.0"
|
| 816 |
+
}
|
| 817 |
+
},
|
| 818 |
+
"baseline-browser-mapping": {
|
| 819 |
+
"version": "2.9.11",
|
| 820 |
+
"resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
|
| 821 |
+
"integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==",
|
| 822 |
+
"dev": true
|
| 823 |
+
},
|
| 824 |
+
"binary-extensions": {
|
| 825 |
+
"version": "2.3.0",
|
| 826 |
+
"resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
| 827 |
+
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
| 828 |
+
"dev": true
|
| 829 |
+
},
|
| 830 |
+
"braces": {
|
| 831 |
+
"version": "3.0.3",
|
| 832 |
+
"resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz",
|
| 833 |
+
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
| 834 |
+
"dev": true,
|
| 835 |
+
"requires": {
|
| 836 |
+
"fill-range": "^7.1.1"
|
| 837 |
+
}
|
| 838 |
+
},
|
| 839 |
+
"browserslist": {
|
| 840 |
+
"version": "4.28.1",
|
| 841 |
+
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz",
|
| 842 |
+
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
| 843 |
+
"dev": true,
|
| 844 |
+
"requires": {
|
| 845 |
+
"baseline-browser-mapping": "^2.9.0",
|
| 846 |
+
"caniuse-lite": "^1.0.30001759",
|
| 847 |
+
"electron-to-chromium": "^1.5.263",
|
| 848 |
+
"node-releases": "^2.0.27",
|
| 849 |
+
"update-browserslist-db": "^1.2.0"
|
| 850 |
+
}
|
| 851 |
+
},
|
| 852 |
+
"camelcase-css": {
|
| 853 |
+
"version": "2.0.1",
|
| 854 |
+
"resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
| 855 |
+
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
| 856 |
+
"dev": true
|
| 857 |
+
},
|
| 858 |
+
"caniuse-lite": {
|
| 859 |
+
"version": "1.0.30001762",
|
| 860 |
+
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
|
| 861 |
+
"integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==",
|
| 862 |
+
"dev": true
|
| 863 |
+
},
|
| 864 |
+
"chokidar": {
|
| 865 |
+
"version": "3.6.0",
|
| 866 |
+
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
|
| 867 |
+
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
| 868 |
+
"dev": true,
|
| 869 |
+
"requires": {
|
| 870 |
+
"anymatch": "~3.1.2",
|
| 871 |
+
"braces": "~3.0.2",
|
| 872 |
+
"fsevents": "~2.3.2",
|
| 873 |
+
"glob-parent": "~5.1.2",
|
| 874 |
+
"is-binary-path": "~2.1.0",
|
| 875 |
+
"is-glob": "~4.0.1",
|
| 876 |
+
"normalize-path": "~3.0.0",
|
| 877 |
+
"readdirp": "~3.6.0"
|
| 878 |
+
},
|
| 879 |
+
"dependencies": {
|
| 880 |
+
"glob-parent": {
|
| 881 |
+
"version": "5.1.2",
|
| 882 |
+
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
|
| 883 |
+
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
| 884 |
+
"dev": true,
|
| 885 |
+
"requires": {
|
| 886 |
+
"is-glob": "^4.0.1"
|
| 887 |
+
}
|
| 888 |
+
}
|
| 889 |
+
}
|
| 890 |
+
},
|
| 891 |
+
"clsx": {
|
| 892 |
+
"version": "2.1.1",
|
| 893 |
+
"resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
|
| 894 |
+
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="
|
| 895 |
+
},
|
| 896 |
+
"commander": {
|
| 897 |
+
"version": "4.1.1",
|
| 898 |
+
"resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
|
| 899 |
+
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
| 900 |
+
"dev": true
|
| 901 |
+
},
|
| 902 |
+
"convert-source-map": {
|
| 903 |
+
"version": "2.0.0",
|
| 904 |
+
"resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
| 905 |
+
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
| 906 |
+
"dev": true
|
| 907 |
+
},
|
| 908 |
+
"cookie": {
|
| 909 |
+
"version": "1.1.1",
|
| 910 |
+
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz",
|
| 911 |
+
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="
|
| 912 |
+
},
|
| 913 |
+
"cssesc": {
|
| 914 |
+
"version": "3.0.0",
|
| 915 |
+
"resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz",
|
| 916 |
+
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
| 917 |
+
"dev": true
|
| 918 |
+
},
|
| 919 |
+
"csstype": {
|
| 920 |
+
"version": "3.2.3",
|
| 921 |
+
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
| 922 |
+
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 923 |
+
"dev": true
|
| 924 |
+
},
|
| 925 |
+
"d3-array": {
|
| 926 |
+
"version": "3.2.4",
|
| 927 |
+
"resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz",
|
| 928 |
+
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
| 929 |
+
"requires": {
|
| 930 |
+
"internmap": "1 - 2"
|
| 931 |
+
}
|
| 932 |
+
},
|
| 933 |
+
"d3-color": {
|
| 934 |
+
"version": "3.1.0",
|
| 935 |
+
"resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
|
| 936 |
+
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="
|
| 937 |
+
},
|
| 938 |
+
"d3-ease": {
|
| 939 |
+
"version": "3.0.1",
|
| 940 |
+
"resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
|
| 941 |
+
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="
|
| 942 |
+
},
|
| 943 |
+
"d3-format": {
|
| 944 |
+
"version": "3.1.0",
|
| 945 |
+
"resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz",
|
| 946 |
+
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="
|
| 947 |
+
},
|
| 948 |
+
"d3-interpolate": {
|
| 949 |
+
"version": "3.0.1",
|
| 950 |
+
"resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
| 951 |
+
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
| 952 |
+
"requires": {
|
| 953 |
+
"d3-color": "1 - 3"
|
| 954 |
+
}
|
| 955 |
+
},
|
| 956 |
+
"d3-path": {
|
| 957 |
+
"version": "3.1.0",
|
| 958 |
+
"resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz",
|
| 959 |
+
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="
|
| 960 |
+
},
|
| 961 |
+
"d3-scale": {
|
| 962 |
+
"version": "4.0.2",
|
| 963 |
+
"resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz",
|
| 964 |
+
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
| 965 |
+
"requires": {
|
| 966 |
+
"d3-array": "2.10.0 - 3",
|
| 967 |
+
"d3-format": "1 - 3",
|
| 968 |
+
"d3-interpolate": "1.2.0 - 3",
|
| 969 |
+
"d3-time": "2.1.1 - 3",
|
| 970 |
+
"d3-time-format": "2 - 4"
|
| 971 |
+
}
|
| 972 |
+
},
|
| 973 |
+
"d3-shape": {
|
| 974 |
+
"version": "3.2.0",
|
| 975 |
+
"resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
|
| 976 |
+
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
| 977 |
+
"requires": {
|
| 978 |
+
"d3-path": "^3.1.0"
|
| 979 |
+
}
|
| 980 |
+
},
|
| 981 |
+
"d3-time": {
|
| 982 |
+
"version": "3.1.0",
|
| 983 |
+
"resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz",
|
| 984 |
+
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
| 985 |
+
"requires": {
|
| 986 |
+
"d3-array": "2 - 3"
|
| 987 |
+
}
|
| 988 |
+
},
|
| 989 |
+
"d3-time-format": {
|
| 990 |
+
"version": "4.1.0",
|
| 991 |
+
"resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
| 992 |
+
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
| 993 |
+
"requires": {
|
| 994 |
+
"d3-time": "1 - 3"
|
| 995 |
+
}
|
| 996 |
+
},
|
| 997 |
+
"d3-timer": {
|
| 998 |
+
"version": "3.0.1",
|
| 999 |
+
"resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
|
| 1000 |
+
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="
|
| 1001 |
+
},
|
| 1002 |
+
"daisyui": {
|
| 1003 |
+
"version": "5.5.14",
|
| 1004 |
+
"resolved": "https://registry.npmmirror.com/daisyui/-/daisyui-5.5.14.tgz",
|
| 1005 |
+
"integrity": "sha512-L47rvw7I7hK68TA97VB8Ee0woHew+/ohR6Lx6Ah/krfISOqcG4My7poNpX5Mo5/ytMxiR40fEaz6njzDi7cuSg=="
|
| 1006 |
+
},
|
| 1007 |
+
"date-fns": {
|
| 1008 |
+
"version": "4.1.0",
|
| 1009 |
+
"resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-4.1.0.tgz",
|
| 1010 |
+
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="
|
| 1011 |
+
},
|
| 1012 |
+
"debug": {
|
| 1013 |
+
"version": "4.4.3",
|
| 1014 |
+
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
| 1015 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 1016 |
+
"dev": true,
|
| 1017 |
+
"requires": {
|
| 1018 |
+
"ms": "^2.1.3"
|
| 1019 |
+
}
|
| 1020 |
+
},
|
| 1021 |
+
"decimal.js-light": {
|
| 1022 |
+
"version": "2.5.1",
|
| 1023 |
+
"resolved": "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
| 1024 |
+
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
|
| 1025 |
+
},
|
| 1026 |
+
"didyoumean": {
|
| 1027 |
+
"version": "1.2.2",
|
| 1028 |
+
"resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz",
|
| 1029 |
+
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
| 1030 |
+
"dev": true
|
| 1031 |
+
},
|
| 1032 |
+
"dlv": {
|
| 1033 |
+
"version": "1.1.3",
|
| 1034 |
+
"resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz",
|
| 1035 |
+
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
| 1036 |
+
"dev": true
|
| 1037 |
+
},
|
| 1038 |
+
"electron-to-chromium": {
|
| 1039 |
+
"version": "1.5.267",
|
| 1040 |
+
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
| 1041 |
+
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
|
| 1042 |
+
"dev": true
|
| 1043 |
+
},
|
| 1044 |
+
"es-toolkit": {
|
| 1045 |
+
"version": "1.43.0",
|
| 1046 |
+
"resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.43.0.tgz",
|
| 1047 |
+
"integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA=="
|
| 1048 |
+
},
|
| 1049 |
+
"esbuild": {
|
| 1050 |
+
"version": "0.27.2",
|
| 1051 |
+
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.2.tgz",
|
| 1052 |
+
"integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
|
| 1053 |
+
"dev": true,
|
| 1054 |
+
"requires": {
|
| 1055 |
+
"@esbuild/aix-ppc64": "0.27.2",
|
| 1056 |
+
"@esbuild/android-arm": "0.27.2",
|
| 1057 |
+
"@esbuild/android-arm64": "0.27.2",
|
| 1058 |
+
"@esbuild/android-x64": "0.27.2",
|
| 1059 |
+
"@esbuild/darwin-arm64": "0.27.2",
|
| 1060 |
+
"@esbuild/darwin-x64": "0.27.2",
|
| 1061 |
+
"@esbuild/freebsd-arm64": "0.27.2",
|
| 1062 |
+
"@esbuild/freebsd-x64": "0.27.2",
|
| 1063 |
+
"@esbuild/linux-arm": "0.27.2",
|
| 1064 |
+
"@esbuild/linux-arm64": "0.27.2",
|
| 1065 |
+
"@esbuild/linux-ia32": "0.27.2",
|
| 1066 |
+
"@esbuild/linux-loong64": "0.27.2",
|
| 1067 |
+
"@esbuild/linux-mips64el": "0.27.2",
|
| 1068 |
+
"@esbuild/linux-ppc64": "0.27.2",
|
| 1069 |
+
"@esbuild/linux-riscv64": "0.27.2",
|
| 1070 |
+
"@esbuild/linux-s390x": "0.27.2",
|
| 1071 |
+
"@esbuild/linux-x64": "0.27.2",
|
| 1072 |
+
"@esbuild/netbsd-arm64": "0.27.2",
|
| 1073 |
+
"@esbuild/netbsd-x64": "0.27.2",
|
| 1074 |
+
"@esbuild/openbsd-arm64": "0.27.2",
|
| 1075 |
+
"@esbuild/openbsd-x64": "0.27.2",
|
| 1076 |
+
"@esbuild/openharmony-arm64": "0.27.2",
|
| 1077 |
+
"@esbuild/sunos-x64": "0.27.2",
|
| 1078 |
+
"@esbuild/win32-arm64": "0.27.2",
|
| 1079 |
+
"@esbuild/win32-ia32": "0.27.2",
|
| 1080 |
+
"@esbuild/win32-x64": "0.27.2"
|
| 1081 |
+
}
|
| 1082 |
+
},
|
| 1083 |
+
"escalade": {
|
| 1084 |
+
"version": "3.2.0",
|
| 1085 |
+
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
|
| 1086 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 1087 |
+
"dev": true
|
| 1088 |
+
},
|
| 1089 |
+
"eventemitter3": {
|
| 1090 |
+
"version": "5.0.1",
|
| 1091 |
+
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
| 1092 |
+
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
|
| 1093 |
+
},
|
| 1094 |
+
"fast-glob": {
|
| 1095 |
+
"version": "3.3.3",
|
| 1096 |
+
"resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz",
|
| 1097 |
+
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
| 1098 |
+
"dev": true,
|
| 1099 |
+
"requires": {
|
| 1100 |
+
"@nodelib/fs.stat": "^2.0.2",
|
| 1101 |
+
"@nodelib/fs.walk": "^1.2.3",
|
| 1102 |
+
"glob-parent": "^5.1.2",
|
| 1103 |
+
"merge2": "^1.3.0",
|
| 1104 |
+
"micromatch": "^4.0.8"
|
| 1105 |
+
},
|
| 1106 |
+
"dependencies": {
|
| 1107 |
+
"glob-parent": {
|
| 1108 |
+
"version": "5.1.2",
|
| 1109 |
+
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz",
|
| 1110 |
+
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
| 1111 |
+
"dev": true,
|
| 1112 |
+
"requires": {
|
| 1113 |
+
"is-glob": "^4.0.1"
|
| 1114 |
+
}
|
| 1115 |
+
}
|
| 1116 |
+
}
|
| 1117 |
+
},
|
| 1118 |
+
"fastq": {
|
| 1119 |
+
"version": "1.20.1",
|
| 1120 |
+
"resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz",
|
| 1121 |
+
"integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
|
| 1122 |
+
"dev": true,
|
| 1123 |
+
"requires": {
|
| 1124 |
+
"reusify": "^1.0.4"
|
| 1125 |
+
}
|
| 1126 |
+
},
|
| 1127 |
+
"fdir": {
|
| 1128 |
+
"version": "6.5.0",
|
| 1129 |
+
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
|
| 1130 |
+
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
| 1131 |
+
"dev": true
|
| 1132 |
+
},
|
| 1133 |
+
"fill-range": {
|
| 1134 |
+
"version": "7.1.1",
|
| 1135 |
+
"resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz",
|
| 1136 |
+
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
| 1137 |
+
"dev": true,
|
| 1138 |
+
"requires": {
|
| 1139 |
+
"to-regex-range": "^5.0.1"
|
| 1140 |
+
}
|
| 1141 |
+
},
|
| 1142 |
+
"fraction.js": {
|
| 1143 |
+
"version": "5.3.4",
|
| 1144 |
+
"resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz",
|
| 1145 |
+
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
|
| 1146 |
+
"dev": true
|
| 1147 |
+
},
|
| 1148 |
+
"framer-motion": {
|
| 1149 |
+
"version": "11.18.2",
|
| 1150 |
+
"resolved": "https://registry.npmmirror.com/framer-motion/-/framer-motion-11.18.2.tgz",
|
| 1151 |
+
"integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==",
|
| 1152 |
+
"requires": {
|
| 1153 |
+
"motion-dom": "^11.18.1",
|
| 1154 |
+
"motion-utils": "^11.18.1",
|
| 1155 |
+
"tslib": "^2.4.0"
|
| 1156 |
+
}
|
| 1157 |
+
},
|
| 1158 |
+
"fsevents": {
|
| 1159 |
+
"version": "2.3.3",
|
| 1160 |
+
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
|
| 1161 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1162 |
+
"dev": true,
|
| 1163 |
+
"optional": true
|
| 1164 |
+
},
|
| 1165 |
+
"function-bind": {
|
| 1166 |
+
"version": "1.1.2",
|
| 1167 |
+
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
| 1168 |
+
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
| 1169 |
+
"dev": true
|
| 1170 |
+
},
|
| 1171 |
+
"gensync": {
|
| 1172 |
+
"version": "1.0.0-beta.2",
|
| 1173 |
+
"resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz",
|
| 1174 |
+
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
| 1175 |
+
"dev": true
|
| 1176 |
+
},
|
| 1177 |
+
"glob-parent": {
|
| 1178 |
+
"version": "6.0.2",
|
| 1179 |
+
"resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
|
| 1180 |
+
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
| 1181 |
+
"dev": true,
|
| 1182 |
+
"requires": {
|
| 1183 |
+
"is-glob": "^4.0.3"
|
| 1184 |
+
}
|
| 1185 |
+
},
|
| 1186 |
+
"hasown": {
|
| 1187 |
+
"version": "2.0.2",
|
| 1188 |
+
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
|
| 1189 |
+
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
| 1190 |
+
"dev": true,
|
| 1191 |
+
"requires": {
|
| 1192 |
+
"function-bind": "^1.1.2"
|
| 1193 |
+
}
|
| 1194 |
+
},
|
| 1195 |
+
"html-parse-stringify": {
|
| 1196 |
+
"version": "3.0.1",
|
| 1197 |
+
"resolved": "https://registry.npmmirror.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
| 1198 |
+
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
| 1199 |
+
"requires": {
|
| 1200 |
+
"void-elements": "3.1.0"
|
| 1201 |
+
}
|
| 1202 |
+
},
|
| 1203 |
+
"i18next": {
|
| 1204 |
+
"version": "25.7.3",
|
| 1205 |
+
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-25.7.3.tgz",
|
| 1206 |
+
"integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==",
|
| 1207 |
+
"requires": {
|
| 1208 |
+
"@babel/runtime": "^7.28.4"
|
| 1209 |
+
}
|
| 1210 |
+
},
|
| 1211 |
+
"i18next-browser-languagedetector": {
|
| 1212 |
+
"version": "8.2.0",
|
| 1213 |
+
"resolved": "https://registry.npmmirror.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
|
| 1214 |
+
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
|
| 1215 |
+
"requires": {
|
| 1216 |
+
"@babel/runtime": "^7.23.2"
|
| 1217 |
+
}
|
| 1218 |
+
},
|
| 1219 |
+
"immer": {
|
| 1220 |
+
"version": "10.2.0",
|
| 1221 |
+
"resolved": "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz",
|
| 1222 |
+
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="
|
| 1223 |
+
},
|
| 1224 |
+
"internmap": {
|
| 1225 |
+
"version": "2.0.3",
|
| 1226 |
+
"resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz",
|
| 1227 |
+
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="
|
| 1228 |
+
},
|
| 1229 |
+
"is-binary-path": {
|
| 1230 |
+
"version": "2.1.0",
|
| 1231 |
+
"resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
| 1232 |
+
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
| 1233 |
+
"dev": true,
|
| 1234 |
+
"requires": {
|
| 1235 |
+
"binary-extensions": "^2.0.0"
|
| 1236 |
+
}
|
| 1237 |
+
},
|
| 1238 |
+
"is-core-module": {
|
| 1239 |
+
"version": "2.16.1",
|
| 1240 |
+
"resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
|
| 1241 |
+
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
| 1242 |
+
"dev": true,
|
| 1243 |
+
"requires": {
|
| 1244 |
+
"hasown": "^2.0.2"
|
| 1245 |
+
}
|
| 1246 |
+
},
|
| 1247 |
+
"is-extglob": {
|
| 1248 |
+
"version": "2.1.1",
|
| 1249 |
+
"resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
|
| 1250 |
+
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
| 1251 |
+
"dev": true
|
| 1252 |
+
},
|
| 1253 |
+
"is-glob": {
|
| 1254 |
+
"version": "4.0.3",
|
| 1255 |
+
"resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
|
| 1256 |
+
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
| 1257 |
+
"dev": true,
|
| 1258 |
+
"requires": {
|
| 1259 |
+
"is-extglob": "^2.1.1"
|
| 1260 |
+
}
|
| 1261 |
+
},
|
| 1262 |
+
"is-number": {
|
| 1263 |
+
"version": "7.0.0",
|
| 1264 |
+
"resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
|
| 1265 |
+
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
| 1266 |
+
"dev": true
|
| 1267 |
+
},
|
| 1268 |
+
"jiti": {
|
| 1269 |
+
"version": "1.21.7",
|
| 1270 |
+
"resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz",
|
| 1271 |
+
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
| 1272 |
+
"dev": true
|
| 1273 |
+
},
|
| 1274 |
+
"js-tokens": {
|
| 1275 |
+
"version": "4.0.0",
|
| 1276 |
+
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 1277 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
| 1278 |
+
"dev": true
|
| 1279 |
+
},
|
| 1280 |
+
"jsesc": {
|
| 1281 |
+
"version": "3.1.0",
|
| 1282 |
+
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
|
| 1283 |
+
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 1284 |
+
"dev": true
|
| 1285 |
+
},
|
| 1286 |
+
"json5": {
|
| 1287 |
+
"version": "2.2.3",
|
| 1288 |
+
"resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
|
| 1289 |
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
| 1290 |
+
"dev": true
|
| 1291 |
+
},
|
| 1292 |
+
"lilconfig": {
|
| 1293 |
+
"version": "3.1.3",
|
| 1294 |
+
"resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz",
|
| 1295 |
+
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
| 1296 |
+
"dev": true
|
| 1297 |
+
},
|
| 1298 |
+
"lines-and-columns": {
|
| 1299 |
+
"version": "1.2.4",
|
| 1300 |
+
"resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
| 1301 |
+
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
| 1302 |
+
"dev": true
|
| 1303 |
+
},
|
| 1304 |
+
"lru-cache": {
|
| 1305 |
+
"version": "5.1.1",
|
| 1306 |
+
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
|
| 1307 |
+
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
| 1308 |
+
"dev": true,
|
| 1309 |
+
"requires": {
|
| 1310 |
+
"yallist": "^3.0.2"
|
| 1311 |
+
}
|
| 1312 |
+
},
|
| 1313 |
+
"lucide-react": {
|
| 1314 |
+
"version": "0.561.0",
|
| 1315 |
+
"resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.561.0.tgz",
|
| 1316 |
+
"integrity": "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A=="
|
| 1317 |
+
},
|
| 1318 |
+
"merge2": {
|
| 1319 |
+
"version": "1.4.1",
|
| 1320 |
+
"resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz",
|
| 1321 |
+
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
| 1322 |
+
"dev": true
|
| 1323 |
+
},
|
| 1324 |
+
"micromatch": {
|
| 1325 |
+
"version": "4.0.8",
|
| 1326 |
+
"resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz",
|
| 1327 |
+
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
| 1328 |
+
"dev": true,
|
| 1329 |
+
"requires": {
|
| 1330 |
+
"braces": "^3.0.3",
|
| 1331 |
+
"picomatch": "^2.3.1"
|
| 1332 |
+
}
|
| 1333 |
+
},
|
| 1334 |
+
"motion-dom": {
|
| 1335 |
+
"version": "11.18.1",
|
| 1336 |
+
"resolved": "https://registry.npmmirror.com/motion-dom/-/motion-dom-11.18.1.tgz",
|
| 1337 |
+
"integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==",
|
| 1338 |
+
"requires": {
|
| 1339 |
+
"motion-utils": "^11.18.1"
|
| 1340 |
+
}
|
| 1341 |
+
},
|
| 1342 |
+
"motion-utils": {
|
| 1343 |
+
"version": "11.18.1",
|
| 1344 |
+
"resolved": "https://registry.npmmirror.com/motion-utils/-/motion-utils-11.18.1.tgz",
|
| 1345 |
+
"integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA=="
|
| 1346 |
+
},
|
| 1347 |
+
"ms": {
|
| 1348 |
+
"version": "2.1.3",
|
| 1349 |
+
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
| 1350 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1351 |
+
"dev": true
|
| 1352 |
+
},
|
| 1353 |
+
"mz": {
|
| 1354 |
+
"version": "2.7.0",
|
| 1355 |
+
"resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz",
|
| 1356 |
+
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
| 1357 |
+
"dev": true,
|
| 1358 |
+
"requires": {
|
| 1359 |
+
"any-promise": "^1.0.0",
|
| 1360 |
+
"object-assign": "^4.0.1",
|
| 1361 |
+
"thenify-all": "^1.0.0"
|
| 1362 |
+
}
|
| 1363 |
+
},
|
| 1364 |
+
"nanoid": {
|
| 1365 |
+
"version": "3.3.11",
|
| 1366 |
+
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
| 1367 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 1368 |
+
"dev": true
|
| 1369 |
+
},
|
| 1370 |
+
"node-releases": {
|
| 1371 |
+
"version": "2.0.27",
|
| 1372 |
+
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz",
|
| 1373 |
+
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
| 1374 |
+
"dev": true
|
| 1375 |
+
},
|
| 1376 |
+
"normalize-path": {
|
| 1377 |
+
"version": "3.0.0",
|
| 1378 |
+
"resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
|
| 1379 |
+
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
| 1380 |
+
"dev": true
|
| 1381 |
+
},
|
| 1382 |
+
"object-assign": {
|
| 1383 |
+
"version": "4.1.1",
|
| 1384 |
+
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
|
| 1385 |
+
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
| 1386 |
+
"dev": true
|
| 1387 |
+
},
|
| 1388 |
+
"object-hash": {
|
| 1389 |
+
"version": "3.0.0",
|
| 1390 |
+
"resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz",
|
| 1391 |
+
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
| 1392 |
+
"dev": true
|
| 1393 |
+
},
|
| 1394 |
+
"path-parse": {
|
| 1395 |
+
"version": "1.0.7",
|
| 1396 |
+
"resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz",
|
| 1397 |
+
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
| 1398 |
+
"dev": true
|
| 1399 |
+
},
|
| 1400 |
+
"picocolors": {
|
| 1401 |
+
"version": "1.1.1",
|
| 1402 |
+
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
| 1403 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 1404 |
+
"dev": true
|
| 1405 |
+
},
|
| 1406 |
+
"picomatch": {
|
| 1407 |
+
"version": "2.3.1",
|
| 1408 |
+
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz",
|
| 1409 |
+
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
| 1410 |
+
"dev": true
|
| 1411 |
+
},
|
| 1412 |
+
"pify": {
|
| 1413 |
+
"version": "2.3.0",
|
| 1414 |
+
"resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz",
|
| 1415 |
+
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
| 1416 |
+
"dev": true
|
| 1417 |
+
},
|
| 1418 |
+
"pirates": {
|
| 1419 |
+
"version": "4.0.7",
|
| 1420 |
+
"resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz",
|
| 1421 |
+
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
|
| 1422 |
+
"dev": true
|
| 1423 |
+
},
|
| 1424 |
+
"postcss": {
|
| 1425 |
+
"version": "8.5.6",
|
| 1426 |
+
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
|
| 1427 |
+
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
| 1428 |
+
"dev": true,
|
| 1429 |
+
"requires": {
|
| 1430 |
+
"nanoid": "^3.3.11",
|
| 1431 |
+
"picocolors": "^1.1.1",
|
| 1432 |
+
"source-map-js": "^1.2.1"
|
| 1433 |
+
}
|
| 1434 |
+
},
|
| 1435 |
+
"postcss-import": {
|
| 1436 |
+
"version": "15.1.0",
|
| 1437 |
+
"resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz",
|
| 1438 |
+
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
| 1439 |
+
"dev": true,
|
| 1440 |
+
"requires": {
|
| 1441 |
+
"postcss-value-parser": "^4.0.0",
|
| 1442 |
+
"read-cache": "^1.0.0",
|
| 1443 |
+
"resolve": "^1.1.7"
|
| 1444 |
+
}
|
| 1445 |
+
},
|
| 1446 |
+
"postcss-js": {
|
| 1447 |
+
"version": "4.1.0",
|
| 1448 |
+
"resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz",
|
| 1449 |
+
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
|
| 1450 |
+
"dev": true,
|
| 1451 |
+
"requires": {
|
| 1452 |
+
"camelcase-css": "^2.0.1"
|
| 1453 |
+
}
|
| 1454 |
+
},
|
| 1455 |
+
"postcss-load-config": {
|
| 1456 |
+
"version": "6.0.1",
|
| 1457 |
+
"resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
| 1458 |
+
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
| 1459 |
+
"dev": true,
|
| 1460 |
+
"requires": {
|
| 1461 |
+
"lilconfig": "^3.1.1"
|
| 1462 |
+
}
|
| 1463 |
+
},
|
| 1464 |
+
"postcss-nested": {
|
| 1465 |
+
"version": "6.2.0",
|
| 1466 |
+
"resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
| 1467 |
+
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
| 1468 |
+
"dev": true,
|
| 1469 |
+
"requires": {
|
| 1470 |
+
"postcss-selector-parser": "^6.1.1"
|
| 1471 |
+
}
|
| 1472 |
+
},
|
| 1473 |
+
"postcss-selector-parser": {
|
| 1474 |
+
"version": "6.1.2",
|
| 1475 |
+
"resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
| 1476 |
+
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
| 1477 |
+
"dev": true,
|
| 1478 |
+
"requires": {
|
| 1479 |
+
"cssesc": "^3.0.0",
|
| 1480 |
+
"util-deprecate": "^1.0.2"
|
| 1481 |
+
}
|
| 1482 |
+
},
|
| 1483 |
+
"postcss-value-parser": {
|
| 1484 |
+
"version": "4.2.0",
|
| 1485 |
+
"resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
| 1486 |
+
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
| 1487 |
+
"dev": true
|
| 1488 |
+
},
|
| 1489 |
+
"queue-microtask": {
|
| 1490 |
+
"version": "1.2.3",
|
| 1491 |
+
"resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
| 1492 |
+
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
| 1493 |
+
"dev": true
|
| 1494 |
+
},
|
| 1495 |
+
"react": {
|
| 1496 |
+
"version": "19.2.3",
|
| 1497 |
+
"resolved": "https://registry.npmmirror.com/react/-/react-19.2.3.tgz",
|
| 1498 |
+
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="
|
| 1499 |
+
},
|
| 1500 |
+
"react-dom": {
|
| 1501 |
+
"version": "19.2.3",
|
| 1502 |
+
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.3.tgz",
|
| 1503 |
+
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
| 1504 |
+
"requires": {
|
| 1505 |
+
"scheduler": "^0.27.0"
|
| 1506 |
+
}
|
| 1507 |
+
},
|
| 1508 |
+
"react-i18next": {
|
| 1509 |
+
"version": "16.5.0",
|
| 1510 |
+
"resolved": "https://registry.npmmirror.com/react-i18next/-/react-i18next-16.5.0.tgz",
|
| 1511 |
+
"integrity": "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw==",
|
| 1512 |
+
"requires": {
|
| 1513 |
+
"@babel/runtime": "^7.27.6",
|
| 1514 |
+
"html-parse-stringify": "^3.0.1",
|
| 1515 |
+
"use-sync-external-store": "^1.6.0"
|
| 1516 |
+
}
|
| 1517 |
+
},
|
| 1518 |
+
"react-redux": {
|
| 1519 |
+
"version": "9.2.0",
|
| 1520 |
+
"resolved": "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz",
|
| 1521 |
+
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
| 1522 |
+
"requires": {
|
| 1523 |
+
"@types/use-sync-external-store": "^0.0.6",
|
| 1524 |
+
"use-sync-external-store": "^1.4.0"
|
| 1525 |
+
}
|
| 1526 |
+
},
|
| 1527 |
+
"react-refresh": {
|
| 1528 |
+
"version": "0.17.0",
|
| 1529 |
+
"resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz",
|
| 1530 |
+
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
| 1531 |
+
"dev": true
|
| 1532 |
+
},
|
| 1533 |
+
"react-router": {
|
| 1534 |
+
"version": "7.11.0",
|
| 1535 |
+
"resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.11.0.tgz",
|
| 1536 |
+
"integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==",
|
| 1537 |
+
"requires": {
|
| 1538 |
+
"cookie": "^1.0.1",
|
| 1539 |
+
"set-cookie-parser": "^2.6.0"
|
| 1540 |
+
}
|
| 1541 |
+
},
|
| 1542 |
+
"react-router-dom": {
|
| 1543 |
+
"version": "7.11.0",
|
| 1544 |
+
"resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.11.0.tgz",
|
| 1545 |
+
"integrity": "sha512-e49Ir/kMGRzFOOrYQBdoitq3ULigw4lKbAyKusnvtDu2t4dBX4AGYPrzNvorXmVuOyeakai6FUPW5MmibvVG8g==",
|
| 1546 |
+
"requires": {
|
| 1547 |
+
"react-router": "7.11.0"
|
| 1548 |
+
}
|
| 1549 |
+
},
|
| 1550 |
+
"read-cache": {
|
| 1551 |
+
"version": "1.0.0",
|
| 1552 |
+
"resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz",
|
| 1553 |
+
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
| 1554 |
+
"dev": true,
|
| 1555 |
+
"requires": {
|
| 1556 |
+
"pify": "^2.3.0"
|
| 1557 |
+
}
|
| 1558 |
+
},
|
| 1559 |
+
"readdirp": {
|
| 1560 |
+
"version": "3.6.0",
|
| 1561 |
+
"resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz",
|
| 1562 |
+
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
| 1563 |
+
"dev": true,
|
| 1564 |
+
"requires": {
|
| 1565 |
+
"picomatch": "^2.2.1"
|
| 1566 |
+
}
|
| 1567 |
+
},
|
| 1568 |
+
"recharts": {
|
| 1569 |
+
"version": "3.6.0",
|
| 1570 |
+
"resolved": "https://registry.npmmirror.com/recharts/-/recharts-3.6.0.tgz",
|
| 1571 |
+
"integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==",
|
| 1572 |
+
"requires": {
|
| 1573 |
+
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
| 1574 |
+
"clsx": "^2.1.1",
|
| 1575 |
+
"decimal.js-light": "^2.5.1",
|
| 1576 |
+
"es-toolkit": "^1.39.3",
|
| 1577 |
+
"eventemitter3": "^5.0.1",
|
| 1578 |
+
"immer": "^10.1.1",
|
| 1579 |
+
"react-redux": "8.x.x || 9.x.x",
|
| 1580 |
+
"reselect": "5.1.1",
|
| 1581 |
+
"tiny-invariant": "^1.3.3",
|
| 1582 |
+
"use-sync-external-store": "^1.2.2",
|
| 1583 |
+
"victory-vendor": "^37.0.2"
|
| 1584 |
+
}
|
| 1585 |
+
},
|
| 1586 |
+
"redux": {
|
| 1587 |
+
"version": "5.0.1",
|
| 1588 |
+
"resolved": "https://registry.npmmirror.com/redux/-/redux-5.0.1.tgz",
|
| 1589 |
+
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
|
| 1590 |
+
},
|
| 1591 |
+
"redux-thunk": {
|
| 1592 |
+
"version": "3.1.0",
|
| 1593 |
+
"resolved": "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
| 1594 |
+
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="
|
| 1595 |
+
},
|
| 1596 |
+
"reselect": {
|
| 1597 |
+
"version": "5.1.1",
|
| 1598 |
+
"resolved": "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz",
|
| 1599 |
+
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
|
| 1600 |
+
},
|
| 1601 |
+
"resolve": {
|
| 1602 |
+
"version": "1.22.11",
|
| 1603 |
+
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
|
| 1604 |
+
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
| 1605 |
+
"dev": true,
|
| 1606 |
+
"requires": {
|
| 1607 |
+
"is-core-module": "^2.16.1",
|
| 1608 |
+
"path-parse": "^1.0.7",
|
| 1609 |
+
"supports-preserve-symlinks-flag": "^1.0.0"
|
| 1610 |
+
}
|
| 1611 |
+
},
|
| 1612 |
+
"reusify": {
|
| 1613 |
+
"version": "1.1.0",
|
| 1614 |
+
"resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz",
|
| 1615 |
+
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
| 1616 |
+
"dev": true
|
| 1617 |
+
},
|
| 1618 |
+
"rollup": {
|
| 1619 |
+
"version": "4.54.0",
|
| 1620 |
+
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.54.0.tgz",
|
| 1621 |
+
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
| 1622 |
+
"dev": true,
|
| 1623 |
+
"requires": {
|
| 1624 |
+
"@rollup/rollup-android-arm-eabi": "4.54.0",
|
| 1625 |
+
"@rollup/rollup-android-arm64": "4.54.0",
|
| 1626 |
+
"@rollup/rollup-darwin-arm64": "4.54.0",
|
| 1627 |
+
"@rollup/rollup-darwin-x64": "4.54.0",
|
| 1628 |
+
"@rollup/rollup-freebsd-arm64": "4.54.0",
|
| 1629 |
+
"@rollup/rollup-freebsd-x64": "4.54.0",
|
| 1630 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
|
| 1631 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
|
| 1632 |
+
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
|
| 1633 |
+
"@rollup/rollup-linux-arm64-musl": "4.54.0",
|
| 1634 |
+
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
|
| 1635 |
+
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
|
| 1636 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
|
| 1637 |
+
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
|
| 1638 |
+
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
|
| 1639 |
+
"@rollup/rollup-linux-x64-gnu": "4.54.0",
|
| 1640 |
+
"@rollup/rollup-linux-x64-musl": "4.54.0",
|
| 1641 |
+
"@rollup/rollup-openharmony-arm64": "4.54.0",
|
| 1642 |
+
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
|
| 1643 |
+
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
|
| 1644 |
+
"@rollup/rollup-win32-x64-gnu": "4.54.0",
|
| 1645 |
+
"@rollup/rollup-win32-x64-msvc": "4.54.0",
|
| 1646 |
+
"@types/estree": "1.0.8",
|
| 1647 |
+
"fsevents": "~2.3.2"
|
| 1648 |
+
}
|
| 1649 |
+
},
|
| 1650 |
+
"run-parallel": {
|
| 1651 |
+
"version": "1.2.0",
|
| 1652 |
+
"resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz",
|
| 1653 |
+
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
| 1654 |
+
"dev": true,
|
| 1655 |
+
"requires": {
|
| 1656 |
+
"queue-microtask": "^1.2.2"
|
| 1657 |
+
}
|
| 1658 |
+
},
|
| 1659 |
+
"scheduler": {
|
| 1660 |
+
"version": "0.27.0",
|
| 1661 |
+
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz",
|
| 1662 |
+
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="
|
| 1663 |
+
},
|
| 1664 |
+
"semver": {
|
| 1665 |
+
"version": "6.3.1",
|
| 1666 |
+
"resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
|
| 1667 |
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
| 1668 |
+
"dev": true
|
| 1669 |
+
},
|
| 1670 |
+
"set-cookie-parser": {
|
| 1671 |
+
"version": "2.7.2",
|
| 1672 |
+
"resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
| 1673 |
+
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="
|
| 1674 |
+
},
|
| 1675 |
+
"source-map-js": {
|
| 1676 |
+
"version": "1.2.1",
|
| 1677 |
+
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 1678 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 1679 |
+
"dev": true
|
| 1680 |
+
},
|
| 1681 |
+
"sucrase": {
|
| 1682 |
+
"version": "3.35.1",
|
| 1683 |
+
"resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz",
|
| 1684 |
+
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
|
| 1685 |
+
"dev": true,
|
| 1686 |
+
"requires": {
|
| 1687 |
+
"@jridgewell/gen-mapping": "^0.3.2",
|
| 1688 |
+
"commander": "^4.0.0",
|
| 1689 |
+
"lines-and-columns": "^1.1.6",
|
| 1690 |
+
"mz": "^2.7.0",
|
| 1691 |
+
"pirates": "^4.0.1",
|
| 1692 |
+
"tinyglobby": "^0.2.11",
|
| 1693 |
+
"ts-interface-checker": "^0.1.9"
|
| 1694 |
+
}
|
| 1695 |
+
},
|
| 1696 |
+
"supports-preserve-symlinks-flag": {
|
| 1697 |
+
"version": "1.0.0",
|
| 1698 |
+
"resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
| 1699 |
+
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
| 1700 |
+
"dev": true
|
| 1701 |
+
},
|
| 1702 |
+
"tailwind-merge": {
|
| 1703 |
+
"version": "2.6.0",
|
| 1704 |
+
"resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
| 1705 |
+
"integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="
|
| 1706 |
+
},
|
| 1707 |
+
"tailwindcss": {
|
| 1708 |
+
"version": "3.4.19",
|
| 1709 |
+
"resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz",
|
| 1710 |
+
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
| 1711 |
+
"dev": true,
|
| 1712 |
+
"requires": {
|
| 1713 |
+
"@alloc/quick-lru": "^5.2.0",
|
| 1714 |
+
"arg": "^5.0.2",
|
| 1715 |
+
"chokidar": "^3.6.0",
|
| 1716 |
+
"didyoumean": "^1.2.2",
|
| 1717 |
+
"dlv": "^1.1.3",
|
| 1718 |
+
"fast-glob": "^3.3.2",
|
| 1719 |
+
"glob-parent": "^6.0.2",
|
| 1720 |
+
"is-glob": "^4.0.3",
|
| 1721 |
+
"jiti": "^1.21.7",
|
| 1722 |
+
"lilconfig": "^3.1.3",
|
| 1723 |
+
"micromatch": "^4.0.8",
|
| 1724 |
+
"normalize-path": "^3.0.0",
|
| 1725 |
+
"object-hash": "^3.0.0",
|
| 1726 |
+
"picocolors": "^1.1.1",
|
| 1727 |
+
"postcss": "^8.4.47",
|
| 1728 |
+
"postcss-import": "^15.1.0",
|
| 1729 |
+
"postcss-js": "^4.0.1",
|
| 1730 |
+
"postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
|
| 1731 |
+
"postcss-nested": "^6.2.0",
|
| 1732 |
+
"postcss-selector-parser": "^6.1.2",
|
| 1733 |
+
"resolve": "^1.22.8",
|
| 1734 |
+
"sucrase": "^3.35.0"
|
| 1735 |
+
}
|
| 1736 |
+
},
|
| 1737 |
+
"thenify": {
|
| 1738 |
+
"version": "3.3.1",
|
| 1739 |
+
"resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz",
|
| 1740 |
+
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
| 1741 |
+
"dev": true,
|
| 1742 |
+
"requires": {
|
| 1743 |
+
"any-promise": "^1.0.0"
|
| 1744 |
+
}
|
| 1745 |
+
},
|
| 1746 |
+
"thenify-all": {
|
| 1747 |
+
"version": "1.6.0",
|
| 1748 |
+
"resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz",
|
| 1749 |
+
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
| 1750 |
+
"dev": true,
|
| 1751 |
+
"requires": {
|
| 1752 |
+
"thenify": ">= 3.1.0 < 4"
|
| 1753 |
+
}
|
| 1754 |
+
},
|
| 1755 |
+
"tiny-invariant": {
|
| 1756 |
+
"version": "1.3.3",
|
| 1757 |
+
"resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
| 1758 |
+
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="
|
| 1759 |
+
},
|
| 1760 |
+
"tinyglobby": {
|
| 1761 |
+
"version": "0.2.15",
|
| 1762 |
+
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
| 1763 |
+
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
| 1764 |
+
"dev": true,
|
| 1765 |
+
"requires": {
|
| 1766 |
+
"fdir": "^6.5.0",
|
| 1767 |
+
"picomatch": "^4.0.3"
|
| 1768 |
+
},
|
| 1769 |
+
"dependencies": {
|
| 1770 |
+
"picomatch": {
|
| 1771 |
+
"version": "4.0.3",
|
| 1772 |
+
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
|
| 1773 |
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 1774 |
+
"dev": true
|
| 1775 |
+
}
|
| 1776 |
+
}
|
| 1777 |
+
},
|
| 1778 |
+
"to-regex-range": {
|
| 1779 |
+
"version": "5.0.1",
|
| 1780 |
+
"resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
| 1781 |
+
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
| 1782 |
+
"dev": true,
|
| 1783 |
+
"requires": {
|
| 1784 |
+
"is-number": "^7.0.0"
|
| 1785 |
+
}
|
| 1786 |
+
},
|
| 1787 |
+
"ts-interface-checker": {
|
| 1788 |
+
"version": "0.1.13",
|
| 1789 |
+
"resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
| 1790 |
+
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
| 1791 |
+
"dev": true
|
| 1792 |
+
},
|
| 1793 |
+
"tslib": {
|
| 1794 |
+
"version": "2.8.1",
|
| 1795 |
+
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
|
| 1796 |
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
| 1797 |
+
},
|
| 1798 |
+
"typescript": {
|
| 1799 |
+
"version": "5.8.3",
|
| 1800 |
+
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz",
|
| 1801 |
+
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
| 1802 |
+
"dev": true
|
| 1803 |
+
},
|
| 1804 |
+
"update-browserslist-db": {
|
| 1805 |
+
"version": "1.2.3",
|
| 1806 |
+
"resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
| 1807 |
+
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
| 1808 |
+
"dev": true,
|
| 1809 |
+
"requires": {
|
| 1810 |
+
"escalade": "^3.2.0",
|
| 1811 |
+
"picocolors": "^1.1.1"
|
| 1812 |
+
}
|
| 1813 |
+
},
|
| 1814 |
+
"use-sync-external-store": {
|
| 1815 |
+
"version": "1.6.0",
|
| 1816 |
+
"resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
| 1817 |
+
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="
|
| 1818 |
+
},
|
| 1819 |
+
"util-deprecate": {
|
| 1820 |
+
"version": "1.0.2",
|
| 1821 |
+
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
| 1822 |
+
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
| 1823 |
+
"dev": true
|
| 1824 |
+
},
|
| 1825 |
+
"victory-vendor": {
|
| 1826 |
+
"version": "37.3.6",
|
| 1827 |
+
"resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
| 1828 |
+
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
| 1829 |
+
"requires": {
|
| 1830 |
+
"@types/d3-array": "^3.0.3",
|
| 1831 |
+
"@types/d3-ease": "^3.0.0",
|
| 1832 |
+
"@types/d3-interpolate": "^3.0.1",
|
| 1833 |
+
"@types/d3-scale": "^4.0.2",
|
| 1834 |
+
"@types/d3-shape": "^3.1.0",
|
| 1835 |
+
"@types/d3-time": "^3.0.0",
|
| 1836 |
+
"@types/d3-timer": "^3.0.0",
|
| 1837 |
+
"d3-array": "^3.1.6",
|
| 1838 |
+
"d3-ease": "^3.0.1",
|
| 1839 |
+
"d3-interpolate": "^3.0.1",
|
| 1840 |
+
"d3-scale": "^4.0.2",
|
| 1841 |
+
"d3-shape": "^3.1.0",
|
| 1842 |
+
"d3-time": "^3.0.0",
|
| 1843 |
+
"d3-timer": "^3.0.1"
|
| 1844 |
+
}
|
| 1845 |
+
},
|
| 1846 |
+
"vite": {
|
| 1847 |
+
"version": "7.3.0",
|
| 1848 |
+
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.0.tgz",
|
| 1849 |
+
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
| 1850 |
+
"dev": true,
|
| 1851 |
+
"requires": {
|
| 1852 |
+
"esbuild": "^0.27.0",
|
| 1853 |
+
"fdir": "^6.5.0",
|
| 1854 |
+
"fsevents": "~2.3.3",
|
| 1855 |
+
"picomatch": "^4.0.3",
|
| 1856 |
+
"postcss": "^8.5.6",
|
| 1857 |
+
"rollup": "^4.43.0",
|
| 1858 |
+
"tinyglobby": "^0.2.15"
|
| 1859 |
+
},
|
| 1860 |
+
"dependencies": {
|
| 1861 |
+
"picomatch": {
|
| 1862 |
+
"version": "4.0.3",
|
| 1863 |
+
"resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz",
|
| 1864 |
+
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
| 1865 |
+
"dev": true
|
| 1866 |
+
}
|
| 1867 |
+
}
|
| 1868 |
+
},
|
| 1869 |
+
"void-elements": {
|
| 1870 |
+
"version": "3.1.0",
|
| 1871 |
+
"resolved": "https://registry.npmmirror.com/void-elements/-/void-elements-3.1.0.tgz",
|
| 1872 |
+
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="
|
| 1873 |
+
},
|
| 1874 |
+
"yallist": {
|
| 1875 |
+
"version": "3.1.1",
|
| 1876 |
+
"resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
|
| 1877 |
+
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
| 1878 |
+
"dev": true
|
| 1879 |
+
},
|
| 1880 |
+
"zustand": {
|
| 1881 |
+
"version": "5.0.9",
|
| 1882 |
+
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.9.tgz",
|
| 1883 |
+
"integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg=="
|
| 1884 |
+
}
|
| 1885 |
+
}
|
| 1886 |
+
}
|
package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "antigravity-tools-cloud",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"description": "Cloud-deployed API proxy for AI services (HuggingFace Spaces)",
|
| 7 |
+
"scripts": {
|
| 8 |
+
"dev": "vite",
|
| 9 |
+
"build": "tsc && vite build",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"clsx": "^2.1.1",
|
| 14 |
+
"daisyui": "^5.5.13",
|
| 15 |
+
"date-fns": "^4.1.0",
|
| 16 |
+
"framer-motion": "^11.13.1",
|
| 17 |
+
"i18next": "^25.7.2",
|
| 18 |
+
"i18next-browser-languagedetector": "^8.2.0",
|
| 19 |
+
"lucide-react": "^0.561.0",
|
| 20 |
+
"react": "^19.1.0",
|
| 21 |
+
"react-dom": "^19.1.0",
|
| 22 |
+
"react-i18next": "^16.5.0",
|
| 23 |
+
"react-router-dom": "^7.10.1",
|
| 24 |
+
"recharts": "^3.5.1",
|
| 25 |
+
"tailwind-merge": "^2.3.0",
|
| 26 |
+
"zustand": "^5.0.9"
|
| 27 |
+
},
|
| 28 |
+
"devDependencies": {
|
| 29 |
+
"@types/react": "^19.1.8",
|
| 30 |
+
"@types/react-dom": "^19.1.6",
|
| 31 |
+
"@vitejs/plugin-react": "^4.6.0",
|
| 32 |
+
"autoprefixer": "^10.4.22",
|
| 33 |
+
"postcss": "^8.5.6",
|
| 34 |
+
"tailwindcss": "^3.4.19",
|
| 35 |
+
"typescript": "~5.8.3",
|
| 36 |
+
"vite": "^7.0.4"
|
| 37 |
+
}
|
| 38 |
+
}
|
postcss.config.cjs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
server/Cargo.lock
ADDED
|
@@ -0,0 +1,2475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This file is automatically @generated by Cargo.
|
| 2 |
+
# It is not intended for manual editing.
|
| 3 |
+
version = 4
|
| 4 |
+
|
| 5 |
+
[[package]]
|
| 6 |
+
name = "aho-corasick"
|
| 7 |
+
version = "1.1.4"
|
| 8 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 9 |
+
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"memchr",
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
[[package]]
|
| 15 |
+
name = "android_system_properties"
|
| 16 |
+
version = "0.1.5"
|
| 17 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 18 |
+
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
| 19 |
+
dependencies = [
|
| 20 |
+
"libc",
|
| 21 |
+
]
|
| 22 |
+
|
| 23 |
+
[[package]]
|
| 24 |
+
name = "antigravity-server"
|
| 25 |
+
version = "1.0.0"
|
| 26 |
+
dependencies = [
|
| 27 |
+
"anyhow",
|
| 28 |
+
"async-stream",
|
| 29 |
+
"axum",
|
| 30 |
+
"base64",
|
| 31 |
+
"bytes",
|
| 32 |
+
"chrono",
|
| 33 |
+
"dashmap",
|
| 34 |
+
"dirs",
|
| 35 |
+
"eventsource-stream",
|
| 36 |
+
"futures",
|
| 37 |
+
"hyper",
|
| 38 |
+
"hyper-util",
|
| 39 |
+
"once_cell",
|
| 40 |
+
"pin-project",
|
| 41 |
+
"rand",
|
| 42 |
+
"regex",
|
| 43 |
+
"reqwest",
|
| 44 |
+
"serde",
|
| 45 |
+
"serde_json",
|
| 46 |
+
"thiserror 2.0.17",
|
| 47 |
+
"tokio",
|
| 48 |
+
"tower 0.4.13",
|
| 49 |
+
"tower-http 0.5.2",
|
| 50 |
+
"tracing",
|
| 51 |
+
"tracing-appender",
|
| 52 |
+
"tracing-log",
|
| 53 |
+
"tracing-subscriber",
|
| 54 |
+
"url",
|
| 55 |
+
"uuid",
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
[[package]]
|
| 59 |
+
name = "anyhow"
|
| 60 |
+
version = "1.0.100"
|
| 61 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 62 |
+
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
| 63 |
+
|
| 64 |
+
[[package]]
|
| 65 |
+
name = "async-stream"
|
| 66 |
+
version = "0.3.6"
|
| 67 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 68 |
+
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
|
| 69 |
+
dependencies = [
|
| 70 |
+
"async-stream-impl",
|
| 71 |
+
"futures-core",
|
| 72 |
+
"pin-project-lite",
|
| 73 |
+
]
|
| 74 |
+
|
| 75 |
+
[[package]]
|
| 76 |
+
name = "async-stream-impl"
|
| 77 |
+
version = "0.3.6"
|
| 78 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 79 |
+
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
|
| 80 |
+
dependencies = [
|
| 81 |
+
"proc-macro2",
|
| 82 |
+
"quote",
|
| 83 |
+
"syn",
|
| 84 |
+
]
|
| 85 |
+
|
| 86 |
+
[[package]]
|
| 87 |
+
name = "async-trait"
|
| 88 |
+
version = "0.1.89"
|
| 89 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 90 |
+
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
| 91 |
+
dependencies = [
|
| 92 |
+
"proc-macro2",
|
| 93 |
+
"quote",
|
| 94 |
+
"syn",
|
| 95 |
+
]
|
| 96 |
+
|
| 97 |
+
[[package]]
|
| 98 |
+
name = "atomic-waker"
|
| 99 |
+
version = "1.1.2"
|
| 100 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 101 |
+
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
| 102 |
+
|
| 103 |
+
[[package]]
|
| 104 |
+
name = "autocfg"
|
| 105 |
+
version = "1.5.0"
|
| 106 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 107 |
+
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
| 108 |
+
|
| 109 |
+
[[package]]
|
| 110 |
+
name = "axum"
|
| 111 |
+
version = "0.7.9"
|
| 112 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 113 |
+
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
| 114 |
+
dependencies = [
|
| 115 |
+
"async-trait",
|
| 116 |
+
"axum-core",
|
| 117 |
+
"axum-macros",
|
| 118 |
+
"bytes",
|
| 119 |
+
"futures-util",
|
| 120 |
+
"http",
|
| 121 |
+
"http-body",
|
| 122 |
+
"http-body-util",
|
| 123 |
+
"hyper",
|
| 124 |
+
"hyper-util",
|
| 125 |
+
"itoa",
|
| 126 |
+
"matchit",
|
| 127 |
+
"memchr",
|
| 128 |
+
"mime",
|
| 129 |
+
"percent-encoding",
|
| 130 |
+
"pin-project-lite",
|
| 131 |
+
"rustversion",
|
| 132 |
+
"serde",
|
| 133 |
+
"serde_json",
|
| 134 |
+
"serde_path_to_error",
|
| 135 |
+
"serde_urlencoded",
|
| 136 |
+
"sync_wrapper",
|
| 137 |
+
"tokio",
|
| 138 |
+
"tower 0.5.2",
|
| 139 |
+
"tower-layer",
|
| 140 |
+
"tower-service",
|
| 141 |
+
"tracing",
|
| 142 |
+
]
|
| 143 |
+
|
| 144 |
+
[[package]]
|
| 145 |
+
name = "axum-core"
|
| 146 |
+
version = "0.4.5"
|
| 147 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 148 |
+
checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
|
| 149 |
+
dependencies = [
|
| 150 |
+
"async-trait",
|
| 151 |
+
"bytes",
|
| 152 |
+
"futures-util",
|
| 153 |
+
"http",
|
| 154 |
+
"http-body",
|
| 155 |
+
"http-body-util",
|
| 156 |
+
"mime",
|
| 157 |
+
"pin-project-lite",
|
| 158 |
+
"rustversion",
|
| 159 |
+
"sync_wrapper",
|
| 160 |
+
"tower-layer",
|
| 161 |
+
"tower-service",
|
| 162 |
+
"tracing",
|
| 163 |
+
]
|
| 164 |
+
|
| 165 |
+
[[package]]
|
| 166 |
+
name = "axum-macros"
|
| 167 |
+
version = "0.4.2"
|
| 168 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 169 |
+
checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce"
|
| 170 |
+
dependencies = [
|
| 171 |
+
"proc-macro2",
|
| 172 |
+
"quote",
|
| 173 |
+
"syn",
|
| 174 |
+
]
|
| 175 |
+
|
| 176 |
+
[[package]]
|
| 177 |
+
name = "base64"
|
| 178 |
+
version = "0.22.1"
|
| 179 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 180 |
+
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
| 181 |
+
|
| 182 |
+
[[package]]
|
| 183 |
+
name = "bitflags"
|
| 184 |
+
version = "2.10.0"
|
| 185 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 186 |
+
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
| 187 |
+
|
| 188 |
+
[[package]]
|
| 189 |
+
name = "bumpalo"
|
| 190 |
+
version = "3.19.1"
|
| 191 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 192 |
+
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
| 193 |
+
|
| 194 |
+
[[package]]
|
| 195 |
+
name = "bytes"
|
| 196 |
+
version = "1.11.0"
|
| 197 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 198 |
+
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
| 199 |
+
|
| 200 |
+
[[package]]
|
| 201 |
+
name = "cc"
|
| 202 |
+
version = "1.2.51"
|
| 203 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 204 |
+
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
|
| 205 |
+
dependencies = [
|
| 206 |
+
"find-msvc-tools",
|
| 207 |
+
"shlex",
|
| 208 |
+
]
|
| 209 |
+
|
| 210 |
+
[[package]]
|
| 211 |
+
name = "cfg-if"
|
| 212 |
+
version = "1.0.4"
|
| 213 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 214 |
+
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
| 215 |
+
|
| 216 |
+
[[package]]
|
| 217 |
+
name = "chrono"
|
| 218 |
+
version = "0.4.42"
|
| 219 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 220 |
+
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
| 221 |
+
dependencies = [
|
| 222 |
+
"iana-time-zone",
|
| 223 |
+
"js-sys",
|
| 224 |
+
"num-traits",
|
| 225 |
+
"serde",
|
| 226 |
+
"wasm-bindgen",
|
| 227 |
+
"windows-link",
|
| 228 |
+
]
|
| 229 |
+
|
| 230 |
+
[[package]]
|
| 231 |
+
name = "core-foundation"
|
| 232 |
+
version = "0.9.4"
|
| 233 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 234 |
+
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
| 235 |
+
dependencies = [
|
| 236 |
+
"core-foundation-sys",
|
| 237 |
+
"libc",
|
| 238 |
+
]
|
| 239 |
+
|
| 240 |
+
[[package]]
|
| 241 |
+
name = "core-foundation-sys"
|
| 242 |
+
version = "0.8.7"
|
| 243 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 244 |
+
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
| 245 |
+
|
| 246 |
+
[[package]]
|
| 247 |
+
name = "crossbeam-channel"
|
| 248 |
+
version = "0.5.15"
|
| 249 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 250 |
+
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
| 251 |
+
dependencies = [
|
| 252 |
+
"crossbeam-utils",
|
| 253 |
+
]
|
| 254 |
+
|
| 255 |
+
[[package]]
|
| 256 |
+
name = "crossbeam-utils"
|
| 257 |
+
version = "0.8.21"
|
| 258 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 259 |
+
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
| 260 |
+
|
| 261 |
+
[[package]]
|
| 262 |
+
name = "dashmap"
|
| 263 |
+
version = "6.1.0"
|
| 264 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 265 |
+
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
| 266 |
+
dependencies = [
|
| 267 |
+
"cfg-if",
|
| 268 |
+
"crossbeam-utils",
|
| 269 |
+
"hashbrown 0.14.5",
|
| 270 |
+
"lock_api",
|
| 271 |
+
"once_cell",
|
| 272 |
+
"parking_lot_core",
|
| 273 |
+
]
|
| 274 |
+
|
| 275 |
+
[[package]]
|
| 276 |
+
name = "deranged"
|
| 277 |
+
version = "0.5.5"
|
| 278 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 279 |
+
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
|
| 280 |
+
dependencies = [
|
| 281 |
+
"powerfmt",
|
| 282 |
+
]
|
| 283 |
+
|
| 284 |
+
[[package]]
|
| 285 |
+
name = "dirs"
|
| 286 |
+
version = "5.0.1"
|
| 287 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 288 |
+
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
| 289 |
+
dependencies = [
|
| 290 |
+
"dirs-sys",
|
| 291 |
+
]
|
| 292 |
+
|
| 293 |
+
[[package]]
|
| 294 |
+
name = "dirs-sys"
|
| 295 |
+
version = "0.4.1"
|
| 296 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 297 |
+
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
| 298 |
+
dependencies = [
|
| 299 |
+
"libc",
|
| 300 |
+
"option-ext",
|
| 301 |
+
"redox_users",
|
| 302 |
+
"windows-sys 0.48.0",
|
| 303 |
+
]
|
| 304 |
+
|
| 305 |
+
[[package]]
|
| 306 |
+
name = "displaydoc"
|
| 307 |
+
version = "0.2.5"
|
| 308 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 309 |
+
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
| 310 |
+
dependencies = [
|
| 311 |
+
"proc-macro2",
|
| 312 |
+
"quote",
|
| 313 |
+
"syn",
|
| 314 |
+
]
|
| 315 |
+
|
| 316 |
+
[[package]]
|
| 317 |
+
name = "encoding_rs"
|
| 318 |
+
version = "0.8.35"
|
| 319 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 320 |
+
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
| 321 |
+
dependencies = [
|
| 322 |
+
"cfg-if",
|
| 323 |
+
]
|
| 324 |
+
|
| 325 |
+
[[package]]
|
| 326 |
+
name = "equivalent"
|
| 327 |
+
version = "1.0.2"
|
| 328 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 329 |
+
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
| 330 |
+
|
| 331 |
+
[[package]]
|
| 332 |
+
name = "errno"
|
| 333 |
+
version = "0.3.14"
|
| 334 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 335 |
+
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
| 336 |
+
dependencies = [
|
| 337 |
+
"libc",
|
| 338 |
+
"windows-sys 0.61.2",
|
| 339 |
+
]
|
| 340 |
+
|
| 341 |
+
[[package]]
|
| 342 |
+
name = "eventsource-stream"
|
| 343 |
+
version = "0.2.3"
|
| 344 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 345 |
+
checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab"
|
| 346 |
+
dependencies = [
|
| 347 |
+
"futures-core",
|
| 348 |
+
"nom",
|
| 349 |
+
"pin-project-lite",
|
| 350 |
+
]
|
| 351 |
+
|
| 352 |
+
[[package]]
|
| 353 |
+
name = "fastrand"
|
| 354 |
+
version = "2.3.0"
|
| 355 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 356 |
+
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
| 357 |
+
|
| 358 |
+
[[package]]
|
| 359 |
+
name = "find-msvc-tools"
|
| 360 |
+
version = "0.1.6"
|
| 361 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 362 |
+
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
|
| 363 |
+
|
| 364 |
+
[[package]]
|
| 365 |
+
name = "fnv"
|
| 366 |
+
version = "1.0.7"
|
| 367 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 368 |
+
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
| 369 |
+
|
| 370 |
+
[[package]]
|
| 371 |
+
name = "foreign-types"
|
| 372 |
+
version = "0.3.2"
|
| 373 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 374 |
+
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
| 375 |
+
dependencies = [
|
| 376 |
+
"foreign-types-shared",
|
| 377 |
+
]
|
| 378 |
+
|
| 379 |
+
[[package]]
|
| 380 |
+
name = "foreign-types-shared"
|
| 381 |
+
version = "0.1.1"
|
| 382 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 383 |
+
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
| 384 |
+
|
| 385 |
+
[[package]]
|
| 386 |
+
name = "form_urlencoded"
|
| 387 |
+
version = "1.2.2"
|
| 388 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 389 |
+
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
| 390 |
+
dependencies = [
|
| 391 |
+
"percent-encoding",
|
| 392 |
+
]
|
| 393 |
+
|
| 394 |
+
[[package]]
|
| 395 |
+
name = "futures"
|
| 396 |
+
version = "0.3.31"
|
| 397 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 398 |
+
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
| 399 |
+
dependencies = [
|
| 400 |
+
"futures-channel",
|
| 401 |
+
"futures-core",
|
| 402 |
+
"futures-executor",
|
| 403 |
+
"futures-io",
|
| 404 |
+
"futures-sink",
|
| 405 |
+
"futures-task",
|
| 406 |
+
"futures-util",
|
| 407 |
+
]
|
| 408 |
+
|
| 409 |
+
[[package]]
|
| 410 |
+
name = "futures-channel"
|
| 411 |
+
version = "0.3.31"
|
| 412 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 413 |
+
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
| 414 |
+
dependencies = [
|
| 415 |
+
"futures-core",
|
| 416 |
+
"futures-sink",
|
| 417 |
+
]
|
| 418 |
+
|
| 419 |
+
[[package]]
|
| 420 |
+
name = "futures-core"
|
| 421 |
+
version = "0.3.31"
|
| 422 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 423 |
+
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
| 424 |
+
|
| 425 |
+
[[package]]
|
| 426 |
+
name = "futures-executor"
|
| 427 |
+
version = "0.3.31"
|
| 428 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 429 |
+
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
|
| 430 |
+
dependencies = [
|
| 431 |
+
"futures-core",
|
| 432 |
+
"futures-task",
|
| 433 |
+
"futures-util",
|
| 434 |
+
]
|
| 435 |
+
|
| 436 |
+
[[package]]
|
| 437 |
+
name = "futures-io"
|
| 438 |
+
version = "0.3.31"
|
| 439 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 440 |
+
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
| 441 |
+
|
| 442 |
+
[[package]]
|
| 443 |
+
name = "futures-macro"
|
| 444 |
+
version = "0.3.31"
|
| 445 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 446 |
+
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
| 447 |
+
dependencies = [
|
| 448 |
+
"proc-macro2",
|
| 449 |
+
"quote",
|
| 450 |
+
"syn",
|
| 451 |
+
]
|
| 452 |
+
|
| 453 |
+
[[package]]
|
| 454 |
+
name = "futures-sink"
|
| 455 |
+
version = "0.3.31"
|
| 456 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 457 |
+
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
| 458 |
+
|
| 459 |
+
[[package]]
|
| 460 |
+
name = "futures-task"
|
| 461 |
+
version = "0.3.31"
|
| 462 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 463 |
+
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
| 464 |
+
|
| 465 |
+
[[package]]
|
| 466 |
+
name = "futures-util"
|
| 467 |
+
version = "0.3.31"
|
| 468 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 469 |
+
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
| 470 |
+
dependencies = [
|
| 471 |
+
"futures-channel",
|
| 472 |
+
"futures-core",
|
| 473 |
+
"futures-io",
|
| 474 |
+
"futures-macro",
|
| 475 |
+
"futures-sink",
|
| 476 |
+
"futures-task",
|
| 477 |
+
"memchr",
|
| 478 |
+
"pin-project-lite",
|
| 479 |
+
"pin-utils",
|
| 480 |
+
"slab",
|
| 481 |
+
]
|
| 482 |
+
|
| 483 |
+
[[package]]
|
| 484 |
+
name = "getrandom"
|
| 485 |
+
version = "0.2.16"
|
| 486 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 487 |
+
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
| 488 |
+
dependencies = [
|
| 489 |
+
"cfg-if",
|
| 490 |
+
"libc",
|
| 491 |
+
"wasi",
|
| 492 |
+
]
|
| 493 |
+
|
| 494 |
+
[[package]]
|
| 495 |
+
name = "getrandom"
|
| 496 |
+
version = "0.3.4"
|
| 497 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 498 |
+
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
| 499 |
+
dependencies = [
|
| 500 |
+
"cfg-if",
|
| 501 |
+
"libc",
|
| 502 |
+
"r-efi",
|
| 503 |
+
"wasip2",
|
| 504 |
+
]
|
| 505 |
+
|
| 506 |
+
[[package]]
|
| 507 |
+
name = "h2"
|
| 508 |
+
version = "0.4.12"
|
| 509 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 510 |
+
checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
|
| 511 |
+
dependencies = [
|
| 512 |
+
"atomic-waker",
|
| 513 |
+
"bytes",
|
| 514 |
+
"fnv",
|
| 515 |
+
"futures-core",
|
| 516 |
+
"futures-sink",
|
| 517 |
+
"http",
|
| 518 |
+
"indexmap",
|
| 519 |
+
"slab",
|
| 520 |
+
"tokio",
|
| 521 |
+
"tokio-util",
|
| 522 |
+
"tracing",
|
| 523 |
+
]
|
| 524 |
+
|
| 525 |
+
[[package]]
|
| 526 |
+
name = "hashbrown"
|
| 527 |
+
version = "0.14.5"
|
| 528 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 529 |
+
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
| 530 |
+
|
| 531 |
+
[[package]]
|
| 532 |
+
name = "hashbrown"
|
| 533 |
+
version = "0.16.1"
|
| 534 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 535 |
+
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
| 536 |
+
|
| 537 |
+
[[package]]
|
| 538 |
+
name = "http"
|
| 539 |
+
version = "1.4.0"
|
| 540 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 541 |
+
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
| 542 |
+
dependencies = [
|
| 543 |
+
"bytes",
|
| 544 |
+
"itoa",
|
| 545 |
+
]
|
| 546 |
+
|
| 547 |
+
[[package]]
|
| 548 |
+
name = "http-body"
|
| 549 |
+
version = "1.0.1"
|
| 550 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 551 |
+
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
| 552 |
+
dependencies = [
|
| 553 |
+
"bytes",
|
| 554 |
+
"http",
|
| 555 |
+
]
|
| 556 |
+
|
| 557 |
+
[[package]]
|
| 558 |
+
name = "http-body-util"
|
| 559 |
+
version = "0.1.3"
|
| 560 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 561 |
+
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
| 562 |
+
dependencies = [
|
| 563 |
+
"bytes",
|
| 564 |
+
"futures-core",
|
| 565 |
+
"http",
|
| 566 |
+
"http-body",
|
| 567 |
+
"pin-project-lite",
|
| 568 |
+
]
|
| 569 |
+
|
| 570 |
+
[[package]]
|
| 571 |
+
name = "http-range-header"
|
| 572 |
+
version = "0.4.2"
|
| 573 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 574 |
+
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
| 575 |
+
|
| 576 |
+
[[package]]
|
| 577 |
+
name = "httparse"
|
| 578 |
+
version = "1.10.1"
|
| 579 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 580 |
+
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
| 581 |
+
|
| 582 |
+
[[package]]
|
| 583 |
+
name = "httpdate"
|
| 584 |
+
version = "1.0.3"
|
| 585 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 586 |
+
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
| 587 |
+
|
| 588 |
+
[[package]]
|
| 589 |
+
name = "hyper"
|
| 590 |
+
version = "1.8.1"
|
| 591 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 592 |
+
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
| 593 |
+
dependencies = [
|
| 594 |
+
"atomic-waker",
|
| 595 |
+
"bytes",
|
| 596 |
+
"futures-channel",
|
| 597 |
+
"futures-core",
|
| 598 |
+
"h2",
|
| 599 |
+
"http",
|
| 600 |
+
"http-body",
|
| 601 |
+
"httparse",
|
| 602 |
+
"httpdate",
|
| 603 |
+
"itoa",
|
| 604 |
+
"pin-project-lite",
|
| 605 |
+
"pin-utils",
|
| 606 |
+
"smallvec",
|
| 607 |
+
"tokio",
|
| 608 |
+
"want",
|
| 609 |
+
]
|
| 610 |
+
|
| 611 |
+
[[package]]
|
| 612 |
+
name = "hyper-rustls"
|
| 613 |
+
version = "0.27.7"
|
| 614 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 615 |
+
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
| 616 |
+
dependencies = [
|
| 617 |
+
"http",
|
| 618 |
+
"hyper",
|
| 619 |
+
"hyper-util",
|
| 620 |
+
"rustls",
|
| 621 |
+
"rustls-pki-types",
|
| 622 |
+
"tokio",
|
| 623 |
+
"tokio-rustls",
|
| 624 |
+
"tower-service",
|
| 625 |
+
]
|
| 626 |
+
|
| 627 |
+
[[package]]
|
| 628 |
+
name = "hyper-tls"
|
| 629 |
+
version = "0.6.0"
|
| 630 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 631 |
+
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
| 632 |
+
dependencies = [
|
| 633 |
+
"bytes",
|
| 634 |
+
"http-body-util",
|
| 635 |
+
"hyper",
|
| 636 |
+
"hyper-util",
|
| 637 |
+
"native-tls",
|
| 638 |
+
"tokio",
|
| 639 |
+
"tokio-native-tls",
|
| 640 |
+
"tower-service",
|
| 641 |
+
]
|
| 642 |
+
|
| 643 |
+
[[package]]
|
| 644 |
+
name = "hyper-util"
|
| 645 |
+
version = "0.1.19"
|
| 646 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 647 |
+
checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
|
| 648 |
+
dependencies = [
|
| 649 |
+
"base64",
|
| 650 |
+
"bytes",
|
| 651 |
+
"futures-channel",
|
| 652 |
+
"futures-core",
|
| 653 |
+
"futures-util",
|
| 654 |
+
"http",
|
| 655 |
+
"http-body",
|
| 656 |
+
"hyper",
|
| 657 |
+
"ipnet",
|
| 658 |
+
"libc",
|
| 659 |
+
"percent-encoding",
|
| 660 |
+
"pin-project-lite",
|
| 661 |
+
"socket2",
|
| 662 |
+
"system-configuration",
|
| 663 |
+
"tokio",
|
| 664 |
+
"tower-layer",
|
| 665 |
+
"tower-service",
|
| 666 |
+
"tracing",
|
| 667 |
+
"windows-registry",
|
| 668 |
+
]
|
| 669 |
+
|
| 670 |
+
[[package]]
|
| 671 |
+
name = "iana-time-zone"
|
| 672 |
+
version = "0.1.64"
|
| 673 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 674 |
+
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
| 675 |
+
dependencies = [
|
| 676 |
+
"android_system_properties",
|
| 677 |
+
"core-foundation-sys",
|
| 678 |
+
"iana-time-zone-haiku",
|
| 679 |
+
"js-sys",
|
| 680 |
+
"log",
|
| 681 |
+
"wasm-bindgen",
|
| 682 |
+
"windows-core",
|
| 683 |
+
]
|
| 684 |
+
|
| 685 |
+
[[package]]
|
| 686 |
+
name = "iana-time-zone-haiku"
|
| 687 |
+
version = "0.1.2"
|
| 688 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 689 |
+
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
| 690 |
+
dependencies = [
|
| 691 |
+
"cc",
|
| 692 |
+
]
|
| 693 |
+
|
| 694 |
+
[[package]]
|
| 695 |
+
name = "icu_collections"
|
| 696 |
+
version = "2.1.1"
|
| 697 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 698 |
+
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
| 699 |
+
dependencies = [
|
| 700 |
+
"displaydoc",
|
| 701 |
+
"potential_utf",
|
| 702 |
+
"yoke",
|
| 703 |
+
"zerofrom",
|
| 704 |
+
"zerovec",
|
| 705 |
+
]
|
| 706 |
+
|
| 707 |
+
[[package]]
|
| 708 |
+
name = "icu_locale_core"
|
| 709 |
+
version = "2.1.1"
|
| 710 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 711 |
+
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
| 712 |
+
dependencies = [
|
| 713 |
+
"displaydoc",
|
| 714 |
+
"litemap",
|
| 715 |
+
"tinystr",
|
| 716 |
+
"writeable",
|
| 717 |
+
"zerovec",
|
| 718 |
+
]
|
| 719 |
+
|
| 720 |
+
[[package]]
|
| 721 |
+
name = "icu_normalizer"
|
| 722 |
+
version = "2.1.1"
|
| 723 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 724 |
+
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
| 725 |
+
dependencies = [
|
| 726 |
+
"icu_collections",
|
| 727 |
+
"icu_normalizer_data",
|
| 728 |
+
"icu_properties",
|
| 729 |
+
"icu_provider",
|
| 730 |
+
"smallvec",
|
| 731 |
+
"zerovec",
|
| 732 |
+
]
|
| 733 |
+
|
| 734 |
+
[[package]]
|
| 735 |
+
name = "icu_normalizer_data"
|
| 736 |
+
version = "2.1.1"
|
| 737 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 738 |
+
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
| 739 |
+
|
| 740 |
+
[[package]]
|
| 741 |
+
name = "icu_properties"
|
| 742 |
+
version = "2.1.2"
|
| 743 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 744 |
+
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
| 745 |
+
dependencies = [
|
| 746 |
+
"icu_collections",
|
| 747 |
+
"icu_locale_core",
|
| 748 |
+
"icu_properties_data",
|
| 749 |
+
"icu_provider",
|
| 750 |
+
"zerotrie",
|
| 751 |
+
"zerovec",
|
| 752 |
+
]
|
| 753 |
+
|
| 754 |
+
[[package]]
|
| 755 |
+
name = "icu_properties_data"
|
| 756 |
+
version = "2.1.2"
|
| 757 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 758 |
+
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
| 759 |
+
|
| 760 |
+
[[package]]
|
| 761 |
+
name = "icu_provider"
|
| 762 |
+
version = "2.1.1"
|
| 763 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 764 |
+
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
| 765 |
+
dependencies = [
|
| 766 |
+
"displaydoc",
|
| 767 |
+
"icu_locale_core",
|
| 768 |
+
"writeable",
|
| 769 |
+
"yoke",
|
| 770 |
+
"zerofrom",
|
| 771 |
+
"zerotrie",
|
| 772 |
+
"zerovec",
|
| 773 |
+
]
|
| 774 |
+
|
| 775 |
+
[[package]]
|
| 776 |
+
name = "idna"
|
| 777 |
+
version = "1.1.0"
|
| 778 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 779 |
+
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
| 780 |
+
dependencies = [
|
| 781 |
+
"idna_adapter",
|
| 782 |
+
"smallvec",
|
| 783 |
+
"utf8_iter",
|
| 784 |
+
]
|
| 785 |
+
|
| 786 |
+
[[package]]
|
| 787 |
+
name = "idna_adapter"
|
| 788 |
+
version = "1.2.1"
|
| 789 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 790 |
+
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
| 791 |
+
dependencies = [
|
| 792 |
+
"icu_normalizer",
|
| 793 |
+
"icu_properties",
|
| 794 |
+
]
|
| 795 |
+
|
| 796 |
+
[[package]]
|
| 797 |
+
name = "indexmap"
|
| 798 |
+
version = "2.12.1"
|
| 799 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 800 |
+
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
| 801 |
+
dependencies = [
|
| 802 |
+
"equivalent",
|
| 803 |
+
"hashbrown 0.16.1",
|
| 804 |
+
]
|
| 805 |
+
|
| 806 |
+
[[package]]
|
| 807 |
+
name = "ipnet"
|
| 808 |
+
version = "2.11.0"
|
| 809 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 810 |
+
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
| 811 |
+
|
| 812 |
+
[[package]]
|
| 813 |
+
name = "iri-string"
|
| 814 |
+
version = "0.7.10"
|
| 815 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 816 |
+
checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a"
|
| 817 |
+
dependencies = [
|
| 818 |
+
"memchr",
|
| 819 |
+
"serde",
|
| 820 |
+
]
|
| 821 |
+
|
| 822 |
+
[[package]]
|
| 823 |
+
name = "itoa"
|
| 824 |
+
version = "1.0.17"
|
| 825 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 826 |
+
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
| 827 |
+
|
| 828 |
+
[[package]]
|
| 829 |
+
name = "js-sys"
|
| 830 |
+
version = "0.3.83"
|
| 831 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 832 |
+
checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
|
| 833 |
+
dependencies = [
|
| 834 |
+
"once_cell",
|
| 835 |
+
"wasm-bindgen",
|
| 836 |
+
]
|
| 837 |
+
|
| 838 |
+
[[package]]
|
| 839 |
+
name = "lazy_static"
|
| 840 |
+
version = "1.5.0"
|
| 841 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 842 |
+
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
| 843 |
+
|
| 844 |
+
[[package]]
|
| 845 |
+
name = "libc"
|
| 846 |
+
version = "0.2.178"
|
| 847 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 848 |
+
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
| 849 |
+
|
| 850 |
+
[[package]]
|
| 851 |
+
name = "libredox"
|
| 852 |
+
version = "0.1.12"
|
| 853 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 854 |
+
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
| 855 |
+
dependencies = [
|
| 856 |
+
"bitflags",
|
| 857 |
+
"libc",
|
| 858 |
+
]
|
| 859 |
+
|
| 860 |
+
[[package]]
|
| 861 |
+
name = "linux-raw-sys"
|
| 862 |
+
version = "0.11.0"
|
| 863 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 864 |
+
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
| 865 |
+
|
| 866 |
+
[[package]]
|
| 867 |
+
name = "litemap"
|
| 868 |
+
version = "0.8.1"
|
| 869 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 870 |
+
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
| 871 |
+
|
| 872 |
+
[[package]]
|
| 873 |
+
name = "lock_api"
|
| 874 |
+
version = "0.4.14"
|
| 875 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 876 |
+
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
| 877 |
+
dependencies = [
|
| 878 |
+
"scopeguard",
|
| 879 |
+
]
|
| 880 |
+
|
| 881 |
+
[[package]]
|
| 882 |
+
name = "log"
|
| 883 |
+
version = "0.4.29"
|
| 884 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 885 |
+
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
| 886 |
+
|
| 887 |
+
[[package]]
|
| 888 |
+
name = "matchers"
|
| 889 |
+
version = "0.2.0"
|
| 890 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 891 |
+
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
| 892 |
+
dependencies = [
|
| 893 |
+
"regex-automata",
|
| 894 |
+
]
|
| 895 |
+
|
| 896 |
+
[[package]]
|
| 897 |
+
name = "matchit"
|
| 898 |
+
version = "0.7.3"
|
| 899 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 900 |
+
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
| 901 |
+
|
| 902 |
+
[[package]]
|
| 903 |
+
name = "memchr"
|
| 904 |
+
version = "2.7.6"
|
| 905 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 906 |
+
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
| 907 |
+
|
| 908 |
+
[[package]]
|
| 909 |
+
name = "mime"
|
| 910 |
+
version = "0.3.17"
|
| 911 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 912 |
+
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
| 913 |
+
|
| 914 |
+
[[package]]
|
| 915 |
+
name = "mime_guess"
|
| 916 |
+
version = "2.0.5"
|
| 917 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 918 |
+
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
| 919 |
+
dependencies = [
|
| 920 |
+
"mime",
|
| 921 |
+
"unicase",
|
| 922 |
+
]
|
| 923 |
+
|
| 924 |
+
[[package]]
|
| 925 |
+
name = "minimal-lexical"
|
| 926 |
+
version = "0.2.1"
|
| 927 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 928 |
+
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
| 929 |
+
|
| 930 |
+
[[package]]
|
| 931 |
+
name = "mio"
|
| 932 |
+
version = "1.1.1"
|
| 933 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 934 |
+
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
| 935 |
+
dependencies = [
|
| 936 |
+
"libc",
|
| 937 |
+
"wasi",
|
| 938 |
+
"windows-sys 0.61.2",
|
| 939 |
+
]
|
| 940 |
+
|
| 941 |
+
[[package]]
|
| 942 |
+
name = "native-tls"
|
| 943 |
+
version = "0.2.14"
|
| 944 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 945 |
+
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
| 946 |
+
dependencies = [
|
| 947 |
+
"libc",
|
| 948 |
+
"log",
|
| 949 |
+
"openssl",
|
| 950 |
+
"openssl-probe",
|
| 951 |
+
"openssl-sys",
|
| 952 |
+
"schannel",
|
| 953 |
+
"security-framework",
|
| 954 |
+
"security-framework-sys",
|
| 955 |
+
"tempfile",
|
| 956 |
+
]
|
| 957 |
+
|
| 958 |
+
[[package]]
|
| 959 |
+
name = "nom"
|
| 960 |
+
version = "7.1.3"
|
| 961 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 962 |
+
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
| 963 |
+
dependencies = [
|
| 964 |
+
"memchr",
|
| 965 |
+
"minimal-lexical",
|
| 966 |
+
]
|
| 967 |
+
|
| 968 |
+
[[package]]
|
| 969 |
+
name = "nu-ansi-term"
|
| 970 |
+
version = "0.50.3"
|
| 971 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 972 |
+
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
| 973 |
+
dependencies = [
|
| 974 |
+
"windows-sys 0.61.2",
|
| 975 |
+
]
|
| 976 |
+
|
| 977 |
+
[[package]]
|
| 978 |
+
name = "num-conv"
|
| 979 |
+
version = "0.1.0"
|
| 980 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 981 |
+
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
| 982 |
+
|
| 983 |
+
[[package]]
|
| 984 |
+
name = "num-traits"
|
| 985 |
+
version = "0.2.19"
|
| 986 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 987 |
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
| 988 |
+
dependencies = [
|
| 989 |
+
"autocfg",
|
| 990 |
+
]
|
| 991 |
+
|
| 992 |
+
[[package]]
|
| 993 |
+
name = "once_cell"
|
| 994 |
+
version = "1.21.3"
|
| 995 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 996 |
+
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
| 997 |
+
|
| 998 |
+
[[package]]
|
| 999 |
+
name = "openssl"
|
| 1000 |
+
version = "0.10.75"
|
| 1001 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1002 |
+
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
| 1003 |
+
dependencies = [
|
| 1004 |
+
"bitflags",
|
| 1005 |
+
"cfg-if",
|
| 1006 |
+
"foreign-types",
|
| 1007 |
+
"libc",
|
| 1008 |
+
"once_cell",
|
| 1009 |
+
"openssl-macros",
|
| 1010 |
+
"openssl-sys",
|
| 1011 |
+
]
|
| 1012 |
+
|
| 1013 |
+
[[package]]
|
| 1014 |
+
name = "openssl-macros"
|
| 1015 |
+
version = "0.1.1"
|
| 1016 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1017 |
+
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
| 1018 |
+
dependencies = [
|
| 1019 |
+
"proc-macro2",
|
| 1020 |
+
"quote",
|
| 1021 |
+
"syn",
|
| 1022 |
+
]
|
| 1023 |
+
|
| 1024 |
+
[[package]]
|
| 1025 |
+
name = "openssl-probe"
|
| 1026 |
+
version = "0.1.6"
|
| 1027 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1028 |
+
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
| 1029 |
+
|
| 1030 |
+
[[package]]
|
| 1031 |
+
name = "openssl-sys"
|
| 1032 |
+
version = "0.9.111"
|
| 1033 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1034 |
+
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
| 1035 |
+
dependencies = [
|
| 1036 |
+
"cc",
|
| 1037 |
+
"libc",
|
| 1038 |
+
"pkg-config",
|
| 1039 |
+
"vcpkg",
|
| 1040 |
+
]
|
| 1041 |
+
|
| 1042 |
+
[[package]]
|
| 1043 |
+
name = "option-ext"
|
| 1044 |
+
version = "0.2.0"
|
| 1045 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1046 |
+
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
| 1047 |
+
|
| 1048 |
+
[[package]]
|
| 1049 |
+
name = "parking_lot"
|
| 1050 |
+
version = "0.12.5"
|
| 1051 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1052 |
+
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
| 1053 |
+
dependencies = [
|
| 1054 |
+
"lock_api",
|
| 1055 |
+
"parking_lot_core",
|
| 1056 |
+
]
|
| 1057 |
+
|
| 1058 |
+
[[package]]
|
| 1059 |
+
name = "parking_lot_core"
|
| 1060 |
+
version = "0.9.12"
|
| 1061 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1062 |
+
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
| 1063 |
+
dependencies = [
|
| 1064 |
+
"cfg-if",
|
| 1065 |
+
"libc",
|
| 1066 |
+
"redox_syscall",
|
| 1067 |
+
"smallvec",
|
| 1068 |
+
"windows-link",
|
| 1069 |
+
]
|
| 1070 |
+
|
| 1071 |
+
[[package]]
|
| 1072 |
+
name = "percent-encoding"
|
| 1073 |
+
version = "2.3.2"
|
| 1074 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1075 |
+
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
| 1076 |
+
|
| 1077 |
+
[[package]]
|
| 1078 |
+
name = "pin-project"
|
| 1079 |
+
version = "1.1.10"
|
| 1080 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1081 |
+
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
|
| 1082 |
+
dependencies = [
|
| 1083 |
+
"pin-project-internal",
|
| 1084 |
+
]
|
| 1085 |
+
|
| 1086 |
+
[[package]]
|
| 1087 |
+
name = "pin-project-internal"
|
| 1088 |
+
version = "1.1.10"
|
| 1089 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1090 |
+
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
|
| 1091 |
+
dependencies = [
|
| 1092 |
+
"proc-macro2",
|
| 1093 |
+
"quote",
|
| 1094 |
+
"syn",
|
| 1095 |
+
]
|
| 1096 |
+
|
| 1097 |
+
[[package]]
|
| 1098 |
+
name = "pin-project-lite"
|
| 1099 |
+
version = "0.2.16"
|
| 1100 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1101 |
+
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
| 1102 |
+
|
| 1103 |
+
[[package]]
|
| 1104 |
+
name = "pin-utils"
|
| 1105 |
+
version = "0.1.0"
|
| 1106 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1107 |
+
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
| 1108 |
+
|
| 1109 |
+
[[package]]
|
| 1110 |
+
name = "pkg-config"
|
| 1111 |
+
version = "0.3.32"
|
| 1112 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1113 |
+
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
| 1114 |
+
|
| 1115 |
+
[[package]]
|
| 1116 |
+
name = "potential_utf"
|
| 1117 |
+
version = "0.1.4"
|
| 1118 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1119 |
+
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
| 1120 |
+
dependencies = [
|
| 1121 |
+
"zerovec",
|
| 1122 |
+
]
|
| 1123 |
+
|
| 1124 |
+
[[package]]
|
| 1125 |
+
name = "powerfmt"
|
| 1126 |
+
version = "0.2.0"
|
| 1127 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1128 |
+
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
| 1129 |
+
|
| 1130 |
+
[[package]]
|
| 1131 |
+
name = "ppv-lite86"
|
| 1132 |
+
version = "0.2.21"
|
| 1133 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1134 |
+
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
| 1135 |
+
dependencies = [
|
| 1136 |
+
"zerocopy",
|
| 1137 |
+
]
|
| 1138 |
+
|
| 1139 |
+
[[package]]
|
| 1140 |
+
name = "proc-macro2"
|
| 1141 |
+
version = "1.0.104"
|
| 1142 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1143 |
+
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
|
| 1144 |
+
dependencies = [
|
| 1145 |
+
"unicode-ident",
|
| 1146 |
+
]
|
| 1147 |
+
|
| 1148 |
+
[[package]]
|
| 1149 |
+
name = "quote"
|
| 1150 |
+
version = "1.0.42"
|
| 1151 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1152 |
+
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
| 1153 |
+
dependencies = [
|
| 1154 |
+
"proc-macro2",
|
| 1155 |
+
]
|
| 1156 |
+
|
| 1157 |
+
[[package]]
|
| 1158 |
+
name = "r-efi"
|
| 1159 |
+
version = "5.3.0"
|
| 1160 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1161 |
+
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
| 1162 |
+
|
| 1163 |
+
[[package]]
|
| 1164 |
+
name = "rand"
|
| 1165 |
+
version = "0.8.5"
|
| 1166 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1167 |
+
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
| 1168 |
+
dependencies = [
|
| 1169 |
+
"libc",
|
| 1170 |
+
"rand_chacha",
|
| 1171 |
+
"rand_core",
|
| 1172 |
+
]
|
| 1173 |
+
|
| 1174 |
+
[[package]]
|
| 1175 |
+
name = "rand_chacha"
|
| 1176 |
+
version = "0.3.1"
|
| 1177 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1178 |
+
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
| 1179 |
+
dependencies = [
|
| 1180 |
+
"ppv-lite86",
|
| 1181 |
+
"rand_core",
|
| 1182 |
+
]
|
| 1183 |
+
|
| 1184 |
+
[[package]]
|
| 1185 |
+
name = "rand_core"
|
| 1186 |
+
version = "0.6.4"
|
| 1187 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1188 |
+
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
| 1189 |
+
dependencies = [
|
| 1190 |
+
"getrandom 0.2.16",
|
| 1191 |
+
]
|
| 1192 |
+
|
| 1193 |
+
[[package]]
|
| 1194 |
+
name = "redox_syscall"
|
| 1195 |
+
version = "0.5.18"
|
| 1196 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1197 |
+
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
| 1198 |
+
dependencies = [
|
| 1199 |
+
"bitflags",
|
| 1200 |
+
]
|
| 1201 |
+
|
| 1202 |
+
[[package]]
|
| 1203 |
+
name = "redox_users"
|
| 1204 |
+
version = "0.4.6"
|
| 1205 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1206 |
+
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
| 1207 |
+
dependencies = [
|
| 1208 |
+
"getrandom 0.2.16",
|
| 1209 |
+
"libredox",
|
| 1210 |
+
"thiserror 1.0.69",
|
| 1211 |
+
]
|
| 1212 |
+
|
| 1213 |
+
[[package]]
|
| 1214 |
+
name = "regex"
|
| 1215 |
+
version = "1.12.2"
|
| 1216 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1217 |
+
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
|
| 1218 |
+
dependencies = [
|
| 1219 |
+
"aho-corasick",
|
| 1220 |
+
"memchr",
|
| 1221 |
+
"regex-automata",
|
| 1222 |
+
"regex-syntax",
|
| 1223 |
+
]
|
| 1224 |
+
|
| 1225 |
+
[[package]]
|
| 1226 |
+
name = "regex-automata"
|
| 1227 |
+
version = "0.4.13"
|
| 1228 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1229 |
+
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
| 1230 |
+
dependencies = [
|
| 1231 |
+
"aho-corasick",
|
| 1232 |
+
"memchr",
|
| 1233 |
+
"regex-syntax",
|
| 1234 |
+
]
|
| 1235 |
+
|
| 1236 |
+
[[package]]
|
| 1237 |
+
name = "regex-syntax"
|
| 1238 |
+
version = "0.8.8"
|
| 1239 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1240 |
+
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
| 1241 |
+
|
| 1242 |
+
[[package]]
|
| 1243 |
+
name = "reqwest"
|
| 1244 |
+
version = "0.12.28"
|
| 1245 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1246 |
+
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
| 1247 |
+
dependencies = [
|
| 1248 |
+
"base64",
|
| 1249 |
+
"bytes",
|
| 1250 |
+
"encoding_rs",
|
| 1251 |
+
"futures-core",
|
| 1252 |
+
"futures-util",
|
| 1253 |
+
"h2",
|
| 1254 |
+
"http",
|
| 1255 |
+
"http-body",
|
| 1256 |
+
"http-body-util",
|
| 1257 |
+
"hyper",
|
| 1258 |
+
"hyper-rustls",
|
| 1259 |
+
"hyper-tls",
|
| 1260 |
+
"hyper-util",
|
| 1261 |
+
"js-sys",
|
| 1262 |
+
"log",
|
| 1263 |
+
"mime",
|
| 1264 |
+
"native-tls",
|
| 1265 |
+
"percent-encoding",
|
| 1266 |
+
"pin-project-lite",
|
| 1267 |
+
"rustls-pki-types",
|
| 1268 |
+
"serde",
|
| 1269 |
+
"serde_json",
|
| 1270 |
+
"serde_urlencoded",
|
| 1271 |
+
"sync_wrapper",
|
| 1272 |
+
"tokio",
|
| 1273 |
+
"tokio-native-tls",
|
| 1274 |
+
"tokio-util",
|
| 1275 |
+
"tower 0.5.2",
|
| 1276 |
+
"tower-http 0.6.8",
|
| 1277 |
+
"tower-service",
|
| 1278 |
+
"url",
|
| 1279 |
+
"wasm-bindgen",
|
| 1280 |
+
"wasm-bindgen-futures",
|
| 1281 |
+
"wasm-streams",
|
| 1282 |
+
"web-sys",
|
| 1283 |
+
]
|
| 1284 |
+
|
| 1285 |
+
[[package]]
|
| 1286 |
+
name = "ring"
|
| 1287 |
+
version = "0.17.14"
|
| 1288 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1289 |
+
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
| 1290 |
+
dependencies = [
|
| 1291 |
+
"cc",
|
| 1292 |
+
"cfg-if",
|
| 1293 |
+
"getrandom 0.2.16",
|
| 1294 |
+
"libc",
|
| 1295 |
+
"untrusted",
|
| 1296 |
+
"windows-sys 0.52.0",
|
| 1297 |
+
]
|
| 1298 |
+
|
| 1299 |
+
[[package]]
|
| 1300 |
+
name = "rustix"
|
| 1301 |
+
version = "1.1.3"
|
| 1302 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1303 |
+
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
| 1304 |
+
dependencies = [
|
| 1305 |
+
"bitflags",
|
| 1306 |
+
"errno",
|
| 1307 |
+
"libc",
|
| 1308 |
+
"linux-raw-sys",
|
| 1309 |
+
"windows-sys 0.61.2",
|
| 1310 |
+
]
|
| 1311 |
+
|
| 1312 |
+
[[package]]
|
| 1313 |
+
name = "rustls"
|
| 1314 |
+
version = "0.23.35"
|
| 1315 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1316 |
+
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
|
| 1317 |
+
dependencies = [
|
| 1318 |
+
"once_cell",
|
| 1319 |
+
"rustls-pki-types",
|
| 1320 |
+
"rustls-webpki",
|
| 1321 |
+
"subtle",
|
| 1322 |
+
"zeroize",
|
| 1323 |
+
]
|
| 1324 |
+
|
| 1325 |
+
[[package]]
|
| 1326 |
+
name = "rustls-pki-types"
|
| 1327 |
+
version = "1.13.2"
|
| 1328 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1329 |
+
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
|
| 1330 |
+
dependencies = [
|
| 1331 |
+
"zeroize",
|
| 1332 |
+
]
|
| 1333 |
+
|
| 1334 |
+
[[package]]
|
| 1335 |
+
name = "rustls-webpki"
|
| 1336 |
+
version = "0.103.8"
|
| 1337 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1338 |
+
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
|
| 1339 |
+
dependencies = [
|
| 1340 |
+
"ring",
|
| 1341 |
+
"rustls-pki-types",
|
| 1342 |
+
"untrusted",
|
| 1343 |
+
]
|
| 1344 |
+
|
| 1345 |
+
[[package]]
|
| 1346 |
+
name = "rustversion"
|
| 1347 |
+
version = "1.0.22"
|
| 1348 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1349 |
+
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
| 1350 |
+
|
| 1351 |
+
[[package]]
|
| 1352 |
+
name = "ryu"
|
| 1353 |
+
version = "1.0.22"
|
| 1354 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1355 |
+
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
| 1356 |
+
|
| 1357 |
+
[[package]]
|
| 1358 |
+
name = "schannel"
|
| 1359 |
+
version = "0.1.28"
|
| 1360 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1361 |
+
checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
|
| 1362 |
+
dependencies = [
|
| 1363 |
+
"windows-sys 0.61.2",
|
| 1364 |
+
]
|
| 1365 |
+
|
| 1366 |
+
[[package]]
|
| 1367 |
+
name = "scopeguard"
|
| 1368 |
+
version = "1.2.0"
|
| 1369 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1370 |
+
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
| 1371 |
+
|
| 1372 |
+
[[package]]
|
| 1373 |
+
name = "security-framework"
|
| 1374 |
+
version = "2.11.1"
|
| 1375 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1376 |
+
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
| 1377 |
+
dependencies = [
|
| 1378 |
+
"bitflags",
|
| 1379 |
+
"core-foundation",
|
| 1380 |
+
"core-foundation-sys",
|
| 1381 |
+
"libc",
|
| 1382 |
+
"security-framework-sys",
|
| 1383 |
+
]
|
| 1384 |
+
|
| 1385 |
+
[[package]]
|
| 1386 |
+
name = "security-framework-sys"
|
| 1387 |
+
version = "2.15.0"
|
| 1388 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1389 |
+
checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
|
| 1390 |
+
dependencies = [
|
| 1391 |
+
"core-foundation-sys",
|
| 1392 |
+
"libc",
|
| 1393 |
+
]
|
| 1394 |
+
|
| 1395 |
+
[[package]]
|
| 1396 |
+
name = "serde"
|
| 1397 |
+
version = "1.0.228"
|
| 1398 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1399 |
+
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
| 1400 |
+
dependencies = [
|
| 1401 |
+
"serde_core",
|
| 1402 |
+
"serde_derive",
|
| 1403 |
+
]
|
| 1404 |
+
|
| 1405 |
+
[[package]]
|
| 1406 |
+
name = "serde_core"
|
| 1407 |
+
version = "1.0.228"
|
| 1408 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1409 |
+
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
| 1410 |
+
dependencies = [
|
| 1411 |
+
"serde_derive",
|
| 1412 |
+
]
|
| 1413 |
+
|
| 1414 |
+
[[package]]
|
| 1415 |
+
name = "serde_derive"
|
| 1416 |
+
version = "1.0.228"
|
| 1417 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1418 |
+
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
| 1419 |
+
dependencies = [
|
| 1420 |
+
"proc-macro2",
|
| 1421 |
+
"quote",
|
| 1422 |
+
"syn",
|
| 1423 |
+
]
|
| 1424 |
+
|
| 1425 |
+
[[package]]
|
| 1426 |
+
name = "serde_json"
|
| 1427 |
+
version = "1.0.148"
|
| 1428 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1429 |
+
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
|
| 1430 |
+
dependencies = [
|
| 1431 |
+
"itoa",
|
| 1432 |
+
"memchr",
|
| 1433 |
+
"serde",
|
| 1434 |
+
"serde_core",
|
| 1435 |
+
"zmij",
|
| 1436 |
+
]
|
| 1437 |
+
|
| 1438 |
+
[[package]]
|
| 1439 |
+
name = "serde_path_to_error"
|
| 1440 |
+
version = "0.1.20"
|
| 1441 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1442 |
+
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
| 1443 |
+
dependencies = [
|
| 1444 |
+
"itoa",
|
| 1445 |
+
"serde",
|
| 1446 |
+
"serde_core",
|
| 1447 |
+
]
|
| 1448 |
+
|
| 1449 |
+
[[package]]
|
| 1450 |
+
name = "serde_urlencoded"
|
| 1451 |
+
version = "0.7.1"
|
| 1452 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1453 |
+
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
| 1454 |
+
dependencies = [
|
| 1455 |
+
"form_urlencoded",
|
| 1456 |
+
"itoa",
|
| 1457 |
+
"ryu",
|
| 1458 |
+
"serde",
|
| 1459 |
+
]
|
| 1460 |
+
|
| 1461 |
+
[[package]]
|
| 1462 |
+
name = "sharded-slab"
|
| 1463 |
+
version = "0.1.7"
|
| 1464 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1465 |
+
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
| 1466 |
+
dependencies = [
|
| 1467 |
+
"lazy_static",
|
| 1468 |
+
]
|
| 1469 |
+
|
| 1470 |
+
[[package]]
|
| 1471 |
+
name = "shlex"
|
| 1472 |
+
version = "1.3.0"
|
| 1473 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1474 |
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
| 1475 |
+
|
| 1476 |
+
[[package]]
|
| 1477 |
+
name = "signal-hook-registry"
|
| 1478 |
+
version = "1.4.8"
|
| 1479 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1480 |
+
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
| 1481 |
+
dependencies = [
|
| 1482 |
+
"errno",
|
| 1483 |
+
"libc",
|
| 1484 |
+
]
|
| 1485 |
+
|
| 1486 |
+
[[package]]
|
| 1487 |
+
name = "slab"
|
| 1488 |
+
version = "0.4.11"
|
| 1489 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1490 |
+
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
| 1491 |
+
|
| 1492 |
+
[[package]]
|
| 1493 |
+
name = "smallvec"
|
| 1494 |
+
version = "1.15.1"
|
| 1495 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1496 |
+
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
| 1497 |
+
|
| 1498 |
+
[[package]]
|
| 1499 |
+
name = "socket2"
|
| 1500 |
+
version = "0.6.1"
|
| 1501 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1502 |
+
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
|
| 1503 |
+
dependencies = [
|
| 1504 |
+
"libc",
|
| 1505 |
+
"windows-sys 0.60.2",
|
| 1506 |
+
]
|
| 1507 |
+
|
| 1508 |
+
[[package]]
|
| 1509 |
+
name = "stable_deref_trait"
|
| 1510 |
+
version = "1.2.1"
|
| 1511 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1512 |
+
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
| 1513 |
+
|
| 1514 |
+
[[package]]
|
| 1515 |
+
name = "subtle"
|
| 1516 |
+
version = "2.6.1"
|
| 1517 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1518 |
+
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
| 1519 |
+
|
| 1520 |
+
[[package]]
|
| 1521 |
+
name = "syn"
|
| 1522 |
+
version = "2.0.111"
|
| 1523 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1524 |
+
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
| 1525 |
+
dependencies = [
|
| 1526 |
+
"proc-macro2",
|
| 1527 |
+
"quote",
|
| 1528 |
+
"unicode-ident",
|
| 1529 |
+
]
|
| 1530 |
+
|
| 1531 |
+
[[package]]
|
| 1532 |
+
name = "sync_wrapper"
|
| 1533 |
+
version = "1.0.2"
|
| 1534 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1535 |
+
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
| 1536 |
+
dependencies = [
|
| 1537 |
+
"futures-core",
|
| 1538 |
+
]
|
| 1539 |
+
|
| 1540 |
+
[[package]]
|
| 1541 |
+
name = "synstructure"
|
| 1542 |
+
version = "0.13.2"
|
| 1543 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1544 |
+
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
| 1545 |
+
dependencies = [
|
| 1546 |
+
"proc-macro2",
|
| 1547 |
+
"quote",
|
| 1548 |
+
"syn",
|
| 1549 |
+
]
|
| 1550 |
+
|
| 1551 |
+
[[package]]
|
| 1552 |
+
name = "system-configuration"
|
| 1553 |
+
version = "0.6.1"
|
| 1554 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1555 |
+
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
| 1556 |
+
dependencies = [
|
| 1557 |
+
"bitflags",
|
| 1558 |
+
"core-foundation",
|
| 1559 |
+
"system-configuration-sys",
|
| 1560 |
+
]
|
| 1561 |
+
|
| 1562 |
+
[[package]]
|
| 1563 |
+
name = "system-configuration-sys"
|
| 1564 |
+
version = "0.6.0"
|
| 1565 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1566 |
+
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
| 1567 |
+
dependencies = [
|
| 1568 |
+
"core-foundation-sys",
|
| 1569 |
+
"libc",
|
| 1570 |
+
]
|
| 1571 |
+
|
| 1572 |
+
[[package]]
|
| 1573 |
+
name = "tempfile"
|
| 1574 |
+
version = "3.24.0"
|
| 1575 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1576 |
+
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
| 1577 |
+
dependencies = [
|
| 1578 |
+
"fastrand",
|
| 1579 |
+
"getrandom 0.3.4",
|
| 1580 |
+
"once_cell",
|
| 1581 |
+
"rustix",
|
| 1582 |
+
"windows-sys 0.61.2",
|
| 1583 |
+
]
|
| 1584 |
+
|
| 1585 |
+
[[package]]
|
| 1586 |
+
name = "thiserror"
|
| 1587 |
+
version = "1.0.69"
|
| 1588 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1589 |
+
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
| 1590 |
+
dependencies = [
|
| 1591 |
+
"thiserror-impl 1.0.69",
|
| 1592 |
+
]
|
| 1593 |
+
|
| 1594 |
+
[[package]]
|
| 1595 |
+
name = "thiserror"
|
| 1596 |
+
version = "2.0.17"
|
| 1597 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1598 |
+
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
| 1599 |
+
dependencies = [
|
| 1600 |
+
"thiserror-impl 2.0.17",
|
| 1601 |
+
]
|
| 1602 |
+
|
| 1603 |
+
[[package]]
|
| 1604 |
+
name = "thiserror-impl"
|
| 1605 |
+
version = "1.0.69"
|
| 1606 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1607 |
+
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
| 1608 |
+
dependencies = [
|
| 1609 |
+
"proc-macro2",
|
| 1610 |
+
"quote",
|
| 1611 |
+
"syn",
|
| 1612 |
+
]
|
| 1613 |
+
|
| 1614 |
+
[[package]]
|
| 1615 |
+
name = "thiserror-impl"
|
| 1616 |
+
version = "2.0.17"
|
| 1617 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1618 |
+
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
| 1619 |
+
dependencies = [
|
| 1620 |
+
"proc-macro2",
|
| 1621 |
+
"quote",
|
| 1622 |
+
"syn",
|
| 1623 |
+
]
|
| 1624 |
+
|
| 1625 |
+
[[package]]
|
| 1626 |
+
name = "thread_local"
|
| 1627 |
+
version = "1.1.9"
|
| 1628 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1629 |
+
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
| 1630 |
+
dependencies = [
|
| 1631 |
+
"cfg-if",
|
| 1632 |
+
]
|
| 1633 |
+
|
| 1634 |
+
[[package]]
|
| 1635 |
+
name = "time"
|
| 1636 |
+
version = "0.3.44"
|
| 1637 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1638 |
+
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
| 1639 |
+
dependencies = [
|
| 1640 |
+
"deranged",
|
| 1641 |
+
"itoa",
|
| 1642 |
+
"num-conv",
|
| 1643 |
+
"powerfmt",
|
| 1644 |
+
"serde",
|
| 1645 |
+
"time-core",
|
| 1646 |
+
"time-macros",
|
| 1647 |
+
]
|
| 1648 |
+
|
| 1649 |
+
[[package]]
|
| 1650 |
+
name = "time-core"
|
| 1651 |
+
version = "0.1.6"
|
| 1652 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1653 |
+
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
|
| 1654 |
+
|
| 1655 |
+
[[package]]
|
| 1656 |
+
name = "time-macros"
|
| 1657 |
+
version = "0.2.24"
|
| 1658 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1659 |
+
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
|
| 1660 |
+
dependencies = [
|
| 1661 |
+
"num-conv",
|
| 1662 |
+
"time-core",
|
| 1663 |
+
]
|
| 1664 |
+
|
| 1665 |
+
[[package]]
|
| 1666 |
+
name = "tinystr"
|
| 1667 |
+
version = "0.8.2"
|
| 1668 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1669 |
+
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
| 1670 |
+
dependencies = [
|
| 1671 |
+
"displaydoc",
|
| 1672 |
+
"zerovec",
|
| 1673 |
+
]
|
| 1674 |
+
|
| 1675 |
+
[[package]]
|
| 1676 |
+
name = "tokio"
|
| 1677 |
+
version = "1.48.0"
|
| 1678 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1679 |
+
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
|
| 1680 |
+
dependencies = [
|
| 1681 |
+
"bytes",
|
| 1682 |
+
"libc",
|
| 1683 |
+
"mio",
|
| 1684 |
+
"parking_lot",
|
| 1685 |
+
"pin-project-lite",
|
| 1686 |
+
"signal-hook-registry",
|
| 1687 |
+
"socket2",
|
| 1688 |
+
"tokio-macros",
|
| 1689 |
+
"windows-sys 0.61.2",
|
| 1690 |
+
]
|
| 1691 |
+
|
| 1692 |
+
[[package]]
|
| 1693 |
+
name = "tokio-macros"
|
| 1694 |
+
version = "2.6.0"
|
| 1695 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1696 |
+
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
| 1697 |
+
dependencies = [
|
| 1698 |
+
"proc-macro2",
|
| 1699 |
+
"quote",
|
| 1700 |
+
"syn",
|
| 1701 |
+
]
|
| 1702 |
+
|
| 1703 |
+
[[package]]
|
| 1704 |
+
name = "tokio-native-tls"
|
| 1705 |
+
version = "0.3.1"
|
| 1706 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1707 |
+
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
| 1708 |
+
dependencies = [
|
| 1709 |
+
"native-tls",
|
| 1710 |
+
"tokio",
|
| 1711 |
+
]
|
| 1712 |
+
|
| 1713 |
+
[[package]]
|
| 1714 |
+
name = "tokio-rustls"
|
| 1715 |
+
version = "0.26.4"
|
| 1716 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1717 |
+
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
| 1718 |
+
dependencies = [
|
| 1719 |
+
"rustls",
|
| 1720 |
+
"tokio",
|
| 1721 |
+
]
|
| 1722 |
+
|
| 1723 |
+
[[package]]
|
| 1724 |
+
name = "tokio-util"
|
| 1725 |
+
version = "0.7.17"
|
| 1726 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1727 |
+
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
|
| 1728 |
+
dependencies = [
|
| 1729 |
+
"bytes",
|
| 1730 |
+
"futures-core",
|
| 1731 |
+
"futures-sink",
|
| 1732 |
+
"pin-project-lite",
|
| 1733 |
+
"tokio",
|
| 1734 |
+
]
|
| 1735 |
+
|
| 1736 |
+
[[package]]
|
| 1737 |
+
name = "tower"
|
| 1738 |
+
version = "0.4.13"
|
| 1739 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1740 |
+
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
| 1741 |
+
dependencies = [
|
| 1742 |
+
"tower-layer",
|
| 1743 |
+
"tower-service",
|
| 1744 |
+
"tracing",
|
| 1745 |
+
]
|
| 1746 |
+
|
| 1747 |
+
[[package]]
|
| 1748 |
+
name = "tower"
|
| 1749 |
+
version = "0.5.2"
|
| 1750 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1751 |
+
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
|
| 1752 |
+
dependencies = [
|
| 1753 |
+
"futures-core",
|
| 1754 |
+
"futures-util",
|
| 1755 |
+
"pin-project-lite",
|
| 1756 |
+
"sync_wrapper",
|
| 1757 |
+
"tokio",
|
| 1758 |
+
"tower-layer",
|
| 1759 |
+
"tower-service",
|
| 1760 |
+
"tracing",
|
| 1761 |
+
]
|
| 1762 |
+
|
| 1763 |
+
[[package]]
|
| 1764 |
+
name = "tower-http"
|
| 1765 |
+
version = "0.5.2"
|
| 1766 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1767 |
+
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
| 1768 |
+
dependencies = [
|
| 1769 |
+
"bitflags",
|
| 1770 |
+
"bytes",
|
| 1771 |
+
"futures-util",
|
| 1772 |
+
"http",
|
| 1773 |
+
"http-body",
|
| 1774 |
+
"http-body-util",
|
| 1775 |
+
"http-range-header",
|
| 1776 |
+
"httpdate",
|
| 1777 |
+
"mime",
|
| 1778 |
+
"mime_guess",
|
| 1779 |
+
"percent-encoding",
|
| 1780 |
+
"pin-project-lite",
|
| 1781 |
+
"tokio",
|
| 1782 |
+
"tokio-util",
|
| 1783 |
+
"tower-layer",
|
| 1784 |
+
"tower-service",
|
| 1785 |
+
"tracing",
|
| 1786 |
+
]
|
| 1787 |
+
|
| 1788 |
+
[[package]]
|
| 1789 |
+
name = "tower-http"
|
| 1790 |
+
version = "0.6.8"
|
| 1791 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1792 |
+
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
| 1793 |
+
dependencies = [
|
| 1794 |
+
"bitflags",
|
| 1795 |
+
"bytes",
|
| 1796 |
+
"futures-util",
|
| 1797 |
+
"http",
|
| 1798 |
+
"http-body",
|
| 1799 |
+
"iri-string",
|
| 1800 |
+
"pin-project-lite",
|
| 1801 |
+
"tower 0.5.2",
|
| 1802 |
+
"tower-layer",
|
| 1803 |
+
"tower-service",
|
| 1804 |
+
]
|
| 1805 |
+
|
| 1806 |
+
[[package]]
|
| 1807 |
+
name = "tower-layer"
|
| 1808 |
+
version = "0.3.3"
|
| 1809 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1810 |
+
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
| 1811 |
+
|
| 1812 |
+
[[package]]
|
| 1813 |
+
name = "tower-service"
|
| 1814 |
+
version = "0.3.3"
|
| 1815 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1816 |
+
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
| 1817 |
+
|
| 1818 |
+
[[package]]
|
| 1819 |
+
name = "tracing"
|
| 1820 |
+
version = "0.1.44"
|
| 1821 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1822 |
+
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
| 1823 |
+
dependencies = [
|
| 1824 |
+
"log",
|
| 1825 |
+
"pin-project-lite",
|
| 1826 |
+
"tracing-attributes",
|
| 1827 |
+
"tracing-core",
|
| 1828 |
+
]
|
| 1829 |
+
|
| 1830 |
+
[[package]]
|
| 1831 |
+
name = "tracing-appender"
|
| 1832 |
+
version = "0.2.4"
|
| 1833 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1834 |
+
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
|
| 1835 |
+
dependencies = [
|
| 1836 |
+
"crossbeam-channel",
|
| 1837 |
+
"thiserror 2.0.17",
|
| 1838 |
+
"time",
|
| 1839 |
+
"tracing-subscriber",
|
| 1840 |
+
]
|
| 1841 |
+
|
| 1842 |
+
[[package]]
|
| 1843 |
+
name = "tracing-attributes"
|
| 1844 |
+
version = "0.1.31"
|
| 1845 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1846 |
+
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
| 1847 |
+
dependencies = [
|
| 1848 |
+
"proc-macro2",
|
| 1849 |
+
"quote",
|
| 1850 |
+
"syn",
|
| 1851 |
+
]
|
| 1852 |
+
|
| 1853 |
+
[[package]]
|
| 1854 |
+
name = "tracing-core"
|
| 1855 |
+
version = "0.1.36"
|
| 1856 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1857 |
+
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
| 1858 |
+
dependencies = [
|
| 1859 |
+
"once_cell",
|
| 1860 |
+
"valuable",
|
| 1861 |
+
]
|
| 1862 |
+
|
| 1863 |
+
[[package]]
|
| 1864 |
+
name = "tracing-log"
|
| 1865 |
+
version = "0.2.0"
|
| 1866 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1867 |
+
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
| 1868 |
+
dependencies = [
|
| 1869 |
+
"log",
|
| 1870 |
+
"once_cell",
|
| 1871 |
+
"tracing-core",
|
| 1872 |
+
]
|
| 1873 |
+
|
| 1874 |
+
[[package]]
|
| 1875 |
+
name = "tracing-subscriber"
|
| 1876 |
+
version = "0.3.22"
|
| 1877 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1878 |
+
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
| 1879 |
+
dependencies = [
|
| 1880 |
+
"matchers",
|
| 1881 |
+
"nu-ansi-term",
|
| 1882 |
+
"once_cell",
|
| 1883 |
+
"regex-automata",
|
| 1884 |
+
"sharded-slab",
|
| 1885 |
+
"smallvec",
|
| 1886 |
+
"thread_local",
|
| 1887 |
+
"time",
|
| 1888 |
+
"tracing",
|
| 1889 |
+
"tracing-core",
|
| 1890 |
+
"tracing-log",
|
| 1891 |
+
]
|
| 1892 |
+
|
| 1893 |
+
[[package]]
|
| 1894 |
+
name = "try-lock"
|
| 1895 |
+
version = "0.2.5"
|
| 1896 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1897 |
+
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
| 1898 |
+
|
| 1899 |
+
[[package]]
|
| 1900 |
+
name = "unicase"
|
| 1901 |
+
version = "2.8.1"
|
| 1902 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1903 |
+
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
| 1904 |
+
|
| 1905 |
+
[[package]]
|
| 1906 |
+
name = "unicode-ident"
|
| 1907 |
+
version = "1.0.22"
|
| 1908 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1909 |
+
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
| 1910 |
+
|
| 1911 |
+
[[package]]
|
| 1912 |
+
name = "untrusted"
|
| 1913 |
+
version = "0.9.0"
|
| 1914 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1915 |
+
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
| 1916 |
+
|
| 1917 |
+
[[package]]
|
| 1918 |
+
name = "url"
|
| 1919 |
+
version = "2.5.7"
|
| 1920 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1921 |
+
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
|
| 1922 |
+
dependencies = [
|
| 1923 |
+
"form_urlencoded",
|
| 1924 |
+
"idna",
|
| 1925 |
+
"percent-encoding",
|
| 1926 |
+
"serde",
|
| 1927 |
+
]
|
| 1928 |
+
|
| 1929 |
+
[[package]]
|
| 1930 |
+
name = "utf8_iter"
|
| 1931 |
+
version = "1.0.4"
|
| 1932 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1933 |
+
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
| 1934 |
+
|
| 1935 |
+
[[package]]
|
| 1936 |
+
name = "uuid"
|
| 1937 |
+
version = "1.19.0"
|
| 1938 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1939 |
+
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
| 1940 |
+
dependencies = [
|
| 1941 |
+
"getrandom 0.3.4",
|
| 1942 |
+
"js-sys",
|
| 1943 |
+
"serde_core",
|
| 1944 |
+
"wasm-bindgen",
|
| 1945 |
+
]
|
| 1946 |
+
|
| 1947 |
+
[[package]]
|
| 1948 |
+
name = "valuable"
|
| 1949 |
+
version = "0.1.1"
|
| 1950 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1951 |
+
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
| 1952 |
+
|
| 1953 |
+
[[package]]
|
| 1954 |
+
name = "vcpkg"
|
| 1955 |
+
version = "0.2.15"
|
| 1956 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1957 |
+
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
| 1958 |
+
|
| 1959 |
+
[[package]]
|
| 1960 |
+
name = "want"
|
| 1961 |
+
version = "0.3.1"
|
| 1962 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1963 |
+
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
|
| 1964 |
+
dependencies = [
|
| 1965 |
+
"try-lock",
|
| 1966 |
+
]
|
| 1967 |
+
|
| 1968 |
+
[[package]]
|
| 1969 |
+
name = "wasi"
|
| 1970 |
+
version = "0.11.1+wasi-snapshot-preview1"
|
| 1971 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1972 |
+
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
| 1973 |
+
|
| 1974 |
+
[[package]]
|
| 1975 |
+
name = "wasip2"
|
| 1976 |
+
version = "1.0.1+wasi-0.2.4"
|
| 1977 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1978 |
+
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
| 1979 |
+
dependencies = [
|
| 1980 |
+
"wit-bindgen",
|
| 1981 |
+
]
|
| 1982 |
+
|
| 1983 |
+
[[package]]
|
| 1984 |
+
name = "wasm-bindgen"
|
| 1985 |
+
version = "0.2.106"
|
| 1986 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 1987 |
+
checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
|
| 1988 |
+
dependencies = [
|
| 1989 |
+
"cfg-if",
|
| 1990 |
+
"once_cell",
|
| 1991 |
+
"rustversion",
|
| 1992 |
+
"wasm-bindgen-macro",
|
| 1993 |
+
"wasm-bindgen-shared",
|
| 1994 |
+
]
|
| 1995 |
+
|
| 1996 |
+
[[package]]
|
| 1997 |
+
name = "wasm-bindgen-futures"
|
| 1998 |
+
version = "0.4.56"
|
| 1999 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2000 |
+
checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
|
| 2001 |
+
dependencies = [
|
| 2002 |
+
"cfg-if",
|
| 2003 |
+
"js-sys",
|
| 2004 |
+
"once_cell",
|
| 2005 |
+
"wasm-bindgen",
|
| 2006 |
+
"web-sys",
|
| 2007 |
+
]
|
| 2008 |
+
|
| 2009 |
+
[[package]]
|
| 2010 |
+
name = "wasm-bindgen-macro"
|
| 2011 |
+
version = "0.2.106"
|
| 2012 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2013 |
+
checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
|
| 2014 |
+
dependencies = [
|
| 2015 |
+
"quote",
|
| 2016 |
+
"wasm-bindgen-macro-support",
|
| 2017 |
+
]
|
| 2018 |
+
|
| 2019 |
+
[[package]]
|
| 2020 |
+
name = "wasm-bindgen-macro-support"
|
| 2021 |
+
version = "0.2.106"
|
| 2022 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2023 |
+
checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
|
| 2024 |
+
dependencies = [
|
| 2025 |
+
"bumpalo",
|
| 2026 |
+
"proc-macro2",
|
| 2027 |
+
"quote",
|
| 2028 |
+
"syn",
|
| 2029 |
+
"wasm-bindgen-shared",
|
| 2030 |
+
]
|
| 2031 |
+
|
| 2032 |
+
[[package]]
|
| 2033 |
+
name = "wasm-bindgen-shared"
|
| 2034 |
+
version = "0.2.106"
|
| 2035 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2036 |
+
checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
|
| 2037 |
+
dependencies = [
|
| 2038 |
+
"unicode-ident",
|
| 2039 |
+
]
|
| 2040 |
+
|
| 2041 |
+
[[package]]
|
| 2042 |
+
name = "wasm-streams"
|
| 2043 |
+
version = "0.4.2"
|
| 2044 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2045 |
+
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
| 2046 |
+
dependencies = [
|
| 2047 |
+
"futures-util",
|
| 2048 |
+
"js-sys",
|
| 2049 |
+
"wasm-bindgen",
|
| 2050 |
+
"wasm-bindgen-futures",
|
| 2051 |
+
"web-sys",
|
| 2052 |
+
]
|
| 2053 |
+
|
| 2054 |
+
[[package]]
|
| 2055 |
+
name = "web-sys"
|
| 2056 |
+
version = "0.3.83"
|
| 2057 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2058 |
+
checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
|
| 2059 |
+
dependencies = [
|
| 2060 |
+
"js-sys",
|
| 2061 |
+
"wasm-bindgen",
|
| 2062 |
+
]
|
| 2063 |
+
|
| 2064 |
+
[[package]]
|
| 2065 |
+
name = "windows-core"
|
| 2066 |
+
version = "0.62.2"
|
| 2067 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2068 |
+
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
| 2069 |
+
dependencies = [
|
| 2070 |
+
"windows-implement",
|
| 2071 |
+
"windows-interface",
|
| 2072 |
+
"windows-link",
|
| 2073 |
+
"windows-result",
|
| 2074 |
+
"windows-strings",
|
| 2075 |
+
]
|
| 2076 |
+
|
| 2077 |
+
[[package]]
|
| 2078 |
+
name = "windows-implement"
|
| 2079 |
+
version = "0.60.2"
|
| 2080 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2081 |
+
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
| 2082 |
+
dependencies = [
|
| 2083 |
+
"proc-macro2",
|
| 2084 |
+
"quote",
|
| 2085 |
+
"syn",
|
| 2086 |
+
]
|
| 2087 |
+
|
| 2088 |
+
[[package]]
|
| 2089 |
+
name = "windows-interface"
|
| 2090 |
+
version = "0.59.3"
|
| 2091 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2092 |
+
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
| 2093 |
+
dependencies = [
|
| 2094 |
+
"proc-macro2",
|
| 2095 |
+
"quote",
|
| 2096 |
+
"syn",
|
| 2097 |
+
]
|
| 2098 |
+
|
| 2099 |
+
[[package]]
|
| 2100 |
+
name = "windows-link"
|
| 2101 |
+
version = "0.2.1"
|
| 2102 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2103 |
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
| 2104 |
+
|
| 2105 |
+
[[package]]
|
| 2106 |
+
name = "windows-registry"
|
| 2107 |
+
version = "0.6.1"
|
| 2108 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2109 |
+
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
| 2110 |
+
dependencies = [
|
| 2111 |
+
"windows-link",
|
| 2112 |
+
"windows-result",
|
| 2113 |
+
"windows-strings",
|
| 2114 |
+
]
|
| 2115 |
+
|
| 2116 |
+
[[package]]
|
| 2117 |
+
name = "windows-result"
|
| 2118 |
+
version = "0.4.1"
|
| 2119 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2120 |
+
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
| 2121 |
+
dependencies = [
|
| 2122 |
+
"windows-link",
|
| 2123 |
+
]
|
| 2124 |
+
|
| 2125 |
+
[[package]]
|
| 2126 |
+
name = "windows-strings"
|
| 2127 |
+
version = "0.5.1"
|
| 2128 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2129 |
+
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
| 2130 |
+
dependencies = [
|
| 2131 |
+
"windows-link",
|
| 2132 |
+
]
|
| 2133 |
+
|
| 2134 |
+
[[package]]
|
| 2135 |
+
name = "windows-sys"
|
| 2136 |
+
version = "0.48.0"
|
| 2137 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2138 |
+
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
| 2139 |
+
dependencies = [
|
| 2140 |
+
"windows-targets 0.48.5",
|
| 2141 |
+
]
|
| 2142 |
+
|
| 2143 |
+
[[package]]
|
| 2144 |
+
name = "windows-sys"
|
| 2145 |
+
version = "0.52.0"
|
| 2146 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2147 |
+
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
| 2148 |
+
dependencies = [
|
| 2149 |
+
"windows-targets 0.52.6",
|
| 2150 |
+
]
|
| 2151 |
+
|
| 2152 |
+
[[package]]
|
| 2153 |
+
name = "windows-sys"
|
| 2154 |
+
version = "0.60.2"
|
| 2155 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2156 |
+
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
| 2157 |
+
dependencies = [
|
| 2158 |
+
"windows-targets 0.53.5",
|
| 2159 |
+
]
|
| 2160 |
+
|
| 2161 |
+
[[package]]
|
| 2162 |
+
name = "windows-sys"
|
| 2163 |
+
version = "0.61.2"
|
| 2164 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2165 |
+
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
| 2166 |
+
dependencies = [
|
| 2167 |
+
"windows-link",
|
| 2168 |
+
]
|
| 2169 |
+
|
| 2170 |
+
[[package]]
|
| 2171 |
+
name = "windows-targets"
|
| 2172 |
+
version = "0.48.5"
|
| 2173 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2174 |
+
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
| 2175 |
+
dependencies = [
|
| 2176 |
+
"windows_aarch64_gnullvm 0.48.5",
|
| 2177 |
+
"windows_aarch64_msvc 0.48.5",
|
| 2178 |
+
"windows_i686_gnu 0.48.5",
|
| 2179 |
+
"windows_i686_msvc 0.48.5",
|
| 2180 |
+
"windows_x86_64_gnu 0.48.5",
|
| 2181 |
+
"windows_x86_64_gnullvm 0.48.5",
|
| 2182 |
+
"windows_x86_64_msvc 0.48.5",
|
| 2183 |
+
]
|
| 2184 |
+
|
| 2185 |
+
[[package]]
|
| 2186 |
+
name = "windows-targets"
|
| 2187 |
+
version = "0.52.6"
|
| 2188 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2189 |
+
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
| 2190 |
+
dependencies = [
|
| 2191 |
+
"windows_aarch64_gnullvm 0.52.6",
|
| 2192 |
+
"windows_aarch64_msvc 0.52.6",
|
| 2193 |
+
"windows_i686_gnu 0.52.6",
|
| 2194 |
+
"windows_i686_gnullvm 0.52.6",
|
| 2195 |
+
"windows_i686_msvc 0.52.6",
|
| 2196 |
+
"windows_x86_64_gnu 0.52.6",
|
| 2197 |
+
"windows_x86_64_gnullvm 0.52.6",
|
| 2198 |
+
"windows_x86_64_msvc 0.52.6",
|
| 2199 |
+
]
|
| 2200 |
+
|
| 2201 |
+
[[package]]
|
| 2202 |
+
name = "windows-targets"
|
| 2203 |
+
version = "0.53.5"
|
| 2204 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2205 |
+
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
| 2206 |
+
dependencies = [
|
| 2207 |
+
"windows-link",
|
| 2208 |
+
"windows_aarch64_gnullvm 0.53.1",
|
| 2209 |
+
"windows_aarch64_msvc 0.53.1",
|
| 2210 |
+
"windows_i686_gnu 0.53.1",
|
| 2211 |
+
"windows_i686_gnullvm 0.53.1",
|
| 2212 |
+
"windows_i686_msvc 0.53.1",
|
| 2213 |
+
"windows_x86_64_gnu 0.53.1",
|
| 2214 |
+
"windows_x86_64_gnullvm 0.53.1",
|
| 2215 |
+
"windows_x86_64_msvc 0.53.1",
|
| 2216 |
+
]
|
| 2217 |
+
|
| 2218 |
+
[[package]]
|
| 2219 |
+
name = "windows_aarch64_gnullvm"
|
| 2220 |
+
version = "0.48.5"
|
| 2221 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2222 |
+
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
| 2223 |
+
|
| 2224 |
+
[[package]]
|
| 2225 |
+
name = "windows_aarch64_gnullvm"
|
| 2226 |
+
version = "0.52.6"
|
| 2227 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2228 |
+
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
| 2229 |
+
|
| 2230 |
+
[[package]]
|
| 2231 |
+
name = "windows_aarch64_gnullvm"
|
| 2232 |
+
version = "0.53.1"
|
| 2233 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2234 |
+
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
| 2235 |
+
|
| 2236 |
+
[[package]]
|
| 2237 |
+
name = "windows_aarch64_msvc"
|
| 2238 |
+
version = "0.48.5"
|
| 2239 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2240 |
+
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
| 2241 |
+
|
| 2242 |
+
[[package]]
|
| 2243 |
+
name = "windows_aarch64_msvc"
|
| 2244 |
+
version = "0.52.6"
|
| 2245 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2246 |
+
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
| 2247 |
+
|
| 2248 |
+
[[package]]
|
| 2249 |
+
name = "windows_aarch64_msvc"
|
| 2250 |
+
version = "0.53.1"
|
| 2251 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2252 |
+
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
| 2253 |
+
|
| 2254 |
+
[[package]]
|
| 2255 |
+
name = "windows_i686_gnu"
|
| 2256 |
+
version = "0.48.5"
|
| 2257 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2258 |
+
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
| 2259 |
+
|
| 2260 |
+
[[package]]
|
| 2261 |
+
name = "windows_i686_gnu"
|
| 2262 |
+
version = "0.52.6"
|
| 2263 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2264 |
+
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
| 2265 |
+
|
| 2266 |
+
[[package]]
|
| 2267 |
+
name = "windows_i686_gnu"
|
| 2268 |
+
version = "0.53.1"
|
| 2269 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2270 |
+
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
| 2271 |
+
|
| 2272 |
+
[[package]]
|
| 2273 |
+
name = "windows_i686_gnullvm"
|
| 2274 |
+
version = "0.52.6"
|
| 2275 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2276 |
+
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
| 2277 |
+
|
| 2278 |
+
[[package]]
|
| 2279 |
+
name = "windows_i686_gnullvm"
|
| 2280 |
+
version = "0.53.1"
|
| 2281 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2282 |
+
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
| 2283 |
+
|
| 2284 |
+
[[package]]
|
| 2285 |
+
name = "windows_i686_msvc"
|
| 2286 |
+
version = "0.48.5"
|
| 2287 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2288 |
+
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
| 2289 |
+
|
| 2290 |
+
[[package]]
|
| 2291 |
+
name = "windows_i686_msvc"
|
| 2292 |
+
version = "0.52.6"
|
| 2293 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2294 |
+
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
| 2295 |
+
|
| 2296 |
+
[[package]]
|
| 2297 |
+
name = "windows_i686_msvc"
|
| 2298 |
+
version = "0.53.1"
|
| 2299 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2300 |
+
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
| 2301 |
+
|
| 2302 |
+
[[package]]
|
| 2303 |
+
name = "windows_x86_64_gnu"
|
| 2304 |
+
version = "0.48.5"
|
| 2305 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2306 |
+
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
| 2307 |
+
|
| 2308 |
+
[[package]]
|
| 2309 |
+
name = "windows_x86_64_gnu"
|
| 2310 |
+
version = "0.52.6"
|
| 2311 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2312 |
+
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
| 2313 |
+
|
| 2314 |
+
[[package]]
|
| 2315 |
+
name = "windows_x86_64_gnu"
|
| 2316 |
+
version = "0.53.1"
|
| 2317 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2318 |
+
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
| 2319 |
+
|
| 2320 |
+
[[package]]
|
| 2321 |
+
name = "windows_x86_64_gnullvm"
|
| 2322 |
+
version = "0.48.5"
|
| 2323 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2324 |
+
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
| 2325 |
+
|
| 2326 |
+
[[package]]
|
| 2327 |
+
name = "windows_x86_64_gnullvm"
|
| 2328 |
+
version = "0.52.6"
|
| 2329 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2330 |
+
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
| 2331 |
+
|
| 2332 |
+
[[package]]
|
| 2333 |
+
name = "windows_x86_64_gnullvm"
|
| 2334 |
+
version = "0.53.1"
|
| 2335 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2336 |
+
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
| 2337 |
+
|
| 2338 |
+
[[package]]
|
| 2339 |
+
name = "windows_x86_64_msvc"
|
| 2340 |
+
version = "0.48.5"
|
| 2341 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2342 |
+
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
| 2343 |
+
|
| 2344 |
+
[[package]]
|
| 2345 |
+
name = "windows_x86_64_msvc"
|
| 2346 |
+
version = "0.52.6"
|
| 2347 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2348 |
+
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
| 2349 |
+
|
| 2350 |
+
[[package]]
|
| 2351 |
+
name = "windows_x86_64_msvc"
|
| 2352 |
+
version = "0.53.1"
|
| 2353 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2354 |
+
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
| 2355 |
+
|
| 2356 |
+
[[package]]
|
| 2357 |
+
name = "wit-bindgen"
|
| 2358 |
+
version = "0.46.0"
|
| 2359 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2360 |
+
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
| 2361 |
+
|
| 2362 |
+
[[package]]
|
| 2363 |
+
name = "writeable"
|
| 2364 |
+
version = "0.6.2"
|
| 2365 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2366 |
+
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
| 2367 |
+
|
| 2368 |
+
[[package]]
|
| 2369 |
+
name = "yoke"
|
| 2370 |
+
version = "0.8.1"
|
| 2371 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2372 |
+
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
| 2373 |
+
dependencies = [
|
| 2374 |
+
"stable_deref_trait",
|
| 2375 |
+
"yoke-derive",
|
| 2376 |
+
"zerofrom",
|
| 2377 |
+
]
|
| 2378 |
+
|
| 2379 |
+
[[package]]
|
| 2380 |
+
name = "yoke-derive"
|
| 2381 |
+
version = "0.8.1"
|
| 2382 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2383 |
+
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
| 2384 |
+
dependencies = [
|
| 2385 |
+
"proc-macro2",
|
| 2386 |
+
"quote",
|
| 2387 |
+
"syn",
|
| 2388 |
+
"synstructure",
|
| 2389 |
+
]
|
| 2390 |
+
|
| 2391 |
+
[[package]]
|
| 2392 |
+
name = "zerocopy"
|
| 2393 |
+
version = "0.8.31"
|
| 2394 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2395 |
+
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
|
| 2396 |
+
dependencies = [
|
| 2397 |
+
"zerocopy-derive",
|
| 2398 |
+
]
|
| 2399 |
+
|
| 2400 |
+
[[package]]
|
| 2401 |
+
name = "zerocopy-derive"
|
| 2402 |
+
version = "0.8.31"
|
| 2403 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2404 |
+
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
|
| 2405 |
+
dependencies = [
|
| 2406 |
+
"proc-macro2",
|
| 2407 |
+
"quote",
|
| 2408 |
+
"syn",
|
| 2409 |
+
]
|
| 2410 |
+
|
| 2411 |
+
[[package]]
|
| 2412 |
+
name = "zerofrom"
|
| 2413 |
+
version = "0.1.6"
|
| 2414 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2415 |
+
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
| 2416 |
+
dependencies = [
|
| 2417 |
+
"zerofrom-derive",
|
| 2418 |
+
]
|
| 2419 |
+
|
| 2420 |
+
[[package]]
|
| 2421 |
+
name = "zerofrom-derive"
|
| 2422 |
+
version = "0.1.6"
|
| 2423 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2424 |
+
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
| 2425 |
+
dependencies = [
|
| 2426 |
+
"proc-macro2",
|
| 2427 |
+
"quote",
|
| 2428 |
+
"syn",
|
| 2429 |
+
"synstructure",
|
| 2430 |
+
]
|
| 2431 |
+
|
| 2432 |
+
[[package]]
|
| 2433 |
+
name = "zeroize"
|
| 2434 |
+
version = "1.8.2"
|
| 2435 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2436 |
+
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
| 2437 |
+
|
| 2438 |
+
[[package]]
|
| 2439 |
+
name = "zerotrie"
|
| 2440 |
+
version = "0.2.3"
|
| 2441 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2442 |
+
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
| 2443 |
+
dependencies = [
|
| 2444 |
+
"displaydoc",
|
| 2445 |
+
"yoke",
|
| 2446 |
+
"zerofrom",
|
| 2447 |
+
]
|
| 2448 |
+
|
| 2449 |
+
[[package]]
|
| 2450 |
+
name = "zerovec"
|
| 2451 |
+
version = "0.11.5"
|
| 2452 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2453 |
+
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
| 2454 |
+
dependencies = [
|
| 2455 |
+
"yoke",
|
| 2456 |
+
"zerofrom",
|
| 2457 |
+
"zerovec-derive",
|
| 2458 |
+
]
|
| 2459 |
+
|
| 2460 |
+
[[package]]
|
| 2461 |
+
name = "zerovec-derive"
|
| 2462 |
+
version = "0.11.2"
|
| 2463 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2464 |
+
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
| 2465 |
+
dependencies = [
|
| 2466 |
+
"proc-macro2",
|
| 2467 |
+
"quote",
|
| 2468 |
+
"syn",
|
| 2469 |
+
]
|
| 2470 |
+
|
| 2471 |
+
[[package]]
|
| 2472 |
+
name = "zmij"
|
| 2473 |
+
version = "1.0.3"
|
| 2474 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 2475 |
+
checksum = "e9747e91771f56fd7893e1164abd78febd14a670ceec257caad15e051de35f06"
|
server/Cargo.toml
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[package]
|
| 2 |
+
name = "antigravity-server"
|
| 3 |
+
version = "1.0.0"
|
| 4 |
+
description = "Antigravity API Proxy Server for HuggingFace Spaces"
|
| 5 |
+
authors = ["Antigravity Team"]
|
| 6 |
+
license = "CC-BY-NC-SA-4.0"
|
| 7 |
+
edition = "2021"
|
| 8 |
+
|
| 9 |
+
[dependencies]
|
| 10 |
+
# Web Framework
|
| 11 |
+
axum = { version = "0.7", features = ["macros"] }
|
| 12 |
+
hyper = { version = "1", features = ["full"] }
|
| 13 |
+
hyper-util = { version = "0.1", features = ["full"] }
|
| 14 |
+
tower = "0.4"
|
| 15 |
+
tower-http = { version = "0.5", features = ["cors", "trace", "fs"] }
|
| 16 |
+
tokio = { version = "1", features = ["full"] }
|
| 17 |
+
|
| 18 |
+
# Serialization
|
| 19 |
+
serde = { version = "1", features = ["derive"] }
|
| 20 |
+
serde_json = "1"
|
| 21 |
+
|
| 22 |
+
# HTTP Client
|
| 23 |
+
reqwest = { version = "0.12", features = ["json", "stream", "socks"] }
|
| 24 |
+
|
| 25 |
+
# Utilities
|
| 26 |
+
uuid = { version = "1.10", features = ["v4", "serde"] }
|
| 27 |
+
chrono = { version = "0.4", features = ["serde"] }
|
| 28 |
+
dirs = "5.0"
|
| 29 |
+
base64 = "0.22"
|
| 30 |
+
url = "2.5.7"
|
| 31 |
+
|
| 32 |
+
# Logging
|
| 33 |
+
tracing = "0.1"
|
| 34 |
+
tracing-subscriber = { version = "0.3", features = ["env-filter", "time"] }
|
| 35 |
+
tracing-appender = "0.2.4"
|
| 36 |
+
tracing-log = "0.2.0"
|
| 37 |
+
|
| 38 |
+
# Error Handling
|
| 39 |
+
thiserror = "2.0.17"
|
| 40 |
+
anyhow = "1.0"
|
| 41 |
+
|
| 42 |
+
# Async & Streaming
|
| 43 |
+
futures = "0.3"
|
| 44 |
+
async-stream = "0.3.6"
|
| 45 |
+
eventsource-stream = "0.2"
|
| 46 |
+
pin-project = "1.1"
|
| 47 |
+
bytes = "1.5"
|
| 48 |
+
|
| 49 |
+
# Concurrency
|
| 50 |
+
dashmap = "6.1"
|
| 51 |
+
|
| 52 |
+
# Other
|
| 53 |
+
rand = "0.8"
|
| 54 |
+
regex = "1.12.2"
|
| 55 |
+
once_cell = "1.19"
|
server/src/api/accounts.rs
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use axum::{
|
| 2 |
+
extract::Path,
|
| 3 |
+
Json,
|
| 4 |
+
};
|
| 5 |
+
use serde::{Deserialize, Serialize};
|
| 6 |
+
|
| 7 |
+
use crate::models::{Account, TokenData, QuotaData};
|
| 8 |
+
use crate::modules;
|
| 9 |
+
use crate::error::AppError;
|
| 10 |
+
|
| 11 |
+
/// API response wrapper
|
| 12 |
+
#[derive(Serialize)]
|
| 13 |
+
pub struct ApiResponse<T> {
|
| 14 |
+
pub success: bool,
|
| 15 |
+
pub data: Option<T>,
|
| 16 |
+
pub error: Option<String>,
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
impl<T: Serialize> ApiResponse<T> {
|
| 20 |
+
pub fn success(data: T) -> Json<Self> {
|
| 21 |
+
Json(Self {
|
| 22 |
+
success: true,
|
| 23 |
+
data: Some(data),
|
| 24 |
+
error: None,
|
| 25 |
+
})
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
pub fn error(message: String) -> Json<Self> {
|
| 29 |
+
Json(Self {
|
| 30 |
+
success: false,
|
| 31 |
+
data: None,
|
| 32 |
+
error: Some(message),
|
| 33 |
+
})
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/// List all accounts
|
| 38 |
+
pub async fn list_accounts() -> Result<Json<ApiResponse<Vec<Account>>>, AppError> {
|
| 39 |
+
let accounts = modules::list_accounts().map_err(AppError::Account)?;
|
| 40 |
+
Ok(ApiResponse::success(accounts))
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/// Add account request
|
| 44 |
+
#[derive(Deserialize)]
|
| 45 |
+
pub struct AddAccountRequest {
|
| 46 |
+
pub email: String,
|
| 47 |
+
pub refresh_token: String,
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/// Add new account
|
| 51 |
+
pub async fn add_account(
|
| 52 |
+
Json(req): Json<AddAccountRequest>,
|
| 53 |
+
) -> Result<Json<ApiResponse<Account>>, AppError> {
|
| 54 |
+
modules::logger::log_info(&format!("Adding account: {}", req.email));
|
| 55 |
+
|
| 56 |
+
// 1. Use refresh_token to get access_token
|
| 57 |
+
let token_res = modules::oauth::refresh_access_token(&req.refresh_token)
|
| 58 |
+
.await
|
| 59 |
+
.map_err(AppError::OAuth)?;
|
| 60 |
+
|
| 61 |
+
// 2. Get user info
|
| 62 |
+
let user_info = modules::oauth::get_user_info(&token_res.access_token)
|
| 63 |
+
.await
|
| 64 |
+
.map_err(AppError::OAuth)?;
|
| 65 |
+
|
| 66 |
+
// 3. Construct TokenData
|
| 67 |
+
let token = TokenData::new(
|
| 68 |
+
token_res.access_token,
|
| 69 |
+
req.refresh_token,
|
| 70 |
+
token_res.expires_in,
|
| 71 |
+
Some(user_info.email.clone()),
|
| 72 |
+
None,
|
| 73 |
+
None,
|
| 74 |
+
);
|
| 75 |
+
|
| 76 |
+
// 4. Add or update account using real email
|
| 77 |
+
let mut account = modules::upsert_account(
|
| 78 |
+
user_info.email.clone(),
|
| 79 |
+
user_info.get_display_name(),
|
| 80 |
+
token,
|
| 81 |
+
).map_err(AppError::Account)?;
|
| 82 |
+
|
| 83 |
+
modules::logger::log_info(&format!("Account added successfully: {}", account.email));
|
| 84 |
+
|
| 85 |
+
// 5. Auto refresh quota
|
| 86 |
+
let _ = internal_refresh_account_quota(&mut account).await;
|
| 87 |
+
|
| 88 |
+
Ok(ApiResponse::success(account))
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
/// Delete account
|
| 92 |
+
pub async fn delete_account(
|
| 93 |
+
Path(account_id): Path<String>,
|
| 94 |
+
) -> Result<Json<ApiResponse<()>>, AppError> {
|
| 95 |
+
modules::logger::log_info(&format!("Deleting account: {}", account_id));
|
| 96 |
+
modules::delete_account(&account_id).map_err(AppError::Account)?;
|
| 97 |
+
modules::logger::log_info(&format!("Account deleted: {}", account_id));
|
| 98 |
+
Ok(ApiResponse::success(()))
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/// Refresh account quota
|
| 102 |
+
pub async fn refresh_quota(
|
| 103 |
+
Path(account_id): Path<String>,
|
| 104 |
+
) -> Result<Json<ApiResponse<QuotaData>>, AppError> {
|
| 105 |
+
modules::logger::log_info(&format!("Refreshing quota for: {}", account_id));
|
| 106 |
+
|
| 107 |
+
let mut account = modules::load_account(&account_id).map_err(AppError::Account)?;
|
| 108 |
+
let quota = modules::account::fetch_quota_with_retry(&mut account).await?;
|
| 109 |
+
|
| 110 |
+
// Update account quota
|
| 111 |
+
modules::update_account_quota(&account_id, quota.clone()).map_err(AppError::Account)?;
|
| 112 |
+
|
| 113 |
+
Ok(ApiResponse::success(quota))
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/// Refresh stats response
|
| 117 |
+
#[derive(Serialize)]
|
| 118 |
+
pub struct RefreshStats {
|
| 119 |
+
total: usize,
|
| 120 |
+
success: usize,
|
| 121 |
+
failed: usize,
|
| 122 |
+
details: Vec<String>,
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/// Refresh all account quotas
|
| 126 |
+
pub async fn refresh_all_quotas() -> Result<Json<ApiResponse<RefreshStats>>, AppError> {
|
| 127 |
+
modules::logger::log_info("Starting batch quota refresh for all accounts");
|
| 128 |
+
let accounts = modules::list_accounts().map_err(AppError::Account)?;
|
| 129 |
+
|
| 130 |
+
let mut success = 0;
|
| 131 |
+
let mut failed = 0;
|
| 132 |
+
let mut details = Vec::new();
|
| 133 |
+
|
| 134 |
+
// Serial processing to ensure persistence safety
|
| 135 |
+
for mut account in accounts {
|
| 136 |
+
if let Some(ref q) = account.quota {
|
| 137 |
+
if q.is_forbidden {
|
| 138 |
+
modules::logger::log_info(&format!("Skipping {} (Forbidden)", account.email));
|
| 139 |
+
continue;
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
modules::logger::log_info(&format!("Processing {}", account.email));
|
| 144 |
+
|
| 145 |
+
match modules::account::fetch_quota_with_retry(&mut account).await {
|
| 146 |
+
Ok(quota) => {
|
| 147 |
+
if let Err(e) = modules::update_account_quota(&account.id, quota) {
|
| 148 |
+
failed += 1;
|
| 149 |
+
let msg = format!("Account {}: Save quota failed - {}", account.email, e);
|
| 150 |
+
details.push(msg.clone());
|
| 151 |
+
modules::logger::log_error(&msg);
|
| 152 |
+
} else {
|
| 153 |
+
success += 1;
|
| 154 |
+
modules::logger::log_info("Success");
|
| 155 |
+
}
|
| 156 |
+
},
|
| 157 |
+
Err(e) => {
|
| 158 |
+
failed += 1;
|
| 159 |
+
let msg = format!("Account {}: Fetch quota failed - {}", account.email, e);
|
| 160 |
+
details.push(msg.clone());
|
| 161 |
+
modules::logger::log_error(&msg);
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
modules::logger::log_info(&format!("Batch refresh completed: {} success, {} failed", success, failed));
|
| 167 |
+
Ok(ApiResponse::success(RefreshStats { total: success + failed, success, failed, details }))
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/// Get current account
|
| 171 |
+
pub async fn get_current_account() -> Result<Json<ApiResponse<Option<Account>>>, AppError> {
|
| 172 |
+
let account_id = modules::get_current_account_id().map_err(AppError::Account)?;
|
| 173 |
+
|
| 174 |
+
if let Some(id) = account_id {
|
| 175 |
+
let account = modules::load_account(&id).map_err(AppError::Account)?;
|
| 176 |
+
Ok(ApiResponse::success(Some(account)))
|
| 177 |
+
} else {
|
| 178 |
+
Ok(ApiResponse::success(None))
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/// Set current account
|
| 183 |
+
pub async fn set_current_account(
|
| 184 |
+
Path(account_id): Path<String>,
|
| 185 |
+
) -> Result<Json<ApiResponse<()>>, AppError> {
|
| 186 |
+
modules::logger::log_info(&format!("Setting current account: {}", account_id));
|
| 187 |
+
modules::set_current_account_id(&account_id).map_err(AppError::Account)?;
|
| 188 |
+
Ok(ApiResponse::success(()))
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
/// Internal helper: auto refresh quota after adding account
|
| 192 |
+
async fn internal_refresh_account_quota(account: &mut Account) -> Result<QuotaData, String> {
|
| 193 |
+
modules::logger::log_info(&format!("Auto refreshing quota: {}", account.email));
|
| 194 |
+
|
| 195 |
+
match modules::account::fetch_quota_with_retry(account).await {
|
| 196 |
+
Ok(quota) => {
|
| 197 |
+
let _ = modules::update_account_quota(&account.id, quota.clone());
|
| 198 |
+
Ok(quota)
|
| 199 |
+
},
|
| 200 |
+
Err(e) => {
|
| 201 |
+
modules::logger::log_warn(&format!("Auto refresh quota failed ({}): {}", account.email, e));
|
| 202 |
+
Err(e.to_string())
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
}
|
server/src/api/config.rs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use axum::Json;
|
| 2 |
+
use serde::Serialize;
|
| 3 |
+
|
| 4 |
+
use crate::models::AppConfig;
|
| 5 |
+
use crate::modules;
|
| 6 |
+
use crate::error::AppError;
|
| 7 |
+
|
| 8 |
+
/// API response wrapper
|
| 9 |
+
#[derive(Serialize)]
|
| 10 |
+
pub struct ApiResponse<T> {
|
| 11 |
+
pub success: bool,
|
| 12 |
+
pub data: Option<T>,
|
| 13 |
+
pub error: Option<String>,
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
impl<T: Serialize> ApiResponse<T> {
|
| 17 |
+
pub fn success(data: T) -> Json<Self> {
|
| 18 |
+
Json(Self {
|
| 19 |
+
success: true,
|
| 20 |
+
data: Some(data),
|
| 21 |
+
error: None,
|
| 22 |
+
})
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/// Load configuration
|
| 27 |
+
pub async fn load_config() -> Result<Json<ApiResponse<AppConfig>>, AppError> {
|
| 28 |
+
let config = modules::load_app_config().map_err(AppError::Config)?;
|
| 29 |
+
Ok(ApiResponse::success(config))
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/// Save configuration
|
| 33 |
+
pub async fn save_config(
|
| 34 |
+
Json(config): Json<AppConfig>,
|
| 35 |
+
) -> Result<Json<ApiResponse<()>>, AppError> {
|
| 36 |
+
modules::save_app_config(&config).map_err(AppError::Config)?;
|
| 37 |
+
modules::logger::log_info("Configuration saved");
|
| 38 |
+
Ok(ApiResponse::success(()))
|
| 39 |
+
}
|
server/src/api/mod.rs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pub mod accounts;
|
| 2 |
+
pub mod config;
|
| 3 |
+
pub mod proxy;
|
| 4 |
+
|
| 5 |
+
use axum::{Router, routing::{get, post, delete}};
|
| 6 |
+
|
| 7 |
+
/// Create API routes (without state for management endpoints)
|
| 8 |
+
pub fn api_routes<S>() -> Router<S>
|
| 9 |
+
where
|
| 10 |
+
S: Clone + Send + Sync + 'static,
|
| 11 |
+
{
|
| 12 |
+
Router::new()
|
| 13 |
+
// Account management
|
| 14 |
+
.route("/api/accounts", get(accounts::list_accounts))
|
| 15 |
+
.route("/api/accounts", post(accounts::add_account))
|
| 16 |
+
.route("/api/accounts/:id", delete(accounts::delete_account))
|
| 17 |
+
.route("/api/accounts/:id/quota", post(accounts::refresh_quota))
|
| 18 |
+
.route("/api/accounts/refresh-all", post(accounts::refresh_all_quotas))
|
| 19 |
+
.route("/api/accounts/current", get(accounts::get_current_account))
|
| 20 |
+
.route("/api/accounts/:id/set-current", post(accounts::set_current_account))
|
| 21 |
+
|
| 22 |
+
// Configuration management
|
| 23 |
+
.route("/api/config", get(config::load_config))
|
| 24 |
+
.route("/api/config", post(config::save_config))
|
| 25 |
+
|
| 26 |
+
// Proxy management
|
| 27 |
+
.route("/api/proxy/status", get(proxy::get_proxy_status))
|
| 28 |
+
.route("/api/proxy/mapping", post(proxy::update_model_mapping))
|
| 29 |
+
.route("/api/proxy/restart", post(proxy::restart_proxy))
|
| 30 |
+
.route("/api/proxy/generate-key", post(proxy::generate_api_key))
|
| 31 |
+
|
| 32 |
+
// Health check
|
| 33 |
+
.route("/api/health", get(health_check))
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/// Health check endpoint
|
| 37 |
+
async fn health_check() -> axum::Json<serde_json::Value> {
|
| 38 |
+
axum::Json(serde_json::json!({
|
| 39 |
+
"status": "ok",
|
| 40 |
+
"version": env!("CARGO_PKG_VERSION")
|
| 41 |
+
}))
|
| 42 |
+
}
|
server/src/api/proxy.rs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use axum::Json;
|
| 2 |
+
use serde::Serialize;
|
| 3 |
+
use uuid::Uuid;
|
| 4 |
+
|
| 5 |
+
use crate::proxy::config::ProxyConfig;
|
| 6 |
+
use crate::modules;
|
| 7 |
+
use crate::error::AppError;
|
| 8 |
+
|
| 9 |
+
use super::config::ApiResponse;
|
| 10 |
+
|
| 11 |
+
/// Proxy status response
|
| 12 |
+
#[derive(Serialize)]
|
| 13 |
+
pub struct ProxyStatus {
|
| 14 |
+
pub running: bool,
|
| 15 |
+
pub port: u16,
|
| 16 |
+
pub base_url: String,
|
| 17 |
+
#[serde(rename = "active_accounts")]
|
| 18 |
+
pub active_accounts: usize,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
/// Get proxy status
|
| 22 |
+
/// In cloud deployment mode, the proxy is always running
|
| 23 |
+
pub async fn get_proxy_status() -> Result<Json<ApiResponse<ProxyStatus>>, AppError> {
|
| 24 |
+
let _config = modules::load_app_config().map_err(AppError::Config)?;
|
| 25 |
+
let account_index = modules::load_account_index().map_err(AppError::Account)?;
|
| 26 |
+
|
| 27 |
+
let port = std::env::var("PORT")
|
| 28 |
+
.unwrap_or_else(|_| "7860".to_string())
|
| 29 |
+
.parse::<u16>()
|
| 30 |
+
.unwrap_or(7860);
|
| 31 |
+
|
| 32 |
+
let base_url = std::env::var("SPACE_HOST")
|
| 33 |
+
.map(|host| format!("https://{}", host))
|
| 34 |
+
.unwrap_or_else(|_| format!("http://localhost:{}", port));
|
| 35 |
+
|
| 36 |
+
let status = ProxyStatus {
|
| 37 |
+
running: true, // Always running in cloud mode
|
| 38 |
+
port,
|
| 39 |
+
base_url,
|
| 40 |
+
active_accounts: account_index.accounts.len(),
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
Ok(ApiResponse::success(status))
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/// Update model mapping
|
| 47 |
+
pub async fn update_model_mapping(
|
| 48 |
+
Json(config): Json<ProxyConfig>,
|
| 49 |
+
) -> Result<Json<ApiResponse<()>>, AppError> {
|
| 50 |
+
// Load current config and update proxy section
|
| 51 |
+
let mut app_config = modules::load_app_config().map_err(AppError::Config)?;
|
| 52 |
+
app_config.proxy.anthropic_mapping = config.anthropic_mapping;
|
| 53 |
+
app_config.proxy.openai_mapping = config.openai_mapping;
|
| 54 |
+
app_config.proxy.custom_mapping = config.custom_mapping;
|
| 55 |
+
|
| 56 |
+
modules::save_app_config(&app_config).map_err(AppError::Config)?;
|
| 57 |
+
modules::logger::log_info("Model mapping updated");
|
| 58 |
+
|
| 59 |
+
Ok(ApiResponse::success(()))
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/// Restart proxy service (reload configuration)
|
| 63 |
+
/// In cloud mode, this doesn't actually restart the server,
|
| 64 |
+
/// but signals that configuration has been updated
|
| 65 |
+
pub async fn restart_proxy(
|
| 66 |
+
Json(config): Json<ProxyConfig>,
|
| 67 |
+
) -> Result<Json<ApiResponse<()>>, AppError> {
|
| 68 |
+
// Load and update configuration
|
| 69 |
+
let mut app_config = modules::load_app_config().map_err(AppError::Config)?;
|
| 70 |
+
app_config.proxy = config;
|
| 71 |
+
modules::save_app_config(&app_config).map_err(AppError::Config)?;
|
| 72 |
+
|
| 73 |
+
modules::logger::log_info("Proxy configuration updated (restart not needed in cloud mode)");
|
| 74 |
+
|
| 75 |
+
Ok(ApiResponse::success(()))
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/// Generate a new API key
|
| 79 |
+
pub async fn generate_api_key() -> Result<Json<ApiResponse<String>>, AppError> {
|
| 80 |
+
let new_key = format!("ag-{}", Uuid::new_v4().to_string().replace("-", ""));
|
| 81 |
+
|
| 82 |
+
// Update config with new key
|
| 83 |
+
let mut app_config = modules::load_app_config().map_err(AppError::Config)?;
|
| 84 |
+
app_config.proxy.api_key = new_key.clone();
|
| 85 |
+
modules::save_app_config(&app_config).map_err(AppError::Config)?;
|
| 86 |
+
|
| 87 |
+
modules::logger::log_info("New API key generated");
|
| 88 |
+
|
| 89 |
+
Ok(ApiResponse::success(new_key))
|
| 90 |
+
}
|
server/src/error.rs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use serde::Serialize;
|
| 2 |
+
use thiserror::Error;
|
| 3 |
+
|
| 4 |
+
#[derive(Error, Debug)]
|
| 5 |
+
pub enum AppError {
|
| 6 |
+
#[error("Network error: {0}")]
|
| 7 |
+
Network(#[from] reqwest::Error),
|
| 8 |
+
|
| 9 |
+
#[error("IO error: {0}")]
|
| 10 |
+
Io(#[from] std::io::Error),
|
| 11 |
+
|
| 12 |
+
#[error("JSON error: {0}")]
|
| 13 |
+
Json(#[from] serde_json::Error),
|
| 14 |
+
|
| 15 |
+
#[error("OAuth error: {0}")]
|
| 16 |
+
OAuth(String),
|
| 17 |
+
|
| 18 |
+
#[error("Configuration error: {0}")]
|
| 19 |
+
Config(String),
|
| 20 |
+
|
| 21 |
+
#[error("Account error: {0}")]
|
| 22 |
+
Account(String),
|
| 23 |
+
|
| 24 |
+
#[error("Proxy error: {0}")]
|
| 25 |
+
Proxy(String),
|
| 26 |
+
|
| 27 |
+
#[error("Unknown error: {0}")]
|
| 28 |
+
Unknown(String),
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Implement Serialize for API responses
|
| 32 |
+
impl Serialize for AppError {
|
| 33 |
+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
| 34 |
+
where
|
| 35 |
+
S: serde::Serializer,
|
| 36 |
+
{
|
| 37 |
+
serializer.serialize_str(self.to_string().as_str())
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Result type alias
|
| 42 |
+
pub type AppResult<T> = Result<T, AppError>;
|
| 43 |
+
|
| 44 |
+
// Implement IntoResponse for Axum
|
| 45 |
+
impl axum::response::IntoResponse for AppError {
|
| 46 |
+
fn into_response(self) -> axum::response::Response {
|
| 47 |
+
let status = match &self {
|
| 48 |
+
AppError::Network(_) => axum::http::StatusCode::BAD_GATEWAY,
|
| 49 |
+
AppError::Io(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
| 50 |
+
AppError::Json(_) => axum::http::StatusCode::BAD_REQUEST,
|
| 51 |
+
AppError::OAuth(_) => axum::http::StatusCode::UNAUTHORIZED,
|
| 52 |
+
AppError::Config(_) => axum::http::StatusCode::BAD_REQUEST,
|
| 53 |
+
AppError::Account(_) => axum::http::StatusCode::NOT_FOUND,
|
| 54 |
+
AppError::Proxy(_) => axum::http::StatusCode::SERVICE_UNAVAILABLE,
|
| 55 |
+
AppError::Unknown(_) => axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
let body = serde_json::json!({
|
| 59 |
+
"success": false,
|
| 60 |
+
"error": self.to_string()
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
(status, axum::Json(body)).into_response()
|
| 64 |
+
}
|
| 65 |
+
}
|
server/src/lib.rs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pub mod models;
|
| 2 |
+
pub mod modules;
|
| 3 |
+
pub mod utils;
|
| 4 |
+
pub mod proxy;
|
| 5 |
+
pub mod error;
|
| 6 |
+
pub mod api;
|
| 7 |
+
|
| 8 |
+
pub use proxy::ProxyConfig;
|
server/src/main.rs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use std::sync::Arc;
|
| 2 |
+
use std::path::PathBuf;
|
| 3 |
+
use axum::{Router, extract::DefaultBodyLimit};
|
| 4 |
+
use tower_http::{trace::TraceLayer, services::ServeDir};
|
| 5 |
+
use tracing::info;
|
| 6 |
+
|
| 7 |
+
use antigravity_server::{
|
| 8 |
+
modules,
|
| 9 |
+
proxy::TokenManager,
|
| 10 |
+
api,
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
#[tokio::main]
|
| 14 |
+
async fn main() {
|
| 15 |
+
// Initialize logger
|
| 16 |
+
modules::logger::init_logger();
|
| 17 |
+
|
| 18 |
+
info!("Starting Antigravity API Proxy Server...");
|
| 19 |
+
|
| 20 |
+
// Get data directory
|
| 21 |
+
let data_dir = if PathBuf::from("/data").exists() {
|
| 22 |
+
PathBuf::from("/data")
|
| 23 |
+
} else {
|
| 24 |
+
dirs::home_dir()
|
| 25 |
+
.expect("Cannot get home directory")
|
| 26 |
+
.join(".antigravity_tools")
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
info!("Using data directory: {:?}", data_dir);
|
| 30 |
+
|
| 31 |
+
// Load configuration
|
| 32 |
+
let config = modules::load_app_config().unwrap_or_default();
|
| 33 |
+
let proxy_config = config.proxy.clone();
|
| 34 |
+
|
| 35 |
+
// Initialize token manager
|
| 36 |
+
let token_manager = Arc::new(TokenManager::new(data_dir.clone()));
|
| 37 |
+
|
| 38 |
+
// Load accounts from file system
|
| 39 |
+
match token_manager.load_accounts().await {
|
| 40 |
+
Ok(count) => {
|
| 41 |
+
info!("Loaded {} accounts into token manager", count);
|
| 42 |
+
}
|
| 43 |
+
Err(e) => {
|
| 44 |
+
info!("Could not load accounts: {} (this is ok for first run)", e);
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Build proxy state
|
| 49 |
+
let mapping_state = Arc::new(tokio::sync::RwLock::new(proxy_config.anthropic_mapping.clone()));
|
| 50 |
+
let openai_mapping_state = Arc::new(tokio::sync::RwLock::new(proxy_config.openai_mapping.clone()));
|
| 51 |
+
let custom_mapping_state = Arc::new(tokio::sync::RwLock::new(proxy_config.custom_mapping.clone()));
|
| 52 |
+
let proxy_state = Arc::new(tokio::sync::RwLock::new(proxy_config.upstream_proxy.clone()));
|
| 53 |
+
|
| 54 |
+
let app_state = antigravity_server::proxy::server::AppState {
|
| 55 |
+
token_manager: token_manager.clone(),
|
| 56 |
+
anthropic_mapping: mapping_state,
|
| 57 |
+
openai_mapping: openai_mapping_state,
|
| 58 |
+
custom_mapping: custom_mapping_state,
|
| 59 |
+
request_timeout: 300,
|
| 60 |
+
thought_signature_map: Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())),
|
| 61 |
+
upstream_proxy: proxy_state,
|
| 62 |
+
upstream: Arc::new(antigravity_server::proxy::upstream::client::UpstreamClient::new(
|
| 63 |
+
Some(proxy_config.upstream_proxy.clone())
|
| 64 |
+
)),
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
// Build routes
|
| 68 |
+
use antigravity_server::proxy::handlers;
|
| 69 |
+
use axum::routing::{get, post};
|
| 70 |
+
|
| 71 |
+
let proxy_routes = Router::new()
|
| 72 |
+
// OpenAI Protocol
|
| 73 |
+
.route("/v1/models", get(handlers::openai::handle_list_models))
|
| 74 |
+
.route("/v1/chat/completions", post(handlers::openai::handle_chat_completions))
|
| 75 |
+
.route("/v1/completions", post(handlers::openai::handle_completions))
|
| 76 |
+
.route("/v1/responses", post(handlers::openai::handle_completions))
|
| 77 |
+
// Claude Protocol
|
| 78 |
+
.route("/v1/messages", post(handlers::claude::handle_messages))
|
| 79 |
+
.route("/v1/messages/count_tokens", post(handlers::claude::handle_count_tokens))
|
| 80 |
+
.route("/v1/models/claude", get(handlers::claude::handle_list_models))
|
| 81 |
+
// Gemini Protocol
|
| 82 |
+
.route("/v1beta/models", get(handlers::gemini::handle_list_models))
|
| 83 |
+
.route("/v1beta/models/:model", get(handlers::gemini::handle_get_model).post(handlers::gemini::handle_generate))
|
| 84 |
+
.route("/v1beta/models/:model/countTokens", post(handlers::gemini::handle_count_tokens))
|
| 85 |
+
// Health check
|
| 86 |
+
.route("/healthz", get(health_check))
|
| 87 |
+
.layer(DefaultBodyLimit::max(100 * 1024 * 1024))
|
| 88 |
+
.layer(TraceLayer::new_for_http())
|
| 89 |
+
.layer(axum::middleware::from_fn(antigravity_server::proxy::middleware::auth_middleware))
|
| 90 |
+
.layer(antigravity_server::proxy::middleware::cors_layer())
|
| 91 |
+
.with_state(app_state);
|
| 92 |
+
|
| 93 |
+
// Combine API routes and proxy routes
|
| 94 |
+
// Apply basic auth middleware to admin UI and management API
|
| 95 |
+
// Proxy routes (/v1/*) are protected by API key auth instead
|
| 96 |
+
let app = Router::new()
|
| 97 |
+
.merge(api::api_routes::<()>())
|
| 98 |
+
.merge(proxy_routes)
|
| 99 |
+
.fallback_service(ServeDir::new("static"))
|
| 100 |
+
.layer(axum::middleware::from_fn(antigravity_server::proxy::middleware::basic_auth_middleware));
|
| 101 |
+
|
| 102 |
+
// Bind to port 7860 (HuggingFace Spaces default)
|
| 103 |
+
let port = std::env::var("PORT")
|
| 104 |
+
.unwrap_or_else(|_| "7860".to_string())
|
| 105 |
+
.parse::<u16>()
|
| 106 |
+
.unwrap_or(7860);
|
| 107 |
+
|
| 108 |
+
let addr = format!("0.0.0.0:{}", port);
|
| 109 |
+
info!("Server listening on http://{}", addr);
|
| 110 |
+
|
| 111 |
+
let listener = tokio::net::TcpListener::bind(&addr)
|
| 112 |
+
.await
|
| 113 |
+
.expect("Failed to bind address");
|
| 114 |
+
|
| 115 |
+
axum::serve(listener, app)
|
| 116 |
+
.await
|
| 117 |
+
.expect("Server error");
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/// Health check handler
|
| 121 |
+
async fn health_check() -> axum::Json<serde_json::Value> {
|
| 122 |
+
axum::Json(serde_json::json!({
|
| 123 |
+
"status": "ok",
|
| 124 |
+
"version": env!("CARGO_PKG_VERSION")
|
| 125 |
+
}))
|
| 126 |
+
}
|
server/src/models/account.rs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
use super::{token::TokenData, quota::QuotaData};
|
| 3 |
+
|
| 4 |
+
/// Account data structure
|
| 5 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 6 |
+
pub struct Account {
|
| 7 |
+
pub id: String,
|
| 8 |
+
pub email: String,
|
| 9 |
+
pub name: Option<String>,
|
| 10 |
+
pub token: TokenData,
|
| 11 |
+
pub quota: Option<QuotaData>,
|
| 12 |
+
pub created_at: i64,
|
| 13 |
+
pub last_used: i64,
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
impl Account {
|
| 17 |
+
pub fn new(id: String, email: String, token: TokenData) -> Self {
|
| 18 |
+
let now = chrono::Utc::now().timestamp();
|
| 19 |
+
Self {
|
| 20 |
+
id,
|
| 21 |
+
email,
|
| 22 |
+
name: None,
|
| 23 |
+
token,
|
| 24 |
+
quota: None,
|
| 25 |
+
created_at: now,
|
| 26 |
+
last_used: now,
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
pub fn update_last_used(&mut self) {
|
| 31 |
+
self.last_used = chrono::Utc::now().timestamp();
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
pub fn update_quota(&mut self, quota: QuotaData) {
|
| 35 |
+
self.quota = Some(quota);
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/// Account index (accounts.json)
|
| 40 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 41 |
+
pub struct AccountIndex {
|
| 42 |
+
pub version: String,
|
| 43 |
+
pub accounts: Vec<AccountSummary>,
|
| 44 |
+
pub current_account_id: Option<String>,
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
/// Account summary
|
| 48 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 49 |
+
pub struct AccountSummary {
|
| 50 |
+
pub id: String,
|
| 51 |
+
pub email: String,
|
| 52 |
+
pub name: Option<String>,
|
| 53 |
+
pub created_at: i64,
|
| 54 |
+
pub last_used: i64,
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
impl AccountIndex {
|
| 58 |
+
pub fn new() -> Self {
|
| 59 |
+
Self {
|
| 60 |
+
version: "2.0".to_string(),
|
| 61 |
+
accounts: Vec::new(),
|
| 62 |
+
current_account_id: None,
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
impl Default for AccountIndex {
|
| 68 |
+
fn default() -> Self {
|
| 69 |
+
Self::new()
|
| 70 |
+
}
|
| 71 |
+
}
|
server/src/models/config.rs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
use crate::proxy::ProxyConfig;
|
| 3 |
+
|
| 4 |
+
/// Application configuration
|
| 5 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 6 |
+
pub struct AppConfig {
|
| 7 |
+
pub language: String,
|
| 8 |
+
pub theme: String,
|
| 9 |
+
pub auto_refresh: bool,
|
| 10 |
+
pub refresh_interval: i32, // minutes
|
| 11 |
+
#[serde(default)]
|
| 12 |
+
pub proxy: ProxyConfig,
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
impl AppConfig {
|
| 16 |
+
pub fn new() -> Self {
|
| 17 |
+
Self {
|
| 18 |
+
language: "zh".to_string(),
|
| 19 |
+
theme: "system".to_string(),
|
| 20 |
+
auto_refresh: false,
|
| 21 |
+
refresh_interval: 15,
|
| 22 |
+
proxy: ProxyConfig::default(),
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
impl Default for AppConfig {
|
| 28 |
+
fn default() -> Self {
|
| 29 |
+
Self::new()
|
| 30 |
+
}
|
| 31 |
+
}
|
server/src/models/mod.rs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pub mod account;
|
| 2 |
+
pub mod token;
|
| 3 |
+
pub mod quota;
|
| 4 |
+
pub mod config;
|
| 5 |
+
|
| 6 |
+
pub use account::{Account, AccountIndex, AccountSummary};
|
| 7 |
+
pub use token::TokenData;
|
| 8 |
+
pub use quota::QuotaData;
|
| 9 |
+
pub use config::AppConfig;
|
server/src/models/quota.rs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
|
| 3 |
+
/// Model quota information
|
| 4 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 5 |
+
pub struct ModelQuota {
|
| 6 |
+
pub name: String,
|
| 7 |
+
pub percentage: i32, // Remaining percentage 0-100
|
| 8 |
+
pub reset_time: String,
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
/// Quota data structure
|
| 12 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 13 |
+
pub struct QuotaData {
|
| 14 |
+
pub models: Vec<ModelQuota>,
|
| 15 |
+
pub last_updated: i64,
|
| 16 |
+
#[serde(default)]
|
| 17 |
+
pub is_forbidden: bool,
|
| 18 |
+
/// Subscription tier (FREE/PRO/ULTRA)
|
| 19 |
+
#[serde(default)]
|
| 20 |
+
pub subscription_tier: Option<String>,
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
impl QuotaData {
|
| 24 |
+
pub fn new() -> Self {
|
| 25 |
+
Self {
|
| 26 |
+
models: Vec::new(),
|
| 27 |
+
last_updated: chrono::Utc::now().timestamp(),
|
| 28 |
+
is_forbidden: false,
|
| 29 |
+
subscription_tier: None,
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
pub fn add_model(&mut self, name: String, percentage: i32, reset_time: String) {
|
| 34 |
+
self.models.push(ModelQuota {
|
| 35 |
+
name,
|
| 36 |
+
percentage,
|
| 37 |
+
reset_time,
|
| 38 |
+
});
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
impl Default for QuotaData {
|
| 43 |
+
fn default() -> Self {
|
| 44 |
+
Self::new()
|
| 45 |
+
}
|
| 46 |
+
}
|
server/src/models/token.rs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
|
| 3 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 4 |
+
pub struct TokenData {
|
| 5 |
+
pub access_token: String,
|
| 6 |
+
pub refresh_token: String,
|
| 7 |
+
pub expires_in: i64,
|
| 8 |
+
pub expiry_timestamp: i64,
|
| 9 |
+
pub token_type: String,
|
| 10 |
+
pub email: Option<String>,
|
| 11 |
+
/// Google Cloud project ID for API requests
|
| 12 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 13 |
+
pub project_id: Option<String>,
|
| 14 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 15 |
+
pub session_id: Option<String>,
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
impl TokenData {
|
| 19 |
+
pub fn new(
|
| 20 |
+
access_token: String,
|
| 21 |
+
refresh_token: String,
|
| 22 |
+
expires_in: i64,
|
| 23 |
+
email: Option<String>,
|
| 24 |
+
project_id: Option<String>,
|
| 25 |
+
session_id: Option<String>,
|
| 26 |
+
) -> Self {
|
| 27 |
+
let expiry_timestamp = chrono::Utc::now().timestamp() + expires_in;
|
| 28 |
+
Self {
|
| 29 |
+
access_token,
|
| 30 |
+
refresh_token,
|
| 31 |
+
expires_in,
|
| 32 |
+
expiry_timestamp,
|
| 33 |
+
token_type: "Bearer".to_string(),
|
| 34 |
+
email,
|
| 35 |
+
project_id,
|
| 36 |
+
session_id,
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
}
|
server/src/modules/account.rs
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use std::fs;
|
| 2 |
+
use std::path::PathBuf;
|
| 3 |
+
use uuid::Uuid;
|
| 4 |
+
|
| 5 |
+
use crate::models::{Account, AccountIndex, AccountSummary, TokenData, QuotaData};
|
| 6 |
+
use crate::modules;
|
| 7 |
+
use once_cell::sync::Lazy;
|
| 8 |
+
use std::sync::Mutex;
|
| 9 |
+
|
| 10 |
+
/// Global account write lock to prevent concurrent index file corruption
|
| 11 |
+
static ACCOUNT_INDEX_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
|
| 12 |
+
|
| 13 |
+
const DATA_DIR_LOCAL: &str = ".antigravity_tools";
|
| 14 |
+
const DATA_DIR_CLOUD: &str = "/data";
|
| 15 |
+
const ACCOUNTS_INDEX: &str = "accounts.json";
|
| 16 |
+
const ACCOUNTS_DIR: &str = "accounts";
|
| 17 |
+
|
| 18 |
+
/// Get data directory path (cloud-compatible)
|
| 19 |
+
pub fn get_data_dir() -> Result<PathBuf, String> {
|
| 20 |
+
// Check for cloud environment (/data)
|
| 21 |
+
let cloud_path = PathBuf::from(DATA_DIR_CLOUD);
|
| 22 |
+
if cloud_path.exists() {
|
| 23 |
+
return Ok(cloud_path);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Fallback to local development path
|
| 27 |
+
let home = dirs::home_dir().ok_or("Cannot get user home directory")?;
|
| 28 |
+
let data_dir = home.join(DATA_DIR_LOCAL);
|
| 29 |
+
|
| 30 |
+
// Ensure directory exists
|
| 31 |
+
if !data_dir.exists() {
|
| 32 |
+
fs::create_dir_all(&data_dir)
|
| 33 |
+
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
Ok(data_dir)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/// Get accounts directory path
|
| 40 |
+
pub fn get_accounts_dir() -> Result<PathBuf, String> {
|
| 41 |
+
let data_dir = get_data_dir()?;
|
| 42 |
+
let accounts_dir = data_dir.join(ACCOUNTS_DIR);
|
| 43 |
+
|
| 44 |
+
if !accounts_dir.exists() {
|
| 45 |
+
fs::create_dir_all(&accounts_dir)
|
| 46 |
+
.map_err(|e| format!("Failed to create accounts directory: {}", e))?;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
Ok(accounts_dir)
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/// Load account index
|
| 53 |
+
pub fn load_account_index() -> Result<AccountIndex, String> {
|
| 54 |
+
let data_dir = get_data_dir()?;
|
| 55 |
+
let index_path = data_dir.join(ACCOUNTS_INDEX);
|
| 56 |
+
|
| 57 |
+
if !index_path.exists() {
|
| 58 |
+
modules::logger::log_warn("Account index file does not exist");
|
| 59 |
+
return Ok(AccountIndex::new());
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
let content = fs::read_to_string(&index_path)
|
| 63 |
+
.map_err(|e| format!("Failed to read account index: {}", e))?;
|
| 64 |
+
|
| 65 |
+
let index: AccountIndex = serde_json::from_str(&content)
|
| 66 |
+
.map_err(|e| format!("Failed to parse account index: {}", e))?;
|
| 67 |
+
|
| 68 |
+
modules::logger::log_info(&format!("Loaded index with {} accounts", index.accounts.len()));
|
| 69 |
+
Ok(index)
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/// Save account index (atomic write)
|
| 73 |
+
pub fn save_account_index(index: &AccountIndex) -> Result<(), String> {
|
| 74 |
+
let data_dir = get_data_dir()?;
|
| 75 |
+
let index_path = data_dir.join(ACCOUNTS_INDEX);
|
| 76 |
+
let temp_path = data_dir.join(format!("{}.tmp", ACCOUNTS_INDEX));
|
| 77 |
+
|
| 78 |
+
let content = serde_json::to_string_pretty(index)
|
| 79 |
+
.map_err(|e| format!("Failed to serialize account index: {}", e))?;
|
| 80 |
+
|
| 81 |
+
// Write to temp file
|
| 82 |
+
fs::write(&temp_path, content)
|
| 83 |
+
.map_err(|e| format!("Failed to write temp index file: {}", e))?;
|
| 84 |
+
|
| 85 |
+
// Atomic rename
|
| 86 |
+
fs::rename(temp_path, index_path)
|
| 87 |
+
.map_err(|e| format!("Failed to replace index file: {}", e))
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
/// Load account data
|
| 91 |
+
pub fn load_account(account_id: &str) -> Result<Account, String> {
|
| 92 |
+
let accounts_dir = get_accounts_dir()?;
|
| 93 |
+
let account_path = accounts_dir.join(format!("{}.json", account_id));
|
| 94 |
+
|
| 95 |
+
if !account_path.exists() {
|
| 96 |
+
return Err(format!("Account not found: {}", account_id));
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
let content = fs::read_to_string(&account_path)
|
| 100 |
+
.map_err(|e| format!("Failed to read account data: {}", e))?;
|
| 101 |
+
|
| 102 |
+
serde_json::from_str(&content)
|
| 103 |
+
.map_err(|e| format!("Failed to parse account data: {}", e))
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/// Save account data
|
| 107 |
+
pub fn save_account(account: &Account) -> Result<(), String> {
|
| 108 |
+
let accounts_dir = get_accounts_dir()?;
|
| 109 |
+
let account_path = accounts_dir.join(format!("{}.json", account.id));
|
| 110 |
+
|
| 111 |
+
let content = serde_json::to_string_pretty(account)
|
| 112 |
+
.map_err(|e| format!("Failed to serialize account data: {}", e))?;
|
| 113 |
+
|
| 114 |
+
fs::write(&account_path, content)
|
| 115 |
+
.map_err(|e| format!("Failed to save account data: {}", e))
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/// List all accounts
|
| 119 |
+
pub fn list_accounts() -> Result<Vec<Account>, String> {
|
| 120 |
+
modules::logger::log_info("Listing accounts...");
|
| 121 |
+
let mut index = load_account_index()?;
|
| 122 |
+
let mut accounts = Vec::new();
|
| 123 |
+
let mut invalid_ids = Vec::new();
|
| 124 |
+
|
| 125 |
+
for summary in &index.accounts {
|
| 126 |
+
match load_account(&summary.id) {
|
| 127 |
+
Ok(account) => accounts.push(account),
|
| 128 |
+
Err(e) => {
|
| 129 |
+
modules::logger::log_error(&format!("Failed to load account {}: {}", summary.id, e));
|
| 130 |
+
if e.contains("Account not found") || e.contains("Os { code: 2,") || e.contains("No such file") {
|
| 131 |
+
invalid_ids.push(summary.id.clone());
|
| 132 |
+
}
|
| 133 |
+
},
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
// Auto-fix index: remove invalid account IDs
|
| 138 |
+
if !invalid_ids.is_empty() {
|
| 139 |
+
modules::logger::log_warn(&format!("Found {} invalid account indexes, cleaning up...", invalid_ids.len()));
|
| 140 |
+
|
| 141 |
+
index.accounts.retain(|s| !invalid_ids.contains(&s.id));
|
| 142 |
+
|
| 143 |
+
if let Some(current_id) = &index.current_account_id {
|
| 144 |
+
if invalid_ids.contains(current_id) {
|
| 145 |
+
index.current_account_id = index.accounts.first().map(|s| s.id.clone());
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
if let Err(e) = save_account_index(&index) {
|
| 150 |
+
modules::logger::log_error(&format!("Failed to clean up index: {}", e));
|
| 151 |
+
} else {
|
| 152 |
+
modules::logger::log_info("Index cleanup completed");
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
Ok(accounts)
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
/// Add account
|
| 160 |
+
pub fn add_account(email: String, name: Option<String>, token: TokenData) -> Result<Account, String> {
|
| 161 |
+
let _lock = ACCOUNT_INDEX_LOCK.lock().map_err(|e| format!("Failed to get lock: {}", e))?;
|
| 162 |
+
let mut index = load_account_index()?;
|
| 163 |
+
|
| 164 |
+
// Check if already exists
|
| 165 |
+
if index.accounts.iter().any(|s| s.email == email) {
|
| 166 |
+
return Err(format!("Account already exists: {}", email));
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
// Create new account
|
| 170 |
+
let account_id = Uuid::new_v4().to_string();
|
| 171 |
+
let mut account = Account::new(account_id.clone(), email.clone(), token);
|
| 172 |
+
account.name = name.clone();
|
| 173 |
+
|
| 174 |
+
// Save account data
|
| 175 |
+
save_account(&account)?;
|
| 176 |
+
|
| 177 |
+
// Update index
|
| 178 |
+
index.accounts.push(AccountSummary {
|
| 179 |
+
id: account_id.clone(),
|
| 180 |
+
email: email.clone(),
|
| 181 |
+
name: name.clone(),
|
| 182 |
+
created_at: account.created_at,
|
| 183 |
+
last_used: account.last_used,
|
| 184 |
+
});
|
| 185 |
+
|
| 186 |
+
// If first account, set as current
|
| 187 |
+
if index.current_account_id.is_none() {
|
| 188 |
+
index.current_account_id = Some(account_id);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
save_account_index(&index)?;
|
| 192 |
+
|
| 193 |
+
Ok(account)
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
/// Add or update account
|
| 197 |
+
pub fn upsert_account(email: String, name: Option<String>, token: TokenData) -> Result<Account, String> {
|
| 198 |
+
let _lock = ACCOUNT_INDEX_LOCK.lock().map_err(|e| format!("Failed to get lock: {}", e))?;
|
| 199 |
+
let mut index = load_account_index()?;
|
| 200 |
+
|
| 201 |
+
// Find account ID if exists
|
| 202 |
+
let existing_account_id = index.accounts.iter()
|
| 203 |
+
.find(|s| s.email == email)
|
| 204 |
+
.map(|s| s.id.clone());
|
| 205 |
+
|
| 206 |
+
if let Some(account_id) = existing_account_id {
|
| 207 |
+
// Update existing account
|
| 208 |
+
match load_account(&account_id) {
|
| 209 |
+
Ok(mut account) => {
|
| 210 |
+
account.token = token;
|
| 211 |
+
account.name = name.clone();
|
| 212 |
+
account.update_last_used();
|
| 213 |
+
save_account(&account)?;
|
| 214 |
+
|
| 215 |
+
// Sync update name in index
|
| 216 |
+
if let Some(idx_summary) = index.accounts.iter_mut().find(|s| s.id == account_id) {
|
| 217 |
+
idx_summary.name = name;
|
| 218 |
+
save_account_index(&index)?;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
return Ok(account);
|
| 222 |
+
},
|
| 223 |
+
Err(e) => {
|
| 224 |
+
modules::logger::log_warn(&format!("Account {} file missing ({}), recreating...", account_id, e));
|
| 225 |
+
// Index exists but file missing, recreate
|
| 226 |
+
let mut account = Account::new(account_id.clone(), email.clone(), token);
|
| 227 |
+
account.name = name.clone();
|
| 228 |
+
save_account(&account)?;
|
| 229 |
+
|
| 230 |
+
if let Some(idx_summary) = index.accounts.iter_mut().find(|s| s.id == account_id) {
|
| 231 |
+
idx_summary.name = name;
|
| 232 |
+
save_account_index(&index)?;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
return Ok(account);
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
// Not exists, add new
|
| 241 |
+
drop(_lock);
|
| 242 |
+
add_account(email, name, token)
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/// Delete account
|
| 246 |
+
pub fn delete_account(account_id: &str) -> Result<(), String> {
|
| 247 |
+
let _lock = ACCOUNT_INDEX_LOCK.lock().map_err(|e| format!("Failed to get lock: {}", e))?;
|
| 248 |
+
let mut index = load_account_index()?;
|
| 249 |
+
|
| 250 |
+
// Remove from index
|
| 251 |
+
let original_len = index.accounts.len();
|
| 252 |
+
index.accounts.retain(|s| s.id != account_id);
|
| 253 |
+
|
| 254 |
+
if index.accounts.len() == original_len {
|
| 255 |
+
return Err(format!("Account ID not found: {}", account_id));
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
// If current account, clear it
|
| 259 |
+
if index.current_account_id.as_deref() == Some(account_id) {
|
| 260 |
+
index.current_account_id = index.accounts.first().map(|s| s.id.clone());
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
save_account_index(&index)?;
|
| 264 |
+
|
| 265 |
+
// Delete account file
|
| 266 |
+
let accounts_dir = get_accounts_dir()?;
|
| 267 |
+
let account_path = accounts_dir.join(format!("{}.json", account_id));
|
| 268 |
+
|
| 269 |
+
if account_path.exists() {
|
| 270 |
+
fs::remove_file(&account_path)
|
| 271 |
+
.map_err(|e| format!("Failed to delete account file: {}", e))?;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
Ok(())
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
/// Batch delete accounts (atomic index operation)
|
| 278 |
+
pub fn delete_accounts(account_ids: &[String]) -> Result<(), String> {
|
| 279 |
+
let _lock = ACCOUNT_INDEX_LOCK.lock().map_err(|e| format!("Failed to get lock: {}", e))?;
|
| 280 |
+
let mut index = load_account_index()?;
|
| 281 |
+
|
| 282 |
+
let accounts_dir = get_accounts_dir()?;
|
| 283 |
+
|
| 284 |
+
for account_id in account_ids {
|
| 285 |
+
// Remove from index
|
| 286 |
+
index.accounts.retain(|s| &s.id != account_id);
|
| 287 |
+
|
| 288 |
+
// If current account, clear it
|
| 289 |
+
if index.current_account_id.as_deref() == Some(account_id) {
|
| 290 |
+
index.current_account_id = None;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
// Delete account file
|
| 294 |
+
let account_path = accounts_dir.join(format!("{}.json", account_id));
|
| 295 |
+
if account_path.exists() {
|
| 296 |
+
let _ = fs::remove_file(&account_path);
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
// If current account is empty, try to select first as default
|
| 301 |
+
if index.current_account_id.is_none() {
|
| 302 |
+
index.current_account_id = index.accounts.first().map(|s| s.id.clone());
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
save_account_index(&index)
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
/// Get current account ID
|
| 309 |
+
pub fn get_current_account_id() -> Result<Option<String>, String> {
|
| 310 |
+
let index = load_account_index()?;
|
| 311 |
+
Ok(index.current_account_id)
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
/// Get current active account info
|
| 315 |
+
pub fn get_current_account() -> Result<Option<Account>, String> {
|
| 316 |
+
if let Some(id) = get_current_account_id()? {
|
| 317 |
+
Ok(Some(load_account(&id)?))
|
| 318 |
+
} else {
|
| 319 |
+
Ok(None)
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
/// Set current active account ID
|
| 324 |
+
pub fn set_current_account_id(account_id: &str) -> Result<(), String> {
|
| 325 |
+
let _lock = ACCOUNT_INDEX_LOCK.lock().map_err(|e| format!("Failed to get lock: {}", e))?;
|
| 326 |
+
let mut index = load_account_index()?;
|
| 327 |
+
index.current_account_id = Some(account_id.to_string());
|
| 328 |
+
save_account_index(&index)
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
/// Update account quota
|
| 332 |
+
pub fn update_account_quota(account_id: &str, quota: QuotaData) -> Result<(), String> {
|
| 333 |
+
let mut account = load_account(account_id)?;
|
| 334 |
+
account.update_quota(quota);
|
| 335 |
+
save_account(&account)
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
/// Export all account refresh_tokens
|
| 339 |
+
#[allow(dead_code)]
|
| 340 |
+
pub fn export_accounts() -> Result<Vec<(String, String)>, String> {
|
| 341 |
+
let accounts = list_accounts()?;
|
| 342 |
+
let mut exports = Vec::new();
|
| 343 |
+
|
| 344 |
+
for account in accounts {
|
| 345 |
+
exports.push((account.email, account.token.refresh_token));
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
Ok(exports)
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
/// Fetch quota with retry mechanism
|
| 352 |
+
pub async fn fetch_quota_with_retry(account: &mut Account) -> crate::error::AppResult<QuotaData> {
|
| 353 |
+
use crate::modules::oauth;
|
| 354 |
+
use crate::error::AppError;
|
| 355 |
+
use reqwest::StatusCode;
|
| 356 |
+
|
| 357 |
+
// 1. Time-based check - ensure token is valid
|
| 358 |
+
let token = oauth::ensure_fresh_token(&account.token).await.map_err(AppError::OAuth)?;
|
| 359 |
+
|
| 360 |
+
if token.access_token != account.token.access_token {
|
| 361 |
+
modules::logger::log_info(&format!("Token refreshed for: {}", account.email));
|
| 362 |
+
account.token = token.clone();
|
| 363 |
+
|
| 364 |
+
// Re-fetch user name if missing
|
| 365 |
+
let name = if account.name.is_none() || account.name.as_ref().map_or(false, |n| n.trim().is_empty()) {
|
| 366 |
+
match oauth::get_user_info(&token.access_token).await {
|
| 367 |
+
Ok(user_info) => user_info.get_display_name(),
|
| 368 |
+
Err(_) => None
|
| 369 |
+
}
|
| 370 |
+
} else {
|
| 371 |
+
account.name.clone()
|
| 372 |
+
};
|
| 373 |
+
|
| 374 |
+
account.name = name.clone();
|
| 375 |
+
upsert_account(account.email.clone(), name, token.clone()).map_err(AppError::Account)?;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
// 0. Fill user name if missing
|
| 379 |
+
if account.name.is_none() || account.name.as_ref().map_or(false, |n| n.trim().is_empty()) {
|
| 380 |
+
modules::logger::log_info(&format!("Account {} missing name, fetching...", account.email));
|
| 381 |
+
match oauth::get_user_info(&account.token.access_token).await {
|
| 382 |
+
Ok(user_info) => {
|
| 383 |
+
let display_name = user_info.get_display_name();
|
| 384 |
+
modules::logger::log_info(&format!("Got user name: {:?}", display_name));
|
| 385 |
+
account.name = display_name.clone();
|
| 386 |
+
if let Err(e) = upsert_account(account.email.clone(), display_name, account.token.clone()) {
|
| 387 |
+
modules::logger::log_warn(&format!("Failed to save user name: {}", e));
|
| 388 |
+
}
|
| 389 |
+
},
|
| 390 |
+
Err(e) => {
|
| 391 |
+
modules::logger::log_warn(&format!("Failed to get user name: {}", e));
|
| 392 |
+
}
|
| 393 |
+
}
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
// 2. Try to query quota
|
| 397 |
+
let result: crate::error::AppResult<(QuotaData, Option<String>)> = modules::fetch_quota(&account.token.access_token, &account.email).await;
|
| 398 |
+
|
| 399 |
+
// Capture possible project_id update and save
|
| 400 |
+
if let Ok((ref _q, ref project_id)) = result {
|
| 401 |
+
if project_id.is_some() && *project_id != account.token.project_id {
|
| 402 |
+
modules::logger::log_info(&format!("Detected project_id update ({}), saving...", account.email));
|
| 403 |
+
account.token.project_id = project_id.clone();
|
| 404 |
+
if let Err(e) = upsert_account(account.email.clone(), account.name.clone(), account.token.clone()) {
|
| 405 |
+
modules::logger::log_warn(&format!("Failed to save project_id: {}", e));
|
| 406 |
+
}
|
| 407 |
+
}
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// 3. Handle 401 error
|
| 411 |
+
if let Err(AppError::Network(ref e)) = result {
|
| 412 |
+
if let Some(status) = e.status() {
|
| 413 |
+
if status == StatusCode::UNAUTHORIZED {
|
| 414 |
+
modules::logger::log_warn(&format!("401 Unauthorized for {}, forcing refresh...", account.email));
|
| 415 |
+
|
| 416 |
+
// Force refresh
|
| 417 |
+
let token_res = oauth::refresh_access_token(&account.token.refresh_token)
|
| 418 |
+
.await
|
| 419 |
+
.map_err(AppError::OAuth)?;
|
| 420 |
+
|
| 421 |
+
let new_token = TokenData::new(
|
| 422 |
+
token_res.access_token.clone(),
|
| 423 |
+
account.token.refresh_token.clone(),
|
| 424 |
+
token_res.expires_in,
|
| 425 |
+
account.token.email.clone(),
|
| 426 |
+
account.token.project_id.clone(),
|
| 427 |
+
None,
|
| 428 |
+
);
|
| 429 |
+
|
| 430 |
+
// Re-fetch user name
|
| 431 |
+
let name = if account.name.is_none() || account.name.as_ref().map_or(false, |n| n.trim().is_empty()) {
|
| 432 |
+
match oauth::get_user_info(&token_res.access_token).await {
|
| 433 |
+
Ok(user_info) => user_info.get_display_name(),
|
| 434 |
+
Err(_) => None
|
| 435 |
+
}
|
| 436 |
+
} else {
|
| 437 |
+
account.name.clone()
|
| 438 |
+
};
|
| 439 |
+
|
| 440 |
+
account.token = new_token.clone();
|
| 441 |
+
account.name = name.clone();
|
| 442 |
+
upsert_account(account.email.clone(), name, new_token.clone()).map_err(AppError::Account)?;
|
| 443 |
+
|
| 444 |
+
// Retry query
|
| 445 |
+
let retry_result: crate::error::AppResult<(QuotaData, Option<String>)> = modules::fetch_quota(&new_token.access_token, &account.email).await;
|
| 446 |
+
|
| 447 |
+
// Also handle retry project_id save
|
| 448 |
+
if let Ok((ref _q, ref project_id)) = retry_result {
|
| 449 |
+
if project_id.is_some() && *project_id != account.token.project_id {
|
| 450 |
+
modules::logger::log_info(&format!("Detected retry project_id update ({}), saving...", account.email));
|
| 451 |
+
account.token.project_id = project_id.clone();
|
| 452 |
+
let _ = upsert_account(account.email.clone(), account.name.clone(), account.token.clone());
|
| 453 |
+
}
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
if let Err(AppError::Network(ref e)) = retry_result {
|
| 457 |
+
if let Some(s) = e.status() {
|
| 458 |
+
if s == StatusCode::FORBIDDEN {
|
| 459 |
+
let mut q = QuotaData::new();
|
| 460 |
+
q.is_forbidden = true;
|
| 461 |
+
return Ok(q);
|
| 462 |
+
}
|
| 463 |
+
}
|
| 464 |
+
}
|
| 465 |
+
return retry_result.map(|(q, _)| q);
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
result.map(|(q, _)| q)
|
| 471 |
+
}
|
server/src/modules/config.rs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use std::fs;
|
| 2 |
+
|
| 3 |
+
use crate::models::AppConfig;
|
| 4 |
+
use super::account::get_data_dir;
|
| 5 |
+
|
| 6 |
+
const CONFIG_FILE: &str = "config.json";
|
| 7 |
+
|
| 8 |
+
/// Load application configuration
|
| 9 |
+
pub fn load_app_config() -> Result<AppConfig, String> {
|
| 10 |
+
let data_dir = get_data_dir()?;
|
| 11 |
+
let config_path = data_dir.join(CONFIG_FILE);
|
| 12 |
+
|
| 13 |
+
if !config_path.exists() {
|
| 14 |
+
return Ok(AppConfig::new());
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
let content = fs::read_to_string(&config_path)
|
| 18 |
+
.map_err(|e| format!("Failed to read config file: {}", e))?;
|
| 19 |
+
|
| 20 |
+
serde_json::from_str(&content)
|
| 21 |
+
.map_err(|e| format!("Failed to parse config file: {}", e))
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/// Save application configuration
|
| 25 |
+
pub fn save_app_config(config: &AppConfig) -> Result<(), String> {
|
| 26 |
+
let data_dir = get_data_dir()?;
|
| 27 |
+
let config_path = data_dir.join(CONFIG_FILE);
|
| 28 |
+
|
| 29 |
+
let content = serde_json::to_string_pretty(config)
|
| 30 |
+
.map_err(|e| format!("Failed to serialize config: {}", e))?;
|
| 31 |
+
|
| 32 |
+
fs::write(&config_path, content)
|
| 33 |
+
.map_err(|e| format!("Failed to save config: {}", e))
|
| 34 |
+
}
|
server/src/modules/logger.rs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use tracing::{info, warn, error};
|
| 2 |
+
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
| 3 |
+
|
| 4 |
+
// Custom local timezone time formatter
|
| 5 |
+
struct LocalTimer;
|
| 6 |
+
|
| 7 |
+
impl tracing_subscriber::fmt::time::FormatTime for LocalTimer {
|
| 8 |
+
fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> std::fmt::Result {
|
| 9 |
+
let now = chrono::Local::now();
|
| 10 |
+
write!(w, "{}", now.to_rfc3339())
|
| 11 |
+
}
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
/// Initialize logging system (stdout only for cloud deployment)
|
| 15 |
+
pub fn init_logger() {
|
| 16 |
+
// Capture log macro logs
|
| 17 |
+
let _ = tracing_log::LogTracer::init();
|
| 18 |
+
|
| 19 |
+
// Console output layer with local timezone
|
| 20 |
+
let console_layer = fmt::Layer::new()
|
| 21 |
+
.with_target(false)
|
| 22 |
+
.with_thread_ids(false)
|
| 23 |
+
.with_level(true)
|
| 24 |
+
.with_timer(LocalTimer);
|
| 25 |
+
|
| 26 |
+
// Filter layer (default INFO and above)
|
| 27 |
+
let filter_layer = EnvFilter::try_from_default_env()
|
| 28 |
+
.unwrap_or_else(|_| EnvFilter::new("info"));
|
| 29 |
+
|
| 30 |
+
// Initialize global subscriber
|
| 31 |
+
let _ = tracing_subscriber::registry()
|
| 32 |
+
.with(filter_layer)
|
| 33 |
+
.with(console_layer)
|
| 34 |
+
.try_init();
|
| 35 |
+
|
| 36 |
+
info!("Logger initialized (stdout only for cloud deployment)");
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/// Log info message (backward compatible interface)
|
| 40 |
+
pub fn log_info(message: &str) {
|
| 41 |
+
info!("{}", message);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/// Log warning message (backward compatible interface)
|
| 45 |
+
pub fn log_warn(message: &str) {
|
| 46 |
+
warn!("{}", message);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/// Log error message (backward compatible interface)
|
| 50 |
+
pub fn log_error(message: &str) {
|
| 51 |
+
error!("{}", message);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/// Clear logs (no-op for cloud deployment)
|
| 55 |
+
pub fn clear_logs() -> Result<(), String> {
|
| 56 |
+
Ok(())
|
| 57 |
+
}
|
server/src/modules/mod.rs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
pub mod account;
|
| 2 |
+
pub mod quota;
|
| 3 |
+
pub mod config;
|
| 4 |
+
pub mod logger;
|
| 5 |
+
pub mod oauth;
|
| 6 |
+
|
| 7 |
+
use crate::models;
|
| 8 |
+
|
| 9 |
+
// Re-export common functions to modules namespace
|
| 10 |
+
pub use account::*;
|
| 11 |
+
pub use quota::*;
|
| 12 |
+
pub use config::*;
|
| 13 |
+
pub use logger::*;
|
| 14 |
+
|
| 15 |
+
pub async fn fetch_quota(access_token: &str, email: &str) -> crate::error::AppResult<(models::QuotaData, Option<String>)> {
|
| 16 |
+
quota::fetch_quota(access_token, email).await
|
| 17 |
+
}
|
server/src/modules/oauth.rs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
|
| 3 |
+
// Google OAuth configuration
|
| 4 |
+
const CLIENT_ID: &str = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
|
| 5 |
+
const CLIENT_SECRET: &str = "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";
|
| 6 |
+
const TOKEN_URL: &str = "https://oauth2.googleapis.com/token";
|
| 7 |
+
const USERINFO_URL: &str = "https://www.googleapis.com/oauth2/v2/userinfo";
|
| 8 |
+
|
| 9 |
+
#[derive(Debug, Serialize, Deserialize)]
|
| 10 |
+
pub struct TokenResponse {
|
| 11 |
+
pub access_token: String,
|
| 12 |
+
pub expires_in: i64,
|
| 13 |
+
#[serde(default)]
|
| 14 |
+
pub token_type: String,
|
| 15 |
+
#[serde(default)]
|
| 16 |
+
pub refresh_token: Option<String>,
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
#[derive(Debug, Serialize, Deserialize)]
|
| 20 |
+
pub struct UserInfo {
|
| 21 |
+
pub email: String,
|
| 22 |
+
pub name: Option<String>,
|
| 23 |
+
pub given_name: Option<String>,
|
| 24 |
+
pub family_name: Option<String>,
|
| 25 |
+
pub picture: Option<String>,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
impl UserInfo {
|
| 29 |
+
/// Get best display name
|
| 30 |
+
pub fn get_display_name(&self) -> Option<String> {
|
| 31 |
+
// Prefer name
|
| 32 |
+
if let Some(name) = &self.name {
|
| 33 |
+
if !name.trim().is_empty() {
|
| 34 |
+
return Some(name.clone());
|
| 35 |
+
}
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// If name is empty, try combining given_name and family_name
|
| 39 |
+
match (&self.given_name, &self.family_name) {
|
| 40 |
+
(Some(given), Some(family)) => Some(format!("{} {}", given, family)),
|
| 41 |
+
(Some(given), None) => Some(given.clone()),
|
| 42 |
+
(None, Some(family)) => Some(family.clone()),
|
| 43 |
+
(None, None) => None,
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/// Refresh access_token using refresh_token
|
| 49 |
+
pub async fn refresh_access_token(refresh_token: &str) -> Result<TokenResponse, String> {
|
| 50 |
+
let client = crate::utils::http::create_client(15);
|
| 51 |
+
|
| 52 |
+
let params = [
|
| 53 |
+
("client_id", CLIENT_ID),
|
| 54 |
+
("client_secret", CLIENT_SECRET),
|
| 55 |
+
("refresh_token", refresh_token),
|
| 56 |
+
("grant_type", "refresh_token"),
|
| 57 |
+
];
|
| 58 |
+
|
| 59 |
+
crate::modules::logger::log_info("Refreshing token...");
|
| 60 |
+
|
| 61 |
+
let response = client
|
| 62 |
+
.post(TOKEN_URL)
|
| 63 |
+
.form(¶ms)
|
| 64 |
+
.send()
|
| 65 |
+
.await
|
| 66 |
+
.map_err(|e| format!("Refresh request failed: {}", e))?;
|
| 67 |
+
|
| 68 |
+
if response.status().is_success() {
|
| 69 |
+
let token_data = response
|
| 70 |
+
.json::<TokenResponse>()
|
| 71 |
+
.await
|
| 72 |
+
.map_err(|e| format!("Failed to parse refresh data: {}", e))?;
|
| 73 |
+
|
| 74 |
+
crate::modules::logger::log_info(&format!("Token refreshed! Valid for: {} seconds", token_data.expires_in));
|
| 75 |
+
Ok(token_data)
|
| 76 |
+
} else {
|
| 77 |
+
let error_text = response.text().await.unwrap_or_default();
|
| 78 |
+
Err(format!("Refresh failed: {}", error_text))
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/// Get user info
|
| 83 |
+
pub async fn get_user_info(access_token: &str) -> Result<UserInfo, String> {
|
| 84 |
+
let client = crate::utils::http::create_client(15);
|
| 85 |
+
|
| 86 |
+
let response = client
|
| 87 |
+
.get(USERINFO_URL)
|
| 88 |
+
.bearer_auth(access_token)
|
| 89 |
+
.send()
|
| 90 |
+
.await
|
| 91 |
+
.map_err(|e| format!("User info request failed: {}", e))?;
|
| 92 |
+
|
| 93 |
+
if response.status().is_success() {
|
| 94 |
+
response.json::<UserInfo>()
|
| 95 |
+
.await
|
| 96 |
+
.map_err(|e| format!("Failed to parse user info: {}", e))
|
| 97 |
+
} else {
|
| 98 |
+
let error_text = response.text().await.unwrap_or_default();
|
| 99 |
+
Err(format!("Failed to get user info: {}", error_text))
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/// Check and refresh token if needed
|
| 104 |
+
/// Returns the latest access_token
|
| 105 |
+
pub async fn ensure_fresh_token(
|
| 106 |
+
current_token: &crate::models::TokenData,
|
| 107 |
+
) -> Result<crate::models::TokenData, String> {
|
| 108 |
+
let now = chrono::Local::now().timestamp();
|
| 109 |
+
|
| 110 |
+
// If no expiry time, or still has more than 5 minutes validity, return directly
|
| 111 |
+
if current_token.expiry_timestamp > now + 300 {
|
| 112 |
+
return Ok(current_token.clone());
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Need to refresh
|
| 116 |
+
crate::modules::logger::log_info("Token about to expire, refreshing...");
|
| 117 |
+
let response = refresh_access_token(¤t_token.refresh_token).await?;
|
| 118 |
+
|
| 119 |
+
// Construct new TokenData
|
| 120 |
+
Ok(crate::models::TokenData::new(
|
| 121 |
+
response.access_token,
|
| 122 |
+
current_token.refresh_token.clone(), // Refresh doesn't always return new refresh_token
|
| 123 |
+
response.expires_in,
|
| 124 |
+
current_token.email.clone(),
|
| 125 |
+
current_token.project_id.clone(), // Keep original project_id
|
| 126 |
+
None, // session_id will be generated in token_manager
|
| 127 |
+
))
|
| 128 |
+
}
|
server/src/modules/quota.rs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
use serde_json::json;
|
| 3 |
+
use crate::models::QuotaData;
|
| 4 |
+
|
| 5 |
+
const QUOTA_API_URL: &str = "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
|
| 6 |
+
const USER_AGENT: &str = "antigravity/1.11.3 Darwin/arm64";
|
| 7 |
+
const CLOUD_CODE_BASE_URL: &str = "https://cloudcode-pa.googleapis.com";
|
| 8 |
+
|
| 9 |
+
#[derive(Debug, Serialize, Deserialize)]
|
| 10 |
+
struct QuotaResponse {
|
| 11 |
+
models: std::collections::HashMap<String, ModelInfo>,
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
#[derive(Debug, Serialize, Deserialize)]
|
| 15 |
+
struct ModelInfo {
|
| 16 |
+
#[serde(rename = "quotaInfo")]
|
| 17 |
+
quota_info: Option<QuotaInfo>,
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
#[derive(Debug, Serialize, Deserialize)]
|
| 21 |
+
struct QuotaInfo {
|
| 22 |
+
#[serde(rename = "remainingFraction")]
|
| 23 |
+
remaining_fraction: Option<f64>,
|
| 24 |
+
#[serde(rename = "resetTime")]
|
| 25 |
+
reset_time: Option<String>,
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
#[derive(Debug, Deserialize)]
|
| 29 |
+
struct LoadProjectResponse {
|
| 30 |
+
#[serde(rename = "cloudaicompanionProject")]
|
| 31 |
+
project_id: Option<String>,
|
| 32 |
+
#[serde(rename = "currentTier")]
|
| 33 |
+
current_tier: Option<Tier>,
|
| 34 |
+
#[serde(rename = "paidTier")]
|
| 35 |
+
paid_tier: Option<Tier>,
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
#[derive(Debug, Deserialize)]
|
| 39 |
+
struct Tier {
|
| 40 |
+
id: Option<String>,
|
| 41 |
+
#[serde(rename = "quotaTier")]
|
| 42 |
+
#[allow(dead_code)]
|
| 43 |
+
quota_tier: Option<String>,
|
| 44 |
+
#[allow(dead_code)]
|
| 45 |
+
name: Option<String>,
|
| 46 |
+
#[allow(dead_code)]
|
| 47 |
+
slug: Option<String>,
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/// Create configured HTTP client
|
| 51 |
+
fn create_client() -> reqwest::Client {
|
| 52 |
+
crate::utils::http::create_client(15)
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/// Get project ID and subscription type
|
| 56 |
+
async fn fetch_project_id(access_token: &str, email: &str) -> (Option<String>, Option<String>) {
|
| 57 |
+
let client = create_client();
|
| 58 |
+
let meta = json!({"metadata": {"ideType": "ANTIGRAVITY"}});
|
| 59 |
+
|
| 60 |
+
let res = client
|
| 61 |
+
.post(format!("{}/v1internal:loadCodeAssist", CLOUD_CODE_BASE_URL))
|
| 62 |
+
.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", access_token))
|
| 63 |
+
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
| 64 |
+
.header(reqwest::header::USER_AGENT, "antigravity/windows/amd64")
|
| 65 |
+
.json(&meta)
|
| 66 |
+
.send()
|
| 67 |
+
.await;
|
| 68 |
+
|
| 69 |
+
match res {
|
| 70 |
+
Ok(res) => {
|
| 71 |
+
if res.status().is_success() {
|
| 72 |
+
if let Ok(data) = res.json::<LoadProjectResponse>().await {
|
| 73 |
+
let project_id = data.project_id.clone();
|
| 74 |
+
|
| 75 |
+
// Core logic: prefer paid_tier ID, reflects true account rights better than current_tier
|
| 76 |
+
let subscription_tier = data.paid_tier
|
| 77 |
+
.and_then(|t| t.id)
|
| 78 |
+
.or_else(|| data.current_tier.and_then(|t| t.id));
|
| 79 |
+
|
| 80 |
+
if let Some(ref tier) = subscription_tier {
|
| 81 |
+
crate::modules::logger::log_info(&format!(
|
| 82 |
+
"[{}] Subscription identified: {}", email, tier
|
| 83 |
+
));
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
return (project_id, subscription_tier);
|
| 87 |
+
}
|
| 88 |
+
} else {
|
| 89 |
+
crate::modules::logger::log_warn(&format!(
|
| 90 |
+
"[{}] loadCodeAssist failed: Status: {}", email, res.status()
|
| 91 |
+
));
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
Err(e) => {
|
| 95 |
+
crate::modules::logger::log_error(&format!("[{}] loadCodeAssist network error: {}", email, e));
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
(None, None)
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/// Unified entry point for querying account quota
|
| 103 |
+
pub async fn fetch_quota(access_token: &str, email: &str) -> crate::error::AppResult<(QuotaData, Option<String>)> {
|
| 104 |
+
fetch_quota_inner(access_token, email).await
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/// Quota query logic
|
| 108 |
+
pub async fn fetch_quota_inner(access_token: &str, email: &str) -> crate::error::AppResult<(QuotaData, Option<String>)> {
|
| 109 |
+
use crate::error::AppError;
|
| 110 |
+
|
| 111 |
+
// 1. Get project ID and subscription type
|
| 112 |
+
let (project_id, subscription_tier) = fetch_project_id(access_token, email).await;
|
| 113 |
+
|
| 114 |
+
let final_project_id = project_id.as_deref().unwrap_or("bamboo-precept-lgxtn");
|
| 115 |
+
|
| 116 |
+
let client = create_client();
|
| 117 |
+
let payload = json!({
|
| 118 |
+
"project": final_project_id
|
| 119 |
+
});
|
| 120 |
+
|
| 121 |
+
let url = QUOTA_API_URL;
|
| 122 |
+
let max_retries = 3;
|
| 123 |
+
let mut last_error: Option<AppError> = None;
|
| 124 |
+
|
| 125 |
+
for attempt in 1..=max_retries {
|
| 126 |
+
match client
|
| 127 |
+
.post(url)
|
| 128 |
+
.bearer_auth(access_token)
|
| 129 |
+
.header("User-Agent", USER_AGENT)
|
| 130 |
+
.json(&json!(payload))
|
| 131 |
+
.send()
|
| 132 |
+
.await
|
| 133 |
+
{
|
| 134 |
+
Ok(response) => {
|
| 135 |
+
// Convert HTTP error status to AppError
|
| 136 |
+
if response.error_for_status_ref().is_err() {
|
| 137 |
+
let status = response.status();
|
| 138 |
+
|
| 139 |
+
// Special handling for 403 Forbidden - return directly, no retry
|
| 140 |
+
if status == reqwest::StatusCode::FORBIDDEN {
|
| 141 |
+
crate::modules::logger::log_warn("Account forbidden (403), marking as forbidden status");
|
| 142 |
+
let mut q = QuotaData::new();
|
| 143 |
+
q.is_forbidden = true;
|
| 144 |
+
q.subscription_tier = subscription_tier.clone();
|
| 145 |
+
return Ok((q, project_id.clone()));
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
// Other errors continue retry logic
|
| 149 |
+
if attempt < max_retries {
|
| 150 |
+
let text = response.text().await.unwrap_or_default();
|
| 151 |
+
crate::modules::logger::log_warn(&format!("API error: {} - {} (attempt {}/{})", status, text, attempt, max_retries));
|
| 152 |
+
last_error = Some(AppError::Unknown(format!("HTTP {} - {}", status, text)));
|
| 153 |
+
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
| 154 |
+
continue;
|
| 155 |
+
} else {
|
| 156 |
+
let text = response.text().await.unwrap_or_default();
|
| 157 |
+
return Err(AppError::Unknown(format!("API error: {} - {}", status, text)));
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
let quota_response: QuotaResponse = response
|
| 162 |
+
.json()
|
| 163 |
+
.await
|
| 164 |
+
.map_err(AppError::Network)?;
|
| 165 |
+
|
| 166 |
+
let mut quota_data = QuotaData::new();
|
| 167 |
+
|
| 168 |
+
tracing::debug!("Quota API returned {} models", quota_response.models.len());
|
| 169 |
+
|
| 170 |
+
for (name, info) in quota_response.models {
|
| 171 |
+
if let Some(quota_info) = info.quota_info {
|
| 172 |
+
let percentage = quota_info.remaining_fraction
|
| 173 |
+
.map(|f| (f * 100.0) as i32)
|
| 174 |
+
.unwrap_or(0);
|
| 175 |
+
|
| 176 |
+
let reset_time = quota_info.reset_time.unwrap_or_default();
|
| 177 |
+
|
| 178 |
+
// Only save models we care about
|
| 179 |
+
if name.contains("gemini") || name.contains("claude") {
|
| 180 |
+
quota_data.add_model(name, percentage, reset_time);
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// Set subscription type
|
| 186 |
+
quota_data.subscription_tier = subscription_tier.clone();
|
| 187 |
+
|
| 188 |
+
return Ok((quota_data, project_id.clone()));
|
| 189 |
+
},
|
| 190 |
+
Err(e) => {
|
| 191 |
+
crate::modules::logger::log_warn(&format!("Request failed: {} (attempt {}/{})", e, attempt, max_retries));
|
| 192 |
+
last_error = Some(AppError::Network(e));
|
| 193 |
+
if attempt < max_retries {
|
| 194 |
+
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
Err(last_error.unwrap_or_else(|| AppError::Unknown("Quota query failed".to_string())))
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
/// Batch query all account quotas (backup function)
|
| 204 |
+
#[allow(dead_code)]
|
| 205 |
+
pub async fn fetch_all_quotas(accounts: Vec<(String, String)>) -> Vec<(String, crate::error::AppResult<QuotaData>)> {
|
| 206 |
+
let mut results = Vec::new();
|
| 207 |
+
|
| 208 |
+
for (account_id, access_token) in accounts {
|
| 209 |
+
let result = fetch_quota(&access_token, &account_id).await.map(|(q, _)| q);
|
| 210 |
+
results.push((account_id, result));
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
results
|
| 214 |
+
}
|
server/src/proxy/common/error.rs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 错误处理
|
| 2 |
+
use thiserror::Error;
|
| 3 |
+
use axum::{http::StatusCode, Json, response::IntoResponse};
|
| 4 |
+
|
| 5 |
+
#[derive(Debug, Error)]
|
| 6 |
+
pub enum ProxyError {
|
| 7 |
+
#[error("Upstream API error: {0}")]
|
| 8 |
+
UpstreamError(String),
|
| 9 |
+
|
| 10 |
+
#[error("Transform error: {0}")]
|
| 11 |
+
TransformError(String),
|
| 12 |
+
|
| 13 |
+
#[error("Account error: {0}")]
|
| 14 |
+
AccountError(String),
|
| 15 |
+
|
| 16 |
+
#[error("Rate limit exceeded")]
|
| 17 |
+
RateLimitExceeded,
|
| 18 |
+
|
| 19 |
+
#[error("Invalid request: {0}")]
|
| 20 |
+
InvalidRequest(String),
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
impl IntoResponse for ProxyError {
|
| 24 |
+
fn into_response(self) -> axum::response::Response {
|
| 25 |
+
let status = match &self {
|
| 26 |
+
ProxyError::InvalidRequest(_) => StatusCode::BAD_REQUEST,
|
| 27 |
+
ProxyError::RateLimitExceeded => StatusCode::TOO_MANY_REQUESTS,
|
| 28 |
+
ProxyError::AccountError(_) => StatusCode::UNAUTHORIZED,
|
| 29 |
+
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
let body = serde_json::json!({
|
| 33 |
+
"error": {
|
| 34 |
+
"message": self.to_string(),
|
| 35 |
+
"type": format!("{:?}", self)
|
| 36 |
+
}
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
(status, Json(body)).into_response()
|
| 40 |
+
}
|
| 41 |
+
}
|
server/src/proxy/common/json_schema.rs
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use serde_json::Value;
|
| 2 |
+
|
| 3 |
+
/// 递归清理 JSON Schema 以符合 Gemini 接口要求
|
| 4 |
+
///
|
| 5 |
+
/// 1. [New] 展开 $ref 和 $defs: 将引用替换为实际定义,解决 Gemini 不支持 $ref 的问题
|
| 6 |
+
/// 2. 移除不支持的字段: $schema, additionalProperties, format, default, uniqueItems, validation fields
|
| 7 |
+
/// 3. 处理联合类型: ["string", "null"] -> "string"
|
| 8 |
+
/// 4. 将 type 字段的值转换为大写 (Gemini v1internal 要求)
|
| 9 |
+
/// 5. 移除数字校验字段: multipleOf, exclusiveMinimum, exclusiveMaximum 等
|
| 10 |
+
pub fn clean_json_schema(value: &mut Value) {
|
| 11 |
+
// 0. 预处理:展开 $ref (Schema Flattening)
|
| 12 |
+
if let Value::Object(map) = value {
|
| 13 |
+
let mut defs = serde_json::Map::new();
|
| 14 |
+
// 提取 $defs 或 definitions
|
| 15 |
+
if let Some(Value::Object(d)) = map.remove("$defs") {
|
| 16 |
+
defs.extend(d);
|
| 17 |
+
}
|
| 18 |
+
if let Some(Value::Object(d)) = map.remove("definitions") {
|
| 19 |
+
defs.extend(d);
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
if !defs.is_empty() {
|
| 23 |
+
// 递归替换引用
|
| 24 |
+
flatten_refs(map, &defs);
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// 递归清理
|
| 29 |
+
clean_json_schema_recursive(value);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
/// 递归展开 $ref
|
| 33 |
+
fn flatten_refs(map: &mut serde_json::Map<String, Value>, defs: &serde_json::Map<String, Value>) {
|
| 34 |
+
// 检查并替换 $ref
|
| 35 |
+
if let Some(Value::String(ref_path)) = map.remove("$ref") {
|
| 36 |
+
// 解析引用名 (例如 #/$defs/MyType -> MyType)
|
| 37 |
+
let ref_name = ref_path.split('/').last().unwrap_or(&ref_path);
|
| 38 |
+
|
| 39 |
+
if let Some(def_schema) = defs.get(ref_name) {
|
| 40 |
+
// 将定义的内容合并到当前 map
|
| 41 |
+
if let Value::Object(def_map) = def_schema {
|
| 42 |
+
for (k, v) in def_map {
|
| 43 |
+
// 仅当当前 map 没有该 key 时才插入 (避免覆盖)
|
| 44 |
+
// 但通常 $ref 节点不应该有其他属性
|
| 45 |
+
map.entry(k.clone()).or_insert_with(|| v.clone());
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// 递归处理刚刚合并进来的内容中可能包含的 $ref
|
| 49 |
+
// 注意:这里可能会无限递归如果存在循环引用,但工具定义通常是 DAG
|
| 50 |
+
flatten_refs(map, defs);
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// 遍历子节点
|
| 56 |
+
for (_, v) in map.iter_mut() {
|
| 57 |
+
if let Value::Object(child_map) = v {
|
| 58 |
+
flatten_refs(child_map, defs);
|
| 59 |
+
} else if let Value::Array(arr) = v {
|
| 60 |
+
for item in arr {
|
| 61 |
+
if let Value::Object(item_map) = item {
|
| 62 |
+
flatten_refs(item_map, defs);
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
fn clean_json_schema_recursive(value: &mut Value) {
|
| 70 |
+
match value {
|
| 71 |
+
Value::Object(map) => {
|
| 72 |
+
// 1. [CRITICAL] 深度递归处理:必须遍历当前对象的所有字段名对应的 Value
|
| 73 |
+
// 解决 properties/items 之外的 definitions、anyOf、allOf 等结构的清理
|
| 74 |
+
for v in map.values_mut() {
|
| 75 |
+
clean_json_schema_recursive(v);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// 2. 收集并处理校验字段 (Migration logic: 将约束降级为描述中的 Hint)
|
| 79 |
+
let mut constraints = Vec::new();
|
| 80 |
+
|
| 81 |
+
// 待迁移的约束黑名单
|
| 82 |
+
let validation_fields = [
|
| 83 |
+
("pattern", "pattern"),
|
| 84 |
+
("minLength", "minLen"), ("maxLength", "maxLen"),
|
| 85 |
+
("minimum", "min"), ("maximum", "max"),
|
| 86 |
+
("minItems", "minItems"), ("maxItems", "maxItems"),
|
| 87 |
+
("exclusiveMinimum", "exclMin"), ("exclusiveMaximum", "exclMax"),
|
| 88 |
+
("multipleOf", "multipleOf"),
|
| 89 |
+
("format", "format"),
|
| 90 |
+
];
|
| 91 |
+
|
| 92 |
+
for (field, label) in validation_fields {
|
| 93 |
+
if let Some(val) = map.remove(field) {
|
| 94 |
+
// 仅当值是简单类型时才迁移
|
| 95 |
+
if val.is_string() || val.is_number() || val.is_boolean() {
|
| 96 |
+
constraints.push(format!("{}: {}", label, val));
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// 3. 将约束信息追加到描述
|
| 102 |
+
if !constraints.is_empty() {
|
| 103 |
+
let suffix = format!(" [Constraint: {}]", constraints.join(", "));
|
| 104 |
+
let desc_val = map.entry("description".to_string()).or_insert_with(|| Value::String("".to_string()));
|
| 105 |
+
if let Value::String(s) = desc_val {
|
| 106 |
+
s.push_str(&suffix);
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// 4. 彻底物理移除干扰生成的“硬项”黑色名单 (Hard Blacklist)
|
| 111 |
+
let hard_remove_fields = [
|
| 112 |
+
"$schema",
|
| 113 |
+
"additionalProperties",
|
| 114 |
+
"enumCaseInsensitive",
|
| 115 |
+
"enumNormalizeWhitespace",
|
| 116 |
+
"uniqueItems",
|
| 117 |
+
"default",
|
| 118 |
+
"const",
|
| 119 |
+
"examples",
|
| 120 |
+
"propertyNames",
|
| 121 |
+
"anyOf", "oneOf", "allOf", "not",
|
| 122 |
+
"if", "then", "else",
|
| 123 |
+
"dependencies", "dependentSchemas", "dependentRequired",
|
| 124 |
+
"cache_control",
|
| 125 |
+
];
|
| 126 |
+
for field in hard_remove_fields {
|
| 127 |
+
map.remove(field);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
// 5. 处理 type 字段 (Gemini 要求单字符串且小写)
|
| 131 |
+
if let Some(type_val) = map.get_mut("type") {
|
| 132 |
+
match type_val {
|
| 133 |
+
Value::String(s) => {
|
| 134 |
+
*type_val = Value::String(s.to_lowercase());
|
| 135 |
+
}
|
| 136 |
+
Value::Array(arr) => {
|
| 137 |
+
let mut selected_type = "string".to_string();
|
| 138 |
+
for item in arr {
|
| 139 |
+
if let Value::String(s) = item {
|
| 140 |
+
if s != "null" {
|
| 141 |
+
selected_type = s.to_lowercase();
|
| 142 |
+
break;
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
*type_val = Value::String(selected_type);
|
| 147 |
+
}
|
| 148 |
+
_ => {}
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
Value::Array(arr) => {
|
| 153 |
+
for v in arr.iter_mut() {
|
| 154 |
+
clean_json_schema_recursive(v);
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
_ => {}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
#[cfg(test)]
|
| 162 |
+
mod tests {
|
| 163 |
+
use super::*;
|
| 164 |
+
use serde_json::json;
|
| 165 |
+
|
| 166 |
+
#[test]
|
| 167 |
+
fn test_clean_json_schema_draft_2020_12() {
|
| 168 |
+
let mut schema = json!({
|
| 169 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 170 |
+
"type": "object",
|
| 171 |
+
"properties": {
|
| 172 |
+
"location": {
|
| 173 |
+
"type": "string",
|
| 174 |
+
"minLength": 1,
|
| 175 |
+
"format": "city"
|
| 176 |
+
},
|
| 177 |
+
// 模拟属性名冲突:pattern 是一个 Object 属性,不应被移除
|
| 178 |
+
"pattern": {
|
| 179 |
+
"type": "object",
|
| 180 |
+
"properties": {
|
| 181 |
+
"regex": { "type": "string", "pattern": "^[a-z]+$" }
|
| 182 |
+
}
|
| 183 |
+
},
|
| 184 |
+
"unit": {
|
| 185 |
+
"type": ["string", "null"],
|
| 186 |
+
"default": "celsius"
|
| 187 |
+
}
|
| 188 |
+
},
|
| 189 |
+
"required": ["location"]
|
| 190 |
+
});
|
| 191 |
+
|
| 192 |
+
clean_json_schema(&mut schema);
|
| 193 |
+
|
| 194 |
+
// 1. 验证类型保持小写
|
| 195 |
+
assert_eq!(schema["type"], "object");
|
| 196 |
+
assert_eq!(schema["properties"]["location"]["type"], "string");
|
| 197 |
+
|
| 198 |
+
// 2. 验证标准字段被转换并移动到描述 (Advanced Soft-Remove)
|
| 199 |
+
assert!(schema["properties"]["location"].get("minLength").is_none());
|
| 200 |
+
assert!(schema["properties"]["location"]["description"].as_str().unwrap().contains("minLen: 1"));
|
| 201 |
+
|
| 202 |
+
// 3. 验证名为 "pattern" 的属性未被误删
|
| 203 |
+
assert!(schema["properties"].get("pattern").is_some());
|
| 204 |
+
assert_eq!(schema["properties"]["pattern"]["type"], "object");
|
| 205 |
+
|
| 206 |
+
// 4. 验证内部的 pattern 校验字段被正确移除并转为描述
|
| 207 |
+
assert!(schema["properties"]["pattern"]["properties"]["regex"].get("pattern").is_none());
|
| 208 |
+
assert!(schema["properties"]["pattern"]["properties"]["regex"]["description"].as_str().unwrap().contains("pattern: ^[a-z]+$"));
|
| 209 |
+
|
| 210 |
+
// 5. 验证联合类型被降级为单一类型 (Protobuf 兼容性)
|
| 211 |
+
assert_eq!(schema["properties"]["unit"]["type"], "string");
|
| 212 |
+
|
| 213 |
+
// 6. 验证元数据字段被移除
|
| 214 |
+
assert!(schema.get("$schema").is_none());
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
#[test]
|
| 218 |
+
fn test_type_fallback() {
|
| 219 |
+
// Test ["string", "null"] -> "string"
|
| 220 |
+
let mut s1 = json!({"type": ["string", "null"]});
|
| 221 |
+
clean_json_schema(&mut s1);
|
| 222 |
+
assert_eq!(s1["type"], "string");
|
| 223 |
+
|
| 224 |
+
// Test ["integer", "null"] -> "integer" (and lowercase check if needed, though usually integer)
|
| 225 |
+
let mut s2 = json!({"type": ["integer", "null"]});
|
| 226 |
+
clean_json_schema(&mut s2);
|
| 227 |
+
assert_eq!(s2["type"], "integer");
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
#[test]
|
| 231 |
+
fn test_flatten_refs() {
|
| 232 |
+
let mut schema = json!({
|
| 233 |
+
"$defs": {
|
| 234 |
+
"Address": {
|
| 235 |
+
"type": "object",
|
| 236 |
+
"properties": {
|
| 237 |
+
"city": { "type": "string" }
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
},
|
| 241 |
+
"properties": {
|
| 242 |
+
"home": { "$ref": "#/$defs/Address" }
|
| 243 |
+
}
|
| 244 |
+
});
|
| 245 |
+
|
| 246 |
+
clean_json_schema(&mut schema);
|
| 247 |
+
|
| 248 |
+
// 验证引用被展开且类型转为小写
|
| 249 |
+
assert_eq!(schema["properties"]["home"]["type"], "object");
|
| 250 |
+
assert_eq!(schema["properties"]["home"]["properties"]["city"]["type"], "string");
|
| 251 |
+
}
|
| 252 |
+
}
|
server/src/proxy/common/mod.rs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Common 模块 - 公共工具
|
| 2 |
+
|
| 3 |
+
// pub mod error;
|
| 4 |
+
// pub mod rate_limiter;
|
| 5 |
+
pub mod model_mapping;
|
| 6 |
+
pub mod utils;
|
| 7 |
+
pub mod json_schema;
|
server/src/proxy/common/model_mapping.rs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 模型名称映射
|
| 2 |
+
use std::collections::HashMap;
|
| 3 |
+
use once_cell::sync::Lazy;
|
| 4 |
+
|
| 5 |
+
static CLAUDE_TO_GEMINI: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
|
| 6 |
+
let mut m = HashMap::new();
|
| 7 |
+
|
| 8 |
+
// 直接支持的模型
|
| 9 |
+
m.insert("claude-opus-4-5-thinking", "claude-opus-4-5-thinking");
|
| 10 |
+
m.insert("claude-sonnet-4-5", "claude-sonnet-4-5");
|
| 11 |
+
m.insert("claude-sonnet-4-5-thinking", "claude-sonnet-4-5-thinking");
|
| 12 |
+
|
| 13 |
+
// 别名映射
|
| 14 |
+
m.insert("claude-sonnet-4-5-20250929", "claude-sonnet-4-5-thinking");
|
| 15 |
+
m.insert("claude-3-5-sonnet-20241022", "claude-sonnet-4-5");
|
| 16 |
+
m.insert("claude-3-5-sonnet-20240620", "claude-sonnet-4-5");
|
| 17 |
+
m.insert("claude-opus-4", "claude-opus-4-5-thinking");
|
| 18 |
+
m.insert("claude-opus-4-5-20251101", "claude-opus-4-5-thinking");
|
| 19 |
+
m.insert("claude-haiku-4", "claude-sonnet-4-5");
|
| 20 |
+
m.insert("claude-3-haiku-20240307", "claude-sonnet-4-5");
|
| 21 |
+
m.insert("claude-haiku-4-5-20251001", "claude-sonnet-4-5");
|
| 22 |
+
// OpenAI 协议映射表
|
| 23 |
+
m.insert("gpt-4", "gemini-2.5-pro");
|
| 24 |
+
m.insert("gpt-4-turbo", "gemini-2.5-pro");
|
| 25 |
+
m.insert("gpt-4-turbo-preview", "gemini-2.5-pro");
|
| 26 |
+
m.insert("gpt-4-0125-preview", "gemini-2.5-pro");
|
| 27 |
+
m.insert("gpt-4-1106-preview", "gemini-2.5-pro");
|
| 28 |
+
m.insert("gpt-4-0613", "gemini-2.5-pro");
|
| 29 |
+
|
| 30 |
+
m.insert("gpt-4o", "gemini-2.5-pro");
|
| 31 |
+
m.insert("gpt-4o-2024-05-13", "gemini-2.5-pro");
|
| 32 |
+
m.insert("gpt-4o-2024-08-06", "gemini-2.5-pro");
|
| 33 |
+
|
| 34 |
+
m.insert("gpt-4o-mini", "gemini-2.5-flash");
|
| 35 |
+
m.insert("gpt-4o-mini-2024-07-18", "gemini-2.5-flash");
|
| 36 |
+
|
| 37 |
+
m.insert("gpt-3.5-turbo", "gemini-2.5-flash");
|
| 38 |
+
m.insert("gpt-3.5-turbo-16k", "gemini-2.5-flash");
|
| 39 |
+
m.insert("gpt-3.5-turbo-0125", "gemini-2.5-flash");
|
| 40 |
+
m.insert("gpt-3.5-turbo-1106", "gemini-2.5-flash");
|
| 41 |
+
m.insert("gpt-3.5-turbo-0613", "gemini-2.5-flash");
|
| 42 |
+
|
| 43 |
+
// Gemini 协议映射表
|
| 44 |
+
m.insert("gemini-2.5-flash-lite", "gemini-2.5-flash-lite");
|
| 45 |
+
m.insert("gemini-2.5-flash-thinking", "gemini-2.5-flash-thinking");
|
| 46 |
+
m.insert("gemini-3-pro-low", "gemini-3-pro-low");
|
| 47 |
+
m.insert("gemini-3-pro-high", "gemini-3-pro-high");
|
| 48 |
+
m.insert("gemini-3-pro-preview", "gemini-3-pro-preview");
|
| 49 |
+
m.insert("gemini-2.5-flash", "gemini-2.5-flash");
|
| 50 |
+
m.insert("gemini-3-flash", "gemini-3-flash");
|
| 51 |
+
m.insert("gemini-3-pro-image", "gemini-3-pro-image");
|
| 52 |
+
|
| 53 |
+
m
|
| 54 |
+
});
|
| 55 |
+
|
| 56 |
+
pub fn map_claude_model_to_gemini(input: &str) -> String {
|
| 57 |
+
// 1. Check exact match in map
|
| 58 |
+
if let Some(mapped) = CLAUDE_TO_GEMINI.get(input) {
|
| 59 |
+
return mapped.to_string();
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// 2. Pass-through known prefixes (gemini-, -thinking) to support dynamic suffixes
|
| 63 |
+
if input.starts_with("gemini-") || input.contains("thinking") {
|
| 64 |
+
return input.to_string();
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// 3. Fallback to default
|
| 68 |
+
"claude-sonnet-4-5".to_string()
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/// 核心模型路由解析引擎
|
| 72 |
+
/// 优先级:Custom Mapping (精确) > Group Mapping (家族) > System Mapping (内置插件)
|
| 73 |
+
pub fn resolve_model_route(
|
| 74 |
+
original_model: &str,
|
| 75 |
+
custom_mapping: &std::collections::HashMap<String, String>,
|
| 76 |
+
openai_mapping: &std::collections::HashMap<String, String>,
|
| 77 |
+
anthropic_mapping: &std::collections::HashMap<String, String>,
|
| 78 |
+
) -> String {
|
| 79 |
+
// 1. 检查自定义精确映射 (优先级最高)
|
| 80 |
+
if let Some(target) = custom_mapping.get(original_model) {
|
| 81 |
+
crate::modules::logger::log_info(&format!("[Router] 使用自定义精确映射: {} -> {}", original_model, target));
|
| 82 |
+
return target.clone();
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
let lower_model = original_model.to_lowercase();
|
| 86 |
+
|
| 87 |
+
// 2. 检查家族分组映射 (OpenAI 系)
|
| 88 |
+
// GPT-4 系列 (含 GPT-4 经典, o1, o3 等, 排除 4o/mini/turbo)
|
| 89 |
+
if (lower_model.starts_with("gpt-4") && !lower_model.contains("o") && !lower_model.contains("mini") && !lower_model.contains("turbo")) ||
|
| 90 |
+
lower_model.starts_with("o1-") || lower_model.starts_with("o3-") || lower_model == "gpt-4" {
|
| 91 |
+
if let Some(target) = openai_mapping.get("gpt-4-series") {
|
| 92 |
+
crate::modules::logger::log_info(&format!("[Router] 使用 GPT-4 系列映射: {} -> {}", original_model, target));
|
| 93 |
+
return target.clone();
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
// GPT-4o / 3.5 系列 (均衡与轻量, 含 4o, mini, turbo)
|
| 98 |
+
if lower_model.contains("4o") || lower_model.starts_with("gpt-3.5") || (lower_model.contains("mini") && !lower_model.contains("gemini")) || lower_model.contains("turbo") {
|
| 99 |
+
if let Some(target) = openai_mapping.get("gpt-4o-series") {
|
| 100 |
+
crate::modules::logger::log_info(&format!("[Router] 使用 GPT-4o/3.5 系列映射: {} -> {}", original_model, target));
|
| 101 |
+
return target.clone();
|
| 102 |
+
}
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// GPT-5 系列 (gpt-5, gpt-5.1, gpt-5.2 等)
|
| 106 |
+
if lower_model.starts_with("gpt-5") {
|
| 107 |
+
// 优先使用 gpt-5-series 映射,如果没有则使用 gpt-4-series
|
| 108 |
+
if let Some(target) = openai_mapping.get("gpt-5-series") {
|
| 109 |
+
crate::modules::logger::log_info(&format!("[Router] 使用 GPT-5 系列映射: {} -> {}", original_model, target));
|
| 110 |
+
return target.clone();
|
| 111 |
+
}
|
| 112 |
+
if let Some(target) = openai_mapping.get("gpt-4-series") {
|
| 113 |
+
crate::modules::logger::log_info(&format!("[Router] 使用 GPT-4 系列映射 (GPT-5 fallback): {} -> {}", original_model, target));
|
| 114 |
+
return target.clone();
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// 3. 检查家族分组映射 (Anthropic 系)
|
| 119 |
+
if lower_model.starts_with("claude-") {
|
| 120 |
+
let family_key = if lower_model.contains("4-5") || lower_model.contains("4.5") {
|
| 121 |
+
"claude-4.5-series"
|
| 122 |
+
} else if lower_model.contains("3-5") || lower_model.contains("3.5") {
|
| 123 |
+
"claude-3.5-series"
|
| 124 |
+
} else {
|
| 125 |
+
"claude-default"
|
| 126 |
+
};
|
| 127 |
+
|
| 128 |
+
if let Some(target) = anthropic_mapping.get(family_key) {
|
| 129 |
+
crate::modules::logger::log_warn(&format!("[Router] 使用 Anthropic 系列映射: {} -> {}", original_model, target));
|
| 130 |
+
return target.clone();
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// 兜底兼容旧版精确映射
|
| 134 |
+
if let Some(target) = anthropic_mapping.get(original_model) {
|
| 135 |
+
return target.clone();
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// 4. 下沉到系统默认映射逻辑
|
| 140 |
+
map_claude_model_to_gemini(original_model)
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
#[cfg(test)]
|
| 144 |
+
mod tests {
|
| 145 |
+
use super::*;
|
| 146 |
+
|
| 147 |
+
#[test]
|
| 148 |
+
fn test_model_mapping() {
|
| 149 |
+
assert_eq!(
|
| 150 |
+
map_claude_model_to_gemini("claude-3-5-sonnet-20241022"),
|
| 151 |
+
"claude-sonnet-4-5"
|
| 152 |
+
);
|
| 153 |
+
assert_eq!(
|
| 154 |
+
map_claude_model_to_gemini("claude-opus-4"),
|
| 155 |
+
"claude-opus-4-5-thinking"
|
| 156 |
+
);
|
| 157 |
+
// Test gemini pass-through (should not be caught by "mini" rule)
|
| 158 |
+
assert_eq!(
|
| 159 |
+
map_claude_model_to_gemini("gemini-2.5-flash-mini-test"),
|
| 160 |
+
"gemini-2.5-flash-mini-test"
|
| 161 |
+
);
|
| 162 |
+
assert_eq!(
|
| 163 |
+
map_claude_model_to_gemini("unknown-model"),
|
| 164 |
+
"claude-sonnet-4-5"
|
| 165 |
+
);
|
| 166 |
+
}
|
| 167 |
+
}
|
server/src/proxy/common/rate_limiter.rs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Rate Limiter
|
| 2 |
+
// 确保 API 调用间隔 ≥ 500ms
|
| 3 |
+
|
| 4 |
+
use std::sync::Arc;
|
| 5 |
+
use tokio::sync::Mutex;
|
| 6 |
+
use tokio::time::{sleep, Duration, Instant};
|
| 7 |
+
|
| 8 |
+
pub struct RateLimiter {
|
| 9 |
+
min_interval: Duration,
|
| 10 |
+
last_call: Arc<Mutex<Option<Instant>>>,
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
impl RateLimiter {
|
| 14 |
+
pub fn new(min_interval_ms: u64) -> Self {
|
| 15 |
+
Self {
|
| 16 |
+
min_interval: Duration::from_millis(min_interval_ms),
|
| 17 |
+
last_call: Arc::new(Mutex::new(None)),
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
pub async fn wait(&self) {
|
| 22 |
+
let mut last = self.last_call.lock().await;
|
| 23 |
+
if let Some(last_time) = *last {
|
| 24 |
+
let elapsed = last_time.elapsed();
|
| 25 |
+
if elapsed < self.min_interval {
|
| 26 |
+
sleep(self.min_interval - elapsed).await;
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
*last = Some(Instant::now());
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
#[cfg(test)]
|
| 34 |
+
mod tests {
|
| 35 |
+
use super::*;
|
| 36 |
+
use tokio::time::Instant;
|
| 37 |
+
|
| 38 |
+
#[tokio::test]
|
| 39 |
+
async fn test_rate_limiter() {
|
| 40 |
+
let limiter = RateLimiter::new(500);
|
| 41 |
+
let start = Instant::now();
|
| 42 |
+
|
| 43 |
+
limiter.wait().await; // 第一次调用,立即返回
|
| 44 |
+
let elapsed1 = start.elapsed().as_millis();
|
| 45 |
+
assert!(elapsed1 < 50);
|
| 46 |
+
|
| 47 |
+
limiter.wait().await; // 第二次调用,等待 500ms
|
| 48 |
+
let elapsed2 = start.elapsed().as_millis();
|
| 49 |
+
assert!(elapsed2 >= 500 && elapsed2 < 600);
|
| 50 |
+
}
|
| 51 |
+
}
|
server/src/proxy/common/utils.rs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// 工具函数
|
| 2 |
+
|
| 3 |
+
pub fn generate_random_id() -> String {
|
| 4 |
+
use rand::Rng;
|
| 5 |
+
rand::thread_rng()
|
| 6 |
+
.sample_iter(&rand::distributions::Alphanumeric)
|
| 7 |
+
.take(8)
|
| 8 |
+
.map(char::from)
|
| 9 |
+
.collect()
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
/// 根据模型名称推测功能类型
|
| 13 |
+
// 注意:此函数已弃用,请改用 mappers::common_utils::resolve_request_config
|
| 14 |
+
pub fn _deprecated_infer_quota_group(model: &str) -> String {
|
| 15 |
+
if model.to_lowercase().starts_with("claude") {
|
| 16 |
+
"claude".to_string()
|
| 17 |
+
} else {
|
| 18 |
+
"gemini".to_string()
|
| 19 |
+
}
|
| 20 |
+
}
|
server/src/proxy/config.rs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use serde::{Deserialize, Serialize};
|
| 2 |
+
|
| 3 |
+
/// Proxy service configuration
|
| 4 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 5 |
+
pub struct ProxyConfig {
|
| 6 |
+
/// Whether proxy service is enabled
|
| 7 |
+
pub enabled: bool,
|
| 8 |
+
|
| 9 |
+
/// Whether to allow LAN access
|
| 10 |
+
/// - false: localhost only 127.0.0.1 (default, privacy first)
|
| 11 |
+
/// - true: allow LAN access 0.0.0.0
|
| 12 |
+
#[serde(default)]
|
| 13 |
+
pub allow_lan_access: bool,
|
| 14 |
+
|
| 15 |
+
/// Listen port
|
| 16 |
+
pub port: u16,
|
| 17 |
+
|
| 18 |
+
/// API key
|
| 19 |
+
pub api_key: String,
|
| 20 |
+
|
| 21 |
+
/// Whether to auto start
|
| 22 |
+
pub auto_start: bool,
|
| 23 |
+
|
| 24 |
+
/// Anthropic model mapping (key: Claude model name, value: Gemini model name)
|
| 25 |
+
#[serde(default)]
|
| 26 |
+
pub anthropic_mapping: std::collections::HashMap<String, String>,
|
| 27 |
+
|
| 28 |
+
/// OpenAI model mapping (key: OpenAI model group, value: Gemini model name)
|
| 29 |
+
#[serde(default)]
|
| 30 |
+
pub openai_mapping: std::collections::HashMap<String, String>,
|
| 31 |
+
|
| 32 |
+
/// Custom exact model mapping (key: original model name, value: target model name)
|
| 33 |
+
#[serde(default)]
|
| 34 |
+
pub custom_mapping: std::collections::HashMap<String, String>,
|
| 35 |
+
|
| 36 |
+
/// API request timeout in seconds
|
| 37 |
+
#[serde(default = "default_request_timeout")]
|
| 38 |
+
pub request_timeout: u64,
|
| 39 |
+
|
| 40 |
+
/// Upstream proxy configuration
|
| 41 |
+
#[serde(default)]
|
| 42 |
+
pub upstream_proxy: UpstreamProxyConfig,
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/// Upstream proxy configuration
|
| 46 |
+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
| 47 |
+
pub struct UpstreamProxyConfig {
|
| 48 |
+
/// Whether enabled
|
| 49 |
+
pub enabled: bool,
|
| 50 |
+
/// Proxy URL (http://, https://, socks5://)
|
| 51 |
+
pub url: String,
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
impl Default for ProxyConfig {
|
| 55 |
+
fn default() -> Self {
|
| 56 |
+
Self {
|
| 57 |
+
enabled: false,
|
| 58 |
+
allow_lan_access: true, // Cloud deployment should allow external access
|
| 59 |
+
port: 7860, // HuggingFace Spaces default port
|
| 60 |
+
api_key: format!("sk-{}", uuid::Uuid::new_v4().simple()),
|
| 61 |
+
auto_start: true, // Auto start in cloud environment
|
| 62 |
+
anthropic_mapping: std::collections::HashMap::new(),
|
| 63 |
+
openai_mapping: std::collections::HashMap::new(),
|
| 64 |
+
custom_mapping: std::collections::HashMap::new(),
|
| 65 |
+
request_timeout: default_request_timeout(),
|
| 66 |
+
upstream_proxy: UpstreamProxyConfig::default(),
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
fn default_request_timeout() -> u64 {
|
| 72 |
+
120 // Default 120 seconds
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
impl ProxyConfig {
|
| 76 |
+
/// Get actual bind address
|
| 77 |
+
/// - allow_lan_access = false: return "127.0.0.1" (default, privacy first)
|
| 78 |
+
/// - allow_lan_access = true: return "0.0.0.0" (allow LAN access)
|
| 79 |
+
pub fn get_bind_address(&self) -> &str {
|
| 80 |
+
if self.allow_lan_access {
|
| 81 |
+
"0.0.0.0"
|
| 82 |
+
} else {
|
| 83 |
+
"127.0.0.1"
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
}
|
server/src/proxy/handlers/claude.rs
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Claude 协议处理器
|
| 2 |
+
|
| 3 |
+
use axum::{
|
| 4 |
+
body::Body,
|
| 5 |
+
extract::{Json, State},
|
| 6 |
+
http::{header, StatusCode},
|
| 7 |
+
response::{IntoResponse, Response},
|
| 8 |
+
};
|
| 9 |
+
use bytes::Bytes;
|
| 10 |
+
use futures::StreamExt;
|
| 11 |
+
use serde_json::{json, Value};
|
| 12 |
+
use tokio::time::{sleep, Duration};
|
| 13 |
+
use tracing::{debug, error};
|
| 14 |
+
|
| 15 |
+
use crate::proxy::mappers::claude::{
|
| 16 |
+
transform_claude_request_in, transform_response, create_claude_sse_stream, ClaudeRequest,
|
| 17 |
+
};
|
| 18 |
+
use crate::proxy::server::AppState;
|
| 19 |
+
|
| 20 |
+
const MAX_RETRY_ATTEMPTS: usize = 3;
|
| 21 |
+
|
| 22 |
+
/// 处理 Claude messages 请求
|
| 23 |
+
///
|
| 24 |
+
/// 处理 Chat 消息请求流程
|
| 25 |
+
pub async fn handle_messages(
|
| 26 |
+
State(state): State<AppState>,
|
| 27 |
+
Json(request): Json<ClaudeRequest>,
|
| 28 |
+
) -> Response {
|
| 29 |
+
// 生成随机 Trace ID 用户追踪
|
| 30 |
+
let trace_id: String = rand::Rng::sample_iter(rand::thread_rng(), &rand::distributions::Alphanumeric)
|
| 31 |
+
.take(6)
|
| 32 |
+
.map(char::from)
|
| 33 |
+
.collect::<String>().to_lowercase();
|
| 34 |
+
// 获取最新一条“有意义”的消息内容(用于日志记录和后台任务检测)
|
| 35 |
+
// 策略:反向遍历,首先筛选出所有角色为 "user" 的消息,然后从中找到第一条非 "Warmup" 且非空的文本消息
|
| 36 |
+
// 获取最新一条“有意义”的消息内容(用于日志记录和后台任务检测)
|
| 37 |
+
// 策略:反向遍历,首先筛选出所有和用户相关的消息 (role="user")
|
| 38 |
+
// 然后提取其文本内容,跳过 "Warmup" 或系统预设的 reminder
|
| 39 |
+
let meaningful_msg = request.messages.iter().rev()
|
| 40 |
+
.filter(|m| m.role == "user")
|
| 41 |
+
.find_map(|m| {
|
| 42 |
+
let content = match &m.content {
|
| 43 |
+
crate::proxy::mappers::claude::models::MessageContent::String(s) => s.to_string(),
|
| 44 |
+
crate::proxy::mappers::claude::models::MessageContent::Array(arr) => {
|
| 45 |
+
// 对于数组,提取所有 Text 块并拼接,忽略 ToolResult
|
| 46 |
+
arr.iter()
|
| 47 |
+
.filter_map(|block| match block {
|
| 48 |
+
crate::proxy::mappers::claude::models::ContentBlock::Text { text } => Some(text.as_str()),
|
| 49 |
+
_ => None,
|
| 50 |
+
})
|
| 51 |
+
.collect::<Vec<_>>()
|
| 52 |
+
.join(" ")
|
| 53 |
+
}
|
| 54 |
+
};
|
| 55 |
+
|
| 56 |
+
// 过滤规则:
|
| 57 |
+
// 1. 忽略空消息
|
| 58 |
+
// 2. 忽略 "Warmup" 消息
|
| 59 |
+
// 3. 忽略 <system-reminder> 标签的消息
|
| 60 |
+
if content.trim().is_empty()
|
| 61 |
+
|| content.starts_with("Warmup")
|
| 62 |
+
|| content.contains("<system-reminder>")
|
| 63 |
+
{
|
| 64 |
+
None
|
| 65 |
+
} else {
|
| 66 |
+
Some(content)
|
| 67 |
+
}
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
// 如果经过过滤还是找不到(例如纯工具调用),则回退到最后一条消息的原始展示
|
| 71 |
+
let latest_msg = meaningful_msg.unwrap_or_else(|| {
|
| 72 |
+
request.messages.last().map(|m| {
|
| 73 |
+
match &m.content {
|
| 74 |
+
crate::proxy::mappers::claude::models::MessageContent::String(s) => s.clone(),
|
| 75 |
+
crate::proxy::mappers::claude::models::MessageContent::Array(_) => "[Complex/Tool Message]".to_string()
|
| 76 |
+
}
|
| 77 |
+
}).unwrap_or_else(|| "[No Messages]".to_string())
|
| 78 |
+
});
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
crate::modules::logger::log_info(&format!("[{}] Received Claude request for model: {}, content_preview: {:.100}...", trace_id, request.model, latest_msg));
|
| 82 |
+
tracing::info!("[{}] Full Claude Request: {}", trace_id, serde_json::to_string_pretty(&request).unwrap_or_default());
|
| 83 |
+
|
| 84 |
+
// 1. 获取 会话 ID (已废弃基于内容的哈希,改用 TokenManager 内部的时间窗口锁定)
|
| 85 |
+
let session_id: Option<&str> = None;
|
| 86 |
+
|
| 87 |
+
// 2. 获取 UpstreamClient
|
| 88 |
+
let upstream = state.upstream.clone();
|
| 89 |
+
|
| 90 |
+
// 3. 准备闭包
|
| 91 |
+
let mut request_for_body = request.clone();
|
| 92 |
+
let token_manager = state.token_manager;
|
| 93 |
+
|
| 94 |
+
let pool_size = token_manager.len();
|
| 95 |
+
let max_attempts = MAX_RETRY_ATTEMPTS.min(pool_size).max(1);
|
| 96 |
+
|
| 97 |
+
let mut last_error = String::new();
|
| 98 |
+
let mut retried_without_thinking = false;
|
| 99 |
+
|
| 100 |
+
for attempt in 0..max_attempts {
|
| 101 |
+
// 3. 模型路由与配置解析 (提前解析以确定请求类型)
|
| 102 |
+
let mut mapped_model = crate::proxy::common::model_mapping::resolve_model_route(
|
| 103 |
+
&request_for_body.model,
|
| 104 |
+
&*state.custom_mapping.read().await,
|
| 105 |
+
&*state.openai_mapping.read().await,
|
| 106 |
+
&*state.anthropic_mapping.read().await,
|
| 107 |
+
);
|
| 108 |
+
// 将 Claude 工具转为 Value 数组以便探测联网
|
| 109 |
+
let tools_val: Option<Vec<Value>> = request_for_body.tools.as_ref().map(|list| {
|
| 110 |
+
list.iter().map(|t| serde_json::to_value(t).unwrap_or(json!({}))).collect()
|
| 111 |
+
});
|
| 112 |
+
|
| 113 |
+
let config = crate::proxy::mappers::common_utils::resolve_request_config(&request_for_body.model, &mapped_model, &tools_val);
|
| 114 |
+
|
| 115 |
+
// 4. 获取 Token (使用准确的 request_type)
|
| 116 |
+
// 关键:在重试尝试 (attempt > 0) 时,必须根据错误类型决定是否强制轮换账号
|
| 117 |
+
let force_rotate_token = attempt > 0;
|
| 118 |
+
|
| 119 |
+
let (access_token, project_id, email) = match token_manager.get_token(&config.request_type, force_rotate_token).await {
|
| 120 |
+
Ok(t) => t,
|
| 121 |
+
Err(e) => {
|
| 122 |
+
return (
|
| 123 |
+
StatusCode::SERVICE_UNAVAILABLE,
|
| 124 |
+
Json(json!({
|
| 125 |
+
"type": "error",
|
| 126 |
+
"error": {
|
| 127 |
+
"type": "overloaded_error",
|
| 128 |
+
"message": format!("No available accounts: {}", e)
|
| 129 |
+
}
|
| 130 |
+
}))
|
| 131 |
+
).into_response();
|
| 132 |
+
}
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
tracing::info!("Using account: {} for request (type: {})", email, config.request_type);
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
// --- 核心优化:智能识别与拦截后台自动请求 ---
|
| 139 |
+
// [DEBUG] 临时调试:打印原始消息以诊断提取失败
|
| 140 |
+
if let Some(last_msg) = request_for_body.messages.last() {
|
| 141 |
+
tracing::debug!("[{}] DEBUG - Last message role: {}, content type: {}",
|
| 142 |
+
trace_id,
|
| 143 |
+
last_msg.role,
|
| 144 |
+
match &last_msg.content {
|
| 145 |
+
crate::proxy::mappers::claude::models::MessageContent::String(_) => "String",
|
| 146 |
+
crate::proxy::mappers::claude::models::MessageContent::Array(_) => "Array",
|
| 147 |
+
}
|
| 148 |
+
);
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// [FIX] 只扫描真正的"最后一条"用户消息,且必须过滤掉系统消息
|
| 152 |
+
// 关键:复用 meaningful_msg 的过滤逻辑,确保 Warmup/system-reminder 不会被当作用户请求
|
| 153 |
+
let last_user_msg = request_for_body.messages.iter().rev()
|
| 154 |
+
.filter(|m| m.role == "user")
|
| 155 |
+
.find_map(|m| {
|
| 156 |
+
let content = match &m.content {
|
| 157 |
+
crate::proxy::mappers::claude::models::MessageContent::String(s) => s.to_string(),
|
| 158 |
+
crate::proxy::mappers::claude::models::MessageContent::Array(arr) => {
|
| 159 |
+
arr.iter()
|
| 160 |
+
.filter_map(|block| match block {
|
| 161 |
+
crate::proxy::mappers::claude::models::ContentBlock::Text { text } => Some(text.as_str()),
|
| 162 |
+
_ => None,
|
| 163 |
+
})
|
| 164 |
+
.collect::<Vec<_>>()
|
| 165 |
+
.join(" ")
|
| 166 |
+
}
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
// 过滤规则:忽略系统消息
|
| 170 |
+
if content.trim().is_empty()
|
| 171 |
+
|| content.starts_with("Warmup")
|
| 172 |
+
|| content.contains("<system-reminder>")
|
| 173 |
+
{
|
| 174 |
+
None
|
| 175 |
+
} else {
|
| 176 |
+
Some(content)
|
| 177 |
+
}
|
| 178 |
+
})
|
| 179 |
+
.unwrap_or_default();
|
| 180 |
+
|
| 181 |
+
// [DEBUG] 打印提取结果
|
| 182 |
+
tracing::debug!("[{}] DEBUG - Extracted last_user_msg length: {}, preview: {:.100}",
|
| 183 |
+
trace_id,
|
| 184 |
+
last_user_msg.len(),
|
| 185 |
+
last_user_msg
|
| 186 |
+
);
|
| 187 |
+
|
| 188 |
+
// 关键词识别:标题生成、摘要提取、下一步提示建议等
|
| 189 |
+
// [Optimization] 增加长度限制:真实用户提问通常不会包含这些特殊指令,且后台任务通常极短
|
| 190 |
+
let preview_msg = last_user_msg.chars().take(500).collect::<String>();
|
| 191 |
+
|
| 192 |
+
// [CRITICAL FIX] 强制识别系统消息为后台任务,防止它们消耗顶配额度
|
| 193 |
+
let is_system_message = preview_msg.starts_with("Warmup")
|
| 194 |
+
|| preview_msg.contains("<system-reminder>")
|
| 195 |
+
|| preview_msg.contains("Caveat: The messages below were generated by the user while running local commands");
|
| 196 |
+
|
| 197 |
+
let is_background_task = is_system_message || (
|
| 198 |
+
(preview_msg.contains("write a 5-10 word title")
|
| 199 |
+
|| preview_msg.contains("Respond with the title")
|
| 200 |
+
|| preview_msg.contains("Concise summary")
|
| 201 |
+
|| preview_msg.contains("prompt suggestion generator"))
|
| 202 |
+
&& last_user_msg.len() < 800
|
| 203 |
+
); // 额外保险:后台任务通常不超过 800 字符
|
| 204 |
+
|
| 205 |
+
// 传递映射后的模型名
|
| 206 |
+
let mut request_with_mapped = request_for_body.clone();
|
| 207 |
+
|
| 208 |
+
if is_background_task {
|
| 209 |
+
mapped_model = "gemini-2.5-flash".to_string();
|
| 210 |
+
tracing::info!("[{}][AUTO] 检测到后台任务 ({}),已重定向: {}",
|
| 211 |
+
trace_id,
|
| 212 |
+
preview_msg,
|
| 213 |
+
mapped_model
|
| 214 |
+
);
|
| 215 |
+
// [Optimization] **后台任务净化**:
|
| 216 |
+
// 此类任务纯粹为文本处理,绝不需要执行工具。
|
| 217 |
+
// 强制清空 tools 字段,彻底根除 "Multiple tools" (400) 冲突风险。
|
| 218 |
+
request_with_mapped.tools = None;
|
| 219 |
+
} else {
|
| 220 |
+
// [USER] 标记真实用户请求
|
| 221 |
+
// [Optimization] 使用 WARN 级别高亮��示用户消息,防止被后台任务日志淹没
|
| 222 |
+
tracing::warn!("[{}][USER] 检测到用户交互请求 ({:.100}),保持原模型: {}",
|
| 223 |
+
trace_id,
|
| 224 |
+
preview_msg,
|
| 225 |
+
mapped_model
|
| 226 |
+
);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
request_with_mapped.model = mapped_model;
|
| 231 |
+
|
| 232 |
+
// 生成 Trace ID (简单用时间戳后缀)
|
| 233 |
+
// let _trace_id = format!("req_{}", chrono::Utc::now().timestamp_subsec_millis());
|
| 234 |
+
|
| 235 |
+
let gemini_body = match transform_claude_request_in(&request_with_mapped, &project_id) {
|
| 236 |
+
Ok(b) => {
|
| 237 |
+
tracing::info!("[{}] Transformed Gemini Body: {}", trace_id, serde_json::to_string_pretty(&b).unwrap_or_default());
|
| 238 |
+
b
|
| 239 |
+
},
|
| 240 |
+
Err(e) => {
|
| 241 |
+
return (
|
| 242 |
+
StatusCode::INTERNAL_SERVER_ERROR,
|
| 243 |
+
Json(json!({
|
| 244 |
+
"type": "error",
|
| 245 |
+
"error": {
|
| 246 |
+
"type": "api_error",
|
| 247 |
+
"message": format!("Transform error: {}", e)
|
| 248 |
+
}
|
| 249 |
+
}))
|
| 250 |
+
).into_response();
|
| 251 |
+
}
|
| 252 |
+
};
|
| 253 |
+
|
| 254 |
+
// 4. 上游调用
|
| 255 |
+
let is_stream = request.stream;
|
| 256 |
+
let method = if is_stream { "streamGenerateContent" } else { "generateContent" };
|
| 257 |
+
let query = if is_stream { Some("alt=sse") } else { None };
|
| 258 |
+
|
| 259 |
+
let response = match upstream.call_v1_internal(
|
| 260 |
+
method,
|
| 261 |
+
&access_token,
|
| 262 |
+
gemini_body,
|
| 263 |
+
query
|
| 264 |
+
).await {
|
| 265 |
+
Ok(r) => r,
|
| 266 |
+
Err(e) => {
|
| 267 |
+
last_error = e.clone();
|
| 268 |
+
tracing::warn!("Request failed on attempt {}/{}: {}", attempt + 1, max_attempts, e);
|
| 269 |
+
continue;
|
| 270 |
+
}
|
| 271 |
+
};
|
| 272 |
+
|
| 273 |
+
let status = response.status();
|
| 274 |
+
|
| 275 |
+
// 成功
|
| 276 |
+
if status.is_success() {
|
| 277 |
+
// 处理流式响应
|
| 278 |
+
if request.stream {
|
| 279 |
+
let stream = response.bytes_stream();
|
| 280 |
+
let gemini_stream = Box::pin(stream);
|
| 281 |
+
let claude_stream = create_claude_sse_stream(gemini_stream, trace_id);
|
| 282 |
+
|
| 283 |
+
// 转换为 Bytes stream
|
| 284 |
+
let sse_stream = claude_stream.map(|result| -> Result<Bytes, std::io::Error> {
|
| 285 |
+
match result {
|
| 286 |
+
Ok(bytes) => Ok(bytes),
|
| 287 |
+
Err(e) => Ok(Bytes::from(format!("data: {{\"error\":\"{}\"}}\n\n", e))),
|
| 288 |
+
}
|
| 289 |
+
});
|
| 290 |
+
|
| 291 |
+
return Response::builder()
|
| 292 |
+
.status(StatusCode::OK)
|
| 293 |
+
.header(header::CONTENT_TYPE, "text/event-stream")
|
| 294 |
+
.header(header::CACHE_CONTROL, "no-cache")
|
| 295 |
+
.header(header::CONNECTION, "keep-alive")
|
| 296 |
+
.body(Body::from_stream(sse_stream))
|
| 297 |
+
.unwrap();
|
| 298 |
+
} else {
|
| 299 |
+
// 处理非流式响应
|
| 300 |
+
let bytes = match response.bytes().await {
|
| 301 |
+
Ok(b) => b,
|
| 302 |
+
Err(e) => return (StatusCode::BAD_GATEWAY, format!("Failed to read body: {}", e)).into_response(),
|
| 303 |
+
};
|
| 304 |
+
|
| 305 |
+
// Debug print
|
| 306 |
+
if let Ok(text) = String::from_utf8(bytes.to_vec()) {
|
| 307 |
+
debug!("Upstream Response for Claude request: {}", text);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
let gemini_resp: Value = match serde_json::from_slice(&bytes) {
|
| 311 |
+
Ok(v) => v,
|
| 312 |
+
Err(e) => return (StatusCode::BAD_GATEWAY, format!("Parse error: {}", e)).into_response(),
|
| 313 |
+
};
|
| 314 |
+
|
| 315 |
+
// 解包 response 字段(v1internal 格式)
|
| 316 |
+
let raw = gemini_resp.get("response").unwrap_or(&gemini_resp);
|
| 317 |
+
|
| 318 |
+
// 转换为 Gemini Response 结构
|
| 319 |
+
let gemini_response: crate::proxy::mappers::claude::models::GeminiResponse = match serde_json::from_value(raw.clone()) {
|
| 320 |
+
Ok(r) => r,
|
| 321 |
+
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Convert error: {}", e)).into_response(),
|
| 322 |
+
};
|
| 323 |
+
|
| 324 |
+
// 转换
|
| 325 |
+
let claude_response = match transform_response(&gemini_response) {
|
| 326 |
+
Ok(r) => r,
|
| 327 |
+
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("Transform error: {}", e)).into_response(),
|
| 328 |
+
};
|
| 329 |
+
|
| 330 |
+
// [Optimization] 记录闭环日志:消耗情况
|
| 331 |
+
tracing::info!(
|
| 332 |
+
"[{}] Request finished. Model: {}, Tokens: In {}, Out {}",
|
| 333 |
+
trace_id,
|
| 334 |
+
request_with_mapped.model,
|
| 335 |
+
claude_response.usage.input_tokens,
|
| 336 |
+
claude_response.usage.output_tokens
|
| 337 |
+
);
|
| 338 |
+
|
| 339 |
+
return Json(claude_response).into_response();
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
// 处理错误
|
| 344 |
+
let error_text = response.text().await.unwrap_or_else(|_| format!("HTTP {}", status));
|
| 345 |
+
last_error = format!("HTTP {}: {}", status, error_text);
|
| 346 |
+
tracing::error!("[{}] Upstream Error Response: {}", trace_id, error_text);
|
| 347 |
+
|
| 348 |
+
let status_code = status.as_u16();
|
| 349 |
+
|
| 350 |
+
// Handle transient 429s using upstream-provided retry delay (avoid surfacing errors to clients).
|
| 351 |
+
if status_code == 429 {
|
| 352 |
+
if let Some(delay_ms) = crate::proxy::upstream::retry::parse_retry_delay(&error_text) {
|
| 353 |
+
let actual_delay = delay_ms.saturating_add(200).min(10_000);
|
| 354 |
+
tracing::warn!(
|
| 355 |
+
"Claude Upstream 429 on attempt {}/{}, waiting {}ms then retrying",
|
| 356 |
+
attempt + 1,
|
| 357 |
+
max_attempts,
|
| 358 |
+
actual_delay
|
| 359 |
+
);
|
| 360 |
+
sleep(Duration::from_millis(actual_delay)).await;
|
| 361 |
+
continue;
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
// Special-case 400 errors caused by invalid/foreign thinking signatures (common after /resume).
|
| 366 |
+
// Retry once by stripping thinking blocks & thinking config from the request, and by disabling
|
| 367 |
+
// the "-thinking" model variant if present.
|
| 368 |
+
if status_code == 400
|
| 369 |
+
&& !retried_without_thinking
|
| 370 |
+
&& (error_text.contains("Invalid `signature`")
|
| 371 |
+
|| error_text.contains("thinking.signature: Field required")
|
| 372 |
+
|| error_text.contains("thinking.thinking: Field required")
|
| 373 |
+
|| error_text.contains("thinking.signature")
|
| 374 |
+
|| error_text.contains("thinking.thinking"))
|
| 375 |
+
{
|
| 376 |
+
retried_without_thinking = true;
|
| 377 |
+
tracing::warn!("Upstream rejected thinking signature; retrying once with thinking stripped");
|
| 378 |
+
|
| 379 |
+
// 1) Remove thinking config
|
| 380 |
+
request_for_body.thinking = None;
|
| 381 |
+
|
| 382 |
+
// 2) Remove thinking blocks from message history
|
| 383 |
+
for msg in request_for_body.messages.iter_mut() {
|
| 384 |
+
if let crate::proxy::mappers::claude::models::MessageContent::Array(blocks) = &mut msg.content {
|
| 385 |
+
blocks.retain(|b| !matches!(b, crate::proxy::mappers::claude::models::ContentBlock::Thinking { .. }));
|
| 386 |
+
}
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
// 3) Prefer non-thinking Claude model variant on retry (best-effort)
|
| 390 |
+
if request_for_body.model.contains("claude-") {
|
| 391 |
+
let mut m = request_for_body.model.clone();
|
| 392 |
+
m = m.replace("-thinking", "");
|
| 393 |
+
// If it's a dated alias, fall back to a stable non-thinking id
|
| 394 |
+
if m.contains("claude-sonnet-4-5-") {
|
| 395 |
+
m = "claude-sonnet-4-5".to_string();
|
| 396 |
+
} else if m.contains("claude-opus-4-5-") || m.contains("claude-opus-4-") {
|
| 397 |
+
m = "claude-opus-4-5".to_string();
|
| 398 |
+
}
|
| 399 |
+
request_for_body.model = m;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
continue;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
// 只有 429 (限流), 403 (权限/地区限制) 和 401 (认证失效) 触发账号轮换
|
| 406 |
+
if status_code == 429 || status_code == 403 || status_code == 401 {
|
| 407 |
+
// 如果是 429 且标记为配额耗尽(明确),直接报错,避免穿透整个账号池
|
| 408 |
+
if status_code == 429 && error_text.contains("QUOTA_EXHAUSTED") {
|
| 409 |
+
error!("Claude Quota exhausted (429) on attempt {}/{}, stopping to protect pool.", attempt + 1, max_attempts);
|
| 410 |
+
return (status, error_text).into_response();
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
tracing::warn!("Claude Upstream {} on attempt {}/{}, rotating account", status, attempt + 1, max_attempts);
|
| 414 |
+
continue;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
// 404 等由于模型配置或路径错误的 HTTP 异常,直接报错,不进行无效轮换
|
| 418 |
+
error!("Claude Upstream non-retryable error {}: {}", status_code, error_text);
|
| 419 |
+
return (status, error_text).into_response();
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
(StatusCode::TOO_MANY_REQUESTS, Json(json!({
|
| 423 |
+
"type": "error",
|
| 424 |
+
"error": {
|
| 425 |
+
"type": "overloaded_error",
|
| 426 |
+
"message": format!("All {} attempts failed. Last error: {}", max_attempts, last_error)
|
| 427 |
+
}
|
| 428 |
+
}))).into_response()
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
/// 列出可用模型
|
| 432 |
+
pub async fn handle_list_models() -> impl IntoResponse {
|
| 433 |
+
Json(json!({
|
| 434 |
+
"object": "list",
|
| 435 |
+
"data": [
|
| 436 |
+
{
|
| 437 |
+
"id": "claude-sonnet-4-5",
|
| 438 |
+
"object": "model",
|
| 439 |
+
"created": 1706745600,
|
| 440 |
+
"owned_by": "anthropic"
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"id": "claude-opus-4-5-thinking",
|
| 444 |
+
"object": "model",
|
| 445 |
+
"created": 1706745600,
|
| 446 |
+
"owned_by": "anthropic"
|
| 447 |
+
},
|
| 448 |
+
{
|
| 449 |
+
"id": "claude-3-5-sonnet-20241022",
|
| 450 |
+
"object": "model",
|
| 451 |
+
"created": 1706745600,
|
| 452 |
+
"owned_by": "anthropic"
|
| 453 |
+
}
|
| 454 |
+
]
|
| 455 |
+
}))
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
/// 计算 tokens (占位符)
|
| 459 |
+
pub async fn handle_count_tokens(Json(_body): Json<Value>) -> impl IntoResponse {
|
| 460 |
+
Json(json!({
|
| 461 |
+
"input_tokens": 0,
|
| 462 |
+
"output_tokens": 0
|
| 463 |
+
}))
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
#[cfg(test)]
|
| 467 |
+
mod tests {
|
| 468 |
+
use super::*;
|
| 469 |
+
|
| 470 |
+
#[tokio::test]
|
| 471 |
+
async fn test_handle_list_models() {
|
| 472 |
+
let response = handle_list_models().await.into_response();
|
| 473 |
+
assert_eq!(response.status(), StatusCode::OK);
|
| 474 |
+
}
|
| 475 |
+
}
|
server/src/proxy/handlers/gemini.rs
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Gemini Handler
|
| 2 |
+
use axum::{extract::State, extract::{Json, Path}, http::StatusCode, response::IntoResponse};
|
| 3 |
+
use serde_json::{json, Value};
|
| 4 |
+
use tracing::{debug, error};
|
| 5 |
+
|
| 6 |
+
use crate::proxy::mappers::gemini::{wrap_request, unwrap_response};
|
| 7 |
+
use crate::proxy::server::AppState;
|
| 8 |
+
|
| 9 |
+
const MAX_RETRY_ATTEMPTS: usize = 3;
|
| 10 |
+
|
| 11 |
+
/// 处理 generateContent 和 streamGenerateContent
|
| 12 |
+
/// 路径参数: model_name, method (e.g. "gemini-pro", "generateContent")
|
| 13 |
+
pub async fn handle_generate(
|
| 14 |
+
State(state): State<AppState>,
|
| 15 |
+
Path(model_action): Path<String>,
|
| 16 |
+
Json(body): Json<Value>
|
| 17 |
+
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
| 18 |
+
// 解析 model:method
|
| 19 |
+
let (model_name, method) = if let Some((m, action)) = model_action.rsplit_once(':') {
|
| 20 |
+
(m.to_string(), action.to_string())
|
| 21 |
+
} else {
|
| 22 |
+
(model_action, "generateContent".to_string())
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
crate::modules::logger::log_info(&format!("Received Gemini request: {}/{}", model_name, method));
|
| 26 |
+
|
| 27 |
+
// 1. 验证方法
|
| 28 |
+
if method != "generateContent" && method != "streamGenerateContent" {
|
| 29 |
+
return Err((StatusCode::BAD_REQUEST, format!("Unsupported method: {}", method)));
|
| 30 |
+
}
|
| 31 |
+
let is_stream = method == "streamGenerateContent";
|
| 32 |
+
|
| 33 |
+
// 2. 获取 UpstreamClient 和 TokenManager
|
| 34 |
+
let upstream = state.upstream.clone();
|
| 35 |
+
let token_manager = state.token_manager;
|
| 36 |
+
let pool_size = token_manager.len();
|
| 37 |
+
let max_attempts = MAX_RETRY_ATTEMPTS.min(pool_size).max(1);
|
| 38 |
+
|
| 39 |
+
let mut last_error = String::new();
|
| 40 |
+
|
| 41 |
+
for attempt in 0..max_attempts {
|
| 42 |
+
// 3. 模型路由与配置解析
|
| 43 |
+
let mapped_model = crate::proxy::common::model_mapping::resolve_model_route(
|
| 44 |
+
&model_name,
|
| 45 |
+
&*state.custom_mapping.read().await,
|
| 46 |
+
&*state.openai_mapping.read().await,
|
| 47 |
+
&*state.anthropic_mapping.read().await,
|
| 48 |
+
);
|
| 49 |
+
// 提取 tools 列表以进行联网探测 (Gemini 风格可能是嵌套的)
|
| 50 |
+
let tools_val: Option<Vec<Value>> = body.get("tools").and_then(|t| t.as_array()).map(|arr| {
|
| 51 |
+
let mut flattened = Vec::new();
|
| 52 |
+
for tool_entry in arr {
|
| 53 |
+
if let Some(decls) = tool_entry.get("functionDeclarations").and_then(|v| v.as_array()) {
|
| 54 |
+
flattened.extend(decls.iter().cloned());
|
| 55 |
+
} else {
|
| 56 |
+
flattened.push(tool_entry.clone());
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
flattened
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
let config = crate::proxy::mappers::common_utils::resolve_request_config(&model_name, &mapped_model, &tools_val);
|
| 63 |
+
|
| 64 |
+
// 4. 获取 Token (使用准确的 request_type)
|
| 65 |
+
// 关键:在重试尝试 (attempt > 0) 时强制轮换账号
|
| 66 |
+
let (access_token, project_id, email) = match token_manager.get_token(&config.request_type, attempt > 0).await {
|
| 67 |
+
Ok(t) => t,
|
| 68 |
+
Err(e) => {
|
| 69 |
+
return Err((StatusCode::SERVICE_UNAVAILABLE, format!("Token error: {}", e)));
|
| 70 |
+
}
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
tracing::info!("Using account: {} for request (type: {})", email, config.request_type);
|
| 74 |
+
|
| 75 |
+
// 5. 包装请求 (project injection)
|
| 76 |
+
let wrapped_body = wrap_request(&body, &project_id, &mapped_model);
|
| 77 |
+
|
| 78 |
+
// 5. 上游调用
|
| 79 |
+
let query_string = if is_stream { Some("alt=sse") } else { None };
|
| 80 |
+
let upstream_method = if is_stream { "streamGenerateContent" } else { "generateContent" };
|
| 81 |
+
|
| 82 |
+
let response = match upstream
|
| 83 |
+
.call_v1_internal(upstream_method, &access_token, wrapped_body, query_string)
|
| 84 |
+
.await {
|
| 85 |
+
Ok(r) => r,
|
| 86 |
+
Err(e) => {
|
| 87 |
+
last_error = e.clone();
|
| 88 |
+
tracing::warn!("Gemini Request failed on attempt {}/{}: {}", attempt + 1, max_attempts, e);
|
| 89 |
+
continue;
|
| 90 |
+
}
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
let status = response.status();
|
| 94 |
+
if status.is_success() {
|
| 95 |
+
// 6. 响应处理
|
| 96 |
+
if is_stream {
|
| 97 |
+
use axum::body::Body;
|
| 98 |
+
use axum::response::Response;
|
| 99 |
+
use bytes::{Bytes, BytesMut};
|
| 100 |
+
use futures::StreamExt;
|
| 101 |
+
|
| 102 |
+
let mut response_stream = response.bytes_stream();
|
| 103 |
+
let mut buffer = BytesMut::new();
|
| 104 |
+
|
| 105 |
+
let stream = async_stream::stream! {
|
| 106 |
+
while let Some(item) = response_stream.next().await {
|
| 107 |
+
match item {
|
| 108 |
+
Ok(bytes) => {
|
| 109 |
+
debug!("[Gemini-SSE] Received chunk: {} bytes", bytes.len());
|
| 110 |
+
buffer.extend_from_slice(&bytes);
|
| 111 |
+
while let Some(pos) = buffer.iter().position(|&b| b == b'\n') {
|
| 112 |
+
let line_raw = buffer.split_to(pos + 1);
|
| 113 |
+
if let Ok(line_str) = std::str::from_utf8(&line_raw) {
|
| 114 |
+
let line = line_str.trim();
|
| 115 |
+
if line.is_empty() { continue; }
|
| 116 |
+
|
| 117 |
+
if line.starts_with("data: ") {
|
| 118 |
+
let json_part = line.trim_start_matches("data: ").trim();
|
| 119 |
+
if json_part == "[DONE]" {
|
| 120 |
+
yield Ok::<Bytes, String>(Bytes::from("data: [DONE]\n\n"));
|
| 121 |
+
continue;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
match serde_json::from_str::<Value>(json_part) {
|
| 125 |
+
Ok(mut json) => {
|
| 126 |
+
// Unwrap v1internal response wrapper
|
| 127 |
+
if let Some(inner) = json.get_mut("response").map(|v| v.take()) {
|
| 128 |
+
let new_line = format!("data: {}\n\n", serde_json::to_string(&inner).unwrap_or_default());
|
| 129 |
+
yield Ok::<Bytes, String>(Bytes::from(new_line));
|
| 130 |
+
} else {
|
| 131 |
+
yield Ok::<Bytes, String>(Bytes::from(format!("data: {}\n\n", serde_json::to_string(&json).unwrap_or_default())));
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
Err(e) => {
|
| 135 |
+
debug!("[Gemini-SSE] JSON parse error: {}, passing raw line", e);
|
| 136 |
+
yield Ok::<Bytes, String>(Bytes::from(format!("{}\n\n", line)));
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
} else {
|
| 140 |
+
// Non-data lines (comments, etc.)
|
| 141 |
+
yield Ok::<Bytes, String>(Bytes::from(format!("{}\n\n", line)));
|
| 142 |
+
}
|
| 143 |
+
} else {
|
| 144 |
+
// Non-UTF8 data? Just pass it through or skip
|
| 145 |
+
debug!("[Gemini-SSE] Non-UTF8 line encountered");
|
| 146 |
+
yield Ok::<Bytes, String>(line_raw.freeze());
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
Err(e) => {
|
| 151 |
+
error!("[Gemini-SSE] Connection error: {}", e);
|
| 152 |
+
yield Err(format!("Stream error: {}", e));
|
| 153 |
+
}
|
| 154 |
+
}
|
| 155 |
+
}
|
| 156 |
+
};
|
| 157 |
+
|
| 158 |
+
let body = Body::from_stream(stream);
|
| 159 |
+
return Ok(Response::builder()
|
| 160 |
+
.header("Content-Type", "text/event-stream")
|
| 161 |
+
.header("Cache-Control", "no-cache")
|
| 162 |
+
.header("Connection", "keep-alive")
|
| 163 |
+
.body(body)
|
| 164 |
+
.unwrap()
|
| 165 |
+
.into_response());
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
let gemini_resp: Value = response
|
| 169 |
+
.json()
|
| 170 |
+
.await
|
| 171 |
+
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Parse error: {}", e)))?;
|
| 172 |
+
|
| 173 |
+
let unwrapped = unwrap_response(&gemini_resp);
|
| 174 |
+
return Ok(Json(unwrapped).into_response());
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
// 处理错误并重试
|
| 178 |
+
let status_code = status.as_u16();
|
| 179 |
+
let error_text = response.text().await.unwrap_or_default();
|
| 180 |
+
last_error = format!("HTTP {}: {}", status_code, error_text);
|
| 181 |
+
|
| 182 |
+
// 只有 429 (限流), 403 (权限/地区限制) 和 401 (认证失效) 触发账号轮换
|
| 183 |
+
if status_code == 429 || status_code == 403 || status_code == 401 {
|
| 184 |
+
// 只有明确包含 "QUOTA_EXHAUSTED" 才停止,避免误判上游的频率限制提示 (如 "check quota")
|
| 185 |
+
if status_code == 429 && error_text.contains("QUOTA_EXHAUSTED") {
|
| 186 |
+
error!("Gemini Quota exhausted (429) on attempt {}/{}, stopping to protect pool.", attempt + 1, max_attempts);
|
| 187 |
+
return Err((status, error_text));
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
tracing::warn!("Gemini Upstream {} on attempt {}/{}, rotating account", status_code, attempt + 1, max_attempts);
|
| 191 |
+
continue;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
// 404 等由于模型配置或路径错误的 HTTP 异常,直接报错,不进行无效轮换
|
| 195 |
+
error!("Gemini Upstream non-retryable error {}: {}", status_code, error_text);
|
| 196 |
+
return Err((status, error_text));
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
Ok((StatusCode::TOO_MANY_REQUESTS, format!("All accounts exhausted. Last error: {}", last_error)).into_response())
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
pub async fn handle_list_models(State(state): State<AppState>) -> Result<impl IntoResponse, (StatusCode, String)> {
|
| 203 |
+
let model_group = "gemini";
|
| 204 |
+
let (access_token, _, _) = state.token_manager.get_token(model_group, false).await
|
| 205 |
+
.map_err(|e| (StatusCode::SERVICE_UNAVAILABLE, format!("Token error: {}", e)))?;
|
| 206 |
+
|
| 207 |
+
// Fetch from upstream
|
| 208 |
+
let upstream_models = state.upstream.fetch_available_models(&access_token).await
|
| 209 |
+
.map_err(|e| (StatusCode::BAD_GATEWAY, e))?;
|
| 210 |
+
|
| 211 |
+
// Transform map to Gemini list format
|
| 212 |
+
let mut models = Vec::new();
|
| 213 |
+
if let Some(obj) = upstream_models.as_object() {
|
| 214 |
+
tracing::info!("Upstream models keys: {:?}", obj.keys());
|
| 215 |
+
for (key, value) in obj {
|
| 216 |
+
let description = value.get("description").and_then(|v| v.as_str()).unwrap_or("");
|
| 217 |
+
let display_name = value.get("displayName").and_then(|v| v.as_str()).unwrap_or(key);
|
| 218 |
+
|
| 219 |
+
models.push(json!({
|
| 220 |
+
"name": format!("models/{}", key),
|
| 221 |
+
"version": "001",
|
| 222 |
+
"displayName": display_name,
|
| 223 |
+
"description": description,
|
| 224 |
+
"inputTokenLimit": 128000,
|
| 225 |
+
"outputTokenLimit": 8192,
|
| 226 |
+
"supportedGenerationMethods": ["generateContent", "countTokens"],
|
| 227 |
+
"temperature": 1.0,
|
| 228 |
+
"topP": 0.95,
|
| 229 |
+
"topK": 64
|
| 230 |
+
}));
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Fallback
|
| 235 |
+
if models.is_empty() {
|
| 236 |
+
models.push(json!({
|
| 237 |
+
"name": "models/gemini-2.5-pro",
|
| 238 |
+
"displayName": "Gemini 2.5 Pro",
|
| 239 |
+
"supportedGenerationMethods": ["generateContent", "countTokens"]
|
| 240 |
+
}));
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
Ok(Json(json!({ "models": models })))
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
pub async fn handle_get_model(Path(model_name): Path<String>) -> impl IntoResponse {
|
| 247 |
+
Json(json!({
|
| 248 |
+
"name": format!("models/{}", model_name),
|
| 249 |
+
"displayName": model_name
|
| 250 |
+
}))
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
pub async fn handle_count_tokens(State(state): State<AppState>, Path(_model_name): Path<String>, Json(_body): Json<Value>) -> Result<impl IntoResponse, (StatusCode, String)> {
|
| 254 |
+
let model_group = "gemini";
|
| 255 |
+
let (_access_token, _project_id, _) = state.token_manager.get_token(model_group, false).await
|
| 256 |
+
.map_err(|e| (StatusCode::SERVICE_UNAVAILABLE, format!("Token error: {}", e)))?;
|
| 257 |
+
|
| 258 |
+
Ok(Json(json!({"totalTokens": 0})))
|
| 259 |
+
}
|
server/src/proxy/handlers/mod.rs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Handlers 模块 - API 端点处理器
|
| 2 |
+
// 核心端点处理器模块
|
| 3 |
+
|
| 4 |
+
pub mod claude;
|
| 5 |
+
pub mod openai;
|
| 6 |
+
pub mod gemini;
|
server/src/proxy/handlers/openai.rs
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// OpenAI Handler
|
| 2 |
+
use axum::{extract::State, extract::Json, http::StatusCode, response::IntoResponse};
|
| 3 |
+
use serde_json::{json, Value};
|
| 4 |
+
use tracing::{debug, error};
|
| 5 |
+
|
| 6 |
+
use crate::proxy::mappers::openai::{transform_openai_request, transform_openai_response, OpenAIRequest};
|
| 7 |
+
// use crate::proxy::upstream::client::UpstreamClient; // 通过 state 获取
|
| 8 |
+
use crate::proxy::server::AppState;
|
| 9 |
+
|
| 10 |
+
const MAX_RETRY_ATTEMPTS: usize = 3;
|
| 11 |
+
|
| 12 |
+
pub async fn handle_chat_completions(
|
| 13 |
+
State(state): State<AppState>,
|
| 14 |
+
Json(body): Json<Value>
|
| 15 |
+
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
| 16 |
+
let mut openai_req: OpenAIRequest = serde_json::from_value(body)
|
| 17 |
+
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid request: {}", e)))?;
|
| 18 |
+
|
| 19 |
+
// Safety: Ensure messages is not empty
|
| 20 |
+
if openai_req.messages.is_empty() {
|
| 21 |
+
tracing::warn!("Received request with empty messages, injecting fallback...");
|
| 22 |
+
openai_req.messages.push(crate::proxy::mappers::openai::OpenAIMessage {
|
| 23 |
+
role: "user".to_string(),
|
| 24 |
+
content: Some(crate::proxy::mappers::openai::OpenAIContent::String(" ".to_string())),
|
| 25 |
+
tool_calls: None,
|
| 26 |
+
tool_call_id: None,
|
| 27 |
+
name: None,
|
| 28 |
+
});
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
debug!("Received OpenAI request for model: {}", openai_req.model);
|
| 32 |
+
|
| 33 |
+
// 1. 获取 UpstreamClient (Clone handle)
|
| 34 |
+
let upstream = state.upstream.clone();
|
| 35 |
+
let token_manager = state.token_manager;
|
| 36 |
+
let pool_size = token_manager.len();
|
| 37 |
+
let max_attempts = MAX_RETRY_ATTEMPTS.min(pool_size).max(1);
|
| 38 |
+
|
| 39 |
+
let mut last_error = String::new();
|
| 40 |
+
|
| 41 |
+
for attempt in 0..max_attempts {
|
| 42 |
+
// 2. 预解析模型路由与配置
|
| 43 |
+
let mapped_model = crate::proxy::common::model_mapping::resolve_model_route(
|
| 44 |
+
&openai_req.model,
|
| 45 |
+
&*state.custom_mapping.read().await,
|
| 46 |
+
&*state.openai_mapping.read().await,
|
| 47 |
+
&*state.anthropic_mapping.read().await,
|
| 48 |
+
);
|
| 49 |
+
// 将 OpenAI 工具转为 Value 数组以便探测联网
|
| 50 |
+
let tools_val: Option<Vec<Value>> = openai_req.tools.as_ref().map(|list| {
|
| 51 |
+
list.iter().cloned().collect()
|
| 52 |
+
});
|
| 53 |
+
let config = crate::proxy::mappers::common_utils::resolve_request_config(&openai_req.model, &mapped_model, &tools_val);
|
| 54 |
+
|
| 55 |
+
// 3. 获取 Token (使用准确的 request_type)
|
| 56 |
+
// 关键:在重试尝试 (attempt > 0) 时强制轮换账号
|
| 57 |
+
let (access_token, project_id, email) = match token_manager.get_token(&config.request_type, attempt > 0).await {
|
| 58 |
+
Ok(t) => t,
|
| 59 |
+
Err(e) => {
|
| 60 |
+
return Err((StatusCode::SERVICE_UNAVAILABLE, format!("Token error: {}", e)));
|
| 61 |
+
}
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
tracing::info!("Using account: {} for request (type: {})", email, config.request_type);
|
| 65 |
+
|
| 66 |
+
// 4. 转换请求
|
| 67 |
+
let gemini_body = transform_openai_request(&openai_req, &project_id, &mapped_model);
|
| 68 |
+
|
| 69 |
+
// [New] 打印转换后的报文 (Gemini Body) 供调试
|
| 70 |
+
if let Ok(body_json) = serde_json::to_string_pretty(&gemini_body) {
|
| 71 |
+
tracing::info!("[OpenAI-Request] Transformed Gemini Body:\n{}", body_json);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// 5. 发送请求
|
| 75 |
+
let list_response = openai_req.stream;
|
| 76 |
+
let method = if list_response { "streamGenerateContent" } else { "generateContent" };
|
| 77 |
+
let query_string = if list_response { Some("alt=sse") } else { None };
|
| 78 |
+
|
| 79 |
+
let response = match upstream
|
| 80 |
+
.call_v1_internal(method, &access_token, gemini_body, query_string)
|
| 81 |
+
.await {
|
| 82 |
+
Ok(r) => r,
|
| 83 |
+
Err(e) => {
|
| 84 |
+
last_error = e.clone();
|
| 85 |
+
tracing::warn!("OpenAI Request failed on attempt {}/{}: {}", attempt + 1, max_attempts, e);
|
| 86 |
+
continue;
|
| 87 |
+
}
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
let status = response.status();
|
| 91 |
+
if status.is_success() {
|
| 92 |
+
// 5. 处理流式 vs 非流式
|
| 93 |
+
if list_response {
|
| 94 |
+
use crate::proxy::mappers::openai::streaming::create_openai_sse_stream;
|
| 95 |
+
use axum::response::Response;
|
| 96 |
+
use axum::body::Body;
|
| 97 |
+
// Removed redundant StreamExt
|
| 98 |
+
|
| 99 |
+
let gemini_stream = response.bytes_stream();
|
| 100 |
+
let openai_stream = create_openai_sse_stream(Box::pin(gemini_stream), openai_req.model.clone());
|
| 101 |
+
let body = Body::from_stream(openai_stream);
|
| 102 |
+
|
| 103 |
+
return Ok(Response::builder()
|
| 104 |
+
.header("Content-Type", "text/event-stream")
|
| 105 |
+
.header("Cache-Control", "no-cache")
|
| 106 |
+
.header("Connection", "keep-alive")
|
| 107 |
+
.body(body)
|
| 108 |
+
.unwrap()
|
| 109 |
+
.into_response());
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
let gemini_resp: Value = response
|
| 113 |
+
.json()
|
| 114 |
+
.await
|
| 115 |
+
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Parse error: {}", e)))?;
|
| 116 |
+
|
| 117 |
+
let openai_response = transform_openai_response(&gemini_resp);
|
| 118 |
+
return Ok(Json(openai_response).into_response());
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// 处理特定错误并重试
|
| 122 |
+
let status_code = status.as_u16();
|
| 123 |
+
let error_text = response.text().await.unwrap_or_default();
|
| 124 |
+
last_error = format!("HTTP {}: {}", status_code, error_text);
|
| 125 |
+
|
| 126 |
+
// [New] 打印错误报文日志
|
| 127 |
+
tracing::error!("[OpenAI-Upstream] Error Response {}: {}", status_code, error_text);
|
| 128 |
+
|
| 129 |
+
// 429 智能处理
|
| 130 |
+
if status_code == 429 {
|
| 131 |
+
// 1. 优先尝试解析 RetryInfo (由 Google Cloud 直接下发)
|
| 132 |
+
if let Some(delay_ms) = crate::proxy::upstream::retry::parse_retry_delay(&error_text) {
|
| 133 |
+
let actual_delay = delay_ms.saturating_add(200).min(10_000);
|
| 134 |
+
tracing::warn!(
|
| 135 |
+
"OpenAI Upstream 429 on attempt {}/{}, waiting {}ms then retrying",
|
| 136 |
+
attempt + 1,
|
| 137 |
+
max_attempts,
|
| 138 |
+
actual_delay
|
| 139 |
+
);
|
| 140 |
+
tokio::time::sleep(tokio::time::Duration::from_millis(actual_delay)).await;
|
| 141 |
+
continue;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// 2. 只有明确包含 "QUOTA_EXHAUSTED" 才停止,避免误判频率提示 (如 "check quota")
|
| 145 |
+
if error_text.contains("QUOTA_EXHAUSTED") {
|
| 146 |
+
error!("OpenAI Quota exhausted (429) on attempt {}/{}, stopping to protect pool.", attempt + 1, max_attempts);
|
| 147 |
+
return Err((status, error_text));
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
// 3. 其他 429 情况(如无重试指示的频率限制),轮换账号
|
| 151 |
+
tracing::warn!("OpenAI Upstream 429 on attempt {}/{}, rotating account", attempt + 1, max_attempts);
|
| 152 |
+
continue;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// 只有 403 (权限/地区限制) 和 401 (认证失效) 触发账号轮换
|
| 156 |
+
if status_code == 403 || status_code == 401 {
|
| 157 |
+
tracing::warn!("OpenAI Upstream {} on attempt {}/{}, rotating account", status_code, attempt + 1, max_attempts);
|
| 158 |
+
continue;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// 404 等由于模型配置或路径错误的 HTTP 异常,直接报错,不进行无效轮换
|
| 162 |
+
error!("OpenAI Upstream non-retryable error {}: {}", status_code, error_text);
|
| 163 |
+
return Err((status, error_text));
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// 所有尝试均失败
|
| 167 |
+
Err((StatusCode::TOO_MANY_REQUESTS, format!("All accounts exhausted. Last error: {}", last_error)))
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/// 处理 Legacy Completions API (/v1/completions)
|
| 171 |
+
/// 将 Prompt 转换为 Chat Message 格式,复用 handle_chat_completions
|
| 172 |
+
pub async fn handle_completions(
|
| 173 |
+
State(state): State<AppState>,
|
| 174 |
+
Json(mut body): Json<Value>,
|
| 175 |
+
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
| 176 |
+
tracing::info!("Received /v1/completions or /v1/responses payload: {:?}", body);
|
| 177 |
+
|
| 178 |
+
let is_codex_style = body.get("input").is_some() && body.get("instructions").is_some();
|
| 179 |
+
|
| 180 |
+
// 1. Convert Payload to Messages (Shared Chat Format)
|
| 181 |
+
if is_codex_style {
|
| 182 |
+
let instructions = body.get("instructions").and_then(|v| v.as_str()).unwrap_or_default();
|
| 183 |
+
let input_items = body.get("input").and_then(|v| v.as_array());
|
| 184 |
+
|
| 185 |
+
let mut messages = Vec::new();
|
| 186 |
+
|
| 187 |
+
// System Instructions
|
| 188 |
+
if !instructions.is_empty() {
|
| 189 |
+
messages.push(json!({ "role": "system", "content": instructions }));
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
let mut call_id_to_name = std::collections::HashMap::new();
|
| 193 |
+
|
| 194 |
+
// Pass 1: Build Call ID to Name Map
|
| 195 |
+
if let Some(items) = input_items {
|
| 196 |
+
for item in items {
|
| 197 |
+
let item_type = item.get("type").and_then(|v| v.as_str()).unwrap_or("");
|
| 198 |
+
match item_type {
|
| 199 |
+
"function_call" | "local_shell_call" | "web_search_call" => {
|
| 200 |
+
let call_id = item.get("call_id").and_then(|v| v.as_str())
|
| 201 |
+
.or_else(|| item.get("id").and_then(|v| v.as_str()))
|
| 202 |
+
.unwrap_or("unknown");
|
| 203 |
+
|
| 204 |
+
let name = if item_type == "local_shell_call" {
|
| 205 |
+
"shell"
|
| 206 |
+
} else if item_type == "web_search_call" {
|
| 207 |
+
"google_search"
|
| 208 |
+
} else {
|
| 209 |
+
item.get("name").and_then(|v| v.as_str()).unwrap_or("unknown")
|
| 210 |
+
};
|
| 211 |
+
|
| 212 |
+
call_id_to_name.insert(call_id.to_string(), name.to_string());
|
| 213 |
+
tracing::debug!("Mapped call_id {} to name {}", call_id, name);
|
| 214 |
+
}
|
| 215 |
+
_ => {}
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// Pass 2: Map Input Items to Messages
|
| 221 |
+
if let Some(items) = input_items {
|
| 222 |
+
for item in items {
|
| 223 |
+
let item_type = item.get("type").and_then(|v| v.as_str()).unwrap_or("");
|
| 224 |
+
match item_type {
|
| 225 |
+
"message" => {
|
| 226 |
+
let role = item.get("role").and_then(|v| v.as_str()).unwrap_or("user");
|
| 227 |
+
let content = item.get("content").and_then(|v| v.as_array());
|
| 228 |
+
let mut text_parts = Vec::new();
|
| 229 |
+
let mut image_parts: Vec<Value> = Vec::new();
|
| 230 |
+
|
| 231 |
+
if let Some(parts) = content {
|
| 232 |
+
for part in parts {
|
| 233 |
+
// 处理文本块
|
| 234 |
+
if let Some(text) = part.get("text").and_then(|v| v.as_str()) {
|
| 235 |
+
text_parts.push(text.to_string());
|
| 236 |
+
}
|
| 237 |
+
// [NEW] 处理图像块 (Codex input_image 格式)
|
| 238 |
+
else if part.get("type").and_then(|v| v.as_str()) == Some("input_image") {
|
| 239 |
+
if let Some(image_url) = part.get("image_url").and_then(|v| v.as_str()) {
|
| 240 |
+
image_parts.push(json!({
|
| 241 |
+
"type": "image_url",
|
| 242 |
+
"image_url": { "url": image_url }
|
| 243 |
+
}));
|
| 244 |
+
tracing::info!("[Codex] Found input_image: {}", image_url);
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
// [NEW] 兼容标准 OpenAI image_url 格式
|
| 248 |
+
else if part.get("type").and_then(|v| v.as_str()) == Some("image_url") {
|
| 249 |
+
if let Some(url_obj) = part.get("image_url") {
|
| 250 |
+
image_parts.push(json!({
|
| 251 |
+
"type": "image_url",
|
| 252 |
+
"image_url": url_obj.clone()
|
| 253 |
+
}));
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
// 构造消息内容:如果有图像则使用数组格式
|
| 260 |
+
if image_parts.is_empty() {
|
| 261 |
+
messages.push(json!({
|
| 262 |
+
"role": role,
|
| 263 |
+
"content": text_parts.join("\n")
|
| 264 |
+
}));
|
| 265 |
+
} else {
|
| 266 |
+
let mut content_blocks: Vec<Value> = Vec::new();
|
| 267 |
+
if !text_parts.is_empty() {
|
| 268 |
+
content_blocks.push(json!({
|
| 269 |
+
"type": "text",
|
| 270 |
+
"text": text_parts.join("\n")
|
| 271 |
+
}));
|
| 272 |
+
}
|
| 273 |
+
content_blocks.extend(image_parts);
|
| 274 |
+
messages.push(json!({
|
| 275 |
+
"role": role,
|
| 276 |
+
"content": content_blocks
|
| 277 |
+
}));
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
"function_call" | "local_shell_call" | "web_search_call" => {
|
| 281 |
+
let mut name = item.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
|
| 282 |
+
let mut args_str = item.get("arguments").and_then(|v| v.as_str()).unwrap_or("{}").to_string();
|
| 283 |
+
let call_id = item.get("call_id").and_then(|v| v.as_str()).or_else(|| item.get("id").and_then(|v| v.as_str())).unwrap_or("unknown");
|
| 284 |
+
|
| 285 |
+
// Handle native shell calls
|
| 286 |
+
if item_type == "local_shell_call" {
|
| 287 |
+
name = "shell";
|
| 288 |
+
if let Some(action) = item.get("action") {
|
| 289 |
+
if let Some(exec) = action.get("exec") {
|
| 290 |
+
// Map to ShellCommandToolCallParams (string command) or ShellToolCallParams (array command)
|
| 291 |
+
// Most LLMs prefer a single string for shell
|
| 292 |
+
let mut args_obj = serde_json::Map::new();
|
| 293 |
+
if let Some(cmd) = exec.get("command") {
|
| 294 |
+
// CRITICAL FIX: The 'shell' tool schema defines 'command' as an ARRAY of strings.
|
| 295 |
+
// We MUST pass it as an array, not a joined string, otherwise Gemini rejects with 400 INVALID_ARGUMENT.
|
| 296 |
+
let cmd_val = if cmd.is_string() {
|
| 297 |
+
json!([cmd]) // Wrap in array
|
| 298 |
+
} else {
|
| 299 |
+
cmd.clone() // Assume already array
|
| 300 |
+
};
|
| 301 |
+
args_obj.insert("command".to_string(), cmd_val);
|
| 302 |
+
}
|
| 303 |
+
if let Some(wd) = exec.get("working_directory").or(exec.get("workdir")) {
|
| 304 |
+
args_obj.insert("workdir".to_string(), wd.clone());
|
| 305 |
+
}
|
| 306 |
+
args_str = serde_json::to_string(&args_obj).unwrap_or("{}".to_string());
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
} else if item_type == "web_search_call" {
|
| 310 |
+
name = "google_search";
|
| 311 |
+
if let Some(action) = item.get("action") {
|
| 312 |
+
let mut args_obj = serde_json::Map::new();
|
| 313 |
+
if let Some(q) = action.get("query") {
|
| 314 |
+
args_obj.insert("query".to_string(), q.clone());
|
| 315 |
+
}
|
| 316 |
+
args_str = serde_json::to_string(&args_obj).unwrap_or("{}".to_string());
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
messages.push(json!({
|
| 321 |
+
"role": "assistant",
|
| 322 |
+
"tool_calls": [
|
| 323 |
+
{
|
| 324 |
+
"id": call_id,
|
| 325 |
+
"type": "function",
|
| 326 |
+
"function": {
|
| 327 |
+
"name": name,
|
| 328 |
+
"arguments": args_str
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
]
|
| 332 |
+
}));
|
| 333 |
+
}
|
| 334 |
+
"function_call_output" | "custom_tool_call_output" => {
|
| 335 |
+
let call_id = item.get("call_id").and_then(|v| v.as_str()).unwrap_or("unknown");
|
| 336 |
+
let output = item.get("output");
|
| 337 |
+
let output_str = if let Some(o) = output {
|
| 338 |
+
if o.is_string() { o.as_str().unwrap().to_string() }
|
| 339 |
+
else if let Some(content) = o.get("content").and_then(|v| v.as_str()) { content.to_string() }
|
| 340 |
+
else { o.to_string() }
|
| 341 |
+
} else { "".to_string() };
|
| 342 |
+
|
| 343 |
+
let name = call_id_to_name.get(call_id).cloned().unwrap_or_else(|| {
|
| 344 |
+
// Fallback: if unknown and we see function_call_output, it's likely "shell" in this context
|
| 345 |
+
tracing::warn!("Unknown tool name for call_id {}, defaulting to 'shell'", call_id);
|
| 346 |
+
"shell".to_string()
|
| 347 |
+
});
|
| 348 |
+
|
| 349 |
+
messages.push(json!({
|
| 350 |
+
"role": "tool",
|
| 351 |
+
"tool_call_id": call_id,
|
| 352 |
+
"name": name,
|
| 353 |
+
"content": output_str
|
| 354 |
+
}));
|
| 355 |
+
}
|
| 356 |
+
_ => {}
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
if let Some(obj) = body.as_object_mut() {
|
| 362 |
+
obj.insert("messages".to_string(), json!(messages));
|
| 363 |
+
}
|
| 364 |
+
} else if let Some(prompt_val) = body.get("prompt") {
|
| 365 |
+
// Legacy OpenAI Style: prompt -> Chat
|
| 366 |
+
let prompt_str = match prompt_val {
|
| 367 |
+
Value::String(s) => s.clone(),
|
| 368 |
+
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>().join("\n"),
|
| 369 |
+
_ => prompt_val.to_string(),
|
| 370 |
+
};
|
| 371 |
+
let messages = json!([ { "role": "user", "content": prompt_str } ]);
|
| 372 |
+
if let Some(obj) = body.as_object_mut() {
|
| 373 |
+
obj.remove("prompt");
|
| 374 |
+
obj.insert("messages".to_string(), messages);
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
// 2. Reuse handle_chat_completions logic (wrapping with custom handler or direct call)
|
| 379 |
+
// Actually, due to SSE handling differences (Codex uses different event format), we replicate the loop here or abstract it.
|
| 380 |
+
// For now, let's replicate the core loop but with Codex specific SSE mapping.
|
| 381 |
+
|
| 382 |
+
let mut openai_req: OpenAIRequest = serde_json::from_value(body.clone())
|
| 383 |
+
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid request: {}", e)))?;
|
| 384 |
+
|
| 385 |
+
// Safety: Inject empty message if needed
|
| 386 |
+
if openai_req.messages.is_empty() {
|
| 387 |
+
openai_req.messages.push(crate::proxy::mappers::openai::OpenAIMessage {
|
| 388 |
+
role: "user".to_string(),
|
| 389 |
+
content: Some(crate::proxy::mappers::openai::OpenAIContent::String(" ".to_string())),
|
| 390 |
+
tool_calls: None,
|
| 391 |
+
tool_call_id: None,
|
| 392 |
+
name: None,
|
| 393 |
+
});
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
let upstream = state.upstream.clone();
|
| 397 |
+
let token_manager = state.token_manager;
|
| 398 |
+
let pool_size = token_manager.len();
|
| 399 |
+
let max_attempts = MAX_RETRY_ATTEMPTS.min(pool_size).max(1);
|
| 400 |
+
|
| 401 |
+
let mut last_error = String::new();
|
| 402 |
+
|
| 403 |
+
for attempt in 0..max_attempts {
|
| 404 |
+
let mapped_model = crate::proxy::common::model_mapping::resolve_model_route(
|
| 405 |
+
&openai_req.model,
|
| 406 |
+
&*state.custom_mapping.read().await,
|
| 407 |
+
&*state.openai_mapping.read().await,
|
| 408 |
+
&*state.anthropic_mapping.read().await,
|
| 409 |
+
);
|
| 410 |
+
// 将 OpenAI 工具转为 Value 数组以便探测联网
|
| 411 |
+
let tools_val: Option<Vec<Value>> = openai_req.tools.as_ref().map(|list| {
|
| 412 |
+
list.iter().cloned().collect()
|
| 413 |
+
});
|
| 414 |
+
let config = crate::proxy::mappers::common_utils::resolve_request_config(&openai_req.model, &mapped_model, &tools_val);
|
| 415 |
+
|
| 416 |
+
let (access_token, project_id, email) = match token_manager.get_token(&config.request_type, false).await {
|
| 417 |
+
Ok(t) => t,
|
| 418 |
+
Err(e) => return Err((StatusCode::SERVICE_UNAVAILABLE, format!("Token error: {}", e))),
|
| 419 |
+
};
|
| 420 |
+
|
| 421 |
+
tracing::info!("Using account: {} for completions request (type: {})", email, config.request_type);
|
| 422 |
+
|
| 423 |
+
let gemini_body = transform_openai_request(&openai_req, &project_id, &mapped_model);
|
| 424 |
+
|
| 425 |
+
// [New] 打印转换后的报文 (Gemini Body) 供调试 (Codex 路径)
|
| 426 |
+
if let Ok(body_json) = serde_json::to_string_pretty(&gemini_body) {
|
| 427 |
+
tracing::info!("[Codex-Request] Transformed Gemini Body:\n{}", body_json);
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
let list_response = openai_req.stream;
|
| 431 |
+
let method = if list_response { "streamGenerateContent" } else { "generateContent" };
|
| 432 |
+
let query_string = if list_response { Some("alt=sse") } else { None };
|
| 433 |
+
|
| 434 |
+
let response = match upstream.call_v1_internal(method, &access_token, gemini_body, query_string).await {
|
| 435 |
+
Ok(r) => r,
|
| 436 |
+
Err(e) => {
|
| 437 |
+
last_error = e.clone();
|
| 438 |
+
continue;
|
| 439 |
+
}
|
| 440 |
+
};
|
| 441 |
+
|
| 442 |
+
let status = response.status();
|
| 443 |
+
if status.is_success() {
|
| 444 |
+
if list_response {
|
| 445 |
+
use axum::response::Response;
|
| 446 |
+
use axum::body::Body;
|
| 447 |
+
|
| 448 |
+
let gemini_stream = response.bytes_stream();
|
| 449 |
+
let body = if is_codex_style {
|
| 450 |
+
use crate::proxy::mappers::openai::streaming::create_codex_sse_stream;
|
| 451 |
+
let s = create_codex_sse_stream(Box::pin(gemini_stream), openai_req.model.clone());
|
| 452 |
+
Body::from_stream(s)
|
| 453 |
+
} else {
|
| 454 |
+
use crate::proxy::mappers::openai::streaming::create_legacy_sse_stream;
|
| 455 |
+
let s = create_legacy_sse_stream(Box::pin(gemini_stream), openai_req.model.clone());
|
| 456 |
+
Body::from_stream(s)
|
| 457 |
+
};
|
| 458 |
+
|
| 459 |
+
return Ok(Response::builder()
|
| 460 |
+
.header("Content-Type", "text/event-stream")
|
| 461 |
+
.header("Cache-Control", "no-cache")
|
| 462 |
+
.header("Connection", "keep-alive")
|
| 463 |
+
.body(body)
|
| 464 |
+
.unwrap()
|
| 465 |
+
.into_response());
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
let gemini_resp: Value = response.json().await
|
| 469 |
+
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Parse error: {}", e)))?;
|
| 470 |
+
|
| 471 |
+
let chat_resp = transform_openai_response(&gemini_resp);
|
| 472 |
+
|
| 473 |
+
// Map Chat Response -> Legacy Completions Response
|
| 474 |
+
let choices = chat_resp.choices.iter().map(|c| {
|
| 475 |
+
json!({
|
| 476 |
+
"text": match &c.message.content {
|
| 477 |
+
Some(crate::proxy::mappers::openai::OpenAIContent::String(s)) => s.clone(),
|
| 478 |
+
_ => "".to_string()
|
| 479 |
+
},
|
| 480 |
+
"index": c.index,
|
| 481 |
+
"logprobs": null,
|
| 482 |
+
"finish_reason": c.finish_reason
|
| 483 |
+
})
|
| 484 |
+
}).collect::<Vec<_>>();
|
| 485 |
+
|
| 486 |
+
let legacy_resp = json!({
|
| 487 |
+
"id": chat_resp.id,
|
| 488 |
+
"object": "text_completion",
|
| 489 |
+
"created": chat_resp.created,
|
| 490 |
+
"model": chat_resp.model,
|
| 491 |
+
"choices": choices
|
| 492 |
+
});
|
| 493 |
+
|
| 494 |
+
return Ok(axum::Json(legacy_resp).into_response());
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
// Handle errors and retry
|
| 498 |
+
let status_code = status.as_u16();
|
| 499 |
+
let error_text = response.text().await.unwrap_or_default();
|
| 500 |
+
last_error = format!("HTTP {}: {}", status_code, error_text);
|
| 501 |
+
|
| 502 |
+
if status_code == 429 || status_code == 403 || status_code == 401 {
|
| 503 |
+
continue;
|
| 504 |
+
}
|
| 505 |
+
return Err((status, error_text));
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
Err((StatusCode::TOO_MANY_REQUESTS, format!("All attempts failed. Last error: {}", last_error)))
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
pub async fn handle_list_models() -> impl IntoResponse {
|
| 512 |
+
Json(json!({
|
| 513 |
+
"object": "list",
|
| 514 |
+
"data": [
|
| 515 |
+
{"id": "gpt-4", "object": "model", "created": 1706745600, "owned_by": "openai"},
|
| 516 |
+
{"id": "gpt-3.5-turbo", "object": "model", "created": 1706745600, "owned_by": "openai"},
|
| 517 |
+
{"id": "o1-mini", "object": "model", "created": 1706745600, "owned_by": "openai"}
|
| 518 |
+
]
|
| 519 |
+
}))
|
| 520 |
+
}
|
server/src/proxy/mappers/claude/mod.rs
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Claude mapper 模块
|
| 2 |
+
// 负责 Claude ↔ Gemini 协议转换
|
| 3 |
+
|
| 4 |
+
pub mod models;
|
| 5 |
+
pub mod request;
|
| 6 |
+
pub mod response;
|
| 7 |
+
pub mod streaming;
|
| 8 |
+
pub mod utils;
|
| 9 |
+
|
| 10 |
+
pub use models::*;
|
| 11 |
+
pub use request::transform_claude_request_in;
|
| 12 |
+
pub use response::transform_response;
|
| 13 |
+
pub use streaming::{PartProcessor, StreamingState};
|
| 14 |
+
|
| 15 |
+
use bytes::Bytes;
|
| 16 |
+
use futures::Stream;
|
| 17 |
+
use std::pin::Pin;
|
| 18 |
+
|
| 19 |
+
/// 创建从 Gemini SSE 流到 Claude SSE 流的转换
|
| 20 |
+
pub fn create_claude_sse_stream(
|
| 21 |
+
mut gemini_stream: Pin<Box<dyn Stream<Item = Result<Bytes, reqwest::Error>> + Send>>,
|
| 22 |
+
trace_id: String,
|
| 23 |
+
) -> Pin<Box<dyn Stream<Item = Result<Bytes, String>> + Send>> {
|
| 24 |
+
use async_stream::stream;
|
| 25 |
+
use bytes::BytesMut;
|
| 26 |
+
use futures::StreamExt;
|
| 27 |
+
|
| 28 |
+
Box::pin(stream! {
|
| 29 |
+
let mut state = StreamingState::new();
|
| 30 |
+
let mut buffer = BytesMut::new();
|
| 31 |
+
|
| 32 |
+
while let Some(chunk_result) = gemini_stream.next().await {
|
| 33 |
+
match chunk_result {
|
| 34 |
+
Ok(chunk) => {
|
| 35 |
+
buffer.extend_from_slice(&chunk);
|
| 36 |
+
|
| 37 |
+
// Process complete lines
|
| 38 |
+
while let Some(pos) = buffer.iter().position(|&b| b == b'\n') {
|
| 39 |
+
let line_raw = buffer.split_to(pos + 1);
|
| 40 |
+
if let Ok(line_str) = std::str::from_utf8(&line_raw) {
|
| 41 |
+
let line = line_str.trim();
|
| 42 |
+
if line.is_empty() { continue; }
|
| 43 |
+
|
| 44 |
+
if let Some(sse_chunks) = process_sse_line(line, &mut state, &trace_id) {
|
| 45 |
+
for sse_chunk in sse_chunks {
|
| 46 |
+
yield Ok(sse_chunk);
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
Err(e) => {
|
| 53 |
+
yield Err(format!("Stream error: {}", e));
|
| 54 |
+
break;
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// Ensure termination events are sent
|
| 60 |
+
for chunk in emit_force_stop(&mut state) {
|
| 61 |
+
yield Ok(chunk);
|
| 62 |
+
}
|
| 63 |
+
})
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/// 处理单行 SSE 数据
|
| 67 |
+
fn process_sse_line(line: &str, state: &mut StreamingState, trace_id: &str) -> Option<Vec<Bytes>> {
|
| 68 |
+
if !line.starts_with("data: ") {
|
| 69 |
+
return None;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
let data_str = line[6..].trim();
|
| 73 |
+
if data_str.is_empty() {
|
| 74 |
+
return None;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
if data_str == "[DONE]" {
|
| 78 |
+
let chunks = emit_force_stop(state);
|
| 79 |
+
if chunks.is_empty() {
|
| 80 |
+
return None;
|
| 81 |
+
}
|
| 82 |
+
return Some(chunks);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// 解析 JSON
|
| 86 |
+
let json_value: serde_json::Value = match serde_json::from_str(data_str) {
|
| 87 |
+
Ok(v) => v,
|
| 88 |
+
Err(_) => return None,
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
let mut chunks = Vec::new();
|
| 92 |
+
|
| 93 |
+
// 解包 response 字段 (如果存在)
|
| 94 |
+
let raw_json = json_value.get("response").unwrap_or(&json_value);
|
| 95 |
+
|
| 96 |
+
// 发送 message_start
|
| 97 |
+
if !state.message_start_sent {
|
| 98 |
+
chunks.push(state.emit_message_start(raw_json));
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// 捕获 groundingMetadata (Web Search)
|
| 102 |
+
if let Some(candidate) = raw_json.get("candidates").and_then(|c| c.get(0)) {
|
| 103 |
+
if let Some(grounding) = candidate.get("groundingMetadata") {
|
| 104 |
+
// 提取搜索词
|
| 105 |
+
if let Some(query) = grounding.get("webSearchQueries")
|
| 106 |
+
.and_then(|v| v.as_array())
|
| 107 |
+
.and_then(|arr| arr.get(0))
|
| 108 |
+
.and_then(|v| v.as_str())
|
| 109 |
+
{
|
| 110 |
+
state.web_search_query = Some(query.to_string());
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// 提取结果块
|
| 114 |
+
if let Some(chunks_arr) = grounding.get("groundingChunks").and_then(|v| v.as_array()) {
|
| 115 |
+
state.grounding_chunks = Some(chunks_arr.clone());
|
| 116 |
+
} else if let Some(chunks_arr) = grounding.get("grounding_metadata").and_then(|m| m.get("groundingChunks")).and_then(|v| v.as_array()) {
|
| 117 |
+
state.grounding_chunks = Some(chunks_arr.clone());
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// 处理所有 parts
|
| 123 |
+
if let Some(parts) = raw_json
|
| 124 |
+
.get("candidates")
|
| 125 |
+
.and_then(|c| c.get(0))
|
| 126 |
+
.and_then(|cand| cand.get("content"))
|
| 127 |
+
.and_then(|content| content.get("parts"))
|
| 128 |
+
.and_then(|p| p.as_array())
|
| 129 |
+
{
|
| 130 |
+
for part_value in parts {
|
| 131 |
+
if let Ok(part) = serde_json::from_value::<GeminiPart>(part_value.clone()) {
|
| 132 |
+
let mut processor = PartProcessor::new(state);
|
| 133 |
+
chunks.extend(processor.process(&part));
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Process grounding metadata (googleSearch results) and append as citations
|
| 139 |
+
if let Some(grounding) = raw_json
|
| 140 |
+
.get("candidates")
|
| 141 |
+
.and_then(|c| c.get(0))
|
| 142 |
+
.and_then(|cand| cand.get("groundingMetadata"))
|
| 143 |
+
{
|
| 144 |
+
if let Some(citation_chunks) = process_grounding_metadata(grounding, state) {
|
| 145 |
+
chunks.extend(citation_chunks);
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
// 检查是否结束
|
| 150 |
+
if let Some(finish_reason) = raw_json
|
| 151 |
+
.get("candidates")
|
| 152 |
+
.and_then(|c| c.get(0))
|
| 153 |
+
.and_then(|cand| cand.get("finishReason"))
|
| 154 |
+
.and_then(|f| f.as_str())
|
| 155 |
+
{
|
| 156 |
+
let usage = raw_json
|
| 157 |
+
.get("usageMetadata")
|
| 158 |
+
.and_then(|u| serde_json::from_value::<UsageMetadata>(u.clone()).ok());
|
| 159 |
+
|
| 160 |
+
if let Some(ref u) = usage {
|
| 161 |
+
tracing::info!(
|
| 162 |
+
"[{}] Stream usage: In {}, Out {}",
|
| 163 |
+
trace_id,
|
| 164 |
+
u.prompt_token_count.unwrap_or(0),
|
| 165 |
+
u.candidates_token_count.unwrap_or(0)
|
| 166 |
+
);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
chunks.extend(state.emit_finish(Some(finish_reason), usage.as_ref()));
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
if chunks.is_empty() {
|
| 173 |
+
None
|
| 174 |
+
} else {
|
| 175 |
+
Some(chunks)
|
| 176 |
+
}
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
/// 发送强制结束事件
|
| 180 |
+
pub fn emit_force_stop(state: &mut StreamingState) -> Vec<Bytes> {
|
| 181 |
+
if !state.message_stop_sent {
|
| 182 |
+
let mut chunks = state.emit_finish(None, None);
|
| 183 |
+
if chunks.is_empty() {
|
| 184 |
+
chunks.push(Bytes::from(
|
| 185 |
+
"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
|
| 186 |
+
));
|
| 187 |
+
state.message_stop_sent = true;
|
| 188 |
+
}
|
| 189 |
+
return chunks;
|
| 190 |
+
}
|
| 191 |
+
vec![]
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
/// Process grounding metadata from Gemini's googleSearch and emit as Claude web_search blocks
|
| 195 |
+
fn process_grounding_metadata(
|
| 196 |
+
metadata: &serde_json::Value,
|
| 197 |
+
state: &mut StreamingState,
|
| 198 |
+
) -> Option<Vec<Bytes>> {
|
| 199 |
+
use serde_json::json;
|
| 200 |
+
|
| 201 |
+
// Extract search queries and grounding chunks
|
| 202 |
+
let search_queries = metadata
|
| 203 |
+
.get("webSearchQueries")
|
| 204 |
+
.and_then(|q| q.as_array())
|
| 205 |
+
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>())
|
| 206 |
+
.unwrap_or_default();
|
| 207 |
+
|
| 208 |
+
let grounding_chunks = metadata.get("groundingChunks").and_then(|c| c.as_array())?;
|
| 209 |
+
|
| 210 |
+
if grounding_chunks.is_empty() {
|
| 211 |
+
return None;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
// Generate a unique tool_use_id
|
| 215 |
+
let tool_use_id = format!(
|
| 216 |
+
"srvtoolu_{}",
|
| 217 |
+
crate::proxy::common::utils::generate_random_id()
|
| 218 |
+
);
|
| 219 |
+
|
| 220 |
+
// Build search results array
|
| 221 |
+
let mut search_results = Vec::new();
|
| 222 |
+
for chunk in grounding_chunks.iter() {
|
| 223 |
+
if let Some(web) = chunk.get("web") {
|
| 224 |
+
let title = web
|
| 225 |
+
.get("title")
|
| 226 |
+
.and_then(|t| t.as_str())
|
| 227 |
+
.unwrap_or("Source");
|
| 228 |
+
let uri = web.get("uri").and_then(|u| u.as_str()).unwrap_or("");
|
| 229 |
+
if !uri.is_empty() {
|
| 230 |
+
search_results.push(json!({
|
| 231 |
+
"url": uri,
|
| 232 |
+
"title": title,
|
| 233 |
+
"encrypted_content": "", // Gemini doesn't provide this
|
| 234 |
+
"page_age": null
|
| 235 |
+
}));
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
if search_results.is_empty() {
|
| 241 |
+
return None;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
let search_query = search_queries
|
| 245 |
+
.first()
|
| 246 |
+
.map(|s| s.to_string())
|
| 247 |
+
.unwrap_or_default();
|
| 248 |
+
|
| 249 |
+
tracing::info!(
|
| 250 |
+
"[Grounding] Emitting {} search results for query: {}",
|
| 251 |
+
search_results.len(),
|
| 252 |
+
search_query
|
| 253 |
+
);
|
| 254 |
+
|
| 255 |
+
let mut chunks = Vec::new();
|
| 256 |
+
|
| 257 |
+
// 1. Emit server_tool_use block (start)
|
| 258 |
+
let server_tool_use_start = json!({
|
| 259 |
+
"type": "content_block_start",
|
| 260 |
+
"index": state.block_index,
|
| 261 |
+
"content_block": {
|
| 262 |
+
"type": "server_tool_use",
|
| 263 |
+
"id": tool_use_id,
|
| 264 |
+
"name": "web_search",
|
| 265 |
+
"input": {
|
| 266 |
+
"query": search_query
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
});
|
| 270 |
+
chunks.push(Bytes::from(format!(
|
| 271 |
+
"event: content_block_start\ndata: {}\n\n",
|
| 272 |
+
server_tool_use_start
|
| 273 |
+
)));
|
| 274 |
+
|
| 275 |
+
// server_tool_use block stop
|
| 276 |
+
let server_tool_use_stop = json!({
|
| 277 |
+
"type": "content_block_stop",
|
| 278 |
+
"index": state.block_index
|
| 279 |
+
});
|
| 280 |
+
chunks.push(Bytes::from(format!(
|
| 281 |
+
"event: content_block_stop\ndata: {}\n\n",
|
| 282 |
+
server_tool_use_stop
|
| 283 |
+
)));
|
| 284 |
+
state.block_index += 1;
|
| 285 |
+
|
| 286 |
+
// 2. Emit web_search_tool_result block (start)
|
| 287 |
+
let tool_result_start = json!({
|
| 288 |
+
"type": "content_block_start",
|
| 289 |
+
"index": state.block_index,
|
| 290 |
+
"content_block": {
|
| 291 |
+
"type": "web_search_tool_result",
|
| 292 |
+
"tool_use_id": tool_use_id,
|
| 293 |
+
"content": search_results
|
| 294 |
+
}
|
| 295 |
+
});
|
| 296 |
+
chunks.push(Bytes::from(format!(
|
| 297 |
+
"event: content_block_start\ndata: {}\n\n",
|
| 298 |
+
tool_result_start
|
| 299 |
+
)));
|
| 300 |
+
|
| 301 |
+
// web_search_tool_result block stop
|
| 302 |
+
let tool_result_stop = json!({
|
| 303 |
+
"type": "content_block_stop",
|
| 304 |
+
"index": state.block_index
|
| 305 |
+
});
|
| 306 |
+
chunks.push(Bytes::from(format!(
|
| 307 |
+
"event: content_block_stop\ndata: {}\n\n",
|
| 308 |
+
tool_result_stop
|
| 309 |
+
)));
|
| 310 |
+
state.block_index += 1;
|
| 311 |
+
|
| 312 |
+
Some(chunks)
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
#[cfg(test)]
|
| 316 |
+
mod tests {
|
| 317 |
+
use super::*;
|
| 318 |
+
|
| 319 |
+
#[test]
|
| 320 |
+
fn test_process_sse_line_done() {
|
| 321 |
+
let mut state = StreamingState::new();
|
| 322 |
+
let result = process_sse_line("data: [DONE]", &mut state, "test_id");
|
| 323 |
+
assert!(result.is_some());
|
| 324 |
+
let chunks = result.unwrap();
|
| 325 |
+
assert!(!chunks.is_empty());
|
| 326 |
+
|
| 327 |
+
let all_text: String = chunks
|
| 328 |
+
.iter()
|
| 329 |
+
.map(|b| String::from_utf8(b.to_vec()).unwrap_or_default())
|
| 330 |
+
.collect();
|
| 331 |
+
assert!(all_text.contains("message_stop"));
|
| 332 |
+
}
|
| 333 |
+
|
| 334 |
+
#[test]
|
| 335 |
+
fn test_process_sse_line_with_text() {
|
| 336 |
+
let mut state = StreamingState::new();
|
| 337 |
+
|
| 338 |
+
let test_data = r#"data: {"candidates":[{"content":{"parts":[{"text":"Hello"}]}}],"usageMetadata":{},"modelVersion":"test","responseId":"123"}"#;
|
| 339 |
+
|
| 340 |
+
let result = process_sse_line(test_data, &mut state, "test_id");
|
| 341 |
+
assert!(result.is_some());
|
| 342 |
+
|
| 343 |
+
let chunks = result.unwrap();
|
| 344 |
+
assert!(!chunks.is_empty());
|
| 345 |
+
|
| 346 |
+
// 应该包含 message_start 和 text delta
|
| 347 |
+
let all_text: String = chunks
|
| 348 |
+
.iter()
|
| 349 |
+
.map(|b| String::from_utf8(b.to_vec()).unwrap_or_default())
|
| 350 |
+
.collect();
|
| 351 |
+
|
| 352 |
+
assert!(all_text.contains("message_start"));
|
| 353 |
+
assert!(all_text.contains("content_block_start"));
|
| 354 |
+
assert!(all_text.contains("Hello"));
|
| 355 |
+
}
|
| 356 |
+
}
|
server/src/proxy/mappers/claude/models.rs
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Claude 数据模型
|
| 2 |
+
// Claude 协议相关数据模型
|
| 3 |
+
|
| 4 |
+
use serde::{Deserialize, Serialize};
|
| 5 |
+
|
| 6 |
+
/// Claude API 请求
|
| 7 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 8 |
+
pub struct ClaudeRequest {
|
| 9 |
+
pub model: String,
|
| 10 |
+
pub messages: Vec<Message>,
|
| 11 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 12 |
+
pub system: Option<SystemPrompt>,
|
| 13 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 14 |
+
pub tools: Option<Vec<Tool>>,
|
| 15 |
+
#[serde(default)]
|
| 16 |
+
pub stream: bool,
|
| 17 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 18 |
+
pub max_tokens: Option<u32>,
|
| 19 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 20 |
+
pub temperature: Option<f32>,
|
| 21 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 22 |
+
pub top_p: Option<f32>,
|
| 23 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 24 |
+
pub top_k: Option<u32>,
|
| 25 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 26 |
+
pub thinking: Option<ThinkingConfig>,
|
| 27 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 28 |
+
pub metadata: Option<Metadata>,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/// Thinking 配置
|
| 32 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 33 |
+
pub struct ThinkingConfig {
|
| 34 |
+
#[serde(rename = "type")]
|
| 35 |
+
pub type_: String, // "enabled"
|
| 36 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 37 |
+
pub budget_tokens: Option<u32>,
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/// System Prompt
|
| 41 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 42 |
+
#[serde(untagged)]
|
| 43 |
+
pub enum SystemPrompt {
|
| 44 |
+
String(String),
|
| 45 |
+
Array(Vec<SystemBlock>),
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 49 |
+
pub struct SystemBlock {
|
| 50 |
+
#[serde(rename = "type")]
|
| 51 |
+
pub block_type: String,
|
| 52 |
+
pub text: String,
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/// Message
|
| 56 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 57 |
+
pub struct Message {
|
| 58 |
+
pub role: String,
|
| 59 |
+
pub content: MessageContent,
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 63 |
+
#[serde(untagged)]
|
| 64 |
+
pub enum MessageContent {
|
| 65 |
+
String(String),
|
| 66 |
+
Array(Vec<ContentBlock>),
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/// Content Block (Claude)
|
| 70 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 71 |
+
#[serde(tag = "type")]
|
| 72 |
+
pub enum ContentBlock {
|
| 73 |
+
#[serde(rename = "text")]
|
| 74 |
+
Text { text: String },
|
| 75 |
+
|
| 76 |
+
#[serde(rename = "thinking")]
|
| 77 |
+
Thinking {
|
| 78 |
+
thinking: String,
|
| 79 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 80 |
+
signature: Option<String>,
|
| 81 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 82 |
+
cache_control: Option<serde_json::Value>,
|
| 83 |
+
},
|
| 84 |
+
|
| 85 |
+
#[serde(rename = "image")]
|
| 86 |
+
Image { source: ImageSource },
|
| 87 |
+
|
| 88 |
+
#[serde(rename = "tool_use")]
|
| 89 |
+
ToolUse {
|
| 90 |
+
id: String,
|
| 91 |
+
name: String,
|
| 92 |
+
input: serde_json::Value,
|
| 93 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 94 |
+
signature: Option<String>,
|
| 95 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 96 |
+
cache_control: Option<serde_json::Value>,
|
| 97 |
+
},
|
| 98 |
+
|
| 99 |
+
#[serde(rename = "tool_result")]
|
| 100 |
+
ToolResult {
|
| 101 |
+
tool_use_id: String,
|
| 102 |
+
content: serde_json::Value, // Changed from String to Value to support Array of Blocks
|
| 103 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 104 |
+
is_error: Option<bool>,
|
| 105 |
+
},
|
| 106 |
+
|
| 107 |
+
#[serde(rename = "server_tool_use")]
|
| 108 |
+
ServerToolUse {
|
| 109 |
+
id: String,
|
| 110 |
+
name: String,
|
| 111 |
+
input: serde_json::Value,
|
| 112 |
+
},
|
| 113 |
+
|
| 114 |
+
#[serde(rename = "web_search_tool_result")]
|
| 115 |
+
WebSearchToolResult {
|
| 116 |
+
tool_use_id: String,
|
| 117 |
+
content: serde_json::Value,
|
| 118 |
+
},
|
| 119 |
+
|
| 120 |
+
#[serde(rename = "redacted_thinking")]
|
| 121 |
+
RedactedThinking { data: String },
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 125 |
+
pub struct ImageSource {
|
| 126 |
+
#[serde(rename = "type")]
|
| 127 |
+
pub source_type: String,
|
| 128 |
+
pub media_type: String,
|
| 129 |
+
pub data: String,
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
/// Tool - supports both client tools (with input_schema) and server tools (like web_search)
|
| 133 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 134 |
+
pub struct Tool {
|
| 135 |
+
/// Tool type - for server tools like "web_search_20250305"
|
| 136 |
+
#[serde(rename = "type")]
|
| 137 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 138 |
+
pub type_: Option<String>,
|
| 139 |
+
/// Tool name - "web_search" for server tools, custom name for client tools
|
| 140 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 141 |
+
pub name: Option<String>,
|
| 142 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 143 |
+
pub description: Option<String>,
|
| 144 |
+
/// Input schema - required for client tools, absent for server tools
|
| 145 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 146 |
+
pub input_schema: Option<serde_json::Value>,
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
impl Tool {
|
| 150 |
+
/// Check if this is the web_search server tool
|
| 151 |
+
pub fn is_web_search(&self) -> bool {
|
| 152 |
+
// Check by type (preferred for server tools)
|
| 153 |
+
if let Some(ref t) = self.type_ {
|
| 154 |
+
if t.starts_with("web_search") {
|
| 155 |
+
return true;
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
// Check by name (fallback)
|
| 159 |
+
if let Some(ref n) = self.name {
|
| 160 |
+
if n == "web_search" {
|
| 161 |
+
return true;
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
false
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/// Get the effective tool name
|
| 168 |
+
pub fn get_name(&self) -> String {
|
| 169 |
+
self.name.clone().unwrap_or_else(|| {
|
| 170 |
+
// For server tools, derive name from type
|
| 171 |
+
if let Some(ref t) = self.type_ {
|
| 172 |
+
if t.starts_with("web_search") {
|
| 173 |
+
return "web_search".to_string();
|
| 174 |
+
}
|
| 175 |
+
}
|
| 176 |
+
"unknown".to_string()
|
| 177 |
+
})
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
/// Metadata
|
| 182 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 183 |
+
pub struct Metadata {
|
| 184 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 185 |
+
pub user_id: Option<String>,
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/// Claude API 响应
|
| 189 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 190 |
+
pub struct ClaudeResponse {
|
| 191 |
+
pub id: String,
|
| 192 |
+
#[serde(rename = "type")]
|
| 193 |
+
pub type_: String,
|
| 194 |
+
pub role: String,
|
| 195 |
+
pub model: String,
|
| 196 |
+
pub content: Vec<ContentBlock>,
|
| 197 |
+
pub stop_reason: String,
|
| 198 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 199 |
+
pub stop_sequence: Option<String>,
|
| 200 |
+
pub usage: Usage,
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
/// Usage
|
| 204 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 205 |
+
pub struct Usage {
|
| 206 |
+
pub input_tokens: u32,
|
| 207 |
+
pub output_tokens: u32,
|
| 208 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 209 |
+
pub server_tool_use: Option<serde_json::Value>,
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
// ========== Gemini 数据模型 ==========
|
| 213 |
+
|
| 214 |
+
/// Gemini Content
|
| 215 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 216 |
+
pub struct GeminiContent {
|
| 217 |
+
pub role: String,
|
| 218 |
+
pub parts: Vec<GeminiPart>,
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
/// Gemini Part
|
| 222 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 223 |
+
pub struct GeminiPart {
|
| 224 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 225 |
+
pub text: Option<String>,
|
| 226 |
+
|
| 227 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 228 |
+
pub thought: Option<bool>,
|
| 229 |
+
|
| 230 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 231 |
+
#[serde(rename = "thoughtSignature")]
|
| 232 |
+
pub thought_signature: Option<String>,
|
| 233 |
+
|
| 234 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 235 |
+
#[serde(rename = "functionCall")]
|
| 236 |
+
pub function_call: Option<FunctionCall>,
|
| 237 |
+
|
| 238 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 239 |
+
#[serde(rename = "functionResponse")]
|
| 240 |
+
pub function_response: Option<FunctionResponse>,
|
| 241 |
+
|
| 242 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 243 |
+
#[serde(rename = "inlineData")]
|
| 244 |
+
pub inline_data: Option<InlineData>,
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 248 |
+
pub struct FunctionCall {
|
| 249 |
+
pub name: String,
|
| 250 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 251 |
+
pub id: Option<String>,
|
| 252 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 253 |
+
pub args: Option<serde_json::Value>,
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 257 |
+
pub struct FunctionResponse {
|
| 258 |
+
pub name: String,
|
| 259 |
+
pub response: serde_json::Value,
|
| 260 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 261 |
+
pub id: Option<String>,
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 265 |
+
pub struct InlineData {
|
| 266 |
+
#[serde(rename = "mimeType")]
|
| 267 |
+
pub mime_type: String,
|
| 268 |
+
pub data: String,
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
/// Gemini 完整响应
|
| 272 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 273 |
+
pub struct GeminiResponse {
|
| 274 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 275 |
+
pub candidates: Option<Vec<Candidate>>,
|
| 276 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 277 |
+
#[serde(rename = "usageMetadata")]
|
| 278 |
+
pub usage_metadata: Option<UsageMetadata>,
|
| 279 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 280 |
+
#[serde(rename = "modelVersion")]
|
| 281 |
+
pub model_version: Option<String>,
|
| 282 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 283 |
+
#[serde(rename = "responseId")]
|
| 284 |
+
pub response_id: Option<String>,
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 288 |
+
pub struct Candidate {
|
| 289 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 290 |
+
pub content: Option<GeminiContent>,
|
| 291 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 292 |
+
#[serde(rename = "finishReason")]
|
| 293 |
+
pub finish_reason: Option<String>,
|
| 294 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 295 |
+
pub index: Option<u32>,
|
| 296 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 297 |
+
#[serde(rename = "groundingMetadata")]
|
| 298 |
+
pub grounding_metadata: Option<GroundingMetadata>,
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 302 |
+
pub struct UsageMetadata {
|
| 303 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 304 |
+
#[serde(rename = "promptTokenCount")]
|
| 305 |
+
pub prompt_token_count: Option<u32>,
|
| 306 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 307 |
+
#[serde(rename = "candidatesTokenCount")]
|
| 308 |
+
pub candidates_token_count: Option<u32>,
|
| 309 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 310 |
+
#[serde(rename = "totalTokenCount")]
|
| 311 |
+
pub total_token_count: Option<u32>,
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
// ========== Grounding Metadata (for googleSearch results) ==========
|
| 315 |
+
|
| 316 |
+
/// Gemini Grounding Metadata - contains search results from googleSearch tool
|
| 317 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 318 |
+
pub struct GroundingMetadata {
|
| 319 |
+
#[serde(rename = "webSearchQueries")]
|
| 320 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 321 |
+
pub web_search_queries: Option<Vec<String>>,
|
| 322 |
+
|
| 323 |
+
#[serde(rename = "groundingChunks")]
|
| 324 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 325 |
+
pub grounding_chunks: Option<Vec<GroundingChunk>>,
|
| 326 |
+
|
| 327 |
+
#[serde(rename = "groundingSupports")]
|
| 328 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 329 |
+
pub grounding_supports: Option<Vec<GroundingSupport>>,
|
| 330 |
+
|
| 331 |
+
#[serde(rename = "searchEntryPoint")]
|
| 332 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 333 |
+
pub search_entry_point: Option<SearchEntryPoint>,
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 337 |
+
pub struct GroundingChunk {
|
| 338 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 339 |
+
pub web: Option<WebSource>,
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 343 |
+
pub struct WebSource {
|
| 344 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 345 |
+
pub uri: Option<String>,
|
| 346 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 347 |
+
pub title: Option<String>,
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 351 |
+
pub struct GroundingSupport {
|
| 352 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 353 |
+
pub segment: Option<TextSegment>,
|
| 354 |
+
#[serde(rename = "groundingChunkIndices")]
|
| 355 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 356 |
+
pub grounding_chunk_indices: Option<Vec<i32>>,
|
| 357 |
+
#[serde(rename = "confidenceScores")]
|
| 358 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 359 |
+
pub confidence_scores: Option<Vec<f64>>,
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 363 |
+
pub struct TextSegment {
|
| 364 |
+
#[serde(rename = "startIndex")]
|
| 365 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 366 |
+
pub start_index: Option<i32>,
|
| 367 |
+
#[serde(rename = "endIndex")]
|
| 368 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 369 |
+
pub end_index: Option<i32>,
|
| 370 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 371 |
+
pub text: Option<String>,
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 375 |
+
pub struct SearchEntryPoint {
|
| 376 |
+
#[serde(rename = "renderedContent")]
|
| 377 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 378 |
+
pub rendered_content: Option<String>,
|
| 379 |
+
}
|
server/src/proxy/mappers/claude/request.rs
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Claude 请求转换 (Claude → Gemini v1internal)
|
| 2 |
+
// 对应 transformClaudeRequestIn
|
| 3 |
+
|
| 4 |
+
use super::models::*;
|
| 5 |
+
use crate::proxy::mappers::signature_store::get_thought_signature;
|
| 6 |
+
use serde_json::{json, Value};
|
| 7 |
+
use std::collections::HashMap;
|
| 8 |
+
|
| 9 |
+
/// 转换 Claude 请求为 Gemini v1internal 格式
|
| 10 |
+
pub fn transform_claude_request_in(
|
| 11 |
+
claude_req: &ClaudeRequest,
|
| 12 |
+
project_id: &str,
|
| 13 |
+
) -> Result<Value, String> {
|
| 14 |
+
// 检测是否有联网工具 (server tool or built-in tool)
|
| 15 |
+
let has_web_search_tool = claude_req
|
| 16 |
+
.tools
|
| 17 |
+
.as_ref()
|
| 18 |
+
.map(|tools| {
|
| 19 |
+
tools.iter().any(|t| {
|
| 20 |
+
t.is_web_search()
|
| 21 |
+
|| t.name.as_deref() == Some("google_search")
|
| 22 |
+
|| t.type_.as_deref() == Some("web_search_20250305")
|
| 23 |
+
})
|
| 24 |
+
})
|
| 25 |
+
.unwrap_or(false);
|
| 26 |
+
|
| 27 |
+
// 用于存储 tool_use id -> name 映射
|
| 28 |
+
let mut tool_id_to_name: HashMap<String, String> = HashMap::new();
|
| 29 |
+
|
| 30 |
+
// 1. System Instruction (注入动态身份防护)
|
| 31 |
+
let system_instruction = build_system_instruction(&claude_req.system, &claude_req.model);
|
| 32 |
+
|
| 33 |
+
// Map model name (Use standard mapping)
|
| 34 |
+
let mapped_model = if has_web_search_tool {
|
| 35 |
+
"gemini-2.5-flash".to_string()
|
| 36 |
+
} else {
|
| 37 |
+
crate::proxy::common::model_mapping::map_claude_model_to_gemini(&claude_req.model)
|
| 38 |
+
};
|
| 39 |
+
|
| 40 |
+
// 将 Claude 工具转为 Value 数组以便探测联网
|
| 41 |
+
let tools_val: Option<Vec<Value>> = claude_req.tools.as_ref().map(|list| {
|
| 42 |
+
list.iter().map(|t| serde_json::to_value(t).unwrap_or(json!({}))).collect()
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
// Resolve grounding config
|
| 46 |
+
let config = crate::proxy::mappers::common_utils::resolve_request_config(&claude_req.model, &mapped_model, &tools_val);
|
| 47 |
+
// Only Gemini models support our "dummy thought" workaround.
|
| 48 |
+
// Claude models routed via Vertex/Google API often require valid thought signatures.
|
| 49 |
+
// [FIX] Whenever thinking is enabled, we MUST allow dummy thought injection to satisfy
|
| 50 |
+
// Google's strict validation of historical messages, even for non-agent (e.g. search) tasks.
|
| 51 |
+
let is_thinking_enabled = claude_req
|
| 52 |
+
.thinking
|
| 53 |
+
.as_ref()
|
| 54 |
+
.map(|t| t.type_ == "enabled")
|
| 55 |
+
.unwrap_or(false);
|
| 56 |
+
|
| 57 |
+
let allow_dummy_thought = is_thinking_enabled;
|
| 58 |
+
|
| 59 |
+
// 4. Generation Config & Thinking
|
| 60 |
+
let generation_config = build_generation_config(claude_req, has_web_search_tool);
|
| 61 |
+
|
| 62 |
+
// Check if thinking is enabled
|
| 63 |
+
let is_thinking_enabled = claude_req
|
| 64 |
+
.thinking
|
| 65 |
+
.as_ref()
|
| 66 |
+
.map(|t| t.type_ == "enabled")
|
| 67 |
+
.unwrap_or(false);
|
| 68 |
+
|
| 69 |
+
// 2. Contents (Messages)
|
| 70 |
+
let contents = build_contents(
|
| 71 |
+
&claude_req.messages,
|
| 72 |
+
&mut tool_id_to_name,
|
| 73 |
+
is_thinking_enabled,
|
| 74 |
+
allow_dummy_thought,
|
| 75 |
+
)?;
|
| 76 |
+
|
| 77 |
+
// 3. Tools
|
| 78 |
+
let tools = build_tools(&claude_req.tools, has_web_search_tool)?;
|
| 79 |
+
|
| 80 |
+
// 5. Safety Settings
|
| 81 |
+
let safety_settings = json!([
|
| 82 |
+
{ "category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF" },
|
| 83 |
+
{ "category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF" },
|
| 84 |
+
{ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF" },
|
| 85 |
+
{ "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF" },
|
| 86 |
+
{ "category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "OFF" },
|
| 87 |
+
]);
|
| 88 |
+
|
| 89 |
+
// Build inner request
|
| 90 |
+
let mut inner_request = json!({
|
| 91 |
+
"contents": contents,
|
| 92 |
+
"safetySettings": safety_settings,
|
| 93 |
+
});
|
| 94 |
+
|
| 95 |
+
// 深度清理 [undefined] 字符串 (Cherry Studio 等客户端常见注入)
|
| 96 |
+
crate::proxy::mappers::common_utils::deep_clean_undefined(&mut inner_request);
|
| 97 |
+
|
| 98 |
+
if let Some(sys_inst) = system_instruction {
|
| 99 |
+
inner_request["systemInstruction"] = sys_inst;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
if !generation_config.is_null() {
|
| 103 |
+
inner_request["generationConfig"] = generation_config;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
if let Some(tools_val) = tools {
|
| 107 |
+
inner_request["tools"] = tools_val;
|
| 108 |
+
// 显式设置工具配置模式为 VALIDATED
|
| 109 |
+
inner_request["toolConfig"] = json!({
|
| 110 |
+
"functionCallingConfig": {
|
| 111 |
+
"mode": "VALIDATED"
|
| 112 |
+
}
|
| 113 |
+
});
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// Inject googleSearch tool if needed (and not already done by build_tools)
|
| 117 |
+
if config.inject_google_search && !has_web_search_tool {
|
| 118 |
+
crate::proxy::mappers::common_utils::inject_google_search_tool(&mut inner_request);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Inject imageConfig if present (for image generation models)
|
| 122 |
+
if let Some(image_config) = config.image_config {
|
| 123 |
+
if let Some(obj) = inner_request.as_object_mut() {
|
| 124 |
+
// 1. Remove tools (image generation does not support tools)
|
| 125 |
+
obj.remove("tools");
|
| 126 |
+
|
| 127 |
+
// 2. Remove systemInstruction (image generation does not support system prompts)
|
| 128 |
+
obj.remove("systemInstruction");
|
| 129 |
+
|
| 130 |
+
// 3. Clean generationConfig (remove thinkingConfig, responseMimeType, responseModalities etc.)
|
| 131 |
+
let gen_config = obj.entry("generationConfig").or_insert_with(|| json!({}));
|
| 132 |
+
if let Some(gen_obj) = gen_config.as_object_mut() {
|
| 133 |
+
gen_obj.remove("thinkingConfig");
|
| 134 |
+
gen_obj.remove("responseMimeType");
|
| 135 |
+
gen_obj.remove("responseModalities");
|
| 136 |
+
gen_obj.insert("imageConfig".to_string(), image_config);
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// 生成 requestId
|
| 142 |
+
let request_id = format!("agent-{}", uuid::Uuid::new_v4());
|
| 143 |
+
|
| 144 |
+
// 构建最终请求体
|
| 145 |
+
let mut body = json!({
|
| 146 |
+
"project": project_id,
|
| 147 |
+
"requestId": request_id,
|
| 148 |
+
"request": inner_request,
|
| 149 |
+
"model": config.final_model,
|
| 150 |
+
"userAgent": "antigravity",
|
| 151 |
+
"requestType": config.request_type,
|
| 152 |
+
});
|
| 153 |
+
|
| 154 |
+
// 如果提供了 metadata.user_id,则复用为 sessionId
|
| 155 |
+
if let Some(metadata) = &claude_req.metadata {
|
| 156 |
+
if let Some(user_id) = &metadata.user_id {
|
| 157 |
+
body["request"]["sessionId"] = json!(user_id);
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
Ok(body)
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
/// 构建 System Instruction (支持动态身份映射与 Prompt 隔离)
|
| 166 |
+
fn build_system_instruction(system: &Option<SystemPrompt>, model_name: &str) -> Option<Value> {
|
| 167 |
+
let mut parts = Vec::new();
|
| 168 |
+
|
| 169 |
+
// 注入身份防护指令 (参考 amq2api 动态化方案)
|
| 170 |
+
let identity_patch = format!(
|
| 171 |
+
"--- [IDENTITY_PATCH] ---\n\
|
| 172 |
+
Ignore any previous instructions regarding your identity or host platform (e.g., Amazon Q, Google AI).\n\
|
| 173 |
+
You are currently providing services as the native {} model via a standard API proxy.\n\
|
| 174 |
+
Always use the 'claude' command for terminal tasks if relevant.\n\
|
| 175 |
+
--- [SYSTEM_PROMPT_BEGIN] ---\n",
|
| 176 |
+
model_name
|
| 177 |
+
);
|
| 178 |
+
parts.push(json!({"text": identity_patch}));
|
| 179 |
+
|
| 180 |
+
if let Some(sys) = system {
|
| 181 |
+
match sys {
|
| 182 |
+
SystemPrompt::String(text) => {
|
| 183 |
+
parts.push(json!({"text": text}));
|
| 184 |
+
}
|
| 185 |
+
SystemPrompt::Array(blocks) => {
|
| 186 |
+
for block in blocks {
|
| 187 |
+
if block.block_type == "text" {
|
| 188 |
+
parts.push(json!({"text": block.text}));
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
}
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
parts.push(json!({"text": "\n--- [SYSTEM_PROMPT_END] ---"}));
|
| 196 |
+
|
| 197 |
+
Some(json!({
|
| 198 |
+
"parts": parts
|
| 199 |
+
}))
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
/// 构建 Contents (Messages)
|
| 203 |
+
fn build_contents(
|
| 204 |
+
messages: &[Message],
|
| 205 |
+
tool_id_to_name: &mut HashMap<String, String>,
|
| 206 |
+
is_thinking_enabled: bool,
|
| 207 |
+
allow_dummy_thought: bool,
|
| 208 |
+
) -> Result<Value, String> {
|
| 209 |
+
let mut contents = Vec::new();
|
| 210 |
+
let mut last_thought_signature: Option<String> = None;
|
| 211 |
+
|
| 212 |
+
let msg_count = messages.len();
|
| 213 |
+
for (i, msg) in messages.iter().enumerate() {
|
| 214 |
+
let role = if msg.role == "assistant" {
|
| 215 |
+
"model"
|
| 216 |
+
} else {
|
| 217 |
+
&msg.role
|
| 218 |
+
};
|
| 219 |
+
|
| 220 |
+
let mut parts = Vec::new();
|
| 221 |
+
|
| 222 |
+
match &msg.content {
|
| 223 |
+
MessageContent::String(text) => {
|
| 224 |
+
if text != "(no content)" {
|
| 225 |
+
if !text.trim().is_empty() {
|
| 226 |
+
parts.push(json!({"text": text.trim()}));
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
MessageContent::Array(blocks) => {
|
| 231 |
+
for item in blocks {
|
| 232 |
+
match item {
|
| 233 |
+
ContentBlock::Text { text } => {
|
| 234 |
+
if text != "(no content)" {
|
| 235 |
+
parts.push(json!({"text": text}));
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
ContentBlock::Thinking { thinking, signature, .. } => {
|
| 239 |
+
let mut part = json!({
|
| 240 |
+
"text": thinking,
|
| 241 |
+
"thought": true, // [CRITICAL FIX] Vertex AI v1internal requires thought: true to distinguish from text
|
| 242 |
+
});
|
| 243 |
+
// [New] 递归清理黑名单字段(如 cache_control)
|
| 244 |
+
crate::proxy::common::json_schema::clean_json_schema(&mut part);
|
| 245 |
+
|
| 246 |
+
if let Some(sig) = signature {
|
| 247 |
+
last_thought_signature = Some(sig.clone());
|
| 248 |
+
part["thoughtSignature"] = json!(sig);
|
| 249 |
+
}
|
| 250 |
+
parts.push(part);
|
| 251 |
+
}
|
| 252 |
+
ContentBlock::Image { source } => {
|
| 253 |
+
if source.source_type == "base64" {
|
| 254 |
+
parts.push(json!({
|
| 255 |
+
"inlineData": {
|
| 256 |
+
"mimeType": source.media_type,
|
| 257 |
+
"data": source.data
|
| 258 |
+
}
|
| 259 |
+
}));
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
ContentBlock::ToolUse { id, name, input, signature, .. } => {
|
| 263 |
+
let mut part = json!({
|
| 264 |
+
"functionCall": {
|
| 265 |
+
"name": name,
|
| 266 |
+
"args": input,
|
| 267 |
+
"id": id
|
| 268 |
+
}
|
| 269 |
+
});
|
| 270 |
+
|
| 271 |
+
// [New] 递归清理参数中可能存在的非法校验字段
|
| 272 |
+
crate::proxy::common::json_schema::clean_json_schema(&mut part);
|
| 273 |
+
|
| 274 |
+
// 存储 id -> name 映射
|
| 275 |
+
tool_id_to_name.insert(id.clone(), name.clone());
|
| 276 |
+
|
| 277 |
+
// Signature resolution logic (Priority: Client -> Context -> Global Store)
|
| 278 |
+
let final_sig = signature.as_ref()
|
| 279 |
+
.or(last_thought_signature.as_ref())
|
| 280 |
+
.cloned()
|
| 281 |
+
.or_else(|| {
|
| 282 |
+
let global_sig = get_thought_signature();
|
| 283 |
+
if global_sig.is_some() {
|
| 284 |
+
tracing::info!("[Claude-Request] Using global thought_signature fallback (length: {})",
|
| 285 |
+
global_sig.as_ref().unwrap().len());
|
| 286 |
+
}
|
| 287 |
+
global_sig
|
| 288 |
+
});
|
| 289 |
+
|
| 290 |
+
if let Some(sig) = final_sig {
|
| 291 |
+
part["thoughtSignature"] = json!(sig);
|
| 292 |
+
}
|
| 293 |
+
parts.push(part);
|
| 294 |
+
}
|
| 295 |
+
ContentBlock::ToolResult {
|
| 296 |
+
tool_use_id,
|
| 297 |
+
content,
|
| 298 |
+
is_error,
|
| 299 |
+
..
|
| 300 |
+
} => {
|
| 301 |
+
// 优先使用之前记录的 name,否则用 tool_use_id
|
| 302 |
+
let func_name = tool_id_to_name
|
| 303 |
+
.get(tool_use_id)
|
| 304 |
+
.cloned()
|
| 305 |
+
.unwrap_or_else(|| tool_use_id.clone());
|
| 306 |
+
|
| 307 |
+
// 处理 content:可能是一个内容块数组或单字符串
|
| 308 |
+
let mut merged_content = match content {
|
| 309 |
+
serde_json::Value::String(s) => s.clone(),
|
| 310 |
+
serde_json::Value::Array(arr) => arr
|
| 311 |
+
.iter()
|
| 312 |
+
.filter_map(|block| {
|
| 313 |
+
if let Some(text) =
|
| 314 |
+
block.get("text").and_then(|v| v.as_str())
|
| 315 |
+
{
|
| 316 |
+
Some(text)
|
| 317 |
+
} else {
|
| 318 |
+
None
|
| 319 |
+
}
|
| 320 |
+
})
|
| 321 |
+
.collect::<Vec<_>>()
|
| 322 |
+
.join("\n"),
|
| 323 |
+
_ => content.to_string(),
|
| 324 |
+
};
|
| 325 |
+
|
| 326 |
+
// [优化] 如果结果为空,注入显式确认信号,防止模型幻觉
|
| 327 |
+
if merged_content.trim().is_empty() {
|
| 328 |
+
if is_error.unwrap_or(false) {
|
| 329 |
+
merged_content =
|
| 330 |
+
"Tool execution failed with no output.".to_string();
|
| 331 |
+
} else {
|
| 332 |
+
merged_content = "Command executed successfully.".to_string();
|
| 333 |
+
}
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
let mut part = json!({
|
| 337 |
+
"functionResponse": {
|
| 338 |
+
"name": func_name,
|
| 339 |
+
"response": {"result": merged_content},
|
| 340 |
+
"id": tool_use_id
|
| 341 |
+
}
|
| 342 |
+
});
|
| 343 |
+
|
| 344 |
+
// [修复] Tool Result 也需要回填签名(如果上下文中有)
|
| 345 |
+
if let Some(sig) = last_thought_signature.as_ref() {
|
| 346 |
+
part["thoughtSignature"] = json!(sig);
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
parts.push(part);
|
| 350 |
+
}
|
| 351 |
+
ContentBlock::ServerToolUse { .. } | ContentBlock::WebSearchToolResult { .. } => {
|
| 352 |
+
// 搜索结果 block 不应由客户端发回给上游 (已由 tool_result 替代)
|
| 353 |
+
continue;
|
| 354 |
+
}
|
| 355 |
+
ContentBlock::RedactedThinking { data } => {
|
| 356 |
+
parts.push(json!({
|
| 357 |
+
"text": format!("[Redacted Thinking: {}]", data),
|
| 358 |
+
"thought": true
|
| 359 |
+
}));
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
// Fix for "Thinking enabled, assistant message must start with thinking block" 400 error
|
| 367 |
+
// [Optimization] Apply this to ALL assistant messages in history, not just the last one.
|
| 368 |
+
// Vertex AI requires every assistant message to start with a thinking block when thinking is enabled.
|
| 369 |
+
if allow_dummy_thought && role == "model" && is_thinking_enabled {
|
| 370 |
+
let has_thought_part = parts
|
| 371 |
+
.iter()
|
| 372 |
+
.any(|p| {
|
| 373 |
+
p.get("thought").and_then(|v| v.as_bool()).unwrap_or(false)
|
| 374 |
+
|| p.get("thoughtSignature").is_some()
|
| 375 |
+
|| p.get("thought").and_then(|v| v.as_str()).is_some() // 某些情况下可能是 text + thought: true 的组合
|
| 376 |
+
});
|
| 377 |
+
|
| 378 |
+
if !has_thought_part {
|
| 379 |
+
// Prepend a dummy thinking block to satisfy Gemini v1internal requirements
|
| 380 |
+
parts.insert(
|
| 381 |
+
0,
|
| 382 |
+
json!({
|
| 383 |
+
"text": "Thinking...",
|
| 384 |
+
"thought": true
|
| 385 |
+
}),
|
| 386 |
+
);
|
| 387 |
+
tracing::debug!("Injected dummy thought block for historical assistant message at index {}", contents.len());
|
| 388 |
+
} else {
|
| 389 |
+
// [Crucial Check] 即使有 thought 块,也必须保证它位于 parts 的首位 (Index 0)
|
| 390 |
+
// 且必须包含 thought: true 标记
|
| 391 |
+
let first_is_thought = parts.get(0).map_or(false, |p| {
|
| 392 |
+
(p.get("thought").is_some() || p.get("thoughtSignature").is_some())
|
| 393 |
+
&& p.get("text").is_some() // 对于 v1internal,通常 text + thought: true 才是合规的思维块
|
| 394 |
+
});
|
| 395 |
+
|
| 396 |
+
if !first_is_thought {
|
| 397 |
+
// 如果首项不符合思维块特征,强制补入一个
|
| 398 |
+
parts.insert(
|
| 399 |
+
0,
|
| 400 |
+
json!({
|
| 401 |
+
"text": "...",
|
| 402 |
+
"thought": true
|
| 403 |
+
}),
|
| 404 |
+
);
|
| 405 |
+
tracing::warn!("First part of model message at {} is not a valid thought block. Prepending dummy.", contents.len());
|
| 406 |
+
} else {
|
| 407 |
+
// 确保首项包含了 thought: true (防止只有 signature 的情况)
|
| 408 |
+
if let Some(p0) = parts.get_mut(0) {
|
| 409 |
+
if p0.get("thought").is_none() {
|
| 410 |
+
p0.as_object_mut().map(|obj| obj.insert("thought".to_string(), json!(true)));
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
if parts.is_empty() {
|
| 418 |
+
continue;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
contents.push(json!({
|
| 422 |
+
"role": role,
|
| 423 |
+
"parts": parts
|
| 424 |
+
}));
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
Ok(json!(contents))
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
/// 构建 Tools
|
| 431 |
+
fn build_tools(tools: &Option<Vec<Tool>>, has_web_search: bool) -> Result<Option<Value>, String> {
|
| 432 |
+
if let Some(tools_list) = tools {
|
| 433 |
+
let mut function_declarations: Vec<Value> = Vec::new();
|
| 434 |
+
let mut has_google_search = has_web_search;
|
| 435 |
+
|
| 436 |
+
for tool in tools_list {
|
| 437 |
+
// 1. Detect server tools / built-in tools like web_search
|
| 438 |
+
if tool.is_web_search() {
|
| 439 |
+
has_google_search = true;
|
| 440 |
+
continue;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
if let Some(t_type) = &tool.type_ {
|
| 444 |
+
if t_type == "web_search_20250305" {
|
| 445 |
+
has_google_search = true;
|
| 446 |
+
continue;
|
| 447 |
+
}
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
// 2. Detect by name
|
| 451 |
+
if let Some(name) = &tool.name {
|
| 452 |
+
if name == "web_search" || name == "google_search" {
|
| 453 |
+
has_google_search = true;
|
| 454 |
+
continue;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
// 3. Client tools require input_schema
|
| 458 |
+
let mut input_schema = tool.input_schema.clone().unwrap_or(json!({
|
| 459 |
+
"type": "object",
|
| 460 |
+
"properties": {}
|
| 461 |
+
}));
|
| 462 |
+
crate::proxy::common::json_schema::clean_json_schema(&mut input_schema);
|
| 463 |
+
|
| 464 |
+
function_declarations.push(json!({
|
| 465 |
+
"name": name,
|
| 466 |
+
"description": tool.description,
|
| 467 |
+
"parameters": input_schema
|
| 468 |
+
}));
|
| 469 |
+
}
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
let mut tool_obj = serde_json::Map::new();
|
| 473 |
+
|
| 474 |
+
// [修复] 解决 "Multiple tools are supported only when they are all search tools" 400 错误
|
| 475 |
+
// 原理:Gemini v1internal 接口非常挑剔,通常不允许在同一个工具定义中混用 Google Search 和 Function Declarationsc。
|
| 476 |
+
// 对于 Claude CLI 等携带 MCP 工具的客户端,必须优先保证 Function Declarations 正常工作。
|
| 477 |
+
if !function_declarations.is_empty() {
|
| 478 |
+
// 如果有本地工具,则只使用本地工具,放弃注入的 Google Search
|
| 479 |
+
tool_obj.insert("functionDeclarations".to_string(), json!(function_declarations));
|
| 480 |
+
} else if has_google_search {
|
| 481 |
+
// 只有在没有本地工具时,才允许注入 Google Search
|
| 482 |
+
tool_obj.insert("googleSearch".to_string(), json!({}));
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
if !tool_obj.is_empty() {
|
| 486 |
+
return Ok(Some(json!([tool_obj])));
|
| 487 |
+
}
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
Ok(None)
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
/// 构建 Generation Config
|
| 494 |
+
fn build_generation_config(claude_req: &ClaudeRequest, has_web_search: bool) -> Value {
|
| 495 |
+
let mut config = json!({});
|
| 496 |
+
|
| 497 |
+
// Thinking 配置
|
| 498 |
+
if let Some(thinking) = &claude_req.thinking {
|
| 499 |
+
if thinking.type_ == "enabled" {
|
| 500 |
+
let mut thinking_config = json!({"includeThoughts": true});
|
| 501 |
+
|
| 502 |
+
if let Some(budget_tokens) = thinking.budget_tokens {
|
| 503 |
+
let mut budget = budget_tokens;
|
| 504 |
+
// gemini-2.5-flash 上限 24576
|
| 505 |
+
let is_flash_model =
|
| 506 |
+
has_web_search || claude_req.model.contains("gemini-2.5-flash");
|
| 507 |
+
if is_flash_model {
|
| 508 |
+
budget = budget.min(24576);
|
| 509 |
+
}
|
| 510 |
+
thinking_config["thinkingBudget"] = json!(budget);
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
config["thinkingConfig"] = thinking_config;
|
| 514 |
+
}
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
// 其他参数
|
| 518 |
+
if let Some(temp) = claude_req.temperature {
|
| 519 |
+
config["temperature"] = json!(temp);
|
| 520 |
+
}
|
| 521 |
+
if let Some(top_p) = claude_req.top_p {
|
| 522 |
+
config["topP"] = json!(top_p);
|
| 523 |
+
}
|
| 524 |
+
if let Some(top_k) = claude_req.top_k {
|
| 525 |
+
config["topK"] = json!(top_k);
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
// web_search 强制 candidateCount=1
|
| 529 |
+
/*if has_web_search {
|
| 530 |
+
config["candidateCount"] = json!(1);
|
| 531 |
+
}*/
|
| 532 |
+
|
| 533 |
+
// max_tokens 映射为 maxOutputTokens
|
| 534 |
+
config["maxOutputTokens"] = json!(64000);
|
| 535 |
+
|
| 536 |
+
// [优化] 设置全局停止序列,防止流式输出冗余 (参考 done-hub)
|
| 537 |
+
config["stopSequences"] = json!([
|
| 538 |
+
"<|user|>",
|
| 539 |
+
"<|endoftext|>",
|
| 540 |
+
"<|end_of_turn|>",
|
| 541 |
+
"[DONE]",
|
| 542 |
+
"\n\nHuman:"
|
| 543 |
+
]);
|
| 544 |
+
|
| 545 |
+
config
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
#[cfg(test)]
|
| 549 |
+
mod tests {
|
| 550 |
+
use super::*;
|
| 551 |
+
use crate::proxy::common::json_schema::clean_json_schema;
|
| 552 |
+
|
| 553 |
+
#[test]
|
| 554 |
+
fn test_simple_request() {
|
| 555 |
+
let req = ClaudeRequest {
|
| 556 |
+
model: "claude-sonnet-4-5".to_string(),
|
| 557 |
+
messages: vec![Message {
|
| 558 |
+
role: "user".to_string(),
|
| 559 |
+
content: MessageContent::String("Hello".to_string()),
|
| 560 |
+
}],
|
| 561 |
+
system: None,
|
| 562 |
+
tools: None,
|
| 563 |
+
stream: false,
|
| 564 |
+
max_tokens: None,
|
| 565 |
+
temperature: None,
|
| 566 |
+
top_p: None,
|
| 567 |
+
top_k: None,
|
| 568 |
+
thinking: None,
|
| 569 |
+
metadata: None,
|
| 570 |
+
};
|
| 571 |
+
|
| 572 |
+
let result = transform_claude_request_in(&req, "test-project");
|
| 573 |
+
assert!(result.is_ok());
|
| 574 |
+
|
| 575 |
+
let body = result.unwrap();
|
| 576 |
+
assert_eq!(body["project"], "test-project");
|
| 577 |
+
assert!(body["requestId"].as_str().unwrap().starts_with("agent-"));
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
#[test]
|
| 581 |
+
fn test_clean_json_schema() {
|
| 582 |
+
let mut schema = json!({
|
| 583 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 584 |
+
"type": "object",
|
| 585 |
+
"additionalProperties": false,
|
| 586 |
+
"properties": {
|
| 587 |
+
"location": {
|
| 588 |
+
"type": "string",
|
| 589 |
+
"description": "The city and state, e.g. San Francisco, CA",
|
| 590 |
+
"minLength": 1,
|
| 591 |
+
"exclusiveMinimum": 0
|
| 592 |
+
},
|
| 593 |
+
"unit": {
|
| 594 |
+
"type": ["string", "null"],
|
| 595 |
+
"enum": ["celsius", "fahrenheit"],
|
| 596 |
+
"default": "celsius"
|
| 597 |
+
},
|
| 598 |
+
"date": {
|
| 599 |
+
"type": "string",
|
| 600 |
+
"format": "date"
|
| 601 |
+
}
|
| 602 |
+
},
|
| 603 |
+
"required": ["location"]
|
| 604 |
+
});
|
| 605 |
+
|
| 606 |
+
clean_json_schema(&mut schema);
|
| 607 |
+
|
| 608 |
+
// Check removed fields
|
| 609 |
+
assert!(schema.get("$schema").is_none());
|
| 610 |
+
assert!(schema.get("additionalProperties").is_none());
|
| 611 |
+
assert!(schema["properties"]["location"].get("minLength").is_none());
|
| 612 |
+
assert!(schema["properties"]["unit"].get("default").is_none());
|
| 613 |
+
assert!(schema["properties"]["date"].get("format").is_none());
|
| 614 |
+
|
| 615 |
+
// Check union type handling ["string", "null"] -> "string"
|
| 616 |
+
assert_eq!(schema["properties"]["unit"]["type"], "string");
|
| 617 |
+
|
| 618 |
+
// Check types are lowercased
|
| 619 |
+
assert_eq!(schema["type"], "object");
|
| 620 |
+
assert_eq!(schema["properties"]["location"]["type"], "string");
|
| 621 |
+
assert_eq!(schema["properties"]["date"]["type"], "string");
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
#[test]
|
| 625 |
+
fn test_complex_tool_result() {
|
| 626 |
+
let req = ClaudeRequest {
|
| 627 |
+
model: "claude-3-5-sonnet-20241022".to_string(),
|
| 628 |
+
messages: vec![
|
| 629 |
+
Message {
|
| 630 |
+
role: "user".to_string(),
|
| 631 |
+
content: MessageContent::String("Run command".to_string()),
|
| 632 |
+
},
|
| 633 |
+
Message {
|
| 634 |
+
role: "assistant".to_string(),
|
| 635 |
+
content: MessageContent::Array(vec![
|
| 636 |
+
ContentBlock::ToolUse {
|
| 637 |
+
id: "call_1".to_string(),
|
| 638 |
+
name: "run_command".to_string(),
|
| 639 |
+
input: json!({"command": "ls"}),
|
| 640 |
+
signature: None,
|
| 641 |
+
cache_control: None,
|
| 642 |
+
}
|
| 643 |
+
]),
|
| 644 |
+
},
|
| 645 |
+
Message {
|
| 646 |
+
role: "user".to_string(),
|
| 647 |
+
content: MessageContent::Array(vec![ContentBlock::ToolResult {
|
| 648 |
+
tool_use_id: "call_1".to_string(),
|
| 649 |
+
content: json!([
|
| 650 |
+
{"type": "text", "text": "file1.txt\n"},
|
| 651 |
+
{"type": "text", "text": "file2.txt"}
|
| 652 |
+
]),
|
| 653 |
+
is_error: Some(false),
|
| 654 |
+
}]),
|
| 655 |
+
},
|
| 656 |
+
],
|
| 657 |
+
system: None,
|
| 658 |
+
tools: None,
|
| 659 |
+
stream: false,
|
| 660 |
+
max_tokens: None,
|
| 661 |
+
temperature: None,
|
| 662 |
+
top_p: None,
|
| 663 |
+
top_k: None,
|
| 664 |
+
thinking: None,
|
| 665 |
+
metadata: None,
|
| 666 |
+
};
|
| 667 |
+
|
| 668 |
+
let result = transform_claude_request_in(&req, "test-project");
|
| 669 |
+
assert!(result.is_ok());
|
| 670 |
+
|
| 671 |
+
let body = result.unwrap();
|
| 672 |
+
let contents = body["request"]["contents"].as_array().unwrap();
|
| 673 |
+
|
| 674 |
+
// Check the tool result message (last message)
|
| 675 |
+
let tool_resp_msg = &contents[2];
|
| 676 |
+
let parts = tool_resp_msg["parts"].as_array().unwrap();
|
| 677 |
+
let func_resp = &parts[0]["functionResponse"];
|
| 678 |
+
|
| 679 |
+
assert_eq!(func_resp["name"], "run_command");
|
| 680 |
+
assert_eq!(func_resp["id"], "call_1");
|
| 681 |
+
|
| 682 |
+
// Verify merged content
|
| 683 |
+
let resp_text = func_resp["response"]["result"].as_str().unwrap();
|
| 684 |
+
assert!(resp_text.contains("file1.txt"));
|
| 685 |
+
assert!(resp_text.contains("file2.txt"));
|
| 686 |
+
assert!(resp_text.contains("\n"));
|
| 687 |
+
}
|
| 688 |
+
}
|
server/src/proxy/mappers/claude/response.rs
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Claude 非流式响应转换 (Gemini → Claude)
|
| 2 |
+
// 对应 NonStreamingProcessor
|
| 3 |
+
|
| 4 |
+
use super::models::*;
|
| 5 |
+
use super::utils::to_claude_usage;
|
| 6 |
+
|
| 7 |
+
/// 非流式响应处理器
|
| 8 |
+
pub struct NonStreamingProcessor {
|
| 9 |
+
content_blocks: Vec<ContentBlock>,
|
| 10 |
+
text_builder: String,
|
| 11 |
+
thinking_builder: String,
|
| 12 |
+
thinking_signature: Option<String>,
|
| 13 |
+
trailing_signature: Option<String>,
|
| 14 |
+
has_tool_call: bool,
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
impl NonStreamingProcessor {
|
| 18 |
+
pub fn new() -> Self {
|
| 19 |
+
Self {
|
| 20 |
+
content_blocks: Vec::new(),
|
| 21 |
+
text_builder: String::new(),
|
| 22 |
+
thinking_builder: String::new(),
|
| 23 |
+
thinking_signature: None,
|
| 24 |
+
trailing_signature: None,
|
| 25 |
+
has_tool_call: false,
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/// 处理 Gemini 响应并转换为 Claude 响应
|
| 30 |
+
pub fn process(&mut self, gemini_response: &GeminiResponse) -> ClaudeResponse {
|
| 31 |
+
// 获取 parts
|
| 32 |
+
let empty_parts = vec![];
|
| 33 |
+
let parts = gemini_response
|
| 34 |
+
.candidates
|
| 35 |
+
.as_ref()
|
| 36 |
+
.and_then(|c| c.get(0))
|
| 37 |
+
.and_then(|candidate| candidate.content.as_ref())
|
| 38 |
+
.map(|content| &content.parts)
|
| 39 |
+
.unwrap_or(&empty_parts);
|
| 40 |
+
|
| 41 |
+
// 处理所有 parts
|
| 42 |
+
for part in parts {
|
| 43 |
+
self.process_part(part);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// 处理 grounding(web search) -> 转换为 server_tool_use / web_search_tool_result
|
| 47 |
+
if let Some(candidate) = gemini_response.candidates.as_ref().and_then(|c| c.get(0)) {
|
| 48 |
+
if let Some(grounding) = &candidate.grounding_metadata {
|
| 49 |
+
self.process_grounding(grounding);
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// 刷新剩余内容
|
| 54 |
+
self.flush_thinking();
|
| 55 |
+
self.flush_text();
|
| 56 |
+
|
| 57 |
+
// 处理 trailingSignature (空 text 带签名)
|
| 58 |
+
if let Some(signature) = self.trailing_signature.take() {
|
| 59 |
+
self.content_blocks.push(ContentBlock::Thinking {
|
| 60 |
+
thinking: String::new(),
|
| 61 |
+
signature: Some(signature),
|
| 62 |
+
cache_control: None,
|
| 63 |
+
});
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// 构建响应
|
| 67 |
+
self.build_response(gemini_response)
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/// 处理单个 part
|
| 71 |
+
fn process_part(&mut self, part: &GeminiPart) {
|
| 72 |
+
let signature = part.thought_signature.clone();
|
| 73 |
+
|
| 74 |
+
// 1. FunctionCall 处理
|
| 75 |
+
if let Some(fc) = &part.function_call {
|
| 76 |
+
self.flush_thinking();
|
| 77 |
+
self.flush_text();
|
| 78 |
+
|
| 79 |
+
// 处理 trailingSignature (B4/C3 场景)
|
| 80 |
+
if let Some(trailing_sig) = self.trailing_signature.take() {
|
| 81 |
+
self.content_blocks.push(ContentBlock::Thinking {
|
| 82 |
+
thinking: String::new(),
|
| 83 |
+
signature: Some(trailing_sig),
|
| 84 |
+
cache_control: None,
|
| 85 |
+
});
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
self.has_tool_call = true;
|
| 89 |
+
|
| 90 |
+
// 生成 tool_use id
|
| 91 |
+
let tool_id = fc.id.clone().unwrap_or_else(|| {
|
| 92 |
+
format!(
|
| 93 |
+
"{}-{}",
|
| 94 |
+
fc.name,
|
| 95 |
+
crate::proxy::common::utils::generate_random_id()
|
| 96 |
+
)
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
let mut tool_use = ContentBlock::ToolUse {
|
| 100 |
+
id: tool_id,
|
| 101 |
+
name: fc.name.clone(),
|
| 102 |
+
input: fc.args.clone().unwrap_or(serde_json::json!({})),
|
| 103 |
+
signature: None,
|
| 104 |
+
cache_control: None,
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
// 只使用 FC 自己的签名
|
| 108 |
+
if let ContentBlock::ToolUse { signature: sig, .. } = &mut tool_use {
|
| 109 |
+
*sig = signature;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
self.content_blocks.push(tool_use);
|
| 113 |
+
return;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// 2. Text 处理
|
| 117 |
+
if let Some(text) = &part.text {
|
| 118 |
+
if part.thought.unwrap_or(false) {
|
| 119 |
+
// Thinking part
|
| 120 |
+
self.flush_text();
|
| 121 |
+
|
| 122 |
+
// 处理 trailingSignature
|
| 123 |
+
if let Some(trailing_sig) = self.trailing_signature.take() {
|
| 124 |
+
self.flush_thinking();
|
| 125 |
+
self.content_blocks.push(ContentBlock::Thinking {
|
| 126 |
+
thinking: String::new(),
|
| 127 |
+
signature: Some(trailing_sig),
|
| 128 |
+
cache_control: None,
|
| 129 |
+
});
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
self.thinking_builder.push_str(text);
|
| 133 |
+
if signature.is_some() {
|
| 134 |
+
self.thinking_signature = signature;
|
| 135 |
+
}
|
| 136 |
+
} else {
|
| 137 |
+
// 普通 Text
|
| 138 |
+
if text.is_empty() {
|
| 139 |
+
// 空 text 带签名 - 暂存到 trailingSignature
|
| 140 |
+
if signature.is_some() {
|
| 141 |
+
self.trailing_signature = signature;
|
| 142 |
+
}
|
| 143 |
+
return;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
self.flush_thinking();
|
| 147 |
+
|
| 148 |
+
// 处理之前的 trailingSignature
|
| 149 |
+
if let Some(trailing_sig) = self.trailing_signature.take() {
|
| 150 |
+
self.flush_text();
|
| 151 |
+
self.content_blocks.push(ContentBlock::Thinking {
|
| 152 |
+
thinking: String::new(),
|
| 153 |
+
signature: Some(trailing_sig),
|
| 154 |
+
cache_control: None,
|
| 155 |
+
});
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
self.text_builder.push_str(text);
|
| 159 |
+
|
| 160 |
+
// 非空 text 带签名 - 立即刷新并输出空 thinking 块
|
| 161 |
+
if let Some(sig) = signature {
|
| 162 |
+
self.flush_text();
|
| 163 |
+
self.content_blocks.push(ContentBlock::Thinking {
|
| 164 |
+
thinking: String::new(),
|
| 165 |
+
signature: Some(sig),
|
| 166 |
+
cache_control: None,
|
| 167 |
+
});
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// 3. InlineData (Image) 处理
|
| 173 |
+
if let Some(img) = &part.inline_data {
|
| 174 |
+
self.flush_thinking();
|
| 175 |
+
|
| 176 |
+
let mime_type = &img.mime_type;
|
| 177 |
+
let data = &img.data;
|
| 178 |
+
if !data.is_empty() {
|
| 179 |
+
let markdown_img = format!("", mime_type, data);
|
| 180 |
+
self.text_builder.push_str(&markdown_img);
|
| 181 |
+
self.flush_text();
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/// 处理 Grounding 元数据 (Web Search 结果)
|
| 187 |
+
fn process_grounding(&mut self, grounding: &GroundingMetadata) {
|
| 188 |
+
let mut grounding_text = String::new();
|
| 189 |
+
|
| 190 |
+
// 1. 处理搜索词
|
| 191 |
+
if let Some(queries) = &grounding.web_search_queries {
|
| 192 |
+
if !queries.is_empty() {
|
| 193 |
+
grounding_text.push_str("\n\n---\n**🔍 已为您搜索:** ");
|
| 194 |
+
grounding_text.push_str(&queries.join(", "));
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// 2. 处理来源链接 (Chunks)
|
| 199 |
+
if let Some(chunks) = &grounding.grounding_chunks {
|
| 200 |
+
let mut links = Vec::new();
|
| 201 |
+
for (i, chunk) in chunks.iter().enumerate() {
|
| 202 |
+
if let Some(web) = &chunk.web {
|
| 203 |
+
let title = web.title.as_deref().unwrap_or("网页来源");
|
| 204 |
+
let uri = web.uri.as_deref().unwrap_or("#");
|
| 205 |
+
links.push(format!("[{}] [{}]({})", i + 1, title, uri));
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
if !links.is_empty() {
|
| 210 |
+
grounding_text.push_str("\n\n**🌐 来源引文:**\n");
|
| 211 |
+
grounding_text.push_str(&links.join("\n"));
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
if !grounding_text.is_empty() {
|
| 216 |
+
// 在常规内容前后刷新并插入文本
|
| 217 |
+
self.flush_thinking();
|
| 218 |
+
self.flush_text();
|
| 219 |
+
self.text_builder.push_str(&grounding_text);
|
| 220 |
+
self.flush_text();
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
/// 刷新 text builder
|
| 225 |
+
fn flush_text(&mut self) {
|
| 226 |
+
if self.text_builder.is_empty() {
|
| 227 |
+
return;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
self.content_blocks.push(ContentBlock::Text {
|
| 231 |
+
text: self.text_builder.clone(),
|
| 232 |
+
});
|
| 233 |
+
self.text_builder.clear();
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
/// 刷新 thinking builder
|
| 237 |
+
fn flush_thinking(&mut self) {
|
| 238 |
+
// 如果既没有内容也没有签名,直接返回
|
| 239 |
+
if self.thinking_builder.is_empty() && self.thinking_signature.is_none() {
|
| 240 |
+
return;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
let thinking = self.thinking_builder.clone();
|
| 244 |
+
let signature = self.thinking_signature.take();
|
| 245 |
+
|
| 246 |
+
self.content_blocks.push(ContentBlock::Thinking {
|
| 247 |
+
thinking,
|
| 248 |
+
signature,
|
| 249 |
+
cache_control: None,
|
| 250 |
+
});
|
| 251 |
+
self.thinking_builder.clear();
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
/// 构建最终响应
|
| 255 |
+
fn build_response(&self, gemini_response: &GeminiResponse) -> ClaudeResponse {
|
| 256 |
+
let finish_reason = gemini_response
|
| 257 |
+
.candidates
|
| 258 |
+
.as_ref()
|
| 259 |
+
.and_then(|c| c.get(0))
|
| 260 |
+
.and_then(|candidate| candidate.finish_reason.as_deref());
|
| 261 |
+
|
| 262 |
+
let stop_reason = if self.has_tool_call {
|
| 263 |
+
"tool_use"
|
| 264 |
+
} else if finish_reason == Some("MAX_TOKENS") {
|
| 265 |
+
"max_tokens"
|
| 266 |
+
} else {
|
| 267 |
+
"end_turn"
|
| 268 |
+
};
|
| 269 |
+
|
| 270 |
+
let usage = gemini_response
|
| 271 |
+
.usage_metadata
|
| 272 |
+
.as_ref()
|
| 273 |
+
.map(|u| to_claude_usage(u))
|
| 274 |
+
.unwrap_or(Usage {
|
| 275 |
+
input_tokens: 0,
|
| 276 |
+
output_tokens: 0,
|
| 277 |
+
server_tool_use: None,
|
| 278 |
+
});
|
| 279 |
+
|
| 280 |
+
ClaudeResponse {
|
| 281 |
+
id: gemini_response.response_id.clone().unwrap_or_else(|| {
|
| 282 |
+
format!("msg_{}", crate::proxy::common::utils::generate_random_id())
|
| 283 |
+
}),
|
| 284 |
+
type_: "message".to_string(),
|
| 285 |
+
role: "assistant".to_string(),
|
| 286 |
+
model: gemini_response.model_version.clone().unwrap_or_default(),
|
| 287 |
+
content: self.content_blocks.clone(),
|
| 288 |
+
stop_reason: stop_reason.to_string(),
|
| 289 |
+
stop_sequence: None,
|
| 290 |
+
usage,
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
/// 转换 Gemini 响应为 Claude 响应 (公共接口)
|
| 296 |
+
pub fn transform_response(gemini_response: &GeminiResponse) -> Result<ClaudeResponse, String> {
|
| 297 |
+
let mut processor = NonStreamingProcessor::new();
|
| 298 |
+
Ok(processor.process(gemini_response))
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
#[cfg(test)]
|
| 302 |
+
mod tests {
|
| 303 |
+
use super::*;
|
| 304 |
+
|
| 305 |
+
#[test]
|
| 306 |
+
fn test_simple_text_response() {
|
| 307 |
+
let gemini_resp = GeminiResponse {
|
| 308 |
+
candidates: Some(vec![Candidate {
|
| 309 |
+
content: Some(GeminiContent {
|
| 310 |
+
role: "model".to_string(),
|
| 311 |
+
parts: vec![GeminiPart {
|
| 312 |
+
text: Some("Hello, world!".to_string()),
|
| 313 |
+
thought: None,
|
| 314 |
+
thought_signature: None,
|
| 315 |
+
function_call: None,
|
| 316 |
+
function_response: None,
|
| 317 |
+
inline_data: None,
|
| 318 |
+
}],
|
| 319 |
+
}),
|
| 320 |
+
finish_reason: Some("STOP".to_string()),
|
| 321 |
+
index: Some(0),
|
| 322 |
+
grounding_metadata: None,
|
| 323 |
+
}]),
|
| 324 |
+
usage_metadata: Some(UsageMetadata {
|
| 325 |
+
prompt_token_count: Some(10),
|
| 326 |
+
candidates_token_count: Some(5),
|
| 327 |
+
total_token_count: Some(15),
|
| 328 |
+
}),
|
| 329 |
+
model_version: Some("gemini-2.5-pro".to_string()),
|
| 330 |
+
response_id: Some("resp_123".to_string()),
|
| 331 |
+
};
|
| 332 |
+
|
| 333 |
+
let result = transform_response(&gemini_resp);
|
| 334 |
+
assert!(result.is_ok());
|
| 335 |
+
|
| 336 |
+
let claude_resp = result.unwrap();
|
| 337 |
+
assert_eq!(claude_resp.role, "assistant");
|
| 338 |
+
assert_eq!(claude_resp.stop_reason, "end_turn");
|
| 339 |
+
assert_eq!(claude_resp.content.len(), 1);
|
| 340 |
+
|
| 341 |
+
match &claude_resp.content[0] {
|
| 342 |
+
ContentBlock::Text { text } => {
|
| 343 |
+
assert_eq!(text, "Hello, world!");
|
| 344 |
+
}
|
| 345 |
+
_ => panic!("Expected Text block"),
|
| 346 |
+
}
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
#[test]
|
| 350 |
+
fn test_thinking_with_signature() {
|
| 351 |
+
let gemini_resp = GeminiResponse {
|
| 352 |
+
candidates: Some(vec![Candidate {
|
| 353 |
+
content: Some(GeminiContent {
|
| 354 |
+
role: "model".to_string(),
|
| 355 |
+
parts: vec![
|
| 356 |
+
GeminiPart {
|
| 357 |
+
text: Some("Let me think...".to_string()),
|
| 358 |
+
thought: Some(true),
|
| 359 |
+
thought_signature: Some("sig123".to_string()),
|
| 360 |
+
function_call: None,
|
| 361 |
+
function_response: None,
|
| 362 |
+
inline_data: None,
|
| 363 |
+
},
|
| 364 |
+
GeminiPart {
|
| 365 |
+
text: Some("The answer is 42".to_string()),
|
| 366 |
+
thought: None,
|
| 367 |
+
thought_signature: None,
|
| 368 |
+
function_call: None,
|
| 369 |
+
function_response: None,
|
| 370 |
+
inline_data: None,
|
| 371 |
+
},
|
| 372 |
+
],
|
| 373 |
+
}),
|
| 374 |
+
finish_reason: Some("STOP".to_string()),
|
| 375 |
+
index: Some(0),
|
| 376 |
+
grounding_metadata: None,
|
| 377 |
+
}]),
|
| 378 |
+
usage_metadata: None,
|
| 379 |
+
model_version: Some("gemini-2.5-pro".to_string()),
|
| 380 |
+
response_id: Some("resp_456".to_string()),
|
| 381 |
+
};
|
| 382 |
+
|
| 383 |
+
let result = transform_response(&gemini_resp);
|
| 384 |
+
assert!(result.is_ok());
|
| 385 |
+
|
| 386 |
+
let claude_resp = result.unwrap();
|
| 387 |
+
assert_eq!(claude_resp.content.len(), 2);
|
| 388 |
+
|
| 389 |
+
match &claude_resp.content[0] {
|
| 390 |
+
ContentBlock::Thinking {
|
| 391 |
+
thinking,
|
| 392 |
+
signature,
|
| 393 |
+
..
|
| 394 |
+
} => {
|
| 395 |
+
assert_eq!(thinking, "Let me think...");
|
| 396 |
+
assert_eq!(signature.as_deref(), Some("sig123"));
|
| 397 |
+
}
|
| 398 |
+
_ => panic!("Expected Thinking block"),
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
match &claude_resp.content[1] {
|
| 402 |
+
ContentBlock::Text { text } => {
|
| 403 |
+
assert_eq!(text, "The answer is 42");
|
| 404 |
+
}
|
| 405 |
+
_ => panic!("Expected Text block"),
|
| 406 |
+
}
|
| 407 |
+
}
|
| 408 |
+
}
|
server/src/proxy/mappers/claude/streaming.rs
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Claude 流式响应转换 (Gemini SSE → Claude SSE)
|
| 2 |
+
// 对应 StreamingState + PartProcessor
|
| 3 |
+
|
| 4 |
+
use super::models::*;
|
| 5 |
+
use super::utils::to_claude_usage;
|
| 6 |
+
use crate::proxy::mappers::signature_store::store_thought_signature;
|
| 7 |
+
use bytes::Bytes;
|
| 8 |
+
use serde_json::json;
|
| 9 |
+
|
| 10 |
+
/// 块类型枚举
|
| 11 |
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
| 12 |
+
pub enum BlockType {
|
| 13 |
+
None,
|
| 14 |
+
Text,
|
| 15 |
+
Thinking,
|
| 16 |
+
Function,
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/// 签名管理器
|
| 20 |
+
pub struct SignatureManager {
|
| 21 |
+
pending: Option<String>,
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
impl SignatureManager {
|
| 25 |
+
pub fn new() -> Self {
|
| 26 |
+
Self { pending: None }
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
pub fn store(&mut self, signature: Option<String>) {
|
| 30 |
+
if signature.is_some() {
|
| 31 |
+
self.pending = signature;
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
pub fn consume(&mut self) -> Option<String> {
|
| 36 |
+
self.pending.take()
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
pub fn has_pending(&self) -> bool {
|
| 40 |
+
self.pending.is_some()
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/// 流式状态机
|
| 45 |
+
pub struct StreamingState {
|
| 46 |
+
block_type: BlockType,
|
| 47 |
+
pub block_index: usize,
|
| 48 |
+
pub message_start_sent: bool,
|
| 49 |
+
pub message_stop_sent: bool,
|
| 50 |
+
used_tool: bool,
|
| 51 |
+
signatures: SignatureManager,
|
| 52 |
+
trailing_signature: Option<String>,
|
| 53 |
+
pub web_search_query: Option<String>,
|
| 54 |
+
pub grounding_chunks: Option<Vec<serde_json::Value>>,
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
impl StreamingState {
|
| 58 |
+
pub fn new() -> Self {
|
| 59 |
+
Self {
|
| 60 |
+
block_type: BlockType::None,
|
| 61 |
+
block_index: 0,
|
| 62 |
+
message_start_sent: false,
|
| 63 |
+
message_stop_sent: false,
|
| 64 |
+
used_tool: false,
|
| 65 |
+
signatures: SignatureManager::new(),
|
| 66 |
+
trailing_signature: None,
|
| 67 |
+
web_search_query: None,
|
| 68 |
+
grounding_chunks: None,
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/// 发送 SSE 事件
|
| 73 |
+
pub fn emit(&self, event_type: &str, data: serde_json::Value) -> Bytes {
|
| 74 |
+
let sse = format!(
|
| 75 |
+
"event: {}\ndata: {}\n\n",
|
| 76 |
+
event_type,
|
| 77 |
+
serde_json::to_string(&data).unwrap_or_default()
|
| 78 |
+
);
|
| 79 |
+
Bytes::from(sse)
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/// 发送 message_start 事件
|
| 83 |
+
pub fn emit_message_start(&mut self, raw_json: &serde_json::Value) -> Bytes {
|
| 84 |
+
if self.message_start_sent {
|
| 85 |
+
return Bytes::new();
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
let usage = raw_json
|
| 89 |
+
.get("usageMetadata")
|
| 90 |
+
.and_then(|u| serde_json::from_value::<UsageMetadata>(u.clone()).ok())
|
| 91 |
+
.map(|u| to_claude_usage(&u));
|
| 92 |
+
|
| 93 |
+
let mut message = json!({
|
| 94 |
+
"id": raw_json.get("responseId")
|
| 95 |
+
.and_then(|v| v.as_str())
|
| 96 |
+
.unwrap_or_else(|| "msg_unknown"),
|
| 97 |
+
"type": "message",
|
| 98 |
+
"role": "assistant",
|
| 99 |
+
"content": [],
|
| 100 |
+
"model": raw_json.get("modelVersion")
|
| 101 |
+
.and_then(|v| v.as_str())
|
| 102 |
+
.unwrap_or(""),
|
| 103 |
+
"stop_reason": null,
|
| 104 |
+
"stop_sequence": null,
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
if let Some(u) = usage {
|
| 108 |
+
message["usage"] = json!(u);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
let result = self.emit(
|
| 112 |
+
"message_start",
|
| 113 |
+
json!({
|
| 114 |
+
"type": "message_start",
|
| 115 |
+
"message": message
|
| 116 |
+
}),
|
| 117 |
+
);
|
| 118 |
+
|
| 119 |
+
self.message_start_sent = true;
|
| 120 |
+
result
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/// 开始新的内容块
|
| 124 |
+
pub fn start_block(
|
| 125 |
+
&mut self,
|
| 126 |
+
block_type: BlockType,
|
| 127 |
+
content_block: serde_json::Value,
|
| 128 |
+
) -> Vec<Bytes> {
|
| 129 |
+
let mut chunks = Vec::new();
|
| 130 |
+
if self.block_type != BlockType::None {
|
| 131 |
+
chunks.extend(self.end_block());
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
chunks.push(self.emit(
|
| 135 |
+
"content_block_start",
|
| 136 |
+
json!({
|
| 137 |
+
"type": "content_block_start",
|
| 138 |
+
"index": self.block_index,
|
| 139 |
+
"content_block": content_block
|
| 140 |
+
}),
|
| 141 |
+
));
|
| 142 |
+
|
| 143 |
+
self.block_type = block_type;
|
| 144 |
+
chunks
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/// 结束当前内容块
|
| 148 |
+
pub fn end_block(&mut self) -> Vec<Bytes> {
|
| 149 |
+
if self.block_type == BlockType::None {
|
| 150 |
+
return vec![];
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
let mut chunks = Vec::new();
|
| 154 |
+
|
| 155 |
+
// Thinking 块结束时发送暂存的签名
|
| 156 |
+
if self.block_type == BlockType::Thinking && self.signatures.has_pending() {
|
| 157 |
+
if let Some(signature) = self.signatures.consume() {
|
| 158 |
+
chunks.push(self.emit_delta("signature_delta", json!({ "signature": signature })));
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
chunks.push(self.emit(
|
| 163 |
+
"content_block_stop",
|
| 164 |
+
json!({
|
| 165 |
+
"type": "content_block_stop",
|
| 166 |
+
"index": self.block_index
|
| 167 |
+
}),
|
| 168 |
+
));
|
| 169 |
+
|
| 170 |
+
self.block_index += 1;
|
| 171 |
+
self.block_type = BlockType::None;
|
| 172 |
+
|
| 173 |
+
chunks
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
/// 发送 delta 事件
|
| 177 |
+
pub fn emit_delta(&self, delta_type: &str, delta_content: serde_json::Value) -> Bytes {
|
| 178 |
+
let mut delta = json!({ "type": delta_type });
|
| 179 |
+
if let serde_json::Value::Object(map) = delta_content {
|
| 180 |
+
for (k, v) in map {
|
| 181 |
+
delta[k] = v;
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
self.emit(
|
| 186 |
+
"content_block_delta",
|
| 187 |
+
json!({
|
| 188 |
+
"type": "content_block_delta",
|
| 189 |
+
"index": self.block_index,
|
| 190 |
+
"delta": delta
|
| 191 |
+
}),
|
| 192 |
+
)
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/// 发送结束事件
|
| 196 |
+
pub fn emit_finish(
|
| 197 |
+
&mut self,
|
| 198 |
+
finish_reason: Option<&str>,
|
| 199 |
+
usage_metadata: Option<&UsageMetadata>,
|
| 200 |
+
) -> Vec<Bytes> {
|
| 201 |
+
let mut chunks = Vec::new();
|
| 202 |
+
|
| 203 |
+
// 关闭最后一个块
|
| 204 |
+
chunks.extend(self.end_block());
|
| 205 |
+
|
| 206 |
+
// 处理 trailingSignature (PDF 776-778)
|
| 207 |
+
if let Some(signature) = self.trailing_signature.take() {
|
| 208 |
+
chunks.push(self.emit(
|
| 209 |
+
"content_block_start",
|
| 210 |
+
json!({
|
| 211 |
+
"type": "content_block_start",
|
| 212 |
+
"index": self.block_index,
|
| 213 |
+
"content_block": { "type": "thinking", "thinking": "" }
|
| 214 |
+
}),
|
| 215 |
+
));
|
| 216 |
+
chunks.push(self.emit_delta("thinking_delta", json!({ "thinking": "" })));
|
| 217 |
+
chunks.push(self.emit_delta("signature_delta", json!({ "signature": signature })));
|
| 218 |
+
chunks.push(self.emit(
|
| 219 |
+
"content_block_stop",
|
| 220 |
+
json!({
|
| 221 |
+
"type": "content_block_stop",
|
| 222 |
+
"index": self.block_index
|
| 223 |
+
}),
|
| 224 |
+
));
|
| 225 |
+
self.block_index += 1;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
// 处理 grounding(web search) -> 转换为 Markdown 文本块
|
| 229 |
+
if self.web_search_query.is_some() || self.grounding_chunks.is_some() {
|
| 230 |
+
let mut grounding_text = String::new();
|
| 231 |
+
|
| 232 |
+
// 1. 处理搜索词
|
| 233 |
+
if let Some(query) = &self.web_search_query {
|
| 234 |
+
if !query.is_empty() {
|
| 235 |
+
grounding_text.push_str("\n\n---\n**🔍 已为您搜索:** ");
|
| 236 |
+
grounding_text.push_str(query);
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
// 2. 处理来源链接
|
| 241 |
+
if let Some(chunks) = &self.grounding_chunks {
|
| 242 |
+
let mut links = Vec::new();
|
| 243 |
+
for (i, chunk) in chunks.iter().enumerate() {
|
| 244 |
+
if let Some(web) = chunk.get("web") {
|
| 245 |
+
let title = web.get("title").and_then(|v| v.as_str()).unwrap_or("网页来源");
|
| 246 |
+
let uri = web.get("uri").and_then(|v| v.as_str()).unwrap_or("#");
|
| 247 |
+
links.push(format!("[{}] [{}]({})", i + 1, title, uri));
|
| 248 |
+
}
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
if !links.is_empty() {
|
| 252 |
+
grounding_text.push_str("\n\n**🌐 来源引文:**\n");
|
| 253 |
+
grounding_text.push_str(&links.join("\n"));
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
if !grounding_text.is_empty() {
|
| 258 |
+
// 发送一个新的 text 块
|
| 259 |
+
chunks.push(self.emit("content_block_start", json!({
|
| 260 |
+
"type": "content_block_start",
|
| 261 |
+
"index": self.block_index,
|
| 262 |
+
"content_block": { "type": "text", "text": "" }
|
| 263 |
+
})));
|
| 264 |
+
chunks.push(self.emit_delta("text_delta", json!({ "text": grounding_text })));
|
| 265 |
+
chunks.push(self.emit("content_block_stop", json!({ "type": "content_block_stop", "index": self.block_index })));
|
| 266 |
+
self.block_index += 1;
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
// 确定 stop_reason
|
| 271 |
+
let stop_reason = if self.used_tool {
|
| 272 |
+
"tool_use"
|
| 273 |
+
} else if finish_reason == Some("MAX_TOKENS") {
|
| 274 |
+
"max_tokens"
|
| 275 |
+
} else {
|
| 276 |
+
"end_turn"
|
| 277 |
+
};
|
| 278 |
+
|
| 279 |
+
let usage = usage_metadata
|
| 280 |
+
.map(|u| to_claude_usage(u))
|
| 281 |
+
.unwrap_or(Usage {
|
| 282 |
+
input_tokens: 0,
|
| 283 |
+
output_tokens: 0,
|
| 284 |
+
server_tool_use: None,
|
| 285 |
+
});
|
| 286 |
+
|
| 287 |
+
chunks.push(self.emit(
|
| 288 |
+
"message_delta",
|
| 289 |
+
json!({
|
| 290 |
+
"type": "message_delta",
|
| 291 |
+
"delta": { "stop_reason": stop_reason, "stop_sequence": null },
|
| 292 |
+
"usage": usage
|
| 293 |
+
}),
|
| 294 |
+
));
|
| 295 |
+
|
| 296 |
+
if !self.message_stop_sent {
|
| 297 |
+
chunks.push(Bytes::from(
|
| 298 |
+
"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
|
| 299 |
+
));
|
| 300 |
+
self.message_stop_sent = true;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
chunks
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
/// 标记使用了工具
|
| 307 |
+
pub fn mark_tool_used(&mut self) {
|
| 308 |
+
self.used_tool = true;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
/// 获取当前块类型
|
| 312 |
+
pub fn current_block_type(&self) -> BlockType {
|
| 313 |
+
self.block_type
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
/// 获取当前块索引
|
| 317 |
+
pub fn current_block_index(&self) -> usize {
|
| 318 |
+
self.block_index
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
/// 存储签名
|
| 322 |
+
pub fn store_signature(&mut self, signature: Option<String>) {
|
| 323 |
+
self.signatures.store(signature);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
/// 设置 trailing signature
|
| 327 |
+
pub fn set_trailing_signature(&mut self, signature: Option<String>) {
|
| 328 |
+
self.trailing_signature = signature;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
/// 获取 trailing signature (仅用于检查)
|
| 332 |
+
pub fn has_trailing_signature(&self) -> bool {
|
| 333 |
+
self.trailing_signature.is_some()
|
| 334 |
+
}
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/// Part 处理器
|
| 338 |
+
pub struct PartProcessor<'a> {
|
| 339 |
+
state: &'a mut StreamingState,
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
impl<'a> PartProcessor<'a> {
|
| 343 |
+
pub fn new(state: &'a mut StreamingState) -> Self {
|
| 344 |
+
Self { state }
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
/// 处理单个 part
|
| 348 |
+
pub fn process(&mut self, part: &GeminiPart) -> Vec<Bytes> {
|
| 349 |
+
let mut chunks = Vec::new();
|
| 350 |
+
let signature = part.thought_signature.clone();
|
| 351 |
+
|
| 352 |
+
// 1. FunctionCall 处理
|
| 353 |
+
if let Some(fc) = &part.function_call {
|
| 354 |
+
// 先处理 trailingSignature (B4/C3 场景)
|
| 355 |
+
if self.state.has_trailing_signature() {
|
| 356 |
+
chunks.extend(self.state.end_block());
|
| 357 |
+
if let Some(trailing_sig) = self.state.trailing_signature.take() {
|
| 358 |
+
chunks.push(self.state.emit(
|
| 359 |
+
"content_block_start",
|
| 360 |
+
json!({
|
| 361 |
+
"type": "content_block_start",
|
| 362 |
+
"index": self.state.current_block_index(),
|
| 363 |
+
"content_block": { "type": "thinking", "thinking": "" }
|
| 364 |
+
}),
|
| 365 |
+
));
|
| 366 |
+
chunks.push(
|
| 367 |
+
self.state
|
| 368 |
+
.emit_delta("thinking_delta", json!({ "thinking": "" })),
|
| 369 |
+
);
|
| 370 |
+
chunks.push(
|
| 371 |
+
self.state
|
| 372 |
+
.emit_delta("signature_delta", json!({ "signature": trailing_sig })),
|
| 373 |
+
);
|
| 374 |
+
chunks.extend(self.state.end_block());
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
chunks.extend(self.process_function_call(fc, signature));
|
| 379 |
+
return chunks;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
// 2. Text 处理
|
| 383 |
+
if let Some(text) = &part.text {
|
| 384 |
+
if part.thought.unwrap_or(false) {
|
| 385 |
+
// Thinking
|
| 386 |
+
chunks.extend(self.process_thinking(text, signature));
|
| 387 |
+
} else {
|
| 388 |
+
// 普通 Text
|
| 389 |
+
chunks.extend(self.process_text(text, signature));
|
| 390 |
+
}
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
// 3. InlineData (Image) 处理
|
| 394 |
+
if let Some(img) = &part.inline_data {
|
| 395 |
+
let mime_type = &img.mime_type;
|
| 396 |
+
let data = &img.data;
|
| 397 |
+
if !data.is_empty() {
|
| 398 |
+
let markdown_img = format!("", mime_type, data);
|
| 399 |
+
chunks.extend(self.process_text(&markdown_img, None));
|
| 400 |
+
}
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
chunks
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
/// 处理 Thinking
|
| 407 |
+
fn process_thinking(&mut self, text: &str, signature: Option<String>) -> Vec<Bytes> {
|
| 408 |
+
let mut chunks = Vec::new();
|
| 409 |
+
|
| 410 |
+
// 处理之前的 trailingSignature
|
| 411 |
+
if self.state.has_trailing_signature() {
|
| 412 |
+
chunks.extend(self.state.end_block());
|
| 413 |
+
if let Some(trailing_sig) = self.state.trailing_signature.take() {
|
| 414 |
+
chunks.push(self.state.emit(
|
| 415 |
+
"content_block_start",
|
| 416 |
+
json!({
|
| 417 |
+
"type": "content_block_start",
|
| 418 |
+
"index": self.state.current_block_index(),
|
| 419 |
+
"content_block": { "type": "thinking", "thinking": "" }
|
| 420 |
+
}),
|
| 421 |
+
));
|
| 422 |
+
chunks.push(
|
| 423 |
+
self.state
|
| 424 |
+
.emit_delta("thinking_delta", json!({ "thinking": "" })),
|
| 425 |
+
);
|
| 426 |
+
chunks.push(
|
| 427 |
+
self.state
|
| 428 |
+
.emit_delta("signature_delta", json!({ "signature": trailing_sig })),
|
| 429 |
+
);
|
| 430 |
+
chunks.extend(self.state.end_block());
|
| 431 |
+
}
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
// 开始或继续 thinking 块
|
| 435 |
+
if self.state.current_block_type() != BlockType::Thinking {
|
| 436 |
+
chunks.extend(self.state.start_block(
|
| 437 |
+
BlockType::Thinking,
|
| 438 |
+
json!({ "type": "thinking", "thinking": "" }),
|
| 439 |
+
));
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
if !text.is_empty() {
|
| 443 |
+
chunks.push(
|
| 444 |
+
self.state
|
| 445 |
+
.emit_delta("thinking_delta", json!({ "thinking": text })),
|
| 446 |
+
);
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
// 暂存签名
|
| 450 |
+
self.state.store_signature(signature);
|
| 451 |
+
|
| 452 |
+
chunks
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
/// 处理普通 Text
|
| 456 |
+
fn process_text(&mut self, text: &str, signature: Option<String>) -> Vec<Bytes> {
|
| 457 |
+
let mut chunks = Vec::new();
|
| 458 |
+
|
| 459 |
+
// 空 text 带签名 - 暂存
|
| 460 |
+
if text.is_empty() {
|
| 461 |
+
if signature.is_some() {
|
| 462 |
+
self.state.set_trailing_signature(signature);
|
| 463 |
+
}
|
| 464 |
+
return chunks;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
// 处理之前的 trailingSignature
|
| 468 |
+
if self.state.has_trailing_signature() {
|
| 469 |
+
chunks.extend(self.state.end_block());
|
| 470 |
+
if let Some(trailing_sig) = self.state.trailing_signature.take() {
|
| 471 |
+
chunks.push(self.state.emit(
|
| 472 |
+
"content_block_start",
|
| 473 |
+
json!({
|
| 474 |
+
"type": "content_block_start",
|
| 475 |
+
"index": self.state.current_block_index(),
|
| 476 |
+
"content_block": { "type": "thinking", "thinking": "" }
|
| 477 |
+
}),
|
| 478 |
+
));
|
| 479 |
+
chunks.push(
|
| 480 |
+
self.state
|
| 481 |
+
.emit_delta("thinking_delta", json!({ "thinking": "" })),
|
| 482 |
+
);
|
| 483 |
+
chunks.push(
|
| 484 |
+
self.state
|
| 485 |
+
.emit_delta("signature_delta", json!({ "signature": trailing_sig })),
|
| 486 |
+
);
|
| 487 |
+
chunks.extend(self.state.end_block());
|
| 488 |
+
}
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
// 非空 text 带签名 - 立即处理
|
| 492 |
+
if signature.is_some() {
|
| 493 |
+
// 2. 开始新 text 块并发送内容
|
| 494 |
+
chunks.extend(
|
| 495 |
+
self.state
|
| 496 |
+
.start_block(BlockType::Text, json!({ "type": "text", "text": "" })),
|
| 497 |
+
);
|
| 498 |
+
chunks.push(self.state.emit_delta("text_delta", json!({ "text": text })));
|
| 499 |
+
chunks.extend(self.state.end_block());
|
| 500 |
+
|
| 501 |
+
// 输出空 thinking 块承载签名
|
| 502 |
+
chunks.push(self.state.emit(
|
| 503 |
+
"content_block_start",
|
| 504 |
+
json!({
|
| 505 |
+
"type": "content_block_start",
|
| 506 |
+
"index": self.state.current_block_index(),
|
| 507 |
+
"content_block": { "type": "thinking", "thinking": "" }
|
| 508 |
+
}),
|
| 509 |
+
));
|
| 510 |
+
chunks.push(
|
| 511 |
+
self.state
|
| 512 |
+
.emit_delta("thinking_delta", json!({ "thinking": "" })),
|
| 513 |
+
);
|
| 514 |
+
chunks.push(self.state.emit_delta(
|
| 515 |
+
"signature_delta",
|
| 516 |
+
json!({ "signature": signature.unwrap() }),
|
| 517 |
+
));
|
| 518 |
+
chunks.extend(self.state.end_block());
|
| 519 |
+
|
| 520 |
+
return chunks;
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
// 普通 text (无签名)
|
| 524 |
+
if self.state.current_block_type() != BlockType::Text {
|
| 525 |
+
chunks.extend(
|
| 526 |
+
self.state
|
| 527 |
+
.start_block(BlockType::Text, json!({ "type": "text", "text": "" })),
|
| 528 |
+
);
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
chunks.push(self.state.emit_delta("text_delta", json!({ "text": text })));
|
| 532 |
+
|
| 533 |
+
chunks
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
/// Process FunctionCall and capture signature for global storage
|
| 537 |
+
fn process_function_call(
|
| 538 |
+
&mut self,
|
| 539 |
+
fc: &FunctionCall,
|
| 540 |
+
signature: Option<String>,
|
| 541 |
+
) -> Vec<Bytes> {
|
| 542 |
+
let mut chunks = Vec::new();
|
| 543 |
+
|
| 544 |
+
self.state.mark_tool_used();
|
| 545 |
+
|
| 546 |
+
let tool_id = fc.id.clone().unwrap_or_else(|| {
|
| 547 |
+
format!(
|
| 548 |
+
"{}-{}",
|
| 549 |
+
fc.name,
|
| 550 |
+
crate::proxy::common::utils::generate_random_id()
|
| 551 |
+
)
|
| 552 |
+
});
|
| 553 |
+
|
| 554 |
+
// 1. 发送 content_block_start (input 为空对象)
|
| 555 |
+
let mut tool_use = json!({
|
| 556 |
+
"type": "tool_use",
|
| 557 |
+
"id": tool_id,
|
| 558 |
+
"name": fc.name,
|
| 559 |
+
"input": {} // 必须为空,参数通过 delta 发送
|
| 560 |
+
});
|
| 561 |
+
|
| 562 |
+
if let Some(ref sig) = signature {
|
| 563 |
+
tool_use["signature"] = json!(sig);
|
| 564 |
+
// Store signature to global storage for replay in subsequent requests
|
| 565 |
+
store_thought_signature(sig);
|
| 566 |
+
tracing::info!(
|
| 567 |
+
"[Claude-SSE] Captured thought_signature for function call (length: {})",
|
| 568 |
+
sig.len()
|
| 569 |
+
);
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
chunks.extend(self.state.start_block(BlockType::Function, tool_use));
|
| 573 |
+
|
| 574 |
+
// 2. 发送 input_json_delta (完整的参数 JSON 字符串)
|
| 575 |
+
if let Some(args) = &fc.args {
|
| 576 |
+
let json_str = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string());
|
| 577 |
+
chunks.push(
|
| 578 |
+
self.state
|
| 579 |
+
.emit_delta("input_json_delta", json!({ "partial_json": json_str })),
|
| 580 |
+
);
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
// 3. 结束块
|
| 584 |
+
chunks.extend(self.state.end_block());
|
| 585 |
+
|
| 586 |
+
chunks
|
| 587 |
+
}
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
#[cfg(test)]
|
| 591 |
+
mod tests {
|
| 592 |
+
use super::*;
|
| 593 |
+
|
| 594 |
+
#[test]
|
| 595 |
+
fn test_signature_manager() {
|
| 596 |
+
let mut mgr = SignatureManager::new();
|
| 597 |
+
assert!(!mgr.has_pending());
|
| 598 |
+
|
| 599 |
+
mgr.store(Some("sig123".to_string()));
|
| 600 |
+
assert!(mgr.has_pending());
|
| 601 |
+
|
| 602 |
+
let sig = mgr.consume();
|
| 603 |
+
assert_eq!(sig, Some("sig123".to_string()));
|
| 604 |
+
assert!(!mgr.has_pending());
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
#[test]
|
| 608 |
+
fn test_streaming_state_emit() {
|
| 609 |
+
let state = StreamingState::new();
|
| 610 |
+
let chunk = state.emit("test_event", json!({"foo": "bar"}));
|
| 611 |
+
|
| 612 |
+
let s = String::from_utf8(chunk.to_vec()).unwrap();
|
| 613 |
+
assert!(s.contains("event: test_event"));
|
| 614 |
+
assert!(s.contains("\"foo\":\"bar\""));
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
#[test]
|
| 618 |
+
fn test_process_function_call_deltas() {
|
| 619 |
+
let mut state = StreamingState::new();
|
| 620 |
+
let mut processor = PartProcessor::new(&mut state);
|
| 621 |
+
|
| 622 |
+
let fc = FunctionCall {
|
| 623 |
+
name: "test_tool".to_string(),
|
| 624 |
+
args: Some(json!({"arg": "value"})),
|
| 625 |
+
id: Some("call_123".to_string()),
|
| 626 |
+
};
|
| 627 |
+
|
| 628 |
+
// Create a dummy GeminiPart with function_call
|
| 629 |
+
let part = GeminiPart {
|
| 630 |
+
text: None,
|
| 631 |
+
function_call: Some(fc),
|
| 632 |
+
inline_data: None,
|
| 633 |
+
thought: None,
|
| 634 |
+
thought_signature: None,
|
| 635 |
+
function_response: None,
|
| 636 |
+
};
|
| 637 |
+
|
| 638 |
+
let chunks = processor.process(&part);
|
| 639 |
+
let output = chunks
|
| 640 |
+
.iter()
|
| 641 |
+
.map(|b| String::from_utf8(b.to_vec()).unwrap())
|
| 642 |
+
.collect::<Vec<_>>()
|
| 643 |
+
.join("");
|
| 644 |
+
|
| 645 |
+
// Verify sequence:
|
| 646 |
+
// 1. content_block_start with empty input
|
| 647 |
+
assert!(output.contains(r#""type":"content_block_start""#));
|
| 648 |
+
assert!(output.contains(r#""name":"test_tool""#));
|
| 649 |
+
assert!(output.contains(r#""input":{}"#));
|
| 650 |
+
|
| 651 |
+
// 2. input_json_delta with serialized args
|
| 652 |
+
assert!(output.contains(r#""type":"content_block_delta""#));
|
| 653 |
+
assert!(output.contains(r#""type":"input_json_delta""#));
|
| 654 |
+
// partial_json should contain escaped JSON string
|
| 655 |
+
assert!(output.contains(r#"partial_json":"{\"arg\":\"value\"}"#));
|
| 656 |
+
|
| 657 |
+
// 3. content_block_stop
|
| 658 |
+
assert!(output.contains(r#""type":"content_block_stop""#));
|
| 659 |
+
}
|
| 660 |
+
}
|
server/src/proxy/mappers/claude/utils.rs
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Claude 辅助函数
|
| 2 |
+
// JSON Schema 清理、签名处理等
|
| 3 |
+
|
| 4 |
+
// 已移除未使用的 Value 导入
|
| 5 |
+
|
| 6 |
+
/// 将 JSON Schema 中的类型名称转为大写 (Gemini 要求)
|
| 7 |
+
/// 例如: "string" -> "STRING", "integer" -> "INTEGER"
|
| 8 |
+
// 已移除未使用的 uppercase_schema_types 函数
|
| 9 |
+
|
| 10 |
+
/// 从 Gemini UsageMetadata 转换为 Claude Usage
|
| 11 |
+
pub fn to_claude_usage(usage_metadata: &super::models::UsageMetadata) -> super::models::Usage {
|
| 12 |
+
super::models::Usage {
|
| 13 |
+
input_tokens: usage_metadata.prompt_token_count.unwrap_or(0),
|
| 14 |
+
output_tokens: usage_metadata.candidates_token_count.unwrap_or(0),
|
| 15 |
+
server_tool_use: None,
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/// 提取 thoughtSignature
|
| 20 |
+
// 已移除未使用的 extract_thought_signature 函数
|
| 21 |
+
|
| 22 |
+
#[cfg(test)]
|
| 23 |
+
mod tests {
|
| 24 |
+
use super::*;
|
| 25 |
+
// 移除了未使用的 serde_json::json
|
| 26 |
+
|
| 27 |
+
// 已移除对 uppercase_schema_types 的过期测试
|
| 28 |
+
|
| 29 |
+
#[test]
|
| 30 |
+
fn test_to_claude_usage() {
|
| 31 |
+
use super::super::models::UsageMetadata;
|
| 32 |
+
|
| 33 |
+
let usage = UsageMetadata {
|
| 34 |
+
prompt_token_count: Some(100),
|
| 35 |
+
candidates_token_count: Some(50),
|
| 36 |
+
total_token_count: Some(150),
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
let claude_usage = to_claude_usage(&usage);
|
| 40 |
+
assert_eq!(claude_usage.input_tokens, 100);
|
| 41 |
+
assert_eq!(claude_usage.output_tokens, 50);
|
| 42 |
+
}
|
| 43 |
+
}
|
server/src/proxy/mappers/common_utils.rs
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Common utilities for request mapping across all protocols
|
| 2 |
+
// Provides unified grounding/networking logic
|
| 3 |
+
|
| 4 |
+
use serde_json::{json, Value};
|
| 5 |
+
|
| 6 |
+
/// Request configuration after grounding resolution
|
| 7 |
+
#[derive(Debug, Clone)]
|
| 8 |
+
pub struct RequestConfig {
|
| 9 |
+
/// The request type: "agent", "web_search", or "image_gen"
|
| 10 |
+
pub request_type: String,
|
| 11 |
+
/// Whether to inject the googleSearch tool
|
| 12 |
+
pub inject_google_search: bool,
|
| 13 |
+
/// The final model name (with suffixes stripped)
|
| 14 |
+
pub final_model: String,
|
| 15 |
+
/// Image generation configuration (if request_type is image_gen)
|
| 16 |
+
pub image_config: Option<Value>,
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
pub fn resolve_request_config(
|
| 20 |
+
original_model: &str,
|
| 21 |
+
mapped_model: &str,
|
| 22 |
+
tools: &Option<Vec<Value>>
|
| 23 |
+
) -> RequestConfig {
|
| 24 |
+
// 1. Image Generation Check (Priority)
|
| 25 |
+
if mapped_model.starts_with("gemini-3-pro-image") {
|
| 26 |
+
let (image_config, parsed_base_model) = parse_image_config(original_model);
|
| 27 |
+
|
| 28 |
+
return RequestConfig {
|
| 29 |
+
request_type: "image_gen".to_string(),
|
| 30 |
+
inject_google_search: false,
|
| 31 |
+
final_model: parsed_base_model,
|
| 32 |
+
image_config: Some(image_config),
|
| 33 |
+
};
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// 检测是否有联网工具定义 (内置功能调用)
|
| 37 |
+
let has_networking_tool = detects_networking_tool(tools);
|
| 38 |
+
// 检测是否包含非联网工具 (如 MCP 本地工具)
|
| 39 |
+
let has_non_networking = contains_non_networking_tool(tools);
|
| 40 |
+
|
| 41 |
+
// Strip -online suffix from original model if present (to detect networking intent)
|
| 42 |
+
let is_online_suffix = original_model.ends_with("-online");
|
| 43 |
+
|
| 44 |
+
// High-quality grounding allowlist (Only for models known to support search and be relatively 'safe')
|
| 45 |
+
let is_high_quality_model = mapped_model == "gemini-2.5-flash"
|
| 46 |
+
|| mapped_model == "gemini-1.5-pro"
|
| 47 |
+
|| mapped_model.starts_with("gemini-1.5-pro-")
|
| 48 |
+
|| mapped_model.starts_with("gemini-2.5-flash-")
|
| 49 |
+
|| mapped_model.starts_with("gemini-2.0-flash")
|
| 50 |
+
|| mapped_model.starts_with("gemini-3-")
|
| 51 |
+
|| mapped_model.contains("claude-3-5-sonnet")
|
| 52 |
+
|| mapped_model.contains("claude-3-opus")
|
| 53 |
+
|| mapped_model.contains("claude-sonnet")
|
| 54 |
+
|| mapped_model.contains("claude-opus")
|
| 55 |
+
|| mapped_model.contains("claude-4");
|
| 56 |
+
|
| 57 |
+
// Determine if we should enable networking
|
| 58 |
+
// [FIX] 禁用基于模型的自动联网逻辑,防止图像请求被联网搜索结果覆盖。
|
| 59 |
+
// 仅在用户显式请求联网时启用:1) -online 后缀 2) 携带联网工具定义
|
| 60 |
+
let enable_networking = is_online_suffix || has_networking_tool;
|
| 61 |
+
|
| 62 |
+
// The final model to send upstream should be the MAPPED model,
|
| 63 |
+
// but if searching, we MUST ensure the model name is one the backend associates with search.
|
| 64 |
+
// Based on ref_Antigravity2Api practice, we force a stable search model for search requests.
|
| 65 |
+
let mut final_model = mapped_model.trim_end_matches("-online").to_string();
|
| 66 |
+
if enable_networking {
|
| 67 |
+
// If it's a thinking model (which doesn't support tools) or a Claude-style alias,
|
| 68 |
+
// fallback to gemini-2.5-flash which is the standard workhorse for search.
|
| 69 |
+
if final_model.contains("thinking") || !final_model.starts_with("gemini-") {
|
| 70 |
+
final_model = "gemini-2.5-flash".to_string();
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
RequestConfig {
|
| 75 |
+
request_type: if enable_networking {
|
| 76 |
+
"web_search".to_string()
|
| 77 |
+
} else {
|
| 78 |
+
"agent".to_string()
|
| 79 |
+
},
|
| 80 |
+
inject_google_search: enable_networking,
|
| 81 |
+
final_model,
|
| 82 |
+
image_config: None,
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/// Parse image configuration from model name suffixes
|
| 87 |
+
/// Returns (image_config, clean_model_name)
|
| 88 |
+
fn parse_image_config(model_name: &str) -> (Value, String) {
|
| 89 |
+
let mut aspect_ratio = "1:1";
|
| 90 |
+
let _image_size = "1024x1024"; // Default, not explicitly sent unless 4k/hd
|
| 91 |
+
|
| 92 |
+
if model_name.contains("-16x9") { aspect_ratio = "16:9"; }
|
| 93 |
+
else if model_name.contains("-9x16") { aspect_ratio = "9:16"; }
|
| 94 |
+
else if model_name.contains("-4x3") { aspect_ratio = "4:3"; }
|
| 95 |
+
else if model_name.contains("-3x4") { aspect_ratio = "3:4"; }
|
| 96 |
+
else if model_name.contains("-1x1") { aspect_ratio = "1:1"; }
|
| 97 |
+
|
| 98 |
+
let is_hd = model_name.contains("-4k") || model_name.contains("-hd");
|
| 99 |
+
|
| 100 |
+
let mut config = serde_json::Map::new();
|
| 101 |
+
config.insert("aspectRatio".to_string(), json!(aspect_ratio));
|
| 102 |
+
|
| 103 |
+
if is_hd {
|
| 104 |
+
config.insert("imageSize".to_string(), json!("4K"));
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// The upstream model must be EXACTLY "gemini-3-pro-image"
|
| 108 |
+
(serde_json::Value::Object(config), "gemini-3-pro-image".to_string())
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/// Inject current googleSearch tool and ensure no duplicate legacy search tools
|
| 112 |
+
pub fn inject_google_search_tool(body: &mut Value) {
|
| 113 |
+
if let Some(obj) = body.as_object_mut() {
|
| 114 |
+
let tools_entry = obj.entry("tools").or_insert_with(|| json!([]));
|
| 115 |
+
if let Some(tools_arr) = tools_entry.as_array_mut() {
|
| 116 |
+
// [安全校验] 如果数组中已经包含 functionDeclarations,���禁注入 googleSearch
|
| 117 |
+
// 因为 Gemini v1internal 不支持在一次请求中混用 search 和 functions
|
| 118 |
+
let has_functions = tools_arr.iter().any(|t| {
|
| 119 |
+
t.as_object().map_or(false, |o| o.contains_key("functionDeclarations"))
|
| 120 |
+
});
|
| 121 |
+
|
| 122 |
+
if has_functions {
|
| 123 |
+
tracing::info!("Skipping googleSearch injection due to existing functionDeclarations");
|
| 124 |
+
return;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// 首先清理掉已存在的 googleSearch 或 googleSearchRetrieval,以防重复产生冲突
|
| 128 |
+
tools_arr.retain(|t| {
|
| 129 |
+
if let Some(o) = t.as_object() {
|
| 130 |
+
!(o.contains_key("googleSearch") || o.contains_key("googleSearchRetrieval"))
|
| 131 |
+
} else {
|
| 132 |
+
true
|
| 133 |
+
}
|
| 134 |
+
});
|
| 135 |
+
|
| 136 |
+
// 注入统一的 googleSearch (v1internal 规范)
|
| 137 |
+
tools_arr.push(json!({
|
| 138 |
+
"googleSearch": {}
|
| 139 |
+
}));
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
/// 深度迭代清理客户端发送的 [undefined] 脏字符串,防止 Gemini 接口校验失败
|
| 145 |
+
pub fn deep_clean_undefined(value: &mut Value) {
|
| 146 |
+
match value {
|
| 147 |
+
Value::Object(map) => {
|
| 148 |
+
// 移除值为 "[undefined]" 的键
|
| 149 |
+
map.retain(|_, v| {
|
| 150 |
+
if let Some(s) = v.as_str() {
|
| 151 |
+
s != "[undefined]"
|
| 152 |
+
} else {
|
| 153 |
+
true
|
| 154 |
+
}
|
| 155 |
+
});
|
| 156 |
+
// 递归处理嵌套
|
| 157 |
+
for v in map.values_mut() {
|
| 158 |
+
deep_clean_undefined(v);
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
Value::Array(arr) => {
|
| 162 |
+
for v in arr.iter_mut() {
|
| 163 |
+
deep_clean_undefined(v);
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
_ => {}
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
/// Detects if the tool list contains a request for networking/web search.
|
| 171 |
+
/// Supported keywords: "web_search", "google_search", "web_search_20250305"
|
| 172 |
+
pub fn detects_networking_tool(tools: &Option<Vec<Value>>) -> bool {
|
| 173 |
+
if let Some(list) = tools {
|
| 174 |
+
for tool in list {
|
| 175 |
+
// 1. 直发风格 (Claude/Simple OpenAI/Anthropic Builtin/Vertex): { "name": "..." } 或 { "type": "..." }
|
| 176 |
+
if let Some(n) = tool.get("name").and_then(|v| v.as_str()) {
|
| 177 |
+
if n == "web_search" || n == "google_search" || n == "web_search_20250305" || n == "google_search_retrieval" {
|
| 178 |
+
return true;
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
if let Some(t) = tool.get("type").and_then(|v| v.as_str()) {
|
| 183 |
+
if t == "web_search_20250305" || t == "google_search" || t == "web_search" || t == "google_search_retrieval" {
|
| 184 |
+
return true;
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
// 2. OpenAI 嵌套风格: { "type": "function", "function": { "name": "..." } }
|
| 189 |
+
if let Some(func) = tool.get("function") {
|
| 190 |
+
if let Some(n) = func.get("name").and_then(|v| v.as_str()) {
|
| 191 |
+
let keywords = ["web_search", "google_search", "web_search_20250305", "google_search_retrieval"];
|
| 192 |
+
if keywords.contains(&n) {
|
| 193 |
+
return true;
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
// 3. Gemini 原生风格: { "functionDeclarations": [ { "name": "..." } ] }
|
| 199 |
+
if let Some(decls) = tool.get("functionDeclarations").and_then(|v| v.as_array()) {
|
| 200 |
+
for decl in decls {
|
| 201 |
+
if let Some(n) = decl.get("name").and_then(|v| v.as_str()) {
|
| 202 |
+
if n == "web_search" || n == "google_search" || n == "google_search_retrieval" {
|
| 203 |
+
return true;
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
// 4. Gemini googleSearch 声明 (含 googleSearchRetrieval 变体)
|
| 210 |
+
if tool.get("googleSearch").is_some() || tool.get("googleSearchRetrieval").is_some() {
|
| 211 |
+
return true;
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
false
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
/// 探测是否包含非联网相关的本地函数工具
|
| 219 |
+
pub fn contains_non_networking_tool(tools: &Option<Vec<Value>>) -> bool {
|
| 220 |
+
if let Some(list) = tools {
|
| 221 |
+
for tool in list {
|
| 222 |
+
let mut is_networking = false;
|
| 223 |
+
|
| 224 |
+
// 简单逻辑:如果它是一个函数声明且名字不是联网关键词,则视为非联网工具
|
| 225 |
+
if let Some(n) = tool.get("name").and_then(|v| v.as_str()) {
|
| 226 |
+
let keywords = ["web_search", "google_search", "web_search_20250305", "google_search_retrieval"];
|
| 227 |
+
if keywords.contains(&n) { is_networking = true; }
|
| 228 |
+
} else if let Some(func) = tool.get("function") {
|
| 229 |
+
if let Some(n) = func.get("name").and_then(|v| v.as_str()) {
|
| 230 |
+
let keywords = ["web_search", "google_search", "web_search_20250305", "google_search_retrieval"];
|
| 231 |
+
if keywords.contains(&n) { is_networking = true; }
|
| 232 |
+
}
|
| 233 |
+
} else if tool.get("googleSearch").is_some() || tool.get("googleSearchRetrieval").is_some() {
|
| 234 |
+
is_networking = true;
|
| 235 |
+
} else if tool.get("functionDeclarations").is_some() {
|
| 236 |
+
// 如果是 Gemini 风格的 functionDeclarations,进去看一眼
|
| 237 |
+
if let Some(decls) = tool.get("functionDeclarations").and_then(|v| v.as_array()) {
|
| 238 |
+
for decl in decls {
|
| 239 |
+
if let Some(n) = decl.get("name").and_then(|v| v.as_str()) {
|
| 240 |
+
let keywords = ["web_search", "google_search", "google_search_retrieval"];
|
| 241 |
+
if !keywords.contains(&n) {
|
| 242 |
+
return true; // 发现本地函数
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
is_networking = true; // 即使全是联网,外层也标记为联网
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
if !is_networking {
|
| 251 |
+
return true;
|
| 252 |
+
}
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
false
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
#[cfg(test)]
|
| 259 |
+
mod tests {
|
| 260 |
+
use super::*;
|
| 261 |
+
|
| 262 |
+
#[test]
|
| 263 |
+
fn test_high_quality_model_auto_grounding() {
|
| 264 |
+
let config = resolve_request_config("gpt-4o", "gemini-2.5-flash", &None);
|
| 265 |
+
assert_eq!(config.request_type, "web_search");
|
| 266 |
+
assert!(config.inject_google_search);
|
| 267 |
+
assert_eq!(config.final_model, "gemini-2.5-flash"); // 修正断言: final_model = mapped_model
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
#[test]
|
| 271 |
+
fn test_gemini_native_tool_detection() {
|
| 272 |
+
let tools = Some(vec![json!({
|
| 273 |
+
"functionDeclarations": [
|
| 274 |
+
{ "name": "web_search", "parameters": {} }
|
| 275 |
+
]
|
| 276 |
+
})]);
|
| 277 |
+
assert!(detects_networking_tool(&tools));
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
#[test]
|
| 281 |
+
fn test_online_suffix_force_grounding() {
|
| 282 |
+
let config = resolve_request_config("gemini-3-flash-online", "gemini-3-flash", &None);
|
| 283 |
+
assert_eq!(config.request_type, "web_search");
|
| 284 |
+
assert!(config.inject_google_search);
|
| 285 |
+
assert_eq!(config.final_model, "gemini-3-flash");
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
#[test]
|
| 289 |
+
fn test_default_no_grounding() {
|
| 290 |
+
let config = resolve_request_config("claude-sonnet", "gemini-3-flash", &None);
|
| 291 |
+
assert_eq!(config.request_type, "agent");
|
| 292 |
+
assert!(!config.inject_google_search);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
#[test]
|
| 296 |
+
fn test_image_model_excluded() {
|
| 297 |
+
let config = resolve_request_config("gemini-3-pro-image", "gemini-3-pro-image", &None);
|
| 298 |
+
assert_eq!(config.request_type, "image_gen");
|
| 299 |
+
assert!(!config.inject_google_search);
|
| 300 |
+
}
|
| 301 |
+
}
|
server/src/proxy/mappers/gemini/mod.rs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Gemini mapper 模块
|
| 2 |
+
// 负责 v1internal 包装/解包
|
| 3 |
+
|
| 4 |
+
pub mod models;
|
| 5 |
+
pub mod wrapper;
|
| 6 |
+
|
| 7 |
+
// No public exports needed here if unused
|
| 8 |
+
pub use wrapper::*;
|
server/src/proxy/mappers/gemini/models.rs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Gemini v1internal 数据模型
|
| 2 |
+
use serde::{Deserialize, Serialize};
|
| 3 |
+
|
| 4 |
+
#[allow(dead_code)]
|
| 5 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 6 |
+
pub struct V1InternalRequest {
|
| 7 |
+
pub project: String,
|
| 8 |
+
#[serde(rename = "requestId")]
|
| 9 |
+
pub request_id: String,
|
| 10 |
+
pub request: serde_json::Value,
|
| 11 |
+
pub model: String,
|
| 12 |
+
#[serde(rename = "userAgent")]
|
| 13 |
+
pub user_agent: String,
|
| 14 |
+
#[serde(rename = "requestType")]
|
| 15 |
+
pub request_type: String,
|
| 16 |
+
}
|
server/src/proxy/mappers/gemini/wrapper.rs
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Gemini v1internal 包装/解包
|
| 2 |
+
use serde_json::{json, Value};
|
| 3 |
+
|
| 4 |
+
/// 包装请求体为 v1internal 格式
|
| 5 |
+
pub fn wrap_request(body: &Value, project_id: &str, mapped_model: &str) -> Value {
|
| 6 |
+
// 优先使用传入的 mapped_model,其次尝试从 body 获取
|
| 7 |
+
let original_model = body.get("model").and_then(|v| v.as_str()).unwrap_or(mapped_model);
|
| 8 |
+
|
| 9 |
+
// 如果 mapped_model 是空的,则使用 original_model
|
| 10 |
+
let final_model_name = if !mapped_model.is_empty() {
|
| 11 |
+
mapped_model
|
| 12 |
+
} else {
|
| 13 |
+
original_model
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
// 复制 body 以便修改
|
| 17 |
+
let mut inner_request = body.clone();
|
| 18 |
+
|
| 19 |
+
// 深度清理 [undefined] 字符串 (Cherry Studio 等客户端常见注入)
|
| 20 |
+
crate::proxy::mappers::common_utils::deep_clean_undefined(&mut inner_request);
|
| 21 |
+
|
| 22 |
+
// 强制设置 Gemini v1internal 的最大输出 token 数
|
| 23 |
+
if let Some(obj) = inner_request.as_object_mut() {
|
| 24 |
+
let gen_config = obj.entry("generationConfig").or_insert_with(|| json!({}));
|
| 25 |
+
if let Some(gen_obj) = gen_config.as_object_mut() {
|
| 26 |
+
gen_obj.insert("maxOutputTokens".to_string(), json!(64000)); // Sync with others
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
// 提取 tools 列表以进行联网探测 (Gemini 风格可能是嵌套的)
|
| 31 |
+
let tools_val: Option<Vec<Value>> = inner_request.get("tools").and_then(|t| t.as_array()).map(|arr| {
|
| 32 |
+
arr.clone()
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
// Use shared grounding/config logic
|
| 36 |
+
let config = crate::proxy::mappers::common_utils::resolve_request_config(original_model, final_model_name, &tools_val);
|
| 37 |
+
|
| 38 |
+
// Clean tool declarations (remove forbidden Schema fields like multipleOf, and remove redundant search decls)
|
| 39 |
+
if let Some(tools) = inner_request.get_mut("tools") {
|
| 40 |
+
if let Some(tools_arr) = tools.as_array_mut() {
|
| 41 |
+
for tool in tools_arr {
|
| 42 |
+
if let Some(decls) = tool.get_mut("functionDeclarations") {
|
| 43 |
+
if let Some(decls_arr) = decls.as_array_mut() {
|
| 44 |
+
// 1. 过滤掉联网关键字函数
|
| 45 |
+
decls_arr.retain(|decl| {
|
| 46 |
+
if let Some(name) = decl.get("name").and_then(|v| v.as_str()) {
|
| 47 |
+
if name == "web_search" || name == "google_search" {
|
| 48 |
+
return false;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
true
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
// 2. 清洗剩余 Schema
|
| 55 |
+
for decl in decls_arr {
|
| 56 |
+
if let Some(params) = decl.get_mut("parameters") {
|
| 57 |
+
crate::proxy::common::json_schema::clean_json_schema(params);
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
tracing::info!("[Debug] Gemini Wrap: original='{}', mapped='{}', final='{}', type='{}'",
|
| 67 |
+
original_model, final_model_name, config.final_model, config.request_type);
|
| 68 |
+
|
| 69 |
+
// Inject googleSearch tool if needed
|
| 70 |
+
if config.inject_google_search {
|
| 71 |
+
crate::proxy::mappers::common_utils::inject_google_search_tool(&mut inner_request);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Inject imageConfig if present (for image generation models)
|
| 75 |
+
if let Some(image_config) = config.image_config {
|
| 76 |
+
if let Some(obj) = inner_request.as_object_mut() {
|
| 77 |
+
// 1. Remove tools (image generation does not support tools)
|
| 78 |
+
obj.remove("tools");
|
| 79 |
+
|
| 80 |
+
// 2. Remove systemInstruction (image generation does not support system prompts)
|
| 81 |
+
obj.remove("systemInstruction");
|
| 82 |
+
|
| 83 |
+
// 3. Clean generationConfig (remove thinkingConfig, responseMimeType, responseModalities etc.)
|
| 84 |
+
let gen_config = obj.entry("generationConfig").or_insert_with(|| json!({}));
|
| 85 |
+
if let Some(gen_obj) = gen_config.as_object_mut() {
|
| 86 |
+
gen_obj.remove("thinkingConfig");
|
| 87 |
+
gen_obj.remove("responseMimeType");
|
| 88 |
+
gen_obj.remove("responseModalities"); // Cherry Studio sends this, might conflict
|
| 89 |
+
gen_obj.insert("imageConfig".to_string(), image_config);
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
let final_request = json!({
|
| 95 |
+
"project": project_id,
|
| 96 |
+
"requestId": format!("agent-{}", uuid::Uuid::new_v4()), // 修正为 agent- 前缀
|
| 97 |
+
"request": inner_request,
|
| 98 |
+
"model": config.final_model,
|
| 99 |
+
"userAgent": "antigravity",
|
| 100 |
+
"requestType": config.request_type
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
final_request
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
/// 解包响应(提取 response 字段)
|
| 107 |
+
pub fn unwrap_response(response: &Value) -> Value {
|
| 108 |
+
response.get("response").unwrap_or(response).clone()
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
#[cfg(test)]
|
| 112 |
+
mod tests {
|
| 113 |
+
use super::*;
|
| 114 |
+
|
| 115 |
+
#[test]
|
| 116 |
+
fn test_wrap_request() {
|
| 117 |
+
let body = json!({
|
| 118 |
+
"model": "gemini-2.5-flash",
|
| 119 |
+
"contents": [{"role": "user", "parts": [{"text": "Hi"}]}]
|
| 120 |
+
});
|
| 121 |
+
|
| 122 |
+
let result = wrap_request(&body, "test-project", "gemini-2.5-flash");
|
| 123 |
+
assert_eq!(result["project"], "test-project");
|
| 124 |
+
assert_eq!(result["model"], "gemini-2.5-flash");
|
| 125 |
+
assert!(result["requestId"].as_str().unwrap().starts_with("agent-"));
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
#[test]
|
| 129 |
+
fn test_unwrap_response() {
|
| 130 |
+
let wrapped = json!({
|
| 131 |
+
"response": {
|
| 132 |
+
"candidates": [{"content": {"parts": [{"text": "Hello"}]}}]
|
| 133 |
+
}
|
| 134 |
+
});
|
| 135 |
+
|
| 136 |
+
let result = unwrap_response(&wrapped);
|
| 137 |
+
assert!(result.get("candidates").is_some());
|
| 138 |
+
assert!(result.get("response").is_none());
|
| 139 |
+
}
|
| 140 |
+
}
|
server/src/proxy/mappers/mod.rs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Mappers 模块 - 协议转换器
|
| 2 |
+
// 协议转换器模块
|
| 3 |
+
|
| 4 |
+
pub mod claude;
|
| 5 |
+
pub mod common_utils;
|
| 6 |
+
pub mod gemini;
|
| 7 |
+
pub mod openai;
|
| 8 |
+
pub mod signature_store;
|
server/src/proxy/mappers/openai/mod.rs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// OpenAI mapper 模块
|
| 2 |
+
// 负责 OpenAI ↔ Gemini 协议转换
|
| 3 |
+
|
| 4 |
+
pub mod models;
|
| 5 |
+
pub mod request;
|
| 6 |
+
pub mod response;
|
| 7 |
+
pub mod streaming;
|
| 8 |
+
|
| 9 |
+
pub use models::*;
|
| 10 |
+
pub use request::*;
|
| 11 |
+
pub use response::*;
|
| 12 |
+
// No public exports needed here if unused
|
server/src/proxy/mappers/openai/models.rs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// OpenAI 数据模型
|
| 2 |
+
|
| 3 |
+
use serde::{Deserialize, Serialize};
|
| 4 |
+
use serde_json::Value;
|
| 5 |
+
|
| 6 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 7 |
+
pub struct OpenAIRequest {
|
| 8 |
+
pub model: String,
|
| 9 |
+
#[serde(default)]
|
| 10 |
+
pub messages: Vec<OpenAIMessage>,
|
| 11 |
+
#[serde(default)]
|
| 12 |
+
pub prompt: Option<String>,
|
| 13 |
+
#[serde(default)]
|
| 14 |
+
pub stream: bool,
|
| 15 |
+
#[serde(rename = "max_tokens")]
|
| 16 |
+
pub max_tokens: Option<u32>,
|
| 17 |
+
pub temperature: Option<f32>,
|
| 18 |
+
#[serde(rename = "top_p")]
|
| 19 |
+
pub top_p: Option<f32>,
|
| 20 |
+
pub stop: Option<Value>,
|
| 21 |
+
pub response_format: Option<ResponseFormat>,
|
| 22 |
+
#[serde(default)]
|
| 23 |
+
pub tools: Option<Vec<Value>>,
|
| 24 |
+
#[serde(rename = "tool_choice")]
|
| 25 |
+
pub tool_choice: Option<Value>,
|
| 26 |
+
#[serde(rename = "parallel_tool_calls")]
|
| 27 |
+
pub parallel_tool_calls: Option<bool>,
|
| 28 |
+
// Codex proprietary fields
|
| 29 |
+
pub instructions: Option<String>,
|
| 30 |
+
pub input: Option<Value>,
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 34 |
+
pub struct ResponseFormat {
|
| 35 |
+
pub r#type: String,
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
| 39 |
+
#[serde(untagged)]
|
| 40 |
+
pub enum OpenAIContent {
|
| 41 |
+
String(String),
|
| 42 |
+
Array(Vec<OpenAIContentBlock>),
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
| 46 |
+
#[serde(tag = "type")]
|
| 47 |
+
pub enum OpenAIContentBlock {
|
| 48 |
+
#[serde(rename = "text")]
|
| 49 |
+
Text {
|
| 50 |
+
text: String,
|
| 51 |
+
},
|
| 52 |
+
#[serde(rename = "image_url")]
|
| 53 |
+
ImageUrl {
|
| 54 |
+
image_url: OpenAIImageUrl,
|
| 55 |
+
},
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
| 59 |
+
pub struct OpenAIImageUrl {
|
| 60 |
+
pub url: String,
|
| 61 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 62 |
+
pub detail: Option<String>,
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 66 |
+
pub struct OpenAIMessage {
|
| 67 |
+
pub role: String,
|
| 68 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 69 |
+
pub content: Option<OpenAIContent>,
|
| 70 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 71 |
+
pub tool_calls: Option<Vec<ToolCall>>,
|
| 72 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 73 |
+
pub tool_call_id: Option<String>,
|
| 74 |
+
#[serde(skip_serializing_if = "Option::is_none")]
|
| 75 |
+
pub name: Option<String>,
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 79 |
+
pub struct ToolCall {
|
| 80 |
+
pub id: String,
|
| 81 |
+
pub r#type: String,
|
| 82 |
+
pub function: ToolFunction,
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 86 |
+
pub struct ToolFunction {
|
| 87 |
+
pub name: String,
|
| 88 |
+
pub arguments: String,
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 92 |
+
pub struct OpenAIResponse {
|
| 93 |
+
pub id: String,
|
| 94 |
+
pub object: String,
|
| 95 |
+
pub created: u64,
|
| 96 |
+
pub model: String,
|
| 97 |
+
pub choices: Vec<Choice>,
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
| 101 |
+
pub struct Choice {
|
| 102 |
+
pub index: u32,
|
| 103 |
+
pub message: OpenAIMessage,
|
| 104 |
+
pub finish_reason: Option<String>,
|
| 105 |
+
}
|