Upload 76 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +3 -0
- .github/workflows/main.yml +56 -0
- Dockerfile +20 -0
- Dockerfile_git +14 -0
- app/__init__.py +0 -0
- app/api/__init__.py +9 -0
- app/api/auth.py +14 -0
- app/api/client_disconnect.py +120 -0
- app/api/dashboard.py +182 -0
- app/api/gemini_handlers.py +59 -0
- app/api/nonstream_handlers.py +157 -0
- app/api/request_handlers.py +154 -0
- app/api/routes.py +217 -0
- app/api/stream_handlers.py +284 -0
- app/config/__init__.py +3 -0
- app/config/safety.py +49 -0
- app/config/settings.py +81 -0
- app/main.py +283 -0
- app/models/__init__.py +19 -0
- app/models/schemas.py +46 -0
- app/services/__init__.py +7 -0
- app/services/gemini.py +367 -0
- app/templates/__init__.py +1 -0
- app/templates/assets/favicon.ico +0 -0
- app/templates/assets/index.css +1 -0
- app/templates/assets/index.html +14 -0
- app/templates/assets/main.js +0 -0
- app/templates/index.html +15 -0
- app/utils/__init__.py +12 -0
- app/utils/api_key.py +88 -0
- app/utils/cache.py +137 -0
- app/utils/error_handling.py +120 -0
- app/utils/logging.py +72 -0
- app/utils/maintenance.py +96 -0
- app/utils/rate_limiting.py +36 -0
- app/utils/request.py +72 -0
- app/utils/response.py +59 -0
- app/utils/stats.py +185 -0
- app/utils/version.py +49 -0
- hajimiUI/.gitignore +30 -0
- hajimiUI/.vscode/extensions.json +3 -0
- hajimiUI/README.md +29 -0
- hajimiUI/build.js +56 -0
- hajimiUI/index.html +13 -0
- hajimiUI/jsconfig.json +8 -0
- hajimiUI/package-lock.json +2812 -0
- hajimiUI/package.json +23 -0
- hajimiUI/public/favicon.ico +0 -0
- hajimiUI/src/App.vue +14 -0
- hajimiUI/src/assets/base.css +136 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
wiki/img/files.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
wiki/img/settings.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
wiki/img/spaces.png filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/main.yml
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
on:
|
| 2 |
+
schedule:
|
| 3 |
+
- cron: '0 * * * *'
|
| 4 |
+
workflow_dispatch: # 支持手动触发
|
| 5 |
+
|
| 6 |
+
jobs:
|
| 7 |
+
build-release:
|
| 8 |
+
runs-on: ubuntu-latest
|
| 9 |
+
permissions:
|
| 10 |
+
contents: write
|
| 11 |
+
|
| 12 |
+
steps:
|
| 13 |
+
- name: Checkout code
|
| 14 |
+
uses: actions/checkout@v4
|
| 15 |
+
|
| 16 |
+
- name: Install GitHub CLI
|
| 17 |
+
run: |
|
| 18 |
+
sudo apt update && sudo apt install -y curl
|
| 19 |
+
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
| 20 |
+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
| 21 |
+
sudo apt update && sudo apt install -y gh
|
| 22 |
+
|
| 23 |
+
- name: Get version
|
| 24 |
+
id: version
|
| 25 |
+
run: |
|
| 26 |
+
VERSION=$(grep 'version=' version.txt | cut -d'=' -f2)
|
| 27 |
+
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
| 28 |
+
|
| 29 |
+
- name: Add build timestamp
|
| 30 |
+
run: date +%s > app/build_timestamp.txt
|
| 31 |
+
|
| 32 |
+
- name: Create app.zip (flat structure)
|
| 33 |
+
run: |
|
| 34 |
+
cd app && zip -r ../app.zip . # 使用`.`打包当前目录所有文件,不包含父目录
|
| 35 |
+
|
| 36 |
+
- name: Create release bundle
|
| 37 |
+
run: |
|
| 38 |
+
RELEASE_ZIP="hajimi-${{ steps.version.outputs.version }}.zip"
|
| 39 |
+
zip $RELEASE_ZIP app.zip Dockerfile requirements.txt version.txt
|
| 40 |
+
echo "RELEASE_ZIP=${RELEASE_ZIP}" >> $GITHUB_ENV
|
| 41 |
+
|
| 42 |
+
- name: Cleanup existing release
|
| 43 |
+
env:
|
| 44 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
| 45 |
+
run: |
|
| 46 |
+
if gh release view "${{ steps.version.outputs.version }}"; then
|
| 47 |
+
gh release delete "${{ steps.version.outputs.version }}" --yes
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
- name: Create new release
|
| 51 |
+
env:
|
| 52 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
| 53 |
+
run: |
|
| 54 |
+
gh release create "${{ steps.version.outputs.version }}" \
|
| 55 |
+
--title "hajimi-${{ steps.version.outputs.version }}" \
|
| 56 |
+
"${{ env.RELEASE_ZIP }}"
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# 安装 unzip 工具
|
| 6 |
+
RUN apt-get update && apt-get install -y unzip && rm -rf /var/lib/apt/lists/*
|
| 7 |
+
|
| 8 |
+
COPY app.zip .
|
| 9 |
+
COPY requirements.txt .
|
| 10 |
+
COPY version.txt .
|
| 11 |
+
RUN mkdir -p app
|
| 12 |
+
# 解压 app.zip 文件
|
| 13 |
+
RUN unzip app.zip -d app && rm app.zip
|
| 14 |
+
|
| 15 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 16 |
+
|
| 17 |
+
# 环境变量 (在 Hugging Face Spaces 中设置)
|
| 18 |
+
# ENV GEMINI_API_KEYS=your_key_1,your_key_2,your_key_3
|
| 19 |
+
|
| 20 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
Dockerfile_git
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY ./app /app/app
|
| 6 |
+
COPY requirements.txt .
|
| 7 |
+
COPY version.txt .
|
| 8 |
+
|
| 9 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 10 |
+
|
| 11 |
+
# 环境变量 (在 Hugging Face Spaces 中设置)
|
| 12 |
+
# ENV GEMINI_API_KEYS=your_key_1,your_key_2,your_key_3
|
| 13 |
+
|
| 14 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
app/__init__.py
ADDED
|
File without changes
|
app/api/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.api.routes import router, init_router
|
| 2 |
+
from app.api.dashboard import dashboard_router, init_dashboard_router
|
| 3 |
+
|
| 4 |
+
__all__ = [
|
| 5 |
+
'router',
|
| 6 |
+
'init_router',
|
| 7 |
+
'dashboard_router',
|
| 8 |
+
'init_dashboard_router'
|
| 9 |
+
]
|
app/api/auth.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import HTTPException, Request
|
| 2 |
+
|
| 3 |
+
# 密码验证依赖
|
| 4 |
+
async def verify_password(request: Request, PASSWORD: str = None):
|
| 5 |
+
"""验证请求中的Bearer令牌是否与配置的密码匹配"""
|
| 6 |
+
if PASSWORD:
|
| 7 |
+
auth_header = request.headers.get("Authorization")
|
| 8 |
+
if not auth_header or not auth_header.startswith("Bearer "):
|
| 9 |
+
raise HTTPException(
|
| 10 |
+
status_code=401, detail="Unauthorized: Missing or invalid token")
|
| 11 |
+
token = auth_header.split(" ")[1]
|
| 12 |
+
if token != PASSWORD:
|
| 13 |
+
raise HTTPException(
|
| 14 |
+
status_code=401, detail="Unauthorized: Invalid token")
|
app/api/client_disconnect.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import time
|
| 3 |
+
from fastapi import Request
|
| 4 |
+
from app.models import ChatCompletionRequest
|
| 5 |
+
from app.utils import create_error_response
|
| 6 |
+
from app.utils.logging import log
|
| 7 |
+
|
| 8 |
+
# 客户端断开检测函数
|
| 9 |
+
async def check_client_disconnect(http_request: Request, current_api_key: str, request_type: str, model: str):
|
| 10 |
+
"""检查客户端是否断开连接"""
|
| 11 |
+
while True:
|
| 12 |
+
if await http_request.is_disconnected():
|
| 13 |
+
extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': model, 'error_message': '检测到客户端断开连接'}
|
| 14 |
+
log('info', "客户端连接已中断,等待API请求完成", extra=extra_log)
|
| 15 |
+
return True
|
| 16 |
+
await asyncio.sleep(0.5)
|
| 17 |
+
|
| 18 |
+
# 客户端断开处理函数
|
| 19 |
+
async def handle_client_disconnect(
|
| 20 |
+
gemini_task: asyncio.Task,
|
| 21 |
+
chat_request: ChatCompletionRequest,
|
| 22 |
+
request_type: str,
|
| 23 |
+
current_api_key: str,
|
| 24 |
+
response_cache_manager,
|
| 25 |
+
cache_key: str = None,
|
| 26 |
+
client_ip: str = None
|
| 27 |
+
):
|
| 28 |
+
try:
|
| 29 |
+
# 等待API任务完成,使用shield防止它被取消
|
| 30 |
+
response_content = await asyncio.shield(gemini_task)
|
| 31 |
+
|
| 32 |
+
# 检查响应文本是否为空
|
| 33 |
+
if response_content is None or response_content.text == "":
|
| 34 |
+
if response_content is None:
|
| 35 |
+
log('info', "客户端断开后API任务返回None",
|
| 36 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
|
| 37 |
+
else:
|
| 38 |
+
extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'status_code': 204}
|
| 39 |
+
log('info', "客户端断开后Gemini API 返回空响应", extra=extra_log)
|
| 40 |
+
|
| 41 |
+
# 删除任何现有缓存,因为响应为空
|
| 42 |
+
if cache_key and cache_key in response_cache_manager.cache:
|
| 43 |
+
log('info', f"因空响应,删除缓存: {cache_key[:8]}...",
|
| 44 |
+
extra={'cache_operation': 'remove-on-empty', 'request_type': request_type})
|
| 45 |
+
del response_cache_manager.cache[cache_key]
|
| 46 |
+
|
| 47 |
+
# 返回错误响应而不是None
|
| 48 |
+
return create_error_response(chat_request.model, "AI未返回任何内容,请重试")
|
| 49 |
+
|
| 50 |
+
# 首先检查是否有现有缓存
|
| 51 |
+
cached_response, cache_hit = response_cache_manager.get(cache_key)
|
| 52 |
+
if cache_hit:
|
| 53 |
+
log('info', f"客户端断开但找到已存在缓存,将删除: {cache_key[:8]}...",
|
| 54 |
+
extra={'cache_operation': 'disconnect-found-cache', 'request_type': request_type})
|
| 55 |
+
|
| 56 |
+
# 安全删除缓存
|
| 57 |
+
if cache_key in response_cache_manager.cache:
|
| 58 |
+
del response_cache_manager.cache[cache_key]
|
| 59 |
+
|
| 60 |
+
# 不返回缓存,而是创建新响应并缓存
|
| 61 |
+
|
| 62 |
+
# 创建新响应
|
| 63 |
+
from app.utils.response import create_response
|
| 64 |
+
response = create_response(chat_request, response_content)
|
| 65 |
+
|
| 66 |
+
# 客户端已断开,此响应不会实际发送,可以考虑将其缓存以供后续使用
|
| 67 |
+
# 如果确实需要缓存,则可以取消下面的注释
|
| 68 |
+
# cache_response(response, cache_key, client_ip)
|
| 69 |
+
|
| 70 |
+
return response
|
| 71 |
+
except asyncio.CancelledError:
|
| 72 |
+
# 对于取消异常,仍然尝试继续完成任务
|
| 73 |
+
log('info', "客户端断开后任务被取消,但我们仍会尝试完成",
|
| 74 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
|
| 75 |
+
|
| 76 |
+
# 检查任务是否已经完成
|
| 77 |
+
if gemini_task.done() and not gemini_task.cancelled():
|
| 78 |
+
try:
|
| 79 |
+
response_content = gemini_task.result()
|
| 80 |
+
|
| 81 |
+
# 首先检查是否有现有缓存
|
| 82 |
+
cached_response, cache_hit = response_cache_manager.get(cache_key)
|
| 83 |
+
if cache_hit:
|
| 84 |
+
log('info', f"任务被取消但找到已存在缓存,将删除: {cache_key[:8]}...",
|
| 85 |
+
extra={'cache_operation': 'cancel-found-cache', 'request_type': request_type})
|
| 86 |
+
|
| 87 |
+
# 安全删除缓存
|
| 88 |
+
if cache_key in response_cache_manager.cache:
|
| 89 |
+
del response_cache_manager.cache[cache_key]
|
| 90 |
+
|
| 91 |
+
# 创建但不缓存响应
|
| 92 |
+
from app.utils.response import create_response
|
| 93 |
+
response = create_response(chat_request, response_content)
|
| 94 |
+
return response
|
| 95 |
+
except Exception as inner_e:
|
| 96 |
+
log('error', f"客户端断开后从已完成任务获取结果失败: {str(inner_e)}",
|
| 97 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
|
| 98 |
+
|
| 99 |
+
# 删除缓存,因为出现错误
|
| 100 |
+
if cache_key and cache_key in response_cache_manager.cache:
|
| 101 |
+
log('info', f"因任务获取结果失败,删除缓存: {cache_key[:8]}...",
|
| 102 |
+
extra={'cache_operation': 'remove-on-error', 'request_type': request_type})
|
| 103 |
+
del response_cache_manager.cache[cache_key]
|
| 104 |
+
|
| 105 |
+
# 创建错误响应而不是返回None
|
| 106 |
+
return create_error_response(chat_request.model, "请求处理过程中发生错误,请重试")
|
| 107 |
+
except Exception as e:
|
| 108 |
+
# 处理API任务异常
|
| 109 |
+
error_msg = str(e)
|
| 110 |
+
extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message': error_msg}
|
| 111 |
+
log('error', f"客户端断开后处理API响应时出错: {error_msg}", extra=extra_log)
|
| 112 |
+
|
| 113 |
+
# 删除缓存,因为出现错误
|
| 114 |
+
if cache_key and cache_key in response_cache_manager.cache:
|
| 115 |
+
log('info', f"因API响应错误,删除缓存: {cache_key[:8]}...",
|
| 116 |
+
extra={'cache_operation': 'remove-on-error', 'request_type': request_type})
|
| 117 |
+
del response_cache_manager.cache[cache_key]
|
| 118 |
+
|
| 119 |
+
# 创建错误响应而不是返回None
|
| 120 |
+
return create_error_response(chat_request.model, f"请求处理错误: {error_msg}")
|
app/api/dashboard.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from datetime import datetime, timedelta
|
| 3 |
+
import time
|
| 4 |
+
from app.utils import (
|
| 5 |
+
log_manager,
|
| 6 |
+
ResponseCacheManager,
|
| 7 |
+
ActiveRequestsManager,
|
| 8 |
+
clean_expired_stats
|
| 9 |
+
)
|
| 10 |
+
from app.config.settings import (
|
| 11 |
+
api_call_stats,
|
| 12 |
+
client_request_history,
|
| 13 |
+
API_KEY_DAILY_LIMIT,
|
| 14 |
+
FAKE_STREAMING,
|
| 15 |
+
FAKE_STREAMING_INTERVAL,
|
| 16 |
+
RANDOM_STRING,
|
| 17 |
+
RANDOM_STRING_LENGTH,
|
| 18 |
+
MAX_REQUESTS_PER_MINUTE,
|
| 19 |
+
MAX_REQUESTS_PER_DAY_PER_IP,
|
| 20 |
+
CACHE_EXPIRY_TIME,
|
| 21 |
+
MAX_CACHE_ENTRIES,
|
| 22 |
+
REMOVE_CACHE_AFTER_USE,
|
| 23 |
+
ENABLE_RECONNECT_DETECTION,
|
| 24 |
+
version
|
| 25 |
+
)
|
| 26 |
+
from app.services import GeminiClient
|
| 27 |
+
|
| 28 |
+
# 创建路由器
|
| 29 |
+
dashboard_router = APIRouter(prefix="/api", tags=["dashboard"])
|
| 30 |
+
|
| 31 |
+
# 全局变量引用,将在init_dashboard_router中设置
|
| 32 |
+
key_manager = None
|
| 33 |
+
response_cache_manager = None
|
| 34 |
+
active_requests_manager = None
|
| 35 |
+
|
| 36 |
+
def init_dashboard_router(
|
| 37 |
+
key_mgr,
|
| 38 |
+
cache_mgr,
|
| 39 |
+
active_req_mgr
|
| 40 |
+
):
|
| 41 |
+
"""初始化仪表盘路由器"""
|
| 42 |
+
global key_manager, response_cache_manager, active_requests_manager
|
| 43 |
+
key_manager = key_mgr
|
| 44 |
+
response_cache_manager = cache_mgr
|
| 45 |
+
active_requests_manager = active_req_mgr
|
| 46 |
+
return dashboard_router
|
| 47 |
+
|
| 48 |
+
@dashboard_router.get("/dashboard-data")
|
| 49 |
+
async def get_dashboard_data():
|
| 50 |
+
"""获取仪表盘数据的API端点,用于动态刷新"""
|
| 51 |
+
# 先清理过期数据,确保统计数据是最新的
|
| 52 |
+
clean_expired_stats(api_call_stats)
|
| 53 |
+
response_cache_manager.clean_expired() # 使用管理器清理缓存
|
| 54 |
+
active_requests_manager.clean_completed() # 使用管理器清理活跃请求
|
| 55 |
+
|
| 56 |
+
# 获取当前统计数据
|
| 57 |
+
now = datetime.now()
|
| 58 |
+
|
| 59 |
+
# 计算过去24小时的调用总数
|
| 60 |
+
last_24h_calls = sum(api_call_stats['last_24h']['total'].values())
|
| 61 |
+
|
| 62 |
+
# 计算过去一小时内的调用总数
|
| 63 |
+
one_hour_ago = now - timedelta(hours=1)
|
| 64 |
+
hourly_calls = 0
|
| 65 |
+
for hour_key, count in api_call_stats['hourly']['total'].items():
|
| 66 |
+
try:
|
| 67 |
+
hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
|
| 68 |
+
if hour_time >= one_hour_ago:
|
| 69 |
+
hourly_calls += count
|
| 70 |
+
except ValueError:
|
| 71 |
+
continue
|
| 72 |
+
|
| 73 |
+
# 计算过去一分钟内的调用总数
|
| 74 |
+
one_minute_ago = now - timedelta(minutes=1)
|
| 75 |
+
minute_calls = 0
|
| 76 |
+
for minute_key, count in api_call_stats['minute']['total'].items():
|
| 77 |
+
try:
|
| 78 |
+
minute_time = datetime.strptime(minute_key, '%Y-%m-%d %H:%M')
|
| 79 |
+
if minute_time >= one_minute_ago:
|
| 80 |
+
minute_calls += count
|
| 81 |
+
except ValueError:
|
| 82 |
+
continue
|
| 83 |
+
|
| 84 |
+
# 获取API密钥使用统计
|
| 85 |
+
api_key_stats = []
|
| 86 |
+
for api_key in key_manager.api_keys:
|
| 87 |
+
# 获取API密钥前8位作为标识
|
| 88 |
+
api_key_id = api_key[:8]
|
| 89 |
+
|
| 90 |
+
# 计算24小时内的调用次数和按模型分类的调用次数
|
| 91 |
+
calls_24h = 0
|
| 92 |
+
model_stats = {}
|
| 93 |
+
|
| 94 |
+
if 'by_endpoint' in api_call_stats['last_24h'] and api_key in api_call_stats['last_24h']['by_endpoint']:
|
| 95 |
+
# 遍历所有模型
|
| 96 |
+
for model, model_data in api_call_stats['last_24h']['by_endpoint'][api_key].items():
|
| 97 |
+
model_calls = sum(model_data.values())
|
| 98 |
+
calls_24h += model_calls
|
| 99 |
+
model_stats[model] = model_calls
|
| 100 |
+
|
| 101 |
+
# 计算使用百分比
|
| 102 |
+
usage_percent = (calls_24h / API_KEY_DAILY_LIMIT) * 100 if API_KEY_DAILY_LIMIT > 0 else 0
|
| 103 |
+
|
| 104 |
+
# 添加到结果列表
|
| 105 |
+
api_key_stats.append({
|
| 106 |
+
'api_key': api_key_id,
|
| 107 |
+
'calls_24h': calls_24h,
|
| 108 |
+
'limit': API_KEY_DAILY_LIMIT,
|
| 109 |
+
'usage_percent': round(usage_percent, 2),
|
| 110 |
+
'model_stats': model_stats # 添加按模型分类的统计数据
|
| 111 |
+
})
|
| 112 |
+
|
| 113 |
+
# 按使用百分比降序排序
|
| 114 |
+
api_key_stats.sort(key=lambda x: x['usage_percent'], reverse=True)
|
| 115 |
+
|
| 116 |
+
# 获取最近的日志
|
| 117 |
+
recent_logs = log_manager.get_recent_logs(500) # 获取最近500条日志
|
| 118 |
+
|
| 119 |
+
# 获取缓存统计
|
| 120 |
+
total_cache = len(response_cache_manager.cache)
|
| 121 |
+
valid_cache = sum(1 for _, data in response_cache_manager.cache.items()
|
| 122 |
+
if time.time() < data.get('expiry_time', 0))
|
| 123 |
+
cache_by_model = {}
|
| 124 |
+
|
| 125 |
+
# 分析缓存数据
|
| 126 |
+
for _, cache_data in response_cache_manager.cache.items():
|
| 127 |
+
if time.time() < cache_data.get('expiry_time', 0):
|
| 128 |
+
# 按模型统计缓存
|
| 129 |
+
model = cache_data.get('response', {}).model
|
| 130 |
+
if model:
|
| 131 |
+
if model in cache_by_model:
|
| 132 |
+
cache_by_model[model] += 1
|
| 133 |
+
else:
|
| 134 |
+
cache_by_model[model] = 1
|
| 135 |
+
|
| 136 |
+
# 获取请求历史统计
|
| 137 |
+
history_count = len(client_request_history)
|
| 138 |
+
|
| 139 |
+
# 获取活跃请求统计
|
| 140 |
+
active_count = len(active_requests_manager.active_requests)
|
| 141 |
+
active_done = sum(1 for task in active_requests_manager.active_requests.values() if task.done())
|
| 142 |
+
active_pending = active_count - active_done
|
| 143 |
+
|
| 144 |
+
# 返回JSON格式的数据
|
| 145 |
+
return {
|
| 146 |
+
"key_count": len(key_manager.api_keys),
|
| 147 |
+
"model_count": len(GeminiClient.AVAILABLE_MODELS),
|
| 148 |
+
"retry_count": len(key_manager.api_keys),
|
| 149 |
+
"last_24h_calls": last_24h_calls,
|
| 150 |
+
"hourly_calls": hourly_calls,
|
| 151 |
+
"minute_calls": minute_calls,
|
| 152 |
+
"current_time": datetime.now().strftime('%H:%M:%S'),
|
| 153 |
+
"logs": recent_logs,
|
| 154 |
+
"api_key_stats": api_key_stats,
|
| 155 |
+
# 添加配置信息
|
| 156 |
+
"max_requests_per_minute": MAX_REQUESTS_PER_MINUTE,
|
| 157 |
+
"max_requests_per_day_per_ip": MAX_REQUESTS_PER_DAY_PER_IP,
|
| 158 |
+
# 添加版本信息
|
| 159 |
+
"local_version": version["local_version"],
|
| 160 |
+
"remote_version": version["remote_version"],
|
| 161 |
+
"has_update": version["has_update"],
|
| 162 |
+
# 添加流式响应配置
|
| 163 |
+
"fake_streaming": FAKE_STREAMING,
|
| 164 |
+
"fake_streaming_interval": FAKE_STREAMING_INTERVAL,
|
| 165 |
+
# 添加随机字符串配置
|
| 166 |
+
"random_string": RANDOM_STRING,
|
| 167 |
+
"random_string_length": RANDOM_STRING_LENGTH,
|
| 168 |
+
# 添加缓存信息
|
| 169 |
+
"cache_entries": total_cache,
|
| 170 |
+
"valid_cache": valid_cache,
|
| 171 |
+
"expired_cache": total_cache - valid_cache,
|
| 172 |
+
"cache_expiry_time": CACHE_EXPIRY_TIME,
|
| 173 |
+
"max_cache_entries": MAX_CACHE_ENTRIES,
|
| 174 |
+
"cache_by_model": cache_by_model,
|
| 175 |
+
"request_history_count": history_count,
|
| 176 |
+
"enable_reconnect_detection": ENABLE_RECONNECT_DETECTION,
|
| 177 |
+
"remove_cache_after_use": REMOVE_CACHE_AFTER_USE,
|
| 178 |
+
# 添加活跃请求池信息
|
| 179 |
+
"active_count": active_count,
|
| 180 |
+
"active_done": active_done,
|
| 181 |
+
"active_pending": active_pending
|
| 182 |
+
}
|
app/api/gemini_handlers.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from app.models import ChatCompletionRequest
|
| 3 |
+
from app.services import GeminiClient
|
| 4 |
+
from app.utils.logging import log
|
| 5 |
+
|
| 6 |
+
# Gemini完成请求函数
|
| 7 |
+
async def run_gemini_completion(
|
| 8 |
+
gemini_client,
|
| 9 |
+
chat_request: ChatCompletionRequest,
|
| 10 |
+
contents,
|
| 11 |
+
system_instruction,
|
| 12 |
+
request_type: str,
|
| 13 |
+
current_api_key: str,
|
| 14 |
+
safety_settings,
|
| 15 |
+
safety_settings_g2
|
| 16 |
+
):
|
| 17 |
+
"""运行Gemini非流式请求"""
|
| 18 |
+
# 记录函数调用状态
|
| 19 |
+
run_fn = run_gemini_completion
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
# 创建一个不会被客户端断开影响的任务
|
| 23 |
+
response_future = asyncio.create_task(
|
| 24 |
+
asyncio.to_thread(
|
| 25 |
+
gemini_client.complete_chat,
|
| 26 |
+
chat_request,
|
| 27 |
+
contents,
|
| 28 |
+
safety_settings_g2 if 'gemini-2.0-flash-exp' in chat_request.model else safety_settings,
|
| 29 |
+
system_instruction
|
| 30 |
+
)
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
# 使用shield防止任务被外部取消
|
| 34 |
+
response_content = await asyncio.shield(response_future)
|
| 35 |
+
|
| 36 |
+
# 只在第一次调用时记录完成日志
|
| 37 |
+
if not hasattr(run_fn, 'logged_complete'):
|
| 38 |
+
log('info', "非流式请求成功完成",
|
| 39 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
|
| 40 |
+
run_fn.logged_complete = True
|
| 41 |
+
return response_content
|
| 42 |
+
except asyncio.CancelledError:
|
| 43 |
+
# 即使任务被取消,我们也确保正在进行的API请求能够完成
|
| 44 |
+
if 'response_future' in locals() and not response_future.done():
|
| 45 |
+
try:
|
| 46 |
+
# 使用shield确保任务不被取消,并等待它完成
|
| 47 |
+
response_content = await asyncio.shield(response_future)
|
| 48 |
+
log('info', "API请求在客户端断开后完成",
|
| 49 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
|
| 50 |
+
return response_content
|
| 51 |
+
except Exception as e:
|
| 52 |
+
log('info', "API调用因客户端断开而失败",
|
| 53 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message': f'API请求在客户端断开后失败: {str(e)}'})
|
| 54 |
+
raise
|
| 55 |
+
|
| 56 |
+
# 如果任务尚未开始或已经失败,记录日志
|
| 57 |
+
log('info', "API调用因客户端断开而取消",
|
| 58 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message': '客户端断开导致API调用取消'})
|
| 59 |
+
raise
|
app/api/nonstream_handlers.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from fastapi import HTTPException, status, Request
|
| 3 |
+
from app.models import ChatCompletionRequest
|
| 4 |
+
from app.services import GeminiClient
|
| 5 |
+
from app.utils import cache_response, update_api_call_stats
|
| 6 |
+
from app.utils.logging import log
|
| 7 |
+
from .client_disconnect import check_client_disconnect, handle_client_disconnect
|
| 8 |
+
from .gemini_handlers import run_gemini_completion
|
| 9 |
+
|
| 10 |
+
# 非流式请求处理函数
|
| 11 |
+
async def process_nonstream_request(
|
| 12 |
+
chat_request: ChatCompletionRequest,
|
| 13 |
+
http_request: Request,
|
| 14 |
+
request_type: str,
|
| 15 |
+
contents,
|
| 16 |
+
system_instruction,
|
| 17 |
+
current_api_key: str,
|
| 18 |
+
response_cache_manager,
|
| 19 |
+
active_requests_manager,
|
| 20 |
+
safety_settings,
|
| 21 |
+
safety_settings_g2,
|
| 22 |
+
api_call_stats,
|
| 23 |
+
cache_key: str = None,
|
| 24 |
+
client_ip: str = None
|
| 25 |
+
):
|
| 26 |
+
"""处理非流式API请求"""
|
| 27 |
+
gemini_client = GeminiClient(current_api_key)
|
| 28 |
+
|
| 29 |
+
# 创建任务
|
| 30 |
+
gemini_task = asyncio.create_task(
|
| 31 |
+
run_gemini_completion(
|
| 32 |
+
gemini_client,
|
| 33 |
+
chat_request,
|
| 34 |
+
contents,
|
| 35 |
+
system_instruction,
|
| 36 |
+
request_type,
|
| 37 |
+
current_api_key,
|
| 38 |
+
safety_settings,
|
| 39 |
+
safety_settings_g2
|
| 40 |
+
)
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
disconnect_task = asyncio.create_task(
|
| 44 |
+
check_client_disconnect(
|
| 45 |
+
http_request,
|
| 46 |
+
current_api_key,
|
| 47 |
+
request_type,
|
| 48 |
+
chat_request.model
|
| 49 |
+
)
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
# 先等待看是否API任务先完成,或者客户端先断开连接
|
| 54 |
+
done, pending = await asyncio.wait(
|
| 55 |
+
[gemini_task, disconnect_task],
|
| 56 |
+
return_when=asyncio.FIRST_COMPLETED
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
if disconnect_task in done:
|
| 60 |
+
# 客户端已断开连接,但我们仍继续完成API请求以便缓存结果
|
| 61 |
+
return await handle_client_disconnect(
|
| 62 |
+
gemini_task,
|
| 63 |
+
chat_request,
|
| 64 |
+
request_type,
|
| 65 |
+
current_api_key,
|
| 66 |
+
response_cache_manager,
|
| 67 |
+
cache_key,
|
| 68 |
+
client_ip
|
| 69 |
+
)
|
| 70 |
+
else:
|
| 71 |
+
# API任务先完成,取消断开检测任务
|
| 72 |
+
disconnect_task.cancel()
|
| 73 |
+
|
| 74 |
+
# 获取响应内容
|
| 75 |
+
response_content = await gemini_task
|
| 76 |
+
|
| 77 |
+
# 检查缓存是否已经存在,如果存在则不再创建新缓存
|
| 78 |
+
cached_response, cache_hit = response_cache_manager.get(cache_key)
|
| 79 |
+
if cache_hit:
|
| 80 |
+
log('info', f"缓存已存在,直接返回: {cache_key[:8]}...",
|
| 81 |
+
extra={'cache_operation': 'use-existing', 'request_type': request_type})
|
| 82 |
+
|
| 83 |
+
# 安全删除缓存
|
| 84 |
+
if cache_key in response_cache_manager.cache:
|
| 85 |
+
del response_cache_manager.cache[cache_key]
|
| 86 |
+
log('info', f"缓存使用后已删除: {cache_key[:8]}...",
|
| 87 |
+
extra={'cache_operation': 'used-and-removed', 'request_type': request_type})
|
| 88 |
+
|
| 89 |
+
return cached_response
|
| 90 |
+
|
| 91 |
+
# 创建响应
|
| 92 |
+
from app.utils.response import create_response
|
| 93 |
+
response = create_response(chat_request, response_content)
|
| 94 |
+
|
| 95 |
+
# 缓存响应
|
| 96 |
+
cache_response(response, cache_key, client_ip, response_cache_manager, update_api_call_stats, endpoint=current_api_key,model=chat_request.model)
|
| 97 |
+
|
| 98 |
+
# 立即删除缓存,确保只能使用一次
|
| 99 |
+
if cache_key and cache_key in response_cache_manager.cache:
|
| 100 |
+
del response_cache_manager.cache[cache_key]
|
| 101 |
+
log('info', f"缓存创建后立即删除: {cache_key[:8]}...",
|
| 102 |
+
extra={'cache_operation': 'store-and-remove', 'request_type': request_type})
|
| 103 |
+
|
| 104 |
+
# 返回响应
|
| 105 |
+
return response
|
| 106 |
+
|
| 107 |
+
except asyncio.CancelledError:
|
| 108 |
+
extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model, 'error_message':"请求被取消"}
|
| 109 |
+
log('info', "请求取消", extra=extra_log)
|
| 110 |
+
|
| 111 |
+
# 在请求被取消时先检查缓存中是否已有结果
|
| 112 |
+
cached_response, cache_hit = response_cache_manager.get(cache_key)
|
| 113 |
+
if cache_hit:
|
| 114 |
+
log('info', f"请求取消但找到有效缓存,使用缓存响应: {cache_key[:8]}...",
|
| 115 |
+
extra={'cache_operation': 'use-cache-on-cancel', 'request_type': request_type})
|
| 116 |
+
|
| 117 |
+
# 安全删除缓存
|
| 118 |
+
if cache_key in response_cache_manager.cache:
|
| 119 |
+
del response_cache_manager.cache[cache_key]
|
| 120 |
+
log('info', f"缓存使用后已删除: {cache_key[:8]}...",
|
| 121 |
+
extra={'cache_operation': 'used-and-removed', 'request_type': request_type})
|
| 122 |
+
|
| 123 |
+
return cached_response
|
| 124 |
+
|
| 125 |
+
# 尝试完成正在进行的API请求
|
| 126 |
+
if not gemini_task.done():
|
| 127 |
+
log('info', "请求取消但API请求尚未完成,继续等待...",
|
| 128 |
+
extra={'key': current_api_key[:8], 'request_type': request_type})
|
| 129 |
+
|
| 130 |
+
# 使用shield确保任务不会被取消
|
| 131 |
+
response_content = await asyncio.shield(gemini_task)
|
| 132 |
+
|
| 133 |
+
# 创建响应
|
| 134 |
+
from app.utils.response import create_response
|
| 135 |
+
response = create_response(chat_request, response_content)
|
| 136 |
+
|
| 137 |
+
# 不缓存这个响应,直接返回
|
| 138 |
+
return response
|
| 139 |
+
else:
|
| 140 |
+
# 任务已完成,获取结果
|
| 141 |
+
response_content = gemini_task.result()
|
| 142 |
+
|
| 143 |
+
# 创建响应
|
| 144 |
+
from app.utils.response import create_response
|
| 145 |
+
response = create_response(chat_request, response_content)
|
| 146 |
+
|
| 147 |
+
# 不缓存这个响应,直接返回
|
| 148 |
+
return response
|
| 149 |
+
|
| 150 |
+
except HTTPException as e:
|
| 151 |
+
if e.status_code == status.HTTP_408_REQUEST_TIMEOUT:
|
| 152 |
+
extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model,
|
| 153 |
+
'status_code': 408, 'error_message': '客户端连接中断'}
|
| 154 |
+
log('error', "客户端连接中断,终止后续重试", extra=extra_log)
|
| 155 |
+
raise
|
| 156 |
+
else:
|
| 157 |
+
raise
|
app/api/request_handlers.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
+
from typing import Literal
|
| 4 |
+
from fastapi import HTTPException, Request, status
|
| 5 |
+
from fastapi.responses import StreamingResponse
|
| 6 |
+
from app.models import ChatCompletionRequest
|
| 7 |
+
from app.services import GeminiClient
|
| 8 |
+
from app.utils import protect_from_abuse, handle_gemini_error, handle_api_error
|
| 9 |
+
from app.utils.logging import log
|
| 10 |
+
from .stream_handlers import process_stream_request
|
| 11 |
+
from .nonstream_handlers import process_nonstream_request
|
| 12 |
+
|
| 13 |
+
# 请求处理函数
|
| 14 |
+
async def process_request(
|
| 15 |
+
chat_request: ChatCompletionRequest,
|
| 16 |
+
http_request: Request,
|
| 17 |
+
request_type: Literal['stream', 'non-stream'],
|
| 18 |
+
key_manager,
|
| 19 |
+
response_cache_manager,
|
| 20 |
+
active_requests_manager,
|
| 21 |
+
safety_settings,
|
| 22 |
+
safety_settings_g2,
|
| 23 |
+
api_call_stats,
|
| 24 |
+
FAKE_STREAMING,
|
| 25 |
+
FAKE_STREAMING_INTERVAL,
|
| 26 |
+
MAX_REQUESTS_PER_MINUTE,
|
| 27 |
+
MAX_REQUESTS_PER_DAY_PER_IP,
|
| 28 |
+
cache_key: str = None,
|
| 29 |
+
client_ip: str = None
|
| 30 |
+
):
|
| 31 |
+
"""处理API请求的主函数,根据需要处理流式或非流式请求"""
|
| 32 |
+
global current_api_key
|
| 33 |
+
|
| 34 |
+
# 请求前基本检查
|
| 35 |
+
protect_from_abuse(
|
| 36 |
+
http_request, MAX_REQUESTS_PER_MINUTE, MAX_REQUESTS_PER_DAY_PER_IP)
|
| 37 |
+
if chat_request.model not in GeminiClient.AVAILABLE_MODELS:
|
| 38 |
+
log('error', "无效的模型",
|
| 39 |
+
extra={'request_type': request_type, 'model': chat_request.model, 'status_code': 400})
|
| 40 |
+
raise HTTPException(
|
| 41 |
+
status_code=status.HTTP_400_BAD_REQUEST, detail="无效的模型")
|
| 42 |
+
|
| 43 |
+
# 重置已尝试的密钥
|
| 44 |
+
key_manager.reset_tried_keys_for_request()
|
| 45 |
+
# 转换消息格式
|
| 46 |
+
contents, system_instruction = GeminiClient.convert_messages(
|
| 47 |
+
GeminiClient, chat_request.messages,model=chat_request.model)
|
| 48 |
+
|
| 49 |
+
# 设置重试次数(使用可用API密钥数量作为最大重试次数)
|
| 50 |
+
retry_attempts = len(key_manager.api_keys) if key_manager.api_keys else 1
|
| 51 |
+
|
| 52 |
+
# 尝试使用不同API密钥
|
| 53 |
+
for attempt in range(1, retry_attempts + 1):
|
| 54 |
+
# 获取密钥
|
| 55 |
+
current_api_key = key_manager.get_available_key()
|
| 56 |
+
|
| 57 |
+
# 检查API密钥是否可用
|
| 58 |
+
if current_api_key is None:
|
| 59 |
+
log('warning', "没有可用的 API 密钥,跳过本次尝试",
|
| 60 |
+
extra={'request_type': request_type, 'model': chat_request.model})
|
| 61 |
+
break
|
| 62 |
+
|
| 63 |
+
# 记录当前尝试的密钥信息
|
| 64 |
+
log('info', f"第 {attempt}/{retry_attempts} 次尝试 ... 使用密钥: {current_api_key[:8]}...",
|
| 65 |
+
extra={'key': current_api_key[:8], 'request_type': request_type, 'model': chat_request.model})
|
| 66 |
+
|
| 67 |
+
# 服务器错误重试逻辑
|
| 68 |
+
server_error_retries = 3
|
| 69 |
+
for server_retry in range(1, server_error_retries + 1):
|
| 70 |
+
try:
|
| 71 |
+
# 根据请求类型分别处理
|
| 72 |
+
if chat_request.stream:
|
| 73 |
+
try:
|
| 74 |
+
return await process_stream_request(
|
| 75 |
+
chat_request,
|
| 76 |
+
http_request,
|
| 77 |
+
contents,
|
| 78 |
+
system_instruction,
|
| 79 |
+
current_api_key,
|
| 80 |
+
key_manager,
|
| 81 |
+
safety_settings,
|
| 82 |
+
safety_settings_g2,
|
| 83 |
+
api_call_stats,
|
| 84 |
+
FAKE_STREAMING,
|
| 85 |
+
FAKE_STREAMING_INTERVAL
|
| 86 |
+
)
|
| 87 |
+
except Exception as e:
|
| 88 |
+
# 捕获流式请求的异常,但不立即返回错误,而是抛出异常让外层循环处理
|
| 89 |
+
# 记录错误并继续尝试下一个API密钥
|
| 90 |
+
error_detail = handle_gemini_error(e, current_api_key, key_manager)
|
| 91 |
+
log('info', f"流式请求失败: {error_detail}",
|
| 92 |
+
extra={'key': current_api_key[:8], 'request_type': 'stream', 'model': chat_request.model})
|
| 93 |
+
raise
|
| 94 |
+
else:
|
| 95 |
+
return await process_nonstream_request(
|
| 96 |
+
chat_request,
|
| 97 |
+
http_request,
|
| 98 |
+
request_type,
|
| 99 |
+
contents,
|
| 100 |
+
system_instruction,
|
| 101 |
+
current_api_key,
|
| 102 |
+
response_cache_manager,
|
| 103 |
+
active_requests_manager,
|
| 104 |
+
safety_settings,
|
| 105 |
+
safety_settings_g2,
|
| 106 |
+
api_call_stats,
|
| 107 |
+
cache_key,
|
| 108 |
+
client_ip
|
| 109 |
+
)
|
| 110 |
+
except HTTPException as e:
|
| 111 |
+
if e.status_code == status.HTTP_408_REQUEST_TIMEOUT:
|
| 112 |
+
log('info', "客户端连接中断",
|
| 113 |
+
extra={'key': current_api_key[:8], 'request_type': request_type,
|
| 114 |
+
'model': chat_request.model, 'status_code': 408})
|
| 115 |
+
raise
|
| 116 |
+
else:
|
| 117 |
+
raise
|
| 118 |
+
except Exception as e:
|
| 119 |
+
# 使用统一的API错误处理函数
|
| 120 |
+
error_result = await handle_api_error(
|
| 121 |
+
e,
|
| 122 |
+
current_api_key,
|
| 123 |
+
key_manager,
|
| 124 |
+
request_type,
|
| 125 |
+
chat_request.model,
|
| 126 |
+
server_retry - 1
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
# 如果需要删除缓存,清除缓存
|
| 130 |
+
if error_result.get('remove_cache', False) and cache_key and cache_key in response_cache_manager.cache:
|
| 131 |
+
log('info', f"因API错误,删除缓存: {cache_key[:8]}...",
|
| 132 |
+
extra={'cache_operation': 'remove-on-error', 'request_type': request_type})
|
| 133 |
+
del response_cache_manager.cache[cache_key]
|
| 134 |
+
|
| 135 |
+
else:
|
| 136 |
+
# 跳出循环
|
| 137 |
+
break
|
| 138 |
+
|
| 139 |
+
# 如果所有尝试都失败
|
| 140 |
+
msg = "所有API密钥均请求失败,请稍后重试"
|
| 141 |
+
log('error', "API key 替换失败,所有API key都已尝试,请重新配置或稍后重试", extra={'request_type': 'switch_key'})
|
| 142 |
+
|
| 143 |
+
# 对于流式请求,创建一个特殊的StreamingResponse返回错误
|
| 144 |
+
if chat_request.stream:
|
| 145 |
+
async def error_generator():
|
| 146 |
+
error_json = json.dumps({'error': {'message': msg, 'type': 'api_error'}})
|
| 147 |
+
yield f"data: {error_json}\n\n"
|
| 148 |
+
yield "data: [DONE]\n\n"
|
| 149 |
+
|
| 150 |
+
return StreamingResponse(error_generator(), media_type="text/event-stream")
|
| 151 |
+
else:
|
| 152 |
+
# 非流式请求使用标准HTTP异常
|
| 153 |
+
raise HTTPException(
|
| 154 |
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=msg)
|
app/api/routes.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, Request, Depends, status
|
| 2 |
+
from fastapi.responses import JSONResponse, StreamingResponse
|
| 3 |
+
from app.models import ChatCompletionRequest, ChatCompletionResponse, ErrorResponse, ModelList
|
| 4 |
+
from app.services import GeminiClient
|
| 5 |
+
from app.utils import generate_cache_key
|
| 6 |
+
|
| 7 |
+
from app.config.settings import (
|
| 8 |
+
api_call_stats,
|
| 9 |
+
BLOCKED_MODELS
|
| 10 |
+
)
|
| 11 |
+
import asyncio
|
| 12 |
+
import time
|
| 13 |
+
from app.utils.logging import log
|
| 14 |
+
|
| 15 |
+
# 导入拆分后的模块
|
| 16 |
+
from .auth import verify_password
|
| 17 |
+
from .request_handlers import process_request
|
| 18 |
+
|
| 19 |
+
# 创建路由器
|
| 20 |
+
router = APIRouter()
|
| 21 |
+
|
| 22 |
+
# 全局变量引用 - 这些将在main.py中初始化并传递给路由
|
| 23 |
+
key_manager = None
|
| 24 |
+
response_cache_manager = None
|
| 25 |
+
active_requests_manager = None
|
| 26 |
+
safety_settings = None
|
| 27 |
+
safety_settings_g2 = None
|
| 28 |
+
current_api_key = None
|
| 29 |
+
FAKE_STREAMING = None
|
| 30 |
+
FAKE_STREAMING_INTERVAL = None
|
| 31 |
+
PASSWORD = None
|
| 32 |
+
MAX_REQUESTS_PER_MINUTE = None
|
| 33 |
+
MAX_REQUESTS_PER_DAY_PER_IP = None
|
| 34 |
+
|
| 35 |
+
# 初始化路由器的函数
|
| 36 |
+
def init_router(
|
| 37 |
+
_key_manager,
|
| 38 |
+
_response_cache_manager,
|
| 39 |
+
_active_requests_manager,
|
| 40 |
+
_safety_settings,
|
| 41 |
+
_safety_settings_g2,
|
| 42 |
+
_current_api_key,
|
| 43 |
+
_fake_streaming,
|
| 44 |
+
_fake_streaming_interval,
|
| 45 |
+
_password,
|
| 46 |
+
_max_requests_per_minute,
|
| 47 |
+
_max_requests_per_day_per_ip
|
| 48 |
+
):
|
| 49 |
+
global key_manager, response_cache_manager, active_requests_manager
|
| 50 |
+
global safety_settings, safety_settings_g2, current_api_key
|
| 51 |
+
global FAKE_STREAMING, FAKE_STREAMING_INTERVAL
|
| 52 |
+
global PASSWORD, MAX_REQUESTS_PER_MINUTE, MAX_REQUESTS_PER_DAY_PER_IP
|
| 53 |
+
|
| 54 |
+
key_manager = _key_manager
|
| 55 |
+
response_cache_manager = _response_cache_manager
|
| 56 |
+
active_requests_manager = _active_requests_manager
|
| 57 |
+
safety_settings = _safety_settings
|
| 58 |
+
safety_settings_g2 = _safety_settings_g2
|
| 59 |
+
current_api_key = _current_api_key
|
| 60 |
+
FAKE_STREAMING = _fake_streaming
|
| 61 |
+
FAKE_STREAMING_INTERVAL = _fake_streaming_interval
|
| 62 |
+
PASSWORD = _password
|
| 63 |
+
MAX_REQUESTS_PER_MINUTE = _max_requests_per_minute
|
| 64 |
+
MAX_REQUESTS_PER_DAY_PER_IP = _max_requests_per_day_per_ip
|
| 65 |
+
|
| 66 |
+
# 自定义密码验证依赖
|
| 67 |
+
async def custom_verify_password(request: Request):
|
| 68 |
+
await verify_password(request, PASSWORD)
|
| 69 |
+
|
| 70 |
+
# API路由
|
| 71 |
+
@router.get("/v1/models", response_model=ModelList)
|
| 72 |
+
def list_models():
|
| 73 |
+
log('info', "Received request to list models", extra={'request_type': 'list_models', 'status_code': 200})
|
| 74 |
+
filtered_models = [model for model in GeminiClient.AVAILABLE_MODELS if model not in BLOCKED_MODELS]
|
| 75 |
+
return ModelList(data=[{"id": model, "object": "model", "created": 1678888888, "owned_by": "organization-owner"} for model in filtered_models])
|
| 76 |
+
|
| 77 |
+
@router.post("/v1/chat/completions", response_model=ChatCompletionResponse)
|
| 78 |
+
async def chat_completions(request: ChatCompletionRequest, http_request: Request, _: None = Depends(custom_verify_password)):
|
| 79 |
+
# 获取客户端IP
|
| 80 |
+
client_ip = http_request.client.host if http_request.client else "unknown"
|
| 81 |
+
# 流式请求直接处理,不使用缓存
|
| 82 |
+
if request.stream:
|
| 83 |
+
return await process_request(
|
| 84 |
+
request,
|
| 85 |
+
http_request,
|
| 86 |
+
"stream",
|
| 87 |
+
key_manager,
|
| 88 |
+
response_cache_manager,
|
| 89 |
+
active_requests_manager,
|
| 90 |
+
safety_settings,
|
| 91 |
+
safety_settings_g2,
|
| 92 |
+
api_call_stats,
|
| 93 |
+
FAKE_STREAMING,
|
| 94 |
+
FAKE_STREAMING_INTERVAL,
|
| 95 |
+
MAX_REQUESTS_PER_MINUTE,
|
| 96 |
+
MAX_REQUESTS_PER_DAY_PER_IP
|
| 97 |
+
)
|
| 98 |
+
# 生成完整缓存键 - 用于精确匹配
|
| 99 |
+
cache_key = generate_cache_key(request)
|
| 100 |
+
|
| 101 |
+
# 记录请求缓存键信息
|
| 102 |
+
log('info', f"请求缓存键: {cache_key[:8]}...",
|
| 103 |
+
extra={'cache_key': cache_key[:8], 'request_type': 'non-stream'})
|
| 104 |
+
|
| 105 |
+
# 检查精确缓存是否存在且未过期
|
| 106 |
+
cached_response, cache_hit = response_cache_manager.get(cache_key)
|
| 107 |
+
if cache_hit:
|
| 108 |
+
# 精确缓存命中
|
| 109 |
+
log('info', f"精确缓存命中: {cache_key[:8]}...",
|
| 110 |
+
extra={'cache_operation': 'hit', 'request_type': 'non-stream'})
|
| 111 |
+
|
| 112 |
+
# 同时清理相关的活跃任务,避免后续请求等待已经不需要的任务
|
| 113 |
+
active_requests_manager.remove_by_prefix(f"cache:{cache_key}")
|
| 114 |
+
|
| 115 |
+
# 安全删除缓存
|
| 116 |
+
if cache_key in response_cache_manager.cache:
|
| 117 |
+
del response_cache_manager.cache[cache_key]
|
| 118 |
+
log('info', f"缓存使用后已删除: {cache_key[:8]}...",
|
| 119 |
+
extra={'cache_operation': 'used-and-removed', 'request_type': 'non-stream'})
|
| 120 |
+
|
| 121 |
+
# 返回缓存响应
|
| 122 |
+
return cached_response
|
| 123 |
+
|
| 124 |
+
# 构建包含缓存键的活跃请求池键
|
| 125 |
+
pool_key = f"cache:{cache_key}"
|
| 126 |
+
|
| 127 |
+
# 查找所有使用相同缓存键的活跃任务
|
| 128 |
+
active_task = active_requests_manager.get(pool_key)
|
| 129 |
+
if active_task and not active_task.done():
|
| 130 |
+
log('info', f"发现相同请求的进行中任务",
|
| 131 |
+
extra={'request_type': 'non-stream', 'model': request.model})
|
| 132 |
+
|
| 133 |
+
# 等待已有任务完成
|
| 134 |
+
try:
|
| 135 |
+
# 设置超时,避免无限等待
|
| 136 |
+
await asyncio.wait_for(active_task, timeout=180)
|
| 137 |
+
|
| 138 |
+
# 通过缓存管理器获取已完成任务的结果
|
| 139 |
+
cached_response, cache_hit = response_cache_manager.get(cache_key)
|
| 140 |
+
if cache_hit:
|
| 141 |
+
# 安全删除缓存
|
| 142 |
+
if cache_key in response_cache_manager.cache:
|
| 143 |
+
del response_cache_manager.cache[cache_key]
|
| 144 |
+
log('info', f"使用已完成任务的缓存后删除: {cache_key[:8]}...",
|
| 145 |
+
extra={'cache_operation': 'used-and-removed', 'request_type': 'non-stream'})
|
| 146 |
+
|
| 147 |
+
return cached_response
|
| 148 |
+
|
| 149 |
+
# 如果缓存已被清除或不存在,使用任务结果
|
| 150 |
+
if active_task.done() and not active_task.cancelled():
|
| 151 |
+
result = active_task.result()
|
| 152 |
+
if result:
|
| 153 |
+
# 使用原始结果时,我们需要创建一个新的响应对象
|
| 154 |
+
# 避免使用可能已被其他请求修改的对象
|
| 155 |
+
new_response = ChatCompletionResponse(
|
| 156 |
+
id=f"chatcmpl-{int(time.time()*1000)}",
|
| 157 |
+
object="chat.completion",
|
| 158 |
+
created=int(time.time()),
|
| 159 |
+
model=result.model,
|
| 160 |
+
choices=result.choices
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
# 不要缓存此结果,因为它很可能是一个已存在但被使用后清除的缓存
|
| 164 |
+
return new_response
|
| 165 |
+
except (asyncio.TimeoutError, asyncio.CancelledError) as e:
|
| 166 |
+
# 任务超时或被取消的情况下,记录日志然后让代码继续执行
|
| 167 |
+
error_type = "超时" if isinstance(e, asyncio.TimeoutError) else "被取消"
|
| 168 |
+
log('warning', f"等待已有任务{error_type}: {pool_key}",
|
| 169 |
+
extra={'request_type': 'non-stream', 'model': request.model})
|
| 170 |
+
|
| 171 |
+
# 从活跃请求池移除该任务
|
| 172 |
+
if active_task.done() or active_task.cancelled():
|
| 173 |
+
active_requests_manager.remove(pool_key)
|
| 174 |
+
log('info', f"已从活跃请求池移除{error_type}任务: {pool_key}",
|
| 175 |
+
extra={'request_type': 'non-stream'})
|
| 176 |
+
|
| 177 |
+
# 创建非流式请求处理任务
|
| 178 |
+
process_task = asyncio.create_task(
|
| 179 |
+
process_request(
|
| 180 |
+
request,
|
| 181 |
+
http_request,
|
| 182 |
+
"non-stream",
|
| 183 |
+
key_manager,
|
| 184 |
+
response_cache_manager,
|
| 185 |
+
active_requests_manager,
|
| 186 |
+
safety_settings,
|
| 187 |
+
safety_settings_g2,
|
| 188 |
+
api_call_stats,
|
| 189 |
+
FAKE_STREAMING,
|
| 190 |
+
FAKE_STREAMING_INTERVAL,
|
| 191 |
+
MAX_REQUESTS_PER_MINUTE,
|
| 192 |
+
MAX_REQUESTS_PER_DAY_PER_IP,
|
| 193 |
+
cache_key,
|
| 194 |
+
client_ip
|
| 195 |
+
)
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
# 将任务添加到活跃请求池
|
| 199 |
+
active_requests_manager.add(pool_key, process_task)
|
| 200 |
+
|
| 201 |
+
# 等待任务完成
|
| 202 |
+
try:
|
| 203 |
+
response = await process_task
|
| 204 |
+
return response
|
| 205 |
+
except Exception as e:
|
| 206 |
+
# 如果任务失败,从活跃请求池中移除
|
| 207 |
+
active_requests_manager.remove(pool_key)
|
| 208 |
+
|
| 209 |
+
# 检查是否已有缓存的结果(可能是由另一个任务创建的)
|
| 210 |
+
cached_response, cache_hit = response_cache_manager.get(cache_key)
|
| 211 |
+
if cache_hit:
|
| 212 |
+
log('info', f"任务失败但找到缓存,使用缓存结果: {cache_key[:8]}...",
|
| 213 |
+
extra={'request_type': 'non-stream', 'model': request.model})
|
| 214 |
+
return cached_response
|
| 215 |
+
|
| 216 |
+
# 重新抛出异常
|
| 217 |
+
raise
|
app/api/stream_handlers.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
import random
|
| 5 |
+
from fastapi import Request
|
| 6 |
+
from fastapi.responses import StreamingResponse
|
| 7 |
+
from app.models import ChatCompletionRequest
|
| 8 |
+
from app.services import GeminiClient
|
| 9 |
+
from app.utils import handle_gemini_error, update_api_call_stats
|
| 10 |
+
from app.utils.logging import log
|
| 11 |
+
|
| 12 |
+
# 流式请求处理函数
|
| 13 |
+
async def process_stream_request(
|
| 14 |
+
chat_request: ChatCompletionRequest,
|
| 15 |
+
http_request: Request,
|
| 16 |
+
contents,
|
| 17 |
+
system_instruction,
|
| 18 |
+
current_api_key: str,
|
| 19 |
+
key_manager,
|
| 20 |
+
safety_settings,
|
| 21 |
+
safety_settings_g2,
|
| 22 |
+
api_call_stats,
|
| 23 |
+
FAKE_STREAMING,
|
| 24 |
+
FAKE_STREAMING_INTERVAL
|
| 25 |
+
) -> StreamingResponse:
|
| 26 |
+
"""处理流式API请求"""
|
| 27 |
+
|
| 28 |
+
# 创建一个直接流式响应的生成器函数
|
| 29 |
+
async def stream_response_generator():
|
| 30 |
+
# 如果启用了假流式模式,使用随机遍历API密钥的方式
|
| 31 |
+
if FAKE_STREAMING:
|
| 32 |
+
# 创建一个队列用于在任务之间传递数据
|
| 33 |
+
queue = asyncio.Queue()
|
| 34 |
+
keep_alive_task = None
|
| 35 |
+
api_request_task = None
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
# 创建一个保持连接的任务,持续发送换行符
|
| 39 |
+
async def keep_alive_sender():
|
| 40 |
+
try:
|
| 41 |
+
# 创建一个Gemini客户端用于发送保持连接的换行符
|
| 42 |
+
keep_alive_client = GeminiClient(current_api_key)
|
| 43 |
+
|
| 44 |
+
# 启动保持连接的生成器
|
| 45 |
+
keep_alive_generator = keep_alive_client.stream_chat(
|
| 46 |
+
chat_request,
|
| 47 |
+
contents,
|
| 48 |
+
safety_settings_g2 if 'gemini-2.0-flash-exp' in chat_request.model else safety_settings,
|
| 49 |
+
system_instruction
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# 持续发送换行符直到被取消
|
| 53 |
+
async for line in keep_alive_generator:
|
| 54 |
+
if line == "\n":
|
| 55 |
+
# 将换行符格式化为SSE格式
|
| 56 |
+
formatted_chunk = {
|
| 57 |
+
"id": "chatcmpl-keepalive",
|
| 58 |
+
"object": "chat.completion.chunk",
|
| 59 |
+
"created": int(time.time()),
|
| 60 |
+
"model": chat_request.model,
|
| 61 |
+
"choices": [{"delta": {"content": ""}, "index": 0, "finish_reason": None}]
|
| 62 |
+
}
|
| 63 |
+
# 将格式化的换行符放入队列
|
| 64 |
+
await queue.put(f"data: {json.dumps(formatted_chunk)}\n\n")
|
| 65 |
+
except asyncio.CancelledError:
|
| 66 |
+
# log('info', "保持连接任务被取消",
|
| 67 |
+
# extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
|
| 68 |
+
raise
|
| 69 |
+
except Exception as e:
|
| 70 |
+
log('error', f"保持连接任务出错: {str(e)}",
|
| 71 |
+
extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
|
| 72 |
+
# 将错误放入队列
|
| 73 |
+
await queue.put(None)
|
| 74 |
+
raise
|
| 75 |
+
|
| 76 |
+
# 创建一个任务来随机遍历API密钥并请求内容
|
| 77 |
+
async def api_request_handler():
|
| 78 |
+
success = False
|
| 79 |
+
try:
|
| 80 |
+
# 重置已尝试的密钥
|
| 81 |
+
key_manager.reset_tried_keys_for_request()
|
| 82 |
+
|
| 83 |
+
# 获取可用的API密钥
|
| 84 |
+
available_keys = key_manager.api_keys.copy()
|
| 85 |
+
random.shuffle(available_keys) # 随机打乱密钥顺序
|
| 86 |
+
|
| 87 |
+
# 遍历所有API密钥尝试获取响应
|
| 88 |
+
for attempt, api_key in enumerate(available_keys, 1):
|
| 89 |
+
try:
|
| 90 |
+
log('info', f"假流式模式: 尝试API密钥 {api_key[:8]}... ({attempt}/{len(available_keys)})",
|
| 91 |
+
extra={'key': api_key[:8], 'request_type': 'fake-stream', 'model': chat_request.model})
|
| 92 |
+
|
| 93 |
+
# 创建一个新的客户端使用当前API密钥
|
| 94 |
+
non_stream_client = GeminiClient(api_key)
|
| 95 |
+
|
| 96 |
+
# 使用非流式方式请求内容
|
| 97 |
+
response_content = await asyncio.to_thread(
|
| 98 |
+
non_stream_client.complete_chat,
|
| 99 |
+
chat_request,
|
| 100 |
+
contents,
|
| 101 |
+
safety_settings_g2 if 'gemini-2.0-flash-exp' in chat_request.model else safety_settings,
|
| 102 |
+
system_instruction
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# 检查响应是否有效
|
| 106 |
+
if response_content and response_content.text:
|
| 107 |
+
log('info', f"假流式模式: API密钥 {api_key[:8]}... 成功获取响应",
|
| 108 |
+
extra={'key': api_key[:8], 'request_type': 'fake-stream', 'model': chat_request.model})
|
| 109 |
+
|
| 110 |
+
# 将完整响应分割成小块,模拟流式返回
|
| 111 |
+
full_text = response_content.text
|
| 112 |
+
chunk_size = max(len(full_text) // 10, 1) # 至少分成10块,每块至少1个字符
|
| 113 |
+
|
| 114 |
+
for i in range(0, len(full_text), chunk_size):
|
| 115 |
+
chunk = full_text[i:i+chunk_size]
|
| 116 |
+
formatted_chunk = {
|
| 117 |
+
"id": "chatcmpl-someid",
|
| 118 |
+
"object": "chat.completion.chunk",
|
| 119 |
+
"created": int(time.time()),
|
| 120 |
+
"model": chat_request.model,
|
| 121 |
+
"choices": [{"delta": {"role": "assistant", "content": chunk}, "index": 0, "finish_reason": None}]
|
| 122 |
+
}
|
| 123 |
+
# 将格式化的内容块放入队列
|
| 124 |
+
await queue.put(f"data: {json.dumps(formatted_chunk)}\n\n")
|
| 125 |
+
|
| 126 |
+
success = True
|
| 127 |
+
# 更新API调用统计
|
| 128 |
+
update_api_call_stats(api_call_stats, endpoint=api_key,model=chat_request.model)
|
| 129 |
+
break # 成功获取响应,退出循环
|
| 130 |
+
else:
|
| 131 |
+
log('warning', f"假流式模式: API密钥 {api_key[:8]}... 返回空响应",
|
| 132 |
+
extra={'key': api_key[:8], 'request_type': 'fake-stream', 'model': chat_request.model})
|
| 133 |
+
except Exception as e:
|
| 134 |
+
error_detail = handle_gemini_error(e, api_key, key_manager)
|
| 135 |
+
log('error', f"假流式模式: API密钥 {api_key[:8]}... 请求失败: {error_detail}",
|
| 136 |
+
extra={'key': api_key[:8], 'request_type': 'fake-stream', 'model': chat_request.model})
|
| 137 |
+
# 继续尝试下一个API密钥
|
| 138 |
+
|
| 139 |
+
# 如果所有API密钥都尝试失败
|
| 140 |
+
if not success:
|
| 141 |
+
error_msg = "所有API密钥均请求失败,请稍后重试"
|
| 142 |
+
log('error', error_msg,
|
| 143 |
+
extra={'key': 'ALL', 'request_type': 'fake-stream', 'model': chat_request.model})
|
| 144 |
+
|
| 145 |
+
# 添加错误信息到队列
|
| 146 |
+
error_json = {
|
| 147 |
+
"id": "chatcmpl-error",
|
| 148 |
+
"object": "chat.completion.chunk",
|
| 149 |
+
"created": int(time.time()),
|
| 150 |
+
"model": chat_request.model,
|
| 151 |
+
"choices": [{"delta": {"content": f"\n\n[错误: {error_msg}]"}, "index": 0, "finish_reason": "error"}]
|
| 152 |
+
}
|
| 153 |
+
await queue.put(f"data: {json.dumps(error_json)}\n\n")
|
| 154 |
+
|
| 155 |
+
# 添加完成标记到队列
|
| 156 |
+
await queue.put("data: [DONE]\n\n")
|
| 157 |
+
# 添加None表示队列结束
|
| 158 |
+
await queue.put(None)
|
| 159 |
+
|
| 160 |
+
except asyncio.CancelledError:
|
| 161 |
+
log('info', "API请求任务被取消",
|
| 162 |
+
extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
|
| 163 |
+
# 添加None表示队列结束
|
| 164 |
+
await queue.put(None)
|
| 165 |
+
raise
|
| 166 |
+
except Exception as e:
|
| 167 |
+
log('error', f"API请求任务出错: {str(e)}",
|
| 168 |
+
extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
|
| 169 |
+
# 添加错误信息到队列
|
| 170 |
+
error_json = {
|
| 171 |
+
"id": "chatcmpl-error",
|
| 172 |
+
"object": "chat.completion.chunk",
|
| 173 |
+
"created": int(time.time()),
|
| 174 |
+
"model": chat_request.model,
|
| 175 |
+
"choices": [{"delta": {"content": f"\n\n[错误: {str(e)}]"}, "index": 0, "finish_reason": "error"}]
|
| 176 |
+
}
|
| 177 |
+
await queue.put(f"data: {json.dumps(error_json)}\n\n")
|
| 178 |
+
await queue.put("data: [DONE]\n\n")
|
| 179 |
+
# 添加None表示队列结束
|
| 180 |
+
await queue.put(None)
|
| 181 |
+
raise
|
| 182 |
+
|
| 183 |
+
# 启动保持连接的任务
|
| 184 |
+
keep_alive_task = asyncio.create_task(keep_alive_sender())
|
| 185 |
+
# 启动API请求任务
|
| 186 |
+
api_request_task = asyncio.create_task(api_request_handler())
|
| 187 |
+
|
| 188 |
+
# 从队列中获取数据并发送给客户端
|
| 189 |
+
while True:
|
| 190 |
+
chunk = await queue.get()
|
| 191 |
+
if chunk is None: # None表示队列结束
|
| 192 |
+
break
|
| 193 |
+
yield chunk
|
| 194 |
+
|
| 195 |
+
# 如果API请求任务已完成,取消保持连接任务
|
| 196 |
+
if api_request_task.done() and not keep_alive_task.done():
|
| 197 |
+
keep_alive_task.cancel()
|
| 198 |
+
|
| 199 |
+
except asyncio.CancelledError:
|
| 200 |
+
log('info', "流式响应生成器被取消",
|
| 201 |
+
extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
|
| 202 |
+
# 取消所有任务
|
| 203 |
+
if keep_alive_task and not keep_alive_task.done():
|
| 204 |
+
keep_alive_task.cancel()
|
| 205 |
+
if api_request_task and not api_request_task.done():
|
| 206 |
+
api_request_task.cancel()
|
| 207 |
+
except Exception as e:
|
| 208 |
+
log('error', f"流式响应生成器出错: {str(e)}",
|
| 209 |
+
extra={'key': current_api_key[:8], 'request_type': 'fake-stream'})
|
| 210 |
+
# 取消所有任务
|
| 211 |
+
if keep_alive_task and not keep_alive_task.done():
|
| 212 |
+
keep_alive_task.cancel()
|
| 213 |
+
if api_request_task and not api_request_task.done():
|
| 214 |
+
api_request_task.cancel()
|
| 215 |
+
# 发送错误信息给客户端
|
| 216 |
+
error_json = {
|
| 217 |
+
"id": "chatcmpl-error",
|
| 218 |
+
"object": "chat.completion.chunk",
|
| 219 |
+
"created": int(time.time()),
|
| 220 |
+
"model": chat_request.model,
|
| 221 |
+
"choices": [{"delta": {"content": f"\n\n[错误: {str(e)}]"}, "index": 0, "finish_reason": "error"}]
|
| 222 |
+
}
|
| 223 |
+
yield f"data: {json.dumps(error_json)}\n\n"
|
| 224 |
+
yield "data: [DONE]\n\n"
|
| 225 |
+
finally:
|
| 226 |
+
# 确保所有任务都被取消
|
| 227 |
+
if keep_alive_task and not keep_alive_task.done():
|
| 228 |
+
keep_alive_task.cancel()
|
| 229 |
+
if api_request_task and not api_request_task.done():
|
| 230 |
+
api_request_task.cancel()
|
| 231 |
+
else:
|
| 232 |
+
# 原始流式请求处理逻辑
|
| 233 |
+
gemini_client = GeminiClient(current_api_key)
|
| 234 |
+
success = False
|
| 235 |
+
|
| 236 |
+
try:
|
| 237 |
+
# 直接迭代生成器并发送响应块
|
| 238 |
+
async for chunk in gemini_client.stream_chat(
|
| 239 |
+
chat_request,
|
| 240 |
+
contents,
|
| 241 |
+
safety_settings_g2 if 'gemini-2.0-flash-exp' in chat_request.model else safety_settings,
|
| 242 |
+
system_instruction
|
| 243 |
+
):
|
| 244 |
+
# 空字符串跳过
|
| 245 |
+
if not chunk:
|
| 246 |
+
continue
|
| 247 |
+
|
| 248 |
+
formatted_chunk = {
|
| 249 |
+
"id": "chatcmpl-someid",
|
| 250 |
+
"object": "chat.completion.chunk",
|
| 251 |
+
"created": int(time.time()),
|
| 252 |
+
"model": chat_request.model,
|
| 253 |
+
"choices": [{"delta": {"role": "assistant", "content": chunk}, "index": 0, "finish_reason": None}]
|
| 254 |
+
}
|
| 255 |
+
success = True # 只要有一个chunk成功,就标记为成功
|
| 256 |
+
yield f"data: {json.dumps(formatted_chunk)}\n\n"
|
| 257 |
+
|
| 258 |
+
# 如果成功获取到响应,更新API调用统计
|
| 259 |
+
if success:
|
| 260 |
+
update_api_call_stats(api_call_stats,endpoint=current_api_key,model=chat_request.model)
|
| 261 |
+
|
| 262 |
+
yield "data: [DONE]\n\n"
|
| 263 |
+
|
| 264 |
+
except asyncio.CancelledError:
|
| 265 |
+
log('info', "客户端连接已中断",
|
| 266 |
+
extra={'key': current_api_key[:8], 'request_type': 'stream', 'model': chat_request.model})
|
| 267 |
+
except Exception as e:
|
| 268 |
+
error_detail = handle_gemini_error(e, current_api_key, key_manager)
|
| 269 |
+
log('error', f"流式请求失败: {error_detail}",
|
| 270 |
+
extra={'key': current_api_key[:8], 'request_type': 'stream', 'model': chat_request.model})
|
| 271 |
+
# 发送错误信息给客户端
|
| 272 |
+
error_json = {
|
| 273 |
+
"id": "chatcmpl-error",
|
| 274 |
+
"object": "chat.completion.chunk",
|
| 275 |
+
"created": int(time.time()),
|
| 276 |
+
"model": chat_request.model,
|
| 277 |
+
"choices": [{"delta": {"content": f"\n\n[错误: {error_detail}]"}, "index": 0, "finish_reason": "error"}]
|
| 278 |
+
}
|
| 279 |
+
yield f"data: {json.dumps(error_json)}\n\n"
|
| 280 |
+
yield "data: [DONE]\n\n"
|
| 281 |
+
# 重新抛出异常,这样process_request可以捕获它
|
| 282 |
+
raise e
|
| 283 |
+
|
| 284 |
+
return StreamingResponse(stream_response_generator(), media_type="text/event-stream")
|
app/config/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 配置模块初始化文件
|
| 2 |
+
from app.config.settings import *
|
| 3 |
+
from app.config.safety import *
|
app/config/safety.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 安全设置配置
|
| 2 |
+
|
| 3 |
+
# Gemini 1.0 安全设置
|
| 4 |
+
SAFETY_SETTINGS = [
|
| 5 |
+
{
|
| 6 |
+
"category": "HARM_CATEGORY_HARASSMENT",
|
| 7 |
+
"threshold": "BLOCK_NONE"
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"category": "HARM_CATEGORY_HATE_SPEECH",
|
| 11 |
+
"threshold": "BLOCK_NONE"
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
| 15 |
+
"threshold": "BLOCK_NONE"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
| 19 |
+
"threshold": "BLOCK_NONE"
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"category": 'HARM_CATEGORY_CIVIC_INTEGRITY',
|
| 23 |
+
"threshold": 'BLOCK_NONE'
|
| 24 |
+
}
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
# Gemini 2.0 安全设置
|
| 28 |
+
SAFETY_SETTINGS_G2 = [
|
| 29 |
+
{
|
| 30 |
+
"category": "HARM_CATEGORY_HARASSMENT",
|
| 31 |
+
"threshold": "OFF"
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"category": "HARM_CATEGORY_HATE_SPEECH",
|
| 35 |
+
"threshold": "OFF"
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
| 39 |
+
"threshold": "OFF"
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
| 43 |
+
"threshold": "OFF"
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"category": 'HARM_CATEGORY_CIVIC_INTEGRITY',
|
| 47 |
+
"threshold": 'OFF'
|
| 48 |
+
}
|
| 49 |
+
]
|
app/config/settings.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pathlib
|
| 3 |
+
import logging
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
|
| 6 |
+
# 基础目录设置
|
| 7 |
+
BASE_DIR = pathlib.Path(__file__).parent.parent
|
| 8 |
+
|
| 9 |
+
# 流式响应配置
|
| 10 |
+
FAKE_STREAMING = os.environ.get("FAKE_STREAMING", "true").lower() in ["true", "1", "yes"]
|
| 11 |
+
# 假流式请求的空内容返回间隔(秒)
|
| 12 |
+
FAKE_STREAMING_INTERVAL = float(os.environ.get("FAKE_STREAMING_INTERVAL", "1"))
|
| 13 |
+
|
| 14 |
+
#随机字符串
|
| 15 |
+
RANDOM_STRING = os.environ.get("RANDOM_STRING", "true").lower() in ["true", "1", "yes"]
|
| 16 |
+
RANDOM_STRING_LENGTH = int(os.environ.get("RANDOM_STRING_LENGTH", "5"))
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# 日志配置
|
| 20 |
+
logging.getLogger("uvicorn").disabled = True
|
| 21 |
+
logging.getLogger("uvicorn.access").disabled = True
|
| 22 |
+
|
| 23 |
+
# 安全配置
|
| 24 |
+
PASSWORD = os.environ.get("PASSWORD", "123").strip('"')
|
| 25 |
+
MAX_REQUESTS_PER_MINUTE = int(os.environ.get("MAX_REQUESTS_PER_MINUTE", "30"))
|
| 26 |
+
MAX_REQUESTS_PER_DAY_PER_IP = int(os.environ.get("MAX_REQUESTS_PER_DAY_PER_IP", "600"))
|
| 27 |
+
RETRY_DELAY = 1
|
| 28 |
+
MAX_RETRY_DELAY = 16
|
| 29 |
+
|
| 30 |
+
# API密钥使用限制
|
| 31 |
+
# 默认每个API密钥每24小时可使用次数
|
| 32 |
+
API_KEY_DAILY_LIMIT = int(os.environ.get("API_KEY_DAILY_LIMIT", "100"))
|
| 33 |
+
|
| 34 |
+
# 缓存配置
|
| 35 |
+
CACHE_EXPIRY_TIME = int(os.environ.get("CACHE_EXPIRY_TIME", "1200")) # 默认20分钟
|
| 36 |
+
MAX_CACHE_ENTRIES = int(os.environ.get("MAX_CACHE_ENTRIES", "500")) # 默认最多缓存500条响应
|
| 37 |
+
REMOVE_CACHE_AFTER_USE = os.environ.get("REMOVE_CACHE_AFTER_USE", "true").lower() in ["true", "1", "yes"]
|
| 38 |
+
|
| 39 |
+
# 请求历史配置
|
| 40 |
+
REQUEST_HISTORY_EXPIRY_TIME = int(os.environ.get("REQUEST_HISTORY_EXPIRY_TIME", "600")) # 默认10分钟
|
| 41 |
+
ENABLE_RECONNECT_DETECTION = os.environ.get("ENABLE_RECONNECT_DETECTION", "true").lower() in ["true", "1", "yes"]
|
| 42 |
+
|
| 43 |
+
serach={
|
| 44 |
+
"search_mode":os.environ.get("SERACH_MODE", "true").lower() in ["true", "1", "yes"],
|
| 45 |
+
"search_prompt":os.environ.get("SERACH_PROMPT", "(使用搜索工具联网搜索,需要在content中结合搜索内容)").strip('"')
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
version={
|
| 49 |
+
"local_version":"0.0.0",
|
| 50 |
+
"remote_version":"0.0.0",
|
| 51 |
+
"has_update":False
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
# API调用统计
|
| 55 |
+
api_call_stats = {
|
| 56 |
+
'last_24h': {
|
| 57 |
+
'total': {}, # 按小时统计过去24小时总调用次数
|
| 58 |
+
'by_endpoint': {} # 按API端点分类的24小时统计(也用于API密钥统计)
|
| 59 |
+
},
|
| 60 |
+
'hourly': {
|
| 61 |
+
'total': {}, # 按小时统计过去一小时总调用次数
|
| 62 |
+
'by_endpoint': {} # 按API端点分类的小时统计(也用于API密钥统计)
|
| 63 |
+
},
|
| 64 |
+
'minute': {
|
| 65 |
+
'total': {}, # 按分钟统计过去一分钟总调用次数
|
| 66 |
+
'by_endpoint': {} # 按API端点分类的分钟统计(也用于API密钥统计)
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
# 客户端IP到最近请求的映射,用于识别重连请求
|
| 71 |
+
client_request_history = {}
|
| 72 |
+
|
| 73 |
+
# 模型屏蔽列表配置
|
| 74 |
+
# 默认屏蔽的模型列表
|
| 75 |
+
DEFAULT_BLOCKED_MODELS = []
|
| 76 |
+
|
| 77 |
+
# 从环境变量中读取屏蔽模型列表,如果未设置则使用默认列表
|
| 78 |
+
# 环境变量格式应为逗号分隔的模型名称字符串
|
| 79 |
+
BLOCKED_MODELS = os.environ.get("BLOCKED_MODELS", ",".join(DEFAULT_BLOCKED_MODELS))
|
| 80 |
+
# 将字符串转换为列表
|
| 81 |
+
BLOCKED_MODELS = [model.strip() for model in BLOCKED_MODELS.split(",") if model.strip()]
|
app/main.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, HTTPException, Request, status
|
| 2 |
+
from fastapi.responses import JSONResponse, HTMLResponse
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.templating import Jinja2Templates
|
| 5 |
+
from app.models import ErrorResponse
|
| 6 |
+
from app.services import GeminiClient
|
| 7 |
+
from app.utils import (
|
| 8 |
+
APIKeyManager,
|
| 9 |
+
test_api_key,
|
| 10 |
+
format_log_message,
|
| 11 |
+
log_manager,
|
| 12 |
+
ResponseCacheManager,
|
| 13 |
+
ActiveRequestsManager,
|
| 14 |
+
clean_expired_stats,
|
| 15 |
+
update_api_call_stats,
|
| 16 |
+
check_version,
|
| 17 |
+
schedule_cache_cleanup,
|
| 18 |
+
handle_exception,
|
| 19 |
+
log
|
| 20 |
+
)
|
| 21 |
+
from app.api import router, init_router, dashboard_router, init_dashboard_router
|
| 22 |
+
from app.config.settings import (
|
| 23 |
+
FAKE_STREAMING,
|
| 24 |
+
FAKE_STREAMING_INTERVAL,
|
| 25 |
+
RANDOM_STRING,
|
| 26 |
+
RANDOM_STRING_LENGTH,
|
| 27 |
+
PASSWORD,
|
| 28 |
+
MAX_REQUESTS_PER_MINUTE,
|
| 29 |
+
MAX_REQUESTS_PER_DAY_PER_IP,
|
| 30 |
+
RETRY_DELAY,
|
| 31 |
+
MAX_RETRY_DELAY,
|
| 32 |
+
CACHE_EXPIRY_TIME,
|
| 33 |
+
MAX_CACHE_ENTRIES,
|
| 34 |
+
REMOVE_CACHE_AFTER_USE,
|
| 35 |
+
REQUEST_HISTORY_EXPIRY_TIME,
|
| 36 |
+
ENABLE_RECONNECT_DETECTION,
|
| 37 |
+
api_call_stats,
|
| 38 |
+
client_request_history,
|
| 39 |
+
version,
|
| 40 |
+
API_KEY_DAILY_LIMIT
|
| 41 |
+
)
|
| 42 |
+
from app.config.safety import SAFETY_SETTINGS, SAFETY_SETTINGS_G2
|
| 43 |
+
import os
|
| 44 |
+
import json
|
| 45 |
+
import asyncio
|
| 46 |
+
import time
|
| 47 |
+
import logging
|
| 48 |
+
from datetime import datetime, timedelta
|
| 49 |
+
import sys
|
| 50 |
+
import pathlib
|
| 51 |
+
import threading
|
| 52 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 53 |
+
|
| 54 |
+
# 设置模板目录
|
| 55 |
+
BASE_DIR = pathlib.Path(__file__).parent
|
| 56 |
+
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
| 57 |
+
|
| 58 |
+
app = FastAPI(limit="50M")
|
| 59 |
+
|
| 60 |
+
# --------------- 全局实例 ---------------
|
| 61 |
+
|
| 62 |
+
# 初始化API密钥管理器
|
| 63 |
+
key_manager = APIKeyManager()
|
| 64 |
+
current_api_key = key_manager.get_available_key()
|
| 65 |
+
|
| 66 |
+
# 创建全局缓存字典,将作为缓存管理器的内部存储
|
| 67 |
+
response_cache = {}
|
| 68 |
+
|
| 69 |
+
# 初始化缓存管理器,使用全局字典作为存储
|
| 70 |
+
response_cache_manager = ResponseCacheManager(
|
| 71 |
+
expiry_time=CACHE_EXPIRY_TIME,
|
| 72 |
+
max_entries=MAX_CACHE_ENTRIES,
|
| 73 |
+
remove_after_use=REMOVE_CACHE_AFTER_USE,
|
| 74 |
+
cache_dict=response_cache
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# 活跃请求池 - 将作为活跃请求管理器的内部存储
|
| 78 |
+
active_requests_pool = {}
|
| 79 |
+
|
| 80 |
+
# 初始化活跃请求管理器
|
| 81 |
+
active_requests_manager = ActiveRequestsManager(requests_pool=active_requests_pool)
|
| 82 |
+
|
| 83 |
+
# --------------- 工具函数 ---------------
|
| 84 |
+
|
| 85 |
+
def switch_api_key():
|
| 86 |
+
global current_api_key
|
| 87 |
+
key = key_manager.get_available_key() # get_available_key 会处理栈的逻辑
|
| 88 |
+
if key:
|
| 89 |
+
current_api_key = key
|
| 90 |
+
log('info', f"API key 替换为 → {current_api_key[:8]}...", extra={'key': current_api_key[:8], 'request_type': 'switch_key'})
|
| 91 |
+
else:
|
| 92 |
+
log('error', "API key 替换失败,所有API key都已尝试,请重新配置或稍后重试", extra={'key': 'N/A', 'request_type': 'switch_key', 'status_code': 'N/A'})
|
| 93 |
+
|
| 94 |
+
async def check_key(key):
|
| 95 |
+
"""检查单个API密钥是否有效"""
|
| 96 |
+
is_valid = await test_api_key(key)
|
| 97 |
+
status_msg = "有效" if is_valid else "无效"
|
| 98 |
+
log('info', f"API Key {key[:10]}... {status_msg}.")
|
| 99 |
+
return key if is_valid else None
|
| 100 |
+
|
| 101 |
+
def check_key_in_thread(key):
|
| 102 |
+
"""在线程中运行异步检查密钥函数"""
|
| 103 |
+
loop = asyncio.new_event_loop()
|
| 104 |
+
asyncio.set_event_loop(loop)
|
| 105 |
+
try:
|
| 106 |
+
valid_key = loop.run_until_complete(check_key(key))
|
| 107 |
+
if valid_key:
|
| 108 |
+
# 如果密钥有效,立即添加到可用列表中
|
| 109 |
+
key_manager.api_keys.append(valid_key)
|
| 110 |
+
# 重置密钥栈以包含新的有效密钥
|
| 111 |
+
key_manager._reset_key_stack()
|
| 112 |
+
log('info', f"API Key {valid_key[:8]}... 已添加到可用列表")
|
| 113 |
+
return valid_key
|
| 114 |
+
finally:
|
| 115 |
+
loop.close()
|
| 116 |
+
|
| 117 |
+
async def check_keys():
|
| 118 |
+
"""启动线程池来并行检查所有密钥"""
|
| 119 |
+
# 保存原始密钥列表的副本
|
| 120 |
+
all_keys = key_manager.api_keys.copy()
|
| 121 |
+
# 清空当前密钥列表,将在检查过程中逐个添加有效密钥
|
| 122 |
+
key_manager.api_keys = []
|
| 123 |
+
|
| 124 |
+
log('info', f"开始在单独线程中检查 {len(all_keys)} 个API密钥...")
|
| 125 |
+
|
| 126 |
+
# 创建线程池
|
| 127 |
+
with ThreadPoolExecutor(max_workers=min(10, len(all_keys))) as executor:
|
| 128 |
+
# 提交所有密钥检查任务
|
| 129 |
+
future_to_key = {executor.submit(check_key_in_thread, key): key for key in all_keys}
|
| 130 |
+
|
| 131 |
+
# 等待所有任务完成
|
| 132 |
+
for future in future_to_key:
|
| 133 |
+
try:
|
| 134 |
+
future.result() # 获取结果但不需要处理,因为check_key_in_thread已经处理了有效密钥
|
| 135 |
+
except Exception as exc:
|
| 136 |
+
log('error', f"检查密钥时发生错误: {exc}")
|
| 137 |
+
|
| 138 |
+
if not key_manager.api_keys:
|
| 139 |
+
log('error', "没有可用的 API 密钥!", extra={'key': 'N/A', 'request_type': 'startup', 'status_code': 'N/A'})
|
| 140 |
+
|
| 141 |
+
return key_manager.api_keys
|
| 142 |
+
|
| 143 |
+
# 设置全局异常处理
|
| 144 |
+
sys.excepthook = handle_exception
|
| 145 |
+
|
| 146 |
+
# --------------- 事件处理 ---------------
|
| 147 |
+
|
| 148 |
+
@app.on_event("startup")
|
| 149 |
+
async def startup_event():
|
| 150 |
+
log('info', "Starting Gemini API proxy...")
|
| 151 |
+
|
| 152 |
+
# 启动缓存清理定时任务
|
| 153 |
+
schedule_cache_cleanup(response_cache_manager, active_requests_manager)
|
| 154 |
+
|
| 155 |
+
# 检查版本
|
| 156 |
+
await check_version()
|
| 157 |
+
|
| 158 |
+
# 先同步检查一个密钥,用于加载可用模型
|
| 159 |
+
all_keys = key_manager.api_keys.copy()
|
| 160 |
+
key_manager.api_keys = [] # 清空当前密钥列表
|
| 161 |
+
|
| 162 |
+
# 尝试找到一个有效的密钥
|
| 163 |
+
valid_key_found = False
|
| 164 |
+
for key in all_keys:
|
| 165 |
+
is_valid = await test_api_key(key)
|
| 166 |
+
if is_valid:
|
| 167 |
+
key_manager.api_keys.append(key)
|
| 168 |
+
key_manager._reset_key_stack()
|
| 169 |
+
log('info', f"初始检查: API Key {key[:8]}... 有效,已添加到可用列表")
|
| 170 |
+
valid_key_found = True
|
| 171 |
+
|
| 172 |
+
# 使用这个有效密钥加载可用模型
|
| 173 |
+
try:
|
| 174 |
+
all_models = await GeminiClient.list_available_models(key)
|
| 175 |
+
GeminiClient.AVAILABLE_MODELS = [model.replace(
|
| 176 |
+
"models/", "") for model in all_models]
|
| 177 |
+
log('info', "Available models loaded.")
|
| 178 |
+
except Exception as e:
|
| 179 |
+
log('warning', f"无法加载可用模型: {str(e)}")
|
| 180 |
+
|
| 181 |
+
break # 找到一个有效密钥后就跳出循环
|
| 182 |
+
|
| 183 |
+
if not valid_key_found:
|
| 184 |
+
log('warning', "初始检查未找到有效密钥,将在后台继续检查")
|
| 185 |
+
|
| 186 |
+
# 在后台线程中检查剩余的密钥
|
| 187 |
+
remaining_keys = [k for k in all_keys if k not in key_manager.api_keys]
|
| 188 |
+
if remaining_keys:
|
| 189 |
+
def check_remaining_keys():
|
| 190 |
+
loop = asyncio.new_event_loop()
|
| 191 |
+
asyncio.set_event_loop(loop)
|
| 192 |
+
try:
|
| 193 |
+
# 创建线程池检查剩余密钥
|
| 194 |
+
with ThreadPoolExecutor(max_workers=min(10, len(remaining_keys))) as executor:
|
| 195 |
+
future_to_key = {executor.submit(check_key_in_thread, key): key for key in remaining_keys}
|
| 196 |
+
for future in future_to_key:
|
| 197 |
+
try:
|
| 198 |
+
future.result()
|
| 199 |
+
except Exception as exc:
|
| 200 |
+
log('error', f"检查密钥时发生错误: {exc}")
|
| 201 |
+
finally:
|
| 202 |
+
loop.close()
|
| 203 |
+
log('info', f"后台密钥检查完成,当前可用密钥数量: {len(key_manager.api_keys)}")
|
| 204 |
+
|
| 205 |
+
# 启动后台线程检查剩余密钥
|
| 206 |
+
threading.Thread(target=check_remaining_keys, daemon=True).start()
|
| 207 |
+
log('info', f"后台线程已启动,正在检查剩余的 {len(remaining_keys)} 个API密钥...")
|
| 208 |
+
|
| 209 |
+
# 显示当前可用密钥
|
| 210 |
+
key_manager.show_all_keys()
|
| 211 |
+
log('info', f"当前可用 API 密钥数量:{len(key_manager.api_keys)}")
|
| 212 |
+
log('info', f"最大重试次数设置为:{len(key_manager.api_keys)}")
|
| 213 |
+
|
| 214 |
+
# 初始化路由器
|
| 215 |
+
init_router(
|
| 216 |
+
key_manager,
|
| 217 |
+
response_cache_manager,
|
| 218 |
+
active_requests_manager,
|
| 219 |
+
SAFETY_SETTINGS,
|
| 220 |
+
SAFETY_SETTINGS_G2,
|
| 221 |
+
current_api_key,
|
| 222 |
+
FAKE_STREAMING,
|
| 223 |
+
FAKE_STREAMING_INTERVAL,
|
| 224 |
+
PASSWORD,
|
| 225 |
+
MAX_REQUESTS_PER_MINUTE,
|
| 226 |
+
MAX_REQUESTS_PER_DAY_PER_IP
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
# 初始化仪表盘路由器
|
| 230 |
+
init_dashboard_router(
|
| 231 |
+
key_manager,
|
| 232 |
+
response_cache_manager,
|
| 233 |
+
active_requests_manager
|
| 234 |
+
)
|
| 235 |
+
|
| 236 |
+
# 初始化路由器
|
| 237 |
+
init_router(
|
| 238 |
+
key_manager,
|
| 239 |
+
response_cache_manager,
|
| 240 |
+
active_requests_manager,
|
| 241 |
+
SAFETY_SETTINGS,
|
| 242 |
+
SAFETY_SETTINGS_G2,
|
| 243 |
+
current_api_key,
|
| 244 |
+
FAKE_STREAMING,
|
| 245 |
+
FAKE_STREAMING_INTERVAL,
|
| 246 |
+
PASSWORD,
|
| 247 |
+
MAX_REQUESTS_PER_MINUTE,
|
| 248 |
+
MAX_REQUESTS_PER_DAY_PER_IP
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
# 初始化仪表盘路由器
|
| 252 |
+
init_dashboard_router(
|
| 253 |
+
key_manager,
|
| 254 |
+
response_cache_manager,
|
| 255 |
+
active_requests_manager
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
+
# --------------- 异常处理 ---------------
|
| 259 |
+
|
| 260 |
+
@app.exception_handler(Exception)
|
| 261 |
+
async def global_exception_handler(request: Request, exc: Exception):
|
| 262 |
+
from app.utils import translate_error
|
| 263 |
+
error_message = translate_error(str(exc))
|
| 264 |
+
extra_log_unhandled_exception = {'status_code': 500, 'error_message': error_message}
|
| 265 |
+
log('error', f"Unhandled exception: {error_message}", extra=extra_log_unhandled_exception)
|
| 266 |
+
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=ErrorResponse(message=str(exc), type="internal_error").dict())
|
| 267 |
+
|
| 268 |
+
# --------------- 路由 ---------------
|
| 269 |
+
|
| 270 |
+
# 包含API路由
|
| 271 |
+
app.include_router(router)
|
| 272 |
+
app.include_router(dashboard_router)
|
| 273 |
+
|
| 274 |
+
# 挂载静态文件目录
|
| 275 |
+
app.mount("/assets", StaticFiles(directory="app/templates/assets"), name="assets")
|
| 276 |
+
|
| 277 |
+
@app.get("/", response_class=HTMLResponse)
|
| 278 |
+
async def root(request: Request):
|
| 279 |
+
"""
|
| 280 |
+
根路由 - 返回静态 HTML 文件
|
| 281 |
+
"""
|
| 282 |
+
# 直接返回 index.html 文件
|
| 283 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
app/models/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.models.schemas import (
|
| 2 |
+
Message,
|
| 3 |
+
ChatCompletionRequest,
|
| 4 |
+
Choice,
|
| 5 |
+
Usage,
|
| 6 |
+
ChatCompletionResponse,
|
| 7 |
+
ErrorResponse,
|
| 8 |
+
ModelList
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
'Message',
|
| 13 |
+
'ChatCompletionRequest',
|
| 14 |
+
'Choice',
|
| 15 |
+
'Usage',
|
| 16 |
+
'ChatCompletionResponse',
|
| 17 |
+
'ErrorResponse',
|
| 18 |
+
'ModelList'
|
| 19 |
+
]
|
app/models/schemas.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Optional, Union, Literal, Any
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
|
| 4 |
+
class Message(BaseModel):
|
| 5 |
+
role: str
|
| 6 |
+
content: Union[str, List[Dict[str, Any]]]
|
| 7 |
+
|
| 8 |
+
class ChatCompletionRequest(BaseModel):
|
| 9 |
+
model: str
|
| 10 |
+
messages: List[Message]
|
| 11 |
+
temperature: float = 0.7
|
| 12 |
+
top_p: Optional[float] = 1.0
|
| 13 |
+
n: int = 1
|
| 14 |
+
stream: bool = False
|
| 15 |
+
stop: Optional[Union[str, List[str]]] = None
|
| 16 |
+
max_tokens: Optional[int] = None
|
| 17 |
+
presence_penalty: Optional[float] = 0.0
|
| 18 |
+
frequency_penalty: Optional[float] = 0.0
|
| 19 |
+
|
| 20 |
+
class Choice(BaseModel):
|
| 21 |
+
index: int
|
| 22 |
+
message: Message
|
| 23 |
+
finish_reason: Optional[str] = None
|
| 24 |
+
|
| 25 |
+
class Usage(BaseModel):
|
| 26 |
+
prompt_tokens: int = 0
|
| 27 |
+
completion_tokens: int = 0
|
| 28 |
+
total_tokens: int = 0
|
| 29 |
+
|
| 30 |
+
class ChatCompletionResponse(BaseModel):
|
| 31 |
+
id: str
|
| 32 |
+
object: Literal["chat.completion"]
|
| 33 |
+
created: int
|
| 34 |
+
model: str
|
| 35 |
+
choices: List[Choice]
|
| 36 |
+
usage: Usage = Field(default_factory=Usage)
|
| 37 |
+
|
| 38 |
+
class ErrorResponse(BaseModel):
|
| 39 |
+
message: str
|
| 40 |
+
type: str
|
| 41 |
+
param: Optional[str] = None
|
| 42 |
+
code: Optional[str] = None
|
| 43 |
+
|
| 44 |
+
class ModelList(BaseModel):
|
| 45 |
+
object: str = "list"
|
| 46 |
+
data: List[Dict]
|
app/services/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.services.gemini import GeminiClient, ResponseWrapper, GeneratedText
|
| 2 |
+
|
| 3 |
+
__all__ = [
|
| 4 |
+
'GeminiClient',
|
| 5 |
+
'ResponseWrapper',
|
| 6 |
+
'GeneratedText'
|
| 7 |
+
]
|
app/services/gemini.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import asyncio
|
| 5 |
+
import time
|
| 6 |
+
from app.models import ChatCompletionRequest, Message
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from typing import Optional, Dict, Any, List
|
| 9 |
+
import httpx
|
| 10 |
+
import logging
|
| 11 |
+
import secrets
|
| 12 |
+
import string
|
| 13 |
+
from app.utils import format_log_message
|
| 14 |
+
from app.config import settings
|
| 15 |
+
from app.config.settings import (
|
| 16 |
+
RANDOM_STRING,
|
| 17 |
+
RANDOM_STRING_LENGTH,
|
| 18 |
+
serach
|
| 19 |
+
)
|
| 20 |
+
from app.utils.logging import log
|
| 21 |
+
|
| 22 |
+
def generate_secure_random_string(length):
|
| 23 |
+
all_characters = string.ascii_letters + string.digits
|
| 24 |
+
secure_random_string = ''.join(secrets.choice(all_characters) for _ in range(length))
|
| 25 |
+
return secure_random_string
|
| 26 |
+
|
| 27 |
+
logger = logging.getLogger('my_logger')
|
| 28 |
+
|
| 29 |
+
# 是否启用假流式请求 默认启用
|
| 30 |
+
FAKE_STREAMING = os.environ.get("FAKE_STREAMING", "true").lower() in ["true", "1", "yes"]
|
| 31 |
+
# 假流式请求的空内容返回间隔(秒)
|
| 32 |
+
FAKE_STREAMING_INTERVAL = float(os.environ.get("FAKE_STREAMING_INTERVAL", "1"))
|
| 33 |
+
|
| 34 |
+
@dataclass
|
| 35 |
+
class GeneratedText:
|
| 36 |
+
text: str
|
| 37 |
+
finish_reason: Optional[str] = None
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class ResponseWrapper:
|
| 41 |
+
def __init__(self, data: Dict[Any, Any]): # 正确的初始化方法名
|
| 42 |
+
self._data = data
|
| 43 |
+
self._text = self._extract_text()
|
| 44 |
+
self._finish_reason = self._extract_finish_reason()
|
| 45 |
+
self._prompt_token_count = self._extract_prompt_token_count()
|
| 46 |
+
self._candidates_token_count = self._extract_candidates_token_count()
|
| 47 |
+
self._total_token_count = self._extract_total_token_count()
|
| 48 |
+
self._thoughts = self._extract_thoughts()
|
| 49 |
+
self._json_dumps = json.dumps(self._data, indent=4, ensure_ascii=False)
|
| 50 |
+
|
| 51 |
+
def _extract_thoughts(self) -> Optional[str]:
|
| 52 |
+
try:
|
| 53 |
+
for part in self._data['candidates'][0]['content']['parts']:
|
| 54 |
+
if 'thought' in part:
|
| 55 |
+
return part['text']
|
| 56 |
+
return ""
|
| 57 |
+
except (KeyError, IndexError):
|
| 58 |
+
return ""
|
| 59 |
+
|
| 60 |
+
def _extract_text(self) -> str:
|
| 61 |
+
try:
|
| 62 |
+
text=""
|
| 63 |
+
for part in self._data['candidates'][0]['content']['parts']:
|
| 64 |
+
if 'thought' not in part:
|
| 65 |
+
text+=part['text']
|
| 66 |
+
return part['text']
|
| 67 |
+
except (KeyError, IndexError):
|
| 68 |
+
return ""
|
| 69 |
+
|
| 70 |
+
def _extract_finish_reason(self) -> Optional[str]:
|
| 71 |
+
try:
|
| 72 |
+
return self._data['candidates'][0].get('finishReason')
|
| 73 |
+
except (KeyError, IndexError):
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
def _extract_prompt_token_count(self) -> Optional[int]:
|
| 77 |
+
try:
|
| 78 |
+
return self._data['usageMetadata'].get('promptTokenCount')
|
| 79 |
+
except (KeyError):
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
def _extract_candidates_token_count(self) -> Optional[int]:
|
| 83 |
+
try:
|
| 84 |
+
return self._data['usageMetadata'].get('candidatesTokenCount')
|
| 85 |
+
except (KeyError):
|
| 86 |
+
return None
|
| 87 |
+
|
| 88 |
+
def _extract_total_token_count(self) -> Optional[int]:
|
| 89 |
+
try:
|
| 90 |
+
return self._data['usageMetadata'].get('totalTokenCount')
|
| 91 |
+
except (KeyError):
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
@property
|
| 95 |
+
def text(self) -> str:
|
| 96 |
+
return self._text
|
| 97 |
+
|
| 98 |
+
@property
|
| 99 |
+
def finish_reason(self) -> Optional[str]:
|
| 100 |
+
return self._finish_reason
|
| 101 |
+
|
| 102 |
+
@property
|
| 103 |
+
def prompt_token_count(self) -> Optional[int]:
|
| 104 |
+
return self._prompt_token_count
|
| 105 |
+
|
| 106 |
+
@property
|
| 107 |
+
def candidates_token_count(self) -> Optional[int]:
|
| 108 |
+
return self._candidates_token_count
|
| 109 |
+
|
| 110 |
+
@property
|
| 111 |
+
def total_token_count(self) -> Optional[int]:
|
| 112 |
+
return self._total_token_count
|
| 113 |
+
|
| 114 |
+
@property
|
| 115 |
+
def thoughts(self) -> Optional[str]:
|
| 116 |
+
return self._thoughts
|
| 117 |
+
|
| 118 |
+
@property
|
| 119 |
+
def json_dumps(self) -> str:
|
| 120 |
+
return self._json_dumps
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class GeminiClient:
|
| 124 |
+
|
| 125 |
+
AVAILABLE_MODELS = []
|
| 126 |
+
EXTRA_MODELS = os.environ.get("EXTRA_MODELS", "").split(",")
|
| 127 |
+
|
| 128 |
+
def __init__(self, api_key: str):
|
| 129 |
+
self.api_key = api_key
|
| 130 |
+
|
| 131 |
+
# 将流式和非流式请求的通用部分提取为共享方法
|
| 132 |
+
def _prepare_request_data(self, request, contents, safety_settings, system_instruction):
|
| 133 |
+
api_version = "v1alpha" if "think" in request.model else "v1beta"
|
| 134 |
+
if serach["search_mode"]:
|
| 135 |
+
data = {
|
| 136 |
+
"contents": contents,
|
| 137 |
+
"tools": [{"google_search": {}}],
|
| 138 |
+
"generationConfig": self._get_generation_config(request),
|
| 139 |
+
"safetySettings": safety_settings,
|
| 140 |
+
}
|
| 141 |
+
else:
|
| 142 |
+
data = {
|
| 143 |
+
"contents": contents,
|
| 144 |
+
"generationConfig": self._get_generation_config(request),
|
| 145 |
+
"safetySettings": safety_settings,
|
| 146 |
+
}
|
| 147 |
+
if system_instruction:
|
| 148 |
+
data["system_instruction"] = system_instruction
|
| 149 |
+
return api_version, data
|
| 150 |
+
|
| 151 |
+
def _get_generation_config(self, request):
|
| 152 |
+
config_params = {
|
| 153 |
+
"temperature": request.temperature,
|
| 154 |
+
"maxOutputTokens": request.max_tokens,
|
| 155 |
+
"topP": request.top_p,
|
| 156 |
+
"stopSequences": request.stop if isinstance(request.stop, list) else [request.stop] if request.stop is not None else None,
|
| 157 |
+
"candidateCount": request.n
|
| 158 |
+
}
|
| 159 |
+
return {k: v for k, v in config_params.items() if v is not None}
|
| 160 |
+
|
| 161 |
+
async def stream_chat(self, request: ChatCompletionRequest, contents, safety_settings, system_instruction):
|
| 162 |
+
|
| 163 |
+
# 检查是否启用假流式请求
|
| 164 |
+
if FAKE_STREAMING:
|
| 165 |
+
extra_log={'key': self.api_key[:8], 'request_type': 'fake_stream', 'model': request.model}
|
| 166 |
+
log('INFO', "使用假流式请求模式(发送换行符保持连接)", extra=extra_log)
|
| 167 |
+
try:
|
| 168 |
+
|
| 169 |
+
# 每隔一段时间发送换行符作为保活消息,直到外部取消此生成器
|
| 170 |
+
start_time = time.time()
|
| 171 |
+
while True:
|
| 172 |
+
yield "\n"
|
| 173 |
+
await asyncio.sleep(FAKE_STREAMING_INTERVAL)
|
| 174 |
+
|
| 175 |
+
# 如果等待时间过长(超过300秒),抛出超时异常,让外部处理
|
| 176 |
+
if time.time() - start_time > 300:
|
| 177 |
+
log('ERROR', f"假流式请求等待时间过长",extra=extra_log)
|
| 178 |
+
|
| 179 |
+
raise TimeoutError("假流式请求等待时间过长")
|
| 180 |
+
|
| 181 |
+
except Exception as e:
|
| 182 |
+
if not isinstance(e, asyncio.CancelledError):
|
| 183 |
+
log('ERROR', f"假流式处理期间发生错误: {str(e)}", extra=extra_log)
|
| 184 |
+
raise e
|
| 185 |
+
finally:
|
| 186 |
+
log('INFO', "假流式请求结束", extra=extra_log)
|
| 187 |
+
else:
|
| 188 |
+
# 真流式请求处理逻辑
|
| 189 |
+
extra_log = {'key': self.api_key[:8], 'request_type': 'stream', 'model': request.model}
|
| 190 |
+
log('INFO', "真流式请求开始", extra=extra_log)
|
| 191 |
+
|
| 192 |
+
api_version, data = self._prepare_request_data(request, contents, safety_settings, system_instruction)
|
| 193 |
+
model= request.model.removesuffix("-search")
|
| 194 |
+
url = f"https://generativelanguage.googleapis.com/{api_version}/models/{model}:streamGenerateContent?key={self.api_key}&alt=sse"
|
| 195 |
+
headers = {
|
| 196 |
+
"Content-Type": "application/json",
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
async with httpx.AsyncClient() as client:
|
| 200 |
+
async with client.stream("POST", url, headers=headers, json=data, timeout=600) as response:
|
| 201 |
+
buffer = b""
|
| 202 |
+
try:
|
| 203 |
+
async for line in response.aiter_lines():
|
| 204 |
+
if not line.strip():
|
| 205 |
+
continue
|
| 206 |
+
if line.startswith("data: "):
|
| 207 |
+
line = line[len("data: "):]
|
| 208 |
+
buffer += line.encode('utf-8')
|
| 209 |
+
try:
|
| 210 |
+
data = json.loads(buffer.decode('utf-8'))
|
| 211 |
+
buffer = b""
|
| 212 |
+
if 'candidates' in data and data['candidates']:
|
| 213 |
+
candidate = data['candidates'][0]
|
| 214 |
+
if 'content' in candidate:
|
| 215 |
+
content = candidate['content']
|
| 216 |
+
if 'parts' in content and content['parts']:
|
| 217 |
+
parts = content['parts']
|
| 218 |
+
text = ""
|
| 219 |
+
for part in parts:
|
| 220 |
+
if 'text' in part:
|
| 221 |
+
text += part['text']
|
| 222 |
+
if text:
|
| 223 |
+
yield text
|
| 224 |
+
|
| 225 |
+
if candidate.get("finishReason") and candidate.get("finishReason") != "STOP":
|
| 226 |
+
error_msg = f"模型的响应被截断: {candidate.get('finishReason')}"
|
| 227 |
+
extra_log_error = {'key': self.api_key[:8], 'request_type': 'stream', 'model': request.model, 'status_code': 'ERROR', 'error_message': error_msg}
|
| 228 |
+
log_msg = format_log_message('WARNING', error_msg, extra=extra_log_error)
|
| 229 |
+
logger.warning(log_msg)
|
| 230 |
+
raise ValueError(error_msg)
|
| 231 |
+
|
| 232 |
+
if 'safetyRatings' in candidate:
|
| 233 |
+
for rating in candidate['safetyRatings']:
|
| 234 |
+
if rating['probability'] == 'HIGH':
|
| 235 |
+
error_msg = f"模型的响应被截断: {rating['category']}"
|
| 236 |
+
extra_log_safety = {'key': self.api_key[:8], 'request_type': 'stream', 'model': request.model, 'status_code': 'ERROR', 'error_message': error_msg}
|
| 237 |
+
log_msg = format_log_message('WARNING', error_msg, extra=extra_log_safety)
|
| 238 |
+
logger.warning(log_msg)
|
| 239 |
+
raise ValueError(error_msg)
|
| 240 |
+
except json.JSONDecodeError:
|
| 241 |
+
continue
|
| 242 |
+
except Exception as e:
|
| 243 |
+
error_msg = f"流式处理期间发生错误: {str(e)}"
|
| 244 |
+
extra_log_stream_error = {'key': self.api_key[:8], 'request_type': 'stream', 'model': request.model, 'status_code': 'ERROR', 'error_message': error_msg}
|
| 245 |
+
log_msg = format_log_message('ERROR', error_msg, extra=extra_log_stream_error)
|
| 246 |
+
logger.error(log_msg)
|
| 247 |
+
raise e
|
| 248 |
+
except Exception as e:
|
| 249 |
+
raise e
|
| 250 |
+
finally:
|
| 251 |
+
log_msg = format_log_message('INFO', "流式请求结束", extra=extra_log)
|
| 252 |
+
logger.info(log_msg)
|
| 253 |
+
|
| 254 |
+
def complete_chat(self, request: ChatCompletionRequest, contents, safety_settings, system_instruction):
|
| 255 |
+
extra_log = {'key': self.api_key[:8], 'request_type': 'non-stream', 'model': request.model}
|
| 256 |
+
log('info', "非流式请求开始", extra=extra_log)
|
| 257 |
+
|
| 258 |
+
api_version, data = self._prepare_request_data(request, contents, safety_settings, system_instruction)
|
| 259 |
+
model= request.model.removesuffix("-search")
|
| 260 |
+
url = f"https://generativelanguage.googleapis.com/{api_version}/models/{model}:generateContent?key={self.api_key}"
|
| 261 |
+
headers = {
|
| 262 |
+
"Content-Type": "application/json",
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
try:
|
| 266 |
+
response = requests.post(url, headers=headers, json=data)
|
| 267 |
+
response.raise_for_status()
|
| 268 |
+
log('info', "非流式请求成功完成", extra=extra_log)
|
| 269 |
+
|
| 270 |
+
return ResponseWrapper(response.json())
|
| 271 |
+
except Exception as e:
|
| 272 |
+
raise
|
| 273 |
+
|
| 274 |
+
def convert_messages(self, messages, use_system_prompt=False,model=None):
|
| 275 |
+
gemini_history = []
|
| 276 |
+
errors = []
|
| 277 |
+
system_instruction_text = ""
|
| 278 |
+
is_system_phase = use_system_prompt
|
| 279 |
+
for i, message in enumerate(messages):
|
| 280 |
+
role = message.role
|
| 281 |
+
content = message.content
|
| 282 |
+
if isinstance(content, str):
|
| 283 |
+
if is_system_phase and role == 'system':
|
| 284 |
+
if system_instruction_text:
|
| 285 |
+
system_instruction_text += "\n" + content
|
| 286 |
+
else:
|
| 287 |
+
system_instruction_text = content
|
| 288 |
+
else:
|
| 289 |
+
is_system_phase = False
|
| 290 |
+
|
| 291 |
+
if role in ['user', 'system']:
|
| 292 |
+
role_to_use = 'user'
|
| 293 |
+
elif role == 'assistant':
|
| 294 |
+
role_to_use = 'model'
|
| 295 |
+
else:
|
| 296 |
+
errors.append(f"Invalid role: {role}")
|
| 297 |
+
continue
|
| 298 |
+
|
| 299 |
+
if gemini_history and gemini_history[-1]['role'] == role_to_use:
|
| 300 |
+
gemini_history[-1]['parts'].append({"text": content})
|
| 301 |
+
else:
|
| 302 |
+
gemini_history.append(
|
| 303 |
+
{"role": role_to_use, "parts": [{"text": content}]})
|
| 304 |
+
elif isinstance(content, list):
|
| 305 |
+
parts = []
|
| 306 |
+
for item in content:
|
| 307 |
+
if item.get('type') == 'text':
|
| 308 |
+
parts.append({"text": item.get('text')})
|
| 309 |
+
elif item.get('type') == 'image_url':
|
| 310 |
+
image_data = item.get('image_url', {}).get('url', '')
|
| 311 |
+
if image_data.startswith('data:image/'):
|
| 312 |
+
try:
|
| 313 |
+
mime_type, base64_data = image_data.split(';')[0].split(':')[1], image_data.split(',')[1]
|
| 314 |
+
parts.append({
|
| 315 |
+
"inline_data": {
|
| 316 |
+
"mime_type": mime_type,
|
| 317 |
+
"data": base64_data
|
| 318 |
+
}
|
| 319 |
+
})
|
| 320 |
+
except (IndexError, ValueError):
|
| 321 |
+
errors.append(
|
| 322 |
+
f"Invalid data URI for image: {image_data}")
|
| 323 |
+
else:
|
| 324 |
+
errors.append(
|
| 325 |
+
f"Invalid image URL format for item: {item}")
|
| 326 |
+
|
| 327 |
+
if parts:
|
| 328 |
+
if role in ['user', 'system']:
|
| 329 |
+
role_to_use = 'user'
|
| 330 |
+
elif role == 'assistant':
|
| 331 |
+
role_to_use = 'model'
|
| 332 |
+
else:
|
| 333 |
+
errors.append(f"Invalid role: {role}")
|
| 334 |
+
continue
|
| 335 |
+
if gemini_history and gemini_history[-1]['role'] == role_to_use:
|
| 336 |
+
gemini_history[-1]['parts'].extend(parts)
|
| 337 |
+
else:
|
| 338 |
+
gemini_history.append(
|
| 339 |
+
{"role": role_to_use, "parts": parts})
|
| 340 |
+
if errors:
|
| 341 |
+
return errors
|
| 342 |
+
else:
|
| 343 |
+
# 只有当search_mode为真且模型名称以-search结尾时,才添加搜索提示
|
| 344 |
+
if settings.serach["search_mode"] and model and model.endswith("-search"):
|
| 345 |
+
gemini_history.insert(len(gemini_history)-2,{'role': 'user', 'parts': [{'text':settings.serach["search_prompt"]}]})
|
| 346 |
+
if RANDOM_STRING:
|
| 347 |
+
gemini_history.insert(1,{'role': 'user', 'parts': [{'text': generate_secure_random_string(RANDOM_STRING_LENGTH)}]})
|
| 348 |
+
gemini_history.insert(len(gemini_history)-1,{'role': 'user', 'parts': [{'text': generate_secure_random_string(RANDOM_STRING_LENGTH)}]})
|
| 349 |
+
log_msg = format_log_message('INFO', "伪装消息成功")
|
| 350 |
+
return gemini_history, {"parts": [{"text": system_instruction_text}]}
|
| 351 |
+
|
| 352 |
+
@staticmethod
|
| 353 |
+
async def list_available_models(api_key) -> list:
|
| 354 |
+
url = "https://generativelanguage.googleapis.com/v1beta/models?key={}".format(
|
| 355 |
+
api_key)
|
| 356 |
+
async with httpx.AsyncClient() as client:
|
| 357 |
+
response = await client.get(url)
|
| 358 |
+
response.raise_for_status()
|
| 359 |
+
data = response.json()
|
| 360 |
+
models = []
|
| 361 |
+
for model in data.get("models", []):
|
| 362 |
+
models.append(model["name"])
|
| 363 |
+
if model["name"].startswith("models/gemini-2"):
|
| 364 |
+
models.append(model["name"] + "-search")
|
| 365 |
+
models.extend(GeminiClient.EXTRA_MODELS)
|
| 366 |
+
|
| 367 |
+
return models
|
app/templates/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Templates package initialization
|
app/templates/assets/favicon.ico
ADDED
|
|
app/templates/assets/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
:root{--vt-c-white: #ffffff;--vt-c-white-soft: #f8f8f8;--vt-c-white-mute: #f2f2f2;--vt-c-black: #181818;--vt-c-black-soft: #222222;--vt-c-black-mute: #282828;--vt-c-indigo: #2c3e50;--vt-c-divider-light-1: rgba(60, 60, 60, .29);--vt-c-divider-light-2: rgba(60, 60, 60, .12);--vt-c-divider-dark-1: rgba(84, 84, 84, .65);--vt-c-divider-dark-2: rgba(84, 84, 84, .48);--vt-c-text-light-1: var(--vt-c-indigo);--vt-c-text-light-2: rgba(60, 60, 60, .66);--vt-c-text-dark-1: var(--vt-c-white);--vt-c-text-dark-2: rgba(235, 235, 235, .64)}:root{--color-background: var(--vt-c-white);--color-background-soft: var(--vt-c-white-soft);--color-background-mute: var(--vt-c-white-mute);--color-border: var(--vt-c-divider-light-2);--color-border-hover: var(--vt-c-divider-light-1);--color-heading: var(--vt-c-text-light-1);--color-text: var(--vt-c-text-light-1);--section-gap: 160px;--card-background: #ffffff;--card-border: #e0e0e0;--button-primary: #007bff;--button-primary-hover: #0069d9;--button-text: #ffffff;--stats-item-bg: #f8f9fa;--log-entry-bg: #f8f9fa;--log-entry-border: #e9ecef;--toggle-bg: #ccc;--toggle-active: #007bff}.dark-mode{--color-background: var(--vt-c-black);--color-background-soft: var(--vt-c-black-soft);--color-background-mute: var(--vt-c-black-mute);--color-border: var(--vt-c-divider-dark-2);--color-border-hover: var(--vt-c-divider-dark-1);--color-heading: var(--vt-c-text-dark-1);--color-text: var(--vt-c-text-dark-2);--card-background: #2d2d2d;--card-border: #444444;--button-primary: #0056b3;--button-primary-hover: #004494;--button-text: #ffffff;--stats-item-bg: #333333;--log-entry-bg: #333333;--log-entry-border: #444444;--toggle-bg: #555555;--toggle-active: #0056b3}@media (prefers-color-scheme: dark){:root:not(.dark-mode):not(.light-mode){--color-background: var(--vt-c-black);--color-background-soft: var(--vt-c-black-soft);--color-background-mute: var(--vt-c-black-mute);--color-border: var(--vt-c-divider-dark-2);--color-border-hover: var(--vt-c-divider-dark-1);--color-heading: var(--vt-c-text-dark-1);--color-text: var(--vt-c-text-dark-2);--card-background: #2d2d2d;--card-border: #444444;--button-primary: #0056b3;--button-primary-hover: #004494;--button-text: #ffffff;--stats-item-bg: #333333;--log-entry-bg: #333333;--log-entry-border: #444444;--toggle-bg: #555555;--toggle-active: #0056b3}}*,*:before,*:after{box-sizing:border-box;margin:0;font-weight:400}body{min-height:100vh;color:var(--color-text);background:var(--color-background);transition:color .5s,background-color .5s;line-height:1.6;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:15px;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{max-width:1280px;margin:0 auto;padding:1rem;font-weight:400}a,.green{text-decoration:none;color:#00bd7e;transition:.4s;padding:3px}@media (hover: hover){a:hover{background-color:#00bd7e33}}body{margin:0;padding:0}.info-box[data-v-ec28689b]{background-color:var(--card-background);border:1px solid var(--card-border);border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 2px 4px #0000000d;transition:background-color .3s,border-color .3s,box-shadow .3s}@media (max-width: 768px){.info-box[data-v-ec28689b]{margin-bottom:12px}}@media (max-width: 480px){.info-box[data-v-ec28689b]{margin-bottom:8px}}.status[data-v-ec28689b]{color:#28a745;font-weight:700;font-size:18px;margin-bottom:20px;text-align:center}.section-title[data-v-ec28689b]{color:var(--color-heading);border-bottom:1px solid var(--color-border);padding-bottom:10px;margin-bottom:20px;transition:color .3s,border-color .3s}.stats-grid[data-v-ec28689b]{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-top:15px;margin-bottom:20px}@media (max-width: 768px){.stats-grid[data-v-ec28689b]{gap:6px}}.stat-card[data-v-ec28689b]{background-color:var(--stats-item-bg);padding:15px;border-radius:8px;text-align:center;box-shadow:0 2px 4px #0000000d;transition:transform .2s,background-color .3s,box-shadow .3s}.stat-card[data-v-ec28689b]:hover{transform:translateY(-2px);box-shadow:0 4px 8px #0000001a}.stat-value[data-v-ec28689b]{font-size:24px;font-weight:700;color:var(--button-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .3s}.stat-label[data-v-ec28689b]{font-size:14px;color:var(--color-text);margin-top:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .3s}@media (max-width: 768px){.stat-card[data-v-ec28689b]{padding:8px 5px}.stat-value[data-v-ec28689b]{font-size:16px}.stat-label[data-v-ec28689b]{font-size:11px;margin-top:3px}}@media (max-width: 480px){.stat-card[data-v-ec28689b]{padding:6px 3px}.stat-value[data-v-ec28689b]{font-size:14px}.stat-label[data-v-ec28689b]{font-size:10px;margin-top:2px}}.api-key-stats-container[data-v-ec28689b]{margin-top:20px}.api-key-stats-list[data-v-ec28689b]{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-top:15px}@media (max-width: 992px){.api-key-stats-list[data-v-ec28689b]{grid-template-columns:repeat(2,1fr)}}@media (max-width: 576px){.api-key-stats-list[data-v-ec28689b]{grid-template-columns:1fr}}.api-key-item[data-v-ec28689b]{background-color:var(--stats-item-bg);border-radius:8px;padding:15px;box-shadow:0 2px 4px #0000000d;transition:background-color .3s,box-shadow .3s}.api-key-header[data-v-ec28689b]{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.api-key-name[data-v-ec28689b]{font-weight:700;color:var(--color-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:50%;transition:color .3s}.api-key-usage[data-v-ec28689b]{display:flex;align-items:center;gap:10px;white-space:nowrap}.api-key-count[data-v-ec28689b]{font-weight:700;color:var(--button-primary);transition:color .3s}@media (max-width: 768px){.api-key-item[data-v-ec28689b]{padding:8px}.api-key-header[data-v-ec28689b]{margin-bottom:6px}.api-key-name[data-v-ec28689b]{font-size:12px}.api-key-usage[data-v-ec28689b]{font-size:12px;gap:5px}}@media (max-width: 480px){.api-key-item[data-v-ec28689b]{padding:6px}.api-key-name[data-v-ec28689b]{font-size:11px;max-width:45%}.api-key-usage[data-v-ec28689b]{font-size:11px;gap:3px}}.progress-container[data-v-ec28689b]{width:100%;height:10px;background-color:var(--color-background-soft);border-radius:5px;overflow:hidden;transition:background-color .3s}.progress-bar[data-v-ec28689b]{height:100%;border-radius:5px;transition:width .3s ease,background-color .3s}.progress-bar.low[data-v-ec28689b]{background-color:#28a745}.progress-bar.medium[data-v-ec28689b]{background-color:#ffc107}.progress-bar.high[data-v-ec28689b]{background-color:#dc3545}.model-stats-container[data-v-ec28689b]{margin-top:10px;border-top:1px dashed var(--color-border);padding-top:10px;transition:border-color .3s}.model-stats-header[data-v-ec28689b]{display:flex;justify-content:space-between;align-items:center;cursor:pointer;-webkit-user-select:none;user-select:none;margin-bottom:8px;color:var(--color-heading);font-size:14px;transition:color .3s}.model-stats-title[data-v-ec28689b]{font-weight:600}.model-stats-toggle[data-v-ec28689b]{font-size:12px}.model-stats-list[data-v-ec28689b]{display:flex;flex-direction:column;gap:8px}.model-stat-item[data-v-ec28689b]{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background-color:var(--color-background-mute);border-radius:4px;font-size:13px;transition:transform .2s,box-shadow .2s,background-color .3s}.model-name[data-v-ec28689b]{font-weight:500;color:var(--color-heading);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60%;transition:color .3s}.model-count[data-v-ec28689b]{display:flex;align-items:center;gap:8px;color:var(--button-primary);font-weight:600;transition:color .3s}.model-usage-text[data-v-ec28689b]{color:var(--color-text);font-weight:400;font-size:12px;transition:color .3s}.model-progress-container[data-v-ec28689b]{width:60px;height:6px;background-color:var(--color-background-soft);border-radius:3px;overflow:hidden;margin-left:5px;transition:background-color .3s}.model-progress-bar[data-v-ec28689b]{height:100%;border-radius:3px;transition:width .3s ease,background-color .3s}.view-more-models[data-v-ec28689b]{text-align:center;color:var(--button-primary);font-size:12px;cursor:pointer;padding:8px;margin-top:5px;border-radius:4px;background-color:#007bff0d;transition:all .2s ease,color .3s,background-color .3s}.view-more-models[data-v-ec28689b]:hover{background-color:#007bff1a;transform:translateY(-1px);box-shadow:0 2px 5px #0000000d}.fold-header[data-v-ec28689b]{cursor:pointer;-webkit-user-select:none;user-select:none;display:flex;justify-content:space-between;align-items:center;transition:background-color .2s;border-radius:6px;padding:5px 8px}.fold-header[data-v-ec28689b]:hover{background-color:var(--color-background-mute)}.fold-icon[data-v-ec28689b]{display:inline-flex;align-items:center;justify-content:center;transition:transform .3s ease}.fold-icon.rotated[data-v-ec28689b]{transform:rotate(180deg)}.fold-content[data-v-ec28689b]{overflow:hidden}.fold-enter-active[data-v-ec28689b],.fold-leave-active[data-v-ec28689b]{transition:all .3s ease;max-height:1000px;opacity:1;overflow:hidden}.fold-enter-from[data-v-ec28689b],.fold-leave-to[data-v-ec28689b]{max-height:0;opacity:0;overflow:hidden}.model-stat-item[data-v-ec28689b]:hover{transform:translateY(-2px);box-shadow:0 2px 8px #0000000d}@media (max-width: 768px){.model-stats-container[data-v-ec28689b]{margin-top:8px;padding-top:8px}.model-stats-header[data-v-ec28689b]{font-size:12px;margin-bottom:6px}.model-stat-item[data-v-ec28689b]{padding:4px 8px;font-size:11px}.model-progress-container[data-v-ec28689b]{width:40px;height:4px}}.info-box[data-v-fda98e8f]{background-color:var(--card-background);border:1px solid var(--card-border);border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 2px 4px #0000000d;transition:background-color .3s,border-color .3s,box-shadow .3s}@media (max-width: 768px){.info-box[data-v-fda98e8f]{margin-bottom:12px}}@media (max-width: 480px){.info-box[data-v-fda98e8f]{margin-bottom:8px}}.section-title[data-v-fda98e8f]{color:var(--color-heading);border-bottom:1px solid var(--color-border);padding-bottom:10px;margin-bottom:20px;transition:color .3s,border-color .3s}.stats-grid[data-v-fda98e8f]{display:grid;grid-template-columns:repeat(3,1fr);gap:15px;margin-top:15px;margin-bottom:20px}@media (max-width: 768px){.stats-grid[data-v-fda98e8f]{gap:6px}}.stat-card[data-v-fda98e8f]{background-color:var(--stats-item-bg);padding:15px;border-radius:8px;text-align:center;box-shadow:0 2px 4px #0000000d;transition:transform .2s,background-color .3s,box-shadow .3s}.stat-card[data-v-fda98e8f]:hover{transform:translateY(-2px);box-shadow:0 4px 8px #0000001a}.stat-value[data-v-fda98e8f]{font-size:24px;font-weight:700;color:var(--button-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .3s}.stat-label[data-v-fda98e8f]{font-size:14px;color:var(--color-text);margin-top:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .3s}@media (max-width: 768px){.stat-card[data-v-fda98e8f]{padding:8px 5px}.stat-value[data-v-fda98e8f]{font-size:16px}.stat-label[data-v-fda98e8f]{font-size:11px;margin-top:3px}}@media (max-width: 480px){.stat-card[data-v-fda98e8f]{padding:6px 3px}.stat-value[data-v-fda98e8f]{font-size:14px}.stat-label[data-v-fda98e8f]{font-size:10px;margin-top:2px}}.update-needed[data-v-fda98e8f]{color:#dc3545!important}.up-to-date[data-v-fda98e8f]{color:#28a745!important}.info-box[data-v-c3726400]{background-color:var(--card-background);border:1px solid var(--card-border);border-radius:8px;padding:20px;margin-bottom:20px;box-shadow:0 2px 4px #0000000d;transition:background-color .3s,border-color .3s,box-shadow .3s}@media (max-width: 768px){.info-box[data-v-c3726400]{margin-bottom:12px}}@media (max-width: 480px){.info-box[data-v-c3726400]{margin-bottom:8px}}.section-title[data-v-c3726400]{color:var(--color-heading);border-bottom:1px solid var(--color-border);padding-bottom:10px;margin-bottom:20px;transition:color .3s,border-color .3s}.log-filter[data-v-c3726400]{display:flex;justify-content:center;margin-bottom:15px;gap:10px;flex-wrap:wrap}.log-filter button[data-v-c3726400]{padding:5px 10px;border:1px solid var(--card-border);border-radius:4px;background-color:var(--stats-item-bg);color:var(--color-text);cursor:pointer;min-width:60px;transition:background-color .3s,color .3s,border-color .3s}.log-filter button.active[data-v-c3726400]{background-color:var(--button-primary);color:var(--button-text);border-color:var(--button-primary)}@media (max-width: 768px){.log-filter[data-v-c3726400]{gap:6px;margin-bottom:12px}.log-filter button[data-v-c3726400]{padding:4px 8px;font-size:12px;min-width:50px}}@media (max-width: 480px){.log-filter[data-v-c3726400]{gap:4px;margin-bottom:10px}.log-filter button[data-v-c3726400]{padding:3px 6px;font-size:11px;min-width:40px}}.log-container[data-v-c3726400]{background-color:var(--log-entry-bg);border:1px solid var(--log-entry-border);border-radius:8px;padding:15px;margin-top:20px;max-height:500px;overflow-y:auto;font-family:monospace;font-size:14px;line-height:1.5;transition:background-color .3s,border-color .3s}.log-entry[data-v-c3726400]{margin-bottom:8px;padding:8px;border-radius:4px;word-break:break-word;transition:background-color .3s,border-color .3s}.log-entry.INFO[data-v-c3726400]{background-color:#17a2b81a;border-left:4px solid #17a2b8}.log-entry.WARNING[data-v-c3726400]{background-color:#ffc1071a;border-left:4px solid #ffc107}.log-entry.ERROR[data-v-c3726400]{background-color:#dc35451a;border-left:4px solid #dc3545}.log-entry.DEBUG[data-v-c3726400]{background-color:#17a2b81a;border-left:4px solid #17a2b8}.log-timestamp[data-v-c3726400]{color:var(--color-text);font-size:12px;margin-right:10px;opacity:.8;transition:color .3s}.log-level[data-v-c3726400]{font-weight:700;margin-right:10px}.log-level.INFO[data-v-c3726400]{color:#17a2b8}.log-level.WARNING[data-v-c3726400]{color:#ffc107}.log-level.ERROR[data-v-c3726400]{color:#dc3545}.log-level.DEBUG[data-v-c3726400]{color:#17a2b8}.log-message[data-v-c3726400]{color:var(--color-text);transition:color .3s}@media (max-width: 768px){.log-container[data-v-c3726400]{padding:10px;font-size:13px}.log-entry[data-v-c3726400]{padding:6px;margin-bottom:6px}.log-timestamp[data-v-c3726400]{font-size:11px;display:block;margin-bottom:3px}}@media (max-width: 480px){.log-container[data-v-c3726400]{padding:8px;font-size:12px}.log-entry[data-v-c3726400]{padding:5px;margin-bottom:5px}.log-timestamp[data-v-c3726400]{font-size:10px}.log-level[data-v-c3726400]{margin-right:5px}}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;line-height:1.6;background-color:var(--color-background);color:var(--color-text);margin:0;padding:0;transition:background-color .3s,color .3s}.dashboard{max-width:1200px;margin:0 auto;padding:20px}.header-container{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}h1{color:var(--color-heading);margin:0;font-size:1.8rem}.theme-toggle{display:flex;align-items:center}.toggle-label{margin-left:8px;font-size:1.2rem}.switch{position:relative;display:inline-block;width:60px;height:30px}.switch input{opacity:0;width:0;height:0}.slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:var(--toggle-bg);transition:.4s}.slider:before{position:absolute;content:"";height:22px;width:22px;left:4px;bottom:4px;background-color:#fff;transition:.4s}input:checked+.slider{background-color:var(--toggle-active)}input:focus+.slider{box-shadow:0 0 1px var(--toggle-active)}input:checked+.slider:before{transform:translate(30px)}.slider.round{border-radius:34px}.slider.round:before{border-radius:50%}@media (max-width: 768px){.dashboard{padding:10px 8px}.header-container{flex-direction:row;justify-content:space-between;align-items:center;margin-bottom:15px}h1{font-size:1.4rem;text-align:left;margin-right:10px}}@media (max-width: 480px){.dashboard{padding:6px 4px}h1{font-size:1.2rem}.switch{width:50px;height:26px}.slider:before{height:18px;width:18px}input:checked+.slider:before{transform:translate(24px)}.toggle-label{margin-left:5px;font-size:1rem}}.refresh-button{display:block;margin:20px auto;padding:10px 20px;background-color:var(--button-primary);color:var(--button-text);border:none;border-radius:4px;font-size:16px;cursor:pointer;transition:background-color .2s}.refresh-button:hover{background-color:var(--button-primary-hover)}@media (max-width: 768px){:deep(.info-box){padding:10px 6px;margin-bottom:10px;border-radius:6px;background-color:var(--card-background);border:1px solid var(--card-border)}:deep(.section-title){font-size:1.1rem;margin-bottom:10px;padding-bottom:6px;color:var(--color-heading);border-bottom:1px solid var(--color-border)}:deep(.stats-grid){gap:5px;margin-top:10px;margin-bottom:15px}.refresh-button{margin:15px auto;padding:8px 16px;font-size:14px}}@media (max-width: 480px){:deep(.info-box){padding:8px 4px;margin-bottom:6px;border-radius:5px}:deep(.section-title){font-size:1rem;margin-bottom:8px;padding-bottom:4px}:deep(.stats-grid){gap:4px;margin-top:8px;margin-bottom:10px}.refresh-button{margin:10px auto;padding:6px 12px;font-size:13px}}
|
app/templates/assets/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<link rel="icon" href="/favicon.ico">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>HAJIMI</title>
|
| 8 |
+
<script type="module" crossorigin src="/main.js"></script>
|
| 9 |
+
<link rel="stylesheet" crossorigin href="/index.css">
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<div id="app"></div>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|
app/templates/assets/main.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
app/templates/index.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="zh-CN">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<link rel="icon" href="/assets/favicon.ico">
|
| 7 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 8 |
+
<title>Gemini API 代理服务</title>
|
| 9 |
+
<script type="module" crossorigin src="/assets/main.js"></script>
|
| 10 |
+
<link rel="stylesheet" href="/assets/index.css">
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
<div id="app"></div>
|
| 14 |
+
</body>
|
| 15 |
+
</html>
|
app/utils/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Utils package initialization
|
| 2 |
+
|
| 3 |
+
from app.utils.logging import logger, log_manager, format_log_message,log
|
| 4 |
+
from app.utils.api_key import APIKeyManager, test_api_key
|
| 5 |
+
from app.utils.error_handling import handle_gemini_error, translate_error, handle_api_error
|
| 6 |
+
from app.utils.rate_limiting import protect_from_abuse
|
| 7 |
+
from app.utils.cache import ResponseCacheManager, generate_cache_key, cache_response
|
| 8 |
+
from app.utils.request import ActiveRequestsManager, check_client_disconnect
|
| 9 |
+
from app.utils.stats import clean_expired_stats, update_api_call_stats
|
| 10 |
+
from app.utils.response import create_chat_response, create_error_response, create_response, handle_exception
|
| 11 |
+
from app.utils.version import check_version
|
| 12 |
+
from app.utils.maintenance import handle_exception, schedule_cache_cleanup
|
app/utils/api_key.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
import re
|
| 3 |
+
import os
|
| 4 |
+
import logging
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
from apscheduler.schedulers.background import BackgroundScheduler
|
| 7 |
+
from app.utils.logging import format_log_message
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger("my_logger")
|
| 10 |
+
|
| 11 |
+
class APIKeyManager:
|
| 12 |
+
def __init__(self):
|
| 13 |
+
self.api_keys = re.findall(
|
| 14 |
+
r"AIzaSy[a-zA-Z0-9_-]{33}", os.environ.get('GEMINI_API_KEYS', ""))
|
| 15 |
+
self.key_stack = [] # 初始化密钥栈
|
| 16 |
+
self._reset_key_stack() # 初始化时创建随机密钥栈
|
| 17 |
+
# self.api_key_blacklist = set()
|
| 18 |
+
# self.api_key_blacklist_duration = 60
|
| 19 |
+
self.scheduler = BackgroundScheduler()
|
| 20 |
+
self.scheduler.start()
|
| 21 |
+
self.tried_keys_for_request = set() # 用于跟踪当前请求尝试中已试过的 key
|
| 22 |
+
|
| 23 |
+
def _reset_key_stack(self):
|
| 24 |
+
"""创建并随机化密钥栈"""
|
| 25 |
+
shuffled_keys = self.api_keys[:] # 创建 api_keys 的副本以避免直接修改原列表
|
| 26 |
+
random.shuffle(shuffled_keys)
|
| 27 |
+
self.key_stack = shuffled_keys
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def get_available_key(self):
|
| 31 |
+
"""从栈顶获取密钥,若栈空则重新生成"""
|
| 32 |
+
while self.key_stack:
|
| 33 |
+
key = self.key_stack.pop()
|
| 34 |
+
# if key not in self.api_key_blacklist and key not in self.tried_keys_for_request:
|
| 35 |
+
if key not in self.tried_keys_for_request:
|
| 36 |
+
self.tried_keys_for_request.add(key)
|
| 37 |
+
return key
|
| 38 |
+
|
| 39 |
+
# 栈空,重新生成密钥栈
|
| 40 |
+
self._reset_key_stack()
|
| 41 |
+
|
| 42 |
+
# 再次尝试从新栈中获取密钥 (迭代一次)
|
| 43 |
+
while self.key_stack:
|
| 44 |
+
key = self.key_stack.pop()
|
| 45 |
+
# if key not in self.api_key_blacklist and key not in self.tried_keys_for_request:
|
| 46 |
+
if key not in self.tried_keys_for_request:
|
| 47 |
+
self.tried_keys_for_request.add(key)
|
| 48 |
+
return key
|
| 49 |
+
|
| 50 |
+
if not self.api_keys:
|
| 51 |
+
log_msg = format_log_message('ERROR', "没有配置任何 API 密钥!")
|
| 52 |
+
logger.error(log_msg)
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
return None
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def show_all_keys(self):
|
| 59 |
+
log_msg = format_log_message('INFO', f"当前可用API key个数: {len(self.api_keys)} ")
|
| 60 |
+
logger.info(log_msg)
|
| 61 |
+
for i, api_key in enumerate(self.api_keys):
|
| 62 |
+
log_msg = format_log_message('INFO', f"API Key{i}: {api_key[:8]}...{api_key[-3:]}")
|
| 63 |
+
logger.info(log_msg)
|
| 64 |
+
|
| 65 |
+
# def blacklist_key(self, key):
|
| 66 |
+
# log_msg = format_log_message('WARNING', f"{key[:8]} → 暂时禁用 {self.api_key_blacklist_duration} 秒")
|
| 67 |
+
# logger.warning(log_msg)
|
| 68 |
+
# self.api_key_blacklist.add(key)
|
| 69 |
+
# self.scheduler.add_job(lambda: self.api_key_blacklist.discard(key), 'date',
|
| 70 |
+
# run_date=datetime.now() + timedelta(seconds=self.api_key_blacklist_duration))
|
| 71 |
+
|
| 72 |
+
def reset_tried_keys_for_request(self):
|
| 73 |
+
"""在新的请求尝试时重置已尝试的 key 集合"""
|
| 74 |
+
self.tried_keys_for_request = set()
|
| 75 |
+
|
| 76 |
+
async def test_api_key(api_key: str) -> bool:
|
| 77 |
+
"""
|
| 78 |
+
测试 API 密钥是否有效。
|
| 79 |
+
"""
|
| 80 |
+
try:
|
| 81 |
+
import httpx
|
| 82 |
+
url = "https://generativelanguage.googleapis.com/v1beta/models?key={}".format(api_key)
|
| 83 |
+
async with httpx.AsyncClient() as client:
|
| 84 |
+
response = await client.get(url)
|
| 85 |
+
response.raise_for_status()
|
| 86 |
+
return True
|
| 87 |
+
except Exception:
|
| 88 |
+
return False
|
app/utils/cache.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import hashlib
|
| 3 |
+
import json
|
| 4 |
+
from typing import Dict, Any, Optional
|
| 5 |
+
import logging
|
| 6 |
+
from app.utils.logging import log
|
| 7 |
+
from app.config.settings import (
|
| 8 |
+
api_call_stats
|
| 9 |
+
)
|
| 10 |
+
logger = logging.getLogger("my_logger")
|
| 11 |
+
|
| 12 |
+
class ResponseCacheManager:
|
| 13 |
+
"""管理API响应缓存的类"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, expiry_time: int, max_entries: int, remove_after_use: bool = True,
|
| 16 |
+
cache_dict: Dict[str, Dict[str, Any]] = None):
|
| 17 |
+
self.cache = cache_dict if cache_dict is not None else {} # 使用传入的缓存字典或创建新字典
|
| 18 |
+
self.expiry_time = expiry_time
|
| 19 |
+
self.max_entries = max_entries
|
| 20 |
+
self.remove_after_use = remove_after_use
|
| 21 |
+
|
| 22 |
+
def get(self, cache_key: str):
|
| 23 |
+
"""获取缓存项,如果存在且未过期"""
|
| 24 |
+
now = time.time()
|
| 25 |
+
if cache_key in self.cache and now < self.cache[cache_key].get('expiry_time', 0):
|
| 26 |
+
cached_item = self.cache[cache_key]
|
| 27 |
+
|
| 28 |
+
# 获取响应但先不删除
|
| 29 |
+
response = cached_item['response']
|
| 30 |
+
|
| 31 |
+
# 返回响应
|
| 32 |
+
return response, True
|
| 33 |
+
|
| 34 |
+
return None, False
|
| 35 |
+
|
| 36 |
+
def store(self, cache_key: str, response, client_ip: str = None):
|
| 37 |
+
"""存储响应到缓存"""
|
| 38 |
+
now = time.time()
|
| 39 |
+
self.cache[cache_key] = {
|
| 40 |
+
'response': response,
|
| 41 |
+
'expiry_time': now + self.expiry_time,
|
| 42 |
+
'created_at': now,
|
| 43 |
+
'client_ip': client_ip
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
log('info', f"响应已缓存: {cache_key[:8]}...",
|
| 47 |
+
extra={'cache_operation': 'store', 'request_type': 'non-stream'})
|
| 48 |
+
|
| 49 |
+
# 如果缓存超过限制,清理最旧的
|
| 50 |
+
self.clean_if_needed()
|
| 51 |
+
|
| 52 |
+
def clean_expired(self):
|
| 53 |
+
"""清理所有过期的缓存项"""
|
| 54 |
+
now = time.time()
|
| 55 |
+
expired_keys = [k for k, v in self.cache.items() if now > v.get('expiry_time', 0)]
|
| 56 |
+
|
| 57 |
+
for key in expired_keys:
|
| 58 |
+
del self.cache[key]
|
| 59 |
+
log('info', f"清理过期缓存: {key[:8]}...", extra={'cache_operation': 'clean'})
|
| 60 |
+
|
| 61 |
+
def clean_if_needed(self):
|
| 62 |
+
"""如果缓存数量超过限制,清理最旧的项目"""
|
| 63 |
+
if len(self.cache) <= self.max_entries:
|
| 64 |
+
return
|
| 65 |
+
|
| 66 |
+
# 按创建时间排序
|
| 67 |
+
sorted_keys = sorted(self.cache.keys(),
|
| 68 |
+
key=lambda k: self.cache[k].get('created_at', 0))
|
| 69 |
+
|
| 70 |
+
# 计算需要删除的数量
|
| 71 |
+
to_remove = len(self.cache) - self.max_entries
|
| 72 |
+
|
| 73 |
+
# 删除最旧的项
|
| 74 |
+
for key in sorted_keys[:to_remove]:
|
| 75 |
+
del self.cache[key]
|
| 76 |
+
log('info', f"缓存容量限制,删除旧缓存: {key[:8]}...", extra={'cache_operation': 'limit'})
|
| 77 |
+
|
| 78 |
+
def generate_cache_key(chat_request) -> str:
|
| 79 |
+
"""生成请求的唯一缓存键"""
|
| 80 |
+
# 创建包含请求关键信息的字典
|
| 81 |
+
request_data = {
|
| 82 |
+
'model': chat_request.model,
|
| 83 |
+
'messages': []
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
# 添加消息内容
|
| 87 |
+
for msg in chat_request.messages:
|
| 88 |
+
if isinstance(msg.content, str):
|
| 89 |
+
message_data = {'role': msg.role, 'content': msg.content}
|
| 90 |
+
request_data['messages'].append(message_data)
|
| 91 |
+
elif isinstance(msg.content, list):
|
| 92 |
+
content_list = []
|
| 93 |
+
for item in msg.content:
|
| 94 |
+
if item.get('type') == 'text':
|
| 95 |
+
content_list.append({'type': 'text', 'text': item.get('text')})
|
| 96 |
+
# 对于图像数据,我们只使用标识符而不是全部数据
|
| 97 |
+
elif item.get('type') == 'image_url':
|
| 98 |
+
image_data = item.get('image_url', {}).get('url', '')
|
| 99 |
+
if image_data.startswith('data:image/'):
|
| 100 |
+
# 对于base64图像,使用前32字符作为标识符
|
| 101 |
+
content_list.append({'type': 'image_url', 'hash': hashlib.md5(image_data[:32].encode()).hexdigest()})
|
| 102 |
+
else:
|
| 103 |
+
content_list.append({'type': 'image_url', 'url': image_data})
|
| 104 |
+
request_data['messages'].append({'role': msg.role, 'content': content_list})
|
| 105 |
+
|
| 106 |
+
# 将字典转换为JSON字符串并计算哈希值
|
| 107 |
+
json_data = json.dumps(request_data, sort_keys=True)
|
| 108 |
+
return hashlib.md5(json_data.encode()).hexdigest()
|
| 109 |
+
|
| 110 |
+
def cache_response(response, cache_key, client_ip, response_cache_manager, update_api_call_stats, endpoint=None,model=None):
|
| 111 |
+
"""
|
| 112 |
+
将响应存入缓存
|
| 113 |
+
|
| 114 |
+
参数:
|
| 115 |
+
- response: 响应对象
|
| 116 |
+
- cache_key: 缓存键
|
| 117 |
+
- client_ip: 客户端IP
|
| 118 |
+
- response_cache_manager: 缓存管理器
|
| 119 |
+
- update_api_call_stats: 更新统计的函数
|
| 120 |
+
- api_key: API密钥,用于更新API密钥使用统计
|
| 121 |
+
"""
|
| 122 |
+
if not cache_key:
|
| 123 |
+
return
|
| 124 |
+
|
| 125 |
+
# 先检查缓存是否已存在
|
| 126 |
+
existing_cache = cache_key in response_cache_manager.cache
|
| 127 |
+
|
| 128 |
+
if existing_cache:
|
| 129 |
+
log('info', f"缓存已存在,跳��存储: {cache_key[:8]}...",
|
| 130 |
+
extra={'cache_operation': 'skip-existing', 'request_type': 'non-stream'})
|
| 131 |
+
else:
|
| 132 |
+
response_cache_manager.store(cache_key, response, client_ip)
|
| 133 |
+
log('info', f"API响应已缓存: {cache_key[:8]}...",
|
| 134 |
+
extra={'cache_operation': 'store-new', 'request_type': 'non-stream'})
|
| 135 |
+
|
| 136 |
+
# 更新API调用统计
|
| 137 |
+
update_api_call_stats(api_call_stats,endpoint=endpoint,model=model)
|
app/utils/error_handling.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import logging
|
| 3 |
+
import asyncio
|
| 4 |
+
from fastapi import HTTPException, status
|
| 5 |
+
from app.utils.logging import format_log_message
|
| 6 |
+
from app.utils.logging import log
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger("my_logger")
|
| 9 |
+
|
| 10 |
+
def handle_gemini_error(error, current_api_key, key_manager) -> str:
|
| 11 |
+
if isinstance(error, requests.exceptions.HTTPError):
|
| 12 |
+
status_code = error.response.status_code
|
| 13 |
+
if status_code == 400:
|
| 14 |
+
try:
|
| 15 |
+
error_data = error.response.json()
|
| 16 |
+
if 'error' in error_data:
|
| 17 |
+
if error_data['error'].get('code') == "invalid_argument":
|
| 18 |
+
error_message = "无效的 API 密钥"
|
| 19 |
+
extra_log_invalid_key = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
|
| 20 |
+
log_msg = format_log_message('ERROR', f"{current_api_key[:8]} ... {current_api_key[-3:]} → 无效,可能已过期或被删除", extra=extra_log_invalid_key)
|
| 21 |
+
logger.error(log_msg)
|
| 22 |
+
# key_manager.blacklist_key(current_api_key)
|
| 23 |
+
|
| 24 |
+
return error_message
|
| 25 |
+
error_message = error_data['error'].get(
|
| 26 |
+
'message', 'Bad Request')
|
| 27 |
+
extra_log_400 = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
|
| 28 |
+
log_msg = format_log_message('WARNING', f"400 错误请求: {error_message}", extra=extra_log_400)
|
| 29 |
+
logger.warning(log_msg)
|
| 30 |
+
return f"400 错误请求: {error_message}"
|
| 31 |
+
except ValueError:
|
| 32 |
+
error_message = "400 错误请求:响应不是有效的JSON格式"
|
| 33 |
+
extra_log_400_json = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
|
| 34 |
+
log_msg = format_log_message('WARNING', error_message, extra=extra_log_400_json)
|
| 35 |
+
logger.warning(log_msg)
|
| 36 |
+
return error_message
|
| 37 |
+
|
| 38 |
+
elif status_code == 429:
|
| 39 |
+
error_message = "API 密钥配额已用尽或其他原因"
|
| 40 |
+
extra_log_429 = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
|
| 41 |
+
log_msg = format_log_message('WARNING', f"{current_api_key[:8]} ... {current_api_key[-3:]} → 429 官方资源耗尽或其他原因", extra=extra_log_429)
|
| 42 |
+
logger.warning(log_msg)
|
| 43 |
+
# key_manager.blacklist_key(current_api_key)
|
| 44 |
+
|
| 45 |
+
return error_message
|
| 46 |
+
|
| 47 |
+
elif status_code == 403:
|
| 48 |
+
error_message = "权限被拒绝"
|
| 49 |
+
extra_log_403 = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
|
| 50 |
+
log_msg = format_log_message('ERROR', f"{current_api_key[:8]} ... {current_api_key[-3:]} → 403 权限被拒绝", extra=extra_log_403)
|
| 51 |
+
logger.error(log_msg)
|
| 52 |
+
# key_manager.blacklist_key(current_api_key)
|
| 53 |
+
|
| 54 |
+
return error_message
|
| 55 |
+
else:
|
| 56 |
+
error_message = f"未知错误: {status_code}"
|
| 57 |
+
extra_log_other = {'key': current_api_key[:8], 'status_code': status_code, 'error_message': error_message}
|
| 58 |
+
log_msg = format_log_message('WARNING', f"{current_api_key[:8]} ... {current_api_key[-3:]} → {status_code} 未知错误", extra=extra_log_other)
|
| 59 |
+
logger.warning(log_msg)
|
| 60 |
+
|
| 61 |
+
return f"未知错误/模型不可用: {status_code}"
|
| 62 |
+
|
| 63 |
+
elif isinstance(error, requests.exceptions.ConnectionError):
|
| 64 |
+
error_message = "连接错误"
|
| 65 |
+
log_msg = format_log_message('WARNING', error_message, extra={'error_message': error_message})
|
| 66 |
+
logger.warning(log_msg)
|
| 67 |
+
return error_message
|
| 68 |
+
|
| 69 |
+
elif isinstance(error, requests.exceptions.Timeout):
|
| 70 |
+
error_message = "请求超时"
|
| 71 |
+
log_msg = format_log_message('WARNING', error_message, extra={'error_message': error_message})
|
| 72 |
+
logger.warning(log_msg)
|
| 73 |
+
return error_message
|
| 74 |
+
else:
|
| 75 |
+
error_message = f"发生未知错误: {error}"
|
| 76 |
+
log_msg = format_log_message('ERROR', error_message, extra={'error_message': error_message})
|
| 77 |
+
logger.error(log_msg)
|
| 78 |
+
return error_message
|
| 79 |
+
|
| 80 |
+
def translate_error(message: str) -> str:
|
| 81 |
+
if "quota exceeded" in message.lower():
|
| 82 |
+
return "API 密钥配额已用尽"
|
| 83 |
+
if "invalid argument" in message.lower():
|
| 84 |
+
return "无效参数"
|
| 85 |
+
if "internal server error" in message.lower():
|
| 86 |
+
return "服务器内部错误"
|
| 87 |
+
if "service unavailable" in message.lower():
|
| 88 |
+
return "服务不可用"
|
| 89 |
+
return message
|
| 90 |
+
|
| 91 |
+
async def handle_api_error(e: Exception, api_key: str, key_manager, request_type: str, model: str, retry_count: int = 0):
|
| 92 |
+
"""统一处理API错误"""
|
| 93 |
+
|
| 94 |
+
if isinstance(e, requests.exceptions.HTTPError) :
|
| 95 |
+
status_code = e.response.status_code
|
| 96 |
+
# 对500和503错误实现自动重试机制, 最多���试3次
|
| 97 |
+
if retry_count < 3 and (status_code == 500 or status_code == 503):
|
| 98 |
+
error_message = 'Gemini API 内部错误' if (status_code == 500) else "Gemini API 服务目前不可用"
|
| 99 |
+
|
| 100 |
+
# 等待时间 : MIN_RETRY_DELAY=1, MAX_RETRY_DELAY=16
|
| 101 |
+
wait_time = min(1 * (2 ** retry_count), 16)
|
| 102 |
+
log('warning', f"{error_message},将等待{wait_time}秒后重试 ({retry_count+1}/3)",
|
| 103 |
+
extra={'key': api_key[:8], 'request_type': request_type, 'model': model, 'status_code': int(status_code)})
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# 等待后返回重试信号
|
| 107 |
+
await asyncio.sleep(wait_time)
|
| 108 |
+
return {'remove_cache': False}
|
| 109 |
+
|
| 110 |
+
# 重试次数用尽,在日志中输出错误状态码
|
| 111 |
+
log('error', f"Gemini 服务器错误({status_code}), 且重试{retry_count}次后仍然失败",
|
| 112 |
+
extra={'key': api_key[:8], 'request_type': request_type, 'model': model, 'status_code': int(status_code)})
|
| 113 |
+
|
| 114 |
+
# 不再切换密钥,向客户端抛出HTTP异常
|
| 115 |
+
raise HTTPException(status_code=int(status_code),
|
| 116 |
+
detail=f"Gemini API 服务器错误({status_code}),请稍后重试")
|
| 117 |
+
|
| 118 |
+
# 对于其他错误,返回切换密钥的信号,并输出错误信息到日志中
|
| 119 |
+
error_detail = handle_gemini_error(e, api_key, key_manager)
|
| 120 |
+
return {'should_switch_key': True, 'error': error_detail, 'remove_cache': True}
|
app/utils/logging.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from collections import deque
|
| 4 |
+
from threading import Lock
|
| 5 |
+
|
| 6 |
+
DEBUG = False # 可以从环境变量中获取
|
| 7 |
+
|
| 8 |
+
LOG_FORMAT_DEBUG = '%(asctime)s - %(levelname)s - [%(key)s]-%(request_type)s-[%(model)s]-%(status_code)s: %(message)s - %(error_message)s'
|
| 9 |
+
LOG_FORMAT_NORMAL = '[%(asctime)s] [%(levelname)s] [%(key)s]-%(request_type)s-[%(model)s]-%(status_code)s: %(message)s'
|
| 10 |
+
|
| 11 |
+
# 配置 logger
|
| 12 |
+
logger = logging.getLogger("my_logger")
|
| 13 |
+
logger.setLevel(logging.DEBUG)
|
| 14 |
+
|
| 15 |
+
# 控制台处理器
|
| 16 |
+
console_handler = logging.StreamHandler()
|
| 17 |
+
console_formatter = logging.Formatter('%(message)s')
|
| 18 |
+
console_handler.setFormatter(console_formatter)
|
| 19 |
+
logger.addHandler(console_handler)
|
| 20 |
+
|
| 21 |
+
# 日志缓存,用于在网页上显示最近的日志
|
| 22 |
+
class LogManager:
|
| 23 |
+
def __init__(self, max_logs=100):
|
| 24 |
+
self.logs = deque(maxlen=max_logs) # 使用双端队列存储最近的日志
|
| 25 |
+
self.lock = Lock()
|
| 26 |
+
|
| 27 |
+
def add_log(self, log_entry):
|
| 28 |
+
with self.lock:
|
| 29 |
+
self.logs.append(log_entry)
|
| 30 |
+
|
| 31 |
+
def get_recent_logs(self, count=50):
|
| 32 |
+
with self.lock:
|
| 33 |
+
return list(self.logs)[-count:]
|
| 34 |
+
|
| 35 |
+
# 创建日志管理器实例
|
| 36 |
+
log_manager = LogManager()
|
| 37 |
+
|
| 38 |
+
def format_log_message(level, message, extra=None):
|
| 39 |
+
extra = extra or {}
|
| 40 |
+
log_values = {
|
| 41 |
+
'asctime': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 42 |
+
'levelname': level,
|
| 43 |
+
'key': extra.get('key', 'N/A'),
|
| 44 |
+
'request_type': extra.get('request_type', 'N/A'),
|
| 45 |
+
'model': extra.get('model', 'N/A'),
|
| 46 |
+
'status_code': extra.get('status_code', 'N/A'),
|
| 47 |
+
'error_message': extra.get('error_message', ''),
|
| 48 |
+
'message': message
|
| 49 |
+
}
|
| 50 |
+
log_format = LOG_FORMAT_DEBUG if DEBUG else LOG_FORMAT_NORMAL
|
| 51 |
+
formatted_log = log_format % log_values
|
| 52 |
+
|
| 53 |
+
# 将格式化后的日志添加到日志管理器
|
| 54 |
+
log_entry = {
|
| 55 |
+
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 56 |
+
'level': level,
|
| 57 |
+
'key': extra.get('key', 'N/A'),
|
| 58 |
+
'request_type': extra.get('request_type', 'N/A'),
|
| 59 |
+
'model': extra.get('model', 'N/A'),
|
| 60 |
+
'status_code': extra.get('status_code', 'N/A'),
|
| 61 |
+
'message': message,
|
| 62 |
+
'error_message': extra.get('error_message', ''),
|
| 63 |
+
'formatted': formatted_log
|
| 64 |
+
}
|
| 65 |
+
log_manager.add_log(log_entry)
|
| 66 |
+
|
| 67 |
+
return formatted_log
|
| 68 |
+
|
| 69 |
+
def log(level: str, message: str, **extra):
|
| 70 |
+
"""简化日志记录的统一函数"""
|
| 71 |
+
msg = format_log_message(level.upper(), message, extra=extra)
|
| 72 |
+
getattr(logger, level.lower())(msg)
|
app/utils/maintenance.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys,asyncio
|
| 2 |
+
#from apscheduler.schedulers.background import BackgroundScheduler
|
| 3 |
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler # 替换为异步调度器
|
| 4 |
+
from app.utils.logging import log
|
| 5 |
+
from app.utils.stats import clean_expired_stats
|
| 6 |
+
from app.config import api_call_stats
|
| 7 |
+
from app.utils import check_version
|
| 8 |
+
from zoneinfo import ZoneInfo
|
| 9 |
+
from app.config import settings
|
| 10 |
+
def handle_exception(exc_type, exc_value, exc_traceback):
|
| 11 |
+
"""
|
| 12 |
+
全局异常处理函数
|
| 13 |
+
|
| 14 |
+
处理未捕获的异常,并记录到日志中
|
| 15 |
+
"""
|
| 16 |
+
if issubclass(exc_type, KeyboardInterrupt):
|
| 17 |
+
sys.excepthook(exc_type, exc_value, exc_traceback)
|
| 18 |
+
return
|
| 19 |
+
from app.utils.error_handling import translate_error
|
| 20 |
+
error_message = translate_error(str(exc_value))
|
| 21 |
+
log('error', f"未捕获的异常: {error_message}", status_code=500, error_message=error_message)
|
| 22 |
+
'''
|
| 23 |
+
def schedule_cache_cleanup(response_cache_manager, active_requests_manager):
|
| 24 |
+
"""
|
| 25 |
+
设置定期清理缓存和活跃请求的定时任务
|
| 26 |
+
顺便定时检查更新
|
| 27 |
+
Args:
|
| 28 |
+
response_cache_manager: 响应缓存管理器实例
|
| 29 |
+
active_requests_manager: 活跃请求管理器实例
|
| 30 |
+
"""
|
| 31 |
+
scheduler = BackgroundScheduler()
|
| 32 |
+
scheduler.add_job(response_cache_manager.clean_expired, 'interval', minutes=1) # 每分钟清理过期缓存
|
| 33 |
+
scheduler.add_job(active_requests_manager.clean_completed, 'interval', seconds=30) # 每30秒清理已完成的活跃请求
|
| 34 |
+
scheduler.add_job(active_requests_manager.clean_long_running, 'interval', minutes=5, args=[300]) # 每5分钟清理运行超过5分钟的任务
|
| 35 |
+
scheduler.add_job(clean_expired_stats, 'interval', minutes=5,args=[api_call_stats]) # 每5分钟清理过期的统计数据
|
| 36 |
+
scheduler.add_job(check_version, 'interval', minutes=1) # 每4小时检查更新
|
| 37 |
+
scheduler.start()
|
| 38 |
+
|
| 39 |
+
return scheduler
|
| 40 |
+
|
| 41 |
+
'''
|
| 42 |
+
def schedule_cache_cleanup(response_cache_manager, active_requests_manager):
|
| 43 |
+
"""
|
| 44 |
+
设置定期清理缓存和活跃请求的定时任务
|
| 45 |
+
顺便定时检查更新
|
| 46 |
+
Args:
|
| 47 |
+
response_cache_manager: 响应缓存管理器实例
|
| 48 |
+
active_requests_manager: 活跃请求管理器实例
|
| 49 |
+
"""
|
| 50 |
+
beijing_tz = ZoneInfo("Asia/Shanghai")
|
| 51 |
+
scheduler = AsyncIOScheduler(timezone=beijing_tz) # 使用 AsyncIOScheduler 替代 BackgroundScheduler
|
| 52 |
+
|
| 53 |
+
# 添加任务时直接传递异步函数(无需额外包装)
|
| 54 |
+
scheduler.add_job(response_cache_manager.clean_expired, 'interval', minutes=1)
|
| 55 |
+
scheduler.add_job(active_requests_manager.clean_completed, 'interval', seconds=30)
|
| 56 |
+
scheduler.add_job(active_requests_manager.clean_long_running, 'interval', minutes=5, args=[300])
|
| 57 |
+
scheduler.add_job(clean_expired_stats, 'interval', minutes=5, args=[api_call_stats])
|
| 58 |
+
scheduler.add_job(check_version, 'interval', hours=4)
|
| 59 |
+
scheduler.add_job(api_call_stats_clean, 'cron', hour=16,minute=0) # 每天16:00清理统计数据
|
| 60 |
+
scheduler.start()
|
| 61 |
+
return scheduler
|
| 62 |
+
|
| 63 |
+
async def api_call_stats_clean():
|
| 64 |
+
"""
|
| 65 |
+
每天定时重置API调用统计数据
|
| 66 |
+
|
| 67 |
+
将settings.api_call_stats重置为初始空结构
|
| 68 |
+
"""
|
| 69 |
+
from app.utils.logging import log
|
| 70 |
+
|
| 71 |
+
# 清空原字典中的数据,而不是重新赋值
|
| 72 |
+
settings.api_call_stats['last_24h']['total'].clear()
|
| 73 |
+
settings.api_call_stats['last_24h']['by_endpoint'].clear()
|
| 74 |
+
settings.api_call_stats['hourly']['total'].clear()
|
| 75 |
+
settings.api_call_stats['hourly']['by_endpoint'].clear()
|
| 76 |
+
settings.api_call_stats['minute']['total'].clear()
|
| 77 |
+
settings.api_call_stats['minute']['by_endpoint'].clear()
|
| 78 |
+
|
| 79 |
+
# 重新初始化结构
|
| 80 |
+
settings.api_call_stats = {
|
| 81 |
+
'last_24h': {
|
| 82 |
+
'total': {},
|
| 83 |
+
'by_endpoint': {}
|
| 84 |
+
},
|
| 85 |
+
'hourly': {
|
| 86 |
+
'total': {},
|
| 87 |
+
'by_endpoint': {}
|
| 88 |
+
},
|
| 89 |
+
'minute': {
|
| 90 |
+
'total': {},
|
| 91 |
+
'by_endpoint': {}
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
# 记录日志,确认函数被执行
|
| 96 |
+
log('info', "API调用统计数据已重置")
|
app/utils/rate_limiting.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from threading import Lock
|
| 3 |
+
from fastapi import HTTPException, Request
|
| 4 |
+
|
| 5 |
+
rate_limit_data = {}
|
| 6 |
+
rate_limit_lock = Lock()
|
| 7 |
+
|
| 8 |
+
def protect_from_abuse(request: Request, max_requests_per_minute: int = 30, max_requests_per_day_per_ip: int = 600):
|
| 9 |
+
now = int(time.time())
|
| 10 |
+
minute = now // 60
|
| 11 |
+
day = now // (60 * 60 * 24)
|
| 12 |
+
|
| 13 |
+
minute_key = f"{request.url.path}:{minute}"
|
| 14 |
+
day_key = f"{request.client.host}:{day}"
|
| 15 |
+
|
| 16 |
+
with rate_limit_lock:
|
| 17 |
+
minute_count, minute_timestamp = rate_limit_data.get(
|
| 18 |
+
minute_key, (0, now))
|
| 19 |
+
if now - minute_timestamp >= 60:
|
| 20 |
+
minute_count = 0
|
| 21 |
+
minute_timestamp = now
|
| 22 |
+
minute_count += 1
|
| 23 |
+
rate_limit_data[minute_key] = (minute_count, minute_timestamp)
|
| 24 |
+
|
| 25 |
+
day_count, day_timestamp = rate_limit_data.get(day_key, (0, now))
|
| 26 |
+
if now - day_timestamp >= 86400:
|
| 27 |
+
day_count = 0
|
| 28 |
+
day_timestamp = now
|
| 29 |
+
day_count += 1
|
| 30 |
+
rate_limit_data[day_key] = (day_count, day_timestamp)
|
| 31 |
+
|
| 32 |
+
if minute_count > max_requests_per_minute:
|
| 33 |
+
raise HTTPException(status_code=429, detail={
|
| 34 |
+
"message": "Too many requests per minute", "limit": max_requests_per_minute})
|
| 35 |
+
if day_count > max_requests_per_day_per_ip:
|
| 36 |
+
raise HTTPException(status_code=429, detail={"message": "Too many requests per day from this IP", "limit": max_requests_per_day_per_ip})
|
app/utils/request.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import time
|
| 3 |
+
from typing import Dict, Any
|
| 4 |
+
from app.utils.logging import log
|
| 5 |
+
|
| 6 |
+
class ActiveRequestsManager:
|
| 7 |
+
"""管理活跃API请求的类"""
|
| 8 |
+
|
| 9 |
+
def __init__(self, requests_pool: Dict[str, asyncio.Task] = None):
|
| 10 |
+
self.active_requests = requests_pool if requests_pool is not None else {} # 存储活跃请求
|
| 11 |
+
|
| 12 |
+
def add(self, key: str, task: asyncio.Task):
|
| 13 |
+
"""添加新的活跃请求任务"""
|
| 14 |
+
task.creation_time = time.time() # 添加创建时间属性
|
| 15 |
+
self.active_requests[key] = task
|
| 16 |
+
|
| 17 |
+
def get(self, key: str):
|
| 18 |
+
"""获取活跃请求任务"""
|
| 19 |
+
return self.active_requests.get(key)
|
| 20 |
+
|
| 21 |
+
def remove(self, key: str):
|
| 22 |
+
"""移除活跃请求任务"""
|
| 23 |
+
if key in self.active_requests:
|
| 24 |
+
del self.active_requests[key]
|
| 25 |
+
return True
|
| 26 |
+
return False
|
| 27 |
+
|
| 28 |
+
def remove_by_prefix(self, prefix: str):
|
| 29 |
+
"""移除所有以特定前缀开头的活跃请求任务"""
|
| 30 |
+
keys_to_remove = [k for k in self.active_requests.keys() if k.startswith(prefix)]
|
| 31 |
+
for key in keys_to_remove:
|
| 32 |
+
self.remove(key)
|
| 33 |
+
return len(keys_to_remove)
|
| 34 |
+
|
| 35 |
+
def clean_completed(self):
|
| 36 |
+
"""清理所有已完成或已取消的任务"""
|
| 37 |
+
keys_to_remove = []
|
| 38 |
+
|
| 39 |
+
for key, task in self.active_requests.items():
|
| 40 |
+
if task.done() or task.cancelled():
|
| 41 |
+
keys_to_remove.append(key)
|
| 42 |
+
|
| 43 |
+
for key in keys_to_remove:
|
| 44 |
+
self.remove(key)
|
| 45 |
+
|
| 46 |
+
# if keys_to_remove:
|
| 47 |
+
# log('info', f"清理已完成请求任务: {len(keys_to_remove)}个", cleanup='active_requests')
|
| 48 |
+
|
| 49 |
+
def clean_long_running(self, max_age_seconds: int = 300):
|
| 50 |
+
"""清理长时间运行的任务"""
|
| 51 |
+
now = time.time()
|
| 52 |
+
long_running_keys = []
|
| 53 |
+
|
| 54 |
+
for key, task in list(self.active_requests.items()):
|
| 55 |
+
if (hasattr(task, 'creation_time') and
|
| 56 |
+
task.creation_time < now - max_age_seconds and
|
| 57 |
+
not task.done() and not task.cancelled()):
|
| 58 |
+
|
| 59 |
+
long_running_keys.append(key)
|
| 60 |
+
task.cancel() # 取消长时间运行的任务
|
| 61 |
+
|
| 62 |
+
if long_running_keys:
|
| 63 |
+
log('warning', f"取消长时间运行的任务: {len(long_running_keys)}个", cleanup='long_running_tasks')
|
| 64 |
+
|
| 65 |
+
async def check_client_disconnect(http_request, current_api_key: str, request_type: str, model: str):
|
| 66 |
+
"""检查客户端是否断开连接"""
|
| 67 |
+
while True:
|
| 68 |
+
if await http_request.is_disconnected():
|
| 69 |
+
extra_log = {'key': current_api_key[:8], 'request_type': request_type, 'model': model, 'error_message': '检测到客户端断开连接'}
|
| 70 |
+
log('info', "客户端连接已中断,等待API请求完成", extra=extra_log)
|
| 71 |
+
return True
|
| 72 |
+
await asyncio.sleep(0.5)
|
app/utils/response.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from fastapi import status
|
| 3 |
+
from fastapi.responses import JSONResponse
|
| 4 |
+
|
| 5 |
+
def create_chat_response(model: str, choices: list, id: str = None):
|
| 6 |
+
"""创建标准响应对象的工厂函数"""
|
| 7 |
+
return {
|
| 8 |
+
"id": id or f"chatcmpl-{int(time.time()*1000)}",
|
| 9 |
+
"object": "chat.completion",
|
| 10 |
+
"created": int(time.time()),
|
| 11 |
+
"model": model,
|
| 12 |
+
"choices": choices,
|
| 13 |
+
"usage": {
|
| 14 |
+
"prompt_tokens": 0,
|
| 15 |
+
"completion_tokens": 0,
|
| 16 |
+
"total_tokens": 0
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
def create_error_response(model: str, error_message: str):
|
| 21 |
+
"""创建错误响应对象的工厂函数"""
|
| 22 |
+
return create_chat_response(
|
| 23 |
+
model=model,
|
| 24 |
+
choices=[{
|
| 25 |
+
"index": 0,
|
| 26 |
+
"message": {
|
| 27 |
+
"role": "assistant",
|
| 28 |
+
"content": error_message
|
| 29 |
+
},
|
| 30 |
+
"finish_reason": "error"
|
| 31 |
+
}]
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
def create_response(chat_request, response_content):
|
| 35 |
+
"""创建标准响应对象但不缓存"""
|
| 36 |
+
# 创建响应对象
|
| 37 |
+
return create_chat_response(
|
| 38 |
+
model=chat_request.model,
|
| 39 |
+
choices=[{
|
| 40 |
+
"index": 0,
|
| 41 |
+
"message": {
|
| 42 |
+
"role": "assistant",
|
| 43 |
+
"content": response_content.text
|
| 44 |
+
},
|
| 45 |
+
"finish_reason": "stop"
|
| 46 |
+
}]
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
def handle_exception(exc_type, exc_value, exc_traceback, translate_error, log):
|
| 50 |
+
"""处理全局异常的函数"""
|
| 51 |
+
if issubclass(exc_type, KeyboardInterrupt):
|
| 52 |
+
# 对于KeyboardInterrupt,使用默认处理
|
| 53 |
+
import sys
|
| 54 |
+
sys.excepthook(exc_type, exc_value, exc_traceback)
|
| 55 |
+
return
|
| 56 |
+
|
| 57 |
+
# 对于其他异常,记录日志
|
| 58 |
+
error_message = translate_error(str(exc_value))
|
| 59 |
+
log('error', f"未捕获的异常: {error_message}", status_code=500, error_message=error_message)
|
app/utils/stats.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timedelta
|
| 2 |
+
from app.utils.logging import log
|
| 3 |
+
|
| 4 |
+
def clean_expired_stats(api_call_stats):
|
| 5 |
+
"""清理过期统计数据的函数"""
|
| 6 |
+
now = datetime.now()
|
| 7 |
+
|
| 8 |
+
# 清理24小时前的数据
|
| 9 |
+
# 清理总调用次数
|
| 10 |
+
for hour_key in list(api_call_stats['last_24h']['total'].keys()):
|
| 11 |
+
try:
|
| 12 |
+
hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
|
| 13 |
+
if (now - hour_time).total_seconds() > 24 * 3600: # 超过24小时
|
| 14 |
+
del api_call_stats['last_24h']['total'][hour_key]
|
| 15 |
+
except ValueError:
|
| 16 |
+
# 如果键格式不正确,直接删除
|
| 17 |
+
del api_call_stats['last_24h']['total'][hour_key]
|
| 18 |
+
|
| 19 |
+
# 清理按端点和模型分类的数据
|
| 20 |
+
for endpoint in list(api_call_stats['last_24h']['by_endpoint'].keys()):
|
| 21 |
+
if not isinstance(api_call_stats['last_24h']['by_endpoint'][endpoint], dict):
|
| 22 |
+
del api_call_stats['last_24h']['by_endpoint'][endpoint]
|
| 23 |
+
continue
|
| 24 |
+
|
| 25 |
+
for model in list(api_call_stats['last_24h']['by_endpoint'][endpoint].keys()):
|
| 26 |
+
if not isinstance(api_call_stats['last_24h']['by_endpoint'][endpoint][model], dict):
|
| 27 |
+
del api_call_stats['last_24h']['by_endpoint'][endpoint][model]
|
| 28 |
+
continue
|
| 29 |
+
|
| 30 |
+
for hour_key in list(api_call_stats['last_24h']['by_endpoint'][endpoint][model].keys()):
|
| 31 |
+
try:
|
| 32 |
+
hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
|
| 33 |
+
if (now - hour_time).total_seconds() > 24 * 3600: # 超过24小时
|
| 34 |
+
del api_call_stats['last_24h']['by_endpoint'][endpoint][model][hour_key]
|
| 35 |
+
except ValueError:
|
| 36 |
+
# 如果键格式不正确,直接删除
|
| 37 |
+
del api_call_stats['last_24h']['by_endpoint'][endpoint][model][hour_key]
|
| 38 |
+
|
| 39 |
+
# 清理一小时前的小时统计数据
|
| 40 |
+
one_hour_ago = now - timedelta(hours=1)
|
| 41 |
+
# 清理总调用次数
|
| 42 |
+
for hour_key in list(api_call_stats['hourly']['total'].keys()):
|
| 43 |
+
try:
|
| 44 |
+
hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
|
| 45 |
+
if hour_time < one_hour_ago:
|
| 46 |
+
del api_call_stats['hourly']['total'][hour_key]
|
| 47 |
+
except ValueError:
|
| 48 |
+
# 如果键格式不正确,直接删除
|
| 49 |
+
del api_call_stats['hourly']['total'][hour_key]
|
| 50 |
+
|
| 51 |
+
# 清理按端点和模型分类的数据
|
| 52 |
+
for endpoint in list(api_call_stats['hourly']['by_endpoint'].keys()):
|
| 53 |
+
if not isinstance(api_call_stats['hourly']['by_endpoint'][endpoint], dict):
|
| 54 |
+
del api_call_stats['hourly']['by_endpoint'][endpoint]
|
| 55 |
+
continue
|
| 56 |
+
|
| 57 |
+
for model in list(api_call_stats['hourly']['by_endpoint'][endpoint].keys()):
|
| 58 |
+
if not isinstance(api_call_stats['hourly']['by_endpoint'][endpoint][model], dict):
|
| 59 |
+
del api_call_stats['hourly']['by_endpoint'][endpoint][model]
|
| 60 |
+
continue
|
| 61 |
+
|
| 62 |
+
for hour_key in list(api_call_stats['hourly']['by_endpoint'][endpoint][model].keys()):
|
| 63 |
+
try:
|
| 64 |
+
hour_time = datetime.strptime(hour_key, '%Y-%m-%d %H:00')
|
| 65 |
+
if hour_time < one_hour_ago:
|
| 66 |
+
del api_call_stats['hourly']['by_endpoint'][endpoint][model][hour_key]
|
| 67 |
+
except ValueError:
|
| 68 |
+
# 如果键格式不正确,直接删除
|
| 69 |
+
del api_call_stats['hourly']['by_endpoint'][endpoint][model][hour_key]
|
| 70 |
+
|
| 71 |
+
# 清理一分钟前的分钟统计数据
|
| 72 |
+
one_minute_ago = now - timedelta(minutes=1)
|
| 73 |
+
# 清理总调用次数
|
| 74 |
+
for minute_key in list(api_call_stats['minute']['total'].keys()):
|
| 75 |
+
try:
|
| 76 |
+
minute_time = datetime.strptime(minute_key, '%Y-%m-%d %H:%M')
|
| 77 |
+
if minute_time < one_minute_ago:
|
| 78 |
+
del api_call_stats['minute']['total'][minute_key]
|
| 79 |
+
except ValueError:
|
| 80 |
+
# 如果键格式不正确,直接删除
|
| 81 |
+
del api_call_stats['minute']['total'][minute_key]
|
| 82 |
+
|
| 83 |
+
# 清理按端点和模型分类的数据
|
| 84 |
+
for endpoint in list(api_call_stats['minute']['by_endpoint'].keys()):
|
| 85 |
+
if not isinstance(api_call_stats['minute']['by_endpoint'][endpoint], dict):
|
| 86 |
+
del api_call_stats['minute']['by_endpoint'][endpoint]
|
| 87 |
+
continue
|
| 88 |
+
|
| 89 |
+
for model in list(api_call_stats['minute']['by_endpoint'][endpoint].keys()):
|
| 90 |
+
if not isinstance(api_call_stats['minute']['by_endpoint'][endpoint][model], dict):
|
| 91 |
+
del api_call_stats['minute']['by_endpoint'][endpoint][model]
|
| 92 |
+
continue
|
| 93 |
+
|
| 94 |
+
for minute_key in list(api_call_stats['minute']['by_endpoint'][endpoint][model].keys()):
|
| 95 |
+
try:
|
| 96 |
+
minute_time = datetime.strptime(minute_key, '%Y-%m-%d %H:%M')
|
| 97 |
+
if minute_time < one_minute_ago:
|
| 98 |
+
del api_call_stats['minute']['by_endpoint'][endpoint][model][minute_key]
|
| 99 |
+
except ValueError:
|
| 100 |
+
# 如果键格式不正确,直接删除
|
| 101 |
+
del api_call_stats['minute']['by_endpoint'][endpoint][model][minute_key]
|
| 102 |
+
|
| 103 |
+
def update_api_call_stats(api_call_stats, endpoint=None, model=None):
|
| 104 |
+
"""
|
| 105 |
+
更新API调用统计的函数
|
| 106 |
+
|
| 107 |
+
参数:
|
| 108 |
+
- api_call_stats: 统计数据字典
|
| 109 |
+
- endpoint: API端点,为None则只更新总调用次数
|
| 110 |
+
- model: 模型名称,与endpoint一起使用来分类统计数据
|
| 111 |
+
"""
|
| 112 |
+
now = datetime.now()
|
| 113 |
+
hour_key = now.strftime('%Y-%m-%d %H:00')
|
| 114 |
+
minute_key = now.strftime('%Y-%m-%d %H:%M')
|
| 115 |
+
|
| 116 |
+
# 检查并清理过期统计
|
| 117 |
+
clean_expired_stats(api_call_stats)
|
| 118 |
+
|
| 119 |
+
# 初始化总调用次数键(如果不存在)
|
| 120 |
+
if hour_key not in api_call_stats['last_24h']['total']:
|
| 121 |
+
api_call_stats['last_24h']['total'][hour_key] = 0
|
| 122 |
+
if hour_key not in api_call_stats['hourly']['total']:
|
| 123 |
+
api_call_stats['hourly']['total'][hour_key] = 0
|
| 124 |
+
if minute_key not in api_call_stats['minute']['total']:
|
| 125 |
+
api_call_stats['minute']['total'][minute_key] = 0
|
| 126 |
+
|
| 127 |
+
# 更新总调用次数统计
|
| 128 |
+
api_call_stats['last_24h']['total'][hour_key] += 1
|
| 129 |
+
api_call_stats['hourly']['total'][hour_key] += 1
|
| 130 |
+
api_call_stats['minute']['total'][minute_key] += 1
|
| 131 |
+
|
| 132 |
+
# 如果提供了端点,更新按端点分类的统计
|
| 133 |
+
if endpoint and model:
|
| 134 |
+
# 确保端点字典存在
|
| 135 |
+
for period in ['last_24h', 'hourly', 'minute']:
|
| 136 |
+
if endpoint not in api_call_stats[period]['by_endpoint']:
|
| 137 |
+
api_call_stats[period]['by_endpoint'][endpoint] = {}
|
| 138 |
+
|
| 139 |
+
if model not in api_call_stats[period]['by_endpoint'][endpoint]:
|
| 140 |
+
api_call_stats[period]['by_endpoint'][endpoint][model] = {}
|
| 141 |
+
|
| 142 |
+
# 初始化端点和模型特定的时间键(如果不存在)
|
| 143 |
+
if hour_key not in api_call_stats['last_24h']['by_endpoint'][endpoint][model]:
|
| 144 |
+
api_call_stats['last_24h']['by_endpoint'][endpoint][model][hour_key] = 0
|
| 145 |
+
|
| 146 |
+
if hour_key not in api_call_stats['hourly']['by_endpoint'][endpoint][model]:
|
| 147 |
+
api_call_stats['hourly']['by_endpoint'][endpoint][model][hour_key] = 0
|
| 148 |
+
|
| 149 |
+
if minute_key not in api_call_stats['minute']['by_endpoint'][endpoint][model]:
|
| 150 |
+
api_call_stats['minute']['by_endpoint'][endpoint][model][minute_key] = 0
|
| 151 |
+
|
| 152 |
+
# 更新端点特定的统计
|
| 153 |
+
if model is not None:
|
| 154 |
+
api_call_stats['last_24h']['by_endpoint'][endpoint][model][hour_key] += 1
|
| 155 |
+
api_call_stats['hourly']['by_endpoint'][endpoint][model][hour_key] += 1
|
| 156 |
+
api_call_stats['minute']['by_endpoint'][endpoint][model][minute_key] += 1
|
| 157 |
+
|
| 158 |
+
# 计算总调用次数
|
| 159 |
+
total_24h = sum(api_call_stats['last_24h']['total'].values())
|
| 160 |
+
total_hourly = sum(api_call_stats['hourly']['total'].values())
|
| 161 |
+
total_minute = sum(api_call_stats['minute']['total'].values())
|
| 162 |
+
|
| 163 |
+
log_message = "API调用统计已更新: 24小时=%s, 1小时=%s, 1分钟=%s" % (
|
| 164 |
+
total_24h, total_hourly, total_minute
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# 如果提供了端点和模型,添加端点特定的统计信息
|
| 168 |
+
if endpoint and model is not None:
|
| 169 |
+
try:
|
| 170 |
+
endpoint_24h = sum(api_call_stats['last_24h']['by_endpoint'][endpoint][model].values())
|
| 171 |
+
endpoint_hourly = sum(api_call_stats['hourly']['by_endpoint'][endpoint][model].values())
|
| 172 |
+
endpoint_minute = sum(api_call_stats['minute']['by_endpoint'][endpoint][model].values())
|
| 173 |
+
|
| 174 |
+
log_message += " | 端点 '%s' 模型 '%s': 24小时=%s, 1小时=%s, 1分钟=%s" % (
|
| 175 |
+
endpoint[:8], model[:8], endpoint_24h, endpoint_hourly, endpoint_minute
|
| 176 |
+
)
|
| 177 |
+
except (KeyError, TypeError):
|
| 178 |
+
# 如果统计数据结构中缺少某些键,记录基本信息
|
| 179 |
+
log_message += " | 端点 '%s' 模型 '%s': 统计数据不完整" % (
|
| 180 |
+
endpoint[:8], model[:8]
|
| 181 |
+
)
|
| 182 |
+
else:
|
| 183 |
+
log_message += " | 端点 '%s' 模型 '%s': 统计数据不完整"
|
| 184 |
+
|
| 185 |
+
log('info', log_message)
|
app/utils/version.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import logging
|
| 3 |
+
from app.utils.logging import log
|
| 4 |
+
import app.config.settings as settings
|
| 5 |
+
async def check_version():
|
| 6 |
+
"""
|
| 7 |
+
检查应用程序版本更新
|
| 8 |
+
|
| 9 |
+
从本地和远程获取版本信息,并比较版本号以确定是否有更新
|
| 10 |
+
"""
|
| 11 |
+
# 导入全局变量
|
| 12 |
+
try:
|
| 13 |
+
# 读取本地版本
|
| 14 |
+
with open("./version.txt", "r") as f:
|
| 15 |
+
version_line = f.read().strip()
|
| 16 |
+
settings.version['local_version'] = version_line.split("=")[1] if "=" in version_line else "0.0.0"
|
| 17 |
+
|
| 18 |
+
# 获取远程版本
|
| 19 |
+
github_url = "https://raw.githubusercontent.com/wyeeeee/hajimi/refs/heads/main/version.txt"
|
| 20 |
+
response = requests.get(github_url, timeout=5)
|
| 21 |
+
if response.status_code == 200:
|
| 22 |
+
version_line = response.text.strip()
|
| 23 |
+
settings.version['remote_version']= version_line.split("=")[1] if "=" in version_line else "0.0.0"
|
| 24 |
+
# 比较版本号
|
| 25 |
+
local_parts = [int(x) for x in settings.version['local_version'].split(".")]
|
| 26 |
+
remote_parts = [int(x) for x in settings.version['remote_version'].split(".")]
|
| 27 |
+
|
| 28 |
+
# 确保两个列表长度相同
|
| 29 |
+
while len(local_parts) < len(remote_parts):
|
| 30 |
+
local_parts.append(0)
|
| 31 |
+
while len(remote_parts) < len(local_parts):
|
| 32 |
+
remote_parts.append(0)
|
| 33 |
+
|
| 34 |
+
# 比较版本号
|
| 35 |
+
settings.version['has_update'] = False
|
| 36 |
+
for i in range(len(local_parts)):
|
| 37 |
+
if remote_parts[i] > local_parts[i]:
|
| 38 |
+
settings.version['has_update'] = True
|
| 39 |
+
break
|
| 40 |
+
elif remote_parts[i] < local_parts[i]:
|
| 41 |
+
break
|
| 42 |
+
|
| 43 |
+
log('info', f"版本检查: 本地版本 {settings.version['local_version']}, 远程版本 {settings.version['remote_version']}, 有更新: {settings.version['has_update']}")
|
| 44 |
+
else:
|
| 45 |
+
log('warning', f"无法获取远程版本信息,HTTP状态码: {response.status_code}")
|
| 46 |
+
except Exception as e:
|
| 47 |
+
log('error', f"版本检查失败: {str(e)}")
|
| 48 |
+
|
| 49 |
+
return settings.version['has_update']
|
hajimiUI/.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
.DS_Store
|
| 12 |
+
dist
|
| 13 |
+
dist-ssr
|
| 14 |
+
coverage
|
| 15 |
+
*.local
|
| 16 |
+
|
| 17 |
+
/cypress/videos/
|
| 18 |
+
/cypress/screenshots/
|
| 19 |
+
|
| 20 |
+
# Editor directories and files
|
| 21 |
+
.vscode/*
|
| 22 |
+
!.vscode/extensions.json
|
| 23 |
+
.idea
|
| 24 |
+
*.suo
|
| 25 |
+
*.ntvs*
|
| 26 |
+
*.njsproj
|
| 27 |
+
*.sln
|
| 28 |
+
*.sw?
|
| 29 |
+
|
| 30 |
+
*.tsbuildinfo
|
hajimiUI/.vscode/extensions.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"recommendations": ["Vue.volar"]
|
| 3 |
+
}
|
hajimiUI/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# hajimiUI
|
| 2 |
+
|
| 3 |
+
This template should help get you started developing with Vue 3 in Vite.
|
| 4 |
+
|
| 5 |
+
## Recommended IDE Setup
|
| 6 |
+
|
| 7 |
+
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
| 8 |
+
|
| 9 |
+
## Customize configuration
|
| 10 |
+
|
| 11 |
+
See [Vite Configuration Reference](https://vite.dev/config/).
|
| 12 |
+
|
| 13 |
+
## Project Setup
|
| 14 |
+
|
| 15 |
+
```sh
|
| 16 |
+
npm install
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
### Compile and Hot-Reload for Development
|
| 20 |
+
|
| 21 |
+
```sh
|
| 22 |
+
npm run dev
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
### Compile and Minify for Production
|
| 26 |
+
|
| 27 |
+
```sh
|
| 28 |
+
npm run build
|
| 29 |
+
```
|
hajimiUI/build.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import { execSync } from 'child_process';
|
| 4 |
+
import { fileURLToPath } from 'url';
|
| 5 |
+
|
| 6 |
+
// 获取 __dirname 等效值
|
| 7 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 8 |
+
const __dirname = path.dirname(__filename);
|
| 9 |
+
|
| 10 |
+
// 确保目标目录存在
|
| 11 |
+
const templatesDir = path.resolve(__dirname, '../app/templates');
|
| 12 |
+
const assetsDir = path.resolve(templatesDir, 'assets');
|
| 13 |
+
|
| 14 |
+
if (!fs.existsSync(templatesDir)) {
|
| 15 |
+
fs.mkdirSync(templatesDir, { recursive: true });
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
if (!fs.existsSync(assetsDir)) {
|
| 19 |
+
fs.mkdirSync(assetsDir, { recursive: true });
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// 构建 Vue 应用
|
| 23 |
+
console.log('正在构建 Vue 应用...');
|
| 24 |
+
execSync('npm run build', { stdio: 'inherit' });
|
| 25 |
+
|
| 26 |
+
// 检查生成的 CSS 文件名
|
| 27 |
+
const cssFiles = fs.readdirSync(assetsDir).filter(file => file.endsWith('.css'));
|
| 28 |
+
const cssFileName = cssFiles.length > 0 ? cssFiles[0] : 'index.css';
|
| 29 |
+
|
| 30 |
+
console.log(`检测到 CSS 文件: ${cssFileName}`);
|
| 31 |
+
|
| 32 |
+
// 创建一个简单的 index.html 文件,引用构建后的资源
|
| 33 |
+
const indexContent = `
|
| 34 |
+
<!DOCTYPE html>
|
| 35 |
+
<html lang="zh-CN">
|
| 36 |
+
<head>
|
| 37 |
+
<meta charset="UTF-8">
|
| 38 |
+
<link rel="icon" href="/assets/favicon.ico">
|
| 39 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 40 |
+
<title>Gemini API 代理服务</title>
|
| 41 |
+
<script type="module" crossorigin src="/assets/main.js"></script>
|
| 42 |
+
<link rel="stylesheet" href="/assets/${cssFileName}">
|
| 43 |
+
</head>
|
| 44 |
+
<body>
|
| 45 |
+
<div id="app"></div>
|
| 46 |
+
</body>
|
| 47 |
+
</html>
|
| 48 |
+
`;
|
| 49 |
+
|
| 50 |
+
// 将 index.html 写入到 app/templates 目录
|
| 51 |
+
const targetIndexPath = path.resolve(templatesDir, 'index.html');
|
| 52 |
+
fs.writeFileSync(targetIndexPath, indexContent);
|
| 53 |
+
|
| 54 |
+
console.log('构建完成!');
|
| 55 |
+
console.log(`- index.html 已创建到: ${targetIndexPath}`);
|
| 56 |
+
console.log(`- 静态资源已输出到: ${assetsDir}`);
|
hajimiUI/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<link rel="icon" href="/favicon.ico">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>HAJIMI</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="app"></div>
|
| 11 |
+
<script type="module" src="/src/main.js"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
hajimiUI/jsconfig.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"paths": {
|
| 4 |
+
"@/*": ["./src/*"]
|
| 5 |
+
}
|
| 6 |
+
},
|
| 7 |
+
"exclude": ["node_modules", "dist"]
|
| 8 |
+
}
|
hajimiUI/package-lock.json
ADDED
|
@@ -0,0 +1,2812 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "hajimiui",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "hajimiui",
|
| 9 |
+
"version": "0.0.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"hajimiui": "file:",
|
| 12 |
+
"pinia": "^3.0.1",
|
| 13 |
+
"vue": "^3.5.13",
|
| 14 |
+
"vue-router": "^4.5.0"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@vitejs/plugin-vue": "^5.2.3",
|
| 18 |
+
"vite": "^6.2.4",
|
| 19 |
+
"vite-plugin-vue-devtools": "^7.7.2"
|
| 20 |
+
}
|
| 21 |
+
},
|
| 22 |
+
"node_modules/@ampproject/remapping": {
|
| 23 |
+
"version": "2.3.0",
|
| 24 |
+
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
| 25 |
+
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
| 26 |
+
"dev": true,
|
| 27 |
+
"license": "Apache-2.0",
|
| 28 |
+
"dependencies": {
|
| 29 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 30 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 31 |
+
},
|
| 32 |
+
"engines": {
|
| 33 |
+
"node": ">=6.0.0"
|
| 34 |
+
}
|
| 35 |
+
},
|
| 36 |
+
"node_modules/@antfu/utils": {
|
| 37 |
+
"version": "0.7.10",
|
| 38 |
+
"resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz",
|
| 39 |
+
"integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==",
|
| 40 |
+
"dev": true,
|
| 41 |
+
"license": "MIT",
|
| 42 |
+
"funding": {
|
| 43 |
+
"url": "https://github.com/sponsors/antfu"
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
"node_modules/@babel/code-frame": {
|
| 47 |
+
"version": "7.26.2",
|
| 48 |
+
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
| 49 |
+
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
| 50 |
+
"dev": true,
|
| 51 |
+
"license": "MIT",
|
| 52 |
+
"dependencies": {
|
| 53 |
+
"@babel/helper-validator-identifier": "^7.25.9",
|
| 54 |
+
"js-tokens": "^4.0.0",
|
| 55 |
+
"picocolors": "^1.0.0"
|
| 56 |
+
},
|
| 57 |
+
"engines": {
|
| 58 |
+
"node": ">=6.9.0"
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"node_modules/@babel/compat-data": {
|
| 62 |
+
"version": "7.26.8",
|
| 63 |
+
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz",
|
| 64 |
+
"integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==",
|
| 65 |
+
"dev": true,
|
| 66 |
+
"license": "MIT",
|
| 67 |
+
"engines": {
|
| 68 |
+
"node": ">=6.9.0"
|
| 69 |
+
}
|
| 70 |
+
},
|
| 71 |
+
"node_modules/@babel/core": {
|
| 72 |
+
"version": "7.26.10",
|
| 73 |
+
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
|
| 74 |
+
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
|
| 75 |
+
"dev": true,
|
| 76 |
+
"license": "MIT",
|
| 77 |
+
"dependencies": {
|
| 78 |
+
"@ampproject/remapping": "^2.2.0",
|
| 79 |
+
"@babel/code-frame": "^7.26.2",
|
| 80 |
+
"@babel/generator": "^7.26.10",
|
| 81 |
+
"@babel/helper-compilation-targets": "^7.26.5",
|
| 82 |
+
"@babel/helper-module-transforms": "^7.26.0",
|
| 83 |
+
"@babel/helpers": "^7.26.10",
|
| 84 |
+
"@babel/parser": "^7.26.10",
|
| 85 |
+
"@babel/template": "^7.26.9",
|
| 86 |
+
"@babel/traverse": "^7.26.10",
|
| 87 |
+
"@babel/types": "^7.26.10",
|
| 88 |
+
"convert-source-map": "^2.0.0",
|
| 89 |
+
"debug": "^4.1.0",
|
| 90 |
+
"gensync": "^1.0.0-beta.2",
|
| 91 |
+
"json5": "^2.2.3",
|
| 92 |
+
"semver": "^6.3.1"
|
| 93 |
+
},
|
| 94 |
+
"engines": {
|
| 95 |
+
"node": ">=6.9.0"
|
| 96 |
+
},
|
| 97 |
+
"funding": {
|
| 98 |
+
"type": "opencollective",
|
| 99 |
+
"url": "https://opencollective.com/babel"
|
| 100 |
+
}
|
| 101 |
+
},
|
| 102 |
+
"node_modules/@babel/generator": {
|
| 103 |
+
"version": "7.27.0",
|
| 104 |
+
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz",
|
| 105 |
+
"integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==",
|
| 106 |
+
"dev": true,
|
| 107 |
+
"license": "MIT",
|
| 108 |
+
"dependencies": {
|
| 109 |
+
"@babel/parser": "^7.27.0",
|
| 110 |
+
"@babel/types": "^7.27.0",
|
| 111 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 112 |
+
"@jridgewell/trace-mapping": "^0.3.25",
|
| 113 |
+
"jsesc": "^3.0.2"
|
| 114 |
+
},
|
| 115 |
+
"engines": {
|
| 116 |
+
"node": ">=6.9.0"
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
"node_modules/@babel/helper-annotate-as-pure": {
|
| 120 |
+
"version": "7.25.9",
|
| 121 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz",
|
| 122 |
+
"integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==",
|
| 123 |
+
"dev": true,
|
| 124 |
+
"license": "MIT",
|
| 125 |
+
"dependencies": {
|
| 126 |
+
"@babel/types": "^7.25.9"
|
| 127 |
+
},
|
| 128 |
+
"engines": {
|
| 129 |
+
"node": ">=6.9.0"
|
| 130 |
+
}
|
| 131 |
+
},
|
| 132 |
+
"node_modules/@babel/helper-compilation-targets": {
|
| 133 |
+
"version": "7.27.0",
|
| 134 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz",
|
| 135 |
+
"integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==",
|
| 136 |
+
"dev": true,
|
| 137 |
+
"license": "MIT",
|
| 138 |
+
"dependencies": {
|
| 139 |
+
"@babel/compat-data": "^7.26.8",
|
| 140 |
+
"@babel/helper-validator-option": "^7.25.9",
|
| 141 |
+
"browserslist": "^4.24.0",
|
| 142 |
+
"lru-cache": "^5.1.1",
|
| 143 |
+
"semver": "^6.3.1"
|
| 144 |
+
},
|
| 145 |
+
"engines": {
|
| 146 |
+
"node": ">=6.9.0"
|
| 147 |
+
}
|
| 148 |
+
},
|
| 149 |
+
"node_modules/@babel/helper-create-class-features-plugin": {
|
| 150 |
+
"version": "7.27.0",
|
| 151 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz",
|
| 152 |
+
"integrity": "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==",
|
| 153 |
+
"dev": true,
|
| 154 |
+
"license": "MIT",
|
| 155 |
+
"dependencies": {
|
| 156 |
+
"@babel/helper-annotate-as-pure": "^7.25.9",
|
| 157 |
+
"@babel/helper-member-expression-to-functions": "^7.25.9",
|
| 158 |
+
"@babel/helper-optimise-call-expression": "^7.25.9",
|
| 159 |
+
"@babel/helper-replace-supers": "^7.26.5",
|
| 160 |
+
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
|
| 161 |
+
"@babel/traverse": "^7.27.0",
|
| 162 |
+
"semver": "^6.3.1"
|
| 163 |
+
},
|
| 164 |
+
"engines": {
|
| 165 |
+
"node": ">=6.9.0"
|
| 166 |
+
},
|
| 167 |
+
"peerDependencies": {
|
| 168 |
+
"@babel/core": "^7.0.0"
|
| 169 |
+
}
|
| 170 |
+
},
|
| 171 |
+
"node_modules/@babel/helper-member-expression-to-functions": {
|
| 172 |
+
"version": "7.25.9",
|
| 173 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz",
|
| 174 |
+
"integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==",
|
| 175 |
+
"dev": true,
|
| 176 |
+
"license": "MIT",
|
| 177 |
+
"dependencies": {
|
| 178 |
+
"@babel/traverse": "^7.25.9",
|
| 179 |
+
"@babel/types": "^7.25.9"
|
| 180 |
+
},
|
| 181 |
+
"engines": {
|
| 182 |
+
"node": ">=6.9.0"
|
| 183 |
+
}
|
| 184 |
+
},
|
| 185 |
+
"node_modules/@babel/helper-module-imports": {
|
| 186 |
+
"version": "7.25.9",
|
| 187 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
|
| 188 |
+
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
|
| 189 |
+
"dev": true,
|
| 190 |
+
"license": "MIT",
|
| 191 |
+
"dependencies": {
|
| 192 |
+
"@babel/traverse": "^7.25.9",
|
| 193 |
+
"@babel/types": "^7.25.9"
|
| 194 |
+
},
|
| 195 |
+
"engines": {
|
| 196 |
+
"node": ">=6.9.0"
|
| 197 |
+
}
|
| 198 |
+
},
|
| 199 |
+
"node_modules/@babel/helper-module-transforms": {
|
| 200 |
+
"version": "7.26.0",
|
| 201 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
|
| 202 |
+
"integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
|
| 203 |
+
"dev": true,
|
| 204 |
+
"license": "MIT",
|
| 205 |
+
"dependencies": {
|
| 206 |
+
"@babel/helper-module-imports": "^7.25.9",
|
| 207 |
+
"@babel/helper-validator-identifier": "^7.25.9",
|
| 208 |
+
"@babel/traverse": "^7.25.9"
|
| 209 |
+
},
|
| 210 |
+
"engines": {
|
| 211 |
+
"node": ">=6.9.0"
|
| 212 |
+
},
|
| 213 |
+
"peerDependencies": {
|
| 214 |
+
"@babel/core": "^7.0.0"
|
| 215 |
+
}
|
| 216 |
+
},
|
| 217 |
+
"node_modules/@babel/helper-optimise-call-expression": {
|
| 218 |
+
"version": "7.25.9",
|
| 219 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz",
|
| 220 |
+
"integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==",
|
| 221 |
+
"dev": true,
|
| 222 |
+
"license": "MIT",
|
| 223 |
+
"dependencies": {
|
| 224 |
+
"@babel/types": "^7.25.9"
|
| 225 |
+
},
|
| 226 |
+
"engines": {
|
| 227 |
+
"node": ">=6.9.0"
|
| 228 |
+
}
|
| 229 |
+
},
|
| 230 |
+
"node_modules/@babel/helper-plugin-utils": {
|
| 231 |
+
"version": "7.26.5",
|
| 232 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz",
|
| 233 |
+
"integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==",
|
| 234 |
+
"dev": true,
|
| 235 |
+
"license": "MIT",
|
| 236 |
+
"engines": {
|
| 237 |
+
"node": ">=6.9.0"
|
| 238 |
+
}
|
| 239 |
+
},
|
| 240 |
+
"node_modules/@babel/helper-replace-supers": {
|
| 241 |
+
"version": "7.26.5",
|
| 242 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz",
|
| 243 |
+
"integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==",
|
| 244 |
+
"dev": true,
|
| 245 |
+
"license": "MIT",
|
| 246 |
+
"dependencies": {
|
| 247 |
+
"@babel/helper-member-expression-to-functions": "^7.25.9",
|
| 248 |
+
"@babel/helper-optimise-call-expression": "^7.25.9",
|
| 249 |
+
"@babel/traverse": "^7.26.5"
|
| 250 |
+
},
|
| 251 |
+
"engines": {
|
| 252 |
+
"node": ">=6.9.0"
|
| 253 |
+
},
|
| 254 |
+
"peerDependencies": {
|
| 255 |
+
"@babel/core": "^7.0.0"
|
| 256 |
+
}
|
| 257 |
+
},
|
| 258 |
+
"node_modules/@babel/helper-skip-transparent-expression-wrappers": {
|
| 259 |
+
"version": "7.25.9",
|
| 260 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz",
|
| 261 |
+
"integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==",
|
| 262 |
+
"dev": true,
|
| 263 |
+
"license": "MIT",
|
| 264 |
+
"dependencies": {
|
| 265 |
+
"@babel/traverse": "^7.25.9",
|
| 266 |
+
"@babel/types": "^7.25.9"
|
| 267 |
+
},
|
| 268 |
+
"engines": {
|
| 269 |
+
"node": ">=6.9.0"
|
| 270 |
+
}
|
| 271 |
+
},
|
| 272 |
+
"node_modules/@babel/helper-string-parser": {
|
| 273 |
+
"version": "7.25.9",
|
| 274 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
| 275 |
+
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
| 276 |
+
"license": "MIT",
|
| 277 |
+
"engines": {
|
| 278 |
+
"node": ">=6.9.0"
|
| 279 |
+
}
|
| 280 |
+
},
|
| 281 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 282 |
+
"version": "7.25.9",
|
| 283 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
| 284 |
+
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
| 285 |
+
"license": "MIT",
|
| 286 |
+
"engines": {
|
| 287 |
+
"node": ">=6.9.0"
|
| 288 |
+
}
|
| 289 |
+
},
|
| 290 |
+
"node_modules/@babel/helper-validator-option": {
|
| 291 |
+
"version": "7.25.9",
|
| 292 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
|
| 293 |
+
"integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
|
| 294 |
+
"dev": true,
|
| 295 |
+
"license": "MIT",
|
| 296 |
+
"engines": {
|
| 297 |
+
"node": ">=6.9.0"
|
| 298 |
+
}
|
| 299 |
+
},
|
| 300 |
+
"node_modules/@babel/helpers": {
|
| 301 |
+
"version": "7.27.0",
|
| 302 |
+
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz",
|
| 303 |
+
"integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==",
|
| 304 |
+
"dev": true,
|
| 305 |
+
"license": "MIT",
|
| 306 |
+
"dependencies": {
|
| 307 |
+
"@babel/template": "^7.27.0",
|
| 308 |
+
"@babel/types": "^7.27.0"
|
| 309 |
+
},
|
| 310 |
+
"engines": {
|
| 311 |
+
"node": ">=6.9.0"
|
| 312 |
+
}
|
| 313 |
+
},
|
| 314 |
+
"node_modules/@babel/parser": {
|
| 315 |
+
"version": "7.27.0",
|
| 316 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
|
| 317 |
+
"integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
|
| 318 |
+
"license": "MIT",
|
| 319 |
+
"dependencies": {
|
| 320 |
+
"@babel/types": "^7.27.0"
|
| 321 |
+
},
|
| 322 |
+
"bin": {
|
| 323 |
+
"parser": "bin/babel-parser.js"
|
| 324 |
+
},
|
| 325 |
+
"engines": {
|
| 326 |
+
"node": ">=6.0.0"
|
| 327 |
+
}
|
| 328 |
+
},
|
| 329 |
+
"node_modules/@babel/plugin-proposal-decorators": {
|
| 330 |
+
"version": "7.25.9",
|
| 331 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.25.9.tgz",
|
| 332 |
+
"integrity": "sha512-smkNLL/O1ezy9Nhy4CNosc4Va+1wo5w4gzSZeLe6y6dM4mmHfYOCPolXQPHQxonZCF+ZyebxN9vqOolkYrSn5g==",
|
| 333 |
+
"dev": true,
|
| 334 |
+
"license": "MIT",
|
| 335 |
+
"dependencies": {
|
| 336 |
+
"@babel/helper-create-class-features-plugin": "^7.25.9",
|
| 337 |
+
"@babel/helper-plugin-utils": "^7.25.9",
|
| 338 |
+
"@babel/plugin-syntax-decorators": "^7.25.9"
|
| 339 |
+
},
|
| 340 |
+
"engines": {
|
| 341 |
+
"node": ">=6.9.0"
|
| 342 |
+
},
|
| 343 |
+
"peerDependencies": {
|
| 344 |
+
"@babel/core": "^7.0.0-0"
|
| 345 |
+
}
|
| 346 |
+
},
|
| 347 |
+
"node_modules/@babel/plugin-syntax-decorators": {
|
| 348 |
+
"version": "7.25.9",
|
| 349 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.25.9.tgz",
|
| 350 |
+
"integrity": "sha512-ryzI0McXUPJnRCvMo4lumIKZUzhYUO/ScI+Mz4YVaTLt04DHNSjEUjKVvbzQjZFLuod/cYEc07mJWhzl6v4DPg==",
|
| 351 |
+
"dev": true,
|
| 352 |
+
"license": "MIT",
|
| 353 |
+
"dependencies": {
|
| 354 |
+
"@babel/helper-plugin-utils": "^7.25.9"
|
| 355 |
+
},
|
| 356 |
+
"engines": {
|
| 357 |
+
"node": ">=6.9.0"
|
| 358 |
+
},
|
| 359 |
+
"peerDependencies": {
|
| 360 |
+
"@babel/core": "^7.0.0-0"
|
| 361 |
+
}
|
| 362 |
+
},
|
| 363 |
+
"node_modules/@babel/plugin-syntax-import-attributes": {
|
| 364 |
+
"version": "7.26.0",
|
| 365 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz",
|
| 366 |
+
"integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==",
|
| 367 |
+
"dev": true,
|
| 368 |
+
"license": "MIT",
|
| 369 |
+
"dependencies": {
|
| 370 |
+
"@babel/helper-plugin-utils": "^7.25.9"
|
| 371 |
+
},
|
| 372 |
+
"engines": {
|
| 373 |
+
"node": ">=6.9.0"
|
| 374 |
+
},
|
| 375 |
+
"peerDependencies": {
|
| 376 |
+
"@babel/core": "^7.0.0-0"
|
| 377 |
+
}
|
| 378 |
+
},
|
| 379 |
+
"node_modules/@babel/plugin-syntax-import-meta": {
|
| 380 |
+
"version": "7.10.4",
|
| 381 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
|
| 382 |
+
"integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
|
| 383 |
+
"dev": true,
|
| 384 |
+
"license": "MIT",
|
| 385 |
+
"dependencies": {
|
| 386 |
+
"@babel/helper-plugin-utils": "^7.10.4"
|
| 387 |
+
},
|
| 388 |
+
"peerDependencies": {
|
| 389 |
+
"@babel/core": "^7.0.0-0"
|
| 390 |
+
}
|
| 391 |
+
},
|
| 392 |
+
"node_modules/@babel/plugin-syntax-jsx": {
|
| 393 |
+
"version": "7.25.9",
|
| 394 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz",
|
| 395 |
+
"integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==",
|
| 396 |
+
"dev": true,
|
| 397 |
+
"license": "MIT",
|
| 398 |
+
"dependencies": {
|
| 399 |
+
"@babel/helper-plugin-utils": "^7.25.9"
|
| 400 |
+
},
|
| 401 |
+
"engines": {
|
| 402 |
+
"node": ">=6.9.0"
|
| 403 |
+
},
|
| 404 |
+
"peerDependencies": {
|
| 405 |
+
"@babel/core": "^7.0.0-0"
|
| 406 |
+
}
|
| 407 |
+
},
|
| 408 |
+
"node_modules/@babel/plugin-syntax-typescript": {
|
| 409 |
+
"version": "7.25.9",
|
| 410 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz",
|
| 411 |
+
"integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==",
|
| 412 |
+
"dev": true,
|
| 413 |
+
"license": "MIT",
|
| 414 |
+
"dependencies": {
|
| 415 |
+
"@babel/helper-plugin-utils": "^7.25.9"
|
| 416 |
+
},
|
| 417 |
+
"engines": {
|
| 418 |
+
"node": ">=6.9.0"
|
| 419 |
+
},
|
| 420 |
+
"peerDependencies": {
|
| 421 |
+
"@babel/core": "^7.0.0-0"
|
| 422 |
+
}
|
| 423 |
+
},
|
| 424 |
+
"node_modules/@babel/plugin-transform-typescript": {
|
| 425 |
+
"version": "7.27.0",
|
| 426 |
+
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.27.0.tgz",
|
| 427 |
+
"integrity": "sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg==",
|
| 428 |
+
"dev": true,
|
| 429 |
+
"license": "MIT",
|
| 430 |
+
"dependencies": {
|
| 431 |
+
"@babel/helper-annotate-as-pure": "^7.25.9",
|
| 432 |
+
"@babel/helper-create-class-features-plugin": "^7.27.0",
|
| 433 |
+
"@babel/helper-plugin-utils": "^7.26.5",
|
| 434 |
+
"@babel/helper-skip-transparent-expression-wrappers": "^7.25.9",
|
| 435 |
+
"@babel/plugin-syntax-typescript": "^7.25.9"
|
| 436 |
+
},
|
| 437 |
+
"engines": {
|
| 438 |
+
"node": ">=6.9.0"
|
| 439 |
+
},
|
| 440 |
+
"peerDependencies": {
|
| 441 |
+
"@babel/core": "^7.0.0-0"
|
| 442 |
+
}
|
| 443 |
+
},
|
| 444 |
+
"node_modules/@babel/template": {
|
| 445 |
+
"version": "7.27.0",
|
| 446 |
+
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
|
| 447 |
+
"integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==",
|
| 448 |
+
"dev": true,
|
| 449 |
+
"license": "MIT",
|
| 450 |
+
"dependencies": {
|
| 451 |
+
"@babel/code-frame": "^7.26.2",
|
| 452 |
+
"@babel/parser": "^7.27.0",
|
| 453 |
+
"@babel/types": "^7.27.0"
|
| 454 |
+
},
|
| 455 |
+
"engines": {
|
| 456 |
+
"node": ">=6.9.0"
|
| 457 |
+
}
|
| 458 |
+
},
|
| 459 |
+
"node_modules/@babel/traverse": {
|
| 460 |
+
"version": "7.27.0",
|
| 461 |
+
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
|
| 462 |
+
"integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
|
| 463 |
+
"dev": true,
|
| 464 |
+
"license": "MIT",
|
| 465 |
+
"dependencies": {
|
| 466 |
+
"@babel/code-frame": "^7.26.2",
|
| 467 |
+
"@babel/generator": "^7.27.0",
|
| 468 |
+
"@babel/parser": "^7.27.0",
|
| 469 |
+
"@babel/template": "^7.27.0",
|
| 470 |
+
"@babel/types": "^7.27.0",
|
| 471 |
+
"debug": "^4.3.1",
|
| 472 |
+
"globals": "^11.1.0"
|
| 473 |
+
},
|
| 474 |
+
"engines": {
|
| 475 |
+
"node": ">=6.9.0"
|
| 476 |
+
}
|
| 477 |
+
},
|
| 478 |
+
"node_modules/@babel/types": {
|
| 479 |
+
"version": "7.27.0",
|
| 480 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz",
|
| 481 |
+
"integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==",
|
| 482 |
+
"license": "MIT",
|
| 483 |
+
"dependencies": {
|
| 484 |
+
"@babel/helper-string-parser": "^7.25.9",
|
| 485 |
+
"@babel/helper-validator-identifier": "^7.25.9"
|
| 486 |
+
},
|
| 487 |
+
"engines": {
|
| 488 |
+
"node": ">=6.9.0"
|
| 489 |
+
}
|
| 490 |
+
},
|
| 491 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 492 |
+
"version": "0.25.2",
|
| 493 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz",
|
| 494 |
+
"integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==",
|
| 495 |
+
"cpu": [
|
| 496 |
+
"ppc64"
|
| 497 |
+
],
|
| 498 |
+
"dev": true,
|
| 499 |
+
"license": "MIT",
|
| 500 |
+
"optional": true,
|
| 501 |
+
"os": [
|
| 502 |
+
"aix"
|
| 503 |
+
],
|
| 504 |
+
"engines": {
|
| 505 |
+
"node": ">=18"
|
| 506 |
+
}
|
| 507 |
+
},
|
| 508 |
+
"node_modules/@esbuild/android-arm": {
|
| 509 |
+
"version": "0.25.2",
|
| 510 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz",
|
| 511 |
+
"integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==",
|
| 512 |
+
"cpu": [
|
| 513 |
+
"arm"
|
| 514 |
+
],
|
| 515 |
+
"dev": true,
|
| 516 |
+
"license": "MIT",
|
| 517 |
+
"optional": true,
|
| 518 |
+
"os": [
|
| 519 |
+
"android"
|
| 520 |
+
],
|
| 521 |
+
"engines": {
|
| 522 |
+
"node": ">=18"
|
| 523 |
+
}
|
| 524 |
+
},
|
| 525 |
+
"node_modules/@esbuild/android-arm64": {
|
| 526 |
+
"version": "0.25.2",
|
| 527 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz",
|
| 528 |
+
"integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==",
|
| 529 |
+
"cpu": [
|
| 530 |
+
"arm64"
|
| 531 |
+
],
|
| 532 |
+
"dev": true,
|
| 533 |
+
"license": "MIT",
|
| 534 |
+
"optional": true,
|
| 535 |
+
"os": [
|
| 536 |
+
"android"
|
| 537 |
+
],
|
| 538 |
+
"engines": {
|
| 539 |
+
"node": ">=18"
|
| 540 |
+
}
|
| 541 |
+
},
|
| 542 |
+
"node_modules/@esbuild/android-x64": {
|
| 543 |
+
"version": "0.25.2",
|
| 544 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz",
|
| 545 |
+
"integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==",
|
| 546 |
+
"cpu": [
|
| 547 |
+
"x64"
|
| 548 |
+
],
|
| 549 |
+
"dev": true,
|
| 550 |
+
"license": "MIT",
|
| 551 |
+
"optional": true,
|
| 552 |
+
"os": [
|
| 553 |
+
"android"
|
| 554 |
+
],
|
| 555 |
+
"engines": {
|
| 556 |
+
"node": ">=18"
|
| 557 |
+
}
|
| 558 |
+
},
|
| 559 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 560 |
+
"version": "0.25.2",
|
| 561 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz",
|
| 562 |
+
"integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==",
|
| 563 |
+
"cpu": [
|
| 564 |
+
"arm64"
|
| 565 |
+
],
|
| 566 |
+
"dev": true,
|
| 567 |
+
"license": "MIT",
|
| 568 |
+
"optional": true,
|
| 569 |
+
"os": [
|
| 570 |
+
"darwin"
|
| 571 |
+
],
|
| 572 |
+
"engines": {
|
| 573 |
+
"node": ">=18"
|
| 574 |
+
}
|
| 575 |
+
},
|
| 576 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 577 |
+
"version": "0.25.2",
|
| 578 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz",
|
| 579 |
+
"integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==",
|
| 580 |
+
"cpu": [
|
| 581 |
+
"x64"
|
| 582 |
+
],
|
| 583 |
+
"dev": true,
|
| 584 |
+
"license": "MIT",
|
| 585 |
+
"optional": true,
|
| 586 |
+
"os": [
|
| 587 |
+
"darwin"
|
| 588 |
+
],
|
| 589 |
+
"engines": {
|
| 590 |
+
"node": ">=18"
|
| 591 |
+
}
|
| 592 |
+
},
|
| 593 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 594 |
+
"version": "0.25.2",
|
| 595 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz",
|
| 596 |
+
"integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==",
|
| 597 |
+
"cpu": [
|
| 598 |
+
"arm64"
|
| 599 |
+
],
|
| 600 |
+
"dev": true,
|
| 601 |
+
"license": "MIT",
|
| 602 |
+
"optional": true,
|
| 603 |
+
"os": [
|
| 604 |
+
"freebsd"
|
| 605 |
+
],
|
| 606 |
+
"engines": {
|
| 607 |
+
"node": ">=18"
|
| 608 |
+
}
|
| 609 |
+
},
|
| 610 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 611 |
+
"version": "0.25.2",
|
| 612 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz",
|
| 613 |
+
"integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==",
|
| 614 |
+
"cpu": [
|
| 615 |
+
"x64"
|
| 616 |
+
],
|
| 617 |
+
"dev": true,
|
| 618 |
+
"license": "MIT",
|
| 619 |
+
"optional": true,
|
| 620 |
+
"os": [
|
| 621 |
+
"freebsd"
|
| 622 |
+
],
|
| 623 |
+
"engines": {
|
| 624 |
+
"node": ">=18"
|
| 625 |
+
}
|
| 626 |
+
},
|
| 627 |
+
"node_modules/@esbuild/linux-arm": {
|
| 628 |
+
"version": "0.25.2",
|
| 629 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz",
|
| 630 |
+
"integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==",
|
| 631 |
+
"cpu": [
|
| 632 |
+
"arm"
|
| 633 |
+
],
|
| 634 |
+
"dev": true,
|
| 635 |
+
"license": "MIT",
|
| 636 |
+
"optional": true,
|
| 637 |
+
"os": [
|
| 638 |
+
"linux"
|
| 639 |
+
],
|
| 640 |
+
"engines": {
|
| 641 |
+
"node": ">=18"
|
| 642 |
+
}
|
| 643 |
+
},
|
| 644 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 645 |
+
"version": "0.25.2",
|
| 646 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz",
|
| 647 |
+
"integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==",
|
| 648 |
+
"cpu": [
|
| 649 |
+
"arm64"
|
| 650 |
+
],
|
| 651 |
+
"dev": true,
|
| 652 |
+
"license": "MIT",
|
| 653 |
+
"optional": true,
|
| 654 |
+
"os": [
|
| 655 |
+
"linux"
|
| 656 |
+
],
|
| 657 |
+
"engines": {
|
| 658 |
+
"node": ">=18"
|
| 659 |
+
}
|
| 660 |
+
},
|
| 661 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 662 |
+
"version": "0.25.2",
|
| 663 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz",
|
| 664 |
+
"integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==",
|
| 665 |
+
"cpu": [
|
| 666 |
+
"ia32"
|
| 667 |
+
],
|
| 668 |
+
"dev": true,
|
| 669 |
+
"license": "MIT",
|
| 670 |
+
"optional": true,
|
| 671 |
+
"os": [
|
| 672 |
+
"linux"
|
| 673 |
+
],
|
| 674 |
+
"engines": {
|
| 675 |
+
"node": ">=18"
|
| 676 |
+
}
|
| 677 |
+
},
|
| 678 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 679 |
+
"version": "0.25.2",
|
| 680 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz",
|
| 681 |
+
"integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==",
|
| 682 |
+
"cpu": [
|
| 683 |
+
"loong64"
|
| 684 |
+
],
|
| 685 |
+
"dev": true,
|
| 686 |
+
"license": "MIT",
|
| 687 |
+
"optional": true,
|
| 688 |
+
"os": [
|
| 689 |
+
"linux"
|
| 690 |
+
],
|
| 691 |
+
"engines": {
|
| 692 |
+
"node": ">=18"
|
| 693 |
+
}
|
| 694 |
+
},
|
| 695 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 696 |
+
"version": "0.25.2",
|
| 697 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz",
|
| 698 |
+
"integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==",
|
| 699 |
+
"cpu": [
|
| 700 |
+
"mips64el"
|
| 701 |
+
],
|
| 702 |
+
"dev": true,
|
| 703 |
+
"license": "MIT",
|
| 704 |
+
"optional": true,
|
| 705 |
+
"os": [
|
| 706 |
+
"linux"
|
| 707 |
+
],
|
| 708 |
+
"engines": {
|
| 709 |
+
"node": ">=18"
|
| 710 |
+
}
|
| 711 |
+
},
|
| 712 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 713 |
+
"version": "0.25.2",
|
| 714 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz",
|
| 715 |
+
"integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==",
|
| 716 |
+
"cpu": [
|
| 717 |
+
"ppc64"
|
| 718 |
+
],
|
| 719 |
+
"dev": true,
|
| 720 |
+
"license": "MIT",
|
| 721 |
+
"optional": true,
|
| 722 |
+
"os": [
|
| 723 |
+
"linux"
|
| 724 |
+
],
|
| 725 |
+
"engines": {
|
| 726 |
+
"node": ">=18"
|
| 727 |
+
}
|
| 728 |
+
},
|
| 729 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 730 |
+
"version": "0.25.2",
|
| 731 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz",
|
| 732 |
+
"integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==",
|
| 733 |
+
"cpu": [
|
| 734 |
+
"riscv64"
|
| 735 |
+
],
|
| 736 |
+
"dev": true,
|
| 737 |
+
"license": "MIT",
|
| 738 |
+
"optional": true,
|
| 739 |
+
"os": [
|
| 740 |
+
"linux"
|
| 741 |
+
],
|
| 742 |
+
"engines": {
|
| 743 |
+
"node": ">=18"
|
| 744 |
+
}
|
| 745 |
+
},
|
| 746 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 747 |
+
"version": "0.25.2",
|
| 748 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz",
|
| 749 |
+
"integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==",
|
| 750 |
+
"cpu": [
|
| 751 |
+
"s390x"
|
| 752 |
+
],
|
| 753 |
+
"dev": true,
|
| 754 |
+
"license": "MIT",
|
| 755 |
+
"optional": true,
|
| 756 |
+
"os": [
|
| 757 |
+
"linux"
|
| 758 |
+
],
|
| 759 |
+
"engines": {
|
| 760 |
+
"node": ">=18"
|
| 761 |
+
}
|
| 762 |
+
},
|
| 763 |
+
"node_modules/@esbuild/linux-x64": {
|
| 764 |
+
"version": "0.25.2",
|
| 765 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz",
|
| 766 |
+
"integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==",
|
| 767 |
+
"cpu": [
|
| 768 |
+
"x64"
|
| 769 |
+
],
|
| 770 |
+
"dev": true,
|
| 771 |
+
"license": "MIT",
|
| 772 |
+
"optional": true,
|
| 773 |
+
"os": [
|
| 774 |
+
"linux"
|
| 775 |
+
],
|
| 776 |
+
"engines": {
|
| 777 |
+
"node": ">=18"
|
| 778 |
+
}
|
| 779 |
+
},
|
| 780 |
+
"node_modules/@esbuild/netbsd-arm64": {
|
| 781 |
+
"version": "0.25.2",
|
| 782 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz",
|
| 783 |
+
"integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==",
|
| 784 |
+
"cpu": [
|
| 785 |
+
"arm64"
|
| 786 |
+
],
|
| 787 |
+
"dev": true,
|
| 788 |
+
"license": "MIT",
|
| 789 |
+
"optional": true,
|
| 790 |
+
"os": [
|
| 791 |
+
"netbsd"
|
| 792 |
+
],
|
| 793 |
+
"engines": {
|
| 794 |
+
"node": ">=18"
|
| 795 |
+
}
|
| 796 |
+
},
|
| 797 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 798 |
+
"version": "0.25.2",
|
| 799 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz",
|
| 800 |
+
"integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==",
|
| 801 |
+
"cpu": [
|
| 802 |
+
"x64"
|
| 803 |
+
],
|
| 804 |
+
"dev": true,
|
| 805 |
+
"license": "MIT",
|
| 806 |
+
"optional": true,
|
| 807 |
+
"os": [
|
| 808 |
+
"netbsd"
|
| 809 |
+
],
|
| 810 |
+
"engines": {
|
| 811 |
+
"node": ">=18"
|
| 812 |
+
}
|
| 813 |
+
},
|
| 814 |
+
"node_modules/@esbuild/openbsd-arm64": {
|
| 815 |
+
"version": "0.25.2",
|
| 816 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz",
|
| 817 |
+
"integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==",
|
| 818 |
+
"cpu": [
|
| 819 |
+
"arm64"
|
| 820 |
+
],
|
| 821 |
+
"dev": true,
|
| 822 |
+
"license": "MIT",
|
| 823 |
+
"optional": true,
|
| 824 |
+
"os": [
|
| 825 |
+
"openbsd"
|
| 826 |
+
],
|
| 827 |
+
"engines": {
|
| 828 |
+
"node": ">=18"
|
| 829 |
+
}
|
| 830 |
+
},
|
| 831 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 832 |
+
"version": "0.25.2",
|
| 833 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz",
|
| 834 |
+
"integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==",
|
| 835 |
+
"cpu": [
|
| 836 |
+
"x64"
|
| 837 |
+
],
|
| 838 |
+
"dev": true,
|
| 839 |
+
"license": "MIT",
|
| 840 |
+
"optional": true,
|
| 841 |
+
"os": [
|
| 842 |
+
"openbsd"
|
| 843 |
+
],
|
| 844 |
+
"engines": {
|
| 845 |
+
"node": ">=18"
|
| 846 |
+
}
|
| 847 |
+
},
|
| 848 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 849 |
+
"version": "0.25.2",
|
| 850 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz",
|
| 851 |
+
"integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==",
|
| 852 |
+
"cpu": [
|
| 853 |
+
"x64"
|
| 854 |
+
],
|
| 855 |
+
"dev": true,
|
| 856 |
+
"license": "MIT",
|
| 857 |
+
"optional": true,
|
| 858 |
+
"os": [
|
| 859 |
+
"sunos"
|
| 860 |
+
],
|
| 861 |
+
"engines": {
|
| 862 |
+
"node": ">=18"
|
| 863 |
+
}
|
| 864 |
+
},
|
| 865 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 866 |
+
"version": "0.25.2",
|
| 867 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz",
|
| 868 |
+
"integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==",
|
| 869 |
+
"cpu": [
|
| 870 |
+
"arm64"
|
| 871 |
+
],
|
| 872 |
+
"dev": true,
|
| 873 |
+
"license": "MIT",
|
| 874 |
+
"optional": true,
|
| 875 |
+
"os": [
|
| 876 |
+
"win32"
|
| 877 |
+
],
|
| 878 |
+
"engines": {
|
| 879 |
+
"node": ">=18"
|
| 880 |
+
}
|
| 881 |
+
},
|
| 882 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 883 |
+
"version": "0.25.2",
|
| 884 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz",
|
| 885 |
+
"integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==",
|
| 886 |
+
"cpu": [
|
| 887 |
+
"ia32"
|
| 888 |
+
],
|
| 889 |
+
"dev": true,
|
| 890 |
+
"license": "MIT",
|
| 891 |
+
"optional": true,
|
| 892 |
+
"os": [
|
| 893 |
+
"win32"
|
| 894 |
+
],
|
| 895 |
+
"engines": {
|
| 896 |
+
"node": ">=18"
|
| 897 |
+
}
|
| 898 |
+
},
|
| 899 |
+
"node_modules/@esbuild/win32-x64": {
|
| 900 |
+
"version": "0.25.2",
|
| 901 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz",
|
| 902 |
+
"integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==",
|
| 903 |
+
"cpu": [
|
| 904 |
+
"x64"
|
| 905 |
+
],
|
| 906 |
+
"dev": true,
|
| 907 |
+
"license": "MIT",
|
| 908 |
+
"optional": true,
|
| 909 |
+
"os": [
|
| 910 |
+
"win32"
|
| 911 |
+
],
|
| 912 |
+
"engines": {
|
| 913 |
+
"node": ">=18"
|
| 914 |
+
}
|
| 915 |
+
},
|
| 916 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 917 |
+
"version": "0.3.8",
|
| 918 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
| 919 |
+
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
| 920 |
+
"dev": true,
|
| 921 |
+
"license": "MIT",
|
| 922 |
+
"dependencies": {
|
| 923 |
+
"@jridgewell/set-array": "^1.2.1",
|
| 924 |
+
"@jridgewell/sourcemap-codec": "^1.4.10",
|
| 925 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 926 |
+
},
|
| 927 |
+
"engines": {
|
| 928 |
+
"node": ">=6.0.0"
|
| 929 |
+
}
|
| 930 |
+
},
|
| 931 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 932 |
+
"version": "3.1.2",
|
| 933 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 934 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 935 |
+
"dev": true,
|
| 936 |
+
"license": "MIT",
|
| 937 |
+
"engines": {
|
| 938 |
+
"node": ">=6.0.0"
|
| 939 |
+
}
|
| 940 |
+
},
|
| 941 |
+
"node_modules/@jridgewell/set-array": {
|
| 942 |
+
"version": "1.2.1",
|
| 943 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
| 944 |
+
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
| 945 |
+
"dev": true,
|
| 946 |
+
"license": "MIT",
|
| 947 |
+
"engines": {
|
| 948 |
+
"node": ">=6.0.0"
|
| 949 |
+
}
|
| 950 |
+
},
|
| 951 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 952 |
+
"version": "1.5.0",
|
| 953 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
| 954 |
+
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
| 955 |
+
"license": "MIT"
|
| 956 |
+
},
|
| 957 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 958 |
+
"version": "0.3.25",
|
| 959 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
| 960 |
+
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
| 961 |
+
"dev": true,
|
| 962 |
+
"license": "MIT",
|
| 963 |
+
"dependencies": {
|
| 964 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 965 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 966 |
+
}
|
| 967 |
+
},
|
| 968 |
+
"node_modules/@polka/url": {
|
| 969 |
+
"version": "1.0.0-next.28",
|
| 970 |
+
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz",
|
| 971 |
+
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
|
| 972 |
+
"dev": true,
|
| 973 |
+
"license": "MIT"
|
| 974 |
+
},
|
| 975 |
+
"node_modules/@rollup/pluginutils": {
|
| 976 |
+
"version": "5.1.4",
|
| 977 |
+
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
| 978 |
+
"integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
|
| 979 |
+
"dev": true,
|
| 980 |
+
"license": "MIT",
|
| 981 |
+
"dependencies": {
|
| 982 |
+
"@types/estree": "^1.0.0",
|
| 983 |
+
"estree-walker": "^2.0.2",
|
| 984 |
+
"picomatch": "^4.0.2"
|
| 985 |
+
},
|
| 986 |
+
"engines": {
|
| 987 |
+
"node": ">=14.0.0"
|
| 988 |
+
},
|
| 989 |
+
"peerDependencies": {
|
| 990 |
+
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
| 991 |
+
},
|
| 992 |
+
"peerDependenciesMeta": {
|
| 993 |
+
"rollup": {
|
| 994 |
+
"optional": true
|
| 995 |
+
}
|
| 996 |
+
}
|
| 997 |
+
},
|
| 998 |
+
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 999 |
+
"version": "4.39.0",
|
| 1000 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.39.0.tgz",
|
| 1001 |
+
"integrity": "sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==",
|
| 1002 |
+
"cpu": [
|
| 1003 |
+
"arm"
|
| 1004 |
+
],
|
| 1005 |
+
"dev": true,
|
| 1006 |
+
"license": "MIT",
|
| 1007 |
+
"optional": true,
|
| 1008 |
+
"os": [
|
| 1009 |
+
"android"
|
| 1010 |
+
]
|
| 1011 |
+
},
|
| 1012 |
+
"node_modules/@rollup/rollup-android-arm64": {
|
| 1013 |
+
"version": "4.39.0",
|
| 1014 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.39.0.tgz",
|
| 1015 |
+
"integrity": "sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==",
|
| 1016 |
+
"cpu": [
|
| 1017 |
+
"arm64"
|
| 1018 |
+
],
|
| 1019 |
+
"dev": true,
|
| 1020 |
+
"license": "MIT",
|
| 1021 |
+
"optional": true,
|
| 1022 |
+
"os": [
|
| 1023 |
+
"android"
|
| 1024 |
+
]
|
| 1025 |
+
},
|
| 1026 |
+
"node_modules/@rollup/rollup-darwin-arm64": {
|
| 1027 |
+
"version": "4.39.0",
|
| 1028 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.39.0.tgz",
|
| 1029 |
+
"integrity": "sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==",
|
| 1030 |
+
"cpu": [
|
| 1031 |
+
"arm64"
|
| 1032 |
+
],
|
| 1033 |
+
"dev": true,
|
| 1034 |
+
"license": "MIT",
|
| 1035 |
+
"optional": true,
|
| 1036 |
+
"os": [
|
| 1037 |
+
"darwin"
|
| 1038 |
+
]
|
| 1039 |
+
},
|
| 1040 |
+
"node_modules/@rollup/rollup-darwin-x64": {
|
| 1041 |
+
"version": "4.39.0",
|
| 1042 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.39.0.tgz",
|
| 1043 |
+
"integrity": "sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==",
|
| 1044 |
+
"cpu": [
|
| 1045 |
+
"x64"
|
| 1046 |
+
],
|
| 1047 |
+
"dev": true,
|
| 1048 |
+
"license": "MIT",
|
| 1049 |
+
"optional": true,
|
| 1050 |
+
"os": [
|
| 1051 |
+
"darwin"
|
| 1052 |
+
]
|
| 1053 |
+
},
|
| 1054 |
+
"node_modules/@rollup/rollup-freebsd-arm64": {
|
| 1055 |
+
"version": "4.39.0",
|
| 1056 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.39.0.tgz",
|
| 1057 |
+
"integrity": "sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==",
|
| 1058 |
+
"cpu": [
|
| 1059 |
+
"arm64"
|
| 1060 |
+
],
|
| 1061 |
+
"dev": true,
|
| 1062 |
+
"license": "MIT",
|
| 1063 |
+
"optional": true,
|
| 1064 |
+
"os": [
|
| 1065 |
+
"freebsd"
|
| 1066 |
+
]
|
| 1067 |
+
},
|
| 1068 |
+
"node_modules/@rollup/rollup-freebsd-x64": {
|
| 1069 |
+
"version": "4.39.0",
|
| 1070 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.39.0.tgz",
|
| 1071 |
+
"integrity": "sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==",
|
| 1072 |
+
"cpu": [
|
| 1073 |
+
"x64"
|
| 1074 |
+
],
|
| 1075 |
+
"dev": true,
|
| 1076 |
+
"license": "MIT",
|
| 1077 |
+
"optional": true,
|
| 1078 |
+
"os": [
|
| 1079 |
+
"freebsd"
|
| 1080 |
+
]
|
| 1081 |
+
},
|
| 1082 |
+
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
| 1083 |
+
"version": "4.39.0",
|
| 1084 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.39.0.tgz",
|
| 1085 |
+
"integrity": "sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==",
|
| 1086 |
+
"cpu": [
|
| 1087 |
+
"arm"
|
| 1088 |
+
],
|
| 1089 |
+
"dev": true,
|
| 1090 |
+
"license": "MIT",
|
| 1091 |
+
"optional": true,
|
| 1092 |
+
"os": [
|
| 1093 |
+
"linux"
|
| 1094 |
+
]
|
| 1095 |
+
},
|
| 1096 |
+
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
| 1097 |
+
"version": "4.39.0",
|
| 1098 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.39.0.tgz",
|
| 1099 |
+
"integrity": "sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==",
|
| 1100 |
+
"cpu": [
|
| 1101 |
+
"arm"
|
| 1102 |
+
],
|
| 1103 |
+
"dev": true,
|
| 1104 |
+
"license": "MIT",
|
| 1105 |
+
"optional": true,
|
| 1106 |
+
"os": [
|
| 1107 |
+
"linux"
|
| 1108 |
+
]
|
| 1109 |
+
},
|
| 1110 |
+
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
| 1111 |
+
"version": "4.39.0",
|
| 1112 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.39.0.tgz",
|
| 1113 |
+
"integrity": "sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==",
|
| 1114 |
+
"cpu": [
|
| 1115 |
+
"arm64"
|
| 1116 |
+
],
|
| 1117 |
+
"dev": true,
|
| 1118 |
+
"license": "MIT",
|
| 1119 |
+
"optional": true,
|
| 1120 |
+
"os": [
|
| 1121 |
+
"linux"
|
| 1122 |
+
]
|
| 1123 |
+
},
|
| 1124 |
+
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
| 1125 |
+
"version": "4.39.0",
|
| 1126 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.39.0.tgz",
|
| 1127 |
+
"integrity": "sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==",
|
| 1128 |
+
"cpu": [
|
| 1129 |
+
"arm64"
|
| 1130 |
+
],
|
| 1131 |
+
"dev": true,
|
| 1132 |
+
"license": "MIT",
|
| 1133 |
+
"optional": true,
|
| 1134 |
+
"os": [
|
| 1135 |
+
"linux"
|
| 1136 |
+
]
|
| 1137 |
+
},
|
| 1138 |
+
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
| 1139 |
+
"version": "4.39.0",
|
| 1140 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.39.0.tgz",
|
| 1141 |
+
"integrity": "sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==",
|
| 1142 |
+
"cpu": [
|
| 1143 |
+
"loong64"
|
| 1144 |
+
],
|
| 1145 |
+
"dev": true,
|
| 1146 |
+
"license": "MIT",
|
| 1147 |
+
"optional": true,
|
| 1148 |
+
"os": [
|
| 1149 |
+
"linux"
|
| 1150 |
+
]
|
| 1151 |
+
},
|
| 1152 |
+
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
| 1153 |
+
"version": "4.39.0",
|
| 1154 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.39.0.tgz",
|
| 1155 |
+
"integrity": "sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==",
|
| 1156 |
+
"cpu": [
|
| 1157 |
+
"ppc64"
|
| 1158 |
+
],
|
| 1159 |
+
"dev": true,
|
| 1160 |
+
"license": "MIT",
|
| 1161 |
+
"optional": true,
|
| 1162 |
+
"os": [
|
| 1163 |
+
"linux"
|
| 1164 |
+
]
|
| 1165 |
+
},
|
| 1166 |
+
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
| 1167 |
+
"version": "4.39.0",
|
| 1168 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.39.0.tgz",
|
| 1169 |
+
"integrity": "sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==",
|
| 1170 |
+
"cpu": [
|
| 1171 |
+
"riscv64"
|
| 1172 |
+
],
|
| 1173 |
+
"dev": true,
|
| 1174 |
+
"license": "MIT",
|
| 1175 |
+
"optional": true,
|
| 1176 |
+
"os": [
|
| 1177 |
+
"linux"
|
| 1178 |
+
]
|
| 1179 |
+
},
|
| 1180 |
+
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
| 1181 |
+
"version": "4.39.0",
|
| 1182 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.39.0.tgz",
|
| 1183 |
+
"integrity": "sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==",
|
| 1184 |
+
"cpu": [
|
| 1185 |
+
"riscv64"
|
| 1186 |
+
],
|
| 1187 |
+
"dev": true,
|
| 1188 |
+
"license": "MIT",
|
| 1189 |
+
"optional": true,
|
| 1190 |
+
"os": [
|
| 1191 |
+
"linux"
|
| 1192 |
+
]
|
| 1193 |
+
},
|
| 1194 |
+
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
| 1195 |
+
"version": "4.39.0",
|
| 1196 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.39.0.tgz",
|
| 1197 |
+
"integrity": "sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==",
|
| 1198 |
+
"cpu": [
|
| 1199 |
+
"s390x"
|
| 1200 |
+
],
|
| 1201 |
+
"dev": true,
|
| 1202 |
+
"license": "MIT",
|
| 1203 |
+
"optional": true,
|
| 1204 |
+
"os": [
|
| 1205 |
+
"linux"
|
| 1206 |
+
]
|
| 1207 |
+
},
|
| 1208 |
+
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
| 1209 |
+
"version": "4.39.0",
|
| 1210 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.39.0.tgz",
|
| 1211 |
+
"integrity": "sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==",
|
| 1212 |
+
"cpu": [
|
| 1213 |
+
"x64"
|
| 1214 |
+
],
|
| 1215 |
+
"dev": true,
|
| 1216 |
+
"license": "MIT",
|
| 1217 |
+
"optional": true,
|
| 1218 |
+
"os": [
|
| 1219 |
+
"linux"
|
| 1220 |
+
]
|
| 1221 |
+
},
|
| 1222 |
+
"node_modules/@rollup/rollup-linux-x64-musl": {
|
| 1223 |
+
"version": "4.39.0",
|
| 1224 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.39.0.tgz",
|
| 1225 |
+
"integrity": "sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==",
|
| 1226 |
+
"cpu": [
|
| 1227 |
+
"x64"
|
| 1228 |
+
],
|
| 1229 |
+
"dev": true,
|
| 1230 |
+
"license": "MIT",
|
| 1231 |
+
"optional": true,
|
| 1232 |
+
"os": [
|
| 1233 |
+
"linux"
|
| 1234 |
+
]
|
| 1235 |
+
},
|
| 1236 |
+
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
| 1237 |
+
"version": "4.39.0",
|
| 1238 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.39.0.tgz",
|
| 1239 |
+
"integrity": "sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==",
|
| 1240 |
+
"cpu": [
|
| 1241 |
+
"arm64"
|
| 1242 |
+
],
|
| 1243 |
+
"dev": true,
|
| 1244 |
+
"license": "MIT",
|
| 1245 |
+
"optional": true,
|
| 1246 |
+
"os": [
|
| 1247 |
+
"win32"
|
| 1248 |
+
]
|
| 1249 |
+
},
|
| 1250 |
+
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
| 1251 |
+
"version": "4.39.0",
|
| 1252 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.39.0.tgz",
|
| 1253 |
+
"integrity": "sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==",
|
| 1254 |
+
"cpu": [
|
| 1255 |
+
"ia32"
|
| 1256 |
+
],
|
| 1257 |
+
"dev": true,
|
| 1258 |
+
"license": "MIT",
|
| 1259 |
+
"optional": true,
|
| 1260 |
+
"os": [
|
| 1261 |
+
"win32"
|
| 1262 |
+
]
|
| 1263 |
+
},
|
| 1264 |
+
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
| 1265 |
+
"version": "4.39.0",
|
| 1266 |
+
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.39.0.tgz",
|
| 1267 |
+
"integrity": "sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==",
|
| 1268 |
+
"cpu": [
|
| 1269 |
+
"x64"
|
| 1270 |
+
],
|
| 1271 |
+
"dev": true,
|
| 1272 |
+
"license": "MIT",
|
| 1273 |
+
"optional": true,
|
| 1274 |
+
"os": [
|
| 1275 |
+
"win32"
|
| 1276 |
+
]
|
| 1277 |
+
},
|
| 1278 |
+
"node_modules/@sec-ant/readable-stream": {
|
| 1279 |
+
"version": "0.4.1",
|
| 1280 |
+
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
| 1281 |
+
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
|
| 1282 |
+
"dev": true,
|
| 1283 |
+
"license": "MIT"
|
| 1284 |
+
},
|
| 1285 |
+
"node_modules/@sindresorhus/merge-streams": {
|
| 1286 |
+
"version": "4.0.0",
|
| 1287 |
+
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
|
| 1288 |
+
"integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
|
| 1289 |
+
"dev": true,
|
| 1290 |
+
"license": "MIT",
|
| 1291 |
+
"engines": {
|
| 1292 |
+
"node": ">=18"
|
| 1293 |
+
},
|
| 1294 |
+
"funding": {
|
| 1295 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1296 |
+
}
|
| 1297 |
+
},
|
| 1298 |
+
"node_modules/@types/estree": {
|
| 1299 |
+
"version": "1.0.7",
|
| 1300 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
| 1301 |
+
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
| 1302 |
+
"dev": true,
|
| 1303 |
+
"license": "MIT"
|
| 1304 |
+
},
|
| 1305 |
+
"node_modules/@vitejs/plugin-vue": {
|
| 1306 |
+
"version": "5.2.3",
|
| 1307 |
+
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
|
| 1308 |
+
"integrity": "sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==",
|
| 1309 |
+
"dev": true,
|
| 1310 |
+
"license": "MIT",
|
| 1311 |
+
"engines": {
|
| 1312 |
+
"node": "^18.0.0 || >=20.0.0"
|
| 1313 |
+
},
|
| 1314 |
+
"peerDependencies": {
|
| 1315 |
+
"vite": "^5.0.0 || ^6.0.0",
|
| 1316 |
+
"vue": "^3.2.25"
|
| 1317 |
+
}
|
| 1318 |
+
},
|
| 1319 |
+
"node_modules/@vue/babel-helper-vue-transform-on": {
|
| 1320 |
+
"version": "1.4.0",
|
| 1321 |
+
"resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.4.0.tgz",
|
| 1322 |
+
"integrity": "sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==",
|
| 1323 |
+
"dev": true,
|
| 1324 |
+
"license": "MIT"
|
| 1325 |
+
},
|
| 1326 |
+
"node_modules/@vue/babel-plugin-jsx": {
|
| 1327 |
+
"version": "1.4.0",
|
| 1328 |
+
"resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.4.0.tgz",
|
| 1329 |
+
"integrity": "sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA==",
|
| 1330 |
+
"dev": true,
|
| 1331 |
+
"license": "MIT",
|
| 1332 |
+
"dependencies": {
|
| 1333 |
+
"@babel/helper-module-imports": "^7.25.9",
|
| 1334 |
+
"@babel/helper-plugin-utils": "^7.26.5",
|
| 1335 |
+
"@babel/plugin-syntax-jsx": "^7.25.9",
|
| 1336 |
+
"@babel/template": "^7.26.9",
|
| 1337 |
+
"@babel/traverse": "^7.26.9",
|
| 1338 |
+
"@babel/types": "^7.26.9",
|
| 1339 |
+
"@vue/babel-helper-vue-transform-on": "1.4.0",
|
| 1340 |
+
"@vue/babel-plugin-resolve-type": "1.4.0",
|
| 1341 |
+
"@vue/shared": "^3.5.13"
|
| 1342 |
+
},
|
| 1343 |
+
"peerDependencies": {
|
| 1344 |
+
"@babel/core": "^7.0.0-0"
|
| 1345 |
+
},
|
| 1346 |
+
"peerDependenciesMeta": {
|
| 1347 |
+
"@babel/core": {
|
| 1348 |
+
"optional": true
|
| 1349 |
+
}
|
| 1350 |
+
}
|
| 1351 |
+
},
|
| 1352 |
+
"node_modules/@vue/babel-plugin-resolve-type": {
|
| 1353 |
+
"version": "1.4.0",
|
| 1354 |
+
"resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.4.0.tgz",
|
| 1355 |
+
"integrity": "sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ==",
|
| 1356 |
+
"dev": true,
|
| 1357 |
+
"license": "MIT",
|
| 1358 |
+
"dependencies": {
|
| 1359 |
+
"@babel/code-frame": "^7.26.2",
|
| 1360 |
+
"@babel/helper-module-imports": "^7.25.9",
|
| 1361 |
+
"@babel/helper-plugin-utils": "^7.26.5",
|
| 1362 |
+
"@babel/parser": "^7.26.9",
|
| 1363 |
+
"@vue/compiler-sfc": "^3.5.13"
|
| 1364 |
+
},
|
| 1365 |
+
"funding": {
|
| 1366 |
+
"url": "https://github.com/sponsors/sxzz"
|
| 1367 |
+
},
|
| 1368 |
+
"peerDependencies": {
|
| 1369 |
+
"@babel/core": "^7.0.0-0"
|
| 1370 |
+
}
|
| 1371 |
+
},
|
| 1372 |
+
"node_modules/@vue/compiler-core": {
|
| 1373 |
+
"version": "3.5.13",
|
| 1374 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
|
| 1375 |
+
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
|
| 1376 |
+
"license": "MIT",
|
| 1377 |
+
"dependencies": {
|
| 1378 |
+
"@babel/parser": "^7.25.3",
|
| 1379 |
+
"@vue/shared": "3.5.13",
|
| 1380 |
+
"entities": "^4.5.0",
|
| 1381 |
+
"estree-walker": "^2.0.2",
|
| 1382 |
+
"source-map-js": "^1.2.0"
|
| 1383 |
+
}
|
| 1384 |
+
},
|
| 1385 |
+
"node_modules/@vue/compiler-dom": {
|
| 1386 |
+
"version": "3.5.13",
|
| 1387 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
|
| 1388 |
+
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
|
| 1389 |
+
"license": "MIT",
|
| 1390 |
+
"dependencies": {
|
| 1391 |
+
"@vue/compiler-core": "3.5.13",
|
| 1392 |
+
"@vue/shared": "3.5.13"
|
| 1393 |
+
}
|
| 1394 |
+
},
|
| 1395 |
+
"node_modules/@vue/compiler-sfc": {
|
| 1396 |
+
"version": "3.5.13",
|
| 1397 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
|
| 1398 |
+
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
|
| 1399 |
+
"license": "MIT",
|
| 1400 |
+
"dependencies": {
|
| 1401 |
+
"@babel/parser": "^7.25.3",
|
| 1402 |
+
"@vue/compiler-core": "3.5.13",
|
| 1403 |
+
"@vue/compiler-dom": "3.5.13",
|
| 1404 |
+
"@vue/compiler-ssr": "3.5.13",
|
| 1405 |
+
"@vue/shared": "3.5.13",
|
| 1406 |
+
"estree-walker": "^2.0.2",
|
| 1407 |
+
"magic-string": "^0.30.11",
|
| 1408 |
+
"postcss": "^8.4.48",
|
| 1409 |
+
"source-map-js": "^1.2.0"
|
| 1410 |
+
}
|
| 1411 |
+
},
|
| 1412 |
+
"node_modules/@vue/compiler-ssr": {
|
| 1413 |
+
"version": "3.5.13",
|
| 1414 |
+
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
|
| 1415 |
+
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
|
| 1416 |
+
"license": "MIT",
|
| 1417 |
+
"dependencies": {
|
| 1418 |
+
"@vue/compiler-dom": "3.5.13",
|
| 1419 |
+
"@vue/shared": "3.5.13"
|
| 1420 |
+
}
|
| 1421 |
+
},
|
| 1422 |
+
"node_modules/@vue/devtools-api": {
|
| 1423 |
+
"version": "7.7.2",
|
| 1424 |
+
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.2.tgz",
|
| 1425 |
+
"integrity": "sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==",
|
| 1426 |
+
"license": "MIT",
|
| 1427 |
+
"dependencies": {
|
| 1428 |
+
"@vue/devtools-kit": "^7.7.2"
|
| 1429 |
+
}
|
| 1430 |
+
},
|
| 1431 |
+
"node_modules/@vue/devtools-core": {
|
| 1432 |
+
"version": "7.7.2",
|
| 1433 |
+
"resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.7.2.tgz",
|
| 1434 |
+
"integrity": "sha512-lexREWj1lKi91Tblr38ntSsy6CvI8ba7u+jmwh2yruib/ltLUcsIzEjCnrkh1yYGGIKXbAuYV2tOG10fGDB9OQ==",
|
| 1435 |
+
"dev": true,
|
| 1436 |
+
"license": "MIT",
|
| 1437 |
+
"dependencies": {
|
| 1438 |
+
"@vue/devtools-kit": "^7.7.2",
|
| 1439 |
+
"@vue/devtools-shared": "^7.7.2",
|
| 1440 |
+
"mitt": "^3.0.1",
|
| 1441 |
+
"nanoid": "^5.0.9",
|
| 1442 |
+
"pathe": "^2.0.2",
|
| 1443 |
+
"vite-hot-client": "^0.2.4"
|
| 1444 |
+
},
|
| 1445 |
+
"peerDependencies": {
|
| 1446 |
+
"vue": "^3.0.0"
|
| 1447 |
+
}
|
| 1448 |
+
},
|
| 1449 |
+
"node_modules/@vue/devtools-core/node_modules/nanoid": {
|
| 1450 |
+
"version": "5.1.5",
|
| 1451 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.5.tgz",
|
| 1452 |
+
"integrity": "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==",
|
| 1453 |
+
"dev": true,
|
| 1454 |
+
"funding": [
|
| 1455 |
+
{
|
| 1456 |
+
"type": "github",
|
| 1457 |
+
"url": "https://github.com/sponsors/ai"
|
| 1458 |
+
}
|
| 1459 |
+
],
|
| 1460 |
+
"license": "MIT",
|
| 1461 |
+
"bin": {
|
| 1462 |
+
"nanoid": "bin/nanoid.js"
|
| 1463 |
+
},
|
| 1464 |
+
"engines": {
|
| 1465 |
+
"node": "^18 || >=20"
|
| 1466 |
+
}
|
| 1467 |
+
},
|
| 1468 |
+
"node_modules/@vue/devtools-kit": {
|
| 1469 |
+
"version": "7.7.2",
|
| 1470 |
+
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.2.tgz",
|
| 1471 |
+
"integrity": "sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==",
|
| 1472 |
+
"license": "MIT",
|
| 1473 |
+
"dependencies": {
|
| 1474 |
+
"@vue/devtools-shared": "^7.7.2",
|
| 1475 |
+
"birpc": "^0.2.19",
|
| 1476 |
+
"hookable": "^5.5.3",
|
| 1477 |
+
"mitt": "^3.0.1",
|
| 1478 |
+
"perfect-debounce": "^1.0.0",
|
| 1479 |
+
"speakingurl": "^14.0.1",
|
| 1480 |
+
"superjson": "^2.2.1"
|
| 1481 |
+
}
|
| 1482 |
+
},
|
| 1483 |
+
"node_modules/@vue/devtools-shared": {
|
| 1484 |
+
"version": "7.7.2",
|
| 1485 |
+
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.2.tgz",
|
| 1486 |
+
"integrity": "sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==",
|
| 1487 |
+
"license": "MIT",
|
| 1488 |
+
"dependencies": {
|
| 1489 |
+
"rfdc": "^1.4.1"
|
| 1490 |
+
}
|
| 1491 |
+
},
|
| 1492 |
+
"node_modules/@vue/reactivity": {
|
| 1493 |
+
"version": "3.5.13",
|
| 1494 |
+
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
| 1495 |
+
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
|
| 1496 |
+
"license": "MIT",
|
| 1497 |
+
"dependencies": {
|
| 1498 |
+
"@vue/shared": "3.5.13"
|
| 1499 |
+
}
|
| 1500 |
+
},
|
| 1501 |
+
"node_modules/@vue/runtime-core": {
|
| 1502 |
+
"version": "3.5.13",
|
| 1503 |
+
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
|
| 1504 |
+
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
|
| 1505 |
+
"license": "MIT",
|
| 1506 |
+
"dependencies": {
|
| 1507 |
+
"@vue/reactivity": "3.5.13",
|
| 1508 |
+
"@vue/shared": "3.5.13"
|
| 1509 |
+
}
|
| 1510 |
+
},
|
| 1511 |
+
"node_modules/@vue/runtime-dom": {
|
| 1512 |
+
"version": "3.5.13",
|
| 1513 |
+
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
|
| 1514 |
+
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
|
| 1515 |
+
"license": "MIT",
|
| 1516 |
+
"dependencies": {
|
| 1517 |
+
"@vue/reactivity": "3.5.13",
|
| 1518 |
+
"@vue/runtime-core": "3.5.13",
|
| 1519 |
+
"@vue/shared": "3.5.13",
|
| 1520 |
+
"csstype": "^3.1.3"
|
| 1521 |
+
}
|
| 1522 |
+
},
|
| 1523 |
+
"node_modules/@vue/server-renderer": {
|
| 1524 |
+
"version": "3.5.13",
|
| 1525 |
+
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
|
| 1526 |
+
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
|
| 1527 |
+
"license": "MIT",
|
| 1528 |
+
"dependencies": {
|
| 1529 |
+
"@vue/compiler-ssr": "3.5.13",
|
| 1530 |
+
"@vue/shared": "3.5.13"
|
| 1531 |
+
},
|
| 1532 |
+
"peerDependencies": {
|
| 1533 |
+
"vue": "3.5.13"
|
| 1534 |
+
}
|
| 1535 |
+
},
|
| 1536 |
+
"node_modules/@vue/shared": {
|
| 1537 |
+
"version": "3.5.13",
|
| 1538 |
+
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
|
| 1539 |
+
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
| 1540 |
+
"license": "MIT"
|
| 1541 |
+
},
|
| 1542 |
+
"node_modules/birpc": {
|
| 1543 |
+
"version": "0.2.19",
|
| 1544 |
+
"resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.19.tgz",
|
| 1545 |
+
"integrity": "sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==",
|
| 1546 |
+
"license": "MIT",
|
| 1547 |
+
"funding": {
|
| 1548 |
+
"url": "https://github.com/sponsors/antfu"
|
| 1549 |
+
}
|
| 1550 |
+
},
|
| 1551 |
+
"node_modules/browserslist": {
|
| 1552 |
+
"version": "4.24.4",
|
| 1553 |
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
|
| 1554 |
+
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
|
| 1555 |
+
"dev": true,
|
| 1556 |
+
"funding": [
|
| 1557 |
+
{
|
| 1558 |
+
"type": "opencollective",
|
| 1559 |
+
"url": "https://opencollective.com/browserslist"
|
| 1560 |
+
},
|
| 1561 |
+
{
|
| 1562 |
+
"type": "tidelift",
|
| 1563 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1564 |
+
},
|
| 1565 |
+
{
|
| 1566 |
+
"type": "github",
|
| 1567 |
+
"url": "https://github.com/sponsors/ai"
|
| 1568 |
+
}
|
| 1569 |
+
],
|
| 1570 |
+
"license": "MIT",
|
| 1571 |
+
"dependencies": {
|
| 1572 |
+
"caniuse-lite": "^1.0.30001688",
|
| 1573 |
+
"electron-to-chromium": "^1.5.73",
|
| 1574 |
+
"node-releases": "^2.0.19",
|
| 1575 |
+
"update-browserslist-db": "^1.1.1"
|
| 1576 |
+
},
|
| 1577 |
+
"bin": {
|
| 1578 |
+
"browserslist": "cli.js"
|
| 1579 |
+
},
|
| 1580 |
+
"engines": {
|
| 1581 |
+
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 1582 |
+
}
|
| 1583 |
+
},
|
| 1584 |
+
"node_modules/bundle-name": {
|
| 1585 |
+
"version": "4.1.0",
|
| 1586 |
+
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
|
| 1587 |
+
"integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
|
| 1588 |
+
"dev": true,
|
| 1589 |
+
"license": "MIT",
|
| 1590 |
+
"dependencies": {
|
| 1591 |
+
"run-applescript": "^7.0.0"
|
| 1592 |
+
},
|
| 1593 |
+
"engines": {
|
| 1594 |
+
"node": ">=18"
|
| 1595 |
+
},
|
| 1596 |
+
"funding": {
|
| 1597 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1598 |
+
}
|
| 1599 |
+
},
|
| 1600 |
+
"node_modules/caniuse-lite": {
|
| 1601 |
+
"version": "1.0.30001712",
|
| 1602 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001712.tgz",
|
| 1603 |
+
"integrity": "sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==",
|
| 1604 |
+
"dev": true,
|
| 1605 |
+
"funding": [
|
| 1606 |
+
{
|
| 1607 |
+
"type": "opencollective",
|
| 1608 |
+
"url": "https://opencollective.com/browserslist"
|
| 1609 |
+
},
|
| 1610 |
+
{
|
| 1611 |
+
"type": "tidelift",
|
| 1612 |
+
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
| 1613 |
+
},
|
| 1614 |
+
{
|
| 1615 |
+
"type": "github",
|
| 1616 |
+
"url": "https://github.com/sponsors/ai"
|
| 1617 |
+
}
|
| 1618 |
+
],
|
| 1619 |
+
"license": "CC-BY-4.0"
|
| 1620 |
+
},
|
| 1621 |
+
"node_modules/convert-source-map": {
|
| 1622 |
+
"version": "2.0.0",
|
| 1623 |
+
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
| 1624 |
+
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
| 1625 |
+
"dev": true,
|
| 1626 |
+
"license": "MIT"
|
| 1627 |
+
},
|
| 1628 |
+
"node_modules/copy-anything": {
|
| 1629 |
+
"version": "3.0.5",
|
| 1630 |
+
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
|
| 1631 |
+
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
|
| 1632 |
+
"license": "MIT",
|
| 1633 |
+
"dependencies": {
|
| 1634 |
+
"is-what": "^4.1.8"
|
| 1635 |
+
},
|
| 1636 |
+
"engines": {
|
| 1637 |
+
"node": ">=12.13"
|
| 1638 |
+
},
|
| 1639 |
+
"funding": {
|
| 1640 |
+
"url": "https://github.com/sponsors/mesqueeb"
|
| 1641 |
+
}
|
| 1642 |
+
},
|
| 1643 |
+
"node_modules/cross-spawn": {
|
| 1644 |
+
"version": "7.0.6",
|
| 1645 |
+
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
| 1646 |
+
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
| 1647 |
+
"dev": true,
|
| 1648 |
+
"license": "MIT",
|
| 1649 |
+
"dependencies": {
|
| 1650 |
+
"path-key": "^3.1.0",
|
| 1651 |
+
"shebang-command": "^2.0.0",
|
| 1652 |
+
"which": "^2.0.1"
|
| 1653 |
+
},
|
| 1654 |
+
"engines": {
|
| 1655 |
+
"node": ">= 8"
|
| 1656 |
+
}
|
| 1657 |
+
},
|
| 1658 |
+
"node_modules/csstype": {
|
| 1659 |
+
"version": "3.1.3",
|
| 1660 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
| 1661 |
+
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
| 1662 |
+
"license": "MIT"
|
| 1663 |
+
},
|
| 1664 |
+
"node_modules/debug": {
|
| 1665 |
+
"version": "4.4.0",
|
| 1666 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
| 1667 |
+
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
| 1668 |
+
"dev": true,
|
| 1669 |
+
"license": "MIT",
|
| 1670 |
+
"dependencies": {
|
| 1671 |
+
"ms": "^2.1.3"
|
| 1672 |
+
},
|
| 1673 |
+
"engines": {
|
| 1674 |
+
"node": ">=6.0"
|
| 1675 |
+
},
|
| 1676 |
+
"peerDependenciesMeta": {
|
| 1677 |
+
"supports-color": {
|
| 1678 |
+
"optional": true
|
| 1679 |
+
}
|
| 1680 |
+
}
|
| 1681 |
+
},
|
| 1682 |
+
"node_modules/default-browser": {
|
| 1683 |
+
"version": "5.2.1",
|
| 1684 |
+
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
|
| 1685 |
+
"integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
|
| 1686 |
+
"dev": true,
|
| 1687 |
+
"license": "MIT",
|
| 1688 |
+
"dependencies": {
|
| 1689 |
+
"bundle-name": "^4.1.0",
|
| 1690 |
+
"default-browser-id": "^5.0.0"
|
| 1691 |
+
},
|
| 1692 |
+
"engines": {
|
| 1693 |
+
"node": ">=18"
|
| 1694 |
+
},
|
| 1695 |
+
"funding": {
|
| 1696 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1697 |
+
}
|
| 1698 |
+
},
|
| 1699 |
+
"node_modules/default-browser-id": {
|
| 1700 |
+
"version": "5.0.0",
|
| 1701 |
+
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
|
| 1702 |
+
"integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
|
| 1703 |
+
"dev": true,
|
| 1704 |
+
"license": "MIT",
|
| 1705 |
+
"engines": {
|
| 1706 |
+
"node": ">=18"
|
| 1707 |
+
},
|
| 1708 |
+
"funding": {
|
| 1709 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1710 |
+
}
|
| 1711 |
+
},
|
| 1712 |
+
"node_modules/define-lazy-prop": {
|
| 1713 |
+
"version": "3.0.0",
|
| 1714 |
+
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
|
| 1715 |
+
"integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
|
| 1716 |
+
"dev": true,
|
| 1717 |
+
"license": "MIT",
|
| 1718 |
+
"engines": {
|
| 1719 |
+
"node": ">=12"
|
| 1720 |
+
},
|
| 1721 |
+
"funding": {
|
| 1722 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1723 |
+
}
|
| 1724 |
+
},
|
| 1725 |
+
"node_modules/electron-to-chromium": {
|
| 1726 |
+
"version": "1.5.134",
|
| 1727 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.134.tgz",
|
| 1728 |
+
"integrity": "sha512-zSwzrLg3jNP3bwsLqWHmS5z2nIOQ5ngMnfMZOWWtXnqqQkPVyOipxK98w+1beLw1TB+EImPNcG8wVP/cLVs2Og==",
|
| 1729 |
+
"dev": true,
|
| 1730 |
+
"license": "ISC"
|
| 1731 |
+
},
|
| 1732 |
+
"node_modules/entities": {
|
| 1733 |
+
"version": "4.5.0",
|
| 1734 |
+
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
| 1735 |
+
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
| 1736 |
+
"license": "BSD-2-Clause",
|
| 1737 |
+
"engines": {
|
| 1738 |
+
"node": ">=0.12"
|
| 1739 |
+
},
|
| 1740 |
+
"funding": {
|
| 1741 |
+
"url": "https://github.com/fb55/entities?sponsor=1"
|
| 1742 |
+
}
|
| 1743 |
+
},
|
| 1744 |
+
"node_modules/error-stack-parser-es": {
|
| 1745 |
+
"version": "0.1.5",
|
| 1746 |
+
"resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz",
|
| 1747 |
+
"integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==",
|
| 1748 |
+
"dev": true,
|
| 1749 |
+
"license": "MIT",
|
| 1750 |
+
"funding": {
|
| 1751 |
+
"url": "https://github.com/sponsors/antfu"
|
| 1752 |
+
}
|
| 1753 |
+
},
|
| 1754 |
+
"node_modules/esbuild": {
|
| 1755 |
+
"version": "0.25.2",
|
| 1756 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz",
|
| 1757 |
+
"integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==",
|
| 1758 |
+
"dev": true,
|
| 1759 |
+
"hasInstallScript": true,
|
| 1760 |
+
"license": "MIT",
|
| 1761 |
+
"bin": {
|
| 1762 |
+
"esbuild": "bin/esbuild"
|
| 1763 |
+
},
|
| 1764 |
+
"engines": {
|
| 1765 |
+
"node": ">=18"
|
| 1766 |
+
},
|
| 1767 |
+
"optionalDependencies": {
|
| 1768 |
+
"@esbuild/aix-ppc64": "0.25.2",
|
| 1769 |
+
"@esbuild/android-arm": "0.25.2",
|
| 1770 |
+
"@esbuild/android-arm64": "0.25.2",
|
| 1771 |
+
"@esbuild/android-x64": "0.25.2",
|
| 1772 |
+
"@esbuild/darwin-arm64": "0.25.2",
|
| 1773 |
+
"@esbuild/darwin-x64": "0.25.2",
|
| 1774 |
+
"@esbuild/freebsd-arm64": "0.25.2",
|
| 1775 |
+
"@esbuild/freebsd-x64": "0.25.2",
|
| 1776 |
+
"@esbuild/linux-arm": "0.25.2",
|
| 1777 |
+
"@esbuild/linux-arm64": "0.25.2",
|
| 1778 |
+
"@esbuild/linux-ia32": "0.25.2",
|
| 1779 |
+
"@esbuild/linux-loong64": "0.25.2",
|
| 1780 |
+
"@esbuild/linux-mips64el": "0.25.2",
|
| 1781 |
+
"@esbuild/linux-ppc64": "0.25.2",
|
| 1782 |
+
"@esbuild/linux-riscv64": "0.25.2",
|
| 1783 |
+
"@esbuild/linux-s390x": "0.25.2",
|
| 1784 |
+
"@esbuild/linux-x64": "0.25.2",
|
| 1785 |
+
"@esbuild/netbsd-arm64": "0.25.2",
|
| 1786 |
+
"@esbuild/netbsd-x64": "0.25.2",
|
| 1787 |
+
"@esbuild/openbsd-arm64": "0.25.2",
|
| 1788 |
+
"@esbuild/openbsd-x64": "0.25.2",
|
| 1789 |
+
"@esbuild/sunos-x64": "0.25.2",
|
| 1790 |
+
"@esbuild/win32-arm64": "0.25.2",
|
| 1791 |
+
"@esbuild/win32-ia32": "0.25.2",
|
| 1792 |
+
"@esbuild/win32-x64": "0.25.2"
|
| 1793 |
+
}
|
| 1794 |
+
},
|
| 1795 |
+
"node_modules/escalade": {
|
| 1796 |
+
"version": "3.2.0",
|
| 1797 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 1798 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 1799 |
+
"dev": true,
|
| 1800 |
+
"license": "MIT",
|
| 1801 |
+
"engines": {
|
| 1802 |
+
"node": ">=6"
|
| 1803 |
+
}
|
| 1804 |
+
},
|
| 1805 |
+
"node_modules/estree-walker": {
|
| 1806 |
+
"version": "2.0.2",
|
| 1807 |
+
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
| 1808 |
+
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
| 1809 |
+
"license": "MIT"
|
| 1810 |
+
},
|
| 1811 |
+
"node_modules/execa": {
|
| 1812 |
+
"version": "9.5.2",
|
| 1813 |
+
"resolved": "https://registry.npmjs.org/execa/-/execa-9.5.2.tgz",
|
| 1814 |
+
"integrity": "sha512-EHlpxMCpHWSAh1dgS6bVeoLAXGnJNdR93aabr4QCGbzOM73o5XmRfM/e5FUqsw3aagP8S8XEWUWFAxnRBnAF0Q==",
|
| 1815 |
+
"dev": true,
|
| 1816 |
+
"license": "MIT",
|
| 1817 |
+
"dependencies": {
|
| 1818 |
+
"@sindresorhus/merge-streams": "^4.0.0",
|
| 1819 |
+
"cross-spawn": "^7.0.3",
|
| 1820 |
+
"figures": "^6.1.0",
|
| 1821 |
+
"get-stream": "^9.0.0",
|
| 1822 |
+
"human-signals": "^8.0.0",
|
| 1823 |
+
"is-plain-obj": "^4.1.0",
|
| 1824 |
+
"is-stream": "^4.0.1",
|
| 1825 |
+
"npm-run-path": "^6.0.0",
|
| 1826 |
+
"pretty-ms": "^9.0.0",
|
| 1827 |
+
"signal-exit": "^4.1.0",
|
| 1828 |
+
"strip-final-newline": "^4.0.0",
|
| 1829 |
+
"yoctocolors": "^2.0.0"
|
| 1830 |
+
},
|
| 1831 |
+
"engines": {
|
| 1832 |
+
"node": "^18.19.0 || >=20.5.0"
|
| 1833 |
+
},
|
| 1834 |
+
"funding": {
|
| 1835 |
+
"url": "https://github.com/sindresorhus/execa?sponsor=1"
|
| 1836 |
+
}
|
| 1837 |
+
},
|
| 1838 |
+
"node_modules/figures": {
|
| 1839 |
+
"version": "6.1.0",
|
| 1840 |
+
"resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
|
| 1841 |
+
"integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
|
| 1842 |
+
"dev": true,
|
| 1843 |
+
"license": "MIT",
|
| 1844 |
+
"dependencies": {
|
| 1845 |
+
"is-unicode-supported": "^2.0.0"
|
| 1846 |
+
},
|
| 1847 |
+
"engines": {
|
| 1848 |
+
"node": ">=18"
|
| 1849 |
+
},
|
| 1850 |
+
"funding": {
|
| 1851 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1852 |
+
}
|
| 1853 |
+
},
|
| 1854 |
+
"node_modules/fs-extra": {
|
| 1855 |
+
"version": "11.3.0",
|
| 1856 |
+
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
|
| 1857 |
+
"integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
|
| 1858 |
+
"dev": true,
|
| 1859 |
+
"license": "MIT",
|
| 1860 |
+
"dependencies": {
|
| 1861 |
+
"graceful-fs": "^4.2.0",
|
| 1862 |
+
"jsonfile": "^6.0.1",
|
| 1863 |
+
"universalify": "^2.0.0"
|
| 1864 |
+
},
|
| 1865 |
+
"engines": {
|
| 1866 |
+
"node": ">=14.14"
|
| 1867 |
+
}
|
| 1868 |
+
},
|
| 1869 |
+
"node_modules/fsevents": {
|
| 1870 |
+
"version": "2.3.3",
|
| 1871 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1872 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1873 |
+
"dev": true,
|
| 1874 |
+
"hasInstallScript": true,
|
| 1875 |
+
"license": "MIT",
|
| 1876 |
+
"optional": true,
|
| 1877 |
+
"os": [
|
| 1878 |
+
"darwin"
|
| 1879 |
+
],
|
| 1880 |
+
"engines": {
|
| 1881 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1882 |
+
}
|
| 1883 |
+
},
|
| 1884 |
+
"node_modules/gensync": {
|
| 1885 |
+
"version": "1.0.0-beta.2",
|
| 1886 |
+
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
| 1887 |
+
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
| 1888 |
+
"dev": true,
|
| 1889 |
+
"license": "MIT",
|
| 1890 |
+
"engines": {
|
| 1891 |
+
"node": ">=6.9.0"
|
| 1892 |
+
}
|
| 1893 |
+
},
|
| 1894 |
+
"node_modules/get-stream": {
|
| 1895 |
+
"version": "9.0.1",
|
| 1896 |
+
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
|
| 1897 |
+
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
|
| 1898 |
+
"dev": true,
|
| 1899 |
+
"license": "MIT",
|
| 1900 |
+
"dependencies": {
|
| 1901 |
+
"@sec-ant/readable-stream": "^0.4.1",
|
| 1902 |
+
"is-stream": "^4.0.1"
|
| 1903 |
+
},
|
| 1904 |
+
"engines": {
|
| 1905 |
+
"node": ">=18"
|
| 1906 |
+
},
|
| 1907 |
+
"funding": {
|
| 1908 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1909 |
+
}
|
| 1910 |
+
},
|
| 1911 |
+
"node_modules/globals": {
|
| 1912 |
+
"version": "11.12.0",
|
| 1913 |
+
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
| 1914 |
+
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
| 1915 |
+
"dev": true,
|
| 1916 |
+
"license": "MIT",
|
| 1917 |
+
"engines": {
|
| 1918 |
+
"node": ">=4"
|
| 1919 |
+
}
|
| 1920 |
+
},
|
| 1921 |
+
"node_modules/graceful-fs": {
|
| 1922 |
+
"version": "4.2.11",
|
| 1923 |
+
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
| 1924 |
+
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
| 1925 |
+
"dev": true,
|
| 1926 |
+
"license": "ISC"
|
| 1927 |
+
},
|
| 1928 |
+
"node_modules/hajimiui": {
|
| 1929 |
+
"resolved": "",
|
| 1930 |
+
"link": true
|
| 1931 |
+
},
|
| 1932 |
+
"node_modules/hookable": {
|
| 1933 |
+
"version": "5.5.3",
|
| 1934 |
+
"resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
|
| 1935 |
+
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
| 1936 |
+
"license": "MIT"
|
| 1937 |
+
},
|
| 1938 |
+
"node_modules/human-signals": {
|
| 1939 |
+
"version": "8.0.1",
|
| 1940 |
+
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz",
|
| 1941 |
+
"integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==",
|
| 1942 |
+
"dev": true,
|
| 1943 |
+
"license": "Apache-2.0",
|
| 1944 |
+
"engines": {
|
| 1945 |
+
"node": ">=18.18.0"
|
| 1946 |
+
}
|
| 1947 |
+
},
|
| 1948 |
+
"node_modules/is-docker": {
|
| 1949 |
+
"version": "3.0.0",
|
| 1950 |
+
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
| 1951 |
+
"integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
|
| 1952 |
+
"dev": true,
|
| 1953 |
+
"license": "MIT",
|
| 1954 |
+
"bin": {
|
| 1955 |
+
"is-docker": "cli.js"
|
| 1956 |
+
},
|
| 1957 |
+
"engines": {
|
| 1958 |
+
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
| 1959 |
+
},
|
| 1960 |
+
"funding": {
|
| 1961 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1962 |
+
}
|
| 1963 |
+
},
|
| 1964 |
+
"node_modules/is-inside-container": {
|
| 1965 |
+
"version": "1.0.0",
|
| 1966 |
+
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
| 1967 |
+
"integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
|
| 1968 |
+
"dev": true,
|
| 1969 |
+
"license": "MIT",
|
| 1970 |
+
"dependencies": {
|
| 1971 |
+
"is-docker": "^3.0.0"
|
| 1972 |
+
},
|
| 1973 |
+
"bin": {
|
| 1974 |
+
"is-inside-container": "cli.js"
|
| 1975 |
+
},
|
| 1976 |
+
"engines": {
|
| 1977 |
+
"node": ">=14.16"
|
| 1978 |
+
},
|
| 1979 |
+
"funding": {
|
| 1980 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1981 |
+
}
|
| 1982 |
+
},
|
| 1983 |
+
"node_modules/is-plain-obj": {
|
| 1984 |
+
"version": "4.1.0",
|
| 1985 |
+
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
| 1986 |
+
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
|
| 1987 |
+
"dev": true,
|
| 1988 |
+
"license": "MIT",
|
| 1989 |
+
"engines": {
|
| 1990 |
+
"node": ">=12"
|
| 1991 |
+
},
|
| 1992 |
+
"funding": {
|
| 1993 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1994 |
+
}
|
| 1995 |
+
},
|
| 1996 |
+
"node_modules/is-stream": {
|
| 1997 |
+
"version": "4.0.1",
|
| 1998 |
+
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
|
| 1999 |
+
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
|
| 2000 |
+
"dev": true,
|
| 2001 |
+
"license": "MIT",
|
| 2002 |
+
"engines": {
|
| 2003 |
+
"node": ">=18"
|
| 2004 |
+
},
|
| 2005 |
+
"funding": {
|
| 2006 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2007 |
+
}
|
| 2008 |
+
},
|
| 2009 |
+
"node_modules/is-unicode-supported": {
|
| 2010 |
+
"version": "2.1.0",
|
| 2011 |
+
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
| 2012 |
+
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
| 2013 |
+
"dev": true,
|
| 2014 |
+
"license": "MIT",
|
| 2015 |
+
"engines": {
|
| 2016 |
+
"node": ">=18"
|
| 2017 |
+
},
|
| 2018 |
+
"funding": {
|
| 2019 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2020 |
+
}
|
| 2021 |
+
},
|
| 2022 |
+
"node_modules/is-what": {
|
| 2023 |
+
"version": "4.1.16",
|
| 2024 |
+
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
|
| 2025 |
+
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
|
| 2026 |
+
"license": "MIT",
|
| 2027 |
+
"engines": {
|
| 2028 |
+
"node": ">=12.13"
|
| 2029 |
+
},
|
| 2030 |
+
"funding": {
|
| 2031 |
+
"url": "https://github.com/sponsors/mesqueeb"
|
| 2032 |
+
}
|
| 2033 |
+
},
|
| 2034 |
+
"node_modules/is-wsl": {
|
| 2035 |
+
"version": "3.1.0",
|
| 2036 |
+
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
|
| 2037 |
+
"integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
|
| 2038 |
+
"dev": true,
|
| 2039 |
+
"license": "MIT",
|
| 2040 |
+
"dependencies": {
|
| 2041 |
+
"is-inside-container": "^1.0.0"
|
| 2042 |
+
},
|
| 2043 |
+
"engines": {
|
| 2044 |
+
"node": ">=16"
|
| 2045 |
+
},
|
| 2046 |
+
"funding": {
|
| 2047 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2048 |
+
}
|
| 2049 |
+
},
|
| 2050 |
+
"node_modules/isexe": {
|
| 2051 |
+
"version": "2.0.0",
|
| 2052 |
+
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
| 2053 |
+
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
| 2054 |
+
"dev": true,
|
| 2055 |
+
"license": "ISC"
|
| 2056 |
+
},
|
| 2057 |
+
"node_modules/js-tokens": {
|
| 2058 |
+
"version": "4.0.0",
|
| 2059 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 2060 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
| 2061 |
+
"dev": true,
|
| 2062 |
+
"license": "MIT"
|
| 2063 |
+
},
|
| 2064 |
+
"node_modules/jsesc": {
|
| 2065 |
+
"version": "3.1.0",
|
| 2066 |
+
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
| 2067 |
+
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 2068 |
+
"dev": true,
|
| 2069 |
+
"license": "MIT",
|
| 2070 |
+
"bin": {
|
| 2071 |
+
"jsesc": "bin/jsesc"
|
| 2072 |
+
},
|
| 2073 |
+
"engines": {
|
| 2074 |
+
"node": ">=6"
|
| 2075 |
+
}
|
| 2076 |
+
},
|
| 2077 |
+
"node_modules/json5": {
|
| 2078 |
+
"version": "2.2.3",
|
| 2079 |
+
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
| 2080 |
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
| 2081 |
+
"dev": true,
|
| 2082 |
+
"license": "MIT",
|
| 2083 |
+
"bin": {
|
| 2084 |
+
"json5": "lib/cli.js"
|
| 2085 |
+
},
|
| 2086 |
+
"engines": {
|
| 2087 |
+
"node": ">=6"
|
| 2088 |
+
}
|
| 2089 |
+
},
|
| 2090 |
+
"node_modules/jsonfile": {
|
| 2091 |
+
"version": "6.1.0",
|
| 2092 |
+
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
| 2093 |
+
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
| 2094 |
+
"dev": true,
|
| 2095 |
+
"license": "MIT",
|
| 2096 |
+
"dependencies": {
|
| 2097 |
+
"universalify": "^2.0.0"
|
| 2098 |
+
},
|
| 2099 |
+
"optionalDependencies": {
|
| 2100 |
+
"graceful-fs": "^4.1.6"
|
| 2101 |
+
}
|
| 2102 |
+
},
|
| 2103 |
+
"node_modules/kolorist": {
|
| 2104 |
+
"version": "1.8.0",
|
| 2105 |
+
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
| 2106 |
+
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
|
| 2107 |
+
"dev": true,
|
| 2108 |
+
"license": "MIT"
|
| 2109 |
+
},
|
| 2110 |
+
"node_modules/lru-cache": {
|
| 2111 |
+
"version": "5.1.1",
|
| 2112 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
| 2113 |
+
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
| 2114 |
+
"dev": true,
|
| 2115 |
+
"license": "ISC",
|
| 2116 |
+
"dependencies": {
|
| 2117 |
+
"yallist": "^3.0.2"
|
| 2118 |
+
}
|
| 2119 |
+
},
|
| 2120 |
+
"node_modules/magic-string": {
|
| 2121 |
+
"version": "0.30.17",
|
| 2122 |
+
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
| 2123 |
+
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
| 2124 |
+
"license": "MIT",
|
| 2125 |
+
"dependencies": {
|
| 2126 |
+
"@jridgewell/sourcemap-codec": "^1.5.0"
|
| 2127 |
+
}
|
| 2128 |
+
},
|
| 2129 |
+
"node_modules/mitt": {
|
| 2130 |
+
"version": "3.0.1",
|
| 2131 |
+
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
| 2132 |
+
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
| 2133 |
+
"license": "MIT"
|
| 2134 |
+
},
|
| 2135 |
+
"node_modules/mrmime": {
|
| 2136 |
+
"version": "2.0.1",
|
| 2137 |
+
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
| 2138 |
+
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
| 2139 |
+
"dev": true,
|
| 2140 |
+
"license": "MIT",
|
| 2141 |
+
"engines": {
|
| 2142 |
+
"node": ">=10"
|
| 2143 |
+
}
|
| 2144 |
+
},
|
| 2145 |
+
"node_modules/ms": {
|
| 2146 |
+
"version": "2.1.3",
|
| 2147 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 2148 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 2149 |
+
"dev": true,
|
| 2150 |
+
"license": "MIT"
|
| 2151 |
+
},
|
| 2152 |
+
"node_modules/nanoid": {
|
| 2153 |
+
"version": "3.3.11",
|
| 2154 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 2155 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 2156 |
+
"funding": [
|
| 2157 |
+
{
|
| 2158 |
+
"type": "github",
|
| 2159 |
+
"url": "https://github.com/sponsors/ai"
|
| 2160 |
+
}
|
| 2161 |
+
],
|
| 2162 |
+
"license": "MIT",
|
| 2163 |
+
"bin": {
|
| 2164 |
+
"nanoid": "bin/nanoid.cjs"
|
| 2165 |
+
},
|
| 2166 |
+
"engines": {
|
| 2167 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 2168 |
+
}
|
| 2169 |
+
},
|
| 2170 |
+
"node_modules/node-releases": {
|
| 2171 |
+
"version": "2.0.19",
|
| 2172 |
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
| 2173 |
+
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
| 2174 |
+
"dev": true,
|
| 2175 |
+
"license": "MIT"
|
| 2176 |
+
},
|
| 2177 |
+
"node_modules/npm-run-path": {
|
| 2178 |
+
"version": "6.0.0",
|
| 2179 |
+
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
|
| 2180 |
+
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
|
| 2181 |
+
"dev": true,
|
| 2182 |
+
"license": "MIT",
|
| 2183 |
+
"dependencies": {
|
| 2184 |
+
"path-key": "^4.0.0",
|
| 2185 |
+
"unicorn-magic": "^0.3.0"
|
| 2186 |
+
},
|
| 2187 |
+
"engines": {
|
| 2188 |
+
"node": ">=18"
|
| 2189 |
+
},
|
| 2190 |
+
"funding": {
|
| 2191 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2192 |
+
}
|
| 2193 |
+
},
|
| 2194 |
+
"node_modules/npm-run-path/node_modules/path-key": {
|
| 2195 |
+
"version": "4.0.0",
|
| 2196 |
+
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
|
| 2197 |
+
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
|
| 2198 |
+
"dev": true,
|
| 2199 |
+
"license": "MIT",
|
| 2200 |
+
"engines": {
|
| 2201 |
+
"node": ">=12"
|
| 2202 |
+
},
|
| 2203 |
+
"funding": {
|
| 2204 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2205 |
+
}
|
| 2206 |
+
},
|
| 2207 |
+
"node_modules/open": {
|
| 2208 |
+
"version": "10.1.0",
|
| 2209 |
+
"resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz",
|
| 2210 |
+
"integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==",
|
| 2211 |
+
"dev": true,
|
| 2212 |
+
"license": "MIT",
|
| 2213 |
+
"dependencies": {
|
| 2214 |
+
"default-browser": "^5.2.1",
|
| 2215 |
+
"define-lazy-prop": "^3.0.0",
|
| 2216 |
+
"is-inside-container": "^1.0.0",
|
| 2217 |
+
"is-wsl": "^3.1.0"
|
| 2218 |
+
},
|
| 2219 |
+
"engines": {
|
| 2220 |
+
"node": ">=18"
|
| 2221 |
+
},
|
| 2222 |
+
"funding": {
|
| 2223 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2224 |
+
}
|
| 2225 |
+
},
|
| 2226 |
+
"node_modules/parse-ms": {
|
| 2227 |
+
"version": "4.0.0",
|
| 2228 |
+
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
|
| 2229 |
+
"integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
|
| 2230 |
+
"dev": true,
|
| 2231 |
+
"license": "MIT",
|
| 2232 |
+
"engines": {
|
| 2233 |
+
"node": ">=18"
|
| 2234 |
+
},
|
| 2235 |
+
"funding": {
|
| 2236 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2237 |
+
}
|
| 2238 |
+
},
|
| 2239 |
+
"node_modules/path-key": {
|
| 2240 |
+
"version": "3.1.1",
|
| 2241 |
+
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
| 2242 |
+
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
| 2243 |
+
"dev": true,
|
| 2244 |
+
"license": "MIT",
|
| 2245 |
+
"engines": {
|
| 2246 |
+
"node": ">=8"
|
| 2247 |
+
}
|
| 2248 |
+
},
|
| 2249 |
+
"node_modules/pathe": {
|
| 2250 |
+
"version": "2.0.3",
|
| 2251 |
+
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
| 2252 |
+
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
| 2253 |
+
"dev": true,
|
| 2254 |
+
"license": "MIT"
|
| 2255 |
+
},
|
| 2256 |
+
"node_modules/perfect-debounce": {
|
| 2257 |
+
"version": "1.0.0",
|
| 2258 |
+
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
| 2259 |
+
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
| 2260 |
+
"license": "MIT"
|
| 2261 |
+
},
|
| 2262 |
+
"node_modules/picocolors": {
|
| 2263 |
+
"version": "1.1.1",
|
| 2264 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 2265 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 2266 |
+
"license": "ISC"
|
| 2267 |
+
},
|
| 2268 |
+
"node_modules/picomatch": {
|
| 2269 |
+
"version": "4.0.2",
|
| 2270 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
| 2271 |
+
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
| 2272 |
+
"dev": true,
|
| 2273 |
+
"license": "MIT",
|
| 2274 |
+
"engines": {
|
| 2275 |
+
"node": ">=12"
|
| 2276 |
+
},
|
| 2277 |
+
"funding": {
|
| 2278 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 2279 |
+
}
|
| 2280 |
+
},
|
| 2281 |
+
"node_modules/pinia": {
|
| 2282 |
+
"version": "3.0.1",
|
| 2283 |
+
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.1.tgz",
|
| 2284 |
+
"integrity": "sha512-WXglsDzztOTH6IfcJ99ltYZin2mY8XZCXujkYWVIJlBjqsP6ST7zw+Aarh63E1cDVYeyUcPCxPHzJpEOmzB6Wg==",
|
| 2285 |
+
"license": "MIT",
|
| 2286 |
+
"dependencies": {
|
| 2287 |
+
"@vue/devtools-api": "^7.7.2"
|
| 2288 |
+
},
|
| 2289 |
+
"funding": {
|
| 2290 |
+
"url": "https://github.com/sponsors/posva"
|
| 2291 |
+
},
|
| 2292 |
+
"peerDependencies": {
|
| 2293 |
+
"typescript": ">=4.4.4",
|
| 2294 |
+
"vue": "^2.7.0 || ^3.5.11"
|
| 2295 |
+
},
|
| 2296 |
+
"peerDependenciesMeta": {
|
| 2297 |
+
"typescript": {
|
| 2298 |
+
"optional": true
|
| 2299 |
+
}
|
| 2300 |
+
}
|
| 2301 |
+
},
|
| 2302 |
+
"node_modules/postcss": {
|
| 2303 |
+
"version": "8.5.3",
|
| 2304 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
| 2305 |
+
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
| 2306 |
+
"funding": [
|
| 2307 |
+
{
|
| 2308 |
+
"type": "opencollective",
|
| 2309 |
+
"url": "https://opencollective.com/postcss/"
|
| 2310 |
+
},
|
| 2311 |
+
{
|
| 2312 |
+
"type": "tidelift",
|
| 2313 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 2314 |
+
},
|
| 2315 |
+
{
|
| 2316 |
+
"type": "github",
|
| 2317 |
+
"url": "https://github.com/sponsors/ai"
|
| 2318 |
+
}
|
| 2319 |
+
],
|
| 2320 |
+
"license": "MIT",
|
| 2321 |
+
"dependencies": {
|
| 2322 |
+
"nanoid": "^3.3.8",
|
| 2323 |
+
"picocolors": "^1.1.1",
|
| 2324 |
+
"source-map-js": "^1.2.1"
|
| 2325 |
+
},
|
| 2326 |
+
"engines": {
|
| 2327 |
+
"node": "^10 || ^12 || >=14"
|
| 2328 |
+
}
|
| 2329 |
+
},
|
| 2330 |
+
"node_modules/pretty-ms": {
|
| 2331 |
+
"version": "9.2.0",
|
| 2332 |
+
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
|
| 2333 |
+
"integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==",
|
| 2334 |
+
"dev": true,
|
| 2335 |
+
"license": "MIT",
|
| 2336 |
+
"dependencies": {
|
| 2337 |
+
"parse-ms": "^4.0.0"
|
| 2338 |
+
},
|
| 2339 |
+
"engines": {
|
| 2340 |
+
"node": ">=18"
|
| 2341 |
+
},
|
| 2342 |
+
"funding": {
|
| 2343 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2344 |
+
}
|
| 2345 |
+
},
|
| 2346 |
+
"node_modules/rfdc": {
|
| 2347 |
+
"version": "1.4.1",
|
| 2348 |
+
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
| 2349 |
+
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
| 2350 |
+
"license": "MIT"
|
| 2351 |
+
},
|
| 2352 |
+
"node_modules/rollup": {
|
| 2353 |
+
"version": "4.39.0",
|
| 2354 |
+
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.39.0.tgz",
|
| 2355 |
+
"integrity": "sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==",
|
| 2356 |
+
"dev": true,
|
| 2357 |
+
"license": "MIT",
|
| 2358 |
+
"dependencies": {
|
| 2359 |
+
"@types/estree": "1.0.7"
|
| 2360 |
+
},
|
| 2361 |
+
"bin": {
|
| 2362 |
+
"rollup": "dist/bin/rollup"
|
| 2363 |
+
},
|
| 2364 |
+
"engines": {
|
| 2365 |
+
"node": ">=18.0.0",
|
| 2366 |
+
"npm": ">=8.0.0"
|
| 2367 |
+
},
|
| 2368 |
+
"optionalDependencies": {
|
| 2369 |
+
"@rollup/rollup-android-arm-eabi": "4.39.0",
|
| 2370 |
+
"@rollup/rollup-android-arm64": "4.39.0",
|
| 2371 |
+
"@rollup/rollup-darwin-arm64": "4.39.0",
|
| 2372 |
+
"@rollup/rollup-darwin-x64": "4.39.0",
|
| 2373 |
+
"@rollup/rollup-freebsd-arm64": "4.39.0",
|
| 2374 |
+
"@rollup/rollup-freebsd-x64": "4.39.0",
|
| 2375 |
+
"@rollup/rollup-linux-arm-gnueabihf": "4.39.0",
|
| 2376 |
+
"@rollup/rollup-linux-arm-musleabihf": "4.39.0",
|
| 2377 |
+
"@rollup/rollup-linux-arm64-gnu": "4.39.0",
|
| 2378 |
+
"@rollup/rollup-linux-arm64-musl": "4.39.0",
|
| 2379 |
+
"@rollup/rollup-linux-loongarch64-gnu": "4.39.0",
|
| 2380 |
+
"@rollup/rollup-linux-powerpc64le-gnu": "4.39.0",
|
| 2381 |
+
"@rollup/rollup-linux-riscv64-gnu": "4.39.0",
|
| 2382 |
+
"@rollup/rollup-linux-riscv64-musl": "4.39.0",
|
| 2383 |
+
"@rollup/rollup-linux-s390x-gnu": "4.39.0",
|
| 2384 |
+
"@rollup/rollup-linux-x64-gnu": "4.39.0",
|
| 2385 |
+
"@rollup/rollup-linux-x64-musl": "4.39.0",
|
| 2386 |
+
"@rollup/rollup-win32-arm64-msvc": "4.39.0",
|
| 2387 |
+
"@rollup/rollup-win32-ia32-msvc": "4.39.0",
|
| 2388 |
+
"@rollup/rollup-win32-x64-msvc": "4.39.0",
|
| 2389 |
+
"fsevents": "~2.3.2"
|
| 2390 |
+
}
|
| 2391 |
+
},
|
| 2392 |
+
"node_modules/run-applescript": {
|
| 2393 |
+
"version": "7.0.0",
|
| 2394 |
+
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz",
|
| 2395 |
+
"integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==",
|
| 2396 |
+
"dev": true,
|
| 2397 |
+
"license": "MIT",
|
| 2398 |
+
"engines": {
|
| 2399 |
+
"node": ">=18"
|
| 2400 |
+
},
|
| 2401 |
+
"funding": {
|
| 2402 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2403 |
+
}
|
| 2404 |
+
},
|
| 2405 |
+
"node_modules/semver": {
|
| 2406 |
+
"version": "6.3.1",
|
| 2407 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
| 2408 |
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
| 2409 |
+
"dev": true,
|
| 2410 |
+
"license": "ISC",
|
| 2411 |
+
"bin": {
|
| 2412 |
+
"semver": "bin/semver.js"
|
| 2413 |
+
}
|
| 2414 |
+
},
|
| 2415 |
+
"node_modules/shebang-command": {
|
| 2416 |
+
"version": "2.0.0",
|
| 2417 |
+
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
| 2418 |
+
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
| 2419 |
+
"dev": true,
|
| 2420 |
+
"license": "MIT",
|
| 2421 |
+
"dependencies": {
|
| 2422 |
+
"shebang-regex": "^3.0.0"
|
| 2423 |
+
},
|
| 2424 |
+
"engines": {
|
| 2425 |
+
"node": ">=8"
|
| 2426 |
+
}
|
| 2427 |
+
},
|
| 2428 |
+
"node_modules/shebang-regex": {
|
| 2429 |
+
"version": "3.0.0",
|
| 2430 |
+
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
| 2431 |
+
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
| 2432 |
+
"dev": true,
|
| 2433 |
+
"license": "MIT",
|
| 2434 |
+
"engines": {
|
| 2435 |
+
"node": ">=8"
|
| 2436 |
+
}
|
| 2437 |
+
},
|
| 2438 |
+
"node_modules/signal-exit": {
|
| 2439 |
+
"version": "4.1.0",
|
| 2440 |
+
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
| 2441 |
+
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
| 2442 |
+
"dev": true,
|
| 2443 |
+
"license": "ISC",
|
| 2444 |
+
"engines": {
|
| 2445 |
+
"node": ">=14"
|
| 2446 |
+
},
|
| 2447 |
+
"funding": {
|
| 2448 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 2449 |
+
}
|
| 2450 |
+
},
|
| 2451 |
+
"node_modules/sirv": {
|
| 2452 |
+
"version": "3.0.1",
|
| 2453 |
+
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
|
| 2454 |
+
"integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==",
|
| 2455 |
+
"dev": true,
|
| 2456 |
+
"license": "MIT",
|
| 2457 |
+
"dependencies": {
|
| 2458 |
+
"@polka/url": "^1.0.0-next.24",
|
| 2459 |
+
"mrmime": "^2.0.0",
|
| 2460 |
+
"totalist": "^3.0.0"
|
| 2461 |
+
},
|
| 2462 |
+
"engines": {
|
| 2463 |
+
"node": ">=18"
|
| 2464 |
+
}
|
| 2465 |
+
},
|
| 2466 |
+
"node_modules/source-map-js": {
|
| 2467 |
+
"version": "1.2.1",
|
| 2468 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 2469 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 2470 |
+
"license": "BSD-3-Clause",
|
| 2471 |
+
"engines": {
|
| 2472 |
+
"node": ">=0.10.0"
|
| 2473 |
+
}
|
| 2474 |
+
},
|
| 2475 |
+
"node_modules/speakingurl": {
|
| 2476 |
+
"version": "14.0.1",
|
| 2477 |
+
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
|
| 2478 |
+
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
| 2479 |
+
"license": "BSD-3-Clause",
|
| 2480 |
+
"engines": {
|
| 2481 |
+
"node": ">=0.10.0"
|
| 2482 |
+
}
|
| 2483 |
+
},
|
| 2484 |
+
"node_modules/strip-final-newline": {
|
| 2485 |
+
"version": "4.0.0",
|
| 2486 |
+
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
|
| 2487 |
+
"integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
|
| 2488 |
+
"dev": true,
|
| 2489 |
+
"license": "MIT",
|
| 2490 |
+
"engines": {
|
| 2491 |
+
"node": ">=18"
|
| 2492 |
+
},
|
| 2493 |
+
"funding": {
|
| 2494 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2495 |
+
}
|
| 2496 |
+
},
|
| 2497 |
+
"node_modules/superjson": {
|
| 2498 |
+
"version": "2.2.2",
|
| 2499 |
+
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
|
| 2500 |
+
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
|
| 2501 |
+
"license": "MIT",
|
| 2502 |
+
"dependencies": {
|
| 2503 |
+
"copy-anything": "^3.0.2"
|
| 2504 |
+
},
|
| 2505 |
+
"engines": {
|
| 2506 |
+
"node": ">=16"
|
| 2507 |
+
}
|
| 2508 |
+
},
|
| 2509 |
+
"node_modules/totalist": {
|
| 2510 |
+
"version": "3.0.1",
|
| 2511 |
+
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
| 2512 |
+
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
| 2513 |
+
"dev": true,
|
| 2514 |
+
"license": "MIT",
|
| 2515 |
+
"engines": {
|
| 2516 |
+
"node": ">=6"
|
| 2517 |
+
}
|
| 2518 |
+
},
|
| 2519 |
+
"node_modules/unicorn-magic": {
|
| 2520 |
+
"version": "0.3.0",
|
| 2521 |
+
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
|
| 2522 |
+
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
|
| 2523 |
+
"dev": true,
|
| 2524 |
+
"license": "MIT",
|
| 2525 |
+
"engines": {
|
| 2526 |
+
"node": ">=18"
|
| 2527 |
+
},
|
| 2528 |
+
"funding": {
|
| 2529 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2530 |
+
}
|
| 2531 |
+
},
|
| 2532 |
+
"node_modules/universalify": {
|
| 2533 |
+
"version": "2.0.1",
|
| 2534 |
+
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
| 2535 |
+
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
| 2536 |
+
"dev": true,
|
| 2537 |
+
"license": "MIT",
|
| 2538 |
+
"engines": {
|
| 2539 |
+
"node": ">= 10.0.0"
|
| 2540 |
+
}
|
| 2541 |
+
},
|
| 2542 |
+
"node_modules/update-browserslist-db": {
|
| 2543 |
+
"version": "1.1.3",
|
| 2544 |
+
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
| 2545 |
+
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
| 2546 |
+
"dev": true,
|
| 2547 |
+
"funding": [
|
| 2548 |
+
{
|
| 2549 |
+
"type": "opencollective",
|
| 2550 |
+
"url": "https://opencollective.com/browserslist"
|
| 2551 |
+
},
|
| 2552 |
+
{
|
| 2553 |
+
"type": "tidelift",
|
| 2554 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 2555 |
+
},
|
| 2556 |
+
{
|
| 2557 |
+
"type": "github",
|
| 2558 |
+
"url": "https://github.com/sponsors/ai"
|
| 2559 |
+
}
|
| 2560 |
+
],
|
| 2561 |
+
"license": "MIT",
|
| 2562 |
+
"dependencies": {
|
| 2563 |
+
"escalade": "^3.2.0",
|
| 2564 |
+
"picocolors": "^1.1.1"
|
| 2565 |
+
},
|
| 2566 |
+
"bin": {
|
| 2567 |
+
"update-browserslist-db": "cli.js"
|
| 2568 |
+
},
|
| 2569 |
+
"peerDependencies": {
|
| 2570 |
+
"browserslist": ">= 4.21.0"
|
| 2571 |
+
}
|
| 2572 |
+
},
|
| 2573 |
+
"node_modules/vite": {
|
| 2574 |
+
"version": "6.2.5",
|
| 2575 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz",
|
| 2576 |
+
"integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==",
|
| 2577 |
+
"dev": true,
|
| 2578 |
+
"license": "MIT",
|
| 2579 |
+
"dependencies": {
|
| 2580 |
+
"esbuild": "^0.25.0",
|
| 2581 |
+
"postcss": "^8.5.3",
|
| 2582 |
+
"rollup": "^4.30.1"
|
| 2583 |
+
},
|
| 2584 |
+
"bin": {
|
| 2585 |
+
"vite": "bin/vite.js"
|
| 2586 |
+
},
|
| 2587 |
+
"engines": {
|
| 2588 |
+
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
|
| 2589 |
+
},
|
| 2590 |
+
"funding": {
|
| 2591 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 2592 |
+
},
|
| 2593 |
+
"optionalDependencies": {
|
| 2594 |
+
"fsevents": "~2.3.3"
|
| 2595 |
+
},
|
| 2596 |
+
"peerDependencies": {
|
| 2597 |
+
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
| 2598 |
+
"jiti": ">=1.21.0",
|
| 2599 |
+
"less": "*",
|
| 2600 |
+
"lightningcss": "^1.21.0",
|
| 2601 |
+
"sass": "*",
|
| 2602 |
+
"sass-embedded": "*",
|
| 2603 |
+
"stylus": "*",
|
| 2604 |
+
"sugarss": "*",
|
| 2605 |
+
"terser": "^5.16.0",
|
| 2606 |
+
"tsx": "^4.8.1",
|
| 2607 |
+
"yaml": "^2.4.2"
|
| 2608 |
+
},
|
| 2609 |
+
"peerDependenciesMeta": {
|
| 2610 |
+
"@types/node": {
|
| 2611 |
+
"optional": true
|
| 2612 |
+
},
|
| 2613 |
+
"jiti": {
|
| 2614 |
+
"optional": true
|
| 2615 |
+
},
|
| 2616 |
+
"less": {
|
| 2617 |
+
"optional": true
|
| 2618 |
+
},
|
| 2619 |
+
"lightningcss": {
|
| 2620 |
+
"optional": true
|
| 2621 |
+
},
|
| 2622 |
+
"sass": {
|
| 2623 |
+
"optional": true
|
| 2624 |
+
},
|
| 2625 |
+
"sass-embedded": {
|
| 2626 |
+
"optional": true
|
| 2627 |
+
},
|
| 2628 |
+
"stylus": {
|
| 2629 |
+
"optional": true
|
| 2630 |
+
},
|
| 2631 |
+
"sugarss": {
|
| 2632 |
+
"optional": true
|
| 2633 |
+
},
|
| 2634 |
+
"terser": {
|
| 2635 |
+
"optional": true
|
| 2636 |
+
},
|
| 2637 |
+
"tsx": {
|
| 2638 |
+
"optional": true
|
| 2639 |
+
},
|
| 2640 |
+
"yaml": {
|
| 2641 |
+
"optional": true
|
| 2642 |
+
}
|
| 2643 |
+
}
|
| 2644 |
+
},
|
| 2645 |
+
"node_modules/vite-hot-client": {
|
| 2646 |
+
"version": "0.2.4",
|
| 2647 |
+
"resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-0.2.4.tgz",
|
| 2648 |
+
"integrity": "sha512-a1nzURqO7DDmnXqabFOliz908FRmIppkBKsJthS8rbe8hBEXwEwe4C3Pp33Z1JoFCYfVL4kTOMLKk0ZZxREIeA==",
|
| 2649 |
+
"dev": true,
|
| 2650 |
+
"license": "MIT",
|
| 2651 |
+
"funding": {
|
| 2652 |
+
"url": "https://github.com/sponsors/antfu"
|
| 2653 |
+
},
|
| 2654 |
+
"peerDependencies": {
|
| 2655 |
+
"vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0"
|
| 2656 |
+
}
|
| 2657 |
+
},
|
| 2658 |
+
"node_modules/vite-plugin-inspect": {
|
| 2659 |
+
"version": "0.8.9",
|
| 2660 |
+
"resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.9.tgz",
|
| 2661 |
+
"integrity": "sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A==",
|
| 2662 |
+
"dev": true,
|
| 2663 |
+
"license": "MIT",
|
| 2664 |
+
"dependencies": {
|
| 2665 |
+
"@antfu/utils": "^0.7.10",
|
| 2666 |
+
"@rollup/pluginutils": "^5.1.3",
|
| 2667 |
+
"debug": "^4.3.7",
|
| 2668 |
+
"error-stack-parser-es": "^0.1.5",
|
| 2669 |
+
"fs-extra": "^11.2.0",
|
| 2670 |
+
"open": "^10.1.0",
|
| 2671 |
+
"perfect-debounce": "^1.0.0",
|
| 2672 |
+
"picocolors": "^1.1.1",
|
| 2673 |
+
"sirv": "^3.0.0"
|
| 2674 |
+
},
|
| 2675 |
+
"engines": {
|
| 2676 |
+
"node": ">=14"
|
| 2677 |
+
},
|
| 2678 |
+
"funding": {
|
| 2679 |
+
"url": "https://github.com/sponsors/antfu"
|
| 2680 |
+
},
|
| 2681 |
+
"peerDependencies": {
|
| 2682 |
+
"vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1"
|
| 2683 |
+
},
|
| 2684 |
+
"peerDependenciesMeta": {
|
| 2685 |
+
"@nuxt/kit": {
|
| 2686 |
+
"optional": true
|
| 2687 |
+
}
|
| 2688 |
+
}
|
| 2689 |
+
},
|
| 2690 |
+
"node_modules/vite-plugin-vue-devtools": {
|
| 2691 |
+
"version": "7.7.2",
|
| 2692 |
+
"resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.7.2.tgz",
|
| 2693 |
+
"integrity": "sha512-5V0UijQWiSBj32blkyPEqIbzc6HO9c1bwnBhx+ay2dzU0FakH+qMdNUT8nF9BvDE+i6I1U8CqCuJiO20vKEdQw==",
|
| 2694 |
+
"dev": true,
|
| 2695 |
+
"license": "MIT",
|
| 2696 |
+
"dependencies": {
|
| 2697 |
+
"@vue/devtools-core": "^7.7.2",
|
| 2698 |
+
"@vue/devtools-kit": "^7.7.2",
|
| 2699 |
+
"@vue/devtools-shared": "^7.7.2",
|
| 2700 |
+
"execa": "^9.5.1",
|
| 2701 |
+
"sirv": "^3.0.0",
|
| 2702 |
+
"vite-plugin-inspect": "0.8.9",
|
| 2703 |
+
"vite-plugin-vue-inspector": "^5.3.1"
|
| 2704 |
+
},
|
| 2705 |
+
"engines": {
|
| 2706 |
+
"node": ">=v14.21.3"
|
| 2707 |
+
},
|
| 2708 |
+
"peerDependencies": {
|
| 2709 |
+
"vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0"
|
| 2710 |
+
}
|
| 2711 |
+
},
|
| 2712 |
+
"node_modules/vite-plugin-vue-inspector": {
|
| 2713 |
+
"version": "5.3.1",
|
| 2714 |
+
"resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.1.tgz",
|
| 2715 |
+
"integrity": "sha512-cBk172kZKTdvGpJuzCCLg8lJ909wopwsu3Ve9FsL1XsnLBiRT9U3MePcqrgGHgCX2ZgkqZmAGR8taxw+TV6s7A==",
|
| 2716 |
+
"dev": true,
|
| 2717 |
+
"license": "MIT",
|
| 2718 |
+
"dependencies": {
|
| 2719 |
+
"@babel/core": "^7.23.0",
|
| 2720 |
+
"@babel/plugin-proposal-decorators": "^7.23.0",
|
| 2721 |
+
"@babel/plugin-syntax-import-attributes": "^7.22.5",
|
| 2722 |
+
"@babel/plugin-syntax-import-meta": "^7.10.4",
|
| 2723 |
+
"@babel/plugin-transform-typescript": "^7.22.15",
|
| 2724 |
+
"@vue/babel-plugin-jsx": "^1.1.5",
|
| 2725 |
+
"@vue/compiler-dom": "^3.3.4",
|
| 2726 |
+
"kolorist": "^1.8.0",
|
| 2727 |
+
"magic-string": "^0.30.4"
|
| 2728 |
+
},
|
| 2729 |
+
"peerDependencies": {
|
| 2730 |
+
"vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0"
|
| 2731 |
+
}
|
| 2732 |
+
},
|
| 2733 |
+
"node_modules/vue": {
|
| 2734 |
+
"version": "3.5.13",
|
| 2735 |
+
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
| 2736 |
+
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
|
| 2737 |
+
"license": "MIT",
|
| 2738 |
+
"dependencies": {
|
| 2739 |
+
"@vue/compiler-dom": "3.5.13",
|
| 2740 |
+
"@vue/compiler-sfc": "3.5.13",
|
| 2741 |
+
"@vue/runtime-dom": "3.5.13",
|
| 2742 |
+
"@vue/server-renderer": "3.5.13",
|
| 2743 |
+
"@vue/shared": "3.5.13"
|
| 2744 |
+
},
|
| 2745 |
+
"peerDependencies": {
|
| 2746 |
+
"typescript": "*"
|
| 2747 |
+
},
|
| 2748 |
+
"peerDependenciesMeta": {
|
| 2749 |
+
"typescript": {
|
| 2750 |
+
"optional": true
|
| 2751 |
+
}
|
| 2752 |
+
}
|
| 2753 |
+
},
|
| 2754 |
+
"node_modules/vue-router": {
|
| 2755 |
+
"version": "4.5.0",
|
| 2756 |
+
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz",
|
| 2757 |
+
"integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==",
|
| 2758 |
+
"license": "MIT",
|
| 2759 |
+
"dependencies": {
|
| 2760 |
+
"@vue/devtools-api": "^6.6.4"
|
| 2761 |
+
},
|
| 2762 |
+
"funding": {
|
| 2763 |
+
"url": "https://github.com/sponsors/posva"
|
| 2764 |
+
},
|
| 2765 |
+
"peerDependencies": {
|
| 2766 |
+
"vue": "^3.2.0"
|
| 2767 |
+
}
|
| 2768 |
+
},
|
| 2769 |
+
"node_modules/vue-router/node_modules/@vue/devtools-api": {
|
| 2770 |
+
"version": "6.6.4",
|
| 2771 |
+
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
|
| 2772 |
+
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
| 2773 |
+
"license": "MIT"
|
| 2774 |
+
},
|
| 2775 |
+
"node_modules/which": {
|
| 2776 |
+
"version": "2.0.2",
|
| 2777 |
+
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
| 2778 |
+
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
| 2779 |
+
"dev": true,
|
| 2780 |
+
"license": "ISC",
|
| 2781 |
+
"dependencies": {
|
| 2782 |
+
"isexe": "^2.0.0"
|
| 2783 |
+
},
|
| 2784 |
+
"bin": {
|
| 2785 |
+
"node-which": "bin/node-which"
|
| 2786 |
+
},
|
| 2787 |
+
"engines": {
|
| 2788 |
+
"node": ">= 8"
|
| 2789 |
+
}
|
| 2790 |
+
},
|
| 2791 |
+
"node_modules/yallist": {
|
| 2792 |
+
"version": "3.1.1",
|
| 2793 |
+
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
| 2794 |
+
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
| 2795 |
+
"dev": true,
|
| 2796 |
+
"license": "ISC"
|
| 2797 |
+
},
|
| 2798 |
+
"node_modules/yoctocolors": {
|
| 2799 |
+
"version": "2.1.1",
|
| 2800 |
+
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
|
| 2801 |
+
"integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
|
| 2802 |
+
"dev": true,
|
| 2803 |
+
"license": "MIT",
|
| 2804 |
+
"engines": {
|
| 2805 |
+
"node": ">=18"
|
| 2806 |
+
},
|
| 2807 |
+
"funding": {
|
| 2808 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2809 |
+
}
|
| 2810 |
+
}
|
| 2811 |
+
}
|
| 2812 |
+
}
|
hajimiUI/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "hajimiui",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"build:app": "node build.js",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"hajimiui": "file:",
|
| 14 |
+
"pinia": "^3.0.1",
|
| 15 |
+
"vue": "^3.5.13",
|
| 16 |
+
"vue-router": "^4.5.0"
|
| 17 |
+
},
|
| 18 |
+
"devDependencies": {
|
| 19 |
+
"@vitejs/plugin-vue": "^5.2.3",
|
| 20 |
+
"vite": "^6.2.4",
|
| 21 |
+
"vite-plugin-vue-devtools": "^7.7.2"
|
| 22 |
+
}
|
| 23 |
+
}
|
hajimiUI/public/favicon.ico
ADDED
|
|
hajimiUI/src/App.vue
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup>
|
| 2 |
+
import { RouterView } from 'vue-router'
|
| 3 |
+
</script>
|
| 4 |
+
|
| 5 |
+
<template>
|
| 6 |
+
<RouterView />
|
| 7 |
+
</template>
|
| 8 |
+
|
| 9 |
+
<style>
|
| 10 |
+
body {
|
| 11 |
+
margin: 0;
|
| 12 |
+
padding: 0;
|
| 13 |
+
}
|
| 14 |
+
</style>
|
hajimiUI/src/assets/base.css
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* color palette from <https://github.com/vuejs/theme> */
|
| 2 |
+
:root {
|
| 3 |
+
--vt-c-white: #ffffff;
|
| 4 |
+
--vt-c-white-soft: #f8f8f8;
|
| 5 |
+
--vt-c-white-mute: #f2f2f2;
|
| 6 |
+
|
| 7 |
+
--vt-c-black: #181818;
|
| 8 |
+
--vt-c-black-soft: #222222;
|
| 9 |
+
--vt-c-black-mute: #282828;
|
| 10 |
+
|
| 11 |
+
--vt-c-indigo: #2c3e50;
|
| 12 |
+
|
| 13 |
+
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
| 14 |
+
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
| 15 |
+
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
| 16 |
+
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
| 17 |
+
|
| 18 |
+
--vt-c-text-light-1: var(--vt-c-indigo);
|
| 19 |
+
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
| 20 |
+
--vt-c-text-dark-1: var(--vt-c-white);
|
| 21 |
+
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* semantic color variables for this project */
|
| 25 |
+
:root {
|
| 26 |
+
--color-background: var(--vt-c-white);
|
| 27 |
+
--color-background-soft: var(--vt-c-white-soft);
|
| 28 |
+
--color-background-mute: var(--vt-c-white-mute);
|
| 29 |
+
|
| 30 |
+
--color-border: var(--vt-c-divider-light-2);
|
| 31 |
+
--color-border-hover: var(--vt-c-divider-light-1);
|
| 32 |
+
|
| 33 |
+
--color-heading: var(--vt-c-text-light-1);
|
| 34 |
+
--color-text: var(--vt-c-text-light-1);
|
| 35 |
+
|
| 36 |
+
--section-gap: 160px;
|
| 37 |
+
|
| 38 |
+
/* 自定义颜色变量 */
|
| 39 |
+
--card-background: #ffffff;
|
| 40 |
+
--card-border: #e0e0e0;
|
| 41 |
+
--button-primary: #007bff;
|
| 42 |
+
--button-primary-hover: #0069d9;
|
| 43 |
+
--button-text: #ffffff;
|
| 44 |
+
--stats-item-bg: #f8f9fa;
|
| 45 |
+
--log-entry-bg: #f8f9fa;
|
| 46 |
+
--log-entry-border: #e9ecef;
|
| 47 |
+
--toggle-bg: #ccc;
|
| 48 |
+
--toggle-active: #007bff;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* 夜间模式变量 */
|
| 52 |
+
.dark-mode {
|
| 53 |
+
--color-background: var(--vt-c-black);
|
| 54 |
+
--color-background-soft: var(--vt-c-black-soft);
|
| 55 |
+
--color-background-mute: var(--vt-c-black-mute);
|
| 56 |
+
|
| 57 |
+
--color-border: var(--vt-c-divider-dark-2);
|
| 58 |
+
--color-border-hover: var(--vt-c-divider-dark-1);
|
| 59 |
+
|
| 60 |
+
--color-heading: var(--vt-c-text-dark-1);
|
| 61 |
+
--color-text: var(--vt-c-text-dark-2);
|
| 62 |
+
|
| 63 |
+
/* 自定义夜间模式颜色 */
|
| 64 |
+
--card-background: #2d2d2d;
|
| 65 |
+
--card-border: #444444;
|
| 66 |
+
--button-primary: #0056b3;
|
| 67 |
+
--button-primary-hover: #004494;
|
| 68 |
+
--button-text: #ffffff;
|
| 69 |
+
--stats-item-bg: #333333;
|
| 70 |
+
--log-entry-bg: #333333;
|
| 71 |
+
--log-entry-border: #444444;
|
| 72 |
+
--toggle-bg: #555555;
|
| 73 |
+
--toggle-active: #0056b3;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* 保留系统偏好设置的支持 */
|
| 77 |
+
@media (prefers-color-scheme: dark) {
|
| 78 |
+
:root:not(.dark-mode):not(.light-mode) {
|
| 79 |
+
--color-background: var(--vt-c-black);
|
| 80 |
+
--color-background-soft: var(--vt-c-black-soft);
|
| 81 |
+
--color-background-mute: var(--vt-c-black-mute);
|
| 82 |
+
|
| 83 |
+
--color-border: var(--vt-c-divider-dark-2);
|
| 84 |
+
--color-border-hover: var(--vt-c-divider-dark-1);
|
| 85 |
+
|
| 86 |
+
--color-heading: var(--vt-c-text-dark-1);
|
| 87 |
+
--color-text: var(--vt-c-text-dark-2);
|
| 88 |
+
|
| 89 |
+
/* 自定义夜间模式颜色 */
|
| 90 |
+
--card-background: #2d2d2d;
|
| 91 |
+
--card-border: #444444;
|
| 92 |
+
--button-primary: #0056b3;
|
| 93 |
+
--button-primary-hover: #004494;
|
| 94 |
+
--button-text: #ffffff;
|
| 95 |
+
--stats-item-bg: #333333;
|
| 96 |
+
--log-entry-bg: #333333;
|
| 97 |
+
--log-entry-border: #444444;
|
| 98 |
+
--toggle-bg: #555555;
|
| 99 |
+
--toggle-active: #0056b3;
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
*,
|
| 104 |
+
*::before,
|
| 105 |
+
*::after {
|
| 106 |
+
box-sizing: border-box;
|
| 107 |
+
margin: 0;
|
| 108 |
+
font-weight: normal;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
body {
|
| 112 |
+
min-height: 100vh;
|
| 113 |
+
color: var(--color-text);
|
| 114 |
+
background: var(--color-background);
|
| 115 |
+
transition:
|
| 116 |
+
color 0.5s,
|
| 117 |
+
background-color 0.5s;
|
| 118 |
+
line-height: 1.6;
|
| 119 |
+
font-family:
|
| 120 |
+
Inter,
|
| 121 |
+
-apple-system,
|
| 122 |
+
BlinkMacSystemFont,
|
| 123 |
+
'Segoe UI',
|
| 124 |
+
Roboto,
|
| 125 |
+
Oxygen,
|
| 126 |
+
Ubuntu,
|
| 127 |
+
Cantarell,
|
| 128 |
+
'Fira Sans',
|
| 129 |
+
'Droid Sans',
|
| 130 |
+
'Helvetica Neue',
|
| 131 |
+
sans-serif;
|
| 132 |
+
font-size: 15px;
|
| 133 |
+
text-rendering: optimizeLegibility;
|
| 134 |
+
-webkit-font-smoothing: antialiased;
|
| 135 |
+
-moz-osx-font-smoothing: grayscale;
|
| 136 |
+
}
|