linchuans commited on
Commit
3e35e18
·
verified ·
1 Parent(s): dac5f8f

Upload 76 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +3 -0
  2. .github/workflows/main.yml +56 -0
  3. Dockerfile +20 -0
  4. Dockerfile_git +14 -0
  5. app/__init__.py +0 -0
  6. app/api/__init__.py +9 -0
  7. app/api/auth.py +14 -0
  8. app/api/client_disconnect.py +120 -0
  9. app/api/dashboard.py +182 -0
  10. app/api/gemini_handlers.py +59 -0
  11. app/api/nonstream_handlers.py +157 -0
  12. app/api/request_handlers.py +154 -0
  13. app/api/routes.py +217 -0
  14. app/api/stream_handlers.py +284 -0
  15. app/config/__init__.py +3 -0
  16. app/config/safety.py +49 -0
  17. app/config/settings.py +81 -0
  18. app/main.py +283 -0
  19. app/models/__init__.py +19 -0
  20. app/models/schemas.py +46 -0
  21. app/services/__init__.py +7 -0
  22. app/services/gemini.py +367 -0
  23. app/templates/__init__.py +1 -0
  24. app/templates/assets/favicon.ico +0 -0
  25. app/templates/assets/index.css +1 -0
  26. app/templates/assets/index.html +14 -0
  27. app/templates/assets/main.js +0 -0
  28. app/templates/index.html +15 -0
  29. app/utils/__init__.py +12 -0
  30. app/utils/api_key.py +88 -0
  31. app/utils/cache.py +137 -0
  32. app/utils/error_handling.py +120 -0
  33. app/utils/logging.py +72 -0
  34. app/utils/maintenance.py +96 -0
  35. app/utils/rate_limiting.py +36 -0
  36. app/utils/request.py +72 -0
  37. app/utils/response.py +59 -0
  38. app/utils/stats.py +185 -0
  39. app/utils/version.py +49 -0
  40. hajimiUI/.gitignore +30 -0
  41. hajimiUI/.vscode/extensions.json +3 -0
  42. hajimiUI/README.md +29 -0
  43. hajimiUI/build.js +56 -0
  44. hajimiUI/index.html +13 -0
  45. hajimiUI/jsconfig.json +8 -0
  46. hajimiUI/package-lock.json +2812 -0
  47. hajimiUI/package.json +23 -0
  48. hajimiUI/public/favicon.ico +0 -0
  49. hajimiUI/src/App.vue +14 -0
  50. 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
+ }