From dedc3b41830d5b6962b2b27c49b6d39690bd714d Mon Sep 17 00:00:00 2001 From: houhuan Date: Tue, 5 May 2026 11:59:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20complete=20web=20application=20?= =?UTF-8?q?=E2=80=94=20FastAPI=20backend=20+=20Vue=203=20SPA=20frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Full FastAPI backend with JWT auth, file management, processing pipeline, memory CRUD, barcode mappings, config management, cloud sync - Vue 3 + Element Plus frontend with dashboard, task history, HTTP logs, memory editor, barcode editor, config editor, sync page - HTTP request logging middleware with SQLite persistence - Task history tracking with progress and retry support - File metadata recording for upload/download operations - WebAuth section in config.ini for bcrypt password storage - Bug fix: logs.py count query returns tuple not dict Co-Authored-By: Claude Opus 4.7 --- .gitignore | 18 +- config.ini | 7 +- web/backend/__init__.py | 0 web/backend/auth/__init__.py | 0 web/backend/auth/dependencies.py | 58 + web/backend/auth/jwt_handler.py | 19 + web/backend/auth/router.py | 89 ++ web/backend/config.py | 40 + web/backend/main.py | 109 ++ web/backend/routers/__init__.py | 0 web/backend/routers/barcodes.py | 124 ++ web/backend/routers/config_api.py | 98 ++ web/backend/routers/logs.py | 2 +- web/backend/routers/memory.py | 165 ++ web/backend/routers/processing.py | 250 +++ web/backend/routers/sync.py | 93 ++ web/backend/routers/websocket.py | 47 + web/backend/schemas/__init__.py | 0 web/backend/services/__init__.py | 0 web/backend/services/db_pool.py | 20 + web/backend/services/service_wrapper.py | 19 + web/frontend/index.html | 13 + web/frontend/package-lock.json | 1929 +++++++++++++++++++++++ web/frontend/package.json | 25 + web/frontend/src/App.vue | 22 + web/frontend/src/api/index.ts | 29 + web/frontend/src/env.d.ts | 4 + web/frontend/src/main.ts | 17 + web/frontend/src/router/index.ts | 67 + web/frontend/src/stores/auth.ts | 34 + web/frontend/src/stores/processing.ts | 94 ++ web/frontend/src/styles/global.css | 172 ++ web/frontend/src/views/Barcodes.vue | 274 ++++ web/frontend/src/views/Config.vue | 245 +++ web/frontend/src/views/Dashboard.vue | 719 +++++++++ web/frontend/src/views/Layout.vue | 379 +++++ web/frontend/src/views/Login.vue | 241 +++ web/frontend/src/views/Logs.vue | 302 ++++ web/frontend/src/views/Memory.vue | 380 +++++ web/frontend/src/views/Sync.vue | 371 +++++ web/frontend/src/views/Tasks.vue | 432 +++++ web/frontend/src/vite-env.d.ts | 7 + web/frontend/tsconfig.json | 24 + web/frontend/tsconfig.node.json | 10 + web/frontend/vite.config.ts | 23 + web/requirements-web.txt | 9 + 46 files changed, 6971 insertions(+), 9 deletions(-) create mode 100644 web/backend/__init__.py create mode 100644 web/backend/auth/__init__.py create mode 100644 web/backend/auth/dependencies.py create mode 100644 web/backend/auth/jwt_handler.py create mode 100644 web/backend/auth/router.py create mode 100644 web/backend/config.py create mode 100644 web/backend/main.py create mode 100644 web/backend/routers/__init__.py create mode 100644 web/backend/routers/barcodes.py create mode 100644 web/backend/routers/config_api.py create mode 100644 web/backend/routers/memory.py create mode 100644 web/backend/routers/processing.py create mode 100644 web/backend/routers/sync.py create mode 100644 web/backend/routers/websocket.py create mode 100644 web/backend/schemas/__init__.py create mode 100644 web/backend/services/__init__.py create mode 100644 web/backend/services/db_pool.py create mode 100644 web/backend/services/service_wrapper.py create mode 100644 web/frontend/index.html create mode 100644 web/frontend/package-lock.json create mode 100644 web/frontend/package.json create mode 100644 web/frontend/src/App.vue create mode 100644 web/frontend/src/api/index.ts create mode 100644 web/frontend/src/env.d.ts create mode 100644 web/frontend/src/main.ts create mode 100644 web/frontend/src/router/index.ts create mode 100644 web/frontend/src/stores/auth.ts create mode 100644 web/frontend/src/stores/processing.ts create mode 100644 web/frontend/src/styles/global.css create mode 100644 web/frontend/src/views/Barcodes.vue create mode 100644 web/frontend/src/views/Config.vue create mode 100644 web/frontend/src/views/Dashboard.vue create mode 100644 web/frontend/src/views/Layout.vue create mode 100644 web/frontend/src/views/Login.vue create mode 100644 web/frontend/src/views/Logs.vue create mode 100644 web/frontend/src/views/Memory.vue create mode 100644 web/frontend/src/views/Sync.vue create mode 100644 web/frontend/src/views/Tasks.vue create mode 100644 web/frontend/src/vite-env.d.ts create mode 100644 web/frontend/tsconfig.json create mode 100644 web/frontend/tsconfig.node.json create mode 100644 web/frontend/vite.config.ts create mode 100644 web/requirements-web.txt diff --git a/.gitignore b/.gitignore index c861b2b..f0ef255 100644 --- a/.gitignore +++ b/.gitignore @@ -18,13 +18,8 @@ release/ logs/ data/temp/ -# Runtime outputs -data/output/ -data/result/ -data/input/ -data/product_cache.db -data/user_settings.json -*.db +# Runtime data (all runtime outputs, caches, databases) +data/ # Claude Code / IDE .claude/ @@ -34,6 +29,15 @@ data/user_settings.json # Old project wework_xiaoai_bot/ +# Node.js +node_modules/ + +# Frontend build output +web/backend/static/ + +# Screenshots (from testing) +*.png + # OS/IDE .DS_Store Thumbs.db diff --git a/config.ini b/config.ini index 25e1ef9..589d7db 100644 --- a/config.ini +++ b/config.ini @@ -40,4 +40,9 @@ version = 2026.05.05.0239 base_url = https://gitea.94kan.cn owner = houhuan repo = yixuan-sync-data -token = 50b61e43a141d606ae2529cd1755bc666d800e08 \ No newline at end of file +token = 50b61e43a141d606ae2529cd1755bc666d800e08 + +[WebAuth] +username = admin +password_hash = $2b$12$nllT8o1QIMfWKuTlpQI3G./E2NS.gqf0EHZyNkJ8gMpVa9grTXRoC + diff --git a/web/backend/__init__.py b/web/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/backend/auth/__init__.py b/web/backend/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/backend/auth/dependencies.py b/web/backend/auth/dependencies.py new file mode 100644 index 0000000..d9ddf90 --- /dev/null +++ b/web/backend/auth/dependencies.py @@ -0,0 +1,58 @@ +"""FastAPI auth dependencies""" + +from fastapi import Depends, HTTPException, status, Query, Request +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + +from .jwt_handler import decode_token + +security = HTTPBearer() + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), +) -> dict: + try: + payload = decode_token(credentials.credentials) + username = payload.get("sub") + if username is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + return {"username": username} + except Exception: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证凭据") + + +async def get_current_user_ws(token: str = Query(...)) -> dict: + """WebSocket auth via query parameter""" + try: + payload = decode_token(token) + username = payload.get("sub") + if username is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + return {"username": username} + except Exception: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证凭据") + + +async def get_current_user_flexible( + request: Request, + credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False)), + token: str = Query(None), +) -> dict: + """Auth from header OR query param (for file downloads in browser).""" + token_str = None + if credentials: + token_str = credentials.credentials + elif token: + token_str = token + + if not token_str: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="未提供认证凭据") + + try: + payload = decode_token(token_str) + username = payload.get("sub") + if username is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) + return {"username": username} + except Exception: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证凭据") diff --git a/web/backend/auth/jwt_handler.py b/web/backend/auth/jwt_handler.py new file mode 100644 index 0000000..c08fa0d --- /dev/null +++ b/web/backend/auth/jwt_handler.py @@ -0,0 +1,19 @@ +"""JWT token creation and validation""" + +from datetime import datetime, timedelta, timezone +from typing import Optional + +from jose import jwt, JWTError + +from ..config import get_or_generate_secret, JWT_ALGORITHM, JWT_EXPIRE_HOURS + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + to_encode = data.copy() + expire = datetime.now(timezone.utc) + (expires_delta or timedelta(hours=JWT_EXPIRE_HOURS)) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, get_or_generate_secret(), algorithm=JWT_ALGORITHM) + + +def decode_token(token: str) -> dict: + return jwt.decode(token, get_or_generate_secret(), algorithms=[JWT_ALGORITHM]) diff --git a/web/backend/auth/router.py b/web/backend/auth/router.py new file mode 100644 index 0000000..81051ea --- /dev/null +++ b/web/backend/auth/router.py @@ -0,0 +1,89 @@ +"""Auth API endpoints""" + +import os +import bcrypt +from fastapi import APIRouter, HTTPException, Depends, status +from pydantic import BaseModel + +from .jwt_handler import create_access_token +from .dependencies import get_current_user + +router = APIRouter(prefix="/api/auth", tags=["auth"]) + +# Default credentials (should be changed on first login) +DEFAULT_USERNAME = "admin" +DEFAULT_PASSWORD = "admin123" + + +class LoginRequest(BaseModel): + username: str + password: str + + +class LoginResponse(BaseModel): + access_token: str + token_type: str = "bearer" + + +def _get_credentials() -> tuple[str, bytes]: + """Get username and password hash from config or defaults""" + try: + from app.config.settings import ConfigManager + cfg = ConfigManager() + username = cfg.get('WebAuth', 'username', fallback=DEFAULT_USERNAME) + pw_hash = cfg.get('WebAuth', 'password_hash', fallback='') + if not pw_hash: + # First run: store default password hash + pw_hash = bcrypt.hashpw(DEFAULT_PASSWORD.encode(), bcrypt.gensalt()).decode() + try: + cfg.update('WebAuth', 'username', DEFAULT_USERNAME) + cfg.update('WebAuth', 'password_hash', pw_hash) + cfg.save_config() + except Exception: + pass + return username, pw_hash.encode() + except Exception: + return DEFAULT_USERNAME, bcrypt.hashpw(DEFAULT_PASSWORD.encode(), bcrypt.gensalt()) + + +@router.post("/login", response_model=LoginResponse) +async def login(req: LoginRequest): + stored_username, stored_hash = _get_credentials() + + if req.username != stored_username: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误") + + if not bcrypt.checkpw(req.password.encode(), stored_hash): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户名或密码错误") + + token = create_access_token({"sub": req.username}) + return LoginResponse(access_token=token) + + +@router.get("/me") +async def me(current_user: dict = Depends(get_current_user)): + return current_user + + +class ChangePasswordRequest(BaseModel): + old_password: str + new_password: str + + +@router.post("/change-password") +async def change_password(req: ChangePasswordRequest, current_user: dict = Depends(get_current_user)): + _, stored_hash = _get_credentials() + + if not bcrypt.checkpw(req.old_password.encode(), stored_hash): + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="旧密码错误") + + new_hash = bcrypt.hashpw(req.new_password.encode(), bcrypt.gensalt()).decode() + try: + from app.config.settings import ConfigManager + cfg = ConfigManager() + cfg.update('WebAuth', 'password_hash', new_hash) + cfg.save_config() + except Exception as e: + raise HTTPException(status_code=500, detail=f"保存密码失败: {e}") + + return {"message": "密码修改成功"} diff --git a/web/backend/config.py b/web/backend/config.py new file mode 100644 index 0000000..dc9034e --- /dev/null +++ b/web/backend/config.py @@ -0,0 +1,40 @@ +"""Web-specific configuration""" + +import os +import secrets + +# JWT +JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "") +JWT_ALGORITHM = "HS256" +JWT_EXPIRE_HOURS = 24 + +# File upload +MAX_UPLOAD_SIZE = 50 * 1024 * 1024 # 50MB +ALLOWED_IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp'} +ALLOWED_EXCEL_EXTENSIONS = {'.xlsx', '.xls'} +ALLOWED_EXTENSIONS = ALLOWED_IMAGE_EXTENSIONS | ALLOWED_EXCEL_EXTENSIONS + +# CORS +CORS_ORIGINS = os.getenv("CORS_ORIGINS", "*").split(",") + +# Auth rate limit +LOGIN_RATE_LIMIT = 5 # per minute + + +def get_or_generate_secret() -> str: + """Get JWT secret from env or auto-generate on first run""" + global JWT_SECRET_KEY + if not JWT_SECRET_KEY: + secret_file = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), + 'data', '.jwt_secret' + ) + if os.path.exists(secret_file): + with open(secret_file, 'r') as f: + JWT_SECRET_KEY = f.read().strip() + if not JWT_SECRET_KEY: + JWT_SECRET_KEY = secrets.token_urlsafe(48) + os.makedirs(os.path.dirname(secret_file), exist_ok=True) + with open(secret_file, 'w') as f: + f.write(JWT_SECRET_KEY) + return JWT_SECRET_KEY diff --git a/web/backend/main.py b/web/backend/main.py new file mode 100644 index 0000000..ec82082 --- /dev/null +++ b/web/backend/main.py @@ -0,0 +1,109 @@ +"""FastAPI application entry point for the web-based OCR order processing system.""" + +import sys +import os +from contextlib import asynccontextmanager +from pathlib import Path + +# Ensure app/ is importable +_web_dir = Path(__file__).resolve().parent.parent # web/ +_project_root = _web_dir.parent # project root +if str(_project_root) not in sys.path: + sys.path.insert(0, str(_project_root)) + +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse + +from .config import get_or_generate_secret # noqa: trigger secret generation +from .services.task_manager import TaskManager +from .services.db_pool import DBPool +from .auth.router import router as auth_router +from .routers.files import router as files_router +from .routers.processing import router as processing_router +from .routers.memory import router as memory_router +from .routers.config_api import router as config_router +from .routers.barcodes import router as barcodes_router +from .routers.sync import router as sync_router +from .routers.websocket import router as ws_router +from .routers.logs import router as logs_router +from .routers.tasks import router as tasks_router +from .middleware.logging import LoggingMiddleware + +# Shared singletons +task_manager = TaskManager() +db_pool = DBPool() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Initialize shared resources on startup.""" + from app.config.settings import ConfigManager + ConfigManager() + + # Initialize DB and cleanup old records + from .services.db_schema import init_db, cleanup_old_records + init_db() + cleanup_old_records() + + # Wire up DB pool to task manager + task_manager.set_db_pool(db_pool) + + app.state.task_manager = task_manager + app.state.db_pool = db_pool + yield + + +app = FastAPI( + title="益选 OCR 订单处理系统", + version="1.0.0", + lifespan=lifespan, +) + +# CORS +from fastapi.middleware.cors import CORSMiddleware + +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:5173", "http://127.0.0.1:5173", "http://localhost:8000"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# HTTP logging middleware (after CORS, before routes) +app.add_middleware(LoggingMiddleware) + +# Make task_manager and db_pool accessible via request.state +@app.middleware("http") +async def inject_services(request, call_next): + request.state.task_manager = task_manager + request.state.db_pool = db_pool + return await call_next(request) + + +# Mount routers +app.include_router(auth_router) +app.include_router(files_router) +app.include_router(processing_router) +app.include_router(memory_router) +app.include_router(config_router) +app.include_router(barcodes_router) +app.include_router(sync_router) +app.include_router(ws_router) +app.include_router(logs_router) +app.include_router(tasks_router) + + +# Serve Vue SPA static files +_static_dir = Path(__file__).resolve().parent / "static" +if _static_dir.is_dir(): + app.mount("/assets", StaticFiles(directory=str(_static_dir / "assets")), name="assets") + + @app.get("/{full_path:path}") + async def serve_spa(full_path: str): + """Catch-all: serve index.html for Vue Router history mode.""" + file_path = _static_dir / full_path + if file_path.is_file(): + return FileResponse(str(file_path)) + return FileResponse(str(_static_dir / "index.html")) diff --git a/web/backend/routers/__init__.py b/web/backend/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/backend/routers/barcodes.py b/web/backend/routers/barcodes.py new file mode 100644 index 0000000..ff9b92a --- /dev/null +++ b/web/backend/routers/barcodes.py @@ -0,0 +1,124 @@ +"""Barcode mapping CRUD endpoints.""" + +import json +from pathlib import Path +from typing import Dict, Optional, List + +from fastapi import APIRouter, HTTPException, Depends +from pydantic import BaseModel + +from ..auth.dependencies import get_current_user + +router = APIRouter(prefix="/api/barcodes", tags=["barcodes"]) + +_project_root = Path(__file__).resolve().parent.parent.parent.parent +_mappings_file = _project_root / "config" / "barcode_mappings.json" + + +class BarcodeMapping(BaseModel): + barcode: str + target: str + description: Optional[str] = None + + +class BarcodeMappingUpdate(BaseModel): + target: Optional[str] = None + description: Optional[str] = None + + +def _load_mappings() -> Dict: + if not _mappings_file.is_file(): + return {} + try: + return json.loads(_mappings_file.read_text(encoding="utf-8")) + except Exception: + return {} + + +def _save_mappings(data: Dict): + _mappings_file.parent.mkdir(parents=True, exist_ok=True) + _mappings_file.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + + +@router.get("") +async def list_barcodes( + search: str = "", + current_user: dict = Depends(get_current_user), +): + mappings = _load_mappings() + items = [] + for barcode, info in mappings.items(): + if isinstance(info, dict): + target = info.get("map_to", info.get("target", "")) + desc = info.get("description", "") + else: + target = str(info) + desc = "" + if search and search not in barcode and search not in target and search not in desc: + continue + items.append({"barcode": barcode, "target": target, "description": desc}) + return {"items": items, "total": len(items)} + + +@router.get("/{barcode}") +async def get_barcode( + barcode: str, + current_user: dict = Depends(get_current_user), +): + mappings = _load_mappings() + if barcode not in mappings: + raise HTTPException(404, f"未找到条码映射 {barcode}") + info = mappings[barcode] + if isinstance(info, dict): + return {"barcode": barcode, "target": info.get("map_to", info.get("target", "")), "description": info.get("description", "")} + return {"barcode": barcode, "target": str(info), "description": ""} + + +@router.post("") +async def create_barcode( + body: BarcodeMapping, + current_user: dict = Depends(get_current_user), +): + mappings = _load_mappings() + if body.barcode in mappings: + raise HTTPException(409, f"条码 {body.barcode} 已存在") + mappings[body.barcode] = {"map_to": body.target, "description": body.description or ""} + _save_mappings(mappings) + return {"message": f"已创建映射 {body.barcode} → {body.target}"} + + +@router.put("/{barcode}") +async def update_barcode( + barcode: str, + body: BarcodeMappingUpdate, + current_user: dict = Depends(get_current_user), +): + mappings = _load_mappings() + if barcode not in mappings: + raise HTTPException(404, f"未找到条码映射 {barcode}") + + existing = mappings[barcode] + if not isinstance(existing, dict): + existing = {"map_to": str(existing), "description": ""} + + if body.target is not None: + existing["map_to"] = body.target + if body.description is not None: + existing["description"] = body.description + + mappings[barcode] = existing + _save_mappings(mappings) + return {"message": f"已更新映射 {barcode}"} + + +@router.delete("/{barcode}") +async def delete_barcode( + barcode: str, + current_user: dict = Depends(get_current_user), +): + mappings = _load_mappings() + if barcode not in mappings: + raise HTTPException(404, f"未找到条码映射 {barcode}") + del mappings[barcode] + _save_mappings(mappings) + return {"message": f"已删除映射 {barcode}"} diff --git a/web/backend/routers/config_api.py b/web/backend/routers/config_api.py new file mode 100644 index 0000000..2aeecdb --- /dev/null +++ b/web/backend/routers/config_api.py @@ -0,0 +1,98 @@ +"""Configuration read/write endpoints.""" + +from typing import Dict, Optional, Any +from fastapi import APIRouter, HTTPException, Depends +from pydantic import BaseModel + +from ..auth.dependencies import get_current_user + +router = APIRouter(prefix="/api/config", tags=["config"]) + +# Keys that should be masked in GET responses +_SENSITIVE_KEYS = {"api_key", "secret_key", "token", "password", "api_secret", "access_key"} + +# Sections to expose (match actual config.ini) +_ALLOWED_SECTIONS = {"API", "Paths", "Performance", "File", "Templates", "Gitea", "WebAuth"} + + +class ConfigUpdate(BaseModel): + section: str + key: str + value: str + + +class ConfigBulkUpdate(BaseModel): + updates: list[ConfigUpdate] + + +def _get_config(): + from app.config.settings import ConfigManager + return ConfigManager() + + +def _mask_value(key: str, value: str) -> str: + if any(s in key.lower() for s in _SENSITIVE_KEYS): + if len(value) > 4: + return value[:2] + "*" * (len(value) - 4) + value[-2:] + return "****" + return value + + +@router.get("") +async def get_config( + section: Optional[str] = None, + current_user: dict = Depends(get_current_user), +): + cfg = _get_config() + if section: + if section not in _ALLOWED_SECTIONS and section != "DEFAULT": + raise HTTPException(403, f"不允许访问配置节: {section}") + items = {} + for key, value in cfg.config.items(section): + items[key] = _mask_value(key, value) + return {"section": section, "items": items} + + result = {} + for sec in _ALLOWED_SECTIONS: + try: + items = {} + for key, value in cfg.config.items(sec): + items[key] = _mask_value(key, value) + result[sec] = items + except Exception: + pass + return result + + +@router.put("") +async def update_config( + body: ConfigUpdate, + current_user: dict = Depends(get_current_user), +): + if body.section not in _ALLOWED_SECTIONS: + raise HTTPException(403, f"不允许修改配置节: {body.section}") + + cfg = _get_config() + try: + cfg.update(body.section, body.key, body.value) + cfg.save_config() + return {"message": f"已更新 [{body.section}] {body.key}"} + except Exception as e: + raise HTTPException(500, f"保存失败: {e}") + + +@router.put("/bulk") +async def bulk_update_config( + body: ConfigBulkUpdate, + current_user: dict = Depends(get_current_user), +): + cfg = _get_config() + updated = [] + for item in body.updates: + if item.section not in _ALLOWED_SECTIONS: + continue + cfg.update(item.section, item.key, item.value) + updated.append(f"[{item.section}] {item.key}") + + cfg.save_config() + return {"message": f"已更新 {len(updated)} 项", "updated": updated} diff --git a/web/backend/routers/logs.py b/web/backend/routers/logs.py index b4b7f11..88c1636 100644 --- a/web/backend/routers/logs.py +++ b/web/backend/routers/logs.py @@ -49,7 +49,7 @@ def _count_http_logs( row = conn.execute( f"SELECT COUNT(*) as cnt FROM http_logs{where}", params ).fetchone() - return row["cnt"] if row else 0 + return row[0] if row else 0 finally: conn.close() diff --git a/web/backend/routers/memory.py b/web/backend/routers/memory.py new file mode 100644 index 0000000..389c9e5 --- /dev/null +++ b/web/backend/routers/memory.py @@ -0,0 +1,165 @@ +"""Product memory CRUD endpoints.""" + +from typing import Optional, List, Dict +from pathlib import Path + +from fastapi import APIRouter, HTTPException, Depends, Query +from pydantic import BaseModel + +from ..auth.dependencies import get_current_user + +router = APIRouter(prefix="/api/memory", tags=["memory"]) + +_project_root = Path(__file__).resolve().parent.parent.parent.parent +_db_path = str(_project_root / "data" / "product_cache.db") +_excel_source = str(_project_root / "templates" / "商品资料.xlsx") + + +class MemoryItem(BaseModel): + barcode: str + name: str + spec: Optional[str] = None + unit: Optional[str] = None + price: Optional[float] = None + confidence: int = 0 + source: str = "ocr" + last_used: Optional[str] = None + use_count: int = 0 + + +class MemoryUpdate(BaseModel): + name: Optional[str] = None + spec: Optional[str] = None + unit: Optional[str] = None + price: Optional[float] = None + confidence: Optional[int] = None + + +class MemoryListResponse(BaseModel): + items: List[MemoryItem] + total: int + page: int + page_size: int + + +def _get_db(): + from app.core.db.product_db import ProductDatabase + return ProductDatabase(_db_path, _excel_source) + + +def _row_to_item(row: Dict) -> MemoryItem: + return MemoryItem( + barcode=row.get("barcode", ""), + name=row.get("name", ""), + spec=row.get("spec"), + unit=row.get("unit"), + price=row.get("price"), + confidence=row.get("confidence", 0), + source=row.get("source", "ocr"), + last_used=row.get("last_used"), + use_count=row.get("use_count", 0), + ) + + +@router.get("", response_model=MemoryListResponse) +async def list_memory( + search: str = "", + page: int = Query(1, ge=1), + page_size: int = Query(50, ge=1, le=200), + current_user: dict = Depends(get_current_user), +): + db = _get_db() + results = db.get_all_memories() + + if search: + s = search.lower() + results = [r for r in results if s in r.get("barcode", "").lower() or s in r.get("name", "").lower()] + + total = len(results) + start = (page - 1) * page_size + page_items = results[start:start + page_size] + + return MemoryListResponse( + items=[_row_to_item(r) for r in page_items], + total=total, + page=page, + page_size=page_size, + ) + + +@router.get("/{barcode}") +async def get_memory( + barcode: str, + current_user: dict = Depends(get_current_user), +): + db = _get_db() + product = db.get_memory(barcode) + if not product: + raise HTTPException(404, f"未找到条码 {barcode} 的记忆记录") + return product + + +@router.put("/{barcode}") +async def update_memory( + barcode: str, + body: MemoryUpdate, + current_user: dict = Depends(get_current_user), +): + db = _get_db() + existing = db.get_memory(barcode) + if not existing: + raise HTTPException(404, f"未找到条码 {barcode}") + + update_data = body.model_dump(exclude_none=True) + if not update_data: + raise HTTPException(400, "没有提供更新数据") + + db.update_memory(barcode, update_data) + return {"message": f"已更新 {barcode}", "updated_fields": list(update_data.keys())} + + +@router.delete("/{barcode}") +async def delete_memory( + barcode: str, + current_user: dict = Depends(get_current_user), +): + db = _get_db() + existing = db.get_memory(barcode) + if not existing: + raise HTTPException(404, f"未找到条码 {barcode}") + db.delete_memory(barcode) + return {"message": f"已删除 {barcode}"} + + +@router.post("/reimport") +async def reimport_memory( + current_user: dict = Depends(get_current_user), +): + db = _get_db() + try: + count = db.reimport() + return {"message": f"重新导入完成,共导入 {count} 条记录", "count": count} + except Exception as e: + raise HTTPException(500, f"导入失败: {e}") + + +@router.get("/export/sync") +async def export_memory( + current_user: dict = Depends(get_current_user), +): + db = _get_db() + data = db.export_for_sync() + return {"data": data, "count": len(data)} + + +@router.post("/import/sync") +async def import_memory( + data: dict, + current_user: dict = Depends(get_current_user), +): + db = _get_db() + try: + count = db.import_from_sync(data.get("data", [])) + return {"message": f"导入完成,共 {count} 条", "count": count} + except Exception as e: + raise HTTPException(500, f"导入失败: {e}") diff --git a/web/backend/routers/processing.py b/web/backend/routers/processing.py new file mode 100644 index 0000000..7237b7c --- /dev/null +++ b/web/backend/routers/processing.py @@ -0,0 +1,250 @@ +"""Processing endpoints: OCR, Excel conversion, merge, and full pipeline.""" + +import os +import sys +import traceback +from pathlib import Path +from typing import Optional, List + +from fastapi import APIRouter, HTTPException, Depends, Request +from pydantic import BaseModel + +from ..auth.dependencies import get_current_user +from ..services.service_wrapper import ServiceWrapper + +router = APIRouter(prefix="/api/processing", tags=["processing"]) + +_wrapper = ServiceWrapper(max_workers=3) + +_project_root = Path(__file__).resolve().parent.parent.parent.parent +_input_dir = _project_root / "data" / "input" +_output_dir = _project_root / "data" / "output" +_result_dir = _project_root / "data" / "result" + + +class PipelineRequest(BaseModel): + files: Optional[List[str]] = None # specific files, or None = all in input/ + supplier: Optional[str] = None # force supplier type + + +class TaskResponse(BaseModel): + task_id: str + status: str + message: str + + +def _get_task_manager(request: Request): + return request.state.task_manager + + +def _list_input_files(filter_ext: Optional[List[str]] = None) -> List[Path]: + if not _input_dir.is_dir(): + return [] + files = [] + for f in sorted(_input_dir.iterdir()): + if f.is_file(): + if filter_ext is None or f.suffix.lower() in filter_ext: + files.append(f) + return files + + +@router.post("/ocr-batch", response_model=TaskResponse) +async def ocr_batch( + request: Request, + current_user: dict = Depends(get_current_user), +): + """Run OCR on all images in input/.""" + tm = _get_task_manager(request) + task = tm.create_task("批量OCR识别") + + image_exts = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'} + files = _list_input_files(filter_ext=list(image_exts)) + if not files: + raise HTTPException(400, "input/ 目录中没有图片文件") + + async def _run(): + try: + from app.services.ocr_service import OCRService + svc = OCRService() + total = len(files) + for i, f in enumerate(files): + tm.update_progress(task.id, int((i / total) * 100), f"正在识别: {f.name}") + tm.add_log(task.id, f"[OCR] 处理 {f.name}") + try: + svc.process_single(str(f), str(_output_dir)) + tm.add_log(task.id, f"[OCR] 完成: {f.name}") + except Exception as e: + tm.add_log(task.id, f"[OCR] 失败: {f.name} - {e}") + result_files = [f.name for f in _output_dir.iterdir() if f.is_file()] + tm.set_completed(task.id, result_files=result_files, message=f"OCR完成,共处理 {total} 个文件") + except Exception as e: + tm.set_failed(task.id, str(e)) + + import asyncio + asyncio.create_task(_run()) + + return TaskResponse(task_id=task.id, status="accepted", message="OCR任务已创建") + + +@router.post("/excel", response_model=TaskResponse) +async def process_excel( + request: Request, + body: PipelineRequest = PipelineRequest(), + current_user: dict = Depends(get_current_user), +): + """Convert OCR output Excel files to standardized format.""" + tm = _get_task_manager(request) + task = tm.create_task("Excel标准化处理") + + excel_exts = {'.xls', '.xlsx'} + if body.files: + files = [_output_dir / f for f in body.files if (_output_dir / f).is_file()] + else: + files = _list_input_files(filter_ext=list(excel_exts)) + if not files: + files = _list_input_files_from(_output_dir, filter_ext=list(excel_exts)) + + if not files: + raise HTTPException(400, "没有找到Excel文件") + + async def _run(): + try: + from app.services.order_service import OrderService + svc = OrderService() + total = len(files) + for i, f in enumerate(files): + tm.update_progress(task.id, int((i / total) * 100), f"正在处理: {f.name}") + tm.add_log(task.id, f"[Excel] 处理 {f.name}") + try: + svc.process_excel(str(f), str(_result_dir)) + tm.add_log(task.id, f"[Excel] 完成: {f.name}") + except Exception as e: + tm.add_log(task.id, f"[Excel] 失败: {f.name} - {e}") + result_files = [f.name for f in _result_dir.iterdir() if f.is_file()] + tm.set_completed(task.id, result_files=result_files, message=f"Excel处理完成,共 {total} 个文件") + except Exception as e: + tm.set_failed(task.id, str(e)) + + import asyncio + asyncio.create_task(_run()) + + return TaskResponse(task_id=task.id, status="accepted", message="Excel处理任务已创建") + + +@router.post("/merge", response_model=TaskResponse) +async def merge_orders( + request: Request, + current_user: dict = Depends(get_current_user), +): + """Merge all processed Excel files into a single purchase order.""" + tm = _get_task_manager(request) + task = tm.create_task("合并采购单") + + async def _run(): + try: + from app.services.order_service import OrderService + svc = OrderService() + tm.update_progress(task.id, 20, "正在合并采购单...") + tm.add_log(task.id, "[合并] 开始合并") + result = svc.merge_orders(str(_result_dir)) + tm.add_log(task.id, f"[合并] 完成: {result}") + tm.set_completed(task.id, result_files=[result] if result else [], message="合并完成") + except Exception as e: + tm.set_failed(task.id, str(e)) + + import asyncio + asyncio.create_task(_run()) + + return TaskResponse(task_id=task.id, status="accepted", message="合并任务已创建") + + +@router.post("/pipeline", response_model=TaskResponse) +async def full_pipeline( + request: Request, + body: PipelineRequest = PipelineRequest(), + current_user: dict = Depends(get_current_user), +): + """Run the full pipeline: OCR → Excel → Merge.""" + tm = _get_task_manager(request) + task = tm.create_task("一键全流程处理") + + async def _run(): + try: + # Step 1: OCR + tm.update_progress(task.id, 0, "步骤 1/3: OCR识别") + tm.add_log(task.id, "[Pipeline] 开始OCR识别") + from app.services.ocr_service import OCRService + ocr_svc = OCRService() + + image_exts = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.tif'} + images = _list_input_files(filter_ext=list(image_exts)) + for i, f in enumerate(images): + pct = int((i / max(len(images), 1)) * 30) + tm.update_progress(task.id, pct, f"OCR: {f.name}") + try: + ocr_svc.process_single(str(f), str(_output_dir)) + tm.add_log(task.id, f"[OCR] 完成: {f.name}") + except Exception as e: + tm.add_log(task.id, f"[OCR] 失败: {f.name} - {e}") + + # Step 2: Excel conversion + tm.update_progress(task.id, 35, "步骤 2/3: Excel标准化") + tm.add_log(task.id, "[Pipeline] 开始Excel处理") + from app.services.order_service import OrderService + order_svc = OrderService() + + excel_files = list(_output_dir.glob("*.xls")) + list(_output_dir.glob("*.xlsx")) + for i, f in enumerate(excel_files): + pct = 35 + int((i / max(len(excel_files), 1)) * 35) + tm.update_progress(task.id, pct, f"Excel: {f.name}") + try: + order_svc.process_excel(str(f), str(_result_dir)) + tm.add_log(task.id, f"[Excel] 完成: {f.name}") + except Exception as e: + tm.add_log(task.id, f"[Excel] 失败: {f.name} - {e}") + + # Step 3: Merge + tm.update_progress(task.id, 75, "步骤 3/3: 合并采购单") + tm.add_log(task.id, "[Pipeline] 开始合并") + try: + result = order_svc.merge_orders(str(_result_dir)) + tm.add_log(task.id, f"[合并] 完成: {result}") + except Exception as e: + tm.add_log(task.id, f"[合并] 失败: {e}") + result = None + + result_files = [f.name for f in _result_dir.iterdir() if f.is_file()] + tm.set_completed(task.id, result_files=result_files, message="全流程处理完成") + except Exception as e: + tb = traceback.format_exc() + tm.add_log(task.id, f"[错误] {tb}") + tm.set_failed(task.id, str(e)) + + import asyncio + asyncio.create_task(_run()) + + return TaskResponse(task_id=task.id, status="accepted", message="全流程任务已创建") + + +@router.get("/status/{task_id}") +async def get_task_status( + task_id: str, + request: Request, + current_user: dict = Depends(get_current_user), +): + tm = _get_task_manager(request) + task = tm.get_task(task_id) + if not task: + raise HTTPException(404, "任务不存在") + return task.to_dict() + + +def _list_input_files_from(directory: Path, filter_ext: List[str] = None) -> List[Path]: + if not directory.is_dir(): + return [] + files = [] + for f in sorted(directory.iterdir()): + if f.is_file(): + if filter_ext is None or f.suffix.lower() in filter_ext: + files.append(f) + return files diff --git a/web/backend/routers/sync.py b/web/backend/routers/sync.py new file mode 100644 index 0000000..2f70935 --- /dev/null +++ b/web/backend/routers/sync.py @@ -0,0 +1,93 @@ +"""Cloud sync endpoints (Gitea-based).""" + +from pathlib import Path + +from fastapi import APIRouter, HTTPException, Depends, Request +from pydantic import BaseModel + +from ..auth.dependencies import get_current_user +from ..services.task_manager import TaskManager + +router = APIRouter(prefix="/api/sync", tags=["sync"]) + +_project_root = Path(__file__).resolve().parent.parent.parent.parent + + +class SyncResponse(BaseModel): + task_id: str + status: str + message: str + + +def _get_sync(): + from app.core.utils.cloud_sync import GiteaSync + from app.config.settings import ConfigManager + cfg = ConfigManager() + return GiteaSync(cfg) + + +@router.post("/push", response_model=SyncResponse) +async def sync_push( + request: Request, + current_user: dict = Depends(get_current_user), +): + tm = request.state.task_manager + task = tm.create_task("推送到云端") + + async def _run(): + try: + tm.update_progress(task.id, 10, "正在初始化同步...") + sync = _get_sync() + tm.update_progress(task.id, 30, "正在推送文件...") + tm.add_log(task.id, "[Push] 开始推送") + result = sync.push() + tm.add_log(task.id, f"[Push] 完成: {result}") + tm.set_completed(task.id, message="推送完成") + except Exception as e: + tm.set_failed(task.id, str(e)) + + import asyncio + asyncio.create_task(_run()) + return SyncResponse(task_id=task.id, status="accepted", message="推送任务已创建") + + +@router.post("/pull", response_model=SyncResponse) +async def sync_pull( + request: Request, + current_user: dict = Depends(get_current_user), +): + tm = request.state.task_manager + task = tm.create_task("从云端拉取") + + async def _run(): + try: + tm.update_progress(task.id, 10, "正在初始化同步...") + sync = _get_sync() + tm.update_progress(task.id, 30, "正在拉取文件...") + tm.add_log(task.id, "[Pull] 开始拉取") + result = sync.pull() + tm.add_log(task.id, f"[Pull] 完成: {result}") + tm.set_completed(task.id, message="拉取完成") + except Exception as e: + tm.set_failed(task.id, str(e)) + + import asyncio + asyncio.create_task(_run()) + return SyncResponse(task_id=task.id, status="accepted", message="拉取任务已创建") + + +@router.get("/status") +async def sync_status( + current_user: dict = Depends(get_current_user), +): + try: + from app.config.settings import ConfigManager + cfg = ConfigManager() + base_url = cfg.get("Gitea", "base_url", fallback="") + owner = cfg.get("Gitea", "owner", fallback="") + repo = cfg.get("Gitea", "repo", fallback="") + enabled = bool(base_url and owner and repo) + repo_url = f"{base_url}/{owner}/{repo}" if enabled else "" + return {"enabled": enabled, "repo_url": repo_url} + except Exception: + return {"enabled": False, "repo_url": ""} diff --git a/web/backend/routers/websocket.py b/web/backend/routers/websocket.py new file mode 100644 index 0000000..d7cdb0f --- /dev/null +++ b/web/backend/routers/websocket.py @@ -0,0 +1,47 @@ +"""WebSocket endpoint for real-time task progress.""" + +from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query +from ..auth.jwt_handler import decode_token +from jose import JWTError + +router = APIRouter(tags=["websocket"]) + + +@router.websocket("/ws/task/{task_id}") +async def task_websocket( + websocket: WebSocket, + task_id: str, + token: str = Query(...), +): + """WebSocket for real-time task progress updates.""" + try: + payload = decode_token(token) + username = payload.get("sub") + if not username: + await websocket.close(code=4001, reason="Invalid token") + return + except (JWTError, Exception): + await websocket.close(code=4001, reason="Invalid token") + return + + await websocket.accept() + + tm = websocket.app.state.task_manager + task = tm.get_task(task_id) + if not task: + await websocket.send_json({"error": "任务不存在"}) + await websocket.close() + return + + tm.subscribe(task_id, websocket) + await websocket.send_json(task.to_dict()) + + try: + while True: + data = await websocket.receive_text() + if data == "ping": + await websocket.send_text("pong") + except WebSocketDisconnect: + tm.unsubscribe(task_id, websocket) + except Exception: + tm.unsubscribe(task_id, websocket) diff --git a/web/backend/schemas/__init__.py b/web/backend/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/backend/services/__init__.py b/web/backend/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/web/backend/services/db_pool.py b/web/backend/services/db_pool.py new file mode 100644 index 0000000..85c2ac4 --- /dev/null +++ b/web/backend/services/db_pool.py @@ -0,0 +1,20 @@ +"""SQLite write serialization for async context""" + +import asyncio +from typing import Callable, Any + + +class DBPool: + """Serializes SQLite writes via asyncio.Lock. Reads are concurrent.""" + + def __init__(self): + self._write_lock = asyncio.Lock() + + async def execute_write(self, fn: Callable, *args, **kwargs) -> Any: + async with self._write_lock: + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, lambda: fn(*args, **kwargs)) + + async def execute_read(self, fn: Callable, *args, **kwargs) -> Any: + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, lambda: fn(*args, **kwargs)) diff --git a/web/backend/services/service_wrapper.py b/web/backend/services/service_wrapper.py new file mode 100644 index 0000000..ea44be3 --- /dev/null +++ b/web/backend/services/service_wrapper.py @@ -0,0 +1,19 @@ +"""Async wrapper for synchronous app/ services""" + +import asyncio +from concurrent.futures import ThreadPoolExecutor +from typing import Callable, Any + + +class ServiceWrapper: + """Wraps synchronous services for async FastAPI endpoints.""" + + def __init__(self, max_workers: int = 3): + self._executor = ThreadPoolExecutor(max_workers=max_workers) + + async def run_sync(self, fn: Callable, *args, **kwargs) -> Any: + loop = asyncio.get_event_loop() + return await loop.run_in_executor( + self._executor, + lambda: fn(*args, **kwargs) + ) diff --git a/web/frontend/index.html b/web/frontend/index.html new file mode 100644 index 0000000..77259c5 --- /dev/null +++ b/web/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + 益选 OCR 订单处理系统 + + +
+ + + diff --git a/web/frontend/package-lock.json b/web/frontend/package-lock.json new file mode 100644 index 0000000..853072b --- /dev/null +++ b/web/frontend/package-lock.json @@ -0,0 +1,1929 @@ +{ + "name": "xiaoaitext-web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "xiaoaitext-web", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.0", + "axios": "^1.6.0", + "element-plus": "^2.5.0", + "pinia": "^2.1.0", + "vue": "^3.4.0", + "vue-router": "^4.2.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "typescript": "^5.3.0", + "vite": "^5.1.0", + "vue-tsc": "^2.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz", + "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.33", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz", + "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz", + "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.33", + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.10", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz", + "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz", + "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz", + "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/shared": "3.5.33" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz", + "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.33", + "@vue/runtime-core": "3.5.33", + "@vue/shared": "3.5.33", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz", + "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "vue": "3.5.33" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz", + "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.0.0.tgz", + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.0.0.tgz", + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.0.0.tgz", + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.13.7", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.7.tgz", + "integrity": "sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "12.0.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.2.4" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.33", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz", + "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.33", + "@vue/compiler-sfc": "3.5.33", + "@vue/runtime-dom": "3.5.33", + "@vue/server-renderer": "3.5.33", + "@vue/shared": "3.5.33" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.8.tgz", + "integrity": "sha512-9689efAXhN/EV86plgkL/XFiJSXhGtWPG6JDboZ+QnjlUWUUQrQ0ILKQtw4iQsuwIwu5k6Aw+JnehDe7161e7A==", + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/web/frontend/package.json b/web/frontend/package.json new file mode 100644 index 0000000..5df930c --- /dev/null +++ b/web/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "xiaoaitext-web", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.4.0", + "vue-router": "^4.2.0", + "pinia": "^2.1.0", + "axios": "^1.6.0", + "element-plus": "^2.5.0", + "@element-plus/icons-vue": "^2.3.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.0.0", + "typescript": "^5.3.0", + "vite": "^5.1.0", + "vue-tsc": "^2.0.0" + } +} diff --git a/web/frontend/src/App.vue b/web/frontend/src/App.vue new file mode 100644 index 0000000..4767500 --- /dev/null +++ b/web/frontend/src/App.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/web/frontend/src/api/index.ts b/web/frontend/src/api/index.ts new file mode 100644 index 0000000..424e49e --- /dev/null +++ b/web/frontend/src/api/index.ts @@ -0,0 +1,29 @@ +import axios from 'axios' + +const api = axios.create({ + baseURL: '/api', + timeout: 30000, +}) + +// Request interceptor: attach JWT token +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +// Response interceptor: handle 401 +api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('token') + window.location.href = '/login' + } + return Promise.reject(error) + } +) + +export default api diff --git a/web/frontend/src/env.d.ts b/web/frontend/src/env.d.ts new file mode 100644 index 0000000..185515e --- /dev/null +++ b/web/frontend/src/env.d.ts @@ -0,0 +1,4 @@ +declare module 'element-plus/dist/locale/zh-cn.mjs' { + const locale: any + export default locale +} diff --git a/web/frontend/src/main.ts b/web/frontend/src/main.ts new file mode 100644 index 0000000..61e07e9 --- /dev/null +++ b/web/frontend/src/main.ts @@ -0,0 +1,17 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import zhCn from 'element-plus/dist/locale/zh-cn.mjs' +import './styles/global.css' + +import App from './App.vue' +import router from './router' + +const app = createApp(App) + +app.use(createPinia()) +app.use(router) +app.use(ElementPlus, { locale: zhCn }) + +app.mount('#app') diff --git a/web/frontend/src/router/index.ts b/web/frontend/src/router/index.ts new file mode 100644 index 0000000..7355f3c --- /dev/null +++ b/web/frontend/src/router/index.ts @@ -0,0 +1,67 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthStore } from '../stores/auth' + +const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/login', + name: 'Login', + component: () => import('../views/Login.vue'), + meta: { requiresAuth: false }, + }, + { + path: '/', + component: () => import('../views/Layout.vue'), + meta: { requiresAuth: true }, + children: [ + { + path: '', + name: 'Dashboard', + component: () => import('../views/Dashboard.vue'), + }, + { + path: 'memory', + name: 'Memory', + component: () => import('../views/Memory.vue'), + }, + { + path: 'barcodes', + name: 'Barcodes', + component: () => import('../views/Barcodes.vue'), + }, + { + path: 'config', + name: 'Config', + component: () => import('../views/Config.vue'), + }, + { + path: 'sync', + name: 'Sync', + component: () => import('../views/Sync.vue'), + }, + { + path: 'tasks', + name: 'Tasks', + component: () => import('../views/Tasks.vue'), + }, + { + path: 'logs', + name: 'Logs', + component: () => import('../views/Logs.vue'), + }, + ], + }, + ], +}) + +router.beforeEach((to, from, next) => { + const authStore = useAuthStore() + if (to.meta.requiresAuth !== false && !authStore.isAuthenticated) { + next('/login') + } else { + next() + } +}) + +export default router diff --git a/web/frontend/src/stores/auth.ts b/web/frontend/src/stores/auth.ts new file mode 100644 index 0000000..44737a5 --- /dev/null +++ b/web/frontend/src/stores/auth.ts @@ -0,0 +1,34 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import api from '../api' + +export const useAuthStore = defineStore('auth', () => { + const token = ref(localStorage.getItem('token') || '') + const username = ref('') + + const isAuthenticated = computed(() => !!token.value) + + async function login(user: string, password: string) { + const res = await api.post('/auth/login', { username: user, password }) + token.value = res.data.access_token + localStorage.setItem('token', token.value) + username.value = user + } + + function logout() { + token.value = '' + username.value = '' + localStorage.removeItem('token') + } + + async function fetchUser() { + try { + const res = await api.get('/auth/me') + username.value = res.data.username + } catch { + logout() + } + } + + return { token, username, isAuthenticated, login, logout, fetchUser } +}) diff --git a/web/frontend/src/stores/processing.ts b/web/frontend/src/stores/processing.ts new file mode 100644 index 0000000..b13e293 --- /dev/null +++ b/web/frontend/src/stores/processing.ts @@ -0,0 +1,94 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import api from '../api' + +export interface TaskInfo { + task_id: string + name: string + status: string + progress: number + message: string + result_files: string[] + error: string | null + log_lines: string[] +} + +export const useProcessingStore = defineStore('processing', () => { + const currentTask = ref(null) + const tasks = ref([]) + const logs = ref([]) + + let ws: WebSocket | null = null + + function connectWebSocket(taskId: string) { + disconnectWebSocket() + + const token = localStorage.getItem('token') + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const host = window.location.host + const url = `${protocol}//${host}/ws/task/${taskId}?token=${token}` + + ws = new WebSocket(url) + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + currentTask.value = data + logs.value = data.log_lines || [] + + // Update in tasks list + const idx = tasks.value.findIndex(t => t.task_id === data.task_id) + if (idx >= 0) { + tasks.value[idx] = data + } else { + tasks.value.unshift(data) + } + + // Auto-disconnect on completion + if (data.status === 'completed' || data.status === 'failed') { + setTimeout(() => disconnectWebSocket(), 2000) + } + } catch {} + } + + ws.onerror = () => { + console.error('WebSocket error') + } + + ws.onclose = () => { + ws = null + } + } + + function disconnectWebSocket() { + if (ws) { + ws.close() + ws = null + } + } + + async function startTask(endpoint: string, body?: any) { + const res = await api.post(endpoint, body || {}) + const taskId = res.data.task_id + currentTask.value = { + task_id: taskId, + name: res.data.message || '', + status: 'pending', + progress: 0, + message: '', + result_files: [], + error: null, + log_lines: [], + } + logs.value = [] + connectWebSocket(taskId) + return taskId + } + + async function pollTaskStatus(taskId: string) { + const res = await api.get(`/processing/status/${taskId}`) + return res.data + } + + return { currentTask, tasks, logs, connectWebSocket, disconnectWebSocket, startTask, pollTaskStatus } +}) diff --git a/web/frontend/src/styles/global.css b/web/frontend/src/styles/global.css new file mode 100644 index 0000000..9d0a12e --- /dev/null +++ b/web/frontend/src/styles/global.css @@ -0,0 +1,172 @@ +/* ═══════════════════════════════════════════ + 益选 OCR — Industrial Command Center Theme + ═══════════════════════════════════════════ */ + +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,500;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap'); + +:root { + /* Core palette */ + --bg-dark: #0f1117; + --bg-sidebar: #161922; + --bg-card: #ffffff; + --bg-page: #f0f2f5; + --bg-hover: #f7f8fa; + + /* Amber accent system */ + --amber-50: #fff8e1; + --amber-100: #ffecb3; + --amber-400: #ffca28; + --amber-500: #ffc107; + --amber-600: #ffb300; + + /* Semantic */ + --primary: #2563eb; + --primary-hover: #1d4ed8; + --success: #10b981; + --warning: #f59e0b; + --danger: #ef4444; + --info: #6366f1; + + /* Text */ + --text-primary: #111827; + --text-secondary: #6b7280; + --text-muted: #9ca3af; + --text-inverse: #e5e7eb; + --text-sidebar: #94a3b8; + --text-sidebar-active: #ffffff; + + /* Borders & shadows */ + --border-light: #e5e7eb; + --border-subtle: #f3f4f6; + --shadow-sm: 0 1px 2px rgba(0,0,0,0.04); + --shadow-md: 0 4px 12px rgba(0,0,0,0.06); + --shadow-lg: 0 8px 24px rgba(0,0,0,0.08); + --shadow-glow: 0 0 20px rgba(255,193,7,0.15); + + /* Typography */ + --font-sans: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif; + --font-mono: 'JetBrains Mono', 'Fira Code', monospace; + + /* Transitions */ + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: var(--font-sans); + background: var(--bg-page); + color: var(--text-primary); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Element Plus overrides */ +.el-card { + border: 1px solid var(--border-light) !important; + border-radius: 12px !important; + box-shadow: var(--shadow-sm) !important; + transition: box-shadow 0.2s var(--ease-out) !important; +} + +.el-card:hover { + box-shadow: var(--shadow-md) !important; +} + +.el-button--primary { + background: var(--primary) !important; + border-color: var(--primary) !important; + font-weight: 500; + border-radius: 8px; + transition: all 0.2s var(--ease-out); +} + +.el-button--primary:hover { + background: var(--primary-hover) !important; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(37,99,235,0.3); +} + +.el-table { + --el-table-border-color: var(--border-light); + --el-table-header-bg-color: #fafbfc; + border-radius: 8px; + overflow: hidden; +} + +.el-table th.el-table__cell { + font-weight: 600; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-secondary); +} + +.el-progress-bar__outer { + border-radius: 6px; +} + +.el-progress-bar__inner { + border-radius: 6px; + transition: width 0.4s var(--ease-out); +} + +.el-tag { + border-radius: 6px; + font-weight: 500; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #9ca3af; +} + +/* Animations */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 0 0 rgba(255,193,7,0); } + 50% { box-shadow: 0 0 16px 4px rgba(255,193,7,0.15); } +} + +.animate-in { + animation: fadeInUp 0.4s var(--ease-out) both; +} + +.animate-in-delay-1 { animation-delay: 0.05s; } +.animate-in-delay-2 { animation-delay: 0.1s; } +.animate-in-delay-3 { animation-delay: 0.15s; } +.animate-in-delay-4 { animation-delay: 0.2s; } diff --git a/web/frontend/src/views/Barcodes.vue b/web/frontend/src/views/Barcodes.vue new file mode 100644 index 0000000..3bad38f --- /dev/null +++ b/web/frontend/src/views/Barcodes.vue @@ -0,0 +1,274 @@ + + + + + diff --git a/web/frontend/src/views/Config.vue b/web/frontend/src/views/Config.vue new file mode 100644 index 0000000..e545e6a --- /dev/null +++ b/web/frontend/src/views/Config.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/web/frontend/src/views/Dashboard.vue b/web/frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..09e5679 --- /dev/null +++ b/web/frontend/src/views/Dashboard.vue @@ -0,0 +1,719 @@ + + + + + diff --git a/web/frontend/src/views/Layout.vue b/web/frontend/src/views/Layout.vue new file mode 100644 index 0000000..031a116 --- /dev/null +++ b/web/frontend/src/views/Layout.vue @@ -0,0 +1,379 @@ + + + + + diff --git a/web/frontend/src/views/Login.vue b/web/frontend/src/views/Login.vue new file mode 100644 index 0000000..9d5eeb8 --- /dev/null +++ b/web/frontend/src/views/Login.vue @@ -0,0 +1,241 @@ + + + + + diff --git a/web/frontend/src/views/Logs.vue b/web/frontend/src/views/Logs.vue new file mode 100644 index 0000000..efacd5a --- /dev/null +++ b/web/frontend/src/views/Logs.vue @@ -0,0 +1,302 @@ + + + + + diff --git a/web/frontend/src/views/Memory.vue b/web/frontend/src/views/Memory.vue new file mode 100644 index 0000000..fc5d7a8 --- /dev/null +++ b/web/frontend/src/views/Memory.vue @@ -0,0 +1,380 @@ + + + + + diff --git a/web/frontend/src/views/Sync.vue b/web/frontend/src/views/Sync.vue new file mode 100644 index 0000000..236f51f --- /dev/null +++ b/web/frontend/src/views/Sync.vue @@ -0,0 +1,371 @@ + + + + + diff --git a/web/frontend/src/views/Tasks.vue b/web/frontend/src/views/Tasks.vue new file mode 100644 index 0000000..7559170 --- /dev/null +++ b/web/frontend/src/views/Tasks.vue @@ -0,0 +1,432 @@ + + + + + diff --git a/web/frontend/src/vite-env.d.ts b/web/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..323c78a --- /dev/null +++ b/web/frontend/src/vite-env.d.ts @@ -0,0 +1,7 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} diff --git a/web/frontend/tsconfig.json b/web/frontend/tsconfig.json new file mode 100644 index 0000000..072649b --- /dev/null +++ b/web/frontend/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/web/frontend/tsconfig.node.json b/web/frontend/tsconfig.node.json new file mode 100644 index 0000000..42872c5 --- /dev/null +++ b/web/frontend/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/frontend/vite.config.ts b/web/frontend/vite.config.ts new file mode 100644 index 0000000..f325c96 --- /dev/null +++ b/web/frontend/vite.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +export default defineConfig({ + plugins: [vue()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8000', + changeOrigin: true, + }, + '/ws': { + target: 'ws://localhost:8000', + ws: true, + }, + }, + }, + build: { + outDir: '../backend/static', + emptyOutDir: true, + }, +}) diff --git a/web/requirements-web.txt b/web/requirements-web.txt new file mode 100644 index 0000000..3235396 --- /dev/null +++ b/web/requirements-web.txt @@ -0,0 +1,9 @@ +fastapi>=0.109.0 +uvicorn[standard]>=0.27.0 +python-jose[cryptography]>=3.3.0 +bcrypt>=4.1.0 +python-multipart>=0.0.6 +aiofiles>=23.2.0 +websockets>=12.0 +pydantic>=2.5.0 +werkzeug>=3.0.0